Auto-indexing service and GraphQL API for AT Protocol Records
quickslice.slices.network/
atproto
gleam
graphql
1import { createStorageKeys } from './storage/keys';
2import { createStorage, Storage } from './storage/storage';
3import { getOrCreateDPoPKey } from './auth/dpop';
4import { initiateLogin, handleOAuthCallback, logout as doLogout, LoginOptions } from './auth/oauth';
5import { getValidAccessToken, hasValidSession } from './auth/tokens';
6import { graphqlRequest } from './graphql';
7import { generateNamespaceHash } from './utils/crypto';
8
9export interface QuicksliceClientOptions {
10 server: string;
11 clientId: string;
12 redirectUri?: string;
13 scope?: string;
14}
15
16export interface User {
17 did: string;
18}
19
20export interface QueryOptions {
21 signal?: AbortSignal;
22}
23
24export class QuicksliceClient {
25 private server: string;
26 private clientId: string;
27 private redirectUri?: string;
28 private scope?: string;
29 private graphqlUrl: string;
30 private authorizeUrl: string;
31 private tokenUrl: string;
32 private initialized = false;
33 private namespace: string = '';
34 private storage: Storage | null = null;
35
36 constructor(options: QuicksliceClientOptions) {
37 this.server = options.server.replace(/\/$/, ''); // Remove trailing slash
38 this.clientId = options.clientId;
39 this.redirectUri = options.redirectUri;
40 this.scope = options.scope;
41
42 this.graphqlUrl = `${this.server}/graphql`;
43 this.authorizeUrl = `${this.server}/oauth/authorize`;
44 this.tokenUrl = `${this.server}/oauth/token`;
45 }
46
47 /**
48 * Initialize the client - must be called before other methods
49 */
50 async init(): Promise<void> {
51 if (this.initialized) return;
52
53 // Generate namespace from clientId
54 this.namespace = await generateNamespaceHash(this.clientId);
55
56 // Create namespaced storage
57 const keys = createStorageKeys(this.namespace);
58 this.storage = createStorage(keys);
59
60 // Ensure DPoP key exists
61 await getOrCreateDPoPKey(this.namespace);
62
63 this.initialized = true;
64 }
65
66 private getStorage(): Storage {
67 if (!this.storage) {
68 throw new Error('Client not initialized. Call init() first.');
69 }
70 return this.storage;
71 }
72
73 /**
74 * Start OAuth login flow
75 */
76 async loginWithRedirect(options: LoginOptions = {}): Promise<void> {
77 await this.init();
78 await initiateLogin(this.getStorage(), this.authorizeUrl, this.clientId, {
79 ...options,
80 redirectUri: options.redirectUri || this.redirectUri,
81 scope: options.scope || this.scope,
82 });
83 }
84
85 /**
86 * Handle OAuth callback after redirect
87 * Returns true if callback was handled
88 */
89 async handleRedirectCallback(): Promise<boolean> {
90 await this.init();
91 return await handleOAuthCallback(this.getStorage(), this.namespace, this.tokenUrl);
92 }
93
94 /**
95 * Logout and clear all stored data
96 */
97 async logout(options: { reload?: boolean } = {}): Promise<void> {
98 await this.init();
99 await doLogout(this.getStorage(), this.namespace, options);
100 }
101
102 /**
103 * Check if user is authenticated
104 */
105 async isAuthenticated(): Promise<boolean> {
106 await this.init();
107 return hasValidSession(this.getStorage());
108 }
109
110 /**
111 * Get current user's DID (from stored token data)
112 * For richer profile info, use client.query() with your own schema
113 */
114 async getUser(): Promise<User | null> {
115 await this.init();
116 if (!hasValidSession(this.getStorage())) {
117 return null;
118 }
119
120 const did = this.getStorage().get('userDid');
121 if (!did) {
122 return null;
123 }
124
125 return { did };
126 }
127
128 /**
129 * Get access token (auto-refreshes if needed)
130 */
131 async getAccessToken(): Promise<string> {
132 await this.init();
133 return await getValidAccessToken(this.getStorage(), this.namespace, this.tokenUrl);
134 }
135
136 /**
137 * Execute a GraphQL query (authenticated)
138 */
139 async query<T = unknown>(
140 query: string,
141 variables: Record<string, unknown> = {},
142 options: QueryOptions = {}
143 ): Promise<T> {
144 await this.init();
145 return await graphqlRequest<T>(
146 this.getStorage(),
147 this.namespace,
148 this.graphqlUrl,
149 this.tokenUrl,
150 query,
151 variables,
152 true,
153 options.signal
154 );
155 }
156
157 /**
158 * Execute a GraphQL mutation (authenticated)
159 */
160 async mutate<T = unknown>(
161 mutation: string,
162 variables: Record<string, unknown> = {},
163 options: QueryOptions = {}
164 ): Promise<T> {
165 return this.query<T>(mutation, variables, options);
166 }
167
168 /**
169 * Execute a public GraphQL query (no auth)
170 */
171 async publicQuery<T = unknown>(
172 query: string,
173 variables: Record<string, unknown> = {},
174 options: QueryOptions = {}
175 ): Promise<T> {
176 await this.init();
177 return await graphqlRequest<T>(
178 this.getStorage(),
179 this.namespace,
180 this.graphqlUrl,
181 this.tokenUrl,
182 query,
183 variables,
184 false,
185 options.signal
186 );
187 }
188}