@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 };