@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,288 @@
1
+ /**
2
+ * CollaboratorOperationsImpl - SDS collaborator management operations.
3
+ *
4
+ * This module provides the implementation for managing collaborator
5
+ * access on Shared Data Server (SDS) repositories.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ import { NetworkError } from "../core/errors.js";
11
+ import type { CollaboratorPermissions, Session } from "../core/types.js";
12
+ import type { CollaboratorOperations } from "./interfaces.js";
13
+ import type { RepositoryRole, RepositoryAccessGrant } from "./types.js";
14
+
15
+ /**
16
+ * Implementation of collaborator operations for SDS access control.
17
+ *
18
+ * This class manages access permissions for shared repositories on
19
+ * Shared Data Servers (SDS). It provides role-based access control
20
+ * with predefined permission sets.
21
+ *
22
+ * @remarks
23
+ * This class is typically not instantiated directly. Access it through
24
+ * {@link Repository.collaborators} on an SDS-connected repository.
25
+ *
26
+ * **Role Hierarchy**:
27
+ * - `viewer`: Read-only access
28
+ * - `editor`: Read + Create + Update
29
+ * - `admin`: All permissions except ownership transfer
30
+ * - `owner`: Full control including ownership management
31
+ *
32
+ * **SDS API Endpoints Used**:
33
+ * - `com.atproto.sds.grantAccess`: Grant access to a user
34
+ * - `com.atproto.sds.revokeAccess`: Revoke access from a user
35
+ * - `com.atproto.sds.listCollaborators`: List all collaborators
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * // Get SDS repository
40
+ * const sdsRepo = sdk.repository(session, { server: "sds" });
41
+ *
42
+ * // Grant editor access
43
+ * await sdsRepo.collaborators.grant({
44
+ * userDid: "did:plc:new-user",
45
+ * role: "editor",
46
+ * });
47
+ *
48
+ * // List all collaborators
49
+ * const collaborators = await sdsRepo.collaborators.list();
50
+ *
51
+ * // Check specific user
52
+ * const hasAccess = await sdsRepo.collaborators.hasAccess("did:plc:someone");
53
+ * const role = await sdsRepo.collaborators.getRole("did:plc:someone");
54
+ * ```
55
+ *
56
+ * @internal
57
+ */
58
+ export class CollaboratorOperationsImpl implements CollaboratorOperations {
59
+ /**
60
+ * Creates a new CollaboratorOperationsImpl.
61
+ *
62
+ * @param session - Authenticated OAuth session with fetchHandler
63
+ * @param repoDid - DID of the repository to manage
64
+ * @param serverUrl - SDS server URL
65
+ *
66
+ * @internal
67
+ */
68
+ constructor(
69
+ private session: Session,
70
+ private repoDid: string,
71
+ private serverUrl: string,
72
+ ) {}
73
+
74
+ /**
75
+ * Converts a role to its corresponding permissions object.
76
+ *
77
+ * @param role - The role to convert
78
+ * @returns Permission flags for the role
79
+ * @internal
80
+ */
81
+ private roleToPermissions(role: RepositoryRole): CollaboratorPermissions {
82
+ switch (role) {
83
+ case "viewer":
84
+ return { read: true, create: false, update: false, delete: false, admin: false, owner: false };
85
+ case "editor":
86
+ return { read: true, create: true, update: true, delete: false, admin: false, owner: false };
87
+ case "admin":
88
+ return { read: true, create: true, update: true, delete: true, admin: true, owner: false };
89
+ case "owner":
90
+ return { read: true, create: true, update: true, delete: true, admin: true, owner: true };
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Determines the role from a permissions object.
96
+ *
97
+ * @param permissions - The permissions to analyze
98
+ * @returns The highest role matching the permissions
99
+ * @internal
100
+ */
101
+ private permissionsToRole(permissions: CollaboratorPermissions): RepositoryRole {
102
+ if (permissions.owner) return "owner";
103
+ if (permissions.admin) return "admin";
104
+ if (permissions.create || permissions.update) return "editor";
105
+ return "viewer";
106
+ }
107
+
108
+ /**
109
+ * Grants repository access to a user.
110
+ *
111
+ * @param params - Grant parameters
112
+ * @param params.userDid - DID of the user to grant access to
113
+ * @param params.role - Role to assign (determines permissions)
114
+ * @throws {@link NetworkError} if the grant operation fails
115
+ *
116
+ * @remarks
117
+ * If the user already has access, their permissions are updated
118
+ * to the new role.
119
+ *
120
+ * @example
121
+ * ```typescript
122
+ * // Grant viewer access
123
+ * await repo.collaborators.grant({
124
+ * userDid: "did:plc:viewer-user",
125
+ * role: "viewer",
126
+ * });
127
+ *
128
+ * // Upgrade to editor
129
+ * await repo.collaborators.grant({
130
+ * userDid: "did:plc:viewer-user",
131
+ * role: "editor",
132
+ * });
133
+ * ```
134
+ */
135
+ async grant(params: { userDid: string; role: RepositoryRole }): Promise<void> {
136
+ const permissions = this.roleToPermissions(params.role);
137
+
138
+ const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.atproto.sds.grantAccess`, {
139
+ method: "POST",
140
+ headers: { "Content-Type": "application/json" },
141
+ body: JSON.stringify({
142
+ repo: this.repoDid,
143
+ userDid: params.userDid,
144
+ permissions,
145
+ }),
146
+ });
147
+
148
+ if (!response.ok) {
149
+ throw new NetworkError(`Failed to grant access: ${response.statusText}`);
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Revokes repository access from a user.
155
+ *
156
+ * @param params - Revoke parameters
157
+ * @param params.userDid - DID of the user to revoke access from
158
+ * @throws {@link NetworkError} if the revoke operation fails
159
+ *
160
+ * @remarks
161
+ * - Cannot revoke access from the repository owner
162
+ * - Revoked access is recorded with a `revokedAt` timestamp
163
+ *
164
+ * @example
165
+ * ```typescript
166
+ * await repo.collaborators.revoke({
167
+ * userDid: "did:plc:former-collaborator",
168
+ * });
169
+ * ```
170
+ */
171
+ async revoke(params: { userDid: string }): Promise<void> {
172
+ const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.atproto.sds.revokeAccess`, {
173
+ method: "POST",
174
+ headers: { "Content-Type": "application/json" },
175
+ body: JSON.stringify({
176
+ repo: this.repoDid,
177
+ userDid: params.userDid,
178
+ }),
179
+ });
180
+
181
+ if (!response.ok) {
182
+ throw new NetworkError(`Failed to revoke access: ${response.statusText}`);
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Lists all collaborators on the repository.
188
+ *
189
+ * @returns Promise resolving to array of access grants
190
+ * @throws {@link NetworkError} if the list operation fails
191
+ *
192
+ * @remarks
193
+ * The list includes both active and revoked collaborators.
194
+ * Check `revokedAt` to filter active collaborators.
195
+ *
196
+ * @example
197
+ * ```typescript
198
+ * const collaborators = await repo.collaborators.list();
199
+ *
200
+ * // Filter active collaborators
201
+ * const active = collaborators.filter(c => !c.revokedAt);
202
+ *
203
+ * // Group by role
204
+ * const byRole = {
205
+ * owners: active.filter(c => c.role === "owner"),
206
+ * admins: active.filter(c => c.role === "admin"),
207
+ * editors: active.filter(c => c.role === "editor"),
208
+ * viewers: active.filter(c => c.role === "viewer"),
209
+ * };
210
+ * ```
211
+ */
212
+ async list(): Promise<RepositoryAccessGrant[]> {
213
+ const response = await this.session.fetchHandler(
214
+ `${this.serverUrl}/xrpc/com.atproto.sds.listCollaborators?repo=${encodeURIComponent(this.repoDid)}`,
215
+ { method: "GET" },
216
+ );
217
+
218
+ if (!response.ok) {
219
+ throw new NetworkError(`Failed to list collaborators: ${response.statusText}`);
220
+ }
221
+
222
+ const data = await response.json();
223
+ return (data.collaborators || []).map(
224
+ (c: {
225
+ userDid: string;
226
+ permissions: CollaboratorPermissions;
227
+ grantedBy: string;
228
+ grantedAt: string;
229
+ revokedAt?: string;
230
+ }) => ({
231
+ userDid: c.userDid,
232
+ role: this.permissionsToRole(c.permissions),
233
+ permissions: c.permissions,
234
+ grantedBy: c.grantedBy,
235
+ grantedAt: c.grantedAt,
236
+ revokedAt: c.revokedAt,
237
+ }),
238
+ );
239
+ }
240
+
241
+ /**
242
+ * Checks if a user has any access to the repository.
243
+ *
244
+ * @param userDid - DID of the user to check
245
+ * @returns Promise resolving to `true` if user has active access
246
+ *
247
+ * @remarks
248
+ * Returns `false` if:
249
+ * - User was never granted access
250
+ * - User's access was revoked
251
+ * - The list operation fails (error is suppressed)
252
+ *
253
+ * @example
254
+ * ```typescript
255
+ * if (await repo.collaborators.hasAccess("did:plc:someone")) {
256
+ * console.log("User has access");
257
+ * }
258
+ * ```
259
+ */
260
+ async hasAccess(userDid: string): Promise<boolean> {
261
+ try {
262
+ const collaborators = await this.list();
263
+ return collaborators.some((c) => c.userDid === userDid && !c.revokedAt);
264
+ } catch {
265
+ return false;
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Gets the role assigned to a user.
271
+ *
272
+ * @param userDid - DID of the user to check
273
+ * @returns Promise resolving to the user's role, or `null` if no active access
274
+ *
275
+ * @example
276
+ * ```typescript
277
+ * const role = await repo.collaborators.getRole("did:plc:someone");
278
+ * if (role === "admin" || role === "owner") {
279
+ * // User can manage other collaborators
280
+ * }
281
+ * ```
282
+ */
283
+ async getRole(userDid: string): Promise<RepositoryRole | null> {
284
+ const collaborators = await this.list();
285
+ const collab = collaborators.find((c) => c.userDid === userDid && !c.revokedAt);
286
+ return collab?.role ?? null;
287
+ }
288
+ }