@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.
Files changed (83) hide show
  1. package/.turbo/turbo-build.log +328 -0
  2. package/.turbo/turbo-test.log +118 -0
  3. package/CHANGELOG.md +16 -0
  4. package/LICENSE +21 -0
  5. package/README.md +100 -0
  6. package/dist/errors.cjs +260 -0
  7. package/dist/errors.cjs.map +1 -0
  8. package/dist/errors.d.ts +233 -0
  9. package/dist/errors.mjs +253 -0
  10. package/dist/errors.mjs.map +1 -0
  11. package/dist/index.cjs +4531 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.d.ts +3430 -0
  14. package/dist/index.mjs +4448 -0
  15. package/dist/index.mjs.map +1 -0
  16. package/dist/lexicons.cjs +420 -0
  17. package/dist/lexicons.cjs.map +1 -0
  18. package/dist/lexicons.d.ts +227 -0
  19. package/dist/lexicons.mjs +410 -0
  20. package/dist/lexicons.mjs.map +1 -0
  21. package/dist/storage.cjs +270 -0
  22. package/dist/storage.cjs.map +1 -0
  23. package/dist/storage.d.ts +474 -0
  24. package/dist/storage.mjs +267 -0
  25. package/dist/storage.mjs.map +1 -0
  26. package/dist/testing.cjs +415 -0
  27. package/dist/testing.cjs.map +1 -0
  28. package/dist/testing.d.ts +928 -0
  29. package/dist/testing.mjs +410 -0
  30. package/dist/testing.mjs.map +1 -0
  31. package/dist/types.cjs +220 -0
  32. package/dist/types.cjs.map +1 -0
  33. package/dist/types.d.ts +2118 -0
  34. package/dist/types.mjs +212 -0
  35. package/dist/types.mjs.map +1 -0
  36. package/eslint.config.mjs +22 -0
  37. package/package.json +90 -0
  38. package/rollup.config.js +75 -0
  39. package/src/auth/OAuthClient.ts +497 -0
  40. package/src/core/SDK.ts +410 -0
  41. package/src/core/config.ts +243 -0
  42. package/src/core/errors.ts +257 -0
  43. package/src/core/interfaces.ts +324 -0
  44. package/src/core/types.ts +281 -0
  45. package/src/errors.ts +57 -0
  46. package/src/index.ts +107 -0
  47. package/src/lexicons.ts +64 -0
  48. package/src/repository/BlobOperationsImpl.ts +199 -0
  49. package/src/repository/CollaboratorOperationsImpl.ts +288 -0
  50. package/src/repository/HypercertOperationsImpl.ts +1146 -0
  51. package/src/repository/LexiconRegistry.ts +332 -0
  52. package/src/repository/OrganizationOperationsImpl.ts +234 -0
  53. package/src/repository/ProfileOperationsImpl.ts +281 -0
  54. package/src/repository/RecordOperationsImpl.ts +340 -0
  55. package/src/repository/Repository.ts +482 -0
  56. package/src/repository/interfaces.ts +868 -0
  57. package/src/repository/types.ts +111 -0
  58. package/src/services/hypercerts/types.ts +87 -0
  59. package/src/storage/InMemorySessionStore.ts +127 -0
  60. package/src/storage/InMemoryStateStore.ts +146 -0
  61. package/src/storage.ts +63 -0
  62. package/src/testing/index.ts +67 -0
  63. package/src/testing/mocks.ts +142 -0
  64. package/src/testing/stores.ts +285 -0
  65. package/src/testing.ts +64 -0
  66. package/src/types.ts +86 -0
  67. package/tests/auth/OAuthClient.test.ts +164 -0
  68. package/tests/core/SDK.test.ts +176 -0
  69. package/tests/core/errors.test.ts +81 -0
  70. package/tests/repository/BlobOperationsImpl.test.ts +154 -0
  71. package/tests/repository/CollaboratorOperationsImpl.test.ts +323 -0
  72. package/tests/repository/HypercertOperationsImpl.test.ts +652 -0
  73. package/tests/repository/LexiconRegistry.test.ts +192 -0
  74. package/tests/repository/OrganizationOperationsImpl.test.ts +242 -0
  75. package/tests/repository/ProfileOperationsImpl.test.ts +254 -0
  76. package/tests/repository/RecordOperationsImpl.test.ts +375 -0
  77. package/tests/repository/Repository.test.ts +149 -0
  78. package/tests/utils/fixtures.ts +117 -0
  79. package/tests/utils/mocks.ts +109 -0
  80. package/tests/utils/repository-fixtures.ts +78 -0
  81. package/tsconfig.json +11 -0
  82. package/tsconfig.tsbuildinfo +1 -0
  83. package/vitest.config.ts +30 -0
