@starci/hfs 4.2.16 → 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.
- package/CHANGELOG.md +21 -0
- package/README.md +7 -0
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +4 -4
- package/runtime/knowledge/hfs/rules.yaml +41 -13
- package/runtime/knowledge/hfs/slots.yaml +99 -43
- package/runtime/knowledge/patterns/be/api.yaml +64 -1
- package/runtime/knowledge/patterns/be/cli.yaml +23 -5
- package/runtime/knowledge/patterns/be/webhooks.yaml +187 -32
- package/runtime/knowledge/patterns/fe/folder.yaml +3 -0
- package/runtime/modules/kernel/failure-codes.yaml +15 -4
- package/runtime/scripts/api/git/cat-file.mjs +5 -0
- package/runtime/scripts/api/sops/resolve-sops.mjs +7 -0
- package/runtime/scripts/api/sops/seal.mjs +1 -1
- package/runtime/scripts/hfs/architecture/backend-app-files.mjs +110 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +20 -3
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +157 -41
- package/runtime/scripts/hfs/architecture/canon-clones.mjs +240 -0
- package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +143 -0
- package/runtime/scripts/hfs/architecture/clones.mjs +17 -8
- package/runtime/scripts/hfs/architecture/connection-map.mjs +14 -12
- package/runtime/scripts/hfs/architecture/context-map.mjs +25 -14
- package/runtime/scripts/hfs/architecture/context-owner.mjs +1 -2
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +14 -9
- package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +104 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +30 -11
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +16 -3
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +17 -0
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +21 -12
- package/runtime/scripts/hfs/architecture/register-once.mjs +13 -5
- package/runtime/scripts/hfs/architecture/required-files.mjs +2 -1
- package/runtime/scripts/hfs/architecture/staged-consumers.mjs +37 -0
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +3 -6
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -6
- package/runtime/scripts/hfs/architecture/tiers.mjs +8 -1
- package/runtime/scripts/hfs/architecture/typescript.mjs +1 -8
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +1 -11
- package/runtime/scripts/hfs/check.mjs +18 -26
- package/runtime/scripts/hfs/manifest-shape.mjs +27 -2
- package/runtime/scripts/hfs/path-findings.mjs +32 -2
- package/runtime/scripts/hfs/rules/database-migrations.mjs +36 -35
- package/runtime/scripts/hfs/rules/database.mjs +2 -2
- package/runtime/scripts/hfs/rules/edition.mjs +5 -5
- package/runtime/scripts/hfs/rules/proof-commands.mjs +2 -0
- package/runtime/scripts/hfs/rules/secrets.mjs +13 -0
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +128 -0
- package/runtime/scripts/hfs/rules/stacks.mjs +13 -8
- package/runtime/scripts/hfs/secret-document.mjs +97 -0
- package/runtime/scripts/hfs/secret-errors.mjs +20 -0
- package/runtime/scripts/hfs/secret-files.mjs +124 -0
- package/runtime/scripts/hfs/secret-init.mjs +192 -0
- package/runtime/scripts/hfs/secret-output.mjs +100 -0
- package/runtime/scripts/hfs/secret-sops.mjs +98 -0
- package/runtime/scripts/hfs/secret-store.mjs +161 -0
- package/runtime/scripts/hfs/secret.mjs +135 -195
- package/runtime/scripts/hfs/slot-imports.mjs +3 -1
- package/runtime/scripts/hfs/slot-required.mjs +13 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +2 -1
- package/runtime/scripts/hfs/tree-findings.mjs +22 -0
- package/runtime/scripts/lib/roots.mjs +59 -0
- package/runtime/scripts/lib/swarm-stack.mjs +139 -0
- package/runtime/scripts/lib/ts-analysis.mjs +20 -0
- package/scaffold/add-cli-lite.mjs +10 -11
- package/scaffold/add-inbox-lite.mjs +15 -84
- package/scaffold/add-plan.mjs +183 -0
- package/scaffold/add-table.mjs +44 -5
- package/scaffold/add.mjs +33 -166
- package/scaffold/app.mjs +5 -5
- package/scaffold/image.mjs +2 -2
- package/src/main.mjs +12 -6
- package/sync/index.mjs +23 -16
- package/templates/app/gitignore +1 -0
- package/templates/app/gitignore-lite +1 -0
- package/templates/app/hooks/husky/pre-commit +7 -12
- package/templates/app/skeleton/.sops.yaml +1 -1
- package/templates/app/skeleton-lite/.sops.yaml +1 -1
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +16 -0
- package/templates/app/skeleton-lite/.starciwork/workspace.yaml +9 -0
- package/templates/app/skeleton-lite/README.md +4 -2
- package/templates/app/starciwork.gitignore-lite +6 -0
- package/templates/be/inbox/database-client.spec.ts.tpl +15 -20
- package/templates/be/inbox/database-client.ts.tpl +7 -22
- package/templates/be/inbox/service.spec.ts.tpl +16 -20
- package/templates/be/inbox/service.ts.tpl +6 -10
- package/templates/be/inbox/sql.ts.tpl +2 -2
- package/templates/be/inbox/webhook-door.spec.ts.tpl +5 -5
- package/templates/be/inbox/webhook-door.ts.tpl +4 -3
- package/templates/be/patterns/webhooks/webhook.door.spec.ts.tpl +3 -3
- package/templates/be/patterns/webhooks/webhook.door.ts.tpl +3 -3
- package/templates/be/skeleton/apps/__app__/src/__app__.env.ts +21 -0
- package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +1 -1
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +7 -39
- package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +42 -0
- package/templates/be/skeleton/apps/__app__/src/main.ts +3 -9
- package/templates/be/skeleton/apps/cli/src/cli.env.ts +13 -0
- package/templates/be/skeleton/apps/cli/src/cli.options.ts +6 -8
- package/templates/be/skeleton/apps/cli/src/main.ts +3 -2
- package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +6 -6
- package/templates/be/skeleton-lite/apps/api/src/api.env.ts +25 -0
- package/templates/be/skeleton-lite/apps/api/src/api.options.ts +1 -1
- package/templates/be/skeleton-lite/apps/api/src/app.module.ts +6 -42
- package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +45 -0
- package/templates/be/skeleton-lite/apps/api/src/main.ts +4 -11
- package/templates/be/skeleton-lite/apps/cli/src/cli.env.ts +17 -0
- package/upgrade/file-state.mjs +68 -0
- package/upgrade/index.mjs +7 -56
- package/runtime/scripts/api/git/log.mjs +0 -5
- 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
|
|
45
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
7
|
-
replay window),
|
|
8
|
-
|
|
9
|
-
|
|
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-
|
|
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
|
|
24
|
-
one
|
|
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`).
|
|
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
|
|
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
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
92
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
117
|
-
|
|
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
|
|
124
|
-
write:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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';
|