@starci/hfs 4.3.0 → 4.4.1
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 +14 -0
- package/README.md +1 -1
- package/db/migrate.mjs +361 -0
- package/db/post-cutover.mjs +136 -0
- package/emit/contracts.mjs +2 -2
- package/emit/db-types.mjs +12 -3
- package/lint/run.mjs +3 -3
- package/package.json +4 -1
- package/release/gate.mjs +298 -0
- package/release/index.mjs +103 -0
- package/release/push.mjs +110 -0
- package/report/sonar.mjs +1 -1
- package/runtime/config.example.yaml +7 -6
- package/runtime/engine/config.mjs +10 -18
- package/runtime/engine/removed-vocabulary.mjs +0 -2
- package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
- package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
- package/runtime/knowledge/hfs/rules.yaml +2855 -163
- package/runtime/knowledge/hfs/slots.yaml +333 -62
- package/runtime/knowledge/patterns/be/api.yaml +32 -5
- package/runtime/knowledge/patterns/be/cli.yaml +14 -4
- package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
- package/runtime/knowledge/patterns/be/jobs.yaml +20 -2
- package/runtime/knowledge/patterns/be/projections.yaml +4 -2
- package/runtime/knowledge/patterns/be/queues.yaml +11 -3
- package/runtime/knowledge/patterns/be/reactors.yaml +20 -1
- package/runtime/knowledge/patterns/be/realtime.yaml +25 -3
- package/runtime/knowledge/patterns/be/saga.yaml +20 -3
- package/runtime/knowledge/patterns/be/webhooks.yaml +23 -4
- package/runtime/knowledge/patterns/fe/folder.yaml +1 -0
- package/runtime/knowledge/sonar-gate.yaml +1 -1
- package/runtime/modules/kernel/failure-codes.yaml +3091 -43
- package/runtime/modules/kernel/removed-vocabulary.yaml +30 -0
- package/runtime/modules/models/runtimes.yaml +5 -7
- package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
- package/runtime/packages/cli/src/catalog.generated.mjs +13769 -0
- package/runtime/packages/cli/src/validate-args.mjs +213 -0
- package/runtime/scripts/api/git/add.mjs +5 -0
- package/runtime/scripts/api/git/init.mjs +7 -0
- package/runtime/scripts/api/git/lib.mjs +4 -1
- package/runtime/scripts/api/git/tag.mjs +5 -0
- package/runtime/scripts/api/node/lib.mjs +20 -0
- package/runtime/scripts/api/node/run-node.mjs +6 -0
- package/runtime/scripts/api/sops/lib.mjs +42 -1
- package/runtime/scripts/api/sops/selected-age-recipient.mjs +7 -0
- package/runtime/scripts/api/supabase/database-dump.mjs +5 -0
- package/runtime/scripts/api/supabase/database-query.mjs +5 -0
- package/runtime/scripts/api/supabase/lib.mjs +44 -0
- package/runtime/scripts/api/supabase/migration-repair.mjs +5 -0
- package/runtime/scripts/hfs/app-wiring.mjs +201 -0
- package/runtime/scripts/hfs/architecture/backend-app-files.mjs +1 -8
- package/runtime/scripts/hfs/architecture/backend.mjs +2 -7
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
- package/runtime/scripts/hfs/architecture/config.mjs +1 -1
- package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +9 -41
- package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
- package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
- package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
- package/runtime/scripts/hfs/architecture/index.mjs +1 -1
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +28 -13
- package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
- package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
- package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
- package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
- package/runtime/scripts/hfs/architecture/register-once.mjs +2 -2
- package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +211 -11
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -1
- package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
- package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
- package/runtime/scripts/hfs/architecture/tiers.mjs +4 -4
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +8 -2
- package/runtime/scripts/hfs/architecture.mjs +1 -1
- package/runtime/scripts/hfs/check-results.mjs +28 -0
- package/runtime/scripts/hfs/check.mjs +46 -47
- package/runtime/scripts/hfs/coverage-from.mjs +70 -0
- package/runtime/scripts/hfs/coverage-scope.mjs +1 -1
- package/runtime/scripts/hfs/db-baseline-verb.mjs +276 -0
- package/runtime/scripts/hfs/db-plan-verb.mjs +254 -0
- package/runtime/scripts/hfs/db-restamp-verb.mjs +60 -0
- package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
- package/runtime/scripts/hfs/doc-commands.mjs +367 -0
- package/runtime/scripts/hfs/edition-slots.mjs +2 -2
- package/runtime/scripts/hfs/example-secrets.mjs +158 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +69 -9
- package/runtime/scripts/hfs/param-names.mjs +1 -1
- package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
- package/runtime/scripts/hfs/rule-catalog.mjs +158 -11
- package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
- package/runtime/scripts/hfs/rules/a15-findings.mjs +88 -0
- package/runtime/scripts/hfs/rules/app-env-required.mjs +20 -0
- package/runtime/scripts/hfs/rules/app-root.mjs +2 -2
- package/runtime/scripts/hfs/rules/assets.mjs +79 -0
- package/runtime/scripts/hfs/rules/boot-proof.mjs +16 -0
- package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +59 -0
- package/runtime/scripts/hfs/rules/cli.mjs +1 -1
- package/runtime/scripts/hfs/rules/contract-compat.mjs +1 -1
- package/runtime/scripts/hfs/rules/contract.mjs +67 -6
- package/runtime/scripts/hfs/rules/coverage-below.mjs +230 -0
- package/runtime/scripts/hfs/rules/database-access.mjs +343 -0
- package/runtime/scripts/hfs/rules/database-baseline.mjs +142 -0
- package/runtime/scripts/hfs/rules/database-config.mjs +250 -2
- package/runtime/scripts/hfs/rules/database-constants.mjs +9 -0
- package/runtime/scripts/hfs/rules/database-migrations.mjs +286 -7
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +119 -9
- package/runtime/scripts/hfs/rules/database-queue-sources.mjs +176 -0
- package/runtime/scripts/hfs/rules/database-queues.mjs +361 -0
- package/runtime/scripts/hfs/rules/database-sql.mjs +91 -9
- package/runtime/scripts/hfs/rules/database.mjs +9 -0
- package/runtime/scripts/hfs/rules/deploy-scan.mjs +192 -0
- package/runtime/scripts/hfs/rules/deploy-workflow.mjs +219 -0
- package/runtime/scripts/hfs/rules/doc-fresh.mjs +286 -0
- package/runtime/scripts/hfs/rules/docker-health.mjs +249 -0
- package/runtime/scripts/hfs/rules/docker.mjs +24 -7
- package/runtime/scripts/hfs/rules/edition.mjs +9 -8
- package/runtime/scripts/hfs/rules/env-parity.mjs +373 -0
- package/runtime/scripts/hfs/rules/env-value-shape.mjs +113 -0
- package/runtime/scripts/hfs/rules/event-bus.mjs +3 -2
- package/runtime/scripts/hfs/rules/event-tables.mjs +100 -0
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +115 -15
- package/runtime/scripts/hfs/rules/fe-stack.mjs +57 -0
- package/runtime/scripts/hfs/rules/feature-files.mjs +28 -0
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +54 -35
- package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -2
- package/runtime/scripts/hfs/rules/lexicon.mjs +62 -0
- package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +89 -0
- package/runtime/scripts/hfs/rules/monorepo.mjs +14 -1
- package/runtime/scripts/hfs/rules/pipeline.mjs +8 -1
- package/runtime/scripts/hfs/rules/release-provenance.mjs +211 -0
- package/runtime/scripts/hfs/rules/saga-delivery.mjs +66 -0
- package/runtime/scripts/hfs/rules/saga.mjs +8 -16
- package/runtime/scripts/hfs/rules/script-hygiene.mjs +151 -0
- package/runtime/scripts/hfs/rules/secret-custody-only.mjs +65 -0
- package/runtime/scripts/hfs/rules/secrets.mjs +13 -0
- package/runtime/scripts/hfs/rules/services.mjs +4 -4
- package/runtime/scripts/hfs/rules/stack-tooling.mjs +93 -0
- package/runtime/scripts/hfs/rules/stacks-deploy.mjs +76 -0
- package/runtime/scripts/hfs/rules/stacks-ingress.mjs +87 -0
- package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -0
- package/runtime/scripts/hfs/rules/stacks-release.mjs +366 -0
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +41 -1
- package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +2 -2
- package/runtime/scripts/hfs/rules/supply-chain.mjs +167 -0
- package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +101 -0
- package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +77 -0
- package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +165 -0
- package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +103 -0
- package/runtime/scripts/hfs/rules/unused-symbol.mjs +320 -0
- package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +185 -0
- package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +19 -0
- package/runtime/scripts/hfs/sealed-members.mjs +101 -0
- package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +200 -0
- package/runtime/scripts/hfs/secret-edit.mjs +170 -0
- package/runtime/scripts/hfs/secret-init.mjs +127 -19
- package/runtime/scripts/hfs/secret-sops.mjs +38 -6
- package/runtime/scripts/hfs/secret-store.mjs +56 -54
- package/runtime/scripts/hfs/secret.mjs +34 -5
- package/runtime/scripts/hfs/slot-imports.mjs +18 -5
- package/runtime/scripts/hfs/slots.mjs +3 -2
- package/runtime/scripts/hfs/stack-config-kit.mjs +34 -0
- package/runtime/scripts/hfs/stack-deploy-config.mjs +313 -0
- package/runtime/scripts/hfs/stack-ingress-config.mjs +237 -0
- package/runtime/scripts/hfs/stacks-gate.mjs +414 -0
- package/runtime/scripts/hfs/starcistacks-runtime.mjs +61 -0
- package/runtime/scripts/hfs/typescript-programs.mjs +0 -8
- package/runtime/scripts/lib/command-tokens.mjs +126 -0
- package/runtime/scripts/lib/compose-values.mjs +11 -0
- package/runtime/scripts/lib/deployment-findings.mjs +7 -0
- package/runtime/scripts/lib/env.mjs +2 -2
- package/runtime/scripts/lib/git.mjs +1 -1
- package/runtime/scripts/lib/i18n.mjs +0 -2
- package/runtime/scripts/lib/json-clone.mjs +61 -0
- package/runtime/scripts/lib/language.mjs +1 -1
- package/runtime/scripts/lib/list.mjs +4 -0
- package/runtime/scripts/lib/roots.mjs +2 -4
- package/runtime/scripts/lib/runtime-example.mjs +13 -0
- package/runtime/scripts/lib/secret-patterns.mjs +60 -5
- package/runtime/scripts/lib/sql-ast.mjs +32 -0
- package/runtime/scripts/lib/stack-services.mjs +1 -1
- package/runtime/scripts/lib/swarm-stack.mjs +5 -18
- package/runtime/scripts/lib/trim-end.mjs +8 -0
- package/runtime/scripts/lib/ts-analysis.mjs +5 -2
- package/runtime/scripts/lib/ts-ast.mjs +27 -2
- package/runtime/scripts/lib/workflow-events.mjs +8 -0
- package/scaffold/add-app.mjs +77 -59
- package/scaffold/add-cli-lite.mjs +14 -6
- package/scaffold/add-compose.mjs +79 -0
- package/scaffold/add-database-lite.mjs +14 -0
- package/scaffold/add-fe-overlay.mjs +1 -0
- package/scaffold/add-fe-page.mjs +1 -0
- package/scaffold/add-inbox-lite.mjs +74 -42
- package/scaffold/add-table.mjs +110 -87
- package/scaffold/add.mjs +159 -44
- package/scaffold/app.mjs +124 -39
- package/scaffold/edition-gate.mjs +3 -3
- package/scaffold/lite-exports.mjs +2 -1
- package/scaffold/service.mjs +2 -2
- package/scaffold/supabase-ports.mjs +1 -1
- package/src/main.mjs +229 -116
- package/stack/age-install.mjs +37 -0
- package/stack/backup.mjs +100 -0
- package/stack/build-args.mjs +42 -0
- package/stack/build.mjs +173 -0
- package/stack/cli.mjs +130 -0
- package/stack/config.mjs +57 -0
- package/stack/context.mjs +57 -0
- package/stack/converge.mjs +107 -0
- package/stack/deploy.mjs +301 -0
- package/stack/digest.mjs +16 -0
- package/stack/docker-transport.mjs +53 -0
- package/stack/edge-alias.mjs +118 -0
- package/stack/errors.mjs +21 -0
- package/stack/image-scan.mjs +152 -0
- package/stack/ingress-create.mjs +114 -0
- package/stack/ingress-publish.mjs +273 -0
- package/stack/ingress.mjs +706 -0
- package/stack/jobs.mjs +51 -0
- package/stack/live-restore.mjs +177 -0
- package/stack/prepare.mjs +266 -0
- package/stack/private-file.mjs +22 -0
- package/stack/provision.mjs +90 -0
- package/stack/prune.mjs +46 -0
- package/stack/release-store.mjs +87 -0
- package/stack/remote.mjs +205 -0
- package/stack/render.mjs +149 -0
- package/stack/retire.mjs +47 -0
- package/stack/sealed.mjs +149 -0
- package/stack/task.mjs +42 -0
- package/stack/verify.mjs +99 -0
- package/sync/ci-commands.mjs +13 -0
- package/sync/format.mjs +1 -1
- package/sync/hygiene.mjs +1 -1
- package/sync/index.mjs +110 -54
- package/sync/stack-images.mjs +127 -0
- package/templates/app/ci-workflows/github/workflows/ci.yml +5 -1
- package/templates/app/ci-workflows/github/workflows/images.yml +15 -3
- package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +148 -0
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +7 -3
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +38 -6
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +14 -2
- package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +148 -0
- package/templates/app/codegen-graphql.mjs.partial +13 -2
- package/templates/app/codegen-lite/scripts/codegen.mjs +6 -1
- package/templates/app/codegen-prelude.mjs.partial +8 -0
- package/templates/app/gitignore +3 -0
- package/templates/app/gitignore-lite +3 -0
- package/templates/app/package-scripts/package.json +1 -0
- package/templates/app/package-scripts/supabase.json +7 -0
- package/templates/app/package-scripts-lite/package.json +1 -7
- package/templates/app/skeleton/.editorconfig +1 -0
- package/templates/app/skeleton/.gitattributes +7 -1
- package/templates/app/skeleton/.github/dependabot.yml +17 -0
- package/templates/app/skeleton/.nvmrc +1 -1
- package/templates/app/skeleton/.starcistacks/application-stacks.yaml +134 -1
- package/templates/app/skeleton/.starcistacks/dev/README.md +16 -0
- package/templates/app/skeleton/.starcistacks/dev/environment.json +5 -0
- package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +71 -0
- package/templates/app/skeleton/.starcistacks/dev/runtime/env/KEYS.md +14 -0
- package/templates/app/skeleton/.starcistacks/production/README.md +37 -0
- package/templates/app/skeleton/.starcistacks/production/environment.json +5 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/Caddyfile +15 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +9 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +142 -0
- package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +14 -0
- package/templates/app/skeleton/README.md +6 -6
- package/templates/app/skeleton/scripts/codegen.mjs +1 -2
- package/templates/app/skeleton-lite/.editorconfig +16 -0
- package/templates/app/skeleton-lite/.gitattributes +8 -0
- package/templates/app/skeleton-lite/.github/dependabot.yml +17 -0
- package/templates/app/skeleton-lite/.nvmrc +1 -0
- package/templates/app/skeleton-lite/.sops.yaml +3 -2
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +178 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +20 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +2 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +16 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +8 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +120 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -0
- package/templates/app/skeleton-lite/.starcistacks/production/README.md +38 -0
- package/templates/app/skeleton-lite/.starcistacks/production/environment.json +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/Caddyfile +11 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/images.lock.yaml +8 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +121 -0
- package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +21 -0
- package/templates/app/skeleton-lite/.starciwork/brand/index.yaml +12 -0
- package/templates/app/skeleton-lite/README.md +5 -3
- package/templates/app/skeleton-lite/supabase/config.toml +8 -1
- package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +15 -6
- package/templates/app/upgrade-full/.starcistacks/dev/README.md +13 -7
- package/templates/be/image/worker/Dockerfile +1 -1
- package/templates/be/inbox/database-client.spec.ts.tpl +17 -18
- package/templates/be/inbox/database-client.ts.tpl +12 -11
- package/templates/be/inbox/delivery-recorded.event.ts.tpl +36 -0
- package/templates/be/inbox/event-index.ts.tpl +1 -0
- package/templates/be/inbox/events.json.tpl +10 -0
- package/templates/be/inbox/migration.sql.tpl +1 -0
- package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +42 -0
- package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +9 -0
- package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +6 -0
- package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +19 -0
- package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +2 -0
- package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +16 -0
- package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +45 -0
- package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +15 -0
- package/templates/be/inbox/platform/event-bus/index.ts.tpl +5 -0
- package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +5 -0
- package/templates/be/inbox/platform/inbox/inbox.decorators.ts.tpl +9 -0
- package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +6 -0
- package/templates/be/inbox/platform/inbox/inbox.module.ts.tpl +19 -0
- package/templates/be/inbox/platform/inbox/inbox.options.ts.tpl +2 -0
- package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +10 -0
- package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +41 -0
- package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +19 -0
- package/templates/be/inbox/platform/inbox/index.ts.tpl +3 -0
- package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +7 -0
- package/templates/be/inbox/platform/migration.sql.tpl +48 -0
- package/templates/be/inbox/service.spec.ts.tpl +42 -9
- package/templates/be/inbox/service.ts.tpl +23 -8
- package/templates/be/inbox/webhook-door.spec.ts.tpl +1 -1
- package/templates/be/inbox/webhook-door.ts.tpl +2 -2
- package/templates/be/patterns/api/feature.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/api/feature.module.ts.tpl +2 -1
- package/templates/be/patterns/api/feature.options.ts.tpl +2 -0
- package/templates/be/patterns/api/graphql.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/api/graphql.module.ts.tpl +3 -2
- package/templates/be/patterns/api/graphql.options.ts.tpl +2 -0
- package/templates/be/patterns/cli/group.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/cli/group.module.ts.tpl +2 -1
- package/templates/be/patterns/cli/group.options.ts.tpl +2 -0
- package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +0 -3
- package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +0 -2
- package/templates/be/patterns/jobs/job.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/jobs/job.module.ts.tpl +2 -1
- package/templates/be/patterns/jobs/job.options.ts.tpl +2 -0
- package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/jobs/job.queue.module.ts.tpl +6 -3
- package/templates/be/patterns/jobs/job.queue.options.ts.tpl +2 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +3 -1
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +25 -9
- package/templates/be/patterns/queues/platform/queue.config.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform/queue.index.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +2 -2
- package/templates/be/patterns/queues/platform/queue.module.ts.tpl +11 -0
- package/templates/be/patterns/queues/platform/queue.options.ts.tpl +10 -0
- package/templates/be/patterns/queues/queue.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/queues/queue.module.ts.tpl +2 -1
- package/templates/be/patterns/queues/queue.options.ts.tpl +2 -0
- package/templates/be/patterns/reactors/message.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/reactors/message.module.ts.tpl +6 -3
- package/templates/be/patterns/reactors/message.options.ts.tpl +2 -0
- package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/reactors/reactor.module.ts.tpl +2 -1
- package/templates/be/patterns/reactors/reactor.options.ts.tpl +2 -0
- package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +188 -0
- package/templates/be/patterns/realtime/channel.gateway.ts.tpl +22 -0
- package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +2 -1
- package/templates/be/patterns/realtime/channel.graphql.options.ts.tpl +2 -0
- package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +110 -0
- package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +7 -0
- package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/saga/saga.message.module.ts.tpl +6 -3
- package/templates/be/patterns/saga/saga.message.options.ts.tpl +2 -0
- package/templates/be/patterns/saga/saga.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/saga/saga.module.ts.tpl +2 -1
- package/templates/be/patterns/saga/saga.options.ts.tpl +2 -0
- package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +2 -1
- package/templates/be/patterns/webhooks/webhook.http.options.ts.tpl +2 -0
- package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +80 -26
- package/templates/be/platform/graphql/localize-errors.mapper.ts +2 -5
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +11 -9
- package/templates/be/skeleton/apps/__app__/src/compose/api.ts +4 -0
- package/templates/be/skeleton/apps/__app__/src/compose/domain.ts +11 -0
- package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +2 -0
- package/templates/be/skeleton/apps/__app__/src/compose/system.ts +5 -0
- package/templates/be/skeleton/apps/__app__/src/compose/webhooks.ts +4 -0
- package/templates/be/skeleton/apps/__app__/src/main.ts +15 -1
- package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
- package/templates/be/skeleton/apps/cli/src/cli.options.ts +6 -14
- package/templates/be/skeleton/apps/cli/src/compose/cli.ts +5 -0
- package/templates/be/skeleton/apps/cli/src/compose/platform.ts +24 -0
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.contracts.ts +2 -0
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.handler.ts +22 -0
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.query.ts +11 -0
- package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/api/system-health/system-health.module.ts +5 -3
- package/templates/be/skeleton/src/features/api/system-health/system-health.options.ts +2 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/ready.controller.ts +19 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module.ts +8 -3
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/cli.module.ts +3 -2
- package/templates/be/skeleton/src/features/cli/cli.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +2 -1
- package/templates/be/skeleton/src/features/cli/migrate/migrate.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +2 -1
- package/templates/be/skeleton/src/features/cli/seed/seed.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +5 -6
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +56 -4
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +36 -5
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +39 -8
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +14 -8
- package/templates/be/skeleton/src/modules/domain/identity/credential.contracts.ts +26 -0
- package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +119 -0
- package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +43 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +16 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -1
- package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +22 -4
- package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +8 -2
- package/templates/be/skeleton/src/modules/domain/identity/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +11 -4
- package/templates/be/skeleton/src/modules/platform/database/database-probe.service.spec.ts +32 -0
- package/templates/be/skeleton/src/modules/platform/database/database-probe.service.ts +14 -0
- package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
- package/templates/be/skeleton/src/modules/platform/database/index.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +8 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +25 -6
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +16 -6
- package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +130 -0
- package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +58 -0
- package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +3 -0
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -4
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -10
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +3 -1
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/http-security/index.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +12 -5
- package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +13 -8
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.contracts.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.decorators.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +86 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +39 -0
- package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +139 -131
- package/templates/be/skeleton/src/modules/platform/i18n/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +91 -67
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +5 -4
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +0 -2
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +20 -6
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +16 -4
- package/templates/be/skeleton/src/modules/platform/retry/index.ts +3 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.port.ts +11 -0
- package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.spec.ts +36 -0
- package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.ts +10 -0
- package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +54 -0
- package/templates/be/skeleton-lite/apps/api/src/app.module.ts +14 -18
- package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +4 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +10 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +2 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/system.ts +5 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/webhooks.ts +4 -0
- package/templates/be/skeleton-lite/apps/api/src/main.ts +2 -3
- package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +71 -5
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +29 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +68 -15
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +14 -9
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +9 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +16 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +19 -4
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +5 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -1
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -3
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +17 -5
- package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +24 -13
- package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +5 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/errors/database.error.ts +16 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +3 -0
- package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +5 -2
- package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +4 -1
- package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +7 -0
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +29 -0
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +16 -0
- package/templates/be/table/api/feature.module-definition.ts.tpl +6 -0
- package/templates/be/table/api/feature.module.ts.tpl +2 -1
- package/templates/be/table/api/feature.options.ts.tpl +2 -0
- package/templates/be/table/api/graphql.module-definition.ts.tpl +6 -0
- package/templates/be/table/api/graphql.module.ts.tpl +3 -2
- package/templates/be/table/api/graphql.options.ts.tpl +2 -0
- package/templates/be/table/migration.sql.tpl +1 -0
- package/templates/be/upgrade-full/apps/cli/src/app.module.ts +4 -6
- package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +7 -0
- package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +5 -0
- package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +9 -0
- package/templates/be/upgrade-full/apps/cli/src/main.ts +3 -0
- package/templates/be/upgrade-full/src/features/cli/cli.module.ts +3 -2
- package/templates/fe/auth/supabase-client.ts.tpl +34 -0
- package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +11 -0
- package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/index.tsx +6 -1
- package/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx +4 -0
- package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +31 -0
- package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/index.tsx +24 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -1
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +3 -4
- package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +15 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +18 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +19 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +7 -1
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/en.json +57 -0
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +6 -0
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/en.json +31 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +1 -1
- package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -0
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +1 -34
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +7 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +57 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +30 -24
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +1 -1
- package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/locale.ts +2 -2
- package/upgrade/check.mjs +3 -1
- package/upgrade/index.mjs +54 -41
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// R271: a product release keeps its v<semver> tag, root package version, image tags and database deployment ref together.
|
|
2
|
+
// Ordinary untagged development trees have no release version to compare. Workflow checks still run there in both editions.
|
|
3
|
+
// YAML is parsed, so comments and unrelated jobs cannot supply a guard. Shell guards are deliberately a small, explicit form;
|
|
4
|
+
// opaque helper scripts and computed image metadata cannot prove provenance in this static check.
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { parseYaml } from '../../../engine/yaml.mjs';
|
|
8
|
+
import { isPlainObject } from '../../../engine/plain-object.mjs';
|
|
9
|
+
import { revParseQuery } from '../../api/git/rev-parse-query.mjs';
|
|
10
|
+
import { tag } from '../../api/git/tag.mjs';
|
|
11
|
+
import { gitOutputOf, withoutGitLocalEnv } from '../../lib/git.mjs';
|
|
12
|
+
import { found, readJson, readText } from './read.mjs';
|
|
13
|
+
|
|
14
|
+
export const HFS_RELEASE_TAG_MISMATCH = 'HFS_RELEASE_TAG_MISMATCH';
|
|
15
|
+
export const HFS_DB_DEPLOY_UNGUARDED = 'HFS_DB_DEPLOY_UNGUARDED';
|
|
16
|
+
const SEMVER = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
17
|
+
const WORKFLOW = /^\.github\/workflows\/(images|db-deploy)\.ya?ml$/;
|
|
18
|
+
const TAG = 1;
|
|
19
|
+
const VERSION_PREFIX = 2;
|
|
20
|
+
const RELEASE_REF = TAG | VERSION_PREFIX;
|
|
21
|
+
|
|
22
|
+
function gitTags(repoRoot, env) {
|
|
23
|
+
const options = { cwd: repoRoot, env: withoutGitLocalEnv(env), encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000, maxBuffer: 64 * 1024, windowsHide: true };
|
|
24
|
+
try {
|
|
25
|
+
// A nested example belongs to its own product, never to an ancestor runtime's tags.
|
|
26
|
+
const top = gitOutputOf(revParseQuery(['--show-toplevel'], options)).trim();
|
|
27
|
+
if (path.relative(fs.realpathSync(top), fs.realpathSync(repoRoot)) !== '') return [];
|
|
28
|
+
const tags = gitOutputOf(tag(['--points-at', 'HEAD'], options)).trim().split(/\r?\n/).filter((value) => value.startsWith('v'));
|
|
29
|
+
if (env.GITHUB_ACTIONS === 'true' && env.GITHUB_REF?.startsWith('refs/tags/v')) tags.push(env.GITHUB_REF.slice('refs/tags/'.length));
|
|
30
|
+
return [...new Set(tags)];
|
|
31
|
+
} catch { return []; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function tagFindings(repoRoot, version, env) {
|
|
35
|
+
return gitTags(repoRoot, env).filter((tag) => !SEMVER.test(tag.slice(1)) || tag.slice(1) !== version)
|
|
36
|
+
.map((tag) => found(HFS_RELEASE_TAG_MISMATCH, 'package.json', 'the release tag at HEAD must be v<semver> and equal the root package.json version; align the version before cutting the release', { tag, version: typeof version === 'string' ? version : null }));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Split only outside strings/function calls; arbitrary expressions remain unproved atoms.
|
|
40
|
+
function splitBoolean(expression, operator) {
|
|
41
|
+
const parts = [];
|
|
42
|
+
let start = 0, depth = 0, quote = '';
|
|
43
|
+
for (let i = 0; i < expression.length; i += 1) {
|
|
44
|
+
const char = expression[i];
|
|
45
|
+
if (quote) { if (char === quote) quote = ''; continue; }
|
|
46
|
+
if (char === "'" || char === '"') { quote = char; continue; }
|
|
47
|
+
if (char === '(') depth += 1;
|
|
48
|
+
if (char === ')') depth -= 1;
|
|
49
|
+
if (depth === 0 && expression.slice(i, i + 2) === operator) { parts.push(expression.slice(start, i)); start = i + 2; i += 1; }
|
|
50
|
+
}
|
|
51
|
+
parts.push(expression.slice(start));
|
|
52
|
+
return parts;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function enclosingParentheses(text) {
|
|
56
|
+
let depth = 0, quote = '';
|
|
57
|
+
for (let i = 0; i < text.length - 1; i += 1) {
|
|
58
|
+
const char = text[i];
|
|
59
|
+
if (quote) { if (char === quote) quote = ''; continue; }
|
|
60
|
+
if (char === "'" || char === '"') { quote = char; continue; }
|
|
61
|
+
if (char === '(') depth += 1;
|
|
62
|
+
if (char === ')' && --depth === 0) return false;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function unwrap(expression) {
|
|
68
|
+
let text = expression.trim();
|
|
69
|
+
while (text.startsWith('(') && text.endsWith(')') && enclosingParentheses(text)) text = text.slice(1, -1).trim();
|
|
70
|
+
return text;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function workflowExpression(value) {
|
|
74
|
+
const text = value.trim();
|
|
75
|
+
return text.startsWith('${{') && text.endsWith('}}') ? text.slice(3, -2).trim() : text;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function refProof(value) {
|
|
79
|
+
if (typeof value !== 'string') return 0;
|
|
80
|
+
const expression = unwrap(workflowExpression(value));
|
|
81
|
+
const alternatives = splitBoolean(expression, '||');
|
|
82
|
+
if (alternatives.length > 1) return alternatives.map(refProof).reduce((a, b) => a & b);
|
|
83
|
+
const conjuncts = splitBoolean(expression, '&&');
|
|
84
|
+
if (conjuncts.length > 1) return conjuncts.map(refProof).reduce((a, b) => a | b, 0);
|
|
85
|
+
if (/^startsWith\(\s*github\.ref\s*,\s*(['"])refs\/tags\/v\1\s*\)$/.test(expression)) return RELEASE_REF;
|
|
86
|
+
const exact = /^github\.ref\s*==\s*(['"])refs\/tags\/v([^'"\s]+)\1$/.exec(expression);
|
|
87
|
+
if (exact && SEMVER.test(exact[2])) return RELEASE_REF;
|
|
88
|
+
if (/^(?:github\.ref_type\s*==\s*(['"])tag\1|(['"])tag\2\s*==\s*github\.ref_type)$/.test(expression)) return TAG;
|
|
89
|
+
if (/^startsWith\(\s*github\.ref_name\s*,\s*(['"])v\1\s*\)$/.test(expression)) return VERSION_PREFIX;
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function tagOnlyTrigger(workflow) {
|
|
94
|
+
const events = workflow.on;
|
|
95
|
+
if (!events || typeof events !== 'object' || Array.isArray(events) || Object.keys(events).join() !== 'push') return false;
|
|
96
|
+
const push = events.push;
|
|
97
|
+
if (!push || typeof push !== 'object' || push.branches || push['branches-ignore'] || push['tags-ignore']) return false;
|
|
98
|
+
const tags = Array.isArray(push.tags) ? push.tags : [];
|
|
99
|
+
const included = tags.filter((tag) => typeof tag === 'string' && !tag.startsWith('!'));
|
|
100
|
+
return included.length > 0 && included.every((tag) => tag.startsWith('v'));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function shellTagGuard(step) {
|
|
104
|
+
if (step.if !== undefined || step['continue-on-error'] || typeof step.run !== 'string') return false;
|
|
105
|
+
const command = step.run.trim().replace(/^set -[a-z]+(?:\s+pipefail)?\s*\n/, '').trim();
|
|
106
|
+
// The whole guard step must reject a non-release ref; a nested, commented or later guard is insufficient.
|
|
107
|
+
return /^\[\[\s*["']?\$(?:GITHUB_REF|\{GITHUB_REF\})["']?\s*==\s*refs\/tags\/v\*\s*\]\]\s*\|\|\s*exit\s+[1-9]\d*\s*;?$/.test(command)
|
|
108
|
+
|| /^case\s+["']?\$(?:GITHUB_REF|\{GITHUB_REF\})["']?\s+in\s+refs\/tags\/v\*\)\s*;;\s*\*\)\s*exit\s+[1-9]\d*\s*;;\s*esac\s*;?$/.test(command);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function checkoutPreservesRef(job) {
|
|
112
|
+
return (job.steps ?? []).filter((step) => /^actions\/checkout@/.test(step.uses ?? '')).every((step) => {
|
|
113
|
+
const ref = step.with?.ref;
|
|
114
|
+
return ref === undefined || /^\$\{\{\s*github\.(?:ref|ref_name|sha)\s*\}\}$/.test(String(ref));
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function setupStep(step) {
|
|
119
|
+
return /^(?:actions\/checkout|actions\/setup-node)@/.test(step.uses ?? '')
|
|
120
|
+
|| (typeof step.run === 'string' && /^npm ci --ignore-scripts\s*$/.test(step.run.trim()));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function unguardedDeployStep(step, shellGuarded, guarded) {
|
|
124
|
+
// Checkout, Node setup and script-disabled npm install may precede the guard. Opaque commands may deploy.
|
|
125
|
+
// A later always()/failure() step can run despite the rejecting shell guard's failure.
|
|
126
|
+
const shellConstrained = shellGuarded && !/\b(?:always|failure|cancelled)\s*\(/.test(String(step.if ?? ''));
|
|
127
|
+
return (step.run || step.uses) && !setupStep(step) && !guarded && !shellConstrained && refProof(step.if) !== RELEASE_REF;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function dbJobConstrained(job, guarded) {
|
|
131
|
+
let shellGuarded = false;
|
|
132
|
+
let constrained = !job.uses || guarded;
|
|
133
|
+
for (const step of job.steps ?? []) {
|
|
134
|
+
if (shellTagGuard(step)) { shellGuarded = true; continue; }
|
|
135
|
+
if (unguardedDeployStep(step, shellGuarded, guarded)) constrained = false;
|
|
136
|
+
}
|
|
137
|
+
if (!job.uses && !(job.steps ?? []).some((step) => step.run || step.uses)) constrained = false;
|
|
138
|
+
return constrained;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function dbFindings(file, workflow) {
|
|
142
|
+
const jobs = Object.entries(workflow.jobs ?? {});
|
|
143
|
+
if (!jobs.length) return [found(HFS_DB_DEPLOY_UNGUARDED, file, `${file} has no verifiable database deploy job; require a release tag before migration`)];
|
|
144
|
+
const findings = [];
|
|
145
|
+
for (const [name, job] of jobs) {
|
|
146
|
+
if (!job || typeof job !== 'object') continue;
|
|
147
|
+
const guarded = tagOnlyTrigger(workflow) || refProof(job.if) === RELEASE_REF;
|
|
148
|
+
const constrained = dbJobConstrained(job, guarded);
|
|
149
|
+
if (!constrained || !checkoutPreservesRef(job)) findings.push(found(HFS_DB_DEPLOY_UNGUARDED, file, `${file} job ${name} can deploy a non-release ref; require refs/tags/v at the job or before any deploy command, and check out that ref`, { job: name }));
|
|
150
|
+
}
|
|
151
|
+
return findings;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function validJob(job) {
|
|
155
|
+
return isPlainObject(job) && (job.steps === undefined || (Array.isArray(job.steps) && job.steps.every(isPlainObject)));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function validWorkflow(workflow) {
|
|
159
|
+
return isPlainObject(workflow) && isPlainObject(workflow.jobs) && Object.values(workflow.jobs).every(validJob);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function imageTagIsRelease(value, version) {
|
|
163
|
+
if (typeof value !== 'string') return false;
|
|
164
|
+
const tags = value.split(/[\n,]+/).map((tag) => tag.trim()).filter(Boolean);
|
|
165
|
+
return tags.length > 0 && tags.every((tag) => {
|
|
166
|
+
const ref = tag.replace(/\$\{\{\s*github\.ref_name\s*\}\}/g, '<release-ref>');
|
|
167
|
+
if (ref.endsWith(':<release-ref>')) return true;
|
|
168
|
+
return SEMVER.test(String(version)) && ref.endsWith(`:v${version}`);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function imageFindings(file, workflow, version) {
|
|
173
|
+
const findings = [];
|
|
174
|
+
let builds = 0;
|
|
175
|
+
for (const [name, job] of Object.entries(workflow.jobs ?? {})) {
|
|
176
|
+
for (const step of job?.steps ?? []) {
|
|
177
|
+
if (!/^docker\/build-push-action@/.test(step.uses ?? '')) continue;
|
|
178
|
+
builds += 1;
|
|
179
|
+
const guarded = tagOnlyTrigger(workflow) || refProof(job.if) === RELEASE_REF || refProof(step.if) === RELEASE_REF;
|
|
180
|
+
if (!guarded || !checkoutPreservesRef(job) || !imageTagIsRelease(step.with?.tags, version)) findings.push(found(HFS_RELEASE_TAG_MISMATCH, file, `${file} job ${name} must build the checked-out release ref and tag every image with v<package.json version> or github.ref_name under a release-tag guard`, { job: name }));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (!builds) findings.push(found(HFS_RELEASE_TAG_MISMATCH, file, `${file} has no verifiable release image tags; declare docker/build-push-action tags from the release ref`));
|
|
184
|
+
return findings;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function productRoot(repo, isRoot) {
|
|
188
|
+
return isRoot !== false && !['be', 'fe', 'runtime'].includes(repo.profile)
|
|
189
|
+
&& (!repo.kind || repo.kind === 'app') && ['lite', 'full'].includes(repo.edition ?? 'full');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Synchronous product-root provenance findings; root and workflows are judged in lite and full, never a side or runtime. */
|
|
193
|
+
export function releaseProvenanceFindings({ repoRoot, files = [], repo = {}, isRoot, inScope = () => true, env = process.env } = {}) {
|
|
194
|
+
if (!productRoot(repo, isRoot)) return [];
|
|
195
|
+
const version = readJson(repoRoot, 'package.json')?.version;
|
|
196
|
+
const packageInScope = files.includes('package.json') && inScope('package.json');
|
|
197
|
+
const findings = packageInScope ? tagFindings(repoRoot, version, env) : [];
|
|
198
|
+
for (const file of files) {
|
|
199
|
+
const match = WORKFLOW.exec(file);
|
|
200
|
+
if (!match || (!inScope(file) && !packageInScope)) continue;
|
|
201
|
+
const code = match[1] === 'images' ? HFS_RELEASE_TAG_MISMATCH : HFS_DB_DEPLOY_UNGUARDED;
|
|
202
|
+
let workflow;
|
|
203
|
+
try { workflow = parseYaml(readText(repoRoot, file) ?? ''); } catch { /* unreadable YAML cannot prove a release */ }
|
|
204
|
+
if (!validWorkflow(workflow)) {
|
|
205
|
+
findings.push(found(code, file, `${file} cannot be parsed as a workflow; restore its release tag checks`));
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
findings.push(...(match[1] === 'images' ? imageFindings(file, workflow, version) : dbFindings(file, workflow)));
|
|
209
|
+
}
|
|
210
|
+
return findings;
|
|
211
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Trace the delivery parameter into command constructor arguments, rather than arbitrary reads.
|
|
2
|
+
import { accessName, isConstVariable } from '../../lib/ts-ast.mjs';
|
|
3
|
+
import { sourceValue } from './database-queue-sources.mjs';
|
|
4
|
+
|
|
5
|
+
function unwrap(ts, node) {
|
|
6
|
+
const value = sourceValue(ts, node);
|
|
7
|
+
return value && ts.isNonNullExpression(value) ? unwrap(ts, value.expression) : value;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function resolve(ts, node, aliases, seen = new Set()) {
|
|
11
|
+
const value = unwrap(ts, node);
|
|
12
|
+
if (!value || !ts.isIdentifier(value) || !aliases.has(value.text) || seen.has(value.text)) return value;
|
|
13
|
+
return resolve(ts, aliases.get(value.text), aliases, new Set(seen).add(value.text));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function carriesKey(ts, node, delivery, aliases, seen = new Set()) {
|
|
17
|
+
const value = resolve(ts, node, aliases);
|
|
18
|
+
if (!value || seen.has(value)) return false;
|
|
19
|
+
const next = new Set(seen).add(value);
|
|
20
|
+
if (accessName(ts, value) === 'eventId') {
|
|
21
|
+
const receiver = resolve(ts, value.expression, aliases);
|
|
22
|
+
return ts.isIdentifier(receiver) && receiver.text === delivery;
|
|
23
|
+
}
|
|
24
|
+
if (ts.isObjectLiteralExpression(value)) return value.properties.some((property) => carriesKey(ts, property, delivery, aliases, next));
|
|
25
|
+
if (ts.isPropertyAssignment(value)) return carriesKey(ts, value.initializer, delivery, aliases, next);
|
|
26
|
+
if (ts.isShorthandPropertyAssignment(value)) return carriesKey(ts, value.name, delivery, aliases, next);
|
|
27
|
+
if (ts.isArrayLiteralExpression(value)) return value.elements.some((element) => carriesKey(ts, element, delivery, aliases, next));
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function localAliases(ts, method) {
|
|
32
|
+
const aliases = new Map();
|
|
33
|
+
for (const statement of method.body?.statements ?? []) {
|
|
34
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
35
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
36
|
+
if (ts.isIdentifier(declaration.name) && declaration.initializer && isConstVariable(ts, declaration)) aliases.set(declaration.name.text, declaration.initializer);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return aliases;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function methodForwards(ts, method) {
|
|
43
|
+
const parameter = method.parameters[0]?.name;
|
|
44
|
+
if (!parameter || !ts.isIdentifier(parameter)) return false;
|
|
45
|
+
const aliases = localAliases(ts, method);
|
|
46
|
+
const commands = [];
|
|
47
|
+
const visit = (node) => {
|
|
48
|
+
// A read in a nested function does not forward the handler's delivery.
|
|
49
|
+
if (ts.isFunctionLike(node)) return;
|
|
50
|
+
if (ts.isCallExpression(node) && accessName(ts, node.expression) === 'execute') commands.push(resolve(ts, node.arguments[0], aliases));
|
|
51
|
+
ts.forEachChild(node, visit);
|
|
52
|
+
};
|
|
53
|
+
if (method.body) visit(method.body);
|
|
54
|
+
return commands.length > 0 && commands.every((command) => command && ts.isNewExpression(command)
|
|
55
|
+
&& (command.arguments ?? []).some((argument) => carriesKey(ts, argument, parameter.text, aliases)));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function forwardsDeliveryKey(ts, source) {
|
|
59
|
+
const handlers = [];
|
|
60
|
+
const visit = (node) => {
|
|
61
|
+
if (ts.isMethodDeclaration(node) && node.name.getText(source) === 'handle') handlers.push(node);
|
|
62
|
+
else ts.forEachChild(node, visit);
|
|
63
|
+
};
|
|
64
|
+
visit(source);
|
|
65
|
+
return handlers.length > 0 && handlers.every((method) => methodForwards(ts, method));
|
|
66
|
+
}
|
|
@@ -20,6 +20,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
20
20
|
import { loadTypescript } from '../../lib/package-at.mjs';
|
|
21
21
|
import { found, readJson, readText } from './read.mjs';
|
|
22
22
|
import { byCodeUnit } from '../../lib/list.mjs';
|
|
23
|
+
import { forwardsDeliveryKey } from './saga-delivery.mjs';
|
|
23
24
|
|
|
24
25
|
const SAGA_STEP_COMPENSATION = 'BE_SAGA_STEP_COMPENSATION';
|
|
25
26
|
const SAGA_STATE_VERSIONED = 'BE_SAGA_STATE_VERSIONED';
|
|
@@ -81,17 +82,6 @@ function statesVersion(ts, sourceFile) {
|
|
|
81
82
|
return versioned;
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
/** Whether a source text reads `.eventId` of something (the id of the delivery travels on). */
|
|
85
|
-
function readsEventId(ts, sourceFile) {
|
|
86
|
-
let read = false;
|
|
87
|
-
const visit = (node) => {
|
|
88
|
-
if (ts.isPropertyAccessExpression(node) && node.name.text === 'eventId') read = true;
|
|
89
|
-
ts.forEachChild(node, visit);
|
|
90
|
-
};
|
|
91
|
-
visit(sourceFile);
|
|
92
|
-
return read;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
85
|
/** The sagas of the app: every folder of the saga kind (`be/src/features/saga/<saga>/`). */
|
|
96
86
|
const featuresOf = (files) => [...new Set(files.map((file) => SAGA_FILE.exec(file)?.[1]).filter((feature) => feature !== undefined))].sort(byCodeUnit);
|
|
97
87
|
|
|
@@ -186,7 +176,7 @@ function consumerFindings(ts, repoRoot, feature, files) {
|
|
|
186
176
|
const findings = [];
|
|
187
177
|
for (const file of filesForFeature(files, CONSUMER, feature)) {
|
|
188
178
|
const text = readText(repoRoot, file);
|
|
189
|
-
if (text !== null && !
|
|
179
|
+
if (text !== null && !forwardsDeliveryKey(ts, parse(ts, text))) findings.push(found(SAGA_CONSUMER_DEDUPE, file, `${file} is a consumer of the saga ${feature} but does not pass the delivery id (\`message.eventId\`) to every dispatched command; the saga takes every event through the inbox, so the consumer hands the id to its command.`, { feature }));
|
|
190
180
|
}
|
|
191
181
|
return findings;
|
|
192
182
|
}
|
|
@@ -214,7 +204,7 @@ function compensationE2eFindings(compensationPaths, specs) {
|
|
|
214
204
|
}
|
|
215
205
|
|
|
216
206
|
/** Findings of R169 to R173 over the tracked paths `files` of the app at `repoRoot`. */
|
|
217
|
-
export function sagaFindings({ repoRoot, files }) {
|
|
207
|
+
export function sagaFindings({ repoRoot, files, repo }) {
|
|
218
208
|
const features = featuresOf(files);
|
|
219
209
|
if (features.length === 0) return [];
|
|
220
210
|
const ts = typescriptFor(repoRoot);
|
|
@@ -223,7 +213,6 @@ export function sagaFindings({ repoRoot, files }) {
|
|
|
223
213
|
for (const contract of files.filter((file) => CONTRACT.test(file))) {
|
|
224
214
|
for (const [name, event] of Object.entries(readJson(repoRoot, contract)?.events ?? {})) events.set(name, { contract, ...event });
|
|
225
215
|
}
|
|
226
|
-
const specs = files.filter((file) => E2E_SPEC.test(file)).map((file) => ({ file, text: readText(repoRoot, file) ?? '' })).filter(({ text }) => USE_TEST_WORLD.test(text));
|
|
227
216
|
const findings = [];
|
|
228
217
|
const compensationPaths = [];
|
|
229
218
|
for (const feature of features) {
|
|
@@ -231,7 +220,10 @@ export function sagaFindings({ repoRoot, files }) {
|
|
|
231
220
|
findings.push(...result.findings);
|
|
232
221
|
compensationPaths.push(...result.compensationPaths);
|
|
233
222
|
}
|
|
234
|
-
// R173
|
|
235
|
-
|
|
223
|
+
// R173 is the full-edition test-world proof; lite keeps R169-R172 and proves its scenarios through R284.
|
|
224
|
+
if (repo?.edition !== 'lite') {
|
|
225
|
+
const specs = files.filter((file) => E2E_SPEC.test(file)).map((file) => ({ file, text: readText(repoRoot, file) ?? '' })).filter(({ text }) => USE_TEST_WORLD.test(text));
|
|
226
|
+
findings.push(...compensationE2eFindings(compensationPaths, specs));
|
|
227
|
+
}
|
|
236
228
|
return findings;
|
|
237
229
|
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// R252: inspect executable source for plaintext loads, tracing and service-role custody.
|
|
2
|
+
// Only source is read. Secret files and sealed envelopes are excluded before any read,
|
|
3
|
+
// and findings carry a path and a fixed explanation, never source text or values.
|
|
4
|
+
import { found, readJson, readText } from './read.mjs';
|
|
5
|
+
|
|
6
|
+
const PLAINTEXT_LOAD = 'HFS_SCRIPT_PLAINTEXT_LOAD';
|
|
7
|
+
const XTRACE = 'HFS_SCRIPT_XTRACE';
|
|
8
|
+
const SERVICE_ROLE = 'HFS_SERVICE_ROLE_SCOPE';
|
|
9
|
+
const SUPABASE_OWNER = 'be.integrations.supabase';
|
|
10
|
+
const SECRET_FILE = /(?:^|\/)(?:secrets?\.env(?:\.[^/]*)?|\.env(?:\.[^/]*)?|\.nivo-lite(?:\/|$))|\.enc$/iu;
|
|
11
|
+
const EXCLUDED_TREE = /(?:^|\/)(?:node_modules|\.git|\.claude|\.secrets|secrets|tests?|__tests__|docs|coverage|dist|build)(?:\/|$)/u;
|
|
12
|
+
const TEST_FILE = /(?:\.(?:spec|test)|-spec)\.[cm]?[jt]sx?$/u;
|
|
13
|
+
const SOURCE = /\.(?:[cm]?[jt]sx?|sh|bash|zsh|ksh|ps1|psm1|bat|cmd|py|rb|pl)$/iu;
|
|
14
|
+
const WORKFLOW = /(?:^|\/)\.?(?:github\/)?workflows\/[^/]+\.ya?ml$/u;
|
|
15
|
+
const DELIVERY = /(?:^|\/)(?:deploy|scripts|bin)\//u;
|
|
16
|
+
const DOCKER = /(?:^|\/)(?:Dockerfile(?:\.[^/]*)?|(?:docker-)?compose(?:[.-][^/]*)?\.ya?ml)$/iu;
|
|
17
|
+
const SECRET_REFERENCE = /(?:^|[\s/\\'"`(=])(?:secrets?\.env(?:\.[\w.-]+)?|\.env(?:\.[\w.-]+)?|\.nivo-lite(?:[/\\][\w./\\-]*)?)(?=$|[\s/\\'"`),;])/iu;
|
|
18
|
+
const EXAMPLE_REFERENCE = /(?:secrets?\.env|\.env)\.(?:example|sample|template)(?=$|[^\w.-])/giu;
|
|
19
|
+
const LOAD = /(?:^|[\s;&|('"`])(?:source|cat|tac|less|more|head|tail|grep|egrep|fgrep|awk|gawk|sed|base64|Get-Content|gc|Import-DotEnv|env-cmd|load_dotenv|loadEnvFile|readFile(?:Sync)?|readTextFile(?:Sync)?|ReadAllText|ReadAllLines|dotenv\s*\.\s*(?:config|parse))\b|\b(?:File|IO)\s*\.\s*(?:read|readlines)\b|\.\s*read_(?:text|bytes)\s*\(|(?:^|[\s;&|])\.[\t ]+|(?<!<)<(?!<)/iu;
|
|
20
|
+
const READ_CALL = /\b(?:readFile(?:Sync)?|readTextFile(?:Sync)?|ReadAllText|ReadAllLines|load_dotenv|loadEnvFile|dotenv\s*\.\s*(?:config|parse))\s*\(/giu;
|
|
21
|
+
const TRACE_COMMAND = /(?:^|[\s;&|('"`]|#!)(?:set|\/?(?:[\w.-]+\/)*(?:bash|sh|dash|zsh|ksh))\s+/giu;
|
|
22
|
+
const SHELL_OPTIONS = /\bSHELLOPTS\s*=\s*['"]?([^\s;'"]*)/giu;
|
|
23
|
+
const SERVICE_ROLE_NAME = /(?:^|[^a-z0-9])(?:[a-z0-9]+[_-])*service[_-]?role(?:[_-][a-z0-9]+)*(?=$|[^a-z0-9])/iu;
|
|
24
|
+
|
|
25
|
+
/** Retain executable strings, including remote shell commands, but discard comment-only lines. */
|
|
26
|
+
function executableText(text) {
|
|
27
|
+
return text.replace(/\/\*[\s\S]*?\*\//gu, '').split(/\r?\n/u)
|
|
28
|
+
.filter((line) => !/^\s*(?:#(?!!)|\/\/)/u.test(line)).join('\n')
|
|
29
|
+
.replace(/(?:\\|`)\r?\n/gu, ' ');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const hasSecretReference = (text) => SECRET_REFERENCE.test(text.replace(EXAMPLE_REFERENCE, 'example-file'));
|
|
33
|
+
|
|
34
|
+
function callArguments(text, from) {
|
|
35
|
+
let depth = 1;
|
|
36
|
+
let quote = '';
|
|
37
|
+
for (let at = from; at < Math.min(text.length, from + 1024); at += 1) {
|
|
38
|
+
const character = text[at];
|
|
39
|
+
if (character === '\\') { at += 1; continue; }
|
|
40
|
+
if (quote) { if (character === quote) quote = ''; continue; }
|
|
41
|
+
if (['\'', '"', '`'].includes(character)) { quote = character; continue; }
|
|
42
|
+
if (character === '(') depth += 1;
|
|
43
|
+
if (character === ')' && --depth === 0) return text.slice(from, at);
|
|
44
|
+
}
|
|
45
|
+
return '';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A helper may assign a path first and source/read the variable on the next line. */
|
|
49
|
+
function secretAliases(lines) {
|
|
50
|
+
const aliases = [];
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
const equals = line.indexOf('=');
|
|
53
|
+
if (equals < 0 || !hasSecretReference(line.slice(equals + 1))) continue;
|
|
54
|
+
const name = line.slice(0, equals).trim().replace(/^(?:export|const|let|var)\s+/iu, '').replace(/^\$/u, '');
|
|
55
|
+
if (/^[a-z_]\w*$/iu.test(name)) aliases.push(new RegExp(String.raw`\b${name}\b`, 'u'));
|
|
56
|
+
}
|
|
57
|
+
return aliases;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** JS, .NET and dotenv readers can put their filename on a later line of the call. */
|
|
61
|
+
function multilineLoad(text, secretReference) {
|
|
62
|
+
for (const match of text.matchAll(READ_CALL)) {
|
|
63
|
+
const args = callArguments(text, match.index + match[0].length);
|
|
64
|
+
if (secretReference(args)) return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function pythonLoad(text) {
|
|
70
|
+
// Python's default open mode is reading; explicit write/append modes are not loads.
|
|
71
|
+
for (const match of text.matchAll(/\bopen\s*\(/gu)) {
|
|
72
|
+
const args = callArguments(text, match.index + match[0].length).split(',').map((value) => value.trim());
|
|
73
|
+
const filename = /^(['"])([^'"\r\n]+)\1$/u.exec(args[0]);
|
|
74
|
+
const mode = /^(['"])([^'"]*)\1$/u.exec(args[1] ?? '');
|
|
75
|
+
if (filename && (!args[1] || mode && /^[rU]/u.test(mode[2])) && hasSecretReference(filename[2])) return true;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function plaintextLoad(text) {
|
|
81
|
+
const lines = text.split(/\r?\n/u);
|
|
82
|
+
const aliases = secretAliases(lines);
|
|
83
|
+
const secretReference = (value) => hasSecretReference(value) || aliases.some((alias) => alias.test(value));
|
|
84
|
+
return lines.some((line) => LOAD.test(line) && secretReference(line)) || multilineLoad(text, secretReference) || pythonLoad(text);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function tracingOptions(source) {
|
|
88
|
+
let previous = '';
|
|
89
|
+
for (const [token] of source.matchAll(/[^\s;'"})`]+/gu)) {
|
|
90
|
+
if (!token.startsWith('-')) {
|
|
91
|
+
if (!previous || !/^[\w-]+$/u.test(token)) return false;
|
|
92
|
+
if (/^-o$/iu.test(previous) && /^xtrace$/iu.test(token)) return true;
|
|
93
|
+
previous = '';
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (!/^--?[\w-]+$/u.test(token)) return false;
|
|
97
|
+
if (/^-[a-z]+$/iu.test(token) && token.toLowerCase().includes('x')) return true;
|
|
98
|
+
previous = token;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Parse shell options individually so repeated flags cannot create regex backtracking. */
|
|
104
|
+
function enablesTracing(text) {
|
|
105
|
+
if (/--reveal\b/iu.test(text)) return true;
|
|
106
|
+
for (const match of text.matchAll(SHELL_OPTIONS)) {
|
|
107
|
+
if (/\bxtrace\b/iu.test(match[1])) return true;
|
|
108
|
+
}
|
|
109
|
+
for (const match of text.matchAll(TRACE_COMMAND)) {
|
|
110
|
+
const from = match.index + match[0].length;
|
|
111
|
+
const newline = text.indexOf('\n', from);
|
|
112
|
+
if (tracingOptions(text.slice(from, newline < 0 ? text.length : newline))) return true;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function integrationOwns(file, resolver) {
|
|
118
|
+
if (!file.startsWith('be/src/modules/integrations/supabase/')) return false;
|
|
119
|
+
return !resolver?.ownerOf || resolver.ownerOf(file)?.slot === SUPABASE_OWNER;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isSource(file) {
|
|
123
|
+
if (SECRET_FILE.test(file) || EXCLUDED_TREE.test(file) || TEST_FILE.test(file)) return false;
|
|
124
|
+
return SOURCE.test(file) || WORKFLOW.test(file) || DOCKER.test(file)
|
|
125
|
+
|| file.endsWith('/package.json') || file === 'package.json'
|
|
126
|
+
|| (DELIVERY.test(file) && !/\.[a-z0-9]+$/iu.test(file));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function sourceText(repoRoot, file) {
|
|
130
|
+
if (file === 'package.json' || file.endsWith('/package.json')) {
|
|
131
|
+
const scripts = readJson(repoRoot, file)?.scripts;
|
|
132
|
+
return scripts && typeof scripts === 'object' ? Object.values(scripts).filter((value) => typeof value === 'string').join('\n') : null;
|
|
133
|
+
}
|
|
134
|
+
return readText(repoRoot, file);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Both editions share this custody boundary; the app root scans `all` once, including its sides. */
|
|
138
|
+
export function scriptHygieneFindings({ repoRoot, files = [], all = files, repo, resolver, isRoot, inScope = () => true }) {
|
|
139
|
+
if (repo?.kind !== 'app' || !['lite', 'full'].includes(repo.edition) || isRoot === false) return [];
|
|
140
|
+
const findings = [];
|
|
141
|
+
for (const file of new Set(all)) {
|
|
142
|
+
if (!inScope(file) || !isSource(file)) continue;
|
|
143
|
+
const raw = sourceText(repoRoot, file);
|
|
144
|
+
if (raw === null) continue;
|
|
145
|
+
const text = executableText(raw);
|
|
146
|
+
if (plaintextLoad(text)) findings.push(found(PLAINTEXT_LOAD, file, 'Executable source loads a plaintext secret file; use starci app secret with sealed envelopes.'));
|
|
147
|
+
if (enablesTracing(text)) findings.push(found(XTRACE, file, 'Executable source enables shell tracing or secret reveal; remove xtrace and --reveal.'));
|
|
148
|
+
if (SERVICE_ROLE_NAME.test(text.replace(/([a-z0-9])([A-Z])/gu, '$1_$2')) && !integrationOwns(file, resolver)) findings.push(found(SERVICE_ROLE, file, 'Service-role access belongs only to be/src/modules/integrations/supabase/ and its configuration.'));
|
|
149
|
+
}
|
|
150
|
+
return findings;
|
|
151
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// secret-custody-only.mjs - R277: the only custody a tracked secret may take is a sops envelope at
|
|
2
|
+
// `.starcistacks/<env>/secrets/**.enc`. Everything else is refused in every edition:
|
|
3
|
+
// - a tracked plaintext secret file by being (`secrets.env`, a non-dot `*.env` with secret keys, a member of
|
|
4
|
+
// a `secrets/` directory, deploy-side dotenv) - HFS_SECRET_PLAINTEXT_FILE;
|
|
5
|
+
// - an `.enc` outside the sealed home - HFS_SECRET_OUTSIDE_ENC (lite already reports it as
|
|
6
|
+
// HFS_LITE_SECRET_CUSTODY, so the lite check keeps that one code).
|
|
7
|
+
// Dotfile `.env*`, `.secrets/` and `.starcistacks/<env>/secrets/` plaintext are already refused by R06's
|
|
8
|
+
// FORBIDDEN_FILES; this rule refuses the names R06's path list does not name. A finding never carries a value.
|
|
9
|
+
import { secretHits, secretNameLike } from '../../lib/secret-patterns.mjs';
|
|
10
|
+
import { found, readText } from './read.mjs';
|
|
11
|
+
|
|
12
|
+
const PLAINTEXT_FILE = 'HFS_SECRET_PLAINTEXT_FILE';
|
|
13
|
+
const OUTSIDE_ENC = 'HFS_SECRET_OUTSIDE_ENC';
|
|
14
|
+
|
|
15
|
+
// `secrets.env` at any depth, plus `.secret.env`/`.secrets.env` - a dotenv file named for what it holds.
|
|
16
|
+
const SECRET_ENV = /(?:^|\/)\.?secrets?\.env$/i;
|
|
17
|
+
// A non-dot `<name>.env` or `<name>.env.<profile>` tracked file (deploy/prod.env, config.env.local); the
|
|
18
|
+
// documented example/template suffixes are shareable forms, not custody.
|
|
19
|
+
const ENV_FILE = /(?:^|\/)[^/.][^/]*\.env(?:\.(?!example|sample|template)[^/]*)?$/i;
|
|
20
|
+
// A plaintext secret store: `secrets/<name>.yaml` and friends - except under `.starcistacks/<env>/secrets/`,
|
|
21
|
+
// which R06 already judges, and the sealed `.enc` members, which are custody.
|
|
22
|
+
const SECRETS_DIR = /(?:^|\/)secrets\/[^/]+\.(?:ya?ml|env|json|conf|cfg|ini|toml|txt|properties)$/i;
|
|
23
|
+
const SEALED_DIR = /^\.starcistacks\/[^/]+\/secrets\//;
|
|
24
|
+
const SEALED_HOME = /^\.starcistacks\/[^/]+\/secrets\/[^/]+\.enc$/;
|
|
25
|
+
|
|
26
|
+
const KEY_LINE = /^\s*(?:export\s+)?([A-Za-z_$][\w$]*)\s*=/;
|
|
27
|
+
const hasSecretKeys = (file, text) =>
|
|
28
|
+
text.split(/\r?\n/).some((line) => {
|
|
29
|
+
const key = KEY_LINE.exec(line);
|
|
30
|
+
return (key && secretNameLike(key[1])) || secretHits(file, line).length > 0;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const sealedCustodyFindings = (file, posix, repo) => {
|
|
34
|
+
if (SEALED_HOME.test(posix) || repo.edition === 'lite') return [];
|
|
35
|
+
return [found(OUTSIDE_ENC, file,
|
|
36
|
+
`${file} is a sealed secret outside .starcistacks/<env>/secrets/; only that directory may hold committed secret envelopes`)];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function secretCustodyOnlyFindings({ repoRoot, files, repo, inScope }) {
|
|
40
|
+
const findings = [];
|
|
41
|
+
for (const file of files.filter((f) => !inScope || inScope(f))) {
|
|
42
|
+
const posix = file.replaceAll('\\', '/');
|
|
43
|
+
if (file.endsWith('.enc')) {
|
|
44
|
+
findings.push(...sealedCustodyFindings(file, posix, repo));
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (SEALED_DIR.test(posix)) continue;
|
|
48
|
+
if (SECRET_ENV.test(posix)) {
|
|
49
|
+
findings.push(found(PLAINTEXT_FILE, file,
|
|
50
|
+
`${file} is a tracked plaintext secrets dotenv; seal secrets with starci app secret set into .starcistacks/<env>/secrets/<slug>.enc - never commit the plaintext`));
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (SECRETS_DIR.test(posix)) {
|
|
54
|
+
findings.push(found(PLAINTEXT_FILE, file,
|
|
55
|
+
`${file} is a tracked plaintext file inside a secrets/ directory; seal secrets with sops into .starcistacks/<env>/secrets/<slug>.enc - never commit the plaintext`));
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!ENV_FILE.test(posix)) continue;
|
|
59
|
+
const text = readText(repoRoot, file);
|
|
60
|
+
if (text === null || !hasSecretKeys(file, text)) continue;
|
|
61
|
+
findings.push(found(PLAINTEXT_FILE, file,
|
|
62
|
+
`${file} is a tracked dotenv file holding secret keys; seal secrets with sops into .starcistacks/<env>/secrets/<slug>.enc - never commit the plaintext`));
|
|
63
|
+
}
|
|
64
|
+
return findings;
|
|
65
|
+
}
|
|
@@ -66,3 +66,16 @@ export function secretFindings({ repoRoot, files, resolver }) {
|
|
|
66
66
|
}
|
|
67
67
|
return findings;
|
|
68
68
|
}
|
|
69
|
+
|
|
70
|
+
// The age identity stays on the host: `.claude/secret.env` (SOPS_AGE_KEY) or the sops config dir's `keys.txt`,
|
|
71
|
+
// both host-local files. A tracked `secret.env`, `keys.txt` or `*.agekey` is the identity committed.
|
|
72
|
+
const AGE_KEY_FILE = /^\.claude\/secret\.env$|(?:^|\/)keys\.txt$|\.agekey$/i;
|
|
73
|
+
export function ageKeyCustodyFindings({ files, inScope }) {
|
|
74
|
+
const findings = [];
|
|
75
|
+
for (const file of files.filter((f) => !inScope || inScope(f))) {
|
|
76
|
+
if (!AGE_KEY_FILE.test(file)) continue;
|
|
77
|
+
findings.push(found('HFS_AGE_KEY_CUSTODY', file,
|
|
78
|
+
`${file} is a tracked sops/age identity file; the age key lives only in the host-local .claude/secret.env or the sops keys.txt - never commit the identity`));
|
|
79
|
+
}
|
|
80
|
+
return findings;
|
|
81
|
+
}
|
|
@@ -21,6 +21,7 @@ import { propertyText } from '../../lib/ts-ast.mjs';
|
|
|
21
21
|
import { folded, readEventClasses, snapshotText } from '../../lib/event-contract.mjs';
|
|
22
22
|
import { found, readText } from './read.mjs';
|
|
23
23
|
import { byCodeUnit } from '../../lib/list.mjs';
|
|
24
|
+
import { eventTableFindings, eventTableServices } from './event-tables.mjs';
|
|
24
25
|
|
|
25
26
|
const SERVICE_PLACEMENT = 'HFS_SERVICE_PLACEMENT';
|
|
26
27
|
const IMAGE_UNPINNED = 'HFS_IMAGE_UNPINNED';
|
|
@@ -165,11 +166,9 @@ function undeclaredCompensationFindings(classes, declared) {
|
|
|
165
166
|
.map((event) => found(EVENT_CONTRACT, event.file, `${event.file}: event "${event.name}" compensates "${event.compensates}", which no event class declares; name the event whose step it undoes.`, { service: event.service, event: event.name }));
|
|
166
167
|
}
|
|
167
168
|
|
|
168
|
-
/** Findings of R166:
|
|
169
|
+
/** Findings of R166: event classes and literal tables match their snapshots, consumers agree, and compensation targets exist. */
|
|
169
170
|
function eventContractFindings({ repoRoot, files }) {
|
|
170
|
-
const classFiles = files.filter((file) => EVENT_CLASS_FILE.test(file));
|
|
171
171
|
const snapshots = files.filter((file) => SNAPSHOT.test(file));
|
|
172
|
-
if (classFiles.length === 0 && snapshots.length === 0) return [];
|
|
173
172
|
const ts = typescriptFor(repoRoot);
|
|
174
173
|
if (ts === null) return [];
|
|
175
174
|
const tracked = new Set(files);
|
|
@@ -177,7 +176,8 @@ function eventContractFindings({ repoRoot, files }) {
|
|
|
177
176
|
const findings = problems.map(({ service, file, problem }) => found(EVENT_CONTRACT, file, `${file} is not an event class the contract can be emitted from: ${problem}.`, { service }));
|
|
178
177
|
const services = [...new Set(classes.map((event) => event.service))].sort(byCodeUnit);
|
|
179
178
|
const declared = new Set(classes.map((event) => event.name));
|
|
180
|
-
|
|
179
|
+
const providers = [...services, ...eventTableServices(files)];
|
|
180
|
+
findings.push(...snapshotDriftFindings(repoRoot, tracked, services, classes), ...leftBehindSnapshotFindings(snapshots, providers, problems), ...undeclaredCompensationFindings(classes, declared), ...eventTableFindings({ ts, repoRoot, files }));
|
|
181
181
|
return findings;
|
|
182
182
|
}
|
|
183
183
|
|