@hypercerts-org/sdk-core 0.2.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +328 -0
- package/.turbo/turbo-test.log +118 -0
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/dist/errors.cjs +260 -0
- package/dist/errors.cjs.map +1 -0
- package/dist/errors.d.ts +233 -0
- package/dist/errors.mjs +253 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.cjs +4531 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +3430 -0
- package/dist/index.mjs +4448 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lexicons.cjs +420 -0
- package/dist/lexicons.cjs.map +1 -0
- package/dist/lexicons.d.ts +227 -0
- package/dist/lexicons.mjs +410 -0
- package/dist/lexicons.mjs.map +1 -0
- package/dist/storage.cjs +270 -0
- package/dist/storage.cjs.map +1 -0
- package/dist/storage.d.ts +474 -0
- package/dist/storage.mjs +267 -0
- package/dist/storage.mjs.map +1 -0
- package/dist/testing.cjs +415 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.ts +928 -0
- package/dist/testing.mjs +410 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/types.cjs +220 -0
- package/dist/types.cjs.map +1 -0
- package/dist/types.d.ts +2118 -0
- package/dist/types.mjs +212 -0
- package/dist/types.mjs.map +1 -0
- package/eslint.config.mjs +22 -0
- package/package.json +90 -0
- package/rollup.config.js +75 -0
- package/src/auth/OAuthClient.ts +497 -0
- package/src/core/SDK.ts +410 -0
- package/src/core/config.ts +243 -0
- package/src/core/errors.ts +257 -0
- package/src/core/interfaces.ts +324 -0
- package/src/core/types.ts +281 -0
- package/src/errors.ts +57 -0
- package/src/index.ts +107 -0
- package/src/lexicons.ts +64 -0
- package/src/repository/BlobOperationsImpl.ts +199 -0
- package/src/repository/CollaboratorOperationsImpl.ts +288 -0
- package/src/repository/HypercertOperationsImpl.ts +1146 -0
- package/src/repository/LexiconRegistry.ts +332 -0
- package/src/repository/OrganizationOperationsImpl.ts +234 -0
- package/src/repository/ProfileOperationsImpl.ts +281 -0
- package/src/repository/RecordOperationsImpl.ts +340 -0
- package/src/repository/Repository.ts +482 -0
- package/src/repository/interfaces.ts +868 -0
- package/src/repository/types.ts +111 -0
- package/src/services/hypercerts/types.ts +87 -0
- package/src/storage/InMemorySessionStore.ts +127 -0
- package/src/storage/InMemoryStateStore.ts +146 -0
- package/src/storage.ts +63 -0
- package/src/testing/index.ts +67 -0
- package/src/testing/mocks.ts +142 -0
- package/src/testing/stores.ts +285 -0
- package/src/testing.ts +64 -0
- package/src/types.ts +86 -0
- package/tests/auth/OAuthClient.test.ts +164 -0
- package/tests/core/SDK.test.ts +176 -0
- package/tests/core/errors.test.ts +81 -0
- package/tests/repository/BlobOperationsImpl.test.ts +154 -0
- package/tests/repository/CollaboratorOperationsImpl.test.ts +323 -0
- package/tests/repository/HypercertOperationsImpl.test.ts +652 -0
- package/tests/repository/LexiconRegistry.test.ts +192 -0
- package/tests/repository/OrganizationOperationsImpl.test.ts +242 -0
- package/tests/repository/ProfileOperationsImpl.test.ts +254 -0
- package/tests/repository/RecordOperationsImpl.test.ts +375 -0
- package/tests/repository/Repository.test.ts +149 -0
- package/tests/utils/fixtures.ts +117 -0
- package/tests/utils/mocks.ts +109 -0
- package/tests/utils/repository-fixtures.ts +78 -0
- package/tsconfig.json +11 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +30 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import { NodeOAuthClient, JoseKey, type NodeSavedSession } from "@atproto/oauth-client-node";
|
|
2
|
+
import type { SessionStore, StateStore, LoggerInterface } from "../core/interfaces.js";
|
|
3
|
+
import type { ATProtoSDKConfig } from "../core/config.js";
|
|
4
|
+
import { AuthenticationError, NetworkError } from "../core/errors.js";
|
|
5
|
+
import { InMemorySessionStore } from "../storage/InMemorySessionStore.js";
|
|
6
|
+
import { InMemoryStateStore } from "../storage/InMemoryStateStore.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Options for the OAuth authorization flow.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
interface AuthorizeOptions {
|
|
14
|
+
/**
|
|
15
|
+
* OAuth scope string to request specific permissions.
|
|
16
|
+
* Overrides the default scope from the SDK configuration.
|
|
17
|
+
*/
|
|
18
|
+
scope?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* OAuth 2.0 client for AT Protocol authentication with DPoP support.
|
|
23
|
+
*
|
|
24
|
+
* This class wraps the `@atproto/oauth-client-node` library to provide
|
|
25
|
+
* OAuth 2.0 authentication with the following features:
|
|
26
|
+
*
|
|
27
|
+
* - **DPoP (Demonstrating Proof of Possession)**: Binds tokens to cryptographic keys
|
|
28
|
+
* to prevent token theft and replay attacks
|
|
29
|
+
* - **PKCE (Proof Key for Code Exchange)**: Protects against authorization code interception
|
|
30
|
+
* - **Automatic Token Refresh**: Transparently refreshes expired access tokens
|
|
31
|
+
* - **Session Persistence**: Stores sessions in configurable storage backends
|
|
32
|
+
*
|
|
33
|
+
* @remarks
|
|
34
|
+
* This class is typically used internally by {@link ATProtoSDK}. Direct usage
|
|
35
|
+
* is only needed for advanced scenarios.
|
|
36
|
+
*
|
|
37
|
+
* The client uses lazy initialization - the underlying `NodeOAuthClient` is
|
|
38
|
+
* created asynchronously on first use. This allows the constructor to return
|
|
39
|
+
* synchronously while deferring async key parsing.
|
|
40
|
+
*
|
|
41
|
+
* @example Direct usage (advanced)
|
|
42
|
+
* ```typescript
|
|
43
|
+
* import { OAuthClient } from "@hypercerts-org/sdk";
|
|
44
|
+
*
|
|
45
|
+
* const client = new OAuthClient({
|
|
46
|
+
* oauth: {
|
|
47
|
+
* clientId: "https://my-app.com/client-metadata.json",
|
|
48
|
+
* redirectUri: "https://my-app.com/callback",
|
|
49
|
+
* scope: "atproto transition:generic",
|
|
50
|
+
* jwksUri: "https://my-app.com/.well-known/jwks.json",
|
|
51
|
+
* jwkPrivate: process.env.JWK_PRIVATE_KEY!,
|
|
52
|
+
* },
|
|
53
|
+
* servers: { pds: "https://bsky.social" },
|
|
54
|
+
* });
|
|
55
|
+
*
|
|
56
|
+
* // Start authorization
|
|
57
|
+
* const authUrl = await client.authorize("user.bsky.social");
|
|
58
|
+
*
|
|
59
|
+
* // Handle callback
|
|
60
|
+
* const session = await client.callback(new URLSearchParams(callbackUrl.search));
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* @see {@link ATProtoSDK} for the recommended high-level API
|
|
64
|
+
* @see https://atproto.com/specs/oauth for AT Protocol OAuth specification
|
|
65
|
+
*/
|
|
66
|
+
export class OAuthClient {
|
|
67
|
+
/** The underlying NodeOAuthClient instance (lazily initialized) */
|
|
68
|
+
private client: NodeOAuthClient | null = null;
|
|
69
|
+
|
|
70
|
+
/** Promise that resolves to the initialized client */
|
|
71
|
+
private clientPromise: Promise<NodeOAuthClient>;
|
|
72
|
+
|
|
73
|
+
/** SDK configuration */
|
|
74
|
+
private config: ATProtoSDKConfig;
|
|
75
|
+
|
|
76
|
+
/** Optional logger for debugging */
|
|
77
|
+
private logger?: LoggerInterface;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Creates a new OAuth client.
|
|
81
|
+
*
|
|
82
|
+
* @param config - SDK configuration including OAuth credentials and server URLs
|
|
83
|
+
* @throws {@link AuthenticationError} if the JWK private key is not valid JSON
|
|
84
|
+
*
|
|
85
|
+
* @remarks
|
|
86
|
+
* The constructor validates the JWK format synchronously but defers
|
|
87
|
+
* the actual client initialization to the first API call.
|
|
88
|
+
*/
|
|
89
|
+
constructor(config: ATProtoSDKConfig) {
|
|
90
|
+
this.config = config;
|
|
91
|
+
this.logger = config.logger;
|
|
92
|
+
|
|
93
|
+
// Validate JWK format synchronously (before async initialization)
|
|
94
|
+
try {
|
|
95
|
+
JSON.parse(config.oauth.jwkPrivate);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
throw new AuthenticationError("Failed to parse JWK private key. Ensure it is valid JSON.", error);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Initialize client lazily (async initialization)
|
|
101
|
+
this.clientPromise = this.initializeClient();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Initializes the NodeOAuthClient asynchronously.
|
|
106
|
+
*
|
|
107
|
+
* This method is called lazily on first use. It:
|
|
108
|
+
* 1. Parses the JWK private key(s)
|
|
109
|
+
* 2. Builds OAuth client metadata
|
|
110
|
+
* 3. Creates the underlying NodeOAuthClient
|
|
111
|
+
*
|
|
112
|
+
* @returns Promise resolving to the initialized client
|
|
113
|
+
* @internal
|
|
114
|
+
*/
|
|
115
|
+
private async initializeClient(): Promise<NodeOAuthClient> {
|
|
116
|
+
if (this.client) {
|
|
117
|
+
return this.client;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Parse JWK private key (already validated in constructor)
|
|
121
|
+
const privateJWK = JSON.parse(this.config.oauth.jwkPrivate) as {
|
|
122
|
+
keys: Array<{ kid: string; [key: string]: unknown }>;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Build client metadata
|
|
126
|
+
const clientMetadata = this.buildClientMetadata();
|
|
127
|
+
|
|
128
|
+
// Convert JWK keys to JoseKey instances (await here)
|
|
129
|
+
const keyset = await Promise.all(
|
|
130
|
+
privateJWK.keys.map((key) =>
|
|
131
|
+
JoseKey.fromImportable(key as unknown as Parameters<typeof JoseKey.fromImportable>[0], key.kid),
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Create fetch with timeout
|
|
136
|
+
const fetchWithTimeout = this.createFetchWithTimeout(this.config.timeouts?.pdsMetadata ?? 30000);
|
|
137
|
+
|
|
138
|
+
// Use provided stores or fall back to in-memory implementations
|
|
139
|
+
const stateStore = this.config.storage?.stateStore ?? new InMemoryStateStore();
|
|
140
|
+
const sessionStore = this.config.storage?.sessionStore ?? new InMemorySessionStore();
|
|
141
|
+
|
|
142
|
+
this.client = new NodeOAuthClient({
|
|
143
|
+
clientMetadata,
|
|
144
|
+
keyset,
|
|
145
|
+
stateStore: this.createStateStoreAdapter(stateStore),
|
|
146
|
+
sessionStore: this.createSessionStoreAdapter(sessionStore),
|
|
147
|
+
handleResolver: this.config.servers?.pds,
|
|
148
|
+
fetch: this.config.fetch ?? fetchWithTimeout,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return this.client;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Gets the OAuth client instance, initializing if needed.
|
|
156
|
+
*
|
|
157
|
+
* @returns Promise resolving to the initialized client
|
|
158
|
+
* @internal
|
|
159
|
+
*/
|
|
160
|
+
private async getClient(): Promise<NodeOAuthClient> {
|
|
161
|
+
return this.clientPromise;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Builds OAuth client metadata from configuration.
|
|
166
|
+
*
|
|
167
|
+
* The metadata describes your application to the authorization server
|
|
168
|
+
* and must match what's published at your `clientId` URL.
|
|
169
|
+
*
|
|
170
|
+
* @returns OAuth client metadata object
|
|
171
|
+
* @internal
|
|
172
|
+
*
|
|
173
|
+
* @remarks
|
|
174
|
+
* Key metadata fields:
|
|
175
|
+
* - `client_id`: URL to your client metadata JSON
|
|
176
|
+
* - `redirect_uris`: Where to redirect after auth (must match config)
|
|
177
|
+
* - `dpop_bound_access_tokens`: Always true for AT Protocol
|
|
178
|
+
* - `token_endpoint_auth_method`: Uses private_key_jwt for security
|
|
179
|
+
*/
|
|
180
|
+
private buildClientMetadata() {
|
|
181
|
+
const clientIdUrl = new URL(this.config.oauth.clientId);
|
|
182
|
+
return {
|
|
183
|
+
client_id: this.config.oauth.clientId,
|
|
184
|
+
client_name: "ATProto SDK Client",
|
|
185
|
+
client_uri: clientIdUrl.origin,
|
|
186
|
+
redirect_uris: [this.config.oauth.redirectUri] as [string, ...string[]],
|
|
187
|
+
scope: this.config.oauth.scope,
|
|
188
|
+
grant_types: ["authorization_code", "refresh_token"] as ["authorization_code", "refresh_token"],
|
|
189
|
+
response_types: ["code"] as ["code"],
|
|
190
|
+
application_type: "web" as const,
|
|
191
|
+
token_endpoint_auth_method: "private_key_jwt" as const,
|
|
192
|
+
token_endpoint_auth_signing_alg: "ES256",
|
|
193
|
+
dpop_bound_access_tokens: true,
|
|
194
|
+
jwks_uri: this.config.oauth.jwksUri,
|
|
195
|
+
} as const;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Creates a fetch handler with timeout support.
|
|
200
|
+
*
|
|
201
|
+
* @param timeoutMs - Request timeout in milliseconds
|
|
202
|
+
* @returns A fetch function that aborts after the timeout
|
|
203
|
+
* @internal
|
|
204
|
+
*/
|
|
205
|
+
private createFetchWithTimeout(timeoutMs: number): typeof fetch {
|
|
206
|
+
return async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
207
|
+
const controller = new AbortController();
|
|
208
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const response = await fetch(input, {
|
|
212
|
+
...init,
|
|
213
|
+
signal: controller.signal,
|
|
214
|
+
});
|
|
215
|
+
clearTimeout(timeoutId);
|
|
216
|
+
return response;
|
|
217
|
+
} catch (error) {
|
|
218
|
+
clearTimeout(timeoutId);
|
|
219
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
220
|
+
throw new NetworkError(`Request timeout after ${timeoutMs}ms`, error);
|
|
221
|
+
}
|
|
222
|
+
throw new NetworkError("Network request failed", error);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Creates a state store adapter compatible with NodeOAuthClient.
|
|
229
|
+
*
|
|
230
|
+
* @param store - The StateStore implementation to adapt
|
|
231
|
+
* @returns An adapter compatible with NodeOAuthClient
|
|
232
|
+
* @internal
|
|
233
|
+
*/
|
|
234
|
+
private createStateStoreAdapter(store: StateStore): import("@atproto/oauth-client-node").NodeSavedStateStore {
|
|
235
|
+
return {
|
|
236
|
+
get: (key: string) => store.get(key),
|
|
237
|
+
set: (key: string, value: import("@atproto/oauth-client-node").NodeSavedState) => store.set(key, value),
|
|
238
|
+
del: (key: string) => store.del(key),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Creates a session store adapter compatible with NodeOAuthClient.
|
|
244
|
+
*
|
|
245
|
+
* @param store - The SessionStore implementation to adapt
|
|
246
|
+
* @returns An adapter compatible with NodeOAuthClient
|
|
247
|
+
* @internal
|
|
248
|
+
*/
|
|
249
|
+
private createSessionStoreAdapter(store: SessionStore): import("@atproto/oauth-client-node").NodeSavedSessionStore {
|
|
250
|
+
return {
|
|
251
|
+
get: (did: string) => store.get(did),
|
|
252
|
+
set: (did: string, session: NodeSavedSession) => store.set(did, session),
|
|
253
|
+
del: (did: string) => store.del(did),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Initiates the OAuth authorization flow.
|
|
259
|
+
*
|
|
260
|
+
* This method resolves the user's identity from their identifier,
|
|
261
|
+
* generates PKCE codes, creates OAuth state, and returns an
|
|
262
|
+
* authorization URL to redirect the user to.
|
|
263
|
+
*
|
|
264
|
+
* @param identifier - The user's ATProto identifier. Accepts:
|
|
265
|
+
* - Handle (e.g., `"alice.bsky.social"`)
|
|
266
|
+
* - DID (e.g., `"did:plc:abc123..."`)
|
|
267
|
+
* - PDS URL (e.g., `"https://bsky.social"`)
|
|
268
|
+
* @param options - Optional authorization settings
|
|
269
|
+
* @returns A Promise resolving to the authorization URL
|
|
270
|
+
* @throws {@link AuthenticationError} if authorization setup fails
|
|
271
|
+
* @throws {@link NetworkError} if identity resolution fails
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* ```typescript
|
|
275
|
+
* // Get authorization URL
|
|
276
|
+
* const authUrl = await client.authorize("user.bsky.social");
|
|
277
|
+
*
|
|
278
|
+
* // Redirect user (in a web app)
|
|
279
|
+
* window.location.href = authUrl;
|
|
280
|
+
*
|
|
281
|
+
* // Or return to client (in an API)
|
|
282
|
+
* res.json({ authUrl });
|
|
283
|
+
* ```
|
|
284
|
+
*/
|
|
285
|
+
async authorize(identifier: string, options?: AuthorizeOptions): Promise<string> {
|
|
286
|
+
try {
|
|
287
|
+
this.logger?.debug("Initiating OAuth authorization", { identifier });
|
|
288
|
+
|
|
289
|
+
const client = await this.getClient();
|
|
290
|
+
const scope = options?.scope ?? this.config.oauth.scope;
|
|
291
|
+
const authUrl = await client.authorize(identifier, { scope });
|
|
292
|
+
|
|
293
|
+
this.logger?.debug("Authorization URL generated", { identifier });
|
|
294
|
+
// Convert URL to string if needed
|
|
295
|
+
return typeof authUrl === "string" ? authUrl : authUrl.toString();
|
|
296
|
+
} catch (error) {
|
|
297
|
+
this.logger?.error("Authorization failed", { identifier, error });
|
|
298
|
+
if (error instanceof NetworkError || error instanceof AuthenticationError) {
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
301
|
+
throw new AuthenticationError(
|
|
302
|
+
`Failed to initiate authorization: ${error instanceof Error ? error.message : String(error)}`,
|
|
303
|
+
error,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Handles the OAuth callback and exchanges the authorization code for tokens.
|
|
310
|
+
*
|
|
311
|
+
* Call this method when the user is redirected back to your application.
|
|
312
|
+
* It validates the state, exchanges the code for tokens, and creates
|
|
313
|
+
* a persistent session.
|
|
314
|
+
*
|
|
315
|
+
* @param params - URL search parameters from the callback. Expected parameters:
|
|
316
|
+
* - `code`: The authorization code
|
|
317
|
+
* - `state`: The state parameter (for CSRF protection)
|
|
318
|
+
* - `iss`: The issuer (authorization server URL)
|
|
319
|
+
* @returns A Promise resolving to the authenticated OAuth session
|
|
320
|
+
* @throws {@link AuthenticationError} if:
|
|
321
|
+
* - The callback contains an OAuth error
|
|
322
|
+
* - The state is invalid or expired
|
|
323
|
+
* - The code exchange fails
|
|
324
|
+
* - Session persistence fails
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* ```typescript
|
|
328
|
+
* // In your callback route handler
|
|
329
|
+
* app.get("/callback", async (req, res) => {
|
|
330
|
+
* const params = new URLSearchParams(req.url.split("?")[1]);
|
|
331
|
+
*
|
|
332
|
+
* try {
|
|
333
|
+
* const session = await client.callback(params);
|
|
334
|
+
* // Store DID for session restoration
|
|
335
|
+
* req.session.userDid = session.sub;
|
|
336
|
+
* res.redirect("/dashboard");
|
|
337
|
+
* } catch (error) {
|
|
338
|
+
* res.redirect("/login?error=auth_failed");
|
|
339
|
+
* }
|
|
340
|
+
* });
|
|
341
|
+
* ```
|
|
342
|
+
*
|
|
343
|
+
* @remarks
|
|
344
|
+
* After successful token exchange, this method verifies that the session
|
|
345
|
+
* was properly persisted by attempting to restore it. This ensures the
|
|
346
|
+
* storage backend is working correctly.
|
|
347
|
+
*/
|
|
348
|
+
async callback(params: URLSearchParams): Promise<import("@atproto/oauth-client").OAuthSession> {
|
|
349
|
+
try {
|
|
350
|
+
this.logger?.debug("Processing OAuth callback");
|
|
351
|
+
|
|
352
|
+
// Check for OAuth errors
|
|
353
|
+
const error = params.get("error");
|
|
354
|
+
if (error) {
|
|
355
|
+
const errorDescription = params.get("error_description");
|
|
356
|
+
throw new AuthenticationError(errorDescription || error);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const client = await this.getClient();
|
|
360
|
+
const result = await client.callback(params);
|
|
361
|
+
const session = result.session;
|
|
362
|
+
const did = session.sub;
|
|
363
|
+
|
|
364
|
+
this.logger?.info("OAuth callback successful", { did });
|
|
365
|
+
|
|
366
|
+
// Verify session can be restored (validates persistence)
|
|
367
|
+
try {
|
|
368
|
+
const restored = await client.restore(did);
|
|
369
|
+
if (!restored) {
|
|
370
|
+
throw new AuthenticationError("OAuth session was not persisted");
|
|
371
|
+
}
|
|
372
|
+
this.logger?.debug("Session verified and restorable", { did });
|
|
373
|
+
} catch (restoreError) {
|
|
374
|
+
this.logger?.error("Failed to verify persisted session", {
|
|
375
|
+
did,
|
|
376
|
+
error: restoreError,
|
|
377
|
+
});
|
|
378
|
+
throw new AuthenticationError("Failed to persist OAuth session", restoreError);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return session;
|
|
382
|
+
} catch (error) {
|
|
383
|
+
this.logger?.error("OAuth callback failed", { error });
|
|
384
|
+
if (error instanceof AuthenticationError) {
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
throw new AuthenticationError(
|
|
388
|
+
`OAuth callback failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
389
|
+
error,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Restores an OAuth session by DID.
|
|
396
|
+
*
|
|
397
|
+
* Use this method to restore a previously authenticated session.
|
|
398
|
+
* The method automatically refreshes expired access tokens using
|
|
399
|
+
* the stored refresh token.
|
|
400
|
+
*
|
|
401
|
+
* @param did - The user's Decentralized Identifier (e.g., `"did:plc:abc123..."`)
|
|
402
|
+
* @returns A Promise resolving to the session, or `null` if not found
|
|
403
|
+
* @throws {@link AuthenticationError} if session restoration fails (not for missing sessions)
|
|
404
|
+
* @throws {@link NetworkError} if token refresh requires network and fails
|
|
405
|
+
*
|
|
406
|
+
* @example
|
|
407
|
+
* ```typescript
|
|
408
|
+
* // On application startup or request
|
|
409
|
+
* const userDid = req.session.userDid;
|
|
410
|
+
* if (userDid) {
|
|
411
|
+
* const session = await client.restore(userDid);
|
|
412
|
+
* if (session) {
|
|
413
|
+
* // Session restored, user is authenticated
|
|
414
|
+
* req.atprotoSession = session;
|
|
415
|
+
* } else {
|
|
416
|
+
* // No session found, user needs to log in
|
|
417
|
+
* delete req.session.userDid;
|
|
418
|
+
* }
|
|
419
|
+
* }
|
|
420
|
+
* ```
|
|
421
|
+
*
|
|
422
|
+
* @remarks
|
|
423
|
+
* Token refresh is handled automatically by the underlying OAuth client.
|
|
424
|
+
* If the refresh token has expired or been revoked, this method will
|
|
425
|
+
* throw an {@link AuthenticationError}.
|
|
426
|
+
*/
|
|
427
|
+
async restore(did: string): Promise<import("@atproto/oauth-client").OAuthSession | null> {
|
|
428
|
+
try {
|
|
429
|
+
this.logger?.debug("Restoring session", { did });
|
|
430
|
+
|
|
431
|
+
const client = await this.getClient();
|
|
432
|
+
const session = await client.restore(did);
|
|
433
|
+
|
|
434
|
+
if (session) {
|
|
435
|
+
this.logger?.debug("Session restored", { did });
|
|
436
|
+
} else {
|
|
437
|
+
this.logger?.debug("No session found", { did });
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return session;
|
|
441
|
+
} catch (error) {
|
|
442
|
+
this.logger?.error("Failed to restore session", { did, error });
|
|
443
|
+
if (error instanceof NetworkError) {
|
|
444
|
+
throw error;
|
|
445
|
+
}
|
|
446
|
+
throw new AuthenticationError(
|
|
447
|
+
`Failed to restore session: ${error instanceof Error ? error.message : String(error)}`,
|
|
448
|
+
error,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Revokes an OAuth session.
|
|
455
|
+
*
|
|
456
|
+
* This method invalidates the session's tokens both locally and
|
|
457
|
+
* (if supported) on the authorization server. After revocation,
|
|
458
|
+
* the session cannot be restored.
|
|
459
|
+
*
|
|
460
|
+
* @param did - The user's DID to revoke
|
|
461
|
+
* @throws {@link AuthenticationError} if revocation fails
|
|
462
|
+
*
|
|
463
|
+
* @example
|
|
464
|
+
* ```typescript
|
|
465
|
+
* // Log out endpoint
|
|
466
|
+
* app.post("/logout", async (req, res) => {
|
|
467
|
+
* const userDid = req.session.userDid;
|
|
468
|
+
* if (userDid) {
|
|
469
|
+
* await client.revoke(userDid);
|
|
470
|
+
* delete req.session.userDid;
|
|
471
|
+
* }
|
|
472
|
+
* res.redirect("/");
|
|
473
|
+
* });
|
|
474
|
+
* ```
|
|
475
|
+
*
|
|
476
|
+
* @remarks
|
|
477
|
+
* Even if revocation fails on the server, the local session is
|
|
478
|
+
* removed. The error is thrown to inform you that remote revocation
|
|
479
|
+
* may not have succeeded.
|
|
480
|
+
*/
|
|
481
|
+
async revoke(did: string): Promise<void> {
|
|
482
|
+
try {
|
|
483
|
+
this.logger?.debug("Revoking session", { did });
|
|
484
|
+
|
|
485
|
+
const client = await this.getClient();
|
|
486
|
+
await client.revoke(did);
|
|
487
|
+
|
|
488
|
+
this.logger?.info("Session revoked", { did });
|
|
489
|
+
} catch (error) {
|
|
490
|
+
this.logger?.error("Failed to revoke session", { did, error });
|
|
491
|
+
throw new AuthenticationError(
|
|
492
|
+
`Failed to revoke session: ${error instanceof Error ? error.message : String(error)}`,
|
|
493
|
+
error,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|