@decohere/http 0.0.1-dev.116

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 (206) hide show
  1. package/README.md +246 -0
  2. package/commonjs/src/app/app.js +166 -0
  3. package/commonjs/src/app/index.js +19 -0
  4. package/commonjs/src/app/module.js +17 -0
  5. package/commonjs/src/auth/ability.js +54 -0
  6. package/commonjs/src/auth/auth.config.js +23 -0
  7. package/commonjs/src/auth/auth.extensions.js +106 -0
  8. package/commonjs/src/auth/auth.types.js +65 -0
  9. package/commonjs/src/auth/casl-authorization.service.js +36 -0
  10. package/commonjs/src/auth/index.js +23 -0
  11. package/commonjs/src/auth/principal-provider.js +19 -0
  12. package/commonjs/src/auth/safe-outbound-url.js +102 -0
  13. package/commonjs/src/auth/token-service.js +151 -0
  14. package/commonjs/src/builder/builder.js +95 -0
  15. package/commonjs/src/builder/index.js +18 -0
  16. package/commonjs/src/const.js +87 -0
  17. package/commonjs/src/health/health.controller.js +75 -0
  18. package/commonjs/src/health/health.extensions.js +50 -0
  19. package/commonjs/src/health/index.js +21 -0
  20. package/commonjs/src/health/readiness.service.js +67 -0
  21. package/commonjs/src/health/readiness.types.js +7 -0
  22. package/commonjs/src/hosting/hosted-service-host.js +88 -0
  23. package/commonjs/src/hosting/hosted-service.js +11 -0
  24. package/commonjs/src/hosting/index.js +19 -0
  25. package/commonjs/src/http/http.compression.js +27 -0
  26. package/commonjs/src/http/http.extensions.js +269 -0
  27. package/commonjs/src/http/http.options.js +26 -0
  28. package/commonjs/src/http/http.options.store.js +20 -0
  29. package/commonjs/src/http/http.security.js +27 -0
  30. package/commonjs/src/http/http.static.js +248 -0
  31. package/commonjs/src/http/index.js +22 -0
  32. package/commonjs/src/index.js +27 -0
  33. package/commonjs/src/openapi/index.js +45 -0
  34. package/commonjs/src/openapi/openapi.generator.js +103 -0
  35. package/commonjs/src/openapi/openapi.types.js +4 -0
  36. package/commonjs/src/openapi/request-schema.js +187 -0
  37. package/commonjs/src/openapi/request-validator.js +85 -0
  38. package/commonjs/src/providers/fastify.provider.js +391 -0
  39. package/commonjs/src/providers/index.js +18 -0
  40. package/commonjs/src/routing/anvil.types.js +99 -0
  41. package/commonjs/src/routing/decorators/argument.decorator.js +49 -0
  42. package/commonjs/src/routing/decorators/auth.decorator.js +70 -0
  43. package/commonjs/src/routing/decorators/controller.decorator.js +16 -0
  44. package/commonjs/src/routing/decorators/field.decorator.js +25 -0
  45. package/commonjs/src/routing/decorators/index.js +24 -0
  46. package/commonjs/src/routing/decorators/request.decorator.js +30 -0
  47. package/commonjs/src/routing/decorators/response.decorator.js +34 -0
  48. package/commonjs/src/routing/decorators/route.decorator.js +67 -0
  49. package/commonjs/src/routing/filters/exception-filter-host.js +145 -0
  50. package/commonjs/src/routing/filters/exception-filter.js +28 -0
  51. package/commonjs/src/routing/index.js +25 -0
  52. package/commonjs/src/routing/middleware/http-access-log.middleware.js +47 -0
  53. package/commonjs/src/routing/middleware/middleware.js +20 -0
  54. package/commonjs/src/routing/middleware/request-id.js +30 -0
  55. package/commonjs/src/routing/path.util.js +51 -0
  56. package/commonjs/src/routing/pipes/index.js +18 -0
  57. package/commonjs/src/routing/pipes/pipes.js +145 -0
  58. package/commonjs/src/routing/services/index.js +18 -0
  59. package/commonjs/src/routing/services/route-argument-resolver.js +77 -0
  60. package/commonjs/src/routing/services/route-registry.js +194 -0
  61. package/commonjs/src/routing/services/router.js +158 -0
  62. package/commonjs/src/sse/index.js +26 -0
  63. package/commonjs/src/sse/sse.decorator.js +42 -0
  64. package/commonjs/src/sse/sse.encoder.js +73 -0
  65. package/commonjs/src/sse/sse.types.js +36 -0
  66. package/commonjs/src/sse/sse.wire.js +40 -0
  67. package/commonjs/src/testing/boot-module.js +38 -0
  68. package/commonjs/src/testing/index.js +20 -0
  69. package/commonjs/src/testing/sse-reader.js +98 -0
  70. package/es6/src/app/app.js +148 -0
  71. package/es6/src/app/index.js +2 -0
  72. package/es6/src/app/module.js +7 -0
  73. package/es6/src/auth/ability.js +45 -0
  74. package/es6/src/auth/auth.config.js +19 -0
  75. package/es6/src/auth/auth.extensions.js +83 -0
  76. package/es6/src/auth/auth.types.js +35 -0
  77. package/es6/src/auth/casl-authorization.service.js +40 -0
  78. package/es6/src/auth/index.js +6 -0
  79. package/es6/src/auth/principal-provider.js +14 -0
  80. package/es6/src/auth/safe-outbound-url.js +99 -0
  81. package/es6/src/auth/token-service.js +141 -0
  82. package/es6/src/builder/builder.js +85 -0
  83. package/es6/src/builder/index.js +1 -0
  84. package/es6/src/const.js +33 -0
  85. package/es6/src/health/health.controller.js +65 -0
  86. package/es6/src/health/health.extensions.js +40 -0
  87. package/es6/src/health/index.js +4 -0
  88. package/es6/src/health/readiness.service.js +57 -0
  89. package/es6/src/health/readiness.types.js +4 -0
  90. package/es6/src/hosting/hosted-service-host.js +81 -0
  91. package/es6/src/hosting/hosted-service.js +8 -0
  92. package/es6/src/hosting/index.js +2 -0
  93. package/es6/src/http/http.compression.js +9 -0
  94. package/es6/src/http/http.extensions.js +259 -0
  95. package/es6/src/http/http.options.js +16 -0
  96. package/es6/src/http/http.options.store.js +13 -0
  97. package/es6/src/http/http.security.js +9 -0
  98. package/es6/src/http/http.static.js +224 -0
  99. package/es6/src/http/index.js +5 -0
  100. package/es6/src/index.js +10 -0
  101. package/es6/src/openapi/index.js +3 -0
  102. package/es6/src/openapi/openapi.generator.js +82 -0
  103. package/es6/src/openapi/openapi.types.js +1 -0
  104. package/es6/src/openapi/request-schema.js +160 -0
  105. package/es6/src/openapi/request-validator.js +71 -0
  106. package/es6/src/providers/fastify.provider.js +376 -0
  107. package/es6/src/providers/index.js +1 -0
  108. package/es6/src/routing/anvil.types.js +57 -0
  109. package/es6/src/routing/decorators/argument.decorator.js +28 -0
  110. package/es6/src/routing/decorators/auth.decorator.js +58 -0
  111. package/es6/src/routing/decorators/controller.decorator.js +6 -0
  112. package/es6/src/routing/decorators/field.decorator.js +15 -0
  113. package/es6/src/routing/decorators/index.js +7 -0
  114. package/es6/src/routing/decorators/request.decorator.js +24 -0
  115. package/es6/src/routing/decorators/response.decorator.js +16 -0
  116. package/es6/src/routing/decorators/route.decorator.js +37 -0
  117. package/es6/src/routing/filters/exception-filter-host.js +127 -0
  118. package/es6/src/routing/filters/exception-filter.js +10 -0
  119. package/es6/src/routing/index.js +8 -0
  120. package/es6/src/routing/middleware/http-access-log.middleware.js +37 -0
  121. package/es6/src/routing/middleware/middleware.js +10 -0
  122. package/es6/src/routing/middleware/request-id.js +14 -0
  123. package/es6/src/routing/path.util.js +33 -0
  124. package/es6/src/routing/pipes/index.js +1 -0
  125. package/es6/src/routing/pipes/pipes.js +118 -0
  126. package/es6/src/routing/services/index.js +1 -0
  127. package/es6/src/routing/services/route-argument-resolver.js +67 -0
  128. package/es6/src/routing/services/route-registry.js +184 -0
  129. package/es6/src/routing/services/router.js +148 -0
  130. package/es6/src/sse/index.js +2 -0
  131. package/es6/src/sse/sse.decorator.js +31 -0
  132. package/es6/src/sse/sse.encoder.js +67 -0
  133. package/es6/src/sse/sse.types.js +22 -0
  134. package/es6/src/sse/sse.wire.js +30 -0
  135. package/es6/src/testing/boot-module.js +35 -0
  136. package/es6/src/testing/index.js +2 -0
  137. package/es6/src/testing/sse-reader.js +91 -0
  138. package/package.json +64 -0
  139. package/types/src/app/app.d.ts +41 -0
  140. package/types/src/app/index.d.ts +2 -0
  141. package/types/src/app/module.d.ts +15 -0
  142. package/types/src/auth/ability.d.ts +19 -0
  143. package/types/src/auth/auth.config.d.ts +28 -0
  144. package/types/src/auth/auth.extensions.d.ts +53 -0
  145. package/types/src/auth/auth.types.d.ts +73 -0
  146. package/types/src/auth/casl-authorization.service.d.ts +28 -0
  147. package/types/src/auth/index.d.ts +6 -0
  148. package/types/src/auth/principal-provider.d.ts +20 -0
  149. package/types/src/auth/safe-outbound-url.d.ts +9 -0
  150. package/types/src/auth/token-service.d.ts +31 -0
  151. package/types/src/builder/builder.d.ts +36 -0
  152. package/types/src/builder/index.d.ts +1 -0
  153. package/types/src/const.d.ts +32 -0
  154. package/types/src/health/health.controller.d.ts +16 -0
  155. package/types/src/health/health.extensions.d.ts +31 -0
  156. package/types/src/health/index.d.ts +4 -0
  157. package/types/src/health/readiness.service.d.ts +9 -0
  158. package/types/src/health/readiness.types.d.ts +14 -0
  159. package/types/src/hosting/hosted-service-host.d.ts +37 -0
  160. package/types/src/hosting/hosted-service.d.ts +31 -0
  161. package/types/src/hosting/index.d.ts +2 -0
  162. package/types/src/http/http.compression.d.ts +32 -0
  163. package/types/src/http/http.extensions.d.ts +79 -0
  164. package/types/src/http/http.options.d.ts +39 -0
  165. package/types/src/http/http.options.store.d.ts +7 -0
  166. package/types/src/http/http.security.d.ts +20 -0
  167. package/types/src/http/http.static.d.ts +160 -0
  168. package/types/src/http/index.d.ts +5 -0
  169. package/types/src/index.d.ts +10 -0
  170. package/types/src/openapi/index.d.ts +6 -0
  171. package/types/src/openapi/openapi.generator.d.ts +6 -0
  172. package/types/src/openapi/openapi.types.d.ts +58 -0
  173. package/types/src/openapi/request-schema.d.ts +21 -0
  174. package/types/src/openapi/request-validator.d.ts +9 -0
  175. package/types/src/providers/fastify.provider.d.ts +56 -0
  176. package/types/src/providers/index.d.ts +1 -0
  177. package/types/src/routing/anvil.types.d.ts +119 -0
  178. package/types/src/routing/decorators/argument.decorator.d.ts +11 -0
  179. package/types/src/routing/decorators/auth.decorator.d.ts +30 -0
  180. package/types/src/routing/decorators/controller.decorator.d.ts +3 -0
  181. package/types/src/routing/decorators/field.decorator.d.ts +2 -0
  182. package/types/src/routing/decorators/index.d.ts +7 -0
  183. package/types/src/routing/decorators/request.decorator.d.ts +8 -0
  184. package/types/src/routing/decorators/response.decorator.d.ts +4 -0
  185. package/types/src/routing/decorators/route.decorator.d.ts +7 -0
  186. package/types/src/routing/filters/exception-filter-host.d.ts +19 -0
  187. package/types/src/routing/filters/exception-filter.d.ts +16 -0
  188. package/types/src/routing/index.d.ts +8 -0
  189. package/types/src/routing/middleware/http-access-log.middleware.d.ts +8 -0
  190. package/types/src/routing/middleware/middleware.d.ts +10 -0
  191. package/types/src/routing/middleware/request-id.d.ts +6 -0
  192. package/types/src/routing/path.util.d.ts +9 -0
  193. package/types/src/routing/pipes/index.d.ts +1 -0
  194. package/types/src/routing/pipes/pipes.d.ts +21 -0
  195. package/types/src/routing/services/index.d.ts +1 -0
  196. package/types/src/routing/services/route-argument-resolver.d.ts +10 -0
  197. package/types/src/routing/services/route-registry.d.ts +20 -0
  198. package/types/src/routing/services/router.d.ts +22 -0
  199. package/types/src/sse/index.d.ts +3 -0
  200. package/types/src/sse/sse.decorator.d.ts +17 -0
  201. package/types/src/sse/sse.encoder.d.ts +16 -0
  202. package/types/src/sse/sse.types.d.ts +37 -0
  203. package/types/src/sse/sse.wire.d.ts +9 -0
  204. package/types/src/testing/boot-module.d.ts +34 -0
  205. package/types/src/testing/index.d.ts +4 -0
  206. package/types/src/testing/sse-reader.d.ts +29 -0
