@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.
@@ -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
@@ -105,6 +107,27 @@ export class CollaboratorOperationsImpl implements CollaboratorOperations {
105
107
  return "viewer";
106
108
  }
107
109
 
110
+ /**
111
+ * Converts a permission string array to a permissions object.
112
+ *
113
+ * The SDS API returns permissions as an array of strings (e.g., ["read", "create"]).
114
+ * This method converts them to the boolean flag format used by the SDK.
115
+ *
116
+ * @param permissionArray - Array of permission strings from SDS API
117
+ * @returns Permission flags object
118
+ * @internal
119
+ */
120
+ private parsePermissions(permissionArray: string[]): CollaboratorPermissions {
121
+ return {
122
+ read: permissionArray.includes("read"),
123
+ create: permissionArray.includes("create"),
124
+ update: permissionArray.includes("update"),
125
+ delete: permissionArray.includes("delete"),
126
+ admin: permissionArray.includes("admin"),
127
+ owner: permissionArray.includes("owner"),
128
+ };
129
+ }
130
+
108
131
  /**
109
132
  * Grants repository access to a user.
110
133
  *
@@ -135,7 +158,7 @@ export class CollaboratorOperationsImpl implements CollaboratorOperations {
135
158
  async grant(params: { userDid: string; role: RepositoryRole }): Promise<void> {
136
159
  const permissions = this.roleToPermissions(params.role);
137
160
 
138
- const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.atproto.sds.grantAccess`, {
161
+ const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.sds.repo.grantAccess`, {
139
162
  method: "POST",
140
163
  headers: { "Content-Type": "application/json" },
141
164
  body: JSON.stringify({
@@ -169,7 +192,7 @@ export class CollaboratorOperationsImpl implements CollaboratorOperations {
169
192
  * ```
170
193
  */
