primitive-admin 1.1.0-alpha.73 → 1.1.0-alpha.74

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 (94) hide show
  1. package/README.md +18 -25
  2. package/assets/skill/skills/primitive-platform/SKILL.md +30 -14
  3. package/dist/bin/primitive.js +14 -11
  4. package/dist/bin/primitive.js.map +1 -1
  5. package/dist/src/commands/analytics.js +117 -2
  6. package/dist/src/commands/analytics.js.map +1 -1
  7. package/dist/src/commands/auth.js +13 -107
  8. package/dist/src/commands/auth.js.map +1 -1
  9. package/dist/src/commands/collection-type-configs.js +1 -1
  10. package/dist/src/commands/collection-type-configs.js.map +1 -1
  11. package/dist/src/commands/database-type-configs.js +1 -1
  12. package/dist/src/commands/database-type-configs.js.map +1 -1
  13. package/dist/src/commands/databases.js +1 -1
  14. package/dist/src/commands/databases.js.map +1 -1
  15. package/dist/src/commands/documents.js +749 -29
  16. package/dist/src/commands/documents.js.map +1 -1
  17. package/dist/src/commands/env.js +44 -6
  18. package/dist/src/commands/env.js.map +1 -1
  19. package/dist/src/commands/functions.d.ts +20 -0
  20. package/dist/src/commands/functions.js +329 -0
  21. package/dist/src/commands/functions.js.map +1 -0
  22. package/dist/src/commands/groups.js +1 -1
  23. package/dist/src/commands/groups.js.map +1 -1
  24. package/dist/src/commands/init.js +2 -15
  25. package/dist/src/commands/init.js.map +1 -1
  26. package/dist/src/commands/integrations.js +5 -3
  27. package/dist/src/commands/integrations.js.map +1 -1
  28. package/dist/src/commands/prompts.js +5 -7
  29. package/dist/src/commands/prompts.js.map +1 -1
  30. package/dist/src/commands/rule-sets.js +1 -1
  31. package/dist/src/commands/rule-sets.js.map +1 -1
  32. package/dist/src/commands/scripts.js +6 -7
  33. package/dist/src/commands/scripts.js.map +1 -1
  34. package/dist/src/commands/sync.d.ts +63 -0
  35. package/dist/src/commands/sync.js +650 -2
  36. package/dist/src/commands/sync.js.map +1 -1
  37. package/dist/src/commands/workflows.js +5 -7
  38. package/dist/src/commands/workflows.js.map +1 -1
  39. package/dist/src/lib/api-client.d.ts +156 -1
  40. package/dist/src/lib/api-client.js +219 -3
  41. package/dist/src/lib/api-client.js.map +1 -1
  42. package/dist/src/lib/app-settings-descriptor.js +4 -0
  43. package/dist/src/lib/app-settings-descriptor.js.map +1 -1
  44. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +5 -7
  45. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -1
  46. package/dist/src/lib/config-object-descriptor.js +39 -0
  47. package/dist/src/lib/config-object-descriptor.js.map +1 -1
  48. package/dist/src/lib/config-surface.d.ts +10 -0
  49. package/dist/src/lib/config-surface.js +17 -0
  50. package/dist/src/lib/config-surface.js.map +1 -1
  51. package/dist/src/lib/config.d.ts +13 -2
  52. package/dist/src/lib/config.js +20 -37
  53. package/dist/src/lib/config.js.map +1 -1
  54. package/dist/src/lib/credentials-store.d.ts +4 -12
  55. package/dist/src/lib/credentials-store.js +49 -82
  56. package/dist/src/lib/credentials-store.js.map +1 -1
  57. package/dist/src/lib/env-resolver-core.d.ts +23 -7
  58. package/dist/src/lib/env-resolver-core.js +44 -4
  59. package/dist/src/lib/env-resolver-core.js.map +1 -1
  60. package/dist/src/lib/function-bundle.d.ts +131 -0
  61. package/dist/src/lib/function-bundle.js +321 -0
  62. package/dist/src/lib/function-bundle.js.map +1 -0
  63. package/dist/src/lib/function-sync.d.ts +157 -0
  64. package/dist/src/lib/function-sync.js +371 -0
  65. package/dist/src/lib/function-sync.js.map +1 -0
  66. package/dist/src/lib/function-triggers.d.ts +54 -0
  67. package/dist/src/lib/function-triggers.js +223 -0
  68. package/dist/src/lib/function-triggers.js.map +1 -0
  69. package/dist/src/lib/generated-config-surfaces.d.ts +1 -0
  70. package/dist/src/lib/generated-config-surfaces.js +279 -1
  71. package/dist/src/lib/generated-config-surfaces.js.map +1 -1
  72. package/dist/src/lib/init-ios-links.d.ts +6 -5
  73. package/dist/src/lib/init-ios-links.js +8 -7
  74. package/dist/src/lib/init-ios-links.js.map +1 -1
  75. package/dist/src/lib/init-xcode.d.ts +4 -3
  76. package/dist/src/lib/init-xcode.js +28 -5
  77. package/dist/src/lib/init-xcode.js.map +1 -1
  78. package/dist/src/lib/ios-app-id.d.ts +34 -0
  79. package/dist/src/lib/ios-app-id.js +69 -0
  80. package/dist/src/lib/ios-app-id.js.map +1 -0
  81. package/dist/src/lib/project-config.d.ts +15 -1
  82. package/dist/src/lib/project-config.js +31 -3
  83. package/dist/src/lib/project-config.js.map +1 -1
  84. package/dist/src/lib/sync-resource-types.d.ts +21 -2
  85. package/dist/src/lib/sync-resource-types.js +104 -6
  86. package/dist/src/lib/sync-resource-types.js.map +1 -1
  87. package/dist/src/lib/test-case-variables.d.ts +14 -0
  88. package/dist/src/lib/test-case-variables.js +42 -0
  89. package/dist/src/lib/test-case-variables.js.map +1 -1
  90. package/dist/src/lib/workflow-usage.d.ts +198 -0
  91. package/dist/src/lib/workflow-usage.js +312 -0
  92. package/dist/src/lib/workflow-usage.js.map +1 -0
  93. package/dist/src/types/index.d.ts +12 -2
  94. package/package.json +2 -1
