primitive-admin 1.0.60 → 1.0.61

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 (40) hide show
  1. package/dist/bin/primitive.js +4 -0
  2. package/dist/bin/primitive.js.map +1 -1
  3. package/dist/src/commands/admins.js +22 -8
  4. package/dist/src/commands/admins.js.map +1 -1
  5. package/dist/src/commands/analytics.js +60 -0
  6. package/dist/src/commands/analytics.js.map +1 -1
  7. package/dist/src/commands/blob-buckets.js +37 -2
  8. package/dist/src/commands/blob-buckets.js.map +1 -1
  9. package/dist/src/commands/connections.d.ts +2 -0
  10. package/dist/src/commands/connections.js +95 -0
  11. package/dist/src/commands/connections.js.map +1 -0
  12. package/dist/src/commands/init.js +2 -2
  13. package/dist/src/commands/init.js.map +1 -1
  14. package/dist/src/commands/sessions.d.ts +2 -0
  15. package/dist/src/commands/sessions.js +63 -0
  16. package/dist/src/commands/sessions.js.map +1 -0
  17. package/dist/src/commands/skill.js +2 -2
  18. package/dist/src/commands/skill.js.map +1 -1
  19. package/dist/src/commands/sync.js +120 -13
  20. package/dist/src/commands/sync.js.map +1 -1
  21. package/dist/src/commands/workflows.js +56 -13
  22. package/dist/src/commands/workflows.js.map +1 -1
  23. package/dist/src/lib/api-client.d.ts +89 -0
  24. package/dist/src/lib/api-client.js +118 -15
  25. package/dist/src/lib/api-client.js.map +1 -1
  26. package/dist/src/lib/generated-allowlist.js +1 -0
  27. package/dist/src/lib/generated-allowlist.js.map +1 -1
  28. package/dist/src/lib/skill-installer.d.ts +4 -2
  29. package/dist/src/lib/skill-installer.js +139 -11
  30. package/dist/src/lib/skill-installer.js.map +1 -1
  31. package/dist/src/lib/workflow-fragments.d.ts +23 -0
  32. package/dist/src/lib/workflow-fragments.js +229 -8
  33. package/dist/src/lib/workflow-fragments.js.map +1 -1
  34. package/dist/src/lib/workflow-payload.d.ts +1 -1
  35. package/dist/src/lib/workflow-payload.js +17 -0
  36. package/dist/src/lib/workflow-payload.js.map +1 -1
  37. package/dist/src/lib/workflow-toml-validator.d.ts +57 -1
  38. package/dist/src/lib/workflow-toml-validator.js +104 -1
  39. package/dist/src/lib/workflow-toml-validator.js.map +1 -1
  40. package/package.json +1 -1
