@hypercerts-org/sdk-core 0.2.0-beta.0 → 0.4.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/dist/types.d.ts CHANGED
@@ -1242,16 +1242,6 @@ interface HypercertWithMetadata {
1242
1242
  record: HypercertClaim;
1243
1243
  }
1244
1244
 
1245
- /**
1246
- * Repository interfaces - Operation contracts for repository functionality.
1247
- *
1248
- * This module defines the interfaces for all repository operations,
1249
- * providing clear contracts for record management, blob handling,
1250
- * profile management, and domain-specific operations.
1251
- *
1252
- * @packageDocumentation
1253
- */
1254
-
1255
1245
  /**
1256
1246
  * Parameters for creating a new hypercert.
1257
1247
  *
@@ -1998,6 +1988,17 @@ interface HypercertOperations extends EventEmitter<HypercertEvents> {
1998
1988
  * const hasAccess = await repo.collaborators.hasAccess("did:plc:someone");
1999
1989
  * const role = await repo.collaborators.getRole("did:plc:someone");
2000
1990
  *
1991
+ * // Get current user permissions
1992
+ * const permissions = await repo.collaborators.getPermissions();
1993
+ * if (permissions.admin) {
1994
+ * // Can manage collaborators
1995
+ * }
1996
+ *
1997
+ * // Transfer ownership
1998
+ * await repo.collaborators.transferOwnership({
1999
+ * newOwnerDid: "did:plc:new-owner",
2000
+ * });
2001
+ *
2001
2002
  * // Revoke access
2002
2003
  * await repo.collaborators.revoke({ userDid: "did:plc:former-user" });
2003
2004
  * ```
@@ -2043,6 +2044,24 @@ interface CollaboratorOperations {
2043
2044
  * @returns Promise resolving to role, or `null` if no access
2044
2045
  */
2045
2046
  getRole(userDid: string): Promise<RepositoryRole | null>;
2047
+ /**
2048
+ * Gets the current user's permissions for this repository.
2049
+ *
2050
+ * @returns Promise resolving to permission flags
2051
+ */
2052
+ getPermissions(): Promise<CollaboratorPermissions>;
2053
+ /**
2054
+ * Transfers repository ownership to another user.
2055
+ *
2056
+ * **WARNING**: This action is irreversible. The new owner will have
2057
+ * full control of the repository.
2058
+ *
2059
+ * @param params - Transfer parameters
2060
+ * @param params.newOwnerDid - DID of the user to transfer ownership to
2061
+ */
2062
+ transferOwnership(params: {
2063
+ newOwnerDid: string;
2064
+ }): Promise<void>;
2046
2065
  }
2047
2066
  /**
2048
2067
  * Organization operations for SDS organization management.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypercerts-org/sdk-core",
3
- "version": "0.2.0-beta.0",
3
+ "version": "0.4.0-beta.0",
4
4
  "description": "Framework-agnostic ATProto SDK core for authentication, repository operations, and lexicon management",
5
5
  "main": "dist/index.cjs",
6
6
  "repository": {
@@ -75,7 +75,7 @@
75
75
  "@atproto/oauth-client-node": "^0.3.10",
76
76
  "eventemitter3": "^5.0.1",
77
77
  "zod": "^3.24.4",
78
- "@hypercerts-org/lexicon": "0.2.0-beta.0"
78
+ "@hypercerts-org/lexicon": "0.4.0-beta.0"
79
79
  },
80
80
  "scripts": {
81
81
  "test": "vitest",
@@ -30,9 +30,11 @@ import type { RepositoryRole, RepositoryAccessGrant } from "./types.js";
30
30
  * - `owner`: Full control including ownership management
31
31
  *
32
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
33
+ * - `com.sds.repo.grantAccess`: Grant access to a user
34
+ * - `com.sds.repo.revokeAccess`: Revoke access from a user
35
+ * - `com.sds.repo.listCollaborators`: List all collaborators
36
+ * - `com.sds.repo.getPermissions`: Get current user's permissions
37
+ * - `com.sds.repo.transferOwnership`: Transfer repository ownership
36
38
  *
37
39
  * @example
38
40
  * ```typescript
@@ -135,7 +137,7 @@ export class CollaboratorOperationsImpl implements CollaboratorOperations {
135
137
  async grant(params: { userDid: string; role: RepositoryRole }): Promise<void> {
136
138
  const permissions = this.roleToPermissions(params.role);
137
139
 
138
- const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.atproto.sds.grantAccess`, {
140
+ const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.sds.repo.grantAccess`, {
139
141
  method: "POST",
140
142
  headers: { "Content-Type": "application/json" },
141
143
  body: JSON.stringify({
@@ -169,7 +171,7 @@ export class CollaboratorOperationsImpl implements CollaboratorOperations {
169
171
  * ```
170
172
  */
