gruber 0.6.0 → 0.7.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +45 -2
  2. package/README.md +106 -18
  3. package/core/authentication.d.ts +5 -5
  4. package/core/authentication.d.ts.map +1 -1
  5. package/core/authentication.js +8 -8
  6. package/core/authentication.test.js +11 -8
  7. package/core/authentication.ts +11 -8
  8. package/core/authorization.d.ts +8 -5
  9. package/core/authorization.d.ts.map +1 -1
  10. package/core/authorization.js +21 -10
  11. package/core/authorization.test.js +36 -6
  12. package/core/authorization.ts +26 -15
  13. package/core/fetch-router.d.ts +7 -0
  14. package/core/fetch-router.d.ts.map +1 -1
  15. package/core/fetch-router.js +11 -1
  16. package/core/fetch-router.ts +18 -1
  17. package/core/http.d.ts.map +1 -1
  18. package/core/http.js +3 -1
  19. package/core/http.ts +7 -1
  20. package/core/mod.d.ts +2 -1
  21. package/core/mod.d.ts.map +1 -1
  22. package/core/mod.js +2 -1
  23. package/core/mod.ts +2 -1
  24. package/core/postgres.d.ts +10 -6
  25. package/core/postgres.d.ts.map +1 -1
  26. package/core/postgres.ts +13 -8
  27. package/core/server-sent-events.d.ts +50 -0
  28. package/core/server-sent-events.d.ts.map +1 -0
  29. package/core/server-sent-events.js +75 -0
  30. package/core/server-sent-events.ts +112 -0
  31. package/core/store.d.ts +10 -11
  32. package/core/store.d.ts.map +1 -1
  33. package/core/store.js +9 -9
  34. package/core/store.ts +20 -26
  35. package/core/terminator.d.ts +3 -2
  36. package/core/terminator.d.ts.map +1 -1
  37. package/core/terminator.js +6 -2
  38. package/core/terminator.test.js +14 -13
  39. package/core/terminator.ts +8 -3
  40. package/core/test-deps.js +2 -2
  41. package/core/tokens.d.ts +28 -0
  42. package/core/tokens.d.ts.map +1 -0
  43. package/core/{jwt.js → tokens.js} +5 -11
  44. package/core/{jwt.ts → tokens.ts} +13 -24
  45. package/core/types.d.ts +39 -0
  46. package/core/types.d.ts.map +1 -1
  47. package/core/types.js +0 -1
  48. package/core/types.ts +45 -2
  49. package/package.json +1 -1
  50. package/source/express-router.d.ts +5 -2
  51. package/source/express-router.d.ts.map +1 -1
  52. package/source/express-router.js +15 -12
  53. package/source/express-router.ts +18 -13
  54. package/source/koa-router.d.ts +3 -0
  55. package/source/koa-router.d.ts.map +1 -1
  56. package/source/koa-router.js +13 -1
  57. package/source/koa-router.ts +15 -1
  58. package/source/node-router.d.ts.map +1 -1
  59. package/source/node-router.js +5 -4
  60. package/source/node-router.ts +6 -5
  61. package/source/postgres.d.ts +4 -4
  62. package/source/postgres.d.ts.map +1 -1
  63. package/source/postgres.js +4 -3
  64. package/source/postgres.ts +7 -6
  65. package/source/terminator.d.ts.map +1 -1
  66. package/source/terminator.js +1 -4
  67. package/source/terminator.ts +1 -4
  68. package/core/jwt.d.ts +0 -33
  69. package/core/jwt.d.ts.map +0 -1
package/CHANGELOG.md CHANGED
@@ -2,9 +2,52 @@
2
2
 
3
3
  This file documents notable changes to the project
4
4
 
5
- ## next
5
+ ## 0.7.0
6
6
 
