Skip to content

Commit 14f80e2

Browse files
fix(service-automation): the http node's url and headers describes route an outbound credential to a connector's credentialRef, and the showcase says its webhook url is served (#20590) (#20672)
Fixes #20590 Clause-②: no ## What this changes This is the `domain:services` face of triage's direction A on this card (ruling `5891721503`, "Direction: A, taken whole"), scoped by the claim `5891908897`. It carries two guidance edits and nothing else. Nothing new is withheld on read. 1. **The `http` action descriptor's `configSchema`** (`packages/services/service-automation/src/builtin/http-nodes.ts`). The `url` field read "Target URL" and the `headers` field read "Request headers". Both now say that the value is stored in the flow definition, that a flow definition is served to every member who can read flows, and that an outbound credential goes to a `connector_action` on a declarative connector whose `auth.credentialRef` names the secret. *(Seat's correction after patch round 1: that holds for `headers`. For `url`, a query-string key goes to a declarative `rest` connector with `api-key` auth (`paramName`, `auth.credentialRef`), and a webhook whose path is the secret goes to a token-authenticated connector such as `slack`, since no `credentialRef` variant carries a path-borne secret.)* Of this node's config, only `signingSecret` is withheld on read (`FLOW_NODE_CREDENTIAL_KEYS`), so `url` and `headers` are served as authored. A code comment beside the two fields records that. 2. **The showcase's webhook-url teaching site** (`examples/app-showcase/src/automation/flows/index.ts`, the `slack_post` node in `showcase_fan_out_notify`). The branch comment used to teach "post through an incoming webhook". It now sends a real post through a connector, as `TaskCompletedSlackFlow` already does. A comment directly above the placeholder url says that the url is served with the flow definition to every member who can read flows, and that an incoming-webhook url's path is its secret, so a real one never goes there. A changeset (`patch`, `@objectstack/service-automation`) covers the descriptor text, which ships in that package's `dist`. The showcase is `private`, so it takes no changeset. ## Mechanism assumptions, measured at `992c9ac87` **A1: where the descriptor text reaches an author.** `AutomationEngine.getActionDescriptors()` backs the runtime automation domain's `GET /automation/actions`, which is the designer palette's feed. The client SDK calls it as `automation.listActions`. The `configSchema` it serves is what the Studio property form is built from (`io-node-form-zod-ledger.test.ts` header). - Measured with a one-shot probe (not committed): the real runtime `HttpDispatcher`, over a real `AutomationEngine` with `installBuiltinNodes`, answered `GET /automation/actions` with `200` and 17 descriptors. The `http` descriptor's `url` and `headers` descriptions are the new text. - NOT MEASURED: how the Studio renderer shows a field `description`. That lives in the sibling UI repository, which is outside this run's scope. - **`connector_action`:** its descriptor publishes **no** `configSchema`. That is deliberate: the executor reads only the `FlowNodeSchema.connectorConfig` sibling block. The same probe answered `configSchema: null` for it, and `io-node-form-zod-ledger.test.ts` and `config-schemas.test.ts` both pin it. So there is no services-side `input` describe to steer. The `connectorConfig.input` describe lives in `packages/spec`, which is #20654's face. - **Wording:** the text matches the spec describes #20654 will carry, in substance: a flow definition is served to every member who can read flows, and an outbound credential goes to a declarative connector's `auth.credentialRef`. It also agrees with the flows guide sentence #20663 landed. The exact words are this PR's own. **A2: the showcase site.** The showcase could not simply take the connector route. - `showcase_fan_out_notify` is the platform checklist's `parallel` + `http` fixture (`docs/qa/platform-checklist/areas/automation.json`, `automation.flow-node-type-matrix`). That item locates the `http` variant's step in this flow's runs. Turning `slack_post` into a `connector_action` would move the tested surface. - A provider-bound connector with a real `credentialRef` fails the boot when the reference does not resolve (`resolveInstanceAuth`, ADR-0097 §3). That is why every showcase instance declares `auth: { type: 'none' }`. Adding one would also touch `src/system/connectors/index.ts`, which is outside this card's file surface. - So the url stays, and the comment beside it says plainly that it is served. The edit is comment-only: with whitespace minified, esbuild 0.28.1 compiles the file at the merge base and at `992c9ac87` to byte-identical JavaScript (sha256 prefix `6197a04e663ec0d8`, 31,595 bytes). The positive control is a one-character change in the same url, which does change the output. ## Tests, at `992c9ac87` - `@objectstack/service-automation`: 154 test files and 1,913 tests passed (`vitest run --maxWorkers=2`). `typecheck` passed, including `check:test-typecheck`. `tsconfig.test.json` compiles `http-nodes.test.ts` (counted with `--listFiles`). - New pin in `http-nodes.test.ts`: the `url` and `headers` descriptions each name `auth.credentialRef` and `connector_action`. It asserts the named route, not the prose. - **Ablation**, with the fix committed first, through `scripts/ablation-replace.mjs` in wrap mode plus a shell trap. The mutation put both descriptions back to their pre-fix text. On disk: `auth.credentialRef` count 2 to 0, and the old one-line fields 0 to 1 each. Result: exactly the two new cases failed, "2 failed, 8 passed", with the messages "expected 'Target URL' to contain 'auth.credentialRef'" and "expected 'Request headers' ...". Restore proof: blob `67f3d9d05a64` equals the HEAD blob, and `git diff HEAD` is empty. The test imports the source file directly, so no build sits in the path. - **Published surface:** the built `dist/index.js` and `dist/index.cjs` each carry the new text twice, and the old `headers` description zero times. The positive control, the unchanged `method` description, appears once in each. `files[]` is `dist`, `README.md`, `CHANGELOG.md`. - `@objectstack/example-showcase`: `typecheck` passed, and the full suite passed: 29 files, 385 tests. - Lint, narrowed to the three touched `.ts` files: 3 files, 0 errors, 0 warnings (eslint `--no-inline-config --format json`). The narrowing is sound for three reasons. `--print-config` resolves a rule set for each file, so none is ignored. No `parserOptions.project` or `projectService` is set, so linting is not type-aware. A diff therefore cannot move any untouched file's verdict. The full `pnpm lint` is CI's. ## Gates `node scripts/pm/dispatch-gates.mjs --commands` at `992c9ac87` derived 63 commands. All 63 were run with each exit code captured before any pipe, and all exit 0. `--ran` reconciles "63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN", with every code recorded. - `check:dual-build-cjs-loads` and `check:type-check-debt` first answered `PREREQUISITE NOT MET` (exit 3). They were re-run after a turbo build of every package, and both then passed. - The four roster families were run separately, and each exited 0: `node scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm check:error-code-casing` and `pnpm check:filter-alias-parity`. - The derivation reports that `origin/main` has moved five commits past this branch's base. None of those commits touches a path in this diff. ## Acceptance notes - `content/docs/automation/flows.mdx` still lists "an `http` node posting to an incoming webhook" as the replacement for the retired Slack `actionType`, in its retired-shapes table. The docs face's PR recorded the same line. It is outside this card's file surface. - #20654 (spec describes, and the lint predicate and advisory) and #20657 (the automation skill) remain open, and each carries its own face. The flows guide face landed separately. ## Patch round 1 (the seat's append) - **R1** (at-tier record `5894416988`): `ConnectorInstanceAuthSchema` has no variant for a secret in the url path, so the `url` describe no longer sends a path-secret webhook to `auth.credentialRef`. It routes by shape: - a query-string key goes to a declarative `rest` connector with `api-key` auth (`paramName`, `auth.credentialRef`); - a path-secret webhook goes to a token-authenticated connector (the `slack` bot token). - Every named route was verified at source (`connector-auth.zod.ts`, `plugin.ts` `resolveInstanceAuth`, `rest-connector.ts` `applyAuth`, `slack-connector.ts`). - The `headers` describe is unchanged. The code comment no longer claims the spec describes already match (#20654 is open). - The changeset lists the three routes. - **At `695bb749`:** `service-automation` 1913 passed, `typecheck` exit 0. The ablation of the url clause turned exactly the url pin red, with the headers pin green, and the restore was proven. `dist` carries the new clause. 63 / 63 derived gates and the 4 ⛔ rosters exit 0. - **The same over-reach elsewhere:** the landed `flows.mdx` callout and its retired-shapes row are #20682. #20654 and #20657 carry wording guards. --- _Generated by [Claude Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7184436 commit 14f80e2

4 files changed

Lines changed: 82 additions & 6 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/service-automation': patch
3+
---
4+
5+
The `http` node's designer form says an outbound credential never goes in the node's `url` or `headers`, and where it goes instead (#20590)
6+
7+
Clause-②: no
8+
9+
The `http` action descriptor's `configSchema` is what the flow designer's palette and property form read (`GET /api/v1/automation/actions`). It described `url` as "Target URL" and `headers` as "Request headers", with no word about credentials. Both values are stored in the flow definition, and a flow definition is served to every member who can read flows; of the node's config, only `signingSecret` is withheld. The two field descriptions now say this, and name where the credential goes instead:
10+
11+
- a credential in a header: a `connector_action` on a declarative connector whose `auth.credentialRef` names the secret;
12+
- a key in the query string: a declarative `rest` connector with `api-key` auth, whose `paramName` names the parameter and whose `auth.credentialRef` names the secret;
13+
- a webhook whose path is the secret: a token-authenticated connector instead, such as the `slack` connector with its bot token. No `credentialRef` variant carries a secret in the url path.
14+
15+
Description text only. No config key is added or removed, nothing more is withheld on read, and there is nothing to migrate.

‎examples/app-showcase/src/automation/flows/index.ts‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,17 +1014,27 @@ export const FanOutNotifyFlow = defineFlow({
10141014
edges: [],
10151015
},
10161016
{
1017-
// Slack is a CONNECTOR, not a notify channel (#4343): post through
1018-
// an incoming webhook, or a `connector_action` with the Slack
1019-
// connector. The retired `script` + `actionType: 'slack'` shape
1020-
// logged a line and delivered nothing.
1017+
// Slack is a CONNECTOR, not a notify channel (#4343). The retired
1018+
// `script` + `actionType: 'slack'` shape logged a line and
1019+
// delivered nothing. A real post goes through a connector, as
1020+
// `TaskCompletedSlackFlow` above does. This branch stays an `http`
1021+
// node because it is the showcase's `parallel` + `http` fixture
1022+
// (docs/qa/platform-checklist/areas/automation.json).
10211023
name: 'Post to Slack',
10221024
nodes: [
10231025
{
10241026
id: 'slack_post',
10251027
type: 'http',
10261028
label: 'Slack Notify',
10271029
config: {
1030+
// A placeholder, and it must stay one. This url is served with
1031+
// the flow definition to every member who can read flows, and
1032+
// an incoming-webhook url's path IS its secret, so a real one
1033+
// never goes here. A credentialed call goes through a
1034+
// connector whose credential lives outside the definition:
1035+
// a declarative instance's `auth.credentialRef`
1036+
// (src/system/connectors/index.ts), or the `slack` connector
1037+
// `TaskCompletedSlackFlow` uses.
10281038
url: 'https://hooks.slack.com/services/T000/B000/XXXX',
10291039
method: 'POST',
10301040
body: { channel: '#tasks', text: 'Task done: {record.title}' },

‎packages/services/service-automation/src/builtin/http-nodes.test.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,25 @@ describe('http (canonical node)', () => {
6161
expect(d?.paradigms).toEqual(expect.arrayContaining(['flow', 'approval']));
6262
});
6363

64+
// #20590 — the designer palette (`GET /api/v1/automation/actions`) serves
65+
// this configSchema as the node's authoring form. A credential typed into
66+
// `url` or `headers` is served with the flow definition, so both fields
67+
// name the route that keeps it out: a declarative connector's
68+
// `auth.credentialRef`, called through `connector_action` (for `url`, the
69+
// route for a query-string key; a path-borne secret has no `credentialRef`
70+
// variant and is sent to a token-authenticated connector instead). The
71+
// prose is free to change; the named route is what is held here.
72+
it.each(['url', 'headers'])('the %s field names the connector credentialRef route for a credential', (key) => {
73+
const engine = new AutomationEngine(createTestLogger());
74+
registerHttpNodes(engine, createCtx());
75+
const props = (engine.getActionDescriptor('http')?.configSchema as
76+
| { properties?: Record<string, { description?: string }> }
77+
| undefined)?.properties;
78+
const description = props?.[key]?.description ?? '';
79+
expect(description).toContain('auth.credentialRef');
80+
expect(description).toContain('connector_action');
81+
});
82+
6483
it('does NOT register the removed http_request/http_call/webhook aliases (11.0)', () => {
6584
const engine = new AutomationEngine(createTestLogger());
6685
registerHttpNodes(engine, createCtx());

‎packages/services/service-automation/src/builtin/http-nodes.ts‎

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,41 @@ export function registerHttpNodes(engine: AutomationEngine, ctx: PluginContext):
9797
type: 'object',
9898
required: ['url'],
9999
properties: {
100-
url: { type: 'string', description: 'Target URL' },
100+
// #20590 — `url` and `headers` carry the credential steer, the
101+
// same steer #20654 brings to the `HttpConfigSchema` describes
102+
// in `@objectstack/spec/automation`. This config is stored in
103+
// the flow definition, and every definition read serves it to
104+
// any member who can read flows. Only `signingSecret` is
105+
// withheld (`flow-credential-projection.ts`), so a credential in
106+
// `url` or `headers` is served as authored. Nothing else is
107+
// withheld, because a withheld non-credential breaks the round
108+
// trip; the remedy is to keep the credential out of the
109+
// definition. The route depends on where the credential sits.
110+
// A header or a query-string key is carried by a declarative
111+
// connector's `auth.credentialRef` (`bearer` / `basic` /
112+
// `api-key`, whose `paramName` puts the key in the query). No
113+
// `credentialRef` variant carries a secret in the url PATH, so
114+
// a path-secret webhook is replaced by a token-authenticated
115+
// connector (`@objectstack/connector-slack`'s bot token).
116+
url: {
117+
type: 'string',
118+
description:
119+
'Target URL. Stored in the flow definition, which is served to every member who can read '
120+
+ 'flows, so never a secret-bearing URL. A key in the query string: call the upstream with a '
121+
+ '`connector_action` on a declarative `rest` connector with `api-key` auth, whose '
122+
+ '`paramName` names the parameter and `auth.credentialRef` names the secret. A webhook whose '
123+
+ 'path is the secret: call the service through a token-authenticated connector instead, such '
124+
+ 'as the `slack` connector with its bot token.',
125+
},
101126
method: { type: 'string', description: 'HTTP method (default GET; POST when durable)' },
102-
headers: { type: 'object', description: 'Request headers' },
127+
headers: {
128+
type: 'object',
129+
description:
130+
'Request headers. Stored in the flow definition, which is served to every member who can '
131+
+ 'read flows, so never put a credential here (an `Authorization` value, an API key): call an '
132+
+ 'authenticated upstream through a `connector_action` on a declarative connector whose '
133+
+ '`auth.credentialRef` names the secret.',
134+
},
103135
body: { description: 'Request body (JSON-serialised)' },
104136
durable: {
105137
type: 'boolean',

0 commit comments

Comments
 (0)