@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,15 @@
1
+ import type { HttpRequest } from "@o3co/auth-provider-foundation/http";
2
+ /** This deployment's cookies, named and set by one rule. */
3
+ export interface Cookies {
4
+ /** The cookie the request carries once under this name; none when sent twice, or over plain http where cookies are secure. */
5
+ read(request: HttpRequest, name: string): string | undefined;
6
+ /**
7
+ * A `Set-Cookie` value: path `/`, HttpOnly, SameSite=Lax; zero seconds removes it. A cross-site one
8
+ * is SameSite=None and only ever `__Host-` and Secure: it throws where cookies cannot be secure. A name that is not a token, or a value with anything but cookie-octets, throws.
9
+ */
10
+ write(name: string, value: string, maxAgeSeconds: number, options?: {
11
+ readonly crossSite?: true;
12
+ }): string;
13
+ }
14
+ export declare const cookies: import("@o3co/auth-provider-foundation/kernel").Contract<Cookies>;
15
+ export { cookiesFor } from "./cookies.js";
@@ -0,0 +1,3 @@
1
+ import { contract } from "@o3co/auth-provider-foundation/kernel";
2
+ export const cookies = contract("cookies");
3
+ export { cookiesFor } from "./cookies.js";
@@ -0,0 +1,3 @@
1
+ import type { Cookies } from "./contract.js";
2
+ /** Cookies for this deployment: `__Host-` and Secure where they can be secure, plain names otherwise. */
3
+ export declare function cookiesFor(secure: boolean): Cookies;
@@ -0,0 +1,39 @@
1
+ /** A cookie's name: an HTTP token (RFC 9110 §5.6.2). */
2
+ const tokenName = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
3
+ /** A cookie's value: cookie-octets only (RFC 6265 §4.1.1), so it cannot end the cookie or add an attribute. */
4
+ const cookieValue = /^[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]*$/;
5
+ /** Whether a request came over HTTPS, by the scheme its source holds. */
6
+ const overHttps = (source) => source.scheme === "https";
7
+ /** Cookies for this deployment: `__Host-` and Secure where they can be secure, plain names otherwise. */
8
+ export function cookiesFor(secure) {
9
+ const nameOf = (base) => (secure ? `__Host-${base}` : base);
10
+ return Object.freeze({
11
+ read: (request, base) => {
12
+ if (secure && !overHttps(request.source))
13
+ return undefined;
14
+ const name = nameOf(base);
15
+ const found = (request.headers.get("cookie") ?? "")
16
+ .split(";")
17
+ .map((part) => part.trim())
18
+ .filter((part) => part.startsWith(`${name}=`));
19
+ return found.length === 1 ? found[0]?.slice(name.length + 1) : undefined;
20
+ },
21
+ write: (base, value, maxAgeSeconds, options = {}) => {
22
+ if (options.crossSite && !secure)
23
+ throw new Error("a cross-site cookie needs secure cookies");
24
+ if (!tokenName.test(base))
25
+ throw new Error("a cookie name must be a token");
26
+ if (!cookieValue.test(value))
27
+ throw new Error("a cookie value must be cookie-octets");
28
+ const sameSite = options.crossSite ? "None" : "Lax";
29
+ return [
30
+ `${nameOf(base)}=${value}`,
31
+ "Path=/",
32
+ "HttpOnly",
33
+ `SameSite=${sameSite}`,
34
+ ...(secure ? ["Secure"] : []),
35
+ `Max-Age=${maxAgeSeconds}`,
36
+ ].join("; ");
37
+ },
38
+ });
39
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: import("@o3co/auth-provider-foundation/kernel").Module;
2
+ export default _default;
@@ -0,0 +1,7 @@
1
+ import { site } from "@o3co/auth-provider-foundation/http";
2
+ import { defineModule } from "@o3co/auth-provider-foundation/kernel";
3
+ import { cookies } from "./contract.js";
4
+ import { cookiesFor } from "./cookies.js";
5
+ export default defineModule({ name: "cookies" }, (m) => [
6
+ m.provide(cookies).by(async (c) => cookiesFor((await c.get(site)).secureCookies)),
7
+ ]);
@@ -0,0 +1,38 @@
1
+ # external-jwt
2
+
3
+ ## Responsibilities
4
+ - Checks a JWT signed by a key from outside (header, key, signature, times, issuer, audience, subject), and keeps each key set fetched from a URL once in the process; knows no feature.
5
+
6
+ ## Not here
7
+ - What to expect (issuer, audiences, subject, algorithms, clock allowance, longest lifetime): the caller's, from its own section.
8
+ - Single use of a `jti`, and the meaning of any other claim: the caller's.
9
+ - Where requests may go, and the transport: `outbound`.
10
+
11
+ ## Offers
12
+ - The `externalJwt` contract: a verifier from a key source and the caller's policy, and the asymmetric algorithms it knows.
13
+ - Key sources: JWKs written in; PEM keys, each for the algorithm it is given for; a key set at a URL, fetched through `outbound` from the caller's hosts.
14
+ - Verifying answers valid (the header and claims, frozen), invalid with a reason, or unavailable when no key set can be had.
15
+ - Reasons: `malformed`, `algorithm`, `no-key`, `ambiguous-key`, `signature`, `expired`, `not-yet`, `future-iat`, `too-long`, `issuer`, `audience`, `subject`. A caller maps them to its own words.
16
+ - Composition definition, with the operator section `key-set-max-age-ms` (3600000–2592000000, default 86400000), how long after its fetch a key set is used while no new one can be had.
17
+ - A test signer (`./testing`): a key pair as a JWK and PEM, signing claims or exact bytes with any header, as a sender might.
18
+
19
+ ## Invariants
20
+ - A token not written in its one form (`keys`' rule for a JWS) is malformed, so one token has one string.
21
+ - Only asymmetric algorithms the caller allows are taken; none and shared secrets never are.
22
+ - A header with `b64` or `crit`, a `kid` that is no string, or a `typ` other than the caller's is malformed. `typ` compares without case and with or without `application/`.
23
+ - Keys are read and fit to the algorithm by `keys`' rules for outside keys, and must agree with the header's `kid`; exactly one must, else no key or an ambiguous key.
24
+ - The claims are one JSON object in UTF-8 with a numeric `exp`; `nbf` and `iat`, where given, are numeric.
25
+ - On `kernel`'s clock, with the caller's allowance on each side, a token is expired at `exp`, not yet valid before `nbf`, and refused for an `iat` in the future.
26
+ - With a longest lifetime, neither `exp` ahead nor `iat` behind may pass it.
27
+ - An expected audience is met by any one of the token's audiences; a list of audiences holds only strings.
28
+ - A key set is fetched when first needed and used an hour from when its fetch began; before then, only for a key it lacks, never for one it holds twice.
29
+ - One fetch runs at a time, shared by every check waiting for it; fetches are at least a minute apart, whether they worked or not.
30
+ - Only a `200` answer holding a `keys` list is a set; entries that are no public key are skipped.
31
+ - While no set can be had, the last one is used until `key-set-max-age-ms` after its fetch; past that, or with none, the answer is unavailable.
32
+ - A refused destination, an unreachable host, or an answer that is no JSON is an outage; any other error is thrown.
33
+
34
+ ## Data
35
+ - Key sets, one per URL and set of hosts, held in the process; nothing is stored.
36
+
37
+ ## Depends on
38
+ - `kernel`, `keys`, `outbound`
@@ -0,0 +1,58 @@
1
+ export type Jwk = Readonly<Record<string, unknown>>;
2
+ /** A JWS algorithm's name; only the asymmetric ones `algorithms` lists ever verify. */
3
+ export type Algorithm = string;
4
+ /** A key set at a URL, fetched only from the hosts named. */
5
+ export interface JwksSource {
6
+ readonly url: string;
7
+ readonly hosts: readonly string[];
8
+ }
9
+ /** A public key written as PEM, taken only for the algorithm it is given for. */
10
+ export interface PemKey {
11
+ readonly alg: Algorithm;
12
+ readonly kid?: string;
13
+ readonly pem: string;
14
+ }
15
+ /** Where the keys come from: written in, as PEM, or a key set at a URL. */
16
+ export type KeySource = {
17
+ readonly inline: readonly Jwk[];
18
+ } | {
19
+ readonly pem: readonly PemKey[];
20
+ } | {
21
+ readonly jwks: JwksSource;
22
+ };
23
+ /** What the caller decides: each value is the caller's own, given from its section. */
24
+ export interface Policy {
25
+ readonly algorithms: readonly Algorithm[];
26
+ /** How far the issuer's clock may be from `kernel`'s. */
27
+ readonly skewMs: number;
28
+ readonly maxLifetimeMs?: number;
29
+ /** The header's `typ`, compared as RFC 7515 says. */
30
+ readonly typ?: string;
31
+ readonly expect?: {
32
+ readonly iss?: string;
33
+ /** Any one of these among the token's audiences. */
34
+ readonly aud?: readonly string[];
35
+ readonly sub?: string;
36
+ };
37
+ }
38
+ export type Reason = "malformed" | "algorithm" | "no-key" | "ambiguous-key" | "signature" | "expired" | "not-yet" | "future-iat" | "too-long" | "issuer" | "audience" | "subject";
39
+ export type Verified = {
40
+ readonly kind: "valid";
41
+ readonly header: Readonly<Record<string, unknown>>;
42
+ readonly claims: Readonly<Record<string, unknown>>;
43
+ } | {
44
+ readonly kind: "invalid";
45
+ readonly reason: Reason;
46
+ } | {
47
+ readonly kind: "unavailable";
48
+ };
49
+ export interface Verifier {
50
+ verify(jwt: string): Promise<Verified>;
51
+ }
52
+ /** Makes verifiers; one per process, keeping each key set once, by its URL and hosts. */
53
+ export interface ExternalJwt {
54
+ verifierOf(keys: KeySource, policy: Policy): Verifier;
55
+ /** The asymmetric algorithms it knows; a policy may name any of them. */
56
+ readonly algorithms: readonly Algorithm[];
57
+ }
58
+ export declare const externalJwt: import("@o3co/auth-provider-foundation/kernel").Contract<ExternalJwt>;
@@ -0,0 +1,2 @@
1
+ import { contract } from "@o3co/auth-provider-foundation/kernel";
2
+ export const externalJwt = contract("external-jwt");
@@ -0,0 +1,20 @@
1
+ import { type Clock } from "@o3co/auth-provider-foundation/kernel";
2
+ import { type Outbound } from "@o3co/auth-provider-foundation/outbound";
3
+ import type { JwksSource } from "./contract.js";
4
+ import { type Candidate } from "./keys.js";
5
+ /**
6
+ * The key set at a URL: fetched when first needed, kept an hour, fetched again at most once a minute,
7
+ * and past `maxAgeMs` after it was fetched never used.
8
+ */
9
+ export declare function jwksAt(source: JwksSource, deps: {
10
+ readonly clock: Clock;
11
+ readonly outbound: Outbound;
12
+ readonly maxAgeMs: number;
13
+ }): {
14
+ /** The set to check with: the kept one while it holds, else a fresh one where it can be had, else the last. */
15
+ keys: () => Promise<readonly Candidate[] | undefined>;
16
+ /** A fresh set for a key the kept one lacks, where a fetch is due. */
17
+ again: () => Promise<readonly Candidate[] | undefined>;
18
+ };
19
+ /** A key set at a URL, as one composition keeps it. */
20
+ export type KeySet = ReturnType<typeof jwksAt>;
@@ -0,0 +1,61 @@
1
+ import { isUnavailable } from "@o3co/auth-provider-foundation/kernel";
2
+ import { DestinationRefused, jsonObjectOf, } from "@o3co/auth-provider-foundation/outbound";
3
+ import { fromJwk } from "./keys.js";
4
+ /** A set is used this long after it was fetched. */
5
+ const holdMs = 3_600_000;
6
+ /** Fetches, whether they worked or not, are at least this far apart. */
7
+ const spacingMs = 60_000;
8
+ /**
9
+ * The key set at a URL: fetched when first needed, kept an hour, fetched again at most once a minute,
10
+ * and past `maxAgeMs` after it was fetched never used.
11
+ */
12
+ export function jwksAt(source, deps) {
13
+ const { clock } = deps;
14
+ let kept;
15
+ let tried;
16
+ let fetching;
17
+ const fetchAndKeep = async () => {
18
+ // The set's age counts from the start, however long the answer takes.
19
+ const began = clock.now();
20
+ tried = began;
21
+ let response;
22
+ try {
23
+ response = await deps.outbound.fetch(source.url, { headers: { accept: "application/json" } }, { hosts: source.hosts });
24
+ }
25
+ catch (error) {
26
+ if (isUnavailable(error) || error instanceof DestinationRefused)
27
+ return;
28
+ throw error;
29
+ }
30
+ const answer = jsonObjectOf(response);
31
+ const listed = answer.kind === "object" ? answer.value.keys : undefined;
32
+ if (!Array.isArray(listed))
33
+ return;
34
+ const keys = listed.flatMap((jwk) => jwk && typeof jwk === "object" && !Array.isArray(jwk) ? (fromJwk(jwk) ?? []) : []);
35
+ kept = { keys, fetchedAt: began };
36
+ };
37
+ /** One fetch at a time, shared by every check that waits for it, and none within a minute of the last. */
38
+ const refresh = async () => {
39
+ if (!fetching && (tried === undefined || clock.now() - tried >= spacingMs)) {
40
+ fetching = fetchAndKeep().finally(() => {
41
+ fetching = undefined;
42
+ });
43
+ }
44
+ await fetching;
45
+ };
46
+ /** The kept set while it is young enough to use, even when it cannot be renewed. */
47
+ const usable = () => kept && clock.now() - kept.fetchedAt < deps.maxAgeMs ? kept.keys : undefined;
48
+ return {
49
+ /** The set to check with: the kept one while it holds, else a fresh one where it can be had, else the last. */
50
+ keys: async () => {
51
+ if (!kept || clock.now() - kept.fetchedAt >= holdMs)
52
+ await refresh();
53
+ return usable();
54
+ },
55
+ /** A fresh set for a key the kept one lacks, where a fetch is due. */
56
+ again: async () => {
57
+ await refresh();
58
+ return usable();
59
+ },
60
+ };
61
+ }
@@ -0,0 +1,16 @@
1
+ import type { KeyObject } from "node:crypto";
2
+ import type { Algorithm, Jwk, PemKey } from "./contract.js";
3
+ /** A key that may verify: as a JWK, for choosing it, and as the platform reads it. */
4
+ export interface Candidate {
5
+ readonly jwk: Jwk;
6
+ readonly key: KeyObject;
7
+ }
8
+ /** The asymmetric algorithms `keys` knows. */
9
+ export declare const algorithms: readonly Algorithm[];
10
+ export declare const isAlgorithm: (alg: unknown) => alg is Algorithm;
11
+ /** A public key `keys` reads from a JWK; none for anything else. */
12
+ export declare const fromJwk: (jwk: Jwk) => Candidate | undefined;
13
+ /** A PEM public key, taken only for the algorithm it is given for; a private one never is. */
14
+ export declare function fromPem(given: PemKey): Candidate | undefined;
15
+ /** The one key that may verify under the header's algorithm and id. */
16
+ export declare function picked(candidates: readonly Candidate[], alg: Algorithm, kid: string | undefined): Candidate | "no-key" | "ambiguous-key";
@@ -0,0 +1,26 @@
1
+ import { publicJwk, publicPem, verifiesUnder, verifyAlgorithms } from "@o3co/auth-provider-foundation/keys";
2
+ /** The asymmetric algorithms `keys` knows. */
3
+ export const algorithms = verifyAlgorithms;
4
+ export const isAlgorithm = (alg) => typeof alg === "string" && algorithms.includes(alg);
5
+ /** A public key `keys` reads from a JWK; none for anything else. */
6
+ export const fromJwk = (jwk) => publicJwk(jwk);
7
+ /** A PEM public key, taken only for the algorithm it is given for; a private one never is. */
8
+ export function fromPem(given) {
9
+ const key = publicPem(given.pem);
10
+ if (!key)
11
+ return undefined;
12
+ const jwk = {
13
+ ...key.export({ format: "jwk" }),
14
+ alg: given.alg,
15
+ ...(given.kid ? { kid: given.kid } : {}),
16
+ };
17
+ return { jwk, key };
18
+ }
19
+ /** The one key that may verify under the header's algorithm and id. */
20
+ export function picked(candidates, alg, kid) {
21
+ const matching = candidates.filter(({ jwk }) => verifiesUnder(jwk, alg) && (kid === undefined || jwk.kid === kid));
22
+ const [only, ...more] = matching;
23
+ if (!only)
24
+ return "no-key";
25
+ return more.length > 0 ? "ambiguous-key" : only;
26
+ }
@@ -0,0 +1,6 @@
1
+ import { z } from "zod";
2
+ export declare const section: z.ZodObject<{
3
+ "key-set-max-age-ms": z.ZodDefault<z.ZodNumber>;
4
+ }, z.core.$strict>;
5
+ declare const _default: import("@o3co/auth-provider-foundation/kernel").Module;
6
+ export default _default;
@@ -0,0 +1,21 @@
1
+ import { defineModule } from "@o3co/auth-provider-foundation/kernel";
2
+ import { outbound } from "@o3co/auth-provider-foundation/outbound";
3
+ import { z } from "zod";
4
+ import { externalJwt } from "./contract.js";
5
+ import { externalJwtOver } from "./verifier.js";
6
+ export const section = z.strictObject({
7
+ "key-set-max-age-ms": z
8
+ .number()
9
+ .int()
10
+ .min(3_600_000)
11
+ .max(2_592_000_000)
12
+ .default(86_400_000)
13
+ .describe("How long after its fetch a key set is still used while no new one can be had, in milliseconds."),
14
+ });
15
+ export default defineModule({ name: "external-jwt", section }, (m) => [
16
+ m.provide(externalJwt).by(async (c) => externalJwtOver({
17
+ clock: c.clock,
18
+ outbound: await c.get(outbound),
19
+ keySetMaxAgeMs: c.section["key-set-max-age-ms"],
20
+ })),
21
+ ]);
@@ -0,0 +1,38 @@
1
+ /** What differs by algorithm: the key pair, the digest, and how an EC signature is written (RFC 7518 §3.4). */
2
+ declare const byAlg: {
3
+ ES256: {
4
+ pair: () => import("node:crypto").KeyPairExportResult<{
5
+ namedCurve: string;
6
+ }>;
7
+ digest: string;
8
+ encoding: {
9
+ dsaEncoding: "ieee-p1363";
10
+ };
11
+ };
12
+ RS256: {
13
+ pair: () => import("node:crypto").KeyPairExportResult<{
14
+ modulusLength: number;
15
+ }>;
16
+ digest: string;
17
+ encoding: {};
18
+ };
19
+ EdDSA: {
20
+ pair: () => import("node:crypto").KeyPairExportResult<{}>;
21
+ digest: null;
22
+ encoding: {};
23
+ };
24
+ };
25
+ type TestAlgorithm = keyof typeof byAlg;
26
+ /**
27
+ * A key pair for a test: its public key as a JWK (with `kid` where given) and as PEM, and signers
28
+ * that write any header as a sender might, unencoded payloads and critical extensions included.
29
+ */
30
+ export declare function testSigner(alg: TestAlgorithm, kid?: string): Promise<{
31
+ jwk: Record<string, unknown>;
32
+ pem: string;
33
+ /** Signs the claims; `header` adds to or, with undefined, removes from `{ alg, kid }`. */
34
+ sign: (claims: unknown, header?: Record<string, unknown>) => Promise<string>;
35
+ /** Signs a payload's exact bytes, which no JSON value can carry; `header` is as for `sign`. */
36
+ signBytes: (payload: Uint8Array, header?: Record<string, unknown>) => Promise<string>;
37
+ }>;
38
+ export {};
@@ -0,0 +1,43 @@
1
+ import { generateKeyPairSync, sign } from "node:crypto";
2
+ /** What differs by algorithm: the key pair, the digest, and how an EC signature is written (RFC 7518 §3.4). */
3
+ const byAlg = {
4
+ ES256: {
5
+ pair: () => generateKeyPairSync("ec", { namedCurve: "P-256" }),
6
+ digest: "sha256",
7
+ encoding: { dsaEncoding: "ieee-p1363" },
8
+ },
9
+ RS256: {
10
+ pair: () => generateKeyPairSync("rsa", { modulusLength: 2048 }),
11
+ digest: "sha256",
12
+ encoding: {},
13
+ },
14
+ EdDSA: { pair: () => generateKeyPairSync("ed25519"), digest: null, encoding: {} },
15
+ };
16
+ const encoded = (text) => Buffer.from(text).toString("base64url");
17
+ /**
18
+ * A key pair for a test: its public key as a JWK (with `kid` where given) and as PEM, and signers
19
+ * that write any header as a sender might, unencoded payloads and critical extensions included.
20
+ */
21
+ export async function testSigner(alg, kid) {
22
+ const { publicKey, privateKey } = byAlg[alg].pair();
23
+ const jwk = {
24
+ ...publicKey.export({ format: "jwk" }),
25
+ ...(kid ? { kid } : {}),
26
+ };
27
+ return {
28
+ jwk,
29
+ pem: publicKey.export({ format: "pem", type: "spki" }).toString(),
30
+ /** Signs the claims; `header` adds to or, with undefined, removes from `{ alg, kid }`. */
31
+ sign: async (claims, header = {}) => signed(Buffer.from(JSON.stringify(claims)), header),
32
+ /** Signs a payload's exact bytes, which no JSON value can carry; `header` is as for `sign`. */
33
+ signBytes: async (payload, header = {}) => signed(payload, header),
34
+ };
35
+ function signed(payload, header) {
36
+ const written = Object.fromEntries(Object.entries({ alg, ...(kid ? { kid } : {}), ...header }).filter(([, value]) => value !== undefined));
37
+ const body = Buffer.from(payload);
38
+ const input = `${encoded(JSON.stringify(written))}.${written.b64 === false ? body.toString() : body.toString("base64url")}`;
39
+ const { digest, encoding } = byAlg[alg];
40
+ const signature = sign(digest, Buffer.from(input), { key: privateKey, ...encoding });
41
+ return `${input}.${signature.toString("base64url")}`;
42
+ }
43
+ }
@@ -0,0 +1,10 @@
1
+ import { type Clock } from "@o3co/auth-provider-foundation/kernel";
2
+ import type { Outbound } from "@o3co/auth-provider-foundation/outbound";
3
+ import type { ExternalJwt } from "./contract.js";
4
+ /** Verifiers on `kernel`'s clock, fetching key sets through `outbound`, each set kept once by its URL and hosts. */
5
+ export declare function externalJwtOver(deps: {
6
+ readonly clock: Clock;
7
+ readonly outbound: Outbound;
8
+ /** How long after it was fetched a key set may still be used. */
9
+ readonly keySetMaxAgeMs: number;
10
+ }): ExternalJwt;
@@ -0,0 +1,147 @@
1
+ import { deepFreeze } from "@o3co/auth-provider-foundation/kernel";
2
+ import { isCanonicalJws } from "@o3co/auth-provider-foundation/keys";
3
+ import { compactVerify, decodeProtectedHeader } from "jose";
4
+ import { jwksAt } from "./jwks.js";
5
+ import { algorithms, fromJwk, fromPem, isAlgorithm, picked } from "./keys.js";
6
+ /** A `typ` as RFC 7515 compares it: without case and without `application/`. */
7
+ const mediaTypeOf = (typ) => typeof typ === "string" ? typ.toLowerCase().replace(/^application\//, "") : undefined;
8
+ /** Claims are UTF-8, as RFC 7519 says; bytes that are not refuse them. */
9
+ const utf8 = new TextDecoder("utf-8", { fatal: true });
10
+ const isTime = (value) => typeof value === "number" && Number.isFinite(value);
11
+ /** Verifiers on `kernel`'s clock, fetching key sets through `outbound`, each set kept once by its URL and hosts. */
12
+ export function externalJwtOver(deps) {
13
+ const sets = new Map();
14
+ const setAt = (url, hosts) => {
15
+ const key = JSON.stringify([url, [...hosts].sort()]);
16
+ const kept = sets.get(key);
17
+ if (kept)
18
+ return kept;
19
+ const made = jwksAt({ url, hosts }, { clock: deps.clock, outbound: deps.outbound, maxAgeMs: deps.keySetMaxAgeMs });
20
+ sets.set(key, made);
21
+ return made;
22
+ };
23
+ const chooserOf = (keys) => {
24
+ if ("jwks" in keys)
25
+ return fromSet(setAt(keys.jwks.url, keys.jwks.hosts));
26
+ const written = "inline" in keys
27
+ ? keys.inline.flatMap((jwk) => fromJwk(jwk) ?? [])
28
+ : keys.pem.flatMap((given) => fromPem(given) ?? []);
29
+ return async (alg, kid) => picked(written, alg, kid);
30
+ };
31
+ return {
32
+ algorithms,
33
+ verifierOf: (keys, policy) => verifierWith(chooserOf(keys), policy, deps.clock),
34
+ };
35
+ }
36
+ /** Picks from the kept set, and from a fresh one only when the kept set has no key that fits. */
37
+ function fromSet(set) {
38
+ return async (alg, kid) => {
39
+ const held = await set.keys();
40
+ const first = held ? picked(held, alg, kid) : undefined;
41
+ // An ambiguous pick is the set's own answer; fetching again would not settle it.
42
+ if (first !== undefined && first !== "no-key")
43
+ return first;
44
+ const again = await set.again();
45
+ return again ? picked(again, alg, kid) : "unavailable";
46
+ };
47
+ }
48
+ function headerOf(jwt) {
49
+ try {
50
+ return decodeProtectedHeader(jwt);
51
+ }
52
+ catch {
53
+ return undefined;
54
+ }
55
+ }
56
+ async function signedPayload(jwt, key) {
57
+ try {
58
+ return (await compactVerify(jwt, key.key)).payload;
59
+ }
60
+ catch {
61
+ return undefined;
62
+ }
63
+ }
64
+ /** One JSON object in UTF-8, or undefined. */
65
+ function claimsOf(payload) {
66
+ try {
67
+ const claims = JSON.parse(utf8.decode(payload));
68
+ return typeof claims === "object" && claims !== null ? claims : undefined;
69
+ }
70
+ catch {
71
+ return undefined;
72
+ }
73
+ }
74
+ /** What is wrong with the claims under the policy at `now`, if anything. */
75
+ function claimsFault(claims, policy, now) {
76
+ const { exp, nbf, iat } = claims;
77
+ if (!isTime(exp) || (nbf !== undefined && !isTime(nbf)) || (iat !== undefined && !isTime(iat))) {
78
+ return "malformed";
79
+ }
80
+ const [expMs, nbfMs, iatMs] = [
81
+ exp * 1000,
82
+ isTime(nbf) ? nbf * 1000 : undefined,
83
+ isTime(iat) ? iat * 1000 : undefined,
84
+ ];
85
+ const skew = policy.skewMs;
86
+ if (expMs <= now - skew)
87
+ return "expired";
88
+ if (nbfMs !== undefined && nbfMs > now + skew)
89
+ return "not-yet";
90
+ if (iatMs !== undefined && iatMs > now + skew)
91
+ return "future-iat";
92
+ if (policy.maxLifetimeMs !== undefined) {
93
+ const reach = policy.maxLifetimeMs + skew;
94
+ if (expMs - now > reach || (iatMs !== undefined && now - iatMs > reach))
95
+ return "too-long";
96
+ }
97
+ const expected = policy.expect;
98
+ if (expected?.iss !== undefined && claims.iss !== expected.iss)
99
+ return "issuer";
100
+ const wanted = expected?.aud;
101
+ if (wanted !== undefined) {
102
+ const audiences = typeof claims.aud === "string" ? [claims.aud] : claims.aud;
103
+ // A list of audiences holds only strings, as RFC 7519 says.
104
+ const allStrings = Array.isArray(audiences) && audiences.every((a) => typeof a === "string");
105
+ if (!allStrings || !audiences.some((a) => wanted.includes(a)))
106
+ return "audience";
107
+ }
108
+ if (expected?.sub !== undefined && claims.sub !== expected.sub)
109
+ return "subject";
110
+ return undefined;
111
+ }
112
+ function verifierWith(choose, policy, clock) {
113
+ const invalid = (reason) => ({ kind: "invalid", reason });
114
+ return {
115
+ verify: async (jwt) => {
116
+ const header = isCanonicalJws(jwt) ? headerOf(jwt) : undefined;
117
+ if (!header)
118
+ return invalid("malformed");
119
+ // A JWT is encoded, and no critical extension is taken.
120
+ if (header.b64 !== undefined || header.crit !== undefined)
121
+ return invalid("malformed");
122
+ if (policy.typ !== undefined && mediaTypeOf(header.typ) !== mediaTypeOf(policy.typ))
123
+ return invalid("malformed");
124
+ const kid = header.kid;
125
+ if (kid !== undefined && typeof kid !== "string")
126
+ return invalid("malformed");
127
+ const alg = header.alg;
128
+ if (!isAlgorithm(alg) || !policy.algorithms.includes(alg))
129
+ return invalid("algorithm");
130
+ const key = await choose(alg, kid);
131
+ if (key === "unavailable")
132
+ return { kind: "unavailable" };
133
+ if (typeof key === "string")
134
+ return invalid(key);
135
+ const payload = await signedPayload(jwt, key);
136
+ if (!payload)
137
+ return invalid("signature");
138
+ const claims = claimsOf(payload);
139
+ if (!claims)
140
+ return invalid("malformed");
141
+ const fault = claimsFault(claims, policy, clock.now());
142
+ if (fault)
143
+ return invalid(fault);
144
+ return { kind: "valid", header: deepFreeze({ ...header }), claims: deepFreeze(claims) };
145
+ },
146
+ };
147
+ }
package/http/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # http
2
+
3
+ ## Responsibilities
4
+ - Owns the HTTP entry point: it connects the routes that modules contribute and applies the policies each route declares.
5
+ - Reads what a request sends (query, form, JSON, headers) once at the boundary into read-only maps.
6
+ - Answers unavailable with `503`: retryable, with no body, kept out of caches (`noStore`: `no-store`, `Pragma: no-cache`).
7
+
8
+ ## Not here
9
+ - What a route means and its error vocabulary (the contributing module's); what a policy decides and answers (its owner's).
10
+ - Any name taken from request headers: the public origin comes only from configuration.
11
+
12
+ ## Offers
13
+ - `routes`: a method, a path and a handler, with the policies used (and their options), the bodies taken and cross-origin use when declared; a GET route also answers HEAD, with the same headers and no body.
14
+ - Paths: an exact path comes first, then the longest `/*` above it; a handler reads the URL parser's path (dot segments resolved, percent-encoding kept as sent), and a `/*` handler checks the segments under it by its own rules.
15
+ - Bodies: a route may take a form, JSON, none, or several, and any other body is `415`, refused only after the route's policies have seen the request.
16
+ - A form and a JSON body share one size limit of 64 KiB: a larger form is read as empty, and a larger JSON body is `413`.
17
+ - A JSON body is one object in well-formed UTF-8, frozen, naming no member twice in one object; anything else is `400`.
18
+ - Values with the same name keep the order sent: `single` gives a name's only value, `repeated` the names with more, and whether one must be single is the route's.
19
+ - A request's fields are its body's values by name; a JSON member that is not a string is told apart as `mistyped`, its meaning the route's.
20
+ - Cross-origin routes: a preflight is answered before the policies, and answers vary by `Origin`, naming the request's origin only when listed, never `*` and never with credentials.
21
+ - A preflight from a listed origin, for a method a declared route takes, names the methods of the path's declared routes, the request headers the route takes, and a max age of 600 seconds.
22
+ - Any other preflight on such a path gets `204` with nothing to read by.
23
+ - `loads`: what a page declares it loads (the origins it frames, its inline scripts); its answer's policy allows those and nothing else, and a frame a policy cannot name makes the answer `500`.
24
+ - Policies: `policy` makes one, unique as a value; `use` is a route's use of one with its options; `policies` takes one rule per policy, which passes the request on or answers it; a route's policies run in its order before its handler, and the first answer ends the request.
25
+ - The request type, with its source as `request-source` decides it from the direct peer, the headers and this section's trusted proxies.
26
+ - `listening` (the port) and `site` (the public origin; cookies can be made secure unless it is http); an answer's `cookies` are sent one `Set-Cookie` each; `client-certificate`'s `certificateForwarding` is provided from this section.
27
+ - `requestEnds`: each request's end, once, answered or closed: the method sent, the declared path of the route reached (none when none), the status written or `closed` when the client closed first, and the milliseconds taken (never negative); an observer's throw is dropped.
28
+ - Observers are told after the request, asynchronously, and what they throw or reject is dropped.
29
+ - `./testing`: test configuration and request builders with working defaults.
30
+
31
+ ## Invariants
32
+ - Boot is refused, naming the route and the policy, when two routes take one method and path, a route uses a policy no module applies, or a policy has two rules; policies match by value, and a label only names one in a report.
33
+ - Every answer it writes carries the security headers, set in one place: a Content-Security-Policy that loads nothing by default and lets no one frame the answer, HSTS, `nosniff`, `X-Frame-Options`, `Referrer-Policy: no-referrer` and the cross-origin ones; a route's own values of them, its policy included, are replaced; only `loads` widens a page's policy.
34
+ - Stop refuses new connections and closes idle ones; requests in flight finish (`Connection: close`) until its deadline cuts them.
35
+ - A request target that is not a URL gets `400`, an answer whose headers cannot be written gets `500`, no request stops the process, and error answers never show exception text or internal values.
36
+ - A path no route reaches gets `404`, and a path whose routes take other methods gets `405` naming them in `Allow`.
37
+
38
+ ## Depends on
39
+ - `kernel`, `request-source`, `client-certificate`.
@@ -0,0 +1,5 @@
1
+ http {
2
+ host = 127.0.0.1
3
+ port = 8080
4
+ public-origin = "https://login.example.invalid"
5
+ }