@starci/hfs 4.0.8 → 4.0.10
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 +9 -0
- package/LICENSE +21 -0
- package/README.md +81 -39
- package/emit/contracts.mjs +56 -9
- package/emit/db-types.mjs +169 -0
- package/emit/events.mjs +41 -0
- package/emit/operations-worker.mjs +1 -1
- package/emit/operations.mjs +2 -2
- package/emit/schema-worker.mjs +1 -1
- package/lint/bound-sys.cjs +50 -0
- package/lint/run.mjs +131 -24
- package/package.json +16 -10
- package/report/sonar.mjs +8 -8
- package/runtime/engine/admission.mjs +308 -0
- package/runtime/engine/canonical-json.mjs +10 -0
- package/runtime/engine/config.mjs +486 -0
- package/runtime/engine/db/blob.mjs +315 -0
- package/runtime/engine/db/ledger-paths.mjs +83 -0
- package/runtime/engine/db/ledger.mjs +1118 -0
- package/runtime/engine/db/machine-connection.mjs +135 -0
- package/runtime/engine/db/machine-schema.mjs +84 -0
- package/runtime/engine/db/machine.mjs +1367 -0
- package/runtime/engine/db/migrations/machine/0001-init.sql +924 -0
- package/runtime/engine/db/migrations/runtime/0001-init.sql +1108 -0
- package/runtime/engine/db/provider-reservations.mjs +101 -0
- package/runtime/engine/digest.mjs +16 -0
- package/runtime/engine/invalid-config.mjs +15 -0
- package/runtime/engine/orca-config.mjs +27 -0
- package/runtime/engine/refuse.mjs +11 -0
- package/runtime/engine/secrets.mjs +130 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +162 -31
- package/runtime/knowledge/hfs/peer-integrations.yaml +2 -2
- package/runtime/knowledge/hfs/rules.yaml +2059 -0
- package/runtime/knowledge/hfs/slots.yaml +912 -120
- package/runtime/knowledge/patterns/be/api.yaml +369 -0
- package/runtime/knowledge/patterns/be/cli.yaml +297 -0
- package/runtime/knowledge/patterns/be/domain.yaml +113 -0
- package/runtime/knowledge/patterns/be/event-bus.yaml +285 -0
- package/runtime/knowledge/patterns/be/jobs.yaml +261 -0
- package/runtime/knowledge/patterns/be/projections.yaml +178 -0
- package/runtime/knowledge/patterns/be/queues.yaml +236 -0
- package/runtime/knowledge/patterns/be/reactors.yaml +140 -0
- package/runtime/knowledge/patterns/be/realtime.yaml +222 -0
- package/runtime/knowledge/patterns/be/saga.yaml +248 -0
- package/runtime/knowledge/patterns/be/webhooks.yaml +221 -0
- package/runtime/knowledge/patterns/fe/folder.yaml +131 -25
- package/runtime/knowledge/sonar-gate.yaml +52 -18
- package/runtime/modules/kernel/failure-codes.yaml +611 -51
- package/runtime/modules/models/registry.yaml +553 -0
- package/runtime/modules/models/runtimes.yaml +695 -0
- package/runtime/modules/ops/_labels.yaml +53 -0
- package/runtime/scripts/api/fs/forbidden-root.mjs +51 -0
- package/runtime/scripts/api/fs/is-link-like.mjs +25 -0
- package/runtime/scripts/api/fs/lib.mjs +16 -2
- package/runtime/scripts/api/fs/safe-remove.mjs +12 -132
- package/runtime/scripts/api/git/config-get.mjs +10 -0
- package/runtime/scripts/api/git/diff.mjs +5 -0
- package/runtime/scripts/api/git/lib.mjs +18 -26
- package/runtime/scripts/api/git/log.mjs +5 -0
- package/runtime/scripts/api/git/ls-files.mjs +6 -0
- package/runtime/scripts/api/git/ls-tree.mjs +5 -0
- package/runtime/scripts/api/git/merge-base.mjs +9 -0
- package/runtime/scripts/api/git/remote.mjs +5 -0
- package/runtime/scripts/api/git/rev-parse-query.mjs +5 -0
- package/runtime/scripts/api/git/show.mjs +6 -0
- package/runtime/scripts/api/node/lib.mjs +14 -0
- package/runtime/scripts/api/node/spawn-node.mjs +6 -0
- package/runtime/scripts/api/process/lib.mjs +111 -0
- package/runtime/scripts/api/process/owned-process.mjs +78 -0
- package/runtime/scripts/api/process/resolve-real-tool.mjs +46 -0
- package/runtime/scripts/api/process/run-program.mjs +7 -0
- package/runtime/scripts/api/process/stop-owned-process.mjs +9 -0
- package/runtime/scripts/api/sops/decrypt.mjs +9 -12
- package/runtime/scripts/api/sops/lib.mjs +255 -0
- package/runtime/scripts/api/sops/seal.mjs +22 -0
- package/runtime/scripts/connectors/lib.mjs +488 -0
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +220 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +16 -23
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +16 -34
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +3 -1
- package/runtime/scripts/hfs/architecture/clones.mjs +4 -4
- package/runtime/scripts/hfs/architecture/config.mjs +9 -6
- package/runtime/scripts/hfs/architecture/connection-map.mjs +20 -8
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +88 -0
- package/runtime/scripts/hfs/architecture/context-map.mjs +237 -0
- package/runtime/scripts/hfs/architecture/context-owner.mjs +82 -0
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +47 -0
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +60 -0
- package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +4 -10
- package/runtime/scripts/hfs/architecture/contracts.mjs +33 -162
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +58 -29
- package/runtime/scripts/hfs/architecture/doc-language.mjs +1 -1
- package/runtime/scripts/hfs/architecture/entrypoint.mjs +2 -1
- package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -2
- package/runtime/scripts/hfs/architecture/feature-shape.mjs +5 -2
- package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/hfs/architecture/frontend.mjs +17 -43
- package/runtime/scripts/hfs/architecture/hfs.mjs +24 -39
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +1 -1
- package/runtime/scripts/hfs/architecture/index.mjs +17 -4
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +44 -11
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +27 -0
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +30 -6
- package/runtime/scripts/hfs/architecture/next-data.mjs +22 -63
- package/runtime/scripts/hfs/architecture/owners.mjs +35 -15
- package/runtime/scripts/hfs/architecture/register-once.mjs +2 -1
- package/runtime/scripts/hfs/architecture/registration.mjs +23 -81
- package/runtime/scripts/hfs/architecture/required-files.mjs +20 -12
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +17 -114
- package/runtime/scripts/hfs/architecture/server-action.mjs +24 -0
- package/runtime/scripts/hfs/architecture/side-boundary.mjs +16 -0
- package/runtime/scripts/hfs/architecture/source-names.mjs +27 -147
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +59 -18
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +64 -0
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +111 -0
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +131 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +42 -0
- package/runtime/scripts/hfs/architecture/supabase.mjs +434 -0
- package/runtime/scripts/hfs/architecture/surface.mjs +9 -3
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +9 -19
- package/runtime/scripts/hfs/architecture/tiers.mjs +47 -5
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +3 -3
- package/runtime/scripts/hfs/architecture/typescript.mjs +12 -26
- package/runtime/scripts/hfs/architecture.mjs +4 -4
- package/runtime/scripts/hfs/check.mjs +75 -72
- package/runtime/scripts/hfs/coverage-scope.mjs +162 -0
- package/runtime/scripts/hfs/declaration-shape.mjs +62 -0
- package/runtime/scripts/hfs/declaration-slots.mjs +75 -0
- package/runtime/scripts/hfs/edition-slots.mjs +66 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +109 -32
- package/runtime/scripts/hfs/param-names.mjs +18 -0
- package/runtime/scripts/hfs/path-findings.mjs +1 -1
- package/runtime/scripts/hfs/repo-identity.mjs +9 -6
- package/runtime/scripts/hfs/rule-params-shape.mjs +50 -0
- package/runtime/scripts/hfs/rules/app-root.mjs +51 -0
- package/runtime/scripts/hfs/rules/cli.mjs +37 -0
- package/runtime/scripts/hfs/rules/contract-compat.mjs +71 -0
- package/runtime/scripts/hfs/rules/contract.mjs +10 -9
- package/runtime/scripts/hfs/rules/database-config.mjs +102 -0
- package/runtime/scripts/hfs/rules/database-constants.mjs +13 -0
- package/runtime/scripts/hfs/rules/database-migrations.mjs +111 -0
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +233 -0
- package/runtime/scripts/hfs/rules/database-sql.mjs +336 -0
- package/runtime/scripts/hfs/rules/database.mjs +68 -0
- package/runtime/scripts/hfs/rules/deps.mjs +1 -1
- package/runtime/scripts/hfs/rules/docker.mjs +259 -0
- package/runtime/scripts/hfs/rules/edition.mjs +111 -0
- package/runtime/scripts/hfs/rules/event-bus.mjs +153 -0
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +1 -1
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +6 -5
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +9 -9
- package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -6
- package/runtime/scripts/hfs/rules/kinds.mjs +84 -0
- package/runtime/scripts/hfs/rules/lint-suppression.mjs +1 -1
- package/runtime/scripts/hfs/rules/monorepo.mjs +179 -0
- package/runtime/scripts/hfs/rules/peer-integrations.mjs +3 -3
- package/runtime/scripts/hfs/rules/pipeline.mjs +11 -21
- package/runtime/scripts/hfs/rules/proof-commands.mjs +2 -2
- package/runtime/scripts/hfs/rules/read.mjs +1 -1
- package/runtime/scripts/hfs/rules/repo-local-checks.mjs +2 -2
- package/runtime/scripts/hfs/rules/saga.mjs +170 -0
- package/runtime/scripts/hfs/rules/secrets.mjs +3 -3
- package/runtime/scripts/hfs/rules/services.mjs +220 -0
- package/runtime/scripts/hfs/rules/spec-placement.mjs +1 -1
- package/runtime/scripts/hfs/rules/stacks.mjs +3 -3
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +89 -0
- package/runtime/scripts/hfs/rules/test-topology.mjs +2 -2
- package/runtime/scripts/hfs/secret.mjs +209 -0
- package/runtime/scripts/hfs/slots.mjs +78 -108
- package/runtime/scripts/hfs/sql/pg-parse.mjs +46 -0
- package/runtime/scripts/hfs/test-world-slot.mjs +6 -0
- package/runtime/scripts/hfs/tree.mjs +5 -4
- package/runtime/scripts/hfs/typescript-programs.mjs +0 -5
- package/runtime/scripts/hfs/view.mjs +3 -2
- package/runtime/scripts/lib/clip.mjs +19 -0
- package/runtime/scripts/lib/display-names.mjs +259 -0
- package/runtime/scripts/lib/dockerfile.mjs +124 -0
- package/runtime/scripts/lib/env.mjs +10 -0
- package/runtime/scripts/lib/event-contract.mjs +103 -0
- package/runtime/scripts/lib/example-refs.mjs +158 -0
- package/runtime/scripts/lib/fs-kind.mjs +14 -4
- package/runtime/scripts/lib/git.mjs +36 -0
- package/runtime/scripts/lib/graphql-contract.mjs +1 -1
- package/runtime/scripts/lib/i18n.mjs +86 -0
- package/runtime/scripts/lib/json-schema.mjs +52 -0
- package/runtime/scripts/lib/json.mjs +13 -3
- package/runtime/scripts/lib/language.mjs +24 -5
- package/runtime/scripts/lib/list.mjs +20 -0
- package/runtime/scripts/lib/mutation-fence.mjs +15 -0
- package/runtime/scripts/lib/package-at.mjs +37 -0
- package/runtime/scripts/lib/path-key.mjs +57 -2
- package/runtime/scripts/lib/process-identity.mjs +6 -0
- package/runtime/scripts/lib/read-text.mjs +34 -0
- package/runtime/scripts/lib/read-yaml.mjs +18 -0
- package/runtime/scripts/lib/redact.mjs +161 -0
- package/runtime/scripts/lib/regex.mjs +4 -0
- package/runtime/scripts/lib/same-text.mjs +5 -0
- package/runtime/scripts/lib/secret-patterns.mjs +4 -3
- package/runtime/scripts/lib/sleep-sync.mjs +2 -1
- package/runtime/scripts/lib/sleep.mjs +7 -0
- package/runtime/scripts/lib/sops-envelope.mjs +112 -0
- package/runtime/scripts/lib/source-phrases.mjs +34 -0
- package/runtime/scripts/lib/sqlite.mjs +21 -0
- package/runtime/scripts/lib/stack-services.mjs +7 -31
- package/runtime/scripts/lib/ts-ast.mjs +35 -0
- package/runtime/scripts/lib/walk.mjs +4 -3
- package/scaffold/add-app.mjs +174 -0
- package/scaffold/add-cli-lite.mjs +257 -0
- package/scaffold/add-inbox-lite.mjs +314 -0
- package/scaffold/add-table.mjs +344 -0
- package/scaffold/add.mjs +299 -0
- package/scaffold/app.mjs +358 -69
- package/scaffold/edition-gate.mjs +25 -0
- package/scaffold/image.mjs +19 -0
- package/scaffold/lite-exports.mjs +186 -0
- package/scaffold/service.mjs +32 -26
- package/scaffold/supabase-ports.mjs +26 -0
- package/src/main.mjs +327 -0
- package/sync/cli.mjs +6 -9
- package/sync/format.mjs +1 -1
- package/sync/hygiene.mjs +10 -10
- package/sync/index.mjs +167 -104
- package/sync/managed.mjs +19 -11
- package/sync/sonar-key.mjs +5 -0
- package/templates/app/ci-workflows/github/workflows/ci.yml +14 -14
- package/templates/app/ci-workflows/github/workflows/images.yml +37 -0
- package/templates/app/ci-workflows/sonar-steps.yml +9 -0
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +66 -0
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +31 -0
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +37 -0
- package/templates/app/codegen-lite/scripts/codegen.mjs +32 -0
- package/templates/app/docker-ignore/dockerignore +50 -0
- package/templates/app/gitignore-lite +35 -0
- package/templates/app/hooks/husky/pre-commit +3 -8
- package/templates/app/hooks/husky/pre-push +45 -6
- package/templates/app/package-scripts/core.json +8 -0
- package/templates/app/package-scripts/package.json +3 -10
- package/templates/app/package-scripts-lite/package.json +15 -0
- package/templates/app/quality-config/codecov.yml +15 -3
- package/templates/app/quality-config/sonar-base.properties +4 -0
- package/templates/app/quality-config/sonar-project.properties +1 -4
- package/templates/app/quality-config-lite/sonar-project.properties +5 -0
- package/templates/app/skeleton/.starcistacks/dev/seeds/primary-notes.sql +5 -0
- package/templates/app/skeleton/.starciwork/features/system-health/index.yaml +4 -0
- package/templates/app/skeleton/.starciwork/{features/index.yaml → index.yaml} +2 -2
- package/templates/app/skeleton/README.md +7 -5
- package/templates/app/skeleton-lite/.sops.yaml +3 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +5 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/secrets/.gitkeep +1 -0
- package/templates/app/skeleton-lite/.starciwork/shell/index.yaml +55 -0
- package/templates/app/skeleton-lite/README.md +40 -0
- package/templates/app/skeleton-lite/supabase/config.toml +41 -0
- package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +56 -0
- package/templates/app/skeleton-lite/supabase/seed.sql +0 -0
- package/templates/app/task-graph/turbo.json +23 -0
- package/templates/app/tool-config-lite/prettierignore +2 -0
- package/templates/app/tool-config-lite/prettierrc +1 -0
- package/templates/app/upgrade-full/.starcistacks/application-stacks.yaml +38 -0
- package/templates/app/upgrade-full/.starcistacks/dev/README.md +10 -0
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/api.yaml +13 -0
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/compose.yaml +3 -0
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/postgres.yaml +12 -0
- package/templates/app/upgrade-full/.starcistacks/dev/infra/metadata.json +7 -0
- package/templates/be/common/main-listen.ts.partial +14 -0
- package/templates/be/common/sql-ident.ts.partial +7 -0
- package/templates/be/common/sql-tag.ts.partial +5 -0
- package/templates/be/image/api/Dockerfile +38 -0
- package/templates/be/image/cli/Dockerfile +38 -0
- package/templates/be/image/worker/Dockerfile +36 -0
- package/templates/be/inbox/database-client.ts.tpl +50 -0
- package/templates/be/inbox/index.ts.tpl +2 -0
- package/templates/be/inbox/migration.sql.tpl +29 -0
- package/templates/be/inbox/module-definition.ts.tpl +6 -0
- package/templates/be/inbox/module.ts.tpl +14 -0
- package/templates/be/inbox/options.ts.tpl +2 -0
- package/templates/be/inbox/service.ts.tpl +33 -0
- package/templates/be/inbox/sql.ts.tpl +16 -0
- package/templates/be/inbox/webhook-door.ts.tpl +36 -0
- package/templates/be/patterns/api/application.command.http.ts.tpl +10 -0
- package/templates/be/patterns/api/application.command.ts.tpl +10 -0
- package/templates/be/patterns/api/application.contracts.ts.tpl +11 -0
- package/templates/be/patterns/api/application.handler.http.ts.tpl +22 -0
- package/templates/be/patterns/api/application.handler.ts.tpl +22 -0
- package/templates/be/patterns/api/feature.index.http.ts.tpl +1 -0
- package/templates/be/patterns/api/feature.index.ts.tpl +1 -0
- package/templates/be/patterns/api/feature.module.ts.tpl +6 -0
- package/templates/be/patterns/api/graphql.input.ts.tpl +13 -0
- package/templates/be/patterns/api/graphql.mapper.ts.tpl +9 -0
- package/templates/be/patterns/api/graphql.module.ts.tpl +7 -0
- package/templates/be/patterns/api/graphql.resolver.ts.tpl +27 -0
- package/templates/be/patterns/api/graphql.type.ts.tpl +9 -0
- package/templates/be/patterns/api/http.controller.ts.tpl +27 -0
- package/templates/be/patterns/api/http.mapper.ts.tpl +9 -0
- package/templates/be/patterns/api/http.module.ts.tpl +7 -0
- package/templates/be/patterns/api/http.request.ts.tpl +10 -0
- package/templates/be/patterns/api/http.response.ts.tpl +5 -0
- package/templates/be/patterns/cli/group.cli.spec.ts.tpl +15 -0
- package/templates/be/patterns/cli/group.cli.ts.tpl +11 -0
- package/templates/be/patterns/cli/group.module.ts.tpl +7 -0
- package/templates/be/patterns/cli/sub.run.cli.spec.ts.tpl +17 -0
- package/templates/be/patterns/cli/sub.run.cli.ts.tpl +15 -0
- package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +22 -0
- package/templates/be/patterns/event-bus/platform/event-bus.config.ts.tpl +19 -0
- package/templates/be/patterns/event-bus/platform/event-bus.contracts.ts.tpl +58 -0
- package/templates/be/patterns/event-bus/platform/event-bus.decorators.ts.tpl +46 -0
- package/templates/be/patterns/event-bus/platform/event-bus.log-events.ts.tpl +12 -0
- package/templates/be/patterns/event-bus/platform/event-bus.module-definition.ts.tpl +10 -0
- package/templates/be/patterns/event-bus/platform/event-bus.module.ts.tpl +52 -0
- package/templates/be/patterns/event-bus/platform/event-bus.options.ts.tpl +19 -0
- package/templates/be/patterns/event-bus/platform/event-bus.port.ts.tpl +28 -0
- package/templates/be/patterns/event-bus/platform/event-bus.service.spec.ts.tpl +165 -0
- package/templates/be/patterns/event-bus/platform/event-bus.service.ts.tpl +93 -0
- package/templates/be/patterns/event-bus/platform/event-envelope.policy.spec.ts.tpl +90 -0
- package/templates/be/patterns/event-bus/platform/event-envelope.policy.ts.tpl +58 -0
- package/templates/be/patterns/event-bus/platform/event-relay.service.spec.ts.tpl +136 -0
- package/templates/be/patterns/event-bus/platform/event-relay.service.ts.tpl +56 -0
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +272 -0
- package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +128 -0
- package/templates/be/patterns/event-bus/platform/event-transport.port.ts.tpl +180 -0
- package/templates/be/patterns/event-bus/platform/event.policy.spec.ts.tpl +67 -0
- package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +38 -0
- package/templates/be/patterns/event-bus/platform/index.ts.tpl +11 -0
- package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.spec.ts.tpl +348 -0
- package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +241 -0
- package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +15 -0
- package/templates/be/patterns/event-bus/platform/persistence/connection.ts.tpl +8 -0
- package/templates/be/patterns/event-bus/platform/persistence/entities/event-outbox.entity.ts.tpl +37 -0
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.rows.ts.tpl +11 -0
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +12 -0
- package/templates/be/patterns/event-bus/platform/persistence/migrations/1789800009000-create-event-outbox.ts.tpl +26 -0
- package/templates/be/patterns/jobs/job.index.ts.tpl +1 -0
- package/templates/be/patterns/jobs/job.module.ts.tpl +6 -0
- package/templates/be/patterns/jobs/job.processor.ts.tpl +20 -0
- package/templates/be/patterns/jobs/job.queue.module.ts.tpl +20 -0
- package/templates/be/patterns/jobs/job.step.ts.tpl +19 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +31 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.ts.tpl +13 -0
- package/templates/be/patterns/jobs/platform/job-claim.service.spec.ts.tpl +150 -0
- package/templates/be/patterns/jobs/platform/job-claim.service.ts.tpl +100 -0
- package/templates/be/patterns/jobs/platform/job-runner.service.spec.ts.tpl +138 -0
- package/templates/be/patterns/jobs/platform/job-runner.service.ts.tpl +69 -0
- package/templates/be/patterns/jobs/platform/jobs.config.ts.tpl +11 -0
- package/templates/be/patterns/jobs/platform/jobs.contracts.ts.tpl +62 -0
- package/templates/be/patterns/jobs/platform/jobs.decorators.ts.tpl +31 -0
- package/templates/be/patterns/jobs/platform/jobs.error.ts.tpl +22 -0
- package/templates/be/patterns/jobs/platform/jobs.index.ts.tpl +11 -0
- package/templates/be/patterns/jobs/platform/jobs.log-events.ts.tpl +9 -0
- package/templates/be/patterns/jobs/platform/jobs.messages.ts.tpl +15 -0
- package/templates/be/patterns/jobs/platform/jobs.module-definition.ts.tpl +10 -0
- package/templates/be/patterns/jobs/platform/jobs.module.ts.tpl +32 -0
- package/templates/be/patterns/jobs/platform/jobs.options.ts.tpl +11 -0
- package/templates/be/patterns/jobs/platform/jobs.port.ts.tpl +36 -0
- package/templates/be/patterns/jobs/platform/persistence.connection.ts.tpl +8 -0
- package/templates/be/patterns/jobs/platform/persistence.job.entity.ts.tpl +53 -0
- package/templates/be/patterns/jobs/platform/persistence.jobs.rows.ts.tpl +13 -0
- package/templates/be/patterns/jobs/platform/persistence.jobs.sql.ts.tpl +33 -0
- package/templates/be/patterns/jobs/platform/persistence.migration.ts.tpl +29 -0
- package/templates/be/patterns/outbox/platform/index.ts.tpl +2 -0
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +96 -0
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +78 -0
- package/templates/be/patterns/outbox/platform/outbox.log-events.ts.tpl +5 -0
- package/templates/be/patterns/projections/migration.ts.tpl +18 -0
- package/templates/be/patterns/projections/persistence.connection.ts.tpl +8 -0
- package/templates/be/patterns/projections/projection.entity.ts.tpl +9 -0
- package/templates/be/patterns/projections/projection.index.ts.tpl +3 -0
- package/templates/be/patterns/projections/projection.module-definition.ts.tpl +7 -0
- package/templates/be/patterns/projections/projection.module.ts.tpl +18 -0
- package/templates/be/patterns/projections/projection.options.ts.tpl +2 -0
- package/templates/be/patterns/projections/projection.projection.spec.ts.tpl +55 -0
- package/templates/be/patterns/projections/projection.projection.ts.tpl +21 -0
- package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.spec.ts.tpl +142 -0
- package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.ts.tpl +81 -0
- package/templates/be/patterns/queues/platform/persistence.connection.ts.tpl +8 -0
- package/templates/be/patterns/queues/platform/persistence.migration.ts.tpl +25 -0
- package/templates/be/patterns/queues/platform/persistence.queue-outbox.entity.ts.tpl +25 -0
- package/templates/be/patterns/queues/platform/persistence.queue.rows.ts.tpl +9 -0
- package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +12 -0
- package/templates/be/patterns/queues/platform/queue-outbox.service.spec.ts.tpl +48 -0
- package/templates/be/patterns/queues/platform/queue-outbox.service.ts.tpl +17 -0
- package/templates/be/patterns/queues/platform/queue-relay.service.spec.ts.tpl +128 -0
- package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +48 -0
- package/templates/be/patterns/queues/platform/queue-transport.port.ts.tpl +115 -0
- package/templates/be/patterns/queues/platform/queue-worker.service.spec.ts.tpl +91 -0
- package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +45 -0
- package/templates/be/patterns/queues/platform/queue.config.ts.tpl +15 -0
- package/templates/be/patterns/queues/platform/queue.contracts.ts.tpl +31 -0
- package/templates/be/patterns/queues/platform/queue.decorators.ts.tpl +45 -0
- package/templates/be/patterns/queues/platform/queue.index.ts.tpl +7 -0
- package/templates/be/patterns/queues/platform/queue.log-events.ts.tpl +5 -0
- package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +10 -0
- package/templates/be/patterns/queues/platform/queue.module.ts.tpl +62 -0
- package/templates/be/patterns/queues/platform/queue.options.ts.tpl +22 -0
- package/templates/be/patterns/queues/platform/queue.policy.spec.ts.tpl +19 -0
- package/templates/be/patterns/queues/platform/queue.policy.ts.tpl +11 -0
- package/templates/be/patterns/queues/platform/queue.port.ts.tpl +14 -0
- package/templates/be/patterns/queues/queue.index.ts.tpl +3 -0
- package/templates/be/patterns/queues/queue.module.ts.tpl +6 -0
- package/templates/be/patterns/queues/queue.queue.ts.tpl +23 -0
- package/templates/be/patterns/reactors/application.command.ts.tpl +10 -0
- package/templates/be/patterns/reactors/application.contracts.ts.tpl +12 -0
- package/templates/be/patterns/reactors/application.handler.ts.tpl +22 -0
- package/templates/be/patterns/reactors/consumer.ts.tpl +19 -0
- package/templates/be/patterns/reactors/message.module.ts.tpl +20 -0
- package/templates/be/patterns/reactors/reactor.index.ts.tpl +1 -0
- package/templates/be/patterns/reactors/reactor.module.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.index.ts.tpl +1 -0
- package/templates/be/patterns/realtime/channel.input.ts.tpl +12 -0
- package/templates/be/patterns/realtime/channel.subscription.spec.ts.tpl +51 -0
- package/templates/be/patterns/realtime/channel.subscription.ts.tpl +32 -0
- package/templates/be/patterns/realtime/channel.type.ts.tpl +9 -0
- package/templates/be/patterns/saga/saga.compensate.command.ts.tpl +11 -0
- package/templates/be/patterns/saga/saga.compensate.contracts.ts.tpl +7 -0
- package/templates/be/patterns/saga/saga.compensate.handler.ts.tpl +22 -0
- package/templates/be/patterns/saga/saga.compensation.ts.tpl +21 -0
- package/templates/be/patterns/saga/saga.complete.command.ts.tpl +11 -0
- package/templates/be/patterns/saga/saga.complete.contracts.ts.tpl +7 -0
- package/templates/be/patterns/saga/saga.complete.handler.ts.tpl +22 -0
- package/templates/be/patterns/saga/saga.done.consumer.ts.tpl +21 -0
- package/templates/be/patterns/saga/saga.failed.consumer.ts.tpl +21 -0
- package/templates/be/patterns/saga/saga.index.ts.tpl +1 -0
- package/templates/be/patterns/saga/saga.message.module.ts.tpl +23 -0
- package/templates/be/patterns/saga/saga.module.ts.tpl +20 -0
- package/templates/be/patterns/saga/saga.service.spec.ts.tpl +70 -0
- package/templates/be/patterns/saga/saga.service.ts.tpl +44 -0
- package/templates/be/patterns/saga/saga.state.ts.tpl +14 -0
- package/templates/be/patterns/saga/saga.step.ts.tpl +11 -0
- package/templates/be/patterns/saga/saga.undo.command.ts.tpl +10 -0
- package/templates/be/patterns/saga/saga.undo.contracts.ts.tpl +5 -0
- package/templates/be/patterns/saga/saga.undo.handler.ts.tpl +21 -0
- package/templates/be/patterns/webhooks/webhook.door.spec.ts.tpl +72 -0
- package/templates/be/patterns/webhooks/webhook.door.ts.tpl +36 -0
- package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +6 -0
- package/templates/be/patterns/webhooks/webhook.index.ts.tpl +1 -0
- package/templates/be/patterns/webhooks/webhook.request.ts.tpl +9 -0
- package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +3 -0
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +15 -2
- package/templates/be/skeleton/apps/__app__/src/main.ts +7 -15
- package/templates/be/skeleton/apps/cli/src/app.module.ts +30 -0
- package/templates/be/skeleton/apps/cli/src/cli.options.ts +22 -0
- package/templates/be/skeleton/apps/cli/src/main.ts +24 -0
- package/templates/be/skeleton/src/features/cli/cli.module.ts +7 -0
- package/templates/be/skeleton/src/features/cli/index.ts +1 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +17 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +15 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +7 -0
- package/templates/be/skeleton/src/features/cli/migrate/subs/run.cli.spec.ts +17 -0
- package/templates/be/skeleton/src/features/cli/migrate/subs/run.cli.ts +18 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +17 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +15 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +7 -0
- package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +33 -0
- package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.ts +34 -0
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +13 -0
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +40 -0
- package/templates/be/skeleton/src/modules/domain/identity/messages/identity.messages.ts +1 -1
- package/templates/be/skeleton/src/modules/domain/note/errors/note.error.ts +16 -0
- package/templates/be/skeleton/src/modules/domain/note/index.ts +3 -0
- package/templates/be/skeleton/src/modules/domain/note/note.contracts.ts +19 -0
- package/templates/be/skeleton/src/modules/domain/note/note.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/domain/note/note.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/domain/note/note.module.ts +19 -0
- package/templates/be/skeleton/src/modules/domain/note/note.options.ts +2 -0
- package/templates/be/skeleton/src/modules/domain/note/note.service.spec.ts +76 -0
- package/templates/be/skeleton/src/modules/domain/note/note.service.ts +37 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/connection.ts +8 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/entities/note.entity.ts +17 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/migrations/1790000000000-init-note.ts +22 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/note.rows.ts +17 -0
- package/templates/be/skeleton/src/modules/domain/note/persistence/note.sql.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +20 -0
- package/templates/be/skeleton/src/modules/platform/config/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.contracts.ts +18 -0
- package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +41 -0
- package/templates/be/skeleton/src/modules/platform/database/connection-source.client.ts +17 -0
- package/templates/be/skeleton/src/modules/platform/database/database.module-definition.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/database/database.module.ts +77 -0
- package/templates/be/skeleton/src/modules/platform/database/database.options.ts +30 -0
- package/templates/be/skeleton/src/modules/platform/database/database.port.ts +65 -0
- package/templates/be/skeleton/src/modules/platform/database/database.sql.ts +47 -0
- package/templates/be/skeleton/src/modules/platform/database/errors/database.error.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/database/index.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +30 -0
- package/templates/be/skeleton/src/modules/platform/database/migration-runner.service.spec.ts +84 -0
- package/templates/be/skeleton/src/modules/platform/database/migration-runner.service.ts +23 -0
- package/templates/be/skeleton/src/modules/platform/database/primary.config.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/database/primary.connection.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +10 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +108 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +75 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-runner.service.spec.ts +88 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-runner.service.ts +29 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +48 -0
- package/templates/be/skeleton/src/modules/platform/errors/messages/errors.messages.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +12 -0
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +8 -1
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.decorators.ts +8 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +14 -1
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +12 -0
- package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +10 -2
- package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.spec.ts +63 -0
- package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +108 -0
- package/templates/be/skeleton/src/modules/platform/http-security/request-validation.service.spec.ts +39 -0
- package/templates/be/skeleton/src/modules/platform/http-security/request-validation.service.ts +25 -0
- package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +172 -0
- package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.ts +66 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +3 -1
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +52 -0
- package/templates/be/skeleton-lite/apps/api/Dockerfile +37 -0
- package/templates/be/skeleton-lite/apps/api/src/api.options.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/src/app.module.ts +81 -0
- package/templates/be/skeleton-lite/apps/api/src/main.ts +34 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +14 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +50 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +26 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +19 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +26 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +7 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +6 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +19 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/index.ts +10 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.ts +24 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.config.ts +8 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.decorators.ts +17 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.jwks.ts +32 -0
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.options.ts +24 -0
- package/templates/be/skeleton-lite/src/modules/platform/cqrs/index.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +54 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.options.ts +23 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +10 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.sql.ts +49 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/migration-runner.service.ts +21 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/primary.config.ts +11 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/seed-runner.service.ts +28 -0
- package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +25 -0
- package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +9 -0
- package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +5 -0
- package/templates/be/table/error.ts.tpl +16 -0
- package/templates/be/table/index.ts.tpl +2 -0
- package/templates/be/table/migration.sql.tpl +51 -0
- package/templates/be/table/module-definition.ts.tpl +7 -0
- package/templates/be/table/module.ts.tpl +14 -0
- package/templates/be/table/options.ts.tpl +2 -0
- package/templates/be/table/rows.ts.tpl +4 -0
- package/templates/be/table/service.ts.tpl +23 -0
- package/templates/be/table/sql.ts.tpl +4 -0
- package/templates/be/tool-config/jest.config.js +3 -1
- package/templates/be/upgrade-full/apps/cli/src/app.module.ts +14 -0
- package/templates/be/upgrade-full/apps/cli/src/main.ts +26 -0
- package/templates/be/upgrade-full/src/features/cli/cli.module.ts +6 -0
- package/templates/be/upgrade-full/src/features/cli/migrate/subs/run.cli.ts +23 -0
- package/templates/common/gitignore.base +1 -2
- package/templates/fe/app/package.json.tpl +1 -0
- package/templates/fe/app/tsconfig.json +12 -0
- package/templates/fe/common/app-layout.tsx.partial +8 -0
- package/templates/fe/common/site-shell.tsx.partial +9 -0
- package/templates/fe/image/next/Dockerfile +42 -0
- package/templates/fe/skeleton/apps/app/next.config.ts +23 -0
- package/templates/fe/skeleton/apps/app/postcss.config.mjs +7 -0
- package/templates/fe/skeleton/apps/{__app__ → app}/src/app/[locale]/error.tsx +2 -2
- package/templates/fe/skeleton/apps/app/src/app/[locale]/layout.tsx +22 -0
- package/templates/fe/skeleton/apps/{__app__ → app}/src/app/[locale]/loading.tsx +2 -2
- package/templates/fe/skeleton/apps/{__app__ → app}/src/app/[locale]/not-found.tsx +2 -2
- package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +9 -0
- package/templates/fe/skeleton/apps/{__app__ → app}/src/app/global-error.tsx +2 -2
- package/templates/fe/skeleton/apps/app/src/app/globals.css +10 -0
- package/templates/fe/skeleton/apps/app/src/features/layouts/AppLayout/index.tsx +18 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppErrorPage/component.tsx +19 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppErrorPage/index.tsx +11 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppGlobalErrorPage/index.tsx +16 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/component.tsx +10 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +15 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppLoadingPage/index.tsx +8 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppNotFoundPage/index.tsx +9 -0
- package/templates/fe/skeleton/apps/app/src/modules/i18n/index.ts +4 -0
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +28 -0
- package/templates/fe/skeleton/apps/app/src/modules/i18n/request.ts +4 -0
- package/templates/fe/skeleton/apps/app/src/modules/routes/index.ts +4 -0
- package/templates/fe/skeleton/apps/app/src/proxy.ts +1 -0
- package/templates/fe/skeleton/apps/{__app__ → landing}/next.config.ts +4 -2
- package/templates/fe/skeleton/apps/landing/postcss.config.mjs +7 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/error.tsx +10 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/layout.tsx +22 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/loading.tsx +6 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/not-found.tsx +6 -0
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +9 -0
- package/templates/fe/skeleton/apps/landing/src/app/global-error.tsx +10 -0
- package/templates/fe/skeleton/apps/landing/src/app/globals.css +10 -0
- package/templates/fe/skeleton/apps/landing/src/features/layouts/LandingLayout/index.tsx +24 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingErrorPage/component.tsx +18 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingErrorPage/index.tsx +11 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingGlobalErrorPage/index.tsx +16 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingLoadingPage/index.tsx +8 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingNotFoundPage/index.tsx +9 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingPage/component.tsx +24 -0
- package/templates/fe/skeleton/apps/landing/src/features/pages/LandingPage/index.tsx +16 -0
- package/templates/fe/skeleton/apps/landing/src/modules/config/index.ts +12 -0
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/index.ts +7 -0
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/vi.json +31 -0
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/request.ts +4 -0
- package/templates/fe/skeleton/apps/landing/src/modules/routes/index.ts +4 -0
- package/templates/fe/skeleton/apps/landing/src/proxy.ts +1 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +49 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/proxy.ts +24 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +20 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/branches/Providers/index.tsx +23 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/ErrorNotice/index.tsx +14 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/GlobalErrorNotice/index.tsx +25 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/LoadingNotice/index.tsx +9 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/LocaleShell/index.tsx +21 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/NotFoundNotice/index.tsx +19 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/SiteShell/classNames.ts +4 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/SiteShell/index.tsx +19 -0
- package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +6 -0
- package/templates/fe/skeleton-lite/apps/web/next.config.ts +22 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/error.tsx +10 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +37 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/loading.tsx +6 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/not-found.tsx +6 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +9 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/providers.tsx +26 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/auth/callback/route.ts +24 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/global-error.tsx +10 -0
- package/templates/fe/{skeleton/apps/__app__ → skeleton-lite/apps/web}/src/app/globals.css +1 -1
- package/templates/fe/skeleton-lite/apps/web/src/app/health/live/route.ts +5 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/component.tsx +29 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +41 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignOutButton/component.tsx +17 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignOutButton/index.tsx +38 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/ErrorNotice/index.tsx +13 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/GlobalErrorNotice/index.tsx +24 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/LoadingNotice/index.tsx +7 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/NotFoundNotice/index.tsx +18 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/SiteShell/classNames.ts +4 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/SiteShell/index.tsx +13 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/layouts/AppLayout/index.tsx +31 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppErrorPage/component.tsx +12 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppErrorPage/index.tsx +10 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppGlobalErrorPage/index.tsx +15 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/component.tsx +14 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +30 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppLoadingPage/index.tsx +8 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppNotFoundPage/index.tsx +9 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/brand/brand.css +20 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/brand/index.ts +2 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/config/index.ts +20 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/read-session.ts +25 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-in.ts +25 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +17 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/browser.ts +14 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/index.ts +3 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/outcome.ts +59 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/principal.ts +29 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/server.ts +70 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +50 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/index.ts +8 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +48 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +16 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +14 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/routes/index.ts +7 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/routes/safe-next-path.ts +26 -0
- package/templates/fe/skeleton-lite/apps/web/src/proxy.ts +43 -0
- package/templates/fe/table/read.ts.tpl +16 -0
- package/templates/fe/table/write.ts.tpl +16 -0
- package/upgrade/check.mjs +137 -0
- package/upgrade/index.mjs +524 -0
- package/bin/hfs.mjs +0 -233
- package/runtime/scripts/api/fs/rmdir-link.mjs +0 -12
- package/runtime/scripts/lib/test-secrets.mjs +0 -116
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/layout.tsx +0 -20
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/page.tsx +0 -9
- package/templates/fe/skeleton/apps/__app__/src/components/composites/FailureScreen/index.tsx +0 -28
- package/templates/fe/skeleton/apps/__app__/src/features/layouts/LocaleShell/index.tsx +0 -35
- package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/component.tsx +0 -19
- package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/index.tsx +0 -11
- package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/component.tsx +0 -24
- package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/index.tsx +0 -14
- package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/component.tsx +0 -21
- package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/index.tsx +0 -15
- package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/component.tsx +0 -23
- package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/index.tsx +0 -8
- package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/component.tsx +0 -28
- package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/index.tsx +0 -8
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/config.ts +0 -8
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/index.ts +0 -2
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages/vi.json +0 -25
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/request.ts +0 -19
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/routing.ts +0 -9
- package/templates/fe/skeleton/apps/__app__/src/modules/routes/index.ts +0 -4
- package/templates/fe/skeleton/apps/__app__/src/proxy.ts +0 -10
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/application/check-liveness.contracts.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/application/check-liveness.handler.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/application/check-liveness.query.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/index.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/system-health.module.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/transport/http/live.controller.ts +0 -0
- /package/templates/be/skeleton/src/features/{system-health → api/system-health}/transport/http/system-health-http.module.ts +0 -0
- /package/templates/fe/skeleton/apps/{__app__ → app}/src/app/health/live/route.ts +0 -0
- /package/templates/fe/skeleton/apps/{__app__ → app}/src/modules/config/index.ts +0 -0
- /package/templates/fe/{skeleton/apps/__app__ → skeleton-lite/apps/web}/postcss.config.mjs +0 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { machineKit } from './machine-ast.mjs';
|
|
3
|
+
import { isServerActionModule } from './server-action.mjs';
|
|
4
|
+
import { callName, chainParts, contextTypesClient, isDatabaseType, isSupabaseCall, isSupabaseValue, moduleNameOf, reportAt, signatureFromSupabase, supabaseClientType, SUPABASE_MODULES, unwrap } from './supabase-ast.mjs';
|
|
5
|
+
import { checkBackendJwt } from './supabase-be.mjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* L10-L16, the slot-driven Supabase machine. Supabase is one data transport with one owner on each side:
|
|
9
|
+
* `fe.modules.db` / `fe.package.db` in a front end and `be.integrations.supabase` in a back end. The checks below
|
|
10
|
+
* recognise provider calls and types by their imported package declarations, not by local variable or type names.
|
|
11
|
+
*/
|
|
12
|
+
export const FRONTEND_SUPABASE_RULE_IDS = Object.freeze([
|
|
13
|
+
'FE_SUPABASE_CLIENT_OWNER',
|
|
14
|
+
'FE_DB_RESULT_TYPED',
|
|
15
|
+
'FE_DB_ERROR_HANDLED',
|
|
16
|
+
'FE_SERVICE_ROLE_FORBIDDEN',
|
|
17
|
+
'FE_AUTH_SESSION_TRUST',
|
|
18
|
+
'FE_DB_WRITE_SHAPE',
|
|
19
|
+
'FE_ROUTE_HANDLER_FORBIDDEN',
|
|
20
|
+
]);
|
|
21
|
+
export const BACKEND_SUPABASE_RULE_IDS = Object.freeze(['BE_SUPABASE_CLIENT_OWNER', 'BE_SUPABASE_JWT_VERIFIED']);
|
|
22
|
+
|
|
23
|
+
const FE_CLIENT_OWNER = 'FE_SUPABASE_CLIENT_OWNER';
|
|
24
|
+
const BE_CLIENT_OWNER = 'BE_SUPABASE_CLIENT_OWNER';
|
|
25
|
+
const FE_RESULT_TYPED = 'FE_DB_RESULT_TYPED';
|
|
26
|
+
const FE_ERROR_HANDLED = 'FE_DB_ERROR_HANDLED';
|
|
27
|
+
const FE_SERVICE_ROLE = 'FE_SERVICE_ROLE_FORBIDDEN';
|
|
28
|
+
const FE_SESSION_TRUST = 'FE_AUTH_SESSION_TRUST';
|
|
29
|
+
const FE_WRITE_SHAPE = 'FE_DB_WRITE_SHAPE';
|
|
30
|
+
const FE_ROUTE_HANDLER = 'FE_ROUTE_HANDLER_FORBIDDEN';
|
|
31
|
+
const FE_DB_SLOTS = new Set(['fe.modules.db', 'fe.package.db', 'fe.modules.db.outcome']);
|
|
32
|
+
const BE_SUPABASE_SLOT = 'be.integrations.supabase';
|
|
33
|
+
const FE_CONFIG_SLOT = 'fe.modules.config';
|
|
34
|
+
const SUPABASE_CALLS = new Set(['from', 'rpc']);
|
|
35
|
+
const QUERY_TERMINALS = new Set(['single', 'maybeSingle', 'returns']);
|
|
36
|
+
const QUERY_WRITES = new Set(['insert', 'update', 'upsert', 'delete']);
|
|
37
|
+
const LIST_BOUNDS = new Set(['gt', 'gte', 'lt', 'lte', 'range']);
|
|
38
|
+
const supabaseSlotEnabled = (input, slotId) => {
|
|
39
|
+
const slot = input.graph.resolver.slot(slotId);
|
|
40
|
+
return Boolean(slot && input.graph.resolver.slotEnabled(slot));
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function checkClientOwnership(input, { slots, ruleId }) {
|
|
44
|
+
const kit = machineKit(input);
|
|
45
|
+
const { ts } = kit;
|
|
46
|
+
const violations = [];
|
|
47
|
+
let imports = 0;
|
|
48
|
+
let clients = 0;
|
|
49
|
+
let calls = 0;
|
|
50
|
+
for (const file of input.graph.files.values()) {
|
|
51
|
+
const checker = kit.checkerOf(file.sourceFile);
|
|
52
|
+
const owned = slots.has(file.slot);
|
|
53
|
+
kit.walk(file.sourceFile, node => {
|
|
54
|
+
const moduleName = moduleNameOf(ts, node);
|
|
55
|
+
if (moduleName && SUPABASE_MODULES.has(moduleName)) {
|
|
56
|
+
imports += 1;
|
|
57
|
+
if (!owned) reportAt(violations, kit, ruleId, file, node, `${moduleName} is imported by ${file.rel}, whose slot is ${file.slot ?? 'unowned'}; Supabase imports belong only to ${[...slots].join(' or ')}.`, { module: moduleName, slot: file.slot ?? null });
|
|
58
|
+
}
|
|
59
|
+
if (ts.isTypeReferenceNode(node) && supabaseClientType(kit, checker, node)) {
|
|
60
|
+
clients += 1;
|
|
61
|
+
if (!isDatabaseType(ts, node.typeArguments?.[0])) reportAt(violations, kit, ruleId, file, node, 'SupabaseClient must carry the committed Database type as SupabaseClient<Database>.');
|
|
62
|
+
}
|
|
63
|
+
if (!ts.isCallExpression(node)) return true;
|
|
64
|
+
const binding = kit.importBinding(checker, node.expression);
|
|
65
|
+
if (binding && SUPABASE_MODULES.has(binding.module) && /^create(?:Client|ServerClient|BrowserClient)$/u.test(binding.name)) {
|
|
66
|
+
clients += 1;
|
|
67
|
+
if (!isDatabaseType(ts, node.typeArguments?.[0]) && !contextTypesClient(kit, checker, node)) {
|
|
68
|
+
reportAt(violations, kit, ruleId, file, node, `${binding.name} creates an untyped Supabase client; create it as ${binding.name}<Database>(...) (or give it the contextual type SupabaseClient<Database>).`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (!isSupabaseCall(kit, checker, node)) return true;
|
|
72
|
+
const properties = chainParts(ts, node);
|
|
73
|
+
const method = callName(ts, node);
|
|
74
|
+
if (!SUPABASE_CALLS.has(method) && !properties.includes('auth') && !properties.includes('storage')) return true;
|
|
75
|
+
calls += 1;
|
|
76
|
+
if (!owned) reportAt(violations, kit, ruleId, file, node, `A Supabase ${method ?? 'client'} call is made from ${file.slot ?? 'an unowned path'}; database, auth and storage calls belong only to ${[...slots].join(' or ')}.`, { method, slot: file.slot ?? null });
|
|
77
|
+
return true;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return { violations, coverage: { status: 'checked', imports, clients, calls, ownerSlots: [...slots] } };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const symbolOf = (kit, checker, node) => kit.aliased(checker, kit.symbolAt(checker, node)) ?? kit.symbolAt(checker, node);
|
|
84
|
+
|
|
85
|
+
const sameSymbol = (kit, checker, node, symbol) => Boolean(symbol && symbolOf(kit, checker, node) === symbol);
|
|
86
|
+
|
|
87
|
+
const fromAwaitedSupabase = (kit, checker, node, seen = new Set()) => {
|
|
88
|
+
const current = unwrap(kit.ts, node);
|
|
89
|
+
if (!current || seen.has(current)) return false;
|
|
90
|
+
seen.add(current);
|
|
91
|
+
if (kit.ts.isCallExpression(current)) return isSupabaseCall(kit, checker, current);
|
|
92
|
+
if (!kit.ts.isIdentifier(current)) return false;
|
|
93
|
+
for (const declaration of kit.declarationsOf(checker, current)) {
|
|
94
|
+
if (kit.ts.isVariableDeclaration(declaration) && declaration.initializer && fromAwaitedSupabase(kit, checker, declaration.initializer, seen)) return true;
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const callIsToOutcome = (kit, checker, graph, call) => {
|
|
100
|
+
if (!kit.ts.isCallExpression(call)) return false;
|
|
101
|
+
const expression = call.expression;
|
|
102
|
+
const name = kit.ts.isIdentifier(expression) ? expression.text : (kit.ts.isPropertyAccessExpression(expression) ? expression.name.text : null);
|
|
103
|
+
if (name !== 'toOutcome') return false;
|
|
104
|
+
return kit.declarationsOf(checker, kit.ts.isPropertyAccessExpression(expression) ? expression.name : expression)
|
|
105
|
+
.some(declaration => {
|
|
106
|
+
const rel = kit.graphPath(declaration);
|
|
107
|
+
return rel !== null && FE_DB_SLOTS.has(graph.files.get(rel)?.slot);
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const parentCall = (ts, node) => {
|
|
112
|
+
let current = node;
|
|
113
|
+
while (current.parent && ts.isParenthesizedExpression(current.parent)) current = current.parent;
|
|
114
|
+
return ts.isCallExpression(current.parent) ? current.parent : null;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const handledAwait = (kit, checker, graph, awaitNode) => {
|
|
118
|
+
const direct = parentCall(kit.ts, awaitNode);
|
|
119
|
+
if (direct && callIsToOutcome(kit, checker, graph, direct) && direct.arguments.some(argument => argument === awaitNode || argument === awaitNode.parent)) return true;
|
|
120
|
+
let cursor = awaitNode;
|
|
121
|
+
while (cursor.parent && kit.ts.isParenthesizedExpression(cursor.parent)) cursor = cursor.parent;
|
|
122
|
+
const declaration = kit.ts.isVariableDeclaration(cursor.parent) ? cursor.parent : null;
|
|
123
|
+
if (!declaration) return false;
|
|
124
|
+
if (kit.ts.isIdentifier(declaration.name)) {
|
|
125
|
+
const result = symbolOf(kit, checker, declaration.name);
|
|
126
|
+
let handled = false;
|
|
127
|
+
kit.walk(fileOf(awaitNode), node => {
|
|
128
|
+
if (kit.ts.isPropertyAccessExpression(node) && node.name.text === 'error' && sameSymbol(kit, checker, node.expression, result)) handled = true;
|
|
129
|
+
if (kit.ts.isElementAccessExpression(node) && kit.ts.isStringLiteralLike(node.argumentExpression) && node.argumentExpression.text === 'error' && sameSymbol(kit, checker, node.expression, result)) handled = true;
|
|
130
|
+
if (kit.ts.isCallExpression(node) && callIsToOutcome(kit, checker, graph, node) && node.arguments.some(argument => sameSymbol(kit, checker, unwrap(kit.ts, argument), result))) handled = true;
|
|
131
|
+
return !handled;
|
|
132
|
+
});
|
|
133
|
+
return handled;
|
|
134
|
+
}
|
|
135
|
+
if (!kit.ts.isObjectBindingPattern(declaration.name)) return false;
|
|
136
|
+
const errorBinding = declaration.name.elements.find(element => {
|
|
137
|
+
const property = element.propertyName ?? element.name;
|
|
138
|
+
return kit.ts.isIdentifier(property) && property.text === 'error';
|
|
139
|
+
});
|
|
140
|
+
if (!errorBinding || !kit.ts.isIdentifier(errorBinding.name)) return false;
|
|
141
|
+
const error = symbolOf(kit, checker, errorBinding.name);
|
|
142
|
+
let references = 0;
|
|
143
|
+
kit.walk(fileOf(awaitNode), node => {
|
|
144
|
+
if (kit.ts.isIdentifier(node) && sameSymbol(kit, checker, node, error)) references += 1;
|
|
145
|
+
return true;
|
|
146
|
+
});
|
|
147
|
+
return references > 1;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const fileOf = node => node.getSourceFile();
|
|
151
|
+
|
|
152
|
+
const dataSymbolFromAwait = (kit, checker, identifier) => {
|
|
153
|
+
const symbol = symbolOf(kit, checker, identifier);
|
|
154
|
+
for (const declaration of symbol?.declarations ?? []) {
|
|
155
|
+
if (!kit.ts.isBindingElement(declaration) || !kit.ts.isObjectBindingPattern(declaration.parent)) continue;
|
|
156
|
+
const property = declaration.propertyName ?? declaration.name;
|
|
157
|
+
if (!kit.ts.isIdentifier(property) || property.text !== 'data') continue;
|
|
158
|
+
const variable = declaration.parent.parent;
|
|
159
|
+
if (kit.ts.isVariableDeclaration(variable) && variable.initializer && fromAwaitedSupabase(kit, checker, variable.initializer)) return symbol;
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
function checkFrontendQueryResults(input) {
|
|
165
|
+
const kit = machineKit(input);
|
|
166
|
+
const { ts } = kit;
|
|
167
|
+
const violations = [];
|
|
168
|
+
let awaited = 0;
|
|
169
|
+
let listReads = 0;
|
|
170
|
+
for (const file of input.graph.files.values()) {
|
|
171
|
+
const checker = kit.checkerOf(file.sourceFile);
|
|
172
|
+
const seen = new Set();
|
|
173
|
+
const report = (ruleId, node, message) => {
|
|
174
|
+
const key = `${ruleId}:${node.pos}:${message}`;
|
|
175
|
+
if (seen.has(key)) return;
|
|
176
|
+
seen.add(key);
|
|
177
|
+
reportAt(violations, kit, ruleId, file, node, message);
|
|
178
|
+
};
|
|
179
|
+
kit.walk(file.sourceFile, node => {
|
|
180
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && QUERY_TERMINALS.has(node.expression.name.text)
|
|
181
|
+
&& node.typeArguments?.length && isSupabaseValue(kit, checker, node.expression.expression)) {
|
|
182
|
+
report(FE_RESULT_TYPED, node, `${node.expression.name.text}<T>() supplies a result generic; derive the row type from Database and let the Supabase chain infer it.`);
|
|
183
|
+
}
|
|
184
|
+
if ((ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isNonNullExpression(node)) && fromAwaitedSupabase(kit, checker, node.expression)) {
|
|
185
|
+
report(FE_RESULT_TYPED, node, 'A Supabase query result is cast or non-null asserted; narrow its typed data/error outcome instead.');
|
|
186
|
+
}
|
|
187
|
+
if (ts.isAwaitExpression(node) && isSupabaseValue(kit, checker, node.expression)) {
|
|
188
|
+
const expression = unwrap(ts, node.expression);
|
|
189
|
+
if (!ts.isCallExpression(expression)) return true;
|
|
190
|
+
awaited += 1;
|
|
191
|
+
const methods = chainParts(ts, expression);
|
|
192
|
+
if (methods.includes('select') && !methods.some(method => QUERY_WRITES.has(method)) && !methods.includes('single') && !methods.includes('maybeSingle')) {
|
|
193
|
+
listReads += 1;
|
|
194
|
+
if (!methods.includes('limit') && !methods.some(method => LIST_BOUNDS.has(method))) {
|
|
195
|
+
report(FE_RESULT_TYPED, node, 'A Supabase list read has no .limit(), .range(), or keyset bound (.gt/.gte/.lt/.lte); bound every multi-row read.');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (!handledAwait(kit, checker, input.graph, node)) {
|
|
199
|
+
report(FE_ERROR_HANDLED, node, 'The awaited Supabase result does not read its error and is not passed whole to toOutcome(result).');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken && ts.isArrayLiteralExpression(node.right) && node.right.elements.length === 0) {
|
|
203
|
+
const left = unwrap(ts, node.left);
|
|
204
|
+
const swallowed = ts.isIdentifier(left) ? Boolean(dataSymbolFromAwait(kit, checker, left))
|
|
205
|
+
: ts.isPropertyAccessExpression(left) && left.name.text === 'data' && fromAwaitedSupabase(kit, checker, left.expression);
|
|
206
|
+
if (swallowed) report(FE_ERROR_HANDLED, node, 'Supabase data is collapsed with ?? []; refused or unavailable is not empty data. Pass the whole result to toOutcome.');
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return { violations, coverage: { status: 'checked', awaited, listReads } };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const words = text => String(text).replace(/([a-z0-9])([A-Z])/gu, '$1_$2').toUpperCase();
|
|
215
|
+
const serviceRoleText = text => /(?:^|[^A-Z0-9])SERVICE[_ -]?ROLE(?:[^A-Z0-9]|$)/iu.test(words(text));
|
|
216
|
+
const credentialEnv = name => /(?:^|_)(?:SECRET|PRIVATE|TOKEN|PASSWORD)$/u.test(name) || /(?:^|_)SERVICE_ROLE(?:_|$)/u.test(name);
|
|
217
|
+
const CORE_PUBLIC_ENV = new Set(['NEXT_PUBLIC_SUPABASE_URL', 'NEXT_PUBLIC_SUPABASE_ANON_KEY', 'NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY', 'NEXT_PUBLIC_SITE_URL']);
|
|
218
|
+
|
|
219
|
+
const envRead = (ts, node) => {
|
|
220
|
+
if (!ts.isPropertyAccessExpression(node) && !ts.isElementAccessExpression(node)) return null;
|
|
221
|
+
const object = node.expression;
|
|
222
|
+
const processEnv = ts.isPropertyAccessExpression(object) && ts.isIdentifier(object.expression) && object.expression.text === 'process' && object.name.text === 'env';
|
|
223
|
+
const importMetaEnv = ts.isPropertyAccessExpression(object) && object.name.text === 'env' && object.expression.kind === ts.SyntaxKind.MetaProperty;
|
|
224
|
+
if (!processEnv && !importMetaEnv) return null;
|
|
225
|
+
if (ts.isPropertyAccessExpression(node)) return node.name.text;
|
|
226
|
+
return ts.isStringLiteralLike(node.argumentExpression) ? node.argumentExpression.text : null;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
function checkFrontendSecretNames(input) {
|
|
230
|
+
const kit = machineKit(input);
|
|
231
|
+
const { ts } = kit;
|
|
232
|
+
const violations = [];
|
|
233
|
+
let reads = 0;
|
|
234
|
+
for (const file of input.graph.files.values()) {
|
|
235
|
+
const seen = new Set();
|
|
236
|
+
const report = (node, message, name) => {
|
|
237
|
+
const key = `${node.pos}:${name ?? ''}`;
|
|
238
|
+
if (seen.has(key)) return;
|
|
239
|
+
seen.add(key);
|
|
240
|
+
reportAt(violations, kit, FE_SERVICE_ROLE, file, node, message, name ? { name } : {});
|
|
241
|
+
};
|
|
242
|
+
kit.walk(file.sourceFile, node => {
|
|
243
|
+
if (ts.isIdentifier(node) && serviceRoleText(node.text)) report(node, 'A SERVICE_ROLE identifier appears in the front end; the browser and Next process never hold the Supabase service role.', node.text);
|
|
244
|
+
if (ts.isStringLiteralLike(node) && serviceRoleText(node.text)) report(node, 'A service_role string appears in the front end; the service role belongs behind the back-end guard chain.', node.text);
|
|
245
|
+
const name = envRead(ts, node);
|
|
246
|
+
if (!name) return true;
|
|
247
|
+
reads += 1;
|
|
248
|
+
if (credentialEnv(name)) report(node, `${name} is a credential-shaped environment read in the front end; no *_SECRET, *_PRIVATE, *_TOKEN, *_PASSWORD or service-role value enters fe/.`, name);
|
|
249
|
+
else if (name.startsWith('NEXT_PUBLIC_') && !CORE_PUBLIC_ENV.has(name) && file.slot !== FE_CONFIG_SLOT) {
|
|
250
|
+
report(node, `${name} is not a core Supabase/site public variable and is read outside ${FE_CONFIG_SLOT}; product public configuration is declared by the one config owner.`, name);
|
|
251
|
+
}
|
|
252
|
+
return true;
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return { violations, coverage: { status: 'checked', envReads: reads } };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const isClientModule = (ts, sourceFile) => sourceFile.statements.some((statement, index) => index < 4 && ts.isExpressionStatement(statement)
|
|
259
|
+
&& ts.isStringLiteralLike(statement.expression) && statement.expression.text === 'use client');
|
|
260
|
+
|
|
261
|
+
const isDirectiveStatement = (ts, statement) => ts.isExpressionStatement(statement) && ts.isStringLiteralLike(statement.expression);
|
|
262
|
+
|
|
263
|
+
const exportedValues = (ts, sourceFile) => {
|
|
264
|
+
const hasExport = node => node.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
|
265
|
+
const out = [];
|
|
266
|
+
for (const statement of sourceFile.statements) {
|
|
267
|
+
if (ts.isFunctionDeclaration(statement) && hasExport(statement) && statement.name && statement.body) {
|
|
268
|
+
out.push({ name: statement.name.text, implementation: statement.body, functionNode: statement });
|
|
269
|
+
}
|
|
270
|
+
if (!ts.isVariableStatement(statement) || !hasExport(statement)) continue;
|
|
271
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
272
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) continue;
|
|
273
|
+
const functionNode = (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))
|
|
274
|
+
&& ts.isBlock(declaration.initializer.body) ? declaration.initializer : null;
|
|
275
|
+
out.push({ name: declaration.name.text, implementation: declaration.initializer, functionNode });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return out;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const exportedActions = (ts, sourceFile) => exportedValues(ts, sourceFile).map(value => value.functionNode).filter(Boolean);
|
|
282
|
+
const exportedImplementations = (ts, sourceFile, name) => exportedValues(ts, sourceFile)
|
|
283
|
+
.filter(value => value.name === name).map(value => value.implementation);
|
|
284
|
+
|
|
285
|
+
const contains = (kit, root, predicate) => {
|
|
286
|
+
let found = false;
|
|
287
|
+
kit.walk(root, node => {
|
|
288
|
+
if (predicate(node)) found = true;
|
|
289
|
+
return !found;
|
|
290
|
+
});
|
|
291
|
+
return found;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const principalCall = (kit, checker, graph, node) => kit.ts.isCallExpression(node) && (() => {
|
|
295
|
+
const expression = node.expression;
|
|
296
|
+
const name = kit.ts.isIdentifier(expression) ? expression : (kit.ts.isPropertyAccessExpression(expression) ? expression.name : null);
|
|
297
|
+
if (!name || name.text !== 'getPrincipal') return false;
|
|
298
|
+
return kit.declarationsOf(checker, name).some(declaration => {
|
|
299
|
+
const rel = kit.graphPath(declaration);
|
|
300
|
+
return rel !== null && FE_DB_SLOTS.has(graph.files.get(rel)?.slot);
|
|
301
|
+
});
|
|
302
|
+
})();
|
|
303
|
+
|
|
304
|
+
const firstPrincipal = (kit, checker, graph, statement) => {
|
|
305
|
+
let call = null;
|
|
306
|
+
kit.walk(statement, node => {
|
|
307
|
+
if (kit.ts.isAwaitExpression(node) && principalCall(kit, checker, graph, unwrap(kit.ts, node.expression))) call = unwrap(kit.ts, node.expression);
|
|
308
|
+
return call === null;
|
|
309
|
+
});
|
|
310
|
+
if (!call || !kit.ts.isVariableStatement(statement)) return { call, symbol: null };
|
|
311
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
312
|
+
if (kit.ts.isIdentifier(declaration.name) && declaration.initializer && contains(kit, declaration.initializer, node => node === call)) {
|
|
313
|
+
return { call, symbol: symbolOf(kit, checker, declaration.name) };
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return { call, symbol: null };
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const typedRefusal = (kit, node) => contains(kit, node, child => (kit.ts.isStringLiteralLike(child) && child.text === 'refused')
|
|
320
|
+
|| (kit.ts.isIdentifier(child) && /^refused(?:Outcome)?$/iu.test(child.text)));
|
|
321
|
+
|
|
322
|
+
const parseUse = (ts, identifier) => {
|
|
323
|
+
let current = identifier;
|
|
324
|
+
while (current.parent && (ts.isParenthesizedExpression(current.parent) || ts.isAsExpression(current.parent))) current = current.parent;
|
|
325
|
+
const call = current.parent;
|
|
326
|
+
return ts.isCallExpression(call) && call.arguments.includes(current) && ts.isPropertyAccessExpression(call.expression)
|
|
327
|
+
&& ['parse', 'safeParse'].includes(call.expression.name.text);
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const parameterBindings = (ts, name) => {
|
|
331
|
+
if (ts.isIdentifier(name)) return [name];
|
|
332
|
+
if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) return name.elements.flatMap(element => ts.isBindingElement(element) ? parameterBindings(ts, element.name) : []);
|
|
333
|
+
return [];
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
function checkFrontendAuthAndRoutes(input) {
|
|
337
|
+
const kit = machineKit(input);
|
|
338
|
+
const { ts } = kit;
|
|
339
|
+
const violations = [];
|
|
340
|
+
let actions = 0;
|
|
341
|
+
let routes = 0;
|
|
342
|
+
for (const file of input.graph.files.values()) {
|
|
343
|
+
const checker = kit.checkerOf(file.sourceFile);
|
|
344
|
+
if (!isClientModule(ts, file.sourceFile)) {
|
|
345
|
+
kit.walk(file.sourceFile, node => {
|
|
346
|
+
if (ts.isCallExpression(node) && callName(ts, node) === 'getSession' && signatureFromSupabase(checker, node)) {
|
|
347
|
+
reportAt(violations, kit, FE_SESSION_TRUST, file, node, 'auth.getSession() trusts cookie-backed session data in server code; call getPrincipal() (getClaims), and getUser() for sensitive mutations.');
|
|
348
|
+
}
|
|
349
|
+
return true;
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
if (FE_DB_SLOTS.has(file.slot)) {
|
|
353
|
+
for (const implementation of exportedImplementations(ts, file.sourceFile, 'getPrincipal')) {
|
|
354
|
+
const readsClaims = contains(kit, implementation, node => ts.isCallExpression(node) && callName(ts, node) === 'getClaims'
|
|
355
|
+
&& signatureFromSupabase(checker, node));
|
|
356
|
+
if (!readsClaims) {
|
|
357
|
+
reportAt(violations, kit, FE_SESSION_TRUST, file, implementation, 'getPrincipal() must establish the server principal through Supabase auth.getClaims().');
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (FE_DB_SLOTS.has(file.slot) && path.posix.basename(file.rel).startsWith('write-')) {
|
|
362
|
+
const classified = input.graph.resolver.classifyPath(file.rel);
|
|
363
|
+
const belowSlot = classified.root && file.rel.startsWith(`${classified.root}/`) ? file.rel.slice(classified.root.length + 1) : '';
|
|
364
|
+
const authenticatesAnonymous = (input.graph.resolver.slot(file.slot)?.anonymousActions ?? []).includes(belowSlot);
|
|
365
|
+
const declared = exportedActions(ts, file.sourceFile);
|
|
366
|
+
if (!declared.length) reportAt(violations, kit, FE_WRITE_SHAPE, file, file.sourceFile, `${file.rel} is a write module but exports no Server Action function.`);
|
|
367
|
+
for (const action of declared) {
|
|
368
|
+
actions += 1;
|
|
369
|
+
const functionLevel = action.body.statements[0]?.expression?.text === 'use server' && isDirectiveStatement(ts, action.body.statements[0]);
|
|
370
|
+
if (!isServerActionModule(ts, file.sourceFile) && !functionLevel) {
|
|
371
|
+
reportAt(violations, kit, FE_WRITE_SHAPE, file, action, 'An exported write function must be a Server Action through a file-level or function-level `use server` directive.');
|
|
372
|
+
}
|
|
373
|
+
// Function directives are the prologue, not work: the principal is the first non-directive statement.
|
|
374
|
+
const work = action.body.statements.slice(action.body.statements.findIndex(statement => !isDirectiveStatement(ts, statement)) >>> 0);
|
|
375
|
+
const first = work[0];
|
|
376
|
+
const principal = first ? firstPrincipal(kit, checker, input.graph, first) : { call: null, symbol: null };
|
|
377
|
+
if (!principal.call || !principal.symbol) {
|
|
378
|
+
reportAt(violations, kit, FE_SESSION_TRUST, file, first ?? action, 'A write Server Action must begin with `const principal = await getPrincipal()` from the db owner.');
|
|
379
|
+
} else if (!authenticatesAnonymous) {
|
|
380
|
+
const refuses = work.slice(1).some(statement => ts.isIfStatement(statement)
|
|
381
|
+
&& contains(kit, statement.expression, node => ts.isIdentifier(node) && sameSymbol(kit, checker, node, principal.symbol))
|
|
382
|
+
&& typedRefusal(kit, statement.thenStatement));
|
|
383
|
+
if (!refuses) reportAt(violations, kit, FE_SESSION_TRUST, file, action, 'A write Server Action must refuse an anonymous principal with a typed `refused` Outcome before doing work.');
|
|
384
|
+
}
|
|
385
|
+
for (const parameter of action.parameters.flatMap(item => parameterBindings(ts, item.name))) {
|
|
386
|
+
const symbol = symbolOf(kit, checker, parameter);
|
|
387
|
+
const references = [];
|
|
388
|
+
kit.walk(action.body, node => {
|
|
389
|
+
if (ts.isIdentifier(node) && sameSymbol(kit, checker, node, symbol)) references.push(node);
|
|
390
|
+
return true;
|
|
391
|
+
});
|
|
392
|
+
const firstUse = references.sort((a, b) => a.getStart() - b.getStart())[0];
|
|
393
|
+
if (firstUse && !parseUse(ts, firstUse)) reportAt(violations, kit, FE_WRITE_SHAPE, file, firstUse, `Server Action input ${parameter.text} is used before schema.parse(...) or schema.safeParse(...).`, { parameter: parameter.text });
|
|
394
|
+
}
|
|
395
|
+
const sensitive = /(?:^|\/)(?:billing|payment|payout|refund|charge|subscription|password|credential|role|permission|membership)(?:\/|-|\.)/iu.test(file.rel);
|
|
396
|
+
if (sensitive && !contains(kit, action.body, node => ts.isCallExpression(node) && callName(ts, node) === 'getUser' && signatureFromSupabase(checker, node))) {
|
|
397
|
+
reportAt(violations, kit, FE_SESSION_TRUST, file, action, 'A money, account-security or role-changing Server Action must call auth.getUser() before the mutation.');
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (file.rel.endsWith('/route.ts')) {
|
|
402
|
+
routes += 1;
|
|
403
|
+
const classified = input.graph.resolver.classifyPath(file.rel);
|
|
404
|
+
const app = classified.bindings?.app;
|
|
405
|
+
const live = app !== undefined && file.rel === `apps/${app}/src/app/health/live/route.ts`;
|
|
406
|
+
if (!live && file.slot !== 'fe.route.callback') {
|
|
407
|
+
reportAt(violations, kit, FE_ROUTE_HANDLER, file, file.sourceFile, `${file.rel} is a front-end route handler outside health/live and the fe.route.callback slot; webhooks, cron, public APIs and third-party callbacks belong to be features.`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return { violations, coverage: { status: 'checked', actions, routes } };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function checkFrontendSupabase(input) {
|
|
415
|
+
if (!supabaseSlotEnabled(input, 'fe.modules.db')) return { violations: [], coverage: { status: 'not-applicable', reason: 'the Supabase front-end database slot is not enabled' } };
|
|
416
|
+
const owner = checkClientOwnership(input, { slots: FE_DB_SLOTS, ruleId: FE_CLIENT_OWNER });
|
|
417
|
+
const results = checkFrontendQueryResults(input);
|
|
418
|
+
const secrets = checkFrontendSecretNames(input);
|
|
419
|
+
const auth = checkFrontendAuthAndRoutes(input);
|
|
420
|
+
return {
|
|
421
|
+
violations: [...owner.violations, ...results.violations, ...secrets.violations, ...auth.violations],
|
|
422
|
+
coverage: { status: 'checked', owner: owner.coverage, results: results.coverage, secrets: secrets.coverage, auth: auth.coverage },
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function checkBackendSupabase(input) {
|
|
427
|
+
if (!supabaseSlotEnabled(input, BE_SUPABASE_SLOT)) return { violations: [], coverage: { status: 'not-applicable', reason: 'the Supabase back-end integration slot is not enabled' } };
|
|
428
|
+
const owner = checkClientOwnership(input, { slots: new Set([BE_SUPABASE_SLOT]), ruleId: BE_CLIENT_OWNER });
|
|
429
|
+
const jwt = checkBackendJwt(input);
|
|
430
|
+
return {
|
|
431
|
+
violations: [...owner.violations, ...jwt.violations],
|
|
432
|
+
coverage: { status: 'checked', owner: owner.coverage, jwt: jwt.coverage },
|
|
433
|
+
};
|
|
434
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The lint surface: which architecture-machine findings an ESLint rule judges instead of `
|
|
2
|
+
* The lint surface: which architecture-machine findings an ESLint rule judges instead of `starci app check`.
|
|
3
3
|
* Single source of truth. Each enforcer id is the rules.yaml enforcer; `codes` are the exact violation.ruleId strings its
|
|
4
4
|
* check file emits on source files. plugins: 'be' = back-end profile repos, 'fe' = front-end profile, graph checks run for both.
|
|
5
5
|
*/
|
|
@@ -24,6 +24,7 @@ export const LINT_ENFORCERS = Object.freeze([
|
|
|
24
24
|
enforcer('tier-direction', BOTH, ['BE_TIER_DIRECTION', 'FE_TIER_DIRECTION'], 'An import goes only to a tier the slot matrix allows.'),
|
|
25
25
|
enforcer('owner-cycle', BOTH, ['ARCH_OWNER_CYCLE'], 'Owners never import each other in a cycle.'),
|
|
26
26
|
enforcer('feature-imports-feature', BE, ['BE_FEATURE_IMPORTS_FEATURE'], 'A feature never imports another feature.'),
|
|
27
|
+
enforcer('kind-isolation', BE, ['BE_KIND_ISOLATION'], 'A feature of one trigger kind never imports a feature of another kind; the kinds meet through the event bus.'),
|
|
27
28
|
enforcer('app-isolation', FE, ['FE_APP_ISOLATION'], 'An app never imports another app.'),
|
|
28
29
|
enforcer('feature-layout', BE, ['BE_FEATURE_LAYOUT_INVALID'], 'A feature file sits in the folder its role names.'),
|
|
29
30
|
enforcer('application-never-imports-transport', BE, ['BE_APPLICATION_IMPORTS_TRANSPORT', 'BE_APPLICATION_TRANSPORT_FRAMEWORK'], 'Application code never imports transport code, directly or through a chain.'),
|
|
@@ -33,6 +34,11 @@ export const LINT_ENFORCERS = Object.freeze([
|
|
|
33
34
|
enforcer('app-composition-only', BE, ['BE_APP_COMPOSITION_ONLY', 'BE_APP_BUSINESS_ROLE'], 'An app only composes modules and holds no business role.'),
|
|
34
35
|
enforcer('entrypoint-only-in-apps', BE, ['BE_ENTRYPOINT_ONLY_IN_APPS'], 'NestFactory and bootstrap live only in an app main file.'),
|
|
35
36
|
enforcer('schema-owner', BE, ['BE_SCHEMA_OWNER'], 'Each entity array is registered by one owner across all apps.'),
|
|
37
|
+
enforcer('sql-returning', BE, ['BE_SQL_RETURNING_SHAPE'], 'An UPDATE or DELETE with RETURNING is wrapped in a CTE SELECT, because EntityManager.query returns it as [rows, count].'),
|
|
38
|
+
enforcer('context-owner', BE, ['BE_CONTEXT_OWNER'], 'An app composes only the contexts (connections) it owns; the migration apps compose all of them.'),
|
|
39
|
+
enforcer('context-coupling', BE, ['BE_CONTEXT_COUPLING'], 'Contexts are never coupled by a relation, a foreign key or an import; only by events.'),
|
|
40
|
+
enforcer('context-transaction', BE, ['BE_CONTEXT_TRANSACTION'], 'One transaction touches one context connection.'),
|
|
41
|
+
enforcer('context-platform-tables', BE, ['BE_CONTEXT_PLATFORM_TABLES'], 'A per-connection platform capability registers its tables on every connection that uses it.'),
|
|
36
42
|
enforcer('error-code-unique', BE, ['BE_ERROR_HOME'], 'An error code is declared once in the repository.'),
|
|
37
43
|
enforcer('error-masked', BE, ['BE_ERROR_MASKED'], 'Every app wires the error filter that masks internal errors.'),
|
|
38
44
|
enforcer('default-deny-app-guard', BE, ['BE_DEFAULT_DENY'], 'Every app registers the default-deny guard before any other guard.'),
|
|
@@ -74,7 +80,7 @@ export const LINT_CODES = new Set(LINT_ENFORCERS.flatMap(e => e.codes));
|
|
|
74
80
|
export const TS_SOURCE = /\.(?:[cm]?tsx?)$/;
|
|
75
81
|
|
|
76
82
|
/** True when a finding sits on an existing TypeScript file of the repository: the only findings an editor can show on a line. */
|
|
77
|
-
|
|
83
|
+
function attachesToSource(repositoryRoot, violation) {
|
|
78
84
|
const file = violation?.path;
|
|
79
85
|
if (typeof file !== 'string' || !TS_SOURCE.test(file)) return false;
|
|
80
86
|
return fs.existsSync(path.join(repositoryRoot, ...file.split('/')));
|
|
@@ -87,5 +93,5 @@ export const codeOf = (finding) => finding?.ruleId ?? finding?.code;
|
|
|
87
93
|
export const belongsTo = (enforcer, finding) => enforcer.codes.includes(codeOf(finding))
|
|
88
94
|
&& (finding.origin ?? 'machine') === (enforcer.origin ?? 'machine') && (enforcer.via === undefined || finding.check === enforcer.via);
|
|
89
95
|
|
|
90
|
-
/** True when some lint rule owns the finding and it sits on an existing TypeScript file: it is an ESLint report, not a `
|
|
96
|
+
/** True when some lint rule owns the finding and it sits on an existing TypeScript file: it is an ESLint report, not a `starci app check` finding. */
|
|
91
97
|
export const onLintSurface = (repositoryRoot, finding) => LINT_ENFORCERS.some(enforcer => belongsTo(enforcer, finding)) && attachesToSource(repositoryRoot, finding);
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { checkerScope } from './required-files.mjs';
|
|
2
2
|
import { allowsFile } from '../allows.mjs';
|
|
3
3
|
import { locateDeclaration } from '../slots.mjs';
|
|
4
4
|
import { DEFAULT_ENVIRONMENT, STACKS_DIRECTORY, STATEFUL_KINDS, namesOfService, readStack } from '../../lib/stack-services.mjs';
|
|
5
|
+
import { unwrapEach } from './ast-walks.mjs';
|
|
6
|
+
import { nameText, sourceLocation } from '../../lib/ts-ast.mjs';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* R47 `test-world-files` (BE_TEST_TOPOLOGY). Judgements over the test world, read through slots, the repository's own
|
|
@@ -40,18 +42,9 @@ const NAMED_FORM = `export const { useTestWorld, useSandbox } = ${DEFINE}({ ...
|
|
|
40
42
|
const defaultEnvironment = DEFAULT_ENVIRONMENT;
|
|
41
43
|
const KEBAB = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
42
44
|
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
let current = node;
|
|
46
|
-
while (current && (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isSatisfiesExpression?.(current))) current = current.expression;
|
|
47
|
-
return current;
|
|
48
|
-
};
|
|
49
|
-
const propertyOf = (ts, literal, key) => literal.properties.find(property => ts.isPropertyAssignment(property) && nameOf(ts, property.name) === key)?.initializer ?? null;
|
|
45
|
+
const unwrap = (ts, node) => unwrapEach(ts, node, [ts.isParenthesizedExpression, ts.isAsExpression, ts.isSatisfiesExpression]);
|
|
46
|
+
const propertyOf = (ts, literal, key) => literal.properties.find(property => ts.isPropertyAssignment(property) && nameText(ts, property.name) === key)?.initializer ?? null;
|
|
50
47
|
const isExported = (ts, statement) => (statement.modifiers ?? []).some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
|
51
|
-
const positionOf = (sourceFile, node) => {
|
|
52
|
-
const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
53
|
-
return { line: position.line + 1, column: position.character + 1 };
|
|
54
|
-
};
|
|
55
48
|
|
|
56
49
|
/** The object literal of `defineTestWorld(<object literal>)`, or null. */
|
|
57
50
|
function defineLiteral(ts, expression) {
|
|
@@ -89,7 +82,7 @@ function fakeEntriesOf(ts, sourceFile, literal) {
|
|
|
89
82
|
const value = node ? unwrap(ts, node) : null;
|
|
90
83
|
if (!value || !ts.isObjectLiteralExpression(value)) return [];
|
|
91
84
|
return value.properties
|
|
92
|
-
.map(property => ({ name:
|
|
85
|
+
.map(property => ({ name: nameText(ts, property.name), ...sourceLocation(sourceFile, property) }))
|
|
93
86
|
.filter(entry => entry.name);
|
|
94
87
|
}
|
|
95
88
|
|
|
@@ -107,21 +100,18 @@ function fakedByOf(ts, sourceFile, literal) {
|
|
|
107
100
|
if (!fake) continue;
|
|
108
101
|
const reason = propertyOf(ts, body, 'reason');
|
|
109
102
|
entries.push({
|
|
110
|
-
service:
|
|
103
|
+
service: nameText(ts, property.name),
|
|
111
104
|
fake: ts.isStringLiteralLike(fake) ? fake.text : '',
|
|
112
105
|
reason: reason && ts.isStringLiteralLike(reason) ? reason.text.trim() : '',
|
|
113
|
-
...
|
|
106
|
+
...sourceLocation(sourceFile, property),
|
|
114
107
|
});
|
|
115
108
|
}
|
|
116
109
|
return entries;
|
|
117
110
|
}
|
|
118
111
|
|
|
119
112
|
export function checkTestWorldFiles(input) {
|
|
120
|
-
const { config, graph,
|
|
121
|
-
const ts = context.ts;
|
|
122
|
-
const resolver = graph.resolver;
|
|
113
|
+
const { config, graph, ts, resolver, tree } = checkerScope(input);
|
|
123
114
|
const { suffixes } = resolver.ruleParams();
|
|
124
|
-
const tree = treeOf(config.root);
|
|
125
115
|
const violations = [];
|
|
126
116
|
const fakeProviders = new Map();
|
|
127
117
|
let worldRoot = null;
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
import { isServerActionModule } from './server-action.mjs';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* HFS checks 1 and 2 (knowledge/hfs/slots.yaml `tiers`):
|
|
3
5
|
* 1. the tier direction matrix: every import, re-export and type-only import between two owners must go from a tier to
|
|
4
6
|
* a tier its `mayImport` lists (BE_TIER_DIRECTION / FE_TIER_DIRECTION); a backend feature importing another feature
|
|
5
|
-
* is the one direction with its own code (BE_FEATURE_IMPORTS_FEATURE, R28), never BE_TIER_DIRECTION,
|
|
7
|
+
* is the one direction with its own code (BE_FEATURE_IMPORTS_FEATURE, R28), never BE_TIER_DIRECTION, and a feature of one trigger
|
|
8
|
+
* kind (slot field `trigger`: api, webhooks, realtime, saga, reactors, jobs, cli) importing a feature of another is
|
|
9
|
+
* BE_KIND_ISOLATION, whose fix is the event bus,
|
|
6
10
|
* an app never imports another app (FE_APP_ISOLATION), and a component layer imports only the layers after it;
|
|
7
11
|
* 2. owner cycles: a strongly connected component of the owner graph, type-only imports included (ARCH_OWNER_CYCLE),
|
|
8
12
|
* reported with the cycle path and the import that closes each hop.
|
|
9
13
|
* Importing inside one owner is always allowed; the public-entry rule stays with owners.mjs (ARCH_OWNER_EXPORT_BYPASS).
|
|
10
14
|
*/
|
|
11
|
-
export const TIER_RULE_IDS = ['BE_TIER_DIRECTION', 'BE_FEATURE_IMPORTS_FEATURE', 'FE_TIER_DIRECTION', 'FE_APP_ISOLATION', 'ARCH_OWNER_CYCLE'];
|
|
15
|
+
export const TIER_RULE_IDS = ['BE_TIER_DIRECTION', 'BE_FEATURE_IMPORTS_FEATURE', 'BE_KIND_ISOLATION', 'FE_TIER_DIRECTION', 'FE_APP_ISOLATION', 'ARCH_OWNER_CYCLE'];
|
|
12
16
|
|
|
13
17
|
const REASON_TEXT = {
|
|
14
18
|
tierDirection: ({ fromTier, toTier, mayImport }) => `a ${fromTier} may import only ${mayImport.join(', ') || 'nothing'}; it imports a ${toTier}`,
|
|
@@ -70,6 +74,27 @@ function cycleThrough(adjacency, members, origin) {
|
|
|
70
74
|
return [origin, origin];
|
|
71
75
|
}
|
|
72
76
|
|
|
77
|
+
/** The trigger kind (slot field `trigger`) of the owner that holds `file`, or null when its owner's slot names none. */
|
|
78
|
+
const triggerOf = (resolver, file) => {
|
|
79
|
+
const owner = resolver.ownerOf(file);
|
|
80
|
+
return owner ? (resolver.slot(owner.slot)?.trigger ?? null) : null;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** The db Server Action an edge targets, or null. */
|
|
84
|
+
const serverActionTarget = (graph, edge) => {
|
|
85
|
+
if (graph.profile !== 'fe') return null;
|
|
86
|
+
const to = graph.resolver.classifyPath(edge.to);
|
|
87
|
+
return to.slot === 'fe.modules.db' && /^write-[^/]+\.ts$/u.test(edge.to.split('/').at(-1))
|
|
88
|
+
&& isServerActionModule(null, graph.files.get(edge.to).sourceFile) ? to : null;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** A connected block may call a db writer directly when that writer is a file-level Next Server Action. */
|
|
92
|
+
const blockCallsServerAction = (graph, edge) => {
|
|
93
|
+
if (!serverActionTarget(graph, edge)) return false;
|
|
94
|
+
const from = graph.resolver.classifyPath(edge.from);
|
|
95
|
+
return from.slot === 'fe.components' && from.kind === 'blocks' && from.role === 'entry';
|
|
96
|
+
};
|
|
97
|
+
|
|
73
98
|
export function checkTiers(graph) {
|
|
74
99
|
const { resolver, profile } = graph;
|
|
75
100
|
const violations = [];
|
|
@@ -81,15 +106,32 @@ export function checkTiers(graph) {
|
|
|
81
106
|
const verdict = resolver.importAllowed(edge.from, edge.to);
|
|
82
107
|
if (verdict.reason === 'unowned' || verdict.reason?.startsWith('slot')) { unclassified += 1; continue; }
|
|
83
108
|
edgesChecked += 1;
|
|
84
|
-
|
|
109
|
+
const actionTarget = serverActionTarget(graph, edge);
|
|
110
|
+
if ((verdict.allowed || verdict.reason === 'notPublicEntry') && actionTarget && graph.unit(edge.from) !== graph.unit(edge.to) && !blockCallsServerAction(graph, edge)) {
|
|
111
|
+
counts.tierDirection += 1;
|
|
112
|
+
violations.push({
|
|
113
|
+
ruleId: tierRule,
|
|
114
|
+
path: edge.from, line: edge.line, column: edge.column,
|
|
115
|
+
specifier: edge.specifier, resolvedPath: edge.to, typeOnly: !edge.runtime,
|
|
116
|
+
fromTier: resolver.tierOf(edge.from), toTier: resolver.tierOf(edge.to),
|
|
117
|
+
message: `${edge.from} -> ${edge.to}: a db Server Action is called directly only by a connected block entry; hooks and other tiers use their declared data path${edge.runtime ? '' : ' (type-only imports count)'}.`,
|
|
118
|
+
});
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (verdict.allowed || (verdict.reason === 'tierDirection' && blockCallsServerAction(graph, edge)) || !REASON_TEXT[verdict.reason]) continue;
|
|
85
122
|
counts[verdict.reason] += 1;
|
|
86
123
|
const featureToFeature = profile === 'be' && verdict.reason === 'tierDirection' && verdict.fromTier === 'feature' && verdict.toTier === 'feature';
|
|
124
|
+
const fromKind = featureToFeature ? triggerOf(resolver, edge.from) : null;
|
|
125
|
+
const toKind = featureToFeature ? triggerOf(resolver, edge.to) : null;
|
|
126
|
+
const crossKind = fromKind !== null && toKind !== null && fromKind !== toKind;
|
|
87
127
|
violations.push({
|
|
88
|
-
ruleId: verdict.reason === 'crossApp' ? 'FE_APP_ISOLATION' : featureToFeature ? 'BE_FEATURE_IMPORTS_FEATURE' : tierRule,
|
|
128
|
+
ruleId: verdict.reason === 'crossApp' ? 'FE_APP_ISOLATION' : crossKind ? 'BE_KIND_ISOLATION' : featureToFeature ? 'BE_FEATURE_IMPORTS_FEATURE' : tierRule,
|
|
89
129
|
path: edge.from, line: edge.line, column: edge.column,
|
|
90
130
|
specifier: edge.specifier, resolvedPath: edge.to, typeOnly: !edge.runtime,
|
|
91
131
|
fromTier: verdict.fromTier ?? null, toTier: verdict.toTier ?? null,
|
|
92
|
-
message:
|
|
132
|
+
message: crossKind
|
|
133
|
+
? `${edge.from} -> ${edge.to}: a ${fromKind} feature imports a ${toKind} feature; no kind imports another, every cross-kind call is an event published with eventBus.publish(event, tx) from a domain service${edge.runtime ? '' : ' (type-only imports count)'}.`
|
|
134
|
+
: `${edge.from} -> ${edge.to}: ${REASON_TEXT[verdict.reason](verdict)}${edge.runtime ? '' : ' (type-only imports count)'}.`,
|
|
93
135
|
});
|
|
94
136
|
}
|
|
95
137
|
|