@hypercerts-org/sdk-core 0.5.0-beta.0 → 0.7.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 (57) hide show
  1. package/README.md +130 -8
  2. package/dist/index.cjs +93 -15
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.ts +64 -1
  5. package/dist/index.mjs +93 -16
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +9 -5
  8. package/.turbo/turbo-build.log +0 -40
  9. package/.turbo/turbo-test.log +0 -119
  10. package/CHANGELOG.md +0 -62
  11. package/eslint.config.mjs +0 -22
  12. package/rollup.config.js +0 -75
  13. package/src/auth/OAuthClient.ts +0 -497
  14. package/src/core/SDK.ts +0 -410
  15. package/src/core/config.ts +0 -243
  16. package/src/core/errors.ts +0 -257
  17. package/src/core/interfaces.ts +0 -324
  18. package/src/core/types.ts +0 -282
  19. package/src/errors.ts +0 -57
  20. package/src/index.ts +0 -107
  21. package/src/lexicons.ts +0 -64
  22. package/src/repository/BlobOperationsImpl.ts +0 -199
  23. package/src/repository/CollaboratorOperationsImpl.ts +0 -442
  24. package/src/repository/HypercertOperationsImpl.ts +0 -1146
  25. package/src/repository/LexiconRegistry.ts +0 -332
  26. package/src/repository/OrganizationOperationsImpl.ts +0 -282
  27. package/src/repository/ProfileOperationsImpl.ts +0 -281
  28. package/src/repository/RecordOperationsImpl.ts +0 -340
  29. package/src/repository/Repository.ts +0 -482
  30. package/src/repository/interfaces.ts +0 -909
  31. package/src/repository/types.ts +0 -111
  32. package/src/services/hypercerts/types.ts +0 -87
  33. package/src/storage/InMemorySessionStore.ts +0 -127
  34. package/src/storage/InMemoryStateStore.ts +0 -146
  35. package/src/storage.ts +0 -63
  36. package/src/testing/index.ts +0 -67
  37. package/src/testing/mocks.ts +0 -142
  38. package/src/testing/stores.ts +0 -285
  39. package/src/testing.ts +0 -64
  40. package/src/types.ts +0 -86
  41. package/tests/auth/OAuthClient.test.ts +0 -164
  42. package/tests/core/SDK.test.ts +0 -176
  43. package/tests/core/errors.test.ts +0 -81
  44. package/tests/repository/BlobOperationsImpl.test.ts +0 -155
  45. package/tests/repository/CollaboratorOperationsImpl.test.ts +0 -438
  46. package/tests/repository/HypercertOperationsImpl.test.ts +0 -652
  47. package/tests/repository/LexiconRegistry.test.ts +0 -192
  48. package/tests/repository/OrganizationOperationsImpl.test.ts +0 -240
  49. package/tests/repository/ProfileOperationsImpl.test.ts +0 -254
  50. package/tests/repository/RecordOperationsImpl.test.ts +0 -375
  51. package/tests/repository/Repository.test.ts +0 -149
  52. package/tests/utils/fixtures.ts +0 -117
  53. package/tests/utils/mocks.ts +0 -109
  54. package/tests/utils/repository-fixtures.ts +0 -78
  55. package/tsconfig.json +0 -11
  56. package/tsconfig.tsbuildinfo +0 -1
  57. package/vitest.config.ts +0 -30
