Repository navigation
Commit fa44dd2
feat(spec): an api flow's start node may name its hook secret (config.secretRef), resolved per post (#25703)
Fixes #25675
Clause-②: yes (widening)
Metadata-protocol: added
Ruling-ref: 6386630545
An `api` flow's start node may now name its inbound hook secret instead
of carrying it: `config.secretRef: 'NAME'`. The deployment fills that
name, `os validate` and the engine accept the reference, and the engine
resolves it at every inbound post. An app package can ship an inbound
`api` flow without publishing a credential, and the repository's one
literal hook secret (the showcase webhook flow) is gone.
Ruling 6386630545 (batch #12 item 3, letter A), verbatim:
> 「`api` 型流程的起始节点可以声明**密钥引用**(流程凭据名 / 环境变量名)而不是字面值;部署方填值,`os validate`
接受引用,引擎每次入站请求按引用解析,复用 #20790 已有的写入式流程凭据存储与
`resolveSecret`。公开仓库里不再出现任何凭据。」
## What changes
**`@objectstack/spec`: the key.**
`packages/spec/src/automation/api-trigger-secret-ref.zod.ts` (new)
declares `ApiTriggerSecretRefSchema`: a name of the shape
`[A-Za-z_][A-Za-z0-9_]*`, never in the platform's `OS_` namespace. Both
rules are patterns, so the published JSON Schema carries them too.
`FlowSchema`'s superRefine runs `apiTriggerSecretRefRefusals` on every
start node and refuses, at `nodes[i].config.secretRef`:
- a reference that is not a name;
- one beside a literal `secret`. Two sources for one credential, and the
literal would publish it;
- one on a flow that binds no inbound `api` trigger
(`resolveFlowTriggerKind`). Nothing would ever read it.
`apiTriggerSecretRefOf(config)` is the one reader of the name. The lint
rule and the engine both call it, so neither restates the grammar.
**`@objectstack/lint`: `flow-api-trigger-secret-missing` accepts a
usable reference.** It accepts one at all three commands and at the
runtime publish gate, with no restored-credential position needed. A
malformed reference is still "no secret". The rule's one-line remedy and
its `os explain` entry now name `config.secretRef`.
**`@objectstack/service-automation`: resolution per post.**
- `hookSecretResolver` gives a start node with a usable `secretRef` a
reader. At every call the reader asks the credential resolver for that
name. It is never cached and never asks the channel. A name that
resolves to nothing, blank or whitespace rejects with
`FlowCredentialUnresolvableError` (`SERVICE_UNAVAILABLE` / 503). The
error names the flow and the reference, never a value. `trigger-api`
already answers that rejection `503` and logs it, so an unfilled
reference refuses every post and never admits one.
- The resolver is the plugin's existing `credentialResolver`, the one a
declarative connector's `auth.credentialRef` goes through (ADR-0097 §3).
It moved to `credential-resolver.ts` (re-exported from `plugin.ts`
unchanged) and is handed to the engine through the new
`AutomationEngineOptions.credentialResolver`. The default is the open
tier's environment variable.
- The registration check passes on that reader. Its refusal text now
names `config.secretRef` first.
- The write-only flow credential channel gives the start-node position a
fifth form, `referenced`. Saving a flow that names its secret removes
any hook-secret row the channel held for that node, the way a cleared
position does, so no stale credential stays at rest. `heldPaths` does
not report it.
**`@objectstack/trigger-api`**: the arm-time refusal and the docblock
say a `secretRef` is resolved by the automation engine. The trigger
never reads the name itself, so a host that binds without that engine
still refuses the flow.
**Showcase**: `showcase_inbound_task_webhook` now carries `secretRef:
'SHOWCASE_WEBHOOK_SECRET'`, and the literal `'showcase-webhook-secret'`
and its "Demo secret" comment are removed. `git grep
showcase-webhook-secret` (CHANGELOGs aside) finds 0 hits. The two
platform-checklist items that signed with the old literal now export
`SHOWCASE_WEBHOOK_SECRET` at boot and sign with it.
**Docs**: `automation/flows.mdx` (the credential callout said a packaged
flow keeps its literal in source), `automation/connectors.mdx` (the
resolver now serves `secretRef` too), and the regenerated reference
page. The `objectstack-automation` skill's start-node table (it said a
non-blank `secret` is required) is corrected in #25708, a governed
`skills/**` PR that merges after this one. Only
`skills/objectstack-automation/references/_index.md` rides here: it is
`gen:skill-refs` output that `check:skill-refs` requires beside the spec
change, and the generated-surface exception (#11705) lifts it.
## Decisions taken (recorded for the reviewer)
1. **A name, resolved through the credential resolver, rather than a
flow-credential-store name.** The ruling allows "流程凭据名 / 环境变量名".
Measured on `origin/main`: the flow credential store is keyed by
position (flow, node, key), not by a name. Its only write door is the
metadata save door, and a packaged flow cannot be overlaid (`flow` has
`allowEnvironmentOverlay: false`). So a deployment has no door to fill a
store entry for a packaged flow, and a store-name form would need a new
write surface. The connector `credentialRef` seam already resolves a
name per deployment: an environment variable in the open tier, or a
host-wired secrets service. Reusing that seam serves both arms of the
ruling with one dialect. The binding's `resolveSecret` (#20790) is the
per-post reader, as ruled.
2. **A distinct key, not a tagged `secret` value.** `secretRef` cannot
be mistaken for the secret, and a literal beside it is refused.
3. **Not withheld on read.** The reference is a name, not a credential.
Withholding it would break the GET → edit → PUT round trip for no
protection. Only `config.secret` stays in the redaction table.
4. **`OS_` names are refused.** Whoever signs the posts holds the hook
secret, so a hook secret that is a platform secret (`OS_AUTH_SECRET`)
would hand it out.
5. **An unfilled reference does not block registration.** The deployment
may fill it after deploy. Each post is refused `503` until then, with a
located `warn` naming the flow and the reference.
## Acceptance notes
- A stored flow row written before the #20790 migration still carries a
literal `secret`. If an author edits it to `secretRef` through `/meta`,
the carry-forward would restore that literal beside the reference, and
`FlowSchema` then refuses the save. That is loud, not silent, and the
boot migration moves such literals out first. Not changed here.
## Verification
All readings are at head `8a23b3ad`, where `origin/main` `b77bb37e` is
merged in. Test suites were last run at `1abed922` or earlier. The
commits after that change only `llms.txt` counts (and the test-alias pin
and docblock, which were re-run).
- **Package suites** (full `test`, each through the verify lock):
`@objectstack/spec` 0, `@objectstack/lint` 0, `@objectstack/trigger-api`
0, `@objectstack/service-automation` 0, `@objectstack/metadata-protocol`
0, `@objectstack/runtime` 0.
- **`test:repo`**: `@objectstack/spec` 0 (79/79 files, 1269/1269 tests,
after the docblock fix; the first run counted 32 demoted descriptions
against the pinned 31), `@objectstack/runtime` 0, `@objectstack/rest` 0.
- **Typecheck**: `spec`, `lint`, `service-automation` and `trigger-api`
0. `example-showcase` NOT MEASURED: ten TS2307 errors from workspace
packages outside the build closure (`service-print`, `plugin-approvals`,
`connector-rest`), none in a changed file. The showcase flow's new start
node parses with `FlowSchema` against the rebuilt spec (`true`).
- **Spec generated artifacts**: `pnpm --filter @objectstack/spec
check:generated`, all 14 up to date after a build at the merged head.
- **Derived gates** (`dispatch-gates --commands`, 128 families): 124
exit 0. Four exit 3, a refused prerequisite, NOT MEASURED:
`check-plugin-teardown-shape --self-test` (shallow clone, needs
`--unshallow`), `spec check:skill-examples` (`client-react` not built),
`check:dual-build-cjs-loads` and `check:i18n-coverage` (the full
workspace build). `--ran` reconciliation: 128 accounted, 124 run, 4
NOT-MEASURED, 0 UNRUN.
- `check-platform-checklist --changed-from origin/main`: OK.
`check:nul-bytes`: 0.
- **At head `088e4423`** (the split, which only restores `SKILL.md` to
`origin/main`): `dispatch-gates --commands` 125 derived, 121 exit 0, the
same four NOT MEASURED, 0 UNRUN. Ablations on the two fail-closed
branches (the unfilled-name `503` in `service-automation`, the
beside-a-literal refusal in `spec`) each turned their pins red and were
restored blob-identical (report addendum `6388889634` on #25675).
- **Dogfood**: n/a. The title has no `!` and Clause-② has no (narrowing)
arm.
_Body edited by the `domain:spec` seat 2 after the governed split, as
the dev proposed in `6388889634`: the skill row and its readings moved
to #25708, which carries its own 维护者速读; this PR's draft 维护者速读 is
dropped, since its diff no longer needs a human merge._
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01NZJKktPWaz2EZJ95a9XZa8
---
_Generated by [Claude
Code](https://claude.ai/code/session_01NZJKktPWaz2EZJ95a9XZa8)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 4bcc4df commit fa44dd2
35 files changed
Lines changed: 728 additions & 52 deletions
File tree
- .changeset
- content/docs
- automation
- getting-started
- references
- automation
- docs/qa/platform-checklist/areas
- examples/app-showcase/src/automation/flows
- packages
- lint/src
- services/service-automation/src
- spec
- api-surface
- declaration-map
- export-origins
- json-schema.manifest
- src
- automation
- triggers/trigger-api/src
- skills/objectstack-automation/references
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
235 | 235 | | |
236 | 236 | | |
237 | 237 | | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
238 | 243 | | |
239 | 244 | | |
240 | 245 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
515 | 515 | | |
516 | 516 | | |
517 | 517 | | |
518 | | - | |
| 518 | + | |
519 | 519 | | |
520 | 520 | | |
521 | 521 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
150 | 150 | | |
151 | 151 | | |
152 | 152 | | |
153 | | - | |
| 153 | + | |
154 | 154 | | |
155 | 155 | | |
156 | 156 | | |
| |||
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
12 | 13 | | |
13 | 14 | | |
14 | 15 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| 16 | + | |
16 | 17 | | |
17 | 18 | | |
18 | 19 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| |||
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
25 | | - | |
| 25 | + | |
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
| 37 | + | |
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| |||
106 | 106 | | |
107 | 107 | | |
108 | 108 | | |
109 | | - | |
| 109 | + | |
110 | 110 | | |
111 | 111 | | |
112 | 112 | | |
113 | 113 | | |
114 | 114 | | |
| 115 | + | |
115 | 116 | | |
116 | 117 | | |
117 | 118 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
465 | 465 | | |
466 | 466 | | |
467 | 467 | | |
468 | | - | |
| 468 | + | |
469 | 469 | | |
470 | 470 | | |
471 | 471 | | |
| |||
617 | 617 | | |
618 | 618 | | |
619 | 619 | | |
620 | | - | |
| 620 | + | |
621 | 621 | | |
622 | 622 | | |
623 | | - | |
| 623 | + | |
624 | 624 | | |
625 | 625 | | |
626 | 626 | | |
| |||
0 commit comments