@@ -0,0 +1,40 @@
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
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./ability.js";
2
+ export * from "./auth.config.js";
3
+ export * from "./auth.types.js";
4
+ export * from "./casl-authorization.service.js";
5
+ export * from "./principal-provider.js";
6
+ export * from "./token-service.js";
@@ -0,0 +1,14 @@
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
+ }
@@ -0,0 +1,99 @@
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
+ }
@@ -0,0 +1,141 @@
1
+ import { createRemoteJWKSet, importJWK, importSPKI, jwtVerify, SignJWT } from "jose";
2
+ import { UnauthorizedError } from "../routing/anvil.types.js";
3
+ import { createPrincipalFromPayload } from "./ability.js";
4
+ import { assertDevStaticAllowed, assertNonEmptyAudience, isDevStaticConfig, isDiscoveryConfig, isJwksConfig, isPublicKeyConfig, isSecretConfig } from "./auth.types.js";
5
+ import { assertSafeOutboundUrl } from "./safe-outbound-url.js";
6
+ export class TokenService {
7
+ config;
8
+ principalMapper;
9
+ /** Lazily resolved JWKS set when using OIDC discovery. */ discoveryJwks;
10
+ discoveryIssuer;
11
+ /** Remote key set for `jwksUrl` mode, created once so jose can cache the keys (#499). */ configuredJwks;
12
+ constructor(config, principalMapper){
13
+ if (isDevStaticConfig(config)) {
14
+ assertDevStaticAllowed();
15
+ } else {
16
+ assertNonEmptyAudience(config.audience);
17
+ }
18
+ this.config = config;
19
+ this.principalMapper = principalMapper;
20
+ }
21
+ async verifyRequest(request) {
22
+ const authHeader = request.headers.authorization;
23
+ if (!authHeader) {
24
+ throw new UnauthorizedError("Missing Authorization header");
25
+ }
26
+ const spaceIdx = authHeader.indexOf(" ");
27
+ if (spaceIdx === -1) {
28
+ throw new UnauthorizedError("Malformed Authorization header");
29
+ }
30
+ const scheme = authHeader.slice(0, spaceIdx);
31
+ if (scheme !== "Bearer") {
32
+ throw new UnauthorizedError(`Unsupported authorization scheme: ${scheme}`);
33
+ }
34
+ const credentials = authHeader.slice(spaceIdx + 1);
35
+ return this.verify(credentials);
36
+ }
37
+ async verify(credentials) {
38
+ if (isDevStaticConfig(this.config)) {
39
+ return this.config.devStatic;
40
+ }
41
+ try {
42
+ const { key, issuer, audience } = await this.resolveVerificationContext();
43
+ // jose's overloads don't accept Key | JWKS-getter as a union; runtime does.
44
+ const { payload } = await jwtVerify(credentials, key, {
45
+ issuer,
46
+ audience
47
+ });
48
+ return this.mapPrincipal(payload);
49
+ } catch (err) {
50
+ throw new UnauthorizedError(err instanceof Error ? err.message : "Token verification failed");
51
+ }
52
+ }
53
+ /**
54
+ * Resolve how to verify tokens for the configured strategy:
55
+ * - OIDC discovery → fetch JWKS + issuer from `/.well-known/openid-configuration`
56
+ * - explicit JWKS URL → remote key set + configured issuer/audience
57
+ * - public key / shared secret → static key material + configured issuer/audience
58
+ */ async resolveVerificationContext() {
59
+ if (isDiscoveryConfig(this.config)) {
60
+ const { jwks, issuer } = await this.resolveOidcDiscovery();
61
+ return {
62
+ key: jwks,
63
+ issuer,
64
+ audience: this.config.audience
65
+ };
66
+ }
67
+ if (isJwksConfig(this.config)) {
68
+ this.configuredJwks ??= createRemoteJWKSet(await assertSafeOutboundUrl(this.config.jwksUrl, "jwksUrl"));
69
+ return {
70
+ key: this.configuredJwks,
71
+ issuer: this.config.issuer,
72
+ audience: this.config.audience
73
+ };
74
+ }
75
+ return {
76
+ key: await this.getStaticKey(),
77
+ issuer: this.config.issuer,
78
+ audience: this.config.audience
79
+ };
80
+ }
81
+ async resolveOidcDiscovery() {
82
+ if (this.discoveryJwks && this.discoveryIssuer) {
83
+ return {
84
+ jwks: this.discoveryJwks,
85
+ issuer: this.discoveryIssuer
86
+ };
87
+ }
88
+ const { discoveryUrl } = this.config;
89
+ await assertSafeOutboundUrl(discoveryUrl, "discoveryUrl");
90
+ const resp = await fetch(`${discoveryUrl.replace(/\/$/, "")}/.well-known/openid-configuration`, {
91
+ redirect: "error"
92
+ });
93
+ if (!resp.ok) {
94
+ throw new Error(`OIDC discovery failed for ${discoveryUrl}: HTTP ${resp.status}`);
95
+ }
96
+ const doc = await resp.json();
97
+ if (typeof doc.jwks_uri !== "string" || doc.jwks_uri.length === 0) {
98
+ throw new Error(`OIDC discovery for ${discoveryUrl} did not return jwks_uri`);
99
+ }
100
+ const jwksUrl = await assertSafeOutboundUrl(doc.jwks_uri, "jwks_uri");
101
+ this.discoveryIssuer = doc.issuer;
102
+ this.discoveryJwks = createRemoteJWKSet(jwksUrl);
103
+ return {
104
+ jwks: this.discoveryJwks,
105
+ issuer: this.discoveryIssuer
106
+ };
107
+ }
108
+ /**
109
+ * Sign a payload and return a JWT string.
110
+ * Only available when configured with a SecretConfig (HS256).
111
+ * For RS256/ES256 issuance, the caller is responsible for key management.
112
+ */ async sign(payload, expiresIn = "1h") {
113
+ if (!isSecretConfig(this.config)) {
114
+ throw new Error("TokenService.sign() requires SecretConfig (shared secret / HS256)");
115
+ }
116
+ const key = new TextEncoder().encode(this.config.secret);
117
+ return new SignJWT(payload).setProtectedHeader({
118
+ alg: "HS256"
119
+ }).setIssuedAt().setIssuer(this.config.issuer ?? "").setAudience(this.config.audience ?? "").setExpirationTime(expiresIn).sign(key);
120
+ }
121
+ async getStaticKey() {
122
+ if (isPublicKeyConfig(this.config)) {
123
+ const cfg = this.config;
124
+ const trimmed = cfg.publicKey.trim();
125
+ if (trimmed.startsWith("{")) {
126
+ const jwk = JSON.parse(trimmed);
127
+ return importJWK(jwk);
128
+ }
129
+ return importSPKI(trimmed, cfg.algorithms?.[0] ?? "RS256");
130
+ }
131
+ if (isSecretConfig(this.config)) {
132
+ const enc = new TextEncoder().encode(this.config.secret);
133
+ return enc;
134
+ }
135
+ throw new Error("Invalid TokenService configuration");
136
+ }
137
+ mapPrincipal(payload) {
138
+ if (this.principalMapper) return this.principalMapper(payload);
139
+ return createPrincipalFromPayload(payload);
140
+ }
141
+ }
@@ -0,0 +1,85 @@
1
+ import { ServiceContainerBuilder } from "@decohere/crucible";
2
+ import { ConfigurationServiceToken } from "@decohere/core/configuration";
3
+ import { scribe } from "@decohere/scribe";
4
+ import { App } from "../app/app.js";
5
+ import { HostedServiceHostToken } from "../const.js";
6
+ import { HostedServiceHost } from "../hosting/hosted-service-host.js";
7
+ export class Builder {
8
+ static create(options) {
9
+ return new Builder(options);
10
+ }
11
+ containerBuilder;
12
+ applicationName;
13
+ logger = scribe.logger("name");
14
+ userServices = [];
15
+ preBuildSteps = [];
16
+ postBuildSteps = [];
17
+ hostedServices = new HostedServiceHost();
18
+ constructor(options){
19
+ this.containerBuilder = options?.containerBuilder ?? new ServiceContainerBuilder();
20
+ this.applicationName = options?.applicationName;
21
+ }
22
+ withServices(userServices) {
23
+ this.userServices.push(userServices);
24
+ return this;
25
+ }
26
+ /**
27
+ * Run {@link HostedService}s for the life of the app: started in this order before the HTTP
28
+ * server listens, stopped in reverse when the app stops (including SIGTERM and Flux reboots).
29
+ * Each is registered as a singleton, so other services can inject it by its class.
30
+ */ withHostedService(...types) {
31
+ for (const type of types)this.containerBuilder.registerSingleton(type);
32
+ this.hostedServices.add(...types);
33
+ return this;
34
+ }
35
+ registerPreBuildStep(step) {
36
+ this.preBuildSteps.push(step);
37
+ }
38
+ registerPostBuildStep(step) {
39
+ this.postBuildSteps.push(step);
40
+ }
41
+ async buildAndStart(type) {
42
+ const app = await this.build(type);
43
+ await app.start();
44
+ this.logger.info("App started...");
45
+ return app;
46
+ }
47
+ async build(type) {
48
+ this.logger.info("Building App...");
49
+ this.configureUserServices();
50
+ this.configureApp(type);
51
+ await this.runPreBuildSteps(this.containerBuilder);
52
+ const container = this.containerBuilder.build();
53
+ await this.runPostBuildSteps(container);
54
+ const app = this.resolveApp(container, type);
55
+ return app;
56
+ }
57
+ configureUserServices() {
58
+ for (const userService of this.userServices){
59
+ userService(this.containerBuilder);
60
+ }
61
+ }
62
+ async runPreBuildSteps(containerBuilder) {
63
+ if (this.preBuildSteps.length === 0) {
64
+ return;
65
+ }
66
+ const container = containerBuilder.build();
67
+ const config = container.resolve(ConfigurationServiceToken);
68
+ for (const step of this.preBuildSteps){
69
+ await step(config, containerBuilder);
70
+ }
71
+ }
72
+ async runPostBuildSteps(container) {
73
+ for (const step of this.postBuildSteps){
74
+ await step(container);
75
+ }
76
+ }
77
+ configureApp(type) {
78
+ this.containerBuilder.registerSingleton(HostedServiceHostToken, this.hostedServices);
79
+ this.containerBuilder.registerSingleton(type ?? App);
80
+ }
81
+ resolveApp(container, type) {
82
+ const app = type ? container.resolve(type) : container.resolve(App);
83
+ return app;
84
+ }
85
+ }
@@ -0,0 +1 @@
1
+ export * from "./builder.js";
@@ -0,0 +1,33 @@
1
+ const Anvil = "anvil";
2
+ const RoutingKey = `${Anvil}:Routing`;
3
+ export const RouteRegistryToken = Symbol("RouteRegistryToken");
4
+ export const HttpProviderToken = Symbol("HttpProviderToken");
5
+ export const RouterToken = Symbol("RouterToken");
6
+ export const PrincipalToken = Symbol("PrincipalToken");
7
+ export const IPrincipalProviderToken = Symbol("IPrincipalProvider");
8
+ export const TokenServiceToken = Symbol("TokenService");
9
+ export const AuthorizationServiceToken = Symbol("AuthorizationService");
10
+ export const HttpOptionsToken = Symbol("HttpOptions");
11
+ export const ExceptionFilterHostToken = Symbol("ExceptionFilterHost");
12
+ export const ReadinessCheckToken = Symbol("ReadinessCheck");
13
+ export const ReadinessServiceToken = Symbol("ReadinessService");
14
+ export const HostedServiceHostToken = Symbol("HostedServiceHost");
15
+ export const HTTPMethods = {
16
+ GET: "GET",
17
+ PUT: "PUT",
18
+ POST: "POST",
19
+ PATCH: "PATCH",
20
+ DELETE: "DELETE"
21
+ };
22
+ export const MetadataKeys = {
23
+ controller: `${RoutingKey}:controller`,
24
+ route: `${RoutingKey}:route`,
25
+ parameter: `${RoutingKey}:parameter`,
26
+ fieldMap: `${RoutingKey}:fieldMap`,
27
+ authorize: `${RoutingKey}:authorize`,
28
+ allowAnonymous: `${RoutingKey}:allowAnonymous`,
29
+ httpCode: `${RoutingKey}:httpCode`,
30
+ headers: `${RoutingKey}:headers`,
31
+ catch: `${RoutingKey}:catch`,
32
+ sse: `${RoutingKey}:sse`
33
+ };
@@ -0,0 +1,65 @@
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 { inject, injectable } from "@decohere/crucible/decorators";
16
+ import { ReadinessServiceToken } from "../const.js";
17
+ import { HttpError } from "../routing/anvil.types.js";
18
+ import { AllowAnonymous, Controller, Get } from "../routing/index.js";
19
+ import { ReadinessService } from "./readiness.service.js";
20
+ export class HealthController {
21
+ readiness;
22
+ constructor(readiness){
23
+ this.readiness = readiness;
24
+ }
25
+ health() {
26
+ return {
27
+ status: "ok"
28
+ };
29
+ }
30
+ async ready() {
31
+ const result = await this.readiness.checkAll();
32
+ if (!result.ok) {
33
+ throw new HttpError("Not ready", 503);
34
+ }
35
+ return {
36
+ status: "ok"
37
+ };
38
+ }
39
+ }
40
+ _ts_decorate([
41
+ AllowAnonymous(),
42
+ Get("/health"),
43
+ _ts_metadata("design:type", Function),
44
+ _ts_metadata("design:paramtypes", []),
45
+ _ts_metadata("design:returntype", void 0)
46
+ ], HealthController.prototype, "health", null);
47
+ _ts_decorate([
48
+ AllowAnonymous(),
49
+ Get("/ready"),
50
+ _ts_metadata("design:type", Function),
51
+ _ts_metadata("design:paramtypes", []),
52
+ _ts_metadata("design:returntype", Promise)
53
+ ], HealthController.prototype, "ready", null);
54
+ HealthController = _ts_decorate([
55
+ Controller({
56
+ prefix: "/",
57
+ globalPrefix: false
58
+ }),
59
+ injectable(),
60
+ _ts_param(0, inject(ReadinessServiceToken)),
61
+ _ts_metadata("design:type", Function),
62
+ _ts_metadata("design:paramtypes", [
63
+ typeof ReadinessService === "undefined" ? Object : ReadinessService
64
+ ])
65
+ ], HealthController);
@@ -0,0 +1,40 @@
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 { ReadinessCheckToken, ReadinessServiceToken } from "../const.js";
13
+ import "../http/http.extensions.js";
14
+ import { getOrCreateHttpOptions } from "../http/http.options.store.js";
15
+ import { HealthController } from "./health.controller.js";
16
+ import { ReadinessService } from "./readiness.service.js";
17
+ export class BuilderExtensions {
18
+ static withHealth(builder, options) {
19
+ builder.withServices((containerBuilder)=>{
20
+ containerBuilder.registerSingleton(ReadinessServiceToken, ReadinessService);
21
+ for (const check of options?.checks ?? []){
22
+ containerBuilder.registerSingleton(ReadinessCheckToken, check);
23
+ }
24
+ });
25
+ if (options?.underGlobalPrefix === true) {
26
+ getOrCreateHttpOptions(builder).underGlobalPrefix.add(HealthController);
27
+ }
28
+ builder.withControllers(HealthController);
29
+ return builder;
30
+ }
31
+ }
32
+ _ts_decorate([
33
+ extension(Builder),
34
+ _ts_metadata("design:type", Function),
35
+ _ts_metadata("design:paramtypes", [
36
+ typeof Builder === "undefined" ? Object : Builder,
37
+ typeof WithHealthOptions === "undefined" ? Object : WithHealthOptions
38
+ ]),
39
+ _ts_metadata("design:returntype", void 0)
40
+ ], BuilderExtensions, "withHealth", null);
@@ -0,0 +1,4 @@
1
+ export * from "./health.controller.js";
2
+ export * from "./health.extensions.js";
3
+ export * from "./readiness.service.js";
4
+ export * from "./readiness.types.js";
@@ -0,0 +1,57 @@
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 { inject, injectable } from "@decohere/crucible/decorators";
16
+ import { HostedServiceHostToken, ReadinessCheckToken } from "../const.js";
17
+ export class ReadinessService {
18
+ hostedServices;
19
+ checks;
20
+ constructor(checks = [], hostedServices){
21
+ this.hostedServices = hostedServices;
22
+ this.checks = checks;
23
+ }
24
+ /** Registered checks, then each started hosted service that has a `check()` (still warming up). */ async checkAll() {
25
+ const failures = [];
26
+ const probes = [
27
+ ...this.checks,
28
+ ...this.hostedServices?.readinessChecks() ?? []
29
+ ];
30
+ for (const probe of probes){
31
+ try {
32
+ await probe.check();
33
+ } catch {
34
+ failures.push(probe.name);
35
+ }
36
+ }
37
+ if (failures.length > 0) {
38
+ return {
39
+ ok: false,
40
+ failures
41
+ };
42
+ }
43
+ return {
44
+ ok: true
45
+ };
46
+ }
47
+ }
48
+ ReadinessService = _ts_decorate([
49
+ injectable(),
50
+ _ts_param(0, inject(ReadinessCheckToken)),
51
+ _ts_param(1, inject(HostedServiceHostToken)),
52
+ _ts_metadata("design:type", Function),
53
+ _ts_metadata("design:paramtypes", [
54
+ Array,
55
+ typeof HostedServiceHost === "undefined" ? Object : HostedServiceHost
56
+ ])
57
+ ], ReadinessService);
@@ -0,0 +1,4 @@
1
+ /**
2
+ * A dependency probe run by `GET /ready`.
3
+ * Register instances under {@link ReadinessCheckToken} (multi-inject).
4
+ */ export { };