Repository navigation
feat(spec): an api flow's start node may name its hook secret (config.secretRef), resolved per post - #25703
Conversation
…rence (config.secretRef) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NZJKktPWaz2EZJ95a9XZa8
… config.secretRef Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NZJKktPWaz2EZJ95a9XZa8
…cretRef per post through the credential resolver Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NZJKktPWaz2EZJ95a9XZa8
…WEBHOOK_SECRET) instead of carrying a literal Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NZJKktPWaz2EZJ95a9XZa8
…tion skill, and the changeset Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NZJKktPWaz2EZJ95a9XZa8
…rtifacts Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NZJKktPWaz2EZJ95a9XZa8
…i-flow-secret-ref
…pens without a level-1 heading Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NZJKktPWaz2EZJ95a9XZa8
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NZJKktPWaz2EZJ95a9XZa8
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NZJKktPWaz2EZJ95a9XZa8
📓 Docs Drift CheckThis PR changes 4 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 144 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 426240476fcf853e1d51c33c4b8fe370742dbb8f && git checkout 426240476fcf853e1d51c33c4b8fe370742dbb8f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b1790841bb0bf2c7c696e858de51e8105704471a 088e442390cafbc51ac080fb3721626e440dfacf && git checkout -B drift-repro b1790841bb0bf2c7c696e858de51e8105704471a && git merge --no-ff 088e442390cafbc51ac080fb3721626e440dfacf
node scripts/docs-audit/affected-docs.mjs --json b1790841bb0bf2c7c696e858de51e8105704471a
|
…overned PR Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NZJKktPWaz2EZJ95a9XZa8
席位核对ACCEPT · #25675 ·
Generated by Claude Code |
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS
Generated by Claude Code |
Fixes #25675
Clause-②: yes (widening)
Metadata-protocol: added
Ruling-ref: 6386630545
An
apiflow's start node may now name its inbound hook secret instead of carrying it:config.secretRef: 'NAME'. The deployment fills that name,os validateand the engine accept the reference, and the engine resolves it at every inbound post. An app package can ship an inboundapiflow 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:
What changes
@objectstack/spec: the key.packages/spec/src/automation/api-trigger-secret-ref.zod.ts(new) declaresApiTriggerSecretRefSchema: a name of the shape[A-Za-z_][A-Za-z0-9_]*, never in the platform'sOS_namespace. Both rules are patterns, so the published JSON Schema carries them too.FlowSchema's superRefine runsapiTriggerSecretRefRefusalson every start node and refuses, atnodes[i].config.secretRef:secret. Two sources for one credential, and the literal would publish it;apitrigger (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-missingaccepts 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 itsos explainentry now nameconfig.secretRef.@objectstack/service-automation: resolution per post.hookSecretResolvergives a start node with a usablesecretRefa 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 withFlowCredentialUnresolvableError(SERVICE_UNAVAILABLE/ 503). The error names the flow and the reference, never a value.trigger-apialready answers that rejection503and logs it, so an unfilled reference refuses every post and never admits one.credentialResolver, the one a declarative connector'sauth.credentialRefgoes through (ADR-0097 §3). It moved tocredential-resolver.ts(re-exported fromplugin.tsunchanged) and is handed to the engine through the newAutomationEngineOptions.credentialResolver. The default is the open tier's environment variable.config.secretReffirst.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.heldPathsdoes not report it.@objectstack/trigger-api: the arm-time refusal and the docblock say asecretRefis 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_webhooknow carriessecretRef: '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 exportSHOWCASE_WEBHOOK_SECRETat 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 servessecretReftoo), and the regenerated reference page. Theobjectstack-automationskill's start-node table (it said a non-blanksecretis required) is corrected in #25708, a governedskills/**PR that merges after this one. Onlyskills/objectstack-automation/references/_index.mdrides here: it isgen:skill-refsoutput thatcheck:skill-refsrequires beside the spec change, and the generated-surface exception (#11705) lifts it.Decisions taken (recorded for the reviewer)
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 (flowhasallowEnvironmentOverlay: 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 connectorcredentialRefseam 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'sresolveSecret(security(flows): move a flow's inbound-hook secret out of flow metadata into the write-only secret seam #7799 established — no read, the generic data door included, returns it #20790) is the per-post reader, as ruled.secretvalue.secretRefcannot be mistaken for the secret, and a literal beside it is refused.config.secretstays in the redaction table.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.503until then, with a locatedwarnnaming the flow and the reference.Acceptance notes
secret. If an author edits it tosecretRefthrough/meta, the carry-forward would restore that literal beside the reference, andFlowSchemathen 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, whereorigin/mainb77bb37eis merged in. Test suites were last run at1abed922or earlier. The commits after that change onlyllms.txtcounts (and the test-alias pin and docblock, which were re-run).test, each through the verify lock):@objectstack/spec0,@objectstack/lint0,@objectstack/trigger-api0,@objectstack/service-automation0,@objectstack/metadata-protocol0,@objectstack/runtime0.test:repo:@objectstack/spec0 (79/79 files, 1269/1269 tests, after the docblock fix; the first run counted 32 demoted descriptions against the pinned 31),@objectstack/runtime0,@objectstack/rest0.spec,lint,service-automationandtrigger-api0.example-showcaseNOT 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 withFlowSchemaagainst the rebuilt spec (true).pnpm --filter @objectstack/spec check:generated, all 14 up to date after a build at the merged head.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-reactnot built),check:dual-build-cjs-loadsandcheck:i18n-coverage(the full workspace build).--ranreconciliation: 128 accounted, 124 run, 4 NOT-MEASURED, 0 UNRUN.check-platform-checklist --changed-from origin/main: OK.check:nul-bytes: 0.088e4423(the split, which only restoresSKILL.mdtoorigin/main):dispatch-gates --commands125 derived, 121 exit 0, the same four NOT MEASURED, 0 UNRUN. Ablations on the two fail-closed branches (the unfilled-name503inservice-automation, the beside-a-literal refusal inspec) each turned their pins red and were restored blob-identical (report addendum6388889634on An app package cannot ship an inboundapiflow:config.secretmust be a literal in source, and there is no deployment-time seam for a package-authored flow #25675).!and Clause-② has no (narrowing) arm.Body edited by the
domain:specseat 2 after the governed split, as the dev proposed in6388889634: 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.ai/code/session_01NZJKktPWaz2EZJ95a9XZa8
Generated by Claude Code