@objectstack/connector-openapi 14.8.0 → 15.1.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/dist/index.mjs CHANGED
@@ -211,14 +211,132 @@ function titleize(name) {
211
211
  return name.split("_").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
212
212
  }
213
213
 
214
+ // src/openapi-provider.ts
215
+ var OPENAPI_PROVIDER_KEY = "openapi";
216
+ async function loadOpenApiDocument(spec, fetchImpl, ctx) {
217
+ const connectorName = ctx.name;
218
+ if (spec && typeof spec === "object" && !Array.isArray(spec)) {
219
+ return spec;
220
+ }
221
+ if (typeof spec === "string" && spec.length > 0) {
222
+ if (/^https?:\/\//i.test(spec)) {
223
+ const doFetch = fetchImpl ?? fetch;
224
+ const res = await doFetch(spec);
225
+ if (!res.ok) {
226
+ throw new Error(
227
+ `connector-openapi provider: connector '${connectorName}' failed to fetch spec '${spec}' (HTTP ${res.status}).`
228
+ );
229
+ }
230
+ return await res.json();
231
+ }
232
+ if (!ctx.loadPackageFile) {
233
+ throw new Error(
234
+ `connector-openapi provider: connector '${connectorName}' providerConfig.spec '${spec}' is a file path, but this host provides no package file access \u2014 inline the OpenAPI document or use an http(s) URL.`
235
+ );
236
+ }
237
+ let text;
238
+ try {
239
+ text = await ctx.loadPackageFile(spec);
240
+ } catch (err) {
241
+ throw new Error(
242
+ `connector-openapi provider: connector '${connectorName}' failed to read providerConfig.spec '${spec}': ${err.message}`
243
+ );
244
+ }
245
+ try {
246
+ const parsed = JSON.parse(text);
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}' providerConfig.spec '${spec}' is not a parseable OpenAPI JSON document: ${err.message}`
254
+ );
255
+ }
256
+ }
257
+ throw new Error(
258
+ `connector-openapi provider: connector '${connectorName}' requires providerConfig.spec \u2014 an inline OpenAPI 3.x document object, an http(s) URL, or a package-relative file path.`
259
+ );
260
+ }
261
+ function createOpenApiProviderFactory(deps = {}) {
262
+ return async (ctx) => {
263
+ const cfg = ctx.providerConfig ?? {};
264
+ if (cfg.baseUrl !== void 0 && typeof cfg.baseUrl !== "string") {
265
+ throw new Error(
266
+ `connector-openapi provider: connector '${ctx.name}' providerConfig.baseUrl must be a string when set.`
267
+ );
268
+ }
269
+ const document = await loadOpenApiDocument(cfg.spec, deps.fetchImpl, ctx);
270
+ const auth = ctx.auth;
271
+ return createOpenApiConnector({
272
+ name: ctx.name,
273
+ label: ctx.label,
274
+ description: ctx.description,
275
+ document,
276
+ baseUrl: typeof cfg.baseUrl === "string" ? cfg.baseUrl : void 0,
277
+ auth,
278
+ fetchImpl: deps.fetchImpl
279
+ });
280
+ };
281
+ }
282
+
214
283
  // src/connector-openapi-plugin.ts
215
284
  function registerOpenApiConnector(registry, config) {
216
285
  const { def, handlers } = createOpenApiConnector(config);
217
286
  registry.registerConnector(def, handlers);
218
287
  return def.name;
219
288
  }
