@o3co/auth-provider-foundation 0.17.0 → 0.18.0-rc.1

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 (293) hide show
  1. package/README.md +2 -867
  2. package/audit/README.md +38 -0
  3. package/audit/config.example +3 -0
  4. package/audit/src/contract.d.ts +37 -0
  5. package/audit/src/contract.js +29 -0
  6. package/audit/src/module.d.ts +2 -0
  7. package/audit/src/module.js +151 -0
  8. package/audit/src/section.d.ts +5 -0
  9. package/audit/src/section.js +19 -0
  10. package/audit/src/testing.d.ts +16 -0
  11. package/audit/src/testing.js +19 -0
  12. package/client-certificate/README.md +27 -0
  13. package/client-certificate/src/certificate.d.ts +9 -0
  14. package/client-certificate/src/certificate.js +40 -0
  15. package/client-certificate/src/contract.d.ts +23 -0
  16. package/client-certificate/src/contract.js +5 -0
  17. package/client-certificate/src/pem.d.ts +4 -0
  18. package/client-certificate/src/pem.js +19 -0
  19. package/client-certificate/src/settings.d.ts +13 -0
  20. package/client-certificate/src/settings.js +22 -0
  21. package/client-certificate/src/warnings.d.ts +7 -0
  22. package/client-certificate/src/warnings.js +17 -0
  23. package/client-certificate/src/xfcc.d.ts +5 -0
  24. package/client-certificate/src/xfcc.js +72 -0
  25. package/cookies/README.md +34 -0
  26. package/cookies/src/contract.d.ts +15 -0
  27. package/cookies/src/contract.js +3 -0
  28. package/cookies/src/cookies.d.ts +3 -0
  29. package/cookies/src/cookies.js +39 -0
  30. package/cookies/src/module.d.ts +2 -0
  31. package/cookies/src/module.js +7 -0
  32. package/external-jwt/README.md +38 -0
  33. package/external-jwt/src/contract.d.ts +58 -0
  34. package/external-jwt/src/contract.js +2 -0
  35. package/external-jwt/src/jwks.d.ts +20 -0
  36. package/external-jwt/src/jwks.js +61 -0
  37. package/external-jwt/src/keys.d.ts +16 -0
  38. package/external-jwt/src/keys.js +26 -0
  39. package/external-jwt/src/module.d.ts +6 -0
  40. package/external-jwt/src/module.js +21 -0
  41. package/external-jwt/src/testing.d.ts +38 -0
  42. package/external-jwt/src/testing.js +43 -0
  43. package/external-jwt/src/verifier.d.ts +10 -0
  44. package/external-jwt/src/verifier.js +147 -0
  45. package/http/README.md +39 -0
  46. package/http/config.example +5 -0
  47. package/http/src/bodies/README.md +31 -0
  48. package/http/src/bodies/body.d.ts +6 -0
  49. package/http/src/bodies/body.js +69 -0
  50. package/http/src/bodies/json.d.ts +2 -0
  51. package/http/src/bodies/json.js +35 -0
  52. package/http/src/bodies/values.d.ts +20 -0
  53. package/http/src/bodies/values.js +45 -0
  54. package/http/src/contract.d.ts +113 -0
  55. package/http/src/contract.js +23 -0
  56. package/http/src/cors/README.md +24 -0
  57. package/http/src/cors/cors.d.ts +5 -0
  58. package/http/src/cors/cors.js +35 -0
  59. package/http/src/ending/README.md +23 -0
  60. package/http/src/ending/ending.d.ts +5 -0
  61. package/http/src/ending/ending.js +27 -0
  62. package/http/src/guards/README.md +20 -0
  63. package/http/src/guards/guards.d.ts +19 -0
  64. package/http/src/guards/guards.js +31 -0
  65. package/http/src/module.d.ts +2 -0
  66. package/http/src/module.js +46 -0
  67. package/http/src/security-headers/README.md +23 -0
  68. package/http/src/security-headers/security-headers.d.ts +3 -0
  69. package/http/src/security-headers/security-headers.js +58 -0
  70. package/http/src/serving/README.md +33 -0
  71. package/http/src/serving/server.d.ts +17 -0
  72. package/http/src/serving/server.js +174 -0
  73. package/http/src/settings/README.md +16 -0
  74. package/http/src/settings/section.d.ts +21 -0
  75. package/http/src/settings/section.js +37 -0
  76. package/http/src/testing.d.ts +37 -0
  77. package/http/src/testing.js +24 -0
  78. package/http-ops/README.md +22 -0
  79. package/http-ops/src/liveness.d.ts +3 -0
  80. package/http-ops/src/liveness.js +10 -0
  81. package/http-ops/src/module.d.ts +2 -0
  82. package/http-ops/src/module.js +24 -0
  83. package/http-ops/src/readiness.d.ts +10 -0
  84. package/http-ops/src/readiness.js +54 -0
  85. package/http-ops/src/section.d.ts +4 -0
  86. package/http-ops/src/section.js +10 -0
  87. package/http-ops/src/timing.d.ts +9 -0
  88. package/http-ops/src/timing.js +30 -0
  89. package/kernel/README.md +39 -0
  90. package/kernel/config.example +4 -0
  91. package/kernel/src/assemble.d.ts +27 -0
  92. package/kernel/src/assemble.js +429 -0
  93. package/kernel/src/config.d.ts +13 -0
  94. package/kernel/src/config.js +32 -0
  95. package/kernel/src/contract.d.ts +11 -0
  96. package/kernel/src/contract.js +10 -0
  97. package/kernel/src/every.d.ts +7 -0
  98. package/kernel/src/every.js +36 -0
  99. package/kernel/src/freeze.d.ts +5 -0
  100. package/kernel/src/freeze.js +17 -0
  101. package/kernel/src/handles.d.ts +19 -0
  102. package/kernel/src/handles.js +6 -0
  103. package/kernel/src/log.d.ts +12 -0
  104. package/kernel/src/log.js +55 -0
  105. package/kernel/src/module.d.ts +67 -0
  106. package/kernel/src/module.js +18 -0
  107. package/kernel/src/readiness.d.ts +24 -0
  108. package/kernel/src/readiness.js +72 -0
  109. package/kernel/src/refusal.d.ts +19 -0
  110. package/kernel/src/refusal.js +28 -0
  111. package/kernel/src/runtime.d.ts +35 -0
  112. package/kernel/src/runtime.js +5 -0
  113. package/kernel/src/secret.d.ts +2 -0
  114. package/kernel/src/secret.js +7 -0
  115. package/kernel/src/settings.d.ts +20 -0
  116. package/kernel/src/settings.js +23 -0
  117. package/kernel/src/testing.d.ts +31 -0
  118. package/kernel/src/testing.js +99 -0
  119. package/keys/README.md +39 -0
  120. package/keys/config.example +7 -0
  121. package/keys/src/contract.d.ts +21 -0
  122. package/keys/src/contract.js +5 -0
  123. package/keys/src/jwks.d.ts +3 -0
  124. package/keys/src/jwks.js +26 -0
  125. package/keys/src/jws.d.ts +7 -0
  126. package/keys/src/jws.js +11 -0
  127. package/keys/src/module.d.ts +2 -0
  128. package/keys/src/module.js +31 -0
  129. package/keys/src/public.d.ts +21 -0
  130. package/keys/src/public.js +100 -0
  131. package/keys/src/ring.d.ts +19 -0
  132. package/keys/src/ring.js +60 -0
  133. package/keys/src/secret.d.ts +17 -0
  134. package/keys/src/secret.js +33 -0
  135. package/keys/src/section.d.ts +43 -0
  136. package/keys/src/section.js +55 -0
  137. package/keys/src/signer.d.ts +3 -0
  138. package/keys/src/signer.js +61 -0
  139. package/keys/src/testing.d.ts +28 -0
  140. package/keys/src/testing.js +12 -0
  141. package/mail/README.md +45 -0
  142. package/mail/config.example +3 -0
  143. package/mail/src/address.d.ts +7 -0
  144. package/mail/src/address.js +45 -0
  145. package/mail/src/contract.d.ts +38 -0
  146. package/mail/src/contract.js +15 -0
  147. package/mail/src/lines.d.ts +4 -0
  148. package/mail/src/lines.js +4 -0
  149. package/mail/src/module.d.ts +3 -0
  150. package/mail/src/module.js +27 -0
  151. package/mail/src/plain-text.d.ts +6 -0
  152. package/mail/src/plain-text.js +23 -0
  153. package/mail/src/prepare.d.ts +14 -0
  154. package/mail/src/prepare.js +69 -0
  155. package/mail/src/section.d.ts +4 -0
  156. package/mail/src/section.js +7 -0
  157. package/mail/src/testing.d.ts +26 -0
  158. package/mail/src/testing.js +140 -0
  159. package/metrics/README.md +45 -0
  160. package/metrics/src/build.d.ts +6 -0
  161. package/metrics/src/build.js +36 -0
  162. package/metrics/src/contract.d.ts +46 -0
  163. package/metrics/src/contract.js +8 -0
  164. package/metrics/src/gauge.d.ts +9 -0
  165. package/metrics/src/gauge.js +67 -0
  166. package/metrics/src/instrument.d.ts +50 -0
  167. package/metrics/src/instrument.js +58 -0
  168. package/metrics/src/module.d.ts +2 -0
  169. package/metrics/src/module.js +6 -0
  170. package/metrics/src/registry.d.ts +13 -0
  171. package/metrics/src/registry.js +95 -0
  172. package/metrics/src/testing.d.ts +16 -0
  173. package/metrics/src/testing.js +104 -0
  174. package/outbound/README.md +39 -0
  175. package/outbound/config.example +5 -0
  176. package/outbound/src/contract.d.ts +76 -0
  177. package/outbound/src/contract.js +36 -0
  178. package/outbound/src/module.d.ts +28 -0
  179. package/outbound/src/module.js +188 -0
  180. package/outbound/src/policy.d.ts +54 -0
  181. package/outbound/src/policy.js +187 -0
  182. package/outbound/src/transport.d.ts +21 -0
  183. package/outbound/src/transport.js +176 -0
  184. package/package.json +186 -44
  185. package/rate-limit/README.md +39 -0
  186. package/rate-limit/src/audit-events.d.ts +8 -0
  187. package/rate-limit/src/audit-events.js +8 -0
  188. package/rate-limit/src/contract.d.ts +125 -0
  189. package/rate-limit/src/contract.js +27 -0
  190. package/rate-limit/src/lockout.d.ts +13 -0
  191. package/rate-limit/src/lockout.js +195 -0
  192. package/rate-limit/src/module.d.ts +2 -0
  193. package/rate-limit/src/module.js +50 -0
  194. package/rate-limit/src/partition.d.ts +4 -0
  195. package/rate-limit/src/partition.js +4 -0
  196. package/rate-limit/src/per-source.d.ts +11 -0
  197. package/rate-limit/src/per-source.js +52 -0
  198. package/rate-limit/src/source.d.ts +2 -0
  199. package/rate-limit/src/source.js +12 -0
  200. package/rate-limit/src/testing.d.ts +2 -0
  201. package/rate-limit/src/testing.js +68 -0
  202. package/records/README.md +39 -0
  203. package/records/src/records.d.ts +100 -0
  204. package/records/src/records.js +293 -0
  205. package/records/src/shards.d.ts +10 -0
  206. package/records/src/shards.js +2 -0
  207. package/records/src/testing.d.ts +39 -0
  208. package/records/src/testing.js +137 -0
  209. package/request-source/README.md +38 -0
  210. package/request-source/src/contract.d.ts +18 -0
  211. package/request-source/src/contract.js +7 -0
  212. package/request-source/src/settings.d.ts +17 -0
  213. package/request-source/src/settings.js +46 -0
  214. package/request-source/src/source.d.ts +14 -0
  215. package/request-source/src/source.js +55 -0
  216. package/request-source/src/warnings.d.ts +4 -0
  217. package/request-source/src/warnings.js +15 -0
  218. package/seal/README.md +30 -0
  219. package/seal/config.example +8 -0
  220. package/seal/src/contract.d.ts +13 -0
  221. package/seal/src/contract.js +2 -0
  222. package/seal/src/module.d.ts +2 -0
  223. package/seal/src/module.js +57 -0
  224. package/seal/src/section.d.ts +9 -0
  225. package/seal/src/section.js +15 -0
  226. package/seal/src/testing.d.ts +11 -0
  227. package/seal/src/testing.js +5 -0
  228. package/secret-hash/README.md +33 -0
  229. package/secret-hash/src/checker.d.ts +9 -0
  230. package/secret-hash/src/checker.js +53 -0
  231. package/secret-hash/src/contract.d.ts +20 -0
  232. package/secret-hash/src/contract.js +10 -0
  233. package/secret-hash/src/hashes.d.ts +23 -0
  234. package/secret-hash/src/hashes.js +73 -0
  235. package/secret-hash/src/module.d.ts +2 -0
  236. package/secret-hash/src/module.js +17 -0
  237. package/secret-hash/src/testing.d.ts +13 -0
  238. package/secret-hash/src/testing.js +31 -0
  239. package/store/README.md +39 -0
  240. package/store/config.example +3 -0
  241. package/store/src/contract.d.ts +227 -0
  242. package/store/src/contract.js +261 -0
  243. package/store/src/memory.d.ts +11 -0
  244. package/store/src/memory.js +241 -0
  245. package/store/src/module.d.ts +2 -0
  246. package/store/src/module.js +14 -0
  247. package/store/src/section.d.ts +4 -0
  248. package/store/src/section.js +7 -0
  249. package/store/src/simulated.d.ts +41 -0
  250. package/store/src/simulated.js +216 -0
  251. package/store/src/testing.d.ts +33 -0
  252. package/store/src/testing.js +955 -0
  253. package/url-setting/README.md +29 -0
  254. package/url-setting/src/contract.d.ts +18 -0
  255. package/url-setting/src/contract.js +99 -0
  256. package/README.ja.md +0 -309
  257. package/config/reference.conf +0 -20
  258. package/dist/endpointUrl.d.mts +0 -53
  259. package/dist/endpointUrl.d.mts.map +0 -1
  260. package/dist/endpointUrl.mjs +0 -132
  261. package/dist/index.d.mts +0 -11
  262. package/dist/index.d.mts.map +0 -1
  263. package/dist/index.mjs +0 -67
  264. package/dist/mfa/HttpMfaFactorStore.d.mts +0 -60
  265. package/dist/mfa/HttpMfaFactorStore.d.mts.map +0 -1
  266. package/dist/mfa/HttpMfaFactorStore.mjs +0 -205
  267. package/dist/mfa/markEnrolled.d.mts +0 -4
  268. package/dist/mfa/markEnrolled.d.mts.map +0 -1
  269. package/dist/mfa/markEnrolled.mjs +0 -31
  270. package/dist/mfa/module.d.mts +0 -27
  271. package/dist/mfa/module.d.mts.map +0 -1
  272. package/dist/mfa/module.mjs +0 -56
  273. package/dist/mfa/section.d.mts +0 -45
  274. package/dist/mfa/section.d.mts.map +0 -1
  275. package/dist/mfa/section.mjs +0 -99
  276. package/dist/mfa/storeFailure.d.mts +0 -52
  277. package/dist/mfa/storeFailure.d.mts.map +0 -1
  278. package/dist/mfa/storeFailure.mjs +0 -107
  279. package/dist/repositories/HttpUserRepository.d.mts +0 -130
  280. package/dist/repositories/HttpUserRepository.d.mts.map +0 -1
  281. package/dist/repositories/HttpUserRepository.mjs +0 -376
  282. package/dist/repositories/storeErrors.d.mts +0 -72
  283. package/dist/repositories/storeErrors.d.mts.map +0 -1
  284. package/dist/repositories/storeErrors.mjs +0 -183
  285. package/dist/repositories/wwwAuthenticate.d.mts +0 -10
  286. package/dist/repositories/wwwAuthenticate.d.mts.map +0 -1
  287. package/dist/repositories/wwwAuthenticate.mjs +0 -54
  288. package/dist/storeTransport.d.mts +0 -97
  289. package/dist/storeTransport.d.mts.map +0 -1
  290. package/dist/storeTransport.mjs +0 -290
  291. package/dist/testing/index.d.mts +0 -28
  292. package/dist/testing/index.d.mts.map +0 -1
  293. package/dist/testing/index.mjs +0 -58
