@starci/hfs 4.3.0 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/db/migrate.mjs +361 -0
- package/db/post-cutover.mjs +136 -0
- package/emit/db-types.mjs +10 -1
- package/lint/run.mjs +1 -1
- package/package.json +4 -1
- package/release/gate.mjs +298 -0
- package/release/index.mjs +103 -0
- package/release/push.mjs +110 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
- package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
- package/runtime/knowledge/hfs/rules.yaml +3401 -152
- package/runtime/knowledge/hfs/slots.yaml +344 -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 +3113 -43
- package/runtime/modules/models/runtimes.yaml +1 -1
- package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
- package/runtime/packages/cli/src/catalog.generated.mjs +14104 -0
- package/runtime/packages/cli/src/validate-args.mjs +213 -0
- package/runtime/scripts/api/git/add.mjs +5 -0
- package/runtime/scripts/api/git/init.mjs +7 -0
- package/runtime/scripts/api/git/lib.mjs +3 -0
- package/runtime/scripts/api/git/tag.mjs +5 -0
- package/runtime/scripts/api/node/lib.mjs +20 -0
- package/runtime/scripts/api/node/run-node.mjs +6 -0
- package/runtime/scripts/api/sops/lib.mjs +41 -0
- package/runtime/scripts/api/sops/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/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/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 +3 -3
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +8 -2
- 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/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 +67 -7
- package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
- package/runtime/scripts/hfs/rule-catalog.mjs +156 -11
- package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
- package/runtime/scripts/hfs/rules/a15-findings.mjs +88 -0
- package/runtime/scripts/hfs/rules/app-env-required.mjs +20 -0
- package/runtime/scripts/hfs/rules/app-root.mjs +2 -2
- package/runtime/scripts/hfs/rules/assets.mjs +79 -0
- package/runtime/scripts/hfs/rules/boot-proof.mjs +16 -0
- package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +59 -0
- package/runtime/scripts/hfs/rules/contract.mjs +66 -5
- package/runtime/scripts/hfs/rules/coverage-below.mjs +230 -0
- package/runtime/scripts/hfs/rules/database-access.mjs +343 -0
- package/runtime/scripts/hfs/rules/database-baseline.mjs +142 -0
- package/runtime/scripts/hfs/rules/database-config.mjs +250 -2
- package/runtime/scripts/hfs/rules/database-constants.mjs +9 -0
- package/runtime/scripts/hfs/rules/database-migrations.mjs +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 +1 -1
- package/runtime/scripts/hfs/rules/supply-chain.mjs +167 -0
- package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +101 -0
- package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +77 -0
- package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +165 -0
- package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +103 -0
- package/runtime/scripts/hfs/rules/unused-symbol.mjs +320 -0
- package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +185 -0
- package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +19 -0
- package/runtime/scripts/hfs/sealed-members.mjs +101 -0
- package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +200 -0
- package/runtime/scripts/hfs/secret-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 +1 -0
- package/runtime/scripts/hfs/stack-config-kit.mjs +34 -0
- package/runtime/scripts/hfs/stack-deploy-config.mjs +313 -0
- package/runtime/scripts/hfs/stack-ingress-config.mjs +237 -0
- package/runtime/scripts/hfs/stacks-gate.mjs +414 -0
- package/runtime/scripts/hfs/starcistacks-runtime.mjs +61 -0
- package/runtime/scripts/lib/command-tokens.mjs +126 -0
- package/runtime/scripts/lib/compose-values.mjs +11 -0
- package/runtime/scripts/lib/deployment-findings.mjs +7 -0
- package/runtime/scripts/lib/json-clone.mjs +61 -0
- package/runtime/scripts/lib/list.mjs +4 -0
- package/runtime/scripts/lib/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 +122 -0
- package/scaffold/add-fe-page.mjs +269 -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/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/index.mjs +108 -52
- 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 +53 -40
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// R250: host scheduling and application logic do not belong in deployment sources; compose mutations stay in dev.
|
|
2
|
+
import { parseWorkflow } from '../runtime-rules/workflow-source.mjs';
|
|
3
|
+
import { words } from '../../lib/dockerfile.mjs';
|
|
4
|
+
import { found, readText } from './read.mjs';
|
|
5
|
+
|
|
6
|
+
const WORKFLOW = /(?:^|\/)\.github\/workflows\/[^/]+\.ya?ml$/;
|
|
7
|
+
const DEPLOY = /(?:^|\/)deploy\//;
|
|
8
|
+
const SCRIPT = /(?:^|\/)scripts\//;
|
|
9
|
+
const COMPOSE = /(?:^|\/)(?:docker-)?compose(?:[.-][^/]*)?\.ya?ml$/;
|
|
10
|
+
const DOCKERFILE = /(?:^|\/)Dockerfile(?:\.[^/]*)?$/;
|
|
11
|
+
const CREDENTIAL_FILE = /(?:^|\/)(?:secrets?\.env(?:\.[^/]*)?|\.env(?:\.[^/]*)?|secrets\/)|\.enc$/i;
|
|
12
|
+
const HOST_CRON = /\b(?:crontab|anacron|crond|supercronic)\b|\/etc\/cron(?:\b|[./])|\bcron\.d\b|\/var\/spool\/cron\b|@(?:reboot|yearly|annually|monthly|weekly|daily|midnight|hourly)\b|\b[\w.@-]+\.timer\b|\bOn(?:Calendar|BootSec|UnitActiveSec|ActiveSec)\s*=|(?:^|[\s;|&"'])at\s+(?:(?:-[a-z]+\s+)*)((?:-[ft]\b)|now\b|tomorrow\b|\d{1,2}:\d{2}\b|midnight\b|noon\b)/im;
|
|
13
|
+
const HOST_CRON_PATH = /(?:^|\/)(?:crontab(?:$|\/)|cron\.d(?:\/|$)|[^/]+\.(?:timer|crontab)$)/;
|
|
14
|
+
const CRON_NUMERIC_FIELD = /^[\d*,/-]+$/;
|
|
15
|
+
const CRON_NAMED_FIELD = /^[\w*,/-]+$/;
|
|
16
|
+
const CRON_CONTAINER = /\bimage\s*:\s*["']?(?:[^\s"']*\/)?(?:cron|crond|supercronic|ofelia)(?=[:@\s"']|$)|\b(?:command|entrypoint)\s*:[^\n]*\b(?:cron|crond|supercronic)\b/i;
|
|
17
|
+
const SLEEP_LOOP = /\b(?:while\s+(?:true|:|\[\s*1\s*\])|for\s*\(\(\s*;\s*;\s*\)\))\s*[;\n][\s\S]{0,4000}?\bsleep\s+/i;
|
|
18
|
+
const INLINE_NODE = /\bnode\b[^\n;&|]*?\s(?:-(?:e|p|pe)\b|--(?:eval|print)(?:\s|=))/;
|
|
19
|
+
const INLINE_SQL_ACTION = /\b(?:insert\s+into|update\s+[\w."']+\s+set|delete\s+from|create\s+(?:table|function|view)|alter\s+table|truncate\s+(?:table\s+)?[\w."'])\b/i;
|
|
20
|
+
const REMOTE_SHELL = /\b(?:curl|wget)\b[^|\n]*\|\s*(?:sudo\s+)?(?:ba|da|z)?sh\b/i;
|
|
21
|
+
const COMPOSE_COMMAND = /\bdocker(?:\s+(?:--(?:context|host)(?:\s+|=)\S+|-H\s+\S+))*\s+compose\b[^\n;&|]*|\bdocker-compose\b[^\n;&|]*/g;
|
|
22
|
+
const COMPOSE_OPTIONS = new Set(['-f', '--file', '-p', '--project-name', '--project-directory', '--env-file', '--profile', '--parallel', '--ansi', '--progress']);
|
|
23
|
+
const DEV = /(?:^|[\s/._="'-])(?:dev|development|local)(?=$|[\s/._"'-])/i;
|
|
24
|
+
const NON_DEV = /(?:^|[\s/._="'-])(?:prod|production|staging|stage|vps|preview)(?=$|[\s/._"'-])/i;
|
|
25
|
+
const WORKFLOW_ENV_NAMES = ['DEPLOY_ENV', 'NODE_ENV', 'ENVIRONMENT', 'STARCI_ENV'];
|
|
26
|
+
|
|
27
|
+
/** Comments carry no executable scheduling; preserve strings so embedded SSH commands remain visible. */
|
|
28
|
+
function sourceText(text) {
|
|
29
|
+
return text.split('\n').map((line) => {
|
|
30
|
+
const trimmed = line.trimStart();
|
|
31
|
+
return trimmed.startsWith('#') || trimmed.startsWith('//') ? '' : line;
|
|
32
|
+
}).join('\n').replaceAll(/\\\r?\n\s*/g, ' ');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function cronTable(text) {
|
|
36
|
+
return text.split('\n').some((line) => {
|
|
37
|
+
const fields = line.trimStart().replace(/^["']/, '').trimEnd().split(/\s+/);
|
|
38
|
+
return fields.length >= 6
|
|
39
|
+
&& fields.slice(0, 3).every((field) => CRON_NUMERIC_FIELD.test(field))
|
|
40
|
+
&& fields.slice(3, 5).every((field) => CRON_NAMED_FIELD.test(field));
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function firstWordOffsets(text) {
|
|
45
|
+
const offsets = new Set();
|
|
46
|
+
let offset = 0;
|
|
47
|
+
for (const line of text.split(/[\r\n\u2028\u2029]/)) {
|
|
48
|
+
offsets.add(offset + line.length - line.trimStart().length);
|
|
49
|
+
offset += line.length + 1;
|
|
50
|
+
}
|
|
51
|
+
return offsets;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function inlineSql(text) {
|
|
55
|
+
if (INLINE_SQL_ACTION.test(text)) return true;
|
|
56
|
+
const offsets = firstWordOffsets(text);
|
|
57
|
+
for (const match of text.matchAll(/select/gi)) {
|
|
58
|
+
const preceding = text[match.index - 1];
|
|
59
|
+
const startsSql = offsets.has(match.index) || preceding === '"' || preceding === "'";
|
|
60
|
+
if (startsSql && /^\s+[^\n;]/.test(text.slice(match.index + match[0].length))) return true;
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function deploymentSource(file) {
|
|
66
|
+
if (CREDENTIAL_FILE.test(file) || /\.(?:md|png|jpe?g|svg|pdf|zip|gz)$/i.test(file)) return false;
|
|
67
|
+
return DEPLOY.test(file) || SCRIPT.test(file) || WORKFLOW.test(file) || COMPOSE.test(file) || DOCKERFILE.test(file);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function stackEnvironments(text) {
|
|
71
|
+
return [...text.matchAll(/\.starcistacks\/([^/\s"']+)\//g)].map((match) => match[1]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function composeTargets(command) {
|
|
75
|
+
const parts = words(command);
|
|
76
|
+
const targets = [];
|
|
77
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
78
|
+
if (['-f', '--file', '--env-file', '--project-directory'].includes(parts[i])) {
|
|
79
|
+
i += 1;
|
|
80
|
+
targets.push(parts[i] ?? '');
|
|
81
|
+
}
|
|
82
|
+
else if (/^--(?:file|env-file|project-directory)=/.test(parts[i])) targets.push(parts[i].slice(parts[i].indexOf('=') + 1));
|
|
83
|
+
else if (/^-f.+/.test(parts[i])) targets.push(parts[i].slice(2));
|
|
84
|
+
}
|
|
85
|
+
return targets;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function targetsNonDev(command, { environment, directory, remote }) {
|
|
89
|
+
if (remote || /--context(?:=|\s+)(?:prod(?:uction)?|staging|vps)\b|--host(?:=|\s+)ssh:\/\//i.test(command)) return true;
|
|
90
|
+
const targets = composeTargets(command);
|
|
91
|
+
if (targets.length) return targets.some((target) => {
|
|
92
|
+
const environments = stackEnvironments(target);
|
|
93
|
+
return environments.length ? environments.some((name) => !/^(?:dev|development|local)$/i.test(name)) : !DEV.test(target) || NON_DEV.test(target);
|
|
94
|
+
});
|
|
95
|
+
if (NON_DEV.test(environment) || NON_DEV.test(directory)) return true;
|
|
96
|
+
if (DEV.test(environment) || DEV.test(directory)) return false;
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function composeMutation(command) {
|
|
101
|
+
const parts = words(command);
|
|
102
|
+
const composeAt = parts.findIndex((part) => part === 'compose' || part === 'docker-compose');
|
|
103
|
+
const arguments_ = parts.slice(composeAt + 1);
|
|
104
|
+
for (let i = 0; i < arguments_.length; i += 1) {
|
|
105
|
+
const argument = arguments_[i];
|
|
106
|
+
if (COMPOSE_OPTIONS.has(argument)) { i += 1; continue; }
|
|
107
|
+
if (argument.startsWith('-')) continue;
|
|
108
|
+
return ['up', 'down', 'pull'].includes(argument);
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Job-local context prevents a production job from changing how a separate dev compose step is judged. */
|
|
114
|
+
function workflowCommands(doc) {
|
|
115
|
+
const entries = [];
|
|
116
|
+
for (const job of Object.values(doc?.jobs ?? {})) {
|
|
117
|
+
const environment = typeof job?.environment === 'string' ? job.environment : job?.environment?.name ?? '';
|
|
118
|
+
const environmentNames = WORKFLOW_ENV_NAMES.map((key) => job?.env?.[key] ?? doc?.env?.[key] ?? '').join(' ');
|
|
119
|
+
const context = { environment: `${environment} ${environmentNames}` };
|
|
120
|
+
for (const step of Array.isArray(job?.steps) ? job.steps : []) {
|
|
121
|
+
const directory = step?.['working-directory'] ?? job?.defaults?.run?.['working-directory'] ?? doc?.defaults?.run?.['working-directory'] ?? '';
|
|
122
|
+
const stepEnvironments = WORKFLOW_ENV_NAMES.map((key) => step?.env?.[key] ?? '').join(' ');
|
|
123
|
+
for (const source of [step?.run, step?.with?.script]) {
|
|
124
|
+
if (typeof source === 'string') entries.push({ source: sourceText(source), context: { ...context, environment: `${context.environment} ${stepEnvironments}`, directory, remote: Boolean(step?.with?.host && !/^(?:localhost|127\.0\.0\.1)$/.test(step.with.host)) } });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return entries;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function composeOnVps(file, text, doc) {
|
|
132
|
+
let entries = doc ? workflowCommands(doc) : [{ source: text, context: { environment: '', directory: '' } }];
|
|
133
|
+
if (file.endsWith('/package.json')) {
|
|
134
|
+
try { entries = Object.values(JSON.parse(text)?.scripts ?? {}).filter((command) => typeof command === 'string').map((source) => ({ source, context: { environment: '', directory: '' } })); } catch { /* Invalid JSON is judged by its own rule. */ }
|
|
135
|
+
}
|
|
136
|
+
return entries.some(({ source, context }) => [...source.matchAll(COMPOSE_COMMAND)].some((match) => {
|
|
137
|
+
const before = source.slice(0, match.index);
|
|
138
|
+
const linePrefix = before.slice(before.lastIndexOf('\n') + 1);
|
|
139
|
+
const directory = workingDirectory(before) ?? context.directory;
|
|
140
|
+
const remote = context.remote || (/\bssh\b/.test(linePrefix) && !/\b(?:localhost|127\.0\.0\.1)\b/.test(linePrefix));
|
|
141
|
+
return composeMutation(match[0]) && targetsNonDev(match[0], { ...context, directory, remote });
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function workingDirectory(source) {
|
|
146
|
+
let directory;
|
|
147
|
+
for (const command of source.split(/[\n;&]/)) {
|
|
148
|
+
const match = /^cd\s+([\w./-]+)/.exec(command.trimStart());
|
|
149
|
+
if (match) directory = match[1];
|
|
150
|
+
}
|
|
151
|
+
return directory;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function workflowSchedule(doc, text) {
|
|
155
|
+
return Boolean(doc?.on && typeof doc.on === 'object' && Object.hasOwn(doc.on, 'schedule'))
|
|
156
|
+
|| (!doc && text.split('\n').some((line) => /^schedule\s*:/.test(line.trimStart())));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function deploymentLogic(file, text) {
|
|
160
|
+
return DEPLOY.test(file) && /\.(?:sh|bash)$/.test(file)
|
|
161
|
+
&& (INLINE_NODE.test(text) || /\bpsql\b/.test(text) || inlineSql(text) || REMOTE_SHELL.test(text));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function sourceFindings(file, text, doc) {
|
|
165
|
+
const findings = [];
|
|
166
|
+
const cronContainer = CRON_CONTAINER.test(text);
|
|
167
|
+
const host = HOST_CRON.test(text) || HOST_CRON_PATH.test(file) || cronTable(text) || cronContainer || /\b(?:service\s+cron|cron\s+-[a-z]|cron\s*$)/im.test(text);
|
|
168
|
+
if (host) findings.push(found('HFS_HOST_CRON', file, 'Host cron, systemd timers and at jobs are forbidden; use an app-owned scheduler or a declared replicated-job'));
|
|
169
|
+
if ((WORKFLOW.test(file) && workflowSchedule(doc, text)) || (DEPLOY.test(file) && host) || cronContainer || SLEEP_LOOP.test(text)) {
|
|
170
|
+
findings.push(found('HFS_DEPLOY_SCHEDULER', file, 'Deployment sources must not own recurring schedules; move scheduled work into the app and declare its job'));
|
|
171
|
+
}
|
|
172
|
+
if (deploymentLogic(file, text)) {
|
|
173
|
+
findings.push(found('HFS_DEPLOY_NO_LOGIC', file, 'Deployment shell scripts must delegate to docker, starci or the built cli app; move inline application or database logic into the cli app'));
|
|
174
|
+
}
|
|
175
|
+
if (composeOnVps(file, text, doc)) findings.push(found('HFS_DEPLOY_COMPOSE_ON_VPS', file, 'Compose up, down and pull are dev-only; deploy a non-dev environment through its declared Swarm or Kubernetes runtime'));
|
|
176
|
+
return findings;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Findings are names and paths only: deployment source snippets and credential values are never reported. */
|
|
180
|
+
export function deployScanFindings({ repoRoot, files, all, repo, isRoot, inScope = () => true }) {
|
|
181
|
+
if (repo?.kind !== 'app' || !['lite', 'full'].includes(repo.edition) || isRoot === false) return [];
|
|
182
|
+
const findings = [];
|
|
183
|
+
for (const file of new Set(all ?? files)) {
|
|
184
|
+
if (!inScope(file) || !deploymentSource(file)) continue;
|
|
185
|
+
const raw = readText(repoRoot, file);
|
|
186
|
+
if (raw === null) continue;
|
|
187
|
+
const text = sourceText(raw);
|
|
188
|
+
const doc = WORKFLOW.test(file) ? parseWorkflow(raw) : null;
|
|
189
|
+
findings.push(...sourceFindings(file, text, doc));
|
|
190
|
+
}
|
|
191
|
+
return findings;
|
|
192
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// R251: product workflow custody, immutable dependencies and release-only triggers, in both editions.
|
|
2
|
+
import { parseWorkflow, workflowFindings } from '../runtime-rules/workflow-source.mjs';
|
|
3
|
+
import { found, readText } from './read.mjs';
|
|
4
|
+
import { isPlainObject as record } from '../../../engine/plain-object.mjs';
|
|
5
|
+
import { workflowEventOptions } from '../../lib/workflow-events.mjs';
|
|
6
|
+
|
|
7
|
+
const DEPLOY = 'HFS_DEPLOY_WORKFLOW';
|
|
8
|
+
const PIN = 'HFS_WORKFLOW_PIN';
|
|
9
|
+
const TOOL = 'HFS_CI_TOOL_INSTALL';
|
|
10
|
+
const TRIGGER = 'HFS_APP_WORKFLOW_TRIGGER';
|
|
11
|
+
// Only the age identity and SSH transport credentials belong in the CI secret store.
|
|
12
|
+
const CI_SECRET_ENV_NAMES = new Set(['SOPS_AGE_KEY', 'SSH_KEY', 'SSH_PRIVATE_KEY', 'SSH_KNOWN_HOSTS', 'VPS_SSH_KEY', 'VPS_SSH_PRIVATE_KEY', 'VPS_KNOWN_HOSTS', 'VPS_SSH_KNOWN_HOSTS', 'DEPLOY_SSH_KEY', 'DEPLOY_SSH_PRIVATE_KEY', 'DEPLOY_KNOWN_HOSTS', 'DEPLOY_SSH_KNOWN_HOSTS']);
|
|
13
|
+
const nonempty = (value) => typeof value === 'string' && value.trim().length > 0;
|
|
14
|
+
function executable(script) {
|
|
15
|
+
let result = '';
|
|
16
|
+
let quote = '';
|
|
17
|
+
for (let index = 0; index < script.length; index++) {
|
|
18
|
+
const character = script[index];
|
|
19
|
+
if (character === '\\' && quote !== "'") { result += character + (script[++index] ?? ''); continue; }
|
|
20
|
+
if (!quote && character === '#' && (index === 0 || /[\s;&|()]/.test(script[index - 1]))) {
|
|
21
|
+
while (index < script.length && script[index] !== '\n') index++;
|
|
22
|
+
result += '\n';
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (character === quote) quote = '';
|
|
26
|
+
else if (!quote && (character === "'" || character === '"')) quote = character;
|
|
27
|
+
result += character;
|
|
28
|
+
}
|
|
29
|
+
return result.replace(/\\\r?\n/g, ' ');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function commands(script) {
|
|
33
|
+
const result = [];
|
|
34
|
+
let quote = '';
|
|
35
|
+
let start = 0;
|
|
36
|
+
for (let index = 0; index < script.length; index++) {
|
|
37
|
+
const character = script[index];
|
|
38
|
+
if (character === '\\' && quote !== "'") { index++; continue; }
|
|
39
|
+
if (character === quote) quote = '';
|
|
40
|
+
else if (!quote && (character === "'" || character === '"')) quote = character;
|
|
41
|
+
else if (!quote && /[\n;&|]/.test(character)) { result.push({ text: script.slice(start, index).trim(), start, end: index, separator: character }); start = index + 1; }
|
|
42
|
+
}
|
|
43
|
+
result.push({ text: script.slice(start).trim(), start });
|
|
44
|
+
return result.filter((command) => command.text);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const commandBody = (command) => command.replace(/^(?:(?:sudo|env)\s+|[A-Za-z_]\w*=[^\s]+\s+)*/, '');
|
|
48
|
+
|
|
49
|
+
function entries(value) {
|
|
50
|
+
if (!value || typeof value !== 'object') return [];
|
|
51
|
+
return Object.entries(value).flatMap(([key, member]) => [{ key, value: member }, ...entries(member)]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const strings = (value) => entries(value).filter((entry) => typeof entry.value === 'string').map((entry) => entry.value);
|
|
55
|
+
const activeSteps = (job) => (Array.isArray(job.steps) ? job.steps : []).filter((step) => record(step) && step.if !== false && step.if !== 'false' && !/^\$\{\{\s*false\s*\}\}$/.test(String(step.if ?? '')));
|
|
56
|
+
const scripts = (job) => activeSteps(job).flatMap((step) => [step.run, step.with?.script].filter(nonempty).map(executable));
|
|
57
|
+
const validEnvironment = (value) => nonempty(value) || (record(value) && nonempty(value.name));
|
|
58
|
+
const validPermissions = (value) => record(value) || value === 'read-all' || value === 'write-all';
|
|
59
|
+
const validConcurrency = (value) => nonempty(value) || (record(value) && nonempty(value.group));
|
|
60
|
+
const deployName = (value) => !/authorize[\s_.-]+deploy[\s_.-]+key/i.test(value) && /(?:^|[\s/_.-])deploy(?:[\s/_.-]|$)/i.test(value);
|
|
61
|
+
const materializeCommand = (script) => commands(script).some(({ text }) => /^starci\s+app\s+secret\s+materialize\b/.test(commandBody(text)));
|
|
62
|
+
const deploymentCommand = (script) => commands(script).some(({ text }) => /^(?:starci\s+(?:app\s+)?(?:deploy|secret\s+materialize)\b|docker\s+stack\s+deploy\b|(?:ba?sh\s+)?(?:\.\/)?deploy\/|(?:ba?sh\s+)?(?:\.\/)?(?:scripts\/)?[\w.-]*deploy[\w.-]*\.sh\b|npm\s+run\s+deploy(?=[:\s]|$))/.test(commandBody(text)));
|
|
63
|
+
|
|
64
|
+
function hasAgeIdentity(job, doc) {
|
|
65
|
+
const steps = activeSteps(job);
|
|
66
|
+
if ([doc.env, job.env, ...steps.map((step) => step.env)].some((env) => record(env) && nonempty(env.SOPS_AGE_KEY))) return true;
|
|
67
|
+
const reference = /\$\{\{\s*secrets\s*(?:\.\s*SOPS_AGE_KEY|\[\s*(['"])SOPS_AGE_KEY\1\s*\])/;
|
|
68
|
+
return steps.some((step) => strings(step.with).some((value) => reference.test(value)) || (typeof step.run === 'string' && reference.test(executable(step.run))));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function triggerFindings(doc, refuse) {
|
|
72
|
+
const on = workflowEventOptions(doc.on);
|
|
73
|
+
if (!record(on) || !Object.keys(on).length) return [refuse(TRIGGER, 'declare release tags [v*] and/or workflow_dispatch')];
|
|
74
|
+
const findings = [];
|
|
75
|
+
for (const [event, options] of Object.entries(on)) {
|
|
76
|
+
if (event === 'workflow_dispatch') continue;
|
|
77
|
+
if (event !== 'push') {
|
|
78
|
+
findings.push(refuse(TRIGGER, `trigger ${event} is forbidden; app CI starts only on release tags [v*] or workflow_dispatch`));
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (!record(options) || !Array.isArray(options.tags) || options.tags.length !== 1 || options.tags[0] !== 'v*' || Object.keys(options).some((key) => key !== 'tags')) {
|
|
82
|
+
findings.push(refuse(TRIGGER, 'push must contain only tags: [v*]; branch and path filters are forbidden'));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return findings;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function actionFindings(doc, refuse) {
|
|
89
|
+
const findings = [];
|
|
90
|
+
for (const { key, value } of entries(doc)) {
|
|
91
|
+
if (key !== 'uses') continue;
|
|
92
|
+
const local = typeof value === 'string' && /^\.\/[\w./-]+$/.test(value);
|
|
93
|
+
const remote = typeof value === 'string' && /^[\w.-]+\/[\w.-]+(?:\/[\w./-]+)?@[a-f\d]{40}$/i.test(value);
|
|
94
|
+
const container = typeof value === 'string' && /^docker:\/\/[^\s@]+@sha256:[a-f\d]{64}$/i.test(value);
|
|
95
|
+
if (!local && !remote && !container) findings.push(refuse(PIN, 'every remote uses entry needs a full 40-hex commit SHA (a container action needs a sha256 digest)'));
|
|
96
|
+
}
|
|
97
|
+
return findings;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function secretProblems(doc) {
|
|
101
|
+
const problems = [];
|
|
102
|
+
if (entries(doc).some(({ key, value }) => key === 'secrets' && value === 'inherit')) problems.push('inherited secrets are forbidden');
|
|
103
|
+
for (const value of strings(doc)) {
|
|
104
|
+
for (const expression of value.matchAll(/\$\{\{([\s\S]*?)\}\}/g)) {
|
|
105
|
+
const text = expression[1];
|
|
106
|
+
const refs = [...text.matchAll(/\bsecrets\s*(?:\.\s*([A-Za-z_]\w*)|\[\s*(['"])([^'"]+)\2\s*\])/g)];
|
|
107
|
+
for (const ref of refs) if (!CI_SECRET_ENV_NAMES.has(ref[1] ?? ref[3])) problems.push('a secrets reference is outside the age-key/SSH transport allowlist');
|
|
108
|
+
if (/\bsecrets\b/.test(text.replace(/\bsecrets\s*(?:\.\s*[A-Za-z_]\w*|\[\s*(['"])[^'"]+\1\s*\])/g, ''))) problems.push('dynamic or bulk secrets access is forbidden');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return [...new Set(problems)];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function dotenvTarget(script, start) {
|
|
115
|
+
let index = start;
|
|
116
|
+
while (index < script.length && /\s/.test(script[index])) index++;
|
|
117
|
+
if (script[index] === '"' || script[index] === "'") index++;
|
|
118
|
+
const from = index;
|
|
119
|
+
while (index < script.length && !/[\s"';|<>]/.test(script[index])) index++;
|
|
120
|
+
if (index < script.length && !/[\s"';|]/.test(script[index])) return false;
|
|
121
|
+
const target = script.slice(from, index);
|
|
122
|
+
const name = target.slice(target.lastIndexOf('/') + 1);
|
|
123
|
+
if (name.startsWith('.env')) return /^[.\w-]*$/.test(name.slice(4));
|
|
124
|
+
return name.endsWith('.env') && /^[\w.-]+$/.test(name.slice(0, -4));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function scriptWritesDotenv(value) {
|
|
128
|
+
const script = executable(value);
|
|
129
|
+
for (const match of script.matchAll(/>{1,2}|\btee\s+(?:-a\s+)?/g)) {
|
|
130
|
+
if (dotenvTarget(script, match.index + match[0].length)) return true;
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function writesDotenv(doc) {
|
|
136
|
+
return entries(doc).some(({ key, value }) => key === 'env_file' || key === 'env-file' || (['run', 'script'].includes(key) && typeof value === 'string' && scriptWritesDotenv(value)));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function deploymentJobs(doc, file) {
|
|
140
|
+
const jobs = record(doc.jobs) ? Object.entries(doc.jobs).filter(([, job]) => record(job)) : [];
|
|
141
|
+
const identified = jobs.filter(([name, job]) => deployName(name) || deployName(String(job.name ?? '')) || scripts(job).some(deploymentCommand) || (typeof job.uses === 'string' && deployName(job.uses)));
|
|
142
|
+
if (identified.length) return identified;
|
|
143
|
+
if (deployName(file) || deployName(String(doc.name ?? ''))) return jobs;
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function deploymentJobFindings(name, job, doc, refuse) {
|
|
148
|
+
const findings = [];
|
|
149
|
+
if (!validEnvironment(job.environment)) findings.push(refuse(DEPLOY, `job ${name} must declare its protected environment`));
|
|
150
|
+
if (!validPermissions(job.permissions ?? doc.permissions)) findings.push(refuse(DEPLOY, `job ${name} must declare permissions at job or workflow scope`));
|
|
151
|
+
if (!validConcurrency(job.concurrency ?? doc.concurrency)) findings.push(refuse(DEPLOY, `job ${name} must declare concurrency at job or workflow scope`));
|
|
152
|
+
if (!hasAgeIdentity(job, doc) && !scripts(job).some(materializeCommand)) findings.push(refuse(DEPLOY, `job ${name} must inject SOPS_AGE_KEY or run starci app secret materialize`));
|
|
153
|
+
return findings;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function deploymentFindings(doc, file, refuse) {
|
|
157
|
+
const deployment = deploymentJobs(doc, file);
|
|
158
|
+
const named = deployName(file) || deployName(String(doc.name ?? ''));
|
|
159
|
+
if (!deployment.length && !named) return [];
|
|
160
|
+
const findings = [];
|
|
161
|
+
if (!deployment.length) findings.push(refuse(DEPLOY, 'the deploy workflow must declare a deployment job'));
|
|
162
|
+
for (const [name, job] of deployment) findings.push(...deploymentJobFindings(name, job, doc, refuse));
|
|
163
|
+
for (const problem of secretProblems(doc)) findings.push(refuse(DEPLOY, problem));
|
|
164
|
+
if (writesDotenv(doc)) findings.push(refuse(DEPLOY, 'deploy workflows must never write dotenv files or use env_file blobs; materialize sealed custody'));
|
|
165
|
+
return findings;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// A version label alone is not an integrity pin. The immutable reference or verified artifact must belong to this install.
|
|
169
|
+
function verifiedArtifact(artifact, prior) {
|
|
170
|
+
if (!/^\.\/[\w./-]+\.(?:tgz|tar\.gz|whl)$/.test(artifact)) return false;
|
|
171
|
+
return prior.split(/\r?\n/).some((line) => {
|
|
172
|
+
const checksum = line.trim().match(/^echo\s+(['"])([a-f\d]{64})\s+\*?(\S+)\1\s*\|\s*sha256sum\s+(?:--check|-c)\s+-\s*(?:$|&&)/i);
|
|
173
|
+
return checksum && checksum[3].replace(/^\.\//, '') === artifact.slice(2) && !line.includes('||');
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function pinnedInstall(command, prior) {
|
|
178
|
+
if (/^(?:python(?:3)?\s+-m\s+)?pip(?:3)?\s+install\b/.test(command)) {
|
|
179
|
+
// pip's hash-checking mode refuses every unhashed direct or transitive requirement itself.
|
|
180
|
+
if (/(?:^|\s)--require-hashes(?:\s|$)/.test(command)) return true;
|
|
181
|
+
const targets = command.replace(/^(?:python(?:3)?\s+-m\s+)?pip(?:3)?\s+install\s*/, '').split(/\s+/).filter((token) => !token.startsWith('-'));
|
|
182
|
+
return targets.length > 0 && targets.every((target) => /^git\+https:\/\/\S+@[a-f\d]{40}$/i.test(target) || verifiedArtifact(target, prior));
|
|
183
|
+
}
|
|
184
|
+
if (/^go\s+install\b/.test(command)) {
|
|
185
|
+
const targets = command.replace(/^go\s+install\s*/, '').split(/\s+/).filter((token) => !token.startsWith('-'));
|
|
186
|
+
return targets.length > 0 && targets.every((target) => /^[\w./-]+@[a-f\d]{40}$/i.test(target));
|
|
187
|
+
}
|
|
188
|
+
const targets = command.replace(/^npm\s+(?:(?:-g|--global|--location=global)\s+)*(?:i|install)\s*/, '').split(/\s+/).filter((token) => !token.startsWith('-')).map((token) => token.replace(/^["']|["']$/g, ''));
|
|
189
|
+
return targets.length > 0 && targets.every((target) => /^git\+https:\/\/\S+#[a-f\d]{40}$/i.test(target) || verifiedArtifact(target, prior));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function toolFindings(doc, refuse) {
|
|
193
|
+
const findings = [];
|
|
194
|
+
for (const { key, value } of entries(doc)) {
|
|
195
|
+
if (!['run', 'script'].includes(key) || typeof value !== 'string') continue;
|
|
196
|
+
const script = executable(value);
|
|
197
|
+
const statements = commands(script);
|
|
198
|
+
if (statements.some((command, index) => /^\b(?:curl|wget)\b/.test(commandBody(command.text)) && command.separator === '|' && /^(?:ba)?sh\b/.test(commandBody(statements[index + 1]?.text ?? '')) || /^(?:ba)?sh\s+<\(\s*(?:curl|wget)\b/.test(commandBody(command.text)))) findings.push(refuse(TOOL, 'a downloaded installer must be checksum-verified before execution; curl/wget piped to a shell is forbidden'));
|
|
199
|
+
for (const command of statements) {
|
|
200
|
+
const body = commandBody(command.text);
|
|
201
|
+
const install = /^(?:go\s+install|(?:python(?:3)?\s+-m\s+)?pip(?:3)?\s+install)\b/.test(body) || /^npm\s+(?:(?:-g|--global|--location=global)\s+)*(?:i|install)\b/.test(body) && /(?:^|\s)(?:-g|--global|--location=global)(?:\s|$)/.test(body);
|
|
202
|
+
if (install && !pinnedInstall(body, script.slice(0, command.start))) findings.push(refuse(TOOL, 'tool installs need an immutable hash or a verified artifact; use a SHA-pinned setup action instead of a version-only install'));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return findings;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function perWorkflow({ path: file, text }) {
|
|
209
|
+
const refuse = (code, message) => found(code, file, `${code} ${file}: ${message}`);
|
|
210
|
+
const doc = parseWorkflow(text);
|
|
211
|
+
if (!record(doc)) return [refuse(TRIGGER, 'the workflow must be valid YAML with a release-only on block'), ...(deployName(file) ? [refuse(DEPLOY, 'the deploy workflow must be valid YAML')] : [])];
|
|
212
|
+
return [...triggerFindings(doc, refuse), ...actionFindings(doc, refuse), ...deploymentFindings(doc, file, refuse), ...toolFindings(doc, refuse)];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Synchronous R251 findings over the tracked workflows of a lite/full app. */
|
|
216
|
+
export function deployWorkflowFindings({ repoRoot, files, all = files, repo, isRoot, inScope = () => true }) {
|
|
217
|
+
if (isRoot === false || repo?.kind !== 'app' || !['lite', 'full'].includes(repo.edition)) return [];
|
|
218
|
+
return workflowFindings({ files: all.filter((file) => inScope(file)), read: (file) => readText(repoRoot, file) }, perWorkflow);
|
|
219
|
+
}
|