gruber 0.6.1 → 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 +36 -0
- package/README.md +121 -10
- package/core/authorization.d.ts +13 -1
- package/core/authorization.d.ts.map +1 -1
- package/core/authorization.js +29 -10
- package/core/authorization.test.js +73 -2
- package/core/authorization.ts +47 -11
- 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 +10 -0
- package/core/fetch-router.d.ts.map +1 -1
- package/core/fetch-router.js +17 -1
- package/core/fetch-router.ts +28 -1
- package/core/http.d.ts.map +1 -1
- package/core/http.js +3 -1
- package/core/http.ts +7 -1
- package/core/mod.d.ts +3 -1
- package/core/mod.d.ts.map +1 -1
- package/core/mod.js +3 -1
- package/core/mod.ts +3 -1
- package/core/server-sent-events.d.ts +50 -0
- package/core/server-sent-events.d.ts.map +1 -0
- package/core/server-sent-events.js +75 -0
- package/core/server-sent-events.ts +112 -0
- package/core/store.d.ts +6 -7
- package/core/store.d.ts.map +1 -1
- package/core/store.ts +9 -11
- 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/core/types.d.ts +7 -0
- package/core/types.d.ts.map +1 -1
- package/core/types.js +0 -1
- package/core/types.ts +6 -4
- package/package.json +1 -1
- package/source/express-router.d.ts +2 -0
- package/source/express-router.d.ts.map +1 -1
- package/source/express-router.js +6 -0
- package/source/express-router.ts +7 -0
- package/source/koa-router.d.ts +2 -0
- package/source/koa-router.d.ts.map +1 -1
- package/source/koa-router.js +6 -0
- package/source/koa-router.ts +7 -0
- package/source/node-router.d.ts +1 -1
- package/source/node-router.d.ts.map +1 -1
- package/source/node-router.js +12 -5
- package/source/node-router.ts +14 -5
- package/source/postgres.d.ts +1 -1
- package/source/postgres.d.ts.map +1 -1
- package/source/postgres.js +4 -3
- package/source/postgres.ts +5 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,42 @@
|
|
|
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
|
+
|
|
19
|
+
## 0.7.0
|
|
20
|
+
|
|
21
|
+
**new**
|
|
22
|
+
|
|
23
|
+
- Add Server-sent events utilities
|
|
24
|
+
- Add experimental `log` option to `FetchRouter`
|
|
25
|
+
- Add experimental `expressMiddleware` and `koaMiddleware`
|
|
26
|
+
|
|
27
|
+
**fixes**
|
|
28
|
+
|
|
29
|
+
- Fix malformed node HTTP headers when they contain a comma
|
|
30
|
+
- Ignore invalid cookies rather than throw an error
|
|
31
|
+
- `getRequestBody` also checks for `multipart/form-data`
|
|
32
|
+
- node: `request.signal` is now triggered if the request is cancelled
|
|
33
|
+
|
|
34
|
+
## 0.6.2
|
|
35
|
+
|
|
36
|
+
**fixes**
|
|
37
|
+
|
|
38
|
+
- 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.
|
|
39
|
+
- Experimental stores use dependency types too
|
|
40
|
+
|
|
5
41
|
## 0.6.1
|
|
6
42
|
|
|
7
43
|
**new**
|
package/README.md
CHANGED
|
@@ -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
|
-
###
|
|
729
|
+
### HTTP
|
|
730
730
|
|
|
731
731
|
#### defineRoute
|
|
732
732
|
|
|
@@ -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
|
|
@@ -1144,7 +1198,7 @@ The idea is you might check for `user:books:write` inside a request handler agai
|
|
|
1144
1198
|
|
|
1145
1199
|
### Authentication
|
|
1146
1200
|
|
|
1147
|
-
> UNSTABLE
|
|
1201
|
+
> VERY UNSTABLE
|
|
1148
1202
|
|
|
1149
1203
|
Authentication provides a service to help users get authorization to use the application.
|
|
1150
1204
|
|
|
@@ -1183,6 +1237,56 @@ const { token, headers, redirect } = await authn.finish(login);
|
|
|
1183
1237
|
These would obviously be spread accross multiple endpoints and you transfer
|
|
1184
1238
|
the token / code combination to the user in a way that proves they are who they claim to be.
|
|
1185
1239
|
|
|
1240
|
+
### Server Sent Events
|
|
1241
|
+
|
|
1242
|
+
Gruber includes utilities for sending [Server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) from regular route definitions.
|
|
1243
|
+
|
|
1244
|
+
```ts
|
|
1245
|
+
import { defineRoute, ServerSentEventStream, sseHeaders } from "gruber";
|
|
1246
|
+
|
|
1247
|
+
const stream = defineRoute({
|
|
1248
|
+
method: "GET",
|
|
1249
|
+
pathname: "/stream",
|
|
1250
|
+
async handler({ request }) {
|
|
1251
|
+
let counter = 0;
|
|
1252
|
+
let timerId = null;
|
|
1253
|
+
|
|
1254
|
+
// Create a stream to pipe data to the response,
|
|
1255
|
+
// it sends an incrementing counter every second
|
|
1256
|
+
const stream = new ReadableStream({
|
|
1257
|
+
start(controller) {
|
|
1258
|
+
timerId = setInterval(() => {
|
|
1259
|
+
counter++;
|
|
1260
|
+
controller.enqueue({ data: JSON.stringify({ counter }) });
|
|
1261
|
+
}, 1_000);
|
|
1262
|
+
},
|
|
1263
|
+
cancel() {
|
|
1264
|
+
if (timerId) clearInterval(timerId);
|
|
1265
|
+
},
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
// Create a response that transforms the stream into an SSE body
|
|
1269
|
+
return new Response(stream.pipeThrough(new ServerSentEventStream()), {
|
|
1270
|
+
headers: {
|
|
1271
|
+
"content-type": "text/event-stream",
|
|
1272
|
+
"cache-control": "no-cache",
|
|
1273
|
+
connection: "keep-alive",
|
|
1274
|
+
},
|
|
1275
|
+
});
|
|
1276
|
+
},
|
|
1277
|
+
});
|
|
1278
|
+
```
|
|
1279
|
+
|
|
1280
|
+
> You might want to use [ReadableStream.from](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/from_static) to create the stream
|
|
1281
|
+
|
|
1282
|
+
#### ServerSentEventMessage
|
|
1283
|
+
|
|
1284
|
+
`ServerSentEventMessage` is an interface for the payload to be delivered to the client.
|
|
1285
|
+
|
|
1286
|
+
#### ServerSentEventStream
|
|
1287
|
+
|
|
1288
|
+
`ServerSentEventStream` is a [TransformStream]() that converts `ServerSentEventMessage` into the raw bytes to send to a client.
|
|
1289
|
+
|
|
1186
1290
|
### Utilities
|
|
1187
1291
|
|
|
1188
1292
|
#### loader
|
|
@@ -1470,11 +1574,18 @@ const server = http.createServer((req) => {
|
|
|
1470
1574
|
`getResponseReadable` creates a [streams:Readable](https://nodejs.org/api/stream.html#class-streamreadable) from the body of a fetch Response.
|
|
1471
1575
|
|
|
1472
1576
|
```js
|
|
1577
|
+
import http from "node:http";
|
|
1473
1578
|
import { getResponseReadable } from "gruber/node-router.js";
|
|
1474
1579
|
|
|
1475
|
-
const
|
|
1580
|
+
const server = http.createServer((req, res) => {
|
|
1581
|
+
const readable = getResponseReadable(new Response("some body"), res);
|
|
1582
|
+
});
|
|
1476
1583
|
```
|
|
1477
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
|
+
|
|
1478
1589
|
## Development
|
|
1479
1590
|
|
|
1480
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,
|
|
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
|
@@ -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
|
-
|
|
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]
|
|
@@ -62,23 +67,37 @@ export class AuthorizationService {
|
|
|
62
67
|
return (_getRequestBearer(request) ??
|
|
63
68
|
_getRequestCookie(request, this.options.cookieName));
|
|
64
69
|
}
|
|
65
|
-
|
|
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 = {}) {
|
|
66
84
|
const authz = this.getAuthorization(request);
|
|
67
85
|
if (!authz)
|
|
68
86
|
throw HTTPError.unauthorized("no authorization present");
|
|
69
87
|
const verified = await this.tokens.verify(authz);
|
|
70
88
|
if (!verified)
|
|
71
89
|
throw HTTPError.unauthorized("no valid authorization");
|
|
72
|
-
|
|
90
|
+
if (options.scope && !includesScope(verified.scope, options.scope)) {
|
|
91
|
+
throw HTTPError.unauthorized("missing required scope: " + options.scope);
|
|
92
|
+
}
|
|
93
|
+
return this._processToken(verified);
|
|
73
94
|
}
|
|
74
95
|
async assertUser(request, options = {}) {
|
|
75
|
-
const verified = await this.assert(request
|
|
76
|
-
|
|
77
|
-
|
|
96
|
+
const verified = await this.assert(request, {
|
|
97
|
+
scope: options.scope,
|
|
98
|
+
});
|
|
99
|
+
if (verified.kind !== "user")
|
|
78
100
|
throw HTTPError.unauthorized("not a user");
|
|
79
|
-
|
|
80
|
-
throw HTTPError.unauthorized("missing required scope: " + options.scope);
|
|
81
|
-
}
|
|
82
|
-
return { userId, scope };
|
|
101
|
+
return verified;
|
|
83
102
|
}
|
|
84
103
|
}
|
|
@@ -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", () => {
|
|
@@ -124,13 +135,59 @@ describe("AuthorizationService", () => {
|
|
|
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("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
|
+
|
|
182
|
+
describe("assert", () => {
|
|
127
183
|
it("parses bearer", async () => {
|
|
128
184
|
const { authz } = setup();
|
|
129
185
|
|
|
130
186
|
const request = new Request("https://example.com", {
|
|
131
187
|
headers: { Authorization: 'Bearer {"scope":"user","userId":1}' },
|
|
132
188
|
});
|
|
133
|
-
assertEquals(await authz.
|
|
189
|
+
assertEquals(await authz.assert(request), {
|
|
190
|
+
kind: "user",
|
|
134
191
|
scope: "user",
|
|
135
192
|
userId: 1,
|
|
136
193
|
});
|
|
@@ -141,11 +198,23 @@ describe("AuthorizationService", () => {
|
|
|
141
198
|
const request = new Request("https://example.com", {
|
|
142
199
|
headers: { Cookie: 'testing_session={"scope":"user","userId":1}' },
|
|
143
200
|
});
|
|
144
|
-
assertEquals(await authz.
|
|
201
|
+
assertEquals(await authz.assert(request), {
|
|
202
|
+
kind: "user",
|
|
145
203
|
scope: "user",
|
|
146
204
|
userId: 1,
|
|
147
205
|
});
|
|
148
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
|
+
});
|
|
149
218
|
});
|
|
150
219
|
|
|
151
220
|
describe("assertUser", () => {
|
|
@@ -155,6 +224,7 @@ describe("AuthorizationService", () => {
|
|
|
155
224
|
headers: { Authorization: 'Bearer {"scope":"user","userId":1}' },
|
|
156
225
|
});
|
|
157
226
|
assertEquals(await authz.assertUser(request, { scope: "user" }), {
|
|
227
|
+
kind: "user",
|
|
158
228
|
userId: 1,
|
|
159
229
|
scope: "user",
|
|
160
230
|
});
|
|
@@ -165,6 +235,7 @@ describe("AuthorizationService", () => {
|
|
|
165
235
|
headers: { Cookie: 'testing_session={"scope":"user","userId":1}' },
|
|
166
236
|
});
|
|
167
237
|
assertEquals(await authz.assertUser(request, { scope: "user" }), {
|
|
238
|
+
kind: "user",
|
|
168
239
|
userId: 1,
|
|
169
240
|
scope: "user",
|
|
170
241
|
});
|
package/core/authorization.ts
CHANGED
|
@@ -29,7 +29,11 @@ export function _getRequestBearer(request: Request) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export function _getRequestCookie(request: Request, cookieName: string) {
|
|
32
|
-
|
|
32
|
+
try {
|
|
33
|
+
return _getCookies(request.headers)[cookieName] ?? null;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
/**
|
|
@@ -57,16 +61,28 @@ export function includesScope(actual: string, expected: string) {
|
|
|
57
61
|
return _checkScope(actual, _expandScopes(expected));
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
export interface AssertOptions {
|
|
65
|
+
scope?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
60
68
|
export interface AssertUserOptions {
|
|
61
69
|
scope?: string;
|
|
62
70
|
}
|
|
63
71
|
|
|
64
72
|
// NOTE: should userId be a string for future-proofing / to align to JWTs?
|
|
65
73
|
export interface AssertUserResult {
|
|
74
|
+
kind: "user";
|
|
66
75
|
userId: number;
|
|
67
76
|
scope: string;
|
|
68
77
|
}
|
|
69
78
|
|
|
79
|
+
export interface AssertServiceResult {
|
|
80
|
+
kind: "service";
|
|
81
|
+
scope: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type AuthorizationResult = AssertUserResult | AssertServiceResult;
|
|
85
|
+
|
|
70
86
|
export interface AbstractAuthorizationService {
|
|
71
87
|
getAuthorization(request: Request): string | null;
|
|
72
88
|
assert(request: Request): Promise<AuthzToken>;
|
|
@@ -94,29 +110,49 @@ export class AuthorizationService implements AbstractAuthorizationService {
|
|
|
94
110
|
);
|
|
95
111
|
}
|
|
96
112
|
|
|
97
|
-
|
|
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> {
|
|
98
132
|
const authz = this.getAuthorization(request);
|
|
99
133
|
|
|
100
134
|
if (!authz) throw HTTPError.unauthorized("no authorization present");
|
|
101
135
|
|
|
102
136
|
const verified = await this.tokens.verify(authz);
|
|
103
137
|
if (!verified) throw HTTPError.unauthorized("no valid authorization");
|
|
104
|
-
|
|
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);
|
|
105
144
|
}
|
|
106
145
|
|
|
107
146
|
async assertUser(
|
|
108
147
|
request: Request,
|
|
109
148
|
options: AssertUserOptions = {},
|
|
110
149
|
): Promise<AssertUserResult> {
|
|
111
|
-
const verified = await this.assert(request
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (userId === undefined) throw HTTPError.unauthorized("not a user");
|
|
150
|
+
const verified = await this.assert(request, {
|
|
151
|
+
scope: options.scope,
|
|
152
|
+
});
|
|
115
153
|
|
|
116
|
-
if (
|
|
117
|
-
throw HTTPError.unauthorized("missing required scope: " + options.scope);
|
|
118
|
-
}
|
|
154
|
+
if (verified.kind !== "user") throw HTTPError.unauthorized("not a user");
|
|
119
155
|
|
|
120
|
-
return
|
|
156
|
+
return verified;
|
|
121
157
|
}
|
|
122
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
|
+
}
|