@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,91 @@
1
+ /** Incremental parser for the `text/event-stream` format. */ class SseParser {
2
+ buffer = "";
3
+ data = [];
4
+ current = {
5
+ data: ""
6
+ };
7
+ hasFields = false;
8
+ events = [];
9
+ comments = [];
10
+ push(chunk) {
11
+ this.buffer += chunk;
12
+ let newline;
13
+ while((newline = this.buffer.search(/\r\n|\r|\n/)) !== -1){
14
+ const line = this.buffer.slice(0, newline);
15
+ const eol = this.buffer.slice(newline).startsWith("\r\n") ? 2 : 1;
16
+ this.buffer = this.buffer.slice(newline + eol);
17
+ this.line(line);
18
+ }
19
+ }
20
+ line(line) {
21
+ if (line === "") {
22
+ if (this.hasFields) {
23
+ this.events.push({
24
+ ...this.current,
25
+ data: this.data.join("\n")
26
+ });
27
+ }
28
+ this.data = [];
29
+ this.current = {
30
+ data: ""
31
+ };
32
+ this.hasFields = false;
33
+ return;
34
+ }
35
+ if (line.startsWith(":")) {
36
+ this.comments.push(line.slice(1).trim());
37
+ return;
38
+ }
39
+ const colon = line.indexOf(":");
40
+ const field = colon === -1 ? line : line.slice(0, colon);
41
+ let value = colon === -1 ? "" : line.slice(colon + 1);
42
+ if (value.startsWith(" ")) value = value.slice(1);
43
+ this.hasFields = true;
44
+ if (field === "data") this.data.push(value);
45
+ else if (field === "event") this.current.event = value;
46
+ else if (field === "id") this.current.id = value;
47
+ else if (field === "retry") this.current.retry = Number(value);
48
+ }
49
+ }
50
+ /**
51
+ * GET `url` as an `EventSource` would and collect events until `count` have arrived or the
52
+ * server ends the stream. A non-200 answer (401, 403, ...) returns at once with no events.
53
+ */ export async function readEventStream(url, options = {}) {
54
+ const count = options.count ?? 1;
55
+ const abort = new AbortController();
56
+ const timer = setTimeout(()=>abort.abort(new Error(`No ${count} events from ${url} in time`)), options.timeoutMs ?? 5000);
57
+ try {
58
+ const response = await fetch(url, {
59
+ headers: {
60
+ accept: "text/event-stream",
61
+ ...options.headers
62
+ },
63
+ signal: abort.signal
64
+ });
65
+ const parser = new SseParser();
66
+ let ended = false;
67
+ if (response.status === 200 && response.body != null) {
68
+ const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
69
+ while(parser.events.length < count){
70
+ const { done, value } = await reader.read();
71
+ if (done) {
72
+ ended = true;
73
+ break;
74
+ }
75
+ parser.push(value);
76
+ }
77
+ await reader.cancel();
78
+ } else {
79
+ await response.body?.cancel();
80
+ }
81
+ return {
82
+ status: response.status,
83
+ headers: response.headers,
84
+ events: parser.events.slice(0, count),
85
+ comments: parser.comments,
86
+ ended
87
+ };
88
+ } finally{
89
+ clearTimeout(timer);
90
+ }
91
+ }
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@decohere/http",
3
+ "version": "0.0.1-dev.116",
4
+ "description": "Web API and microservice platform built on Fastify with decorator-based routing",
5
+ "keywords": [
6
+ "typescript",
7
+ "http",
8
+ "fastify",
9
+ "rest",
10
+ "api",
11
+ "microservice",
12
+ "decorators",
13
+ "decohere"
14
+ ],
15
+ "license": "ISC",
16
+ "author": "Typesmiths",
17
+ "type": "module",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./types/src/index.d.ts",
21
+ "import": "./es6/src/index.js",
22
+ "require": "./commonjs/src/index.js"
23
+ },
24
+ "./providers": {
25
+ "types": "./types/src/providers/index.d.ts",
26
+ "import": "./es6/src/providers/index.js",
27
+ "require": "./commonjs/src/providers/index.js"
28
+ },
29
+ "./auth": {
30
+ "types": "./types/src/auth/auth.extensions.d.ts",
31
+ "import": "./es6/src/auth/auth.extensions.js",
32
+ "require": "./commonjs/src/auth/auth.extensions.js"
33
+ },
34
+ "./testing": {
35
+ "types": "./types/src/testing/index.d.ts",
36
+ "import": "./es6/src/testing/index.js",
37
+ "require": "./commonjs/src/testing/index.js"
38
+ },
39
+ "./http": {
40
+ "types": "./types/src/http/index.d.ts",
41
+ "import": "./es6/src/http/index.js",
42
+ "require": "./commonjs/src/http/index.js"
43
+ }
44
+ },
45
+ "scripts": {
46
+ "test": "echo \"Error: no test specified\" && exit 1"
47
+ },
48
+ "dependencies": {
49
+ "@casl/ability": "^6.8.1",
50
+ "@decohere/core": "0.0.1-dev.116",
51
+ "@decohere/crucible": "0.0.1-dev.116",
52
+ "@decohere/scribe": "0.0.1-dev.116",
53
+ "@fastify/compress": "^9.2.1",
54
+ "@fastify/cors": "^11.3.0",
55
+ "@fastify/helmet": "^13.1.0",
56
+ "@fastify/rate-limit": "^11.2.0",
57
+ "@fastify/static": "^10.1.6",
58
+ "ajv": "^8.18.0",
59
+ "fastify": "^5.8.5",
60
+ "jose": "^6.2.3",
61
+ "picomatch": "^4.0.4"
62
+ },
63
+ "devDependencies": {}
64
+ }
@@ -0,0 +1,41 @@
1
+ import { type LifetimeScope } from "@decohere/crucible";
2
+ import { type IConfigurationService } from "@decohere/core/configuration";
3
+ import type { HostedServiceHost } from "../hosting/hosted-service-host.js";
4
+ import type { HttpProvider, StartResponse } from "../routing/anvil.types.js";
5
+ export declare class ApiConfiguration {
6
+ port: number;
7
+ }
8
+ export type StartOptions = {
9
+ port: number;
10
+ host?: string;
11
+ /**
12
+ * Signals that stop the app gracefully: stop accepting connections, let in-flight
13
+ * requests finish, stop hosted services, then dispose services. Default SIGTERM and SIGINT; `false` to
14
+ * handle signals yourself.
15
+ */
16
+ shutdownSignals?: readonly NodeJS.Signals[] | false;
17
+ /** How long a signalled shutdown may take before the process exits with code 1. Default 10s. */
18
+ shutdownTimeoutMs?: number;
19
+ };
20
+ export declare class App {
21
+ private readonly container;
22
+ private configurationService?;
23
+ private httpProvider?;
24
+ private hostedServices?;
25
+ private _addresses;
26
+ private logger;
27
+ private disposing;
28
+ private disposingContainer;
29
+ private signalHandler;
30
+ private shutdownSignals;
31
+ constructor(container: LifetimeScope, configurationService?: IConfigurationService, httpProvider?: HttpProvider, hostedServices?: HostedServiceHost);
32
+ get addresses(): readonly string[];
33
+ start(options?: StartOptions): Promise<StartResponse>;
34
+ [Symbol.asyncDispose](): Promise<void>;
35
+ /** Stop the app. Calling it again (or from a signal) returns the same shutdown. */
36
+ dispose(): Promise<void>;
37
+ private stop;
38
+ private stopOnSignals;
39
+ private removeSignalHandlers;
40
+ private getPort;
41
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./app.js";
2
+ export * from "./module.js";
@@ -0,0 +1,15 @@
1
+ import type { ContainerBuilder } from "@decohere/crucible";
2
+ import type { Constructable } from "@decohere/core/reflection";
3
+ export type ModuleDefinition = {
4
+ name: string;
5
+ entities?: Constructable[];
6
+ repositories?: Constructable[];
7
+ registerServices?(containerBuilder: ContainerBuilder): void;
8
+ };
9
+ export declare abstract class Module {
10
+ abstract name: string;
11
+ constructor(data: Partial<Module>);
12
+ readonly entities?: readonly Constructable[];
13
+ readonly repositories?: readonly Constructable[];
14
+ abstract registerServices?(containerBuilder: ContainerBuilder): void;
15
+ }
@@ -0,0 +1,19 @@
1
+ import type { Principal } from "./auth.types.js";
2
+ /** Root role — expands to CASL `manage`/`all` (covers every future ability). */
3
+ export declare const ADMIN_ROLE = "admin";
4
+ /**
5
+ * Default CASL ability factory.
6
+ *
7
+ * - `admin` role → `manage` / `all` (wildcard)
8
+ * - otherwise map `principal.scopes` of the form `"action:Subject"` to CASL rules
9
+ *
10
+ * Pass a custom factory to `withAuth()` (or override the app stub) for product-specific rules.
11
+ */
12
+ export declare function defineAbilitiesFor(principal: Principal): import("@casl/ability").MongoAbility<import("@casl/ability").AbilityTuple, import("@casl/ability").MongoQuery>;
13
+ /**
14
+ * Map a raw JWT payload → {@link Principal}.
15
+ *
16
+ * Merges OAuth `scope` / `scopes` with an IdP `permissions` claim (e.g. Auth0 API permissions).
17
+ * Override via `withAuth(..., { principalMapper })` when claims differ.
18
+ */
19
+ export declare function createPrincipalFromPayload(payload: Record<string, unknown>): Principal;
@@ -0,0 +1,28 @@
1
+ import type { Principal } from "./auth.types.js";
2
+ /**
3
+ * Configuration for the TokenService, read from the `auth` section of the
4
+ * application config file. Exactly one of the discriminant fields must be set.
5
+ *
6
+ * Production → use `jwksUrl`, `publicKey`, or `secret`.
7
+ * Development → use `devStatic` (only works when NODE_ENV=development).
8
+ */
9
+ export declare class AuthConfig {
10
+ /** OIDC discovery URL — the issuer base URL, e.g. https://accounts.google.com.
11
+ * The framework appends `/.well-known/openid-configuration` automatically.
12
+ * Use this instead of `jwksUrl` for any standard OIDC-compliant IdP.
13
+ */
14
+ discoveryUrl?: string;
15
+ /** JWKS endpoint URL (RS256 / ES256 via remote key set). */
16
+ jwksUrl?: string;
17
+ /** PEM-encoded public key or JWK JSON string (RS256 / ES256 static key). */
18
+ publicKey?: string;
19
+ algorithms?: string[];
20
+ /** Shared secret for HS256. */
21
+ secret?: string;
22
+ /** Issuer claim to validate (all non-devStatic variants). */
23
+ issuer?: string;
24
+ /** Audience claim to validate (all non-devStatic variants). */
25
+ audience?: string;
26
+ /** Static principal injected for every request (development only). */
27
+ devStatic?: Principal;
28
+ }
@@ -0,0 +1,53 @@
1
+ import { Builder } from "../builder/builder.js";
2
+ import type { PrincipalMapper } from "./auth.types.js";
3
+ import type { AbilityFactory } from "./casl-authorization.service.js";
4
+ export { ADMIN_ROLE, createPrincipalFromPayload, defineAbilitiesFor } from "./ability.js";
5
+ export type WithAuthOptions = {
6
+ /**
7
+ * Override the default JWT payload → Principal mapping.
8
+ * Use this when your IdP stores roles or scopes in non-standard claims.
9
+ *
10
+ * @example Keycloak realm roles:
11
+ * ```ts
12
+ * builder.withAuth(factory, {
13
+ * principalMapper: (p) => ({
14
+ * sub: p.sub as string,
15
+ * scopes: (p.realm_access as any)?.roles?.map((r: string) => `manage:${r}`) ?? [],
16
+ * }),
17
+ * });
18
+ * ```
19
+ */
20
+ principalMapper?: PrincipalMapper;
21
+ };
22
+ declare module "@decohere/http" {
23
+ interface Builder {
24
+ /**
25
+ * Configure JWT authentication from the `auth` config section and wire
26
+ * up a CASL authorization service.
27
+ *
28
+ * By default, `defineAbilitiesFor` grants `manage`/`all` for the `admin`
29
+ * role and maps `principal.scopes` of the form `"action:Subject"` to CASL
30
+ * rules. Pass a custom `abilityFactory` to override that behaviour.
31
+ *
32
+ * Requires `.withConfiguration(...)` to be called on the builder.
33
+ *
34
+ * @example Default (scope-based, no boilerplate):
35
+ * ```ts
36
+ * builder.withConfiguration(YamlDataSource).withAuth();
37
+ * ```
38
+ *
39
+ * @example Custom override:
40
+ * ```ts
41
+ * builder.withAuth((principal) => {
42
+ * const { can, build } = new AbilityBuilder(createMongoAbility);
43
+ * // custom logic ...
44
+ * return build();
45
+ * });
46
+ * ```
47
+ */
48
+ withAuth(abilityFactory?: AbilityFactory, options?: WithAuthOptions): Builder;
49
+ }
50
+ }
51
+ export declare class BuilderExtensions {
52
+ static withAuth(builder: Builder, abilityFactory?: AbilityFactory, options?: WithAuthOptions): Builder<import("@decohere/http").App>;
53
+ }
@@ -0,0 +1,73 @@
1
+ export type Principal = {
2
+ sub: string;
3
+ roles?: string[];
4
+ scopes?: string[];
5
+ [key: string]: unknown;
6
+ };
7
+ /** Maps a raw JWT payload to a Principal. Override the default via withAuth() options. */
8
+ export type PrincipalMapper = (payload: Record<string, unknown>) => Principal;
9
+ export type JwksConfig = {
10
+ jwksUrl: string;
11
+ issuer?: string;
12
+ /** Required — empty/undefined disables jose `aud` checks (cross-API token accept). */
13
+ audience: string;
14
+ };
15
+ export type PublicKeyConfig = {
16
+ publicKey: string;
17
+ algorithms?: string[];
18
+ issuer?: string;
19
+ /** Required — empty/undefined disables jose `aud` checks (cross-API token accept). */
20
+ audience: string;
21
+ };
22
+ export type SecretConfig = {
23
+ secret: string;
24
+ issuer?: string;
25
+ /** Required — empty/undefined disables jose `aud` checks (cross-API token accept). */
26
+ audience: string;
27
+ };
28
+ export type DevStaticConfig = {
29
+ devStatic: Principal;
30
+ };
31
+ /**
32
+ * Resolve IdP configuration automatically from an OIDC discovery endpoint
33
+ * (`{discoveryUrl}/.well-known/openid-configuration`). The `discoveryUrl` is
34
+ * also used as the expected `iss` claim unless the discovery document provides
35
+ * a different value.
36
+ */
37
+ export type DiscoveryConfig = {
38
+ discoveryUrl: string;
39
+ /** Required — empty/undefined disables jose `aud` checks (cross-API token accept). */
40
+ audience: string;
41
+ };
42
+ /**
43
+ * Framework-agnostic authorization service. Implement this interface (e.g.
44
+ * via CaslAuthorizationService) and register it under AuthorizationServiceToken.
45
+ * Throw ForbiddenError to deny access.
46
+ */
47
+ export interface IAuthorizationService {
48
+ authorizeRequest(request: import("../routing/anvil.types.js").HttpRequest, action: string, subject: string): Promise<Principal>;
49
+ /**
50
+ * Verify the request has a valid principal without checking a CASL ability.
51
+ * Used when auth is enabled but the route has no `@Authorize` metadata.
52
+ */
53
+ authenticateRequest(request: import("../routing/anvil.types.js").HttpRequest): Promise<Principal>;
54
+ authorize(principal: Principal, action: string, subject: string): Promise<void>;
55
+ }
56
+ export type TokenServiceConfig = JwksConfig | PublicKeyConfig | SecretConfig | DevStaticConfig | DiscoveryConfig;
57
+ export declare function isDiscoveryConfig(config: TokenServiceConfig): config is DiscoveryConfig;
58
+ export declare function isDevStaticConfig(config: TokenServiceConfig): config is DevStaticConfig;
59
+ export declare function isJwksConfig(config: TokenServiceConfig): config is JwksConfig;
60
+ export declare function isPublicKeyConfig(config: TokenServiceConfig): config is PublicKeyConfig;
61
+ export declare function isSecretConfig(config: TokenServiceConfig): config is SecretConfig;
62
+ /** `devStatic` is only valid for local development NODE_ENV values. */
63
+ export declare function isDevelopmentNodeEnv(nodeEnv?: string): boolean;
64
+ /**
65
+ * Refuse `devStatic` outside development — otherwise any Bearer string is
66
+ * accepted as the configured principal (auth bypass in production).
67
+ */
68
+ export declare function assertDevStaticAllowed(nodeEnv?: string): void;
69
+ /**
70
+ * jose skips `aud` validation when `audience` is undefined or "". Require a
71
+ * non-empty audience for every non-devStatic verification strategy.
72
+ */
73
+ export declare function assertNonEmptyAudience(audience: string | undefined): asserts audience is string;
@@ -0,0 +1,28 @@
1
+ import type { AnyAbility } from "@casl/ability";
2
+ import type { HttpRequest } from "../routing/anvil.types.js";
3
+ import type { IAuthorizationService, Principal } from "./auth.types.js";
4
+ import type { TokenService } from "./token-service.js";
5
+ export type AbilityFactory = (principal: Principal) => AnyAbility;
6
+ /**
7
+ * IAuthorizationService implementation backed by CASL.
8
+ *
9
+ * Register it under AuthorizationServiceToken:
10
+ * ```ts
11
+ * builder.withServices((c) =>
12
+ * c.registerSingleton(AuthorizationServiceToken, new CaslAuthorizationService((principal) => {
13
+ * const { can, build } = new AbilityBuilder(createMongoAbility);
14
+ * if (principal.roles?.includes("admin")) can("manage", "all");
15
+ * else can("read", "Post");
16
+ * return build();
17
+ * }))
18
+ * );
19
+ * ```
20
+ */
21
+ export declare class CaslAuthorizationService implements IAuthorizationService {
22
+ private readonly factory;
23
+ private readonly tokenService?;
24
+ constructor(factory: AbilityFactory, tokenService?: TokenService);
25
+ authorizeRequest(request: HttpRequest, action: string, subject: string): Promise<Principal>;
26
+ authenticateRequest(request: HttpRequest): Promise<Principal>;
27
+ authorize(principal: Principal, action: string, subject: string): Promise<void>;
28
+ }
@@ -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,20 @@
1
+ import type { Principal } from "./auth.types.js";
2
+ /**
3
+ * Read-only view of the authenticated principal for the current request.
4
+ * Inject this interface (via IPrincipalProviderToken) in any request-scoped
5
+ * service to access the verified identity without coupling to the routing layer.
6
+ */
7
+ export interface IPrincipalProvider {
8
+ readonly currentPrincipal: Principal | undefined;
9
+ }
10
+ /**
11
+ * Concrete, request-scoped implementation. The Router sets the principal
12
+ * after JWT verification; downstream services consume it via IPrincipalProvider.
13
+ *
14
+ * @internal — framework use only. Inject IPrincipalProvider, not this class.
15
+ */
16
+ export declare class PrincipalProvider implements IPrincipalProvider {
17
+ private _principal;
18
+ get currentPrincipal(): Principal | undefined;
19
+ setPrincipal(principal: Principal | undefined): void;
20
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Validate URLs used for OIDC discovery / JWKS fetches to reduce SSRF risk.
3
+ *
4
+ * - http(s) only; https required except loopback (local IdP tests)
5
+ * - no credentials in the URL
6
+ * - hostname must not resolve to private, link-local, or metadata ranges
7
+ * (loopback allowed only when NODE_ENV is development/dev/test)
8
+ */
9
+ export declare function assertSafeOutboundUrl(raw: string, label: string): Promise<URL>;
@@ -0,0 +1,31 @@
1
+ import { type JWTPayload } from "jose";
2
+ import type { HttpRequest } from "../routing/anvil.types.js";
3
+ import type { Principal, PrincipalMapper, TokenServiceConfig } from "./auth.types.js";
4
+ export declare class TokenService {
5
+ private readonly config;
6
+ private readonly principalMapper?;
7
+ /** Lazily resolved JWKS set when using OIDC discovery. */
8
+ private discoveryJwks?;
9
+ private discoveryIssuer?;
10
+ /** Remote key set for `jwksUrl` mode, created once so jose can cache the keys (#499). */
11
+ private configuredJwks?;
12
+ constructor(config: TokenServiceConfig, principalMapper?: PrincipalMapper);
13
+ verifyRequest(request: HttpRequest): Promise<Principal>;
14
+ verify(credentials: string): Promise<Principal>;
15
+ /**
16
+ * Resolve how to verify tokens for the configured strategy:
17
+ * - OIDC discovery → fetch JWKS + issuer from `/.well-known/openid-configuration`
18
+ * - explicit JWKS URL → remote key set + configured issuer/audience
19
+ * - public key / shared secret → static key material + configured issuer/audience
20
+ */
21
+ private resolveVerificationContext;
22
+ private resolveOidcDiscovery;
23
+ /**
24
+ * Sign a payload and return a JWT string.
25
+ * Only available when configured with a SecretConfig (HS256).
26
+ * For RS256/ES256 issuance, the caller is responsible for key management.
27
+ */
28
+ sign(payload: Omit<JWTPayload, "iss" | "aud"> & Record<string, unknown>, expiresIn?: string): Promise<string>;
29
+ private getStaticKey;
30
+ private mapPrincipal;
31
+ }
@@ -0,0 +1,36 @@
1
+ import { type ContainerBuilder, ServiceContainerBuilder, type IContainer } from "@decohere/crucible";
2
+ import { type IConfigurationService } from "@decohere/core/configuration";
3
+ import type { Constructable } from "@decohere/core/reflection";
4
+ import { App } from "../app/app.js";
5
+ import type { HostedService } from "../hosting/hosted-service.js";
6
+ export type BuilderOptions = {
7
+ containerBuilder?: ServiceContainerBuilder;
8
+ applicationName?: string;
9
+ };
10
+ export declare class Builder<T extends App = App> {
11
+ static create<T extends App = App>(options?: BuilderOptions): Builder<T>;
12
+ readonly containerBuilder: ServiceContainerBuilder;
13
+ readonly applicationName?: string;
14
+ private readonly logger;
15
+ private userServices;
16
+ private preBuildSteps;
17
+ private postBuildSteps;
18
+ private readonly hostedServices;
19
+ constructor(options?: BuilderOptions);
20
+ withServices(userServices: (containerBuilder: ContainerBuilder) => void): Builder<T>;
21
+ /**
22
+ * Run {@link HostedService}s for the life of the app: started in this order before the HTTP
23
+ * server listens, stopped in reverse when the app stops (including SIGTERM and Flux reboots).
24
+ * Each is registered as a singleton, so other services can inject it by its class.
25
+ */
26
+ withHostedService(...types: Constructable<HostedService>[]): Builder<T>;
27
+ registerPreBuildStep(step: (config: IConfigurationService, containerBuilder: ContainerBuilder) => Promise<void> | void): void;
28
+ registerPostBuildStep(step: (container: IContainer) => Promise<void> | void): void;
29
+ buildAndStart<T extends App>(type?: Constructable<T>): Promise<T | App>;
30
+ build<T extends App = App>(type?: Constructable<T>): Promise<T | App>;
31
+ private configureUserServices;
32
+ private runPreBuildSteps;
33
+ private runPostBuildSteps;
34
+ private configureApp;
35
+ private resolveApp;
36
+ }
@@ -0,0 +1 @@
1
+ export * from "./builder.js";
@@ -0,0 +1,32 @@
1
+ export declare const RouteRegistryToken: unique symbol;
2
+ export declare const HttpProviderToken: unique symbol;
3
+ export declare const RouterToken: unique symbol;
4
+ export declare const PrincipalToken: unique symbol;
5
+ export declare const IPrincipalProviderToken: unique symbol;
6
+ export declare const TokenServiceToken: unique symbol;
7
+ export declare const AuthorizationServiceToken: unique symbol;
8
+ export declare const HttpOptionsToken: unique symbol;
9
+ export declare const ExceptionFilterHostToken: unique symbol;
10
+ export declare const ReadinessCheckToken: unique symbol;
11
+ export declare const ReadinessServiceToken: unique symbol;
12
+ export declare const HostedServiceHostToken: unique symbol;
13
+ export type HTTPMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS";
14
+ export declare const HTTPMethods: {
15
+ GET: "GET";
16
+ PUT: "PUT";
17
+ POST: "POST";
18
+ PATCH: "PATCH";
19
+ DELETE: "DELETE";
20
+ };
21
+ export declare const MetadataKeys: {
22
+ controller: string;
23
+ route: string;
24
+ parameter: string;
25
+ fieldMap: string;
26
+ authorize: string;
27
+ allowAnonymous: string;
28
+ httpCode: string;
29
+ headers: string;
30
+ catch: string;
31
+ sse: string;
32
+ };
@@ -0,0 +1,16 @@
1
+ import { ReadinessService } from "./readiness.service.js";
2
+ /**
3
+ * Liveness (`/health`) and readiness (`/ready`) probes.
4
+ * Both are anonymous so orchestration can scrape them with auth enabled, and stay at the root
5
+ * under a global prefix, where platforms look for them.
6
+ */
7
+ export declare class HealthController {
8
+ private readonly readiness;
9
+ constructor(readiness: ReadinessService);
10
+ health(): {
11
+ status: string;
12
+ };
13
+ ready(): Promise<{
14
+ status: string;
15
+ }>;
16
+ }
@@ -0,0 +1,31 @@
1
+ import { Builder } from "../builder/builder.js";
2
+ import "../http/http.extensions.js";
3
+ import type { IReadinessCheck } from "./readiness.types.js";
4
+ export type WithHealthOptions = {
5
+ /** Extra readiness probes (e.g. custom dependency checks). */
6
+ checks?: IReadinessCheck[];
7
+ /**
8
+ * Serve the probes under the global prefix (`/api/health`) instead of at the root. Default
9
+ * `false`: Cloud Run, Kubernetes and most load balancers probe the root.
10
+ */
11
+ underGlobalPrefix?: boolean;
12
+ };
13
+ declare module "@decohere/http" {
14
+ interface Builder {
15
+ /**
16
+ * Register anonymous `/health` (liveness) and `/ready` (readiness) routes.
17
+ *
18
+ * Readiness runs every {@link IReadinessCheck} registered under
19
+ * {@link ReadinessCheckToken} (TypeORM registers one per datasource when
20
+ * `.withTypeOrm()` is used). Pass `checks` for app-specific probes.
21
+ *
22
+ * The probes stay at the root under `withGlobalPrefix`, unless `underGlobalPrefix` is set.
23
+ *
24
+ * Requires `.withHttpProvider(...)` (or `@decohere/http/http` side-effect).
25
+ */
26
+ withHealth(options?: WithHealthOptions): Builder;
27
+ }
28
+ }
29
+ export declare class BuilderExtensions {
30
+ static withHealth(builder: Builder, options?: WithHealthOptions): Builder<import("@decohere/http").App>;
31
+ }
@@ -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,9 @@
1
+ import type { HostedServiceHost } from "../hosting/hosted-service-host.js";
2
+ import type { IReadinessCheck, ReadinessResult } from "./readiness.types.js";
3
+ export declare class ReadinessService {
4
+ private readonly hostedServices?;
5
+ private readonly checks;
6
+ constructor(checks?: IReadinessCheck[], hostedServices?: HostedServiceHost);
7
+ /** Registered checks, then each started hosted service that has a `check()` (still warming up). */
8
+ checkAll(): Promise<ReadinessResult>;
9
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * A dependency probe run by `GET /ready`.
3
+ * Register instances under {@link ReadinessCheckToken} (multi-inject).
4
+ */
5
+ export interface IReadinessCheck {
6
+ readonly name: string;
7
+ check(): Promise<void>;
8
+ }
9
+ export type ReadinessResult = {
10
+ ok: true;
11
+ } | {
12
+ ok: false;
13
+ failures: string[];
14
+ };