@executor-js/plugin-openapi 0.1.0 → 1.4.20

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.
Files changed (41) hide show
  1. package/dist/AddOpenApiSource-FLMNI742.js +19 -0
  2. package/dist/AddOpenApiSource-FLMNI742.js.map +1 -0
  3. package/dist/EditOpenApiSource-I4NIGIIJ.js +665 -0
  4. package/dist/EditOpenApiSource-I4NIGIIJ.js.map +1 -0
  5. package/dist/OpenApiSourceSummary-CM46DB4L.js +122 -0
  6. package/dist/OpenApiSourceSummary-CM46DB4L.js.map +1 -0
  7. package/dist/api/group.d.ts +224 -16
  8. package/dist/api/index.d.ts +634 -0
  9. package/dist/chunk-E7PZ2QGD.js +1303 -0
  10. package/dist/chunk-E7PZ2QGD.js.map +1 -0
  11. package/dist/chunk-GFQUEZUW.js +216 -0
  12. package/dist/chunk-GFQUEZUW.js.map +1 -0
  13. package/dist/chunk-OZ67JNID.js +1447 -0
  14. package/dist/chunk-OZ67JNID.js.map +1 -0
  15. package/dist/chunk-TGDT6QCH.js +1120 -0
  16. package/dist/chunk-TGDT6QCH.js.map +1 -0
  17. package/dist/client.js +165 -0
  18. package/dist/client.js.map +1 -0
  19. package/dist/core.js +8 -10
  20. package/dist/index.js +2 -1
  21. package/dist/react/AddOpenApiSource.d.ts +1 -1
  22. package/dist/react/OpenApiSourceDetailsFields.d.ts +18 -0
  23. package/dist/react/atoms.d.ts +320 -15
  24. package/dist/react/client.d.ts +226 -15
  25. package/dist/sdk/extract.d.ts +54 -3
  26. package/dist/sdk/index.d.ts +1 -1
  27. package/dist/sdk/invoke.d.ts +48 -4
  28. package/dist/sdk/openapi-utils.d.ts +4 -3
  29. package/dist/sdk/parse.d.ts +1 -1
  30. package/dist/sdk/parse.test.d.ts +1 -0
  31. package/dist/sdk/plugin.d.ts +247 -128
  32. package/dist/sdk/preview.d.ts +201 -49
  33. package/dist/sdk/store.d.ts +155 -45
  34. package/dist/sdk/types.d.ts +204 -137
  35. package/dist/sdk/usage-scope-isolation.test.d.ts +1 -0
  36. package/dist/testing/index.d.ts +34 -0
  37. package/dist/testing.js +56 -0
  38. package/dist/testing.js.map +1 -0
  39. package/package.json +16 -4
  40. package/dist/chunk-RBE3CVB4.js +0 -2837
  41. package/dist/chunk-RBE3CVB4.js.map +0 -1
