@starci/hfs 4.2.17 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +7 -0
  3. package/package.json +1 -1
  4. package/runtime/knowledge/hfs/canon-pins.yaml +4 -4
  5. package/runtime/knowledge/hfs/rules.yaml +41 -13
  6. package/runtime/knowledge/hfs/slots.yaml +99 -43
  7. package/runtime/knowledge/patterns/be/api.yaml +64 -1
  8. package/runtime/knowledge/patterns/be/cli.yaml +23 -5
  9. package/runtime/knowledge/patterns/be/webhooks.yaml +187 -32
  10. package/runtime/knowledge/patterns/fe/folder.yaml +3 -0
  11. package/runtime/modules/kernel/failure-codes.yaml +15 -4
  12. package/runtime/scripts/api/git/cat-file.mjs +5 -0
  13. package/runtime/scripts/api/sops/resolve-sops.mjs +7 -0
  14. package/runtime/scripts/api/sops/seal.mjs +1 -1
  15. package/runtime/scripts/hfs/architecture/backend-app-files.mjs +110 -0
  16. package/runtime/scripts/hfs/architecture/backend.mjs +20 -3
  17. package/runtime/scripts/hfs/architecture/background-unowned.mjs +157 -41
  18. package/runtime/scripts/hfs/architecture/canon-clones.mjs +240 -0
  19. package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +143 -0
  20. package/runtime/scripts/hfs/architecture/clones.mjs +17 -8
  21. package/runtime/scripts/hfs/architecture/connection-map.mjs +14 -12
  22. package/runtime/scripts/hfs/architecture/context-map.mjs +25 -14
  23. package/runtime/scripts/hfs/architecture/context-owner.mjs +1 -2
  24. package/runtime/scripts/hfs/architecture/dead-exports.mjs +14 -9
  25. package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +104 -0
  26. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +30 -11
  27. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +16 -3
  28. package/runtime/scripts/hfs/architecture/machine-ast.mjs +17 -0
  29. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +21 -12
  30. package/runtime/scripts/hfs/architecture/register-once.mjs +13 -5
  31. package/runtime/scripts/hfs/architecture/required-files.mjs +2 -1
  32. package/runtime/scripts/hfs/architecture/staged-consumers.mjs +37 -0
  33. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +3 -6
  34. package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -6
  35. package/runtime/scripts/hfs/architecture/tiers.mjs +8 -1
  36. package/runtime/scripts/hfs/architecture/typescript.mjs +1 -8
  37. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +1 -11
  38. package/runtime/scripts/hfs/check.mjs +18 -26
  39. package/runtime/scripts/hfs/manifest-shape.mjs +27 -2
  40. package/runtime/scripts/hfs/path-findings.mjs +32 -2
  41. package/runtime/scripts/hfs/rules/database-migrations.mjs +36 -35
  42. package/runtime/scripts/hfs/rules/database.mjs +2 -2
  43. package/runtime/scripts/hfs/rules/edition.mjs +5 -5
  44. package/runtime/scripts/hfs/rules/proof-commands.mjs +2 -0
  45. package/runtime/scripts/hfs/rules/secrets.mjs +13 -0
  46. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +128 -0
  47. package/runtime/scripts/hfs/rules/stacks.mjs +13 -8
  48. package/runtime/scripts/hfs/secret-document.mjs +97 -0
  49. package/runtime/scripts/hfs/secret-errors.mjs +20 -0
  50. package/runtime/scripts/hfs/secret-files.mjs +124 -0
  51. package/runtime/scripts/hfs/secret-init.mjs +192 -0
  52. package/runtime/scripts/hfs/secret-output.mjs +100 -0
  53. package/runtime/scripts/hfs/secret-sops.mjs +98 -0
  54. package/runtime/scripts/hfs/secret-store.mjs +161 -0
  55. package/runtime/scripts/hfs/secret.mjs +135 -195
  56. package/runtime/scripts/hfs/slot-imports.mjs +3 -1
  57. package/runtime/scripts/hfs/slot-required.mjs +13 -0
  58. package/runtime/scripts/hfs/slot-semantic-problems.mjs +2 -1
  59. package/runtime/scripts/hfs/tree-findings.mjs +22 -0
  60. package/runtime/scripts/lib/roots.mjs +59 -0
  61. package/runtime/scripts/lib/swarm-stack.mjs +139 -0
  62. package/runtime/scripts/lib/ts-analysis.mjs +20 -0
  63. package/scaffold/add-cli-lite.mjs +10 -11
  64. package/scaffold/add-inbox-lite.mjs +15 -84
  65. package/scaffold/add-plan.mjs +183 -0
  66. package/scaffold/add-table.mjs +44 -5
  67. package/scaffold/add.mjs +33 -166
  68. package/scaffold/app.mjs +5 -5
  69. package/scaffold/image.mjs +2 -2
  70. package/src/main.mjs +12 -6
  71. package/sync/index.mjs +23 -16
  72. package/templates/app/gitignore +1 -0
  73. package/templates/app/gitignore-lite +1 -0
  74. package/templates/app/hooks/husky/pre-commit +7 -12
  75. package/templates/app/skeleton/.sops.yaml +1 -1
  76. package/templates/app/skeleton-lite/.sops.yaml +1 -1
  77. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +16 -0
  78. package/templates/app/skeleton-lite/.starciwork/workspace.yaml +9 -0
  79. package/templates/app/skeleton-lite/README.md +4 -2
  80. package/templates/app/starciwork.gitignore-lite +6 -0
  81. package/templates/be/inbox/database-client.spec.ts.tpl +15 -20
  82. package/templates/be/inbox/database-client.ts.tpl +7 -22
  83. package/templates/be/inbox/service.spec.ts.tpl +16 -20
  84. package/templates/be/inbox/service.ts.tpl +6 -10
  85. package/templates/be/inbox/sql.ts.tpl +2 -2
  86. package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -3
  87. package/templates/be/inbox/webhook-door.ts.tpl +4 -3
  88. package/templates/be/patterns/webhooks/webhook.door.spec.ts.tpl +3 -3
  89. package/templates/be/patterns/webhooks/webhook.door.ts.tpl +3 -3
  90. package/templates/be/skeleton/apps/__app__/src/__app__.env.ts +21 -0
  91. package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +1 -1
  92. package/templates/be/skeleton/apps/__app__/src/app.module.ts +7 -39
  93. package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +42 -0
  94. package/templates/be/skeleton/apps/__app__/src/main.ts +3 -9
  95. package/templates/be/skeleton/apps/cli/src/cli.env.ts +13 -0
  96. package/templates/be/skeleton/apps/cli/src/cli.options.ts +6 -8
  97. package/templates/be/skeleton/apps/cli/src/main.ts +3 -2
  98. package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +6 -6
  99. package/templates/be/skeleton-lite/apps/api/src/api.env.ts +25 -0
  100. package/templates/be/skeleton-lite/apps/api/src/api.options.ts +1 -1
  101. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +6 -42
  102. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +45 -0
  103. package/templates/be/skeleton-lite/apps/api/src/main.ts +4 -11
  104. package/templates/be/skeleton-lite/apps/cli/src/cli.env.ts +17 -0
  105. package/upgrade/file-state.mjs +68 -0
  106. package/upgrade/index.mjs +7 -56
  107. package/runtime/scripts/api/git/log.mjs +0 -5
  108. package/templates/app/skeleton-lite/.starciwork/shell/index.yaml +0 -55
