@hypercerts-org/sdk-core 0.2.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +328 -0
- package/.turbo/turbo-test.log +118 -0
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/dist/errors.cjs +260 -0
- package/dist/errors.cjs.map +1 -0
- package/dist/errors.d.ts +233 -0
- package/dist/errors.mjs +253 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.cjs +4531 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +3430 -0
- package/dist/index.mjs +4448 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lexicons.cjs +420 -0
- package/dist/lexicons.cjs.map +1 -0
- package/dist/lexicons.d.ts +227 -0
- package/dist/lexicons.mjs +410 -0
- package/dist/lexicons.mjs.map +1 -0
- package/dist/storage.cjs +270 -0
- package/dist/storage.cjs.map +1 -0
- package/dist/storage.d.ts +474 -0
- package/dist/storage.mjs +267 -0
- package/dist/storage.mjs.map +1 -0
- package/dist/testing.cjs +415 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.ts +928 -0
- package/dist/testing.mjs +410 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/types.cjs +220 -0
- package/dist/types.cjs.map +1 -0
- package/dist/types.d.ts +2118 -0
- package/dist/types.mjs +212 -0
- package/dist/types.mjs.map +1 -0
- package/eslint.config.mjs +22 -0
- package/package.json +90 -0
- package/rollup.config.js +75 -0
- package/src/auth/OAuthClient.ts +497 -0
- package/src/core/SDK.ts +410 -0
- package/src/core/config.ts +243 -0
- package/src/core/errors.ts +257 -0
- package/src/core/interfaces.ts +324 -0
- package/src/core/types.ts +281 -0
- package/src/errors.ts +57 -0
- package/src/index.ts +107 -0
- package/src/lexicons.ts +64 -0
- package/src/repository/BlobOperationsImpl.ts +199 -0
- package/src/repository/CollaboratorOperationsImpl.ts +288 -0
- package/src/repository/HypercertOperationsImpl.ts +1146 -0
- package/src/repository/LexiconRegistry.ts +332 -0
- package/src/repository/OrganizationOperationsImpl.ts +234 -0
- package/src/repository/ProfileOperationsImpl.ts +281 -0
- package/src/repository/RecordOperationsImpl.ts +340 -0
- package/src/repository/Repository.ts +482 -0
- package/src/repository/interfaces.ts +868 -0
- package/src/repository/types.ts +111 -0
- package/src/services/hypercerts/types.ts +87 -0
- package/src/storage/InMemorySessionStore.ts +127 -0
- package/src/storage/InMemoryStateStore.ts +146 -0
- package/src/storage.ts +63 -0
- package/src/testing/index.ts +67 -0
- package/src/testing/mocks.ts +142 -0
- package/src/testing/stores.ts +285 -0
- package/src/testing.ts +64 -0
- package/src/types.ts +86 -0
- package/tests/auth/OAuthClient.test.ts +164 -0
- package/tests/core/SDK.test.ts +176 -0
- package/tests/core/errors.test.ts +81 -0
- package/tests/repository/BlobOperationsImpl.test.ts +154 -0
- package/tests/repository/CollaboratorOperationsImpl.test.ts +323 -0
- package/tests/repository/HypercertOperationsImpl.test.ts +652 -0
- package/tests/repository/LexiconRegistry.test.ts +192 -0
- package/tests/repository/OrganizationOperationsImpl.test.ts +242 -0
- package/tests/repository/ProfileOperationsImpl.test.ts +254 -0
- package/tests/repository/RecordOperationsImpl.test.ts +375 -0
- package/tests/repository/Repository.test.ts +149 -0
- package/tests/utils/fixtures.ts +117 -0
- package/tests/utils/mocks.ts +109 -0
- package/tests/utils/repository-fixtures.ts +78 -0
- package/tsconfig.json +11 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +30 -0
|
@@ -0,0 +1,332 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
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.atproto.sds.createRepository`: Create a new organization
|
|
33
|
+
* - `com.atproto.sds.listRepositories`: 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 response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.atproto.sds.createRepository`, {
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers: { "Content-Type": "application/json" },
|
|
115
|
+
body: JSON.stringify(params),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
throw new NetworkError(`Failed to create organization: ${response.statusText}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const data = await response.json();
|
|
123
|
+
return {
|
|
124
|
+
did: data.did,
|
|
125
|
+
handle: data.handle,
|
|
126
|
+
name: data.name,
|
|
127
|
+
description: data.description,
|
|
128
|
+
createdAt: data.createdAt || new Date().toISOString(),
|
|
129
|
+
accessType: "owner",
|
|
130
|
+
permissions: { read: true, create: true, update: true, delete: true, admin: true, owner: true },
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Gets an organization by its DID.
|
|
136
|
+
*
|
|
137
|
+
* @param did - The organization's DID
|
|
138
|
+
* @returns Promise resolving to organization info, or `null` if not found
|
|
139
|
+
*
|
|
140
|
+
* @remarks
|
|
141
|
+
* This method searches through the user's accessible organizations.
|
|
142
|
+
* If the organization exists but the user doesn't have access,
|
|
143
|
+
* it will return `null`.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```typescript
|
|
147
|
+
* const org = await repo.organizations.get("did:plc:org123");
|
|
148
|
+
* if (org) {
|
|
149
|
+
* console.log(`Found: ${org.name}`);
|
|
150
|
+
* console.log(`Your role: ${org.accessType}`);
|
|
151
|
+
* } else {
|
|
152
|
+
* console.log("Organization not found or no access");
|
|
153
|
+
* }
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
async get(did: string): Promise<OrganizationInfo | null> {
|
|
157
|
+
try {
|
|
158
|
+
const orgs = await this.list();
|
|
159
|
+
return orgs.find((o) => o.did === did) ?? null;
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Lists organizations the current user has access to.
|
|
167
|
+
*
|
|
168
|
+
* @returns Promise resolving to array of organization info
|
|
169
|
+
* @throws {@link NetworkError} if the list operation fails
|
|
170
|
+
*
|
|
171
|
+
* @remarks
|
|
172
|
+
* Returns organizations where the user is either:
|
|
173
|
+
* - The owner
|
|
174
|
+
* - A collaborator with any permission level
|
|
175
|
+
*
|
|
176
|
+
* The `accessType` field indicates the user's relationship to each organization.
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```typescript
|
|
180
|
+
* const orgs = await repo.organizations.list();
|
|
181
|
+
*
|
|
182
|
+
* // Filter by access type
|
|
183
|
+
* const owned = orgs.filter(o => o.accessType === "owner");
|
|
184
|
+
* const collaborated = orgs.filter(o => o.accessType === "collaborator");
|
|
185
|
+
*
|
|
186
|
+
* console.log(`You own ${owned.length} organizations`);
|
|
187
|
+
* console.log(`You collaborate on ${collaborated.length} organizations`);
|
|
188
|
+
* ```
|
|
189
|
+
*
|
|
190
|
+
* @example Display organization details
|
|
191
|
+
* ```typescript
|
|
192
|
+
* const orgs = await repo.organizations.list();
|
|
193
|
+
*
|
|
194
|
+
* for (const org of orgs) {
|
|
195
|
+
* console.log(`${org.name} (@${org.handle})`);
|
|
196
|
+
* console.log(` DID: ${org.did}`);
|
|
197
|
+
* console.log(` Access: ${org.accessType}`);
|
|
198
|
+
* if (org.description) {
|
|
199
|
+
* console.log(` Description: ${org.description}`);
|
|
200
|
+
* }
|
|
201
|
+
* }
|
|
202
|
+
* ```
|
|
203
|
+
*/
|
|
204
|
+
async list(): Promise<OrganizationInfo[]> {
|
|
205
|
+
const response = await this.session.fetchHandler(
|
|
206
|
+
`${this.serverUrl}/xrpc/com.atproto.sds.listRepositories?userDid=${encodeURIComponent(this.session.did || this.session.sub)}`,
|
|
207
|
+
{ method: "GET" },
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
if (!response.ok) {
|
|
211
|
+
throw new NetworkError(`Failed to list organizations: ${response.statusText}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const data = await response.json();
|
|
215
|
+
return (data.repositories || []).map(
|
|
216
|
+
(r: {
|
|
217
|
+
did: string;
|
|
218
|
+
handle: string;
|
|
219
|
+
name: string;
|
|
220
|
+
description?: string;
|
|
221
|
+
accessType: "owner" | "collaborator";
|
|
222
|
+
permissions: CollaboratorPermissions;
|
|
223
|
+
}) => ({
|
|
224
|
+
did: r.did,
|
|
225
|
+
handle: r.handle,
|
|
226
|
+
name: r.name,
|
|
227
|
+
description: r.description,
|
|
228
|
+
createdAt: new Date().toISOString(), // SDS may not return this
|
|
229
|
+
accessType: r.accessType,
|
|
230
|
+
permissions: r.permissions,
|
|
231
|
+
}),
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|