171
173
  async revoke(params: { userDid: string }): Promise<void> {
172
- const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.atproto.sds.revokeAccess`, {
174
+ const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.sds.repo.revokeAccess`, {
173
175
  method: "POST",
174
176
  headers: { "Content-Type": "application/json" },
175
177
  body: JSON.stringify({
@@ -211,7 +213,7 @@ export class CollaboratorOperationsImpl implements CollaboratorOperations {
211
213
  */
212
214
  async list(): Promise<RepositoryAccessGrant[]> {
213
215
  const response = await this.session.fetchHandler(
214
- `${this.serverUrl}/xrpc/com.atproto.sds.listCollaborators?repo=${encodeURIComponent(this.repoDid)}`,
216
+ `${this.serverUrl}/xrpc/com.sds.repo.listCollaborators?repo=${encodeURIComponent(this.repoDid)}`,
215
217
  { method: "GET" },
216
218
  );
217
219
 
@@ -285,4 +287,110 @@ export class CollaboratorOperationsImpl implements CollaboratorOperations {
285
287
  const collab = collaborators.find((c) => c.userDid === userDid && !c.revokedAt);
286
288
  return collab?.role ?? null;
287
289
  }
290
+
291
+ /**
292
+ * Gets the current user's permissions for this repository.
293
+ *
294
+ * @returns Promise resolving to the permission flags
295
+ * @throws {@link NetworkError} if the request fails
296
+ *
297
+ * @remarks
298
+ * This is useful for checking what actions the current user can perform
299
+ * before attempting operations that might fail due to insufficient permissions.
300
+ *
301
+ * @example
302
+ * ```typescript
303
+ * const permissions = await repo.collaborators.getPermissions();
304
+ *
305
+ * if (permissions.admin) {
306
+ * // Show admin UI
307
+ * console.log("You can manage collaborators");
308
+ * }
309
+ *
310
+ * if (permissions.create) {
311
+ * console.log("You can create records");
312
+ * }
313
+ * ```
314
+ *
315
+ * @example Conditional UI rendering
316
+ * ```typescript
317
+ * const permissions = await repo.collaborators.getPermissions();
318
+ *
319
+ * // Show/hide UI elements based on permissions
320
+ * const canEdit = permissions.update;
321
+ * const canDelete = permissions.delete;
322
+ * const isAdmin = permissions.admin;
323
+ * const isOwner = permissions.owner;
324
+ * ```
325
+ */
326
+ async getPermissions(): Promise<CollaboratorPermissions> {
327
+ const response = await this.session.fetchHandler(
328
+ `${this.serverUrl}/xrpc/com.sds.repo.getPermissions?repo=${encodeURIComponent(this.repoDid)}`,
329
+ { method: "GET" },
330
+ );
331
+
332
+ if (!response.ok) {
333
+ throw new NetworkError(`Failed to get permissions: ${response.statusText}`);
334
+ }
335
+
336
+ const data = await response.json();
337
+ return data.permissions as CollaboratorPermissions;
338
+ }
339
+
340
+ /**
341
+ * Transfers repository ownership to another user.
342
+ *
343
+ * @param params - Transfer parameters
344
+ * @param params.newOwnerDid - DID of the user to transfer ownership to
345
+ * @throws {@link NetworkError} if the transfer fails
346
+ *
347
+ * @remarks
348
+ * **IMPORTANT**: This action is irreversible. Once ownership is transferred:
349
+ * - The new owner gains full control of the repository
350
+ * - Your role will be changed to admin (or specified role)
351
+ * - You cannot transfer ownership back without the new owner's approval
352
+ *
353
+ * **Requirements**:
354
+ * - You must be the current owner
355
+ * - The new owner must have an existing account
356
+ * - The new owner will be notified of the ownership transfer
357
+ *
358
+ * @example
359
+ * ```typescript
360
+ * // Transfer ownership to another user
361
+ * await repo.collaborators.transferOwnership({
362
+ * newOwnerDid: "did:plc:new-owner",
363
+ * });
364
+ *
365
+ * console.log("Ownership transferred successfully");
366
+ * // You are now an admin, not the owner
367
+ * ```
368
+ *
369
+ * @example With confirmation
370
+ * ```typescript
371
+ * const confirmTransfer = await askUser(
372
+ * "Are you sure you want to transfer ownership? This cannot be undone."
373
+ * );
374
+ *
375
+ * if (confirmTransfer) {
376
+ * await repo.collaborators.transferOwnership({
377
+ * newOwnerDid: "did:plc:new-owner",
378
+ * });
379
+ * }
380
+ * ```
381
+ */
382
+ async transferOwnership(params: { newOwnerDid: string }): Promise<void> {
383
+ const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.sds.repo.transferOwnership`, {
384
+ method: "POST",
385
+ headers: { "Content-Type": "application/json" },
386
+ body: JSON.stringify({
387
+ repo: this.repoDid,
388
+ newOwner: params.newOwnerDid,
389
+ }),
390
+ });
391
+
392
+ if (!response.ok) {
393
+ throw new NetworkError(`Failed to transfer ownership: ${response.statusText}`);
394
+ }
395
+ }
288
396
  }
@@ -29,8 +29,8 @@ import type { OrganizationInfo } from "./types.js";
29
29
  * {@link Repository.organizations} on an SDS-connected repository.
30
30
  *
31
31
  * **SDS API Endpoints Used**:
32
- * - `com.atproto.sds.createRepository`: Create a new organization
33
- * - `com.atproto.sds.listRepositories`: List accessible organizations
32
+ * - `com.sds.organization.create`: Create a new organization
33
+ * - `com.sds.organization.list`: List accessible organizations
34
34
  *
35
35
  * **Access Types**:
36
36
  * - `"owner"`: User created or owns the organization
@@ -109,7 +109,7 @@ export class OrganizationOperationsImpl implements OrganizationOperations {
109
109
  * ```
110
110
  */
111
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`, {
112
+ const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.sds.organization.create`, {
113
113
  method: "POST",
114
114
  headers: { "Content-Type": "application/json" },
115
115
  body: JSON.stringify(params),
@@ -203,7 +203,7 @@ export class OrganizationOperationsImpl implements OrganizationOperations {
203
203
  */
204
204
  async list(): Promise<OrganizationInfo[]> {
205
205
  const response = await this.session.fetchHandler(
206
- `${this.serverUrl}/xrpc/com.atproto.sds.listRepositories?userDid=${encodeURIComponent(this.session.did || this.session.sub)}`,
206
+ `${this.serverUrl}/xrpc/com.sds.organization.list?userDid=${encodeURIComponent(this.session.did || this.session.sub)}`,
207
207
  { method: "GET" },
208
208
  );
209
209
 
@@ -773,6 +773,17 @@ export interface HypercertOperations extends EventEmitter<HypercertEvents> {
773
773
  * const hasAccess = await repo.collaborators.hasAccess("did:plc:someone");
774
774
  * const role = await repo.collaborators.getRole("did:plc:someone");
775
775
  *
776
+ * // Get current user permissions
777
+ * const permissions = await repo.collaborators.getPermissions();
778
+ * if (permissions.admin) {
779
+ * // Can manage collaborators
780
+ * }
781
+ *
782
+ * // Transfer ownership
783
+ * await repo.collaborators.transferOwnership({
784
+ * newOwnerDid: "did:plc:new-owner",
785
+ * });
786
+ *
776
787
  * // Revoke access
777
788
  * await repo.collaborators.revoke({ userDid: "did:plc:former-user" });
778
789
  * ```
@@ -817,6 +828,24 @@ export interface CollaboratorOperations {
817
828
  * @returns Promise resolving to role, or `null` if no access
818
829
  */
819
830
  getRole(userDid: string): Promise<RepositoryRole | null>;
831
+
832
+ /**
833
+ * Gets the current user's permissions for this repository.
834
+ *
835
+ * @returns Promise resolving to permission flags
836
+ */
837
+ getPermissions(): Promise<import("../core/types.js").CollaboratorPermissions>;
838
+
839
+ /**
840
+ * Transfers repository ownership to another user.
841
+ *
842
+ * **WARNING**: This action is irreversible. The new owner will have
843
+ * full control of the repository.
844
+ *
845
+ * @param params - Transfer parameters
846
+ * @param params.newOwnerDid - DID of the user to transfer ownership to
847
+ */
848
+ transferOwnership(params: { newOwnerDid: string }): Promise<void>;
820
849
  }
821
850
 
822
851
  /**
@@ -28,7 +28,7 @@ describe("CollaboratorOperationsImpl", () => {
28
28
  await collaboratorOps.grant({ userDid: "did:plc:newuser", role: "viewer" });
29
29
 
30
30
  expect(mockSession.fetchHandler).toHaveBeenCalledWith(
31
- `${serverUrl}/xrpc/com.atproto.sds.grantAccess`,
31
+ `${serverUrl}/xrpc/com.sds.repo.grantAccess`,
32
32
  expect.objectContaining({
33
33
  method: "POST",
34
34
  body: expect.stringContaining('"read":true'),
@@ -88,9 +88,7 @@ describe("CollaboratorOperationsImpl", () => {
88
88
  statusText: "Forbidden",
89
89
  });
90
90
 
91
- await expect(
92
- collaboratorOps.grant({ userDid: "did:plc:newuser", role: "viewer" }),
93
- ).rejects.toThrow(NetworkError);
91
+ await expect(collaboratorOps.grant({ userDid: "did:plc:newuser", role: "viewer" })).rejects.toThrow(NetworkError);
94
92
  });
95
93
  });
96
94
 
@@ -104,7 +102,7 @@ describe("CollaboratorOperationsImpl", () => {
104
102
  await collaboratorOps.revoke({ userDid: "did:plc:revokeduser" });
105
103
 
106
104
  expect(mockSession.fetchHandler).toHaveBeenCalledWith(
107
- `${serverUrl}/xrpc/com.atproto.sds.revokeAccess`,
105
+ `${serverUrl}/xrpc/com.sds.repo.revokeAccess`,
108
106
  expect.objectContaining({
109
107
  method: "POST",
110
108
  }),
@@ -121,9 +119,7 @@ describe("CollaboratorOperationsImpl", () => {
121
119
  statusText: "Not Found",
122
120
  });
123
121
 
124
- await expect(
125
- collaboratorOps.revoke({ userDid: "did:plc:user" }),
126
- ).rejects.toThrow(NetworkError);
122
+ await expect(collaboratorOps.revoke({ userDid: "did:plc:user" })).rejects.toThrow(NetworkError);
127
123
  });
128
124
  });
129
125
 
@@ -320,4 +316,123 @@ describe("CollaboratorOperationsImpl", () => {
320
316
  expect(result).toBeNull();
321
317
  });
322
318
  });
319
+
320
+ describe("getPermissions", () => {
321
+ it("should get current user permissions successfully", async () => {
322
+ mockSession.fetchHandler.mockResolvedValue({
323
+ ok: true,
324
+ json: async () => ({
325
+ permissions: {
326
+ read: true,
327
+ create: true,
328
+ update: true,
329
+ delete: false,
330
+ admin: false,
331
+ owner: false,
332
+ },
333
+ }),
334
+ });
335
+
336
+ const result = await collaboratorOps.getPermissions();
337
+
338
+ expect(mockSession.fetchHandler).toHaveBeenCalledWith(
339
+ `${serverUrl}/xrpc/com.sds.repo.getPermissions?repo=${encodeURIComponent(repoDid)}`,
340
+ expect.objectContaining({
341
+ method: "GET",
342
+ }),
343
+ );
344
+
345
+ expect(result.read).toBe(true);
346
+ expect(result.create).toBe(true);
347
+ expect(result.update).toBe(true);
348
+ expect(result.delete).toBe(false);
349
+ expect(result.admin).toBe(false);
350
+ expect(result.owner).toBe(false);
351
+ });
352
+
353
+ it("should handle owner permissions", async () => {
354
+ mockSession.fetchHandler.mockResolvedValue({
355
+ ok: true,
356
+ json: async () => ({
357
+ permissions: {
358
+ read: true,
359
+ create: true,
360
+ update: true,
361
+ delete: true,
362
+ admin: true,
363
+ owner: true,
364
+ },
365
+ }),
366
+ });
367
+
368
+ const result = await collaboratorOps.getPermissions();
369
+
370
+ expect(result.owner).toBe(true);
371
+ expect(result.admin).toBe(true);
372
+ });
373
+
374
+ it("should throw NetworkError on failure", async () => {
375
+ mockSession.fetchHandler.mockResolvedValue({
376
+ ok: false,
377
+ statusText: "Forbidden",
378
+ });
379
+
380
+ await expect(collaboratorOps.getPermissions()).rejects.toThrow(NetworkError);
381
+ });
382
+ });
383
+
384
+ describe("transferOwnership", () => {
385
+ it("should transfer ownership successfully", async () => {
386
+ mockSession.fetchHandler.mockResolvedValue({
387
+ ok: true,
388
+ json: async () => ({}),
389
+ });
390
+
391
+ await collaboratorOps.transferOwnership({ newOwnerDid: "did:plc:new-owner" });
392
+
393
+ expect(mockSession.fetchHandler).toHaveBeenCalledWith(
394
+ `${serverUrl}/xrpc/com.sds.repo.transferOwnership`,
395
+ expect.objectContaining({
396
+ method: "POST",
397
+ }),
398
+ );
399
+
400
+ const body = JSON.parse(mockSession.fetchHandler.mock.calls[0][1].body);
401
+ expect(body.repo).toBe(repoDid);
402
+ expect(body.newOwner).toBe("did:plc:new-owner");
403
+ });
404
+
405
+ it("should throw NetworkError on failure", async () => {
406
+ mockSession.fetchHandler.mockResolvedValue({
407
+ ok: false,
408
+ statusText: "Forbidden",
409
+ });
410
+
411
+ await expect(collaboratorOps.transferOwnership({ newOwnerDid: "did:plc:new-owner" })).rejects.toThrow(
412
+ NetworkError,
413
+ );
414
+ });
415
+
416
+ it("should throw NetworkError when not owner", async () => {
417
+ mockSession.fetchHandler.mockResolvedValue({
418
+ ok: false,
419
+ statusText: "Forbidden: Only the owner can transfer ownership",
420
+ });
421
+
422
+ await expect(collaboratorOps.transferOwnership({ newOwnerDid: "did:plc:new-owner" })).rejects.toThrow(
423
+ NetworkError,
424
+ );
425
+ });
426
+
427
+ it("should throw NetworkError when new owner does not exist", async () => {
428
+ mockSession.fetchHandler.mockResolvedValue({
429
+ ok: false,
430
+ statusText: "Not Found: New owner DID not found",
431
+ });
432
+
433
+ await expect(collaboratorOps.transferOwnership({ newOwnerDid: "did:plc:nonexistent" })).rejects.toThrow(
434
+ NetworkError,
435
+ );
436
+ });
437
+ });
323
438
  });
@@ -44,7 +44,7 @@ describe("OrganizationOperationsImpl", () => {
44
44
  expect(result.permissions.owner).toBe(true);
45
45
 
46
46
  expect(mockSession.fetchHandler).toHaveBeenCalledWith(
47
- `${serverUrl}/xrpc/com.atproto.sds.createRepository`,
47
+ `${serverUrl}/xrpc/com.sds.organization.create`,
48
48
  expect.objectContaining({
49
49
  method: "POST",
50
50
  headers: { "Content-Type": "application/json" },