import { Effect, Layer, Option } from "effect";import { HttpClient, HttpClientRequest } from "effect/unstable/http";import type { ToolFileValue } from "@executor-js/sdk/core"; import { OpenApiInvocationError } from "./errors";import { resolveServerUrl } from "./openapi-utils";import { type EncodingObject, type OperationFileHint, type OperationBinding, InvocationResult, type MediaBinding, type OperationParameter, type ServerInfo,} from "./types"; // ---------------------------------------------------------------------------// Parameter reading// --------------------------------------------------------------------------- const CONTAINER_KEYS: Record<string, readonly string[]> = { path: ["path", "pathParams", "params"], query: ["query", "queryParams", "params"], header: ["headers", "header"], cookie: ["cookies", "cookie"],}; const readParamValue = (args: Record<string, unknown>, param: OperationParameter): unknown => { const direct = args[param.name]; if (direct !== undefined) return direct; for (const key of CONTAINER_KEYS[param.location] ?? []) { const container = args[key]; if (typeof container === "object" && container !== null && !Array.isArray(container)) { const nested = (container as Record<string, unknown>)[param.name]; if (nested !== undefined) return nested; } } return undefined;}; const primitiveToString = (value: unknown): string => typeof value === "object" && value !== null ? JSON.stringify(value) : String(value); // RFC 3986 §2.2 reserved chars. `allowReserved: true` leaves these// unencoded; default OAS behavior encodes everything non-unreserved.const RESERVED_UNENCODED_RE = /[A-Za-z0-9\-._~:/?#[\]@!$&'()*+,;=]/; const encodeReservedAware = (raw: string, allowReserved: boolean): string => { if (!allowReserved) return encodeURIComponent(raw); // Walk char-by-char so the reserved set passes through as-is. let out = ""; for (const ch of raw) { out += RESERVED_UNENCODED_RE.test(ch) ? ch : encodeURIComponent(ch); } return out;}; const queryParamValues = (value: unknown, param: OperationParameter): string[] => { if (value === undefined || value === null) return []; if (!Array.isArray(value)) return [primitiveToString(value)]; const style = Option.getOrUndefined(param.style) ?? "form"; const explode = Option.getOrElse(param.explode, () => true); if (explode) return value.map(primitiveToString); const separator = style === "spaceDelimited" ? " " : style === "pipeDelimited" ? "|" : ","; return [value.map(primitiveToString).join(separator)];}; // ---------------------------------------------------------------------------// Path resolution// --------------------------------------------------------------------------- const resolvePath = Effect.fn("OpenApi.resolvePath")(function* ( pathTemplate: string, args: Record<string, unknown>, parameters: readonly OperationParameter[],) { let resolved = pathTemplate; for (const param of parameters) { if (param.location !== "path") continue; const value = readParamValue(args, param); if (value === undefined || value === null) { if (param.required) { return yield* new OpenApiInvocationError({ message: `Missing required path parameter: ${param.name}`, statusCode: Option.none(), }); } continue; } const encoded = encodeReservedAware( String(value), Option.getOrElse(param.allowReserved, () => false), ); resolved = resolved.replaceAll(`{${param.name}}`, encoded); resolved = resolved.replaceAll(`{+${param.name}}`, encoded); } const remaining = [...resolved.matchAll(/\{([^{}]+)\}/g)] .map((m) => m[1]) .filter((v): v is string => typeof v === "string"); for (const name of remaining) { const value = args[name]; if (value !== undefined && value !== null) { resolved = resolved.replaceAll(`{${name}}`, encodeURIComponent(String(value))); } } const unresolved = [...resolved.matchAll(/\{([^{}]+)\}/g)] .map((m) => m[1]) .filter((v): v is string => typeof v === "string"); if (unresolved.length > 0) { return yield* new OpenApiInvocationError({ message: `Unresolved path parameters: ${[...new Set(unresolved)].join(", ")}`, statusCode: Option.none(), }); } return resolved;}); // GitHub (and some other upstreams) reject requests that lack a User-Agent// header with a 403 ("Request forbidden by administrative rules"), which is// indistinguishable from a credential rejection downstream. Send a default so// those calls succeed. It is applied before operation header params and// resolved auth headers, so a spec- or connection-provided User-Agent wins.const DEFAULT_USER_AGENT = "executor"; const applyHeaders = ( request: HttpClientRequest.HttpClientRequest, headers: Record<string, string>,): HttpClientRequest.HttpClientRequest => { let req = request; for (const [name, value] of Object.entries(headers)) { req = HttpClientRequest.setHeader(req, name, value); } return req;}; // ---------------------------------------------------------------------------// Response helpers// --------------------------------------------------------------------------- const normalizeContentType = (ct: string | null | undefined): string => ct?.split(";")[0]?.trim().toLowerCase() ?? ""; const isJsonContentType = (ct: string | null | undefined): boolean => { const normalized = normalizeContentType(ct); if (!normalized) return false; return ( normalized === "application/json" || normalized.includes("+json") || normalized.includes("json") );}; const isFormUrlEncoded = (ct: string | null | undefined): boolean => normalizeContentType(ct) === "application/x-www-form-urlencoded"; const isMultipartFormData = (ct: string | null | undefined): boolean => normalizeContentType(ct).startsWith("multipart/form-data"); const isXmlContentType = (ct: string | null | undefined): boolean => { const normalized = normalizeContentType(ct); if (!normalized) return false; return ( normalized === "application/xml" || normalized === "text/xml" || normalized.endsWith("+xml") );}; const isTextContentType = (ct: string | null | undefined): boolean => normalizeContentType(ct).startsWith("text/"); const isOctetStream = (ct: string | null | undefined): boolean => normalizeContentType(ct) === "application/octet-stream"; const bytesToBase64 = (bytes: Uint8Array): string => { let binary = ""; const chunkSize = 0x8000; for (let i = 0; i < bytes.length; i += chunkSize) { binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize)); } return btoa(binary);}; const normalizeBase64 = (value: string, encoding: "base64" | "base64url"): string => { const compact = value.replace(/\s/g, ""); const alphabet = encoding === "base64url" ? compact.replace(/-/g, "+").replace(/_/g, "/") : compact; const remainder = alphabet.length % 4; return remainder === 0 ? alphabet : `${alphabet}${"=".repeat(4 - remainder)}`;}; const byteLengthFromBase64 = (base64: string): number => { const compact = base64.replace(/\s/g, ""); const padding = compact.endsWith("==") ? 2 : compact.endsWith("=") ? 1 : 0; return Math.max(0, Math.floor((compact.length * 3) / 4) - padding);}; const isGenericMimeType = (mimeType: string): boolean => normalizeContentType(mimeType) === "application/octet-stream"; const startsWithBytes = (bytes: Uint8Array, prefix: readonly number[]): boolean => prefix.every((byte, index) => bytes[index] === byte); const isLikelyUtf8Text = (bytes: Uint8Array): boolean => { if (bytes.length === 0) return false; let text: string; // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: TextDecoder throws while probing arbitrary binary content try { text = new TextDecoder("utf-8", { fatal: true }).decode(bytes); } catch { return false; } let suspicious = 0; for (let index = 0; index < text.length; index += 1) { const code = text.charCodeAt(index); const allowedControl = code === 0x09 || code === 0x0a || code === 0x0c || code === 0x0d; if (code === 0x00) return false; if (code < 0x20 && !allowedControl) suspicious += 1; } return suspicious / Math.max(1, text.length) <= 0.02;}; const sniffMimeType = (bytes: Uint8Array): string | null => { if (startsWithBytes(bytes, [0xff, 0xd8, 0xff])) return "image/jpeg"; if (startsWithBytes(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) { return "image/png"; } if ( startsWithBytes(bytes, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) || startsWithBytes(bytes, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61]) ) { return "image/gif"; } if ( startsWithBytes(bytes, [0x52, 0x49, 0x46, 0x46]) && bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50 ) { return "image/webp"; } if (startsWithBytes(bytes, [0x25, 0x50, 0x44, 0x46, 0x2d])) return "application/pdf"; if ( startsWithBytes(bytes, [0x50, 0x4b, 0x03, 0x04]) || startsWithBytes(bytes, [0x50, 0x4b, 0x05, 0x06]) || startsWithBytes(bytes, [0x50, 0x4b, 0x07, 0x08]) ) { return "application/zip"; } if (isLikelyUtf8Text(bytes)) return "text/plain"; return null;}; const bytesFromBase64Prefix = (base64: string): Uint8Array => { const prefix = base64.slice(0, Math.min(base64.length, 64)); const binary = atob(prefix); const bytes = new Uint8Array(binary.length); for (let index = 0; index < binary.length; index += 1) { bytes[index] = binary.charCodeAt(index); } return bytes;}; const sniffMimeTypeFromBase64 = (base64: string): string | null => sniffMimeType(bytesFromBase64Prefix(base64)); type DecodedBase64Body = { readonly ok: true; readonly bytes: Uint8Array } | { readonly ok: false }; const base64ToUint8Array = (value: string): Uint8Array | null => { let binary = ""; // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: atob throws for invalid base64; invalid shapes are treated as non-byte input try { binary = atob(normalizeBase64(value, "base64")); } catch { return null; } const bytes = new Uint8Array(binary.length); for (let index = 0; index < binary.length; index += 1) { bytes[index] = binary.charCodeAt(index); } return bytes;}; const decodeBase64Body = (value: string): DecodedBase64Body => { const bytes = base64ToUint8Array(value); return bytes ? { ok: true, bytes } : { ok: false };}; const toUint8Array = (value: unknown): Uint8Array | null => { if (value instanceof Uint8Array) return value; if (value instanceof ArrayBuffer) return new Uint8Array(value); if (ArrayBuffer.isView(value)) { const view = value as ArrayBufferView; return new Uint8Array(view.buffer, view.byteOffset, view.byteLength); } if (Array.isArray(value) && value.every((v) => typeof v === "number")) { return new Uint8Array(value as readonly number[]); } return null;}; const readNestedBodyBase64 = (value: unknown): unknown => typeof value === "object" && value !== null && !Array.isArray(value) && Object.prototype.hasOwnProperty.call(value, "bodyBase64") ? (value as Record<string, unknown>).bodyBase64 : undefined; const readHintString = (option: OperationFileHint["dataField"], fallback: string): string => Option.getOrElse(option, () => fallback); const readHintMimeType = (hint: OperationFileHint, fallback: string): string => Option.getOrElse(hint.mimeType, () => fallback); const readHintEncoding = (hint: OperationFileHint): "base64" | "base64url" => Option.getOrElse(hint.encoding, () => "base64"); const fileFromByteField = (body: unknown, hint: OperationFileHint): ToolFileValue | null => { if (typeof body !== "object" || body === null || Array.isArray(body)) return null; const record = body as Record<string, unknown>; const dataField = readHintString(hint.dataField, "data"); const rawData = record[dataField]; if (typeof rawData !== "string") return null; const data = normalizeBase64(rawData, readHintEncoding(hint)); const sizeField = Option.getOrUndefined(hint.sizeField); const byteLength = sizeField && typeof record[sizeField] === "number" ? record[sizeField] : byteLengthFromBase64(data); const hintedMimeType = readHintMimeType(hint, "application/octet-stream"); return { _tag: "ToolFile", mimeType: isGenericMimeType(hintedMimeType) ? (sniffMimeTypeFromBase64(data) ?? hintedMimeType) : hintedMimeType, encoding: "base64", data, byteLength, };}; const fileFromBinaryBytes = ( bytes: Uint8Array, hint: OperationFileHint, contentType: string | null | undefined,): ToolFileValue => { const hintedMimeType = contentType ?? readHintMimeType(hint, "application/octet-stream"); return { _tag: "ToolFile", mimeType: isGenericMimeType(hintedMimeType) ? (sniffMimeType(bytes) ?? hintedMimeType) : hintedMimeType, encoding: "base64", data: bytesToBase64(bytes), byteLength: bytes.byteLength, };}; type FormDataRecord = Parameters<typeof HttpClientRequest.bodyFormDataRecord>[1];type FormDataCoercible = FormDataRecord[string]; // Pull a plain ArrayBuffer out of a Uint8Array — `new Blob([u8])` rejects// views whose `.buffer` is `SharedArrayBuffer | ArrayBuffer` under strict// lib.dom typings.const toArrayBuffer = (bytes: Uint8Array): ArrayBuffer => { const copy = new ArrayBuffer(bytes.byteLength); new Uint8Array(copy).set(bytes); return copy;}; // ---------------------------------------------------------------------------// OpenAPI 3.x encoding — per-property style/explode/allowReserved/contentType// for multipart/form-data and application/x-www-form-urlencoded bodies.// Spec ref: https://spec.openapis.org/oas/v3.1.0#encoding-object// --------------------------------------------------------------------------- type StyleExplode = { readonly style: string; readonly explode: boolean; readonly allowReserved: boolean;}; const DEFAULT_FORM_STYLE: StyleExplode = { style: "form", explode: true, allowReserved: false,}; const resolveStyleExplode = (e: EncodingObject | undefined): StyleExplode => { if (!e) return DEFAULT_FORM_STYLE; return { style: Option.getOrElse(e.style, () => DEFAULT_FORM_STYLE.style), explode: Option.getOrElse(e.explode, () => DEFAULT_FORM_STYLE.explode), allowReserved: Option.getOrElse(e.allowReserved, () => DEFAULT_FORM_STYLE.allowReserved), };}; const encodeFormValue = (v: unknown, allowReserved: boolean): string => { const raw = typeof v === "object" && v !== null ? JSON.stringify(v) : String(v); return encodeReservedAware(raw, allowReserved);}; /** * Serialize a record to application/x-www-form-urlencoded with OAS3 style * rules honored per-field. Supports `form` (default), `deepObject`, * `pipeDelimited`, `spaceDelimited` styles with `explode` true / false. */const serializeFormUrlEncoded = ( value: Record<string, unknown>, encoding: Record<string, EncodingObject> | undefined,): string => { const parts: string[] = []; for (const [key, raw] of Object.entries(value)) { if (raw === undefined || raw === null) continue; const { style, explode, allowReserved } = resolveStyleExplode(encoding?.[key]); const encKey = encodeURIComponent(key); if (Array.isArray(raw)) { if (explode) { for (const v of raw) { parts.push(`${encKey}=${encodeFormValue(v, allowReserved)}`); } } else { const sep = style === "spaceDelimited" ? " " : style === "pipeDelimited" ? "|" : ","; parts.push( `${encKey}=${encodeFormValue( raw.map((v) => (typeof v === "object" ? JSON.stringify(v) : String(v))).join(sep), allowReserved, )}`, ); } continue; } if (typeof raw === "object") { const entries = Object.entries(raw as Record<string, unknown>).filter( ([, v]) => v !== undefined && v !== null, ); if (style === "deepObject") { for (const [subkey, subval] of entries) { // Encode the whole `key[subkey]` fragment so `[` / `]` become // `%5B` / `%5D`. Matches swagger-client's behaviour and remains // accepted by common server-side parsers (qs, Rails, etc.). parts.push( `${encodeURIComponent(`${key}[${subkey}]`)}=${encodeFormValue(subval, allowReserved)}`, ); } } else if (explode) { // form + explode=true on object: sub-keys become top-level fields. for (const [subkey, subval] of entries) { parts.push(`${encodeURIComponent(subkey)}=${encodeFormValue(subval, allowReserved)}`); } } else { // form + explode=false on object: flatten to csv key,val,key,val. const flat = entries.flatMap(([k, v]) => [ k, typeof v === "object" ? JSON.stringify(v) : String(v), ]); parts.push(`${encKey}=${encodeFormValue(flat.join(","), allowReserved)}`); } continue; } parts.push(`${encKey}=${encodeFormValue(raw, allowReserved)}`); } return parts.join("&");}; /** * Best-effort build of a multipart FormData entry record. * * If `encoding[key].contentType` is declared (OAS3 §4.8.15), wrap the value * in a `Blob` with that type so the runtime multipart framer emits the * per-part `Content-Type` header (e.g. `application/json` for a metadata * part whose server expects parsed JSON). * * Otherwise: primitives pass through, arrays handle their item types, byte * shapes wrap as Blob, nested objects JSON-stringify (never `[object Object]`). */const coerceFormDataRecord = ( value: Record<string, unknown>, encoding: Record<string, EncodingObject> | undefined,): FormDataRecord => { const out: Record<string, FormDataCoercible> = {}; for (const [key, raw] of Object.entries(value)) { if (raw === undefined || raw === null) continue; const partType = encoding?.[key] ? Option.getOrUndefined(encoding[key]!.contentType)Lines 1–500 of 943. Load more
import { Effect, Layer, Option } from "effect"; import { HttpClient, HttpClientRequest } from "effect/unstable/http"; import type { ToolFileValue } from "@executor-js/sdk/core"; import { OpenApiInvocationError } from "./errors"; import { resolveServerUrl } from "./openapi-utils"; import { type EncodingObject, type OperationFileHint, type OperationBinding, InvocationResult, type MediaBinding, type OperationParameter, type ServerInfo, } from "./types"; // --------------------------------------------------------------------------- // Parameter reading // --------------------------------------------------------------------------- const CONTAINER_KEYS: Record<string, readonly string[]> = { path: ["path", "pathParams", "params"], query: ["query", "queryParams", "params"], header: ["headers", "header"], cookie: ["cookies", "cookie"], }; const readParamValue = (args: Record<string, unknown>, param: OperationParameter): unknown => { const direct = args[param.name]; if (direct !== undefined) return direct; for (const key of CONTAINER_KEYS[param.location] ?? []) { const container = args[key]; if (typeof container === "object" && container !== null && !Array.isArray(container)) { const nested = (container as Record<string, unknown>)[param.name]; if (nested !== undefined) return nested; } } return undefined; }; const primitiveToString = (value: unknown): string => typeof value === "object" && value !== null ? JSON.stringify(value) : String(value); // RFC 3986 §2.2 reserved chars. `allowReserved: true` leaves these // unencoded; default OAS behavior encodes everything non-unreserved. const RESERVED_UNENCODED_RE = /[A-Za-z0-9\-._~:/?#[\]@!$&'()*+,;=]/; const encodeReservedAware = (raw: string, allowReserved: boolean): string => { if (!allowReserved) return encodeURIComponent(raw); // Walk char-by-char so the reserved set passes through as-is. let out = ""; for (const ch of raw) { out += RESERVED_UNENCODED_RE.test(ch) ? ch : encodeURIComponent(ch); } return out; }; const queryParamValues = (value: unknown, param: OperationParameter): string[] => { if (value === undefined || value === null) return []; if (!Array.isArray(value)) return [primitiveToString(value)]; const style = Option.getOrUndefined(param.style) ?? "form"; const explode = Option.getOrElse(param.explode, () => true); if (explode) return value.map(primitiveToString); const separator = style === "spaceDelimited" ? " " : style === "pipeDelimited" ? "|" : ","; return [value.map(primitiveToString).join(separator)]; }; // --------------------------------------------------------------------------- // Path resolution // --------------------------------------------------------------------------- const resolvePath = Effect.fn("OpenApi.resolvePath")(function* ( pathTemplate: string, args: Record<string, unknown>, parameters: readonly OperationParameter[], ) { let resolved = pathTemplate; for (const param of parameters) { if (param.location !== "path") continue; const value = readParamValue(args, param); if (value === undefined || value === null) { if (param.required) { return yield* new OpenApiInvocationError({ message: `Missing required path parameter: ${param.name}`, statusCode: Option.none(), }); } continue; } const encoded = encodeReservedAware( String(value), Option.getOrElse(param.allowReserved, () => false), ); resolved = resolved.replaceAll(`{${param.name}}`, encoded); resolved = resolved.replaceAll(`{+${param.name}}`, encoded); } const remaining = [...resolved.matchAll(/\{([^{}]+)\}/g)] .map((m) => m[1]) .filter((v): v is string => typeof v === "string"); for (const name of remaining) { const value = args[name]; if (value !== undefined && value !== null) { resolved = resolved.replaceAll(`{${name}}`, encodeURIComponent(String(value))); } } const unresolved = [...resolved.matchAll(/\{([^{}]+)\}/g)] .map((m) => m[1]) .filter((v): v is string => typeof v === "string"); if (unresolved.length > 0) { return yield* new OpenApiInvocationError({ message: `Unresolved path parameters: ${[...new Set(unresolved)].join(", ")}`, statusCode: Option.none(), }); } return resolved; }); // GitHub (and some other upstreams) reject requests that lack a User-Agent // header with a 403 ("Request forbidden by administrative rules"), which is // indistinguishable from a credential rejection downstream. Send a default so // those calls succeed. It is applied before operation header params and // resolved auth headers, so a spec- or connection-provided User-Agent wins. const DEFAULT_USER_AGENT = "executor"; const applyHeaders = ( request: HttpClientRequest.HttpClientRequest, headers: Record<string, string>, ): HttpClientRequest.HttpClientRequest => { let req = request; for (const [name, value] of Object.entries(headers)) { req = HttpClientRequest.setHeader(req, name, value); } return req; }; // --------------------------------------------------------------------------- // Response helpers // --------------------------------------------------------------------------- const normalizeContentType = (ct: string | null | undefined): string => ct?.split(";")[0]?.trim().toLowerCase() ?? ""; const isJsonContentType = (ct: string | null | undefined): boolean => { const normalized = normalizeContentType(ct); if (!normalized) return false; return ( normalized === "application/json" || normalized.includes("+json") || normalized.includes("json") ); }; const isFormUrlEncoded = (ct: string | null | undefined): boolean => normalizeContentType(ct) === "application/x-www-form-urlencoded"; const isMultipartFormData = (ct: string | null | undefined): boolean => normalizeContentType(ct).startsWith("multipart/form-data"); const isXmlContentType = (ct: string | null | undefined): boolean => { const normalized = normalizeContentType(ct); if (!normalized) return false; return ( normalized === "application/xml" || normalized === "text/xml" || normalized.endsWith("+xml") ); }; const isTextContentType = (ct: string | null | undefined): boolean => normalizeContentType(ct).startsWith("text/"); const isOctetStream = (ct: string | null | undefined): boolean => normalizeContentType(ct) === "application/octet-stream"; const bytesToBase64 = (bytes: Uint8Array): string => { let binary = ""; const chunkSize = 0x8000; for (let i = 0; i < bytes.length; i += chunkSize) { binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize)); } return btoa(binary); }; const normalizeBase64 = (value: string, encoding: "base64" | "base64url"): string => { const compact = value.replace(/\s/g, ""); const alphabet = encoding === "base64url" ? compact.replace(/-/g, "+").replace(/_/g, "/") : compact; const remainder = alphabet.length % 4; return remainder === 0 ? alphabet : `${alphabet}${"=".repeat(4 - remainder)}`; }; const byteLengthFromBase64 = (base64: string): number => { const compact = base64.replace(/\s/g, ""); const padding = compact.endsWith("==") ? 2 : compact.endsWith("=") ? 1 : 0; return Math.max(0, Math.floor((compact.length * 3) / 4) - padding); }; const isGenericMimeType = (mimeType: string): boolean => normalizeContentType(mimeType) === "application/octet-stream"; const startsWithBytes = (bytes: Uint8Array, prefix: readonly number[]): boolean => prefix.every((byte, index) => bytes[index] === byte); const isLikelyUtf8Text = (bytes: Uint8Array): boolean => { if (bytes.length === 0) return false; let text: string; // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: TextDecoder throws while probing arbitrary binary content try { text = new TextDecoder("utf-8", { fatal: true }).decode(bytes); } catch { return false; } let suspicious = 0; for (let index = 0; index < text.length; index += 1) { const code = text.charCodeAt(index); const allowedControl = code === 0x09 || code === 0x0a || code === 0x0c || code === 0x0d; if (code === 0x00) return false; if (code < 0x20 && !allowedControl) suspicious += 1; } return suspicious / Math.max(1, text.length) <= 0.02; }; const sniffMimeType = (bytes: Uint8Array): string | null => { if (startsWithBytes(bytes, [0xff, 0xd8, 0xff])) return "image/jpeg"; if (startsWithBytes(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) { return "image/png"; } if ( startsWithBytes(bytes, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) || startsWithBytes(bytes, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61]) ) { return "image/gif"; } if ( startsWithBytes(bytes, [0x52, 0x49, 0x46, 0x46]) && bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50 ) { return "image/webp"; } if (startsWithBytes(bytes, [0x25, 0x50, 0x44, 0x46, 0x2d])) return "application/pdf"; if ( startsWithBytes(bytes, [0x50, 0x4b, 0x03, 0x04]) || startsWithBytes(bytes, [0x50, 0x4b, 0x05, 0x06]) || startsWithBytes(bytes, [0x50, 0x4b, 0x07, 0x08]) ) { return "application/zip"; } if (isLikelyUtf8Text(bytes)) return "text/plain"; return null; }; const bytesFromBase64Prefix = (base64: string): Uint8Array => { const prefix = base64.slice(0, Math.min(base64.length, 64)); const binary = atob(prefix); const bytes = new Uint8Array(binary.length); for (let index = 0; index < binary.length; index += 1) { bytes[index] = binary.charCodeAt(index); } return bytes; }; const sniffMimeTypeFromBase64 = (base64: string): string | null => sniffMimeType(bytesFromBase64Prefix(base64)); type DecodedBase64Body = { readonly ok: true; readonly bytes: Uint8Array } | { readonly ok: false }; const base64ToUint8Array = (value: string): Uint8Array | null => { let binary = ""; // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: atob throws for invalid base64; invalid shapes are treated as non-byte input try { binary = atob(normalizeBase64(value, "base64")); } catch { return null; } const bytes = new Uint8Array(binary.length); for (let index = 0; index < binary.length; index += 1) { bytes[index] = binary.charCodeAt(index); } return bytes; }; const decodeBase64Body = (value: string): DecodedBase64Body => { const bytes = base64ToUint8Array(value); return bytes ? { ok: true, bytes } : { ok: false }; }; const toUint8Array = (value: unknown): Uint8Array | null => { if (value instanceof Uint8Array) return value; if (value instanceof ArrayBuffer) return new Uint8Array(value); if (ArrayBuffer.isView(value)) { const view = value as ArrayBufferView; return new Uint8Array(view.buffer, view.byteOffset, view.byteLength); } if (Array.isArray(value) && value.every((v) => typeof v === "number")) { return new Uint8Array(value as readonly number[]); } return null; }; const readNestedBodyBase64 = (value: unknown): unknown => typeof value === "object" && value !== null && !Array.isArray(value) && Object.prototype.hasOwnProperty.call(value, "bodyBase64") ? (value as Record<string, unknown>).bodyBase64 : undefined; const readHintString = (option: OperationFileHint["dataField"], fallback: string): string => Option.getOrElse(option, () => fallback); const readHintMimeType = (hint: OperationFileHint, fallback: string): string => Option.getOrElse(hint.mimeType, () => fallback); const readHintEncoding = (hint: OperationFileHint): "base64" | "base64url" => Option.getOrElse(hint.encoding, () => "base64"); const fileFromByteField = (body: unknown, hint: OperationFileHint): ToolFileValue | null => { if (typeof body !== "object" || body === null || Array.isArray(body)) return null; const record = body as Record<string, unknown>; const dataField = readHintString(hint.dataField, "data"); const rawData = record[dataField]; if (typeof rawData !== "string") return null; const data = normalizeBase64(rawData, readHintEncoding(hint)); const sizeField = Option.getOrUndefined(hint.sizeField); const byteLength = sizeField && typeof record[sizeField] === "number" ? record[sizeField] : byteLengthFromBase64(data); const hintedMimeType = readHintMimeType(hint, "application/octet-stream"); return { _tag: "ToolFile", mimeType: isGenericMimeType(hintedMimeType) ? (sniffMimeTypeFromBase64(data) ?? hintedMimeType) : hintedMimeType, encoding: "base64", data, byteLength, }; }; const fileFromBinaryBytes = ( bytes: Uint8Array, hint: OperationFileHint, contentType: string | null | undefined, ): ToolFileValue => { const hintedMimeType = contentType ?? readHintMimeType(hint, "application/octet-stream"); return { _tag: "ToolFile", mimeType: isGenericMimeType(hintedMimeType) ? (sniffMimeType(bytes) ?? hintedMimeType) : hintedMimeType, encoding: "base64", data: bytesToBase64(bytes), byteLength: bytes.byteLength, }; }; type FormDataRecord = Parameters<typeof HttpClientRequest.bodyFormDataRecord>[1]; type FormDataCoercible = FormDataRecord[string]; // Pull a plain ArrayBuffer out of a Uint8Array — `new Blob([u8])` rejects // views whose `.buffer` is `SharedArrayBuffer | ArrayBuffer` under strict // lib.dom typings. const toArrayBuffer = (bytes: Uint8Array): ArrayBuffer => { const copy = new ArrayBuffer(bytes.byteLength); new Uint8Array(copy).set(bytes); return copy; }; // --------------------------------------------------------------------------- // OpenAPI 3.x encoding — per-property style/explode/allowReserved/contentType // for multipart/form-data and application/x-www-form-urlencoded bodies. // Spec ref: https://spec.openapis.org/oas/v3.1.0#encoding-object // --------------------------------------------------------------------------- type StyleExplode = { readonly style: string; readonly explode: boolean; readonly allowReserved: boolean; }; const DEFAULT_FORM_STYLE: StyleExplode = { style: "form", explode: true, allowReserved: false, }; const resolveStyleExplode = (e: EncodingObject | undefined): StyleExplode => { if (!e) return DEFAULT_FORM_STYLE; return { style: Option.getOrElse(e.style, () => DEFAULT_FORM_STYLE.style), explode: Option.getOrElse(e.explode, () => DEFAULT_FORM_STYLE.explode), allowReserved: Option.getOrElse(e.allowReserved, () => DEFAULT_FORM_STYLE.allowReserved), }; }; const encodeFormValue = (v: unknown, allowReserved: boolean): string => { const raw = typeof v === "object" && v !== null ? JSON.stringify(v) : String(v); return encodeReservedAware(raw, allowReserved); }; /** * Serialize a record to application/x-www-form-urlencoded with OAS3 style * rules honored per-field. Supports `form` (default), `deepObject`, * `pipeDelimited`, `spaceDelimited` styles with `explode` true / false. */ const serializeFormUrlEncoded = ( value: Record<string, unknown>, encoding: Record<string, EncodingObject> | undefined, ): string => { const parts: string[] = []; for (const [key, raw] of Object.entries(value)) { if (raw === undefined || raw === null) continue; const { style, explode, allowReserved } = resolveStyleExplode(encoding?.[key]); const encKey = encodeURIComponent(key); if (Array.isArray(raw)) { if (explode) { for (const v of raw) { parts.push(`${encKey}=${encodeFormValue(v, allowReserved)}`); } } else { const sep = style === "spaceDelimited" ? " " : style === "pipeDelimited" ? "|" : ","; parts.push( `${encKey}=${encodeFormValue( raw.map((v) => (typeof v === "object" ? JSON.stringify(v) : String(v))).join(sep), allowReserved, )}`, ); } continue; } if (typeof raw === "object") { const entries = Object.entries(raw as Record<string, unknown>).filter( ([, v]) => v !== undefined && v !== null, ); if (style === "deepObject") { for (const [subkey, subval] of entries) { // Encode the whole `key[subkey]` fragment so `[` / `]` become // `%5B` / `%5D`. Matches swagger-client's behaviour and remains // accepted by common server-side parsers (qs, Rails, etc.). parts.push( `${encodeURIComponent(`${key}[${subkey}]`)}=${encodeFormValue(subval, allowReserved)}`, ); } } else if (explode) { // form + explode=true on object: sub-keys become top-level fields. for (const [subkey, subval] of entries) { parts.push(`${encodeURIComponent(subkey)}=${encodeFormValue(subval, allowReserved)}`); } } else { // form + explode=false on object: flatten to csv key,val,key,val. const flat = entries.flatMap(([k, v]) => [ k, typeof v === "object" ? JSON.stringify(v) : String(v), ]); parts.push(`${encKey}=${encodeFormValue(flat.join(","), allowReserved)}`); } continue; } parts.push(`${encKey}=${encodeFormValue(raw, allowReserved)}`); } return parts.join("&"); }; /** * Best-effort build of a multipart FormData entry record. * * If `encoding[key].contentType` is declared (OAS3 §4.8.15), wrap the value * in a `Blob` with that type so the runtime multipart framer emits the * per-part `Content-Type` header (e.g. `application/json` for a metadata * part whose server expects parsed JSON). * * Otherwise: primitives pass through, arrays handle their item types, byte * shapes wrap as Blob, nested objects JSON-stringify (never `[object Object]`). */ const coerceFormDataRecord = ( value: Record<string, unknown>, encoding: Record<string, EncodingObject> | undefined, ): FormDataRecord => { const out: Record<string, FormDataCoercible> = {}; for (const [key, raw] of Object.entries(value)) { if (raw === undefined || raw === null) continue; const partType = encoding?.[key] ? Option.getOrUndefined(encoding[key]!.contentType)