@hypercerts-org/sdk-core 0.2.0-beta.0 → 0.5.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/index.cjs CHANGED
@@ -1670,7 +1670,7 @@ class BlobOperationsImpl {
1670
1670
  throw new NetworkError("Failed to upload blob");
1671
1671
  }
1672
1672
  return {
1673
- ref: result.data.blob.ref,
1673
+ ref: { $link: result.data.blob.ref.toString() },
1674
1674
  mimeType: result.data.blob.mimeType,
1675
1675
  size: result.data.blob.size,
1676
1676
  };
@@ -2177,7 +2177,7 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
2177
2177
  if (uploadResult.success) {
2178
2178
  imageBlobRef = {
2179
2179
  $type: "blob",
2180
- ref: uploadResult.data.blob.ref,
2180
+ ref: { $link: uploadResult.data.blob.ref.toString() },
2181
2181
  mimeType: uploadResult.data.blob.mimeType,
2182
2182
  size: uploadResult.data.blob.size,
2183
2183
  };
@@ -2602,7 +2602,7 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
2602
2602
  if (uploadResult.success) {
2603
2603
  locationValue = {
2604
2604
  $type: "blob",
2605
- ref: uploadResult.data.blob.ref,
2605
+ ref: { $link: uploadResult.data.blob.ref.toString() },
2606
2606
  mimeType: uploadResult.data.blob.mimeType,
2607
2607
  size: uploadResult.data.blob.size,
2608
2608
  };
@@ -2887,7 +2887,7 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
2887
2887
  if (uploadResult.success) {
2888
2888
  coverPhotoRef = {
2889
2889
  $type: "blob",
2890
- ref: uploadResult.data.blob.ref,
2890
+ ref: { $link: uploadResult.data.blob.ref.toString() },
2891
2891
  mimeType: uploadResult.data.blob.mimeType,
2892
2892
  size: uploadResult.data.blob.size,
2893
2893
  };
@@ -3041,9 +3041,11 @@ class HypercertOperationsImpl extends eventemitter3.EventEmitter {
3041
3041
  * - `owner`: Full control including ownership management
3042
3042
  *
3043
3043
  * **SDS API Endpoints Used**:
3044
- * - `com.atproto.sds.grantAccess`: Grant access to a user
3045
- * - `com.atproto.sds.revokeAccess`: Revoke access from a user
3046
- * - `com.atproto.sds.listCollaborators`: List all collaborators
3044
+ * - `com.sds.repo.grantAccess`: Grant access to a user
3045
+ * - `com.sds.repo.revokeAccess`: Revoke access from a user
3046
+ * - `com.sds.repo.listCollaborators`: List all collaborators
3047
+ * - `com.sds.repo.getPermissions`: Get current user's permissions
3048
+ * - `com.sds.repo.transferOwnership`: Transfer repository ownership
3047
3049
  *
3048
3050
  * @example
3049
3051
  * ```typescript
@@ -3116,6 +3118,26 @@ class CollaboratorOperationsImpl {
3116
3118
  return "editor";
3117
3119
  return "viewer";
3118
3120
  }
3121
+ /**
3122
+ * Converts a permission string array to a permissions object.
3123
+ *
3124
+ * The SDS API returns permissions as an array of strings (e.g., ["read", "create"]).
3125
+ * This method converts them to the boolean flag format used by the SDK.
3126
+ *
3127
+ * @param permissionArray - Array of permission strings from SDS API
3128
+ * @returns Permission flags object
3129
+ * @internal
3130
+ */
3131
+ parsePermissions(permissionArray) {
3132
+ return {
3133
+ read: permissionArray.includes("read"),
3134
+ create: permissionArray.includes("create"),
3135
+ update: permissionArray.includes("update"),
3136
+ delete: permissionArray.includes("delete"),
3137
+ admin: permissionArray.includes("admin"),
3138
+ owner: permissionArray.includes("owner"),
3139
+ };
3140
+ }
3119
3141
  /**
3120
3142
  * Grants repository access to a user.
3121
3143
  *
@@ -3145,7 +3167,7 @@ class CollaboratorOperationsImpl {
3145
3167
  */
3146
3168
  async grant(params) {
3147
3169
  const permissions = this.roleToPermissions(params.role);
3148
- const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.atproto.sds.grantAccess`, {
3170
+ const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.sds.repo.grantAccess`, {
3149
3171
  method: "POST",
3150
3172
  headers: { "Content-Type": "application/json" },
3151
3173
  body: JSON.stringify({
@@ -3177,7 +3199,7 @@ class CollaboratorOperationsImpl {
3177
3199
  * ```
3178
3200
  */
3179
3201
  async revoke(params) {
3180
- const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.atproto.sds.revokeAccess`, {
3202
+ const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.sds.repo.revokeAccess`, {
3181
3203
  method: "POST",
3182
3204
  headers: { "Content-Type": "application/json" },
3183
3205
  body: JSON.stringify({
@@ -3192,7 +3214,10 @@ class CollaboratorOperationsImpl {
3192
3214
  /**
3193
3215
  * Lists all collaborators on the repository.
3194
3216
  *
3195
- * @returns Promise resolving to array of access grants
3217
+ * @param params - Optional pagination parameters
3218
+ * @param params.limit - Maximum number of results (1-100, default 50)
3219
+ * @param params.cursor - Pagination cursor from previous response
3220
+ * @returns Promise resolving to collaborators and optional cursor
3196
3221
  * @throws {@link NetworkError} if the list operation fails
3197
3222
  *
3198
3223
  * @remarks
@@ -3201,34 +3226,49 @@ class CollaboratorOperationsImpl {
3201
3226
  *
3202
3227
  * @example
3203
3228
  * ```typescript
3204
- * const collaborators = await repo.collaborators.list();
3229
+ * // Get first page
3230
+ * const page1 = await repo.collaborators.list({ limit: 10 });
3231
+ * console.log(`Found ${page1.collaborators.length} collaborators`);
3232
+ *
3233
+ * // Get next page if available
3234
+ * if (page1.cursor) {
3235
+ * const page2 = await repo.collaborators.list({ limit: 10, cursor: page1.cursor });
3236
+ * }
3205
3237
  *
3206
3238
  * // Filter active collaborators
3207
- * const active = collaborators.filter(c => !c.revokedAt);
3208
- *
3209
- * // Group by role
3210
- * const byRole = {
3211
- * owners: active.filter(c => c.role === "owner"),
3212
- * admins: active.filter(c => c.role === "admin"),
3213
- * editors: active.filter(c => c.role === "editor"),
3214
- * viewers: active.filter(c => c.role === "viewer"),
3215
- * };
3239
+ * const active = page1.collaborators.filter(c => !c.revokedAt);
3216
3240
  * ```
3217
3241
  */
3218
- async list() {
3219
- const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.atproto.sds.listCollaborators?repo=${encodeURIComponent(this.repoDid)}`, { method: "GET" });
3242
+ async list(params) {
3243
+ const queryParams = new URLSearchParams({
3244
+ repo: this.repoDid,
3245
+ });
3246
+ if (params?.limit !== undefined) {
3247
+ queryParams.set("limit", params.limit.toString());
3248
+ }
3249
+ if (params?.cursor) {
3250
+ queryParams.set("cursor", params.cursor);
3251
+ }
3252
+ const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.sds.repo.listCollaborators?${queryParams.toString()}`, { method: "GET" });
3220
3253
  if (!response.ok) {
3221
3254
  throw new NetworkError(`Failed to list collaborators: ${response.statusText}`);
3222
3255
  }
3223
3256
  const data = await response.json();
3224
- return (data.collaborators || []).map((c) => ({
3225
- userDid: c.userDid,
3226
- role: this.permissionsToRole(c.permissions),
3227
- permissions: c.permissions,
3228
- grantedBy: c.grantedBy,
3229
- grantedAt: c.grantedAt,
3230
- revokedAt: c.revokedAt,
3231
- }));
3257
+ const collaborators = (data.collaborators || []).map((c) => {
3258
+ const permissions = this.parsePermissions(c.permissions);
3259
+ return {
3260
+ userDid: c.userDid,
3261
+ role: this.permissionsToRole(permissions),
3262
+ permissions: permissions,
3263
+ grantedBy: c.grantedBy,
3264
+ grantedAt: c.grantedAt,
3265
+ revokedAt: c.revokedAt,
3266
+ };
3267
+ });
3268
+ return {
3269
+ collaborators,
3270
+ cursor: data.cursor,
3271
+ };
3232
3272
  }
3233
3273
  /**
3234
3274
  * Checks if a user has any access to the repository.
@@ -3251,7 +3291,7 @@ class CollaboratorOperationsImpl {
3251
3291
  */
3252
3292
  async hasAccess(userDid) {
3253
3293
  try {
3254
- const collaborators = await this.list();
3294
+ const { collaborators } = await this.list();
3255
3295
  return collaborators.some((c) => c.userDid === userDid && !c.revokedAt);
3256
3296
  }
3257
3297
  catch {
@@ -3273,10 +3313,108 @@ class CollaboratorOperationsImpl {
3273
3313
  * ```
3274
3314
  */
3275
3315
  async getRole(userDid) {
3276
- const collaborators = await this.list();
3316
+ const { collaborators } = await this.list();
3277
3317
  const collab = collaborators.find((c) => c.userDid === userDid && !c.revokedAt);
3278
3318
  return collab?.role ?? null;
3279
3319
  }
3320
+ /**
3321
+ * Gets the current user's permissions for this repository.
3322
+ *
3323
+ * @returns Promise resolving to the permission flags
3324
+ * @throws {@link NetworkError} if the request fails
3325
+ *
3326
+ * @remarks
3327
+ * This is useful for checking what actions the current user can perform
3328
+ * before attempting operations that might fail due to insufficient permissions.
3329
+ *
3330
+ * @example
3331
+ * ```typescript
3332
+ * const permissions = await repo.collaborators.getPermissions();
3333
+ *
3334
+ * if (permissions.admin) {
3335
+ * // Show admin UI
3336
+ * console.log("You can manage collaborators");
3337
+ * }
3338
+ *
3339
+ * if (permissions.create) {
3340
+ * console.log("You can create records");
3341
+ * }
3342
+ * ```
3343
+ *
3344
+ * @example Conditional UI rendering
3345
+ * ```typescript
3346
+ * const permissions = await repo.collaborators.getPermissions();
3347
+ *
3348
+ * // Show/hide UI elements based on permissions
3349
+ * const canEdit = permissions.update;
3350
+ * const canDelete = permissions.delete;
3351
+ * const isAdmin = permissions.admin;
3352
+ * const isOwner = permissions.owner;
3353
+ * ```
3354
+ */
3355
+ async getPermissions() {
3356
+ const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.sds.repo.getPermissions?repo=${encodeURIComponent(this.repoDid)}`, { method: "GET" });
3357
+ if (!response.ok) {
3358
+ throw new NetworkError(`Failed to get permissions: ${response.statusText}`);
3359
+ }
3360
+ const data = await response.json();
3361
+ return data.permissions;
3362
+ }
3363
+ /**
3364
+ * Transfers repository ownership to another user.
3365
+ *
3366
+ * @param params - Transfer parameters
3367
+ * @param params.newOwnerDid - DID of the user to transfer ownership to
3368
+ * @throws {@link NetworkError} if the transfer fails
3369
+ *
3370
+ * @remarks
3371
+ * **IMPORTANT**: This action is irreversible. Once ownership is transferred:
3372
+ * - The new owner gains full control of the repository
3373
+ * - Your role will be changed to admin (or specified role)
3374
+ * - You cannot transfer ownership back without the new owner's approval
3375
+ *
3376
+ * **Requirements**:
3377
+ * - You must be the current owner
3378
+ * - The new owner must have an existing account
3379
+ * - The new owner will be notified of the ownership transfer
3380
+ *
3381
+ * @example
3382
+ * ```typescript
3383
+ * // Transfer ownership to another user
3384
+ * await repo.collaborators.transferOwnership({
3385
+ * newOwnerDid: "did:plc:new-owner",
3386
+ * });
3387
+ *
3388
+ * console.log("Ownership transferred successfully");
3389
+ * // You are now an admin, not the owner
3390
+ * ```
3391
+ *
3392
+ * @example With confirmation
3393
+ * ```typescript
3394
+ * const confirmTransfer = await askUser(
3395
+ * "Are you sure you want to transfer ownership? This cannot be undone."
3396
+ * );
3397
+ *
3398
+ * if (confirmTransfer) {
3399
+ * await repo.collaborators.transferOwnership({
3400
+ * newOwnerDid: "did:plc:new-owner",
3401
+ * });
3402
+ * }
3403
+ * ```
3404
+ */
3405
+ async transferOwnership(params) {
3406
+ const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.sds.repo.transferOwnership`, {
3407
+ method: "POST",
3408
+ headers: { "Content-Type": "application/json" },
3409
+ body: JSON.stringify({
3410
+ repo: this.repoDid,
3411
+ newOwner: params.newOwnerDid,
3412
+ }),
3413
+ });
3414
+ if (!response.ok) {
3415
+ throw new NetworkError(`Failed to transfer ownership: ${response.statusText}`);
3416
+ }
3417
+ }
3280
3418
  }
3281
3419
 
3282
3420
  /**
@@ -3303,8 +3441,8 @@ class CollaboratorOperationsImpl {
3303
3441
  * {@link Repository.organizations} on an SDS-connected repository.
3304
3442
  *
3305
3443
  * **SDS API Endpoints Used**:
3306
- * - `com.atproto.sds.createRepository`: Create a new organization
3307
- * - `com.atproto.sds.listRepositories`: List accessible organizations
3444
+ * - `com.sds.organization.create`: Create a new organization
3445
+ * - `com.sds.organization.list`: List accessible organizations
3308
3446
  *
3309
3447
  * **Access Types**:
3310
3448
  * - `"owner"`: User created or owns the organization
@@ -3382,10 +3520,17 @@ class OrganizationOperationsImpl {
3382
3520
  * ```
3383
3521
  */
3384
3522
  async create(params) {
3385
- const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.atproto.sds.createRepository`, {
3523
+ const userDid = this.session.did || this.session.sub;
3524
+ if (!userDid) {
3525
+ throw new NetworkError("No authenticated user found");
3526
+ }
3527
+ const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.sds.organization.create`, {
3386
3528
  method: "POST",
3387
3529
  headers: { "Content-Type": "application/json" },
3388
- body: JSON.stringify(params),
3530
+ body: JSON.stringify({
3531
+ ...params,
3532
+ creatorDid: userDid,
3533
+ }),
3389
3534
  });
3390
3535
  if (!response.ok) {
3391
3536
  throw new NetworkError(`Failed to create organization: ${response.statusText}`);
@@ -3397,8 +3542,15 @@ class OrganizationOperationsImpl {
3397
3542
  name: data.name,
3398
3543
  description: data.description,
3399
3544
  createdAt: data.createdAt || new Date().toISOString(),
3400
- accessType: "owner",
3401
- permissions: { read: true, create: true, update: true, delete: true, admin: true, owner: true },
3545
+ accessType: data.accessType || "owner",
3546
+ permissions: data.permissions || {
3547
+ read: true,
3548
+ create: true,
3549
+ update: true,
3550
+ delete: true,
3551
+ admin: true,
3552
+ owner: true,
3553
+ },
3402
3554
  };
3403
3555
  }
3404
3556
  /**
@@ -3425,8 +3577,8 @@ class OrganizationOperationsImpl {
3425
3577
  */
3426
3578
  async get(did) {
3427
3579
  try {
3428
- const orgs = await this.list();
3429
- return orgs.find((o) => o.did === did) ?? null;
3580
+ const { organizations } = await this.list();
3581
+ return organizations.find((o) => o.did === did) ?? null;
3430
3582
  }
3431
3583
  catch {
3432
3584
  return null;
@@ -3435,7 +3587,10 @@ class OrganizationOperationsImpl {
3435
3587
  /**
3436
3588
  * Lists organizations the current user has access to.
3437
3589
  *
3438
- * @returns Promise resolving to array of organization info
3590
+ * @param params - Optional pagination parameters
3591
+ * @param params.limit - Maximum number of results (1-100, default 50)
3592
+ * @param params.cursor - Pagination cursor from previous response
3593
+ * @returns Promise resolving to organizations and optional cursor
3439
3594
  * @throws {@link NetworkError} if the list operation fails
3440
3595
  *
3441
3596
  * @remarks
@@ -3447,21 +3602,25 @@ class OrganizationOperationsImpl {
3447
3602
  *
3448
3603
  * @example
3449
3604
  * ```typescript
3450
- * const orgs = await repo.organizations.list();
3605
+ * // Get first page
3606
+ * const page1 = await repo.organizations.list({ limit: 20 });
3607
+ * console.log(`Found ${page1.organizations.length} organizations`);
3451
3608
  *
3452
- * // Filter by access type
3453
- * const owned = orgs.filter(o => o.accessType === "owner");
3454
- * const collaborated = orgs.filter(o => o.accessType === "collaborator");
3609
+ * // Get next page if available
3610
+ * if (page1.cursor) {
3611
+ * const page2 = await repo.organizations.list({ limit: 20, cursor: page1.cursor });
3612
+ * }
3455
3613
  *
3456
- * console.log(`You own ${owned.length} organizations`);
3457
- * console.log(`You collaborate on ${collaborated.length} organizations`);
3614
+ * // Filter by access type
3615
+ * const owned = page1.organizations.filter(o => o.accessType === "owner");
3616
+ * const shared = page1.organizations.filter(o => o.accessType === "shared");
3458
3617
  * ```
3459
3618
  *
3460
3619
  * @example Display organization details
3461
3620
  * ```typescript
3462
- * const orgs = await repo.organizations.list();
3621
+ * const { organizations } = await repo.organizations.list();
3463
3622
  *
3464
- * for (const org of orgs) {
3623
+ * for (const org of organizations) {
3465
3624
  * console.log(`${org.name} (@${org.handle})`);
3466
3625
  * console.log(` DID: ${org.did}`);
3467
3626
  * console.log(` Access: ${org.accessType}`);
@@ -3471,21 +3630,38 @@ class OrganizationOperationsImpl {
3471
3630
  * }
3472
3631
  * ```
3473
3632
  */
3474
- async list() {
3475
- const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.atproto.sds.listRepositories?userDid=${encodeURIComponent(this.session.did || this.session.sub)}`, { method: "GET" });
3633
+ async list(params) {
3634
+ const userDid = this.session.did || this.session.sub;
3635
+ if (!userDid) {
3636
+ throw new NetworkError("No authenticated user found");
3637
+ }
3638
+ const queryParams = new URLSearchParams({
3639
+ userDid,
3640
+ });
3641
+ if (params?.limit !== undefined) {
3642
+ queryParams.set("limit", params.limit.toString());
3643
+ }
3644
+ if (params?.cursor) {
3645
+ queryParams.set("cursor", params.cursor);
3646
+ }
3647
+ const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.sds.organization.list?${queryParams.toString()}`, { method: "GET" });
3476
3648
  if (!response.ok) {
3477
3649
  throw new NetworkError(`Failed to list organizations: ${response.statusText}`);
3478
3650
  }
3479
3651
  const data = await response.json();
3480
- return (data.repositories || []).map((r) => ({
3652
+ const organizations = (data.organizations || []).map((r) => ({
3481
3653
  did: r.did,
3482
3654
  handle: r.handle,
3483
3655
  name: r.name,
3484
3656
  description: r.description,
3485
- createdAt: new Date().toISOString(), // SDS may not return this
3657
+ createdAt: r.createdAt || new Date().toISOString(),
3486
3658
  accessType: r.accessType,
3487
3659
  permissions: r.permissions,
3488
3660
  }));
3661
+ return {
3662
+ organizations,
3663
+ cursor: data.cursor,
3664
+ };
3489
3665
  }
3490
3666
  }
3491
3667
 
@@ -4408,9 +4584,10 @@ const OrganizationSchema = zod.z.object({
4408
4584
  /**
4409
4585
  * How the current user relates to this organization.
4410
4586
  * - `"owner"`: User created or owns the organization
4411
- * - `"collaborator"`: User was invited to collaborate
4587
+ * - `"shared"`: User was invited to collaborate (has permissions)
4588
+ * - `"none"`: User has no access to this organization
4412
4589
  */
4413
- accessType: zod.z.enum(["owner", "collaborator"]),
4590
+ accessType: zod.z.enum(["owner", "shared", "none"]),
4414
4591
  });
4415
4592
  /**
4416
4593
  * Zod schema for collaborator data.