@@ -0,0 +1,1447 @@
1
+ import {
2
+ ConfiguredHeaderBinding,
3
+ ConfiguredHeaderValue,
4
+ HeaderValue,
5
+ InvocationResult,
6
+ OAuth2SourceConfig,
7
+ OpenApiCredentialInput,
8
+ OpenApiExtractionError,
9
+ OpenApiInvocationError,
10
+ OpenApiOAuthError,
11
+ OpenApiSourceBindingRef,
12
+ OperationBinding,
13
+ extract,
14
+ makeDefaultOpenapiStore,
15
+ openapiSchema,
16
+ parse,
17
+ previewSpec,
18
+ resolveBaseUrl,
19
+ resolveSpecText
20
+ } from "./chunk-E7PZ2QGD.js";
21
+
22
+ // src/sdk/invoke.ts
23
+ import { Effect, Layer, Option } from "effect";
24
+ import { HttpClient, HttpClientRequest } from "effect/unstable/http";
25
+ var CONTAINER_KEYS = {
26
+ path: ["path", "pathParams", "params"],
27
+ query: ["query", "queryParams", "params"],
28
+ header: ["headers", "header"],
29
+ cookie: ["cookies", "cookie"]
30
+ };
31
+ var readParamValue = (args, param) => {
32
+ const direct = args[param.name];
33
+ if (direct !== void 0) return direct;
34
+ for (const key of CONTAINER_KEYS[param.location] ?? []) {
35
+ const container = args[key];
36
+ if (typeof container === "object" && container !== null && !Array.isArray(container)) {
37
+ const nested = container[param.name];
38
+ if (nested !== void 0) return nested;
39
+ }
40
+ }
41
+ return void 0;
42
+ };
43
+ var resolvePath = Effect.fn("OpenApi.resolvePath")(function* (pathTemplate, args, parameters) {
44
+ let resolved = pathTemplate;
45
+ for (const param of parameters) {
46
+ if (param.location !== "path") continue;
47
+ const value = readParamValue(args, param);
48
+ if (value === void 0 || value === null) {
49
+ if (param.required) {
50
+ return yield* new OpenApiInvocationError({
51
+ message: `Missing required path parameter: ${param.name}`,
52
+ statusCode: Option.none()
53
+ });
54
+ }
55
+ continue;
56
+ }
57
+ resolved = resolved.replaceAll(`{${param.name}}`, encodeURIComponent(String(value)));
58
+ }
59
+ const remaining = [...resolved.matchAll(/\{([^{}]+)\}/g)].map((m) => m[1]).filter((v) => typeof v === "string");
60
+ for (const name of remaining) {
61
+ const value = args[name];
62
+ if (value !== void 0 && value !== null) {
63
+ resolved = resolved.replaceAll(`{${name}}`, encodeURIComponent(String(value)));
64
+ }
65
+ }
66
+ const unresolved = [...resolved.matchAll(/\{([^{}]+)\}/g)].map((m) => m[1]).filter((v) => typeof v === "string");
67
+ if (unresolved.length > 0) {
68
+ return yield* new OpenApiInvocationError({
69
+ message: `Unresolved path parameters: ${[...new Set(unresolved)].join(", ")}`,
70
+ statusCode: Option.none()
71
+ });
72
+ }
73
+ return resolved;
74
+ });
75
+ var resolveHeaders = (headers, secrets) => {
76
+ const entries = Object.entries(headers);
77
+ const secretCount = entries.reduce(
78
+ (acc, [, value]) => typeof value === "string" ? acc : acc + 1,
79
+ 0
80
+ );
81
+ return Effect.gen(function* () {
82
+ const values = yield* Effect.all(
83
+ entries.map(
84
+ ([name, value]) => typeof value === "string" ? Effect.succeed({ name, value }) : secrets.get(value.secretId).pipe(
85
+ Effect.catchTag(
86
+ "SecretOwnedByConnectionError",
87
+ () => Effect.fail(
88
+ new OpenApiInvocationError({
89
+ message: `Failed to resolve secret "${value.secretId}" for header "${name}"`,
90
+ statusCode: Option.none()
91
+ })
92
+ )
93
+ ),
94
+ Effect.flatMap(
95
+ (secret) => secret === null ? Effect.fail(
96
+ new OpenApiInvocationError({
97
+ message: `Failed to resolve secret "${value.secretId}" for header "${name}"`,
98
+ statusCode: Option.none()
99
+ })
100
+ ) : Effect.succeed({
101
+ name,
102
+ value: value.prefix ? `${value.prefix}${secret}` : secret
103
+ })
104
+ )
105
+ )
106
+ ),
107
+ { concurrency: "unbounded" }
108
+ );
109
+ const resolved = {};
110
+ for (const { name, value } of values) resolved[name] = value;
111
+ return resolved;
112
+ }).pipe(
113
+ Effect.withSpan("plugin.openapi.secret.resolve", {
114
+ attributes: {
115
+ "plugin.openapi.headers.total": entries.length,
116
+ "plugin.openapi.headers.secret_count": secretCount
117
+ }
118
+ })
119
+ );
120
+ };
121
+ var applyHeaders = (request, headers) => {
122
+ let req = request;
123
+ for (const [name, value] of Object.entries(headers)) {
124
+ req = HttpClientRequest.setHeader(req, name, value);
125
+ }
126
+ return req;
127
+ };
128
+ var normalizeContentType = (ct) => ct?.split(";")[0]?.trim().toLowerCase() ?? "";
129
+ var isJsonContentType = (ct) => {
130
+ const normalized = normalizeContentType(ct);
131
+ if (!normalized) return false;
132
+ return normalized === "application/json" || normalized.includes("+json") || normalized.includes("json");
133
+ };
134
+ var isFormUrlEncoded = (ct) => normalizeContentType(ct) === "application/x-www-form-urlencoded";
135
+ var isMultipartFormData = (ct) => normalizeContentType(ct).startsWith("multipart/form-data");
136
+ var isXmlContentType = (ct) => {
137
+ const normalized = normalizeContentType(ct);
138
+ if (!normalized) return false;
139
+ return normalized === "application/xml" || normalized === "text/xml" || normalized.endsWith("+xml");
140
+ };
141
+ var isTextContentType = (ct) => normalizeContentType(ct).startsWith("text/");
142
+ var isOctetStream = (ct) => normalizeContentType(ct) === "application/octet-stream";
143
+ var toUint8Array = (value) => {
144
+ if (value instanceof Uint8Array) return value;
145
+ if (value instanceof ArrayBuffer) return new Uint8Array(value);
146
+ if (ArrayBuffer.isView(value)) {
147
+ const view = value;
148
+ return new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
149
+ }
150
+ if (Array.isArray(value) && value.every((v) => typeof v === "number")) {
151
+ return new Uint8Array(value);
152
+ }
153
+ return null;
154
+ };
155
+ var toArrayBuffer = (bytes) => {
156
+ const copy = new ArrayBuffer(bytes.byteLength);
157
+ new Uint8Array(copy).set(bytes);
158
+ return copy;
159
+ };
160
+ var DEFAULT_FORM_STYLE = {
161
+ style: "form",
162
+ explode: true,
163
+ allowReserved: false
164
+ };
165
+ var resolveStyleExplode = (e) => {
166
+ if (!e) return DEFAULT_FORM_STYLE;
167
+ return {
168
+ style: Option.getOrElse(e.style, () => DEFAULT_FORM_STYLE.style),
169
+ explode: Option.getOrElse(e.explode, () => DEFAULT_FORM_STYLE.explode),
170
+ allowReserved: Option.getOrElse(e.allowReserved, () => DEFAULT_FORM_STYLE.allowReserved)
171
+ };
172
+ };
173
+ var RESERVED_UNENCODED_RE = /[A-Za-z0-9\-._~:/?#[\]@!$&'()*+,;=]/;
174
+ var encodeFormValue = (v, allowReserved) => {
175
+ const raw = typeof v === "object" && v !== null ? JSON.stringify(v) : String(v);
176
+ if (!allowReserved) return encodeURIComponent(raw);
177
+ let out = "";
178
+ for (const ch of raw) {
179
+ out += RESERVED_UNENCODED_RE.test(ch) ? ch : encodeURIComponent(ch);
180
+ }
181
+ return out;
182
+ };
183
+ var serializeFormUrlEncoded = (value, encoding) => {
184
+ const parts = [];
185
+ for (const [key, raw] of Object.entries(value)) {
186
+ if (raw === void 0 || raw === null) continue;
187
+ const { style, explode, allowReserved } = resolveStyleExplode(encoding?.[key]);
188
+ const encKey = encodeURIComponent(key);
189
+ if (Array.isArray(raw)) {
190
+ if (explode) {
191
+ for (const v of raw) {
192
+ parts.push(`${encKey}=${encodeFormValue(v, allowReserved)}`);
193
+ }
194
+ } else {
195
+ const sep = style === "spaceDelimited" ? " " : style === "pipeDelimited" ? "|" : ",";
196
+ parts.push(
197
+ `${encKey}=${encodeFormValue(
198
+ raw.map((v) => typeof v === "object" ? JSON.stringify(v) : String(v)).join(sep),
199
+ allowReserved
200
+ )}`
201
+ );
202
+ }
203
+ continue;
204
+ }
205
+ if (typeof raw === "object") {
206
+ const entries = Object.entries(raw).filter(
207
+ ([, v]) => v !== void 0 && v !== null
208
+ );
209
+ if (style === "deepObject") {
210
+ for (const [subkey, subval] of entries) {
211
+ parts.push(
212
+ `${encodeURIComponent(`${key}[${subkey}]`)}=${encodeFormValue(subval, allowReserved)}`
213
+ );
214
+ }
215
+ } else if (explode) {
216
+ for (const [subkey, subval] of entries) {
217
+ parts.push(`${encodeURIComponent(subkey)}=${encodeFormValue(subval, allowReserved)}`);
218
+ }
219
+ } else {
220
+ const flat = entries.flatMap(([k, v]) => [
221
+ k,
222
+ typeof v === "object" ? JSON.stringify(v) : String(v)
223
+ ]);
224
+ parts.push(`${encKey}=${encodeFormValue(flat.join(","), allowReserved)}`);
225
+ }
226
+ continue;
227
+ }
228
+ parts.push(`${encKey}=${encodeFormValue(raw, allowReserved)}`);
229
+ }
230
+ return parts.join("&");
231
+ };
232
+ var coerceFormDataRecord = (value, encoding) => {
233
+ const out = {};
234
+ for (const [key, raw] of Object.entries(value)) {
235
+ if (raw === void 0 || raw === null) continue;
236
+ const partType = encoding?.[key] ? Option.getOrUndefined(encoding[key].contentType) : void 0;
237
+ if (partType) {
238
+ const isJson = partType.startsWith("application/json") || partType.includes("+json");
239
+ const serialized = typeof raw === "string" ? raw : isJson ? JSON.stringify(raw) : typeof raw === "object" ? JSON.stringify(raw) : String(raw);
240
+ out[key] = new Blob([serialized], { type: partType });
241
+ continue;
242
+ }
243
+ if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean" || raw instanceof Blob || typeof File !== "undefined" && raw instanceof File) {
244
+ out[key] = raw;
245
+ continue;
246
+ }
247
+ if (Array.isArray(raw)) {
248
+ out[key] = raw.map(
249
+ (v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean" || v instanceof Blob || typeof File !== "undefined" && v instanceof File ? v : JSON.stringify(v)
250
+ );
251
+ continue;
252
+ }
253
+ const bytes = toUint8Array(raw);
254
+ if (bytes) {
255
+ out[key] = new Blob([toArrayBuffer(bytes)]);
256
+ continue;
257
+ }
258
+ out[key] = JSON.stringify(raw);
259
+ }
260
+ return out;
261
+ };
262
+ var applyRequestBody = (request, contentType, bodyValue, encoding) => {
263
+ if (isJsonContentType(contentType)) {
264
+ if (typeof bodyValue === "string") {
265
+ return HttpClientRequest.bodyText(request, bodyValue, contentType);
266
+ }
267
+ return HttpClientRequest.bodyJsonUnsafe(request, bodyValue);
268
+ }
269
+ if (isFormUrlEncoded(contentType)) {
270
+ if (typeof bodyValue === "string") {
271
+ return HttpClientRequest.bodyText(request, bodyValue, contentType);
272
+ }
273
+ if (typeof bodyValue === "object" && bodyValue !== null && !Array.isArray(bodyValue)) {
274
+ const serialized = serializeFormUrlEncoded(bodyValue, encoding);
275
+ return HttpClientRequest.bodyText(request, serialized, contentType);
276
+ }
277
+ return HttpClientRequest.bodyUrlParams(
278
+ request,
279
+ bodyValue
280
+ );
281
+ }
282
+ if (isMultipartFormData(contentType)) {
283
+ if (bodyValue instanceof FormData) {
284
+ return HttpClientRequest.bodyFormData(request, bodyValue);
285
+ }
286
+ if (typeof bodyValue === "object" && bodyValue !== null) {
287
+ return HttpClientRequest.bodyFormDataRecord(
288
+ request,
289
+ coerceFormDataRecord(bodyValue, encoding)
290
+ );
291
+ }
292
+ return HttpClientRequest.bodyText(request, String(bodyValue), contentType);
293
+ }
294
+ if (isOctetStream(contentType)) {
295
+ const bytes2 = toUint8Array(bodyValue);
296
+ if (bytes2) return HttpClientRequest.bodyUint8Array(request, bytes2, contentType);
297
+ if (typeof bodyValue === "string") {
298
+ return HttpClientRequest.bodyText(request, bodyValue, contentType);
299
+ }
300
+ return HttpClientRequest.bodyText(request, JSON.stringify(bodyValue), contentType);
301
+ }
302
+ if (isXmlContentType(contentType) || isTextContentType(contentType)) {
303
+ if (typeof bodyValue === "string") {
304
+ return HttpClientRequest.bodyText(request, bodyValue, contentType);
305
+ }
306
+ const bytes2 = toUint8Array(bodyValue);
307
+ if (bytes2) return HttpClientRequest.bodyUint8Array(request, bytes2, contentType);
308
+ return HttpClientRequest.bodyText(request, JSON.stringify(bodyValue), contentType);
309
+ }
310
+ if (typeof bodyValue === "string") {
311
+ return HttpClientRequest.bodyText(request, bodyValue, contentType);
312
+ }
313
+ const bytes = toUint8Array(bodyValue);
314
+ if (bytes) return HttpClientRequest.bodyUint8Array(request, bytes, contentType);
315
+ return HttpClientRequest.bodyText(request, JSON.stringify(bodyValue), contentType);
316
+ };
317
+ var invoke = Effect.fn("OpenApi.invoke")(function* (operation, args, resolvedHeaders, sourceQueryParams = {}) {
318
+ const client = yield* HttpClient.HttpClient;
319
+ yield* Effect.annotateCurrentSpan({
320
+ "http.method": operation.method.toUpperCase(),
321
+ "http.route": operation.pathTemplate,
322
+ "plugin.openapi.method": operation.method.toUpperCase(),
323
+ "plugin.openapi.path_template": operation.pathTemplate,
324
+ "plugin.openapi.headers.resolved_count": Object.keys(resolvedHeaders).length
325
+ });
326
+ const resolvedPath = yield* resolvePath(operation.pathTemplate, args, operation.parameters);
327
+ const path = resolvedPath.startsWith("/") ? resolvedPath : `/${resolvedPath}`;
328
+ let request = HttpClientRequest.make(operation.method.toUpperCase())(path);
329
+ for (const [name, value] of Object.entries(sourceQueryParams)) {
330
+ request = HttpClientRequest.setUrlParam(request, name, value);
331
+ }
332
+ for (const param of operation.parameters) {
333
+ if (param.location !== "query") continue;
334
+ const value = readParamValue(args, param);
335
+ if (value === void 0 || value === null) continue;
336
+ request = HttpClientRequest.setUrlParam(request, param.name, String(value));
337
+ }
338
+ for (const param of operation.parameters) {
339
+ if (param.location !== "header") continue;
340
+ const value = readParamValue(args, param);
341
+ if (value === void 0 || value === null) continue;
342
+ request = HttpClientRequest.setHeader(request, param.name, String(value));
343
+ }
344
+ if (Option.isSome(operation.requestBody)) {
345
+ const rb = operation.requestBody.value;
346
+ const bodyValue = args.body ?? args.input;
347
+ if (bodyValue !== void 0) {
348
+ const contentsOpt = Option.getOrUndefined(rb.contents);
349
+ const requestedCt = typeof args.contentType === "string" ? args.contentType : void 0;
350
+ const selected = contentsOpt && requestedCt ? contentsOpt.find((c) => c.contentType === requestedCt) : void 0;
351
+ const chosenCt = selected?.contentType ?? rb.contentType;
352
+ const chosenEncoding = selected ? Option.getOrUndefined(selected.encoding) : contentsOpt && contentsOpt[0] ? Option.getOrUndefined(contentsOpt[0].encoding) : void 0;
353
+ request = applyRequestBody(request, chosenCt, bodyValue, chosenEncoding);
354
+ }
355
+ }
356
+ request = applyHeaders(request, resolvedHeaders);
357
+ const response = yield* client.execute(request).pipe(
358
+ Effect.mapError(
359
+ (err) => new OpenApiInvocationError({
360
+ message: "HTTP request failed",
361
+ statusCode: Option.none(),
362
+ cause: err
363
+ })
364
+ )
365
+ );
366
+ const status = response.status;
367
+ yield* Effect.annotateCurrentSpan({
368
+ "http.status_code": status
369
+ });
370
+ const responseHeaders = { ...response.headers };
371
+ const contentType = response.headers["content-type"] ?? null;
372
+ const mapBodyError = Effect.mapError(
373
+ (err) => new OpenApiInvocationError({
374
+ message: "Failed to read response body",
375
+ statusCode: Option.some(status),
376
+ cause: err
377
+ })
378
+ );
379
+ const responseBody = status === 204 ? null : isJsonContentType(contentType) ? yield* response.json.pipe(
380
+ Effect.catch(() => response.text),
381
+ mapBodyError
382
+ ) : yield* response.text.pipe(mapBodyError);
383
+ const ok = status >= 200 && status < 300;
384
+ return InvocationResult.make({
385
+ status,
386
+ headers: responseHeaders,
387
+ data: ok ? responseBody : null,
388
+ error: ok ? null : responseBody
389
+ });
390
+ });
391
+ var invokeWithLayer = (operation, args, baseUrl, resolvedHeaders, sourceQueryParams, httpClientLayer) => {
392
+ const clientWithBaseUrl = baseUrl ? Layer.effect(
393
+ HttpClient.HttpClient,
394
+ Effect.map(
395
+ Effect.service(HttpClient.HttpClient),
396
+ HttpClient.mapRequest(HttpClientRequest.prependUrl(baseUrl))
397
+ )
398
+ ).pipe(Layer.provide(httpClientLayer)) : httpClientLayer;
399
+ return invoke(operation, args, resolvedHeaders, sourceQueryParams).pipe(
400
+ Effect.provide(clientWithBaseUrl),
401
+ Effect.withSpan("plugin.openapi.invoke", {
402
+ attributes: {
403
+ "plugin.openapi.method": operation.method.toUpperCase(),
404
+ "plugin.openapi.path_template": operation.pathTemplate,
405
+ "plugin.openapi.base_url": baseUrl
406
+ }
407
+ })
408
+ );
409
+ };
410
+ var REQUIRE_APPROVAL = /* @__PURE__ */ new Set(["post", "put", "patch", "delete"]);
411
+ var annotationsForOperation = (method, pathTemplate) => {
412
+ const m = method.toLowerCase();
413
+ if (!REQUIRE_APPROVAL.has(m)) return {};
414
+ return {
415
+ requiresApproval: true,
416
+ approvalDescription: `${method.toUpperCase()} ${pathTemplate}`
417
+ };
418
+ };
419
+
420
+ // src/sdk/plugin.ts
421
+ import { Effect as Effect2, Option as Option2, Predicate, Schema } from "effect";
422
+ import {
423
+ ScopeId,
424
+ SecretId,
425
+ SourceDetectionResult,
426
+ StorageError,
427
+ definePlugin,
428
+ tool,
429
+ resolveSecretBackedMap
430
+ } from "@executor-js/sdk/core";
431
+ import {
432
+ headersToConfigValues
433
+ } from "@executor-js/config";
434
+
435
+ // src/sdk/definitions.ts
436
+ var splitWords = (value) => value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z0-9]+)/g, "$1 $2").replace(/[^a-zA-Z0-9]+/g, " ").trim().split(/\s+/).filter((part) => part.length > 0);
437
+ var normalizeWord = (value) => value.toLowerCase();
438
+ var toCamelCase = (value) => {
439
+ const words = splitWords(value).map(normalizeWord);
440
+ if (words.length === 0) return "tool";
441
+ const [first, ...rest] = words;
442
+ return `${first}${rest.map((p) => `${p[0]?.toUpperCase() ?? ""}${p.slice(1)}`).join("")}`;
443
+ };
444
+ var toPascalCase = (value) => {
445
+ const camel = toCamelCase(value);
446
+ return `${camel[0]?.toUpperCase() ?? ""}${camel.slice(1)}`;
447
+ };
448
+ var VERSION_SEGMENT_REGEX = /^v\d+(?:[._-]\d+)?$/i;
449
+ var IGNORED_PATH_SEGMENTS = /* @__PURE__ */ new Set(["api"]);
450
+ var pathSegmentsFromTemplate = (pathTemplate) => pathTemplate.split("/").map((s) => s.trim()).filter((s) => s.length > 0);
451
+ var isPathParameterSegment = (segment) => segment.startsWith("{") && segment.endsWith("}");
452
+ var normalizeGroupSegment = (value) => {
453
+ const candidate = value?.trim();
454
+ if (!candidate) return null;
455
+ return toCamelCase(candidate);
456
+ };
457
+ var deriveVersionSegment = (pathTemplate) => pathSegmentsFromTemplate(pathTemplate).map((s) => s.toLowerCase()).find((s) => VERSION_SEGMENT_REGEX.test(s));
458
+ var derivePathGroup = (pathTemplate) => {
459
+ for (const segment of pathSegmentsFromTemplate(pathTemplate)) {
460
+ const lower = segment.toLowerCase();
461
+ if (VERSION_SEGMENT_REGEX.test(lower)) continue;
462
+ if (IGNORED_PATH_SEGMENTS.has(lower)) continue;
463
+ if (isPathParameterSegment(segment)) continue;
464
+ return normalizeGroupSegment(segment) ?? "root";
465
+ }
466
+ return "root";
467
+ };
468
+ var splitOperationIdSegments = (value) => value.split(/[/.]+/).map((s) => s.trim()).filter((s) => s.length > 0);
469
+ var deriveLeafSeed = (operationId, group) => {
470
+ const segments = splitOperationIdSegments(operationId);
471
+ if (segments.length > 1) {
472
+ const [first, ...rest] = segments;
473
+ if ((normalizeGroupSegment(first) ?? first) === group && rest.length > 0) {
474
+ return rest.join(" ");
475
+ }
476
+ }
477
+ return operationId;
478
+ };
479
+ var fallbackLeafSeed = (method, pathTemplate, group) => {
480
+ const relevantSegments = pathSegmentsFromTemplate(pathTemplate).filter((s) => !VERSION_SEGMENT_REGEX.test(s.toLowerCase())).filter((s) => !IGNORED_PATH_SEGMENTS.has(s.toLowerCase())).filter((s) => !isPathParameterSegment(s)).map((s) => normalizeGroupSegment(s) ?? s).filter((s) => s !== group);
481
+ const segmentSuffix = relevantSegments.map((s) => toPascalCase(s)).join("");
482
+ return `${method}${segmentSuffix || "Operation"}`;
483
+ };
484
+ var deriveLeaf = (operationId, method, pathTemplate, group) => {
485
+ const preferred = toCamelCase(deriveLeafSeed(operationId, group));
486
+ if (preferred.length > 0 && preferred !== group) return preferred;
487
+ return toCamelCase(fallbackLeafSeed(method, pathTemplate, group));
488
+ };
489
+ var resolveCollisions = (definitions) => {
490
+ const staged = definitions.map((d) => ({ ...d }));
491
+ const applyFactory = (items, factory) => {
492
+ const byPath = /* @__PURE__ */ new Map();
493
+ for (const item of items) {
494
+ const bucket = byPath.get(item.toolPath) ?? [];
495
+ bucket.push(item);
496
+ byPath.set(item.toolPath, bucket);
497
+ }
498
+ for (const bucket of byPath.values()) {
499
+ if (bucket.length < 2) continue;
500
+ for (const d of bucket) {
501
+ d.toolPath = factory(d);
502
+ }
503
+ }
504
+ };
505
+ applyFactory(
506
+ staged,
507
+ (d) => d.versionSegment ? `${d.group}.${d.versionSegment}.${d.leaf}` : d.toolPath
508
+ );
509
+ applyFactory(staged, (d) => {
510
+ const prefix = d.versionSegment ? `${d.group}.${d.versionSegment}` : d.group;
511
+ return `${prefix}.${d.leaf}${toPascalCase(d.method)}`;
512
+ });
513
+ applyFactory(staged, (d) => {
514
+ const prefix = d.versionSegment ? `${d.group}.${d.versionSegment}` : d.group;
515
+ return `${prefix}.${d.leaf}${toPascalCase(d.method)}${d.operationHash.slice(0, 8)}`;
516
+ });
517
+ return staged.map((d) => ({
518
+ toolPath: d.toolPath,
519
+ group: d.group,
520
+ leaf: d.leaf,
521
+ operationIndex: d.operationIndex,
522
+ operation: d.operation
523
+ }));
524
+ };
525
+ var stableHash = (value) => {
526
+ const str = JSON.stringify(value, Object.keys(value).sort());
527
+ let hash = 0;
528
+ for (let i = 0; i < str.length; i++) {
529
+ hash = (hash << 5) - hash + str.charCodeAt(i) | 0;
530
+ }
531
+ return Math.abs(hash).toString(36).padStart(8, "0");
532
+ };
533
+ var compileToolDefinitions = (operations) => {
534
+ const raw = operations.map((op, index) => {
535
+ const operationId = op.operationId;
536
+ const group = normalizeGroupSegment(op.tags[0]) ?? derivePathGroup(op.pathTemplate);
537
+ const leaf = deriveLeaf(operationId, op.method, op.pathTemplate, group);
538
+ const versionSegment = deriveVersionSegment(op.pathTemplate);
539
+ const operationHash = stableHash({
540
+ method: op.method,
541
+ path: op.pathTemplate,
542
+ operationId
543
+ });
544
+ return {
545
+ toolPath: `${group}.${leaf}`,
546
+ group,
547
+ leaf,
548
+ versionSegment,
549
+ method: op.method,
550
+ operationHash,
551
+ operationIndex: index,
552
+ operation: op
553
+ };
554
+ });
555
+ return resolveCollisions(raw).sort((a, b) => a.toolPath.localeCompare(b.toolPath));
556
+ };
557
+
558
+ // src/sdk/plugin.ts
559
+ var PreviewSpecInputSchema = Schema.Struct({
560
+ spec: Schema.String,
561
+ specFetchCredentials: Schema.optional(
562
+ Schema.Struct({
563
+ headers: Schema.optional(Schema.Record(Schema.String, HeaderValue)),
564
+ queryParams: Schema.optional(Schema.Record(Schema.String, HeaderValue))
565
+ })
566
+ )
567
+ });
568
+ var OpenApiHeaderInputSchema = Schema.Union([HeaderValue, ConfiguredHeaderValue]);
569
+ var OpenApiOAuthInputSchema = OAuth2SourceConfig;
570
+ var AddSourceInputSchema = Schema.Struct({
571
+ scope: Schema.String,
572
+ spec: Schema.String,
573
+ name: Schema.optional(Schema.String),
574
+ baseUrl: Schema.optional(Schema.String),
575
+ namespace: Schema.optional(Schema.String),
576
+ headers: Schema.optional(Schema.Record(Schema.String, OpenApiHeaderInputSchema)),
577
+ queryParams: Schema.optional(Schema.Record(Schema.String, OpenApiCredentialInput)),
578
+ oauth2: Schema.optional(OpenApiOAuthInputSchema),
579
+ credentialTargetScope: Schema.optional(Schema.String),
580
+ specFetchCredentials: Schema.optional(
581
+ Schema.Struct({
582
+ headers: Schema.optional(Schema.Record(Schema.String, HeaderValue)),
583
+ queryParams: Schema.optional(Schema.Record(Schema.String, HeaderValue))
584
+ })
585
+ )
586
+ });
587
+ var AddSourceOutputSchema = Schema.Struct({
588
+ sourceId: Schema.String,
589
+ toolCount: Schema.Number
590
+ });
591
+ var PreviewSpecInputStandardSchema = Schema.toStandardSchemaV1(
592
+ Schema.toStandardJSONSchemaV1(PreviewSpecInputSchema)
593
+ );
594
+ var AddSourceInputStandardSchema = Schema.toStandardSchemaV1(
595
+ Schema.toStandardJSONSchemaV1(AddSourceInputSchema)
596
+ );
597
+ var AddSourceOutputStandardSchema = Schema.toStandardSchemaV1(
598
+ Schema.toStandardJSONSchemaV1(AddSourceOutputSchema)
599
+ );
600
+ var normalizeOpenApiRefs = (node) => {
601
+ if (node == null || typeof node !== "object") return node;
602
+ if (Array.isArray(node)) {
603
+ let changed2 = false;
604
+ const out = node.map((item) => {
605
+ const n = normalizeOpenApiRefs(item);
606
+ if (n !== item) changed2 = true;
607
+ return n;
608
+ });
609
+ return changed2 ? out : node;
610
+ }
611
+ const obj = node;
612
+ if (typeof obj.$ref === "string") {
613
+ const match = obj.$ref.match(/^#\/components\/schemas\/(.+)$/);
614
+ if (match) return { ...obj, $ref: `#/$defs/${match[1]}` };
615
+ return obj;
616
+ }
617
+ let changed = false;
618
+ const result = {};
619
+ for (const [k, v] of Object.entries(obj)) {
620
+ const n = normalizeOpenApiRefs(v);
621
+ if (n !== v) changed = true;
622
+ result[k] = n;
623
+ }
624
+ return changed ? result : obj;
625
+ };
626
+ var toBinding = (def) => OperationBinding.make({
627
+ method: def.operation.method,
628
+ pathTemplate: def.operation.pathTemplate,
629
+ parameters: [...def.operation.parameters],
630
+ requestBody: def.operation.requestBody
631
+ });
632
+ var descriptionFor = (def) => {
633
+ const op = def.operation;
634
+ return Option2.getOrElse(
635
+ op.description,
636
+ () => Option2.getOrElse(op.summary, () => `${op.method.toUpperCase()} ${op.pathTemplate}`)
637
+ );
638
+ };
639
+ var slotPart = (value) => value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "default";
640
+ var headerSlotFromName = (name) => `header:${slotPart(name)}`;
641
+ var queryParamSlotFromName = (name) => `query_param:${slotPart(name)}`;
642
+ var specFetchHeaderSlotFromName = (name) => `spec_fetch_header:${slotPart(name)}`;
643
+ var specFetchQueryParamSlotFromName = (name) => `spec_fetch_query_param:${slotPart(name)}`;
644
+ var canonicalizeHeaders = (headers) => {
645
+ const nextHeaders = {};
646
+ const bindings = [];
647
+ for (const [name, value] of Object.entries(headers ?? {})) {
648
+ if (typeof value === "string") {
649
+ nextHeaders[name] = value;
650
+ continue;
651
+ }
652
+ if ("kind" in value) {
653
+ nextHeaders[name] = value;
654
+ continue;
655
+ }
656
+ const slot = headerSlotFromName(name);
657
+ nextHeaders[name] = ConfiguredHeaderBinding.make({
658
+ kind: "binding",
659
+ slot,
660
+ prefix: value.prefix
661
+ });
662
+ bindings.push({
663
+ slot,
664
+ targetScope: "targetScope" in value ? value.targetScope : void 0,
665
+ value: {
666
+ kind: "secret",
667
+ secretId: SecretId.make(value.secretId),
668
+ ..."secretScopeId" in value && value.secretScopeId ? { secretScopeId: value.secretScopeId } : {}
669
+ }
670
+ });
671
+ }
672
+ return { headers: nextHeaders, bindings };
673
+ };
674
+ var canonicalizeCredentialMap = (values, slotForName) => {
675
+ const nextValues = {};
676
+ const bindings = [];
677
+ for (const [name, value] of Object.entries(values ?? {})) {
678
+ if (typeof value === "string") {
679
+ nextValues[name] = value;
680
+ continue;
681
+ }
682
+ if ("kind" in value) {
683
+ nextValues[name] = value;
684
+ continue;
685
+ }
686
+ const slot = slotForName(name);
687
+ nextValues[name] = ConfiguredHeaderBinding.make({
688
+ kind: "binding",
689
+ slot,
690
+ prefix: value.prefix
691
+ });
692
+ bindings.push({
693
+ slot,
694
+ targetScope: "targetScope" in value ? value.targetScope : void 0,
695
+ value: {
696
+ kind: "secret",
697
+ secretId: SecretId.make(value.secretId),
698
+ ..."secretScopeId" in value && value.secretScopeId ? { secretScopeId: value.secretScopeId } : {}
699
+ }
700
+ });
701
+ }
702
+ return { values: nextValues, bindings };
703
+ };
704
+ var canonicalizeSpecFetchCredentials = (credentials) => {
705
+ const headers = canonicalizeCredentialMap(credentials?.headers, specFetchHeaderSlotFromName);
706
+ const queryParams = canonicalizeCredentialMap(
707
+ credentials?.queryParams,
708
+ specFetchQueryParamSlotFromName
709
+ );
710
+ const nextCredentials = Object.keys(headers.values).length === 0 && Object.keys(queryParams.values).length === 0 ? void 0 : {
711
+ ...Object.keys(headers.values).length > 0 ? { headers: headers.values } : {},
712
+ ...Object.keys(queryParams.values).length > 0 ? { queryParams: queryParams.values } : {}
713
+ };
714
+ return {
715
+ credentials: nextCredentials,
716
+ bindings: [...headers.bindings, ...queryParams.bindings]
717
+ };
718
+ };
719
+ var canonicalizeOAuth2 = (oauth2) => {
720
+ if (!oauth2) return { bindings: [] };
721
+ return {
722
+ oauth2,
723
+ bindings: []
724
+ };
725
+ };
726
+ var OPENAPI_PLUGIN_ID = "openapi";
727
+ var scopeRanks = (ctx) => new Map(ctx.scopes.map((scope, index) => [String(scope.id), index]));
728
+ var scopeRank = (ranks, scopeId) => ranks.get(scopeId) ?? Infinity;
729
+ var coreBindingToOpenApiBinding = (binding) => OpenApiSourceBindingRef.make({
730
+ sourceId: binding.sourceId,
731
+ sourceScopeId: binding.sourceScopeId,
732
+ scopeId: binding.scopeId,
733
+ slot: binding.slotKey,
734
+ value: binding.value,
735
+ createdAt: binding.createdAt,
736
+ updatedAt: binding.updatedAt
737
+ });
738
+ var listOpenApiSourceBindings = (ctx, sourceId, sourceScope) => Effect2.gen(function* () {
739
+ const ranks = scopeRanks(ctx);
740
+ const sourceSourceRank = scopeRank(ranks, sourceScope);
741
+ if (sourceSourceRank === Infinity) return [];
742
+ const bindings = yield* ctx.credentialBindings.listForSource({
743
+ pluginId: OPENAPI_PLUGIN_ID,
744
+ sourceId,
745
+ sourceScope: ScopeId.make(sourceScope)
746
+ });
747
+ return bindings.filter((binding) => scopeRank(ranks, binding.scopeId) <= sourceSourceRank).map(coreBindingToOpenApiBinding);
748
+ });
749
+ var resolveOpenApiSourceBinding = (ctx, sourceId, sourceScope, slot) => Effect2.gen(function* () {
750
+ const ranks = scopeRanks(ctx);
751
+ const sourceSourceRank = scopeRank(ranks, sourceScope);
752
+ if (sourceSourceRank === Infinity) return null;
753
+ const bindings = yield* ctx.credentialBindings.listForSource({
754
+ pluginId: OPENAPI_PLUGIN_ID,
755
+ sourceId,
756
+ sourceScope: ScopeId.make(sourceScope)
757
+ });
758
+ const binding = bindings.filter(
759
+ (candidate) => candidate.slotKey === slot && scopeRank(ranks, candidate.scopeId) <= sourceSourceRank
760
+ ).sort((a, b) => scopeRank(ranks, a.scopeId) - scopeRank(ranks, b.scopeId))[0];
761
+ return binding ? coreBindingToOpenApiBinding(binding) : null;
762
+ });
763
+ var validateOpenApiBindingTarget = (ctx, input) => Effect2.gen(function* () {
764
+ const ranks = scopeRanks(ctx);
765
+ const sourceSourceRank = scopeRank(ranks, input.sourceScope);
766
+ const targetRank = scopeRank(ranks, input.targetScope);
767
+ const scopeList = `[${ctx.scopes.map((s) => s.id).join(", ")}]`;
768
+ if (sourceSourceRank === Infinity) {
769
+ return yield* new StorageError({
770
+ message: `OpenAPI source binding references source scope "${input.sourceScope}" which is not in the executor's scope stack ${scopeList}.`,
771
+ cause: void 0
772
+ });
773
+ }
774
+ if (targetRank === Infinity) {
775
+ return yield* new StorageError({
776
+ message: `OpenAPI source binding targets scope "${input.targetScope}" which is not in the executor's scope stack ${scopeList}.`,
777
+ cause: void 0
778
+ });
779
+ }
780
+ if (targetRank > sourceSourceRank) {
781
+ return yield* new StorageError({
782
+ message: `OpenAPI source bindings for "${input.sourceId}" cannot be written at outer scope "${input.targetScope}" because the base source lives at "${input.sourceScope}"`,
783
+ cause: void 0
784
+ });
785
+ }
786
+ });
787
+ var targetScopeForBinding = (fallbackTargetScope, binding) => {
788
+ const targetScope = binding.targetScope ?? fallbackTargetScope;
789
+ if (targetScope) return Effect2.succeed(targetScope);
790
+ return Effect2.fail(
791
+ new OpenApiOAuthError({
792
+ message: "credentialTargetScope is required when adding direct OpenAPI credentials"
793
+ })
794
+ );
795
+ };
796
+ var findOuterSource = (ctx, namespace, scope) => Effect2.gen(function* () {
797
+ const ranks = scopeRanks(ctx);
798
+ const baseRank = scopeRank(ranks, scope);
799
+ for (let index = baseRank + 1; index < ctx.scopes.length; index++) {
800
+ const candidateScope = ctx.scopes[index];
801
+ if (!candidateScope) continue;
802
+ const source = yield* ctx.storage.getSource(namespace, candidateScope.id);
803
+ if (source) return source;
804
+ }
805
+ return null;
806
+ });
807
+ var resolveEffectiveSourceConfig = (ctx, base) => Effect2.gen(function* () {
808
+ const fallback = yield* findOuterSource(ctx, base.namespace, base.scope);
809
+ if (!fallback) {
810
+ return {
811
+ config: base.config,
812
+ headersSource: base,
813
+ queryParamsSource: base,
814
+ specFetchCredentialsSource: base,
815
+ oauth2Source: base
816
+ };
817
+ }
818
+ const hasBaseHeaders = Object.keys(base.config.headers ?? {}).length > 0;
819
+ const hasBaseQueryParams = Object.keys(base.config.queryParams ?? {}).length > 0;
820
+ const hasBaseSpecFetchCredentials = base.config.specFetchCredentials !== void 0;
821
+ return {
822
+ config: {
823
+ ...base.config,
824
+ sourceUrl: base.config.sourceUrl ?? fallback.config.sourceUrl,
825
+ baseUrl: fallback.config.baseUrl,
826
+ namespace: base.config.namespace ?? fallback.config.namespace,
827
+ headers: hasBaseHeaders ? base.config.headers : fallback.config.headers,
828
+ queryParams: hasBaseQueryParams ? base.config.queryParams : fallback.config.queryParams,
829
+ specFetchCredentials: base.config.specFetchCredentials ?? fallback.config.specFetchCredentials,
830
+ oauth2: base.config.oauth2 ?? fallback.config.oauth2
831
+ },
832
+ headersSource: hasBaseHeaders ? base : fallback,
833
+ queryParamsSource: hasBaseQueryParams ? base : fallback,
834
+ specFetchCredentialsSource: hasBaseSpecFetchCredentials ? base : fallback,
835
+ oauth2Source: base.config.oauth2 ? base : fallback
836
+ };
837
+ });
838
+ var resolveConfiguredValueMap = (ctx, params) => Effect2.gen(function* () {
839
+ const resolved = {};
840
+ for (const [name, value] of Object.entries(params.values)) {
841
+ if (typeof value === "string") {
842
+ resolved[name] = value;
843
+ continue;
844
+ }
845
+ const binding = yield* resolveOpenApiSourceBinding(
846
+ ctx,
847
+ params.sourceId,
848
+ params.sourceScope,
849
+ value.slot
850
+ );
851
+ if (binding?.value.kind === "secret") {
852
+ const secret = yield* ctx.secrets.getAtScope(binding.value.secretId, binding.value.secretScopeId ?? binding.scopeId).pipe(
853
+ Effect2.catchTag(
854
+ "SecretOwnedByConnectionError",
855
+ () => Effect2.fail(
856
+ new OpenApiOAuthError({
857
+ message: `Secret not found for header "${name}"`
858
+ })
859
+ )
860
+ )
861
+ );
862
+ if (secret === null) {
863
+ return yield* new OpenApiOAuthError({
864
+ message: `Missing secret "${binding.value.secretId}" for ${params.missingLabel} "${name}"`
865
+ });
866
+ }
867
+ resolved[name] = value.prefix ? `${value.prefix}${secret}` : secret;
868
+ continue;
869
+ }
870
+ if (binding?.value.kind === "text") {
871
+ resolved[name] = value.prefix ? `${value.prefix}${binding.value.text}` : binding.value.text;
872
+ continue;
873
+ }
874
+ return yield* new OpenApiOAuthError({
875
+ message: `Missing binding for ${params.missingLabel} "${name}"`
876
+ });
877
+ }
878
+ return resolved;
879
+ });
880
+ var resolveConfiguredHeaders = (ctx, params) => resolveConfiguredValueMap(ctx, {
881
+ sourceId: params.sourceId,
882
+ sourceScope: params.sourceScope,
883
+ values: params.headers,
884
+ missingLabel: "header"
885
+ });
886
+ var resolveSecretBackedValues = (ctx, values) => resolveSecretBackedMap({
887
+ values,
888
+ getSecret: ctx.secrets.get,
889
+ onMissing: (name) => new OpenApiOAuthError({
890
+ message: `Secret not found for "${name}"`
891
+ }),
892
+ onError: (err, name) => Predicate.isTagged("SecretOwnedByConnectionError")(err) ? new OpenApiOAuthError({
893
+ message: `Secret not found for "${name}"`
894
+ }) : err
895
+ }).pipe(
896
+ Effect2.mapError(
897
+ (err) => Predicate.isTagged("SecretOwnedByConnectionError")(err) ? new OpenApiOAuthError({ message: "Secret resolution failed" }) : err
898
+ ),
899
+ Effect2.map((resolved) => resolved ?? {})
900
+ );
901
+ var resolveOAuthConnectionId = (ctx, params) => Effect2.gen(function* () {
902
+ const binding = yield* resolveOpenApiSourceBinding(
903
+ ctx,
904
+ params.sourceId,
905
+ params.sourceScope,
906
+ params.oauth2.connectionSlot
907
+ );
908
+ if (binding?.value.kind === "connection") {
909
+ const connectionId = binding.value.connectionId;
910
+ const connection = yield* ctx.connections.getAtScope(connectionId, binding.scopeId);
911
+ return connection ? { connectionId, scopeId: binding.scopeId } : null;
912
+ }
913
+ return null;
914
+ });
915
+ var resolveSpecFetchInputCredentials = (ctx, credentials) => Effect2.gen(function* () {
916
+ if (!credentials) return void 0;
917
+ return {
918
+ headers: yield* resolveSecretBackedValues(ctx, credentials.headers),
919
+ queryParams: yield* resolveSecretBackedValues(ctx, credentials.queryParams)
920
+ };
921
+ });
922
+ var resolveStoredSpecFetchCredentials = (ctx, params) => Effect2.gen(function* () {
923
+ if (!params.credentials) return void 0;
924
+ return {
925
+ headers: yield* resolveConfiguredValueMap(ctx, {
926
+ sourceId: params.sourceId,
927
+ sourceScope: params.sourceScope,
928
+ values: params.credentials.headers ?? {},
929
+ missingLabel: "spec fetch header"
930
+ }),
931
+ queryParams: yield* resolveConfiguredValueMap(ctx, {
932
+ sourceId: params.sourceId,
933
+ sourceScope: params.sourceScope,
934
+ values: params.credentials.queryParams ?? {},
935
+ missingLabel: "spec fetch query parameter"
936
+ })
937
+ };
938
+ });
939
+ var toOpenApiSourceConfig = (namespace, config) => {
940
+ const configHeaders = {};
941
+ for (const [name, value] of Object.entries(config.headers ?? {})) {
942
+ if (typeof value === "string" || !("kind" in value)) {
943
+ configHeaders[name] = value;
944
+ }
945
+ }
946
+ return {
947
+ kind: "openapi",
948
+ spec: config.spec,
949
+ baseUrl: config.baseUrl,
950
+ namespace,
951
+ headers: headersToConfigValues(
952
+ Object.keys(configHeaders).length > 0 ? configHeaders : void 0
953
+ )
954
+ };
955
+ };
956
+ var isHttpUrl = (s) => s.startsWith("http://") || s.startsWith("https://");
957
+ var openApiPlugin = definePlugin((options) => {
958
+ const rebuildSource = (ctx, input) => Effect2.gen(function* () {
959
+ const doc = yield* parse(input.specText);
960
+ const result = yield* extract(doc);
961
+ const namespace = input.namespace ?? Option2.getOrElse(result.title, () => "api").toLowerCase().replace(/[^a-z0-9]+/g, "_");
962
+ const outerSource = yield* findOuterSource(ctx, namespace, input.scope);
963
+ if (outerSource && input.baseUrl !== void 0 && input.baseUrl.trim() !== "") {
964
+ return yield* new OpenApiOAuthError({
965
+ message: "OpenAPI source shadows inherit the outer source base URL"
966
+ });
967
+ }
968
+ const hoistedDefs = {};
969
+ if (doc.components?.schemas) {
970
+ for (const [k, v] of Object.entries(doc.components.schemas)) {
971
+ hoistedDefs[k] = normalizeOpenApiRefs(v);
972
+ }
973
+ }
974
+ const baseUrl = outerSource ? void 0 : input.baseUrl ?? resolveBaseUrl(result.servers);
975
+ const canonicalHeaders = canonicalizeHeaders(input.headers);
976
+ const canonicalQueryParams = canonicalizeCredentialMap(
977
+ input.queryParams,
978
+ queryParamSlotFromName
979
+ );
980
+ const canonicalSpecFetchCredentials = canonicalizeSpecFetchCredentials(
981
+ input.specFetchCredentials
982
+ );
983
+ const canonicalOAuth2 = canonicalizeOAuth2(input.oauth2);
984
+ const directBindings = [
985
+ ...canonicalHeaders.bindings,
986
+ ...canonicalQueryParams.bindings,
987
+ ...canonicalSpecFetchCredentials.bindings,
988
+ ...canonicalOAuth2.bindings
989
+ ];
990
+ for (const binding of directBindings) {
991
+ const bindingTargetScope = yield* targetScopeForBinding(
992
+ input.credentialTargetScope,
993
+ binding
994
+ );
995
+ yield* validateOpenApiBindingTarget(ctx, {
996
+ sourceId: namespace,
997
+ sourceScope: input.scope,
998
+ targetScope: bindingTargetScope
999
+ });
1000
+ }
1001
+ const definitions = compileToolDefinitions(result.operations);
1002
+ const sourceName = input.name ?? Option2.getOrElse(result.title, () => namespace);
1003
+ const sourceConfig = {
1004
+ spec: input.specText,
1005
+ sourceUrl: input.sourceUrl,
1006
+ baseUrl,
1007
+ namespace: input.namespace,
1008
+ headers: canonicalHeaders.headers,
1009
+ queryParams: canonicalQueryParams.values,
1010
+ specFetchCredentials: canonicalSpecFetchCredentials.credentials,
1011
+ oauth2: canonicalOAuth2.oauth2
1012
+ };
1013
+ const storedSource = {
1014
+ namespace,
1015
+ scope: input.scope,
1016
+ name: sourceName,
1017
+ config: sourceConfig
1018
+ };
1019
+ const storedOps = definitions.map((def) => ({
1020
+ toolId: `${namespace}.${def.toolPath}`,
1021
+ sourceId: namespace,
1022
+ binding: toBinding(def)
1023
+ }));
1024
+ yield* ctx.transaction(
1025
+ Effect2.gen(function* () {
1026
+ yield* ctx.storage.upsertSource(storedSource, storedOps);
1027
+ yield* ctx.core.sources.register({
1028
+ id: namespace,
1029
+ scope: input.scope,
1030
+ kind: "openapi",
1031
+ name: sourceName,
1032
+ url: baseUrl || void 0,
1033
+ canRemove: true,
1034
+ // `canRefresh` reflects whether we still know the
1035
+ // origin URL — sources added from raw spec text have
1036
+ // nothing to re-fetch, so refresh stays disabled.
1037
+ canRefresh: input.sourceUrl != null,
1038
+ canEdit: true,
1039
+ tools: definitions.map((def) => ({
1040
+ name: def.toolPath,
1041
+ description: descriptionFor(def),
1042
+ inputSchema: normalizeOpenApiRefs(Option2.getOrUndefined(def.operation.inputSchema)),
1043
+ outputSchema: normalizeOpenApiRefs(Option2.getOrUndefined(def.operation.outputSchema))
1044
+ }))
1045
+ });
1046
+ if (directBindings.length > 0) {
1047
+ for (const binding of directBindings) {
1048
+ const bindingTargetScope = yield* targetScopeForBinding(
1049
+ input.credentialTargetScope,
1050
+ binding
1051
+ );
1052
+ yield* ctx.credentialBindings.set({
1053
+ targetScope: ScopeId.make(bindingTargetScope),
1054
+ pluginId: OPENAPI_PLUGIN_ID,
1055
+ sourceId: namespace,
1056
+ sourceScope: ScopeId.make(input.scope),
1057
+ slotKey: binding.slot,
1058
+ value: binding.value
1059
+ });
1060
+ }
1061
+ }
1062
+ if (Object.keys(hoistedDefs).length > 0) {
1063
+ yield* ctx.core.definitions.register({
1064
+ sourceId: namespace,
1065
+ scope: input.scope,
1066
+ definitions: hoistedDefs
1067
+ });
1068
+ }
1069
+ })
1070
+ );
1071
+ return { sourceId: namespace, toolCount: definitions.length };
1072
+ });
1073
+ const refreshSourceInternal = (ctx, sourceId, scope) => Effect2.gen(function* () {
1074
+ const httpClientLayer = options?.httpClientLayer ?? ctx.httpClientLayer;
1075
+ const existing = yield* ctx.storage.getSource(sourceId, scope);
1076
+ if (!existing) return;
1077
+ const effective = yield* resolveEffectiveSourceConfig(ctx, existing);
1078
+ const resolvedConfig = effective.config;
1079
+ const sourceUrl = resolvedConfig.sourceUrl;
1080
+ if (!sourceUrl) return;
1081
+ const credentials = yield* resolveStoredSpecFetchCredentials(ctx, {
1082
+ sourceId: existing.namespace,
1083
+ sourceScope: effective.specFetchCredentialsSource.scope,
1084
+ credentials: resolvedConfig.specFetchCredentials
1085
+ });
1086
+ const specText = yield* resolveSpecText(sourceUrl, credentials).pipe(
1087
+ Effect2.provide(httpClientLayer)
1088
+ );
1089
+ yield* rebuildSource(ctx, {
1090
+ specText,
1091
+ scope,
1092
+ sourceUrl,
1093
+ name: existing.name,
1094
+ baseUrl: resolvedConfig.baseUrl,
1095
+ namespace: existing.namespace,
1096
+ headers: existing.config.headers,
1097
+ queryParams: existing.config.queryParams,
1098
+ specFetchCredentials: existing.config.specFetchCredentials,
1099
+ oauth2: existing.config.oauth2,
1100
+ credentialTargetScope: scope
1101
+ });
1102
+ });
1103
+ return {
1104
+ id: "openapi",
1105
+ packageName: "@executor-js/plugin-openapi",
1106
+ schema: openapiSchema,
1107
+ storage: (deps) => makeDefaultOpenapiStore(deps),
1108
+ extension: (ctx) => {
1109
+ const httpClientLayer = options?.httpClientLayer ?? ctx.httpClientLayer;
1110
+ const addSpecInternal = (config) => Effect2.gen(function* () {
1111
+ const credentials = yield* resolveSpecFetchInputCredentials(
1112
+ ctx,
1113
+ config.specFetchCredentials
1114
+ );
1115
+ const specText = yield* resolveSpecText(config.spec, credentials).pipe(
1116
+ Effect2.provide(httpClientLayer)
1117
+ );
1118
+ return yield* rebuildSource(ctx, {
1119
+ specText,
1120
+ scope: config.scope,
1121
+ sourceUrl: isHttpUrl(config.spec) ? config.spec : void 0,
1122
+ name: config.name,
1123
+ baseUrl: config.baseUrl,
1124
+ namespace: config.namespace,
1125
+ headers: config.headers,
1126
+ queryParams: config.queryParams,
1127
+ specFetchCredentials: config.specFetchCredentials,
1128
+ oauth2: config.oauth2,
1129
+ // Default to the source's own scope. refreshSource and editSource
1130
+ // do the same; without this, config-sync's addSpec — which never
1131
+ // passes the field — fails with "credentialTargetScope is
1132
+ // required" the moment the jsonc declares any header secret.
1133
+ credentialTargetScope: config.credentialTargetScope ?? config.scope
1134
+ });
1135
+ });
1136
+ const configFile = options?.configFile;
1137
+ return {
1138
+ previewSpec: (input) => Effect2.gen(function* () {
1139
+ const previewInput = typeof input === "string" ? { spec: input } : input;
1140
+ const credentials = yield* resolveSpecFetchInputCredentials(
1141
+ ctx,
1142
+ previewInput.specFetchCredentials
1143
+ );
1144
+ const specText = yield* resolveSpecText(previewInput.spec, credentials).pipe(
1145
+ Effect2.provide(httpClientLayer)
1146
+ );
1147
+ return yield* previewSpec(specText).pipe(Effect2.provide(httpClientLayer));
1148
+ }),
1149
+ addSpec: (config) => Effect2.gen(function* () {
1150
+ const result = yield* addSpecInternal(config);
1151
+ if (configFile) {
1152
+ yield* configFile.upsertSource(toOpenApiSourceConfig(result.sourceId, config));
1153
+ }
1154
+ return result;
1155
+ }),
1156
+ removeSpec: (namespace, scope) => Effect2.gen(function* () {
1157
+ yield* ctx.transaction(
1158
+ Effect2.gen(function* () {
1159
+ yield* ctx.credentialBindings.removeForSource({
1160
+ pluginId: OPENAPI_PLUGIN_ID,
1161
+ sourceId: namespace,
1162
+ sourceScope: ScopeId.make(scope)
1163
+ });
1164
+ yield* ctx.storage.removeSource(namespace, scope);
1165
+ yield* ctx.core.sources.unregister({
1166
+ id: namespace,
1167
+ targetScope: scope
1168
+ });
1169
+ })
1170
+ );
1171
+ if (configFile) {
1172
+ yield* configFile.removeSource(namespace);
1173
+ }
1174
+ }),
1175
+ getSource: (namespace, scope) => Effect2.gen(function* () {
1176
+ const source = yield* ctx.storage.getSource(namespace, scope);
1177
+ if (!source) return null;
1178
+ const effective = yield* resolveEffectiveSourceConfig(ctx, source);
1179
+ return {
1180
+ ...source,
1181
+ config: effective.config
1182
+ };
1183
+ }),
1184
+ updateSource: (namespace, scope, input) => Effect2.gen(function* () {
1185
+ const existing = yield* ctx.storage.getSource(namespace, scope);
1186
+ if (!existing) return;
1187
+ const canonicalHeaders = input.headers !== void 0 ? canonicalizeHeaders(input.headers) : null;
1188
+ const canonicalOAuth2 = input.oauth2 !== void 0 ? canonicalizeOAuth2(input.oauth2) : null;
1189
+ const canonicalQueryParams = input.queryParams !== void 0 ? canonicalizeCredentialMap(input.queryParams, queryParamSlotFromName) : null;
1190
+ const directBindings = [
1191
+ ...canonicalHeaders?.bindings ?? [],
1192
+ ...canonicalQueryParams?.bindings ?? [],
1193
+ ...canonicalOAuth2?.bindings ?? []
1194
+ ];
1195
+ const affectedPrefixes = [
1196
+ ...input.headers !== void 0 ? ["header:"] : [],
1197
+ ...input.queryParams !== void 0 ? ["query_param:"] : [],
1198
+ ...input.oauth2 !== void 0 ? ["oauth2:"] : []
1199
+ ];
1200
+ const targetScope = input.credentialTargetScope ?? scope;
1201
+ if (input.baseUrl !== void 0 && input.baseUrl.trim() !== "") {
1202
+ const outerSource = yield* findOuterSource(ctx, namespace, scope);
1203
+ if (outerSource) {
1204
+ return yield* new OpenApiOAuthError({
1205
+ message: "OpenAPI source shadows inherit the outer source base URL"
1206
+ });
1207
+ }
1208
+ }
1209
+ if (affectedPrefixes.length > 0 || directBindings.length > 0) {
1210
+ yield* validateOpenApiBindingTarget(ctx, {
1211
+ sourceId: namespace,
1212
+ sourceScope: scope,
1213
+ targetScope
1214
+ });
1215
+ }
1216
+ yield* ctx.transaction(
1217
+ Effect2.gen(function* () {
1218
+ yield* ctx.storage.updateSourceMeta(namespace, scope, {
1219
+ name: input.name?.trim() || void 0,
1220
+ baseUrl: input.baseUrl,
1221
+ headers: canonicalHeaders?.headers,
1222
+ queryParams: canonicalQueryParams?.values,
1223
+ oauth2: canonicalOAuth2?.oauth2
1224
+ });
1225
+ if (affectedPrefixes.length > 0 || directBindings.length > 0) {
1226
+ yield* ctx.credentialBindings.replaceForSource({
1227
+ targetScope: ScopeId.make(targetScope),
1228
+ pluginId: OPENAPI_PLUGIN_ID,
1229
+ sourceId: namespace,
1230
+ sourceScope: ScopeId.make(scope),
1231
+ slotPrefixes: affectedPrefixes,
1232
+ bindings: directBindings.map((binding) => ({
1233
+ slotKey: binding.slot,
1234
+ value: binding.value
1235
+ }))
1236
+ });
1237
+ }
1238
+ })
1239
+ );
1240
+ }),
1241
+ listSourceBindings: (sourceId, sourceScope) => listOpenApiSourceBindings(ctx, sourceId, sourceScope),
1242
+ setSourceBinding: (input) => Effect2.gen(function* () {
1243
+ yield* validateOpenApiBindingTarget(ctx, {
1244
+ sourceId: input.sourceId,
1245
+ sourceScope: input.sourceScope,
1246
+ targetScope: input.scope
1247
+ });
1248
+ const binding = yield* ctx.credentialBindings.set({
1249
+ targetScope: input.scope,
1250
+ pluginId: OPENAPI_PLUGIN_ID,
1251
+ sourceId: input.sourceId,
1252
+ sourceScope: input.sourceScope,
1253
+ slotKey: input.slot,
1254
+ value: input.value
1255
+ });
1256
+ return coreBindingToOpenApiBinding(binding);
1257
+ }),
1258
+ removeSourceBinding: (sourceId, sourceScope, slot, scope) => Effect2.gen(function* () {
1259
+ yield* validateOpenApiBindingTarget(ctx, {
1260
+ sourceId,
1261
+ sourceScope,
1262
+ targetScope: scope
1263
+ });
1264
+ yield* ctx.credentialBindings.remove({
1265
+ targetScope: ScopeId.make(scope),
1266
+ pluginId: OPENAPI_PLUGIN_ID,
1267
+ sourceId,
1268
+ sourceScope: ScopeId.make(sourceScope),
1269
+ slotKey: slot
1270
+ });
1271
+ })
1272
+ };
1273
+ },
1274
+ staticSources: (self) => [
1275
+ {
1276
+ id: "openapi",
1277
+ kind: "executor",
1278
+ name: "OpenAPI",
1279
+ tools: [
1280
+ tool({
1281
+ name: "previewSpec",
1282
+ description: "Preview an OpenAPI document before adding it as a source",
1283
+ inputSchema: PreviewSpecInputStandardSchema,
1284
+ execute: (input) => self.previewSpec(input)
1285
+ }),
1286
+ tool({
1287
+ name: "addSource",
1288
+ description: "Add an OpenAPI source and register its operations as tools",
1289
+ annotations: {
1290
+ requiresApproval: true,
1291
+ approvalDescription: "Add an OpenAPI source"
1292
+ },
1293
+ inputSchema: AddSourceInputStandardSchema,
1294
+ outputSchema: AddSourceOutputStandardSchema,
1295
+ execute: (input) => self.addSpec(input)
1296
+ })
1297
+ ]
1298
+ }
1299
+ ],
1300
+ invokeTool: ({ ctx, toolRow, args }) => Effect2.gen(function* () {
1301
+ const httpClientLayer = options?.httpClientLayer ?? ctx.httpClientLayer;
1302
+ const toolScope = toolRow.scope_id;
1303
+ const op = yield* ctx.storage.getOperationByToolId(toolRow.id, toolScope);
1304
+ if (!op) {
1305
+ return yield* new OpenApiExtractionError({
1306
+ message: `No OpenAPI operation found for tool "${toolRow.id}"`
1307
+ });
1308
+ }
1309
+ const source = yield* ctx.storage.getSource(op.sourceId, toolScope);
1310
+ if (!source) {
1311
+ return yield* new OpenApiExtractionError({
1312
+ message: `No OpenAPI source found for "${op.sourceId}"`
1313
+ });
1314
+ }
1315
+ const effective = yield* resolveEffectiveSourceConfig(ctx, source);
1316
+ const config = effective.config;
1317
+ const resolvedHeaders = yield* resolveConfiguredHeaders(ctx, {
1318
+ sourceId: op.sourceId,
1319
+ sourceScope: effective.headersSource.scope,
1320
+ headers: config.headers ?? {}
1321
+ });
1322
+ const resolvedQueryParams = yield* resolveConfiguredValueMap(ctx, {
1323
+ sourceId: op.sourceId,
1324
+ sourceScope: effective.queryParamsSource.scope,
1325
+ values: config.queryParams ?? {},
1326
+ missingLabel: "query parameter"
1327
+ });
1328
+ if (config.oauth2) {
1329
+ const connection = yield* resolveOAuthConnectionId(ctx, {
1330
+ sourceId: op.sourceId,
1331
+ sourceScope: effective.oauth2Source.scope,
1332
+ oauth2: config.oauth2
1333
+ });
1334
+ if (!connection) {
1335
+ return yield* new OpenApiOAuthError({
1336
+ message: `OAuth configuration for "${op.sourceId}" is missing a connection binding`
1337
+ });
1338
+ }
1339
+ const accessToken = yield* ctx.connections.accessTokenAtScope(connection.connectionId, connection.scopeId).pipe(
1340
+ Effect2.mapError(
1341
+ () => new OpenApiOAuthError({
1342
+ message: "OAuth connection resolution failed"
1343
+ })
1344
+ )
1345
+ );
1346
+ resolvedHeaders.authorization = `Bearer ${accessToken}`;
1347
+ }
1348
+ const result = yield* invokeWithLayer(
1349
+ op.binding,
1350
+ args ?? {},
1351
+ config.baseUrl ?? "",
1352
+ resolvedHeaders,
1353
+ resolvedQueryParams,
1354
+ httpClientLayer
1355
+ );
1356
+ return result;
1357
+ }),
1358
+ resolveAnnotations: ({ ctx, sourceId, toolRows }) => Effect2.gen(function* () {
1359
+ const scopes = /* @__PURE__ */ new Set();
1360
+ for (const row of toolRows) {
1361
+ scopes.add(row.scope_id);
1362
+ }
1363
+ const entries = yield* Effect2.forEach(
1364
+ [...scopes],
1365
+ (scope) => Effect2.gen(function* () {
1366
+ const ops = yield* ctx.storage.listOperationsBySource(sourceId, scope);
1367
+ const byId = /* @__PURE__ */ new Map();
1368
+ for (const op of ops) byId.set(op.toolId, op.binding);
1369
+ return [scope, byId];
1370
+ }),
1371
+ { concurrency: "unbounded" }
1372
+ );
1373
+ const byScope = new Map(entries);
1374
+ const out = {};
1375
+ for (const row of toolRows) {
1376
+ const binding = byScope.get(row.scope_id)?.get(row.id);
1377
+ if (binding) {
1378
+ out[row.id] = annotationsForOperation(binding.method, binding.pathTemplate);
1379
+ }
1380
+ }
1381
+ return out;
1382
+ }),
1383
+ removeSource: ({ ctx, sourceId, scope }) => Effect2.gen(function* () {
1384
+ yield* ctx.transaction(
1385
+ Effect2.gen(function* () {
1386
+ yield* ctx.credentialBindings.removeForSource({
1387
+ pluginId: OPENAPI_PLUGIN_ID,
1388
+ sourceId,
1389
+ sourceScope: ScopeId.make(scope)
1390
+ });
1391
+ yield* ctx.storage.removeSource(sourceId, scope);
1392
+ })
1393
+ );
1394
+ if (options?.configFile) {
1395
+ yield* options.configFile.removeSource(sourceId);
1396
+ }
1397
+ }),
1398
+ // OpenAPI credential usages are reported by the core `credential_binding`
1399
+ // table. Source storage carries only source-owned slot structure.
1400
+ usagesForSecret: () => Effect2.succeed([]),
1401
+ usagesForConnection: () => Effect2.succeed([]),
1402
+ // Re-fetch the spec from its origin URL (captured at addSpec time)
1403
+ // and replay the same parse → extract → upsertSource → register
1404
+ // path used by addSpec. Sources without a stored URL surface a
1405
+ // typed `OpenApiParseError` — the executor only dispatches refresh
1406
+ // when `canRefresh: true`, so a raw-text source reaching here
1407
+ // means stale UI state, which is worth surfacing to the caller.
1408
+ refreshSource: ({ ctx, sourceId, scope }) => refreshSourceInternal(ctx, sourceId, scope),
1409
+ detect: ({ ctx, url }) => Effect2.gen(function* () {
1410
+ const httpClientLayer = options?.httpClientLayer ?? ctx.httpClientLayer;
1411
+ const trimmed = url.trim();
1412
+ if (!trimmed) return null;
1413
+ const parsed = yield* Effect2.try({
1414
+ try: () => new URL(trimmed),
1415
+ catch: (error) => error
1416
+ }).pipe(Effect2.option);
1417
+ if (Option2.isNone(parsed)) return null;
1418
+ const specText = yield* resolveSpecText(trimmed).pipe(
1419
+ Effect2.provide(httpClientLayer),
1420
+ Effect2.catch(() => Effect2.succeed(null))
1421
+ );
1422
+ if (specText === null) return null;
1423
+ const doc = yield* parse(specText).pipe(Effect2.catch(() => Effect2.succeed(null)));
1424
+ if (!doc) return null;
1425
+ const result = yield* extract(doc).pipe(Effect2.catch(() => Effect2.succeed(null)));
1426
+ if (!result) return null;
1427
+ const namespace = Option2.getOrElse(result.title, () => "api").toLowerCase().replace(/[^a-z0-9]+/g, "_");
1428
+ const name = Option2.getOrElse(result.title, () => namespace);
1429
+ return SourceDetectionResult.make({
1430
+ kind: "openapi",
1431
+ confidence: "high",
1432
+ endpoint: trimmed,
1433
+ name,
1434
+ namespace
1435
+ });
1436
+ })
1437
+ };
1438
+ });
1439
+
1440
+ export {
1441
+ resolveHeaders,
1442
+ invoke,
1443
+ invokeWithLayer,
1444
+ annotationsForOperation,
1445
+ openApiPlugin
1446
+ };
1447
+ //# sourceMappingURL=chunk-OZ67JNID.js.map