@starci/hfs 4.11.0 → 4.11.11
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 +85 -0
- package/README.md +43 -1
- package/db/drift.mjs +8 -6
- package/db/image.mjs +9 -4
- package/db/migrate.mjs +3 -3
- package/package.json +2 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +13 -8
- package/runtime/knowledge/hfs/rules.yaml +30 -13
- package/runtime/knowledge/hfs/slots.yaml +63 -37
- package/runtime/knowledge/patterns/be/event-bus.yaml +12 -10
- package/runtime/knowledge/patterns/be/queues.yaml +2 -3
- package/runtime/knowledge/patterns/be/reactors.yaml +1 -1
- package/runtime/modules/kernel/failure-codes.yaml +17 -17
- package/runtime/modules/schemas/application-stacks.schema.yaml +4 -2
- package/runtime/scripts/api/sops/lib.mjs +30 -0
- package/runtime/scripts/api/sops/update-keys.mjs +20 -0
- 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 +50 -10
- 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/deploy-workflow.mjs +19 -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 +2 -2
- 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 +108 -9
- package/runtime/scripts/hfs/secret-standard.mjs +369 -0
- package/runtime/scripts/hfs/secret-store.mjs +23 -9
- package/runtime/scripts/hfs/secret.mjs +140 -46
- package/runtime/scripts/hfs/slots.mjs +8 -1
- package/runtime/scripts/hfs/stack-deploy-config.mjs +31 -10
- 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 +2 -2
- package/scaffold/add-inbox-lite.mjs +2 -2
- package/scaffold/app.mjs +2 -2
- package/src/main.mjs +6 -4
- package/stack/age-install.mjs +4 -3
- package/stack/build.mjs +1 -1
- package/stack/config.mjs +2 -1
- package/stack/context.mjs +32 -2
- package/stack/docker-transport.mjs +2 -1
- package/stack/remote.mjs +8 -5
- package/stack/sealed.mjs +9 -8
- package/stack/server-key.mjs +296 -0
- package/stack/ssh-session.mjs +7 -1
- package/stack/target-binding.mjs +2 -1
- package/sync/hygiene.mjs +1 -1
- package/sync/index.mjs +36 -10
- package/templates/app/ci-workflows/github/workflows/images.yml +2 -0
- package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +3 -1
- package/templates/app/ci-workflows/github/workflows/stack-key-restore.yml +58 -0
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +2 -0
- package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +3 -1
- package/templates/app/ci-workflows-lite/github/workflows/stack-key-restore.yml +58 -0
- package/templates/app/db-bootstrap/db/bootstrap.sql +7 -4
- 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 +1 -1
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +1 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +1 -1
- package/templates/app/skeleton-lite/.starcistacks/production/README.md +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-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 +1 -1
- package/templates/be/patterns/jobs/platform-lite/job-tick.service.ts.tpl +1 -1
- 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 +38 -3
- package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.ts +36 -4
- 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/apps/api/src/api.env.ts +4 -0
- package/templates/be/skeleton-lite/apps/api/src/api.options.ts +3 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +1 -1
- 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/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +1 -0
- package/runtime/scripts/hfs/rules/database-queue-outbox.mjs +0 -102
- package/templates/app/db-image/db/Dockerfile +0 -8
- 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.14.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).
|
|
@@ -55,6 +55,11 @@ versioning:
|
|
|
55
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
56
|
# (db/Dockerfile, written by `starci app sync --write`), narrows ruleParams.db.extensions to the extensions that image carries, and gives
|
|
57
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).
|
|
61
|
+
# 3.14.0 (ruling app-secrets-one-standard, 2026-10-29) adds the optional app.ci-stack-key-restore, the managed workflow that writes the
|
|
62
|
+
# deploy server's age identity back from its GitHub Environment backup.
|
|
58
63
|
|
|
59
64
|
# Field meaning, per slot.
|
|
60
65
|
# id stable name, never changes meaning inside a major.
|
|
@@ -129,8 +134,8 @@ testValues: [unit-beside, e2e, none]
|
|
|
129
134
|
# event bus, saga, reactors, projection, realtime, webhooks, fenced job), declared in hfs.json the same way, and the same unit
|
|
130
135
|
# specs (unitRoles, logicRoles, 100 per file, @starci/jest-preset doubles, builders). What differs is ADAPTERS and DELIVERY
|
|
131
136
|
# (ruling:R212/case-10): schema authority in db/migrations, the queue port on BullMQ over the Redis hfs.json
|
|
132
|
-
# sides.be.backing declares (as in full, fed by the Postgres outbox), event-bus ports on pgmq
|
|
133
|
-
#
|
|
137
|
+
# sides.be.backing declares (as in full, fed by the Postgres outbox), event-bus ports on pgmq topics sent in the publisher's
|
|
138
|
+
# 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
|
|
134
139
|
# outbox (R335), storage on S3-compatible MinIO (integrations/object-storage) and auth through integrations/passport. Realtime uses SSE or the socket.io gateway
|
|
135
140
|
# 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;
|
|
136
141
|
# declared patterns owe their integration proofs (R284). Lite has no e2e world. Production uses declared docker-swarm under
|
|
@@ -209,11 +214,9 @@ ruleParams:
|
|
|
209
214
|
db:
|
|
210
215
|
# R259: btree_gist supplies UUID equality for the booking interval exclusion constraint.
|
|
211
216
|
# R259: the extensions the db image carries (ruling:lite-official-postgres-image): the official postgres image's contrib set and pgvector
|
|
212
|
-
# from PGDG; unaccent serves Vietnamese-content search functions
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
# job enqueues only by inserting one row into this table, writing only these columns, one of which is `queue`.
|
|
216
|
-
queueOutbox: {table: public.queue, columns: [queue, payload, created_at, correlation_id]}
|
|
217
|
+
# from PGDG; unaccent serves Vietnamese-content search functions; pgmq is the event bus (ruling:lite-pgmq-bus-bullmq-jobs), installed
|
|
218
|
+
# from its pinned release. No pg_cron, pg_net or supabase_vault.
|
|
219
|
+
extensions: [pgcrypto, pg_trgm, vector, uuid-ossp, unaccent, btree_gist, pgmq]
|
|
217
220
|
common:
|
|
218
221
|
# HFS_SIZE_GROWTH: a file above soft may not grow against its parent commit; a new file stays within soft. Slot
|
|
219
222
|
# budgets (component.tsx 300, hooks 200, modules 400) are stricter than this cap where they apply.
|
|
@@ -429,29 +432,36 @@ ruleParams:
|
|
|
429
432
|
# 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,
|
|
430
433
|
# bullmq in platform/queue and ioredis/redis in integrations/cache and integrations/redis (ruling:R212/case-10: the queue is
|
|
431
434
|
# Redis in both editions; a backing library is legal only when hfs.json sides.be.backing declares its service, which
|
|
432
|
-
# HFS_BACKING_UNDECLARED checks), except Kafka, which is owned nowhere (platform/event-bus runs pgmq
|
|
435
|
+
# HFS_BACKING_UNDECLARED checks), except Kafka, which is owned nowhere (platform/event-bus runs pgmq topics through
|
|
433
436
|
# the shared EntityManager); realtime is the same socket.io gateway as full, run inside the api process, scaled horizontally and stateless (hub redis when
|
|
434
437
|
# Redis is declared, else hub postgres with @socket.io/postgres-adapter; no sticky sessions).
|
|
435
438
|
# R334: the database functions code may call through platform/database's callDbFunction, advisoryXactLock and notify helpers,
|
|
436
|
-
# each {name, reason};
|
|
439
|
+
# each {name, reason}; a pg built-in is listed bare (called in pg_catalog), every other function schema-qualified (public.job_claim),
|
|
440
|
+
# and an app adds its own, schema-qualified, in hfs.json sides.be.dbFunctions.
|
|
437
441
|
dbFunctions:
|
|
438
442
|
- {name: pg_advisory_xact_lock, reason: "advisoryXactLock: a transaction-scoped lock TypeORM has no API for"}
|
|
439
443
|
- {name: hashtextextended, reason: "advisoryXactLock: the 64-bit key of a text lock name"}
|
|
440
444
|
- {name: pg_notify, reason: "notify: LISTEN/NOTIFY of the Postgres realtime hub"}
|
|
441
445
|
- {name: to_regproc, reason: "callDbFunction: whether an optional database function exists"}
|
|
442
|
-
- {name:
|
|
443
|
-
- {name:
|
|
444
|
-
- {name:
|
|
445
|
-
- {name:
|
|
446
|
-
- {name:
|
|
447
|
-
- {name:
|
|
448
|
-
- {name:
|
|
449
|
-
- {name:
|
|
450
|
-
- {name:
|
|
451
|
-
- {name:
|
|
452
|
-
- {name:
|
|
453
|
-
- {name:
|
|
454
|
-
- {name:
|
|
446
|
+
- {name: gen_random_uuid, reason: "callDbFunction: a random uuid from the database (PG13+ core, pg_catalog) where a token is minted in the transaction"}
|
|
447
|
+
- {name: public.job_claim, reason: "platform/jobs (add job, lite): the fenced claim upsert of jobs.sql"}
|
|
448
|
+
- {name: public.job_advance, reason: "platform/jobs (add job, lite): the token-guarded step write of jobs.sql"}
|
|
449
|
+
- {name: public.job_complete, reason: "platform/jobs (add job, lite): the token-guarded completion of jobs.sql"}
|
|
450
|
+
- {name: public.job_fail, reason: "platform/jobs (add job, lite): the token-guarded failure of jobs.sql"}
|
|
451
|
+
- {name: public.job_tick_claim, reason: "platform/jobs (add job, lite): the once-per-minute claim of a scheduler tick, jobs.sql (R335)"}
|
|
452
|
+
- {name: public.queue_write, reason: "platform/queue (add queue, lite): the outbox insert of queue-outbox.sql"}
|
|
453
|
+
- {name: public.queue_waiting, reason: "platform/queue (add queue, lite): the relay read FOR UPDATE SKIP LOCKED of queue-outbox.sql"}
|
|
454
|
+
- {name: public.queue_mark_sent, reason: "platform/queue (add queue, lite): the relay mark of queue-outbox.sql"}
|
|
455
|
+
- {name: public.inbox_claim_event, reason: "platform/inbox (add webhook, lite): the conflict-safe delivery claim"}
|
|
456
|
+
- {name: public.inbox_claim_client, reason: "platform/inbox (add webhook, lite): the client claim upsert returning its committed outcome"}
|
|
457
|
+
- {name: public.inbox_save_client_outcome, reason: "platform/inbox (add webhook, lite): the saved client outcome"}
|
|
458
|
+
- {name: public.inbox_sweep_clients, reason: "platform/inbox (add webhook, lite): the bounded retention sweep FOR UPDATE SKIP LOCKED"}
|
|
459
|
+
- {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"}
|
|
460
|
+
- {name: public.event_bus_read, reason: "platform/event-bus (add webhook, lite): a consumer's pgmq read with a visibility timeout"}
|
|
461
|
+
- {name: public.event_bus_archive, reason: "platform/event-bus (add webhook, lite): the archive of a handled message"}
|
|
462
|
+
- {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"}
|
|
463
|
+
- {name: public.event_bus_dead_letters, reason: "platform/event-bus (add webhook, lite): the operator read of a consumer's dead-letter queue"}
|
|
464
|
+
- {name: public.event_bus_requeue, reason: "platform/event-bus (add webhook, lite): the operator requeue of one dead letter"}
|
|
455
465
|
|
|
456
466
|
lite:
|
|
457
467
|
schemaAuthority: sql
|
|
@@ -668,6 +678,19 @@ slots:
|
|
|
668
678
|
managedBy: ci-workflows
|
|
669
679
|
liteManagedBy: ci-workflows-lite
|
|
670
680
|
rules: [HFS_MANAGED_FILE_DRIFT]
|
|
681
|
+
- id: app.ci-stack-key-restore
|
|
682
|
+
since: 3.14.0
|
|
683
|
+
profiles: [app]
|
|
684
|
+
editionWhy: "ruling:app-secrets-one-standard: both editions deploy to a Docker Swarm server whose own age identity the owner backs up as a protected GitHub Environment secret; this workflow writes it back."
|
|
685
|
+
# The server identity restore workflow: workflow_dispatch only (like app.ci-stack-deploy); `starci app secret server-key <env> --restore github` dispatches it, rendered by starci app sync.
|
|
686
|
+
path: ".github/workflows/stack-key-restore.yml"
|
|
687
|
+
presence: optional
|
|
688
|
+
tracked: tracked
|
|
689
|
+
tier: none
|
|
690
|
+
tests: none
|
|
691
|
+
managedBy: ci-workflows
|
|
692
|
+
liteManagedBy: ci-workflows-lite
|
|
693
|
+
rules: [HFS_MANAGED_FILE_DRIFT]
|
|
671
694
|
- id: app.dockerignore
|
|
672
695
|
since: 3.1.0
|
|
673
696
|
profiles: [app]
|
|
@@ -870,7 +893,7 @@ slots:
|
|
|
870
893
|
# The db service supplies the database; schedules are platform/jobs ticks (R335); production has no Compose or host cron.
|
|
871
894
|
lite:
|
|
872
895
|
requires: [application-stacks.yaml]
|
|
873
|
-
allows: [application-stacks.yaml, "<env>/README.md", "<env>/environment.json", "<env>/infra/swarm/**", "<env>/infra/metadata.json",
|
|
896
|
+
allows: [application-stacks.yaml, images/db/Dockerfile, "<env>/README.md", "<env>/environment.json", "<env>/infra/swarm/**", "<env>/infra/metadata.json",
|
|
874
897
|
"<env>/runtime/{config,env}/**", "<env>/runtime/env/KEYS.md", "<env>/secrets/<slug>.enc", "<env>/secrets/.gitkeep", "<env>/seeds/**"]
|
|
875
898
|
forbids: ["<env>/runtime/files/**", "**/*.enc outside <env>/secrets/", DESIGN.md, deployment.json, "k8s/ at root"]
|
|
876
899
|
rules: [HFS_STACKS_SHAPE, HFS_STACKS_IN_SIDE, HFS_PLAINTEXT_SECRET, HFS_IDENTITY_CUSTODY]
|
|
@@ -931,7 +954,7 @@ slots:
|
|
|
931
954
|
rules: [HFS_PLAINTEXT_SECRET]
|
|
932
955
|
|
|
933
956
|
# ----- app root: the database of a lite app (ruling:lite-self-hosted-postgres-passport-minio): a self-hosted PostgreSQL in the app's
|
|
934
|
-
# stack (the `db` service, built from the managed db/Dockerfile: the official postgres image plus pgvector, ruling:lite-
|
|
957
|
+
# 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
|
|
935
958
|
# `starci app db migrate --url-env <NAME>`. A full app keeps its TypeORM migrations under be/ and has no such folder.
|
|
936
959
|
- id: app.db
|
|
937
960
|
profiles: [app]
|
|
@@ -984,10 +1007,11 @@ slots:
|
|
|
984
1007
|
- id: app.db.image
|
|
985
1008
|
profiles: [app]
|
|
986
1009
|
editions: [lite]
|
|
987
|
-
# The managed image of the db service (templates/app/db-image,
|
|
988
|
-
# postgres image plus postgresql-17-pgvector from PGDG
|
|
989
|
-
# of `starci app db drift`
|
|
990
|
-
|
|
1010
|
+
# The managed image of the db service (templates/app/db-image, rulings lite-official-postgres-image and lite-db-image-in-stacks): FROM
|
|
1011
|
+
# the digest-pinned official postgres image plus postgresql-17-pgvector from PGDG and the sha256-verified pgmq release. It lives with
|
|
1012
|
+
# the stacks; the stack's db service, the local db container, the scratch database of `starci app db drift` and the squash replay
|
|
1013
|
+
# all build it.
|
|
1014
|
+
path: ".starcistacks/images/db/Dockerfile"
|
|
991
1015
|
presence: required
|
|
992
1016
|
tracked: tracked
|
|
993
1017
|
managedBy: db-image
|
|
@@ -998,9 +1022,11 @@ slots:
|
|
|
998
1022
|
- id: app.db.bootstrap
|
|
999
1023
|
profiles: [app]
|
|
1000
1024
|
editions: [lite]
|
|
1001
|
-
# The managed pre-migration bootstrap (templates/app/db-bootstrap): the
|
|
1002
|
-
# set, auth.uid/role/jwt stubs and the NOLOGIN browser roles, so migrations
|
|
1003
|
-
# official postgres image (nothing else of Supabase: no supabase_admin
|
|
1025
|
+
# The managed pre-migration bootstrap (templates/app/db-bootstrap): the extensions schema every allowed extension is created in, the
|
|
1026
|
+
# auth and storage schemas, auth.users with the GoTrue column set, auth.uid/role/jwt stubs and the NOLOGIN browser roles, so migrations
|
|
1027
|
+
# written for a hosted Supabase apply to the db service's official postgres image (nothing else of Supabase: no supabase_admin or
|
|
1028
|
+
# realtime publication). `starci app db migrate` applies it before the first migration of an empty database; sync renders all of it
|
|
1029
|
+
# but the one marked section `starci app db baseline --squash` writes (the source's app roles and the grants a per-database dump loses).
|
|
1004
1030
|
path: "db/bootstrap.sql"
|
|
1005
1031
|
presence: required
|
|
1006
1032
|
tracked: tracked
|
|
@@ -1785,7 +1811,7 @@ slots:
|
|
|
1785
1811
|
- id: be.platform.event-bus
|
|
1786
1812
|
since: 3.1.0
|
|
1787
1813
|
profiles: [be]
|
|
1788
|
-
editionWhy: "ruling:R212/case-2 (owner:lite-production-swarm) (with the Work boundary amended by ruling:R212/case-4): lite implements the event ports
|
|
1814
|
+
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."
|
|
1789
1815
|
path: "src/modules/platform/event-bus/"
|
|
1790
1816
|
presence: opt-in
|
|
1791
1817
|
pattern: event-bus # the ONE durable event transport: transactional outbox, relay, Kafka, inbox, retry and dead letter (knowledge/patterns/be/event-bus.yaml)
|
|
@@ -1794,10 +1820,10 @@ slots:
|
|
|
1794
1820
|
owner: true
|
|
1795
1821
|
lite:
|
|
1796
1822
|
why: >-
|
|
1797
|
-
the
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1823
|
+
the EventBus and EventConsumer ports on pgmq (ruling:lite-pgmq-bus-bullmq-jobs): publish sends the envelope to its
|
|
1824
|
+
topic in the publisher's transaction, so no outbox or relay runs; each consumer group reads its own queue bound to the
|
|
1825
|
+
event's name with a visibility timeout, archives a handled message, lets a failed one reappear and buries one past its
|
|
1826
|
+
maxDeliveries in its __dead queue (BE-EVENT-BUS-6). Jobs stay on BullMQ (platform/queue). No Kafka (knowledge/patterns/be/event-bus.yaml liteAdapter)
|
|
1801
1827
|
requires: [index.ts, event-bus.module.ts, event-bus.port.ts, event-bus.contracts.ts, event-bus.service.ts, event-bus.service.spec.ts]
|
|
1802
1828
|
tests: unit-beside
|
|
1803
1829
|
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
|
|
@@ -98,12 +98,11 @@ rules:
|
|
|
98
98
|
write: 'order-expiry.queue.ts declares scheduler { every: 5 minutes } for the expire-orders job; the processor dispatches ExpireOrdersCommand.'
|
|
99
99
|
- id: case-2
|
|
100
100
|
when: A lite app sends booking reminders every fifteen minutes
|
|
101
|
-
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."
|
|
102
102
|
verification:
|
|
103
103
|
automated:
|
|
104
104
|
- BE_INFRA_OWNER
|
|
105
105
|
- DB_PGMQ_DECLARED
|
|
106
|
-
- DB_CRON_OWNED
|
|
107
106
|
manual:
|
|
108
107
|
- Confirm the cadence and the job's idempotence.
|
|
109
108
|
relatedExamples: [queue]
|
|
@@ -277,7 +276,7 @@ liteAdapter:
|
|
|
277
276
|
its append-only file (WAITAOF) and still holds it, so a job Redis loses inside the at most one second fsync window leaves
|
|
278
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
|
|
279
278
|
pattern: the work is a direct call inside the request, or a declared replicated-job running a cli command. pgmq
|
|
280
|
-
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
|
|
281
280
|
tables as one db migration, the Redis declaration and the persisted Redis of every docker-swarm environment.
|
|
282
281
|
ports:
|
|
283
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)"}
|
|
@@ -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: >-
|
|
@@ -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
|
|
|
@@ -5601,6 +5590,17 @@ BE_PERSISTENCE_NO_RAW_SQL:
|
|
|
5601
5590
|
owner: op-retry
|
|
5602
5591
|
kind: check-finding
|
|
5603
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
5604
|
BE_JOBS_SCHEDULER_OWNER:
|
|
5605
5605
|
title: "A schedule outside the platform/jobs tick scheduler"
|
|
5606
5606
|
title_vi: "Lịch chạy định kỳ nằm ngoài bộ lập lịch tick của platform/jobs"
|
|
@@ -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 (the image its managed db/Dockerfile builds), a Docker container in
|
|
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."}
|
|
@@ -310,6 +310,36 @@ export function runSelectedSops(bin, request, { selection, invocation, env, cwd,
|
|
|
310
310
|
}
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
+
/**
|
|
314
|
+
* `sops --config <config> updatekeys -y --input-type <type> <file>` under an inline selected identity: the member's data key is
|
|
315
|
+
* opened with SOPS_AGE_KEY in an isolated child environment and wrapped again to the recipients the config's rule names; no value
|
|
316
|
+
* is decrypted to stdout or disk. The tool profile is the selected one; captures are wiped.
|
|
317
|
+
*/
|
|
318
|
+
export function runSelectedUpdateKeys(bin, { file, config, inputType }, { selection, invocation, env, cwd, timeout } = {}) {
|
|
319
|
+
if (selection?.error?.identityRefusal !== 'inline-context-unqualified' || typeof selection.inlineName !== 'string' || !env || typeof env !== 'object') return held('selected-context-missing');
|
|
320
|
+
if (!invocation || typeof invocation.runProgram !== 'function' || typeof invocation.resolveRealTool !== 'function') return held('selected-context-missing');
|
|
321
|
+
if (!['yaml', 'dotenv', 'json'].includes(inputType) || !path.isAbsolute(file ?? '') || !path.isAbsolute(config ?? '')) return held('invalid-request');
|
|
322
|
+
const key = env[selection.inlineName];
|
|
323
|
+
if (typeof key !== 'string' || !/^AGE-SECRET-KEY-1[0-9A-Z]+$/.test(key.trim())) return held('invalid-inline');
|
|
324
|
+
const captures = [];
|
|
325
|
+
try {
|
|
326
|
+
const exe = bin ?? resolveSops(env), age = invocation.resolveRealTool('age-keygen', { env });
|
|
327
|
+
if (!nativeFile(exe) || !nativeFile(age)) return held('native-tool-unavailable');
|
|
328
|
+
if (!auditAbsent(cwd)) return held('unsupported-audit-context');
|
|
329
|
+
const publicEnv = isolatedSopsEnv(env, null);
|
|
330
|
+
const options = { cwd, shell: false, encoding: 'buffer', windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'], timeout: timeout ?? IDENTITY_TIMEOUT_MS, maxBuffer: CREDENTIAL_FILE_MAX_BYTES };
|
|
331
|
+
const run = (program, args, childEnv) => {
|
|
332
|
+
const result = invocation.runProgram(program, args, { ...options, env: childEnv });
|
|
333
|
+
captures.push(result); return result;
|
|
334
|
+
};
|
|
335
|
+
if (!toolProfileSupported(run(exe, ['--disable-version-check', '--version'], publicEnv), run(age, ['--version'], publicEnv))) return held('unsupported-tool-profile');
|
|
336
|
+
const result = run(exe, ['--disable-version-check', '--config', config, 'updatekeys', '-y', '--input-type', inputType, file], isolatedSopsEnv(env, selection.inlineName));
|
|
337
|
+
if (!captured(result)) return { status: result?.status ?? null, stdout: '', stderr: '', error: result?.error ?? null };
|
|
338
|
+
return { status: 0, stdout: '', stderr: '', error: null };
|
|
339
|
+
} catch { return held('selected-native-refused'); }
|
|
340
|
+
finally { wipeCaptures(captures); }
|
|
341
|
+
}
|
|
342
|
+
|
|
313
343
|
const identityRequestValid = ({ env, cwd, invocation, assertLease, consume }) =>
|
|
314
344
|
env && typeof env === 'object' && path.isAbsolute(cwd ?? '') && typeof consume === 'function'
|
|
315
345
|
&& typeof assertLease === 'function' && typeof invocation?.runProgram === 'function'
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// update-keys.mjs — the public SOPS `updatekeys` call: a sealed member is wrapped again to the recipients its .sops.yaml rule
|
|
2
|
+
// names. Only the member's data key is opened (with the caller's identity); no value is decrypted to stdout or disk.
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { sopsIdentityEnv } from '../../../engine/secrets.mjs';
|
|
5
|
+
import { resolveSops, runSelectedUpdateKeys } from './lib.mjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Run `<bin> --config <config> updatekeys -y --input-type <inputType> <file>` (bin null: the sops resolveSops finds). An inline
|
|
9
|
+
* SOPS_AGE_KEY goes through the selected isolation (runSelectedUpdateKeys); a SOPS_AGE_KEY_FILE runs with the selected environment.
|
|
10
|
+
* {status, stdout, stderr, error}; stdout and stderr carry public key names only.
|
|
11
|
+
*/
|
|
12
|
+
export function updateKeys(bin, { file, config, inputType }, { invocation = null, env = process.env, cwd = undefined, timeout = undefined } = {}) {
|
|
13
|
+
const selected = sopsIdentityEnv(env, { required: true });
|
|
14
|
+
if (selected.error?.identityRefusal === 'inline-context-unqualified') return runSelectedUpdateKeys(bin, { file, config, inputType }, { selection: selected, invocation, env, cwd, timeout });
|
|
15
|
+
if (selected.error) return { status: null, stdout: '', stderr: '', error: selected.error };
|
|
16
|
+
const exe = bin ?? resolveSops(selected.env);
|
|
17
|
+
if (!exe) return { status: null, stdout: '', stderr: '', error: Object.assign(new Error('sops is not installed (Windows: winget install Mozilla.SOPS)'), { code: 'SOPS_MISSING' }) };
|
|
18
|
+
const r = spawnSync(exe, ['--disable-version-check', '--config', config, 'updatekeys', '-y', '--input-type', inputType, file], { cwd, env: selected.env, encoding: 'utf8', windowsHide: true, timeout, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
19
|
+
return { status: r.status, stdout: r.stdout, stderr: r.stderr, error: r.error ?? null };
|
|
20
|
+
}
|
|
@@ -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;
|