@starci/hfs 4.2.17 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +7 -0
- package/db/migrate.mjs +361 -0
- package/db/post-cutover.mjs +136 -0
- package/emit/db-types.mjs +10 -1
- package/lint/run.mjs +1 -1
- package/package.json +4 -1
- package/release/gate.mjs +298 -0
- package/release/index.mjs +103 -0
- package/release/push.mjs +110 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
- package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
- package/runtime/knowledge/hfs/rules.yaml +3437 -160
- package/runtime/knowledge/hfs/slots.yaml +430 -92
- package/runtime/knowledge/patterns/be/api.yaml +92 -2
- package/runtime/knowledge/patterns/be/cli.yaml +37 -9
- package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
- package/runtime/knowledge/patterns/be/jobs.yaml +20 -2
- package/runtime/knowledge/patterns/be/projections.yaml +4 -2
- package/runtime/knowledge/patterns/be/queues.yaml +11 -3
- package/runtime/knowledge/patterns/be/reactors.yaml +20 -1
- package/runtime/knowledge/patterns/be/realtime.yaml +25 -3
- package/runtime/knowledge/patterns/be/saga.yaml +20 -3
- package/runtime/knowledge/patterns/be/webhooks.yaml +210 -36
- package/runtime/knowledge/patterns/fe/folder.yaml +4 -0
- package/runtime/knowledge/sonar-gate.yaml +1 -1
- package/runtime/modules/kernel/failure-codes.yaml +3128 -47
- package/runtime/modules/models/runtimes.yaml +1 -1
- package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
- package/runtime/packages/cli/src/catalog.generated.mjs +14104 -0
- package/runtime/packages/cli/src/validate-args.mjs +213 -0
- package/runtime/scripts/api/git/add.mjs +5 -0
- package/runtime/scripts/api/git/cat-file.mjs +5 -0
- package/runtime/scripts/api/git/init.mjs +7 -0
- package/runtime/scripts/api/git/lib.mjs +3 -0
- package/runtime/scripts/api/git/tag.mjs +5 -0
- package/runtime/scripts/api/node/lib.mjs +20 -0
- package/runtime/scripts/api/node/run-node.mjs +6 -0
- package/runtime/scripts/api/sops/lib.mjs +41 -0
- package/runtime/scripts/api/sops/resolve-sops.mjs +7 -0
- package/runtime/scripts/api/sops/seal.mjs +1 -1
- package/runtime/scripts/api/sops/selected-age-recipient.mjs +7 -0
- package/runtime/scripts/api/supabase/database-dump.mjs +5 -0
- package/runtime/scripts/api/supabase/database-query.mjs +5 -0
- package/runtime/scripts/api/supabase/lib.mjs +44 -0
- package/runtime/scripts/api/supabase/migration-repair.mjs +5 -0
- package/runtime/scripts/hfs/app-wiring.mjs +201 -0
- package/runtime/scripts/hfs/architecture/backend-app-files.mjs +103 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +22 -10
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +157 -41
- package/runtime/scripts/hfs/architecture/canon-clones.mjs +240 -0
- package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +143 -0
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
- package/runtime/scripts/hfs/architecture/clones.mjs +17 -8
- package/runtime/scripts/hfs/architecture/connection-map.mjs +14 -12
- package/runtime/scripts/hfs/architecture/context-map.mjs +25 -14
- package/runtime/scripts/hfs/architecture/context-owner.mjs +1 -2
- package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +23 -50
- package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
- package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +104 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +30 -11
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
- package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
- package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +16 -3
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +17 -0
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +49 -25
- package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
- package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
- package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
- package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
- package/runtime/scripts/hfs/architecture/register-once.mjs +15 -7
- package/runtime/scripts/hfs/architecture/required-files.mjs +2 -1
- package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
- package/runtime/scripts/hfs/architecture/staged-consumers.mjs +37 -0
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +214 -17
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -6
- package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
- package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
- package/runtime/scripts/hfs/architecture/tiers.mjs +11 -4
- package/runtime/scripts/hfs/architecture/typescript.mjs +1 -8
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +9 -13
- package/runtime/scripts/hfs/check-results.mjs +28 -0
- package/runtime/scripts/hfs/check.mjs +63 -72
- package/runtime/scripts/hfs/coverage-from.mjs +70 -0
- package/runtime/scripts/hfs/db-baseline-verb.mjs +276 -0
- package/runtime/scripts/hfs/db-plan-verb.mjs +254 -0
- package/runtime/scripts/hfs/db-restamp-verb.mjs +60 -0
- package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
- package/runtime/scripts/hfs/doc-commands.mjs +367 -0
- package/runtime/scripts/hfs/edition-slots.mjs +2 -2
- package/runtime/scripts/hfs/example-secrets.mjs +158 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +91 -6
- package/runtime/scripts/hfs/path-findings.mjs +32 -2
- package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
- package/runtime/scripts/hfs/rule-catalog.mjs +156 -11
- package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
- package/runtime/scripts/hfs/rules/a15-findings.mjs +88 -0
- package/runtime/scripts/hfs/rules/app-env-required.mjs +20 -0
- package/runtime/scripts/hfs/rules/app-root.mjs +2 -2
- package/runtime/scripts/hfs/rules/assets.mjs +79 -0
- package/runtime/scripts/hfs/rules/boot-proof.mjs +16 -0
- package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +59 -0
- package/runtime/scripts/hfs/rules/contract.mjs +66 -5
- package/runtime/scripts/hfs/rules/coverage-below.mjs +230 -0
- package/runtime/scripts/hfs/rules/database-access.mjs +343 -0
- package/runtime/scripts/hfs/rules/database-baseline.mjs +142 -0
- package/runtime/scripts/hfs/rules/database-config.mjs +250 -2
- package/runtime/scripts/hfs/rules/database-constants.mjs +9 -0
- package/runtime/scripts/hfs/rules/database-migrations.mjs +317 -37
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +119 -9
- package/runtime/scripts/hfs/rules/database-queue-sources.mjs +176 -0
- package/runtime/scripts/hfs/rules/database-queues.mjs +361 -0
- package/runtime/scripts/hfs/rules/database-sql.mjs +91 -9
- package/runtime/scripts/hfs/rules/database.mjs +11 -2
- package/runtime/scripts/hfs/rules/deploy-scan.mjs +192 -0
- package/runtime/scripts/hfs/rules/deploy-workflow.mjs +219 -0
- package/runtime/scripts/hfs/rules/doc-fresh.mjs +286 -0
- package/runtime/scripts/hfs/rules/docker-health.mjs +249 -0
- package/runtime/scripts/hfs/rules/docker.mjs +24 -7
- package/runtime/scripts/hfs/rules/edition.mjs +11 -10
- package/runtime/scripts/hfs/rules/env-parity.mjs +373 -0
- package/runtime/scripts/hfs/rules/env-value-shape.mjs +113 -0
- package/runtime/scripts/hfs/rules/event-bus.mjs +3 -2
- package/runtime/scripts/hfs/rules/event-tables.mjs +100 -0
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +115 -15
- package/runtime/scripts/hfs/rules/fe-stack.mjs +57 -0
- package/runtime/scripts/hfs/rules/feature-files.mjs +28 -0
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +54 -35
- package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -2
- package/runtime/scripts/hfs/rules/lexicon.mjs +62 -0
- package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +89 -0
- package/runtime/scripts/hfs/rules/monorepo.mjs +14 -1
- package/runtime/scripts/hfs/rules/pipeline.mjs +8 -1
- package/runtime/scripts/hfs/rules/proof-commands.mjs +2 -0
- package/runtime/scripts/hfs/rules/release-provenance.mjs +211 -0
- package/runtime/scripts/hfs/rules/saga-delivery.mjs +66 -0
- package/runtime/scripts/hfs/rules/saga.mjs +8 -16
- package/runtime/scripts/hfs/rules/script-hygiene.mjs +151 -0
- package/runtime/scripts/hfs/rules/secret-custody-only.mjs +65 -0
- package/runtime/scripts/hfs/rules/secrets.mjs +26 -0
- package/runtime/scripts/hfs/rules/services.mjs +4 -4
- package/runtime/scripts/hfs/rules/stack-tooling.mjs +93 -0
- package/runtime/scripts/hfs/rules/stacks-deploy.mjs +76 -0
- package/runtime/scripts/hfs/rules/stacks-ingress.mjs +87 -0
- package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -0
- package/runtime/scripts/hfs/rules/stacks-release.mjs +366 -0
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +168 -0
- package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
- package/runtime/scripts/hfs/rules/stacks.mjs +13 -8
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +1 -1
- package/runtime/scripts/hfs/rules/supply-chain.mjs +167 -0
- package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +101 -0
- package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +77 -0
- package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +165 -0
- package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +103 -0
- package/runtime/scripts/hfs/rules/unused-symbol.mjs +320 -0
- package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +185 -0
- package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +19 -0
- package/runtime/scripts/hfs/sealed-members.mjs +101 -0
- package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +200 -0
- package/runtime/scripts/hfs/secret-document.mjs +97 -0
- package/runtime/scripts/hfs/secret-edit.mjs +170 -0
- package/runtime/scripts/hfs/secret-errors.mjs +20 -0
- package/runtime/scripts/hfs/secret-files.mjs +124 -0
- package/runtime/scripts/hfs/secret-init.mjs +300 -0
- package/runtime/scripts/hfs/secret-output.mjs +100 -0
- package/runtime/scripts/hfs/secret-sops.mjs +130 -0
- package/runtime/scripts/hfs/secret-store.mjs +163 -0
- package/runtime/scripts/hfs/secret.mjs +162 -193
- package/runtime/scripts/hfs/slot-imports.mjs +18 -3
- package/runtime/scripts/hfs/slot-required.mjs +13 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +2 -1
- package/runtime/scripts/hfs/slots.mjs +1 -0
- package/runtime/scripts/hfs/stack-config-kit.mjs +34 -0
- package/runtime/scripts/hfs/stack-deploy-config.mjs +313 -0
- package/runtime/scripts/hfs/stack-ingress-config.mjs +237 -0
- package/runtime/scripts/hfs/stacks-gate.mjs +414 -0
- package/runtime/scripts/hfs/starcistacks-runtime.mjs +61 -0
- package/runtime/scripts/hfs/tree-findings.mjs +22 -0
- package/runtime/scripts/lib/command-tokens.mjs +126 -0
- package/runtime/scripts/lib/compose-values.mjs +11 -0
- package/runtime/scripts/lib/deployment-findings.mjs +7 -0
- package/runtime/scripts/lib/json-clone.mjs +61 -0
- package/runtime/scripts/lib/list.mjs +4 -0
- package/runtime/scripts/lib/roots.mjs +59 -0
- package/runtime/scripts/lib/runtime-example.mjs +13 -0
- package/runtime/scripts/lib/secret-patterns.mjs +60 -5
- package/runtime/scripts/lib/sql-ast.mjs +32 -0
- package/runtime/scripts/lib/stack-services.mjs +1 -1
- package/runtime/scripts/lib/swarm-stack.mjs +126 -0
- package/runtime/scripts/lib/trim-end.mjs +8 -0
- package/runtime/scripts/lib/ts-analysis.mjs +23 -0
- package/runtime/scripts/lib/ts-ast.mjs +27 -2
- package/runtime/scripts/lib/workflow-events.mjs +8 -0
- package/scaffold/add-app.mjs +77 -59
- package/scaffold/add-cli-lite.mjs +24 -17
- package/scaffold/add-compose.mjs +79 -0
- package/scaffold/add-database-lite.mjs +14 -0
- package/scaffold/add-fe-overlay.mjs +122 -0
- package/scaffold/add-fe-page.mjs +269 -0
- package/scaffold/add-inbox-lite.mjs +87 -124
- package/scaffold/add-plan.mjs +183 -0
- package/scaffold/add-table.mjs +132 -70
- package/scaffold/add.mjs +186 -204
- package/scaffold/app.mjs +128 -43
- package/scaffold/edition-gate.mjs +3 -3
- package/scaffold/image.mjs +2 -2
- package/scaffold/lite-exports.mjs +2 -1
- package/scaffold/service.mjs +2 -2
- package/src/main.mjs +238 -119
- package/stack/age-install.mjs +37 -0
- package/stack/backup.mjs +100 -0
- package/stack/build-args.mjs +42 -0
- package/stack/build.mjs +173 -0
- package/stack/cli.mjs +130 -0
- package/stack/config.mjs +57 -0
- package/stack/context.mjs +57 -0
- package/stack/converge.mjs +107 -0
- package/stack/deploy.mjs +301 -0
- package/stack/digest.mjs +16 -0
- package/stack/docker-transport.mjs +53 -0
- package/stack/edge-alias.mjs +118 -0
- package/stack/errors.mjs +21 -0
- package/stack/image-scan.mjs +152 -0
- package/stack/ingress-create.mjs +114 -0
- package/stack/ingress-publish.mjs +273 -0
- package/stack/ingress.mjs +706 -0
- package/stack/jobs.mjs +51 -0
- package/stack/live-restore.mjs +177 -0
- package/stack/prepare.mjs +266 -0
- package/stack/private-file.mjs +22 -0
- package/stack/provision.mjs +90 -0
- package/stack/prune.mjs +46 -0
- package/stack/release-store.mjs +87 -0
- package/stack/remote.mjs +205 -0
- package/stack/render.mjs +149 -0
- package/stack/retire.mjs +47 -0
- package/stack/sealed.mjs +149 -0
- package/stack/task.mjs +42 -0
- package/stack/verify.mjs +99 -0
- package/sync/ci-commands.mjs +13 -0
- package/sync/index.mjs +127 -64
- package/sync/stack-images.mjs +127 -0
- package/templates/app/ci-workflows/github/workflows/ci.yml +5 -1
- package/templates/app/ci-workflows/github/workflows/images.yml +15 -3
- package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +148 -0
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +7 -3
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +38 -6
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +14 -2
- package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +148 -0
- package/templates/app/codegen-graphql.mjs.partial +13 -2
- package/templates/app/codegen-lite/scripts/codegen.mjs +6 -1
- package/templates/app/codegen-prelude.mjs.partial +8 -0
- package/templates/app/gitignore +4 -0
- package/templates/app/gitignore-lite +4 -0
- package/templates/app/hooks/husky/pre-commit +7 -12
- package/templates/app/package-scripts/package.json +1 -0
- package/templates/app/package-scripts/supabase.json +7 -0
- package/templates/app/package-scripts-lite/package.json +1 -7
- package/templates/app/skeleton/.editorconfig +1 -0
- package/templates/app/skeleton/.gitattributes +7 -1
- package/templates/app/skeleton/.github/dependabot.yml +17 -0
- package/templates/app/skeleton/.nvmrc +1 -1
- package/templates/app/skeleton/.sops.yaml +1 -1
- package/templates/app/skeleton/.starcistacks/application-stacks.yaml +134 -1
- package/templates/app/skeleton/.starcistacks/dev/README.md +16 -0
- package/templates/app/skeleton/.starcistacks/dev/environment.json +5 -0
- package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +71 -0
- package/templates/app/skeleton/.starcistacks/dev/runtime/env/KEYS.md +14 -0
- package/templates/app/skeleton/.starcistacks/production/README.md +37 -0
- package/templates/app/skeleton/.starcistacks/production/environment.json +5 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/Caddyfile +15 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +9 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +142 -0
- package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +14 -0
- package/templates/app/skeleton/README.md +6 -6
- package/templates/app/skeleton/scripts/codegen.mjs +1 -2
- package/templates/app/skeleton-lite/.editorconfig +16 -0
- package/templates/app/skeleton-lite/.gitattributes +8 -0
- package/templates/app/skeleton-lite/.github/dependabot.yml +17 -0
- package/templates/app/skeleton-lite/.nvmrc +1 -0
- package/templates/app/skeleton-lite/.sops.yaml +3 -2
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +193 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +20 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +2 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +16 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +8 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +120 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -0
- package/templates/app/skeleton-lite/.starcistacks/production/README.md +38 -0
- package/templates/app/skeleton-lite/.starcistacks/production/environment.json +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/Caddyfile +11 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/images.lock.yaml +8 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +121 -0
- package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +21 -0
- package/templates/app/skeleton-lite/.starciwork/brand/index.yaml +12 -0
- package/templates/app/skeleton-lite/.starciwork/workspace.yaml +9 -0
- package/templates/app/skeleton-lite/README.md +8 -4
- package/templates/app/skeleton-lite/supabase/config.toml +8 -1
- package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +15 -6
- package/templates/app/starciwork.gitignore-lite +6 -0
- package/templates/app/upgrade-full/.starcistacks/dev/README.md +13 -7
- package/templates/be/image/worker/Dockerfile +1 -1
- package/templates/be/inbox/database-client.spec.ts.tpl +22 -28
- package/templates/be/inbox/database-client.ts.tpl +14 -28
- package/templates/be/inbox/delivery-recorded.event.ts.tpl +36 -0
- package/templates/be/inbox/event-index.ts.tpl +1 -0
- package/templates/be/inbox/events.json.tpl +10 -0
- package/templates/be/inbox/migration.sql.tpl +1 -0
- package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +42 -0
- package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +9 -0
- package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +6 -0
- package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +19 -0
- package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +2 -0
- package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +16 -0
- package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +45 -0
- package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +15 -0
- package/templates/be/inbox/platform/event-bus/index.ts.tpl +5 -0
- package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +5 -0
- package/templates/be/inbox/platform/inbox/inbox.decorators.ts.tpl +9 -0
- package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +6 -0
- package/templates/be/inbox/platform/inbox/inbox.module.ts.tpl +19 -0
- package/templates/be/inbox/platform/inbox/inbox.options.ts.tpl +2 -0
- package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +10 -0
- package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +41 -0
- package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +19 -0
- package/templates/be/inbox/platform/inbox/index.ts.tpl +3 -0
- package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +7 -0
- package/templates/be/inbox/platform/migration.sql.tpl +48 -0
- package/templates/be/inbox/service.spec.ts.tpl +49 -20
- package/templates/be/inbox/service.ts.tpl +27 -16
- package/templates/be/inbox/sql.ts.tpl +2 -2
- package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -3
- package/templates/be/inbox/webhook-door.ts.tpl +4 -3
- package/templates/be/patterns/api/feature.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/api/feature.module.ts.tpl +2 -1
- package/templates/be/patterns/api/feature.options.ts.tpl +2 -0
- package/templates/be/patterns/api/graphql.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/api/graphql.module.ts.tpl +3 -2
- package/templates/be/patterns/api/graphql.options.ts.tpl +2 -0
- package/templates/be/patterns/cli/group.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/cli/group.module.ts.tpl +2 -1
- package/templates/be/patterns/cli/group.options.ts.tpl +2 -0
- package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +0 -3
- package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +0 -2
- package/templates/be/patterns/jobs/job.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/jobs/job.module.ts.tpl +2 -1
- package/templates/be/patterns/jobs/job.options.ts.tpl +2 -0
- package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/jobs/job.queue.module.ts.tpl +6 -3
- package/templates/be/patterns/jobs/job.queue.options.ts.tpl +2 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +3 -1
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +25 -9
- package/templates/be/patterns/queues/platform/queue.config.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform/queue.index.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +2 -2
- package/templates/be/patterns/queues/platform/queue.module.ts.tpl +11 -0
- package/templates/be/patterns/queues/platform/queue.options.ts.tpl +10 -0
- package/templates/be/patterns/queues/queue.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/queues/queue.module.ts.tpl +2 -1
- package/templates/be/patterns/queues/queue.options.ts.tpl +2 -0
- package/templates/be/patterns/reactors/message.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/reactors/message.module.ts.tpl +6 -3
- package/templates/be/patterns/reactors/message.options.ts.tpl +2 -0
- package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/reactors/reactor.module.ts.tpl +2 -1
- package/templates/be/patterns/reactors/reactor.options.ts.tpl +2 -0
- package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +188 -0
- package/templates/be/patterns/realtime/channel.gateway.ts.tpl +22 -0
- package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +2 -1
- package/templates/be/patterns/realtime/channel.graphql.options.ts.tpl +2 -0
- package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +110 -0
- package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +7 -0
- package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/saga/saga.message.module.ts.tpl +6 -3
- package/templates/be/patterns/saga/saga.message.options.ts.tpl +2 -0
- package/templates/be/patterns/saga/saga.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/saga/saga.module.ts.tpl +2 -1
- package/templates/be/patterns/saga/saga.options.ts.tpl +2 -0
- package/templates/be/patterns/webhooks/webhook.door.spec.ts.tpl +3 -3
- package/templates/be/patterns/webhooks/webhook.door.ts.tpl +3 -3
- package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +2 -1
- package/templates/be/patterns/webhooks/webhook.http.options.ts.tpl +2 -0
- package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +80 -26
- package/templates/be/platform/graphql/localize-errors.mapper.ts +2 -5
- package/templates/be/skeleton/apps/__app__/src/__app__.env.ts +21 -0
- package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +1 -1
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +15 -45
- package/templates/be/skeleton/apps/__app__/src/compose/api.ts +4 -0
- package/templates/be/skeleton/apps/__app__/src/compose/domain.ts +11 -0
- package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +44 -0
- package/templates/be/skeleton/apps/__app__/src/compose/system.ts +5 -0
- package/templates/be/skeleton/apps/__app__/src/compose/webhooks.ts +4 -0
- package/templates/be/skeleton/apps/__app__/src/main.ts +18 -10
- package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
- package/templates/be/skeleton/apps/cli/src/cli.env.ts +13 -0
- package/templates/be/skeleton/apps/cli/src/cli.options.ts +7 -17
- package/templates/be/skeleton/apps/cli/src/compose/cli.ts +5 -0
- package/templates/be/skeleton/apps/cli/src/compose/platform.ts +24 -0
- package/templates/be/skeleton/apps/cli/src/main.ts +3 -2
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.contracts.ts +2 -0
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.handler.ts +22 -0
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.query.ts +11 -0
- package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/api/system-health/system-health.module.ts +5 -3
- package/templates/be/skeleton/src/features/api/system-health/system-health.options.ts +2 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/ready.controller.ts +19 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module.ts +8 -3
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/cli.module.ts +3 -2
- package/templates/be/skeleton/src/features/cli/cli.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +2 -1
- package/templates/be/skeleton/src/features/cli/migrate/migrate.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +2 -1
- package/templates/be/skeleton/src/features/cli/seed/seed.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +5 -6
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +56 -4
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +36 -5
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +39 -8
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +14 -8
- package/templates/be/skeleton/src/modules/domain/identity/credential.contracts.ts +26 -0
- package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +119 -0
- package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +43 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +16 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -1
- package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +22 -4
- package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +8 -2
- package/templates/be/skeleton/src/modules/domain/identity/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +6 -6
- package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +11 -4
- package/templates/be/skeleton/src/modules/platform/database/database-probe.service.spec.ts +32 -0
- package/templates/be/skeleton/src/modules/platform/database/database-probe.service.ts +14 -0
- package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
- package/templates/be/skeleton/src/modules/platform/database/index.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +8 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +25 -6
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +16 -6
- package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +130 -0
- package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +58 -0
- package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +3 -0
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -4
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -10
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +3 -1
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/http-security/index.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +12 -5
- package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +13 -8
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.contracts.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.decorators.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +86 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +39 -0
- package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +139 -131
- package/templates/be/skeleton/src/modules/platform/i18n/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +91 -67
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +5 -4
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +0 -2
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +20 -6
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +16 -4
- package/templates/be/skeleton/src/modules/platform/retry/index.ts +3 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.port.ts +11 -0
- package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.spec.ts +36 -0
- package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.ts +10 -0
- package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +54 -0
- package/templates/be/skeleton-lite/apps/api/src/api.env.ts +25 -0
- package/templates/be/skeleton-lite/apps/api/src/api.options.ts +1 -1
- package/templates/be/skeleton-lite/apps/api/src/app.module.ts +18 -58
- package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +4 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +10 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +47 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/system.ts +5 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/webhooks.ts +4 -0
- package/templates/be/skeleton-lite/apps/api/src/main.ts +5 -13
- package/templates/be/skeleton-lite/apps/cli/src/cli.env.ts +17 -0
- package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +71 -5
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +29 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +68 -15
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +14 -9
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +9 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +16 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +19 -4
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +5 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -1
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -3
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +17 -5
- package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +24 -13
- package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +5 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/errors/database.error.ts +16 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +3 -0
- package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +5 -2
- package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +4 -1
- package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +7 -0
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +29 -0
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +16 -0
- package/templates/be/table/api/feature.module-definition.ts.tpl +6 -0
- package/templates/be/table/api/feature.module.ts.tpl +2 -1
- package/templates/be/table/api/feature.options.ts.tpl +2 -0
- package/templates/be/table/api/graphql.module-definition.ts.tpl +6 -0
- package/templates/be/table/api/graphql.module.ts.tpl +3 -2
- package/templates/be/table/api/graphql.options.ts.tpl +2 -0
- package/templates/be/table/migration.sql.tpl +1 -0
- package/templates/be/upgrade-full/apps/cli/src/app.module.ts +4 -6
- package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +7 -0
- package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +5 -0
- package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +9 -0
- package/templates/be/upgrade-full/apps/cli/src/main.ts +3 -0
- package/templates/be/upgrade-full/src/features/cli/cli.module.ts +3 -2
- package/templates/fe/auth/supabase-client.ts.tpl +34 -0
- package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +11 -0
- package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/index.tsx +6 -1
- package/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx +4 -0
- package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +31 -0
- package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/index.tsx +24 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -1
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +3 -4
- package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +15 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +18 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +19 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +7 -1
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/en.json +57 -0
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +6 -0
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/en.json +31 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +1 -1
- package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -0
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +1 -34
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +7 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +57 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +30 -24
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +1 -1
- package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/locale.ts +2 -2
- package/upgrade/check.mjs +3 -1
- package/upgrade/file-state.mjs +68 -0
- package/upgrade/index.mjs +58 -94
- package/runtime/scripts/api/git/log.mjs +0 -5
- package/templates/app/skeleton-lite/.starciwork/shell/index.yaml +0 -55
|
@@ -3,9 +3,11 @@ import { canonical, isInside } from './config.mjs';
|
|
|
3
3
|
import { parseContract, installedSWR } from './next-data-contract.mjs';
|
|
4
4
|
import { relativePath, unwrapExpression } from './typescript.mjs';
|
|
5
5
|
import { anyDescendant, normalizedSymbol, normalizedSymbolValue, valueSymbol as sharedValueSymbol } from './ast-walks.mjs';
|
|
6
|
-
import {
|
|
6
|
+
import { sourceLocation } from '../../lib/ts-ast.mjs';
|
|
7
7
|
import { byCodeUnit } from '../../lib/list.mjs';
|
|
8
8
|
import { createNextDataKeyInspector } from './next-data-key.mjs';
|
|
9
|
+
import { functionNode, constInitializer, inferredSymbol } from './next-data-symbols.mjs';
|
|
10
|
+
import { inferredFetcher, inferredIdentities, inferredKeyBindings } from './next-data-inference.mjs';
|
|
9
11
|
export const SWR_KEY_RULE_ID = 'FE_SWR_KEY_IDENTITY';
|
|
10
12
|
export const SWR_MUTATION_RULE_ID = 'FE_SWR_MUTATION_RESOURCE_IDENTITY';
|
|
11
13
|
export const SWR_DATA_RULE_IDS = [SWR_KEY_RULE_ID, SWR_MUTATION_RULE_ID];
|
|
@@ -155,15 +157,6 @@ function discoverCalls(config, context, targets) {
|
|
|
155
157
|
return calls;
|
|
156
158
|
}
|
|
157
159
|
|
|
158
|
-
function functionNode(ts, declaration) {
|
|
159
|
-
if (ts.isFunctionDeclaration(declaration) && declaration.body) return declaration;
|
|
160
|
-
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
161
|
-
const value = unwrapExpression(ts, declaration.initializer);
|
|
162
|
-
if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) return value;
|
|
163
|
-
}
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
160
|
function exportedHook(config, context, entry, reasons) {
|
|
168
161
|
const source = context.files.find(candidate => canonical(candidate.fileName) === canonical(path.join(config.root, ...entry.path.split('/'))));
|
|
169
162
|
if (!source) {
|
|
@@ -223,19 +216,15 @@ function resolvedIdentities(ts, checker, fn, entry, reasons) {
|
|
|
223
216
|
return identities.length === entry.identities.length ? identities : null;
|
|
224
217
|
}
|
|
225
218
|
|
|
226
|
-
function constInitializer(ts, symbol) {
|
|
227
|
-
const declarations = symbol?.getDeclarations?.() ?? [];
|
|
228
|
-
if (declarations.length !== 1 || !ts.isVariableDeclaration(declarations[0]) || !declarations[0].initializer
|
|
229
|
-
|| !isConstVariable(ts, declarations[0])) return null;
|
|
230
|
-
return declarations[0].initializer;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
219
|
function violation(config, call, ruleId, message, extra = {}) {
|
|
234
220
|
return { ruleId, path: relativePath(config.root, call.source.fileName), ...sourceLocation(call.source, call.node), message, ...extra };
|
|
235
221
|
}
|
|
236
222
|
|
|
237
|
-
const
|
|
238
|
-
SWR_KEY_RULE_ID, SWR_MUTATION_RULE_ID }
|
|
223
|
+
const keyInspectorDependencies = { anyDescendant, normalizedSymbol, unwrapExpression, constInitializer, sourceLocation, violation,
|
|
224
|
+
SWR_KEY_RULE_ID, SWR_MUTATION_RULE_ID };
|
|
225
|
+
const inspectKey = createNextDataKeyInspector(keyInspectorDependencies);
|
|
226
|
+
|
|
227
|
+
const inspectInferredKey = createNextDataKeyInspector({ ...keyInspectorDependencies, normalizedSymbol: inferredSymbol });
|
|
239
228
|
|
|
240
229
|
function isUnshadowedCommonJsRequire(ts, checker, expression, seen = new Set(), depth = 0) {
|
|
241
230
|
expression = unwrapExpression(ts, expression);
|
|
@@ -372,7 +361,26 @@ function lifecycleCoverage(ruleIds, contract, installed, calls, details) {
|
|
|
372
361
|
swr: { name: installed.name, version: installed.version, major: installed.major } };
|
|
373
362
|
}
|
|
374
363
|
|
|
375
|
-
|
|
364
|
+
function inspectDefaultLifecycle(config, context, installed, lifecycleCalls, violations, reasons) {
|
|
365
|
+
for (const call of lifecycleCalls) {
|
|
366
|
+
const fetcher = inferredFetcher(context.ts, call.checker, call.node.arguments[1]);
|
|
367
|
+
if (!fetcher) {
|
|
368
|
+
reasons.push(`${relativePath(config.root, call.source.fileName)}:${sourceLocation(call.source, call.node).line} fetcher inputs cannot be inferred; dataLifecycle is required to declare identities`);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const entry = { id: `swr:${relativePath(config.root, call.source.fileName)}:${sourceLocation(call.source, call.node).line}`,
|
|
372
|
+
path: relativePath(config.root, call.source.fileName), export: call.owner?.name?.text ?? '<inferred hook>', kind: call.kind };
|
|
373
|
+
const identities = inferredIdentities(context, call, fetcher, reasons);
|
|
374
|
+
const selected = new Set(identities.map(identity => identity.id));
|
|
375
|
+
const findings = [];
|
|
376
|
+
inspectInferredKey(config, context, entry, call, [...identities, ...inferredKeyBindings(context, call, reasons)], findings, reasons);
|
|
377
|
+
violations.push(...findings.filter(finding => finding.identity === undefined || selected.has(finding.identity)));
|
|
378
|
+
}
|
|
379
|
+
const details = [...new Set(reasons)].sort(byCodeUnit);
|
|
380
|
+
return { violations, coverage: lifecycleCoverage([...SWR_DATA_RULE_IDS], { hooks: lifecycleCalls }, installed, lifecycleCalls.length, details) };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Check declared lifecycles, or infer captured request identities when production SWR has no declaration. */
|
|
376
384
|
export function checkFrontendDataLifecycle(config, context) {
|
|
377
385
|
const ruleIds = [...SWR_DATA_RULE_IDS];
|
|
378
386
|
const references = swrReferences(config, context);
|
|
@@ -381,17 +389,17 @@ export function checkFrontendDataLifecycle(config, context) {
|
|
|
381
389
|
return unavailableDataLifecycle(ruleIds, [String(error.message ?? error)]);
|
|
382
390
|
}
|
|
383
391
|
if (!contract && !references.found) return { violations: [], coverage: { status: 'not-applicable', ruleIds } };
|
|
384
|
-
if (!contract) return { violations: [], coverage: { status: 'unavailable', ruleIds,
|
|
385
|
-
details: ['package.json#starci.codePatterns.next.dataLifecycle is required when production source selects SWR', ...references.unsupported] } };
|
|
386
392
|
const reasons = [];
|
|
387
393
|
const violations = [];
|
|
388
394
|
reasons.push(...references.unsupported);
|
|
395
|
+
if (!contract && references.unsupported.length) reasons.push('package.json#starci.codePatterns.next.dataLifecycle is required for unsupported SWR bindings');
|
|
389
396
|
let installed;
|
|
390
397
|
try { installed = installedSWR(config.packageRoot ?? config.root); } catch (error) {
|
|
391
398
|
return { violations, coverage: { status: 'unavailable', ruleIds, details: [String(error.message ?? error)] } };
|
|
392
399
|
}
|
|
393
|
-
if (installed.major !== contract.swr.major) reasons.push(`installed swr ${installed.version} does not match declared major ${contract.swr.major}`);
|
|
400
|
+
if (contract && installed.major !== contract.swr.major) reasons.push(`installed swr ${installed.version} does not match declared major ${contract.swr.major}`);
|
|
394
401
|
const { lifecycleCalls } = lifecycleCallSets(config, context, installed, reasons);
|
|
402
|
+
if (!contract) return inspectDefaultLifecycle(config, context, installed, lifecycleCalls, violations, reasons);
|
|
395
403
|
const { hookCache, entriesByHook } = hookEntries(config, context, contract, reasons);
|
|
396
404
|
inspectHookLifecycle(config, context, lifecycleCalls, hookCache, entriesByHook, violations, reasons);
|
|
397
405
|
addUndeclaredLifecycleReasons(config, lifecycleCalls, hookCache, reasons);
|
|
@@ -96,9 +96,23 @@ function checkBackend(graph) {
|
|
|
96
96
|
const reached = reachFrom(graph, rootFiles);
|
|
97
97
|
const violations = [];
|
|
98
98
|
reportUncomposedBackendOwners(byUnit, reached, roots, counts, violations);
|
|
99
|
+
reportUncomposedTransports(graph, reached, roots, counts, violations);
|
|
99
100
|
return { violations, coverage: { status: 'checked', ...counts } };
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
/** A reached feature does not compose its other transport modules. Judge each module's runtime path. */
|
|
104
|
+
function reportUncomposedTransports(graph, reached, roots, counts, violations) {
|
|
105
|
+
for (const [rel, node] of graph.files) {
|
|
106
|
+
if (!node.owner || !rel.startsWith('src/features/') || TEST_FILE.test(rel)) continue;
|
|
107
|
+
if (!/\/transport\/[^/]+\/[^/]+\.module\.[cm]?[jt]sx?$/.test(rel) || reached.has(rel)) continue;
|
|
108
|
+
counts.notComposed += 1;
|
|
109
|
+
violations.push({
|
|
110
|
+
ruleId: 'BE_FEATURE_NOT_COMPOSED', path: rel, owner: node.owner.root, slot: node.slot, appRoots: roots,
|
|
111
|
+
message: `Transport module ${rel} is not composed into any app: no runtime import path leads from ${roots.join(', ')} to it (a type-only import composes nothing).`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
102
116
|
/** Route table of one app: arrays of segments, each {kind:'static'|'dyn'|'catch'|'optcatch', text}. */
|
|
103
117
|
function routeSegmentsOf(parts) {
|
|
104
118
|
const segments = [];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { machineKit } from './machine-ast.mjs';
|
|
2
|
+
import { isComposeGroup, machineKit } from './machine-ast.mjs';
|
|
3
3
|
import { callName } from './supabase-ast.mjs';
|
|
4
4
|
import { isTestWorldSlot } from '../test-world-slot.mjs';
|
|
5
5
|
import { byCodeUnit } from '../../lib/list.mjs';
|
|
@@ -7,14 +7,15 @@ import { byCodeUnit } from '../../lib/list.mjs';
|
|
|
7
7
|
/**
|
|
8
8
|
* R45 `register-once` (BE_MODULE_SHAPE), the module graph read from the app roots (owner rule of 2026-09-30, the
|
|
9
9
|
* reference's `CodingModule imports DeviceModule` shape). Each capability has one representative module: the only module
|
|
10
|
-
* of the capability an app registers, once, in `apps/<app>/src/app.module.ts` as `X.register({ isGlobal: true, ... })
|
|
11
|
-
*
|
|
10
|
+
* of the capability an app registers, once, in `apps/<app>/src/app.module.ts` as `X.register({ isGlobal: true, ... })`, or in a
|
|
11
|
+
* composition group `apps/<app>/src/compose/<area>.ts` the root spreads (an app root here means the root and its groups).
|
|
12
|
+
* It may split itself into sub-modules imported as `imports: [Sub.register({})]`; a sub-module is never in an app and has one
|
|
12
13
|
* importer, its parent. Over every `@Module` class of the program:
|
|
13
14
|
*
|
|
14
15
|
* - a module in an app root's imports (a `X.register(...)` call or a listed class, helpers of the root included) is
|
|
15
16
|
* listed once there, is imported by no other module, and, when it is a capability module of src/modules, is registered
|
|
16
17
|
* with the literal `isGlobal: true` (a bare capability class is not a registration);
|
|
17
|
-
* - a module imported by a non-app module has exactly one importer and appears in no app root; the one exception is
|
|
18
|
+
* - a module imported by a non-app module (as `Sub.register({})`) has exactly one importer and appears in no app root; the one exception is
|
|
18
19
|
* the feature case: a feature's application module is imported by each protocol module of the same feature;
|
|
19
20
|
* - `isGlobal: true` appears only in an app root.
|
|
20
21
|
*/
|
|
@@ -57,6 +58,7 @@ function enclosingModule(ts, modules, node) {
|
|
|
57
58
|
/** Records the reference an element of an imports list, or a register call, makes to a module. */
|
|
58
59
|
function noteReference(scan, element, viaImports) {
|
|
59
60
|
const { ts, kit, modules, checker, file, seen, references, isAppRoot } = scan;
|
|
61
|
+
if (seen.has(element)) return;
|
|
60
62
|
const call = callName(ts, element) === 'register';
|
|
61
63
|
if (ts.isCallExpression(element) && !call) return;
|
|
62
64
|
const target = targetOf(kit, modules, checker, call ? element.expression.expression : element);
|
|
@@ -72,14 +74,19 @@ const isImportsList = (scan, node) => scan.ts.isPropertyAssignment(node) && scan
|
|
|
72
74
|
const isStrayGlobal = (scan, node) => scan.ts.isPropertyAssignment(node) && scan.kit.propertyNameText(node.name) === 'isGlobal'
|
|
73
75
|
&& node.initializer.kind === scan.ts.SyntaxKind.TrueKeyword && !scan.isAppRoot(scan.file) && !scan.isWorld(scan.file);
|
|
74
76
|
|
|
77
|
+
// A composition group's module list is any array of the group: the root spreads it into its imports.
|
|
78
|
+
const isGroupList = (scan, node) => scan.ts.isArrayLiteralExpression(node) && isComposeGroup(scan.file.rel);
|
|
79
|
+
|
|
75
80
|
function visitReferenceNode(scan, node) {
|
|
76
81
|
if (isImportsList(scan, node)) {
|
|
77
82
|
for (const element of node.initializer.elements) noteReference(scan, element, true);
|
|
83
|
+
} else if (isGroupList(scan, node)) {
|
|
84
|
+
for (const element of node.elements) noteReference(scan, element, true);
|
|
78
85
|
} else if (callName(scan.ts, node) === 'register' && !scan.seen.has(node)) {
|
|
79
86
|
noteReference(scan, node, false);
|
|
80
87
|
}
|
|
81
88
|
if (isStrayGlobal(scan, node)) {
|
|
82
|
-
scan.report(scan.file, node, '`isGlobal: true` appears only in the app root (apps/<app>/src/app.module.ts), where the representative module of a capability is registered; a module never decides its own globality.');
|
|
89
|
+
scan.report(scan.file, node, '`isGlobal: true` appears only in the app root (apps/<app>/src/app.module.ts or a group of apps/<app>/src/compose/), where the representative module of a capability is registered; a module never decides its own globality.');
|
|
83
90
|
}
|
|
84
91
|
return true;
|
|
85
92
|
}
|
|
@@ -170,11 +177,12 @@ export function checkRegisterOnce(input) {
|
|
|
170
177
|
const { ts } = kit;
|
|
171
178
|
const violations = [];
|
|
172
179
|
const report = (file, node, message, extra = {}) => violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
|
|
173
|
-
const isAppRoot = file => Boolean(file.slot?.startsWith('be.app.')) && path.posix.basename(file.rel) === 'app.module.ts';
|
|
180
|
+
const isAppRoot = file => Boolean(file.slot?.startsWith('be.app.')) && (path.posix.basename(file.rel) === 'app.module.ts' || isComposeGroup(file.rel));
|
|
174
181
|
// The test world (slot be.tests.world) is the test composition root: useTestWorld registers capability modules with
|
|
175
182
|
// `isGlobal: true` the way an app root does. A spec (integration, contract, e2e) is not a composition root.
|
|
176
183
|
const isWorld = file => isTestWorldSlot(file.slot);
|
|
177
|
-
const appOf = file => config.apps.find(app => file.rel === `apps/${app.name}/src/app.module.ts`
|
|
184
|
+
const appOf = file => config.apps.find(app => file.rel === `apps/${app.name}/src/app.module.ts`
|
|
185
|
+
|| (isComposeGroup(file.rel) && file.rel.startsWith(`apps/${app.name}/src/compose/`)))?.name ?? null;
|
|
178
186
|
const modules = moduleDeclarations(graph, kit, ts);
|
|
179
187
|
const references = moduleReferences(graph, kit, ts, modules, isWorld, isAppRoot, report);
|
|
180
188
|
const rootReferences = references.filter(item => item.root);
|
|
@@ -4,6 +4,7 @@ import { lsFiles } from '../../api/git/ls-files.mjs';
|
|
|
4
4
|
import { revParseQuery } from '../../api/git/rev-parse-query.mjs';
|
|
5
5
|
import { gitOutputOf } from '../../lib/git.mjs';
|
|
6
6
|
import { trimTrailingSlashes } from '../trailing-slashes.mjs';
|
|
7
|
+
import { requiredFileApplies } from '../slot-required.mjs';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* HFS check 5: the files and directories the slot manifest requires (knowledge/hfs/slots.yaml `requires`,
|
|
@@ -173,7 +174,7 @@ function requirementExists(tree, target) {
|
|
|
173
174
|
}
|
|
174
175
|
|
|
175
176
|
function reportMissingRequirement(requirement, tree, report) {
|
|
176
|
-
if (requirementExists(tree, requirement.target)) return;
|
|
177
|
+
if (requirementExists(tree, requirement.target) || !requiredFileApplies(requirement.slot, requirement.target, tree.files)) return;
|
|
177
178
|
const rootExists = !requirement.root || tree.directories.has(requirement.root);
|
|
178
179
|
if (!rootExists) {
|
|
179
180
|
report(ruleFor(requirement.slot, requirement.target, false), requirement.root, requirement.slot, requirement.root, { kind: 'directory' },
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { isInside } from './config.mjs';
|
|
3
3
|
import { relativePath, unwrapExpression } from './typescript.mjs';
|
|
4
|
-
import { commonJsRequireReasons, decoratorCallee, moduleExportsOf, nodeDecorators, normalizedSymbol, normalizedSymbolValue, programSourcesOf, returnedExpressions, selectedNode, valueSymbol, violation } from './ast-walks.mjs';
|
|
4
|
+
import { commonJsRequireReasons, constructedDecoratorKind, decoratorCallee, moduleExportsOf, mutableDecoratorKind, nodeDecorators, normalizedSymbol, normalizedSymbolValue, programSourcesOf, returnedExpressions, selectedNode, valueSymbol, violation } from './ast-walks.mjs';
|
|
5
5
|
import { createBackendSourceShapeChecker } from './source-names-shape.mjs';
|
|
6
|
+
import { byCodeUnit } from '../../lib/list.mjs';
|
|
6
7
|
|
|
7
8
|
const SOURCE_LAYOUT_RULE_ID = 'BE_FEATURE_LAYOUT_INVALID';
|
|
8
9
|
const SOURCE_NAME_RULE_ID = 'BE_SOURCE_FORM';
|
|
9
10
|
const FRAMEWORK_EXPORTS = new Map([
|
|
10
|
-
['@nestjs/graphql', new Set(['Args', 'ArgsType', 'InputType', 'Mutation', 'ObjectType', 'Query', 'registerEnumType'])],
|
|
11
|
+
['@nestjs/graphql', new Set(['Args', 'ArgsType', 'InputType', 'Mutation', 'ObjectType', 'Query', 'Subscription', 'registerEnumType'])],
|
|
11
12
|
['typeorm', new Set(['Entity', 'EntitySchema', 'MigrationInterface', 'ViewEntity'])],
|
|
12
13
|
]);
|
|
13
14
|
const APPLICATION_ROLES = new Set(['command', 'contracts', 'handler', 'query']);
|
|
@@ -237,6 +238,11 @@ function graphqlField({ config, context, sourceFile, node, decorator, kind, nami
|
|
|
237
238
|
}
|
|
238
239
|
const methodName = node.name && (context.ts.isIdentifier(node.name) || context.ts.isStringLiteralLike(node.name)) ? node.name.text : null;
|
|
239
240
|
const name = selected.status === 'literal' ? selected.value : methodName;
|
|
241
|
+
if (selected.status === 'literal') {
|
|
242
|
+
if (!methodName) namingReasons.push(`${relativePath(config.root, sourceFile.fileName)} cannot prove the @${kind} method name`);
|
|
243
|
+
else if (selected.value !== methodName) violations.push(violation(config, sourceFile, selected.node, SOURCE_NAME_RULE_ID,
|
|
244
|
+
`GraphQL @${kind} field name ${selected.value} must equal its method name ${methodName}.`));
|
|
245
|
+
}
|
|
240
246
|
if (!name) namingReasons.push(`${relativePath(config.root, sourceFile.fileName)} cannot prove the @${kind} field name`);
|
|
241
247
|
else if (!camelCase(name)) violations.push(violation(config, sourceFile, selected.node ?? node.name ?? node, SOURCE_NAME_RULE_ID,
|
|
242
248
|
`GraphQL field name ${name} must be camelCase.`));
|
|
@@ -316,7 +322,46 @@ const checkBackendSourceShapeImpl = createBackendSourceShapeChecker({
|
|
|
316
322
|
TRACE_NEW,
|
|
317
323
|
});
|
|
318
324
|
|
|
325
|
+
function inspectSubscriptionSource({ config, context, sourceFile, checker, framework, configuredRoot, namingReasons, violations }) {
|
|
326
|
+
const visit = node => {
|
|
327
|
+
for (const decorator of nodeDecorators(context.ts, node)) {
|
|
328
|
+
const kind = decoratorKind(context.ts, checker, decorator, framework.bySymbol);
|
|
329
|
+
if (kind === 'Subscription') graphqlField({ config, context, sourceFile, node, decorator, kind, namingReasons, violations });
|
|
330
|
+
else if (!kind && !configuredRoot) {
|
|
331
|
+
const dynamic = mutableDecoratorKind(context.ts, checker, decorator, framework.bySymbol)
|
|
332
|
+
?? constructedDecoratorKind(context.ts, checker, decorator, framework.bySymbol, TRACE_NEW);
|
|
333
|
+
if (['Subscription', 'multiple framework', 'unproven framework'].includes(dynamic)) {
|
|
334
|
+
namingReasons.push(`${relativePath(config.root, sourceFile.fileName)} uses a mutable or constructed ${dynamic} decorator identity`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
context.ts.forEachChild(node, visit);
|
|
339
|
+
};
|
|
340
|
+
visit(sourceFile);
|
|
341
|
+
}
|
|
342
|
+
|
|
319
343
|
export function checkBackendSourceShape(config, context) {
|
|
320
|
-
|
|
344
|
+
const result = checkBackendSourceShapeImpl(config, context);
|
|
345
|
+
const roots = absoluteRoots(config.root, [...config.backend.features, ...config.backend.modules]);
|
|
346
|
+
const localFiles = new Set(context.files.map(file => canonical(file.fileName)));
|
|
347
|
+
const frameworkByChecker = new Map();
|
|
348
|
+
const namingReasons = [];
|
|
349
|
+
// The shared shape walk dispatches Query and Mutation; subscriptions use the same naming relation here.
|
|
350
|
+
for (const sourceFile of context.files) {
|
|
351
|
+
const fileName = canonical(sourceFile.fileName);
|
|
352
|
+
const configuredRoot = locatedRoot(roots, fileName);
|
|
353
|
+
const owner = config.hfs?.ownerOf?.(relativePath(config.root, fileName));
|
|
354
|
+
const slot = owner ? config.hfs.slot(owner.slot) : null;
|
|
355
|
+
if (!configuredRoot && !(slot?.tier === 'feature' && slot.profiles?.includes('be'))) continue;
|
|
356
|
+
const checker = context.checkerFor(fileName);
|
|
357
|
+
if (!frameworkByChecker.has(checker)) frameworkByChecker.set(checker, frameworkTargets(config, context, checker, localFiles));
|
|
358
|
+
const framework = frameworkByChecker.get(checker);
|
|
359
|
+
inspectSubscriptionSource({ config, context, sourceFile, checker, framework, configuredRoot, namingReasons, violations: result.violations });
|
|
360
|
+
}
|
|
361
|
+
if (namingReasons.length) result.coverage.naming = {
|
|
362
|
+
status: 'unavailable', reason: 'one or more backend source naming relations are not statically proved',
|
|
363
|
+
details: [...new Set([...(result.coverage.naming.details ?? []), ...namingReasons])].sort(byCodeUnit),
|
|
364
|
+
};
|
|
365
|
+
return result;
|
|
321
366
|
}
|
|
322
367
|
export { SOURCE_LAYOUT_RULE_ID, SOURCE_NAME_RULE_ID };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { catFile } from '../../api/git/cat-file.mjs';
|
|
2
|
+
import { diff } from '../../api/git/diff.mjs';
|
|
3
|
+
import { gitOutputOf } from '../../lib/git.mjs';
|
|
4
|
+
|
|
5
|
+
/** Eligible additions, copies, edits and rename destinations in the index, read in one blob batch. */
|
|
6
|
+
export function stagedConsumerSources(root, accept) {
|
|
7
|
+
try {
|
|
8
|
+
const options = { cwd: root, maxBuffer: 256 * 1024 * 1024 };
|
|
9
|
+
const files = gitOutputOf(diff(['--cached', '--relative', '--name-only', '--diff-filter=ACMR', '-z', '--', '.'], options))
|
|
10
|
+
.split('\0').filter(file => file && accept(file));
|
|
11
|
+
if (!files.length) return [];
|
|
12
|
+
// :./ keeps paths relative to this side's root even when git's top level is the parent app folder.
|
|
13
|
+
// -z terminates requests with NUL; output headers and blob trailers still use LF.
|
|
14
|
+
const bytes = gitOutputOf(catFile(['--batch', '-z'], {
|
|
15
|
+
...options, encoding: null, input: files.map(file => `:./${file}\0`).join(''),
|
|
16
|
+
}));
|
|
17
|
+
if (!Buffer.isBuffer(bytes)) return [];
|
|
18
|
+
const sources = [];
|
|
19
|
+
let cursor = 0;
|
|
20
|
+
for (const file of files) {
|
|
21
|
+
const end = bytes.indexOf(10, cursor);
|
|
22
|
+
if (end < 0) return [];
|
|
23
|
+
const header = /^[0-9a-f]+ blob (\d+)$/u.exec(bytes.subarray(cursor, end).toString('utf8'));
|
|
24
|
+
if (!header) return []; // An index changed during the read: discard this supplement, keep disk consumers.
|
|
25
|
+
const size = Number(header[1]);
|
|
26
|
+
const start = end + 1;
|
|
27
|
+
const next = start + size;
|
|
28
|
+
if (!Number.isSafeInteger(size) || next >= bytes.length || bytes[next] !== 10) return [];
|
|
29
|
+
sources.push({ file, text: bytes.subarray(start, next).toString('utf8') });
|
|
30
|
+
cursor = next + 1;
|
|
31
|
+
}
|
|
32
|
+
return sources;
|
|
33
|
+
} catch {
|
|
34
|
+
// A non-git fixture (or an unavailable index) still uses the existing filesystem consumers.
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// supabase-ast.mjs - shared TypeScript-symbol and expression helpers for the front-end and back-end Supabase
|
|
2
2
|
// architecture checks. Recognition follows imported declarations and inferred types, never local spelling alone.
|
|
3
|
-
|
|
3
|
+
import { unwrapExpression } from '../../lib/ts-analysis.mjs';
|
|
4
|
+
import { accessName as memberName, hasAnyFlag } from '../../lib/ts-ast.mjs';
|
|
5
|
+
export { accessName as memberName } from '../../lib/ts-ast.mjs';
|
|
6
|
+
|
|
7
|
+
export const SUPABASE_MODULES = Object.freeze({ has: name => typeof name === 'string' && name.startsWith('@supabase/') });
|
|
4
8
|
|
|
5
9
|
export const moduleNameOf = (ts, node) => {
|
|
6
10
|
if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) return node.moduleSpecifier.text;
|
|
@@ -15,7 +19,7 @@ export const importedFrom = (kit, checker, node, modules, names = null) => {
|
|
|
15
19
|
return Boolean(binding && modules.has(binding.module) && (names === null || names.has(binding.name)));
|
|
16
20
|
};
|
|
17
21
|
|
|
18
|
-
const declarationFromSupabase = (declaration) => /(?:^|\/)node_modules\/@supabase\/(
|
|
22
|
+
const declarationFromSupabase = (declaration) => /(?:^|\/)node_modules\/@supabase\/[^/]+(?:\/|$)/u
|
|
19
23
|
.test(String(declaration?.getSourceFile?.().fileName ?? '').replaceAll('\\', '/'));
|
|
20
24
|
|
|
21
25
|
const typeFromSupabase = (checker, node) => {
|
|
@@ -38,12 +42,7 @@ const signatureFromSupabase = (checker, call) => {
|
|
|
38
42
|
try { return declarationFromSupabase(checker.getResolvedSignature(call)?.declaration); } catch { return false; }
|
|
39
43
|
};
|
|
40
44
|
|
|
41
|
-
export const unwrap = (ts, node) => {
|
|
42
|
-
let current = node;
|
|
43
|
-
while (current && (ts.isParenthesizedExpression(current) || ts.isAwaitExpression(current) || ts.isAsExpression(current)
|
|
44
|
-
|| ts.isTypeAssertionExpression(current) || ts.isNonNullExpression(current))) current = current.expression;
|
|
45
|
-
return current;
|
|
46
|
-
};
|
|
45
|
+
export const unwrap = (ts, node) => unwrapExpression(ts, node, { awaitExpressions: true, satisfiesExpressions: false });
|
|
47
46
|
|
|
48
47
|
const isSupabaseValue = (kit, checker, node) => {
|
|
49
48
|
const current = unwrap(kit.ts, node);
|
|
@@ -53,23 +52,19 @@ const isSupabaseValue = (kit, checker, node) => {
|
|
|
53
52
|
return typeFromSupabase(checker, current);
|
|
54
53
|
};
|
|
55
54
|
|
|
56
|
-
export const callName = (ts, call) => ts.isCallExpression(call)
|
|
55
|
+
export const callName = (ts, call) => ts.isCallExpression(call) ? memberName(ts, call.expression) : null;
|
|
57
56
|
|
|
58
57
|
export const chainParts = (ts, expression) => {
|
|
59
58
|
const names = [];
|
|
60
59
|
let current = unwrap(ts, expression);
|
|
61
60
|
while (current) {
|
|
62
61
|
if (ts.isCallExpression(current)) {
|
|
63
|
-
if (ts.isPropertyAccessExpression(current.expression)) {
|
|
64
|
-
names.unshift(current.expression.name.text);
|
|
65
|
-
current = unwrap(ts, current.expression.expression);
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
62
|
current = unwrap(ts, current.expression);
|
|
69
63
|
continue;
|
|
70
64
|
}
|
|
71
|
-
if (ts.isPropertyAccessExpression(current)) {
|
|
72
|
-
|
|
65
|
+
if (ts.isPropertyAccessExpression(current) || ts.isElementAccessExpression(current)) {
|
|
66
|
+
const name = memberName(ts, current);
|
|
67
|
+
if (name) names.unshift(name);
|
|
73
68
|
current = unwrap(ts, current.expression);
|
|
74
69
|
continue;
|
|
75
70
|
}
|
|
@@ -79,7 +74,47 @@ export const chainParts = (ts, expression) => {
|
|
|
79
74
|
};
|
|
80
75
|
|
|
81
76
|
export const isSupabaseCall = (kit, checker, node) => kit.ts.isCallExpression(node)
|
|
82
|
-
&& (signatureFromSupabase(checker, node) || isSupabaseValue(kit, checker,
|
|
77
|
+
&& (signatureFromSupabase(checker, node) || isSupabaseValue(kit, checker,
|
|
78
|
+
kit.ts.isPropertyAccessExpression(node.expression) || kit.ts.isElementAccessExpression(node.expression) ? node.expression.expression : node.expression));
|
|
79
|
+
|
|
80
|
+
const authStorageValue = (kit, checker, expression, seen = new Set()) => {
|
|
81
|
+
const current = unwrap(kit.ts, expression);
|
|
82
|
+
if (!current || seen.has(current)) return false;
|
|
83
|
+
seen.add(current);
|
|
84
|
+
if (['auth', 'storage'].includes(memberName(kit.ts, current)) && isSupabaseValue(kit, checker, current.expression)) return true;
|
|
85
|
+
if (checker.getTypeAtLocation(current).symbol?.declarations?.some(declaration =>
|
|
86
|
+
/\/node_modules\/@supabase\/(?:storage-js|auth-js)\//u.test(String(declaration.getSourceFile().fileName).replaceAll('\\', '/')))) return true;
|
|
87
|
+
if (!kit.ts.isIdentifier(current)) return false;
|
|
88
|
+
return kit.declarationsOf(checker, current).some(declaration =>
|
|
89
|
+
(kit.ts.isVariableDeclaration(declaration) || kit.ts.isPropertyDeclaration(declaration)) && declaration.initializer
|
|
90
|
+
&& authStorageValue(kit, checker, declaration.initializer, seen));
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const chainAccesses = (ts, node) => {
|
|
94
|
+
const accesses = [];
|
|
95
|
+
let current = unwrap(ts, node);
|
|
96
|
+
while (current) {
|
|
97
|
+
if (ts.isCallExpression(current)) current = unwrap(ts, current.expression);
|
|
98
|
+
else if (ts.isPropertyAccessExpression(current) || ts.isElementAccessExpression(current)) {
|
|
99
|
+
accesses.unshift(current);
|
|
100
|
+
current = unwrap(ts, current.expression);
|
|
101
|
+
} else break;
|
|
102
|
+
}
|
|
103
|
+
return accesses;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** The first forbidden SDK access in a call chain, anchored once even when more methods follow it. */
|
|
107
|
+
export const supabaseDataAccess = (kit, checker, node, members, { storageAllowed = false } = {}) => {
|
|
108
|
+
for (const access of chainAccesses(kit.ts, node)) {
|
|
109
|
+
const name = memberName(kit.ts, access);
|
|
110
|
+
if (storageAllowed && (name === 'storage' || name === 'auth')) return null;
|
|
111
|
+
if (members.has(name) && isSupabaseValue(kit, checker, access.expression)) {
|
|
112
|
+
if (storageAllowed && authStorageValue(kit, checker, access.expression)) return null;
|
|
113
|
+
return { name, node: access };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
};
|
|
83
118
|
|
|
84
119
|
export const isDatabaseType = (ts, node) => {
|
|
85
120
|
if (!node) return false;
|
|
@@ -109,3 +144,165 @@ export const contextTypesClient = (kit, checker, call) => {
|
|
|
109
144
|
export const reportAt = (violations, kit, ruleId, file, node, message, extra = {}) => {
|
|
110
145
|
violations.push({ ruleId, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
|
|
111
146
|
};
|
|
147
|
+
|
|
148
|
+
const declarationsOf = (ts, checker, node) => {
|
|
149
|
+
const target = ts.isPropertyAccessExpression(node) ? node.name : node;
|
|
150
|
+
// A shorthand property (`issuer,`) names the variable it reads, not the property it creates.
|
|
151
|
+
let symbol = ts.isShorthandPropertyAssignment(target.parent ?? {}) && target.parent.name === target
|
|
152
|
+
? checker.getShorthandAssignmentValueSymbol(target.parent)
|
|
153
|
+
: checker.getSymbolAtLocation(target);
|
|
154
|
+
if (symbol && hasAnyFlag(symbol.flags, ts.SymbolFlags.Alias)) symbol = checker.getAliasedSymbol(symbol);
|
|
155
|
+
return symbol?.declarations ?? [];
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const configType = (checker, node) => {
|
|
159
|
+
const type = checker.getTypeAtLocation(node);
|
|
160
|
+
return [type.symbol, type.aliasSymbol].some(symbol => symbol?.declarations?.some(declaration =>
|
|
161
|
+
/(?:\.config|\.options|\.env)\.ts$|\/modules\/config\/|\/node_modules\/@nestjs\/config\//u.test(String(declaration.getSourceFile().fileName).replaceAll('\\', '/'))
|
|
162
|
+
|| /(?:Config|Options|Env)$/u.test(symbol.name)));
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/** Trace issuer construction through configuration values and local URL/string transformations. */
|
|
166
|
+
const issuerFromConfig = (ts, checker, expression, seen = new Set()) => {
|
|
167
|
+
const node = unwrap(ts, expression);
|
|
168
|
+
if (!node || seen.has(node)) return false;
|
|
169
|
+
const next = new Set(seen).add(node);
|
|
170
|
+
if (ts.isIdentifier(node)) {
|
|
171
|
+
return declarationsOf(ts, checker, node).some(declaration => {
|
|
172
|
+
if (ts.isVariableDeclaration(declaration) && declaration.initializer) return issuerFromConfig(ts, checker, declaration.initializer, next);
|
|
173
|
+
if (ts.isParameter(declaration) || ts.isPropertyDeclaration(declaration)) return configType(checker, node);
|
|
174
|
+
return /(?:\.config|\.env)\.ts$|\/modules\/config\//u.test(String(declaration.getSourceFile().fileName).replaceAll('\\', '/'));
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) {
|
|
178
|
+
return issuerFromConfig(ts, checker, node.expression, next) || declarationsOf(ts, checker, node).some(declaration =>
|
|
179
|
+
(ts.isParameter(declaration) || ts.isPropertyDeclaration(declaration)) && !declaration.initializer && configType(checker, node));
|
|
180
|
+
}
|
|
181
|
+
if (ts.isTemplateExpression(node)) return node.templateSpans.some(span => issuerFromConfig(ts, checker, span.expression, next));
|
|
182
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
183
|
+
return issuerFromConfig(ts, checker, node.left, next) || issuerFromConfig(ts, checker, node.right, next);
|
|
184
|
+
}
|
|
185
|
+
if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
|
|
186
|
+
return issuerFromConfig(ts, checker, node.expression, next) || node.arguments?.some(argument => issuerFromConfig(ts, checker, argument, next));
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const objectValue = (ts, checker, expression) => {
|
|
192
|
+
const node = unwrap(ts, expression);
|
|
193
|
+
if (!node) return null;
|
|
194
|
+
if (ts.isObjectLiteralExpression(node)) return node;
|
|
195
|
+
if (!ts.isIdentifier(node)) return null;
|
|
196
|
+
return declarationsOf(ts, checker, node).find(declaration => ts.isVariableDeclaration(declaration)
|
|
197
|
+
&& declaration.initializer && ts.isObjectLiteralExpression(unwrap(ts, declaration.initializer)))?.initializer ?? null;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const literalString = (ts, checker, expression) => {
|
|
201
|
+
const node = unwrap(ts, expression);
|
|
202
|
+
if (!node) return null;
|
|
203
|
+
if (ts.isStringLiteralLike(node)) return node.text;
|
|
204
|
+
const type = checker.getTypeAtLocation(node);
|
|
205
|
+
return type.isStringLiteral?.() ? type.value : null;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/** A role read must belong to the payload produced by this verification, including destructuring aliases. */
|
|
209
|
+
const verifiedPayload = (ts, checker, expression, call, seen = new Set()) => {
|
|
210
|
+
const node = unwrap(ts, expression);
|
|
211
|
+
if (!node || seen.has(node)) return false;
|
|
212
|
+
const next = new Set(seen).add(node);
|
|
213
|
+
if ((ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) && memberName(ts, node) === 'payload') {
|
|
214
|
+
const value = unwrap(ts, node.expression);
|
|
215
|
+
if (value === call) return true;
|
|
216
|
+
if (ts.isIdentifier(value)) return declarationsOf(ts, checker, value).some(declaration =>
|
|
217
|
+
ts.isVariableDeclaration(declaration) && unwrap(ts, declaration.initializer) === call);
|
|
218
|
+
}
|
|
219
|
+
if (!ts.isIdentifier(node)) return false;
|
|
220
|
+
return declarationsOf(ts, checker, node).some(declaration => {
|
|
221
|
+
if (ts.isVariableDeclaration(declaration)) return declaration.initializer && verifiedPayload(ts, checker, declaration.initializer, call, next);
|
|
222
|
+
if (!ts.isBindingElement(declaration) || (declaration.propertyName ?? declaration.name).getText() !== 'payload') return false;
|
|
223
|
+
return unwrap(ts, declaration.parent.parent.initializer) === call;
|
|
224
|
+
});
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const rejects = (ts, statement) => {
|
|
228
|
+
if (ts.isBlock(statement)) {
|
|
229
|
+
const exit = statement.statements.find(item => ts.isThrowStatement(item) || ts.isReturnStatement(item));
|
|
230
|
+
return Boolean(exit && rejects(ts, exit));
|
|
231
|
+
}
|
|
232
|
+
if (ts.isThrowStatement(statement)) return true;
|
|
233
|
+
if (!ts.isReturnStatement(statement)) return false;
|
|
234
|
+
const value = statement.expression && unwrap(ts, statement.expression);
|
|
235
|
+
return !value || value.kind === ts.SyntaxKind.NullKeyword || value.kind === ts.SyntaxKind.FalseKeyword
|
|
236
|
+
|| (ts.isCallExpression(value) && /^(?:refused|err|fail|failure)$/u.test(value.expression.getText()));
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const roleCondition = (ts, checker, condition, call, equality = false) => {
|
|
240
|
+
const node = unwrap(ts, condition);
|
|
241
|
+
if (!node || !ts.isBinaryExpression(node)) return false;
|
|
242
|
+
const combined = equality ? ts.SyntaxKind.AmpersandAmpersandToken : ts.SyntaxKind.BarBarToken;
|
|
243
|
+
if (node.operatorToken.kind === combined) return roleCondition(ts, checker, node.left, call, equality) || roleCondition(ts, checker, node.right, call, equality);
|
|
244
|
+
const operators = equality ? [ts.SyntaxKind.EqualsEqualsEqualsToken, ts.SyntaxKind.EqualsEqualsToken]
|
|
245
|
+
: [ts.SyntaxKind.ExclamationEqualsEqualsToken, ts.SyntaxKind.ExclamationEqualsToken];
|
|
246
|
+
if (!operators.includes(node.operatorToken.kind)) return false;
|
|
247
|
+
const role = expression => (ts.isPropertyAccessExpression(expression) || ts.isElementAccessExpression(expression))
|
|
248
|
+
&& memberName(ts, expression) === 'role' && verifiedPayload(ts, checker, expression.expression, call);
|
|
249
|
+
return (role(node.left) && literalString(ts, checker, node.right) === 'authenticated')
|
|
250
|
+
|| (role(node.right) && literalString(ts, checker, node.left) === 'authenticated');
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const returnsPrincipal = (ts, statement) => {
|
|
254
|
+
let accepted = false;
|
|
255
|
+
const visit = node => {
|
|
256
|
+
if (ts.isFunctionLike(node)) return;
|
|
257
|
+
if (ts.isReturnStatement(node) && !rejects(ts, node)) accepted = true;
|
|
258
|
+
ts.forEachChild(node, visit);
|
|
259
|
+
};
|
|
260
|
+
visit(statement);
|
|
261
|
+
return accepted;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const statementsAfterVerify = (ts, call) => {
|
|
265
|
+
let statement = call;
|
|
266
|
+
while (statement.parent && !ts.isBlock(statement.parent)) statement = statement.parent;
|
|
267
|
+
const block = statement.parent;
|
|
268
|
+
if (!block || !ts.isBlock(block)) return [];
|
|
269
|
+
return block.statements.slice(block.statements.indexOf(statement) + 1);
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const rejectsRoleGuard = (ts, checker, statement, call) => {
|
|
273
|
+
if (!ts.isIfStatement(statement)) return false;
|
|
274
|
+
if (roleCondition(ts, checker, statement.expression, call) && rejects(ts, statement.thenStatement)) return true;
|
|
275
|
+
return Boolean(statement.elseStatement && roleCondition(ts, checker, statement.expression, call, true) && rejects(ts, statement.elseStatement));
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const rejectsOtherRoles = (ts, checker, call) => {
|
|
279
|
+
for (const item of statementsAfterVerify(ts, call)) {
|
|
280
|
+
if (ts.isReturnStatement(item) || ts.isThrowStatement(item)) return false;
|
|
281
|
+
if (rejectsRoleGuard(ts, checker, item, call)) return true;
|
|
282
|
+
if (returnsPrincipal(ts, item)) return false;
|
|
283
|
+
}
|
|
284
|
+
return false;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
/** R260 is shared by the architecture machine and the published typed ESLint rule. */
|
|
288
|
+
export const jwtPolicyProblems = (ts, checker, call) => {
|
|
289
|
+
const problems = [];
|
|
290
|
+
const options = objectValue(ts, checker, call.arguments[2]);
|
|
291
|
+
const property = name => options?.properties.findLast(entry =>
|
|
292
|
+
(ts.isPropertyAssignment(entry) || ts.isShorthandPropertyAssignment(entry))
|
|
293
|
+
&& (ts.isIdentifier(entry.name) || ts.isStringLiteralLike(entry.name)) && entry.name.text === name);
|
|
294
|
+
const value = name => {
|
|
295
|
+
const entry = property(name);
|
|
296
|
+
return entry && unwrap(ts, ts.isPropertyAssignment(entry) ? entry.initializer : entry.name);
|
|
297
|
+
};
|
|
298
|
+
const algorithms = value('algorithms');
|
|
299
|
+
const secure = new Set(['ES256', 'RS256', 'EdDSA']);
|
|
300
|
+
if (!algorithms || !ts.isArrayLiteralExpression(algorithms) || !algorithms.elements.length
|
|
301
|
+
|| algorithms.elements.some(item => !ts.isStringLiteralLike(item) || !secure.has(item.text))
|
|
302
|
+
|| options?.properties.some(entry => ts.isSpreadAssignment(entry))) {
|
|
303
|
+
problems.push('BE_JWT_ALG_PINNED: algorithms must be a non-empty literal subset of ES256, RS256 and EdDSA.');
|
|
304
|
+
}
|
|
305
|
+
if (!issuerFromConfig(ts, checker, value('issuer'))) problems.push('BE_JWT_ALG_PINNED: issuer must derive from typed configuration.');
|
|
306
|
+
if (!rejectsOtherRoles(ts, checker, call)) problems.push('BE_JWT_ALG_PINNED: the verified principal must reject every role other than authenticated before returning it.');
|
|
307
|
+
return problems;
|
|
308
|
+
};
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { machineKit } from './machine-ast.mjs';
|
|
5
5
|
import { importedFrom, reportAt, unwrap } from './supabase-ast.mjs';
|
|
6
|
+
import { accessName } from '../../lib/ts-ast.mjs';
|
|
6
7
|
|
|
7
8
|
const BE_SUPABASE_SLOT = 'be.integrations.supabase';
|
|
8
9
|
const BE_JWT_VERIFIED = 'BE_SUPABASE_JWT_VERIFIED';
|
|
@@ -47,12 +48,6 @@ const importedCall = (kit, checker, call) => {
|
|
|
47
48
|
return owner?.name === 'default' ? { module: owner.module, name: call.expression.name.text } : null;
|
|
48
49
|
};
|
|
49
50
|
|
|
50
|
-
const accessName = (ts, node) => {
|
|
51
|
-
if (ts.isPropertyAccessExpression(node)) return node.name.text;
|
|
52
|
-
if (ts.isElementAccessExpression(node) && ts.isStringLiteralLike(node.argumentExpression)) return node.argumentExpression.text;
|
|
53
|
-
return null;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
51
|
const requestLike = (ts, node) => {
|
|
57
52
|
const current = unwrap(ts, node);
|
|
58
53
|
if (ts.isIdentifier(current)) return /^(?:req|request)$/iu.test(current.text);
|