Skip to content

Commit aca58cd

Browse files
feat(lint): warn on a credential-shaped literal in a connector's header or env map (#29424)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9QkrVNjxTuWMqLuQJ2j3ycU)_ Fixes #29341 Clause-②: yes (widening) Three new exports on the `@objectstack/lint` entry and a new advisory on the connector save door; nothing is refused or narrowed. Metadata-protocol: none ## 维护者速读 - 改了什么:连接器四个请求头/环境变量映射(`providerConfig.headers`、`defaultHeaders`、`transport.headers`、`transport.env`)里写死的疑似凭据,`os validate` / `os build` / `os lint` 与连接器保存都会给出警告,指向 `auth.credentialRef`。 - 为什么改:执行 #28640 裁决 ①A(记录 6566912252):这些映射读取时整块隐去,但写死的密钥仍存在元数据里;作者在写的时候就该被提醒。 - 风险与回滚:只是警告,任何保存都不会被拒;判定沿用流程规则那套启发式,误报只多一行提示。回滚即撤销本 PR。 - 先看:`packages/lint/src/lint-connector-credential-literals.ts`(规则与措辞,尤其 stdio 那一句)、`packages/lint/src/authoring-rules.ts` 新条目、`packages/lint/src/runtime-gate.ts` 新增的 `connector` 映射行。 - 两分钟验证:`pnpm --filter @objectstack/lint exec vitest run src/lint-connector-credential-literals.test.ts`,预期 16 个用例全绿。 - 需要你做的:无,席位落地。 ## What changes - **New advisory rule `connector-credential-literal`** (`packages/lint/src/lint-connector-credential-literals.ts`). It asks the existing `isCredentialShapedLiteral` predicate (`credential-literal.ts`, unchanged, no second name list) over each declarative connector entry's four maps: `providerConfig.headers`, `providerConfig.defaultHeaders`, `providerConfig.transport.headers`, `providerConfig.transport.env`. One `warning` per credential-shaped literal, located by path (`connectors[i].providerConfig.defaultHeaders["X-Api-Key"]`), never echoing the value. - **Wording.** Header maps: `header 'NAME' in MAP holds a credential-shaped literal, kept in metadata, not the secrets layer; move it to the connector's auth.credentialRef`, with a fix line naming the `bearer` / `api-key` auth variants. The `transport.env` map gets a different steer, because a stdio MCP server takes no `auth` (see Acceptance notes): `… a stdio server takes no auth; auth.credentialRef reaches it only over http`. - **Registry entry** in `AUTHORING_RULES`: `tier: 'advisory'`, all three commands (`validate`, `build`, `lint`), `surfaces: CLI_AND_RUNTIME`, `runtimeTypes: ['connector']`. - **Save door.** `TYPE_TO_STACK_KEY` gains `connector: 'connectors'`, so a `PUT /meta/connector/:name` publish runs the rule through the existing runtime authoring gate and the findings ride the save response's `advisories` (the channel `flow-credential-literal` already uses). No new channel was built. - **`os explain connector-credential-literal`** entry in `rule-explanations.ts`: the four maps, why withholding on read does not move the value out of metadata, the auth variants, the stdio limit, why it only warns. - Barrel exports `lintConnectorCredentialLiterals`, `CONNECTOR_CREDENTIAL_LITERAL`, `ConnectorCredentialLiteralFinding`. - Changeset `.changeset/29341-connector-credential-literal.md` (`@objectstack/lint` patch). **Forced edits (not tests):** the four CLI transcripts that print `Running author-time rules (N)` go 60 → 61 (`content/docs/deployment/cli.mdx`, `deployment/validating-metadata.mdx`, `getting-started/build-with-claude-code.mdx`, `ui/react-pages.mdx`), because the registry gained one entry (`check:docs-transcript-drift`). No existing test was edited. Not touched: the connector redactor (`connector-credential-redaction.ts`), the package export path (#29342), `credential-literal.ts`, anything in `packages/spec`. ## Tests - `packages/lint/src/lint-connector-credential-literals.test.ts` (16): a `Bearer …` literal in each of the four maps warns at its own path; a credential name alone warns; `Accept: application/json`, an `auth.credentialRef`-routed connector, a template, a blank and a non-string value draw nothing; values outside the four maps are not this rule's; malformed stacks never throw; **the four maps equal the connector redactor's withheld maps**, read through the public `getMetadataTypeRedactor('connector')`; registry entry shape; CLI commands and runtime gate report warnings only; `stackKeyForType('connector') === 'connectors'`; barrel; `os explain` names every map. - `packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts`, new block (2), through the real `saveMetaItem`: LIT saves of a `rest`, an `mcp` http and an `mcp` stdio connector each land (rows stored, literal at rest unchanged) and carry one `warning` advisory per map on the 2xx, with no sentinel in the response; DARK (credentialRef-routed, `Accept` only) carries none. - `packages/cli/test/connector-credential-literal-doors.e2e.test.ts` (2, nightly integration tier like its flow sibling): `os validate --json` and `os lint --json` exit 0 on LIT with the four warnings, and print none on DARK. ## Acceptance notes - **The stdio steer departs from the ruling's phrasing on purpose, and stays inside its letter.** ①A says the warning names "the literal and the `auth.credentialRef` route". Measured: the MCP provider folds a resolved `auth` into an http transport's headers only (`connector-mcp/src/mcp-provider.ts` `applyAuthToHeaders`, "Not applied to stdio transports"), the stdio client receives `transport.env` as its environment, and the connector guide says "Credentials for a stdio server ride `transport.env`, not `auth`". A plain "move it to `auth.credentialRef`" on an env entry would point at a route the runtime does not deliver for stdio. So the env warning names `auth.credentialRef` with its condition (an http transport). Flagged to the seat as an open question. - A TypeScript config that writes `env: { TOKEN: process.env.TOKEN }` for a stdio server is evaluated to a literal before the rule reads it, so it warns at `os validate` too. That is the heuristic's accepted false-positive cost (a line of reading); nothing is refused. - The rule's four-map list is a mirror of the spec's `CONNECTOR_CREDENTIAL_MAP_PATHS`, which the spec does not export (ruling ③A keeps it off the spec export contract). The equality is pinned by test through the public redactor API instead of an import. - A save of the served form (maps withheld) carries the maps forward after the gate runs, so a round trip re-warns nothing; only a map the author actually sends is judged. ## Verification All readings on head `28b45f8e` (this branch with `origin/main` merged), container-local. <details><summary>Tests</summary> - `pnpm --filter @objectstack/lint test`: Test Files 190 passed (190), Tests 7846 passed (7846). - `pnpm --filter @objectstack/metadata-protocol test`: Test Files 304 passed, 4 skipped (308); Tests 22155 passed, 26 skipped. - `pnpm --filter @objectstack/lint typecheck` and `pnpm --filter @objectstack/metadata-protocol typecheck`: exit 0. - `pnpm --filter @objectstack/cli exec vitest run --project unit test/explain-rule-id.test.ts`: 14/14 (every `os explain` entry, the new one included). - `OS_TEST_TIERS=nightly … --project integration test/connector-credential-literal-doors.e2e.test.ts test/flow-credential-literal-doors.e2e.test.ts`: 2 files, 4/4. - `pnpm --filter @objectstack/spec check:generated`: all 14 generated artifacts up to date. - `pnpm lint`: exit 0. </details> <details><summary>Ablation (each leg: mutate through scripts/ablation-replace.mjs, rebuild lint, dist pre-flight, run, restore, rebuild, pre-flight --absent)</summary> - **A1, nothing is refused.** Rule severity mutated to `error`. Lint unit: 3 red (warning assertions; runtime gate "never errors"). Save door: LIT red with `connector/vendor_api failed author-time validation: 2 issues — … [connector-credential-literal]`, the 422 the pin forbids. Restored: blob == HEAD, dist marker absent. - **A2, a non-credential header draws nothing.** The predicate call replaced with "any string". Lint unit: 6 red (Accept and routed connector now warn, LIT paths 8 instead of 4). Save door: LIT and DARK red. Restored. - **A3, the save door is wired.** The `connector: 'connectors'` row renamed. Lint unit: 2 red (`stackKeyForType` null, rule not run at the gate). Save door: LIT red (`expected [] to deeply equal [ …(4) ]`). Restored. </details> <details><summary>Gates</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 90 families on `28b45f8e`; all 90 ran, all exit 0. Five first answered exit 3 (prerequisite not met: a shallow-clone pinned commit, client-react and full package dists, and one queued behind my own build) and were re-run green after `git fetch` of the pinned commit and a full `turbo run build --filter=./packages/**`. `--ran` reconciliation: 90 derived, 90 run, 0 NOT-MEASURED, 0 UNRUN. </details> Dogfood battery: not owed (no `!` in the title, no `(narrowing)` arm). --- _Generated by [Claude Code](https://claude.ai/code/session_01ADHkmPZY72rbsdkcLx2ZoH)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01ADHkmPZY72rbsdkcLx2ZoH <!-- signed: domain:server#3 · os-steve · session_01ADHkmPZY72rbsdkcLx2ZoH --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0c64014 commit aca58cd

