@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,6 @@
1
+ export function contract(label) {
2
+ return Object.freeze({ kind: "contract", label });
3
+ }
4
+ export function extensionPoint(label, rule) {
5
+ return Object.freeze({ kind: "extension-point", label, rule: Object.freeze(rule) });
6
+ }
@@ -0,0 +1,12 @@
1
+ import type { Clock, Log, LogSink } from "./runtime.js";
2
+ /** In order: a threshold lets through its own level and those after it. */
3
+ export declare const thresholds: readonly ["trace", "debug", "info", "warn", "error", "fatal", "silent"];
4
+ export type Threshold = (typeof thresholds)[number];
5
+ export declare const logSinks: import("./handles.js").ExtensionPoint<LogSink>;
6
+ /** The same log, its calls never throwing. */
7
+ export declare function guardedLog(log: Log): Log;
8
+ /**
9
+ * A log handing each line at or above `threshold` to every sink, in order. `sinks` answers
10
+ * undefined while the sinks are being built, and those lines are dropped.
11
+ */
12
+ export declare function routedLog(threshold: Threshold, clock: Clock, sinks: () => readonly LogSink[] | undefined): Log;
@@ -0,0 +1,55 @@
1
+ import { extensionPoint } from "./handles.js";
2
+ /** In order: a threshold lets through its own level and those after it. */
3
+ export const thresholds = ["trace", "debug", "info", "warn", "error", "fatal", "silent"];
4
+ export const logSinks = extensionPoint("logSinks", { multiplicity: "many" });
5
+ /** A Log whose calls never throw, whatever `write` does. */
6
+ function logOf(write) {
7
+ const method = (level) => (event, fields) => {
8
+ try {
9
+ write(level, event, fields);
10
+ }
11
+ catch {
12
+ // Log's contract: a call never throws, so no caller guards one.
13
+ }
14
+ };
15
+ return Object.freeze({
16
+ trace: method("trace"),
17
+ debug: method("debug"),
18
+ info: method("info"),
19
+ warn: method("warn"),
20
+ error: method("error"),
21
+ fatal: method("fatal"),
22
+ });
23
+ }
24
+ /** The same log, its calls never throwing. */
25
+ export function guardedLog(log) {
26
+ return logOf((level, event, fields) => log[level](event, fields));
27
+ }
28
+ /**
29
+ * A log handing each line at or above `threshold` to every sink, in order. `sinks` answers
30
+ * undefined while the sinks are being built, and those lines are dropped.
31
+ */
32
+ export function routedLog(threshold, clock, sinks) {
33
+ const floor = thresholds.indexOf(threshold);
34
+ return logOf((level, event, fields) => {
35
+ if (thresholds.indexOf(level) < floor)
36
+ return;
37
+ const list = sinks();
38
+ if (list === undefined)
39
+ return;
40
+ const line = Object.freeze({
41
+ level,
42
+ event,
43
+ at: clock.now(),
44
+ fields: Object.freeze({ ...fields }),
45
+ });
46
+ for (const sink of list) {
47
+ try {
48
+ sink.write(line);
49
+ }
50
+ catch {
51
+ // A failing sink must not keep the line from the next sink.
52
+ }
53
+ }
54
+ });
55
+ }
@@ -0,0 +1,67 @@
1
+ import type { ZodType } from "zod";
2
+ import type { Contract, ExtensionPoint } from "./handles.js";
3
+ import type { ReadinessReport } from "./readiness.js";
4
+ import type { Refusal } from "./refusal.js";
5
+ import type { Runtime } from "./runtime.js";
6
+ export interface BuildContext<S> extends Runtime {
7
+ readonly section: S;
8
+ get<T>(contract: Contract<T>): Promise<T>;
9
+ collect<T>(point: ExtensionPoint<T>): Promise<readonly T[]>;
10
+ /**
11
+ * The interface a contract names, for use once assembly is done; it builds nothing now, so a
12
+ * contributor may hold its owner's. Called during assembly, or after a refused one, it throws.
13
+ */
14
+ late<T>(contract: Contract<T>): () => T;
15
+ /**
16
+ * Whether every contribution to `probes` passes within `deadlineMs` (1 to 2^31-1, else it
17
+ * rejects with a TypeError), with each probe's result by name; unready and all down once stop
18
+ * begins. It rejects until assembly is done.
19
+ */
20
+ readinessReport(deadlineMs: number): Promise<ReadinessReport>;
21
+ onStart(hook: () => Promise<void> | void): void;
22
+ /**
23
+ * Hooks run in reverse order, each awaited; one that fails is logged and the next runs. At the
24
+ * stop deadline the signal aborts, and the hooks left are called without waiting.
25
+ */
26
+ onStop(hook: (signal: AbortSignal) => Promise<void> | void): void;
27
+ /**
28
+ * Runs `task` every `intervalMs` from the start, never two runs at once. Stopping aborts the
29
+ * running run's signal and waits for it, before the stop hooks. A failed run is logged, and the
30
+ * next one comes as usual.
31
+ */
32
+ every(intervalMs: number, task: (signal: AbortSignal) => Promise<void>): void;
33
+ refuse(where: string, reason: string): never;
34
+ /** Refuses with every problem found at once, so the operator sees them together. */
35
+ refuseAll(refusals: readonly Refusal[]): never;
36
+ }
37
+ export type Factory<S, T> = (context: BuildContext<S>) => T | Promise<T>;
38
+ export interface Provision<S> {
39
+ readonly contract: Contract<unknown>;
40
+ readonly create: Factory<S, unknown>;
41
+ }
42
+ export interface Contribution<S> {
43
+ readonly point: ExtensionPoint<unknown>;
44
+ readonly create: Factory<S, unknown>;
45
+ }
46
+ export interface ModuleHead<S> {
47
+ readonly name: string;
48
+ readonly section?: ZodType<S>;
49
+ /** A function is asked only with a section that passed its schema. */
50
+ readonly replicaSafe?: boolean | ((section: S) => boolean);
51
+ }
52
+ export interface ModuleDefinition<S = unknown> extends ModuleHead<S> {
53
+ readonly provides?: readonly Provision<S>[];
54
+ readonly contributes?: readonly Contribution<S>[];
55
+ }
56
+ /** A module with its section type erased, as assembly sees it. */
57
+ export type Module = ModuleDefinition<unknown>;
58
+ /** Two steps, so the value type is fixed by the handle before the factory is checked. */
59
+ export interface Parts<S> {
60
+ provide<T>(contract: Contract<T>): {
61
+ by(create: Factory<S, T>): Provision<S>;
62
+ };
63
+ contribute<T>(point: ExtensionPoint<T>): {
64
+ by(create: Factory<S, T>): Contribution<S>;
65
+ };
66
+ }
67
+ export declare function defineModule<S = undefined>(head: ModuleHead<S>, body?: (parts: Parts<S>) => readonly (Provision<S> | Contribution<S>)[]): Module;
@@ -0,0 +1,18 @@
1
+ const parts = {
2
+ provide: (contract) => ({
3
+ by: (create) => ({ contract: contract, create }),
4
+ }),
5
+ contribute: (point) => ({
6
+ // keyOf takes T, so a point is not assignable to the unknown one directly.
7
+ by: (create) => ({ point: point, create }),
8
+ }),
9
+ };
10
+ export function defineModule(head, body = () => []) {
11
+ const all = body(parts);
12
+ const definition = {
13
+ ...head,
14
+ provides: all.filter((part) => "contract" in part),
15
+ contributes: all.filter((part) => "point" in part),
16
+ };
17
+ return Object.freeze(definition);
18
+ }
@@ -0,0 +1,24 @@
1
+ import type { Runtime } from "./runtime.js";
2
+ export interface Probe {
3
+ /** The resource's name, as the log shows it; the key at `probes`. */
4
+ readonly name: string;
5
+ /** Resolves when the resource answers, rejects when it does not. Aborted at the deadline. */
6
+ check(signal: AbortSignal): Promise<void>;
7
+ }
8
+ export type Readiness = "ready" | "unready";
9
+ /** One run's answer with each probe's result, in contribution order. */
10
+ export interface ReadinessReport {
11
+ readonly answer: Readiness;
12
+ readonly probes: readonly {
13
+ readonly name: string;
14
+ readonly up: boolean;
15
+ }[];
16
+ }
17
+ export declare const probes: import("./handles.js").ExtensionPoint<Probe>;
18
+ /** A timer's delay in whole ms, from 1 to the longest a timer takes (2^31-1). */
19
+ export declare function isTimerMs(ms: number): boolean;
20
+ /**
21
+ * Answers whether every probe passes within the caller's deadline, joining a check still running.
22
+ * A check is aborted once no caller waits for it.
23
+ */
24
+ export declare function readinessOf(list: readonly Probe[], runtime: Runtime, stopping: () => boolean): (deadlineMs: number) => Promise<ReadinessReport>;
@@ -0,0 +1,72 @@
1
+ import { deepFreeze } from "./freeze.js";
2
+ import { extensionPoint } from "./handles.js";
3
+ export const probes = extensionPoint("probes", {
4
+ multiplicity: "keyed",
5
+ keyOf: (probe) => probe.name,
6
+ });
7
+ /** A timer's delay in whole ms, from 1 to the longest a timer takes (2^31-1). */
8
+ export function isTimerMs(ms) {
9
+ return Number.isInteger(ms) && ms >= 1 && ms <= 2 ** 31 - 1;
10
+ }
11
+ /**
12
+ * Answers whether every probe passes within the caller's deadline, joining a check still running.
13
+ * A check is aborted once no caller waits for it.
14
+ */
15
+ export function readinessOf(list, runtime, stopping) {
16
+ const running = new Map();
17
+ const begin = (probe) => {
18
+ const existing = running.get(probe);
19
+ if (existing)
20
+ return existing;
21
+ const controller = new AbortController();
22
+ // Started inside a promise, so a check that throws at once fails like one that rejects.
23
+ const done = Promise.resolve().then(() => probe.check(controller.signal));
24
+ const clear = () => running.delete(probe);
25
+ // This also handles the rejection when nobody waits for the check any more.
26
+ done.then(clear, clear);
27
+ const entry = { done, controller, waiting: 0 };
28
+ running.set(probe, entry);
29
+ return entry;
30
+ };
31
+ const one = async (probe, deadline) => {
32
+ const started = runtime.clock.now();
33
+ const entry = begin(probe);
34
+ entry.waiting++;
35
+ const result = await Promise.race([
36
+ entry.done.then(() => ({ outcome: "passed" }), (error) => ({
37
+ outcome: "failed",
38
+ kind: error instanceof Error ? error.name : typeof error,
39
+ })),
40
+ deadline,
41
+ ]);
42
+ entry.waiting--;
43
+ if (result.outcome === "passed")
44
+ return true;
45
+ if (result.outcome === "late" && entry.waiting === 0)
46
+ entry.controller.abort();
47
+ runtime.log.warn("kernel.probe_failed", {
48
+ probe: probe.name,
49
+ ...result,
50
+ ms: runtime.clock.now() - started,
51
+ });
52
+ return false;
53
+ };
54
+ return (deadlineMs) => {
55
+ if (!isTimerMs(deadlineMs)) {
56
+ return Promise.reject(new TypeError("the readiness deadline must be a whole number of ms, 1 to 2^31-1"));
57
+ }
58
+ const report = (passed, answer) => deepFreeze({
59
+ answer,
60
+ probes: list.map((probe, i) => ({ name: probe.name, up: passed[i] === true })),
61
+ });
62
+ // Once stop begins, no probe runs and every one reads down.
63
+ if (stopping())
64
+ return Promise.resolve(report([], "unready"));
65
+ const { promise: deadline, resolve } = Promise.withResolvers();
66
+ const timer = setTimeout(() => resolve({ outcome: "late" }), deadlineMs);
67
+ timer.unref();
68
+ return Promise.all(list.map((probe) => one(probe, deadline)))
69
+ .then((passed) => report(passed, passed.every(Boolean) ? "ready" : "unready"))
70
+ .finally(() => clearTimeout(timer));
71
+ };
72
+ }
@@ -0,0 +1,19 @@
1
+ export interface Refusal {
2
+ readonly where: string;
3
+ readonly reason: string;
4
+ }
5
+ /** Raised by assembly when anything refuses; nothing is served after it. */
6
+ export declare class BootRefused extends Error {
7
+ readonly refusals: readonly Refusal[];
8
+ constructor(refusals: readonly Refusal[]);
9
+ }
10
+ /** Thrown by refuse and refuseAll inside a factory; assembly records its refusals. Never leaves the kernel. */
11
+ export declare class RefusalSignal extends Error {
12
+ readonly refusals: readonly Refusal[];
13
+ constructor(refusals: readonly Refusal[]);
14
+ }
15
+ export declare class Unavailable extends Error {
16
+ readonly what: string;
17
+ constructor(what: string);
18
+ }
19
+ export declare function isUnavailable(error: unknown): error is Unavailable;
@@ -0,0 +1,28 @@
1
+ /** Raised by assembly when anything refuses; nothing is served after it. */
2
+ export class BootRefused extends Error {
3
+ refusals;
4
+ constructor(refusals) {
5
+ super(`boot refused (${refusals.length})`);
6
+ this.name = "BootRefused";
7
+ this.refusals = Object.freeze([...refusals]);
8
+ }
9
+ }
10
+ /** Thrown by refuse and refuseAll inside a factory; assembly records its refusals. Never leaves the kernel. */
11
+ export class RefusalSignal extends Error {
12
+ refusals;
13
+ constructor(refusals) {
14
+ super(`${refusals.length} refusal(s)`);
15
+ this.refusals = refusals;
16
+ }
17
+ }
18
+ export class Unavailable extends Error {
19
+ what;
20
+ constructor(what) {
21
+ super(`${what} unavailable`);
22
+ this.name = "Unavailable";
23
+ this.what = what;
24
+ }
25
+ }
26
+ export function isUnavailable(error) {
27
+ return error instanceof Unavailable;
28
+ }
@@ -0,0 +1,35 @@
1
+ export interface Clock {
2
+ now(): number;
3
+ }
4
+ export interface Random {
5
+ bytes(length: number): Uint8Array;
6
+ }
7
+ export type LogFields = Readonly<Record<string, string | number | boolean>>;
8
+ /** A call never throws: kernel guards every log it hands out, so no caller wraps one in try. */
9
+ export interface Log {
10
+ trace(event: string, fields?: LogFields): void;
11
+ debug(event: string, fields?: LogFields): void;
12
+ info(event: string, fields?: LogFields): void;
13
+ warn(event: string, fields?: LogFields): void;
14
+ error(event: string, fields?: LogFields): void;
15
+ fatal(event: string, fields?: LogFields): void;
16
+ }
17
+ export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
18
+ export interface LogLine {
19
+ readonly level: LogLevel;
20
+ readonly event: string;
21
+ /** Epoch ms, on the kernel's clock. */
22
+ readonly at: number;
23
+ readonly fields: LogFields;
24
+ }
25
+ /** Where log lines go; contributed to `logSinks`. */
26
+ export interface LogSink {
27
+ write(line: LogLine): void;
28
+ }
29
+ export interface Runtime {
30
+ readonly clock: Clock;
31
+ readonly random: Random;
32
+ readonly log: Log;
33
+ }
34
+ export declare const systemClock: Clock;
35
+ export declare const systemRandom: Random;
@@ -0,0 +1,5 @@
1
+ import { randomBytes } from "node:crypto";
2
+ export const systemClock = Object.freeze({ now: () => Date.now() });
3
+ export const systemRandom = Object.freeze({
4
+ bytes: (length) => new Uint8Array(randomBytes(length)),
5
+ });
@@ -0,0 +1,2 @@
1
+ /** Whether two secrets (strings as UTF-8) hold the same bytes; only equal lengths are compared, in constant time. */
2
+ export declare function sameSecret(a: string | Uint8Array, b: string | Uint8Array): boolean;
@@ -0,0 +1,7 @@
1
+ import { timingSafeEqual } from "node:crypto";
2
+ /** Whether two secrets (strings as UTF-8) hold the same bytes; only equal lengths are compared, in constant time. */
3
+ export function sameSecret(a, b) {
4
+ const x = typeof a === "string" ? Buffer.from(a, "utf8") : a;
5
+ const y = typeof b === "string" ? Buffer.from(b, "utf8") : b;
6
+ return x.byteLength === y.byteLength && timingSafeEqual(x, y);
7
+ }
@@ -0,0 +1,20 @@
1
+ import { z } from "zod";
2
+ export declare const kernelSection: z.ZodObject<{
3
+ deployment: z.ZodEnum<{
4
+ single: "single";
5
+ multi: "multi";
6
+ }>;
7
+ logging: z.ZodDefault<z.ZodObject<{
8
+ level: z.ZodDefault<z.ZodEnum<{
9
+ trace: "trace";
10
+ debug: "debug";
11
+ info: "info";
12
+ warn: "warn";
13
+ error: "error";
14
+ fatal: "fatal";
15
+ silent: "silent";
16
+ }>>;
17
+ }, z.core.$strict>>;
18
+ "stop-seconds": z.ZodDefault<z.ZodInt>;
19
+ }, z.core.$strict>;
20
+ export type KernelSettings = z.output<typeof kernelSection>;
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+ import { thresholds } from "./log.js";
3
+ const stopRange = { error: "whole seconds from 1 to 3600" };
4
+ export const kernelSection = z.strictObject({
5
+ deployment: z
6
+ .enum(["single", "multi"], { error: "single or multi" })
7
+ .describe("single or multi; multi refuses a module unsafe on multiple instances"),
8
+ logging: z
9
+ .strictObject({
10
+ level: z
11
+ .enum(thresholds, { error: thresholds.join(", ") })
12
+ .default("info")
13
+ .describe("the lowest level logged (trace to fatal), or silent for none"),
14
+ }, { error: "holds only level" })
15
+ .default({ level: "info" })
16
+ .describe("the log"),
17
+ "stop-seconds": z
18
+ .int(stopRange)
19
+ .min(1, stopRange)
20
+ .max(3600, stopRange)
21
+ .default(30)
22
+ .describe("whole seconds a stop given no deadline waits for its hooks; an orchestrator's grace period must be longer"),
23
+ }, { error: "needs a section holding only deployment, logging and stop-seconds" });
@@ -0,0 +1,31 @@
1
+ import type { Contract } from "./handles.js";
2
+ import type { Module } from "./module.js";
3
+ import { type Log, type LogLine, type Runtime } from "./runtime.js";
4
+ /** A log that writes nothing. */
5
+ export declare const quietLog: Log;
6
+ /** A runtime for tests: the system clock and randomness and a quiet log, unless the test sets one. */
7
+ export declare function quietRuntime(parts?: Partial<Runtime>): Runtime;
8
+ export interface RecordingLog {
9
+ readonly log: Log;
10
+ /** The lines so far, in order, with their level; fields are empty when the call gave none. */
11
+ lines(): readonly Omit<LogLine, "at">[];
12
+ }
13
+ export declare function recordingLog(): RecordingLog;
14
+ /** Names of T's members that are functions answering a promise. Sync methods and values are not switchable. */
15
+ export type AsyncMethodOf<T> = {
16
+ [K in keyof T]-?: T[K] extends (...args: never[]) => Promise<unknown> ? K : never;
17
+ }[keyof T] & string;
18
+ export interface UnavailableSwitch<T extends object> {
19
+ /** T with each async method going through this switch; other members read through to `implementation`. */
20
+ wrap(implementation: T): T;
21
+ /** The next call of `method` rejects with Unavailable and does not reach the implementation. Called n times, the next n calls do. */
22
+ failNext(method: AsyncMethodOf<T>): void;
23
+ /** From now on, every call of these methods rejects with Unavailable and does not reach the implementation. */
24
+ unavailable(method: AsyncMethodOf<T>, ...more: AsyncMethodOf<T>[]): void;
25
+ /** Calls reach the implementation again; pending failNext calls are dropped. */
26
+ restore(): void;
27
+ }
28
+ /** `what` goes into each Unavailable it throws. */
29
+ export declare function unavailableSwitch<T extends object>(what?: string): UnavailableSwitch<T>;
30
+ /** The same module, its provision of `provided` built and then wrapped by `by`; every other part unchanged. Throws TypeError at once if the module does not provide `provided`. */
31
+ export declare function switchedModule<T extends object>(module: Module, provided: Contract<T>, by: UnavailableSwitch<T>): Module;
@@ -0,0 +1,99 @@
1
+ import { Unavailable } from "./refusal.js";
2
+ import { systemClock, systemRandom, } from "./runtime.js";
3
+ /** A log that writes nothing. */
4
+ export const quietLog = Object.freeze({
5
+ trace: () => { },
6
+ debug: () => { },
7
+ info: () => { },
8
+ warn: () => { },
9
+ error: () => { },
10
+ fatal: () => { },
11
+ });
12
+ /** A runtime for tests: the system clock and randomness and a quiet log, unless the test sets one. */
13
+ export function quietRuntime(parts = {}) {
14
+ return Object.freeze({ clock: systemClock, random: systemRandom, log: quietLog, ...parts });
15
+ }
16
+ export function recordingLog() {
17
+ const kept = [];
18
+ const writer = (level) => (event, fields) => {
19
+ kept.push(Object.freeze({ level, event, fields: Object.freeze({ ...fields }) }));
20
+ };
21
+ return Object.freeze({
22
+ log: Object.freeze({
23
+ trace: writer("trace"),
24
+ debug: writer("debug"),
25
+ info: writer("info"),
26
+ warn: writer("warn"),
27
+ error: writer("error"),
28
+ fatal: writer("fatal"),
29
+ }),
30
+ lines: () => [...kept],
31
+ });
32
+ }
33
+ function memberKeys(implementation) {
34
+ const keys = new Set();
35
+ for (let at = implementation; at !== null && at !== Object.prototype; at = Object.getPrototypeOf(at)) {
36
+ for (const key of Reflect.ownKeys(at))
37
+ if (key !== "constructor")
38
+ keys.add(key);
39
+ }
40
+ return [...keys];
41
+ }
42
+ /** `what` goes into each Unavailable it throws. */
43
+ export function unavailableSwitch(what = "test") {
44
+ const pending = new Map();
45
+ const down = new Set();
46
+ const fails = (method) => {
47
+ if (down.has(method))
48
+ return true;
49
+ const left = pending.get(method) ?? 0;
50
+ if (left === 0)
51
+ return false;
52
+ pending.set(method, left - 1);
53
+ return true;
54
+ };
55
+ return Object.freeze({
56
+ wrap(implementation) {
57
+ const source = implementation;
58
+ const wrapped = {};
59
+ for (const key of memberKeys(implementation)) {
60
+ if (typeof source[key] === "function") {
61
+ wrapped[key] = (...args) => fails(key)
62
+ ? Promise.reject(new Unavailable(what))
63
+ : Reflect.apply(source[key], implementation, args);
64
+ }
65
+ else {
66
+ Object.defineProperty(wrapped, key, { enumerable: true, get: () => source[key] });
67
+ }
68
+ }
69
+ return Object.freeze(wrapped);
70
+ },
71
+ failNext(method) {
72
+ pending.set(method, (pending.get(method) ?? 0) + 1);
73
+ },
74
+ unavailable(method, ...more) {
75
+ for (const name of [method, ...more])
76
+ down.add(name);
77
+ },
78
+ restore() {
79
+ pending.clear();
80
+ down.clear();
81
+ },
82
+ });
83
+ }
84
+ /** The same module, its provision of `provided` built and then wrapped by `by`; every other part unchanged. Throws TypeError at once if the module does not provide `provided`. */
85
+ export function switchedModule(module, provided, by) {
86
+ const provides = module.provides ?? [];
87
+ if (!provides.some((part) => part.contract === provided)) {
88
+ throw new TypeError(`${module.name} does not provide ${provided.label}`);
89
+ }
90
+ return Object.freeze({
91
+ ...module,
92
+ provides: provides.map((part) => part.contract !== provided
93
+ ? part
94
+ : {
95
+ contract: part.contract,
96
+ create: async (context) => by.wrap((await part.create(context))),
97
+ }),
98
+ });
99
+ }
package/keys/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # keys
2
+
3
+ ## Responsibilities
4
+ - Reads the provider's signing key ring at boot, signs and verifies with it, and publishes its public key set (JWKS).
5
+ - Reads public key material from outside (PEM and JWK) by one set of rules, for every module that verifies another party's signature.
6
+
7
+ ## Not here
8
+ - The meaning of what is signed. Which outside keys a module trusts, and where it gets them.
9
+
10
+ ## Offers
11
+ - `keys`: sign claims under a `typ` (adding the `kid`), and verify a token, answering valid with frozen claims or why not (`malformed`, `typ`, `kid_unknown`, `kid_expired`, `signature`).
12
+ - Whether signing is asymmetric, the `alg` it signs with, and the path of the public key set, a route contributed to `http` that answers its listed origins.
13
+ - `publicJwk`: a JWK the platform reads as a public key, kept as given.
14
+ - `publicPem`: a public key written as PEM.
15
+ - `verifiesUnder`: whether a JWK may verify, under an algorithm or any of `verifyAlgorithms`.
16
+ - `publicMembers`: a JWK rebuilt from its type's public members alone, without reading it.
17
+ - `isCanonicalJws`: whether a compact JWS is written in its one form; `verify` answers any other as `malformed`. `secretBytes` (exactly or at least n): the setting of a secret's bytes every module takes, strict base64 of at least 8 byte values, never a `REPLACE-` sample; a refusal never names the value.
18
+ - Operator section: `signing` (`kid`, `alg`, and a PKCS8 PEM `private-key`, or for HS256 a hex or base64 `secret`), `next` (`kid`, `alg`, `public-key`), `previous` (`kid`, `alg`, `public-key`, `expires-at`).
19
+ - Test configuration builders (`./testing`), with a new Ed25519 signing key unless a test sets one.
20
+
21
+ ## Invariants
22
+ - Key material has no default; a missing or unreadable key, or one not of its `alg`'s kind, refuses boot.
23
+ - A signing `private-key` not written as PEM is refused by the section, before it is read; so is the fragment's `REPLACE-` placeholder.
24
+ - EdDSA takes Ed25519, ES256 takes P-256, RS256 takes RSA of 2048 bits or more; an HMAC secret is 32 bytes or more.
25
+ - A previous or next key is the provider's own: a private PEM is taken too, and only its public half is used. The next key is published and verifies, but never signs, so verifiers hold it before it signs.
26
+ - A `kid` is 1–128 of letters, digits and `. _ ~ -`, unique in the ring.
27
+ - A token is verified by the key its `kid` names, after its `typ`, and refused once that key's `expires-at` is reached.
28
+ - A payload the key signed that is no JSON is answered as a bad signature.
29
+ - The published set holds public members alone, of unexpired keys; with nothing to publish, it answers 404 `no-store`.
30
+ - The set carries a strong ETag, answers 304 to it, and is cached five minutes.
31
+ - An outside JWK with a private member, a `kid` that is no string, or a key member not in its one form, is refused; so is a private PEM.
32
+ - An outside key may verify only for signatures (`use`, `key_ops`) and only under an algorithm its kind and curve fit.
33
+
34
+ ## Depends on
35
+ - `kernel`, `http`
36
+
37
+ ## Vocabulary
38
+ - key ring: the current signing key, the next key, and past verification keys still within their expiry.
39
+ - claim / `typ`: the content and type of a JWT being signed, owned by the side that asks for the signature.
@@ -0,0 +1,7 @@
1
+ keys {
2
+ signing {
3
+ kid = signing-1
4
+ alg = EdDSA
5
+ private-key = "REPLACE-PKCS8-PEM-PRIVATE-KEY"
6
+ }
7
+ }
@@ -0,0 +1,21 @@
1
+ export type VerifyResult = {
2
+ readonly kind: "valid";
3
+ readonly claims: Readonly<Record<string, unknown>>;
4
+ } | {
5
+ readonly kind: "invalid";
6
+ readonly reason: "malformed" | "typ" | "kid_unknown" | "kid_expired" | "signature";
7
+ };
8
+ export interface Keys {
9
+ /** True when signatures can be checked with a public key; false for a shared secret. */
10
+ asymmetric(): boolean;
11
+ /** The JWS `alg` signatures are made with (the current signing key's). */
12
+ algorithm(): string;
13
+ /** The path on `http`'s origin the public key set is published at. */
14
+ keySetPath(): string;
15
+ sign(claims: Readonly<Record<string, unknown>>, typ: string): Promise<string>;
16
+ verify(token: string, typ: string): Promise<VerifyResult>;
17
+ }
18
+ export declare const keys: import("@o3co/auth-provider-foundation/kernel").Contract<Keys>;
19
+ export { isCanonicalJws } from "./jws.js";
20
+ export { type Jwk, publicJwk, publicMembers, publicPem, verifiesUnder, verifyAlgorithms, } from "./public.js";
21
+ export { secretBytes } from "./secret.js";
@@ -0,0 +1,5 @@
1
+ import { contract } from "@o3co/auth-provider-foundation/kernel";
2
+ export const keys = contract("keys");
3
+ export { isCanonicalJws } from "./jws.js";
4
+ export { publicJwk, publicMembers, publicPem, verifiesUnder, verifyAlgorithms, } from "./public.js";
5
+ export { secretBytes } from "./secret.js";
@@ -0,0 +1,3 @@
1
+ import { type HttpRequest, type HttpResponse } from "@o3co/auth-provider-foundation/http";
2
+ import { type Ring } from "./ring.js";
3
+ export declare function jwksResponse(ring: Ring, now: number, request: HttpRequest): HttpResponse;
@@ -0,0 +1,26 @@
1
+ import { createHash } from "node:crypto";
2
+ import { noStore } from "@o3co/auth-provider-foundation/http";
3
+ import { expired } from "./ring.js";
4
+ const cacheControl = "public, max-age=300";
5
+ function etagMatches(header, etag) {
6
+ return header?.split(",").some((tag) => tag.trim() === etag) ?? false;
7
+ }
8
+ export function jwksResponse(ring, now, request) {
9
+ const published = ring.publicKeys.filter((k) => !expired(k, () => now));
10
+ if (published.length === 0) {
11
+ return { status: 404, headers: noStore, body: { json: { error: "jwks_not_published" } } };
12
+ }
13
+ const body = JSON.stringify({
14
+ keys: published.map((k) => ({
15
+ ...k.key.export({ format: "jwk" }),
16
+ kid: k.kid,
17
+ alg: k.alg,
18
+ use: "sig",
19
+ })),
20
+ });
21
+ const etag = `"${createHash("sha256").update(body).digest("base64url")}"`;
22
+ const headers = { etag, "cache-control": cacheControl };
23
+ if (etagMatches(request.headers.get("if-none-match"), etag))
24
+ return { status: 304, headers };
25
+ return { status: 200, headers: { ...headers, "content-type": "application/json" }, body };
26
+ }