@@ -1,3 +1,20 @@
1
+ /**
2
+ * Metadata for a single bucket blob, as returned by the metadata endpoint
3
+ * `GET blob-buckets/:bucketId/blobs/:blobId/metadata`. Mirrors the published
4
+ * JS client's `BlobInfo` shape.
5
+ */
6
+ export interface BlobInfo {
7
+ blobId: string;
8
+ bucketId: string;
9
+ filename: string | null;
10
+ contentType: string | null;
11
+ numBytes: number;
12
+ sha256: string | null;
13
+ tags: string[];
14
+ createdBy: string | null;
15
+ uploaded?: string | null;
16
+ etag?: string | null;
17
+ }
1
18
  export declare class ApiError extends Error {
2
19
  statusCode: number;
3
20
  code?: string;
@@ -262,6 +279,22 @@ interface AnalyticsEventsGroupedResponse {
262
279
  rows: AnalyticsGroupedRow[];
263
280
  _timing: AnalyticsTiming;
264
281
  }
282
+ interface AnalyticsErrorGroupRow {
283
+ fingerprint: string;
284
+ normalized_title: string;
285
+ source: string;
286
+ status_class: string;
287
+ total: number;
288
+ daily: Array<{
289
+ day: string;
290
+ count: number;
291
+ }>;
292
+ }
293
+ interface AnalyticsErrorsGroupsResponse {
294
+ window_days: number;
295
+ rows: AnalyticsErrorGroupRow[];
296
+ _timing: AnalyticsTiming;
297
+ }
265
298
  interface AnalyticsIntegrationMetric {
266
299
  integrationKey: string;
267
300
  invocations: number;
@@ -379,6 +412,9 @@ export declare class ApiClient {
379
412
  cursor?: string;
380
413
  }): Promise<{
381
414
  items: any[];
415
+ nextCursor?: string;
416
+ hasMore?: boolean;
417
+ /** @deprecated Alias of `nextCursor` (#1316). */
382
418
  cursor?: string;
383
419
  }>;
384
420
  createInvitation(appId: string, data: {
@@ -405,9 +441,11 @@ export declare class ApiClient {
405
441
  type?: string;
406
442
  email?: string;
407
443
  limit?: number;
444
+ cursor?: string;
408
445
  }): Promise<{
409
446
  grants: any[];
410
447
  nextCursor?: string | null;
448
+ hasMore?: boolean;
411
449
  }>;
412
450
  revokeDeferredGrant(appId: string, deferredId: string, type: string): Promise<any>;
413
451
  listIntegrations(appId: string, params?: {
@@ -482,6 +520,31 @@ export declare class ApiClient {
482
520
  listWebhookEvents(appId: string, webhookId: string, params?: {
483
521
  limit?: number;
484
522
  }): Promise<any>;
523
+ /**
524
+ * List the live connections the server believes exist for exactly one of a
525
+ * document, user, or database. Read-only inspection over admin-api. Rows are
526
+ * per-connection-×-subscription bindings, so `connectionId` is not unique
527
+ * across rows. `params` on a database-subscription row is caller-defined
528
+ * bound CEL input — typed `unknown`.
529
+ */
530
+ listConnections(appId: string, selector: {
531
+ documentId?: string;
532
+ userId?: string;
533
+ databaseId?: string;
534
+ }, params?: {
535
+ limit?: number;
536
+ cursor?: string;
537
+ }): Promise<{
538
+ items: unknown[];
539
+ nextCursor?: string | null;
540
+ }>;
541
+ listSessions(appId: string, userId: string, params?: {
542
+ limit?: number;
543
+ cursor?: string;
544
+ }): Promise<{
545
+ items: unknown[];
546
+ nextCursor?: string | null;
547
+ }>;
485
548
  testWebhook(appId: string, webhookId: string, payload?: any): Promise<any>;
486
549
  listLocks(appId: string): Promise<{
487
550
  locks: any[];
@@ -716,6 +779,8 @@ export declare class ApiClient {
716
779
  forward?: boolean;
717
780
  }): Promise<{
718
781
  items: any[];
782
+ nextCursor?: string | null;
783
+ /** @deprecated Alias of `nextCursor` (#1316). */
719
784
  cursor?: string | null;
720
785
  }>;
721
786
  getWorkflowRunStatus(appId: string, workflowId: string, runId: string): Promise<any>;
@@ -791,6 +856,11 @@ export declare class ApiClient {
791
856
  windowDays?: number;
792
857
  [key: string]: any;
793
858
  }): Promise<AnalyticsEventsGroupedResponse>;
859
+ getAnalyticsErrorsGroups(appId: string, params?: {
860
+ windowDays?: number;
861
+ limit?: number;
862
+ [key: string]: any;
863
+ }): Promise<AnalyticsErrorsGroupsResponse>;
794
864
  getAnalyticsTopWorkflows(appId: string, params?: {
795
865
  windowDays?: number;
796
866
  limit?: number;
@@ -1056,6 +1126,8 @@ export declare class ApiClient {
1056
1126
  timestamps?: Record<string, any> | null;
1057
1127
  schema?: string | null;
1058
1128
  metadataManifest?: any;
1129
+ }, options?: {
1130
+ dryRun?: boolean;
1059
1131
  }): Promise<any>;
1060
1132
  updateDatabaseTypeConfig(appId: string, databaseType: string, data: {
1061
1133
  ruleSetId?: string | null;
@@ -1068,6 +1140,11 @@ export declare class ApiClient {
1068
1140
  pendingOpDeletes?: string[];
1069
1141
  finalOpNames?: string[];
1070
1142
  metadataManifest?: any;
1143
+ pendingOpUpdates?: Array<{
1144
+ name: string;
1145
+ access?: string | null;
1146
+ params?: Record<string, any> | null;
1147
+ }>;
1071
1148
  }, expectedModifiedAt?: string, options?: {
1072
1149
  dryRun?: boolean;
1073
1150
  acceptWarnings?: boolean;
@@ -1095,10 +1172,12 @@ export declare class ApiClient {
1095
1172
  dryRun?: boolean;
1096
1173
  schemaOverride?: string | null;
1097
1174
  defaultAccess?: string | null;
1175
+ metadataManifestOverride?: Record<string, any> | null;
1098
1176
  }): Promise<any>;
1099
1177
  updateDatabaseTypeOperation(appId: string, databaseType: string, name: string, data: Record<string, any>, expectedModifiedAt?: string, options?: {
1100
1178
  dryRun?: boolean;
1101
1179
  schemaOverride?: string | null;
1180
+ metadataManifestOverride?: Record<string, any> | null;
1102
1181
  }): Promise<any>;
1103
1182
  deleteDatabaseTypeOperation(appId: string, databaseType: string, name: string): Promise<any>;
1104
1183
  listDatabaseTypeSubscriptions(appId: string, databaseType: string): Promise<any[]>;
@@ -1123,6 +1202,9 @@ export declare class ApiClient {
1123
1202
  cursor?: string;
1124
1203
  }): Promise<{
1125
1204
  items: any[];
1205
+ nextCursor?: string;
1206
+ hasMore?: boolean;
1207
+ /** @deprecated Alias of `nextCursor` (#1316). */
1126
1208
  cursor?: string;
1127
1209
  }>;
1128
1210
  createGroup(appId: string, data: {
@@ -1141,6 +1223,9 @@ export declare class ApiClient {
1141
1223
  include?: "profiles";
1142
1224
  }): Promise<{
1143
1225
  items: any[];
1226
+ nextCursor?: string;
1227
+ hasMore?: boolean;
1228
+ /** @deprecated Alias of `nextCursor` (#1316). */
1144
1229
  cursor?: string;
1145
1230
  }>;
1146
1231
  addGroupMember(appId: string, groupType: string, groupId: string, data: {
@@ -1351,6 +1436,9 @@ export declare class ApiClient {
1351
1436
  limit?: number;
1352
1437
  }): Promise<{
1353
1438
  items: any[];
1439
+ nextCursor?: string;
1440
+ hasMore?: boolean;
1441
+ /** @deprecated Alias of `nextCursor` (#1316). */
1354
1442
  cursor?: string;
1355
1443
  }>;
1356
1444
  uploadBucketBlob(appId: string, bucketIdOrKey: string, data: Buffer, meta: {
@@ -1362,6 +1450,7 @@ export declare class ApiClient {
1362
1450
  deleteBucketBlob(appId: string, bucketIdOrKey: string, blobId: string): Promise<any>;
1363
1451
  deleteBucketBlobs(appId: string, bucketIdOrKey: string, blobIds: string[]): Promise<any>;
1364
1452
  getBucketBlobSignedUrl(appId: string, bucketIdOrKey: string, blobId: string, expiresInSeconds?: number): Promise<any>;
1453
+ getBucketBlobMetadata(appId: string, bucketIdOrKey: string, blobId: string): Promise<BlobInfo>;
1365
1454
  }
1366
1455
  export declare const apiClient: ApiClient;
1367
1456
  export {};
@@ -313,7 +313,14 @@ export class ApiClient {
313
313
  if (cursor)
314
314
  qs.set("cursor", cursor);
315
315
  const resp = await this.get(`/admin/api/admins/me/apps?${qs.toString()}`);
316
- return { items: Array.isArray(resp.apps) ? resp.apps : [], nextCursor: resp.nextCursor };
316
+ // The server now emits `items` with `apps` as a deprecation-window
317
+ // alias (#1316). Read `items` first so listings keep working once the
318
+ // alias is removed; fall back to the legacy `apps` key meanwhile.
319
+ return {
320
+ items: resp.items ??
321
+ (Array.isArray(resp.apps) ? resp.apps : []),
322
+ nextCursor: resp.nextCursor ?? resp.cursor,
323
+ };
317
324
  });
318
325
  return { apps };
319
326
  }
@@ -403,7 +410,16 @@ export class ApiClient {
403
410
  // INVITATIONS
404
411
  // ============================================
405
412
  async listInvitations(appId, params) {
406
- return this.get(`/app/${appId}/api/invitations`, params);
413
+ const result = await this.get(`/app/${appId}/api/invitations`, params);
414
+ // #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias
415
+ // so the CLI keeps paginating against pre-#1316 servers.
416
+ const nextCursor = result?.nextCursor ?? result?.cursor;
417
+ return {
418
+ items: result?.items ?? [],
419
+ nextCursor,
420
+ hasMore: result?.hasMore ?? nextCursor != null,
421
+ cursor: nextCursor,
422
+ };
407
423
  }
408
424
  async createInvitation(appId, data) {
409
425
  return this.post(`/app/${appId}/api/invitations`, data);
@@ -433,8 +449,9 @@ export class ApiClient {
433
449
  async listDeferredGrants(appId, params) {
434
450
  const result = await this.get(`/app/${appId}/api/deferred-grants`, params);
435
451
  return {
436
- grants: result?.grants ?? [],
452
+ grants: result?.grants ?? result?.items ?? [],
437
453
  nextCursor: result?.nextCursor ?? null,
454
+ hasMore: result?.hasMore,
438
455
  };
439
456
  }
440
457
  async revokeDeferredGrant(appId, deferredId, type) {
@@ -576,6 +593,36 @@ export class ApiClient {
576
593
  const result = await this.get(`/admin/api/apps/${appId}/webhooks/${webhookId}/events`, params);
577
594
  return result;
578
595
  }
596
+ // ============================================
597
+ // CONNECTIONS INSPECTION (#1968)
598
+ // ============================================
599
+ /**
600
+ * List the live connections the server believes exist for exactly one of a
601
+ * document, user, or database. Read-only inspection over admin-api. Rows are
602
+ * per-connection-×-subscription bindings, so `connectionId` is not unique
603
+ * across rows. `params` on a database-subscription row is caller-defined
604
+ * bound CEL input — typed `unknown`.
605
+ */
606
+ async listConnections(appId, selector, params) {
607
+ const result = await this.get(`/admin/api/apps/${appId}/connections`, {
608
+ ...selector,
609
+ ...params,
610
+ });
611
+ return {
612
+ items: result?.items ?? [],
613
+ nextCursor: result?.nextCursor ?? null,
614
+ };
615
+ }
616
+ async listSessions(appId, userId, params) {
617
+ const result = await this.get(`/admin/api/apps/${appId}/sessions`, {
618
+ userId,
619
+ ...params,
620
+ });
621
+ return {
622
+ items: result?.items ?? [],
623
+ nextCursor: result?.nextCursor ?? null,
624
+ };
625
+ }
579
626
  async testWebhook(appId, webhookId, payload) {
580
627
  return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/test`, payload || {});
581
628
  }
@@ -952,9 +999,12 @@ export class ApiClient {
952
999
  }
953
1000
  async listWorkflowRuns(appId, workflowId, params) {
954
1001
  const result = await this.get(`/admin/api/apps/${appId}/workflows/${workflowId}/runs`, params);
1002
+ // #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias.
1003
+ const nextCursor = result?.nextCursor ?? result?.cursor ?? null;
955
1004
  return {
956
1005
  items: result?.items ?? [],
957
- cursor: result?.cursor ?? null,
1006
+ nextCursor,
1007
+ cursor: nextCursor,
958
1008
  };
959
1009
  }
960
1010
  async getWorkflowRunStatus(appId, workflowId, runId) {
@@ -1040,6 +1090,9 @@ export class ApiClient {
1040
1090
  async getAnalyticsEventsGrouped(appId, params) {
1041
1091
  return this.get(`/app/${appId}/api/analytics/events/grouped`, params);
1042
1092
  }
1093
+ async getAnalyticsErrorsGroups(appId, params) {
1094
+ return this.get(`/app/${appId}/api/analytics/errors/groups`, params);
1095
+ }
1043
1096
  async getAnalyticsTopWorkflows(appId, params) {
1044
1097
  return this.get(`/app/${appId}/api/analytics/workflows/top`, params);
1045
1098
  }
@@ -1051,9 +1104,12 @@ export class ApiClient {
1051
1104
  // ============================================
1052
1105
  async listAllAdmins(params) {
1053
1106
  const result = await this.get("/admin/api/admins", params);
1107
+ // #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias.
1108
+ const nextCursor = result?.nextCursor ?? result?.cursor ?? null;
1054
1109
  return {
1055
- admins: result?.admins ?? [],
1056
- cursor: result?.cursor ?? null,
1110
+ admins: result?.admins ?? result?.items ?? [],
1111
+ nextCursor,
1112
+ cursor: nextCursor,
1057
1113
  };
1058
1114
  }
1059
1115
  async searchAdminByEmail(email) {
@@ -1357,8 +1413,14 @@ export class ApiClient {
1357
1413
  async getDatabaseTypeConfig(appId, databaseType) {
1358
1414
  return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}`);
1359
1415
  }
1360
- async createDatabaseTypeConfig(appId, data) {
1361
- return this.post(`/app/${appId}/api/databases/types`, data);
1416
+ async createDatabaseTypeConfig(appId, data,
1417
+ // Issue #1336 (codex round-1 P2): `dryRun` runs every create-time
1418
+ // validation (inline CEL rule lint, manifest shape, schema TOML parse)
1419
+ // WITHOUT persisting, so a fresh-type `sync push --dry-run` surfaces a
1420
+ // malformed `defaultAccess`/manifest the same way the real POST would.
1421
+ options) {
1422
+ const query = options?.dryRun ? "?dryRun=true" : "";
1423
+ return this.post(`/app/${appId}/api/databases/types${query}`, data);
1362
1424
  }
1363
1425
  async updateDatabaseTypeConfig(appId, databaseType, data, expectedModifiedAt, options) {
1364
1426
  const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
@@ -1413,16 +1475,24 @@ export class ApiClient {
1413
1475
  if (options?.dryRun && options.defaultAccess !== undefined) {
1414
1476
  body = { ...body, defaultAccess: options.defaultAccess };
1415
1477
  }
1478
+ if (options?.dryRun && options.metadataManifestOverride !== undefined) {
1479
+ body = { ...body, metadataManifestOverride: options.metadataManifestOverride };
1480
+ }
1416
1481
  return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations${query}`, body);
1417
1482
  }
1418
1483
  async updateDatabaseTypeOperation(appId, databaseType, name, data, expectedModifiedAt,
1419
1484
  // Issue #813 (1A): dry-run runs the op-edit gate without persisting.
1420
1485
  // `schemaOverride` gates against the schema the same push is landing.
1486
+ // Issue #1336: `metadataManifestOverride` gates the op-access lint against
1487
+ // the manifest the same push is landing (see `createDatabaseTypeOperation`).
1421
1488
  options) {
1422
1489
  let body = expectedModifiedAt ? { ...data, expectedModifiedAt } : { ...data };
1423
1490
  if (options?.dryRun && options.schemaOverride !== undefined) {
1424
1491
  body = { ...body, schemaOverride: options.schemaOverride };
1425
1492
  }
1493
+ if (options?.dryRun && options.metadataManifestOverride !== undefined) {
1494
+ body = { ...body, metadataManifestOverride: options.metadataManifestOverride };
1495
+ }
1426
1496
  const query = options?.dryRun ? "?dryRun=true" : "";
1427
1497
  return this.patch(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations/${encodeURIComponent(name)}${query}`, body);
1428
1498
  }
@@ -1466,7 +1536,16 @@ export class ApiClient {
1466
1536
  // GROUPS
1467
1537
  // ============================================
1468
1538
  async listGroups(appId, params) {
1469
- return this.get(`/app/${appId}/api/groups`, params);
1539
+ const result = await this.get(`/app/${appId}/api/groups`, params);
1540
+ // #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias
1541
+ // so the CLI keeps paginating against pre-#1316 servers.
1542
+ const nextCursor = result?.nextCursor ?? result?.cursor;
1543
+ return {
1544
+ items: result?.items ?? [],
1545
+ nextCursor,
1546
+ hasMore: result?.hasMore ?? nextCursor != null,
1547
+ cursor: nextCursor,
1548
+ };
1470
1549
  }
1471
1550
  async createGroup(appId, data) {
1472
1551
  return this.post(`/app/${appId}/api/groups`, data);
@@ -1485,7 +1564,16 @@ export class ApiClient {
1485
1564
  // ============================================
1486
1565
  async listGroupMembers(appId, groupType, groupId, options) {
1487
1566
  const qs = options?.include ? `?include=${options.include}` : "";
1488
- return this.get(`/app/${appId}/api/groups/${groupType}/${groupId}/members${qs}`);
1567
+ const result = await this.get(`/app/${appId}/api/groups/${groupType}/${groupId}/members${qs}`);
1568
+ // #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias
1569
+ // so the CLI keeps paginating against pre-#1316 servers.
1570
+ const nextCursor = result?.nextCursor ?? result?.cursor;
1571
+ return {
1572
+ items: result?.items ?? [],
1573
+ nextCursor,
1574
+ hasMore: result?.hasMore ?? nextCursor != null,
1575
+ cursor: nextCursor,
1576
+ };
1489
1577
  }
1490
1578
  async addGroupMember(appId, groupType, groupId, data) {
1491
1579
  return this.post(`/app/${appId}/api/groups/${groupType}/${groupId}/members`, data);
@@ -1530,7 +1618,7 @@ export class ApiClient {
1530
1618
  const result = await this.get(`/app/${appId}/api/collections${q ? `?${q}` : ""}`);
1531
1619
  return {
1532
1620
  items: result?.items ?? [],
1533
- nextCursor: result?.cursor ?? null,
1621
+ nextCursor: result?.nextCursor ?? result?.cursor ?? null,
1534
1622
  };
1535
1623
  }
1536
1624
  async listAllCollections(appId, params) {
@@ -1543,7 +1631,7 @@ export class ApiClient {
1543
1631
  const result = await this.get(`/app/${appId}/api/admin/collections${q ? `?${q}` : ""}`);
1544
1632
  return {
1545
1633
  items: result?.items ?? [],
1546
- nextCursor: result?.cursor ?? null,
1634
+ nextCursor: result?.nextCursor ?? result?.cursor ?? null,
1547
1635
  };
1548
1636
  }
1549
1637
  async createCollection(appId, data) {
@@ -1571,7 +1659,7 @@ export class ApiClient {
1571
1659
  const result = await this.get(`/app/${appId}/api/collections/${collectionId}/documents${q ? `?${q}` : ""}`);
1572
1660
  return {
1573
1661
  items: result?.items ?? [],
1574
- nextCursor: result?.cursor ?? null,
1662
+ nextCursor: result?.nextCursor ?? result?.cursor ?? null,
1575
1663
  };
1576
1664
  }
1577
1665
  async addCollectionDocument(appId, collectionId, data) {
@@ -1590,7 +1678,7 @@ export class ApiClient {
1590
1678
  const result = await this.get(`/app/${appId}/api/documents/${documentId}/collections${q ? `?${q}` : ""}`);
1591
1679
  return {
1592
1680
  items: result?.items ?? [],
1593
- nextCursor: result?.cursor ?? null,
1681
+ nextCursor: result?.nextCursor ?? result?.cursor ?? null,
1594
1682
  };
1595
1683
  }
1596
1684
  // ============================================
@@ -1860,7 +1948,16 @@ export class ApiClient {
1860
1948
  return this.delete(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}`);
1861
1949
  }
1862
1950
  async listBucketBlobs(appId, bucketIdOrKey, params) {
1863
- return this.get(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs`, params);
1951
+ const result = await this.get(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs`, params);
1952
+ // #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias
1953
+ // so the CLI keeps paginating against pre-#1316 servers.
1954
+ const nextCursor = result?.nextCursor ?? result?.cursor;
1955
+ return {
1956
+ items: result?.items ?? [],
1957
+ nextCursor,
1958
+ hasMore: result?.hasMore ?? nextCursor != null,
1959
+ cursor: nextCursor,
1960
+ };
1864
1961
  }
1865
1962
  async uploadBucketBlob(appId, bucketIdOrKey, data, meta) {
1866
1963
  const credentials = await this.ensureAuthenticated();
@@ -1915,6 +2012,12 @@ export class ApiClient {
1915
2012
  expiresInSeconds: expiresInSeconds || 300,
1916
2013
  });
1917
2014
  }
2015
+ // Blob metadata (#1966): head a single blob without downloading its bytes.
2016
+ // Mirrors listBucketBlobs/downloadBucketBlob addressing; the server heads the
2017
+ // R2 object and returns the serialized BlobInfo shape.
2018
+ async getBucketBlobMetadata(appId, bucketIdOrKey, blobId) {
2019
+ return this.get(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs/${blobId}/metadata`);
2020
+ }
1918
2021
  }
1919
2022
  // Export a singleton instance
1920
2023
  export const apiClient = new ApiClient();