@ayepi/core 0.1.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 ADDED
@@ -0,0 +1,1968 @@
1
+ import { DEFAULT_RETRY_OPTIONS, RetryAbort, backoff, getDefaultRetryOptions, retry, setDefaultRetryOptions } from "./retry.js";
2
+ import { DEFAULT_BUCKETS, createMetrics, formatPrometheus } from "./stats.js";
3
+ import { a as createClientCache, c as ApiFailure, d as joinPattern, f as matchParts, h as splitPattern, i as createCallerContext, l as reject, m as path, n as client, o as stableStringify, p as paramKeys, r as CallerRateLimited, s as ApiError, t as wsTransport, u as buildParts } from "./ws-transport.js";
4
+ import { z } from "zod";
5
+ //#region src/endpoint.ts
6
+ /**
7
+ * # Endpoint specs
8
+ *
9
+ * Endpoint configuration ({@link EndpointConfig}), the compile-time config
10
+ * validator ({@link CheckCfg}), event configuration, and the two entry points
11
+ * that turn declarations into a runtime spec: {@link endpoint} and {@link spec}.
12
+ *
13
+ * The central invariant is **disjoint kinds**: every path-param key is declared
14
+ * exactly once (loader XOR template XOR `params` schema) and positioned exactly
15
+ * once; query keys are disjoint from path; body keys from path∪query; files keys
16
+ * from all. That disjointness is what lets the four kinds merge losslessly into a
17
+ * single `data` payload. It is enforced both at compile time (via {@link CheckCfg})
18
+ * and at `spec()` time (via {@link normalizeEndpoint}).
19
+ *
20
+ * @module
21
+ */
22
+ function makeEndpoint(cfg, mws, prefixes) {
23
+ return {
24
+ kind: "endpoint",
25
+ cfg,
26
+ mws,
27
+ prefixes
28
+ };
29
+ }
30
+ /**
31
+ * Define a bare endpoint with no middleware.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * const getReport = endpoint({
36
+ * method: 'GET',
37
+ * path: reportPath,
38
+ * response: z.object({ year: z.number(), slug: z.string() }),
39
+ * })
40
+ * ```
41
+ */
42
+ function endpoint(cfg) {
43
+ return makeEndpoint(cfg, [], []);
44
+ }
45
+ /**
46
+ * Finalize a set of endpoints + events into a spec, validating every endpoint at
47
+ * definition time.
48
+ *
49
+ * Beyond the compile-time {@link CheckCfg} guarantees, this performs runtime
50
+ * sanity checks (flag exclusivity, kind shapes) and full
51
+ * {@link normalizeEndpoint | path/coverage/disjointness} validation — throwing
52
+ * immediately on any violation so misconfiguration fails at module init.
53
+ *
54
+ * @returns the same spec object, now type-branded and validated.
55
+ */
56
+ function spec(spec) {
57
+ for (const [name, def] of Object.entries(spec.endpoints)) {
58
+ const c = def.cfg;
59
+ if (c.streamIn && (c.body || c.files)) throw new Error(`endpoint "${name}": streamIn excludes body/files`);
60
+ if (c.files && "body" in c.files) throw new Error(`endpoint "${name}": "body" is reserved as the multipart JSON field name`);
61
+ if (c.streamOut && c.response) throw new Error(`endpoint "${name}": streamOut excludes response`);
62
+ if (c.responses && c.response) throw new Error(`endpoint "${name}": responses (multi-status) excludes response`);
63
+ if (c.responses && c.streamOut) throw new Error(`endpoint "${name}": responses excludes streamOut`);
64
+ if (c.streamEncoding && !(c.streamOut instanceof z.ZodType)) throw new Error(`endpoint "${name}": streamEncoding requires a typed (schema) streamOut`);
65
+ if (c.bodyEncoding === "urlencoded" && !(c.body instanceof z.ZodObject)) throw new Error(`endpoint "${name}": urlencoded bodies must be z.object(...)`);
66
+ if (c.download && typeof c.streamOut !== "string") throw new Error(`endpoint "${name}": download requires a raw (content-type) streamOut`);
67
+ for (const kind of [
68
+ "params",
69
+ "query",
70
+ "headers",
71
+ "cookies"
72
+ ]) {
73
+ const s = c[kind];
74
+ if (s && !(s instanceof z.ZodObject)) throw new Error(`endpoint "${name}": ${kind} must be a z.object(...)`);
75
+ }
76
+ normalizeEndpoint(name, def);
77
+ }
78
+ let cached;
79
+ Object.defineProperty(spec, SPEC_MANIFEST, {
80
+ value: () => cached ??= manifestFromSpec(spec),
81
+ enumerable: false,
82
+ configurable: true
83
+ });
84
+ return spec;
85
+ }
86
+ /**
87
+ * Global-registry symbol under which {@link spec} stashes its lazy
88
+ * {@link manifestFromSpec} builder. Global (`Symbol.for`) so consumers — notably
89
+ * the zod-free `client` — can read it off a spec value **without importing this
90
+ * module**, so a manifest-only bundle never pulls in the deriver or zod.
91
+ *
92
+ * @internal
93
+ */
94
+ const SPEC_MANIFEST = Symbol.for("ayepi.manifest");
95
+ /**
96
+ * Derive the zod-free {@link Manifest} from a spec — exactly the routing data
97
+ * `app.manifest()` returns, computed purely from the endpoint/event definitions.
98
+ * Used by {@link server} and stamped (cached) onto every spec by {@link spec}, so
99
+ * {@link client} can take a spec directly.
100
+ *
101
+ * This runs the zod-bearing {@link normalizeEndpoint}, so importing it — or
102
+ * handing a spec to the client — brings zod into the bundle. Pass a prebuilt
103
+ * manifest instead to keep a frontend schema-free.
104
+ */
105
+ function manifestFromSpec(spec) {
106
+ const eps = Object.entries(spec.endpoints).map(([name, def]) => normalizeEndpoint(name, def));
107
+ return {
108
+ endpoints: Object.fromEntries(eps.map((e) => [e.name, {
109
+ method: e.method,
110
+ path: e.path,
111
+ ws: e.ws,
112
+ httpOnly: e.httpOnly,
113
+ streamIn: e.streamInCt,
114
+ itemsIn: e.itemsIn,
115
+ streamOut: e.streamOutCt,
116
+ items: e.items,
117
+ p: e.p,
118
+ q: e.q,
119
+ b: e.bRaw ? "raw" : e.b,
120
+ f: e.f,
121
+ hasBody: Boolean(e.def.cfg.body),
122
+ hasHeaders: Boolean(e.def.cfg.headers),
123
+ multi: e.multi,
124
+ bodyEnc: e.bodyEnc
125
+ }])),
126
+ events: Object.fromEntries(Object.entries(spec.events ?? {}).map(([name, cfg]) => [name, {
127
+ ws: cfg.ws ?? name,
128
+ hasParams: Boolean(cfg.params)
129
+ }]))
130
+ };
131
+ }
132
+ /** Read the keys of a `z.object` schema, or `null` for any other (or absent) schema. @internal */
133
+ function objectKeys(s) {
134
+ if (!s) return null;
135
+ if (s instanceof z.ZodObject) return Object.keys(s.shape);
136
+ return null;
137
+ }
138
+ /**
139
+ * Resolve an endpoint declaration into a {@link NormalizedEp}: assemble its path
140
+ * parts (prefixes → own path → default), verify exact-once param declaration and
141
+ * positioning, and enforce kind disjointness. Throws on any violation.
142
+ *
143
+ * @internal
144
+ */
145
+ function normalizeEndpoint(name, def) {
146
+ const c = def.cfg;
147
+ const chain = resolveChain(def.mws);
148
+ const fail = (msg) => {
149
+ throw new Error(`endpoint "${name}": ${msg}`);
150
+ };
151
+ const loaders = /* @__PURE__ */ new Map();
152
+ for (const m of chain) if (m.paramKey && m.paramSchema) loaders.set(m.paramKey, m.paramSchema);
153
+ const tplSchemas = /* @__PURE__ */ new Map();
154
+ const declareTpl = (tpl, where) => {
155
+ for (const k of tpl.keys) {
156
+ if (loaders.has(k) || tplSchemas.has(k)) fail(`param ":${k}" is declared more than once (${where})`);
157
+ tplSchemas.set(k, tpl.schemas[k]);
158
+ }
159
+ };
160
+ for (const pre of def.prefixes) if (typeof pre !== "string") declareTpl(pre, "prefix path");
161
+ const ownTpl = typeof c.path === "object" && c.path !== null ? c.path : null;
162
+ if (ownTpl) declareTpl(ownTpl, "endpoint path");
163
+ const cfgP = objectKeys(c.params) ?? [];
164
+ for (const k of cfgP) if (loaders.has(k) || tplSchemas.has(k)) fail(`param ":${k}" is declared more than once (params schema)`);
165
+ const declared = new Set([
166
+ ...loaders.keys(),
167
+ ...tplSchemas.keys(),
168
+ ...cfgP
169
+ ]);
170
+ const parts = [];
171
+ for (const pre of def.prefixes) parts.push(...typeof pre === "string" ? splitPattern(pre) : pre.parts);
172
+ if (ownTpl) parts.push(...ownTpl.parts);
173
+ else if (typeof c.path === "string") parts.push(...splitPattern(c.path));
174
+ else {
175
+ parts.push({
176
+ t: "lit",
177
+ v: name
178
+ });
179
+ const positioned = new Set(paramKeys(parts));
180
+ for (const k of declared) if (!positioned.has(k)) parts.push({
181
+ t: "param",
182
+ k
183
+ });
184
+ }
185
+ const positions = paramKeys(parts);
186
+ const posSet = new Set(positions);
187
+ if (positions.length !== posSet.size) fail(`path positions a param more than once: ${joinPattern(parts)}`);
188
+ for (const k of positions) if (!declared.has(k)) fail(`path references undeclared param ":${k}"`);
189
+ for (const k of declared) if (!posSet.has(k)) fail(`declared param ":${k}" has no position in path ${joinPattern(parts)}`);
190
+ const p = [...declared];
191
+ const q = objectKeys(c.query) ?? [];
192
+ const b = c.body ? objectKeys(c.body) : null;
193
+ const bRaw = Boolean(c.body) && b === null;
194
+ const f = Object.keys(c.files ?? {});
195
+ const taken = /* @__PURE__ */ new Map();
196
+ for (const [kind, keys] of [
197
+ ["path", p],
198
+ ["query", q],
199
+ ["body", b ?? []],
200
+ ["files", f]
201
+ ]) for (const k of keys) {
202
+ const prev = taken.get(k);
203
+ if (prev) fail(`key "${k}" appears in both ${prev} and ${kind} — kinds must be disjoint`);
204
+ taken.set(k, kind);
205
+ }
206
+ if (bRaw && (p.length > 0 || q.length > 0 || f.length > 0)) fail("a non-object body is the entire data payload — params/query/files are not allowed alongside it");
207
+ const itemsIn = c.streamIn instanceof z.ZodType;
208
+ const streamInCt = typeof c.streamIn === "string" ? c.streamIn : itemsIn ? "application/x-ndjson" : null;
209
+ const items = c.streamOut instanceof z.ZodType;
210
+ const sse = items && c.streamEncoding === "sse";
211
+ const streamOutCt = typeof c.streamOut === "string" ? c.streamOut : items ? sse ? "text/event-stream" : "application/x-ndjson" : null;
212
+ const httpOnly = Boolean(c.httpOnly || typeof c.streamIn === "string" || typeof c.streamOut === "string" || f.length > 0);
213
+ return {
214
+ name,
215
+ def,
216
+ method: c.method ?? "POST",
217
+ parts,
218
+ path: joinPattern(parts),
219
+ ws: c.ws ?? null,
220
+ wsEligible: !httpOnly,
221
+ httpOnly,
222
+ streamInCt,
223
+ itemsIn,
224
+ streamOutCt,
225
+ items,
226
+ sse,
227
+ multi: Boolean(c.responses && Object.keys(c.responses).length > 0),
228
+ bodyEnc: c.body ? c.bodyEncoding ?? "json" : null,
229
+ p,
230
+ q,
231
+ b,
232
+ bRaw,
233
+ f,
234
+ loaders,
235
+ tplSchemas,
236
+ chain
237
+ };
238
+ }
239
+ //#endregion
240
+ //#region src/middleware.ts
241
+ /**
242
+ * Declare the context a middleware def provides — `provides: ctx<{ user: User }>()`.
243
+ * Returns a type-only token; carries no runtime value.
244
+ */
245
+ function ctx() {
246
+ return {};
247
+ }
248
+ function placeholderRun(name) {
249
+ return () => {
250
+ throw new Error(`middleware "${name}" has no implementation — bind it with implement(api).middleware(def, impl)`);
251
+ };
252
+ }
253
+ function makeMiddleware(name, requires, optional, paramKey, paramSchema, doc) {
254
+ const self = {
255
+ kind: "middleware",
256
+ name,
257
+ requires,
258
+ optional,
259
+ paramKey,
260
+ paramSchema,
261
+ doc,
262
+ run: placeholderRun(name),
263
+ with(...mws) {
264
+ return makeStack([self, ...mws], []);
265
+ },
266
+ path(p) {
267
+ return makeStack([self], [p]);
268
+ },
269
+ endpoint(cfg) {
270
+ return makeStack([self], []).endpoint(cfg);
271
+ },
272
+ group(g) {
273
+ return makeStack([self], []).group(g);
274
+ }
275
+ };
276
+ return self;
277
+ }
278
+ function makeStack(mws, prefixes) {
279
+ return {
280
+ kind: "stack",
281
+ mws,
282
+ prefixes,
283
+ with(...more) {
284
+ return makeStack([...mws, ...more], prefixes);
285
+ },
286
+ path(p) {
287
+ return makeStack(mws, [...prefixes, p]);
288
+ },
289
+ endpoint(cfg) {
290
+ return makeEndpoint(cfg, mws, prefixes);
291
+ },
292
+ group(g) {
293
+ const out = {};
294
+ for (const [k, cfg] of Object.entries(g)) out[k] = makeEndpoint(cfg, mws, prefixes);
295
+ return out;
296
+ }
297
+ };
298
+ }
299
+ /**
300
+ * Compose one or more middleware into a {@link Stack} — the free-function form of
301
+ * {@link Middleware.with | mw.with(...)}, which reads more naturally when bundling
302
+ * several middleware at a group: `...use(auth, tel).group({ … })` instead of
303
+ * `...auth.with(tel).group({ … })`.
304
+ *
305
+ * The middleware run in the order given (subject to `requires`/`optional`
306
+ * resolution), exactly as with `.with()`.
307
+ *
308
+ * @typeParam M - the middleware list, in declared order (at least one).
309
+ *
310
+ * @example
311
+ * ```ts
312
+ * spec({
313
+ * endpoints: {
314
+ * ...use(auth, tel).group({ me, createJob }),
315
+ * ...use(auth, jobLoader).path('/jobs/:jobId').group({ jobStatus }),
316
+ * },
317
+ * })
318
+ * ```
319
+ */
320
+ function use(...mws) {
321
+ return makeStack([...mws], []);
322
+ }
323
+ function middlewareImpl(name, opts) {
324
+ return makeMiddleware(name, opts?.requires ?? [], opts?.optional ?? [], void 0, void 0, opts?.doc);
325
+ }
326
+ function loaderImpl(key, schema, opts) {
327
+ return makeMiddleware(key, opts?.requires ?? [], opts?.optional ?? [], key, schema, opts?.doc);
328
+ }
329
+ /**
330
+ * Create a middleware **def** — a frontend-safe contract (name, contributed
331
+ * context, docs, dependencies) with **no** runtime fn. Bind the implementation
332
+ * with `implement(api).middleware(def, impl)` in your server entry.
333
+ *
334
+ * @example A plain def that provides `{ user }`:
335
+ * ```ts
336
+ * const auth = middleware('auth', { provides: ctx<{ user: User }>() })
337
+ * // server.ts:
338
+ * implement(api).middleware(auth, async (io) => io.next({ user: await authenticate(io.req) }))
339
+ * ```
340
+ *
341
+ * @example A def that requires `auth` (auto-included) and provides `{ org }`:
342
+ * ```ts
343
+ * const org = middleware('org', { provides: ctx<{ org: Org }>(), requires: [auth] })
344
+ * // server.ts:
345
+ * implement(api).middleware(org, async (io) => io.next({ org: await loadOrg(io.ctx.user) }))
346
+ * ```
347
+ *
348
+ * @example A no-context, purely-runtime def (e.g. logging/telemetry):
349
+ * ```ts
350
+ * const log = middleware('log')
351
+ * implement(api).middleware(log, async (io) => io.next())
352
+ * ```
353
+ *
354
+ * @example A loader def that owns the `:projectId` path param:
355
+ * ```ts
356
+ * const project = middleware.loader('projectId', z.uuid(), { provides: ctx<{ project: Project }>() })
357
+ * // server.ts:
358
+ * implement(api).middleware(project, async (io) => io.next({ project: await loadProject(io.value) }))
359
+ * ```
360
+ */
361
+ const middleware = Object.assign(middlewareImpl, { loader: loaderImpl });
362
+ /**
363
+ * Create a middleware that **injects a typed value** onto the handler context under
364
+ * `name` — the one-call form of `middleware(name, { provides: ctx<{ [name]: V }>() })`
365
+ * plus its impl. Hand it a function, a service object, config, or any data, and every
366
+ * endpoint whose chain includes it reads `io.ctx[name]`.
367
+ *
368
+ * The result is **both** the def (use it in the spec: `use(svc).group(…)` /
369
+ * `svc.endpoint(…)`) and the bound def+impl (bind it once:
370
+ * `implement(api).middleware(svc)`).
371
+ *
372
+ * @typeParam N - the context key (a string literal).
373
+ * @typeParam V - the injected value's type.
374
+ * @param name - the key the value is injected under (also the middleware name).
375
+ * @param value - the value to inject, or a factory `(io) => value` re-run per
376
+ * invocation (may be async). A **callable** `value` is treated as a factory — to
377
+ * inject a bare function as the value, wrap it: `provide('fn', () => myFn)`.
378
+ *
379
+ * @example
380
+ * ```ts
381
+ * const services = provide('services', { db, mailer }); // shared.ts / spec
382
+ * export const api = spec({ endpoints: { ...services.group({ sendInvite }) } });
383
+ * implement(api).middleware(services); // server.ts (bind once)
384
+ * // handler: ({ services }) => services.mailer.send(…)
385
+ * ```
386
+ */
387
+ function provide(name, value) {
388
+ const def = middleware(name, { provides: ctx() });
389
+ const impl = async (io) => {
390
+ const v = typeof value === "function" ? await value(io) : value;
391
+ return io.next({ [name]: v });
392
+ };
393
+ return Object.assign(def, {
394
+ def,
395
+ impl
396
+ });
397
+ }
398
+ /**
399
+ * Expand `requires` (auto-include) and topologically order a middleware list.
400
+ *
401
+ * `requires` edges pull dependencies in and force them earlier; `optional` edges
402
+ * only reorder middleware that are *already present*. Throws on a dependency
403
+ * cycle.
404
+ *
405
+ * @internal
406
+ */
407
+ function resolveChain(mws) {
408
+ const present = /* @__PURE__ */ new Set();
409
+ const order = [];
410
+ const visit = (m, trail) => {
411
+ if (present.has(m)) return;
412
+ if (trail.has(m)) throw new Error(`middleware cycle involving "${m.name}"`);
413
+ trail.add(m);
414
+ for (const r of m.requires) visit(r, trail);
415
+ trail.delete(m);
416
+ present.add(m);
417
+ order.push(m);
418
+ };
419
+ for (const m of mws) visit(m, /* @__PURE__ */ new Set());
420
+ const idx = new Map(order.map((m, i) => [m, i]));
421
+ return [...order].sort((a, b) => {
422
+ if (a.optional.includes(b)) return 1;
423
+ if (b.optional.includes(a)) return -1;
424
+ return idx.get(a) - idx.get(b);
425
+ });
426
+ }
427
+ //#endregion
428
+ //#region src/broker.ts
429
+ /**
430
+ * In-process {@link Broker} — the default when no broker is supplied.
431
+ *
432
+ * Share a single instance between multiple {@link Server}s to simulate a
433
+ * multi-pod deployment in tests: an `emit` on one server is heard by
434
+ * subscribers on the other.
435
+ *
436
+ * @example
437
+ * ```ts
438
+ * const broker = localBroker()
439
+ * const a = server(api, handlers, { broker })
440
+ * const b = server(api, handlers, { broker })
441
+ * a.emit('systemNotice', { msg: 'hi' }) // delivered to b's subscribers too
442
+ * ```
443
+ */
444
+ function localBroker() {
445
+ const listeners = /* @__PURE__ */ new Set();
446
+ return {
447
+ publish(m) {
448
+ for (const l of listeners) l(m);
449
+ },
450
+ subscribe(l) {
451
+ listeners.add(l);
452
+ return () => void listeners.delete(l);
453
+ }
454
+ };
455
+ }
456
+ //#endregion
457
+ //#region src/jsonschema.ts
458
+ /**
459
+ * # JSON Schema helpers
460
+ *
461
+ * Thin wrappers over `z.toJSONSchema` shared by the OpenAPI and AsyncAPI
462
+ * generators. Internal to the package.
463
+ *
464
+ * @module
465
+ */
466
+ /**
467
+ * Convert a zod schema to a JSON Schema (input view), degrading gracefully to a
468
+ * placeholder for schemas zod can't represent.
469
+ *
470
+ * @internal
471
+ */
472
+ function jsonSchema(s) {
473
+ try {
474
+ return z.toJSONSchema(s, { io: "input" });
475
+ } catch {
476
+ return {
477
+ type: "string",
478
+ description: "unrepresentable schema"
479
+ };
480
+ }
481
+ }
482
+ /**
483
+ * Extract the `properties` map of an object schema's JSON Schema, or `{}` when
484
+ * the schema is absent or not object-shaped.
485
+ *
486
+ * @internal
487
+ */
488
+ function propSchemas(s) {
489
+ const js = s ? jsonSchema(s) : null;
490
+ if (js && typeof js === "object" && !Array.isArray(js) && typeof js.properties === "object") return js.properties;
491
+ return {};
492
+ }
493
+ //#endregion
494
+ //#region src/openapi.ts
495
+ /**
496
+ * Build the OpenAPI 3.1 document for a set of normalized endpoints.
497
+ *
498
+ * @param eps - the spec's normalized endpoints.
499
+ * @param specDoc - spec-level doc patches (`doc.openapi`).
500
+ * @param info - optional document `info` (title/version).
501
+ * @internal
502
+ */
503
+ function buildOpenapi(eps, specDoc, info) {
504
+ const paths = {};
505
+ const securitySchemes = {};
506
+ for (const e of eps) {
507
+ const c = e.def.cfg;
508
+ const oaPath = "/" + e.parts.map((part) => part.t === "param" ? `{${part.k}}` : part.v).join("/");
509
+ const pProps = {
510
+ ...propSchemas(c.params),
511
+ ...Object.fromEntries([...e.tplSchemas].map(([k, s]) => [k, jsonSchema(s)])),
512
+ ...Object.fromEntries([...e.loaders].map(([k, s]) => [k, jsonSchema(s)]))
513
+ };
514
+ const qProps = propSchemas(c.query);
515
+ const hProps = propSchemas(c.headers);
516
+ const ckProps = propSchemas(c.cookies);
517
+ const parameters = [
518
+ ...e.p.map((k) => ({
519
+ name: k,
520
+ in: "path",
521
+ required: true,
522
+ schema: pProps[k] ?? { type: "string" }
523
+ })),
524
+ ...e.q.map((k) => ({
525
+ name: k,
526
+ in: "query",
527
+ required: false,
528
+ schema: qProps[k] ?? { type: "string" }
529
+ })),
530
+ ...Object.entries(hProps).map(([k, schema]) => ({
531
+ name: k,
532
+ in: "header",
533
+ required: true,
534
+ schema
535
+ })),
536
+ ...Object.entries(ckProps).map(([k, schema]) => ({
537
+ name: k,
538
+ in: "cookie",
539
+ required: false,
540
+ schema
541
+ }))
542
+ ];
543
+ let requestBody;
544
+ if (c.streamIn) requestBody = e.itemsIn ? {
545
+ required: true,
546
+ content: { [e.streamInCt]: { schema: {
547
+ type: "array",
548
+ items: jsonSchema(c.streamIn)
549
+ } } }
550
+ } : {
551
+ required: true,
552
+ content: { [e.streamInCt]: { schema: {
553
+ type: "string",
554
+ format: "binary"
555
+ } } }
556
+ };
557
+ else if (e.f.length > 0) {
558
+ const props = { ...c.body ? { body: jsonSchema(c.body) } : {} };
559
+ for (const k of e.f) props[k] = {
560
+ type: "string",
561
+ format: "binary"
562
+ };
563
+ requestBody = {
564
+ required: true,
565
+ content: { "multipart/form-data": { schema: {
566
+ type: "object",
567
+ properties: props
568
+ } } }
569
+ };
570
+ } else if (c.body) requestBody = {
571
+ required: true,
572
+ content: { [e.bodyEnc === "urlencoded" ? "application/x-www-form-urlencoded" : "application/json"]: { schema: jsonSchema(c.body) } }
573
+ };
574
+ const responses = c.streamOut ? e.items ? { "200": {
575
+ description: "NDJSON item stream",
576
+ content: { [e.streamOutCt]: { schema: {
577
+ type: "array",
578
+ items: jsonSchema(c.streamOut)
579
+ } } }
580
+ } } : { "200": {
581
+ description: "stream",
582
+ content: { [e.streamOutCt]: { schema: {
583
+ type: "string",
584
+ format: "binary"
585
+ } } }
586
+ } } : c.responses ? Object.fromEntries(Object.entries(c.responses).map(([st, schema]) => [st, {
587
+ description: `status ${st}`,
588
+ content: { "application/json": { schema: jsonSchema(schema) } }
589
+ }])) : c.response ? { "200": {
590
+ description: "ok",
591
+ content: { "application/json": { schema: jsonSchema(c.response) } }
592
+ } } : { "204": { description: "no content" } };
593
+ for (const [status, schema] of Object.entries(c.errors ?? {})) responses[status] = {
594
+ description: `declared error ${status}`,
595
+ content: { "application/json": { schema: jsonSchema(schema) } }
596
+ };
597
+ let op = {
598
+ operationId: c.doc?.operationId ?? e.name,
599
+ parameters,
600
+ responses
601
+ };
602
+ if (requestBody) op.requestBody = requestBody;
603
+ if (c.doc?.summary) op.summary = c.doc.summary;
604
+ if (c.doc?.description) op.description = c.doc.description;
605
+ if (c.doc?.tags) op.tags = [...c.doc.tags];
606
+ if (c.doc?.deprecated) op.deprecated = true;
607
+ const security = [];
608
+ for (const m of e.chain) for (const [schemeName, scheme] of Object.entries(m.doc?.security ?? {})) {
609
+ securitySchemes[schemeName] = scheme;
610
+ security.push({ [schemeName]: [] });
611
+ }
612
+ if (security.length > 0) op.security = security;
613
+ for (const m of e.chain) if (m.doc?.openapi) op = m.doc.openapi(op);
614
+ if (c.doc?.openapi) op = c.doc.openapi(op);
615
+ const entry = paths[oaPath] ?? {};
616
+ entry[e.method.toLowerCase()] = op;
617
+ paths[oaPath] = entry;
618
+ }
619
+ let doc = {
620
+ openapi: "3.1.0",
621
+ info: {
622
+ title: info?.title ?? "api",
623
+ version: info?.version ?? "0.0.0"
624
+ },
625
+ paths,
626
+ ...Object.keys(securitySchemes).length > 0 ? { components: { securitySchemes } } : {}
627
+ };
628
+ if (specDoc?.openapi) doc = specDoc.openapi(doc);
629
+ return doc;
630
+ }
631
+ //#endregion
632
+ //#region src/asyncapi.ts
633
+ /**
634
+ * Convert a params object's JSON-Schema `properties` into AsyncAPI 3.0 **Parameter
635
+ * Objects**. Channel parameters are string-valued and must NOT carry a JSON-Schema
636
+ * `type`/`format` (the 3.0 Parameter Object only allows `description`/`enum`/`default`/
637
+ * `examples`/`location`) — emitting `type` makes a validator reject the document.
638
+ */
639
+ function paramObjects(props) {
640
+ const out = {};
641
+ for (const [k, v] of Object.entries(props)) {
642
+ const o = v;
643
+ const param = {};
644
+ if (typeof o.description === "string") param.description = o.description;
645
+ if (Array.isArray(o.enum)) param.enum = o.enum.map((e) => String(e));
646
+ if (o.default !== void 0) param.default = String(o.default);
647
+ out[k] = param;
648
+ }
649
+ return out;
650
+ }
651
+ /**
652
+ * Build the AsyncAPI 3.0 document for a set of normalized endpoints and events.
653
+ *
654
+ * @param eps - the spec's normalized endpoints.
655
+ * @param events - the spec's normalized events (name + config + resolved ws id).
656
+ * @param specDoc - spec-level doc patches (`doc.asyncapi`).
657
+ * @param info - optional document `info` (title/version).
658
+ * @internal
659
+ */
660
+ function buildAsyncapi(eps, events, specDoc, info) {
661
+ const channels = {};
662
+ const operations = {};
663
+ for (const ev of events) {
664
+ let channel = {
665
+ address: ev.ws,
666
+ ...ev.cfg.doc?.summary ? { summary: ev.cfg.doc.summary } : {},
667
+ ...ev.cfg.doc?.description ? { description: ev.cfg.doc.description } : {},
668
+ ...ev.cfg.params ? { parameters: paramObjects(propSchemas(ev.cfg.params)) } : {},
669
+ messages: { event: { payload: jsonSchema(ev.cfg.data) } }
670
+ };
671
+ if (ev.cfg.doc?.asyncapi) channel = ev.cfg.doc.asyncapi(channel);
672
+ channels[ev.ws] = channel;
673
+ operations[`receive.${ev.name}`] = {
674
+ action: "receive",
675
+ channel: { $ref: `#/channels/${ev.ws}` }
676
+ };
677
+ }
678
+ for (const e of eps) {
679
+ if (!e.wsEligible) continue;
680
+ const c = e.def.cfg;
681
+ const address = e.ws ?? e.path;
682
+ const dataSchema = e.bRaw ? jsonSchema(c.body) : {
683
+ type: "object",
684
+ properties: {
685
+ ...Object.fromEntries([...e.tplSchemas].map(([k, sch]) => [k, jsonSchema(sch)])),
686
+ ...Object.fromEntries([...e.loaders].map(([k, sch]) => [k, jsonSchema(sch)])),
687
+ ...propSchemas(c.params),
688
+ ...propSchemas(c.query),
689
+ ...propSchemas(c.body)
690
+ }
691
+ };
692
+ const ref = (key) => `#/channels/${key.replace(/\//g, "~1")}`;
693
+ channels[address] = {
694
+ address,
695
+ ...e.ws ? {} : { description: `frame: { id, type: "${e.path}", method: "${e.method}", data }` },
696
+ messages: { request: { payload: {
697
+ type: "object",
698
+ properties: {
699
+ id: { type: "string" },
700
+ type: { const: address },
701
+ ...e.ws ? {} : { method: { const: e.method } },
702
+ data: dataSchema
703
+ }
704
+ } } }
705
+ };
706
+ const replyKey = `${address}/reply`;
707
+ const declared = c.errors ? Object.keys(c.errors) : [];
708
+ const replyProps = {
709
+ id: { type: "string" },
710
+ $status: {
711
+ type: "integer",
712
+ description: "2xx on success"
713
+ }
714
+ };
715
+ if (c.response) replyProps.data = jsonSchema(c.response);
716
+ channels[replyKey] = {
717
+ address,
718
+ messages: {
719
+ reply: { payload: {
720
+ type: "object",
721
+ properties: replyProps
722
+ } },
723
+ error: {
724
+ ...declared.length > 0 ? { description: `error frame — declared statuses: ${declared.join(", ")}` } : {},
725
+ payload: {
726
+ type: "object",
727
+ required: ["$status"],
728
+ properties: {
729
+ id: { type: "string" },
730
+ $status: {
731
+ type: "integer",
732
+ description: "non-2xx — the client throws an ApiError"
733
+ },
734
+ $error: {
735
+ type: "string",
736
+ description: "human-readable error message"
737
+ },
738
+ $code: {
739
+ type: "string",
740
+ description: "machine-readable error code (e.g. UNAUTHORIZED, VALIDATION)"
741
+ },
742
+ data: { description: "typed error body for declared errors" }
743
+ }
744
+ }
745
+ }
746
+ }
747
+ };
748
+ const op = {
749
+ action: "send",
750
+ channel: { $ref: ref(address) },
751
+ messages: [{ $ref: `${ref(address)}/messages/request` }],
752
+ reply: {
753
+ channel: { $ref: ref(replyKey) },
754
+ messages: [{ $ref: `${ref(replyKey)}/messages/reply` }, { $ref: `${ref(replyKey)}/messages/error` }]
755
+ }
756
+ };
757
+ operations[`call.${e.name}`] = op;
758
+ }
759
+ let doc = {
760
+ asyncapi: "3.0.0",
761
+ info: {
762
+ title: info?.title ?? "api",
763
+ version: info?.version ?? "0.0.0"
764
+ },
765
+ channels,
766
+ operations
767
+ };
768
+ if (specDoc?.asyncapi) doc = specDoc.asyncapi(doc);
769
+ return doc;
770
+ }
771
+ //#endregion
772
+ //#region src/docs-ui.ts
773
+ const DEFAULTS = {
774
+ openapiJson: "/docs/openapi.json",
775
+ asyncapiJson: "/docs/asyncapi.json",
776
+ swagger: "/docs/swagger",
777
+ redoc: "/docs/redoc",
778
+ asyncapi: "/docs/asyncapi"
779
+ };
780
+ /** Resolve the `docs` option into concrete routes, or `null` when disabled. @internal */
781
+ function normalizeDocs(opt) {
782
+ if (!opt) return null;
783
+ const o = opt === true ? {} : opt;
784
+ const pick = (v, d) => v === false ? null : v ?? d;
785
+ return {
786
+ info: o.info,
787
+ openapiJson: pick(o.openapiJson, DEFAULTS.openapiJson),
788
+ asyncapiJson: pick(o.asyncapiJson, DEFAULTS.asyncapiJson),
789
+ swagger: pick(o.swagger, DEFAULTS.swagger),
790
+ redoc: pick(o.redoc, DEFAULTS.redoc),
791
+ asyncapi: pick(o.asyncapi, DEFAULTS.asyncapi)
792
+ };
793
+ }
794
+ /** Escape a string for safe inclusion in an HTML attribute. */
795
+ function attr(s) {
796
+ return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
797
+ }
798
+ /** A self-contained Swagger UI page (loaded from unpkg) pointed at `specUrl`. */
799
+ function swaggerHtml(specUrl, title = "API — Swagger UI") {
800
+ return `<!doctype html>
801
+ <html lang="en">
802
+ <head>
803
+ <meta charset="utf-8" />
804
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
805
+ <title>${attr(title)}</title>
806
+ <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" />
807
+ </head>
808
+ <body>
809
+ <div id="swagger-ui"></div>
810
+ <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js" crossorigin><\/script>
811
+ <script>
812
+ window.ui = SwaggerUIBundle({ url: ${JSON.stringify(specUrl)}, dom_id: '#swagger-ui' })
813
+ <\/script>
814
+ </body>
815
+ </html>`;
816
+ }
817
+ /** A self-contained ReDoc page (loaded from the Redocly CDN) pointed at `specUrl`. */
818
+ function redocHtml(specUrl, title = "API — ReDoc") {
819
+ return `<!doctype html>
820
+ <html lang="en">
821
+ <head>
822
+ <meta charset="utf-8" />
823
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
824
+ <title>${attr(title)}</title>
825
+ </head>
826
+ <body style="margin: 0">
827
+ <redoc spec-url="${attr(specUrl)}"></redoc>
828
+ <script src="https://cdn.redocly.com/redoc/latest/bundles/redoc.standalone.js" crossorigin><\/script>
829
+ </body>
830
+ </html>`;
831
+ }
832
+ /** A self-contained AsyncAPI page (the `@asyncapi/web-component`, loaded from unpkg) pointed at `specUrl`. */
833
+ function asyncapiHtml(specUrl, title = "API — AsyncAPI") {
834
+ const css = "https://unpkg.com/@asyncapi/react-component@2/styles/default.min.css";
835
+ return `<!doctype html>
836
+ <html lang="en">
837
+ <head>
838
+ <meta charset="utf-8" />
839
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
840
+ <title>${attr(title)}</title>
841
+ <link rel="stylesheet" href="${css}" />
842
+ </head>
843
+ <body>
844
+ <asyncapi-component schemaUrl="${attr(specUrl)}" cssImportPath="${css}"></asyncapi-component>
845
+ <script src="https://unpkg.com/@asyncapi/web-component@2/lib/asyncapi-web-component.js" defer><\/script>
846
+ </body>
847
+ </html>`;
848
+ }
849
+ //#endregion
850
+ //#region src/server.ts
851
+ /**
852
+ * # Server
853
+ *
854
+ * The fetch-native server. {@link server} takes a {@link AnySpec | spec} and
855
+ * handler bags and returns a {@link Server} whose `fetch(Request) => Response` is
856
+ * the whole HTTP surface, plus a `ws` object (`open`/`message`/`close`) that
857
+ * speaks the JSON frame protocol. Everything is web-standard `Request`/`Response`
858
+ * /streams — Node/Express/etc. live in adapters at the edge, never here.
859
+ *
860
+ * Responsibilities: middleware chain execution, payload assembly from the single
861
+ * `data` kind tables, HTTP body parsing (JSON/urlencoded/multipart/raw/items),
862
+ * raw + item + SSE streaming with Range/206/HEAD, CORS, the WebSocket call /
863
+ * stream / sub-unsub protocol, event fanout through the {@link Broker}, and
864
+ * OpenAPI/AsyncAPI generation (delegated to {@link buildOpenapi}/{@link buildAsyncapi}).
865
+ *
866
+ * @module
867
+ */
868
+ /** Readable-side highWaterMark for the `$out` transform: accept the first chunk before a reader attaches so headers can commit. */
869
+ const OUT_HIGH_WATER_MARK = 1;
870
+ /** Synthetic status for an aborted/cancelled call (no HTTP response is produced). */
871
+ const ABORTED_STATUS = 0;
872
+ function parseCookieHeader(header) {
873
+ const out = {};
874
+ if (!header) return out;
875
+ for (const part of header.split(";")) {
876
+ const i = part.indexOf("=");
877
+ if (i < 0) continue;
878
+ out[part.slice(0, i).trim()] = decodeURIComponent(part.slice(i + 1).trim());
879
+ }
880
+ return out;
881
+ }
882
+ function serializeCookie(name, value, o) {
883
+ let s = `${name}=${encodeURIComponent(value)}`;
884
+ if (o?.path) s += `; Path=${o.path}`;
885
+ if (o?.domain) s += `; Domain=${o.domain}`;
886
+ if (o?.maxAge !== void 0) s += `; Max-Age=${o.maxAge}`;
887
+ if (o?.expires) s += `; Expires=${o.expires.toUTCString()}`;
888
+ if (o?.httpOnly) s += "; HttpOnly";
889
+ if (o?.secure) s += "; Secure";
890
+ if (o?.sameSite) s += `; SameSite=${o.sameSite}`;
891
+ return s;
892
+ }
893
+ /**
894
+ * Begin implementing a spec. The returned {@link Implementor} is a chainable
895
+ * builder: bind middleware impls with `.middleware(def, impl)` and endpoint
896
+ * handlers with `.handlers({...})`/`.handle(name, fn)`, then hand it to
897
+ * {@link server}. Split work across multiple `implement()` builders if you like —
898
+ * `server()` merges them.
899
+ *
900
+ * @example
901
+ * ```ts
902
+ * const impl = implement(api)
903
+ * .middleware(auth, async (io) => io.next({ user: await authenticate(io.req) }))
904
+ * .handlers({ getUser: ({ data }) => loadUser(data.id) })
905
+ * ```
906
+ */
907
+ function implement(spec) {
908
+ const bag = {
909
+ handlers: /* @__PURE__ */ new Map(),
910
+ middleware: /* @__PURE__ */ new Map()
911
+ };
912
+ const self = {
913
+ __bag: bag,
914
+ middleware(defOrBound, impl) {
915
+ if (impl) bag.middleware.set(defOrBound, impl);
916
+ else {
917
+ const b = defOrBound;
918
+ bag.middleware.set(b.def, b.impl);
919
+ }
920
+ return self;
921
+ },
922
+ handlers(h) {
923
+ for (const [k, fn] of Object.entries(h)) {
924
+ if (bag.handlers.has(k)) throw new Error(`duplicate handler for endpoint "${k}"`);
925
+ bag.handlers.set(k, fn);
926
+ }
927
+ return self;
928
+ },
929
+ handle(name, fn) {
930
+ if (bag.handlers.has(name)) throw new Error(`duplicate handler for endpoint "${name}"`);
931
+ bag.handlers.set(name, fn);
932
+ return self;
933
+ }
934
+ };
935
+ return self;
936
+ }
937
+ /**
938
+ * View a running {@link Server} as a typed {@link LocalClient} for spec `S` — for
939
+ * calling endpoints in-process (full chain + validation, no HTTP) by name + data.
940
+ *
941
+ * @param app - the running server (its `call` is the in-process caller).
942
+ * @param spec - the spec to type against (its endpoints/`CallReturn` shape `S`).
943
+ *
944
+ * @example
945
+ * ```ts
946
+ * const users = localClient(app, usersSpec);
947
+ * const u = await users.call('getUser', { id: 'u1' }); // typed, in-process
948
+ * ```
949
+ */
950
+ function localClient(app, spec) {
951
+ return app;
952
+ }
953
+ /**
954
+ * Assemble a {@link Server} from a spec and one or more {@link implement} builders.
955
+ *
956
+ * Every endpoint must have exactly one handler: a missing handler is a **compile
957
+ * error** that names the offending endpoints (via the final `error` argument), and
958
+ * a duplicate/unknown handler throws at startup. Every middleware in an endpoint or
959
+ * event-guard chain must be bound via `.middleware(def, impl)` — an unbound def
960
+ * throws at assembly.
961
+ *
962
+ * @param spec - the validated spec from {@link spec}.
963
+ * @param builders - one or more builders from {@link implement}.
964
+ * @param rest - `[options?]` when all handlers are present, else `[{ missingHandlers }]`.
965
+ *
966
+ * @example
967
+ * ```ts
968
+ * const app = server(api, [implement(api).middleware(auth, authImpl).handlers({ … })], { cors, broker })
969
+ * const res = await app.fetch(new Request('http://x/getUser/u1', { method: 'POST' }))
970
+ * ```
971
+ */
972
+ function server(spec, builders, ...rest) {
973
+ const options = rest[0];
974
+ const table = /* @__PURE__ */ new Map();
975
+ const implMap = /* @__PURE__ */ new Map();
976
+ const eps = [];
977
+ const routes = [];
978
+ const events = [];
979
+ const byName = /* @__PURE__ */ new Map();
980
+ const byWs = /* @__PURE__ */ new Map();
981
+ const byRoute = /* @__PURE__ */ new Map();
982
+ const boundChains = /* @__PURE__ */ new Map();
983
+ const eventsByWs = /* @__PURE__ */ new Map();
984
+ const subscribers = /* @__PURE__ */ new Map();
985
+ const specDocs = [];
986
+ let manifestCache = null;
987
+ let openapiJsonCache = null;
988
+ let asyncapiJsonCache = null;
989
+ const invalidate = () => {
990
+ manifestCache = null;
991
+ openapiJsonCache = null;
992
+ asyncapiJsonCache = null;
993
+ };
994
+ const bind = (chain, where) => chain.map((m) => {
995
+ const impl = implMap.get(m);
996
+ if (!impl) throw new Error(`middleware "${m.name}" (${where}) has no implementation — bind it with implement(api).middleware(def, impl)`);
997
+ return {
998
+ ...m,
999
+ run: impl
1000
+ };
1001
+ });
1002
+ function register(regSpec, regBuilders) {
1003
+ const newHandlers = [];
1004
+ const newImpls = [];
1005
+ for (const b of regBuilders) {
1006
+ for (const [k, fn] of b.__bag.handlers) {
1007
+ if (table.has(k) || newHandlers.some(([n]) => n === k)) throw new Error(`duplicate handler for endpoint "${k}"`);
1008
+ if (!(k in regSpec.endpoints)) throw new Error(`handler for unknown endpoint "${k}"`);
1009
+ newHandlers.push([k, fn]);
1010
+ }
1011
+ for (const [def, impl] of b.__bag.middleware) {
1012
+ if (implMap.has(def) || newImpls.some(([d]) => d === def)) throw new Error(`duplicate implementation for middleware "${def.name}"`);
1013
+ newImpls.push([def, impl]);
1014
+ }
1015
+ }
1016
+ for (const k of Object.keys(regSpec.endpoints)) if (!newHandlers.some(([n]) => n === k)) throw new Error(`missing handler for endpoint "${k}"`);
1017
+ const newEps = Object.entries(regSpec.endpoints).map(([name, def]) => normalizeEndpoint(name, def));
1018
+ for (const e of newEps) {
1019
+ if (e.wsEligible && byRoute.has(`${e.method} ${e.path}`)) throw new Error(`route "${e.method} ${e.path}" is already installed`);
1020
+ if (e.ws !== null && e.wsEligible && (byWs.has(e.ws) || eventsByWs.has(e.ws))) throw new Error(`ws id "${e.ws}" is already installed`);
1021
+ }
1022
+ const newEvents = Object.entries(regSpec.events ?? {}).map(([name, cfg]) => ({
1023
+ name,
1024
+ cfg,
1025
+ ws: cfg.ws ?? name
1026
+ }));
1027
+ for (const ev of newEvents) {
1028
+ if (events.some((x) => x.name === ev.name)) throw new Error(`event "${ev.name}" is already installed`);
1029
+ if (eventsByWs.has(ev.ws) || byWs.has(ev.ws) || newEps.some((e) => e.ws === ev.ws)) throw new Error(`event channel "${ev.ws}" collides with an existing channel or ws id`);
1030
+ }
1031
+ for (const [def, impl] of newImpls) implMap.set(def, impl);
1032
+ for (const [k, fn] of newHandlers) table.set(k, fn);
1033
+ for (const e of newEps) {
1034
+ boundChains.set(e, bind(e.chain, `endpoint "${e.name}"`));
1035
+ byName.set(e.name, e);
1036
+ eps.push(e);
1037
+ routes.push({
1038
+ e,
1039
+ parts: e.parts
1040
+ });
1041
+ if (e.wsEligible) byRoute.set(`${e.method} ${e.path}`, e);
1042
+ if (e.ws !== null && e.wsEligible) byWs.set(e.ws, e);
1043
+ }
1044
+ const liveEvents = newEvents.map((ev) => ({
1045
+ ...ev,
1046
+ chain: bind(resolveChain(ev.cfg.guard ?? []), `event "${ev.name}"`)
1047
+ }));
1048
+ for (const ev of liveEvents) {
1049
+ events.push(ev);
1050
+ eventsByWs.set(ev.ws, ev);
1051
+ }
1052
+ if (regSpec.doc) specDocs.push(regSpec.doc);
1053
+ invalidate();
1054
+ return {
1055
+ eps: newEps,
1056
+ events: liveEvents,
1057
+ impls: newImpls.map(([d]) => d),
1058
+ doc: regSpec.doc
1059
+ };
1060
+ }
1061
+ function unregister(handle) {
1062
+ for (const e of handle.eps) {
1063
+ boundChains.delete(e);
1064
+ byName.delete(e.name);
1065
+ table.delete(e.name);
1066
+ if (e.wsEligible) byRoute.delete(`${e.method} ${e.path}`);
1067
+ if (e.ws !== null && e.wsEligible) byWs.delete(e.ws);
1068
+ const ri = routes.findIndex((r) => r.e === e);
1069
+ if (ri >= 0) routes.splice(ri, 1);
1070
+ const ei = eps.indexOf(e);
1071
+ if (ei >= 0) eps.splice(ei, 1);
1072
+ }
1073
+ for (const ev of handle.events) {
1074
+ const i = events.indexOf(ev);
1075
+ if (i >= 0) events.splice(i, 1);
1076
+ eventsByWs.delete(ev.ws);
1077
+ for (const key of [...subscribers.keys()]) if (key === ev.ws || key.startsWith(`${ev.ws}|`)) subscribers.delete(key);
1078
+ }
1079
+ for (const def of handle.impls) implMap.delete(def);
1080
+ if (handle.doc) {
1081
+ const di = specDocs.indexOf(handle.doc);
1082
+ if (di >= 0) specDocs.splice(di, 1);
1083
+ }
1084
+ invalidate();
1085
+ }
1086
+ register(spec, builders);
1087
+ const buildManifest = () => ({
1088
+ endpoints: Object.fromEntries(eps.map((e) => [e.name, {
1089
+ method: e.method,
1090
+ path: e.path,
1091
+ ws: e.ws,
1092
+ httpOnly: e.httpOnly,
1093
+ streamIn: e.streamInCt,
1094
+ itemsIn: e.itemsIn,
1095
+ streamOut: e.streamOutCt,
1096
+ items: e.items,
1097
+ p: e.p,
1098
+ q: e.q,
1099
+ b: e.bRaw ? "raw" : e.b,
1100
+ f: e.f,
1101
+ hasBody: Boolean(e.def.cfg.body),
1102
+ hasHeaders: Boolean(e.def.cfg.headers),
1103
+ multi: e.multi,
1104
+ bodyEnc: e.bodyEnc
1105
+ }])),
1106
+ events: Object.fromEntries(events.map((ev) => [ev.name, {
1107
+ ws: ev.ws,
1108
+ hasParams: Boolean(ev.cfg.params)
1109
+ }]))
1110
+ });
1111
+ const getManifest = () => manifestCache ??= buildManifest();
1112
+ /** Compose every installed spec's doc patches into one (applied last, in install order). */
1113
+ const composeDoc = () => ({
1114
+ openapi: (doc) => specDocs.reduce((acc, d) => d.openapi ? d.openapi(acc) : acc, doc),
1115
+ asyncapi: (doc) => specDocs.reduce((acc, d) => d.asyncapi ? d.asyncapi(acc) : acc, doc)
1116
+ });
1117
+ const docs = normalizeDocs(options?.docs);
1118
+ const openapiJson = () => openapiJsonCache ??= JSON.stringify(buildOpenapi(eps, composeDoc(), docs?.info));
1119
+ const asyncapiJson = () => asyncapiJsonCache ??= JSON.stringify(buildAsyncapi(eps, events, composeDoc(), docs?.info));
1120
+ const jsonDoc = (body) => new Response(body, { headers: { "content-type": "application/json; charset=utf-8" } });
1121
+ const htmlDoc = (body) => new Response(body, { headers: { "content-type": "text/html; charset=utf-8" } });
1122
+ function docResponse(pathname) {
1123
+ if (!docs) return null;
1124
+ if (docs.openapiJson && pathname === docs.openapiJson) return jsonDoc(openapiJson());
1125
+ if (docs.asyncapiJson && pathname === docs.asyncapiJson) return jsonDoc(asyncapiJson());
1126
+ if (docs.swagger && docs.openapiJson && pathname === docs.swagger) return htmlDoc(swaggerHtml(docs.openapiJson));
1127
+ if (docs.redoc && docs.openapiJson && pathname === docs.redoc) return htmlDoc(redocHtml(docs.openapiJson));
1128
+ if (docs.asyncapi && docs.asyncapiJson && pathname === docs.asyncapi) return htmlDoc(asyncapiHtml(docs.asyncapiJson));
1129
+ return null;
1130
+ }
1131
+ async function runChain(chain, req, rawParams, terminal, info) {
1132
+ const fns = metaFns(info.meta);
1133
+ let ctx = {};
1134
+ const loaderVals = {};
1135
+ const step = async (i) => {
1136
+ if (i >= chain.length) return terminal(ctx, loaderVals);
1137
+ const m = chain[i];
1138
+ let nextCalled = false;
1139
+ const io = {
1140
+ req,
1141
+ body: info.body,
1142
+ get ctx() {
1143
+ return ctx;
1144
+ },
1145
+ next: async (add) => {
1146
+ nextCalled = true;
1147
+ if (add) ctx = {
1148
+ ...ctx,
1149
+ ...add
1150
+ };
1151
+ return step(i + 1);
1152
+ },
1153
+ transport: info.transport,
1154
+ route: info.route,
1155
+ signal: info.signal,
1156
+ setHeader: fns.setHeader,
1157
+ status: fns.status
1158
+ };
1159
+ if (info.ws) io.ws = info.ws;
1160
+ if (m.paramKey && m.paramSchema) {
1161
+ if (!(m.paramKey in rawParams)) throw reject(400, "BAD_REQUEST", `missing path param "${m.paramKey}"`);
1162
+ const v = m.paramSchema.parse(rawParams[m.paramKey]);
1163
+ io.value = v;
1164
+ loaderVals[m.paramKey] = v;
1165
+ }
1166
+ const out = await m.run(io);
1167
+ if (out instanceof Response) return out;
1168
+ if (!nextCalled) throw new Error(`middleware "${m.name}" returned without calling next()`);
1169
+ return out;
1170
+ };
1171
+ return step(0);
1172
+ }
1173
+ const makeMeta = () => ({
1174
+ status: null,
1175
+ headers: [],
1176
+ length: null,
1177
+ committed: false
1178
+ });
1179
+ const metaFns = (meta) => ({
1180
+ status: (code) => {
1181
+ if (!meta) return;
1182
+ if (meta.committed) throw new Error("$status must be called before the response is committed");
1183
+ meta.status = code;
1184
+ },
1185
+ setHeader: (name, value) => {
1186
+ if (!meta) return;
1187
+ if (meta.committed) throw new Error("$setHeader must be called before the response is committed");
1188
+ meta.headers.push([name, value]);
1189
+ },
1190
+ setCookie: (name, value, opts) => {
1191
+ if (!meta) return;
1192
+ if (meta.committed) throw new Error("$setCookie must be called before the response is committed");
1193
+ meta.headers.push(["set-cookie", serializeCookie(name, value, opts)]);
1194
+ }
1195
+ });
1196
+ /** disjoint kinds: split a flat data payload back into p/q/b by the endpoint's key tables */
1197
+ function kindsFromData(e, data) {
1198
+ if (e.bRaw) return {
1199
+ p: {},
1200
+ q: {},
1201
+ b: data
1202
+ };
1203
+ const p = {};
1204
+ const q = {};
1205
+ const b = {};
1206
+ const pSet = new Set(e.p);
1207
+ const qSet = new Set(e.q);
1208
+ const bSet = new Set(e.b ?? []);
1209
+ const fSet = new Set(e.f);
1210
+ for (const [k, v] of Object.entries(data ?? {})) if (pSet.has(k)) p[k] = v;
1211
+ else if (qSet.has(k)) q[k] = v;
1212
+ else if (bSet.has(k)) b[k] = v;
1213
+ else if (fSet.has(k)) continue;
1214
+ else throw reject(400, "VALIDATION", `key "${k}" does not belong to this endpoint`);
1215
+ return {
1216
+ p,
1217
+ q,
1218
+ b: e.b !== null ? b : void 0
1219
+ };
1220
+ }
1221
+ /** root payload names owned by the framework — middleware ctx may not collide with them */
1222
+ const RESERVED_CTX = new Set([
1223
+ "data",
1224
+ "stream",
1225
+ "headers",
1226
+ "cookies",
1227
+ "out",
1228
+ "download",
1229
+ "length",
1230
+ "fail",
1231
+ "status",
1232
+ "header",
1233
+ "cookie",
1234
+ "req",
1235
+ "signal",
1236
+ "emit"
1237
+ ]);
1238
+ function assemble(e, ctx, kinds, req, signal, streamCtl, meta) {
1239
+ const data = e.bRaw ? kinds.b : {
1240
+ ...kinds.p,
1241
+ ...kinds.q,
1242
+ ...e.b !== null ? kinds.b : {},
1243
+ ...kinds.f
1244
+ };
1245
+ const payload = {};
1246
+ for (const [k, v] of Object.entries(ctx)) {
1247
+ if (RESERVED_CTX.has(k)) throw new Error(`middleware ctx key "${k}" collides with a reserved payload name`);
1248
+ payload[k] = v;
1249
+ }
1250
+ if (e.bRaw || Object.keys(data).length > 0) payload.data = data;
1251
+ if (kinds.stream) payload.stream = kinds.stream;
1252
+ if (streamCtl) {
1253
+ payload.out = streamCtl.out;
1254
+ payload.download = streamCtl.download;
1255
+ payload.length = streamCtl.length;
1256
+ }
1257
+ if (e.def.cfg.headers) payload.headers = kinds.h;
1258
+ if (e.def.cfg.cookies) payload.cookies = kinds.ck;
1259
+ const errors = e.def.cfg.errors;
1260
+ if (errors && Object.keys(errors).length > 0) payload.fail = (status, data2) => {
1261
+ const schema = errors[status];
1262
+ if (!schema) throw new Error(`endpoint "${e.name}": fail(${status}) is not a declared error status`);
1263
+ throw new ApiFailure(status, schema.parse(data2));
1264
+ };
1265
+ const fns = metaFns(meta);
1266
+ payload.status = fns.status;
1267
+ payload.header = fns.setHeader;
1268
+ payload.cookie = fns.setCookie;
1269
+ payload.req = req;
1270
+ payload.signal = signal;
1271
+ payload.emit = emit;
1272
+ return payload;
1273
+ }
1274
+ async function parseFiles(e, form) {
1275
+ const files = {};
1276
+ for (const [key, schema] of Object.entries(e.def.cfg.files ?? {})) {
1277
+ const all = form.getAll(key).filter((v) => typeof v !== "string");
1278
+ const raw = schema instanceof z.ZodArray || schema instanceof z.ZodOptional && schema.unwrap() instanceof z.ZodArray ? all : all[0];
1279
+ files[key] = schema.parse(raw);
1280
+ }
1281
+ const rawBody = form.get("body");
1282
+ return {
1283
+ files,
1284
+ body: typeof rawBody === "string" ? JSON.parse(rawBody) : void 0
1285
+ };
1286
+ }
1287
+ function queryToObject(e, sp) {
1288
+ const out = {};
1289
+ for (const k of e.q) {
1290
+ const vals = sp.getAll(k);
1291
+ if (vals.length === 0) continue;
1292
+ out[k] = vals.length === 1 ? vals[0] : vals;
1293
+ }
1294
+ return out;
1295
+ }
1296
+ async function invoke(e, req, rawParams, rawQuery, rawBody, rawFiles, stream, streamCtl, meta, signal, chainCtx) {
1297
+ const route = {
1298
+ kind: "endpoint",
1299
+ name: e.name,
1300
+ method: e.method,
1301
+ path: e.path,
1302
+ ws: e.ws
1303
+ };
1304
+ return runChain(boundChains.get(e), req, rawParams, async (ctx, loaderVals) => {
1305
+ const c = e.def.cfg;
1306
+ const cfgParamKeys = (objectKeys(c.params) ?? []).filter((k) => !e.loaders.has(k) && !e.tplSchemas.has(k));
1307
+ const picked = {};
1308
+ for (const k of cfgParamKeys) picked[k] = rawParams[k];
1309
+ const tplVals = {};
1310
+ for (const [k, schema] of e.tplSchemas) tplVals[k] = schema.parse(rawParams[k]);
1311
+ const p = {
1312
+ ...loaderVals,
1313
+ ...tplVals,
1314
+ ...c.params ? c.params.parse(picked) : {}
1315
+ };
1316
+ const q = c.query ? c.query.parse(rawQuery) : {};
1317
+ const b = c.body ? c.body.parse(rawBody) : void 0;
1318
+ const f = {};
1319
+ if (rawFiles) for (const [k, schema] of Object.entries(c.files ?? {})) f[k] = rawFiles[k] !== void 0 ? rawFiles[k] : schema.parse(void 0);
1320
+ let h;
1321
+ if (c.headers) {
1322
+ const raw = {};
1323
+ for (const k of objectKeys(c.headers) ?? []) {
1324
+ const v = req.headers.get(k);
1325
+ if (v !== null) raw[k] = v;
1326
+ }
1327
+ h = c.headers.parse(raw);
1328
+ }
1329
+ let ck;
1330
+ if (c.cookies) ck = c.cookies.parse(parseCookieHeader(req.headers.get("cookie")));
1331
+ const result = await table.get(e.name)(assemble(e, ctx, {
1332
+ p,
1333
+ q,
1334
+ b,
1335
+ f: rawFiles ? f : {},
1336
+ stream,
1337
+ h,
1338
+ ck
1339
+ }, req, signal ?? req.signal, streamCtl, meta));
1340
+ if (c.streamOut) return result;
1341
+ if (c.responses) {
1342
+ const r = result;
1343
+ const schema = c.responses[r.status];
1344
+ if (!schema) throw new Error(`endpoint "${e.name}": status ${r.status} is not a declared response status`);
1345
+ return {
1346
+ status: r.status,
1347
+ data: schema.parse(r.data)
1348
+ };
1349
+ }
1350
+ if (c.response) return c.response.parse(result);
1351
+ }, {
1352
+ route,
1353
+ transport: chainCtx.transport,
1354
+ signal: signal ?? req.signal,
1355
+ meta,
1356
+ ws: chainCtx.ws,
1357
+ body: rawBody
1358
+ });
1359
+ }
1360
+ /** In-process call: route by name, split the flat data, run the full chain via {@link invoke} (transport `'local'`). */
1361
+ function callLocal(name, ...args) {
1362
+ const e = byName.get(name);
1363
+ if (!e) throw new Error(`unknown endpoint "${name}"`);
1364
+ const hasInput = e.bRaw || e.b !== null || e.p.length > 0 || e.q.length > 0;
1365
+ const data = hasInput ? args[0] : void 0;
1366
+ const opts = hasInput ? args[1] : args[0];
1367
+ const { p, q, b } = kindsFromData(e, data);
1368
+ return invoke(e, new Request(`http://local/${name}`, {
1369
+ method: e.method,
1370
+ headers: new Headers(opts?.headers ?? {})
1371
+ }), p, q, b, null, void 0, void 0, makeMeta(), opts?.signal, { transport: "local" });
1372
+ }
1373
+ function toStream(v) {
1374
+ if (v instanceof ReadableStream) return v;
1375
+ const iter = v;
1376
+ const enc = new TextEncoder();
1377
+ return new ReadableStream({ async start(controller) {
1378
+ for await (const chunk of iter) controller.enqueue(typeof chunk === "string" ? enc.encode(chunk) : chunk);
1379
+ controller.close();
1380
+ } });
1381
+ }
1382
+ function asyncQueue() {
1383
+ const buf = [];
1384
+ let done = false;
1385
+ let err;
1386
+ let wake = null;
1387
+ return {
1388
+ push(v) {
1389
+ buf.push(v);
1390
+ wake?.();
1391
+ },
1392
+ end() {
1393
+ done = true;
1394
+ wake?.();
1395
+ },
1396
+ fail(e) {
1397
+ err = e;
1398
+ done = true;
1399
+ wake?.();
1400
+ },
1401
+ async *[Symbol.asyncIterator]() {
1402
+ for (;;) {
1403
+ if (buf.length > 0) {
1404
+ yield buf.shift();
1405
+ continue;
1406
+ }
1407
+ if (err !== void 0) throw err;
1408
+ if (done) return;
1409
+ await new Promise((r) => wake = r);
1410
+ wake = null;
1411
+ }
1412
+ }
1413
+ };
1414
+ }
1415
+ /** NDJSON request body → typed async iterable; each item validated as the handler pulls it. */
1416
+ async function* decodeItems(body, schema) {
1417
+ const reader = body.getReader();
1418
+ const dec = new TextDecoder();
1419
+ let buf = "";
1420
+ try {
1421
+ for (;;) {
1422
+ const { done, value } = await reader.read();
1423
+ if (done) break;
1424
+ buf += dec.decode(value, { stream: true });
1425
+ let nl;
1426
+ while ((nl = buf.indexOf("\n")) >= 0) {
1427
+ const line = buf.slice(0, nl);
1428
+ buf = buf.slice(nl + 1);
1429
+ if (line.trim()) yield schema.parse(JSON.parse(line));
1430
+ }
1431
+ }
1432
+ buf += dec.decode();
1433
+ if (buf.trim()) yield schema.parse(JSON.parse(buf));
1434
+ } finally {
1435
+ await reader.cancel().catch(() => {});
1436
+ }
1437
+ }
1438
+ /** Identity transform behind `out`: encodes strings, signals the first write, can error mid-stream. */
1439
+ function createOut() {
1440
+ const enc = new TextEncoder();
1441
+ let wrote = false;
1442
+ let resolveFirst;
1443
+ const firstWrite = new Promise((r) => resolveFirst = r);
1444
+ let ctl;
1445
+ const ts = new TransformStream({
1446
+ start(c) {
1447
+ ctl = c;
1448
+ },
1449
+ transform(chunk, c) {
1450
+ if (!wrote) {
1451
+ wrote = true;
1452
+ resolveFirst();
1453
+ }
1454
+ c.enqueue(typeof chunk === "string" ? enc.encode(chunk) : chunk);
1455
+ }
1456
+ }, void 0, { highWaterMark: OUT_HIGH_WATER_MARK });
1457
+ return {
1458
+ readable: ts.readable,
1459
+ writable: ts.writable,
1460
+ firstWrite,
1461
+ wrote: () => wrote,
1462
+ fail: (err) => {
1463
+ try {
1464
+ ctl.error(err);
1465
+ } catch {}
1466
+ }
1467
+ };
1468
+ }
1469
+ /** Typed item stream → NDJSON or SSE bytes; each item validated against the schema as it flows. */
1470
+ function itemStream(v, schema, sse) {
1471
+ const it = v[Symbol.asyncIterator]();
1472
+ const enc = new TextEncoder();
1473
+ return new ReadableStream({
1474
+ async pull(controller) {
1475
+ const { done, value } = await it.next();
1476
+ if (done) return controller.close();
1477
+ const json = JSON.stringify(schema.parse(value));
1478
+ controller.enqueue(enc.encode(sse ? `data: ${json}\n\n` : json + "\n"));
1479
+ },
1480
+ async cancel() {
1481
+ await it.return?.(void 0);
1482
+ }
1483
+ });
1484
+ }
1485
+ /** Byte-range slicer for resumable downloads: skip `start`, stop after `end` (inclusive). */
1486
+ function sliceStream(src, start, end) {
1487
+ let pos = 0;
1488
+ const reader = src.getReader();
1489
+ return new ReadableStream({
1490
+ async pull(controller) {
1491
+ for (;;) {
1492
+ const { done, value } = await reader.read();
1493
+ if (done) return controller.close();
1494
+ const from = pos;
1495
+ pos += value.byteLength;
1496
+ if (pos <= start) continue;
1497
+ const sliceFrom = Math.max(0, start - from);
1498
+ const sliceTo = Math.min(value.byteLength, end + 1 - from);
1499
+ controller.enqueue(value.subarray(sliceFrom, sliceTo));
1500
+ if (pos > end) {
1501
+ await reader.cancel().catch(() => {});
1502
+ return controller.close();
1503
+ }
1504
+ return;
1505
+ }
1506
+ },
1507
+ async cancel() {
1508
+ await reader.cancel().catch(() => {});
1509
+ }
1510
+ });
1511
+ }
1512
+ function parseRange(header) {
1513
+ const m = header ? /^bytes=(\d+)-(\d*)$/.exec(header) : null;
1514
+ if (!m) return null;
1515
+ return {
1516
+ start: Number(m[1]),
1517
+ end: m[2] ? Number(m[2]) : null
1518
+ };
1519
+ }
1520
+ function errorResponse(err) {
1521
+ if (err instanceof ApiFailure) return Response.json(err.data, { status: err.status });
1522
+ if (err instanceof ApiError) return Response.json({ error: {
1523
+ code: err.code,
1524
+ message: err.message
1525
+ } }, { status: err.status });
1526
+ if (err instanceof z.ZodError) return Response.json({ error: {
1527
+ code: "VALIDATION",
1528
+ issues: err.issues
1529
+ } }, { status: 400 });
1530
+ return Response.json({ error: {
1531
+ code: "INTERNAL",
1532
+ message: err instanceof Error ? err.message : "internal error"
1533
+ } }, { status: 500 });
1534
+ }
1535
+ async function fetchHandler(req) {
1536
+ const url = new URL(req.url);
1537
+ if (req.method === "GET") {
1538
+ const doc = docResponse(url.pathname);
1539
+ if (doc) return doc;
1540
+ }
1541
+ const effMethod = req.method === "HEAD" ? "GET" : req.method;
1542
+ for (const { e, parts } of routes) {
1543
+ if (effMethod !== e.method) continue;
1544
+ const matched = matchParts(parts, url.pathname);
1545
+ if (!matched) continue;
1546
+ try {
1547
+ const rawParams = { ...matched };
1548
+ const c = e.def.cfg;
1549
+ let rawBody;
1550
+ let rawFiles = null;
1551
+ let stream;
1552
+ if (c.streamIn) {
1553
+ const body = req.body ?? new ReadableStream({ start: (ctl) => ctl.close() });
1554
+ stream = e.itemsIn ? decodeItems(body, c.streamIn) : body;
1555
+ } else if (e.f.length > 0) {
1556
+ const parsed = await parseFiles(e, await req.formData());
1557
+ rawFiles = parsed.files;
1558
+ rawBody = parsed.body;
1559
+ } else if (c.body) if (e.bodyEnc === "urlencoded") {
1560
+ const form = new URLSearchParams(await req.text());
1561
+ const obj = {};
1562
+ for (const k of new Set(form.keys())) {
1563
+ const all = form.getAll(k);
1564
+ obj[k] = all.length === 1 ? all[0] : all;
1565
+ }
1566
+ rawBody = obj;
1567
+ } else rawBody = await req.json();
1568
+ const rawQuery = queryToObject(e, url.searchParams);
1569
+ const meta = makeMeta();
1570
+ if (c.streamOut && !e.items) {
1571
+ const t = createOut();
1572
+ const dl = {
1573
+ filename: c.download ?? null,
1574
+ ct: e.streamOutCt,
1575
+ committed: false
1576
+ };
1577
+ const download = (filename, contentType) => {
1578
+ if (dl.committed) throw new Error("$download must be called before streaming starts");
1579
+ dl.filename = filename;
1580
+ if (contentType) dl.ct = contentType;
1581
+ };
1582
+ const length = (totalBytes) => {
1583
+ if (meta.committed) throw new Error("$length must be called before streaming starts");
1584
+ meta.length = totalBytes;
1585
+ };
1586
+ const handlerDone = invoke(e, req, rawParams, rawQuery, rawBody, rawFiles, stream, {
1587
+ out: t.writable,
1588
+ download,
1589
+ length
1590
+ }, meta, void 0, { transport: "http" });
1591
+ const winner = await Promise.race([t.firstWrite.then(() => ({ kind: "wrote" })), handlerDone.then((result) => ({
1592
+ kind: "done",
1593
+ result
1594
+ }), (err) => ({
1595
+ kind: "fail",
1596
+ err
1597
+ }))]);
1598
+ if (winner.kind === "fail" && !t.wrote()) return errorResponse(winner.err);
1599
+ if (winner.kind === "done" && winner.result instanceof Response) {
1600
+ if (!t.writable.locked) t.writable.close().catch(() => {});
1601
+ return winner.result;
1602
+ }
1603
+ /* v8 ignore next 3 */ if (winner.kind === "done" && winner.result != null && t.wrote()) return errorResponse(/* @__PURE__ */ new Error(`endpoint "${e.name}": handler both wrote to $out and returned a stream`));
1604
+ dl.committed = true;
1605
+ meta.committed = true;
1606
+ const headers = new Headers(meta.headers);
1607
+ headers.set("content-type", dl.ct);
1608
+ headers.set("accept-ranges", "bytes");
1609
+ if (dl.filename) headers.set("content-disposition", `attachment; filename="${dl.filename}"`);
1610
+ let body;
1611
+ if (winner.kind === "done" && winner.result != null) body = toStream(winner.result);
1612
+ else {
1613
+ if (winner.kind === "done" && !t.wrote() && !t.writable.locked) t.writable.close();
1614
+ else handlerDone.then((result) => {
1615
+ if (result != null) t.fail(/* @__PURE__ */ new Error(`endpoint "${e.name}": handler both wrote to $out and returned a stream`));
1616
+ }, (err) => t.fail(err));
1617
+ body = t.readable;
1618
+ }
1619
+ const range = e.method === "GET" ? parseRange(req.headers.get("range")) : null;
1620
+ if (meta.length !== null) {
1621
+ if (range) {
1622
+ const start = range.start;
1623
+ const end = Math.min(range.end ?? meta.length - 1, meta.length - 1);
1624
+ if (start >= meta.length || start > end) {
1625
+ body.cancel().catch(() => {});
1626
+ headers.set("content-range", `bytes */${meta.length}`);
1627
+ return new Response(null, {
1628
+ status: 416,
1629
+ headers
1630
+ });
1631
+ }
1632
+ headers.set("content-range", `bytes ${start}-${end}/${meta.length}`);
1633
+ headers.set("content-length", String(end - start + 1));
1634
+ return new Response(sliceStream(body, start, end), {
1635
+ status: 206,
1636
+ headers
1637
+ });
1638
+ }
1639
+ headers.set("content-length", String(meta.length));
1640
+ }
1641
+ return new Response(body, {
1642
+ status: meta.status ?? 200,
1643
+ headers
1644
+ });
1645
+ }
1646
+ const result = await invoke(e, req, rawParams, rawQuery, rawBody, rawFiles, stream, void 0, meta, void 0, { transport: "http" });
1647
+ if (result instanceof Response) return result;
1648
+ meta.committed = true;
1649
+ const headers = new Headers(meta.headers);
1650
+ if (c.streamOut) {
1651
+ headers.set("content-type", e.streamOutCt);
1652
+ return new Response(itemStream(result, c.streamOut, e.sse), {
1653
+ status: meta.status ?? 200,
1654
+ headers
1655
+ });
1656
+ }
1657
+ if (c.responses) {
1658
+ const r = result;
1659
+ return Response.json(r.data, {
1660
+ status: r.status,
1661
+ headers
1662
+ });
1663
+ }
1664
+ if (c.response) return Response.json(result, {
1665
+ status: meta.status ?? 200,
1666
+ headers
1667
+ });
1668
+ return new Response(null, {
1669
+ status: meta.status ?? 204,
1670
+ headers
1671
+ });
1672
+ } catch (err) {
1673
+ return errorResponse(err);
1674
+ }
1675
+ }
1676
+ return Response.json({ error: { code: "NOT_FOUND" } }, { status: 404 });
1677
+ }
1678
+ function corsHeadersFor(req) {
1679
+ const cors = options?.cors;
1680
+ if (!cors) return null;
1681
+ const origin = req.headers.get("origin");
1682
+ if (!origin) return null;
1683
+ const allowed = cors.origin === "*" ? "*" : typeof cors.origin === "string" ? cors.origin === origin ? origin : null : cors.origin.includes(origin) ? origin : null;
1684
+ if (!allowed) return null;
1685
+ const h = { "access-control-allow-origin": allowed };
1686
+ if (cors.credentials) h["access-control-allow-credentials"] = "true";
1687
+ if (allowed !== "*") h.vary = "origin";
1688
+ if (cors.exposeHeaders?.length) h["access-control-expose-headers"] = cors.exposeHeaders.join(", ");
1689
+ return h;
1690
+ }
1691
+ async function fetchEntry(req) {
1692
+ const ch = corsHeadersFor(req);
1693
+ if (options?.cors && req.method === "OPTIONS" && req.headers.get("access-control-request-method")) {
1694
+ const headers = new Headers(ch ?? {});
1695
+ headers.set("access-control-allow-methods", [...new Set(eps.map((e) => e.method))].join(", "));
1696
+ headers.set("access-control-allow-headers", req.headers.get("access-control-request-headers") ?? "*");
1697
+ if (options.cors.maxAge !== void 0) headers.set("access-control-max-age", String(options.cors.maxAge));
1698
+ return new Response(null, {
1699
+ status: 204,
1700
+ headers
1701
+ });
1702
+ }
1703
+ let res = await fetchHandler(req);
1704
+ if (ch || req.method === "HEAD") {
1705
+ const headers = new Headers(res.headers);
1706
+ for (const [k, v] of Object.entries(ch ?? {})) headers.set(k, v);
1707
+ if (req.method === "HEAD") {
1708
+ res.body?.cancel().catch(() => {});
1709
+ return new Response(null, {
1710
+ status: res.status,
1711
+ headers
1712
+ });
1713
+ }
1714
+ res = new Response(res.body, {
1715
+ status: res.status,
1716
+ headers
1717
+ });
1718
+ }
1719
+ return res;
1720
+ }
1721
+ let connSeq = 0;
1722
+ const canon = (v) => JSON.stringify(v, Object.keys(v ?? {}).sort());
1723
+ const subKey = (ws, params) => `${ws}|${canon(params ?? {})}`;
1724
+ const broker = options?.broker ?? localBroker();
1725
+ function deliverLocal(msg) {
1726
+ const frame = JSON.stringify({
1727
+ type: msg.ch,
1728
+ params: msg.params,
1729
+ data: msg.data
1730
+ });
1731
+ const conns = subscribers.get(subKey(msg.ch, msg.params));
1732
+ if (conns) for (const c of conns) try {
1733
+ c.send(frame);
1734
+ } catch {}
1735
+ }
1736
+ broker.subscribe((raw) => {
1737
+ try {
1738
+ deliverLocal(JSON.parse(raw));
1739
+ } catch {}
1740
+ });
1741
+ function emitImpl(name, ...args) {
1742
+ const ev = events.find((x) => x.name === name);
1743
+ if (!ev) throw new Error(`unknown event "${name}"`);
1744
+ const [params, data] = ev.cfg.params ? [args[0], args[1]] : [void 0, args[0]];
1745
+ const pOut = ev.cfg.params ? ev.cfg.params.parse(params) : {};
1746
+ const dOut = ev.cfg.data.parse(data);
1747
+ try {
1748
+ const published = broker.publish(JSON.stringify({
1749
+ ch: ev.ws,
1750
+ params: pOut,
1751
+ data: dOut
1752
+ }));
1753
+ if (published) Promise.resolve(published).catch(() => {});
1754
+ } catch {}
1755
+ }
1756
+ const emit = emitImpl;
1757
+ /** Map a middleware short-circuit Response to a ws frame: JSON body → result frame, else error frame. */
1758
+ async function sendShortCircuit(conn, id, res) {
1759
+ const isJson = (res.headers.get("content-type") ?? "").includes("application/json");
1760
+ if (res.ok && isJson) {
1761
+ const data = await res.json().catch(() => null);
1762
+ conn.send(JSON.stringify({
1763
+ id,
1764
+ $status: res.status,
1765
+ data
1766
+ }));
1767
+ return;
1768
+ }
1769
+ let data = null;
1770
+ try {
1771
+ data = isJson ? await res.json() : await res.text();
1772
+ } catch {}
1773
+ conn.send(JSON.stringify({
1774
+ id,
1775
+ $status: res.status,
1776
+ $code: "SHORT_CIRCUIT",
1777
+ $error: typeof data === "string" ? data : void 0,
1778
+ data
1779
+ }));
1780
+ }
1781
+ return {
1782
+ spec,
1783
+ fetch: fetchEntry,
1784
+ manifest: getManifest,
1785
+ emit,
1786
+ openapi: (info) => buildOpenapi(eps, composeDoc(), info),
1787
+ asyncapi: (info) => buildAsyncapi(eps, events, composeDoc(), info),
1788
+ call: callLocal,
1789
+ install: ((regSpec, regBuilders) => register(regSpec, regBuilders)),
1790
+ uninstall: unregister,
1791
+ ws: {
1792
+ open(send, req) {
1793
+ return {
1794
+ id: ++connSeq,
1795
+ req,
1796
+ send,
1797
+ subs: /* @__PURE__ */ new Set(),
1798
+ streams: /* @__PURE__ */ new Map(),
1799
+ calls: /* @__PURE__ */ new Map()
1800
+ };
1801
+ },
1802
+ async message(conn, raw) {
1803
+ let frame;
1804
+ try {
1805
+ frame = JSON.parse(raw);
1806
+ } catch {
1807
+ conn.send(JSON.stringify({
1808
+ $status: 400,
1809
+ $code: "BAD_FRAME",
1810
+ $error: "bad frame"
1811
+ }));
1812
+ return;
1813
+ }
1814
+ const id = frame.id;
1815
+ const fail = (err) => {
1816
+ const out = err instanceof ApiFailure ? {
1817
+ id,
1818
+ $status: err.status,
1819
+ data: err.data
1820
+ } : err instanceof ApiError ? {
1821
+ id,
1822
+ $status: err.status,
1823
+ $code: err.code,
1824
+ $error: err.message
1825
+ } : err instanceof z.ZodError ? {
1826
+ id,
1827
+ $status: 400,
1828
+ $code: "VALIDATION",
1829
+ $error: "VALIDATION",
1830
+ data: { issues: err.issues }
1831
+ } : {
1832
+ id,
1833
+ $status: 500,
1834
+ $code: "INTERNAL",
1835
+ $error: err instanceof Error ? err.message : "internal error"
1836
+ };
1837
+ conn.send(JSON.stringify(out));
1838
+ };
1839
+ try {
1840
+ if (frame.ping === true) {
1841
+ conn.send(JSON.stringify({ pong: true }));
1842
+ return;
1843
+ }
1844
+ if (typeof frame.sub === "string") {
1845
+ const ev = eventsByWs.get(frame.sub);
1846
+ if (!ev) throw reject(404, "NOT_FOUND", `unknown channel "${frame.sub}"`);
1847
+ const pOut = ev.cfg.params ? ev.cfg.params.parse(frame.params) : {};
1848
+ const guard = await runChain(ev.chain, conn.req, {}, async () => void 0, {
1849
+ route: {
1850
+ kind: "event",
1851
+ name: ev.name,
1852
+ ws: ev.ws
1853
+ },
1854
+ transport: "ws",
1855
+ signal: conn.req.signal,
1856
+ meta: void 0,
1857
+ ws: {
1858
+ id: String(id),
1859
+ data: frame.params,
1860
+ conn
1861
+ }
1862
+ });
1863
+ if (guard instanceof Response) {
1864
+ await sendShortCircuit(conn, id, guard);
1865
+ return;
1866
+ }
1867
+ const key = subKey(ev.ws, pOut);
1868
+ let set = subscribers.get(key);
1869
+ if (!set) subscribers.set(key, set = /* @__PURE__ */ new Set());
1870
+ set.add(conn);
1871
+ conn.subs.add(key);
1872
+ conn.send(JSON.stringify({
1873
+ id,
1874
+ $status: 200
1875
+ }));
1876
+ } else if (typeof frame.unsub === "string") {
1877
+ const ev = eventsByWs.get(frame.unsub);
1878
+ if (ev) {
1879
+ const pOut = ev.cfg.params ? ev.cfg.params.parse(frame.params) : {};
1880
+ const key = subKey(ev.ws, pOut);
1881
+ subscribers.get(key)?.delete(conn);
1882
+ conn.subs.delete(key);
1883
+ }
1884
+ conn.send(JSON.stringify({
1885
+ id,
1886
+ $status: 200
1887
+ }));
1888
+ } else if ("chunk" in frame) conn.streams.get(String(id))?.push(frame.chunk);
1889
+ else if (frame.end === true) conn.streams.get(String(id))?.end();
1890
+ else if (frame.abort === true) {
1891
+ conn.calls.get(String(id))?.abort();
1892
+ conn.streams.get(String(id))?.fail(new ApiError(ABORTED_STATUS, "ABORTED"));
1893
+ } else if (typeof frame.type === "string") {
1894
+ const e = typeof frame.method === "string" ? byRoute.get(`${frame.method} ${frame.type}`) : byWs.get(frame.type);
1895
+ if (!e) throw reject(404, "NOT_FOUND", `unknown ws endpoint "${frame.type}"`);
1896
+ const { p, q, b } = kindsFromData(e, frame.data);
1897
+ const ac = new AbortController();
1898
+ conn.calls.set(String(id), ac);
1899
+ const wsMeta = makeMeta();
1900
+ let stream;
1901
+ if (e.itemsIn) {
1902
+ const queue = asyncQueue();
1903
+ conn.streams.set(String(id), queue);
1904
+ const schema = e.def.cfg.streamIn;
1905
+ stream = (async function* () {
1906
+ for await (const raw of queue) yield schema.parse(raw);
1907
+ })();
1908
+ }
1909
+ try {
1910
+ const result = await invoke(e, conn.req, p, q, b, null, stream, void 0, wsMeta, ac.signal, {
1911
+ transport: "ws",
1912
+ ws: {
1913
+ id: String(id),
1914
+ data: frame.data,
1915
+ conn
1916
+ }
1917
+ });
1918
+ if (result instanceof Response) await sendShortCircuit(conn, id, result);
1919
+ else if (e.items) {
1920
+ const schema = e.def.cfg.streamOut;
1921
+ for await (const item of result) {
1922
+ if (ac.signal.aborted) break;
1923
+ conn.send(JSON.stringify({
1924
+ id,
1925
+ chunk: schema.parse(item)
1926
+ }));
1927
+ }
1928
+ if (!ac.signal.aborted) conn.send(JSON.stringify({
1929
+ id,
1930
+ end: true
1931
+ }));
1932
+ } else if (e.multi) {
1933
+ const r = result;
1934
+ if (!ac.signal.aborted) conn.send(JSON.stringify({
1935
+ id,
1936
+ $status: r.status,
1937
+ data: {
1938
+ status: r.status,
1939
+ data: r.data
1940
+ }
1941
+ }));
1942
+ } else if (!ac.signal.aborted) conn.send(JSON.stringify({
1943
+ id,
1944
+ $status: wsMeta.status ?? 200,
1945
+ data: result
1946
+ }));
1947
+ } finally {
1948
+ conn.streams.delete(String(id));
1949
+ conn.calls.delete(String(id));
1950
+ }
1951
+ } else throw reject(400, "BAD_FRAME", "unrecognized frame");
1952
+ } catch (err) {
1953
+ fail(err);
1954
+ }
1955
+ },
1956
+ close(conn) {
1957
+ for (const key of conn.subs) subscribers.get(key)?.delete(conn);
1958
+ conn.subs.clear();
1959
+ for (const ac of conn.calls.values()) ac.abort();
1960
+ conn.calls.clear();
1961
+ for (const s of conn.streams.values()) s.fail(new ApiError(ABORTED_STATUS, "ABORTED"));
1962
+ conn.streams.clear();
1963
+ }
1964
+ }
1965
+ };
1966
+ }
1967
+ //#endregion
1968
+ export { ApiError, CallerRateLimited, DEFAULT_BUCKETS, DEFAULT_RETRY_OPTIONS, RetryAbort, asyncapiHtml, backoff, buildParts, client, createCallerContext, createClientCache, createMetrics, ctx, endpoint, formatPrometheus, getDefaultRetryOptions, implement, joinPattern, localBroker, localClient, manifestFromSpec, matchParts, middleware, path, provide, redocHtml, reject, retry, server, setDefaultRetryOptions, spec, splitPattern, stableStringify, swaggerHtml, use, wsTransport };