Skip to content

Commit ed54768

Browse files
feat(lint,spec): a credential typed as a literal into a served flow position draws an author-time advisory that routes it to a connector (#20654) (#20698)
Fixes #20654 Clause-②: no This PR is the `packages/spec` + `packages/lint` face of #20590's direction A (triage ruling `5891721503`, carried by the card and restated in `5891910265`). The services face (the `http` descriptor text and the showcase) is #20590's own claim, and the docs and skill faces are #20655 and #20657. #20590 is not closed by this PR. ## What changes A flow definition is served, as authored, to every member who can read flows. The read path withholds the credential slots the spec declares, but an open map or a url cannot be withheld: an ordinary value there is indistinguishable from a credential. So this PR steers and warns. Nothing is withheld and nothing is refused. - **One predicate, one home.** `@objectstack/lint` gains `isCredentialShapedLiteral(name, value)` in `packages/lint/src/credential-literal.ts`. It is the measured scanner's R1/R2 rule, unchanged: a credential-named key (R1) or an auth-scheme value (R2), on a non-blank string that carries no `{…}` template. The name lists are module-private. A test pins that no other shipped source file in the package holds them. - **One advisory.** `lintFlowCredentialLiterals` (rule id `flow-credential-literal`, `packages/lint/src/lint-flow-credential-literals.ts`) reports one `warning` per credential-shaped literal in three positions: an `http` node's `config.headers` entry, a query parameter of an `http` node's `config.url`, and a node's `connectorConfig.input` at any depth. Nodes nested in `try_catch` / `loop` / `parallel` regions are included. The finding names the key and the position, and it never carries the value. - **Routed by shape.** A header credential goes to a connector whose `auth` is `bearer` or a header `api-key`. A query-string key goes to `api-key` with `paramName`. A connector input goes to the connector's own `auth.credentialRef`. The route is in the message, which is what the `os validate` and `os lint` text faces print; the hint carries the full declaration. No text promises a variant that carries a secret in a url path. This follows the wording guard the services seat relayed in `5894445934`. - **Registered once.** There is one new `AUTHORING_RULES` entry, `tier: 'advisory'`, on all three commands, with `surfaces: ['cli', 'runtime-publish']` and `runtimeTypes: ['flow']`. The wiring guard's "every advisory rule really is advisory" check reads the rule's source for an `error` severity. The finding type is `severity: 'warning'`. - **Describes.** `HttpConfigSchema.headers` and a flow node's `connectorConfig.input` say the definition is served to every flow reader, and route an outbound credential as above. The wording changes; no shape changes. `content/docs/references/automation/{flow,io-node-config}.mdx` are regenerated with `gen:docs`. - **Changeset.** `@objectstack/lint` minor (new exports and a new advisory), `@objectstack/spec` patch (describe text only). Both packages' `dist` carry the change: measured by grep of `files[]` after the build, with a positive control. ## Mechanism hypotheses, measured 1. **Lit control on the base (`f4ce10c89d`): confirmed.** A flow with a credential-shaped literal in an `http` header, a url query key and a nested connector input got these answers: - The save door (`evaluateRuntimeAuthoringGate`, `state: 'active'`) returned `error: null, advisories: []`. The dark control (the same flow with `{var}` templates) returned the same. - The live-channel control, a `try_catch` with no `catch`, drew exactly one advisory, `flow-try-catch-without-catch`. - `os validate --json` exited 0 with `valid: true`. Its only warning was `No apps or plugins defined — this stack may not do much`, lit and dark alike. - `os lint --json` exited 0 with `passed: true` and `issues: []`, lit and dark alike. 2. **No door-side change: confirmed.** The registry entry alone reaches all three doors. `packages/cli/src/commands/*` and `runtime-authoring-gate.ts` are untouched. 3. **Published surface.** `packages/lint` has no api-surface or export-origins shards. The `.` entry gains `isCredentialShapedLiteral`, `lintFlowCredentialLiterals`, `FLOW_CREDENTIAL_LITERAL` and the type `FlowCredentialLiteralFinding`. `./runtime` exports nothing new. 4. **Derived artifacts moved.** - `check:docs-transcript-drift`: the four CLI transcripts print `Running author-time rules (48)`, up from 47. There is no generator; each line is set to the value the gate derives from `authoringRulesFor(cmd)`. - `check:docs`: the two reference pages above, via `gen:docs`. - No rule catalog exists. - `check:generated` reports all 15 artifacts up to date. 5. **Ablation: confirmed, in the predicted direction.** The ablation ran at `f7fe981913` through `scripts/ablation-replace.mjs` (WRAP mode). It made the predicate always return false, then rebuilt `@objectstack/lint`. - The dist preflight found the marker in 4 built files. - The lint pins went red on 33 tests: all 27 positive predicate cases and 6 rule pins (the pin set, severity, wording, array walk, reach through `runAuthoringRules` for each command, reach through `runRuntimeAuthoringRules`). The negatives, the draws-nothing controls, the registry entry, the export and the one-home pin stayed green, 21 in all. - The save-door pin went red on LIT and stayed green on DARK and draft. The `os validate` and `os lint` pins both went red. - Restore: the blob matched HEAD, `git diff HEAD` was empty, the marker was absent from all 14 dist files and the tree was clean. Then lint 54/54, save door 3/3 and CLI doors 2/2 all passed. ## Verification record (HEAD `ae5d8af6f8`, which includes current `main`) - The door readings after the change, on the same lit and dark fixtures: - `os validate` exits 0 with `valid: true`. Lit prints four `flow-credential-literal` warnings: the header, the url query key, the header inside the `try` region and the nested connector input. Dark prints none. - `os lint` exits 0 with `passed: true`. Lit reports the same four warnings; dark reports none. - The save door returns `error: null`. Lit carries one advisory per literal on the advisory channel; dark and draft carry none. - No finding carries a sentinel value. - The pins: - `packages/lint`: the pin set is the measured scanner's positive-control fixture (literal arm, doc-block arm, probe flow), carried over as the evaluated stack. It draws exactly 12 advisories, and the template, ordinary-value and `signingSecret` controls draw none. - The save door: `packages/metadata-protocol/src/runtime-authoring-gate.flow-credential-literal.test.ts`. - `os validate` / `os lint`: `packages/cli/test/flow-credential-literal-doors.e2e.test.ts`. It spawns the CLI, so it sits in the nightly tier by name; the per-PR half is the per-command `runAuthoringRules` pin in lint. - Tests: - `@objectstack/lint`: 117 files, 5433 tests passed, typecheck exit 0. - `@objectstack/metadata-protocol`: 190 files passed and 3 skipped (2786 tests passed, 19 skipped), typecheck exit 0. - `@objectstack/cli`: unit project 234 files, 3355 tests passed, typecheck exit 0. The door pin passes 2/2 under `OS_TEST_TIERS=nightly`. The per-PR integration project is left to CI. - Example apps: `os lint` draws 0 `flow-credential-literal` findings on `app-showcase`, `app-crm` and `app-todo`. - Gates: - `dispatch-gates --commands` derives 111 gates at `ae5d8af6f8`. All 111 ran with their exit codes captured before any pipe, and all exited 0. - `--ran` reconciliation: 111 derived, 111 run, 0 NOT-MEASURED, 0 UNRUN. - `check:type-check-debt` ran under the verify lock, because it runs its own closure build. - ESLint (a proven narrowing): the config's population is `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`. `--format json` over the 10 changed files in it reports 0 errors and 0 warnings. The config enables no type-aware linting, so this diff cannot move the verdict on any untouched file. ## Acceptance notes - **Boundaries of the rule.** It is the measured R1/R2 rule, unchanged, and it is deliberately not widened here. - It does not judge userinfo in a url, a secret carried in a url path (the incoming-webhook shape), or an `http` node's `body`. - A name like `X-Authorization` is missed by R1 unless its value opens with an auth scheme. - A setting under a credential-sounding name (a `session…` or `auth…` key holding a non-secret string) draws a false positive. That costs a line of reading, which is the ruling's trade. - **`--strict`** on `os validate` / `os lint` promotes this warning to a failure, exactly as it promotes every warning. No door treats it as an error otherwise. - **Staying in step.** The `http` descriptor's `configSchema` text in `service-automation` is #20590's face (PR #20672), and it is being tightened to the same shape routing. The landed flows-guide callout is filed separately for its url-path over-reach, and this PR does not copy it. - **The template test** reuses the `http` executor's interpolation token (a single-brace `{…}` span with no brace inside, as `template.ts` interpolates it). The module cites it; no test pins it against that file. --- _Generated by [Claude Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent a8acee2 commit ed54768

17 files changed

Lines changed: 1220 additions & 9 deletions
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/lint': minor
3+
'@objectstack/spec': patch
4+
---
5+
6+
A credential typed as a literal into a flow position that every flow reader is served now draws one `flow-credential-literal` warning at `os validate`, `os build`, `os lint` and the runtime publish gate, and the spec describes of those positions route an outbound credential to a declarative connector's `credentialRef` (#20654).
7+
8+
Clause-②: no
9+
10+
**Why.** A flow definition is served, as authored, to every member who can read flows. The flow read path withholds the credential slots the spec declares, but it cannot withhold a value inside an open map or a url, because it cannot tell a credential there from an ordinary value. The supported home for an outbound credential is a declarative connector: its `auth: { type, credentialRef }` names a secrets-layer reference that is resolved at boot and never stored in metadata.
11+
12+
**What the warning covers.** An `http` node's `config.headers` entry, a query parameter of an `http` node's `config.url`, and a node's `connectorConfig.input` at any depth, including nodes inside `try_catch`, `loop` and `parallel` regions. A value draws when it is a non-blank string with no `{…}` template, and either its name reads as a credential (`Authorization`, `Cookie`, `x-api-key`, a name carrying `token`, `secret`, `password` and similar) or it opens with an auth scheme (`Bearer`, `Basic`, `Token`, `Digest`, `ApiKey`) followed by a value. A `{variable}` template is resolved per run and draws nothing.
13+
14+
**What it does not do.** It never refuses: every finding is a `warning`, and a save, validate, build or lint that passed before still passes (`--strict` promotes it, as it promotes every warning). It never echoes the value it names. Nothing is withheld on any read.
15+
16+
**Fix, by where the credential sits.** Declare a `connectors:` entry with a `provider` and call it from a `connector_action` node. A header credential goes to `auth: { type: 'bearer', credentialRef }`, or to `auth: { type: 'api-key', headerName, credentialRef }` for a key in a named header. A key in the url's query string goes to `auth: { type: 'api-key', paramName, credentialRef }`. On a connector node, drop the credential from `input`: the connector authenticates through its own `auth.credentialRef`.
17+
18+
`@objectstack/lint` exports the rule `lintFlowCredentialLiterals`, its id `FLOW_CREDENTIAL_LITERAL`, and the one predicate it asks, `isCredentialShapedLiteral(name, value)`. In `@objectstack/spec`, only the descriptions of `HttpConfigSchema.headers` and a flow node's `connectorConfig.input` change; no shape changes.

‎content/docs/deployment/cli.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,7 @@ os compile --json # JSON output for CI pipelines
604604
→ Normalizing stack definition...
605605
→ Lowering inline handlers...
606606
→ Validating protocol compliance...
607-
→ Running author-time rules (47)...
607+
→ Running author-time rules (48)...
608608
→ Checking capability providers (#3366)...
609609
→ Collecting package docs (ADR-0046)... 0 collected
610610
→ Writing artifact...

‎content/docs/deployment/validating-metadata.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,7 @@ A clean run walks the registry and reports timing:
679679
Config: /path/to/support-desk/objectstack.config.ts
680680
Load time: 21ms
681681
→ Validating against ObjectStack Protocol...
682-
→ Running author-time rules (47)...
682+
→ Running author-time rules (48)...
683683
→ Checking capability providers (#3366)...
684684
→ Checking package docs (ADR-0046)...
685685

‎content/docs/getting-started/build-with-claude-code.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ visible: 'status != "resolved"'
270270
◆ Validate
271271
────────────────────────────────────────
272272
→ Validating against ObjectStack Protocol...
273-
→ Running author-time rules (47)...
273+
→ Running author-time rules (48)...
274274
275275
✗ Author-time rules failed (1 issue)
276276
• stack · action 'resolve_ticket' visible: bare reference `status` — a

‎content/docs/references/automation/flow.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ const result = FlowSchema.parse(data);
155155
| :--- | :--- | :--- | :--- |
156156
| **connectorId** | `string` | ✅ | Registered connector name |
157157
| **actionId** | `string` | ✅ | Action key declared by the connector |
158-
| **input** | `Record<string, any>` | optional | Mapped inputs for the action |
158+
| **input** | `Record<string, any>` | optional | Mapped inputs for the action. The flow definition, this map included, is served to every member who can read flows, so never put a credential here: the connector authenticates through its own `auth.credentialRef`. |
159159

160160
### Nested Shape: `FlowNode.inputSchema[string]`
161161

‎content/docs/references/automation/io-node-config.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ const result = HttpConfigSchema.parse(data);
7979
| :--- | :--- | :--- | :--- |
8080
| **url** | `string` | ✅ | Target URL |
8181
| **method** | `string` | optional | HTTP method (default GET; POST when durable) |
82-
| **headers** | `Record<string, string>` | optional | Request headers |
82+
| **headers** | `Record<string, string>` | optional | Request headers. The flow definition, this map included, is served to every member who can read flows, so never put a credential here: declare a connector whose `auth` is `bearer` or `api-key` (a header), with `auth.credentialRef` naming the secret, and call it from a `connector_action` node. |
8383
| **body** | `any` | optional | Request body (JSON-serialised) |
8484
| **durable** | `boolean` | optional | Fire-and-forget via the durable outbox (retry/dead-letter) instead of inline request/response |
8585
| **timeoutMs** | `number` | optional | Per-request timeout (ms) |

‎content/docs/ui/react-pages.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ objectstack validate
382382
────────────────────────────────────────
383383
→ Loading configuration...
384384
→ Validating against ObjectStack Protocol...
385-
→ Running author-time rules (47)...
385+
→ Running author-time rules (48)...
386386
→ Checking capability providers (#3366)...
387387
→ Checking package docs (ADR-0046)...
388388
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* `os validate` and `os lint` — the two CLI doors of the `flow-credential-literal`
5+
* advisory, run for real.
6+
*
7+
* The registry proves the rule is WIRED to all three commands
8+
* (`@objectstack/lint`'s own tests); only running the commands proves each one
9+
* PRINTS it, on the channel an author reads, without failing the run. The
10+
* measured starting point was silence at both doors: a flow carrying a
11+
* credential-shaped literal in an `http` node's headers and url and in a
12+
* connector node's input validated clean and linted clean.
13+
*
14+
* Two fixtures, one flow each: LIT carries three literals (a header, a url
15+
* query parameter, a nested connector input); DARK is the same flow with
16+
* `{var}` templates in those three positions, and must stay silent. Every
17+
* value is a probe sentinel, not a credential.
18+
*
19+
* Integration tier: it spawns the source CLI (`packages/cli/vitest-tiers.ts`).
20+
*/
21+
22+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
23+
import { execFile } from 'node:child_process';
24+
import { mkdtempSync, rmSync } from 'node:fs';
25+
import { tmpdir } from 'node:os';
26+
import { join, resolve } from 'node:path';
27+
import { fileURLToPath } from 'node:url';
28+
import { FLOW_CREDENTIAL_LITERAL } from '@objectstack/lint';
29+
import { childEnv } from './helpers/serve-process.js';
30+
import { writeDefineStackConfig } from './helpers/define-stack-fixture.js';
31+
32+
const HERE = resolve(fileURLToPath(import.meta.url), '..');
33+
const CLI = resolve(HERE, '../bin/run-dev.js');
34+
const TSX = resolve(HERE, '../../../node_modules/.bin/tsx');
35+
36+
const SENTINELS = ['cli-header-sentinel', 'cli-url-sentinel', 'cli-input-sentinel'] as const;
37+
38+
const stackOf = (literal: boolean) => ({
39+
manifest: {
40+
id: `com.example.credential.${literal ? 'lit' : 'dark'}`,
41+
namespace: 'cred',
42+
version: '1.0.0',
43+
type: 'app',
44+
name: 'Credential door probe',
45+
engines: { protocol: '^17' },
46+
},
47+
objects: [
48+
{
49+
name: 'cred_note',
50+
label: 'Note',
51+
pluralLabel: 'Notes',
52+
sharingModel: 'private',
53+
fields: { name: { type: 'text', label: 'Name', required: true } },
54+
},
55+
],
56+
flows: [
57+
{
58+
name: 'cred_probe',
59+
label: 'Credential probe',
60+
type: 'autolaunched',
61+
variables: [{ name: 'api_token', type: 'text', isInput: true }],
62+
nodes: [
63+
{ id: 'start', type: 'start', label: 'Start' },
64+
{
65+
id: 'call',
66+
type: 'http',
67+
label: 'Call',
68+
config: {
69+
url: literal
70+
? `https://example.invalid/hook?api_key=${SENTINELS[1]}`
71+
: 'https://example.invalid/hook?api_key={api_token}',
72+
method: 'POST',
73+
headers: {
74+
Authorization: literal ? `Bearer ${SENTINELS[0]}` : 'Bearer {api_token}',
75+
'X-Trace-Label': 'plain-value',
76+
},
77+
},
78+
},
79+
{
80+
id: 'conn',
81+
type: 'connector_action',
82+
label: 'Connector',
83+
connectorConfig: {
84+
connectorId: 'rest',
85+
actionId: 'request',
86+
input: { method: 'GET', path: '/p', auth: { clientSecret: literal ? SENTINELS[2] : '{api_token}' } },
87+
},
88+
},
89+
{ id: 'end', type: 'end', label: 'End' },
90+
],
91+
edges: [
92+
{ id: 'e1', source: 'start', target: 'call' },
93+
{ id: 'e2', source: 'call', target: 'conn' },
94+
{ id: 'e3', source: 'conn', target: 'end' },
95+
],
96+
},
97+
],
98+
});
99+
100+
const LIT_PATHS = [
101+
'flows[0].nodes[1].config.headers.Authorization',
102+
'flows[0].nodes[1].config.url',
103+
'flows[0].nodes[2].connectorConfig.input.auth.clientSecret',
104+
];
105+
106+
interface Run {
107+
code: number;
108+
stdout: string;
109+
stderr: string;
110+
}
111+
112+
function runCli(args: string[], cwd: string): Promise<Run> {
113+
return new Promise((resolvePromise) => {
114+
execFile(
115+
TSX,
116+
[CLI, ...args],
117+
{ cwd, maxBuffer: 16 * 1024 * 1024, env: childEnv({ NO_COLOR: '1' }) },
118+
(err, stdout, stderr) => {
119+
resolvePromise({
120+
code: err ? (typeof (err as { code?: unknown }).code === 'number' ? (err as unknown as { code: number }).code : 1) : 0,
121+
stdout: String(stdout),
122+
stderr: String(stderr),
123+
});
124+
},
125+
);
126+
});
127+
}
128+
129+
interface Finding {
130+
rule?: string;
131+
path?: string;
132+
severity?: string;
133+
message?: string;
134+
}
135+
136+
/** `os validate --json`: registry advisories ride `warnings` as finding objects. */
137+
const validateFindings = (stdout: string): Finding[] =>
138+
((JSON.parse(stdout) as { warnings?: unknown[] }).warnings ?? []).filter(
139+
(w): w is Finding => !!w && typeof w === 'object' && (w as Finding).rule === FLOW_CREDENTIAL_LITERAL,
140+
);
141+
142+
/** `os lint --json`: every finding is an `issues` entry. */
143+
const lintFindings = (stdout: string): Finding[] =>
144+
((JSON.parse(stdout) as { issues?: Finding[] }).issues ?? []).filter((i) => i.rule === FLOW_CREDENTIAL_LITERAL);
145+
146+
const dirs = new Map<'lit' | 'dark', string>();
147+
148+
beforeAll(() => {
149+
for (const kind of ['lit', 'dark'] as const) {
150+
const dir = mkdtempSync(join(tmpdir(), `os-flow-credential-${kind}-`));
151+
writeDefineStackConfig(dir, stackOf(kind === 'lit'));
152+
dirs.set(kind, dir);
153+
}
154+
});
155+
156+
afterAll(() => {
157+
for (const dir of dirs.values()) rmSync(dir, { recursive: true, force: true });
158+
});
159+
160+
describe('flow-credential-literal at the CLI doors', () => {
161+
it('os validate — LIT prints one warning per literal and still passes; DARK prints none', async () => {
162+
const lit = await runCli(['validate', '--json'], dirs.get('lit')!);
163+
expect(lit.code, `validate failed:\n${lit.stdout}\n${lit.stderr}`).toBe(0);
164+
expect((JSON.parse(lit.stdout) as { valid?: boolean }).valid).toBe(true);
165+
const found = validateFindings(lit.stdout);
166+
expect(found.map((f) => f.path)).toEqual(LIT_PATHS);
167+
expect(found.every((f) => f.severity === 'warning')).toBe(true);
168+
// The route is in the message, which is what the text face prints.
169+
expect(found.every((f) => (f.message ?? '').includes('auth.credentialRef'))).toBe(true);
170+
for (const sentinel of SENTINELS) expect(JSON.stringify(found).includes(sentinel), sentinel).toBe(false);
171+
172+
const dark = await runCli(['validate', '--json'], dirs.get('dark')!);
173+
expect(dark.code, `validate failed:\n${dark.stdout}\n${dark.stderr}`).toBe(0);
174+
expect(validateFindings(dark.stdout)).toEqual([]);
175+
}, 180_000);
176+
177+
it('os lint — LIT reports one warning per literal and still passes; DARK reports none', async () => {
178+
const lit = await runCli(['lint', '--json'], dirs.get('lit')!);
179+
expect(lit.code, `lint failed:\n${lit.stdout}\n${lit.stderr}`).toBe(0);
180+
expect((JSON.parse(lit.stdout) as { passed?: boolean }).passed).toBe(true);
181+
const found = lintFindings(lit.stdout);
182+
expect(found.map((f) => f.path)).toEqual(LIT_PATHS);
183+
expect(found.every((f) => f.severity === 'warning')).toBe(true);
184+
expect(found.every((f) => (f.message ?? '').includes('auth.credentialRef'))).toBe(true);
185+
for (const sentinel of SENTINELS) expect(JSON.stringify(found).includes(sentinel), sentinel).toBe(false);
186+
187+
const dark = await runCli(['lint', '--json'], dirs.get('dark')!);
188+
expect(dark.code, `lint failed:\n${dark.stdout}\n${dark.stderr}`).toBe(0);
189+
expect(lintFindings(dark.stdout)).toEqual([]);
190+
}, 180_000);
191+
});

‎packages/lint/src/authoring-rules.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ import { validateRuleCompilability } from './validate-rule-compilability.js';
138138
import { validateRuleSchemaFormats } from './validate-rule-schema-formats.js';
139139
import { validateActionLocations } from './validate-action-locations.js';
140140
import { lintFlowPatterns } from './lint-flow-patterns.js';
141+
import { lintFlowCredentialLiterals } from './lint-flow-credential-literals.js';
141142
import { lintLivenessProperties } from './lint-liveness-properties.js';
142143
import { lintAutonumberFormats } from './lint-autonumber-formats.js';
143144
import { lintViewRefs } from './lint-view-refs.js';
@@ -1465,6 +1466,33 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [
14651466
hint: f.hint,
14661467
})),
14671468
},
1469+
// A credential typed as a LITERAL into a flow position every flow reader is
1470+
// served — an `http` node's `headers` or `url` query, a node's
1471+
// `connectorConfig.input` — named at every authoring door, with the
1472+
// declarative connector's `credentialRef` as the route (the triage ruling on
1473+
// #20590, direction A). Nothing is withheld and nothing is refused: the
1474+
// predicate is a heuristic, so the tier is `advisory`, and
1475+
// `authoring-rule-wiring.test.ts` holds the source to it. The fourth door is the one a Studio / REST / MCP
1476+
// author of a flow has, so the entry is on it for `flow`, exactly where
1477+
// `lintFlowPatterns`' advisories already surface.
1478+
{
1479+
name: 'lintFlowCredentialLiterals',
1480+
tier: 'advisory',
1481+
input: 'parsed',
1482+
commands: ALL,
1483+
source: 'packages/lint/src/lint-flow-credential-literals.ts',
1484+
surfaces: CLI_AND_RUNTIME,
1485+
runtimeTypes: ['flow'],
1486+
run: (stack) =>
1487+
lintFlowCredentialLiterals(stack).map((f) => ({
1488+
severity: f.severity,
1489+
rule: f.rule,
1490+
where: f.where,
1491+
path: f.path,
1492+
message: f.message,
1493+
hint: f.hint,
1494+
})),
1495+
},
14681496
// The spec-liveness loop on the author side: a property the ledger marks
14691497
// dead-and-misleading or experimental is set hopefully and does nothing.
14701498
// Ledger-driven (entries opt in via `authorWarn`), so it is high-signal and

0 commit comments

Comments
 (0)