@@ -0,0 +1,58 @@
1
+ import { createHash } from "node:crypto";
2
+ /** Every answer's policy: nothing loads but from this origin, and no one frames it. */
3
+ const policy = [
4
+ "default-src 'none'",
5
+ "base-uri 'self'",
6
+ "font-src 'self' https: data:",
7
+ "form-action 'self'",
8
+ "frame-ancestors 'none'",
9
+ "img-src 'self' data:",
10
+ "object-src 'none'",
11
+ "script-src 'self'",
12
+ "script-src-attr 'none'",
13
+ "style-src 'self' https: 'unsafe-inline'",
14
+ "upgrade-insecure-requests",
15
+ ].join(";");
16
+ /** The headers besides the policy that every answer carries, as http sets them. */
17
+ const fixed = Object.freeze({
18
+ "cross-origin-opener-policy": "same-origin",
19
+ "cross-origin-resource-policy": "same-origin",
20
+ "origin-agent-cluster": "?1",
21
+ "referrer-policy": "no-referrer",
22
+ "strict-transport-security": "max-age=31536000; includeSubDomains",
23
+ "x-content-type-options": "nosniff",
24
+ "x-dns-prefetch-control": "off",
25
+ "x-download-options": "noopen",
26
+ "x-frame-options": "SAMEORIGIN",
27
+ "x-permitted-cross-domain-policies": "none",
28
+ "x-xss-protection": "0",
29
+ });
30
+ /** An origin a policy's host source can name: no IPv6 literal, no `_`, nothing after the port. */
31
+ const framable = /^https?:\/\/[A-Za-z0-9.-]+(?::\d{1,5})?$/;
32
+ /** A page's policy: its frames and scripts, and nothing else. Throws on a frame it cannot name. */
33
+ function pagePolicy(loads) {
34
+ const frames = [...new Set(loads.frames)];
35
+ if (!frames.every((frame) => framable.test(frame)))
36
+ throw new TypeError("unnamable frame");
37
+ const scripts = (loads.scripts ?? []).map((text) => `'sha256-${createHash("sha256").update(text).digest("base64")}'`);
38
+ return [
39
+ "default-src 'none'",
40
+ "base-uri 'none'",
41
+ "form-action 'none'",
42
+ "frame-ancestors 'none'",
43
+ ...(frames.length ? [`frame-src ${frames.join(" ")}`] : []),
44
+ ...(scripts.length ? [`script-src ${scripts.join(" ")}`] : []),
45
+ ].join("; ");
46
+ }
47
+ /** The answer's headers, the security headers and the policy in place of the route's. */
48
+ export function secured(headers, loads) {
49
+ const own = Object.entries(headers).filter(([name]) => {
50
+ const lower = name.toLowerCase();
51
+ return !Object.hasOwn(fixed, lower) && lower !== "content-security-policy";
52
+ });
53
+ return {
54
+ ...fixed,
55
+ "content-security-policy": loads ? pagePolicy(loads) : policy,
56
+ ...Object.fromEntries(own),
57
+ };
58
+ }
@@ -0,0 +1,33 @@
1
+ # serving
2
+
3
+ Internal module of `http`: the server that answers each request with the route it reaches.
4
+
5
+ ## Responsibilities
6
+ - Reads a request's head, finds the route its path reaches, has the route's policies and handler answer, and writes the answer.
7
+
8
+ ## Not here
9
+ - What a request's body holds (`bodies`), how policies apply (`guards`), cross-origin answers (`cors`), the security headers (`security-headers`), and telling a request's end (`ending`).
10
+ - Checking the segments under a `/*` path: a `/*` handler checks them by its own rules.
11
+
12
+ ## Offers
13
+ - `serve`: the routes, guarded by their policies, answered on a Node HTTP server.
14
+ - `drain`: stops the server, cutting what is left once the stop's deadline passes.
15
+
16
+ ## Invariants
17
+ - Paths: an exact path comes first, then the longest `/*` above it.
18
+ - A handler reads the URL parser's path (dot segments resolved, percent-encoding kept as sent).
19
+ - A GET route also answers HEAD, with the same headers and no body.
20
+ - A path no route reaches gets `404`, and a path whose routes take other methods gets `405` naming them in `Allow`.
21
+ - A preflight to a cross-origin route is answered before the policies.
22
+ - A body the route does not take is refused only after the route's policies have seen the request.
23
+ - The request a policy or handler gets is frozen, and its maps cannot be written to.
24
+ - A policy's or handler's failure is logged by its kind only, never its message.
25
+ - Any other failure is answered `500` and logged as a warning, `http.unexpected_failure`, by the route reached (when known) and its kind only, never its message.
26
+ - A request target that is not a URL gets `400`.
27
+ - An answer whose headers cannot be written gets `500`, with none of its own headers.
28
+ - An answer's length is stated in bytes, so a HEAD answer carries the length its GET would.
29
+ - Stop refuses new connections and closes idle ones.
30
+ - Requests in flight finish (`Connection: close`) until stop's deadline cuts them.
31
+
32
+ ## Depends on
33
+ - `http` (its contract), `kernel`, `request-source`, `client-certificate`; `bodies`, `guards`, `cors`, `security-headers`, `ending`.
@@ -0,0 +1,17 @@
1
+ import { type Server } from "node:http";
2
+ import { type Clock, type Log } from "@o3co/auth-provider-foundation/kernel";
3
+ import { type TrustedProxies } from "@o3co/auth-provider-foundation/request-source";
4
+ import { type RequestEndObserver } from "../contract.js";
5
+ import type { GuardedRoute } from "../guards/guards.js";
6
+ /** What the server answers with. */
7
+ export interface Serving {
8
+ readonly routes: readonly GuardedRoute[];
9
+ readonly proxies: TrustedProxies;
10
+ readonly origins: ReadonlySet<string>;
11
+ readonly ends: readonly RequestEndObserver[];
12
+ readonly log: Log;
13
+ readonly clock: Clock;
14
+ }
15
+ export declare function serve(serving: Serving): Server;
16
+ /** Accepts nothing new and lets requests in flight finish; once `signal` aborts, closes the rest. */
17
+ export declare function drain(server: Server, signal: AbortSignal): Promise<void>;
@@ -0,0 +1,174 @@
1
+ import { createServer, validateHeaderName, validateHeaderValue, } from "node:http";
2
+ import { ignoredCertificates } from "@o3co/auth-provider-foundation/client-certificate";
3
+ import { isUnavailable } from "@o3co/auth-provider-foundation/kernel";
4
+ import { ignoredForwarding, sourceOf } from "@o3co/auth-provider-foundation/request-source";
5
+ import { bodyOf } from "../bodies/body.js";
6
+ import { paramsOf, readOnly } from "../bodies/values.js";
7
+ import { noStore, } from "../contract.js";
8
+ import { crossOrigin, preflight } from "../cors/cors.js";
9
+ import { ending } from "../ending/ending.js";
10
+ import { secured } from "../security-headers/security-headers.js";
11
+ /** The request's head; undefined when the request target is not a URL. */
12
+ function frozenHead(message, proxies) {
13
+ const url = URL.parse(message.url ?? "/", "http://placeholder");
14
+ if (url === null)
15
+ return undefined;
16
+ const headers = new Map();
17
+ for (const [key, value] of Object.entries(message.headers)) {
18
+ if (value !== undefined)
19
+ headers.set(key, Array.isArray(value) ? value.join(", ") : value);
20
+ }
21
+ return Object.freeze({
22
+ method: message.method ?? "GET",
23
+ path: url.pathname,
24
+ query: paramsOf(url.searchParams),
25
+ headers: readOnly(headers),
26
+ source: sourceOf(proxies, message.socket.remoteAddress ?? "", headers),
27
+ });
28
+ }
29
+ /** Writes the answer; one whose headers cannot be written becomes a bare 500, as does any failure. */
30
+ function write(res, response, log) {
31
+ try {
32
+ const { headers, payload } = serialized(response);
33
+ for (const [name, value] of Object.entries(headers)) {
34
+ validateHeaderName(name);
35
+ for (const v of [value].flat())
36
+ validateHeaderValue(name, v);
37
+ }
38
+ res.writeHead(response.status, headers);
39
+ res.end(payload);
40
+ }
41
+ catch (error) {
42
+ log.error("http.unwritable", { kind: error instanceof Error ? error.name : typeof error });
43
+ if (!res.headersSent)
44
+ res.writeHead(500, secured(noStore));
45
+ res.end();
46
+ }
47
+ }
48
+ /** The answer as written: its header fields, security headers, cookies and length included, and its payload. */
49
+ function serialized(response) {
50
+ const headers = secured(response.headers ?? {}, response.loads);
51
+ if (response.cookies?.length)
52
+ headers["set-cookie"] = [...response.cookies];
53
+ const body = response.body;
54
+ const isJson = typeof body === "object" && "json" in body;
55
+ if (isJson)
56
+ headers["content-type"] ??= "application/json";
57
+ const payload = isJson ? JSON.stringify(body.json) : body;
58
+ // Stated, not chunked, so a HEAD answer carries the length its GET would.
59
+ if (payload !== undefined)
60
+ headers["content-length"] = String(Buffer.byteLength(payload));
61
+ return { headers, payload };
62
+ }
63
+ const bare = (status) => ({ status, headers: noStore });
64
+ /** The routes a path reaches: those of exactly that path, or else of the longest `/*` above it. */
65
+ function reaching(routes, path) {
66
+ const exact = routes.filter((r) => r.path === path);
67
+ if (exact.length > 0)
68
+ return exact;
69
+ const under = routes.filter((r) => r.path.endsWith("/*") && path.startsWith(r.path.slice(0, -1)));
70
+ const longest = Math.max(0, ...under.map((r) => r.path.length));
71
+ return under.filter((r) => r.path.length === longest);
72
+ }
73
+ function routeFor(reached, method) {
74
+ const route = reached.find((r) => r.method === method);
75
+ return route ?? (method === "HEAD" ? reached.find((r) => r.method === "GET") : undefined);
76
+ }
77
+ async function answer(serving, head, message, nameRoute) {
78
+ const reaches = reaching(serving.routes, head.path);
79
+ const origin = head.headers.get("origin");
80
+ const asked = head.headers.get("access-control-request-method");
81
+ if (head.method === "OPTIONS" && asked !== undefined) {
82
+ const answered = preflight(reaches, asked, origin, serving.origins);
83
+ if (answered)
84
+ return answered;
85
+ }
86
+ const route = routeFor(reaches, head.method);
87
+ if (!route) {
88
+ if (reaches.length === 0)
89
+ return bare(404);
90
+ const allowed = reaches.flatMap((r) => (r.method === "GET" ? ["GET", "HEAD"] : [r.method]));
91
+ return { status: 405, headers: { ...noStore, allow: allowed.join(", ") } };
92
+ }
93
+ nameRoute(route.path);
94
+ const response = await handled(route, head, message, serving.log);
95
+ return route.cors ? crossOrigin(route.cors, origin, serving.origins, response) : response;
96
+ }
97
+ async function handled(route, head, message, log) {
98
+ const { refused, ...body } = await bodyOf(route.takes, message);
99
+ const request = Object.freeze({ ...head, ...body });
100
+ try {
101
+ const guarded = (await route.guard?.(request)) ?? "pass";
102
+ if (guarded !== "pass")
103
+ return guarded;
104
+ if (refused)
105
+ return bare(refused);
106
+ return await route.handle(request);
107
+ }
108
+ catch (error) {
109
+ if (isUnavailable(error)) {
110
+ log.warn("http.unavailable", { what: error.what });
111
+ return bare(503);
112
+ }
113
+ log.error("http.failed", { kind: error instanceof Error ? error.name : typeof error });
114
+ return bare(500);
115
+ }
116
+ }
117
+ /** Warns of the forwarded headers a request's peer sent and were ignored, in http's words. */
118
+ function ignoredHeaders({ proxies, log, clock }) {
119
+ const forwarding = ignoredForwarding(proxies, clock);
120
+ const certificates = ignoredCertificates(proxies.certificate, clock);
121
+ return (head, peer) => {
122
+ const fields = { peer: head.source.address };
123
+ if (forwarding(peer, head.headers))
124
+ log.warn("http.forwarded_from_untrusted_peer", fields);
125
+ if (certificates(head.headers, head.source.clientCertificate)) {
126
+ log.warn("http.client_certificate_ignored", fields);
127
+ }
128
+ };
129
+ }
130
+ export function serve(serving) {
131
+ const { proxies, log, clock } = serving;
132
+ const warn = ignoredHeaders(serving);
133
+ const server = createServer((req, res) => {
134
+ // Outside the chain, so it must never throw: it reads the clock and adds listeners; observers
135
+ // are told later. Every other step runs inside, so a throw there is answered.
136
+ const told = ending(serving.ends, clock, req, res);
137
+ let route;
138
+ const nameRoute = (path) => {
139
+ route = path;
140
+ told(path);
141
+ };
142
+ const reply = (response) => {
143
+ const stopping = !server.listening;
144
+ if (stopping)
145
+ res.setHeader("connection", "close");
146
+ write(res, response, log);
147
+ };
148
+ void Promise.resolve()
149
+ .then(() => {
150
+ const head = frozenHead(req, proxies);
151
+ if (head === undefined)
152
+ return bare(400);
153
+ warn(head, req.socket.remoteAddress ?? "");
154
+ return answer(serving, head, req, nameRoute);
155
+ })
156
+ .then(reply)
157
+ .catch((error) => {
158
+ const kind = error instanceof Error ? error.name : typeof error;
159
+ log.warn("http.unexpected_failure", { ...(route && { route }), kind });
160
+ reply(bare(500));
161
+ });
162
+ });
163
+ return server;
164
+ }
165
+ /** Accepts nothing new and lets requests in flight finish; once `signal` aborts, closes the rest. */
166
+ export function drain(server, signal) {
167
+ const closed = new Promise((resolve) => server.close(() => resolve()));
168
+ const cut = () => server.closeAllConnections();
169
+ if (signal.aborted)
170
+ cut();
171
+ else
172
+ signal.addEventListener("abort", cut, { once: true });
173
+ return closed;
174
+ }
@@ -0,0 +1,16 @@
1
+ # settings
2
+
3
+ Internal module of `http`: the schema of `http`'s operator section.
4
+
5
+ ## Responsibilities
6
+ - Decides which settings boot, and says what each means in its description (the settings list).
7
+
8
+ ## Not here
9
+ - The trusted proxies and hops (`request-source`'s `sourceSettings`) and where the client's certificate is forwarded (`client-certificate`'s `certificateSettings`): the section takes their parts under its own keys.
10
+ - What the settings drive: listening, a request's source, cookies, cross-origin answers.
11
+
12
+ ## Offers
13
+ - The section's schema, each key described.
14
+
15
+ ## Depends on
16
+ - `zod`, `request-source`, `client-certificate`.
@@ -0,0 +1,21 @@
1
+ import { z } from "zod";
2
+ export declare const section: z.ZodObject<{
3
+ host: z.ZodDefault<z.ZodString>;
4
+ port: z.ZodDefault<z.ZodNumber>;
5
+ "trusted-proxies": z.ZodDefault<z.ZodArray<z.ZodString>>;
6
+ "public-origin": z.ZodOptional<z.ZodURL>;
7
+ "cors-origins": z.ZodDefault<z.ZodArray<z.ZodURL>>;
8
+ "insecure-origin": z.ZodDefault<z.ZodBoolean>;
9
+ "proxy-hops": z.ZodDefault<z.ZodNumber>;
10
+ "client-certificate": z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
11
+ dialect: z.ZodLiteral<"rfc9440">;
12
+ header: z.ZodDefault<z.ZodString>;
13
+ "chain-header": z.ZodDefault<z.ZodString>;
14
+ }, z.core.$strict>, z.ZodObject<{
15
+ dialect: z.ZodLiteral<"pem">;
16
+ header: z.ZodString;
17
+ }, z.core.$strict>, z.ZodObject<{
18
+ dialect: z.ZodLiteral<"xfcc">;
19
+ header: z.ZodDefault<z.ZodString>;
20
+ }, z.core.$strict>], "dialect">>;
21
+ }, z.core.$strict>;
@@ -0,0 +1,37 @@
1
+ import { certificateSettings } from "@o3co/auth-provider-foundation/client-certificate";
2
+ import { sourceSettings } from "@o3co/auth-provider-foundation/request-source";
3
+ import { z } from "zod";
4
+ export const section = z
5
+ .strictObject({
6
+ host: z.string().default("127.0.0.1").describe("The address it listens on."),
7
+ port: z
8
+ .number()
9
+ .int()
10
+ .min(0)
11
+ .max(65535)
12
+ .default(8080)
13
+ .describe("The port it listens on; 0 takes a free one."),
14
+ "trusted-proxies": sourceSettings.shape["trusted-proxies"],
15
+ "public-origin": z
16
+ .url({ protocol: /^https?$/ })
17
+ .refine((s) => new URL(s).origin === s, "origin only (no path)")
18
+ .optional()
19
+ .describe("The scheme, host and port users reach this deployment at: an origin only, with no path."),
20
+ "cors-origins": z
21
+ .array(z
22
+ .url({ protocol: /^https$/ })
23
+ .refine((s) => !s.includes("*") && new URL(s).origin === s, "an https origin"))
24
+ .default([])
25
+ .describe("The https origins whose pages may read cross-origin routes: exact, without a pattern."),
26
+ "insecure-origin": z
27
+ .boolean()
28
+ .default(false)
29
+ .describe("Accepts a plain-http `public-origin`, knowing its cookies cannot be made secure: an http `public-origin` boots only with it."),
30
+ "proxy-hops": sourceSettings.shape["proxy-hops"],
31
+ /** Where a trusted proxy forwards the client's certificate; none is read without it. */
32
+ "client-certificate": certificateSettings.optional(),
33
+ })
34
+ .refine((s) => !s["public-origin"]?.startsWith("http:") || s["insecure-origin"], {
35
+ message: "an http public origin needs insecure-origin set explicitly",
36
+ path: ["public-origin"],
37
+ });
@@ -0,0 +1,37 @@
1
+ import type { Source } from "@o3co/auth-provider-foundation/request-source";
2
+ import type { z } from "zod";
3
+ import type { HttpRequest } from "./contract.js";
4
+ import type { section } from "./settings/section.js";
5
+ export type HttpSettings = z.input<typeof section>;
6
+ /** The http part of a test's configuration: any free port, unless the test sets one. */
7
+ export declare function httpConfig(settings?: Partial<HttpSettings>): {
8
+ http: {
9
+ host?: string | undefined;
10
+ port: number | undefined;
11
+ "trusted-proxies"?: string[] | undefined;
12
+ "public-origin"?: string | undefined;
13
+ "cors-origins"?: string[] | undefined;
14
+ "insecure-origin"?: boolean | undefined;
15
+ "proxy-hops"?: number | undefined;
16
+ "client-certificate"?: {
17
+ dialect: "rfc9440";
18
+ header?: string | undefined;
19
+ "chain-header"?: string | undefined;
20
+ } | {
21
+ dialect: "pem";
22
+ header: string;
23
+ } | {
24
+ dialect: "xfcc";
25
+ header?: string | undefined;
26
+ } | undefined;
27
+ };
28
+ };
29
+ type Plain = ReadonlyMap<string, readonly string[]>;
30
+ /** A test's request: an HTTPS GET of `/` from 127.0.0.1, empty unless said; read-only as the server gives it. */
31
+ export declare function testRequest(parts?: Partial<Omit<HttpRequest, "source" | "query" | "form" | "fields">> & {
32
+ readonly query?: Plain;
33
+ readonly form?: Plain;
34
+ readonly fields?: Plain;
35
+ readonly source?: Partial<Source>;
36
+ }): HttpRequest;
37
+ export {};
@@ -0,0 +1,24 @@
1
+ import { deepFreeze } from "@o3co/auth-provider-foundation/kernel";
2
+ import { fieldsOf, readOnly, valuesOf } from "./bodies/values.js";
3
+ /** The http part of a test's configuration: any free port, unless the test sets one. */
4
+ export function httpConfig(settings = {}) {
5
+ return { http: { port: 0, ...settings } };
6
+ }
7
+ /** A test's request: an HTTPS GET of `/` from 127.0.0.1, empty unless said; read-only as the server gives it. */
8
+ export function testRequest(parts = {}) {
9
+ const { query, form: sentForm, fields, json: sentJson, headers, source, ...rest } = parts;
10
+ const form = valuesOf(sentForm ?? new Map());
11
+ const json = sentJson && deepFreeze(structuredClone(sentJson));
12
+ const read = json ? { ...fieldsOf(json), json } : { fields: form };
13
+ return Object.freeze({
14
+ method: "GET",
15
+ path: "/",
16
+ ...read,
17
+ ...rest,
18
+ headers: readOnly(new Map(headers)),
19
+ query: valuesOf(query ?? new Map()),
20
+ form,
21
+ fields: fields ? valuesOf(fields) : read.fields,
22
+ source: Object.freeze({ address: "127.0.0.1", scheme: "https", determined: true, ...source }),
23
+ });
24
+ }
@@ -0,0 +1,22 @@
1
+ # http-ops
2
+
3
+ ## Responsibilities
4
+ - Answers the liveness and readiness routes, and times each request for `metrics`.
5
+
6
+ ## Not here
7
+ - What each readiness probe checks (the module holding the connection); running them under a deadline (`kernel`).
8
+ - Serving the routes, and telling each request's end (`http`); publishing the counts (`metrics-prometheus`).
9
+
10
+ ## Offers
11
+ - `GET /_healthcheck`: `200` `{"status":"ok"}`, checking no dependency; like `/readyz`, it uses no policy, takes no body (a body or a type is `415`, as anywhere) and answers `no-store`.
12
+ - `GET /readyz`: `200` `{"status":"ready"}` or `503` `{"status":"unready"}` as the kernel's readiness answers within `readiness-deadline-ms`, never naming what failed; requests share one run, and an unready answer is reused for 1 s from its start. The gauge `auth_dependency_up` gives each probe 1 or 0 from that run, by the name its module chose (no series without probes).
13
+ - The histogram `http_request_duration_seconds` counts each request once, answered or closed, by method (`other` outside the standard nine), the declared path of the route reached (`unmatched` when none) and the status written (`499` when the client closed first).
14
+ - Section `http-ops`: `readiness-deadline-ms`, its key in the settings list.
15
+ - Composition definition: comes by default with `http`.
16
+
17
+ ## Invariants
18
+ - `/readyz` is ready only when every probe passed within the deadline; one failing, late or unanswered (the store's among them) makes it unready.
19
+ - No label holds a value a request brought (its path, query, headers or body) or a user's: methods outside the nine are `other`, routes are declared paths, statuses are numbers, and dependencies are the names their modules chose.
20
+
21
+ ## Depends on
22
+ - `kernel`, `http`, and `metrics` by its contract only (never needed).
@@ -0,0 +1,3 @@
1
+ import { type Route } from "@o3co/auth-provider-foundation/http";
2
+ /** `GET /_healthcheck`: 200 regardless of dependencies, checking none; like any route, 415 to a body. */
3
+ export declare function liveness(): Route;
@@ -0,0 +1,10 @@
1
+ import { noStore } from "@o3co/auth-provider-foundation/http";
2
+ /** `GET /_healthcheck`: 200 regardless of dependencies, checking none; like any route, 415 to a body. */
3
+ export function liveness() {
4
+ return {
5
+ method: "GET",
6
+ path: "/_healthcheck",
7
+ takes: ["none"],
8
+ handle: () => ({ status: 200, headers: { ...noStore }, body: { json: { status: "ok" } } }),
9
+ };
10
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: import("@o3co/auth-provider-foundation/kernel").Module;
2
+ export default _default;
@@ -0,0 +1,24 @@
1
+ import { requestEnds, routes } from "@o3co/auth-provider-foundation/http";
2
+ import { contract, defineModule, probes } from "@o3co/auth-provider-foundation/kernel";
3
+ import { instruments } from "@o3co/auth-provider-foundation/metrics";
4
+ import { liveness } from "./liveness.js";
5
+ import { dependencyUp, readyz, sharedReadiness } from "./readiness.js";
6
+ import { section } from "./section.js";
7
+ import { requestDuration, timing } from "./timing.js";
8
+ /** The request-duration histogram, labelled by the declared paths of every route. */
9
+ const duration = contract("http-ops.request-duration");
10
+ /** The readiness run `/readyz` and the dependency gauge share, one per assembly. */
11
+ const readiness = contract("http-ops.shared-readiness");
12
+ export default defineModule({ name: "http-ops", section }, (m) => [
13
+ m
14
+ .provide(readiness)
15
+ .by((c) => sharedReadiness((ms) => c.readinessReport(ms), c.section["readiness-deadline-ms"], c.clock)),
16
+ m.contribute(routes).by(async (c) => readyz(await c.get(readiness))),
17
+ m.contribute(instruments).by(async (c) => dependencyUp((await c.collect(probes)).map((probe) => probe.name), await c.get(readiness))),
18
+ m.contribute(routes).by(() => liveness()),
19
+ m
20
+ .provide(duration)
21
+ .by(async (c) => requestDuration((await c.collect(routes)).map((route) => route.path))),
22
+ m.contribute(instruments).by((c) => c.get(duration)),
23
+ m.contribute(requestEnds).by(async (c) => timing(await c.get(duration))),
24
+ ]);
@@ -0,0 +1,10 @@
1
+ import { type Route } from "@o3co/auth-provider-foundation/http";
2
+ import type { Clock, ReadinessReport } from "@o3co/auth-provider-foundation/kernel";
3
+ import { type Gauge } from "@o3co/auth-provider-foundation/metrics";
4
+ export type SharedReadiness = () => Promise<ReadinessReport>;
5
+ /** One readiness run shared by `/readyz` and the dependency gauge: joined in flight, unready reused. */
6
+ export declare function sharedReadiness(report: (deadlineMs: number) => Promise<ReadinessReport>, deadlineMs: number, clock: Clock): SharedReadiness;
7
+ /** `GET /readyz`: the shared run's answer, never which probe failed. */
8
+ export declare function readyz(shared: SharedReadiness): Route;
9
+ /** `auth_dependency_up`, as legacy names it: 1 or 0 per probe, from the shared run at each sample. */
10
+ export declare function dependencyUp(names: readonly string[], shared: SharedReadiness): Gauge;
@@ -0,0 +1,54 @@
1
+ import { noStore } from "@o3co/auth-provider-foundation/http";
2
+ import { gauge } from "@o3co/auth-provider-foundation/metrics";
3
+ /** How long an unready report serves later callers, counted from when its run started. */
4
+ const reuseMs = 1000;
5
+ /** One readiness run shared by `/readyz` and the dependency gauge: joined in flight, unready reused. */
6
+ export function sharedReadiness(report, deadlineMs, clock) {
7
+ let last;
8
+ return () => {
9
+ const now = clock.now();
10
+ const fresh = (started) => now >= started && now - started < reuseMs;
11
+ // Ready is never reused, so a failure or a stop shows at once; a clock set back runs anew.
12
+ if (last && (!last.settled || (last.settled === "unready" && fresh(last.started)))) {
13
+ return last.report;
14
+ }
15
+ const run = { started: now, report: report(deadlineMs) };
16
+ run.report.then(({ answer }) => {
17
+ run.settled = answer;
18
+ }, () => {
19
+ run.settled = "unready";
20
+ });
21
+ last = run;
22
+ return run.report;
23
+ };
24
+ }
25
+ /** `GET /readyz`: the shared run's answer, never which probe failed. */
26
+ export function readyz(shared) {
27
+ return {
28
+ method: "GET",
29
+ path: "/readyz",
30
+ takes: ["none"],
31
+ handle: async () => {
32
+ const { answer } = await shared();
33
+ return {
34
+ status: answer === "ready" ? 200 : 503,
35
+ headers: noStore,
36
+ body: { json: { status: answer } },
37
+ };
38
+ },
39
+ };
40
+ }
41
+ /** `auth_dependency_up`, as legacy names it: 1 or 0 per probe, from the shared run at each sample. */
42
+ export function dependencyUp(names, shared) {
43
+ const spec = {
44
+ name: "auth_dependency_up",
45
+ help: "1 when a backing dependency answered its readiness probe, 0 otherwise.",
46
+ };
47
+ // A label needs one value at least; with no probes the gauge has no label and no series.
48
+ if (names.length === 0)
49
+ return gauge({ ...spec, labels: {} }, () => []);
50
+ return gauge({ ...spec, labels: { dependency: names } }, async () => (await shared()).probes.map((probe) => ({
51
+ labels: { dependency: probe.name },
52
+ value: probe.up ? 1 : 0,
53
+ })));
54
+ }
@@ -0,0 +1,4 @@
1
+ import { z } from "zod";
2
+ export declare const section: z.ZodObject<{
3
+ "readiness-deadline-ms": z.ZodDefault<z.ZodNumber>;
4
+ }, z.core.$strict>;
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+ export const section = z.strictObject({
3
+ "readiness-deadline-ms": z
4
+ .number()
5
+ .int()
6
+ .min(1)
7
+ .max(2 ** 31 - 1)
8
+ .default(1000)
9
+ .describe("How long `GET /readyz` waits for the probes before it answers unready, in whole milliseconds; set it below the orchestrator's own timeout for the check."),
10
+ });
@@ -0,0 +1,9 @@
1
+ import type { RequestEndObserver } from "@o3co/auth-provider-foundation/http";
2
+ import { type Histogram } from "@o3co/auth-provider-foundation/metrics";
3
+ type Label = "method" | "route" | "status";
4
+ export type Duration = Histogram<Label>;
5
+ /** Labels come from http's own decisions: the routes' declared paths, never a request's path. */
6
+ export declare function requestDuration(paths: readonly string[]): Duration;
7
+ /** Counts each request's end once by method, the declared route reached and the status written. */
8
+ export declare function timing(duration: Duration): RequestEndObserver;
9
+ export {};
@@ -0,0 +1,30 @@
1
+ import { histogram } from "@o3co/auth-provider-foundation/metrics";
2
+ const METHODS = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "TRACE", "CONNECT"];
3
+ // 100-599. Node writes up to 999; metrics drops a status above 599, warning once.
4
+ const STATUSES = Array.from({ length: 500 }, (_, i) => String(100 + i));
5
+ // No route's handler ran: 400, 404, 405 and preflight answers.
6
+ const UNMATCHED = "unmatched";
7
+ /** Labels come from http's own decisions: the routes' declared paths, never a request's path. */
8
+ export function requestDuration(paths) {
9
+ return histogram({
10
+ name: "http_request_duration_seconds",
11
+ help: "HTTP request latency in seconds, by method, route and status.",
12
+ labels: {
13
+ method: [...METHODS, "other"],
14
+ route: [...new Set([...paths, UNMATCHED])],
15
+ status: STATUSES,
16
+ },
17
+ buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
18
+ });
19
+ }
20
+ /** Counts each request's end once by method, the declared route reached and the status written. */
21
+ export function timing(duration) {
22
+ return {
23
+ ended: ({ method, route, status, ms }) => {
24
+ // 499: the client closed before any answer was written.
25
+ const written = status === "closed" ? "499" : String(status);
26
+ const known = METHODS.find((m) => m === method) ?? "other";
27
+ duration.observe({ method: known, route: route ?? UNMATCHED, status: written }, ms / 1000);
28
+ },
29
+ };
30
+ }