7
- ...
7
+ **new**
8
+
9
+ - Add Server-sent events utilities
10
+ - Add experimental `log` option to `FetchRouter`
11
+ - Add experimental `expressMiddleware` and `koaMiddleware`
12
+
13
+ **fixes**
14
+
15
+ - Fix malformed node HTTP headers when they contain a comma
16
+ - Ignore invalid cookies rather than throw an error
17
+ - `getRequestBody` also checks for `multipart/form-data`
18
+ - node: `request.signal` is now triggered if the request is cancelled
19
+
20
+ ## 0.6.2
21
+
22
+ **fixes**
23
+
24
+ - Use `unknown` type when expecting a dependency, its now on the consumer to make sure they pass the correct thing. TypeScript-ing this is too hard.
25
+ - Experimental stores use dependency types too
26
+
27
+ ## 0.6.1
28
+
29
+ **new**
30
+
31
+ - Expose `includesScope` to check scopes
32
+
33
+ **fixes**
34
+
35
+ - Node & Express handle streams properly, they write the head then stream the response.
36
+ - Log HTTP errors in `ExpressRouter` and `KoaRoater`
37
+ - Took out type dependencies
38
+ - JWTs have the correct expiration time
39
+
40
+ **changed**
41
+
42
+ - Renamed `formatCode` to `formatAuthenticationCode`
43
+
44
+ **unstable**
45
+
46
+ - Renamed unstable `expireAfter` to `maxAge`
47
+ - Renamed unstable `JWTService` to `TokenService`
48
+ - Renamed unstable `JoseJwtService` to `JoseTokens`
49
+ - Renamed unstable `Authorization#getAuthorization` to `Authorization#assert`
50
+ - Added unstable `Authorization#getAuthorization`
8
51
 
9
52
  ## 0.6.0
10
53
 