@@ -1,332 +0,0 @@
1
- import type { Agent } from "@atproto/api";
2
- import type { LexiconDoc } from "@atproto/lexicon";
3
- import { Lexicons } from "@atproto/lexicon";
4
- import { ValidationError } from "../core/errors.js";
5
-
6
- /**
7
- * Result of validating a record against a lexicon schema.
8
- */
9
- export interface ValidationResult {
10
- /**
11
- * Whether the record is valid according to the lexicon schema.
12
- */
13
- valid: boolean;
14
-
15
- /**
16
- * Error message if validation failed.
17
- *
18
- * Only present when `valid` is `false`.
19
- */
20
- error?: string;
21
- }
22
-
23
- /**
24
- * Registry for managing and validating AT Protocol lexicon schemas.
25
- *
26
- * Lexicons are schema definitions that describe the structure of records
27
- * in the AT Protocol. This registry allows you to:
28
- *
29
- * - Register custom lexicons for your application's record types
30
- * - Validate records against their lexicon schemas
31
- * - Extend the AT Protocol Agent with custom lexicon support
32
- *
33
- * @remarks
34
- * The SDK automatically registers hypercert lexicons when creating a Repository.
35
- * You only need to use this class directly if you're working with custom
36
- * record types.
37
- *
38
- * **Lexicon IDs** follow the NSID (Namespaced Identifier) format:
39
- * `{authority}.{name}` (e.g., `org.hypercerts.hypercert`)
40
- *
41
- * @example Registering custom lexicons
42
- * ```typescript
43
- * const registry = sdk.getLexiconRegistry();
44
- *
45
- * // Register a single lexicon
46
- * registry.register({
47
- * lexicon: 1,
48
- * id: "org.example.myRecord",
49
- * defs: {
50
- * main: {
51
- * type: "record",
52
- * key: "tid",
53
- * record: {
54
- * type: "object",
55
- * required: ["title", "createdAt"],
56
- * properties: {
57
- * title: { type: "string" },
58
- * description: { type: "string" },
59
- * createdAt: { type: "string", format: "datetime" },
60
- * },
61
- * },
62
- * },
63
- * },
64
- * });
65
- *
66
- * // Register multiple lexicons at once
67
- * registry.registerMany([lexicon1, lexicon2, lexicon3]);
68
- * ```
69
- *
70
- * @example Validating records
71
- * ```typescript
72
- * const result = registry.validate("org.example.myRecord", {
73
- * title: "Test",
74
- * createdAt: new Date().toISOString(),
75
- * });
76
- *
77
- * if (!result.valid) {
78
- * console.error(`Validation failed: ${result.error}`);
79
- * }
80
- * ```
81
- *
82
- * @see https://atproto.com/specs/lexicon for the Lexicon specification
83
- */
84
- export class LexiconRegistry {
85
- /** Map of lexicon ID to lexicon document */
86
- private lexicons = new Map<string, LexiconDoc>();
87
-
88
- /** Lexicons collection for validation */
89
- private lexiconsCollection: Lexicons;
90
-
91
- /**
92
- * Creates a new LexiconRegistry.
93
- *
94
- * The registry starts empty. Use {@link register} or {@link registerMany}
95
- * to add lexicons.
96
- */
97
- constructor() {
98
- this.lexiconsCollection = new Lexicons();
99
- }
100
-
101
- /**
102
- * Registers a single lexicon schema.
103
- *
104
- * @param lexicon - The lexicon document to register
105
- * @throws {@link ValidationError} if the lexicon doesn't have an `id` field
106
- *
107
- * @remarks
108
- * If a lexicon with the same ID is already registered, it will be
109
- * replaced with the new definition. This is useful for testing but
110
- * should generally be avoided in production.
111
- *
112
- * @example
113
- * ```typescript
114
- * registry.register({
115
- * lexicon: 1,
116
- * id: "org.example.post",
117
- * defs: {
118
- * main: {
119
- * type: "record",
120
- * key: "tid",
121
- * record: {
122
- * type: "object",
123
- * required: ["text", "createdAt"],
124
- * properties: {
125
- * text: { type: "string", maxLength: 300 },
126
- * createdAt: { type: "string", format: "datetime" },
127
- * },
128
- * },
129
- * },
130
- * },
131
- * });
132
- * ```
133
- */
134
- register(lexicon: LexiconDoc): void {
135
- if (!lexicon.id) {
136
- throw new ValidationError("Lexicon must have an 'id' field");
137
- }
138
-
139
- // Remove existing lexicon if present (to allow overwriting)
140
- if (this.lexicons.has(lexicon.id)) {
141
- // Lexicons collection doesn't support removal, so we create a new one
142
- // This is a limitation - in practice, lexicons shouldn't be overwritten
143
- // But we allow it for testing and flexibility
144
- const existingLexicon = this.lexicons.get(lexicon.id);
145
- if (existingLexicon) {
146
- // Try to remove from collection (may fail if not supported)
147
- try {
148
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
149
- (this.lexiconsCollection as any).remove?.(lexicon.id);
150
- } catch {
151
- // If removal fails, create a new collection
152
- this.lexiconsCollection = new Lexicons();
153
- // Re-register all other lexicons
154
- for (const [id, lex] of this.lexicons.entries()) {
155
- if (id !== lexicon.id) {
156
- this.lexiconsCollection.add(lex);
157
- }
158
- }
159
- }
160
- }
161
- }
162
-
163
- this.lexicons.set(lexicon.id, lexicon);
164
- this.lexiconsCollection.add(lexicon);
165
- }
166
-
167
- /**
168
- * Registers multiple lexicons at once.
169
- *
170
- * @param lexicons - Array of lexicon documents to register
171
- *
172
- * @example
173
- * ```typescript
174
- * import { HYPERCERT_LEXICONS } from "@hypercerts-org/sdk/lexicons";
175
- *
176
- * registry.registerMany(HYPERCERT_LEXICONS);
177
- * ```
178
- */
179
- registerMany(lexicons: LexiconDoc[]): void {
180
- for (const lexicon of lexicons) {
181
- this.register(lexicon);
182
- }
183
- }
184
-
185
- /**
186
- * Gets a lexicon document by ID.
187
- *
188
- * @param id - The lexicon NSID (e.g., "org.hypercerts.hypercert")
189
- * @returns The lexicon document, or `undefined` if not registered
190
- *
191
- * @example
192
- * ```typescript
193
- * const lexicon = registry.get("org.hypercerts.hypercert");
194
- * if (lexicon) {
195
- * console.log(`Found lexicon: ${lexicon.id}`);
196
- * }
197
- * ```
198
- */
199
- get(id: string): LexiconDoc | undefined {
200
- return this.lexicons.get(id);
201
- }
202
-
203
- /**
204
- * Validates a record against a collection's lexicon schema.
205
- *
206
- * @param collection - The collection NSID (same as lexicon ID)
207
- * @param record - The record data to validate
208
- * @returns Validation result with `valid` boolean and optional `error` message
209
- *
210
- * @remarks
211
- * - If no lexicon is registered for the collection, validation passes
212
- * (we can't validate against unknown schemas)
213
- * - Validation checks required fields and type constraints defined
214
- * in the lexicon schema
215
- *
216
- * @example
217
- * ```typescript
218
- * const result = registry.validate("org.hypercerts.hypercert", {
219
- * title: "My Hypercert",
220
- * description: "Description...",
221
- * // ... other fields
222
- * });
223
- *
224
- * if (!result.valid) {
225
- * throw new Error(`Invalid record: ${result.error}`);
226
- * }
227
- * ```
228
- */
229
- validate(collection: string, record: unknown): ValidationResult {
230
- // Check if we have a lexicon registered for this collection
231
- // Collection format is typically "namespace.collection" (e.g., "app.bsky.feed.post")
232
- // Lexicon ID format is the same
233
- const lexiconId = collection;
234
- const lexicon = this.lexicons.get(lexiconId);
235
- if (!lexicon) {
236
- // No lexicon registered - validation passes (can't validate unknown schemas)
237
- return { valid: true };
238
- }
239
-
240
- // Check required fields if the lexicon defines them
241
- const recordDef = lexicon.defs?.record;
242
- if (recordDef && typeof recordDef === "object" && "record" in recordDef) {
243
- const recordSchema = recordDef.record;
244
- if (typeof recordSchema === "object" && "required" in recordSchema && Array.isArray(recordSchema.required)) {
245
- const recordObj = record as Record<string, unknown>;
246
- for (const requiredField of recordSchema.required) {
247
- if (typeof requiredField === "string" && !(requiredField in recordObj)) {
248
- return {
249
- valid: false,
250
- error: `Missing required field: ${requiredField}`,
251
- };
252
- }
253
- }
254
- }
255
- }
256
-
257
- try {
258
- this.lexiconsCollection.assertValidRecord(collection, record);
259
- return { valid: true };
260
- } catch (error) {
261
- // If error indicates lexicon not found, treat as validation pass
262
- // (the lexicon might exist in Agent's collection but not ours)
263
- const errorMessage = error instanceof Error ? error.message : String(error);
264
- if (errorMessage.includes("not found") || errorMessage.includes("Lexicon not found")) {
265
- return { valid: true };
266
- }
267
- return {
268
- valid: false,
269
- error: errorMessage,
270
- };
271
- }
272
- }
273
-
274
- /**
275
- * Adds all registered lexicons to an AT Protocol Agent instance.
276
- *
277
- * This allows the Agent to understand custom lexicon types when making
278
- * API requests.
279
- *
280
- * @param agent - The Agent instance to extend
281
- *
282
- * @remarks
283
- * This is called automatically when creating a Repository. You typically
284
- * don't need to call this directly unless you're using the Agent
285
- * independently.
286
- *
287
- * @internal
288
- */
289
- addToAgent(agent: Agent): void {
290
- // Access the internal lexicons collection and merge our lexicons
291
- // The Agent's lex property is a Lexicons instance
292
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
293
- const agentLex = (agent as any).lex as Lexicons;
294
-
295
- // Add each registered lexicon to the agent
296
- for (const lexicon of this.lexicons.values()) {
297
- agentLex.add(lexicon);
298
- }
299
- }
300
-
301
- /**
302
- * Gets all registered lexicon IDs.
303
- *
304
- * @returns Array of lexicon NSIDs
305
- *
306
- * @example
307
- * ```typescript
308
- * const ids = registry.getRegisteredIds();
309
- * console.log(`Registered lexicons: ${ids.join(", ")}`);
310
- * ```
311
- */
312
- getRegisteredIds(): string[] {
313
- return Array.from(this.lexicons.keys());
314
- }
315
-
316
- /**
317
- * Checks if a lexicon is registered.
318
- *
319
- * @param id - The lexicon NSID to check
320
- * @returns `true` if the lexicon is registered
321
- *
322
- * @example
323
- * ```typescript
324
- * if (registry.has("org.hypercerts.hypercert")) {
325
- * // Hypercert lexicon is available
326
- * }
327
- * ```
328
- */
329
- has(id: string): boolean {
330
- return this.lexicons.has(id);
331
- }
332
- }
@@ -1,282 +0,0 @@
1
- /**
2
- * OrganizationOperationsImpl - SDS organization management operations.
3
- *
4
- * This module provides the implementation for creating and managing
5
- * organizations on Shared Data Server (SDS) instances.
6
- *
7
- * @packageDocumentation
8
- */
9
-
10
- import { NetworkError } from "../core/errors.js";
11
- import type { CollaboratorPermissions, Session } from "../core/types.js";
12
- import type { LoggerInterface } from "../core/interfaces.js";
13
- import type { OrganizationOperations } from "./interfaces.js";
14
- import type { OrganizationInfo } from "./types.js";
15
-
16
- /**
17
- * Implementation of organization operations for SDS management.
18
- *
19
- * Organizations on SDS provide a way to create shared repositories
20
- * that multiple users can collaborate on. Each organization has:
21
- *
22
- * - A unique DID (Decentralized Identifier)
23
- * - A handle for human-readable identification
24
- * - An owner and optional collaborators
25
- * - Its own repository for storing records
26
- *
27
- * @remarks
28
- * This class is typically not instantiated directly. Access it through
29
- * {@link Repository.organizations} on an SDS-connected repository.
30
- *
31
- * **SDS API Endpoints Used**:
32
- * - `com.sds.organization.create`: Create a new organization
33
- * - `com.sds.organization.list`: List accessible organizations
34
- *
35
- * **Access Types**:
36
- * - `"owner"`: User created or owns the organization
37
- * - `"collaborator"`: User was invited with specific permissions
38
- *
39
- * @example
40
- * ```typescript
41
- * // Get SDS repository
42
- * const sdsRepo = sdk.repository(session, { server: "sds" });
43
- *
44
- * // Create an organization
45
- * const org = await sdsRepo.organizations.create({
46
- * name: "My Team",
47
- * description: "A team for impact projects",
48
- * });
49
- *
50
- * // List organizations you have access to
51
- * const orgs = await sdsRepo.organizations.list();
52
- *
53
- * // Get specific organization
54
- * const orgInfo = await sdsRepo.organizations.get(org.did);
55
- * ```
56
- *
57
- * @internal
58
- */
59
- export class OrganizationOperationsImpl implements OrganizationOperations {
60
- /**
61
- * Creates a new OrganizationOperationsImpl.
62
- *
63
- * @param session - Authenticated OAuth session with fetchHandler
64
- * @param _repoDid - DID of the user's repository (reserved for future use)
65
- * @param serverUrl - SDS server URL
66
- * @param _logger - Optional logger for debugging (reserved for future use)
67
- *
68
- * @internal
69
- */
70
- constructor(
71
- private session: Session,
72
- private _repoDid: string,
73
- private serverUrl: string,
74
- private _logger?: LoggerInterface,
75
- ) {}
76
-
77
- /**
78
- * Creates a new organization.
79
- *
80
- * @param params - Organization parameters
81
- * @param params.name - Display name for the organization
82
- * @param params.description - Optional description of the organization's purpose
83
- * @param params.handle - Optional custom handle. If not provided, one is auto-generated.
84
- * @returns Promise resolving to the created organization info
85
- * @throws {@link NetworkError} if organization creation fails
86
- *
87
- * @remarks
88
- * The creating user automatically becomes the owner with full permissions.
89
- *
90
- * **Handle Format**: Handles are typically formatted as
91
- * `{name}.sds.{domain}` (e.g., "my-team.sds.hypercerts.org").
92
- * If you provide a custom handle, it must be unique on the SDS.
93
- *
94
- * @example Basic organization
95
- * ```typescript
96
- * const org = await repo.organizations.create({
97
- * name: "Climate Action Team",
98
- * });
99
- * console.log(`Created org: ${org.did}`);
100
- * ```
101
- *
102
- * @example With description and custom handle
103
- * ```typescript
104
- * const org = await repo.organizations.create({
105
- * name: "Reforestation Initiative",
106
- * description: "Coordinating tree planting projects worldwide",
107
- * handle: "reforestation",
108
- * });
109
- * ```
110
- */
111
- async create(params: { name: string; description?: string; handle?: string }): Promise<OrganizationInfo> {
112
- const userDid = this.session.did || this.session.sub;
113
- if (!userDid) {
114
- throw new NetworkError("No authenticated user found");
115
- }
116
-
117
- const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.sds.organization.create`, {
118
- method: "POST",
119
- headers: { "Content-Type": "application/json" },
120
- body: JSON.stringify({
121
- ...params,
122
- creatorDid: userDid,
123
- }),
124
- });
125
-
126
- if (!response.ok) {
127
- throw new NetworkError(`Failed to create organization: ${response.statusText}`);
128
- }
129
-
130
- const data = await response.json();
131
- return {
132
- did: data.did,
133
- handle: data.handle,
134
- name: data.name,
135
- description: data.description,
136
- createdAt: data.createdAt || new Date().toISOString(),
137
- accessType: data.accessType || "owner",
138
- permissions: data.permissions || {
139
- read: true,
140
- create: true,
141
- update: true,
142
- delete: true,
143
- admin: true,
144
- owner: true,
145
- },
146
- };
147
- }
148
-
149
- /**
150
- * Gets an organization by its DID.
151
- *
152
- * @param did - The organization's DID
153
- * @returns Promise resolving to organization info, or `null` if not found
154
- *
155
- * @remarks
156
- * This method searches through the user's accessible organizations.
157
- * If the organization exists but the user doesn't have access,
158
- * it will return `null`.
159
- *
160
- * @example
161
- * ```typescript
162
- * const org = await repo.organizations.get("did:plc:org123");
163
- * if (org) {
164
- * console.log(`Found: ${org.name}`);
165
- * console.log(`Your role: ${org.accessType}`);
166
- * } else {
167
- * console.log("Organization not found or no access");
168
- * }
169
- * ```
170
- */
171
- async get(did: string): Promise<OrganizationInfo | null> {
172
- try {
173
- const { organizations } = await this.list();
174
- return organizations.find((o) => o.did === did) ?? null;
175
- } catch {
176
- return null;
177
- }
178
- }
179
-
180
- /**
181
- * Lists organizations the current user has access to.
182
- *
183
- * @param params - Optional pagination parameters
184
- * @param params.limit - Maximum number of results (1-100, default 50)
185
- * @param params.cursor - Pagination cursor from previous response
186
- * @returns Promise resolving to organizations and optional cursor
187
- * @throws {@link NetworkError} if the list operation fails
188
- *
189
- * @remarks
190
- * Returns organizations where the user is either:
191
- * - The owner
192
- * - A collaborator with any permission level
193
- *
194
- * The `accessType` field indicates the user's relationship to each organization.
195
- *
196
- * @example
197
- * ```typescript
198
- * // Get first page
199
- * const page1 = await repo.organizations.list({ limit: 20 });
200
- * console.log(`Found ${page1.organizations.length} organizations`);
201
- *
202
- * // Get next page if available
203
- * if (page1.cursor) {
204
- * const page2 = await repo.organizations.list({ limit: 20, cursor: page1.cursor });
205
- * }
206
- *
207
- * // Filter by access type
208
- * const owned = page1.organizations.filter(o => o.accessType === "owner");
209
- * const shared = page1.organizations.filter(o => o.accessType === "shared");
210
- * ```
211
- *
212
- * @example Display organization details
213
- * ```typescript
214
- * const { organizations } = await repo.organizations.list();
215
- *
216
- * for (const org of organizations) {
217
- * console.log(`${org.name} (@${org.handle})`);
218
- * console.log(` DID: ${org.did}`);
219
- * console.log(` Access: ${org.accessType}`);
220
- * if (org.description) {
221
- * console.log(` Description: ${org.description}`);
222
- * }
223
- * }
224
- * ```
225
- */
226
- async list(params?: { limit?: number; cursor?: string }): Promise<{
227
- organizations: OrganizationInfo[];
228
- cursor?: string;
229
- }> {
230
- const userDid = this.session.did || this.session.sub;
231
- if (!userDid) {
232
- throw new NetworkError("No authenticated user found");
233
- }
234
-
235
- const queryParams = new URLSearchParams({
236
- userDid,
237
- });
238
-
239
- if (params?.limit !== undefined) {
240
- queryParams.set("limit", params.limit.toString());
241
- }
242
-
243
- if (params?.cursor) {
244
- queryParams.set("cursor", params.cursor);
245
- }
246
-
247
- const response = await this.session.fetchHandler(
248
- `${this.serverUrl}/xrpc/com.sds.organization.list?${queryParams.toString()}`,
249
- { method: "GET" },
250
- );
251
-
252
- if (!response.ok) {
253
- throw new NetworkError(`Failed to list organizations: ${response.statusText}`);
254
- }
255
-
256
- const data = await response.json();
257
- const organizations = (data.organizations || []).map(
258
- (r: {
259
- did: string;
260
- handle: string;
261
- name: string;
262
- description?: string;
263
- createdAt?: string;
264
- accessType: "owner" | "shared" | "none";
265
- permissions: CollaboratorPermissions;
266
- }) => ({
267
- did: r.did,
268
- handle: r.handle,
269
- name: r.name,
270
- description: r.description,
271
- createdAt: r.createdAt || new Date().toISOString(),
272
- accessType: r.accessType,
273
- permissions: r.permissions,
274
- }),
275
- );
276
-
277
- return {
278
- organizations,
279
- cursor: data.cursor,
280
- };
281
- }
282
- }