@objectstack/connector-openapi 15.1.1 → 16.0.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +62 -0
- package/dist/index.d.mts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +27 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +27 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/openapi-provider.test.ts +71 -0
- package/src/openapi-provider.ts +65 -7
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @objectstack/connector-openapi@
|
|
2
|
+
> @objectstack/connector-openapi@16.0.0-rc.0 build /home/runner/work/framework/framework/packages/connectors/connector-openapi
|
|
3
3
|
> tsup --config ../../../tsup.config.ts
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
[34mCLI[39m Cleaning output folder
|
|
11
11
|
[34mESM[39m Build start
|
|
12
12
|
[34mCJS[39m Build start
|
|
13
|
-
[
|
|
14
|
-
[
|
|
15
|
-
[
|
|
16
|
-
[
|
|
17
|
-
[
|
|
18
|
-
[
|
|
13
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m12.94 KB[39m
|
|
14
|
+
[32mESM[39m [1mdist/index.mjs.map [22m[32m39.15 KB[39m
|
|
15
|
+
[32mESM[39m ⚡️ Build success in 83ms
|
|
16
|
+
[32mCJS[39m [1mdist/index.js [22m[32m14.22 KB[39m
|
|
17
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m40.70 KB[39m
|
|
18
|
+
[32mCJS[39m ⚡️ Build success in 88ms
|
|
19
19
|
[34mDTS[39m Build start
|
|
20
|
-
[32mDTS[39m ⚡️ Build success in
|
|
21
|
-
[32mDTS[39m [1mdist/index.d.mts [22m[32m9.
|
|
22
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[32m9.
|
|
20
|
+
[32mDTS[39m ⚡️ Build success in 13445ms
|
|
21
|
+
[32mDTS[39m [1mdist/index.d.mts [22m[32m9.35 KB[39m
|
|
22
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m9.35 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,67 @@
|
|
|
1
1
|
# @objectstack/connector-openapi
|
|
2
2
|
|
|
3
|
+
## 16.0.0-rc.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 41e703b: feat(connector-openapi): degrade + retry on an unreachable remote spec URL (#3049 follow-up)
|
|
8
|
+
|
|
9
|
+
The `openapi` provider fetches `providerConfig.spec` when it is an http(s) URL.
|
|
10
|
+
That fetch previously threw plain on any failure, so a momentarily-unreachable
|
|
11
|
+
spec endpoint aborted the whole app boot. It now classifies the fault the same
|
|
12
|
+
way `connector-mcp` classifies its connect path (ADR-0097):
|
|
13
|
+
|
|
14
|
+
- **Network error** (DNS / connection refused / timeout) or a **transient HTTP
|
|
15
|
+
status** (`408` / `429` / `5xx`, mirroring the `retryableStatusCodes`
|
|
16
|
+
convention) throws `ConnectorUpstreamUnavailableError` — the materializer
|
|
17
|
+
degrades the instance (`state: 'degraded'` on `GET /connectors`, dispatch
|
|
18
|
+
fails clearly) and retries with backoff plus on every `metadata:reloaded`.
|
|
19
|
+
- A **wrong URL** (non-retryable `4xx`) or an **unparseable document** stays a
|
|
20
|
+
plain, fatal configuration fault.
|
|
21
|
+
|
|
22
|
+
Inline and file-path (`#3016`) specs do no boot I/O and are unaffected.
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- Updated dependencies [f972574]
|
|
27
|
+
- Updated dependencies [22013aa]
|
|
28
|
+
- Updated dependencies [3ad3dd5]
|
|
29
|
+
- Updated dependencies [3a18b60]
|
|
30
|
+
- Updated dependencies [a8aa34c]
|
|
31
|
+
- Updated dependencies [e057f42]
|
|
32
|
+
- Updated dependencies [a3823b2]
|
|
33
|
+
- Updated dependencies [43a3efb]
|
|
34
|
+
- Updated dependencies [524696a]
|
|
35
|
+
- Updated dependencies [5e3301d]
|
|
36
|
+
- Updated dependencies [dd9f223]
|
|
37
|
+
- Updated dependencies [46e876c]
|
|
38
|
+
- Updated dependencies [5f05de2]
|
|
39
|
+
- Updated dependencies [021ba4c]
|
|
40
|
+
- Updated dependencies [158aa14]
|
|
41
|
+
- Updated dependencies [d2723e2]
|
|
42
|
+
- Updated dependencies [fefcd54]
|
|
43
|
+
- Updated dependencies [beaf2de]
|
|
44
|
+
- Updated dependencies [369eb6e]
|
|
45
|
+
- Updated dependencies [b659111]
|
|
46
|
+
- Updated dependencies [5754a23]
|
|
47
|
+
- Updated dependencies [6c270a6]
|
|
48
|
+
- Updated dependencies [290e2f0]
|
|
49
|
+
- Updated dependencies [668dd17]
|
|
50
|
+
- Updated dependencies [8abf133]
|
|
51
|
+
- Updated dependencies [e0859b1]
|
|
52
|
+
- Updated dependencies [04ecd4e]
|
|
53
|
+
- Updated dependencies [4d5a892]
|
|
54
|
+
- Updated dependencies [16cebeb]
|
|
55
|
+
- Updated dependencies [86d30af]
|
|
56
|
+
- Updated dependencies [8923843]
|
|
57
|
+
- Updated dependencies [a2795f6]
|
|
58
|
+
- Updated dependencies [f16b492]
|
|
59
|
+
- Updated dependencies [4b6fde8]
|
|
60
|
+
- Updated dependencies [2018df9]
|
|
61
|
+
- Updated dependencies [fc5a3a2]
|
|
62
|
+
- @objectstack/spec@16.0.0-rc.0
|
|
63
|
+
- @objectstack/core@16.0.0-rc.0
|
|
64
|
+
|
|
3
65
|
## 15.1.1
|
|
4
66
|
|
|
5
67
|
### Patch Changes
|
package/dist/index.d.mts
CHANGED
|
@@ -201,8 +201,10 @@ interface OpenApiProviderDeps {
|
|
|
201
201
|
* generates for a hand-wired OpenAPI connector — one action per operation over a
|
|
202
202
|
* static-auth HTTP transport, with the resolved `auth` applied.
|
|
203
203
|
*
|
|
204
|
-
* Hard-fails on invalid config (missing
|
|
205
|
-
* misconfigured instance fails boot loudly.
|
|
204
|
+
* Hard-fails on invalid config (missing spec, a wrong spec URL, an unparseable
|
|
205
|
+
* document, a bad base URL), so a misconfigured instance fails boot loudly. A
|
|
206
|
+
* remote spec URL that is merely unreachable / transiently failing instead
|
|
207
|
+
* degrades the instance and retries (#3049) — see {@link loadOpenApiDocument}.
|
|
206
208
|
*/
|
|
207
209
|
declare function createOpenApiProviderFactory(deps?: OpenApiProviderDeps): ConnectorProviderFactory;
|
|
208
210
|
|
package/dist/index.d.ts
CHANGED
|
@@ -201,8 +201,10 @@ interface OpenApiProviderDeps {
|
|
|
201
201
|
* generates for a hand-wired OpenAPI connector — one action per operation over a
|
|
202
202
|
* static-auth HTTP transport, with the resolved `auth` applied.
|
|
203
203
|
*
|
|
204
|
-
* Hard-fails on invalid config (missing
|
|
205
|
-
* misconfigured instance fails boot loudly.
|
|
204
|
+
* Hard-fails on invalid config (missing spec, a wrong spec URL, an unparseable
|
|
205
|
+
* document, a bad base URL), so a misconfigured instance fails boot loudly. A
|
|
206
|
+
* remote spec URL that is merely unreachable / transiently failing instead
|
|
207
|
+
* degrades the instance and retries (#3049) — see {@link loadOpenApiDocument}.
|
|
206
208
|
*/
|
|
207
209
|
declare function createOpenApiProviderFactory(deps?: OpenApiProviderDeps): ConnectorProviderFactory;
|
|
208
210
|
|
package/dist/index.js
CHANGED
|
@@ -242,7 +242,9 @@ function titleize(name) {
|
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
// src/openapi-provider.ts
|
|
245
|
+
var import_integration = require("@objectstack/spec/integration");
|
|
245
246
|
var OPENAPI_PROVIDER_KEY = "openapi";
|
|
247
|
+
var SPEC_FETCH_RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
|
|
246
248
|
async function loadOpenApiDocument(spec, fetchImpl, ctx) {
|
|
247
249
|
const connectorName = ctx.name;
|
|
248
250
|
if (spec && typeof spec === "object" && !Array.isArray(spec)) {
|
|
@@ -251,13 +253,36 @@ async function loadOpenApiDocument(spec, fetchImpl, ctx) {
|
|
|
251
253
|
if (typeof spec === "string" && spec.length > 0) {
|
|
252
254
|
if (/^https?:\/\//i.test(spec)) {
|
|
253
255
|
const doFetch = fetchImpl ?? fetch;
|
|
254
|
-
|
|
256
|
+
let res;
|
|
257
|
+
try {
|
|
258
|
+
res = await doFetch(spec);
|
|
259
|
+
} catch (err) {
|
|
260
|
+
throw new import_integration.ConnectorUpstreamUnavailableError(
|
|
261
|
+
`connector-openapi provider: connector '${connectorName}' could not reach spec URL '${spec}': ${err.message}`,
|
|
262
|
+
{ cause: err }
|
|
263
|
+
);
|
|
264
|
+
}
|
|
255
265
|
if (!res.ok) {
|
|
266
|
+
if (SPEC_FETCH_RETRYABLE_STATUS.has(res.status)) {
|
|
267
|
+
throw new import_integration.ConnectorUpstreamUnavailableError(
|
|
268
|
+
`connector-openapi provider: connector '${connectorName}' got a transient HTTP ${res.status} fetching spec '${spec}'.`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
256
271
|
throw new Error(
|
|
257
272
|
`connector-openapi provider: connector '${connectorName}' failed to fetch spec '${spec}' (HTTP ${res.status}).`
|
|
258
273
|
);
|
|
259
274
|
}
|
|
260
|
-
|
|
275
|
+
try {
|
|
276
|
+
const parsed = await res.json();
|
|
277
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
278
|
+
throw new Error("not a JSON object");
|
|
279
|
+
}
|
|
280
|
+
return parsed;
|
|
281
|
+
} catch (err) {
|
|
282
|
+
throw new Error(
|
|
283
|
+
`connector-openapi provider: connector '${connectorName}' fetched spec '${spec}' but it is not a parseable OpenAPI JSON document: ${err.message}`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
261
286
|
}
|
|
262
287
|
if (!ctx.loadPackageFile) {
|
|
263
288
|
throw new Error(
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/openapi-connector.ts","../src/openapi-provider.ts","../src/connector-openapi-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * @objectstack/connector-openapi\n *\n * Generate an ObjectStack {@link Connector} from a declarative OpenAPI 3.x\n * document (ADR-0023). One operation becomes one connector action; a single\n * generic handler drives a self-contained static-auth HTTP transport (mirroring\n * `@objectstack/connector-rest`). The generated connector is an ordinary\n * `type: 'api'` connector — registered via `engine.registerConnector` with no\n * new engine surface.\n *\n * Open-source scope: static auth only (`none` / `api-key` / `basic` / `bearer`),\n * credentials supplied by the caller. Managed OAuth2, credential vaulting, and\n * per-tenant lifecycle are the enterprise tier (ADR-0015 / 0022).\n */\n\nexport {\n createOpenApiConnector,\n type OpenApiConnectorBundle,\n type OpenApiConnectorConfig,\n type OpenApiDocument,\n type OpenApiPathItem,\n type OpenApiOperation,\n type OpenApiParameter,\n type OpenApiRequestBody,\n type OpenApiResponse,\n type OpenApiSecurityScheme,\n type OperationInfo,\n type RestAuth,\n type JsonSchema,\n} from './openapi-connector.js';\nexport {\n registerOpenApiConnector,\n ConnectorOpenApiPlugin,\n type ConnectorOpenApiPluginOptions,\n type ConnectorRegistrySurface,\n} from './connector-openapi-plugin.js';\nexport {\n createOpenApiProviderFactory,\n OPENAPI_PROVIDER_KEY,\n type OpenApiProviderDeps,\n} from './openapi-provider.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Connector } from '@objectstack/spec/integration';\n\n/**\n * OpenAPI connector generator — turns a declarative OpenAPI 3.x document into a\n * {@link Connector} definition + handler map (ADR-0023).\n *\n * Each OpenAPI operation maps to one connector action; a single generic handler\n * (closing over the operation's method + path template) drives one shared HTTP\n * request implementation. That transport mirrors `@objectstack/connector-rest`\n * (build URL from base+path+query, apply static auth, JSON-encode the body,\n * normalise the response to `{ status, ok, body }`) — kept inline so this package\n * stays self-contained, depending only on `@objectstack/core` + `@objectstack/spec`\n * like its sibling connectors. The output is an ordinary `type: 'api'` connector,\n * registered via `engine.registerConnector(def, handlers)` exactly like a\n * hand-written one — the registry, the `connector_action` node, the discovery\n * route, and the Studio palette never know it came from OpenAPI.\n *\n * Open-source scope: **static** auth only (`none` / `api-key` / `basic` /\n * `bearer`), with credentials supplied by the caller. Managed OAuth2, credential\n * vaulting, and per-tenant lifecycle are the enterprise tier (ADR-0015 / 0022).\n */\n\n/** Static auth understood by the generated connector (the open-source subset). */\nexport type RestAuth = Extract<Connector['authentication'], { type: 'none' | 'api-key' | 'basic' | 'bearer' }>;\n\n/** An action on a Connector definition (derived to avoid guessing export names). */\ntype ConnectorAction = NonNullable<Connector['actions']>[number];\n\n/** Handler signature accepted by the connector registry (ADR-0018 §Addendum). */\ntype ConnectorHandler = (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>;\n\n/** A connector definition paired with its action handlers, ready for registerConnector(). */\nexport interface OpenApiConnectorBundle {\n def: Connector;\n handlers: Record<string, ConnectorHandler>;\n}\n\n/** A free-form JSON Schema fragment (matches ConnectorAction input/outputSchema). */\nexport type JsonSchema = Record<string, unknown>;\n\n/** Minimal subset of an OpenAPI 3.x document consumed by the generator.\n * The caller is responsible for loading and de-referencing ($ref) the doc. */\nexport interface OpenApiDocument {\n openapi?: string;\n info?: { title?: string; description?: string; version?: string };\n servers?: { url: string }[];\n paths?: Record<string, OpenApiPathItem>;\n components?: { securitySchemes?: Record<string, OpenApiSecurityScheme> };\n}\n\nexport interface OpenApiPathItem {\n [method: string]: OpenApiOperation | unknown;\n}\n\nexport interface OpenApiOperation {\n operationId?: string;\n summary?: string;\n description?: string;\n tags?: string[];\n parameters?: OpenApiParameter[];\n requestBody?: OpenApiRequestBody;\n responses?: Record<string, OpenApiResponse>;\n}\n\nexport interface OpenApiParameter {\n name: string;\n in: 'path' | 'query' | 'header' | 'cookie';\n required?: boolean;\n description?: string;\n schema?: JsonSchema;\n}\n\nexport interface OpenApiRequestBody {\n required?: boolean;\n description?: string;\n content?: Record<string, { schema?: JsonSchema }>;\n}\n\nexport interface OpenApiResponse {\n description?: string;\n content?: Record<string, { schema?: JsonSchema }>;\n}\n\nexport interface OpenApiSecurityScheme {\n type: 'apiKey' | 'http' | 'oauth2' | 'openIdConnect';\n name?: string;\n in?: 'header' | 'query' | 'cookie';\n scheme?: string;\n}\n\n/** Flattened view of a single operation, passed to the `include` predicate. */\nexport interface OperationInfo {\n operationId?: string;\n method: string;\n path: string;\n tags?: string[];\n summary?: string;\n description?: string;\n}\n\n/** Configuration for {@link createOpenApiConnector}. */\nexport interface OpenApiConnectorConfig {\n /** Connector machine name (snake_case). Defaults to a slug of info.title. */\n name?: string;\n /** Human-friendly label. Defaults to info.title (then name). */\n label?: string;\n /** Description. Defaults to info.description. */\n description?: string;\n /** Icon identifier for the Studio palette. Defaults to `globe`. */\n icon?: string;\n /** The parsed OpenAPI 3.x document (caller loads/derefs it). */\n document: OpenApiDocument;\n /** Override the base URL (else servers[0].url). */\n baseUrl?: string;\n /** Static auth with credentials. Defaults to `{ type: 'none' }`. */\n auth?: RestAuth;\n /** Headers merged into every request (request-level headers win). */\n defaultHeaders?: Record<string, string>;\n /** Only include operations for which this predicate returns true (allowlist). */\n include?: (op: OperationInfo) => boolean;\n /** Injected fetch implementation (defaults to global `fetch`). */\n fetchImpl?: typeof fetch;\n}\n\n/** OpenAPI HTTP method keys, in a deterministic order. */\nconst HTTP_METHODS = ['get', 'put', 'post', 'delete', 'patch', 'options', 'head', 'trace'] as const;\n\n/** Input passed to the shared request transport. */\ninterface RequestInput {\n method: string;\n path: string;\n headers?: Record<string, string>;\n query?: Record<string, string>;\n body?: unknown;\n}\n\n/**\n * Build an OpenAPI connector definition and its handler map.\n *\n * @returns the `Connector` definition (`def`) and a `handlers` record keyed by\n * action key, suitable for `engine.registerConnector(def, handlers)`.\n */\nexport function createOpenApiConnector(config: OpenApiConnectorConfig): OpenApiConnectorBundle {\n const { document, include } = config;\n const auth: RestAuth = config.auth ?? { type: 'none' };\n const doFetch = config.fetchImpl ?? fetch;\n const name = config.name ?? slug(document.info?.title ?? 'openapi_connector');\n const label = config.label ?? document.info?.title ?? titleize(name);\n const description = config.description ?? document.info?.description;\n const baseUrl = config.baseUrl ?? document.servers?.[0]?.url;\n if (!baseUrl) {\n throw new Error('createOpenApiConnector: no base URL — provide config.baseUrl or document.servers[0].url');\n }\n\n // One shared transport (mirrors connector-rest) reused by every action handler.\n async function request(input: RequestInput): Promise<Record<string, unknown>> {\n const method = input.method.toUpperCase();\n const headers: Record<string, string> = { ...config.defaultHeaders, ...input.headers };\n const query: Record<string, string> = { ...input.query };\n applyAuth(auth, headers, query);\n\n const url = buildUrl(baseUrl as string, input.path, query);\n const hasBody = input.body !== undefined && method !== 'GET' && method !== 'HEAD';\n if (hasBody && headers['Content-Type'] === undefined && headers['content-type'] === undefined) {\n headers['Content-Type'] = 'application/json';\n }\n\n const response = await doFetch(url, {\n method,\n headers,\n body: hasBody ? JSON.stringify(input.body) : undefined,\n });\n\n const contentType = response.headers.get('content-type') ?? '';\n const parsed = contentType.includes('application/json') ? await response.json() : await response.text();\n return { status: response.status, ok: response.ok, body: parsed };\n }\n\n const actions: ConnectorAction[] = [];\n const handlers: Record<string, ConnectorHandler> = {};\n const seenKeys = new Set<string>();\n\n for (const op of collectOperations(document)) {\n if (include && !include(toInfo(op))) continue;\n const key = uniqueKey(op.operationId ?? slug(`${op.method}_${op.path}`), seenKeys);\n\n actions.push({\n key,\n label: op.summary ?? titleize(key),\n description: op.description,\n inputSchema: buildInputSchema(op),\n outputSchema: buildOutputSchema(op),\n });\n\n handlers[key] = async (input: Record<string, unknown>) => {\n const req = input as { path?: unknown; query?: unknown; header?: unknown; body?: unknown };\n return request({\n method: op.method,\n path: interpolatePath(op.path, asRecord(req.path)),\n query: stringifyValues(asRecord(req.query)),\n headers: stringifyValues(asRecord(req.header)),\n body: req.body,\n });\n };\n }\n\n const def: Connector = {\n name,\n label,\n type: 'api',\n description,\n icon: config.icon ?? 'globe',\n authentication: auth,\n // Defaulted by ConnectorSchema; set explicitly so the literal satisfies\n // the (post-parse) Connector output type (mirrors connector-rest/mcp).\n status: 'active',\n enabled: true,\n connectionTimeoutMs: 30000,\n requestTimeoutMs: 30000,\n actions,\n };\n\n return { def, handlers };\n}\n\ninterface Op extends OpenApiOperation {\n method: string;\n path: string;\n}\n\n/** Flatten paths × methods into a deterministic list of operations. */\nfunction collectOperations(doc: OpenApiDocument): Op[] {\n const ops: Op[] = [];\n for (const [path, item] of Object.entries(doc.paths ?? {})) {\n if (!item || typeof item !== 'object') continue;\n const record = item as Record<string, unknown>;\n for (const method of HTTP_METHODS) {\n const operation = record[method] as OpenApiOperation | undefined;\n if (!operation || typeof operation !== 'object') continue;\n ops.push({ ...operation, method, path });\n }\n }\n return ops;\n}\n\nfunction toInfo(op: Op): OperationInfo {\n return {\n operationId: op.operationId,\n method: op.method,\n path: op.path,\n tags: op.tags,\n summary: op.summary,\n description: op.description,\n };\n}\n\n/**\n * Assemble the action inputSchema from an operation's parameters + requestBody.\n * Produces { type: 'object', properties: { path, query, header, body }, required }\n * where only non-empty sections are emitted.\n */\nfunction buildInputSchema(op: OpenApiOperation): JsonSchema | undefined {\n const sections: Record<'path' | 'query' | 'header', { props: Record<string, JsonSchema>; required: string[] }> = {\n path: { props: {}, required: [] },\n query: { props: {}, required: [] },\n header: { props: {}, required: [] },\n };\n\n for (const p of op.parameters ?? []) {\n if (!p || typeof p !== 'object' || '$ref' in p) continue;\n if (p.in !== 'path' && p.in !== 'query' && p.in !== 'header') continue;\n const sec = sections[p.in];\n sec.props[p.name] = p.schema ?? (p.description ? { type: 'string', description: p.description } : { type: 'string' });\n if (p.required) sec.required.push(p.name);\n }\n\n const properties: Record<string, JsonSchema> = {};\n const required: string[] = [];\n for (const where of ['path', 'query', 'header'] as const) {\n const sec = sections[where];\n if (Object.keys(sec.props).length === 0) continue;\n const schema: JsonSchema = { type: 'object', properties: sec.props };\n if (sec.required.length) schema.required = sec.required;\n properties[where] = schema;\n // Path params are always required when present; others only if any are.\n if (where === 'path' || sec.required.length) required.push(where);\n }\n\n const bodySchema = extractRequestBodySchema(op.requestBody);\n if (bodySchema) {\n properties.body = bodySchema;\n if (op.requestBody && !('$ref' in op.requestBody) && op.requestBody.required) required.push('body');\n }\n\n if (Object.keys(properties).length === 0) return undefined;\n const schema: JsonSchema = { type: 'object', properties };\n if (required.length) schema.required = required;\n return schema;\n}\n\n/** Pick the success response's JSON schema (200 → first 2xx → default). */\nfunction buildOutputSchema(op: OpenApiOperation): JsonSchema | undefined {\n const responses = op.responses;\n if (!responses) return undefined;\n let code: string | undefined;\n if (responses['200']) code = '200';\n else code = Object.keys(responses).find((c) => /^2\\d\\d$/.test(c));\n if (!code && responses['default']) code = 'default';\n if (!code) return undefined;\n const resp = responses[code];\n if (!resp || typeof resp !== 'object' || '$ref' in resp) return undefined;\n return pickJsonSchema(resp.content);\n}\n\n/** Extract the requestBody JSON schema (prefers application/json). */\nfunction extractRequestBodySchema(rb: OpenApiRequestBody | undefined): JsonSchema | undefined {\n if (!rb || typeof rb !== 'object' || '$ref' in rb) return undefined;\n return pickJsonSchema(rb.content);\n}\n\n/** Choose the application/json schema, falling back to the first content type. */\nfunction pickJsonSchema(content: Record<string, { schema?: JsonSchema }> | undefined): JsonSchema | undefined {\n if (!content) return undefined;\n const chosen = content['application/json'] ?? Object.values(content)[0];\n return chosen?.schema;\n}\n\n/** Build the request URL from base + path + query, encoding query params. */\nfunction buildUrl(baseUrl: string, path: string, query: Record<string, string>): string {\n const base = baseUrl.replace(/\\/+$/, '');\n const suffix = path ? (path.startsWith('/') ? path : `/${path}`) : '';\n const url = new URL(base + suffix);\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null) url.searchParams.set(key, String(value));\n }\n return url.toString();\n}\n\n/** Apply static auth to the outgoing headers / query (mirrors connector-rest). */\nfunction applyAuth(auth: RestAuth, headers: Record<string, string>, query: Record<string, string>): void {\n switch (auth.type) {\n case 'none':\n return;\n case 'bearer':\n headers['Authorization'] = `Bearer ${auth.token}`;\n return;\n case 'basic': {\n const encoded = Buffer.from(`${auth.username}:${auth.password}`).toString('base64');\n headers['Authorization'] = `Basic ${encoded}`;\n return;\n }\n case 'api-key':\n if (auth.paramName) query[auth.paramName] = auth.key;\n else headers[auth.headerName ?? 'X-API-Key'] = auth.key;\n return;\n }\n}\n\n/** Interpolate {name} path templates with encoded values from the input. */\nfunction interpolatePath(template: string, pathParams: Record<string, unknown>): string {\n return template.replace(/\\{([^}]+)\\}/g, (_match, key: string) => {\n const value = pathParams[key];\n return value === undefined || value === null ? `{${key}}` : encodeURIComponent(String(value));\n });\n}\n\n/** Coerce a record of mixed values into string values, dropping null/undefined. */\nfunction stringifyValues(rec: Record<string, unknown>): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(rec)) {\n if (v === undefined || v === null) continue;\n out[k] = String(v);\n }\n return out;\n}\n\n/** Return v if it is a plain object, else an empty record. */\nfunction asRecord(v: unknown): Record<string, unknown> {\n return v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : {};\n}\n\n/** Ensure a deterministically unique action key within the connector. */\nfunction uniqueKey(base: string, seen: Set<string>): string {\n let candidate = base;\n if (seen.has(candidate)) {\n let i = 2;\n while (seen.has(`${base}_${i}`)) i++;\n candidate = `${base}_${i}`;\n }\n seen.add(candidate);\n return candidate;\n}\n\n/** Slugify a string into a snake_case machine name (`/^[a-z_][a-z0-9_]*$/`). */\nfunction slug(s: string): string {\n const out = s\n .normalize('NFKD')\n .replace(/[^a-zA-Z0-9]+/g, '_')\n .replace(/^_+|_+$/g, '')\n .toLowerCase();\n if (!out) return 'connector';\n return /^[a-z_]/.test(out) ? out : `op_${out}`;\n}\n\n/** Title-case a snake_case key for a default label (`get_pets` → `Get Pets`). */\nfunction titleize(name: string): string {\n return name\n .split('_')\n .filter(Boolean)\n .map((w) => w.charAt(0).toUpperCase() + w.slice(1))\n .join(' ');\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type {\n ConnectorProviderContext,\n ConnectorProviderFactory,\n ResolvedConnectorAuth,\n} from '@objectstack/spec/integration';\nimport {\n createOpenApiConnector,\n type OpenApiDocument,\n type RestAuth,\n} from './openapi-connector.js';\n\n/**\n * The provider key this package contributes (ADR-0097). A declarative\n * `connectors:` entry with `provider: 'openapi'` is materialized by this factory.\n */\nexport const OPENAPI_PROVIDER_KEY = 'openapi';\n\n/** Injectable dependencies for {@link createOpenApiProviderFactory} (tests). */\nexport interface OpenApiProviderDeps {\n /** Injected fetch implementation (spec fetch + request transport); defaults to global `fetch`. */\n fetchImpl?: typeof fetch;\n}\n\n/** Shape of `providerConfig` for a `provider: 'openapi'` declarative instance. */\ninterface OpenApiProviderConfig {\n /**\n * The OpenAPI 3.x document: an inline object, an http(s) URL to fetch at\n * boot, or a file path resolved relative to the declaring stack/package root\n * (`'./billing-openapi.json'`, #3016).\n */\n spec?: unknown;\n /** Override the base URL (else the document's `servers[0].url`). */\n baseUrl?: unknown;\n}\n\n/**\n * Resolve `providerConfig.spec` into a parsed OpenAPI document (ADR-0097;\n * union per #3016): an inline document object (the reliable, no-I/O-at-boot\n * form used by the showcase), an http(s) URL fetched at materialization, or a\n * **file path** read through the host's `ctx.loadPackageFile` — which resolves\n * it relative to the declaring stack/package root and confines the read to\n * that root (absolute / `..`-escaping paths are rejected there). Every failure\n * throws, so the materializer's reconcile policy applies: fatal at boot, the\n * entry is skipped on reload.\n */\nasync function loadOpenApiDocument(\n spec: unknown,\n fetchImpl: typeof fetch | undefined,\n ctx: ConnectorProviderContext,\n): Promise<OpenApiDocument> {\n const connectorName = ctx.name;\n if (spec && typeof spec === 'object' && !Array.isArray(spec)) {\n return spec as OpenApiDocument;\n }\n if (typeof spec === 'string' && spec.length > 0) {\n if (/^https?:\\/\\//i.test(spec)) {\n const doFetch = fetchImpl ?? fetch;\n const res = await doFetch(spec);\n if (!res.ok) {\n throw new Error(\n `connector-openapi provider: connector '${connectorName}' failed to fetch spec '${spec}' (HTTP ${res.status}).`,\n );\n }\n return (await res.json()) as OpenApiDocument;\n }\n // File path — dereferenced through the host capability so resolution stays\n // anchored to (and confined within) the declaring stack/package root.\n if (!ctx.loadPackageFile) {\n throw new Error(\n `connector-openapi provider: connector '${connectorName}' providerConfig.spec '${spec}' is a file path, ` +\n `but this host provides no package file access — inline the OpenAPI document or use an http(s) URL.`,\n );\n }\n let text: string;\n try {\n text = await ctx.loadPackageFile(spec);\n } catch (err) {\n throw new Error(\n `connector-openapi provider: connector '${connectorName}' failed to read providerConfig.spec '${spec}': ` +\n `${(err as Error).message}`,\n );\n }\n try {\n const parsed: unknown = JSON.parse(text);\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error('not a JSON object');\n }\n return parsed as OpenApiDocument;\n } catch (err) {\n throw new Error(\n `connector-openapi provider: connector '${connectorName}' providerConfig.spec '${spec}' is not a parseable ` +\n `OpenAPI JSON document: ${(err as Error).message}`,\n );\n }\n }\n throw new Error(\n `connector-openapi provider: connector '${connectorName}' requires providerConfig.spec — an inline OpenAPI 3.x ` +\n `document object, an http(s) URL, or a package-relative file path.`,\n );\n}\n\n/**\n * Build the `openapi` {@link ConnectorProviderFactory} (ADR-0097 / ADR-0023). At\n * boot the automation service invokes it for each `provider: 'openapi'`\n * declarative instance: it loads the OpenAPI document from `providerConfig.spec`,\n * then produces the same `{ def, handlers }` bundle {@link createOpenApiConnector}\n * generates for a hand-wired OpenAPI connector — one action per operation over a\n * static-auth HTTP transport, with the resolved `auth` applied.\n *\n * Hard-fails on invalid config (missing/unfetchable spec, no base URL), so a\n * misconfigured instance fails boot loudly.\n */\nexport function createOpenApiProviderFactory(deps: OpenApiProviderDeps = {}): ConnectorProviderFactory {\n return async (ctx) => {\n const cfg = (ctx.providerConfig ?? {}) as OpenApiProviderConfig;\n if (cfg.baseUrl !== undefined && typeof cfg.baseUrl !== 'string') {\n throw new Error(\n `connector-openapi provider: connector '${ctx.name}' providerConfig.baseUrl must be a string when set.`,\n );\n }\n const document = await loadOpenApiDocument(cfg.spec, deps.fetchImpl, ctx);\n const auth = ctx.auth as ResolvedConnectorAuth | undefined as RestAuth | undefined;\n return createOpenApiConnector({\n name: ctx.name,\n label: ctx.label,\n description: ctx.description,\n document,\n baseUrl: typeof cfg.baseUrl === 'string' ? cfg.baseUrl : undefined,\n auth,\n fetchImpl: deps.fetchImpl,\n });\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { Connector, ConnectorProviderFactory } from '@objectstack/spec/integration';\nimport { createOpenApiConnector, type OpenApiConnectorConfig } from './openapi-connector.js';\nimport { createOpenApiProviderFactory, OPENAPI_PROVIDER_KEY } from './openapi-provider.js';\n\n/**\n * Minimal surface of the automation engine this package depends on — the\n * connector registry (ADR-0018 §Addendum) plus the provider registry (ADR-0097).\n * Kept structural so callers need no runtime dependency on\n * `@objectstack/service-automation` (mirrors connector-rest / connector-mcp).\n */\nexport interface ConnectorRegistrySurface {\n registerConnector(\n def: Connector,\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >,\n ): void;\n unregisterConnector(name: string): void;\n registerConnectorProvider(providerKey: string, factory: ConnectorProviderFactory): void;\n}\n\n/**\n * Generate an OpenAPI-backed connector and register it on the engine's connector\n * registry so the baseline `connector_action` node can dispatch to the generated\n * actions (ADR-0023). Returns the registered connector name.\n */\nexport function registerOpenApiConnector(registry: ConnectorRegistrySurface, config: OpenApiConnectorConfig): string {\n const { def, handlers } = createOpenApiConnector(config);\n registry.registerConnector(def, handlers);\n return def.name;\n}\n\n/**\n * Options for {@link ConnectorOpenApiPlugin}. All optional (ADR-0097): with no\n * `document` the plugin contributes only the `openapi` provider factory — so a\n * stack can declare `provider: 'openapi'` instances as pure metadata. Supply a\n * `document` (+ config) to ALSO register one hand-wired OpenAPI connector.\n */\nexport interface ConnectorOpenApiPluginOptions extends Partial<OpenApiConnectorConfig> {\n /** Injected fetch implementation forwarded to the `openapi` provider factory (tests). */\n fetchImpl?: typeof fetch;\n}\n\n/**\n * ConnectorOpenApiPlugin — contributes the OpenAPI generic executor (ADR-0023) in\n * two forms:\n *\n * 1. **Provider factory** (`openapi`, ADR-0097): registered at `init()` so the\n * automation service can materialize declarative `provider: 'openapi'`\n * `connectors:` entries — loading the OpenAPI document and mapping each\n * operation to a connector action — at boot.\n * 2. **Hand-wired instance** (optional, back-compat): when constructed with a\n * `document`, it also registers one concrete OpenAPI connector at `start()`.\n *\n * If no automation engine is present the plugin logs and skips.\n */\nexport class ConnectorOpenApiPlugin implements Plugin {\n name = 'com.objectstack.connector.openapi';\n version = '1.0.0';\n type = 'standard' as const;\n // Ensure the automation engine (and its connector/provider registries) exist first.\n dependencies = ['com.objectstack.service-automation'];\n\n private readonly options: ConnectorOpenApiPluginOptions;\n private connectorName?: string;\n private automation?: ConnectorRegistrySurface;\n\n constructor(options: ConnectorOpenApiPluginOptions = {}) {\n this.options = options;\n }\n\n async init(ctx: PluginContext): Promise<void> {\n // Contribute the `openapi` provider factory (ADR-0097) before the\n // automation service materializes declarative instances during its start().\n const automation = this.tryGetAutomation(ctx);\n if (automation && typeof automation.registerConnectorProvider === 'function') {\n automation.registerConnectorProvider(\n OPENAPI_PROVIDER_KEY,\n createOpenApiProviderFactory({ fetchImpl: this.options.fetchImpl }),\n );\n ctx.logger.info(\"ConnectorOpenApiPlugin: registered 'openapi' connector provider\");\n }\n }\n\n async start(ctx: PluginContext): Promise<void> {\n // Provider-only usage (no document) contributes just the factory in init().\n if (!this.options.document) return;\n\n const automation = this.tryGetAutomation(ctx);\n if (!automation || typeof automation.registerConnector !== 'function') {\n ctx.logger.info('ConnectorOpenApiPlugin: no automation engine — OpenAPI connector not registered');\n return;\n }\n\n this.connectorName = registerOpenApiConnector(automation, this.options as OpenApiConnectorConfig);\n this.automation = automation;\n ctx.logger.info(`ConnectorOpenApiPlugin: OpenAPI connector '${this.connectorName}' registered`);\n }\n\n async stop(_ctx: PluginContext): Promise<void> {\n if (this.automation && this.connectorName) {\n try { this.automation.unregisterConnector(this.connectorName); } catch { /* ignore */ }\n }\n }\n\n private tryGetAutomation(ctx: PluginContext): ConnectorRegistrySurface | undefined {\n try {\n return ctx.getService<ConnectorRegistrySurface>('automation');\n } catch {\n return undefined;\n }\n }\n}\n"],"mappings":"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","names":["schema"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/openapi-connector.ts","../src/openapi-provider.ts","../src/connector-openapi-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * @objectstack/connector-openapi\n *\n * Generate an ObjectStack {@link Connector} from a declarative OpenAPI 3.x\n * document (ADR-0023). One operation becomes one connector action; a single\n * generic handler drives a self-contained static-auth HTTP transport (mirroring\n * `@objectstack/connector-rest`). The generated connector is an ordinary\n * `type: 'api'` connector — registered via `engine.registerConnector` with no\n * new engine surface.\n *\n * Open-source scope: static auth only (`none` / `api-key` / `basic` / `bearer`),\n * credentials supplied by the caller. Managed OAuth2, credential vaulting, and\n * per-tenant lifecycle are the enterprise tier (ADR-0015 / 0022).\n */\n\nexport {\n createOpenApiConnector,\n type OpenApiConnectorBundle,\n type OpenApiConnectorConfig,\n type OpenApiDocument,\n type OpenApiPathItem,\n type OpenApiOperation,\n type OpenApiParameter,\n type OpenApiRequestBody,\n type OpenApiResponse,\n type OpenApiSecurityScheme,\n type OperationInfo,\n type RestAuth,\n type JsonSchema,\n} from './openapi-connector.js';\nexport {\n registerOpenApiConnector,\n ConnectorOpenApiPlugin,\n type ConnectorOpenApiPluginOptions,\n type ConnectorRegistrySurface,\n} from './connector-openapi-plugin.js';\nexport {\n createOpenApiProviderFactory,\n OPENAPI_PROVIDER_KEY,\n type OpenApiProviderDeps,\n} from './openapi-provider.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Connector } from '@objectstack/spec/integration';\n\n/**\n * OpenAPI connector generator — turns a declarative OpenAPI 3.x document into a\n * {@link Connector} definition + handler map (ADR-0023).\n *\n * Each OpenAPI operation maps to one connector action; a single generic handler\n * (closing over the operation's method + path template) drives one shared HTTP\n * request implementation. That transport mirrors `@objectstack/connector-rest`\n * (build URL from base+path+query, apply static auth, JSON-encode the body,\n * normalise the response to `{ status, ok, body }`) — kept inline so this package\n * stays self-contained, depending only on `@objectstack/core` + `@objectstack/spec`\n * like its sibling connectors. The output is an ordinary `type: 'api'` connector,\n * registered via `engine.registerConnector(def, handlers)` exactly like a\n * hand-written one — the registry, the `connector_action` node, the discovery\n * route, and the Studio palette never know it came from OpenAPI.\n *\n * Open-source scope: **static** auth only (`none` / `api-key` / `basic` /\n * `bearer`), with credentials supplied by the caller. Managed OAuth2, credential\n * vaulting, and per-tenant lifecycle are the enterprise tier (ADR-0015 / 0022).\n */\n\n/** Static auth understood by the generated connector (the open-source subset). */\nexport type RestAuth = Extract<Connector['authentication'], { type: 'none' | 'api-key' | 'basic' | 'bearer' }>;\n\n/** An action on a Connector definition (derived to avoid guessing export names). */\ntype ConnectorAction = NonNullable<Connector['actions']>[number];\n\n/** Handler signature accepted by the connector registry (ADR-0018 §Addendum). */\ntype ConnectorHandler = (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>;\n\n/** A connector definition paired with its action handlers, ready for registerConnector(). */\nexport interface OpenApiConnectorBundle {\n def: Connector;\n handlers: Record<string, ConnectorHandler>;\n}\n\n/** A free-form JSON Schema fragment (matches ConnectorAction input/outputSchema). */\nexport type JsonSchema = Record<string, unknown>;\n\n/** Minimal subset of an OpenAPI 3.x document consumed by the generator.\n * The caller is responsible for loading and de-referencing ($ref) the doc. */\nexport interface OpenApiDocument {\n openapi?: string;\n info?: { title?: string; description?: string; version?: string };\n servers?: { url: string }[];\n paths?: Record<string, OpenApiPathItem>;\n components?: { securitySchemes?: Record<string, OpenApiSecurityScheme> };\n}\n\nexport interface OpenApiPathItem {\n [method: string]: OpenApiOperation | unknown;\n}\n\nexport interface OpenApiOperation {\n operationId?: string;\n summary?: string;\n description?: string;\n tags?: string[];\n parameters?: OpenApiParameter[];\n requestBody?: OpenApiRequestBody;\n responses?: Record<string, OpenApiResponse>;\n}\n\nexport interface OpenApiParameter {\n name: string;\n in: 'path' | 'query' | 'header' | 'cookie';\n required?: boolean;\n description?: string;\n schema?: JsonSchema;\n}\n\nexport interface OpenApiRequestBody {\n required?: boolean;\n description?: string;\n content?: Record<string, { schema?: JsonSchema }>;\n}\n\nexport interface OpenApiResponse {\n description?: string;\n content?: Record<string, { schema?: JsonSchema }>;\n}\n\nexport interface OpenApiSecurityScheme {\n type: 'apiKey' | 'http' | 'oauth2' | 'openIdConnect';\n name?: string;\n in?: 'header' | 'query' | 'cookie';\n scheme?: string;\n}\n\n/** Flattened view of a single operation, passed to the `include` predicate. */\nexport interface OperationInfo {\n operationId?: string;\n method: string;\n path: string;\n tags?: string[];\n summary?: string;\n description?: string;\n}\n\n/** Configuration for {@link createOpenApiConnector}. */\nexport interface OpenApiConnectorConfig {\n /** Connector machine name (snake_case). Defaults to a slug of info.title. */\n name?: string;\n /** Human-friendly label. Defaults to info.title (then name). */\n label?: string;\n /** Description. Defaults to info.description. */\n description?: string;\n /** Icon identifier for the Studio palette. Defaults to `globe`. */\n icon?: string;\n /** The parsed OpenAPI 3.x document (caller loads/derefs it). */\n document: OpenApiDocument;\n /** Override the base URL (else servers[0].url). */\n baseUrl?: string;\n /** Static auth with credentials. Defaults to `{ type: 'none' }`. */\n auth?: RestAuth;\n /** Headers merged into every request (request-level headers win). */\n defaultHeaders?: Record<string, string>;\n /** Only include operations for which this predicate returns true (allowlist). */\n include?: (op: OperationInfo) => boolean;\n /** Injected fetch implementation (defaults to global `fetch`). */\n fetchImpl?: typeof fetch;\n}\n\n/** OpenAPI HTTP method keys, in a deterministic order. */\nconst HTTP_METHODS = ['get', 'put', 'post', 'delete', 'patch', 'options', 'head', 'trace'] as const;\n\n/** Input passed to the shared request transport. */\ninterface RequestInput {\n method: string;\n path: string;\n headers?: Record<string, string>;\n query?: Record<string, string>;\n body?: unknown;\n}\n\n/**\n * Build an OpenAPI connector definition and its handler map.\n *\n * @returns the `Connector` definition (`def`) and a `handlers` record keyed by\n * action key, suitable for `engine.registerConnector(def, handlers)`.\n */\nexport function createOpenApiConnector(config: OpenApiConnectorConfig): OpenApiConnectorBundle {\n const { document, include } = config;\n const auth: RestAuth = config.auth ?? { type: 'none' };\n const doFetch = config.fetchImpl ?? fetch;\n const name = config.name ?? slug(document.info?.title ?? 'openapi_connector');\n const label = config.label ?? document.info?.title ?? titleize(name);\n const description = config.description ?? document.info?.description;\n const baseUrl = config.baseUrl ?? document.servers?.[0]?.url;\n if (!baseUrl) {\n throw new Error('createOpenApiConnector: no base URL — provide config.baseUrl or document.servers[0].url');\n }\n\n // One shared transport (mirrors connector-rest) reused by every action handler.\n async function request(input: RequestInput): Promise<Record<string, unknown>> {\n const method = input.method.toUpperCase();\n const headers: Record<string, string> = { ...config.defaultHeaders, ...input.headers };\n const query: Record<string, string> = { ...input.query };\n applyAuth(auth, headers, query);\n\n const url = buildUrl(baseUrl as string, input.path, query);\n const hasBody = input.body !== undefined && method !== 'GET' && method !== 'HEAD';\n if (hasBody && headers['Content-Type'] === undefined && headers['content-type'] === undefined) {\n headers['Content-Type'] = 'application/json';\n }\n\n const response = await doFetch(url, {\n method,\n headers,\n body: hasBody ? JSON.stringify(input.body) : undefined,\n });\n\n const contentType = response.headers.get('content-type') ?? '';\n const parsed = contentType.includes('application/json') ? await response.json() : await response.text();\n return { status: response.status, ok: response.ok, body: parsed };\n }\n\n const actions: ConnectorAction[] = [];\n const handlers: Record<string, ConnectorHandler> = {};\n const seenKeys = new Set<string>();\n\n for (const op of collectOperations(document)) {\n if (include && !include(toInfo(op))) continue;\n const key = uniqueKey(op.operationId ?? slug(`${op.method}_${op.path}`), seenKeys);\n\n actions.push({\n key,\n label: op.summary ?? titleize(key),\n description: op.description,\n inputSchema: buildInputSchema(op),\n outputSchema: buildOutputSchema(op),\n });\n\n handlers[key] = async (input: Record<string, unknown>) => {\n const req = input as { path?: unknown; query?: unknown; header?: unknown; body?: unknown };\n return request({\n method: op.method,\n path: interpolatePath(op.path, asRecord(req.path)),\n query: stringifyValues(asRecord(req.query)),\n headers: stringifyValues(asRecord(req.header)),\n body: req.body,\n });\n };\n }\n\n const def: Connector = {\n name,\n label,\n type: 'api',\n description,\n icon: config.icon ?? 'globe',\n authentication: auth,\n // Defaulted by ConnectorSchema; set explicitly so the literal satisfies\n // the (post-parse) Connector output type (mirrors connector-rest/mcp).\n status: 'active',\n enabled: true,\n connectionTimeoutMs: 30000,\n requestTimeoutMs: 30000,\n actions,\n };\n\n return { def, handlers };\n}\n\ninterface Op extends OpenApiOperation {\n method: string;\n path: string;\n}\n\n/** Flatten paths × methods into a deterministic list of operations. */\nfunction collectOperations(doc: OpenApiDocument): Op[] {\n const ops: Op[] = [];\n for (const [path, item] of Object.entries(doc.paths ?? {})) {\n if (!item || typeof item !== 'object') continue;\n const record = item as Record<string, unknown>;\n for (const method of HTTP_METHODS) {\n const operation = record[method] as OpenApiOperation | undefined;\n if (!operation || typeof operation !== 'object') continue;\n ops.push({ ...operation, method, path });\n }\n }\n return ops;\n}\n\nfunction toInfo(op: Op): OperationInfo {\n return {\n operationId: op.operationId,\n method: op.method,\n path: op.path,\n tags: op.tags,\n summary: op.summary,\n description: op.description,\n };\n}\n\n/**\n * Assemble the action inputSchema from an operation's parameters + requestBody.\n * Produces { type: 'object', properties: { path, query, header, body }, required }\n * where only non-empty sections are emitted.\n */\nfunction buildInputSchema(op: OpenApiOperation): JsonSchema | undefined {\n const sections: Record<'path' | 'query' | 'header', { props: Record<string, JsonSchema>; required: string[] }> = {\n path: { props: {}, required: [] },\n query: { props: {}, required: [] },\n header: { props: {}, required: [] },\n };\n\n for (const p of op.parameters ?? []) {\n if (!p || typeof p !== 'object' || '$ref' in p) continue;\n if (p.in !== 'path' && p.in !== 'query' && p.in !== 'header') continue;\n const sec = sections[p.in];\n sec.props[p.name] = p.schema ?? (p.description ? { type: 'string', description: p.description } : { type: 'string' });\n if (p.required) sec.required.push(p.name);\n }\n\n const properties: Record<string, JsonSchema> = {};\n const required: string[] = [];\n for (const where of ['path', 'query', 'header'] as const) {\n const sec = sections[where];\n if (Object.keys(sec.props).length === 0) continue;\n const schema: JsonSchema = { type: 'object', properties: sec.props };\n if (sec.required.length) schema.required = sec.required;\n properties[where] = schema;\n // Path params are always required when present; others only if any are.\n if (where === 'path' || sec.required.length) required.push(where);\n }\n\n const bodySchema = extractRequestBodySchema(op.requestBody);\n if (bodySchema) {\n properties.body = bodySchema;\n if (op.requestBody && !('$ref' in op.requestBody) && op.requestBody.required) required.push('body');\n }\n\n if (Object.keys(properties).length === 0) return undefined;\n const schema: JsonSchema = { type: 'object', properties };\n if (required.length) schema.required = required;\n return schema;\n}\n\n/** Pick the success response's JSON schema (200 → first 2xx → default). */\nfunction buildOutputSchema(op: OpenApiOperation): JsonSchema | undefined {\n const responses = op.responses;\n if (!responses) return undefined;\n let code: string | undefined;\n if (responses['200']) code = '200';\n else code = Object.keys(responses).find((c) => /^2\\d\\d$/.test(c));\n if (!code && responses['default']) code = 'default';\n if (!code) return undefined;\n const resp = responses[code];\n if (!resp || typeof resp !== 'object' || '$ref' in resp) return undefined;\n return pickJsonSchema(resp.content);\n}\n\n/** Extract the requestBody JSON schema (prefers application/json). */\nfunction extractRequestBodySchema(rb: OpenApiRequestBody | undefined): JsonSchema | undefined {\n if (!rb || typeof rb !== 'object' || '$ref' in rb) return undefined;\n return pickJsonSchema(rb.content);\n}\n\n/** Choose the application/json schema, falling back to the first content type. */\nfunction pickJsonSchema(content: Record<string, { schema?: JsonSchema }> | undefined): JsonSchema | undefined {\n if (!content) return undefined;\n const chosen = content['application/json'] ?? Object.values(content)[0];\n return chosen?.schema;\n}\n\n/** Build the request URL from base + path + query, encoding query params. */\nfunction buildUrl(baseUrl: string, path: string, query: Record<string, string>): string {\n const base = baseUrl.replace(/\\/+$/, '');\n const suffix = path ? (path.startsWith('/') ? path : `/${path}`) : '';\n const url = new URL(base + suffix);\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null) url.searchParams.set(key, String(value));\n }\n return url.toString();\n}\n\n/** Apply static auth to the outgoing headers / query (mirrors connector-rest). */\nfunction applyAuth(auth: RestAuth, headers: Record<string, string>, query: Record<string, string>): void {\n switch (auth.type) {\n case 'none':\n return;\n case 'bearer':\n headers['Authorization'] = `Bearer ${auth.token}`;\n return;\n case 'basic': {\n const encoded = Buffer.from(`${auth.username}:${auth.password}`).toString('base64');\n headers['Authorization'] = `Basic ${encoded}`;\n return;\n }\n case 'api-key':\n if (auth.paramName) query[auth.paramName] = auth.key;\n else headers[auth.headerName ?? 'X-API-Key'] = auth.key;\n return;\n }\n}\n\n/** Interpolate {name} path templates with encoded values from the input. */\nfunction interpolatePath(template: string, pathParams: Record<string, unknown>): string {\n return template.replace(/\\{([^}]+)\\}/g, (_match, key: string) => {\n const value = pathParams[key];\n return value === undefined || value === null ? `{${key}}` : encodeURIComponent(String(value));\n });\n}\n\n/** Coerce a record of mixed values into string values, dropping null/undefined. */\nfunction stringifyValues(rec: Record<string, unknown>): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(rec)) {\n if (v === undefined || v === null) continue;\n out[k] = String(v);\n }\n return out;\n}\n\n/** Return v if it is a plain object, else an empty record. */\nfunction asRecord(v: unknown): Record<string, unknown> {\n return v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : {};\n}\n\n/** Ensure a deterministically unique action key within the connector. */\nfunction uniqueKey(base: string, seen: Set<string>): string {\n let candidate = base;\n if (seen.has(candidate)) {\n let i = 2;\n while (seen.has(`${base}_${i}`)) i++;\n candidate = `${base}_${i}`;\n }\n seen.add(candidate);\n return candidate;\n}\n\n/** Slugify a string into a snake_case machine name (`/^[a-z_][a-z0-9_]*$/`). */\nfunction slug(s: string): string {\n const out = s\n .normalize('NFKD')\n .replace(/[^a-zA-Z0-9]+/g, '_')\n .replace(/^_+|_+$/g, '')\n .toLowerCase();\n if (!out) return 'connector';\n return /^[a-z_]/.test(out) ? out : `op_${out}`;\n}\n\n/** Title-case a snake_case key for a default label (`get_pets` → `Get Pets`). */\nfunction titleize(name: string): string {\n return name\n .split('_')\n .filter(Boolean)\n .map((w) => w.charAt(0).toUpperCase() + w.slice(1))\n .join(' ');\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type {\n ConnectorProviderContext,\n ConnectorProviderFactory,\n ResolvedConnectorAuth,\n} from '@objectstack/spec/integration';\nimport { ConnectorUpstreamUnavailableError } from '@objectstack/spec/integration';\nimport {\n createOpenApiConnector,\n type OpenApiDocument,\n type RestAuth,\n} from './openapi-connector.js';\n\n/**\n * The provider key this package contributes (ADR-0097). A declarative\n * `connectors:` entry with `provider: 'openapi'` is materialized by this factory.\n */\nexport const OPENAPI_PROVIDER_KEY = 'openapi';\n\n/**\n * HTTP statuses treated as **transient** when fetching a remote spec at boot\n * (#3049 follow-up): a timeout / rate-limit / 5xx means the spec endpoint is\n * momentarily unhealthy, not that the connector is misconfigured — so the\n * instance degrades and retries rather than aborting boot. Mirrors the\n * connector request-retry convention (`retryableStatusCodes` default in\n * `ConnectorSchema`). Any OTHER non-2xx (400 / 401 / 403 / 404 / 410 …) means\n * the request itself is wrong — a configuration fault that stays fatal.\n */\nconst SPEC_FETCH_RETRYABLE_STATUS = new Set([408, 429, 500, 502, 503, 504]);\n\n/** Injectable dependencies for {@link createOpenApiProviderFactory} (tests). */\nexport interface OpenApiProviderDeps {\n /** Injected fetch implementation (spec fetch + request transport); defaults to global `fetch`. */\n fetchImpl?: typeof fetch;\n}\n\n/** Shape of `providerConfig` for a `provider: 'openapi'` declarative instance. */\ninterface OpenApiProviderConfig {\n /**\n * The OpenAPI 3.x document: an inline object, an http(s) URL to fetch at\n * boot, or a file path resolved relative to the declaring stack/package root\n * (`'./billing-openapi.json'`, #3016).\n *\n * A remote URL that is unreachable / transiently failing (network error,\n * 408 / 429 / 5xx) degrades the instance and retries (#3049); a wrong URL\n * (non-retryable 4xx) or an unparseable document stays a fatal config fault.\n */\n spec?: unknown;\n /** Override the base URL (else the document's `servers[0].url`). */\n baseUrl?: unknown;\n}\n\n/**\n * Resolve `providerConfig.spec` into a parsed OpenAPI document (ADR-0097;\n * union per #3016): an inline document object (the reliable, no-I/O-at-boot\n * form used by the showcase), an http(s) URL fetched at materialization, or a\n * **file path** read through the host's `ctx.loadPackageFile` — which resolves\n * it relative to the declaring stack/package root and confines the read to\n * that root (absolute / `..`-escaping paths are rejected there).\n *\n * Fault classification (#3049 seam, symmetric with connector-mcp's connect\n * path): a remote spec URL that is unreachable or transiently failing throws\n * {@link ConnectorUpstreamUnavailableError} — the materializer degrades the\n * instance and retries. Every OTHER failure (missing spec, a wrong URL,\n * an unparseable document, no host file access) throws a plain error, which\n * stays fatal at boot / a skipped entry on reload.\n */\nasync function loadOpenApiDocument(\n spec: unknown,\n fetchImpl: typeof fetch | undefined,\n ctx: ConnectorProviderContext,\n): Promise<OpenApiDocument> {\n const connectorName = ctx.name;\n if (spec && typeof spec === 'object' && !Array.isArray(spec)) {\n return spec as OpenApiDocument;\n }\n if (typeof spec === 'string' && spec.length > 0) {\n if (/^https?:\\/\\//i.test(spec)) {\n const doFetch = fetchImpl ?? fetch;\n let res: Response;\n try {\n res = await doFetch(spec);\n } catch (err) {\n // The spec endpoint is unreachable — DNS / connection refused /\n // timeout / network error. Operational, not a config mistake: degrade\n // + retry rather than fail boot.\n throw new ConnectorUpstreamUnavailableError(\n `connector-openapi provider: connector '${connectorName}' could not reach spec URL '${spec}': ${(err as Error).message}`,\n { cause: err },\n );\n }\n if (!res.ok) {\n if (SPEC_FETCH_RETRYABLE_STATUS.has(res.status)) {\n // A transient server-side status (timeout / rate-limit / 5xx) — the\n // endpoint is momentarily unhealthy, so treat it as upstream-unavailable.\n throw new ConnectorUpstreamUnavailableError(\n `connector-openapi provider: connector '${connectorName}' got a transient HTTP ${res.status} fetching spec '${spec}'.`,\n );\n }\n // A non-retryable status (400 / 401 / 403 / 404 / 410 …) — the request\n // is wrong (bad URL, missing/insufficient auth for the spec endpoint):\n // a configuration fault that stays fatal.\n throw new Error(\n `connector-openapi provider: connector '${connectorName}' failed to fetch spec '${spec}' (HTTP ${res.status}).`,\n );\n }\n // A 2xx with an unparseable / non-object body is a content fault (the\n // endpoint served the wrong thing) — fatal, symmetric with the file-path\n // parse below.\n try {\n const parsed: unknown = await res.json();\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error('not a JSON object');\n }\n return parsed as OpenApiDocument;\n } catch (err) {\n throw new Error(\n `connector-openapi provider: connector '${connectorName}' fetched spec '${spec}' but it is not a parseable ` +\n `OpenAPI JSON document: ${(err as Error).message}`,\n );\n }\n }\n // File path — dereferenced through the host capability so resolution stays\n // anchored to (and confined within) the declaring stack/package root.\n if (!ctx.loadPackageFile) {\n throw new Error(\n `connector-openapi provider: connector '${connectorName}' providerConfig.spec '${spec}' is a file path, ` +\n `but this host provides no package file access — inline the OpenAPI document or use an http(s) URL.`,\n );\n }\n let text: string;\n try {\n text = await ctx.loadPackageFile(spec);\n } catch (err) {\n throw new Error(\n `connector-openapi provider: connector '${connectorName}' failed to read providerConfig.spec '${spec}': ` +\n `${(err as Error).message}`,\n );\n }\n try {\n const parsed: unknown = JSON.parse(text);\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error('not a JSON object');\n }\n return parsed as OpenApiDocument;\n } catch (err) {\n throw new Error(\n `connector-openapi provider: connector '${connectorName}' providerConfig.spec '${spec}' is not a parseable ` +\n `OpenAPI JSON document: ${(err as Error).message}`,\n );\n }\n }\n throw new Error(\n `connector-openapi provider: connector '${connectorName}' requires providerConfig.spec — an inline OpenAPI 3.x ` +\n `document object, an http(s) URL, or a package-relative file path.`,\n );\n}\n\n/**\n * Build the `openapi` {@link ConnectorProviderFactory} (ADR-0097 / ADR-0023). At\n * boot the automation service invokes it for each `provider: 'openapi'`\n * declarative instance: it loads the OpenAPI document from `providerConfig.spec`,\n * then produces the same `{ def, handlers }` bundle {@link createOpenApiConnector}\n * generates for a hand-wired OpenAPI connector — one action per operation over a\n * static-auth HTTP transport, with the resolved `auth` applied.\n *\n * Hard-fails on invalid config (missing spec, a wrong spec URL, an unparseable\n * document, a bad base URL), so a misconfigured instance fails boot loudly. A\n * remote spec URL that is merely unreachable / transiently failing instead\n * degrades the instance and retries (#3049) — see {@link loadOpenApiDocument}.\n */\nexport function createOpenApiProviderFactory(deps: OpenApiProviderDeps = {}): ConnectorProviderFactory {\n return async (ctx) => {\n const cfg = (ctx.providerConfig ?? {}) as OpenApiProviderConfig;\n if (cfg.baseUrl !== undefined && typeof cfg.baseUrl !== 'string') {\n throw new Error(\n `connector-openapi provider: connector '${ctx.name}' providerConfig.baseUrl must be a string when set.`,\n );\n }\n const document = await loadOpenApiDocument(cfg.spec, deps.fetchImpl, ctx);\n const auth = ctx.auth as ResolvedConnectorAuth | undefined as RestAuth | undefined;\n return createOpenApiConnector({\n name: ctx.name,\n label: ctx.label,\n description: ctx.description,\n document,\n baseUrl: typeof cfg.baseUrl === 'string' ? cfg.baseUrl : undefined,\n auth,\n fetchImpl: deps.fetchImpl,\n });\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { Connector, ConnectorProviderFactory } from '@objectstack/spec/integration';\nimport { createOpenApiConnector, type OpenApiConnectorConfig } from './openapi-connector.js';\nimport { createOpenApiProviderFactory, OPENAPI_PROVIDER_KEY } from './openapi-provider.js';\n\n/**\n * Minimal surface of the automation engine this package depends on — the\n * connector registry (ADR-0018 §Addendum) plus the provider registry (ADR-0097).\n * Kept structural so callers need no runtime dependency on\n * `@objectstack/service-automation` (mirrors connector-rest / connector-mcp).\n */\nexport interface ConnectorRegistrySurface {\n registerConnector(\n def: Connector,\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >,\n ): void;\n unregisterConnector(name: string): void;\n registerConnectorProvider(providerKey: string, factory: ConnectorProviderFactory): void;\n}\n\n/**\n * Generate an OpenAPI-backed connector and register it on the engine's connector\n * registry so the baseline `connector_action` node can dispatch to the generated\n * actions (ADR-0023). Returns the registered connector name.\n */\nexport function registerOpenApiConnector(registry: ConnectorRegistrySurface, config: OpenApiConnectorConfig): string {\n const { def, handlers } = createOpenApiConnector(config);\n registry.registerConnector(def, handlers);\n return def.name;\n}\n\n/**\n * Options for {@link ConnectorOpenApiPlugin}. All optional (ADR-0097): with no\n * `document` the plugin contributes only the `openapi` provider factory — so a\n * stack can declare `provider: 'openapi'` instances as pure metadata. Supply a\n * `document` (+ config) to ALSO register one hand-wired OpenAPI connector.\n */\nexport interface ConnectorOpenApiPluginOptions extends Partial<OpenApiConnectorConfig> {\n /** Injected fetch implementation forwarded to the `openapi` provider factory (tests). */\n fetchImpl?: typeof fetch;\n}\n\n/**\n * ConnectorOpenApiPlugin — contributes the OpenAPI generic executor (ADR-0023) in\n * two forms:\n *\n * 1. **Provider factory** (`openapi`, ADR-0097): registered at `init()` so the\n * automation service can materialize declarative `provider: 'openapi'`\n * `connectors:` entries — loading the OpenAPI document and mapping each\n * operation to a connector action — at boot.\n * 2. **Hand-wired instance** (optional, back-compat): when constructed with a\n * `document`, it also registers one concrete OpenAPI connector at `start()`.\n *\n * If no automation engine is present the plugin logs and skips.\n */\nexport class ConnectorOpenApiPlugin implements Plugin {\n name = 'com.objectstack.connector.openapi';\n version = '1.0.0';\n type = 'standard' as const;\n // Ensure the automation engine (and its connector/provider registries) exist first.\n dependencies = ['com.objectstack.service-automation'];\n\n private readonly options: ConnectorOpenApiPluginOptions;\n private connectorName?: string;\n private automation?: ConnectorRegistrySurface;\n\n constructor(options: ConnectorOpenApiPluginOptions = {}) {\n this.options = options;\n }\n\n async init(ctx: PluginContext): Promise<void> {\n // Contribute the `openapi` provider factory (ADR-0097) before the\n // automation service materializes declarative instances during its start().\n const automation = this.tryGetAutomation(ctx);\n if (automation && typeof automation.registerConnectorProvider === 'function') {\n automation.registerConnectorProvider(\n OPENAPI_PROVIDER_KEY,\n createOpenApiProviderFactory({ fetchImpl: this.options.fetchImpl }),\n );\n ctx.logger.info(\"ConnectorOpenApiPlugin: registered 'openapi' connector provider\");\n }\n }\n\n async start(ctx: PluginContext): Promise<void> {\n // Provider-only usage (no document) contributes just the factory in init().\n if (!this.options.document) return;\n\n const automation = this.tryGetAutomation(ctx);\n if (!automation || typeof automation.registerConnector !== 'function') {\n ctx.logger.info('ConnectorOpenApiPlugin: no automation engine — OpenAPI connector not registered');\n return;\n }\n\n this.connectorName = registerOpenApiConnector(automation, this.options as OpenApiConnectorConfig);\n this.automation = automation;\n ctx.logger.info(`ConnectorOpenApiPlugin: OpenAPI connector '${this.connectorName}' registered`);\n }\n\n async stop(_ctx: PluginContext): Promise<void> {\n if (this.automation && this.connectorName) {\n try { this.automation.unregisterConnector(this.connectorName); } catch { /* ignore */ }\n }\n }\n\n private tryGetAutomation(ctx: PluginContext): ConnectorRegistrySurface | undefined {\n try {\n return ctx.getService<ConnectorRegistrySurface>('automation');\n } catch {\n return undefined;\n }\n }\n}\n"],"mappings":"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","names":["schema"]}
|
package/dist/index.mjs
CHANGED
|
@@ -212,7 +212,9 @@ function titleize(name) {
|
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
// src/openapi-provider.ts
|
|
215
|
+
import { ConnectorUpstreamUnavailableError } from "@objectstack/spec/integration";
|
|
215
216
|
var OPENAPI_PROVIDER_KEY = "openapi";
|
|
217
|
+
var SPEC_FETCH_RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
|
|
216
218
|
async function loadOpenApiDocument(spec, fetchImpl, ctx) {
|
|
217
219
|
const connectorName = ctx.name;
|
|
218
220
|
if (spec && typeof spec === "object" && !Array.isArray(spec)) {
|
|
@@ -221,13 +223,36 @@ async function loadOpenApiDocument(spec, fetchImpl, ctx) {
|
|
|
221
223
|
if (typeof spec === "string" && spec.length > 0) {
|
|
222
224
|
if (/^https?:\/\//i.test(spec)) {
|
|
223
225
|
const doFetch = fetchImpl ?? fetch;
|
|
224
|
-
|
|
226
|
+
let res;
|
|
227
|
+
try {
|
|
228
|
+
res = await doFetch(spec);
|
|
229
|
+
} catch (err) {
|
|
230
|
+
throw new ConnectorUpstreamUnavailableError(
|
|
231
|
+
`connector-openapi provider: connector '${connectorName}' could not reach spec URL '${spec}': ${err.message}`,
|
|
232
|
+
{ cause: err }
|
|
233
|
+
);
|
|
234
|
+
}
|
|
225
235
|
if (!res.ok) {
|
|
236
|
+
if (SPEC_FETCH_RETRYABLE_STATUS.has(res.status)) {
|
|
237
|
+
throw new ConnectorUpstreamUnavailableError(
|
|
238
|
+
`connector-openapi provider: connector '${connectorName}' got a transient HTTP ${res.status} fetching spec '${spec}'.`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
226
241
|
throw new Error(
|
|
227
242
|
`connector-openapi provider: connector '${connectorName}' failed to fetch spec '${spec}' (HTTP ${res.status}).`
|
|
228
243
|
);
|
|
229
244
|
}
|
|
230
|
-
|
|
245
|
+
try {
|
|
246
|
+
const parsed = await res.json();
|
|
247
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
248
|
+
throw new Error("not a JSON object");
|
|
249
|
+
}
|
|
250
|
+
return parsed;
|
|
251
|
+
} catch (err) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
`connector-openapi provider: connector '${connectorName}' fetched spec '${spec}' but it is not a parseable OpenAPI JSON document: ${err.message}`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
231
256
|
}
|
|
232
257
|
if (!ctx.loadPackageFile) {
|
|
233
258
|
throw new Error(
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/openapi-connector.ts","../src/openapi-provider.ts","../src/connector-openapi-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Connector } from '@objectstack/spec/integration';\n\n/**\n * OpenAPI connector generator — turns a declarative OpenAPI 3.x document into a\n * {@link Connector} definition + handler map (ADR-0023).\n *\n * Each OpenAPI operation maps to one connector action; a single generic handler\n * (closing over the operation's method + path template) drives one shared HTTP\n * request implementation. That transport mirrors `@objectstack/connector-rest`\n * (build URL from base+path+query, apply static auth, JSON-encode the body,\n * normalise the response to `{ status, ok, body }`) — kept inline so this package\n * stays self-contained, depending only on `@objectstack/core` + `@objectstack/spec`\n * like its sibling connectors. The output is an ordinary `type: 'api'` connector,\n * registered via `engine.registerConnector(def, handlers)` exactly like a\n * hand-written one — the registry, the `connector_action` node, the discovery\n * route, and the Studio palette never know it came from OpenAPI.\n *\n * Open-source scope: **static** auth only (`none` / `api-key` / `basic` /\n * `bearer`), with credentials supplied by the caller. Managed OAuth2, credential\n * vaulting, and per-tenant lifecycle are the enterprise tier (ADR-0015 / 0022).\n */\n\n/** Static auth understood by the generated connector (the open-source subset). */\nexport type RestAuth = Extract<Connector['authentication'], { type: 'none' | 'api-key' | 'basic' | 'bearer' }>;\n\n/** An action on a Connector definition (derived to avoid guessing export names). */\ntype ConnectorAction = NonNullable<Connector['actions']>[number];\n\n/** Handler signature accepted by the connector registry (ADR-0018 §Addendum). */\ntype ConnectorHandler = (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>;\n\n/** A connector definition paired with its action handlers, ready for registerConnector(). */\nexport interface OpenApiConnectorBundle {\n def: Connector;\n handlers: Record<string, ConnectorHandler>;\n}\n\n/** A free-form JSON Schema fragment (matches ConnectorAction input/outputSchema). */\nexport type JsonSchema = Record<string, unknown>;\n\n/** Minimal subset of an OpenAPI 3.x document consumed by the generator.\n * The caller is responsible for loading and de-referencing ($ref) the doc. */\nexport interface OpenApiDocument {\n openapi?: string;\n info?: { title?: string; description?: string; version?: string };\n servers?: { url: string }[];\n paths?: Record<string, OpenApiPathItem>;\n components?: { securitySchemes?: Record<string, OpenApiSecurityScheme> };\n}\n\nexport interface OpenApiPathItem {\n [method: string]: OpenApiOperation | unknown;\n}\n\nexport interface OpenApiOperation {\n operationId?: string;\n summary?: string;\n description?: string;\n tags?: string[];\n parameters?: OpenApiParameter[];\n requestBody?: OpenApiRequestBody;\n responses?: Record<string, OpenApiResponse>;\n}\n\nexport interface OpenApiParameter {\n name: string;\n in: 'path' | 'query' | 'header' | 'cookie';\n required?: boolean;\n description?: string;\n schema?: JsonSchema;\n}\n\nexport interface OpenApiRequestBody {\n required?: boolean;\n description?: string;\n content?: Record<string, { schema?: JsonSchema }>;\n}\n\nexport interface OpenApiResponse {\n description?: string;\n content?: Record<string, { schema?: JsonSchema }>;\n}\n\nexport interface OpenApiSecurityScheme {\n type: 'apiKey' | 'http' | 'oauth2' | 'openIdConnect';\n name?: string;\n in?: 'header' | 'query' | 'cookie';\n scheme?: string;\n}\n\n/** Flattened view of a single operation, passed to the `include` predicate. */\nexport interface OperationInfo {\n operationId?: string;\n method: string;\n path: string;\n tags?: string[];\n summary?: string;\n description?: string;\n}\n\n/** Configuration for {@link createOpenApiConnector}. */\nexport interface OpenApiConnectorConfig {\n /** Connector machine name (snake_case). Defaults to a slug of info.title. */\n name?: string;\n /** Human-friendly label. Defaults to info.title (then name). */\n label?: string;\n /** Description. Defaults to info.description. */\n description?: string;\n /** Icon identifier for the Studio palette. Defaults to `globe`. */\n icon?: string;\n /** The parsed OpenAPI 3.x document (caller loads/derefs it). */\n document: OpenApiDocument;\n /** Override the base URL (else servers[0].url). */\n baseUrl?: string;\n /** Static auth with credentials. Defaults to `{ type: 'none' }`. */\n auth?: RestAuth;\n /** Headers merged into every request (request-level headers win). */\n defaultHeaders?: Record<string, string>;\n /** Only include operations for which this predicate returns true (allowlist). */\n include?: (op: OperationInfo) => boolean;\n /** Injected fetch implementation (defaults to global `fetch`). */\n fetchImpl?: typeof fetch;\n}\n\n/** OpenAPI HTTP method keys, in a deterministic order. */\nconst HTTP_METHODS = ['get', 'put', 'post', 'delete', 'patch', 'options', 'head', 'trace'] as const;\n\n/** Input passed to the shared request transport. */\ninterface RequestInput {\n method: string;\n path: string;\n headers?: Record<string, string>;\n query?: Record<string, string>;\n body?: unknown;\n}\n\n/**\n * Build an OpenAPI connector definition and its handler map.\n *\n * @returns the `Connector` definition (`def`) and a `handlers` record keyed by\n * action key, suitable for `engine.registerConnector(def, handlers)`.\n */\nexport function createOpenApiConnector(config: OpenApiConnectorConfig): OpenApiConnectorBundle {\n const { document, include } = config;\n const auth: RestAuth = config.auth ?? { type: 'none' };\n const doFetch = config.fetchImpl ?? fetch;\n const name = config.name ?? slug(document.info?.title ?? 'openapi_connector');\n const label = config.label ?? document.info?.title ?? titleize(name);\n const description = config.description ?? document.info?.description;\n const baseUrl = config.baseUrl ?? document.servers?.[0]?.url;\n if (!baseUrl) {\n throw new Error('createOpenApiConnector: no base URL — provide config.baseUrl or document.servers[0].url');\n }\n\n // One shared transport (mirrors connector-rest) reused by every action handler.\n async function request(input: RequestInput): Promise<Record<string, unknown>> {\n const method = input.method.toUpperCase();\n const headers: Record<string, string> = { ...config.defaultHeaders, ...input.headers };\n const query: Record<string, string> = { ...input.query };\n applyAuth(auth, headers, query);\n\n const url = buildUrl(baseUrl as string, input.path, query);\n const hasBody = input.body !== undefined && method !== 'GET' && method !== 'HEAD';\n if (hasBody && headers['Content-Type'] === undefined && headers['content-type'] === undefined) {\n headers['Content-Type'] = 'application/json';\n }\n\n const response = await doFetch(url, {\n method,\n headers,\n body: hasBody ? JSON.stringify(input.body) : undefined,\n });\n\n const contentType = response.headers.get('content-type') ?? '';\n const parsed = contentType.includes('application/json') ? await response.json() : await response.text();\n return { status: response.status, ok: response.ok, body: parsed };\n }\n\n const actions: ConnectorAction[] = [];\n const handlers: Record<string, ConnectorHandler> = {};\n const seenKeys = new Set<string>();\n\n for (const op of collectOperations(document)) {\n if (include && !include(toInfo(op))) continue;\n const key = uniqueKey(op.operationId ?? slug(`${op.method}_${op.path}`), seenKeys);\n\n actions.push({\n key,\n label: op.summary ?? titleize(key),\n description: op.description,\n inputSchema: buildInputSchema(op),\n outputSchema: buildOutputSchema(op),\n });\n\n handlers[key] = async (input: Record<string, unknown>) => {\n const req = input as { path?: unknown; query?: unknown; header?: unknown; body?: unknown };\n return request({\n method: op.method,\n path: interpolatePath(op.path, asRecord(req.path)),\n query: stringifyValues(asRecord(req.query)),\n headers: stringifyValues(asRecord(req.header)),\n body: req.body,\n });\n };\n }\n\n const def: Connector = {\n name,\n label,\n type: 'api',\n description,\n icon: config.icon ?? 'globe',\n authentication: auth,\n // Defaulted by ConnectorSchema; set explicitly so the literal satisfies\n // the (post-parse) Connector output type (mirrors connector-rest/mcp).\n status: 'active',\n enabled: true,\n connectionTimeoutMs: 30000,\n requestTimeoutMs: 30000,\n actions,\n };\n\n return { def, handlers };\n}\n\ninterface Op extends OpenApiOperation {\n method: string;\n path: string;\n}\n\n/** Flatten paths × methods into a deterministic list of operations. */\nfunction collectOperations(doc: OpenApiDocument): Op[] {\n const ops: Op[] = [];\n for (const [path, item] of Object.entries(doc.paths ?? {})) {\n if (!item || typeof item !== 'object') continue;\n const record = item as Record<string, unknown>;\n for (const method of HTTP_METHODS) {\n const operation = record[method] as OpenApiOperation | undefined;\n if (!operation || typeof operation !== 'object') continue;\n ops.push({ ...operation, method, path });\n }\n }\n return ops;\n}\n\nfunction toInfo(op: Op): OperationInfo {\n return {\n operationId: op.operationId,\n method: op.method,\n path: op.path,\n tags: op.tags,\n summary: op.summary,\n description: op.description,\n };\n}\n\n/**\n * Assemble the action inputSchema from an operation's parameters + requestBody.\n * Produces { type: 'object', properties: { path, query, header, body }, required }\n * where only non-empty sections are emitted.\n */\nfunction buildInputSchema(op: OpenApiOperation): JsonSchema | undefined {\n const sections: Record<'path' | 'query' | 'header', { props: Record<string, JsonSchema>; required: string[] }> = {\n path: { props: {}, required: [] },\n query: { props: {}, required: [] },\n header: { props: {}, required: [] },\n };\n\n for (const p of op.parameters ?? []) {\n if (!p || typeof p !== 'object' || '$ref' in p) continue;\n if (p.in !== 'path' && p.in !== 'query' && p.in !== 'header') continue;\n const sec = sections[p.in];\n sec.props[p.name] = p.schema ?? (p.description ? { type: 'string', description: p.description } : { type: 'string' });\n if (p.required) sec.required.push(p.name);\n }\n\n const properties: Record<string, JsonSchema> = {};\n const required: string[] = [];\n for (const where of ['path', 'query', 'header'] as const) {\n const sec = sections[where];\n if (Object.keys(sec.props).length === 0) continue;\n const schema: JsonSchema = { type: 'object', properties: sec.props };\n if (sec.required.length) schema.required = sec.required;\n properties[where] = schema;\n // Path params are always required when present; others only if any are.\n if (where === 'path' || sec.required.length) required.push(where);\n }\n\n const bodySchema = extractRequestBodySchema(op.requestBody);\n if (bodySchema) {\n properties.body = bodySchema;\n if (op.requestBody && !('$ref' in op.requestBody) && op.requestBody.required) required.push('body');\n }\n\n if (Object.keys(properties).length === 0) return undefined;\n const schema: JsonSchema = { type: 'object', properties };\n if (required.length) schema.required = required;\n return schema;\n}\n\n/** Pick the success response's JSON schema (200 → first 2xx → default). */\nfunction buildOutputSchema(op: OpenApiOperation): JsonSchema | undefined {\n const responses = op.responses;\n if (!responses) return undefined;\n let code: string | undefined;\n if (responses['200']) code = '200';\n else code = Object.keys(responses).find((c) => /^2\\d\\d$/.test(c));\n if (!code && responses['default']) code = 'default';\n if (!code) return undefined;\n const resp = responses[code];\n if (!resp || typeof resp !== 'object' || '$ref' in resp) return undefined;\n return pickJsonSchema(resp.content);\n}\n\n/** Extract the requestBody JSON schema (prefers application/json). */\nfunction extractRequestBodySchema(rb: OpenApiRequestBody | undefined): JsonSchema | undefined {\n if (!rb || typeof rb !== 'object' || '$ref' in rb) return undefined;\n return pickJsonSchema(rb.content);\n}\n\n/** Choose the application/json schema, falling back to the first content type. */\nfunction pickJsonSchema(content: Record<string, { schema?: JsonSchema }> | undefined): JsonSchema | undefined {\n if (!content) return undefined;\n const chosen = content['application/json'] ?? Object.values(content)[0];\n return chosen?.schema;\n}\n\n/** Build the request URL from base + path + query, encoding query params. */\nfunction buildUrl(baseUrl: string, path: string, query: Record<string, string>): string {\n const base = baseUrl.replace(/\\/+$/, '');\n const suffix = path ? (path.startsWith('/') ? path : `/${path}`) : '';\n const url = new URL(base + suffix);\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null) url.searchParams.set(key, String(value));\n }\n return url.toString();\n}\n\n/** Apply static auth to the outgoing headers / query (mirrors connector-rest). */\nfunction applyAuth(auth: RestAuth, headers: Record<string, string>, query: Record<string, string>): void {\n switch (auth.type) {\n case 'none':\n return;\n case 'bearer':\n headers['Authorization'] = `Bearer ${auth.token}`;\n return;\n case 'basic': {\n const encoded = Buffer.from(`${auth.username}:${auth.password}`).toString('base64');\n headers['Authorization'] = `Basic ${encoded}`;\n return;\n }\n case 'api-key':\n if (auth.paramName) query[auth.paramName] = auth.key;\n else headers[auth.headerName ?? 'X-API-Key'] = auth.key;\n return;\n }\n}\n\n/** Interpolate {name} path templates with encoded values from the input. */\nfunction interpolatePath(template: string, pathParams: Record<string, unknown>): string {\n return template.replace(/\\{([^}]+)\\}/g, (_match, key: string) => {\n const value = pathParams[key];\n return value === undefined || value === null ? `{${key}}` : encodeURIComponent(String(value));\n });\n}\n\n/** Coerce a record of mixed values into string values, dropping null/undefined. */\nfunction stringifyValues(rec: Record<string, unknown>): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(rec)) {\n if (v === undefined || v === null) continue;\n out[k] = String(v);\n }\n return out;\n}\n\n/** Return v if it is a plain object, else an empty record. */\nfunction asRecord(v: unknown): Record<string, unknown> {\n return v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : {};\n}\n\n/** Ensure a deterministically unique action key within the connector. */\nfunction uniqueKey(base: string, seen: Set<string>): string {\n let candidate = base;\n if (seen.has(candidate)) {\n let i = 2;\n while (seen.has(`${base}_${i}`)) i++;\n candidate = `${base}_${i}`;\n }\n seen.add(candidate);\n return candidate;\n}\n\n/** Slugify a string into a snake_case machine name (`/^[a-z_][a-z0-9_]*$/`). */\nfunction slug(s: string): string {\n const out = s\n .normalize('NFKD')\n .replace(/[^a-zA-Z0-9]+/g, '_')\n .replace(/^_+|_+$/g, '')\n .toLowerCase();\n if (!out) return 'connector';\n return /^[a-z_]/.test(out) ? out : `op_${out}`;\n}\n\n/** Title-case a snake_case key for a default label (`get_pets` → `Get Pets`). */\nfunction titleize(name: string): string {\n return name\n .split('_')\n .filter(Boolean)\n .map((w) => w.charAt(0).toUpperCase() + w.slice(1))\n .join(' ');\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type {\n ConnectorProviderContext,\n ConnectorProviderFactory,\n ResolvedConnectorAuth,\n} from '@objectstack/spec/integration';\nimport {\n createOpenApiConnector,\n type OpenApiDocument,\n type RestAuth,\n} from './openapi-connector.js';\n\n/**\n * The provider key this package contributes (ADR-0097). A declarative\n * `connectors:` entry with `provider: 'openapi'` is materialized by this factory.\n */\nexport const OPENAPI_PROVIDER_KEY = 'openapi';\n\n/** Injectable dependencies for {@link createOpenApiProviderFactory} (tests). */\nexport interface OpenApiProviderDeps {\n /** Injected fetch implementation (spec fetch + request transport); defaults to global `fetch`. */\n fetchImpl?: typeof fetch;\n}\n\n/** Shape of `providerConfig` for a `provider: 'openapi'` declarative instance. */\ninterface OpenApiProviderConfig {\n /**\n * The OpenAPI 3.x document: an inline object, an http(s) URL to fetch at\n * boot, or a file path resolved relative to the declaring stack/package root\n * (`'./billing-openapi.json'`, #3016).\n */\n spec?: unknown;\n /** Override the base URL (else the document's `servers[0].url`). */\n baseUrl?: unknown;\n}\n\n/**\n * Resolve `providerConfig.spec` into a parsed OpenAPI document (ADR-0097;\n * union per #3016): an inline document object (the reliable, no-I/O-at-boot\n * form used by the showcase), an http(s) URL fetched at materialization, or a\n * **file path** read through the host's `ctx.loadPackageFile` — which resolves\n * it relative to the declaring stack/package root and confines the read to\n * that root (absolute / `..`-escaping paths are rejected there). Every failure\n * throws, so the materializer's reconcile policy applies: fatal at boot, the\n * entry is skipped on reload.\n */\nasync function loadOpenApiDocument(\n spec: unknown,\n fetchImpl: typeof fetch | undefined,\n ctx: ConnectorProviderContext,\n): Promise<OpenApiDocument> {\n const connectorName = ctx.name;\n if (spec && typeof spec === 'object' && !Array.isArray(spec)) {\n return spec as OpenApiDocument;\n }\n if (typeof spec === 'string' && spec.length > 0) {\n if (/^https?:\\/\\//i.test(spec)) {\n const doFetch = fetchImpl ?? fetch;\n const res = await doFetch(spec);\n if (!res.ok) {\n throw new Error(\n `connector-openapi provider: connector '${connectorName}' failed to fetch spec '${spec}' (HTTP ${res.status}).`,\n );\n }\n return (await res.json()) as OpenApiDocument;\n }\n // File path — dereferenced through the host capability so resolution stays\n // anchored to (and confined within) the declaring stack/package root.\n if (!ctx.loadPackageFile) {\n throw new Error(\n `connector-openapi provider: connector '${connectorName}' providerConfig.spec '${spec}' is a file path, ` +\n `but this host provides no package file access — inline the OpenAPI document or use an http(s) URL.`,\n );\n }\n let text: string;\n try {\n text = await ctx.loadPackageFile(spec);\n } catch (err) {\n throw new Error(\n `connector-openapi provider: connector '${connectorName}' failed to read providerConfig.spec '${spec}': ` +\n `${(err as Error).message}`,\n );\n }\n try {\n const parsed: unknown = JSON.parse(text);\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error('not a JSON object');\n }\n return parsed as OpenApiDocument;\n } catch (err) {\n throw new Error(\n `connector-openapi provider: connector '${connectorName}' providerConfig.spec '${spec}' is not a parseable ` +\n `OpenAPI JSON document: ${(err as Error).message}`,\n );\n }\n }\n throw new Error(\n `connector-openapi provider: connector '${connectorName}' requires providerConfig.spec — an inline OpenAPI 3.x ` +\n `document object, an http(s) URL, or a package-relative file path.`,\n );\n}\n\n/**\n * Build the `openapi` {@link ConnectorProviderFactory} (ADR-0097 / ADR-0023). At\n * boot the automation service invokes it for each `provider: 'openapi'`\n * declarative instance: it loads the OpenAPI document from `providerConfig.spec`,\n * then produces the same `{ def, handlers }` bundle {@link createOpenApiConnector}\n * generates for a hand-wired OpenAPI connector — one action per operation over a\n * static-auth HTTP transport, with the resolved `auth` applied.\n *\n * Hard-fails on invalid config (missing/unfetchable spec, no base URL), so a\n * misconfigured instance fails boot loudly.\n */\nexport function createOpenApiProviderFactory(deps: OpenApiProviderDeps = {}): ConnectorProviderFactory {\n return async (ctx) => {\n const cfg = (ctx.providerConfig ?? {}) as OpenApiProviderConfig;\n if (cfg.baseUrl !== undefined && typeof cfg.baseUrl !== 'string') {\n throw new Error(\n `connector-openapi provider: connector '${ctx.name}' providerConfig.baseUrl must be a string when set.`,\n );\n }\n const document = await loadOpenApiDocument(cfg.spec, deps.fetchImpl, ctx);\n const auth = ctx.auth as ResolvedConnectorAuth | undefined as RestAuth | undefined;\n return createOpenApiConnector({\n name: ctx.name,\n label: ctx.label,\n description: ctx.description,\n document,\n baseUrl: typeof cfg.baseUrl === 'string' ? cfg.baseUrl : undefined,\n auth,\n fetchImpl: deps.fetchImpl,\n });\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { Connector, ConnectorProviderFactory } from '@objectstack/spec/integration';\nimport { createOpenApiConnector, type OpenApiConnectorConfig } from './openapi-connector.js';\nimport { createOpenApiProviderFactory, OPENAPI_PROVIDER_KEY } from './openapi-provider.js';\n\n/**\n * Minimal surface of the automation engine this package depends on — the\n * connector registry (ADR-0018 §Addendum) plus the provider registry (ADR-0097).\n * Kept structural so callers need no runtime dependency on\n * `@objectstack/service-automation` (mirrors connector-rest / connector-mcp).\n */\nexport interface ConnectorRegistrySurface {\n registerConnector(\n def: Connector,\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >,\n ): void;\n unregisterConnector(name: string): void;\n registerConnectorProvider(providerKey: string, factory: ConnectorProviderFactory): void;\n}\n\n/**\n * Generate an OpenAPI-backed connector and register it on the engine's connector\n * registry so the baseline `connector_action` node can dispatch to the generated\n * actions (ADR-0023). Returns the registered connector name.\n */\nexport function registerOpenApiConnector(registry: ConnectorRegistrySurface, config: OpenApiConnectorConfig): string {\n const { def, handlers } = createOpenApiConnector(config);\n registry.registerConnector(def, handlers);\n return def.name;\n}\n\n/**\n * Options for {@link ConnectorOpenApiPlugin}. All optional (ADR-0097): with no\n * `document` the plugin contributes only the `openapi` provider factory — so a\n * stack can declare `provider: 'openapi'` instances as pure metadata. Supply a\n * `document` (+ config) to ALSO register one hand-wired OpenAPI connector.\n */\nexport interface ConnectorOpenApiPluginOptions extends Partial<OpenApiConnectorConfig> {\n /** Injected fetch implementation forwarded to the `openapi` provider factory (tests). */\n fetchImpl?: typeof fetch;\n}\n\n/**\n * ConnectorOpenApiPlugin — contributes the OpenAPI generic executor (ADR-0023) in\n * two forms:\n *\n * 1. **Provider factory** (`openapi`, ADR-0097): registered at `init()` so the\n * automation service can materialize declarative `provider: 'openapi'`\n * `connectors:` entries — loading the OpenAPI document and mapping each\n * operation to a connector action — at boot.\n * 2. **Hand-wired instance** (optional, back-compat): when constructed with a\n * `document`, it also registers one concrete OpenAPI connector at `start()`.\n *\n * If no automation engine is present the plugin logs and skips.\n */\nexport class ConnectorOpenApiPlugin implements Plugin {\n name = 'com.objectstack.connector.openapi';\n version = '1.0.0';\n type = 'standard' as const;\n // Ensure the automation engine (and its connector/provider registries) exist first.\n dependencies = ['com.objectstack.service-automation'];\n\n private readonly options: ConnectorOpenApiPluginOptions;\n private connectorName?: string;\n private automation?: ConnectorRegistrySurface;\n\n constructor(options: ConnectorOpenApiPluginOptions = {}) {\n this.options = options;\n }\n\n async init(ctx: PluginContext): Promise<void> {\n // Contribute the `openapi` provider factory (ADR-0097) before the\n // automation service materializes declarative instances during its start().\n const automation = this.tryGetAutomation(ctx);\n if (automation && typeof automation.registerConnectorProvider === 'function') {\n automation.registerConnectorProvider(\n OPENAPI_PROVIDER_KEY,\n createOpenApiProviderFactory({ fetchImpl: this.options.fetchImpl }),\n );\n ctx.logger.info(\"ConnectorOpenApiPlugin: registered 'openapi' connector provider\");\n }\n }\n\n async start(ctx: PluginContext): Promise<void> {\n // Provider-only usage (no document) contributes just the factory in init().\n if (!this.options.document) return;\n\n const automation = this.tryGetAutomation(ctx);\n if (!automation || typeof automation.registerConnector !== 'function') {\n ctx.logger.info('ConnectorOpenApiPlugin: no automation engine — OpenAPI connector not registered');\n return;\n }\n\n this.connectorName = registerOpenApiConnector(automation, this.options as OpenApiConnectorConfig);\n this.automation = automation;\n ctx.logger.info(`ConnectorOpenApiPlugin: OpenAPI connector '${this.connectorName}' registered`);\n }\n\n async stop(_ctx: PluginContext): Promise<void> {\n if (this.automation && this.connectorName) {\n try { this.automation.unregisterConnector(this.connectorName); } catch { /* ignore */ }\n }\n }\n\n private tryGetAutomation(ctx: PluginContext): ConnectorRegistrySurface | undefined {\n try {\n return ctx.getService<ConnectorRegistrySurface>('automation');\n } catch {\n return undefined;\n }\n }\n}\n"],"mappings":"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","names":["schema"]}
|
|
1
|
+
{"version":3,"sources":["../src/openapi-connector.ts","../src/openapi-provider.ts","../src/connector-openapi-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Connector } from '@objectstack/spec/integration';\n\n/**\n * OpenAPI connector generator — turns a declarative OpenAPI 3.x document into a\n * {@link Connector} definition + handler map (ADR-0023).\n *\n * Each OpenAPI operation maps to one connector action; a single generic handler\n * (closing over the operation's method + path template) drives one shared HTTP\n * request implementation. That transport mirrors `@objectstack/connector-rest`\n * (build URL from base+path+query, apply static auth, JSON-encode the body,\n * normalise the response to `{ status, ok, body }`) — kept inline so this package\n * stays self-contained, depending only on `@objectstack/core` + `@objectstack/spec`\n * like its sibling connectors. The output is an ordinary `type: 'api'` connector,\n * registered via `engine.registerConnector(def, handlers)` exactly like a\n * hand-written one — the registry, the `connector_action` node, the discovery\n * route, and the Studio palette never know it came from OpenAPI.\n *\n * Open-source scope: **static** auth only (`none` / `api-key` / `basic` /\n * `bearer`), with credentials supplied by the caller. Managed OAuth2, credential\n * vaulting, and per-tenant lifecycle are the enterprise tier (ADR-0015 / 0022).\n */\n\n/** Static auth understood by the generated connector (the open-source subset). */\nexport type RestAuth = Extract<Connector['authentication'], { type: 'none' | 'api-key' | 'basic' | 'bearer' }>;\n\n/** An action on a Connector definition (derived to avoid guessing export names). */\ntype ConnectorAction = NonNullable<Connector['actions']>[number];\n\n/** Handler signature accepted by the connector registry (ADR-0018 §Addendum). */\ntype ConnectorHandler = (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>;\n\n/** A connector definition paired with its action handlers, ready for registerConnector(). */\nexport interface OpenApiConnectorBundle {\n def: Connector;\n handlers: Record<string, ConnectorHandler>;\n}\n\n/** A free-form JSON Schema fragment (matches ConnectorAction input/outputSchema). */\nexport type JsonSchema = Record<string, unknown>;\n\n/** Minimal subset of an OpenAPI 3.x document consumed by the generator.\n * The caller is responsible for loading and de-referencing ($ref) the doc. */\nexport interface OpenApiDocument {\n openapi?: string;\n info?: { title?: string; description?: string; version?: string };\n servers?: { url: string }[];\n paths?: Record<string, OpenApiPathItem>;\n components?: { securitySchemes?: Record<string, OpenApiSecurityScheme> };\n}\n\nexport interface OpenApiPathItem {\n [method: string]: OpenApiOperation | unknown;\n}\n\nexport interface OpenApiOperation {\n operationId?: string;\n summary?: string;\n description?: string;\n tags?: string[];\n parameters?: OpenApiParameter[];\n requestBody?: OpenApiRequestBody;\n responses?: Record<string, OpenApiResponse>;\n}\n\nexport interface OpenApiParameter {\n name: string;\n in: 'path' | 'query' | 'header' | 'cookie';\n required?: boolean;\n description?: string;\n schema?: JsonSchema;\n}\n\nexport interface OpenApiRequestBody {\n required?: boolean;\n description?: string;\n content?: Record<string, { schema?: JsonSchema }>;\n}\n\nexport interface OpenApiResponse {\n description?: string;\n content?: Record<string, { schema?: JsonSchema }>;\n}\n\nexport interface OpenApiSecurityScheme {\n type: 'apiKey' | 'http' | 'oauth2' | 'openIdConnect';\n name?: string;\n in?: 'header' | 'query' | 'cookie';\n scheme?: string;\n}\n\n/** Flattened view of a single operation, passed to the `include` predicate. */\nexport interface OperationInfo {\n operationId?: string;\n method: string;\n path: string;\n tags?: string[];\n summary?: string;\n description?: string;\n}\n\n/** Configuration for {@link createOpenApiConnector}. */\nexport interface OpenApiConnectorConfig {\n /** Connector machine name (snake_case). Defaults to a slug of info.title. */\n name?: string;\n /** Human-friendly label. Defaults to info.title (then name). */\n label?: string;\n /** Description. Defaults to info.description. */\n description?: string;\n /** Icon identifier for the Studio palette. Defaults to `globe`. */\n icon?: string;\n /** The parsed OpenAPI 3.x document (caller loads/derefs it). */\n document: OpenApiDocument;\n /** Override the base URL (else servers[0].url). */\n baseUrl?: string;\n /** Static auth with credentials. Defaults to `{ type: 'none' }`. */\n auth?: RestAuth;\n /** Headers merged into every request (request-level headers win). */\n defaultHeaders?: Record<string, string>;\n /** Only include operations for which this predicate returns true (allowlist). */\n include?: (op: OperationInfo) => boolean;\n /** Injected fetch implementation (defaults to global `fetch`). */\n fetchImpl?: typeof fetch;\n}\n\n/** OpenAPI HTTP method keys, in a deterministic order. */\nconst HTTP_METHODS = ['get', 'put', 'post', 'delete', 'patch', 'options', 'head', 'trace'] as const;\n\n/** Input passed to the shared request transport. */\ninterface RequestInput {\n method: string;\n path: string;\n headers?: Record<string, string>;\n query?: Record<string, string>;\n body?: unknown;\n}\n\n/**\n * Build an OpenAPI connector definition and its handler map.\n *\n * @returns the `Connector` definition (`def`) and a `handlers` record keyed by\n * action key, suitable for `engine.registerConnector(def, handlers)`.\n */\nexport function createOpenApiConnector(config: OpenApiConnectorConfig): OpenApiConnectorBundle {\n const { document, include } = config;\n const auth: RestAuth = config.auth ?? { type: 'none' };\n const doFetch = config.fetchImpl ?? fetch;\n const name = config.name ?? slug(document.info?.title ?? 'openapi_connector');\n const label = config.label ?? document.info?.title ?? titleize(name);\n const description = config.description ?? document.info?.description;\n const baseUrl = config.baseUrl ?? document.servers?.[0]?.url;\n if (!baseUrl) {\n throw new Error('createOpenApiConnector: no base URL — provide config.baseUrl or document.servers[0].url');\n }\n\n // One shared transport (mirrors connector-rest) reused by every action handler.\n async function request(input: RequestInput): Promise<Record<string, unknown>> {\n const method = input.method.toUpperCase();\n const headers: Record<string, string> = { ...config.defaultHeaders, ...input.headers };\n const query: Record<string, string> = { ...input.query };\n applyAuth(auth, headers, query);\n\n const url = buildUrl(baseUrl as string, input.path, query);\n const hasBody = input.body !== undefined && method !== 'GET' && method !== 'HEAD';\n if (hasBody && headers['Content-Type'] === undefined && headers['content-type'] === undefined) {\n headers['Content-Type'] = 'application/json';\n }\n\n const response = await doFetch(url, {\n method,\n headers,\n body: hasBody ? JSON.stringify(input.body) : undefined,\n });\n\n const contentType = response.headers.get('content-type') ?? '';\n const parsed = contentType.includes('application/json') ? await response.json() : await response.text();\n return { status: response.status, ok: response.ok, body: parsed };\n }\n\n const actions: ConnectorAction[] = [];\n const handlers: Record<string, ConnectorHandler> = {};\n const seenKeys = new Set<string>();\n\n for (const op of collectOperations(document)) {\n if (include && !include(toInfo(op))) continue;\n const key = uniqueKey(op.operationId ?? slug(`${op.method}_${op.path}`), seenKeys);\n\n actions.push({\n key,\n label: op.summary ?? titleize(key),\n description: op.description,\n inputSchema: buildInputSchema(op),\n outputSchema: buildOutputSchema(op),\n });\n\n handlers[key] = async (input: Record<string, unknown>) => {\n const req = input as { path?: unknown; query?: unknown; header?: unknown; body?: unknown };\n return request({\n method: op.method,\n path: interpolatePath(op.path, asRecord(req.path)),\n query: stringifyValues(asRecord(req.query)),\n headers: stringifyValues(asRecord(req.header)),\n body: req.body,\n });\n };\n }\n\n const def: Connector = {\n name,\n label,\n type: 'api',\n description,\n icon: config.icon ?? 'globe',\n authentication: auth,\n // Defaulted by ConnectorSchema; set explicitly so the literal satisfies\n // the (post-parse) Connector output type (mirrors connector-rest/mcp).\n status: 'active',\n enabled: true,\n connectionTimeoutMs: 30000,\n requestTimeoutMs: 30000,\n actions,\n };\n\n return { def, handlers };\n}\n\ninterface Op extends OpenApiOperation {\n method: string;\n path: string;\n}\n\n/** Flatten paths × methods into a deterministic list of operations. */\nfunction collectOperations(doc: OpenApiDocument): Op[] {\n const ops: Op[] = [];\n for (const [path, item] of Object.entries(doc.paths ?? {})) {\n if (!item || typeof item !== 'object') continue;\n const record = item as Record<string, unknown>;\n for (const method of HTTP_METHODS) {\n const operation = record[method] as OpenApiOperation | undefined;\n if (!operation || typeof operation !== 'object') continue;\n ops.push({ ...operation, method, path });\n }\n }\n return ops;\n}\n\nfunction toInfo(op: Op): OperationInfo {\n return {\n operationId: op.operationId,\n method: op.method,\n path: op.path,\n tags: op.tags,\n summary: op.summary,\n description: op.description,\n };\n}\n\n/**\n * Assemble the action inputSchema from an operation's parameters + requestBody.\n * Produces { type: 'object', properties: { path, query, header, body }, required }\n * where only non-empty sections are emitted.\n */\nfunction buildInputSchema(op: OpenApiOperation): JsonSchema | undefined {\n const sections: Record<'path' | 'query' | 'header', { props: Record<string, JsonSchema>; required: string[] }> = {\n path: { props: {}, required: [] },\n query: { props: {}, required: [] },\n header: { props: {}, required: [] },\n };\n\n for (const p of op.parameters ?? []) {\n if (!p || typeof p !== 'object' || '$ref' in p) continue;\n if (p.in !== 'path' && p.in !== 'query' && p.in !== 'header') continue;\n const sec = sections[p.in];\n sec.props[p.name] = p.schema ?? (p.description ? { type: 'string', description: p.description } : { type: 'string' });\n if (p.required) sec.required.push(p.name);\n }\n\n const properties: Record<string, JsonSchema> = {};\n const required: string[] = [];\n for (const where of ['path', 'query', 'header'] as const) {\n const sec = sections[where];\n if (Object.keys(sec.props).length === 0) continue;\n const schema: JsonSchema = { type: 'object', properties: sec.props };\n if (sec.required.length) schema.required = sec.required;\n properties[where] = schema;\n // Path params are always required when present; others only if any are.\n if (where === 'path' || sec.required.length) required.push(where);\n }\n\n const bodySchema = extractRequestBodySchema(op.requestBody);\n if (bodySchema) {\n properties.body = bodySchema;\n if (op.requestBody && !('$ref' in op.requestBody) && op.requestBody.required) required.push('body');\n }\n\n if (Object.keys(properties).length === 0) return undefined;\n const schema: JsonSchema = { type: 'object', properties };\n if (required.length) schema.required = required;\n return schema;\n}\n\n/** Pick the success response's JSON schema (200 → first 2xx → default). */\nfunction buildOutputSchema(op: OpenApiOperation): JsonSchema | undefined {\n const responses = op.responses;\n if (!responses) return undefined;\n let code: string | undefined;\n if (responses['200']) code = '200';\n else code = Object.keys(responses).find((c) => /^2\\d\\d$/.test(c));\n if (!code && responses['default']) code = 'default';\n if (!code) return undefined;\n const resp = responses[code];\n if (!resp || typeof resp !== 'object' || '$ref' in resp) return undefined;\n return pickJsonSchema(resp.content);\n}\n\n/** Extract the requestBody JSON schema (prefers application/json). */\nfunction extractRequestBodySchema(rb: OpenApiRequestBody | undefined): JsonSchema | undefined {\n if (!rb || typeof rb !== 'object' || '$ref' in rb) return undefined;\n return pickJsonSchema(rb.content);\n}\n\n/** Choose the application/json schema, falling back to the first content type. */\nfunction pickJsonSchema(content: Record<string, { schema?: JsonSchema }> | undefined): JsonSchema | undefined {\n if (!content) return undefined;\n const chosen = content['application/json'] ?? Object.values(content)[0];\n return chosen?.schema;\n}\n\n/** Build the request URL from base + path + query, encoding query params. */\nfunction buildUrl(baseUrl: string, path: string, query: Record<string, string>): string {\n const base = baseUrl.replace(/\\/+$/, '');\n const suffix = path ? (path.startsWith('/') ? path : `/${path}`) : '';\n const url = new URL(base + suffix);\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null) url.searchParams.set(key, String(value));\n }\n return url.toString();\n}\n\n/** Apply static auth to the outgoing headers / query (mirrors connector-rest). */\nfunction applyAuth(auth: RestAuth, headers: Record<string, string>, query: Record<string, string>): void {\n switch (auth.type) {\n case 'none':\n return;\n case 'bearer':\n headers['Authorization'] = `Bearer ${auth.token}`;\n return;\n case 'basic': {\n const encoded = Buffer.from(`${auth.username}:${auth.password}`).toString('base64');\n headers['Authorization'] = `Basic ${encoded}`;\n return;\n }\n case 'api-key':\n if (auth.paramName) query[auth.paramName] = auth.key;\n else headers[auth.headerName ?? 'X-API-Key'] = auth.key;\n return;\n }\n}\n\n/** Interpolate {name} path templates with encoded values from the input. */\nfunction interpolatePath(template: string, pathParams: Record<string, unknown>): string {\n return template.replace(/\\{([^}]+)\\}/g, (_match, key: string) => {\n const value = pathParams[key];\n return value === undefined || value === null ? `{${key}}` : encodeURIComponent(String(value));\n });\n}\n\n/** Coerce a record of mixed values into string values, dropping null/undefined. */\nfunction stringifyValues(rec: Record<string, unknown>): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(rec)) {\n if (v === undefined || v === null) continue;\n out[k] = String(v);\n }\n return out;\n}\n\n/** Return v if it is a plain object, else an empty record. */\nfunction asRecord(v: unknown): Record<string, unknown> {\n return v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : {};\n}\n\n/** Ensure a deterministically unique action key within the connector. */\nfunction uniqueKey(base: string, seen: Set<string>): string {\n let candidate = base;\n if (seen.has(candidate)) {\n let i = 2;\n while (seen.has(`${base}_${i}`)) i++;\n candidate = `${base}_${i}`;\n }\n seen.add(candidate);\n return candidate;\n}\n\n/** Slugify a string into a snake_case machine name (`/^[a-z_][a-z0-9_]*$/`). */\nfunction slug(s: string): string {\n const out = s\n .normalize('NFKD')\n .replace(/[^a-zA-Z0-9]+/g, '_')\n .replace(/^_+|_+$/g, '')\n .toLowerCase();\n if (!out) return 'connector';\n return /^[a-z_]/.test(out) ? out : `op_${out}`;\n}\n\n/** Title-case a snake_case key for a default label (`get_pets` → `Get Pets`). */\nfunction titleize(name: string): string {\n return name\n .split('_')\n .filter(Boolean)\n .map((w) => w.charAt(0).toUpperCase() + w.slice(1))\n .join(' ');\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type {\n ConnectorProviderContext,\n ConnectorProviderFactory,\n ResolvedConnectorAuth,\n} from '@objectstack/spec/integration';\nimport { ConnectorUpstreamUnavailableError } from '@objectstack/spec/integration';\nimport {\n createOpenApiConnector,\n type OpenApiDocument,\n type RestAuth,\n} from './openapi-connector.js';\n\n/**\n * The provider key this package contributes (ADR-0097). A declarative\n * `connectors:` entry with `provider: 'openapi'` is materialized by this factory.\n */\nexport const OPENAPI_PROVIDER_KEY = 'openapi';\n\n/**\n * HTTP statuses treated as **transient** when fetching a remote spec at boot\n * (#3049 follow-up): a timeout / rate-limit / 5xx means the spec endpoint is\n * momentarily unhealthy, not that the connector is misconfigured — so the\n * instance degrades and retries rather than aborting boot. Mirrors the\n * connector request-retry convention (`retryableStatusCodes` default in\n * `ConnectorSchema`). Any OTHER non-2xx (400 / 401 / 403 / 404 / 410 …) means\n * the request itself is wrong — a configuration fault that stays fatal.\n */\nconst SPEC_FETCH_RETRYABLE_STATUS = new Set([408, 429, 500, 502, 503, 504]);\n\n/** Injectable dependencies for {@link createOpenApiProviderFactory} (tests). */\nexport interface OpenApiProviderDeps {\n /** Injected fetch implementation (spec fetch + request transport); defaults to global `fetch`. */\n fetchImpl?: typeof fetch;\n}\n\n/** Shape of `providerConfig` for a `provider: 'openapi'` declarative instance. */\ninterface OpenApiProviderConfig {\n /**\n * The OpenAPI 3.x document: an inline object, an http(s) URL to fetch at\n * boot, or a file path resolved relative to the declaring stack/package root\n * (`'./billing-openapi.json'`, #3016).\n *\n * A remote URL that is unreachable / transiently failing (network error,\n * 408 / 429 / 5xx) degrades the instance and retries (#3049); a wrong URL\n * (non-retryable 4xx) or an unparseable document stays a fatal config fault.\n */\n spec?: unknown;\n /** Override the base URL (else the document's `servers[0].url`). */\n baseUrl?: unknown;\n}\n\n/**\n * Resolve `providerConfig.spec` into a parsed OpenAPI document (ADR-0097;\n * union per #3016): an inline document object (the reliable, no-I/O-at-boot\n * form used by the showcase), an http(s) URL fetched at materialization, or a\n * **file path** read through the host's `ctx.loadPackageFile` — which resolves\n * it relative to the declaring stack/package root and confines the read to\n * that root (absolute / `..`-escaping paths are rejected there).\n *\n * Fault classification (#3049 seam, symmetric with connector-mcp's connect\n * path): a remote spec URL that is unreachable or transiently failing throws\n * {@link ConnectorUpstreamUnavailableError} — the materializer degrades the\n * instance and retries. Every OTHER failure (missing spec, a wrong URL,\n * an unparseable document, no host file access) throws a plain error, which\n * stays fatal at boot / a skipped entry on reload.\n */\nasync function loadOpenApiDocument(\n spec: unknown,\n fetchImpl: typeof fetch | undefined,\n ctx: ConnectorProviderContext,\n): Promise<OpenApiDocument> {\n const connectorName = ctx.name;\n if (spec && typeof spec === 'object' && !Array.isArray(spec)) {\n return spec as OpenApiDocument;\n }\n if (typeof spec === 'string' && spec.length > 0) {\n if (/^https?:\\/\\//i.test(spec)) {\n const doFetch = fetchImpl ?? fetch;\n let res: Response;\n try {\n res = await doFetch(spec);\n } catch (err) {\n // The spec endpoint is unreachable — DNS / connection refused /\n // timeout / network error. Operational, not a config mistake: degrade\n // + retry rather than fail boot.\n throw new ConnectorUpstreamUnavailableError(\n `connector-openapi provider: connector '${connectorName}' could not reach spec URL '${spec}': ${(err as Error).message}`,\n { cause: err },\n );\n }\n if (!res.ok) {\n if (SPEC_FETCH_RETRYABLE_STATUS.has(res.status)) {\n // A transient server-side status (timeout / rate-limit / 5xx) — the\n // endpoint is momentarily unhealthy, so treat it as upstream-unavailable.\n throw new ConnectorUpstreamUnavailableError(\n `connector-openapi provider: connector '${connectorName}' got a transient HTTP ${res.status} fetching spec '${spec}'.`,\n );\n }\n // A non-retryable status (400 / 401 / 403 / 404 / 410 …) — the request\n // is wrong (bad URL, missing/insufficient auth for the spec endpoint):\n // a configuration fault that stays fatal.\n throw new Error(\n `connector-openapi provider: connector '${connectorName}' failed to fetch spec '${spec}' (HTTP ${res.status}).`,\n );\n }\n // A 2xx with an unparseable / non-object body is a content fault (the\n // endpoint served the wrong thing) — fatal, symmetric with the file-path\n // parse below.\n try {\n const parsed: unknown = await res.json();\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error('not a JSON object');\n }\n return parsed as OpenApiDocument;\n } catch (err) {\n throw new Error(\n `connector-openapi provider: connector '${connectorName}' fetched spec '${spec}' but it is not a parseable ` +\n `OpenAPI JSON document: ${(err as Error).message}`,\n );\n }\n }\n // File path — dereferenced through the host capability so resolution stays\n // anchored to (and confined within) the declaring stack/package root.\n if (!ctx.loadPackageFile) {\n throw new Error(\n `connector-openapi provider: connector '${connectorName}' providerConfig.spec '${spec}' is a file path, ` +\n `but this host provides no package file access — inline the OpenAPI document or use an http(s) URL.`,\n );\n }\n let text: string;\n try {\n text = await ctx.loadPackageFile(spec);\n } catch (err) {\n throw new Error(\n `connector-openapi provider: connector '${connectorName}' failed to read providerConfig.spec '${spec}': ` +\n `${(err as Error).message}`,\n );\n }\n try {\n const parsed: unknown = JSON.parse(text);\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error('not a JSON object');\n }\n return parsed as OpenApiDocument;\n } catch (err) {\n throw new Error(\n `connector-openapi provider: connector '${connectorName}' providerConfig.spec '${spec}' is not a parseable ` +\n `OpenAPI JSON document: ${(err as Error).message}`,\n );\n }\n }\n throw new Error(\n `connector-openapi provider: connector '${connectorName}' requires providerConfig.spec — an inline OpenAPI 3.x ` +\n `document object, an http(s) URL, or a package-relative file path.`,\n );\n}\n\n/**\n * Build the `openapi` {@link ConnectorProviderFactory} (ADR-0097 / ADR-0023). At\n * boot the automation service invokes it for each `provider: 'openapi'`\n * declarative instance: it loads the OpenAPI document from `providerConfig.spec`,\n * then produces the same `{ def, handlers }` bundle {@link createOpenApiConnector}\n * generates for a hand-wired OpenAPI connector — one action per operation over a\n * static-auth HTTP transport, with the resolved `auth` applied.\n *\n * Hard-fails on invalid config (missing spec, a wrong spec URL, an unparseable\n * document, a bad base URL), so a misconfigured instance fails boot loudly. A\n * remote spec URL that is merely unreachable / transiently failing instead\n * degrades the instance and retries (#3049) — see {@link loadOpenApiDocument}.\n */\nexport function createOpenApiProviderFactory(deps: OpenApiProviderDeps = {}): ConnectorProviderFactory {\n return async (ctx) => {\n const cfg = (ctx.providerConfig ?? {}) as OpenApiProviderConfig;\n if (cfg.baseUrl !== undefined && typeof cfg.baseUrl !== 'string') {\n throw new Error(\n `connector-openapi provider: connector '${ctx.name}' providerConfig.baseUrl must be a string when set.`,\n );\n }\n const document = await loadOpenApiDocument(cfg.spec, deps.fetchImpl, ctx);\n const auth = ctx.auth as ResolvedConnectorAuth | undefined as RestAuth | undefined;\n return createOpenApiConnector({\n name: ctx.name,\n label: ctx.label,\n description: ctx.description,\n document,\n baseUrl: typeof cfg.baseUrl === 'string' ? cfg.baseUrl : undefined,\n auth,\n fetchImpl: deps.fetchImpl,\n });\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { Connector, ConnectorProviderFactory } from '@objectstack/spec/integration';\nimport { createOpenApiConnector, type OpenApiConnectorConfig } from './openapi-connector.js';\nimport { createOpenApiProviderFactory, OPENAPI_PROVIDER_KEY } from './openapi-provider.js';\n\n/**\n * Minimal surface of the automation engine this package depends on — the\n * connector registry (ADR-0018 §Addendum) plus the provider registry (ADR-0097).\n * Kept structural so callers need no runtime dependency on\n * `@objectstack/service-automation` (mirrors connector-rest / connector-mcp).\n */\nexport interface ConnectorRegistrySurface {\n registerConnector(\n def: Connector,\n handlers: Record<\n string,\n (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>\n >,\n ): void;\n unregisterConnector(name: string): void;\n registerConnectorProvider(providerKey: string, factory: ConnectorProviderFactory): void;\n}\n\n/**\n * Generate an OpenAPI-backed connector and register it on the engine's connector\n * registry so the baseline `connector_action` node can dispatch to the generated\n * actions (ADR-0023). Returns the registered connector name.\n */\nexport function registerOpenApiConnector(registry: ConnectorRegistrySurface, config: OpenApiConnectorConfig): string {\n const { def, handlers } = createOpenApiConnector(config);\n registry.registerConnector(def, handlers);\n return def.name;\n}\n\n/**\n * Options for {@link ConnectorOpenApiPlugin}. All optional (ADR-0097): with no\n * `document` the plugin contributes only the `openapi` provider factory — so a\n * stack can declare `provider: 'openapi'` instances as pure metadata. Supply a\n * `document` (+ config) to ALSO register one hand-wired OpenAPI connector.\n */\nexport interface ConnectorOpenApiPluginOptions extends Partial<OpenApiConnectorConfig> {\n /** Injected fetch implementation forwarded to the `openapi` provider factory (tests). */\n fetchImpl?: typeof fetch;\n}\n\n/**\n * ConnectorOpenApiPlugin — contributes the OpenAPI generic executor (ADR-0023) in\n * two forms:\n *\n * 1. **Provider factory** (`openapi`, ADR-0097): registered at `init()` so the\n * automation service can materialize declarative `provider: 'openapi'`\n * `connectors:` entries — loading the OpenAPI document and mapping each\n * operation to a connector action — at boot.\n * 2. **Hand-wired instance** (optional, back-compat): when constructed with a\n * `document`, it also registers one concrete OpenAPI connector at `start()`.\n *\n * If no automation engine is present the plugin logs and skips.\n */\nexport class ConnectorOpenApiPlugin implements Plugin {\n name = 'com.objectstack.connector.openapi';\n version = '1.0.0';\n type = 'standard' as const;\n // Ensure the automation engine (and its connector/provider registries) exist first.\n dependencies = ['com.objectstack.service-automation'];\n\n private readonly options: ConnectorOpenApiPluginOptions;\n private connectorName?: string;\n private automation?: ConnectorRegistrySurface;\n\n constructor(options: ConnectorOpenApiPluginOptions = {}) {\n this.options = options;\n }\n\n async init(ctx: PluginContext): Promise<void> {\n // Contribute the `openapi` provider factory (ADR-0097) before the\n // automation service materializes declarative instances during its start().\n const automation = this.tryGetAutomation(ctx);\n if (automation && typeof automation.registerConnectorProvider === 'function') {\n automation.registerConnectorProvider(\n OPENAPI_PROVIDER_KEY,\n createOpenApiProviderFactory({ fetchImpl: this.options.fetchImpl }),\n );\n ctx.logger.info(\"ConnectorOpenApiPlugin: registered 'openapi' connector provider\");\n }\n }\n\n async start(ctx: PluginContext): Promise<void> {\n // Provider-only usage (no document) contributes just the factory in init().\n if (!this.options.document) return;\n\n const automation = this.tryGetAutomation(ctx);\n if (!automation || typeof automation.registerConnector !== 'function') {\n ctx.logger.info('ConnectorOpenApiPlugin: no automation engine — OpenAPI connector not registered');\n return;\n }\n\n this.connectorName = registerOpenApiConnector(automation, this.options as OpenApiConnectorConfig);\n this.automation = automation;\n ctx.logger.info(`ConnectorOpenApiPlugin: OpenAPI connector '${this.connectorName}' registered`);\n }\n\n async stop(_ctx: PluginContext): Promise<void> {\n if (this.automation && this.connectorName) {\n try { this.automation.unregisterConnector(this.connectorName); } catch { /* ignore */ }\n }\n }\n\n private tryGetAutomation(ctx: PluginContext): ConnectorRegistrySurface | undefined {\n try {\n return ctx.getService<ConnectorRegistrySurface>('automation');\n } catch {\n return undefined;\n }\n }\n}\n"],"mappings":"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","names":["schema"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/connector-openapi",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "16.0.0-rc.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "OpenAPI 3.x connector generator for ObjectStack — turns a declarative OpenAPI document into connector actions on the automation engine's registry, with a self-contained static-auth HTTP transport (ADR-0023).",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@objectstack/core": "
|
|
17
|
-
"@objectstack/spec": "
|
|
16
|
+
"@objectstack/core": "16.0.0-rc.0",
|
|
17
|
+
"@objectstack/spec": "16.0.0-rc.0"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/node": "^26.1.1",
|
|
21
21
|
"typescript": "^6.0.3",
|
|
22
22
|
"vitest": "^4.1.10",
|
|
23
|
-
"@objectstack/service-automation": "
|
|
23
|
+
"@objectstack/service-automation": "16.0.0-rc.0"
|
|
24
24
|
},
|
|
25
25
|
"keywords": [
|
|
26
26
|
"objectstack",
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { describe, it, expect } from 'vitest';
|
|
7
7
|
import type { ConnectorProviderContext } from '@objectstack/spec/integration';
|
|
8
|
+
import { isConnectorUpstreamUnavailable } from '@objectstack/spec/integration';
|
|
8
9
|
import { createOpenApiProviderFactory, OPENAPI_PROVIDER_KEY } from './openapi-provider.js';
|
|
9
10
|
|
|
10
11
|
const petstore = {
|
|
@@ -96,3 +97,73 @@ describe('openapi provider factory (ADR-0097)', () => {
|
|
|
96
97
|
).rejects.toThrow(/no package file access/);
|
|
97
98
|
});
|
|
98
99
|
});
|
|
100
|
+
|
|
101
|
+
// ── #3049 follow-up: remote-URL fetch fault classification ──────────────────
|
|
102
|
+
//
|
|
103
|
+
// A remote spec URL that is unreachable / transiently failing is an OPERATIONAL
|
|
104
|
+
// fault: the factory throws the CONNECTOR_UPSTREAM_UNAVAILABLE marker so the
|
|
105
|
+
// materializer degrades + retries the instance instead of failing boot
|
|
106
|
+
// (symmetric with connector-mcp's connect path). A wrong URL (non-retryable
|
|
107
|
+
// 4xx) or an unparseable document stays a plain, fatal configuration fault.
|
|
108
|
+
|
|
109
|
+
describe('openapi provider — remote spec fetch fault classification (#3049)', () => {
|
|
110
|
+
const url = 'https://petstore.example.com/openapi.json';
|
|
111
|
+
const cfg = { providerConfig: { spec: url } };
|
|
112
|
+
|
|
113
|
+
it('classifies a network failure (fetch rejects) as upstream-unavailable, keeping the cause', async () => {
|
|
114
|
+
const boom = new Error('connect ECONNREFUSED 93.184.216.34:443');
|
|
115
|
+
const fetchImpl = (async () => { throw boom; }) as unknown as typeof fetch;
|
|
116
|
+
const factory = createOpenApiProviderFactory({ fetchImpl });
|
|
117
|
+
|
|
118
|
+
const err = await factory(ctx(cfg)).then(
|
|
119
|
+
() => { throw new Error('expected rejection'); },
|
|
120
|
+
(e: unknown) => e,
|
|
121
|
+
);
|
|
122
|
+
expect(isConnectorUpstreamUnavailable(err)).toBe(true);
|
|
123
|
+
expect((err as Error).message).toMatch(/'pets' could not reach spec URL/);
|
|
124
|
+
expect((err as Error).message).toContain('ECONNREFUSED');
|
|
125
|
+
expect((err as { cause?: unknown }).cause).toBe(boom);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it.each([408, 429, 500, 502, 503, 504])(
|
|
129
|
+
'classifies a transient HTTP %i as upstream-unavailable (retryable)',
|
|
130
|
+
async (status) => {
|
|
131
|
+
const fetchImpl = (async () => ({ ok: false, status, json: async () => ({}) }) as unknown as Response) as unknown as typeof fetch;
|
|
132
|
+
const factory = createOpenApiProviderFactory({ fetchImpl });
|
|
133
|
+
const err = await factory(ctx(cfg)).then(() => { throw new Error('expected rejection'); }, (e: unknown) => e);
|
|
134
|
+
expect(isConnectorUpstreamUnavailable(err), `HTTP ${status} should degrade`).toBe(true);
|
|
135
|
+
expect((err as Error).message).toContain(String(status));
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
it.each([400, 401, 403, 404, 410])(
|
|
140
|
+
'keeps a non-retryable HTTP %i a plain (fatal) configuration fault',
|
|
141
|
+
async (status) => {
|
|
142
|
+
const fetchImpl = (async () => ({ ok: false, status, json: async () => ({}) }) as unknown as Response) as unknown as typeof fetch;
|
|
143
|
+
const factory = createOpenApiProviderFactory({ fetchImpl });
|
|
144
|
+
const err = await factory(ctx(cfg)).then(() => { throw new Error('expected rejection'); }, (e: unknown) => e);
|
|
145
|
+
expect(isConnectorUpstreamUnavailable(err), `HTTP ${status} should stay fatal`).toBe(false);
|
|
146
|
+
expect((err as Error).message).toMatch(/failed to fetch spec/);
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
it('keeps a 2xx-but-unparseable body a plain (fatal) content fault, not upstream-unavailable', async () => {
|
|
151
|
+
const fetchImpl = (async () => ({
|
|
152
|
+
ok: true,
|
|
153
|
+
status: 200,
|
|
154
|
+
json: async () => { throw new SyntaxError('Unexpected token < in JSON'); },
|
|
155
|
+
}) as unknown as Response) as unknown as typeof fetch;
|
|
156
|
+
const factory = createOpenApiProviderFactory({ fetchImpl });
|
|
157
|
+
const err = await factory(ctx(cfg)).then(() => { throw new Error('expected rejection'); }, (e: unknown) => e);
|
|
158
|
+
expect(isConnectorUpstreamUnavailable(err)).toBe(false);
|
|
159
|
+
expect((err as Error).message).toMatch(/not a parseable.*OpenAPI JSON document/s);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('keeps a 2xx non-object body (array) a plain (fatal) content fault', async () => {
|
|
163
|
+
const fetchImpl = (async () => ({ ok: true, status: 200, json: async () => [1, 2] }) as unknown as Response) as unknown as typeof fetch;
|
|
164
|
+
const factory = createOpenApiProviderFactory({ fetchImpl });
|
|
165
|
+
const err = await factory(ctx(cfg)).then(() => { throw new Error('expected rejection'); }, (e: unknown) => e);
|
|
166
|
+
expect(isConnectorUpstreamUnavailable(err)).toBe(false);
|
|
167
|
+
expect((err as Error).message).toMatch(/not a parseable.*OpenAPI JSON document/s);
|
|
168
|
+
});
|
|
169
|
+
});
|
package/src/openapi-provider.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
ConnectorProviderFactory,
|
|
6
6
|
ResolvedConnectorAuth,
|
|
7
7
|
} from '@objectstack/spec/integration';
|
|
8
|
+
import { ConnectorUpstreamUnavailableError } from '@objectstack/spec/integration';
|
|
8
9
|
import {
|
|
9
10
|
createOpenApiConnector,
|
|
10
11
|
type OpenApiDocument,
|
|
@@ -17,6 +18,17 @@ import {
|
|
|
17
18
|
*/
|
|
18
19
|
export const OPENAPI_PROVIDER_KEY = 'openapi';
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* HTTP statuses treated as **transient** when fetching a remote spec at boot
|
|
23
|
+
* (#3049 follow-up): a timeout / rate-limit / 5xx means the spec endpoint is
|
|
24
|
+
* momentarily unhealthy, not that the connector is misconfigured — so the
|
|
25
|
+
* instance degrades and retries rather than aborting boot. Mirrors the
|
|
26
|
+
* connector request-retry convention (`retryableStatusCodes` default in
|
|
27
|
+
* `ConnectorSchema`). Any OTHER non-2xx (400 / 401 / 403 / 404 / 410 …) means
|
|
28
|
+
* the request itself is wrong — a configuration fault that stays fatal.
|
|
29
|
+
*/
|
|
30
|
+
const SPEC_FETCH_RETRYABLE_STATUS = new Set([408, 429, 500, 502, 503, 504]);
|
|
31
|
+
|
|
20
32
|
/** Injectable dependencies for {@link createOpenApiProviderFactory} (tests). */
|
|
21
33
|
export interface OpenApiProviderDeps {
|
|
22
34
|
/** Injected fetch implementation (spec fetch + request transport); defaults to global `fetch`. */
|
|
@@ -29,6 +41,10 @@ interface OpenApiProviderConfig {
|
|
|
29
41
|
* The OpenAPI 3.x document: an inline object, an http(s) URL to fetch at
|
|
30
42
|
* boot, or a file path resolved relative to the declaring stack/package root
|
|
31
43
|
* (`'./billing-openapi.json'`, #3016).
|
|
44
|
+
*
|
|
45
|
+
* A remote URL that is unreachable / transiently failing (network error,
|
|
46
|
+
* 408 / 429 / 5xx) degrades the instance and retries (#3049); a wrong URL
|
|
47
|
+
* (non-retryable 4xx) or an unparseable document stays a fatal config fault.
|
|
32
48
|
*/
|
|
33
49
|
spec?: unknown;
|
|
34
50
|
/** Override the base URL (else the document's `servers[0].url`). */
|
|
@@ -41,9 +57,14 @@ interface OpenApiProviderConfig {
|
|
|
41
57
|
* form used by the showcase), an http(s) URL fetched at materialization, or a
|
|
42
58
|
* **file path** read through the host's `ctx.loadPackageFile` — which resolves
|
|
43
59
|
* it relative to the declaring stack/package root and confines the read to
|
|
44
|
-
* that root (absolute / `..`-escaping paths are rejected there).
|
|
45
|
-
*
|
|
46
|
-
*
|
|
60
|
+
* that root (absolute / `..`-escaping paths are rejected there).
|
|
61
|
+
*
|
|
62
|
+
* Fault classification (#3049 seam, symmetric with connector-mcp's connect
|
|
63
|
+
* path): a remote spec URL that is unreachable or transiently failing throws
|
|
64
|
+
* {@link ConnectorUpstreamUnavailableError} — the materializer degrades the
|
|
65
|
+
* instance and retries. Every OTHER failure (missing spec, a wrong URL,
|
|
66
|
+
* an unparseable document, no host file access) throws a plain error, which
|
|
67
|
+
* stays fatal at boot / a skipped entry on reload.
|
|
47
68
|
*/
|
|
48
69
|
async function loadOpenApiDocument(
|
|
49
70
|
spec: unknown,
|
|
@@ -57,13 +78,48 @@ async function loadOpenApiDocument(
|
|
|
57
78
|
if (typeof spec === 'string' && spec.length > 0) {
|
|
58
79
|
if (/^https?:\/\//i.test(spec)) {
|
|
59
80
|
const doFetch = fetchImpl ?? fetch;
|
|
60
|
-
|
|
81
|
+
let res: Response;
|
|
82
|
+
try {
|
|
83
|
+
res = await doFetch(spec);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
// The spec endpoint is unreachable — DNS / connection refused /
|
|
86
|
+
// timeout / network error. Operational, not a config mistake: degrade
|
|
87
|
+
// + retry rather than fail boot.
|
|
88
|
+
throw new ConnectorUpstreamUnavailableError(
|
|
89
|
+
`connector-openapi provider: connector '${connectorName}' could not reach spec URL '${spec}': ${(err as Error).message}`,
|
|
90
|
+
{ cause: err },
|
|
91
|
+
);
|
|
92
|
+
}
|
|
61
93
|
if (!res.ok) {
|
|
94
|
+
if (SPEC_FETCH_RETRYABLE_STATUS.has(res.status)) {
|
|
95
|
+
// A transient server-side status (timeout / rate-limit / 5xx) — the
|
|
96
|
+
// endpoint is momentarily unhealthy, so treat it as upstream-unavailable.
|
|
97
|
+
throw new ConnectorUpstreamUnavailableError(
|
|
98
|
+
`connector-openapi provider: connector '${connectorName}' got a transient HTTP ${res.status} fetching spec '${spec}'.`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
// A non-retryable status (400 / 401 / 403 / 404 / 410 …) — the request
|
|
102
|
+
// is wrong (bad URL, missing/insufficient auth for the spec endpoint):
|
|
103
|
+
// a configuration fault that stays fatal.
|
|
62
104
|
throw new Error(
|
|
63
105
|
`connector-openapi provider: connector '${connectorName}' failed to fetch spec '${spec}' (HTTP ${res.status}).`,
|
|
64
106
|
);
|
|
65
107
|
}
|
|
66
|
-
|
|
108
|
+
// A 2xx with an unparseable / non-object body is a content fault (the
|
|
109
|
+
// endpoint served the wrong thing) — fatal, symmetric with the file-path
|
|
110
|
+
// parse below.
|
|
111
|
+
try {
|
|
112
|
+
const parsed: unknown = await res.json();
|
|
113
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
114
|
+
throw new Error('not a JSON object');
|
|
115
|
+
}
|
|
116
|
+
return parsed as OpenApiDocument;
|
|
117
|
+
} catch (err) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`connector-openapi provider: connector '${connectorName}' fetched spec '${spec}' but it is not a parseable ` +
|
|
120
|
+
`OpenAPI JSON document: ${(err as Error).message}`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
67
123
|
}
|
|
68
124
|
// File path — dereferenced through the host capability so resolution stays
|
|
69
125
|
// anchored to (and confined within) the declaring stack/package root.
|
|
@@ -109,8 +165,10 @@ async function loadOpenApiDocument(
|
|
|
109
165
|
* generates for a hand-wired OpenAPI connector — one action per operation over a
|
|
110
166
|
* static-auth HTTP transport, with the resolved `auth` applied.
|
|
111
167
|
*
|
|
112
|
-
* Hard-fails on invalid config (missing
|
|
113
|
-
* misconfigured instance fails boot loudly.
|
|
168
|
+
* Hard-fails on invalid config (missing spec, a wrong spec URL, an unparseable
|
|
169
|
+
* document, a bad base URL), so a misconfigured instance fails boot loudly. A
|
|
170
|
+
* remote spec URL that is merely unreachable / transiently failing instead
|
|
171
|
+
* degrades the instance and retries (#3049) — see {@link loadOpenApiDocument}.
|
|
114
172
|
*/
|
|
115
173
|
export function createOpenApiProviderFactory(deps: OpenApiProviderDeps = {}): ConnectorProviderFactory {
|
|
116
174
|
return async (ctx) => {
|