package/README.md CHANGED
@@ -172,7 +172,7 @@ export async function runServer(options) {
172
172
  If you were using Deno, the same server would look like:
173
173
 
174
174
  ```js
175
- import { DenoRouter } from "@gruber/mod.js";
175
+ import { DenoRouter } from "gruber/mod.js";
176
176
 
177
177
  import helloRoute from "./hello-route.js";
178
178
 
@@ -229,13 +229,13 @@ or if it is terminating existing connections ready to be descheduled.
229
229
  Terminator is here to help with this use-case.
230
230
 
231
231
  ```js
232
- import { DenoRouter, Terminator } from "@gruber/mod.js";
232
+ import { DenoRouter, getTerminator } from "gruber";
233
233
 
234
234
  import { appConfig } from "./config.js";
235
235
  import helloRoute from "./hello-route.js";
236
236
 
237
237
  // 1. Create your Terminator, maybe call them arnie
238
- export const terminator = new Terminator({
238
+ export const terminator = getTerminator({
239
239
  timeout: appConfig.env === "development" ? 0 : 5_000,
240
240
  });
241
241
 
@@ -726,7 +726,7 @@ TODO: I'm not happy with this, will need to come back to it.
726
726
 
727
727
  ## Core library
728
728
 
729
- ### http
729
+ ### HTTP
730
730
 
731
731
  #### defineRoute
732
732
 
@@ -989,9 +989,9 @@ Terminator helps you gracefully deploy servers with zero downtime when using a l
989
989
  import { Terminator } from "gruber/terminator.js";
990
990
 
991
991
  // All options are optional, these are also the defaults
992
- const arnie = new Terminator({
992
+ const arnie = getTerminator({
993
993
  signals: ["SIGINT", "SIGTERM"],
994
- timeout: 5_000,
994
+ timeout: 5_000, // perhaps: appConfig.env === 'development' ? 0 : 5_000
995
995
  });
996
996
 
997
997
  // Generate a HTTP response based on the current state of the Terminator
@@ -1035,7 +1035,7 @@ await store.set("/some/key", { name: "Geoff Testington", age: 42 });
1035
1035
  await store.set(
1036
1036
  "/login/55",
1037
1037
  { token: "abcdef" },
1038
- { expireAfter: 30 * 1_000 /* 30 seconds */ },
1038
+ { maxAge: 30 * 1_000 /* 30 seconds */ },
1039
1039
  );
1040
1040
 
1041
1041
  // Retrieve geoff, types optional
@@ -1045,7 +1045,7 @@ const geoff = await store.get<GeoffRecord>("/some/key");
1045
1045
  await store.delete("/some/key");
1046
1046
  ```
1047
1047
 
1048
- The store is meant for temporary resources, so its mostly meant to be called with the `expireAfter` option
1048
+ The store is meant for temporary resources, so its mostly meant to be called with the `maxAge` option
1049
1049
 
1050
1050
  > The `MemoryStore` is also useful for testing, you can provide a TimerService to mock time
1051
1051
 
@@ -1063,6 +1063,34 @@ const redis: RedisClientType;
1063
1063
  const store = new RedisStore(redis, { prefix: "/v2" });
1064
1064
  ```
1065
1065
 
1066
+ ### JWT
1067
+
1068
+ An abstraction around signing a JWT for a user with an access scope.
1069
+ There is currently one implementation using [jose](https://github.com/panva/jose).
1070
+
1071
+ ```ts
1072
+ import { JoseJwtService } from "gruber";
1073
+ import * as jose from "jose";
1074
+
1075
+ const jwt = new JoseJwtService(
1076
+ {
1077
+ secret: "top_secret",
1078
+ issuer: "myapp.io",
1079
+ audience: "myapp.io",
1080
+ },
1081
+ jose,
1082
+ );
1083
+
1084
+ // string
1085
+ const token = await jwt.sign("user:books:read", {
1086
+ userId: 1,
1087
+ maxAge: 30 * 24 * 60 * 60 * 1_000, // 30 days
1088
+ });
1089
+
1090
+ // { userId, scope } or null
1091
+ const parsed = await jwt.verify(token);
1092
+ ```
1093
+
1066
1094
  ### Authorization
1067
1095
 
1068
1096
  > UNSTABLE
@@ -1070,29 +1098,38 @@ const store = new RedisStore(redis, { prefix: "/v2" });
1070
1098
  A module for checking Request objects have authorization to perform actions on the server
1071
1099
 
1072
1100
  ```ts
1073
- import { JWTService, AuthorizationService } from "gruber";
1101
+ import { TokenService, AuthorizationService, includesScope } from "gruber";
1074
1102
 
1075
- const jwt: JWTService;
1076
- const authz = new AuthorizationService({ cookieName: "my_session" }, jwt);
1103
+ const tokens: TokenService;
1104
+ const authz = new AuthorizationService({ cookieName: "my_session" }, tokens);
1077
1105
 
1078
- const { userId, scope } = await authz.getAuthorization(
1106
+ // string | null
1107
+ const token = authz.getAuthorization(
1079
1108
  new Request("https://example.com", {
1080
1109
  headers: { Authorization: "Bearer some-long-secure-token" },
1081
1110
  }),
1082
1111
  );
1083
1112
 
1084
- const { userId, scope } = await authz.getAuthorization(
1113
+ // { userId: number | undefined, scope: string }
1114
+ const { userId, scope } = await authz.assert(
1085
1115
  new Request("https://example.com", {
1086
- headers: { Cookie: "my_session=some-long-secure-token" },
1116
+ headers: { Authorization: "Bearer some-long-secure-token" },
1087
1117
  }),
1088
1118
  );
1089
1119
 
1120
+ // { userId: number, scope: string }
1090
1121
  const { userId, scope } = await authz.assertUser(
1091
- "user:books:read",
1092
1122
  new Request("https://example.com", {
1093
- headers: { Authorization: "Bearer some-long-secure-token" },
1123
+ headers: { Cookie: "my_session=some-long-secure-token" },
1094
1124
  }),
1125
+ { scope: "user:books:read" },
1095
1126
  );
1127
+
1128
+ includesScope("user:books:read", "user:books:read"); // true
1129
+ includesScope("user:books", "user:books:read"); // true
1130
+ includesScope("user", "user:books:read"); // true
1131
+ includesScope("user", "user:podcasts"); // true
1132
+ includesScope("user:books", "user:podcasts"); // false
1096
1133
  ```
1097
1134
 
1098
1135
  Any of these methods will throw a `HTTPError.unauthorized` (a 401) if the authorization is not present or invalid.
@@ -1107,7 +1144,7 @@ The idea is you might check for `user:books:write` inside a request handler agai
1107
1144
 
1108
1145
  ### Authentication
1109
1146
 
1110
- > UNSTABLE
1147
+ > VERY UNSTABLE
1111
1148
 
1112
1149
  Authentication provides a service to help users get authorization to use the application.
1113
1150
 
@@ -1146,6 +1183,56 @@ const { token, headers, redirect } = await authn.finish(login);
1146
1183
  These would obviously be spread accross multiple endpoints and you transfer
1147
1184
  the token / code combination to the user in a way that proves they are who they claim to be.
1148
1185
 
1186
+ ### Server Sent Events
1187
+
1188
+ Gruber includes utilities for sending [Server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) from regular route definitions.
1189
+
1190
+ ```ts
1191
+ import { defineRoute, ServerSentEventStream, sseHeaders } from "gruber";
1192
+
1193
+ const stream = defineRoute({
1194
+ method: "GET",
1195
+ pathname: "/stream",
1196
+ async handler({ request }) {
1197
+ let counter = 0;
1198
+ let timerId = null;
1199
+
1200
+ // Create a stream to pipe data to the response,
1201
+ // it sends an incrementing counter every second
1202
+ const stream = new ReadableStream({
1203
+ start(controller) {
1204
+ timerId = setInterval(() => {
1205
+ counter++;
1206
+ controller.enqueue({ data: JSON.stringify({ counter }) });
1207
+ }, 1_000);
1208
+ },
1209
+ cancel() {
1210
+ if (timerId) clearInterval(timerId);
1211
+ },
1212
+ });
1213
+
1214
+ // Create a response that transforms the stream into an SSE body
1215
+ return new Response(stream.pipeThrough(new ServerSentEventStream()), {
1216
+ headers: {
1217
+ "content-type": "text/event-stream",
1218
+ "cache-control": "no-cache",
1219
+ connection: "keep-alive",
1220
+ },
1221
+ });
1222
+ },
1223
+ });
1224
+ ```
1225
+
1226
+ > You might want to use [ReadableStream.from](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/from_static) to create the stream
1227
+
1228
+ #### ServerSentEventMessage
1229
+
1230
+ `ServerSentEventMessage` is an interface for the payload to be delivered to the client.
1231
+
1232
+ #### ServerSentEventStream
1233
+
1234
+ `ServerSentEventStream` is a [TransformStream]() that converts `ServerSentEventMessage` into the raw bytes to send to a client.
1235
+
1149
1236
  ### Utilities
1150
1237
 
1151
1238
  #### loader
@@ -1477,6 +1564,7 @@ export function loader<T>(handler: Loader<T>): Loader<T> {
1477
1564
 
1478
1565
  ```js
1479
1566
  async function retryWithBackoff({
1567
+ timers = window,
1480
1568
  maxRetries = 20,
1481
1569
  interval = 1_000,
1482
1570
  handler,
@@ -1486,7 +1574,7 @@ async function retryWithBackoff({
1486
1574
  const result = await handler();
1487
1575
  return result;
1488
1576
  } catch {
1489
- await new Promise((r) => setTimeout(r, i * interval));
1577
+ await new Promise((r) => timers.setTimeout(r, i * interval));
1490
1578
  }
1491
1579
  }
1492
1580
  console.error("Could not connect to database");
@@ -1,6 +1,6 @@
1
1
  import { RandomService } from "./random.ts";
2
2
  import { Store } from "./store.ts";
3
- import { JWTService } from "./jwt.ts";
3
+ import { TokenService } from "./tokens.ts";
4
4
  /**
5
5
  * An in-progress authentication, being stored while the client completes their challenge
6
6
  */
@@ -26,11 +26,11 @@ export interface AuthnResult {
26
26
  redirect: string;
27
27
  }
28
28
  export interface AbstractAuthenticationService {
29
- check(token: string, code: number): Promise<AuthnRequest | null>;
29
+ check(token: string | undefined | null, code: string | number | undefined | null): Promise<AuthnRequest | null>;
30
30
  start(userId: number, redirectUrl: string | URL): Promise<AuthnCheck>;
31
31
  finish(request: AuthnRequest): Promise<AuthnResult>;
32
32
  }
33
- export declare function formatCode(code: number): string;
33
+ export declare function formatAuthenticationCode(code: number): string;
34
34
  export interface AuthenticationServiceOptions {
35
35
  allowedHosts: () => URL[] | Promise<URL[]>;
36
36
  cookieName: string;
@@ -41,8 +41,8 @@ export declare class AuthenticationService implements AbstractAuthenticationServ
41
41
  options: AuthenticationServiceOptions;
42
42
  store: Store;
43
43
  random: RandomService;
44
- jwt: JWTService;
45
- constructor(options: AuthenticationServiceOptions, store: Store, random: RandomService, jwt: JWTService);
44
+ tokens: TokenService;
45
+ constructor(options: AuthenticationServiceOptions, store: Store, random: RandomService, tokens: TokenService);
46
46
  _canRedirect(input: string | URL, hosts: URL[]): boolean;
47
47
  check(token: string | undefined | null, code: string | number | undefined | null): Promise<AuthnRequest | null>;
48
48
  start(userId: number, redirectUrl: string | URL): Promise<AuthnCheck>;
@@ -1 +1 @@
1
- {"version":3,"file":"authentication.d.ts","sourceRoot":"","sources":["authentication.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAEtC;;GAEG;AACH,MAAM,WAAW,YAAY;IAC5B,MAAM,EAAE,MAAM,CAAC;IAEf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACb;AAKD;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACb;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,6BAA6B;IAC7C,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IACjE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACtE,MAAM,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CACpD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,UAKtC;AAED,MAAM,WAAW,4BAA4B;IAC5C,YAAY,EAAE,MAAM,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,mBAAmB,CAAC,aAAa,EAAE,MAAM,CAAC;IAC1C,mBAAmB,CAAC,eAAe,EAAE,MAAM,CAAC;CAC5C;AAED,qBAAa,qBAAsB,YAAW,6BAA6B;IAElE,OAAO,EAAE,4BAA4B;IACrC,KAAK,EAAE,KAAK;IACZ,MAAM,EAAE,aAAa;IACrB,GAAG,EAAE,UAAU;gBAHf,OAAO,EAAE,4BAA4B,EACrC,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,aAAa,EACrB,GAAG,EAAE,UAAU;IAOvB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE;IAcxC,KAAK,CACV,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EAChC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,IAAI,GACtC,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAkBzB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC,UAAU,CAAC;IAoBrE,MAAM,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;CAuBzD"}
1
+ {"version":3,"file":"authentication.d.ts","sourceRoot":"","sources":["authentication.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C;;GAEG;AACH,MAAM,WAAW,YAAY;IAC5B,MAAM,EAAE,MAAM,CAAC;IAEf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACb;AAKD;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACb;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,6BAA6B;IAC7C,KAAK,CACJ,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EAChC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,IAAI,GACtC,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAChC,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IACtE,MAAM,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CACpD;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,UAKpD;AAED,MAAM,WAAW,4BAA4B;IAC5C,YAAY,EAAE,MAAM,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,mBAAmB,CAAC,aAAa,EAAE,MAAM,CAAC;IAC1C,mBAAmB,CAAC,eAAe,EAAE,MAAM,CAAC;CAC5C;AAED,qBAAa,qBAAsB,YAAW,6BAA6B;IAElE,OAAO,EAAE,4BAA4B;IACrC,KAAK,EAAE,KAAK;IACZ,MAAM,EAAE,aAAa;IACrB,MAAM,EAAE,YAAY;gBAHpB,OAAO,EAAE,4BAA4B,EACrC,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,YAAY;IAO5B,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE;IAcxC,KAAK,CACV,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EAChC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,IAAI,GACtC,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAkBzB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC,UAAU,CAAC;IAoBrE,MAAM,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;CAuBzD"}
@@ -1,5 +1,5 @@
1
1
  import { HTTPError } from "./http.js";
2
- export function formatCode(code) {
2
+ export function formatAuthenticationCode(code) {
3
3
  return [
4
4
  code.toString().padStart(6, "0").slice(0, 3),
5
5
  code.toString().padStart(6, "0").slice(3, 6),
@@ -9,12 +9,12 @@ export class AuthenticationService {
9
9
  options;
10
10
  store;
11
11
  random;
12
- jwt;
13
- constructor(options, store, random, jwt) {
12
+ tokens;
13
+ constructor(options, store, random, tokens) {
14
14
  this.options = options;
15
15
  this.store = store;
16
16
  this.random = random;
17
- this.jwt = jwt;
17
+ this.tokens = tokens;
18
18
  }
19
19
  //
20
20
  // Internal
@@ -59,23 +59,23 @@ export class AuthenticationService {
59
59
  code: this.random.number(0, 999_999),
60
60
  };
61
61
  await this.store.set(`/authn/request/${token}`, request, {
62
- expireAfter: this.options.loginDuration,
62
+ maxAge: this.options.loginDuration,
63
63
  });
64
64
  return { token, code: request.code };
65
65
  }
66
66
  async finish(request) {
67
67
  const headers = new Headers();
68
68
  headers.set("Location", request.redirect);
69
- const token = await this.jwt.sign("user", {
69
+ const token = await this.tokens.sign("user", {
70
70
  userId: request.userId,
71
- expireAfter: this.options.sessionDuration,
71
+ maxAge: this.options.sessionDuration,
72
72
  });
73
73
  const duration = Math.floor(this.options.sessionDuration / 1000);
74
74
  // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
75
75
  headers.append("Set-Cookie", `${this.options.cookieName}=${token}; Max-Age=${duration}; Path=/; HttpOnly`);
76
76
  // TODO: Microsoft "safe links" opens URLs, generates auth then throws it away
77
77
  // Maybe it should be a counter? like 3 you get uses
78
- // await cache.delete(`/authn/request/${token}`)
78
+ // await cache.delete(`/authn/request/${request.token}`)
79
79
  return { token, headers, redirect: request.redirect };
80
80
  }
81
81
  }
@@ -1,17 +1,20 @@
1
1
  import {
2
2
  assertEquals,
3
3
  describe,
4
- fakeJwt,
4
+ fakeTokens,
5
5
  fakeRandom,
6
6
  fakeTimers,
7
7
  it,
8
8
  } from "./test-deps.js";
9
- import { AuthenticationService, formatCode } from "./authentication.ts";
9
+ import {
10
+ AuthenticationService,
11
+ formatAuthenticationCode,
12
+ } from "./authentication.ts";
10
13
  import { MemoryStore } from "./store.ts";
11
14
 
12
- describe("formatCode", () => {
15
+ describe("formatAuthenticationCode", () => {
13
16
  it("pads and seperates", () => {
14
- assertEquals(formatCode(12345), "012 345");
17
+ assertEquals(formatAuthenticationCode(12345), "012 345");
15
18
  });
16
19
  });
17
20
 
@@ -25,9 +28,9 @@ describe("AuthenticationService", () => {
25
28
  };
26
29
  const random = fakeRandom();
27
30
  const store = new MemoryStore(fakeTimers());
28
- const jwt = fakeJwt();
29
- const authn = new AuthenticationService(options, store, random, jwt);
30
- return { options, random, store, jwt, authn };
31
+ const tokens = fakeTokens();
32
+ const authn = new AuthenticationService(options, store, random, tokens);
33
+ return { options, random, store, tokens, authn };
31
34
  }
32
35
 
33
36
  describe("_canRedirect", () => {
@@ -126,7 +129,7 @@ describe("AuthenticationService", () => {
126
129
  const token = JSON.stringify({
127
130
  scope: "user",
128
131
  userId: 1,
129
- expireAfter: 20_000,
132
+ maxAge: 20_000,
130
133
  });
131
134
 
132
135
  assertEquals(result.token, token);
@@ -1,7 +1,7 @@
1
1
  import { HTTPError } from "./http.ts";
2
2
  import { RandomService } from "./random.ts";
3
3
  import { Store } from "./store.ts";
4
- import { JWTService } from "./jwt.ts";
4
+ import { TokenService } from "./tokens.ts";
5
5
 
6
6
  /**
7
7
  * An in-progress authentication, being stored while the client completes their challenge
@@ -35,12 +35,15 @@ export interface AuthnResult {
35
35
  }
36
36
 
37
37
  export interface AbstractAuthenticationService {
38
- check(token: string, code: number): Promise<AuthnRequest | null>;
38
+ check(
39
+ token: string | undefined | null,
40
+ code: string | number | undefined | null,
41
+ ): Promise<AuthnRequest | null>;
39
42
  start(userId: number, redirectUrl: string | URL): Promise<AuthnCheck>;
40
43
  finish(request: AuthnRequest): Promise<AuthnResult>;
41
44
  }
42
45
 
43
- export function formatCode(code: number) {
46
+ export function formatAuthenticationCode(code: number) {
44
47
  return [
45
48
  code.toString().padStart(6, "0").slice(0, 3),
46
49
  code.toString().padStart(6, "0").slice(3, 6),
@@ -59,7 +62,7 @@ export class AuthenticationService implements AbstractAuthenticationService {
59
62
  public options: AuthenticationServiceOptions,
60
63
  public store: Store,
61
64
  public random: RandomService,
62
- public jwt: JWTService,
65
+ public tokens: TokenService,
63
66
  ) {}
64
67
 
65
68
  //
@@ -115,7 +118,7 @@ export class AuthenticationService implements AbstractAuthenticationService {
115
118
  };
116
119
 
117
120
  await this.store.set<AuthnRequest>(`/authn/request/${token}`, request, {
118
- expireAfter: this.options.loginDuration,
121
+ maxAge: this.options.loginDuration,
119
122
  });
120
123
 
121
124
  return { token, code: request.code };
@@ -125,9 +128,9 @@ export class AuthenticationService implements AbstractAuthenticationService {
125
128
  const headers = new Headers();
126
129
  headers.set("Location", request.redirect);
127
130
 
128
- const token = await this.jwt.sign("user", {
131
+ const token = await this.tokens.sign("user", {
129
132
  userId: request.userId,
130
- expireAfter: this.options.sessionDuration,
133
+ maxAge: this.options.sessionDuration,
131
134
  });
132
135
 
133
136
  const duration = Math.floor(this.options.sessionDuration / 1000);
@@ -140,7 +143,7 @@ export class AuthenticationService implements AbstractAuthenticationService {
140
143
 
141
144
  // TODO: Microsoft "safe links" opens URLs, generates auth then throws it away
142
145
  // Maybe it should be a counter? like 3 you get uses
143
- // await cache.delete(`/authn/request/${token}`)
146
+ // await cache.delete(`/authn/request/${request.token}`)
144
147
 
145
148
  return { token, headers, redirect: request.redirect };
146
149
  }
@@ -1,4 +1,4 @@
1
- import { AuthzToken, JWTService } from "./jwt.ts";
1
+ import { AuthzToken, TokenService } from "./tokens.ts";
2
2
  /**
3
3
  * Based on deno std
4
4
  * https://github.com/denoland/std/blob/065296ca5a05a47f9741df8f99c32fae4f960070/http/cookie.ts#L254C1-L270C2
@@ -11,6 +11,7 @@ export declare function _getRequestCookie(request: Request, cookieName: string):
11
11
  */
12
12
  export declare function _expandScopes(scope: string): string[];
13
13
  export declare function _checkScope(actual: string, expected: string[]): boolean;
14
+ export declare function includesScope(actual: string, expected: string): boolean;
14
15
  export interface AssertUserOptions {
15
16
  scope?: string;
16
17
  }
@@ -19,7 +20,8 @@ export interface AssertUserResult {
19
20
  scope: string;
20
21
  }
21
22
  export interface AbstractAuthorizationService {
22
- getAuthorization(request: Request): Promise<AuthzToken>;
23
+ getAuthorization(request: Request): string | null;
24
+ assert(request: Request): Promise<AuthzToken>;
23
25
  assertUser(request: Request, options?: AssertUserOptions): Promise<AssertUserResult>;
24
26
  }
25
27
  export interface AuthorizationServiceOptions {
@@ -28,9 +30,10 @@ export interface AuthorizationServiceOptions {
28
30
  /** @unstable */
29
31
  export declare class AuthorizationService implements AbstractAuthorizationService {
30
32
  options: AuthorizationServiceOptions;
31
- jwt: JWTService;
32
- constructor(options: AuthorizationServiceOptions, jwt: JWTService);
33
- getAuthorization(request: Request): Promise<AuthzToken>;
33
+ tokens: TokenService;
34
+ constructor(options: AuthorizationServiceOptions, tokens: TokenService);
35
+ getAuthorization(request: Request): string | null;
36
+ assert(request: Request): Promise<AuthzToken>;
34
37
  assertUser(request: Request, options?: AssertUserOptions): Promise<AssertUserResult>;
35
38
  }
36
39
  //# 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,UAAU,EAAE,MAAM,UAAU,CAAC;AAElD;;;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,iBAErE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,YAQ1C;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,WAM7D;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,OAAO,CAAC,UAAU,CAAC,CAAC;IACxD,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;IACxE,OAAO,EAAE,2BAA2B,CAAC;IACrC,GAAG,EAAE,UAAU,CAAC;gBAEJ,OAAO,EAAE,2BAA2B,EAAE,GAAG,EAAE,UAAU;IAK3D,gBAAgB,CAAC,OAAO,EAAE,OAAO;IAYjC,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,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"}
@@ -24,7 +24,12 @@ export function _getRequestBearer(request) {
24
24
  return /^bearer (.+)$/i.exec(authz)?.[1] ?? null;
25
25
  }
26
26
  export function _getRequestCookie(request, cookieName) {
27
- return _getCookies(request.headers)[cookieName] ?? null;
27
+ try {
28
+ return _getCookies(request.headers)[cookieName] ?? null;
29
+ }
30
+ catch {
31
+ return null;
32
+ }
28
33
  }
29
34
  /**
30
35
  * a:b:c -> [a, a:b, a:b:c]
@@ -47,30 +52,36 @@ export function _checkScope(actual, expected) {
47
52
  }
48
53
  return false;
49
54
  }
55
+ export function includesScope(actual, expected) {
56
+ return _checkScope(actual, _expandScopes(expected));
57
+ }
50
58
  /** @unstable */
51
59
  export class AuthorizationService {
52
60
  options;
53
- jwt;
54
- constructor(options, jwt) {
61
+ tokens;
62
+ constructor(options, tokens) {
55
63
  this.options = options;
56
- this.jwt = jwt;
64
+ this.tokens = tokens;
65
+ }
66
+ getAuthorization(request) {
67
+ return (_getRequestBearer(request) ??
68
+ _getRequestCookie(request, this.options.cookieName));
57
69
  }
58
- async getAuthorization(request) {
59
- const authz = _getRequestBearer(request) ??
60
- _getRequestCookie(request, this.options.cookieName);
70
+ async assert(request) {
71
+ const authz = this.getAuthorization(request);
61
72
  if (!authz)
62
73
  throw HTTPError.unauthorized("no authorization present");
63
- const verified = await this.jwt.verify(authz);
74
+ const verified = await this.tokens.verify(authz);
64
75
  if (!verified)
65
76
  throw HTTPError.unauthorized("no valid authorization");
66
77
  return verified;
67
78
  }
68
79
  async assertUser(request, options = {}) {
69
- const verified = await this.getAuthorization(request);
80
+ const verified = await this.assert(request);
70
81
  const { userId, scope } = verified;
71
82
  if (userId === undefined)
72
83
  throw HTTPError.unauthorized("not a user");
73
- if (options.scope && !_checkScope(scope, _expandScopes(options.scope))) {
84
+ if (options.scope && !includesScope(scope, options.scope)) {
74
85
  throw HTTPError.unauthorized("missing required scope: " + options.scope);
75
86
  }
76
87
  return { userId, scope };
@@ -2,7 +2,7 @@ import {
2
2
  assertEquals,
3
3
  assertThrows,
4
4
  describe,
5
- fakeJwt,
5
+ fakeTokens,
6
6
  it,
7
7
  } from "./test-deps.js";
8
8
  import {
@@ -72,6 +72,17 @@ describe("_getRequestCookie", () => {
72
72
  "abcdef",
73
73
  );
74
74
  });
75
+ it("does not throw", () => {
76
+ assertEquals(
77
+ _getRequestCookie(
78
+ new Request("https://example.com", {
79
+ headers: { Cookie: "not_;a-cookie" },
80
+ }),
81
+ "my_cookie",
82
+ ),
83
+ null,
84
+ );
85
+ });
75
86
  });
76
87
 
77
88
  describe("_expandScopes", () => {
@@ -118,19 +129,38 @@ describe("_checkScope", () => {
118
129
  describe("AuthorizationService", () => {
119
130
  function setup() {
120
131
  const options = { cookieName: "testing_session" };
121
- const jwt = fakeJwt();
122
- const authz = new AuthorizationService(options, jwt);
123
- return { options, jwt, authz };
132
+ const tokens = fakeTokens();
133
+ const authz = new AuthorizationService(options, tokens);
134
+ return { options, tokens, authz };
124
135
  }
125
136
 
126
137
  describe("getAuthorization", () => {
138
+ it("parses bearer", () => {
139
+ const { authz } = setup();
140
+
141
+ const request = new Request("https://example.com", {
142
+ headers: { Authorization: "Bearer test_bearer_token" },
143
+ });
144
+ assertEquals(authz.getAuthorization(request), "test_bearer_token");
145
+ });
146
+ it("parses cookies", () => {
147
+ const { authz } = setup();
148
+
149
+ const request = new Request("https://example.com", {
150
+ headers: { Cookie: "testing_session=test_cookie_value" },
151
+ });
152
+ assertEquals(authz.getAuthorization(request), "test_cookie_value");
153
+ });
154
+ });
155
+
156
+ describe("assert", () => {
127
157
  it("parses bearer", async () => {
128
158
  const { authz } = setup();
129
159
 
130
160
  const request = new Request("https://example.com", {
131
161
  headers: { Authorization: 'Bearer {"scope":"user","userId":1}' },
132
162
  });
133
- assertEquals(await authz.getAuthorization(request), {
163
+ assertEquals(await authz.assert(request), {
134
164
  scope: "user",
135
165
  userId: 1,
136
166
  });
@@ -141,7 +171,7 @@ describe("AuthorizationService", () => {
141
171
  const request = new Request("https://example.com", {
142
172
  headers: { Cookie: 'testing_session={"scope":"user","userId":1}' },
143
173
  });
144
- assertEquals(await authz.getAuthorization(request), {
174
+ assertEquals(await authz.assert(request), {
145
175
  scope: "user",
146
176
  userId: 1,
147
177
  });