primitive-admin 1.1.0-alpha.8 → 1.1.0-alpha.80
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/README.md +444 -106
- package/assets/skill/skills/primitive-platform/SKILL.md +811 -0
- package/dist/bin/primitive.d.ts +2 -0
- package/dist/bin/primitive.js +373 -21
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/admins.d.ts +2 -0
- package/dist/src/commands/admins.js +128 -19
- package/dist/src/commands/admins.js.map +1 -1
- package/dist/src/commands/analytics.d.ts +2 -0
- package/dist/src/commands/analytics.js +700 -55
- package/dist/src/commands/analytics.js.map +1 -1
- package/dist/src/commands/apps.d.ts +2 -0
- package/dist/src/commands/apps.js +54 -96
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/auth.d.ts +2 -0
- package/dist/src/commands/auth.js +196 -105
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/blob-buckets.d.ts +2 -0
- package/dist/src/commands/blob-buckets.js +330 -0
- package/dist/src/commands/blob-buckets.js.map +1 -0
- package/dist/src/commands/catalog.d.ts +2 -0
- package/dist/src/commands/catalog.js +37 -38
- package/dist/src/commands/catalog.js.map +1 -1
- package/dist/src/commands/collection-type-configs.d.ts +2 -0
- package/dist/src/commands/collection-type-configs.js +84 -0
- package/dist/src/commands/collection-type-configs.js.map +1 -0
- package/dist/src/commands/collections.d.ts +2 -0
- package/dist/src/commands/collections.js +565 -0
- package/dist/src/commands/collections.js.map +1 -0
- package/dist/src/commands/comparisons.d.ts +2 -0
- package/dist/src/commands/comparisons.js +6 -6
- package/dist/src/commands/comparisons.js.map +1 -1
- package/dist/src/commands/config.d.ts +46 -0
- package/dist/src/commands/config.js +465 -0
- package/dist/src/commands/config.js.map +1 -0
- package/dist/src/commands/connections.d.ts +2 -0
- package/dist/src/commands/connections.js +100 -0
- package/dist/src/commands/connections.js.map +1 -0
- package/dist/src/commands/cron-triggers.d.ts +2 -0
- package/dist/src/commands/cron-triggers.js +265 -0
- package/dist/src/commands/cron-triggers.js.map +1 -0
- package/dist/src/commands/database-type-configs.d.ts +2 -0
- package/dist/src/commands/database-type-configs.js +163 -0
- package/dist/src/commands/database-type-configs.js.map +1 -0
- package/dist/src/commands/database-types.d.ts +2 -0
- package/dist/src/commands/database-types.js +171 -0
- package/dist/src/commands/database-types.js.map +1 -0
- package/dist/src/commands/databases.d.ts +65 -0
- package/dist/src/commands/databases.js +1859 -238
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.d.ts +2 -0
- package/dist/src/commands/documents.js +2062 -19
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/email-templates.d.ts +2 -0
- package/dist/src/commands/email-templates.js +174 -0
- package/dist/src/commands/email-templates.js.map +1 -0
- package/dist/src/commands/env.d.ts +23 -0
- package/dist/src/commands/env.js +385 -0
- package/dist/src/commands/env.js.map +1 -0
- package/dist/src/commands/feature-flags.d.ts +14 -0
- package/dist/src/commands/feature-flags.js +116 -0
- package/dist/src/commands/feature-flags.js.map +1 -0
- package/dist/src/commands/functions.d.ts +20 -0
- package/dist/src/commands/functions.js +672 -0
- package/dist/src/commands/functions.js.map +1 -0
- package/dist/src/commands/group-type-configs.d.ts +2 -0
- package/dist/src/commands/group-type-configs.js +86 -0
- package/dist/src/commands/group-type-configs.js.map +1 -0
- package/dist/src/commands/groups.d.ts +2 -0
- package/dist/src/commands/groups.js +39 -108
- package/dist/src/commands/groups.js.map +1 -1
- package/dist/src/commands/guides.d.ts +223 -0
- package/dist/src/commands/guides.js +617 -65
- package/dist/src/commands/guides.js.map +1 -1
- package/dist/src/commands/init.d.ts +37 -0
- package/dist/src/commands/init.js +1693 -208
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/integrations.d.ts +2 -0
- package/dist/src/commands/integrations.js +402 -182
- package/dist/src/commands/integrations.js.map +1 -1
- package/dist/src/commands/llm.d.ts +2 -0
- package/dist/src/commands/llm.js +13 -8
- package/dist/src/commands/llm.js.map +1 -1
- package/dist/src/commands/locks.d.ts +8 -0
- package/dist/src/commands/locks.js +160 -0
- package/dist/src/commands/locks.js.map +1 -0
- package/dist/src/commands/metadata-category-configs.d.ts +12 -0
- package/dist/src/commands/metadata-category-configs.js +112 -0
- package/dist/src/commands/metadata-category-configs.js.map +1 -0
- package/dist/src/commands/metadata.d.ts +2 -0
- package/dist/src/commands/metadata.js +281 -0
- package/dist/src/commands/metadata.js.map +1 -0
- package/dist/src/commands/prompts.d.ts +2 -0
- package/dist/src/commands/prompts.js +275 -621
- package/dist/src/commands/prompts.js.map +1 -1
- package/dist/src/commands/rule-sets.d.ts +3 -0
- package/dist/src/commands/rule-sets.js +137 -147
- package/dist/src/commands/rule-sets.js.map +1 -1
- package/dist/src/commands/scripts.d.ts +30 -0
- package/dist/src/commands/scripts.js +679 -0
- package/dist/src/commands/scripts.js.map +1 -0
- package/dist/src/commands/secrets.d.ts +2 -0
- package/dist/src/commands/secrets.js +108 -0
- package/dist/src/commands/secrets.js.map +1 -0
- package/dist/src/commands/sessions.d.ts +2 -0
- package/dist/src/commands/sessions.js +75 -0
- package/dist/src/commands/sessions.js.map +1 -0
- package/dist/src/commands/settings.d.ts +18 -0
- package/dist/src/commands/settings.js +54 -0
- package/dist/src/commands/settings.js.map +1 -0
- package/dist/src/commands/skill.d.ts +2 -0
- package/dist/src/commands/skill.js +29 -0
- package/dist/src/commands/skill.js.map +1 -0
- package/dist/src/commands/sync-app-settings.d.ts +158 -0
- package/dist/src/commands/sync-app-settings.js +328 -0
- package/dist/src/commands/sync-app-settings.js.map +1 -0
- package/dist/src/commands/sync.d.ts +2514 -0
- package/dist/src/commands/sync.js +16106 -848
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/tokens.d.ts +2 -0
- package/dist/src/commands/tokens.js +130 -21
- package/dist/src/commands/tokens.js.map +1 -1
- package/dist/src/commands/users.d.ts +2 -0
- package/dist/src/commands/users.js +532 -23
- package/dist/src/commands/users.js.map +1 -1
- package/dist/src/commands/vars.d.ts +8 -0
- package/dist/src/commands/vars.js +96 -0
- package/dist/src/commands/vars.js.map +1 -0
- package/dist/src/commands/waitlist.d.ts +2 -0
- package/dist/src/commands/waitlist.js +10 -10
- package/dist/src/commands/waitlist.js.map +1 -1
- package/dist/src/commands/webhooks.d.ts +2 -0
- package/dist/src/commands/webhooks.js +562 -0
- package/dist/src/commands/webhooks.js.map +1 -0
- package/dist/src/commands/workflows.d.ts +116 -0
- package/dist/src/commands/workflows.js +1638 -729
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/access-rule-display.d.ts +21 -0
- package/dist/src/lib/access-rule-display.js +34 -0
- package/dist/src/lib/access-rule-display.js.map +1 -0
- package/dist/src/lib/api-client.d.ts +2147 -0
- package/dist/src/lib/api-client.js +2042 -160
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/app-match-guard.d.ts +44 -0
- package/dist/src/lib/app-match-guard.js +72 -0
- package/dist/src/lib/app-match-guard.js.map +1 -0
- package/dist/src/lib/app-settings-descriptor.d.ts +263 -0
- package/dist/src/lib/app-settings-descriptor.js +584 -0
- package/dist/src/lib/app-settings-descriptor.js.map +1 -0
- package/dist/src/lib/auth-flow.d.ts +8 -0
- package/dist/src/lib/batch.d.ts +26 -0
- package/dist/src/lib/batch.js +32 -0
- package/dist/src/lib/batch.js.map +1 -0
- package/dist/src/lib/block-layout.d.ts +160 -0
- package/dist/src/lib/block-layout.js +451 -0
- package/dist/src/lib/block-layout.js.map +1 -0
- package/dist/src/lib/block-selector.d.ts +58 -0
- package/dist/src/lib/block-selector.js +92 -0
- package/dist/src/lib/block-selector.js.map +1 -0
- package/dist/src/lib/canonical-json.d.ts +12 -0
- package/dist/src/lib/canonical-json.js +35 -0
- package/dist/src/lib/canonical-json.js.map +1 -0
- package/dist/src/lib/channel.d.ts +30 -0
- package/dist/src/lib/channel.js +68 -0
- package/dist/src/lib/channel.js.map +1 -0
- package/dist/src/lib/cli-manifest.d.ts +68 -0
- package/dist/src/lib/cli-manifest.js +71 -0
- package/dist/src/lib/cli-manifest.js.map +1 -0
- package/dist/src/lib/codegen-shared/generatedFiles.d.ts +101 -0
- package/dist/src/lib/codegen-shared/generatedFiles.js +191 -0
- package/dist/src/lib/codegen-shared/generatedFiles.js.map +1 -0
- package/dist/src/lib/codegen-shared/prettierStable.d.ts +262 -0
- package/dist/src/lib/codegen-shared/prettierStable.js +610 -0
- package/dist/src/lib/codegen-shared/prettierStable.js.map +1 -0
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +38 -0
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +46 -0
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -0
- package/dist/src/lib/config-json-field.d.ts +28 -0
- package/dist/src/lib/config-json-field.js +56 -0
- package/dist/src/lib/config-json-field.js.map +1 -0
- package/dist/src/lib/config-object-descriptor.d.ts +127 -0
- package/dist/src/lib/config-object-descriptor.js +696 -0
- package/dist/src/lib/config-object-descriptor.js.map +1 -0
- package/dist/src/lib/config-payload.d.ts +85 -0
- package/dist/src/lib/config-payload.js +116 -0
- package/dist/src/lib/config-payload.js.map +1 -0
- package/dist/src/lib/config-surface.d.ts +140 -0
- package/dist/src/lib/config-surface.js +317 -0
- package/dist/src/lib/config-surface.js.map +1 -0
- package/dist/src/lib/config-toml.d.ts +10 -0
- package/dist/src/lib/config-toml.js +42 -0
- package/dist/src/lib/config-toml.js.map +1 -0
- package/dist/src/lib/config.d.ts +71 -0
- package/dist/src/lib/config.js +71 -68
- package/dist/src/lib/config.js.map +1 -1
- package/dist/src/lib/confirm-prompt.d.ts +83 -0
- package/dist/src/lib/confirm-prompt.js +110 -0
- package/dist/src/lib/confirm-prompt.js.map +1 -0
- package/dist/src/lib/constants.d.ts +11 -0
- package/dist/src/lib/constants.js +12 -0
- package/dist/src/lib/constants.js.map +1 -0
- package/dist/src/lib/crash-handlers.d.ts +20 -0
- package/dist/src/lib/crash-handlers.js +49 -0
- package/dist/src/lib/crash-handlers.js.map +1 -0
- package/dist/src/lib/credentials-store.d.ts +104 -0
- package/dist/src/lib/credentials-store.js +336 -0
- package/dist/src/lib/credentials-store.js.map +1 -0
- package/dist/src/lib/csv.d.ts +47 -0
- package/dist/src/lib/csv.js +172 -0
- package/dist/src/lib/csv.js.map +1 -0
- package/dist/src/lib/data-input.d.ts +23 -0
- package/dist/src/lib/data-input.js +50 -0
- package/dist/src/lib/data-input.js.map +1 -0
- package/dist/src/lib/db-codegen/dbFingerprint.d.ts +10 -0
- package/dist/src/lib/db-codegen/dbFingerprint.js +17 -0
- package/dist/src/lib/db-codegen/dbFingerprint.js.map +1 -0
- package/dist/src/lib/db-codegen/dbGenerator.d.ts +67 -0
- package/dist/src/lib/db-codegen/dbGenerator.js +170 -0
- package/dist/src/lib/db-codegen/dbGenerator.js.map +1 -0
- package/dist/src/lib/db-codegen/dbNaming.d.ts +87 -0
- package/dist/src/lib/db-codegen/dbNaming.js +180 -0
- package/dist/src/lib/db-codegen/dbNaming.js.map +1 -0
- package/dist/src/lib/db-codegen/dbTemplates.d.ts +272 -0
- package/dist/src/lib/db-codegen/dbTemplates.js +480 -0
- package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -0
- package/dist/src/lib/db-codegen/dbTsTypes.d.ts +73 -0
- package/dist/src/lib/db-codegen/dbTsTypes.js +139 -0
- package/dist/src/lib/db-codegen/dbTsTypes.js.map +1 -0
- package/dist/src/lib/db-codegen/dbTypeIR.d.ts +146 -0
- package/dist/src/lib/db-codegen/dbTypeIR.js +517 -0
- package/dist/src/lib/db-codegen/dbTypeIR.js.map +1 -0
- package/dist/src/lib/db-codegen/generated-operation-def-descriptor.d.ts +112 -0
- package/dist/src/lib/db-codegen/generated-operation-def-descriptor.js +211 -0
- package/dist/src/lib/db-codegen/generated-operation-def-descriptor.js.map +1 -0
- package/dist/src/lib/document-export-permissions.d.ts +30 -0
- package/dist/src/lib/document-export-permissions.js +54 -0
- package/dist/src/lib/document-export-permissions.js.map +1 -0
- package/dist/src/lib/env-resolver-core.d.ts +253 -0
- package/dist/src/lib/env-resolver-core.js +459 -0
- package/dist/src/lib/env-resolver-core.js.map +1 -0
- package/dist/src/lib/env-resolver.d.ts +99 -0
- package/dist/src/lib/env-resolver.js +153 -0
- package/dist/src/lib/env-resolver.js.map +1 -0
- package/dist/src/lib/fetch.d.ts +5 -0
- package/dist/src/lib/function-bundle.d.ts +141 -0
- package/dist/src/lib/function-bundle.js +333 -0
- package/dist/src/lib/function-bundle.js.map +1 -0
- package/dist/src/lib/function-collect.d.ts +123 -0
- package/dist/src/lib/function-collect.js +594 -0
- package/dist/src/lib/function-collect.js.map +1 -0
- package/dist/src/lib/function-db-types.d.ts +187 -0
- package/dist/src/lib/function-db-types.js +782 -0
- package/dist/src/lib/function-db-types.js.map +1 -0
- package/dist/src/lib/function-document-types.d.ts +50 -0
- package/dist/src/lib/function-document-types.js +163 -0
- package/dist/src/lib/function-document-types.js.map +1 -0
- package/dist/src/lib/function-grants-preflight.d.ts +64 -0
- package/dist/src/lib/function-grants-preflight.js +100 -0
- package/dist/src/lib/function-grants-preflight.js.map +1 -0
- package/dist/src/lib/function-log-tail.d.ts +83 -0
- package/dist/src/lib/function-log-tail.js +115 -0
- package/dist/src/lib/function-log-tail.js.map +1 -0
- package/dist/src/lib/function-schema-codegen.d.ts +118 -0
- package/dist/src/lib/function-schema-codegen.js +402 -0
- package/dist/src/lib/function-schema-codegen.js.map +1 -0
- package/dist/src/lib/function-sync.d.ts +263 -0
- package/dist/src/lib/function-sync.js +604 -0
- package/dist/src/lib/function-sync.js.map +1 -0
- package/dist/src/lib/function-triggers.d.ts +66 -0
- package/dist/src/lib/function-triggers.js +285 -0
- package/dist/src/lib/function-triggers.js.map +1 -0
- package/dist/src/lib/function-typecheck.d.ts +86 -0
- package/dist/src/lib/function-typecheck.js +370 -0
- package/dist/src/lib/function-typecheck.js.map +1 -0
- package/dist/src/lib/generated-allowlist.d.ts +28 -0
- package/dist/src/lib/generated-allowlist.js +281 -0
- package/dist/src/lib/generated-allowlist.js.map +1 -0
- package/dist/src/lib/generated-config-surfaces.d.ts +1445 -0
- package/dist/src/lib/generated-config-surfaces.js +5836 -0
- package/dist/src/lib/generated-config-surfaces.js.map +1 -0
- package/dist/src/lib/generated-sdk-types.d.ts +12 -0
- package/dist/src/lib/generated-sdk-types.js +13 -0
- package/dist/src/lib/generated-sdk-types.js.map +1 -0
- package/dist/src/lib/generated-template-lint.d.ts +212 -0
- package/dist/src/lib/generated-template-lint.js +624 -0
- package/dist/src/lib/generated-template-lint.js.map +1 -0
- package/dist/src/lib/generated-workflow-model-fields.d.ts +20 -0
- package/dist/src/lib/generated-workflow-model-fields.js +53 -0
- package/dist/src/lib/generated-workflow-model-fields.js.map +1 -0
- package/dist/src/lib/google-client-secret-status.d.ts +35 -0
- package/dist/src/lib/google-client-secret-status.js +56 -0
- package/dist/src/lib/google-client-secret-status.js.map +1 -0
- package/dist/src/lib/init-adopt.d.ts +16 -0
- package/dist/src/lib/init-adopt.js +34 -0
- package/dist/src/lib/init-adopt.js.map +1 -0
- package/dist/src/lib/init-assets.d.ts +39 -0
- package/dist/src/lib/init-assets.js +97 -0
- package/dist/src/lib/init-assets.js.map +1 -0
- package/dist/src/lib/init-client-platforms.d.ts +14 -0
- package/dist/src/lib/init-client-platforms.js +71 -0
- package/dist/src/lib/init-client-platforms.js.map +1 -0
- package/dist/src/lib/init-config.d.ts +98 -0
- package/dist/src/lib/init-config.js +186 -0
- package/dist/src/lib/init-config.js.map +1 -0
- package/dist/src/lib/init-email-redirect-uris.d.ts +37 -0
- package/dist/src/lib/init-email-redirect-uris.js +46 -0
- package/dist/src/lib/init-email-redirect-uris.js.map +1 -0
- package/dist/src/lib/init-ios-links.d.ts +91 -0
- package/dist/src/lib/init-ios-links.js +219 -0
- package/dist/src/lib/init-ios-links.js.map +1 -0
- package/dist/src/lib/init-plan.d.ts +80 -0
- package/dist/src/lib/init-plan.js +95 -0
- package/dist/src/lib/init-plan.js.map +1 -0
- package/dist/src/lib/init-production-env.d.ts +48 -0
- package/dist/src/lib/init-production-env.js +59 -0
- package/dist/src/lib/init-production-env.js.map +1 -0
- package/dist/src/lib/init-schema.d.ts +74 -0
- package/dist/src/lib/init-schema.js +358 -0
- package/dist/src/lib/init-schema.js.map +1 -0
- package/dist/src/lib/init-xcode.d.ts +34 -0
- package/dist/src/lib/init-xcode.js +138 -0
- package/dist/src/lib/init-xcode.js.map +1 -0
- package/dist/src/lib/integration-request-config.d.ts +30 -0
- package/dist/src/lib/integration-request-config.js +145 -0
- package/dist/src/lib/integration-request-config.js.map +1 -0
- package/dist/src/lib/integration-selector.d.ts +42 -0
- package/dist/src/lib/integration-selector.js +46 -0
- package/dist/src/lib/integration-selector.js.map +1 -0
- package/dist/src/lib/ios-app-id.d.ts +34 -0
- package/dist/src/lib/ios-app-id.js +69 -0
- package/dist/src/lib/ios-app-id.js.map +1 -0
- package/dist/src/lib/local-state.d.ts +55 -0
- package/dist/src/lib/local-state.js +167 -0
- package/dist/src/lib/local-state.js.map +1 -0
- package/dist/src/lib/local-test-cases.d.ts +63 -0
- package/dist/src/lib/local-test-cases.js +136 -0
- package/dist/src/lib/local-test-cases.js.map +1 -0
- package/dist/src/lib/log-inspection.d.ts +680 -0
- package/dist/src/lib/log-inspection.js +784 -0
- package/dist/src/lib/log-inspection.js.map +1 -0
- package/dist/src/lib/migration-nag.d.ts +49 -0
- package/dist/src/lib/migration-nag.js +164 -0
- package/dist/src/lib/migration-nag.js.map +1 -0
- package/dist/src/lib/object-status-filter.d.ts +22 -0
- package/dist/src/lib/object-status-filter.js +45 -0
- package/dist/src/lib/object-status-filter.js.map +1 -0
- package/dist/src/lib/output.d.ts +109 -0
- package/dist/src/lib/output.js +191 -8
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/package-manager.d.ts +140 -0
- package/dist/src/lib/package-manager.js +305 -0
- package/dist/src/lib/package-manager.js.map +1 -0
- package/dist/src/lib/paginate.d.ts +83 -0
- package/dist/src/lib/paginate.js +95 -0
- package/dist/src/lib/paginate.js.map +1 -0
- package/dist/src/lib/platform-owned.d.ts +63 -0
- package/dist/src/lib/platform-owned.js +85 -0
- package/dist/src/lib/platform-owned.js.map +1 -0
- package/dist/src/lib/project-config.d.ts +122 -0
- package/dist/src/lib/project-config.js +244 -0
- package/dist/src/lib/project-config.js.map +1 -0
- package/dist/src/lib/query-operators.d.ts +43 -0
- package/dist/src/lib/query-operators.js +80 -0
- package/dist/src/lib/query-operators.js.map +1 -0
- package/dist/src/lib/record-filter.d.ts +18 -0
- package/dist/src/lib/record-filter.js +55 -0
- package/dist/src/lib/record-filter.js.map +1 -0
- package/dist/src/lib/refresh-admin-credentials.d.ts +65 -0
- package/dist/src/lib/refresh-admin-credentials.js +103 -0
- package/dist/src/lib/refresh-admin-credentials.js.map +1 -0
- package/dist/src/lib/resolve-init-dev-port.d.ts +56 -0
- package/dist/src/lib/resolve-init-dev-port.js +55 -0
- package/dist/src/lib/resolve-init-dev-port.js.map +1 -0
- package/dist/src/lib/resolve-init-server.d.ts +64 -0
- package/dist/src/lib/resolve-init-server.js +77 -0
- package/dist/src/lib/resolve-init-server.js.map +1 -0
- package/dist/src/lib/resolve-platform.d.ts +74 -0
- package/dist/src/lib/resolve-platform.js +105 -0
- package/dist/src/lib/resolve-platform.js.map +1 -0
- package/dist/src/lib/run-status.d.ts +19 -0
- package/dist/src/lib/run-status.generated.d.ts +39 -0
- package/dist/src/lib/run-status.generated.js +66 -0
- package/dist/src/lib/run-status.generated.js.map +1 -0
- package/dist/src/lib/run-status.js +19 -0
- package/dist/src/lib/run-status.js.map +1 -0
- package/dist/src/lib/server-text-normalization.d.ts +51 -0
- package/dist/src/lib/server-text-normalization.js +90 -0
- package/dist/src/lib/server-text-normalization.js.map +1 -0
- package/dist/src/lib/server-url.d.ts +22 -0
- package/dist/src/lib/server-url.js +33 -0
- package/dist/src/lib/server-url.js.map +1 -0
- package/dist/src/lib/signing-secret-status.d.ts +81 -0
- package/dist/src/lib/signing-secret-status.js +116 -0
- package/dist/src/lib/signing-secret-status.js.map +1 -0
- package/dist/src/lib/skill-installer.d.ts +25 -0
- package/dist/src/lib/skill-installer.js +266 -0
- package/dist/src/lib/skill-installer.js.map +1 -0
- package/dist/src/lib/snapshots.d.ts +98 -0
- package/dist/src/lib/snapshots.js +294 -0
- package/dist/src/lib/snapshots.js.map +1 -0
- package/dist/src/lib/swift-codegen/dbGenerator.d.ts +113 -0
- package/dist/src/lib/swift-codegen/dbGenerator.js +914 -0
- package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -0
- package/dist/src/lib/swift-codegen/dbSwiftTypes.d.ts +42 -0
- package/dist/src/lib/swift-codegen/dbSwiftTypes.js +100 -0
- package/dist/src/lib/swift-codegen/dbSwiftTypes.js.map +1 -0
- package/dist/src/lib/swift-codegen/generator.d.ts +94 -0
- package/dist/src/lib/swift-codegen/generator.js +440 -0
- package/dist/src/lib/swift-codegen/generator.js.map +1 -0
- package/dist/src/lib/swift-codegen/schemaToSwift.d.ts +72 -0
- package/dist/src/lib/swift-codegen/schemaToSwift.js +644 -0
- package/dist/src/lib/swift-codegen/schemaToSwift.js.map +1 -0
- package/dist/src/lib/swift-codegen/siblingSymbols.d.ts +94 -0
- package/dist/src/lib/swift-codegen/siblingSymbols.js +155 -0
- package/dist/src/lib/swift-codegen/siblingSymbols.js.map +1 -0
- package/dist/src/lib/swift-codegen/swiftNaming.d.ts +85 -0
- package/dist/src/lib/swift-codegen/swiftNaming.js +198 -0
- package/dist/src/lib/swift-codegen/swiftNaming.js.map +1 -0
- package/dist/src/lib/sync-dir-selector.d.ts +21 -0
- package/dist/src/lib/sync-dir-selector.js +30 -0
- package/dist/src/lib/sync-dir-selector.js.map +1 -0
- package/dist/src/lib/sync-paths.d.ts +123 -0
- package/dist/src/lib/sync-paths.js +193 -0
- package/dist/src/lib/sync-paths.js.map +1 -0
- package/dist/src/lib/sync-resource-types.d.ts +563 -0
- package/dist/src/lib/sync-resource-types.js +1073 -0
- package/dist/src/lib/sync-resource-types.js.map +1 -0
- package/dist/src/lib/sync-selectors.d.ts +95 -0
- package/dist/src/lib/sync-selectors.js +228 -0
- package/dist/src/lib/sync-selectors.js.map +1 -0
- package/dist/src/lib/template.d.ts +170 -0
- package/dist/src/lib/template.js +484 -68
- package/dist/src/lib/template.js.map +1 -1
- package/dist/src/lib/test-case-file-names.d.ts +40 -0
- package/dist/src/lib/test-case-file-names.js +91 -0
- package/dist/src/lib/test-case-file-names.js.map +1 -0
- package/dist/src/lib/test-case-keys.d.ts +29 -0
- package/dist/src/lib/test-case-keys.js +55 -0
- package/dist/src/lib/test-case-keys.js.map +1 -0
- package/dist/src/lib/test-case-variables.d.ts +29 -0
- package/dist/src/lib/test-case-variables.js +71 -0
- package/dist/src/lib/test-case-variables.js.map +1 -0
- package/dist/src/lib/token-inject.d.ts +56 -0
- package/dist/src/lib/token-inject.js +204 -0
- package/dist/src/lib/token-inject.js.map +1 -0
- package/dist/src/lib/toml-database-config.d.ts +123 -0
- package/dist/src/lib/toml-database-config.js +544 -0
- package/dist/src/lib/toml-database-config.js.map +1 -0
- package/dist/src/lib/toml-metadata-config.d.ts +151 -0
- package/dist/src/lib/toml-metadata-config.js +476 -0
- package/dist/src/lib/toml-metadata-config.js.map +1 -0
- package/dist/src/lib/toml-native-form.d.ts +46 -0
- package/dist/src/lib/toml-native-form.js +78 -0
- package/dist/src/lib/toml-native-form.js.map +1 -0
- package/dist/src/lib/toml-params-validator.d.ts +129 -0
- package/dist/src/lib/toml-params-validator.js +298 -0
- package/dist/src/lib/toml-params-validator.js.map +1 -0
- package/dist/src/lib/toml-scalar-edit.d.ts +43 -0
- package/dist/src/lib/toml-scalar-edit.js +283 -0
- package/dist/src/lib/toml-scalar-edit.js.map +1 -0
- package/dist/src/lib/user-selector.d.ts +24 -0
- package/dist/src/lib/user-selector.js +33 -0
- package/dist/src/lib/user-selector.js.map +1 -0
- package/dist/src/lib/version-check.d.ts +35 -0
- package/dist/src/lib/version-check.js +241 -0
- package/dist/src/lib/version-check.js.map +1 -0
- package/dist/src/lib/watch.d.ts +121 -0
- package/dist/src/lib/watch.js +169 -0
- package/dist/src/lib/watch.js.map +1 -0
- package/dist/src/lib/web-url.d.ts +40 -0
- package/dist/src/lib/web-url.js +76 -0
- package/dist/src/lib/web-url.js.map +1 -0
- package/dist/src/lib/workflow-apply.d.ts +110 -0
- package/dist/src/lib/workflow-apply.js +164 -0
- package/dist/src/lib/workflow-apply.js.map +1 -0
- package/dist/src/lib/workflow-codegen/generated-schema-descriptor.d.ts +129 -0
- package/dist/src/lib/workflow-codegen/generated-schema-descriptor.js +269 -0
- package/dist/src/lib/workflow-codegen/generated-schema-descriptor.js.map +1 -0
- package/dist/src/lib/workflow-codegen/generator.d.ts +96 -0
- package/dist/src/lib/workflow-codegen/generator.js +361 -0
- package/dist/src/lib/workflow-codegen/generator.js.map +1 -0
- package/dist/src/lib/workflow-codegen/invokerIR.d.ts +94 -0
- package/dist/src/lib/workflow-codegen/invokerIR.js +76 -0
- package/dist/src/lib/workflow-codegen/invokerIR.js.map +1 -0
- package/dist/src/lib/workflow-codegen/naming.d.ts +33 -0
- package/dist/src/lib/workflow-codegen/naming.js +81 -0
- package/dist/src/lib/workflow-codegen/naming.js.map +1 -0
- package/dist/src/lib/workflow-codegen/schemaToTs.d.ts +80 -0
- package/dist/src/lib/workflow-codegen/schemaToTs.js +303 -0
- package/dist/src/lib/workflow-codegen/schemaToTs.js.map +1 -0
- package/dist/src/lib/workflow-config-apply.d.ts +70 -0
- package/dist/src/lib/workflow-config-apply.js +137 -0
- package/dist/src/lib/workflow-config-apply.js.map +1 -0
- package/dist/src/lib/workflow-config-sidecar.d.ts +63 -0
- package/dist/src/lib/workflow-config-sidecar.js +96 -0
- package/dist/src/lib/workflow-config-sidecar.js.map +1 -0
- package/dist/src/lib/workflow-defaults.d.ts +29 -0
- package/dist/src/lib/workflow-defaults.js +41 -0
- package/dist/src/lib/workflow-defaults.js.map +1 -0
- package/dist/src/lib/workflow-field-descriptor.d.ts +91 -0
- package/dist/src/lib/workflow-field-descriptor.js +153 -0
- package/dist/src/lib/workflow-field-descriptor.js.map +1 -0
- package/dist/src/lib/workflow-fragments.d.ts +64 -0
- package/dist/src/lib/workflow-fragments.js +342 -0
- package/dist/src/lib/workflow-fragments.js.map +1 -0
- package/dist/src/lib/workflow-include-preserve.d.ts +76 -0
- package/dist/src/lib/workflow-include-preserve.js +286 -0
- package/dist/src/lib/workflow-include-preserve.js.map +1 -0
- package/dist/src/lib/workflow-payload.d.ts +98 -0
- package/dist/src/lib/workflow-payload.js +178 -0
- package/dist/src/lib/workflow-payload.js.map +1 -0
- package/dist/src/lib/workflow-toml-validator.d.ts +207 -0
- package/dist/src/lib/workflow-toml-validator.js +762 -0
- package/dist/src/lib/workflow-toml-validator.js.map +1 -0
- package/dist/src/lib/workflow-usage.d.ts +198 -0
- package/dist/src/lib/workflow-usage.js +312 -0
- package/dist/src/lib/workflow-usage.js.map +1 -0
- package/dist/src/types/index.d.ts +591 -0
- package/dist/src/validators.d.ts +65 -0
- package/dist/src/validators.js +64 -0
- package/dist/src/validators.js.map +1 -0
- package/package.json +34 -9
|
@@ -1,36 +1,241 @@
|
|
|
1
1
|
import { loadCredentials, saveCredentials, isTokenExpiringSoon, } from "./config.js";
|
|
2
|
+
import { getCurrentEnvNameSafe } from "./env-resolver.js";
|
|
2
3
|
import { fetchWithTLS } from "./fetch.js";
|
|
4
|
+
import { paginateAll, normalizeCliListEnvelope } from "./paginate.js";
|
|
5
|
+
import { RefreshError, refreshAdminCredentials, } from "./refresh-admin-credentials.js";
|
|
6
|
+
import { drainAppsUntilConverged, } from "./workflow-usage.js";
|
|
3
7
|
export class ApiError extends Error {
|
|
4
8
|
statusCode;
|
|
5
9
|
code;
|
|
6
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Structured `details` payload from `corsErrorResponse`.
|
|
12
|
+
*
|
|
13
|
+
* The server's error envelope can emit `details` as either:
|
|
14
|
+
* - an array of validation issues (e.g. `[{ path, message }, ...]`) —
|
|
15
|
+
* this is what most legacy endpoints produce, and what sync.ts walks
|
|
16
|
+
* via `Array.isArray(err.details)` / `for (const detail of ...)`.
|
|
17
|
+
* - a record of structured offender fields (e.g. `{ refs, operations,
|
|
18
|
+
* opCount, line, column, ... }`) — emitted by the issue #666 schema
|
|
19
|
+
* gate and consumed by the typed exception subclasses below.
|
|
20
|
+
*
|
|
21
|
+
* Callers must narrow before use: `Array.isArray(err.details)` for the
|
|
22
|
+
* legacy shape, otherwise treat as `Record<string, any>`.
|
|
23
|
+
*/
|
|
24
|
+
details;
|
|
25
|
+
constructor(message, statusCode, code, details) {
|
|
7
26
|
super(message);
|
|
8
27
|
this.statusCode = statusCode;
|
|
9
28
|
this.code = code;
|
|
10
29
|
this.name = "ApiError";
|
|
30
|
+
if (details !== undefined) {
|
|
31
|
+
this.details = details;
|
|
32
|
+
}
|
|
11
33
|
}
|
|
12
34
|
}
|
|
13
35
|
export class ConflictError extends ApiError {
|
|
14
36
|
serverModifiedAt;
|
|
15
37
|
expectedModifiedAt;
|
|
16
|
-
constructor(message, serverModifiedAt, expectedModifiedAt) {
|
|
17
|
-
super(message, 409, "CONFLICT");
|
|
38
|
+
constructor(message, serverModifiedAt, expectedModifiedAt, details) {
|
|
39
|
+
super(message, 409, "CONFLICT", details);
|
|
18
40
|
this.serverModifiedAt = serverModifiedAt;
|
|
19
41
|
this.expectedModifiedAt = expectedModifiedAt;
|
|
20
42
|
this.name = "ConflictError";
|
|
21
43
|
}
|
|
22
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Extract a human-readable error message + structured fields from a non-OK
|
|
47
|
+
* HTTP response body. Single source of truth used by every error-handler call
|
|
48
|
+
* site in this file (see issue #684).
|
|
49
|
+
*/
|
|
50
|
+
export function parseErrorResponse(response, text, path) {
|
|
51
|
+
// Empty body → fall back to status code.
|
|
52
|
+
if (!text) {
|
|
53
|
+
return { message: `HTTP ${response.status}` };
|
|
54
|
+
}
|
|
55
|
+
let errorData;
|
|
56
|
+
try {
|
|
57
|
+
errorData = JSON.parse(text);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Non-JSON body. Surface the existing `<!DOCTYPE` special-case (an HTML
|
|
61
|
+
// 404 page from hitting the wrong path) so we don't regress the helpful
|
|
62
|
+
// "API endpoint not found" message at api-client.ts:343.
|
|
63
|
+
if (text.includes("<!DOCTYPE")) {
|
|
64
|
+
const where = path ? `: ${path}` : "";
|
|
65
|
+
return {
|
|
66
|
+
message: `API endpoint not found${where}. Make sure the server is running.`,
|
|
67
|
+
htmlNotFound: true,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// Other non-JSON bodies (e.g. plain-text 502 from a proxy) — surface the
|
|
71
|
+
// raw text so the operator at least sees what the server returned.
|
|
72
|
+
return { message: text };
|
|
73
|
+
}
|
|
74
|
+
// Server's standard envelope uses `error`; ConflictError + integrations
|
|
75
|
+
// proxy use `message`. Prefer `error` (more common), fall back to `message`.
|
|
76
|
+
const message = (typeof errorData?.error === "string" && errorData.error) ||
|
|
77
|
+
(typeof errorData?.message === "string" && errorData.message) ||
|
|
78
|
+
`HTTP ${response.status}`;
|
|
79
|
+
// Per issue #666 addendum A1, `code` may be at the top level or nested
|
|
80
|
+
// under `details.code` when the server's bespoke envelope didn't flatten.
|
|
81
|
+
const code = (typeof errorData?.code === "string" ? errorData.code : undefined) ??
|
|
82
|
+
(typeof errorData?.details?.code === "string"
|
|
83
|
+
? errorData.details.code
|
|
84
|
+
: undefined);
|
|
85
|
+
// Accept either an array (legacy) or a plain object (#666 schema gate).
|
|
86
|
+
const details = Array.isArray(errorData?.details)
|
|
87
|
+
? errorData.details
|
|
88
|
+
: errorData?.details && typeof errorData.details === "object"
|
|
89
|
+
? errorData.details
|
|
90
|
+
: undefined;
|
|
91
|
+
return { message, code, details, raw: errorData };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Typed exception classes for the database-schema feature (issue #666).
|
|
95
|
+
* Each maps 1:1 to a server `code` value emitted from the op-edit or
|
|
96
|
+
* schema-edit gate. They all extend ApiError so existing catch-all paths
|
|
97
|
+
* continue to work; specialized catch blocks can branch on `instanceof`.
|
|
98
|
+
*
|
|
99
|
+
* Per round-2 addendum A1, `details` is always preserved so callers can
|
|
100
|
+
* extract structured offender lists (refs[], operations[], etc.).
|
|
101
|
+
*/
|
|
102
|
+
export class SchemaRequiredError extends ApiError {
|
|
103
|
+
constructor(message, details) {
|
|
104
|
+
super(message, 422, "SCHEMA_REQUIRED", details);
|
|
105
|
+
this.name = "SchemaRequiredError";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function detailsRecord(details) {
|
|
109
|
+
return details && !Array.isArray(details) && typeof details === "object"
|
|
110
|
+
? details
|
|
111
|
+
: undefined;
|
|
112
|
+
}
|
|
113
|
+
export class OperationRefError extends ApiError {
|
|
114
|
+
constructor(message, details) {
|
|
115
|
+
super(message, 422, "OPERATION_REFERENCES_UNDEFINED", details);
|
|
116
|
+
this.name = "OperationRefError";
|
|
117
|
+
}
|
|
118
|
+
get refs() {
|
|
119
|
+
const d = detailsRecord(this.details);
|
|
120
|
+
return Array.isArray(d?.refs) ? d.refs : [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export class SchemaBreaksOpsError extends ApiError {
|
|
124
|
+
constructor(message, details) {
|
|
125
|
+
super(message, 422, "SCHEMA_BREAKS_OPERATIONS", details);
|
|
126
|
+
this.name = "SchemaBreaksOpsError";
|
|
127
|
+
}
|
|
128
|
+
get operations() {
|
|
129
|
+
const d = detailsRecord(this.details);
|
|
130
|
+
return Array.isArray(d?.operations) ? d.operations : [];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export class SchemaHasUncheckableOpsError extends ApiError {
|
|
134
|
+
constructor(message, details) {
|
|
135
|
+
super(message, 422, "SCHEMA_HAS_UNCHECKABLE_OPS", details);
|
|
136
|
+
this.name = "SchemaHasUncheckableOpsError";
|
|
137
|
+
}
|
|
138
|
+
get operations() {
|
|
139
|
+
const d = detailsRecord(this.details);
|
|
140
|
+
return Array.isArray(d?.operations) ? d.operations : [];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export class TomlParseError extends ApiError {
|
|
144
|
+
constructor(message, details) {
|
|
145
|
+
super(message, 400, "TOML_PARSE_ERROR", details);
|
|
146
|
+
this.name = "TomlParseError";
|
|
147
|
+
}
|
|
148
|
+
get line() {
|
|
149
|
+
const d = detailsRecord(this.details);
|
|
150
|
+
return typeof d?.line === "number" ? d.line : undefined;
|
|
151
|
+
}
|
|
152
|
+
get column() {
|
|
153
|
+
const d = detailsRecord(this.details);
|
|
154
|
+
return typeof d?.column === "number" ? d.column : undefined;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export class OpsExistError extends ApiError {
|
|
158
|
+
constructor(message, details) {
|
|
159
|
+
super(message, 409, "OPS_EXIST", details);
|
|
160
|
+
this.name = "OpsExistError";
|
|
161
|
+
}
|
|
162
|
+
get opCount() {
|
|
163
|
+
const d = detailsRecord(this.details);
|
|
164
|
+
return typeof d?.opCount === "number" ? d.opCount : 0;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Normalize a `databases operations execute` result to the CLI's one list
|
|
169
|
+
* envelope — but only when the result is a Durable Object query page
|
|
170
|
+
* (issue #2440).
|
|
171
|
+
*
|
|
172
|
+
* A registered operation is a list only sometimes. Of the shapes the server's
|
|
173
|
+
* operation dispatch can return, exactly two carry a top-level `data` array: a
|
|
174
|
+
* bare `query` and a `pipeline` whose `returnField` names a query step. A
|
|
175
|
+
* `count` (`{ count }`), an `aggregate` (`{ result }`), a mutation
|
|
176
|
+
* (`{ results }`), an `applyToQuery` (`{ matched, affected, failed, … }`) and a
|
|
177
|
+
* `returnField: "all"` pipeline (`{ steps: { … } }`) do not, so blanket
|
|
178
|
+
* normalization would corrupt them. This helper therefore tests the shape
|
|
179
|
+
* first and passes everything else through byte-for-byte — including the
|
|
180
|
+
* nested steps of a `returnField: "all"` pipeline, which are operation-defined
|
|
181
|
+
* objects the CLI must not reach into.
|
|
182
|
+
*
|
|
183
|
+
* Deliberately narrow, not a general-purpose predicate: the contract is
|
|
184
|
+
* specifically "an operations-execute result may be a DO query page".
|
|
185
|
+
* `executeDatabaseOperation()` is the only caller.
|
|
186
|
+
*
|
|
187
|
+
* The conversion itself is delegated to `normalizeCliListEnvelope()` so cursor
|
|
188
|
+
* handling, `prevCursor` dropping and `hasMore` derivation have exactly one
|
|
189
|
+
* implementation. That helper throws on an unrecognized shape — correct when
|
|
190
|
+
* the payload is supposed to be a list, wrong here, where the payload is the
|
|
191
|
+
* caller's own result — which is why the shape test has to run first. `_timing`
|
|
192
|
+
* (`--timing`) is re-attached afterwards, since it rides alongside the envelope
|
|
193
|
+
* rather than inside it.
|
|
194
|
+
*/
|
|
195
|
+
export function normalizeDatabaseOperationResult(raw) {
|
|
196
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
197
|
+
return raw;
|
|
198
|
+
const page = raw;
|
|
199
|
+
if (!Array.isArray(page.data))
|
|
200
|
+
return raw;
|
|
201
|
+
const envelope = normalizeCliListEnvelope(page);
|
|
202
|
+
if (page._timing !== undefined)
|
|
203
|
+
envelope._timing = page._timing;
|
|
204
|
+
return envelope;
|
|
205
|
+
}
|
|
23
206
|
export class ApiClient {
|
|
207
|
+
/**
|
|
208
|
+
* Loaded on first use (`ensureAuthenticated`), never in a constructor.
|
|
209
|
+
*
|
|
210
|
+
* There used to be an `apiClient` singleton constructed at MODULE scope, so
|
|
211
|
+
* a constructor body ran while `bin/primitive.ts` was still being imported —
|
|
212
|
+
* before its first `try`, and before any command had said whether it needed
|
|
213
|
+
* the project at all. `loadCredentials()` resolves the environment, so on a
|
|
214
|
+
* project still holding the pre-#3153 layout that read threw the stale-layout
|
|
215
|
+
* refusal out of module evaluation, where not even the `uncaughtException`
|
|
216
|
+
* handler can reach it: the user got Node's raw stack trace, and `guides
|
|
217
|
+
* get` — the command that serves the very document the refusal names — died
|
|
218
|
+
* with it (#3273). The singleton is gone (#3154, it had no callers) and the
|
|
219
|
+
* read stays lazy, which is what keeps that shape from coming back.
|
|
220
|
+
*
|
|
221
|
+
* Nothing is lost by waiting, and one thing is gained: a `login` earlier in
|
|
222
|
+
* the same process is visible to a later request.
|
|
223
|
+
*/
|
|
24
224
|
credentials = null;
|
|
25
|
-
constructor() {
|
|
26
|
-
this.credentials = loadCredentials();
|
|
27
|
-
}
|
|
28
225
|
async ensureAuthenticated() {
|
|
29
226
|
if (!this.credentials) {
|
|
30
227
|
this.credentials = loadCredentials();
|
|
31
228
|
}
|
|
32
229
|
if (!this.credentials) {
|
|
33
|
-
|
|
230
|
+
// Name the environment when there is one (#1293). Before #3152 this case
|
|
231
|
+
// surfaced from `resolveAppId`, because an unauthenticated environment
|
|
232
|
+
// resolved no app at all; now the environment always names its app, so
|
|
233
|
+
// "not logged in" is only ever an auth failure, and it is reported here,
|
|
234
|
+
// once, for every command.
|
|
235
|
+
const envName = getCurrentEnvNameSafe();
|
|
236
|
+
throw new ApiError(envName
|
|
237
|
+
? `Not logged in to environment "${envName}". Run 'primitive -e ${envName} login'.`
|
|
238
|
+
: "Not logged in. Run 'primitive login' first.", 401);
|
|
34
239
|
}
|
|
35
240
|
// Check if token needs refresh
|
|
36
241
|
if (isTokenExpiringSoon(this.credentials)) {
|
|
@@ -39,39 +244,24 @@ export class ApiClient {
|
|
|
39
244
|
return this.credentials;
|
|
40
245
|
}
|
|
41
246
|
async refreshToken() {
|
|
42
|
-
if (!this.credentials
|
|
43
|
-
throw new ApiError("
|
|
247
|
+
if (!this.credentials) {
|
|
248
|
+
throw new ApiError("Not logged in. Run 'primitive login' first.", 401);
|
|
44
249
|
}
|
|
45
|
-
const url = `${this.credentials.serverUrl}/admin/api/auth/refresh`;
|
|
46
250
|
try {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
};
|
|
50
|
-
if (this.credentials.globalAdminAppId) {
|
|
51
|
-
headers["X-Global-Admin-App-Id"] = this.credentials.globalAdminAppId;
|
|
52
|
-
}
|
|
53
|
-
const response = await fetchWithTLS(url, {
|
|
54
|
-
method: "POST",
|
|
55
|
-
headers,
|
|
56
|
-
body: JSON.stringify({ refreshToken: this.credentials.refreshToken }),
|
|
57
|
-
});
|
|
58
|
-
if (!response.ok) {
|
|
59
|
-
throw new ApiError("Token refresh failed. Please login again.", 401);
|
|
60
|
-
}
|
|
61
|
-
const data = await response.json();
|
|
62
|
-
// Update credentials with new tokens
|
|
63
|
-
this.credentials = {
|
|
64
|
-
...this.credentials,
|
|
65
|
-
accessToken: data.accessToken || data.token,
|
|
66
|
-
refreshToken: data.refreshToken || this.credentials.refreshToken,
|
|
67
|
-
expiresAt: data.expiresAt,
|
|
68
|
-
};
|
|
251
|
+
const updated = await refreshAdminCredentials(this.credentials);
|
|
252
|
+
this.credentials = updated;
|
|
69
253
|
saveCredentials(this.credentials);
|
|
70
254
|
}
|
|
71
|
-
catch (
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
255
|
+
catch (err) {
|
|
256
|
+
if (err instanceof RefreshError) {
|
|
257
|
+
// Preserve historical behavior: ApiClient surfaces refresh failures
|
|
258
|
+
// as 401s regardless of whether the underlying cause was a network
|
|
259
|
+
// error or a server-side rejection. Callers that need a finer
|
|
260
|
+
// distinction (e.g. `primitive token`) consume RefreshError directly
|
|
261
|
+
// from refresh-admin-credentials.ts instead of going through here.
|
|
262
|
+
throw new ApiError("Token refresh failed. Please login again.", 401);
|
|
263
|
+
}
|
|
264
|
+
throw err;
|
|
75
265
|
}
|
|
76
266
|
}
|
|
77
267
|
async request(path, options = {}) {
|
|
@@ -92,20 +282,42 @@ export class ApiClient {
|
|
|
92
282
|
});
|
|
93
283
|
const text = await response.text();
|
|
94
284
|
if (!response.ok) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
285
|
+
const parsed = parseErrorResponse(response, text, path);
|
|
286
|
+
// Preserve the `<!DOCTYPE` → 404 ApiError shape (status forced to 404).
|
|
287
|
+
if (parsed.htmlNotFound) {
|
|
288
|
+
throw new ApiError(parsed.message, 404);
|
|
98
289
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
290
|
+
// Narrow details to the record shape for the issue #666 typed-exception
|
|
291
|
+
// dispatch below. Conflict metadata may live under `details.*` (canonical
|
|
292
|
+
// location per A1) or on the top-level envelope (legacy).
|
|
293
|
+
const detailsRecord = parsed.details && !Array.isArray(parsed.details)
|
|
294
|
+
? parsed.details
|
|
295
|
+
: undefined;
|
|
296
|
+
const serverModifiedAt = detailsRecord?.serverModifiedAt ?? parsed.raw?.serverModifiedAt;
|
|
297
|
+
const expectedModifiedAt = detailsRecord?.expectedModifiedAt ?? parsed.raw?.expectedModifiedAt;
|
|
298
|
+
// Typed exceptions for the schema-feature (issue #666).
|
|
299
|
+
if (response.status === 409 && parsed.code === "CONFLICT") {
|
|
300
|
+
throw new ConflictError(parsed.message, serverModifiedAt, expectedModifiedAt, detailsRecord);
|
|
301
|
+
}
|
|
302
|
+
if (response.status === 409 && parsed.code === "OPS_EXIST") {
|
|
303
|
+
throw new OpsExistError(parsed.message, detailsRecord);
|
|
304
|
+
}
|
|
305
|
+
if (response.status === 400 && parsed.code === "TOML_PARSE_ERROR") {
|
|
306
|
+
throw new TomlParseError(parsed.message, detailsRecord);
|
|
307
|
+
}
|
|
308
|
+
if (response.status === 422 && parsed.code === "SCHEMA_REQUIRED") {
|
|
309
|
+
throw new SchemaRequiredError(parsed.message, detailsRecord);
|
|
104
310
|
}
|
|
105
|
-
if (response.status ===
|
|
106
|
-
throw new
|
|
311
|
+
if (response.status === 422 && parsed.code === "OPERATION_REFERENCES_UNDEFINED") {
|
|
312
|
+
throw new OperationRefError(parsed.message, detailsRecord);
|
|
107
313
|
}
|
|
108
|
-
|
|
314
|
+
if (response.status === 422 && parsed.code === "SCHEMA_BREAKS_OPERATIONS") {
|
|
315
|
+
throw new SchemaBreaksOpsError(parsed.message, detailsRecord);
|
|
316
|
+
}
|
|
317
|
+
if (response.status === 422 && parsed.code === "SCHEMA_HAS_UNCHECKABLE_OPS") {
|
|
318
|
+
throw new SchemaHasUncheckableOpsError(parsed.message, detailsRecord);
|
|
319
|
+
}
|
|
320
|
+
throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
|
|
109
321
|
}
|
|
110
322
|
return text ? JSON.parse(text) : null;
|
|
111
323
|
}
|
|
@@ -155,7 +367,59 @@ export class ApiClient {
|
|
|
155
367
|
// APPS
|
|
156
368
|
// ============================================
|
|
157
369
|
async listApps() {
|
|
158
|
-
|
|
370
|
+
// The server paginates cursor-style (default pageSize=25, cap=100). Historically
|
|
371
|
+
// this method issued a single un-paginated GET and silently truncated to the
|
|
372
|
+
// first page, which broke `primitive apps list` for admins with more than
|
|
373
|
+
// 25 apps (see #436). Iterate every page.
|
|
374
|
+
const apps = await paginateAll(async (cursor) => {
|
|
375
|
+
const qs = new URLSearchParams({ limit: "100" });
|
|
376
|
+
if (cursor)
|
|
377
|
+
qs.set("cursor", cursor);
|
|
378
|
+
const resp = await this.get(`/admin/api/admins/me/apps?${qs.toString()}`);
|
|
379
|
+
// The server now emits `items` with `apps` as a deprecation-window
|
|
380
|
+
// alias (#1316). Read `items` first so listings keep working once the
|
|
381
|
+
// alias is removed; fall back to the legacy `apps` key meanwhile.
|
|
382
|
+
return {
|
|
383
|
+
items: resp.items ??
|
|
384
|
+
(Array.isArray(resp.apps) ? resp.apps : []),
|
|
385
|
+
nextCursor: resp.nextCursor ?? resp.cursor,
|
|
386
|
+
};
|
|
387
|
+
});
|
|
388
|
+
return { apps };
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Every app on the server, not just the caller's own.
|
|
392
|
+
*
|
|
393
|
+
* Deliberately separate from `listApps()`, whose contract is "apps assigned
|
|
394
|
+
* to me" (`GET /admin/api/admins/me/apps`, backed by `admin.queryApps()`) —
|
|
395
|
+
* the contract every other command depends on. `analytics workflow-usage
|
|
396
|
+
* --all-apps` has to reach apps nobody is assigned to, which is the global
|
|
397
|
+
* route `GET /admin/api/apps` (`App.iterateAll()`), and that route
|
|
398
|
+
* identifies an app as `appId` rather than `id` (D3130-001).
|
|
399
|
+
*
|
|
400
|
+
* One drain of that route is lossy — its cursor re-scans `App.iterateAll()`
|
|
401
|
+
* from the start of each page, so a list that changes under it both repeats
|
|
402
|
+
* and drops rows — so the walk is the union of repeated drains
|
|
403
|
+
* (`drainAppsUntilConverged`, which carries the measurement).
|
|
404
|
+
*/
|
|
405
|
+
async listAllAppsGlobal() {
|
|
406
|
+
const drainOnce = () => paginateAll(async (cursor) => {
|
|
407
|
+
const qs = new URLSearchParams({ limit: "100" });
|
|
408
|
+
if (cursor)
|
|
409
|
+
qs.set("cursor", cursor);
|
|
410
|
+
const resp = await this.get(`/admin/api/apps?${qs.toString()}`);
|
|
411
|
+
// `items` first, with the `apps` alias (#1316) as the fallback,
|
|
412
|
+
// exactly as `listApps()` reads its own envelope.
|
|
413
|
+
const rows = resp.items ?? (Array.isArray(resp.apps) ? resp.apps : []);
|
|
414
|
+
return {
|
|
415
|
+
items: rows.map((row) => ({
|
|
416
|
+
appId: String(row.appId ?? row.id ?? ""),
|
|
417
|
+
name: String(row.name ?? ""),
|
|
418
|
+
})),
|
|
419
|
+
nextCursor: resp.nextCursor ?? resp.cursor,
|
|
420
|
+
};
|
|
421
|
+
});
|
|
422
|
+
return drainAppsUntilConverged(drainOnce);
|
|
159
423
|
}
|
|
160
424
|
async createApp(data) {
|
|
161
425
|
return this.post("/admin/api/apps", data);
|
|
@@ -181,12 +445,70 @@ export class ApiClient {
|
|
|
181
445
|
async addUserByEmail(appId, data) {
|
|
182
446
|
return this.post(`/admin/api/apps/${appId}/users/add-by-email`, data);
|
|
183
447
|
}
|
|
184
|
-
async
|
|
185
|
-
return this.
|
|
448
|
+
async mintTestJwt(appId, userId, role) {
|
|
449
|
+
return this.post(`/admin/api/apps/${appId}/users/${userId}/mint-test-jwt`, role ? { role } : {});
|
|
450
|
+
}
|
|
451
|
+
async rebuildUserSearchText(appId) {
|
|
452
|
+
return this.post(`/admin/api/apps/${appId}/users/rebuild-search-text`, {});
|
|
453
|
+
}
|
|
454
|
+
async listUsers(appId, params) {
|
|
455
|
+
const query = new URLSearchParams();
|
|
456
|
+
if (params?.name)
|
|
457
|
+
query.set("name", params.name);
|
|
458
|
+
if (params?.email)
|
|
459
|
+
query.set("email", params.email);
|
|
460
|
+
if (params?.userId)
|
|
461
|
+
query.set("userId", params.userId);
|
|
462
|
+
if (params?.limit)
|
|
463
|
+
query.set("limit", String(params.limit));
|
|
464
|
+
if (params?.cursor)
|
|
465
|
+
query.set("cursor", params.cursor);
|
|
466
|
+
const path = query.toString()
|
|
467
|
+
? `/app/${appId}/api/users?${query.toString()}`
|
|
468
|
+
: `/app/${appId}/api/users`;
|
|
469
|
+
const result = await this.get(path);
|
|
470
|
+
return {
|
|
471
|
+
items: result?.items ?? (Array.isArray(result) ? result : []),
|
|
472
|
+
nextCursor: result?.nextCursor ?? null,
|
|
473
|
+
};
|
|
186
474
|
}
|
|
187
475
|
async removeUser(appId, userId) {
|
|
188
476
|
return this.delete(`/app/${appId}/api/users/${userId}`);
|
|
189
477
|
}
|
|
478
|
+
/**
|
|
479
|
+
* #2803 — take a user's access away, or give it back. Distinct from
|
|
480
|
+
* `removeUser`, which detaches the membership: this blocks the person's auth
|
|
481
|
+
* in the app, revokes their sessions and tokens, and is reversible. Both
|
|
482
|
+
* routes already existed; nothing on the CLI reached them.
|
|
483
|
+
*/
|
|
484
|
+
async setUserAvailability(appId, userId, action) {
|
|
485
|
+
return this.put(`/admin/api/apps/${appId}/users/${userId}/${action}`, {});
|
|
486
|
+
}
|
|
487
|
+
// ============================================
|
|
488
|
+
// FEATURE FLAGS (super-admin)
|
|
489
|
+
// ============================================
|
|
490
|
+
/**
|
|
491
|
+
* #2803 — the platform's feature flags. A server-owned admin toggle with a
|
|
492
|
+
* console but, until now, no CLI: the same "one control, both surfaces" rule
|
|
493
|
+
* the carrying types get.
|
|
494
|
+
*/
|
|
495
|
+
async listFeatureFlags() {
|
|
496
|
+
return this.get(`/admin/api/flags`);
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* The flag itself, not the `{ flag }` envelope the route answers with —
|
|
500
|
+
* `feature-flags get` prints the key, the enabled state and the per-app
|
|
501
|
+
* overrides, and reading them off the envelope printed a blank row.
|
|
502
|
+
*/
|
|
503
|
+
async getFeatureFlag(flagKey) {
|
|
504
|
+
const result = await this.get(`/admin/api/flags/${flagKey}`);
|
|
505
|
+
return result?.flag ?? result;
|
|
506
|
+
}
|
|
507
|
+
/** Same envelope, same unwrap: `enable`/`disable` print the updated flag. */
|
|
508
|
+
async setFeatureFlagEnabled(flagKey, enabled) {
|
|
509
|
+
const result = await this.put(`/admin/api/flags/${flagKey}`, { enabled });
|
|
510
|
+
return result?.flag ?? result;
|
|
511
|
+
}
|
|
190
512
|
async updateUserRole(appId, userId, role) {
|
|
191
513
|
return this.put(`/app/${appId}/api/users/${userId}/role`, { role });
|
|
192
514
|
}
|
|
@@ -196,14 +518,39 @@ export class ApiClient {
|
|
|
196
518
|
async transferAdminOwnership(appId, adminId) {
|
|
197
519
|
return this.post(`/admin/api/apps/${appId}/admins/transfer-ownership`, { adminId });
|
|
198
520
|
}
|
|
521
|
+
// App-level console admin management
|
|
522
|
+
async listAppAdmins(appId) {
|
|
523
|
+
return this.get(`/admin/api/apps/${appId}/admins`);
|
|
524
|
+
}
|
|
525
|
+
async addAppAdmin(appId, data) {
|
|
526
|
+
return this.post(`/admin/api/apps/${appId}/admins`, data);
|
|
527
|
+
}
|
|
528
|
+
async removeAppAdmin(appId, adminId) {
|
|
529
|
+
return this.delete(`/admin/api/apps/${appId}/admins/${adminId}`);
|
|
530
|
+
}
|
|
531
|
+
async listAppAdminInvitations(appId) {
|
|
532
|
+
return this.get(`/admin/api/apps/${appId}/admin-invitations`);
|
|
533
|
+
}
|
|
534
|
+
async deleteAppAdminInvitation(appId, invitationId) {
|
|
535
|
+
return this.delete(`/admin/api/apps/${appId}/admin-invitations/${invitationId}`);
|
|
536
|
+
}
|
|
199
537
|
async transferDocumentOwnership(appId, documentId, newOwnerId) {
|
|
200
538
|
return this.post(`/app/${appId}/api/documents/${documentId}/permissions/transfer`, { newOwnerId });
|
|
201
539
|
}
|
|
202
540
|
// ============================================
|
|
203
541
|
// INVITATIONS
|
|
204
542
|
// ============================================
|
|
205
|
-
async listInvitations(appId) {
|
|
206
|
-
|
|
543
|
+
async listInvitations(appId, params) {
|
|
544
|
+
const result = await this.get(`/app/${appId}/api/invitations`, params);
|
|
545
|
+
// #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias
|
|
546
|
+
// so the CLI keeps paginating against pre-#1316 servers.
|
|
547
|
+
const nextCursor = result?.nextCursor ?? result?.cursor;
|
|
548
|
+
return {
|
|
549
|
+
items: result?.items ?? [],
|
|
550
|
+
nextCursor,
|
|
551
|
+
hasMore: result?.hasMore ?? nextCursor != null,
|
|
552
|
+
cursor: nextCursor,
|
|
553
|
+
};
|
|
207
554
|
}
|
|
208
555
|
async createInvitation(appId, data) {
|
|
209
556
|
return this.post(`/app/${appId}/api/invitations`, data);
|
|
@@ -228,6 +575,20 @@ export class ApiClient {
|
|
|
228
575
|
return this.delete(`/app/${appId}/api/waitlist/${waitlistId}`);
|
|
229
576
|
}
|
|
230
577
|
// ============================================
|
|
578
|
+
// DEFERRED GRANTS
|
|
579
|
+
// ============================================
|
|
580
|
+
async listDeferredGrants(appId, params) {
|
|
581
|
+
const result = await this.get(`/app/${appId}/api/deferred-grants`, params);
|
|
582
|
+
return {
|
|
583
|
+
grants: result?.grants ?? result?.items ?? [],
|
|
584
|
+
nextCursor: result?.nextCursor ?? null,
|
|
585
|
+
hasMore: result?.hasMore,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
async revokeDeferredGrant(appId, deferredId, type) {
|
|
589
|
+
return this.delete(`/app/${appId}/api/deferred-grants/${deferredId}?type=${type}`);
|
|
590
|
+
}
|
|
591
|
+
// ============================================
|
|
231
592
|
// INTEGRATIONS
|
|
232
593
|
// ============================================
|
|
233
594
|
async listIntegrations(appId, params) {
|
|
@@ -237,9 +598,19 @@ export class ApiClient {
|
|
|
237
598
|
nextCursor: result?.nextCursor ?? null,
|
|
238
599
|
};
|
|
239
600
|
}
|
|
601
|
+
/** Integration detail (#2631: `accessRule` is on the detail, not the list summary). */
|
|
240
602
|
async getIntegration(appId, integrationId) {
|
|
241
603
|
return this.get(`/admin/api/apps/${appId}/integrations/${integrationId}`);
|
|
242
604
|
}
|
|
605
|
+
/**
|
|
606
|
+
* An integration's named configs. Test cases pin a config by NAME so the
|
|
607
|
+
* sidecar is portable across apps (#2769); this is the map pull and push
|
|
608
|
+
* resolve that name through, the same way prompts/workflows/scripts do.
|
|
609
|
+
*/
|
|
610
|
+
async listIntegrationConfigs(appId, integrationId) {
|
|
611
|
+
const result = await this.get(`/admin/api/apps/${appId}/integrations/${integrationId}/configs`);
|
|
612
|
+
return { items: result?.items ?? result ?? [] };
|
|
613
|
+
}
|
|
243
614
|
async createIntegration(appId, payload) {
|
|
244
615
|
return this.post(`/admin/api/apps/${appId}/integrations`, payload);
|
|
245
616
|
}
|
|
@@ -247,8 +618,15 @@ export class ApiClient {
|
|
|
247
618
|
const body = expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload;
|
|
248
619
|
return this.patch(`/admin/api/apps/${appId}/integrations/${integrationId}`, body);
|
|
249
620
|
}
|
|
621
|
+
/**
|
|
622
|
+
* Delete an integration. Archives it by default; `{ hard: true }` destroys
|
|
623
|
+
* the row instead, which is what `config push --prune` passes (#2803) so a
|
|
624
|
+
* pruned integration stops holding its key. The positional boolean is still
|
|
625
|
+
* accepted for the existing callers.
|
|
626
|
+
*/
|
|
250
627
|
async deleteIntegration(appId, integrationId, hard) {
|
|
251
|
-
const
|
|
628
|
+
const isHard = typeof hard === "object" ? hard?.hard === true : hard === true;
|
|
629
|
+
const path = isHard
|
|
252
630
|
? `/admin/api/apps/${appId}/integrations/${integrationId}?hard=true`
|
|
253
631
|
: `/admin/api/apps/${appId}/integrations/${integrationId}`;
|
|
254
632
|
return this.delete(path);
|
|
@@ -260,17 +638,352 @@ export class ApiClient {
|
|
|
260
638
|
const result = await this.get(`/admin/api/apps/${appId}/integrations/${integrationId}/logs`, params);
|
|
261
639
|
return result?.items ?? [];
|
|
262
640
|
}
|
|
263
|
-
async
|
|
264
|
-
const result = await this.get(`/admin/api/apps/${appId}/
|
|
641
|
+
async listWorkflowRunIntegrationLogs(appId, runId, params) {
|
|
642
|
+
const result = await this.get(`/admin/api/apps/${appId}/workflows/runs/${runId}/integration-logs`, params);
|
|
643
|
+
return result?.items ?? [];
|
|
644
|
+
}
|
|
645
|
+
// ============================================
|
|
646
|
+
// APP SECRETS
|
|
647
|
+
// ============================================
|
|
648
|
+
async listAppSecrets(appId) {
|
|
649
|
+
const result = await this.get(`/admin/api/apps/${appId}/secrets`);
|
|
650
|
+
return result?.items ?? [];
|
|
651
|
+
}
|
|
652
|
+
async createAppSecret(appId, payload) {
|
|
653
|
+
return this.post(`/admin/api/apps/${appId}/secrets`, payload);
|
|
654
|
+
}
|
|
655
|
+
async updateAppSecret(appId, secretId, payload) {
|
|
656
|
+
return this.put(`/admin/api/apps/${appId}/secrets/${secretId}`, payload);
|
|
657
|
+
}
|
|
658
|
+
async upsertAppSecret(appId, key, payload) {
|
|
659
|
+
return this.put(`/admin/api/apps/${appId}/secrets/by-key/${key}`, payload);
|
|
660
|
+
}
|
|
661
|
+
async deleteAppSecret(appId, secretId) {
|
|
662
|
+
return this.delete(`/admin/api/apps/${appId}/secrets/${secretId}`);
|
|
663
|
+
}
|
|
664
|
+
// ============================================
|
|
665
|
+
// APP CONFIG VARS (issue #1364 — non-secret twin of secrets)
|
|
666
|
+
// ============================================
|
|
667
|
+
async listAppConfigVars(appId) {
|
|
668
|
+
const result = await this.get(`/admin/api/apps/${appId}/vars`);
|
|
265
669
|
return result?.items ?? [];
|
|
266
670
|
}
|
|
267
|
-
async
|
|
268
|
-
|
|
269
|
-
|
|
671
|
+
async upsertAppConfigVar(appId, key, payload, expectedModifiedAt, options = {}) {
|
|
672
|
+
// Encode the key so a malformed one (e.g. containing "/") reaches the
|
|
673
|
+
// server's key validation (400) instead of producing a routing 404.
|
|
674
|
+
// `expectedModifiedAt` (issue #1423 review r-2 P1) is the optimistic-
|
|
675
|
+
// concurrency precondition for an UPDATE — the server rejects the write
|
|
676
|
+
// with a 409 CONFLICT (surfaced as `ConflictError`) if the var changed
|
|
677
|
+
// since it.
|
|
678
|
+
//
|
|
679
|
+
// `expectNotExists` (issue #1423 review r-3 P1a) is the create-only
|
|
680
|
+
// precondition. A create has no baseline timestamp to send, but without
|
|
681
|
+
// any precondition the by-key upsert silently overwrites a var created
|
|
682
|
+
// remotely since our snapshot. Setting `expectNotExists` makes the server
|
|
683
|
+
// 409 CONFLICT if the key already exists instead of overwriting it.
|
|
684
|
+
const body = { ...payload };
|
|
685
|
+
if (expectedModifiedAt !== undefined)
|
|
686
|
+
body.expectedModifiedAt = expectedModifiedAt;
|
|
687
|
+
if (options.expectNotExists)
|
|
688
|
+
body.expectNotExists = true;
|
|
689
|
+
return this.put(`/admin/api/apps/${appId}/vars/by-key/${encodeURIComponent(key)}`, body);
|
|
690
|
+
}
|
|
691
|
+
async deleteAppConfigVar(appId, key, expectedModifiedAt) {
|
|
692
|
+
// The DELETE carries the optimistic-concurrency precondition (issue #1423
|
|
693
|
+
// review r-2 P1) as a query param since it has no body; the server 409s
|
|
694
|
+
// (ConflictError) if the var was edited remotely since `expectedModifiedAt`.
|
|
695
|
+
let path = `/admin/api/apps/${appId}/vars/by-key/${encodeURIComponent(key)}`;
|
|
696
|
+
if (expectedModifiedAt !== undefined) {
|
|
697
|
+
path += `?expectedModifiedAt=${encodeURIComponent(expectedModifiedAt)}`;
|
|
698
|
+
}
|
|
699
|
+
return this.delete(path);
|
|
700
|
+
}
|
|
701
|
+
// ============================================
|
|
702
|
+
// WEBHOOKS
|
|
703
|
+
// ============================================
|
|
704
|
+
async listWebhooks(appId, params) {
|
|
705
|
+
const result = await this.get(`/admin/api/apps/${appId}/webhooks`, params);
|
|
706
|
+
return {
|
|
707
|
+
items: result?.items ?? [],
|
|
708
|
+
nextCursor: result?.nextCursor ?? null,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
async getWebhook(appId, webhookId) {
|
|
712
|
+
return this.get(`/admin/api/apps/${appId}/webhooks/${webhookId}`);
|
|
713
|
+
}
|
|
714
|
+
async createWebhook(appId, payload) {
|
|
715
|
+
return this.post(`/admin/api/apps/${appId}/webhooks`, payload);
|
|
716
|
+
}
|
|
717
|
+
async updateWebhook(appId, webhookId, payload, expectedModifiedAt) {
|
|
718
|
+
const body = expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload;
|
|
719
|
+
return this.patch(`/admin/api/apps/${appId}/webhooks/${webhookId}`, body);
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Delete a webhook. Archives it by default; `hard` hard-deletes the row
|
|
723
|
+
* instead (#2232), which is what frees a slot under the per-app webhook cap
|
|
724
|
+
* and releases the webhook key for reuse. Same `?hard=true` verb the admin
|
|
725
|
+
* integrations delete uses.
|
|
726
|
+
*/
|
|
727
|
+
async deleteWebhook(appId, webhookId, options) {
|
|
728
|
+
const query = options?.hard ? "?hard=true" : "";
|
|
729
|
+
return this.delete(`/admin/api/apps/${appId}/webhooks/${webhookId}${query}`);
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* #2803 — the operational verb pair for a webhook. It posts to the
|
|
733
|
+
* enable/disable route rather than PATCHing `status`, which is server-owned
|
|
734
|
+
* and refused on every configuration write path.
|
|
735
|
+
*/
|
|
736
|
+
async setWebhookAvailability(appId, webhookId, action) {
|
|
737
|
+
return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/${action}`, {});
|
|
738
|
+
}
|
|
739
|
+
// ── Server functions (#3179) ─────────────────────────────────────────
|
|
740
|
+
//
|
|
741
|
+
// The `functions` family mirrors workflows and scripts: a header with the
|
|
742
|
+
// gate and the entry, and append-only immutable config versions carrying the
|
|
743
|
+
// authored TOML bytes, the sources and the built bundle. The listing drains
|
|
744
|
+
// server-side, so it has no cursor.
|
|
745
|
+
async listFunctions(appId) {
|
|
746
|
+
const result = await this.get(`/admin/api/apps/${appId}/functions`);
|
|
747
|
+
return { items: result?.items ?? [] };
|
|
748
|
+
}
|
|
749
|
+
async getFunction(appId, functionId) {
|
|
750
|
+
return this.get(`/admin/api/apps/${appId}/functions/${functionId}`);
|
|
751
|
+
}
|
|
752
|
+
async createFunction(appId, payload) {
|
|
753
|
+
return this.post(`/admin/api/apps/${appId}/functions`, payload);
|
|
754
|
+
}
|
|
755
|
+
async updateFunction(appId, functionId, payload) {
|
|
756
|
+
return this.patch(`/admin/api/apps/${appId}/functions/${functionId}`, payload);
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Archive a function, or destroy it with `hard`. Archiving KEEPS the key
|
|
760
|
+
* reserved in the app's single key namespace; the hard delete is what
|
|
761
|
+
* releases it, destroys every config version and removes the R2 objects.
|
|
762
|
+
*/
|
|
763
|
+
async deleteFunction(appId, functionId, options) {
|
|
764
|
+
const query = options?.hard ? "?hard=true" : "";
|
|
765
|
+
return this.delete(`/admin/api/apps/${appId}/functions/${functionId}${query}`);
|
|
766
|
+
}
|
|
767
|
+
/** #2803 — the operational verb pair for a server function. */
|
|
768
|
+
async setFunctionAvailability(appId, functionId, action) {
|
|
769
|
+
return this.post(`/admin/api/apps/${appId}/functions/${functionId}/${action}`, {});
|
|
770
|
+
}
|
|
771
|
+
/** Every config version of a function, oldest first. */
|
|
772
|
+
/**
|
|
773
|
+
* A function's runs, newest first (#3181).
|
|
774
|
+
*
|
|
775
|
+
* A function run has no `workflowId`, so no workflow runs listing can see
|
|
776
|
+
* it — this is the only surface that lists one. Cursor-paginated like the
|
|
777
|
+
* other admin listings.
|
|
778
|
+
*/
|
|
779
|
+
async listFunctionRuns(appId, functionId, params) {
|
|
780
|
+
const result = await this.get(`/admin/api/apps/${appId}/functions/${functionId}/runs`, params);
|
|
781
|
+
return { items: result?.items ?? [], nextCursor: result?.nextCursor ?? null };
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* A function's invocation records, newest first — #3287.
|
|
785
|
+
*
|
|
786
|
+
* The ADMIN route, beside the runs listing above and for the same reason:
|
|
787
|
+
* the CLI acts as an operator, not as a member of the app. The app-API twin
|
|
788
|
+
* (`functions.logs`) is what a FUNCTION reads through the SDK profile.
|
|
789
|
+
*/
|
|
790
|
+
async listFunctionLogs(appId, functionId, params) {
|
|
791
|
+
const result = await this.get(`/admin/api/apps/${appId}/functions/${functionId}/logs`, params);
|
|
792
|
+
return { items: result?.items ?? [], nextCursor: result?.nextCursor ?? null };
|
|
793
|
+
}
|
|
794
|
+
async listFunctionConfigs(appId, functionId) {
|
|
795
|
+
const result = await this.get(`/admin/api/apps/${appId}/functions/${functionId}/configs`);
|
|
796
|
+
return { items: result?.items ?? [] };
|
|
797
|
+
}
|
|
798
|
+
async getFunctionConfig(appId, functionId, configId) {
|
|
799
|
+
return this.get(`/admin/api/apps/${appId}/functions/${functionId}/configs/${configId}`);
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* The stored envelope: the authored TOML bytes, every source file and the
|
|
803
|
+
* built bundle, all base64. This is what `config pull` writes back verbatim.
|
|
804
|
+
*/
|
|
805
|
+
async getFunctionConfigEnvelope(appId, functionId, configId) {
|
|
806
|
+
return this.get(`/admin/api/apps/${appId}/functions/${functionId}/configs/${configId}/envelope`);
|
|
807
|
+
}
|
|
808
|
+
/** Push a config version. The server verifies `contentHash` itself. */
|
|
809
|
+
async pushFunctionConfig(appId, functionId, payload) {
|
|
810
|
+
return this.post(`/admin/api/apps/${appId}/functions/${functionId}/configs`, payload);
|
|
811
|
+
}
|
|
812
|
+
/** #2803 — the operational verb pair for a prompt (admin API). */
|
|
813
|
+
async setPromptAvailability(appId, promptId, action) {
|
|
814
|
+
return this.post(`/admin/api/apps/${appId}/prompts/${promptId}/${action}`, {});
|
|
815
|
+
}
|
|
816
|
+
/** #2803 — the operational verb pair for an integration (admin API). */
|
|
817
|
+
async setIntegrationAvailability(appId, integrationId, action) {
|
|
818
|
+
return this.post(`/admin/api/apps/${appId}/integrations/${integrationId}/${action}`, {});
|
|
819
|
+
}
|
|
820
|
+
async rotateWebhookSecret(appId, webhookId, payload) {
|
|
821
|
+
return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/rotate-secret`, payload);
|
|
822
|
+
}
|
|
823
|
+
async listWebhookEvents(appId, webhookId, params) {
|
|
824
|
+
const result = await this.get(`/admin/api/apps/${appId}/webhooks/${webhookId}/events`, params);
|
|
825
|
+
// #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias.
|
|
826
|
+
const nextCursor = result?.nextCursor ?? result?.cursor ?? null;
|
|
827
|
+
return {
|
|
828
|
+
items: result?.items ?? [],
|
|
829
|
+
hasMore: result?.hasMore ?? !!nextCursor,
|
|
830
|
+
nextCursor,
|
|
831
|
+
cursor: nextCursor,
|
|
832
|
+
};
|
|
270
833
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
834
|
+
// ============================================
|
|
835
|
+
// CONNECTIONS INSPECTION (#1968)
|
|
836
|
+
// ============================================
|
|
837
|
+
/**
|
|
838
|
+
* List the live connections the server believes exist for exactly one of a
|
|
839
|
+
* document, user, or database. Read-only inspection over admin-api. Rows are
|
|
840
|
+
* per-connection-×-subscription bindings, so `connectionId` is not unique
|
|
841
|
+
* across rows. `params` on a database-subscription row is caller-defined
|
|
842
|
+
* bound CEL input — typed `unknown`.
|
|
843
|
+
*/
|
|
844
|
+
async listConnections(appId, selector, params) {
|
|
845
|
+
const result = await this.get(`/admin/api/apps/${appId}/connections`, {
|
|
846
|
+
...selector,
|
|
847
|
+
...params,
|
|
848
|
+
});
|
|
849
|
+
return {
|
|
850
|
+
items: result?.items ?? [],
|
|
851
|
+
nextCursor: result?.nextCursor ?? null,
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
async listSessions(appId, userId, params) {
|
|
855
|
+
const result = await this.get(`/admin/api/apps/${appId}/sessions`, {
|
|
856
|
+
userId,
|
|
857
|
+
...params,
|
|
858
|
+
});
|
|
859
|
+
return {
|
|
860
|
+
items: result?.items ?? [],
|
|
861
|
+
nextCursor: result?.nextCursor ?? null,
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
async testWebhook(appId, webhookId, payload) {
|
|
865
|
+
return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/test`, payload || {});
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Run a webhook's configured verifier over headers and a body captured from
|
|
869
|
+
* a real delivery, and report whether that signature checks out (#2445).
|
|
870
|
+
*
|
|
871
|
+
* `headers` is sent as ordered `[name, value]` pairs so a capture carrying a
|
|
872
|
+
* field name on more than one line survives the round trip. Nothing is
|
|
873
|
+
* delivered: no delivery record, no workflow run.
|
|
874
|
+
*/
|
|
875
|
+
async verifyWebhook(appId, webhookId, payload) {
|
|
876
|
+
return this.post(`/admin/api/apps/${appId}/webhooks/${webhookId}/verify`, payload);
|
|
877
|
+
}
|
|
878
|
+
// ============================================
|
|
879
|
+
// NAMED LOCKS (#1518)
|
|
880
|
+
// ============================================
|
|
881
|
+
async listLocks(appId) {
|
|
882
|
+
const result = await this.get(`/app/${appId}/api/locks`);
|
|
883
|
+
return { locks: result?.locks ?? [] };
|
|
884
|
+
}
|
|
885
|
+
async getLockStatus(appId, key) {
|
|
886
|
+
return this.post(`/app/${appId}/api/locks/status`, { key });
|
|
887
|
+
}
|
|
888
|
+
async acquireLock(appId, key, ttlMs) {
|
|
889
|
+
return this.post(`/app/${appId}/api/locks/acquire`, { key, ttlMs });
|
|
890
|
+
}
|
|
891
|
+
async releaseLock(appId, key, handleId) {
|
|
892
|
+
return this.post(`/app/${appId}/api/locks/release`, {
|
|
893
|
+
key,
|
|
894
|
+
handle: { handleId },
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
// ============================================
|
|
898
|
+
// CRON TRIGGERS
|
|
899
|
+
// ============================================
|
|
900
|
+
async listCronTriggers(appId) {
|
|
901
|
+
const result = await this.get(`/app/${appId}/api/cron-triggers`);
|
|
902
|
+
return { items: result?.items ?? [] };
|
|
903
|
+
}
|
|
904
|
+
async getCronTrigger(appId, triggerId) {
|
|
905
|
+
return this.get(`/app/${appId}/api/cron-triggers/${triggerId}`);
|
|
906
|
+
}
|
|
907
|
+
async createCronTrigger(appId, payload) {
|
|
908
|
+
return this.post(`/app/${appId}/api/cron-triggers`, payload);
|
|
909
|
+
}
|
|
910
|
+
async updateCronTrigger(appId, triggerId, payload) {
|
|
911
|
+
return this.put(`/app/${appId}/api/cron-triggers/${triggerId}`, payload);
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Retire a cron trigger. The default soft-deletes (writes the `archived`
|
|
915
|
+
* tombstone); `{ hard: true }` destroys the row, which is what
|
|
916
|
+
* `config push --prune` passes (#2803) so a pruned trigger stops consuming
|
|
917
|
+
* the per-app cap and holding its key.
|
|
918
|
+
*/
|
|
919
|
+
async deleteCronTrigger(appId, triggerId, options = {}) {
|
|
920
|
+
const query = options.hard ? "?hard=true" : "";
|
|
921
|
+
return this.delete(`/app/${appId}/api/cron-triggers/${triggerId}${query}`);
|
|
922
|
+
}
|
|
923
|
+
async disableCronTrigger(appId, triggerId) {
|
|
924
|
+
return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/disable`, {});
|
|
925
|
+
}
|
|
926
|
+
async enableCronTrigger(appId, triggerId) {
|
|
927
|
+
return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/enable`, {});
|
|
928
|
+
}
|
|
929
|
+
async testCronTrigger(appId, triggerId) {
|
|
930
|
+
return this.post(`/app/${appId}/api/cron-triggers/${triggerId}/test`, {});
|
|
931
|
+
}
|
|
932
|
+
// ============================================
|
|
933
|
+
// ITERATIONS (iterate-users introspection / reset — #1209)
|
|
934
|
+
// ============================================
|
|
935
|
+
async listIterations(appId) {
|
|
936
|
+
const result = await this.get(`/app/${appId}/api/iterations`);
|
|
937
|
+
return { items: result?.items ?? [] };
|
|
938
|
+
}
|
|
939
|
+
async getIteration(appId, iterationName) {
|
|
940
|
+
return this.get(`/app/${appId}/api/iterations/${encodeURIComponent(iterationName)}`);
|
|
941
|
+
}
|
|
942
|
+
async resetIteration(appId, iterationName) {
|
|
943
|
+
return this.post(`/app/${appId}/api/iterations/${encodeURIComponent(iterationName)}/reset`, {});
|
|
944
|
+
}
|
|
945
|
+
// ============================================
|
|
946
|
+
// RESOURCE METADATA (values — issue #1352)
|
|
947
|
+
// ============================================
|
|
948
|
+
async readResourceMetadata(appId, resourceType, resourceId, category) {
|
|
949
|
+
return this.get(`/app/${appId}/api/resources/${encodeURIComponent(resourceType)}/${encodeURIComponent(resourceId)}/metadata/${encodeURIComponent(category)}`);
|
|
950
|
+
}
|
|
951
|
+
async writeResourceMetadata(appId, resourceType, resourceId, category, data) {
|
|
952
|
+
return this.put(`/app/${appId}/api/resources/${encodeURIComponent(resourceType)}/${encodeURIComponent(resourceId)}/metadata/${encodeURIComponent(category)}`, { data });
|
|
953
|
+
}
|
|
954
|
+
async batchReadResourceMetadata(appId, requests) {
|
|
955
|
+
return this.post(`/app/${appId}/api/resources/metadata/batch`, {
|
|
956
|
+
requests,
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* List every stored metadata category on one resource (issue #1402 — debug
|
|
961
|
+
* tooling). The CLI authenticates as a console admin, so the app-level
|
|
962
|
+
* owner/admin bypass returns every category regardless of its readRule.
|
|
963
|
+
*/
|
|
964
|
+
async listResourceMetadata(appId, resourceType, resourceId) {
|
|
965
|
+
return this.get(`/app/${appId}/api/resources/${encodeURIComponent(resourceType)}/${encodeURIComponent(resourceId)}/metadata`);
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Delete one resource's metadata for one category (issue #1402 — debug
|
|
969
|
+
* tooling). Idempotent: deleting an absent item succeeds with
|
|
970
|
+
* `deleted: false` rather than a 404.
|
|
971
|
+
*/
|
|
972
|
+
async deleteResourceMetadata(appId, resourceType, resourceId, category) {
|
|
973
|
+
return this.delete(`/app/${appId}/api/resources/${encodeURIComponent(resourceType)}/${encodeURIComponent(resourceId)}/metadata/${encodeURIComponent(category)}`);
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Reverse-resolve a resource by a category's unique metadata value (issue
|
|
977
|
+
* #2137). A miss is a success with `resourceId: null`, never an error. The
|
|
978
|
+
* CLI authenticates as a console admin, so the app-level owner/admin bypass
|
|
979
|
+
* skips readRule evaluation.
|
|
980
|
+
*/
|
|
981
|
+
async resolveResourceMetadata(appId, resourceType, category, key, value) {
|
|
982
|
+
return this.post(`/app/${appId}/api/metadata/resolve`, {
|
|
983
|
+
resourceType,
|
|
984
|
+
category,
|
|
985
|
+
key,
|
|
986
|
+
value,
|
|
274
987
|
});
|
|
275
988
|
}
|
|
276
989
|
// ============================================
|
|
@@ -293,6 +1006,12 @@ export class ApiClient {
|
|
|
293
1006
|
const body = expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload;
|
|
294
1007
|
return this.patch(`/admin/api/apps/${appId}/prompts/${promptId}`, body);
|
|
295
1008
|
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Retire a prompt, or destroy it.
|
|
1011
|
+
*
|
|
1012
|
+
* `hard` was always sent by both first-party clients and always ignored by
|
|
1013
|
+
* the server; #2887 made it mean something — the plain call now archives.
|
|
1014
|
+
*/
|
|
296
1015
|
async deletePrompt(appId, promptId, hard) {
|
|
297
1016
|
const path = hard
|
|
298
1017
|
? `/admin/api/apps/${appId}/prompts/${promptId}?hard=true`
|
|
@@ -325,36 +1044,135 @@ export class ApiClient {
|
|
|
325
1044
|
async duplicatePromptConfig(appId, promptId, configId, payload) {
|
|
326
1045
|
return this.post(`/admin/api/apps/${appId}/prompts/${promptId}/configs/${configId}/duplicate`, payload || {});
|
|
327
1046
|
}
|
|
1047
|
+
// ============================================
|
|
1048
|
+
// SCRIPTS (Rhai transforms) — #1000 (prompt-model convergence)
|
|
1049
|
+
// ============================================
|
|
1050
|
+
//
|
|
1051
|
+
// The `Script` model is the authoring surface for `script` workflow
|
|
1052
|
+
// steps. A `Script` is a HEADER (name/description/activeConfigId); the
|
|
1053
|
+
// Rhai body lives on versioned `ScriptConfig` rows resolved LIVE at run
|
|
1054
|
+
// time. CLI `config push` reads `transforms/*.rhai` files and reconciles
|
|
1055
|
+
// them: a new file → create script (mints a default config); a changed
|
|
1056
|
+
// file → create a new config + activate it (zero fan-out — referencing
|
|
1057
|
+
// workflows pick up the new body on their next run).
|
|
1058
|
+
async listScripts(appId) {
|
|
1059
|
+
const result = await this.get(`/admin/api/apps/${appId}/scripts`);
|
|
1060
|
+
return { items: result?.items ?? [] };
|
|
1061
|
+
}
|
|
1062
|
+
async getScript(appId, scriptId) {
|
|
1063
|
+
return this.get(`/admin/api/apps/${appId}/scripts/${scriptId}`);
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* List a script's versioned configs (the `ScriptConfig` rows). Each config
|
|
1067
|
+
* is a distinct block version: `configId` is the stable version selector,
|
|
1068
|
+
* `contentHash` is its content identity, and `status` reports whether it is
|
|
1069
|
+
* `active`/`draft`/`archived`. The script header's `activeConfigId` names
|
|
1070
|
+
* the live version. Used by `scripts configs list` and by the script test
|
|
1071
|
+
* commands to run against a specific pinned version.
|
|
1072
|
+
*/
|
|
1073
|
+
async listScriptConfigs(appId, scriptId) {
|
|
1074
|
+
const result = await this.get(`/admin/api/apps/${appId}/scripts/${scriptId}/configs`);
|
|
1075
|
+
return { items: result?.items ?? [] };
|
|
1076
|
+
}
|
|
1077
|
+
async createScript(appId, payload) {
|
|
1078
|
+
// Mints the script header + a default `active` config carrying the body.
|
|
1079
|
+
return this.post(`/admin/api/apps/${appId}/scripts`, payload);
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Push a new body for an existing script by creating a new `ScriptConfig`
|
|
1083
|
+
* and activating it (the prompt-model "edit → activate" flow). Referencing
|
|
1084
|
+
* workflows pick up the new body on their next run with no fan-out. The
|
|
1085
|
+
* config name is unique per script, so we mint a timestamped name.
|
|
1086
|
+
*/
|
|
1087
|
+
async pushScriptBody(appId, scriptId, body,
|
|
1088
|
+
/**
|
|
1089
|
+
* The active state the caller believes holds right now (#2731 B10).
|
|
1090
|
+
* Creating an inactive config races nothing, so the guard sits on the
|
|
1091
|
+
* ACTIVATION step: if the active config has moved since the caller read it,
|
|
1092
|
+
* the server rejects with a 409 instead of overwriting a concurrent script
|
|
1093
|
+
* change. It takes BOTH halves of "what is active", because a config's body
|
|
1094
|
+
* can be edited in place without the active id changing — the id alone
|
|
1095
|
+
* would let such an edit through. Omitted (e.g. `--force`) means activate
|
|
1096
|
+
* unconditionally.
|
|
1097
|
+
*/
|
|
1098
|
+
expectedActive) {
|
|
1099
|
+
const configName = `sync-${Date.now()}`;
|
|
1100
|
+
const config = await this.post(`/admin/api/apps/${appId}/scripts/${scriptId}/configs`, { configName, body });
|
|
1101
|
+
const activateBody = {};
|
|
1102
|
+
if (expectedActive?.configId) {
|
|
1103
|
+
activateBody.expectedActiveConfigId = expectedActive.configId;
|
|
1104
|
+
}
|
|
1105
|
+
if (expectedActive?.modifiedAt) {
|
|
1106
|
+
activateBody.expectedActiveModifiedAt = expectedActive.modifiedAt;
|
|
1107
|
+
}
|
|
1108
|
+
await this.post(`/admin/api/apps/${appId}/scripts/${scriptId}/configs/${config.configId}/activate`, activateBody);
|
|
1109
|
+
return config;
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Delete a script header (cascades its configs). `force` overrides the
|
|
1113
|
+
* server's guard against deleting a script that an active workflow still
|
|
1114
|
+
* references by name — without it the server answers 409.
|
|
1115
|
+
*/
|
|
1116
|
+
async deleteScript(appId, scriptId, force) {
|
|
1117
|
+
const path = force
|
|
1118
|
+
? `/admin/api/apps/${appId}/scripts/${scriptId}?force=true`
|
|
1119
|
+
: `/admin/api/apps/${appId}/scripts/${scriptId}`;
|
|
1120
|
+
return this.delete(path);
|
|
1121
|
+
}
|
|
328
1122
|
async getPromptSchema(appId, promptId) {
|
|
329
1123
|
return this.get(`/admin/api/apps/${appId}/prompts/${promptId}/schema`);
|
|
330
1124
|
}
|
|
331
1125
|
// ============================================
|
|
332
1126
|
// BLOCK TEST CASES (Prompts, Integrations, Workflows)
|
|
333
1127
|
// ============================================
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
1128
|
+
/**
|
|
1129
|
+
* One page of a block's test cases. The endpoint paginates (default 50, cap
|
|
1130
|
+
* 100) and reports `nextCursor`; callers that reconcile the sync tree must
|
|
1131
|
+
* drain every page before deleting or classifying anything (#2769).
|
|
1132
|
+
*/
|
|
1133
|
+
async listTestCases(appId, blockType, blockId, params = {}) {
|
|
1134
|
+
const query = new URLSearchParams();
|
|
1135
|
+
if (params.limit !== undefined)
|
|
1136
|
+
query.set("limit", String(params.limit));
|
|
1137
|
+
if (params.cursor)
|
|
1138
|
+
query.set("cursor", params.cursor);
|
|
1139
|
+
const suffix = query.toString() ? `?${query.toString()}` : "";
|
|
1140
|
+
const result = await this.get(`/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases${suffix}`);
|
|
1141
|
+
return {
|
|
1142
|
+
items: result?.items ?? result ?? [],
|
|
1143
|
+
nextCursor: result?.nextCursor ?? undefined,
|
|
1144
|
+
};
|
|
337
1145
|
}
|
|
338
1146
|
async getTestCase(appId, blockType, blockId, testCaseId) {
|
|
339
1147
|
return this.get(`/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases/${testCaseId}`);
|
|
340
1148
|
}
|
|
341
1149
|
async createTestCase(appId, blockType, blockId, payload) {
|
|
342
|
-
//
|
|
1150
|
+
// #2769 — the JSON fields travel PARSED. The handler stringifies them for
|
|
1151
|
+
// storage (`createBlockTestCase`) and `serializeBlockTestCase` parses them
|
|
1152
|
+
// on the way back out, so a client that stringifies first stores a JSON
|
|
1153
|
+
// string of a JSON string and reads back a quoted blob.
|
|
343
1154
|
const serverPayload = {
|
|
344
1155
|
name: payload.name,
|
|
345
|
-
inputVariables:
|
|
1156
|
+
inputVariables: payload.inputVariables,
|
|
346
1157
|
configId: payload.configId,
|
|
347
1158
|
evaluatorPromptId: payload.evaluatorPromptId,
|
|
348
1159
|
evaluatorConfigId: payload.evaluatorConfigId,
|
|
349
1160
|
};
|
|
1161
|
+
// #2896 — only when the caller has one: an older server rejects the key
|
|
1162
|
+
// outright, and push reads that 400 to learn what it is talking to.
|
|
1163
|
+
if (payload.key !== undefined)
|
|
1164
|
+
serverPayload.key = payload.key;
|
|
1165
|
+
if (payload.description !== undefined) {
|
|
1166
|
+
serverPayload.description = payload.description;
|
|
1167
|
+
}
|
|
350
1168
|
if (payload.expectedOutputPattern) {
|
|
351
1169
|
serverPayload.expectedOutputPattern = payload.expectedOutputPattern;
|
|
352
1170
|
}
|
|
353
1171
|
if (payload.expectedOutputContains) {
|
|
354
|
-
serverPayload.expectedOutputContains =
|
|
1172
|
+
serverPayload.expectedOutputContains = payload.expectedOutputContains;
|
|
355
1173
|
}
|
|
356
1174
|
if (payload.expectedJsonSubset) {
|
|
357
|
-
serverPayload.expectedJsonSubset =
|
|
1175
|
+
serverPayload.expectedJsonSubset = payload.expectedJsonSubset;
|
|
358
1176
|
}
|
|
359
1177
|
return this.post(`/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases`, serverPayload);
|
|
360
1178
|
}
|
|
@@ -362,21 +1180,23 @@ export class ApiClient {
|
|
|
362
1180
|
const serverPayload = {};
|
|
363
1181
|
if (payload.name !== undefined)
|
|
364
1182
|
serverPayload.name = payload.name;
|
|
1183
|
+
if (payload.description !== undefined) {
|
|
1184
|
+
serverPayload.description = payload.description;
|
|
1185
|
+
}
|
|
1186
|
+
// #2769 — parsed on the wire (see `createTestCase`), and an explicit `null`
|
|
1187
|
+
// is how the authored file clears a field: the handler only touches keys
|
|
1188
|
+
// that are present, so an omitted key retains the previous value.
|
|
365
1189
|
if (payload.inputVariables !== undefined) {
|
|
366
|
-
serverPayload.inputVariables =
|
|
1190
|
+
serverPayload.inputVariables = payload.inputVariables;
|
|
367
1191
|
}
|
|
368
1192
|
if (payload.expectedOutputPattern !== undefined) {
|
|
369
1193
|
serverPayload.expectedOutputPattern = payload.expectedOutputPattern;
|
|
370
1194
|
}
|
|
371
1195
|
if (payload.expectedOutputContains !== undefined) {
|
|
372
|
-
serverPayload.expectedOutputContains = payload.expectedOutputContains
|
|
373
|
-
? JSON.stringify(payload.expectedOutputContains)
|
|
374
|
-
: null;
|
|
1196
|
+
serverPayload.expectedOutputContains = payload.expectedOutputContains;
|
|
375
1197
|
}
|
|
376
1198
|
if (payload.expectedJsonSubset !== undefined) {
|
|
377
|
-
serverPayload.expectedJsonSubset = payload.expectedJsonSubset
|
|
378
|
-
? JSON.stringify(payload.expectedJsonSubset)
|
|
379
|
-
: null;
|
|
1199
|
+
serverPayload.expectedJsonSubset = payload.expectedJsonSubset;
|
|
380
1200
|
}
|
|
381
1201
|
if (payload.configId !== undefined)
|
|
382
1202
|
serverPayload.configId = payload.configId;
|
|
@@ -386,6 +1206,8 @@ export class ApiClient {
|
|
|
386
1206
|
if (payload.evaluatorConfigId !== undefined) {
|
|
387
1207
|
serverPayload.evaluatorConfigId = payload.evaluatorConfigId;
|
|
388
1208
|
}
|
|
1209
|
+
if (payload.key !== undefined)
|
|
1210
|
+
serverPayload.key = payload.key;
|
|
389
1211
|
return this.patch(`/admin/api/apps/${appId}/blocks/${blockType}/${blockId}/test-cases/${testCaseId}`, serverPayload);
|
|
390
1212
|
}
|
|
391
1213
|
async deleteTestCase(appId, blockType, blockId, testCaseId) {
|
|
@@ -415,14 +1237,8 @@ export class ApiClient {
|
|
|
415
1237
|
});
|
|
416
1238
|
const text = await response.text();
|
|
417
1239
|
if (!response.ok) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
errorData = JSON.parse(text);
|
|
421
|
-
}
|
|
422
|
-
catch {
|
|
423
|
-
errorData = { message: text || `HTTP ${response.status}` };
|
|
424
|
-
}
|
|
425
|
-
throw new ApiError(errorData.message || `HTTP ${response.status}`, response.status, errorData.code);
|
|
1240
|
+
const parsed = parseErrorResponse(response, text, url);
|
|
1241
|
+
throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
|
|
426
1242
|
}
|
|
427
1243
|
return text ? JSON.parse(text) : null;
|
|
428
1244
|
}
|
|
@@ -442,14 +1258,8 @@ export class ApiClient {
|
|
|
442
1258
|
});
|
|
443
1259
|
if (!response.ok) {
|
|
444
1260
|
const text = await response.text();
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
errorData = JSON.parse(text);
|
|
448
|
-
}
|
|
449
|
-
catch {
|
|
450
|
-
errorData = { message: text || `HTTP ${response.status}` };
|
|
451
|
-
}
|
|
452
|
-
throw new ApiError(errorData.message || `HTTP ${response.status}`, response.status, errorData.code);
|
|
1261
|
+
const parsed = parseErrorResponse(response, text, url);
|
|
1262
|
+
throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
|
|
453
1263
|
}
|
|
454
1264
|
const arrayBuffer = await response.arrayBuffer();
|
|
455
1265
|
return {
|
|
@@ -487,6 +1297,16 @@ export class ApiClient {
|
|
|
487
1297
|
async getWorkflow(appId, workflowId) {
|
|
488
1298
|
return this.get(`/admin/api/apps/${appId}/workflows/${workflowId}`);
|
|
489
1299
|
}
|
|
1300
|
+
/**
|
|
1301
|
+
* One app's workflow step usage report (#3132).
|
|
1302
|
+
*
|
|
1303
|
+
* `windowDays` is omitted from the query when the caller did not ask for a
|
|
1304
|
+
* window, so the server applies its own default rather than the CLI
|
|
1305
|
+
* restating it on the wire.
|
|
1306
|
+
*/
|
|
1307
|
+
async getWorkflowUsage(appId, params) {
|
|
1308
|
+
return this.get(`/admin/api/apps/${appId}/workflows/usage`, params);
|
|
1309
|
+
}
|
|
490
1310
|
async createWorkflow(appId, payload) {
|
|
491
1311
|
return this.post(`/admin/api/apps/${appId}/workflows`, payload);
|
|
492
1312
|
}
|
|
@@ -494,15 +1314,33 @@ export class ApiClient {
|
|
|
494
1314
|
const body = expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload;
|
|
495
1315
|
return this.patch(`/admin/api/apps/${appId}/workflows/${workflowId}`, body);
|
|
496
1316
|
}
|
|
497
|
-
|
|
498
|
-
|
|
1317
|
+
/**
|
|
1318
|
+
* Retire a workflow, or destroy it (#2887).
|
|
1319
|
+
*
|
|
1320
|
+
* The plain call ARCHIVES: the definition row stays, so its runs, revisions
|
|
1321
|
+
* and test cases keep resolving. `{ hard: true }` performs the full cascade
|
|
1322
|
+
* and is what releases the `workflowKey` — the flag `config push --prune`
|
|
1323
|
+
* sends, for the same reason it sends it for webhooks and integrations.
|
|
1324
|
+
*/
|
|
1325
|
+
async deleteWorkflow(appId, workflowId, options) {
|
|
1326
|
+
const path = `/admin/api/apps/${appId}/workflows/${workflowId}`;
|
|
1327
|
+
return this.delete(options?.hard ? `${path}?hard=true` : path);
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* #2645 criterion 9 — the operational toggle. Deliberately NOT the config
|
|
1331
|
+
* PATCH: `config push` owns every configuration field, and taking a workflow
|
|
1332
|
+
* out of service must not touch one, or an operator's action would show up as
|
|
1333
|
+
* drift against the repo. Same route shape as `cron-triggers pause|resume`.
|
|
1334
|
+
*/
|
|
1335
|
+
async disableWorkflow(appId, workflowId) {
|
|
1336
|
+
return this.post(`/app/${appId}/api/workflows/${workflowId}/disable`, {});
|
|
1337
|
+
}
|
|
1338
|
+
async enableWorkflow(appId, workflowId) {
|
|
1339
|
+
return this.post(`/app/${appId}/api/workflows/${workflowId}/enable`, {});
|
|
499
1340
|
}
|
|
500
1341
|
async updateWorkflowDraft(appId, workflowId, payload) {
|
|
501
1342
|
return this.put(`/admin/api/apps/${appId}/workflows/${workflowId}/draft`, payload);
|
|
502
1343
|
}
|
|
503
|
-
async publishWorkflow(appId, workflowId) {
|
|
504
|
-
return this.post(`/admin/api/apps/${appId}/workflows/${workflowId}/publish`, {});
|
|
505
|
-
}
|
|
506
1344
|
async previewWorkflow(appId, workflowId, payload) {
|
|
507
1345
|
return this.post(`/admin/api/apps/${appId}/workflows/${workflowId}/preview`, payload);
|
|
508
1346
|
}
|
|
@@ -511,14 +1349,43 @@ export class ApiClient {
|
|
|
511
1349
|
}
|
|
512
1350
|
async listWorkflowRuns(appId, workflowId, params) {
|
|
513
1351
|
const result = await this.get(`/admin/api/apps/${appId}/workflows/${workflowId}/runs`, params);
|
|
1352
|
+
// #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias.
|
|
1353
|
+
const nextCursor = result?.nextCursor ?? result?.cursor ?? null;
|
|
514
1354
|
return {
|
|
515
1355
|
items: result?.items ?? [],
|
|
516
|
-
|
|
1356
|
+
hasMore: result?.hasMore,
|
|
1357
|
+
nextCursor,
|
|
1358
|
+
cursor: nextCursor,
|
|
1359
|
+
resumeAfter: result?.resumeAfter,
|
|
1360
|
+
scanned: typeof result?.scanned === "number" ? result.scanned : undefined,
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Workflow runs started by one app user, across every workflow (#1967).
|
|
1365
|
+
* Backed by `GET /admin/api/apps/{appId}/users/{userId}/workflow-runs`,
|
|
1366
|
+
* which resolves the app user before querying the `runsByUser` index.
|
|
1367
|
+
*/
|
|
1368
|
+
async listUserWorkflowRuns(appId, userId, params) {
|
|
1369
|
+
const result = await this.get(`/admin/api/apps/${appId}/users/${encodeURIComponent(userId)}/workflow-runs`, params);
|
|
1370
|
+
const nextCursor = result?.nextCursor ?? result?.cursor ?? null;
|
|
1371
|
+
return {
|
|
1372
|
+
items: result?.items ?? [],
|
|
1373
|
+
hasMore: result?.hasMore,
|
|
1374
|
+
nextCursor,
|
|
1375
|
+
cursor: nextCursor,
|
|
1376
|
+
scanned: typeof result?.scanned === "number" ? result.scanned : undefined,
|
|
517
1377
|
};
|
|
518
1378
|
}
|
|
519
1379
|
async getWorkflowRunStatus(appId, workflowId, runId) {
|
|
520
1380
|
return this.get(`/admin/api/apps/${appId}/workflows/${workflowId}/runs/${runId}/status`);
|
|
521
1381
|
}
|
|
1382
|
+
async getWorkflowStepRuns(appId, workflowId, runId) {
|
|
1383
|
+
const result = await this.get(`/admin/api/apps/${appId}/workflows/${workflowId}/runs/${runId}/steps`);
|
|
1384
|
+
return { items: result?.items ?? [] };
|
|
1385
|
+
}
|
|
1386
|
+
// Workflow analytics have one client path, `getAnalyticsTopWorkflows`
|
|
1387
|
+
// (issue #2766): `getTopWorkflows` was a second name for the same endpoint,
|
|
1388
|
+
// and `getWorkflowAnalytics` called a route the server never registered.
|
|
522
1389
|
// ============================================
|
|
523
1390
|
// WORKFLOW CONFIGURATIONS
|
|
524
1391
|
// ============================================
|
|
@@ -547,29 +1414,78 @@ export class ApiClient {
|
|
|
547
1414
|
// ============================================
|
|
548
1415
|
// ANALYTICS
|
|
549
1416
|
// ============================================
|
|
550
|
-
async getAnalyticsOverview(appId, params) {
|
|
551
|
-
return this.get(`/app/${appId}/api/analytics/overview`, params);
|
|
552
|
-
}
|
|
553
1417
|
async getAnalyticsTopUsers(appId, params) {
|
|
554
1418
|
return this.get(`/app/${appId}/api/analytics/users/top`, params);
|
|
555
1419
|
}
|
|
556
|
-
async getAnalyticsUserTimeline(appId, userUlid, params) {
|
|
557
|
-
return this.get(`/app/${appId}/api/analytics/users/${userUlid}/timeline`, params);
|
|
558
|
-
}
|
|
559
|
-
async getAnalyticsUserEvents(appId, userUlid, params) {
|
|
560
|
-
return this.get(`/app/${appId}/api/analytics/users/${userUlid}/events`, params);
|
|
561
|
-
}
|
|
562
1420
|
async getAnalyticsIntegrationMetrics(appId, params) {
|
|
563
1421
|
return this.get(`/app/${appId}/api/analytics/integrations`, params);
|
|
564
1422
|
}
|
|
1423
|
+
async getAnalyticsOverviewDau(appId) {
|
|
1424
|
+
return this.get(`/app/${appId}/api/analytics/overview/dau`);
|
|
1425
|
+
}
|
|
1426
|
+
async getAnalyticsOverviewWau(appId) {
|
|
1427
|
+
return this.get(`/app/${appId}/api/analytics/overview/wau`);
|
|
1428
|
+
}
|
|
1429
|
+
async getAnalyticsOverviewMau(appId) {
|
|
1430
|
+
return this.get(`/app/${appId}/api/analytics/overview/mau`);
|
|
1431
|
+
}
|
|
1432
|
+
async getAnalyticsOverviewGrowth(appId, params) {
|
|
1433
|
+
return this.get(`/app/${appId}/api/analytics/overview/growth`, params);
|
|
1434
|
+
}
|
|
1435
|
+
async getAnalyticsDailyActive(appId, params) {
|
|
1436
|
+
return this.get(`/app/${appId}/api/analytics/daily-active`, params);
|
|
1437
|
+
}
|
|
1438
|
+
async getAnalyticsRollingActive(appId, params) {
|
|
1439
|
+
return this.get(`/app/${appId}/api/analytics/rolling-active`, params);
|
|
1440
|
+
}
|
|
1441
|
+
async getAnalyticsCohortRetention(appId) {
|
|
1442
|
+
return this.get(`/app/${appId}/api/analytics/cohort-retention`);
|
|
1443
|
+
}
|
|
1444
|
+
async getAnalyticsUserSearch(appId, params) {
|
|
1445
|
+
return this.get(`/app/${appId}/api/analytics/users/search`, params);
|
|
1446
|
+
}
|
|
1447
|
+
async getAnalyticsUserDetail(appId, userUlid) {
|
|
1448
|
+
return this.get(`/app/${appId}/api/analytics/users/${userUlid}/detail`);
|
|
1449
|
+
}
|
|
1450
|
+
async getAnalyticsUserSnapshot(appId, userUlid) {
|
|
1451
|
+
return this.get(`/app/${appId}/api/analytics/users/${userUlid}/snapshot`);
|
|
1452
|
+
}
|
|
1453
|
+
async getAnalyticsEvents(appId, params) {
|
|
1454
|
+
return this.get(`/app/${appId}/api/analytics/events`, params);
|
|
1455
|
+
}
|
|
1456
|
+
async getAnalyticsEventsGrouped(appId, params) {
|
|
1457
|
+
return this.get(`/app/${appId}/api/analytics/events/grouped`, params);
|
|
1458
|
+
}
|
|
1459
|
+
async getAnalyticsErrorsGroups(appId, params) {
|
|
1460
|
+
return this.get(`/app/${appId}/api/analytics/errors/groups`, params);
|
|
1461
|
+
}
|
|
1462
|
+
async getAnalyticsTopWorkflows(appId, params) {
|
|
1463
|
+
return this.get(`/app/${appId}/api/analytics/workflows/top`, params);
|
|
1464
|
+
}
|
|
1465
|
+
async getAnalyticsTopPrompts(appId, params) {
|
|
1466
|
+
return this.get(`/app/${appId}/api/analytics/prompts/top`, params);
|
|
1467
|
+
}
|
|
565
1468
|
// ============================================
|
|
566
1469
|
// SUPER ADMIN: ADMINS
|
|
567
1470
|
// ============================================
|
|
1471
|
+
/**
|
|
1472
|
+
* The signed-in admin's own profile, `role` included.
|
|
1473
|
+
*
|
|
1474
|
+
* The role is read from the server rather than from the cached credentials
|
|
1475
|
+
* file: a role changed after login would otherwise let a stale local record
|
|
1476
|
+
* decide an authorization question.
|
|
1477
|
+
*/
|
|
1478
|
+
async getMe() {
|
|
1479
|
+
return this.get("/admin/api/me");
|
|
1480
|
+
}
|
|
568
1481
|
async listAllAdmins(params) {
|
|
569
1482
|
const result = await this.get("/admin/api/admins", params);
|
|
1483
|
+
// #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias.
|
|
1484
|
+
const nextCursor = result?.nextCursor ?? result?.cursor ?? null;
|
|
570
1485
|
return {
|
|
571
|
-
admins: result?.admins ?? [],
|
|
572
|
-
|
|
1486
|
+
admins: result?.admins ?? result?.items ?? [],
|
|
1487
|
+
nextCursor,
|
|
1488
|
+
cursor: nextCursor,
|
|
573
1489
|
};
|
|
574
1490
|
}
|
|
575
1491
|
async searchAdminByEmail(email) {
|
|
@@ -637,21 +1553,6 @@ export class ApiClient {
|
|
|
637
1553
|
return this.delete(`/admin/api/catalog/integrations/${catalogId}`);
|
|
638
1554
|
}
|
|
639
1555
|
// ============================================
|
|
640
|
-
// LLM UTILITIES
|
|
641
|
-
// ============================================
|
|
642
|
-
async listLlmModels(provider = "openrouter") {
|
|
643
|
-
return this.get("/admin/api/llm/models", { provider });
|
|
644
|
-
}
|
|
645
|
-
async generatePrompt(payload) {
|
|
646
|
-
return this.post("/admin/api/llm/generate-prompt", payload);
|
|
647
|
-
}
|
|
648
|
-
async generateEvaluator(payload) {
|
|
649
|
-
return this.post("/admin/api/llm/generate-evaluator", payload);
|
|
650
|
-
}
|
|
651
|
-
async generateWorkflowEvaluator(payload) {
|
|
652
|
-
return this.post("/admin/api/llm/generate-evaluator-workflow", payload);
|
|
653
|
-
}
|
|
654
|
-
// ============================================
|
|
655
1556
|
// BATCH TEST EXECUTION
|
|
656
1557
|
// ============================================
|
|
657
1558
|
async startBatchTests(appId, blockType, blockId, payload) {
|
|
@@ -670,6 +1571,33 @@ export class ApiClient {
|
|
|
670
1571
|
return this.get(`/admin/api/apps/${appId}/comparisons/${group}`);
|
|
671
1572
|
}
|
|
672
1573
|
// ============================================
|
|
1574
|
+
// EMAIL TEMPLATES
|
|
1575
|
+
// ============================================
|
|
1576
|
+
async listEmailTemplates(appId) {
|
|
1577
|
+
return this.get(`/admin/api/apps/${appId}/email-templates`);
|
|
1578
|
+
}
|
|
1579
|
+
async getEmailTemplate(appId, emailType) {
|
|
1580
|
+
return this.get(`/admin/api/apps/${appId}/email-templates/${emailType}`);
|
|
1581
|
+
}
|
|
1582
|
+
/**
|
|
1583
|
+
* Create or replace an email-template override.
|
|
1584
|
+
*
|
|
1585
|
+
* `expectedModifiedAt` is the optimistic-concurrency token every sibling
|
|
1586
|
+
* config endpoint already takes (#2731 B10). Optional and additive: when
|
|
1587
|
+
* omitted the write is unconditional, which is how `--force` and every
|
|
1588
|
+
* pre-existing caller keep working; when supplied and stale, the server
|
|
1589
|
+
* answers 409 rather than silently overwriting a concurrent edit.
|
|
1590
|
+
*/
|
|
1591
|
+
async upsertEmailTemplate(appId, emailType, payload, expectedModifiedAt) {
|
|
1592
|
+
return this.put(`/admin/api/apps/${appId}/email-templates/${emailType}`, expectedModifiedAt ? { ...payload, expectedModifiedAt } : payload);
|
|
1593
|
+
}
|
|
1594
|
+
async deleteEmailTemplate(appId, emailType) {
|
|
1595
|
+
return this.delete(`/admin/api/apps/${appId}/email-templates/${emailType}`);
|
|
1596
|
+
}
|
|
1597
|
+
async testEmailTemplate(appId, emailType, payload) {
|
|
1598
|
+
return this.post(`/admin/api/apps/${appId}/email-templates/${emailType}/test`, payload || {});
|
|
1599
|
+
}
|
|
1600
|
+
// ============================================
|
|
673
1601
|
// ACCESS TOKENS
|
|
674
1602
|
// ============================================
|
|
675
1603
|
async createToken(appId, data) {
|
|
@@ -688,8 +1616,42 @@ export class ApiClient {
|
|
|
688
1616
|
// ============================================
|
|
689
1617
|
// DATABASES
|
|
690
1618
|
// ============================================
|
|
691
|
-
|
|
692
|
-
|
|
1619
|
+
/**
|
|
1620
|
+
* List an app's databases, following the cursor to the end.
|
|
1621
|
+
*
|
|
1622
|
+
* The endpoint returns the `{ items, hasMore, nextCursor }` envelope and
|
|
1623
|
+
* caps a page at 100 (#1958), so `databases list` needs the follow-up pages
|
|
1624
|
+
* to show every database. `paginateAll` carries the repeat-cursor and
|
|
1625
|
+
* max-page guards, so a server that hands out a non-advancing cursor fails
|
|
1626
|
+
* loudly instead of looping forever.
|
|
1627
|
+
*
|
|
1628
|
+
* `options.owner` narrows the result to the databases that user created; the
|
|
1629
|
+
* server resolves it through the `databasesByCreator` index (#1965).
|
|
1630
|
+
*/
|
|
1631
|
+
async listDatabases(appId, options) {
|
|
1632
|
+
return paginateAll(async (cursor) => {
|
|
1633
|
+
const params = new URLSearchParams();
|
|
1634
|
+
if (options?.owner)
|
|
1635
|
+
params.set("owner", options.owner);
|
|
1636
|
+
if (cursor)
|
|
1637
|
+
params.set("cursor", cursor);
|
|
1638
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
1639
|
+
const resp = await this.get(`/app/${appId}/api/databases${qs}`);
|
|
1640
|
+
// Tolerant read: a server older than #1958 answers with a bare array.
|
|
1641
|
+
// Without this a published CLI pointed at a not-yet-upgraded server
|
|
1642
|
+
// would print nothing at all. Such a server also predates the `owner`
|
|
1643
|
+
// filter (#1965) and ignores the unknown query parameter, so apply the
|
|
1644
|
+
// filter here — the legacy rows carry `createdBy`, so this reproduces
|
|
1645
|
+
// the server's own post-join filter (#2245).
|
|
1646
|
+
if (Array.isArray(resp)) {
|
|
1647
|
+
return {
|
|
1648
|
+
items: options?.owner
|
|
1649
|
+
? resp.filter((db) => db?.createdBy === options.owner)
|
|
1650
|
+
: resp,
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
return { items: resp?.items ?? [], nextCursor: resp?.nextCursor };
|
|
1654
|
+
});
|
|
693
1655
|
}
|
|
694
1656
|
async createDatabase(appId, data) {
|
|
695
1657
|
return this.post(`/app/${appId}/api/databases`, data);
|
|
@@ -703,29 +1665,49 @@ export class ApiClient {
|
|
|
703
1665
|
async deleteDatabase(appId, databaseId) {
|
|
704
1666
|
return this.delete(`/app/${appId}/api/databases/${databaseId}`);
|
|
705
1667
|
}
|
|
1668
|
+
/**
|
|
1669
|
+
* Read a database's CEL context dict.
|
|
1670
|
+
*
|
|
1671
|
+
* The HTTP path stays `/metadata` because the wire field name is still
|
|
1672
|
+
* `metadata`; only the client/CLI-facing helper names were reframed.
|
|
1673
|
+
*/
|
|
1674
|
+
async getDatabaseCelContext(appId, databaseId) {
|
|
1675
|
+
return this.get(`/app/${appId}/api/databases/${databaseId}/metadata`);
|
|
1676
|
+
}
|
|
1677
|
+
/** Update a database's CEL context dict (merge with existing). */
|
|
1678
|
+
async updateDatabaseCelContext(appId, databaseId, celContext) {
|
|
1679
|
+
return this.patch(`/app/${appId}/api/databases/${databaseId}/metadata`, celContext);
|
|
1680
|
+
}
|
|
1681
|
+
/** @deprecated Use {@link getDatabaseCelContext} instead. */
|
|
1682
|
+
async getDatabaseMetadata(appId, databaseId) {
|
|
1683
|
+
return this.getDatabaseCelContext(appId, databaseId);
|
|
1684
|
+
}
|
|
1685
|
+
/** @deprecated Use {@link updateDatabaseCelContext} instead. */
|
|
1686
|
+
async updateDatabaseMetadata(appId, databaseId, metadata) {
|
|
1687
|
+
return this.updateDatabaseCelContext(appId, databaseId, metadata);
|
|
1688
|
+
}
|
|
706
1689
|
// ============================================
|
|
707
1690
|
// DATABASE PERMISSIONS
|
|
708
1691
|
// ============================================
|
|
709
1692
|
async listDatabasePermissions(appId, databaseId) {
|
|
710
1693
|
return this.get(`/app/${appId}/api/databases/${databaseId}/permissions`);
|
|
711
1694
|
}
|
|
712
|
-
async
|
|
713
|
-
return this.put(`/app/${appId}/api/databases/${databaseId}/permissions`,
|
|
1695
|
+
async addDatabaseManager(appId, databaseId, userId) {
|
|
1696
|
+
return this.put(`/app/${appId}/api/databases/${databaseId}/permissions`, {
|
|
1697
|
+
userId,
|
|
1698
|
+
permission: "manager",
|
|
1699
|
+
});
|
|
714
1700
|
}
|
|
715
|
-
async
|
|
1701
|
+
async removeDatabaseManager(appId, databaseId, userId) {
|
|
716
1702
|
return this.delete(`/app/${appId}/api/databases/${databaseId}/permissions/${userId}`);
|
|
717
1703
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
async listDatabaseGroupPermissions(appId, databaseId) {
|
|
722
|
-
return this.get(`/app/${appId}/api/databases/${databaseId}/group-permissions`);
|
|
723
|
-
}
|
|
724
|
-
async grantDatabaseGroupPermission(appId, databaseId, data) {
|
|
725
|
-
return this.post(`/app/${appId}/api/databases/${databaseId}/group-permissions`, data);
|
|
1704
|
+
/** @deprecated Use {@link addDatabaseManager} instead. */
|
|
1705
|
+
async grantDatabasePermission(appId, databaseId, data) {
|
|
1706
|
+
return this.addDatabaseManager(appId, databaseId, data.userId);
|
|
726
1707
|
}
|
|
727
|
-
|
|
728
|
-
|
|
1708
|
+
/** @deprecated Use {@link removeDatabaseManager} instead. */
|
|
1709
|
+
async revokeDatabasePermission(appId, databaseId, userId) {
|
|
1710
|
+
return this.delete(`/app/${appId}/api/databases/${databaseId}/permissions/${userId}`);
|
|
729
1711
|
}
|
|
730
1712
|
// ============================================
|
|
731
1713
|
// DATABASE RECORDS & SCHEMA
|
|
@@ -736,17 +1718,140 @@ export class ApiClient {
|
|
|
736
1718
|
async describeDatabaseModel(appId, databaseId, modelName) {
|
|
737
1719
|
return this.get(`/app/${appId}/api/databases/${databaseId}/records/describe`, { modelName });
|
|
738
1720
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
1721
|
+
/**
|
|
1722
|
+
* Query records in a database model. The Durable Object answers with its own
|
|
1723
|
+
* `{ data, hasMore, nextCursor?, prevCursor? }` envelope; that wire shape is
|
|
1724
|
+
* shared with the JS client, the Swift client and web-admin, so the CLI
|
|
1725
|
+
* normalizes it at its own boundary instead (issue #2357) and hands callers
|
|
1726
|
+
* the same `{ items, hasMore, nextCursor? }` envelope as
|
|
1727
|
+
* {@link queryDocumentRecords}.
|
|
1728
|
+
*/
|
|
1729
|
+
async queryDatabaseRecords(appId, databaseId, modelName, queryOptions) {
|
|
1730
|
+
const body = { modelName };
|
|
1731
|
+
if (queryOptions?.filter)
|
|
1732
|
+
body.filter = queryOptions.filter;
|
|
1733
|
+
const options = {};
|
|
1734
|
+
if (queryOptions?.limit)
|
|
1735
|
+
options.limit = queryOptions.limit;
|
|
1736
|
+
if (queryOptions?.cursor)
|
|
1737
|
+
options.uniqueStartKey = queryOptions.cursor;
|
|
1738
|
+
if (Object.keys(options).length > 0)
|
|
1739
|
+
body.options = options;
|
|
1740
|
+
const raw = await this.post(`/app/${appId}/api/databases/${databaseId}/records/query`, body);
|
|
1741
|
+
return normalizeCliListEnvelope(raw);
|
|
1742
|
+
}
|
|
1743
|
+
async countDatabaseRecords(appId, databaseId, modelName, filter) {
|
|
1744
|
+
const body = { modelName };
|
|
1745
|
+
if (filter)
|
|
1746
|
+
body.filter = filter;
|
|
1747
|
+
return this.post(`/app/${appId}/api/databases/${databaseId}/records/count`, body);
|
|
1748
|
+
}
|
|
1749
|
+
async aggregateDatabaseRecords(appId, databaseId, modelName, options) {
|
|
1750
|
+
return this.post(`/app/${appId}/api/databases/${databaseId}/records/aggregate`, {
|
|
1751
|
+
modelName,
|
|
1752
|
+
options,
|
|
1753
|
+
});
|
|
744
1754
|
}
|
|
745
|
-
|
|
746
|
-
|
|
1755
|
+
/**
|
|
1756
|
+
* Upsert one record via `records/save`: creates the record when the id is
|
|
1757
|
+
* new, and merges the supplied fields into it when it already exists (the DO
|
|
1758
|
+
* writes with `ON CONFLICT … json_patch`, so fields you leave out survive).
|
|
1759
|
+
* `patchDatabaseRecord` is the same merge but requires the record to exist.
|
|
1760
|
+
*
|
|
1761
|
+
* `id` is optional: when omitted the body carries no `id`, which the
|
|
1762
|
+
* DatabaseDO only accepts alongside an `upsertOn` field — callers that want a
|
|
1763
|
+
* plain create must supply an id themselves (`databases records save` mints a
|
|
1764
|
+
* ULID, matching the CSV import path).
|
|
1765
|
+
*
|
|
1766
|
+
* Returns the endpoint's flat `{ success, id, appliedFields }` — not the
|
|
1767
|
+
* saved record.
|
|
1768
|
+
*/
|
|
1769
|
+
async saveDatabaseRecord(appId, databaseId, modelName, id, data) {
|
|
1770
|
+
const body = { modelName, data };
|
|
1771
|
+
if (id !== undefined)
|
|
1772
|
+
body.id = id;
|
|
1773
|
+
return this.post(`/app/${appId}/api/databases/${databaseId}/records/save`, body);
|
|
1774
|
+
}
|
|
1775
|
+
/**
|
|
1776
|
+
* Merge the supplied fields into an existing record via `records/patch`.
|
|
1777
|
+
*
|
|
1778
|
+
* Fields absent from `data` are left as they are. Unlike `saveDatabaseRecord`
|
|
1779
|
+
* the record must already exist — the DatabaseDO answers 404 for an unknown
|
|
1780
|
+
* id rather than creating one. Returns the flat
|
|
1781
|
+
* `{ success, id, appliedFields }`.
|
|
1782
|
+
*/
|
|
1783
|
+
async patchDatabaseRecord(appId, databaseId, modelName, id, data) {
|
|
1784
|
+
return this.post(`/app/${appId}/api/databases/${databaseId}/records/patch`, {
|
|
1785
|
+
modelName,
|
|
1786
|
+
id,
|
|
1787
|
+
data,
|
|
1788
|
+
});
|
|
747
1789
|
}
|
|
748
|
-
|
|
749
|
-
|
|
1790
|
+
// ============================================
|
|
1791
|
+
// DATABASE OPERATIONS (registered operations)
|
|
1792
|
+
// ============================================
|
|
1793
|
+
async listDatabaseOperations(appId, databaseId) {
|
|
1794
|
+
return this.get(`/app/${appId}/api/databases/${databaseId}/operations`);
|
|
1795
|
+
}
|
|
1796
|
+
/**
|
|
1797
|
+
* Execute a registered operation and, when the result is a query page,
|
|
1798
|
+
* normalize it to the CLI's one list envelope (issue #2440).
|
|
1799
|
+
*
|
|
1800
|
+
* The registered operation decides the response shape, so the normalization
|
|
1801
|
+
* is conditional — see {@link normalizeDatabaseOperationResult}. Everything
|
|
1802
|
+
* else (a `count`, an `aggregate`, a mutation, a `returnField: "all"`
|
|
1803
|
+
* pipeline) is returned exactly as the server sent it.
|
|
1804
|
+
*/
|
|
1805
|
+
async executeDatabaseOperation(appId, databaseId, operationName, data, token, options) {
|
|
1806
|
+
const path = `/app/${appId}/api/databases/${databaseId}/operations/${encodeURIComponent(operationName)}/execute`;
|
|
1807
|
+
const extraHeaders = {};
|
|
1808
|
+
if (options?.timing) {
|
|
1809
|
+
extraHeaders["X-Timing"] = "true";
|
|
1810
|
+
}
|
|
1811
|
+
if (token) {
|
|
1812
|
+
return normalizeDatabaseOperationResult(await this.requestWithToken(path, token, {
|
|
1813
|
+
method: "POST",
|
|
1814
|
+
body: JSON.stringify(data || {}),
|
|
1815
|
+
headers: extraHeaders,
|
|
1816
|
+
}));
|
|
1817
|
+
}
|
|
1818
|
+
return normalizeDatabaseOperationResult(await this.request(path, {
|
|
1819
|
+
method: "POST",
|
|
1820
|
+
body: JSON.stringify(data || {}),
|
|
1821
|
+
headers: extraHeaders,
|
|
1822
|
+
}));
|
|
1823
|
+
}
|
|
1824
|
+
/**
|
|
1825
|
+
* Execute a registered batch (bulk) database operation. Posts a chunk of
|
|
1826
|
+
* items to the canonical `operations/:name/batch` endpoint (the same one the
|
|
1827
|
+
* client library's `executeBatch` uses — NOT the deprecated `import-bulk`
|
|
1828
|
+
* alias). Used by `databases import-csv`.
|
|
1829
|
+
*
|
|
1830
|
+
* @returns `{ imported, failed }` — DO-level write outcome counts. Per-item
|
|
1831
|
+
* validation/access failures abort the whole chunk with a 4xx (thrown).
|
|
1832
|
+
*/
|
|
1833
|
+
async executeBatch(appId, databaseId, operationName, batch) {
|
|
1834
|
+
return this.post(`/app/${appId}/api/databases/${databaseId}/operations/${encodeURIComponent(operationName)}/batch`, { batch });
|
|
1835
|
+
}
|
|
1836
|
+
/**
|
|
1837
|
+
* Make a request using a specific JWT token instead of the CLI's credentials.
|
|
1838
|
+
* Used for executing operations as a different user.
|
|
1839
|
+
*/
|
|
1840
|
+
async requestWithToken(path, token, options = {}) {
|
|
1841
|
+
const credentials = await this.ensureAuthenticated();
|
|
1842
|
+
const url = `${credentials.serverUrl}${path}`;
|
|
1843
|
+
const headers = {
|
|
1844
|
+
Authorization: `Bearer ${token}`,
|
|
1845
|
+
"Content-Type": "application/json",
|
|
1846
|
+
...(options.headers || {}),
|
|
1847
|
+
};
|
|
1848
|
+
const response = await fetchWithTLS(url, { ...options, headers });
|
|
1849
|
+
const text = await response.text();
|
|
1850
|
+
if (!response.ok) {
|
|
1851
|
+
const parsed = parseErrorResponse(response, text, path);
|
|
1852
|
+
throw new ApiError(parsed.message, response.status, parsed.code, parsed.details);
|
|
1853
|
+
}
|
|
1854
|
+
return text ? JSON.parse(text) : null;
|
|
750
1855
|
}
|
|
751
1856
|
// ============================================
|
|
752
1857
|
// DATABASE INDEXES
|
|
@@ -760,11 +1865,160 @@ export class ApiClient {
|
|
|
760
1865
|
async dropDatabaseIndex(appId, databaseId, data) {
|
|
761
1866
|
return this.post(`/app/${appId}/api/databases/${databaseId}/records/index/drop`, data);
|
|
762
1867
|
}
|
|
1868
|
+
/**
|
|
1869
|
+
* Issue #974: back-provision an existing database instance's schema-declared
|
|
1870
|
+
* unique indexes by reading its type schema and registering any declared-but-
|
|
1871
|
+
* missing unique index. Idempotent.
|
|
1872
|
+
*/
|
|
1873
|
+
async reindexDatabaseFromSchema(appId, databaseId) {
|
|
1874
|
+
return this.post(`/app/${appId}/api/databases/${databaseId}/reindex`, {});
|
|
1875
|
+
}
|
|
1876
|
+
// ============================================
|
|
1877
|
+
// DATABASE TYPE CONFIGS
|
|
1878
|
+
// ============================================
|
|
1879
|
+
async listDatabaseTypeConfigs(appId) {
|
|
1880
|
+
return this.get(`/app/${appId}/api/databases/types`);
|
|
1881
|
+
}
|
|
1882
|
+
async getDatabaseTypeConfig(appId, databaseType) {
|
|
1883
|
+
return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}`);
|
|
1884
|
+
}
|
|
1885
|
+
async createDatabaseTypeConfig(appId, data,
|
|
1886
|
+
// Issue #1336 (codex round-1 P2): `dryRun` runs every create-time
|
|
1887
|
+
// validation (inline CEL rule lint, manifest shape, schema TOML parse)
|
|
1888
|
+
// WITHOUT persisting, so a fresh-type `config push --dry-run` surfaces a
|
|
1889
|
+
// malformed `defaultAccess`/manifest the same way the real POST would.
|
|
1890
|
+
options) {
|
|
1891
|
+
const query = options?.dryRun ? "?dryRun=true" : "";
|
|
1892
|
+
return this.post(`/app/${appId}/api/databases/types${query}`, data);
|
|
1893
|
+
}
|
|
1894
|
+
async updateDatabaseTypeConfig(appId, databaseType, data, expectedModifiedAt, options) {
|
|
1895
|
+
const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
|
|
1896
|
+
const qs = new URLSearchParams();
|
|
1897
|
+
if (options?.dryRun)
|
|
1898
|
+
qs.set("dryRun", "true");
|
|
1899
|
+
if (options?.acceptWarnings)
|
|
1900
|
+
qs.set("acceptWarnings", "true");
|
|
1901
|
+
const query = qs.toString();
|
|
1902
|
+
const path = `/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}${query ? `?${query}` : ""}`;
|
|
1903
|
+
return this.patch(path, body);
|
|
1904
|
+
}
|
|
1905
|
+
async deleteDatabaseTypeConfig(appId, databaseType, options) {
|
|
1906
|
+
const query = options?.force ? "?force=true" : "";
|
|
1907
|
+
return this.delete(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}${query}`);
|
|
1908
|
+
}
|
|
1909
|
+
/**
|
|
1910
|
+
* Issue #666 Phase 3: ask the server to scaffold a TOML schema from
|
|
1911
|
+
* existing ops + DO field introspection. Read-only — does NOT persist.
|
|
1912
|
+
*/
|
|
1913
|
+
async scaffoldDatabaseTypeSchema(appId, databaseType) {
|
|
1914
|
+
return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/schema:scaffold`, {});
|
|
1915
|
+
}
|
|
1916
|
+
// ============================================
|
|
1917
|
+
// DATABASE TYPE OPERATIONS
|
|
1918
|
+
// ============================================
|
|
1919
|
+
async listDatabaseTypeOperations(appId, databaseType) {
|
|
1920
|
+
return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations`);
|
|
1921
|
+
}
|
|
1922
|
+
async getDatabaseTypeOperation(appId, databaseType, name) {
|
|
1923
|
+
return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations/${encodeURIComponent(name)}`);
|
|
1924
|
+
}
|
|
1925
|
+
async createDatabaseTypeOperation(appId, databaseType, data,
|
|
1926
|
+
// Issue #813 (1A): when `dryRun` is set the server runs the op-edit gate
|
|
1927
|
+
// without persisting — used by `config push --dry-run` to surface gate
|
|
1928
|
+
// failures the real push would hit. `schemaOverride` lets the gate run
|
|
1929
|
+
// against the schema the SAME push is about to land (so an op that depends
|
|
1930
|
+
// on a new field isn't falsely rejected against the stale stored schema).
|
|
1931
|
+
//
|
|
1932
|
+
// Issue #915 (defect a, follow-up): for a FRESH db-type the parent config
|
|
1933
|
+
// does not exist on the server yet, so the server's "access required"
|
|
1934
|
+
// check has no `defaultAccess` to fall back on. Thread the TOML-declared
|
|
1935
|
+
// type-level `defaultAccess` so an op that omits `access` (intending to
|
|
1936
|
+
// inherit it after the type is created) isn't falsely rejected during the
|
|
1937
|
+
// dry-run. Server only trusts this on the fresh-type dry-run path.
|
|
1938
|
+
options) {
|
|
1939
|
+
const query = options?.dryRun ? "?dryRun=true" : "";
|
|
1940
|
+
let body = data;
|
|
1941
|
+
if (options?.dryRun && options.schemaOverride !== undefined) {
|
|
1942
|
+
body = { ...body, schemaOverride: options.schemaOverride };
|
|
1943
|
+
}
|
|
1944
|
+
if (options?.dryRun && options.defaultAccess !== undefined) {
|
|
1945
|
+
body = { ...body, defaultAccess: options.defaultAccess };
|
|
1946
|
+
}
|
|
1947
|
+
if (options?.dryRun && options.metadataManifestOverride !== undefined) {
|
|
1948
|
+
body = { ...body, metadataManifestOverride: options.metadataManifestOverride };
|
|
1949
|
+
}
|
|
1950
|
+
return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations${query}`, body);
|
|
1951
|
+
}
|
|
1952
|
+
async updateDatabaseTypeOperation(appId, databaseType, name, data, expectedModifiedAt,
|
|
1953
|
+
// Issue #813 (1A): dry-run runs the op-edit gate without persisting.
|
|
1954
|
+
// `schemaOverride` gates against the schema the same push is landing.
|
|
1955
|
+
// Issue #1336: `metadataManifestOverride` gates the op-access lint against
|
|
1956
|
+
// the manifest the same push is landing (see `createDatabaseTypeOperation`).
|
|
1957
|
+
options) {
|
|
1958
|
+
let body = expectedModifiedAt ? { ...data, expectedModifiedAt } : { ...data };
|
|
1959
|
+
if (options?.dryRun && options.schemaOverride !== undefined) {
|
|
1960
|
+
body = { ...body, schemaOverride: options.schemaOverride };
|
|
1961
|
+
}
|
|
1962
|
+
if (options?.dryRun && options.metadataManifestOverride !== undefined) {
|
|
1963
|
+
body = { ...body, metadataManifestOverride: options.metadataManifestOverride };
|
|
1964
|
+
}
|
|
1965
|
+
const query = options?.dryRun ? "?dryRun=true" : "";
|
|
1966
|
+
return this.patch(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations/${encodeURIComponent(name)}${query}`, body);
|
|
1967
|
+
}
|
|
1968
|
+
async deleteDatabaseTypeOperation(appId, databaseType, name) {
|
|
1969
|
+
return this.delete(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/operations/${encodeURIComponent(name)}`);
|
|
1970
|
+
}
|
|
1971
|
+
// ============================================
|
|
1972
|
+
// DATABASE TYPE SUBSCRIPTIONS (#740 / #803)
|
|
1973
|
+
// ============================================
|
|
1974
|
+
//
|
|
1975
|
+
// Transparent passthroughs against the server's type-scoped subscription
|
|
1976
|
+
// routes (GET/POST/PUT/DELETE `/databases/types/:type/subscriptions[/:key]`).
|
|
1977
|
+
// The wire format is authoritative in the controller
|
|
1978
|
+
// (`database-type-subscriptions-controller.ts`): create requires
|
|
1979
|
+
// `subscriptionKey`, `displayName`, `modelName`, `filter` (CEL), `access`
|
|
1980
|
+
// (CEL); optional `description`, `select` (string[]), `emit`
|
|
1981
|
+
// (enter/update/leave subset), `params` (object, ≤5 entries), `status`.
|
|
1982
|
+
//
|
|
1983
|
+
// The list endpoint wraps results in `{ items: [...] }` (unlike operations,
|
|
1984
|
+
// which returns a bare array). We unwrap here so callers get an array,
|
|
1985
|
+
// matching `listDatabaseTypeOperations`.
|
|
1986
|
+
async listDatabaseTypeSubscriptions(appId, databaseType) {
|
|
1987
|
+
const result = await this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions`);
|
|
1988
|
+
if (Array.isArray(result))
|
|
1989
|
+
return result;
|
|
1990
|
+
return Array.isArray(result?.items) ? result.items : [];
|
|
1991
|
+
}
|
|
1992
|
+
async getDatabaseTypeSubscription(appId, databaseType, subscriptionKey) {
|
|
1993
|
+
return this.get(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions/${encodeURIComponent(subscriptionKey)}`);
|
|
1994
|
+
}
|
|
1995
|
+
async createDatabaseTypeSubscription(appId, databaseType, data) {
|
|
1996
|
+
return this.post(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions`, data);
|
|
1997
|
+
}
|
|
1998
|
+
async updateDatabaseTypeSubscription(appId, databaseType, subscriptionKey, data,
|
|
1999
|
+
// #2731 B10 — optimistic concurrency, as the sibling operation update has
|
|
2000
|
+
// had. Omitted (by `--force`, and by every caller predating it) means the
|
|
2001
|
+
// write stays unconditional.
|
|
2002
|
+
expectedModifiedAt) {
|
|
2003
|
+
return this.put(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions/${encodeURIComponent(subscriptionKey)}`, expectedModifiedAt ? { ...data, expectedModifiedAt } : data);
|
|
2004
|
+
}
|
|
2005
|
+
async deleteDatabaseTypeSubscription(appId, databaseType, subscriptionKey) {
|
|
2006
|
+
return this.delete(`/app/${appId}/api/databases/types/${encodeURIComponent(databaseType)}/subscriptions/${encodeURIComponent(subscriptionKey)}`);
|
|
2007
|
+
}
|
|
763
2008
|
// ============================================
|
|
764
2009
|
// GROUPS
|
|
765
2010
|
// ============================================
|
|
766
2011
|
async listGroups(appId, params) {
|
|
767
|
-
|
|
2012
|
+
const result = await this.get(`/app/${appId}/api/groups`, params);
|
|
2013
|
+
// #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias
|
|
2014
|
+
// so the CLI keeps paginating against pre-#1316 servers.
|
|
2015
|
+
const nextCursor = result?.nextCursor ?? result?.cursor;
|
|
2016
|
+
return {
|
|
2017
|
+
items: result?.items ?? [],
|
|
2018
|
+
nextCursor,
|
|
2019
|
+
hasMore: result?.hasMore ?? nextCursor != null,
|
|
2020
|
+
cursor: nextCursor,
|
|
2021
|
+
};
|
|
768
2022
|
}
|
|
769
2023
|
async createGroup(appId, data) {
|
|
770
2024
|
return this.post(`/app/${appId}/api/groups`, data);
|
|
@@ -781,8 +2035,18 @@ export class ApiClient {
|
|
|
781
2035
|
// ============================================
|
|
782
2036
|
// GROUP MEMBERS
|
|
783
2037
|
// ============================================
|
|
784
|
-
async listGroupMembers(appId, groupType, groupId) {
|
|
785
|
-
|
|
2038
|
+
async listGroupMembers(appId, groupType, groupId, options) {
|
|
2039
|
+
const qs = options?.include ? `?include=${options.include}` : "";
|
|
2040
|
+
const result = await this.get(`/app/${appId}/api/groups/${groupType}/${groupId}/members${qs}`);
|
|
2041
|
+
// #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias
|
|
2042
|
+
// so the CLI keeps paginating against pre-#1316 servers.
|
|
2043
|
+
const nextCursor = result?.nextCursor ?? result?.cursor;
|
|
2044
|
+
return {
|
|
2045
|
+
items: result?.items ?? [],
|
|
2046
|
+
nextCursor,
|
|
2047
|
+
hasMore: result?.hasMore ?? nextCursor != null,
|
|
2048
|
+
cursor: nextCursor,
|
|
2049
|
+
};
|
|
786
2050
|
}
|
|
787
2051
|
async addGroupMember(appId, groupType, groupId, data) {
|
|
788
2052
|
return this.post(`/app/${appId}/api/groups/${groupType}/${groupId}/members`, data);
|
|
@@ -790,18 +2054,12 @@ export class ApiClient {
|
|
|
790
2054
|
async removeGroupMember(appId, groupType, groupId, userId) {
|
|
791
2055
|
return this.delete(`/app/${appId}/api/groups/${groupType}/${groupId}/members/${userId}`);
|
|
792
2056
|
}
|
|
793
|
-
async updateGroupMemberRole(appId, groupType, groupId, userId, data) {
|
|
794
|
-
return this.patch(`/app/${appId}/api/groups/${groupType}/${groupId}/members/${userId}`, data);
|
|
795
|
-
}
|
|
796
2057
|
// ============================================
|
|
797
2058
|
// GROUP RESOURCE LISTINGS
|
|
798
2059
|
// ============================================
|
|
799
2060
|
async listGroupDocuments(appId, groupType, groupId) {
|
|
800
2061
|
return this.get(`/app/${appId}/api/groups/${groupType}/${groupId}/documents`);
|
|
801
2062
|
}
|
|
802
|
-
async listGroupDatabases(appId, groupType, groupId) {
|
|
803
|
-
return this.get(`/app/${appId}/api/groups/${groupType}/${groupId}/databases`);
|
|
804
|
-
}
|
|
805
2063
|
// ============================================
|
|
806
2064
|
// USER MEMBERSHIPS
|
|
807
2065
|
// ============================================
|
|
@@ -821,6 +2079,156 @@ export class ApiClient {
|
|
|
821
2079
|
return this.delete(`/app/${appId}/api/documents/${documentId}/group-permissions/${groupType}/${groupId}`);
|
|
822
2080
|
}
|
|
823
2081
|
// ============================================
|
|
2082
|
+
// COLLECTIONS
|
|
2083
|
+
// ============================================
|
|
2084
|
+
async listCollections(appId, params) {
|
|
2085
|
+
const qs = new URLSearchParams();
|
|
2086
|
+
if (params?.limit)
|
|
2087
|
+
qs.set("limit", String(params.limit));
|
|
2088
|
+
if (params?.cursor)
|
|
2089
|
+
qs.set("cursor", params.cursor);
|
|
2090
|
+
const q = qs.toString();
|
|
2091
|
+
const result = await this.get(`/app/${appId}/api/collections${q ? `?${q}` : ""}`);
|
|
2092
|
+
return {
|
|
2093
|
+
items: result?.items ?? [],
|
|
2094
|
+
nextCursor: result?.nextCursor ?? result?.cursor ?? null,
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
2097
|
+
async listAllCollections(appId, params) {
|
|
2098
|
+
const qs = new URLSearchParams();
|
|
2099
|
+
if (params?.limit)
|
|
2100
|
+
qs.set("limit", String(params.limit));
|
|
2101
|
+
if (params?.cursor)
|
|
2102
|
+
qs.set("cursor", params.cursor);
|
|
2103
|
+
const q = qs.toString();
|
|
2104
|
+
const result = await this.get(`/app/${appId}/api/admin/collections${q ? `?${q}` : ""}`);
|
|
2105
|
+
return {
|
|
2106
|
+
items: result?.items ?? [],
|
|
2107
|
+
nextCursor: result?.nextCursor ?? result?.cursor ?? null,
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
async createCollection(appId, data) {
|
|
2111
|
+
return this.post(`/app/${appId}/api/collections`, data);
|
|
2112
|
+
}
|
|
2113
|
+
async getCollection(appId, collectionId) {
|
|
2114
|
+
return this.get(`/app/${appId}/api/collections/${collectionId}`);
|
|
2115
|
+
}
|
|
2116
|
+
async updateCollection(appId, collectionId, data) {
|
|
2117
|
+
return this.patch(`/app/${appId}/api/collections/${collectionId}`, data);
|
|
2118
|
+
}
|
|
2119
|
+
async deleteCollection(appId, collectionId) {
|
|
2120
|
+
return this.delete(`/app/${appId}/api/collections/${collectionId}`);
|
|
2121
|
+
}
|
|
2122
|
+
// ============================================
|
|
2123
|
+
// COLLECTION DOCUMENTS
|
|
2124
|
+
// ============================================
|
|
2125
|
+
async listCollectionDocuments(appId, collectionId, params) {
|
|
2126
|
+
const qs = new URLSearchParams();
|
|
2127
|
+
if (params?.limit)
|
|
2128
|
+
qs.set("limit", String(params.limit));
|
|
2129
|
+
if (params?.cursor)
|
|
2130
|
+
qs.set("cursor", params.cursor);
|
|
2131
|
+
const q = qs.toString();
|
|
2132
|
+
const result = await this.get(`/app/${appId}/api/collections/${collectionId}/documents${q ? `?${q}` : ""}`);
|
|
2133
|
+
return {
|
|
2134
|
+
items: result?.items ?? [],
|
|
2135
|
+
nextCursor: result?.nextCursor ?? result?.cursor ?? null,
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
async addCollectionDocument(appId, collectionId, data) {
|
|
2139
|
+
return this.post(`/app/${appId}/api/collections/${collectionId}/documents`, data);
|
|
2140
|
+
}
|
|
2141
|
+
async removeCollectionDocument(appId, collectionId, documentId) {
|
|
2142
|
+
return this.delete(`/app/${appId}/api/collections/${collectionId}/documents/${documentId}`);
|
|
2143
|
+
}
|
|
2144
|
+
async listCollectionsForDocument(appId, documentId, params) {
|
|
2145
|
+
const qs = new URLSearchParams();
|
|
2146
|
+
if (params?.limit)
|
|
2147
|
+
qs.set("limit", String(params.limit));
|
|
2148
|
+
if (params?.cursor)
|
|
2149
|
+
qs.set("cursor", params.cursor);
|
|
2150
|
+
const q = qs.toString();
|
|
2151
|
+
const result = await this.get(`/app/${appId}/api/documents/${documentId}/collections${q ? `?${q}` : ""}`);
|
|
2152
|
+
return {
|
|
2153
|
+
items: result?.items ?? [],
|
|
2154
|
+
nextCursor: result?.nextCursor ?? result?.cursor ?? null,
|
|
2155
|
+
};
|
|
2156
|
+
}
|
|
2157
|
+
// ============================================
|
|
2158
|
+
// COLLECTION ACCESS (GROUPS + MEMBERS)
|
|
2159
|
+
// ============================================
|
|
2160
|
+
async getCollectionAccess(appId, collectionId) {
|
|
2161
|
+
return this.get(`/app/${appId}/api/collections/${collectionId}/access`);
|
|
2162
|
+
}
|
|
2163
|
+
async grantCollectionGroupPermission(appId, collectionId, data) {
|
|
2164
|
+
return this.post(`/app/${appId}/api/collections/${collectionId}/group-permissions`, data);
|
|
2165
|
+
}
|
|
2166
|
+
async revokeCollectionGroupPermission(appId, collectionId, groupType, groupId) {
|
|
2167
|
+
return this.delete(`/app/${appId}/api/collections/${collectionId}/group-permissions/${groupType}/${groupId}`);
|
|
2168
|
+
}
|
|
2169
|
+
async addCollectionMember(appId, collectionId, data) {
|
|
2170
|
+
return this.post(`/app/${appId}/api/collections/${collectionId}/members`, data);
|
|
2171
|
+
}
|
|
2172
|
+
async removeCollectionMember(appId, collectionId, userId) {
|
|
2173
|
+
return this.delete(`/app/${appId}/api/collections/${collectionId}/members/${userId}`);
|
|
2174
|
+
}
|
|
2175
|
+
// ============================================
|
|
2176
|
+
// COLLECTION TYPE CONFIGS
|
|
2177
|
+
// ============================================
|
|
2178
|
+
async listCollectionTypeConfigs(appId) {
|
|
2179
|
+
return this.get(`/app/${appId}/api/collection-type-configs`);
|
|
2180
|
+
}
|
|
2181
|
+
async getCollectionTypeConfig(appId, collectionType) {
|
|
2182
|
+
return this.get(`/app/${appId}/api/collection-type-configs/${collectionType}`);
|
|
2183
|
+
}
|
|
2184
|
+
async createCollectionTypeConfig(appId, data) {
|
|
2185
|
+
return this.post(`/app/${appId}/api/collection-type-configs`, data);
|
|
2186
|
+
}
|
|
2187
|
+
async updateCollectionTypeConfig(appId, collectionType, data, expectedModifiedAt) {
|
|
2188
|
+
const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
|
|
2189
|
+
return this.patch(`/app/${appId}/api/collection-type-configs/${collectionType}`, body);
|
|
2190
|
+
}
|
|
2191
|
+
async deleteCollectionTypeConfig(appId, collectionType) {
|
|
2192
|
+
return this.delete(`/app/${appId}/api/collection-type-configs/${collectionType}`);
|
|
2193
|
+
}
|
|
2194
|
+
// ============================================
|
|
2195
|
+
// METADATA CATEGORY CONFIGS (issue #1304, P-B)
|
|
2196
|
+
// ============================================
|
|
2197
|
+
/**
|
|
2198
|
+
* List all metadata category configs for an app. The route returns
|
|
2199
|
+
* `{ configs: [...] }`; unwrap to a bare array to match the other
|
|
2200
|
+
* `list*Configs` helpers.
|
|
2201
|
+
*/
|
|
2202
|
+
async listMetadataCategoryConfigs(appId) {
|
|
2203
|
+
const res = await this.get(`/app/${appId}/api/metadata-categories`);
|
|
2204
|
+
return Array.isArray(res?.configs) ? res.configs : [];
|
|
2205
|
+
}
|
|
2206
|
+
async getMetadataCategoryConfig(appId, resourceType, category) {
|
|
2207
|
+
return this.get(`/app/${appId}/api/metadata-categories/${encodeURIComponent(resourceType)}/${encodeURIComponent(category)}`);
|
|
2208
|
+
}
|
|
2209
|
+
/**
|
|
2210
|
+
* Create or replace a metadata category config (idempotent upsert via the
|
|
2211
|
+
* path-addressed PUT route). `resourceType` / `category` identify the config;
|
|
2212
|
+
* the body carries `schema` / `readRule` / `writeRule` / `description`.
|
|
2213
|
+
*/
|
|
2214
|
+
async upsertMetadataCategoryConfig(appId, resourceType, category, data,
|
|
2215
|
+
/** Optimistic-concurrency token; see `upsertEmailTemplate` (#2731 B10). */
|
|
2216
|
+
expectedModifiedAt) {
|
|
2217
|
+
return this.put(`/app/${appId}/api/metadata-categories/${encodeURIComponent(resourceType)}/${encodeURIComponent(category)}`, expectedModifiedAt ? { ...data, expectedModifiedAt } : data);
|
|
2218
|
+
}
|
|
2219
|
+
/**
|
|
2220
|
+
* Delete a metadata category config (issue #1426, admin-gated route shipped
|
|
2221
|
+
* in #1364). Orphan semantics: the config row is hard-deleted; any stored
|
|
2222
|
+
* `ResourceMetadata` value rows for the category are left behind and become
|
|
2223
|
+
* unreachable (no query path from a category to its values). Delete the
|
|
2224
|
+
* values first if you need them gone — an orphaned value row can't be
|
|
2225
|
+
* removed through any surface once its config is gone. Returns
|
|
2226
|
+
* `{ resourceType, category, deleted }`; a missing config is a 404.
|
|
2227
|
+
*/
|
|
2228
|
+
async deleteMetadataCategoryConfig(appId, resourceType, category) {
|
|
2229
|
+
return this.delete(`/app/${appId}/api/metadata-categories/${encodeURIComponent(resourceType)}/${encodeURIComponent(category)}`);
|
|
2230
|
+
}
|
|
2231
|
+
// ============================================
|
|
824
2232
|
// ACCESS RULE SETS
|
|
825
2233
|
// ============================================
|
|
826
2234
|
async listRuleSets(appId, params) {
|
|
@@ -832,13 +2240,487 @@ export class ApiClient {
|
|
|
832
2240
|
async getRuleSet(appId, ruleSetId) {
|
|
833
2241
|
return this.get(`/app/${appId}/api/rule-sets/${ruleSetId}`);
|
|
834
2242
|
}
|
|
835
|
-
async updateRuleSet(appId, ruleSetId, data) {
|
|
836
|
-
|
|
2243
|
+
async updateRuleSet(appId, ruleSetId, data, expectedModifiedAt) {
|
|
2244
|
+
const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
|
|
2245
|
+
return this.patch(`/app/${appId}/api/rule-sets/${ruleSetId}`, body);
|
|
837
2246
|
}
|
|
838
2247
|
async deleteRuleSet(appId, ruleSetId) {
|
|
839
2248
|
return this.delete(`/app/${appId}/api/rule-sets/${ruleSetId}`);
|
|
840
2249
|
}
|
|
2250
|
+
async getRuleSetSchema(appId) {
|
|
2251
|
+
return this.get(`/app/${appId}/api/rule-sets/schema`);
|
|
2252
|
+
}
|
|
2253
|
+
async getRuleSetResourceTypes(appId) {
|
|
2254
|
+
return this.get(`/app/${appId}/api/rule-sets/resource-types`);
|
|
2255
|
+
}
|
|
2256
|
+
async testRuleSet(appId, ruleSetId, data) {
|
|
2257
|
+
return this.post(`/app/${appId}/api/rule-sets/${ruleSetId}/test`, data);
|
|
2258
|
+
}
|
|
2259
|
+
async debugRuleSet(appId, data) {
|
|
2260
|
+
return this.post(`/app/${appId}/api/rule-sets/debug`, data);
|
|
2261
|
+
}
|
|
2262
|
+
// ============================================
|
|
2263
|
+
// GROUP TYPE CONFIGS
|
|
2264
|
+
// ============================================
|
|
2265
|
+
async listGroupTypeConfigs(appId) {
|
|
2266
|
+
return this.get(`/app/${appId}/api/group-type-configs`);
|
|
2267
|
+
}
|
|
2268
|
+
async getGroupTypeConfig(appId, groupType) {
|
|
2269
|
+
return this.get(`/app/${appId}/api/group-type-configs/${groupType}`);
|
|
2270
|
+
}
|
|
2271
|
+
async createGroupTypeConfig(appId, data) {
|
|
2272
|
+
return this.post(`/app/${appId}/api/group-type-configs`, data);
|
|
2273
|
+
}
|
|
2274
|
+
async updateGroupTypeConfig(appId, groupType, data, expectedModifiedAt) {
|
|
2275
|
+
const body = expectedModifiedAt ? { ...data, expectedModifiedAt } : data;
|
|
2276
|
+
return this.patch(`/app/${appId}/api/group-type-configs/${groupType}`, body);
|
|
2277
|
+
}
|
|
2278
|
+
async deleteGroupTypeConfig(appId, groupType) {
|
|
2279
|
+
return this.delete(`/app/${appId}/api/group-type-configs/${groupType}`);
|
|
2280
|
+
}
|
|
2281
|
+
// ============================================
|
|
2282
|
+
// DOCUMENT EXPORT / IMPORT
|
|
2283
|
+
// ============================================
|
|
2284
|
+
async exportDocumentState(appId, documentId) {
|
|
2285
|
+
return this.get(`/app/${appId}/api/documents/${documentId}/export/state`);
|
|
2286
|
+
}
|
|
2287
|
+
/**
|
|
2288
|
+
* The export chain of a large document (#2816, behavior 20): the latest
|
|
2289
|
+
* completed snapshot, the sealed overlays after its epoch, and the open
|
|
2290
|
+
* epoch's overlay inline. Every other artifact arrives as an expiring
|
|
2291
|
+
* signature, read through {@link downloadDocumentArtifact}.
|
|
2292
|
+
*/
|
|
2293
|
+
async exportDocumentChain(appId, documentId) {
|
|
2294
|
+
return this.get(`/app/${appId}/api/documents/${documentId}/export/chain`);
|
|
2295
|
+
}
|
|
2296
|
+
/**
|
|
2297
|
+
* Read one granted artifact of a large document.
|
|
2298
|
+
*
|
|
2299
|
+
* The path comes from the chain and is origin-relative — the room is a
|
|
2300
|
+
* Durable Object and has no reliable idea which host the caller reached it
|
|
2301
|
+
* through — and the signature in it is the whole authorization, so no key is
|
|
2302
|
+
* ever composed here.
|
|
2303
|
+
*/
|
|
2304
|
+
async downloadDocumentArtifact(_appId, grantedPath) {
|
|
2305
|
+
const credentials = await this.ensureAuthenticated();
|
|
2306
|
+
const url = `${credentials.serverUrl}${grantedPath}`;
|
|
2307
|
+
const response = await fetchWithTLS(url);
|
|
2308
|
+
if (!response.ok) {
|
|
2309
|
+
throw new ApiError(`Failed to download document artifact: ${response.statusText}`, response.status);
|
|
2310
|
+
}
|
|
2311
|
+
return Buffer.from(await response.arrayBuffer());
|
|
2312
|
+
}
|
|
2313
|
+
async importDocumentState(appId, documentId, stateBase64) {
|
|
2314
|
+
return this.post(`/app/${appId}/api/documents/${documentId}/import/state`, { state: stateBase64 });
|
|
2315
|
+
}
|
|
2316
|
+
/**
|
|
2317
|
+
* Upload one artifact of a large document's import chain (#2816,
|
|
2318
|
+
* behavior 20).
|
|
2319
|
+
*
|
|
2320
|
+
* The bytes travel raw and unmodified: the manifest's digest is over the
|
|
2321
|
+
* STORED bytes, so re-encoding a chunk here would fail its own verification
|
|
2322
|
+
* on install. The query says which artifact it is — the server derives the
|
|
2323
|
+
* key from the document it belongs to, so nothing here composes one.
|
|
2324
|
+
*/
|
|
2325
|
+
async uploadDocumentChainArtifact(appId, documentId, query, body) {
|
|
2326
|
+
const credentials = await this.ensureAuthenticated();
|
|
2327
|
+
const search = new URLSearchParams();
|
|
2328
|
+
for (const [key, value] of Object.entries(query)) {
|
|
2329
|
+
search.set(key, String(value));
|
|
2330
|
+
}
|
|
2331
|
+
const url = `${credentials.serverUrl}/app/${appId}/api/documents/${documentId}` +
|
|
2332
|
+
`/import/chain/artifact?${search.toString()}`;
|
|
2333
|
+
const headers = {
|
|
2334
|
+
Authorization: `Bearer ${credentials.accessToken}`,
|
|
2335
|
+
"Content-Type": "application/octet-stream",
|
|
2336
|
+
};
|
|
2337
|
+
if (credentials.globalAdminAppId) {
|
|
2338
|
+
headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
|
|
2339
|
+
}
|
|
2340
|
+
const response = await fetchWithTLS(url, {
|
|
2341
|
+
method: "POST",
|
|
2342
|
+
headers,
|
|
2343
|
+
body: body,
|
|
2344
|
+
});
|
|
2345
|
+
const result = await response.json().catch(() => ({}));
|
|
2346
|
+
if (!response.ok) {
|
|
2347
|
+
throw new ApiError(result?.error ||
|
|
2348
|
+
`Failed to upload import artifact: ${response.statusText}`, response.status);
|
|
2349
|
+
}
|
|
2350
|
+
return result;
|
|
2351
|
+
}
|
|
2352
|
+
/** Start installing an uploaded chain (#2816, behavior 20). */
|
|
2353
|
+
async installDocumentChain(appId, documentId, plan) {
|
|
2354
|
+
return this.post(`/app/${appId}/api/documents/${documentId}/import/chain`, plan);
|
|
2355
|
+
}
|
|
2356
|
+
/** How far a document's chain install has got (#2816, behavior 20). */
|
|
2357
|
+
async getDocumentChainInstall(appId, documentId) {
|
|
2358
|
+
return this.get(`/app/${appId}/api/documents/${documentId}/import/chain`);
|
|
2359
|
+
}
|
|
2360
|
+
async getDocument(appId, documentId) {
|
|
2361
|
+
return this.get(`/app/${appId}/api/documents/${documentId}`);
|
|
2362
|
+
}
|
|
2363
|
+
/**
|
|
2364
|
+
* Delete a document and everything hanging off it (#2756). The server route
|
|
2365
|
+
* runs the same cascade the client SDK's `documents.delete()` triggers —
|
|
2366
|
+
* Yjs state and update history, blob rows and objects, aliases, user/group
|
|
2367
|
+
* permissions, invitations, collection memberships — and returns
|
|
2368
|
+
* `{ success, message }`, which callers pass through under `--json`.
|
|
2369
|
+
*/
|
|
2370
|
+
async deleteDocument(appId, documentId) {
|
|
2371
|
+
return this.delete(`/app/${appId}/api/documents/${documentId}`);
|
|
2372
|
+
}
|
|
2373
|
+
/**
|
|
2374
|
+
* Introspect a document's Yjs schema (model names, per-model fields and
|
|
2375
|
+
* indexes). Backs `documents records models` and `documents records
|
|
2376
|
+
* describe`. Reuses the existing `GET documents/:id/schema` endpoint.
|
|
2377
|
+
*/
|
|
2378
|
+
async getDocumentSchema(appId, documentId) {
|
|
2379
|
+
return this.get(`/app/${appId}/api/documents/${documentId}/schema`);
|
|
2380
|
+
}
|
|
2381
|
+
/**
|
|
2382
|
+
* Query records in a document model (issue #1964 Phase 3). Reuses the
|
|
2383
|
+
* server-side `GET documents/:id/records/:model` endpoint, which runs
|
|
2384
|
+
* through the caller's document permission (reader+) or the app-admin arm.
|
|
2385
|
+
* Returns the unified `{ items, hasMore, nextCursor? }` envelope. The cursor
|
|
2386
|
+
* is an opaque token — feed `nextCursor` back as `cursor` for the next page.
|
|
2387
|
+
* The server also dual-emits a deprecated `cursor` alias (#1316); the
|
|
2388
|
+
* normalizer drops it so CLI output is already on `nextCursor` when #1982
|
|
2389
|
+
* removes it.
|
|
2390
|
+
*/
|
|
2391
|
+
async queryDocumentRecords(appId, documentId, modelName, queryOptions) {
|
|
2392
|
+
const query = {};
|
|
2393
|
+
if (queryOptions?.filter)
|
|
2394
|
+
query.filter = JSON.stringify(queryOptions.filter);
|
|
2395
|
+
// Forward `limit` whenever the caller supplied one (including an invalid
|
|
2396
|
+
// 0/negative) so the server validates and returns a clean 400 rather than
|
|
2397
|
+
// silently applying the default.
|
|
2398
|
+
if (queryOptions?.limit !== undefined)
|
|
2399
|
+
query.limit = queryOptions.limit;
|
|
2400
|
+
if (queryOptions?.cursor)
|
|
2401
|
+
query.cursor = queryOptions.cursor;
|
|
2402
|
+
const raw = await this.get(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}`, query);
|
|
2403
|
+
return normalizeCliListEnvelope(raw);
|
|
2404
|
+
}
|
|
2405
|
+
/**
|
|
2406
|
+
* Count records in a document model (issue #1964 Phase 3). Returns `{ count }`.
|
|
2407
|
+
*/
|
|
2408
|
+
async countDocumentRecords(appId, documentId, modelName, queryOptions) {
|
|
2409
|
+
const query = {};
|
|
2410
|
+
if (queryOptions?.filter)
|
|
2411
|
+
query.filter = JSON.stringify(queryOptions.filter);
|
|
2412
|
+
return this.get(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}/count`, query);
|
|
2413
|
+
}
|
|
2414
|
+
/**
|
|
2415
|
+
* Aggregate records in a document model (issue #2437) — the documents twin
|
|
2416
|
+
* of {@link aggregateDatabaseRecords}. `POST documents/:id/records/:model/aggregate`
|
|
2417
|
+
* with `{ options: { operations, groupBy?, filter? } }`, answering `{ result }`
|
|
2418
|
+
* in the same shape as the database endpoint.
|
|
2419
|
+
*
|
|
2420
|
+
* `groupBy` takes plain field names only: StringSet facet grouping and
|
|
2421
|
+
* `{ field, contains }` membership grouping are database-only, which is why
|
|
2422
|
+
* this signature is narrower than the databases one.
|
|
2423
|
+
*/
|
|
2424
|
+
async aggregateDocumentRecords(appId, documentId, modelName, options) {
|
|
2425
|
+
return this.post(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}/aggregate`, { options });
|
|
2426
|
+
}
|
|
2427
|
+
/**
|
|
2428
|
+
* Fetch platform-vocabulary document statistics (issue #1964 Phase 3):
|
|
2429
|
+
* record/model/blob counts, approximate size, and last-modified timestamp.
|
|
2430
|
+
*/
|
|
2431
|
+
async getDocumentStats(appId, documentId) {
|
|
2432
|
+
return this.get(`/app/${appId}/api/documents/${documentId}/stats`);
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* Create or replace a record in a document model (issue #1964 Phase 4).
|
|
2436
|
+
* `POST documents/:id/records/:model` with `{ id, data, options? }` —
|
|
2437
|
+
* server-side write through the document facade (Yjs-first, CRDT-safe,
|
|
2438
|
+
* broadcasts to connected clients). Requires read-write+ on the document
|
|
2439
|
+
* or the app-admin arm. Returns `{ record }`.
|
|
2440
|
+
*/
|
|
2441
|
+
async saveDocumentRecord(appId, documentId, modelName, body) {
|
|
2442
|
+
return this.post(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}`, body);
|
|
2443
|
+
}
|
|
2444
|
+
/**
|
|
2445
|
+
* Merge fields into an existing record (issue #1964 Phase 4).
|
|
2446
|
+
* `PATCH documents/:id/records/:model/:recordId` with `{ data, options? }`.
|
|
2447
|
+
* Returns `{ record }`.
|
|
2448
|
+
*/
|
|
2449
|
+
async patchDocumentRecord(appId, documentId, modelName, recordId, body) {
|
|
2450
|
+
return this.patch(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}/${encodeURIComponent(recordId)}`, body);
|
|
2451
|
+
}
|
|
2452
|
+
/**
|
|
2453
|
+
* Delete a record from a document model (issue #1964 Phase 4).
|
|
2454
|
+
* `DELETE documents/:id/records/:model/:recordId`. A missing record id is
|
|
2455
|
+
* a silent no-op per the facade contract. Returns `{ deleted: true }`.
|
|
2456
|
+
*/
|
|
2457
|
+
async deleteDocumentRecord(appId, documentId, modelName, recordId) {
|
|
2458
|
+
return this.delete(`/app/${appId}/api/documents/${documentId}/records/${encodeURIComponent(modelName)}/${encodeURIComponent(recordId)}`);
|
|
2459
|
+
}
|
|
2460
|
+
/**
|
|
2461
|
+
* Apply an ordered multi-model create/patch/delete blob atomically (issue
|
|
2462
|
+
* #1964 Phase 4, wrapping the #1517 `bulkUpdate` facade). All-or-nothing:
|
|
2463
|
+
* a validation failure commits nothing. Returns the facade's
|
|
2464
|
+
* `{ applied, added, updated, deleted }`.
|
|
2465
|
+
*/
|
|
2466
|
+
async bulkDocumentRecords(appId, documentId, operations) {
|
|
2467
|
+
return this.post(`/app/${appId}/api/documents/${documentId}/records/bulk`, { operations });
|
|
2468
|
+
}
|
|
2469
|
+
async createDocument(appId, data) {
|
|
2470
|
+
return this.post(`/app/${appId}/api/documents`, data);
|
|
2471
|
+
}
|
|
2472
|
+
async listDocumentPermissions(appId, documentId) {
|
|
2473
|
+
return this.get(`/app/${appId}/api/documents/${documentId}/permissions`);
|
|
2474
|
+
}
|
|
2475
|
+
async grantDocumentPermission(appId, documentId, permissions) {
|
|
2476
|
+
return this.put(`/app/${appId}/api/documents/${documentId}/permissions`, { permissions });
|
|
2477
|
+
}
|
|
2478
|
+
/**
|
|
2479
|
+
* Revoke a user's permission on a document. Mirrors the published client's
|
|
2480
|
+
* `documents.removePermission` wire behavior (`src/client/api/documentsApi.ts`):
|
|
2481
|
+
* a userId targets the path route `DELETE .../permissions/:userId`, while an
|
|
2482
|
+
* email targets the query-param route `DELETE .../permissions?email=...`
|
|
2483
|
+
* (unified email removal, #452). This duplication of the client's wire shape
|
|
2484
|
+
* is deliberate and parity-tested (issue #1964 amendment 8).
|
|
2485
|
+
*/
|
|
2486
|
+
async revokeDocumentPermission(appId, documentId, target) {
|
|
2487
|
+
if (target.email) {
|
|
2488
|
+
const email = encodeURIComponent(target.email);
|
|
2489
|
+
return this.delete(`/app/${appId}/api/documents/${documentId}/permissions?email=${email}`);
|
|
2490
|
+
}
|
|
2491
|
+
if (!target.userId) {
|
|
2492
|
+
throw new Error("revokeDocumentPermission: userId or email is required");
|
|
2493
|
+
}
|
|
2494
|
+
return this.delete(`/app/${appId}/api/documents/${documentId}/permissions/${target.userId}`);
|
|
2495
|
+
}
|
|
2496
|
+
async getDocumentLinkAccess(appId, documentId) {
|
|
2497
|
+
return this.get(`/app/${appId}/api/documents/${documentId}/link-access`);
|
|
2498
|
+
}
|
|
2499
|
+
async setDocumentLinkAccess(appId, documentId, level) {
|
|
2500
|
+
return this.put(`/app/${appId}/api/documents/${documentId}/link-access`, { level });
|
|
2501
|
+
}
|
|
2502
|
+
async clearDocumentLinkAccess(appId, documentId) {
|
|
2503
|
+
return this.delete(`/app/${appId}/api/documents/${documentId}/link-access`);
|
|
2504
|
+
}
|
|
2505
|
+
/**
|
|
2506
|
+
* Pending deferred grants for a document (#2951 — replaces the removed
|
|
2507
|
+
* legacy `GET /documents/:id/invitations`). Returns the `{ items }`
|
|
2508
|
+
* envelope; rows are already filtered server-side to unresolved, unexpired
|
|
2509
|
+
* grants whose app invitation is still outstanding.
|
|
2510
|
+
*/
|
|
2511
|
+
async listDocumentPendingInvitations(appId, documentId) {
|
|
2512
|
+
return this.get(`/app/${appId}/api/documents/${documentId}/pending-invitations`);
|
|
2513
|
+
}
|
|
2514
|
+
async listDocumentBlobs(appId, documentId) {
|
|
2515
|
+
return this.get(`/app/${appId}/api/documents/${documentId}/blobs`);
|
|
2516
|
+
}
|
|
2517
|
+
async downloadBlob(appId, documentId, blobId) {
|
|
2518
|
+
const credentials = await this.ensureAuthenticated();
|
|
2519
|
+
const url = `${credentials.serverUrl}/app/${appId}/api/documents/${documentId}/blobs/${blobId}/download`;
|
|
2520
|
+
const headers = {
|
|
2521
|
+
Authorization: `Bearer ${credentials.accessToken}`,
|
|
2522
|
+
};
|
|
2523
|
+
if (credentials.globalAdminAppId) {
|
|
2524
|
+
headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
|
|
2525
|
+
}
|
|
2526
|
+
const response = await fetchWithTLS(url, { headers });
|
|
2527
|
+
if (!response.ok) {
|
|
2528
|
+
throw new ApiError(`Failed to download blob: ${response.statusText}`, response.status);
|
|
2529
|
+
}
|
|
2530
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
2531
|
+
return Buffer.from(arrayBuffer);
|
|
2532
|
+
}
|
|
2533
|
+
async uploadBlob(appId, documentId, blobId, data, meta) {
|
|
2534
|
+
const credentials = await this.ensureAuthenticated();
|
|
2535
|
+
const url = `${credentials.serverUrl}/app/${appId}/api/documents/${documentId}/blobs/${blobId}`;
|
|
2536
|
+
const headers = {
|
|
2537
|
+
Authorization: `Bearer ${credentials.accessToken}`,
|
|
2538
|
+
"Content-Type": meta.contentType,
|
|
2539
|
+
"X-Blob-Filename": encodeURIComponent(meta.filename),
|
|
2540
|
+
"X-Blob-Size": String(data.length),
|
|
2541
|
+
"X-Blob-Sha256": meta.sha256,
|
|
2542
|
+
};
|
|
2543
|
+
if (credentials.globalAdminAppId) {
|
|
2544
|
+
headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
|
|
2545
|
+
}
|
|
2546
|
+
const response = await fetchWithTLS(url, {
|
|
2547
|
+
method: "PUT",
|
|
2548
|
+
headers,
|
|
2549
|
+
body: data,
|
|
2550
|
+
});
|
|
2551
|
+
if (!response.ok) {
|
|
2552
|
+
const text = await response.text();
|
|
2553
|
+
throw new ApiError(`Failed to upload blob: ${text}`, response.status);
|
|
2554
|
+
}
|
|
2555
|
+
return response.json();
|
|
2556
|
+
}
|
|
2557
|
+
async listDocumentAliases(appId, documentId) {
|
|
2558
|
+
return this.get(`/app/${appId}/api/documents/${documentId}/aliases`);
|
|
2559
|
+
}
|
|
2560
|
+
async setDocumentAlias(appId, aliasScope, aliasKey, documentId, ownerUserId, mustNotExist) {
|
|
2561
|
+
return this.put(`/app/${appId}/api/document-aliases/${aliasScope}/${encodeURIComponent(aliasKey)}`, {
|
|
2562
|
+
documentId,
|
|
2563
|
+
userId: ownerUserId,
|
|
2564
|
+
mustNotExist,
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
2567
|
+
/**
|
|
2568
|
+
* Resolve an app user by exact email — the single email → userId lookup in
|
|
2569
|
+
* the CLI (issue #2763). It reads the member-accessible `users/lookup`
|
|
2570
|
+
* endpoint rather than the admin-only `GET /users` list, and returns the
|
|
2571
|
+
* server's `{ exists, user? }` envelope as-is: a 403 or a transport failure
|
|
2572
|
+
* rejects instead of being flattened into "no such user", which is exactly
|
|
2573
|
+
* how the deleted admin-list helper misreported permission problems.
|
|
2574
|
+
*/
|
|
2575
|
+
async lookupUserByEmail(appId, email) {
|
|
2576
|
+
return this.get(`/app/${appId}/api/users/lookup`, { email });
|
|
2577
|
+
}
|
|
2578
|
+
async listAdminDocuments(appId, userId) {
|
|
2579
|
+
const result = await this.get(`/admin/api/apps/${appId}/documents`, { userId });
|
|
2580
|
+
return result?.documents || result || [];
|
|
2581
|
+
}
|
|
2582
|
+
/**
|
|
2583
|
+
* The document `AppUser.rootDocId` points at, or null when the user has none
|
|
2584
|
+
* yet (#3135). A user who is not a member of the app is a 404, which
|
|
2585
|
+
* `documents import` reads as "this export has no target here" — distinct
|
|
2586
|
+
* from "this user has no root document yet".
|
|
2587
|
+
*/
|
|
2588
|
+
async getUserRootDocument(appId, userId) {
|
|
2589
|
+
return this.get(`/admin/api/apps/${appId}/users/${userId}/root-document`);
|
|
2590
|
+
}
|
|
2591
|
+
/**
|
|
2592
|
+
* Get or create the user's root document (#3135).
|
|
2593
|
+
*
|
|
2594
|
+
* The server runs the same idempotent get-or-create sign-in uses, so this
|
|
2595
|
+
* never mints a second root. `created` is true only when this call's claim
|
|
2596
|
+
* won — an import must not report "created", or merge without `--overwrite`,
|
|
2597
|
+
* for a root a concurrent sign-in minted first.
|
|
2598
|
+
*/
|
|
2599
|
+
async ensureUserRootDocument(appId, userId) {
|
|
2600
|
+
return this.post(`/admin/api/apps/${appId}/users/${userId}/root-document`, {});
|
|
2601
|
+
}
|
|
2602
|
+
// ============================================
|
|
2603
|
+
// DATABASE EXPORT / IMPORT
|
|
2604
|
+
// ============================================
|
|
2605
|
+
// `saveDatabaseRecord` lives with the other record verbs under
|
|
2606
|
+
// "DATABASE RECORDS & SCHEMA" above.
|
|
2607
|
+
/**
|
|
2608
|
+
* Apply an ordered batch of record operations via `records/batch`.
|
|
2609
|
+
*
|
|
2610
|
+
* `atomic` (default off, matching the DatabaseDO) makes the whole batch
|
|
2611
|
+
* all-or-nothing: the DO rolls every write back at the first failed
|
|
2612
|
+
* operation and answers with that operation's status. `databases records
|
|
2613
|
+
* bulk` sends `atomic: true` so it matches the documents twin's
|
|
2614
|
+
* all-or-nothing contract (#2437); the CSV/import paths keep the default
|
|
2615
|
+
* partial-success behavior.
|
|
2616
|
+
*/
|
|
2617
|
+
async batchDatabaseRecords(appId, databaseId, operations, options) {
|
|
2618
|
+
const body = { operations };
|
|
2619
|
+
if (options?.atomic)
|
|
2620
|
+
body.atomic = true;
|
|
2621
|
+
return this.post(`/app/${appId}/api/databases/${databaseId}/records/batch`, body);
|
|
2622
|
+
}
|
|
2623
|
+
async deleteDatabaseRecord(appId, databaseId, modelName, id) {
|
|
2624
|
+
return this.post(`/app/${appId}/api/databases/${databaseId}/records/delete`, { modelName, id });
|
|
2625
|
+
}
|
|
2626
|
+
async batchDeleteDatabaseRecords(appId, databaseId, operations) {
|
|
2627
|
+
return this.post(`/app/${appId}/api/databases/${databaseId}/records/batch`, { operations });
|
|
2628
|
+
}
|
|
2629
|
+
async listDatabaseUniqueConstraints(appId, databaseId) {
|
|
2630
|
+
const result = await this.get(`/app/${appId}/api/databases/${databaseId}/records/unique-constraints`);
|
|
2631
|
+
return result?.constraints || result || [];
|
|
2632
|
+
}
|
|
2633
|
+
async registerDatabaseUniqueConstraint(appId, databaseId, constraint) {
|
|
2634
|
+
return this.post(`/app/${appId}/api/databases/${databaseId}/records/unique-constraint/register`, constraint);
|
|
2635
|
+
}
|
|
2636
|
+
// ============================================
|
|
2637
|
+
// BLOB BUCKETS
|
|
2638
|
+
// ============================================
|
|
2639
|
+
async listBlobBuckets(appId) {
|
|
2640
|
+
return this.get(`/app/${appId}/api/blob-buckets`);
|
|
2641
|
+
}
|
|
2642
|
+
async createBlobBucket(appId, payload) {
|
|
2643
|
+
return this.post(`/app/${appId}/api/blob-buckets`, payload);
|
|
2644
|
+
}
|
|
2645
|
+
async getBlobBucket(appId, bucketIdOrKey) {
|
|
2646
|
+
return this.get(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}`);
|
|
2647
|
+
}
|
|
2648
|
+
async updateBlobBucket(appId, bucketIdOrKey, payload) {
|
|
2649
|
+
return this.patch(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}`, payload);
|
|
2650
|
+
}
|
|
2651
|
+
async deleteBlobBucket(appId, bucketIdOrKey) {
|
|
2652
|
+
return this.delete(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}`);
|
|
2653
|
+
}
|
|
2654
|
+
async listBucketBlobs(appId, bucketIdOrKey, params) {
|
|
2655
|
+
const result = await this.get(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs`, params);
|
|
2656
|
+
// #1316: prefer `nextCursor`; fall back to the deprecated `cursor` alias
|
|
2657
|
+
// so the CLI keeps paginating against pre-#1316 servers.
|
|
2658
|
+
const nextCursor = result?.nextCursor ?? result?.cursor;
|
|
2659
|
+
return {
|
|
2660
|
+
items: result?.items ?? [],
|
|
2661
|
+
nextCursor,
|
|
2662
|
+
hasMore: result?.hasMore ?? nextCursor != null,
|
|
2663
|
+
cursor: nextCursor,
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
async uploadBucketBlob(appId, bucketIdOrKey, data, meta) {
|
|
2667
|
+
const credentials = await this.ensureAuthenticated();
|
|
2668
|
+
const url = `${credentials.serverUrl}/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs`;
|
|
2669
|
+
const headers = {
|
|
2670
|
+
Authorization: `Bearer ${credentials.accessToken}`,
|
|
2671
|
+
"Content-Type": meta.contentType,
|
|
2672
|
+
"X-Blob-Filename": encodeURIComponent(meta.filename),
|
|
2673
|
+
};
|
|
2674
|
+
if (meta.tags && meta.tags.length > 0) {
|
|
2675
|
+
headers["X-Blob-Tags"] = JSON.stringify(meta.tags);
|
|
2676
|
+
}
|
|
2677
|
+
if (credentials.globalAdminAppId) {
|
|
2678
|
+
headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
|
|
2679
|
+
}
|
|
2680
|
+
const response = await fetchWithTLS(url, {
|
|
2681
|
+
method: "POST",
|
|
2682
|
+
headers,
|
|
2683
|
+
body: data,
|
|
2684
|
+
});
|
|
2685
|
+
if (!response.ok) {
|
|
2686
|
+
const text = await response.text();
|
|
2687
|
+
throw new ApiError(`Failed to upload blob: ${text}`, response.status);
|
|
2688
|
+
}
|
|
2689
|
+
return response.json();
|
|
2690
|
+
}
|
|
2691
|
+
async downloadBucketBlob(appId, bucketIdOrKey, blobId) {
|
|
2692
|
+
const credentials = await this.ensureAuthenticated();
|
|
2693
|
+
const url = `${credentials.serverUrl}/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs/${blobId}`;
|
|
2694
|
+
const headers = {
|
|
2695
|
+
Authorization: `Bearer ${credentials.accessToken}`,
|
|
2696
|
+
};
|
|
2697
|
+
if (credentials.globalAdminAppId) {
|
|
2698
|
+
headers["X-Global-Admin-App-Id"] = credentials.globalAdminAppId;
|
|
2699
|
+
}
|
|
2700
|
+
const response = await fetchWithTLS(url, { headers });
|
|
2701
|
+
if (!response.ok) {
|
|
2702
|
+
throw new ApiError(`Failed to download blob: ${response.statusText}`, response.status);
|
|
2703
|
+
}
|
|
2704
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
2705
|
+
return Buffer.from(arrayBuffer);
|
|
2706
|
+
}
|
|
2707
|
+
async deleteBucketBlob(appId, bucketIdOrKey, blobId) {
|
|
2708
|
+
return this.delete(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs/${blobId}`);
|
|
2709
|
+
}
|
|
2710
|
+
// Batch delete (#1455): one round-trip for N ids via POST .../blobs/delete.
|
|
2711
|
+
async deleteBucketBlobs(appId, bucketIdOrKey, blobIds) {
|
|
2712
|
+
return this.post(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs/delete`, { blobIds });
|
|
2713
|
+
}
|
|
2714
|
+
async getBucketBlobSignedUrl(appId, bucketIdOrKey, blobId, expiresInSeconds) {
|
|
2715
|
+
return this.post(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs/${blobId}/signed-url`, {
|
|
2716
|
+
expiresInSeconds: expiresInSeconds || 300,
|
|
2717
|
+
});
|
|
2718
|
+
}
|
|
2719
|
+
// Blob metadata (#1966): head a single blob without downloading its bytes.
|
|
2720
|
+
// Mirrors listBucketBlobs/downloadBucketBlob addressing; the server heads the
|
|
2721
|
+
// R2 object and returns the serialized BlobInfo shape.
|
|
2722
|
+
async getBucketBlobMetadata(appId, bucketIdOrKey, blobId) {
|
|
2723
|
+
return this.get(`/app/${appId}/api/blob-buckets/${encodeURIComponent(bucketIdOrKey)}/blobs/${blobId}/metadata`);
|
|
2724
|
+
}
|
|
841
2725
|
}
|
|
842
|
-
// Export a singleton instance
|
|
843
|
-
export const apiClient = new ApiClient();
|
|
844
2726
|
//# sourceMappingURL=api-client.js.map
|