Skip to content

Commit 7f17135

Browse files
feat(connector-openapi): declarative providerConfig.include operation allowlist (#27387)
Part of #26709 (Work item 1 only). Item 3(c) is held for a contract decision, reported on the card; item 4 lives on #27373. Clause-②: yes (widening) Metadata-protocol: added Skills-docs: follow-up owed — skills/objectstack-automation/references/integration-recipes.md, skills/objectstack-automation/evals/integrations-declared.json ## 维护者速读 - 改了什么:声明式 `openapi` 连接器实例新增 `providerConfig.include`(操作白名单),只有列出的操作成为 action;条目写 `operationId`,无 `operationId` 的操作写生成的 `method_path` 键。 - 为什么改:大型 API(GitHub 1,242 个操作、Graph 约 1.8 万)今天只能整份暴露或手工裁剪文档;条目写错或上游改名时启动直接失败并列出文档里的键,不会静默少一个 action。 - 风险与回滚:纯新增键,不写 `include` 的实例行为不变;回滚即还原本 PR,已写 `include` 的实例会重新暴露全部操作。 - 先看:`packages/connectors/connector-openapi/src/openapi-provider.ts`(未知条目拒收与形状校验两段),`openapi-connector.ts`(抽出唯一的 action 键推导 `operationActionKey`),`openapi-provider.test.ts`。 - 两分钟验证:`pnpm --filter @objectstack/connector-openapi exec vitest run --maxWorkers=2 src/openapi-provider.test.ts`,预期 38 passed,其中 `providerConfig.include` 组 9 个用例全绿。 - 需要你做的:无,席位落地。 ## What changes - **`connector-openapi` provider:** `providerConfig.include` (optional) is an array of action keys: an operation's `operationId`, or, when it has none, the generated `method_path` key (`delete /customers/{id}` is `delete_customers_id`). It is handed to the generator's existing `include` predicate (`createOpenApiConnector`'s `include?: (op: OperationInfo) => boolean`), which the provider did not forward before. - **Unknown entry = fatal configuration fault.** An entry the document does not carry throws a plain `Error` after the bundle is generated: fatal at boot, a skipped entry on reload, never `ConnectorUpstreamUnavailableError`. The message names the entry and lists up to 20 of the document's keys. - **Shape checked before the document loads.** `include` that is not an array of non-empty strings throws plain before any spec fetch, so a malformed allowlist never exposes the whole document. - **One key derivation.** `operationActionKey` (exported from `openapi-connector.ts`, not from the package index) is the single spelling of "operationId, else the `method_path` slug". The generator keys its actions with it, and the provider matches `include` entries against it, so the entry and the action it admits can never be spelled two ways. - **Docs:** `content/docs/automation/connectors.mdx` documents the key in the `openapi` section. One changeset (`@objectstack/connector-openapi`: minor). Same allowlist meaning as the `mcp` provider's `include` (`mcp-provider.ts:48`, `:223-226`): a list of names, and only listed names become actions. The two providers now differ on bad entries. The card text makes an unknown `openapi` entry fatal, but the `mcp` provider still ignores an unknown tool name (zero actions) and drops a non-array `include` without a word (every tool exposed). This PR does not touch `connector-mcp`; the divergence is reported on the card for a decision. ## Tests Head `3d7773a5`. New `describe('openapi provider — providerConfig.include')` in `openapi-provider.test.ts`, 9 cases: - control: no `include` gives every operation (`listCustomers`, `createCustomer`, `delete_customers_id`); - `include: ['listCustomers']` gives exactly that action and that handler; - an operation without `operationId` is named by its `method_path` key; document order is kept; - `include: ['listCustomers', 'nope']` is refused with a plain `Error` (not upstream-unavailable). The message names `'nope'` only and lists the document's keys; - an operation that has an `operationId` is not named by its `method_path` key; - `'listCustomers'`, `['listCustomers', 7]`, `['']`, an object and `null` are each refused by shape, and the spec URL is fetched 0 times; - `include: []` admits nothing, the same as an empty `mcp` include; - real boot (`LiteKernel` + `AutomationServicePlugin` + `ConnectorOpenApiPlugin`, declared through the registry): the instance is `ready` with exactly `['listCustomers']` in `getConnectorDescriptors()`; with `['nope']` the bootstrap rejects naming it, and no degraded reason is recorded. These are provider-level plain-`Error` faults. No HTTP envelope is involved, so the assertions check the error class, the upstream-unavailable marker and the named entry, the same way this file's existing Swagger 2.0 refusals do. <details><summary>Package test + typecheck (os-verify-lock, VERDICT command-exit 0)</summary> ```text pnpm --filter @objectstack/connector-openapi test && pnpm --filter @objectstack/connector-openapi typecheck Test Files 4 passed (4) Tests 50 passed (50) check:test-typecheck: OK — @objectstack/connector-openapi's test layer compiles under packages/connectors/connector-openapi/tsconfig.test.json; 1 file(s) / 5 error(s) / 1 pinned signature(s) held in test-typecheck-debt.json os-verify-lock: VERDICT command-exit 0 ``` eslint --no-inline-config on the three changed .ts files: exit 0. </details> <details><summary>Ablations (node scripts/ablation-replace.mjs; tests import src directly, no dist hop; each restore proven blob == HEAD 9609ea6, git diff HEAD empty)</summary> - A1, the provider stops forwarding the predicate (`include,` to `include: undefined,`): exit 1, 5 failed. The four allowlist cases fail, and so does the boot case that expects exactly `['listCustomers']`. - A2, the unknown-entry refusal is disabled (the `unknown.length` comparison flipped so it is never true): exit 1, 3 failed | 35 passed (`'nope'`, the method_path-for-operationId case, the boot refusal). - A3, the shape check is disabled: exit 1, 1 failed | 37 passed (the shape case). </details> ## Acceptance notes - The card's acceptance line for item 1 (`include: ['listCustomers']` exposes exactly that action; `include: ['nope']` refuses at materialization with the operation name in the message) is pinned at both the factory and the real-boot layer. - A key the generator de-duplicates (two operations sharing one `operationId`, which yields `x` and `x_2`) is admitted by its base key, which admits both. The `_2` spelling is not an `include` entry. Noted, not filed: no producer measured. - `skills/objectstack-automation/references/integration-recipes.md` (line 63, "The `openapi` provider has no `include` key — the trimmed document is the allowlist.") and the `integrations-declared.json` eval's expected outputs become false when this lands. `skills/**` is Tier H and outside this PR, so the follow-up is owed and named in the `Skills-docs:` line. - File surface: `openapi-connector.ts` sits outside the claim's declared surface. It was edited because the provider needs the generator's own key derivation. The other way was a second copy of the slug, i.e. two spellings of one key. The edit is the helper plus its one call site. ## Verification <details><summary>Derived gate families (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, tree 3d7773a, 5 paths)</summary> 88 commands, run sequentially, each exit captured before any pipe: 85 exit 0; 3 exit 3 PREREQUISITE NOT MET = NOT MEASURED: - `node scripts/check-plugin-teardown-shape.mjs --self-test` (asks for `git fetch --unshallow`); - `pnpm --filter @objectstack/spec run check:skill-examples` (reads `packages/client-react/dist`, not built here); - `pnpm check:dual-build-cjs-loads` (reads every package's `dist`; Build Core owns it). `--ran` reconciliation: "88 derived famil(ies) accounted for — 85 run, 3 NOT-MEASURED (3 DERIVED from a recorded exit 3)", exit 0. </details> Not run locally, CI-owned: `test:repo`, the repo-wide lint, and the full families of the required checks. `connector-mcp` is untouched, so it owes no test here. There is no dogfood run: the title has no `!` and Clause-② carries no `(narrowing)` arm. --- _Generated by [Claude Code](https://claude.ai/code/session_017xRBcVBKAFk1ntXSjvPt7U)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5c0e4fd commit 7f17135

5 files changed

Lines changed: 265 additions & 5 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/connector-openapi': minor
3+
---
4+
5+
feat(connector-openapi): a declarative `openapi` instance takes `providerConfig.include`, an operation allowlist
6+
7+
Clause-②: yes (widening)
8+
Metadata-protocol: added
9+
10+
A `provider: 'openapi'` connector instance can now list the operations it exposes: `providerConfig.include: string[]`. Only the listed operations become actions, so a large API (hundreds or thousands of operations) comes up as the handful of actions its flows call, without trimming the document by hand. The key has the same allowlist meaning as the `mcp` provider's `include`.
11+
12+
- **An entry is an action key.** Write an operation's `operationId`, or, for an operation the document gives no `operationId`, its generated `method_path` key (`delete /customers/{id}` is `delete_customers_id`). An operation that has an `operationId` is named only by it. These are the same keys the actions are exposed under, so the `actionId` in a flow and the entry in `include` are spelled the same way.
13+
- **An unknown entry fails the boot.** An entry the document does not carry (a typo, or an operation the upstream renamed or removed) is a configuration fault at materialization: boot fails, or the entry is skipped on reload, with an error naming the entry and listing the document's keys. It is never ignored.
14+
- **The shape is checked before the document loads.** An `include` that is not an array of non-empty strings is refused the same way, so a malformed allowlist never exposes the whole document.
15+
- An instance without `include` is unchanged: every operation is an action.

‎content/docs/automation/connectors.mdx‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,9 @@ Do not put a credential in `headers` or `defaultHeaders`: both are stored in met
118118

119119
### `provider: 'openapi'` — one action per operation
120120

121-
Config: **`spec`** (required) and **`baseUrl`** (optional — overrides the
122-
document's `servers[0].url`). The `spec` takes any of three forms:
121+
Config: **`spec`** (required), **`baseUrl`** (optional — overrides the
122+
document's `servers[0].url`) and **`include`** (optional operation allowlist —
123+
only the listed operations become actions). The `spec` takes any of three forms:
123124

124125
- an **inline OpenAPI 3.x document object** — no I/O at boot, the most
125126
deterministic form;
@@ -148,6 +149,23 @@ Each operation becomes an action keyed by its `operationId` (or a
148149
`method_path` slug when the document omits one) — so `getInvoice` in the
149150
document is `actionId: 'getInvoice'` in your flow.
150151

152+
`include` takes those same action keys and has the same allowlist meaning as
153+
the `mcp` provider's `include`: list the operations the instance should expose
154+
and every other operation stays out — the way to bring up a large API (hundreds
155+
or thousands of operations) as the handful of actions your flows call. An
156+
operation without an `operationId` is named by its generated `method_path` key
157+
(`delete /customers/{id}` → `delete_customers_id`); one that has an
158+
`operationId` is named only by it. An entry the document does not carry — a
159+
typo, or an operation the upstream renamed or removed — fails the boot with an
160+
error naming the entry and listing the document's keys; it is never ignored.
161+
162+
```typescript
163+
providerConfig: {
164+
spec: './specs/crm-openapi.json',
165+
include: ['listCustomers', 'getCustomer', 'delete_customers_id'],
166+
},
167+
```
168+
151169
The document must be **OpenAPI 3.x**. A Swagger 2.0 document — or one with no
152170
`openapi` version field — is refused at boot with an error naming the inline
153171
document, URL or file it came from: convert it to OpenAPI 3.x offline and point

‎packages/connectors/connector-openapi/src/openapi-connector.ts‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ export function createOpenApiConnector(config: OpenApiConnectorConfig): OpenApiC
206206

207207
for (const op of collectOperations(document)) {
208208
if (include && !include(toInfo(op))) continue;
209-
const key = uniqueKey(op.operationId ?? slug(`${op.method}_${op.path}`), seenKeys);
209+
const key = uniqueKey(operationActionKey(op), seenKeys);
210210

211211
actions.push({
212212
key,
@@ -268,6 +268,18 @@ function collectOperations(doc: OpenApiDocument): Op[] {
268268
return ops;
269269
}
270270

271+
/**
272+
* The action key an operation generates: its `operationId`, else the
273+
* `method_path` slug (`get /pets/{id}` → `get_pets_id`). The one derivation of
274+
* that key — the generator keys its actions with it (before the collision
275+
* suffix {@link uniqueKey} adds), and the declarative `openapi` provider's
276+
* `providerConfig.include` matches its entries against it, so an allowlist
277+
* entry and the action it admits can never be spelled two ways.
278+
*/
279+
export function operationActionKey(op: Pick<OperationInfo, 'operationId' | 'method' | 'path'>): string {
280+
return op.operationId ?? slug(`${op.method}_${op.path}`);
281+
}
282+
271283
function toInfo(op: Op): OperationInfo {
272284
return {
273285
operationId: op.operationId,

‎packages/connectors/connector-openapi/src/openapi-provider.test.ts‎

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44
// `provider: 'openapi'` connector instance from a spec document + resolved auth.
55

66
import { describe, it, expect } from 'vitest';
7+
import { LiteKernel } from '@objectstack/core';
78
import type { ConnectorProviderContext } from '@objectstack/spec/integration';
89
import { isConnectorUpstreamUnavailable, RetryConfigSchema } from '@objectstack/spec/integration';
10+
import { AutomationServicePlugin, type AutomationEngine } from '@objectstack/service-automation';
11+
import { ConnectorOpenApiPlugin } from './connector-openapi-plugin.js';
912
import { createOpenApiProviderFactory, OPENAPI_PROVIDER_KEY } from './openapi-provider.js';
1013

1114
const petstore = {
@@ -322,3 +325,143 @@ describe('openapi provider — refuses a document that is not OpenAPI 3.x', () =
322325
}
323326
});
324327
});
328+
329+
// ── providerConfig.include — a declarative operation allowlist ─────────────
330+
//
331+
// The same allowlist meaning as the mcp provider's `include`: only the listed
332+
// operations become actions. An entry is an operation's action key — its
333+
// operationId, or the generated method_path key when it has none — and an
334+
// entry naming an operation the document does not carry is a plain (fatal)
335+
// configuration fault at materialization, never ignored.
336+
337+
describe('openapi provider — providerConfig.include', () => {
338+
const crm = {
339+
openapi: '3.0.3',
340+
info: { title: 'CRM', version: '1.0.0' },
341+
servers: [{ url: 'https://crm.example.com' }],
342+
paths: {
343+
'/customers': {
344+
get: { operationId: 'listCustomers', responses: { '200': { description: 'ok' } } },
345+
post: { operationId: 'createCustomer', responses: { '201': { description: 'created' } } },
346+
},
347+
'/customers/{id}': {
348+
// No operationId: the action key is the generated method_path slug.
349+
delete: { responses: { '204': { description: 'gone' } } },
350+
},
351+
},
352+
};
353+
354+
async function refusal(result: unknown): Promise<Error> {
355+
const err = await Promise.resolve(result).then(
356+
() => { throw new Error('expected rejection'); },
357+
(e: unknown) => e,
358+
);
359+
expect(err).toBeInstanceOf(Error);
360+
expect(isConnectorUpstreamUnavailable(err)).toBe(false);
361+
return err as Error;
362+
}
363+
364+
it('without include, every operation is an action (control)', async () => {
365+
const { def } = await createOpenApiProviderFactory()(ctx({ providerConfig: { spec: crm } }));
366+
expect(def.actions?.map((a) => a.key)).toEqual(['listCustomers', 'createCustomer', 'delete_customers_id']);
367+
});
368+
369+
it("include: ['listCustomers'] exposes exactly that action", async () => {
370+
const { def, handlers } = await createOpenApiProviderFactory()(
371+
ctx({ providerConfig: { spec: crm, include: ['listCustomers'] } }),
372+
);
373+
expect(def.actions?.map((a) => a.key)).toEqual(['listCustomers']);
374+
expect(Object.keys(handlers)).toEqual(['listCustomers']);
375+
});
376+
377+
it('names an operation without an operationId by its generated method_path key', async () => {
378+
const { def } = await createOpenApiProviderFactory()(
379+
ctx({ providerConfig: { spec: crm, include: ['delete_customers_id', 'createCustomer'] } }),
380+
);
381+
// Document order, not include order — the allowlist filters, it does not reorder.
382+
expect(def.actions?.map((a) => a.key)).toEqual(['createCustomer', 'delete_customers_id']);
383+
});
384+
385+
it("include: ['nope'] refuses at materialization, naming the entry and the document's keys", async () => {
386+
const err = await refusal(
387+
createOpenApiProviderFactory()(ctx({ providerConfig: { spec: crm, include: ['listCustomers', 'nope'] } })),
388+
);
389+
expect(err.message).toContain("connector 'pets' providerConfig.include names 'nope', which the OpenAPI document does not carry");
390+
expect(err.message).toContain("'listCustomers', 'createCustomer', 'delete_customers_id'");
391+
expect(err.message).not.toContain("'listCustomers', which");
392+
});
393+
394+
it('an operation that has an operationId is not named by its method_path key', async () => {
395+
const err = await refusal(
396+
createOpenApiProviderFactory()(ctx({ providerConfig: { spec: crm, include: ['get_customers'] } })),
397+
);
398+
expect(err.message).toContain("names 'get_customers', which the OpenAPI document does not carry");
399+
});
400+
401+
it('refuses an include that is not an array of non-empty strings, before the document is fetched', async () => {
402+
let fetched = 0;
403+
const fetchImpl = (async () => {
404+
fetched++;
405+
return { ok: true, status: 200, json: async () => crm } as unknown as Response;
406+
}) as unknown as typeof fetch;
407+
const factory = createOpenApiProviderFactory({ fetchImpl });
408+
for (const include of ['listCustomers', ['listCustomers', 7], [''], { listCustomers: true }, null]) {
409+
const err = await refusal(factory(ctx({ providerConfig: { spec: 'https://crm.example.com/openapi.json', include } })));
410+
expect(err.message, JSON.stringify(include)).toContain("connector 'pets' providerConfig.include must be an array of operation keys");
411+
}
412+
expect(fetched).toBe(0);
413+
});
414+
415+
it('an empty include admits no operation, as an empty mcp include admits no tool', async () => {
416+
const { def } = await createOpenApiProviderFactory()(ctx({ providerConfig: { spec: crm, include: [] } }));
417+
expect(def.actions).toEqual([]);
418+
});
419+
420+
describe('through a real boot (AutomationServicePlugin + ConnectorOpenApiPlugin)', () => {
421+
async function boot(include: unknown) {
422+
const declared = [
423+
{ name: 'crm_api', label: 'CRM API', type: 'api', provider: 'openapi', providerConfig: { spec: crm, include } },
424+
];
425+
const kernel = new LiteKernel({ logger: { level: 'silent' } } as never);
426+
kernel.use(new AutomationServicePlugin());
427+
kernel.use({
428+
name: 'test.connector-metadata',
429+
type: 'standard',
430+
version: '1.0.0',
431+
dependencies: ['com.objectstack.service-automation'],
432+
async init(c: { registerService(name: string, service: unknown): void }) {
433+
c.registerService('objectql', {
434+
registry: { listItems: (type: string) => (type === 'connector' ? declared : []) },
435+
});
436+
},
437+
async start() {},
438+
} as never);
439+
kernel.use(new ConnectorOpenApiPlugin());
440+
return kernel;
441+
}
442+
443+
it('a declared include reaches GET /connectors as exactly the listed action', async () => {
444+
const kernel = await boot(['listCustomers']);
445+
try {
446+
await kernel.bootstrap();
447+
const engine = kernel.getService<AutomationEngine>('automation');
448+
const crmApi = engine.getConnectorDescriptors().find((d) => d.name === 'crm_api');
449+
expect(crmApi?.state).toBe('ready');
450+
expect(crmApi?.actions.map((a) => a.key)).toEqual(['listCustomers']);
451+
} finally {
452+
await kernel.shutdown().catch(() => {});
453+
}
454+
});
455+
456+
it('an unknown include entry fails the boot instead of degrading', async () => {
457+
const kernel = await boot(['nope']);
458+
try {
459+
await expect(kernel.bootstrap()).rejects.toThrow(/providerConfig\.include names 'nope'/);
460+
const engine = kernel.getService<AutomationEngine>('automation');
461+
expect(engine.getConnectorDegradedReason('crm_api')).toBeUndefined();
462+
} finally {
463+
await kernel.shutdown().catch(() => {});
464+
}
465+
});
466+
});
467+
});

‎packages/connectors/connector-openapi/src/openapi-provider.ts‎

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
import { ConnectorUpstreamUnavailableError } from '@objectstack/spec/integration';
99
import {
1010
createOpenApiConnector,
11+
operationActionKey,
1112
type OpenApiDocument,
1213
type RestAuth,
1314
} from './openapi-connector.js';
@@ -50,8 +51,40 @@ interface OpenApiProviderConfig {
5051
spec?: unknown;
5152
/** Override the base URL (else the document's `servers[0].url`). */
5253
baseUrl?: unknown;
54+
/**
55+
* Optional operation allowlist — only the listed operations become actions,
56+
* the same allowlist meaning as the `mcp` provider's `include` (tool names
57+
* there, action keys here). Each entry is an operation's `operationId`, or
58+
* the generated `method_path` key (`get_pets_id`) for an operation the
59+
* document gives no `operationId` — i.e. exactly the action key the
60+
* operation is exposed under. An entry naming an operation the document
61+
* does not carry is a configuration fault (see {@link resolveIncludeList}).
62+
*/
63+
include?: unknown;
64+
}
65+
66+
/**
67+
* Validate `providerConfig.include`: absent ⇒ every operation; otherwise an
68+
* array of non-empty strings. Any other shape is a configuration fault, thrown
69+
* plain (fatal at boot, a skipped entry on reload) — an allowlist that silently
70+
* dropped an entry it could not read would expose the very operations the
71+
* author meant to keep out, or hide the one they meant to keep in.
72+
*/
73+
function resolveIncludeList(raw: unknown, connectorName: string): readonly string[] | undefined {
74+
if (raw === undefined) return undefined;
75+
if (!Array.isArray(raw) || raw.some((entry) => typeof entry !== 'string' || entry.length === 0)) {
76+
throw new Error(
77+
`connector-openapi provider: connector '${connectorName}' providerConfig.include must be an array of ` +
78+
`operation keys (strings): each an operationId, or the generated method_path key for an operation ` +
79+
`without one.`,
80+
);
81+
}
82+
return raw as string[];
5383
}
5484

85+
/** How many of the document's action keys an unknown-entry refusal lists. */
86+
const INCLUDE_REFUSAL_KEY_SAMPLE = 20;
87+
5588
/**
5689
* Refuse a document that is not OpenAPI 3.x — a Swagger 2.0 document
5790
* (`swagger: "2.0"`) included. The generator maps the 3.x shape only
@@ -204,8 +237,13 @@ async function loadOpenApiDocument(
204237
* generates for a hand-wired OpenAPI connector — one action per operation over a
205238
* static-auth HTTP transport, with the resolved `auth` applied.
206239
*
240+
* `providerConfig.include` (optional) narrows the actions to an allowlist of
241+
* operation keys, through the generator's own `include` predicate.
242+
*
207243
* Hard-fails on invalid config (missing spec, a wrong spec URL, an unparseable
208-
* document, a document that is not OpenAPI 3.x, a bad base URL), so a misconfigured instance fails boot loudly. A
244+
* document, a document that is not OpenAPI 3.x, a bad base URL, an `include`
245+
* that is not a string array or names an operation the document does not
246+
* carry), so a misconfigured instance fails boot loudly. A
209247
* remote spec URL that is merely unreachable / transiently failing instead
210248
* degrades the instance and retries (#3049) — see {@link loadOpenApiDocument}.
211249
*/
@@ -217,20 +255,54 @@ export function createOpenApiProviderFactory(deps: OpenApiProviderDeps = {}): Co
217255
`connector-openapi provider: connector '${ctx.name}' providerConfig.baseUrl must be a string when set.`,
218256
);
219257
}
258+
const includeList = resolveIncludeList(cfg.include, ctx.name);
220259
const document = await loadOpenApiDocument(cfg.spec, deps.fetchImpl, ctx);
221260
const auth = ctx.auth as ResolvedConnectorAuth | undefined as RestAuth | undefined;
222-
return createOpenApiConnector({
261+
// The allowlist rides the generator's own `include` predicate; every key
262+
// the document offers is recorded on the way, so an entry nothing matched
263+
// can be named in the refusal below.
264+
const documentKeys: string[] = [];
265+
const include = includeList
266+
? (op: Parameters<typeof operationActionKey>[0]) => {
267+
const key = operationActionKey(op);
268+
documentKeys.push(key);
269+
return includeList.includes(key);
270+
}
271+
: undefined;
272+
const bundle = createOpenApiConnector({
223273
name: ctx.name,
224274
label: ctx.label,
225275
description: ctx.description,
226276
document,
227277
baseUrl: typeof cfg.baseUrl === 'string' ? cfg.baseUrl : undefined,
228278
auth,
279+
include,
229280
// ADR-0049 · #18975 — the authored resilience policy, already resolved by
230281
// the materializer, reaches the transport this bundle closes over.
231282
retryConfig: ctx.retryConfig,
232283
requestTimeoutMs: ctx.requestTimeoutMs,
233284
fetchImpl: deps.fetchImpl,
234285
});
286+
if (includeList) {
287+
// An entry naming an operation the document does not carry is a
288+
// configuration fault, thrown plain — fatal at boot, a skipped entry on
289+
// reload. Ignoring it would bring the instance up without the action
290+
// the author listed (a typo, or an operation the upstream renamed or
291+
// removed), and the first sign would be a flow failing at run time.
292+
const offered = new Set(documentKeys);
293+
const unknown = [...new Set(includeList)].filter((entry) => !offered.has(entry));
294+
if (unknown.length > 0) {
295+
const sample = [...offered].slice(0, INCLUDE_REFUSAL_KEY_SAMPLE);
296+
const more = offered.size > sample.length ? `, … ${offered.size - sample.length} more` : '';
297+
throw new Error(
298+
`connector-openapi provider: connector '${ctx.name}' providerConfig.include names ` +
299+
`${unknown.map((entry) => `'${entry}'`).join(', ')}, which the OpenAPI document does not carry. ` +
300+
`Each entry must be an operationId, or the generated method_path key for an operation without one; ` +
301+
`the document offers ${offered.size} operation key(s): ` +
302+
`${sample.map((key) => `'${key}'`).join(', ')}${more}.`,
303+
);
304+
}
305+
}
306+
return bundle;
235307
};
236308
}

0 commit comments

Comments
 (0)