Skip to content

Commit 8c0b6e6

Browse files
authored
Merge pull request #55 from hypercerts-org/fix/sds_endpoints_and_namespaces
Endpoints and nsids for sds operations
2 parents e66c6b5 + 23c3d9a commit 8c0b6e6

File tree

11 files changed

+333
-27
lines changed

11 files changed

+333
-27
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@hypercerts-org/sdk-core": patch
3+
"@hypercerts-org/lexicon": patch
4+
"@hypercerts-org/sdk-react": patch
5+
---
6+
7+
Fix endpoints and NSIDs for SDS operations in CollaboratorOperations and OrganizationOperations

.changeset/ignore-local-npmrc.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
---
3+
4+
Ignore local npmrc file in git

packages/lexicon/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@hypercerts-org/lexicon",
3-
"version": "0.1.0",
3+
"version": "0.3.0",
44
"description": "ATProto lexicon definitions and TypeScript types for the Hypercerts protocol",
55
"type": "module",
66
"main": "dist/index.cjs",

packages/sdk-core/README.md

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,57 @@ const existingSession = await sdk.restoreSession("did:plc:...");
7777

7878
```
7979
repo
80-
├── .records → create, get, update, delete, list
81-
├── .blobs → upload, get
82-
├── .profile → get, update
83-
├── .hypercerts → create, get, update, delete, list, addContribution, addMeasurement
84-
├── .collaborators → grant, revoke, list, hasAccess (SDS only)
80+
├── .records → create, get, update, delete, list
81+
├── .blobs → upload, get
82+
├── .profile → get, update (PDS only)
83+
├── .hypercerts → create, get, update, delete, list, addContribution, addMeasurement
84+
├── .collaborators → grant, revoke, list, hasAccess, getRole, getPermissions, transferOwnership (SDS only)
8585
└── .organizations → create, get, list (SDS only)
8686
```
8787

88+
### Collaborator Operations (SDS only)
89+
90+
```typescript
91+
// Grant access to a user
92+
await repo.collaborators.grant({
93+
userDid: "did:plc:user123",
94+
role: "editor", // viewer | editor | admin | owner
95+
});
96+
97+
// Revoke access
98+
await repo.collaborators.revoke({ userDid: "did:plc:user123" });
99+
100+
// List all collaborators
101+
const collaborators = await repo.collaborators.list();
102+
103+
// Check if user has access
104+
const hasAccess = await repo.collaborators.hasAccess("did:plc:user123");
105+
106+
// Get user's role
107+
const role = await repo.collaborators.getRole("did:plc:user123");
108+
109+
// Get current user's permissions
110+
const permissions = await repo.collaborators.getPermissions();
111+
if (permissions.admin) {
112+
// Can manage collaborators
113+
}
114+
115+
// Transfer ownership (irreversible!)
116+
await repo.collaborators.transferOwnership({
117+
newOwnerDid: "did:plc:new-owner",
118+
});
119+
```
120+
121+
### SDS vs PDS Operations
122+
123+
| Operation | PDS | SDS | Namespace |
124+
|-----------|-----|-----|-----------|
125+
| Records (CRUD) ||| `com.atproto.repo.*` |
126+
| Blobs ||| `com.atproto.repo.uploadBlob`, `com.atproto.sync.getBlob` |
127+
| Profile ||| `app.bsky.actor.profile` |
128+
| Organizations ||| `com.sds.organization.*` |
129+
| Collaborators ||| `com.sds.repo.*` |
130+
88131
## Errors
89132

90133
```typescript

packages/sdk-core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@hypercerts-org/sdk-core",
3-
"version": "0.1.0",
3+
"version": "0.3.0",
44
"description": "Framework-agnostic ATProto SDK core for authentication, repository operations, and lexicon management",
55
"main": "dist/index.cjs",
66
"repository": {

packages/sdk-core/src/repository/CollaboratorOperationsImpl.ts

Lines changed: 114 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@ import type { RepositoryRole, RepositoryAccessGrant } from "./types.js";
3030
* - `owner`: Full control including ownership management
3131
*
3232
* **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
3638
*
3739
* @example
3840
* ```typescript
@@ -135,7 +137,7 @@ export class CollaboratorOperationsImpl implements CollaboratorOperations {
135137
async grant(params: { userDid: string; role: RepositoryRole }): Promise<void> {
136138
const permissions = this.roleToPermissions(params.role);
137139

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`, {
139141
method: "POST",
140142
headers: { "Content-Type": "application/json" },
141143
body: JSON.stringify({
@@ -169,7 +171,7 @@ export class CollaboratorOperationsImpl implements CollaboratorOperations {
169171
* ```
170172
*/
171173
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`, {
173175
method: "POST",
174176
headers: { "Content-Type": "application/json" },
175177
body: JSON.stringify({
@@ -211,7 +213,7 @@ export class CollaboratorOperationsImpl implements CollaboratorOperations {
211213
*/
212214
async list(): Promise<RepositoryAccessGrant[]> {
213215
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)}`,
215217
{ method: "GET" },
216218
);
217219

@@ -285,4 +287,110 @@ export class CollaboratorOperationsImpl implements CollaboratorOperations {
285287
const collab = collaborators.find((c) => c.userDid === userDid && !c.revokedAt);
286288
return collab?.role ?? null;
287289
}
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+
}
288396
}

packages/sdk-core/src/repository/OrganizationOperationsImpl.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ import type { OrganizationInfo } from "./types.js";
2929
* {@link Repository.organizations} on an SDS-connected repository.
3030
*
3131
* **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
3434
*
3535
* **Access Types**:
3636
* - `"owner"`: User created or owns the organization
@@ -109,7 +109,7 @@ export class OrganizationOperationsImpl implements OrganizationOperations {
109109
* ```
110110
*/
111111
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`, {
113113
method: "POST",
114114
headers: { "Content-Type": "application/json" },
115115
body: JSON.stringify(params),
@@ -203,7 +203,7 @@ export class OrganizationOperationsImpl implements OrganizationOperations {
203203
*/
204204
async list(): Promise<OrganizationInfo[]> {
205205
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)}`,
207207
{ method: "GET" },
208208
);
209209

packages/sdk-core/src/repository/interfaces.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,17 @@ export interface HypercertOperations extends EventEmitter<HypercertEvents> {
773773
* const hasAccess = await repo.collaborators.hasAccess("did:plc:someone");
774774
* const role = await repo.collaborators.getRole("did:plc:someone");
775775
*
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+
*
776787
* // Revoke access
777788
* await repo.collaborators.revoke({ userDid: "did:plc:former-user" });
778789
* ```
@@ -817,6 +828,24 @@ export interface CollaboratorOperations {
817828
* @returns Promise resolving to role, or `null` if no access
818829
*/
819830
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>;
820849
}
821850

822851
/**

0 commit comments

Comments
 (0)