primitive-admin 1.1.0-alpha.9 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +523 -109
- package/assets/skill/skills/primitive-platform/SKILL.md +289 -0
- package/dist/bin/primitive.d.ts +2 -0
- package/dist/bin/primitive.js +372 -21
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/admins.d.ts +2 -0
- package/dist/src/commands/admins.js +136 -30
- package/dist/src/commands/admins.js.map +1 -1
- package/dist/src/commands/analytics.d.ts +2 -0
- package/dist/src/commands/analytics.js +713 -55
- package/dist/src/commands/analytics.js.map +1 -1
- package/dist/src/commands/apps.d.ts +2 -0
- package/dist/src/commands/apps.js +65 -100
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/auth-sessions.d.ts +7 -0
- package/dist/src/commands/auth-sessions.js +144 -0
- package/dist/src/commands/auth-sessions.js.map +1 -0
- package/dist/src/commands/auth.d.ts +2 -0
- package/dist/src/commands/auth.js +238 -108
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/blob-buckets.d.ts +2 -0
- package/dist/src/commands/blob-buckets.js +331 -0
- package/dist/src/commands/blob-buckets.js.map +1 -0
- package/dist/src/commands/catalog.d.ts +2 -0
- package/dist/src/commands/catalog.js +63 -48
- package/dist/src/commands/catalog.js.map +1 -1
- package/dist/src/commands/collection-type-configs.d.ts +2 -0
- package/dist/src/commands/collection-type-configs.js +85 -0
- package/dist/src/commands/collection-type-configs.js.map +1 -0
- package/dist/src/commands/collections.d.ts +2 -0
- package/dist/src/commands/collections.js +1282 -0
- package/dist/src/commands/collections.js.map +1 -0
- package/dist/src/commands/comparisons.d.ts +2 -0
- package/dist/src/commands/comparisons.js +6 -6
- package/dist/src/commands/comparisons.js.map +1 -1
- package/dist/src/commands/config.d.ts +46 -0
- package/dist/src/commands/config.js +465 -0
- package/dist/src/commands/config.js.map +1 -0
- package/dist/src/commands/connections.d.ts +2 -0
- package/dist/src/commands/connections.js +99 -0
- package/dist/src/commands/connections.js.map +1 -0
- package/dist/src/commands/cron-triggers.d.ts +2 -0
- package/dist/src/commands/cron-triggers.js +266 -0
- package/dist/src/commands/cron-triggers.js.map +1 -0
- package/dist/src/commands/database-type-configs.d.ts +2 -0
- package/dist/src/commands/database-type-configs.js +164 -0
- package/dist/src/commands/database-type-configs.js.map +1 -0
- package/dist/src/commands/database-types.d.ts +2 -0
- package/dist/src/commands/database-types.js +471 -0
- package/dist/src/commands/database-types.js.map +1 -0
- package/dist/src/commands/databases.d.ts +65 -0
- package/dist/src/commands/databases.js +1887 -243
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.d.ts +60 -0
- package/dist/src/commands/documents.js +2888 -22
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/email-templates.d.ts +2 -0
- package/dist/src/commands/email-templates.js +175 -0
- package/dist/src/commands/email-templates.js.map +1 -0
- package/dist/src/commands/env.d.ts +23 -0
- package/dist/src/commands/env.js +395 -0
- package/dist/src/commands/env.js.map +1 -0
- package/dist/src/commands/feature-flags.d.ts +14 -0
- package/dist/src/commands/feature-flags.js +117 -0
- package/dist/src/commands/feature-flags.js.map +1 -0
- package/dist/src/commands/functions.d.ts +20 -0
- package/dist/src/commands/functions.js +1630 -0
- package/dist/src/commands/functions.js.map +1 -0
- package/dist/src/commands/group-type-configs.d.ts +2 -0
- package/dist/src/commands/group-type-configs.js +87 -0
- package/dist/src/commands/group-type-configs.js.map +1 -0
- package/dist/src/commands/groups.d.ts +2 -0
- package/dist/src/commands/groups.js +65 -113
- package/dist/src/commands/groups.js.map +1 -1
- package/dist/src/commands/guides.d.ts +223 -0
- package/dist/src/commands/guides.js +627 -69
- package/dist/src/commands/guides.js.map +1 -1
- package/dist/src/commands/init.d.ts +37 -0
- package/dist/src/commands/init.js +1693 -208
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/integrations.d.ts +2 -0
- package/dist/src/commands/integrations.js +428 -187
- package/dist/src/commands/integrations.js.map +1 -1
- package/dist/src/commands/llm.d.ts +2 -0
- package/dist/src/commands/llm.js +4 -2
- package/dist/src/commands/llm.js.map +1 -1
- package/dist/src/commands/locks.d.ts +8 -0
- package/dist/src/commands/locks.js +175 -0
- package/dist/src/commands/locks.js.map +1 -0
- package/dist/src/commands/metadata-category-configs.d.ts +12 -0
- package/dist/src/commands/metadata-category-configs.js +113 -0
- package/dist/src/commands/metadata-category-configs.js.map +1 -0
- package/dist/src/commands/metadata.d.ts +2 -0
- package/dist/src/commands/metadata.js +288 -0
- package/dist/src/commands/metadata.js.map +1 -0
- package/dist/src/commands/prompts.d.ts +2 -0
- package/dist/src/commands/prompts.js +322 -634
- package/dist/src/commands/prompts.js.map +1 -1
- package/dist/src/commands/rule-sets.d.ts +3 -0
- package/dist/src/commands/rule-sets.js +139 -148
- package/dist/src/commands/rule-sets.js.map +1 -1
- package/dist/src/commands/scripts.d.ts +30 -0
- package/dist/src/commands/scripts.js +688 -0
- package/dist/src/commands/scripts.js.map +1 -0
- package/dist/src/commands/secrets.d.ts +2 -0
- package/dist/src/commands/secrets.js +109 -0
- package/dist/src/commands/secrets.js.map +1 -0
- package/dist/src/commands/sessions.d.ts +2 -0
- package/dist/src/commands/sessions.js +76 -0
- package/dist/src/commands/sessions.js.map +1 -0
- package/dist/src/commands/skill.d.ts +2 -0
- package/dist/src/commands/skill.js +29 -0
- package/dist/src/commands/skill.js.map +1 -0
- package/dist/src/commands/sync-app-settings.d.ts +158 -0
- package/dist/src/commands/sync-app-settings.js +328 -0
- package/dist/src/commands/sync-app-settings.js.map +1 -0
- package/dist/src/commands/sync.d.ts +2670 -0
- package/dist/src/commands/sync.js +17144 -834
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/tokens.d.ts +2 -0
- package/dist/src/commands/tokens.js +132 -22
- package/dist/src/commands/tokens.js.map +1 -1
- package/dist/src/commands/users.d.ts +2 -0
- package/dist/src/commands/users.js +542 -24
- package/dist/src/commands/users.js.map +1 -1
- package/dist/src/commands/vars.d.ts +8 -0
- package/dist/src/commands/vars.js +97 -0
- package/dist/src/commands/vars.js.map +1 -0
- package/dist/src/commands/waitlist.d.ts +2 -0
- package/dist/src/commands/waitlist.js +12 -11
- package/dist/src/commands/waitlist.js.map +1 -1
- package/dist/src/commands/webhooks.d.ts +31 -0
- package/dist/src/commands/webhooks.js +633 -0
- package/dist/src/commands/webhooks.js.map +1 -0
- package/dist/src/commands/workflows.d.ts +88 -0
- package/dist/src/commands/workflows.js +1568 -742
- 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 +2550 -0
- package/dist/src/lib/api-client.js +2503 -160
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/app-settings-descriptor.d.ts +263 -0
- package/dist/src/lib/app-settings-descriptor.js +583 -0
- package/dist/src/lib/app-settings-descriptor.js.map +1 -0
- package/dist/src/lib/auth-flow.d.ts +8 -0
- package/dist/src/lib/batch.d.ts +26 -0
- package/dist/src/lib/batch.js +32 -0
- package/dist/src/lib/batch.js.map +1 -0
- package/dist/src/lib/block-layout.d.ts +160 -0
- package/dist/src/lib/block-layout.js +451 -0
- package/dist/src/lib/block-layout.js.map +1 -0
- package/dist/src/lib/block-selector.d.ts +58 -0
- package/dist/src/lib/block-selector.js +92 -0
- package/dist/src/lib/block-selector.js.map +1 -0
- package/dist/src/lib/canonical-json.d.ts +12 -0
- package/dist/src/lib/canonical-json.js +35 -0
- package/dist/src/lib/canonical-json.js.map +1 -0
- package/dist/src/lib/channel.d.ts +30 -0
- package/dist/src/lib/channel.js +68 -0
- package/dist/src/lib/channel.js.map +1 -0
- package/dist/src/lib/cli-manifest.d.ts +68 -0
- package/dist/src/lib/cli-manifest.js +71 -0
- package/dist/src/lib/cli-manifest.js.map +1 -0
- package/dist/src/lib/codegen-shared/generatedFiles.d.ts +101 -0
- package/dist/src/lib/codegen-shared/generatedFiles.js +191 -0
- package/dist/src/lib/codegen-shared/generatedFiles.js.map +1 -0
- package/dist/src/lib/codegen-shared/prettierStable.d.ts +262 -0
- package/dist/src/lib/codegen-shared/prettierStable.js +610 -0
- package/dist/src/lib/codegen-shared/prettierStable.js.map +1 -0
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +38 -0
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +46 -0
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -0
- package/dist/src/lib/collection-export.d.ts +184 -0
- package/dist/src/lib/collection-export.js +252 -0
- package/dist/src/lib/collection-export.js.map +1 -0
- package/dist/src/lib/config-json-field.d.ts +28 -0
- package/dist/src/lib/config-json-field.js +56 -0
- package/dist/src/lib/config-json-field.js.map +1 -0
- package/dist/src/lib/config-object-descriptor.d.ts +127 -0
- package/dist/src/lib/config-object-descriptor.js +740 -0
- package/dist/src/lib/config-object-descriptor.js.map +1 -0
- package/dist/src/lib/config-payload.d.ts +92 -0
- package/dist/src/lib/config-payload.js +161 -0
- package/dist/src/lib/config-payload.js.map +1 -0
- package/dist/src/lib/config-surface.d.ts +141 -0
- package/dist/src/lib/config-surface.js +368 -0
- package/dist/src/lib/config-surface.js.map +1 -0
- package/dist/src/lib/config-toml.d.ts +10 -0
- package/dist/src/lib/config-toml.js +42 -0
- package/dist/src/lib/config-toml.js.map +1 -0
- package/dist/src/lib/config.d.ts +71 -0
- package/dist/src/lib/config.js +71 -68
- package/dist/src/lib/config.js.map +1 -1
- package/dist/src/lib/confirm-prompt.d.ts +83 -0
- package/dist/src/lib/confirm-prompt.js +110 -0
- package/dist/src/lib/confirm-prompt.js.map +1 -0
- package/dist/src/lib/constants.d.ts +11 -0
- package/dist/src/lib/constants.js +12 -0
- package/dist/src/lib/constants.js.map +1 -0
- package/dist/src/lib/crash-handlers.d.ts +20 -0
- package/dist/src/lib/crash-handlers.js +49 -0
- package/dist/src/lib/crash-handlers.js.map +1 -0
- package/dist/src/lib/credentials-store.d.ts +104 -0
- package/dist/src/lib/credentials-store.js +336 -0
- package/dist/src/lib/credentials-store.js.map +1 -0
- package/dist/src/lib/csv.d.ts +47 -0
- package/dist/src/lib/csv.js +172 -0
- package/dist/src/lib/csv.js.map +1 -0
- package/dist/src/lib/data-input.d.ts +23 -0
- package/dist/src/lib/data-input.js +50 -0
- package/dist/src/lib/data-input.js.map +1 -0
- package/dist/src/lib/db-codegen/dbFingerprint.d.ts +10 -0
- package/dist/src/lib/db-codegen/dbFingerprint.js +17 -0
- package/dist/src/lib/db-codegen/dbFingerprint.js.map +1 -0
- package/dist/src/lib/db-codegen/dbGenerator.d.ts +67 -0
- package/dist/src/lib/db-codegen/dbGenerator.js +170 -0
- package/dist/src/lib/db-codegen/dbGenerator.js.map +1 -0
- package/dist/src/lib/db-codegen/dbNaming.d.ts +87 -0
- package/dist/src/lib/db-codegen/dbNaming.js +180 -0
- package/dist/src/lib/db-codegen/dbNaming.js.map +1 -0
- package/dist/src/lib/db-codegen/dbTemplates.d.ts +272 -0
- package/dist/src/lib/db-codegen/dbTemplates.js +480 -0
- package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -0
- package/dist/src/lib/db-codegen/dbTsTypes.d.ts +73 -0
- package/dist/src/lib/db-codegen/dbTsTypes.js +139 -0
- package/dist/src/lib/db-codegen/dbTsTypes.js.map +1 -0
- package/dist/src/lib/db-codegen/dbTypeIR.d.ts +146 -0
- package/dist/src/lib/db-codegen/dbTypeIR.js +525 -0
- package/dist/src/lib/db-codegen/dbTypeIR.js.map +1 -0
- package/dist/src/lib/db-codegen/generated-operation-def-descriptor.d.ts +112 -0
- package/dist/src/lib/db-codegen/generated-operation-def-descriptor.js +211 -0
- package/dist/src/lib/db-codegen/generated-operation-def-descriptor.js.map +1 -0
- package/dist/src/lib/deprecation.d.ts +22 -0
- package/dist/src/lib/deprecation.js +43 -0
- package/dist/src/lib/deprecation.js.map +1 -0
- package/dist/src/lib/document-export-permissions.d.ts +30 -0
- package/dist/src/lib/document-export-permissions.js +54 -0
- package/dist/src/lib/document-export-permissions.js.map +1 -0
- package/dist/src/lib/document-ingest-artifact.d.ts +120 -0
- package/dist/src/lib/document-ingest-artifact.js +505 -0
- package/dist/src/lib/document-ingest-artifact.js.map +1 -0
- package/dist/src/lib/document-ingest-input.d.ts +51 -0
- package/dist/src/lib/document-ingest-input.js +132 -0
- package/dist/src/lib/document-ingest-input.js.map +1 -0
- package/dist/src/lib/document-ingest-rows.d.ts +137 -0
- package/dist/src/lib/document-ingest-rows.js +181 -0
- package/dist/src/lib/document-ingest-rows.js.map +1 -0
- package/dist/src/lib/document-ingest.d.ts +101 -0
- package/dist/src/lib/document-ingest.js +384 -0
- package/dist/src/lib/document-ingest.js.map +1 -0
- package/dist/src/lib/env-resolver-core.d.ts +258 -0
- package/dist/src/lib/env-resolver-core.js +447 -0
- package/dist/src/lib/env-resolver-core.js.map +1 -0
- package/dist/src/lib/env-resolver.d.ts +99 -0
- package/dist/src/lib/env-resolver.js +153 -0
- package/dist/src/lib/env-resolver.js.map +1 -0
- package/dist/src/lib/fetch.d.ts +5 -0
- package/dist/src/lib/function-bundle.d.ts +147 -0
- package/dist/src/lib/function-bundle.js +341 -0
- package/dist/src/lib/function-bundle.js.map +1 -0
- package/dist/src/lib/function-collect.d.ts +123 -0
- package/dist/src/lib/function-collect.js +610 -0
- package/dist/src/lib/function-collect.js.map +1 -0
- package/dist/src/lib/function-db-types.d.ts +202 -0
- package/dist/src/lib/function-db-types.js +870 -0
- package/dist/src/lib/function-db-types.js.map +1 -0
- package/dist/src/lib/function-document-types.d.ts +144 -0
- package/dist/src/lib/function-document-types.js +370 -0
- package/dist/src/lib/function-document-types.js.map +1 -0
- package/dist/src/lib/function-grants-preflight.d.ts +64 -0
- package/dist/src/lib/function-grants-preflight.js +105 -0
- package/dist/src/lib/function-grants-preflight.js.map +1 -0
- package/dist/src/lib/function-log-lines.d.ts +76 -0
- package/dist/src/lib/function-log-lines.js +160 -0
- package/dist/src/lib/function-log-lines.js.map +1 -0
- package/dist/src/lib/function-log-row.d.ts +29 -0
- package/dist/src/lib/function-log-row.js +73 -0
- package/dist/src/lib/function-log-row.js.map +1 -0
- package/dist/src/lib/function-log-tail.d.ts +132 -0
- package/dist/src/lib/function-log-tail.js +262 -0
- package/dist/src/lib/function-log-tail.js.map +1 -0
- package/dist/src/lib/function-run.d.ts +289 -0
- package/dist/src/lib/function-run.js +389 -0
- package/dist/src/lib/function-run.js.map +1 -0
- package/dist/src/lib/function-schema-codegen.d.ts +143 -0
- package/dist/src/lib/function-schema-codegen.js +420 -0
- package/dist/src/lib/function-schema-codegen.js.map +1 -0
- package/dist/src/lib/function-sync.d.ts +459 -0
- package/dist/src/lib/function-sync.js +1258 -0
- package/dist/src/lib/function-sync.js.map +1 -0
- package/dist/src/lib/function-trigger-listing.d.ts +27 -0
- package/dist/src/lib/function-trigger-listing.js +70 -0
- package/dist/src/lib/function-trigger-listing.js.map +1 -0
- package/dist/src/lib/function-typecheck.d.ts +86 -0
- package/dist/src/lib/function-typecheck.js +370 -0
- package/dist/src/lib/function-typecheck.js.map +1 -0
- package/dist/src/lib/function-versions.d.ts +122 -0
- package/dist/src/lib/function-versions.js +182 -0
- package/dist/src/lib/function-versions.js.map +1 -0
- package/dist/src/lib/generated-allowlist.d.ts +28 -0
- package/dist/src/lib/generated-allowlist.js +281 -0
- package/dist/src/lib/generated-allowlist.js.map +1 -0
- package/dist/src/lib/generated-config-surfaces.d.ts +2936 -0
- package/dist/src/lib/generated-config-surfaces.js +10569 -0
- package/dist/src/lib/generated-config-surfaces.js.map +1 -0
- package/dist/src/lib/generated-sdk-types.d.ts +12 -0
- package/dist/src/lib/generated-sdk-types.js +13 -0
- package/dist/src/lib/generated-sdk-types.js.map +1 -0
- package/dist/src/lib/generated-template-lint.d.ts +212 -0
- package/dist/src/lib/generated-template-lint.js +624 -0
- package/dist/src/lib/generated-template-lint.js.map +1 -0
- package/dist/src/lib/init-adopt.d.ts +16 -0
- package/dist/src/lib/init-adopt.js +34 -0
- package/dist/src/lib/init-adopt.js.map +1 -0
- package/dist/src/lib/init-assets.d.ts +39 -0
- package/dist/src/lib/init-assets.js +97 -0
- package/dist/src/lib/init-assets.js.map +1 -0
- package/dist/src/lib/init-client-platforms.d.ts +14 -0
- package/dist/src/lib/init-client-platforms.js +71 -0
- package/dist/src/lib/init-client-platforms.js.map +1 -0
- package/dist/src/lib/init-config.d.ts +98 -0
- package/dist/src/lib/init-config.js +186 -0
- package/dist/src/lib/init-config.js.map +1 -0
- package/dist/src/lib/init-email-redirect-uris.d.ts +37 -0
- package/dist/src/lib/init-email-redirect-uris.js +46 -0
- package/dist/src/lib/init-email-redirect-uris.js.map +1 -0
- package/dist/src/lib/init-ios-links.d.ts +91 -0
- package/dist/src/lib/init-ios-links.js +219 -0
- package/dist/src/lib/init-ios-links.js.map +1 -0
- package/dist/src/lib/init-plan.d.ts +80 -0
- package/dist/src/lib/init-plan.js +95 -0
- package/dist/src/lib/init-plan.js.map +1 -0
- package/dist/src/lib/init-production-env.d.ts +48 -0
- package/dist/src/lib/init-production-env.js +59 -0
- package/dist/src/lib/init-production-env.js.map +1 -0
- package/dist/src/lib/init-schema.d.ts +74 -0
- package/dist/src/lib/init-schema.js +358 -0
- package/dist/src/lib/init-schema.js.map +1 -0
- package/dist/src/lib/init-xcode.d.ts +34 -0
- package/dist/src/lib/init-xcode.js +138 -0
- package/dist/src/lib/init-xcode.js.map +1 -0
- package/dist/src/lib/integration-request-config.d.ts +30 -0
- package/dist/src/lib/integration-request-config.js +145 -0
- package/dist/src/lib/integration-request-config.js.map +1 -0
- package/dist/src/lib/integration-selector.d.ts +42 -0
- package/dist/src/lib/integration-selector.js +46 -0
- package/dist/src/lib/integration-selector.js.map +1 -0
- package/dist/src/lib/ios-app-id.d.ts +34 -0
- package/dist/src/lib/ios-app-id.js +69 -0
- package/dist/src/lib/ios-app-id.js.map +1 -0
- package/dist/src/lib/list-options.d.ts +68 -0
- package/dist/src/lib/list-options.js +89 -0
- package/dist/src/lib/list-options.js.map +1 -0
- package/dist/src/lib/local-state.d.ts +55 -0
- package/dist/src/lib/local-state.js +167 -0
- package/dist/src/lib/local-state.js.map +1 -0
- package/dist/src/lib/local-test-cases.d.ts +63 -0
- package/dist/src/lib/local-test-cases.js +136 -0
- package/dist/src/lib/local-test-cases.js.map +1 -0
- package/dist/src/lib/log-inspection.d.ts +715 -0
- package/dist/src/lib/log-inspection.js +816 -0
- package/dist/src/lib/log-inspection.js.map +1 -0
- package/dist/src/lib/logout-admin-session.d.ts +33 -0
- package/dist/src/lib/logout-admin-session.js +70 -0
- package/dist/src/lib/logout-admin-session.js.map +1 -0
- package/dist/src/lib/migration-nag.d.ts +49 -0
- package/dist/src/lib/migration-nag.js +163 -0
- package/dist/src/lib/migration-nag.js.map +1 -0
- package/dist/src/lib/object-status-filter.d.ts +22 -0
- package/dist/src/lib/object-status-filter.js +45 -0
- package/dist/src/lib/object-status-filter.js.map +1 -0
- package/dist/src/lib/output.d.ts +124 -0
- package/dist/src/lib/output.js +219 -8
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/package-manager.d.ts +140 -0
- package/dist/src/lib/package-manager.js +305 -0
- package/dist/src/lib/package-manager.js.map +1 -0
- package/dist/src/lib/paginate.d.ts +98 -0
- package/dist/src/lib/paginate.js +112 -0
- package/dist/src/lib/paginate.js.map +1 -0
- package/dist/src/lib/platform-owned.d.ts +63 -0
- package/dist/src/lib/platform-owned.js +85 -0
- package/dist/src/lib/platform-owned.js.map +1 -0
- package/dist/src/lib/project-config.d.ts +122 -0
- package/dist/src/lib/project-config.js +244 -0
- package/dist/src/lib/project-config.js.map +1 -0
- package/dist/src/lib/prompt-cost-format.d.ts +11 -0
- package/dist/src/lib/prompt-cost-format.js +41 -0
- package/dist/src/lib/prompt-cost-format.js.map +1 -0
- package/dist/src/lib/prompt-schema-codegen.d.ts +147 -0
- package/dist/src/lib/prompt-schema-codegen.js +462 -0
- package/dist/src/lib/prompt-schema-codegen.js.map +1 -0
- package/dist/src/lib/query-operators.d.ts +43 -0
- package/dist/src/lib/query-operators.js +80 -0
- package/dist/src/lib/query-operators.js.map +1 -0
- package/dist/src/lib/record-filter.d.ts +18 -0
- package/dist/src/lib/record-filter.js +55 -0
- package/dist/src/lib/record-filter.js.map +1 -0
- package/dist/src/lib/refresh-admin-credentials.d.ts +73 -0
- package/dist/src/lib/refresh-admin-credentials.js +123 -0
- package/dist/src/lib/refresh-admin-credentials.js.map +1 -0
- package/dist/src/lib/resolve-init-dev-port.d.ts +56 -0
- package/dist/src/lib/resolve-init-dev-port.js +55 -0
- package/dist/src/lib/resolve-init-dev-port.js.map +1 -0
- package/dist/src/lib/resolve-init-server.d.ts +64 -0
- package/dist/src/lib/resolve-init-server.js +77 -0
- package/dist/src/lib/resolve-init-server.js.map +1 -0
- package/dist/src/lib/resolve-owner.d.ts +19 -0
- package/dist/src/lib/resolve-owner.js +20 -0
- package/dist/src/lib/resolve-owner.js.map +1 -0
- package/dist/src/lib/resolve-platform.d.ts +74 -0
- package/dist/src/lib/resolve-platform.js +105 -0
- package/dist/src/lib/resolve-platform.js.map +1 -0
- package/dist/src/lib/run-status.d.ts +19 -0
- package/dist/src/lib/run-status.generated.d.ts +39 -0
- package/dist/src/lib/run-status.generated.js +66 -0
- package/dist/src/lib/run-status.generated.js.map +1 -0
- package/dist/src/lib/run-status.js +19 -0
- package/dist/src/lib/run-status.js.map +1 -0
- package/dist/src/lib/server-text-normalization.d.ts +51 -0
- package/dist/src/lib/server-text-normalization.js +90 -0
- package/dist/src/lib/server-text-normalization.js.map +1 -0
- package/dist/src/lib/server-url.d.ts +22 -0
- package/dist/src/lib/server-url.js +33 -0
- package/dist/src/lib/server-url.js.map +1 -0
- package/dist/src/lib/signing-secret-status.d.ts +81 -0
- package/dist/src/lib/signing-secret-status.js +116 -0
- package/dist/src/lib/signing-secret-status.js.map +1 -0
- package/dist/src/lib/skill-installer.d.ts +71 -0
- package/dist/src/lib/skill-installer.js +441 -0
- package/dist/src/lib/skill-installer.js.map +1 -0
- package/dist/src/lib/snapshot-audit-source.d.ts +45 -0
- package/dist/src/lib/snapshot-audit-source.js +58 -0
- package/dist/src/lib/snapshot-audit-source.js.map +1 -0
- package/dist/src/lib/snapshot-audit-store.d.ts +52 -0
- package/dist/src/lib/snapshot-audit-store.js +196 -0
- package/dist/src/lib/snapshot-audit-store.js.map +1 -0
- package/dist/src/lib/snapshot-audit.d.ts +207 -0
- package/dist/src/lib/snapshot-audit.js +431 -0
- package/dist/src/lib/snapshot-audit.js.map +1 -0
- package/dist/src/lib/snapshot-build-rows.d.ts +60 -0
- package/dist/src/lib/snapshot-build-rows.js +87 -0
- package/dist/src/lib/snapshot-build-rows.js.map +1 -0
- package/dist/src/lib/snapshot-build.d.ts +50 -0
- package/dist/src/lib/snapshot-build.js +111 -0
- package/dist/src/lib/snapshot-build.js.map +1 -0
- package/dist/src/lib/snapshot-manifest-layout.d.ts +61 -0
- package/dist/src/lib/snapshot-manifest-layout.js +70 -0
- package/dist/src/lib/snapshot-manifest-layout.js.map +1 -0
- package/dist/src/lib/snapshots.d.ts +98 -0
- package/dist/src/lib/snapshots.js +294 -0
- package/dist/src/lib/snapshots.js.map +1 -0
- package/dist/src/lib/step-run-table.d.ts +43 -0
- package/dist/src/lib/step-run-table.js +129 -0
- package/dist/src/lib/step-run-table.js.map +1 -0
- package/dist/src/lib/storage-pending-retry.d.ts +26 -0
- package/dist/src/lib/storage-pending-retry.js +42 -0
- package/dist/src/lib/storage-pending-retry.js.map +1 -0
- package/dist/src/lib/swift-codegen/agentGenerator.d.ts +42 -0
- package/dist/src/lib/swift-codegen/agentGenerator.js +118 -0
- package/dist/src/lib/swift-codegen/agentGenerator.js.map +1 -0
- package/dist/src/lib/swift-codegen/banners.d.ts +24 -0
- package/dist/src/lib/swift-codegen/banners.js +25 -0
- package/dist/src/lib/swift-codegen/banners.js.map +1 -0
- package/dist/src/lib/swift-codegen/dbGenerator.d.ts +113 -0
- package/dist/src/lib/swift-codegen/dbGenerator.js +926 -0
- package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -0
- package/dist/src/lib/swift-codegen/dbSwiftTypes.d.ts +42 -0
- package/dist/src/lib/swift-codegen/dbSwiftTypes.js +100 -0
- package/dist/src/lib/swift-codegen/dbSwiftTypes.js.map +1 -0
- package/dist/src/lib/swift-codegen/functionGenerator.d.ts +139 -0
- package/dist/src/lib/swift-codegen/functionGenerator.js +462 -0
- package/dist/src/lib/swift-codegen/functionGenerator.js.map +1 -0
- package/dist/src/lib/swift-codegen/generator.d.ts +100 -0
- package/dist/src/lib/swift-codegen/generator.js +457 -0
- package/dist/src/lib/swift-codegen/generator.js.map +1 -0
- package/dist/src/lib/swift-codegen/schemaToSwift.d.ts +87 -0
- package/dist/src/lib/swift-codegen/schemaToSwift.js +661 -0
- package/dist/src/lib/swift-codegen/schemaToSwift.js.map +1 -0
- package/dist/src/lib/swift-codegen/siblingSymbols.d.ts +94 -0
- package/dist/src/lib/swift-codegen/siblingSymbols.js +155 -0
- package/dist/src/lib/swift-codegen/siblingSymbols.js.map +1 -0
- package/dist/src/lib/swift-codegen/swiftNaming.d.ts +85 -0
- package/dist/src/lib/swift-codegen/swiftNaming.js +198 -0
- package/dist/src/lib/swift-codegen/swiftNaming.js.map +1 -0
- package/dist/src/lib/sync-dir-selector.d.ts +21 -0
- package/dist/src/lib/sync-dir-selector.js +30 -0
- package/dist/src/lib/sync-dir-selector.js.map +1 -0
- package/dist/src/lib/sync-paths.d.ts +128 -0
- package/dist/src/lib/sync-paths.js +195 -0
- package/dist/src/lib/sync-paths.js.map +1 -0
- package/dist/src/lib/sync-resource-types.d.ts +563 -0
- package/dist/src/lib/sync-resource-types.js +1073 -0
- package/dist/src/lib/sync-resource-types.js.map +1 -0
- package/dist/src/lib/sync-selectors.d.ts +138 -0
- package/dist/src/lib/sync-selectors.js +289 -0
- package/dist/src/lib/sync-selectors.js.map +1 -0
- package/dist/src/lib/template.d.ts +170 -0
- package/dist/src/lib/template.js +484 -68
- package/dist/src/lib/template.js.map +1 -1
- package/dist/src/lib/test-case-file-names.d.ts +40 -0
- package/dist/src/lib/test-case-file-names.js +91 -0
- package/dist/src/lib/test-case-file-names.js.map +1 -0
- package/dist/src/lib/test-case-keys.d.ts +29 -0
- package/dist/src/lib/test-case-keys.js +55 -0
- package/dist/src/lib/test-case-keys.js.map +1 -0
- package/dist/src/lib/test-case-variables.d.ts +29 -0
- package/dist/src/lib/test-case-variables.js +71 -0
- package/dist/src/lib/test-case-variables.js.map +1 -0
- package/dist/src/lib/token-inject.d.ts +56 -0
- package/dist/src/lib/token-inject.js +204 -0
- package/dist/src/lib/token-inject.js.map +1 -0
- package/dist/src/lib/toml-database-config.d.ts +123 -0
- package/dist/src/lib/toml-database-config.js +544 -0
- package/dist/src/lib/toml-database-config.js.map +1 -0
- package/dist/src/lib/toml-metadata-config.d.ts +151 -0
- package/dist/src/lib/toml-metadata-config.js +476 -0
- package/dist/src/lib/toml-metadata-config.js.map +1 -0
- package/dist/src/lib/toml-native-form.d.ts +46 -0
- package/dist/src/lib/toml-native-form.js +78 -0
- package/dist/src/lib/toml-native-form.js.map +1 -0
- package/dist/src/lib/toml-params-validator.d.ts +129 -0
- package/dist/src/lib/toml-params-validator.js +298 -0
- package/dist/src/lib/toml-params-validator.js.map +1 -0
- package/dist/src/lib/toml-scalar-edit.d.ts +43 -0
- package/dist/src/lib/toml-scalar-edit.js +283 -0
- package/dist/src/lib/toml-scalar-edit.js.map +1 -0
- package/dist/src/lib/user-selector.d.ts +24 -0
- package/dist/src/lib/user-selector.js +33 -0
- package/dist/src/lib/user-selector.js.map +1 -0
- package/dist/src/lib/version-check.d.ts +35 -0
- package/dist/src/lib/version-check.js +241 -0
- package/dist/src/lib/version-check.js.map +1 -0
- package/dist/src/lib/watch.d.ts +121 -0
- package/dist/src/lib/watch.js +169 -0
- package/dist/src/lib/watch.js.map +1 -0
- package/dist/src/lib/web-url.d.ts +40 -0
- package/dist/src/lib/web-url.js +76 -0
- package/dist/src/lib/web-url.js.map +1 -0
- package/dist/src/lib/webhook-deliver.d.ts +209 -0
- package/dist/src/lib/webhook-deliver.js +519 -0
- package/dist/src/lib/webhook-deliver.js.map +1 -0
- package/dist/src/lib/workflow-apply.d.ts +110 -0
- package/dist/src/lib/workflow-apply.js +164 -0
- package/dist/src/lib/workflow-apply.js.map +1 -0
- package/dist/src/lib/workflow-codegen/generated-schema-descriptor.d.ts +129 -0
- package/dist/src/lib/workflow-codegen/generated-schema-descriptor.js +269 -0
- package/dist/src/lib/workflow-codegen/generated-schema-descriptor.js.map +1 -0
- package/dist/src/lib/workflow-codegen/generator.d.ts +96 -0
- package/dist/src/lib/workflow-codegen/generator.js +361 -0
- package/dist/src/lib/workflow-codegen/generator.js.map +1 -0
- package/dist/src/lib/workflow-codegen/invokerIR.d.ts +94 -0
- package/dist/src/lib/workflow-codegen/invokerIR.js +76 -0
- package/dist/src/lib/workflow-codegen/invokerIR.js.map +1 -0
- package/dist/src/lib/workflow-codegen/naming.d.ts +33 -0
- package/dist/src/lib/workflow-codegen/naming.js +81 -0
- package/dist/src/lib/workflow-codegen/naming.js.map +1 -0
- package/dist/src/lib/workflow-codegen/schemaToTs.d.ts +80 -0
- package/dist/src/lib/workflow-codegen/schemaToTs.js +303 -0
- package/dist/src/lib/workflow-codegen/schemaToTs.js.map +1 -0
- package/dist/src/lib/workflow-config-apply.d.ts +70 -0
- package/dist/src/lib/workflow-config-apply.js +137 -0
- package/dist/src/lib/workflow-config-apply.js.map +1 -0
- package/dist/src/lib/workflow-config-sidecar.d.ts +63 -0
- package/dist/src/lib/workflow-config-sidecar.js +96 -0
- package/dist/src/lib/workflow-config-sidecar.js.map +1 -0
- package/dist/src/lib/workflow-defaults.d.ts +29 -0
- package/dist/src/lib/workflow-defaults.js +41 -0
- package/dist/src/lib/workflow-defaults.js.map +1 -0
- package/dist/src/lib/workflow-fragments.d.ts +64 -0
- package/dist/src/lib/workflow-fragments.js +342 -0
- package/dist/src/lib/workflow-fragments.js.map +1 -0
- package/dist/src/lib/workflow-include-preserve.d.ts +76 -0
- package/dist/src/lib/workflow-include-preserve.js +286 -0
- package/dist/src/lib/workflow-include-preserve.js.map +1 -0
- package/dist/src/lib/workflow-payload.d.ts +98 -0
- package/dist/src/lib/workflow-payload.js +178 -0
- package/dist/src/lib/workflow-payload.js.map +1 -0
- package/dist/src/lib/workflow-toml-validator.d.ts +211 -0
- package/dist/src/lib/workflow-toml-validator.js +770 -0
- package/dist/src/lib/workflow-toml-validator.js.map +1 -0
- package/dist/src/lib/workflow-usage.d.ts +196 -0
- package/dist/src/lib/workflow-usage.js +310 -0
- package/dist/src/lib/workflow-usage.js.map +1 -0
- package/dist/src/types/index.d.ts +591 -0
- package/dist/src/validators.d.ts +65 -0
- package/dist/src/validators.js +64 -0
- package/dist/src/validators.js.map +1 -0
- package/package.json +34 -9
|
@@ -0,0 +1,1258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `functions/<key>.toml` on the wire and back (#3179, project
|
|
3
|
+
* `server-functions`).
|
|
4
|
+
*
|
|
5
|
+
* A function is the one configuration object whose file is not its whole
|
|
6
|
+
* authored surface: the TOML states the gate, the entry and the limits, and the
|
|
7
|
+
* TypeScript the entry reaches is the rest of it. `config push` therefore sends
|
|
8
|
+
* TWO things — a header (create/update) and an immutable config VERSION
|
|
9
|
+
* carrying the authored TOML bytes, every source file and the built bundle —
|
|
10
|
+
* and `config pull` writes both halves back.
|
|
11
|
+
*
|
|
12
|
+
* ── The envelope hash is the shared answer to "changed?" ──────────────────
|
|
13
|
+
*
|
|
14
|
+
* The server decides whether a push is a no-op on `envelopeHash` (sha256 over
|
|
15
|
+
* the TOML bytes, the sources and the bundle), because a comment-only or
|
|
16
|
+
* CRLF-only edit leaves the BUNDLE identical and must still make a version
|
|
17
|
+
* (F-003). The CLI computes the same number to decide whether to make the call
|
|
18
|
+
* at all, through `function-bundle.ts`'s copy of the server's canonical
|
|
19
|
+
* encoding. If those two encodings ever drift, one side re-pushes forever and
|
|
20
|
+
* the other silently drops an edit — `function-bundle-3179.test.ts` pins them
|
|
21
|
+
* against each other.
|
|
22
|
+
*
|
|
23
|
+
* ── Pull writes bytes ────────────────────────────────────────────────────
|
|
24
|
+
*
|
|
25
|
+
* Canonical re-serialization cannot preserve comments, key order or line
|
|
26
|
+
* endings, so the envelope stores the AUTHORED bytes and pull writes those
|
|
27
|
+
* (F-009). It also never trusts a stored path: the files it writes are named by
|
|
28
|
+
* the server, so a path that normalizes outside the tree — or into
|
|
29
|
+
* `node_modules`, which is never authored source — is refused rather than
|
|
30
|
+
* written to a developer's disk. The lexical rules cannot see a symlinked
|
|
31
|
+
* directory inside the tree, so the write step resolves the real path one last
|
|
32
|
+
* time and refuses a destination that lands outside the real root.
|
|
33
|
+
*/
|
|
34
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync, } from "fs";
|
|
35
|
+
import { dirname, isAbsolute, join, posix as posixPath, relative, resolve, sep, } from "path";
|
|
36
|
+
import { parseConfigToml, stringifyConfigToml } from "./config-toml.js";
|
|
37
|
+
import { DOCUMENT_SCHEMA_TREE_FILE, resolveDocumentSchema, } from "./function-document-types.js";
|
|
38
|
+
import { SERVER_FUNCTION_TABLE } from "./config-surface.js";
|
|
39
|
+
import { checkCronTriggerEntry, hasDeclaredTriggers, hasRetiredDatabaseTriggerBlock, ignoredFunctionKeys, retiredDatabaseTriggerTypes, ignoredFunctionKeyWarnings, normalizeTriggerDeclaration, RETIRED_FUNCTION_RUNTIME_KEYS, TriggerDeclarationError, validateSigningSecretDeclaration, } from "./generated-config-surfaces.js";
|
|
40
|
+
import { canonicalJsonStringify } from "./canonical-json.js";
|
|
41
|
+
import { buildPayloadFromToml, projectRecordToToml, tomlOwned } from "./config-payload.js";
|
|
42
|
+
import { serializeJsonField } from "./config-json-field.js";
|
|
43
|
+
import { buildFunctionBundle, FunctionBuildError, functionEnvelopeHash, FUNCTION_ENVELOPE_SCHEMA_VERSION, isDependencyPath, } from "./function-bundle.js";
|
|
44
|
+
import { collectFunctionManifest, FunctionCollectError, } from "./function-collect.js";
|
|
45
|
+
const toPosix = (value) => value.split(sep).join("/");
|
|
46
|
+
/**
|
|
47
|
+
* Collected manifests, keyed by the BUILT BUNDLE they were collected from.
|
|
48
|
+
*
|
|
49
|
+
* Memoized against `contentHash`: the same bytes always collect the same way,
|
|
50
|
+
* and a tree edited mid-command hashes differently and is collected again
|
|
51
|
+
* rather than served a stale answer.
|
|
52
|
+
*/
|
|
53
|
+
const collectedManifests = new Map();
|
|
54
|
+
/**
|
|
55
|
+
* Collect one function's manifest, or answer from this command's memo.
|
|
56
|
+
*
|
|
57
|
+
* `FunctionCollectError` is translated to the push loop's one failure class by
|
|
58
|
+
* the callers, so a collect failure reads like any other build failure for
|
|
59
|
+
* that function and lands nothing.
|
|
60
|
+
*/
|
|
61
|
+
export async function collectManifestForBundle(options) {
|
|
62
|
+
const memo = collectedManifests.get(options.contentHash);
|
|
63
|
+
if (memo)
|
|
64
|
+
return memo;
|
|
65
|
+
const collected = await collectFunctionManifest({
|
|
66
|
+
configDir: options.configDir,
|
|
67
|
+
entry: options.entry,
|
|
68
|
+
functionKey: options.functionKey,
|
|
69
|
+
bundle: options.bundle,
|
|
70
|
+
});
|
|
71
|
+
collectedManifests.set(options.contentHash, collected);
|
|
72
|
+
return collected;
|
|
73
|
+
}
|
|
74
|
+
// ── The trigger rules, read from the one implementation ───────────────────
|
|
75
|
+
/**
|
|
76
|
+
* The server's own `[function.triggers.*]` rules, applied to a parsed
|
|
77
|
+
* `[function]` table, with "nothing declared" spelled as null so a caller can
|
|
78
|
+
* skip the work.
|
|
79
|
+
*
|
|
80
|
+
* `normalizeTriggerDeclaration` comes from `generated-config-surfaces.ts` —
|
|
81
|
+
* `src/config-surface/trigger-declaration.ts` vendored verbatim. Until #3320
|
|
82
|
+
* the CLI ran a hand-written restatement of those rules
|
|
83
|
+
* (`cli/src/lib/function-triggers.ts`), kept in step only by a paired-fixture
|
|
84
|
+
* test; the artifact's `--check` makes drift a build failure instead.
|
|
85
|
+
*
|
|
86
|
+
* Nothing here is the CLI's own any more. Until #3376 this function added a
|
|
87
|
+
* five-field count check of its own, justified as "not a second copy of a
|
|
88
|
+
* rule" precisely because the declaration path never called `parseCron`. It
|
|
89
|
+
* calls it now — `functionTomlPreflightErrors` runs `checkCronTriggerEntry`,
|
|
90
|
+
* which is the server's grammar and the server's sentence — so the precheck
|
|
91
|
+
* was exactly the duplicate this project exists to remove, and went with the
|
|
92
|
+
* justification.
|
|
93
|
+
*/
|
|
94
|
+
export function normalizeFunctionTriggers(functionTable) {
|
|
95
|
+
const declaration = normalizeTriggerDeclaration(functionTable);
|
|
96
|
+
if (!hasDeclaredTriggers(declaration))
|
|
97
|
+
return null;
|
|
98
|
+
return declaration;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The retired runtime keys a `functions/<key>.toml` still carries — #3482.
|
|
102
|
+
*
|
|
103
|
+
* NOT a refusal. Through phase 4 `mode`, `durable` and a cron entry's `mode`
|
|
104
|
+
* are accepted and ignored, with one warning each and every other key of the
|
|
105
|
+
* file applied; phase 5 (#3188) refuses them. The sentences come from the one
|
|
106
|
+
* vendored scanner, so the warning `config push` prints is the sentence the
|
|
107
|
+
* server answers with.
|
|
108
|
+
*/
|
|
109
|
+
export function functionTomlIgnoredKeyWarnings(options) {
|
|
110
|
+
return ignoredFunctionKeyWarnings(`functions/${options.functionKey}.toml`, options.tomlData?.function);
|
|
111
|
+
}
|
|
112
|
+
/** The `entry` refusal, or null. One spelling, two readers. */
|
|
113
|
+
function functionEntryError(functionKey, table) {
|
|
114
|
+
if (String(table?.entry ?? "").trim())
|
|
115
|
+
return null;
|
|
116
|
+
return (`function '${functionKey}': \`entry\` is required — name the TypeScript ` +
|
|
117
|
+
"entry point, relative to the config tree root.");
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Everything about one `functions/<key>.toml` that `config push` can decide
|
|
121
|
+
* from the file alone (#3320).
|
|
122
|
+
*
|
|
123
|
+
* This is the seam the issue is about. These checks used to run inside the
|
|
124
|
+
* APPLY loop, so a `durable = true` beside a webhook block — or a missing
|
|
125
|
+
* `entry`, or a signed scheme with no `signingSecret` — was raised after app
|
|
126
|
+
* settings, vars, integrations, webhooks, cron triggers and database types had
|
|
127
|
+
* already been written, while the command's own framing promised that a
|
|
128
|
+
* refusal meant nothing was applied. Running them in the front-loaded pass
|
|
129
|
+
* (#976) makes that framing true for this class of error.
|
|
130
|
+
*
|
|
131
|
+
* Every rule here is the SERVER's, reached through the generated surface
|
|
132
|
+
* artifact (#3320 part 0), not a local imitation of it. What is deliberately
|
|
133
|
+
* absent is app-state-dependent by construction: whether the referenced secret
|
|
134
|
+
* exists, whether the scheme's `config` is valid under this environment's JWKS
|
|
135
|
+
* policy, per-app caps, key collisions, archived tombstones. A local guess at
|
|
136
|
+
* any of those would be a preflight that gives a false all-clear.
|
|
137
|
+
*
|
|
138
|
+
* Returns the messages the apply loop would have raised, verbatim — so the
|
|
139
|
+
* author reads the same sentence wherever it reaches them, and `config diff`
|
|
140
|
+
* can report it under Invalid (#2880 symmetry).
|
|
141
|
+
*/
|
|
142
|
+
export function functionTomlPreflightErrors(options) {
|
|
143
|
+
const { functionKey, tomlData } = options;
|
|
144
|
+
const table = tomlData?.function ?? {};
|
|
145
|
+
const errors = [];
|
|
146
|
+
const entryError = functionEntryError(functionKey, table);
|
|
147
|
+
if (entryError)
|
|
148
|
+
errors.push(entryError);
|
|
149
|
+
try {
|
|
150
|
+
const declaration = normalizeFunctionTriggers(table);
|
|
151
|
+
const webhook = declaration?.webhook;
|
|
152
|
+
if (webhook) {
|
|
153
|
+
// The file-decidable half of the webhook rule: which schemes need a
|
|
154
|
+
// secret, that one is supplied, and that it is a whole reference. The
|
|
155
|
+
// server runs this same function at the write boundary, and then asks
|
|
156
|
+
// the questions only it can (does `KEY` exist?).
|
|
157
|
+
const secretError = validateSigningSecretDeclaration({
|
|
158
|
+
verificationScheme: webhook.verificationScheme,
|
|
159
|
+
signingSecret: webhook.signingSecret,
|
|
160
|
+
});
|
|
161
|
+
if (secretError) {
|
|
162
|
+
errors.push(`function '${functionKey}': ${secretError.message}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// #3376 — cron expression and timezone, the server's own grammar and
|
|
166
|
+
// `Intl` probe, vendored. One message per broken entry rather than the
|
|
167
|
+
// first: the rule answers rather than throwing, so the file's errors are
|
|
168
|
+
// enumerated the way every other preflight rule enumerates them. The
|
|
169
|
+
// FIRST is byte-identical to what the push boundary would have answered,
|
|
170
|
+
// which is the only one the server ever gets to.
|
|
171
|
+
for (const entry of declaration?.cron ?? []) {
|
|
172
|
+
const refusal = checkCronTriggerEntry(entry);
|
|
173
|
+
if (refusal) {
|
|
174
|
+
errors.push(`function '${functionKey}': ${refusal.message}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
// Throw-based, so the first broken rule per file wins — exactly what the
|
|
180
|
+
// apply loop reported.
|
|
181
|
+
if (err instanceof TriggerDeclarationError) {
|
|
182
|
+
errors.push(`function '${functionKey}': ${err.message}`);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return errors;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Build everything the config-version route needs for one function file.
|
|
192
|
+
*
|
|
193
|
+
* Throws `FunctionBuildError` for a missing `entry`, a missing entry file, a
|
|
194
|
+
* build error, or an import that escapes the config tree — one class, so the
|
|
195
|
+
* push loop has one thing to report and pushes nothing for that function.
|
|
196
|
+
*
|
|
197
|
+
* The file-decidable checks are `functionTomlPreflightErrors`', kept here as
|
|
198
|
+
* defense in depth (#3179's dual-validation shape): `config push` runs them in
|
|
199
|
+
* its preflight now, but `functions` commands and a direct caller reach this
|
|
200
|
+
* function without one.
|
|
201
|
+
*/
|
|
202
|
+
export async function buildFunctionConfigPush(options) {
|
|
203
|
+
const { configDir, functionKey, filePath, tomlData } = options;
|
|
204
|
+
const table = tomlData?.function ?? {};
|
|
205
|
+
const entry = String(table.entry ?? "").trim();
|
|
206
|
+
const preflight = functionTomlPreflightErrors({
|
|
207
|
+
filePath,
|
|
208
|
+
functionKey,
|
|
209
|
+
tomlData,
|
|
210
|
+
});
|
|
211
|
+
if (preflight.length > 0)
|
|
212
|
+
throw new FunctionBuildError(preflight[0]);
|
|
213
|
+
const built = await buildFunctionBundle({ configDir, entry, functionKey });
|
|
214
|
+
// #3187 — the manifest, collected by RUNNING the tree's module scope in a
|
|
215
|
+
// spawned, scrubbed child process (D3187-SO-006). Fail-closed: a push whose
|
|
216
|
+
// collect fails is refused naming the error, because the manifest is inside
|
|
217
|
+
// the envelope and "push now, collect later" would mean a different version.
|
|
218
|
+
let manifest = null;
|
|
219
|
+
try {
|
|
220
|
+
const collected = await collectManifestForBundle({
|
|
221
|
+
configDir,
|
|
222
|
+
entry: built.entry,
|
|
223
|
+
functionKey,
|
|
224
|
+
contentHash: built.contentHash,
|
|
225
|
+
bundle: built.bundle.toString("utf-8"),
|
|
226
|
+
});
|
|
227
|
+
manifest = Buffer.from(JSON.stringify(collected), "utf-8").toString("base64");
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
if (error instanceof FunctionCollectError) {
|
|
231
|
+
throw new FunctionBuildError(error.message);
|
|
232
|
+
}
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
// The AUTHORED bytes. Read from disk rather than re-serialized, so the round
|
|
236
|
+
// trip is byte-identical (F-009).
|
|
237
|
+
const authored = readFileSync(filePath);
|
|
238
|
+
// #3455 — the document schema this version is pushed with.
|
|
239
|
+
const documentSchemaResolution = resolveDocumentSchema(configDir);
|
|
240
|
+
const limits = normalizeLimits(table.limits);
|
|
241
|
+
const push = {
|
|
242
|
+
entry: built.entry,
|
|
243
|
+
toml: authored.toString("base64"),
|
|
244
|
+
sources: built.sources.map((s) => ({
|
|
245
|
+
path: s.path,
|
|
246
|
+
content: s.content.toString("base64"),
|
|
247
|
+
})),
|
|
248
|
+
bundle: built.bundle.toString("base64"),
|
|
249
|
+
contentHash: built.contentHash,
|
|
250
|
+
manifest,
|
|
251
|
+
envelopeHash: "",
|
|
252
|
+
...(limits ? { limits } : {}),
|
|
253
|
+
// #3455 — the app's declared DOCUMENT schema, by the one resolution rule
|
|
254
|
+
// push, pull and diff share. Carried inside the envelope, so an edit to
|
|
255
|
+
// `models/models.toml` changes every function's `envelopeHash` and the
|
|
256
|
+
// next push mints a new version of each: the consequence D3455-001 states,
|
|
257
|
+
// and what makes a running version's seed source knowable.
|
|
258
|
+
...(documentSchemaResolution.source
|
|
259
|
+
? {
|
|
260
|
+
documentSchema: Buffer.from(documentSchemaResolution.source.content, "utf-8").toString("base64"),
|
|
261
|
+
}
|
|
262
|
+
: {}),
|
|
263
|
+
};
|
|
264
|
+
push.envelopeHash = functionEnvelopeHash({
|
|
265
|
+
schemaVersion: FUNCTION_ENVELOPE_SCHEMA_VERSION,
|
|
266
|
+
entry: push.entry,
|
|
267
|
+
toml: push.toml,
|
|
268
|
+
sources: push.sources,
|
|
269
|
+
bundle: push.bundle,
|
|
270
|
+
manifest: push.manifest,
|
|
271
|
+
documentSchema: push.documentSchema,
|
|
272
|
+
});
|
|
273
|
+
return push;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* The per-function failure when a server DID NOT retain the document schema it
|
|
277
|
+
* was sent — #3455 (D3455-010), criterion 7.
|
|
278
|
+
*
|
|
279
|
+
* The server builds its envelope from explicit known fields
|
|
280
|
+
* (`server-functions-controller.ts`) and answers the `envelopeHash` it stored.
|
|
281
|
+
* A server that predates this change drops `documentSchema` silently: it
|
|
282
|
+
* answers 200, the CLI reports the function pushed, first-writer seeding never
|
|
283
|
+
* works, and every later `config diff` reads Modified for a reason nobody can
|
|
284
|
+
* see. Comparing the hash it stored with the one this CLI built is the cheap
|
|
285
|
+
* acknowledgment that closes that.
|
|
286
|
+
*
|
|
287
|
+
* Gated on having SENT a schema, which is exactly what criterion 7 says
|
|
288
|
+
* ("reported successful only when the server retained the schema it was
|
|
289
|
+
* sent"): with nothing sent there is nothing to retain, and naming the
|
|
290
|
+
* document schema in a mismatch caused by something else would send an
|
|
291
|
+
* operator to the wrong place. A response with no `envelopeHash` at all is not
|
|
292
|
+
* judged — silence is not a disagreement.
|
|
293
|
+
*/
|
|
294
|
+
export function documentSchemaRetentionFailure(input) {
|
|
295
|
+
if (!input.sentDocumentSchema)
|
|
296
|
+
return null;
|
|
297
|
+
const stored = input.storedEnvelopeHash;
|
|
298
|
+
if (typeof stored !== "string" || stored.length === 0)
|
|
299
|
+
return null;
|
|
300
|
+
if (stored === input.builtEnvelopeHash)
|
|
301
|
+
return null;
|
|
302
|
+
return ("server did not retain the document schema (envelope hash differs): the " +
|
|
303
|
+
"server needs an upgrade before first-writer seeding works");
|
|
304
|
+
}
|
|
305
|
+
export function documentSchemaDropAnswer(documentSchemaModels) {
|
|
306
|
+
if (Array.isArray(documentSchemaModels)) {
|
|
307
|
+
const models = documentSchemaModels;
|
|
308
|
+
return {
|
|
309
|
+
outcome: "drops",
|
|
310
|
+
message: `This push DROPS the document schema the running version ` +
|
|
311
|
+
`carries (${models.length > 0 ? models.join(", ") : "no models"}): ` +
|
|
312
|
+
`the project has no models/models.toml and the tree has no ` +
|
|
313
|
+
`generated copy, so a function's first write can no longer ` +
|
|
314
|
+
`seed a document model's metadata.`,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
if (documentSchemaModels === null)
|
|
318
|
+
return { outcome: "none" };
|
|
319
|
+
return { outcome: "unsupported" };
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Says ONCE — for the whole push, not once per function — that this push drops
|
|
323
|
+
* a document schema a running version carries (#3455, D3455-004).
|
|
324
|
+
*
|
|
325
|
+
* Removing `models/models.toml` and its generated tree copy is a legitimate
|
|
326
|
+
* edit, so it is a warning and not a refusal; but it turns first-writer seeding
|
|
327
|
+
* off for every document these functions touch, and that must not be silent.
|
|
328
|
+
*
|
|
329
|
+
* The state this holds is "has the run been told yet", and the rule is that
|
|
330
|
+
* only an ANSWER settles it. A function whose active version carries no schema
|
|
331
|
+
* has told us nothing about its siblings, so the next function is still asked —
|
|
332
|
+
* getting that backwards silences a real drop whenever a schema-less function
|
|
333
|
+
* happens to be pushed first, which is the order nothing in the tree controls.
|
|
334
|
+
* The model names live inside the version's envelope rather than on its row,
|
|
335
|
+
* which is why this is a read and not a field of the listing already in hand.
|
|
336
|
+
*/
|
|
337
|
+
export function createDocumentSchemaDropWarner(deps) {
|
|
338
|
+
let settled = deps.treeCarriesSchema;
|
|
339
|
+
return async (functionId, activeConfigId) => {
|
|
340
|
+
if (settled || !activeConfigId)
|
|
341
|
+
return;
|
|
342
|
+
let detail;
|
|
343
|
+
try {
|
|
344
|
+
detail = await deps.readConfig(functionId, activeConfigId);
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
// A read that failed is not an answer either: the next function's may
|
|
348
|
+
// succeed, and losing the warning to one blip is the failure this
|
|
349
|
+
// whole rule exists to avoid.
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const answer = documentSchemaDropAnswer(detail?.documentSchemaModels);
|
|
353
|
+
if (answer.outcome === "none")
|
|
354
|
+
return;
|
|
355
|
+
settled = true;
|
|
356
|
+
if (answer.outcome === "drops")
|
|
357
|
+
deps.warn(answer.message);
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
/** `[function.limits]` as the server stores it, or undefined when absent. */
|
|
361
|
+
function normalizeLimits(value) {
|
|
362
|
+
if (value === undefined || value === null)
|
|
363
|
+
return undefined;
|
|
364
|
+
if (typeof value === "string") {
|
|
365
|
+
try {
|
|
366
|
+
const parsed = JSON.parse(value);
|
|
367
|
+
return parsed && typeof parsed === "object" ? parsed : undefined;
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (typeof value !== "object" || Array.isArray(value))
|
|
374
|
+
return undefined;
|
|
375
|
+
return value;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* The create body. `functionKey` is attached here rather than spread from the
|
|
379
|
+
* table: the identity comes from the file name, exactly as it does for every
|
|
380
|
+
* other keyed type.
|
|
381
|
+
*/
|
|
382
|
+
export function buildFunctionCreatePayload(functionKey, table) {
|
|
383
|
+
const payload = buildPayloadFromToml(SERVER_FUNCTION_TABLE, table ?? {}, {
|
|
384
|
+
mode: "create",
|
|
385
|
+
values: { description: tomlOwned },
|
|
386
|
+
});
|
|
387
|
+
dropUndefined(payload);
|
|
388
|
+
return { functionKey, ...payload };
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* The update body. The key is NOT re-sent — it is the function's claim on the
|
|
392
|
+
* app's single key namespace and the URL already names the row — and `status`
|
|
393
|
+
* is server-owned on the sixth carrying type as on the other five.
|
|
394
|
+
*
|
|
395
|
+
* #3317 — the schemas are TOML-owned on exactly the terms `description` is
|
|
396
|
+
* (#1567), which is what prompts have sent since then: the update handler
|
|
397
|
+
* preserves a field the body omits, so dropping the key would leave a schema
|
|
398
|
+
* live that the file no longer declares, and the comparison — which reads an
|
|
399
|
+
* absent schema as `null` — would report that difference again on every run.
|
|
400
|
+
* Removing `[function.inputSchema]` clears it, and the next diff is Synced.
|
|
401
|
+
*/
|
|
402
|
+
export function buildFunctionUpdatePayload(table) {
|
|
403
|
+
const payload = buildPayloadFromToml(SERVER_FUNCTION_TABLE, table ?? {}, {
|
|
404
|
+
mode: "update",
|
|
405
|
+
values: {
|
|
406
|
+
description: tomlOwned,
|
|
407
|
+
inputSchema: tomlOwned,
|
|
408
|
+
outputSchema: tomlOwned,
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
dropUndefined(payload);
|
|
412
|
+
return payload;
|
|
413
|
+
}
|
|
414
|
+
function dropUndefined(payload) {
|
|
415
|
+
for (const key of Object.keys(payload)) {
|
|
416
|
+
if (payload[key] === undefined)
|
|
417
|
+
delete payload[key];
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* The canonical TOML for a function with no stored authored bytes — a function
|
|
422
|
+
* created through the API that has never had code pushed. When bytes exist,
|
|
423
|
+
* pull writes THOSE; this is the fallback that still produces a pushable file.
|
|
424
|
+
*/
|
|
425
|
+
export function serializeFunction(record, active, log = () => { }) {
|
|
426
|
+
const subject = `Function '${record?.functionKey}'`;
|
|
427
|
+
const table = {
|
|
428
|
+
key: record.functionKey,
|
|
429
|
+
// #3317 — the two schemas are stored as JSON STRINGS on the row, and
|
|
430
|
+
// `projectRecordToToml` reads a `json` field's emptiness as "not a
|
|
431
|
+
// non-empty object": without a producer, every stored schema was dropped
|
|
432
|
+
// here, so the file pull falls back to lost it and the comparison's remote
|
|
433
|
+
// side reported `(absent)` for a schema `functions get` prints. The shared
|
|
434
|
+
// rule emits the value the string encodes, as a native table whenever TOML
|
|
435
|
+
// can hold it.
|
|
436
|
+
...projectRecordToToml(SERVER_FUNCTION_TABLE, record, {
|
|
437
|
+
inputSchema: (rec, field) => serializeJsonField(rec?.[field.field], field.field, subject, log),
|
|
438
|
+
outputSchema: (rec, field) => serializeJsonField(rec?.[field.field], field.field, subject, log),
|
|
439
|
+
}),
|
|
440
|
+
};
|
|
441
|
+
if (active?.entry)
|
|
442
|
+
table.entry = active.entry;
|
|
443
|
+
// #3482 — no `mode` and no `durable`. This is the one file the CLI authors
|
|
444
|
+
// on a developer's behalf, and it must not teach a key the platform ignores
|
|
445
|
+
// and phase 5 refuses.
|
|
446
|
+
// #3182 — the capabilities are version-carried and server-derived, so the
|
|
447
|
+
// fallback file has to state them or a push of it would silently drop the
|
|
448
|
+
// function's declarations. `runAs` and `unscopedReads` are never written
|
|
449
|
+
// back (#3279): a pre-change row's stored value decides nothing, and a
|
|
450
|
+
// pulled file that carried either would be refused on its next push.
|
|
451
|
+
if (Array.isArray(active?.capabilities) && active.capabilities.length > 0) {
|
|
452
|
+
table.capabilities = [...active.capabilities].sort();
|
|
453
|
+
}
|
|
454
|
+
const limits = normalizeLimits(active?.limits);
|
|
455
|
+
if (limits && Object.keys(limits).length > 0)
|
|
456
|
+
table.limits = limits;
|
|
457
|
+
for (const key of Object.keys(table)) {
|
|
458
|
+
if (table[key] === undefined || table[key] === null)
|
|
459
|
+
delete table[key];
|
|
460
|
+
}
|
|
461
|
+
return stringifyConfigToml({ function: table });
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* The tree-relative spelling of `entry` that push actually sends.
|
|
465
|
+
*
|
|
466
|
+
* The build normalizes the authored value (`resolve` + `relative`), and the
|
|
467
|
+
* server stores what it is sent, so a valid `./functions/greet/index.ts`
|
|
468
|
+
* arrives as `functions/greet/index.ts`. Comparing the authored spelling
|
|
469
|
+
* against the stored one would report a function as Modified the instant it
|
|
470
|
+
* was pushed and re-send the same update forever, so both sides are read
|
|
471
|
+
* through the same normalization here.
|
|
472
|
+
*
|
|
473
|
+
* Lexical only: an in-tree SYMLINK spelling is resolved by the build and not
|
|
474
|
+
* by this, which is the one remaining way for the two to differ.
|
|
475
|
+
*/
|
|
476
|
+
function canonicalEntry(value) {
|
|
477
|
+
const raw = toPosix(String(value ?? "").trim());
|
|
478
|
+
if (!raw)
|
|
479
|
+
return null;
|
|
480
|
+
const normalized = posixPath.normalize(raw).replace(/\/+$/, "");
|
|
481
|
+
return normalized || null;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* The entity both sides of `config diff` are projected onto: the header's
|
|
485
|
+
* authored fields plus the keys the VERSION carries. Stated once so a
|
|
486
|
+
* local file and a server record cannot be read into different shapes.
|
|
487
|
+
*
|
|
488
|
+
* Text the server rewrites on the way in is read the same way it will come
|
|
489
|
+
* back (the DSO-003 rule the webhook comparator states): `access` is stored
|
|
490
|
+
* trimmed and `entry` normalized, so an authored value that differs only in
|
|
491
|
+
* that respect is not permanent drift.
|
|
492
|
+
*/
|
|
493
|
+
export function functionDiffEntity(tomlData) {
|
|
494
|
+
const table = tomlData?.function ?? {};
|
|
495
|
+
const limits = normalizeLimits(table.limits);
|
|
496
|
+
const access = typeof table.access === "string" && table.access.trim()
|
|
497
|
+
? table.access.trim()
|
|
498
|
+
: null;
|
|
499
|
+
return {
|
|
500
|
+
description: table.description ?? null,
|
|
501
|
+
access,
|
|
502
|
+
inputSchema: table.inputSchema ?? null,
|
|
503
|
+
outputSchema: table.outputSchema ?? null,
|
|
504
|
+
entry: canonicalEntry(table.entry),
|
|
505
|
+
// #3482 — no `mode` key here. It said nothing about the function any more,
|
|
506
|
+
// and comparing a key neither side stores would report permanent drift for
|
|
507
|
+
// every tree that still carries the retired line.
|
|
508
|
+
limits: limits && Object.keys(limits).length > 0 ? limits : null,
|
|
509
|
+
// #3181 — normalized locally from the SAME rules the server applies, so a
|
|
510
|
+
// trigger edit reads as a named field rather than only as the code-axis
|
|
511
|
+
// `envelopeHash` change it also is. A file whose triggers do not parse is
|
|
512
|
+
// reported as drift rather than throwing out of a read-only diff: push is
|
|
513
|
+
// where the refusal belongs.
|
|
514
|
+
triggers: safeTriggers(table),
|
|
515
|
+
// #3182 — sorted on both sides: the row stores a SET, which has no order,
|
|
516
|
+
// so comparing authored order against it would report permanent drift.
|
|
517
|
+
capabilities: Array.isArray(table.capabilities)
|
|
518
|
+
? [...new Set(table.capabilities.map((c) => String(c)))].sort()
|
|
519
|
+
: [],
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
function safeTriggers(table) {
|
|
523
|
+
try {
|
|
524
|
+
return normalizeFunctionTriggers(table);
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
// ── The retired keys and blocks, stripped on the way out ─────────────────
|
|
531
|
+
//
|
|
532
|
+
// #3482 took the runtime keys out (D3482-007); #3483 takes the whole
|
|
533
|
+
// `[[function.triggers.database]]` block out for the same reason and under the
|
|
534
|
+
// same contract. The difference is only in what a line edit has to reach: a
|
|
535
|
+
// key is one assignment, and a block is a table header and everything under it
|
|
536
|
+
// until the next header.
|
|
537
|
+
/** The two multi-line string delimiters TOML has. */
|
|
538
|
+
const MULTILINE_DELIMITERS = ['"""', "'''"];
|
|
539
|
+
/**
|
|
540
|
+
* Remove `mode`, `durable` and every cron entry's `mode` from authored TOML,
|
|
541
|
+
* preserving every other byte.
|
|
542
|
+
*
|
|
543
|
+
* The issue says `config pull` never writes them, and the pull plan writes the
|
|
544
|
+
* version's AUTHORED bytes verbatim — so without this a tree pulled from a
|
|
545
|
+
* version pushed before the retirement would carry keys phase 5 refuses, and
|
|
546
|
+
* could not be pushed again.
|
|
547
|
+
*
|
|
548
|
+
* ── Three jobs, kept apart ────────────────────────────────────────────────
|
|
549
|
+
*
|
|
550
|
+
* The PARSE decides what is removed and what is reported: `ignoredFunctionKeys`
|
|
551
|
+
* is the very scanner `config push` warns with, so what a pull takes out is
|
|
552
|
+
* exactly what a push said it was ignoring, whatever spelling the author used.
|
|
553
|
+
* TOML has several — `["function"]`, `function . mode = …`, an inline
|
|
554
|
+
* `triggers.cron = [ { mode = … } ]` — and a scan that reads the SOURCE has to
|
|
555
|
+
* know every one of them or it silently leaves a key behind (CR3482-002).
|
|
556
|
+
*
|
|
557
|
+
* The LINE EDIT decides how the bytes change, and is the path every real file
|
|
558
|
+
* takes. A re-serialization would rewrite every comment, blank line, quoting
|
|
559
|
+
* style and key order in a file a human wrote, so a pull would report a hundred
|
|
560
|
+
* changes to make three. Here exactly the matched assignments go and nothing
|
|
561
|
+
* else moves.
|
|
562
|
+
*
|
|
563
|
+
* The VERIFICATION decides whether the line edit was good enough, by parsing
|
|
564
|
+
* what it produced: the result must parse, must carry no retired key, and must
|
|
565
|
+
* otherwise say precisely what the file said. That is the contract stated as a
|
|
566
|
+
* check rather than as a hope — and it is what makes the line edit safe to keep
|
|
567
|
+
* narrow. A file it cannot reach by whole lines (a retired key sharing its line
|
|
568
|
+
* with keys that must survive, inside an inline table) falls back to a
|
|
569
|
+
* serialized rewrite, which is reported, because a pulled file that still
|
|
570
|
+
* carries the key — or one that no longer parses (CR3482-001) — is worse than a
|
|
571
|
+
* reformatted one.
|
|
572
|
+
*
|
|
573
|
+
* Returns the text and one line per removal, so the caller can report what it
|
|
574
|
+
* did rather than silently editing a developer's file.
|
|
575
|
+
*/
|
|
576
|
+
export function stripRetiredFunctionKeys(toml, label = "") {
|
|
577
|
+
const prefix = label ? `${label}: ` : "";
|
|
578
|
+
let before;
|
|
579
|
+
try {
|
|
580
|
+
before = parseConfigToml(toml);
|
|
581
|
+
}
|
|
582
|
+
catch {
|
|
583
|
+
// Not ours to fix, and not ours to guess at. A file that does not parse
|
|
584
|
+
// never pushed, so it is not a stored version's authored bytes; leave
|
|
585
|
+
// every byte where it is.
|
|
586
|
+
return { toml, removals: [] };
|
|
587
|
+
}
|
|
588
|
+
const functionTable = before.function;
|
|
589
|
+
const ignored = ignoredFunctionKeys(functionTable);
|
|
590
|
+
// PRESENCE, not entry count (CR3483-001). `database = []` names no type, and
|
|
591
|
+
// deciding by the list's length left that line in the pulled file — which
|
|
592
|
+
// the next push then refused, because the refusal reads presence. The two
|
|
593
|
+
// halves ask the same question now.
|
|
594
|
+
const hasDatabaseBlock = hasRetiredDatabaseTriggerBlock(functionTable);
|
|
595
|
+
const databaseTypes = retiredDatabaseTriggerTypes(functionTable);
|
|
596
|
+
if (ignored.length === 0 && !hasDatabaseBlock) {
|
|
597
|
+
return { toml, removals: [] };
|
|
598
|
+
}
|
|
599
|
+
const removals = [
|
|
600
|
+
...ignored.map((key) => `${prefix}removed \`${removalPhrase(key)}\` (#3482)`),
|
|
601
|
+
// One line per entry, and one line for a block that has no entry to name:
|
|
602
|
+
// an author who wrote `triggers.database = []` still has a line gone from
|
|
603
|
+
// their file and is owed the same notice as one who wrote a type.
|
|
604
|
+
...(hasDatabaseBlock && databaseTypes.length === 0
|
|
605
|
+
? [`${prefix}removed \`[[function.triggers.database]]\` (#3483)`]
|
|
606
|
+
: databaseTypes.map((type) => `${prefix}removed \`[[function.triggers.database]]\` type ` +
|
|
607
|
+
`'${type}' (#3483)`)),
|
|
608
|
+
];
|
|
609
|
+
const want = withoutRetiredFunctionKeys(before);
|
|
610
|
+
const edited = removeRetiredKeyLines(toml);
|
|
611
|
+
if (saysExactly(edited, want))
|
|
612
|
+
return { toml: edited, removals };
|
|
613
|
+
// The line edit could not reach it. Today that means one thing: a retired key
|
|
614
|
+
// written inside an inline table, sharing its line with keys that have to
|
|
615
|
+
// survive it.
|
|
616
|
+
const rewritten = serializeWithout(want);
|
|
617
|
+
if (rewritten !== null && saysExactly(rewritten, want)) {
|
|
618
|
+
return {
|
|
619
|
+
toml: rewritten,
|
|
620
|
+
removals: [
|
|
621
|
+
...removals,
|
|
622
|
+
`${prefix}rewrote the file to do it — the key shared a line with keys ` +
|
|
623
|
+
`that had to stay, so comments and formatting were not preserved (#3482)`,
|
|
624
|
+
],
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
// Unreachable from any file that parsed: `want` came from this document, so
|
|
628
|
+
// TOML can write it back. If it ever happens, say so and change nothing —
|
|
629
|
+
// a file left as it was can still be edited by hand, and one written back
|
|
630
|
+
// broken cannot be pushed at all.
|
|
631
|
+
const named = [
|
|
632
|
+
...ignored.map((key) => `\`${removalPhrase(key)}\``),
|
|
633
|
+
...(databaseTypes.length > 0
|
|
634
|
+
? ["`[[function.triggers.database]]`"]
|
|
635
|
+
: []),
|
|
636
|
+
].join(", ");
|
|
637
|
+
return {
|
|
638
|
+
toml,
|
|
639
|
+
removals: [
|
|
640
|
+
`${prefix}could NOT remove ${named} — the file is unchanged; delete by ` +
|
|
641
|
+
`hand (#3482, #3483)`,
|
|
642
|
+
],
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
/** How a pull names one retired key it took out. */
|
|
646
|
+
function removalPhrase(key) {
|
|
647
|
+
return key.path === "mode" || key.path === "durable"
|
|
648
|
+
? key.path
|
|
649
|
+
: "mode on a [[function.triggers.cron]] entry";
|
|
650
|
+
}
|
|
651
|
+
/** The document without the retired keys — the answer a pull has to produce. */
|
|
652
|
+
function withoutRetiredFunctionKeys(doc) {
|
|
653
|
+
const fn = doc.function;
|
|
654
|
+
if (!fn || typeof fn !== "object" || Array.isArray(fn))
|
|
655
|
+
return doc;
|
|
656
|
+
const nextFunction = { ...fn };
|
|
657
|
+
for (const key of RETIRED_FUNCTION_RUNTIME_KEYS)
|
|
658
|
+
delete nextFunction[key];
|
|
659
|
+
const triggers = nextFunction.triggers;
|
|
660
|
+
if (triggers && typeof triggers === "object" && !Array.isArray(triggers)) {
|
|
661
|
+
// One pass over the table, so the two removals cannot undo each other: a
|
|
662
|
+
// second `{ ...triggers, … }` built from the ORIGINAL would put back the
|
|
663
|
+
// block the first one took out.
|
|
664
|
+
const nextTriggers = {
|
|
665
|
+
...triggers,
|
|
666
|
+
};
|
|
667
|
+
// #3483 — the whole block, whatever spelling it took.
|
|
668
|
+
delete nextTriggers.database;
|
|
669
|
+
const cron = triggers.cron;
|
|
670
|
+
const withoutMode = (entry) => {
|
|
671
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
672
|
+
return entry;
|
|
673
|
+
}
|
|
674
|
+
const next = { ...entry };
|
|
675
|
+
delete next.mode;
|
|
676
|
+
return next;
|
|
677
|
+
};
|
|
678
|
+
if (Array.isArray(cron)) {
|
|
679
|
+
nextTriggers.cron = cron.map(withoutMode);
|
|
680
|
+
}
|
|
681
|
+
else if (cron && typeof cron === "object") {
|
|
682
|
+
nextTriggers.cron = withoutMode(cron);
|
|
683
|
+
}
|
|
684
|
+
nextFunction.triggers = nextTriggers;
|
|
685
|
+
}
|
|
686
|
+
return { ...doc, function: nextFunction };
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Whether `text` says precisely what `want` says.
|
|
690
|
+
*
|
|
691
|
+
* Empty tables are ignored on both sides: removing the last key of a table
|
|
692
|
+
* removes the table with it in the dotted spelling (`function.mode = …` at the
|
|
693
|
+
* document root leaves no `[function]` behind), which is the right answer and
|
|
694
|
+
* the one the strip has always given.
|
|
695
|
+
*/
|
|
696
|
+
function saysExactly(text, want) {
|
|
697
|
+
try {
|
|
698
|
+
const parsed = parseConfigToml(text);
|
|
699
|
+
const fn = parsed.function;
|
|
700
|
+
if (ignoredFunctionKeys(fn).length)
|
|
701
|
+
return false;
|
|
702
|
+
if (hasRetiredDatabaseTriggerBlock(fn))
|
|
703
|
+
return false;
|
|
704
|
+
return withoutEmptyTables(parsed) === withoutEmptyTables(want);
|
|
705
|
+
}
|
|
706
|
+
catch {
|
|
707
|
+
return false;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
/** Canonical JSON with empty tables dropped, for comparing two documents. */
|
|
711
|
+
function withoutEmptyTables(value) {
|
|
712
|
+
const prune = (node) => {
|
|
713
|
+
if (Array.isArray(node))
|
|
714
|
+
return node.map(prune);
|
|
715
|
+
if (!node || typeof node !== "object")
|
|
716
|
+
return node;
|
|
717
|
+
if (typeof node.toJSON === "function")
|
|
718
|
+
return node;
|
|
719
|
+
const next = {};
|
|
720
|
+
for (const [key, child] of Object.entries(node)) {
|
|
721
|
+
const pruned = prune(child);
|
|
722
|
+
const isEmptyTable = pruned !== null &&
|
|
723
|
+
typeof pruned === "object" &&
|
|
724
|
+
!Array.isArray(pruned) &&
|
|
725
|
+
typeof pruned.toJSON !== "function" &&
|
|
726
|
+
Object.keys(pruned).length === 0;
|
|
727
|
+
if (!isEmptyTable)
|
|
728
|
+
next[key] = pruned;
|
|
729
|
+
}
|
|
730
|
+
return next;
|
|
731
|
+
};
|
|
732
|
+
return canonicalJsonStringify(prune(value));
|
|
733
|
+
}
|
|
734
|
+
/** `want` written back as TOML, or null if TOML cannot carry some value of it. */
|
|
735
|
+
function serializeWithout(want) {
|
|
736
|
+
try {
|
|
737
|
+
const text = stringifyConfigToml(want);
|
|
738
|
+
return text.endsWith("\n") ? text : `${text}\n`;
|
|
739
|
+
}
|
|
740
|
+
catch {
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Delete every line a retired key's assignment occupies, and nothing else.
|
|
746
|
+
*
|
|
747
|
+
* Narrow on purpose, because `mode` is a perfectly good key of some OTHER table
|
|
748
|
+
* (an integration's `bodyMode`, a nested table of the author's own) and inside
|
|
749
|
+
* a multi-line string or an array's value it is not a key at all. A key matches
|
|
750
|
+
* only when the table it sits in and its own dotted name together spell
|
|
751
|
+
* `function.mode`, `function.durable` or `function.triggers.cron.mode` — one
|
|
752
|
+
* comparison that covers `[function]` + `mode`, the root's `function.mode`, a
|
|
753
|
+
* `[[function.triggers.cron]]` entry's `mode`, and every quoted or
|
|
754
|
+
* space-padded spelling of each.
|
|
755
|
+
*
|
|
756
|
+
* Telling a key from text is `scanLine`'s job, and it has to be told properly
|
|
757
|
+
* rather than by looking for delimiters with `indexOf`: a `"""` inside a
|
|
758
|
+
* comment or a quoted value opens nothing, an escaped `\"""` inside a basic
|
|
759
|
+
* string closes nothing, and a value may run past its first line — as a
|
|
760
|
+
* multi-line string, an array or an inline table — in which case the WHOLE
|
|
761
|
+
* assignment goes, because deleting its opening line alone would orphan the
|
|
762
|
+
* rest and leave a file that no longer parses.
|
|
763
|
+
*/
|
|
764
|
+
function removeRetiredKeyLines(toml) {
|
|
765
|
+
const lines = toml.split("\n");
|
|
766
|
+
const kept = [];
|
|
767
|
+
// The table the lines below belong to, by its header. The root is [].
|
|
768
|
+
let table = [];
|
|
769
|
+
let state = { open: null, depth: 0 };
|
|
770
|
+
// Whether the value we are still reading belongs to a key we removed, so its
|
|
771
|
+
// remaining lines go with it.
|
|
772
|
+
let droppingValue = false;
|
|
773
|
+
// #3483 — whether the TABLE the lines below belong to is one we removed, so
|
|
774
|
+
// the whole section goes with its header. A key is one assignment; a block
|
|
775
|
+
// is a header and everything under it until the next header.
|
|
776
|
+
let droppingTable = false;
|
|
777
|
+
for (const line of lines) {
|
|
778
|
+
if (state.open || state.depth > 0) {
|
|
779
|
+
state = scanLine(line, state);
|
|
780
|
+
if (!droppingValue && !droppingTable)
|
|
781
|
+
kept.push(line);
|
|
782
|
+
if (!state.open && state.depth <= 0)
|
|
783
|
+
droppingValue = false;
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
const trimmed = line.trim();
|
|
787
|
+
const header = /^\[\[?\s*([^\]]+?)\s*\]\]?\s*(#.*)?$/.exec(trimmed);
|
|
788
|
+
if (header) {
|
|
789
|
+
table = keyPath(header[1]) ?? table;
|
|
790
|
+
droppingTable = isRetiredTablePath(table);
|
|
791
|
+
if (droppingTable) {
|
|
792
|
+
// The blank lines that separated this block from what came before it
|
|
793
|
+
// separated nothing once it is gone.
|
|
794
|
+
while (kept.length > 0 && kept[kept.length - 1].trim() === "") {
|
|
795
|
+
kept.pop();
|
|
796
|
+
}
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
kept.push(line);
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
if (droppingTable) {
|
|
803
|
+
// Still inside the removed section: the line goes, and a value that runs
|
|
804
|
+
// past its own line goes with it.
|
|
805
|
+
state = scanLine(line, { open: null, depth: 0 });
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
if (isRetiredKeyLine(table, trimmed)) {
|
|
809
|
+
// `mode = """\ntask"""` is a retired key like any other, and one line of
|
|
810
|
+
// it is not.
|
|
811
|
+
state = scanLine(line, { open: null, depth: 0 });
|
|
812
|
+
droppingValue = state.open !== null || state.depth > 0;
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
state = scanLine(line, { open: null, depth: 0 });
|
|
816
|
+
kept.push(line);
|
|
817
|
+
}
|
|
818
|
+
return kept.join("\n");
|
|
819
|
+
}
|
|
820
|
+
/** Whether this line assigns one of the retired keys. */
|
|
821
|
+
function isRetiredKeyLine(table, trimmed) {
|
|
822
|
+
const key = assignmentKey(trimmed);
|
|
823
|
+
if (!key)
|
|
824
|
+
return false;
|
|
825
|
+
const path = [...table, ...key].join(".");
|
|
826
|
+
return (path === "function.mode" ||
|
|
827
|
+
path === "function.durable" ||
|
|
828
|
+
path === "function.triggers.cron.mode" ||
|
|
829
|
+
// #3483 — the inline spelling, `triggers.database = [ { … } ]` under
|
|
830
|
+
// `[function]`, or `function.triggers.database = …` at the root. Its value
|
|
831
|
+
// runs past its own line as often as not, which the caller's
|
|
832
|
+
// `droppingValue` handles exactly as it does for a multi-line `mode`.
|
|
833
|
+
path === "function.triggers.database");
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Whether a table header names a section the strip removes whole — #3483.
|
|
837
|
+
*
|
|
838
|
+
* Prefix-matched rather than compared, so a sub-table of an entry
|
|
839
|
+
* (`[function.triggers.database.filter]`, which TOML allows even though this
|
|
840
|
+
* platform never read one) goes with the block it belongs to instead of being
|
|
841
|
+
* orphaned into a file that no longer parses.
|
|
842
|
+
*/
|
|
843
|
+
function isRetiredTablePath(table) {
|
|
844
|
+
const retired = ["function", "triggers", "database"];
|
|
845
|
+
if (table.length < retired.length)
|
|
846
|
+
return false;
|
|
847
|
+
return retired.every((part, index) => table[index] === part);
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* The dotted name this line assigns to, in components, or null when the line
|
|
851
|
+
* assigns nothing.
|
|
852
|
+
*
|
|
853
|
+
* The `=` is found by scanning rather than by a regex, so a quoted key holding
|
|
854
|
+
* one (`"a=b".mode = 1`) is read as the key it is.
|
|
855
|
+
*/
|
|
856
|
+
function assignmentKey(trimmed) {
|
|
857
|
+
let i = 0;
|
|
858
|
+
while (i < trimmed.length) {
|
|
859
|
+
const char = trimmed[i];
|
|
860
|
+
// The rest of the line is the author's prose, so there is no key on it.
|
|
861
|
+
if (char === "#")
|
|
862
|
+
return null;
|
|
863
|
+
if (char === '"' || char === "'") {
|
|
864
|
+
i = endOfSingleLineString(trimmed, i);
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
if (char === "=")
|
|
868
|
+
break;
|
|
869
|
+
i += 1;
|
|
870
|
+
}
|
|
871
|
+
if (trimmed[i] !== "=")
|
|
872
|
+
return null;
|
|
873
|
+
const raw = trimmed.slice(0, i).trim();
|
|
874
|
+
return raw ? keyPath(raw) : null;
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* A TOML key — a table header's inside or an assignment's left side — split
|
|
878
|
+
* into its components, with quotes and the whitespace TOML allows around the
|
|
879
|
+
* dots taken off. Null for anything this does not understand, so a key the
|
|
880
|
+
* strip cannot read is a key it does not delete.
|
|
881
|
+
*/
|
|
882
|
+
function keyPath(raw) {
|
|
883
|
+
const parts = [];
|
|
884
|
+
let current = "";
|
|
885
|
+
let i = 0;
|
|
886
|
+
while (i < raw.length) {
|
|
887
|
+
const char = raw[i];
|
|
888
|
+
if (char === " " || char === "\t") {
|
|
889
|
+
i += 1;
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
if (char === '"' || char === "'") {
|
|
893
|
+
const end = endOfSingleLineString(raw, i);
|
|
894
|
+
current += raw.slice(i + 1, Math.max(i + 1, end - 1));
|
|
895
|
+
i = end;
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
if (char === ".") {
|
|
899
|
+
parts.push(current);
|
|
900
|
+
current = "";
|
|
901
|
+
i += 1;
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
if (!/[A-Za-z0-9_-]/.test(char))
|
|
905
|
+
return null;
|
|
906
|
+
current += char;
|
|
907
|
+
i += 1;
|
|
908
|
+
}
|
|
909
|
+
parts.push(current);
|
|
910
|
+
return parts;
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Walk one line of TOML from the state it starts in to the one the next line
|
|
914
|
+
* starts in.
|
|
915
|
+
*
|
|
916
|
+
* Not a parser: the strip only needs to tell code from text and to know when a
|
|
917
|
+
* value has ended, which is the whole of what it kept getting wrong. Four rules
|
|
918
|
+
* carry it:
|
|
919
|
+
*
|
|
920
|
+
* - a `#` outside a string comments out the rest of the line, so a `"""` in
|
|
921
|
+
* an author's note opens nothing and a `]` in one closes nothing;
|
|
922
|
+
* - a single-line string is skipped whole, so a `"""` quoted inside one opens
|
|
923
|
+
* nothing either;
|
|
924
|
+
* - inside a BASIC string (`"""`) a backslash escapes the next character, so
|
|
925
|
+
* the `\"""` TOML requires for a literal triple quote does not close it. A
|
|
926
|
+
* literal string (`'''`) has no escapes and ends at its first delimiter;
|
|
927
|
+
* - brackets outside strings nest, so an array or inline table spread over
|
|
928
|
+
* several lines is read to its end — and the lines inside it are read as
|
|
929
|
+
* the value they are, never as keys of the enclosing table.
|
|
930
|
+
*/
|
|
931
|
+
function scanLine(line, from) {
|
|
932
|
+
let open = from.open;
|
|
933
|
+
let depth = from.depth;
|
|
934
|
+
let i = 0;
|
|
935
|
+
while (i < line.length) {
|
|
936
|
+
if (open) {
|
|
937
|
+
if (open === '"""' && line[i] === "\\") {
|
|
938
|
+
i += 2;
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
if (line.startsWith(open, i)) {
|
|
942
|
+
i += open.length;
|
|
943
|
+
open = null;
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
i += 1;
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
const char = line[i];
|
|
950
|
+
// The rest of the line is the author's prose.
|
|
951
|
+
if (char === "#")
|
|
952
|
+
break;
|
|
953
|
+
if (char === '"' || char === "'") {
|
|
954
|
+
const delimiter = MULTILINE_DELIMITERS.find((candidate) => line.startsWith(candidate, i));
|
|
955
|
+
if (delimiter) {
|
|
956
|
+
open = delimiter;
|
|
957
|
+
i += delimiter.length;
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
i = endOfSingleLineString(line, i);
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
if (char === "[" || char === "{")
|
|
964
|
+
depth += 1;
|
|
965
|
+
else if (char === "]" || char === "}")
|
|
966
|
+
depth = Math.max(0, depth - 1);
|
|
967
|
+
i += 1;
|
|
968
|
+
}
|
|
969
|
+
return { open, depth };
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* The index just past the single-line string opening at `start`.
|
|
973
|
+
*
|
|
974
|
+
* An unterminated one answers the end of the line: TOML would refuse such a
|
|
975
|
+
* file anyway, and the strip's business is to leave it as it found it, not to
|
|
976
|
+
* start deleting on the strength of a quote somebody forgot to close.
|
|
977
|
+
*/
|
|
978
|
+
function endOfSingleLineString(line, start) {
|
|
979
|
+
const quote = line[start];
|
|
980
|
+
let i = start + 1;
|
|
981
|
+
while (i < line.length) {
|
|
982
|
+
if (quote === '"' && line[i] === "\\") {
|
|
983
|
+
i += 2;
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
if (line[i] === quote)
|
|
987
|
+
return i + 1;
|
|
988
|
+
i += 1;
|
|
989
|
+
}
|
|
990
|
+
return line.length;
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Why a stored source path may not be written, or null.
|
|
994
|
+
*
|
|
995
|
+
* The same rule the server applies on the way in, applied again on the way out:
|
|
996
|
+
* the file is written to a developer's disk from a path the SERVER chose, so a
|
|
997
|
+
* second check here is the one that matters.
|
|
998
|
+
*/
|
|
999
|
+
export function unsafePulledPath(path) {
|
|
1000
|
+
const raw = toPosix(String(path ?? ""));
|
|
1001
|
+
if (!raw)
|
|
1002
|
+
return "a stored source path is empty";
|
|
1003
|
+
if (raw.startsWith("/") || /^[a-zA-Z]:\//.test(raw)) {
|
|
1004
|
+
return `refused stored source path '${raw}': absolute paths are never written`;
|
|
1005
|
+
}
|
|
1006
|
+
if (raw.split("/").includes("..")) {
|
|
1007
|
+
return `refused stored source path '${raw}': it normalizes outside the config tree`;
|
|
1008
|
+
}
|
|
1009
|
+
if (isDependencyPath(raw)) {
|
|
1010
|
+
return `refused stored source path '${raw}': node_modules is never authored source`;
|
|
1011
|
+
}
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Plan the files `config pull` writes for one function: the authored TOML at
|
|
1016
|
+
* `functions/<key>.toml`, and each stored source at its own path.
|
|
1017
|
+
*
|
|
1018
|
+
* Pure — the caller applies it — so the refusal rules are testable without a
|
|
1019
|
+
* server and without touching a disk.
|
|
1020
|
+
*/
|
|
1021
|
+
export function planFunctionPull(options) {
|
|
1022
|
+
const { configDir, functionKey, envelope } = options;
|
|
1023
|
+
const includeDocumentSchema = options.includeDocumentSchema !== false;
|
|
1024
|
+
const root = resolve(configDir);
|
|
1025
|
+
const files = [];
|
|
1026
|
+
const sourcePaths = [];
|
|
1027
|
+
const refusals = [];
|
|
1028
|
+
const notes = [];
|
|
1029
|
+
const removals = [];
|
|
1030
|
+
if (typeof envelope?.toml === "string") {
|
|
1031
|
+
const relativePath = `functions/${functionKey}.toml`;
|
|
1032
|
+
// #3482 (D3482-007) — the retired runtime keys never reach the tree. Every
|
|
1033
|
+
// other byte of the authored file is written back unchanged, so a pull is
|
|
1034
|
+
// still a byte-identical round trip for a file that carries none of them.
|
|
1035
|
+
// For a file that does, the consequence is one-time and recorded: the
|
|
1036
|
+
// pulled tree differs from the stored envelope, `config diff` reads
|
|
1037
|
+
// Modified, and the next push makes a version whose TOML no longer carries
|
|
1038
|
+
// the key.
|
|
1039
|
+
const stripped = stripRetiredFunctionKeys(Buffer.from(envelope.toml, "base64").toString("utf-8"), relativePath);
|
|
1040
|
+
notes.push(...stripped.removals);
|
|
1041
|
+
files.push({
|
|
1042
|
+
relative: relativePath,
|
|
1043
|
+
absolute: join(root, "functions", `${functionKey}.toml`),
|
|
1044
|
+
content: Buffer.from(stripped.toml, "utf-8"),
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
// #3455 (D3455-009) — the active version's document schema, written back as
|
|
1048
|
+
// the tree's GENERATED copy so a fresh pull into a project with no
|
|
1049
|
+
// `models/models.toml` still rebuilds the same envelope and reads Synced.
|
|
1050
|
+
// Always at this one fixed tree path: it is derived from the envelope's
|
|
1051
|
+
// field, never from a stored path, so it cannot be aimed anywhere.
|
|
1052
|
+
if (includeDocumentSchema) {
|
|
1053
|
+
const copy = planDocumentSchemaPull({
|
|
1054
|
+
configDir,
|
|
1055
|
+
functions: [
|
|
1056
|
+
{
|
|
1057
|
+
functionKey,
|
|
1058
|
+
documentSchema: typeof envelope?.documentSchema === "string" && envelope.documentSchema
|
|
1059
|
+
? envelope.documentSchema
|
|
1060
|
+
: null,
|
|
1061
|
+
},
|
|
1062
|
+
],
|
|
1063
|
+
coversEveryFunction: true,
|
|
1064
|
+
});
|
|
1065
|
+
files.push(...copy.files);
|
|
1066
|
+
removals.push(...copy.removals);
|
|
1067
|
+
refusals.push(...copy.warnings);
|
|
1068
|
+
}
|
|
1069
|
+
for (const source of envelope?.sources ?? []) {
|
|
1070
|
+
const path = toPosix(String(source?.path ?? ""));
|
|
1071
|
+
const refusal = unsafePulledPath(path);
|
|
1072
|
+
if (refusal) {
|
|
1073
|
+
refusals.push(refusal);
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
const absolute = resolve(root, path);
|
|
1077
|
+
// Belt and braces: the string rules above state the contract, this catches
|
|
1078
|
+
// a spelling they did not anticipate on a platform they were not written on.
|
|
1079
|
+
const rel = relative(root, absolute);
|
|
1080
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
1081
|
+
refusals.push(`refused stored source path '${path}': it normalizes outside the config tree`);
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
files.push({
|
|
1085
|
+
relative: path,
|
|
1086
|
+
absolute,
|
|
1087
|
+
content: Buffer.from(String(source?.content ?? ""), "base64"),
|
|
1088
|
+
});
|
|
1089
|
+
sourcePaths.push(path);
|
|
1090
|
+
}
|
|
1091
|
+
return {
|
|
1092
|
+
configDir: root,
|
|
1093
|
+
functionKey,
|
|
1094
|
+
files,
|
|
1095
|
+
sourcePaths,
|
|
1096
|
+
refusals,
|
|
1097
|
+
notes,
|
|
1098
|
+
removals,
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* The tree's generated document-schema copy, decided ONCE for a whole pull —
|
|
1103
|
+
* CR3455-001.
|
|
1104
|
+
*
|
|
1105
|
+
* Push and diff resolve the copy tree-wide (`resolveDocumentSchema`), so the
|
|
1106
|
+
* tree can hold exactly one schema however many functions it has. Deciding it
|
|
1107
|
+
* per function meant the last function applied won: a version carrying no
|
|
1108
|
+
* schema DELETED the copy a schema-bearing sibling had just written, the
|
|
1109
|
+
* sibling then diffed Modified for ever, and a repush from that tree dropped
|
|
1110
|
+
* the schema — the one outcome D3455-009 exists to prevent.
|
|
1111
|
+
*
|
|
1112
|
+
* The rules, and what each is protecting:
|
|
1113
|
+
*
|
|
1114
|
+
* - Any pulled version carrying a schema WINS over one carrying none. The
|
|
1115
|
+
* copy is a fallback consulted only when the project has no
|
|
1116
|
+
* `models/models.toml`; keeping it is what lets a fresh pull re-push.
|
|
1117
|
+
* - The copy is removed only when the pull read EVERY active function and
|
|
1118
|
+
* none carried a schema. A selective pull has not learned anything about
|
|
1119
|
+
* the functions it did not read, and deleting on that evidence would take
|
|
1120
|
+
* the schema away from a version that still pins one.
|
|
1121
|
+
* - Versions that disagree are reported rather than silently reconciled. One
|
|
1122
|
+
* file cannot reproduce two envelopes, so an operator is told which
|
|
1123
|
+
* function the tree now speaks for and which ones will read Modified until
|
|
1124
|
+
* they are pushed again.
|
|
1125
|
+
*/
|
|
1126
|
+
export function planDocumentSchemaPull(options) {
|
|
1127
|
+
const root = resolve(options.configDir);
|
|
1128
|
+
const relativePath = `functions/${DOCUMENT_SCHEMA_TREE_FILE}`;
|
|
1129
|
+
const absolute = join(root, "functions", DOCUMENT_SCHEMA_TREE_FILE);
|
|
1130
|
+
const plan = {
|
|
1131
|
+
configDir: root,
|
|
1132
|
+
files: [],
|
|
1133
|
+
removals: [],
|
|
1134
|
+
warnings: [],
|
|
1135
|
+
};
|
|
1136
|
+
// Sorted by key so the schema a mixed tree keeps is a property of the app,
|
|
1137
|
+
// not of the order the listing happened to come back in.
|
|
1138
|
+
const pulled = [...options.functions].sort((a, b) => a.functionKey.localeCompare(b.functionKey));
|
|
1139
|
+
if (pulled.length === 0)
|
|
1140
|
+
return plan;
|
|
1141
|
+
const carriers = pulled.filter((f) => !!f.documentSchema);
|
|
1142
|
+
if (carriers.length === 0) {
|
|
1143
|
+
if (options.coversEveryFunction) {
|
|
1144
|
+
plan.removals.push({ relative: relativePath, absolute });
|
|
1145
|
+
}
|
|
1146
|
+
return plan;
|
|
1147
|
+
}
|
|
1148
|
+
const chosen = carriers[0];
|
|
1149
|
+
plan.files.push({
|
|
1150
|
+
relative: relativePath,
|
|
1151
|
+
absolute,
|
|
1152
|
+
content: Buffer.from(String(chosen.documentSchema), "base64"),
|
|
1153
|
+
});
|
|
1154
|
+
const disagreeing = pulled
|
|
1155
|
+
.filter((f) => f.documentSchema !== chosen.documentSchema)
|
|
1156
|
+
.map((f) => f.functionKey);
|
|
1157
|
+
if (disagreeing.length > 0) {
|
|
1158
|
+
plan.warnings.push(`${relativePath} holds the document schema '${chosen.functionKey}' is ` +
|
|
1159
|
+
`pinned to; ${disagreeing.join(", ")} pinned a different one or none, ` +
|
|
1160
|
+
"so this tree cannot rebuild every active version's envelope. " +
|
|
1161
|
+
"`config diff` reads those functions Modified until they are pushed again.");
|
|
1162
|
+
}
|
|
1163
|
+
return plan;
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* Why a destination may not be written even though its stored path is
|
|
1167
|
+
* tree-relative, or null.
|
|
1168
|
+
*
|
|
1169
|
+
* `planFunctionPull`'s rules are lexical, and a lexical rule cannot see a
|
|
1170
|
+
* SYMLINK: `functions/shared` pointing at `/etc` makes `functions/shared/x`
|
|
1171
|
+
* normalize inside the tree and land outside it. So the last check before the
|
|
1172
|
+
* write is against the real path — the deepest directory that already exists
|
|
1173
|
+
* is resolved and must still be inside the real tree root, and an existing
|
|
1174
|
+
* destination that is itself a symlink is refused rather than written through.
|
|
1175
|
+
*/
|
|
1176
|
+
function unsafeDestination(realRoot, absolute) {
|
|
1177
|
+
const contained = (path) => path === realRoot || path.startsWith(realRoot + sep);
|
|
1178
|
+
let dir = dirname(absolute);
|
|
1179
|
+
while (!existsSync(dir)) {
|
|
1180
|
+
const parent = dirname(dir);
|
|
1181
|
+
if (parent === dir)
|
|
1182
|
+
break;
|
|
1183
|
+
dir = parent;
|
|
1184
|
+
}
|
|
1185
|
+
let realDir;
|
|
1186
|
+
try {
|
|
1187
|
+
realDir = realpathSync(dir);
|
|
1188
|
+
}
|
|
1189
|
+
catch {
|
|
1190
|
+
return `refused writing '${absolute}': its directory could not be resolved`;
|
|
1191
|
+
}
|
|
1192
|
+
if (!contained(realDir)) {
|
|
1193
|
+
return (`refused writing '${absolute}': a symlinked directory takes it outside ` +
|
|
1194
|
+
"the config tree");
|
|
1195
|
+
}
|
|
1196
|
+
try {
|
|
1197
|
+
if (lstatSync(absolute).isSymbolicLink()) {
|
|
1198
|
+
return (`refused writing '${absolute}': the destination is a symlink, and a ` +
|
|
1199
|
+
"pull writes files, never through links");
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
catch {
|
|
1203
|
+
/* the destination does not exist yet — the normal case */
|
|
1204
|
+
}
|
|
1205
|
+
return null;
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Write a plan's files, creating the directories they need.
|
|
1209
|
+
*
|
|
1210
|
+
* Returns what was written and what was refused; a refusal is reported by the
|
|
1211
|
+
* caller and keeps the file out of the sync state, so prune never claims to
|
|
1212
|
+
* own a path this pull did not write.
|
|
1213
|
+
*/
|
|
1214
|
+
export function applyFunctionPull(plan) {
|
|
1215
|
+
const outcome = { writtenSources: [], refusals: [] };
|
|
1216
|
+
let realRoot;
|
|
1217
|
+
try {
|
|
1218
|
+
realRoot = realpathSync(plan.configDir);
|
|
1219
|
+
}
|
|
1220
|
+
catch {
|
|
1221
|
+
realRoot = plan.configDir;
|
|
1222
|
+
}
|
|
1223
|
+
const sourcePaths = new Set(plan.sourcePaths ?? []);
|
|
1224
|
+
for (const file of plan.files) {
|
|
1225
|
+
const refusal = unsafeDestination(realRoot, file.absolute);
|
|
1226
|
+
if (refusal) {
|
|
1227
|
+
outcome.refusals.push(refusal);
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
const dir = dirname(file.absolute);
|
|
1231
|
+
if (!existsSync(dir))
|
|
1232
|
+
mkdirSync(dir, { recursive: true });
|
|
1233
|
+
writeFileSync(file.absolute, file.content);
|
|
1234
|
+
if (sourcePaths.has(file.relative))
|
|
1235
|
+
outcome.writtenSources.push(file.relative);
|
|
1236
|
+
}
|
|
1237
|
+
// #3455 — a stale generated schema copy, when the active version carries
|
|
1238
|
+
// none. Same real-path check as a write: the path is this CLI's own
|
|
1239
|
+
// constant, but a symlinked `functions/` must not let a pull unlink
|
|
1240
|
+
// something outside the tree.
|
|
1241
|
+
for (const removal of plan.removals) {
|
|
1242
|
+
if (!existsSync(removal.absolute))
|
|
1243
|
+
continue;
|
|
1244
|
+
const refusal = unsafeDestination(realRoot, removal.absolute);
|
|
1245
|
+
if (refusal) {
|
|
1246
|
+
outcome.refusals.push(refusal);
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
try {
|
|
1250
|
+
rmSync(removal.absolute);
|
|
1251
|
+
}
|
|
1252
|
+
catch (error) {
|
|
1253
|
+
outcome.refusals.push(`failed removing '${removal.relative}': ${String(error?.message ?? error)}`);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
return outcome;
|
|
1257
|
+
}
|
|
1258
|
+
//# sourceMappingURL=function-sync.js.map
|