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 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
- ### JWT
1096
+ ### Tokens
1067
1097
 
1068
- An abstraction around signing a JWT for a user with an access scope.
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 { JoseJwtService } from "gruber";
1102
+ import { JoseTokens } from "gruber";
1073
1103
  import * as jose from "jose";
1074
1104
 
1075
- const jwt = new JoseJwtService(
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 | undefined, scope: string }
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 readable = getResponseReadable(new Response("some body"));
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
@@ -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
- assert(request: Request): Promise<AuthzToken>;
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;IAO3B,MAAM,CAAC,OAAO,EAAE,OAAO;IAUvB,UAAU,CACf,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,iBAAsB,GAC7B,OAAO,CAAC,gBAAgB,CAAC;CAY5B"}
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"}
@@ -67,23 +67,37 @@ export class AuthorizationService {
67
67
  return (_getRequestBearer(request) ??
68
68
  _getRequestCookie(request, this.options.cookieName));
69
69
  }
70
- async assert(request) {
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
- return verified;
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
- const { userId, scope } = verified;
82
- if (userId === undefined)
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
- if (options.scope && !includesScope(scope, options.scope)) {
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
  });
@@ -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
- async assert(request: Request) {
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
- return verified;
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
- const { userId, scope } = verified;
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 (options.scope && !includesScope(scope, options.scope)) {
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 { userId, scope };
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
+ }
@@ -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): void;
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;CACjC;AAED,gBAAgB;AAChB,MAAM,WAAW,gBAAgB;IAChC,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;CAC7C;AAMD,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;IAO5C;;;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;CAYlC"}
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"}
@@ -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
- const response = await this.processMatches(request, this.findMatchingRoutes(request));
61
- this._middleware.forEach((fn) => fn(request, response));
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) {
@@ -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): void;
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
- const response = await this.processMatches(
99
+ let response = await this.processMatches(
92
100
  request,
93
101
  this.findMatchingRoutes(request),
94
102
  );
95
- this._middleware.forEach((fn) => fn(request, response));
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,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,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,gBAAgB,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
@@ -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
@@ -23,5 +23,5 @@
23
23
  "import": "./source/*.js"
24
24
  }
25
25
  },
26
- "version": "0.7.0"
26
+ "version": "0.8.0"
27
27
  }
@@ -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,YAGrD;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"}
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"}
@@ -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
- // TODO: check this...
84
- return Readable.fromWeb(response.body);
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) {
@@ -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
- // TODO: check this...
105
- return Readable.fromWeb(response.body as any);
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 */