Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/20590-http-credential-guidance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@objectstack/service-automation': patch
---

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)

Clause-②: no

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:

- a credential in a header: a `connector_action` on a declarative connector whose `auth.credentialRef` names the secret;
- 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;
- 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.

Description text only. No config key is added or removed, nothing more is withheld on read, and there is nothing to migrate.
18 changes: 14 additions & 4 deletions examples/app-showcase/src/automation/flows/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1014,17 +1014,27 @@ export const FanOutNotifyFlow = defineFlow({
edges: [],
},
{
// Slack is a CONNECTOR, not a notify channel (#4343): post through
// an incoming webhook, or a `connector_action` with the Slack
// connector. The retired `script` + `actionType: 'slack'` shape
// logged a line and delivered nothing.
// Slack is a CONNECTOR, not a notify channel (#4343). The retired
// `script` + `actionType: 'slack'` shape logged a line and
// delivered nothing. A real post goes through a connector, as
// `TaskCompletedSlackFlow` above does. This branch stays an `http`
// node because it is the showcase's `parallel` + `http` fixture
// (docs/qa/platform-checklist/areas/automation.json).
name: 'Post to Slack',
nodes: [
{
id: 'slack_post',
type: 'http',
label: 'Slack Notify',
config: {
// A placeholder, and it must stay one. This url is served with
// the flow definition to every member who can read flows, and
// an incoming-webhook url's path IS its secret, so a real one
// never goes here. A credentialed call goes through a
// connector whose credential lives outside the definition:
// a declarative instance's `auth.credentialRef`
// (src/system/connectors/index.ts), or the `slack` connector
// `TaskCompletedSlackFlow` uses.
url: 'https://hooks.slack.com/services/T000/B000/XXXX',
method: 'POST',
body: { channel: '#tasks', text: 'Task done: {record.title}' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,25 @@ describe('http (canonical node)', () => {
expect(d?.paradigms).toEqual(expect.arrayContaining(['flow', 'approval']));
});

// #20590 — the designer palette (`GET /api/v1/automation/actions`) serves
// this configSchema as the node's authoring form. A credential typed into
// `url` or `headers` is served with the flow definition, so both fields
// name the route that keeps it out: a declarative connector's
// `auth.credentialRef`, called through `connector_action` (for `url`, the
// route for a query-string key; a path-borne secret has no `credentialRef`
// variant and is sent to a token-authenticated connector instead). The
// prose is free to change; the named route is what is held here.
it.each(['url', 'headers'])('the %s field names the connector credentialRef route for a credential', (key) => {
const engine = new AutomationEngine(createTestLogger());
registerHttpNodes(engine, createCtx());
const props = (engine.getActionDescriptor('http')?.configSchema as
| { properties?: Record<string, { description?: string }> }
| undefined)?.properties;
const description = props?.[key]?.description ?? '';
expect(description).toContain('auth.credentialRef');
expect(description).toContain('connector_action');
});

it('does NOT register the removed http_request/http_call/webhook aliases (11.0)', () => {
const engine = new AutomationEngine(createTestLogger());
registerHttpNodes(engine, createCtx());
Expand Down
36 changes: 34 additions & 2 deletions packages/services/service-automation/src/builtin/http-nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,41 @@ export function registerHttpNodes(engine: AutomationEngine, ctx: PluginContext):
type: 'object',
required: ['url'],
properties: {
url: { type: 'string', description: 'Target URL' },
// #20590 — `url` and `headers` carry the credential steer, the
// same steer #20654 brings to the `HttpConfigSchema` describes
// in `@objectstack/spec/automation`. This config is stored in
// the flow definition, and every definition read serves it to
// any member who can read flows. Only `signingSecret` is
// withheld (`flow-credential-projection.ts`), so a credential in
// `url` or `headers` is served as authored. Nothing else is
// withheld, because a withheld non-credential breaks the round
// trip; the remedy is to keep the credential out of the
// definition. The route depends on where the credential sits.
// A header or a query-string key is carried by a declarative
// connector's `auth.credentialRef` (`bearer` / `basic` /
// `api-key`, whose `paramName` puts the key in the query). No
// `credentialRef` variant carries a secret in the url PATH, so
// a path-secret webhook is replaced by a token-authenticated
// connector (`@objectstack/connector-slack`'s bot token).
url: {
type: 'string',
description:
'Target URL. Stored in the flow definition, which is served to every member who can read '
+ 'flows, so never a secret-bearing URL. A key in the query string: call the upstream with a '
+ '`connector_action` on a declarative `rest` connector with `api-key` auth, whose '
+ '`paramName` names the parameter and `auth.credentialRef` names the secret. A webhook whose '
+ 'path is the secret: call the service through a token-authenticated connector instead, such '
+ 'as the `slack` connector with its bot token.',
},
method: { type: 'string', description: 'HTTP method (default GET; POST when durable)' },
headers: { type: 'object', description: 'Request headers' },
headers: {
type: 'object',
description:
'Request headers. Stored in the flow definition, which is served to every member who can '
+ 'read flows, so never put a credential here (an `Authorization` value, an API key): call an '
+ 'authenticated upstream through a `connector_action` on a declarative connector whose '
+ '`auth.credentialRef` names the secret.',
},
body: { description: 'Request body (JSON-serialised)' },
durable: {
type: 'boolean',
Expand Down
Loading