Repository navigation
Commit 487a784
Fixes #20529
Clause-②: no (narrowing)
## What this changes
ADR-0041 (status Accepted), `trigger-api` acceptance criteria: "Per-flow
inbound endpoint (...) with a per-flow secret; HMAC signature
verification (GitHub/Stripe style) and a constant-time compare." The
trigger armed a flow's inbound hook with no secret, logging only a
warning, and such a hook skipped signature verification. An `api`
trigger with no secret is now refused at arm time and at registration.
- **`@objectstack/trigger-api`**
- `ApiTrigger.start()` throws when the binding's `config.secret` is
absent, blank after trim, or not a string. The error names the flow and
`config.secret`. It throws before anything is stored in the hook map and
before any queue consumer is subscribed.
- The arm-time warning is removed, because the state it described no
longer exists.
- `ArmedHook.secret` is now non-optional. `handleRequest` verifies every
post, so the type system has no unsigned branch left to reach.
- The route ledger's note, which recorded an unsigned posture, now
records that every hook this door serves is signed.
- **`@objectstack/service-automation`**
- `registerFlow` gains `validateApiTriggerSecret`, placed after the
three existing hard-fail validations.
- It judges the binding that `deriveTriggerBinding` computes. That is
the body of `resolveTriggerBinding`, split out so it also runs over a
flow that is not registered yet. So the rule reads the very `config`
object that `activateFlowTrigger` would hand `start()`, including the
array-form precedence.
- It applies whatever the flow's `status`, like the other registration
refusals.
- What callers see is unchanged in kind:
- The `/automation` create, update and clone doors answer `400
VALIDATION_FAILED` with `details.fields[0] = { field: '(body)', code:
'invalid_value' }`. This throw has the same plain-`Error` shape (the
flow-rejected message) that
`packages/runtime/src/domains/automation-register-error-class.test.ts`
case 4 already pins.
- Boot skips the flow with the existing `[Automation] failed to register
flow` warning.
- No new error code, and no `packages/spec` edit.
**Why the rule lives in two places.** `@objectstack/trigger-api` and
`@objectstack/service-automation` have no dependency on each other. At
boot the trigger registers on `kernel:ready`, after the flow pull, so
the engine cannot ask it at registration time. The engine's copy is the
publish-time refusal the author sees. The trigger's copy protects a host
that binds without the engine. Both read the same binding `config`, so
they cannot disagree about which flows need a secret. A single home that
also reaches `os validate` would be a `packages/spec` rule (see below).
That is the spec lane's call and is not made here.
**Breaking.** The changeset
`.changeset/20529-api-trigger-requires-secret.md` bumps both packages
`minor`. Its BREAKING paragraph gives the remedy: set a non-blank
`config.secret` on the start node. A flow that is only ever started
explicitly is `type: 'autolaunched'`, with no `triggerType: 'api'`, and
needs no secret. Its ADR-0087 disposition is `not-required
(no-migration-prescription)`, accepted by `check-adr-0087-registration`.
## Pin sweep
- **Pins of the old semantics, repo-wide.** A repo-wide `git grep` for
the warning text, "unsigned post" and "accepts unsigned" (CHANGELOGs
excluded) hit four places:
- the test pin, flipped;
- the trigger docblock, rewritten;
- the route-ledger note, rewritten;
- `skills/objectstack-automation/SKILL.md:356`. That file is a Tier H
governed surface outside this card's file surface, so it is reported,
not edited.
- **The flipped pin carries weight.** "accepts unsigned posts when no
secret is configured" became three cases, for a missing, a blank and a
non-string secret. Each asserts all of the following:
- `start()` throws, naming the flow and `config.secret`;
- `listHooks()` is `[]`;
- no queue subscription happened, and no `armed:` log line was written;
- a post to that flow answers `404` with the full `RESOURCE_NOT_FOUND`
body;
- nothing was published or delivered, and the flow never ran.
- **The guarded surface is kept verbatim.** The `401`
missing-or-bad-signature assertions are unchanged; only the test title
lost its "when the flow declares a secret" clause. Every other case that
armed with `{}` now arms with a secret and signs its body, and its
assertion is unchanged.
- **Fixture triage.** Three existing fixtures registered an `api` flow
with no secret:
- `engine.test.ts`: the execution-history fixture changes type to
`autolaunched`. It is only ever run through `engine.execute`, never an
inbound hook.
- `flow-trigger-kind-shared-resolver.test.ts` (the `type: 'api'` and
`triggerType: 'api'` rows) and `flow-activation-ledger.test.ts` (the
`api` entry path) now declare the secret. Their subject is kind
resolution and ledger refusal, so they must stay `api`.
- A repo-wide scan for `api`-kind flow definitions found no other
fixture that reaches a real engine. The only other hits are in
`packages/lint` and `packages/spec` tests, which never call
`registerFlow`.
- **New registration pins** (`api-trigger-secret-registration.test.ts`):
- Five refusal cases: a `type: 'api'` flow with no, blank or non-string
secret; a start-node `triggerType: 'api'` flow; and an `obsolete` flow.
Each asserts that the flow is absent afterwards (`getFlow` is null and
it is not in the runtime states) and that the `api` trigger was never
started.
- Two contrast cases: a signed flow registers, binds, and hands the
trigger its secret; an `autolaunched` flow needs no secret and runs.
- One re-registration case: a re-registration that drops the secret is
refused, and the stored signed version stays, neither stopped nor
re-started.
## Verification record (HEAD `b7325134`)
- **Build.** I built the dependency closure of both packages, then ran a
full `turbo run build --filter=!@objectstack/docs --concurrency=2`:
72/72 tasks, 0 cached. It was needed for the dist-reading gates.
- **Tests**
- `@objectstack/service-automation`: 150 files, 1845 tests, all passed.
- `@objectstack/trigger-api`: 2 files, 26 tests, all passed.
- Both suites ran on `b7325134`, after the last commit.
- **Typecheck**
- `@objectstack/trigger-api` passes. `tsc --listFiles` counts both of
its test files.
- `@objectstack/service-automation` passes, including
`check:test-typecheck`.
- **Ablation 1: arm-time refusal.** `scripts/ablation-replace.mjs`
replaced the throw with the old `logger.warn`.
- Landed: anchor 1 to 0, blob `7e60a8ab` to `cc123fba`.
- Red: `Tests 3 failed | 8 passed (11)`. All three refusal cases failed
with `AssertionError: expected [Function] to throw an error`.
- Restored: blob equals HEAD `7e60a8ab`, and `git diff HEAD` is empty.
- Green before and after: 26/26.
- **Ablation 2: registration refusal.** The `validateApiTriggerSecret`
call was deleted.
- Landed: anchor 1 to 0, blob `679f73dd` to `e66c2db2`.
- Red: `Tests 6 failed | 2 passed (8)`. All five refusal cases and the
re-registration case failed with `expected [Function] to throw an
error`. The two contrast cases stayed green.
- Restored: blob equals HEAD `679f73dd`, and `git diff HEAD` is empty.
- Both suites import the subject from relative source, so no `dist/` was
involved.
- **Gates.** `dispatch-gates --commands`, derived over this diff's 9
paths, gave 62 commands. All 62 exited 0. Reconciling with `--ran` gave
"62 derived, 62 run, 0 NOT-MEASURED (a DERIVED zero)".
`check:dual-build-cjs-loads` first answered `PREREQUISITE NOT MET` (exit
3, not a measurement). After the full build it exited 0.
`check:dts-closure` and `check:lean-entry-closure` were re-run after the
full build too.
- **Lint, narrowed and proven.** eslint `--no-inline-config --format
json` over the 8 changed `.ts` files reported 8 files, 0 errors and 0
warnings. Three facts make that narrowing a measurement rather than a
skip:
- The population comes from the config: each file matches the
`packages/**/*.{ts,tsx,mts,cts}` blocks, and none reported "File
ignored".
- The count comes from the JSON output.
- The config never enables type-aware linting (no
`parserOptions.project`; `eslint.config.mjs` states this at lines
326–328), so this diff cannot move any untouched file's verdict.
- **Declared to CI:** the repo-wide `pnpm lint`, the downstream consumer
suites of `@objectstack/service-automation`, and the full farm.
## The three measurements
1. **Run identity: yes, for the documented pattern.** The inbound
trigger supplies no user. A fired run takes the identity of the flow's
declared `runAs`, which defaults to `'user'`.
- Under the default, data nodes are refused for want of a principal.
- A flow that declares `runAs: 'system'` runs its data nodes with system
elevation. The shipped worked example declares `runAs: 'system'`,
because it creates a record.
2. **Can a non-admin read `config.secret`: yes, by source reading.** I
reported it to the seat as an out-of-scope security finding. It is not
changed here.
3. **Shipped examples, templates, scaffolds: no.** The only shipped
`api` flow is the showcase's worked example, and it carries a secret, so
`examples/**` needs no edit. `packages/create-objectstack` declares no
`api` flow. One thing did turn up: the published automation skill
describes the secret as optional (reported below).
## `os validate` reach
**No.** `os validate` never builds an `AutomationEngine` or calls
`registerFlow`. `packages/cli/src/commands/validate.ts` runs the
`defineStack` parse, the `@objectstack/lint` authoring rules and the
capability preflight. I measured it on a throwaway stack (deleted
afterwards) that declares one `type: 'api'` flow with no secret and
`requires: ['automation', 'triggers', 'queue']`: `os validate` printed
`✓ Validation passed`, exit 0. #20367 (PR #20460) runs the stack's
`defineStack` refusals, and this refusal is not one of them. For `os
validate` to see it, the rule would need to be a `defineStack` refusal
next to the trigger-capability refusal (keyed on
`resolveFlowTriggerKind`), or a `validate-flow-trigger-readiness` rule
in `packages/lint`. Both belong to another lane.
## Acceptance notes
- **`hookId` fallback left as is.** A secret is now mandatory for every
armed hook, so the fallback token no longer has an unsigned form and
nothing concrete argues for changing it here.
- **Out-of-scope findings, reported to the seat and not filed from
here:**
- `skills/objectstack-automation/SKILL.md` (lines 52 and 356) calls the
secret "strongly recommended" and describes `type: 'api'` as "invoked
explicitly … **or** bound as an inbound webhook". The engine binds every
`type: 'api'` flow to the inbound trigger, so an author following it now
writes a flow the runtime refuses. The file is Tier H.
- `os validate` passes a flow the engine refuses (measured above).
- The measurement ② finding.
- **`content/docs/**`.** No line calls the inbound secret optional (0
hits), so there is no docs edit. Two observations, not filed:
- `content/docs/automation/flows.mdx` says an `api` flow "inherits its
organization from whoever triggered it". The inbound trigger passes no
caller session.
- `content/docs/automation/webhooks.mdx` §16 still lists inbound
webhooks as a non-goal with "no runtime".
- Carrier for both: none.
- **Not done here:**
- No `scripts/adr-anchors/` entry for ADR-0041 was added. That path is
outside this card's file surface.
- Whether the Studio flow designer (objectui) can author an `api` flow's
`config.secret` is not measured. The sibling repo is not checked out in
this container.
- **Gate list size.** Derived over the paths this diff actually touches,
the list is 62 commands. The dispatch-time list over the expected paths
was 92, because it also included `examples/**` and `content/docs/**`
paths this diff never touched.
---
_Generated by [Claude
Code](https://claude.ai/code/session_017B6YKCGu8CTY2KBWgwaHAs)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6154165 commit 487a784
9 files changed
Lines changed: 326 additions & 39 deletions
File tree
- .changeset
- packages
- services/service-automation/src
- triggers/trigger-api/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
Lines changed: 135 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1306 | 1306 | | |
1307 | 1307 | | |
1308 | 1308 | | |
1309 | | - | |
| 1309 | + | |
| 1310 | + | |
| 1311 | + | |
1310 | 1312 | | |
1311 | 1313 | | |
1312 | 1314 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3433 | 3433 | | |
3434 | 3434 | | |
3435 | 3435 | | |
| 3436 | + | |
| 3437 | + | |
| 3438 | + | |
| 3439 | + | |
| 3440 | + | |
| 3441 | + | |
| 3442 | + | |
| 3443 | + | |
| 3444 | + | |
| 3445 | + | |
| 3446 | + | |
| 3447 | + | |
| 3448 | + | |
3436 | 3449 | | |
3437 | 3450 | | |
3438 | 3451 | | |
| |||
4191 | 4204 | | |
4192 | 4205 | | |
4193 | 4206 | | |
| 4207 | + | |
| 4208 | + | |
| 4209 | + | |
| 4210 | + | |
| 4211 | + | |
| 4212 | + | |
4194 | 4213 | | |
4195 | 4214 | | |
4196 | 4215 | | |
| |||
9349 | 9368 | | |
9350 | 9369 | | |
9351 | 9370 | | |
| 9371 | + | |
| 9372 | + | |
| 9373 | + | |
| 9374 | + | |
| 9375 | + | |
| 9376 | + | |
| 9377 | + | |
| 9378 | + | |
| 9379 | + | |
| 9380 | + | |
| 9381 | + | |
| 9382 | + | |
| 9383 | + | |
| 9384 | + | |
| 9385 | + | |
| 9386 | + | |
| 9387 | + | |
| 9388 | + | |
| 9389 | + | |
| 9390 | + | |
| 9391 | + | |
| 9392 | + | |
| 9393 | + | |
| 9394 | + | |
| 9395 | + | |
| 9396 | + | |
| 9397 | + | |
| 9398 | + | |
| 9399 | + | |
| 9400 | + | |
| 9401 | + | |
| 9402 | + | |
| 9403 | + | |
| 9404 | + | |
| 9405 | + | |
| 9406 | + | |
| 9407 | + | |
| 9408 | + | |
| 9409 | + | |
| 9410 | + | |
9352 | 9411 | | |
9353 | 9412 | | |
9354 | 9413 | | |
| |||
Lines changed: 2 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
148 | 148 | | |
149 | 149 | | |
150 | 150 | | |
151 | | - | |
| 151 | + | |
| 152 | + | |
152 | 153 | | |
153 | 154 | | |
154 | 155 | | |
| |||
Lines changed: 3 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
115 | 115 | | |
116 | 116 | | |
117 | 117 | | |
118 | | - | |
| 118 | + | |
| 119 | + | |
119 | 120 | | |
120 | 121 | | |
121 | 122 | | |
122 | 123 | | |
123 | | - | |
| 124 | + | |
124 | 125 | | |
125 | 126 | | |
126 | 127 | | |
| |||
0 commit comments