@starci/hfs 4.10.1 → 4.11.8
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 +76 -0
- package/README.md +40 -1
- package/db/drift.mjs +19 -16
- package/db/image.mjs +13 -0
- package/db/migrate.mjs +3 -3
- package/db/postgres.mjs +1 -1
- package/package.json +2 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +23 -8
- package/runtime/knowledge/hfs/rules.yaml +54 -13
- package/runtime/knowledge/hfs/slots.yaml +67 -36
- package/runtime/knowledge/patterns/be/event-bus.yaml +12 -10
- package/runtime/knowledge/patterns/be/idempotency.yaml +1 -1
- package/runtime/knowledge/patterns/be/jobs.yaml +3 -0
- package/runtime/knowledge/patterns/be/queues.yaml +14 -14
- package/runtime/knowledge/patterns/be/reactors.yaml +1 -1
- package/runtime/knowledge/patterns/be/realtime.yaml +1 -1
- package/runtime/modules/kernel/failure-codes.yaml +28 -18
- package/runtime/modules/schemas/application-stacks.schema.yaml +4 -2
- package/runtime/scripts/gates/starcistacks.mjs +6 -2
- package/runtime/scripts/guards/push-scan.mjs +1 -1
- package/runtime/scripts/hfs/ci-declaration.mjs +3 -2
- package/runtime/scripts/hfs/db-baseline-verb.mjs +94 -22
- package/runtime/scripts/hfs/db-errors.mjs +2 -0
- package/runtime/scripts/hfs/db-plan-verb.mjs +17 -5
- package/runtime/scripts/hfs/db-squash.mjs +514 -0
- package/runtime/scripts/hfs/declaration-shape.mjs +3 -15
- package/runtime/scripts/hfs/example-secrets.mjs +4 -3
- package/runtime/scripts/hfs/rule-params-shape.mjs +11 -9
- package/runtime/scripts/hfs/rules/a15-findings.mjs +1 -1
- package/runtime/scripts/hfs/rules/database-config.mjs +1 -1
- package/runtime/scripts/hfs/rules/database-constants.mjs +16 -1
- package/runtime/scripts/hfs/rules/database-queue-sources.mjs +2 -110
- package/runtime/scripts/hfs/rules/database-queues.mjs +53 -239
- package/runtime/scripts/hfs/rules/database-sql.mjs +2 -0
- package/runtime/scripts/hfs/rules/database-transaction.mjs +45 -0
- package/runtime/scripts/hfs/rules/deploy-scan.mjs +1 -1
- package/runtime/scripts/hfs/rules/edition.mjs +1 -1
- package/runtime/scripts/hfs/rules/env-parity.mjs +1 -1
- package/runtime/scripts/hfs/rules/lite-secrets.mjs +3 -3
- package/runtime/scripts/hfs/rules/script-hygiene.mjs +1 -1
- package/runtime/scripts/hfs/rules/secret-custody-only.mjs +2 -2
- package/runtime/scripts/hfs/rules/secrets.mjs +1 -1
- package/runtime/scripts/hfs/rules/stack-tooling.mjs +1 -1
- package/runtime/scripts/hfs/rules/stacks-deploy.mjs +2 -1
- package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +3 -4
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +2 -2
- package/runtime/scripts/hfs/rules/stacks-swarm.mjs +1 -1
- package/runtime/scripts/hfs/rules/stacks.mjs +3 -3
- package/runtime/scripts/hfs/sealed-members.mjs +10 -6
- package/runtime/scripts/hfs/secret-files.mjs +16 -0
- package/runtime/scripts/hfs/secret-init.mjs +120 -21
- package/runtime/scripts/hfs/secret-output.mjs +2 -1
- package/runtime/scripts/hfs/secret-prompt.mjs +128 -0
- package/runtime/scripts/hfs/secret-sops.mjs +93 -9
- package/runtime/scripts/hfs/secret-standard.mjs +315 -0
- package/runtime/scripts/hfs/secret-store.mjs +23 -9
- package/runtime/scripts/hfs/secret.mjs +110 -46
- package/runtime/scripts/hfs/slots.mjs +8 -1
- package/runtime/scripts/hfs/stack-deploy-config.mjs +20 -6
- package/runtime/scripts/hfs/stacks-gate.mjs +9 -2
- package/runtime/scripts/hfs/starcistacks-runtime.mjs +1 -1
- package/runtime/scripts/lib/sealed-member.mjs +36 -0
- package/runtime/scripts/lib/secret-patterns.mjs +1 -1
- package/runtime/scripts/lib/swarm-stack.mjs +4 -6
- package/scaffold/add-inbox-lite.mjs +2 -2
- package/scaffold/add-queue-lite.mjs +24 -5
- package/scaffold/app.mjs +2 -2
- package/src/main.mjs +5 -4
- package/stack/build.mjs +3 -1
- package/stack/config.mjs +2 -1
- package/stack/docker-transport.mjs +2 -1
- package/stack/remote.mjs +4 -3
- package/stack/sealed.mjs +9 -8
- package/stack/target-binding.mjs +2 -1
- package/sync/hygiene.mjs +1 -1
- package/sync/index.mjs +47 -12
- package/templates/app/db-bootstrap/db/bootstrap.sql +9 -14
- package/templates/app/db-image/starcistacks/images/db/Dockerfile +18 -0
- package/templates/app/gitignore +2 -1
- package/templates/app/gitignore-lite +3 -1
- package/templates/app/package-scripts/db.json +2 -1
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +4 -2
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +1 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +1 -1
- package/templates/app/skeleton-lite/.starcistacks/production/README.md +1 -1
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +1 -1
- package/templates/app/skeleton-lite/README.md +2 -2
- package/templates/be/inbox/delivery-recorded.event.ts.tpl +7 -5
- package/templates/be/inbox/events.json.tpl +1 -1
- package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +58 -11
- package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +13 -1
- package/templates/be/inbox/platform/event-bus/event-bus.log-events.ts.tpl +9 -0
- package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +1 -1
- package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +11 -5
- package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +9 -2
- package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +18 -5
- package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +37 -16
- package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +33 -5
- package/templates/be/inbox/platform/event-bus/event-consumer.service.spec.ts.tpl +143 -0
- package/templates/be/inbox/platform/event-bus/event-consumer.service.ts.tpl +119 -0
- package/templates/be/inbox/platform/event-bus/index.ts.tpl +13 -4
- package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +4 -4
- package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +4 -4
- package/templates/be/inbox/platform/migration.sql.tpl +97 -34
- package/templates/be/inbox/service.spec.ts.tpl +1 -1
- package/templates/be/inbox/webhook-door.ts.tpl +2 -2
- package/templates/be/patterns/jobs/platform/jobs.log-events.ts.tpl +2 -0
- package/templates/be/patterns/jobs/platform-lite/job-claim.service.spec.ts.tpl +4 -4
- package/templates/be/patterns/jobs/platform-lite/job-claim.service.ts.tpl +5 -5
- package/templates/be/patterns/jobs/platform-lite/job-tick.service.spec.ts.tpl +65 -0
- package/templates/be/patterns/jobs/platform-lite/job-tick.service.ts.tpl +78 -0
- package/templates/be/patterns/jobs/platform-lite/job-ticks.contracts.ts.tpl +16 -0
- package/templates/be/patterns/jobs/platform-lite/jobs.module.ts.tpl +40 -0
- package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +44 -0
- package/templates/be/patterns/queues/platform-lite/queue-outbox.service.spec.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform-lite/queue-outbox.service.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +2 -2
- package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +2 -2
- package/templates/be/skeleton/apps/__app__/src/__app__.env.ts +4 -0
- package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +3 -0
- package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.spec.ts +37 -2
- package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.ts +36 -3
- package/templates/be/skeleton/src/modules/platform/http/http.config.ts +11 -0
- package/templates/be/skeleton/src/modules/platform/http/http.decorators.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +5 -3
- package/templates/be/skeleton/src/modules/platform/http/http.options.ts +9 -2
- package/templates/be/skeleton/src/modules/platform/http/index.ts +2 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.client.spec.ts +96 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.client.ts +93 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -1
- package/runtime/scripts/hfs/rules/database-queue-outbox.mjs +0 -102
- package/templates/be/skeleton-lite/src/modules/platform/database/database.functions.ts +0 -24
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# catalog READ this file through scripts/hfs/slots.mjs; none of them hardcodes a path.
|
|
8
8
|
# Shape: modules/schemas/hfs-slots.schema.yaml. The repository side is hfs.json: modules/schemas/hfs-repo.schema.yaml.
|
|
9
9
|
schema: starci/hfs-slots@3
|
|
10
|
-
version: 3.
|
|
10
|
+
version: 3.13.0
|
|
11
11
|
|
|
12
12
|
versioning:
|
|
13
13
|
# MAJOR.MINOR.PATCH of this manifest. A repository pins only the MAJOR in hfs.json ("hfs": 3).
|
|
@@ -52,6 +52,12 @@ versioning:
|
|
|
52
52
|
# slot field rewrittenBy, so the release gate's clean-tree check passes over an untracked file a declared dev server writes, and over nothing else.
|
|
53
53
|
# 3.10.0 is a minor (2026-10-29): it ADDS the lite be.platform.database.entities slot, the entity registry (slot fields role and importedBy), so the
|
|
54
54
|
# tier matrix and the public-entry rules admit exactly its <c>Entities imports and its deep importers (persistence.md BE-PERSISTENCE-3).
|
|
55
|
+
# 3.11.0 (ruling:lite-official-postgres-image, 2026-10-29) is, like 3.9.0, a change for lite only: it ADDS the managed app.db.image slot
|
|
56
|
+
# (db/Dockerfile, written by `starci app sync --write`), narrows ruleParams.db.extensions to the extensions that image carries, and gives
|
|
57
|
+
# @nestjs/schedule and cron one owner, platform/jobs (R335).
|
|
58
|
+
# 3.13.0 (rulings lite-pgmq-bus-bullmq-jobs and lite-db-image-in-stacks, 2026-10-29) is a change for lite only: ruleParams.db.extensions
|
|
59
|
+
# admits pgmq, the event bus, ruleParams.db.queueOutbox is retired with the pg_cron half of R258, the lite event-bus functions replace
|
|
60
|
+
# event_bus_write in ruleParams.be.dbFunctions, and app.db.image moves to .starcistacks/images/db/Dockerfile (db/ keeps schema and data).
|
|
55
61
|
|
|
56
62
|
# Field meaning, per slot.
|
|
57
63
|
# id stable name, never changes meaning inside a major.
|
|
@@ -126,9 +132,9 @@ testValues: [unit-beside, e2e, none]
|
|
|
126
132
|
# event bus, saga, reactors, projection, realtime, webhooks, fenced job), declared in hfs.json the same way, and the same unit
|
|
127
133
|
# specs (unitRoles, logicRoles, 100 per file, @starci/jest-preset doubles, builders). What differs is ADAPTERS and DELIVERY
|
|
128
134
|
# (ruling:R212/case-10): schema authority in db/migrations, the queue port on BullMQ over the Redis hfs.json
|
|
129
|
-
# sides.be.backing declares (as in full, fed by the Postgres outbox), event-bus ports on pgmq
|
|
130
|
-
#
|
|
131
|
-
# outbox, storage on S3-compatible MinIO (integrations/object-storage) and auth through integrations/passport. Realtime uses SSE or the socket.io gateway
|
|
135
|
+
# sides.be.backing declares (as in full, fed by the Postgres outbox), event-bus ports on pgmq topics sent in the publisher's
|
|
136
|
+
# transaction (ruling:lite-pgmq-bus-bullmq-jobs), cache and locks on Postgres or the declared Redis, schedules as platform/jobs ticks enqueueing through the queue
|
|
137
|
+
# outbox (R335), storage on S3-compatible MinIO (integrations/object-storage) and auth through integrations/passport. Realtime uses SSE or the socket.io gateway
|
|
132
138
|
# in the api process, which scales horizontally and stateless (memory hub for one replica or tests; the Redis hub and @socket.io/redis-adapter when Redis is declared, else the Postgres hub and @socket.io/postgres-adapter, for N replicas; websocket only, no sticky sessions). Integration and provider sandbox contract specs are optional;
|
|
133
139
|
# declared patterns owe their integration proofs (R284). Lite has no e2e world. Production uses declared docker-swarm under
|
|
134
140
|
# .starcistacks/production/infra/swarm/ (owner ruling 2026-10-14); lite runs its PostgreSQL as the db service of its
|
|
@@ -205,11 +211,10 @@ crossApp: apps never import each other; shared code is a packages/<pkg> slot.
|
|
|
205
211
|
ruleParams:
|
|
206
212
|
db:
|
|
207
213
|
# R259: btree_gist supplies UUID equality for the booking interval exclusion constraint.
|
|
208
|
-
# R259:
|
|
209
|
-
|
|
210
|
-
#
|
|
211
|
-
|
|
212
|
-
queueOutbox: {table: public.queue, columns: [queue, payload, created_at, correlation_id]}
|
|
214
|
+
# R259: the extensions the db image carries (ruling:lite-official-postgres-image): the official postgres image's contrib set and pgvector
|
|
215
|
+
# from PGDG; unaccent serves Vietnamese-content search functions; pgmq is the event bus (ruling:lite-pgmq-bus-bullmq-jobs), installed
|
|
216
|
+
# from its pinned release. No pg_cron, pg_net or supabase_vault.
|
|
217
|
+
extensions: [pgcrypto, pg_trgm, vector, uuid-ossp, unaccent, btree_gist, pgmq]
|
|
213
218
|
common:
|
|
214
219
|
# HFS_SIZE_GROWTH: a file above soft may not grow against its parent commit; a new file stays within soft. Slot
|
|
215
220
|
# budgets (component.tsx 300, hooks 200, modules 400) are stricter than this cap where they apply.
|
|
@@ -255,7 +260,8 @@ ruleParams:
|
|
|
255
260
|
bull: [platform/queue]
|
|
256
261
|
"@nestjs/bull": [platform/queue]
|
|
257
262
|
kafkajs: [platform/event-bus] # pattern event-bus: Kafka is the only event transport
|
|
258
|
-
"@nestjs/schedule": []
|
|
263
|
+
"@nestjs/schedule": [platform/jobs] # R335: only the tick scheduler of platform/jobs (jobs.scheduler.ts); elsewhere a schedule is a BullMQ job scheduler of a queue
|
|
264
|
+
cron: [platform/jobs] # R335: CronJob of the tick scheduler
|
|
259
265
|
eventemitter2: []
|
|
260
266
|
setInterval: [platform/retry, platform/event-bus, platform/queue, platform/http, platform/realtime]
|
|
261
267
|
setTimeout: [platform/retry, platform/event-bus, platform/queue, platform/http]
|
|
@@ -424,28 +430,36 @@ ruleParams:
|
|
|
424
430
|
# logic roles and spec doubles are full's (owner ruling 2026-10-12 (owner:lite-production-swarm), with the Work boundary amended by ruling:R212/case-4). infraOwners: full's owners,
|
|
425
431
|
# bullmq in platform/queue and ioredis/redis in integrations/cache and integrations/redis (ruling:R212/case-10: the queue is
|
|
426
432
|
# Redis in both editions; a backing library is legal only when hfs.json sides.be.backing declares its service, which
|
|
427
|
-
# HFS_BACKING_UNDECLARED checks), except Kafka, which is owned nowhere (platform/event-bus runs pgmq
|
|
433
|
+
# HFS_BACKING_UNDECLARED checks), except Kafka, which is owned nowhere (platform/event-bus runs pgmq topics through
|
|
428
434
|
# the shared EntityManager); realtime is the same socket.io gateway as full, run inside the api process, scaled horizontally and stateless (hub redis when
|
|
429
435
|
# Redis is declared, else hub postgres with @socket.io/postgres-adapter; no sticky sessions).
|
|
430
436
|
# R334: the database functions code may call through platform/database's callDbFunction, advisoryXactLock and notify helpers,
|
|
431
|
-
# each {name, reason};
|
|
437
|
+
# each {name, reason}; a pg built-in is listed bare (called in pg_catalog), every other function schema-qualified (public.job_claim),
|
|
438
|
+
# and an app adds its own, schema-qualified, in hfs.json sides.be.dbFunctions.
|
|
432
439
|
dbFunctions:
|
|
433
440
|
- {name: pg_advisory_xact_lock, reason: "advisoryXactLock: a transaction-scoped lock TypeORM has no API for"}
|
|
434
441
|
- {name: hashtextextended, reason: "advisoryXactLock: the 64-bit key of a text lock name"}
|
|
435
442
|
- {name: pg_notify, reason: "notify: LISTEN/NOTIFY of the Postgres realtime hub"}
|
|
436
443
|
- {name: to_regproc, reason: "callDbFunction: whether an optional database function exists"}
|
|
437
|
-
- {name:
|
|
438
|
-
- {name:
|
|
439
|
-
- {name:
|
|
440
|
-
- {name:
|
|
441
|
-
- {name:
|
|
442
|
-
- {name:
|
|
443
|
-
- {name:
|
|
444
|
-
- {name:
|
|
445
|
-
- {name:
|
|
446
|
-
- {name:
|
|
447
|
-
- {name:
|
|
448
|
-
- {name:
|
|
444
|
+
- {name: gen_random_uuid, reason: "callDbFunction: a random uuid from the database (PG13+ core, pg_catalog) where a token is minted in the transaction"}
|
|
445
|
+
- {name: public.job_claim, reason: "platform/jobs (add job, lite): the fenced claim upsert of jobs.sql"}
|
|
446
|
+
- {name: public.job_advance, reason: "platform/jobs (add job, lite): the token-guarded step write of jobs.sql"}
|
|
447
|
+
- {name: public.job_complete, reason: "platform/jobs (add job, lite): the token-guarded completion of jobs.sql"}
|
|
448
|
+
- {name: public.job_fail, reason: "platform/jobs (add job, lite): the token-guarded failure of jobs.sql"}
|
|
449
|
+
- {name: public.job_tick_claim, reason: "platform/jobs (add job, lite): the once-per-minute claim of a scheduler tick, jobs.sql (R335)"}
|
|
450
|
+
- {name: public.queue_write, reason: "platform/queue (add queue, lite): the outbox insert of queue-outbox.sql"}
|
|
451
|
+
- {name: public.queue_waiting, reason: "platform/queue (add queue, lite): the relay read FOR UPDATE SKIP LOCKED of queue-outbox.sql"}
|
|
452
|
+
- {name: public.queue_mark_sent, reason: "platform/queue (add queue, lite): the relay mark of queue-outbox.sql"}
|
|
453
|
+
- {name: public.inbox_claim_event, reason: "platform/inbox (add webhook, lite): the conflict-safe delivery claim"}
|
|
454
|
+
- {name: public.inbox_claim_client, reason: "platform/inbox (add webhook, lite): the client claim upsert returning its committed outcome"}
|
|
455
|
+
- {name: public.inbox_save_client_outcome, reason: "platform/inbox (add webhook, lite): the saved client outcome"}
|
|
456
|
+
- {name: public.inbox_sweep_clients, reason: "platform/inbox (add webhook, lite): the bounded retention sweep FOR UPDATE SKIP LOCKED"}
|
|
457
|
+
- {name: public.event_bus_send, reason: "platform/event-bus (add webhook, lite): the send of a typed envelope to its pgmq topic in the publisher's transaction"}
|
|
458
|
+
- {name: public.event_bus_read, reason: "platform/event-bus (add webhook, lite): a consumer's pgmq read with a visibility timeout"}
|
|
459
|
+
- {name: public.event_bus_archive, reason: "platform/event-bus (add webhook, lite): the archive of a handled message"}
|
|
460
|
+
- {name: public.event_bus_bury, reason: "platform/event-bus (add webhook, lite): the move of an exhausted or malformed message to its consumer's dead-letter queue"}
|
|
461
|
+
- {name: public.event_bus_dead_letters, reason: "platform/event-bus (add webhook, lite): the operator read of a consumer's dead-letter queue"}
|
|
462
|
+
- {name: public.event_bus_requeue, reason: "platform/event-bus (add webhook, lite): the operator requeue of one dead letter"}
|
|
449
463
|
|
|
450
464
|
lite:
|
|
451
465
|
schemaAuthority: sql
|
|
@@ -861,10 +875,10 @@ slots:
|
|
|
861
875
|
"<env>/runtime/{config,env}/**", "<env>/runtime/env/KEYS.md", "<env>/secrets/<slug>.enc", "<env>/secrets/.gitkeep", "<env>/seeds/**"]
|
|
862
876
|
forbids: ["<env>/runtime/files/**", "**/*.enc outside <env>/secrets/", DESIGN.md, deployment.json, "k8s/ at root"]
|
|
863
877
|
# Owner ruling 2026-10-14: lite declares docker-swarm production, app runtime config, sealed custody and seed data.
|
|
864
|
-
# The db service supplies the database
|
|
878
|
+
# The db service supplies the database; schedules are platform/jobs ticks (R335); production has no Compose or host cron.
|
|
865
879
|
lite:
|
|
866
880
|
requires: [application-stacks.yaml]
|
|
867
|
-
allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/swarm/**", "<env>/infra/metadata.json",
|
|
881
|
+
allows: [application-stacks.yaml, images/db/Dockerfile, "<env>/README.md", "<env>/environment.json", "<env>/infra/swarm/**", "<env>/infra/metadata.json",
|
|
868
882
|
"<env>/runtime/{config,env}/**", "<env>/runtime/env/KEYS.md", "<env>/secrets/<slug>.enc", "<env>/secrets/.gitkeep", "<env>/seeds/**"]
|
|
869
883
|
forbids: ["<env>/runtime/files/**", "**/*.enc outside <env>/secrets/", DESIGN.md, deployment.json, "k8s/ at root"]
|
|
870
884
|
rules: [HFS_STACKS_SHAPE, HFS_STACKS_IN_SIDE, HFS_PLAINTEXT_SECRET, HFS_IDENTITY_CUSTODY]
|
|
@@ -925,7 +939,7 @@ slots:
|
|
|
925
939
|
rules: [HFS_PLAINTEXT_SECRET]
|
|
926
940
|
|
|
927
941
|
# ----- app root: the database of a lite app (ruling:lite-self-hosted-postgres-passport-minio): a self-hosted PostgreSQL in the app's
|
|
928
|
-
# stack (the `db` service,
|
|
942
|
+
# stack (the `db` service, built from the managed .starcistacks/images/db/Dockerfile: the official postgres image plus pgvector and pgmq, ruling:lite-db-image-in-stacks), migrated by
|
|
929
943
|
# `starci app db migrate --url-env <NAME>`. A full app keeps its TypeORM migrations under be/ and has no such folder.
|
|
930
944
|
- id: app.db
|
|
931
945
|
profiles: [app]
|
|
@@ -975,12 +989,29 @@ slots:
|
|
|
975
989
|
tier: none
|
|
976
990
|
tests: none
|
|
977
991
|
since: 3.9.0
|
|
992
|
+
- id: app.db.image
|
|
993
|
+
profiles: [app]
|
|
994
|
+
editions: [lite]
|
|
995
|
+
# The managed image of the db service (templates/app/db-image, rulings lite-official-postgres-image and lite-db-image-in-stacks): FROM
|
|
996
|
+
# the digest-pinned official postgres image plus postgresql-17-pgvector from PGDG and the sha256-verified pgmq release. It lives with
|
|
997
|
+
# the stacks; the stack's db service, the local db container, the scratch database of `starci app db drift` and the squash replay
|
|
998
|
+
# all build it.
|
|
999
|
+
path: ".starcistacks/images/db/Dockerfile"
|
|
1000
|
+
presence: required
|
|
1001
|
+
tracked: tracked
|
|
1002
|
+
managedBy: db-image
|
|
1003
|
+
tier: none
|
|
1004
|
+
tests: none
|
|
1005
|
+
since: 3.11.0
|
|
1006
|
+
rules: [HFS_MANAGED_FILE_DRIFT]
|
|
978
1007
|
- id: app.db.bootstrap
|
|
979
1008
|
profiles: [app]
|
|
980
1009
|
editions: [lite]
|
|
981
|
-
# The managed pre-migration bootstrap (templates/app/db-bootstrap): the
|
|
982
|
-
# GoTrue column set, auth.uid/role/jwt stubs
|
|
983
|
-
# hosted Supabase apply to the
|
|
1010
|
+
# The managed pre-migration bootstrap (templates/app/db-bootstrap): the extensions schema every allowed extension is created in, the
|
|
1011
|
+
# auth and storage schemas, auth.users with the GoTrue column set, auth.uid/role/jwt stubs and the NOLOGIN browser roles, so migrations
|
|
1012
|
+
# written for a hosted Supabase apply to the db service's official postgres image (nothing else of Supabase: no supabase_admin or
|
|
1013
|
+
# realtime publication). `starci app db migrate` applies it before the first migration of an empty database; sync renders all of it
|
|
1014
|
+
# but the one marked section `starci app db baseline --squash` writes (the source's app roles and the grants a per-database dump loses).
|
|
984
1015
|
path: "db/bootstrap.sql"
|
|
985
1016
|
presence: required
|
|
986
1017
|
tracked: tracked
|
|
@@ -1765,7 +1796,7 @@ slots:
|
|
|
1765
1796
|
- id: be.platform.event-bus
|
|
1766
1797
|
since: 3.1.0
|
|
1767
1798
|
profiles: [be]
|
|
1768
|
-
editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite implements the event ports
|
|
1799
|
+
editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite implements the event ports on pgmq (ruling:lite-pgmq-bus-bullmq-jobs); full uses its Kafka adapter."
|
|
1769
1800
|
path: "src/modules/platform/event-bus/"
|
|
1770
1801
|
presence: opt-in
|
|
1771
1802
|
pattern: event-bus # the ONE durable event transport: transactional outbox, relay, Kafka, inbox, retry and dead letter (knowledge/patterns/be/event-bus.yaml)
|
|
@@ -1774,10 +1805,10 @@ slots:
|
|
|
1774
1805
|
owner: true
|
|
1775
1806
|
lite:
|
|
1776
1807
|
why: >-
|
|
1777
|
-
the
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1808
|
+
the EventBus and EventConsumer ports on pgmq (ruling:lite-pgmq-bus-bullmq-jobs): publish sends the envelope to its
|
|
1809
|
+
topic in the publisher's transaction, so no outbox or relay runs; each consumer group reads its own queue bound to the
|
|
1810
|
+
event's name with a visibility timeout, archives a handled message, lets a failed one reappear and buries one past its
|
|
1811
|
+
maxDeliveries in its __dead queue (BE-EVENT-BUS-6). Jobs stay on BullMQ (platform/queue). No Kafka (knowledge/patterns/be/event-bus.yaml liteAdapter)
|
|
1781
1812
|
requires: [index.ts, event-bus.module.ts, event-bus.port.ts, event-bus.contracts.ts, event-bus.service.ts, event-bus.service.spec.ts]
|
|
1782
1813
|
tests: unit-beside
|
|
1783
1814
|
coverage: required
|
|
@@ -1800,7 +1831,7 @@ slots:
|
|
|
1800
1831
|
the same QueueOutbox and QueueWorkerRegistry ports and full's BullMQ adapter on the declared Redis (backing: [redis]): the
|
|
1801
1832
|
relay adds each committed outbox row with the outbox id as the job id, workers retry with exponential backoff and keep a
|
|
1802
1833
|
job that exhausts its attempts as failed (the dead letter); the outbox table comes from a db migration, and a
|
|
1803
|
-
schedule is a BullMQ job scheduler or a
|
|
1834
|
+
schedule is a BullMQ job scheduler or a platform/jobs tick that writes one outbox row, R335 (knowledge/patterns/be/queues.yaml liteAdapter)
|
|
1804
1835
|
requires: [index.ts, queue.module.ts, queue.port.ts, queue.contracts.ts, queue-outbox.service.ts, queue-outbox.service.spec.ts]
|
|
1805
1836
|
tests: unit-beside
|
|
1806
1837
|
coverage: required
|
|
@@ -288,19 +288,21 @@ 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 self-hosted Postgres adapter behind them. ruling:
|
|
292
|
-
#
|
|
291
|
+
# Edition lite (R212): the same ports and rules, a self-hosted Postgres adapter behind them. ruling:lite-pgmq-bus-bullmq-jobs makes pgmq the
|
|
292
|
+
# lite event bus and keeps jobs on BullMQ (platform/queue).
|
|
293
293
|
liteAdapter:
|
|
294
294
|
summary: >-
|
|
295
|
-
Under lite the event bus keeps typed events, the vendored contract, publish(event, tx, correlationId) in the domain transaction,
|
|
296
|
-
the inbox and thin consumers; Kafka is replaced by pgmq
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
295
|
+
Under lite the event bus keeps typed events, the vendored contract, publish(event, tx, correlationId) in the domain transaction,
|
|
296
|
+
the inbox and thin consumers; Kafka is replaced by pgmq topics in the db service. Publish sends the envelope to its topic
|
|
297
|
+
(the event name) in the publisher's transaction, so no outbox or relay stands between them. Each consumer of an event has its
|
|
298
|
+
own pgmq queue `<topic>__<consumer>` and dead-letter queue `<topic>__<consumer>__dead`, created and bound to the event's name
|
|
299
|
+
by a db migration and named by a literal `<TOPIC>_<CONSUMER>_TOPIC_QUEUE` constant under platform/event-bus (its owner, R258);
|
|
300
|
+
every payload carries a business dedupeKey the consumer inbox claims, and subscriptions are static (R336); the application
|
|
301
|
+
role reaches pgmq only through the security-definer event_bus_* functions of the migration (contexts/code/be/event-bus.md BE-EVENT-BUS-6).
|
|
300
302
|
ports:
|
|
301
|
-
- {port:
|
|
302
|
-
- {port:
|
|
303
|
-
- {port:
|
|
303
|
+
- {port: EventBus.publish (platform/event-bus), adapter: "event_bus_send: pgmq.send_topic of the envelope in the domain transaction"}
|
|
304
|
+
- {port: EventConsumer (platform/event-bus), adapter: "event_bus_read with a visibility timeout, the inbox claim on (consumer, dedupeKey) in the dispatched command, event_bus_archive on success, a failed delivery left to reappear, event_bus_bury into <queue>__dead past maxDeliveries (default 5)"}
|
|
305
|
+
- {port: EventBus.deadLetters and requeue (platform/event-bus), adapter: "event_bus_dead_letters and event_bus_requeue over a consumer's dead-letter queue"}
|
|
304
306
|
forbids: [kafkajs]
|
|
305
307
|
proof: >-
|
|
306
308
|
Unit specs as full; declaring event-bus requires integration specs titled `event-bus/<scenario>: ...` for every declared
|
|
@@ -258,7 +258,7 @@ liteAdapter:
|
|
|
258
258
|
idempotency pattern with its required proof scenarios.
|
|
259
259
|
ports:
|
|
260
260
|
- {port: Inbox, adapter: PostgresInbox through the caller's active EntityManager transaction}
|
|
261
|
-
- {port: receipt retention, adapter: a fenced job delivered by the BullMQ queue on the declared Redis and scheduled by a
|
|
261
|
+
- {port: receipt retention, adapter: a fenced job delivered by the BullMQ queue on the declared Redis and scheduled by a platform/jobs tick (R335) 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
264
|
lite's local db integration lane.
|
|
@@ -287,8 +287,11 @@ liteAdapter:
|
|
|
287
287
|
ports:
|
|
288
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
|
+
- {port: tick scheduler (platform/jobs), adapter: "JOB_TICKS of job-ticks.contracts.ts as data and JobTickService (job-tick.service.ts, composed with ScheduleModule.forRoot() by the lite jobs.module.ts): one CronJob per tick in SchedulerRegistry, each firing taking advisoryXactLock and claiming the tick's UTC minute through the job_tick_claim function of the jobs migration before it writes the queue outbox (R335, contexts/code/be/jobs.md BE-JOBS-6)"}
|
|
290
291
|
proof: >-
|
|
291
292
|
Unit specs as full (job-claim.service.spec.ts); declaring fenced-job requires integration specs titled
|
|
292
293
|
`fenced-job/<scenario>: ...` for every declared scenario on the local db service (R284, which runs no container of its
|
|
293
294
|
own): the claim, the fencing token and the guarded writes are Postgres facts, driven there by the real runner without BullMQ.
|
|
295
|
+
The tick scheduler's unit spec (job-tick.service.spec.ts) proves the lock, the minute claim and that a replica that comes
|
|
296
|
+
second enqueues nothing.
|
|
294
297
|
see: [knowledge/patterns/be/queues.yaml]
|
|
@@ -78,18 +78,19 @@ rules:
|
|
|
78
78
|
manual: []
|
|
79
79
|
relatedExamples: [queue]
|
|
80
80
|
- id: BE-QUEUES-3
|
|
81
|
-
title: A schedule is a BullMQ job scheduler declared with its queue, or a
|
|
81
|
+
title: A schedule is a BullMQ job scheduler declared with its queue, or a platform/jobs tick that writes the outbox
|
|
82
82
|
kind: mandatory
|
|
83
|
-
hfsRules: [R90,
|
|
83
|
+
hfsRules: [R90, R335]
|
|
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 a lite app, a
|
|
87
|
-
|
|
88
|
-
`
|
|
89
|
-
|
|
90
|
-
`
|
|
86
|
+
`platform/queue` registers at boot, or, in a lite app, a tick of `platform/jobs` (BE-JOBS-6, R335): `JOB_TICKS` of
|
|
87
|
+
`job-ticks.contracts.ts` lists it as data and `JobTickService` fires it on every replica, where each firing takes
|
|
88
|
+
`advisoryXactLock(tx, "tick:<name>")`, claims the tick's UTC minute through `job_tick_claim` and only then writes one row of
|
|
89
|
+
the tick's queue into the queue outbox, which the relay hands to BullMQ. Its job is processed by the same
|
|
90
|
+
`features/jobs/<job>/<job>.processor.ts` as any enqueued job. `@nestjs/schedule` and `cron` are imported only by that
|
|
91
|
+
scheduler, and `@Cron`, `@Interval`, `@Timeout` and `setInterval` for work are not used anywhere.
|
|
91
92
|
rationale: |-
|
|
92
|
-
One scheduler in Redis, or one
|
|
93
|
+
One scheduler in Redis, or one minute claim in the database, fires once for the whole deployment, where cron in a replicated
|
|
93
94
|
process fires once per replica.
|
|
94
95
|
cases:
|
|
95
96
|
- id: case-1
|
|
@@ -97,12 +98,11 @@ rules:
|
|
|
97
98
|
write: 'order-expiry.queue.ts declares scheduler { every: 5 minutes } for the expire-orders job; the processor dispatches ExpireOrdersCommand.'
|
|
98
99
|
- id: case-2
|
|
99
100
|
when: A lite app sends booking reminders every fifteen minutes
|
|
100
|
-
write: "
|
|
101
|
+
write: "platform/jobs declares a booking_reminder tick every fifteen minutes (BE-JOBS-6): each firing claims its slot and writes one booking_reminder row through the queue outbox; the send-reminder processor is the queue's."
|
|
101
102
|
verification:
|
|
102
103
|
automated:
|
|
103
104
|
- BE_INFRA_OWNER
|
|
104
105
|
- DB_PGMQ_DECLARED
|
|
105
|
-
- DB_CRON_OWNED
|
|
106
106
|
manual:
|
|
107
107
|
- Confirm the cadence and the job's idempotence.
|
|
108
108
|
relatedExamples: [queue]
|
|
@@ -276,19 +276,19 @@ liteAdapter:
|
|
|
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
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
|
|
279
|
+
is the event bus, not a queue transport (ruling:lite-pgmq-bus-bullmq-jobs). `starci app add queue|job` writes this adapter in a lite app, with the outbox and job
|
|
280
280
|
tables as one db migration, the Redis declaration and the persisted Redis of every docker-swarm environment.
|
|
281
281
|
ports:
|
|
282
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
|
|
287
|
-
forbids: [kafkajs
|
|
286
|
+
- {port: schedulers, adapter: "a BullMQ job scheduler declared with its queue, as full, or a platform/jobs tick (JobTickService, R335) that writes one row of the tick's queue into the queue outbox under its lock and minute claim"}
|
|
287
|
+
forbids: [kafkajs]
|
|
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
290
|
requires integration specs titled `queue/<scenario>: ...` for every declared scenario on the local db service (R284,
|
|
291
|
-
which runs no container of its own): the outbox, its commit boundary, the relay identity and the
|
|
291
|
+
which runs no container of its own): the outbox, its commit boundary, the relay identity and the tick's minute claim 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
|
|
294
294
|
persist refusals; the relay's unit spec proves a refused persist leaves the rows waiting.
|
|
@@ -158,7 +158,7 @@ files:
|
|
|
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
|
|
161
|
-
come from the lite event bus (its own pgmq queue per consumer group).
|
|
161
|
+
come from the lite event bus (its own pgmq queue per consumer group, bound to the event's topic).
|
|
162
162
|
ports:
|
|
163
163
|
- {port: EventConsumer delivery, adapter: "the reactor's pgmq queue (see event-bus.yaml liteAdapter)"}
|
|
164
164
|
proof: >-
|
|
@@ -670,7 +670,7 @@ files:
|
|
|
670
670
|
optional: true
|
|
671
671
|
- path: db/migrations/<timestamp>_realtime-events.sql
|
|
672
672
|
slot: repo.side-root-forbidden
|
|
673
|
-
role: Lite edition migration (created with `npm run db:new`) of the same two tables, forced row level security for app_be
|
|
673
|
+
role: Lite edition migration (created with `npm run db:new`) of the same two tables, forced row level security for app_be; the retention queue is fired by a platform/jobs tick (R335).
|
|
674
674
|
optional: true
|
|
675
675
|
- path: src/features/reactors/<name>/transport/message/<event>.consumer.ts
|
|
676
676
|
slot: be.transport.message
|
|
@@ -1827,17 +1827,6 @@ DB_BASELINE_SHAPE:
|
|
|
1827
1827
|
|
|
1828
1828
|
|
|
1829
1829
|
|
|
1830
|
-
DB_CRON_OWNED:
|
|
1831
|
-
title: "A uniquely owned migration cron job only enqueues through the queue outbox"
|
|
1832
|
-
title_vi: "Lịch pg_cron thiếu chủ sở hữu hoặc thực hiện nghiệp vụ"
|
|
1833
|
-
meaning_vi: "Tên lịch bị trùng, hằng hàng đợi hoặc hàm tick không tồn tại, lệnh thực hiện DML nghiệp vụ hay HTTP thay vì chỉ chèn một dòng của hằng hàng đợi vào bảng outbox của ruleParams.db.queueOutbox (chỉ các cột đã khai báo), lịch đang chạy vẫn gọi pgmq.send, hoặc cùng tick còn được lập lịch ở ứng dụng."
|
|
1834
|
-
causes_vi:
|
|
1835
|
-
- "Vi phạm R258: pg_cron chỉ đưa việc vào hàng đợi qua outbox, mỗi tick có đúng một nơi lập lịch và được hủy lịch khi loại bỏ."
|
|
1836
|
-
- "Lịch đang chạy (hoặc tick hiện tại của nó) vẫn gửi bằng pgmq.send; pgmq.send chỉ hợp lệ trong thân gốc của lịch cũ."
|
|
1837
|
-
nextStep_vi: "Đặt lịch trong migration với tên duy nhất, chèn đúng một dòng của hằng hàng đợi vào outbox hàng đợi (trực tiếp hoặc qua một hàm *_tick chỉ làm việc đó), bỏ lịch trùng và gọi cron.unschedule khi xóa tick."
|
|
1838
|
-
owner: op-retry
|
|
1839
|
-
kind: check-finding
|
|
1840
|
-
|
|
1841
1830
|
DB_DEFINER_REVOKE:
|
|
1842
1831
|
title: "Exposed security definer functions revoke browser execution"
|
|
1843
1832
|
title_vi: "Function security definer chưa thu hồi quyền trình duyệt"
|
|
@@ -1909,14 +1898,14 @@ DB_MIGRATION_SHAPE:
|
|
|
1909
1898
|
kind: check-finding
|
|
1910
1899
|
|
|
1911
1900
|
DB_PGMQ_DECLARED:
|
|
1912
|
-
title: "A queue constant keeps a pgmq queue, a pgmq queue has no event-bus owner, or a pgmq
|
|
1913
|
-
title_vi: "Hằng hàng đợi còn hàng đợi pgmq, hàng đợi pgmq không có chủ event-bus, hoặc
|
|
1914
|
-
meaning_vi: "
|
|
1901
|
+
title: "A queue constant keeps a pgmq queue, a pgmq queue has no event-bus owner, or a pgmq queue, binding or grant is declared unsafely"
|
|
1902
|
+
title_vi: "Hằng hàng đợi còn hàng đợi pgmq, hàng đợi pgmq không có chủ event-bus, hoặc hàng đợi, liên kết topic hay quyền pgmq khai báo sai"
|
|
1903
|
+
meaning_vi: "pgmq là event bus và job chạy trên BullMQ (ruling:lite-pgmq-bus-bullmq-jobs), nhưng các migration đang hoạt động vẫn để lại một hàng đợi pgmq trùng tên một hằng hàng đợi trong modules/queues, hoặc một hàng đợi pgmq không được hằng literal *_TOPIC_QUEUE nào trong platform/event-bus đặt tên (hay một hằng như vậy không có hàng đợi pgmq); hoặc pgmq.create hay pgmq.bind_topic không có tên literal trong migration, liên kết một hàng đợi chưa được tạo, tên hằng không phải literal, hay quyền trên đối tượng pgmq vượt vai trò ứng dụng."
|
|
1915
1904
|
causes_vi:
|
|
1916
|
-
- "Vi phạm R258:
|
|
1917
|
-
- "Một hàng đợi pgmq bị bỏ lại hoặc đổi tên mà không có chủ event-bus."
|
|
1905
|
+
- "Vi phạm R258: một job của app chạy trên pgmq thay vì BullMQ trên Redis."
|
|
1906
|
+
- "Một hàng đợi pgmq bị bỏ lại hoặc đổi tên mà không có chủ event-bus, hoặc một liên kết topic trỏ tới hàng đợi chưa tạo."
|
|
1918
1907
|
- "Migration, mã nguồn hoặc quyền truy cập khai báo đối tượng pgmq ngoài migration tĩnh hoặc cho vai trò khác vai trò ứng dụng."
|
|
1919
|
-
nextStep_vi: "Chuyển
|
|
1908
|
+
nextStep_vi: "Chuyển job sang BullMQ trên Redis đã khai báo, rồi xóa hàng đợi pgmq bằng pgmq.drop_queue trong một migration mới; hàng đợi của một nhóm consumer được đặt tên bằng một hằng literal *_TOPIC_QUEUE trong platform/event-bus, tạo bằng pgmq.create rồi liên kết với tên event bằng pgmq.bind_topic trong cùng migration; vai trò ứng dụng đi qua các hàm event_bus_* thay vì nhận quyền trên schema pgmq."
|
|
1920
1909
|
owner: op-retry
|
|
1921
1910
|
kind: check-finding
|
|
1922
1911
|
|
|
@@ -3732,7 +3721,7 @@ HFS_HOST_CRON:
|
|
|
3732
3721
|
meaning_vi: "Tệp triển khai hoặc script tại <path> khai báo crontab, cron.d, systemd timer, @reboot hoặc at; lịch chạy phải do ứng dụng sở hữu."
|
|
3733
3722
|
causes_vi:
|
|
3734
3723
|
- "Vi phạm R250: cài lịch chạy ngoài scheduler của ứng dụng hoặc replicated-job đã khai báo."
|
|
3735
|
-
nextStep_vi: "Chuyển tác vụ sang scheduler hàng đợi của back end, một tick
|
|
3724
|
+
nextStep_vi: "Chuyển tác vụ sang scheduler hàng đợi của back end, một tick của platform/jobs (R335) enqueue qua outbox hàng đợi hoặc replicated-job đã khai báo và gọi lệnh CLI của ứng dụng."
|
|
3736
3725
|
owner: op-retry
|
|
3737
3726
|
kind: check-finding
|
|
3738
3727
|
|
|
@@ -5600,3 +5589,24 @@ BE_PERSISTENCE_NO_RAW_SQL:
|
|
|
5600
5589
|
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
5590
|
owner: op-retry
|
|
5602
5591
|
kind: check-finding
|
|
5592
|
+
|
|
5593
|
+
BE_EVENT_BUS_LITE_SHAPE:
|
|
5594
|
+
title: "A lite event payload without dedupeKey, or a subscription that is not static"
|
|
5595
|
+
title_vi: "Payload event của bản lite thiếu dedupeKey, hoặc đăng ký consumer không tĩnh"
|
|
5596
|
+
meaning_vi: "File `<path>` khai một event (lớp kế thừa BaseEvent trong modules/events) mà kiểu payload không có `dedupeKey`, hoặc gọi `subscribe` với tên hàng đợi không phải hằng `*_TOPIC_QUEUE` import từ `@modules/platform/event-bus`, hay với options không phải object literal của giá trị literal."
|
|
5597
|
+
causes_vi:
|
|
5598
|
+
- "Vi phạm R336: event bus pgmq giao ít nhất một lần và không theo thứ tự, nên inbox của consumer claim (consumer, dedupeKey); thiếu dedupeKey thì không khử trùng được."
|
|
5599
|
+
- "Một đăng ký tính tên hàng đợi lúc chạy, nên không ai thấy được nó trong hằng và migration (R258)."
|
|
5600
|
+
nextStep_vi: "Thêm `readonly dedupeKey: string` (khoá nghiệp vụ, như id đơn hàng) vào kiểu payload hoặc kế thừa `EventPayload`; khai hằng `<TOPIC>_<CONSUMER>_TOPIC_QUEUE` trong platform/event-bus, tạo hàng đợi, hàng đợi `__dead` và bind_topic trong migration, rồi subscribe bằng hằng đó."
|
|
5601
|
+
owner: op-retry
|
|
5602
|
+
kind: check-finding
|
|
5603
|
+
|
|
5604
|
+
BE_JOBS_SCHEDULER_OWNER:
|
|
5605
|
+
title: "A schedule outside the platform/jobs tick scheduler"
|
|
5606
|
+
title_vi: "Lịch chạy định kỳ nằm ngoài bộ lập lịch tick của platform/jobs"
|
|
5607
|
+
meaning_vi: "File `<path>` import @nestjs/schedule hoặc cron ngoài job-tick.service.ts và jobs.module.ts của platform/jobs, dùng decorator Cron/Interval/Timeout, hoặc JobTickService không giữ advisoryXactLock và job_tick_claim trước khi ghi outbox."
|
|
5608
|
+
causes_vi:
|
|
5609
|
+
- "Vi phạm R335: việc định kỳ là tick của platform/jobs (JOB_TICKS), mỗi phút chỉ một replica enqueue nhờ khoá advisory và claim phút trong DB."
|
|
5610
|
+
nextStep_vi: "Khai tick vào JOB_TICKS của platform/jobs/job-ticks.contracts.ts (name, cron, timeZone UTC, queue, payload) và xoá lịch tự viết; giữ advisoryXactLock và job_tick_claim trong JobTickService."
|
|
5611
|
+
owner: op-retry
|
|
5612
|
+
kind: check-finding
|
|
@@ -85,7 +85,7 @@ properties:
|
|
|
85
85
|
command: {type: string, minLength: 1}
|
|
86
86
|
backing:
|
|
87
87
|
description: >-
|
|
88
|
-
The self-hosted database of a lite app: its db component (image
|
|
88
|
+
The self-hosted database of a lite app: its db component (the image its managed .starcistacks/images/db/Dockerfile builds), a Docker container in
|
|
89
89
|
development and a Swarm service in production. Every other dependency is a Docker component too.
|
|
90
90
|
type: object
|
|
91
91
|
additionalProperties:
|
|
@@ -855,5 +855,7 @@ $defs:
|
|
|
855
855
|
- {type: integer, minimum: 1}
|
|
856
856
|
- {type: string, pattern: '^[1-9][0-9]*$'}
|
|
857
857
|
where: {type: string, minLength: 1}
|
|
858
|
-
|
|
858
|
+
member: {type: string, pattern: '^[a-z0-9][a-z0-9-]*$', description: "The provider member holding this key (`secrets/<member>`, the slug of where); with key, the entry declares one key of it."}
|
|
859
|
+
key: {type: string, pattern: '^[A-Z][A-Z0-9_]*$', description: "The one key of the member this entry declares; materialized as its own file, read by the app as <KEY>_FILE."}
|
|
860
|
+
recipientPolicy: {type: string, minLength: 1, description: "Which age recipients the envelope is encrypted to: its environment's rule of the app's managed .sops.yaml."}
|
|
859
861
|
keyCustody: {type: string, minLength: 1, description: "Where the shared decrypting identity is held; docs/host-secrets.md owns host custody and its execution prerequisites."}
|
|
@@ -329,7 +329,10 @@ const checkTrackedCustody = ({ repo, root, add }) => {
|
|
|
329
329
|
// infra/** is tracked source, but a value file inside it (.env*, *.env, *.key, *.pem, *.tfvars)
|
|
330
330
|
// is plaintext custody (inc-5360513a96b3: infra/compose/.env.generated was trackable).
|
|
331
331
|
const infraValue = parts.length > 3 && parts[2] === 'infra' && isInfrastructureValueFile(base) && !base.endsWith('.enc') && !base.endsWith('.example');
|
|
332
|
-
|
|
332
|
+
// An app's sealed member is `<env>/secrets/<member>.enc.yaml` (ruling app-secrets-one-standard, scripts/lib/sealed-member.mjs);
|
|
333
|
+
// runtime custody keeps `<name>.<fmt>.enc`.
|
|
334
|
+
const appMember = parts.length === 4 && parts[2] === 'secrets' && base.endsWith('.enc.yaml');
|
|
335
|
+
if ((custodyArea && !base.endsWith('.enc') && !appMember && !['KEYS.md', '.gitkeep'].includes(base)) || infraValue)
|
|
333
336
|
add('refuse', 'STACKS_PLAINTEXT_TRACKED', file, /\.enc\.(?:ya?ml|json|env)$/u.test(base)
|
|
334
337
|
? `${base} is named the wrong way round: a custody file is <name>.<fmt>.enc (rename it to ${base.replace(/\.enc\.(ya?ml|json|env)$/u, '.$1.enc')}) and every read states the format (scripts/api/sops/exec-env.mjs)`
|
|
335
338
|
: 'a custody member is tracked in plaintext; untrack it (git rm --cached), rotate the value and commit only its .enc twin');
|
|
@@ -363,7 +366,8 @@ const checkIgnoreRules = ({ repo, root, add }) => {
|
|
|
363
366
|
}
|
|
364
367
|
};
|
|
365
368
|
// Custody layout (modules/schemas/stacks-layout.yaml custody, the product repositories' secrets-guard).
|
|
366
|
-
|
|
369
|
+
// The images folder holds the managed db image of a lite app (slot app.db.image), never custody or an environment.
|
|
370
|
+
const envDirs = (repo, root) => fs.readdirSync(path.join(repo, root), { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name !== 'images');
|
|
367
371
|
const checkCustodyLayout = ({ repo, own, add }) => {
|
|
368
372
|
if (!own.rooted) return;
|
|
369
373
|
const root = STACK_ROOT;
|
|
@@ -54,7 +54,7 @@ function diffScanner(files = [], { encText = null, keepHit = null } = {}) {
|
|
|
54
54
|
if (raw.startsWith('+++ ')) {
|
|
55
55
|
flushEnc();
|
|
56
56
|
file = raw.slice(4).replace(/^b\//, '');
|
|
57
|
-
if (/\.enc
|
|
57
|
+
if (/\.enc(?:\.yaml)?$/i.test(file)) enc = { file, line: null, added: [], findings: [] };
|
|
58
58
|
return;
|
|
59
59
|
}
|
|
60
60
|
const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(raw);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { isSealedName, memberOfName } from '../lib/sealed-member.mjs';
|
|
2
3
|
import { list } from '../lib/list.mjs';
|
|
3
4
|
import { parseYaml } from '../../engine/yaml.mjs';
|
|
4
5
|
|
|
@@ -70,7 +71,7 @@ function entries(dir, fs) {
|
|
|
70
71
|
export function sealedKeyNames(root, { fs }) {
|
|
71
72
|
const out = [], base = path.join(root, '.starcistacks');
|
|
72
73
|
for (const env of entries(base, fs).filter((entry) => entry.isDirectory())) {
|
|
73
|
-
for (const member of entries(path.join(base, env.name, 'secrets'), fs).filter((entry) => entry.isFile() && entry.name
|
|
74
|
+
for (const member of entries(path.join(base, env.name, 'secrets'), fs).filter((entry) => entry.isFile() && isSealedName(entry.name))) {
|
|
74
75
|
const file = `.starcistacks/${env.name}/secrets/${member.name}`;
|
|
75
76
|
const source = fs.readFileSync(path.join(root, file), 'utf8');
|
|
76
77
|
const dotenv = [...source.matchAll(/^([A-Za-z_]\w*)=ENC\[/gm)].map((match) => match[1]).filter((key) => !key.startsWith('sops_'));
|
|
@@ -79,7 +80,7 @@ export function sealedKeyNames(root, { fs }) {
|
|
|
79
80
|
const doc = parseYaml(source);
|
|
80
81
|
keys = doc && typeof doc === 'object' && !Array.isArray(doc) ? Object.keys(doc).filter((key) => key !== 'sops' && !key.startsWith('sops_')) : [];
|
|
81
82
|
}
|
|
82
|
-
for (const key of keys) out.push({ env: env.name, member: member.name
|
|
83
|
+
for (const key of keys) out.push({ env: env.name, member: memberOfName(member.name), key, file });
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
return out;
|