@starci/hfs 4.2.17 → 4.4.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/CHANGELOG.md +28 -0
- package/README.md +7 -0
- package/db/migrate.mjs +361 -0
- package/db/post-cutover.mjs +136 -0
- package/emit/db-types.mjs +10 -1
- package/lint/run.mjs +1 -1
- package/package.json +4 -1
- package/release/gate.mjs +298 -0
- package/release/index.mjs +103 -0
- package/release/push.mjs +110 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
- package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
- package/runtime/knowledge/hfs/rules.yaml +3437 -160
- package/runtime/knowledge/hfs/slots.yaml +430 -92
- package/runtime/knowledge/patterns/be/api.yaml +92 -2
- package/runtime/knowledge/patterns/be/cli.yaml +37 -9
- package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
- package/runtime/knowledge/patterns/be/jobs.yaml +20 -2
- package/runtime/knowledge/patterns/be/projections.yaml +4 -2
- package/runtime/knowledge/patterns/be/queues.yaml +11 -3
- package/runtime/knowledge/patterns/be/reactors.yaml +20 -1
- package/runtime/knowledge/patterns/be/realtime.yaml +25 -3
- package/runtime/knowledge/patterns/be/saga.yaml +20 -3
- package/runtime/knowledge/patterns/be/webhooks.yaml +210 -36
- package/runtime/knowledge/patterns/fe/folder.yaml +4 -0
- package/runtime/knowledge/sonar-gate.yaml +1 -1
- package/runtime/modules/kernel/failure-codes.yaml +3128 -47
- package/runtime/modules/models/runtimes.yaml +1 -1
- package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
- package/runtime/packages/cli/src/catalog.generated.mjs +14104 -0
- package/runtime/packages/cli/src/validate-args.mjs +213 -0
- package/runtime/scripts/api/git/add.mjs +5 -0
- package/runtime/scripts/api/git/cat-file.mjs +5 -0
- package/runtime/scripts/api/git/init.mjs +7 -0
- package/runtime/scripts/api/git/lib.mjs +3 -0
- package/runtime/scripts/api/git/tag.mjs +5 -0
- package/runtime/scripts/api/node/lib.mjs +20 -0
- package/runtime/scripts/api/node/run-node.mjs +6 -0
- package/runtime/scripts/api/sops/lib.mjs +41 -0
- package/runtime/scripts/api/sops/resolve-sops.mjs +7 -0
- package/runtime/scripts/api/sops/seal.mjs +1 -1
- package/runtime/scripts/api/sops/selected-age-recipient.mjs +7 -0
- package/runtime/scripts/api/supabase/database-dump.mjs +5 -0
- package/runtime/scripts/api/supabase/database-query.mjs +5 -0
- package/runtime/scripts/api/supabase/lib.mjs +44 -0
- package/runtime/scripts/api/supabase/migration-repair.mjs +5 -0
- package/runtime/scripts/hfs/app-wiring.mjs +201 -0
- package/runtime/scripts/hfs/architecture/backend-app-files.mjs +103 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +22 -10
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +157 -41
- package/runtime/scripts/hfs/architecture/canon-clones.mjs +240 -0
- package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +143 -0
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
- package/runtime/scripts/hfs/architecture/clones.mjs +17 -8
- package/runtime/scripts/hfs/architecture/connection-map.mjs +14 -12
- package/runtime/scripts/hfs/architecture/context-map.mjs +25 -14
- package/runtime/scripts/hfs/architecture/context-owner.mjs +1 -2
- package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +23 -50
- package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
- package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +104 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +30 -11
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
- package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
- package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +16 -3
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +17 -0
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +49 -25
- package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
- package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
- package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
- package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
- package/runtime/scripts/hfs/architecture/register-once.mjs +15 -7
- package/runtime/scripts/hfs/architecture/required-files.mjs +2 -1
- package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
- package/runtime/scripts/hfs/architecture/staged-consumers.mjs +37 -0
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +214 -17
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -6
- package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
- package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
- package/runtime/scripts/hfs/architecture/tiers.mjs +11 -4
- package/runtime/scripts/hfs/architecture/typescript.mjs +1 -8
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +9 -13
- package/runtime/scripts/hfs/check-results.mjs +28 -0
- package/runtime/scripts/hfs/check.mjs +63 -72
- package/runtime/scripts/hfs/coverage-from.mjs +70 -0
- package/runtime/scripts/hfs/db-baseline-verb.mjs +276 -0
- package/runtime/scripts/hfs/db-plan-verb.mjs +254 -0
- package/runtime/scripts/hfs/db-restamp-verb.mjs +60 -0
- package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
- package/runtime/scripts/hfs/doc-commands.mjs +367 -0
- package/runtime/scripts/hfs/edition-slots.mjs +2 -2
- package/runtime/scripts/hfs/example-secrets.mjs +158 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +91 -6
- package/runtime/scripts/hfs/path-findings.mjs +32 -2
- package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
- package/runtime/scripts/hfs/rule-catalog.mjs +156 -11
- package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
- package/runtime/scripts/hfs/rules/a15-findings.mjs +88 -0
- package/runtime/scripts/hfs/rules/app-env-required.mjs +20 -0
- package/runtime/scripts/hfs/rules/app-root.mjs +2 -2
- package/runtime/scripts/hfs/rules/assets.mjs +79 -0
- package/runtime/scripts/hfs/rules/boot-proof.mjs +16 -0
- package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +59 -0
- package/runtime/scripts/hfs/rules/contract.mjs +66 -5
- package/runtime/scripts/hfs/rules/coverage-below.mjs +230 -0
- package/runtime/scripts/hfs/rules/database-access.mjs +343 -0
- package/runtime/scripts/hfs/rules/database-baseline.mjs +142 -0
- package/runtime/scripts/hfs/rules/database-config.mjs +250 -2
- package/runtime/scripts/hfs/rules/database-constants.mjs +9 -0
- package/runtime/scripts/hfs/rules/database-migrations.mjs +317 -37
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +119 -9
- package/runtime/scripts/hfs/rules/database-queue-sources.mjs +176 -0
- package/runtime/scripts/hfs/rules/database-queues.mjs +361 -0
- package/runtime/scripts/hfs/rules/database-sql.mjs +91 -9
- package/runtime/scripts/hfs/rules/database.mjs +11 -2
- package/runtime/scripts/hfs/rules/deploy-scan.mjs +192 -0
- package/runtime/scripts/hfs/rules/deploy-workflow.mjs +219 -0
- package/runtime/scripts/hfs/rules/doc-fresh.mjs +286 -0
- package/runtime/scripts/hfs/rules/docker-health.mjs +249 -0
- package/runtime/scripts/hfs/rules/docker.mjs +24 -7
- package/runtime/scripts/hfs/rules/edition.mjs +11 -10
- package/runtime/scripts/hfs/rules/env-parity.mjs +373 -0
- package/runtime/scripts/hfs/rules/env-value-shape.mjs +113 -0
- package/runtime/scripts/hfs/rules/event-bus.mjs +3 -2
- package/runtime/scripts/hfs/rules/event-tables.mjs +100 -0
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +115 -15
- package/runtime/scripts/hfs/rules/fe-stack.mjs +57 -0
- package/runtime/scripts/hfs/rules/feature-files.mjs +28 -0
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +54 -35
- package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -2
- package/runtime/scripts/hfs/rules/lexicon.mjs +62 -0
- package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +89 -0
- package/runtime/scripts/hfs/rules/monorepo.mjs +14 -1
- package/runtime/scripts/hfs/rules/pipeline.mjs +8 -1
- package/runtime/scripts/hfs/rules/proof-commands.mjs +2 -0
- package/runtime/scripts/hfs/rules/release-provenance.mjs +211 -0
- package/runtime/scripts/hfs/rules/saga-delivery.mjs +66 -0
- package/runtime/scripts/hfs/rules/saga.mjs +8 -16
- package/runtime/scripts/hfs/rules/script-hygiene.mjs +151 -0
- package/runtime/scripts/hfs/rules/secret-custody-only.mjs +65 -0
- package/runtime/scripts/hfs/rules/secrets.mjs +26 -0
- package/runtime/scripts/hfs/rules/services.mjs +4 -4
- package/runtime/scripts/hfs/rules/stack-tooling.mjs +93 -0
- package/runtime/scripts/hfs/rules/stacks-deploy.mjs +76 -0
- package/runtime/scripts/hfs/rules/stacks-ingress.mjs +87 -0
- package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -0
- package/runtime/scripts/hfs/rules/stacks-release.mjs +366 -0
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +168 -0
- package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
- package/runtime/scripts/hfs/rules/stacks.mjs +13 -8
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +1 -1
- package/runtime/scripts/hfs/rules/supply-chain.mjs +167 -0
- package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +101 -0
- package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +77 -0
- package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +165 -0
- package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +103 -0
- package/runtime/scripts/hfs/rules/unused-symbol.mjs +320 -0
- package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +185 -0
- package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +19 -0
- package/runtime/scripts/hfs/sealed-members.mjs +101 -0
- package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +200 -0
- package/runtime/scripts/hfs/secret-document.mjs +97 -0
- package/runtime/scripts/hfs/secret-edit.mjs +170 -0
- package/runtime/scripts/hfs/secret-errors.mjs +20 -0
- package/runtime/scripts/hfs/secret-files.mjs +124 -0
- package/runtime/scripts/hfs/secret-init.mjs +300 -0
- package/runtime/scripts/hfs/secret-output.mjs +100 -0
- package/runtime/scripts/hfs/secret-sops.mjs +130 -0
- package/runtime/scripts/hfs/secret-store.mjs +163 -0
- package/runtime/scripts/hfs/secret.mjs +162 -193
- package/runtime/scripts/hfs/slot-imports.mjs +18 -3
- package/runtime/scripts/hfs/slot-required.mjs +13 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +2 -1
- package/runtime/scripts/hfs/slots.mjs +1 -0
- package/runtime/scripts/hfs/stack-config-kit.mjs +34 -0
- package/runtime/scripts/hfs/stack-deploy-config.mjs +313 -0
- package/runtime/scripts/hfs/stack-ingress-config.mjs +237 -0
- package/runtime/scripts/hfs/stacks-gate.mjs +414 -0
- package/runtime/scripts/hfs/starcistacks-runtime.mjs +61 -0
- package/runtime/scripts/hfs/tree-findings.mjs +22 -0
- package/runtime/scripts/lib/command-tokens.mjs +126 -0
- package/runtime/scripts/lib/compose-values.mjs +11 -0
- package/runtime/scripts/lib/deployment-findings.mjs +7 -0
- package/runtime/scripts/lib/json-clone.mjs +61 -0
- package/runtime/scripts/lib/list.mjs +4 -0
- package/runtime/scripts/lib/roots.mjs +59 -0
- package/runtime/scripts/lib/runtime-example.mjs +13 -0
- package/runtime/scripts/lib/secret-patterns.mjs +60 -5
- package/runtime/scripts/lib/sql-ast.mjs +32 -0
- package/runtime/scripts/lib/stack-services.mjs +1 -1
- package/runtime/scripts/lib/swarm-stack.mjs +126 -0
- package/runtime/scripts/lib/trim-end.mjs +8 -0
- package/runtime/scripts/lib/ts-analysis.mjs +23 -0
- package/runtime/scripts/lib/ts-ast.mjs +27 -2
- package/runtime/scripts/lib/workflow-events.mjs +8 -0
- package/scaffold/add-app.mjs +77 -59
- package/scaffold/add-cli-lite.mjs +24 -17
- package/scaffold/add-compose.mjs +79 -0
- package/scaffold/add-database-lite.mjs +14 -0
- package/scaffold/add-fe-overlay.mjs +122 -0
- package/scaffold/add-fe-page.mjs +269 -0
- package/scaffold/add-inbox-lite.mjs +87 -124
- package/scaffold/add-plan.mjs +183 -0
- package/scaffold/add-table.mjs +132 -70
- package/scaffold/add.mjs +186 -204
- package/scaffold/app.mjs +128 -43
- package/scaffold/edition-gate.mjs +3 -3
- package/scaffold/image.mjs +2 -2
- package/scaffold/lite-exports.mjs +2 -1
- package/scaffold/service.mjs +2 -2
- package/src/main.mjs +238 -119
- package/stack/age-install.mjs +37 -0
- package/stack/backup.mjs +100 -0
- package/stack/build-args.mjs +42 -0
- package/stack/build.mjs +173 -0
- package/stack/cli.mjs +130 -0
- package/stack/config.mjs +57 -0
- package/stack/context.mjs +57 -0
- package/stack/converge.mjs +107 -0
- package/stack/deploy.mjs +301 -0
- package/stack/digest.mjs +16 -0
- package/stack/docker-transport.mjs +53 -0
- package/stack/edge-alias.mjs +118 -0
- package/stack/errors.mjs +21 -0
- package/stack/image-scan.mjs +152 -0
- package/stack/ingress-create.mjs +114 -0
- package/stack/ingress-publish.mjs +273 -0
- package/stack/ingress.mjs +706 -0
- package/stack/jobs.mjs +51 -0
- package/stack/live-restore.mjs +177 -0
- package/stack/prepare.mjs +266 -0
- package/stack/private-file.mjs +22 -0
- package/stack/provision.mjs +90 -0
- package/stack/prune.mjs +46 -0
- package/stack/release-store.mjs +87 -0
- package/stack/remote.mjs +205 -0
- package/stack/render.mjs +149 -0
- package/stack/retire.mjs +47 -0
- package/stack/sealed.mjs +149 -0
- package/stack/task.mjs +42 -0
- package/stack/verify.mjs +99 -0
- package/sync/ci-commands.mjs +13 -0
- package/sync/index.mjs +127 -64
- package/sync/stack-images.mjs +127 -0
- package/templates/app/ci-workflows/github/workflows/ci.yml +5 -1
- package/templates/app/ci-workflows/github/workflows/images.yml +15 -3
- package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +148 -0
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +7 -3
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +38 -6
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +14 -2
- package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +148 -0
- package/templates/app/codegen-graphql.mjs.partial +13 -2
- package/templates/app/codegen-lite/scripts/codegen.mjs +6 -1
- package/templates/app/codegen-prelude.mjs.partial +8 -0
- package/templates/app/gitignore +4 -0
- package/templates/app/gitignore-lite +4 -0
- package/templates/app/hooks/husky/pre-commit +7 -12
- package/templates/app/package-scripts/package.json +1 -0
- package/templates/app/package-scripts/supabase.json +7 -0
- package/templates/app/package-scripts-lite/package.json +1 -7
- package/templates/app/skeleton/.editorconfig +1 -0
- package/templates/app/skeleton/.gitattributes +7 -1
- package/templates/app/skeleton/.github/dependabot.yml +17 -0
- package/templates/app/skeleton/.nvmrc +1 -1
- package/templates/app/skeleton/.sops.yaml +1 -1
- package/templates/app/skeleton/.starcistacks/application-stacks.yaml +134 -1
- package/templates/app/skeleton/.starcistacks/dev/README.md +16 -0
- package/templates/app/skeleton/.starcistacks/dev/environment.json +5 -0
- package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +71 -0
- package/templates/app/skeleton/.starcistacks/dev/runtime/env/KEYS.md +14 -0
- package/templates/app/skeleton/.starcistacks/production/README.md +37 -0
- package/templates/app/skeleton/.starcistacks/production/environment.json +5 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/Caddyfile +15 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +9 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +142 -0
- package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +14 -0
- package/templates/app/skeleton/README.md +6 -6
- package/templates/app/skeleton/scripts/codegen.mjs +1 -2
- package/templates/app/skeleton-lite/.editorconfig +16 -0
- package/templates/app/skeleton-lite/.gitattributes +8 -0
- package/templates/app/skeleton-lite/.github/dependabot.yml +17 -0
- package/templates/app/skeleton-lite/.nvmrc +1 -0
- package/templates/app/skeleton-lite/.sops.yaml +3 -2
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +193 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +20 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +2 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +16 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +8 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +120 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -0
- package/templates/app/skeleton-lite/.starcistacks/production/README.md +38 -0
- package/templates/app/skeleton-lite/.starcistacks/production/environment.json +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/Caddyfile +11 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/images.lock.yaml +8 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +121 -0
- package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +21 -0
- package/templates/app/skeleton-lite/.starciwork/brand/index.yaml +12 -0
- package/templates/app/skeleton-lite/.starciwork/workspace.yaml +9 -0
- package/templates/app/skeleton-lite/README.md +8 -4
- package/templates/app/skeleton-lite/supabase/config.toml +8 -1
- package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +15 -6
- package/templates/app/starciwork.gitignore-lite +6 -0
- package/templates/app/upgrade-full/.starcistacks/dev/README.md +13 -7
- package/templates/be/image/worker/Dockerfile +1 -1
- package/templates/be/inbox/database-client.spec.ts.tpl +22 -28
- package/templates/be/inbox/database-client.ts.tpl +14 -28
- package/templates/be/inbox/delivery-recorded.event.ts.tpl +36 -0
- package/templates/be/inbox/event-index.ts.tpl +1 -0
- package/templates/be/inbox/events.json.tpl +10 -0
- package/templates/be/inbox/migration.sql.tpl +1 -0
- package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +42 -0
- package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +9 -0
- package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +6 -0
- package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +19 -0
- package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +2 -0
- package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +16 -0
- package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +45 -0
- package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +15 -0
- package/templates/be/inbox/platform/event-bus/index.ts.tpl +5 -0
- package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +5 -0
- package/templates/be/inbox/platform/inbox/inbox.decorators.ts.tpl +9 -0
- package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +6 -0
- package/templates/be/inbox/platform/inbox/inbox.module.ts.tpl +19 -0
- package/templates/be/inbox/platform/inbox/inbox.options.ts.tpl +2 -0
- package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +10 -0
- package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +41 -0
- package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +19 -0
- package/templates/be/inbox/platform/inbox/index.ts.tpl +3 -0
- package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +7 -0
- package/templates/be/inbox/platform/migration.sql.tpl +48 -0
- package/templates/be/inbox/service.spec.ts.tpl +49 -20
- package/templates/be/inbox/service.ts.tpl +27 -16
- package/templates/be/inbox/sql.ts.tpl +2 -2
- package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -3
- package/templates/be/inbox/webhook-door.ts.tpl +4 -3
- package/templates/be/patterns/api/feature.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/api/feature.module.ts.tpl +2 -1
- package/templates/be/patterns/api/feature.options.ts.tpl +2 -0
- package/templates/be/patterns/api/graphql.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/api/graphql.module.ts.tpl +3 -2
- package/templates/be/patterns/api/graphql.options.ts.tpl +2 -0
- package/templates/be/patterns/cli/group.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/cli/group.module.ts.tpl +2 -1
- package/templates/be/patterns/cli/group.options.ts.tpl +2 -0
- package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +0 -3
- package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +0 -2
- package/templates/be/patterns/jobs/job.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/jobs/job.module.ts.tpl +2 -1
- package/templates/be/patterns/jobs/job.options.ts.tpl +2 -0
- package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/jobs/job.queue.module.ts.tpl +6 -3
- package/templates/be/patterns/jobs/job.queue.options.ts.tpl +2 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +3 -1
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +25 -9
- package/templates/be/patterns/queues/platform/queue.config.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform/queue.index.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +2 -2
- package/templates/be/patterns/queues/platform/queue.module.ts.tpl +11 -0
- package/templates/be/patterns/queues/platform/queue.options.ts.tpl +10 -0
- package/templates/be/patterns/queues/queue.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/queues/queue.module.ts.tpl +2 -1
- package/templates/be/patterns/queues/queue.options.ts.tpl +2 -0
- package/templates/be/patterns/reactors/message.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/reactors/message.module.ts.tpl +6 -3
- package/templates/be/patterns/reactors/message.options.ts.tpl +2 -0
- package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/reactors/reactor.module.ts.tpl +2 -1
- package/templates/be/patterns/reactors/reactor.options.ts.tpl +2 -0
- package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +188 -0
- package/templates/be/patterns/realtime/channel.gateway.ts.tpl +22 -0
- package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +2 -1
- package/templates/be/patterns/realtime/channel.graphql.options.ts.tpl +2 -0
- package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +110 -0
- package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +7 -0
- package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/saga/saga.message.module.ts.tpl +6 -3
- package/templates/be/patterns/saga/saga.message.options.ts.tpl +2 -0
- package/templates/be/patterns/saga/saga.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/saga/saga.module.ts.tpl +2 -1
- package/templates/be/patterns/saga/saga.options.ts.tpl +2 -0
- package/templates/be/patterns/webhooks/webhook.door.spec.ts.tpl +3 -3
- package/templates/be/patterns/webhooks/webhook.door.ts.tpl +3 -3
- package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +2 -1
- package/templates/be/patterns/webhooks/webhook.http.options.ts.tpl +2 -0
- package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +80 -26
- package/templates/be/platform/graphql/localize-errors.mapper.ts +2 -5
- package/templates/be/skeleton/apps/__app__/src/__app__.env.ts +21 -0
- package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +1 -1
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +15 -45
- package/templates/be/skeleton/apps/__app__/src/compose/api.ts +4 -0
- package/templates/be/skeleton/apps/__app__/src/compose/domain.ts +11 -0
- package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +44 -0
- package/templates/be/skeleton/apps/__app__/src/compose/system.ts +5 -0
- package/templates/be/skeleton/apps/__app__/src/compose/webhooks.ts +4 -0
- package/templates/be/skeleton/apps/__app__/src/main.ts +18 -10
- package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
- package/templates/be/skeleton/apps/cli/src/cli.env.ts +13 -0
- package/templates/be/skeleton/apps/cli/src/cli.options.ts +7 -17
- package/templates/be/skeleton/apps/cli/src/compose/cli.ts +5 -0
- package/templates/be/skeleton/apps/cli/src/compose/platform.ts +24 -0
- package/templates/be/skeleton/apps/cli/src/main.ts +3 -2
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.contracts.ts +2 -0
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.handler.ts +22 -0
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.query.ts +11 -0
- package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/api/system-health/system-health.module.ts +5 -3
- package/templates/be/skeleton/src/features/api/system-health/system-health.options.ts +2 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/ready.controller.ts +19 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module.ts +8 -3
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/cli.module.ts +3 -2
- package/templates/be/skeleton/src/features/cli/cli.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +2 -1
- package/templates/be/skeleton/src/features/cli/migrate/migrate.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +2 -1
- package/templates/be/skeleton/src/features/cli/seed/seed.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +5 -6
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +56 -4
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +36 -5
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +39 -8
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +14 -8
- package/templates/be/skeleton/src/modules/domain/identity/credential.contracts.ts +26 -0
- package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +119 -0
- package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +43 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +16 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -1
- package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +22 -4
- package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +8 -2
- package/templates/be/skeleton/src/modules/domain/identity/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +6 -6
- package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +11 -4
- package/templates/be/skeleton/src/modules/platform/database/database-probe.service.spec.ts +32 -0
- package/templates/be/skeleton/src/modules/platform/database/database-probe.service.ts +14 -0
- package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
- package/templates/be/skeleton/src/modules/platform/database/index.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +8 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +25 -6
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +16 -6
- package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +130 -0
- package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +58 -0
- package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +3 -0
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -4
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -10
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +3 -1
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/http-security/index.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +12 -5
- package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +13 -8
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.contracts.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.decorators.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +86 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +39 -0
- package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +139 -131
- package/templates/be/skeleton/src/modules/platform/i18n/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +91 -67
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +5 -4
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +0 -2
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +20 -6
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +16 -4
- package/templates/be/skeleton/src/modules/platform/retry/index.ts +3 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.port.ts +11 -0
- package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.spec.ts +36 -0
- package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.ts +10 -0
- package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +54 -0
- package/templates/be/skeleton-lite/apps/api/src/api.env.ts +25 -0
- package/templates/be/skeleton-lite/apps/api/src/api.options.ts +1 -1
- package/templates/be/skeleton-lite/apps/api/src/app.module.ts +18 -58
- package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +4 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +10 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +47 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/system.ts +5 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/webhooks.ts +4 -0
- package/templates/be/skeleton-lite/apps/api/src/main.ts +5 -13
- package/templates/be/skeleton-lite/apps/cli/src/cli.env.ts +17 -0
- package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +71 -5
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +29 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +68 -15
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +14 -9
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +9 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +16 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +19 -4
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +5 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -1
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -3
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +17 -5
- package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +24 -13
- package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +5 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/errors/database.error.ts +16 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +3 -0
- package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +5 -2
- package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +4 -1
- package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +7 -0
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +29 -0
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +16 -0
- package/templates/be/table/api/feature.module-definition.ts.tpl +6 -0
- package/templates/be/table/api/feature.module.ts.tpl +2 -1
- package/templates/be/table/api/feature.options.ts.tpl +2 -0
- package/templates/be/table/api/graphql.module-definition.ts.tpl +6 -0
- package/templates/be/table/api/graphql.module.ts.tpl +3 -2
- package/templates/be/table/api/graphql.options.ts.tpl +2 -0
- package/templates/be/table/migration.sql.tpl +1 -0
- package/templates/be/upgrade-full/apps/cli/src/app.module.ts +4 -6
- package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +7 -0
- package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +5 -0
- package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +9 -0
- package/templates/be/upgrade-full/apps/cli/src/main.ts +3 -0
- package/templates/be/upgrade-full/src/features/cli/cli.module.ts +3 -2
- package/templates/fe/auth/supabase-client.ts.tpl +34 -0
- package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +11 -0
- package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/index.tsx +6 -1
- package/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx +4 -0
- package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +31 -0
- package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/index.tsx +24 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -1
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +3 -4
- package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +15 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +18 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +19 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +7 -1
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/en.json +57 -0
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +6 -0
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/en.json +31 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +1 -1
- package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -0
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +1 -34
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +7 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +57 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +30 -24
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +1 -1
- package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/locale.ts +2 -2
- package/upgrade/check.mjs +3 -1
- package/upgrade/file-state.mjs +68 -0
- package/upgrade/index.mjs +58 -94
- package/runtime/scripts/api/git/log.mjs +0 -5
- package/templates/app/skeleton-lite/.starciwork/shell/index.yaml +0 -55
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
// database-migrations.mjs - L02 migration name, UTC ordering and base-branch immutability checks. Git reads go
|
|
2
2
|
// through the runtime API call files, and specs may inject the same small runner contract used by database.mjs.
|
|
3
3
|
import { lsTree } from '../../api/git/ls-tree.mjs';
|
|
4
|
-
import { log } from '../../api/git/log.mjs';
|
|
5
4
|
import { mergeBase } from '../../api/git/merge-base.mjs';
|
|
6
5
|
import { show } from '../../api/git/show.mjs';
|
|
7
6
|
import { withoutGitLocalEnv } from '../../lib/git.mjs';
|
|
8
7
|
import { eachInOrder, findInOrder } from '../../lib/in-order.mjs';
|
|
9
8
|
import { sameText } from '../../lib/same-text.mjs';
|
|
10
9
|
import { found, readText } from './read.mjs';
|
|
11
|
-
import { DB_MIGRATION_SHAPE, isActiveMigration, MIGRATIONS_ARCHIVE_DIR, MIGRATIONS_DIR } from './database-constants.mjs';
|
|
12
|
-
|
|
10
|
+
import { DB_MIGRATION_DESTRUCTIVE, DB_MIGRATION_SHAPE, isActiveMigration, MIGRATIONS_ARCHIVE_DIR, MIGRATIONS_DIR } from './database-constants.mjs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { loadTypescript } from '../../lib/package-at.mjs';
|
|
14
|
+
|
|
15
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const MIGRATION_NAME = /^(\d{14})_([a-z0-9]+(?:-[a-z0-9]+)*|contract_[a-z0-9]+(?:[-_][a-z0-9]+)*|[a-z0-9]+(?:-[a-z0-9]+)*_contract_[a-z0-9]+(?:[-_][a-z0-9]+)*)\.sql$/;
|
|
17
|
+
export const isMigrationName = (name) => MIGRATION_NAME.test(name);
|
|
18
|
+
const RESTAMP_HEADER = /^-- starci:restamp (\d{14}) (\d{14})(?:\r?\n|$)/;
|
|
19
|
+
const withoutRestampHeader = (text) => text?.replace(RESTAMP_HEADER, '') ?? null;
|
|
13
20
|
|
|
14
21
|
/** `<ts14>` -> the UTC instant it names, or null when the digits are no real time (month 13, day 32, ...). */
|
|
15
22
|
export function migrationStamp(text) {
|
|
@@ -27,7 +34,6 @@ export function defaultGit(args, { cwd }) {
|
|
|
27
34
|
if (verb === 'merge-base') { const sha = mergeBase(cwd, rest[0], rest[1]); return { ok: sha !== null, stdout: sha ?? '' }; }
|
|
28
35
|
let call = show;
|
|
29
36
|
if (verb === 'ls-tree') call = lsTree;
|
|
30
|
-
else if (verb === 'log') call = log;
|
|
31
37
|
const result = call(rest, { cwd, env, maxBuffer: 64 * 1024 * 1024 });
|
|
32
38
|
return { ok: !result.error && result.status === 0, stdout: result.stdout };
|
|
33
39
|
}
|
|
@@ -45,11 +51,11 @@ async function baseShaOf(git, repoRoot, base) {
|
|
|
45
51
|
return found;
|
|
46
52
|
}
|
|
47
53
|
|
|
48
|
-
async function
|
|
54
|
+
async function migrationFilesAt(git, repoRoot, sha) {
|
|
49
55
|
const result = await runGit(git, repoRoot, ['ls-tree', '-r', '--name-only', '-z', sha, '--', MIGRATIONS_DIR]);
|
|
50
56
|
if (!result.ok) return [];
|
|
51
57
|
const prefix = `${MIGRATIONS_DIR}/`;
|
|
52
|
-
return result.stdout.split('\0').filter(
|
|
58
|
+
return result.stdout.split('\0').filter((entry) => entry.startsWith(prefix));
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
async function baseFileText(git, repoRoot, sha, file) {
|
|
@@ -58,46 +64,67 @@ async function baseFileText(git, repoRoot, sha, file) {
|
|
|
58
64
|
return result.ok ? result.stdout : null;
|
|
59
65
|
}
|
|
60
66
|
|
|
61
|
-
|
|
62
|
-
const result = await runGit(git, repoRoot, ['log', '-1', '--format=%ct', '--', file]);
|
|
63
|
-
const seconds = Number(result.stdout.trim());
|
|
64
|
-
if (result.ok && Number.isFinite(seconds) && seconds > 0) return { at: seconds * 1000, tracked: true };
|
|
65
|
-
return { at: now(), tracked: false };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function localMigrationFindings(local, repoRoot, git, now) {
|
|
67
|
+
function localMigrationFindings(local) {
|
|
69
68
|
const findings = [];
|
|
70
69
|
const stamps = new Map();
|
|
71
|
-
|
|
70
|
+
for (const entry of local) {
|
|
72
71
|
if (!entry.match) {
|
|
73
72
|
findings.push(found(DB_MIGRATION_SHAPE, entry.file, `${entry.file} is not named <ts14>_<kebab>.sql; a migration is created by \`npm run db:new\` (supabase migration new), never renamed by hand`, { expected: '<ts14>_<kebab>.sql' }));
|
|
74
|
-
|
|
73
|
+
continue;
|
|
75
74
|
}
|
|
76
75
|
const stamp = migrationStamp(entry.match[1]);
|
|
77
76
|
if (stamp === null) {
|
|
78
|
-
findings.push(found(DB_MIGRATION_SHAPE, entry.file, `${entry.file} carries ${entry.match[1]}, which is no valid UTC time;
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
const limit = await migrationTimeLimit(git, repoRoot, entry.file, now);
|
|
82
|
-
if (stamp > limit.at) {
|
|
83
|
-
const boundary = limit.tracked ? `the file's commit time ${new Date(limit.at).toISOString()}` : `the current time ${new Date(limit.at).toISOString()}`;
|
|
84
|
-
findings.push(found(DB_MIGRATION_SHAPE, entry.file, `${entry.file} is stamped ${entry.match[1]}, in the future relative to ${boundary}; a migration stamp is its real creation time, never a hand-picked date`, { stamp: entry.match[1], tracked: limit.tracked }));
|
|
77
|
+
findings.push(found(DB_MIGRATION_SHAPE, entry.file, `${entry.file} carries ${entry.match[1]}, which is no valid UTC time; a migration stamp must name a valid UTC instant`, { stamp: entry.match[1] }));
|
|
78
|
+
continue;
|
|
85
79
|
}
|
|
86
80
|
if (stamps.has(stamp)) findings.push(found(DB_MIGRATION_SHAPE, entry.file, `${entry.file} shares stamp ${entry.match[1]} with ${stamps.get(stamp)}; stamps are strictly increasing`, { stamp: entry.match[1], other: stamps.get(stamp) }));
|
|
87
81
|
stamps.set(stamp, entry.file);
|
|
88
|
-
}
|
|
82
|
+
}
|
|
89
83
|
return findings;
|
|
90
84
|
}
|
|
91
85
|
|
|
92
|
-
|
|
86
|
+
/** HEAD owns the append boundary. Archived names retire the old chain, so a replacement baseline starts afresh. */
|
|
87
|
+
function migrationHistory(files, retired, localNames) {
|
|
88
|
+
const prefix = `${MIGRATIONS_DIR}/`;
|
|
89
|
+
const onHead = new Set(files.filter(isActiveMigration).map((file) => file.slice(prefix.length)));
|
|
90
|
+
const retiredNames = new Set([...retired, ...files.filter((file) => file.startsWith(`${MIGRATIONS_ARCHIVE_DIR}/`)).map((file) => file.slice(MIGRATIONS_ARCHIVE_DIR.length + 1))].filter((name) => !localNames.has(name)));
|
|
91
|
+
const stamps = [...onHead].filter((name) => !retiredNames.has(name)).map((name) => MIGRATION_NAME.exec(name)?.[1]).filter(Boolean).map(migrationStamp).filter((stamp) => stamp !== null);
|
|
92
|
+
return { onHead, latest: stamps.length ? Math.max(...stamps) : null };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function migrationOrderingFindings(local, history, restamped = new Set()) {
|
|
96
|
+
return local.flatMap((entry) => {
|
|
97
|
+
const stamp = entry.match ? migrationStamp(entry.match[1]) : null;
|
|
98
|
+
if (history.onHead.has(entry.name) || restamped.has(entry.name) || stamp === null || history.latest === null || stamp > history.latest) return [];
|
|
99
|
+
return [found(DB_MIGRATION_SHAPE, entry.file, `${entry.file} is stamped ${entry.match[1]}, not after every migration already in repository history (latest active ${new Date(history.latest).toISOString()}); a new migration sorts after the committed active chain`, { stamp: entry.match[1], latest: new Date(history.latest).toISOString() })];
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A restamp changes only the version, and its header proves the explicit old->new map against repository SQL. */
|
|
104
|
+
async function verifiedRestamps(local, files, repoRoot, git, sha) {
|
|
105
|
+
const prefix = `${MIGRATIONS_DIR}/`;
|
|
106
|
+
const active = files.filter(isActiveMigration).map((file) => file.slice(prefix.length));
|
|
107
|
+
const claims = local.map((entry) => ({ ...entry, header: RESTAMP_HEADER.exec(readText(repoRoot, entry.file) ?? '') })).filter((entry) => entry.header);
|
|
108
|
+
const accepted = [];
|
|
109
|
+
await eachInOrder(claims, async (entry) => {
|
|
110
|
+
const [, oldStamp, newStamp] = entry.header;
|
|
111
|
+
if (!entry.match || entry.match[1] !== newStamp || migrationStamp(oldStamp) === null || oldStamp === newStamp) return;
|
|
112
|
+
if (claims.filter((claim) => claim.header[1] === oldStamp).length !== 1) return;
|
|
113
|
+
const names = active.filter((name) => name.startsWith(`${oldStamp}_`) && name.slice(14) === entry.name.slice(14));
|
|
114
|
+
if (names.length !== 1 || local.some((candidate) => candidate.name === names[0])) return;
|
|
115
|
+
const committed = await baseFileText(git, repoRoot, sha, `${prefix}${names[0]}`);
|
|
116
|
+
if (committed === null || !sameText(withoutRestampHeader(readText(repoRoot, entry.file)), withoutRestampHeader(committed))) return;
|
|
117
|
+
accepted.push({ source: names[0], destination: entry.name });
|
|
118
|
+
});
|
|
119
|
+
return accepted;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function baseMigrationEntryFindings(entry, repoRoot, git, baseSha, onBase) {
|
|
93
123
|
if (!entry.match) return [];
|
|
94
|
-
const stamp = migrationStamp(entry.match[1]);
|
|
95
124
|
if (onBase.has(entry.name)) {
|
|
96
125
|
const committed = await baseFileText(git, repoRoot, baseSha.sha, entry.file);
|
|
97
126
|
const current = readText(repoRoot, entry.file);
|
|
98
127
|
if (!sameText(current, committed)) return [found(DB_MIGRATION_SHAPE, entry.file, `${entry.file} differs from its content on ${baseSha.ref}; a migration on the base branch is immutable - write a new migration`, { base: baseSha.ref })];
|
|
99
|
-
} else if (baseMax !== null && stamp !== null && stamp <= baseMax) {
|
|
100
|
-
return [found(DB_MIGRATION_SHAPE, entry.file, `${entry.file} is stamped ${entry.match[1]}, not after every migration on ${baseSha.ref} (latest ${String(new Date(baseMax).toISOString())}); a new migration sorts after the base ones`, { stamp: entry.match[1], base: baseSha.ref })];
|
|
101
128
|
}
|
|
102
129
|
return [];
|
|
103
130
|
}
|
|
@@ -112,26 +139,279 @@ function removedMigrationFindings(local, retired, baseNames, baseSha) {
|
|
|
112
139
|
|
|
113
140
|
/**
|
|
114
141
|
* L02 findings for names, stamps, ordering and immutability of supabase/migrations/*.sql. `archived` are the files of
|
|
115
|
-
* supabase/migrations/_archive/ (app.supabase.migrations.archive): never judged, and a
|
|
116
|
-
* cutover is retired, not removed.
|
|
142
|
+
* supabase/migrations/_archive/ (app.supabase.migrations.archive): never judged, and a migration moved there at a baseline
|
|
143
|
+
* cutover is retired from the ordering chain, not removed. New files append after HEAD's active migrations; timestamps
|
|
144
|
+
* on archived migrations, remote branches and the wall clock do not constrain a replacement baseline.
|
|
117
145
|
*/
|
|
118
|
-
export async function migrationShapeFindings({ repoRoot, migrations, archived = [], git, base
|
|
146
|
+
export async function migrationShapeFindings({ repoRoot, migrations, archived = [], git, base }) {
|
|
119
147
|
const prefix = `${MIGRATIONS_DIR}/`;
|
|
120
148
|
const local = migrations.map((file) => {
|
|
121
149
|
const name = file.startsWith(prefix) ? file.slice(prefix.length) : file;
|
|
122
150
|
return { file, name, match: MIGRATION_NAME.exec(name) };
|
|
123
151
|
});
|
|
124
|
-
const findings =
|
|
152
|
+
const findings = localMigrationFindings(local);
|
|
153
|
+
const retired = new Set(archived.map((file) => file.slice(MIGRATIONS_ARCHIVE_DIR.length + 1)));
|
|
154
|
+
const headFiles = await migrationFilesAt(git, repoRoot, 'HEAD');
|
|
125
155
|
const baseSha = await baseShaOf(git, repoRoot, base);
|
|
156
|
+
const baseFiles = baseSha ? await migrationFilesAt(git, repoRoot, baseSha.sha) : [];
|
|
157
|
+
const restamps = await verifiedRestamps(local, baseSha ? baseFiles : headFiles, repoRoot, git, baseSha?.sha ?? 'HEAD');
|
|
158
|
+
for (const restamp of restamps) retired.add(restamp.source);
|
|
159
|
+
const history = migrationHistory(headFiles, retired, new Set(local.map((entry) => entry.name)));
|
|
160
|
+
findings.push(...migrationOrderingFindings(local, history, new Set(restamps.map((entry) => entry.destination))));
|
|
126
161
|
if (!baseSha) return findings;
|
|
127
|
-
const baseNames =
|
|
162
|
+
const baseNames = baseFiles.filter(isActiveMigration).map((file) => file.slice(prefix.length));
|
|
128
163
|
const onBase = new Set(baseNames);
|
|
129
|
-
const baseStamps = baseNames.map((name) => MIGRATION_NAME.exec(name)?.[1]).filter(Boolean).map(migrationStamp).filter((stamp) => stamp !== null);
|
|
130
|
-
const baseMax = baseStamps.length ? Math.max(...baseStamps) : null;
|
|
131
164
|
await eachInOrder(local, async (entry) => {
|
|
132
|
-
findings.push(...await baseMigrationEntryFindings(entry, repoRoot, git, baseSha, onBase
|
|
165
|
+
findings.push(...await baseMigrationEntryFindings(entry, repoRoot, git, baseSha, onBase));
|
|
133
166
|
});
|
|
134
|
-
const retired = new Set(archived.map((file) => file.slice(MIGRATIONS_ARCHIVE_DIR.length + 1)));
|
|
135
167
|
findings.push(...removedMigrationFindings(local, retired, baseNames, baseSha));
|
|
136
168
|
return findings;
|
|
137
169
|
}
|
|
170
|
+
|
|
171
|
+
const sqlName = (token) => token && ['word', 'ident'].includes(token.t) ? token.v : null;
|
|
172
|
+
const sqlWord = (token, value) => token?.t === 'word' && token.v === value;
|
|
173
|
+
|
|
174
|
+
function commentEnd(text, start) {
|
|
175
|
+
if (text.startsWith('--', start)) {
|
|
176
|
+
const end = text.indexOf('\n', start + 2);
|
|
177
|
+
return end === -1 ? text.length : end;
|
|
178
|
+
}
|
|
179
|
+
if (!text.startsWith('/*', start)) return null;
|
|
180
|
+
let depth = 1;
|
|
181
|
+
let index = start + 2;
|
|
182
|
+
while (index < text.length && depth > 0) {
|
|
183
|
+
if (text.startsWith('/*', index)) { depth += 1; index += 2; }
|
|
184
|
+
else if (text.startsWith('*/', index)) { depth -= 1; index += 2; }
|
|
185
|
+
else index += 1;
|
|
186
|
+
}
|
|
187
|
+
return index;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function quotedToken(text, start, quote, escaped = false) {
|
|
191
|
+
let index = start + 1;
|
|
192
|
+
let value = '';
|
|
193
|
+
while (index < text.length) {
|
|
194
|
+
if (text[index] === quote && text[index + 1] === quote) { value += quote; index += 2; }
|
|
195
|
+
else if (text[index] === quote) return { value, end: index + 1 };
|
|
196
|
+
else if (escaped && text[index] === '\\') { value += text[index + 1] ?? ''; index += 2; }
|
|
197
|
+
else { value += text[index]; index += 1; }
|
|
198
|
+
}
|
|
199
|
+
return { value, end: index };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function sqlTokenAt(text, index) {
|
|
203
|
+
const char = text[index];
|
|
204
|
+
if (char === "'" || char === '"') {
|
|
205
|
+
const token = quotedToken(text, index, char);
|
|
206
|
+
return { t: char === '"' ? 'ident' : 'string', v: token.value, end: token.end };
|
|
207
|
+
}
|
|
208
|
+
if (/[eE]/.test(char) && text[index + 1] === "'") {
|
|
209
|
+
const token = quotedToken(text, index + 1, "'", true);
|
|
210
|
+
return { t: 'string', v: token.value, end: token.end };
|
|
211
|
+
}
|
|
212
|
+
const tag = char === '$' ? /^\$(?:[A-Za-z_]\w*)?\$/.exec(text.slice(index))?.[0] : null;
|
|
213
|
+
if (tag) {
|
|
214
|
+
const end = text.indexOf(tag, index + tag.length);
|
|
215
|
+
return { t: 'body', v: text.slice(index + tag.length, end === -1 ? text.length : end), end: end === -1 ? text.length : end + tag.length };
|
|
216
|
+
}
|
|
217
|
+
const word = /^[A-Za-z_][A-Za-z0-9_$]*/.exec(text.slice(index))?.[0];
|
|
218
|
+
if (word) return { t: 'word', v: word.toLowerCase(), end: index + word.length };
|
|
219
|
+
const number = /^\d+(?:\.\d+)?/.exec(text.slice(index))?.[0];
|
|
220
|
+
if (number) return { t: 'number', v: number, end: index + number.length };
|
|
221
|
+
return { t: 'punct', v: char, end: index + 1 };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Synchronous SQL tokens; literal values stay internal and must never be copied to a finding. */
|
|
225
|
+
export function databaseSqlTokens(text) {
|
|
226
|
+
const tokens = [];
|
|
227
|
+
let index = 0;
|
|
228
|
+
let line = 1;
|
|
229
|
+
while (index < text.length) {
|
|
230
|
+
if (/\s/.test(text[index])) { if (text[index] === '\n') line += 1; index += 1; continue; }
|
|
231
|
+
const end = commentEnd(text, index);
|
|
232
|
+
if (end !== null) { line += text.slice(index, end).split('\n').length - 1; index = end; continue; }
|
|
233
|
+
const token = sqlTokenAt(text, index);
|
|
234
|
+
tokens.push({ t: token.t, v: token.v, line, index });
|
|
235
|
+
line += text.slice(index, token.end).split('\n').length - 1;
|
|
236
|
+
index = token.end;
|
|
237
|
+
}
|
|
238
|
+
return tokens;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Split executable SQL, retaining function bodies as opaque tokens until their language is known. */
|
|
242
|
+
export function databaseSqlStatements(tokens) {
|
|
243
|
+
const statements = [];
|
|
244
|
+
let current = [];
|
|
245
|
+
for (const token of tokens) {
|
|
246
|
+
if (token.t === 'punct' && token.v === ';') { if (current.length) statements.push(current); current = []; }
|
|
247
|
+
else current.push(token);
|
|
248
|
+
}
|
|
249
|
+
if (current.length) statements.push(current);
|
|
250
|
+
return statements;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function executableStatementTokens(statement, depth) {
|
|
254
|
+
const isDo = sqlWord(statement[0], 'do');
|
|
255
|
+
const functionAt = statement.findIndex((token) => sqlWord(token, 'function') || sqlWord(token, 'procedure'));
|
|
256
|
+
const isFunction = sqlWord(statement[0], 'create') && functionAt > 0 && functionAt < 4;
|
|
257
|
+
if (!isDo && !isFunction) return statement;
|
|
258
|
+
const languageAt = statement.findIndex((token) => sqlWord(token, 'language'));
|
|
259
|
+
const language = statement[languageAt + 1]?.v ?? 'sql';
|
|
260
|
+
if (languageAt !== -1 && !['sql', 'plpgsql'].includes(language)) return statement;
|
|
261
|
+
return statement.flatMap((token, index) => {
|
|
262
|
+
const body = token.t === 'body' || (token.t === 'string' && (isDo || sqlWord(statement[index - 1], 'as')));
|
|
263
|
+
if (!body) return [token];
|
|
264
|
+
return databaseExecutableTokens(token.v, depth + 1).map((child) => ({ ...child, line: child.line + token.line - 1 }));
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** SQL and PL/pgSQL bodies are scanned; comments, data literals and other function languages are opaque. */
|
|
269
|
+
export function databaseExecutableTokens(text, depth = 0) {
|
|
270
|
+
const tokens = databaseSqlTokens(text);
|
|
271
|
+
if (depth > 8) return tokens;
|
|
272
|
+
return databaseSqlStatements(tokens).flatMap((statement) => [...executableStatementTokens(statement, depth), { t: 'punct', v: ';' }]);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function nameAt(tokens, index) {
|
|
276
|
+
const first = sqlName(tokens[index]);
|
|
277
|
+
if (first === null) return null;
|
|
278
|
+
const parts = [first];
|
|
279
|
+
let next = index + 1;
|
|
280
|
+
while (tokens[next]?.v === '.' && sqlName(tokens[next + 1]) !== null) { parts.push(sqlName(tokens[next + 1])); next += 2; }
|
|
281
|
+
return { name: parts.join('.'), leaf: parts.at(-1), next };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const skipWords = (tokens, index, words) => {
|
|
285
|
+
let next = index;
|
|
286
|
+
while (words.some((word) => sqlWord(tokens[next], word))) next += 1;
|
|
287
|
+
return next;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
function dropObjects(tokens, start) {
|
|
291
|
+
const kind = tokens[start + 1]?.v;
|
|
292
|
+
if (!['table', 'role', 'user', 'schema', 'type', 'view', 'materialized', 'sequence', 'function', 'procedure', 'index', 'policy', 'trigger'].includes(kind)) return [];
|
|
293
|
+
let index = skipWords(tokens, start + 2, ['view', 'if', 'exists', 'concurrently']);
|
|
294
|
+
const objects = [];
|
|
295
|
+
while (index < tokens.length) {
|
|
296
|
+
const object = nameAt(tokens, index);
|
|
297
|
+
if (!object) break;
|
|
298
|
+
objects.push({ ...object, kind, line: tokens[start].line });
|
|
299
|
+
if (tokens[object.next]?.v !== ',') break;
|
|
300
|
+
index = object.next + 1;
|
|
301
|
+
}
|
|
302
|
+
return objects;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function columnAlterObject(tokens, index, table, line) {
|
|
306
|
+
let next = index + 1;
|
|
307
|
+
if (sqlWord(tokens[next], 'column')) next += 1;
|
|
308
|
+
const column = nameAt(tokens, next);
|
|
309
|
+
if (!column) return null;
|
|
310
|
+
const tail = tokens.slice(column.next);
|
|
311
|
+
const type = sqlWord(tail[0], 'type') || (sqlWord(tail[0], 'set') && sqlWord(tail[1], 'data') && sqlWord(tail[2], 'type'));
|
|
312
|
+
const notNull = sqlWord(tail[0], 'set') && sqlWord(tail[1], 'not') && sqlWord(tail[2], 'null');
|
|
313
|
+
const defaultSet = tokens.some((token, at) => sqlWord(token, 'alter') && sqlName(tokens[at + 1 + Number(sqlWord(tokens[at + 1], 'column'))]) === column.leaf
|
|
314
|
+
&& sqlWord(tokens[at + 2 + Number(sqlWord(tokens[at + 1], 'column'))], 'set') && sqlWord(tokens[at + 3 + Number(sqlWord(tokens[at + 1], 'column'))], 'default'));
|
|
315
|
+
if (!type && !(notNull && !defaultSet)) return null;
|
|
316
|
+
return { name: `${table.name}.${column.name}`, leaf: column.leaf, table: table.leaf, kind: 'column', line };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function alteredColumn(table, column, line) {
|
|
320
|
+
return { name: `${table.name}.${column.name}`, leaf: column.leaf, table: table.leaf, kind: 'column', line };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function tableActionObject(body, index, table, line) {
|
|
324
|
+
if (sqlWord(body[index], 'drop')) {
|
|
325
|
+
const at = skipWords(body, index + 1, ['column', 'constraint', 'if', 'exists']);
|
|
326
|
+
if (sqlWord(body[at], 'default') || sqlWord(body[at], 'not')) return null;
|
|
327
|
+
const column = nameAt(body, at);
|
|
328
|
+
return column ? alteredColumn(table, column, line) : null;
|
|
329
|
+
}
|
|
330
|
+
if (sqlWord(body[index], 'rename')) {
|
|
331
|
+
const column = sqlWord(body[index + 1], 'column') ? nameAt(body, index + 2) : null;
|
|
332
|
+
return column ? alteredColumn(table, column, line) : { ...table, kind: 'table', line };
|
|
333
|
+
}
|
|
334
|
+
return sqlWord(body[index], 'alter') ? columnAlterObject(body, index, table, line) : null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function alterObjects(tokens, start) {
|
|
338
|
+
const table = nameAt(tokens, skipWords(tokens, start + 2, ['if', 'exists', 'only']));
|
|
339
|
+
if (!table) return [];
|
|
340
|
+
const body = tokens.slice(table.next);
|
|
341
|
+
return body.map((token, index) => tableActionObject(body, index, table, tokens[start].line)).filter(Boolean);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function truncatedObjects(tokens, start) {
|
|
345
|
+
const objects = [];
|
|
346
|
+
let index = skipWords(tokens, start + 1, ['table', 'only']);
|
|
347
|
+
while (index < tokens.length) {
|
|
348
|
+
const table = nameAt(tokens, index);
|
|
349
|
+
if (!table) break;
|
|
350
|
+
objects.push({ ...table, kind: 'table', line: tokens[start].line });
|
|
351
|
+
if (tokens[table.next]?.v !== ',') break;
|
|
352
|
+
index = skipWords(tokens, table.next + 1, ['only']);
|
|
353
|
+
}
|
|
354
|
+
return objects;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function destructiveObjects(statement) {
|
|
358
|
+
const objects = [];
|
|
359
|
+
for (let index = 0; index < statement.length; index += 1) {
|
|
360
|
+
if (sqlWord(statement[index], 'drop')) objects.push(...dropObjects(statement, index));
|
|
361
|
+
else if (sqlWord(statement[index], 'truncate')) objects.push(...truncatedObjects(statement, index));
|
|
362
|
+
else if (sqlWord(statement[index], 'alter') && sqlWord(statement[index + 1], 'table')) objects.push(...alterObjects(statement, index));
|
|
363
|
+
else if (sqlWord(statement[index], 'alter') && ['type', 'schema', 'view', 'sequence', 'function', 'procedure', 'index'].includes(statement[index + 1]?.v)) {
|
|
364
|
+
const object = nameAt(statement, index + 2);
|
|
365
|
+
if (object && statement.slice(object.next).some((token) => sqlWord(token, 'rename'))) objects.push({ ...object, kind: statement[index + 1].v, line: statement[index].line });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return objects;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function sourceNames(ts, text, file) {
|
|
372
|
+
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
|
|
373
|
+
const names = new Set();
|
|
374
|
+
const addSql = (sql) => { for (const token of databaseSqlTokens(sql)) if (sqlName(token) !== null) names.add(token.v); };
|
|
375
|
+
const visit = (node) => {
|
|
376
|
+
if (ts.isStringLiteralLike(node)) addSql(node.text);
|
|
377
|
+
if (ts.isTemplateExpression(node)) { addSql(node.head.text); for (const span of node.templateSpans) addSql(span.literal.text); }
|
|
378
|
+
if (/\.entity\.[cm]?tsx?$/.test(file) && ts.isIdentifier(node)) names.add(node.text);
|
|
379
|
+
ts.forEachChild(node, visit);
|
|
380
|
+
};
|
|
381
|
+
visit(source);
|
|
382
|
+
return names;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function destructiveObjectFinding(file, object, contract, references) {
|
|
386
|
+
const reference = references.find(({ names }) => names.has(object.leaf));
|
|
387
|
+
if (contract && !reference) return null;
|
|
388
|
+
const reason = reference && contract ? `${reference.file} still references this object; remove the reference before the contract migration`
|
|
389
|
+
: 'use a *_contract_* migration after the application stops using this object';
|
|
390
|
+
return found(DB_MIGRATION_DESTRUCTIVE, file, `${file}:${object.line} destructively changes ${object.name}; ${reason}`,
|
|
391
|
+
{ line: object.line, object: object.name, ...(reference ? { reference: reference.file } : {}) });
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function destructiveFileFindings(file, text, references) {
|
|
395
|
+
const contract = /_contract_[a-z0-9]+(?:[-_][a-z0-9]+)*\.sql$/.test(file);
|
|
396
|
+
const seen = new Set();
|
|
397
|
+
const objects = databaseSqlStatements(databaseExecutableTokens(text)).flatMap(destructiveObjects);
|
|
398
|
+
return objects.flatMap((object) => {
|
|
399
|
+
const key = `${object.line}:${object.name}`;
|
|
400
|
+
if (seen.has(key)) return [];
|
|
401
|
+
seen.add(key);
|
|
402
|
+
const finding = destructiveObjectFinding(file, object, contract, references);
|
|
403
|
+
return finding ? [finding] : [];
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** Destructive changes require a contract migration and no remaining SQL/entity reference in be/src. */
|
|
408
|
+
export function destructiveMigrationFindings({ repoRoot, migrations, files }) {
|
|
409
|
+
const sources = files.filter((file) => /^be\/src\/.+\.(?:sql|entity)\.[cm]?tsx?$/.test(file));
|
|
410
|
+
const ts = sources.length ? loadTypescript(repoRoot, HERE) : null;
|
|
411
|
+
if (sources.length && !ts) return [found(DB_MIGRATION_DESTRUCTIVE, 'hfs.json', 'destructive migration checks require the TypeScript parser to read be/src references')];
|
|
412
|
+
const references = sources.map((file) => ({ file, names: sourceNames(ts, readText(repoRoot, file) ?? '', file) }));
|
|
413
|
+
return migrations.flatMap((file) => {
|
|
414
|
+
const text = readText(repoRoot, file);
|
|
415
|
+
return text === null ? [] : destructiveFileFindings(file, text, references);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
@@ -5,6 +5,7 @@ import { eachInOrder } from '../../lib/in-order.mjs';
|
|
|
5
5
|
import { parsePlpgsqlBody, parseSql } from '../sql/pg-parse.mjs';
|
|
6
6
|
import { DB_DEFINER_SAFE, DB_DYNAMIC_DDL } from './database-constants.mjs';
|
|
7
7
|
|
|
8
|
+
const DB_DEFINER_REVOKE = 'DB_DEFINER_REVOKE';
|
|
8
9
|
const sval = (node) => node?.String?.sval;
|
|
9
10
|
const nameOf = (list, defaultSchema) => {
|
|
10
11
|
const parts = (list ?? []).map(sval).filter((part) => part !== undefined);
|
|
@@ -183,17 +184,66 @@ async function definerDynamicFindings(file, fn, queries, baseLine) {
|
|
|
183
184
|
return findings;
|
|
184
185
|
}
|
|
185
186
|
|
|
187
|
+
const OUTPUT_PARAMETER_MODES = new Set(['OUT', 'TABLE'].map((mode) => ['FUNC', 'PARAM', mode].join('_')));
|
|
188
|
+
const RESET_ALL_KIND = ['VAR', 'RESET', 'ALL'].join('_');
|
|
189
|
+
const inputParameters = (node) => (node.parameters ?? []).map((item) => item.FunctionParameter)
|
|
190
|
+
.filter((parameter) => !OUTPUT_PARAMETER_MODES.has(parameter?.mode));
|
|
191
|
+
|
|
192
|
+
function typeIdentity(type) {
|
|
193
|
+
const node = type?.TypeName ?? type;
|
|
194
|
+
const parts = (node?.names ?? []).map(sval).filter(Boolean);
|
|
195
|
+
if (parts[0] === 'pg_catalog') parts.shift();
|
|
196
|
+
return `${parts.join('.')}${node?.arrayBounds?.length ? '[]' : ''}`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const functionArguments = (fn) => fn.node.func?.objargs ?? inputParameters(fn.node).map((parameter) => parameter.argType);
|
|
200
|
+
const argumentIdentity = (args) => args.map(typeIdentity).join(',');
|
|
201
|
+
|
|
202
|
+
function matchesFunction(fn, object, defaultSchema) {
|
|
203
|
+
if (nameOf(object?.objname, defaultSchema) !== fn.name) return false;
|
|
204
|
+
return object?.args_unspecified === true || argumentIdentity(object?.objargs ?? []) === argumentIdentity(functionArguments(fn));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const matchesAlter = (fn, alter) => matchesFunction(fn, alter.node.func, alter.schema);
|
|
208
|
+
|
|
209
|
+
function grantMatchesFunction(fn, grant) {
|
|
210
|
+
if (grant.objtype !== 'OBJECT_FUNCTION') return false;
|
|
211
|
+
if (grant.privileges.length && !grant.privileges.includes('execute') && !grant.privileges.includes('all')) return false;
|
|
212
|
+
if (grant.targtype === 'ACL_TARGET_ALL_IN_SCHEMA') return grant.objects.some((object) => sval(object) === fn.schema);
|
|
213
|
+
return grant.objects.some((object) => matchesFunction(fn, object?.ObjectWithArgs, grant.defaultSchema));
|
|
214
|
+
}
|
|
215
|
+
|
|
186
216
|
function revokedRolesFor(fn, grants) {
|
|
187
217
|
const roles = new Set();
|
|
188
218
|
for (const grant of grants) {
|
|
189
|
-
if (grant.
|
|
190
|
-
if (!
|
|
191
|
-
|
|
192
|
-
|
|
219
|
+
if (grant.index <= (fn.originIndex ?? fn.index)) continue;
|
|
220
|
+
if (!grantMatchesFunction(fn, grant)) continue;
|
|
221
|
+
for (const role of grant.grantees) {
|
|
222
|
+
if (grant.grant) roles.delete(role);
|
|
223
|
+
else roles.add(role);
|
|
224
|
+
}
|
|
193
225
|
}
|
|
194
226
|
return roles;
|
|
195
227
|
}
|
|
196
228
|
|
|
229
|
+
function pathSetting(options, pinned, facts) {
|
|
230
|
+
for (const option of options) {
|
|
231
|
+
const setting = option?.DefElem?.arg?.VariableSetStmt;
|
|
232
|
+
if (setting?.name === 'search_path' || setting?.kind === RESET_ALL_KIND) pinned = facts.isSearchPathSet(option.DefElem);
|
|
233
|
+
}
|
|
234
|
+
return pinned;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function hasPinnedSearchPath(fn, facts) {
|
|
238
|
+
let pinned = pathSetting(fn.node.options ?? [], false, facts);
|
|
239
|
+
const originIndex = fn.originIndex ?? fn.index;
|
|
240
|
+
for (const alter of facts.alterFunctions) {
|
|
241
|
+
if (alter.index <= originIndex || !matchesAlter(fn, alter)) continue;
|
|
242
|
+
pinned = pathSetting(alter.node.actions ?? [], pinned, facts);
|
|
243
|
+
}
|
|
244
|
+
return pinned;
|
|
245
|
+
}
|
|
246
|
+
|
|
197
247
|
async function doBlockFindings(file, block) {
|
|
198
248
|
if (block.body === undefined || block.language !== 'plpgsql') {
|
|
199
249
|
return [found(DB_DYNAMIC_DDL, file, `${file}:${block.line} DO block in ${block.language} cannot be inspected; a DO block is plpgsql so a static pass sees its statements`, { line: block.line })];
|
|
@@ -206,14 +256,12 @@ async function doBlockFindings(file, block) {
|
|
|
206
256
|
function definerDeclarationFindings(file, fn, facts, exposed, definer) {
|
|
207
257
|
if (!definer) return [];
|
|
208
258
|
const findings = [];
|
|
209
|
-
|
|
210
|
-
|| facts.alterFunctions.some((alter) => alter.name === fn.name && alter.setsPath && alter.index > fn.index);
|
|
211
|
-
if (!pathSet) findings.push(found(DB_DEFINER_SAFE, file, `${file}:${fn.line} security definer function ${fn.name} sets no search_path; add \`set search_path = ''\` (or a pinned schema list) on the create, or an alter function in the same migration`, { line: fn.line, function: fn.name }));
|
|
259
|
+
if (!hasPinnedSearchPath(fn, facts)) findings.push(found(DB_DEFINER_SAFE, file, `${file}:${fn.line} security definer function ${fn.name} sets no search_path; add \`set search_path = ''\` (or a pinned schema list) on the create, or an alter function in the same migration`, { line: fn.line, function: fn.name }));
|
|
212
260
|
if (exposed.has(fn.schema)) {
|
|
213
261
|
const revoked = revokedRolesFor(fn, facts.grants);
|
|
214
262
|
if (!revoked.has('public') || !revoked.has('anon')) findings.push(found(DB_DEFINER_SAFE, file, `${file}:${fn.line} security definer function ${fn.name} lives in the exposed schema ${fn.schema}; keep it in a non-exposed schema or revoke execute from public and anon in the same migration`, { line: fn.line, function: fn.name }));
|
|
215
263
|
}
|
|
216
|
-
if (!fn.node.is_procedure && !fn.node.returnType) findings.push(found(DB_DEFINER_SAFE, file, `${file}:${fn.line} security definer function ${fn.name} declares no return type`, { line: fn.line, function: fn.name }));
|
|
264
|
+
if (!fn.altered && !fn.node.is_procedure && !fn.node.returnType) findings.push(found(DB_DEFINER_SAFE, file, `${file}:${fn.line} security definer function ${fn.name} declares no return type`, { line: fn.line, function: fn.name }));
|
|
217
265
|
return findings;
|
|
218
266
|
}
|
|
219
267
|
|
|
@@ -235,14 +283,76 @@ async function functionBodyFindings(file, fn, definer) {
|
|
|
235
283
|
async function functionFindings(file, fn, facts, exposed) {
|
|
236
284
|
const definer = securityDefiner(fn.node);
|
|
237
285
|
const findings = definerDeclarationFindings(file, fn, facts, exposed, definer);
|
|
238
|
-
|
|
286
|
+
const becomesDefiner = facts.alterFunctions.some((alter) => alter.securityDefiner && alter.index > fn.index && matchesAlter(fn, alter));
|
|
287
|
+
findings.push(...await functionBodyFindings(file, fn, definer || becomesDefiner));
|
|
239
288
|
return findings;
|
|
240
289
|
}
|
|
241
290
|
|
|
291
|
+
function alteredDefinerFindings(file, alter, facts, exposed) {
|
|
292
|
+
if (!alter.securityDefiner) return [];
|
|
293
|
+
const created = facts.functions.filter((fn) => fn.index < alter.index && matchesAlter(fn, alter)).at(-1);
|
|
294
|
+
const node = created?.node ?? { func: alter.node.func };
|
|
295
|
+
const fn = { ...alter, node, altered: true, originIndex: created?.index ?? -1 };
|
|
296
|
+
return definerDeclarationFindings(file, fn, facts, exposed, true);
|
|
297
|
+
}
|
|
298
|
+
|
|
242
299
|
/** All PL/pgSQL-backed L04/L06 findings for one migration's extracted facts. */
|
|
243
300
|
export async function plpgsqlFindings(file, facts, exposed) {
|
|
244
301
|
const findings = [];
|
|
245
302
|
await eachInOrder(facts.doBlocks, async (block) => { findings.push(...await doBlockFindings(file, block)); });
|
|
246
303
|
await eachInOrder(facts.functions, async (fn) => { findings.push(...await functionFindings(file, fn, facts, exposed)); });
|
|
304
|
+
for (const alter of facts.alterFunctions) findings.push(...alteredDefinerFindings(file, alter, facts, exposed));
|
|
247
305
|
return findings;
|
|
248
306
|
}
|
|
307
|
+
|
|
308
|
+
const functionKey = (fn) => `${fn.name}/${argumentIdentity(functionArguments(fn))}`;
|
|
309
|
+
|
|
310
|
+
function applyFunctionAlter(functions, alter) {
|
|
311
|
+
if (!functionOption({ options: alter.node.actions }, 'security').length) return;
|
|
312
|
+
const matching = [...functions.values()].filter((fn) => matchesAlter(fn, alter));
|
|
313
|
+
if (!matching.length) matching.push({ ...alter, node: { func: alter.node.func }, originIndex: -1 });
|
|
314
|
+
for (const fn of matching) {
|
|
315
|
+
functions.set(functionKey(fn), { ...fn, file: alter.file, line: alter.line, definer: alter.securityDefiner });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function applyFunctionDrop(functions, statement) {
|
|
320
|
+
if (statement.node.removeType !== 'OBJECT_FUNCTION') return;
|
|
321
|
+
for (const fn of functions.values()) {
|
|
322
|
+
if (statement.node.objects?.some((object) => matchesFunction(fn, object.ObjectWithArgs, statement.defaultSchema))) {
|
|
323
|
+
functions.delete(functionKey(fn));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function finalFunctionAccess(analyses) {
|
|
329
|
+
const functions = new Map();
|
|
330
|
+
const grants = [];
|
|
331
|
+
let offset = 0;
|
|
332
|
+
for (const { file, facts } of analyses) {
|
|
333
|
+
const ordered = (items) => items.map((item) => ({ ...item, file, index: offset + item.index }));
|
|
334
|
+
const created = new Map(ordered(facts.functions).map((fn) => [fn.index, fn]));
|
|
335
|
+
const altered = new Map(ordered(facts.alterFunctions).map((fn) => [fn.index, fn]));
|
|
336
|
+
grants.push(...ordered(facts.grants));
|
|
337
|
+
for (const statement of ordered(facts.statements)) {
|
|
338
|
+
const fn = created.get(statement.index);
|
|
339
|
+
if (fn) {
|
|
340
|
+
const key = functionKey(fn);
|
|
341
|
+
const originIndex = functions.get(key)?.originIndex ?? fn.index;
|
|
342
|
+
functions.set(key, { ...fn, originIndex, definer: securityDefiner(fn.node) });
|
|
343
|
+
}
|
|
344
|
+
const alter = altered.get(statement.index);
|
|
345
|
+
if (alter) applyFunctionAlter(functions, alter);
|
|
346
|
+
if (statement.type === 'DropStmt') applyFunctionDrop(functions, statement);
|
|
347
|
+
}
|
|
348
|
+
offset += facts.statements.length;
|
|
349
|
+
}
|
|
350
|
+
return { functions: [...functions.values()], grants };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Authenticated execution is judged after all active migrations, including later revokes and grants. */
|
|
354
|
+
export function migrationSetDefinerFindings(analyses, exposed) {
|
|
355
|
+
const { functions, grants } = finalFunctionAccess(analyses);
|
|
356
|
+
return functions.filter((fn) => fn.definer && exposed.has(fn.schema) && !revokedRolesFor(fn, grants).has('authenticated'))
|
|
357
|
+
.map((fn) => found(DB_DEFINER_REVOKE, fn.file, `${fn.file}:${fn.line} security definer function ${fn.name} lives in the exposed schema ${fn.schema}; the migration set ends without revoking execute from authenticated; revoke execute from public, anon and authenticated`, { line: fn.line, function: fn.name }));
|
|
358
|
+
}
|