289
+ var ConnectorOpenApiPlugin = class {
290
+ constructor(options = {}) {
291
+ this.name = "com.objectstack.connector.openapi";
292
+ this.version = "1.0.0";
293
+ this.type = "standard";
294
+ // Ensure the automation engine (and its connector/provider registries) exist first.
295
+ this.dependencies = ["com.objectstack.service-automation"];
296
+ this.options = options;
297
+ }
298
+ async init(ctx) {
299
+ const automation = this.tryGetAutomation(ctx);
300
+ if (automation && typeof automation.registerConnectorProvider === "function") {
301
+ automation.registerConnectorProvider(
302
+ OPENAPI_PROVIDER_KEY,
303
+ createOpenApiProviderFactory({ fetchImpl: this.options.fetchImpl })
304
+ );
305
+ ctx.logger.info("ConnectorOpenApiPlugin: registered 'openapi' connector provider");
306
+ }
307
+ }
308
+ async start(ctx) {
309
+ if (!this.options.document) return;
310
+ const automation = this.tryGetAutomation(ctx);
311
+ if (!automation || typeof automation.registerConnector !== "function") {
312
+ ctx.logger.info("ConnectorOpenApiPlugin: no automation engine \u2014 OpenAPI connector not registered");
313
+ return;
314
+ }
315
+ this.connectorName = registerOpenApiConnector(automation, this.options);
316
+ this.automation = automation;
317
+ ctx.logger.info(`ConnectorOpenApiPlugin: OpenAPI connector '${this.connectorName}' registered`);
318
+ }
319
+ async stop(_ctx) {
320
+ if (this.automation && this.connectorName) {
321
+ try {
322
+ this.automation.unregisterConnector(this.connectorName);
323
+ } catch {
324
+ }
325
+ }
326
+ }
327
+ tryGetAutomation(ctx) {
328
+ try {
329
+ return ctx.getService("automation");
330
+ } catch {
331
+ return void 0;
332
+ }
333
+ }
334
+ };
220
335
  export {
336
+ ConnectorOpenApiPlugin,
337
+ OPENAPI_PROVIDER_KEY,
221
338
  createOpenApiConnector,
339
+ createOpenApiProviderFactory,
222
340
  registerOpenApiConnector
223
341
  };
