@decohere/http 0.0.1-dev.126 → 0.0.1-dev.5
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/README.md +2 -2
- package/package.json +4 -13
- package/src/app/app.ts +147 -0
- package/src/app/module.ts +19 -0
- package/src/auth/ability.ts +56 -0
- package/src/auth/auth.config.ts +34 -0
- package/src/auth/auth.extensions.ts +115 -0
- package/src/auth/auth.types.ts +124 -0
- package/src/auth/casl-authorization.service.ts +62 -0
- package/{types/src/auth/principal-provider.d.ts → src/auth/principal-provider.ts} +13 -5
- package/src/auth/safe-outbound-url.ts +117 -0
- package/src/auth/token-service.ts +192 -0
- package/src/builder/builder.ts +118 -0
- package/{es6/src/const.js → src/const.ts} +25 -17
- package/src/health/health.controller.ts +34 -0
- package/src/health/health.extensions.ts +53 -0
- package/src/health/readiness.service.ts +33 -0
- package/{types/src/health/readiness.types.d.ts → src/health/readiness.types.ts} +4 -8
- package/src/hosting/hosted-service-host.ts +102 -0
- package/src/hosting/hosted-service.ts +31 -0
- package/src/http/http.compression.ts +38 -0
- package/src/http/http.extensions.ts +244 -0
- package/{types/src/http/http.options.store.d.ts → src/http/http.options.store.ts} +11 -1
- package/src/http/http.options.ts +41 -0
- package/{types/src/http/http.security.d.ts → src/http/http.security.ts} +16 -10
- package/src/http/http.static.ts +359 -0
- package/{es6/src/index.js → src/index.ts} +1 -0
- package/src/openapi/index.ts +22 -0
- package/src/openapi/openapi.generator.ts +78 -0
- package/src/openapi/openapi.types.ts +60 -0
- package/src/openapi/request-schema.ts +155 -0
- package/src/openapi/request-validator.ts +95 -0
- package/src/providers/fastify.provider.ts +454 -0
- package/src/routing/anvil.types.ts +150 -0
- package/src/routing/decorators/argument.decorator.ts +33 -0
- package/src/routing/decorators/auth.decorator.ts +79 -0
- package/src/routing/decorators/controller.decorator.ts +9 -0
- package/src/routing/decorators/field.decorator.ts +15 -0
- package/src/routing/decorators/request.decorator.ts +34 -0
- package/src/routing/decorators/response.decorator.ts +18 -0
- package/src/routing/decorators/route.decorator.ts +52 -0
- package/src/routing/filters/exception-filter-host.ts +90 -0
- package/src/routing/filters/exception-filter.ts +29 -0
- package/src/routing/middleware/http-access-log.middleware.ts +36 -0
- package/src/routing/middleware/middleware.ts +26 -0
- package/src/routing/middleware/request-id.ts +18 -0
- package/src/routing/path.util.ts +38 -0
- package/src/routing/pipes/pipes.ts +132 -0
- package/src/routing/services/route-argument-resolver.ts +92 -0
- package/src/routing/services/route-registry.ts +229 -0
- package/src/routing/services/router.ts +166 -0
- package/src/sse/sse.decorator.ts +41 -0
- package/src/sse/sse.encoder.ts +86 -0
- package/src/sse/sse.types.ts +47 -0
- package/src/sse/sse.wire.ts +34 -0
- package/src/testing/boot-module.ts +63 -0
- package/src/testing/sse-reader.ts +120 -0
- package/tests/app.hosted-services.spec.ts +292 -0
- package/tests/app.shutdown.integration.spec.ts +160 -0
- package/tests/app.spec.ts +132 -0
- package/tests/auth.ability.spec.ts +48 -0
- package/tests/auth.decorators.spec.ts +20 -0
- package/tests/auth.fail-closed.pentest.spec.ts +77 -0
- package/tests/auth.integration.spec.ts +234 -0
- package/tests/auth.jwks-cache.spec.ts +52 -0
- package/tests/auth.missing-auth.pentest.spec.ts +56 -0
- package/tests/auth.oidc-ssrf.pentest.spec.ts +72 -0
- package/tests/auth.oidc.integration.spec.ts +161 -0
- package/tests/auth.token-service.spec.ts +251 -0
- package/tests/builder.spec.ts +12 -0
- package/tests/health.integration.spec.ts +111 -0
- package/tests/helpers/listen-ephemeral.ts +13 -0
- package/tests/helpers/local-oidc-server.ts +100 -0
- package/tests/http.access-log.integration.spec.ts +111 -0
- package/tests/http.boolean-query.integration.spec.ts +59 -0
- package/tests/http.compression.integration.spec.ts +604 -0
- package/tests/http.date-query.integration.spec.ts +71 -0
- package/tests/http.global-prefix.integration.spec.ts +284 -0
- package/tests/http.number-query.integration.spec.ts +65 -0
- package/tests/http.pino-roll-hmr.integration.spec.ts +79 -0
- package/tests/http.pipeline-order.integration.spec.ts +95 -0
- package/tests/http.request-cleanup.integration.spec.ts +57 -0
- package/tests/http.request-id-scope.pentest.spec.ts +50 -0
- package/tests/http.security.integration.spec.ts +106 -0
- package/tests/http.static.integration.spec.ts +847 -0
- package/tests/http.static.spec.ts +111 -0
- package/tests/http.surface.integration.spec.ts +167 -0
- package/tests/openapi.integration.spec.ts +193 -0
- package/tests/openapi.validator-cache.integration.spec.ts +55 -0
- package/tests/routing.decorators.spec.ts +15 -0
- package/tests/routing.registry.spec.ts +44 -0
- package/tests/routing.resolver.spec.ts +92 -0
- package/tests/sse.integration.spec.ts +421 -0
- package/tests/sse.wire.spec.ts +33 -0
- package/LICENSE +0 -15
- package/commonjs/src/app/app.js +0 -166
- package/commonjs/src/app/index.js +0 -19
- package/commonjs/src/app/module.js +0 -17
- package/commonjs/src/auth/ability.js +0 -54
- package/commonjs/src/auth/auth.config.js +0 -23
- package/commonjs/src/auth/auth.extensions.js +0 -106
- package/commonjs/src/auth/auth.types.js +0 -65
- package/commonjs/src/auth/casl-authorization.service.js +0 -36
- package/commonjs/src/auth/index.js +0 -23
- package/commonjs/src/auth/principal-provider.js +0 -19
- package/commonjs/src/auth/safe-outbound-url.js +0 -102
- package/commonjs/src/auth/token-service.js +0 -151
- package/commonjs/src/builder/builder.js +0 -95
- package/commonjs/src/builder/index.js +0 -18
- package/commonjs/src/const.js +0 -87
- package/commonjs/src/health/health.controller.js +0 -75
- package/commonjs/src/health/health.extensions.js +0 -50
- package/commonjs/src/health/index.js +0 -21
- package/commonjs/src/health/readiness.service.js +0 -67
- package/commonjs/src/health/readiness.types.js +0 -7
- package/commonjs/src/hosting/hosted-service-host.js +0 -88
- package/commonjs/src/hosting/hosted-service.js +0 -11
- package/commonjs/src/hosting/index.js +0 -19
- package/commonjs/src/http/http.compression.js +0 -27
- package/commonjs/src/http/http.extensions.js +0 -269
- package/commonjs/src/http/http.options.js +0 -26
- package/commonjs/src/http/http.options.store.js +0 -20
- package/commonjs/src/http/http.security.js +0 -27
- package/commonjs/src/http/http.static.js +0 -248
- package/commonjs/src/http/index.js +0 -22
- package/commonjs/src/index.js +0 -27
- package/commonjs/src/openapi/index.js +0 -45
- package/commonjs/src/openapi/openapi.generator.js +0 -103
- package/commonjs/src/openapi/openapi.types.js +0 -4
- package/commonjs/src/openapi/request-schema.js +0 -187
- package/commonjs/src/openapi/request-validator.js +0 -85
- package/commonjs/src/providers/fastify.provider.js +0 -391
- package/commonjs/src/providers/index.js +0 -18
- package/commonjs/src/routing/anvil.types.js +0 -99
- package/commonjs/src/routing/decorators/argument.decorator.js +0 -49
- package/commonjs/src/routing/decorators/auth.decorator.js +0 -70
- package/commonjs/src/routing/decorators/controller.decorator.js +0 -16
- package/commonjs/src/routing/decorators/field.decorator.js +0 -25
- package/commonjs/src/routing/decorators/index.js +0 -24
- package/commonjs/src/routing/decorators/request.decorator.js +0 -30
- package/commonjs/src/routing/decorators/response.decorator.js +0 -34
- package/commonjs/src/routing/decorators/route.decorator.js +0 -67
- package/commonjs/src/routing/filters/exception-filter-host.js +0 -145
- package/commonjs/src/routing/filters/exception-filter.js +0 -28
- package/commonjs/src/routing/index.js +0 -25
- package/commonjs/src/routing/middleware/http-access-log.middleware.js +0 -47
- package/commonjs/src/routing/middleware/middleware.js +0 -20
- package/commonjs/src/routing/middleware/request-id.js +0 -30
- package/commonjs/src/routing/path.util.js +0 -51
- package/commonjs/src/routing/pipes/index.js +0 -18
- package/commonjs/src/routing/pipes/pipes.js +0 -145
- package/commonjs/src/routing/services/index.js +0 -18
- package/commonjs/src/routing/services/route-argument-resolver.js +0 -77
- package/commonjs/src/routing/services/route-registry.js +0 -194
- package/commonjs/src/routing/services/router.js +0 -158
- package/commonjs/src/sse/index.js +0 -26
- package/commonjs/src/sse/sse.decorator.js +0 -42
- package/commonjs/src/sse/sse.encoder.js +0 -73
- package/commonjs/src/sse/sse.types.js +0 -36
- package/commonjs/src/sse/sse.wire.js +0 -40
- package/commonjs/src/testing/boot-module.js +0 -38
- package/commonjs/src/testing/index.js +0 -20
- package/commonjs/src/testing/sse-reader.js +0 -98
- package/es6/src/app/app.js +0 -148
- package/es6/src/app/index.js +0 -2
- package/es6/src/app/module.js +0 -7
- package/es6/src/auth/ability.js +0 -45
- package/es6/src/auth/auth.config.js +0 -19
- package/es6/src/auth/auth.extensions.js +0 -83
- package/es6/src/auth/auth.types.js +0 -35
- package/es6/src/auth/casl-authorization.service.js +0 -40
- package/es6/src/auth/index.js +0 -6
- package/es6/src/auth/principal-provider.js +0 -14
- package/es6/src/auth/safe-outbound-url.js +0 -99
- package/es6/src/auth/token-service.js +0 -141
- package/es6/src/builder/builder.js +0 -85
- package/es6/src/builder/index.js +0 -1
- package/es6/src/health/health.controller.js +0 -65
- package/es6/src/health/health.extensions.js +0 -40
- package/es6/src/health/index.js +0 -4
- package/es6/src/health/readiness.service.js +0 -57
- package/es6/src/health/readiness.types.js +0 -4
- package/es6/src/hosting/hosted-service-host.js +0 -81
- package/es6/src/hosting/hosted-service.js +0 -8
- package/es6/src/hosting/index.js +0 -2
- package/es6/src/http/http.compression.js +0 -9
- package/es6/src/http/http.extensions.js +0 -259
- package/es6/src/http/http.options.js +0 -16
- package/es6/src/http/http.options.store.js +0 -13
- package/es6/src/http/http.security.js +0 -9
- package/es6/src/http/http.static.js +0 -224
- package/es6/src/http/index.js +0 -5
- package/es6/src/openapi/index.js +0 -3
- package/es6/src/openapi/openapi.generator.js +0 -82
- package/es6/src/openapi/openapi.types.js +0 -1
- package/es6/src/openapi/request-schema.js +0 -160
- package/es6/src/openapi/request-validator.js +0 -71
- package/es6/src/providers/fastify.provider.js +0 -376
- package/es6/src/providers/index.js +0 -1
- package/es6/src/routing/anvil.types.js +0 -57
- package/es6/src/routing/decorators/argument.decorator.js +0 -28
- package/es6/src/routing/decorators/auth.decorator.js +0 -58
- package/es6/src/routing/decorators/controller.decorator.js +0 -6
- package/es6/src/routing/decorators/field.decorator.js +0 -15
- package/es6/src/routing/decorators/index.js +0 -7
- package/es6/src/routing/decorators/request.decorator.js +0 -24
- package/es6/src/routing/decorators/response.decorator.js +0 -16
- package/es6/src/routing/decorators/route.decorator.js +0 -37
- package/es6/src/routing/filters/exception-filter-host.js +0 -127
- package/es6/src/routing/filters/exception-filter.js +0 -10
- package/es6/src/routing/index.js +0 -8
- package/es6/src/routing/middleware/http-access-log.middleware.js +0 -37
- package/es6/src/routing/middleware/middleware.js +0 -10
- package/es6/src/routing/middleware/request-id.js +0 -14
- package/es6/src/routing/path.util.js +0 -33
- package/es6/src/routing/pipes/index.js +0 -1
- package/es6/src/routing/pipes/pipes.js +0 -118
- package/es6/src/routing/services/index.js +0 -1
- package/es6/src/routing/services/route-argument-resolver.js +0 -67
- package/es6/src/routing/services/route-registry.js +0 -184
- package/es6/src/routing/services/router.js +0 -148
- package/es6/src/sse/index.js +0 -2
- package/es6/src/sse/sse.decorator.js +0 -31
- package/es6/src/sse/sse.encoder.js +0 -67
- package/es6/src/sse/sse.types.js +0 -22
- package/es6/src/sse/sse.wire.js +0 -30
- package/es6/src/testing/boot-module.js +0 -35
- package/es6/src/testing/index.js +0 -2
- package/es6/src/testing/sse-reader.js +0 -91
- package/types/src/app/app.d.ts +0 -41
- package/types/src/app/module.d.ts +0 -15
- package/types/src/auth/ability.d.ts +0 -19
- package/types/src/auth/auth.config.d.ts +0 -28
- package/types/src/auth/auth.extensions.d.ts +0 -53
- package/types/src/auth/auth.types.d.ts +0 -73
- package/types/src/auth/casl-authorization.service.d.ts +0 -28
- package/types/src/auth/safe-outbound-url.d.ts +0 -9
- package/types/src/auth/token-service.d.ts +0 -31
- package/types/src/builder/builder.d.ts +0 -36
- package/types/src/const.d.ts +0 -32
- package/types/src/health/health.controller.d.ts +0 -16
- package/types/src/health/health.extensions.d.ts +0 -31
- package/types/src/health/readiness.service.d.ts +0 -9
- package/types/src/hosting/hosted-service-host.d.ts +0 -37
- package/types/src/hosting/hosted-service.d.ts +0 -31
- package/types/src/http/http.compression.d.ts +0 -32
- package/types/src/http/http.extensions.d.ts +0 -79
- package/types/src/http/http.options.d.ts +0 -39
- package/types/src/http/http.static.d.ts +0 -160
- package/types/src/index.d.ts +0 -10
- package/types/src/openapi/index.d.ts +0 -6
- package/types/src/openapi/openapi.generator.d.ts +0 -6
- package/types/src/openapi/openapi.types.d.ts +0 -58
- package/types/src/openapi/request-schema.d.ts +0 -21
- package/types/src/openapi/request-validator.d.ts +0 -9
- package/types/src/providers/fastify.provider.d.ts +0 -56
- package/types/src/routing/anvil.types.d.ts +0 -119
- package/types/src/routing/decorators/argument.decorator.d.ts +0 -11
- package/types/src/routing/decorators/auth.decorator.d.ts +0 -30
- package/types/src/routing/decorators/controller.decorator.d.ts +0 -3
- package/types/src/routing/decorators/field.decorator.d.ts +0 -2
- package/types/src/routing/decorators/request.decorator.d.ts +0 -8
- package/types/src/routing/decorators/response.decorator.d.ts +0 -4
- package/types/src/routing/decorators/route.decorator.d.ts +0 -7
- package/types/src/routing/filters/exception-filter-host.d.ts +0 -19
- package/types/src/routing/filters/exception-filter.d.ts +0 -16
- package/types/src/routing/middleware/http-access-log.middleware.d.ts +0 -8
- package/types/src/routing/middleware/middleware.d.ts +0 -10
- package/types/src/routing/middleware/request-id.d.ts +0 -6
- package/types/src/routing/path.util.d.ts +0 -9
- package/types/src/routing/pipes/pipes.d.ts +0 -21
- package/types/src/routing/services/route-argument-resolver.d.ts +0 -10
- package/types/src/routing/services/route-registry.d.ts +0 -20
- package/types/src/routing/services/router.d.ts +0 -22
- package/types/src/sse/sse.decorator.d.ts +0 -17
- package/types/src/sse/sse.encoder.d.ts +0 -16
- package/types/src/sse/sse.types.d.ts +0 -37
- package/types/src/sse/sse.wire.d.ts +0 -9
- package/types/src/testing/boot-module.d.ts +0 -34
- package/types/src/testing/sse-reader.d.ts +0 -29
- /package/{types/src/app/index.d.ts → src/app/index.ts} +0 -0
- /package/{types/src/auth/index.d.ts → src/auth/index.ts} +0 -0
- /package/{types/src/builder/index.d.ts → src/builder/index.ts} +0 -0
- /package/{types/src/health/index.d.ts → src/health/index.ts} +0 -0
- /package/{types/src/hosting/index.d.ts → src/hosting/index.ts} +0 -0
- /package/{types/src/http/index.d.ts → src/http/index.ts} +0 -0
- /package/{types/src/providers/index.d.ts → src/providers/index.ts} +0 -0
- /package/{types/src/routing/decorators/index.d.ts → src/routing/decorators/index.ts} +0 -0
- /package/{types/src/routing/index.d.ts → src/routing/index.ts} +0 -0
- /package/{types/src/routing/pipes/index.d.ts → src/routing/pipes/index.ts} +0 -0
- /package/{types/src/routing/services/index.d.ts → src/routing/services/index.ts} +0 -0
- /package/{types/src/sse/index.d.ts → src/sse/index.ts} +0 -0
- /package/{types/src/testing/index.d.ts → src/testing/index.ts} +0 -0
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", {
|
|
3
|
-
value: true
|
|
4
|
-
});
|
|
5
|
-
Object.defineProperty(exports, "bootModule", {
|
|
6
|
-
enumerable: true,
|
|
7
|
-
get: function() {
|
|
8
|
-
return bootModule;
|
|
9
|
-
}
|
|
10
|
-
});
|
|
11
|
-
const _builder = require("../builder/builder.js");
|
|
12
|
-
require("../http/http.extensions.js");
|
|
13
|
-
const _fastifyprovider = require("../providers/fastify.provider.js");
|
|
14
|
-
const _ssereader = require("./sse-reader.js");
|
|
15
|
-
async function bootModule(options) {
|
|
16
|
-
let builder = _builder.Builder.create().withHttp(options.provider ?? _fastifyprovider.FastifyProvider).withControllers(...options.controllers);
|
|
17
|
-
if (options.services) builder = builder.withServices(options.services);
|
|
18
|
-
options.configure?.(builder);
|
|
19
|
-
const app = await builder.build();
|
|
20
|
-
await app.start({
|
|
21
|
-
port: 0,
|
|
22
|
-
host: "127.0.0.1",
|
|
23
|
-
shutdownSignals: false
|
|
24
|
-
});
|
|
25
|
-
const baseUrl = app.addresses[0];
|
|
26
|
-
if (baseUrl == null) {
|
|
27
|
-
await app.dispose();
|
|
28
|
-
throw new Error("app.start({ port: 0 }) advertised no IPv4 address");
|
|
29
|
-
}
|
|
30
|
-
const stop = ()=>app.dispose();
|
|
31
|
-
return {
|
|
32
|
-
app,
|
|
33
|
-
baseUrl,
|
|
34
|
-
readEvents: (path, readOptions)=>(0, _ssereader.readEventStream)(`${baseUrl}${path}`, readOptions),
|
|
35
|
-
stop,
|
|
36
|
-
[Symbol.asyncDispose]: stop
|
|
37
|
-
};
|
|
38
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", {
|
|
3
|
-
value: true
|
|
4
|
-
});
|
|
5
|
-
function _export(target, all) {
|
|
6
|
-
for(var name in all)Object.defineProperty(target, name, {
|
|
7
|
-
enumerable: true,
|
|
8
|
-
get: Object.getOwnPropertyDescriptor(all, name).get
|
|
9
|
-
});
|
|
10
|
-
}
|
|
11
|
-
_export(exports, {
|
|
12
|
-
get bootModule () {
|
|
13
|
-
return _bootmodule.bootModule;
|
|
14
|
-
},
|
|
15
|
-
get readEventStream () {
|
|
16
|
-
return _ssereader.readEventStream;
|
|
17
|
-
}
|
|
18
|
-
});
|
|
19
|
-
const _bootmodule = require("./boot-module.js");
|
|
20
|
-
const _ssereader = require("./sse-reader.js");
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", {
|
|
3
|
-
value: true
|
|
4
|
-
});
|
|
5
|
-
Object.defineProperty(exports, "readEventStream", {
|
|
6
|
-
enumerable: true,
|
|
7
|
-
get: function() {
|
|
8
|
-
return readEventStream;
|
|
9
|
-
}
|
|
10
|
-
});
|
|
11
|
-
/** Incremental parser for the `text/event-stream` format. */ class SseParser {
|
|
12
|
-
buffer = "";
|
|
13
|
-
data = [];
|
|
14
|
-
current = {
|
|
15
|
-
data: ""
|
|
16
|
-
};
|
|
17
|
-
hasFields = false;
|
|
18
|
-
events = [];
|
|
19
|
-
comments = [];
|
|
20
|
-
push(chunk) {
|
|
21
|
-
this.buffer += chunk;
|
|
22
|
-
let newline;
|
|
23
|
-
while((newline = this.buffer.search(/\r\n|\r|\n/)) !== -1){
|
|
24
|
-
const line = this.buffer.slice(0, newline);
|
|
25
|
-
const eol = this.buffer.slice(newline).startsWith("\r\n") ? 2 : 1;
|
|
26
|
-
this.buffer = this.buffer.slice(newline + eol);
|
|
27
|
-
this.line(line);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
line(line) {
|
|
31
|
-
if (line === "") {
|
|
32
|
-
if (this.hasFields) {
|
|
33
|
-
this.events.push({
|
|
34
|
-
...this.current,
|
|
35
|
-
data: this.data.join("\n")
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
this.data = [];
|
|
39
|
-
this.current = {
|
|
40
|
-
data: ""
|
|
41
|
-
};
|
|
42
|
-
this.hasFields = false;
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
if (line.startsWith(":")) {
|
|
46
|
-
this.comments.push(line.slice(1).trim());
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
const colon = line.indexOf(":");
|
|
50
|
-
const field = colon === -1 ? line : line.slice(0, colon);
|
|
51
|
-
let value = colon === -1 ? "" : line.slice(colon + 1);
|
|
52
|
-
if (value.startsWith(" ")) value = value.slice(1);
|
|
53
|
-
this.hasFields = true;
|
|
54
|
-
if (field === "data") this.data.push(value);
|
|
55
|
-
else if (field === "event") this.current.event = value;
|
|
56
|
-
else if (field === "id") this.current.id = value;
|
|
57
|
-
else if (field === "retry") this.current.retry = Number(value);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
async function readEventStream(url, options = {}) {
|
|
61
|
-
const count = options.count ?? 1;
|
|
62
|
-
const abort = new AbortController();
|
|
63
|
-
const timer = setTimeout(()=>abort.abort(new Error(`No ${count} events from ${url} in time`)), options.timeoutMs ?? 5000);
|
|
64
|
-
try {
|
|
65
|
-
const response = await fetch(url, {
|
|
66
|
-
headers: {
|
|
67
|
-
accept: "text/event-stream",
|
|
68
|
-
...options.headers
|
|
69
|
-
},
|
|
70
|
-
signal: abort.signal
|
|
71
|
-
});
|
|
72
|
-
const parser = new SseParser();
|
|
73
|
-
let ended = false;
|
|
74
|
-
if (response.status === 200 && response.body != null) {
|
|
75
|
-
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
76
|
-
while(parser.events.length < count){
|
|
77
|
-
const { done, value } = await reader.read();
|
|
78
|
-
if (done) {
|
|
79
|
-
ended = true;
|
|
80
|
-
break;
|
|
81
|
-
}
|
|
82
|
-
parser.push(value);
|
|
83
|
-
}
|
|
84
|
-
await reader.cancel();
|
|
85
|
-
} else {
|
|
86
|
-
await response.body?.cancel();
|
|
87
|
-
}
|
|
88
|
-
return {
|
|
89
|
-
status: response.status,
|
|
90
|
-
headers: response.headers,
|
|
91
|
-
events: parser.events.slice(0, count),
|
|
92
|
-
comments: parser.comments,
|
|
93
|
-
ended
|
|
94
|
-
};
|
|
95
|
-
} finally{
|
|
96
|
-
clearTimeout(timer);
|
|
97
|
-
}
|
|
98
|
-
}
|
package/es6/src/app/app.js
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
function _ts_decorate(decorators, target, key, desc) {
|
|
2
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
-
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
-
}
|
|
7
|
-
function _ts_metadata(k, v) {
|
|
8
|
-
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
-
}
|
|
10
|
-
function _ts_param(paramIndex, decorator) {
|
|
11
|
-
return function(target, key) {
|
|
12
|
-
decorator(target, key, paramIndex);
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
import { LifetimeScopeToken } from "@decohere/crucible";
|
|
16
|
-
import { inject, optional } from "@decohere/crucible/decorators";
|
|
17
|
-
import { ConfigurationServiceToken } from "@decohere/core/configuration";
|
|
18
|
-
import { scribe } from "@decohere/scribe";
|
|
19
|
-
import { HostedServiceHostToken, HttpProviderToken } from "../const.js";
|
|
20
|
-
const DEFAULT_PORT = 3000;
|
|
21
|
-
const DEFAULT_SHUTDOWN_SIGNALS = [
|
|
22
|
-
"SIGTERM",
|
|
23
|
-
"SIGINT"
|
|
24
|
-
];
|
|
25
|
-
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 10_000;
|
|
26
|
-
export class ApiConfiguration {
|
|
27
|
-
port;
|
|
28
|
-
}
|
|
29
|
-
export class App {
|
|
30
|
-
container;
|
|
31
|
-
configurationService;
|
|
32
|
-
httpProvider;
|
|
33
|
-
hostedServices;
|
|
34
|
-
_addresses = Array.of();
|
|
35
|
-
logger = scribe.logger(App.name);
|
|
36
|
-
disposing;
|
|
37
|
-
disposingContainer = false;
|
|
38
|
-
signalHandler;
|
|
39
|
-
shutdownSignals = [];
|
|
40
|
-
constructor(container, configurationService, httpProvider, hostedServices){
|
|
41
|
-
this.container = container;
|
|
42
|
-
this.configurationService = configurationService;
|
|
43
|
-
this.httpProvider = httpProvider;
|
|
44
|
-
this.hostedServices = hostedServices;
|
|
45
|
-
}
|
|
46
|
-
get addresses() {
|
|
47
|
-
return this._addresses;
|
|
48
|
-
}
|
|
49
|
-
async start(options) {
|
|
50
|
-
this.logger.info("Starting app");
|
|
51
|
-
// Before HTTP, so a request never reaches a service that has not started.
|
|
52
|
-
await this.hostedServices?.start(this.container);
|
|
53
|
-
// The app was stopped while a hosted service was still starting.
|
|
54
|
-
if (this.disposing != null) return;
|
|
55
|
-
const hasHostedServices = this.hostedServices != null && !this.hostedServices.isEmpty;
|
|
56
|
-
if (!this.httpProvider) {
|
|
57
|
-
if (hasHostedServices) this.stopOnSignals(options);
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
const port = await this.getPort(options);
|
|
61
|
-
this.logger.info(`Starting http on port ${port}`);
|
|
62
|
-
const startResponse = await this.httpProvider?.start({
|
|
63
|
-
port,
|
|
64
|
-
...options?.host != null ? {
|
|
65
|
-
host: options.host
|
|
66
|
-
} : {}
|
|
67
|
-
});
|
|
68
|
-
this._addresses = [
|
|
69
|
-
...startResponse.addresses
|
|
70
|
-
];
|
|
71
|
-
this.logger.info("Listening on:", this._addresses);
|
|
72
|
-
this.stopOnSignals(options);
|
|
73
|
-
return startResponse;
|
|
74
|
-
}
|
|
75
|
-
[Symbol.asyncDispose]() {
|
|
76
|
-
// The container holds this App too; when stop() disposes the container it lands
|
|
77
|
-
// here, and awaiting our own shutdown would deadlock.
|
|
78
|
-
if (this.disposingContainer) return Promise.resolve();
|
|
79
|
-
return this.dispose();
|
|
80
|
-
}
|
|
81
|
-
/** Stop the app. Calling it again (or from a signal) returns the same shutdown. */ dispose() {
|
|
82
|
-
this.disposing ??= this.stop();
|
|
83
|
-
return this.disposing;
|
|
84
|
-
}
|
|
85
|
-
async stop() {
|
|
86
|
-
// Removed first, so a second Ctrl+C / SIGTERM gets Node's default: exit now.
|
|
87
|
-
this.removeSignalHandlers();
|
|
88
|
-
this.logger.info("Stopping");
|
|
89
|
-
// Hosted services hear the signal at once, so background work winds down while HTTP drains.
|
|
90
|
-
this.hostedServices?.abort();
|
|
91
|
-
try {
|
|
92
|
-
// HTTP first: in-flight requests still need the services (DB pools etc.) that
|
|
93
|
-
// disposing the container would close.
|
|
94
|
-
await this.httpProvider?.stop();
|
|
95
|
-
// Hosted services next, while what they use is still open.
|
|
96
|
-
await this.hostedServices?.stop();
|
|
97
|
-
} finally{
|
|
98
|
-
this.disposingContainer = true;
|
|
99
|
-
await this.container[Symbol.asyncDispose]();
|
|
100
|
-
await scribe.stop();
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
stopOnSignals(options) {
|
|
104
|
-
const signals = options?.shutdownSignals ?? DEFAULT_SHUTDOWN_SIGNALS;
|
|
105
|
-
const timeoutMs = options?.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
|
|
106
|
-
if (signals === false || signals.length === 0) return;
|
|
107
|
-
this.signalHandler = (signal)=>{
|
|
108
|
-
this.logger.info(`Received ${signal}; finishing in-flight requests`);
|
|
109
|
-
// console.error, not the logger: scribe may be the thing that's stuck.
|
|
110
|
-
const timer = setTimeout(()=>{
|
|
111
|
-
console.error(`Shutdown did not finish within ${timeoutMs}ms; exiting.`);
|
|
112
|
-
process.exit(1);
|
|
113
|
-
}, timeoutMs);
|
|
114
|
-
timer.unref();
|
|
115
|
-
this.dispose().then(()=>clearTimeout(timer), (error)=>{
|
|
116
|
-
clearTimeout(timer);
|
|
117
|
-
console.error("Shutdown failed:", error);
|
|
118
|
-
process.exit(1);
|
|
119
|
-
});
|
|
120
|
-
};
|
|
121
|
-
this.shutdownSignals = signals;
|
|
122
|
-
for (const signal of signals)process.on(signal, this.signalHandler);
|
|
123
|
-
}
|
|
124
|
-
removeSignalHandlers() {
|
|
125
|
-
if (!this.signalHandler) return;
|
|
126
|
-
for (const signal of this.shutdownSignals)process.off(signal, this.signalHandler);
|
|
127
|
-
}
|
|
128
|
-
async getPort(options) {
|
|
129
|
-
const port = options?.port ?? (await this.configurationService?.get(ApiConfiguration))?.port ?? DEFAULT_PORT;
|
|
130
|
-
return port;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
App = _ts_decorate([
|
|
134
|
-
_ts_param(0, inject(LifetimeScopeToken)),
|
|
135
|
-
_ts_param(1, inject(ConfigurationServiceToken)),
|
|
136
|
-
_ts_param(1, optional()),
|
|
137
|
-
_ts_param(2, inject(HttpProviderToken)),
|
|
138
|
-
_ts_param(2, optional()),
|
|
139
|
-
_ts_param(3, inject(HostedServiceHostToken)),
|
|
140
|
-
_ts_param(3, optional()),
|
|
141
|
-
_ts_metadata("design:type", Function),
|
|
142
|
-
_ts_metadata("design:paramtypes", [
|
|
143
|
-
typeof LifetimeScope === "undefined" ? Object : LifetimeScope,
|
|
144
|
-
typeof IConfigurationService === "undefined" ? Object : IConfigurationService,
|
|
145
|
-
typeof HttpProvider === "undefined" ? Object : HttpProvider,
|
|
146
|
-
typeof HostedServiceHost === "undefined" ? Object : HostedServiceHost
|
|
147
|
-
])
|
|
148
|
-
], App);
|
package/es6/src/app/index.js
DELETED
package/es6/src/app/module.js
DELETED
package/es6/src/auth/ability.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { AbilityBuilder, createMongoAbility } from "@casl/ability";
|
|
2
|
-
/** Root role — expands to CASL `manage`/`all` (covers every future ability). */ export const ADMIN_ROLE = "admin";
|
|
3
|
-
/**
|
|
4
|
-
* Default CASL ability factory.
|
|
5
|
-
*
|
|
6
|
-
* - `admin` role → `manage` / `all` (wildcard)
|
|
7
|
-
* - otherwise map `principal.scopes` of the form `"action:Subject"` to CASL rules
|
|
8
|
-
*
|
|
9
|
-
* Pass a custom factory to `withAuth()` (or override the app stub) for product-specific rules.
|
|
10
|
-
*/ export function defineAbilitiesFor(principal) {
|
|
11
|
-
const { can, build } = new AbilityBuilder(createMongoAbility);
|
|
12
|
-
if (principal.roles?.includes(ADMIN_ROLE)) {
|
|
13
|
-
can("manage", "all");
|
|
14
|
-
return build();
|
|
15
|
-
}
|
|
16
|
-
for (const scope of principal.scopes ?? []){
|
|
17
|
-
const [action, subject] = scope.split(":");
|
|
18
|
-
if (action && subject) can(action, subject);
|
|
19
|
-
}
|
|
20
|
-
return build();
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Map a raw JWT payload → {@link Principal}.
|
|
24
|
-
*
|
|
25
|
-
* Merges OAuth `scope` / `scopes` with an IdP `permissions` claim (e.g. Auth0 API permissions).
|
|
26
|
-
* Override via `withAuth(..., { principalMapper })` when claims differ.
|
|
27
|
-
*/ export function createPrincipalFromPayload(payload) {
|
|
28
|
-
const sub = typeof payload.sub === "string" ? payload.sub : "";
|
|
29
|
-
const fromScopeClaim = typeof payload.scope === "string" ? payload.scope.split(" ").filter(Boolean) : Array.isArray(payload.scopes) ? payload.scopes.filter((s)=>typeof s === "string") : [];
|
|
30
|
-
const fromPermissions = Array.isArray(payload.permissions) ? payload.permissions.filter((s)=>typeof s === "string") : [];
|
|
31
|
-
const merged = [
|
|
32
|
-
...new Set([
|
|
33
|
-
...fromScopeClaim,
|
|
34
|
-
...fromPermissions
|
|
35
|
-
])
|
|
36
|
-
];
|
|
37
|
-
const scopes = merged.length > 0 ? merged : undefined;
|
|
38
|
-
const roles = Array.isArray(payload.roles) ? payload.roles.filter((r)=>typeof r === "string") : undefined;
|
|
39
|
-
return {
|
|
40
|
-
...payload,
|
|
41
|
-
sub,
|
|
42
|
-
roles: roles && roles.length > 0 ? roles : undefined,
|
|
43
|
-
scopes
|
|
44
|
-
};
|
|
45
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Configuration for the TokenService, read from the `auth` section of the
|
|
3
|
-
* application config file. Exactly one of the discriminant fields must be set.
|
|
4
|
-
*
|
|
5
|
-
* Production → use `jwksUrl`, `publicKey`, or `secret`.
|
|
6
|
-
* Development → use `devStatic` (only works when NODE_ENV=development).
|
|
7
|
-
*/ export class AuthConfig {
|
|
8
|
-
/** OIDC discovery URL — the issuer base URL, e.g. https://accounts.google.com.
|
|
9
|
-
* The framework appends `/.well-known/openid-configuration` automatically.
|
|
10
|
-
* Use this instead of `jwksUrl` for any standard OIDC-compliant IdP.
|
|
11
|
-
*/ discoveryUrl;
|
|
12
|
-
/** JWKS endpoint URL (RS256 / ES256 via remote key set). */ jwksUrl;
|
|
13
|
-
/** PEM-encoded public key or JWK JSON string (RS256 / ES256 static key). */ publicKey;
|
|
14
|
-
algorithms;
|
|
15
|
-
/** Shared secret for HS256. */ secret;
|
|
16
|
-
/** Issuer claim to validate (all non-devStatic variants). */ issuer;
|
|
17
|
-
/** Audience claim to validate (all non-devStatic variants). */ audience;
|
|
18
|
-
/** Static principal injected for every request (development only). */ devStatic;
|
|
19
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
function _ts_decorate(decorators, target, key, desc) {
|
|
2
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
-
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
-
}
|
|
7
|
-
function _ts_metadata(k, v) {
|
|
8
|
-
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
-
}
|
|
10
|
-
import { extension } from "@decohere/core/extensibility";
|
|
11
|
-
import { Builder } from "../builder/builder.js";
|
|
12
|
-
import { AuthorizationServiceToken, TokenServiceToken } from "../const.js";
|
|
13
|
-
import { defineAbilitiesFor } from "./ability.js";
|
|
14
|
-
import { AuthConfig } from "./auth.config.js";
|
|
15
|
-
import { assertDevStaticAllowed, assertNonEmptyAudience } from "./auth.types.js";
|
|
16
|
-
import { CaslAuthorizationService } from "./casl-authorization.service.js";
|
|
17
|
-
import { TokenService } from "./token-service.js";
|
|
18
|
-
export { ADMIN_ROLE, createPrincipalFromPayload, defineAbilitiesFor } from "./ability.js";
|
|
19
|
-
export class BuilderExtensions {
|
|
20
|
-
static withAuth(builder, abilityFactory = defineAbilitiesFor, options) {
|
|
21
|
-
builder.registerPreBuildStep(async (configService, containerBuilder)=>{
|
|
22
|
-
const authConfig = await configService.get(AuthConfig);
|
|
23
|
-
const tokenConfig = toTokenServiceConfig(authConfig);
|
|
24
|
-
const tokenService = new TokenService(tokenConfig, options?.principalMapper);
|
|
25
|
-
containerBuilder.registerSingleton(TokenServiceToken, tokenService);
|
|
26
|
-
containerBuilder.registerSingleton(AuthorizationServiceToken, new CaslAuthorizationService(abilityFactory, tokenService));
|
|
27
|
-
});
|
|
28
|
-
return builder;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
_ts_decorate([
|
|
32
|
-
extension(Builder),
|
|
33
|
-
_ts_metadata("design:type", Function),
|
|
34
|
-
_ts_metadata("design:paramtypes", [
|
|
35
|
-
typeof Builder === "undefined" ? Object : Builder,
|
|
36
|
-
typeof AbilityFactory === "undefined" ? Object : AbilityFactory,
|
|
37
|
-
typeof WithAuthOptions === "undefined" ? Object : WithAuthOptions
|
|
38
|
-
]),
|
|
39
|
-
_ts_metadata("design:returntype", void 0)
|
|
40
|
-
], BuilderExtensions, "withAuth", null);
|
|
41
|
-
function toTokenServiceConfig(config) {
|
|
42
|
-
// Prefer real verification strategies over devStatic so a leftover
|
|
43
|
-
// development block cannot disable OIDC/JWKS in any environment.
|
|
44
|
-
if (config.discoveryUrl != null) {
|
|
45
|
-
assertNonEmptyAudience(config.audience);
|
|
46
|
-
return {
|
|
47
|
-
discoveryUrl: config.discoveryUrl,
|
|
48
|
-
audience: config.audience
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
if (config.jwksUrl != null) {
|
|
52
|
-
assertNonEmptyAudience(config.audience);
|
|
53
|
-
return {
|
|
54
|
-
jwksUrl: config.jwksUrl,
|
|
55
|
-
issuer: config.issuer,
|
|
56
|
-
audience: config.audience
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
if (config.publicKey != null) {
|
|
60
|
-
assertNonEmptyAudience(config.audience);
|
|
61
|
-
return {
|
|
62
|
-
publicKey: config.publicKey,
|
|
63
|
-
algorithms: config.algorithms,
|
|
64
|
-
issuer: config.issuer,
|
|
65
|
-
audience: config.audience
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
if (config.secret != null) {
|
|
69
|
-
assertNonEmptyAudience(config.audience);
|
|
70
|
-
return {
|
|
71
|
-
secret: config.secret,
|
|
72
|
-
issuer: config.issuer,
|
|
73
|
-
audience: config.audience
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
if (config.devStatic != null) {
|
|
77
|
-
assertDevStaticAllowed();
|
|
78
|
-
return {
|
|
79
|
-
devStatic: config.devStatic
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
throw new Error("AuthConfig must specify one of: discoveryUrl, jwksUrl, publicKey, secret, or devStatic");
|
|
83
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
export function isDiscoveryConfig(config) {
|
|
2
|
-
return "discoveryUrl" in config;
|
|
3
|
-
}
|
|
4
|
-
export function isDevStaticConfig(config) {
|
|
5
|
-
return "devStatic" in config;
|
|
6
|
-
}
|
|
7
|
-
export function isJwksConfig(config) {
|
|
8
|
-
return "jwksUrl" in config;
|
|
9
|
-
}
|
|
10
|
-
export function isPublicKeyConfig(config) {
|
|
11
|
-
return "publicKey" in config;
|
|
12
|
-
}
|
|
13
|
-
export function isSecretConfig(config) {
|
|
14
|
-
return "secret" in config;
|
|
15
|
-
}
|
|
16
|
-
/** `devStatic` is only valid for local development NODE_ENV values. */ export function isDevelopmentNodeEnv(nodeEnv = process.env.NODE_ENV) {
|
|
17
|
-
const env = nodeEnv?.toLowerCase();
|
|
18
|
-
return env === "development" || env === "dev";
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Refuse `devStatic` outside development — otherwise any Bearer string is
|
|
22
|
-
* accepted as the configured principal (auth bypass in production).
|
|
23
|
-
*/ export function assertDevStaticAllowed(nodeEnv = process.env.NODE_ENV) {
|
|
24
|
-
if (!isDevelopmentNodeEnv(nodeEnv)) {
|
|
25
|
-
throw new Error(`AuthConfig.devStatic is only allowed when NODE_ENV is "development" or "dev" (got ${JSON.stringify(nodeEnv ?? "")}).`);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* jose skips `aud` validation when `audience` is undefined or "". Require a
|
|
30
|
-
* non-empty audience for every non-devStatic verification strategy.
|
|
31
|
-
*/ export function assertNonEmptyAudience(audience) {
|
|
32
|
-
if (typeof audience !== "string" || audience.trim().length === 0) {
|
|
33
|
-
throw new Error(`JWT audience must be a non-empty string (got ${JSON.stringify(audience)}). ` + "Empty audience disables aud validation and accepts tokens minted for other APIs.");
|
|
34
|
-
}
|
|
35
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { ForbiddenError } from "../routing/anvil.types.js";
|
|
2
|
-
/**
|
|
3
|
-
* IAuthorizationService implementation backed by CASL.
|
|
4
|
-
*
|
|
5
|
-
* Register it under AuthorizationServiceToken:
|
|
6
|
-
* ```ts
|
|
7
|
-
* builder.withServices((c) =>
|
|
8
|
-
* c.registerSingleton(AuthorizationServiceToken, new CaslAuthorizationService((principal) => {
|
|
9
|
-
* const { can, build } = new AbilityBuilder(createMongoAbility);
|
|
10
|
-
* if (principal.roles?.includes("admin")) can("manage", "all");
|
|
11
|
-
* else can("read", "Post");
|
|
12
|
-
* return build();
|
|
13
|
-
* }))
|
|
14
|
-
* );
|
|
15
|
-
* ```
|
|
16
|
-
*/ export class CaslAuthorizationService {
|
|
17
|
-
factory;
|
|
18
|
-
tokenService;
|
|
19
|
-
constructor(factory, tokenService){
|
|
20
|
-
this.factory = factory;
|
|
21
|
-
this.tokenService = tokenService;
|
|
22
|
-
}
|
|
23
|
-
async authorizeRequest(request, action, subject) {
|
|
24
|
-
const principal = await this.authenticateRequest(request);
|
|
25
|
-
await this.authorize(principal, action, subject);
|
|
26
|
-
return principal;
|
|
27
|
-
}
|
|
28
|
-
async authenticateRequest(request) {
|
|
29
|
-
if (!this.tokenService) {
|
|
30
|
-
throw new Error("CaslAuthorizationService requires a TokenService to authenticate requests. Register it via .withAuth().");
|
|
31
|
-
}
|
|
32
|
-
return this.tokenService.verifyRequest(request);
|
|
33
|
-
}
|
|
34
|
-
async authorize(principal, action, subject) {
|
|
35
|
-
const ability = this.factory(principal);
|
|
36
|
-
if (ability.cannot(action, subject)) {
|
|
37
|
-
throw new ForbiddenError(`Cannot perform "${action}" on "${subject}"`);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
package/es6/src/auth/index.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Concrete, request-scoped implementation. The Router sets the principal
|
|
3
|
-
* after JWT verification; downstream services consume it via IPrincipalProvider.
|
|
4
|
-
*
|
|
5
|
-
* @internal — framework use only. Inject IPrincipalProvider, not this class.
|
|
6
|
-
*/ export class PrincipalProvider {
|
|
7
|
-
_principal;
|
|
8
|
-
get currentPrincipal() {
|
|
9
|
-
return this._principal;
|
|
10
|
-
}
|
|
11
|
-
setPrincipal(principal) {
|
|
12
|
-
this._principal = principal;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { lookup } from "node:dns/promises";
|
|
2
|
-
import { BlockList, isIP } from "node:net";
|
|
3
|
-
/**
|
|
4
|
-
* Validate URLs used for OIDC discovery / JWKS fetches to reduce SSRF risk.
|
|
5
|
-
*
|
|
6
|
-
* - http(s) only; https required except loopback (local IdP tests)
|
|
7
|
-
* - no credentials in the URL
|
|
8
|
-
* - hostname must not resolve to private, link-local, or metadata ranges
|
|
9
|
-
* (loopback allowed only when NODE_ENV is development/dev/test)
|
|
10
|
-
*/ export async function assertSafeOutboundUrl(raw, label) {
|
|
11
|
-
let url;
|
|
12
|
-
try {
|
|
13
|
-
url = new URL(raw);
|
|
14
|
-
} catch {
|
|
15
|
-
throw new Error(`Invalid ${label}: ${raw}`);
|
|
16
|
-
}
|
|
17
|
-
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
18
|
-
throw new Error(`${label} must use http or https (got ${url.protocol})`);
|
|
19
|
-
}
|
|
20
|
-
if (url.username || url.password) {
|
|
21
|
-
throw new Error(`${label} must not include credentials`);
|
|
22
|
-
}
|
|
23
|
-
const hostname = url.hostname.replace(/^\[|\]$/g, "");
|
|
24
|
-
if (isBlockedHostname(hostname)) {
|
|
25
|
-
throw new Error(`${label} host is not allowed (${hostname})`);
|
|
26
|
-
}
|
|
27
|
-
const ips = await resolveIps(hostname);
|
|
28
|
-
const allowLoopback = isLoopbackAllowed();
|
|
29
|
-
for (const ip of ips){
|
|
30
|
-
if (isBlockedIp(ip, allowLoopback)) {
|
|
31
|
-
throw new Error(`${label} resolves to a blocked address (${ip})`);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
if (url.protocol === "http:") {
|
|
35
|
-
const loopbackOnly = isLoopbackHostname(hostname) || ips.length > 0 && ips.every((ip)=>isLoopbackIp(ip));
|
|
36
|
-
if (!loopbackOnly || !allowLoopback) {
|
|
37
|
-
throw new Error(`${label} must use https unless targeting loopback in development/test`);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return url;
|
|
41
|
-
}
|
|
42
|
-
function isLoopbackAllowed() {
|
|
43
|
-
const env = process.env.NODE_ENV ?? "";
|
|
44
|
-
return env === "development" || env === "dev" || env === "test";
|
|
45
|
-
}
|
|
46
|
-
function isLoopbackHostname(hostname) {
|
|
47
|
-
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
|
48
|
-
}
|
|
49
|
-
function isBlockedHostname(hostname) {
|
|
50
|
-
const lower = hostname.toLowerCase();
|
|
51
|
-
return lower === "metadata.google.internal" || lower === "metadata" || lower.endsWith(".metadata.google.internal");
|
|
52
|
-
}
|
|
53
|
-
async function resolveIps(hostname) {
|
|
54
|
-
if (isIP(hostname)) {
|
|
55
|
-
return [
|
|
56
|
-
hostname
|
|
57
|
-
];
|
|
58
|
-
}
|
|
59
|
-
const results = await lookup(hostname, {
|
|
60
|
-
all: true,
|
|
61
|
-
verbatim: true
|
|
62
|
-
});
|
|
63
|
-
return results.map((r)=>r.address);
|
|
64
|
-
}
|
|
65
|
-
function isLoopbackIp(ip) {
|
|
66
|
-
const list = new BlockList();
|
|
67
|
-
list.addSubnet("127.0.0.0", 8, "ipv4");
|
|
68
|
-
list.addAddress("::1", "ipv6");
|
|
69
|
-
list.addSubnet("::ffff:127.0.0.0", 104, "ipv6");
|
|
70
|
-
return list.check(ip);
|
|
71
|
-
}
|
|
72
|
-
function isBlockedIp(ip, allowLoopback) {
|
|
73
|
-
if (allowLoopback && isLoopbackIp(ip)) {
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
const list = new BlockList();
|
|
77
|
-
// IPv4 special-use / private
|
|
78
|
-
list.addSubnet("0.0.0.0", 8, "ipv4");
|
|
79
|
-
list.addSubnet("10.0.0.0", 8, "ipv4");
|
|
80
|
-
list.addSubnet("100.64.0.0", 10, "ipv4"); // shared address space
|
|
81
|
-
list.addSubnet("127.0.0.0", 8, "ipv4");
|
|
82
|
-
list.addSubnet("169.254.0.0", 16, "ipv4"); // link-local / cloud metadata
|
|
83
|
-
list.addSubnet("172.16.0.0", 12, "ipv4");
|
|
84
|
-
list.addSubnet("192.168.0.0", 16, "ipv4");
|
|
85
|
-
list.addSubnet("198.18.0.0", 15, "ipv4"); // benchmarking
|
|
86
|
-
// IPv6
|
|
87
|
-
list.addAddress("::1", "ipv6");
|
|
88
|
-
list.addSubnet("fc00::", 7, "ipv6"); // unique local
|
|
89
|
-
list.addSubnet("fe80::", 10, "ipv6"); // link-local
|
|
90
|
-
if (list.check(ip)) {
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
// Mapped IPv4 inside IPv6 (::ffff:a.b.c.d)
|
|
94
|
-
const mapped = ip.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i);
|
|
95
|
-
if (mapped) {
|
|
96
|
-
return isBlockedIp(mapped[1], allowLoopback);
|
|
97
|
-
}
|
|
98
|
-
return false;
|
|
99
|
-
}
|