@voltro/plugin-openapi 0.69.1 → 0.71.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,65 +1,66 @@
1
1
  import { Effect as e, JSONSchema as t } from "effect";
2
2
  import { definePlugin as n } from "@voltro/protocol";
3
3
  import { pluginEnv as r } from "@voltro/env";
4
+ import { publicSurfaceDigest as i } from "@voltro/protocol/rest";
4
5
  //#region src/rpc.ts
5
- var i = "/rpc", a = "rpc", o = (e) => e.ast._tag === "NeverKeyword", s = (e, t) => `${e} ${t}`, c = (e) => `Voltro rpc ${e}. This \`/rpc/<name>\` path is a documentation projection of the procedure's input/output schemas — live calls go through ${e === "query" || e === "stream" ? "the batched `POST /rpc` endpoint (first snapshot / element) or the WebSocket rpc channel (reactive/streaming)" : "the batched `POST /rpc` endpoint"}, NOT a REST request to this path. The request body below is the rpc payload.`, l = (e, t) => {
6
+ var a = "/rpc", o = "rpc", s = (e) => e.ast._tag === "NeverKeyword", c = (e, t) => `${e} ${t}`, l = (e) => `Voltro rpc ${e}. This \`/rpc/<name>\` path is a documentation projection of the procedure's input/output schemas — live calls go through ${e === "query" || e === "stream" ? "the batched `POST /rpc` endpoint (first snapshot / element) or the WebSocket rpc channel (reactive/streaming)" : "the batched `POST /rpc` endpoint"}, NOT a REST request to this path. The request body below is the rpc payload.`, u = (e, t) => {
6
7
  let { kind: n, name: r } = e, i = {
7
8
  required: !0,
8
- content: { "application/json": { schema: f(e.input, t) } }
9
- }, l = (() => {
9
+ content: { "application/json": { schema: p(e.input, t) } }
10
+ }, a = (() => {
10
11
  if (n === "stream") return {
11
12
  description: "Stream of elements. The rpc transport pushes each element as it is produced (WebSocket) or drains to the first element over `POST /rpc`. The schema below is ONE element, not the whole response.",
12
- content: { "application/x-ndjson": { schema: f(e.element, t) } }
13
+ content: { "application/x-ndjson": { schema: p(e.element, t) } }
13
14
  };
14
- let r = f(e.output, t);
15
+ let r = p(e.output, t);
15
16
  return {
16
17
  description: n === "query" ? "OK — the query snapshot (the first/initial snapshot over `POST /rpc`; the WebSocket channel then pushes reactive updates)." : "OK",
17
18
  content: { "application/json": { schema: r } }
18
19
  };
19
- })(), u = o(e.error) ? {} : { 422: {
20
+ })(), u = s(e.error) ? {} : { 422: {
20
21
  description: "A typed error from the procedure — one of its declared error variants (the rpc failure channel).",
21
- content: { "application/json": { schema: f(e.error, t) } }
22
+ content: { "application/json": { schema: p(e.error, t) } }
22
23
  } };
23
24
  return {
24
- tags: [a],
25
- summary: s(n, r),
26
- description: c(n),
25
+ tags: [o],
26
+ summary: c(n, r),
27
+ description: l(n),
27
28
  "x-voltro-rpc": {
28
29
  kind: n,
29
30
  procedure: r
30
31
  },
31
32
  requestBody: i,
32
33
  responses: {
33
- 200: l,
34
+ 200: a,
34
35
  ...u,
35
- 500: _("Internal Server Error — an untyped defect.")
36
+ 500: v("Internal Server Error — an untyped defect.")
36
37
  }
37
38
  };
38
- }, u = (e, t) => {
39
+ }, d = (e, t) => {
39
40
  let n = {};
40
41
  for (let r of e) {
41
- let e = `${i}/${r.name}`;
42
- n[e] = { post: l(r, t) };
42
+ let e = `${a}/${r.name}`;
43
+ n[e] = { post: u(r, t) };
43
44
  }
44
45
  return n;
45
- }, d = (e) => e.replace(/:([A-Za-z0-9_]+)/g, "{$1}"), f = (e, n) => {
46
+ }, f = (e) => e.replace(/:([A-Za-z0-9_]+)/g, "{$1}"), p = (e, n) => {
46
47
  let { $schema: r, $defs: i, ...a } = t.make(e);
47
48
  if (i && typeof i == "object") for (let [e, t] of Object.entries(i)) n[e] = t;
48
49
  return a;
49
- }, p = (e) => {
50
- if (Array.isArray(e)) return e.map(p);
50
+ }, m = (e) => {
51
+ if (Array.isArray(e)) return e.map(m);
51
52
  if (e && typeof e == "object") {
52
53
  let t = {};
53
- for (let [n, r] of Object.entries(e)) t[n] = n === "$ref" && typeof r == "string" && r.startsWith("#/$defs/") ? `#/components/schemas/${r.slice(8)}` : p(r);
54
+ for (let [n, r] of Object.entries(e)) t[n] = n === "$ref" && typeof r == "string" && r.startsWith("#/$defs/") ? `#/components/schemas/${r.slice(8)}` : m(r);
54
55
  return t;
55
56
  }
56
57
  return e;
57
- }, m = (e, t) => {
58
+ }, h = (e, t) => {
58
59
  if (!e) return {
59
60
  parameters: [],
60
61
  requestBody: void 0
61
62
  };
62
- let n = f(e, t), r = n.properties ?? {}, i = new Set(n.required ?? []), a = [];
63
+ let n = p(e, t), r = n.properties ?? {}, i = new Set(n.required ?? []), a = [];
63
64
  for (let [e, t] of [["query", "query"], ["params", "path"]]) {
64
65
  let n = r[e];
65
66
  if (!n || typeof n != "object") continue;
@@ -79,7 +80,7 @@ var i = "/rpc", a = "rpc", o = (e) => e.ast._tag === "NeverKeyword", s = (e, t)
79
80
  content: { "application/json": { schema: o } }
80
81
  } : void 0
81
82
  };
82
- }, h = "VoltroHttpError", g = {
83
+ }, g = "VoltroHttpError", _ = {
83
84
  type: "object",
84
85
  description: "The serve pipeline's error envelope.",
85
86
  properties: {
@@ -97,79 +98,351 @@ var i = "/rpc", a = "rpc", o = (e) => e.ast._tag === "NeverKeyword", s = (e, t)
97
98
  }
98
99
  },
99
100
  required: ["error"]
100
- }, _ = (e) => ({
101
+ }, v = (e) => ({
101
102
  description: e,
102
- content: { "application/json": { schema: { $ref: `#/components/schemas/${h}` } } }
103
- }), v = (e) => ({
104
- ...e.input === void 0 ? {} : { 400: _("Invalid request — the input failed schema decode.") },
103
+ content: { "application/json": { schema: { $ref: `#/components/schemas/${g}` } } }
104
+ }), y = "ProblemDetails", b = {
105
+ type: "object",
106
+ description: "RFC 9457 Problem Details. Extension members carry the error's own fields.",
107
+ properties: {
108
+ type: {
109
+ type: "string",
110
+ format: "uri",
111
+ description: "The error type — `urn:voltro:error:<Tag>`."
112
+ },
113
+ title: { type: "string" },
114
+ status: { type: "integer" },
115
+ detail: { type: "string" }
116
+ },
117
+ required: [
118
+ "type",
119
+ "title",
120
+ "status"
121
+ ],
122
+ additionalProperties: !0
123
+ }, x = (e, t, n = 0) => {
124
+ if (n > 6) return null;
125
+ let r = e.$ref;
126
+ if (typeof r == "string") {
127
+ let e = t[r.split("/").pop() ?? ""];
128
+ return e !== void 0 && typeof e == "object" ? x(e, t, n + 1) : null;
129
+ }
130
+ if (e.example !== void 0) return e.example;
131
+ if (Array.isArray(e.examples) && e.examples.length > 0) return e.examples[0];
132
+ if (e.const !== void 0) return e.const;
133
+ if (Array.isArray(e.enum) && e.enum.length > 0) return e.enum[0];
134
+ let i = e.anyOf ?? e.oneOf;
135
+ if (Array.isArray(i) && i.length > 0) return x(i.find((e) => e.type !== "null") ?? i[0], t, n + 1);
136
+ let a = e.allOf;
137
+ if (Array.isArray(a) && a.length > 0) {
138
+ let e = a.map((e) => x(e, t, n + 1));
139
+ return e.every((e) => typeof e == "object" && !!e && !Array.isArray(e)) ? Object.assign({}, ...e) : e[0];
140
+ }
141
+ let o = e.type;
142
+ switch (Array.isArray(o) ? o.find((e) => e !== "null") : o) {
143
+ case "string": {
144
+ let t = e.format;
145
+ if (t === "date-time") return "2026-01-01T00:00:00.000Z";
146
+ if (t === "date") return "2026-01-01";
147
+ if (t === "email") return "user@example.com";
148
+ if (t === "uri" || t === "url") return "https://example.com";
149
+ if (t === "uuid") return "00000000-0000-4000-8000-000000000000";
150
+ let n = typeof e.minLength == "number" ? e.minLength : 0;
151
+ return "string".padEnd(Math.max(n, 6), "x");
152
+ }
153
+ case "integer": return typeof e.minimum == "number" ? e.minimum : 1;
154
+ case "number": return typeof e.minimum == "number" ? e.minimum : 1;
155
+ case "boolean": return !0;
156
+ case "null": return null;
157
+ case "array": {
158
+ let r = e.items;
159
+ return r !== void 0 && typeof r == "object" && !Array.isArray(r) ? [x(r, t, n + 1)] : [];
160
+ }
161
+ case "object": {
162
+ let r = e.properties ?? {}, i = {};
163
+ for (let [e, a] of Object.entries(r)) i[e] = x(a, t, n + 1);
164
+ return i;
165
+ }
166
+ default: return typeof e.properties == "object" ? x({
167
+ ...e,
168
+ type: "object"
169
+ }, t, n) : null;
170
+ }
171
+ }, S = {
172
+ 400: {
173
+ tag: "InvalidRequest",
174
+ detail: "Invalid request"
175
+ },
176
+ 401: {
177
+ tag: "Unauthorized",
178
+ detail: "Authentication required"
179
+ },
180
+ 403: {
181
+ tag: "Forbidden",
182
+ detail: "Missing required scope: <scope>"
183
+ },
184
+ 410: {
185
+ tag: "Gone",
186
+ detail: "Gone"
187
+ },
188
+ 500: {
189
+ tag: "InternalServerError",
190
+ detail: "Internal Server Error"
191
+ }
192
+ }, C = {
193
+ 400: "Bad Request",
194
+ 401: "Unauthorized",
195
+ 403: "Forbidden",
196
+ 404: "Not Found",
197
+ 409: "Conflict",
198
+ 410: "Gone",
199
+ 422: "Unprocessable Content",
200
+ 429: "Too Many Requests",
201
+ 500: "Internal Server Error"
202
+ }, w = (e, t, n) => {
203
+ let r = t.format.errors === "problem-details" && t.format.encodeError === void 0, i = r ? "application/problem+json" : "application/json", a = /* @__PURE__ */ new Map();
204
+ for (let e of t.errorResponses) {
205
+ let t = a.get(e.status) ?? [], i = p(e.schema, n), o = x(i, n), { _tag: s, ...c } = typeof o == "object" && o ? o : {}, l = r ? {
206
+ type: `urn:voltro:error:${e.tag}`,
207
+ title: e.tag.replace(/([a-z0-9])([A-Z])/g, "$1 $2"),
208
+ status: e.status,
209
+ ...c
210
+ } : {
211
+ error: e.tag,
212
+ ...c
213
+ };
214
+ t.push({
215
+ tag: e.tag,
216
+ example: l,
217
+ schema: r ? { allOf: [{ $ref: `#/components/schemas/${y}` }, {
218
+ type: "object",
219
+ properties: { type: { const: `urn:voltro:error:${e.tag}` } }
220
+ }] } : i
221
+ }), a.set(e.status, t);
222
+ }
223
+ let o = (e, t) => {
224
+ if (!r) return v(t);
225
+ let n = S[e], a = n === void 0 ? {} : { example: {
226
+ type: `urn:voltro:error:${n.tag}`,
227
+ title: n.tag.replace(/([a-z])([A-Z])/g, "$1 $2"),
228
+ status: e,
229
+ detail: n.detail
230
+ } };
231
+ return {
232
+ description: t,
233
+ content: { [i]: {
234
+ schema: { $ref: `#/components/schemas/${y}` },
235
+ ...a
236
+ } }
237
+ };
238
+ }, s = {};
239
+ s[400] = o(400, "Invalid request — the input failed schema decode."), t.security !== "none" && (s[401] = o(401, "Unauthorized — no valid bearer.")), (t.security === "required" || (e.guards?.length ?? 0) > 0) && (s[403] = o(403, "Forbidden — the caller lacks a required scope."));
240
+ for (let [e, t] of [...a.entries()].sort((e, t) => e[0] - t[0])) {
241
+ let n = t.map((e) => e.tag);
242
+ s[String(e)] = {
243
+ description: `${C[e] ?? "Error"} — ${n.join(" | ")}`,
244
+ content: { [i]: {
245
+ schema: t.length === 1 ? t[0].schema : { oneOf: t.map((e) => e.schema) },
246
+ example: t[0].example
247
+ } }
248
+ };
249
+ }
250
+ return e.sunset !== void 0 && (s[410] = o(410, `Gone — the route is past its sunset date (${e.sunset}).`)), s[500] = o(500, "Internal Server Error."), s;
251
+ }, T = (e, t, n) => {
252
+ let r = p(t.input, n), i = r.properties ?? {}, a = new Set(r.required ?? []), o = new Set(t.pathParams), s = [], c = {}, l = [], u = e.example?.request, d = (e) => typeof u == "object" && u && e in u ? { example: u[e] } : {};
253
+ for (let [t, n] of Object.entries(i)) o.has(t) ? s.push({
254
+ name: t,
255
+ in: "path",
256
+ required: !0,
257
+ schema: n,
258
+ ...d(t)
259
+ }) : e.method === "GET" ? s.push({
260
+ name: t,
261
+ in: "query",
262
+ required: a.has(t),
263
+ schema: n,
264
+ ...d(t)
265
+ }) : (c[t] = n, a.has(t) && l.push(t));
266
+ return {
267
+ parameters: s,
268
+ requestBody: e.method !== "GET" && Object.keys(c).length > 0 ? {
269
+ required: l.length > 0,
270
+ content: {
271
+ "application/json": { schema: {
272
+ ...r,
273
+ properties: c,
274
+ required: l
275
+ } },
276
+ "application/x-www-form-urlencoded": { schema: {
277
+ ...r,
278
+ properties: c,
279
+ required: l
280
+ } }
281
+ }
282
+ } : void 0
283
+ };
284
+ }, E = (e, t, n) => {
285
+ if (e.streaming === !0) return {
286
+ description: "Server-Sent Events stream (`snapshot`, then `delta` per change)",
287
+ content: { "text/event-stream": { schema: { type: "string" } } }
288
+ };
289
+ let r = p(t.output, n), i = t.format, a = i.pagination === "link-header" && D(r, n, i.cursorField), o = i.encodeSuccess === void 0 ? i.envelope === "data" ? {
290
+ type: "object",
291
+ properties: {
292
+ data: r,
293
+ ...a ? { next: {
294
+ type: "string",
295
+ description: "The next page's cursor, when there is one."
296
+ } } : {}
297
+ },
298
+ required: ["data"]
299
+ } : i.envelope === "jsonapi" ? {
300
+ type: "object",
301
+ properties: {
302
+ data: { description: "One resource or an array of resources — `{ type, id, attributes }`." },
303
+ links: {
304
+ type: "object",
305
+ properties: { next: { type: "string" } }
306
+ }
307
+ },
308
+ required: ["data"]
309
+ } : r : { description: `Encoded by the '${i.name}' format.` }, s = i.envelope === "jsonapi" ? "application/vnd.api+json" : "application/json", c = {
310
+ ...a ? { Link: {
311
+ description: "RFC 8288 — `<url>; rel=\"next\"` when there is a next page.",
312
+ schema: { type: "string" }
313
+ } } : {},
314
+ ...e.etag === !0 ? { ETag: {
315
+ description: "Weak validator; send it back as `If-None-Match` for a 304.",
316
+ schema: { type: "string" }
317
+ } } : {}
318
+ };
319
+ return {
320
+ description: "OK",
321
+ ...Object.keys(c).length > 0 ? { headers: c } : {},
322
+ content: { [s]: {
323
+ schema: o,
324
+ ...O(e.example?.response, o, n)
325
+ } }
326
+ };
327
+ }, D = (e, t, n) => {
328
+ let r = e.$ref, i = (typeof r == "string" ? t[r.split("/").pop() ?? ""] : e)?.properties;
329
+ return typeof i == "object" && !!i && n in i;
330
+ }, O = (e, t, n) => e === void 0 ? {
331
+ example: x(t, n),
332
+ "x-voltro-example": "derived"
333
+ } : { example: e }, k = (e) => ({
334
+ ...e.input === void 0 ? {} : { 400: v("Invalid request — the input failed schema decode.") },
105
335
  ...e.guards !== void 0 && e.guards.length > 0 ? {
106
- 401: _("Unauthorized — a guard rejected the unauthenticated request."),
107
- 403: _("Forbidden — a guard rejected the request (e.g. a required scope is missing).")
336
+ 401: v("Unauthorized — a guard rejected the unauthenticated request."),
337
+ 403: v("Forbidden — a guard rejected the request (e.g. a required scope is missing).")
108
338
  } : {},
109
- ...e.sunset === void 0 ? {} : { 410: _(`Gone — the route is past its sunset date (${e.sunset}).`) },
110
- 500: _("Internal Server Error.")
339
+ ...e.sunset === void 0 ? {} : { 410: v(`Gone — the route is past its sunset date (${e.sunset}).`) },
340
+ 500: v("Internal Server Error.")
111
341
  });
112
- function y(e, t = {}, n = {}) {
113
- let r = {}, i = {}, a = e.some((e) => e.guards !== void 0 && e.guards.length > 0);
342
+ function A(e, t = {}, n = {}, r = {}) {
343
+ let a = {}, o = {}, s = e.some((e) => e.guards !== void 0 && e.guards.length > 0), c = e.some((e) => e.projection !== void 0 && e.projection.security !== "none"), l = e.some((e) => e.projection?.format.errors === "problem-details" && e.projection.format.encodeError === void 0), u = /* @__PURE__ */ new Set();
114
344
  for (let t of e) {
115
- let { parameters: e, requestBody: n } = m(t.input, i), a = t.guards !== void 0 && t.guards.length > 0;
116
- if (n !== void 0 && t.example?.request !== void 0) {
117
- let e = n.content;
345
+ let e = t.projection, { parameters: n, requestBody: r } = e === void 0 ? h(t.input, o) : T(t, e, o), i = t.etag === !0 && t.method === "GET" && t.streaming !== !0, s = i ? [...n, {
346
+ name: "If-None-Match",
347
+ in: "header",
348
+ required: !1,
349
+ description: "A previously returned ETag. A matching validator returns 304 without a body.",
350
+ schema: { type: "string" }
351
+ }] : n, c = i ? { 304: {
352
+ description: "Not Modified",
353
+ headers: { ETag: {
354
+ description: "The current representation validator.",
355
+ schema: { type: "string" }
356
+ } }
357
+ } } : {}, l = t.guards !== void 0 && t.guards.length > 0;
358
+ if (r !== void 0) {
359
+ let e = r.content;
118
360
  e["application/json"] = {
119
361
  ...e["application/json"],
120
- example: t.example.request
362
+ ...O(t.example?.request, e["application/json"].schema, o)
121
363
  };
122
364
  }
123
- let o = {
365
+ let d = e === void 0 ? l ? { security: [{ bearerAuth: [] }] } : {} : e.security === "required" ? { security: [{ bearerAuth: [] }] } : e.security === "optional" ? { security: [{ bearerAuth: [] }, {}] } : {}, m = t.tags ?? (t.version === void 0 ? void 0 : [t.version]);
366
+ for (let e of m ?? []) u.add(e);
367
+ let g = e?.version ?? t.version, _ = {
368
+ ...t.operationId === void 0 ? {} : { operationId: t.operationId },
124
369
  ...t.summary ? { summary: t.summary } : {},
125
370
  ...t.description ? { description: t.description } : {},
126
371
  ...t.deprecated === void 0 ? {} : { deprecated: !0 },
127
372
  ...t.sunset === void 0 ? {} : { "x-sunset": t.sunset },
128
- ...t.version === void 0 ? {} : {
129
- tags: [t.version],
130
- "x-voltro-api-version": t.version
131
- },
132
- ...e.length > 0 ? { parameters: e } : {},
133
- ...n ? { requestBody: n } : {},
134
- ...a ? { security: [{ bearerAuth: [] }] } : {},
135
- responses: {
373
+ ...m === void 0 ? {} : { tags: m },
374
+ ...g === void 0 ? {} : { "x-voltro-api-version": g },
375
+ ...s.length > 0 ? { parameters: s } : {},
376
+ ...r ? { requestBody: r } : {},
377
+ ...d,
378
+ responses: e === void 0 ? {
136
379
  200: t.streaming === !0 ? {
137
380
  description: "Server-Sent Events stream (`snapshot`, then `delta` per change)",
138
381
  content: { "text/event-stream": { schema: { type: "string" } } }
139
382
  } : {
140
383
  description: "OK",
141
384
  content: { "application/json": {
142
- schema: f(t.output, i),
143
- ...t.example?.response === void 0 ? {} : { example: t.example.response }
385
+ schema: p(t.output, o),
386
+ ...O(t.example?.response, p(t.output, o), o)
144
387
  } }
145
388
  },
146
- ...v(t)
389
+ ...k(t),
390
+ ...c
391
+ } : {
392
+ 200: E(t, e, o),
393
+ ...c,
394
+ ...w(t, e, o)
147
395
  }
148
- }, s = d(t.path), c = r[s] ?? {};
149
- c[t.method.toLowerCase()] = o, r[s] = c;
396
+ };
397
+ e?.format.describe !== void 0 && (_ = e.format.describe(_, {
398
+ tag: e.tag,
399
+ method: t.method,
400
+ path: t.path
401
+ }));
402
+ let v = f(t.path), y = a[v] ?? {};
403
+ y[t.method.toLowerCase()] = _, a[v] = y;
150
404
  }
151
- let o = n.procedures ?? [];
152
- if (o.length > 0) {
153
- let e = u(o, i);
154
- for (let [t, n] of Object.entries(e)) r[t] = {
405
+ let v = n.procedures ?? [];
406
+ if (v.length > 0) {
407
+ let e = d(v, o);
408
+ for (let [t, n] of Object.entries(e)) a[t] = {
155
409
  ...n,
156
- ...r[t] ?? {}
410
+ ...a[t] ?? {}
157
411
  };
158
412
  }
413
+ let x = [...u].sort().map((e) => ({
414
+ name: e,
415
+ ...r.tagDescriptions?.[e] === void 0 ? {} : { description: r.tagDescriptions[e] },
416
+ ...r.tagExternalDocs?.[e] === void 0 ? {} : { externalDocs: r.tagExternalDocs[e] }
417
+ })), S = (r.servers ?? []).map((e) => ({
418
+ url: e.url,
419
+ ...e.description === void 0 ? {} : { description: e.description },
420
+ ...e.environment === void 0 ? {} : { "x-voltro-environment": e.environment }
421
+ }));
159
422
  return {
160
423
  openapi: "3.1.0",
161
424
  info: {
162
425
  title: t.title ?? "API",
163
426
  version: t.version ?? "1.0.0",
164
- ...t.description ? { description: t.description } : {}
427
+ ...t.description ? { description: t.description } : {},
428
+ ...t.contact === void 0 ? {} : { contact: t.contact },
429
+ ...t.license === void 0 ? {} : { license: t.license },
430
+ ...t.termsOfService === void 0 ? {} : { termsOfService: t.termsOfService },
431
+ ...t.logo === void 0 ? {} : { "x-logo": t.logo }
165
432
  },
166
- paths: p(r),
433
+ ...S.length > 0 ? { servers: S } : {},
434
+ ...r.externalDocs === void 0 ? {} : { externalDocs: r.externalDocs },
435
+ ...r.environment === void 0 ? {} : { "x-voltro-environment": r.environment },
436
+ ...e.some((e) => e.projection !== void 0) ? { "x-voltro-surface-digest": i(e) } : {},
437
+ ...x.length > 0 ? { tags: x } : {},
438
+ paths: m(a),
167
439
  components: {
168
440
  schemas: {
169
- ...p(i),
170
- [h]: g
441
+ ...m(o),
442
+ [g]: _,
443
+ ...l ? { [y]: b } : {}
171
444
  },
172
- ...a ? { securitySchemes: { bearerAuth: {
445
+ ...s || c ? { securitySchemes: { bearerAuth: {
173
446
  type: "http",
174
447
  scheme: "bearer"
175
448
  } } } : {}
@@ -178,73 +451,157 @@ function y(e, t = {}, n = {}) {
178
451
  }
179
452
  //#endregion
180
453
  //#region src/swaggerUi.ts
181
- var b = "5.17.14", x = "sha384-wxLW6kwyHktdDGr6Pv1zgm/VGJh99lfUbzSn6HNHBENZlCN7W602k9VkGdxuFvPn", S = "sha384-wmyclcVGX/WhUkdkATwhaK1X1JtiNrr2EoYJ+diV3vj4v6OC5yCeSu+yW13SYJep", C = `https://unpkg.com/swagger-ui-dist@${b}/swagger-ui.css`, w = `https://unpkg.com/swagger-ui-dist@${b}/swagger-ui-bundle.js`, T = (e, t = "API docs") => `<!doctype html>
454
+ var j = "5.17.14", M = "sha384-wxLW6kwyHktdDGr6Pv1zgm/VGJh99lfUbzSn6HNHBENZlCN7W602k9VkGdxuFvPn", N = "sha384-wmyclcVGX/WhUkdkATwhaK1X1JtiNrr2EoYJ+diV3vj4v6OC5yCeSu+yW13SYJep", P = `https://unpkg.com/swagger-ui-dist@${j}/swagger-ui.css`, F = `https://unpkg.com/swagger-ui-dist@${j}/swagger-ui-bundle.js`, I = (e, t = "API docs", n = {}) => `<!doctype html>
182
455
  <html lang="en">
183
456
  <head>
184
457
  <meta charset="utf-8" />
185
458
  <meta name="viewport" content="width=device-width, initial-scale=1" />
186
459
  <title>${t}</title>
187
- <link rel="stylesheet" href="${C}" integrity="${x}" crossorigin="anonymous" />
460
+ <link rel="stylesheet" href="${P}" integrity="${M}" crossorigin="anonymous" />
188
461
  <style>body { margin: 0 }</style>
189
462
  </head>
190
463
  <body>
191
464
  <div id="swagger-ui"></div>
192
- <script src="${w}" integrity="${S}" crossorigin="anonymous"><\/script>
465
+ <script src="${F}" integrity="${N}" crossorigin="anonymous"><\/script>
193
466
  <script>
194
- window.ui = SwaggerUIBundle({ url: ${JSON.stringify(e)}, dom_id: '#swagger-ui', deepLinking: true });
467
+ window.ui = SwaggerUIBundle({ ${n.urls !== void 0 && n.urls.length > 0 ? `urls: ${JSON.stringify(n.urls)}, ${n.primaryName === void 0 ? "" : `'urls.primaryName': ${JSON.stringify(n.primaryName)}, `}` : `url: ${JSON.stringify(e)}, `}dom_id: '#swagger-ui', deepLinking: true });
195
468
  <\/script>
196
469
  </body>
197
- </html>`, E = r([{
198
- name: "OPENAPI_DOCS_TOKEN",
199
- required: !1,
200
- secret: !0,
201
- description: "Bearer token required on GET /openapi.json + GET /docs; unset leaves them public."
202
- }]), D = (e, t) => {
470
+ </html>`, L = r([
471
+ {
472
+ name: "OPENAPI_DOCS_TOKEN",
473
+ required: !1,
474
+ secret: !0,
475
+ description: "Bearer token required on GET /openapi.json + GET /docs; unset leaves them public."
476
+ },
477
+ {
478
+ name: "VOLTRO_PUBLIC_API_ORIGIN",
479
+ required: !1,
480
+ secret: !1,
481
+ description: "The public origin of this api (https://api.example.com) — the default servers[0] of the spec."
482
+ },
483
+ {
484
+ name: "VOLTRO_ENVIRONMENT",
485
+ required: !1,
486
+ secret: !1,
487
+ description: "The environment this instance serves (dev | staging | production) — x-voltro-environment on the spec."
488
+ }
489
+ ]), R = (e, t) => {
203
490
  if (e) return (t.headers.authorization ?? t.headers.Authorization) === `Bearer ${e}` ? void 0 : {
204
491
  status: 401,
205
492
  body: "unauthorized",
206
493
  contentType: "text/plain"
207
494
  };
208
- }, O = (t) => {
209
- let r = t.specPath ?? "/openapi.json", i = t.docsPath ?? "/docs", a = E.read("OPENAPI_DOCS_TOKEN", t.token), o = t.name ? `@voltro/plugin-openapi#${t.name}` : "@voltro/plugin-openapi", s = t.routes ?? [], c = t.procedures ?? [], l = t.includeAppRoutes ?? !0, u = s, d = () => JSON.stringify(y(u, t.info ?? {}, { procedures: c }), null, 2), f = d(), p = (e) => {
210
- if (!l) return;
495
+ }, z = (e) => e.projection?.version ?? e.version, B = (e) => {
496
+ let t = /^v(\d+)$/.exec(e);
497
+ return t === null ? -1 : Number(t[1]);
498
+ }, V = (e) => [...new Set(e.map(z).filter((e) => e !== void 0))].sort((e, t) => B(t) - B(e) || t.localeCompare(e)), H = (e, t) => {
499
+ let n = t.filter((t) => z(t) === e && t.sunset !== void 0).map((e) => e.sunset).sort();
500
+ return n.length > 0 ? `${e} — sunset ${n[0]}` : e;
501
+ }, U = (t) => {
502
+ let r = t.specPath ?? "/openapi.json", i = t.docsPath ?? "/docs", a = L.read("OPENAPI_DOCS_TOKEN", t.token), o = t.name ? `@voltro/plugin-openapi#${t.name}` : "@voltro/plugin-openapi", s = t.routes ?? [], c = t.procedures ?? [], l = c, u = t.includeAppRoutes ?? !0, d = t.includeAppProcedures ?? !1, f = s, p = t.environment ?? L.read("VOLTRO_ENVIRONMENT", void 0) ?? (process.env.NODE_ENV === "production" ? "production" : "dev"), m = L.read("VOLTRO_PUBLIC_API_ORIGIN", void 0), h = t.servers ?? (m === void 0 ? [] : [{
503
+ url: m,
504
+ description: `${p} — this instance`,
505
+ environment: p
506
+ }]), g = {
507
+ ...t.tagDescriptions === void 0 ? {} : { tagDescriptions: t.tagDescriptions },
508
+ ...t.tagExternalDocs === void 0 ? {} : { tagExternalDocs: t.tagExternalDocs },
509
+ ...t.externalDocs === void 0 ? {} : { externalDocs: t.externalDocs },
510
+ ...h.length > 0 ? { servers: h } : {},
511
+ environment: p
512
+ }, _ = (e) => JSON.stringify(A(e === void 0 ? f : f.filter((t) => z(t) === e), t.info ?? {}, e === void 0 ? { procedures: l } : {}, g), null, 2), v = /* @__PURE__ */ new Map(), y = () => {
513
+ v = new Map([["", _()], ...V(f).map((e) => [e, _(e)])]);
514
+ };
515
+ y();
516
+ let b = (e) => {
517
+ if (!u) return;
211
518
  let t = new Set(s.map((e) => `${e.method} ${e.path}`)), n = e.filter((e) => !t.has(`${e.method} ${e.path}`));
212
- u = [...s, ...n], f = d();
213
- }, m = [{
519
+ f = [...s, ...n], y();
520
+ }, x = (e) => {
521
+ if (!d) return;
522
+ let t = new Set(c.map((e) => e.name));
523
+ l = [...c, ...e.filter((e) => !t.has(e.name))], y();
524
+ }, S = r.endsWith(".json") ? r.slice(0, -5) : `${r}/versions`, C = (e) => `${S}/${e}.json`, w = (e) => ({
525
+ status: 404,
526
+ contentType: "application/json",
527
+ body: JSON.stringify({
528
+ error: `no API version '${e}' is mounted`,
529
+ versions: V(f)
530
+ })
531
+ }), T = [{
214
532
  method: "GET",
215
533
  path: r,
216
- handle: async (e) => D(a, e) ?? {
217
- status: 200,
218
- body: f,
219
- contentType: "application/json"
534
+ match: "exact",
535
+ handle: async (e) => {
536
+ let t = R(a, e);
537
+ if (t) return t;
538
+ let n = new URLSearchParams(e.query).get("version") ?? void 0;
539
+ return n !== void 0 && !v.has(n) ? w(n) : {
540
+ status: 200,
541
+ body: v.get(n ?? ""),
542
+ contentType: "application/json"
543
+ };
544
+ }
545
+ }, {
546
+ method: "GET",
547
+ path: S,
548
+ handle: async (e) => {
549
+ let t = R(a, e);
550
+ if (t) return t;
551
+ let n = /\/([^/]+)\.json$/.exec(e.path)?.[1];
552
+ return n === void 0 || n === "" || !v.has(n) ? w(n ?? "") : {
553
+ status: 200,
554
+ body: v.get(n),
555
+ contentType: "application/json"
556
+ };
220
557
  }
221
558
  }];
222
- return i !== !1 && m.push({
559
+ return i !== !1 && T.push({
223
560
  method: "GET",
224
561
  path: i,
225
- handle: async (e) => D(a, e) ?? {
226
- status: 200,
227
- body: T(r, t.info?.title ?? "API docs"),
228
- contentType: "text/html; charset=utf-8"
562
+ match: "exact",
563
+ handle: async (e) => {
564
+ let n = R(a, e);
565
+ if (n) return n;
566
+ let i = V(f), o = i.length > 1 ? [...i.map((e) => ({
567
+ url: C(e),
568
+ name: H(e, f)
569
+ })), {
570
+ url: r,
571
+ name: "all versions"
572
+ }] : void 0;
573
+ return {
574
+ status: 200,
575
+ body: I(r, t.info?.title ?? "API docs", o === void 0 ? {} : {
576
+ urls: o,
577
+ primaryName: o[0].name
578
+ }),
579
+ contentType: "text/html; charset=utf-8"
580
+ };
229
581
  }
230
582
  }), n({
231
583
  name: o,
232
584
  description: "OpenAPI 3.1 spec + Swagger-UI docs from defineRestRoute descriptors + rpc procedures.",
233
585
  permissions: [],
234
- declaredEnv: E.declared,
235
- httpRoutes: m,
236
- onRestSurface: p,
586
+ declaredEnv: L.declared,
587
+ httpRoutes: T,
588
+ onRestSurface: b,
589
+ onProcedureSurface: x,
237
590
  onActivate: (t) => e.sync(() => {
238
591
  t.logger.info("openapi docs active", {
239
592
  specPath: r,
240
593
  docsPath: i,
241
- routes: u.length,
242
- appRoutes: l ? "included once mounted" : "excluded",
243
- procedures: c.length,
594
+ routes: f.length,
595
+ appRoutes: u ? "included once mounted" : "excluded",
596
+ procedures: l.length,
597
+ appProcedures: d ? "included once discovered" : "excluded",
598
+ environment: p,
599
+ servers: h.map((e) => e.url),
600
+ versions: V(f),
244
601
  tokenGated: !!a
245
602
  });
246
603
  })
247
604
  });
248
605
  };
249
606
  //#endregion
250
- export { y as generateOpenApiSpec, O as openapiPlugin, T as swaggerUiHtml };
607
+ export { y as PROBLEM_DETAILS_SCHEMA_NAME, x as deriveExample, A as generateOpenApiSpec, V as mountedVersions, U as openapiPlugin, I as swaggerUiHtml, H as versionLabel };