@@ -12,7 +12,7 @@ appliesTo:
12
12
  family: be
13
13
  # The slot ids this topic teaches (knowledge/hfs/slots.yaml); scripts/gates/read-digest.mjs inverts these
14
14
  # into the READ-step slot -> pattern-topic map (knowledge/op-gate.yaml kinds), each slot's topics in index.yaml order.
15
- slots: [be.feature, be.feature.application]
15
+ slots: [be.feature, be.feature.application, be.app.api]
16
16
  provenance:
17
17
  reviewedAt: 2026-10-02
18
18
  sources:
@@ -21,6 +21,7 @@ provenance:
21
21
  - knowledge/hfs/rules.yaml
22
22
  - owner-locked BE convention 2026-09-30
23
23
  - owner decision 2026-10-02 (features grouped by trigger kind; synchronous inside a context)
24
+ - owner ruling 2026-10-14 (app composition standard: options, compose/<area>.ts groups, budgets)
24
25
  - https://docs.nestjs.com/recipes/cqrs
25
26
  limitations: |-
26
27
  Static checks prove the shape of a feature, a message, its handler and the dispatch, and that SQL touches only tables its
@@ -245,6 +246,68 @@ rules:
245
246
  manual:
246
247
  - Confirm the spec asserts the error code of each refusal, not only a status.
247
248
  relatedExamples: [api]
249
+ - id: BE-API-4
250
+ title: An api app parses env separately and composes in area groups; its options hold values and port bindings only
251
+ kind: mandatory
252
+ hfsRules: [R245, R246, R32, R45, R43]
253
+ requirement: |-
254
+ An app's `src/` (slot `be.app.api`, and alike `be.app.worker` and `be.app.cli`) holds five app roles, each within the
255
+ line budget its slot names (owner ruling 2026-10-14):
256
+ - `main.ts` (80 lines) builds `EnvSource` once, imports and calls `read<App>Env` from `<app>.env.ts`, builds the options
257
+ and boots the app.
258
+ - `<app>.env.ts` (150) exports an explicit typed `<App>Env` interface and `read<App>Env(env: EnvSource): <App>Env`
259
+ (for example `ApiEnv` and `readApiEnv`). It holds fail-fast environment parsing only, via the app's `EnvSource` and
260
+ unchanged capability `parse<C>Config` helpers, with no localhost or default host fallback. Secrets use `env.secret`:
261
+ `EnvSource` resolves `<NAME>_FILE` first for `.starcistacks` custody, then plain `<NAME>` when the file key is absent.
262
+ It holds no logic, module composition, business code or feature imports. Only this app file and `main.ts` may read
263
+ `process.env` or `EnvSource`; `platform/config` owns the standard `EnvSource` reader. The role name stays
264
+ `<app>.env.ts`, never `env.ts`; its interface declares its own readonly parsed members, such as
265
+ `readonly server: ReturnType<typeof parseServerConfig>`, structurally compatible with the app's options.
266
+ - `<app>.options.ts` (200) holds the env-derived values plus its port bindings: provider maps such as
267
+ `{ approvalGate: ApprovalGateAdapter }`, with no environment parsing or reads of `process.env` or `EnvSource`.
268
+ Neither it nor `<app>.env.ts` holds an array
269
+ listing `*Module` classes, module or queue registrations (`register`, `forRoot`, `registerQueue`) or scheduler entries,
270
+ or an `imports`, `modules`, `schedulers` or `processors` list. The env file also refuses standalone module or queue
271
+ registration calls (`BE_APP_OPTIONS_COMPOSITION`).
272
+ - `compose/<area>.ts` (120) is a composition group: it exports the module list of one area of the app (the platform, the
273
+ background mechanisms with their message and queue doors, a product area), as a function of the options when it needs
274
+ them, and the providers of the area's global ports when it has any. It is composition only: no business logic, no
275
+ environment read (no `parse<C>Config`, `process.env` or `EnvSource` reads), and its `X.register({ isGlobal: true, ...options.x })` calls
276
+ count as the app root's registrations (R45).
277
+ - `app.module.ts` (250) imports the groups and spreads them into its `imports` (`...platformModules(options)`), registers
278
+ what is left, and binds the app-wide guards and filter.
279
+ A file over its budget is `BE_APP_COMPOSITION_BUDGET`; the budget is absolute, so an app root that outgrows 250 lines
280
+ moves an area into a new group, never into the env or options file. An entrypoint over 80 moves its parsing into the env file.
281
+ rationale: |-
282
+ A long app root invites moving its module lists into the options file, which then mixes configuration with composition
283
+ and grows without a bound. One group per area keeps each composition reviewable, the options pure data, and the root a
284
+ table of contents. The env file keeps typed, fail-fast parsing visible while the entrypoint stays within 80 lines.
285
+ cases:
286
+ - id: case-1
287
+ when: A new api app is scaffolded
288
+ write: 'apps/api/src/compose/platform.ts exports platformModules(options) (clock, logging, i18n, errors, cqrs, http security, database), and app.module.ts spreads ...platformModules(options) before the domain modules and the guards.'
289
+ - id: case-2
290
+ when: The app root passes 250 lines because every module is listed in it
291
+ write: 'Move one area into apps/<app>/src/compose/<area>.ts (for example compose/background.ts with the event bus, the queue and its schedulers, the jobs and the message and queue doors) and spread it in app.module.ts.'
292
+ - id: case-3
293
+ when: 'api.options.ts exports `modules: [...]`, a `BullModule.registerQueue(...)` list or `schedulers: [orderExpirySchedulerOf(...)]`'
294
+ write: 'Refused (BE_APP_OPTIONS_COMPOSITION): keep only the parsed values (orderExpiry: { everyMs, olderThanMs }) and the port map in the options; the list goes into compose/<area>.ts.'
295
+ - id: case-4
296
+ when: An api app reads capability configuration
297
+ write: 'api.env.ts exports interface ApiEnv and readApiEnv(env: EnvSource): ApiEnv (at most 150 lines); main.ts passes its EnvSource to that reader and builds AppOptions from the parsed values and port bindings within 80 lines. No module list or feature import goes into api.env.ts.'
298
+ verification:
299
+ automated:
300
+ - BE_APP_OPTIONS_COMPOSITION
301
+ - BE_APP_COMPOSITION_BUDGET
302
+ - BE_APP_COMPOSITION_ONLY
303
+ - BE_APP_BUSINESS_ROLE
304
+ - BE_MODULE_SHAPE
305
+ - BE_MODULE_HANDLER_REGISTRATION
306
+ - BE_MODULE_PROVIDER_REREGISTRATION
307
+ - BE_CONFIG_OWNER
308
+ manual:
309
+ - Confirm each group is one area of the app and holds no decision a capability should own.
310
+ relatedExamples: [api]
248
311
  - id: BE-OPERATIONS-1