13 files changed

Lines changed: 839 additions & 4 deletions
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/lint': patch
3+
---
4+
5+
feat(lint): `connector-credential-literal` — `os validate`, `os build`, `os lint` and the connector save door warn on a credential-shaped literal in a connector's header or env map
6+
7+
Clause-②: yes (widening)
8+
Three new exports on the `@objectstack/lint` entry and a new advisory on the connector save door; nothing is refused or narrowed.
9+
Metadata-protocol: none
10+
11+
A declarative connector entry's four maps — `providerConfig.headers`, `providerConfig.defaultHeaders`, `providerConfig.transport.headers` and `providerConfig.transport.env` — are withheld whole on the metadata read path, but a literal credential in one of them is still stored in metadata. The same `credential-literal` predicate the `flow-credential-literal` advisory asks now judges those four maps and names each credential-shaped entry by its position (never its value), with the `auth.credentialRef` route:
12+
13+
- a header entry: move it to the connector's `auth: { type: 'bearer' | 'api-key' | 'basic', credentialRef }`, which the `rest` provider and an `mcp` http transport send themselves;
14+
- a stdio `transport.env` entry: a stdio MCP server takes no `auth`, so the warning says `auth.credentialRef` reaches the server only over an http transport.
15+
16+
Always a `warning`; nothing is refused and every save lands. The rule runs on all three CLI commands and on the runtime publish gate for `connector` (`PUT /meta/connector/:name`), where it rides the save response's `advisories`. `os explain connector-credential-literal` carries the reasoning.

