@neurocode-ai/codemode 1.18.8

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.
@@ -0,0 +1,511 @@
1
+ import { fromSchemaOpenApi3_0, fromSchemaOpenApi3_1 } from "effect/JsonSchema"
2
+ import type { JsonSchema } from "../tool.js"
3
+ import { isBlockedMember } from "../tool-runtime.js"
4
+ import type {
5
+ Body,
6
+ Document,
7
+ InputField,
8
+ OperationInput,
9
+ Parsed,
10
+ SecurityRequirement,
11
+ SecurityScheme,
12
+ } from "./types.js"
13
+
14
+ export const methods = new Set(["get", "put", "post", "delete", "options", "head", "patch", "trace"])
15
+ const parameterLocations = ["path", "query", "header"] as const
16
+ const ignoredHeaderParameters = new Set(["accept", "content-type", "authorization"])
17
+
18
+ export const isRecord = (value: unknown): value is Record<string, unknown> =>
19
+ typeof value === "object" && value !== null && !Array.isArray(value)
20
+
21
+ const asArray = (value: unknown): ReadonlyArray<unknown> => (Array.isArray(value) ? value : [])
22
+
23
+ export const nonEmptyString = (value: unknown): string | undefined =>
24
+ typeof value === "string" && value !== "" ? value : undefined
25
+
26
+ // Guards record lookups keyed by spec- or model-controlled names against
27
+ // prototype-inherited values (e.g. a parameter named `toString`).
28
+ export const own = <T>(record: Readonly<Record<string, T>>, key: string): T | undefined =>
29
+ Object.hasOwn(record, key) ? record[key] : undefined
30
+
31
+ export const resolve = (document: Document, value: unknown): unknown => {
32
+ const next = (current: unknown, seen: ReadonlySet<string>): unknown => {
33
+ if (!isRecord(current)) return current
34
+ const ref = nonEmptyString(current.$ref)
35
+ if (ref === undefined || !ref.startsWith("#/") || seen.has(ref)) return current
36
+ const target = ref
37
+ .slice(2)
38
+ .split("/")
39
+ .map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~"))
40
+ .reduce<unknown>((item, segment) => (isRecord(item) ? own(item, segment) : undefined), document)
41
+ return target === undefined ? current : next(target, new Set([...seen, ref]))
42
+ }
43
+ return next(value, new Set())
44
+ }
45
+
46
+ const projectSchema = (document: Document, value: unknown): JsonSchema => {
47
+ if (!isRecord(value)) return {}
48
+ const normalized = nonEmptyString(document.openapi)?.startsWith("3.0")
49
+ ? fromSchemaOpenApi3_0(value)
50
+ : fromSchemaOpenApi3_1(value)
51
+ return Object.keys(normalized.definitions).length === 0
52
+ ? normalized.schema
53
+ : { ...normalized.schema, $defs: normalized.definitions }
54
+ }
55
+
56
+ export const componentDefinitions = (document: Document): Readonly<Record<string, JsonSchema>> => {
57
+ const components = isRecord(document.components) ? document.components : {}
58
+ const schemas = isRecord(components.schemas) ? components.schemas : {}
59
+ return Object.fromEntries(Object.entries(schemas).map(([name, value]) => [name, projectSchema(document, value)]))
60
+ }
61
+
62
+ const withDefinitions = (schema: JsonSchema, definitions: Readonly<Record<string, JsonSchema>>): JsonSchema => {
63
+ if (Object.keys(definitions).length === 0) return schema
64
+ const local = isRecord(schema.$defs) ? schema.$defs : {}
65
+ return { ...schema, $defs: { ...definitions, ...local } }
66
+ }
67
+
68
+ const isJsonMediaType = (mediaType: string): boolean => {
69
+ const normalized = mediaType.split(";")[0]?.trim().toLowerCase() ?? ""
70
+ return normalized === "application/json" || normalized.endsWith("+json")
71
+ }
72
+
73
+ const isBinaryMediaType = (document: Document, mediaType: string, value: unknown): boolean => {
74
+ const normalized = mediaType.split(";")[0]?.trim().toLowerCase() ?? ""
75
+ if (!isJsonMediaType(normalized) && !normalized.startsWith("text/")) return true
76
+ if (!isRecord(value)) return false
77
+ const schema = resolve(document, value.schema)
78
+ return isRecord(schema) && schema.format === "binary"
79
+ }
80
+
81
+ const jsonContent = (
82
+ content: Record<string, unknown>,
83
+ ): { readonly mediaType: string; readonly schema: unknown } | undefined => {
84
+ const entry = Object.entries(content).find(([mediaType]) => isJsonMediaType(mediaType))
85
+ return entry !== undefined && isRecord(entry[1]) ? { mediaType: entry[0], schema: entry[1].schema } : undefined
86
+ }
87
+
88
+ const isFlattenableObjectBody = (
89
+ schema: unknown,
90
+ requestRequired: boolean,
91
+ ): schema is Record<string, unknown> & { readonly properties: Record<string, unknown> } =>
92
+ isRecord(schema) &&
93
+ requestRequired &&
94
+ schema.type === "object" &&
95
+ isRecord(schema.properties) &&
96
+ schema.additionalProperties === false &&
97
+ schema.nullable !== true &&
98
+ schema.allOf === undefined &&
99
+ schema.anyOf === undefined &&
100
+ schema.oneOf === undefined
101
+
102
+ type PlannedField = Omit<InputField, "inputName">
103
+
104
+ const operationParameters = (
105
+ document: Document,
106
+ pathItem: Record<string, unknown>,
107
+ operation: Record<string, unknown>,
108
+ ): Parsed<ReadonlyArray<PlannedField>> => {
109
+ // Operation-level parameters override path-level ones sharing (location, name).
110
+ const declared = new Map<
111
+ string,
112
+ { readonly name: string; readonly location: string; readonly parameter: Record<string, unknown> }
113
+ >()
114
+ for (const raw of [...asArray(pathItem.parameters), ...asArray(operation.parameters)]) {
115
+ const resolved = resolve(document, raw)
116
+ if (!isRecord(resolved)) return { ok: false, reason: "parameter declaration is invalid or unresolved" }
117
+ const name = nonEmptyString(resolved.name)
118
+ const location = nonEmptyString(resolved.in)
119
+ if (name === undefined || location === undefined)
120
+ return { ok: false, reason: "parameter declaration is missing name or location" }
121
+ declared.set(`${location}:${name}`, { name, location, parameter: resolved })
122
+ }
123
+ const unordered: Array<PlannedField> = []
124
+ for (const item of declared.values()) {
125
+ const name = item.name
126
+ const location = item.location
127
+ const resolved = item.parameter
128
+ if (location === "cookie") return { ok: false, reason: `cookie parameter '${name}' is not supported` }
129
+ if (location !== "path" && location !== "query" && location !== "header") {
130
+ return { ok: false, reason: `parameter '${name}' uses unsupported location '${location}'` }
131
+ }
132
+ if (location === "header" && ignoredHeaderParameters.has(name.toLowerCase())) continue
133
+ if (resolved.schema === undefined && resolved.content === undefined) {
134
+ return { ok: false, reason: `parameter '${name}' declares neither schema nor content` }
135
+ }
136
+ if (resolved.content !== undefined)
137
+ return { ok: false, reason: `parameter '${name}' uses unsupported content encoding` }
138
+ if (resolved.style !== undefined && nonEmptyString(resolved.style) === undefined) {
139
+ return { ok: false, reason: `parameter '${name}' has an invalid style` }
140
+ }
141
+ if (resolved.explode !== undefined && typeof resolved.explode !== "boolean") {
142
+ return { ok: false, reason: `parameter '${name}' has an invalid explode value` }
143
+ }
144
+ if (resolved.allowReserved !== undefined && typeof resolved.allowReserved !== "boolean") {
145
+ return { ok: false, reason: `parameter '${name}' has an invalid allowReserved value` }
146
+ }
147
+ if (resolved.allowReserved === true)
148
+ return { ok: false, reason: `parameter '${name}' uses unsupported allowReserved encoding` }
149
+ const declaredStyle = nonEmptyString(resolved.style) ?? (location === "query" ? "form" : "simple")
150
+ if (location === "query" && declaredStyle !== "form" && declaredStyle !== "deepObject") {
151
+ return { ok: false, reason: `query parameter '${name}' uses unsupported style '${declaredStyle}'` }
152
+ }
153
+ if (location !== "query" && declaredStyle !== "simple") {
154
+ return { ok: false, reason: `${location} parameter '${name}' uses unsupported style '${declaredStyle}'` }
155
+ }
156
+ const style = declaredStyle === "deepObject" ? "deepObject" : declaredStyle === "form" ? "form" : "simple"
157
+ const explode = typeof resolved.explode === "boolean" ? resolved.explode : style === "form"
158
+ if (style === "deepObject" && !explode) {
159
+ return { ok: false, reason: `query parameter '${name}' uses deepObject with explode=false` }
160
+ }
161
+ const base = projectSchema(document, resolved.schema)
162
+ const description = nonEmptyString(resolved.description)
163
+ unordered.push({
164
+ name,
165
+ location,
166
+ required: resolved.required === true || location === "path",
167
+ style,
168
+ explode,
169
+ schema: {
170
+ ...base,
171
+ ...(base.description === undefined && description !== undefined ? { description } : {}),
172
+ },
173
+ })
174
+ }
175
+ return {
176
+ ok: true,
177
+ value: parameterLocations.flatMap((location) => unordered.filter((field) => field.location === location)),
178
+ }
179
+ }
180
+
181
+ const operationBody = (
182
+ document: Document,
183
+ operation: Record<string, unknown>,
184
+ ): Parsed<{ readonly fields: ReadonlyArray<PlannedField>; readonly body: Body | undefined }> => {
185
+ const resolved = resolve(document, operation.requestBody)
186
+ if (!isRecord(resolved)) return { ok: true, value: { fields: [], body: undefined } }
187
+ const content = isRecord(resolved.content) ? resolved.content : {}
188
+ const selected = jsonContent(content)
189
+ if (selected === undefined) {
190
+ return {
191
+ ok: false,
192
+ reason: `request body has no JSON content (declared: ${Object.keys(content).join(", ") || "none"})`,
193
+ }
194
+ }
195
+ const schema = resolve(document, selected.schema)
196
+ const required = resolved.required === true
197
+ if (!isFlattenableObjectBody(schema, required)) {
198
+ return {
199
+ ok: true,
200
+ value: {
201
+ fields: [
202
+ {
203
+ name: "body",
204
+ location: "body",
205
+ required,
206
+ schema: projectSchema(document, selected.schema),
207
+ style: undefined,
208
+ explode: undefined,
209
+ },
210
+ ],
211
+ body: { required, mode: "value", mediaType: selected.mediaType },
212
+ },
213
+ }
214
+ }
215
+ const requiredProperties = new Set(
216
+ Array.isArray(schema.required) ? schema.required.filter((item): item is string => typeof item === "string") : [],
217
+ )
218
+ return {
219
+ ok: true,
220
+ value: {
221
+ fields: Object.entries(schema.properties).map(([name, value]) => ({
222
+ name,
223
+ location: "body" as const,
224
+ required: required && requiredProperties.has(name),
225
+ schema: projectSchema(document, value),
226
+ style: undefined,
227
+ explode: undefined,
228
+ })),
229
+ body: { required, mode: "object", mediaType: selected.mediaType },
230
+ },
231
+ }
232
+ }
233
+
234
+ export const operationInput = (
235
+ document: Document,
236
+ pathItem: Record<string, unknown>,
237
+ operation: Record<string, unknown>,
238
+ ): Parsed<OperationInput> => {
239
+ const parameters = operationParameters(document, pathItem, operation)
240
+ if (!parameters.ok) return parameters
241
+ const requestBody = operationBody(document, operation)
242
+ if (!requestBody.ok) return requestBody
243
+ const fields = [...parameters.value, ...requestBody.value.fields]
244
+
245
+ const conflicts = new Set(
246
+ [...Map.groupBy(fields, (field) => field.name)]
247
+ .filter(([, matches]) => new Set(matches.map((field) => field.location)).size > 1)
248
+ .map(([name]) => name),
249
+ )
250
+ const used = new Set<string>()
251
+ return {
252
+ ok: true,
253
+ value: {
254
+ fields: fields.map((field) => {
255
+ const visibleName = isBlockedMember(field.name) ? `${field.name}_2` : field.name
256
+ const base = conflicts.has(field.name) ? `${field.location}_${visibleName}` : visibleName
257
+ const next = (index: number): string => {
258
+ const candidate = index === 1 ? base : `${base}_${index}`
259
+ return used.has(candidate) ? next(index + 1) : candidate
260
+ }
261
+ const inputName = next(1)
262
+ used.add(inputName)
263
+ return { ...field, inputName }
264
+ }),
265
+ body: requestBody.value.body,
266
+ },
267
+ }
268
+ }
269
+
270
+ export const inputSchema = (
271
+ fields: ReadonlyArray<InputField>,
272
+ definitions: Readonly<Record<string, JsonSchema>>,
273
+ ): JsonSchema => {
274
+ const required = fields.filter((field) => field.required).map((field) => field.inputName)
275
+ return withDefinitions(
276
+ {
277
+ type: "object",
278
+ properties: Object.fromEntries(fields.map((field) => [field.inputName, field.schema])),
279
+ ...(required.length === 0 ? {} : { required }),
280
+ },
281
+ definitions,
282
+ )
283
+ }
284
+
285
+ const successfulResponses = (
286
+ document: Document,
287
+ operation: Record<string, unknown>,
288
+ ): Parsed<ReadonlyArray<Record<string, unknown>>> => {
289
+ if (!isRecord(operation.responses)) return { ok: true, value: [] }
290
+ const entries = Object.entries(operation.responses)
291
+ const selected = [
292
+ ...entries.filter(([status]) => /^2\d\d$/.test(status)).sort(([a], [b]) => a.localeCompare(b)),
293
+ ...entries.filter(([status]) => status.toUpperCase() === "2XX"),
294
+ ]
295
+ const responses: Array<Record<string, unknown>> = []
296
+ for (const [, value] of selected) {
297
+ const resolved = resolve(document, value)
298
+ if (!isRecord(resolved) || nonEmptyString(resolved.$ref) !== undefined) {
299
+ return { ok: false, reason: "successful response declaration is invalid or unresolved" }
300
+ }
301
+ responses.push(resolved)
302
+ }
303
+ return { ok: true, value: responses }
304
+ }
305
+
306
+ export const operationOutput = (
307
+ document: Document,
308
+ operation: Record<string, unknown>,
309
+ definitions: Readonly<Record<string, JsonSchema>>,
310
+ ): Parsed<JsonSchema | undefined> => {
311
+ if (operation["x-websocket"] === true) return { ok: false, reason: "WebSocket operations are not supported" }
312
+ const responses = successfulResponses(document, operation)
313
+ if (!responses.ok) return responses
314
+ const streams = responses.value.some(
315
+ (response) =>
316
+ isRecord(response.content) &&
317
+ Object.keys(response.content).some(
318
+ (mediaType) => mediaType.split(";")[0]?.trim().toLowerCase() === "text/event-stream",
319
+ ),
320
+ )
321
+ if (streams) return { ok: false, reason: "SSE operations are not supported" }
322
+ const binary = responses.value.some(
323
+ (response) =>
324
+ isRecord(response.content) &&
325
+ Object.entries(response.content).some(([mediaType, value]) => isBinaryMediaType(document, mediaType, value)),
326
+ )
327
+ if (binary) return { ok: false, reason: "binary responses are not supported" }
328
+
329
+ const outcomes: Array<JsonSchema> = []
330
+ for (const response of responses.value) {
331
+ if (response.content !== undefined && !isRecord(response.content)) return { ok: true, value: undefined }
332
+ const content = isRecord(response.content) ? response.content : {}
333
+ if (Object.keys(content).length === 0) {
334
+ outcomes.push({ type: "null" })
335
+ continue
336
+ }
337
+ for (const [mediaType, value] of Object.entries(content)) {
338
+ if (!isJsonMediaType(mediaType)) {
339
+ outcomes.push({ type: "string" })
340
+ continue
341
+ }
342
+ if (!isRecord(value) || value.schema === undefined) return { ok: true, value: undefined }
343
+ outcomes.push(projectSchema(document, value.schema))
344
+ }
345
+ }
346
+ if (outcomes.length === 0) return { ok: true, value: undefined }
347
+ return {
348
+ ok: true,
349
+ value: withDefinitions(outcomes.length === 1 ? (outcomes[0] ?? {}) : { anyOf: outcomes }, definitions),
350
+ }
351
+ }
352
+
353
+ const sanitizeOperationSegment = (raw: string): string => {
354
+ const base =
355
+ raw
356
+ .replaceAll(/[^A-Za-z0-9_$]+/g, "_")
357
+ .replace(/^_+|_+$/g, "")
358
+ .replace(/^([0-9])/, "_$1") || "operation"
359
+ return isBlockedMember(base) ? `${base}_2` : base
360
+ }
361
+
362
+ const fallbackOperationId = (method: string, path: string): string =>
363
+ [
364
+ method,
365
+ ...path
366
+ .split("/")
367
+ .filter((part) => part !== "")
368
+ .flatMap((part) => (part.startsWith("{") && part.endsWith("}") ? ["by", part.slice(1, -1)] : [part]))
369
+ .flatMap((part) => part.split(/[^A-Za-z0-9]+/).filter((word) => word !== "")),
370
+ ]
371
+ .map((word, index) => {
372
+ const lower = word.toLowerCase()
373
+ return index === 0 ? lower : `${lower.charAt(0).toUpperCase()}${lower.slice(1)}`
374
+ })
375
+ .join("")
376
+
377
+ export const operationPath = (
378
+ method: string,
379
+ path: string,
380
+ operation: Record<string, unknown>,
381
+ used: ReadonlySet<string>,
382
+ namespaces: ReadonlySet<string>,
383
+ ): ReadonlyArray<string> => {
384
+ const raw = nonEmptyString(operation.operationId)
385
+ const segments = (raw === undefined ? [fallbackOperationId(method, path)] : raw.split(".")).map(
386
+ sanitizeOperationSegment,
387
+ )
388
+ if (isOperationPathAvailable(segments, used, namespaces)) return segments
389
+ const conflict = segments.slice(0, -1).findIndex((_, index) => used.has(segments.slice(0, index + 1).join(".")))
390
+ if (conflict >= 0 && conflict + 1 < segments.length) {
391
+ const collapsed = segments.flatMap((segment, index) => {
392
+ if (index === conflict) {
393
+ const next = segments[index + 1] ?? ""
394
+ return [`${segment}${next.charAt(0).toUpperCase()}${next.slice(1)}`]
395
+ }
396
+ return index === conflict + 1 ? [] : [segment]
397
+ })
398
+ if (isOperationPathAvailable(collapsed, used, namespaces)) return collapsed
399
+ }
400
+ const fallback = segments.join("_")
401
+ const next = (index: number): string => {
402
+ const candidate = `${fallback}_${index}`
403
+ return isOperationPathAvailable([candidate], used, namespaces) ? candidate : next(index + 1)
404
+ }
405
+ return [next(2)]
406
+ }
407
+
408
+ const isOperationPathAvailable = (
409
+ segments: ReadonlyArray<string>,
410
+ used: ReadonlySet<string>,
411
+ namespaces: ReadonlySet<string>,
412
+ ): boolean => {
413
+ const key = segments.join(".")
414
+ if (used.has(key) || namespaces.has(key)) return false
415
+ return segments.slice(0, -1).every((_, index) => !used.has(segments.slice(0, index + 1).join(".")))
416
+ }
417
+
418
+ export const specServerUrl = (source: Record<string, unknown>): Parsed<string> => {
419
+ const server = asArray(source.servers).find(isRecord)
420
+ const url = server === undefined ? undefined : nonEmptyString(server.url)
421
+ if (url === undefined) return { ok: false, reason: "spec declares no servers; pass baseUrl" }
422
+ if (/\{[^{}]+\}/.test(url)) {
423
+ return { ok: false, reason: `server URL '${url}' is not an absolute URL; pass baseUrl` }
424
+ }
425
+ return validateBaseUrl(url)
426
+ }
427
+
428
+ export const validateBaseUrl = (value: string): Parsed<string> => {
429
+ if (!/^https?:\/\//i.test(value)) return { ok: false, reason: `server URL '${value}' is not an absolute HTTP(S) URL` }
430
+ const url = URL.parse(value)
431
+ if (url === null || (url.protocol !== "http:" && url.protocol !== "https:")) {
432
+ return { ok: false, reason: `server URL '${value}' is not an absolute HTTP(S) URL` }
433
+ }
434
+ if (url.search !== "" || url.hash !== "") {
435
+ return { ok: false, reason: `server URL '${value}' contains an unsupported query string or fragment` }
436
+ }
437
+ return { ok: true, value }
438
+ }
439
+
440
+ export const securityRequirements = (value: unknown): Parsed<ReadonlyArray<SecurityRequirement>> => {
441
+ if (value === undefined) return { ok: true, value: [] }
442
+ if (!Array.isArray(value)) return { ok: false, reason: "security declaration is not an array" }
443
+ const requirements: Array<SecurityRequirement> = []
444
+ for (const item of value) {
445
+ if (!isRecord(item)) return { ok: false, reason: "security requirement is not an object" }
446
+ const requirement = Object.create(null) as Record<string, ReadonlyArray<string>>
447
+ for (const [name, scopes] of Object.entries(item)) {
448
+ if (!Array.isArray(scopes)) return { ok: false, reason: "security requirement scopes are not string arrays" }
449
+ const parsed = scopes.filter((scope): scope is string => typeof scope === "string")
450
+ if (parsed.length !== scopes.length) {
451
+ return { ok: false, reason: "security requirement scopes are not string arrays" }
452
+ }
453
+ requirement[name] = parsed
454
+ }
455
+ requirements.push(requirement)
456
+ }
457
+ return { ok: true, value: requirements }
458
+ }
459
+
460
+ export const operationSecurityRequirements = (
461
+ value: unknown,
462
+ defaults: Parsed<ReadonlyArray<SecurityRequirement>>,
463
+ schemes: Readonly<Record<string, SecurityScheme>>,
464
+ ): Parsed<ReadonlyArray<SecurityRequirement>> => {
465
+ const parsed = value === undefined ? defaults : securityRequirements(value)
466
+ if (!parsed.ok) return parsed
467
+ const supported = parsed.value.filter((requirement) =>
468
+ Object.keys(requirement).every((name) => {
469
+ const scheme = own(schemes, name)
470
+ return scheme !== undefined && !(scheme.type === "apiKey" && scheme.in === "cookie")
471
+ }),
472
+ )
473
+ if (parsed.value.length === 0 || supported.length > 0) return { ok: true, value: supported }
474
+
475
+ const names = [...new Set(parsed.value.flatMap((requirement) => Object.keys(requirement)))]
476
+ const cookieScheme = names.find((name) => {
477
+ const definition = own(schemes, name)
478
+ return definition?.type === "apiKey" && definition.in === "cookie"
479
+ })
480
+ return {
481
+ ok: false,
482
+ reason:
483
+ cookieScheme === undefined
484
+ ? `security requirement references missing or malformed scheme: ${names.join(", ")}`
485
+ : `cookie authentication '${cookieScheme}' is not supported`,
486
+ }
487
+ }
488
+
489
+ export const securitySchemes = (document: Document): Readonly<Record<string, SecurityScheme>> => {
490
+ const components = isRecord(document.components) ? document.components : {}
491
+ const declared = isRecord(components.securitySchemes) ? components.securitySchemes : {}
492
+ return Object.fromEntries(
493
+ Object.entries(declared).flatMap<readonly [string, SecurityScheme]>(([name, value]) => {
494
+ const resolved = resolve(document, value)
495
+ if (!isRecord(resolved)) return []
496
+ const type = nonEmptyString(resolved.type)
497
+ if (type === "apiKey") {
498
+ const carrier = nonEmptyString(resolved.in)
499
+ const parameter = nonEmptyString(resolved.name)
500
+ if (parameter === undefined || (carrier !== "header" && carrier !== "query" && carrier !== "cookie")) return []
501
+ return [[name, { type, name: parameter, in: carrier }] as const]
502
+ }
503
+ if (type === "http") {
504
+ const scheme = nonEmptyString(resolved.scheme)?.toLowerCase()
505
+ return scheme === undefined ? [] : [[name, { type, scheme }] as const]
506
+ }
507
+ if (type === "oauth2" || type === "openIdConnect") return [[name, { type }] as const]
508
+ return []
509
+ }),
510
+ )
511
+ }
@@ -0,0 +1,112 @@
1
+ import { Effect } from "effect"
2
+ import { HttpClient } from "effect/unstable/http"
3
+ import type { Definition, JsonSchema } from "../tool.js"
4
+
5
+ /** A parsed OpenAPI 3.x document. YAML must be parsed by the host. */
6
+ export type Document = Record<string, unknown>
7
+
8
+ /** The operation identity handed to auth resolution and errors. */
9
+ export type Operation = {
10
+ readonly operationId: string | undefined
11
+ readonly method: string
12
+ readonly path: string
13
+ readonly summary: string | undefined
14
+ readonly description: string | undefined
15
+ }
16
+
17
+ /** A resolved OpenAPI security scheme from `components.securitySchemes`. */
18
+ export type SecurityScheme =
19
+ | { readonly type: "apiKey"; readonly name: string; readonly in: "header" | "query" | "cookie" }
20
+ | { readonly type: "http"; readonly scheme: string }
21
+ | { readonly type: "oauth2" }
22
+ | { readonly type: "openIdConnect" }
23
+
24
+ /**
25
+ * Credential material returned by a host auth resolver. The carrier for `apiKey`
26
+ * comes from the scheme definition, not the credential. `header` is the escape
27
+ * hatch for nonstandard schemes.
28
+ */
29
+ export type Credential =
30
+ | { readonly type: "bearer"; readonly token: string }
31
+ | { readonly type: "basic"; readonly username: string; readonly password: string }
32
+ | { readonly type: "apiKey"; readonly value: string }
33
+ | { readonly type: "header"; readonly name: string; readonly value: string }
34
+
35
+ /**
36
+ * Resolves credential material for one named security scheme at call time.
37
+ * `undefined` means unavailable, try the next OR alternative; a failure aborts
38
+ * the call rather than falling through.
39
+ */
40
+ export type AuthResolver = (context: {
41
+ readonly name: string
42
+ readonly definition: SecurityScheme
43
+ readonly scopes: ReadonlyArray<string>
44
+ readonly operation: Operation
45
+ }) => Effect.Effect<Credential | undefined, unknown>
46
+
47
+ export type Options = {
48
+ readonly spec: Document
49
+ /** Overrides all document, path, and operation `servers`. Required when no applicable absolute server URL exists. */
50
+ readonly baseUrl?: string | undefined
51
+ /** Host credential resolution, keyed by security scheme name. */
52
+ readonly auth?: { readonly resolve: AuthResolver } | undefined
53
+ /** Static headers on every request. Not model-visible; declared header params may override them, auth always wins. */
54
+ readonly headers?: Readonly<Record<string, string>> | undefined
55
+ }
56
+
57
+ /** An operation that could not be represented as a tool, and why. */
58
+ export type Skipped = {
59
+ readonly method: string
60
+ readonly path: string
61
+ readonly reason: string
62
+ }
63
+
64
+ export type Tools = { [name: string]: Definition<HttpClient.HttpClient> | Tools }
65
+
66
+ export type Result = {
67
+ /** Tool subtree; the host places it under a key in its `tools` tree. */
68
+ readonly tools: Tools
69
+ readonly skipped: ReadonlyArray<Skipped>
70
+ }
71
+
72
+ export type Parsed<T> = { readonly ok: true; readonly value: T } | { readonly ok: false; readonly reason: string }
73
+
74
+ export type InputLocation = "path" | "query" | "header" | "body"
75
+
76
+ export type InputField = {
77
+ /** Model-visible field name after cross-location collision handling. */
78
+ readonly inputName: string
79
+ /** Original parameter or body-property name used on the wire. */
80
+ readonly name: string
81
+ readonly location: InputLocation
82
+ readonly required: boolean
83
+ readonly schema: JsonSchema
84
+ readonly style: "simple" | "form" | "deepObject" | undefined
85
+ readonly explode: boolean | undefined
86
+ }
87
+
88
+ export type Body = { readonly required: boolean; readonly mode: "object" | "value"; readonly mediaType: string }
89
+
90
+ export type OperationInput = {
91
+ readonly fields: ReadonlyArray<InputField>
92
+ readonly body: Body | undefined
93
+ }
94
+
95
+ /** One OR alternative: scheme name -> required scopes. Empty object = unauthenticated is acceptable. */
96
+ export type SecurityRequirement = Readonly<Record<string, ReadonlyArray<string>>>
97
+
98
+ export type Plan = {
99
+ readonly operation: Operation
100
+ readonly url: string
101
+ readonly fields: ReadonlyArray<InputField>
102
+ readonly body: Body | undefined
103
+ readonly security: ReadonlyArray<SecurityRequirement>
104
+ readonly schemes: Readonly<Record<string, SecurityScheme>>
105
+ readonly auth: { readonly resolve: AuthResolver } | undefined
106
+ readonly headers: Readonly<Record<string, string>>
107
+ }
108
+
109
+ export type AppliedAuth = {
110
+ readonly headers: Readonly<Record<string, string>>
111
+ readonly query: Readonly<Record<string, string>>
112
+ }
@@ -0,0 +1,51 @@
1
+ export const arrayMethods = new Set([
2
+ "map",
3
+ "filter",
4
+ "find",
5
+ "findIndex",
6
+ "findLast",
7
+ "findLastIndex",
8
+ "some",
9
+ "every",
10
+ "includes",
11
+ "join",
12
+ "reduce",
13
+ "reduceRight",
14
+ "flatMap",
15
+ "forEach",
16
+ "sort",
17
+ "toSorted",
18
+ "slice",
19
+ "concat",
20
+ "indexOf",
21
+ "lastIndexOf",
22
+ "at",
23
+ "flat",
24
+ "reverse",
25
+ "toReversed",
26
+ "with",
27
+ "push",
28
+ "pop",
29
+ "shift",
30
+ "unshift",
31
+ "splice",
32
+ "fill",
33
+ "copyWithin",
34
+ "keys",
35
+ "values",
36
+ "entries",
37
+ ])
38
+
39
+ export const mapMethods = new Set(["get", "set", "has", "delete", "clear", "forEach", "keys", "values", "entries"])
40
+
41
+ export const setMethods = new Set(["add", "has", "delete", "clear", "forEach", "keys", "values", "entries"])
42
+
43
+ export const spreadItems = (value: unknown): Array<unknown> | undefined => {
44
+ if (Array.isArray(value)) return value
45
+ if (typeof value === "string") return Array.from(value)
46
+ if (value instanceof SandboxMap) return Array.from(value.map.entries(), ([key, item]) => [key, item])
47
+ if (value instanceof SandboxSet) return Array.from(value.set.values())
48
+ if (value instanceof SandboxURLSearchParams) return Array.from(value.params.entries(), ([key, item]) => [key, item])
49
+ return undefined
50
+ }
51
+ import { SandboxMap, SandboxSet, SandboxURLSearchParams } from "../values.js"
@@ -0,0 +1,4 @@
1
+ export const consoleMethods = new Set(["log", "info", "debug", "warn", "error", "dir", "table"])
2
+
3
+ /** Console formatting recursion ceiling; deeper values render as "...". */
4
+ export const MAX_CONSOLE_DEPTH = 32