@@ -0,0 +1,928 @@
1
+ import { OAuthSession, NodeSavedSession, NodeSavedState } from '@atproto/oauth-client-node';
2
+ import { z } from 'zod';
3
+
4
+ /**
5
+ * OAuth session with DPoP (Demonstrating Proof of Possession) support.
6
+ *
7
+ * This type represents an authenticated user session. It wraps the
8
+ * `@atproto/oauth-client-node` OAuthSession and contains:
9
+ * - Access token for API requests
10
+ * - Refresh token for obtaining new access tokens
11
+ * - DPoP key pair for proof-of-possession
12
+ * - User's DID and other identity information
13
+ *
14
+ * @remarks
15
+ * Sessions are managed by the SDK and automatically refresh when tokens expire.
16
+ * Store the user's DID to restore sessions later with {@link ATProtoSDK.restoreSession}.
17
+ *
18
+ * Key properties from OAuthSession:
19
+ * - `did` or `sub`: The user's DID
20
+ * - `handle`: The user's handle (e.g., "user.bsky.social")
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * const session = await sdk.callback(params);
25
+ *
26
+ * // Access user identity
27
+ * console.log(`Logged in as: ${session.did}`);
28
+ *
29
+ * // Use session for repository operations
30
+ * const repo = sdk.repository(session);
31
+ * ```
32
+ *
33
+ * @see https://atproto.com/specs/oauth for OAuth specification
34
+ */
35
+ type Session = OAuthSession;
36
+
37
+ /**
38
+ * Storage interface for persisting OAuth sessions.
39
+ *
40
+ * Implement this interface to provide persistent storage for user sessions.
41
+ * Sessions contain sensitive data including access tokens, refresh tokens,
42
+ * and DPoP key pairs.
43
+ *
44
+ * The SDK provides {@link InMemorySessionStore} for development/testing,
45
+ * but **production applications should implement persistent storage**
46
+ * (e.g., Redis, PostgreSQL, MongoDB).
47
+ *
48
+ * @remarks
49
+ * - Sessions are keyed by the user's DID (Decentralized Identifier)
50
+ * - The `NodeSavedSession` type comes from `@atproto/oauth-client-node`
51
+ * - Sessions may be large (~2-4KB) due to embedded cryptographic keys
52
+ * - Consider encrypting sessions at rest for additional security
53
+ *
54
+ * @example Redis implementation
55
+ * ```typescript
56
+ * import { Redis } from "ioredis";
57
+ * import type { SessionStore } from "@hypercerts-org/sdk";
58
+ * import type { NodeSavedSession } from "@atproto/oauth-client-node";
59
+ *
60
+ * class RedisSessionStore implements SessionStore {
61
+ * constructor(private redis: Redis, private prefix = "session:") {}
62
+ *
63
+ * async get(did: string): Promise<NodeSavedSession | undefined> {
64
+ * const data = await this.redis.get(this.prefix + did);
65
+ * return data ? JSON.parse(data) : undefined;
66
+ * }
67
+ *
68
+ * async set(did: string, session: NodeSavedSession): Promise<void> {
69
+ * // Set with 30-day expiry (sessions can be refreshed)
70
+ * await this.redis.setex(
71
+ * this.prefix + did,
72
+ * 30 * 24 * 60 * 60,
73
+ * JSON.stringify(session)
74
+ * );
75
+ * }
76
+ *
77
+ * async del(did: string): Promise<void> {
78
+ * await this.redis.del(this.prefix + did);
79
+ * }
80
+ * }
81
+ * ```
82
+ *
83
+ * @see {@link InMemorySessionStore} for the default in-memory implementation
84
+ */
85
+ interface SessionStore {
86
+ /**
87
+ * Retrieves a session by DID.
88
+ *
89
+ * @param did - The user's Decentralized Identifier (e.g., "did:plc:abc123...")
90
+ * @returns The stored session, or `undefined` if not found
91
+ */
92
+ get(did: string): Promise<NodeSavedSession | undefined>;
93
+ /**
94
+ * Stores or updates a session.
95
+ *
96
+ * This is called after successful authentication and whenever tokens are refreshed.
97
+ *
98
+ * @param did - The user's DID to use as the storage key
99
+ * @param session - The session data to store (contains tokens, DPoP keys, etc.)
100
+ */
101
+ set(did: string, session: NodeSavedSession): Promise<void>;
102
+ /**
103
+ * Deletes a session.
104
+ *
105
+ * Called when a user logs out or when a session is revoked.
106
+ *
107
+ * @param did - The user's DID
108
+ */
109
+ del(did: string): Promise<void>;
110
+ }
111
+ /**
112
+ * Storage interface for OAuth state during the authorization flow.
113
+ *
114
+ * Implement this interface to provide temporary storage for OAuth state
115
+ * parameters. State is used for CSRF protection and PKCE (Proof Key for
116
+ * Code Exchange) during the authorization flow.
117
+ *
118
+ * @remarks
119
+ * - State is short-lived (typically 10-15 minutes)
120
+ * - Keys are random state strings generated by the OAuth client
121
+ * - The `NodeSavedState` type comes from `@atproto/oauth-client-node`
122
+ * - State should be automatically cleaned up after expiry
123
+ *
124
+ * @example Redis implementation with automatic expiry
125
+ * ```typescript
126
+ * import { Redis } from "ioredis";
127
+ * import type { StateStore } from "@hypercerts-org/sdk";
128
+ * import type { NodeSavedState } from "@atproto/oauth-client-node";
129
+ *
130
+ * class RedisStateStore implements StateStore {
131
+ * constructor(
132
+ * private redis: Redis,
133
+ * private prefix = "oauth-state:",
134
+ * private ttlSeconds = 900 // 15 minutes
135
+ * ) {}
136
+ *
137
+ * async get(key: string): Promise<NodeSavedState | undefined> {
138
+ * const data = await this.redis.get(this.prefix + key);
139
+ * return data ? JSON.parse(data) : undefined;
140
+ * }
141
+ *
142
+ * async set(key: string, state: NodeSavedState): Promise<void> {
143
+ * await this.redis.setex(
144
+ * this.prefix + key,
145
+ * this.ttlSeconds,
146
+ * JSON.stringify(state)
147
+ * );
148
+ * }
149
+ *
150
+ * async del(key: string): Promise<void> {
151
+ * await this.redis.del(this.prefix + key);
152
+ * }
153
+ * }
154
+ * ```
155
+ *
156
+ * @see {@link InMemoryStateStore} for the default in-memory implementation
157
+ */
158
+ interface StateStore {
159
+ /**
160
+ * Retrieves OAuth state by key.
161
+ *
162
+ * @param key - The state key (random string from authorization URL)
163
+ * @returns The stored state, or `undefined` if not found or expired
164
+ */
165
+ get(key: string): Promise<NodeSavedState | undefined>;
166
+ /**
167
+ * Stores OAuth state temporarily.
168
+ *
169
+ * Called when starting the authorization flow. The state should be
170
+ * stored with a short TTL (10-15 minutes recommended).
171
+ *
172
+ * @param key - The state key to use for storage
173
+ * @param state - The OAuth state data (includes PKCE verifier, etc.)
174
+ */
175
+ set(key: string, state: NodeSavedState): Promise<void>;
176
+ /**
177
+ * Deletes OAuth state.
178
+ *
179
+ * Called after the state has been used (successful or failed callback).
180
+ *
181
+ * @param key - The state key to delete
182
+ */
183
+ del(key: string): Promise<void>;
184
+ }
185
+ /**
186
+ * Generic cache interface for profiles, metadata, and other data.
187
+ *
188
+ * Implement this interface to provide caching for frequently accessed data.
189
+ * Caching can significantly reduce API calls and improve performance.
190
+ *
191
+ * @remarks
192
+ * The SDK does not provide a default cache implementation - you must
193
+ * implement this interface if you want caching. Consider using:
194
+ * - In-memory cache (e.g., `lru-cache`) for single-instance applications
195
+ * - Redis for distributed applications
196
+ * - Database-backed cache for persistence
197
+ *
198
+ * @example LRU cache implementation
199
+ * ```typescript
200
+ * import { LRUCache } from "lru-cache";
201
+ * import type { CacheInterface } from "@hypercerts-org/sdk";
202
+ *
203
+ * class LRUCacheAdapter implements CacheInterface {
204
+ * private cache = new LRUCache<string, unknown>({
205
+ * max: 1000,
206
+ * ttl: 5 * 60 * 1000, // 5 minutes default
207
+ * });
208
+ *
209
+ * async get<T>(key: string): Promise<T | undefined> {
210
+ * return this.cache.get(key) as T | undefined;
211
+ * }
212
+ *
213
+ * async set<T>(key: string, value: T, ttlSeconds?: number): Promise<void> {
214
+ * this.cache.set(key, value, {
215
+ * ttl: ttlSeconds ? ttlSeconds * 1000 : undefined,
216
+ * });
217
+ * }
218
+ *
219
+ * async del(key: string): Promise<void> {
220
+ * this.cache.delete(key);
221
+ * }
222
+ *
223
+ * async clear(): Promise<void> {
224
+ * this.cache.clear();
225
+ * }
226
+ * }
227
+ * ```
228
+ */
229
+ interface CacheInterface {
230
+ /**
231
+ * Gets a cached value by key.
232
+ *
233
+ * @typeParam T - The expected type of the cached value
234
+ * @param key - The cache key
235
+ * @returns The cached value, or `undefined` if not found or expired
236
+ */
237
+ get<T>(key: string): Promise<T | undefined>;
238
+ /**
239
+ * Sets a cached value with optional TTL (time-to-live).
240
+ *
241
+ * @typeParam T - The type of the value being cached
242
+ * @param key - The cache key
243
+ * @param value - The value to cache
244
+ * @param ttlSeconds - Optional time-to-live in seconds. If not provided,
245
+ * the cache implementation should use its default TTL.
246
+ */
247
+ set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
248
+ /**
249
+ * Deletes a cached value.
250
+ *
251
+ * @param key - The cache key to delete
252
+ */
253
+ del(key: string): Promise<void>;
254
+ /**
255
+ * Clears all cached values.
256
+ *
257
+ * Use with caution in production as this affects all cached data.
258
+ */
259
+ clear(): Promise<void>;
260
+ }
261
+ /**
262
+ * Logger interface for debugging and observability.
263
+ *
264
+ * Implement this interface to receive log messages from the SDK.
265
+ * The interface is compatible with `console` and most popular
266
+ * logging libraries (Pino, Winston, Bunyan, etc.).
267
+ *
268
+ * @remarks
269
+ * Log levels follow standard conventions:
270
+ * - `debug`: Detailed information for debugging (tokens, request details)
271
+ * - `info`: General operational messages (initialization, successful auth)
272
+ * - `warn`: Potentially problematic situations (deprecated features, retries)
273
+ * - `error`: Errors that don't crash the application (failed requests, validation)
274
+ *
275
+ * @example Using console
276
+ * ```typescript
277
+ * const sdk = new ATProtoSDK({
278
+ * // ...
279
+ * logger: console,
280
+ * });
281
+ * ```
282
+ *
283
+ * @example Using Pino
284
+ * ```typescript
285
+ * import pino from "pino";
286
+ *
287
+ * const logger = pino({ level: "debug" });
288
+ * const sdk = new ATProtoSDK({
289
+ * // ...
290
+ * logger: logger,
291
+ * });
292
+ * ```
293
+ *
294
+ * @example Custom logger with context
295
+ * ```typescript
296
+ * const logger: LoggerInterface = {
297
+ * debug: (msg, ...args) => console.debug(`[SDK] ${msg}`, ...args),
298
+ * info: (msg, ...args) => console.info(`[SDK] ${msg}`, ...args),
299
+ * warn: (msg, ...args) => console.warn(`[SDK] ${msg}`, ...args),
300
+ * error: (msg, ...args) => console.error(`[SDK] ${msg}`, ...args),
301
+ * };
302
+ * ```
303
+ */
304
+ interface LoggerInterface {
305
+ /**
306
+ * Logs debug-level messages.
307
+ *
308
+ * Used for detailed diagnostic information. May include sensitive
309
+ * data like request URLs and headers (but not tokens).
310
+ *
311
+ * @param message - The log message
312
+ * @param args - Additional arguments (objects, error details, etc.)
313
+ */
314
+ debug(message: string, ...args: unknown[]): void;
315
+ /**
316
+ * Logs info-level messages.
317
+ *
318
+ * Used for general operational information like successful
319
+ * initialization, authentication events, etc.
320
+ *
321
+ * @param message - The log message
322
+ * @param args - Additional arguments
323
+ */
324
+ info(message: string, ...args: unknown[]): void;
325
+ /**
326
+ * Logs warning-level messages.
327
+ *
328
+ * Used for potentially problematic situations that don't prevent
329
+ * operation but may indicate issues.
330
+ *
331
+ * @param message - The log message
332
+ * @param args - Additional arguments
333
+ */
334
+ warn(message: string, ...args: unknown[]): void;
335
+ /**
336
+ * Logs error-level messages.
337
+ *
338
+ * Used for error conditions that should be investigated.
339
+ * The SDK continues to operate after logging errors.
340
+ *
341
+ * @param message - The log message
342
+ * @param args - Additional arguments (typically includes the error object)
343
+ */
344
+ error(message: string, ...args: unknown[]): void;
345
+ }
346
+
347
+ /**
348
+ * Zod schema for OAuth configuration validation.
349
+ *
350
+ * @remarks
351
+ * All URLs must be valid and use HTTPS in production. The `jwkPrivate` field
352
+ * should contain the private key in JWK (JSON Web Key) format as a string.
353
+ */
354
+ declare const OAuthConfigSchema: z.ZodObject<{
355
+ /**
356
+ * URL to the OAuth client metadata JSON document.
357
+ * This document describes your application to the authorization server.
358
+ *
359
+ * @see https://atproto.com/specs/oauth#client-metadata
360
+ */
361
+ clientId: z.ZodString;
362
+ /**
363
+ * URL where users are redirected after authentication.
364
+ * Must match one of the redirect URIs in your client metadata.
365
+ */
366
+ redirectUri: z.ZodString;
367
+ /**
368
+ * OAuth scopes to request, space-separated.
369
+ * Common scopes: "atproto", "transition:generic"
370
+ */
371
+ scope: z.ZodString;
372
+ /**
373
+ * URL to your public JWKS (JSON Web Key Set) endpoint.
374
+ * Used by the authorization server to verify your client's signatures.
375
+ */
376
+ jwksUri: z.ZodString;
377
+ /**
378
+ * Private JWK (JSON Web Key) as a JSON string.
379
+ * Used for signing DPoP proofs and client assertions.
380
+ *
381
+ * @remarks
382
+ * This should be kept secret and never exposed to clients.
383
+ * Typically loaded from environment variables or a secrets manager.
384
+ */
385
+ jwkPrivate: z.ZodString;
386
+ }, "strip", z.ZodTypeAny, {
387
+ clientId: string;
388
+ redirectUri: string;
389
+ scope: string;
390
+ jwksUri: string;
391
+ jwkPrivate: string;
392
+ }, {
393
+ clientId: string;
394
+ redirectUri: string;
395
+ scope: string;
396
+ jwksUri: string;
397
+ jwkPrivate: string;
398
+ }>;
399
+ /**
400
+ * Zod schema for server URL configuration.
401
+ *
402
+ * @remarks
403
+ * At least one server (PDS or SDS) should be configured for the SDK to be useful.
404
+ */
405
+ declare const ServerConfigSchema: z.ZodObject<{
406
+ /**
407
+ * Personal Data Server URL - the user's own AT Protocol server.
408
+ * This is the primary server for user data operations.
409
+ *
410
+ * @example "https://bsky.social"
411
+ */
412
+ pds: z.ZodOptional<z.ZodString>;
413
+ /**
414
+ * Shared Data Server URL - for collaborative data storage.
415
+ * Required for collaborator and organization operations.
416
+ *
417
+ * @example "https://sds.hypercerts.org"
418
+ */
419
+ sds: z.ZodOptional<z.ZodString>;
420
+ }, "strip", z.ZodTypeAny, {
421
+ pds?: string | undefined;
422
+ sds?: string | undefined;
423
+ }, {
424
+ pds?: string | undefined;
425
+ sds?: string | undefined;
426
+ }>;
427
+ /**
428
+ * Zod schema for timeout configuration.
429
+ *
430
+ * @remarks
431
+ * All timeout values are in milliseconds.
432
+ */
433
+ declare const TimeoutConfigSchema: z.ZodObject<{
434
+ /**
435
+ * Timeout for fetching PDS metadata during identity resolution.
436
+ * @default 5000 (5 seconds, set by OAuthClient)
437
+ */
438
+ pdsMetadata: z.ZodOptional<z.ZodNumber>;
439
+ /**
440
+ * Timeout for general API requests to PDS/SDS.
441
+ * @default 30000 (30 seconds)
442
+ */
443
+ apiRequests: z.ZodOptional<z.ZodNumber>;
444
+ }, "strip", z.ZodTypeAny, {
445
+ pdsMetadata?: number | undefined;
446
+ apiRequests?: number | undefined;
447
+ }, {
448
+ pdsMetadata?: number | undefined;
449
+ apiRequests?: number | undefined;
450
+ }>;
451
+ /**
452
+ * Configuration options for the ATProto SDK.
453
+ *
454
+ * This interface defines all configuration needed to initialize the SDK,
455
+ * including OAuth credentials, server endpoints, and optional customizations.
456
+ *
457
+ * @example Minimal configuration
458
+ * ```typescript
459
+ * const config: ATProtoSDKConfig = {
460
+ * oauth: {
461
+ * clientId: "https://my-app.com/client-metadata.json",
462
+ * redirectUri: "https://my-app.com/callback",
463
+ * scope: "atproto transition:generic",
464
+ * jwksUri: "https://my-app.com/.well-known/jwks.json",
465
+ * jwkPrivate: process.env.JWK_PRIVATE_KEY!,
466
+ * },
467
+ * servers: {
468
+ * pds: "https://bsky.social",
469
+ * },
470
+ * };
471
+ * ```
472
+ *
473
+ * @example Full configuration with custom storage
474
+ * ```typescript
475
+ * const config: ATProtoSDKConfig = {
476
+ * oauth: { ... },
477
+ * servers: {
478
+ * pds: "https://bsky.social",
479
+ * sds: "https://sds.hypercerts.org",
480
+ * },
481
+ * storage: {
482
+ * sessionStore: new RedisSessionStore(redisClient),
483
+ * stateStore: new RedisStateStore(redisClient),
484
+ * },
485
+ * timeouts: {
486
+ * pdsMetadata: 5000,
487
+ * apiRequests: 30000,
488
+ * },
489
+ * logger: console,
490
+ * };
491
+ * ```
492
+ */
493
+ interface ATProtoSDKConfig {
494
+ /**
495
+ * OAuth 2.0 configuration for authentication.
496
+ *
497
+ * Required fields for the OAuth flow with DPoP (Demonstrating Proof of Possession).
498
+ * Your application must host the client metadata and JWKS endpoints.
499
+ *
500
+ * @see https://atproto.com/specs/oauth for AT Protocol OAuth specification
501
+ */
502
+ oauth: z.infer<typeof OAuthConfigSchema>;
503
+ /**
504
+ * Server URLs for PDS and SDS connections.
505
+ *
506
+ * - **PDS**: Personal Data Server - user's own data storage
507
+ * - **SDS**: Shared Data Server - collaborative storage with access control
508
+ */
509
+ servers?: z.infer<typeof ServerConfigSchema>;
510
+ /**
511
+ * Storage adapters for persisting OAuth sessions and state.
512
+ *
513
+ * If not provided, in-memory implementations are used automatically.
514
+ * **Warning**: In-memory storage is lost on process restart - use persistent
515
+ * storage (Redis, database, etc.) in production.
516
+ *
517
+ * @example
518
+ * ```typescript
519
+ * storage: {
520
+ * sessionStore: new RedisSessionStore(redis),
521
+ * stateStore: new RedisStateStore(redis),
522
+ * }
523
+ * ```
524
+ */
525
+ storage?: {
526
+ /**
527
+ * Persistent storage for OAuth sessions.
528
+ * Sessions contain access tokens, refresh tokens, and DPoP keys.
529
+ */
530
+ sessionStore?: SessionStore;
531
+ /**
532
+ * Temporary storage for OAuth state during the authorization flow.
533
+ * State is short-lived and used for PKCE and CSRF protection.
534
+ */
535
+ stateStore?: StateStore;
536
+ };
537
+ /**
538
+ * Custom fetch implementation for HTTP requests.
539
+ *
540
+ * Use this to add custom headers, logging, or to use a different HTTP client.
541
+ * Must be compatible with the standard Fetch API.
542
+ *
543
+ * @example
544
+ * ```typescript
545
+ * fetch: async (url, init) => {
546
+ * console.log(`Fetching: ${url}`);
547
+ * return globalThis.fetch(url, init);
548
+ * }
549
+ * ```
550
+ */
551
+ fetch?: typeof fetch;
552
+ /**
553
+ * Timeout configuration for network requests.
554
+ * Values are in milliseconds.
555
+ */
556
+ timeouts?: z.infer<typeof TimeoutConfigSchema>;
557
+ /**
558
+ * Cache for profiles, metadata, and other frequently accessed data.
559
+ *
560
+ * Implementing caching can significantly reduce API calls and improve performance.
561
+ * The SDK does not provide a default cache - you must implement {@link CacheInterface}.
562
+ */
563
+ cache?: CacheInterface;
564
+ /**
565
+ * Logger for debugging and observability.
566
+ *
567
+ * The logger receives debug, info, warn, and error messages from the SDK.
568
+ * Compatible with `console` or any logger implementing {@link LoggerInterface}.
569
+ *
570
+ * @example
571
+ * ```typescript
572
+ * logger: console
573
+ * // or
574
+ * logger: pino()
575
+ * // or
576
+ * logger: winston.createLogger({ ... })
577
+ * ```
578
+ */
579
+ logger?: LoggerInterface;
580
+ }
581
+
582
+ /**
583
+ * Mock factories for testing.
584
+ *
585
+ * This module provides factory functions to create mock objects
586
+ * for testing SDK functionality without real AT Protocol connections.
587
+ *
588
+ * @packageDocumentation
589
+ */
590
+
591
+ /**
592
+ * Creates a mock OAuth session for testing.
593
+ *
594
+ * The mock session includes all required properties and a mock
595
+ * `fetchHandler` that returns empty successful responses by default.
596
+ *
597
+ * @param overrides - Partial session object to override default values
598
+ * @returns A mock Session object suitable for testing
599
+ *
600
+ * @remarks
601
+ * The mock session is cast to `Session` type for compatibility.
602
+ * In real usage, sessions come from the OAuth flow and contain
603
+ * actual tokens and a real fetch handler.
604
+ *
605
+ * **Default Values**:
606
+ * - `did`: `"did:plc:test123"`
607
+ * - `handle`: `"test.bsky.social"`
608
+ * - `fetchHandler`: Returns `Response` with `{}` body
609
+ *
610
+ * @example Basic mock session
611
+ * ```typescript
612
+ * import { createMockSession } from "@hypercerts-org/sdk/testing";
613
+ *
614
+ * const session = createMockSession();
615
+ * const repo = sdk.repository(session);
616
+ * ```
617
+ *
618
+ * @example With custom DID
619
+ * ```typescript
620
+ * const session = createMockSession({
621
+ * did: "did:plc:custom-test-user",
622
+ * handle: "custom.bsky.social",
623
+ * });
624
+ * ```
625
+ *
626
+ * @example With custom fetch handler
627
+ * ```typescript
628
+ * const session = createMockSession({
629
+ * fetchHandler: async (url, init) => {
630
+ * // Custom response logic
631
+ * return new Response(JSON.stringify({ success: true }));
632
+ * },
633
+ * });
634
+ * ```
635
+ */
636
+ declare function createMockSession(overrides?: Partial<Session>): Session;
637
+ /**
638
+ * Creates a mock SDK configuration for testing.
639
+ *
640
+ * The configuration includes all required OAuth settings with
641
+ * placeholder values suitable for testing (not real credentials).
642
+ *
643
+ * @param overrides - Partial configuration to override default values
644
+ * @returns A complete ATProtoSDKConfig suitable for testing
645
+ *
646
+ * @remarks
647
+ * The default configuration uses example.com domains and a minimal
648
+ * JWK structure. This is sufficient for unit tests but won't work
649
+ * for integration tests that require real OAuth flows.
650
+ *
651
+ * **Default Values**:
652
+ * - `clientId`: `"https://test.example.com/client-metadata.json"`
653
+ * - `pds`: `"https://bsky.social"`
654
+ * - `sds`: `"https://sds.example.com"`
655
+ *
656
+ * @example Basic test config
657
+ * ```typescript
658
+ * import { createTestConfig } from "@hypercerts-org/sdk/testing";
659
+ *
660
+ * const config = createTestConfig();
661
+ * const sdk = new ATProtoSDK(config);
662
+ * ```
663
+ *
664
+ * @example With custom PDS
665
+ * ```typescript
666
+ * const config = createTestConfig({
667
+ * servers: {
668
+ * pds: "https://custom-pds.example.com",
669
+ * },
670
+ * });
671
+ * ```
672
+ *
673
+ * @example With logger for debugging tests
674
+ * ```typescript
675
+ * const config = createTestConfig({
676
+ * logger: console,
677
+ * });
678
+ * ```
679
+ */
680
+ declare function createTestConfig(overrides?: Partial<ATProtoSDKConfig>): ATProtoSDKConfig;
681
+
682
+ /**
683
+ * Mock storage implementations for testing.
684
+ *
685
+ * This module provides mock implementations of SessionStore and StateStore
686
+ * that track all operations for verification in tests.
687
+ *
688
+ * @packageDocumentation
689
+ */
690
+
691
+ /**
692
+ * Mock session store that tracks all operations.
693
+ *
694
+ * This implementation stores sessions in memory and records all
695
+ * method calls for verification in tests.
696
+ *
697
+ * @remarks
698
+ * Use this in tests to:
699
+ * - Verify that sessions are being stored correctly
700
+ * - Check what DIDs have been accessed
701
+ * - Assert on the number and order of operations
702
+ * - Pre-populate sessions for testing restore flows
703
+ *
704
+ * @example Basic usage
705
+ * ```typescript
706
+ * import { MockSessionStore } from "@hypercerts-org/sdk/testing";
707
+ *
708
+ * const sessionStore = new MockSessionStore();
709
+ * const sdk = new ATProtoSDK({
710
+ * ...config,
711
+ * storage: { sessionStore },
712
+ * });
713
+ *
714
+ * // After some operations...
715
+ * expect(sessionStore.setCalls).toHaveLength(1);
716
+ * expect(sessionStore.getCalls).toContain("did:plc:test123");
717
+ * ```
718
+ *
719
+ * @example Pre-populating for tests
720
+ * ```typescript
721
+ * const sessionStore = new MockSessionStore();
722
+ *
723
+ * // Pre-populate a session
724
+ * await sessionStore.set("did:plc:existing", mockSessionData);
725
+ *
726
+ * // Reset tracking (keeps the data)
727
+ * sessionStore.getCalls = [];
728
+ * sessionStore.setCalls = [];
729
+ *
730
+ * // Now test restore behavior
731
+ * const session = await sdk.restoreSession("did:plc:existing");
732
+ * expect(sessionStore.getCalls).toContain("did:plc:existing");
733
+ * ```
734
+ *
735
+ * @example Asserting on operations
736
+ * ```typescript
737
+ * const sessionStore = new MockSessionStore();
738
+ *
739
+ * // ... perform operations ...
740
+ *
741
+ * // Verify session was stored for correct DID
742
+ * expect(sessionStore.setCalls[0].did).toBe("did:plc:expected");
743
+ *
744
+ * // Verify session was deleted on logout
745
+ * expect(sessionStore.delCalls).toContain("did:plc:logged-out");
746
+ * ```
747
+ */
748
+ declare class MockSessionStore implements SessionStore {
749
+ /**
750
+ * Internal storage for sessions.
751
+ * @internal
752
+ */
753
+ private store;
754
+ /**
755
+ * Record of all `get()` calls made to this store.
756
+ *
757
+ * Each entry is the DID that was requested.
758
+ */
759
+ getCalls: string[];
760
+ /**
761
+ * Record of all `set()` calls made to this store.
762
+ *
763
+ * Each entry contains the DID and session that was stored.
764
+ */
765
+ setCalls: Array<{
766
+ did: string;
767
+ session: NodeSavedSession;
768
+ }>;
769
+ /**
770
+ * Record of all `del()` calls made to this store.
771
+ *
772
+ * Each entry is the DID that was deleted.
773
+ */
774
+ delCalls: string[];
775
+ /**
776
+ * Retrieves a session by DID.
777
+ *
778
+ * Records the call in `getCalls`.
779
+ *
780
+ * @param did - The DID to look up
781
+ * @returns The stored session or undefined
782
+ */
783
+ get(did: string): Promise<NodeSavedSession | undefined>;
784
+ /**
785
+ * Stores a session.
786
+ *
787
+ * Records the call in `setCalls`.
788
+ *
789
+ * @param did - The DID to store under
790
+ * @param session - The session data to store
791
+ */
792
+ set(did: string, session: NodeSavedSession): Promise<void>;
793
+ /**
794
+ * Deletes a session.
795
+ *
796
+ * Records the call in `delCalls`.
797
+ *
798
+ * @param did - The DID to delete
799
+ */
800
+ del(did: string): Promise<void>;
801
+ /**
802
+ * Resets the store to initial state.
803
+ *
804
+ * Clears all stored sessions and all recorded calls.
805
+ * Call this in `beforeEach` or `afterEach` to ensure test isolation.
806
+ *
807
+ * @example
808
+ * ```typescript
809
+ * beforeEach(() => {
810
+ * sessionStore.reset();
811
+ * });
812
+ * ```
813
+ */
814
+ reset(): void;
815
+ }
816
+ /**
817
+ * Mock state store that tracks all operations.
818
+ *
819
+ * This implementation stores OAuth state in memory and records all
820
+ * method calls for verification in tests.
821
+ *
822
+ * @remarks
823
+ * Use this in tests to:
824
+ * - Verify OAuth state is being created during authorization
825
+ * - Check that state is retrieved during callback
826
+ * - Assert that state is cleaned up after use
827
+ * - Test error handling for missing/invalid state
828
+ *
829
+ * @example Basic usage
830
+ * ```typescript
831
+ * import { MockStateStore } from "@hypercerts-org/sdk/testing";
832
+ *
833
+ * const stateStore = new MockStateStore();
834
+ * const sdk = new ATProtoSDK({
835
+ * ...config,
836
+ * storage: { stateStore },
837
+ * });
838
+ *
839
+ * // After authorize()
840
+ * expect(stateStore.setCalls).toHaveLength(1);
841
+ *
842
+ * // After callback()
843
+ * expect(stateStore.getCalls).toHaveLength(1);
844
+ * expect(stateStore.delCalls).toHaveLength(1);
845
+ * ```
846
+ *
847
+ * @example Testing invalid state
848
+ * ```typescript
849
+ * const stateStore = new MockStateStore();
850
+ * // Don't pre-populate - state will be missing
851
+ *
852
+ * // This should fail because state doesn't exist
853
+ * await expect(sdk.callback(params)).rejects.toThrow();
854
+ *
855
+ * // Verify the lookup was attempted
856
+ * expect(stateStore.getCalls).toContain(stateKey);
857
+ * ```
858
+ */
859
+ declare class MockStateStore implements StateStore {
860
+ /**
861
+ * Internal storage for OAuth state.
862
+ * @internal
863
+ */
864
+ private store;
865
+ /**
866
+ * Record of all `get()` calls made to this store.
867
+ *
868
+ * Each entry is the state key that was requested.
869
+ */
870
+ getCalls: string[];
871
+ /**
872
+ * Record of all `set()` calls made to this store.
873
+ *
874
+ * Each entry contains the key and state that was stored.
875
+ */
876
+ setCalls: Array<{
877
+ key: string;
878
+ state: NodeSavedState;
879
+ }>;
880
+ /**
881
+ * Record of all `del()` calls made to this store.
882
+ *
883
+ * Each entry is the state key that was deleted.
884
+ */
885
+ delCalls: string[];
886
+ /**
887
+ * Retrieves OAuth state by key.
888
+ *
889
+ * Records the call in `getCalls`.
890
+ *
891
+ * @param key - The state key to look up
892
+ * @returns The stored state or undefined
893
+ */
894
+ get(key: string): Promise<NodeSavedState | undefined>;
895
+ /**
896
+ * Stores OAuth state.
897
+ *
898
+ * Records the call in `setCalls`.
899
+ *
900
+ * @param key - The state key to store under
901
+ * @param state - The OAuth state data to store
902
+ */
903
+ set(key: string, state: NodeSavedState): Promise<void>;
904
+ /**
905
+ * Deletes OAuth state.
906
+ *
907
+ * Records the call in `delCalls`.
908
+ *
909
+ * @param key - The state key to delete
910
+ */
911
+ del(key: string): Promise<void>;
912
+ /**
913
+ * Resets the store to initial state.
914
+ *
915
+ * Clears all stored state and all recorded calls.
916
+ * Call this in `beforeEach` or `afterEach` to ensure test isolation.
917
+ *
918
+ * @example
919
+ * ```typescript
920
+ * beforeEach(() => {
921
+ * stateStore.reset();
922
+ * });
923
+ * ```
924
+ */
925
+ reset(): void;
926
+ }
927
+
928
+ export { MockSessionStore, MockStateStore, createMockSession, createTestConfig };