249
312
  title: A versioned operation is declared once in the app's typed operation table
250
313
  kind: mandatory
@@ -41,8 +41,16 @@ rules:
41
41
  nest-commander and never serves: no `NestFactory.create*`, no `.listen()`, no microservice connection, no
42
42
  `@nestjs/platform-*` adapter and no `@nestjs/microservices`. `apps/cli/src/app.module.ts` composes the platform modules
43
43
  the commands need (the database capability over every connection with its entities and migrations, like an api app) and
44
- the static cli feature root module `CliModule`; `apps/cli/src/cli.options.ts` carries what the cli reads from the
45
- environment, the connections it migrates among them.
44
+ the static cli feature root module `CliModule`. `apps/cli/src/cli.env.ts` (at most 150 lines) exports an explicit typed
45
+ `CliEnv` interface and `readCliEnv(env: EnvSource): CliEnv` using `EnvSource` and unchanged capability parse helpers.
46
+ The interface declares its own readonly parsed members with types matching those helpers; the options factory binds
47
+ these values to its connection list.
48
+ It holds fail-fast parsing only, with no localhost or default host fallback, logic, module composition, business code
49
+ or feature imports. Secrets use `env.secret`, with `<NAME>_FILE` first for `.starcistacks` custody, then plain `<NAME>`
50
+ when the file key is absent. `main.ts` (at most 80 lines) builds `EnvSource` once and imports and calls `readCliEnv`.
51
+ Only these two app files may read the environment; the role name stays `cli.env.ts`, never `env.ts`.
52
+ `apps/cli/src/cli.options.ts` carries the env-derived values and port bindings, the connections it migrates among them,
53
+ with no parsing or reads of `process.env` or `EnvSource`; compose groups never read the environment either.
46
54
  rationale: |-
47
55
  One image for every operator action gives one place to find, deploy and audit them, and a process that never listens
48
56
  cannot become a second, unguarded door into the application.
@@ -52,7 +60,9 @@ rules:
52
60
  write: |-
53
61
  // be/apps/cli/src/main.ts
54
62
  async function bootstrap(): Promise<void> {
55
- await CommandFactory.run(AppModule.register(parseCliOptions(EnvSource.fromProcess())), new JsonLinesLogger())
63
+ const env = EnvSource.fromProcess()
64
+ const options = createCliAppOptions(readCliEnv(env))
65
+ await CommandFactory.run(AppModule.register(options), new JsonLinesLogger())
56
66
  }
57
67
  void bootstrap()
58
68
  - id: case-2
@@ -60,7 +70,7 @@ rules:
60
70
  write: 'Refused (BE_CLI_BOOTSTRAP): the cli app boots only through CommandFactory.run.'
61
71
  - id: case-3
62
72
  when: A back end declares a connection and has no apps/cli
63
- write: 'Refused (BE_CLI_REQUIRED): add be/apps/cli with main.ts, app.module.ts, cli.options.ts and its Dockerfile, declared as { "name": "cli", "kind": "cli" } in hfs.json sides.be.apps.'
73
+ write: 'Refused (BE_CLI_REQUIRED): add be/apps/cli with main.ts, app.module.ts, cli.env.ts, cli.options.ts and its Dockerfile, declared as { "name": "cli", "kind": "cli" } in hfs.json sides.be.apps.'
64
74
  verification:
65
75
  automated:
66
76
  - BE_CLI_REQUIRED
@@ -196,10 +206,18 @@ files:
196
206
  - path: apps/cli/src/cli.options.ts
197
207
  slot: be.app.cli
198
208
  optional: true
199
- role: what the cli reads from the environment, the connection list it migrates among them
209
+ role: env-derived values and port bindings without parsing, the connection list it migrates among them
200
210
  rules:
201
211
  - BE_CLI_REQUIRED
202
212
  - BE_SCHEMA_AUTHORITY
213
+ - path: apps/cli/src/cli.env.ts
214
+ slot: be.app.cli
215
+ optional: true
216
+ role: 'exports interface CliEnv and readCliEnv(env: EnvSource): CliEnv; fail-fast parsing only, no localhost fallback, logic, composition, business code or feature imports; secrets use env.secret with _FILE first'
217
+ rules:
218
+ - BE_CONFIG_OWNER
219
+ - BE_APP_OPTIONS_COMPOSITION
220
+ - BE_APP_COMPOSITION_BUDGET
203
221
  - path: src/features/cli/index.ts
204
222
  slot: be.cli
205
223
  optional: true
@@ -3,25 +3,34 @@ id: be.feature.webhooks
3
3
  title: Provider webhooks
4
4
  purpose: |-
5
5
  Given a delivery a payment, identity or shipping provider POSTs to us, how is it received? A webhook is a feature KIND:
6
- `be/src/features/webhooks/<provider>/` holds one signed door that proves the delivery came from the provider (signature and
7
- replay window), then hands it to exactly ONE domain intake method. That method records the delivery (deduplicated through the
8
- inbox) and publishes the domain event with `eventBus.publish(event, tx)` inside its own transaction, because publishing is
9
- allowed only from `modules/domain`. The door writes nothing and decides nothing; everything after the proof is an event.
6
+ `be/src/features/webhooks/<provider>/` holds one signed door whose first call proves the delivery came from the provider
7
+ (signature and replay window), either separately or inside its ONE domain method that combines verification and intake. The
8
+ intake records the delivery (deduplicated through the inbox) and publishes a typed receipt event with
9
+ `eventBus.publish(event, tx)` inside its own transaction, because publishing is allowed only from `modules/domain`.
10
+ A reactor or job consumes the durable receipt and dispatches the processing command. Neither the door, its HTTP module,
11
+ nor its called domain intake dispatches on a command/query bus or feature handler. A provider may receive a declared fixed `PROVIDER_ACK` body;
12
+ a subscription provider may also use one GET verifier that echoes its challenge result.
10
13
  appliesTo:
11
14
  - backend
12
15
  family: be
13
16
  provenance:
14
- reviewedAt: 2026-10-02
17
+ reviewedAt: 2026-10-14
15
18
  sources:
16
19
  - knowledge/hfs/README.md
17
20
  - knowledge/hfs/slots.yaml
18
21
  - knowledge/hfs/rules.yaml
19
22
  - knowledge/patterns/be/api-auth.yaml
20
23
  - knowledge/patterns/be/idempotency.yaml
24
+ - knowledge/patterns/be/event-bus.yaml
25
+ - knowledge/patterns/be/reactors.yaml
26
+ - knowledge/patterns/be/jobs.yaml
21
27
  - owner decision 2026-10-02 (feature kinds: webhooks, realtime, reactors, saga, jobs, cli; no kind imports another)
28
+ - owner decision 2026-10-14 (literal provider ACK, one optional GET challenge, durable inbox handoff before reactor/job command dispatch)
22
29
  limitations: |-
23
- Static checks prove the door's shape, that the proof is its first statement and that the only thing it can call after it is
24
- one domain service method. They do not prove the HMAC scheme matches the provider's documentation, that the replay window
30
+ Static checks prove the door's shape, that an unconditional verification call is its first statement and that it makes
31
+ one intake call, either after a separate proof or combined with it. They resolve direct command/query-bus and feature-handler receivers in the called
32
+ domain intake; helper calls, dynamic receivers and unavailable method bodies need semantic review. They do not prove that a domain verifier completes proof
33
+ before intake, that the signature scheme matches the provider's documentation, that the replay window
25
34
  is the right length or that the intake is idempotent; the provider fake of the test world and the three
26
35
  `webhooks/*` scenarios prove those against the real stack.
27
36
  rules:
@@ -33,7 +42,8 @@ rules:
33
42
  A provider webhook lives in `be/src/features/webhooks/<provider>/` (slot `be.feature.webhooks`), enabled by declaring the pattern
