@starci/hfs 4.0.7 → 4.0.9
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 +22 -0
- package/README.md +86 -43
- package/emit/contracts.mjs +56 -9
- package/emit/db-types.mjs +169 -0
- package/emit/events.mjs +41 -0
- package/emit/operations-worker.mjs +1 -1
- package/emit/operations.mjs +2 -2
- package/emit/schema-worker.mjs +1 -1
- package/lint/bound-sys.cjs +50 -0
- package/lint/run.mjs +54 -20
- package/package.json +14 -9
- package/report/sonar.mjs +8 -8
- package/runtime/engine/config.mjs +494 -0
- package/runtime/engine/invalid-config.mjs +15 -0
- package/runtime/engine/orca-config.mjs +27 -0
- package/runtime/engine/runtime-root.mjs +5 -0
- package/runtime/engine/yaml.mjs +3 -3
- package/runtime/knowledge/hfs/canon-pins.yaml +163 -32
- package/runtime/knowledge/hfs/peer-integrations.yaml +2 -2
- package/runtime/knowledge/hfs/rules.yaml +2059 -0
- package/runtime/knowledge/hfs/slots.yaml +915 -123
- package/runtime/knowledge/patterns/be/api.yaml +369 -0
- package/runtime/knowledge/patterns/be/cli.yaml +297 -0
- package/runtime/knowledge/patterns/be/domain.yaml +113 -0
- package/runtime/knowledge/patterns/be/event-bus.yaml +285 -0
- package/runtime/knowledge/patterns/be/jobs.yaml +261 -0
- package/runtime/knowledge/patterns/be/projections.yaml +178 -0
- package/runtime/knowledge/patterns/be/queues.yaml +236 -0
- package/runtime/knowledge/patterns/be/reactors.yaml +140 -0
- package/runtime/knowledge/patterns/be/realtime.yaml +222 -0
- package/runtime/knowledge/patterns/be/saga.yaml +248 -0
- package/runtime/knowledge/patterns/be/webhooks.yaml +221 -0
- package/runtime/knowledge/patterns/fe/folder.yaml +133 -27
- package/runtime/knowledge/sonar-gate.yaml +53 -19
- package/runtime/modules/kernel/failure-codes.yaml +580 -50
- package/runtime/modules/models/registry.yaml +591 -0
- package/runtime/modules/models/runtimes.yaml +701 -0
- package/runtime/scripts/api/fs/forbidden-root.mjs +51 -0
- package/runtime/scripts/api/fs/is-link-like.mjs +25 -0
- package/runtime/scripts/api/fs/lib.mjs +14 -0
- package/runtime/scripts/api/fs/safe-remove.mjs +104 -0
- package/runtime/scripts/api/git/config-get.mjs +10 -0
- package/runtime/scripts/api/git/diff.mjs +5 -0
- package/runtime/scripts/api/git/lib.mjs +17 -27
- package/runtime/scripts/api/git/log.mjs +5 -0
- package/runtime/scripts/api/git/ls-files.mjs +6 -0
- package/runtime/scripts/api/git/ls-tree.mjs +5 -0
- package/runtime/scripts/api/git/merge-base.mjs +9 -0
- package/runtime/scripts/api/git/remote.mjs +5 -0
- package/runtime/scripts/api/git/rev-parse-query.mjs +5 -0
- package/runtime/scripts/api/git/show.mjs +6 -0
- package/runtime/scripts/api/sops/decrypt.mjs +10 -6
- package/runtime/scripts/api/sops/lib.mjs +35 -0
- package/runtime/scripts/api/sops/seal.mjs +18 -0
- package/runtime/scripts/{lib/hfs-allows.mjs → hfs/allows.mjs} +3 -3
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +220 -0
- package/runtime/scripts/{checks → hfs}/architecture/backend.mjs +17 -24
- package/runtime/scripts/{checks → hfs}/architecture/background-unowned.mjs +16 -34
- package/runtime/scripts/{checks → hfs}/architecture/client-reaches-server.mjs +3 -1
- package/runtime/scripts/{checks → hfs}/architecture/clones.mjs +4 -4
- package/runtime/scripts/{checks → hfs}/architecture/config.mjs +11 -8
- package/runtime/scripts/{checks → hfs}/architecture/connection-map.mjs +21 -9
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +88 -0
- package/runtime/scripts/hfs/architecture/context-map.mjs +237 -0
- package/runtime/scripts/hfs/architecture/context-owner.mjs +82 -0
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +47 -0
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +60 -0
- package/runtime/scripts/{checks → hfs}/architecture/contract-fixture-guard.mjs +5 -11
- package/runtime/scripts/{checks → hfs}/architecture/contracts.mjs +33 -162
- package/runtime/scripts/{checks → hfs}/architecture/dead-exports.mjs +58 -29
- package/runtime/scripts/{checks → hfs}/architecture/doc-language.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/entrypoint.mjs +2 -1
- package/runtime/scripts/{checks → hfs}/architecture/fe-slot-allows.mjs +4 -4
- package/runtime/scripts/{checks → hfs}/architecture/feature-shape.mjs +6 -3
- package/runtime/scripts/{checks → hfs}/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/frontend.mjs +17 -43
- package/runtime/scripts/{checks → hfs}/architecture/hfs-graph.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/hfs.mjs +27 -42
- package/runtime/scripts/{checks → hfs}/architecture/i18n-keys.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/index.mjs +17 -4
- package/runtime/scripts/{checks → hfs}/architecture/machine-ast.mjs +44 -11
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +27 -0
- package/runtime/scripts/{checks → hfs}/architecture/module-per-transport.mjs +30 -6
- package/runtime/scripts/hfs/architecture/next-data-contract.mjs +96 -0
- package/runtime/scripts/{checks → hfs}/architecture/next-data.mjs +40 -168
- package/runtime/scripts/{checks → hfs}/architecture/owners.mjs +35 -15
- package/runtime/scripts/{checks → hfs}/architecture/register-once.mjs +2 -1
- package/runtime/scripts/{checks → hfs}/architecture/registration.mjs +23 -81
- package/runtime/scripts/{checks → hfs}/architecture/required-files.mjs +21 -13
- package/runtime/scripts/{checks → hfs}/architecture/schema-owner.mjs +17 -114
- package/runtime/scripts/hfs/architecture/server-action.mjs +24 -0
- package/runtime/scripts/hfs/architecture/side-boundary.mjs +16 -0
- package/runtime/scripts/{checks → hfs}/architecture/source-names.mjs +27 -147
- package/runtime/scripts/{checks → hfs}/architecture/sql-owner.mjs +59 -18
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +64 -0
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +111 -0
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +131 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +42 -0
- package/runtime/scripts/hfs/architecture/supabase.mjs +434 -0
- package/runtime/scripts/{checks → hfs}/architecture/surface.mjs +10 -4
- package/runtime/scripts/{checks → hfs}/architecture/test-world-files.mjs +11 -21
- package/runtime/scripts/{checks → hfs}/architecture/tiers.mjs +47 -5
- package/runtime/scripts/{checks → hfs}/architecture/transport-owner.mjs +3 -3
- package/runtime/scripts/{checks → hfs}/architecture/typescript.mjs +13 -27
- package/runtime/scripts/{checks → hfs}/architecture.mjs +5 -5
- package/runtime/scripts/{lib/hfs-check.mjs → hfs/check.mjs} +95 -84
- package/runtime/scripts/hfs/coverage-scope.mjs +162 -0
- package/runtime/scripts/hfs/declaration-shape.mjs +62 -0
- package/runtime/scripts/hfs/declaration-slots.mjs +75 -0
- package/runtime/scripts/hfs/edition-slots.mjs +66 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +247 -0
- package/runtime/scripts/hfs/param-names.mjs +18 -0
- package/runtime/scripts/{lib/hfs-path-findings.mjs → hfs/path-findings.mjs} +7 -7
- package/runtime/scripts/{lib → hfs}/repo-identity.mjs +9 -6
- package/runtime/scripts/hfs/rule-params-shape.mjs +50 -0
- package/runtime/scripts/hfs/rules/app-root.mjs +51 -0
- package/runtime/scripts/hfs/rules/cli.mjs +37 -0
- package/runtime/scripts/hfs/rules/contract-compat.mjs +71 -0
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/contract.mjs +11 -9
- package/runtime/scripts/hfs/rules/database-config.mjs +102 -0
- package/runtime/scripts/hfs/rules/database-constants.mjs +13 -0
- package/runtime/scripts/hfs/rules/database-migrations.mjs +111 -0
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +233 -0
- package/runtime/scripts/hfs/rules/database-sql.mjs +336 -0
- package/runtime/scripts/hfs/rules/database.mjs +68 -0
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/deps.mjs +1 -1
- package/runtime/scripts/hfs/rules/docker.mjs +259 -0
- package/runtime/scripts/hfs/rules/edition.mjs +111 -0
- package/runtime/scripts/hfs/rules/event-bus.mjs +153 -0
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +66 -0
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/fe-no-tests.mjs +6 -5
- package/runtime/scripts/{lib/hfs-rules/frontend.mjs → hfs/rules/frontend-tree.mjs} +9 -9
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/integration-specs.mjs +3 -6
- package/runtime/scripts/hfs/rules/kinds.mjs +84 -0
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/lint-suppression.mjs +1 -1
- package/runtime/scripts/hfs/rules/monorepo.mjs +179 -0
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/peer-integrations.mjs +3 -3
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/pipeline.mjs +11 -21
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/proof-commands.mjs +2 -2
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/read.mjs +1 -1
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/repo-local-checks.mjs +2 -2
- package/runtime/scripts/hfs/rules/saga.mjs +170 -0
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/secrets.mjs +4 -4
- package/runtime/scripts/hfs/rules/services.mjs +220 -0
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/spec-placement.mjs +1 -1
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/stacks.mjs +5 -5
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +89 -0
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/test-topology.mjs +2 -2
- package/runtime/scripts/hfs/secret.mjs +175 -0
- package/runtime/scripts/{lib/hfs-slots.mjs → hfs/slots.mjs} +114 -150
- package/runtime/scripts/hfs/sql/pg-parse.mjs +46 -0
- package/runtime/scripts/hfs/test-world-slot.mjs +6 -0
- package/runtime/scripts/{lib/hfs-tree.mjs → hfs/tree.mjs} +6 -5
- package/runtime/scripts/{checks → hfs}/typescript-programs.mjs +0 -5
- package/runtime/scripts/{lib/hfs-view.mjs → hfs/view.mjs} +6 -5
- package/runtime/scripts/lib/dockerfile.mjs +124 -0
- package/runtime/scripts/lib/env.mjs +10 -0
- package/runtime/scripts/lib/event-contract.mjs +103 -0
- package/runtime/scripts/lib/git.mjs +36 -0
- package/runtime/scripts/lib/graphql-contract.mjs +429 -0
- package/runtime/scripts/lib/i18n.mjs +86 -0
- package/runtime/scripts/lib/is-main.mjs +13 -0
- package/runtime/scripts/lib/json.mjs +13 -3
- package/runtime/scripts/lib/language.mjs +26 -7
- package/runtime/scripts/lib/list.mjs +20 -0
- package/runtime/scripts/lib/package-at.mjs +37 -0
- package/runtime/scripts/lib/path-key.mjs +59 -2
- package/runtime/scripts/lib/read-text.mjs +34 -0
- package/runtime/scripts/lib/regex.mjs +4 -0
- package/runtime/scripts/lib/same-text.mjs +5 -0
- package/runtime/scripts/lib/secret-patterns.mjs +4 -3
- package/runtime/scripts/lib/sleep-sync.mjs +2 -1
- package/runtime/scripts/lib/sops-envelope.mjs +58 -0
- package/runtime/scripts/lib/stack-declaration.mjs +2 -2
- package/runtime/scripts/lib/stack-services.mjs +7 -31
- package/runtime/scripts/lib/ts-ast.mjs +35 -0
- package/runtime/scripts/{checks/common.mjs → lib/walk.mjs} +5 -15
- package/scaffold/add-app.mjs +174 -0
- package/scaffold/add-cli-lite.mjs +257 -0
- package/scaffold/add-inbox-lite.mjs +314 -0
- package/scaffold/add-table.mjs +344 -0
- package/scaffold/add.mjs +299 -0
- package/scaffold/app.mjs +359 -70
- package/scaffold/edition-gate.mjs +25 -0
- package/scaffold/image.mjs +19 -0
- package/scaffold/lite-exports.mjs +186 -0
- package/scaffold/service.mjs +33 -27
- package/scaffold/supabase-ports.mjs +26 -0
- package/src/main.mjs +327 -0
- package/sync/cli.mjs +6 -9
- package/sync/format.mjs +1 -1
- package/sync/hygiene.mjs +17 -17
- package/sync/index.mjs +171 -71
- package/sync/managed.mjs +19 -11
- package/sync/sonar-key.mjs +5 -0
- package/templates/app/ci-workflows/github/workflows/ci.yml +14 -14
- package/templates/app/ci-workflows/github/workflows/images.yml +37 -0
- package/templates/app/ci-workflows/sonar-steps.yml +9 -0
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +66 -0
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +31 -0
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +37 -0
- package/templates/app/codegen-lite/scripts/codegen.mjs +32 -0
- package/templates/app/docker-ignore/dockerignore +50 -0
- package/templates/app/gitignore-lite +35 -0
- package/templates/app/hooks/husky/pre-commit +3 -8
- package/templates/app/hooks/husky/pre-push +45 -6
- package/templates/app/package-scripts/core.json +8 -0
- package/templates/app/package-scripts/package.json +3 -10
- package/templates/app/package-scripts-lite/package.json +15 -0
- package/templates/app/quality-config/codecov.yml +15 -3
- package/templates/app/quality-config/sonar-base.properties +4 -0
- package/templates/app/quality-config/sonar-project.properties +2 -5
- package/templates/app/quality-config-lite/sonar-project.properties +5 -0
- package/templates/app/skeleton/.starcistacks/dev/seeds/primary-notes.sql +5 -0
- package/templates/app/skeleton/.starciwork/features/system-health/index.yaml +4 -0
- package/templates/app/skeleton/.starciwork/{features/index.yaml → index.yaml} +2 -2
- package/templates/app/skeleton/README.md +7 -5
- package/templates/app/skeleton-lite/.sops.yaml +3 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +5 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/secrets/.gitkeep +1 -0
- package/templates/app/skeleton-lite/.starciwork/shell/index.yaml +55 -0
- package/templates/app/skeleton-lite/README.md +40 -0
- package/templates/app/skeleton-lite/supabase/config.toml +41 -0
- package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +56 -0
- package/templates/app/skeleton-lite/supabase/seed.sql +0 -0
- package/templates/app/task-graph/turbo.json +23 -0
- package/templates/app/tool-config-lite/prettierignore +2 -0
- package/templates/app/tool-config-lite/prettierrc +1 -0
- package/templates/app/upgrade-full/.starcistacks/application-stacks.yaml +38 -0
- package/templates/app/upgrade-full/.starcistacks/dev/README.md +10 -0
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/api.yaml +13 -0
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/compose.yaml +3 -0
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/postgres.yaml +12 -0
- package/templates/app/upgrade-full/.starcistacks/dev/infra/metadata.json +7 -0
- package/templates/be/common/main-listen.ts.partial +14 -0
- package/templates/be/common/sql-ident.ts.partial +7 -0
- package/templates/be/common/sql-tag.ts.partial +5 -0
- package/templates/be/image/api/Dockerfile +38 -0
- package/templates/be/image/cli/Dockerfile +38 -0
- package/templates/be/image/worker/Dockerfile +36 -0
- package/templates/be/inbox/database-client.ts.tpl +50 -0
- package/templates/be/inbox/index.ts.tpl +2 -0
- package/templates/be/inbox/migration.sql.tpl +29 -0
- package/templates/be/inbox/module-definition.ts.tpl +6 -0
- package/templates/be/inbox/module.ts.tpl +14 -0
- package/templates/be/inbox/options.ts.tpl +2 -0
- package/templates/be/inbox/service.ts.tpl +33 -0
- package/templates/be/inbox/sql.ts.tpl +16 -0
- package/templates/be/inbox/webhook-door.ts.tpl +36 -0
- package/templates/be/patterns/api/application.command.http.ts.tpl +10 -0
- package/templates/be/patterns/api/application.command.ts.tpl +10 -0
- package/templates/be/patterns/api/application.contracts.ts.tpl +11 -0
- package/templates/be/patterns/api/application.handler.http.ts.tpl +22 -0
- package/templates/be/patterns/api/application.handler.ts.tpl +22 -0
- package/templates/be/patterns/api/feature.index.http.ts.tpl +1 -0
- package/templates/be/patterns/api/feature.index.ts.tpl +1 -0
- package/templates/be/patterns/api/feature.module.ts.tpl +6 -0
- package/templates/be/patterns/api/graphql.input.ts.tpl +13 -0
- package/templates/be/patterns/api/graphql.mapper.ts.tpl +9 -0
- package/templates/be/patterns/api/graphql.module.ts.tpl +7 -0
- package/templates/be/patterns/api/graphql.resolver.ts.tpl +27 -0
- package/templates/be/patterns/api/graphql.type.ts.tpl +9 -0
- package/templates/be/patterns/api/http.controller.ts.tpl +27 -0
- package/templates/be/patterns/api/http.mapper.ts.tpl +9 -0
- package/templates/be/patterns/api/http.module.ts.tpl +7 -0
- package/templates/be/patterns/api/http.request.ts.tpl +10 -0
- package/templates/be/patterns/api/http.response.ts.tpl +5 -0
- package/templates/be/patterns/cli/group.cli.spec.ts.tpl +15 -0
- package/templates/be/patterns/cli/group.cli.ts.tpl +11 -0
- package/templates/be/patterns/cli/group.module.ts.tpl +7 -0
- package/templates/be/patterns/cli/sub.run.cli.spec.ts.tpl +17 -0
- package/templates/be/patterns/cli/sub.run.cli.ts.tpl +15 -0
- package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +22 -0
- package/templates/be/patterns/event-bus/platform/event-bus.config.ts.tpl +19 -0
- package/templates/be/patterns/event-bus/platform/event-bus.contracts.ts.tpl +58 -0
- package/templates/be/patterns/event-bus/platform/event-bus.decorators.ts.tpl +46 -0
- package/templates/be/patterns/event-bus/platform/event-bus.log-events.ts.tpl +12 -0
- package/templates/be/patterns/event-bus/platform/event-bus.module-definition.ts.tpl +10 -0
- package/templates/be/patterns/event-bus/platform/event-bus.module.ts.tpl +52 -0
- package/templates/be/patterns/event-bus/platform/event-bus.options.ts.tpl +19 -0
- package/templates/be/patterns/event-bus/platform/event-bus.port.ts.tpl +28 -0
- package/templates/be/patterns/event-bus/platform/event-bus.service.spec.ts.tpl +165 -0
- package/templates/be/patterns/event-bus/platform/event-bus.service.ts.tpl +93 -0
- package/templates/be/patterns/event-bus/platform/event-envelope.policy.spec.ts.tpl +90 -0
- package/templates/be/patterns/event-bus/platform/event-envelope.policy.ts.tpl +58 -0
- package/templates/be/patterns/event-bus/platform/event-relay.service.spec.ts.tpl +136 -0
- package/templates/be/patterns/event-bus/platform/event-relay.service.ts.tpl +56 -0
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +272 -0
- package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +128 -0
- package/templates/be/patterns/event-bus/platform/event-transport.port.ts.tpl +180 -0
- package/templates/be/patterns/event-bus/platform/event.policy.spec.ts.tpl +67 -0
- package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +38 -0
- package/templates/be/patterns/event-bus/platform/index.ts.tpl +11 -0
- package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.spec.ts.tpl +348 -0
- package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +241 -0
- package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +15 -0
- package/templates/be/patterns/event-bus/platform/persistence/connection.ts.tpl +8 -0
- package/templates/be/patterns/event-bus/platform/persistence/entities/event-outbox.entity.ts.tpl +37 -0
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.rows.ts.tpl +11 -0
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +12 -0
- package/templates/be/patterns/event-bus/platform/persistence/migrations/1789800009000-create-event-outbox.ts.tpl +26 -0
- package/templates/be/patterns/jobs/job.index.ts.tpl +1 -0
- package/templates/be/patterns/jobs/job.module.ts.tpl +6 -0
- package/templates/be/patterns/jobs/job.processor.ts.tpl +20 -0
- package/templates/be/patterns/jobs/job.queue.module.ts.tpl +20 -0
- package/templates/be/patterns/jobs/job.step.ts.tpl +19 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +31 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.ts.tpl +13 -0
- package/templates/be/patterns/jobs/platform/job-claim.service.spec.ts.tpl +150 -0
- package/templates/be/patterns/jobs/platform/job-claim.service.ts.tpl +100 -0
- package/templates/be/patterns/jobs/platform/job-runner.service.spec.ts.tpl +138 -0
- package/templates/be/patterns/jobs/platform/job-runner.service.ts.tpl +69 -0
- package/templates/be/patterns/jobs/platform/jobs.config.ts.tpl +11 -0
- package/templates/be/patterns/jobs/platform/jobs.contracts.ts.tpl +62 -0
- package/templates/be/patterns/jobs/platform/jobs.decorators.ts.tpl +31 -0
- package/templates/be/patterns/jobs/platform/jobs.error.ts.tpl +22 -0
- package/templates/be/patterns/jobs/platform/jobs.index.ts.tpl +11 -0
- package/templates/be/patterns/jobs/platform/jobs.log-events.ts.tpl +9 -0
- package/templates/be/patterns/jobs/platform/jobs.messages.ts.tpl +15 -0
- package/templates/be/patterns/jobs/platform/jobs.module-definition.ts.tpl +10 -0
- package/templates/be/patterns/jobs/platform/jobs.module.ts.tpl +32 -0
- package/templates/be/patterns/jobs/platform/jobs.options.ts.tpl +11 -0
- package/templates/be/patterns/jobs/platform/jobs.port.ts.tpl +36 -0
- package/templates/be/patterns/jobs/platform/persistence.connection.ts.tpl +8 -0
- package/templates/be/patterns/jobs/platform/persistence.job.entity.ts.tpl +53 -0
- package/templates/be/patterns/jobs/platform/persistence.jobs.rows.ts.tpl +13 -0
- package/templates/be/patterns/jobs/platform/persistence.jobs.sql.ts.tpl +33 -0
- package/templates/be/patterns/jobs/platform/persistence.migration.ts.tpl +29 -0
- package/templates/be/patterns/outbox/platform/index.ts.tpl +2 -0
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +96 -0
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +78 -0
- package/templates/be/patterns/outbox/platform/outbox.log-events.ts.tpl +5 -0
- package/templates/be/patterns/projections/migration.ts.tpl +18 -0
- package/templates/be/patterns/projections/persistence.connection.ts.tpl +8 -0
- package/templates/be/patterns/projections/projection.entity.ts.tpl +9 -0
- package/templates/be/patterns/projections/projection.index.ts.tpl +3 -0
- package/templates/be/patterns/projections/projection.module-definition.ts.tpl +7 -0
- package/templates/be/patterns/projections/projection.module.ts.tpl +18 -0
- package/templates/be/patterns/projections/projection.options.ts.tpl +2 -0
- package/templates/be/patterns/projections/projection.projection.spec.ts.tpl +55 -0
- package/templates/be/patterns/projections/projection.projection.ts.tpl +21 -0
- package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.spec.ts.tpl +142 -0
- package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.ts.tpl +81 -0
- package/templates/be/patterns/queues/platform/persistence.connection.ts.tpl +8 -0
- package/templates/be/patterns/queues/platform/persistence.migration.ts.tpl +25 -0
- package/templates/be/patterns/queues/platform/persistence.queue-outbox.entity.ts.tpl +25 -0
- package/templates/be/patterns/queues/platform/persistence.queue.rows.ts.tpl +9 -0
- package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +12 -0
- package/templates/be/patterns/queues/platform/queue-outbox.service.spec.ts.tpl +48 -0
- package/templates/be/patterns/queues/platform/queue-outbox.service.ts.tpl +17 -0
- package/templates/be/patterns/queues/platform/queue-relay.service.spec.ts.tpl +128 -0
- package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +48 -0
- package/templates/be/patterns/queues/platform/queue-transport.port.ts.tpl +115 -0
- package/templates/be/patterns/queues/platform/queue-worker.service.spec.ts.tpl +91 -0
- package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +45 -0
- package/templates/be/patterns/queues/platform/queue.config.ts.tpl +15 -0
- package/templates/be/patterns/queues/platform/queue.contracts.ts.tpl +31 -0
- package/templates/be/patterns/queues/platform/queue.decorators.ts.tpl +45 -0
- package/templates/be/patterns/queues/platform/queue.index.ts.tpl +7 -0
- package/templates/be/patterns/queues/platform/queue.log-events.ts.tpl +5 -0
- package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +10 -0
- package/templates/be/patterns/queues/platform/queue.module.ts.tpl +62 -0
- package/templates/be/patterns/queues/platform/queue.options.ts.tpl +22 -0
- package/templates/be/patterns/queues/platform/queue.policy.spec.ts.tpl +19 -0
- package/templates/be/patterns/queues/platform/queue.policy.ts.tpl +11 -0
- package/templates/be/patterns/queues/platform/queue.port.ts.tpl +14 -0
- package/templates/be/patterns/queues/queue.index.ts.tpl +3 -0
- package/templates/be/patterns/queues/queue.module.ts.tpl +6 -0
- package/templates/be/patterns/queues/queue.queue.ts.tpl +23 -0
- package/templates/be/patterns/reactors/application.command.ts.tpl +10 -0
- package/templates/be/patterns/reactors/application.contracts.ts.tpl +12 -0
- package/templates/be/patterns/reactors/application.handler.ts.tpl +22 -0
- package/templates/be/patterns/reactors/consumer.ts.tpl +19 -0
- package/templates/be/patterns/reactors/message.module.ts.tpl +20 -0
- package/templates/be/patterns/reactors/reactor.index.ts.tpl +1 -0
- package/templates/be/patterns/reactors/reactor.module.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.index.ts.tpl +1 -0
- package/templates/be/patterns/realtime/channel.input.ts.tpl +12 -0
- package/templates/be/patterns/realtime/channel.subscription.spec.ts.tpl +51 -0
- package/templates/be/patterns/realtime/channel.subscription.ts.tpl +32 -0
- package/templates/be/patterns/realtime/channel.type.ts.tpl +9 -0
- package/templates/be/patterns/saga/saga.compensate.command.ts.tpl +11 -0
- package/templates/be/patterns/saga/saga.compensate.contracts.ts.tpl +7 -0
- package/templates/be/patterns/saga/saga.compensate.handler.ts.tpl +22 -0
- package/templates/be/patterns/saga/saga.compensation.ts.tpl +21 -0
- package/templates/be/patterns/saga/saga.complete.command.ts.tpl +11 -0
- package/templates/be/patterns/saga/saga.complete.contracts.ts.tpl +7 -0
- package/templates/be/patterns/saga/saga.complete.handler.ts.tpl +22 -0
- package/templates/be/patterns/saga/saga.done.consumer.ts.tpl +21 -0
- package/templates/be/patterns/saga/saga.failed.consumer.ts.tpl +21 -0
- package/templates/be/patterns/saga/saga.index.ts.tpl +1 -0
- package/templates/be/patterns/saga/saga.message.module.ts.tpl +23 -0
- package/templates/be/patterns/saga/saga.module.ts.tpl +20 -0
- package/templates/be/patterns/saga/saga.service.spec.ts.tpl +70 -0
- package/templates/be/patterns/saga/saga.service.ts.tpl +44 -0
- package/templates/be/patterns/saga/saga.state.ts.tpl +14 -0
- package/templates/be/patterns/saga/saga.step.ts.tpl +11 -0
- package/templates/be/patterns/saga/saga.undo.command.ts.tpl +10 -0
- package/templates/be/patterns/saga/saga.undo.contracts.ts.tpl +5 -0
- package/templates/be/patterns/saga/saga.undo.handler.ts.tpl +21 -0
- package/templates/be/patterns/webhooks/webhook.door.spec.ts.tpl +72 -0
- package/templates/be/patterns/webhooks/webhook.door.ts.tpl +36 -0
- package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +6 -0
- package/templates/be/patterns/webhooks/webhook.index.ts.tpl +1 -0
- package/templates/be/patterns/webhooks/webhook.request.ts.tpl +9 -0
- package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +3 -0
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +15 -2
- package/templates/be/skeleton/apps/__app__/src/main.ts +7 -15
- package/templates/be/skeleton/apps/cli/src/app.module.ts +30 -0
- package/templates/be/skeleton/apps/cli/src/cli.options.ts +22 -0
- package/templates/be/skeleton/apps/cli/src/main.ts +24 -0
- package/templates/be/skeleton/src/features/cli/cli.module.ts +7 -0
- package/templates/be/skeleton/src/features/cli/index.ts +1 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +17 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +15 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +7 -0
- package/templates/be/skeleton/src/features/cli/migrate/subs/run.cli.spec.ts +17 -0
- package/templates/be/skeleton/src/features/cli/migrate/subs/run.cli.ts +18 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +17 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +15 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +7 -0
- package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +33 -0
- package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.ts +34 -0
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +13 -0
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +40 -0
- package/templates/be/skeleton/src/modules/domain/identity/messages/identity.messages.ts +1 -1
- package/templates/be/skeleton/src/modules/domain/note/errors/note.error.ts +16 -0
- package/templates/be/skeleton/src/modules/domain/note/index.ts +3 -0
- package/templates/be/skeleton/src/modules/domain/note/note.contracts.ts +19 -0
- package/templates/be/skeleton/src/modules/domain/note/note.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/domain/note/note.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/domain/note/note.module.ts +19 -0
- package/templates/be/skeleton/src/modules/domain/note/note.options.ts +2 -0
- package/templates/be/skeleton/src/modules/domain/note/note.service.spec.ts +76 -0
- package/templates/be/skeleton/src/modules/domain/note/note.service.ts +37 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/connection.ts +8 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/entities/note.entity.ts +17 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/migrations/1790000000000-init-note.ts +22 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/note.rows.ts +17 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/note.sql.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +20 -0
- package/templates/be/skeleton/src/modules/platform/config/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.contracts.ts +18 -0
- package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +41 -0
- package/templates/be/skeleton/src/modules/platform/database/connection-source.client.ts +17 -0
- package/templates/be/skeleton/src/modules/platform/database/database.module-definition.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/database/database.module.ts +77 -0
- package/templates/be/skeleton/src/modules/platform/database/database.options.ts +30 -0
- package/templates/be/skeleton/src/modules/platform/database/database.port.ts +65 -0
- package/templates/be/skeleton/src/modules/platform/database/database.sql.ts +47 -0
- package/templates/be/skeleton/src/modules/platform/database/errors/database.error.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/database/index.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +30 -0
- package/templates/be/skeleton/src/modules/platform/database/migration-runner.service.spec.ts +84 -0
- package/templates/be/skeleton/src/modules/platform/database/migration-runner.service.ts +23 -0
- package/templates/be/skeleton/src/modules/platform/database/primary.config.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/database/primary.connection.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +10 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +108 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +75 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-runner.service.spec.ts +88 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-runner.service.ts +29 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +48 -0
- package/templates/be/skeleton/src/modules/platform/errors/messages/errors.messages.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +12 -0
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +8 -1
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.decorators.ts +8 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +14 -1
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +12 -0
- package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +10 -2
- package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.spec.ts +63 -0
- package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.ts +2 -2
- package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +108 -0
- package/templates/be/skeleton/src/modules/platform/http-security/request-validation.service.spec.ts +39 -0
- package/templates/be/skeleton/src/modules/platform/http-security/request-validation.service.ts +25 -0
- package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +172 -0
- package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.ts +66 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +3 -1
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +52 -0
- package/templates/be/skeleton-lite/apps/api/Dockerfile +37 -0
- package/templates/be/skeleton-lite/apps/api/src/api.options.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/src/app.module.ts +81 -0
- package/templates/be/skeleton-lite/apps/api/src/main.ts +34 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +14 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +50 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +26 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +19 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +26 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +7 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +6 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +19 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/index.ts +10 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.ts +24 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.config.ts +8 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.decorators.ts +17 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.jwks.ts +32 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.options.ts +24 -0
- package/templates/be/skeleton-lite/src/modules/platform/cqrs/index.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +54 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.options.ts +23 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +10 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.sql.ts +49 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/migration-runner.service.ts +21 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/primary.config.ts +11 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/seed-runner.service.ts +28 -0
- package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +25 -0
- package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +9 -0
- package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +5 -0
- package/templates/be/table/error.ts.tpl +16 -0
- package/templates/be/table/index.ts.tpl +2 -0
- package/templates/be/table/migration.sql.tpl +51 -0
- package/templates/be/table/module-definition.ts.tpl +7 -0
- package/templates/be/table/module.ts.tpl +14 -0
- package/templates/be/table/options.ts.tpl +2 -0
- package/templates/be/table/rows.ts.tpl +4 -0
- package/templates/be/table/service.ts.tpl +23 -0
- package/templates/be/table/sql.ts.tpl +4 -0
- package/templates/be/tool-config/jest.config.js +3 -1
- package/templates/be/upgrade-full/apps/cli/src/app.module.ts +14 -0
- package/templates/be/upgrade-full/apps/cli/src/main.ts +26 -0
- package/templates/be/upgrade-full/src/features/cli/cli.module.ts +6 -0
- package/templates/be/upgrade-full/src/features/cli/migrate/subs/run.cli.ts +23 -0
- package/templates/common/gitignore.base +1 -2
- package/templates/fe/app/package.json.tpl +1 -0
- package/templates/fe/app/tsconfig.json +12 -0
- package/templates/fe/common/app-layout.tsx.partial +8 -0
- package/templates/fe/common/site-shell.tsx.partial +9 -0
- package/templates/fe/image/next/Dockerfile +42 -0
- package/templates/fe/skeleton/apps/app/next.config.ts +23 -0
- package/templates/fe/skeleton/apps/app/postcss.config.mjs +7 -0
- package/templates/fe/skeleton/apps/{__app__ → app}/src/app/[locale]/error.tsx +3 -3
- package/templates/fe/skeleton/apps/app/src/app/[locale]/layout.tsx +22 -0
- package/templates/fe/skeleton/apps/{__app__ → app}/src/app/[locale]/loading.tsx +2 -2
- package/templates/fe/skeleton/apps/{__app__ → app}/src/app/[locale]/not-found.tsx +2 -2
- package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +9 -0
- package/templates/fe/skeleton/apps/{__app__ → app}/src/app/global-error.tsx +2 -2
- package/templates/fe/skeleton/apps/app/src/app/globals.css +10 -0
- package/templates/fe/skeleton/apps/app/src/features/layouts/AppLayout/index.tsx +18 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppErrorPage/component.tsx +19 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppErrorPage/index.tsx +11 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppGlobalErrorPage/index.tsx +16 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/component.tsx +10 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +15 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppLoadingPage/index.tsx +8 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppNotFoundPage/index.tsx +9 -0
- package/templates/fe/skeleton/apps/app/src/modules/i18n/index.ts +4 -0
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +28 -0
- package/templates/fe/skeleton/apps/app/src/modules/i18n/request.ts +4 -0
- package/templates/fe/skeleton/apps/app/src/modules/routes/index.ts +4 -0
- package/templates/fe/skeleton/apps/app/src/proxy.ts +1 -0
- package/templates/fe/skeleton/apps/landing/next.config.ts +21 -0
- package/templates/fe/skeleton/apps/landing/postcss.config.mjs +7 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/error.tsx +10 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/layout.tsx +22 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/loading.tsx +6 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/not-found.tsx +6 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +9 -0
- package/templates/fe/skeleton/apps/landing/src/app/global-error.tsx +10 -0
- package/templates/fe/skeleton/apps/landing/src/app/globals.css +10 -0
- package/templates/fe/skeleton/apps/landing/src/features/layouts/LandingLayout/index.tsx +24 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingErrorPage/component.tsx +18 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingErrorPage/index.tsx +11 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingGlobalErrorPage/index.tsx +16 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingLoadingPage/index.tsx +8 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingNotFoundPage/index.tsx +9 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingPage/component.tsx +24 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingPage/index.tsx +16 -0
- package/templates/fe/skeleton/apps/landing/src/modules/config/index.ts +12 -0
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/index.ts +7 -0
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/vi.json +31 -0
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/request.ts +4 -0
- package/templates/fe/skeleton/apps/landing/src/modules/routes/index.ts +4 -0
- package/templates/fe/skeleton/apps/landing/src/proxy.ts +1 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +49 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/proxy.ts +24 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +20 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/branches/Providers/index.tsx +23 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/ErrorNotice/index.tsx +14 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/GlobalErrorNotice/index.tsx +25 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/LoadingNotice/index.tsx +9 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/LocaleShell/index.tsx +21 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/NotFoundNotice/index.tsx +19 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/SiteShell/classNames.ts +4 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/SiteShell/index.tsx +19 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +6 -0
- package/templates/fe/skeleton-lite/apps/web/next.config.ts +22 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/error.tsx +10 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +37 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/loading.tsx +6 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/not-found.tsx +6 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +9 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/providers.tsx +26 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/auth/callback/route.ts +24 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/global-error.tsx +10 -0
- package/templates/fe/{skeleton/apps/__app__ → skeleton-lite/apps/web}/src/app/globals.css +1 -1
- package/templates/fe/skeleton-lite/apps/web/src/app/health/live/route.ts +5 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/component.tsx +29 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +41 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignOutButton/component.tsx +17 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignOutButton/index.tsx +38 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/ErrorNotice/index.tsx +13 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/GlobalErrorNotice/index.tsx +24 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/LoadingNotice/index.tsx +7 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/NotFoundNotice/index.tsx +18 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/SiteShell/classNames.ts +4 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/SiteShell/index.tsx +13 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/layouts/AppLayout/index.tsx +31 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppErrorPage/component.tsx +12 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppErrorPage/index.tsx +10 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppGlobalErrorPage/index.tsx +15 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/component.tsx +14 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +30 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppLoadingPage/index.tsx +8 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppNotFoundPage/index.tsx +9 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/brand/brand.css +20 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/brand/index.ts +2 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/config/index.ts +20 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/read-session.ts +25 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-in.ts +25 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +17 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/browser.ts +14 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/index.ts +3 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/outcome.ts +59 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/principal.ts +29 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/server.ts +70 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +50 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/index.ts +8 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +48 -0
- package/templates/fe/{skeleton/apps/__app__ → skeleton-lite/apps/web}/src/modules/i18n/request.ts +4 -2
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +14 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/routes/index.ts +7 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/routes/safe-next-path.ts +26 -0
- package/templates/fe/skeleton-lite/apps/web/src/proxy.ts +43 -0
- package/templates/fe/table/read.ts.tpl +16 -0
- package/templates/fe/table/write.ts.tpl +16 -0
- package/upgrade/check.mjs +137 -0
- package/upgrade/index.mjs +524 -0
- package/bin/hfs.mjs +0 -233
- package/runtime/engine/admission.mjs +0 -284
- package/runtime/engine/digest.mjs +0 -10
- package/runtime/engine/ledger-db.mjs +0 -1245
- package/runtime/engine/machine-db.mjs +0 -1565
- package/runtime/engine/migrations/machine/0001-init.sql +0 -887
- package/runtime/engine/migrations/machine/0002-worktrees-no-workflow-kind.sql +0 -13
- package/runtime/engine/migrations/machine/0003-worktrees-workflow-orca.sql +0 -21
- package/runtime/engine/migrations/runtime/0001-init.sql +0 -1072
- package/runtime/engine/migrations/runtime/0003-usage-unavailable.sql +0 -13
- package/runtime/engine/migrations/runtime/0004-attempt-why.sql +0 -43
- package/runtime/engine/migrations/runtime/0005-ended-workflow-views.sql +0 -93
- package/runtime/scripts/api/fs/rmdir-link.mjs +0 -12
- package/runtime/scripts/lib/artifact-hold.mjs +0 -89
- package/runtime/scripts/lib/artifact-store.mjs +0 -103
- package/runtime/scripts/lib/redact.mjs +0 -148
- package/runtime/scripts/lib/safe-remove.mjs +0 -275
- package/runtime/scripts/lib/test-secrets.mjs +0 -116
- package/templates/fe/skeleton/apps/__app__/next.config.ts +0 -11
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/layout.tsx +0 -20
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/page.tsx +0 -9
- package/templates/fe/skeleton/apps/__app__/src/components/composites/FailureScreen/index.tsx +0 -28
- package/templates/fe/skeleton/apps/__app__/src/features/layouts/LocaleShell/index.tsx +0 -35
- package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/component.tsx +0 -21
- package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/index.tsx +0 -17
- package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/component.tsx +0 -26
- package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/index.tsx +0 -15
- package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/component.tsx +0 -25
- package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/index.tsx +0 -15
- package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/component.tsx +0 -27
- package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/index.tsx +0 -8
- package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/component.tsx +0 -32
- package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/index.tsx +0 -8
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/config.ts +0 -8
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/index.ts +0 -2
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages/vi.json +0 -25
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/routing.ts +0 -9
- package/templates/fe/skeleton/apps/__app__/src/modules/routes/index.ts +0 -4
- package/templates/fe/skeleton/apps/__app__/src/proxy.ts +0 -10
- /package/runtime/scripts/{checks → hfs}/architecture/config-unread.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/constructor-deps.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/cross-app-duplicate.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/default-deny.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/error-codes.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/error-masked.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/hooks-are-hooks.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/injection-token-exported.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/package-shape.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/reachability.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/route-files-thin.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/sql-tokens.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/symbols.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/unit-spec-providers.mjs +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/application/check-liveness.contracts.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/application/check-liveness.handler.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/application/check-liveness.query.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/index.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/system-health.module.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/transport/http/live.controller.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/transport/http/system-health-http.module.ts +0 -0
- /package/templates/fe/skeleton/apps/{__app__ → app}/src/app/health/live/route.ts +0 -0
- /package/templates/fe/skeleton/apps/{__app__ → app}/src/modules/config/index.ts +0 -0
- /package/templates/fe/{skeleton/apps/__app__ → skeleton-lite/apps/web}/postcss.config.mjs +0 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// cli.mjs - the one command line of a back end (R147 BE_CLI_REQUIRED).
|
|
2
|
+
// BE_CLI_REQUIRED a back end that declares a connection (it has migrations to run) or tracks a command (src/features/cli/)
|
|
3
|
+
// declares exactly one app of kind cli, named cli (be/apps/cli), and tracks its image be/apps/cli/Dockerfile:
|
|
4
|
+
// one image, run as `cli <group> <command>`. A cli app with another name, or a second one, is refused.
|
|
5
|
+
// Where a command is declared and that every command has its unit spec beside it are eslint-be's (cli-command-shape, and
|
|
6
|
+
// unit-test-colocated over ruleParams.be.unitRoles).
|
|
7
|
+
// Paths are app-relative; the cli app and the cli feature root are read from the slot manifest through the resolver.
|
|
8
|
+
import { found } from './read.mjs';
|
|
9
|
+
|
|
10
|
+
const CLI_REQUIRED = 'BE_CLI_REQUIRED';
|
|
11
|
+
|
|
12
|
+
/** The one name of the cli app, its kind, and what its image is called below it. */
|
|
13
|
+
export const CLI_APP = 'cli';
|
|
14
|
+
const CLI_KIND = 'cli';
|
|
15
|
+
const IMAGE = 'Dockerfile';
|
|
16
|
+
const CLI_FEATURE_SLOT = 'be.cli';
|
|
17
|
+
const CLI_APP_SLOT = 'be.app.cli';
|
|
18
|
+
|
|
19
|
+
/** The findings of R147 over the tracked paths `files` (app-relative) of the app `repo`; `resolver` is the app's. */
|
|
20
|
+
export function cliFindings({ files, repo, resolver }) {
|
|
21
|
+
if (!repo?.sides) return [];
|
|
22
|
+
const be = repo.sides.be;
|
|
23
|
+
const beResolver = resolver.sides?.be ?? resolver;
|
|
24
|
+
const onBe = files.filter((file) => file.startsWith('be/')).map((file) => file.slice('be/'.length));
|
|
25
|
+
const tracked = new Set(onBe);
|
|
26
|
+
const commandFiles = onBe.filter((file) => beResolver.classifyPath(file).slot === CLI_FEATURE_SLOT);
|
|
27
|
+
const findings = [];
|
|
28
|
+
const cliApps = be.apps.filter((app) => app.kind === CLI_KIND);
|
|
29
|
+
// Where the cli app is only optional (litePresence: schema migration is the Supabase CLI's job) a connection no longer forces it; a tracked command still does.
|
|
30
|
+
const migrates = (be.connections ?? []).length > 0 && beResolver.slot(CLI_APP_SLOT)?.presence !== 'optional';
|
|
31
|
+
const needs = migrates || commandFiles.length > 0;
|
|
32
|
+
const why = migrates ? `it declares the connection${be.connections.length === 1 ? '' : 's'} ${be.connections.map((c) => c.name).join(', ')} (their migrations run as \`cli migrate run\`)` : 'it tracks commands under be/src/features/cli/';
|
|
33
|
+
if (needs && cliApps.length === 0) findings.push(found(CLI_REQUIRED, 'hfs.json', `the back end declares no cli app, but ${why}; declare { "name": "${CLI_APP}", "kind": "${CLI_KIND}" } in sides.be.apps and build be/apps/${CLI_APP} on nest-commander: every one-off action of the back end is one of its commands.`, { needs: why }));
|
|
34
|
+
for (const app of cliApps.filter((entry) => entry.name !== CLI_APP)) findings.push(found(CLI_REQUIRED, 'hfs.json', `the cli app is named ${app.name}; the back end has ONE cli app, be/apps/${CLI_APP}, so its image and its command line are the same in every app.`, { app: app.name }));
|
|
35
|
+
if (cliApps.length > 0 && !tracked.has(`apps/${CLI_APP}/${IMAGE}`) && cliApps.some((app) => app.name === CLI_APP)) findings.push(found(CLI_REQUIRED, `be/apps/${CLI_APP}/${IMAGE}`, `be/apps/${CLI_APP} has no image: track be/apps/${CLI_APP}/${IMAGE}, the one cli image every one-off action runs from (\`cli <group> <command>\`).`, { app: CLI_APP }));
|
|
36
|
+
return findings;
|
|
37
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// contract-compat.mjs - event contracts evolve only additively (BE_CONTRACT_BREAKING, R177).
|
|
2
|
+
//
|
|
3
|
+
// A service publishes its events in `be/contracts/<service>/events.json` (the vendored snapshot hfs emits from the service's event table).
|
|
4
|
+
// The previous published contract is pinned beside it as `be/contracts/<service>/events.pin.json` (the same format, copied when a contract is
|
|
5
|
+
// published). The current snapshot is compared with its pin, event by event:
|
|
6
|
+
// - every pinned event still exists under the same key, with the same `stream` and `version`;
|
|
7
|
+
// - every pinned payload field still exists with the same type and the same optionality (`<type>` or `<type>?`);
|
|
8
|
+
// - a field that is new is optional (events already on the wire do not carry it);
|
|
9
|
+
// - any other member the pinned event declared (for example `compensates`) is unchanged.
|
|
10
|
+
// Anything else is a breaking change: it needs a NEW event key `<event>.v2` next to the old one, which stays until its consumers have moved.
|
|
11
|
+
// A new event, and a new optional field, are additive and pass. A service with no pin is not judged (the pin is opt-in per service).
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { isPlainObject } from '../../../engine/plain-object.mjs';
|
|
14
|
+
import { found, readJson } from './read.mjs';
|
|
15
|
+
|
|
16
|
+
export const CONTRACT_BREAKING = 'BE_CONTRACT_BREAKING';
|
|
17
|
+
|
|
18
|
+
const CONTRACTS = /^be\/contracts\/([^/]+)\/events\.pin\.json$/;
|
|
19
|
+
|
|
20
|
+
const same = (a, b) => JSON.stringify(sortKeys(a)) === JSON.stringify(sortKeys(b));
|
|
21
|
+
function sortKeys(value) {
|
|
22
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
23
|
+
if (!isPlainObject(value)) return value;
|
|
24
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortKeys(value[key])]));
|
|
25
|
+
}
|
|
26
|
+
/** {type, optional} of a payload field spelling (`string`, `number?`, `string[]`). */
|
|
27
|
+
const fieldOf = (spelling) => (typeof spelling === 'string' && spelling.endsWith('?') ? { type: spelling.slice(0, -1), optional: true } : { type: spelling, optional: false });
|
|
28
|
+
|
|
29
|
+
/** The breaking changes of `current` against `pinned`: [{event, message}]. */
|
|
30
|
+
export function breakingChanges(pinned, current) {
|
|
31
|
+
const problems = [];
|
|
32
|
+
const pinnedEvents = isPlainObject(pinned?.events) ? pinned.events : {};
|
|
33
|
+
const currentEvents = isPlainObject(current?.events) ? current.events : {};
|
|
34
|
+
for (const [name, before] of Object.entries(pinnedEvents)) {
|
|
35
|
+
const after = currentEvents[name];
|
|
36
|
+
if (!isPlainObject(after)) { problems.push({ event: name, message: `event ${name} was removed; a published event is never deleted while it can still be on the wire. Keep it, and add ${name}.v2 for the new shape.` }); continue; }
|
|
37
|
+
for (const key of Object.keys(before)) {
|
|
38
|
+
if (key === 'payload') continue;
|
|
39
|
+
if (!same(before[key], after[key])) problems.push({ event: name, message: `event ${name} changed its ${key} from ${JSON.stringify(before[key])} to ${JSON.stringify(after[key] ?? null)}; a published event keeps its ${key}. Add ${name}.v2 for the new shape.` });
|
|
40
|
+
}
|
|
41
|
+
const was = isPlainObject(before.payload) ? before.payload : {};
|
|
42
|
+
const now = isPlainObject(after.payload) ? after.payload : {};
|
|
43
|
+
for (const [field, spelling] of Object.entries(was)) {
|
|
44
|
+
if (!(field in now)) { problems.push({ event: name, message: `event ${name} lost payload field ${field}; fields are only ever added. Keep it, and add ${name}.v2 for the new shape.` }); continue; }
|
|
45
|
+
const a = fieldOf(spelling);
|
|
46
|
+
const b = fieldOf(now[field]);
|
|
47
|
+
if (a.type !== b.type) problems.push({ event: name, message: `event ${name} changed payload field ${field} from ${a.type} to ${b.type}; a field keeps its type. Add ${name}.v2 for the new shape.` });
|
|
48
|
+
else if (a.optional !== b.optional) problems.push({ event: name, message: `event ${name} changed payload field ${field} from ${a.optional ? 'optional' : 'required'} to ${b.optional ? 'optional' : 'required'}; a field keeps its optionality. Add ${name}.v2 for the new shape.` });
|
|
49
|
+
}
|
|
50
|
+
for (const [field, spelling] of Object.entries(now)) {
|
|
51
|
+
if (!(field in was) && !fieldOf(spelling).optional) problems.push({ event: name, message: `event ${name} gained required payload field ${field}; events already on the wire do not carry it, so a new field is optional (${field}?). Make it optional, or add ${name}.v2.` });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return problems;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** R177: the findings of every service whose contract is pinned. `files` are the repository-relative tracked paths. */
|
|
58
|
+
export function contractCompatFindings({ repoRoot, files }) {
|
|
59
|
+
const findings = [];
|
|
60
|
+
for (const rel of [...files].sort()) {
|
|
61
|
+
const match = CONTRACTS.exec(rel);
|
|
62
|
+
if (!match) continue;
|
|
63
|
+
const current = `be/contracts/${match[1]}/events.json`;
|
|
64
|
+
const pinned = readJson(repoRoot, rel);
|
|
65
|
+
const snapshot = readJson(repoRoot, current);
|
|
66
|
+
if (!isPlainObject(pinned) || !isPlainObject(pinned.events)) { findings.push(found(CONTRACT_BREAKING, rel, `${rel} is not a readable event contract (an object with an events table); copy a published ${path.posix.basename(current)} over it.`)); continue; }
|
|
67
|
+
if (!isPlainObject(snapshot) || !isPlainObject(snapshot.events)) { findings.push(found(CONTRACT_BREAKING, current, `${current} is missing or unreadable while ${rel} pins the contract of service ${match[1]}; the contract of a published service is never dropped. Emit it again.`)); continue; }
|
|
68
|
+
for (const { event, message } of breakingChanges(pinned, snapshot)) findings.push(found(CONTRACT_BREAKING, current, `${current}: ${message}`, { event, service: match[1] }));
|
|
69
|
+
}
|
|
70
|
+
return findings;
|
|
71
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// a be side with a GraphQL transport commits `contracts/<app>/schema.graphql` for each api app, and an api app with an operation table
|
|
3
3
|
// commits `contracts/<app>/openapi.json`. The fe side of the same app reads those snapshots where they are (`be/contracts/`, the one
|
|
4
4
|
// cross-side read of hfs.json sides.fe.reads) for its codegen: there is no copy to keep equal.
|
|
5
|
-
// The snapshots are generated by `npm run contract:emit` (`
|
|
5
|
+
// The snapshots are generated by `npm run contract:emit` (`starci app emit`, packages/hfs/emit/contracts.mjs): printSchema of the
|
|
6
6
|
// resolvers the app root composes, and the OpenAPI 3.1 document of the typed operation table `apps/<app>/src/operations.ts`.
|
|
7
7
|
// That the committed snapshot equals what the app emits now is contractEmitFindings (full pass only, never --fast): it emits every
|
|
8
8
|
// api app into a temp directory through the injected `emit` (packages/hfs/emit) and compares hashes; an emit that cannot run is a finding.
|
|
@@ -10,20 +10,20 @@ import { createHash } from 'node:crypto';
|
|
|
10
10
|
import fs from 'node:fs';
|
|
11
11
|
import os from 'node:os';
|
|
12
12
|
import path from 'node:path';
|
|
13
|
-
import { found } from './read.mjs';
|
|
14
|
-
import {
|
|
13
|
+
import { found, readText } from './read.mjs';
|
|
14
|
+
import { safeRemove } from '../../api/fs/safe-remove.mjs';
|
|
15
15
|
|
|
16
16
|
export const CONTRACT_SNAPSHOT_DRIFT = 'HFS_CONTRACT_SNAPSHOT_DRIFT';
|
|
17
|
-
|
|
17
|
+
const GRAPHQL_TRANSPORT_SLOT = 'be.transport.graphql';
|
|
18
18
|
const SNAPSHOT_OF = { graphql: 'schema.graphql', json: 'openapi.json' };
|
|
19
19
|
|
|
20
20
|
/** sha256 of a file's text with line endings folded, or null when it cannot be read. */
|
|
21
|
-
|
|
21
|
+
function contractHash(file) {
|
|
22
22
|
try { return createHash('sha256').update(fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n')).digest('hex'); } catch { return null; }
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/** The findings of R23 for the be side at `repoRoot` (a side of another profile has none). */
|
|
26
|
-
export function contractFindings({ files, repo, resolver }) {
|
|
26
|
+
export function contractFindings({ repoRoot, files, repo, resolver }) {
|
|
27
27
|
const findings = [];
|
|
28
28
|
if (repo.profile !== 'be') return findings;
|
|
29
29
|
const serves = files.some((file) => resolver.classifyPath(file).slot === GRAPHQL_TRANSPORT_SLOT);
|
|
@@ -31,7 +31,8 @@ export function contractFindings({ files, repo, resolver }) {
|
|
|
31
31
|
for (const app of repo.apps.filter((a) => a.kind === 'api')) {
|
|
32
32
|
const operations = `contracts/${app.name}/${SNAPSHOT_OF.json}`;
|
|
33
33
|
if (tracked.has(`apps/${app.name}/src/operations.ts`) && !tracked.has(operations)) findings.push(found(CONTRACT_SNAPSHOT_DRIFT, operations, `${app.name} declares an operation table (apps/${app.name}/src/operations.ts) but ${operations} is not committed; declare be.contract.openapi in hfs.json sides.be.optionalSlots, run \`npm run contract:emit\` and commit the snapshot`, { app: app.name }));
|
|
34
|
-
|
|
34
|
+
// An app serves GraphQL when its root module composes the platform graphql capability; a repository with a GraphQL transport slot does not make every api app one.
|
|
35
|
+
if (!serves || !(readText(repoRoot, `apps/${app.name}/src/app.module.ts`) ?? '').includes('platform/graphql')) continue;
|
|
35
36
|
const snapshot = `contracts/${app.name}/${SNAPSHOT_OF.graphql}`;
|
|
36
37
|
if (!tracked.has(snapshot)) findings.push(found(CONTRACT_SNAPSHOT_DRIFT, snapshot, `${app.name} serves GraphQL but ${snapshot} is not committed; declare be.contract.graphql in hfs.json sides.be.optionalSlots, run \`npm run contract:emit\` and commit the snapshot`, { app: app.name }));
|
|
37
38
|
}
|
|
@@ -65,7 +66,7 @@ export function contractEmitFindings({ repoRoot, files, repo, emit }) {
|
|
|
65
66
|
} catch (error) {
|
|
66
67
|
for (const { artifact } of artifacts) apps.push({ app: app.name, artifact, status: 'emit-failed' });
|
|
67
68
|
const snapshot = `contracts/${app.name}/${SNAPSHOT_OF.graphql}`;
|
|
68
|
-
findings.push(found(CONTRACT_SNAPSHOT_DRIFT, snapshot, `contracts/${app.name} cannot be verified: \`
|
|
69
|
+
findings.push(found(CONTRACT_SNAPSHOT_DRIFT, snapshot, `contracts/${app.name} cannot be verified: \`starci app emit\` failed for ${app.name}: ${errorText(error)}`, { app: app.name, drift: 'emit-failed' }));
|
|
69
70
|
continue;
|
|
70
71
|
}
|
|
71
72
|
for (const { artifact, what } of artifacts) {
|
|
@@ -93,7 +94,8 @@ export function contractEmitFindings({ repoRoot, files, repo, emit }) {
|
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
} finally {
|
|
96
|
-
|
|
97
|
+
// A temp directory this rule made itself, in no ledger's repository: no indexed job artifact can live in it.
|
|
98
|
+
safeRemove(scratch, { hold: () => null });
|
|
97
99
|
}
|
|
98
100
|
return { findings, apps };
|
|
99
101
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// database-config.mjs - L08 Supabase TOML policy and L09 generated-type drift checks. Parsing and type emission stay
|
|
2
|
+
// lazy/injected so applications without Supabase do not load either tool and this rule starts no Docker process.
|
|
3
|
+
import { sameText } from '../../lib/same-text.mjs';
|
|
4
|
+
import { found, readText } from './read.mjs';
|
|
5
|
+
import { DB_CONFIG_POLICY, DB_TYPES_DRIFT, TYPES_FILE } from './database-constants.mjs';
|
|
6
|
+
const ENV_REF = /^env\([A-Za-z_][A-Za-z0-9_]*\)$/;
|
|
7
|
+
const PUBLIC_KEYS = new Set(['anon_key', 'publishable_key', 'public_key']);
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
function tomlEntries(value, path = []) {
|
|
11
|
+
if (!value || typeof value !== 'object') return [];
|
|
12
|
+
if (Array.isArray(value)) return value.flatMap((item) => tomlEntries(item, path));
|
|
13
|
+
return Object.entries(value).flatMap(([key, child]) => {
|
|
14
|
+
const at = [...path, key];
|
|
15
|
+
if (child && typeof child === 'object') {
|
|
16
|
+
if (Array.isArray(child)) {
|
|
17
|
+
if (child.every((item) => item && typeof item === 'object' && !Array.isArray(item))) return child.flatMap((item) => tomlEntries(item, at));
|
|
18
|
+
} else return tomlEntries(child, at);
|
|
19
|
+
}
|
|
20
|
+
return [[at.join('.'), key, child]];
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const isCredentialKey = (key) => {
|
|
25
|
+
const leaf = key.toLowerCase();
|
|
26
|
+
return ['secret', 'client_id', 'key', 'pass', 'password', 'token'].includes(leaf)
|
|
27
|
+
|| /_(secret|password|token|client_id|api_key)$/.test(leaf)
|
|
28
|
+
|| (leaf.endsWith('_key') && !PUBLIC_KEYS.has(leaf));
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const tomlLine = (text, key) => {
|
|
32
|
+
const expression = new RegExp(`^\\s*["']?${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}["']?\\s*=`, 'm');
|
|
33
|
+
const match = expression.exec(text);
|
|
34
|
+
return match ? text.slice(0, match.index).split('\n').length : undefined;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** The parsed TOML of `text`, or null when it does not parse. */
|
|
38
|
+
export async function parseToml(text) {
|
|
39
|
+
try {
|
|
40
|
+
const module = await import('smol-toml');
|
|
41
|
+
return (module.default?.parse ?? module.parse)(text);
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** L08 policy findings in one already-parsed supabase/config.toml. */
|
|
48
|
+
export function configFindings({ file, text, toml, supabase }) {
|
|
49
|
+
const findings = [];
|
|
50
|
+
for (const [at, key, value] of tomlEntries(toml)) {
|
|
51
|
+
if (!isCredentialKey(key)) continue;
|
|
52
|
+
const line = tomlLine(text, key);
|
|
53
|
+
if (typeof value !== 'string' || !ENV_REF.test(value)) {
|
|
54
|
+
findings.push(found(DB_CONFIG_POLICY, file, `${file}${line ? `:${line}` : ''} ${at} holds a literal credential; a secret in config.toml is written env(NAME), never a value`, { ...(line ? { line } : {}), key: at }));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const auth = toml?.auth ?? {};
|
|
58
|
+
if (auth.jwt_expiry !== undefined && (typeof auth.jwt_expiry !== 'number' || auth.jwt_expiry > 3600)) {
|
|
59
|
+
findings.push(found(DB_CONFIG_POLICY, file, `${file} auth.jwt_expiry is ${JSON.stringify(auth.jwt_expiry)}; the access token lives at most 3600 seconds`, { key: 'auth.jwt_expiry' }));
|
|
60
|
+
}
|
|
61
|
+
if (!supabase) return findings;
|
|
62
|
+
const declared = {
|
|
63
|
+
enableSignup: supabase.enableSignup, jwtExpiry: supabase.jwtExpiry, siteUrl: supabase.siteUrl,
|
|
64
|
+
redirectUrls: Array.isArray(supabase.redirectUrls) ? supabase.redirectUrls : undefined,
|
|
65
|
+
};
|
|
66
|
+
if (declared.jwtExpiry !== undefined && auth.jwt_expiry !== declared.jwtExpiry) {
|
|
67
|
+
findings.push(found(DB_CONFIG_POLICY, file, `${file} auth.jwt_expiry is ${JSON.stringify(auth.jwt_expiry ?? 'absent')}; hfs.json supabase.jwtExpiry declares ${declared.jwtExpiry}`, { key: 'auth.jwt_expiry', declared: declared.jwtExpiry }));
|
|
68
|
+
}
|
|
69
|
+
if (declared.enableSignup !== undefined) {
|
|
70
|
+
if (auth.enable_signup !== declared.enableSignup) findings.push(found(DB_CONFIG_POLICY, file, `${file} auth.enable_signup is ${JSON.stringify(auth.enable_signup ?? 'absent')}; hfs.json supabase.enableSignup declares ${declared.enableSignup} - the invite posture is a checked fact, not a doc claim`, { key: 'auth.enable_signup', declared: declared.enableSignup }));
|
|
71
|
+
if (auth.email?.enable_signup !== undefined && auth.email.enable_signup !== declared.enableSignup) {
|
|
72
|
+
findings.push(found(DB_CONFIG_POLICY, file, `${file} auth.email.enable_signup is ${JSON.stringify(auth.email.enable_signup)}; hfs.json supabase.enableSignup declares ${declared.enableSignup}`, { key: 'auth.email.enable_signup', declared: declared.enableSignup }));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (declared.siteUrl !== undefined && auth.site_url !== declared.siteUrl) {
|
|
76
|
+
findings.push(found(DB_CONFIG_POLICY, file, `${file} auth.site_url is ${JSON.stringify(auth.site_url ?? 'absent')}; hfs.json supabase.siteUrl declares ${declared.siteUrl}`, { key: 'auth.site_url', declared: declared.siteUrl }));
|
|
77
|
+
}
|
|
78
|
+
if (declared.redirectUrls !== undefined) {
|
|
79
|
+
const configured = Array.isArray(auth.additional_redirect_urls) ? auth.additional_redirect_urls : [];
|
|
80
|
+
for (const url of configured) {
|
|
81
|
+
if (!declared.redirectUrls.includes(url)) findings.push(found(DB_CONFIG_POLICY, file, `${file} auth.additional_redirect_urls holds ${url}, which hfs.json supabase.redirectUrls does not declare; the config states nothing the app did not declare`, { key: 'auth.additional_redirect_urls', url }));
|
|
82
|
+
}
|
|
83
|
+
for (const url of declared.redirectUrls) {
|
|
84
|
+
if (!configured.includes(url)) findings.push(found(DB_CONFIG_POLICY, file, `${file} auth.additional_redirect_urls omits ${url}, which hfs.json supabase.redirectUrls declares; the config and declaration must carry the same redirect set`, { key: 'auth.additional_redirect_urls', url }));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return findings;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** L09 drift findings for the committed generated database type file. */
|
|
91
|
+
export async function typesFindings({ repoRoot, files, emitTypes, declaration }) {
|
|
92
|
+
if (typeof emitTypes !== 'function') return [];
|
|
93
|
+
const tracked = new Set(files);
|
|
94
|
+
let emitted;
|
|
95
|
+
try { emitted = await emitTypes({ repoRoot, declaration }); } catch (error) {
|
|
96
|
+
return [found(DB_TYPES_DRIFT, TYPES_FILE, `${TYPES_FILE} cannot be verified: the types emit failed (${String(error?.message ?? error).split('\n').filter(Boolean).slice(0, 3).join(' | ')})`, { drift: 'emit-failed' })];
|
|
97
|
+
}
|
|
98
|
+
const committed = tracked.has(TYPES_FILE) ? readText(repoRoot, TYPES_FILE) : null;
|
|
99
|
+
if (committed === null) return [found(DB_TYPES_DRIFT, TYPES_FILE, `${TYPES_FILE} is not committed; run \`npm run contract:emit\` and commit the generated types`, { drift: 'not-committed' })];
|
|
100
|
+
if (!sameText(committed, String(emitted))) return [found(DB_TYPES_DRIFT, TYPES_FILE, `${TYPES_FILE} differs from what \`contract:emit\` generates from the migrations now; run \`npm run contract:emit\` and commit the result`, { drift: 'stale' })];
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Single source of database-rule finding codes and owned Supabase paths.
|
|
2
|
+
export const DB_MIGRATION_SHAPE = 'DB_MIGRATION_SHAPE';
|
|
3
|
+
export const DB_RLS_REQUIRED = 'DB_RLS_REQUIRED';
|
|
4
|
+
export const DB_DYNAMIC_DDL = 'DB_DYNAMIC_DDL';
|
|
5
|
+
export const DB_POLICY_SHAPE = 'DB_POLICY_SHAPE';
|
|
6
|
+
export const DB_DEFINER_SAFE = 'DB_DEFINER_SAFE';
|
|
7
|
+
export const DB_STORAGE_POLICY = 'DB_STORAGE_POLICY';
|
|
8
|
+
export const DB_CONFIG_POLICY = 'DB_CONFIG_POLICY';
|
|
9
|
+
export const DB_TYPES_DRIFT = 'DB_TYPES_DRIFT';
|
|
10
|
+
|
|
11
|
+
export const MIGRATIONS_DIR = 'supabase/migrations';
|
|
12
|
+
export const CONFIG_FILE = 'supabase/config.toml';
|
|
13
|
+
export const TYPES_FILE = 'supabase/types/database.types.ts';
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// database-migrations.mjs - L02 migration name, UTC ordering and base-branch immutability checks. Git reads go
|
|
2
|
+
// through the runtime API call files, and specs may inject the same small runner contract used by database.mjs.
|
|
3
|
+
import { lsTree } from '../../api/git/ls-tree.mjs';
|
|
4
|
+
import { log } from '../../api/git/log.mjs';
|
|
5
|
+
import { mergeBase } from '../../api/git/merge-base.mjs';
|
|
6
|
+
import { show } from '../../api/git/show.mjs';
|
|
7
|
+
import { withoutGitLocalEnv } from '../../lib/git.mjs';
|
|
8
|
+
import { sameText } from '../../lib/same-text.mjs';
|
|
9
|
+
import { found, readText } from './read.mjs';
|
|
10
|
+
import { DB_MIGRATION_SHAPE, MIGRATIONS_DIR } from './database-constants.mjs';
|
|
11
|
+
const MIGRATION_NAME = /^(\d{14})_([a-z0-9]+(?:-[a-z0-9]+)*)\.sql$/;
|
|
12
|
+
|
|
13
|
+
/** `<ts14>` -> the UTC instant it names, or null when the digits are no real time (month 13, day 32, ...). */
|
|
14
|
+
export function migrationStamp(text) {
|
|
15
|
+
const [year, month, day, hour, minute, second] = [Number(text.slice(0, 4)), ...[4, 6, 8, 10, 12].map((at) => Number(text.slice(at, at + 2)))];
|
|
16
|
+
const stamp = Date.UTC(year, month - 1, day, hour, minute, second);
|
|
17
|
+
const date = new Date(stamp);
|
|
18
|
+
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day
|
|
19
|
+
&& date.getUTCHours() === hour && date.getUTCMinutes() === minute && date.getUTCSeconds() === second ? stamp : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The three git reads L02 makes: (args, {cwd}) -> {ok, stdout}. */
|
|
23
|
+
export function defaultGit(args, { cwd }) {
|
|
24
|
+
const [verb, ...rest] = args;
|
|
25
|
+
const env = withoutGitLocalEnv(process.env);
|
|
26
|
+
if (verb === 'merge-base') { const sha = mergeBase(cwd, rest[0], rest[1]); return { ok: sha !== null, stdout: sha ?? '' }; }
|
|
27
|
+
const call = verb === 'ls-tree' ? lsTree : verb === 'log' ? log : show;
|
|
28
|
+
const result = call(rest, { cwd, env, maxBuffer: 64 * 1024 * 1024 });
|
|
29
|
+
return { ok: !result.error && result.status === 0, stdout: result.stdout };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const runGit = (git, repoRoot, args) => Promise.resolve(git(args, { cwd: repoRoot }))
|
|
33
|
+
.then((result) => ({ ok: result?.ok === true, stdout: String(result?.stdout ?? '') }));
|
|
34
|
+
|
|
35
|
+
async function baseShaOf(git, repoRoot, base) {
|
|
36
|
+
for (const ref of base ? [base] : ['origin/main', 'main']) {
|
|
37
|
+
const result = await runGit(git, repoRoot, ['merge-base', 'HEAD', ref]);
|
|
38
|
+
if (result.ok && result.stdout.trim()) return { ref, sha: result.stdout.trim() };
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function baseMigrationNames(git, repoRoot, sha) {
|
|
44
|
+
const result = await runGit(git, repoRoot, ['ls-tree', '-r', '--name-only', '-z', sha, '--', MIGRATIONS_DIR]);
|
|
45
|
+
if (!result.ok) return [];
|
|
46
|
+
const prefix = `${MIGRATIONS_DIR}/`;
|
|
47
|
+
return result.stdout.split('\0').filter(Boolean).map((entry) => (entry.startsWith(prefix) ? entry.slice(prefix.length) : entry));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function baseFileText(git, repoRoot, sha, file) {
|
|
51
|
+
const result = await runGit(git, repoRoot, ['show', `${sha}:${file}`]);
|
|
52
|
+
return result.ok ? result.stdout : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function migrationTimeLimit(git, repoRoot, file, now) {
|
|
56
|
+
const result = await runGit(git, repoRoot, ['log', '-1', '--format=%ct', '--', file]);
|
|
57
|
+
const seconds = Number(result.stdout.trim());
|
|
58
|
+
if (result.ok && Number.isFinite(seconds) && seconds > 0) return { at: seconds * 1000, tracked: true };
|
|
59
|
+
return { at: now(), tracked: false };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
/** L02 findings for names, stamps, ordering and immutability of supabase/migrations/*.sql. */
|
|
64
|
+
export async function migrationShapeFindings({ repoRoot, migrations, git, base, now }) {
|
|
65
|
+
const findings = [];
|
|
66
|
+
const prefix = `${MIGRATIONS_DIR}/`;
|
|
67
|
+
const local = migrations.map((file) => {
|
|
68
|
+
const name = file.startsWith(prefix) ? file.slice(prefix.length) : file;
|
|
69
|
+
return { file, name, match: MIGRATION_NAME.exec(name) };
|
|
70
|
+
});
|
|
71
|
+
const stamps = new Map();
|
|
72
|
+
for (const entry of local) {
|
|
73
|
+
if (!entry.match) {
|
|
74
|
+
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' }));
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const stamp = migrationStamp(entry.match[1]);
|
|
78
|
+
if (stamp === null) {
|
|
79
|
+
findings.push(found(DB_MIGRATION_SHAPE, entry.file, `${entry.file} carries ${entry.match[1]}, which is no valid UTC time; the stamp is the real creation time`, { stamp: entry.match[1] }));
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const limit = await migrationTimeLimit(git, repoRoot, entry.file, now);
|
|
83
|
+
if (stamp > limit.at) {
|
|
84
|
+
const boundary = limit.tracked ? `the file's commit time ${new Date(limit.at).toISOString()}` : `the current time ${new Date(limit.at).toISOString()}`;
|
|
85
|
+
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 }));
|
|
86
|
+
}
|
|
87
|
+
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) }));
|
|
88
|
+
stamps.set(stamp, entry.file);
|
|
89
|
+
}
|
|
90
|
+
const baseSha = await baseShaOf(git, repoRoot, base);
|
|
91
|
+
if (!baseSha) return findings;
|
|
92
|
+
const baseNames = await baseMigrationNames(git, repoRoot, baseSha.sha);
|
|
93
|
+
const onBase = new Set(baseNames);
|
|
94
|
+
const baseStamps = baseNames.map((name) => MIGRATION_NAME.exec(name)?.[1]).filter(Boolean).map(migrationStamp).filter((stamp) => stamp !== null);
|
|
95
|
+
const baseMax = baseStamps.length ? Math.max(...baseStamps) : null;
|
|
96
|
+
for (const entry of local) {
|
|
97
|
+
if (!entry.match) continue;
|
|
98
|
+
const stamp = migrationStamp(entry.match[1]);
|
|
99
|
+
if (onBase.has(entry.name)) {
|
|
100
|
+
const committed = await baseFileText(git, repoRoot, baseSha.sha, entry.file);
|
|
101
|
+
const current = readText(repoRoot, entry.file);
|
|
102
|
+
if (!sameText(current, committed)) findings.push(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 }));
|
|
103
|
+
} else if (baseMax !== null && stamp !== null && stamp <= baseMax) {
|
|
104
|
+
findings.push(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 }));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const name of baseNames) {
|
|
108
|
+
if (!local.some((entry) => entry.name === name)) findings.push(found(DB_MIGRATION_SHAPE, `${MIGRATIONS_DIR}/${name}`, `${MIGRATIONS_DIR}/${name} exists on ${baseSha.ref} but is gone here; a migration on the base branch is immutable - restore it`, { base: baseSha.ref }));
|
|
109
|
+
}
|
|
110
|
+
return findings;
|
|
111
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// PL/pgSQL inspection for DB_DYNAMIC_DDL and DB_DEFINER_SAFE. Function and DO bodies are parsed by libpg-query;
|
|
2
|
+
// dynamic query expressions are then parsed as PostgreSQL expressions instead of matched as raw migration text.
|
|
3
|
+
import { found } from './read.mjs';
|
|
4
|
+
import { parsePlpgsqlBody, parseSql } from '../sql/pg-parse.mjs';
|
|
5
|
+
import { DB_DEFINER_SAFE, DB_DYNAMIC_DDL } from './database-constants.mjs';
|
|
6
|
+
|
|
7
|
+
const sval = (node) => node?.String?.sval;
|
|
8
|
+
const nameOf = (list, defaultSchema) => {
|
|
9
|
+
const parts = (list ?? []).map(sval).filter((part) => part !== undefined);
|
|
10
|
+
return parts.length === 1 && defaultSchema ? `${defaultSchema}.${parts[0]}` : parts.join('.');
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const functionOption = (node, key) => (node.options ?? []).filter((option) => option?.DefElem?.defname === key).map((option) => option.DefElem);
|
|
14
|
+
const securityDefiner = (node) => functionOption(node, 'security').some((option) => option.arg?.Boolean?.boolval === true || option.arg?.Integer?.ival === 1);
|
|
15
|
+
const functionLanguage = (node) => functionOption(node, 'language').map((option) => option.arg?.String?.sval).filter(Boolean).at(-1) ?? 'sql';
|
|
16
|
+
const functionBody = (node) => functionOption(node, 'as').flatMap((option) => (option.arg?.List?.items ?? []).map((item) => item?.String?.sval ?? '')).join('') || null;
|
|
17
|
+
const functionReturnKind = (node) => {
|
|
18
|
+
const name = node.returnType?.names?.map(sval).filter(Boolean).at(-1);
|
|
19
|
+
if (name === 'void') return 'void';
|
|
20
|
+
if (name === 'trigger' || name === 'event_trigger') return 'trigger';
|
|
21
|
+
return node.returnType?.setof ? 'setof' : 'scalar';
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Every dynamic query expression in one parsed PL/pgSQL function. */
|
|
25
|
+
function dynamicQueries(root) {
|
|
26
|
+
const out = [];
|
|
27
|
+
const visit = (value) => {
|
|
28
|
+
if (!value || typeof value !== 'object') return;
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
for (const item of value) visit(item);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
for (const [key, child] of Object.entries(value)) {
|
|
34
|
+
if (key === 'PLpgSQL_stmt_dynexecute' || key === 'PLpgSQL_stmt_dynfors') {
|
|
35
|
+
const text = child?.query?.PLpgSQL_expr?.query;
|
|
36
|
+
if (text !== undefined) out.push({ text, lineno: child.lineno });
|
|
37
|
+
} else if (key === 'PLpgSQL_stmt_open') {
|
|
38
|
+
const text = child?.dynquery?.PLpgSQL_expr?.query;
|
|
39
|
+
if (text !== undefined) out.push({ text, lineno: child.lineno });
|
|
40
|
+
}
|
|
41
|
+
visit(child);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
visit(root);
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function plpgsqlOf(body, parameters, returnKind) {
|
|
49
|
+
try {
|
|
50
|
+
return await parsePlpgsqlBody(body, parameters, returnKind);
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Parse a PL/pgSQL dynamic-query source as one PostgreSQL expression. */
|
|
57
|
+
async function expressionOf(text) {
|
|
58
|
+
try {
|
|
59
|
+
const parsed = await parseSql(`select ${text}`);
|
|
60
|
+
return parsed.stmts?.[0]?.stmt?.SelectStmt?.targetList?.[0]?.ResTarget?.val ?? null;
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const stringValue = (node) => node?.A_Const?.sval?.sval;
|
|
67
|
+
const formatName = (node) => node?.FuncCall?.funcname?.map(sval).filter(Boolean).join('.').toLowerCase() === 'format';
|
|
68
|
+
const operatorName = (node) => node?.A_Expr?.name?.map(sval).filter(Boolean).join('');
|
|
69
|
+
const FORMAT_SPECIFIER = /%(?:(\d+)\$)?-?(?:\d+|\*(?:\d+\$)?)?([sIL%])/g;
|
|
70
|
+
|
|
71
|
+
/** Every format() conversion, or null when the template contains an invalid/unreadable percent sequence. */
|
|
72
|
+
function formatSpecifiers(template) {
|
|
73
|
+
const specs = [];
|
|
74
|
+
let cursor = 0;
|
|
75
|
+
for (const match of template.matchAll(FORMAT_SPECIFIER)) {
|
|
76
|
+
if (template.slice(cursor, match.index).includes('%')) return null;
|
|
77
|
+
specs.push({ position: match[1] ? Number(match[1]) - 1 : null, type: match[2] });
|
|
78
|
+
cursor = match.index + match[0].length;
|
|
79
|
+
}
|
|
80
|
+
return template.slice(cursor).includes('%') ? null : specs;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Render enough of a dynamic expression to let the PostgreSQL parser identify its generated statement kind. */
|
|
84
|
+
function renderExpression(node) {
|
|
85
|
+
const literal = stringValue(node);
|
|
86
|
+
if (literal !== undefined) return { text: literal, complete: true };
|
|
87
|
+
if (node?.TypeCast) return renderExpression(node.TypeCast.arg);
|
|
88
|
+
if (operatorName(node) === '||') {
|
|
89
|
+
const left = renderExpression(node.A_Expr.lexpr);
|
|
90
|
+
const right = renderExpression(node.A_Expr.rexpr);
|
|
91
|
+
return { text: `${left.text}${right.text}`, complete: left.complete && right.complete };
|
|
92
|
+
}
|
|
93
|
+
if (formatName(node)) {
|
|
94
|
+
const args = node.FuncCall.args ?? [];
|
|
95
|
+
const template = stringValue(args[0]);
|
|
96
|
+
if (template === undefined) return { text: 'hfs_dynamic', complete: false };
|
|
97
|
+
const specs = formatSpecifiers(template);
|
|
98
|
+
if (specs === null) return { text: template, complete: false };
|
|
99
|
+
let sequential = 0;
|
|
100
|
+
let specIndex = 0;
|
|
101
|
+
let complete = true;
|
|
102
|
+
const text = template.replace(FORMAT_SPECIFIER, () => {
|
|
103
|
+
const spec = specs[specIndex];
|
|
104
|
+
specIndex += 1;
|
|
105
|
+
if (spec.type === '%') return '%';
|
|
106
|
+
const argumentIndex = spec.position ?? sequential;
|
|
107
|
+
if (spec.position === null) sequential += 1;
|
|
108
|
+
const argument = args[argumentIndex + 1];
|
|
109
|
+
if (spec.type === 'I') return 'hfs_identifier';
|
|
110
|
+
if (spec.type === 'L') return "'hfs_literal'";
|
|
111
|
+
const value = stringValue(argument);
|
|
112
|
+
if (value !== undefined) return value;
|
|
113
|
+
complete = false;
|
|
114
|
+
return 'hfs_dynamic';
|
|
115
|
+
});
|
|
116
|
+
return { text, complete };
|
|
117
|
+
}
|
|
118
|
+
return { text: 'hfs_dynamic', complete: false };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** DDL statement kinds forbidden behind dynamic execution. */
|
|
122
|
+
function containsDynamicDdl(parsed) {
|
|
123
|
+
for (const entry of parsed.stmts ?? []) {
|
|
124
|
+
const type = Object.keys(entry.stmt ?? {})[0];
|
|
125
|
+
if (['CreateStmt', 'CreateTableAsStmt', 'CreatePolicyStmt', 'CreateFunctionStmt', 'AlterTableStmt', 'AlterFunctionStmt', 'AlterPolicyStmt', 'AlterDefaultPrivilegesStmt', 'GrantStmt', 'GrantRoleStmt', 'RenameStmt'].includes(type)) return true;
|
|
126
|
+
if (type === 'DropStmt' && ['OBJECT_TABLE', 'OBJECT_POLICY', 'OBJECT_FUNCTION'].includes(entry.stmt.DropStmt?.removeType)) return true;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function dynamicQueryAnalysis(text) {
|
|
132
|
+
const expression = await expressionOf(text);
|
|
133
|
+
if (expression === null) return { buildsDdl: false, opaque: true };
|
|
134
|
+
const rendered = renderExpression(expression);
|
|
135
|
+
try {
|
|
136
|
+
const parsed = await parseSql(rendered.text);
|
|
137
|
+
return { buildsDdl: containsDynamicDdl(parsed), opaque: false };
|
|
138
|
+
} catch {
|
|
139
|
+
return { buildsDdl: false, opaque: !rendered.complete };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function dynamicDdlFindings(file, queries, where, baseLine) {
|
|
144
|
+
const findings = [];
|
|
145
|
+
for (const { text, lineno } of queries) {
|
|
146
|
+
const line = lineno ? baseLine + lineno - 1 : undefined;
|
|
147
|
+
const analysis = await dynamicQueryAnalysis(text);
|
|
148
|
+
if (analysis.buildsDdl) {
|
|
149
|
+
findings.push(found(DB_DYNAMIC_DDL, file, `${file}${line ? `:${line}` : ''} ${where} builds DDL dynamically (\`${String(text).slice(0, 120)}\`); write the create/alter/drop or grant/revoke out as statements so the parser sees every one`, { ...(line ? { line } : {}), query: text }));
|
|
150
|
+
} else if (analysis.opaque) {
|
|
151
|
+
findings.push(found(DB_DYNAMIC_DDL, file, `${file}${line ? `:${line}` : ''} ${where} runs dynamic SQL the pass cannot read (\`${String(text).slice(0, 120)}\`); dynamic DDL is refused and a query built from an opaque value cannot be judged`, { ...(line ? { line } : {}), query: text }));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return findings;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** A definer expression is safe when it consists only of literals and format() calls whose conversions quote. */
|
|
158
|
+
function safeDefinerExpression(node) {
|
|
159
|
+
if (stringValue(node) !== undefined) return true;
|
|
160
|
+
if (node?.TypeCast) return safeDefinerExpression(node.TypeCast.arg);
|
|
161
|
+
if (operatorName(node) === '||') return safeDefinerExpression(node.A_Expr.lexpr) && safeDefinerExpression(node.A_Expr.rexpr);
|
|
162
|
+
if (!formatName(node)) return false;
|
|
163
|
+
const template = stringValue(node.FuncCall.args?.[0]);
|
|
164
|
+
if (template === undefined) return false;
|
|
165
|
+
const specs = formatSpecifiers(template);
|
|
166
|
+
return specs !== null && specs.every((spec) => ['I', 'L', '%'].includes(spec.type));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function definerDynamicFindings(file, fn, queries, baseLine) {
|
|
170
|
+
const findings = [];
|
|
171
|
+
for (const { text, lineno } of queries) {
|
|
172
|
+
const line = lineno ? baseLine + lineno - 1 : undefined;
|
|
173
|
+
const expression = await expressionOf(text);
|
|
174
|
+
if (expression !== null && safeDefinerExpression(expression)) continue;
|
|
175
|
+
findings.push(found(DB_DEFINER_SAFE, file, `${file}${line ? `:${line}` : ''} security definer function ${fn.name} builds dynamic SQL without quoting every parameter through format() %I or %L (\`${String(text).slice(0, 120)}\`)`, { ...(line ? { line } : {}), function: fn.name, query: text }));
|
|
176
|
+
}
|
|
177
|
+
return findings;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function revokedRolesFor(fn, grants) {
|
|
181
|
+
const roles = new Set();
|
|
182
|
+
for (const grant of grants) {
|
|
183
|
+
if (grant.grant || grant.objtype !== 'OBJECT_FUNCTION') continue;
|
|
184
|
+
if (!grant.privileges.includes('execute') && !grant.privileges.includes('all')) continue;
|
|
185
|
+
const matches = grant.objects.some((object) => nameOf(object?.ObjectWithArgs?.objname, grant.defaultSchema) === fn.name);
|
|
186
|
+
if (matches) for (const role of grant.grantees) roles.add(role);
|
|
187
|
+
}
|
|
188
|
+
return roles;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** All PL/pgSQL-backed L04/L06 findings for one migration's extracted facts. */
|
|
192
|
+
export async function plpgsqlFindings(file, facts, exposed) {
|
|
193
|
+
const findings = [];
|
|
194
|
+
for (const block of facts.doBlocks) {
|
|
195
|
+
if (block.body === undefined || block.language !== 'plpgsql') {
|
|
196
|
+
findings.push(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 }));
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const ast = await plpgsqlOf(block.body);
|
|
200
|
+
if (ast === null) {
|
|
201
|
+
findings.push(found(DB_DYNAMIC_DDL, file, `${file}:${block.line} DO block body is not valid plpgsql; the pass must see every statement a migration runs`, { line: block.line }));
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
findings.push(...await dynamicDdlFindings(file, dynamicQueries(ast), 'DO block', block.line));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
for (const fn of facts.functions) {
|
|
208
|
+
const definer = securityDefiner(fn.node);
|
|
209
|
+
if (definer) {
|
|
210
|
+
const pathSet = functionOption(fn.node, 'set').some((option) => facts.isSearchPathSet(option))
|
|
211
|
+
|| facts.alterFunctions.some((alter) => alter.name === fn.name && alter.setsPath && alter.index > fn.index);
|
|
212
|
+
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 }));
|
|
213
|
+
if (exposed.has(fn.schema)) {
|
|
214
|
+
const revoked = revokedRolesFor(fn, facts.grants);
|
|
215
|
+
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 }));
|
|
216
|
+
}
|
|
217
|
+
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 }));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (functionLanguage(fn.node) !== 'plpgsql') continue;
|
|
221
|
+
const body = functionBody(fn.node);
|
|
222
|
+
if (body === null) continue;
|
|
223
|
+
const ast = await plpgsqlOf(body, fn.node.parameters, functionReturnKind(fn.node));
|
|
224
|
+
if (ast === null) {
|
|
225
|
+
if (definer) findings.push(found(DB_DEFINER_SAFE, file, `${file}:${fn.line} security definer function ${fn.name} has a body the pass cannot read; a definer body must be plain plpgsql`, { line: fn.line, function: fn.name }));
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const queries = dynamicQueries(ast);
|
|
229
|
+
findings.push(...await dynamicDdlFindings(file, queries, `function ${fn.name}`, fn.line));
|
|
230
|
+
if (definer) findings.push(...await definerDynamicFindings(file, fn, queries, fn.line));
|
|
231
|
+
}
|
|
232
|
+
return findings;
|
|
233
|
+
}
|