Auto-indexing service and GraphQL API for AT Protocol Records quickslice.slices.network/
atproto gleam graphql
at main 188 lines 4.8 kB view raw
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}