‎content/docs/deployment/cli.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -771,7 +771,7 @@ os compile --json # JSON output for CI pipelines
771771
→ Normalizing stack definition...
772772
→ Lowering inline handlers...
773773
→ Validating protocol compliance...
774-
→ Running author-time rules (60)...
774+
→ Running author-time rules (61)...
775775
→ Checking that every required capability has a provider installable in this edition...
776776
→ Collecting package docs (ADR-0046)... 0 collected
777777
→ Writing artifact...

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -760,7 +760,7 @@ A clean run walks the registry and reports timing:
760760
Config: /path/to/support-desk/objectstack.config.ts
761761
Load time: 21ms
762762
→ Validating against ObjectStack Protocol...
763-
→ Running author-time rules (60)...
763+
→ Running author-time rules (61)...
764764
→ Checking that every required capability has a provider installable in this edition...
765765
→ Checking package docs (ADR-0046)...
766766

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ visible: 'status != "resolved"'
310310
◆ Validate
311311
────────────────────────────────────────
312312
→ Validating against ObjectStack Protocol...
313-
→ Running author-time rules (60)...
313+
→ Running author-time rules (61)...
314314
315315
✗ Author-time rules failed (1 issue)
316316
• stack · action 'resolve_ticket' visible: bare reference `status` — a

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ objectstack validate
404404
────────────────────────────────────────
405405
→ Loading configuration...
406406
→ Validating against ObjectStack Protocol...
407-
→ Running author-time rules (60)...
407+
→ Running author-time rules (61)...
408408
→ Checking that every required capability has a provider installable in this edition...
409409
→ Checking package docs (ADR-0046)...
410410
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
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
5+
* `connector-credential-literal` 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.
10+
*
11+
* Two fixtures: LIT declares three connectors carrying a `Bearer …` literal in
12+
* each of the four maps (a `rest` entry's `headers` and `defaultHeaders`, an
13+
* `mcp` http transport's `headers`, an `mcp` stdio transport's `env`); DARK is
14+
* the same three routed through `auth.credentialRef` with only ordinary
15+
* entries (`Accept: application/json`) left in the maps, and must stay silent.
16+
* Every value is a probe sentinel, not a credential.
17+
*
18+
* Integration tier: it spawns the source CLI (`packages/cli/vitest-tiers.ts`).
19+
*/
20+
21+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
22+
import { execFile } from 'node:child_process';
23+
import { mkdtempSync, rmSync } from 'node:fs';
24+
import { tmpdir } from 'node:os';
25+
import { join, resolve } from 'node:path';
26+
import { fileURLToPath } from 'node:url';
27+
import { CONNECTOR_CREDENTIAL_LITERAL } from '@objectstack/lint';
28+
import { childEnv } from './helpers/serve-process.js';
29+
import { writeDefineStackConfig } from './helpers/define-stack-fixture.js';
30+
31+
const HERE = resolve(fileURLToPath(import.meta.url), '..');
32+
const CLI = resolve(HERE, '../bin/run-dev.js');
33+
34+
const SENTINELS = [
35+
'cli-headers-sentinel',
36+
'cli-default-headers-sentinel',
37+
'cli-transport-headers-sentinel',
38+
'cli-env-sentinel',
39+
] as const;
40+
41+
const ACCEPT = { Accept: 'application/json' };
42+
43+
const connectorsOf = (literal: boolean) => [
44+
{
45+
name: 'vendor_api',
46+
label: 'Vendor API',
47+
type: 'api',
48+
provider: 'rest',
49+
providerConfig: {
50+
baseUrl: 'https://api.vendor.invalid/v1',
51+
headers: literal ? { ...ACCEPT, Authorization: `Bearer ${SENTINELS[0]}` } : ACCEPT,
52+
defaultHeaders: literal ? { 'X-Vendor-Trace': `Bearer ${SENTINELS[1]}` } : ACCEPT,
53+
},
54+
...(literal ? {} : { auth: { type: 'bearer', credentialRef: 'VENDOR_API_TOKEN' } }),
55+
},
56+
{
57+
name: 'search_tools',
58+
label: 'Search Tools',
59+
type: 'api',
60+
provider: 'mcp',
61+
providerConfig: {
62+
transport: {
63+
kind: 'http',
64+
url: 'https://mcp.vendor.invalid/mcp',
65+
headers: literal ? { Authorization: `Bearer ${SENTINELS[2]}` } : ACCEPT,
66+
},
67+
},
68+
...(literal ? {} : { auth: { type: 'bearer', credentialRef: 'SEARCH_TOOLS_TOKEN' } }),
69+
},
70+
{
71+
name: 'local_tools',
72+
label: 'Local Tools',
73+
type: 'api',
74+
provider: 'mcp',
75+
providerConfig: {
76+
transport: {
77+
kind: 'stdio',
78+
command: 'local-mcp-server',
79+
env: literal ? { MCP_UPSTREAM: `Bearer ${SENTINELS[3]}` } : { LOG_LEVEL: 'info' },
80+
},
81+
},
82+
},
83+
];
84+
85+
const stackOf = (literal: boolean) => ({
86+
manifest: {
87+
id: `com.example.connector.credential.${literal ? 'lit' : 'dark'}`,
88+
namespace: 'conncred',
89+
version: '1.0.0',
90+
type: 'app',
91+
name: 'Connector credential door probe',
92+
engines: { protocol: '^18' },
93+
},
94+
objects: [
95+
{
96+
name: 'conncred_note',
97+
label: 'Note',
98+
pluralLabel: 'Notes',
99+
sharingModel: 'private',
100+
fields: { name: { type: 'text', label: 'Name', required: true } },
101+
},
102+
],
103+
connectors: connectorsOf(literal),
104+
});
105+
106+
const LIT_PATHS = [
107+
'connectors[0].providerConfig.headers.Authorization',
108+
'connectors[0].providerConfig.defaultHeaders["X-Vendor-Trace"]',
109+
'connectors[1].providerConfig.transport.headers.Authorization',
110+
'connectors[2].providerConfig.transport.env.MCP_UPSTREAM',
111+
];
112+
113+
interface Run {
114+
code: number;
115+
stdout: string;
116+
stderr: string;
117+
}
118+
119+
function runCli(args: string[], cwd: string): Promise<Run> {
120+
return new Promise((resolvePromise) => {
121+
execFile(
122+
process.execPath,
123+
[CLI, ...args],
124+
{ cwd, maxBuffer: 16 * 1024 * 1024, env: childEnv({ NO_COLOR: '1' }) },
125+
(err, stdout, stderr) => {
126+
resolvePromise({
127+
code: err ? (typeof (err as { code?: unknown }).code === 'number' ? (err as unknown as { code: number }).code : 1) : 0,
128+
stdout: String(stdout),
129+
stderr: String(stderr),
130+
});
131+
},
132+
);
133+
});
134+
}
135+
136+
interface Finding {
137+
rule?: string;
138+
path?: string;
139+
severity?: string;
140+
message?: string;
141+
}
142+
143+
/** `os validate --json`: registry advisories ride `warnings` as finding objects. */
144+
const validateFindings = (stdout: string): Finding[] =>
145+
((JSON.parse(stdout) as { warnings?: unknown[] }).warnings ?? []).filter(
146+
(w): w is Finding => !!w && typeof w === 'object' && (w as Finding).rule === CONNECTOR_CREDENTIAL_LITERAL,
147+
);
148+
149+
/** `os lint --json`: every finding is an `issues` entry. */
150+
const lintFindings = (stdout: string): Finding[] =>
151+
((JSON.parse(stdout) as { issues?: Finding[] }).issues ?? []).filter((i) => i.rule === CONNECTOR_CREDENTIAL_LITERAL);
152+
153+
const dirs = new Map<'lit' | 'dark', string>();
154+
155+
beforeAll(() => {
156+
for (const kind of ['lit', 'dark'] as const) {
157+
const dir = mkdtempSync(join(tmpdir(), `os-connector-credential-${kind}-`));
158+
writeDefineStackConfig(dir, stackOf(kind === 'lit'));
159+
dirs.set(kind, dir);
160+
}
161+
});
162+
163+
afterAll(() => {
164+
for (const dir of dirs.values()) rmSync(dir, { recursive: true, force: true });
165+
});
166+
167+
describe('connector-credential-literal at the CLI doors', () => {
168+
it('os validate — LIT prints one warning per literal and still passes; DARK prints none', async () => {
169+
const lit = await runCli(['validate', '--json'], dirs.get('lit')!);
170+
expect(lit.code, `validate failed:\n${lit.stdout}\n${lit.stderr}`).toBe(0);
171+
expect((JSON.parse(lit.stdout) as { valid?: boolean }).valid).toBe(true);
172+
const found = validateFindings(lit.stdout);
173+
expect(found.map((f) => f.path)).toEqual(LIT_PATHS);
174+
expect(found.every((f) => f.severity === 'warning')).toBe(true);
175+
// The route is in the message, which is what the text face prints.
176+
expect(found.every((f) => (f.message ?? '').includes('auth.credentialRef'))).toBe(true);
177+
for (const sentinel of SENTINELS) expect(JSON.stringify(found).includes(sentinel), sentinel).toBe(false);
178+
179+
const dark = await runCli(['validate', '--json'], dirs.get('dark')!);
180+
expect(dark.code, `validate failed:\n${dark.stdout}\n${dark.stderr}`).toBe(0);
181+
expect(validateFindings(dark.stdout)).toEqual([]);
182+
}, 180_000);
183+
184+
it('os lint — LIT reports one warning per literal and still passes; DARK reports none', async () => {
185+
const lit = await runCli(['lint', '--json'], dirs.get('lit')!);
186+
expect(lit.code, `lint failed:\n${lit.stdout}\n${lit.stderr}`).toBe(0);
187+
expect((JSON.parse(lit.stdout) as { passed?: boolean }).passed).toBe(true);
188+
const found = lintFindings(lit.stdout);
189+
expect(found.map((f) => f.path)).toEqual(LIT_PATHS);
190+
expect(found.every((f) => f.severity === 'warning')).toBe(true);
191+
expect(found.every((f) => (f.message ?? '').includes('auth.credentialRef'))).toBe(true);
192+
for (const sentinel of SENTINELS) expect(JSON.stringify(found).includes(sentinel), sentinel).toBe(false);
193+
194+
const dark = await runCli(['lint', '--json'], dirs.get('dark')!);
195+
expect(dark.code, `lint failed:\n${dark.stdout}\n${dark.stderr}`).toBe(0);
196+
expect(lintFindings(dark.stdout)).toEqual([]);
197+
}, 180_000);
198+
});

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ import { validateRuleSchemaFormats } from './validate-rule-schema-formats.js';
150150
import { validateActionLocations } from './validate-action-locations.js';
151151
import { lintFlowPatterns } from './lint-flow-patterns.js';
152152
import { lintFlowCredentialLiterals } from './lint-flow-credential-literals.js';
153+
import { lintConnectorCredentialLiterals } from './lint-connector-credential-literals.js';
153154
import { lintLivenessProperties } from './lint-liveness-properties.js';
154155
import { lintAutonumberFormats } from './lint-autonumber-formats.js';
155156
import { lintViewRefs } from './lint-view-refs.js';
@@ -1965,6 +1966,31 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [
19651966
hint: f.hint,
19661967
})),
19671968
},
1969+
// The same predicate over a declarative connector's four header/env maps
1970+
// (`providerConfig.headers`, `defaultHeaders`, `transport.headers`,
1971+
// `transport.env`) — the maps the connector read path withholds whole, which
1972+
// still keep a literal in metadata (ruling 6566912252, letter ①A). Advisory
1973+
// for the same reason as the flow entry above, and on the fourth door for
1974+
// `connector`: `PUT /meta/connector/:name` is the door a Studio / REST / MCP
1975+
// author of a connector has.
1976+
{
1977+
name: 'lintConnectorCredentialLiterals',
1978+
tier: 'advisory',
1979+
input: 'parsed',
1980+
commands: ALL,
1981+
source: 'packages/lint/src/lint-connector-credential-literals.ts',
1982+
surfaces: CLI_AND_RUNTIME,
1983+
runtimeTypes: ['connector'],
1984+
run: (stack) =>
1985+
lintConnectorCredentialLiterals(stack).map((f) => ({
1986+
severity: f.severity,
1987+
rule: f.rule,
1988+
where: f.where,
1989+
path: f.path,
1990+
message: f.message,
1991+
hint: f.hint,
1992+
})),
1993+
},
19681994
// The spec-liveness loop on the author side: a property the ledger marks
19691995
// dead, live-elsewhere or experimental is set hopefully and does nothing
19701996
// here. Ledger-driven (those verdicts warn on their own; any other row only

‎packages/lint/src/index.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,6 +1011,9 @@ export {
10111011
export { lintFlowCredentialLiterals, FLOW_CREDENTIAL_LITERAL } from './lint-flow-credential-literals.js';
10121012
export type { FlowCredentialLiteralFinding } from './lint-flow-credential-literals.js';
10131013
export { isCredentialShapedLiteral } from './credential-literal.js';
1014+
// The same predicate over a declarative connector's four header/env maps.
1015+
export { lintConnectorCredentialLiterals, CONNECTOR_CREDENTIAL_LITERAL } from './lint-connector-credential-literals.js';
1016+
export type { ConnectorCredentialLiteralFinding } from './lint-connector-credential-literals.js';
10141017

10151018
export { lintLivenessProperties } from './lint-liveness-properties.js';
10161019
// #11624 — the ledger's warn set, as a decision procedure. `os lint` runs the

0 commit comments

Comments
 (0)