@smithers-orchestrator/openapi 0.16.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 William Cory
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@smithers-orchestrator/openapi",
3
+ "version": "0.16.0",
4
+ "description": "OpenAPI parsing and AI SDK tool generation for Smithers",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "exports": {
8
+ ".": {
9
+ "types": "./src/index.d.ts",
10
+ "import": "./src/index.js",
11
+ "default": "./src/index.js"
12
+ },
13
+ "./*": {
14
+ "types": "./src/index.d.ts",
15
+ "import": "./src/*.js",
16
+ "default": "./src/*.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "src/"
21
+ ],
22
+ "dependencies": {
23
+ "ai": "^6.0.69",
24
+ "zod": "^4.3.6",
25
+ "@smithers-orchestrator/driver": "0.16.0",
26
+ "@smithers-orchestrator/observability": "0.16.0",
27
+ "@smithers-orchestrator/scheduler": "0.16.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/bun": "latest",
31
+ "typescript": "~5.9.3"
32
+ },
33
+ "scripts": {
34
+ "build": "tsup --dts-only",
35
+ "test": "bun test tests",
36
+ "typecheck": "tsc -p tsconfig.json --noEmit"
37
+ }
38
+ }
@@ -0,0 +1 @@
1
+ export type HttpMethod = "get" | "post" | "put" | "delete" | "patch";
@@ -0,0 +1,6 @@
1
+ import type { RefObject } from "./RefObject.ts";
2
+ import type { SchemaObject } from "./SchemaObject.ts";
3
+
4
+ export type MediaTypeObject = {
5
+ schema?: SchemaObject | RefObject;
6
+ };
@@ -0,0 +1,16 @@
1
+ export type OpenApiAuth =
2
+ | {
3
+ type: "bearer";
4
+ token: string;
5
+ }
6
+ | {
7
+ type: "basic";
8
+ username: string;
9
+ password: string;
10
+ }
11
+ | {
12
+ type: "apiKey";
13
+ name: string;
14
+ value: string;
15
+ in: "header" | "query";
16
+ };
@@ -0,0 +1,23 @@
1
+ import type { ParameterObject } from "./ParameterObject.ts";
2
+ import type { PathItem } from "./PathItem.ts";
3
+ import type { RequestBodyObject } from "./RequestBodyObject.ts";
4
+ import type { SchemaObject } from "./SchemaObject.ts";
5
+
6
+ export type OpenApiSpec = {
7
+ openapi: string;
8
+ info: {
9
+ title: string;
10
+ version: string;
11
+ description?: string;
12
+ };
13
+ servers?: Array<{
14
+ url: string;
15
+ description?: string;
16
+ }>;
17
+ paths: Record<string, PathItem>;
18
+ components?: {
19
+ schemas?: Record<string, SchemaObject>;
20
+ parameters?: Record<string, ParameterObject>;
21
+ requestBodies?: Record<string, RequestBodyObject>;
22
+ };
23
+ };
@@ -0,0 +1,8 @@
1
+ import type { Tool } from "ai";
2
+
3
+ /**
4
+ * Type alias for an AI SDK tool produced from an OpenAPI operation.
5
+ * Re-exported here so JSDoc files can reference a stable name without
6
+ * reaching into the `ai` package directly.
7
+ */
8
+ export type OpenApiTool = Tool;
@@ -0,0 +1,10 @@
1
+ import type { OpenApiAuth } from "./OpenApiAuth.ts";
2
+
3
+ export type OpenApiToolsOptions = {
4
+ baseUrl?: string;
5
+ headers?: Record<string, string>;
6
+ auth?: OpenApiAuth;
7
+ include?: string[];
8
+ exclude?: string[];
9
+ namePrefix?: string;
10
+ };
@@ -0,0 +1,14 @@
1
+ import type { ParameterObject } from "./ParameterObject.ts";
2
+ import type { RefObject } from "./RefObject.ts";
3
+ import type { RequestBodyObject } from "./RequestBodyObject.ts";
4
+
5
+ export type OperationObject = {
6
+ operationId?: string;
7
+ summary?: string;
8
+ description?: string;
9
+ parameters?: Array<ParameterObject | RefObject>;
10
+ requestBody?: RequestBodyObject | RefObject;
11
+ responses?: Record<string, unknown>;
12
+ tags?: string[];
13
+ deprecated?: boolean;
14
+ };
@@ -0,0 +1,11 @@
1
+ import type { RefObject } from "./RefObject.ts";
2
+ import type { SchemaObject } from "./SchemaObject.ts";
3
+
4
+ export type ParameterObject = {
5
+ name: string;
6
+ in: "query" | "header" | "path" | "cookie";
7
+ description?: string;
8
+ required?: boolean;
9
+ schema?: SchemaObject | RefObject;
10
+ deprecated?: boolean;
11
+ };
@@ -0,0 +1,14 @@
1
+ import type { HttpMethod } from "./HttpMethod.ts";
2
+ import type { ParameterObject } from "./ParameterObject.ts";
3
+ import type { RequestBodyObject } from "./RequestBodyObject.ts";
4
+
5
+ export type ParsedOperation = {
6
+ operationId: string;
7
+ method: HttpMethod;
8
+ path: string;
9
+ summary: string;
10
+ description: string;
11
+ parameters: ParameterObject[];
12
+ requestBody?: RequestBodyObject;
13
+ deprecated: boolean;
14
+ };
@@ -0,0 +1,12 @@
1
+ import type { OperationObject } from "./OperationObject.ts";
2
+ import type { ParameterObject } from "./ParameterObject.ts";
3
+ import type { RefObject } from "./RefObject.ts";
4
+
5
+ export type PathItem = {
6
+ get?: OperationObject;
7
+ post?: OperationObject;
8
+ put?: OperationObject;
9
+ delete?: OperationObject;
10
+ patch?: OperationObject;
11
+ parameters?: Array<ParameterObject | RefObject>;
12
+ };
@@ -0,0 +1,3 @@
1
+ export type RefObject = {
2
+ $ref: string;
3
+ };
@@ -0,0 +1,7 @@
1
+ import type { MediaTypeObject } from "./MediaTypeObject.ts";
2
+
3
+ export type RequestBodyObject = {
4
+ description?: string;
5
+ required?: boolean;
6
+ content: Record<string, MediaTypeObject>;
7
+ };
@@ -0,0 +1,23 @@
1
+ import type { RefObject } from "./RefObject.ts";
2
+
3
+ export type SchemaObject = {
4
+ type?: string;
5
+ format?: string;
6
+ description?: string;
7
+ properties?: Record<string, SchemaObject | RefObject>;
8
+ required?: string[];
9
+ items?: SchemaObject | RefObject;
10
+ enum?: unknown[];
11
+ default?: unknown;
12
+ nullable?: boolean;
13
+ oneOf?: Array<SchemaObject | RefObject>;
14
+ anyOf?: Array<SchemaObject | RefObject>;
15
+ allOf?: Array<SchemaObject | RefObject>;
16
+ additionalProperties?: boolean | SchemaObject | RefObject;
17
+ minimum?: number;
18
+ maximum?: number;
19
+ minLength?: number;
20
+ maxLength?: number;
21
+ pattern?: string;
22
+ $ref?: string;
23
+ };
@@ -0,0 +1,65 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Shared private helpers for spec parsing
3
+ // ---------------------------------------------------------------------------
4
+ /** @typedef {import("./HttpMethod.ts").HttpMethod} HttpMethod */
5
+ /** @typedef {import("./ParameterObject.ts").ParameterObject} ParameterObject */
6
+
7
+ /**
8
+ * @param {string} text
9
+ * @returns {Record<string, unknown>}
10
+ */
11
+ export function parseSpecText(text) {
12
+ /** @type {unknown} */
13
+ let parsed;
14
+ // Try JSON first
15
+ try {
16
+ parsed = JSON.parse(text);
17
+ }
18
+ catch {
19
+ // Try YAML
20
+ try {
21
+ const yaml = require("yaml");
22
+ parsed = yaml.parse(text);
23
+ }
24
+ catch {
25
+ throw new Error("Failed to parse OpenAPI spec as JSON or YAML");
26
+ }
27
+ }
28
+ // Validate it looks like an OpenAPI spec
29
+ if (typeof parsed !== "object" ||
30
+ parsed === null ||
31
+ !("openapi" in parsed || "swagger" in parsed) ||
32
+ !("paths" in parsed || "info" in parsed)) {
33
+ throw new Error("Parsed content does not appear to be a valid OpenAPI spec (missing openapi/paths/info fields)");
34
+ }
35
+ return /** @type {Record<string, unknown>} */ (parsed);
36
+ }
37
+ /**
38
+ * Merge path-level and operation-level parameters. Operation-level wins
39
+ * when there is a name+in collision.
40
+ *
41
+ * @param {ParameterObject[]} pathLevel
42
+ * @param {ParameterObject[]} opLevel
43
+ * @returns {ParameterObject[]}
44
+ */
45
+ export function mergeParameters(pathLevel, opLevel) {
46
+ const opKeys = new Set(opLevel.map((p) => `${p.in}:${p.name}`));
47
+ const fromPath = pathLevel.filter((p) => !opKeys.has(`${p.in}:${p.name}`));
48
+ return [...fromPath, ...opLevel];
49
+ }
50
+ /**
51
+ * Generate an operationId from method + path when one is not provided.
52
+ * e.g. GET /pets/{petId} → get_pets_petId
53
+ *
54
+ * @param {HttpMethod} method
55
+ * @param {string} path
56
+ * @returns {string}
57
+ */
58
+ export function generateOperationId(method, path) {
59
+ const cleaned = path
60
+ .replace(/\{([^}]+)\}/g, "$1")
61
+ .replace(/[^a-zA-Z0-9]/g, "_")
62
+ .replace(/_+/g, "_")
63
+ .replace(/^_|_$/g, "");
64
+ return `${method}_${cleaned}`;
65
+ }
@@ -0,0 +1,63 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Build a combined Zod schema from operation parameters + requestBody
3
+ // ---------------------------------------------------------------------------
4
+ import { z } from "zod";
5
+ import { isRef } from "./ref-resolver.js";
6
+ import { jsonSchemaToZod } from "./jsonSchemaToZod.js";
7
+
8
+ /** @typedef {import("./OpenApiSpec.ts").OpenApiSpec} OpenApiSpec */
9
+ /** @typedef {import("./ParameterObject.ts").ParameterObject} ParameterObject */
10
+ /** @typedef {import("./RequestBodyObject.ts").RequestBodyObject} RequestBodyObject */
11
+
12
+ /**
13
+ * Build a single Zod object schema for an operation's input, combining:
14
+ * - path parameters
15
+ * - query parameters
16
+ * - header parameters
17
+ * - request body fields
18
+ *
19
+ * @param {ParameterObject[]} parameters
20
+ * @param {RequestBodyObject | undefined} requestBody
21
+ * @param {OpenApiSpec} spec
22
+ * @returns {z.ZodType}
23
+ */
24
+ export function buildOperationSchema(parameters, requestBody, spec) {
25
+ const props = {};
26
+ const requiredKeys = [];
27
+ // Parameters (path, query, header)
28
+ for (const param of parameters) {
29
+ if (param.in === "cookie")
30
+ continue; // skip cookies
31
+ let paramSchema = jsonSchemaToZod(param.schema, spec);
32
+ if (param.description && !(param.schema && !isRef(param.schema) && param.schema.description)) {
33
+ paramSchema = paramSchema.describe(param.description);
34
+ }
35
+ if (!param.required) {
36
+ paramSchema = paramSchema.optional();
37
+ }
38
+ else {
39
+ requiredKeys.push(param.name);
40
+ }
41
+ props[param.name] = paramSchema;
42
+ }
43
+ // Request body
44
+ if (requestBody) {
45
+ const jsonContent = requestBody.content?.["application/json"];
46
+ if (jsonContent?.schema) {
47
+ const bodySchema = jsonSchemaToZod(jsonContent.schema, spec);
48
+ // If the body schema is an object, merge its properties
49
+ // into the top-level props under a "body" key
50
+ if (requestBody.required) {
51
+ props.body = bodySchema;
52
+ requiredKeys.push("body");
53
+ }
54
+ else {
55
+ props.body = bodySchema.optional();
56
+ }
57
+ }
58
+ }
59
+ if (Object.keys(props).length === 0) {
60
+ return z.object({});
61
+ }
62
+ return z.object(props);
63
+ }
@@ -0,0 +1,50 @@
1
+ // ---------------------------------------------------------------------------
2
+ // extractOperations — extract all operations from an OpenAPI spec
3
+ // ---------------------------------------------------------------------------
4
+ import { deref } from "./ref-resolver.js";
5
+ import { HTTP_METHODS, } from "./types.js";
6
+ import { mergeParameters, generateOperationId } from "./_specHelpers.js";
7
+
8
+ /** @typedef {import("./OpenApiSpec.ts").OpenApiSpec} OpenApiSpec */
9
+ /** @typedef {import("./ParsedOperation.ts").ParsedOperation} ParsedOperation */
10
+
11
+ /**
12
+ * Extract all operations from an OpenAPI spec.
13
+ *
14
+ * @param {OpenApiSpec} spec
15
+ * @returns {ParsedOperation[]}
16
+ */
17
+ export function extractOperations(spec) {
18
+ const operations = [];
19
+ const paths = spec.paths ?? {};
20
+ for (const [path, pathItem] of Object.entries(paths)) {
21
+ if (!pathItem)
22
+ continue;
23
+ // Path-level parameters (shared across all methods)
24
+ const pathParams = (pathItem.parameters ?? []).map((p) => deref(spec, p));
25
+ for (const method of HTTP_METHODS) {
26
+ const operation = pathItem[method];
27
+ if (!operation)
28
+ continue;
29
+ // Merge path-level and operation-level parameters.
30
+ // Operation-level takes precedence (matched by name+in).
31
+ const opParams = (operation.parameters ?? []).map((p) => deref(spec, p));
32
+ const mergedParams = mergeParameters(pathParams, opParams);
33
+ const requestBody = operation.requestBody
34
+ ? deref(spec, operation.requestBody)
35
+ : undefined;
36
+ const operationId = operation.operationId ?? generateOperationId(method, path);
37
+ operations.push({
38
+ operationId,
39
+ method,
40
+ path,
41
+ summary: operation.summary ?? "",
42
+ description: operation.description ?? operation.summary ?? "",
43
+ parameters: mergedParams,
44
+ requestBody,
45
+ deprecated: operation.deprecated ?? false,
46
+ });
47
+ }
48
+ }
49
+ return operations;
50
+ }