171
194
  async revoke(params: { userDid: string }): Promise<void> {
172
- const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.atproto.sds.revokeAccess`, {
195
+ const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.sds.repo.revokeAccess`, {
173
196
  method: "POST",
174
197
  headers: { "Content-Type": "application/json" },
175
198
  body: JSON.stringify({
@@ -186,7 +209,10 @@ export class CollaboratorOperationsImpl implements CollaboratorOperations {
186
209
  /**
187
210
  * Lists all collaborators on the repository.
188
211
  *
189
- * @returns Promise resolving to array of access grants
212
+ * @param params - Optional pagination parameters
213
+ * @param params.limit - Maximum number of results (1-100, default 50)
214
+ * @param params.cursor - Pagination cursor from previous response
215
+ * @returns Promise resolving to collaborators and optional cursor
190
216
  * @throws {@link NetworkError} if the list operation fails
191
217
  *
192
218
  * @remarks
@@ -195,23 +221,37 @@ export class CollaboratorOperationsImpl implements CollaboratorOperations {
195
221
  *
196
222
  * @example
197
223
  * ```typescript
198
- * const collaborators = await repo.collaborators.list();
224
+ * // Get first page
225
+ * const page1 = await repo.collaborators.list({ limit: 10 });
226
+ * console.log(`Found ${page1.collaborators.length} collaborators`);
227
+ *
228
+ * // Get next page if available
229
+ * if (page1.cursor) {
230
+ * const page2 = await repo.collaborators.list({ limit: 10, cursor: page1.cursor });
231
+ * }
199
232
  *
200
233
  * // 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
- * };
234
+ * const active = page1.collaborators.filter(c => !c.revokedAt);
210
235
  * ```
211
236
  */
212
- async list(): Promise<RepositoryAccessGrant[]> {
237
+ async list(params?: { limit?: number; cursor?: string }): Promise<{
238
+ collaborators: RepositoryAccessGrant[];
239
+ cursor?: string;
240
+ }> {
241
+ const queryParams = new URLSearchParams({
242
+ repo: this.repoDid,
243
+ });
244
+
245
+ if (params?.limit !== undefined) {
246
+ queryParams.set("limit", params.limit.toString());
247
+ }
248
+
249
+ if (params?.cursor) {
250
+ queryParams.set("cursor", params.cursor);
251
+ }
252
+
213
253
  const response = await this.session.fetchHandler(
214
- `${this.serverUrl}/xrpc/com.atproto.sds.listCollaborators?repo=${encodeURIComponent(this.repoDid)}`,
254
+ `${this.serverUrl}/xrpc/com.sds.repo.listCollaborators?${queryParams.toString()}`,
215
255
  { method: "GET" },
216
256
  );
217
257
 
@@ -220,22 +260,30 @@ export class CollaboratorOperationsImpl implements CollaboratorOperations {
220
260
  }
221
261
 
222
262
  const data = await response.json();
223
- return (data.collaborators || []).map(
263
+ const collaborators = (data.collaborators || []).map(
224
264
  (c: {
225
265
  userDid: string;
226
- permissions: CollaboratorPermissions;
266
+ permissions: string[]; // SDS API returns string array
227
267
  grantedBy: string;
228
268
  grantedAt: string;
229
269
  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
- }),
270
+ }) => {
271
+ const permissions = this.parsePermissions(c.permissions);
272
+ return {
273
+ userDid: c.userDid,
274
+ role: this.permissionsToRole(permissions),
275
+ permissions: permissions,
276
+ grantedBy: c.grantedBy,
277
+ grantedAt: c.grantedAt,
278
+ revokedAt: c.revokedAt,
279
+ };
280
+ },
238
281
  );
282
+
283
+ return {
284
+ collaborators,
285
+ cursor: data.cursor,
286
+ };
239
287
  }
240
288
 
241
289
  /**
@@ -259,7 +307,7 @@ export class CollaboratorOperationsImpl implements CollaboratorOperations {
259
307
  */
260
308
  async hasAccess(userDid: string): Promise<boolean> {
261
309
  try {
262
- const collaborators = await this.list();
310
+ const { collaborators } = await this.list();
263
311
  return collaborators.some((c) => c.userDid === userDid && !c.revokedAt);
264
312
  } catch {
265
313
  return false;
@@ -281,8 +329,114 @@ export class CollaboratorOperationsImpl implements CollaboratorOperations {
281
329
  * ```
282
330
  */
283
331
  async getRole(userDid: string): Promise<RepositoryRole | null> {
284
- const collaborators = await this.list();
332
+ const { collaborators } = await this.list();
285
333
  const collab = collaborators.find((c) => c.userDid === userDid && !c.revokedAt);
286
334
  return collab?.role ?? null;
287
335
  }
336
+
337
+ /**
338
+ * Gets the current user's permissions for this repository.
339
+ *
340
+ * @returns Promise resolving to the permission flags
341
+ * @throws {@link NetworkError} if the request fails
342
+ *
343
+ * @remarks
344
+ * This is useful for checking what actions the current user can perform
345
+ * before attempting operations that might fail due to insufficient permissions.
346
+ *
347
+ * @example
348
+ * ```typescript
349
+ * const permissions = await repo.collaborators.getPermissions();
350
+ *
351
+ * if (permissions.admin) {
352
+ * // Show admin UI
353
+ * console.log("You can manage collaborators");
354
+ * }
355
+ *
356
+ * if (permissions.create) {
357
+ * console.log("You can create records");
358
+ * }
359
+ * ```
360
+ *
361
+ * @example Conditional UI rendering
362
+ * ```typescript
363
+ * const permissions = await repo.collaborators.getPermissions();
364
+ *
365
+ * // Show/hide UI elements based on permissions
366
+ * const canEdit = permissions.update;
367
+ * const canDelete = permissions.delete;
368
+ * const isAdmin = permissions.admin;
369
+ * const isOwner = permissions.owner;
370
+ * ```
371
+ */
372
+ async getPermissions(): Promise<CollaboratorPermissions> {
373
+ const response = await this.session.fetchHandler(
374
+ `${this.serverUrl}/xrpc/com.sds.repo.getPermissions?repo=${encodeURIComponent(this.repoDid)}`,
375
+ { method: "GET" },
376
+ );
377
+
378
+ if (!response.ok) {
379
+ throw new NetworkError(`Failed to get permissions: ${response.statusText}`);
380
+ }
381
+
382
+ const data = await response.json();
383
+ return data.permissions as CollaboratorPermissions;
384
+ }
385
+
386
+ /**
387
+ * Transfers repository ownership to another user.
388
+ *
389
+ * @param params - Transfer parameters
390
+ * @param params.newOwnerDid - DID of the user to transfer ownership to
391
+ * @throws {@link NetworkError} if the transfer fails
392
+ *
393
+ * @remarks
394
+ * **IMPORTANT**: This action is irreversible. Once ownership is transferred:
395
+ * - The new owner gains full control of the repository
396
+ * - Your role will be changed to admin (or specified role)
397
+ * - You cannot transfer ownership back without the new owner's approval
398
+ *
399
+ * **Requirements**:
400
+ * - You must be the current owner
401
+ * - The new owner must have an existing account
402
+ * - The new owner will be notified of the ownership transfer
403
+ *
404
+ * @example
405
+ * ```typescript
406
+ * // Transfer ownership to another user
407
+ * await repo.collaborators.transferOwnership({
408
+ * newOwnerDid: "did:plc:new-owner",
409
+ * });
410
+ *
411
+ * console.log("Ownership transferred successfully");
412
+ * // You are now an admin, not the owner
413
+ * ```
414
+ *
415
+ * @example With confirmation
416
+ * ```typescript
417
+ * const confirmTransfer = await askUser(
418
+ * "Are you sure you want to transfer ownership? This cannot be undone."
419
+ * );
420
+ *
421
+ * if (confirmTransfer) {
422
+ * await repo.collaborators.transferOwnership({
423
+ * newOwnerDid: "did:plc:new-owner",
424
+ * });
425
+ * }
426
+ * ```
427
+ */
428
+ async transferOwnership(params: { newOwnerDid: string }): Promise<void> {
429
+ const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.sds.repo.transferOwnership`, {
430
+ method: "POST",
431
+ headers: { "Content-Type": "application/json" },
432
+ body: JSON.stringify({
433
+ repo: this.repoDid,
434
+ newOwner: params.newOwnerDid,
435
+ }),
436
+ });
437
+
438
+ if (!response.ok) {
439
+ throw new NetworkError(`Failed to transfer ownership: ${response.statusText}`);
440
+ }
441
+ }
288
442
  }
@@ -208,7 +208,7 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
208
208
  if (uploadResult.success) {
209
209
  imageBlobRef = {
210
210
  $type: "blob",
211
- ref: uploadResult.data.blob.ref,
211
+ ref: { $link: uploadResult.data.blob.ref.toString() },
212
212
  mimeType: uploadResult.data.blob.mimeType,
213
213
  size: uploadResult.data.blob.size,
214
214
  };
@@ -676,7 +676,7 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
676
676
  if (uploadResult.success) {
677
677
  locationValue = {
678
678
  $type: "blob",
679
- ref: uploadResult.data.blob.ref,
679
+ ref: { $link: uploadResult.data.blob.ref.toString() },
680
680
  mimeType: uploadResult.data.blob.mimeType,
681
681
  size: uploadResult.data.blob.size,
682
682
  };
@@ -999,7 +999,7 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
999
999
  if (uploadResult.success) {
1000
1000
  coverPhotoRef = {
1001
1001
  $type: "blob",
1002
- ref: uploadResult.data.blob.ref,
1002
+ ref: { $link: uploadResult.data.blob.ref.toString() },
1003
1003
  mimeType: uploadResult.data.blob.mimeType,
1004
1004
  size: uploadResult.data.blob.size,
1005
1005
  };
@@ -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,10 +109,18 @@ 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 userDid = this.session.did || this.session.sub;
113
+ if (!userDid) {
114
+ throw new NetworkError("No authenticated user found");
115
+ }
116
+
117
+ const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.sds.organization.create`, {
113
118
  method: "POST",
114
119
  headers: { "Content-Type": "application/json" },
115
- body: JSON.stringify(params),
120
+ body: JSON.stringify({
121
+ ...params,
122
+ creatorDid: userDid,
123
+ }),
116
124
  });
117
125
 
118
126
  if (!response.ok) {
@@ -126,8 +134,15 @@ export class OrganizationOperationsImpl implements OrganizationOperations {
126
134
  name: data.name,
127
135
  description: data.description,
128
136
  createdAt: data.createdAt || new Date().toISOString(),
129
- accessType: "owner",
130
- permissions: { read: true, create: true, update: true, delete: true, admin: true, owner: true },
137
+ accessType: data.accessType || "owner",
138
+ permissions: data.permissions || {
139
+ read: true,
140
+ create: true,
141
+ update: true,
142
+ delete: true,
143
+ admin: true,
144
+ owner: true,
145
+ },
131
146
  };
132
147
  }
133
148
 
@@ -155,8 +170,8 @@ export class OrganizationOperationsImpl implements OrganizationOperations {
155
170
  */
156
171
  async get(did: string): Promise<OrganizationInfo | null> {
157
172
  try {
158
- const orgs = await this.list();
159
- return orgs.find((o) => o.did === did) ?? null;
173
+ const { organizations } = await this.list();
174
+ return organizations.find((o) => o.did === did) ?? null;
160
175
  } catch {
161
176
  return null;
162
177
  }
@@ -165,7 +180,10 @@ export class OrganizationOperationsImpl implements OrganizationOperations {
165
180
  /**
166
181
  * Lists organizations the current user has access to.
167
182
  *
168
- * @returns Promise resolving to array of organization info
183
+ * @param params - Optional pagination parameters
184
+ * @param params.limit - Maximum number of results (1-100, default 50)
185
+ * @param params.cursor - Pagination cursor from previous response
186
+ * @returns Promise resolving to organizations and optional cursor
169
187
  * @throws {@link NetworkError} if the list operation fails
170
188
  *
171
189
  * @remarks
@@ -177,21 +195,25 @@ export class OrganizationOperationsImpl implements OrganizationOperations {
177
195
  *
178
196
  * @example
179
197
  * ```typescript
180
- * const orgs = await repo.organizations.list();
198
+ * // Get first page
199
+ * const page1 = await repo.organizations.list({ limit: 20 });
200
+ * console.log(`Found ${page1.organizations.length} organizations`);
181
201
  *
182
- * // Filter by access type
183
- * const owned = orgs.filter(o => o.accessType === "owner");
184
- * const collaborated = orgs.filter(o => o.accessType === "collaborator");
202
+ * // Get next page if available
203
+ * if (page1.cursor) {
204
+ * const page2 = await repo.organizations.list({ limit: 20, cursor: page1.cursor });
205
+ * }
185
206
  *
186
- * console.log(`You own ${owned.length} organizations`);
187
- * console.log(`You collaborate on ${collaborated.length} organizations`);
207
+ * // Filter by access type
208
+ * const owned = page1.organizations.filter(o => o.accessType === "owner");
209
+ * const shared = page1.organizations.filter(o => o.accessType === "shared");
188
210
  * ```
189
211
  *
190
212
  * @example Display organization details
191
213
  * ```typescript
192
- * const orgs = await repo.organizations.list();
214
+ * const { organizations } = await repo.organizations.list();
193
215
  *
194
- * for (const org of orgs) {
216
+ * for (const org of organizations) {
195
217
  * console.log(`${org.name} (@${org.handle})`);
196
218
  * console.log(` DID: ${org.did}`);
197
219
  * console.log(` Access: ${org.accessType}`);
@@ -201,9 +223,29 @@ export class OrganizationOperationsImpl implements OrganizationOperations {
201
223
  * }
202
224
  * ```
203
225
  */
204
- async list(): Promise<OrganizationInfo[]> {
226
+ async list(params?: { limit?: number; cursor?: string }): Promise<{
227
+ organizations: OrganizationInfo[];
228
+ cursor?: string;
229
+ }> {
230
+ const userDid = this.session.did || this.session.sub;
231
+ if (!userDid) {
232
+ throw new NetworkError("No authenticated user found");
233
+ }
234
+
235
+ const queryParams = new URLSearchParams({
236
+ userDid,
237
+ });
238
+
239
+ if (params?.limit !== undefined) {
240
+ queryParams.set("limit", params.limit.toString());
241
+ }
242
+
243
+ if (params?.cursor) {
244
+ queryParams.set("cursor", params.cursor);
245
+ }
246
+
205
247
  const response = await this.session.fetchHandler(
206
- `${this.serverUrl}/xrpc/com.atproto.sds.listRepositories?userDid=${encodeURIComponent(this.session.did || this.session.sub)}`,
248
+ `${this.serverUrl}/xrpc/com.sds.organization.list?${queryParams.toString()}`,
207
249
  { method: "GET" },
208
250
  );
209
251
 
@@ -212,23 +254,29 @@ export class OrganizationOperationsImpl implements OrganizationOperations {
212
254
  }
213
255
 
214
256
  const data = await response.json();
215
- return (data.repositories || []).map(
257
+ const organizations = (data.organizations || []).map(
216
258
  (r: {
217
259
  did: string;
218
260
  handle: string;
219
261
  name: string;
220
262
  description?: string;
221
- accessType: "owner" | "collaborator";
263
+ createdAt?: string;
264
+ accessType: "owner" | "shared" | "none";
222
265
  permissions: CollaboratorPermissions;
223
266
  }) => ({
224
267
  did: r.did,
225
268
  handle: r.handle,
226
269
  name: r.name,
227
270
  description: r.description,
228
- createdAt: new Date().toISOString(), // SDS may not return this
271
+ createdAt: r.createdAt || new Date().toISOString(),
229
272
  accessType: r.accessType,
230
273
  permissions: r.permissions,
231
274
  }),
232
275
  );
276
+
277
+ return {
278
+ organizations,
279
+ cursor: data.cursor,
280
+ };
233
281
  }
234
282
  }
@@ -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
  * ```
@@ -798,9 +809,15 @@ export interface CollaboratorOperations {
798
809
  /**
799
810
  * Lists all collaborators on the repository.
800
811
  *
801
- * @returns Promise resolving to array of access grants
812
+ * @param params - Optional pagination parameters
813
+ * @param params.limit - Maximum number of results (1-100, default 50)
814
+ * @param params.cursor - Pagination cursor from previous response
815
+ * @returns Promise resolving to collaborators and optional cursor
802
816
  */
803
- list(): Promise<RepositoryAccessGrant[]>;
817
+ list(params?: { limit?: number; cursor?: string }): Promise<{
818
+ collaborators: RepositoryAccessGrant[];
819
+ cursor?: string;
820
+ }>;
804
821
 
805
822
  /**
806
823
  * Checks if a user has any access to the repository.
@@ -817,6 +834,24 @@ export interface CollaboratorOperations {
817
834
  * @returns Promise resolving to role, or `null` if no access
818
835
  */
819
836
  getRole(userDid: string): Promise<RepositoryRole | null>;
837
+
838
+ /**
839
+ * Gets the current user's permissions for this repository.
840
+ *
841
+ * @returns Promise resolving to permission flags
842
+ */
843
+ getPermissions(): Promise<import("../core/types.js").CollaboratorPermissions>;
844
+
845
+ /**
846
+ * Transfers repository ownership to another user.
847
+ *
848
+ * **WARNING**: This action is irreversible. The new owner will have
849
+ * full control of the repository.
850
+ *
851
+ * @param params - Transfer parameters
852
+ * @param params.newOwnerDid - DID of the user to transfer ownership to
853
+ */
854
+ transferOwnership(params: { newOwnerDid: string }): Promise<void>;
820
855
  }
821
856
 
822
857
  /**
@@ -862,7 +897,13 @@ export interface OrganizationOperations {
862
897
  /**
863
898
  * Lists organizations the current user has access to.
864
899
  *
865
- * @returns Promise resolving to array of organization info
900
+ * @param params - Optional pagination parameters
901
+ * @param params.limit - Maximum number of results (1-100, default 50)
902
+ * @param params.cursor - Pagination cursor from previous response
903
+ * @returns Promise resolving to organizations and optional cursor
866
904
  */
867
- list(): Promise<OrganizationInfo[]>;
905
+ list(params?: { limit?: number; cursor?: string }): Promise<{
906
+ organizations: OrganizationInfo[];
907
+ cursor?: string;
908
+ }>;
868
909
  }
@@ -85,7 +85,7 @@ export interface OrganizationInfo {
85
85
  name: string;
86
86
  description?: string;
87
87
  createdAt: string;
88
- accessType: "owner" | "collaborator";
88
+ accessType: "owner" | "shared" | "none";
89
89
  permissions: CollaboratorPermissions;
90
90
  collaboratorCount?: number;
91
91
  profile?: {
@@ -28,11 +28,14 @@ describe("BlobOperationsImpl", () => {
28
28
  describe("upload", () => {
29
29
  it("should upload a blob successfully", async () => {
30
30
  const mockBlob = new Blob(["test content"], { type: "text/plain" });
31
+ const mockCID = {
32
+ toString: () => "bafyrei123",
33
+ };
31
34
  mockAgent.com.atproto.repo.uploadBlob.mockResolvedValue({
32
35
  success: true,
33
36
  data: {
34
37
  blob: {
35
- ref: { $link: "bafyrei123" },
38
+ ref: mockCID,
36
39
  mimeType: "text/plain",
37
40
  size: 12,
38
41
  },
@@ -61,10 +64,9 @@ describe("BlobOperationsImpl", () => {
61
64
 
62
65
  await blobOps.upload(mockBlob);
63
66
 
64
- expect(mockAgent.com.atproto.repo.uploadBlob).toHaveBeenCalledWith(
65
- expect.any(Uint8Array),
66
- { encoding: "image/png" },
67
- );
67
+ expect(mockAgent.com.atproto.repo.uploadBlob).toHaveBeenCalledWith(expect.any(Uint8Array), {
68
+ encoding: "image/png",
69
+ });
68
70
  });
69
71
 
70
72
  it("should default to application/octet-stream for blobs without type", async () => {
@@ -82,10 +84,9 @@ describe("BlobOperationsImpl", () => {
82
84
 
83
85
  await blobOps.upload(mockBlob);
84
86
 
85
- expect(mockAgent.com.atproto.repo.uploadBlob).toHaveBeenCalledWith(
86
- expect.any(Uint8Array),
87
- { encoding: "application/octet-stream" },
88
- );
87
+ expect(mockAgent.com.atproto.repo.uploadBlob).toHaveBeenCalledWith(expect.any(Uint8Array), {
88
+ encoding: "application/octet-stream",
89
+ });
89
90
  });
90
91
 
91
92
  it("should throw NetworkError when API returns success: false", async () => {