@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,31 @@
1
+ # bodies
2
+
3
+ Internal module of `http`: the values a request is sent with, by name, from its body as its route takes it and from its query alike.
4
+
5
+ ## Responsibilities
6
+ - Tells a body's kind from its type, and reads a form or a JSON object up to the size limit.
7
+ - Gives the received values by name as read-only maps.
8
+
9
+ ## Not here
10
+ - Which bodies a route takes (the route's declaration).
11
+ - What a value means, and whether it must be single (the route's).
12
+ - What a mistyped member means (the route's).
13
+
14
+ ## Offers
15
+ - A request's body as its route takes it: its form, its fields, the JSON object and its mistyped names, or the status that refuses it.
16
+ - Values by name, read-only, from a map, a query, a form, or a JSON object's fields: `single` gives a name's value when it holds exactly one, and `repeated` the names that hold more.
17
+
18
+ ## Invariants
19
+ - 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`.
20
+ - A JSON body is one object in well-formed UTF-8, frozen, naming no member twice in one object; anything else is `400`.
21
+ - A route may take a form, JSON, none, or several.
22
+ - A request's fields are its body's values by name.
23
+ - A JSON member that is not a string is no field, and its name is mistyped.
24
+ - A body the route does not declare is `415`, and gives no fields.
25
+ - A route that declares no bodies reads a form when one is sent, and ignores any other body.
26
+ - No type and nothing sent is no body.
27
+ - The values of a name keep the order sent.
28
+ - The maps, their value lists and the mistyped names cannot be written to, and a write to a map throws.
29
+
30
+ ## Depends on
31
+ - `http` (its contract: the body kinds), and `kernel` (freezing).
@@ -0,0 +1,6 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ import type { BodyKind, HttpRequest } from "../contract.js";
3
+ /** A request's body as a route that takes these kinds reads it; `refused` is the status refusing it. */
4
+ export declare function bodyOf(takes: readonly BodyKind[] | undefined, message: IncomingMessage): Promise<Pick<HttpRequest, "form" | "fields" | "json" | "mistyped"> & {
5
+ refused?: number;
6
+ }>;
@@ -0,0 +1,69 @@
1
+ import { deepFreeze } from "@o3co/auth-provider-foundation/kernel";
2
+ import { repeatsName } from "./json.js";
3
+ import { fieldsOf, paramsOf, valuesOf } from "./values.js";
4
+ const limit = 64 * 1024;
5
+ const typeOf = (message) => message.headers["content-type"]?.split(";")[0]?.trim().toLowerCase();
6
+ /** The body up to the limit; undefined past it. */
7
+ async function bytesOf(message) {
8
+ const chunks = [];
9
+ let size = 0;
10
+ for await (const chunk of message) {
11
+ size += chunk.length;
12
+ if (size > limit)
13
+ return undefined;
14
+ chunks.push(chunk);
15
+ }
16
+ return Buffer.concat(chunks);
17
+ }
18
+ /** Whether a request has no body: no type, and nothing sent. Reads, and so consumes, an untyped body. */
19
+ const isEmpty = async (message) => typeOf(message) === undefined && (await bytesOf(message))?.length === 0;
20
+ /** A form body up to the limit; anything else, or too large, leaves the form empty. */
21
+ async function formOf(message) {
22
+ if (typeOf(message) !== "application/x-www-form-urlencoded")
23
+ return valuesOf(new Map());
24
+ const bytes = await bytesOf(message);
25
+ return bytes ? paramsOf(new URLSearchParams(bytes.toString("utf8"))) : valuesOf(new Map());
26
+ }
27
+ const kinds = new Map([
28
+ ["application/x-www-form-urlencoded", "form"],
29
+ ["application/json", "json"],
30
+ ]);
31
+ /** Throws on bytes that are not UTF-8, instead of replacing them; a BOM is kept, so refused. */
32
+ const utf8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
33
+ /** A JSON object body, or the status that answers one that cannot be read. */
34
+ async function jsonOf(message) {
35
+ const bytes = await bytesOf(message);
36
+ if (!bytes)
37
+ return 413;
38
+ try {
39
+ const text = utf8.decode(bytes);
40
+ const value = JSON.parse(text);
41
+ const object = typeof value === "object" && value !== null && !Array.isArray(value);
42
+ // A name given twice is read as either value by one parser or another.
43
+ return object && !repeatsName(text) ? deepFreeze(value) : 400;
44
+ }
45
+ catch {
46
+ return 400;
47
+ }
48
+ }
49
+ /** A body read as a form, its fields the form's; `refused` when the request is answered instead. */
50
+ const asForm = (form, refused) => ({
51
+ form,
52
+ fields: form,
53
+ ...(refused === undefined ? {} : { refused }),
54
+ });
55
+ /** A request's body as a route that takes these kinds reads it; `refused` is the status refusing it. */
56
+ export async function bodyOf(takes, message) {
57
+ const kind = (await isEmpty(message)) ? "none" : kinds.get(typeOf(message));
58
+ // A route that declares no bodies reads a form when one is sent, and ignores any other.
59
+ if (takes === undefined)
60
+ return asForm(await formOf(message));
61
+ if (kind === undefined || !takes.includes(kind))
62
+ return asForm(valuesOf(new Map()), 415);
63
+ if (kind !== "json")
64
+ return asForm(await formOf(message));
65
+ const json = await jsonOf(message);
66
+ if (typeof json === "number")
67
+ return asForm(valuesOf(new Map()), json);
68
+ return { form: valuesOf(new Map()), ...fieldsOf(json), json };
69
+ }
@@ -0,0 +1,2 @@
1
+ /** Whether a JSON text, one that parses, names a member twice in one object, at any depth. */
2
+ export declare function repeatsName(text: string): boolean;
@@ -0,0 +1,35 @@
1
+ /** Where the string starting at `at` closes. */
2
+ function closing(text, at) {
3
+ let end = at + 1;
4
+ while (end < text.length && text[end] !== '"')
5
+ end += text[end] === "\\" ? 2 : 1;
6
+ return end;
7
+ }
8
+ /** Whether a JSON text, one that parses, names a member twice in one object, at any depth. */
9
+ export function repeatsName(text) {
10
+ // Per open object its names so far; none for an open array.
11
+ const open = [];
12
+ let atName = false;
13
+ for (let at = 0; at < text.length; at++) {
14
+ const c = text[at];
15
+ if (c === "{" || c === "[")
16
+ open.push(c === "{" ? new Set() : undefined);
17
+ if (c === "}" || c === "]")
18
+ open.pop();
19
+ if (c === "{" || c === ",")
20
+ atName = true;
21
+ if (c !== '"')
22
+ continue;
23
+ const end = closing(text, at);
24
+ const names = open.at(-1);
25
+ if (atName && names) {
26
+ const name = JSON.parse(text.slice(at, end + 1));
27
+ if (names.has(name))
28
+ return true;
29
+ names.add(name);
30
+ }
31
+ atName = false;
32
+ at = end;
33
+ }
34
+ return false;
35
+ }
@@ -0,0 +1,20 @@
1
+ /** A map whose writes throw; freezing a Map does not stop them. */
2
+ export declare function readOnly<K, V>(map: Map<K, V>): ReadonlyMap<K, V>;
3
+ /** Values received under names (query, form, fields): read-only, in the order sent. */
4
+ export interface Values extends ReadonlyMap<string, readonly string[]> {
5
+ /** The value when the name holds exactly one; undefined when it is absent or repeated. */
6
+ single(name: string): string | undefined;
7
+ /** The names holding more than one value, in order; empty when none. */
8
+ repeated(): readonly string[];
9
+ }
10
+ export declare function valuesOf(map: ReadonlyMap<string, readonly string[]>): Values;
11
+ /** A query's or a form's values, grouped by name in the order sent. */
12
+ export declare function paramsOf(search: URLSearchParams): Values;
13
+ /**
14
+ * A JSON object's string members, as fields, and the names of its other members, as mistyped (none
15
+ * when every member is a string). Neither can be written to.
16
+ */
17
+ export declare function fieldsOf(json: Readonly<Record<string, unknown>>): {
18
+ mistyped?: ReadonlySet<string>;
19
+ fields: Values;
20
+ };
@@ -0,0 +1,45 @@
1
+ const throwOnWrite = () => {
2
+ throw new TypeError("a request is read-only");
3
+ };
4
+ /** A map whose writes throw; freezing a Map does not stop them. */
5
+ export function readOnly(map) {
6
+ return Object.freeze(Object.assign(map, { set: throwOnWrite, delete: throwOnWrite, clear: throwOnWrite }));
7
+ }
8
+ export function valuesOf(map) {
9
+ const values = new Map([...map].map(([name, list]) => [name, Object.freeze([...list])]));
10
+ const single = (name) => {
11
+ const list = values.get(name);
12
+ return list?.length === 1 ? list[0] : undefined;
13
+ };
14
+ const repeated = () => Object.freeze([...values].filter(([, list]) => list.length > 1).map(([name]) => name));
15
+ return Object.freeze(Object.assign(values, {
16
+ set: throwOnWrite,
17
+ delete: throwOnWrite,
18
+ clear: throwOnWrite,
19
+ single,
20
+ repeated,
21
+ }));
22
+ }
23
+ /** A query's or a form's values, grouped by name in the order sent. */
24
+ export function paramsOf(search) {
25
+ const grouped = Map.groupBy(search, ([name]) => name);
26
+ return valuesOf(new Map([...grouped].map(([name, pairs]) => [name, pairs.map(([, v]) => v)])));
27
+ }
28
+ /**
29
+ * A JSON object's string members, as fields, and the names of its other members, as mistyped (none
30
+ * when every member is a string). Neither can be written to.
31
+ */
32
+ export function fieldsOf(json) {
33
+ const members = Object.entries(json);
34
+ const strings = members.flatMap(([name, value]) => typeof value === "string" ? [[name, [value]]] : []);
35
+ const others = members.filter(([, value]) => typeof value !== "string").map(([name]) => name);
36
+ const fields = valuesOf(new Map(strings));
37
+ const mistyped = others.length === 0
38
+ ? undefined
39
+ : Object.freeze(Object.assign(new Set(others), {
40
+ add: throwOnWrite,
41
+ delete: throwOnWrite,
42
+ clear: throwOnWrite,
43
+ }));
44
+ return { fields, ...(mistyped ? { mistyped } : {}) };
45
+ }
@@ -0,0 +1,113 @@
1
+ import type { Source } from "@o3co/auth-provider-foundation/request-source";
2
+ import type { Values } from "./bodies/values.js";
3
+ export type { Values } from "./bodies/values.js";
4
+ export type Method = "GET" | "POST" | "PUT" | "DELETE";
5
+ export interface HttpRequest {
6
+ readonly method: string;
7
+ readonly path: string;
8
+ readonly query: Values;
9
+ /** An `application/x-www-form-urlencoded` body; empty for other bodies. */
10
+ readonly form: Values;
11
+ /** A JSON object body, frozen, for a route that takes JSON. */
12
+ readonly json?: Readonly<Record<string, unknown>>;
13
+ /** The body's values by name: a form's fields, or a JSON object's strings. */
14
+ readonly fields: Values;
15
+ /** The names of a JSON object's members that are not strings, so no field; none when there are none. */
16
+ readonly mistyped?: ReadonlySet<string>;
17
+ /** Lower-case names; repeated headers are joined with ", ". */
18
+ readonly headers: ReadonlyMap<string, string>;
19
+ readonly source: Source;
20
+ }
21
+ export type Body = string | Uint8Array | {
22
+ readonly json: unknown;
23
+ };
24
+ /** What a page loads: its answer's policy allows these and nothing else. */
25
+ export interface Loads {
26
+ /** The origins it frames, as `scheme://host[:port]`. */
27
+ readonly frames?: readonly string[];
28
+ /** The text of each inline script it runs. */
29
+ readonly scripts?: readonly string[];
30
+ }
31
+ export interface HttpResponse {
32
+ readonly status: number;
33
+ readonly headers?: Readonly<Record<string, string>>;
34
+ /** `Set-Cookie` values, one header each. */
35
+ readonly cookies?: readonly string[];
36
+ readonly body?: Body;
37
+ readonly loads?: Loads;
38
+ }
39
+ /** A rule a route can ask http to apply before its handler. Its owner makes it; it is unique as a value. */
40
+ export interface Policy<O = undefined> {
41
+ readonly kind: "policy";
42
+ readonly label: string;
43
+ readonly __options?: O;
44
+ }
45
+ export declare function policy<O = undefined>(label: string): Policy<O>;
46
+ /** A route's use of a policy, with what the route gives it. */
47
+ export interface PolicyUse {
48
+ readonly policy: Policy<unknown>;
49
+ readonly options: unknown;
50
+ }
51
+ export declare function use<O>(policy: Policy<O>, ...options: O extends undefined ? [] : [O]): PolicyUse;
52
+ /** How a policy applies: pass the request on to the next, or answer it. Throws Unavailable when it cannot tell. */
53
+ export interface PolicyRule {
54
+ readonly policy: Policy<unknown>;
55
+ apply(request: HttpRequest, options: unknown): "pass" | HttpResponse | Promise<"pass" | HttpResponse>;
56
+ }
57
+ export declare function applying<O>(policy: Policy<O>, apply: (request: HttpRequest, options: O) => "pass" | HttpResponse | Promise<"pass" | HttpResponse>): PolicyRule;
58
+ /** The rules of the policies, one per policy; http matches them to policies by value. */
59
+ export declare const policies: import("@o3co/auth-provider-foundation/kernel").ExtensionPoint<PolicyRule>;
60
+ /** A form is `application/x-www-form-urlencoded`; JSON is `application/json`; none is no body at all. */
61
+ export type BodyKind = "form" | "json" | "none";
62
+ /** A route that takes cross-origin requests from the origins the operator lists; never with credentials. */
63
+ export interface CorsRule {
64
+ /** Request headers a page may send beyond the safelisted ones, lower case. */
65
+ readonly headers?: readonly string[];
66
+ /** Response headers a page may read beyond the safelisted ones, lower case. */
67
+ readonly exposes?: readonly string[];
68
+ }
69
+ export interface Route {
70
+ readonly method: Method;
71
+ readonly path: string;
72
+ /** Applied in this order before `handle`; the first that answers ends the request. */
73
+ readonly uses?: readonly PolicyUse[];
74
+ /** The bodies the route takes, by type; any other is `415`. Without it, only a form is read. */
75
+ readonly takes?: readonly BodyKind[];
76
+ /** Takes cross-origin requests; without it, none is answered as one. */
77
+ readonly cors?: CorsRule;
78
+ handle(request: HttpRequest): HttpResponse | Promise<HttpResponse>;
79
+ }
80
+ export interface Listening {
81
+ port(): number;
82
+ }
83
+ export declare const routes: import("@o3co/auth-provider-foundation/kernel").ExtensionPoint<Route>;
84
+ export declare const listening: import("@o3co/auth-provider-foundation/kernel").Contract<Listening>;
85
+ /** Where browsers reach this deployment. */
86
+ export interface Site {
87
+ /** Undefined when the operator did not state it. */
88
+ readonly origin: string | undefined;
89
+ /** Whether cookies can be made secure (`__Host-` and Secure): false only for a plain-http origin. */
90
+ readonly secureCookies: boolean;
91
+ }
92
+ export declare const site: import("@o3co/auth-provider-foundation/kernel").Contract<Site>;
93
+ /** A request's end, once per request, when it was answered or its connection closed. */
94
+ export interface RequestEnd {
95
+ /** The method as sent. */
96
+ readonly method: string;
97
+ /** The declared path of the route the request reached; none when it reached none. */
98
+ readonly route?: string;
99
+ /** The status written, or `closed` when the client closed before any answer was written. */
100
+ readonly status: number | "closed";
101
+ /** Milliseconds from the request's arrival to its end; never negative. */
102
+ readonly ms: number;
103
+ }
104
+ /** Hears each request's end, after the request and asynchronously; what it throws or rejects is dropped, never reaching the request or the others. */
105
+ export interface RequestEndObserver {
106
+ ended(end: RequestEnd): void;
107
+ }
108
+ export declare const requestEnds: import("@o3co/auth-provider-foundation/kernel").ExtensionPoint<RequestEndObserver>;
109
+ /** Headers keeping an answer out of every cache, HTTP/1.0 ones included (RFC 6749 §5.1). */
110
+ export declare const noStore: Readonly<{
111
+ "cache-control": "no-store";
112
+ pragma: "no-cache";
113
+ }>;
@@ -0,0 +1,23 @@
1
+ import { contract, extensionPoint } from "@o3co/auth-provider-foundation/kernel";
2
+ export function policy(label) {
3
+ return Object.freeze({ kind: "policy", label });
4
+ }
5
+ export function use(policy, ...options) {
6
+ return Object.freeze({ policy: policy, options: options[0] });
7
+ }
8
+ export function applying(policy, apply) {
9
+ return { policy: policy, apply: apply };
10
+ }
11
+ /** The rules of the policies, one per policy; http matches them to policies by value. */
12
+ export const policies = extensionPoint("http.policies", { multiplicity: "many" });
13
+ export const routes = extensionPoint("http.routes", {
14
+ multiplicity: "keyed",
15
+ keyOf: (route) => `${route.method} ${route.path}`,
16
+ });
17
+ export const listening = contract("http.listening");
18
+ export const site = contract("http.site");
19
+ export const requestEnds = extensionPoint("http.request-ends", {
20
+ multiplicity: "many",
21
+ });
22
+ /** Headers keeping an answer out of every cache, HTTP/1.0 ones included (RFC 6749 §5.1). */
23
+ export const noStore = Object.freeze({ "cache-control": "no-store", pragma: "no-cache" });
@@ -0,0 +1,24 @@
1
+ # cors
2
+
3
+ Internal module of `http`: the answers to cross-origin requests, for the routes that declare them.
4
+
5
+ ## Responsibilities
6
+ - Lets the origins the operator lists read the routes that take cross-origin requests.
7
+
8
+ ## Not here
9
+ - Which routes take cross-origin requests (the route's declaration), and which origins are listed (the operator's).
10
+
11
+ ## Offers
12
+ - `crossOrigin`: a declared route's answer, made readable by a listed origin.
13
+ - `preflight`: the answer to a preflight on a path with a declared route.
14
+
15
+ ## Invariants
16
+ - Answers vary by `Origin`, a route's own `Vary` kept.
17
+ - An answer names the request's origin only when it is listed.
18
+ - An answer never names `*`.
19
+ - An answer never allows credentials.
20
+ - 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.
21
+ - Any other preflight on such a path gets `204` with nothing to read by.
22
+
23
+ ## Depends on
24
+ - `http` (its contract).
@@ -0,0 +1,5 @@
1
+ import type { CorsRule, HttpResponse, Route } from "../contract.js";
2
+ /** A declared route's answer: varies by origin, and lets a listed origin read it. */
3
+ export declare function crossOrigin(rule: CorsRule, origin: string | undefined, origins: ReadonlySet<string>, response: HttpResponse): HttpResponse;
4
+ /** A preflight's answer where a route of the path takes cross-origin requests; else undefined. */
5
+ export declare function preflight(reached: readonly Route[], method: string, origin: string | undefined, origins: ReadonlySet<string>): HttpResponse | undefined;
@@ -0,0 +1,35 @@
1
+ const maxAge = "600";
2
+ function varyingByOrigin(headers) {
3
+ const vary = headers.vary;
4
+ return { ...headers, vary: vary ? `${vary}, Origin` : "Origin" };
5
+ }
6
+ /** A declared route's answer: varies by origin, and lets a listed origin read it. */
7
+ export function crossOrigin(rule, origin, origins, response) {
8
+ const headers = varyingByOrigin(response.headers ?? {});
9
+ if (origin !== undefined && origins.has(origin)) {
10
+ headers["access-control-allow-origin"] = origin;
11
+ if (rule.exposes?.length)
12
+ headers["access-control-expose-headers"] = rule.exposes.join(", ");
13
+ }
14
+ return { ...response, headers };
15
+ }
16
+ /** A preflight's answer where a route of the path takes cross-origin requests; else undefined. */
17
+ export function preflight(reached, method, origin, origins) {
18
+ const declared = reached.filter((r) => r.cors);
19
+ if (declared.length === 0)
20
+ return undefined;
21
+ const route = declared.find((r) => r.method === (method === "HEAD" ? "GET" : method));
22
+ if (!route?.cors || origin === undefined || !origins.has(origin)) {
23
+ return { status: 204, headers: { vary: "Origin" } };
24
+ }
25
+ const headers = {
26
+ vary: "Origin",
27
+ "access-control-allow-origin": origin,
28
+ "access-control-allow-methods": declared.map((r) => r.method).join(", "),
29
+ "access-control-max-age": maxAge,
30
+ };
31
+ if (route.cors.headers?.length) {
32
+ headers["access-control-allow-headers"] = route.cors.headers.join(", ");
33
+ }
34
+ return { status: 204, headers };
35
+ }
@@ -0,0 +1,23 @@
1
+ # ending
2
+
3
+ Internal module of `http`: tells the observers of `requestEnds` each request's end.
4
+
5
+ ## Responsibilities
6
+ - Tells each request's end, once, answered or closed.
7
+
8
+ ## Not here
9
+ - What an observer does with an end (its owner's).
10
+
11
+ ## Offers
12
+ - `ending`: a request and its answer → the observers told at its end, and the function naming the route it reached.
13
+
14
+ ## Invariants
15
+ - An end names the method sent.
16
+ - An end names the declared path of the route reached (none when none).
17
+ - An end names the status written, or `closed` when the client closed first.
18
+ - An end names the milliseconds taken (never negative).
19
+ - Observers are told after the request, asynchronously.
20
+ - What an observer throws or rejects is dropped.
21
+
22
+ ## Depends on
23
+ - `http` (its contract), `kernel` (the clock).
@@ -0,0 +1,5 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { Clock } from "@o3co/auth-provider-foundation/kernel";
3
+ import type { RequestEndObserver } from "../contract.js";
4
+ /** Tells each observer of one request's end, once, answered or closed, after the request; gives the function naming the route it reached. */
5
+ export declare function ending(observers: readonly RequestEndObserver[], clock: Clock, req: IncomingMessage, res: ServerResponse): (path: string) => void;
@@ -0,0 +1,27 @@
1
+ /** Tells each observer of one request's end, once, answered or closed, after the request; gives the function naming the route it reached. */
2
+ export function ending(observers, clock, req, res) {
3
+ const started = clock.now();
4
+ const method = req.method ?? "GET";
5
+ let route;
6
+ // An answered request emits finish, then close; the first one ends it.
7
+ let ended = false;
8
+ const end = (status) => {
9
+ if (ended)
10
+ return;
11
+ ended = true;
12
+ // A clock set back gives 0, never a negative time.
13
+ const ms = Math.max(0, clock.now() - started);
14
+ const told = Object.freeze({ method, ...(route && { route }), status, ms });
15
+ for (const observer of observers) {
16
+ // After the listener; nothing above it catches, and an unhandled rejection stops the process.
17
+ void Promise.resolve()
18
+ .then(() => observer.ended(told))
19
+ .catch(() => { });
20
+ }
21
+ };
22
+ res.once("finish", () => end(res.statusCode));
23
+ res.once("close", () => end(res.headersSent ? res.statusCode : "closed"));
24
+ return (path) => {
25
+ route = path;
26
+ };
27
+ }
@@ -0,0 +1,20 @@
1
+ # guards
2
+
3
+ Internal module of `http`: the policies each route uses, matched to the rules modules contribute.
4
+
5
+ ## Responsibilities
6
+ - Guards each route with the rules of the policies it uses.
7
+
8
+ ## Not here
9
+ - What a policy decides and answers (its owner's).
10
+ - Refusing boot (the package's module, from what this finds).
11
+
12
+ ## Offers
13
+ - `withPolicies`: the routes and the rules → the routes guarded, the routes using a policy with no rule, or a policy with two rules.
14
+
15
+ ## Invariants
16
+ - A route's policies run in its order before its handler, and the first answer ends the request.
17
+ - Policies match by value, and a label only names one in a report.
18
+
19
+ ## Depends on
20
+ - `http` (its contract).
@@ -0,0 +1,19 @@
1
+ import type { HttpRequest, HttpResponse, PolicyRule, Route } from "../contract.js";
2
+ /** A route with its guard: the policies it uses, in its order; the first that answers ends the request. */
3
+ export type GuardedRoute = Route & {
4
+ readonly guard?: (request: HttpRequest) => Promise<HttpResponse | "pass">;
5
+ };
6
+ /** Routes guarded by their policies, matched by value (never label); or a policy with no rule, or two. */
7
+ export declare function withPolicies(routes: readonly Route[], rules: readonly PolicyRule[]): {
8
+ readonly kind: "applied";
9
+ readonly routes: readonly GuardedRoute[];
10
+ } | {
11
+ readonly kind: "missing";
12
+ readonly missing: readonly {
13
+ route: Route;
14
+ label: string;
15
+ }[];
16
+ } | {
17
+ readonly kind: "twice";
18
+ readonly label: string;
19
+ };
@@ -0,0 +1,31 @@
1
+ /** Routes guarded by their policies, matched by value (never label); or a policy with no rule, or two. */
2
+ export function withPolicies(routes, rules) {
3
+ const ruleOf = new Map(rules.map((rule) => [rule.policy, rule]));
4
+ const twice = rules.find((rule, at) => rules.findIndex((r) => r.policy === rule.policy) !== at);
5
+ if (twice)
6
+ return { kind: "twice", label: twice.policy.label };
7
+ const missing = routes.flatMap((route) => (route.uses ?? [])
8
+ .filter((use) => !ruleOf.has(use.policy))
9
+ .map((use) => ({ route, label: use.policy.label })));
10
+ if (missing.length > 0)
11
+ return { kind: "missing", missing };
12
+ return {
13
+ kind: "applied",
14
+ routes: routes.map((route) => {
15
+ const uses = route.uses ?? [];
16
+ if (uses.length === 0)
17
+ return route;
18
+ return {
19
+ ...route,
20
+ guard: async (request) => {
21
+ for (const use of uses) {
22
+ const answer = await ruleOf.get(use.policy).apply(request, use.options);
23
+ if (answer !== "pass")
24
+ return answer;
25
+ }
26
+ return "pass";
27
+ },
28
+ };
29
+ }),
30
+ };
31
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: import("@o3co/auth-provider-foundation/kernel").Module;
2
+ export default _default;
@@ -0,0 +1,46 @@
1
+ import { once } from "node:events";
2
+ import { certificateForwarding } from "@o3co/auth-provider-foundation/client-certificate";
3
+ import { defineModule } from "@o3co/auth-provider-foundation/kernel";
4
+ import { proxiesOf } from "@o3co/auth-provider-foundation/request-source";
5
+ import { listening, policies, requestEnds, routes, site } from "./contract.js";
6
+ import { withPolicies } from "./guards/guards.js";
7
+ import { drain, serve } from "./serving/server.js";
8
+ import { section } from "./settings/section.js";
9
+ export default defineModule({ name: "http", section }, (m) => [
10
+ m.provide(site).by((c) => {
11
+ const origin = c.section["public-origin"];
12
+ return Object.freeze({ origin, secureCookies: !origin?.startsWith("http:") });
13
+ }),
14
+ m
15
+ .provide(certificateForwarding)
16
+ .by((c) => Object.freeze({ configured: c.section["client-certificate"] !== undefined })),
17
+ m.provide(listening).by(async (c) => {
18
+ const applied = withPolicies(await c.collect(routes), await c.collect(policies));
19
+ if (applied.kind === "twice") {
20
+ return c.refuse("http.policies", `more than one rule for a policy: ${applied.label}`);
21
+ }
22
+ if (applied.kind === "missing") {
23
+ return c.refuseAll(applied.missing.map(({ route, label }) => ({
24
+ where: `http.routes:${route.method} ${route.path}`,
25
+ reason: `uses a policy no module applies: ${label}`,
26
+ })));
27
+ }
28
+ const certificate = c.section["client-certificate"];
29
+ const server = serve({
30
+ routes: applied.routes,
31
+ proxies: proxiesOf(c.section, certificate),
32
+ origins: new Set(c.section["cors-origins"]),
33
+ ends: await c.collect(requestEnds),
34
+ log: c.log,
35
+ clock: c.clock,
36
+ });
37
+ const port = () => server.address().port;
38
+ c.onStart(async () => {
39
+ server.listen(c.section.port, c.section.host);
40
+ await once(server, "listening");
41
+ c.log.info("http.listening", { port: port() });
42
+ });
43
+ c.onStop((signal) => drain(server, signal));
44
+ return { port };
45
+ }),
46
+ ]);
@@ -0,0 +1,23 @@
1
+ # security-headers
2
+
3
+ Internal module of `http`: the security headers every answer carries, and a page's policy.
4
+
5
+ ## Responsibilities
6
+ - Sets the security headers of every answer in one place.
7
+
8
+ ## Not here
9
+ - What a page loads (the route's `loads`).
10
+
11
+ ## Offers
12
+ - `secured`: an answer's headers → the same with the security headers, the policy in place of the route's.
13
+
14
+ ## Invariants
15
+ - A Content-Security-Policy that loads nothing by default and lets no one frame the answer.
16
+ - HSTS, `nosniff`, `X-Frame-Options`, `Referrer-Policy: no-referrer` and the cross-origin ones.
17
+ - A route's own values of them, its policy included, are replaced.
18
+ - Only `loads` widens a page's policy.
19
+ - A page's policy allows the frames and scripts it declares and nothing else.
20
+ - A frame a policy cannot name makes the answer `500`.
21
+
22
+ ## Depends on
23
+ - `http` (its contract).
@@ -0,0 +1,3 @@
1
+ import type { Loads } from "../contract.js";
2
+ /** The answer's headers, the security headers and the policy in place of the route's. */
3
+ export declare function secured<V>(headers: Readonly<Record<string, V>>, loads?: Loads): Record<string, V | string>;