gruber 0.7.0 → 0.8.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/CHANGELOG.md +14 -0
- package/README.md +69 -8
- package/core/authorization.d.ts +13 -1
- package/core/authorization.d.ts.map +1 -1
- package/core/authorization.js +23 -9
- package/core/authorization.test.js +41 -0
- package/core/authorization.ts +42 -10
- package/core/cors.d.ts +15 -0
- package/core/cors.d.ts.map +1 -0
- package/core/cors.js +50 -0
- package/core/cors.test.js +134 -0
- package/core/cors.ts +79 -0
- package/core/fetch-router.d.ts +4 -1
- package/core/fetch-router.d.ts.map +1 -1
- package/core/fetch-router.js +8 -2
- package/core/fetch-router.ts +13 -3
- package/core/mod.d.ts +2 -1
- package/core/mod.d.ts.map +1 -1
- package/core/mod.js +2 -1
- package/core/mod.ts +2 -1
- package/core/tokens.d.ts +11 -0
- package/core/tokens.d.ts.map +1 -1
- package/core/tokens.js +23 -0
- package/core/tokens.test.js +43 -0
- package/core/tokens.ts +24 -0
- package/package.json +1 -1
- package/source/node-router.d.ts +1 -1
- package/source/node-router.d.ts.map +1 -1
- package/source/node-router.js +8 -3
- package/source/node-router.ts +10 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
This file documents notable changes to the project
|
|
4
4
|
|
|
5
|
+
## 0.8.0
|
|
6
|
+
|
|
7
|
+
**features**
|
|
8
|
+
|
|
9
|
+
- Add optional `options` parameter to `authz.assert` to check the scope
|
|
10
|
+
- Add experimental `authz.from` method to parse the authenticated user
|
|
11
|
+
- Add experimental `CompositeTokens` to combine multiple TokenServices together
|
|
12
|
+
- Add experimental `Cors` utility for addings CORS headers to responses
|
|
13
|
+
- Add experimental `cors` option to `FetchRouter`
|
|
14
|
+
|
|
15
|
+
**fixes**
|
|
16
|
+
|
|
17
|
+
- Add optional `res` parameter to `getResponseReadable` to propagate stream cancellations
|
|
18
|
+
|
|
5
19
|
## 0.7.0
|
|
6
20
|
|
|
7
21
|
**new**
|
package/README.md
CHANGED
|
@@ -867,6 +867,11 @@ Use it to get a `Response` from the provided request, based on the router's rout
|
|
|
867
867
|
const response = await router.getResponse(new Request("http://localhost"));
|
|
868
868
|
```
|
|
869
869
|
|
|
870
|
+
**experimental**
|
|
871
|
+
|
|
872
|
+
- `options.log` turn on HTTP logging with `true` or a custom
|
|
873
|
+
- `options.cors` apply CORS headers with a [Cors](#cors) instance
|
|
874
|
+
|
|
870
875
|
#### unstable http
|
|
871
876
|
|
|
872
877
|
There are some unstable internal methods too:
|
|
@@ -877,6 +882,31 @@ There are some unstable internal methods too:
|
|
|
877
882
|
- `getRequestBody(request)` Get the JSON of FormData body of a request
|
|
878
883
|
- `assertRequestBody(struct, body)` Assert the body matches a structure and return the parsed value
|
|
879
884
|
|
|
885
|
+
#### Cors
|
|
886
|
+
|
|
887
|
+
There is an unstable API for applying [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS) headers to responses.
|
|
888
|
+
|
|
889
|
+
```ts
|
|
890
|
+
import { Cors } from "gruber";
|
|
891
|
+
|
|
892
|
+
const cors = new Cors({
|
|
893
|
+
credentials: true,
|
|
894
|
+
origins: ["http://localhost:8080"],
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
const response = cors.apply(
|
|
898
|
+
new Request("http://localhsot:8000"),
|
|
899
|
+
new Response("ok"),
|
|
900
|
+
);
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
It returns a clone of the response passed to it with CORS headers applied. You should no longer use the response passed into it. The headers it applies are:
|
|
904
|
+
|
|
905
|
+
- `Access-Control-Allow-Methods` set to `GET, HEAD, PUT, PATCH, POST, DELETE`
|
|
906
|
+
- `Access-Control-Allow-Headers` mirrors what is set on `Access-Control-Request-Headers` and adds that to `Vary`
|
|
907
|
+
- `Access-Control-Allow-Origin` allows the `Origin` if it matches the `origins` parameter
|
|
908
|
+
- `Access-Control-Allow-Credentials` is set to `true` if the `credentials` parameter is
|
|
909
|
+
|
|
880
910
|
### Postgres
|
|
881
911
|
|
|
882
912
|
#### getPostgresMigratorOptions
|
|
@@ -1063,16 +1093,16 @@ const redis: RedisClientType;
|
|
|
1063
1093
|
const store = new RedisStore(redis, { prefix: "/v2" });
|
|
1064
1094
|
```
|
|
1065
1095
|
|
|
1066
|
-
###
|
|
1096
|
+
### Tokens
|
|
1067
1097
|
|
|
1068
|
-
An abstraction around signing a
|
|
1098
|
+
An abstraction around signing or storing a token for a user with an access scope.
|
|
1069
1099
|
There is currently one implementation using [jose](https://github.com/panva/jose).
|
|
1070
1100
|
|
|
1071
1101
|
```ts
|
|
1072
|
-
import {
|
|
1102
|
+
import { JoseTokens } from "gruber";
|
|
1073
1103
|
import * as jose from "jose";
|
|
1074
1104
|
|
|
1075
|
-
const jwt = new
|
|
1105
|
+
const jwt = new JoseTokens(
|
|
1076
1106
|
{
|
|
1077
1107
|
secret: "top_secret",
|
|
1078
1108
|
issuer: "myapp.io",
|
|
@@ -1091,6 +1121,22 @@ const token = await jwt.sign("user:books:read", {
|
|
|
1091
1121
|
const parsed = await jwt.verify(token);
|
|
1092
1122
|
```
|
|
1093
1123
|
|
|
1124
|
+
There is also `CompositeTokens` which lets you combine multiple verifiers with one signer.
|
|
1125
|
+
For example, if your app has several methods a client might authenticate and one way it itself signs things,
|
|
1126
|
+
like a user token, or a static service or database app-token.
|
|
1127
|
+
|
|
1128
|
+
> UNSTABLE
|
|
1129
|
+
|
|
1130
|
+
```ts
|
|
1131
|
+
import { CompositeTokens, JoseTokens } from "gruber";
|
|
1132
|
+
import * as jose from "jose";
|
|
1133
|
+
|
|
1134
|
+
const tokens = new CompositeTokens(new JoseTokens("..."), [
|
|
1135
|
+
new JoseTokens("..."),
|
|
1136
|
+
// Different token formats your app accepts
|
|
1137
|
+
]);
|
|
1138
|
+
```
|
|
1139
|
+
|
|
1094
1140
|
### Authorization
|
|
1095
1141
|
|
|
1096
1142
|
> UNSTABLE
|
|
@@ -1110,19 +1156,27 @@ const token = authz.getAuthorization(
|
|
|
1110
1156
|
}),
|
|
1111
1157
|
);
|
|
1112
1158
|
|
|
1113
|
-
// { userId: number |
|
|
1159
|
+
// { kind: 'user', userId: number , scope: string } | { kind: 'service', scope: string }
|
|
1160
|
+
const result = await authz.from(
|
|
1161
|
+
new Request("https://example.com", {
|
|
1162
|
+
headers: { Authorization: "Bearer some-long-secure-token" },
|
|
1163
|
+
}),
|
|
1164
|
+
);
|
|
1165
|
+
|
|
1166
|
+
// { kind: 'user', userId: number , scope: string } | { kind: 'service', scope: string }
|
|
1114
1167
|
const { userId, scope } = await authz.assert(
|
|
1115
1168
|
new Request("https://example.com", {
|
|
1116
1169
|
headers: { Authorization: "Bearer some-long-secure-token" },
|
|
1117
1170
|
}),
|
|
1171
|
+
{ scope: "repo:coffee-club" }, // optional
|
|
1118
1172
|
);
|
|
1119
1173
|
|
|
1120
|
-
// { userId: number, scope: string }
|
|
1174
|
+
// { kind: 'user', userId: number, scope: string }
|
|
1121
1175
|
const { userId, scope } = await authz.assertUser(
|
|
1122
1176
|
new Request("https://example.com", {
|
|
1123
1177
|
headers: { Cookie: "my_session=some-long-secure-token" },
|
|
1124
1178
|
}),
|
|
1125
|
-
{ scope: "user:books:read" },
|
|
1179
|
+
{ scope: "user:books:read" }, // optional
|
|
1126
1180
|
);
|
|
1127
1181
|
|
|
1128
1182
|
includesScope("user:books:read", "user:books:read"); // true
|
|
@@ -1520,11 +1574,18 @@ const server = http.createServer((req) => {
|
|
|
1520
1574
|
`getResponseReadable` creates a [streams:Readable](https://nodejs.org/api/stream.html#class-streamreadable) from the body of a fetch Response.
|
|
1521
1575
|
|
|
1522
1576
|
```js
|
|
1577
|
+
import http from "node:http";
|
|
1523
1578
|
import { getResponseReadable } from "gruber/node-router.js";
|
|
1524
1579
|
|
|
1525
|
-
const
|
|
1580
|
+
const server = http.createServer((req, res) => {
|
|
1581
|
+
const readable = getResponseReadable(new Response("some body"), res);
|
|
1582
|
+
});
|
|
1526
1583
|
```
|
|
1527
1584
|
|
|
1585
|
+
Pass in `res` if you want the readable to be cancelled if reading the response is aborted.
|
|
1586
|
+
|
|
1587
|
+
> NOTE: This relies on the **experimental** [Readable.fromWeb](https://nodejs.org/api/stream.html#streamreadablefromwebreadablestream-options)
|
|
1588
|
+
|
|
1528
1589
|
## Development
|
|
1529
1590
|
|
|
1530
1591
|
WIP stuff
|
package/core/authorization.d.ts
CHANGED
|
@@ -12,13 +12,22 @@ export declare function _getRequestCookie(request: Request, cookieName: string):
|
|
|
12
12
|
export declare function _expandScopes(scope: string): string[];
|
|
13
13
|
export declare function _checkScope(actual: string, expected: string[]): boolean;
|
|
14
14
|
export declare function includesScope(actual: string, expected: string): boolean;
|
|
15
|
+
export interface AssertOptions {
|
|
16
|
+
scope?: string;
|
|
17
|
+
}
|
|
15
18
|
export interface AssertUserOptions {
|
|
16
19
|
scope?: string;
|
|
17
20
|
}
|
|
18
21
|
export interface AssertUserResult {
|
|
22
|
+
kind: "user";
|
|
19
23
|
userId: number;
|
|
20
24
|
scope: string;
|
|
21
25
|
}
|
|
26
|
+
export interface AssertServiceResult {
|
|
27
|
+
kind: "service";
|
|
28
|
+
scope: string;
|
|
29
|
+
}
|
|
30
|
+
export type AuthorizationResult = AssertUserResult | AssertServiceResult;
|
|
22
31
|
export interface AbstractAuthorizationService {
|
|
23
32
|
getAuthorization(request: Request): string | null;
|
|
24
33
|
assert(request: Request): Promise<AuthzToken>;
|
|
@@ -33,7 +42,10 @@ export declare class AuthorizationService implements AbstractAuthorizationServic
|
|
|
33
42
|
tokens: TokenService;
|
|
34
43
|
constructor(options: AuthorizationServiceOptions, tokens: TokenService);
|
|
35
44
|
getAuthorization(request: Request): string | null;
|
|
36
|
-
|
|
45
|
+
_processToken(verified: AuthzToken): AuthorizationResult;
|
|
46
|
+
/** @unstable use at your own risk */
|
|
47
|
+
from(request: Request): Promise<AuthorizationResult | null>;
|
|
48
|
+
assert(request: Request, options?: AssertOptions): Promise<AuthorizationResult>;
|
|
37
49
|
assertUser(request: Request, options?: AssertUserOptions): Promise<AssertUserResult>;
|
|
38
50
|
}
|
|
39
51
|
//# sourceMappingURL=authorization.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authorization.d.ts","sourceRoot":"","sources":["authorization.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEvD;;;GAGG;AACH,wBAAgB,WAAW,CAC1B,OAAO,EAAE,OAAO,GACd,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAapC;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,iBAIjD;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAMrE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,YAQ1C;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,WAM7D;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,WAE7D;AAED,MAAM,WAAW,iBAAiB;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAGD,MAAM,WAAW,gBAAgB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,4BAA4B;IAC5C,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAAC;IAClD,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC9C,UAAU,CACT,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,iBAAiB,GACzB,OAAO,CAAC,gBAAgB,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,2BAA2B;IAC3C,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,gBAAgB;AAChB,qBAAa,oBAAqB,YAAW,4BAA4B;IAEhE,OAAO,EAAE,2BAA2B;IACpC,MAAM,EAAE,YAAY;gBADpB,OAAO,EAAE,2BAA2B,EACpC,MAAM,EAAE,YAAY;IAG5B,gBAAgB,CAAC,OAAO,EAAE,OAAO;
|
|
1
|
+
{"version":3,"file":"authorization.d.ts","sourceRoot":"","sources":["authorization.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEvD;;;GAGG;AACH,wBAAgB,WAAW,CAC1B,OAAO,EAAE,OAAO,GACd,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAapC;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,iBAIjD;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAMrE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,YAQ1C;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,WAM7D;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,WAE7D;AAED,MAAM,WAAW,aAAa;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAGD,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,mBAAmB,GAAG,gBAAgB,GAAG,mBAAmB,CAAC;AAEzE,MAAM,WAAW,4BAA4B;IAC5C,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAAC;IAClD,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC9C,UAAU,CACT,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,iBAAiB,GACzB,OAAO,CAAC,gBAAgB,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,2BAA2B;IAC3C,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,gBAAgB;AAChB,qBAAa,oBAAqB,YAAW,4BAA4B;IAEhE,OAAO,EAAE,2BAA2B;IACpC,MAAM,EAAE,YAAY;gBADpB,OAAO,EAAE,2BAA2B,EACpC,MAAM,EAAE,YAAY;IAG5B,gBAAgB,CAAC,OAAO,EAAE,OAAO;IAOjC,aAAa,CAAC,QAAQ,EAAE,UAAU,GAAG,mBAAmB;IAMxD,qCAAqC;IAC/B,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC;IAQ3D,MAAM,CACX,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,aAAkB,GACzB,OAAO,CAAC,mBAAmB,CAAC;IAezB,UAAU,CACf,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,iBAAsB,GAC7B,OAAO,CAAC,gBAAgB,CAAC;CAS5B"}
|
package/core/authorization.js
CHANGED
|
@@ -67,23 +67,37 @@ export class AuthorizationService {
|
|
|
67
67
|
return (_getRequestBearer(request) ??
|
|
68
68
|
_getRequestCookie(request, this.options.cookieName));
|
|
69
69
|
}
|
|
70
|
-
|
|
70
|
+
_processToken(verified) {
|
|
71
|
+
return typeof verified.userId === "number"
|
|
72
|
+
? { kind: "user", userId: verified.userId, scope: verified.scope }
|
|
73
|
+
: { kind: "service", scope: verified.scope };
|
|
74
|
+
}
|
|
75
|
+
/** @unstable use at your own risk */
|
|
76
|
+
async from(request) {
|
|
77
|
+
const authz = this.getAuthorization(request);
|
|
78
|
+
if (!authz)
|
|
79
|
+
return null;
|
|
80
|
+
const verified = await this.tokens.verify(authz);
|
|
81
|
+
return verified ? this._processToken(verified) : null;
|
|
82
|
+
}
|
|
83
|
+
async assert(request, options = {}) {
|
|
71
84
|
const authz = this.getAuthorization(request);
|
|
72
85
|
if (!authz)
|
|
73
86
|
throw HTTPError.unauthorized("no authorization present");
|
|
74
87
|
const verified = await this.tokens.verify(authz);
|
|
75
88
|
if (!verified)
|
|
76
89
|
throw HTTPError.unauthorized("no valid authorization");
|
|
77
|
-
|
|
90
|
+
if (options.scope && !includesScope(verified.scope, options.scope)) {
|
|
91
|
+
throw HTTPError.unauthorized("missing required scope: " + options.scope);
|
|
92
|
+
}
|
|
93
|
+
return this._processToken(verified);
|
|
78
94
|
}
|
|
79
95
|
async assertUser(request, options = {}) {
|
|
80
|
-
const verified = await this.assert(request
|
|
81
|
-
|
|
82
|
-
|
|
96
|
+
const verified = await this.assert(request, {
|
|
97
|
+
scope: options.scope,
|
|
98
|
+
});
|
|
99
|
+
if (verified.kind !== "user")
|
|
83
100
|
throw HTTPError.unauthorized("not a user");
|
|
84
|
-
|
|
85
|
-
throw HTTPError.unauthorized("missing required scope: " + options.scope);
|
|
86
|
-
}
|
|
87
|
-
return { userId, scope };
|
|
101
|
+
return verified;
|
|
88
102
|
}
|
|
89
103
|
}
|
|
@@ -153,6 +153,32 @@ describe("AuthorizationService", () => {
|
|
|
153
153
|
});
|
|
154
154
|
});
|
|
155
155
|
|
|
156
|
+
describe("from", () => {
|
|
157
|
+
it("parses users", async () => {
|
|
158
|
+
const { authz } = setup();
|
|
159
|
+
|
|
160
|
+
const request = new Request("https://example.com", {
|
|
161
|
+
headers: { Authorization: 'Bearer {"scope":"statuses","userId":1}' },
|
|
162
|
+
});
|
|
163
|
+
assertEquals(await authz.from(request), {
|
|
164
|
+
kind: "user",
|
|
165
|
+
userId: 1,
|
|
166
|
+
scope: "statuses",
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
it("parses services", async () => {
|
|
170
|
+
const { authz } = setup();
|
|
171
|
+
|
|
172
|
+
const request = new Request("https://example.com", {
|
|
173
|
+
headers: { Authorization: 'Bearer {"scope":"coffee-club"}' },
|
|
174
|
+
});
|
|
175
|
+
assertEquals(await authz.from(request), {
|
|
176
|
+
kind: "service",
|
|
177
|
+
scope: "coffee-club",
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
156
182
|
describe("assert", () => {
|
|
157
183
|
it("parses bearer", async () => {
|
|
158
184
|
const { authz } = setup();
|
|
@@ -161,6 +187,7 @@ describe("AuthorizationService", () => {
|
|
|
161
187
|
headers: { Authorization: 'Bearer {"scope":"user","userId":1}' },
|
|
162
188
|
});
|
|
163
189
|
assertEquals(await authz.assert(request), {
|
|
190
|
+
kind: "user",
|
|
164
191
|
scope: "user",
|
|
165
192
|
userId: 1,
|
|
166
193
|
});
|
|
@@ -172,10 +199,22 @@ describe("AuthorizationService", () => {
|
|
|
172
199
|
headers: { Cookie: 'testing_session={"scope":"user","userId":1}' },
|
|
173
200
|
});
|
|
174
201
|
assertEquals(await authz.assert(request), {
|
|
202
|
+
kind: "user",
|
|
175
203
|
scope: "user",
|
|
176
204
|
userId: 1,
|
|
177
205
|
});
|
|
178
206
|
});
|
|
207
|
+
it("parses services", async () => {
|
|
208
|
+
const { authz } = setup();
|
|
209
|
+
|
|
210
|
+
const request = new Request("https://example.com", {
|
|
211
|
+
headers: { Authorization: 'Bearer {"scope":"coffee-club"}' },
|
|
212
|
+
});
|
|
213
|
+
assertEquals(await authz.assert(request), {
|
|
214
|
+
kind: "service",
|
|
215
|
+
scope: "coffee-club",
|
|
216
|
+
});
|
|
217
|
+
});
|
|
179
218
|
});
|
|
180
219
|
|
|
181
220
|
describe("assertUser", () => {
|
|
@@ -185,6 +224,7 @@ describe("AuthorizationService", () => {
|
|
|
185
224
|
headers: { Authorization: 'Bearer {"scope":"user","userId":1}' },
|
|
186
225
|
});
|
|
187
226
|
assertEquals(await authz.assertUser(request, { scope: "user" }), {
|
|
227
|
+
kind: "user",
|
|
188
228
|
userId: 1,
|
|
189
229
|
scope: "user",
|
|
190
230
|
});
|
|
@@ -195,6 +235,7 @@ describe("AuthorizationService", () => {
|
|
|
195
235
|
headers: { Cookie: 'testing_session={"scope":"user","userId":1}' },
|
|
196
236
|
});
|
|
197
237
|
assertEquals(await authz.assertUser(request, { scope: "user" }), {
|
|
238
|
+
kind: "user",
|
|
198
239
|
userId: 1,
|
|
199
240
|
scope: "user",
|
|
200
241
|
});
|
package/core/authorization.ts
CHANGED
|
@@ -61,16 +61,28 @@ export function includesScope(actual: string, expected: string) {
|
|
|
61
61
|
return _checkScope(actual, _expandScopes(expected));
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
export interface AssertOptions {
|
|
65
|
+
scope?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
64
68
|
export interface AssertUserOptions {
|
|
65
69
|
scope?: string;
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
// NOTE: should userId be a string for future-proofing / to align to JWTs?
|
|
69
73
|
export interface AssertUserResult {
|
|
74
|
+
kind: "user";
|
|
70
75
|
userId: number;
|
|
71
76
|
scope: string;
|
|
72
77
|
}
|
|
73
78
|
|
|
79
|
+
export interface AssertServiceResult {
|
|
80
|
+
kind: "service";
|
|
81
|
+
scope: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type AuthorizationResult = AssertUserResult | AssertServiceResult;
|
|
85
|
+
|
|
74
86
|
export interface AbstractAuthorizationService {
|
|
75
87
|
getAuthorization(request: Request): string | null;
|
|
76
88
|
assert(request: Request): Promise<AuthzToken>;
|
|
@@ -98,29 +110,49 @@ export class AuthorizationService implements AbstractAuthorizationService {
|
|
|
98
110
|
);
|
|
99
111
|
}
|
|
100
112
|
|
|
101
|
-
|
|
113
|
+
_processToken(verified: AuthzToken): AuthorizationResult {
|
|
114
|
+
return typeof verified.userId === "number"
|
|
115
|
+
? { kind: "user", userId: verified.userId, scope: verified.scope }
|
|
116
|
+
: { kind: "service", scope: verified.scope };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** @unstable use at your own risk */
|
|
120
|
+
async from(request: Request): Promise<AuthorizationResult | null> {
|
|
121
|
+
const authz = this.getAuthorization(request);
|
|
122
|
+
if (!authz) return null;
|
|
123
|
+
|
|
124
|
+
const verified = await this.tokens.verify(authz);
|
|
125
|
+
return verified ? this._processToken(verified) : null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async assert(
|
|
129
|
+
request: Request,
|
|
130
|
+
options: AssertOptions = {},
|
|
131
|
+
): Promise<AuthorizationResult> {
|
|
102
132
|
const authz = this.getAuthorization(request);
|
|
103
133
|
|
|
104
134
|
if (!authz) throw HTTPError.unauthorized("no authorization present");
|
|
105
135
|
|
|
106
136
|
const verified = await this.tokens.verify(authz);
|
|
107
137
|
if (!verified) throw HTTPError.unauthorized("no valid authorization");
|
|
108
|
-
|
|
138
|
+
|
|
139
|
+
if (options.scope && !includesScope(verified.scope, options.scope)) {
|
|
140
|
+
throw HTTPError.unauthorized("missing required scope: " + options.scope);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return this._processToken(verified);
|
|
109
144
|
}
|
|
110
145
|
|
|
111
146
|
async assertUser(
|
|
112
147
|
request: Request,
|
|
113
148
|
options: AssertUserOptions = {},
|
|
114
149
|
): Promise<AssertUserResult> {
|
|
115
|
-
const verified = await this.assert(request
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (userId === undefined) throw HTTPError.unauthorized("not a user");
|
|
150
|
+
const verified = await this.assert(request, {
|
|
151
|
+
scope: options.scope,
|
|
152
|
+
});
|
|
119
153
|
|
|
120
|
-
if (
|
|
121
|
-
throw HTTPError.unauthorized("missing required scope: " + options.scope);
|
|
122
|
-
}
|
|
154
|
+
if (verified.kind !== "user") throw HTTPError.unauthorized("not a user");
|
|
123
155
|
|
|
124
|
-
return
|
|
156
|
+
return verified;
|
|
125
157
|
}
|
|
126
158
|
}
|
package/core/cors.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface CorsOptions {
|
|
2
|
+
/** Origins you want to be allowed to access this server or "*" for any server */
|
|
3
|
+
origins?: string[];
|
|
4
|
+
/** Whether to allow credentials in requests, default: false */
|
|
5
|
+
credentials?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/** @unstable */
|
|
8
|
+
export declare class Cors {
|
|
9
|
+
origins: Set<string>;
|
|
10
|
+
credentials: boolean;
|
|
11
|
+
constructor(options?: CorsOptions);
|
|
12
|
+
apply(request: Request, response: Response): Response;
|
|
13
|
+
}
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=cors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["cors.ts"],"names":[],"mappings":"AAUA,UAAU,WAAW;IACpB,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAEnB,+DAA+D;IAC/D,WAAW,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,gBAAgB;AAChB,qBAAa,IAAI;IAChB,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACrB,WAAW,EAAE,OAAO,CAAC;gBACT,OAAO,GAAE,WAAgB;IAKrC,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ;CAmD1C"}
|
package/core/cors.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//
|
|
2
|
+
// This was loosely based on https://github.com/expressjs/cors/blob/master/lib/index.js
|
|
3
|
+
//
|
|
4
|
+
// Future work:
|
|
5
|
+
// - "allowedHeaders" to allow-list headers for request-headers
|
|
6
|
+
// - An option for "origins" to be dynamic so it could be pulled from a source like the database
|
|
7
|
+
// - An option to configure which methods are allowed
|
|
8
|
+
//
|
|
9
|
+
/** @unstable */
|
|
10
|
+
export class Cors {
|
|
11
|
+
origins;
|
|
12
|
+
credentials;
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.credentials = options.credentials ?? false;
|
|
15
|
+
this.origins = new Set(options.origins ?? ["*"]);
|
|
16
|
+
}
|
|
17
|
+
apply(request, response) {
|
|
18
|
+
const headers = new Headers(response.headers);
|
|
19
|
+
// HTTP methods
|
|
20
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Allow-Methods
|
|
21
|
+
headers.set("Access-Control-Allow-Methods", "GET, HEAD, PUT, PATCH, POST, DELETE");
|
|
22
|
+
// Headers
|
|
23
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Request-Headers
|
|
24
|
+
if (request.headers.has("Access-Control-Request-Headers")) {
|
|
25
|
+
headers.append("Access-Control-Allow-Headers", request.headers.get("Access-Control-Request-Headers"));
|
|
26
|
+
headers.append("Vary", "Access-Control-Request-Headers");
|
|
27
|
+
}
|
|
28
|
+
// Origins
|
|
29
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Allow-Origin
|
|
30
|
+
if (this.origins.has("*")) {
|
|
31
|
+
headers.set("Access-Control-Allow-Origin", request.headers.get("origin") ?? "*");
|
|
32
|
+
headers.append("Vary", "Origin");
|
|
33
|
+
}
|
|
34
|
+
else if (request.headers.has("origin") &&
|
|
35
|
+
this.origins.has(request.headers.get("origin"))) {
|
|
36
|
+
headers.set("Access-Control-Allow-Origin", request.headers.get("origin"));
|
|
37
|
+
headers.append("Vary", "Origin");
|
|
38
|
+
}
|
|
39
|
+
// Credentials
|
|
40
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Allow-Credentials
|
|
41
|
+
if (this.credentials) {
|
|
42
|
+
headers.set("Access-Control-Allow-Credentials", "true");
|
|
43
|
+
}
|
|
44
|
+
return new Response(response.body, {
|
|
45
|
+
headers,
|
|
46
|
+
status: response.status,
|
|
47
|
+
statusText: response.statusText,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { Cors } from "./cors.ts";
|
|
2
|
+
import { assertEquals, assertMatch, describe, it } from "./test-deps.js";
|
|
3
|
+
|
|
4
|
+
describe("Cors", () => {
|
|
5
|
+
describe("constructor", () => {
|
|
6
|
+
it("stores credentials", () => {
|
|
7
|
+
const cors = new Cors({ credentials: true });
|
|
8
|
+
assertEquals(cors.credentials, true);
|
|
9
|
+
});
|
|
10
|
+
it("stores origins", () => {
|
|
11
|
+
const cors = new Cors({
|
|
12
|
+
origins: ["https://duck.com", "https://example.com"],
|
|
13
|
+
});
|
|
14
|
+
assertEquals(
|
|
15
|
+
cors.origins,
|
|
16
|
+
new Set(["https://duck.com", "https://example.com"]),
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("apply", () => {
|
|
22
|
+
it("adds methods header", () => {
|
|
23
|
+
const request = new Request("http://testing.local");
|
|
24
|
+
const cors = new Cors();
|
|
25
|
+
const response = cors.apply(request, new Response());
|
|
26
|
+
|
|
27
|
+
assertEquals(
|
|
28
|
+
response.headers.get("Access-Control-Allow-Methods"),
|
|
29
|
+
"GET, HEAD, PUT, PATCH, POST, DELETE",
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
it("adds request headers", () => {
|
|
33
|
+
const request = new Request("http://testing.local", {
|
|
34
|
+
headers: {
|
|
35
|
+
"Access-Control-Request-Headers": ["content-type", "x-pingother"],
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
const cors = new Cors();
|
|
39
|
+
const response = cors.apply(request, new Response());
|
|
40
|
+
|
|
41
|
+
assertEquals(
|
|
42
|
+
response.headers.get("Access-Control-Allow-Headers"),
|
|
43
|
+
"content-type,x-pingother",
|
|
44
|
+
"should add the headers to Access-Control-Allow-Headers",
|
|
45
|
+
);
|
|
46
|
+
assertMatch(
|
|
47
|
+
response.headers.get("Vary"),
|
|
48
|
+
/Access-Control-Request-Headers/,
|
|
49
|
+
"should modify the Vary header",
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
it("adds wildcard origins", () => {
|
|
53
|
+
const request = new Request("http://testing.local");
|
|
54
|
+
const cors = new Cors({ origins: ["*"] });
|
|
55
|
+
const response = cors.apply(request, new Response());
|
|
56
|
+
|
|
57
|
+
assertEquals(
|
|
58
|
+
response.headers.get("Access-Control-Allow-Origin"),
|
|
59
|
+
"*",
|
|
60
|
+
"should respond with a wildcard if no origin is available",
|
|
61
|
+
);
|
|
62
|
+
assertMatch(
|
|
63
|
+
response.headers.get("Vary"),
|
|
64
|
+
/Origin/,
|
|
65
|
+
"should modify the Vary header",
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
it("adds requested wildcard origin", () => {
|
|
69
|
+
const request = new Request("http://testing.local", {
|
|
70
|
+
headers: { Origin: "http://testing.local" },
|
|
71
|
+
});
|
|
72
|
+
const cors = new Cors({ origins: ["*"] });
|
|
73
|
+
const response = cors.apply(request, new Response());
|
|
74
|
+
|
|
75
|
+
assertEquals(
|
|
76
|
+
response.headers.get("Access-Control-Allow-Origin"),
|
|
77
|
+
"http://testing.local",
|
|
78
|
+
"should respond with the requested origin",
|
|
79
|
+
);
|
|
80
|
+
assertMatch(
|
|
81
|
+
response.headers.get("Vary"),
|
|
82
|
+
/Origin/,
|
|
83
|
+
"should modify the Vary header",
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
it("adds specific origins", () => {
|
|
87
|
+
const request = new Request("http://testing.local", {
|
|
88
|
+
headers: { Origin: "http://testing.local" },
|
|
89
|
+
});
|
|
90
|
+
const cors = new Cors({ origins: ["http://testing.local"] });
|
|
91
|
+
const response = cors.apply(request, new Response());
|
|
92
|
+
|
|
93
|
+
assertEquals(
|
|
94
|
+
response.headers.get("Access-Control-Allow-Origin"),
|
|
95
|
+
"http://testing.local",
|
|
96
|
+
"should respond with the requested origin",
|
|
97
|
+
);
|
|
98
|
+
assertMatch(
|
|
99
|
+
response.headers.get("Vary"),
|
|
100
|
+
/Origin/,
|
|
101
|
+
"should modify the Vary header",
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("adds credentials", () => {
|
|
106
|
+
const request = new Request("http://testing.local");
|
|
107
|
+
const cors = new Cors({ credentials: true });
|
|
108
|
+
const response = cors.apply(request, new Response());
|
|
109
|
+
|
|
110
|
+
assertEquals(
|
|
111
|
+
response.headers.get("Access-Control-Allow-Credentials"),
|
|
112
|
+
"true",
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("clones the response", async () => {
|
|
117
|
+
const request = new Request("http://testing.local");
|
|
118
|
+
const cors = new Cors();
|
|
119
|
+
const response = cors.apply(
|
|
120
|
+
request,
|
|
121
|
+
new Response("ok", {
|
|
122
|
+
status: 418,
|
|
123
|
+
statusText: "I'm a teapot",
|
|
124
|
+
headers: { "X-Hotel-Bar": "Hotel Bar" },
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
assertEquals(response.status, 418);
|
|
129
|
+
assertEquals(response.statusText, "I'm a teapot");
|
|
130
|
+
assertEquals(await response.text(), "ok");
|
|
131
|
+
assertEquals(response.headers.get("X-Hotel-Bar"), "Hotel Bar");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
package/core/cors.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
//
|
|
2
|
+
// This was loosely based on https://github.com/expressjs/cors/blob/master/lib/index.js
|
|
3
|
+
//
|
|
4
|
+
// Future work:
|
|
5
|
+
// - "allowedHeaders" to allow-list headers for request-headers
|
|
6
|
+
// - An option for "origins" to be dynamic so it could be pulled from a source like the database
|
|
7
|
+
// - An option to configure which methods are allowed
|
|
8
|
+
//
|
|
9
|
+
|
|
10
|
+
//
|
|
11
|
+
interface CorsOptions {
|
|
12
|
+
/** Origins you want to be allowed to access this server or "*" for any server */
|
|
13
|
+
origins?: string[];
|
|
14
|
+
|
|
15
|
+
/** Whether to allow credentials in requests, default: false */
|
|
16
|
+
credentials?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** @unstable */
|
|
20
|
+
export class Cors {
|
|
21
|
+
origins: Set<string>;
|
|
22
|
+
credentials: boolean;
|
|
23
|
+
constructor(options: CorsOptions = {}) {
|
|
24
|
+
this.credentials = options.credentials ?? false;
|
|
25
|
+
this.origins = new Set(options.origins ?? ["*"]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
apply(request: Request, response: Response) {
|
|
29
|
+
const headers = new Headers(response.headers);
|
|
30
|
+
|
|
31
|
+
// HTTP methods
|
|
32
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Allow-Methods
|
|
33
|
+
headers.set(
|
|
34
|
+
"Access-Control-Allow-Methods",
|
|
35
|
+
"GET, HEAD, PUT, PATCH, POST, DELETE",
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// Headers
|
|
39
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Request-Headers
|
|
40
|
+
if (request.headers.has("Access-Control-Request-Headers")) {
|
|
41
|
+
headers.append(
|
|
42
|
+
"Access-Control-Allow-Headers",
|
|
43
|
+
request.headers.get("Access-Control-Request-Headers")!,
|
|
44
|
+
);
|
|
45
|
+
headers.append("Vary", "Access-Control-Request-Headers");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Origins
|
|
49
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Allow-Origin
|
|
50
|
+
if (this.origins.has("*")) {
|
|
51
|
+
headers.set(
|
|
52
|
+
"Access-Control-Allow-Origin",
|
|
53
|
+
request.headers.get("origin") ?? "*",
|
|
54
|
+
);
|
|
55
|
+
headers.append("Vary", "Origin");
|
|
56
|
+
} else if (
|
|
57
|
+
request.headers.has("origin") &&
|
|
58
|
+
this.origins.has(request.headers.get("origin")!)
|
|
59
|
+
) {
|
|
60
|
+
headers.set(
|
|
61
|
+
"Access-Control-Allow-Origin",
|
|
62
|
+
request.headers.get("origin")!,
|
|
63
|
+
);
|
|
64
|
+
headers.append("Vary", "Origin");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Credentials
|
|
68
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Allow-Credentials
|
|
69
|
+
if (this.credentials) {
|
|
70
|
+
headers.set("Access-Control-Allow-Credentials", "true");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return new Response(response.body, {
|
|
74
|
+
headers,
|
|
75
|
+
status: response.status,
|
|
76
|
+
statusText: response.statusText,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
package/core/fetch-router.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Cors } from "./cors.ts";
|
|
1
2
|
import { RouteDefinition } from "./http.ts";
|
|
2
3
|
export type RouteErrorHandler = (error: unknown, request: Request) => unknown;
|
|
3
4
|
export interface MatchedRoute {
|
|
@@ -10,10 +11,12 @@ export interface FetchRouterOptions {
|
|
|
10
11
|
errorHandler?: RouteErrorHandler;
|
|
11
12
|
/** @unstable */
|
|
12
13
|
log?: boolean | _RouteMiddleware;
|
|
14
|
+
/** @unstable */
|
|
15
|
+
cors?: Cors;
|
|
13
16
|
}
|
|
14
17
|
/** @unstable */
|
|
15
18
|
export interface _RouteMiddleware {
|
|
16
|
-
(request: Request, response: Response):
|
|
19
|
+
(request: Request, response: Response): Promise<Response> | Response;
|
|
17
20
|
}
|
|
18
21
|
/** A rudimentary HTTP router using fetch Request & Responses with RouteDefinitions based on URLPattern */
|
|
19
22
|
export declare class FetchRouter {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch-router.d.ts","sourceRoot":"","sources":["fetch-router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,eAAe,EAAE,MAAM,WAAW,CAAC;AAEvD,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC;AAE9E,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,eAAe,CAAC;IACvB,GAAG,EAAE,GAAG,CAAC;IACT,MAAM,EAAE,gBAAgB,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IAClC,MAAM,CAAC,EAAE,eAAe,EAAE,CAAC;IAC3B,YAAY,CAAC,EAAE,iBAAiB,CAAC;IAEjC,gBAAgB;IAChB,GAAG,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"fetch-router.d.ts","sourceRoot":"","sources":["fetch-router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAa,eAAe,EAAE,MAAM,WAAW,CAAC;AAEvD,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC;AAE9E,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,eAAe,CAAC;IACvB,GAAG,EAAE,GAAG,CAAC;IACT,MAAM,EAAE,gBAAgB,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IAClC,MAAM,CAAC,EAAE,eAAe,EAAE,CAAC;IAC3B,YAAY,CAAC,EAAE,iBAAiB,CAAC;IAEjC,gBAAgB;IAChB,GAAG,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAC;IAEjC,gBAAgB;IAChB,IAAI,CAAC,EAAE,IAAI,CAAC;CACZ;AAED,gBAAgB;AAChB,MAAM,WAAW,gBAAgB;IAChC,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAC;CACrE;AAOD,0GAA0G;AAC1G,qBAAa,WAAW;IACvB,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,YAAY,EAAE,iBAAiB,GAAG,SAAS,CAAC;IAC5C,WAAW,EAAE,gBAAgB,EAAE,CAAM;gBAEzB,OAAO,GAAE,kBAAuB;IAU5C;;;OAGG;IACF,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,QAAQ,CAAC,YAAY,CAAC;IAa7D;;OAEG;IACG,cAAc,CACnB,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,QAAQ,CAAC,YAAY,CAAC,GAC7B,OAAO,CAAC,QAAQ,CAAC;IAepB,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,QAAQ;IAUjD,WAAW,CAAC,OAAO,EAAE,OAAO;CAclC"}
|
package/core/fetch-router.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { HTTPError } from "./http.js";
|
|
2
2
|
function _defaultLogger(request, response) {
|
|
3
3
|
console.debug(response.status, request.method.padEnd(5), request.url);
|
|
4
|
+
return response;
|
|
4
5
|
}
|
|
5
6
|
/** A rudimentary HTTP router using fetch Request & Responses with RouteDefinitions based on URLPattern */
|
|
6
7
|
export class FetchRouter {
|
|
@@ -14,6 +15,9 @@ export class FetchRouter {
|
|
|
14
15
|
this._middleware.push(_defaultLogger);
|
|
15
16
|
if (typeof options.log === "function")
|
|
16
17
|
this._middleware.push(options.log);
|
|
18
|
+
if (options.cors) {
|
|
19
|
+
this._middleware.push((req, res) => options.cors.apply(req, res));
|
|
20
|
+
}
|
|
17
21
|
}
|
|
18
22
|
/**
|
|
19
23
|
* Finds routes that match the request method and URLPattern
|
|
@@ -57,8 +61,10 @@ export class FetchRouter {
|
|
|
57
61
|
}
|
|
58
62
|
async getResponse(request) {
|
|
59
63
|
try {
|
|
60
|
-
|
|
61
|
-
this._middleware
|
|
64
|
+
let response = await this.processMatches(request, this.findMatchingRoutes(request));
|
|
65
|
+
for (const fn of this._middleware) {
|
|
66
|
+
response = await fn(request, response);
|
|
67
|
+
}
|
|
62
68
|
return response;
|
|
63
69
|
}
|
|
64
70
|
catch (error) {
|
package/core/fetch-router.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Cors } from "./cors.ts";
|
|
1
2
|
import { HTTPError, RouteDefinition } from "./http.ts";
|
|
2
3
|
|
|
3
4
|
export type RouteErrorHandler = (error: unknown, request: Request) => unknown;
|
|
@@ -14,15 +15,19 @@ export interface FetchRouterOptions {
|
|
|
14
15
|
|
|
15
16
|
/** @unstable */
|
|
16
17
|
log?: boolean | _RouteMiddleware;
|
|
18
|
+
|
|
19
|
+
/** @unstable */
|
|
20
|
+
cors?: Cors;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
/** @unstable */
|
|
20
24
|
export interface _RouteMiddleware {
|
|
21
|
-
(request: Request, response: Response):
|
|
25
|
+
(request: Request, response: Response): Promise<Response> | Response;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
function _defaultLogger(request: Request, response: Response) {
|
|
25
29
|
console.debug(response.status, request.method.padEnd(5), request.url);
|
|
30
|
+
return response;
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
/** A rudimentary HTTP router using fetch Request & Responses with RouteDefinitions based on URLPattern */
|
|
@@ -36,6 +41,9 @@ export class FetchRouter {
|
|
|
36
41
|
this.errorHandler = options.errorHandler ?? undefined;
|
|
37
42
|
if (options.log === true) this._middleware.push(_defaultLogger);
|
|
38
43
|
if (typeof options.log === "function") this._middleware.push(options.log);
|
|
44
|
+
if (options.cors) {
|
|
45
|
+
this._middleware.push((req, res) => options.cors!.apply(req, res));
|
|
46
|
+
}
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
/**
|
|
@@ -88,11 +96,13 @@ export class FetchRouter {
|
|
|
88
96
|
|
|
89
97
|
async getResponse(request: Request) {
|
|
90
98
|
try {
|
|
91
|
-
|
|
99
|
+
let response = await this.processMatches(
|
|
92
100
|
request,
|
|
93
101
|
this.findMatchingRoutes(request),
|
|
94
102
|
);
|
|
95
|
-
this._middleware
|
|
103
|
+
for (const fn of this._middleware) {
|
|
104
|
+
response = await fn(request, response);
|
|
105
|
+
}
|
|
96
106
|
return response;
|
|
97
107
|
} catch (error) {
|
|
98
108
|
return this.handleError(request, error);
|
package/core/mod.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export * from "./authentication.ts";
|
|
2
2
|
export * from "./authorization.ts";
|
|
3
3
|
export * from "./configuration.ts";
|
|
4
|
+
export * from "./cors.ts";
|
|
4
5
|
export * from "./fetch-router.ts";
|
|
5
6
|
export * from "./http.ts";
|
|
6
|
-
export * from "./tokens.ts";
|
|
7
7
|
export * from "./migrator.ts";
|
|
8
8
|
export * from "./postgres.ts";
|
|
9
9
|
export * from "./random.ts";
|
|
@@ -12,5 +12,6 @@ export * from "./store.ts";
|
|
|
12
12
|
export * from "./structures.ts";
|
|
13
13
|
export * from "./terminator.ts";
|
|
14
14
|
export * from "./timers.ts";
|
|
15
|
+
export * from "./tokens.ts";
|
|
15
16
|
export * from "./utilities.ts";
|
|
16
17
|
//# sourceMappingURL=mod.d.ts.map
|
package/core/mod.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["mod.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,
|
|
1
|
+
{"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["mod.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,yBAAyB,CAAC;AACxC,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC"}
|
package/core/mod.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export * from "./authentication.js";
|
|
2
2
|
export * from "./authorization.js";
|
|
3
3
|
export * from "./configuration.js";
|
|
4
|
+
export * from "./cors.js";
|
|
4
5
|
export * from "./fetch-router.js";
|
|
5
6
|
export * from "./http.js";
|
|
6
|
-
export * from "./tokens.js";
|
|
7
7
|
export * from "./migrator.js";
|
|
8
8
|
export * from "./postgres.js";
|
|
9
9
|
export * from "./random.js";
|
|
@@ -12,4 +12,5 @@ export * from "./store.js";
|
|
|
12
12
|
export * from "./structures.js";
|
|
13
13
|
export * from "./terminator.js";
|
|
14
14
|
export * from "./timers.js";
|
|
15
|
+
export * from "./tokens.js";
|
|
15
16
|
export * from "./utilities.js";
|
package/core/mod.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export * from "./authentication.ts";
|
|
2
2
|
export * from "./authorization.ts";
|
|
3
3
|
export * from "./configuration.ts";
|
|
4
|
+
export * from "./cors.ts";
|
|
4
5
|
export * from "./fetch-router.ts";
|
|
5
6
|
export * from "./http.ts";
|
|
6
|
-
export * from "./tokens.ts";
|
|
7
7
|
export * from "./migrator.ts";
|
|
8
8
|
export * from "./postgres.ts";
|
|
9
9
|
export * from "./random.ts";
|
|
@@ -12,4 +12,5 @@ export * from "./store.ts";
|
|
|
12
12
|
export * from "./structures.ts";
|
|
13
13
|
export * from "./terminator.ts";
|
|
14
14
|
export * from "./timers.ts";
|
|
15
|
+
export * from "./tokens.ts";
|
|
15
16
|
export * from "./utilities.ts";
|
package/core/tokens.d.ts
CHANGED
|
@@ -25,4 +25,15 @@ export declare class JoseTokens implements TokenService {
|
|
|
25
25
|
verify(input: string): Promise<AuthzToken | null>;
|
|
26
26
|
sign(scope: string, options?: SignTokenOptions): Promise<string>;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* @unstable
|
|
30
|
+
* A TokenService with multiple verification methods and a single signer
|
|
31
|
+
*/
|
|
32
|
+
export declare class CompositeTokens implements TokenService {
|
|
33
|
+
signer: TokenService;
|
|
34
|
+
verifiers: TokenService[];
|
|
35
|
+
constructor(signer: TokenService, verifiers: TokenService[]);
|
|
36
|
+
verify(token: string): Promise<AuthzToken | null>;
|
|
37
|
+
sign(scope: string, options?: SignTokenOptions): Promise<string>;
|
|
38
|
+
}
|
|
28
39
|
//# sourceMappingURL=tokens.d.ts.map
|
package/core/tokens.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["tokens.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,MAAM,WAAW,UAAU;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,gBAAgB;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mBAAmB,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACpC;AAED,wDAAwD;AACxD,MAAM,WAAW,YAAY;IAC5B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAClD,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACjE;AAED,MAAM,WAAW,iBAAiB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,UAAW,YAAW,YAAY;;IAC9C,OAAO,EAAE,iBAAiB,CAAC;IAC3B,IAAI,EAAE,cAAc,CAAC;gBAGT,OAAO,EAAE,iBAAiB,EAAE,IAAI,EAAE,cAAc;IAMtD,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAgBvD,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB;CAgBlD"}
|
|
1
|
+
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["tokens.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,MAAM,WAAW,UAAU;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,gBAAgB;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mBAAmB,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACpC;AAED,wDAAwD;AACxD,MAAM,WAAW,YAAY;IAC5B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAClD,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACjE;AAED,MAAM,WAAW,iBAAiB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,UAAW,YAAW,YAAY;;IAC9C,OAAO,EAAE,iBAAiB,CAAC;IAC3B,IAAI,EAAE,cAAc,CAAC;gBAGT,OAAO,EAAE,iBAAiB,EAAE,IAAI,EAAE,cAAc;IAMtD,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAgBvD,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB;CAgBlD;AAED;;;GAGG;AACH,qBAAa,eAAgB,YAAW,YAAY;IACnD,MAAM,EAAE,YAAY,CAAC;IACrB,SAAS,EAAE,YAAY,EAAE,CAAC;gBACd,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE;IAKrD,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAOvD,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC;CAGpE"}
|
package/core/tokens.js
CHANGED
|
@@ -38,3 +38,26 @@ export class JoseTokens {
|
|
|
38
38
|
return jwt.sign(this.#secret);
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* @unstable
|
|
43
|
+
* A TokenService with multiple verification methods and a single signer
|
|
44
|
+
*/
|
|
45
|
+
export class CompositeTokens {
|
|
46
|
+
signer;
|
|
47
|
+
verifiers;
|
|
48
|
+
constructor(signer, verifiers) {
|
|
49
|
+
this.signer = signer;
|
|
50
|
+
this.verifiers = verifiers;
|
|
51
|
+
}
|
|
52
|
+
async verify(token) {
|
|
53
|
+
for (const verifier of this.verifiers) {
|
|
54
|
+
const result = await verifier.verify(token);
|
|
55
|
+
if (result)
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
sign(scope, options = {}) {
|
|
61
|
+
return this.signer.sign(scope, options);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertEquals,
|
|
3
|
+
assertThrows,
|
|
4
|
+
describe,
|
|
5
|
+
fakeTokens,
|
|
6
|
+
it,
|
|
7
|
+
} from "./test-deps.js";
|
|
8
|
+
import { CompositeTokens } from "./tokens.ts";
|
|
9
|
+
|
|
10
|
+
describe("CompositeTokens", () => {
|
|
11
|
+
describe("verify", () => {
|
|
12
|
+
it("tries each verifier", async () => {
|
|
13
|
+
const tokens = new CompositeTokens(
|
|
14
|
+
{ sign: () => Promise.resolve("signed_token") },
|
|
15
|
+
[
|
|
16
|
+
{ verify: () => Promise.resolve(null) },
|
|
17
|
+
{ verify: () => Promise.resolve(null) },
|
|
18
|
+
{ verify: () => Promise.resolve({ userId: 1, scope: "statuses" }) },
|
|
19
|
+
{ verify: () => Promise.resolve({ userId: 2, scope: "invalid" }) },
|
|
20
|
+
],
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
assertEquals(await tokens.verify("input_token"), {
|
|
24
|
+
userId: 1,
|
|
25
|
+
scope: "statuses",
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("sign", () => {
|
|
31
|
+
it("uses the signer", async () => {
|
|
32
|
+
const tokens = new CompositeTokens(
|
|
33
|
+
{
|
|
34
|
+
sign: (scope, options) =>
|
|
35
|
+
Promise.resolve(`${scope}__${options.userId}`),
|
|
36
|
+
},
|
|
37
|
+
[],
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
assertEquals(await tokens.sign("statuses", { userId: 1 }), "statuses__1");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|
package/core/tokens.ts
CHANGED
|
@@ -66,3 +66,27 @@ export class JoseTokens implements TokenService {
|
|
|
66
66
|
return jwt.sign(this.#secret);
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @unstable
|
|
72
|
+
* A TokenService with multiple verification methods and a single signer
|
|
73
|
+
*/
|
|
74
|
+
export class CompositeTokens implements TokenService {
|
|
75
|
+
signer: TokenService;
|
|
76
|
+
verifiers: TokenService[];
|
|
77
|
+
constructor(signer: TokenService, verifiers: TokenService[]) {
|
|
78
|
+
this.signer = signer;
|
|
79
|
+
this.verifiers = verifiers;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async verify(token: string): Promise<AuthzToken | null> {
|
|
83
|
+
for (const verifier of this.verifiers) {
|
|
84
|
+
const result = await verifier.verify(token);
|
|
85
|
+
if (result) return result;
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
sign(scope: string, options: SignTokenOptions = {}): Promise<string> {
|
|
90
|
+
return this.signer.sign(scope, options);
|
|
91
|
+
}
|
|
92
|
+
}
|
package/package.json
CHANGED
package/source/node-router.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export declare function applyResponse(response: Response, res: ServerResponse):
|
|
|
29
29
|
export declare function getFetchRequest(req: IncomingMessage): Request;
|
|
30
30
|
export declare function getFetchHeaders(input: IncomingHttpHeaders): Headers;
|
|
31
31
|
export declare function getIncomingMessageBody(req: IncomingMessage): BodyInit | undefined;
|
|
32
|
-
export declare function getResponseReadable(response: Response): Readable;
|
|
32
|
+
export declare function getResponseReadable(response: Response, res?: ServerResponse): Readable;
|
|
33
33
|
/** @unstable */
|
|
34
34
|
export interface ServeHTTPOptions {
|
|
35
35
|
port: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node-router.d.ts","sourceRoot":"","sources":["node-router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAEN,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,cAAc,EACd,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEhD,MAAM,WAAW,iBAAiB;IACjC,MAAM,CAAC,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED;;;;;;;;;;EAUE;AACF,qBAAa,UAAU;IACtB,MAAM,EAAE,WAAW,CAAC;gBACR,OAAO,GAAE,iBAAsB;IAO3C,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAIhD,aAAa,IAAI,eAAe;IAQhC,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IAIpD,OAAO,CAAC,GAAG,EAAE,cAAc,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI;CAGtD;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI,CAS3E;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,eAAe,WAYnD;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,mBAAmB,WAOzD;AAID,wBAAgB,sBAAsB,CACrC,GAAG,EAAE,eAAe,GAClB,QAAQ,GAAG,SAAS,CAItB;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,
|
|
1
|
+
{"version":3,"file":"node-router.d.ts","sourceRoot":"","sources":["node-router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAEN,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,cAAc,EACd,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEhD,MAAM,WAAW,iBAAiB;IACjC,MAAM,CAAC,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED;;;;;;;;;;EAUE;AACF,qBAAa,UAAU;IACtB,MAAM,EAAE,WAAW,CAAC;gBACR,OAAO,GAAE,iBAAsB;IAO3C,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAIhD,aAAa,IAAI,eAAe;IAQhC,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IAIpD,OAAO,CAAC,GAAG,EAAE,cAAc,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI;CAGtD;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI,CAS3E;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,eAAe,WAYnD;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,mBAAmB,WAOzD;AAID,wBAAgB,sBAAsB,CACrC,GAAG,EAAE,eAAe,GAClB,QAAQ,GAAG,SAAS,CAItB;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,CAAC,EAAE,cAAc,YAU3E;AAED,gBAAgB;AAChB,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,gBAAgB;AAChB,MAAM,WAAW,gBAAgB;IAChC,CAAC,OAAO,EAAE,OAAO,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;CAC3C;AAED,gFAAgF;AAChF,wBAAgB,SAAS,CACxB,OAAO,EAAE,gBAAgB,EACzB,OAAO,EAAE,gBAAgB,wEAuBzB"}
|
package/source/node-router.js
CHANGED
|
@@ -79,9 +79,14 @@ export function getIncomingMessageBody(req) {
|
|
|
79
79
|
return undefined;
|
|
80
80
|
return Readable.toWeb(req);
|
|
81
81
|
}
|
|
82
|
-
export function getResponseReadable(response) {
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
export function getResponseReadable(response, res) {
|
|
83
|
+
const ac = new AbortController();
|
|
84
|
+
// Abort controller if the response is aborted (ie the user cancelled streaming)
|
|
85
|
+
res?.once("close", () => {
|
|
86
|
+
console.log("@gruber res close");
|
|
87
|
+
ac.abort();
|
|
88
|
+
});
|
|
89
|
+
return Readable.fromWeb(response.body, { signal: ac.signal });
|
|
85
90
|
}
|
|
86
91
|
/** @unstable A node version of Deno.serve now all the polyfills are in place */
|
|
87
92
|
export function serveHTTP(options, handler) {
|
package/source/node-router.ts
CHANGED
|
@@ -100,9 +100,16 @@ export function getIncomingMessageBody(
|
|
|
100
100
|
return Readable.toWeb(req) as ReadableStream;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
export function getResponseReadable(response: Response) {
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
export function getResponseReadable(response: Response, res?: ServerResponse) {
|
|
104
|
+
const ac = new AbortController();
|
|
105
|
+
|
|
106
|
+
// Abort controller if the response is aborted (ie the user cancelled streaming)
|
|
107
|
+
res?.once("close", () => {
|
|
108
|
+
console.log("@gruber res close");
|
|
109
|
+
ac.abort();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return Readable.fromWeb(response.body as any, { signal: ac.signal });
|
|
106
113
|
}
|
|
107
114
|
|
|
108
115
|
/** @unstable */
|