@@ -1,4 +1,5 @@
1
1
  import { type ListEnvelope } from "./paginate.js";
2
+ import { type GlobalAppRef, type WorkflowUsageReport } from "./workflow-usage.js";
2
3
  /**
3
4
  * Metadata for a single bucket blob, as returned by the metadata endpoint
4
5
  * `GET blob-buckets/:bucketId/blobs/:blobId/metadata`. Mirrors the published
@@ -212,10 +213,19 @@ interface AnalyticsUserSummary {
212
213
  lastSeen: string;
213
214
  eventCount: number;
214
215
  }
216
+ /**
217
+ * A `users.top` row. `firstSeen` is the user's first event ever, invariant
218
+ * under `windowDays`; `firstSeenInWindow` is the window-relative one (#3126).
219
+ * `users.search` rows carry no `firstSeenInWindow`: it queries the full
220
+ * retained range, so its `firstSeen` is already the all-time value.
221
+ */
222
+ interface AnalyticsTopUserRow extends AnalyticsUserSummary {
223
+ firstSeenInWindow: string;
224
+ }
215
225
  interface AnalyticsTopUsersResponse {
216
226
  windowDays: number;
217
227
  limit: number;
218
- results: AnalyticsUserSummary[];
228
+ results: AnalyticsTopUserRow[];
219
229
  _timing: AnalyticsTiming;
220
230
  }
