@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,474 @@
1
+ import { NodeSavedSession, NodeSavedState } from '@atproto/oauth-client-node';
2
+
3
+ /**
4
+ * Storage interface for persisting OAuth sessions.
5
+ *
6
+ * Implement this interface to provide persistent storage for user sessions.
7
+ * Sessions contain sensitive data including access tokens, refresh tokens,
8
+ * and DPoP key pairs.
9
+ *
10
+ * The SDK provides {@link InMemorySessionStore} for development/testing,
11
+ * but **production applications should implement persistent storage**
12
+ * (e.g., Redis, PostgreSQL, MongoDB).
13
+ *
14
+ * @remarks
15
+ * - Sessions are keyed by the user's DID (Decentralized Identifier)
16
+ * - The `NodeSavedSession` type comes from `@atproto/oauth-client-node`
17
+ * - Sessions may be large (~2-4KB) due to embedded cryptographic keys
18
+ * - Consider encrypting sessions at rest for additional security
19
+ *
20
+ * @example Redis implementation
21
+ * ```typescript
22
+ * import { Redis } from "ioredis";
23
+ * import type { SessionStore } from "@hypercerts-org/sdk";
24
+ * import type { NodeSavedSession } from "@atproto/oauth-client-node";
25
+ *
26
+ * class RedisSessionStore implements SessionStore {
27
+ * constructor(private redis: Redis, private prefix = "session:") {}
28
+ *
29
+ * async get(did: string): Promise<NodeSavedSession | undefined> {
30
+ * const data = await this.redis.get(this.prefix + did);
31
+ * return data ? JSON.parse(data) : undefined;
32
+ * }
33
+ *
34
+ * async set(did: string, session: NodeSavedSession): Promise<void> {
35
+ * // Set with 30-day expiry (sessions can be refreshed)
36
+ * await this.redis.setex(
37
+ * this.prefix + did,
38
+ * 30 * 24 * 60 * 60,
39
+ * JSON.stringify(session)
40
+ * );
41
+ * }
42
+ *
43
+ * async del(did: string): Promise<void> {
44
+ * await this.redis.del(this.prefix + did);
45
+ * }
46
+ * }
47
+ * ```
48
+ *
49
+ * @see {@link InMemorySessionStore} for the default in-memory implementation
50
+ */
51
+ interface SessionStore {
52
+ /**
53
+ * Retrieves a session by DID.
54
+ *
55
+ * @param did - The user's Decentralized Identifier (e.g., "did:plc:abc123...")
56
+ * @returns The stored session, or `undefined` if not found
57
+ */
58
+ get(did: string): Promise<NodeSavedSession | undefined>;
59
+ /**
60
+ * Stores or updates a session.
61
+ *
62
+ * This is called after successful authentication and whenever tokens are refreshed.
63
+ *
64
+ * @param did - The user's DID to use as the storage key
65
+ * @param session - The session data to store (contains tokens, DPoP keys, etc.)
66
+ */
67
+ set(did: string, session: NodeSavedSession): Promise<void>;
68
+ /**
69
+ * Deletes a session.
70
+ *
71
+ * Called when a user logs out or when a session is revoked.
72
+ *
73
+ * @param did - The user's DID
74
+ */
75
+ del(did: string): Promise<void>;
76
+ }
77
+ /**
78
+ * Storage interface for OAuth state during the authorization flow.
79
+ *
80
+ * Implement this interface to provide temporary storage for OAuth state
81
+ * parameters. State is used for CSRF protection and PKCE (Proof Key for
82
+ * Code Exchange) during the authorization flow.
83
+ *
84
+ * @remarks
85
+ * - State is short-lived (typically 10-15 minutes)
86
+ * - Keys are random state strings generated by the OAuth client
87
+ * - The `NodeSavedState` type comes from `@atproto/oauth-client-node`
88
+ * - State should be automatically cleaned up after expiry
89
+ *
90
+ * @example Redis implementation with automatic expiry
91
+ * ```typescript
92
+ * import { Redis } from "ioredis";
93
+ * import type { StateStore } from "@hypercerts-org/sdk";
94
+ * import type { NodeSavedState } from "@atproto/oauth-client-node";
95
+ *
96
+ * class RedisStateStore implements StateStore {
97
+ * constructor(
98
+ * private redis: Redis,
99
+ * private prefix = "oauth-state:",
100
+ * private ttlSeconds = 900 // 15 minutes
101
+ * ) {}
102
+ *
103
+ * async get(key: string): Promise<NodeSavedState | undefined> {
104
+ * const data = await this.redis.get(this.prefix + key);
105
+ * return data ? JSON.parse(data) : undefined;
106
+ * }
107
+ *
108
+ * async set(key: string, state: NodeSavedState): Promise<void> {
109
+ * await this.redis.setex(
110
+ * this.prefix + key,
111
+ * this.ttlSeconds,
112
+ * JSON.stringify(state)
113
+ * );
114
+ * }
115
+ *
116
+ * async del(key: string): Promise<void> {
117
+ * await this.redis.del(this.prefix + key);
118
+ * }
119
+ * }
120
+ * ```
121
+ *
122
+ * @see {@link InMemoryStateStore} for the default in-memory implementation
123
+ */
124
+ interface StateStore {
125
+ /**
126
+ * Retrieves OAuth state by key.
127
+ *
128
+ * @param key - The state key (random string from authorization URL)
129
+ * @returns The stored state, or `undefined` if not found or expired
130
+ */
131
+ get(key: string): Promise<NodeSavedState | undefined>;
132
+ /**
133
+ * Stores OAuth state temporarily.
134
+ *
135
+ * Called when starting the authorization flow. The state should be
136
+ * stored with a short TTL (10-15 minutes recommended).
137
+ *
138
+ * @param key - The state key to use for storage
139
+ * @param state - The OAuth state data (includes PKCE verifier, etc.)
140
+ */
141
+ set(key: string, state: NodeSavedState): Promise<void>;
142
+ /**
143
+ * Deletes OAuth state.
144
+ *
145
+ * Called after the state has been used (successful or failed callback).
146
+ *
147
+ * @param key - The state key to delete
148
+ */
149
+ del(key: string): Promise<void>;
150
+ }
151
+ /**
152
+ * Generic cache interface for profiles, metadata, and other data.
153
+ *
154
+ * Implement this interface to provide caching for frequently accessed data.
155
+ * Caching can significantly reduce API calls and improve performance.
156
+ *
157
+ * @remarks
158
+ * The SDK does not provide a default cache implementation - you must
159
+ * implement this interface if you want caching. Consider using:
160
+ * - In-memory cache (e.g., `lru-cache`) for single-instance applications
161
+ * - Redis for distributed applications
162
+ * - Database-backed cache for persistence
163
+ *
164
+ * @example LRU cache implementation
165
+ * ```typescript
166
+ * import { LRUCache } from "lru-cache";
167
+ * import type { CacheInterface } from "@hypercerts-org/sdk";
168
+ *
169
+ * class LRUCacheAdapter implements CacheInterface {
170
+ * private cache = new LRUCache<string, unknown>({
171
+ * max: 1000,
172
+ * ttl: 5 * 60 * 1000, // 5 minutes default
173
+ * });
174
+ *
175
+ * async get<T>(key: string): Promise<T | undefined> {
176
+ * return this.cache.get(key) as T | undefined;
177
+ * }
178
+ *
179
+ * async set<T>(key: string, value: T, ttlSeconds?: number): Promise<void> {
180
+ * this.cache.set(key, value, {
181
+ * ttl: ttlSeconds ? ttlSeconds * 1000 : undefined,
182
+ * });
183
+ * }
184
+ *
185
+ * async del(key: string): Promise<void> {
186
+ * this.cache.delete(key);
187
+ * }
188
+ *
189
+ * async clear(): Promise<void> {
190
+ * this.cache.clear();
191
+ * }
192
+ * }
193
+ * ```
194
+ */
195
+ interface CacheInterface {
196
+ /**
197
+ * Gets a cached value by key.
198
+ *
199
+ * @typeParam T - The expected type of the cached value
200
+ * @param key - The cache key
201
+ * @returns The cached value, or `undefined` if not found or expired
202
+ */
203
+ get<T>(key: string): Promise<T | undefined>;
204
+ /**
205
+ * Sets a cached value with optional TTL (time-to-live).
206
+ *
207
+ * @typeParam T - The type of the value being cached
208
+ * @param key - The cache key
209
+ * @param value - The value to cache
210
+ * @param ttlSeconds - Optional time-to-live in seconds. If not provided,
211
+ * the cache implementation should use its default TTL.
212
+ */
213
+ set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>;
214
+ /**
215
+ * Deletes a cached value.
216
+ *
217
+ * @param key - The cache key to delete
218
+ */
219
+ del(key: string): Promise<void>;
220
+ /**
221
+ * Clears all cached values.
222
+ *
223
+ * Use with caution in production as this affects all cached data.
224
+ */
225
+ clear(): Promise<void>;
226
+ }
227
+
228
+ /**
229
+ * In-memory implementation of the SessionStore interface.
230
+ *
231
+ * This store keeps OAuth sessions in memory using a Map. It's intended
232
+ * for development, testing, and simple use cases where session persistence
233
+ * across restarts is not required.
234
+ *
235
+ * @remarks
236
+ * **Warning**: This implementation is **not suitable for production** because:
237
+ * - Sessions are lost when the process restarts
238
+ * - Sessions cannot be shared across multiple server instances
239
+ * - No automatic cleanup of expired sessions
240
+ *
241
+ * For production, implement {@link SessionStore} with a persistent backend:
242
+ * - **Redis**: Good for distributed systems, supports TTL
243
+ * - **PostgreSQL/MySQL**: Good for existing database infrastructure
244
+ * - **MongoDB**: Good for document-based storage
245
+ *
246
+ * @example Basic usage
247
+ * ```typescript
248
+ * import { InMemorySessionStore } from "@hypercerts-org/sdk/storage";
249
+ *
250
+ * const sessionStore = new InMemorySessionStore();
251
+ *
252
+ * const sdk = new ATProtoSDK({
253
+ * oauth: { ... },
254
+ * storage: {
255
+ * sessionStore, // Will warn in logs for production
256
+ * },
257
+ * });
258
+ * ```
259
+ *
260
+ * @example Testing usage
261
+ * ```typescript
262
+ * const sessionStore = new InMemorySessionStore();
263
+ *
264
+ * // After tests, clean up
265
+ * sessionStore.clear();
266
+ * ```
267
+ *
268
+ * @see {@link SessionStore} for the interface definition
269
+ * @see {@link InMemoryStateStore} for the corresponding state store
270
+ */
271
+ declare class InMemorySessionStore implements SessionStore {
272
+ /**
273
+ * Internal storage for sessions, keyed by DID.
274
+ * @internal
275
+ */
276
+ private sessions;
277
+ /**
278
+ * Retrieves a session by DID.
279
+ *
280
+ * @param did - The user's Decentralized Identifier
281
+ * @returns Promise resolving to the session, or `undefined` if not found
282
+ *
283
+ * @example
284
+ * ```typescript
285
+ * const session = await sessionStore.get("did:plc:abc123");
286
+ * if (session) {
287
+ * console.log("Session found");
288
+ * }
289
+ * ```
290
+ */
291
+ get(did: string): Promise<NodeSavedSession | undefined>;
292
+ /**
293
+ * Stores or updates a session.
294
+ *
295
+ * @param did - The user's DID to use as the key
296
+ * @param session - The session data to store
297
+ *
298
+ * @remarks
299
+ * If a session already exists for the DID, it is overwritten.
300
+ *
301
+ * @example
302
+ * ```typescript
303
+ * await sessionStore.set("did:plc:abc123", sessionData);
304
+ * ```
305
+ */
306
+ set(did: string, session: NodeSavedSession): Promise<void>;
307
+ /**
308
+ * Deletes a session by DID.
309
+ *
310
+ * @param did - The DID of the session to delete
311
+ *
312
+ * @remarks
313
+ * If no session exists for the DID, this is a no-op.
314
+ *
315
+ * @example
316
+ * ```typescript
317
+ * await sessionStore.del("did:plc:abc123");
318
+ * ```
319
+ */
320
+ del(did: string): Promise<void>;
321
+ /**
322
+ * Clears all stored sessions.
323
+ *
324
+ * This is primarily useful for testing to ensure a clean state
325
+ * between test runs.
326
+ *
327
+ * @remarks
328
+ * This method is synchronous (not async) for convenience in test cleanup.
329
+ *
330
+ * @example
331
+ * ```typescript
332
+ * // In test teardown
333
+ * afterEach(() => {
334
+ * sessionStore.clear();
335
+ * });
336
+ * ```
337
+ */
338
+ clear(): void;
339
+ }
340
+
341
+ /**
342
+ * In-memory implementation of the StateStore interface.
343
+ *
344
+ * This store keeps OAuth state parameters in memory using a Map. State is
345
+ * used during the OAuth authorization flow for CSRF protection and PKCE.
346
+ *
347
+ * @remarks
348
+ * **Warning**: This implementation is **not suitable for production** because:
349
+ * - State is lost when the process restarts (breaking in-progress OAuth flows)
350
+ * - State cannot be shared across multiple server instances
351
+ * - No automatic cleanup of expired state (memory leak potential)
352
+ *
353
+ * For production, implement {@link StateStore} with a persistent backend
354
+ * that supports TTL (time-to-live):
355
+ * - **Redis**: Ideal choice with built-in TTL support
356
+ * - **Database with cleanup job**: PostgreSQL/MySQL with periodic cleanup
357
+ *
358
+ * **State Lifecycle**:
359
+ * 1. Created when user starts OAuth flow (`authorize()`)
360
+ * 2. Retrieved and validated during callback
361
+ * 3. Deleted after successful or failed callback
362
+ * 4. Should expire after ~15 minutes if callback never happens
363
+ *
364
+ * @example Basic usage
365
+ * ```typescript
366
+ * import { InMemoryStateStore } from "@hypercerts-org/sdk/storage";
367
+ *
368
+ * const stateStore = new InMemoryStateStore();
369
+ *
370
+ * const sdk = new ATProtoSDK({
371
+ * oauth: { ... },
372
+ * storage: {
373
+ * stateStore, // Will warn in logs for production
374
+ * },
375
+ * });
376
+ * ```
377
+ *
378
+ * @example Testing usage
379
+ * ```typescript
380
+ * const stateStore = new InMemoryStateStore();
381
+ *
382
+ * // After tests, clean up
383
+ * stateStore.clear();
384
+ * ```
385
+ *
386
+ * @see {@link StateStore} for the interface definition
387
+ * @see {@link InMemorySessionStore} for the corresponding session store
388
+ */
389
+ declare class InMemoryStateStore implements StateStore {
390
+ /**
391
+ * Internal storage for OAuth state, keyed by state string.
392
+ * @internal
393
+ */
394
+ private states;
395
+ /**
396
+ * Retrieves OAuth state by key.
397
+ *
398
+ * @param key - The state key (random string from authorization URL)
399
+ * @returns Promise resolving to the state, or `undefined` if not found
400
+ *
401
+ * @remarks
402
+ * The key is a cryptographically random string generated during
403
+ * the authorization request. It's included in the callback URL
404
+ * and used to retrieve the associated PKCE verifier and other data.
405
+ *
406
+ * @example
407
+ * ```typescript
408
+ * // During OAuth callback
409
+ * const state = await stateStore.get(params.get("state")!);
410
+ * if (!state) {
411
+ * throw new Error("Invalid or expired state");
412
+ * }
413
+ * ```
414
+ */
415
+ get(key: string): Promise<NodeSavedState | undefined>;
416
+ /**
417
+ * Stores OAuth state temporarily.
418
+ *
419
+ * @param key - The state key to use for storage
420
+ * @param state - The OAuth state data (includes PKCE verifier, etc.)
421
+ *
422
+ * @remarks
423
+ * In production implementations, state should be stored with a TTL
424
+ * of approximately 10-15 minutes to prevent stale state accumulation.
425
+ *
426
+ * @example
427
+ * ```typescript
428
+ * // Called internally by OAuthClient during authorize()
429
+ * await stateStore.set(stateKey, {
430
+ * // PKCE code verifier, redirect URI, etc.
431
+ * });
432
+ * ```
433
+ */
434
+ set(key: string, state: NodeSavedState): Promise<void>;
435
+ /**
436
+ * Deletes OAuth state by key.
437
+ *
438
+ * @param key - The state key to delete
439
+ *
440
+ * @remarks
441
+ * Called after the OAuth callback is processed (whether successful or not)
442
+ * to clean up the temporary state.
443
+ *
444
+ * @example
445
+ * ```typescript
446
+ * // After processing callback
447
+ * await stateStore.del(stateKey);
448
+ * ```
449
+ */
450
+ del(key: string): Promise<void>;
451
+ /**
452
+ * Clears all stored state.
453
+ *
454
+ * This is primarily useful for testing to ensure a clean state
455
+ * between test runs.
456
+ *
457
+ * @remarks
458
+ * This method is synchronous (not async) for convenience in test cleanup.
459
+ * In production, be careful using this as it will invalidate all
460
+ * in-progress OAuth flows.
461
+ *
462
+ * @example
463
+ * ```typescript
464
+ * // In test teardown
465
+ * afterEach(() => {
466
+ * stateStore.clear();
467
+ * });
468
+ * ```
469
+ */
470
+ clear(): void;
471
+ }
472
+
473
+ export { InMemorySessionStore, InMemoryStateStore };
474
+ export type { CacheInterface, SessionStore, StateStore };