34
43
  `webhooks` in `hfs.json` `sides.be.patterns`. The folder holds `index.ts` (the module class an app composes), `transport/http/<provider>-http.module.ts`, `transport/http/<provider>.webhook.ts` (the
35
44
  door), `<provider>.webhook.spec.ts` (its spec — the kind's door has no service of its own, so the door carries it) and an optional `transport/http/dto/` of typed
36
- payload classes (`<event>.request.ts`). Nothing else exists there: no application layer, no handler, no consumer. The kind
45
+ payload classes (`<event>.request.ts`). `PROVIDER_ACK`, when needed, is an exported typed literal constant in the door or
46
+ its own HTTP module. Nothing else exists there: no application layer, no handler, no consumer. The kind
37
47
  imports only `modules/*`, never another kind, and only an api-kind app composes `<provider>-http.module.ts`.
38
48
  rationale: |-
39
49
  A webhook is an external caller we do not control, so its door is kept apart from the api features a client uses. One place
@@ -76,24 +86,128 @@ rules:
76
86
  - Confirm the provider endpoint is one door and that a second endpoint of the same provider is a second door only when it has a different proof.
77
87
  relatedExamples: [webhooks]
78
88
  - id: BE-WEBHOOK-2
79
- title: The door verifies first, then makes one domain call
89
+ title: The door verifies first and hands the delivery to one domain intake
80
90
  kind: mandatory
81
91
  hfsRules: [R182, R183, R41, R42, R203]
82
92
  requirement: |-
83
- `<provider>.webhook.ts` is one `@Controller` with one `@Post` handler declared
84
- `@Public({ reason: PublicReason.SignedWebhook })`, thin (R203 `BE_FEATURE_THIN`). The handler's FIRST statement is the unconditional
85
- `this.<signature>.verify({ provider, rawBody, signature, timestamp })` of the `WebhookSignatureService` of `platform/http-security`
86
- (HMAC-SHA256 over `<timestamp>.<raw body>` with the provider's secret, a constant-time comparison and a replay window on the signed timestamp; it throws before anything else runs). The provider key and its secret and tolerance are options of `platform/http-security` (`options.webhooks[provider]`). The
87
- second and last call is one method of a domain service, the intake. The door injects only that service and the signature
88
- service (injected with `@InjectWebhookSignature()`), holds no branch, loop or `try`, calls nothing else (no parsing, mapping or publishing) and returns nothing: the
89
- provider gets its status code and an empty body. `@RateLimit(RateTier.Strict)` is required by the signed-webhook reason.
93
+ `<provider>.webhook.ts` is one `@Controller` with exactly one `@Post` handler and optionally one `@Get` subscription
94
+ challenge handler. Each handler is declared
95
+ `@Public({ reason: PublicReason.SignedWebhook })`, thin (R203 `BE_FEATURE_THIN`). The handler's FIRST statement is an unconditional
96
+ direct verification call, awaited when async: either `this.<signature>.verify({ provider, rawBody, signature, timestamp })`
97
+ of the `WebhookSignatureService` of `platform/http-security` or a domain verifier. No data access, nested call or other work
98
+ may run in the door before that proof call. The shared service uses HMAC-SHA256 over `<timestamp>.<raw body>`, a constant-time
99
+ comparison and a replay window on the signed timestamp; its provider key, secret and tolerance are `platform/http-security`
100
+ options (`options.webhooks[provider]`). A domain verifier owns the provider's proof when secrets belong to individual
101
+ connections, as for the billing payment webhooks; it refuses an invalid delivery before intake.
102
+ A domain verifier is a method on a service declared in a domain `<name>.service.ts` whose declared class name ends in
103
+ `Verifier` or `verify`, or an exact domain service method declared by the adjacent `<provider>-http.module.ts` as
104
+ `export const webhookVerifiers = [BillingService.prototype.receiveCassoWebhook] as const`. The array names TypeScript
105
+ method symbols, never strings; a local alias or a class from the platform, integrations or a package grants no domain proof.
106
+ A separate verification call is followed by exactly one domain intake call. A domain method that verifies and then performs
107
+ intake may be the first and only call; a separate domain verification call does not count as intake. The door injects only
108
+ domain services and, when used, the signature service (injected with `@InjectWebhookSignature()`), holds no branch, loop or
109
+ `try`, and calls nothing else (no parsing, mapping or publishing). Neither the door nor its HTTP module imports or injects
110
+ `CommandBus`, `QueryBus` from `@nestjs/cqrs`, or a feature command handler. The called domain intake must not dispatch on
111
+ a command or query bus or feature handler either; it records the inbox receipt and outbox event so a reactor or job can dispatch after commit.
112
+ The POST handler returns nothing (an empty body) or exactly `return PROVIDER_ACK`. That exported constant belongs to the
113
+ same webhook feature, is declared with `as const` or a readonly literal type, and contains only literal object values or
114
+ a literal string. Calls, interpolated templates, spreads, input-derived values, inline returned objects and constants of
115
+ another feature are refused. A provider acknowledgement does not weaken the first-statement proof requirement.
116
+ The optional GET starts with a declared domain verifier's challenge call, awaited when async, and returns exactly that
117
+ call's result: `const challenge = this.verifier.challenge(query); return challenge`, or directly returns that call.
118
+ It makes no intake call and computes no other response. `@RateLimit(RateTier.Strict)` is required on both signed-webhook handlers.
90
119
  rationale: |-
91
- The signature is the authentication of a public door. Making the proof the first statement removes every path to the intake
92
- that skips it, and a door with one call and no decision has nothing left to test except the proof and the hand-over.
120
+ The signature is the authentication of a public door. Making verification the first statement removes every door path that
121
+ skips it; a combined domain method must finish that proof before intake. Keeping one intake and no decision leaves the door
122
+ with only the proof and the hand-over to test.
93
123
  cases:
94
124
  - id: case-1
95
125
  when: A delivery whose raw body must be hashed
96
126
  write: 'enable the raw body for the app (`NestFactory.create(..., { rawBody: true })` in the api app) and read `request.rawBody`; never hash a re-serialized `body`.'
127
+ - id: case-2
128
+ when: A payment provider uses a different secret for each billing connection
129
+ write: 'declare BillingService.prototype.receiveCassoWebhook in the adjacent HTTP module webhookVerifiers array; the first and only handler statement awaits that method, which resolves the connection secret, verifies the provider proof and delegates to the intake. The door never reads the connection or payment data itself.'
130
+ - id: case-3
131
+ when: 'A provider requires exactly {"success": true} instead of an empty acknowledgement'
132
+ write: |-
133
+ In the webhook door's own file declare `export const PROVIDER_ACK = { success: true } as const`.
134
+ After the first verification call and the one domain intake call, return `PROVIDER_ACK` with the provider's required
135
+ status code. `return { success: true }`, `return { success: delivery.accepted }`, a spread, a computed constant,
136
+ and an ACK imported from another feature remain findings.
137
+ - id: case-4
138
+ when: A Meta-style provider uses GET subscription verification and POST deliveries
139
+ write: |-
140
+ Worked tree (the webhook and reactor are separate kinds; the app composes their modules):
141
+ be/src/features/webhooks/meta/index.ts
142
+ be/src/features/webhooks/meta/transport/http/meta-http.module.ts
143
+ be/src/features/webhooks/meta/transport/http/meta.webhook.ts
144
+ be/src/features/webhooks/meta/transport/http/meta.webhook.spec.ts
145
+ be/src/features/webhooks/meta/transport/http/dto/meta-challenge.request.ts
146
+ be/src/features/webhooks/meta/transport/http/dto/meta-event.request.ts
147
+ be/src/modules/domain/meta/index.ts
148
+ be/src/modules/domain/meta/meta-verifier.service.ts
149
+ be/src/modules/domain/meta/meta.service.ts
150
+ be/src/modules/events/core/meta-delivery-recorded.event.ts
151
+ be/src/features/reactors/meta-intake/index.ts
152
+ be/src/features/reactors/meta-intake/meta-intake.module.ts
153
+ be/src/features/reactors/meta-intake/application/process-meta-delivery.command.ts
154
+ be/src/features/reactors/meta-intake/application/process-meta-delivery.handler.ts
155
+ be/src/features/reactors/meta-intake/application/process-meta-delivery.contracts.ts
156
+ be/src/features/reactors/meta-intake/transport/message/meta-delivery-recorded.consumer.ts
157
+ be/src/features/reactors/meta-intake/transport/message/meta-intake-message.module.ts
158
+
159
+ meta.webhook.ts:
160
+ import { Body, Controller, Get, Headers, HttpCode, HttpStatus, Post, Query, Req } from "@nestjs/common"
161
+ import type { RawBodyRequest } from "@nestjs/common"
162
+ import type { Request } from "express"
163
+ import { Public, PublicReason } from "@modules/domain/identity"
164
+ import { MetaService, MetaVerifier } from "@modules/domain/meta"
165
+ import { RateLimit, RateTier } from "@modules/platform/http-security"
166
+ import { MetaChallengeRequest } from "./dto/meta-challenge.request"
167
+ import { MetaEventRequest } from "./dto/meta-event.request"
168
+
169
+ export const PROVIDER_ACK = "EVENT_RECEIVED" as const
170
+
171
+ @Controller("webhooks/meta")
172
+ export class MetaWebhook {
173
+ constructor(private readonly verifier: MetaVerifier, private readonly deliveries: MetaService) {}
174
+
175
+ @Get()
176
+ @Public({ reason: PublicReason.SignedWebhook })
177
+ @RateLimit(RateTier.Strict)
178
+ challenge(@Query() query: MetaChallengeRequest): string {
179
+ const challenge = this.verifier.challenge(query)
180
+ return challenge
181
+ }
182
+
183
+ @Post()
184
+ @HttpCode(HttpStatus.OK)
185
+ @Public({ reason: PublicReason.SignedWebhook })
186
+ @RateLimit(RateTier.Strict)
187
+ async receive(
188
+ @Req() request: RawBodyRequest<Request>,
189
+ @Headers("x-hub-signature-256") signature: string | undefined,
190
+ @Body() delivery: MetaEventRequest,
191
+ ): Promise<typeof PROVIDER_ACK> {
192
+ await this.verifier.verify({ rawBody: request.rawBody, signature })
193
+ await this.deliveries.recordMetaDelivery(delivery)
194
+ return PROVIDER_ACK
195
+ }
196
+ }
197
+
198
+ meta-http.module.ts:
199
+ import { Module } from "@nestjs/common"
200
+ import { MetaWebhook } from "./meta.webhook"
201
+
202
+ @Module({ controllers: [MetaWebhook] })
203
+ export class MetaHttpModule {}
204
+
205
+ MetaVerifier is declared in meta-verifier.service.ts. Its challenge method proves the subscription mode and verify
206
+ token before returning the challenge field unchanged; its verify method owns Meta's signature proof and delivery
207
+ freshness policy. MetaService.recordMetaDelivery only records the delivery and its outbox receipt in one transaction
208
+ (case-1 of BE-WEBHOOK-3). The reactor consumes that receipt and dispatches ProcessMetaDeliveryCommand after commit.
209
+ The provider's real GET and POST proof schemes belong in the verifier, rather than being assumed to match the shared
210
+ timestamp/HMAC convention. The api app composes MetaHttpModule; an api or worker app composes MetaIntakeMessageModule.
97
211
  verification:
98
212
  automated:
99
213
  - BE_WEBHOOK_SHAPE
@@ -102,33 +216,58 @@ rules:
102
216
  - BE_INPUT_BOUNDED
103
217
  - BE_FEATURE_THIN
104
218
  manual:
105
- - Review that the raw body, not a re-serialized one, is verified, and that the replay window matches the provider's tolerance.
219
+ - Review that the raw body, not a re-serialized one, is verified, that the replay window matches the provider's tolerance, and that a domain verifier refuses the delivery before intake or business data access.
220
+ - Trace intake helpers and dynamic dispatch that static resolution cannot inspect; none may dispatch a command before the receipt is durable.
106
221
  relatedExamples: [webhooks]
107
222
  - id: BE-WEBHOOK-3
108
- title: The intake publishes the event inside its own transaction
223
+ title: The intake records the inbox receipt; a reactor or job dispatches after commit
109
224
  kind: mandatory
110
225
  hfsRules: [R80, R152, R151]
111
226
  requirement: |-
112
- The domain intake method (`PaymentService.acceptNotification`) is the delivery method of R80: its first awaited expression
227
+ The sanctioned path is `verify -> inbox.record -> reactor/job dispatches the command`. `inbox.record` describes the
228
+ domain intake's durable recording responsibility, not a new method on the shared `Inbox` port: the real port has `claim`.
229
+ The domain intake method (`PaymentService.recordPaymentDelivery`) is the delivery method of R80: its first awaited expression
113
230
  claims `(source, eventId)` in the `Inbox` and it returns when the claim answers false, so a redelivery is a no-op. It then
114
- records the delivery and calls `this.eventBus.publish(PaymentConfirmedEvent.create({...}), manager)` inside the same
231
+ records the provider payload or a durable receipt reference and calls
232
+ `this.eventBus.publish(PaymentDeliveryRecordedEvent.create({...}), manager)` inside the same
115
233
  transaction (`entityManager.transaction`). The events are typed classes in `modules/events/<service>/<event>.event.ts` that
116
- equal their `be/contracts/<service>/events.json` entry (`payment.confirmed`, `payment.failed`). The webhook door never
117
- imports `modules/events` or the event bus.
234
+ equal their `be/contracts/<service>/events.json` entry (`payment.delivery-recorded`). The webhook door never imports
235
+ `modules/events` or the event bus. The door, HTTP module and called intake do not reach a command or query bus or feature
236
+ handler. A reactor consumes the receipt event and dispatches its own command, forwarding the delivered `eventId` and
237
+ receipt reference; its domain service claims that separate consumer delivery before processing the receipt. A job may
238
+ instead consume a transactionally enqueued receipt reference under the queue and fenced-job patterns.
118
239
  rationale: |-
119
240
  Recording the delivery and writing the outbox row in one transaction means a crash cannot lose or duplicate the event, and
120
241
  keeping the publish in `modules/domain` keeps one place that owns the transaction boundary.
121
242
  cases:
122
243
  - id: case-1
123
- when: A confirmed payment arrives
124
- write: 'acceptNotification claims the inbox row, updates the payment row and publishes PaymentConfirmedEvent in one transaction; a second delivery of the same eventId changes nothing.'
244
+ when: A payment provider retries a delivery whose processing belongs in a reactor
245
+ write: |-
246
+ The POST handler verifies first, then calls PaymentService.recordPaymentDelivery(delivery) once. That domain method
247
+ runs entityManager.transaction(async (manager) => { ... }): its first awaited statement claims
248
+ this.inbox.claim(PAYMENT_SOURCE, delivery.eventId, manager), and a false answer returns. With the same manager it
249
+ saves the durable receipt and publishes PaymentDeliveryRecordedEvent carrying that receipt's id. Its method never
250
+ dispatches CommandBus.execute. The outbox relay delivers the committed event to
251
+ features/reactors/payment-intake/transport/message/payment-delivery-recorded.consumer.ts:
252
+
253
+ async handle(delivery: EventDelivery<PaymentDeliveryRecordedEvent>): Promise<void> {
254
+ await this.commandBus.execute(new ProcessPaymentDeliveryCommand({
255
+ request: { eventId: delivery.eventId, receiptId: delivery.event.payload.receiptId },
256
+ }))
257
+ }
258
+
259
+ The command's handler delegates to its domain service; that service claims the reactor delivery id and processes the
260
+ saved receipt in its transaction. A duplicate provider POST records nothing more, and a duplicate relay delivery
261
+ processes nothing twice. The command, handler and consumer live in the reactor's own kind; no feature imports the
262
+ webhook kind. The equivalent job path enqueues a receipt reference in the recording transaction and processes it
263
+ through the job's platform claim and fencing, never synchronously in the POST handler.
125
264
  verification:
126
265
  automated:
127
266
  - BE_INBOX_DEDUPE_MISSING
128
267
  - BE_OUTBOX_WRITE_TX
129
268
  - BE_EVENT_CLASS_CONTRACT
130
269
  manual:
131
- - Confirm each provider event kind maps to exactly one published event and that an unknown kind is acknowledged and ignored, not published.
270
+ - Confirm each accepted provider delivery records at most one receipt and outbox event, and the reactor or job alone dispatches processing commands.
132
271
  relatedExamples: [webhooks]
133
272
  - id: BE-WEBHOOK-4
134
273
  title: Three behaviours are proven on the test world
@@ -140,8 +279,9 @@ rules:
140
279
  refused and nothing is published) and `accepted-delivery-publishes-once` (a valid delivery publishes the event once and a
141
280
  redelivery publishes nothing more). The provider is the network-edge fake of the test world (`world.fake.<provider>`), which
142
281
  signs with the test secret; a real provider is never called. The unit spec `<provider>.webhook.spec.ts` covers the door
143
- itself with the Nest testing module, a real `WebhookSignatureService` configured with a test secret and a double of the
144
- domain service.
282
+ itself with the Nest testing module and the verifier it uses: a real `WebhookSignatureService` configured with a test secret
283
+ or a real domain verifier configured with the connection's test secret, plus a double of a separate intake service. A method
284
+ that combines verification and intake has its domain service spec proving refusal before intake and the door spec proving delegation.
145
285
  rationale: |-
146
286
  The door has no logic of its own, so the risk is in the wiring: the raw body, the headers, the real signature scheme and the
147
287
  transaction behind the intake. Only a signed request through the real app proves it.
@@ -167,7 +307,7 @@ files:
167
307
  - path: src/features/webhooks/<provider>/transport/http/<provider>.webhook.ts
168
308
  slot: be.feature.webhooks.http
169
309
  template: webhooks/webhook.door.ts.tpl
170
- role: The signed door, one controller with one POST handler.
310
+ role: The signed door, one controller with one POST handler and optionally one GET challenge verifier; an own-feature PROVIDER_ACK is a fixed literal.
171
311
  rules: [BE_WEBHOOK_SHAPE, BE_WEBHOOK_UNVERIFIED, BE_DEFAULT_DENY, BE_INPUT_BOUNDED]
172
312
  spec: src/features/webhooks/<provider>/transport/http/<provider>.webhook.spec.ts
173
313
  - path: src/features/webhooks/<provider>/transport/http/<provider>.webhook.spec.ts
@@ -181,12 +321,12 @@ files:
181
321
  template: webhooks/webhook.request.ts.tpl
182
322
  - path: src/modules/platform/http-security/webhook-signature.service.ts
183
323
  slot: be.platform
184
- role: WebhookSignatureService, the one owner of the signature and replay proof.
324
+ role: WebhookSignatureService, the shared signature and replay proof when provider secrets are platform options.
185
325
  spec: src/modules/platform/http-security/webhook-signature.service.spec.ts
186
326
  optional: true
187
327
  - path: src/modules/domain/<capability>/<capability>.service.ts
188
328
  slot: be.domain
189
- role: The intake method, which claims the inbox and publishes the event in its transaction.
329
+ role: The domain verifier or intake method, which may combine per-connection proof with inbox claim, durable receipt recording and outbox publishing in its transaction; no command/query-bus or feature-handler dispatch.
190
330
  rules: [BE_INBOX_DEDUPE_MISSING, BE_OUTBOX_WRITE_TX]
191
331
  optional: true
192
332
  - path: src/modules/events/<service>/<event>.event.ts
@@ -194,9 +334,24 @@ files:
194
334
  role: The typed event the intake publishes.
195
335
  rules: [BE_EVENT_CLASS_CONTRACT]
196
336
  optional: true
337
+ - path: src/features/reactors/<reactor>/transport/message/<event>.consumer.ts
338
+ slot: be.transport.message
339
+ role: Optional reactor consumer of the durable receipt event; forwards its eventId and receipt reference in one command after commit.
340
+ rules: [BE_EVENT_CONSUMER_SHAPE, BE_FEATURE_THIN]
341
+ optional: true
197
342
  - path: src/tests/e2e/<area>/<provider>-webhook.e2e-spec.ts
198
343
  slot: be.tests.e2e
199
344
  role: "Titles webhooks/<scenario>: ... prove the pattern against the provider fake."
200
345
  spec: webhooks/bad-signature-is-refused, webhooks/replay-is-refused, webhooks/accepted-delivery-publishes-once
201
346
  rules: [BE_PATTERN_SPEC_MISSING]
202
347
  optional: true
348
+ sourceMaintenance: >-
349
+ The add webhook verb keeps the default empty acknowledgement and does not add a consumer. In full, the generated POST
350
+ verifies first and calls the supplied domain service's record<Provider>Delivery once. Implement that method as the
351
+ transactional inbox receipt/outbox recording intake above, then add its reactor or job separately.
352
+ In lite, the generated POST verifies first and calls <Provider>InboxService.record<Provider>Delivery. This record-only
353
+ domain intake uses recordWebhookDelivery to insert a pending provider inbox row atomically in Supabase Postgres, unique on
354
+ (provider, delivery_id), without processing it or marking it complete. Add a reactor or job separately to consume the
355
+ committed pending row and dispatch the processing command (reactors.yaml, jobs.yaml). The --service option validates the
356
+ future domain processing owner; the webhook writer does not import or call that owner synchronously and does not
357
+ synthesize a consumer.
@@ -174,6 +174,9 @@ rules:
174
174
  component.tsx. Pure blocks, composites, branches and leaves use index.tsx only and never gain an
175
175
  empty twin. classNames.ts is present only when the unit owns class strings. Budgets: component.tsx 300 lines,
176
176
  connected index.tsx 200 lines, at most 6 data hooks and 6 useState per connected unit.
177
+ A composition-only feature may render an existing public pure component owner directly instead of creating a
178
+ forwarding component.tsx. The route-state overlay host of FE-BOUNDARY-5 renders components/branches/OverlayHost;
179
+ a connected block still requires its exact private Base twin.
177
180
  rationale: |
178
181
  Stable basenames make class names and the connected and presentational split discoverable without forwarding
179
182
  twins, and budgets keep a unit reviewable.
@@ -245,7 +245,7 @@ BE_APP_BUSINESS_ROLE:
245
245
  BE_APP_COMPOSITION_ONLY:
246
246
  title: "Composition root has stray source"
247
247
  title_vi: "Gốc ghép nối có tệp lạc chỗ"
248
- meaning_vi: "Mã trong ứng dụng gốc không phải tệp mà slot ứng dụng cho phép (main.ts, app.module.ts, <app>.options.ts; operations.ts ở ứng dụng API)."
248
+ meaning_vi: "Mã trong ứng dụng gốc không phải tệp mà slot ứng dụng cho phép (main.ts, app.module.ts, <app>.env.ts, <app>.options.ts, compose/<area>.ts; operations.ts ở ứng dụng API)."
249
249
  causes_vi:
250
250
  - "Thêm tệp không đúng tên vào thư mục ứng dụng gốc"
251
251
  nextStep_vi: "Op vừa tạo ra bản này sẽ được chạy lại để sửa theo lời nhắn của bộ kiểm tra; không cần ai can thiệp thêm."
@@ -802,11 +802,11 @@ DB_DYNAMIC_DDL:
802
802
  kind: check-finding
803
803
 
804
804
  DB_MIGRATION_SHAPE:
805
- title: "A migration is named `<ts14>_<kebab>.sql`, stamped in valid past UTC, ordered after the base's migrations, and immutable once on the base"
805
+ title: "A migration is named `<ts14>_<kebab>.sql`, stamped in valid UTC, appended after HEAD's active migrations, and immutable once on the base"
806
806
  title_vi: "Migration sai hình dạng hoặc bị sửa"
807
- meaning_vi: "`<path>` vi phạm luật migration: tên phải là `<ts14>_<kebab>.sql`, stamp là thời điểm UTC thật không ở tương lai, migration mới xếp sau mọi migration trên base, và migration đã lên base là bất biến (sửa hay xoá đều là finding)."
807
+ meaning_vi: "`<path>` vi phạm luật migration: tên phải là `<ts14>_<kebab>.sql`, stamp là thời điểm UTC thật và không trùng giữa các migration đang hoạt động; migration mới xếp sau chuỗi đang hoạt động đã commit trên HEAD. Archive ghi nhận migration đã nghỉ khỏi chuỗi, nên baseline thay thế có thể mang stamp trước archive. Migration đã lên base là bất biến (sửa hay xoá mà không archive đều là finding)."
808
808
  causes_vi:
809
- - "Vi phạm luật L02: `supabase/migrations/<ts14>_<kebab>.sql`; stamp tăng nghiêm ngặt so với base; file đã lên base không được đổi."
809
+ - "Vi phạm luật L02: `supabase/migrations/<ts14>_<kebab>.sql`; stamp mới tăng nghiêm ngặt so với chuỗi đang hoạt động đã commit trên HEAD, không so với đồng hồ, thời gian commit hay stamp trên remote; file đã lên base không được đổi."
810
810
  nextStep_vi: "Tạo migration mới bằng the Supabase CLI new-migration command, không sửa/đổi tên/xoá migration đã lên base; nếu cần đổi nội dung, viết migration mới."
811
811
  owner: op-retry
812
812
  kind: check-finding
@@ -2055,6 +2055,17 @@ HFS_STACKS_SHAPE:
2055
2055
  owner: op-retry
2056
2056
  kind: check-finding
2057
2057
 
2058
+ HFS_STARCIWORK_LITE_FORBIDDEN:
2059
+ title: "lite .starciwork holds only workspace.yaml and brand/"
2060
+ title_vi: "Work của lite"
2061
+ meaning_vi: "Trong ứng dụng edition lite, .starciwork chỉ được giữ workspace.yaml, .gitignore và brand/** (bản ghi bản vẽ, tham chiếu và tài sản thương hiệu). Cây này không bắt buộc khi ứng dụng chưa có bản ghi thương hiệu."
2062
+ causes_vi:
2063
+ - "Ứng dụng lite còn index.yaml, shell/, features/, _resources/, uat hoặc br trong .starciwork"
2064
+ - "Có tệp khác nằm ngoài workspace.yaml, brand/ và .gitignore trong Work của lite"
2065
+ nextStep_vi: "Giữ nguyên workspace.yaml, brand/** và .gitignore, chuyển nội dung khác ra khỏi .starciwork hoặc nâng ứng dụng lên edition full nếu cần các bản ghi đó, rồi chạy lại kiểm tra."
2066
+ owner: op-retry
2067
+ kind: check-finding
2068
+
2058
2069
  HFS_TOOL_CONFIG_LOCAL:
2059
2070
  title: "Configs only call the factories; no local rules"
2060
2071
  title_vi: "Kho tự đặt luật cục bộ"
@@ -0,0 +1,5 @@
1
+ // cat-file.mjs — `git cat-file <args>`: the type, size or content of repository objects (-e, -p, --batch).
2
+ import { runGit } from './lib.mjs';
3
+
4
+ /** The spawn result {status, stdout, stderr, error} of `git cat-file ...args`; options are runGit's (cwd or dir, config, timeout, maxBuffer, input, encoding, env, git). */
5
+ export const catFile = (args = [], options = {}) => runGit(['cat-file', ...args], options);
@@ -0,0 +1,7 @@
1
+ // resolve-sops.mjs — find the sops executable: PATH, and on Windows the WinGet links and packages.
2
+ import { resolveSops as resolve } from './lib.mjs';
3
+
4
+ /** The absolute sops path, or null. `options`: platform, pathext (PATHEXT names), wingetPackageTree (the full package tree), filesystem (a probe). */
5
+ export function resolveSops(env = process.env, options = {}) {
6
+ return resolve(env, options);
7
+ }
@@ -1,4 +1,4 @@
1
- // seal.mjs - one `sops encrypt` of a plaintext document held in memory: the canon command `starci app secret set` and `starci app secret gen`
1
+ // seal.mjs - one `sops encrypt` of a plaintext document held in memory: `starci app secret set` (including --generate)
2
2
  // (scripts/hfs/secret.mjs). The plaintext travels on the child's stdin and never on its argument list, so it is not visible in a
3
3
  // process listing; the ciphertext comes back on stdout.
4
4
  import { spawnSync } from 'node:child_process';