shelving 1.139.1 → 1.140.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/Resource.d.ts +57 -15
- package/api/Resource.js +31 -12
- package/api/index.d.ts +1 -0
- package/api/index.js +1 -0
- package/api/util.d.ts +33 -0
- package/api/util.js +65 -0
- package/error/ResponseError.d.ts +5 -0
- package/error/ResponseError.js +9 -0
- package/error/index.d.ts +1 -0
- package/error/index.js +1 -0
- package/package.json +1 -1
- package/util/array.js +1 -1
- package/util/color.js +1 -1
- package/util/entity.js +1 -1
- package/util/http.d.ts +89 -0
- package/util/http.js +130 -0
- package/util/index.d.ts +1 -0
- package/util/index.js +1 -0
- package/util/jwt.d.ts +39 -5
- package/util/jwt.js +65 -20
- package/util/string.js +1 -1
- package/util/template.d.ts +17 -9
- package/util/template.js +1 -1
- package/util/validate.d.ts +2 -2
- package/util/validate.js +2 -2
package/api/Resource.d.ts
CHANGED
|
@@ -1,26 +1,33 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AnyCaller } from "../error/BaseError.js";
|
|
2
|
+
import type { Path } from "../util/path.js";
|
|
3
|
+
import { type Validator } from "../util/validate.js";
|
|
4
|
+
/** Types for an HTTP request or response that does something. */
|
|
5
|
+
export type ResourceMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
6
|
+
/** A function that handles a resource request, with a payload and returns a result. */
|
|
7
|
+
export type ResourceCallback<P, R> = (payload: P, request: Request) => R | Promise<R>;
|
|
2
8
|
/**
|
|
3
|
-
* An abstract API resource definition, used to specify types for e.g. serverless functions
|
|
9
|
+
* An abstract API resource definition, used to specify types for e.g. serverless functions.
|
|
4
10
|
*
|
|
5
|
-
* @param
|
|
6
|
-
* @param
|
|
7
|
-
* @param
|
|
11
|
+
* @param method The method of the resource, e.g. `GET`
|
|
12
|
+
* @param path The path of the resource optionally including `{placeholder}` values, e.g. `/patient/{id}`
|
|
13
|
+
* @param payload A `Validator` for the payload of the resource.
|
|
14
|
+
* @param result A `Validator` for the result of the resource.
|
|
8
15
|
*/
|
|
9
|
-
export declare class Resource<P
|
|
10
|
-
/** Resource
|
|
11
|
-
readonly
|
|
16
|
+
export declare class Resource<P, R> implements Validator<R> {
|
|
17
|
+
/** Resource method. */
|
|
18
|
+
readonly method: ResourceMethod;
|
|
19
|
+
/** Resource path, e.g. `/patient/{id}` */
|
|
20
|
+
readonly path: Path;
|
|
12
21
|
/** Payload validator. */
|
|
13
22
|
readonly payload: Validator<P>;
|
|
14
23
|
/** Result validator. */
|
|
15
24
|
readonly result: Validator<R>;
|
|
16
|
-
constructor(
|
|
17
|
-
constructor(name: string, payload: Validator<P>);
|
|
18
|
-
constructor(name: string);
|
|
25
|
+
constructor(method: ResourceMethod, path: Path, payload: Validator<P>, result: Validator<R>);
|
|
19
26
|
/**
|
|
20
27
|
* Validate a payload for this resource.
|
|
21
28
|
*
|
|
22
29
|
* @returns The validated payload for this resource.
|
|
23
|
-
* @throws Feedback if the payload
|
|
30
|
+
* @throws `Feedback` if the payload is invalid. `Feedback` instances can be reported safely back to the end client so they know how to fix their request.
|
|
24
31
|
*/
|
|
25
32
|
prepare(unsafePayload: unknown): P;
|
|
26
33
|
/**
|
|
@@ -29,9 +36,44 @@ export declare class Resource<P = undefined, R = void> implements Validator<R> {
|
|
|
29
36
|
* @returns The validated result for this resource.
|
|
30
37
|
* @throws ValueError if the result could not be validated.
|
|
31
38
|
*/
|
|
32
|
-
validate(unsafeResult: unknown): R;
|
|
39
|
+
validate(unsafeResult: unknown, caller?: AnyCaller): R;
|
|
33
40
|
}
|
|
34
41
|
/** Extract the payload type from a `Resource`. */
|
|
35
|
-
export type PayloadType<X extends Resource
|
|
42
|
+
export type PayloadType<X extends Resource<unknown, unknown>> = X extends Resource<infer Y, unknown> ? Y : never;
|
|
36
43
|
/** Extract the result type from a `Resource`. */
|
|
37
|
-
export type ResourceType<X extends Resource
|
|
44
|
+
export type ResourceType<X extends Resource<unknown, unknown>> = X extends Resource<unknown, infer Y> ? Y : never;
|
|
45
|
+
/**
|
|
46
|
+
* Represent a GET request to a specified path, with validated payload and return types.
|
|
47
|
+
* "The GET method requests a representation of the specified resource. Requests using GET should only retrieve data and should not contain a request content."
|
|
48
|
+
*/
|
|
49
|
+
export declare function GET<P, R>(path: Path, payload?: Validator<P>, result?: Validator<R>): Resource<P, R>;
|
|
50
|
+
export declare function GET<P>(path: Path, payload: Validator<P>): Resource<P, undefined>;
|
|
51
|
+
export declare function GET<R>(path: Path, payload: undefined, result: Validator<R>): Resource<undefined, R>;
|
|
52
|
+
/**
|
|
53
|
+
* Represent a POST request to a specified path, with validated payload and return types.
|
|
54
|
+
* "The POST method submits an entity to the specified resource, often causing a change in state or side effects on the server.
|
|
55
|
+
*/
|
|
56
|
+
export declare function POST<P, R>(path: Path, payload?: Validator<P>, result?: Validator<R>): Resource<P, R>;
|
|
57
|
+
export declare function POST<P>(path: Path, payload: Validator<P>): Resource<P, undefined>;
|
|
58
|
+
export declare function POST<R>(path: Path, payload: undefined, result: Validator<R>): Resource<undefined, R>;
|
|
59
|
+
/**
|
|
60
|
+
* Represent a PUT request to a specified path, with validated payload and return types.
|
|
61
|
+
* "The PUT method replaces all current representations of the target resource with the request content."
|
|
62
|
+
*/
|
|
63
|
+
export declare function PUT<P, R>(path: Path, payload?: Validator<P>, result?: Validator<R>): Resource<P, R>;
|
|
64
|
+
export declare function PUT<P>(path: Path, payload: Validator<P>): Resource<P, undefined>;
|
|
65
|
+
export declare function PUT<R>(path: Path, payload: undefined, result: Validator<R>): Resource<undefined, R>;
|
|
66
|
+
/**
|
|
67
|
+
* Represent a PATCH request to a specified path, with validated payload and return types.
|
|
68
|
+
* "The PATCH method applies partial modifications to a resource."
|
|
69
|
+
*/
|
|
70
|
+
export declare function PATCH<P, R>(path: Path, payload?: Validator<P>, result?: Validator<R>): Resource<P, R>;
|
|
71
|
+
export declare function PATCH<P>(path: Path, payload: Validator<P>): Resource<P, undefined>;
|
|
72
|
+
export declare function PATCH<R>(path: Path, payload: undefined, result: Validator<R>): Resource<undefined, R>;
|
|
73
|
+
/**
|
|
74
|
+
* Represent a DELETE request to a specified path, with validated payload and return types.
|
|
75
|
+
* "The DELETE method deletes the specified resource."
|
|
76
|
+
*/
|
|
77
|
+
export declare function DELETE<P, R>(path: Path, payload?: Validator<P>, result?: Validator<R>): Resource<P, R>;
|
|
78
|
+
export declare function DELETE<P>(path: Path, payload: Validator<P>): Resource<P, undefined>;
|
|
79
|
+
export declare function DELETE<R>(path: Path, payload: undefined, result: Validator<R>): Resource<undefined, R>;
|
package/api/Resource.js
CHANGED
|
@@ -2,21 +2,25 @@ import { ValueError } from "../error/ValueError.js";
|
|
|
2
2
|
import { Feedback } from "../feedback/Feedback.js";
|
|
3
3
|
import { UNDEFINED } from "../util/validate.js";
|
|
4
4
|
/**
|
|
5
|
-
* An abstract API resource definition, used to specify types for e.g. serverless functions
|
|
5
|
+
* An abstract API resource definition, used to specify types for e.g. serverless functions.
|
|
6
6
|
*
|
|
7
|
-
* @param
|
|
8
|
-
* @param
|
|
9
|
-
* @param
|
|
7
|
+
* @param method The method of the resource, e.g. `GET`
|
|
8
|
+
* @param path The path of the resource optionally including `{placeholder}` values, e.g. `/patient/{id}`
|
|
9
|
+
* @param payload A `Validator` for the payload of the resource.
|
|
10
|
+
* @param result A `Validator` for the result of the resource.
|
|
10
11
|
*/
|
|
11
12
|
export class Resource {
|
|
12
|
-
/** Resource
|
|
13
|
-
|
|
13
|
+
/** Resource method. */
|
|
14
|
+
method;
|
|
15
|
+
/** Resource path, e.g. `/patient/{id}` */
|
|
16
|
+
path;
|
|
14
17
|
/** Payload validator. */
|
|
15
18
|
payload;
|
|
16
19
|
/** Result validator. */
|
|
17
20
|
result;
|
|
18
|
-
constructor(
|
|
19
|
-
this.
|
|
21
|
+
constructor(method, path, payload, result) {
|
|
22
|
+
this.method = method;
|
|
23
|
+
this.path = path;
|
|
20
24
|
this.payload = payload;
|
|
21
25
|
this.result = result;
|
|
22
26
|
}
|
|
@@ -24,10 +28,10 @@ export class Resource {
|
|
|
24
28
|
* Validate a payload for this resource.
|
|
25
29
|
*
|
|
26
30
|
* @returns The validated payload for this resource.
|
|
27
|
-
* @throws Feedback if the payload
|
|
31
|
+
* @throws `Feedback` if the payload is invalid. `Feedback` instances can be reported safely back to the end client so they know how to fix their request.
|
|
28
32
|
*/
|
|
29
33
|
prepare(unsafePayload) {
|
|
30
|
-
return this.payload
|
|
34
|
+
return this.payload.validate(unsafePayload);
|
|
31
35
|
}
|
|
32
36
|
/**
|
|
33
37
|
* Validate a result for this resource.
|
|
@@ -35,14 +39,29 @@ export class Resource {
|
|
|
35
39
|
* @returns The validated result for this resource.
|
|
36
40
|
* @throws ValueError if the result could not be validated.
|
|
37
41
|
*/
|
|
38
|
-
validate(unsafeResult) {
|
|
42
|
+
validate(unsafeResult, caller = this.validate) {
|
|
39
43
|
try {
|
|
40
44
|
return this.result.validate(unsafeResult);
|
|
41
45
|
}
|
|
42
46
|
catch (thrown) {
|
|
43
47
|
if (thrown instanceof Feedback)
|
|
44
|
-
throw new ValueError(`Invalid result for resource "${this.
|
|
48
|
+
throw new ValueError(`Invalid result for resource "${this.path}"`, { received: unsafeResult, caller });
|
|
45
49
|
throw thrown;
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
52
|
}
|
|
53
|
+
export function GET(path, payload = UNDEFINED, result = UNDEFINED) {
|
|
54
|
+
return new Resource("GET", path, payload, result);
|
|
55
|
+
}
|
|
56
|
+
export function POST(path, payload = UNDEFINED, result = UNDEFINED) {
|
|
57
|
+
return new Resource("POST", path, payload, result);
|
|
58
|
+
}
|
|
59
|
+
export function PUT(path, payload = UNDEFINED, result = UNDEFINED) {
|
|
60
|
+
return new Resource("PUT", path, payload, result);
|
|
61
|
+
}
|
|
62
|
+
export function PATCH(path, payload = UNDEFINED, result = UNDEFINED) {
|
|
63
|
+
return new Resource("PATCH", path, payload, result);
|
|
64
|
+
}
|
|
65
|
+
export function DELETE(path, payload = UNDEFINED, result = UNDEFINED) {
|
|
66
|
+
return new Resource("DELETE", path, payload, result);
|
|
67
|
+
}
|
package/api/index.d.ts
CHANGED
package/api/index.js
CHANGED
package/api/util.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type OptionalHandler } from "../util/http.js";
|
|
2
|
+
import type { Resource, ResourceCallback } from "./Resource.js";
|
|
3
|
+
/**
|
|
4
|
+
* Create a handler that matches an HTTP `Request` against a `Resource`, and returns a `Response` (possibly async) if the request matches, or `undefined` otherwise.
|
|
5
|
+
*
|
|
6
|
+
* @param resource A `Resource` instance that defines the method, path, and allowed types for the request.
|
|
7
|
+
*
|
|
8
|
+
* @param callback A callback function that will be called with the validated payload and should return the correct response type for the resource.
|
|
9
|
+
* > @param payload is the parse body content of the request.
|
|
10
|
+
* > - If the body is parsed as a `Data` object, also adds in path `{placeholders}` and `?query=` parameters in the URL.
|
|
11
|
+
*
|
|
12
|
+
* @returns An `OptionalHandler` that might handle a given `Request` if it matches the resource's method and path.
|
|
13
|
+
* > @returns `undefined` if the request does not match the resource's method or path.
|
|
14
|
+
* > @returns `Response` (possibly async) if the callback returns a valid response.
|
|
15
|
+
* > @throws `RequestError` if the request matches but the payload from the request is invalid (this is an end user error in the request that needs to be reported back).
|
|
16
|
+
* > @throws `ValueError` if the request matches but the value returned by the callback is invalid (this is more likely internal developer error that is reporting something wrong in the callback implementation).
|
|
17
|
+
*/
|
|
18
|
+
export declare function createResourceHandler<P, R>(resource: Resource<P, R>, callback: ResourceCallback<P, R>): OptionalHandler;
|
|
19
|
+
/**
|
|
20
|
+
* Handle a `Request` to a `Resource` using a `ResourceCallback` that implements the logic for the resource.
|
|
21
|
+
*
|
|
22
|
+
* @param request The `Request` to handle, which may have a method and URL that match the resource's method and path.
|
|
23
|
+
* @param resource A `Resource` instance that defines the method, path, and allowed types for the request.
|
|
24
|
+
* @param callback A callback function that will be called with the validated payload and should return the correct response type for the resource.
|
|
25
|
+
* > @param payload is the parse body content of the request.
|
|
26
|
+
* > - If the body is parsed as a `Data` object, also adds in path `{placeholders}` and `?query=` parameters in the URL.
|
|
27
|
+
*
|
|
28
|
+
* @returns `undefined` if the request does not match the resource's method or path.
|
|
29
|
+
* @returns `Response` (possibly async) if the callback returns a valid response.
|
|
30
|
+
* @throws `Feedback` if the payload the client user provided is invalid. `Feedback` instances can be reported safely back to the end client so they know how to fix their request.
|
|
31
|
+
* @throws `ValueError` if the request matches but the value returned by the callback is invalid (this is an internal error that is reporting something wrong in the callback implementation).
|
|
32
|
+
*/
|
|
33
|
+
export declare function handleResourceRequest<P, R>(request: Request, resource: Resource<P, R>, callback: ResourceCallback<P, R>): Response | Promise<Response> | undefined;
|
package/api/util.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { RequestError } from "../error/RequestError.js";
|
|
2
|
+
import { getRequestData } from "../util/http.js";
|
|
3
|
+
import { matchTemplate } from "../util/template.js";
|
|
4
|
+
import { getURL } from "../util/url.js";
|
|
5
|
+
/**
|
|
6
|
+
* Create a handler that matches an HTTP `Request` against a `Resource`, and returns a `Response` (possibly async) if the request matches, or `undefined` otherwise.
|
|
7
|
+
*
|
|
8
|
+
* @param resource A `Resource` instance that defines the method, path, and allowed types for the request.
|
|
9
|
+
*
|
|
10
|
+
* @param callback A callback function that will be called with the validated payload and should return the correct response type for the resource.
|
|
11
|
+
* > @param payload is the parse body content of the request.
|
|
12
|
+
* > - If the body is parsed as a `Data` object, also adds in path `{placeholders}` and `?query=` parameters in the URL.
|
|
13
|
+
*
|
|
14
|
+
* @returns An `OptionalHandler` that might handle a given `Request` if it matches the resource's method and path.
|
|
15
|
+
* > @returns `undefined` if the request does not match the resource's method or path.
|
|
16
|
+
* > @returns `Response` (possibly async) if the callback returns a valid response.
|
|
17
|
+
* > @throws `RequestError` if the request matches but the payload from the request is invalid (this is an end user error in the request that needs to be reported back).
|
|
18
|
+
* > @throws `ValueError` if the request matches but the value returned by the callback is invalid (this is more likely internal developer error that is reporting something wrong in the callback implementation).
|
|
19
|
+
*/
|
|
20
|
+
export function createResourceHandler(resource, callback) {
|
|
21
|
+
return request => handleResourceRequest(request, resource, callback);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Handle a `Request` to a `Resource` using a `ResourceCallback` that implements the logic for the resource.
|
|
25
|
+
*
|
|
26
|
+
* @param request The `Request` to handle, which may have a method and URL that match the resource's method and path.
|
|
27
|
+
* @param resource A `Resource` instance that defines the method, path, and allowed types for the request.
|
|
28
|
+
* @param callback A callback function that will be called with the validated payload and should return the correct response type for the resource.
|
|
29
|
+
* > @param payload is the parse body content of the request.
|
|
30
|
+
* > - If the body is parsed as a `Data` object, also adds in path `{placeholders}` and `?query=` parameters in the URL.
|
|
31
|
+
*
|
|
32
|
+
* @returns `undefined` if the request does not match the resource's method or path.
|
|
33
|
+
* @returns `Response` (possibly async) if the callback returns a valid response.
|
|
34
|
+
* @throws `Feedback` if the payload the client user provided is invalid. `Feedback` instances can be reported safely back to the end client so they know how to fix their request.
|
|
35
|
+
* @throws `ValueError` if the request matches but the value returned by the callback is invalid (this is an internal error that is reporting something wrong in the callback implementation).
|
|
36
|
+
*/
|
|
37
|
+
export function handleResourceRequest(request, resource, callback) {
|
|
38
|
+
// Ensure the request method e.g. `GET`, does not match the resource method e.g. `POST`
|
|
39
|
+
if (request.method !== resource.method)
|
|
40
|
+
return undefined;
|
|
41
|
+
// Parse the URL of the request.
|
|
42
|
+
const url = getURL(request.url);
|
|
43
|
+
if (!url)
|
|
44
|
+
throw new RequestError("Invalid request URL", { received: request.url, caller: handleResourceRequest });
|
|
45
|
+
// Ensure the request URL e.g. `/user/123` matches the resource path e.g. `/user/{id}`
|
|
46
|
+
// Any `{placeholders}` in the resource path are matched against the request URL to extract parameters.
|
|
47
|
+
const params = matchTemplate(url.pathname, resource.path);
|
|
48
|
+
if (!params)
|
|
49
|
+
return undefined;
|
|
50
|
+
// Extract a data object from the request body and validate it against the resource's payload type.
|
|
51
|
+
const data = getRequestData(request);
|
|
52
|
+
const payload = resource.prepare({ ...params, ...data });
|
|
53
|
+
// Return the `Response` that results from calling the callback with the `Request` and the matching params.
|
|
54
|
+
return _getHandlerResponse(resource, callback, payload, request);
|
|
55
|
+
}
|
|
56
|
+
/** Internal async function that calls `callback()` asyncronously and validates the result. */
|
|
57
|
+
async function _getHandlerResponse(resource, callback, payload, request) {
|
|
58
|
+
// Call the handler with the validated payload to get the result.
|
|
59
|
+
const unsafeResult = await callback(payload, request);
|
|
60
|
+
// Validate the result against the resource's result type.
|
|
61
|
+
// Throw a `ValueError` if the result is not valid, which indicates an internal error in the callback implementation.
|
|
62
|
+
const result = resource.validate(unsafeResult, _getHandlerResponse);
|
|
63
|
+
// Return a new `Response` with a 200 status and the validated result data.
|
|
64
|
+
return Response.json(result);
|
|
65
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { BaseError } from "./BaseError.js";
|
|
2
|
+
/** Error thrown when an HTTP response isn't well-formed. */
|
|
3
|
+
export class ResponseError extends BaseError {
|
|
4
|
+
constructor(message = ResponseError.prototype.message, options) {
|
|
5
|
+
super(message, { caller: ResponseError, ...options });
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
ResponseError.prototype.name = "ResponseError";
|
|
9
|
+
ResponseError.prototype.message = "Invalid response";
|
package/error/index.d.ts
CHANGED
package/error/index.js
CHANGED
package/package.json
CHANGED
package/util/array.js
CHANGED
|
@@ -137,7 +137,7 @@ export function getLast(items) {
|
|
|
137
137
|
return last;
|
|
138
138
|
}
|
|
139
139
|
/** Get the last item from an array or iterable. */
|
|
140
|
-
export function requireLast(items, caller =
|
|
140
|
+
export function requireLast(items, caller = requireLast) {
|
|
141
141
|
const item = getLast(items);
|
|
142
142
|
if (item === undefined)
|
|
143
143
|
throw new RequiredError("Last item is required", { items, caller });
|
package/util/color.js
CHANGED
|
@@ -80,6 +80,6 @@ export function getColor(value) {
|
|
|
80
80
|
/** Convert a possible color to a `Color` instance, or throw `RequiredError` if it can't be converted. */
|
|
81
81
|
export function requireColor(value, caller = requireColor) {
|
|
82
82
|
const color = getColor(value);
|
|
83
|
-
assertColor(color,
|
|
83
|
+
assertColor(color, caller);
|
|
84
84
|
return color;
|
|
85
85
|
}
|
package/util/entity.js
CHANGED
|
@@ -13,5 +13,5 @@ export function requireEntity(entity, caller = requireEntity) {
|
|
|
13
13
|
const bits = entity.split(":", 2);
|
|
14
14
|
if (bits[0] && bits[1])
|
|
15
15
|
return bits;
|
|
16
|
-
throw new RequiredError("Invalid entity", { received: entity, caller
|
|
16
|
+
throw new RequiredError("Invalid entity", { received: entity, caller });
|
|
17
17
|
}
|
package/util/http.d.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { AnyCaller } from "../error/BaseError.js";
|
|
2
|
+
import { RequestError } from "../error/RequestError.js";
|
|
3
|
+
import { ResponseError } from "../error/ResponseError.js";
|
|
4
|
+
import type { ImmutableArray } from "./array.js";
|
|
5
|
+
import { type Data } from "./data.js";
|
|
6
|
+
import type { Optional } from "./optional.js";
|
|
7
|
+
/** A handler function takes a `Request` and returns a `Response` (possibly asynchronously). */
|
|
8
|
+
export type Handler = (request: Request) => Response | Promise<Response>;
|
|
9
|
+
/** An optional handler function _may_ match a `Request` and return a `Response`, or may return `undefined` if it doesn't match. */
|
|
10
|
+
export type OptionalHandler = (request: Request) => Optional<Response | Promise<Response>>;
|
|
11
|
+
/** A list of `OptionalHandler` functions that may match a `Request` */
|
|
12
|
+
export type Handlers = ImmutableArray<OptionalHandler>;
|
|
13
|
+
export declare function _getMessageJSON(message: Request | Response, MessageError: typeof RequestError | typeof ResponseError, caller: AnyCaller): Promise<unknown>;
|
|
14
|
+
export declare function _getMessageData(message: Request | Response, MessageError: typeof RequestError | typeof ResponseError, caller: AnyCaller): Promise<Data | undefined>;
|
|
15
|
+
export declare function _requireMessageData(message: Request | Response, MessageError: typeof RequestError | typeof ResponseError, caller: AnyCaller): Promise<Data>;
|
|
16
|
+
export declare function _getMessageContent(message: Request | Response, MessageError: typeof RequestError | typeof ResponseError, caller: AnyCaller): Promise<unknown>;
|
|
17
|
+
/**
|
|
18
|
+
* Parse the content of an HTTP `Request` based as JSON, or throw `RequestError` if the content could not be getd.
|
|
19
|
+
*
|
|
20
|
+
* @returns string (if the content type is `text/plain`)
|
|
21
|
+
* @returns Data If the content can be parsed as a JSON object, or `undefined` if the content was empty.
|
|
22
|
+
* @throws RequestError if the content is not `text/plain` or `application/json` with valid JSON.
|
|
23
|
+
*/
|
|
24
|
+
export declare function getRequestJSON(message: Request): Promise<unknown>;
|
|
25
|
+
/**
|
|
26
|
+
* Parse the content of an HTTP `Response` based as JSON, or throw `ResponseError` if the content could not be getd.
|
|
27
|
+
*
|
|
28
|
+
* @returns string (if the content type is `text/plain`)
|
|
29
|
+
* @returns Data If the content can be parsed as a JSON object, or `undefined` if the content was empty.
|
|
30
|
+
* @throws ResponseError if the content is not `text/plain` or `application/json` with valid JSON.
|
|
31
|
+
*/
|
|
32
|
+
export declare function getResponseJSON(message: Response): Promise<unknown>;
|
|
33
|
+
/**
|
|
34
|
+
* Require the content of an HTTP `Request` as a data object in JSON format, or throw `RequestError` if the content cannot not be parsed or is not a data object in JSON format.
|
|
35
|
+
*
|
|
36
|
+
* @returns string (if the content type is `text/plain`)
|
|
37
|
+
* @returns Data If the content can be parsed as a JSON object, or `undefined` if the content was empty.
|
|
38
|
+
* @throws RequestError if the content is not `text/plain` or `application/json` with valid JSON.
|
|
39
|
+
*/
|
|
40
|
+
export declare function getRequestData(message: Request): Promise<Data | undefined>;
|
|
41
|
+
/**
|
|
42
|
+
* Require the content of an HTTP `Response` as a data object in JSON format, or throw `ResponseError` if the content cannot not be parsed or is not a data object in JSON format.
|
|
43
|
+
*
|
|
44
|
+
* @returns string (if the content type is `text/plain`)
|
|
45
|
+
* @returns Data If the content can be parsed as a JSON object, or `undefined` if the content was empty.
|
|
46
|
+
* @throws ResponseError if the content is not `text/plain` or `application/json` with valid JSON.
|
|
47
|
+
*/
|
|
48
|
+
export declare function getResponseData(message: Response): Promise<Data | undefined>;
|
|
49
|
+
/**
|
|
50
|
+
* Require the content of an HTTP `Request` as a data object in JSON format, or throw `RequestError` if the content cannot not be parsed or is not a data object in JSON format.
|
|
51
|
+
*
|
|
52
|
+
* @returns string (if the content type is `text/plain`)
|
|
53
|
+
* @returns Data If the content can be parsed as a JSON object, or `undefined` if the content was empty.
|
|
54
|
+
* @throws RequestError if the content is not `application/json` with valid JSON that parses as a Data object.
|
|
55
|
+
*/
|
|
56
|
+
export declare function requireRequestData(message: Request): Promise<Data>;
|
|
57
|
+
/**
|
|
58
|
+
* Require the content of an HTTP `Response` as a data object in JSON format, or throw `ResponseError` if the content cannot not be parsed or is not a data object in JSON format.
|
|
59
|
+
*
|
|
60
|
+
* @returns string (if the content type is `text/plain`)
|
|
61
|
+
* @returns Data If the content can be parsed as a JSON object, or `undefined` if the content was empty.
|
|
62
|
+
* @throws ResponseError if the content is not `application/json` with valid JSON that parses as a Data object.
|
|
63
|
+
*/
|
|
64
|
+
export declare function requireResponseData(message: Response): Promise<Data>;
|
|
65
|
+
/**
|
|
66
|
+
* Get the content of an HTTP `Request` based on its content type, or throw `RequestError` if the content could not be parsed.
|
|
67
|
+
*
|
|
68
|
+
* @returns string If content type is `text/plain` (including empty string if it's empty).
|
|
69
|
+
* @returns unknown If content type is `application/JSON` and has valid JSON (including `undefined` if the content is empty).
|
|
70
|
+
*
|
|
71
|
+
* @throws RequestError if the content is not `text/plain`, or `application/json` with valid JSON.
|
|
72
|
+
*/
|
|
73
|
+
export declare function getRequestContent(message: Request): Promise<unknown>;
|
|
74
|
+
/**
|
|
75
|
+
* Get the content of an HTTP `Response` based on its content type, or throw `ResponseError` if the content could not be parsed.
|
|
76
|
+
*
|
|
77
|
+
* @returns string If content type is `text/plain` (including empty string if it's empty).
|
|
78
|
+
* @returns unknown If content type is `application/JSON` and has valid JSON (including `undefined` if the content is empty).
|
|
79
|
+
*
|
|
80
|
+
* @throws RequestError if the content is not `text/plain` or `application/json` with valid JSON.
|
|
81
|
+
*/
|
|
82
|
+
export declare function getResponseContent(message: Response): Promise<unknown>;
|
|
83
|
+
/**
|
|
84
|
+
* Match a `Request` against a `Handlers` array.
|
|
85
|
+
*
|
|
86
|
+
* @returns The resulting `Response` from the first handler that matches the `Request`.
|
|
87
|
+
* @throws `NotFoundError` if no handler matches the `Request`.
|
|
88
|
+
*/
|
|
89
|
+
export declare function handleRequest(request: Request, handlers: ImmutableArray<OptionalHandler>): Response | Promise<Response>;
|
package/util/http.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { NotFoundError, RequestError } from "../error/RequestError.js";
|
|
2
|
+
import { ResponseError } from "../error/ResponseError.js";
|
|
3
|
+
import { isData } from "./data.js";
|
|
4
|
+
export async function _getMessageJSON(message, MessageError, caller) {
|
|
5
|
+
const trimmed = (await message.text()).trim();
|
|
6
|
+
if (!trimmed.length)
|
|
7
|
+
return undefined;
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(trimmed);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
throw new MessageError("Body must be valid JSON", { received: trimmed, caller });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export async function _getMessageData(message, MessageError, caller) {
|
|
16
|
+
const data = await _getMessageJSON(message, MessageError, caller);
|
|
17
|
+
if (isData(data) || data === undefined)
|
|
18
|
+
return data;
|
|
19
|
+
throw new MessageError("Body must be data object or undefined", { received: data, caller });
|
|
20
|
+
}
|
|
21
|
+
export async function _requireMessageData(message, MessageError, caller) {
|
|
22
|
+
const data = await _getMessageJSON(message, MessageError, caller);
|
|
23
|
+
if (isData(data))
|
|
24
|
+
return data;
|
|
25
|
+
throw new MessageError("Body must be data object", { received: data, caller });
|
|
26
|
+
}
|
|
27
|
+
export function _getMessageContent(message, MessageError, caller) {
|
|
28
|
+
const type = message.headers.get("Content-Type");
|
|
29
|
+
if (type?.startsWith("application/json"))
|
|
30
|
+
return _getMessageJSON(message, MessageError, caller);
|
|
31
|
+
if (type?.startsWith("text/plain"))
|
|
32
|
+
return message.text();
|
|
33
|
+
throw new MessageError("Unexpected content type", { received: type, caller });
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Parse the content of an HTTP `Request` based as JSON, or throw `RequestError` if the content could not be getd.
|
|
37
|
+
*
|
|
38
|
+
* @returns string (if the content type is `text/plain`)
|
|
39
|
+
* @returns Data If the content can be parsed as a JSON object, or `undefined` if the content was empty.
|
|
40
|
+
* @throws RequestError if the content is not `text/plain` or `application/json` with valid JSON.
|
|
41
|
+
*/
|
|
42
|
+
export function getRequestJSON(message) {
|
|
43
|
+
return _getMessageJSON(message, RequestError, getRequestJSON);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Parse the content of an HTTP `Response` based as JSON, or throw `ResponseError` if the content could not be getd.
|
|
47
|
+
*
|
|
48
|
+
* @returns string (if the content type is `text/plain`)
|
|
49
|
+
* @returns Data If the content can be parsed as a JSON object, or `undefined` if the content was empty.
|
|
50
|
+
* @throws ResponseError if the content is not `text/plain` or `application/json` with valid JSON.
|
|
51
|
+
*/
|
|
52
|
+
export function getResponseJSON(message) {
|
|
53
|
+
return _getMessageJSON(message, ResponseError, getResponseJSON);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Require the content of an HTTP `Request` as a data object in JSON format, or throw `RequestError` if the content cannot not be parsed or is not a data object in JSON format.
|
|
57
|
+
*
|
|
58
|
+
* @returns string (if the content type is `text/plain`)
|
|
59
|
+
* @returns Data If the content can be parsed as a JSON object, or `undefined` if the content was empty.
|
|
60
|
+
* @throws RequestError if the content is not `text/plain` or `application/json` with valid JSON.
|
|
61
|
+
*/
|
|
62
|
+
export function getRequestData(message) {
|
|
63
|
+
return _getMessageData(message, RequestError, getRequestData);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Require the content of an HTTP `Response` as a data object in JSON format, or throw `ResponseError` if the content cannot not be parsed or is not a data object in JSON format.
|
|
67
|
+
*
|
|
68
|
+
* @returns string (if the content type is `text/plain`)
|
|
69
|
+
* @returns Data If the content can be parsed as a JSON object, or `undefined` if the content was empty.
|
|
70
|
+
* @throws ResponseError if the content is not `text/plain` or `application/json` with valid JSON.
|
|
71
|
+
*/
|
|
72
|
+
export function getResponseData(message) {
|
|
73
|
+
return _getMessageData(message, ResponseError, getResponseData);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Require the content of an HTTP `Request` as a data object in JSON format, or throw `RequestError` if the content cannot not be parsed or is not a data object in JSON format.
|
|
77
|
+
*
|
|
78
|
+
* @returns string (if the content type is `text/plain`)
|
|
79
|
+
* @returns Data If the content can be parsed as a JSON object, or `undefined` if the content was empty.
|
|
80
|
+
* @throws RequestError if the content is not `application/json` with valid JSON that parses as a Data object.
|
|
81
|
+
*/
|
|
82
|
+
export function requireRequestData(message) {
|
|
83
|
+
return _requireMessageData(message, RequestError, requireRequestData);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Require the content of an HTTP `Response` as a data object in JSON format, or throw `ResponseError` if the content cannot not be parsed or is not a data object in JSON format.
|
|
87
|
+
*
|
|
88
|
+
* @returns string (if the content type is `text/plain`)
|
|
89
|
+
* @returns Data If the content can be parsed as a JSON object, or `undefined` if the content was empty.
|
|
90
|
+
* @throws ResponseError if the content is not `application/json` with valid JSON that parses as a Data object.
|
|
91
|
+
*/
|
|
92
|
+
export function requireResponseData(message) {
|
|
93
|
+
return _requireMessageData(message, ResponseError, requireResponseData);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Get the content of an HTTP `Request` based on its content type, or throw `RequestError` if the content could not be parsed.
|
|
97
|
+
*
|
|
98
|
+
* @returns string If content type is `text/plain` (including empty string if it's empty).
|
|
99
|
+
* @returns unknown If content type is `application/JSON` and has valid JSON (including `undefined` if the content is empty).
|
|
100
|
+
*
|
|
101
|
+
* @throws RequestError if the content is not `text/plain`, or `application/json` with valid JSON.
|
|
102
|
+
*/
|
|
103
|
+
export function getRequestContent(message) {
|
|
104
|
+
return _getMessageContent(message, RequestError, getRequestContent);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Get the content of an HTTP `Response` based on its content type, or throw `ResponseError` if the content could not be parsed.
|
|
108
|
+
*
|
|
109
|
+
* @returns string If content type is `text/plain` (including empty string if it's empty).
|
|
110
|
+
* @returns unknown If content type is `application/JSON` and has valid JSON (including `undefined` if the content is empty).
|
|
111
|
+
*
|
|
112
|
+
* @throws RequestError if the content is not `text/plain` or `application/json` with valid JSON.
|
|
113
|
+
*/
|
|
114
|
+
export function getResponseContent(message) {
|
|
115
|
+
return _getMessageContent(message, ResponseError, getResponseContent);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Match a `Request` against a `Handlers` array.
|
|
119
|
+
*
|
|
120
|
+
* @returns The resulting `Response` from the first handler that matches the `Request`.
|
|
121
|
+
* @throws `NotFoundError` if no handler matches the `Request`.
|
|
122
|
+
*/
|
|
123
|
+
export function handleRequest(request, handlers) {
|
|
124
|
+
for (const handler of handlers) {
|
|
125
|
+
const response = handler(request);
|
|
126
|
+
if (response)
|
|
127
|
+
return response;
|
|
128
|
+
}
|
|
129
|
+
throw new NotFoundError("Not found", { request, caller: handleRequest });
|
|
130
|
+
}
|
package/util/index.d.ts
CHANGED
package/util/index.js
CHANGED
package/util/jwt.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import type { AnyCaller } from "../error/BaseError.js";
|
|
1
2
|
import { type PossibleBytes } from "./bytes.js";
|
|
2
3
|
import type { Data } from "./data.js";
|
|
3
|
-
import type { AnyFunction } from "./function.js";
|
|
4
4
|
/**
|
|
5
5
|
* Encode a JWT and return the string token.
|
|
6
6
|
* - Currently only supports HMAC SHA-512 signing.
|
|
@@ -20,14 +20,48 @@ export type TokenData = {
|
|
|
20
20
|
/**
|
|
21
21
|
* Split a JSON Web Token into its header, payload, and signature, and decode and parse the JSON.
|
|
22
22
|
*/
|
|
23
|
-
export declare function splitToken(token:
|
|
24
|
-
export declare function _splitToken(caller: AnyFunction, token: unknown): TokenData;
|
|
23
|
+
export declare function splitToken(token: string, caller?: AnyCaller): TokenData;
|
|
25
24
|
/**
|
|
26
25
|
* Decode a JWT, verify it, and return the full payload data.
|
|
27
26
|
* - Currently only supports HMAC SHA-512 signing.
|
|
28
27
|
*
|
|
29
28
|
* @throws ValueError If the input parameters, e.g. `secret` or `issuer`, are invalid.
|
|
30
|
-
* @throws
|
|
29
|
+
* @throws UnauthorizedError If the token is invalid or malformed.
|
|
31
30
|
* @throws UnauthorizedError If the token signature is incorrect, token is expired or not issued yet.
|
|
32
31
|
*/
|
|
33
|
-
export declare function verifyToken(token:
|
|
32
|
+
export declare function verifyToken(token: string, secret: PossibleBytes, caller?: AnyCaller): Promise<Data>;
|
|
33
|
+
/**
|
|
34
|
+
* Set the `Authorization: Bearer {token}` on a `Request` object (by reference).
|
|
35
|
+
*
|
|
36
|
+
* @param request The `Request` object to set the token on.
|
|
37
|
+
* @returns The same `Request` object that was passed in.
|
|
38
|
+
*/
|
|
39
|
+
export declare function setRequestToken(request: Request, token: string): Request;
|
|
40
|
+
/**
|
|
41
|
+
* Extract the `Authorization: Bearer {token}` from a `Request` object, or return `undefined` if not set.
|
|
42
|
+
*
|
|
43
|
+
* @param request The `Request` object possibly containing an `Authorization: Bearer {token}` header to extract the token from.
|
|
44
|
+
* @returns The string token extracted from the `Authorization` header, or `undefined` if not set.
|
|
45
|
+
*/
|
|
46
|
+
export declare function getRequestToken(request: Request): string | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Extract the `Authorization: Bearer {token}` from a `Request` object, or throw `UnauthorizedError` if not set or malformed.
|
|
49
|
+
*
|
|
50
|
+
* @param request The `Request` object containing an `Authorization: Bearer {token}` header to extract the token from.
|
|
51
|
+
* @returns The string token extracted from the `Authorization` header.
|
|
52
|
+
* @throws UnauthorizedError If the `Authorization` header is not set, or the JWT it contains is not well-formed.
|
|
53
|
+
*/
|
|
54
|
+
export declare function requireRequestToken(request: Request, caller?: AnyCaller): string;
|
|
55
|
+
/**
|
|
56
|
+
* Extract the `Authorization: Bearer {token}` from a `Request` object and verify it using a signature, or throw `UnauthorizedError` if not set, malformed, or invalid.
|
|
57
|
+
* - Same as doing `requireRequestToken(request)` and then `verifyToken(token, secret)`.
|
|
58
|
+
*
|
|
59
|
+
* @param request The `Request` object containing an `Authorization: Bearer {token}` header to extract the token from.
|
|
60
|
+
* @param secret The secret key to verify the JWT signature with.
|
|
61
|
+
*
|
|
62
|
+
* @returns The decoded payload data from the JWT.
|
|
63
|
+
* @throws UnauthorizedError If the `Authorization` header is not set, the JWT it contains is not well-formed, or the JWT signature is invalid.
|
|
64
|
+
*
|
|
65
|
+
* @example `const { sub, iss, customClaim } = await verifyRequestToken(request, secret);`
|
|
66
|
+
*/
|
|
67
|
+
export declare function verifyRequestToken(request: Request, secret: PossibleBytes, caller?: AnyCaller): Promise<Data>;
|
package/util/jwt.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { UnauthorizedError } from "../error/RequestError.js";
|
|
2
2
|
import { ValueError } from "../error/ValueError.js";
|
|
3
3
|
import { decodeBase64URLBytes, decodeBase64URLString, encodeBase64URL } from "./base64.js";
|
|
4
4
|
import { getBytes, requireBytes } from "./bytes.js";
|
|
@@ -41,23 +41,18 @@ export async function encodeToken(claims, secret) {
|
|
|
41
41
|
/**
|
|
42
42
|
* Split a JSON Web Token into its header, payload, and signature, and decode and parse the JSON.
|
|
43
43
|
*/
|
|
44
|
-
export function splitToken(token) {
|
|
45
|
-
return _splitToken(splitToken, token);
|
|
46
|
-
}
|
|
47
|
-
export function _splitToken(caller, token) {
|
|
48
|
-
if (typeof token !== "string")
|
|
49
|
-
throw new RequestError("JWT token must be string", { received: token, caller });
|
|
44
|
+
export function splitToken(token, caller = splitToken) {
|
|
50
45
|
// Split token.
|
|
51
46
|
const [header, payload, signature] = token.split(".");
|
|
52
47
|
if (!header || !payload || !signature)
|
|
53
|
-
throw new
|
|
48
|
+
throw new UnauthorizedError("JWT token must have header, payload, and signature", { received: token, caller });
|
|
54
49
|
// Decode signature.
|
|
55
50
|
let signatureBytes;
|
|
56
51
|
try {
|
|
57
52
|
signatureBytes = decodeBase64URLBytes(signature);
|
|
58
53
|
}
|
|
59
54
|
catch (cause) {
|
|
60
|
-
throw new
|
|
55
|
+
throw new UnauthorizedError("JWT token signature must be Base64URL encoded", { received: signature, cause, caller });
|
|
61
56
|
}
|
|
62
57
|
// Decode header.
|
|
63
58
|
let headerData;
|
|
@@ -65,7 +60,7 @@ export function _splitToken(caller, token) {
|
|
|
65
60
|
headerData = JSON.parse(decodeBase64URLString(header));
|
|
66
61
|
}
|
|
67
62
|
catch (cause) {
|
|
68
|
-
throw new
|
|
63
|
+
throw new UnauthorizedError("JWT token header must be Base64URL encoded JSON", { received: header, cause, caller });
|
|
69
64
|
}
|
|
70
65
|
// Decode payload.
|
|
71
66
|
let payloadData;
|
|
@@ -73,7 +68,7 @@ export function _splitToken(caller, token) {
|
|
|
73
68
|
payloadData = JSON.parse(decodeBase64URLString(payload));
|
|
74
69
|
}
|
|
75
70
|
catch (cause) {
|
|
76
|
-
throw new
|
|
71
|
+
throw new UnauthorizedError("JWT token payload must be Base64URL encoded JSON", { received: payload, cause, caller });
|
|
77
72
|
}
|
|
78
73
|
return { header, payload, signature, headerData, payloadData, signatureBytes };
|
|
79
74
|
}
|
|
@@ -82,29 +77,79 @@ export function _splitToken(caller, token) {
|
|
|
82
77
|
* - Currently only supports HMAC SHA-512 signing.
|
|
83
78
|
*
|
|
84
79
|
* @throws ValueError If the input parameters, e.g. `secret` or `issuer`, are invalid.
|
|
85
|
-
* @throws
|
|
80
|
+
* @throws UnauthorizedError If the token is invalid or malformed.
|
|
86
81
|
* @throws UnauthorizedError If the token signature is incorrect, token is expired or not issued yet.
|
|
87
82
|
*/
|
|
88
|
-
export async function verifyToken(token, secret) {
|
|
89
|
-
const { header, payload, signature, headerData, payloadData } =
|
|
83
|
+
export async function verifyToken(token, secret, caller = verifyToken) {
|
|
84
|
+
const { header, payload, signature, headerData, payloadData } = splitToken(token, caller);
|
|
90
85
|
// Validate header.
|
|
91
86
|
if (headerData.typ !== HEADER.typ)
|
|
92
|
-
throw new
|
|
87
|
+
throw new UnauthorizedError(`JWT header type must be \"${HEADER.typ}\"`, { received: headerData.typ, caller });
|
|
93
88
|
if (headerData.alg !== HEADER.alg)
|
|
94
|
-
throw new
|
|
89
|
+
throw new UnauthorizedError(`JWT header algorithm must be \"${HEADER.alg}\"`, { received: headerData.alg, caller });
|
|
95
90
|
// Validate signature.
|
|
96
91
|
const key = await _getKey(verifyToken, secret, "verify");
|
|
97
92
|
const isValid = await crypto.subtle.verify("HMAC", key, decodeBase64URLBytes(signature), requireBytes(`${header}.${payload}`));
|
|
98
93
|
if (!isValid)
|
|
99
|
-
throw new UnauthorizedError("JWT signature does not match", { received: token, caller
|
|
94
|
+
throw new UnauthorizedError("JWT signature does not match", { received: token, caller });
|
|
100
95
|
// Validate payload.
|
|
101
96
|
const { nbf, iat, exp } = payloadData;
|
|
102
97
|
const now = Math.floor(Date.now() / 1000);
|
|
103
98
|
if (typeof nbf === "number" && nbf < now - SKEW_MS)
|
|
104
|
-
throw new UnauthorizedError("JWT cannot be used yet", { received: payloadData, expected: now, caller
|
|
99
|
+
throw new UnauthorizedError("JWT cannot be used yet", { received: payloadData, expected: now, caller });
|
|
105
100
|
if (typeof iat === "number" && iat > now + SKEW_MS)
|
|
106
|
-
throw new UnauthorizedError("JWT not issued yet", { received: payloadData, expected: now, caller
|
|
101
|
+
throw new UnauthorizedError("JWT not issued yet", { received: payloadData, expected: now, caller });
|
|
107
102
|
if (typeof exp === "number" && exp < now - SKEW_MS)
|
|
108
|
-
throw new UnauthorizedError("JWT has expired", { received: payloadData, expected: now, caller
|
|
103
|
+
throw new UnauthorizedError("JWT has expired", { received: payloadData, expected: now, caller });
|
|
109
104
|
return payloadData;
|
|
110
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Set the `Authorization: Bearer {token}` on a `Request` object (by reference).
|
|
108
|
+
*
|
|
109
|
+
* @param request The `Request` object to set the token on.
|
|
110
|
+
* @returns The same `Request` object that was passed in.
|
|
111
|
+
*/
|
|
112
|
+
export function setRequestToken(request, token) {
|
|
113
|
+
request.headers.set("Authorization", `Bearer ${token}`);
|
|
114
|
+
return request;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Extract the `Authorization: Bearer {token}` from a `Request` object, or return `undefined` if not set.
|
|
118
|
+
*
|
|
119
|
+
* @param request The `Request` object possibly containing an `Authorization: Bearer {token}` header to extract the token from.
|
|
120
|
+
* @returns The string token extracted from the `Authorization` header, or `undefined` if not set.
|
|
121
|
+
*/
|
|
122
|
+
export function getRequestToken(request) {
|
|
123
|
+
const auth = request.headers.get("Authorization");
|
|
124
|
+
if (auth?.startsWith("Bearer "))
|
|
125
|
+
return auth.substring(7).trim() || undefined;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Extract the `Authorization: Bearer {token}` from a `Request` object, or throw `UnauthorizedError` if not set or malformed.
|
|
129
|
+
*
|
|
130
|
+
* @param request The `Request` object containing an `Authorization: Bearer {token}` header to extract the token from.
|
|
131
|
+
* @returns The string token extracted from the `Authorization` header.
|
|
132
|
+
* @throws UnauthorizedError If the `Authorization` header is not set, or the JWT it contains is not well-formed.
|
|
133
|
+
*/
|
|
134
|
+
export function requireRequestToken(request, caller = requireRequestToken) {
|
|
135
|
+
const token = getRequestToken(request);
|
|
136
|
+
if (!token)
|
|
137
|
+
throw new UnauthorizedError("JWT is required", { received: request.headers.get("Authorization"), caller });
|
|
138
|
+
return token;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Extract the `Authorization: Bearer {token}` from a `Request` object and verify it using a signature, or throw `UnauthorizedError` if not set, malformed, or invalid.
|
|
142
|
+
* - Same as doing `requireRequestToken(request)` and then `verifyToken(token, secret)`.
|
|
143
|
+
*
|
|
144
|
+
* @param request The `Request` object containing an `Authorization: Bearer {token}` header to extract the token from.
|
|
145
|
+
* @param secret The secret key to verify the JWT signature with.
|
|
146
|
+
*
|
|
147
|
+
* @returns The decoded payload data from the JWT.
|
|
148
|
+
* @throws UnauthorizedError If the `Authorization` header is not set, the JWT it contains is not well-formed, or the JWT signature is invalid.
|
|
149
|
+
*
|
|
150
|
+
* @example `const { sub, iss, customClaim } = await verifyRequestToken(request, secret);`
|
|
151
|
+
*/
|
|
152
|
+
export function verifyRequestToken(request, secret, caller = verifyRequestToken) {
|
|
153
|
+
const token = requireRequestToken(request, caller);
|
|
154
|
+
return verifyToken(token, secret, caller);
|
|
155
|
+
}
|
package/util/string.js
CHANGED
|
@@ -7,7 +7,7 @@ export function isString(value, min = 0, max = Number.POSITIVE_INFINITY) {
|
|
|
7
7
|
return typeof value === "string" && value.length >= min && value.length <= max;
|
|
8
8
|
}
|
|
9
9
|
/** Assert that a value is a string (optionally with specified min/max length). */
|
|
10
|
-
export function assertString(value, min, max, caller =
|
|
10
|
+
export function assertString(value, min, max, caller = assertString) {
|
|
11
11
|
if (!isString(value, min, max))
|
|
12
12
|
throw new RequiredError(`Must be string${min !== undefined || max !== undefined ? ` with ${min ?? 0} to ${max ?? "∞"} characters` : ""}`, {
|
|
13
13
|
received: value,
|
package/util/template.d.ts
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import type { ImmutableArray } from "./array.js";
|
|
2
2
|
import { type ImmutableDictionary } from "./dictionary.js";
|
|
3
3
|
import type { NotString } from "./string.js";
|
|
4
|
-
/**
|
|
5
|
-
type
|
|
6
|
-
/**
|
|
7
|
-
type
|
|
4
|
+
/** Dictionary of named template values in `{ myPlaceholder: "value" }` format. */
|
|
5
|
+
export type TemplateDictionary = ImmutableDictionary<string>;
|
|
6
|
+
/** Callback that returns the right replacement string for a given placeholder. */
|
|
7
|
+
export type TemplateCallback = (placeholder: string) => string;
|
|
8
|
+
/**
|
|
9
|
+
* Things that can be converted to the value for a named placeholder.
|
|
10
|
+
*
|
|
11
|
+
* `string` — Single string used for every `{placeholder}`
|
|
12
|
+
* `ImmutableArray<string>` — Array of strings (or functions that return strings) used for `*` numbered placeholders e.g. `["John"]`
|
|
13
|
+
* `TemplateValues` — Object containing named strings used for named placeholders, e.g. `{ myPlaceholder: "Ellie" }`
|
|
14
|
+
* `TemplateCallback` — Function that returns the right string for a named `{placeholder}`.v
|
|
15
|
+
*/
|
|
16
|
+
export type TemplateValues = string | ImmutableArray<string> | TemplateDictionary | TemplateCallback;
|
|
8
17
|
/**
|
|
9
18
|
* Get list of placeholders named in a template string.
|
|
10
19
|
*
|
|
@@ -21,11 +30,11 @@ export declare function getPlaceholders(template: string): readonly string[];
|
|
|
21
30
|
* @param target The string containing values, e.g. `Dave-UK/Manchester`
|
|
22
31
|
* @return An object containing values, e.g. `{ name: "Dave", country: "UK", city: "Manchester" }`, or undefined if target didn't match the template.
|
|
23
32
|
*/
|
|
24
|
-
export declare function matchTemplate(template: string, target: string):
|
|
33
|
+
export declare function matchTemplate(template: string, target: string): TemplateDictionary | undefined;
|
|
25
34
|
/**
|
|
26
35
|
* Match multiple templates against a target string and return the first match.
|
|
27
36
|
*/
|
|
28
|
-
export declare function matchTemplates(templates: Iterable<string> & NotString, target: string):
|
|
37
|
+
export declare function matchTemplates(templates: Iterable<string> & NotString, target: string): TemplateDictionary | undefined;
|
|
29
38
|
/**
|
|
30
39
|
* Turn ":year-:month" and `{ year: "2016"... }` etc into "2016-06..." etc.
|
|
31
40
|
*
|
|
@@ -33,7 +42,6 @@ export declare function matchTemplates(templates: Iterable<string> & NotString,
|
|
|
33
42
|
* @param values An object containing values, e.g. `{ name: "Dave", country: "UK", city: "Manchester" }` (functions are called, everything else converted to string), or a function or string to use for all placeholders.
|
|
34
43
|
* @return The rendered string, e.g. `Dave-UK/Manchester`
|
|
35
44
|
*
|
|
36
|
-
* @throws {
|
|
45
|
+
* @throws {RequiredError} If a placeholder in the template string is not specified in values.
|
|
37
46
|
*/
|
|
38
|
-
export declare function renderTemplate(template: string, values:
|
|
39
|
-
export {};
|
|
47
|
+
export declare function renderTemplate(template: string, values: TemplateValues): string;
|
package/util/template.js
CHANGED
|
@@ -99,7 +99,7 @@ export function matchTemplates(templates, target) {
|
|
|
99
99
|
* @param values An object containing values, e.g. `{ name: "Dave", country: "UK", city: "Manchester" }` (functions are called, everything else converted to string), or a function or string to use for all placeholders.
|
|
100
100
|
* @return The rendered string, e.g. `Dave-UK/Manchester`
|
|
101
101
|
*
|
|
102
|
-
* @throws {
|
|
102
|
+
* @throws {RequiredError} If a placeholder in the template string is not specified in values.
|
|
103
103
|
*/
|
|
104
104
|
export function renderTemplate(template, values) {
|
|
105
105
|
const chunks = _splitTemplateCached(template, renderTemplate);
|
package/util/validate.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BaseError, BaseErrorOptions } from "../error/BaseError.js";
|
|
1
|
+
import type { AnyCaller, BaseError, BaseErrorOptions } from "../error/BaseError.js";
|
|
2
2
|
import type { ImmutableArray, PossibleArray } from "./array.js";
|
|
3
3
|
import type { Constructor } from "./class.js";
|
|
4
4
|
import type { Data } from "./data.js";
|
|
@@ -29,7 +29,7 @@ export type ValidatorsType<T> = {
|
|
|
29
29
|
readonly [K in keyof T]: ValidatorType<T[K]>;
|
|
30
30
|
};
|
|
31
31
|
/** Get value that validates against a given `Validator`, or throw `ValueError` */
|
|
32
|
-
export declare function getValid<T>(value: unknown, validator: Validator<T>, ErrorConstructor?: Constructor<BaseError, [string, BaseErrorOptions]
|
|
32
|
+
export declare function getValid<T>(value: unknown, validator: Validator<T>, ErrorConstructor?: Constructor<BaseError, [string, BaseErrorOptions]>, caller?: AnyCaller): T;
|
|
33
33
|
/**
|
|
34
34
|
* Validate an iterable set of items with a validator.
|
|
35
35
|
*
|
package/util/validate.js
CHANGED
|
@@ -9,13 +9,13 @@ import { isIterable } from "./iterate.js";
|
|
|
9
9
|
import { getNull } from "./null.js";
|
|
10
10
|
import { getUndefined } from "./undefined.js";
|
|
11
11
|
/** Get value that validates against a given `Validator`, or throw `ValueError` */
|
|
12
|
-
export function getValid(value, validator, ErrorConstructor = ValueError) {
|
|
12
|
+
export function getValid(value, validator, ErrorConstructor = ValueError, caller = getValid) {
|
|
13
13
|
try {
|
|
14
14
|
return validator.validate(value);
|
|
15
15
|
}
|
|
16
16
|
catch (thrown) {
|
|
17
17
|
if (thrown instanceof Feedback)
|
|
18
|
-
throw new ErrorConstructor(thrown.message, { received: value, cause: thrown, caller
|
|
18
|
+
throw new ErrorConstructor(thrown.message, { received: value, cause: thrown, caller });
|
|
19
19
|
throw thrown;
|
|
20
20
|
}
|
|
21
21
|
}
|