221
231
  interface AnalyticsUserSearchResponse {
@@ -257,6 +267,9 @@ interface AnalyticsUserSnapshotResponse {
257
267
  interface AnalyticsEventRow {
258
268
  timestamp: string;
259
269
  user_ulid: string;
270
+ /** Display name for `user_ulid` — the system principal's `System`, else
271
+ * null. The id itself is unchanged (#3168). */
272
+ user_name: string | null;
260
273
  action: string;
261
274
  feature: string;
262
275
  route: string;
@@ -468,6 +481,22 @@ export declare class ApiClient {
468
481
  role?: string;
469
482
  }>;
470
483
  }>;
484
+ /**
485
+ * Every app on the server, not just the caller's own.
486
+ *
487
+ * Deliberately separate from `listApps()`, whose contract is "apps assigned
488
+ * to me" (`GET /admin/api/admins/me/apps`, backed by `admin.queryApps()`) —
489
+ * the contract every other command depends on. `analytics workflow-usage
490
+ * --all-apps` has to reach apps nobody is assigned to, which is the global
491
+ * route `GET /admin/api/apps` (`App.iterateAll()`), and that route
492
+ * identifies an app as `appId` rather than `id` (D3130-001).
493
+ *
494
+ * One drain of that route is lossy — its cursor re-scans `App.iterateAll()`
495
+ * from the start of each page, so a list that changes under it both repeats
496
+ * and drops rows — so the walk is the union of repeated drains
497
+ * (`drainAppsUntilConverged`, which carries the measurement).
498
+ */
499
+ listAllAppsGlobal(): Promise<GlobalAppRef[]>;
471
500
  createApp(data: {
472
501
  name: string;
473
502
  initialAdminEmail?: string;
@@ -663,6 +692,48 @@ export declare class ApiClient {
663
692
  * and refused on every configuration write path.
664
693
  */
665
694
  setWebhookAvailability(appId: string, webhookId: string, action: "enable" | "disable"): Promise<any>;
695
+ listFunctions(appId: string): Promise<{
696
+ items: any[];
697
+ }>;
698
+ getFunction(appId: string, functionId: string): Promise<any>;
699
+ createFunction(appId: string, payload: any): Promise<any>;
700
+ updateFunction(appId: string, functionId: string, payload: any): Promise<any>;
701
+ /**
702
+ * Archive a function, or destroy it with `hard`. Archiving KEEPS the key
703
+ * reserved in the app's single key namespace; the hard delete is what
704
+ * releases it, destroys every config version and removes the R2 objects.
705
+ */
706
+ deleteFunction(appId: string, functionId: string, options?: {
707
+ hard?: boolean;
708
+ }): Promise<ArchiveEnvelope>;
709
+ /** #2803 — the operational verb pair for a server function. */
710
+ setFunctionAvailability(appId: string, functionId: string, action: "enable" | "disable"): Promise<any>;
711
+ /** Every config version of a function, oldest first. */
712
+ /**
713
+ * A function's runs, newest first (#3181).
714
+ *
715
+ * A function run has no `workflowId`, so no workflow runs listing can see
716
+ * it — this is the only surface that lists one. Cursor-paginated like the
717
+ * other admin listings.
718
+ */
719
+ listFunctionRuns(appId: string, functionId: string, params?: {
720
+ limit?: number;
721
+ cursor?: string;
722
+ }): Promise<{
723
+ items: any[];
724
+ nextCursor?: string | null;
725
+ }>;
726
+ listFunctionConfigs(appId: string, functionId: string): Promise<{
727
+ items: any[];
728
+ }>;
729
+ getFunctionConfig(appId: string, functionId: string, configId: string): Promise<any>;
730
+ /**
731
+ * The stored envelope: the authored TOML bytes, every source file and the
732
+ * built bundle, all base64. This is what `config pull` writes back verbatim.
733
+ */
734
+ getFunctionConfigEnvelope(appId: string, functionId: string, configId: string): Promise<any>;
735
+ /** Push a config version. The server verifies `contentHash` itself. */
736
+ pushFunctionConfig(appId: string, functionId: string, payload: any): Promise<any>;
666
737
  /** #2803 — the operational verb pair for a prompt (admin API). */
667
738
  setPromptAvailability(appId: string, promptId: string, action: "enable" | "disable"): Promise<any>;
668
739
  /** #2803 — the operational verb pair for an integration (admin API). */
@@ -978,6 +1049,16 @@ export declare class ApiClient {
978
1049
  nextCursor?: string | null;
979
1050
  }>;
980
1051
  getWorkflow(appId: string, workflowId: string): Promise<any>;
1052
+ /**
1053
+ * One app's workflow step usage report (#3132).
1054
+ *
1055
+ * `windowDays` is omitted from the query when the caller did not ask for a
1056
+ * window, so the server applies its own default rather than the CLI
1057
+ * restating it on the wire.
1058
+ */
1059
+ getWorkflowUsage(appId: string, params?: {
1060
+ windowDays?: number;
1061
+ }): Promise<WorkflowUsageReport>;
981
1062
  createWorkflow(appId: string, payload: any): Promise<any>;
982
1063
  updateWorkflow(appId: string, workflowId: string, payload: any, expectedModifiedAt?: string): Promise<any>;
983
1064
  /**
@@ -1136,6 +1217,20 @@ export declare class ApiClient {
1136
1217
  windowDays?: number;
1137
1218
  limit?: number;
1138
1219
  }): Promise<TopPromptsResponse>;
1220
+ /**
1221
+ * The signed-in admin's own profile, `role` included.
1222
+ *
1223
+ * The role is read from the server rather than from the cached credentials
1224
+ * file: a role changed after login would otherwise let a stale local record
1225
+ * decide an authorization question.
1226
+ */
1227
+ getMe(): Promise<{
1228
+ adminId: string;
1229
+ email: string;
1230
+ name: string;
1231
+ role: string;
1232
+ createdAt?: string;
1233
+ }>;
1139
1234
  listAllAdmins(params?: {
1140
1235
  limit?: number;
1141
1236
  cursor?: string;
@@ -1710,7 +1805,45 @@ export declare class ApiClient {
1710
1805
  byteLength: number;
1711
1806
  documentId: string;
1712
1807
  }>;
1808
+ /**
1809
+ * The export chain of a large document (#2816, behavior 20): the latest
1810
+ * completed snapshot, the sealed overlays after its epoch, and the open
1811
+ * epoch's overlay inline. Every other artifact arrives as an expiring
1812
+ * signature, read through {@link downloadDocumentArtifact}.
1813
+ */
1814
+ exportDocumentChain(appId: string, documentId: string): Promise<any>;
1815
+ /**
1816
+ * Read one granted artifact of a large document.
1817
+ *
1818
+ * The path comes from the chain and is origin-relative — the room is a
1819
+ * Durable Object and has no reliable idea which host the caller reached it
1820
+ * through — and the signature in it is the whole authorization, so no key is
1821
+ * ever composed here.
1822
+ */
1823
+ downloadDocumentArtifact(_appId: string, grantedPath: string): Promise<Buffer>;
1713
1824
  importDocumentState(appId: string, documentId: string, stateBase64: string): Promise<any>;
1825
+ /**
1826
+ * Upload one artifact of a large document's import chain (#2816,
1827
+ * behavior 20).
1828
+ *
1829
+ * The bytes travel raw and unmodified: the manifest's digest is over the
1830
+ * STORED bytes, so re-encoding a chunk here would fail its own verification
1831
+ * on install. The query says which artifact it is — the server derives the
1832
+ * key from the document it belongs to, so nothing here composes one.
1833
+ */
1834
+ uploadDocumentChainArtifact(appId: string, documentId: string, query: Record<string, string | number>, body: Buffer): Promise<any>;
1835
+ /** Start installing an uploaded chain (#2816, behavior 20). */
1836
+ installDocumentChain(appId: string, documentId: string, plan: {
1837
+ installId?: string;
1838
+ base: {
1839
+ epoch: number;
1840
+ buildId: string;
1841
+ } | null;
1842
+ overlays: number[];
1843
+ currentEpoch: number;
1844
+ }): Promise<any>;
1845
+ /** How far a document's chain install has got (#2816, behavior 20). */
1846
+ getDocumentChainInstall(appId: string, documentId: string): Promise<any>;
1714
1847
  getDocument(appId: string, documentId: string): Promise<any>;
1715
1848
  /**
1716
1849
  * Delete a document and everything hanging off it (#2756). The server route
@@ -1830,6 +1963,7 @@ export declare class ApiClient {
1830
1963
  documentId?: string;
1831
1964
  tags?: string[];
1832
1965
  createdBy?: string;
1966
+ documentFormat?: 1 | 2;
1833
1967
  }): Promise<any>;
1834
1968
  listDocumentPermissions(appId: string, documentId: string): Promise<any[]>;
1835
1969
  grantDocumentPermission(appId: string, documentId: string, permissions: Array<{
@@ -1879,6 +2013,27 @@ export declare class ApiClient {
1879
2013
  };
1880
2014
  }>;
1881
2015
  listAdminDocuments(appId: string, userId: string): Promise<any[]>;
2016
+ /**
2017
+ * The document `AppUser.rootDocId` points at, or null when the user has none
2018
+ * yet (#3135). A user who is not a member of the app is a 404, which
2019
+ * `documents import` reads as "this export has no target here" — distinct
2020
+ * from "this user has no root document yet".
2021
+ */
2022
+ getUserRootDocument(appId: string, userId: string): Promise<{
2023
+ rootDocId: string | null;
2024
+ }>;
2025
+ /**
2026
+ * Get or create the user's root document (#3135).
2027
+ *
2028
+ * The server runs the same idempotent get-or-create sign-in uses, so this
2029
+ * never mints a second root. `created` is true only when this call's claim
2030
+ * won — an import must not report "created", or merge without `--overwrite`,
2031
+ * for a root a concurrent sign-in minted first.
2032
+ */
2033
+ ensureUserRootDocument(appId: string, userId: string): Promise<{
2034
+ rootDocId: string;
2035
+ created: boolean;
2036
+ }>;
1882
2037
  /**
1883
2038
  * Apply an ordered batch of record operations via `records/batch`.
1884
2039
  *
@@ -1,7 +1,9 @@
1
1
  import { loadCredentials, saveCredentials, isTokenExpiringSoon, } from "./config.js";
2
+ import { getCurrentEnvNameSafe } from "./env-resolver.js";
2
3
  import { fetchWithTLS } from "./fetch.js";
3
4
  import { paginateAll, normalizeCliListEnvelope } from "./paginate.js";
4
5
  import { RefreshError, refreshAdminCredentials, } from "./refresh-admin-credentials.js";
6
+ import { drainAppsUntilConverged, } from "./workflow-usage.js";
5
7
  export class ApiError extends Error {
6
8
  statusCode;
7
9
  code;
@@ -211,7 +213,15 @@ export class ApiClient {
211
213
  this.credentials = loadCredentials();
212
214
  }
213
215
  if (!this.credentials) {
214
- throw new ApiError("Not logged in. Run 'bao-admin login' first.", 401);
216
+ // Name the environment when there is one (#1293). Before #3152 this case
217
+ // surfaced from `resolveAppId`, because an unauthenticated environment
218
+ // resolved no app at all; now the environment always names its app, so
219
+ // "not logged in" is only ever an auth failure, and it is reported here,
220
+ // once, for every command.
221
+ const envName = getCurrentEnvNameSafe();
222
+ throw new ApiError(envName
223
+ ? `Not logged in to environment "${envName}". Run 'primitive -e ${envName} login'.`
224
+ : "Not logged in. Run 'primitive login' first.", 401);
215
225
  }
216
226
  // Check if token needs refresh
217
227
  if (isTokenExpiringSoon(this.credentials)) {
@@ -345,8 +355,8 @@ export class ApiClient {
345
355
  async listApps() {
346
356
  // The server paginates cursor-style (default pageSize=25, cap=100). Historically
347
357
  // this method issued a single un-paginated GET and silently truncated to the
348
- // first page, which broke `primitive use <app-id>` and `primitive apps list`
349
- // for admins with more than 25 apps (see #436). Iterate every page.
358
+ // first page, which broke `primitive apps list` for admins with more than
359
+ // 25 apps (see #436). Iterate every page.
350
360
  const apps = await paginateAll(async (cursor) => {
351
361
  const qs = new URLSearchParams({ limit: "100" });
352
362
  if (cursor)
@@ -363,6 +373,40 @@ export class ApiClient {
363
373
  });
364
374
  return { apps };
365
375
  }
376
+ /**
377
+ * Every app on the server, not just the caller's own.
378
+ *
379
+ * Deliberately separate from `listApps()`, whose contract is "apps assigned
380
+ * to me" (`GET /admin/api/admins/me/apps`, backed by `admin.queryApps()`) —
381
+ * the contract every other command depends on. `analytics workflow-usage
382
+ * --all-apps` has to reach apps nobody is assigned to, which is the global
383
+ * route `GET /admin/api/apps` (`App.iterateAll()`), and that route
384
+ * identifies an app as `appId` rather than `id` (D3130-001).
385
+ *
386
+ * One drain of that route is lossy — its cursor re-scans `App.iterateAll()`
387
+ * from the start of each page, so a list that changes under it both repeats
388
+ * and drops rows — so the walk is the union of repeated drains
389
+ * (`drainAppsUntilConverged`, which carries the measurement).
390
+ */
391
+ async listAllAppsGlobal() {
392
+ const drainOnce = () => paginateAll(async (cursor) => {
393
+ const qs = new URLSearchParams({ limit: "100" });
394
+ if (cursor)
395
+ qs.set("cursor", cursor);
396
+ const resp = await this.get(`/admin/api/apps?${qs.toString()}`);
397
+ // `items` first, with the `apps` alias (#1316) as the fallback,
398
+ // exactly as `listApps()` reads its own envelope.
399
+ const rows = resp.items ?? (Array.isArray(resp.apps) ? resp.apps : []);
400
+ return {
401
+ items: rows.map((row) => ({
402
+ appId: String(row.appId ?? row.id ?? ""),
403
+ name: String(row.name ?? ""),
404
+ })),
405
+ nextCursor: resp.nextCursor ?? resp.cursor,
406
+ };
407
+ });
408
+ return drainAppsUntilConverged(drainOnce);
409
+ }
366
410
  async createApp(data) {
367
411
  return this.post("/admin/api/apps", data);
368
412
  }
@@ -678,6 +722,68 @@ export class ApiClient {
678
722
  async setWebhookAvailability(appId, webhookId, action) {
679
723
  return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/${action}`, {});
680
724
  }
725
+ // ── Server functions (#3179) ─────────────────────────────────────────
726
+ //
727
+ // The `functions` family mirrors workflows and scripts: a header with the
728
+ // gate and the entry, and append-only immutable config versions carrying the
729
+ // authored TOML bytes, the sources and the built bundle. The listing drains
730
+ // server-side, so it has no cursor.
731
+ async listFunctions(appId) {
732
+ const result = await this.get(`/admin/api/apps/${appId}/functions`);
733
+ return { items: result?.items ?? [] };
734
+ }
735
+ async getFunction(appId, functionId) {
736
+ return this.get(`/admin/api/apps/${appId}/functions/${functionId}`);
737
+ }
738
+ async createFunction(appId, payload) {
739
+ return this.post(`/admin/api/apps/${appId}/functions`, payload);
740
+ }
741
+ async updateFunction(appId, functionId, payload) {
742
+ return this.patch(`/admin/api/apps/${appId}/functions/${functionId}`, payload);
743
+ }
744
+ /**
745
+ * Archive a function, or destroy it with `hard`. Archiving KEEPS the key
746
+ * reserved in the app's single key namespace; the hard delete is what
747
+ * releases it, destroys every config version and removes the R2 objects.
748
+ */
749
+ async deleteFunction(appId, functionId, options) {
750
+ const query = options?.hard ? "?hard=true" : "";
751
+ return this.delete(`/admin/api/apps/${appId}/functions/${functionId}${query}`);
752
+ }
753
+ /** #2803 — the operational verb pair for a server function. */
754
+ async setFunctionAvailability(appId, functionId, action) {
755
+ return this.post(`/admin/api/apps/${appId}/functions/${functionId}/${action}`, {});
756
+ }
757
+ /** Every config version of a function, oldest first. */
758
+ /**
759
+ * A function's runs, newest first (#3181).
760
+ *
761
+ * A function run has no `workflowId`, so no workflow runs listing can see
762
+ * it — this is the only surface that lists one. Cursor-paginated like the
763
+ * other admin listings.
764
+ */
765
+ async listFunctionRuns(appId, functionId, params) {
766
+ const result = await this.get(`/admin/api/apps/${appId}/functions/${functionId}/runs`, params);
767
+ return { items: result?.items ?? [], nextCursor: result?.nextCursor ?? null };
768
+ }
769
+ async listFunctionConfigs(appId, functionId) {
770
+ const result = await this.get(`/admin/api/apps/${appId}/functions/${functionId}/configs`);
771
+ return { items: result?.items ?? [] };
772
+ }
773
+ async getFunctionConfig(appId, functionId, configId) {
774
+ return this.get(`/admin/api/apps/${appId}/functions/${functionId}/configs/${configId}`);
775
+ }
776
+ /**
777
+ * The stored envelope: the authored TOML bytes, every source file and the
778
+ * built bundle, all base64. This is what `config pull` writes back verbatim.
779
+ */
780
+ async getFunctionConfigEnvelope(appId, functionId, configId) {
781
+ return this.get(`/admin/api/apps/${appId}/functions/${functionId}/configs/${configId}/envelope`);
782
+ }
783
+ /** Push a config version. The server verifies `contentHash` itself. */
784
+ async pushFunctionConfig(appId, functionId, payload) {
785
+ return this.post(`/admin/api/apps/${appId}/functions/${functionId}/configs`, payload);
786
+ }
681
787
  /** #2803 — the operational verb pair for a prompt (admin API). */
682
788
  async setPromptAvailability(appId, promptId, action) {
683
789
  return this.post(`/admin/api/apps/${appId}/prompts/${promptId}/${action}`, {});
@@ -1166,6 +1272,16 @@ export class ApiClient {
1166
1272
  async getWorkflow(appId, workflowId) {
1167
1273
  return this.get(`/admin/api/apps/${appId}/workflows/${workflowId}`);
1168
1274
  }
1275
+ /**
1276
+ * One app's workflow step usage report (#3132).
1277
+ *
1278
+ * `windowDays` is omitted from the query when the caller did not ask for a
1279
+ * window, so the server applies its own default rather than the CLI
1280
+ * restating it on the wire.
1281
+ */
1282
+ async getWorkflowUsage(appId, params) {
1283
+ return this.get(`/admin/api/apps/${appId}/workflows/usage`, params);
1284
+ }
1169
1285
  async createWorkflow(appId, payload) {
1170
1286
  return this.post(`/admin/api/apps/${appId}/workflows`, payload);
1171
1287
  }
@@ -1327,6 +1443,16 @@ export class ApiClient {
1327
1443
  // ============================================
1328
1444
  // SUPER ADMIN: ADMINS
1329
1445
  // ============================================
1446
+ /**
1447
+ * The signed-in admin's own profile, `role` included.
1448
+ *
1449
+ * The role is read from the server rather than from the cached credentials
1450
+ * file: a role changed after login would otherwise let a stale local record
1451
+ * decide an authorization question.
1452
+ */
1453
+ async getMe() {
1454
+ return this.get("/admin/api/me");
1455
+ }
1330
1456
  async listAllAdmins(params) {
1331
1457
  const result = await this.get("/admin/api/admins", params);
1332
1458
  // #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias.
@@ -2133,9 +2259,79 @@ export class ApiClient {
2133
2259
  async exportDocumentState(appId, documentId) {
2134
2260
  return this.get(`/app/${appId}/api/documents/${documentId}/export/state`);
2135
2261
  }
2262
+ /**
2263
+ * The export chain of a large document (#2816, behavior 20): the latest
2264
+ * completed snapshot, the sealed overlays after its epoch, and the open
2265
+ * epoch's overlay inline. Every other artifact arrives as an expiring
2266
+ * signature, read through {@link downloadDocumentArtifact}.
2267
+ */
2268
+ async exportDocumentChain(appId, documentId) {
2269
+ return this.get(`/app/${appId}/api/documents/${documentId}/export/chain`);
2270
+ }
2271
+ /**
2272
+ * Read one granted artifact of a large document.
2273
+ *
2274
+ * The path comes from the chain and is origin-relative — the room is a
2275
+ * Durable Object and has no reliable idea which host the caller reached it
2276
+ * through — and the signature in it is the whole authorization, so no key is
2277
+ * ever composed here.
2278
+ */
2279
+ async downloadDocumentArtifact(_appId, grantedPath) {
2280
+ const credentials = await this.ensureAuthenticated();
2281
+ const url = `${credentials.serverUrl}${grantedPath}`;
2282
+ const response = await fetchWithTLS(url);
2283
+ if (!response.ok) {
2284
+ throw new ApiError(`Failed to download document artifact: ${response.statusText}`, response.status);
2285
+ }
2286
+ return Buffer.from(await response.arrayBuffer());
2287
+ }
2136
2288
  async importDocumentState(appId, documentId, stateBase64) {
2137
2289
  return this.post(`/app/${appId}/api/documents/${documentId}/import/state`, { state: stateBase64 });
2138
2290
  }
2291
+ /**
2292
+ * Upload one artifact of a large document's import chain (#2816,
2293
+ * behavior 20).
2294
+ *
2295
+ * The bytes travel raw and unmodified: the manifest's digest is over the
2296
+ * STORED bytes, so re-encoding a chunk here would fail its own verification
2297
+ * on install. The query says which artifact it is — the server derives the
2298
+ * key from the document it belongs to, so nothing here composes one.
2299
+ */
2300
+ async uploadDocumentChainArtifact(appId, documentId, query, body) {
2301
+ const credentials = await this.ensureAuthenticated();
2302
+ const search = new URLSearchParams();
2303
+ for (const [key, value] of Object.entries(query)) {
2304
+ search.set(key, String(value));
2305
+ }
2306
+ const url = `${credentials.serverUrl}/app/${appId}/api/documents/${documentId}` +
2307
+ `/import/chain/artifact?${search.toString()}`;
2308
+ const headers = {
2309
+ Authorization: `Bearer ${credentials.accessToken}`,
2310
+ "Content-Type": "application/octet-stream",
2311
+ };
2312
+ if (credentials.globalAdminAppId) {
2313
+ headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
2314
+ }
2315
+ const response = await fetchWithTLS(url, {
2316
+ method: "POST",
2317
+ headers,
2318
+ body: body,
2319
+ });
2320
+ const result = await response.json().catch(() => ({}));
2321
+ if (!response.ok) {
2322
+ throw new ApiError(result?.error ||
2323
+ `Failed to upload import artifact: ${response.statusText}`, response.status);
2324
+ }
2325
+ return result;
2326
+ }
2327
+ /** Start installing an uploaded chain (#2816, behavior 20). */
2328
+ async installDocumentChain(appId, documentId, plan) {
2329
+ return this.post(`/app/${appId}/api/documents/${documentId}/import/chain`, plan);
2330
+ }
2331
+ /** How far a document's chain install has got (#2816, behavior 20). */
2332
+ async getDocumentChainInstall(appId, documentId) {
2333
+ return this.get(`/app/${appId}/api/documents/${documentId}/import/chain`);
2334
+ }
2139
2335
  async getDocument(appId, documentId) {
2140
2336
  return this.get(`/app/${appId}/api/documents/${documentId}`);
2141
2337
  }
@@ -2352,6 +2548,26 @@ export class ApiClient {
2352
2548
  const result = await this.get(`/admin/api/apps/${appId}/documents`, { userId });
2353
2549
  return result?.documents || result || [];
2354
2550
  }
2551
+ /**
2552
+ * The document `AppUser.rootDocId` points at, or null when the user has none
2553
+ * yet (#3135). A user who is not a member of the app is a 404, which
2554
+ * `documents import` reads as "this export has no target here" — distinct
2555
+ * from "this user has no root document yet".
2556
+ */
2557
+ async getUserRootDocument(appId, userId) {
2558
+ return this.get(`/admin/api/apps/${appId}/users/${userId}/root-document`);
2559
+ }
2560
+ /**
2561
+ * Get or create the user's root document (#3135).
2562
+ *
2563
+ * The server runs the same idempotent get-or-create sign-in uses, so this
2564
+ * never mints a second root. `created` is true only when this call's claim
2565
+ * won — an import must not report "created", or merge without `--overwrite`,
2566
+ * for a root a concurrent sign-in minted first.
2567
+ */
2568
+ async ensureUserRootDocument(appId, userId) {
2569
+ return this.post(`/admin/api/apps/${appId}/users/${userId}/root-document`, {});
2570
+ }
2355
2571
  // ============================================
2356
2572
  // DATABASE EXPORT / IMPORT
2357
2573
  // ============================================