224
342
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/openapi-connector.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 { Connector } from '@objectstack/spec/integration';\nimport { createOpenApiConnector, type OpenApiConnectorConfig } from './openapi-connector.js';\n\n/**\n * Minimal surface of the automation engine this helper depends on — the\n * connector registry from ADR-0018 §Addendum. Kept structural so callers need\n * no runtime dependency on `@objectstack/service-automation` (mirrors\n * 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}\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"],"mappings":";AA+HA,IAAM,eAAe,CAAC,OAAO,OAAO,QAAQ,UAAU,SAAS,WAAW,QAAQ,OAAO;AAiBlF,SAAS,uBAAuB,QAAwD;AAC3F,QAAM,EAAE,UAAU,QAAQ,IAAI;AAC9B,QAAM,OAAiB,OAAO,QAAQ,EAAE,MAAM,OAAO;AACrD,QAAM,UAAU,OAAO,aAAa;AACpC,QAAM,OAAO,OAAO,QAAQ,KAAK,SAAS,MAAM,SAAS,mBAAmB;AAC5E,QAAM,QAAQ,OAAO,SAAS,SAAS,MAAM,SAAS,SAAS,IAAI;AACnE,QAAM,cAAc,OAAO,eAAe,SAAS,MAAM;AACzD,QAAM,UAAU,OAAO,WAAW,SAAS,UAAU,CAAC,GAAG;AACzD,MAAI,CAAC,SAAS;AACV,UAAM,IAAI,MAAM,8FAAyF;AAAA,EAC7G;AAGA,iBAAe,QAAQ,OAAuD;AAC1E,UAAM,SAAS,MAAM,OAAO,YAAY;AACxC,UAAM,UAAkC,EAAE,GAAG,OAAO,gBAAgB,GAAG,MAAM,QAAQ;AACrF,UAAM,QAAgC,EAAE,GAAG,MAAM,MAAM;AACvD,cAAU,MAAM,SAAS,KAAK;AAE9B,UAAM,MAAM,SAAS,SAAmB,MAAM,MAAM,KAAK;AACzD,UAAM,UAAU,MAAM,SAAS,UAAa,WAAW,SAAS,WAAW;AAC3E,QAAI,WAAW,QAAQ,cAAc,MAAM,UAAa,QAAQ,cAAc,MAAM,QAAW;AAC3F,cAAQ,cAAc,IAAI;AAAA,IAC9B;AAEA,UAAM,WAAW,MAAM,QAAQ,KAAK;AAAA,MAChC;AAAA,MACA;AAAA,MACA,MAAM,UAAU,KAAK,UAAU,MAAM,IAAI,IAAI;AAAA,IACjD,CAAC;AAED,UAAM,cAAc,SAAS,QAAQ,IAAI,cAAc,KAAK;AAC5D,UAAM,SAAS,YAAY,SAAS,kBAAkB,IAAI,MAAM,SAAS,KAAK,IAAI,MAAM,SAAS,KAAK;AACtG,WAAO,EAAE,QAAQ,SAAS,QAAQ,IAAI,SAAS,IAAI,MAAM,OAAO;AAAA,EACpE;AAEA,QAAM,UAA6B,CAAC;AACpC,QAAM,WAA6C,CAAC;AACpD,QAAM,WAAW,oBAAI,IAAY;AAEjC,aAAW,MAAM,kBAAkB,QAAQ,GAAG;AAC1C,QAAI,WAAW,CAAC,QAAQ,OAAO,EAAE,CAAC,EAAG;AACrC,UAAM,MAAM,UAAU,GAAG,eAAe,KAAK,GAAG,GAAG,MAAM,IAAI,GAAG,IAAI,EAAE,GAAG,QAAQ;AAEjF,YAAQ,KAAK;AAAA,MACT;AAAA,MACA,OAAO,GAAG,WAAW,SAAS,GAAG;AAAA,MACjC,aAAa,GAAG;AAAA,MAChB,aAAa,iBAAiB,EAAE;AAAA,MAChC,cAAc,kBAAkB,EAAE;AAAA,IACtC,CAAC;AAED,aAAS,GAAG,IAAI,OAAO,UAAmC;AACtD,YAAM,MAAM;AACZ,aAAO,QAAQ;AAAA,QACX,QAAQ,GAAG;AAAA,QACX,MAAM,gBAAgB,GAAG,MAAM,SAAS,IAAI,IAAI,CAAC;AAAA,QACjD,OAAO,gBAAgB,SAAS,IAAI,KAAK,CAAC;AAAA,QAC1C,SAAS,gBAAgB,SAAS,IAAI,MAAM,CAAC;AAAA,QAC7C,MAAM,IAAI;AAAA,MACd,CAAC;AAAA,IACL;AAAA,EACJ;AAEA,QAAM,MAAiB;AAAA,IACnB;AAAA,IACA;AAAA,IACA,MAAM;AAAA,IACN;AAAA,IACA,MAAM,OAAO,QAAQ;AAAA,IACrB,gBAAgB;AAAA;AAAA;AAAA,IAGhB,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,qBAAqB;AAAA,IACrB,kBAAkB;AAAA,IAClB;AAAA,EACJ;AAEA,SAAO,EAAE,KAAK,SAAS;AAC3B;AAQA,SAAS,kBAAkB,KAA4B;AACnD,QAAM,MAAY,CAAC;AACnB,aAAW,CAAC,MAAM,IAAI,KAAK,OAAO,QAAQ,IAAI,SAAS,CAAC,CAAC,GAAG;AACxD,QAAI,CAAC,QAAQ,OAAO,SAAS,SAAU;AACvC,UAAM,SAAS;AACf,eAAW,UAAU,cAAc;AAC/B,YAAM,YAAY,OAAO,MAAM;AAC/B,UAAI,CAAC,aAAa,OAAO,cAAc,SAAU;AACjD,UAAI,KAAK,EAAE,GAAG,WAAW,QAAQ,KAAK,CAAC;AAAA,IAC3C;AAAA,EACJ;AACA,SAAO;AACX;AAEA,SAAS,OAAO,IAAuB;AACnC,SAAO;AAAA,IACH,aAAa,GAAG;AAAA,IAChB,QAAQ,GAAG;AAAA,IACX,MAAM,GAAG;AAAA,IACT,MAAM,GAAG;AAAA,IACT,SAAS,GAAG;AAAA,IACZ,aAAa,GAAG;AAAA,EACpB;AACJ;AAOA,SAAS,iBAAiB,IAA8C;AACpE,QAAM,WAA2G;AAAA,IAC7G,MAAM,EAAE,OAAO,CAAC,GAAG,UAAU,CAAC,EAAE;AAAA,IAChC,OAAO,EAAE,OAAO,CAAC,GAAG,UAAU,CAAC,EAAE;AAAA,IACjC,QAAQ,EAAE,OAAO,CAAC,GAAG,UAAU,CAAC,EAAE;AAAA,EACtC;AAEA,aAAW,KAAK,GAAG,cAAc,CAAC,GAAG;AACjC,QAAI,CAAC,KAAK,OAAO,MAAM,YAAY,UAAU,EAAG;AAChD,QAAI,EAAE,OAAO,UAAU,EAAE,OAAO,WAAW,EAAE,OAAO,SAAU;AAC9D,UAAM,MAAM,SAAS,EAAE,EAAE;AACzB,QAAI,MAAM,EAAE,IAAI,IAAI,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,UAAU,aAAa,EAAE,YAAY,IAAI,EAAE,MAAM,SAAS;AACnH,QAAI,EAAE,SAAU,KAAI,SAAS,KAAK,EAAE,IAAI;AAAA,EAC5C;AAEA,QAAM,aAAyC,CAAC;AAChD,QAAM,WAAqB,CAAC;AAC5B,aAAW,SAAS,CAAC,QAAQ,SAAS,QAAQ,GAAY;AACtD,UAAM,MAAM,SAAS,KAAK;AAC1B,QAAI,OAAO,KAAK,IAAI,KAAK,EAAE,WAAW,EAAG;AACzC,UAAMA,UAAqB,EAAE,MAAM,UAAU,YAAY,IAAI,MAAM;AACnE,QAAI,IAAI,SAAS,OAAQ,CAAAA,QAAO,WAAW,IAAI;AAC/C,eAAW,KAAK,IAAIA;AAEpB,QAAI,UAAU,UAAU,IAAI,SAAS,OAAQ,UAAS,KAAK,KAAK;AAAA,EACpE;AAEA,QAAM,aAAa,yBAAyB,GAAG,WAAW;AAC1D,MAAI,YAAY;AACZ,eAAW,OAAO;AAClB,QAAI,GAAG,eAAe,EAAE,UAAU,GAAG,gBAAgB,GAAG,YAAY,SAAU,UAAS,KAAK,MAAM;AAAA,EACtG;AAEA,MAAI,OAAO,KAAK,UAAU,EAAE,WAAW,EAAG,QAAO;AACjD,QAAM,SAAqB,EAAE,MAAM,UAAU,WAAW;AACxD,MAAI,SAAS,OAAQ,QAAO,WAAW;AACvC,SAAO;AACX;AAGA,SAAS,kBAAkB,IAA8C;AACrE,QAAM,YAAY,GAAG;AACrB,MAAI,CAAC,UAAW,QAAO;AACvB,MAAI;AACJ,MAAI,UAAU,KAAK,EAAG,QAAO;AAAA,MACxB,QAAO,OAAO,KAAK,SAAS,EAAE,KAAK,CAAC,MAAM,UAAU,KAAK,CAAC,CAAC;AAChE,MAAI,CAAC,QAAQ,UAAU,SAAS,EAAG,QAAO;AAC1C,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,OAAO,UAAU,IAAI;AAC3B,MAAI,CAAC,QAAQ,OAAO,SAAS,YAAY,UAAU,KAAM,QAAO;AAChE,SAAO,eAAe,KAAK,OAAO;AACtC;AAGA,SAAS,yBAAyB,IAA4D;AAC1F,MAAI,CAAC,MAAM,OAAO,OAAO,YAAY,UAAU,GAAI,QAAO;AAC1D,SAAO,eAAe,GAAG,OAAO;AACpC;AAGA,SAAS,eAAe,SAAsF;AAC1G,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,SAAS,QAAQ,kBAAkB,KAAK,OAAO,OAAO,OAAO,EAAE,CAAC;AACtE,SAAO,QAAQ;AACnB;AAGA,SAAS,SAAS,SAAiB,MAAc,OAAuC;AACpF,QAAM,OAAO,QAAQ,QAAQ,QAAQ,EAAE;AACvC,QAAM,SAAS,OAAQ,KAAK,WAAW,GAAG,IAAI,OAAO,IAAI,IAAI,KAAM;AACnE,QAAM,MAAM,IAAI,IAAI,OAAO,MAAM;AACjC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAC9C,QAAI,UAAU,UAAa,UAAU,KAAM,KAAI,aAAa,IAAI,KAAK,OAAO,KAAK,CAAC;AAAA,EACtF;AACA,SAAO,IAAI,SAAS;AACxB;AAGA,SAAS,UAAU,MAAgB,SAAiC,OAAqC;AACrG,UAAQ,KAAK,MAAM;AAAA,IACf,KAAK;AACD;AAAA,IACJ,KAAK;AACD,cAAQ,eAAe,IAAI,UAAU,KAAK,KAAK;AAC/C;AAAA,IACJ,KAAK,SAAS;AACV,YAAM,UAAU,OAAO,KAAK,GAAG,KAAK,QAAQ,IAAI,KAAK,QAAQ,EAAE,EAAE,SAAS,QAAQ;AAClF,cAAQ,eAAe,IAAI,SAAS,OAAO;AAC3C;AAAA,IACJ;AAAA,IACA,KAAK;AACD,UAAI,KAAK,UAAW,OAAM,KAAK,SAAS,IAAI,KAAK;AAAA,UAC5C,SAAQ,KAAK,cAAc,WAAW,IAAI,KAAK;AACpD;AAAA,EACR;AACJ;AAGA,SAAS,gBAAgB,UAAkB,YAA6C;AACpF,SAAO,SAAS,QAAQ,gBAAgB,CAAC,QAAQ,QAAgB;AAC7D,UAAM,QAAQ,WAAW,GAAG;AAC5B,WAAO,UAAU,UAAa,UAAU,OAAO,IAAI,GAAG,MAAM,mBAAmB,OAAO,KAAK,CAAC;AAAA,EAChG,CAAC;AACL;AAGA,SAAS,gBAAgB,KAAsD;AAC3E,QAAM,MAA8B,CAAC;AACrC,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,GAAG,GAAG;AACtC,QAAI,MAAM,UAAa,MAAM,KAAM;AACnC,QAAI,CAAC,IAAI,OAAO,CAAC;AAAA,EACrB;AACA,SAAO;AACX;AAGA,SAAS,SAAS,GAAqC;AACnD,SAAO,KAAK,OAAO,MAAM,YAAY,CAAC,MAAM,QAAQ,CAAC,IAAK,IAAgC,CAAC;AAC/F;AAGA,SAAS,UAAU,MAAc,MAA2B;AACxD,MAAI,YAAY;AAChB,MAAI,KAAK,IAAI,SAAS,GAAG;AACrB,QAAI,IAAI;AACR,WAAO,KAAK,IAAI,GAAG,IAAI,IAAI,CAAC,EAAE,EAAG;AACjC,gBAAY,GAAG,IAAI,IAAI,CAAC;AAAA,EAC5B;AACA,OAAK,IAAI,SAAS;AAClB,SAAO;AACX;AAGA,SAAS,KAAK,GAAmB;AAC7B,QAAM,MAAM,EACP,UAAU,MAAM,EAChB,QAAQ,kBAAkB,GAAG,EAC7B,QAAQ,YAAY,EAAE,EACtB,YAAY;AACjB,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,UAAU,KAAK,GAAG,IAAI,MAAM,MAAM,GAAG;AAChD;AAGA,SAAS,SAAS,MAAsB;AACpC,SAAO,KACF,MAAM,GAAG,EACT,OAAO,OAAO,EACd,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,IAAI,EAAE,MAAM,CAAC,CAAC,EACjD,KAAK,GAAG;AACjB;;;AClYO,SAAS,yBAAyB,UAAoC,QAAwC;AACjH,QAAM,EAAE,KAAK,SAAS,IAAI,uBAAuB,MAAM;AACvD,WAAS,kBAAkB,KAAK,QAAQ;AACxC,SAAO,IAAI;AACf;","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 {\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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/connector-openapi",
3
- "version": "14.8.0",
3
+ "version": "15.1.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": "14.8.0",
17
- "@objectstack/spec": "14.8.0"
16
+ "@objectstack/core": "15.1.0",
17
+ "@objectstack/spec": "15.1.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": "14.8.0"
23
+ "@objectstack/service-automation": "15.1.0"
24
24
  },
25
25
  "keywords": [
26
26
  "objectstack",
@@ -1,13 +1,15 @@
1
1
  // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
2
 
3
- import type { Connector } from '@objectstack/spec/integration';
3
+ import type { Plugin, PluginContext } from '@objectstack/core';
4
+ import type { Connector, ConnectorProviderFactory } from '@objectstack/spec/integration';
4
5
  import { createOpenApiConnector, type OpenApiConnectorConfig } from './openapi-connector.js';
6
+ import { createOpenApiProviderFactory, OPENAPI_PROVIDER_KEY } from './openapi-provider.js';
5
7
 
6
8
  /**
7
- * Minimal surface of the automation engine this helper depends on — the
8
- * connector registry from ADR-0018 §Addendum. Kept structural so callers need
9
- * no runtime dependency on `@objectstack/service-automation` (mirrors
10
- * connector-rest / connector-mcp).
9
+ * Minimal surface of the automation engine this package depends on — the
10
+ * connector registry (ADR-0018 §Addendum) plus the provider registry (ADR-0097).
11
+ * Kept structural so callers need no runtime dependency on
12
+ * `@objectstack/service-automation` (mirrors connector-rest / connector-mcp).
11
13
  */
