@hypercerts-org/sdk-core 0.4.0-beta.0 → 0.6.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.
Files changed (62) hide show
  1. package/README.md +459 -79
  2. package/dist/index.cjs +128 -47
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.ts +28 -9
  5. package/dist/index.mjs +128 -47
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/types.cjs +3 -2
  8. package/dist/types.cjs.map +1 -1
  9. package/dist/types.d.ts +28 -9
  10. package/dist/types.mjs +3 -2
  11. package/dist/types.mjs.map +1 -1
  12. package/package.json +9 -5
  13. package/.turbo/turbo-build.log +0 -328
  14. package/.turbo/turbo-test.log +0 -118
  15. package/CHANGELOG.md +0 -22
  16. package/eslint.config.mjs +0 -22
  17. package/rollup.config.js +0 -75
  18. package/src/auth/OAuthClient.ts +0 -497
  19. package/src/core/SDK.ts +0 -410
  20. package/src/core/config.ts +0 -243
  21. package/src/core/errors.ts +0 -257
  22. package/src/core/interfaces.ts +0 -324
  23. package/src/core/types.ts +0 -281
  24. package/src/errors.ts +0 -57
  25. package/src/index.ts +0 -107
  26. package/src/lexicons.ts +0 -64
  27. package/src/repository/BlobOperationsImpl.ts +0 -199
  28. package/src/repository/CollaboratorOperationsImpl.ts +0 -396
  29. package/src/repository/HypercertOperationsImpl.ts +0 -1146
  30. package/src/repository/LexiconRegistry.ts +0 -332
  31. package/src/repository/OrganizationOperationsImpl.ts +0 -234
  32. package/src/repository/ProfileOperationsImpl.ts +0 -281
  33. package/src/repository/RecordOperationsImpl.ts +0 -340
  34. package/src/repository/Repository.ts +0 -482
  35. package/src/repository/interfaces.ts +0 -897
  36. package/src/repository/types.ts +0 -111
  37. package/src/services/hypercerts/types.ts +0 -87
  38. package/src/storage/InMemorySessionStore.ts +0 -127
  39. package/src/storage/InMemoryStateStore.ts +0 -146
  40. package/src/storage.ts +0 -63
  41. package/src/testing/index.ts +0 -67
  42. package/src/testing/mocks.ts +0 -142
  43. package/src/testing/stores.ts +0 -285
  44. package/src/testing.ts +0 -64
  45. package/src/types.ts +0 -86
  46. package/tests/auth/OAuthClient.test.ts +0 -164
  47. package/tests/core/SDK.test.ts +0 -176
  48. package/tests/core/errors.test.ts +0 -81
  49. package/tests/repository/BlobOperationsImpl.test.ts +0 -154
  50. package/tests/repository/CollaboratorOperationsImpl.test.ts +0 -438
  51. package/tests/repository/HypercertOperationsImpl.test.ts +0 -652
  52. package/tests/repository/LexiconRegistry.test.ts +0 -192
  53. package/tests/repository/OrganizationOperationsImpl.test.ts +0 -242
  54. package/tests/repository/ProfileOperationsImpl.test.ts +0 -254
  55. package/tests/repository/RecordOperationsImpl.test.ts +0 -375
  56. package/tests/repository/Repository.test.ts +0 -149
  57. package/tests/utils/fixtures.ts +0 -117
  58. package/tests/utils/mocks.ts +0 -109
  59. package/tests/utils/repository-fixtures.ts +0 -78
  60. package/tsconfig.json +0 -11
  61. package/tsconfig.tsbuildinfo +0 -1
  62. package/vitest.config.ts +0 -30
@@ -1,497 +0,0 @@
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
- }