@starci/hfs 4.9.11 → 4.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +7 -9
- package/db/backup-receipt.mjs +10 -47
- package/db/drift.mjs +1 -1
- package/db/dump.mjs +11 -10
- package/db/entity-list.mjs +1 -1
- package/db/migrate.mjs +83 -259
- package/db/new.mjs +46 -0
- package/db/post-cutover.mjs +13 -17
- package/db/postgres.mjs +80 -0
- package/emit/contracts.mjs +5 -9
- package/package.json +1 -1
- package/release/gate.mjs +3 -15
- package/runtime/knowledge/hfs/canon-pins.yaml +44 -24
- package/runtime/knowledge/hfs/design-truths.yaml +1 -1
- package/runtime/knowledge/hfs/fe-stack.yaml +4 -11
- package/runtime/knowledge/hfs/rules.yaml +177 -155
- package/runtime/knowledge/hfs/slots.yaml +164 -172
- package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
- package/runtime/knowledge/patterns/be/idempotency.yaml +3 -3
- package/runtime/knowledge/patterns/be/jobs.yaml +4 -4
- package/runtime/knowledge/patterns/be/projections.yaml +7 -5
- package/runtime/knowledge/patterns/be/queues.yaml +8 -8
- package/runtime/knowledge/patterns/be/reactors.yaml +2 -2
- package/runtime/knowledge/patterns/be/realtime.yaml +6 -6
- package/runtime/knowledge/patterns/be/saga.yaml +3 -3
- package/runtime/knowledge/patterns/be/webhooks.yaml +1 -1
- package/runtime/modules/kernel/command-policy.yaml +1 -1
- package/runtime/modules/kernel/failure-codes.yaml +65 -115
- package/runtime/modules/schemas/application-stacks.schema.yaml +5 -5
- package/runtime/scripts/hfs/architecture/canon-clones.mjs +1 -1
- package/runtime/scripts/hfs/architecture/{supabase-be.mjs → identity.mjs} +45 -15
- package/runtime/scripts/hfs/architecture/index.mjs +2 -3
- package/runtime/scripts/hfs/architecture/{supabase-ast.mjs → jwt-ast.mjs} +2 -103
- package/runtime/scripts/hfs/architecture/register-once.mjs +1 -1
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +29 -4
- package/runtime/scripts/hfs/architecture/server-action.mjs +3 -4
- package/runtime/scripts/hfs/architecture/side-boundary.mjs +1 -1
- package/runtime/scripts/hfs/architecture/{supabase-keys.mjs → sql-keys.mjs} +1 -1
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +25 -27
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +7 -3
- package/runtime/scripts/hfs/check.mjs +6 -6
- package/runtime/scripts/hfs/coverage-scope.mjs +1 -1
- package/runtime/scripts/hfs/db-baseline-verb.mjs +48 -30
- package/runtime/scripts/hfs/db-plan-verb.mjs +17 -106
- package/runtime/scripts/hfs/db-restamp-verb.mjs +11 -10
- package/runtime/scripts/hfs/declaration-shape.mjs +15 -18
- package/runtime/scripts/hfs/manifest-shape.mjs +4 -4
- package/runtime/scripts/hfs/rule-params-shape.mjs +4 -1
- package/runtime/scripts/hfs/rules/a15-findings.mjs +1 -1
- package/runtime/scripts/hfs/rules/cli.mjs +1 -1
- package/runtime/scripts/hfs/rules/database-access.mjs +8 -74
- package/runtime/scripts/hfs/rules/database-baseline.mjs +3 -3
- package/runtime/scripts/hfs/rules/database-config.mjs +6 -214
- package/runtime/scripts/hfs/rules/database-constants.mjs +8 -10
- package/runtime/scripts/hfs/rules/database-migrations.mjs +3 -3
- package/runtime/scripts/hfs/rules/database-queues.mjs +4 -5
- package/runtime/scripts/hfs/rules/database-sql.mjs +1 -1
- package/runtime/scripts/hfs/rules/database.mjs +36 -39
- package/runtime/scripts/hfs/rules/edition.mjs +1 -1
- package/runtime/scripts/hfs/rules/{supabase-secrets.mjs → lite-secrets.mjs} +6 -6
- package/runtime/scripts/hfs/rules/script-hygiene.mjs +1 -10
- package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +5 -4
- package/runtime/scripts/hfs/rules/stacks-release.mjs +3 -2
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +2 -2
- package/runtime/scripts/hfs/rules/stacks.mjs +1 -1
- package/runtime/scripts/hfs/slot-app-view.mjs +1 -1
- package/runtime/scripts/hfs/slots.mjs +8 -2
- package/runtime/scripts/hfs/sql/pg-parse.mjs +2 -2
- package/runtime/scripts/lib/secret-patterns.mjs +2 -2
- package/runtime/scripts/lib/swarm-stack.mjs +7 -4
- package/scaffold/add-app.mjs +2 -10
- package/scaffold/add-cli-lite.mjs +6 -6
- package/scaffold/add-compose.mjs +1 -1
- package/scaffold/add-inbox-lite.mjs +4 -8
- package/scaffold/add-plan.mjs +1 -1
- package/scaffold/add-queue-lite.mjs +16 -24
- package/scaffold/add-table.mjs +34 -26
- package/scaffold/add.mjs +2 -2
- package/scaffold/app.mjs +19 -86
- package/scaffold/lite-exports.mjs +1 -1
- package/src/main.mjs +19 -20
- package/stack/target-binding.mjs +0 -60
- package/stack/urgent-floor.mjs +2 -2
- package/sync/index.mjs +32 -8
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +1 -17
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +6 -8
- package/templates/app/codegen-lite/scripts/codegen.mjs +2 -39
- package/templates/app/db-bootstrap/db/bootstrap.sql +139 -0
- package/templates/app/gitignore-lite +0 -3
- package/templates/app/package-scripts/db.json +9 -0
- package/templates/app/package-scripts/package.json +0 -1
- package/templates/app/package-scripts-lite/package.json +1 -1
- package/templates/app/skeleton/.starcistacks/dev/README.md +1 -1
- package/templates/app/skeleton/.starcistacks/production/README.md +1 -1
- package/templates/app/skeleton/README.md +1 -1
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +84 -19
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +5 -5
- package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +1 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +1 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +69 -6
- package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -7
- package/templates/app/skeleton-lite/.starcistacks/production/README.md +6 -6
- package/templates/app/skeleton-lite/.starcistacks/production/environment.json +1 -1
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +2 -2
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +69 -6
- package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +17 -9
- package/templates/app/skeleton-lite/README.md +11 -11
- package/templates/app/skeleton-lite/{supabase → db}/migrations/__timestamp___baseline.sql +21 -1
- package/templates/app/tool-config-lite/prettierignore +0 -1
- package/templates/be/inbox/database-client.spec.ts.tpl +9 -5
- package/templates/be/inbox/database-client.ts.tpl +13 -4
- package/templates/be/inbox/platform/client-operation-outcomes.sql.tpl +47 -0
- package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +3 -1
- package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +2 -2
- package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +6 -1
- package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +39 -7
- package/templates/be/inbox/platform/migration.sql.tpl +22 -0
- package/templates/be/inbox/service.spec.ts.tpl +2 -2
- package/templates/be/inbox/service.ts.tpl +1 -2
- package/templates/be/patterns/jobs/platform-lite/job-claim.service.spec.ts.tpl +156 -0
- package/templates/be/patterns/jobs/platform-lite/job-claim.service.ts.tpl +114 -0
- package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +51 -0
- package/templates/be/patterns/queues/platform-lite/queue-outbox.service.spec.ts.tpl +62 -0
- package/templates/be/patterns/queues/platform-lite/queue-outbox.service.ts.tpl +20 -0
- package/templates/be/patterns/queues/platform-lite/queue-outbox.sql.tpl +35 -0
- package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +4 -1
- package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +16 -5
- package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +8 -8
- package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +3 -3
- package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +2 -2
- package/templates/be/skeleton-lite/apps/api/Dockerfile +0 -1
- package/templates/be/skeleton-lite/apps/api/src/api.env.ts +9 -5
- package/templates/be/skeleton-lite/apps/api/src/api.options.ts +7 -4
- package/templates/be/skeleton-lite/apps/api/src/app.module.ts +4 -4
- package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +3 -3
- package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +9 -13
- package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +4 -2
- package/templates/be/skeleton-lite/apps/cli/src/cli.env.ts +2 -2
- package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +3 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +4 -4
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +3 -3
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +6 -6
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +3 -3
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/index.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.spec.ts +22 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.ts +25 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.config.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.decorators.ts +9 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.options.ts +35 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/errors/passport.error.ts +22 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/index.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.ts +23 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.ts +29 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.decorators.ts +22 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.options.ts +63 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.ts +11 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.spec.ts +32 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.ts +69 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +37 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts +36 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/oauth-state.store.ts +59 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.functions.ts +24 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +2 -1
- package/templates/be/skeleton-lite/src/modules/platform/database/database.options.ts +4 -2
- package/templates/be/skeleton-lite/src/modules/platform/database/database.sql.ts +19 -6
- package/templates/be/skeleton-lite/src/modules/platform/database/entities.ts +6 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/migration-runner.service.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/database/primary.config.ts +2 -2
- package/templates/be/skeleton-lite/src/modules/platform/database/seed-runner.service.ts +2 -2
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +18 -5
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +1 -1
- package/templates/be/table/entities.index.ts.tpl +4 -0
- package/templates/be/table/entity.ts.tpl +21 -0
- package/templates/be/table/index.ts.tpl +1 -0
- package/templates/be/table/module.ts.tpl +1 -1
- package/templates/be/table/service.api.ts.tpl +10 -11
- package/templates/be/table/service.ts.tpl +5 -6
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/auth-door.ts +30 -0
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +46 -14
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/token-store.ts +31 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/config/index.ts +2 -5
- package/db/local-migrate.mjs +0 -194
- package/db/reset.mjs +0 -125
- package/emit/db-types.mjs +0 -271
- package/runtime/scripts/api/supabase/database-dump.mjs +0 -5
- package/runtime/scripts/api/supabase/database-push.mjs +0 -5
- package/runtime/scripts/api/supabase/database-query.mjs +0 -5
- package/runtime/scripts/api/supabase/lib.mjs +0 -55
- package/runtime/scripts/api/supabase/migration-repair.mjs +0 -5
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +0 -50
- package/runtime/scripts/hfs/architecture/supabase.mjs +0 -226
- package/scaffold/supabase-ports.mjs +0 -26
- package/templates/app/package-scripts/supabase.json +0 -7
- package/templates/app/skeleton-lite/supabase/config.toml +0 -48
- package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +0 -5
- package/templates/be/inbox/platform/inbox/persistence/inbox.rows.ts.tpl +0 -33
- package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +0 -33
- package/templates/be/inbox/sql.ts.tpl +0 -16
- package/templates/be/patterns/jobs/platform-lite/persistence.jobs.sql.ts.tpl +0 -33
- package/templates/be/patterns/queues/platform-lite/persistence.queue.sql.ts.tpl +0 -12
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -16
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/index.ts +0 -6
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +0 -27
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.ts +0 -24
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.config.ts +0 -8
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.decorators.ts +0 -17
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.jwks.ts +0 -32
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.options.ts +0 -24
- package/templates/be/table/rows.ts.tpl +0 -4
- package/templates/be/table/sql.api.ts.tpl +0 -7
- package/templates/be/table/sql.ts.tpl +0 -4
- package/templates/fe/auth/supabase-client.ts.tpl +0 -38
- package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +0 -11
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +0 -18
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +0 -19
- /package/templates/app/skeleton-lite/{supabase → db}/seed.sql +0 -0
|
@@ -288,14 +288,14 @@ files:
|
|
|
288
288
|
role: "The consumed event's e2e flow; titles event-bus/<scenario>: ... prove the pattern."
|
|
289
289
|
spec: event-bus/atomic-with-transaction, event-bus/relay-only-after-commit, event-bus/redelivery-then-dead-letter, event-bus/duplicate-delivery-is-a-noop
|
|
290
290
|
rules: [BE_PATTERN_SPEC_MISSING]
|
|
291
|
-
# Edition lite (R212): the same ports and rules, a
|
|
291
|
+
# Edition lite (R212): the same ports and rules, a self-hosted Postgres adapter behind them. ruling:R212/case-10 moved the lite queue to Redis
|
|
292
292
|
# because full's queue is Redis; full's event bus is Kafka, so the lite event-bus adapter below is unchanged by that ruling.
|
|
293
293
|
liteAdapter:
|
|
294
294
|
summary: >-
|
|
295
295
|
Under lite the event bus keeps typed events, the vendored contract, publish(event, tx, correlationId) in the domain transaction, the outbox,
|
|
296
296
|
the inbox and thin consumers; Kafka is replaced by pgmq-backed topics, or by in-process delivery after commit for a back end
|
|
297
297
|
with a single api app. Each consumer group of a topic has its own pgmq queue `<service>__<event>__<group>`, created by a
|
|
298
|
-
|
|
298
|
+
db migration, named by a literal `<GROUP>_TOPIC_QUEUE` constant under platform/event-bus (its owner, R258) and
|
|
299
299
|
granted only to the application role, so each group keeps its own cursor and dead letter.
|
|
300
300
|
ports:
|
|
301
301
|
- {port: EventPublisher (platform/event-bus), adapter: the same outbox row in the domain transaction}
|
|
@@ -304,4 +304,4 @@ liteAdapter:
|
|
|
304
304
|
forbids: [kafkajs]
|
|
305
305
|
proof: >-
|
|
306
306
|
Unit specs as full; declaring event-bus requires integration specs titled `event-bus/<scenario>: ...` for every declared
|
|
307
|
-
scenario on `
|
|
307
|
+
scenario on the local db service (`npm run db:start`, R284). Lite has no e2e world.
|
|
@@ -81,7 +81,7 @@ rules:
|
|
|
81
81
|
(`ON CONFLICT (source, event_id) DO NOTHING RETURNING event_id`) returns `true` for a new row and `false` for a
|
|
82
82
|
duplicate without aborting the caller's transaction. It never reads first and writes after, which races two concurrent
|
|
83
83
|
deliveries of the same event. Lite uses its existing `public.inbox` table with the same client receipt columns,
|
|
84
|
-
owned by a forward-only
|
|
84
|
+
owned by a forward-only db migration. A client receipt extends this table; no capability creates another
|
|
85
85
|
dedupe table or unique request-key constraint to compete with it.
|
|
86
86
|
rationale: |-
|
|
87
87
|
One shared table with one owner keeps the guarantee "this (source, event id) claims at most once" in one place, and
|
|
@@ -250,7 +250,7 @@ files:
|
|
|
250
250
|
liteAdapter:
|
|
251
251
|
summary: >-
|
|
252
252
|
Lite keeps the Inbox port and transaction-owned client receipt API. Its existing public.inbox
|
|
253
|
-
table uses
|
|
253
|
+
table uses db migration authority; the be/inbox platform templates carry the lite adapter and the
|
|
254
254
|
platform/client-operation-outcomes.sql.tpl forward migration. The files tree above describes the full TypeORM adapter.
|
|
255
255
|
Delivery-only webhook scaffolds do not install a client marker. When adopting this client pattern, add the shared
|
|
256
256
|
idempotency/platform/inbox/effectful.decorators.ts.tpl as src/modules/platform/inbox/effectful.decorators.ts and
|
|
@@ -261,5 +261,5 @@ liteAdapter:
|
|
|
261
261
|
- {port: receipt retention, adapter: a fenced job delivered by the BullMQ queue on the declared Redis and scheduled by a pg_cron tick that enqueues through the queue outbox}
|
|
262
262
|
proof: >-
|
|
263
263
|
The same inbox and fingerprint unit specs apply; declaring idempotency requires both client receipt scenarios in
|
|
264
|
-
lite's local
|
|
264
|
+
lite's local db integration lane.
|
|
265
265
|
see: [knowledge/patterns/be/queues.yaml]
|
|
@@ -277,18 +277,18 @@ files:
|
|
|
277
277
|
role: "Titles fenced-job/<scenario>: ... prove the pattern."
|
|
278
278
|
spec: fenced-job/claim-bumps-token, fenced-job/zombie-fenced-out, fenced-job/redispatch-isolated
|
|
279
279
|
rules: [BE_PATTERN_SPEC_MISSING]
|
|
280
|
-
# Edition lite (R212, owner ruling 2026-10-14): the same ports and rules, a
|
|
280
|
+
# Edition lite (R212, owner ruling 2026-10-14): the same ports and rules, a self-hosted Postgres adapter behind them.
|
|
281
281
|
liteAdapter:
|
|
282
282
|
summary: >-
|
|
283
283
|
Under lite a fenced job is unchanged: the job row with its fencing token, the atomic claim, guarded writes and run keys. The
|
|
284
|
-
row's table is a
|
|
284
|
+
row's table is a db migration, and the processor is fed by full's BullMQ queue on the declared Redis, so a lite app
|
|
285
285
|
that declares fenced-job declares `backing: [redis]`. Without a declared Redis a lite app has no fenced job; a long task is
|
|
286
286
|
a declared replicated-job running a cli command.
|
|
287
287
|
ports:
|
|
288
|
-
- {port: job claim (platform/jobs), adapter: "the
|
|
288
|
+
- {port: job claim (platform/jobs), adapter: "the claim and guarded writes as the job_claim, job_advance, job_complete and job_fail functions of the jobs migration in db/migrations, called through callDbFunction of platform/database (R334)"}
|
|
289
289
|
- {port: processor delivery, adapter: "full's BullMQ worker (attempts and exponential backoff from the queue policy, failed jobs kept as the dead letter)"}
|
|
290
290
|
proof: >-
|
|
291
291
|
Unit specs as full (job-claim.service.spec.ts); declaring fenced-job requires integration specs titled
|
|
292
|
-
`fenced-job/<scenario>: ...` for every declared scenario on the local
|
|
292
|
+
`fenced-job/<scenario>: ...` for every declared scenario on the local db service (R284, which runs no container of its
|
|
293
293
|
own): the claim, the fencing token and the guarded writes are Postgres facts, driven there by the real runner without BullMQ.
|
|
294
294
|
see: [knowledge/patterns/be/queues.yaml]
|
|
@@ -176,15 +176,17 @@ files:
|
|
|
176
176
|
role: "Titles projection/<scenario>: ... prove the pattern."
|
|
177
177
|
spec: projection/recompute-is-idempotent, projection/replay-rebuilds
|
|
178
178
|
rules: [BE_PATTERN_SPEC_MISSING]
|
|
179
|
-
# Edition lite (R212, owner ruling 2026-10-14): the same ports and rules, a
|
|
179
|
+
# Edition lite (R212, owner ruling 2026-10-14): the same ports and rules, a self-hosted Postgres adapter behind them.
|
|
180
180
|
liteAdapter:
|
|
181
181
|
summary: >-
|
|
182
182
|
Under lite a projection is unchanged in behaviour (only it writes its table, recompute is idempotent and replayable, readers
|
|
183
|
-
use get*); its table is a
|
|
184
|
-
|
|
183
|
+
use get*); its table is a db migration mapped by a projection entity under persistence/entities/ (named in PRIMARY_ENTITIES of
|
|
184
|
+
platform/database/entities.ts) and read and written through the EntityManager, never raw SQL (R334, ruling
|
|
185
|
+
lite-persistence-typeorm-entities), and it is kept current by a reactor on the lite event bus or by a job on the queue (BullMQ on
|
|
186
|
+
the declared Redis).
|
|
185
187
|
ports:
|
|
186
|
-
- {port: read model table, adapter: "a
|
|
188
|
+
- {port: read model table, adapter: "a db migration plus its entity under persistence/entities/ and the capability's persistence/entities/index.ts list; no <name>.sql.ts or <name>.rows.ts"}
|
|
187
189
|
proof: >-
|
|
188
190
|
Unit specs as full (the projection is a logic role at 100 per file); declaring projection requires integration specs titled
|
|
189
|
-
`projection/<scenario>: ...` for every declared scenario on `
|
|
191
|
+
`projection/<scenario>: ...` for every declared scenario on the local db service (`npm run db:start`, R284).
|
|
190
192
|
see: [knowledge/patterns/be/event-bus.yaml, knowledge/patterns/be/queues.yaml]
|
|
@@ -83,7 +83,7 @@ rules:
|
|
|
83
83
|
hfsRules: [R90, R258]
|
|
84
84
|
requirement: |-
|
|
85
85
|
A recurring task is a job scheduler (`upsertJobScheduler`) that the queue module declares in its `schedulers` list and
|
|
86
|
-
`platform/queue` registers at boot, or, in
|
|
86
|
+
`platform/queue` registers at boot, or, in a lite app, a uniquely named pg_cron job of a migration whose command
|
|
87
87
|
(directly or through one `*_tick` function) only inserts one row of a queue constant into the queue outbox table of
|
|
88
88
|
`ruleParams.db.queueOutbox` (`public.queue`), which the relay hands to BullMQ, or by calling the enqueue function that the app declares with its own outbox table in `hfs.json` `sides.be.queueOutbox` (redis declared in `sides.be.backing`; the function's active definition only inserts into that table); a tick has exactly one of the two owners. A job name is scheduled once while it is active: a later migration that replaces a tick's command calls `cron.unschedule` of the name first. Its job is processed by the same
|
|
89
89
|
`features/jobs/<job>/<job>.processor.ts` as any enqueued job. `@nestjs/schedule`, `@Cron`, `@Interval`, `@Timeout` and
|
|
@@ -96,7 +96,7 @@ rules:
|
|
|
96
96
|
when: Expire unpaid orders every five minutes
|
|
97
97
|
write: 'order-expiry.queue.ts declares scheduler { every: 5 minutes } for the expire-orders job; the processor dispatches ExpireOrdersCommand.'
|
|
98
98
|
- id: case-2
|
|
99
|
-
when: A lite app
|
|
99
|
+
when: A lite app sends booking reminders every fifteen minutes
|
|
100
100
|
write: "A migration declares private.booking_reminder_tick() as one insert into public.queue (queue, payload, created_at) values ('booking_reminder', jsonb_build_object('tick', true), now()) and schedules it with cron.schedule('booking_reminder_tick', '*/15 * * * *', ...); the send-reminder processor is the queue's."
|
|
101
101
|
verification:
|
|
102
102
|
automated:
|
|
@@ -275,19 +275,19 @@ liteAdapter:
|
|
|
275
275
|
full's adapter: the relay marks an outbox row sent only after the transport's persist step confirms Redis wrote the job to
|
|
276
276
|
its append-only file (WAITAOF) and still holds it, so a job Redis loses inside the at most one second fsync window leaves
|
|
277
277
|
its row waiting and is added again; that is why a lite queue does not lose a job. Without a declared Redis there is no queue
|
|
278
|
-
pattern: the work is a direct call inside the request, or a declared replicated-job running a cli command.
|
|
279
|
-
|
|
280
|
-
tables as one
|
|
278
|
+
pattern: the work is a direct call inside the request, or a declared replicated-job running a cli command. pgmq
|
|
279
|
+
is not a queue transport. `starci app add queue|job` writes this adapter in a lite app, with the outbox and job
|
|
280
|
+
tables as one db migration, the Redis declaration and the persisted Redis of every docker-swarm environment.
|
|
281
281
|
ports:
|
|
282
|
-
- {port: QueueOutbox (platform/queue), adapter: "
|
|
282
|
+
- {port: QueueOutbox (platform/queue), adapter: "the outbox row written through the caller's tx by queue_write, and the relay's queue_waiting and queue_mark_sent, functions of the queue migration in db/migrations called through callDbFunction of platform/database (R334)"}
|
|
283
283
|
- {port: queue transport (queue-transport.port.ts), adapter: "full's bullmq-queue-transport.client.ts over the declared Redis (host, port, db and the password read from its sealed secret file), plus persist(jobs): WAITAOF 1 0 on each queue's connection, then each job's state, refusing with QUEUE_NOT_PERSISTED or QUEUE_JOB_LOST (errors/queue.error.ts)"}
|
|
284
284
|
- {port: outbox relay (queue-relay.service.ts), adapter: "full's relay, which calls persist after the adds and before it marks the rows sent, so a refusal rolls the mark back and the next pass adds the rows again"}
|
|
285
285
|
- {port: QueueWorkerRegistry (platform/queue), adapter: "full's BullMQ worker per queue: attempts and exponential backoff from queue.policy.ts, failed jobs kept as the dead letter"}
|
|
286
|
-
- {port: schedulers, adapter: "a BullMQ job scheduler declared with its queue, as full, or a pg_cron job in a
|
|
286
|
+
- {port: schedulers, adapter: "a BullMQ job scheduler declared with its queue, as full, or a pg_cron job in a db migration whose tick inserts one row of the queue constant into the queue outbox"}
|
|
287
287
|
forbids: [kafkajs, "@nestjs/schedule"]
|
|
288
288
|
proof: >-
|
|
289
289
|
Unit specs as full (queue-outbox, relay and worker services, the BullMQ client against a mocked factory); declaring queue
|
|
290
|
-
requires integration specs titled `queue/<scenario>: ...` for every declared scenario on the local
|
|
290
|
+
requires integration specs titled `queue/<scenario>: ...` for every declared scenario on the local db service (R284,
|
|
291
291
|
which runs no container of its own): the outbox, its commit boundary, the relay identity and the pg_cron tick are Postgres
|
|
292
292
|
facts proved there with the real outbox writer and relay, and the queue transport records what the relay hands to BullMQ
|
|
293
293
|
(the job id is the outbox id). BullMQ itself is covered by the unit spec of the BullMQ client, which also proves the
|
|
@@ -154,7 +154,7 @@ files:
|
|
|
154
154
|
- path: src/tests/e2e/<area>/<event>.e2e-spec.ts
|
|
155
155
|
slot: be.tests.e2e
|
|
156
156
|
role: The reactor's event flow through the bus, redelivery included.
|
|
157
|
-
# Edition lite (R212, owner ruling 2026-10-14): the same ports and rules, a
|
|
157
|
+
# Edition lite (R212, owner ruling 2026-10-14): the same ports and rules, a self-hosted Postgres adapter behind them.
|
|
158
158
|
liteAdapter:
|
|
159
159
|
summary: >-
|
|
160
160
|
Under lite a reactor is unchanged: a thin consumer of another context's events that dispatches one command. Its deliveries
|
|
@@ -163,5 +163,5 @@ liteAdapter:
|
|
|
163
163
|
- {port: EventConsumer delivery, adapter: "the reactor's pgmq queue (see event-bus.yaml liteAdapter)"}
|
|
164
164
|
proof: >-
|
|
165
165
|
`starci app check --wiring` proves the composing lite app's module graph with doubles (R272); declaring event-bus requires
|
|
166
|
-
its scenario-titled integration specs on `
|
|
166
|
+
its scenario-titled integration specs on the local db service (`npm run db:start`, R284), including redelivery through the reactor.
|
|
167
167
|
see: [knowledge/patterns/be/event-bus.yaml]
|
|
@@ -290,8 +290,8 @@ rules:
|
|
|
290
290
|
the memory hub (STACKS_REALTIME_SCALED) and the two-replica scenarios are the realtime scale rule BE-REALTIME-7.
|
|
291
291
|
|
|
292
292
|
Lite (ruling:R212/case-10, owner:lite-vector-is-pgvector):
|
|
293
|
-
ONE instance type, one api deployable, scales horizontally and stateless, with no sticky sessions, on Postgres
|
|
294
|
-
Redis when a declared capability needs it. Vector search uses
|
|
293
|
+
ONE instance type, one api deployable, scales horizontally and stateless, with no sticky sessions, on its own Postgres plus
|
|
294
|
+
Redis when a declared capability needs it. Vector search uses pgvector in the db service. The three doors above are served from that api (the GraphQL subscription door is new
|
|
295
295
|
in lite, ruling:R212/case-7). A lite api with more than one replica uses the Redis hub when Redis is
|
|
296
296
|
declared, else the Postgres hub. The example is the SSE `booking-status` channel.
|
|
297
297
|
|
|
@@ -373,7 +373,7 @@ rules:
|
|
|
373
373
|
snapshot with a query on every reconnect. The engines (`socket.io`, `@socket.io/postgres-adapter`, `@socket.io/redis-adapter`,
|
|
374
374
|
`graphql-ws`, `graphql-subscriptions`, `pg` for LISTEN and `ioredis`) are imported only by `platform/realtime`
|
|
375
375
|
(`BE_INFRA_OWNER`). The tables are migrations of the app (`realtime_events` and, for the Postgres socket adapter,
|
|
376
|
-
`socket_io_attachments`: a
|
|
376
|
+
`socket_io_attachments`: a db migration in lite, a TypeORM migration of `platform/realtime` in full). Declaring realtime
|
|
377
377
|
adds two scenarios, proven with two api replicas: `realtime/push-crosses-replicas` (published through replica A, received on
|
|
378
378
|
replica B) and `realtime/resume-after-reconnect` (`Last-Event-ID` sent to the other replica receives the missed frames).
|
|
379
379
|
rationale: |-
|
|
@@ -406,7 +406,7 @@ rules:
|
|
|
406
406
|
publish writes the realtime_events row and NOTIFYs; every replica LISTENs and writes the frame to its local streams; a
|
|
407
407
|
reconnect to ANY replica sends Last-Event-ID and the hub reads the missed rows from the table. A domain service that owns the
|
|
408
408
|
change publishes inside its own transaction (`hub.publish(topic, frame, tx)`), so the confirmation and its push commit together. At the ingress the stream is
|
|
409
|
-
not buffered or compressed and the idle timeout is above the heartbeat. Migration:
|
|
409
|
+
not buffered or compressed and the idle timeout is above the heartbeat. Migration: db/migrations/<ts>_realtime-events.sql
|
|
410
410
|
(lite) or platform/realtime/persistence/migrations/<epochMs>-create-realtime-events.ts (full).
|
|
411
411
|
- id: case-3
|
|
412
412
|
when: SSE, scale on Redis (Redis declared, lite or full)
|
|
@@ -668,9 +668,9 @@ files:
|
|
|
668
668
|
slot: be.persistence
|
|
669
669
|
role: Full edition migration of the realtime_events replay table and the socket_io_attachments table of the Postgres adapter.
|
|
670
670
|
optional: true
|
|
671
|
-
- path:
|
|
671
|
+
- path: db/migrations/<timestamp>_realtime-events.sql
|
|
672
672
|
slot: repo.side-root-forbidden
|
|
673
|
-
role: Lite edition migration (created with
|
|
673
|
+
role: Lite edition migration (created with `npm run db:new`) of the same two tables, forced row level security for app_be, and the pg_cron tick of the retention queue.
|
|
674
674
|
optional: true
|
|
675
675
|
- path: src/features/reactors/<name>/transport/message/<event>.consumer.ts
|
|
676
676
|
slot: be.transport.message
|
|
@@ -262,7 +262,7 @@ rules:
|
|
|
262
262
|
manual:
|
|
263
263
|
- Check the injected fault hits the compensation, not only the step before it.
|
|
264
264
|
relatedExamples: [saga]
|
|
265
|
-
# Edition lite (R212, owner ruling 2026-10-14): the same ports and rules, a
|
|
265
|
+
# Edition lite (R212, owner ruling 2026-10-14): the same ports and rules, a self-hosted Postgres adapter behind them.
|
|
266
266
|
liteAdapter:
|
|
267
267
|
summary: >-
|
|
268
268
|
Under lite a saga is unchanged: the orchestrator service, the versioned state behind the fenced store, steps and their
|
|
@@ -270,9 +270,9 @@ liteAdapter:
|
|
|
270
270
|
consumer group).
|
|
271
271
|
ports:
|
|
272
272
|
- {port: saga events, adapter: "the lite event-bus adapter (pgmq) - see event-bus.yaml liteAdapter"}
|
|
273
|
-
- {port: saga state store, adapter: "the same table and version fence, created by a
|
|
273
|
+
- {port: saga state store, adapter: "the same table and version fence, created by a db migration"}
|
|
274
274
|
proof: >-
|
|
275
275
|
The orchestrator's unit spec (<saga>.saga.service.spec.ts) as full; declaring saga requires integration specs titled
|
|
276
|
-
`saga/<scenario>: ...` for every declared scenario on `
|
|
276
|
+
`saga/<scenario>: ...` for every declared scenario on the local db service (`npm run db:start`, R284), including each compensation path with
|
|
277
277
|
failure injection and the final state read back. The e2e world of R173 remains full-only.
|
|
278
278
|
see: [knowledge/patterns/be/event-bus.yaml]
|
|
@@ -370,7 +370,7 @@ sourceMaintenance: >-
|
|
|
370
370
|
verifies first and calls the supplied domain service's record<Provider>Delivery once. Implement that method as the
|
|
371
371
|
transactional inbox receipt/outbox recording intake above, then add its reactor or job separately.
|
|
372
372
|
In lite, the generated POST verifies first and calls <Provider>InboxService.record<Provider>Delivery. This record-only
|
|
373
|
-
domain intake uses recordWebhookDelivery to insert a pending provider inbox row atomically in
|
|
373
|
+
domain intake uses recordWebhookDelivery to insert a pending provider inbox row atomically in the app's Postgres, unique on
|
|
374
374
|
(provider, delivery_id), without processing it or marking it complete. Add a reactor or job separately to consume the
|
|
375
375
|
committed pending row and dispatch the processing command (reactors.yaml, jobs.yaml). The --service option validates the
|
|
376
376
|
future domain processing owner; the webhook writer does not import or call that owner synchronously and does not
|
|
@@ -182,7 +182,7 @@ raw-tools:
|
|
|
182
182
|
podman: {use: 'starci docker up, starci docker down, starci docker build, or starci docker ps'}
|
|
183
183
|
kubectl: {use: 'starci app stack'}
|
|
184
184
|
k3d: {use: 'starci app stack'}
|
|
185
|
-
supabase: {use: "starci
|
|
185
|
+
supabase: {use: "starci app db new|plan|migrate|dump, and npm run db:start or db:stop for the app's own database"}
|
|
186
186
|
gh: {use: 'starci release cut (the GitHub release is created from the CHANGELOG section of the tag); anything else is the owner''s'}
|
|
187
187
|
schtasks: {use: 'starci task register, starci task show, or starci task list for runtime scheduled tasks'}
|
|
188
188
|
shutdown: {use: 'ask the owner: a worker never shuts the host down'}
|
|
@@ -870,7 +870,7 @@ BE_JSON_PARSE_UNGUARDED:
|
|
|
870
870
|
BE_JWT_ALG_PINNED:
|
|
871
871
|
title: "JWT verification has no pinned asymmetric algorithm policy"
|
|
872
872
|
title_vi: "Xác minh JWT thiếu chính sách thuật toán bất đối xứng cố định"
|
|
873
|
-
meaning_vi: "Bộ xác minh chưa cố định thuật toán an toàn, nguồn cấu hình của issuer hoặc vai trò authenticated của principal
|
|
873
|
+
meaning_vi: "Bộ xác minh chưa cố định thuật toán an toàn, nguồn cấu hình của issuer hoặc vai trò authenticated của principal."
|
|
874
874
|
causes_vi:
|
|
875
875
|
- "Danh sách algorithms rỗng, động hoặc chứa none hay HS256"
|
|
876
876
|
- "Issuer viết trực tiếp hoặc principal chấp nhận vai trò khác authenticated"
|
|
@@ -1497,25 +1497,7 @@ BE_SQL_TABLE_OWNER:
|
|
|
1497
1497
|
owner: op-retry
|
|
1498
1498
|
kind: check-finding
|
|
1499
1499
|
|
|
1500
|
-
BE_SUPABASE_CLIENT_OWNER:
|
|
1501
|
-
title: "A back-end Supabase client, import or call is outside `be.integrations.supabase`, or is not typed with `Database`"
|
|
1502
|
-
title_vi: "Supabase client phía back end sai chủ sở hữu hoặc thiếu kiểu Database"
|
|
1503
|
-
meaning_vi: "Import/call Supabase ở `<path>` không thuộc `be.integrations.supabase`, hoặc client được tạo mà không mang kiểu `Database`."
|
|
1504
|
-
causes_vi:
|
|
1505
|
-
- "Vi phạm L10: back end dùng EntityManager cho dữ liệu; chỉ integration Supabase sở hữu Auth/Storage client và kiểu Database."
|
|
1506
|
-
nextStep_vi: "Chuyển Auth/Storage client vào `be.integrations.supabase`, dùng API công khai của integration và thêm kiểu `Database`; dữ liệu thường đi qua EntityManager."
|
|
1507
|
-
owner: op-retry
|
|
1508
|
-
kind: check-finding
|
|
1509
1500
|
|
|
1510
|
-
BE_SUPABASE_JWT_VERIFIED:
|
|
1511
|
-
title: "A Supabase principal is derived without pinned JWKS verification, or authorization claims come from the request body"
|
|
1512
|
-
title_vi: "JWT Supabase chưa được xác minh đầy đủ hoặc claim lấy từ request body"
|
|
1513
|
-
meaning_vi: "`<path>` decode JWT không verify, gọi `jwtVerify` ngoài integration/thiếu JWKS, issuer, audience `authenticated` hay algorithm pin, hoặc đọc role/membership từ body."
|
|
1514
|
-
causes_vi:
|
|
1515
|
-
- "Vi phạm L16: principal chỉ đến từ access token đã verify signature/iss/aud/exp/algorithm hoặc được đọc lại từ DB."
|
|
1516
|
-
nextStep_vi: "Dùng `createRemoteJWKSet` + `jwtVerify` trong `be.integrations.supabase`, pin issuer/audience/algorithms và bỏ mọi authorization claim từ body."
|
|
1517
|
-
owner: op-retry
|
|
1518
|
-
kind: check-finding
|
|
1519
1501
|
|
|
1520
1502
|
BE_TEST_BUILDER_ARRANGES:
|
|
1521
1503
|
title: "A test data builder asserts or has a non-deterministic default"
|
|
@@ -1811,15 +1793,6 @@ CLI_ONLY_ENTRY:
|
|
|
1811
1793
|
owner: op-retry
|
|
1812
1794
|
kind: check-finding
|
|
1813
1795
|
|
|
1814
|
-
DB_API_EXPOSED:
|
|
1815
|
-
title: "The database API never exposes pgmq and lite data access is closed"
|
|
1816
|
-
title_vi: "API cơ sở dữ liệu mở schema bị cấm"
|
|
1817
|
-
meaning_vi: "config.toml mở pgmq hoặc pgmq_public, hoặc ứng dụng lite mở API dữ liệu mà không có policy public_read rõ ràng."
|
|
1818
|
-
causes_vi:
|
|
1819
|
-
- "Vi phạm R259: cửa dữ liệu không khớp quy tắc front end chỉ dùng Supabase Auth."
|
|
1820
|
-
nextStep_vi: "Loại pgmq và pgmq_public khỏi schema API; đóng API dữ liệu lite hoặc khai báo policy public_read giới hạn cần thiết."
|
|
1821
|
-
owner: op-retry
|
|
1822
|
-
kind: check-finding
|
|
1823
1796
|
|
|
1824
1797
|
DB_APP_ROLE:
|
|
1825
1798
|
title: "The back-end database role has explicit least privileges"
|
|
@@ -1852,26 +1825,7 @@ DB_BASELINE_SHAPE:
|
|
|
1852
1825
|
owner: op-retry
|
|
1853
1826
|
kind: check-finding
|
|
1854
1827
|
|
|
1855
|
-
DB_BE_ONLY_ACCESS:
|
|
1856
|
-
title: "Exposed product data is accessed through the back end"
|
|
1857
|
-
title_vi: "Dữ liệu exposed được mở trực tiếp cho trình duyệt"
|
|
1858
|
-
meaning_vi: "Migration tại <path> cấp quyền bảng hoặc sequence cho authenticated, khai báo policy cho authenticated, hoặc mở quyền anon ngoài policy select có tên kết thúc _public_read; đối tượng chưa được khai báo ngoại lệ supabase.directAccess."
|
|
1859
|
-
causes_vi:
|
|
1860
|
-
- "Vi phạm R254: Supabase phía front end chỉ phục vụ Auth; dữ liệu sản phẩm đi qua back end."
|
|
1861
|
-
- "Grant hoặc policy mới mở lại cửa dữ liệu trong schema exposed mà không có ngoại lệ rõ ràng."
|
|
1862
|
-
nextStep_vi: "Gỡ grant và policy truy cập trực tiếp; cho back end truy cập bằng app role. Nếu chủ ý mở đối tượng, khai báo bảng trong hfs.json supabase.directAccess; anon chỉ đọc qua policy select *_public_read."
|
|
1863
|
-
owner: op-retry
|
|
1864
|
-
kind: check-finding
|
|
1865
1828
|
|
|
1866
|
-
DB_CONFIG_POLICY:
|
|
1867
|
-
title: "`supabase/config.toml` holds no literal credential (`env(NAME)` only), `auth.jwt_expiry <= 3600`, and signup/site_url/redirects equal the Supabase block of hfs.json"
|
|
1868
|
-
title_vi: "config.toml lệch policy"
|
|
1869
|
-
meaning_vi: "`<path>` (supabase/config.toml) chứa giá trị literal dưới key credential (secret|password|token|client_id|*_key trừ anon/publishable), `auth.jwt_expiry` quá 3600, hoặc `enable_signup`/`site_url`/`additional_redirect_urls` khác với block Supabase mà hfs.json khai báo."
|
|
1870
|
-
causes_vi:
|
|
1871
|
-
- "Vi phạm luật L08: secret trong config.toml viết `env(NAME)`; jwt_expiry tối đa 3600; posture auth là fact được check, không phải claim trong doc."
|
|
1872
|
-
nextStep_vi: "Đổi credential thành `env(TEN_BIEN)`, hạ `jwt_expiry` về <= 3600, và chỉnh block Supabase của hfs.json hoặc config.toml cho khớp nhau."
|
|
1873
|
-
owner: op-retry
|
|
1874
|
-
kind: check-finding
|
|
1875
1829
|
|
|
1876
1830
|
DB_CRON_OWNED:
|
|
1877
1831
|
title: "A uniquely owned migration cron job only enqueues through the queue outbox"
|
|
@@ -1930,7 +1884,7 @@ DB_FORCE_RLS_DEFAULT:
|
|
|
1930
1884
|
meaning_vi: "Bảng tại <path> thuộc app role nhưng thiếu force row level security và không có ngoại lệ forceRls kèm lý do không rỗng trong hfs.json."
|
|
1931
1885
|
causes_vi:
|
|
1932
1886
|
- "Vi phạm R255: chủ sở hữu bảng có thể bỏ qua RLS khi không có force row level security."
|
|
1933
|
-
nextStep_vi: "Thêm alter table <schema>.<table> force row level security trong migration; nếu cần miễn, khai báo đúng bảng và lý do trong hfs.json
|
|
1887
|
+
nextStep_vi: "Thêm alter table <schema>.<table> force row level security trong migration; nếu cần miễn, khai báo đúng bảng và lý do trong hfs.json db.forceRls."
|
|
1934
1888
|
owner: op-retry
|
|
1935
1889
|
kind: check-finding
|
|
1936
1890
|
|
|
@@ -1949,8 +1903,8 @@ DB_MIGRATION_SHAPE:
|
|
|
1949
1903
|
title_vi: "Migration sai hình dạng hoặc bị sửa"
|
|
1950
1904
|
meaning_vi: "`<path>` vi phạm luật migration: tên phải là `<ts14>_<kebab>.sql`, stamp là thời điểm UTC thật và không trùng giữa các migration đang hoạt động; migration mới xếp sau chuỗi đang hoạt động đã commit trên HEAD. Archive ghi nhận migration đã nghỉ khỏi chuỗi, nên baseline thay thế có thể mang stamp trước archive. Migration đã lên base là bất biến (sửa hay xoá mà không archive đều là finding)."
|
|
1951
1905
|
causes_vi:
|
|
1952
|
-
- "Vi phạm luật L02: `
|
|
1953
|
-
nextStep_vi: "Tạo migration mới bằng
|
|
1906
|
+
- "Vi phạm luật L02: `db/migrations/<ts14>_<kebab>.sql`; stamp mới tăng nghiêm ngặt so với chuỗi đang hoạt động đã commit trên HEAD, không so với đồng hồ, thời gian commit hay stamp trên remote; file đã lên base không được đổi."
|
|
1907
|
+
nextStep_vi: "Tạo migration mới bằng `npm run db:new -- <ten>`, không sửa/đổi tên/xoá migration đã lên base; nếu cần đổi nội dung, viết migration mới."
|
|
1954
1908
|
owner: op-retry
|
|
1955
1909
|
kind: check-finding
|
|
1956
1910
|
|
|
@@ -1987,22 +1941,13 @@ DB_POLICY_TAUTOLOGY:
|
|
|
1987
1941
|
owner: op-retry
|
|
1988
1942
|
kind: check-finding
|
|
1989
1943
|
|
|
1990
|
-
DB_REALTIME_UNLISTED:
|
|
1991
|
-
title: "Realtime publication tables require declared direct access"
|
|
1992
|
-
title_vi: "Bảng Realtime chưa được khai báo truy cập trực tiếp"
|
|
1993
|
-
meaning_vi: "Migration tại <path> thêm bảng vào publication supabase_realtime nhưng bảng không có trong hfs.json supabase.directAccess."
|
|
1994
|
-
causes_vi:
|
|
1995
|
-
- "Vi phạm R254: publication Realtime mở một cửa dữ liệu trực tiếp chưa được khai báo."
|
|
1996
|
-
nextStep_vi: "Gỡ bảng khỏi publication supabase_realtime hoặc khai báo ngoại lệ có chủ ý trong hfs.json supabase.directAccess."
|
|
1997
|
-
owner: op-retry
|
|
1998
|
-
kind: check-finding
|
|
1999
1944
|
|
|
2000
1945
|
DB_RLS_REQUIRED:
|
|
2001
1946
|
title: "Every `create table` in an exposed schema is followed by `enable row level security` in the same migration"
|
|
2002
1947
|
title_vi: "Bảng trong schema public thiếu RLS"
|
|
2003
1948
|
meaning_vi: "`<path>` tạo bảng trong schema exposed (`public` và `[api] schemas` của config.toml) mà không có `alter table ... enable row level security` ngay trong cùng migration; bảng khai báo `forceRls` cần cả `force row level security`."
|
|
2004
1949
|
causes_vi:
|
|
2005
|
-
- "Vi phạm luật L03: mọi `create table`/`create table as` trong schema exposed phải có enable (và force khi hfs.json `
|
|
1950
|
+
- "Vi phạm luật L03: mọi `create table`/`create table as` trong schema exposed phải có enable (và force khi hfs.json `db.forceRls` khai báo) trong cùng migration; partition con và bảng temp được miễn."
|
|
2006
1951
|
nextStep_vi: "Viết `alter table <schema>.<table> enable row level security;` ngay sau create trong cùng migration; không dùng vòng lặp dynamic SQL."
|
|
2007
1952
|
owner: op-retry
|
|
2008
1953
|
kind: check-finding
|
|
@@ -2027,15 +1972,6 @@ DB_STORAGE_PREFIX:
|
|
|
2027
1972
|
owner: op-retry
|
|
2028
1973
|
kind: check-finding
|
|
2029
1974
|
|
|
2030
|
-
DB_TYPES_DRIFT:
|
|
2031
|
-
title: "`supabase/types/database.types.ts` is committed and equals what `contract:emit` regenerates from the migrations"
|
|
2032
|
-
title_vi: "database.types.ts lệch với migrations"
|
|
2033
|
-
meaning_vi: "`supabase/types/database.types.ts` chưa được commit, hoặc khác với text mà `contract:emit` sinh lại từ migrations hiện tại (hoặc emit thất bại). Pattern giống R23 HFS_CONTRACT_SNAPSHOT_DRIFT."
|
|
2034
|
-
causes_vi:
|
|
2035
|
-
- "Vi phạm luật L09: file types là artefact sinh; commit bản mới nhất, không sửa tay."
|
|
2036
|
-
nextStep_vi: "Chạy `starci app emit` và commit `supabase/types/database.types.ts` kết quả."
|
|
2037
|
-
owner: op-retry
|
|
2038
|
-
kind: check-finding
|
|
2039
1975
|
|
|
2040
1976
|
DB_VAULT_WRITE:
|
|
2041
1977
|
title: "Migrations contain no vault writes or role passwords"
|
|
@@ -2197,16 +2133,6 @@ FE_BUTTON_NO_VARIANT:
|
|
|
2197
2133
|
owner: op-retry
|
|
2198
2134
|
kind: check-finding
|
|
2199
2135
|
|
|
2200
|
-
FE_CALLBACK_SHAPE:
|
|
2201
|
-
title: "An auth callback performs more than code exchange and redirect"
|
|
2202
|
-
title_vi: "Callback xác thực làm thêm việc ngoài đổi mã và chuyển hướng"
|
|
2203
|
-
meaning_vi: "Handler của fe.route.callback chứa operation dữ liệu, logic sản phẩm, handler bổ sung hoặc phản hồi JSON thay vì chỉ đổi mã xác thực rồi chuyển hướng."
|
|
2204
|
-
causes_vi:
|
|
2205
|
-
- "Callback gọi operation API hoặc xử lý dữ liệu sản phẩm"
|
|
2206
|
-
- "Callback trả JSON hoặc định nghĩa thêm handler"
|
|
2207
|
-
nextStep_vi: "Giữ callback ở việc đọc mã, tạo client xác thực, đổi mã và chuyển hướng; chuyển logic sản phẩm về back end hoặc hook phù hợp."
|
|
2208
|
-
owner: op-retry
|
|
2209
|
-
kind: check-finding
|
|
2210
2136
|
|
|
2211
2137
|
FE_CLIENT_BOUNDARY:
|
|
2212
2138
|
title: "Server first; `\"use client\"` only where allowed"
|
|
@@ -2925,15 +2851,6 @@ FE_SERVER_ONLY_MARK:
|
|
|
2925
2851
|
owner: op-retry
|
|
2926
2852
|
kind: check-finding
|
|
2927
2853
|
|
|
2928
|
-
FE_SERVICE_ROLE_FORBIDDEN:
|
|
2929
|
-
title: "A service-role or credential-shaped environment value enters the front end"
|
|
2930
|
-
title_vi: "Service role hoặc biến môi trường bí mật đi vào front end"
|
|
2931
|
-
meaning_vi: "`<path>` chứa identifier/string service_role hoặc đọc biến `*_SECRET`, `*_PRIVATE`, `*_TOKEN`, `*_PASSWORD`; public config không được khai báo qua chủ `modules/config`."
|
|
2932
|
-
causes_vi:
|
|
2933
|
-
- "Vi phạm L13: front end chỉ giữ URL, anon/publishable key và public config; service role luôn ở back end."
|
|
2934
|
-
nextStep_vi: "Xóa credential khỏi fe/, chuyển thao tác đặc quyền qua use case back end, và khai báo public config tại `modules/config`."
|
|
2935
|
-
owner: op-retry
|
|
2936
|
-
kind: check-finding
|
|
2937
2854
|
|
|
2938
2855
|
FE_SINGLE_COMMIT_PER_RENDER:
|
|
2939
2856
|
title: "A component renders more than one commit"
|
|
@@ -3077,15 +2994,6 @@ FE_SUBSCRIPTION_NO_SNAPSHOT:
|
|
|
3077
2994
|
owner: op-retry
|
|
3078
2995
|
kind: check-finding
|
|
3079
2996
|
|
|
3080
|
-
FE_SUPABASE_CLIENT_OWNER:
|
|
3081
|
-
title: "A front-end Supabase import or call is outside `fe.modules.auth`, or reaches past the auth API"
|
|
3082
|
-
title_vi: "Supabase phía front end ngoài modules/auth hoặc dùng ngoài API auth"
|
|
3083
|
-
meaning_vi: "Import/call Supabase ở `<path>` không thuộc ô `fe.modules.auth`, hoặc gọi API ngoài auth (`.from`, `.rpc`, `.storage`, `.channel`): front end không đọc hay ghi dữ liệu qua Supabase."
|
|
3084
|
-
causes_vi:
|
|
3085
|
-
- "Vi phạm luật R217: Supabase trên front end chỉ là xác thực (đăng nhập, phiên, access token) trong `modules/auth`; dữ liệu đi qua `modules/api` (Apollo GraphQL) và hook SWR tới back end (R212 case-2)."
|
|
3086
|
-
nextStep_vi: "Chuyển client Supabase vào `modules/auth` và chỉ giữ API auth; đọc/ghi dữ liệu bằng query/mutation GraphQL trong `modules/api` và hook `hooks/swr`; không thêm ngoại lệ."
|
|
3087
|
-
owner: op-retry
|
|
3088
|
-
kind: check-finding
|
|
3089
2997
|
|
|
3090
2998
|
FE_SUSPENSE_NULL_FALLBACK:
|
|
3091
2999
|
title: "Suspense fallback renders something"
|
|
@@ -3689,15 +3597,6 @@ HFS_EDITION_INVALID:
|
|
|
3689
3597
|
owner: op-retry
|
|
3690
3598
|
kind: check-finding
|
|
3691
3599
|
|
|
3692
|
-
HFS_EMIT_DB_TYPES_STALE:
|
|
3693
|
-
title: "Database types require current migration history"
|
|
3694
|
-
title_vi: "Lịch sử migration chưa khớp để sinh kiểu"
|
|
3695
|
-
meaning_vi: "Tập phiên bản đã áp dụng trong DB cục bộ khác dấu thời gian tệp supabase/migrations."
|
|
3696
|
-
causes_vi:
|
|
3697
|
-
- "Tập phiên bản đã áp dụng trong DB cục bộ khác dấu thời gian tệp supabase/migrations."
|
|
3698
|
-
nextStep_vi: "Chạy npm run db:reset rồi starci app emit; kiểm tra cả phiên bản thiếu và phiên bản thừa."
|
|
3699
|
-
owner: op-retry
|
|
3700
|
-
kind: verb-refusal
|
|
3701
3600
|
|
|
3702
3601
|
HFS_EMPTY_DIR:
|
|
3703
3602
|
title: "No empty directory outside an `ignored` slot"
|
|
@@ -4302,15 +4201,6 @@ HFS_SERVICE_PLACEMENT:
|
|
|
4302
4201
|
owner: op-retry
|
|
4303
4202
|
kind: check-finding
|
|
4304
4203
|
|
|
4305
|
-
HFS_SERVICE_ROLE_SCOPE:
|
|
4306
|
-
title: "Service-role access belongs to the Supabase integration"
|
|
4307
|
-
title_vi: "Service role được dùng ngoài integration Supabase"
|
|
4308
|
-
meaning_vi: "Mã tại <path> tham chiếu service_role ngoài integration Supabase của back end, kể cả trong script triển khai."
|
|
4309
|
-
causes_vi:
|
|
4310
|
-
- "Vi phạm R252: sử dụng quyền service role ngoài thành phần sở hữu kết nối Supabase."
|
|
4311
|
-
nextStep_vi: "Đưa quyền service role vào be/src/modules/integrations/supabase và truy cập qua port của integration; script gọi CLI của ứng dụng."
|
|
4312
|
-
owner: op-retry
|
|
4313
|
-
kind: check-finding
|
|
4314
4204
|
|
|
4315
4205
|
HFS_SERVICE_STACK_DECLARATION:
|
|
4316
4206
|
title: "Each service app is a service component of the stack"
|
|
@@ -5650,3 +5540,63 @@ BE_ENTITY_SCHEMA_DRIFT:
|
|
|
5650
5540
|
nextStep_vi: "Viết migration mới trong db/migrations để schema có đúng cột đó, hoặc sửa entity cho khớp schema hiện tại, rồi chạy lại starci app db drift cho đến khi xanh."
|
|
5651
5541
|
owner: op-retry
|
|
5652
5542
|
kind: check-finding
|
|
5543
|
+
|
|
5544
|
+
BE_AUTH_PASSPORT_OWNER:
|
|
5545
|
+
title: "An identity library is imported outside integrations/passport"
|
|
5546
|
+
title_vi: "Thư viện định danh bị import ngoài integrations/passport"
|
|
5547
|
+
meaning_vi: "File `<path>` import @nestjs/passport, passport, passport-google-oauth20, passport-local, @nestjs/jwt, jose hoặc bcryptjs mà không thuộc `be.integrations.passport`, hoặc import bcrypt hay client Supabase."
|
|
5548
|
+
causes_vi:
|
|
5549
|
+
- "Vi phạm R328: chỉ integration passport sở hữu chiến lược Google, đăng nhập email/mật khẩu và token riêng của app."
|
|
5550
|
+
nextStep_vi: "Chuyển mã vào `src/modules/integrations/passport/` và gọi qua API công khai của integration; dùng bcryptjs thay bcrypt, bỏ client Supabase."
|
|
5551
|
+
owner: op-retry
|
|
5552
|
+
kind: check-finding
|
|
5553
|
+
|
|
5554
|
+
BE_AUTH_JWT_OWN_JWKS:
|
|
5555
|
+
title: "A lite principal is not built from the app own verified access token"
|
|
5556
|
+
title_vi: "Principal của app lite không đến từ access token riêng đã xác minh"
|
|
5557
|
+
meaning_vi: "`<path>` gọi jwtVerify ngoài integrations/passport, xác minh với createRemoteJWKSet thay vì key set riêng (createLocalJWKSet), thiếu issuer/audience/algorithms, decode token không xác minh hoặc đọc role/membership từ request body."
|
|
5558
|
+
causes_vi:
|
|
5559
|
+
- "Vi phạm R329: principal chỉ đến từ token do chính app ký và xác minh bằng JWKS riêng."
|
|
5560
|
+
nextStep_vi: "Dùng `createTokenKit` của integrations/passport (jwtVerify trên createLocalJWKSet, pin issuer/audience/algorithms) và để domain/identity dựng principal từ kết quả đó."
|
|
5561
|
+
owner: op-retry
|
|
5562
|
+
kind: check-finding
|
|
5563
|
+
|
|
5564
|
+
BE_AUTH_REFRESH_COOKIE:
|
|
5565
|
+
title: "A refresh token leaves the httpOnly cookie"
|
|
5566
|
+
title_vi: "Refresh token rời khỏi cookie httpOnly"
|
|
5567
|
+
meaning_vi: "`<path>` đặt cookie refresh thiếu httpOnly, secure, sameSite strict hoặc path của cửa refresh, hoặc đưa refresh token vào body của request/response."
|
|
5568
|
+
causes_vi:
|
|
5569
|
+
- "Vi phạm R330: refresh token chỉ sống trong cookie httpOnly Secure SameSite=Strict và được xoay vòng mỗi lần dùng."
|
|
5570
|
+
nextStep_vi: "Đặt và xoá cookie qua `setRefreshCookie`/`clearRefreshCookie` của integrations/passport; bỏ trường refreshToken khỏi DTO và kết quả trả về."
|
|
5571
|
+
owner: op-retry
|
|
5572
|
+
kind: check-finding
|
|
5573
|
+
|
|
5574
|
+
BE_STORAGE_OWNER:
|
|
5575
|
+
title: "An S3 SDK is imported outside integrations/object-storage"
|
|
5576
|
+
title_vi: "S3 SDK bị import ngoài integrations/object-storage"
|
|
5577
|
+
meaning_vi: "File `<path>` import @aws-sdk/client-s3 hoặc @aws-sdk/s3-request-presigner ngoài `be.integrations.object-storage`, hoặc import minio."
|
|
5578
|
+
causes_vi:
|
|
5579
|
+
- "Vi phạm R331: chỉ integration storage sở hữu client S3 (MinIO trong lite) và URL presigned."
|
|
5580
|
+
nextStep_vi: "Gọi port ObjectStorage của integrations/object-storage (presignPut, presignGet, remove); bỏ minio."
|
|
5581
|
+
owner: op-retry
|
|
5582
|
+
kind: check-finding
|
|
5583
|
+
|
|
5584
|
+
LITE_DB_MIGRATION:
|
|
5585
|
+
title: "A lite schema change lives outside db/migrations"
|
|
5586
|
+
title_vi: "Thay đổi schema của app lite nằm ngoài db/migrations"
|
|
5587
|
+
meaning_vi: "`<path>` là tệp của thư mục supabase/ hoặc một tệp SQL ngoài db/ (trừ seed của .starcistacks)."
|
|
5588
|
+
causes_vi:
|
|
5589
|
+
- "Vi phạm R332: schema của app lite chỉ nằm trong db/migrations/<ts14>_<kebab>.sql, archive và seed trong db/."
|
|
5590
|
+
nextStep_vi: "Chuyển migration sang db/migrations (giữ nguyên stamp), seed sang db/seed.sql, types sang db/types/; xoá supabase/."
|
|
5591
|
+
owner: op-retry
|
|
5592
|
+
kind: check-finding
|
|
5593
|
+
|
|
5594
|
+
BE_PERSISTENCE_NO_RAW_SQL:
|
|
5595
|
+
title: "Lite domain code runs raw SQL instead of TypeORM entities"
|
|
5596
|
+
title_vi: "Mã domain của app lite chạy SQL thô thay vì entity TypeORM"
|
|
5597
|
+
meaning_vi: "File `<path>` gọi `.query(` hoặc dùng template `sql` ngoài platform/database và ngoài danh sách hàm ruleParams.be.lite.rawSqlFunctions."
|
|
5598
|
+
causes_vi:
|
|
5599
|
+
- "Vi phạm R334: dữ liệu của app lite đi qua entity TypeORM trên EntityManager (find, save, update, upsert, createQueryBuilder)."
|
|
5600
|
+
nextStep_vi: "Khai entity ở persistence/entities/<table>.entity.ts và đọc/ghi bằng EntityManager hoặc createQueryBuilder với tham số có tên; hàm pg/SECURITY DEFINER không diễn đạt được thì khai vào rawSqlFunctions kèm lý do."
|
|
5601
|
+
owner: op-retry
|
|
5602
|
+
kind: check-finding
|
|
@@ -85,16 +85,16 @@ properties:
|
|
|
85
85
|
command: {type: string, minLength: 1}
|
|
86
86
|
backing:
|
|
87
87
|
description: >-
|
|
88
|
-
|
|
89
|
-
service in production. Every other dependency is a Docker component
|
|
88
|
+
The self-hosted database of a lite app: its db component (image supabase/postgres), a Docker container in
|
|
89
|
+
development and a Swarm service in production. Every other dependency is a Docker component too.
|
|
90
90
|
type: object
|
|
91
91
|
additionalProperties:
|
|
92
92
|
type: object
|
|
93
93
|
additionalProperties: false
|
|
94
|
-
required: [provider,
|
|
94
|
+
required: [provider, component]
|
|
95
95
|
properties:
|
|
96
|
-
provider: {const:
|
|
97
|
-
|
|
96
|
+
provider: {const: postgres}
|
|
97
|
+
component: {type: string, minLength: 1}
|
|
98
98
|
environments:
|
|
99
99
|
type: object
|
|
100
100
|
minProperties: 1
|
|
@@ -154,7 +154,7 @@ function readCall(env, call, managers) {
|
|
|
154
154
|
function ownedRead(env, statement) {
|
|
155
155
|
const { ts } = env;
|
|
156
156
|
const call = statementCall(ts, statement);
|
|
157
|
-
if (!call || !ts.isCallExpression(call) || !imported(env, call.expression, '
|
|
157
|
+
if (!call || !ts.isCallExpression(call) || !imported(env, call.expression, 'requireOwnedEntity', '@modules/platform/database') || call.arguments.length !== 5) return false;
|
|
158
158
|
if (!call.arguments.slice(0, 4).every(argument => mapping(env, argument))) return false;
|
|
159
159
|
const callback = unwrap(ts, call.arguments[4]);
|
|
160
160
|
if (!ts.isArrowFunction(callback) || callback.parameters.length || !ts.isBlock(callback.body) || callback.body.statements.length !== 1) return false;
|