primitive-admin 1.0.61 → 1.0.62
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 +125 -69
- package/assets/skill/skills/primitive-platform/SKILL.md +336 -37
- package/dist/bin/primitive.js +25 -14
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/analytics.js +36 -16
- package/dist/src/commands/analytics.js.map +1 -1
- package/dist/src/commands/apps.js +49 -188
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/blob-buckets.js +13 -153
- package/dist/src/commands/blob-buckets.js.map +1 -1
- package/dist/src/commands/collection-type-configs.js +14 -100
- package/dist/src/commands/collection-type-configs.js.map +1 -1
- package/dist/src/commands/collections.js +47 -8
- package/dist/src/commands/collections.js.map +1 -1
- package/dist/src/commands/config.d.ts +46 -0
- package/dist/src/commands/config.js +479 -0
- package/dist/src/commands/config.js.map +1 -0
- package/dist/src/commands/connections.js +12 -7
- package/dist/src/commands/connections.js.map +1 -1
- package/dist/src/commands/cron-triggers.js +84 -176
- package/dist/src/commands/cron-triggers.js.map +1 -1
- package/dist/src/commands/database-type-configs.d.ts +2 -0
- package/dist/src/commands/database-type-configs.js +171 -0
- package/dist/src/commands/database-type-configs.js.map +1 -0
- package/dist/src/commands/database-types.js +4 -4
- package/dist/src/commands/database-types.js.map +1 -1
- package/dist/src/commands/databases.d.ts +63 -0
- package/dist/src/commands/databases.js +649 -87
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.js +869 -12
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/email-templates.js +18 -125
- package/dist/src/commands/email-templates.js.map +1 -1
- package/dist/src/commands/env.d.ts +12 -1
- package/dist/src/commands/env.js +98 -25
- package/dist/src/commands/env.js.map +1 -1
- 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/group-type-configs.js +14 -109
- package/dist/src/commands/group-type-configs.js.map +1 -1
- package/dist/src/commands/init.d.ts +9 -1
- package/dist/src/commands/init.js +1056 -277
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/integrations.js +136 -421
- package/dist/src/commands/integrations.js.map +1 -1
- 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.js +33 -141
- package/dist/src/commands/metadata.js.map +1 -1
- package/dist/src/commands/prompts.js +206 -563
- package/dist/src/commands/prompts.js.map +1 -1
- package/dist/src/commands/rule-sets.d.ts +1 -1
- package/dist/src/commands/rule-sets.js +16 -132
- package/dist/src/commands/rule-sets.js.map +1 -1
- package/dist/src/commands/scripts.js +67 -234
- package/dist/src/commands/scripts.js.map +1 -1
- package/dist/src/commands/sessions.js +17 -5
- package/dist/src/commands/sessions.js.map +1 -1
- package/dist/src/commands/sync-app-settings.d.ts +113 -70
- package/dist/src/commands/sync-app-settings.js +193 -242
- package/dist/src/commands/sync-app-settings.js.map +1 -1
- package/dist/src/commands/sync.d.ts +1932 -49
- package/dist/src/commands/sync.js +9521 -1769
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/tokens.js +8 -2
- package/dist/src/commands/tokens.js.map +1 -1
- package/dist/src/commands/users.js +92 -1
- package/dist/src/commands/users.js.map +1 -1
- package/dist/src/commands/vars.js +44 -58
- package/dist/src/commands/vars.js.map +1 -1
- package/dist/src/commands/webhooks.js +318 -156
- package/dist/src/commands/webhooks.js.map +1 -1
- package/dist/src/commands/workflows.d.ts +49 -12
- package/dist/src/commands/workflows.js +652 -1086
- 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 +541 -61
- package/dist/src/lib/api-client.js +559 -102
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/app-settings-descriptor.d.ts +180 -25
- package/dist/src/lib/app-settings-descriptor.js +403 -82
- package/dist/src/lib/app-settings-descriptor.js.map +1 -1
- 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/cli-manifest.d.ts +8 -0
- package/dist/src/lib/cli-manifest.js +1 -0
- package/dist/src/lib/cli-manifest.js.map +1 -1
- package/dist/src/lib/codegen-shared/generatedFiles.d.ts +6 -2
- package/dist/src/lib/codegen-shared/generatedFiles.js +9 -2
- package/dist/src/lib/codegen-shared/generatedFiles.js.map +1 -1
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +6 -6
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +8 -8
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -1
- package/dist/src/lib/config-object-descriptor.d.ts +127 -0
- package/dist/src/lib/config-object-descriptor.js +658 -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 +130 -0
- package/dist/src/lib/config-surface.js +300 -0
- package/dist/src/lib/config-surface.js.map +1 -0
- package/dist/src/lib/credentials-store.d.ts +10 -0
- package/dist/src/lib/credentials-store.js +23 -0
- package/dist/src/lib/credentials-store.js.map +1 -1
- 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/dbTemplates.js +1 -1
- package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -1
- package/dist/src/lib/db-codegen/dbTypeIR.d.ts +1 -1
- package/dist/src/lib/env-resolver-core.d.ts +147 -0
- package/dist/src/lib/env-resolver-core.js +265 -0
- package/dist/src/lib/env-resolver-core.js.map +1 -0
- package/dist/src/lib/env-resolver.d.ts +28 -6
- package/dist/src/lib/env-resolver.js +44 -32
- package/dist/src/lib/env-resolver.js.map +1 -1
- package/dist/src/lib/generated-allowlist.js +12 -0
- package/dist/src/lib/generated-allowlist.js.map +1 -1
- package/dist/src/lib/generated-config-surfaces.d.ts +682 -0
- package/dist/src/lib/generated-config-surfaces.js +4058 -0
- package/dist/src/lib/generated-config-surfaces.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/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-config.d.ts +44 -5
- package/dist/src/lib/init-config.js +80 -7
- package/dist/src/lib/init-config.js.map +1 -1
- package/dist/src/lib/init-ios-links.d.ts +50 -0
- package/dist/src/lib/init-ios-links.js +153 -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 +33 -0
- package/dist/src/lib/init-xcode.js +114 -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/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/log-inspection.d.ts +568 -0
- package/dist/src/lib/log-inspection.js +639 -0
- package/dist/src/lib/log-inspection.js.map +1 -0
- package/dist/src/lib/migration-nag.d.ts +2 -2
- package/dist/src/lib/migration-nag.js +3 -3
- 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 +24 -1
- package/dist/src/lib/output.js +42 -1
- 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 +50 -0
- package/dist/src/lib/paginate.js +53 -0
- package/dist/src/lib/paginate.js.map +1 -1
- package/dist/src/lib/platform-owned.d.ts +29 -3
- package/dist/src/lib/platform-owned.js +42 -3
- package/dist/src/lib/platform-owned.js.map +1 -1
- package/dist/src/lib/project-config.js +13 -5
- package/dist/src/lib/project-config.js.map +1 -1
- package/dist/src/lib/query-operators.d.ts +1 -1
- package/dist/src/lib/query-operators.js +1 -1
- 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/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 +43 -14
- package/dist/src/lib/resolve-platform.js +74 -12
- package/dist/src/lib/resolve-platform.js.map +1 -1
- 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.js +6 -3
- package/dist/src/lib/skill-installer.js.map +1 -1
- package/dist/src/lib/snapshots.d.ts +2 -2
- package/dist/src/lib/snapshots.js +2 -2
- package/dist/src/lib/swift-codegen/dbGenerator.d.ts +15 -2
- package/dist/src/lib/swift-codegen/dbGenerator.js +250 -18
- package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -1
- package/dist/src/lib/swift-codegen/generator.d.ts +11 -2
- package/dist/src/lib/swift-codegen/generator.js +181 -7
- package/dist/src/lib/swift-codegen/generator.js.map +1 -1
- package/dist/src/lib/swift-codegen/siblingSymbols.d.ts +7 -3
- package/dist/src/lib/swift-codegen/siblingSymbols.js +4 -1
- package/dist/src/lib/swift-codegen/siblingSymbols.js.map +1 -1
- 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 +39 -0
- package/dist/src/lib/sync-paths.js +70 -2
- package/dist/src/lib/sync-paths.js.map +1 -1
- package/dist/src/lib/sync-resource-types.d.ts +138 -31
- package/dist/src/lib/sync-resource-types.js +241 -47
- package/dist/src/lib/sync-resource-types.js.map +1 -1
- 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 +62 -4
- package/dist/src/lib/template.js +147 -39
- package/dist/src/lib/template.js.map +1 -1
- 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 +15 -0
- package/dist/src/lib/test-case-variables.js +29 -0
- package/dist/src/lib/test-case-variables.js.map +1 -0
- package/dist/src/lib/toml-database-config.d.ts +4 -4
- package/dist/src/lib/toml-database-config.js +27 -34
- package/dist/src/lib/toml-database-config.js.map +1 -1
- package/dist/src/lib/toml-metadata-config.d.ts +9 -5
- package/dist/src/lib/toml-metadata-config.js +40 -45
- package/dist/src/lib/toml-metadata-config.js.map +1 -1
- package/dist/src/lib/toml-native-form.d.ts +1 -1
- package/dist/src/lib/toml-native-form.js +1 -1
- package/dist/src/lib/toml-params-validator.d.ts +1 -1
- package/dist/src/lib/toml-params-validator.js +1 -1
- 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 +25 -0
- package/dist/src/lib/version-check.js +77 -8
- package/dist/src/lib/version-check.js.map +1 -1
- 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/workflow-apply.d.ts +69 -45
- package/dist/src/lib/workflow-apply.js +99 -72
- package/dist/src/lib/workflow-apply.js.map +1 -1
- package/dist/src/lib/workflow-codegen/generator.js +4 -4
- package/dist/src/lib/workflow-codegen/generator.js.map +1 -1
- package/dist/src/lib/workflow-codegen/invokerIR.d.ts +11 -0
- package/dist/src/lib/workflow-codegen/invokerIR.js +5 -0
- package/dist/src/lib/workflow-codegen/invokerIR.js.map +1 -1
- 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-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 +31 -20
- package/dist/src/lib/workflow-payload.js +100 -110
- package/dist/src/lib/workflow-payload.js.map +1 -1
- package/dist/src/lib/workflow-toml-validator.d.ts +37 -16
- package/dist/src/lib/workflow-toml-validator.js +141 -16
- package/dist/src/lib/workflow-toml-validator.js.map +1 -1
- package/dist/src/types/index.d.ts +78 -22
- package/dist/src/validators.d.ts +7 -7
- package/dist/src/validators.js +6 -6
- package/package.json +11 -6
- package/dist/src/commands/settings.d.ts +0 -15
- package/dist/src/commands/settings.js +0 -102
- package/dist/src/commands/settings.js.map +0 -1
|
@@ -0,0 +1,4058 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED FILE — DO NOT EDIT BY HAND.
|
|
3
|
+
*
|
|
4
|
+
* Vendored from the canonical server modules under `src/config-surface/` — the
|
|
5
|
+
* ONE definition per configuration object (issue #2644). The server's
|
|
6
|
+
* create/update handlers and this copy read the same field surface, so the CLI's
|
|
7
|
+
* push payloads, pull serializers and TOML key validation cannot drift from what
|
|
8
|
+
* the server accepts.
|
|
9
|
+
*
|
|
10
|
+
* Regenerate with:
|
|
11
|
+
* node cli/scripts/gen-config-surfaces.mjs (runs automatically at CLI prebuild)
|
|
12
|
+
*
|
|
13
|
+
* A freshness guard (`gen-config-surfaces.mjs --check`, asserted by
|
|
14
|
+
* `cli/tests/unit/config-surface-drift-guard.test.ts`) fails if this committed
|
|
15
|
+
* copy does not match the source.
|
|
16
|
+
*/
|
|
17
|
+
// ── src/config-surface/helpers.ts ────────────────────────────────────────
|
|
18
|
+
/**
|
|
19
|
+
* Shared spellings used by every configuration-object definition (issue #2644).
|
|
20
|
+
*
|
|
21
|
+
* These live in one module for a mechanical reason as much as a stylistic one:
|
|
22
|
+
* `cli/scripts/gen-config-surfaces.mjs` concatenates the whole directory into a
|
|
23
|
+
* single vendored artifact, so a `const handler = …` declared per definition
|
|
24
|
+
* module would collide as a duplicate identifier the moment a second object was
|
|
25
|
+
* migrated.
|
|
26
|
+
*
|
|
27
|
+
* Pure: no models, no tenant context, no `env` (see `types.ts` §Purity).
|
|
28
|
+
*/
|
|
29
|
+
/** Both write modes — the common case, spelled once. */
|
|
30
|
+
export const BOTH = ["create", "update"];
|
|
31
|
+
/** Create only: an immutable key or a field the update handler ignores. */
|
|
32
|
+
export const CREATE_ONLY = ["create"];
|
|
33
|
+
/** Update only: a field the create handler assigns itself. */
|
|
34
|
+
export const UPDATE_ONLY = ["update"];
|
|
35
|
+
/**
|
|
36
|
+
* Name the handler that owns a field's validation, normalization and
|
|
37
|
+
* serialization, as `"<repo-relative module>#<export>"`. Classification only —
|
|
38
|
+
* the definition never dispatches through it (#1976 decision of record); the
|
|
39
|
+
* guard asserts the export exists so a renamed handler fails the CLI unit suite
|
|
40
|
+
* instead of leaving a dangling reference (#2644 behavior 2b).
|
|
41
|
+
*/
|
|
42
|
+
export function handledBy(modulePath, exportName) {
|
|
43
|
+
return { handler: `${modulePath}#${exportName}` };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The declared-access manifest's top-level TOML keys (#1304, #1364).
|
|
47
|
+
*
|
|
48
|
+
* Five objects carry the same three-key fragment beside their field table —
|
|
49
|
+
* workflows, database types, and the group / collection / metadata-category
|
|
50
|
+
* configs — parsed by the one `parseDeclaredAccessManifestToml`. Spelling it
|
|
51
|
+
* once here keeps `config push`'s document-level rejection from disagreeing with
|
|
52
|
+
* itself object by object.
|
|
53
|
+
*/
|
|
54
|
+
export const DECLARED_ACCESS_MANIFEST_KEYS = {
|
|
55
|
+
metadata: {
|
|
56
|
+
kind: "structural",
|
|
57
|
+
note: "#1304 declared-access manifest — `[metadata.self]` / `[metadata.paths.*]`. " +
|
|
58
|
+
"Parsed by parseDeclaredAccessManifestToml and stored as the owning " +
|
|
59
|
+
"object's `metadataManifest`.",
|
|
60
|
+
},
|
|
61
|
+
secrets: {
|
|
62
|
+
kind: "structural",
|
|
63
|
+
note: "The manifest's declared secret names (#1304). Same channel as [metadata].",
|
|
64
|
+
},
|
|
65
|
+
vars: {
|
|
66
|
+
kind: "structural",
|
|
67
|
+
note: "The manifest's declared var names (#1364), the non-secret twin of " +
|
|
68
|
+
"`secrets`. Same channel as [metadata].",
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
/** The field names this table accepts on the wire in `mode`, in declaration order. */
|
|
72
|
+
export function writableFieldNames(table, mode) {
|
|
73
|
+
return table.fields
|
|
74
|
+
.filter((f) => f.writableOn.includes(mode))
|
|
75
|
+
.map((f) => f.field);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The field names the server PERSISTS for this table in `mode`: the writable
|
|
79
|
+
* fields, plus the `deprecated` classifications — "still writable, superseded
|
|
80
|
+
* by another field" is what that kind means (`types.ts`), so a superseded key
|
|
81
|
+
* a handler still honors (`accessPolicy`, `passkeyRpId`) belongs in every
|
|
82
|
+
* accepted set, and a field that is NOT writable belongs in none of them.
|
|
83
|
+
*
|
|
84
|
+
* Stated once here because three consumers need the same answer: the generated
|
|
85
|
+
* request schemas, `PUT /settings`'s write allow-list, and the guards.
|
|
86
|
+
*/
|
|
87
|
+
export function acceptedWriteFieldNames(table, mode) {
|
|
88
|
+
const deprecated = Object.entries(table.notExposed)
|
|
89
|
+
.filter(([, classification]) => classification.kind === "deprecated")
|
|
90
|
+
.map(([field]) => field);
|
|
91
|
+
return [...writableFieldNames(table, mode), ...deprecated];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Split a request body into the keys this table accepts in `mode` and the keys
|
|
95
|
+
* it does not.
|
|
96
|
+
*
|
|
97
|
+
* `accepted` preserves the caller's values verbatim — including an explicit
|
|
98
|
+
* `null` or `false`, which are meaningful (clear / opt-out) and must not be
|
|
99
|
+
* coalesced away. Only keys the body actually carries appear, so a handler can
|
|
100
|
+
* keep using presence (`hasOwnProperty`) to distinguish "leave unset" from
|
|
101
|
+
* "set to null".
|
|
102
|
+
*
|
|
103
|
+
* `rejected` is every other key the body carried. Today it is informational;
|
|
104
|
+
* #2644 phase 5 turns it into a 400 through the generated request schemas.
|
|
105
|
+
*/
|
|
106
|
+
export function pickWritableFields(body, table, mode) {
|
|
107
|
+
const writable = new Set(writableFieldNames(table, mode));
|
|
108
|
+
const accepted = {};
|
|
109
|
+
const rejected = [];
|
|
110
|
+
if (!body || typeof body !== "object")
|
|
111
|
+
return { accepted, rejected };
|
|
112
|
+
for (const key of Object.keys(body)) {
|
|
113
|
+
if (writable.has(key)) {
|
|
114
|
+
accepted[key] = body[key];
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
rejected.push(key);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { accepted, rejected };
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The PASSTHROUGH half of the accepted body: the keys this table declares
|
|
124
|
+
* `validation: "passthrough"`, writable in `mode`, that the body actually
|
|
125
|
+
* carries — with their values verbatim.
|
|
126
|
+
*
|
|
127
|
+
* This is what makes criterion 1 true rather than aspirational. A handler that
|
|
128
|
+
* only picked its accepted set still had to name each field again when it built
|
|
129
|
+
* the row to persist, so a scalar field added to a definition alone was
|
|
130
|
+
* accepted on the wire and then dropped on the floor. Handlers spread this into
|
|
131
|
+
* the create/update payload FIRST, so a field with real behavior still lands
|
|
132
|
+
* through its named handler (whose assignment comes after and wins), and a
|
|
133
|
+
* plain scalar needs no handler edit at all.
|
|
134
|
+
*
|
|
135
|
+
* Values are passed through untouched — including an explicit `null` or
|
|
136
|
+
* `false`, which are meaningful (clear / opt-out). "Stored as-is" is exactly
|
|
137
|
+
* what the `passthrough` classification promises.
|
|
138
|
+
*/
|
|
139
|
+
export function passthroughFields(body, table, mode, options = {}) {
|
|
140
|
+
const out = {};
|
|
141
|
+
if (!body || typeof body !== "object")
|
|
142
|
+
return out;
|
|
143
|
+
const handled = new Set(options.handledHere ?? []);
|
|
144
|
+
for (const field of table.fields) {
|
|
145
|
+
if (field.validation !== "passthrough")
|
|
146
|
+
continue;
|
|
147
|
+
if (!field.writableOn.includes(mode))
|
|
148
|
+
continue;
|
|
149
|
+
if (handled.has(field.field))
|
|
150
|
+
continue;
|
|
151
|
+
if (!Object.prototype.hasOwnProperty.call(body, field.field))
|
|
152
|
+
continue;
|
|
153
|
+
out[field.field] = body[field.field];
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
/** `true` when the body carries `field` and this table accepts it in `mode`. */
|
|
158
|
+
export function hasWritableField(accepted, field) {
|
|
159
|
+
return Object.prototype.hasOwnProperty.call(accepted, field);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Whether a value counts as unset for an `emit: "whenSet"` field, so `config pull`
|
|
163
|
+
* omits the key instead of writing a noisy empty one (and `config diff` does not
|
|
164
|
+
* report a difference that is not there).
|
|
165
|
+
*
|
|
166
|
+
* Carried over verbatim from #1033's shipped app-settings descriptor
|
|
167
|
+
* (`cli/src/lib/app-settings-descriptor.ts`): an empty array and an empty
|
|
168
|
+
* object are unset, while `false` and `0` are meaningful values and never are.
|
|
169
|
+
*/
|
|
170
|
+
export function isEmptyForEmit(value, type) {
|
|
171
|
+
if (value === null || value === undefined)
|
|
172
|
+
return true;
|
|
173
|
+
if (type === "string")
|
|
174
|
+
return value === "";
|
|
175
|
+
if (type === "string[]")
|
|
176
|
+
return !Array.isArray(value) || value.length === 0;
|
|
177
|
+
if (type === "json") {
|
|
178
|
+
return (typeof value !== "object" ||
|
|
179
|
+
Array.isArray(value) ||
|
|
180
|
+
Object.keys(value).length === 0);
|
|
181
|
+
}
|
|
182
|
+
return false; // booleans (false) and numbers (0) are meaningful, never empty
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* A field name whose shape says it carries a credential. Such a field must be
|
|
186
|
+
* classified explicitly — exposed with a stated reference-only contract, or
|
|
187
|
+
* `notExposed: { kind: "secret" }` — because a mechanical generalization that
|
|
188
|
+
* merely omits it would read as "not decided" (#2254, #2256).
|
|
189
|
+
*/
|
|
190
|
+
export const SECRETISH_FIELD = /secret|token|password|credential/i;
|
|
191
|
+
/** Model fields the table neither exposes nor classifies. Non-empty is a failure. */
|
|
192
|
+
export function findUnclassifiedFields(table, modelFields) {
|
|
193
|
+
const exposed = new Set(table.fields.map((f) => f.field));
|
|
194
|
+
return modelFields
|
|
195
|
+
.filter((f) => !exposed.has(f) && !(f in table.notExposed))
|
|
196
|
+
.sort();
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Classifications that no longer correspond to a model field — i.e. a field
|
|
200
|
+
* removed from `models.yaml` while the definition still names it. Also a
|
|
201
|
+
* failure: a stale classification must not silently pass, or the coverage claim
|
|
202
|
+
* quietly stops meaning anything.
|
|
203
|
+
*/
|
|
204
|
+
export function findStaleClassifications(table, modelFields) {
|
|
205
|
+
const known = new Set(modelFields);
|
|
206
|
+
return [...table.fields.map((f) => f.field), ...Object.keys(table.notExposed)]
|
|
207
|
+
.filter((f) => !known.has(f))
|
|
208
|
+
.sort();
|
|
209
|
+
}
|
|
210
|
+
// ── Surface-level coverage ───────────────────────────────────────────────
|
|
211
|
+
//
|
|
212
|
+
// A table is not always the right unit. `app-settings` projects ONE model
|
|
213
|
+
// (`App`) over FOUR `app.toml` sections, so `googleClients` is exposed in
|
|
214
|
+
// `[auth]` while `[app]` has never heard of it — per-table coverage would
|
|
215
|
+
// report it unclassified. Coverage is therefore a statement about the OBJECT:
|
|
216
|
+
// every field of every model the object's tables name is exposed in one of
|
|
217
|
+
// them, or classified `notExposed` in one of them.
|
|
218
|
+
//
|
|
219
|
+
// Results are reported as `"<Model>.<field>"` so a multi-model surface (prompt
|
|
220
|
+
// carries `AppPrompt` and `AppPromptConfig`) says which model it means.
|
|
221
|
+
/** The models this surface's tables project, in declaration order, deduped. */
|
|
222
|
+
export function surfaceModels(surface) {
|
|
223
|
+
return [...new Set(surface.tables.map((t) => t.model))];
|
|
224
|
+
}
|
|
225
|
+
/** Model fields for `model`, or a loud failure — a silent `[]` is a vacuous guard. */
|
|
226
|
+
function requireModelFields(modelFields, model) {
|
|
227
|
+
const fields = modelFields[model];
|
|
228
|
+
if (!fields) {
|
|
229
|
+
throw new Error(`config-surface: no models.yaml field list for "${model}". ` +
|
|
230
|
+
"Run `node cli/scripts/gen-config-surfaces.mjs`.");
|
|
231
|
+
}
|
|
232
|
+
return fields;
|
|
233
|
+
}
|
|
234
|
+
/** Every field name the surface exposes for `model`, across all its tables. */
|
|
235
|
+
function exposedForModel(surface, model) {
|
|
236
|
+
return new Set(surface.tables
|
|
237
|
+
.filter((t) => t.model === model)
|
|
238
|
+
.flatMap((t) => t.fields.map((f) => f.field)));
|
|
239
|
+
}
|
|
240
|
+
/** Every field name the surface classifies `notExposed` for `model`. */
|
|
241
|
+
function notExposedForModel(surface, model) {
|
|
242
|
+
return new Set(surface.tables
|
|
243
|
+
.filter((t) => t.model === model)
|
|
244
|
+
.flatMap((t) => Object.keys(t.notExposed)));
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Model fields the whole surface neither exposes nor classifies. Non-empty is
|
|
248
|
+
* a failure: the CLI would silently ignore them and a pull → push cycle would
|
|
249
|
+
* clear them (#2644 criterion 2).
|
|
250
|
+
*/
|
|
251
|
+
export function findUnclassifiedSurfaceFields(surface, modelFields) {
|
|
252
|
+
const out = [];
|
|
253
|
+
for (const model of surfaceModels(surface)) {
|
|
254
|
+
const exposed = exposedForModel(surface, model);
|
|
255
|
+
const classified = notExposedForModel(surface, model);
|
|
256
|
+
for (const field of requireModelFields(modelFields, model)) {
|
|
257
|
+
if (!exposed.has(field) && !classified.has(field)) {
|
|
258
|
+
out.push(`${model}.${field}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return out.sort();
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Classifications naming a field the model no longer has — the other direction,
|
|
266
|
+
* and equally a failure: a stale entry quietly stops meaning anything.
|
|
267
|
+
*/
|
|
268
|
+
export function findStaleSurfaceClassifications(surface, modelFields) {
|
|
269
|
+
const out = [];
|
|
270
|
+
for (const model of surfaceModels(surface)) {
|
|
271
|
+
const known = new Set(requireModelFields(modelFields, model));
|
|
272
|
+
for (const name of [
|
|
273
|
+
...exposedForModel(surface, model),
|
|
274
|
+
...notExposedForModel(surface, model),
|
|
275
|
+
]) {
|
|
276
|
+
if (!known.has(name))
|
|
277
|
+
out.push(`${model}.${name}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return out.sort();
|
|
281
|
+
}
|
|
282
|
+
/** Fields the surface both exposes and classifies `notExposed`. Ambiguous. */
|
|
283
|
+
export function findDoubleClassifiedSurfaceFields(surface) {
|
|
284
|
+
const out = [];
|
|
285
|
+
for (const model of surfaceModels(surface)) {
|
|
286
|
+
const classified = notExposedForModel(surface, model);
|
|
287
|
+
for (const field of exposedForModel(surface, model)) {
|
|
288
|
+
if (classified.has(field))
|
|
289
|
+
out.push(`${model}.${field}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return out.sort();
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Secret-adjacent model fields the surface leaves undecided — neither exposed
|
|
296
|
+
* with a stated contract nor classified `notExposed` (#2254, #2256).
|
|
297
|
+
*/
|
|
298
|
+
export function findUnclassifiedSecretishSurfaceFields(surface, modelFields) {
|
|
299
|
+
return findUnclassifiedSurfaceFields(surface, modelFields).filter((name) => SECRETISH_FIELD.test(name.split(".")[1] ?? name));
|
|
300
|
+
}
|
|
301
|
+
/** Fields classified twice — exposed AND `notExposed`. Ambiguous, so a failure. */
|
|
302
|
+
export function findDoubleClassifiedFields(table) {
|
|
303
|
+
return table.fields
|
|
304
|
+
.map((f) => f.field)
|
|
305
|
+
.filter((f) => f in table.notExposed)
|
|
306
|
+
.sort();
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* `notExposed` / `tomlOnlyKeys` / `responseOnlyKeys` entries whose `note` is
|
|
310
|
+
* missing or blank. A reason is the whole point of the classification.
|
|
311
|
+
*/
|
|
312
|
+
export function findReasonlessClassifications(table) {
|
|
313
|
+
const bad = [];
|
|
314
|
+
const maps = [
|
|
315
|
+
["notExposed", table.notExposed],
|
|
316
|
+
["tomlOnlyKeys", table.tomlOnlyKeys],
|
|
317
|
+
["responseOnlyKeys", table.responseOnlyKeys],
|
|
318
|
+
];
|
|
319
|
+
for (const [label, map] of maps) {
|
|
320
|
+
for (const [field, entry] of Object.entries(map)) {
|
|
321
|
+
if (!entry || typeof entry.note !== "string" || !entry.note.trim()) {
|
|
322
|
+
bad.push(`${label}.${field}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return bad.sort();
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Secret-adjacent model fields with no explicit decision — neither an exposed
|
|
330
|
+
* entry nor a `notExposed` classification. A bare omission is the failure mode
|
|
331
|
+
* this catches.
|
|
332
|
+
*/
|
|
333
|
+
export function findUnclassifiedSecretishFields(table, modelFields) {
|
|
334
|
+
const exposed = new Set(table.fields.map((f) => f.field));
|
|
335
|
+
return modelFields
|
|
336
|
+
.filter((f) => SECRETISH_FIELD.test(f) && !exposed.has(f) && !(f in table.notExposed))
|
|
337
|
+
.sort();
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Every distinct handler reference the table names, as
|
|
341
|
+
* `"<repo-relative module>#<export>"`. The guard resolves each one and fails
|
|
342
|
+
* when the module or the export is missing (#2644 behavior 2b).
|
|
343
|
+
*/
|
|
344
|
+
export function handlerReferences(table) {
|
|
345
|
+
const refs = new Set();
|
|
346
|
+
for (const field of table.fields) {
|
|
347
|
+
if (field.validation !== "passthrough")
|
|
348
|
+
refs.add(field.validation.handler);
|
|
349
|
+
}
|
|
350
|
+
return [...refs].sort();
|
|
351
|
+
}
|
|
352
|
+
/** TOML keys this table accepts: the exposed fields plus the declared extras. */
|
|
353
|
+
export function acceptedTomlKeys(table) {
|
|
354
|
+
return new Set([
|
|
355
|
+
...table.fields.map((f) => f.tomlKey),
|
|
356
|
+
...Object.keys(table.tomlOnlyKeys),
|
|
357
|
+
]);
|
|
358
|
+
}
|
|
359
|
+
// ── Document-level acceptance ────────────────────────────────────────────
|
|
360
|
+
//
|
|
361
|
+
// `acceptedTomlKeys` answers "is this key accepted inside this table". These
|
|
362
|
+
// three answer the question one level up — "is this table accepted in this
|
|
363
|
+
// file, and in the right shape" — which is what makes push's rejection total
|
|
364
|
+
// (design gate, 2026-08-12). A typo'd header (`[integraton]`) or a miswritten
|
|
365
|
+
// array (`[configs]` for `[[configs]]`) used to parse to a root key nothing
|
|
366
|
+
// checked, so the file pushed as though the real table were empty: the
|
|
367
|
+
// TOML-owned fields inside it were sent as explicit nulls and CLEARED.
|
|
368
|
+
/** Top-level TOML keys this object's file accepts: its tables plus the extras. */
|
|
369
|
+
export function acceptedTomlDocumentKeys(surface) {
|
|
370
|
+
return new Set([
|
|
371
|
+
...surface.tables.map((t) => t.tomlPath[0]).filter((k) => !!k),
|
|
372
|
+
...Object.keys(surface.tomlDocumentKeys),
|
|
373
|
+
]);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* `tomlDocumentKeys` entries with no reason, and any that merely restate a
|
|
377
|
+
* field table. Both are failures: a reason is the whole point of the
|
|
378
|
+
* classification, and a duplicate would let a table's shape check be bypassed
|
|
379
|
+
* by declaring it twice.
|
|
380
|
+
*/
|
|
381
|
+
export function findBadDocumentKeyDeclarations(surface) {
|
|
382
|
+
const tableKeys = new Set(surface.tables.map((t) => t.tomlPath[0]));
|
|
383
|
+
const bad = [];
|
|
384
|
+
for (const [key, entry] of Object.entries(surface.tomlDocumentKeys)) {
|
|
385
|
+
if (!entry || typeof entry.note !== "string" || !entry.note.trim()) {
|
|
386
|
+
bad.push(`${key} (no note)`);
|
|
387
|
+
}
|
|
388
|
+
if (tableKeys.has(key))
|
|
389
|
+
bad.push(`${key} (already a field table)`);
|
|
390
|
+
}
|
|
391
|
+
return bad.sort();
|
|
392
|
+
}
|
|
393
|
+
/** Root keys of `tomlData` the surface does not declare. `config push` rejects these. */
|
|
394
|
+
export function findUnknownTomlDocumentKeys(surface, tomlData) {
|
|
395
|
+
if (!tomlData || typeof tomlData !== "object" || Array.isArray(tomlData)) {
|
|
396
|
+
return [];
|
|
397
|
+
}
|
|
398
|
+
const accepted = acceptedTomlDocumentKeys(surface);
|
|
399
|
+
return Object.keys(tomlData)
|
|
400
|
+
.filter((key) => !accepted.has(key))
|
|
401
|
+
.sort();
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Field tables present in the file in the wrong shape — a `repeated` table
|
|
405
|
+
* written as `[configs]` instead of `[[configs]]`, or the reverse.
|
|
406
|
+
*
|
|
407
|
+
* Reported separately from an unknown key because the remedy is different: the
|
|
408
|
+
* table is recognized, its ARRAY MARKERS are wrong, and the builder would
|
|
409
|
+
* otherwise read the misshapen value as "no entries".
|
|
410
|
+
*
|
|
411
|
+
* A repeated table has to be an array OF TABLES, not merely an array: TOML
|
|
412
|
+
* accepts `configs = ["default"]` at the document root, and an element that is
|
|
413
|
+
* not a table has no keys for the per-entry unknown-key check to look at, so it
|
|
414
|
+
* used to sail through preflight and then be applied as a defaulted entry.
|
|
415
|
+
*/
|
|
416
|
+
/** A parsed TOML table: an object, and neither `null` nor an array. */
|
|
417
|
+
function isTomlTable(value) {
|
|
418
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
419
|
+
}
|
|
420
|
+
export function findMisshapenTomlTables(surface, tomlData) {
|
|
421
|
+
if (!tomlData || typeof tomlData !== "object" || Array.isArray(tomlData)) {
|
|
422
|
+
return [];
|
|
423
|
+
}
|
|
424
|
+
const doc = tomlData;
|
|
425
|
+
const out = [];
|
|
426
|
+
for (const table of surface.tables) {
|
|
427
|
+
const key = table.tomlPath[0];
|
|
428
|
+
// Only the FIRST path segment is a document-level statement; a nested path
|
|
429
|
+
// is inside a table this loop has already accepted.
|
|
430
|
+
if (!key || table.tomlPath.length !== 1)
|
|
431
|
+
continue;
|
|
432
|
+
const value = doc[key];
|
|
433
|
+
if (value === undefined || value === null)
|
|
434
|
+
continue;
|
|
435
|
+
if (table.repeated) {
|
|
436
|
+
if (!Array.isArray(value) || !value.every(isTomlTable)) {
|
|
437
|
+
out.push({ key, expected: "array of tables" });
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
else if (Array.isArray(value) || typeof value !== "object") {
|
|
441
|
+
out.push({ key, expected: "table" });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return out.sort((a, b) => a.key.localeCompare(b.key));
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Server-response keys this table recognizes: every model field (exposed or
|
|
448
|
+
* not) plus the declared response-only keys. `config pull` warns about anything
|
|
449
|
+
* else instead of dropping it silently (#2644 criterion 6).
|
|
450
|
+
*/
|
|
451
|
+
export function recognizedResponseKeys(table, modelFields) {
|
|
452
|
+
return new Set([
|
|
453
|
+
...modelFields,
|
|
454
|
+
...table.fields.map((f) => f.field),
|
|
455
|
+
...Object.keys(table.notExposed),
|
|
456
|
+
...Object.keys(table.responseOnlyKeys),
|
|
457
|
+
]);
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Protocol keys every config UPDATE accepts, whichever object it is.
|
|
461
|
+
*
|
|
462
|
+
* `expectedModifiedAt` is the optimistic-concurrency token `config push` attaches
|
|
463
|
+
* to an update body when it has a baseline from the last pull; a handler that
|
|
464
|
+
* does not implement conflict detection ignores it. It is a property of the
|
|
465
|
+
* sync protocol rather than of any one object, which is why it is stated once
|
|
466
|
+
* here instead of in thirteen definitions.
|
|
467
|
+
*/
|
|
468
|
+
export const UPDATE_PROTOCOL_KEYS = ["expectedModifiedAt"];
|
|
469
|
+
/** The body keys this table accepts in `mode`, in a stable order. */
|
|
470
|
+
export function requestSchemaKeys(table, mode) {
|
|
471
|
+
// Writable fields plus the `deprecated` ones, which that kind defines as
|
|
472
|
+
// still writable. `server-owned` and `secret` are excluded by construction —
|
|
473
|
+
// and a legacy slot the handler never reads off the body is `server-owned`,
|
|
474
|
+
// not `deprecated`, so it cannot slip in here (#2644 second opinion,
|
|
475
|
+
// CFG-2644-006).
|
|
476
|
+
const keys = new Set(acceptedWriteFieldNames(table, mode));
|
|
477
|
+
if (mode === "update")
|
|
478
|
+
for (const key of UPDATE_PROTOCOL_KEYS)
|
|
479
|
+
keys.add(key);
|
|
480
|
+
for (const [field, classification] of Object.entries(table.notExposed)) {
|
|
481
|
+
if (classification.kind !== "structural")
|
|
482
|
+
continue;
|
|
483
|
+
// A structural key states which modes carry it in the BODY; omitted means
|
|
484
|
+
// both. An identity that travels in the URL path or comes from the file
|
|
485
|
+
// name declares `[]` and is admitted by no schema — being structural is a
|
|
486
|
+
// statement about the TOML surface, not a licence to accept the key on the
|
|
487
|
+
// wire and drop it (#2644 second opinion, CFG-2644-005).
|
|
488
|
+
if (classification.requestModes &&
|
|
489
|
+
!classification.requestModes.includes(mode)) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
keys.add(field);
|
|
493
|
+
}
|
|
494
|
+
for (const [key, entry] of Object.entries(table.requestOnlyKeys ?? {})) {
|
|
495
|
+
// A request-only key states which modes read it; omitted means both.
|
|
496
|
+
if (!entry.modes || entry.modes.includes(mode))
|
|
497
|
+
keys.add(key);
|
|
498
|
+
}
|
|
499
|
+
return [...keys].sort();
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* The request schema for one object and mode. Generated from the definition, so
|
|
503
|
+
* a schema permitting a key the definition does not is unrepresentable: there
|
|
504
|
+
* is no place to write one.
|
|
505
|
+
*/
|
|
506
|
+
export function configRequestSchema(table, mode) {
|
|
507
|
+
const properties = {};
|
|
508
|
+
for (const key of requestSchemaKeys(table, mode)) {
|
|
509
|
+
properties[key] = {};
|
|
510
|
+
}
|
|
511
|
+
const schema = {
|
|
512
|
+
type: "object",
|
|
513
|
+
properties,
|
|
514
|
+
additionalProperties: false,
|
|
515
|
+
};
|
|
516
|
+
rememberRetiredKeys(schema, RETIRED_CONFIG_KEYS[table.tomlPath[0]]);
|
|
517
|
+
return schema;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Retired-key guidance carried BESIDE a generated schema, not inside it
|
|
521
|
+
* (issue #2803, second opinion).
|
|
522
|
+
*
|
|
523
|
+
* A route that opts into runtime validation is validated before its handler
|
|
524
|
+
* runs, so a handler's targeted "`status` is server-owned, use
|
|
525
|
+
* `<noun> enable|disable`" refusal never fired for the app-API webhook and cron
|
|
526
|
+
* routes: `additionalProperties: false` answered first, with the generic
|
|
527
|
+
* "Request body validation failed". The guidance has to reach the validator.
|
|
528
|
+
*
|
|
529
|
+
* It travels in a side table rather than as a schema keyword because the schema
|
|
530
|
+
* object IS the OpenAPI `meta.request` and is held to a fail-closed keyword
|
|
531
|
+
* allowlist (`RUNTIME_SUPPORTED_SCHEMA_KEYWORDS`) — an invented keyword would
|
|
532
|
+
* be published in the spec and would fail that guard. Keying on the schema's
|
|
533
|
+
* identity also means the guidance cannot drift: it is attached by the same
|
|
534
|
+
* call that builds the key set, so a route cannot opt into one without the
|
|
535
|
+
* other.
|
|
536
|
+
*/
|
|
537
|
+
const retiredKeyGuidance = new WeakMap();
|
|
538
|
+
function rememberRetiredKeys(schema, retired) {
|
|
539
|
+
if (!retired)
|
|
540
|
+
return;
|
|
541
|
+
const entries = {};
|
|
542
|
+
for (const [key, entry] of Object.entries(retired)) {
|
|
543
|
+
// A key the schema ACCEPTS is not retired on this surface, whatever the
|
|
544
|
+
// TOML surface says about it. The accepted key set wins, so the guidance
|
|
545
|
+
// can never shadow a live field.
|
|
546
|
+
if (key in schema.properties)
|
|
547
|
+
continue;
|
|
548
|
+
entries[key] = entry.request;
|
|
549
|
+
}
|
|
550
|
+
if (Object.keys(entries).length > 0)
|
|
551
|
+
retiredKeyGuidance.set(schema, entries);
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* The retired-key guidance for a generated schema, keyed by body key — or
|
|
555
|
+
* `undefined` when the schema's object has none. Consumed by the app API's
|
|
556
|
+
* request validator (`src/app-api/request-validation.ts`).
|
|
557
|
+
*/
|
|
558
|
+
export function retiredRequestKeys(schema) {
|
|
559
|
+
return schema ? retiredKeyGuidance.get(schema) : undefined;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* One schema for an endpoint whose body spans MORE than one table.
|
|
563
|
+
*
|
|
564
|
+
* `POST …/prompts` is the case: it creates the prompt AND seeds its first
|
|
565
|
+
* config, so the body carries `[prompt]` fields and `[[configs]]` fields
|
|
566
|
+
* together. Both halves still come from their definitions — this only says the
|
|
567
|
+
* endpoint accepts the union, in the one place that is true.
|
|
568
|
+
*/
|
|
569
|
+
export function mergeRequestSchemas(...schemas) {
|
|
570
|
+
const properties = {};
|
|
571
|
+
for (const schema of schemas) {
|
|
572
|
+
for (const key of Object.keys(schema.properties))
|
|
573
|
+
properties[key] = {};
|
|
574
|
+
}
|
|
575
|
+
// The union's guidance is the union of its halves'. A key one table retired
|
|
576
|
+
// and another accepts is admitted by the merged key set, so `rememberRetiredKeys`
|
|
577
|
+
// drops it below.
|
|
578
|
+
const retired = {};
|
|
579
|
+
for (const schema of schemas) {
|
|
580
|
+
for (const [key, request] of Object.entries(retiredRequestKeys(schema) ?? {})) {
|
|
581
|
+
retired[key] = { request };
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
const merged = {
|
|
585
|
+
type: "object",
|
|
586
|
+
properties,
|
|
587
|
+
additionalProperties: false,
|
|
588
|
+
};
|
|
589
|
+
rememberRetiredKeys(merged, retired);
|
|
590
|
+
return merged;
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* The body keys `table` does not accept in `mode` — the 400's subject.
|
|
594
|
+
*
|
|
595
|
+
* Used by the admin API, which has no request-schema middleware: its handlers
|
|
596
|
+
* call this directly so both APIs reject the same key set for the same object.
|
|
597
|
+
*/
|
|
598
|
+
export function unknownRequestKeys(body, table, mode) {
|
|
599
|
+
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
600
|
+
return [];
|
|
601
|
+
const known = new Set(requestSchemaKeys(table, mode));
|
|
602
|
+
return Object.keys(body)
|
|
603
|
+
.filter((key) => !known.has(key))
|
|
604
|
+
.sort();
|
|
605
|
+
}
|
|
606
|
+
/** The 400 message naming the unknown key(s), shared by both APIs. */
|
|
607
|
+
export function unknownRequestKeysMessage(keys) {
|
|
608
|
+
return (`Unrecognized request body key${keys.length === 1 ? "" : "s"}: ` +
|
|
609
|
+
`${keys.join(", ")}. This endpoint rejects keys it does not accept rather ` +
|
|
610
|
+
"than ignoring them.");
|
|
611
|
+
}
|
|
612
|
+
export const RETIRED_CONFIG_KEYS = {
|
|
613
|
+
// #2891 — the app-settings auth surface was reshaped so that `app.toml` can
|
|
614
|
+
// state what the server holds. All five keys below stay DECLARED in
|
|
615
|
+
// `models.yaml` (their attributes are still in DynamoDB, and the passkey pair
|
|
616
|
+
// still feeds a runtime fallback), but none is writable or echoed any more,
|
|
617
|
+
// so "removed for app developers" is achieved entirely here.
|
|
618
|
+
//
|
|
619
|
+
// Keyed under the `app` prefix because that is the app-settings surface's
|
|
620
|
+
// identifier at both consumers — the merged `PUT /settings` request schema
|
|
621
|
+
// and the `app.toml` parser — even though four of the five were authored
|
|
622
|
+
// under `[auth]`.
|
|
623
|
+
app: {
|
|
624
|
+
googleClientId: {
|
|
625
|
+
toml: "`googleClientId` is no longer authored in TOML (#2891). Google " +
|
|
626
|
+
"registers a client per platform, so the id belongs to one client: " +
|
|
627
|
+
"move it to `[auth.google.clients.<type>].clientId` (`web`, `ios`, " +
|
|
628
|
+
"`android`, `desktop` or `chrome-extension`) and delete the line.",
|
|
629
|
+
request: "`googleClientId` is retired (#2891). Send the client id inside " +
|
|
630
|
+
"`googleClients` as `[auth.google.clients.<type>].clientId`.",
|
|
631
|
+
},
|
|
632
|
+
googleClientSecret: {
|
|
633
|
+
toml: "`googleClientSecret` is no longer authored in TOML (#2891). The " +
|
|
634
|
+
"secret belongs to one Google client — and Google issues none at all " +
|
|
635
|
+
"for `ios`, `android` or `chrome-extension` — so move it to " +
|
|
636
|
+
"`[auth.google.clients.<type>].clientSecret` (still a whole " +
|
|
637
|
+
"`{{secrets.KEY}}` reference) and delete the line.",
|
|
638
|
+
request: "`googleClientSecret` is retired (#2891). Send the reference inside " +
|
|
639
|
+
"`googleClients` as `[auth.google.clients.<type>].clientSecret`; the " +
|
|
640
|
+
"`web` and `desktop` types require one and the native types reject one.",
|
|
641
|
+
},
|
|
642
|
+
redirectUris: {
|
|
643
|
+
toml: "`redirectUris` is no longer authored in TOML (#2891). A redirect URI " +
|
|
644
|
+
"belongs to the thing that redirects, and one app-wide list served " +
|
|
645
|
+
"both flows: put Google callbacks in " +
|
|
646
|
+
"`[auth.google.clients.<type>].redirectUris` and the magic-link " +
|
|
647
|
+
"allow-list in `emailRedirectUris`, then delete the line. A stored " +
|
|
648
|
+
"list may legitimately have mixed the two — split it.",
|
|
649
|
+
request: "`redirectUris` is retired (#2891). Google callbacks go in " +
|
|
650
|
+
"`googleClients` as `[auth.google.clients.<type>].redirectUris`; the " +
|
|
651
|
+
"magic-link allow-list is `emailRedirectUris`. A stored list may " +
|
|
652
|
+
"legitimately have mixed the two — split it.",
|
|
653
|
+
},
|
|
654
|
+
passkeyRpId: {
|
|
655
|
+
toml: "`passkeyRpId` is no longer authored in TOML (#2891). Passkey " +
|
|
656
|
+
"relying-party config is authored one way only: `passkeyRpConfig`, " +
|
|
657
|
+
"written as `[auth.passkeys]` with an entry per RP ID. Delete the " +
|
|
658
|
+
"line and push again.",
|
|
659
|
+
request: "`passkeyRpId` is retired (#2891). Send the relying-party config as " +
|
|
660
|
+
"`passkeyRpConfig` (`[auth.passkeys]`), which carries the per-rpId " +
|
|
661
|
+
"names.",
|
|
662
|
+
},
|
|
663
|
+
passkeyRpName: {
|
|
664
|
+
toml: "`passkeyRpName` is no longer authored in TOML (#2891). It was the " +
|
|
665
|
+
"other half of the legacy single-rpId config, superseded by " +
|
|
666
|
+
"`passkeyRpConfig`: put the name under its RP ID in `[auth.passkeys]` " +
|
|
667
|
+
"and delete the line.",
|
|
668
|
+
request: "`passkeyRpName` is retired (#2891). Send the relying-party config as " +
|
|
669
|
+
"`passkeyRpConfig` (`[auth.passkeys]`), which carries the per-rpId " +
|
|
670
|
+
"names.",
|
|
671
|
+
},
|
|
672
|
+
// #2884 — email sign-in became ONE flow, so it has ONE setting. Both keys
|
|
673
|
+
// stay DECLARED in `models.yaml` and are mirrored on every write of
|
|
674
|
+
// `emailSignInEnabled`, but neither is authorable any more: a file that
|
|
675
|
+
// still carries one is asking for a method choice that no longer exists.
|
|
676
|
+
magicLinkEnabled: {
|
|
677
|
+
toml: "`magicLinkEnabled` is no longer authored in TOML (#2884). Email " +
|
|
678
|
+
"sign-in is one flow — one request sends one email carrying a code " +
|
|
679
|
+
"and, when a link can be issued, a link — so it has one setting: " +
|
|
680
|
+
"replace this line with `emailSignInEnabled` (`false` turns email " +
|
|
681
|
+
"sign-in off entirely).",
|
|
682
|
+
request: "`magicLinkEnabled` is retired (#2884). Email sign-in has one " +
|
|
683
|
+
"setting: send `emailSignInEnabled`.",
|
|
684
|
+
},
|
|
685
|
+
otpEnabled: {
|
|
686
|
+
toml: "`otpEnabled` is no longer authored in TOML (#2884). It was the " +
|
|
687
|
+
"other half of the retired pair: one email now carries both the code " +
|
|
688
|
+
"and the link, so replace both lines with `emailSignInEnabled`.",
|
|
689
|
+
request: "`otpEnabled` is retired (#2884). Email sign-in has one setting: " +
|
|
690
|
+
"send `emailSignInEnabled`.",
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
// #2645 — pausing moved off `state` onto its own field, so `state` stopped
|
|
694
|
+
// being author-writable rather than becoming a second way to say "paused".
|
|
695
|
+
// Every pre-#2645 pull wrote the line, so existing repos hit this on their
|
|
696
|
+
// first push after upgrading: the failure is deliberate (sponsor decision at
|
|
697
|
+
// the approval gate, 2026-08-15), because the line has to be removed, not
|
|
698
|
+
// silently ignored, or the file keeps claiming a pause state push does not
|
|
699
|
+
// apply.
|
|
700
|
+
//
|
|
701
|
+
// #2907 — each of these names TWO verbs, not one. An author writing
|
|
702
|
+
// `status = "archived"` was not asking about availability: they were asking
|
|
703
|
+
// to retire the object, which is the delete flow and is spelled
|
|
704
|
+
// `primitive <noun> archive`. Answering with `enable|disable` alone sent
|
|
705
|
+
// exactly that author away with verbs that cannot do what they asked.
|
|
706
|
+
cronTrigger: {
|
|
707
|
+
state: {
|
|
708
|
+
toml: "`state` is no longer authored in TOML (#2645), and #2803 replaced it " +
|
|
709
|
+
"with the server-owned `status`. Availability is " +
|
|
710
|
+
"`primitive cron-triggers enable|disable`; retiring the trigger is " +
|
|
711
|
+
"`primitive cron-triggers archive` (#2907). Delete the line and push " +
|
|
712
|
+
"again.",
|
|
713
|
+
request: "`state` is server-owned and superseded by `status` (#2645, #2803). " +
|
|
714
|
+
"Use `primitive cron-triggers enable|disable` to change availability, " +
|
|
715
|
+
"or `primitive cron-triggers archive` to retire the trigger (#2907).",
|
|
716
|
+
},
|
|
717
|
+
status: {
|
|
718
|
+
toml: "`status` is no longer authored in TOML (#2803). Availability is " +
|
|
719
|
+
"server-owned: use `primitive cron-triggers enable|disable`. " +
|
|
720
|
+
"Retiring the trigger is `primitive cron-triggers archive` (#2907) — " +
|
|
721
|
+
"the archived row keeps its key and its slot under the per-app cap. " +
|
|
722
|
+
"Delete the line and push again.",
|
|
723
|
+
request: "`status` is server-owned (#2803). Use " +
|
|
724
|
+
"`primitive cron-triggers enable|disable` to change availability, or " +
|
|
725
|
+
"`primitive cron-triggers archive` to retire the trigger (#2907).",
|
|
726
|
+
},
|
|
727
|
+
},
|
|
728
|
+
// #2803 — the OBJECT-level availability leaves TOML. The per-version
|
|
729
|
+
// `[[configs]] status` (`active | archived`) is a different question — which
|
|
730
|
+
// named config is retired — and stays authorable.
|
|
731
|
+
prompt: {
|
|
732
|
+
status: {
|
|
733
|
+
toml: "`[prompt].status` is no longer authored in TOML (#2803). " +
|
|
734
|
+
"Availability is server-owned: use `primitive prompts enable|disable`. " +
|
|
735
|
+
"Retiring the prompt is `primitive prompts archive` (#2907). " +
|
|
736
|
+
"Delete the line and push again — the per-config `[[configs]] status` " +
|
|
737
|
+
"is unaffected.",
|
|
738
|
+
request: "`status` is server-owned (#2803). Use " +
|
|
739
|
+
"`primitive prompts enable|disable` to change availability, or " +
|
|
740
|
+
"`primitive prompts archive` to retire the prompt (#2907).",
|
|
741
|
+
},
|
|
742
|
+
},
|
|
743
|
+
integration: {
|
|
744
|
+
status: {
|
|
745
|
+
toml: "`status` is no longer authored in TOML (#2803), and `draft` no longer " +
|
|
746
|
+
"exists. Availability is server-owned: use " +
|
|
747
|
+
"`primitive integrations enable|disable`. Retiring the integration is " +
|
|
748
|
+
"`primitive integrations archive` (#2907). Delete the line and push " +
|
|
749
|
+
"again. If this file describes an ARCHIVED integration, delete the " +
|
|
750
|
+
"file instead — pull no longer exports tombstones, and the next " +
|
|
751
|
+
"confirmed `config push --prune` hard-deletes the row.",
|
|
752
|
+
request: "`status` is server-owned (#2803). Use " +
|
|
753
|
+
"`primitive integrations enable|disable` to change availability, or " +
|
|
754
|
+
"`primitive integrations archive` to retire the integration (#2907).",
|
|
755
|
+
},
|
|
756
|
+
},
|
|
757
|
+
// #2803 — availability is one server-owned `status`. As a TOML key it made
|
|
758
|
+
// `config push` a second writer: pushing a file pulled before an operator ran
|
|
759
|
+
// `webhooks disable` put the endpoint back in service as a side effect. The
|
|
760
|
+
// tenant app-API create/update also stored the value UNVALIDATED, so any app
|
|
761
|
+
// admin could write any string into the field the receiver gates on.
|
|
762
|
+
webhook: {
|
|
763
|
+
status: {
|
|
764
|
+
toml: "`status` is no longer authored in TOML (#2803). Availability is " +
|
|
765
|
+
"server-owned: use `primitive webhooks enable|disable`. Retiring the " +
|
|
766
|
+
"webhook is `primitive webhooks archive` (#2907) — the archived row " +
|
|
767
|
+
"keeps its key and its slot under the per-app cap. Delete the " +
|
|
768
|
+
"line and push again. If this file describes an ARCHIVED webhook, " +
|
|
769
|
+
"delete the file instead — pull no longer exports tombstones, and the " +
|
|
770
|
+
"next confirmed `config push --prune` hard-deletes the row, freeing " +
|
|
771
|
+
"its slot and its key.",
|
|
772
|
+
request: "`status` is server-owned (#2803). Use " +
|
|
773
|
+
"`primitive webhooks enable|disable` to change availability, or " +
|
|
774
|
+
"`primitive webhooks archive` to retire the webhook (#2907).",
|
|
775
|
+
},
|
|
776
|
+
},
|
|
777
|
+
// #2803 — availability is one server-owned `status`. As a TOML key it made
|
|
778
|
+
// `config push` a second writer: pushing a file pulled before an operator ran
|
|
779
|
+
// `workflows disable` put the workflow back in service as a side effect.
|
|
780
|
+
workflow: {
|
|
781
|
+
status: {
|
|
782
|
+
toml: "`status` is no longer authored in TOML (#2803). Availability is " +
|
|
783
|
+
"server-owned: use `primitive workflows enable|disable`. Retiring the " +
|
|
784
|
+
"workflow is `primitive workflows archive` (#2907), which keeps its " +
|
|
785
|
+
"runs and revisions resolving. Delete the line and push again.",
|
|
786
|
+
request: "`status` is server-owned (#2803). Use " +
|
|
787
|
+
"`primitive workflows enable|disable` to change availability, or " +
|
|
788
|
+
"`primitive workflows archive` to retire the workflow (#2907).",
|
|
789
|
+
},
|
|
790
|
+
},
|
|
791
|
+
};
|
|
792
|
+
/** The retired-key entry for `key` under `prefix`, or null when it is simply unknown. */
|
|
793
|
+
export function retiredConfigKey(prefix, key) {
|
|
794
|
+
return RETIRED_CONFIG_KEYS[prefix]?.[key] ?? null;
|
|
795
|
+
}
|
|
796
|
+
// ── src/config-surface/workflow.ts ───────────────────────────────────────
|
|
797
|
+
/**
|
|
798
|
+
* The `workflow` configuration object's definition (issue #2644, phase 1).
|
|
799
|
+
*
|
|
800
|
+
* One place records the `[workflow]` field surface: which `WorkflowDefinition`
|
|
801
|
+
* fields the server accepts on create/update, which TOML key each carries,
|
|
802
|
+
* whether `config pull` always emits it, and — for every field with real
|
|
803
|
+
* behavior — the handler that owns that behavior. `createAppWorkflow` /
|
|
804
|
+
* `updateAppWorkflow` take their accepted set from here (`pickWritableFields`),
|
|
805
|
+
* the CLI's push builder and pull serializer read the vendored copy, and the
|
|
806
|
+
* coverage guard cross-checks the whole thing against `models.yaml`.
|
|
807
|
+
*
|
|
808
|
+
* Supersedes `cli/src/lib/workflow-field-descriptor.ts` (#2326) and
|
|
809
|
+
* `WORKFLOW_PAYLOAD_FIELDS_CREATE` / `_UPDATE`
|
|
810
|
+
* (`cli/src/lib/workflow-payload.ts`, #1177), both deleted with this change.
|
|
811
|
+
*/
|
|
812
|
+
const HANDLERS = "src/workflows/config/workflow-field-handlers.ts";
|
|
813
|
+
const handler = (name) => handledBy(HANDLERS, name);
|
|
814
|
+
/**
|
|
815
|
+
* The five per-app / per-user queue limits (#1177). All share one handler: a
|
|
816
|
+
* finite non-negative number, floored.
|
|
817
|
+
*/
|
|
818
|
+
const QUEUE_LIMITS = [
|
|
819
|
+
"perUserMaxRunning",
|
|
820
|
+
"perUserMaxQueued",
|
|
821
|
+
"perAppMaxRunning",
|
|
822
|
+
"perAppMaxQueued",
|
|
823
|
+
"queueTtlSeconds",
|
|
824
|
+
];
|
|
825
|
+
const queueLimitFields = QUEUE_LIMITS.map((field) => ({
|
|
826
|
+
field,
|
|
827
|
+
tomlKey: field,
|
|
828
|
+
type: "number",
|
|
829
|
+
emit: "whenSet",
|
|
830
|
+
writableOn: BOTH,
|
|
831
|
+
validation: handler("normalizeWorkflowQueueLimit"),
|
|
832
|
+
}));
|
|
833
|
+
export const WORKFLOW_SURFACE = {
|
|
834
|
+
label: "workflow",
|
|
835
|
+
tables: [
|
|
836
|
+
{
|
|
837
|
+
tomlPath: ["workflow"],
|
|
838
|
+
repeated: false,
|
|
839
|
+
model: "WorkflowDefinition",
|
|
840
|
+
// Declaration order is the order `config pull` writes the keys, so it
|
|
841
|
+
// deliberately matches the pre-#2644 hand-written serializer — migrating
|
|
842
|
+
// to the definition must not reshuffle every workflow TOML file.
|
|
843
|
+
fields: [
|
|
844
|
+
{
|
|
845
|
+
field: "name",
|
|
846
|
+
tomlKey: "name",
|
|
847
|
+
type: "string",
|
|
848
|
+
emit: "always",
|
|
849
|
+
writableOn: BOTH,
|
|
850
|
+
validation: handler("normalizeWorkflowName"),
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
field: "description",
|
|
854
|
+
tomlKey: "description",
|
|
855
|
+
type: "string",
|
|
856
|
+
emit: "always",
|
|
857
|
+
writableOn: BOTH,
|
|
858
|
+
validation: handler("normalizeWorkflowDescription"),
|
|
859
|
+
},
|
|
860
|
+
{
|
|
861
|
+
field: "accessRule",
|
|
862
|
+
tomlKey: "accessRule",
|
|
863
|
+
type: "string",
|
|
864
|
+
emit: "whenSet",
|
|
865
|
+
writableOn: BOTH,
|
|
866
|
+
validation: handler("normalizeWorkflowAccessRule"),
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
field: "runAs",
|
|
870
|
+
tomlKey: "runAs",
|
|
871
|
+
type: "string",
|
|
872
|
+
emit: "whenSet",
|
|
873
|
+
writableOn: BOTH,
|
|
874
|
+
validation: handler("normalizeWorkflowRunAs"),
|
|
875
|
+
},
|
|
876
|
+
// #1078 Tier 1b — StringSetField on the model, string array in TOML/JSON.
|
|
877
|
+
{
|
|
878
|
+
field: "capabilities",
|
|
879
|
+
tomlKey: "capabilities",
|
|
880
|
+
type: "string[]",
|
|
881
|
+
emit: "whenSet",
|
|
882
|
+
writableOn: BOTH,
|
|
883
|
+
validation: handler("normalizeWorkflowCapabilities"),
|
|
884
|
+
},
|
|
885
|
+
...queueLimitFields,
|
|
886
|
+
{
|
|
887
|
+
field: "dequeueOrder",
|
|
888
|
+
tomlKey: "dequeueOrder",
|
|
889
|
+
type: "string",
|
|
890
|
+
emit: "always",
|
|
891
|
+
writableOn: BOTH,
|
|
892
|
+
validation: handler("normalizeWorkflowDequeueOrder"),
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
field: "requiresClientApply",
|
|
896
|
+
tomlKey: "requiresClientApply",
|
|
897
|
+
type: "boolean",
|
|
898
|
+
emit: "always",
|
|
899
|
+
writableOn: BOTH,
|
|
900
|
+
validation: handler("normalizeWorkflowOptOutFlag"),
|
|
901
|
+
},
|
|
902
|
+
// Accepted on BOTH modes — #807's deferred second PATCH is an update
|
|
903
|
+
// call. The CLI's create-only send is a sequencing invariant owned by
|
|
904
|
+
// `applyWorkflowBody`, not a statement about what the server accepts.
|
|
905
|
+
{
|
|
906
|
+
field: "syncCallable",
|
|
907
|
+
tomlKey: "syncCallable",
|
|
908
|
+
type: "boolean",
|
|
909
|
+
emit: "always",
|
|
910
|
+
writableOn: BOTH,
|
|
911
|
+
validation: handler("normalizeWorkflowSyncCallable"),
|
|
912
|
+
},
|
|
913
|
+
// JSON Schema objects. TOML may present them as an inline table OR as a
|
|
914
|
+
// JSON string; the handler owns that parse.
|
|
915
|
+
{
|
|
916
|
+
field: "inputSchema",
|
|
917
|
+
tomlKey: "inputSchema",
|
|
918
|
+
type: "json",
|
|
919
|
+
emit: "whenSet",
|
|
920
|
+
writableOn: BOTH,
|
|
921
|
+
validation: handler("normalizeWorkflowSchemaValue"),
|
|
922
|
+
},
|
|
923
|
+
{
|
|
924
|
+
field: "outputSchema",
|
|
925
|
+
tomlKey: "outputSchema",
|
|
926
|
+
type: "json",
|
|
927
|
+
emit: "whenSet",
|
|
928
|
+
writableOn: BOTH,
|
|
929
|
+
validation: handler("normalizeWorkflowSchemaValue"),
|
|
930
|
+
},
|
|
931
|
+
// #1518/#1972 — declarative run-scoped lock. `[workflow.lock]` inline
|
|
932
|
+
// table on the TOML side, JSON on the wire.
|
|
933
|
+
{
|
|
934
|
+
field: "lock",
|
|
935
|
+
tomlKey: "lock",
|
|
936
|
+
type: "json",
|
|
937
|
+
emit: "whenSet",
|
|
938
|
+
writableOn: BOTH,
|
|
939
|
+
validation: handler("normalizeWorkflowLockValue"),
|
|
940
|
+
},
|
|
941
|
+
],
|
|
942
|
+
notExposed: {
|
|
943
|
+
workflowId: {
|
|
944
|
+
kind: "server-owned",
|
|
945
|
+
note: "ULID assigned on create; never author-authored.",
|
|
946
|
+
},
|
|
947
|
+
// #2803 — `status` is the ONE availability control and it is
|
|
948
|
+
// server-owned. It used to be a `[workflow]` key, which made `config
|
|
949
|
+
// push` a second writer: pushing a file pulled before an operator ran
|
|
950
|
+
// `workflows disable` put the workflow back in service as a side
|
|
951
|
+
// effect. `server-owned` keeps it out of both the TOML surface and the
|
|
952
|
+
// create/update request schemas, so the only writers are
|
|
953
|
+
// `workflows enable|disable` and create (which assigns `active`).
|
|
954
|
+
status: {
|
|
955
|
+
kind: "server-owned",
|
|
956
|
+
note: "Availability (#2803, #2887): active | inactive | archived. " +
|
|
957
|
+
"Written only by `workflows enable|disable`, the delete flow (a " +
|
|
958
|
+
"plain DELETE writes the `archived` tombstone; `?hard=true` " +
|
|
959
|
+
"destroys), and create, which always assigns `active`. Never " +
|
|
960
|
+
"authored in TOML: `config pull` does not emit it and a file that " +
|
|
961
|
+
"still carries the key fails push (RETIRED_TOML_KEYS).",
|
|
962
|
+
},
|
|
963
|
+
// #2645 criterion 9 — the operational toggle. Configuration is
|
|
964
|
+
// authored in TOML and applied with `config push`, so anything this
|
|
965
|
+
// table exposes has the repo as its single writer. Taking a workflow
|
|
966
|
+
// out of service is an operation with its own single writer
|
|
967
|
+
// (`workflows disable|enable`), so this field must never appear in
|
|
968
|
+
// the `[workflow]` table: `server-owned` keeps it out of both request
|
|
969
|
+
// schemas and the TOML surface, and the coverage guard fails if it is
|
|
970
|
+
// ever moved into `fields`.
|
|
971
|
+
operationallyDisabled: {
|
|
972
|
+
kind: "server-owned",
|
|
973
|
+
note: "Operational toggle (#2645). Written only by the disable/enable " +
|
|
974
|
+
"endpoints (`workflows disable|enable`); read by the dispatch " +
|
|
975
|
+
"path. Never authored in TOML and never accepted on the config " +
|
|
976
|
+
"create/update wire.",
|
|
977
|
+
},
|
|
978
|
+
appId: {
|
|
979
|
+
kind: "server-owned",
|
|
980
|
+
note: "Tenant scope, taken from the route; never on the wire.",
|
|
981
|
+
},
|
|
982
|
+
workflowKey: {
|
|
983
|
+
kind: "structural",
|
|
984
|
+
// Create carries it in the body; update does not read it at all —
|
|
985
|
+
// the URL names the workflow and the CLI checks a contradicting
|
|
986
|
+
// TOML key locally (`workflowKeyMismatchError`).
|
|
987
|
+
requestModes: ["create"],
|
|
988
|
+
note: "The workflow's identity. Comes from the TOML file name / --key and " +
|
|
989
|
+
"is attached by the caller, not spread from the [workflow] table. Its " +
|
|
990
|
+
"TOML spelling is declared in tomlOnlyKeys as `key`.",
|
|
991
|
+
},
|
|
992
|
+
activeConfigId: {
|
|
993
|
+
kind: "server-owned",
|
|
994
|
+
note: "Pointer to the active WorkflowConfiguration, computed on publish.",
|
|
995
|
+
},
|
|
996
|
+
metadataManifest: {
|
|
997
|
+
kind: "structural",
|
|
998
|
+
note: "#1304 declared-access manifest. Travels as the top-level [metadata] / " +
|
|
999
|
+
"secrets tables and is validated by validateManifestShape, not as a " +
|
|
1000
|
+
"[workflow] key.",
|
|
1001
|
+
},
|
|
1002
|
+
// The legacy revision/draft slots are `server-owned`, not `deprecated`:
|
|
1003
|
+
// `deprecated` means "still WRITABLE, superseded by another field"
|
|
1004
|
+
// (`types.ts`), and phase 5's generated request schemas admit every
|
|
1005
|
+
// deprecated key for exactly that reason. None of these six is writable
|
|
1006
|
+
// — create and update never read them off the body; the draft save path
|
|
1007
|
+
// and the R2 writer assign them — so classifying them `deprecated` made
|
|
1008
|
+
// the schema accept a key the handler then dropped, which is the 200-on
|
|
1009
|
+
// -a-key-that-did-nothing this issue exists to end. Their models.yaml
|
|
1010
|
+
// DEPRECATED marking is about the field's future, not about who writes it.
|
|
1011
|
+
latestRevision: {
|
|
1012
|
+
kind: "server-owned",
|
|
1013
|
+
note: "Legacy revision pointer, marked DEPRECATED in models.yaml. " +
|
|
1014
|
+
"Written by the revision save path, never accepted on the wire.",
|
|
1015
|
+
},
|
|
1016
|
+
stepsR2Key: {
|
|
1017
|
+
kind: "server-owned",
|
|
1018
|
+
note: "Legacy steps pointer, marked DEPRECATED in models.yaml. Assigned " +
|
|
1019
|
+
"by the R2 write, never accepted on the wire.",
|
|
1020
|
+
},
|
|
1021
|
+
draftStepsR2Key: {
|
|
1022
|
+
kind: "server-owned",
|
|
1023
|
+
note: "Legacy draft steps pointer, marked DEPRECATED in models.yaml. " +
|
|
1024
|
+
"Assigned by the draft save path, never accepted on the wire.",
|
|
1025
|
+
},
|
|
1026
|
+
draftInputSchema: {
|
|
1027
|
+
kind: "server-owned",
|
|
1028
|
+
note: "Legacy draft slot, marked DEPRECATED in models.yaml. Written by " +
|
|
1029
|
+
"the draft save path from `inputSchema`, never accepted on the wire.",
|
|
1030
|
+
},
|
|
1031
|
+
draftOutputSchema: {
|
|
1032
|
+
kind: "server-owned",
|
|
1033
|
+
note: "Legacy draft slot, marked DEPRECATED in models.yaml. Written by " +
|
|
1034
|
+
"the draft save path from `outputSchema`, never accepted on the wire.",
|
|
1035
|
+
},
|
|
1036
|
+
draftModifiedAt: {
|
|
1037
|
+
kind: "server-owned",
|
|
1038
|
+
note: "Legacy draft timestamp, marked DEPRECATED in models.yaml. Stamped " +
|
|
1039
|
+
"by the draft save path, never accepted on the wire.",
|
|
1040
|
+
},
|
|
1041
|
+
createdBy: {
|
|
1042
|
+
kind: "server-owned",
|
|
1043
|
+
note: "Admin who created the workflow; assigned server-side.",
|
|
1044
|
+
},
|
|
1045
|
+
createdAt: {
|
|
1046
|
+
kind: "server-owned",
|
|
1047
|
+
note: "Timestamp, assigned server-side.",
|
|
1048
|
+
},
|
|
1049
|
+
modifiedAt: {
|
|
1050
|
+
kind: "server-owned",
|
|
1051
|
+
note: "Timestamp, assigned server-side.",
|
|
1052
|
+
},
|
|
1053
|
+
},
|
|
1054
|
+
tomlOnlyKeys: {
|
|
1055
|
+
key: {
|
|
1056
|
+
kind: "structural",
|
|
1057
|
+
note: "The workflow key. `config pull` writes it for readability; push takes " +
|
|
1058
|
+
"the identity from the file name and attaches `workflowKey` itself.",
|
|
1059
|
+
},
|
|
1060
|
+
activeConfigName: {
|
|
1061
|
+
kind: "structural",
|
|
1062
|
+
note: "Names which [[configs]] entry is active. Consumed by the push apply " +
|
|
1063
|
+
"path (which activates the matching config by id), not sent as a " +
|
|
1064
|
+
"[workflow] field.",
|
|
1065
|
+
},
|
|
1066
|
+
},
|
|
1067
|
+
requestOnlyKeys: {
|
|
1068
|
+
steps: {
|
|
1069
|
+
// CREATE only: `updateAppWorkflow` never reads `steps` off the body —
|
|
1070
|
+
// an update writes them through the workflow-config / draft
|
|
1071
|
+
// endpoints (`applyWorkflowBody`) — so a PATCH carrying steps used to
|
|
1072
|
+
// return 200 having saved none of them.
|
|
1073
|
+
modes: CREATE_ONLY,
|
|
1074
|
+
note: "The workflow's step list. Not a `WorkflowDefinition` field — it " +
|
|
1075
|
+
"lives on the active WorkflowConfiguration (and in R2) — but the " +
|
|
1076
|
+
"create call carries it in the same body so a new workflow lands " +
|
|
1077
|
+
"with its steps in one request. Validated by the step allowlist " +
|
|
1078
|
+
"(`cli/src/lib/generated-allowlist.ts`, #998) and the server's " +
|
|
1079
|
+
"definition validation.",
|
|
1080
|
+
},
|
|
1081
|
+
expressions: {
|
|
1082
|
+
// CREATE only, for the same reason as `steps`: they travel on the
|
|
1083
|
+
// steps payload, which an update writes through its own endpoint.
|
|
1084
|
+
modes: CREATE_ONLY,
|
|
1085
|
+
note: "#1614 — the named `expr.*` definitions, which travel on the same " +
|
|
1086
|
+
"steps payload rather than as a definition field.",
|
|
1087
|
+
},
|
|
1088
|
+
},
|
|
1089
|
+
responseOnlyKeys: {
|
|
1090
|
+
platformOwned: {
|
|
1091
|
+
kind: "server-owned",
|
|
1092
|
+
note: "#1660 — derived flag marking a reserved `__internal.` workflow. " +
|
|
1093
|
+
"Computed at serialize time; not persisted, never writable.",
|
|
1094
|
+
},
|
|
1095
|
+
},
|
|
1096
|
+
},
|
|
1097
|
+
],
|
|
1098
|
+
tomlDocumentKeys: {
|
|
1099
|
+
steps: {
|
|
1100
|
+
kind: "structural",
|
|
1101
|
+
note: "The `[[steps]]` array. Validated by the step allowlist " +
|
|
1102
|
+
"(`cli/src/lib/generated-allowlist.ts`, #998) and the server's " +
|
|
1103
|
+
"definition validation; its shape is checked by validateWorkflowToml.",
|
|
1104
|
+
},
|
|
1105
|
+
configs: {
|
|
1106
|
+
kind: "structural",
|
|
1107
|
+
note: "RETIRED as an inline list (#2645): named configurations are authored " +
|
|
1108
|
+
"one per file at `workflows/<key>.configs/<name>.toml` and selected by " +
|
|
1109
|
+
"`[workflow].activeConfigName`. Pull writes the sidecars, push applies " +
|
|
1110
|
+
"them through the workflow-config endpoints, and a file still carrying " +
|
|
1111
|
+
"`[[configs]]` fails validation (validateWorkflowToml) rather than " +
|
|
1112
|
+
"having the list quietly ignored. Declared here so the document-shape " +
|
|
1113
|
+
"check defers to that targeted message.",
|
|
1114
|
+
},
|
|
1115
|
+
...DECLARED_ACCESS_MANIFEST_KEYS,
|
|
1116
|
+
expr: {
|
|
1117
|
+
kind: "structural",
|
|
1118
|
+
note: "#1614 — the named `[expr.cel]` guard definitions, which travel on the " +
|
|
1119
|
+
"steps payload (parseWorkflowExpressionsToml).",
|
|
1120
|
+
},
|
|
1121
|
+
include: {
|
|
1122
|
+
kind: "structural",
|
|
1123
|
+
note: "Workflow fragments (`include = [\"name\"]` or repeated `[[include]]` " +
|
|
1124
|
+
"tables), expanded into `steps` by expandWorkflowTomlData before push.",
|
|
1125
|
+
},
|
|
1126
|
+
},
|
|
1127
|
+
};
|
|
1128
|
+
// ── src/config-surface/prompt.ts ─────────────────────────────────────────
|
|
1129
|
+
/**
|
|
1130
|
+
* The `prompt` configuration object's definition (issue #2644, phase 2).
|
|
1131
|
+
*
|
|
1132
|
+
* `prompts/<key>.toml` carries TWO field tables: the `[prompt]` table backed by
|
|
1133
|
+
* `AppPrompt`, and the repeated `[[configs]]` table backed by `AppPromptConfig`
|
|
1134
|
+
* — one entry per named LLM configuration. Both are declared here, so a field
|
|
1135
|
+
* added to either model is covered by the same guard.
|
|
1136
|
+
*
|
|
1137
|
+
* The `[[configs]]` entries are the reason prompts have two tables rather than
|
|
1138
|
+
* a `structural` note: they are a real field surface the CLI pushes
|
|
1139
|
+
* (`createPromptConfig` / `updatePromptConfig`) and pulls, not an opaque blob.
|
|
1140
|
+
*/
|
|
1141
|
+
const PROMPT_HANDLER = "src/admin-api.ts";
|
|
1142
|
+
export const PROMPT_SURFACE = {
|
|
1143
|
+
label: "prompt",
|
|
1144
|
+
tables: [
|
|
1145
|
+
{
|
|
1146
|
+
tomlPath: ["prompt"],
|
|
1147
|
+
repeated: false,
|
|
1148
|
+
model: "AppPrompt",
|
|
1149
|
+
// Declaration order reproduces the pre-#2644 serializer's key order.
|
|
1150
|
+
fields: [
|
|
1151
|
+
{
|
|
1152
|
+
field: "displayName",
|
|
1153
|
+
tomlKey: "displayName",
|
|
1154
|
+
type: "string",
|
|
1155
|
+
emit: "always",
|
|
1156
|
+
writableOn: BOTH,
|
|
1157
|
+
validation: "passthrough",
|
|
1158
|
+
},
|
|
1159
|
+
{
|
|
1160
|
+
field: "description",
|
|
1161
|
+
tomlKey: "description",
|
|
1162
|
+
type: "string",
|
|
1163
|
+
emit: "whenSet",
|
|
1164
|
+
writableOn: BOTH,
|
|
1165
|
+
validation: "passthrough",
|
|
1166
|
+
},
|
|
1167
|
+
// JSON Schema objects. Stored as JSON strings, returned parsed; TOML
|
|
1168
|
+
// may present them as an inline table or a JSON string.
|
|
1169
|
+
{
|
|
1170
|
+
field: "inputSchema",
|
|
1171
|
+
tomlKey: "inputSchema",
|
|
1172
|
+
type: "json",
|
|
1173
|
+
emit: "whenSet",
|
|
1174
|
+
writableOn: BOTH,
|
|
1175
|
+
validation: handledBy(PROMPT_HANDLER, "AdminAPI"),
|
|
1176
|
+
},
|
|
1177
|
+
// #2644 — accepted by both handlers and returned by the detail, but
|
|
1178
|
+
// never written to TOML before this: a prompt's declared output shape
|
|
1179
|
+
// was invisible to config-as-code.
|
|
1180
|
+
{
|
|
1181
|
+
field: "outputSchema",
|
|
1182
|
+
tomlKey: "outputSchema",
|
|
1183
|
+
type: "json",
|
|
1184
|
+
emit: "whenSet",
|
|
1185
|
+
writableOn: BOTH,
|
|
1186
|
+
validation: handledBy(PROMPT_HANDLER, "AdminAPI"),
|
|
1187
|
+
},
|
|
1188
|
+
// #2652 — the CEL rule gating who may execute the prompt. Required at
|
|
1189
|
+
// create; an explicit null/"" on update clears it (fail-closed). This
|
|
1190
|
+
// declaration is the whole authoring channel: the admin routes' unknown
|
|
1191
|
+
// key guard reads the same table, so it is what makes `accessRule` an
|
|
1192
|
+
// accepted body key as well as a pulled and pushed TOML line.
|
|
1193
|
+
{
|
|
1194
|
+
field: "accessRule",
|
|
1195
|
+
tomlKey: "accessRule",
|
|
1196
|
+
type: "string",
|
|
1197
|
+
emit: "whenSet",
|
|
1198
|
+
writableOn: BOTH,
|
|
1199
|
+
validation: handledBy(PROMPT_HANDLER, "AdminAPI"),
|
|
1200
|
+
},
|
|
1201
|
+
],
|
|
1202
|
+
notExposed: {
|
|
1203
|
+
promptId: {
|
|
1204
|
+
kind: "server-owned",
|
|
1205
|
+
note: "ULID assigned on create; never author-authored.",
|
|
1206
|
+
},
|
|
1207
|
+
// #2803 — the OBJECT-level availability, server-owned. Distinct from
|
|
1208
|
+
// the per-version `[[configs]] status` below, which stays a TOML key:
|
|
1209
|
+
// that says which named config is retired, this says whether the
|
|
1210
|
+
// prompt is in service at all.
|
|
1211
|
+
status: {
|
|
1212
|
+
kind: "server-owned",
|
|
1213
|
+
note: "Availability (#2803, #2887): active | inactive | archived. " +
|
|
1214
|
+
"Written only by `prompts enable|disable`, the delete flow (a " +
|
|
1215
|
+
"plain DELETE writes the `archived` tombstone; `?hard=true` " +
|
|
1216
|
+
"destroys), and create, which always assigns `active`. Never " +
|
|
1217
|
+
"authored in TOML; the per-version `[[configs]] status` is " +
|
|
1218
|
+
"unaffected.",
|
|
1219
|
+
},
|
|
1220
|
+
appId: {
|
|
1221
|
+
kind: "server-owned",
|
|
1222
|
+
note: "Tenant scope, taken from the route; never on the wire.",
|
|
1223
|
+
},
|
|
1224
|
+
promptKey: {
|
|
1225
|
+
kind: "structural",
|
|
1226
|
+
// Create carries it in the body; update does not read it at all
|
|
1227
|
+
// (`updateAppPrompt` — the URL names the prompt), so accepting it
|
|
1228
|
+
// there would 200 a rename that changed nothing.
|
|
1229
|
+
requestModes: ["create"],
|
|
1230
|
+
note: "The prompt's identity. Comes from the TOML file name and is " +
|
|
1231
|
+
"attached by the caller, not spread from the [prompt] table. Its " +
|
|
1232
|
+
"TOML spelling is declared in tomlOnlyKeys as `key`.",
|
|
1233
|
+
},
|
|
1234
|
+
copiedFromCatalogId: {
|
|
1235
|
+
kind: "server-owned",
|
|
1236
|
+
note: "Provenance stamp written when a prompt is copied from the catalog.",
|
|
1237
|
+
},
|
|
1238
|
+
activeConfigId: {
|
|
1239
|
+
kind: "server-owned",
|
|
1240
|
+
note: "Pointer to the active AppPromptConfig. Activation is its own " +
|
|
1241
|
+
"endpoint (activatePromptConfig), not a [prompt] key.",
|
|
1242
|
+
},
|
|
1243
|
+
defaultEvaluatorPromptId: {
|
|
1244
|
+
kind: "server-owned",
|
|
1245
|
+
note: "Link to the evaluator prompt, written when an evaluator is created " +
|
|
1246
|
+
"against this prompt. Managed through the prompts API, not TOML.",
|
|
1247
|
+
},
|
|
1248
|
+
createdBy: {
|
|
1249
|
+
kind: "server-owned",
|
|
1250
|
+
note: "Admin who created the prompt; assigned server-side.",
|
|
1251
|
+
},
|
|
1252
|
+
createdAt: {
|
|
1253
|
+
kind: "server-owned",
|
|
1254
|
+
note: "Timestamp, assigned server-side.",
|
|
1255
|
+
},
|
|
1256
|
+
modifiedAt: {
|
|
1257
|
+
kind: "server-owned",
|
|
1258
|
+
note: "Timestamp, assigned server-side.",
|
|
1259
|
+
},
|
|
1260
|
+
},
|
|
1261
|
+
requestOnlyKeys: {
|
|
1262
|
+
defaultEvaluatorPromptId: {
|
|
1263
|
+
note: "Writable through the prompts API (the console links an evaluator " +
|
|
1264
|
+
"prompt) but never through TOML, which is why it is classified " +
|
|
1265
|
+
"`server-owned` for the sync surface and named here for the request " +
|
|
1266
|
+
"surface. Sending it is accepted; sync never does.",
|
|
1267
|
+
},
|
|
1268
|
+
},
|
|
1269
|
+
tomlOnlyKeys: {
|
|
1270
|
+
key: {
|
|
1271
|
+
kind: "structural",
|
|
1272
|
+
note: "The prompt key. `config pull` writes it for readability; push takes " +
|
|
1273
|
+
"the identity from the file name and attaches `promptKey` itself.",
|
|
1274
|
+
},
|
|
1275
|
+
},
|
|
1276
|
+
responseOnlyKeys: {
|
|
1277
|
+
configs: {
|
|
1278
|
+
kind: "structural",
|
|
1279
|
+
note: "The prompt's AppPromptConfig rows, returned inline on the detail " +
|
|
1280
|
+
"and written as the repeated [[configs]] table below.",
|
|
1281
|
+
},
|
|
1282
|
+
},
|
|
1283
|
+
},
|
|
1284
|
+
{
|
|
1285
|
+
tomlPath: ["configs"],
|
|
1286
|
+
repeated: true,
|
|
1287
|
+
model: "AppPromptConfig",
|
|
1288
|
+
fields: [
|
|
1289
|
+
{
|
|
1290
|
+
field: "configName",
|
|
1291
|
+
tomlKey: "name",
|
|
1292
|
+
type: "string",
|
|
1293
|
+
emit: "always",
|
|
1294
|
+
writableOn: BOTH,
|
|
1295
|
+
// Renamable on update, uniqueness-checked within the prompt.
|
|
1296
|
+
validation: handledBy(PROMPT_HANDLER, "AdminAPI"),
|
|
1297
|
+
},
|
|
1298
|
+
// #2645 — a config's status is configuration: `prompts configs update
|
|
1299
|
+
// --status archived` used to be the only way to retire one, and pull
|
|
1300
|
+
// emitted nothing, so the file could not say which of its configs the
|
|
1301
|
+
// server still runs. Update-only: create hard-codes "active" and
|
|
1302
|
+
// ignores a supplied status, same statement `writableOn` makes for
|
|
1303
|
+
// every create-assigned field.
|
|
1304
|
+
{
|
|
1305
|
+
field: "status",
|
|
1306
|
+
tomlKey: "status",
|
|
1307
|
+
type: "string",
|
|
1308
|
+
emit: "always",
|
|
1309
|
+
writableOn: UPDATE_ONLY,
|
|
1310
|
+
validation: handledBy(PROMPT_HANDLER, "AdminAPI"),
|
|
1311
|
+
},
|
|
1312
|
+
{
|
|
1313
|
+
field: "description",
|
|
1314
|
+
tomlKey: "description",
|
|
1315
|
+
type: "string",
|
|
1316
|
+
emit: "whenSet",
|
|
1317
|
+
writableOn: BOTH,
|
|
1318
|
+
validation: "passthrough",
|
|
1319
|
+
},
|
|
1320
|
+
{
|
|
1321
|
+
field: "provider",
|
|
1322
|
+
tomlKey: "provider",
|
|
1323
|
+
type: "string",
|
|
1324
|
+
emit: "always",
|
|
1325
|
+
writableOn: BOTH,
|
|
1326
|
+
// openrouter | gemini, lower-cased and cross-checked against the model.
|
|
1327
|
+
validation: handledBy(PROMPT_HANDLER, "AdminAPI"),
|
|
1328
|
+
},
|
|
1329
|
+
{
|
|
1330
|
+
field: "model",
|
|
1331
|
+
tomlKey: "model",
|
|
1332
|
+
type: "string",
|
|
1333
|
+
emit: "always",
|
|
1334
|
+
writableOn: BOTH,
|
|
1335
|
+
validation: handledBy(PROMPT_HANDLER, "AdminAPI"),
|
|
1336
|
+
},
|
|
1337
|
+
{
|
|
1338
|
+
field: "systemPrompt",
|
|
1339
|
+
tomlKey: "systemPrompt",
|
|
1340
|
+
type: "string",
|
|
1341
|
+
emit: "whenSet",
|
|
1342
|
+
writableOn: BOTH,
|
|
1343
|
+
// Offloaded to R2 above the inline size limit and read back
|
|
1344
|
+
// transparently, so the TOML value is the whole prompt either way.
|
|
1345
|
+
validation: handledBy("src/services/large-field-storage.ts", "storeField"),
|
|
1346
|
+
},
|
|
1347
|
+
{
|
|
1348
|
+
field: "userPromptTemplate",
|
|
1349
|
+
tomlKey: "userPromptTemplate",
|
|
1350
|
+
type: "string",
|
|
1351
|
+
emit: "whenSet",
|
|
1352
|
+
writableOn: BOTH,
|
|
1353
|
+
validation: handledBy("src/services/large-field-storage.ts", "storeField"),
|
|
1354
|
+
},
|
|
1355
|
+
{
|
|
1356
|
+
field: "temperature",
|
|
1357
|
+
tomlKey: "temperature",
|
|
1358
|
+
type: "number",
|
|
1359
|
+
emit: "whenSet",
|
|
1360
|
+
writableOn: BOTH,
|
|
1361
|
+
// Stored as a string on the model, returned parsed as a number.
|
|
1362
|
+
validation: handledBy(PROMPT_HANDLER, "AdminAPI"),
|
|
1363
|
+
dualEncoded: {
|
|
1364
|
+
note: "#2869 — a `StringField` on the model: the server stores what it " +
|
|
1365
|
+
"is given and returns `parseFloat` of it, so `temperature = " +
|
|
1366
|
+
'"0.2"` and `temperature = 0.2` are one value. Both spellings ' +
|
|
1367
|
+
"compare equal; neither is a validation error.",
|
|
1368
|
+
},
|
|
1369
|
+
},
|
|
1370
|
+
// #2644 — accepted by both handlers, returned by the detail, and never
|
|
1371
|
+
// written to TOML before this.
|
|
1372
|
+
{
|
|
1373
|
+
field: "topP",
|
|
1374
|
+
tomlKey: "topP",
|
|
1375
|
+
type: "number",
|
|
1376
|
+
emit: "whenSet",
|
|
1377
|
+
writableOn: BOTH,
|
|
1378
|
+
validation: handledBy(PROMPT_HANDLER, "AdminAPI"),
|
|
1379
|
+
dualEncoded: {
|
|
1380
|
+
note: "#2869 — a `StringField` returned parsed, exactly as `temperature`.",
|
|
1381
|
+
},
|
|
1382
|
+
},
|
|
1383
|
+
{
|
|
1384
|
+
field: "maxTokens",
|
|
1385
|
+
tomlKey: "maxTokens",
|
|
1386
|
+
type: "number",
|
|
1387
|
+
emit: "whenSet",
|
|
1388
|
+
writableOn: BOTH,
|
|
1389
|
+
validation: "passthrough",
|
|
1390
|
+
},
|
|
1391
|
+
{
|
|
1392
|
+
field: "outputFormat",
|
|
1393
|
+
tomlKey: "outputFormat",
|
|
1394
|
+
type: "string",
|
|
1395
|
+
emit: "whenSet",
|
|
1396
|
+
writableOn: BOTH,
|
|
1397
|
+
validation: "passthrough",
|
|
1398
|
+
},
|
|
1399
|
+
// #2644 — the per-config structured-output schema and provider options.
|
|
1400
|
+
// Both are accepted by the config endpoints and were invisible to sync.
|
|
1401
|
+
{
|
|
1402
|
+
field: "outputSchema",
|
|
1403
|
+
tomlKey: "outputSchema",
|
|
1404
|
+
type: "json",
|
|
1405
|
+
emit: "whenSet",
|
|
1406
|
+
writableOn: BOTH,
|
|
1407
|
+
validation: handledBy(PROMPT_HANDLER, "AdminAPI"),
|
|
1408
|
+
},
|
|
1409
|
+
{
|
|
1410
|
+
field: "providerConfig",
|
|
1411
|
+
tomlKey: "providerConfig",
|
|
1412
|
+
type: "json",
|
|
1413
|
+
emit: "whenSet",
|
|
1414
|
+
writableOn: BOTH,
|
|
1415
|
+
validation: handledBy(PROMPT_HANDLER, "AdminAPI"),
|
|
1416
|
+
},
|
|
1417
|
+
],
|
|
1418
|
+
notExposed: {
|
|
1419
|
+
configId: {
|
|
1420
|
+
kind: "server-owned",
|
|
1421
|
+
note: "ULID assigned on create; never author-authored.",
|
|
1422
|
+
},
|
|
1423
|
+
promptId: {
|
|
1424
|
+
kind: "server-owned",
|
|
1425
|
+
note: "Parent pointer, taken from the route; never on the wire.",
|
|
1426
|
+
},
|
|
1427
|
+
appId: {
|
|
1428
|
+
kind: "server-owned",
|
|
1429
|
+
note: "Tenant scope, taken from the route; never on the wire.",
|
|
1430
|
+
},
|
|
1431
|
+
promptIdConfigName: {
|
|
1432
|
+
kind: "server-owned",
|
|
1433
|
+
note: "Composite uniqueness key derived from promptId + configName.",
|
|
1434
|
+
},
|
|
1435
|
+
systemPromptR2Key: {
|
|
1436
|
+
kind: "server-owned",
|
|
1437
|
+
note: "Where an oversized systemPrompt was offloaded. Assigned by " +
|
|
1438
|
+
"storeField; the value itself round-trips through `systemPrompt`.",
|
|
1439
|
+
},
|
|
1440
|
+
userPromptTemplateR2Key: {
|
|
1441
|
+
kind: "server-owned",
|
|
1442
|
+
note: "Where an oversized userPromptTemplate was offloaded. Assigned by " +
|
|
1443
|
+
"storeField; the value round-trips through `userPromptTemplate`.",
|
|
1444
|
+
},
|
|
1445
|
+
createdBy: {
|
|
1446
|
+
kind: "server-owned",
|
|
1447
|
+
note: "Admin who created the config; assigned server-side.",
|
|
1448
|
+
},
|
|
1449
|
+
createdAt: {
|
|
1450
|
+
kind: "server-owned",
|
|
1451
|
+
note: "Timestamp, assigned server-side.",
|
|
1452
|
+
},
|
|
1453
|
+
modifiedAt: {
|
|
1454
|
+
kind: "server-owned",
|
|
1455
|
+
note: "Timestamp, assigned server-side.",
|
|
1456
|
+
},
|
|
1457
|
+
},
|
|
1458
|
+
tomlOnlyKeys: {
|
|
1459
|
+
// #2645 — the live-config marker. Exactly one [[configs]] entry may
|
|
1460
|
+
// carry `active = true`; pull writes it on the active entry and push
|
|
1461
|
+
// honors it on create AND update by calling the activate endpoint
|
|
1462
|
+
// (activatePromptConfig). It is a control, never a key in the config
|
|
1463
|
+
// body.
|
|
1464
|
+
active: {
|
|
1465
|
+
kind: "structural",
|
|
1466
|
+
note: "Which config the prompt runs (#2645). Pull writes it on the " +
|
|
1467
|
+
"active entry; push enacts it through the activate endpoint " +
|
|
1468
|
+
"(activatePromptConfig), never as a config-body key.",
|
|
1469
|
+
},
|
|
1470
|
+
isActive: {
|
|
1471
|
+
kind: "structural",
|
|
1472
|
+
note: "Legacy spelling of `active` — the create leg read it before pull " +
|
|
1473
|
+
"ever wrote a marker. Declared here so the unrecognized-key " +
|
|
1474
|
+
"rejection does not fail a file that has always used it; pull " +
|
|
1475
|
+
"never writes it.",
|
|
1476
|
+
},
|
|
1477
|
+
},
|
|
1478
|
+
responseOnlyKeys: {},
|
|
1479
|
+
},
|
|
1480
|
+
],
|
|
1481
|
+
// Both of a prompt file's tables ARE field tables (`[prompt]` and the
|
|
1482
|
+
// repeated `[[configs]]`), so there is no third top-level channel. The
|
|
1483
|
+
// sidecar test cases live in `prompts/<key>.tests/` — their own surface.
|
|
1484
|
+
tomlDocumentKeys: {},
|
|
1485
|
+
};
|
|
1486
|
+
// ── src/config-surface/integration.ts ────────────────────────────────────
|
|
1487
|
+
/**
|
|
1488
|
+
* The `integration` configuration object's definition (issue #2644, phase 2).
|
|
1489
|
+
*
|
|
1490
|
+
* `integrations/<key>.toml` carries one `[integration]` field table plus the
|
|
1491
|
+
* top-level `[requestConfig]` table, which is authored structure with its own
|
|
1492
|
+
* validator (`serializeRequestConfig`) and is therefore declared `structural`
|
|
1493
|
+
* rather than re-modelled here.
|
|
1494
|
+
*
|
|
1495
|
+
* ── On the `validation` classifications ──────────────────────────────────
|
|
1496
|
+
* Phase 1 decomposed the workflow handler into one exported normalizer per
|
|
1497
|
+
* field. `createAppIntegration` / `updateAppIntegration` are not decomposed yet,
|
|
1498
|
+
* so a field with real behavior names the module and export that owns it TODAY
|
|
1499
|
+
* (`src/admin-api.ts#AdminAPI`, or the shared helper where one already exists).
|
|
1500
|
+
* The classification is what makes the field visible to every guard; splitting
|
|
1501
|
+
* those handlers up is a later phase's job, not a reason to leave the field
|
|
1502
|
+
* undeclared.
|
|
1503
|
+
*/
|
|
1504
|
+
const INTEGRATION_HANDLER = "src/admin-api.ts";
|
|
1505
|
+
export const INTEGRATION_SURFACE = {
|
|
1506
|
+
label: "integration",
|
|
1507
|
+
tables: [
|
|
1508
|
+
{
|
|
1509
|
+
tomlPath: ["integration"],
|
|
1510
|
+
repeated: false,
|
|
1511
|
+
model: "AppIntegration",
|
|
1512
|
+
// Declaration order is the order `config pull` writes the keys — it matches
|
|
1513
|
+
// the pre-#2644 hand-written serializer so migrating does not reshuffle
|
|
1514
|
+
// every existing integrations/*.toml.
|
|
1515
|
+
fields: [
|
|
1516
|
+
{
|
|
1517
|
+
field: "displayName",
|
|
1518
|
+
tomlKey: "displayName",
|
|
1519
|
+
type: "string",
|
|
1520
|
+
emit: "always",
|
|
1521
|
+
writableOn: BOTH,
|
|
1522
|
+
validation: "passthrough",
|
|
1523
|
+
},
|
|
1524
|
+
{
|
|
1525
|
+
field: "description",
|
|
1526
|
+
tomlKey: "description",
|
|
1527
|
+
type: "string",
|
|
1528
|
+
emit: "whenSet",
|
|
1529
|
+
writableOn: BOTH,
|
|
1530
|
+
validation: "passthrough",
|
|
1531
|
+
},
|
|
1532
|
+
{
|
|
1533
|
+
field: "timeoutMs",
|
|
1534
|
+
tomlKey: "timeoutMs",
|
|
1535
|
+
type: "number",
|
|
1536
|
+
emit: "always",
|
|
1537
|
+
writableOn: BOTH,
|
|
1538
|
+
validation: handledBy(INTEGRATION_HANDLER, "AdminAPI"),
|
|
1539
|
+
// `src/admin-api.ts` createAppIntegration: a create with no
|
|
1540
|
+
// `timeoutMs` stores 300000. Recorded so a hand-authored file that
|
|
1541
|
+
// omits the key compares (and pushes) as the value the server holds
|
|
1542
|
+
// rather than diffing `Modified` forever (#2880 DSO-002).
|
|
1543
|
+
serverDefault: 300000,
|
|
1544
|
+
},
|
|
1545
|
+
// #2644 — accepted by both handlers and returned on the detail, but
|
|
1546
|
+
// never written to TOML before this. Declaring it is what makes it
|
|
1547
|
+
// authorable; leaving it out would be the silent gap this epic closes.
|
|
1548
|
+
{
|
|
1549
|
+
field: "maxRequestBodyBytes",
|
|
1550
|
+
tomlKey: "maxRequestBodyBytes",
|
|
1551
|
+
type: "number",
|
|
1552
|
+
emit: "whenSet",
|
|
1553
|
+
writableOn: BOTH,
|
|
1554
|
+
validation: handledBy(INTEGRATION_HANDLER, "AdminAPI"),
|
|
1555
|
+
// Same create default, same reason (#2880 DSO-002): 1 MiB.
|
|
1556
|
+
serverDefault: 1048576,
|
|
1557
|
+
},
|
|
1558
|
+
// #2631 — the CEL rule gating who may invoke the integration. Required
|
|
1559
|
+
// at create; an explicit null/"" on update clears it (fail-closed).
|
|
1560
|
+
{
|
|
1561
|
+
field: "accessRule",
|
|
1562
|
+
tomlKey: "accessRule",
|
|
1563
|
+
type: "string",
|
|
1564
|
+
emit: "whenSet",
|
|
1565
|
+
writableOn: BOTH,
|
|
1566
|
+
validation: handledBy(INTEGRATION_HANDLER, "AdminAPI"),
|
|
1567
|
+
},
|
|
1568
|
+
],
|
|
1569
|
+
notExposed: {
|
|
1570
|
+
integrationId: {
|
|
1571
|
+
kind: "server-owned",
|
|
1572
|
+
note: "ULID assigned on create; never author-authored.",
|
|
1573
|
+
},
|
|
1574
|
+
// #2803 — availability, server-owned. `draft` is gone from the model;
|
|
1575
|
+
// a stored one reads `inactive` through the canonical normalizer.
|
|
1576
|
+
status: {
|
|
1577
|
+
kind: "server-owned",
|
|
1578
|
+
note: "Availability (#2803): active | inactive | archived. Written only " +
|
|
1579
|
+
"by `integrations enable|disable`, by the delete flow's tombstone, " +
|
|
1580
|
+
"and by create, which always assigns `active`. Never authored in " +
|
|
1581
|
+
"TOML: pull does not emit it and pull skips archived rows.",
|
|
1582
|
+
},
|
|
1583
|
+
appId: {
|
|
1584
|
+
kind: "server-owned",
|
|
1585
|
+
note: "Tenant scope, taken from the route; never on the wire.",
|
|
1586
|
+
},
|
|
1587
|
+
integrationKey: {
|
|
1588
|
+
kind: "structural",
|
|
1589
|
+
// Both modes on purpose. Update ignores the key (`updateAppIntegration`
|
|
1590
|
+
// takes the target from the URL), but every shipped `config push`
|
|
1591
|
+
// attaches it to the update body — criterion 9's breaking change is
|
|
1592
|
+
// for stray keys, not for the normal request of the CLI in the field.
|
|
1593
|
+
requestModes: ["create", "update"],
|
|
1594
|
+
note: "The integration's identity. Comes from the TOML file name and is " +
|
|
1595
|
+
"attached by the caller, not spread from the [integration] table. " +
|
|
1596
|
+
"Its TOML spelling is declared in tomlOnlyKeys as `key`.",
|
|
1597
|
+
},
|
|
1598
|
+
requestConfig: {
|
|
1599
|
+
kind: "structural",
|
|
1600
|
+
note: "The upstream request definition. Authored as the top-level " +
|
|
1601
|
+
"[requestConfig] table and validated by serializeRequestConfig " +
|
|
1602
|
+
"(src/services/integration-utils.ts), not as an [integration] key.",
|
|
1603
|
+
},
|
|
1604
|
+
activeConfigId: {
|
|
1605
|
+
kind: "server-owned",
|
|
1606
|
+
note: "#1183 — pointer to the active versioned config, set on publish.",
|
|
1607
|
+
},
|
|
1608
|
+
copiedFromCatalogId: {
|
|
1609
|
+
kind: "server-owned",
|
|
1610
|
+
note: "Provenance stamp written when an integration is copied from the catalog.",
|
|
1611
|
+
},
|
|
1612
|
+
lastTestedAt: {
|
|
1613
|
+
kind: "server-owned",
|
|
1614
|
+
note: "Timestamp of the last test invocation; written by the test endpoint.",
|
|
1615
|
+
},
|
|
1616
|
+
createdBy: {
|
|
1617
|
+
kind: "server-owned",
|
|
1618
|
+
note: "Admin who created the integration; assigned server-side.",
|
|
1619
|
+
},
|
|
1620
|
+
createdAt: {
|
|
1621
|
+
kind: "server-owned",
|
|
1622
|
+
note: "Timestamp, assigned server-side.",
|
|
1623
|
+
},
|
|
1624
|
+
modifiedAt: {
|
|
1625
|
+
kind: "server-owned",
|
|
1626
|
+
note: "Timestamp, assigned server-side.",
|
|
1627
|
+
},
|
|
1628
|
+
},
|
|
1629
|
+
tomlOnlyKeys: {
|
|
1630
|
+
key: {
|
|
1631
|
+
kind: "structural",
|
|
1632
|
+
note: "The integration key. `config pull` writes it for readability; push " +
|
|
1633
|
+
"takes the identity from the file name and attaches " +
|
|
1634
|
+
"`integrationKey` itself.",
|
|
1635
|
+
},
|
|
1636
|
+
},
|
|
1637
|
+
responseOnlyKeys: {},
|
|
1638
|
+
},
|
|
1639
|
+
],
|
|
1640
|
+
tomlDocumentKeys: {
|
|
1641
|
+
requestConfig: {
|
|
1642
|
+
kind: "structural",
|
|
1643
|
+
note: "The upstream request definition (method, url, headers, body). " +
|
|
1644
|
+
"Authored as its own top-level table and stored as the integration's " +
|
|
1645
|
+
"`requestConfig`; the server validates its shape.",
|
|
1646
|
+
},
|
|
1647
|
+
},
|
|
1648
|
+
};
|
|
1649
|
+
// ── src/config-surface/webhook.ts ────────────────────────────────────────
|
|
1650
|
+
/**
|
|
1651
|
+
* The `webhook` configuration object's definition (issue #2644, phase 2).
|
|
1652
|
+
*
|
|
1653
|
+
* `webhooks/<key>.toml` carries one `[webhook]` field table plus four authored
|
|
1654
|
+
* structures with their own channels — `[verification.<scheme>]` (the stored
|
|
1655
|
+
* `config` blob), `[allowedIps]`, `[inputMapping]` and `[metadata]`. Each is
|
|
1656
|
+
* declared `structural` here with the code that owns it named, so the reason is
|
|
1657
|
+
* visible rather than implied by absence.
|
|
1658
|
+
*
|
|
1659
|
+
* ── Secret-adjacent fields ───────────────────────────────────────────────
|
|
1660
|
+
* `signingSecret` is EXPOSED with a reference-only contract (#2254): the server
|
|
1661
|
+
* returns a whole `{{secrets.KEY}}` reference or `null` and never the
|
|
1662
|
+
* credential, which is exactly what makes `config pull` → `config push` lossless
|
|
1663
|
+
* (#1235). `previousSigningSecret` is `notExposed: secret` — a rotation slot the
|
|
1664
|
+
* author does not write. The derived `signingSecretStatus` / `…Health` keys are
|
|
1665
|
+
* response-only. The coverage guard fails on a bare omission of any of these
|
|
1666
|
+
* (`/secret|token|password|credential/i`), so none of it can be decided by
|
|
1667
|
+
* silence.
|
|
1668
|
+
*
|
|
1669
|
+
* On the `validation` classifications: `createAppWebhook` / `updateAppWebhook`
|
|
1670
|
+
* are not decomposed into per-field handlers, so a field names the shared
|
|
1671
|
+
* validator that owns it where one exists, and `src/admin-api.ts#AdminAPI`
|
|
1672
|
+
* otherwise.
|
|
1673
|
+
*/
|
|
1674
|
+
const WEBHOOK_HANDLER = "src/admin-api.ts";
|
|
1675
|
+
const WEBHOOK_CONFIG_VALIDATION = "src/app-api/services/webhook-verification/config-validation.ts";
|
|
1676
|
+
export const WEBHOOK_SURFACE = {
|
|
1677
|
+
label: "webhook",
|
|
1678
|
+
tables: [
|
|
1679
|
+
{
|
|
1680
|
+
tomlPath: ["webhook"],
|
|
1681
|
+
repeated: false,
|
|
1682
|
+
model: "AppWebhook",
|
|
1683
|
+
// Declaration order reproduces the pre-#2644 serializer's key order.
|
|
1684
|
+
fields: [
|
|
1685
|
+
{
|
|
1686
|
+
field: "displayName",
|
|
1687
|
+
tomlKey: "displayName",
|
|
1688
|
+
type: "string",
|
|
1689
|
+
emit: "always",
|
|
1690
|
+
writableOn: BOTH,
|
|
1691
|
+
validation: "passthrough",
|
|
1692
|
+
},
|
|
1693
|
+
{
|
|
1694
|
+
field: "description",
|
|
1695
|
+
tomlKey: "description",
|
|
1696
|
+
type: "string",
|
|
1697
|
+
emit: "whenSet",
|
|
1698
|
+
writableOn: BOTH,
|
|
1699
|
+
validation: "passthrough",
|
|
1700
|
+
},
|
|
1701
|
+
{
|
|
1702
|
+
field: "workflowKey",
|
|
1703
|
+
tomlKey: "workflowKey",
|
|
1704
|
+
type: "string",
|
|
1705
|
+
emit: "always",
|
|
1706
|
+
writableOn: BOTH,
|
|
1707
|
+
validation: "passthrough",
|
|
1708
|
+
},
|
|
1709
|
+
{
|
|
1710
|
+
field: "verificationScheme",
|
|
1711
|
+
tomlKey: "verificationScheme",
|
|
1712
|
+
type: "string",
|
|
1713
|
+
emit: "always",
|
|
1714
|
+
writableOn: BOTH,
|
|
1715
|
+
validation: handledBy(WEBHOOK_HANDLER, "AdminAPI"),
|
|
1716
|
+
},
|
|
1717
|
+
{
|
|
1718
|
+
field: "toleranceSeconds",
|
|
1719
|
+
tomlKey: "toleranceSeconds",
|
|
1720
|
+
type: "number",
|
|
1721
|
+
emit: "always",
|
|
1722
|
+
writableOn: BOTH,
|
|
1723
|
+
validation: handledBy(WEBHOOK_CONFIG_VALIDATION, "validateReplayProtection"),
|
|
1724
|
+
},
|
|
1725
|
+
{
|
|
1726
|
+
field: "deduplicationEnabled",
|
|
1727
|
+
tomlKey: "deduplicationEnabled",
|
|
1728
|
+
type: "boolean",
|
|
1729
|
+
emit: "always",
|
|
1730
|
+
writableOn: BOTH,
|
|
1731
|
+
validation: handledBy(WEBHOOK_CONFIG_VALIDATION, "validateReplayProtection"),
|
|
1732
|
+
},
|
|
1733
|
+
{
|
|
1734
|
+
field: "deduplicationWindowMs",
|
|
1735
|
+
tomlKey: "deduplicationWindowMs",
|
|
1736
|
+
type: "number",
|
|
1737
|
+
emit: "always",
|
|
1738
|
+
writableOn: BOTH,
|
|
1739
|
+
validation: handledBy(WEBHOOK_CONFIG_VALIDATION, "validateReplayProtection"),
|
|
1740
|
+
},
|
|
1741
|
+
// Per-webhook inbound body cap. Unset means the platform default
|
|
1742
|
+
// (5 MiB), so it is omitted from TOML rather than written as a zero.
|
|
1743
|
+
{
|
|
1744
|
+
field: "maxBodyBytes",
|
|
1745
|
+
tomlKey: "maxBodyBytes",
|
|
1746
|
+
type: "number",
|
|
1747
|
+
emit: "whenSet",
|
|
1748
|
+
writableOn: BOTH,
|
|
1749
|
+
validation: handledBy(WEBHOOK_CONFIG_VALIDATION, "validateMaxBodyBytes"),
|
|
1750
|
+
},
|
|
1751
|
+
{
|
|
1752
|
+
field: "secretGracePeriodMs",
|
|
1753
|
+
tomlKey: "secretGracePeriodMs",
|
|
1754
|
+
type: "number",
|
|
1755
|
+
emit: "always",
|
|
1756
|
+
writableOn: BOTH,
|
|
1757
|
+
validation: handledBy(WEBHOOK_CONFIG_VALIDATION, "normalizeSecretGracePeriodMs"),
|
|
1758
|
+
},
|
|
1759
|
+
// #2254 — reference-only: a whole `{{secrets.KEY}}` reference naming an
|
|
1760
|
+
// existing app secret, and nothing else. The server never returns
|
|
1761
|
+
// credential material here, so the reference round-trips verbatim
|
|
1762
|
+
// (#1235) and a legacy literal is withheld (no line is written).
|
|
1763
|
+
{
|
|
1764
|
+
field: "signingSecret",
|
|
1765
|
+
tomlKey: "signingSecret",
|
|
1766
|
+
type: "string",
|
|
1767
|
+
emit: "whenSet",
|
|
1768
|
+
writableOn: BOTH,
|
|
1769
|
+
validation: handledBy(WEBHOOK_CONFIG_VALIDATION, "validateWholeSecretReference"),
|
|
1770
|
+
},
|
|
1771
|
+
],
|
|
1772
|
+
notExposed: {
|
|
1773
|
+
webhookId: {
|
|
1774
|
+
kind: "server-owned",
|
|
1775
|
+
note: "ULID assigned on create; never author-authored.",
|
|
1776
|
+
},
|
|
1777
|
+
// #2803 — availability is server-owned. As a `[webhook]` key it made
|
|
1778
|
+
// `config push` a second writer: a file pulled before an operator
|
|
1779
|
+
// paused the webhook put the endpoint back in service on the next
|
|
1780
|
+
// push. `server-owned` keeps it out of the TOML surface AND out of the
|
|
1781
|
+
// create/update request schemas, closing the tenant app-API path that
|
|
1782
|
+
// stored `body.status` unvalidated.
|
|
1783
|
+
status: {
|
|
1784
|
+
kind: "server-owned",
|
|
1785
|
+
note: "Availability (#2803): active | inactive | archived. Written only " +
|
|
1786
|
+
"by `webhooks enable|disable` and by the delete flow's tombstone. " +
|
|
1787
|
+
"Never authored in TOML: pull does not emit it, pull skips " +
|
|
1788
|
+
"archived rows entirely, and a file carrying the key fails push.",
|
|
1789
|
+
},
|
|
1790
|
+
appId: {
|
|
1791
|
+
kind: "server-owned",
|
|
1792
|
+
note: "Tenant scope, taken from the route; never on the wire.",
|
|
1793
|
+
},
|
|
1794
|
+
webhookKey: {
|
|
1795
|
+
kind: "structural",
|
|
1796
|
+
// Both modes: the update handlers ignore it, but the shipped CLI
|
|
1797
|
+
// attaches it to every webhook update body (see integration.ts).
|
|
1798
|
+
requestModes: ["create", "update"],
|
|
1799
|
+
note: "The webhook's identity. Comes from the TOML file name and is " +
|
|
1800
|
+
"attached by the caller, not spread from the [webhook] table. Its " +
|
|
1801
|
+
"TOML spelling is declared in tomlOnlyKeys as `key`.",
|
|
1802
|
+
},
|
|
1803
|
+
workflowId: {
|
|
1804
|
+
kind: "server-owned",
|
|
1805
|
+
note: "Resolved from `workflowKey` server-side; the author writes the key.",
|
|
1806
|
+
},
|
|
1807
|
+
previousSigningSecret: {
|
|
1808
|
+
kind: "secret",
|
|
1809
|
+
note: "#2383 rotation slot. Written by the rotate endpoint, never by an " +
|
|
1810
|
+
"author, and reported through the derived signingSecretStatus " +
|
|
1811
|
+
"fields rather than as a TOML key.",
|
|
1812
|
+
},
|
|
1813
|
+
secretRotatedAt: {
|
|
1814
|
+
kind: "server-owned",
|
|
1815
|
+
note: "Stamped by the rotate endpoint; reported, never authored.",
|
|
1816
|
+
},
|
|
1817
|
+
config: {
|
|
1818
|
+
kind: "structural",
|
|
1819
|
+
note: "Per-scheme verification settings. Authored as the top-level " +
|
|
1820
|
+
"[verification.<scheme>] table and converted by " +
|
|
1821
|
+
"webhookConfigToTomlTable / tomlTableToWebhookConfig, whose stored " +
|
|
1822
|
+
"shape differs per scheme (#2109), not as a [webhook] key.",
|
|
1823
|
+
},
|
|
1824
|
+
allowedIpCidrs: {
|
|
1825
|
+
kind: "structural",
|
|
1826
|
+
note: "Authored as the top-level [allowedIps] table (`cidrs = [...]`) and " +
|
|
1827
|
+
"stored as a JSON string, not as a [webhook] key.",
|
|
1828
|
+
},
|
|
1829
|
+
inputMapping: {
|
|
1830
|
+
kind: "structural",
|
|
1831
|
+
note: "Authored as the top-level [inputMapping] table and stored as a " +
|
|
1832
|
+
"JSON string, not as a [webhook] key.",
|
|
1833
|
+
},
|
|
1834
|
+
metadata: {
|
|
1835
|
+
kind: "structural",
|
|
1836
|
+
note: "Authored as the top-level [metadata] table and stored as a JSON " +
|
|
1837
|
+
"string, not as a [webhook] key.",
|
|
1838
|
+
},
|
|
1839
|
+
handshakeRules: {
|
|
1840
|
+
kind: "structural",
|
|
1841
|
+
note: "Provider handshake/challenge rules, validated by " +
|
|
1842
|
+
"src/app-api/services/webhook-handshake.ts. Accepted by the admin " +
|
|
1843
|
+
"API but not part of the TOML surface — `config push` never sends it, " +
|
|
1844
|
+
"so the stored value is left alone rather than cleared.",
|
|
1845
|
+
},
|
|
1846
|
+
lastTriggeredAt: {
|
|
1847
|
+
kind: "server-owned",
|
|
1848
|
+
note: "Stamped by the receiver on delivery; reported, never authored.",
|
|
1849
|
+
},
|
|
1850
|
+
createdBy: {
|
|
1851
|
+
kind: "server-owned",
|
|
1852
|
+
note: "User who created the webhook; assigned server-side.",
|
|
1853
|
+
},
|
|
1854
|
+
createdAt: {
|
|
1855
|
+
kind: "server-owned",
|
|
1856
|
+
note: "Timestamp, assigned server-side.",
|
|
1857
|
+
},
|
|
1858
|
+
modifiedAt: {
|
|
1859
|
+
kind: "server-owned",
|
|
1860
|
+
note: "Timestamp, assigned server-side.",
|
|
1861
|
+
},
|
|
1862
|
+
},
|
|
1863
|
+
tomlOnlyKeys: {
|
|
1864
|
+
key: {
|
|
1865
|
+
kind: "structural",
|
|
1866
|
+
note: "The webhook key. `config pull` writes it for readability; push takes " +
|
|
1867
|
+
"the identity from the file name and attaches `webhookKey` itself.",
|
|
1868
|
+
},
|
|
1869
|
+
},
|
|
1870
|
+
responseOnlyKeys: {
|
|
1871
|
+
signingSecretStatus: {
|
|
1872
|
+
kind: "server-owned",
|
|
1873
|
+
note: "#2254 — derived disclosure state of the stored secret (reference / " +
|
|
1874
|
+
"withheld literal / unset). Computed at serialize time; read-only.",
|
|
1875
|
+
},
|
|
1876
|
+
previousSigningSecretStatus: {
|
|
1877
|
+
kind: "server-owned",
|
|
1878
|
+
note: "Same derivation for the rotation slot. Read-only.",
|
|
1879
|
+
},
|
|
1880
|
+
signingSecretHealth: {
|
|
1881
|
+
kind: "server-owned",
|
|
1882
|
+
note: "#2383 — derived health report on the stored secret. Read-only.",
|
|
1883
|
+
},
|
|
1884
|
+
previousSigningSecretHealth: {
|
|
1885
|
+
kind: "server-owned",
|
|
1886
|
+
note: "Same report for the rotation slot. Read-only.",
|
|
1887
|
+
},
|
|
1888
|
+
},
|
|
1889
|
+
},
|
|
1890
|
+
],
|
|
1891
|
+
tomlDocumentKeys: {
|
|
1892
|
+
verification: {
|
|
1893
|
+
kind: "structural",
|
|
1894
|
+
note: "The `[verification.<scheme>]` table carrying the scheme's settings " +
|
|
1895
|
+
"(#2109). Converted to/from the stored `config` blob by " +
|
|
1896
|
+
"webhookConfigToTomlTable / tomlTableToWebhookConfig.",
|
|
1897
|
+
},
|
|
1898
|
+
allowedIps: {
|
|
1899
|
+
kind: "structural",
|
|
1900
|
+
note: "`[allowedIps] cidrs = [...]` — the source allow-list, stored as the " +
|
|
1901
|
+
"JSON string `allowedIpCidrs`.",
|
|
1902
|
+
},
|
|
1903
|
+
inputMapping: {
|
|
1904
|
+
kind: "structural",
|
|
1905
|
+
note: "The authored payload mapping, stored as the JSON string " +
|
|
1906
|
+
"`inputMapping`; a table rather than a [webhook] key so it can nest.",
|
|
1907
|
+
},
|
|
1908
|
+
metadata: {
|
|
1909
|
+
kind: "structural",
|
|
1910
|
+
note: "The webhook's authored metadata table, stored as the JSON string " +
|
|
1911
|
+
"`metadata`. Unrelated to the declared-access manifest of the type " +
|
|
1912
|
+
"configs, which spells `[metadata]` the same way.",
|
|
1913
|
+
},
|
|
1914
|
+
},
|
|
1915
|
+
};
|
|
1916
|
+
// ── src/config-surface/cron-trigger.ts ───────────────────────────────────
|
|
1917
|
+
/**
|
|
1918
|
+
* The `cron-trigger` configuration object's definition (issue #2644, phase 2).
|
|
1919
|
+
*
|
|
1920
|
+
* `cron-triggers/<key>.toml` carries one `[cronTrigger]` field table plus the
|
|
1921
|
+
* top-level `[rootInput]` and `[inputMapping]` tables, which are authored JSON
|
|
1922
|
+
* structures with their own normalizer and are declared `structural`.
|
|
1923
|
+
*
|
|
1924
|
+
* `state` is classified `structural` (update-only), not a field (#2645):
|
|
1925
|
+
* pausing moved onto the `operationallyDisabled` toggle written by
|
|
1926
|
+
* `cron-triggers pause|resume`, so `state` left the TOML surface — pull does
|
|
1927
|
+
* not emit it, and push REJECTS a file that still carries the line (with
|
|
1928
|
+
* guidance naming the pause verbs; see `RETIRED_TOML_KEYS` in the CLI's
|
|
1929
|
+
* `commands/sync.ts`) — while the update handler keeps reading it for the
|
|
1930
|
+
* console.
|
|
1931
|
+
*/
|
|
1932
|
+
const CRON_TRIGGER_HANDLER = "src/app-api/controllers/cron-triggers-controller.ts";
|
|
1933
|
+
export const CRON_TRIGGER_SURFACE = {
|
|
1934
|
+
label: "cron-trigger",
|
|
1935
|
+
tables: [
|
|
1936
|
+
{
|
|
1937
|
+
tomlPath: ["cronTrigger"],
|
|
1938
|
+
repeated: false,
|
|
1939
|
+
model: "CronTrigger",
|
|
1940
|
+
// Declaration order reproduces the pre-#2644 serializer's key order.
|
|
1941
|
+
fields: [
|
|
1942
|
+
{
|
|
1943
|
+
field: "displayName",
|
|
1944
|
+
tomlKey: "displayName",
|
|
1945
|
+
type: "string",
|
|
1946
|
+
emit: "always",
|
|
1947
|
+
writableOn: BOTH,
|
|
1948
|
+
validation: "passthrough",
|
|
1949
|
+
},
|
|
1950
|
+
{
|
|
1951
|
+
field: "description",
|
|
1952
|
+
tomlKey: "description",
|
|
1953
|
+
type: "string",
|
|
1954
|
+
emit: "whenSet",
|
|
1955
|
+
writableOn: BOTH,
|
|
1956
|
+
validation: "passthrough",
|
|
1957
|
+
},
|
|
1958
|
+
{
|
|
1959
|
+
field: "cron",
|
|
1960
|
+
tomlKey: "cron",
|
|
1961
|
+
type: "string",
|
|
1962
|
+
emit: "always",
|
|
1963
|
+
writableOn: BOTH,
|
|
1964
|
+
validation: handledBy("src/cron-parser.ts", "parseCron"),
|
|
1965
|
+
},
|
|
1966
|
+
{
|
|
1967
|
+
field: "timezone",
|
|
1968
|
+
tomlKey: "timezone",
|
|
1969
|
+
type: "string",
|
|
1970
|
+
emit: "always",
|
|
1971
|
+
writableOn: BOTH,
|
|
1972
|
+
// Validated against Intl; unset reads as "UTC".
|
|
1973
|
+
validation: handledBy(CRON_TRIGGER_HANDLER, "CronTriggersController"),
|
|
1974
|
+
// `cron-triggers-controller.ts` stores `timezone || "UTC"`, so a file
|
|
1975
|
+
// that omits the key describes a trigger running in UTC — compared
|
|
1976
|
+
// and pushed as that, rather than as a difference no push can clear
|
|
1977
|
+
// (#2880 DSO-002).
|
|
1978
|
+
serverDefault: "UTC",
|
|
1979
|
+
},
|
|
1980
|
+
{
|
|
1981
|
+
field: "workflowKey",
|
|
1982
|
+
tomlKey: "workflowKey",
|
|
1983
|
+
type: "string",
|
|
1984
|
+
emit: "always",
|
|
1985
|
+
writableOn: BOTH,
|
|
1986
|
+
// #1002 — a reserved `__internal.` key is rejected at save time.
|
|
1987
|
+
validation: handledBy(CRON_TRIGGER_HANDLER, "CronTriggersController"),
|
|
1988
|
+
},
|
|
1989
|
+
{
|
|
1990
|
+
field: "overlapPolicy",
|
|
1991
|
+
tomlKey: "overlapPolicy",
|
|
1992
|
+
type: "string",
|
|
1993
|
+
emit: "always",
|
|
1994
|
+
writableOn: BOTH,
|
|
1995
|
+
// skip | allow.
|
|
1996
|
+
validation: handledBy(CRON_TRIGGER_HANDLER, "CronTriggersController"),
|
|
1997
|
+
// Same store-time default as `timezone` (#2880 DSO-002).
|
|
1998
|
+
serverDefault: "skip",
|
|
1999
|
+
},
|
|
2000
|
+
],
|
|
2001
|
+
notExposed: {
|
|
2002
|
+
// #2803 — the ONE availability control, server-owned. Written only by
|
|
2003
|
+
// `cron-triggers enable|disable`, the delete flow (the `archived`
|
|
2004
|
+
// tombstone) and the alarm's error-pause path. `server-owned` keeps it
|
|
2005
|
+
// out of both the TOML surface and the create/update request schemas,
|
|
2006
|
+
// so no configuration write can change availability as a side effect.
|
|
2007
|
+
status: {
|
|
2008
|
+
kind: "server-owned",
|
|
2009
|
+
note: "Availability (#2803): active | inactive | archived. Written only " +
|
|
2010
|
+
"by `cron-triggers enable|disable`, the delete flow and the " +
|
|
2011
|
+
"error-pause path. Never authored in TOML: pull does not emit it " +
|
|
2012
|
+
"and a file that still carries the key fails push.",
|
|
2013
|
+
},
|
|
2014
|
+
// #2645 criterion 9 moved pausing off `state`; #2803 retired the field
|
|
2015
|
+
// entirely. It mixed lifecycle (`archived`, `error_paused`) with
|
|
2016
|
+
// availability (`paused`), which is what made two writers possible in
|
|
2017
|
+
// the first place. `server-owned` now, not `structural` with an
|
|
2018
|
+
// update-only mode: the console's carve-out is gone with it, so the
|
|
2019
|
+
// generic update rejects a supplied `state` exactly as create always
|
|
2020
|
+
// did.
|
|
2021
|
+
state: {
|
|
2022
|
+
kind: "server-owned",
|
|
2023
|
+
note: "DEPRECATED (#2803), superseded by `status`. Read only by " +
|
|
2024
|
+
"`canonicalCronStatus()`; normalized back to `active` by " +
|
|
2025
|
+
"`cron-triggers enable`. Never authored in TOML and never accepted " +
|
|
2026
|
+
"on the wire.",
|
|
2027
|
+
},
|
|
2028
|
+
operationallyDisabled: {
|
|
2029
|
+
kind: "server-owned",
|
|
2030
|
+
note: "DEPRECATED (#2803), superseded by `status`. The #2645 operational " +
|
|
2031
|
+
"toggle, cleared by `cron-triggers enable|disable` as rows are " +
|
|
2032
|
+
"touched. Never authored in TOML and never accepted on the config " +
|
|
2033
|
+
"create/update wire.",
|
|
2034
|
+
},
|
|
2035
|
+
triggerId: {
|
|
2036
|
+
kind: "server-owned",
|
|
2037
|
+
note: "ULID assigned on create; never author-authored.",
|
|
2038
|
+
},
|
|
2039
|
+
appId: {
|
|
2040
|
+
kind: "server-owned",
|
|
2041
|
+
note: "Tenant scope, taken from the route; never on the wire.",
|
|
2042
|
+
},
|
|
2043
|
+
triggerKey: {
|
|
2044
|
+
kind: "structural",
|
|
2045
|
+
// Both modes: the update handler ignores it, but the shipped CLI
|
|
2046
|
+
// attaches it to every cron-trigger update body (see integration.ts).
|
|
2047
|
+
requestModes: ["create", "update"],
|
|
2048
|
+
note: "The trigger's identity. Comes from the TOML file name and is " +
|
|
2049
|
+
"attached by the caller, not spread from the [cronTrigger] table. " +
|
|
2050
|
+
"Its TOML spelling is declared in tomlOnlyKeys as `key`.",
|
|
2051
|
+
},
|
|
2052
|
+
rootInput: {
|
|
2053
|
+
kind: "structural",
|
|
2054
|
+
note: "Authored as the top-level [rootInput] table and stored as a JSON " +
|
|
2055
|
+
"string (normalizeJsonString), not as a [cronTrigger] key.",
|
|
2056
|
+
},
|
|
2057
|
+
inputMapping: {
|
|
2058
|
+
kind: "structural",
|
|
2059
|
+
note: "Authored as the top-level [inputMapping] table and stored as a " +
|
|
2060
|
+
"JSON string (normalizeJsonString), not as a [cronTrigger] key.",
|
|
2061
|
+
},
|
|
2062
|
+
lastError: {
|
|
2063
|
+
kind: "server-owned",
|
|
2064
|
+
note: "Last fire failure, written by the trigger DO and cleared on resume.",
|
|
2065
|
+
},
|
|
2066
|
+
lastTriggeredAt: {
|
|
2067
|
+
kind: "server-owned",
|
|
2068
|
+
note: "Stamped on fire; reported, never authored.",
|
|
2069
|
+
},
|
|
2070
|
+
lastTriggeredRunId: {
|
|
2071
|
+
kind: "server-owned",
|
|
2072
|
+
note: "Run started by the last fire; reported, never authored.",
|
|
2073
|
+
},
|
|
2074
|
+
skippedCount: {
|
|
2075
|
+
kind: "server-owned",
|
|
2076
|
+
note: "Runtime counter maintained by the trigger DO.",
|
|
2077
|
+
},
|
|
2078
|
+
firedCount: {
|
|
2079
|
+
kind: "server-owned",
|
|
2080
|
+
note: "Runtime counter maintained by the trigger DO.",
|
|
2081
|
+
},
|
|
2082
|
+
consecutiveNotActiveCount: {
|
|
2083
|
+
kind: "server-owned",
|
|
2084
|
+
note: "Runtime counter behind the auto-pause rule; DO-maintained.",
|
|
2085
|
+
},
|
|
2086
|
+
nextFireAt: {
|
|
2087
|
+
kind: "server-owned",
|
|
2088
|
+
note: "Projected next fire, computed from the schedule by the DO.",
|
|
2089
|
+
},
|
|
2090
|
+
createdBy: {
|
|
2091
|
+
kind: "server-owned",
|
|
2092
|
+
note: "User who created the trigger; assigned server-side.",
|
|
2093
|
+
},
|
|
2094
|
+
createdAt: {
|
|
2095
|
+
kind: "server-owned",
|
|
2096
|
+
note: "Timestamp, assigned server-side.",
|
|
2097
|
+
},
|
|
2098
|
+
modifiedAt: {
|
|
2099
|
+
kind: "server-owned",
|
|
2100
|
+
note: "Timestamp, assigned server-side.",
|
|
2101
|
+
},
|
|
2102
|
+
},
|
|
2103
|
+
requestOnlyKeys: {
|
|
2104
|
+
key: {
|
|
2105
|
+
// CREATE only: `update` never reads it — the trigger is addressed by
|
|
2106
|
+
// id in the path, and its key is immutable — so a `key` sent on
|
|
2107
|
+
// update is a 400 rather than a silently ignored rename.
|
|
2108
|
+
modes: CREATE_ONLY,
|
|
2109
|
+
note: "Alias for `triggerKey` on create, accepted by the controller " +
|
|
2110
|
+
"(`triggerKey || key`) since before sync existed. Declared so the " +
|
|
2111
|
+
"generated request schema keeps accepting a client that uses it.",
|
|
2112
|
+
},
|
|
2113
|
+
name: {
|
|
2114
|
+
note: "Alias for `displayName`, accepted on create and update " +
|
|
2115
|
+
"(`displayName ?? name`). Same reason as `key`.",
|
|
2116
|
+
},
|
|
2117
|
+
},
|
|
2118
|
+
tomlOnlyKeys: {
|
|
2119
|
+
key: {
|
|
2120
|
+
kind: "structural",
|
|
2121
|
+
note: "The trigger key. `config pull` writes it for readability; push takes " +
|
|
2122
|
+
"the identity from the file name and attaches `triggerKey` itself.",
|
|
2123
|
+
},
|
|
2124
|
+
},
|
|
2125
|
+
responseOnlyKeys: {
|
|
2126
|
+
runtime: {
|
|
2127
|
+
kind: "server-owned",
|
|
2128
|
+
note: "DO-derived schedule status (`scheduledAlarm` / `scheduledAlarmAt`) " +
|
|
2129
|
+
"attached to the detail response. Computed, never persisted here.",
|
|
2130
|
+
},
|
|
2131
|
+
},
|
|
2132
|
+
},
|
|
2133
|
+
],
|
|
2134
|
+
tomlDocumentKeys: {
|
|
2135
|
+
rootInput: {
|
|
2136
|
+
kind: "structural",
|
|
2137
|
+
note: "The authored root input for each fire, stored as the JSON string " +
|
|
2138
|
+
"`rootInput` (normalizeJsonString).",
|
|
2139
|
+
},
|
|
2140
|
+
inputMapping: {
|
|
2141
|
+
kind: "structural",
|
|
2142
|
+
note: "The authored input mapping, stored as the JSON string `inputMapping` " +
|
|
2143
|
+
"(normalizeJsonString).",
|
|
2144
|
+
},
|
|
2145
|
+
},
|
|
2146
|
+
};
|
|
2147
|
+
// ── src/config-surface/blob-bucket.ts ────────────────────────────────────
|
|
2148
|
+
/**
|
|
2149
|
+
* The `blob-bucket` configuration object's definition (issue #2644, phase 2).
|
|
2150
|
+
*
|
|
2151
|
+
* `blob-buckets/<key>.toml` carries a single `[bucket]` table.
|
|
2152
|
+
*
|
|
2153
|
+
* `ttlTier` is `writableOn: ["create"]`: the update handler does not read it, so
|
|
2154
|
+
* the retention tier is fixed at creation. `preset` and `ruleSetId` are the two
|
|
2155
|
+
* halves of the access model (#1020) — at most one may change per request, which
|
|
2156
|
+
* is the controller's rule and stays there.
|
|
2157
|
+
*/
|
|
2158
|
+
const BLOB_BUCKET_HANDLER = "src/app-api/controllers/blob-buckets-controller.ts";
|
|
2159
|
+
export const BLOB_BUCKET_SURFACE = {
|
|
2160
|
+
label: "blob-bucket",
|
|
2161
|
+
tables: [
|
|
2162
|
+
{
|
|
2163
|
+
tomlPath: ["bucket"],
|
|
2164
|
+
repeated: false,
|
|
2165
|
+
model: "BlobBucket",
|
|
2166
|
+
// Declaration order reproduces the pre-#2644 serializer's key order.
|
|
2167
|
+
fields: [
|
|
2168
|
+
{
|
|
2169
|
+
field: "name",
|
|
2170
|
+
tomlKey: "name",
|
|
2171
|
+
type: "string",
|
|
2172
|
+
emit: "always",
|
|
2173
|
+
writableOn: BOTH,
|
|
2174
|
+
validation: "passthrough",
|
|
2175
|
+
},
|
|
2176
|
+
{
|
|
2177
|
+
field: "description",
|
|
2178
|
+
tomlKey: "description",
|
|
2179
|
+
type: "string",
|
|
2180
|
+
emit: "whenSet",
|
|
2181
|
+
writableOn: BOTH,
|
|
2182
|
+
validation: "passthrough",
|
|
2183
|
+
},
|
|
2184
|
+
{
|
|
2185
|
+
field: "ttlTier",
|
|
2186
|
+
tomlKey: "ttlTier",
|
|
2187
|
+
type: "string",
|
|
2188
|
+
emit: "always",
|
|
2189
|
+
writableOn: CREATE_ONLY,
|
|
2190
|
+
// Immutable after create: `update` never reads it.
|
|
2191
|
+
validation: handledBy(BLOB_BUCKET_HANDLER, "BlobBucketsController"),
|
|
2192
|
+
},
|
|
2193
|
+
{
|
|
2194
|
+
field: "preset",
|
|
2195
|
+
tomlKey: "preset",
|
|
2196
|
+
type: "string",
|
|
2197
|
+
emit: "whenSet",
|
|
2198
|
+
writableOn: BOTH,
|
|
2199
|
+
// #1020 — the honest access field. A rule-set-backed bucket reports
|
|
2200
|
+
// "custom", which is not written back as a preset.
|
|
2201
|
+
validation: handledBy("src/app-api/resource-types/blob-bucket.ts", "resolveBucketPreset"),
|
|
2202
|
+
},
|
|
2203
|
+
{
|
|
2204
|
+
field: "ruleSetId",
|
|
2205
|
+
tomlKey: "ruleSetId",
|
|
2206
|
+
type: "string",
|
|
2207
|
+
emit: "whenSet",
|
|
2208
|
+
writableOn: BOTH,
|
|
2209
|
+
// Existence-checked against AccessRuleSet; mutually exclusive with a
|
|
2210
|
+
// preset change in the same request.
|
|
2211
|
+
validation: handledBy(BLOB_BUCKET_HANDLER, "BlobBucketsController"),
|
|
2212
|
+
},
|
|
2213
|
+
],
|
|
2214
|
+
notExposed: {
|
|
2215
|
+
bucketId: {
|
|
2216
|
+
kind: "server-owned",
|
|
2217
|
+
note: "ULID assigned on create; never author-authored.",
|
|
2218
|
+
},
|
|
2219
|
+
appId: {
|
|
2220
|
+
kind: "server-owned",
|
|
2221
|
+
note: "Tenant scope, taken from the route; never on the wire.",
|
|
2222
|
+
},
|
|
2223
|
+
bucketKey: {
|
|
2224
|
+
kind: "structural",
|
|
2225
|
+
// Create carries it in the body; update does NOT — the key is
|
|
2226
|
+
// immutable (`blob-buckets-controller.ts` update) and the URL already
|
|
2227
|
+
// names the bucket, so a body carrying it is a 400 rather than a
|
|
2228
|
+
// rename that silently did nothing.
|
|
2229
|
+
requestModes: ["create"],
|
|
2230
|
+
note: "The bucket's identity. Comes from the TOML file name and is " +
|
|
2231
|
+
"attached by the caller, not spread from the [bucket] table. Its " +
|
|
2232
|
+
"TOML spelling is declared in tomlOnlyKeys as `key`.",
|
|
2233
|
+
},
|
|
2234
|
+
accessPolicy: {
|
|
2235
|
+
kind: "deprecated",
|
|
2236
|
+
note: "#1020 — the legacy access field, superseded by `preset`. Still " +
|
|
2237
|
+
"accepted on the wire and returned as a read alias, but authors " +
|
|
2238
|
+
"write `preset`, so pull does not emit it (writing both would " +
|
|
2239
|
+
"author the same decision twice).",
|
|
2240
|
+
},
|
|
2241
|
+
createdBy: {
|
|
2242
|
+
kind: "server-owned",
|
|
2243
|
+
note: "User who created the bucket; assigned server-side.",
|
|
2244
|
+
},
|
|
2245
|
+
createdAt: {
|
|
2246
|
+
kind: "server-owned",
|
|
2247
|
+
note: "Timestamp, assigned server-side.",
|
|
2248
|
+
},
|
|
2249
|
+
modifiedAt: {
|
|
2250
|
+
kind: "server-owned",
|
|
2251
|
+
note: "Timestamp, assigned server-side.",
|
|
2252
|
+
},
|
|
2253
|
+
},
|
|
2254
|
+
tomlOnlyKeys: {
|
|
2255
|
+
key: {
|
|
2256
|
+
kind: "structural",
|
|
2257
|
+
note: "The bucket key. `config pull` writes it for readability; push takes " +
|
|
2258
|
+
"the identity from the file name and attaches `bucketKey` itself.",
|
|
2259
|
+
},
|
|
2260
|
+
accessPolicy: {
|
|
2261
|
+
kind: "deprecated",
|
|
2262
|
+
note: "Accepted in existing files as the pre-#1020 spelling of `preset` " +
|
|
2263
|
+
"and forwarded on push, so an unmigrated file still pushes. Pull " +
|
|
2264
|
+
"writes `preset`.",
|
|
2265
|
+
},
|
|
2266
|
+
},
|
|
2267
|
+
responseOnlyKeys: {},
|
|
2268
|
+
},
|
|
2269
|
+
],
|
|
2270
|
+
// A bucket file is the `[bucket]` table and nothing else: its access model is
|
|
2271
|
+
// a preset or a rule-set reference, both scalars inside that table.
|
|
2272
|
+
tomlDocumentKeys: {},
|
|
2273
|
+
};
|
|
2274
|
+
// ── src/config-surface/email-template.ts ─────────────────────────────────
|
|
2275
|
+
/**
|
|
2276
|
+
* The `email-template` configuration object's definition (issue #2644, phase 2).
|
|
2277
|
+
*
|
|
2278
|
+
* `email-templates/<emailType>.toml` carries a single `[template]` table. The
|
|
2279
|
+
* object is an OVERRIDE of a built-in template: the detail response is
|
|
2280
|
+
* `{ emailType, hasOverride, override: {...}, default: {...} }`, and only the
|
|
2281
|
+
* `override` half is a field surface — the `default` half is what the platform
|
|
2282
|
+
* ships. Both are declared as response-only keys so `config pull`'s
|
|
2283
|
+
* unrecognized-key warning reports genuinely unknown fields rather than the
|
|
2284
|
+
* envelope.
|
|
2285
|
+
*
|
|
2286
|
+
* There is one write endpoint (`PUT …/email-templates/{emailType}`, an upsert),
|
|
2287
|
+
* so every field is writable on both modes.
|
|
2288
|
+
*/
|
|
2289
|
+
/**
|
|
2290
|
+
* Email types RETIRED by #2884, kept named rather than simply deleted.
|
|
2291
|
+
*
|
|
2292
|
+
* Email sign-in sends ONE email from the `email-sign-in` template, so
|
|
2293
|
+
* `magic-link` and `otp` are no longer rendered by any code path — which is
|
|
2294
|
+
* what makes deleting the link block from an `email-sign-in` override an
|
|
2295
|
+
* actual guarantee rather than a hope about which endpoint ran.
|
|
2296
|
+
*
|
|
2297
|
+
* A stored override for a retired type is NOT deleted: it stays listed and
|
|
2298
|
+
* readable (labelled retired, with the guidance below) so an app can find its
|
|
2299
|
+
* customization and migrate it, and deleting it still works. Everything that
|
|
2300
|
+
* would author one — the admin write/preview/test endpoints, `config create`,
|
|
2301
|
+
* a `config push --only` selector — refuses by name instead. Silent
|
|
2302
|
+
* non-rendering is the outcome all of that exists to avoid.
|
|
2303
|
+
*
|
|
2304
|
+
* It lives in the config surface, not beside the default templates, because
|
|
2305
|
+
* the CLI vendors this directory and needs the same sentence.
|
|
2306
|
+
*/
|
|
2307
|
+
export const RETIRED_EMAIL_TYPES = ["magic-link", "otp"];
|
|
2308
|
+
/** True when `emailType` is one of the retired sign-in types. */
|
|
2309
|
+
export function isRetiredEmailType(emailType) {
|
|
2310
|
+
return RETIRED_EMAIL_TYPES.includes(emailType);
|
|
2311
|
+
}
|
|
2312
|
+
/** What every surface says about a retired type, in one sentence. */
|
|
2313
|
+
export function retiredEmailTypeGuidance(emailType) {
|
|
2314
|
+
const carryOver = emailType === "otp"
|
|
2315
|
+
? "the code block (`{{code}}`, `{{expiryMinutes}}`)"
|
|
2316
|
+
: "the link block (`{{{magicLink}}}`), inside `{{#if magicLink}}`";
|
|
2317
|
+
return (`\`${emailType}\` is retired (#2884): email sign-in sends ONE email from ` +
|
|
2318
|
+
"the `email-sign-in` template, and no sign-in path renders this type any " +
|
|
2319
|
+
`more. Re-author ${carryOver} into your \`email-sign-in\` template, then ` +
|
|
2320
|
+
`delete this override.`);
|
|
2321
|
+
}
|
|
2322
|
+
const EMAIL_TEMPLATE_HANDLER = "src/admin-api.ts";
|
|
2323
|
+
export const EMAIL_TEMPLATE_SURFACE = {
|
|
2324
|
+
label: "email-template",
|
|
2325
|
+
tables: [
|
|
2326
|
+
{
|
|
2327
|
+
tomlPath: ["template"],
|
|
2328
|
+
repeated: false,
|
|
2329
|
+
model: "EmailTemplate",
|
|
2330
|
+
// Declaration order reproduces the pre-#2644 serializer's key order.
|
|
2331
|
+
fields: [
|
|
2332
|
+
{
|
|
2333
|
+
field: "subject",
|
|
2334
|
+
tomlKey: "subject",
|
|
2335
|
+
type: "string",
|
|
2336
|
+
emit: "always",
|
|
2337
|
+
writableOn: BOTH,
|
|
2338
|
+
// Size-capped and checked for unescaped `{{{var}}}` interpolation.
|
|
2339
|
+
validation: handledBy(EMAIL_TEMPLATE_HANDLER, "AdminAPI"),
|
|
2340
|
+
},
|
|
2341
|
+
{
|
|
2342
|
+
field: "htmlBody",
|
|
2343
|
+
tomlKey: "htmlBody",
|
|
2344
|
+
type: "string",
|
|
2345
|
+
emit: "whenSet",
|
|
2346
|
+
writableOn: BOTH,
|
|
2347
|
+
validation: handledBy(EMAIL_TEMPLATE_HANDLER, "AdminAPI"),
|
|
2348
|
+
},
|
|
2349
|
+
{
|
|
2350
|
+
field: "textBody",
|
|
2351
|
+
tomlKey: "textBody",
|
|
2352
|
+
type: "string",
|
|
2353
|
+
emit: "whenSet",
|
|
2354
|
+
writableOn: BOTH,
|
|
2355
|
+
validation: handledBy(EMAIL_TEMPLATE_HANDLER, "AdminAPI"),
|
|
2356
|
+
},
|
|
2357
|
+
],
|
|
2358
|
+
notExposed: {
|
|
2359
|
+
templateId: {
|
|
2360
|
+
kind: "server-owned",
|
|
2361
|
+
note: "ULID assigned on create; never author-authored.",
|
|
2362
|
+
},
|
|
2363
|
+
appId: {
|
|
2364
|
+
kind: "server-owned",
|
|
2365
|
+
note: "Tenant scope, taken from the route; never on the wire.",
|
|
2366
|
+
},
|
|
2367
|
+
emailType: {
|
|
2368
|
+
kind: "structural",
|
|
2369
|
+
// Stated, not implied: the upsert handler reads the type from the
|
|
2370
|
+
// route, so no request schema admits the key.
|
|
2371
|
+
requestModes: [],
|
|
2372
|
+
note: "The override's identity — which built-in email it replaces. Comes " +
|
|
2373
|
+
"from the TOML file name and travels in the URL path, not in the " +
|
|
2374
|
+
"request body. Its TOML spelling is declared in tomlOnlyKeys.",
|
|
2375
|
+
},
|
|
2376
|
+
createdBy: {
|
|
2377
|
+
kind: "server-owned",
|
|
2378
|
+
note: "Admin who created the override; assigned server-side.",
|
|
2379
|
+
},
|
|
2380
|
+
createdAt: {
|
|
2381
|
+
kind: "server-owned",
|
|
2382
|
+
note: "Timestamp, assigned server-side.",
|
|
2383
|
+
},
|
|
2384
|
+
modifiedAt: {
|
|
2385
|
+
kind: "server-owned",
|
|
2386
|
+
note: "Timestamp, assigned server-side.",
|
|
2387
|
+
},
|
|
2388
|
+
},
|
|
2389
|
+
tomlOnlyKeys: {
|
|
2390
|
+
emailType: {
|
|
2391
|
+
kind: "structural",
|
|
2392
|
+
note: "`config pull` writes the email type for readability; push takes it " +
|
|
2393
|
+
"from the file name and puts it in the URL, never the body.",
|
|
2394
|
+
},
|
|
2395
|
+
},
|
|
2396
|
+
responseOnlyKeys: {
|
|
2397
|
+
label: {
|
|
2398
|
+
kind: "server-owned",
|
|
2399
|
+
note: "Human-readable name of the built-in email this override replaces " +
|
|
2400
|
+
"(the emailType itself for a custom type). Presentation only.",
|
|
2401
|
+
},
|
|
2402
|
+
description: {
|
|
2403
|
+
kind: "server-owned",
|
|
2404
|
+
note: "What the built-in email is for, shown in the admin UI. Platform " +
|
|
2405
|
+
"copy, not an override field.",
|
|
2406
|
+
},
|
|
2407
|
+
isCustom: {
|
|
2408
|
+
kind: "server-owned",
|
|
2409
|
+
note: "Derived flag: whether the emailType is a custom one rather than a " +
|
|
2410
|
+
"built-in. Computed from the route, never authored.",
|
|
2411
|
+
},
|
|
2412
|
+
variables: {
|
|
2413
|
+
kind: "server-owned",
|
|
2414
|
+
note: "The template variables this email exposes, with their " +
|
|
2415
|
+
"descriptions — the platform's own list for built-ins, extracted " +
|
|
2416
|
+
"from the override's text for a custom type. Documentation, not a " +
|
|
2417
|
+
"field surface.",
|
|
2418
|
+
},
|
|
2419
|
+
hasOverride: {
|
|
2420
|
+
kind: "server-owned",
|
|
2421
|
+
note: "Derived flag: whether this app has replaced the built-in template. " +
|
|
2422
|
+
"Computed at serialize time; `config pull` writes only overrides.",
|
|
2423
|
+
},
|
|
2424
|
+
override: {
|
|
2425
|
+
kind: "structural",
|
|
2426
|
+
note: "The app's override fields, nested in the detail response. Flattened " +
|
|
2427
|
+
"into the [template] table on pull.",
|
|
2428
|
+
},
|
|
2429
|
+
default: {
|
|
2430
|
+
kind: "server-owned",
|
|
2431
|
+
note: "The platform's built-in copy of the template, returned for " +
|
|
2432
|
+
"comparison. Never authored and never written to TOML.",
|
|
2433
|
+
},
|
|
2434
|
+
},
|
|
2435
|
+
},
|
|
2436
|
+
],
|
|
2437
|
+
// An override file is the `[template]` table alone — subject and the two
|
|
2438
|
+
// bodies. The platform default it replaces is server-side, never authored.
|
|
2439
|
+
tomlDocumentKeys: {},
|
|
2440
|
+
};
|
|
2441
|
+
// ── src/config-surface/database-type-config.ts ───────────────────────────
|
|
2442
|
+
/**
|
|
2443
|
+
* The `database-type-config` configuration object's definition (issue #2644, phase 3).
|
|
2444
|
+
*
|
|
2445
|
+
* `database-type-configs/<databaseType>.toml` is the most structured file in the sync
|
|
2446
|
+
* slot: a `[type]` scalar table, plus four authored sub-trees that keep the
|
|
2447
|
+
* validators that already own them, per the spec's scope boundary —
|
|
2448
|
+
*
|
|
2449
|
+
* `[models.*]` → the stored `schema` TOML string (`parseSchemaToml`)
|
|
2450
|
+
* `[[operations]]` → `DatabaseTypeOperation` rows (`toml-params-validator.ts`)
|
|
2451
|
+
* `[[subscriptions]]` → `DatabaseTypeSubscription` rows (#803)
|
|
2452
|
+
* `[triggers]` → the trigger-rule tree (`validateTriggerRules`)
|
|
2453
|
+
* `[metadata]`/`secrets` → the declared-access manifest (#1304)
|
|
2454
|
+
*
|
|
2455
|
+
* `operations` and `subscriptions` are separate models with their own
|
|
2456
|
+
* endpoints, so they are not fields of `DatabaseTypeConfig` at all; `schema`,
|
|
2457
|
+
* `triggers` and `metadataManifest` ARE model fields and are declared
|
|
2458
|
+
* `structural` here, naming the channel and the validator, so their absence
|
|
2459
|
+
* from the `[type]` table reads as a decision rather than an omission.
|
|
2460
|
+
*
|
|
2461
|
+
* The exposed `[type]` scalars are exactly the file-OWNED ones (#1567): `sync
|
|
2462
|
+
* push` sends each as value-or-`null` so removing a line from the TOML clears
|
|
2463
|
+
* it server-side rather than leaving a stale value live. `DB_TYPE_OWNED_SCALARS`
|
|
2464
|
+
* used to state that list a second time in the CLI; it now reads this
|
|
2465
|
+
* definition.
|
|
2466
|
+
*/
|
|
2467
|
+
const DB_TYPE_HANDLER = "src/app-api/controllers/database-type-config-controller.ts";
|
|
2468
|
+
export const DATABASE_TYPE_SURFACE = {
|
|
2469
|
+
label: "database-type-config",
|
|
2470
|
+
tables: [
|
|
2471
|
+
{
|
|
2472
|
+
tomlPath: ["type"],
|
|
2473
|
+
repeated: false,
|
|
2474
|
+
model: "DatabaseTypeConfig",
|
|
2475
|
+
// Declaration order reproduces the pre-#2644 serializer's key order.
|
|
2476
|
+
fields: [
|
|
2477
|
+
{
|
|
2478
|
+
field: "databaseType",
|
|
2479
|
+
tomlKey: "databaseType",
|
|
2480
|
+
type: "string",
|
|
2481
|
+
emit: "always",
|
|
2482
|
+
writableOn: CREATE_ONLY,
|
|
2483
|
+
// The type's identity: required and `#`-free on create, taken from
|
|
2484
|
+
// the URL on update.
|
|
2485
|
+
validation: handledBy(DB_TYPE_HANDLER, "DatabaseTypeConfigController"),
|
|
2486
|
+
},
|
|
2487
|
+
{
|
|
2488
|
+
field: "ruleSetId",
|
|
2489
|
+
tomlKey: "ruleSetName",
|
|
2490
|
+
type: "string",
|
|
2491
|
+
emit: "always",
|
|
2492
|
+
writableOn: BOTH,
|
|
2493
|
+
// Authored as a portable name and sent as an id; the server checks
|
|
2494
|
+
// the set exists and has `resourceType: "database"`.
|
|
2495
|
+
validation: handledBy(DB_TYPE_HANDLER, "DatabaseTypeConfigController"),
|
|
2496
|
+
},
|
|
2497
|
+
{
|
|
2498
|
+
field: "metadataAccess",
|
|
2499
|
+
tomlKey: "celContextAccess",
|
|
2500
|
+
type: "string",
|
|
2501
|
+
emit: "whenSet",
|
|
2502
|
+
writableOn: BOTH,
|
|
2503
|
+
// CEL, parsed before storing (#1336). The legacy `metadataAccess`
|
|
2504
|
+
// spelling stays readable on push — see tomlOnlyKeys.
|
|
2505
|
+
validation: handledBy(DB_TYPE_HANDLER, "DatabaseTypeConfigController"),
|
|
2506
|
+
},
|
|
2507
|
+
{
|
|
2508
|
+
field: "defaultAccess",
|
|
2509
|
+
tomlKey: "defaultAccess",
|
|
2510
|
+
type: "string",
|
|
2511
|
+
emit: "whenSet",
|
|
2512
|
+
writableOn: BOTH,
|
|
2513
|
+
// CEL, plus the shared `md`-shape allowlist (#1523).
|
|
2514
|
+
validation: handledBy(DB_TYPE_HANDLER, "DatabaseTypeConfigController"),
|
|
2515
|
+
},
|
|
2516
|
+
{
|
|
2517
|
+
field: "autoPopulatedFields",
|
|
2518
|
+
tomlKey: "autoPopulatedFields",
|
|
2519
|
+
type: "json",
|
|
2520
|
+
emit: "whenSet",
|
|
2521
|
+
writableOn: BOTH,
|
|
2522
|
+
// #750 — shorthand and verbose forms both accepted, normalized by
|
|
2523
|
+
// `validateAndNormalizeAutoPopulatedFields`.
|
|
2524
|
+
validation: handledBy(DB_TYPE_HANDLER, "DatabaseTypeConfigController"),
|
|
2525
|
+
},
|
|
2526
|
+
{
|
|
2527
|
+
field: "timestamps",
|
|
2528
|
+
tomlKey: "timestamps",
|
|
2529
|
+
type: "json",
|
|
2530
|
+
emit: "whenSet",
|
|
2531
|
+
writableOn: BOTH,
|
|
2532
|
+
// #748 — `{ create?, update?, models? }`, normalized by
|
|
2533
|
+
// `validateAndNormalizeTimestamps`.
|
|
2534
|
+
validation: handledBy(DB_TYPE_HANDLER, "DatabaseTypeConfigController"),
|
|
2535
|
+
},
|
|
2536
|
+
],
|
|
2537
|
+
notExposed: {
|
|
2538
|
+
appId: {
|
|
2539
|
+
kind: "server-owned",
|
|
2540
|
+
note: "Tenant scope, taken from the route; never on the wire.",
|
|
2541
|
+
},
|
|
2542
|
+
triggers: {
|
|
2543
|
+
kind: "structural",
|
|
2544
|
+
note: "The trigger-rule tree, authored as the file's top-level " +
|
|
2545
|
+
"`[triggers]` table rather than a key inside [type]. Validated by " +
|
|
2546
|
+
"`validateTriggerRules` server-side and pushed whole; the file owns " +
|
|
2547
|
+
"it, so an absent table clears it (#1567).",
|
|
2548
|
+
},
|
|
2549
|
+
metadataManifest: {
|
|
2550
|
+
kind: "structural",
|
|
2551
|
+
note: "The declared-access manifest (#1304 P-C): authored as the file's " +
|
|
2552
|
+
"top-level `[metadata]` + `secrets` fragments, validated by " +
|
|
2553
|
+
"`validateDeclaredAccessManifest` before push.",
|
|
2554
|
+
},
|
|
2555
|
+
schema: {
|
|
2556
|
+
kind: "structural",
|
|
2557
|
+
note: "The type's model schema (#666), stored as a raw TOML string and " +
|
|
2558
|
+
"authored as the file's `[models.*]` blocks. Parsed by " +
|
|
2559
|
+
"`parseSchemaToml` server-side; it is a sub-tree with its own " +
|
|
2560
|
+
"prior-state discriminator on push, not a scalar.",
|
|
2561
|
+
},
|
|
2562
|
+
createdAt: {
|
|
2563
|
+
kind: "server-owned",
|
|
2564
|
+
note: "Timestamp, assigned server-side.",
|
|
2565
|
+
},
|
|
2566
|
+
modifiedAt: {
|
|
2567
|
+
kind: "server-owned",
|
|
2568
|
+
note: "Timestamp, assigned server-side.",
|
|
2569
|
+
},
|
|
2570
|
+
createdBy: {
|
|
2571
|
+
kind: "server-owned",
|
|
2572
|
+
note: "User who created the config; assigned server-side.",
|
|
2573
|
+
},
|
|
2574
|
+
},
|
|
2575
|
+
tomlOnlyKeys: {
|
|
2576
|
+
ruleSetId: {
|
|
2577
|
+
kind: "deprecated",
|
|
2578
|
+
note: "The legacy id-based rule-set reference. Still accepted on push " +
|
|
2579
|
+
"so an unmigrated file pushes unchanged; pull writes `ruleSetName`.",
|
|
2580
|
+
},
|
|
2581
|
+
metadataAccess: {
|
|
2582
|
+
kind: "deprecated",
|
|
2583
|
+
note: "The legacy spelling of `celContextAccess`. Read on push when the " +
|
|
2584
|
+
"newer key is absent (#1567 keeps `celContextAccess = \"\"` an " +
|
|
2585
|
+
"explicit clear); pull writes `celContextAccess`.",
|
|
2586
|
+
},
|
|
2587
|
+
},
|
|
2588
|
+
requestOnlyKeys: {
|
|
2589
|
+
pendingOpDeletes: {
|
|
2590
|
+
note: "Operation names this push is deleting, sent with the type-config " +
|
|
2591
|
+
"PATCH so the server validates the remaining schema against the " +
|
|
2592
|
+
"operations that will survive rather than the current set.",
|
|
2593
|
+
},
|
|
2594
|
+
pendingOpUpdates: {
|
|
2595
|
+
note: "#1336 — the post-push form of the operations this push rewrites " +
|
|
2596
|
+
"({ name, access, params }), so the child-op re-lint evaluates " +
|
|
2597
|
+
"each against what the push is about to store rather than the " +
|
|
2598
|
+
"stale row. Read by `type-config-access-lint.ts`, the same " +
|
|
2599
|
+
"look-ahead contract as `pendingOpDeletes` / `finalOpNames`.",
|
|
2600
|
+
},
|
|
2601
|
+
pendingOpUpserts: {
|
|
2602
|
+
modes: ["update"],
|
|
2603
|
+
note: "#2732 — the full post-push BODY of every operation this push " +
|
|
2604
|
+
"lands ({ name, type, modelName, access, definition, params }), so " +
|
|
2605
|
+
"the schema-edit gate lints a rewritten operation as rewritten " +
|
|
2606
|
+
"rather than as stored. That is what makes removing a model and " +
|
|
2607
|
+
"rewriting the operations that named it a single push. Read by " +
|
|
2608
|
+
"`pending-op-set.ts`, which honors an entry only when it is a " +
|
|
2609
|
+
"whole operation body whose name `finalOpNames` keeps — anything " +
|
|
2610
|
+
"less is ignored and the stored operation is gated instead. A " +
|
|
2611
|
+
"create has no stored operations to gate.",
|
|
2612
|
+
},
|
|
2613
|
+
finalOpNames: {
|
|
2614
|
+
note: "The operation names the file will end with, for the same " +
|
|
2615
|
+
"look-ahead validation as `pendingOpDeletes`.",
|
|
2616
|
+
},
|
|
2617
|
+
},
|
|
2618
|
+
responseOnlyKeys: {},
|
|
2619
|
+
},
|
|
2620
|
+
],
|
|
2621
|
+
tomlDocumentKeys: {
|
|
2622
|
+
models: {
|
|
2623
|
+
kind: "structural",
|
|
2624
|
+
note: "#666 — the `[models.<Name>.fields.*]` blocks, re-serialized into the " +
|
|
2625
|
+
"stored `schema` TOML string on push and merged back on pull.",
|
|
2626
|
+
},
|
|
2627
|
+
operations: {
|
|
2628
|
+
kind: "structural",
|
|
2629
|
+
note: "The `[[operations]]` array. Keeps the validator that already owns it " +
|
|
2630
|
+
"(`cli/src/lib/toml-params-validator.ts`) and its own endpoints.",
|
|
2631
|
+
},
|
|
2632
|
+
subscriptions: {
|
|
2633
|
+
kind: "structural",
|
|
2634
|
+
note: "#803 — the `[[subscriptions]]` array, applied through its own endpoints.",
|
|
2635
|
+
},
|
|
2636
|
+
triggers: {
|
|
2637
|
+
kind: "structural",
|
|
2638
|
+
note: "The type's authored trigger table, stored on the type config as " +
|
|
2639
|
+
"`triggers` and validated server-side.",
|
|
2640
|
+
},
|
|
2641
|
+
...DECLARED_ACCESS_MANIFEST_KEYS,
|
|
2642
|
+
},
|
|
2643
|
+
};
|
|
2644
|
+
// ── src/config-surface/rule-set.ts ───────────────────────────────────────
|
|
2645
|
+
/**
|
|
2646
|
+
* The `rule-set` configuration object's definition (issue #2644, phase 3).
|
|
2647
|
+
*
|
|
2648
|
+
* `rule-sets/<name>.toml` carries a `[ruleSet]` scalar table plus the authored
|
|
2649
|
+
* `[rules]` tree — `{ category: { operation: "<CEL>" } }` — which is declared
|
|
2650
|
+
* `structural`: it is a nested authored sub-tree with its own validator
|
|
2651
|
+
* (`validateRules`, server-side, against the set's `resourceType`), and the
|
|
2652
|
+
* spec's scope boundary keeps nested structure with the validator that already
|
|
2653
|
+
* owns it rather than re-modelling it as fields.
|
|
2654
|
+
*
|
|
2655
|
+
* `resourceType` is `create` only: `update` builds its `updates` map from
|
|
2656
|
+
* `name` / `description` / `rules` alone, so a rule set's resource type is
|
|
2657
|
+
* fixed at creation.
|
|
2658
|
+
*/
|
|
2659
|
+
const RULE_SET_HANDLER = "src/app-api/controllers/rule-sets-controller.ts";
|
|
2660
|
+
export const RULE_SET_SURFACE = {
|
|
2661
|
+
label: "rule-set",
|
|
2662
|
+
tables: [
|
|
2663
|
+
{
|
|
2664
|
+
tomlPath: ["ruleSet"],
|
|
2665
|
+
repeated: false,
|
|
2666
|
+
model: "AccessRuleSet",
|
|
2667
|
+
// Declaration order reproduces the pre-#2644 serializer's key order.
|
|
2668
|
+
fields: [
|
|
2669
|
+
{
|
|
2670
|
+
field: "name",
|
|
2671
|
+
tomlKey: "name",
|
|
2672
|
+
type: "string",
|
|
2673
|
+
emit: "always",
|
|
2674
|
+
writableOn: BOTH,
|
|
2675
|
+
// Required, trimmed, and non-empty on both modes.
|
|
2676
|
+
validation: handledBy(RULE_SET_HANDLER, "RuleSetsController"),
|
|
2677
|
+
},
|
|
2678
|
+
{
|
|
2679
|
+
field: "resourceType",
|
|
2680
|
+
tomlKey: "resourceType",
|
|
2681
|
+
type: "string",
|
|
2682
|
+
emit: "always",
|
|
2683
|
+
writableOn: CREATE_ONLY,
|
|
2684
|
+
// Checked against `getValidResourceTypes()`, and singleton types
|
|
2685
|
+
// (#1148) reject a second set. `update` never reads it.
|
|
2686
|
+
validation: handledBy(RULE_SET_HANDLER, "RuleSetsController"),
|
|
2687
|
+
},
|
|
2688
|
+
{
|
|
2689
|
+
field: "description",
|
|
2690
|
+
tomlKey: "description",
|
|
2691
|
+
type: "string",
|
|
2692
|
+
emit: "whenSet",
|
|
2693
|
+
writableOn: BOTH,
|
|
2694
|
+
validation: "passthrough",
|
|
2695
|
+
},
|
|
2696
|
+
],
|
|
2697
|
+
notExposed: {
|
|
2698
|
+
ruleSetId: {
|
|
2699
|
+
kind: "server-owned",
|
|
2700
|
+
note: "ULID assigned on create; the file is keyed by name instead.",
|
|
2701
|
+
},
|
|
2702
|
+
appId: {
|
|
2703
|
+
kind: "server-owned",
|
|
2704
|
+
note: "Tenant scope, taken from the route; never on the wire.",
|
|
2705
|
+
},
|
|
2706
|
+
rules: {
|
|
2707
|
+
kind: "structural",
|
|
2708
|
+
note: "The authored `[rules]` tree, a sibling table of [ruleSet] rather " +
|
|
2709
|
+
"than a key inside it. Its shape is category → operation → CEL, " +
|
|
2710
|
+
"validated by `validateRules` in rule-sets-controller.ts against " +
|
|
2711
|
+
"the set's resourceType; `config push` sends it whole.",
|
|
2712
|
+
},
|
|
2713
|
+
version: {
|
|
2714
|
+
kind: "server-owned",
|
|
2715
|
+
note: "Bumped by the server whenever `rules` changes; never authored.",
|
|
2716
|
+
},
|
|
2717
|
+
createdAt: {
|
|
2718
|
+
kind: "server-owned",
|
|
2719
|
+
note: "Timestamp, assigned server-side.",
|
|
2720
|
+
},
|
|
2721
|
+
modifiedAt: {
|
|
2722
|
+
kind: "server-owned",
|
|
2723
|
+
note: "Timestamp, assigned server-side.",
|
|
2724
|
+
},
|
|
2725
|
+
createdBy: {
|
|
2726
|
+
kind: "server-owned",
|
|
2727
|
+
note: "User who created the rule set; assigned server-side.",
|
|
2728
|
+
},
|
|
2729
|
+
},
|
|
2730
|
+
tomlOnlyKeys: {},
|
|
2731
|
+
responseOnlyKeys: {},
|
|
2732
|
+
},
|
|
2733
|
+
],
|
|
2734
|
+
tomlDocumentKeys: {
|
|
2735
|
+
rules: {
|
|
2736
|
+
kind: "structural",
|
|
2737
|
+
note: "The `[rules.<category>]` tree of CEL expressions — the rule set's " +
|
|
2738
|
+
"whole point. Keeps its own validator and travels beside [ruleSet].",
|
|
2739
|
+
},
|
|
2740
|
+
},
|
|
2741
|
+
};
|
|
2742
|
+
// ── src/config-surface/group-type-config.ts ──────────────────────────────
|
|
2743
|
+
/**
|
|
2744
|
+
* The `group-type-config` configuration object's definition (issue #2644,
|
|
2745
|
+
* phase 3).
|
|
2746
|
+
*
|
|
2747
|
+
* `group-type-configs/<groupType>.toml` carries a single `[groupTypeConfig]`
|
|
2748
|
+
* table, plus the declared-access manifest's top-level `[metadata]` / `secrets`
|
|
2749
|
+
* fragments — which are `structural`: authored elsewhere in the file, with
|
|
2750
|
+
* their own validator (`validateDeclaredAccessManifest` locally,
|
|
2751
|
+
* `validateManifestShape` server-side).
|
|
2752
|
+
*
|
|
2753
|
+
* The rule-set reference is stored as `ruleSetId` but AUTHORED as
|
|
2754
|
+
* `ruleSetName`, because a name is portable across apps and an id is not. The
|
|
2755
|
+
* name → id resolution is the CLI's (`config push` resolves against the app's
|
|
2756
|
+
* rule sets); the server validates that the id exists and has
|
|
2757
|
+
* `resourceType: "group"`. The legacy id spelling stays accepted on push and is
|
|
2758
|
+
* declared in `tomlOnlyKeys`.
|
|
2759
|
+
*/
|
|
2760
|
+
const GROUP_TYPE_HANDLER = "src/app-api/controllers/group-type-config-controller.ts";
|
|
2761
|
+
export const GROUP_TYPE_CONFIG_SURFACE = {
|
|
2762
|
+
label: "group-type-config",
|
|
2763
|
+
tables: [
|
|
2764
|
+
{
|
|
2765
|
+
tomlPath: ["groupTypeConfig"],
|
|
2766
|
+
repeated: false,
|
|
2767
|
+
model: "GroupTypeConfig",
|
|
2768
|
+
// Declaration order reproduces the pre-#2644 serializer's key order.
|
|
2769
|
+
fields: [
|
|
2770
|
+
{
|
|
2771
|
+
field: "groupType",
|
|
2772
|
+
tomlKey: "groupType",
|
|
2773
|
+
type: "string",
|
|
2774
|
+
emit: "always",
|
|
2775
|
+
writableOn: CREATE_ONLY,
|
|
2776
|
+
// The config's identity: required and `#`-free on create, taken from
|
|
2777
|
+
// the URL on update.
|
|
2778
|
+
validation: handledBy(GROUP_TYPE_HANDLER, "GroupTypeConfigController"),
|
|
2779
|
+
},
|
|
2780
|
+
{
|
|
2781
|
+
field: "ruleSetId",
|
|
2782
|
+
tomlKey: "ruleSetName",
|
|
2783
|
+
type: "string",
|
|
2784
|
+
emit: "always",
|
|
2785
|
+
writableOn: BOTH,
|
|
2786
|
+
// Authored as a name, sent as an id; existence and resourceType are
|
|
2787
|
+
// checked server-side, and an empty value clears the reference.
|
|
2788
|
+
validation: handledBy(GROUP_TYPE_HANDLER, "GroupTypeConfigController"),
|
|
2789
|
+
},
|
|
2790
|
+
{
|
|
2791
|
+
field: "autoAddCreator",
|
|
2792
|
+
tomlKey: "autoAddCreator",
|
|
2793
|
+
type: "boolean",
|
|
2794
|
+
emit: "always",
|
|
2795
|
+
writableOn: BOTH,
|
|
2796
|
+
// Defaults to `true` when absent on create, and is coerced with `!!`
|
|
2797
|
+
// on update — not a stored-as-is passthrough.
|
|
2798
|
+
validation: handledBy(GROUP_TYPE_HANDLER, "GroupTypeConfigController"),
|
|
2799
|
+
},
|
|
2800
|
+
],
|
|
2801
|
+
notExposed: {
|
|
2802
|
+
appId: {
|
|
2803
|
+
kind: "server-owned",
|
|
2804
|
+
note: "Tenant scope, taken from the route; never on the wire.",
|
|
2805
|
+
},
|
|
2806
|
+
metadataManifest: {
|
|
2807
|
+
kind: "structural",
|
|
2808
|
+
note: "The declared-access manifest (#1350, extending #1304): authored " +
|
|
2809
|
+
"as the file's top-level `[metadata]` + `secrets` fragments, not " +
|
|
2810
|
+
"as a key inside [groupTypeConfig]. Validated by " +
|
|
2811
|
+
"`validateDeclaredAccessManifest` (cli/src/lib/toml-metadata-config.ts) " +
|
|
2812
|
+
"before push and `validateManifestShape` at persist time.",
|
|
2813
|
+
},
|
|
2814
|
+
createdAt: {
|
|
2815
|
+
kind: "server-owned",
|
|
2816
|
+
note: "Timestamp, assigned server-side.",
|
|
2817
|
+
},
|
|
2818
|
+
modifiedAt: {
|
|
2819
|
+
kind: "server-owned",
|
|
2820
|
+
note: "Timestamp, assigned server-side.",
|
|
2821
|
+
},
|
|
2822
|
+
createdBy: {
|
|
2823
|
+
kind: "server-owned",
|
|
2824
|
+
note: "User who created the config; assigned server-side.",
|
|
2825
|
+
},
|
|
2826
|
+
},
|
|
2827
|
+
tomlOnlyKeys: {
|
|
2828
|
+
ruleSetId: {
|
|
2829
|
+
kind: "deprecated",
|
|
2830
|
+
note: "The legacy id-based rule-set reference. Still accepted on push " +
|
|
2831
|
+
"so an unmigrated file pushes unchanged; pull writes `ruleSetName`.",
|
|
2832
|
+
},
|
|
2833
|
+
},
|
|
2834
|
+
responseOnlyKeys: {},
|
|
2835
|
+
},
|
|
2836
|
+
],
|
|
2837
|
+
tomlDocumentKeys: { ...DECLARED_ACCESS_MANIFEST_KEYS },
|
|
2838
|
+
};
|
|
2839
|
+
// ── src/config-surface/collection-type-config.ts ─────────────────────────
|
|
2840
|
+
/**
|
|
2841
|
+
* The `collection-type-config` configuration object's definition (issue #2644,
|
|
2842
|
+
* phase 3).
|
|
2843
|
+
*
|
|
2844
|
+
* `collection-type-configs/<collectionType>.toml` carries a single
|
|
2845
|
+
* `[collectionTypeConfig]` table plus the declared-access manifest's top-level
|
|
2846
|
+
* `[metadata]` / `secrets` fragments (declared `structural`, as on every other
|
|
2847
|
+
* config that carries one).
|
|
2848
|
+
*
|
|
2849
|
+
* Same shape as `group-type-config` minus `autoAddCreator`: the rule-set
|
|
2850
|
+
* reference is stored as `ruleSetId` and authored as `ruleSetName`, and the
|
|
2851
|
+
* server checks the referenced set has `resourceType: "collection"`.
|
|
2852
|
+
*/
|
|
2853
|
+
const COLLECTION_TYPE_HANDLER = "src/app-api/controllers/collection-type-config-controller.ts";
|
|
2854
|
+
export const COLLECTION_TYPE_CONFIG_SURFACE = {
|
|
2855
|
+
label: "collection-type-config",
|
|
2856
|
+
tables: [
|
|
2857
|
+
{
|
|
2858
|
+
tomlPath: ["collectionTypeConfig"],
|
|
2859
|
+
repeated: false,
|
|
2860
|
+
model: "CollectionTypeConfig",
|
|
2861
|
+
// Declaration order reproduces the pre-#2644 serializer's key order.
|
|
2862
|
+
fields: [
|
|
2863
|
+
{
|
|
2864
|
+
field: "collectionType",
|
|
2865
|
+
tomlKey: "collectionType",
|
|
2866
|
+
type: "string",
|
|
2867
|
+
emit: "always",
|
|
2868
|
+
writableOn: CREATE_ONLY,
|
|
2869
|
+
// The config's identity: required on create, taken from the URL on
|
|
2870
|
+
// update.
|
|
2871
|
+
validation: handledBy(COLLECTION_TYPE_HANDLER, "CollectionTypeConfigController"),
|
|
2872
|
+
},
|
|
2873
|
+
{
|
|
2874
|
+
field: "ruleSetId",
|
|
2875
|
+
tomlKey: "ruleSetName",
|
|
2876
|
+
type: "string",
|
|
2877
|
+
emit: "always",
|
|
2878
|
+
writableOn: BOTH,
|
|
2879
|
+
// Authored as a name, sent as an id; existence and resourceType are
|
|
2880
|
+
// checked server-side, and an empty value clears the reference.
|
|
2881
|
+
validation: handledBy(COLLECTION_TYPE_HANDLER, "CollectionTypeConfigController"),
|
|
2882
|
+
},
|
|
2883
|
+
],
|
|
2884
|
+
notExposed: {
|
|
2885
|
+
appId: {
|
|
2886
|
+
kind: "server-owned",
|
|
2887
|
+
note: "Tenant scope, taken from the route; never on the wire.",
|
|
2888
|
+
},
|
|
2889
|
+
metadataManifest: {
|
|
2890
|
+
kind: "structural",
|
|
2891
|
+
note: "The declared-access manifest (#1304 P-C): authored as the file's " +
|
|
2892
|
+
"top-level `[metadata]` + `secrets` fragments, not as a key inside " +
|
|
2893
|
+
"[collectionTypeConfig]. Validated by " +
|
|
2894
|
+
"`validateDeclaredAccessManifest` (cli/src/lib/toml-metadata-config.ts) " +
|
|
2895
|
+
"before push and `validateManifestShape` at persist time.",
|
|
2896
|
+
},
|
|
2897
|
+
createdAt: {
|
|
2898
|
+
kind: "server-owned",
|
|
2899
|
+
note: "Timestamp, assigned server-side.",
|
|
2900
|
+
},
|
|
2901
|
+
modifiedAt: {
|
|
2902
|
+
kind: "server-owned",
|
|
2903
|
+
note: "Timestamp, assigned server-side.",
|
|
2904
|
+
},
|
|
2905
|
+
createdBy: {
|
|
2906
|
+
kind: "server-owned",
|
|
2907
|
+
note: "User who created the config; assigned server-side.",
|
|
2908
|
+
},
|
|
2909
|
+
},
|
|
2910
|
+
tomlOnlyKeys: {
|
|
2911
|
+
ruleSetId: {
|
|
2912
|
+
kind: "deprecated",
|
|
2913
|
+
note: "The legacy id-based rule-set reference. Still accepted on push " +
|
|
2914
|
+
"so an unmigrated file pushes unchanged; pull writes `ruleSetName`.",
|
|
2915
|
+
},
|
|
2916
|
+
},
|
|
2917
|
+
responseOnlyKeys: {},
|
|
2918
|
+
},
|
|
2919
|
+
],
|
|
2920
|
+
tomlDocumentKeys: { ...DECLARED_ACCESS_MANIFEST_KEYS },
|
|
2921
|
+
};
|
|
2922
|
+
// ── src/config-surface/metadata-category-config.ts ───────────────────────
|
|
2923
|
+
/**
|
|
2924
|
+
* The `metadata-category-config` configuration object's definition (issue
|
|
2925
|
+
* #2644, phase 3).
|
|
2926
|
+
*
|
|
2927
|
+
* `metadata-category-configs/<resourceType>-<category>.toml` carries a single
|
|
2928
|
+
* `[metadataCategoryConfig]` table. Its identity is the `resourceType` /
|
|
2929
|
+
* `category` PAIR read out of the file's content (not from the file name), and
|
|
2930
|
+
* both halves are part of the table — which is why they are exposed fields and
|
|
2931
|
+
* `categoryKey`, the stored `resourceType#category` composite, is not.
|
|
2932
|
+
*
|
|
2933
|
+
* There is one write path (`PUT …/metadata/categories/{resourceType}/{category}`
|
|
2934
|
+
* and its POST twin, both an upsert through
|
|
2935
|
+
* `MetadataController.upsertCategoryConfig`), so every field is writable on
|
|
2936
|
+
* both modes.
|
|
2937
|
+
*
|
|
2938
|
+
* `schema` is exposed rather than structural: unlike workflow `steps` or a
|
|
2939
|
+
* database type's `[models]` blocks it is a single JSON value the server
|
|
2940
|
+
* returns parsed and stores whole, and `config pull` emits it as nested
|
|
2941
|
+
* `[metadataCategoryConfig.schema.fields.*]` tables. Its CONTENT is
|
|
2942
|
+
* handler-owned — `validateCategorySchema` mirrors the server's field-type,
|
|
2943
|
+
* enum and `unique` rules — but the field itself round-trips as one value.
|
|
2944
|
+
*/
|
|
2945
|
+
const METADATA_HANDLER = "src/app-api/controllers/metadata-controller.ts";
|
|
2946
|
+
const CLI_METADATA_TOML = "cli/src/lib/toml-metadata-config.ts";
|
|
2947
|
+
export const METADATA_CATEGORY_CONFIG_SURFACE = {
|
|
2948
|
+
label: "metadata-category-config",
|
|
2949
|
+
tables: [
|
|
2950
|
+
{
|
|
2951
|
+
tomlPath: ["metadataCategoryConfig"],
|
|
2952
|
+
repeated: false,
|
|
2953
|
+
model: "MetadataCategoryConfig",
|
|
2954
|
+
// Declaration order reproduces the pre-#2644 serializer's key order, so
|
|
2955
|
+
// the scalars stay ahead of the nested `[…schema.fields.*]` tables.
|
|
2956
|
+
fields: [
|
|
2957
|
+
{
|
|
2958
|
+
field: "resourceType",
|
|
2959
|
+
tomlKey: "resourceType",
|
|
2960
|
+
type: "string",
|
|
2961
|
+
emit: "always",
|
|
2962
|
+
writableOn: BOTH,
|
|
2963
|
+
// Half of the category's identity; required on every upsert.
|
|
2964
|
+
validation: handledBy(METADATA_HANDLER, "MetadataController"),
|
|
2965
|
+
},
|
|
2966
|
+
{
|
|
2967
|
+
field: "category",
|
|
2968
|
+
tomlKey: "category",
|
|
2969
|
+
type: "string",
|
|
2970
|
+
emit: "always",
|
|
2971
|
+
writableOn: BOTH,
|
|
2972
|
+
// The other half of the identity; required on every upsert.
|
|
2973
|
+
validation: handledBy(METADATA_HANDLER, "MetadataController"),
|
|
2974
|
+
},
|
|
2975
|
+
{
|
|
2976
|
+
field: "readRule",
|
|
2977
|
+
tomlKey: "readRule",
|
|
2978
|
+
type: "string",
|
|
2979
|
+
emit: "whenSet",
|
|
2980
|
+
writableOn: BOTH,
|
|
2981
|
+
// CEL, parsed and stored or nulled.
|
|
2982
|
+
validation: handledBy(METADATA_HANDLER, "MetadataController"),
|
|
2983
|
+
},
|
|
2984
|
+
{
|
|
2985
|
+
field: "writeRule",
|
|
2986
|
+
tomlKey: "writeRule",
|
|
2987
|
+
type: "string",
|
|
2988
|
+
emit: "whenSet",
|
|
2989
|
+
writableOn: BOTH,
|
|
2990
|
+
validation: handledBy(METADATA_HANDLER, "MetadataController"),
|
|
2991
|
+
},
|
|
2992
|
+
{
|
|
2993
|
+
field: "description",
|
|
2994
|
+
tomlKey: "description",
|
|
2995
|
+
type: "string",
|
|
2996
|
+
emit: "whenSet",
|
|
2997
|
+
writableOn: BOTH,
|
|
2998
|
+
validation: "passthrough",
|
|
2999
|
+
},
|
|
3000
|
+
{
|
|
3001
|
+
field: "schema",
|
|
3002
|
+
tomlKey: "schema",
|
|
3003
|
+
type: "json",
|
|
3004
|
+
emit: "always",
|
|
3005
|
+
writableOn: BOTH,
|
|
3006
|
+
// Required on every upsert, and an absent one is written as
|
|
3007
|
+
// `{ fields: {} }` so the file stays pushable. Field types, enums and
|
|
3008
|
+
// the single-`unique` rule are checked locally before push and again
|
|
3009
|
+
// server-side.
|
|
3010
|
+
validation: handledBy(CLI_METADATA_TOML, "validateCategorySchema"),
|
|
3011
|
+
},
|
|
3012
|
+
],
|
|
3013
|
+
notExposed: {
|
|
3014
|
+
appId: {
|
|
3015
|
+
kind: "server-owned",
|
|
3016
|
+
note: "Tenant scope, taken from the route; never on the wire.",
|
|
3017
|
+
},
|
|
3018
|
+
categoryKey: {
|
|
3019
|
+
kind: "server-owned",
|
|
3020
|
+
note: "The stored `resourceType#category` composite. Derived from the " +
|
|
3021
|
+
"two exposed identity fields; writing it too would author the same " +
|
|
3022
|
+
"decision twice.",
|
|
3023
|
+
},
|
|
3024
|
+
metadataManifest: {
|
|
3025
|
+
kind: "structural",
|
|
3026
|
+
note: "The declared-access manifest (#1420 phase 2): authored as the " +
|
|
3027
|
+
"file's top-level `[metadata]` + `secrets` fragments, not as a key " +
|
|
3028
|
+
"inside [metadataCategoryConfig].",
|
|
3029
|
+
},
|
|
3030
|
+
uniqueField: {
|
|
3031
|
+
kind: "server-owned",
|
|
3032
|
+
note: "Derived from the schema's single `unique: true` field (#1361) and " +
|
|
3033
|
+
"maintained by the server's reverse-resolve index; authors declare " +
|
|
3034
|
+
"`unique` on the field, not this pointer.",
|
|
3035
|
+
},
|
|
3036
|
+
schemaVersion: {
|
|
3037
|
+
kind: "server-owned",
|
|
3038
|
+
note: "Bumped by the server when the schema changes; never authored.",
|
|
3039
|
+
},
|
|
3040
|
+
generation: {
|
|
3041
|
+
kind: "server-owned",
|
|
3042
|
+
note: "Server-managed rewrite counter for the category's rows.",
|
|
3043
|
+
},
|
|
3044
|
+
createdAt: {
|
|
3045
|
+
kind: "server-owned",
|
|
3046
|
+
note: "Timestamp, assigned server-side.",
|
|
3047
|
+
},
|
|
3048
|
+
modifiedAt: {
|
|
3049
|
+
kind: "server-owned",
|
|
3050
|
+
note: "Timestamp, assigned server-side.",
|
|
3051
|
+
},
|
|
3052
|
+
createdBy: {
|
|
3053
|
+
kind: "server-owned",
|
|
3054
|
+
note: "User who created the category; assigned server-side.",
|
|
3055
|
+
},
|
|
3056
|
+
},
|
|
3057
|
+
tomlOnlyKeys: {},
|
|
3058
|
+
responseOnlyKeys: {},
|
|
3059
|
+
},
|
|
3060
|
+
],
|
|
3061
|
+
tomlDocumentKeys: { ...DECLARED_ACCESS_MANIFEST_KEYS },
|
|
3062
|
+
};
|
|
3063
|
+
// ── src/config-surface/transform.ts ──────────────────────────────────────
|
|
3064
|
+
/**
|
|
3065
|
+
* The `transform` configuration object's definition (issue #2644, phase 3).
|
|
3066
|
+
*
|
|
3067
|
+
* Transforms are the one synced type with NO TOML field table: a transform is
|
|
3068
|
+
* `transforms/<name>.rhai`, a Rhai source file, and its whole authored surface
|
|
3069
|
+
* is the script body. `config pull` writes the active `ScriptConfig`'s body and
|
|
3070
|
+
* `config push` sends it back; there is no key/value table to define, and the
|
|
3071
|
+
* `Script` / `ScriptConfig` scalars around it (name, description, inputSchema,
|
|
3072
|
+
* limits, status) are not authorable through the sync slot today.
|
|
3073
|
+
*
|
|
3074
|
+
* The entry exists so that is a DECISION rather than an absence. Criterion 3's
|
|
3075
|
+
* registry guard reads `SYNC_RESOURCE_TYPES` and requires a surface per label;
|
|
3076
|
+
* without this module `transform` would have to sit in an exemption list, which
|
|
3077
|
+
* is precisely the "absent from a hand-written list" failure mode this epic
|
|
3078
|
+
* exists to end.
|
|
3079
|
+
*/
|
|
3080
|
+
export const TRANSFORM_SURFACE = {
|
|
3081
|
+
label: "transform",
|
|
3082
|
+
tables: [],
|
|
3083
|
+
// No TOML file at all, so no top-level TOML keys either — stated rather than
|
|
3084
|
+
// left to `noFieldTable` to imply.
|
|
3085
|
+
tomlDocumentKeys: {},
|
|
3086
|
+
noFieldTable: {
|
|
3087
|
+
note: "A transform is a Rhai source file (`transforms/<name>.rhai`); its " +
|
|
3088
|
+
"authored surface is the script body, which config pull writes from the " +
|
|
3089
|
+
"active ScriptConfig and config push sends back whole. The Script / " +
|
|
3090
|
+
"ScriptConfig scalars (name, description, inputSchema, limits, status) " +
|
|
3091
|
+
"are managed through `primitive transforms`, not through a TOML table, " +
|
|
3092
|
+
"so this object has no field surface to define. Its sidecar test cases " +
|
|
3093
|
+
"(`transforms/<name>.tests/`) are the separate `test-case` surface.",
|
|
3094
|
+
},
|
|
3095
|
+
};
|
|
3096
|
+
// ── src/config-surface/test-case.ts ──────────────────────────────────────
|
|
3097
|
+
/**
|
|
3098
|
+
* The `test-case` configuration object's definition (issue #2644, phase 3).
|
|
3099
|
+
*
|
|
3100
|
+
* Test cases round-trip without being one of the per-entity directories: they
|
|
3101
|
+
* live in a sidecar `<key>.tests/` folder beside the block they test — a
|
|
3102
|
+
* prompt, a workflow, a transform or (since #2769) an integration — one
|
|
3103
|
+
* `[test]` table per file. They are not
|
|
3104
|
+
* a `SYNC_RESOURCE_TYPES` label (that table is per-entity directory metadata),
|
|
3105
|
+
* which is exactly why the spec names them explicitly: nothing that syncs sits
|
|
3106
|
+
* outside the registry.
|
|
3107
|
+
*
|
|
3108
|
+
* Three fields are stored as ids and AUTHORED as keys — `configId` /
|
|
3109
|
+
* `evaluatorPromptId` / `evaluatorConfigId` become `configName` /
|
|
3110
|
+
* `evaluatorPromptKey` / `evaluatorConfigName` — so a test file is portable
|
|
3111
|
+
* across apps. The legacy id spellings stay accepted on push and are declared
|
|
3112
|
+
* in `tomlOnlyKeys`.
|
|
3113
|
+
*
|
|
3114
|
+
* Three more are stored as JSON strings and carried in TOML as JSON strings
|
|
3115
|
+
* (`inputVariables`, `expectedOutputContains`, `expectedJsonSubset`) while the
|
|
3116
|
+
* WIRE shape is the parsed value. That asymmetry is the handler's, not the
|
|
3117
|
+
* definition's: `parseTestCaseToml` parses on push and the admin API
|
|
3118
|
+
* re-stringifies on store.
|
|
3119
|
+
*/
|
|
3120
|
+
const TEST_CASE_HANDLER = "src/admin-api.ts";
|
|
3121
|
+
const CLI_SYNC = "cli/src/commands/sync.ts";
|
|
3122
|
+
export const TEST_CASE_SURFACE = {
|
|
3123
|
+
label: "test-case",
|
|
3124
|
+
tables: [
|
|
3125
|
+
{
|
|
3126
|
+
tomlPath: ["test"],
|
|
3127
|
+
repeated: false,
|
|
3128
|
+
model: "BlockTestCase",
|
|
3129
|
+
// Declaration order reproduces the pre-#2644 serializer's key order.
|
|
3130
|
+
fields: [
|
|
3131
|
+
{
|
|
3132
|
+
field: "name",
|
|
3133
|
+
tomlKey: "name",
|
|
3134
|
+
type: "string",
|
|
3135
|
+
emit: "always",
|
|
3136
|
+
writableOn: BOTH,
|
|
3137
|
+
// Required and trimmed on create; an empty name is a 400.
|
|
3138
|
+
validation: handledBy(TEST_CASE_HANDLER, "AdminAPI"),
|
|
3139
|
+
},
|
|
3140
|
+
{
|
|
3141
|
+
field: "description",
|
|
3142
|
+
tomlKey: "description",
|
|
3143
|
+
type: "string",
|
|
3144
|
+
emit: "always",
|
|
3145
|
+
writableOn: BOTH,
|
|
3146
|
+
validation: "passthrough",
|
|
3147
|
+
},
|
|
3148
|
+
{
|
|
3149
|
+
field: "inputVariables",
|
|
3150
|
+
tomlKey: "inputVariables",
|
|
3151
|
+
type: "json",
|
|
3152
|
+
emit: "always",
|
|
3153
|
+
writableOn: BOTH,
|
|
3154
|
+
// Required. TOML carries the JSON text; the wire carries the parsed
|
|
3155
|
+
// object, which the server re-stringifies for storage.
|
|
3156
|
+
validation: handledBy(CLI_SYNC, "parseTestCaseToml"),
|
|
3157
|
+
},
|
|
3158
|
+
{
|
|
3159
|
+
field: "configId",
|
|
3160
|
+
tomlKey: "configName",
|
|
3161
|
+
type: "string",
|
|
3162
|
+
emit: "always",
|
|
3163
|
+
writableOn: BOTH,
|
|
3164
|
+
// Authored as the config's NAME and resolved to an id at push time.
|
|
3165
|
+
validation: handledBy(CLI_SYNC, "parseTestCaseToml"),
|
|
3166
|
+
},
|
|
3167
|
+
{
|
|
3168
|
+
field: "evaluatorPromptId",
|
|
3169
|
+
tomlKey: "evaluatorPromptKey",
|
|
3170
|
+
type: "string",
|
|
3171
|
+
emit: "always",
|
|
3172
|
+
writableOn: BOTH,
|
|
3173
|
+
// Authored as the judge prompt's KEY and resolved to an id at push.
|
|
3174
|
+
validation: handledBy(CLI_SYNC, "parseTestCaseToml"),
|
|
3175
|
+
},
|
|
3176
|
+
{
|
|
3177
|
+
field: "evaluatorConfigId",
|
|
3178
|
+
tomlKey: "evaluatorConfigName",
|
|
3179
|
+
type: "string",
|
|
3180
|
+
emit: "always",
|
|
3181
|
+
writableOn: BOTH,
|
|
3182
|
+
validation: handledBy(CLI_SYNC, "parseTestCaseToml"),
|
|
3183
|
+
},
|
|
3184
|
+
{
|
|
3185
|
+
field: "expectedOutputPattern",
|
|
3186
|
+
tomlKey: "expectedOutputPattern",
|
|
3187
|
+
type: "string",
|
|
3188
|
+
emit: "always",
|
|
3189
|
+
writableOn: BOTH,
|
|
3190
|
+
validation: "passthrough",
|
|
3191
|
+
},
|
|
3192
|
+
{
|
|
3193
|
+
field: "expectedOutputContains",
|
|
3194
|
+
tomlKey: "expectedOutputContains",
|
|
3195
|
+
type: "json",
|
|
3196
|
+
emit: "always",
|
|
3197
|
+
writableOn: BOTH,
|
|
3198
|
+
// JSON array of strings; TOML carries the JSON text.
|
|
3199
|
+
validation: handledBy(CLI_SYNC, "parseTestCaseToml"),
|
|
3200
|
+
},
|
|
3201
|
+
{
|
|
3202
|
+
field: "expectedJsonSubset",
|
|
3203
|
+
tomlKey: "expectedJsonSubset",
|
|
3204
|
+
type: "json",
|
|
3205
|
+
emit: "always",
|
|
3206
|
+
writableOn: BOTH,
|
|
3207
|
+
// JSON object, deep-partial-matched against the parsed output.
|
|
3208
|
+
validation: handledBy(CLI_SYNC, "parseTestCaseToml"),
|
|
3209
|
+
},
|
|
3210
|
+
],
|
|
3211
|
+
notExposed: {
|
|
3212
|
+
testCaseId: {
|
|
3213
|
+
kind: "server-owned",
|
|
3214
|
+
note: "ULID assigned on create. The file name carries the identity in " +
|
|
3215
|
+
"the committed tree — as `key`, since #2896 — so this id is the " +
|
|
3216
|
+
"server's handle, not what a sidecar is reconciled by.",
|
|
3217
|
+
},
|
|
3218
|
+
key: {
|
|
3219
|
+
kind: "structural",
|
|
3220
|
+
requestModes: ["create", "update"],
|
|
3221
|
+
note: "The sidecar's file name (basename without `.toml`) — the case's " +
|
|
3222
|
+
"identity in the committed tree (#2896). It travels in the BODY " +
|
|
3223
|
+
"so a clean checkout with no `.primitive-sync.json` reconciles " +
|
|
3224
|
+
"instead of creating a duplicate of every case; create stamps it " +
|
|
3225
|
+
"and update backfills it onto a legacy case. Never inside the " +
|
|
3226
|
+
"`[test]` table: sidecar bytes and the content hash are unchanged " +
|
|
3227
|
+
"by it.",
|
|
3228
|
+
},
|
|
3229
|
+
keyToken: {
|
|
3230
|
+
kind: "server-owned",
|
|
3231
|
+
note: "`blockComposite#key.toLowerCase()`, maintained by the write path " +
|
|
3232
|
+
"so `uniqueTestCaseKeyPerBlock` enforces per-block, " +
|
|
3233
|
+
"case-insensitive key uniqueness in one transactional write " +
|
|
3234
|
+
"(#2896). Never on the wire in either direction.",
|
|
3235
|
+
},
|
|
3236
|
+
appId: {
|
|
3237
|
+
kind: "server-owned",
|
|
3238
|
+
note: "Tenant scope, taken from the route; never on the wire.",
|
|
3239
|
+
},
|
|
3240
|
+
blockType: {
|
|
3241
|
+
kind: "structural",
|
|
3242
|
+
requestModes: [],
|
|
3243
|
+
note: "Which kind of block the case tests (prompt | workflow | script | " +
|
|
3244
|
+
"integration). " +
|
|
3245
|
+
"Comes from the directory the `<key>.tests/` folder sits in and " +
|
|
3246
|
+
"travels in the URL path, not in the body.",
|
|
3247
|
+
},
|
|
3248
|
+
blockId: {
|
|
3249
|
+
kind: "structural",
|
|
3250
|
+
requestModes: [],
|
|
3251
|
+
note: "The block the case belongs to, resolved from the owning file's " +
|
|
3252
|
+
"key at push time and sent in the URL path.",
|
|
3253
|
+
},
|
|
3254
|
+
blockComposite: {
|
|
3255
|
+
kind: "server-owned",
|
|
3256
|
+
note: "`blockType#blockId`, assembled server-side for indexing.",
|
|
3257
|
+
},
|
|
3258
|
+
attachments: {
|
|
3259
|
+
kind: "server-owned",
|
|
3260
|
+
note: "Uploaded fixture files, managed through the " +
|
|
3261
|
+
"`/test-cases/{id}/attachments` endpoints and stored in R2. The " +
|
|
3262
|
+
"create handler always writes null; sync never carries them.",
|
|
3263
|
+
},
|
|
3264
|
+
createdBy: {
|
|
3265
|
+
kind: "server-owned",
|
|
3266
|
+
note: "Admin who created the case; assigned server-side.",
|
|
3267
|
+
},
|
|
3268
|
+
createdAt: {
|
|
3269
|
+
kind: "server-owned",
|
|
3270
|
+
note: "Timestamp, assigned server-side.",
|
|
3271
|
+
},
|
|
3272
|
+
modifiedAt: {
|
|
3273
|
+
kind: "server-owned",
|
|
3274
|
+
note: "Timestamp, assigned server-side.",
|
|
3275
|
+
},
|
|
3276
|
+
},
|
|
3277
|
+
tomlOnlyKeys: {
|
|
3278
|
+
configId: {
|
|
3279
|
+
kind: "deprecated",
|
|
3280
|
+
note: "The legacy id-based config reference. Still accepted on push so " +
|
|
3281
|
+
"an unmigrated file pushes unchanged; pull writes `configName`.",
|
|
3282
|
+
},
|
|
3283
|
+
evaluatorPromptId: {
|
|
3284
|
+
kind: "deprecated",
|
|
3285
|
+
note: "The legacy id-based evaluator-prompt reference; pull writes " +
|
|
3286
|
+
"`evaluatorPromptKey`.",
|
|
3287
|
+
},
|
|
3288
|
+
evaluatorConfigId: {
|
|
3289
|
+
kind: "deprecated",
|
|
3290
|
+
note: "The legacy id-based evaluator-config reference; pull writes " +
|
|
3291
|
+
"`evaluatorConfigName`.",
|
|
3292
|
+
},
|
|
3293
|
+
},
|
|
3294
|
+
responseOnlyKeys: {},
|
|
3295
|
+
},
|
|
3296
|
+
],
|
|
3297
|
+
// A sidecar is the `[test]` table alone: inputs and expectations are keys
|
|
3298
|
+
// inside it (JSON strings), not sibling tables.
|
|
3299
|
+
tomlDocumentKeys: {},
|
|
3300
|
+
};
|
|
3301
|
+
// ── src/config-surface/app-settings.ts ───────────────────────────────────
|
|
3302
|
+
/**
|
|
3303
|
+
* The `app-settings` configuration object's definition (issue #2644, phase 3).
|
|
3304
|
+
*
|
|
3305
|
+
* `app.toml` is the other surface that round-trips without being a per-entity
|
|
3306
|
+
* file, so — like `test-case` — it is named explicitly rather than reached
|
|
3307
|
+
* through `SYNC_RESOURCE_TYPES`. It is also the object the whole epic
|
|
3308
|
+
* generalizes: #1033 already drives its pull serializer and push parser from
|
|
3309
|
+
* one descriptor (`cli/src/lib/app-settings-descriptor.ts`) against the
|
|
3310
|
+
* server's dependency-free write allow-list (`settings-fields.ts`), with a
|
|
3311
|
+
* parity test between them. This definition is that trio's registry entry, so
|
|
3312
|
+
* the shared coverage guard holds `App` to the same standard as every other
|
|
3313
|
+
* model: every field exposed, or classified with a reason.
|
|
3314
|
+
*
|
|
3315
|
+
* Two shapes here are unlike the per-entity objects:
|
|
3316
|
+
*
|
|
3317
|
+
* - ONE model over FOUR tables. `app.toml`'s `[app]`, `[auth]`, `[cors]` and
|
|
3318
|
+
* `[invitations]` sections all project the same `App` row, so coverage is
|
|
3319
|
+
* asserted per SURFACE (`findUnclassifiedSurfaceFields`) rather than per
|
|
3320
|
+
* table, and the `notExposed` classifications live once, on `[app]`.
|
|
3321
|
+
* - UPDATE only. There is one write path — `PUT /settings` — and an app is
|
|
3322
|
+
* created through a different endpoint entirely, so no field is writable on
|
|
3323
|
+
* "create" in this surface's sense.
|
|
3324
|
+
*
|
|
3325
|
+
* `googleClients` carries a per-entry `clientSecret` on purpose (#2256, #2891):
|
|
3326
|
+
* each stored value is a whole `{{secrets.KEY}}` REFERENCE, never the
|
|
3327
|
+
* credential, so the map round-trips like any other setting. That is the
|
|
3328
|
+
* explicit classification the secret-adjacent rule requires — the guard fails
|
|
3329
|
+
* on a bare omission, not on a stated reference-only contract.
|
|
3330
|
+
*
|
|
3331
|
+
* As of #2891 nothing here is writable-but-unauthorable. `notExposed` carries
|
|
3332
|
+
* no `deprecated` entry and no withheld-on-read field: the five superseded keys
|
|
3333
|
+
* (`googleClientId`, `googleClientSecret`, `redirectUris`, `passkeyRpId`,
|
|
3334
|
+
* `passkeyRpName`) stay DECLARED in `models.yaml` so a row's storage can still
|
|
3335
|
+
* be read, but no write path accepts them and no serializer emits them, which
|
|
3336
|
+
* makes them `server-owned` here rather than `deprecated`.
|
|
3337
|
+
*/
|
|
3338
|
+
const SETTINGS_HANDLER = "src/app-api/controllers/settings-controller.ts";
|
|
3339
|
+
export const APP_SETTINGS_SURFACE = {
|
|
3340
|
+
label: "app-settings",
|
|
3341
|
+
tables: [
|
|
3342
|
+
{
|
|
3343
|
+
tomlPath: ["app"],
|
|
3344
|
+
repeated: false,
|
|
3345
|
+
model: "App",
|
|
3346
|
+
// Declaration order reproduces `APP_SETTINGS_DESCRIPTOR`'s (#1033), which
|
|
3347
|
+
// is the order `app.toml` is written in.
|
|
3348
|
+
fields: [
|
|
3349
|
+
{
|
|
3350
|
+
field: "name",
|
|
3351
|
+
tomlKey: "name",
|
|
3352
|
+
type: "string",
|
|
3353
|
+
emit: "always",
|
|
3354
|
+
writableOn: UPDATE_ONLY,
|
|
3355
|
+
validation: "passthrough",
|
|
3356
|
+
},
|
|
3357
|
+
{
|
|
3358
|
+
field: "mode",
|
|
3359
|
+
tomlKey: "mode",
|
|
3360
|
+
type: "string",
|
|
3361
|
+
emit: "always",
|
|
3362
|
+
writableOn: UPDATE_ONLY,
|
|
3363
|
+
validation: "passthrough",
|
|
3364
|
+
},
|
|
3365
|
+
{
|
|
3366
|
+
field: "baseUrl",
|
|
3367
|
+
tomlKey: "baseUrl",
|
|
3368
|
+
type: "string",
|
|
3369
|
+
emit: "whenSet",
|
|
3370
|
+
writableOn: UPDATE_ONLY,
|
|
3371
|
+
validation: "passthrough",
|
|
3372
|
+
},
|
|
3373
|
+
{
|
|
3374
|
+
field: "waitlistEnabled",
|
|
3375
|
+
tomlKey: "waitlistEnabled",
|
|
3376
|
+
type: "boolean",
|
|
3377
|
+
emit: "always",
|
|
3378
|
+
writableOn: UPDATE_ONLY,
|
|
3379
|
+
validation: "passthrough",
|
|
3380
|
+
},
|
|
3381
|
+
{
|
|
3382
|
+
field: "waitlistNotifyAdmins",
|
|
3383
|
+
tomlKey: "waitlistNotifyAdmins",
|
|
3384
|
+
type: "boolean",
|
|
3385
|
+
emit: "always",
|
|
3386
|
+
writableOn: UPDATE_ONLY,
|
|
3387
|
+
validation: "passthrough",
|
|
3388
|
+
},
|
|
3389
|
+
{
|
|
3390
|
+
field: "directLlmEnabled",
|
|
3391
|
+
tomlKey: "directLlmEnabled",
|
|
3392
|
+
type: "boolean",
|
|
3393
|
+
// Always emitted: an app that has not opted the deprecated direct
|
|
3394
|
+
// LLM/Gemini proxy in must see `directLlmEnabled = false` in its
|
|
3395
|
+
// pulled `app.toml`, not a key it has to already know about (#2687).
|
|
3396
|
+
emit: "always",
|
|
3397
|
+
writableOn: UPDATE_ONLY,
|
|
3398
|
+
// STRICT, not `!!`: the value arms a credit-spending surface read
|
|
3399
|
+
// with `=== true`, so `"false"` must be a 400 rather than a stored
|
|
3400
|
+
// `true`. The generated request schema stays keys-only (#1976), so
|
|
3401
|
+
// the check is `parseDirectLlmEnabledWrite` in the handler.
|
|
3402
|
+
validation: handledBy(SETTINGS_HANDLER, "SettingsController"),
|
|
3403
|
+
},
|
|
3404
|
+
{
|
|
3405
|
+
field: "allowedDomains",
|
|
3406
|
+
tomlKey: "allowedDomains",
|
|
3407
|
+
type: "string[]",
|
|
3408
|
+
emit: "whenSet",
|
|
3409
|
+
writableOn: UPDATE_ONLY,
|
|
3410
|
+
validation: "passthrough",
|
|
3411
|
+
},
|
|
3412
|
+
{
|
|
3413
|
+
field: "testAccountBaseEmails",
|
|
3414
|
+
tomlKey: "testAccountBaseEmails",
|
|
3415
|
+
type: "string[]",
|
|
3416
|
+
emit: "whenSet",
|
|
3417
|
+
writableOn: UPDATE_ONLY,
|
|
3418
|
+
// `+primitivetest` bypass whitelist (#490/#675), shape-checked by
|
|
3419
|
+
// `validateTestAccountBaseEmails`.
|
|
3420
|
+
validation: handledBy(SETTINGS_HANDLER, "SettingsController"),
|
|
3421
|
+
},
|
|
3422
|
+
],
|
|
3423
|
+
notExposed: {
|
|
3424
|
+
appId: {
|
|
3425
|
+
kind: "server-owned",
|
|
3426
|
+
note: "The app's id; taken from the route, never authored.",
|
|
3427
|
+
},
|
|
3428
|
+
createdBy: {
|
|
3429
|
+
kind: "server-owned",
|
|
3430
|
+
note: "User who created the app; assigned server-side.",
|
|
3431
|
+
},
|
|
3432
|
+
createdByAdminId: {
|
|
3433
|
+
kind: "server-owned",
|
|
3434
|
+
note: "Admin who created the app; assigned server-side.",
|
|
3435
|
+
},
|
|
3436
|
+
passkeyRpId: {
|
|
3437
|
+
kind: "server-owned",
|
|
3438
|
+
note: "RETIRED from the write surface by #2891, superseded by " +
|
|
3439
|
+
"`passkeyRpConfig` (`[auth.passkeys]`), which carries the " +
|
|
3440
|
+
"per-rpId names. The attribute stays declared so the passkey " +
|
|
3441
|
+
"runtime can keep reading a legacy row as a fallback, but no " +
|
|
3442
|
+
"write path accepts it and no serializer emits it.",
|
|
3443
|
+
},
|
|
3444
|
+
passkeyRpName: {
|
|
3445
|
+
kind: "server-owned",
|
|
3446
|
+
note: "The other half of the legacy single-rpId passkey config, " +
|
|
3447
|
+
"RETIRED by #2891 alongside it; superseded by `passkeyRpConfig`.",
|
|
3448
|
+
},
|
|
3449
|
+
googleClientId: {
|
|
3450
|
+
kind: "server-owned",
|
|
3451
|
+
note: "RETIRED from the write surface by #2891, superseded by " +
|
|
3452
|
+
"`[auth.google.clients.<type>].clientId` — Google registers a " +
|
|
3453
|
+
"client per platform, so one app-wide id could not state what " +
|
|
3454
|
+
"the app actually has. Declared-and-deprecated storage only.",
|
|
3455
|
+
},
|
|
3456
|
+
googleClientSecret: {
|
|
3457
|
+
kind: "server-owned",
|
|
3458
|
+
note: "RETIRED from the write surface by #2891, superseded by " +
|
|
3459
|
+
"`[auth.google.clients.<type>].clientSecret` — the secret belongs " +
|
|
3460
|
+
"to one client, and Google issues none at all for the native " +
|
|
3461
|
+
"types. Declared-and-deprecated storage only; never read, never " +
|
|
3462
|
+
"echoed.",
|
|
3463
|
+
},
|
|
3464
|
+
magicLinkEnabled: {
|
|
3465
|
+
kind: "server-owned",
|
|
3466
|
+
note: "RETIRED from the write surface by #2884, superseded by " +
|
|
3467
|
+
"`emailSignInEnabled`. Email sign-in is ONE flow — one request, " +
|
|
3468
|
+
"one email carrying a code and (when a link can be issued) a " +
|
|
3469
|
+
"link — so there is no method to enable separately. The " +
|
|
3470
|
+
"attribute stays declared and is MIRRORED on every write of " +
|
|
3471
|
+
"`emailSignInEnabled`, so a row read by an un-migrated consumer " +
|
|
3472
|
+
"still matches behavior.",
|
|
3473
|
+
},
|
|
3474
|
+
otpEnabled: {
|
|
3475
|
+
kind: "server-owned",
|
|
3476
|
+
note: "The other half of the retired pair (#2884); superseded by " +
|
|
3477
|
+
"`emailSignInEnabled` and mirrored the same way.",
|
|
3478
|
+
},
|
|
3479
|
+
redirectUris: {
|
|
3480
|
+
kind: "server-owned",
|
|
3481
|
+
note: "RETIRED from the write surface by #2891. A redirect URI belongs " +
|
|
3482
|
+
"to the thing that redirects: Google callbacks moved to " +
|
|
3483
|
+
"`[auth.google.clients.<type>].redirectUris` and the magic-link " +
|
|
3484
|
+
"allow-list to `emailRedirectUris`. Declared-and-deprecated " +
|
|
3485
|
+
"storage only.",
|
|
3486
|
+
},
|
|
3487
|
+
createdAt: {
|
|
3488
|
+
kind: "server-owned",
|
|
3489
|
+
note: "Timestamp, assigned server-side.",
|
|
3490
|
+
},
|
|
3491
|
+
modifiedAt: {
|
|
3492
|
+
kind: "server-owned",
|
|
3493
|
+
note: "Timestamp, assigned server-side.",
|
|
3494
|
+
},
|
|
3495
|
+
},
|
|
3496
|
+
tomlOnlyKeys: {},
|
|
3497
|
+
responseOnlyKeys: {
|
|
3498
|
+
// `hasGoogleAuth` is gone (#2891): the client map states per-platform
|
|
3499
|
+
// truth directly, so a single derived flag can only be wrong in one
|
|
3500
|
+
// direction or the other. `hasAppleAuth` and `hasPasskey` STAY — the
|
|
3501
|
+
// asymmetry is deliberate, not an oversight to "fix" later: Apple and
|
|
3502
|
+
// passkeys have no map to derive that truth from.
|
|
3503
|
+
hasAppleAuth: {
|
|
3504
|
+
kind: "server-owned",
|
|
3505
|
+
note: "Derived presence flag on GET /settings; no setter.",
|
|
3506
|
+
},
|
|
3507
|
+
hasPasskey: {
|
|
3508
|
+
kind: "server-owned",
|
|
3509
|
+
note: "Derived presence flag on GET /settings; no setter.",
|
|
3510
|
+
},
|
|
3511
|
+
},
|
|
3512
|
+
},
|
|
3513
|
+
{
|
|
3514
|
+
tomlPath: ["auth"],
|
|
3515
|
+
repeated: false,
|
|
3516
|
+
model: "App",
|
|
3517
|
+
fields: [
|
|
3518
|
+
{
|
|
3519
|
+
field: "googleOAuthEnabled",
|
|
3520
|
+
tomlKey: "googleOAuthEnabled",
|
|
3521
|
+
type: "boolean",
|
|
3522
|
+
emit: "whenSet",
|
|
3523
|
+
writableOn: UPDATE_ONLY,
|
|
3524
|
+
validation: "passthrough",
|
|
3525
|
+
},
|
|
3526
|
+
{
|
|
3527
|
+
field: "googleClients",
|
|
3528
|
+
tomlKey: "google",
|
|
3529
|
+
type: "json",
|
|
3530
|
+
emit: "whenSet",
|
|
3531
|
+
writableOn: UPDATE_ONLY,
|
|
3532
|
+
// `[auth.google.clients.<type>]` — one entry per Google client type,
|
|
3533
|
+
// each with its own `clientId`, `redirectUris` and (where the type
|
|
3534
|
+
// takes one) a whole `{{secrets.KEY}}` `clientSecret`. Shape, the
|
|
3535
|
+
// per-type secret table, cross-entry URI uniqueness and the
|
|
3536
|
+
// aggregate caps are all `validateGoogleClientsWrite`
|
|
3537
|
+
// (`src/services/google-clients.ts`), applied by the handler.
|
|
3538
|
+
validation: handledBy(SETTINGS_HANDLER, "SettingsController"),
|
|
3539
|
+
},
|
|
3540
|
+
{
|
|
3541
|
+
field: "passkeyEnabled",
|
|
3542
|
+
tomlKey: "passkeyEnabled",
|
|
3543
|
+
type: "boolean",
|
|
3544
|
+
emit: "always",
|
|
3545
|
+
writableOn: UPDATE_ONLY,
|
|
3546
|
+
validation: "passthrough",
|
|
3547
|
+
},
|
|
3548
|
+
{
|
|
3549
|
+
field: "emailSignInEnabled",
|
|
3550
|
+
tomlKey: "emailSignInEnabled",
|
|
3551
|
+
type: "boolean",
|
|
3552
|
+
// Always emitted: an app's pulled `app.toml` should state whether
|
|
3553
|
+
// email sign-in is on, not leave it to be inferred from the absence
|
|
3554
|
+
// of a key (#2884).
|
|
3555
|
+
emit: "always",
|
|
3556
|
+
writableOn: UPDATE_ONLY,
|
|
3557
|
+
// Mirrored into the two retired model fields by the handler so an
|
|
3558
|
+
// un-migrated reader cannot disagree.
|
|
3559
|
+
validation: handledBy(SETTINGS_HANDLER, "SettingsController"),
|
|
3560
|
+
},
|
|
3561
|
+
{
|
|
3562
|
+
field: "appleSignInEnabled",
|
|
3563
|
+
tomlKey: "appleSignInEnabled",
|
|
3564
|
+
type: "boolean",
|
|
3565
|
+
emit: "whenSet",
|
|
3566
|
+
writableOn: UPDATE_ONLY,
|
|
3567
|
+
validation: "passthrough",
|
|
3568
|
+
},
|
|
3569
|
+
{
|
|
3570
|
+
field: "appleAudiences",
|
|
3571
|
+
tomlKey: "appleAudiences",
|
|
3572
|
+
type: "string[]",
|
|
3573
|
+
emit: "whenSet",
|
|
3574
|
+
writableOn: UPDATE_ONLY,
|
|
3575
|
+
validation: "passthrough",
|
|
3576
|
+
},
|
|
3577
|
+
{
|
|
3578
|
+
field: "emailRedirectUris",
|
|
3579
|
+
tomlKey: "emailRedirectUris",
|
|
3580
|
+
type: "string[]",
|
|
3581
|
+
emit: "whenSet",
|
|
3582
|
+
writableOn: UPDATE_ONLY,
|
|
3583
|
+
// The email sign-in link allow-list, flat in `[auth]` beside the
|
|
3584
|
+
// flag it belongs with (sponsor decision, DSO-2891-003 — no
|
|
3585
|
+
// `[auth.email]` table). Each URI is parsed and canonicalized by
|
|
3586
|
+
// `validateRedirectUris`; matching is fail-closed, and a request
|
|
3587
|
+
// whose target this list does not cover gets a code-only email
|
|
3588
|
+
// rather than a link (#2884).
|
|
3589
|
+
validation: handledBy(SETTINGS_HANDLER, "SettingsController"),
|
|
3590
|
+
},
|
|
3591
|
+
{
|
|
3592
|
+
field: "passkeyRpConfig",
|
|
3593
|
+
tomlKey: "passkeys",
|
|
3594
|
+
type: "json",
|
|
3595
|
+
emit: "whenSet",
|
|
3596
|
+
writableOn: UPDATE_ONLY,
|
|
3597
|
+
// `[auth.passkeys]` — rpId → { name }, shape-checked by
|
|
3598
|
+
// `SettingsController.validatePasskeyRpConfig`.
|
|
3599
|
+
validation: handledBy(SETTINGS_HANDLER, "SettingsController"),
|
|
3600
|
+
},
|
|
3601
|
+
],
|
|
3602
|
+
notExposed: {},
|
|
3603
|
+
tomlOnlyKeys: {},
|
|
3604
|
+
responseOnlyKeys: {},
|
|
3605
|
+
},
|
|
3606
|
+
{
|
|
3607
|
+
tomlPath: ["cors"],
|
|
3608
|
+
repeated: false,
|
|
3609
|
+
model: "App",
|
|
3610
|
+
fields: [
|
|
3611
|
+
{
|
|
3612
|
+
field: "corsMode",
|
|
3613
|
+
tomlKey: "mode",
|
|
3614
|
+
type: "string",
|
|
3615
|
+
emit: "always",
|
|
3616
|
+
writableOn: UPDATE_ONLY,
|
|
3617
|
+
validation: "passthrough",
|
|
3618
|
+
},
|
|
3619
|
+
{
|
|
3620
|
+
field: "corsAllowedOrigins",
|
|
3621
|
+
tomlKey: "allowedOrigins",
|
|
3622
|
+
type: "string[]",
|
|
3623
|
+
emit: "whenSet",
|
|
3624
|
+
writableOn: UPDATE_ONLY,
|
|
3625
|
+
validation: "passthrough",
|
|
3626
|
+
},
|
|
3627
|
+
{
|
|
3628
|
+
field: "corsAllowCredentials",
|
|
3629
|
+
tomlKey: "allowCredentials",
|
|
3630
|
+
type: "boolean",
|
|
3631
|
+
emit: "whenSet",
|
|
3632
|
+
writableOn: UPDATE_ONLY,
|
|
3633
|
+
validation: "passthrough",
|
|
3634
|
+
},
|
|
3635
|
+
{
|
|
3636
|
+
field: "corsAllowedMethods",
|
|
3637
|
+
tomlKey: "allowedMethods",
|
|
3638
|
+
type: "string[]",
|
|
3639
|
+
emit: "whenSet",
|
|
3640
|
+
writableOn: UPDATE_ONLY,
|
|
3641
|
+
validation: "passthrough",
|
|
3642
|
+
},
|
|
3643
|
+
{
|
|
3644
|
+
field: "corsAllowedHeaders",
|
|
3645
|
+
tomlKey: "allowedHeaders",
|
|
3646
|
+
type: "string[]",
|
|
3647
|
+
emit: "whenSet",
|
|
3648
|
+
writableOn: UPDATE_ONLY,
|
|
3649
|
+
validation: "passthrough",
|
|
3650
|
+
},
|
|
3651
|
+
{
|
|
3652
|
+
field: "corsExposedHeaders",
|
|
3653
|
+
tomlKey: "exposedHeaders",
|
|
3654
|
+
type: "string[]",
|
|
3655
|
+
emit: "whenSet",
|
|
3656
|
+
writableOn: UPDATE_ONLY,
|
|
3657
|
+
validation: "passthrough",
|
|
3658
|
+
},
|
|
3659
|
+
{
|
|
3660
|
+
field: "corsMaxAge",
|
|
3661
|
+
tomlKey: "maxAge",
|
|
3662
|
+
type: "number",
|
|
3663
|
+
emit: "whenSet",
|
|
3664
|
+
writableOn: UPDATE_ONLY,
|
|
3665
|
+
validation: "passthrough",
|
|
3666
|
+
},
|
|
3667
|
+
],
|
|
3668
|
+
notExposed: {},
|
|
3669
|
+
tomlOnlyKeys: {},
|
|
3670
|
+
responseOnlyKeys: {},
|
|
3671
|
+
},
|
|
3672
|
+
{
|
|
3673
|
+
tomlPath: ["invitations"],
|
|
3674
|
+
repeated: false,
|
|
3675
|
+
model: "App",
|
|
3676
|
+
fields: [
|
|
3677
|
+
{
|
|
3678
|
+
field: "memberInvitationsEnabled",
|
|
3679
|
+
tomlKey: "enabled",
|
|
3680
|
+
type: "boolean",
|
|
3681
|
+
emit: "always",
|
|
3682
|
+
writableOn: UPDATE_ONLY,
|
|
3683
|
+
validation: "passthrough",
|
|
3684
|
+
},
|
|
3685
|
+
{
|
|
3686
|
+
field: "memberInvitationLimit",
|
|
3687
|
+
tomlKey: "limit",
|
|
3688
|
+
type: "number",
|
|
3689
|
+
emit: "always",
|
|
3690
|
+
writableOn: UPDATE_ONLY,
|
|
3691
|
+
validation: "passthrough",
|
|
3692
|
+
},
|
|
3693
|
+
],
|
|
3694
|
+
notExposed: {},
|
|
3695
|
+
tomlOnlyKeys: {},
|
|
3696
|
+
responseOnlyKeys: {},
|
|
3697
|
+
},
|
|
3698
|
+
],
|
|
3699
|
+
// `app.toml`'s four sections are all field tables; it carries no other
|
|
3700
|
+
// top-level key. (App config VARS live in their own `vars.toml`.)
|
|
3701
|
+
tomlDocumentKeys: {},
|
|
3702
|
+
};
|
|
3703
|
+
// ── src/config-surface/index.ts ──────────────────────────────────────────
|
|
3704
|
+
/**
|
|
3705
|
+
* The configuration-object registry (issue #2644).
|
|
3706
|
+
*
|
|
3707
|
+
* `CONFIG_SURFACES` holds one entry per synced configuration object type. The
|
|
3708
|
+
* registry — not a per-type test — is what makes coverage follow from existing:
|
|
3709
|
+
* `cli/tests/unit/config-surface-drift-guard.test.ts` reads the CLI's
|
|
3710
|
+
* `SYNC_RESOURCE_TYPES` labels and fails when a label has no surface here.
|
|
3711
|
+
*
|
|
3712
|
+
* `CONFIG_SURFACES` is the write authority. `SYNC_RESOURCE_TYPES` keeps its
|
|
3713
|
+
* documented role — directory/state/prune/diff layout metadata, explicitly "not
|
|
3714
|
+
* a write framework" — and gains no write-surface fields; the two are
|
|
3715
|
+
* cross-checked, not merged.
|
|
3716
|
+
*/
|
|
3717
|
+
/**
|
|
3718
|
+
* Every configuration object whose field surface is defined here.
|
|
3719
|
+
*
|
|
3720
|
+
* One entry per `SYNC_RESOURCE_TYPES` label, plus the two surfaces that
|
|
3721
|
+
* round-trip without being a per-entity file: `app-settings` (`app.toml`) and
|
|
3722
|
+
* `test-case` (`<key>.tests/`). Nothing that syncs sits outside the registry —
|
|
3723
|
+
* `PENDING_MIGRATION`, the migration's temporary exemption list, is gone as of
|
|
3724
|
+
* phase 3, so a new synced type has nowhere to be parked and fails the registry
|
|
3725
|
+
* guard until it is defined (#2644 criterion 3).
|
|
3726
|
+
*/
|
|
3727
|
+
export const CONFIG_SURFACES = [
|
|
3728
|
+
WORKFLOW_SURFACE,
|
|
3729
|
+
PROMPT_SURFACE,
|
|
3730
|
+
INTEGRATION_SURFACE,
|
|
3731
|
+
WEBHOOK_SURFACE,
|
|
3732
|
+
CRON_TRIGGER_SURFACE,
|
|
3733
|
+
BLOB_BUCKET_SURFACE,
|
|
3734
|
+
EMAIL_TEMPLATE_SURFACE,
|
|
3735
|
+
DATABASE_TYPE_SURFACE,
|
|
3736
|
+
RULE_SET_SURFACE,
|
|
3737
|
+
GROUP_TYPE_CONFIG_SURFACE,
|
|
3738
|
+
COLLECTION_TYPE_CONFIG_SURFACE,
|
|
3739
|
+
METADATA_CATEGORY_CONFIG_SURFACE,
|
|
3740
|
+
TRANSFORM_SURFACE,
|
|
3741
|
+
TEST_CASE_SURFACE,
|
|
3742
|
+
APP_SETTINGS_SURFACE,
|
|
3743
|
+
];
|
|
3744
|
+
/** The surface for a `SyncResourceType.label`, or undefined when unmigrated. */
|
|
3745
|
+
export function getConfigSurface(label) {
|
|
3746
|
+
return CONFIG_SURFACES.find((s) => s.label === label);
|
|
3747
|
+
}
|
|
3748
|
+
/** One table of one surface, addressed by its TOML path. */
|
|
3749
|
+
export function getConfigTable(label, tomlPath) {
|
|
3750
|
+
const surface = getConfigSurface(label);
|
|
3751
|
+
if (!surface)
|
|
3752
|
+
return undefined;
|
|
3753
|
+
return surface.tables.find((t) => t.tomlPath.length === tomlPath.length &&
|
|
3754
|
+
t.tomlPath.every((segment, i) => segment === tomlPath[i]));
|
|
3755
|
+
}
|
|
3756
|
+
/**
|
|
3757
|
+
* Every field of every `models.yaml` model a configuration-object definition
|
|
3758
|
+
* names, in declaration order — the coverage guard's anchor (#2644 criterion 2).
|
|
3759
|
+
*/
|
|
3760
|
+
export const GENERATED_CONFIG_MODEL_FIELDS = {
|
|
3761
|
+
"AccessRuleSet": [
|
|
3762
|
+
"ruleSetId",
|
|
3763
|
+
"appId",
|
|
3764
|
+
"name",
|
|
3765
|
+
"description",
|
|
3766
|
+
"resourceType",
|
|
3767
|
+
"rules",
|
|
3768
|
+
"version",
|
|
3769
|
+
"createdAt",
|
|
3770
|
+
"modifiedAt",
|
|
3771
|
+
"createdBy",
|
|
3772
|
+
],
|
|
3773
|
+
"App": [
|
|
3774
|
+
"appId",
|
|
3775
|
+
"name",
|
|
3776
|
+
"mode",
|
|
3777
|
+
"waitlistEnabled",
|
|
3778
|
+
"waitlistNotifyAdmins",
|
|
3779
|
+
"createdBy",
|
|
3780
|
+
"createdByAdminId",
|
|
3781
|
+
"googleOAuthEnabled",
|
|
3782
|
+
"googleClientId",
|
|
3783
|
+
"googleClientSecret",
|
|
3784
|
+
"googleClients",
|
|
3785
|
+
"appleSignInEnabled",
|
|
3786
|
+
"appleAudiences",
|
|
3787
|
+
"allowedDomains",
|
|
3788
|
+
"redirectUris",
|
|
3789
|
+
"emailRedirectUris",
|
|
3790
|
+
"testAccountBaseEmails",
|
|
3791
|
+
"corsMode",
|
|
3792
|
+
"corsAllowedOrigins",
|
|
3793
|
+
"corsAllowCredentials",
|
|
3794
|
+
"corsAllowedMethods",
|
|
3795
|
+
"corsAllowedHeaders",
|
|
3796
|
+
"corsExposedHeaders",
|
|
3797
|
+
"corsMaxAge",
|
|
3798
|
+
"passkeyEnabled",
|
|
3799
|
+
"passkeyRpId",
|
|
3800
|
+
"passkeyRpName",
|
|
3801
|
+
"passkeyRpConfig",
|
|
3802
|
+
"emailSignInEnabled",
|
|
3803
|
+
"magicLinkEnabled",
|
|
3804
|
+
"otpEnabled",
|
|
3805
|
+
"baseUrl",
|
|
3806
|
+
"directLlmEnabled",
|
|
3807
|
+
"memberInvitationsEnabled",
|
|
3808
|
+
"memberInvitationLimit",
|
|
3809
|
+
"createdAt",
|
|
3810
|
+
"modifiedAt",
|
|
3811
|
+
],
|
|
3812
|
+
"AppIntegration": [
|
|
3813
|
+
"integrationId",
|
|
3814
|
+
"appId",
|
|
3815
|
+
"integrationKey",
|
|
3816
|
+
"displayName",
|
|
3817
|
+
"description",
|
|
3818
|
+
"status",
|
|
3819
|
+
"requestConfig",
|
|
3820
|
+
"accessRule",
|
|
3821
|
+
"activeConfigId",
|
|
3822
|
+
"timeoutMs",
|
|
3823
|
+
"maxRequestBodyBytes",
|
|
3824
|
+
"copiedFromCatalogId",
|
|
3825
|
+
"lastTestedAt",
|
|
3826
|
+
"createdBy",
|
|
3827
|
+
"createdAt",
|
|
3828
|
+
"modifiedAt",
|
|
3829
|
+
],
|
|
3830
|
+
"AppPrompt": [
|
|
3831
|
+
"promptId",
|
|
3832
|
+
"appId",
|
|
3833
|
+
"promptKey",
|
|
3834
|
+
"displayName",
|
|
3835
|
+
"description",
|
|
3836
|
+
"copiedFromCatalogId",
|
|
3837
|
+
"inputSchema",
|
|
3838
|
+
"outputSchema",
|
|
3839
|
+
"activeConfigId",
|
|
3840
|
+
"defaultEvaluatorPromptId",
|
|
3841
|
+
"status",
|
|
3842
|
+
"accessRule",
|
|
3843
|
+
"createdBy",
|
|
3844
|
+
"createdAt",
|
|
3845
|
+
"modifiedAt",
|
|
3846
|
+
],
|
|
3847
|
+
"AppPromptConfig": [
|
|
3848
|
+
"configId",
|
|
3849
|
+
"promptId",
|
|
3850
|
+
"appId",
|
|
3851
|
+
"configName",
|
|
3852
|
+
"promptIdConfigName",
|
|
3853
|
+
"description",
|
|
3854
|
+
"provider",
|
|
3855
|
+
"model",
|
|
3856
|
+
"systemPrompt",
|
|
3857
|
+
"systemPromptR2Key",
|
|
3858
|
+
"userPromptTemplate",
|
|
3859
|
+
"userPromptTemplateR2Key",
|
|
3860
|
+
"temperature",
|
|
3861
|
+
"topP",
|
|
3862
|
+
"maxTokens",
|
|
3863
|
+
"outputFormat",
|
|
3864
|
+
"outputSchema",
|
|
3865
|
+
"providerConfig",
|
|
3866
|
+
"status",
|
|
3867
|
+
"createdBy",
|
|
3868
|
+
"createdAt",
|
|
3869
|
+
"modifiedAt",
|
|
3870
|
+
],
|
|
3871
|
+
"AppWebhook": [
|
|
3872
|
+
"webhookId",
|
|
3873
|
+
"appId",
|
|
3874
|
+
"webhookKey",
|
|
3875
|
+
"displayName",
|
|
3876
|
+
"description",
|
|
3877
|
+
"status",
|
|
3878
|
+
"workflowKey",
|
|
3879
|
+
"workflowId",
|
|
3880
|
+
"verificationScheme",
|
|
3881
|
+
"signingSecret",
|
|
3882
|
+
"previousSigningSecret",
|
|
3883
|
+
"secretRotatedAt",
|
|
3884
|
+
"secretGracePeriodMs",
|
|
3885
|
+
"toleranceSeconds",
|
|
3886
|
+
"deduplicationEnabled",
|
|
3887
|
+
"deduplicationWindowMs",
|
|
3888
|
+
"maxBodyBytes",
|
|
3889
|
+
"inputMapping",
|
|
3890
|
+
"allowedIpCidrs",
|
|
3891
|
+
"handshakeRules",
|
|
3892
|
+
"config",
|
|
3893
|
+
"metadata",
|
|
3894
|
+
"lastTriggeredAt",
|
|
3895
|
+
"createdBy",
|
|
3896
|
+
"createdAt",
|
|
3897
|
+
"modifiedAt",
|
|
3898
|
+
],
|
|
3899
|
+
"BlobBucket": [
|
|
3900
|
+
"bucketId",
|
|
3901
|
+
"appId",
|
|
3902
|
+
"bucketKey",
|
|
3903
|
+
"name",
|
|
3904
|
+
"description",
|
|
3905
|
+
"ttlTier",
|
|
3906
|
+
"accessPolicy",
|
|
3907
|
+
"preset",
|
|
3908
|
+
"ruleSetId",
|
|
3909
|
+
"createdBy",
|
|
3910
|
+
"createdAt",
|
|
3911
|
+
"modifiedAt",
|
|
3912
|
+
],
|
|
3913
|
+
"BlockTestCase": [
|
|
3914
|
+
"testCaseId",
|
|
3915
|
+
"appId",
|
|
3916
|
+
"blockType",
|
|
3917
|
+
"blockId",
|
|
3918
|
+
"blockComposite",
|
|
3919
|
+
"configId",
|
|
3920
|
+
"key",
|
|
3921
|
+
"keyToken",
|
|
3922
|
+
"name",
|
|
3923
|
+
"description",
|
|
3924
|
+
"inputVariables",
|
|
3925
|
+
"expectedOutputPattern",
|
|
3926
|
+
"expectedOutputContains",
|
|
3927
|
+
"expectedJsonSubset",
|
|
3928
|
+
"evaluatorPromptId",
|
|
3929
|
+
"evaluatorConfigId",
|
|
3930
|
+
"attachments",
|
|
3931
|
+
"createdBy",
|
|
3932
|
+
"createdAt",
|
|
3933
|
+
"modifiedAt",
|
|
3934
|
+
],
|
|
3935
|
+
"CollectionTypeConfig": [
|
|
3936
|
+
"appId",
|
|
3937
|
+
"collectionType",
|
|
3938
|
+
"ruleSetId",
|
|
3939
|
+
"metadataManifest",
|
|
3940
|
+
"createdAt",
|
|
3941
|
+
"modifiedAt",
|
|
3942
|
+
"createdBy",
|
|
3943
|
+
],
|
|
3944
|
+
"CronTrigger": [
|
|
3945
|
+
"triggerId",
|
|
3946
|
+
"appId",
|
|
3947
|
+
"triggerKey",
|
|
3948
|
+
"displayName",
|
|
3949
|
+
"description",
|
|
3950
|
+
"cron",
|
|
3951
|
+
"timezone",
|
|
3952
|
+
"workflowKey",
|
|
3953
|
+
"overlapPolicy",
|
|
3954
|
+
"rootInput",
|
|
3955
|
+
"inputMapping",
|
|
3956
|
+
"status",
|
|
3957
|
+
"state",
|
|
3958
|
+
"operationallyDisabled",
|
|
3959
|
+
"lastError",
|
|
3960
|
+
"lastTriggeredAt",
|
|
3961
|
+
"lastTriggeredRunId",
|
|
3962
|
+
"skippedCount",
|
|
3963
|
+
"firedCount",
|
|
3964
|
+
"consecutiveNotActiveCount",
|
|
3965
|
+
"nextFireAt",
|
|
3966
|
+
"createdBy",
|
|
3967
|
+
"createdAt",
|
|
3968
|
+
"modifiedAt",
|
|
3969
|
+
],
|
|
3970
|
+
"DatabaseTypeConfig": [
|
|
3971
|
+
"appId",
|
|
3972
|
+
"databaseType",
|
|
3973
|
+
"ruleSetId",
|
|
3974
|
+
"triggers",
|
|
3975
|
+
"metadataAccess",
|
|
3976
|
+
"defaultAccess",
|
|
3977
|
+
"metadataManifest",
|
|
3978
|
+
"autoPopulatedFields",
|
|
3979
|
+
"timestamps",
|
|
3980
|
+
"schema",
|
|
3981
|
+
"createdAt",
|
|
3982
|
+
"modifiedAt",
|
|
3983
|
+
"createdBy",
|
|
3984
|
+
],
|
|
3985
|
+
"EmailTemplate": [
|
|
3986
|
+
"templateId",
|
|
3987
|
+
"appId",
|
|
3988
|
+
"emailType",
|
|
3989
|
+
"subject",
|
|
3990
|
+
"htmlBody",
|
|
3991
|
+
"textBody",
|
|
3992
|
+
"createdBy",
|
|
3993
|
+
"createdAt",
|
|
3994
|
+
"modifiedAt",
|
|
3995
|
+
],
|
|
3996
|
+
"GroupTypeConfig": [
|
|
3997
|
+
"appId",
|
|
3998
|
+
"groupType",
|
|
3999
|
+
"ruleSetId",
|
|
4000
|
+
"metadataManifest",
|
|
4001
|
+
"autoAddCreator",
|
|
4002
|
+
"createdAt",
|
|
4003
|
+
"modifiedAt",
|
|
4004
|
+
"createdBy",
|
|
4005
|
+
],
|
|
4006
|
+
"MetadataCategoryConfig": [
|
|
4007
|
+
"appId",
|
|
4008
|
+
"categoryKey",
|
|
4009
|
+
"resourceType",
|
|
4010
|
+
"category",
|
|
4011
|
+
"schema",
|
|
4012
|
+
"readRule",
|
|
4013
|
+
"writeRule",
|
|
4014
|
+
"metadataManifest",
|
|
4015
|
+
"uniqueField",
|
|
4016
|
+
"schemaVersion",
|
|
4017
|
+
"generation",
|
|
4018
|
+
"description",
|
|
4019
|
+
"createdAt",
|
|
4020
|
+
"modifiedAt",
|
|
4021
|
+
"createdBy",
|
|
4022
|
+
],
|
|
4023
|
+
"WorkflowDefinition": [
|
|
4024
|
+
"workflowId",
|
|
4025
|
+
"appId",
|
|
4026
|
+
"workflowKey",
|
|
4027
|
+
"name",
|
|
4028
|
+
"status",
|
|
4029
|
+
"latestRevision",
|
|
4030
|
+
"description",
|
|
4031
|
+
"stepsR2Key",
|
|
4032
|
+
"draftStepsR2Key",
|
|
4033
|
+
"draftInputSchema",
|
|
4034
|
+
"draftOutputSchema",
|
|
4035
|
+
"draftModifiedAt",
|
|
4036
|
+
"activeConfigId",
|
|
4037
|
+
"inputSchema",
|
|
4038
|
+
"outputSchema",
|
|
4039
|
+
"perUserMaxRunning",
|
|
4040
|
+
"perUserMaxQueued",
|
|
4041
|
+
"perAppMaxRunning",
|
|
4042
|
+
"perAppMaxQueued",
|
|
4043
|
+
"queueTtlSeconds",
|
|
4044
|
+
"dequeueOrder",
|
|
4045
|
+
"requiresClientApply",
|
|
4046
|
+
"syncCallable",
|
|
4047
|
+
"accessRule",
|
|
4048
|
+
"metadataManifest",
|
|
4049
|
+
"runAs",
|
|
4050
|
+
"lock",
|
|
4051
|
+
"capabilities",
|
|
4052
|
+
"operationallyDisabled",
|
|
4053
|
+
"createdBy",
|
|
4054
|
+
"createdAt",
|
|
4055
|
+
"modifiedAt",
|
|
4056
|
+
],
|
|
4057
|
+
};
|
|
4058
|
+
//# sourceMappingURL=generated-config-surfaces.js.map
|