@liveblocks/zenrouter 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,397 @@
1
+ import { JSONObject, JSONValue, Decoder } from 'decoders';
2
+ import { Resolve, JsonObject } from '@liveblocks/core';
3
+ import { Pipe, Strings, Tuples, ComposeLeft, Unions, Objects } from 'hotscript';
4
+
5
+ type HeadersInit = Record<string, string> | Headers;
6
+
7
+ declare class HttpError extends Error {
8
+ static readonly codes: {
9
+ [code: number]: string | undefined;
10
+ };
11
+ readonly status: number;
12
+ readonly headers?: HeadersInit;
13
+ constructor(status: number, message?: string, headers?: HeadersInit);
14
+ }
15
+ declare class ValidationError extends HttpError {
16
+ readonly status = 422;
17
+ readonly reason: string;
18
+ constructor(reason: string);
19
+ }
20
+
21
+ /**
22
+ * Checks if a Response is a generic abort response (created by abort()).
23
+ */
24
+ declare function isGenericAbort(resp: Response): boolean;
25
+ /**
26
+ * Returns an empty HTTP 204 response.
27
+ */
28
+ declare function empty(): Response;
29
+ /**
30
+ * Return a JSON response.
31
+ */
32
+ declare function json(value: JSONObject, status?: number, headers?: HeadersInit): Response;
33
+ /**
34
+ * Return an HTML response.
35
+ */
36
+ declare function html(content: string, status?: number, headers?: HeadersInit): Response;
37
+ /**
38
+ * Throws a generic abort Response for the given status code. Use this to
39
+ * terminate the handling of a route and return an HTTP error to the user.
40
+ *
41
+ * The response body will be determined by the configured error handler.
42
+ * To return a custom error body that won't be replaced, throw a json() response instead.
43
+ */
44
+ declare function abort(status: number, headers?: HeadersInit): never;
45
+ /**
46
+ * Return a streaming text response from a generator that yields strings. The
47
+ * stream will be encoded as UTF-8.
48
+ *
49
+ * Small string chunks will get buffered into emitted chunks of `bufSize` bytes (defaults to 64 kB)
50
+ * at least 64kB that many characters before being encoded and enqueued,
51
+ * reducing per-chunk transfer overhead. (By default buffers chunks of at least
52
+ * 64kB size.)
53
+ */
54
+ declare function textStream(iterable: Iterable<string>, headers?: HeadersInit, options?: {
55
+ bufSize: number;
56
+ }): Response;
57
+ /**
58
+ * Return a streaming NDJSON (Newline Delimited JSON) response from a generator
59
+ * that yields JSON values. Each value will be serialized as a single line.
60
+ */
61
+ declare function ndjsonStream(iterable: Iterable<JSONValue>, headers?: HeadersInit): Response;
62
+ /**
63
+ * Return a streaming JSON array response from a generator that yields JSON
64
+ * values. The output will be a valid JSON array: [value1,value2,...]\n
65
+ */
66
+ declare function jsonArrayStream(iterable: Iterable<JSONValue>, headers?: HeadersInit): Response;
67
+
68
+ type ErrorContext<RC> = {
69
+ req: Request;
70
+ ctx?: RC;
71
+ };
72
+ type ErrorHandlerFn<E, RC = unknown> = (error: E, extra: ErrorContext<RC>) => Response | Promise<Response>;
73
+ /**
74
+ * Central registry instance for handling HTTP errors. Has configured defaults
75
+ * for every known HTTP error code. Allows you to override those defaults and
76
+ * provide your own error handling preferences.
77
+ */
78
+ declare class ErrorHandler {
79
+ #private;
80
+ /**
81
+ * Registers a custom HTTP error handler.
82
+ *
83
+ * This will get called whenever an `HttpError` is thrown (which also happens
84
+ * with `abort()`) from a route handler.
85
+ *
86
+ * It will *NOT* get called if a `Response` instance is thrown (or returned)
87
+ * from a handler directly!
88
+ */
89
+ onError(handler: ErrorHandlerFn<HttpError>): void;
90
+ /**
91
+ * Registers a custom uncaught error handler.
92
+ *
93
+ * This will only get called if there is an unexpected error thrown from
94
+ * a route handler, i.e. something that isn't a `Response` instance, or an
95
+ * `HttpError`.
96
+ */
97
+ onUncaughtError(handler: ErrorHandlerFn<unknown>): void;
98
+ /**
99
+ * Given an error, will find the best (most-specific) error handler for it,
100
+ * and return its response.
101
+ */
102
+ handle(err: unknown, extra: ErrorContext<unknown>): Promise<Response>;
103
+ }
104
+
105
+ type Method = (typeof ALL_METHODS)[number];
106
+ declare const ALL_METHODS: readonly ["GET", "POST", "PUT", "PATCH", "DELETE"];
107
+ type PathPattern = `/${string}`;
108
+ type Pattern = `${Method} ${PathPattern}`;
109
+ type PathPrefix = `/${string}/*` | "/*";
110
+ /**
111
+ * From a pattern like:
112
+ *
113
+ * 'GET /foo/<bar>/<qux>/baz'
114
+ *
115
+ * Extracts:
116
+ *
117
+ * { foo: string, bar: string }
118
+ */
119
+ type ExtractParamsBasic<P extends Pattern> = Pipe<P, // ....................................... 'GET /foo/<bar>/<qux>/baz'
120
+ [
121
+ Strings.TrimLeft<`${Method} `>,
122
+ Strings.Split<"/">,
123
+ Tuples.Filter<Strings.StartsWith<"<">>,
124
+ Tuples.Map<ComposeLeft<[
125
+ Strings.Trim<"<" | ">">,
126
+ Unions.ToTuple,
127
+ Tuples.Append<string>
128
+ ]>>,
129
+ Tuples.ToUnion,
130
+ Objects.FromEntries
131
+ ]>;
132
+ /**
133
+ * For:
134
+ *
135
+ * {
136
+ * a: Decoder<number>,
137
+ * b: Decoder<'hi'>,
138
+ * c: Decoder<boolean>,
139
+ * }
140
+ *
141
+ * Will return:
142
+ *
143
+ * {
144
+ * a: number,
145
+ * b: 'hi',
146
+ * c: boolean,
147
+ * }
148
+ *
149
+ */
150
+ type MapDecoderTypes<T> = {
151
+ [K in keyof T]: T[K] extends Decoder<infer V> ? V : never;
152
+ };
153
+ /**
154
+ * From a pattern like:
155
+ *
156
+ * 'GET /foo/<bar>/<n>/baz'
157
+ *
158
+ * Extracts:
159
+ *
160
+ * { foo: string, n: number }
161
+ */
162
+ type ExtractParams<P extends Pattern, TParamTypes extends Record<string, unknown>, E = ExtractParamsBasic<P>> = Resolve<Pick<Omit<E, keyof TParamTypes> & TParamTypes, Extract<keyof E, string>>>;
163
+
164
+ type CorsOptions = {
165
+ /**
166
+ * Send CORS headers only if the requested Origin is in this hardcoded list
167
+ * of origins. Note that the default is '*', but this still required all
168
+ * incoming requests to have an Origin header set.
169
+ *
170
+ * @default '*' (allow any Origin)
171
+ */
172
+ allowedOrigins: "*" | string[];
173
+ /**
174
+ * When sending back CORS headers, tell the browser which methods are
175
+ * allowed.
176
+ *
177
+ * @default All methods (you should likely not have to change this)
178
+ */
179
+ allowedMethods: string[];
180
+ /**
181
+ * Specify what headers are safe to allow on _incoming_ CORS requests.
182
+ *
183
+ * By default, all headers requested by the browser client will be allowed
184
+ * (as most headers will typically be ignored by the endpoint handlers), but
185
+ * you can specify a specific whitelist of allowed headers if you need to.
186
+ *
187
+ * Browsers will only ask for non-standard headers if those should be
188
+ * allowed, i.e. a browser can ask in a preflight (OPTIONS) request, if it's
189
+ * okay to send "X-Test", but won't ask if it's okay to send, say,
190
+ * "User-Agent".
191
+ *
192
+ * Note that this is different from the `exposeHeaders` config:
193
+ * - Allowed Headers: which headers a browser may include when
194
+ * _making_ the CORS request
195
+ * - Exposed Headers: which headers _returned_ in the CORS response the
196
+ * browser is allowed to safely expose to scripts
197
+ *
198
+ * @default '*'
199
+ */
200
+ allowedHeaders: "*" | string[];
201
+ /**
202
+ * The Access-Control-Allow-Credentials response header allows browsers
203
+ * to include include credentials in the next CORS request.
204
+ *
205
+ * Credentials are cookies, TLS client certificates, or WWW-Authentication
206
+ * headers containing a username and password.
207
+ *
208
+ * NOTE: The `Authorization` header is *NOT* considered a credential and as
209
+ * such you don’t need to enable this setting for sending such headers.
210
+ *
211
+ * NOTE: Allowing credentials alone doesn’t cause the browser to send those
212
+ * credentials automatically. For to to happen, make sure to also add `{
213
+ * credentials: "include" }` on the fetch request.
214
+ *
215
+ * WARNING: By default, these credentials are not sent in cross-origin
216
+ * requests, and doing so can make a site vulnerable to CSRF attacks.
217
+ *
218
+ * @default false
219
+ */
220
+ allowCredentials: boolean;
221
+ /**
222
+ * Specify what headers browsers *scripts* can access from the CORS response.
223
+ * This means when a client tries to programmatically read
224
+ * `resp.headers.get('...')`, this header determines which headers will be
225
+ * exposed to that client.
226
+ *
227
+ * Note that this is different from the `allowedHeaders` config:
228
+ * - Allowed Headers: which headers a browser may include when
229
+ * _making_ the CORS request
230
+ * - Exposed Headers: which headers _returned_ in the CORS response the
231
+ * browser is allowed to safely expose to scripts
232
+ *
233
+ * By default, browser scripts can only read the following headers from such
234
+ * responses:
235
+ * - Cache-Control
236
+ * - Content-Language
237
+ * - Content-Type
238
+ * - Expires
239
+ * - Last-Modified
240
+ * - Pragma
241
+ */
242
+ exposeHeaders: string[];
243
+ maxAge?: number;
244
+ /**
245
+ * When `allowedOrigins` isn't an explicit list of origins but '*' (= the
246
+ * default), normally the Origin will get allowed by echoing the Origin value
247
+ * back. When this option is set, it will instead allow '*'.
248
+ *
249
+ * Do not use this in combination with `allowCredentials` as this is not
250
+ * allowed by the spec.
251
+ *
252
+ * @default false
253
+ *
254
+ */
255
+ sendWildcard: boolean;
256
+ /**
257
+ * Always send CORS headers on all responses, even if the request didn't
258
+ * contain an Origin header and thus isn't interested in CORS.
259
+ *
260
+ * @default true
261
+ */
262
+ alwaysSend: boolean;
263
+ /**
264
+ * Normally, when returning a CORS response, it's a good idea to set the
265
+ * Vary header to include 'Origin', to behave better with caching. By default
266
+ * this will be done. If you don't want to auto-add the Vary header, set this
267
+ * to false.
268
+ *
269
+ * @default true
270
+ */
271
+ varyHeader: boolean;
272
+ };
273
+
274
+ /**
275
+ * An Incoming Request is what gets passed to every route handler. It includes
276
+ * the raw (unmodified) request, the derived context (user-defined), the parsed
277
+ * URL, the type-safe params `p`, the parsed query string `q`, and a verified
278
+ * JSON body (if a decoder is provided).
279
+ */
280
+ type IncomingReq<RC, AC, TParams, TBody> = {
281
+ /**
282
+ * The incoming request.
283
+ */
284
+ readonly req: Request;
285
+ /**
286
+ * The incoming request parsed URL.
287
+ * This is equivalent to the result of `new URL(req.url)`.
288
+ */
289
+ readonly url: URL;
290
+ /**
291
+ * The user-defined static context associated with this request. This is the
292
+ * best place to attach metadata you want to carry around along with the
293
+ * request, without having to monkey-patch the request instance.
294
+ *
295
+ * Use this context for static metadata. Do not use it for auth.
296
+ *
297
+ * Basically the result of calling the configured `getContext()` function on
298
+ * the request.
299
+ */
300
+ readonly ctx: Readonly<RC>;
301
+ /**
302
+ * The result of the authorization check for this request. Basically the
303
+ * result of calling the configured `authorize()` function on the request.
304
+ */
305
+ readonly auth: Readonly<AC>;
306
+ /**
307
+ * The type-safe params available for this request. Automatically derived
308
+ * from dynamic placeholders in the pattern.
309
+ */
310
+ readonly p: TParams;
311
+ /**
312
+ * Convenience accessor for the parsed query string.
313
+ * Equivalent to `Object.entries(url.searchParams)`.
314
+ *
315
+ * Will only contain single strings, even if a query param occurs multiple
316
+ * times. If you need to read all of them, use the `url.searchParams` API
317
+ * instead.
318
+ */
319
+ readonly q: Record<string, string | undefined>;
320
+ /**
321
+ * Verified JSON body for this request, if a decoder instance was provided.
322
+ */
323
+ readonly body: TBody;
324
+ };
325
+ /**
326
+ * Limited version of an Incoming Request. This incoming request data is
327
+ * deliberately limited until after a successful auth check. Only once the
328
+ * request has been authorized, further parsing will happen.
329
+ */
330
+ type PreAuthIncomingReq<RC> = Omit<IncomingReq<Readonly<RC>, never, never, never>, "auth" | "p" | "q" | "body">;
331
+ /**
332
+ * Anything that can be returned from an endpoint implementation that would be
333
+ * considered a valid response.
334
+ */
335
+ type ResponseLike = Promise<Response | JsonObject> | Response | JsonObject;
336
+ type RouteHandler<RC, AC, TParams, TBody> = (input: IncomingReq<RC, AC, TParams, TBody>) => ResponseLike;
337
+ type RouterOptions<RC, AC, TParams extends Record<string, Decoder<unknown>>> = {
338
+ errorHandler?: ErrorHandler;
339
+ /**
340
+ * Automatically handle CORS requests. Either set to `true` (to use all the
341
+ * default CORS options), or specify a CorsOptions object.
342
+ *
343
+ * When enabled, this will do two things:
344
+ * 1. It will respond to pre-flight requests (OPTIONS) automatically.
345
+ * 2. It will add the correct CORS headers to all returned responses.
346
+ *
347
+ * @default false
348
+ */
349
+ cors?: Partial<CorsOptions> | boolean;
350
+ getContext?: (req: Request, ...args: readonly any[]) => RC;
351
+ authorize?: AuthFn<RC, AC>;
352
+ params?: TParams;
353
+ debug?: boolean;
354
+ };
355
+ type AuthFn<RC, AC> = (input: PreAuthIncomingReq<RC>) => AC | Promise<AC>;
356
+ declare class ZenRouter<RC, AC, TParams extends Record<string, Decoder<unknown>> = {}> {
357
+ #private;
358
+ constructor(options?: RouterOptions<RC, AC, TParams>);
359
+ get fetch(): (req: Request, ...rest: readonly any[]) => Promise<Response>;
360
+ route<P extends Pattern>(pattern: P, handler: RouteHandler<RC, AC, ExtractParams<P, MapDecoderTypes<TParams>>, never>): void;
361
+ route<P extends Pattern, TBody>(pattern: P, bodyDecoder: Decoder<TBody>, handler: RouteHandler<RC, AC, ExtractParams<P, MapDecoderTypes<TParams>>, TBody>): void;
362
+ onUncaughtError(handler: ErrorHandlerFn<unknown, RC>): this;
363
+ onError(handler: ErrorHandlerFn<HttpError | ValidationError, RC>): this;
364
+ }
365
+
366
+ type RequestHandler = (req: Request, ...args: readonly any[]) => Promise<Response>;
367
+ type RelayOptions = {
368
+ errorHandler?: ErrorHandler;
369
+ };
370
+ /**
371
+ * Relay won't do any route handling itself. It will only hand-off any incoming
372
+ * request to one of the configured routers, based on the incoming request path
373
+ * (first matching prefix path wins).
374
+ *
375
+ * It does NOT check the HTTP verb (GET, POST, etc).
376
+ * It does NOT do any authentication.
377
+ * It does NOT look at any headers.
378
+ *
379
+ * Subrouters (typically Router instances) are responsible for all that
380
+ * themselves.
381
+ *
382
+ * If no matching route is found, it will return a generic 404 error response.
383
+ */
384
+ declare class ZenRelay {
385
+ #private;
386
+ constructor(options?: RelayOptions);
387
+ get fetch(): (req: Request, ...rest: readonly any[]) => Promise<Response>;
388
+ /**
389
+ * If an incoming request matches the given prefix, forward the request as-is
390
+ * to the child router. Relaying happens strictly based on the request URL.
391
+ * It does not look at headers, or the HTTP method, or anything else to
392
+ * decide if it's a match.
393
+ */
394
+ relay(prefix: PathPrefix, router: ZenRouter<any, any, any> | RequestHandler): this;
395
+ }
396
+
397
+ export { type AuthFn, type ErrorContext, ErrorHandler, type ErrorHandlerFn, HttpError, ValidationError, ZenRelay, ZenRouter, abort, empty, html, isGenericAbort, json, jsonArrayStream, ndjsonStream, textStream };
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ function i(r){throw new Error(r)}function E(r,e){let t={};for(let n of Object.keys(r))t[n]=e(r[n],n);return t}var d=class r extends Error{static{this.codes={400:"Bad Request",401:"Unauthorized",403:"Forbidden",404:"Not Found",405:"Method Not Allowed",406:"Not Acceptable",409:"Conflict",411:"Length Required",413:"Payload Too Large",415:"Unsupported Media Type",422:"Unprocessable Entity",426:"Upgrade Required"}}constructor(e,t,n){(typeof e!="number"||e<100||e>=600)&&i(`Invalid HTTP status code: ${e}`),e>=500&&i("Don't use HttpError for 5xx errors"),e>=200&&e<300&&i("Cannot create an HTTP error for a success code"),t??=r.codes[e]??i(`Unknown error code ${e}, provide a message`),super(t),e===422&&!(this instanceof u)&&i("Don't use HttpError for 422 errors, use ValidationError"),this.status=e,this.headers=n}},u=class extends d{constructor(t){super(422);this.status=422;this.reason=t}};var B=1024;function W(r){let e=r[Symbol.iterator]();return new ReadableStream({pull(t){let n=e.next();n.done?t.close():t.enqueue(n.value)},cancel(){e.return?.()}})}var z=typeof ReadableStream.from=="function"?ReadableStream.from.bind(ReadableStream):W;function*k(r,e){for(let t of r)yield e(t)}var _=new WeakSet;function I(r){return _.has(r)}function Te(){return new Response(null,{status:204})}function h(r,e=200,t){return new Response(JSON.stringify(r),{status:e,headers:{...t,"Content-Type":"application/json; charset=utf-8"}})}function Ee(r,e=200,t){return new Response(r,{status:e,headers:{...t,"Content-Type":"text/html; charset=utf-8"}})}function c(r,e){let t=new Response(null,{status:r,headers:e});throw _.add(t),t}var G=new TextEncoder;function*K(r,e){let t="";for(let n of r)t+=n,t.length>=e&&(yield t,t="");t&&(yield t)}function M(r,e,t){let n=K(r,t?.bufSize??64*B),o=k(n,s=>G.encode(s));return new Response(z(o),{headers:e})}function Pe(r,e){let t=k(r,n=>`${JSON.stringify(n)}
2
+ `);return M(t,{...e,"Content-Type":"application/x-ndjson"})}function He(r,e){function*t(){yield"[";let n=!0;for(let o of r)n||(yield","),n=!1,yield JSON.stringify(o);yield`]
3
+ `}return M(t(),{...e,"Content-Type":"application/json; charset=utf-8"})}var X=r=>h({error:r.message,reason:r instanceof u?r.reason:void 0},r.status,r.headers),Q=()=>h({error:"Internal Server Error"},500),f=class{#t=null;#e=null;onError(e){this.#t!==null&&i("An error handler was already registered"),this.#t=e}onUncaughtError(e){this.#e!==null&&i("An uncaught error handler was already registered"),this.#e=e}async handle(e,t){if(e instanceof Response)if(I(e)){let n=e.status,o=Object.fromEntries(e.headers.entries());try{e=new d(n,void 0,o)}catch{return h({error:"Unknown"},n,o)}}else return e;if(e instanceof d){let n=this.#t??X;try{return await n(e,t)}catch(o){e=o}}if(this.#e)try{return await this.#e(e,t)}catch(n){e=n}else console.error(`Uncaught error: ${e?.stack??String(e)}`),console.error("...but no uncaught error handler was set up for this router.");return Q(e,t)}};import{context as fe,trace as me}from"@opentelemetry/api";import{formatShort as Re}from"decoders";var Z=/^[\w-]+$/,Y=/^[a-z]\w*$/,ee=/^\/(([\w-]+|<[\w-]+>)\/)*\*$/,x=["GET","POST","PUT","PATCH","DELETE","OPTIONS"];function q(r){return r.sort((e,t)=>x.indexOf(e)-x.indexOf(t))}var te=["GET","POST","PATCH","PUT","DELETE"];function re(r){if(r.startsWith("<")&&r.endsWith(">")){let e=r.slice(1,-1);return Y.test(e)?e:null}return null}function ne(r){for(let e of te)if(r.startsWith(e))return[e,r.slice(e.length).trimStart()];throw new Error(`Invalid route pattern: ${JSON.stringify(r)}${r.startsWith("/")?`. Did you mean ${JSON.stringify(`GET ${r}`)}?`:""}`)}function v(r,e){let t=e.exact;if(r==="/")return t?/^\/$/:/^\//;if(!r.startsWith("/")){throw new Error(`Route must start with '/', but got ${JSON.stringify(r)}`)}if(r.endsWith("/")){throw new Error(`Route may not end with '/', but got ${JSON.stringify(r)}`)}let n=r.slice(1).split("/"),o=1,s=[];for(let a of n){let l=re(a);if(l!==null)s.push(`(?<${l}>[^/]+)`);else if(Z.test(a))s.push(a);else return i(`Invalid pattern: ${r} (error at position ${o+1})`);o+=a.length+1}return new RegExp("^/"+s.join("/")+(t?"/?$":"(/|$)"))}function U(r){return ee.test(r)||i(`Invalid path prefix: ${r}`),r=r.slice(0,-2),r||="/",v(r,{exact:!1})}function L(r){let[e,t]=ne(r),n=v(t,{exact:!0});return{method:e,matchMethod(o){return e===o.method},matchURL(o){let s=o.pathname.match(n);return s===null?null:s.groups??{}}}}var D=new WeakMap;function g(r){return D.get(r)}function $(r,e){return D.set(r,e),e}var oe={allowedOrigins:"*",allowedMethods:x,allowedHeaders:"*",allowCredentials:!1,exposeHeaders:[],maxAge:void 0,sendWildcard:!1,alwaysSend:!0,varyHeader:!0},b="Access-Control-Allow-Origin",se="Access-Control-Allow-Methods",ae="Access-Control-Allow-Headers",ie="Access-Control-Expose-Headers",le="Access-Control-Allow-Credentials",de="Access-Control-Max-Age",ce="Access-Control-Request-Method",ue="Access-Control-Request-Headers";function pe(r,e){if(r.sendWildcard&&r.allowCredentials)throw new Error("Invalid CORS configuration");let t=r.allowedOrigins==="*",n=t?[]:r.allowedOrigins,o=e.headers.get("Origin")??e.headers.get("X-Relay-Origin");return o?t&&r.sendWildcard?"*":t||n.includes(o)?o:null:r.alwaysSend?t?r.allowCredentials?null:"*":n[0]??null:null}function he(r,e){let t=(e.headers.get(ue)??"").toLowerCase().split(",").map(o=>o.trim()).filter(Boolean),n=r==="*"?t:t.filter(o=>r.includes(o));return n.length>0?n:null}function j(r,e){let t={...oe,...e},n=pe(t,r);if(n===null)return null;let o=new Headers;if(o.set(b,n),t.exposeHeaders.length>0&&o.set(ie,t.exposeHeaders.join(", ")),t.allowCredentials&&o.set(le,"true"),r.method==="OPTIONS"){let s=(r.headers.get(ce)??"").toUpperCase();if(s&&t.allowedMethods.includes(s)){let a=he(t.allowedHeaders,r);a&&o.set(ae,a.join(", ")),t.maxAge&&o.set(de,String(t.maxAge)),o.set(se,t.allowedMethods.join(", "))}else console.log("The request's Access-Control-Request-Method header does not match allowed methods. CORS headers will not be applied.")}return t.varyHeader&&(o.get(b)==="*"||o.set("Vary","Origin")),o}var w=class{#t;#e;#n;#r;#a;#o;#s;constructor(e){this.#o=e?.errorHandler??new f,this.#t=e?.debug??!1,this.#e=e?.getContext??(()=>null),this.#n=e?.authorize??(()=>(console.error("This request was not checked for authorization. Please configure a generic `authorize` function in the ZenRouter constructor."),c(403))),this.#r=[],this.#a=e?.params??{},this.#s=(e?.cors===!0?{}:e?.cors)||null}get fetch(){if(this.#r.length===0)throw new Error("No routes configured yet. Try adding one?");return async(e,...t)=>{let n=await this.#c(e,...t);return this.#h(e,n)}}route(e,t,n){let o=e,s=arguments.length>=3?t:null,a=arguments.length>=3?n:t;this.#d(o,s,a)}onUncaughtError(e){return this.#o.onUncaughtError(e),this}onError(e){return this.#o.onError(e),this}#l(e,...t){return g(e)??$(e,this.#e(e,...t))}#d(e,t,n){let o=L(e);this.#r.push([e,o,this.#n,t,ye(n)])}async#c(e,...t){try{return await this.#p(e,...t)}catch(n){return this.#o.handle(n,{req:e,ctx:g(e)})}}#i(e){let t=new URL(e.url),n=new Set;n.add("OPTIONS");for(let[o,s]of this.#r){if(n.has(s.method))continue;s.matchURL(t)&&n.add(s.method)}return q(Array.from(n))}#u(e){return new Response(null,{status:204,headers:{Allow:this.#i(e).join(", ")}})}async#p(e,...t){if(e.method==="OPTIONS")return this.#u(e);let n=new URL(e.url),o=this.#t?console.log.bind(console):void 0;o?.(`Trying to match ${e.method} ${n.pathname}`);let s=!1;for(let a of this.#r){let[l,m,F,P,J]=a,C=m.matchURL(n);if(C===null){o?.(` ...against ${l}? \u274C No match`);continue}else{if(s=!0,!m.matchMethod(e)){o?.(` ...against ${l}? \u{1F9D0} Path matches, but method did not! ${JSON.stringify(C)}`);continue}o?.(` ...against ${l}? \u2705 Match! ${JSON.stringify(C)}`);let H=me.getSpan(fe.active());H?.setAttribute("zen.route",l);let A={req:e,url:n,ctx:this.#l(e,...t)},O=await F(A);if(!O)return c(403);let R;try{R=E(C,decodeURIComponent),R=E(R,(p,T)=>{let S=this.#a[T];return S===void 0?p:S.verify(p)})}catch{return c(400)}for(let[p,T]of Object.entries(R))H?.setAttribute(`zen.param.${p}`,String(T));let y=P?P.decode(await ge(e)):null;if(y&&!y.ok){let p=Re(y.error);throw new u(p)}let V={...A,auth:O,p:R,q:Object.fromEntries(n.searchParams),get body(){return y===null&&i("Cannot access body: this endpoint did not define a body decoder"),y.value}};return await J(V)}}return s?c(405,{Allow:this.#i(e).join(", ")}):c(404)}#h(e,t){if(!this.#s||t.status===101||t.status>=300&&t.status<400||t.headers.has(b))return t;let n=j(e,this.#s);if(n===null)return t;let o=new Headers(t.headers);for(let[l,m]of n)l==="vary"?o.append(l,m):o.set(l,m);let{status:s,body:a}=t;return new Response(a,{status:s,headers:o})}};function ye(r){return async e=>{let t=await r(e);return t instanceof Response?t:h(t,200)}}async function ge(r){try{let e=await r.text();return e===""?void 0:JSON.parse(e)}catch{c(400)}}var N=class{#t;#e=[];constructor(e){this.#t=e?.errorHandler??new f}get fetch(){return this.#n.bind(this)}relay(e,t){let n=U(e);return this.#e.push([n,t instanceof w?t.fetch:t]),this}async#n(e,...t){try{return await this.#r(e,...t)}catch(n){return n instanceof d||n instanceof Response||console.error(`Relayer caught error in subrouter! This should never happen, as routers should never throw an unexpected error! ${String(n)}`),this.#t.handle(n,{req:e,ctx:g(e)})}}#r(e,...t){let n=new URL(e.url).pathname;for(let[o,s]of this.#e)if(o.test(n))return s(e,...t);return c(404)}};export{f as ErrorHandler,d as HttpError,u as ValidationError,N as ZenRelay,w as ZenRouter,c as abort,Te as empty,Ee as html,I as isGenericAbort,h as json,He as jsonArrayStream,Pe as ndjsonStream,M as textStream};
4
+ // istanbul ignore next -- @preserve
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/utils.ts","../src/responses/HttpError.ts","../src/responses/index.ts","../src/ErrorHandler.ts","../src/Router.ts","../src/lib/matchers.ts","../src/contexts.ts","../src/cors.ts","../src/Relay.ts"],"sourcesContent":["export function raise(message: string): never {\n throw new Error(message);\n}\n\nexport function mapv<T, U>(\n obj: Record<string, T>,\n mapper: (value: T, key: string) => U\n): Record<string, U> {\n const rv: Record<string, U> = {};\n for (const key of Object.keys(obj)) {\n rv[key] = mapper(obj[key], key);\n }\n return rv;\n}\n","import { raise } from \"~/lib/utils.js\";\n\nimport type { HeadersInit } from \"./compat.js\";\n\nexport class HttpError extends Error {\n static readonly codes: { [code: number]: string | undefined } = {\n 400: \"Bad Request\",\n 401: \"Unauthorized\",\n // 402: \"Payment Required\",\n 403: \"Forbidden\",\n 404: \"Not Found\",\n 405: \"Method Not Allowed\",\n 406: \"Not Acceptable\",\n // 407: \"Proxy Authentication Required\",\n // 408: \"Request Timeout\",\n 409: \"Conflict\",\n // 410: \"Gone\",\n 411: \"Length Required\",\n // 412: \"Precondition Failed\",\n 413: \"Payload Too Large\",\n // 414: \"URI Too Long\",\n 415: \"Unsupported Media Type\",\n // 416: \"Range Not Satisfiable\",\n // 417: \"Expectation Failed\",\n // 418: \"I'm a teapot\",\n // 421: \"Misdirected Request\",\n 422: \"Unprocessable Entity\",\n // 423: \"Locked\",\n // 424: \"Failed Dependency\",\n // 425: \"Too Early\",\n 426: \"Upgrade Required\",\n // 428: \"Precondition Required\",\n // 429: \"Too Many Requests\",\n // 431: \"Request Header Fields Too Large\",\n // 451: \"Unavailable For Legal Reasons\",\n // 500: \"Internal Server Error\",\n };\n\n // TODO Add support for \"public reason\" details?\n public readonly status: number;\n public readonly headers?: HeadersInit;\n\n constructor(status: number, message?: string, headers?: HeadersInit) {\n if (typeof status !== \"number\" || status < 100 || status >= 600) {\n raise(`Invalid HTTP status code: ${status}`);\n }\n\n if (status >= 500) {\n raise(\"Don't use HttpError for 5xx errors\");\n }\n\n if (status >= 200 && status < 300) {\n raise(\"Cannot create an HTTP error for a success code\");\n }\n\n message ??=\n HttpError.codes[status] ??\n raise(`Unknown error code ${status}, provide a message`);\n super(message);\n\n if (status === 422 && !(this instanceof ValidationError)) {\n raise(\"Don't use HttpError for 422 errors, use ValidationError\");\n }\n\n this.status = status;\n this.headers = headers;\n }\n}\n\nexport class ValidationError extends HttpError {\n public readonly status = 422;\n public readonly reason: string;\n\n constructor(reason: string) {\n super(422);\n this.reason = reason;\n }\n}\n","import type { JSONObject, JSONValue } from \"decoders\";\n\nimport type { HeadersInit } from \"./compat.js\";\nimport { HttpError, ValidationError } from \"./HttpError.js\";\n\nconst KB = 1024;\n\n/**\n * A simple shim for ReadableStream.from(). Uses the native implementation if\n * available.\n *\n * This polyfill does not guarantee spec-compliance, and merely exists so we\n * can use ReadableStream.from() in environments that don't support this API\n * yet, like Bun, or old Node versions.\n *\n * This API is available in the following runtimes:\n * - Node.js (since v20.6+)\n * - Cloudflare Workers (since Apr 4, 2024)\n *\n * But not supported yet in:\n * - Bun - see https://github.com/oven-sh/bun/issues/3700\n */\nfunction ReadableStream_from_shim<T>(iterable: Iterable<T>): ReadableStream<T> {\n const iterator = iterable[Symbol.iterator]();\n return new ReadableStream<T>({\n pull(controller) {\n const res = iterator.next();\n if (res.done) {\n controller.close();\n } else {\n controller.enqueue(res.value);\n }\n },\n cancel() {\n iterator.return?.();\n },\n });\n}\n\n/* eslint-disable */\nconst ReadableStream_from =\n typeof (ReadableStream as any).from === \"function\"\n ? ((ReadableStream as any).from.bind(ReadableStream) as <T>(\n iterable: Iterable<T>\n ) => ReadableStream<T>)\n : ReadableStream_from_shim;\n/* eslint-enable */\n\nfunction* imap<T, U>(iterable: Iterable<T>, fn: (x: T) => U): Iterable<U> {\n for (const x of iterable) {\n yield fn(x);\n }\n}\n\n/**\n * WeakSet tracking \"generic\" abort responses.\n * Generic responses can be replaced by the error handler with custom error formatting.\n * Non-generic responses (e.g., custom json() responses) are returned verbatim.\n */\nconst genericAborts = new WeakSet<Response>();\n\n/**\n * Checks if a Response is a generic abort response (created by abort()).\n */\nexport function isGenericAbort(resp: Response): boolean {\n return genericAborts.has(resp);\n}\n\n/**\n * Returns an empty HTTP 204 response.\n */\nexport function empty(): Response {\n return new Response(null, { status: 204 });\n}\n\n/**\n * Return a JSON response.\n */\nexport function json(\n value: JSONObject,\n status = 200,\n headers?: HeadersInit\n): Response {\n return new Response(JSON.stringify(value), {\n status,\n headers: { ...headers, \"Content-Type\": \"application/json; charset=utf-8\" },\n });\n}\n\n/**\n * Return an HTML response.\n */\nexport function html(\n content: string,\n status = 200,\n headers?: HeadersInit\n): Response {\n return new Response(content, {\n status,\n headers: { ...headers, \"Content-Type\": \"text/html; charset=utf-8\" },\n });\n}\n\n/**\n * Throws a generic abort Response for the given status code. Use this to\n * terminate the handling of a route and return an HTTP error to the user.\n *\n * The response body will be determined by the configured error handler.\n * To return a custom error body that won't be replaced, throw a json() response instead.\n */\nexport function abort(status: number, headers?: HeadersInit): never {\n const resp = new Response(null, { status, headers });\n genericAborts.add(resp);\n throw resp;\n}\n\nconst encoder = new TextEncoder();\n\n/**\n * Batch small string chunks into larger blocks of at least `minSize`\n * characters before yielding. Reduces per-chunk overhead when the source\n * generator yields many tiny strings.\n */\nfunction* buffered(iterable: Iterable<string>, size: number): Iterable<string> {\n let buf = \"\";\n for (const s of iterable) {\n buf += s;\n if (buf.length >= size) {\n yield buf;\n buf = \"\";\n }\n }\n if (buf) yield buf;\n}\n\n/**\n * Return a streaming text response from a generator that yields strings. The\n * stream will be encoded as UTF-8.\n *\n * Small string chunks will get buffered into emitted chunks of `bufSize` bytes (defaults to 64 kB)\n * at least 64kB that many characters before being encoded and enqueued,\n * reducing per-chunk transfer overhead. (By default buffers chunks of at least\n * 64kB size.)\n */\nexport function textStream(\n iterable: Iterable<string>,\n headers?: HeadersInit,\n options?: {\n bufSize: number;\n }\n): Response {\n const source = buffered(iterable, options?.bufSize ?? 64 * KB);\n const chunks = imap(source, (s) => encoder.encode(s));\n return new Response(\n ReadableStream_from(chunks) as unknown as string,\n // ^^^^^^^^^^^^^^^^^^^^\n // This ugly cast needed due to Node.js vs Cloudflare\n // Workers ReadableStream type mismatch :(\n { headers }\n );\n}\n\n/**\n * Return a streaming NDJSON (Newline Delimited JSON) response from a generator\n * that yields JSON values. Each value will be serialized as a single line.\n */\nexport function ndjsonStream(\n iterable: Iterable<JSONValue>,\n headers?: HeadersInit\n): Response {\n const lines = imap(iterable, (value) => `${JSON.stringify(value)}\\n`);\n return textStream(lines, {\n ...headers,\n \"Content-Type\": \"application/x-ndjson\",\n });\n}\n\n/**\n * Return a streaming JSON array response from a generator that yields JSON\n * values. The output will be a valid JSON array: [value1,value2,...]\\n\n */\nexport function jsonArrayStream(\n iterable: Iterable<JSONValue>,\n headers?: HeadersInit\n): Response {\n function* chunks() {\n yield \"[\";\n let first = true;\n for (const value of iterable) {\n if (!first) yield \",\";\n first = false;\n yield JSON.stringify(value);\n }\n yield \"]\\n\";\n }\n return textStream(chunks(), {\n ...headers,\n \"Content-Type\": \"application/json; charset=utf-8\",\n });\n}\n\nexport { HttpError, ValidationError };\n","import { raise } from \"~/lib/utils.js\";\nimport {\n HttpError,\n isGenericAbort,\n json,\n ValidationError,\n} from \"~/responses/index.js\";\n\nexport type ErrorContext<RC> = {\n req: Request;\n ctx?: RC;\n};\n\nexport type ErrorHandlerFn<E, RC = unknown> = (\n error: E,\n extra: ErrorContext<RC>\n) => Response | Promise<Response>;\n\n// The default handler for HttpErrors, in case no custom handler is provided\nconst defaultHttpErrorHandler: ErrorHandlerFn<HttpError> = (e) =>\n json(\n {\n error: e.message,\n reason: e instanceof ValidationError ? e.reason : undefined,\n },\n e.status,\n e.headers\n );\n\n// The default uncaught error handler, in case no custom handler is provided.\n// It's the ultimate fallback if everything else has failed.\nconst defaultUncaughtErrorHandler: ErrorHandlerFn<unknown> = () =>\n json({ error: \"Internal Server Error\" }, 500);\n\n/**\n * Central registry instance for handling HTTP errors. Has configured defaults\n * for every known HTTP error code. Allows you to override those defaults and\n * provide your own error handling preferences.\n */\nexport class ErrorHandler {\n // A registered error handler, if any error handlers, ordered from most-specific to\n // least-specific\n #_httpErrorHandler: ErrorHandlerFn<HttpError> | null = null;\n\n // A registered error handler to be called for any uncaught (non-HttpError)\n // errors. They will typically be Error instances, but it cannot be\n // guaranteed they are.\n #_uncaughtErrorHandler: ErrorHandlerFn<unknown> | null = null;\n\n /**\n * Registers a custom HTTP error handler.\n *\n * This will get called whenever an `HttpError` is thrown (which also happens\n * with `abort()`) from a route handler.\n *\n * It will *NOT* get called if a `Response` instance is thrown (or returned)\n * from a handler directly!\n */\n public onError(handler: ErrorHandlerFn<HttpError>): void {\n if (this.#_httpErrorHandler !== null) {\n raise(\"An error handler was already registered\");\n }\n this.#_httpErrorHandler = handler;\n }\n\n /**\n * Registers a custom uncaught error handler.\n *\n * This will only get called if there is an unexpected error thrown from\n * a route handler, i.e. something that isn't a `Response` instance, or an\n * `HttpError`.\n */\n public onUncaughtError(handler: ErrorHandlerFn<unknown>): void {\n if (this.#_uncaughtErrorHandler !== null) {\n raise(\"An uncaught error handler was already registered\");\n }\n this.#_uncaughtErrorHandler = handler;\n }\n\n /**\n * Given an error, will find the best (most-specific) error handler for it,\n * and return its response.\n */\n public async handle(\n err: unknown,\n extra: ErrorContext<unknown>\n ): Promise<Response> {\n // If it's a Response, check if it's a generic abort or a custom response\n if (err instanceof Response) {\n if (isGenericAbort(err)) {\n // Generic abort - convert to HttpError and run through normal error handling\n const status = err.status;\n const headers = Object.fromEntries(err.headers.entries());\n try {\n err = new HttpError(status, undefined, headers);\n // Fall through to HttpError handling below\n } catch {\n // Status code not supported by HttpError (5xx, 422, or unknown code)\n return json({ error: \"Unknown\" }, status, headers);\n }\n } else {\n // Custom response - return verbatim\n return err;\n }\n }\n\n // If error is not an instance of HttpError, then it's an otherwise\n // uncaught error that should lead to an 5xx response. We'll wrap it in an\n // UncaughtError instance, so the custom handler will only ever have to\n // deal with HttpErrors.\n if (err instanceof HttpError) {\n const httpErrorHandler =\n this.#_httpErrorHandler ?? defaultHttpErrorHandler;\n try {\n return await httpErrorHandler(err, extra);\n } catch (e) {\n // Fall through, let the uncaught error handler handle it\n err = e;\n }\n }\n\n // At this point, `err` can be anything\n if (this.#_uncaughtErrorHandler) {\n try {\n return await this.#_uncaughtErrorHandler(err, extra);\n } catch (e) {\n // Fall through\n // istanbul ignore next -- @preserve\n err = e;\n }\n } else {\n console.error(`Uncaught error: ${(err as Error)?.stack ?? String(err)}`); // prettier-ignore\n console.error(\"...but no uncaught error handler was set up for this router.\"); // prettier-ignore\n }\n\n // The default uncaught error handler cannot fail. It's the ultimate fallback.\n return defaultUncaughtErrorHandler(err, extra);\n }\n}\n","/* eslint-disable @typescript-eslint/no-unsafe-argument */\n\n// TODO: Make this a local definition?\nimport type { Json, JsonObject } from \"@liveblocks/core\";\nimport { context, trace } from \"@opentelemetry/api\";\nimport type { Decoder } from \"decoders\";\nimport { formatShort } from \"decoders\";\n\nimport type {\n ExtractParams,\n HttpVerb,\n MapDecoderTypes,\n Pattern,\n RouteMatcher,\n} from \"~/lib/matchers.js\";\nimport { routeMatcher, sortHttpVerbsInPlace } from \"~/lib/matchers.js\";\nimport { mapv, raise } from \"~/lib/utils.js\";\nimport type { HttpError } from \"~/responses/index.js\";\nimport { abort, json, ValidationError } from \"~/responses/index.js\";\n\nimport { attachContext, lookupContext } from \"./contexts.js\";\nimport type { CorsOptions } from \"./cors.js\";\nimport { AC_ORIGIN, getCorsHeaders } from \"./cors.js\";\nimport type { ErrorHandlerFn } from \"./ErrorHandler.js\";\nimport { ErrorHandler } from \"./ErrorHandler.js\";\n\n/**\n * An Incoming Request is what gets passed to every route handler. It includes\n * the raw (unmodified) request, the derived context (user-defined), the parsed\n * URL, the type-safe params `p`, the parsed query string `q`, and a verified\n * JSON body (if a decoder is provided).\n */\ntype IncomingReq<RC, AC, TParams, TBody> = {\n /**\n * The incoming request.\n */\n readonly req: Request;\n /**\n * The incoming request parsed URL.\n * This is equivalent to the result of `new URL(req.url)`.\n */\n readonly url: URL;\n /**\n * The user-defined static context associated with this request. This is the\n * best place to attach metadata you want to carry around along with the\n * request, without having to monkey-patch the request instance.\n *\n * Use this context for static metadata. Do not use it for auth.\n *\n * Basically the result of calling the configured `getContext()` function on\n * the request.\n */\n readonly ctx: Readonly<RC>;\n /**\n * The result of the authorization check for this request. Basically the\n * result of calling the configured `authorize()` function on the request.\n */\n readonly auth: Readonly<AC>;\n /**\n * The type-safe params available for this request. Automatically derived\n * from dynamic placeholders in the pattern.\n */\n readonly p: TParams;\n /**\n * Convenience accessor for the parsed query string.\n * Equivalent to `Object.entries(url.searchParams)`.\n *\n * Will only contain single strings, even if a query param occurs multiple\n * times. If you need to read all of them, use the `url.searchParams` API\n * instead.\n */\n readonly q: Record<string, string | undefined>;\n /**\n * Verified JSON body for this request, if a decoder instance was provided.\n */\n readonly body: TBody;\n};\n\n/**\n * Limited version of an Incoming Request. This incoming request data is\n * deliberately limited until after a successful auth check. Only once the\n * request has been authorized, further parsing will happen.\n */\ntype PreAuthIncomingReq<RC> = Omit<\n IncomingReq<Readonly<RC>, never, never, never>,\n \"auth\" | \"p\" | \"q\" | \"body\"\n>;\n\n/**\n * Anything that can be returned from an endpoint implementation that would be\n * considered a valid response.\n */\ntype ResponseLike = Promise<Response | JsonObject> | Response | JsonObject;\n\n// type AuthHandler<R extends Request, RC, TParams> = (\n// input: IncomingReq<R, RC, TParams>\n// ) => boolean;\n\ntype RouteHandler<RC, AC, TParams, TBody> = (\n input: IncomingReq<RC, AC, TParams, TBody>\n) => ResponseLike;\n\ntype RouteTuple<RC, AC> = readonly [\n pattern: Pattern,\n matcher: RouteMatcher,\n auth: AuthFn<RC, AC>,\n bodyDecoder: Decoder<unknown> | null,\n handler: OpaqueRouteHandler<RC, AC>,\n];\n\ntype RouterOptions<RC, AC, TParams extends Record<string, Decoder<unknown>>> = {\n errorHandler?: ErrorHandler;\n\n // Mandatory config\n /**\n * Automatically handle CORS requests. Either set to `true` (to use all the\n * default CORS options), or specify a CorsOptions object.\n *\n * When enabled, this will do two things:\n * 1. It will respond to pre-flight requests (OPTIONS) automatically.\n * 2. It will add the correct CORS headers to all returned responses.\n *\n * @default false\n */\n cors?: Partial<CorsOptions> | boolean;\n getContext?: (req: Request, ...args: readonly any[]) => RC;\n authorize?: AuthFn<RC, AC>;\n\n // Register any param decoders\n params?: TParams;\n\n // Optional config\n debug?: boolean;\n};\n\nexport type AuthFn<RC, AC> = (\n input: PreAuthIncomingReq<RC>\n) => AC | Promise<AC>;\n\ntype OpaqueRouteHandler<RC, AC> = (\n input: IncomingReq<RC, AC, OpaqueParams, unknown>\n) => Promise<Response>;\n\ntype OpaqueParams = Record<string, unknown>;\n\nexport class ZenRouter<\n RC,\n AC,\n TParams extends Record<string, Decoder<unknown>> = {},\n> {\n #_debug: boolean;\n #_contextFn: (req: Request, ...args: readonly any[]) => RC;\n #_defaultAuthFn: AuthFn<RC, AC>;\n #_routes: RouteTuple<RC, AC>[];\n #_paramDecoders: TParams;\n #_errorHandler: ErrorHandler;\n #_cors: Partial<CorsOptions> | null;\n\n constructor(options?: RouterOptions<RC, AC, TParams>) {\n this.#_errorHandler = options?.errorHandler ?? new ErrorHandler();\n this.#_debug = options?.debug ?? false;\n this.#_contextFn = options?.getContext ?? (() => null as any as RC);\n this.#_defaultAuthFn =\n options?.authorize ??\n (() => {\n // TODO Maybe make this fail as a 500 with info in the body? Since this is a setup error and should never be an issue in production.\n console.error(\"This request was not checked for authorization. Please configure a generic `authorize` function in the ZenRouter constructor.\"); // prettier-ignore\n return abort(403);\n });\n this.#_routes = [];\n this.#_paramDecoders = options?.params ?? ({} as TParams);\n this.#_cors = (options?.cors === true ? {} : options?.cors) || null;\n }\n\n // --- PUBLIC APIs -----------------------------------------------------------------\n\n public get fetch(): (\n req: Request,\n ...rest: readonly any[]\n ) => Promise<Response> {\n if (this.#_routes.length === 0) {\n throw new Error(\"No routes configured yet. Try adding one?\");\n }\n\n return async (req: Request, ...rest: readonly any[]): Promise<Response> => {\n const resp = await this.#_tryDispatch(req, ...rest);\n return this.#_addCorsIfNeeded(req, resp);\n };\n }\n\n public route<P extends Pattern>(\n pattern: P,\n handler: RouteHandler<\n RC,\n AC,\n ExtractParams<P, MapDecoderTypes<TParams>>,\n never\n >\n ): void;\n public route<P extends Pattern, TBody>(\n pattern: P,\n bodyDecoder: Decoder<TBody>,\n handler: RouteHandler<\n RC,\n AC,\n ExtractParams<P, MapDecoderTypes<TParams>>,\n TBody\n >\n ): void;\n /* eslint-disable @typescript-eslint/explicit-module-boundary-types */\n /* eslint-disable @typescript-eslint/no-unsafe-assignment */\n public route(first: any, second: any, third?: any): void {\n /* eslint-enable @typescript-eslint/explicit-module-boundary-types */\n const pattern = first;\n const bodyDecoder = arguments.length >= 3 ? second : null;\n const handler = arguments.length >= 3 ? third : second;\n /* eslint-enable @typescript-eslint/no-unsafe-assignment */\n this.#_register(\n pattern,\n bodyDecoder,\n handler as RouteHandler<RC, AC, OpaqueParams, unknown>\n );\n }\n\n // TODO Maybe remove this on the Router class, since it's only a pass-through method\n public onUncaughtError(handler: ErrorHandlerFn<unknown, RC>): this {\n this.#_errorHandler.onUncaughtError(handler as ErrorHandlerFn<unknown>);\n return this;\n }\n\n // TODO Maybe remove this on the Router class, since it's only a pass-through method\n public onError(\n handler: ErrorHandlerFn<HttpError | ValidationError, RC>\n // ^^^^^^^^^^^^^^^\n // Technically this isn't needed, because it is a subclass of\n // HttpError already, but adding it here anyway for clarity.\n ): this {\n this.#_errorHandler.onError(handler as ErrorHandlerFn<unknown>);\n return this;\n }\n\n // public get registerCannedResponse() {\n // const eh = this.#_errorHandler;\n // return eh.registerCannedResponse.bind(eh);\n // }\n\n // --- PRIVATE APIs ----------------------------------------------------------------\n\n #_getContext(req: Request, ...args: readonly any[]): RC {\n return (\n lookupContext<RC>(req) ??\n attachContext(req, this.#_contextFn(req, ...args))\n );\n }\n\n #_register<P extends Pattern>(\n pattern: P,\n bodyDecoder: Decoder<unknown> | null,\n handler: RouteHandler<RC, AC, OpaqueParams, unknown>\n // authFn?: OpaqueAuthFn<RC>\n ): void {\n const matcher = routeMatcher(pattern);\n\n this.#_routes.push([\n pattern,\n matcher,\n /* authFn ?? */ this.#_defaultAuthFn,\n bodyDecoder,\n wrap(handler),\n ]);\n }\n\n /**\n * Calls .#_dispatch(), but will catch any thrown error (which could be\n * a known HTTP error) or an uncaught error, and makes sure to always return\n * a Response.\n */\n async #_tryDispatch(\n req: Request,\n ...args: readonly any[]\n ): Promise<Response> {\n try {\n return await this.#_dispatch(req, ...args); // eslint-disable @typescript-eslint/no-unsafe-argument\n } catch (err) {\n return this.#_errorHandler.handle(err, { req, ctx: lookupContext(req) });\n }\n }\n\n #_getAllowedVerbs(req: Request): string[] {\n const url = new URL(req.url);\n\n const verbs: Set<HttpVerb> = new Set();\n verbs.add(\"OPTIONS\"); // Always include OPTIONS\n\n // Collect HTTP verbs that are valid for this URL\n for (const [_, matcher] of this.#_routes) {\n // If we already collected this method, avoid the regex matching\n if (verbs.has(matcher.method)) continue;\n\n const match = matcher.matchURL(url);\n if (match) {\n verbs.add(matcher.method);\n }\n }\n\n return sortHttpVerbsInPlace(Array.from(verbs));\n }\n\n #_dispatch_OPTIONS(req: Request): Response {\n // All responses to OPTIONS requests must be 2xx\n return new Response(null, {\n status: 204,\n headers: {\n Allow: this.#_getAllowedVerbs(req).join(\", \"),\n },\n });\n }\n\n /**\n * Given an incoming request, starts matching its URL to one of the\n * configured routes, and invoking it if a match is found. Will not (and\n * should not) perform any error handling itself.\n *\n * Can throw:\n * - HTTP 400, if a route matches, but its params are incorrectly encoded\n * - HTTP 403, if a route matches, but the request isn't correctly authorized\n * - HTTP 404, if none of the routes matches\n * - HTTP 405, if a route path matches, but its method did not\n * - HTTP 422, if a route matches, but its body could not be validated\n */\n async #_dispatch(req: Request, ...args: readonly any[]): Promise<Response> {\n if (req.method === \"OPTIONS\") {\n return this.#_dispatch_OPTIONS(req);\n }\n\n const url = new URL(req.url);\n const log = this.#_debug\n ? /* istanbul ignore next */\n console.log.bind(console)\n : undefined;\n log?.(`Trying to match ${req.method} ${url.pathname}`);\n\n // Match routes in the given order\n let pathDidMatch = false;\n for (const tup of this.#_routes) {\n const [pattern, matcher, authorize, bodyDecoder, handler] = tup;\n\n const match = matcher.matchURL(url);\n if (match === null) {\n log?.(` ...against ${pattern}? ❌ No match`);\n continue;\n } else {\n pathDidMatch = true;\n if (!matcher.matchMethod(req)) {\n log?.(\n ` ...against ${pattern}? 🧐 Path matches, but method did not! ${JSON.stringify(match)}`\n );\n continue;\n }\n\n log?.(` ...against ${pattern}? ✅ Match! ${JSON.stringify(match)}`);\n\n // Add route pattern as span attribute\n // This is done early so the route is recorded even for auth/validation errors\n const span = trace.getSpan(context.active());\n span?.setAttribute(\"zen.route\", pattern);\n\n const base = {\n req,\n url,\n ctx: this.#_getContext(req, ...args),\n };\n\n // Perform auth\n const auth = await authorize(base);\n if (!auth) {\n return abort(403);\n }\n\n // Verify route params\n let p;\n try {\n p = mapv(match, decodeURIComponent);\n p = mapv(p, (value, key) => {\n const decoder = this.#_paramDecoders[key];\n return decoder === undefined ? value : decoder.verify(value);\n });\n } catch (err) {\n // A malformed URI that cannot be decoded properly or a param that\n // could not be decoded properly are both Bad Requests\n return abort(400);\n }\n\n // Add decoded route params as span attributes\n for (const [key, value] of Object.entries(p)) {\n span?.setAttribute(`zen.param.${key}`, String(value));\n }\n\n const decodeResult = bodyDecoder\n ? // TODO: This can throw if the body does not contain a valid JSON\n // request. If so, we should return a 400.\n bodyDecoder.decode(await tryReadBodyAsJson(req))\n : null;\n\n if (decodeResult && !decodeResult.ok) {\n const errmsg = formatShort(decodeResult.error);\n throw new ValidationError(errmsg);\n }\n\n // Decode the body\n const input = {\n ...base,\n auth,\n p,\n q: Object.fromEntries(url.searchParams),\n get body() {\n if (decodeResult === null) {\n raise(\"Cannot access body: this endpoint did not define a body decoder\"); // prettier-ignore\n }\n return decodeResult.value;\n },\n };\n\n return await handler(input);\n }\n }\n\n if (pathDidMatch) {\n // If one of the paths did match, we can return a 405 error\n return abort(405, { Allow: this.#_getAllowedVerbs(req).join(\", \") });\n }\n\n return abort(404);\n }\n\n #_addCorsIfNeeded(req: Request, resp: Response): Response {\n if (!this.#_cors) {\n // We don't want to handle CORS\n return resp;\n }\n\n // Never add CORS headers to the following response codes\n if (\n // Never add to 101 (Switching Protocols) or 3xx (redirect) responses\n resp.status === 101 ||\n (resp.status >= 300 && resp.status < 400)\n ) {\n return resp;\n }\n\n // If this response already contains the main CORS header, don't touch it\n // further\n if (resp.headers.has(AC_ORIGIN)) {\n // TODO Maybe throw if this happens? It definitely would be unexpected and\n // undesired and it's better to let Zen Router be in control here.\n return resp;\n }\n\n // If we enabled automatic CORS handling, add necessary CORS headers to the\n // response now\n const corsHeadersToAdd = getCorsHeaders(req, this.#_cors);\n if (corsHeadersToAdd === null) {\n // Not a CORS request, or CORS not allowed\n return resp;\n }\n\n // This requires a CORS response, so let's add the headers to the returned output\n const headers = new Headers(resp.headers);\n for (const [k, v] of corsHeadersToAdd) {\n if (k === \"vary\") {\n // Important to not override any existing Vary headers\n headers.append(k, v);\n } else {\n // Here, `k` is an `Access-Control-*` header\n headers.set(k, v);\n }\n }\n\n // Unfortunately, if we're in a Cloudflare Workers runtime you cannot mutate\n // headers on a Response instance directly (as you can in Node or Bun).\n // So we'll have to reconstruct a new Response instance here :(\n const { status, body } = resp;\n return new Response(body, { status, headers });\n }\n}\n\n/**\n * Helper to handle any endpoint handlers returning a JSON object, and turning\n * that into a 200 response if so.\n */\nfunction wrap<RC, AC>(\n handler: RouteHandler<RC, AC, OpaqueParams, unknown>\n): OpaqueRouteHandler<RC, AC> {\n return async (input) => {\n const result = await handler(input);\n if (result instanceof Response) {\n return result;\n } else {\n return json(result, 200);\n }\n };\n}\n\n/**\n * Attempts to reads the request body as JSON. Will return an empty request\n * body as `undefined`.\n */\n// TODO Currently, this helper will not look at or respect the Content-Type\n// TODO header, and I think that is a bug.\n// TODO Need to think about how to best handle this exactly without breaking\n// TODO this API for the \"lazy\" that never set `content-type` to\n// TODO \"application/json\" explicitly.\nasync function tryReadBodyAsJson(req: Request): Promise<Json | undefined> {\n // Try reading JSON body\n try {\n const text = await req.text();\n return text === \"\" ? undefined : (JSON.parse(text) as Json);\n } catch (e) {\n // Invalid JSON body\n abort(400);\n }\n}\n","import type { Resolve } from \"@liveblocks/core\";\nimport type { Decoder } from \"decoders\";\nimport type {\n ComposeLeft,\n Objects,\n Pipe,\n Strings,\n Tuples,\n Unions,\n} from \"hotscript\";\n\nimport { raise } from \"./utils.js\";\n\nconst cleanSegmentRe = /^[\\w-]+$/;\nconst identifierRe = /^[a-z]\\w*$/;\nconst pathPrefixRegex = /^\\/(([\\w-]+|<[\\w-]+>)\\/)*\\*$/;\n\nexport type Method = (typeof ALL_METHODS)[number];\nexport type HttpVerb = (typeof ALL_HTTP_VERBS)[number];\n\n// All supported HTTP verbs, in their most natural ordering\nexport const ALL_HTTP_VERBS = [\n \"GET\",\n \"POST\",\n \"PUT\",\n \"PATCH\",\n \"DELETE\",\n \"OPTIONS\",\n];\n\nexport function sortHttpVerbsInPlace(verbs: HttpVerb[]): HttpVerb[] {\n return verbs.sort(\n (a, b) => ALL_HTTP_VERBS.indexOf(a) - ALL_HTTP_VERBS.indexOf(b)\n );\n}\n\n//\n// Subset of ALL_HTTP_VERBS, but OPTIONS is not included. This is because Zen\n// Router will automatically allow OPTIONS for all registered routes, i.e. an\n// explicit OPTIONS definition like this is (currently) not allowed:\n//\n// router.route('OPTIONS /my/path', ...)\n//\nexport const ALL_METHODS = [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"] as const;\n\nexport type PathPattern = `/${string}`;\nexport type Pattern = `${Method} ${PathPattern}`;\nexport type PathPrefix = `/${string}/*` | \"/*\";\n\n/**\n * From a pattern like:\n *\n * 'GET /foo/<bar>/<qux>/baz'\n *\n * Extracts:\n *\n * { foo: string, bar: string }\n */\ntype ExtractParamsBasic<P extends Pattern> = Pipe<\n P, // ....................................... 'GET /foo/<bar>/<qux>/baz'\n [\n Strings.TrimLeft<`${Method} `>, // ........ '/foo/<bar>/<qux>/baz'\n Strings.Split<\"/\">, // .................... ['', 'foo', '<bar>', '<qux>', 'baz']\n Tuples.Filter<Strings.StartsWith<\"<\">>, // ['<bar>', '<qux>']\n Tuples.Map<\n ComposeLeft<\n [\n Strings.Trim<\"<\" | \">\">, // ......... ['bar', 'qux']\n Unions.ToTuple, // .................. [['bar'], ['qux']]\n Tuples.Append<string>, // ........... [['bar', string], ['qux', string]]\n ]\n >\n >,\n Tuples.ToUnion, // ........................ ['bar', string] | ['qux', string]\n Objects.FromEntries, // ................... { bar: string; qux: string }\n ]\n>;\n\n/**\n * For:\n *\n * {\n * a: Decoder<number>,\n * b: Decoder<'hi'>,\n * c: Decoder<boolean>,\n * }\n *\n * Will return:\n *\n * {\n * a: number,\n * b: 'hi',\n * c: boolean,\n * }\n *\n */\nexport type MapDecoderTypes<T> = {\n [K in keyof T]: T[K] extends Decoder<infer V> ? V : never;\n};\n\n// export type WithDefaults<A, B> = Pipe<>;\n\n/**\n * From a pattern like:\n *\n * 'GET /foo/<bar>/<n>/baz'\n *\n * Extracts:\n *\n * { foo: string, n: number }\n */\nexport type ExtractParams<\n P extends Pattern,\n TParamTypes extends Record<string, unknown>,\n E = ExtractParamsBasic<P>,\n> = Resolve<\n Pick<Omit<E, keyof TParamTypes> & TParamTypes, Extract<keyof E, string>>\n>;\n\nconst ALL: Method[] = [\"GET\", \"POST\", \"PATCH\", \"PUT\", \"DELETE\"];\n\nexport interface RouteMatcher {\n method: Method;\n matchMethod(req: { method?: string }): boolean;\n matchURL(url: URL): Record<string, string> | null;\n}\n\nfunction segmentAsVariable(s: string): string | null {\n if (s.startsWith(\"<\") && s.endsWith(\">\")) {\n const identifier = s.slice(1, -1);\n return identifierRe.test(identifier) ? identifier : null;\n }\n return null;\n}\n\nfunction splitMethodAndPattern(\n pattern: string\n): [method: Method, pattern: string] {\n for (const method of ALL) {\n if (pattern.startsWith(method)) {\n return [method, pattern.slice(method.length).trimStart()];\n }\n }\n throw new Error(\n `Invalid route pattern: ${JSON.stringify(pattern)}${\n pattern.startsWith(\"/\")\n ? `. Did you mean ${JSON.stringify(`GET ${pattern}`)}?`\n : \"\"\n }`\n );\n}\n\nfunction makePathMatcher(pattern: string, options: { exact: boolean }): RegExp {\n const exact = options.exact;\n if (pattern === \"/\") {\n return exact ? /^\\/$/ : /^\\//;\n }\n\n if (!pattern.startsWith(\"/\")) {\n // istanbul ignore next -- @preserve\n throw new Error(\n `Route must start with '/', but got ${JSON.stringify(pattern)}`\n );\n }\n\n if (pattern.endsWith(\"/\")) {\n // istanbul ignore next -- @preserve\n throw new Error(\n `Route may not end with '/', but got ${JSON.stringify(pattern)}`\n );\n }\n\n const segments = pattern.slice(1).split(\"/\");\n\n let index = 1;\n const regexString: string[] = [];\n for (const segment of segments) {\n const placeholder = segmentAsVariable(segment);\n if (placeholder !== null) {\n regexString.push(`(?<${placeholder}>[^/]+)`);\n } else if (cleanSegmentRe.test(segment)) {\n regexString.push(segment);\n } else {\n return raise(`Invalid pattern: ${pattern} (error at position ${index + 1})`); // prettier-ignore\n }\n\n index += segment.length + 1;\n }\n\n return new RegExp(\"^/\" + regexString.join(\"/\") + (exact ? \"/?$\" : \"(/|$)\"));\n}\n\nexport function makePrefixPathMatcher(prefix: string): RegExp {\n pathPrefixRegex.test(prefix) || raise(`Invalid path prefix: ${prefix}`);\n prefix = prefix.slice(0, -2); // Remove the \"/*\" suffix\n prefix ||= \"/\"; // If the remaining prefix is \"\" (empty string), use a \"/\" instead\n\n // Register the prefix matcher\n return makePathMatcher(prefix, { exact: false });\n}\n\nexport function routeMatcher(input: string): RouteMatcher {\n const [method, pattern] = splitMethodAndPattern(input);\n const regex = makePathMatcher(pattern, { exact: true });\n return {\n method,\n matchMethod(req: Request): boolean {\n return method === req.method;\n },\n matchURL(url: URL) {\n const matches = url.pathname.match(regex);\n if (matches === null) {\n return null;\n }\n return matches.groups ?? {};\n },\n };\n}\n","const ctxs = new WeakMap<Request, unknown>();\n\nexport function lookupContext<C = unknown>(req: Request): C | undefined {\n return (ctxs as WeakMap<Request, C>).get(req);\n}\n\nexport function attachContext<C>(req: Request, ctx: C): C {\n ctxs.set(req, ctx);\n return ctx;\n}\n","import { ALL_HTTP_VERBS } from \"./lib/matchers.js\";\n\nexport type CorsOptions = {\n /**\n * Send CORS headers only if the requested Origin is in this hardcoded list\n * of origins. Note that the default is '*', but this still required all\n * incoming requests to have an Origin header set.\n *\n * @default '*' (allow any Origin)\n */\n allowedOrigins: \"*\" | string[];\n /**\n * When sending back CORS headers, tell the browser which methods are\n * allowed.\n *\n * @default All methods (you should likely not have to change this)\n */\n allowedMethods: string[];\n /**\n * Specify what headers are safe to allow on _incoming_ CORS requests.\n *\n * By default, all headers requested by the browser client will be allowed\n * (as most headers will typically be ignored by the endpoint handlers), but\n * you can specify a specific whitelist of allowed headers if you need to.\n *\n * Browsers will only ask for non-standard headers if those should be\n * allowed, i.e. a browser can ask in a preflight (OPTIONS) request, if it's\n * okay to send \"X-Test\", but won't ask if it's okay to send, say,\n * \"User-Agent\".\n *\n * Note that this is different from the `exposeHeaders` config:\n * - Allowed Headers: which headers a browser may include when\n * _making_ the CORS request\n * - Exposed Headers: which headers _returned_ in the CORS response the\n * browser is allowed to safely expose to scripts\n *\n * @default '*'\n */\n allowedHeaders: \"*\" | string[];\n /**\n * The Access-Control-Allow-Credentials response header allows browsers\n * to include include credentials in the next CORS request.\n *\n * Credentials are cookies, TLS client certificates, or WWW-Authentication\n * headers containing a username and password.\n *\n * NOTE: The `Authorization` header is *NOT* considered a credential and as\n * such you don’t need to enable this setting for sending such headers.\n *\n * NOTE: Allowing credentials alone doesn’t cause the browser to send those\n * credentials automatically. For to to happen, make sure to also add `{\n * credentials: \"include\" }` on the fetch request.\n *\n * WARNING: By default, these credentials are not sent in cross-origin\n * requests, and doing so can make a site vulnerable to CSRF attacks.\n *\n * @default false\n */\n allowCredentials: boolean;\n /**\n * Specify what headers browsers *scripts* can access from the CORS response.\n * This means when a client tries to programmatically read\n * `resp.headers.get('...')`, this header determines which headers will be\n * exposed to that client.\n *\n * Note that this is different from the `allowedHeaders` config:\n * - Allowed Headers: which headers a browser may include when\n * _making_ the CORS request\n * - Exposed Headers: which headers _returned_ in the CORS response the\n * browser is allowed to safely expose to scripts\n *\n * By default, browser scripts can only read the following headers from such\n * responses:\n * - Cache-Control\n * - Content-Language\n * - Content-Type\n * - Expires\n * - Last-Modified\n * - Pragma\n */\n exposeHeaders: string[];\n maxAge?: number;\n /**\n * When `allowedOrigins` isn't an explicit list of origins but '*' (= the\n * default), normally the Origin will get allowed by echoing the Origin value\n * back. When this option is set, it will instead allow '*'.\n *\n * Do not use this in combination with `allowCredentials` as this is not\n * allowed by the spec.\n *\n * @default false\n *\n */\n sendWildcard: boolean;\n /**\n * Always send CORS headers on all responses, even if the request didn't\n * contain an Origin header and thus isn't interested in CORS.\n *\n * @default true\n */\n alwaysSend: boolean;\n /**\n * Normally, when returning a CORS response, it's a good idea to set the\n * Vary header to include 'Origin', to behave better with caching. By default\n * this will be done. If you don't want to auto-add the Vary header, set this\n * to false.\n *\n * @default true\n */\n varyHeader: boolean;\n};\n\n// Maybe make some of these overridable? But for now keep these the defaults\nconst DEFAULT_CORS_OPTIONS: CorsOptions = {\n allowedOrigins: \"*\",\n allowedMethods: ALL_HTTP_VERBS,\n allowedHeaders: \"*\", // By default, allow all incoming headers (we'll ignore most of them anyway)\n allowCredentials: false,\n exposeHeaders: [],\n maxAge: undefined,\n sendWildcard: false,\n alwaysSend: true,\n varyHeader: true,\n};\n\n// Output Response Headers\nexport const AC_ORIGIN = \"Access-Control-Allow-Origin\";\nconst AC_METHODS = \"Access-Control-Allow-Methods\";\nconst AC_ALLOW_HEADERS = \"Access-Control-Allow-Headers\";\nconst AC_EXPOSE_HEADERS = \"Access-Control-Expose-Headers\";\nconst AC_CREDENTIALS = \"Access-Control-Allow-Credentials\";\nconst AC_MAX_AGE = \"Access-Control-Max-Age\";\n\n// Incoming Request Headers\nconst AC_REQUEST_METHOD = \"Access-Control-Request-Method\";\nconst AC_REQUEST_HEADERS = \"Access-Control-Request-Headers\";\n\n/**\n * Computes the value of the Access-Control-Allow-Origin header.\n * Either will be the Request's Origin header echoed back, or a \"*\".\n * Returns `null` if the response should not include any CORS headers.\n */\nfunction getCorsOrigin(options: CorsOptions, req: Request): string | null {\n if (options.sendWildcard && options.allowCredentials) {\n // This combination is not allowed by the spec\n throw new Error(\"Invalid CORS configuration\");\n }\n\n const allowAll = options.allowedOrigins === \"*\";\n const explicitOrigins = allowAll ? [] : (options.allowedOrigins as string[]);\n\n const origin =\n req.headers.get(\"Origin\") ??\n // --------------------------------------------------------------------------------\n // WARNING: Non-standard HTTP hack here!\n // --------------------------------------------------------------------------------\n // Note that X-Relay-Origin is not an HTTP standard! This is done, because the\n // default `fetch()` API will not allow you to manually set the Origin for\n // a request, as it's considered a forbidden header :(\n //\n // This custom header gets set here:\n // https://github.com/liveblocks/liveblocks.io/blob/862935833aa754cb419f2e5e8f7c32fb50e89de1/pages/api/public/authorize.ts#L69-L73\n // --------------------------------------------------------------------------------\n req.headers.get(\"X-Relay-Origin\");\n\n // If the Origin header is not present terminate this set of steps.\n // The request is outside the scope of this specification.-- W3Spec\n if (origin) {\n // If the allowed origins is an asterisk or 'wildcard', always match\n if (allowAll && options.sendWildcard) {\n return \"*\";\n } else if (allowAll || explicitOrigins.includes(origin)) {\n // Add a single Access-Control-Allow-Origin header, with either\n // the value of the Origin header or the string \"*\" as value.\n // -- W3Spec\n return origin;\n } else {\n // The request's Origin header does not match any of allowed origins, so\n // send no CORS-allowed headers back\n return null;\n }\n } else if (options.alwaysSend) {\n // Usually, if a request doesn’t include an Origin header, the client did\n // not request CORS. This means we can ignore this request. However, if\n // this is true, a most-likely-to-be-correct value is still set.\n if (allowAll) {\n // If wildcard is in the origins, even if `sendWildcard` is False,\n // simply send the wildcard. Unless supportsCredentials is True,\n // since that is forbidded by the spec..\n // It is the most-likely to be correct thing to do (the only other\n // option is to return nothing, which almost certainly not what\n // the developer wants if the '*' origin was specified.\n if (options.allowCredentials) {\n return null;\n } else {\n return \"*\";\n }\n } else {\n // Since there can be only one origin sent back, send back the first one\n // as a best-effort\n return explicitOrigins[0] ?? /* istanbul ignore next -- @preserve */ null;\n }\n } else {\n // The request did not contain an 'Origin' header. This means the browser or client did not request CORS, ensure the Origin Header is set\n return null;\n }\n}\n\nfunction getHeadersToAllow(allowed: \"*\" | string[], req: Request) {\n const requested = (req.headers.get(AC_REQUEST_HEADERS) ?? \"\")\n .toLowerCase()\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n\n const result =\n allowed === \"*\" ? requested : requested.filter((h) => allowed.includes(h));\n return result.length > 0 ? result : null;\n}\n\n/**\n * Returns CORS headers to attach to the Response for this request.\n *\n * For both preflight and non-preflight requests:\n * - Will set the AC-Allow-Origin header, echoing back the Origin\n * - Optionally, will set AC-Allow-Credentials and AC-Expose-Headers headers\n * (depending on your config)\n * - Set the Vary header accordingly\n *\n * For preflight-requests only:\n * - Will additionally set AC-Allow-Method and/or AC-Allow-Headers headers\n * (these don't have to be on the non-preflight requests)\n *\n * Returns `null` for non-CORS requests, or if CORS should not be allowed.\n */\nexport function getCorsHeaders(\n req: Request,\n opts: Partial<CorsOptions>\n): Headers | null {\n const options = { ...DEFAULT_CORS_OPTIONS, ...opts } as CorsOptions;\n const originToSet = getCorsOrigin(options, req);\n\n if (originToSet === null) {\n // CORS is not enabled for this route\n return null;\n }\n\n // Construct the CORS headers to put on the response\n // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin#syntax\n const headers: Headers = new Headers();\n headers.set(AC_ORIGIN, originToSet);\n if (options.exposeHeaders.length > 0) {\n headers.set(AC_EXPOSE_HEADERS, options.exposeHeaders.join(\", \"));\n }\n if (options.allowCredentials) {\n headers.set(AC_CREDENTIALS, \"true\"); // case-sensitive\n }\n\n // This is a preflight request\n // http://www.w3.org/TR/cors/#resource-preflight-requests\n if (req.method === \"OPTIONS\") {\n const requestedMethod = (\n req.headers.get(AC_REQUEST_METHOD) ?? \"\"\n ).toUpperCase();\n\n // If there is no Access-Control-Request-Method header or if parsing\n // failed, do not set any additional headers\n if (requestedMethod && options.allowedMethods.includes(requestedMethod)) {\n const headersToAllow = getHeadersToAllow(options.allowedHeaders, req);\n if (headersToAllow) {\n headers.set(AC_ALLOW_HEADERS, headersToAllow.join(\", \"));\n }\n if (options.maxAge) {\n headers.set(AC_MAX_AGE, String(options.maxAge));\n }\n // TODO Optionally, intersect resp.headers.get('Allow') with\n // options.allowedMethods, but it won’t matter much\n headers.set(AC_METHODS, options.allowedMethods.join(\", \"));\n } else {\n console.log(\n \"The request's Access-Control-Request-Method header does not match allowed methods. CORS headers will not be applied.\"\n );\n }\n }\n\n // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin#cors_and_caching\n if (options.varyHeader) {\n if (headers.get(AC_ORIGIN) === \"*\") {\n // Never set a Vary: Origin header if Origin is returned as \"*\"\n } else {\n headers.set(\"Vary\", \"Origin\");\n }\n }\n\n return headers;\n}\n","/* eslint-disable @typescript-eslint/no-unsafe-argument */\n/* eslint-disable @typescript-eslint/no-unsafe-assignment */\n\nimport { abort, HttpError } from \"~/responses/index.js\";\nimport { ZenRouter } from \"~/Router.js\";\n\nimport { lookupContext } from \"./contexts.js\";\nimport { ErrorHandler } from \"./ErrorHandler.js\";\nimport type { PathPrefix } from \"./lib/matchers.js\";\nimport { makePrefixPathMatcher } from \"./lib/matchers.js\";\n\ntype RequestHandler = (\n req: Request,\n ...args: readonly any[]\n) => Promise<Response>;\n\ntype RelayOptions = {\n errorHandler?: ErrorHandler;\n};\n\n/**\n * Relay won't do any route handling itself. It will only hand-off any incoming\n * request to one of the configured routers, based on the incoming request path\n * (first matching prefix path wins).\n *\n * It does NOT check the HTTP verb (GET, POST, etc).\n * It does NOT do any authentication.\n * It does NOT look at any headers.\n *\n * Subrouters (typically Router instances) are responsible for all that\n * themselves.\n *\n * If no matching route is found, it will return a generic 404 error response.\n */\nexport class ZenRelay {\n #_errorHandler: ErrorHandler;\n #_routers: [prefixMatcher: RegExp, handler: RequestHandler][] = [];\n\n constructor(options?: RelayOptions) {\n this.#_errorHandler = options?.errorHandler ?? new ErrorHandler();\n }\n\n public get fetch(): (\n req: Request,\n ...rest: readonly any[]\n ) => Promise<Response> {\n return this.#_tryDispatch.bind(this);\n }\n\n /**\n * If an incoming request matches the given prefix, forward the request as-is\n * to the child router. Relaying happens strictly based on the request URL.\n * It does not look at headers, or the HTTP method, or anything else to\n * decide if it's a match.\n */\n public relay(\n prefix: PathPrefix,\n router:\n | ZenRouter<any, any, any>\n //\n // NOTE: \"RequestHandler\" here is only allowed here to allow passing an\n // IttyRouter.handle instance here directly. Itty router is not built with\n // the same concepts as Zen Router in mind (for example, it can return\n // `undefined` instead of a Response to trigger a fallthrough). Overall,\n // it's better to remove this again once we're done refactoring away all\n // instances of Itty router.\n | RequestHandler\n ): this {\n const prefixMatcher = makePrefixPathMatcher(prefix);\n this.#_routers.push([\n prefixMatcher,\n router instanceof ZenRouter ? router.fetch : router,\n ]);\n return this; // Allow chaining\n }\n\n async #_tryDispatch(\n req: Request,\n ...args: readonly any[]\n ): Promise<Response> {\n try {\n return await this.#_dispatch(req, ...args);\n } catch (err) {\n if (!(err instanceof HttpError || err instanceof Response)) {\n // This case is definitely unexpected, it should never happen when\n // you're using only Relay or Router instances. However, it *can*\n // happen if the handler is a custom function (e.g. you're deferring to\n // itty-router), then this is not guaranteed.\n console.error(`Relayer caught error in subrouter! This should never happen, as routers should never throw an unexpected error! ${String(err)}`); // prettier-ignore\n }\n return this.#_errorHandler.handle(err, {\n req,\n ctx: lookupContext(req),\n });\n }\n }\n\n #_dispatch(req: Request, ...args: readonly any[]): Promise<Response> {\n const path = new URL(req.url).pathname;\n for (const [matcher, handler] of this.#_routers) {\n if (matcher.test(path)) {\n return handler(req, ...args);\n }\n }\n\n // console.warn(`Relayer did not know how to handle requested path: ${path}`);\n return abort(404);\n }\n}\n"],"mappings":"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","names":["raise","message","mapv","obj","mapper","rv","key","HttpError","_HttpError","status","message","headers","raise","ValidationError","reason","KB","ReadableStream_from_shim","iterable","iterator","controller","res","ReadableStream_from","imap","fn","x","genericAborts","isGenericAbort","resp","empty","json","value","status","headers","html","content","abort","encoder","buffered","size","buf","s","textStream","options","source","chunks","ndjsonStream","lines","jsonArrayStream","first","defaultHttpErrorHandler","e","json","ValidationError","defaultUncaughtErrorHandler","ErrorHandler","#_httpErrorHandler","#_uncaughtErrorHandler","handler","raise","err","extra","isGenericAbort","status","headers","HttpError","httpErrorHandler","context","trace","formatShort","cleanSegmentRe","identifierRe","pathPrefixRegex","ALL_HTTP_VERBS","sortHttpVerbsInPlace","verbs","a","b","ALL","segmentAsVariable","s","identifier","identifierRe","splitMethodAndPattern","pattern","method","makePathMatcher","options","exact","segments","index","regexString","segment","placeholder","cleanSegmentRe","raise","makePrefixPathMatcher","prefix","pathPrefixRegex","routeMatcher","input","regex","req","url","matches","ctxs","lookupContext","req","attachContext","ctx","DEFAULT_CORS_OPTIONS","ALL_HTTP_VERBS","AC_ORIGIN","AC_METHODS","AC_ALLOW_HEADERS","AC_EXPOSE_HEADERS","AC_CREDENTIALS","AC_MAX_AGE","AC_REQUEST_METHOD","AC_REQUEST_HEADERS","getCorsOrigin","options","req","allowAll","explicitOrigins","origin","getHeadersToAllow","allowed","requested","s","result","h","getCorsHeaders","opts","originToSet","headers","requestedMethod","headersToAllow","ZenRouter","#_debug","#_contextFn","#_defaultAuthFn","#_routes","#_paramDecoders","#_errorHandler","#_cors","options","ErrorHandler","abort","req","rest","resp","#_tryDispatch","#_addCorsIfNeeded","first","second","third","pattern","bodyDecoder","handler","#_register","#_getContext","args","lookupContext","attachContext","matcher","routeMatcher","wrap","#_dispatch","err","#_getAllowedVerbs","url","verbs","_","sortHttpVerbsInPlace","#_dispatch_OPTIONS","log","pathDidMatch","tup","authorize","match","span","trace","context","base","auth","p","mapv","value","key","decoder","decodeResult","tryReadBodyAsJson","errmsg","formatShort","ValidationError","input","raise","AC_ORIGIN","corsHeadersToAdd","getCorsHeaders","headers","k","v","status","body","result","json","text","ZenRelay","#_errorHandler","#_routers","options","ErrorHandler","#_tryDispatch","prefix","router","prefixMatcher","makePrefixPathMatcher","ZenRouter","req","args","#_dispatch","err","HttpError","lookupContext","path","matcher","handler","abort"]}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@liveblocks/zenrouter",
3
+ "version": "1.0.1",
4
+ "description": "An opinionated router library for building APIs following best practices.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "require": {
15
+ "types": "./dist/index.d.cts",
16
+ "module": "./dist/index.js",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist/**",
23
+ "README.md"
24
+ ],
25
+ "bugs": {
26
+ "url": "https://github.com/liveblocks/zenrouter/issues"
27
+ },
28
+ "scripts": {
29
+ "dev": "tsup --watch",
30
+ "build": "tsup",
31
+ "format": "eslint --color --fix src/ test/; prettier --write src/ test/",
32
+ "lint": "eslint --color src/ test/",
33
+ "lint:package": "publint --strict && attw --pack",
34
+ "test": "vitest run --color",
35
+ "test:watch": "vitest watch --color",
36
+ "test:types": "tsd"
37
+ },
38
+ "license": "Apache-2.0",
39
+ "devDependencies": {
40
+ "@liveblocks/eslint-config": "*",
41
+ "hotscript": "^1.0.13",
42
+ "nanoid": "^3"
43
+ },
44
+ "peerDependencies": {
45
+ "@opentelemetry/api": "^1.9.0",
46
+ "decoders": "^2.8.0-1"
47
+ },
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/liveblocks/zenrouter.git"
51
+ },
52
+ "sideEffects": false
53
+ }