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