12
14
  export interface ConnectorRegistrySurface {
13
15
  registerConnector(
@@ -18,6 +20,7 @@ export interface ConnectorRegistrySurface {
18
20
  >,
19
21
  ): void;
20
22
  unregisterConnector(name: string): void;
23
+ registerConnectorProvider(providerKey: string, factory: ConnectorProviderFactory): void;
21
24
  }
22
25
 
23
26
  /**
@@ -30,3 +33,85 @@ export function registerOpenApiConnector(registry: ConnectorRegistrySurface, con
30
33
  registry.registerConnector(def, handlers);
31
34
  return def.name;
32
35
  }
36
+
37
+ /**
38
+ * Options for {@link ConnectorOpenApiPlugin}. All optional (ADR-0097): with no
39
+ * `document` the plugin contributes only the `openapi` provider factory — so a
40
+ * stack can declare `provider: 'openapi'` instances as pure metadata. Supply a
41
+ * `document` (+ config) to ALSO register one hand-wired OpenAPI connector.
42
+ */
43
+ export interface ConnectorOpenApiPluginOptions extends Partial<OpenApiConnectorConfig> {
44
+ /** Injected fetch implementation forwarded to the `openapi` provider factory (tests). */
45
+ fetchImpl?: typeof fetch;
46
+ }
47
+
48
+ /**
49
+ * ConnectorOpenApiPlugin — contributes the OpenAPI generic executor (ADR-0023) in
50
+ * two forms:
51
+ *
52
+ * 1. **Provider factory** (`openapi`, ADR-0097): registered at `init()` so the
53
+ * automation service can materialize declarative `provider: 'openapi'`
54
+ * `connectors:` entries — loading the OpenAPI document and mapping each
55
+ * operation to a connector action — at boot.
56
+ * 2. **Hand-wired instance** (optional, back-compat): when constructed with a
57
+ * `document`, it also registers one concrete OpenAPI connector at `start()`.
58
+ *
59
+ * If no automation engine is present the plugin logs and skips.
60
+ */
61
+ export class ConnectorOpenApiPlugin implements Plugin {
62
+ name = 'com.objectstack.connector.openapi';
63
+ version = '1.0.0';
64
+ type = 'standard' as const;
65
+ // Ensure the automation engine (and its connector/provider registries) exist first.
66
+ dependencies = ['com.objectstack.service-automation'];
67
+
68
+ private readonly options: ConnectorOpenApiPluginOptions;
69
+ private connectorName?: string;
70
+ private automation?: ConnectorRegistrySurface;
71
+
72
+ constructor(options: ConnectorOpenApiPluginOptions = {}) {
73
+ this.options = options;
74
+ }
75
+
76
+ async init(ctx: PluginContext): Promise<void> {
77
+ // Contribute the `openapi` provider factory (ADR-0097) before the
78
+ // automation service materializes declarative instances during its start().
79
+ const automation = this.tryGetAutomation(ctx);
80
+ if (automation && typeof automation.registerConnectorProvider === 'function') {
81
+ automation.registerConnectorProvider(
82
+ OPENAPI_PROVIDER_KEY,
83
+ createOpenApiProviderFactory({ fetchImpl: this.options.fetchImpl }),
84
+ );
85
+ ctx.logger.info("ConnectorOpenApiPlugin: registered 'openapi' connector provider");
86
+ }
87
+ }
88
+
89
+ async start(ctx: PluginContext): Promise<void> {
90
+ // Provider-only usage (no document) contributes just the factory in init().
91
+ if (!this.options.document) return;
92
+
93
+ const automation = this.tryGetAutomation(ctx);
94
+ if (!automation || typeof automation.registerConnector !== 'function') {
95
+ ctx.logger.info('ConnectorOpenApiPlugin: no automation engine — OpenAPI connector not registered');
96
+ return;
97
+ }
98
+
99
+ this.connectorName = registerOpenApiConnector(automation, this.options as OpenApiConnectorConfig);
100
+ this.automation = automation;
101
+ ctx.logger.info(`ConnectorOpenApiPlugin: OpenAPI connector '${this.connectorName}' registered`);
102
+ }
103
+
104
+ async stop(_ctx: PluginContext): Promise<void> {
105
+ if (this.automation && this.connectorName) {
106
+ try { this.automation.unregisterConnector(this.connectorName); } catch { /* ignore */ }
107
+ }
108
+ }
109
+
110
+ private tryGetAutomation(ctx: PluginContext): ConnectorRegistrySurface | undefined {
111
+ try {
112
+ return ctx.getService<ConnectorRegistrySurface>('automation');
113
+ } catch {
114
+ return undefined;
115
+ }
116
+ }
117
+ }
package/src/index.ts CHANGED
@@ -32,5 +32,12 @@ export {
32
32
  } from './openapi-connector.js';
33
33
  export {
34
34
  registerOpenApiConnector,
35
+ ConnectorOpenApiPlugin,
36
+ type ConnectorOpenApiPluginOptions,
35
37
  type ConnectorRegistrySurface,
36
38
  } from './connector-openapi-plugin.js';
39
+ export {
40
+ createOpenApiProviderFactory,
41
+ OPENAPI_PROVIDER_KEY,
42
+ type OpenApiProviderDeps,
43
+ } from './openapi-provider.js';
@@ -0,0 +1,98 @@
1
+ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2
+ //
3
+ // ADR-0097 — the `openapi` provider factory: materialize a declarative
4
+ // `provider: 'openapi'` connector instance from a spec document + resolved auth.
5
+
6
+ import { describe, it, expect } from 'vitest';
7
+ import type { ConnectorProviderContext } from '@objectstack/spec/integration';
8
+ import { createOpenApiProviderFactory, OPENAPI_PROVIDER_KEY } from './openapi-provider.js';
9
+
10
+ const petstore = {
11
+ openapi: '3.0.0',
12
+ info: { title: 'Petstore', version: '1.0.0' },
13
+ servers: [{ url: 'https://petstore.example.com' }],
14
+ paths: {
15
+ '/pets': {
16
+ get: { operationId: 'listPets', summary: 'List pets', responses: { '200': { description: 'ok' } } },
17
+ },
18
+ },
19
+ };
20
+
21
+ function ctx(partial: Partial<ConnectorProviderContext> & Pick<ConnectorProviderContext, 'providerConfig'>): ConnectorProviderContext {
22
+ return { name: 'pets', label: 'Pets', type: 'api', ...partial };
23
+ }
24
+
25
+ describe('openapi provider factory (ADR-0097)', () => {
26
+ it('advertises the openapi provider key', () => {
27
+ expect(OPENAPI_PROVIDER_KEY).toBe('openapi');
28
+ });
29
+
30
+ it('materializes actions from an inline OpenAPI document (no network at boot)', async () => {
31
+ const factory = createOpenApiProviderFactory();
32
+ const { def, handlers } = await factory(ctx({ providerConfig: { spec: petstore } }));
33
+ expect(def.name).toBe('pets');
34
+ expect(Object.keys(handlers)).toEqual(['listPets']);
35
+ expect(def.actions?.map((a) => a.key)).toEqual(['listPets']);
36
+ });
37
+
38
+ it('fetches the document when spec is an http(s) URL, via the injected fetch', async () => {
39
+ let fetched: string | undefined;
40
+ const fetchImpl = (async (url: string) => {
41
+ fetched = url;
42
+ return { ok: true, status: 200, json: async () => petstore } as unknown as Response;
43
+ }) as unknown as typeof fetch;
44
+ const factory = createOpenApiProviderFactory({ fetchImpl });
45
+ const { def } = await factory(ctx({ providerConfig: { spec: 'https://petstore.example.com/openapi.json' } }));
46
+ expect(fetched).toBe('https://petstore.example.com/openapi.json');
47
+ expect(def.actions?.map((a) => a.key)).toEqual(['listPets']);
48
+ });
49
+
50
+ it('throws when spec is missing', async () => {
51
+ const factory = createOpenApiProviderFactory();
52
+ await expect(factory(ctx({ providerConfig: {} }))).rejects.toThrow(/providerConfig\.spec/);
53
+ });
54
+
55
+ // ── File-path specs (#3016 — ADR-0097 follow-up) ────────────────────────
56
+
57
+ it('reads a file-path spec through the host loadPackageFile capability', async () => {
58
+ const requested: string[] = [];
59
+ const loadPackageFile = async (rel: string) => {
60
+ requested.push(rel);
61
+ return JSON.stringify(petstore);
62
+ };
63
+ const factory = createOpenApiProviderFactory();
64
+ const { def, handlers } = await factory(
65
+ ctx({ providerConfig: { spec: './specs/petstore.json' }, loadPackageFile }),
66
+ );
67
+ expect(requested).toEqual(['./specs/petstore.json']);
68
+ expect(def.actions?.map((a) => a.key)).toEqual(['listPets']);
69
+ expect(Object.keys(handlers)).toEqual(['listPets']);
70
+ });
71
+
72
+ it('surfaces a loader failure (missing file / traversal rejection) with the connector name', async () => {
73
+ const loadPackageFile = async (rel: string) => {
74
+ throw new Error(`package file ref '${rel}' could not be read`);
75
+ };
76
+ const factory = createOpenApiProviderFactory();
77
+ await expect(
78
+ factory(ctx({ providerConfig: { spec: './missing.json' }, loadPackageFile })),
79
+ ).rejects.toThrow(/'pets' failed to read providerConfig\.spec '\.\/missing\.json'.*could not be read/s);
80
+ });
81
+
82
+ it('rejects an unparseable file-path spec with a clear message', async () => {
83
+ const factory = createOpenApiProviderFactory();
84
+ await expect(
85
+ factory(ctx({ providerConfig: { spec: './broken.json' }, loadPackageFile: async () => 'not-json{' })),
86
+ ).rejects.toThrow(/not a parseable.*OpenAPI JSON document/s);
87
+ await expect(
88
+ factory(ctx({ providerConfig: { spec: './array.json' }, loadPackageFile: async () => '[1,2]' })),
89
+ ).rejects.toThrow(/not a parseable.*OpenAPI JSON document/s);
90
+ });
91
+
92
+ it('rejects a file-path spec with a clear message when the host has no package file access', async () => {
93
+ const factory = createOpenApiProviderFactory();
94
+ await expect(
95
+ factory(ctx({ providerConfig: { spec: './petstore.json' } })),
96
+ ).rejects.toThrow(/no package file access/);
97
+ });
98
+ });