@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,38 @@
1
+ # audit
2
+
3
+ ## Responsibilities
4
+ - Takes audit events from the modules that emit them, stamps them with `kernel`'s clock, and hands them to the receivers the operator names.
5
+ - Provides the built-in receiver `log`, which writes one log line per event.
6
+
7
+ ## Not here
8
+ - The kinds of events and what they hold: the emitting side's.
9
+ - Keeping, batching or retrying events: each receiver's.
10
+
11
+ ## Offers
12
+ - `audit`: record an event (kind, subject, attributes as names and string values), returning at once.
13
+ - `audit`: the losses since boot, per named receiver, for a health check to read; the same as the metric `auth_audit_events_lost_total`, by `receiver`.
14
+ - Extension point: receivers, keyed by name, each taking an event.
15
+ - `auditEvents`: a module's declaration of its events (each kind's attributes, and when it is recorded), which it exports as `./audit-events` for the audit event reference; `declaredAudit` records only the kinds declared, with their attributes, and throws on another.
16
+ - The built-in receiver `log`: the kind, time, subject and the attributes' names, never their values.
17
+ - Operator section: `receivers`, their names or `none` (no default); `max-pending` (default 1000), each receiver's deliveries under way at once.
18
+ - Test configuration builders, and a stand-in that keeps every event (`./testing`).
19
+
20
+ ## Invariants
21
+ - A receiver gets the event frozen, with its time (`at`, epoch ms) from `kernel`'s clock, whatever the emitter wrote.
22
+ - The emitter's own attributes are copied, never frozen. A `userAgent`, the request's own text, reaches receivers bounded as 0.17 bounded it: a `?` for each character outside RFC 6749's NQSCHAR, at most 200 characters (a longer one keeps 197 and `...`); the subject and other attributes stay as written.
23
+ - Delivery is best effort (tier 1): receivers run after `record` returns, so an answer never waits for its events. One that throws or rejects never changes the emitter's answer or stops the others.
24
+ - An event is lost past a receiver's pending limit (never queued), when its receiver fails, or when still under way at the stop deadline: at stop, deliveries under way are awaited until `kernel.stop-seconds`.
25
+ - The built-in receiver `log` writes at `info`, so `kernel` drops its lines when the operator's log level is higher; the other receivers are not subject to that level.
26
+ - Every loss is counted, and a warning names the receiver once when it starts losing, with the failure's kind alone (`backlog`, `stopped`, or the error's name, never its message), and once when it next takes an event.
27
+ - Boot is refused without the section, for a name that is no receiver, for a composed receiver left unnamed or named `log`, and for `none` while a receiver is composed.
28
+
29
+ ## Data
30
+ - Each instance's own delivery counts; nothing is shared, so it is safe on many instances.
31
+
32
+ ## Depends on
33
+ - `kernel`, `metrics`
34
+
35
+ ## Vocabulary
36
+ - audit event: an occurrence the emitting module names and wants on record.
37
+ - receiver: a place audit events go.
38
+ - lost: an event a receiver did not take.
@@ -0,0 +1,3 @@
1
+ audit {
2
+ receivers = none
3
+ }
@@ -0,0 +1,37 @@
1
+ /** What an emitter records. The kind and the attributes are the emitter's vocabulary. */
2
+ export interface AuditRecord {
3
+ readonly kind: string;
4
+ readonly subject?: string;
5
+ readonly attributes?: Readonly<Record<string, string>>;
6
+ }
7
+ /** A record as receivers get it, stamped with `kernel`'s clock (epoch ms). */
8
+ export interface AuditEvent extends AuditRecord {
9
+ readonly at: number;
10
+ }
11
+ export interface Audit {
12
+ /** Hands the event to the receivers and returns at once; their failures never reach the caller. */
13
+ record(event: AuditRecord): void;
14
+ /** The events each named receiver did not take since boot (failed or past its pending limit). */
15
+ lost(): Readonly<Record<string, number>>;
16
+ }
17
+ export declare const audit: import("@o3co/auth-provider-foundation/kernel").Contract<Audit>;
18
+ /** A place audit events go. Keeping and retrying them are its own. */
19
+ export interface AuditReceiver {
20
+ readonly name: string;
21
+ receive(event: AuditEvent): Promise<void>;
22
+ }
23
+ export declare const auditReceivers: import("@o3co/auth-provider-foundation/kernel").ExtensionPoint<AuditReceiver>;
24
+ /** An event a module records: the attributes it may carry, and when it is recorded. */
25
+ export interface AuditEventDeclaration {
26
+ readonly attributes: readonly string[];
27
+ readonly when: string;
28
+ }
29
+ /** The events a module declares, by kind; its `./audit-events` exports them for the audit event reference. */
30
+ export declare const auditEvents: <const E extends Readonly<Record<string, AuditEventDeclaration>>>(events: E) => E;
31
+ /** Records only the kinds declared, with only their attributes; an undeclared one throws. */
32
+ export declare function declaredAudit<const E extends Readonly<Record<string, AuditEventDeclaration>>>(recorder: Pick<Audit, "record">, events: E): {
33
+ record<K extends keyof E & string>(kind: K, event?: {
34
+ readonly subject?: string;
35
+ readonly attributes?: Partial<Record<E[K]["attributes"][number], string>>;
36
+ }): void;
37
+ };
@@ -0,0 +1,29 @@
1
+ import { contract, extensionPoint } from "@o3co/auth-provider-foundation/kernel";
2
+ export const audit = contract("audit");
3
+ export const auditReceivers = extensionPoint("audit.receivers", {
4
+ multiplicity: "keyed",
5
+ keyOf: (receiver) => receiver.name,
6
+ });
7
+ /** The events a module declares, by kind; its `./audit-events` exports them for the audit event reference. */
8
+ export const auditEvents = (events) => Object.freeze(events);
9
+ /** Records only the kinds declared, with only their attributes; an undeclared one throws. */
10
+ export function declaredAudit(recorder, events) {
11
+ return {
12
+ record(kind, event = {}) {
13
+ const declared = events[kind];
14
+ if (!declared)
15
+ throw new Error(`${kind} is not declared`);
16
+ for (const name of Object.keys(event.attributes ?? {})) {
17
+ if (!declared.attributes.includes(name))
18
+ throw new Error(`${kind} does not declare ${name}`);
19
+ }
20
+ recorder.record({
21
+ kind,
22
+ ...(event.subject === undefined ? {} : { subject: event.subject }),
23
+ ...(event.attributes === undefined
24
+ ? {}
25
+ : { attributes: event.attributes }),
26
+ });
27
+ },
28
+ };
29
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: import("@o3co/auth-provider-foundation/kernel").Module;
2
+ export default _default;
@@ -0,0 +1,151 @@
1
+ import { contract, deepFreeze, defineModule } from "@o3co/auth-provider-foundation/kernel";
2
+ import { counter, instruments } from "@o3co/auth-provider-foundation/metrics";
3
+ import { audit, auditReceivers, } from "./contract.js";
4
+ import { section } from "./section.js";
5
+ /** The built-in receiver: one log line per event, naming the attributes without their values. */
6
+ const logReceiver = (log) => ({
7
+ name: "log",
8
+ receive: async (event) => {
9
+ log.info("audit", {
10
+ kind: event.kind,
11
+ at: event.at,
12
+ ...(event.subject === undefined ? {} : { subject: event.subject }),
13
+ ...(event.attributes ? { attributes: Object.keys(event.attributes).join(",") } : {}),
14
+ });
15
+ },
16
+ });
17
+ /** RFC 6749's NQSCHAR: printable ASCII but `"` and `\\`. */
18
+ const outsideNqschar = /[^\x20-\x21\x23-\x5b\x5d-\x7e]/gu;
19
+ /** The one attribute a request writes as it likes, bounded as 0.17 bounded it; identifiers are left as written. */
20
+ const requestText = "userAgent";
21
+ /** The most characters it keeps; a longer one is cut, the cut marked. */
22
+ const longest = 200;
23
+ /** The request's text as receivers get it, as 0.17 bounded it: `?` for each character outside NQSCHAR, at most 200. */
24
+ const bounded = (value) => {
25
+ const clean = value.replace(outsideNqschar, "?");
26
+ return clean.length <= longest ? clean : `${clean.slice(0, longest - 3)}...`;
27
+ };
28
+ /** Each named receiver's lost events since boot, as a metric. */
29
+ const lostEvents = contract("audit.lost-events");
30
+ /** Resolves once the signal aborts. */
31
+ const abortOf = (signal) => new Promise((resolve) => {
32
+ if (signal.aborted)
33
+ resolve();
34
+ else
35
+ signal.addEventListener("abort", () => resolve(), { once: true });
36
+ });
37
+ export default defineModule({ name: "audit", section, replicaSafe: true }, (m) => [
38
+ m.provide(lostEvents).by((c) => {
39
+ const named = c.section.receivers === "none" ? [] : c.section.receivers;
40
+ const spec = {
41
+ name: "auth_audit_events_lost_total",
42
+ help: "Audit events a receiver lost since boot: past max-pending, failed, or under way at the stop deadline.",
43
+ };
44
+ // A label needs one value at least; with no receiver the counter has no label and no series.
45
+ if (named.length === 0)
46
+ return counter({ ...spec, labels: {} });
47
+ return counter({ ...spec, labels: { receiver: named } });
48
+ }),
49
+ m.contribute(instruments).by((c) => c.get(lostEvents)),
50
+ m.provide(audit).by(async (c) => {
51
+ const contributed = await c.collect(auditReceivers);
52
+ const named = c.section.receivers === "none" ? [] : c.section.receivers;
53
+ const builtIns = new Map([["log", logReceiver(c.log)]]);
54
+ const known = new Map(builtIns);
55
+ const refusals = [];
56
+ for (const receiver of contributed) {
57
+ if (builtIns.has(receiver.name)) {
58
+ refusals.push({ where: "audit.receivers", reason: `${receiver.name} is a built-in name` });
59
+ }
60
+ else if (!named.includes(receiver.name)) {
61
+ refusals.push({
62
+ where: "audit.receivers",
63
+ reason: `${receiver.name} is composed but not named`,
64
+ });
65
+ }
66
+ known.set(receiver.name, receiver);
67
+ }
68
+ for (const name of named) {
69
+ if (!known.has(name))
70
+ refusals.push({ where: "audit.receivers", reason: `${name} is no receiver` });
71
+ }
72
+ if (refusals.length > 0)
73
+ c.refuseAll(refusals);
74
+ const max = c.section["max-pending"];
75
+ /** One receiver's events under way, lost since boot, and lost since it last took one. */
76
+ const lanes = named.map((name) => ({
77
+ receiver: known.get(name),
78
+ pending: 0,
79
+ lost: 0,
80
+ losing: 0,
81
+ }));
82
+ const counted = await c.get(lostEvents);
83
+ /** Past the stop deadline: what settles after is neither taken nor lost again. */
84
+ let closed = false;
85
+ const underWay = new Set();
86
+ /** `failure` is the kind alone (`backlog`, `stopped`, or the error's name), never a message a receiver wrote. */
87
+ const lose = (lane, failure) => {
88
+ lane.lost += 1;
89
+ counted.add({ receiver: lane.receiver.name });
90
+ lane.losing += 1;
91
+ if (lane.losing === 1) {
92
+ c.log.warn("audit receiver losing events", {
93
+ receiver: lane.receiver.name,
94
+ lost: 1,
95
+ failure,
96
+ });
97
+ }
98
+ };
99
+ const took = (lane) => {
100
+ if (lane.losing === 0)
101
+ return;
102
+ c.log.warn("audit receiver recovered", { receiver: lane.receiver.name, lost: lane.losing });
103
+ lane.losing = 0;
104
+ };
105
+ // At stop, deliveries under way get until the deadline; what is still under way then is lost.
106
+ c.onStop(async (signal) => {
107
+ await Promise.race([Promise.allSettled([...underWay]), abortOf(signal)]);
108
+ closed = true;
109
+ for (const lane of lanes) {
110
+ for (; lane.pending > 0; lane.pending -= 1)
111
+ lose(lane, "stopped");
112
+ }
113
+ });
114
+ return Object.freeze({
115
+ record: (event) => {
116
+ const { attributes, ...rest } = event;
117
+ // Copied, so freezing the event leaves the emitter's own attributes as they were.
118
+ const copied = attributes
119
+ ? {
120
+ attributes: Object.fromEntries(Object.entries(attributes).map(([k, v]) => [k, k === requestText ? bounded(v) : v])),
121
+ }
122
+ : {};
123
+ const stamped = deepFreeze({ ...rest, ...copied, at: c.clock.now() });
124
+ for (const lane of lanes) {
125
+ if (lane.pending >= max) {
126
+ lose(lane, "backlog");
127
+ continue;
128
+ }
129
+ lane.pending += 1;
130
+ // Deferred, so record returns first, and a receiver that throws at once fails like one that rejects.
131
+ const delivery = Promise.resolve()
132
+ .then(() => lane.receiver.receive(stamped))
133
+ .then(() => {
134
+ if (!closed)
135
+ took(lane);
136
+ }, (error) => {
137
+ if (!closed)
138
+ lose(lane, error instanceof Error ? error.name : "thrown");
139
+ })
140
+ .finally(() => {
141
+ if (!closed)
142
+ lane.pending -= 1;
143
+ underWay.delete(delivery);
144
+ });
145
+ underWay.add(delivery);
146
+ }
147
+ },
148
+ lost: () => Object.freeze(Object.fromEntries(lanes.map((l) => [l.receiver.name, l.lost]))),
149
+ });
150
+ }),
151
+ ]);
@@ -0,0 +1,5 @@
1
+ import { z } from "zod";
2
+ export declare const section: z.ZodObject<{
3
+ receivers: z.ZodUnion<readonly [z.ZodLiteral<"none">, z.ZodArray<z.ZodString>]>;
4
+ "max-pending": z.ZodDefault<z.ZodNumber>;
5
+ }, z.core.$strict>;
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+ export const section = z.strictObject({
3
+ receivers: z
4
+ .union([
5
+ z.literal("none"),
6
+ z
7
+ .array(z.string().min(1))
8
+ .min(1)
9
+ .refine((names) => new Set(names).size === names.length, "names must differ"),
10
+ ])
11
+ .describe("The receivers events go to, by name, or `none` to state that events go nowhere."),
12
+ "max-pending": z
13
+ .number()
14
+ .int()
15
+ .min(1)
16
+ .max(1_000_000)
17
+ .default(1000)
18
+ .describe("Each receiver's deliveries under way at once; an event past it is lost."),
19
+ });
@@ -0,0 +1,16 @@
1
+ import type { z } from "zod";
2
+ import { type AuditEvent } from "./contract.js";
3
+ import type { section } from "./section.js";
4
+ export type AuditSettings = z.input<typeof section>;
5
+ /** An audit section for tests: no receivers, stated, unless the test names some. */
6
+ export declare function auditConfig(settings?: Partial<AuditSettings>): {
7
+ audit: {
8
+ receivers: string | string[];
9
+ "max-pending"?: number | undefined;
10
+ };
11
+ };
12
+ /** A stand-in for tests of emitters: it keeps every event, stamped with the composition's clock. */
13
+ export declare function auditStandIn(): {
14
+ module: import("@o3co/auth-provider-foundation/kernel").Module;
15
+ events: AuditEvent[];
16
+ };
@@ -0,0 +1,19 @@
1
+ import { defineModule } from "@o3co/auth-provider-foundation/kernel";
2
+ import { audit } from "./contract.js";
3
+ /** An audit section for tests: no receivers, stated, unless the test names some. */
4
+ export function auditConfig(settings = {}) {
5
+ return { audit: { receivers: "none", ...settings } };
6
+ }
7
+ /** A stand-in for tests of emitters: it keeps every event, stamped with the composition's clock. */
8
+ export function auditStandIn() {
9
+ const events = [];
10
+ const module = defineModule({ name: "audit" }, (m) => [
11
+ m.provide(audit).by((c) => ({
12
+ record: (event) => {
13
+ events.push({ ...event, at: c.clock.now() });
14
+ },
15
+ lost: () => ({}),
16
+ })),
17
+ ]);
18
+ return { module, events };
19
+ }
@@ -0,0 +1,27 @@
1
+ # client-certificate
2
+
3
+ ## Responsibilities
4
+ - Reads the client's certificate that a trusted proxy forwards, in the form the operator names.
5
+
6
+ ## Not here
7
+ - Which peers are trusted proxies (`request-source`); what a certificate holds (its reader).
8
+
9
+ ## Offers
10
+ - The forwarded certificate: its leaf and chain, or `malformed`, as `request-source`'s source carries it.
11
+ - `certificateForwarding`: whether the operator named where certificates are forwarded.
12
+ - `certificateSettings`, the schema part `http`'s section takes as `client-certificate` (its keys in the settings list).
13
+
14
+ ## Invariants
15
+ - No client's certificate is read unless `client-certificate` is set.
16
+ - The client's certificate is read only from a direct peer that is a trusted proxy, each header at most 16 KiB.
17
+ - A certificate sent by another peer is ignored, and logged at most once a minute.
18
+ - Whether the proxy removes the header a client sends is the proxy's to ensure.
19
+ - A certificate read is handed on as copies of its bytes, frozen.
20
+ - PEM: one certificate block, URL-encoded or not, with nothing but whitespace beside it.
21
+ - XFCC: exactly one element; several, repeated header lines among them, are `malformed`.
22
+ - XFCC: outside quotes, `,` parts elements and `;` fields; a quoted value takes `\` before any character; a field needs `=`, and only spaces follow a closing quote.
23
+ - XFCC: Hash, Cert, Chain and Subject come at most once, in any case.
24
+ - XFCC: Cert is one URL-encoded certificate block and nothing else, Chain is certificate blocks only, and Hash is the leaf's SHA-256.
25
+
26
+ ## Depends on
27
+ - `kernel` (the clock of the warning), `zod`.
@@ -0,0 +1,9 @@
1
+ import type { CertificateHeaders, ForwardedCertificate } from "./contract.js";
2
+ export type CertificateRead = {
3
+ readonly kind: "none";
4
+ } | ForwardedCertificate;
5
+ /**
6
+ * The client certificate a trusted proxy forwards, decoded from its transport form only: what the
7
+ * certificate holds, and whether it is any good, is for its reader to judge.
8
+ */
9
+ export declare function certificateOf(headers: ReadonlyMap<string, string>, at: CertificateHeaders): CertificateRead;
@@ -0,0 +1,40 @@
1
+ import { base64, certificateBlocks } from "./pem.js";
2
+ import { xfccOf } from "./xfcc.js";
3
+ const malformed = Object.freeze({ kind: "malformed" });
4
+ /** The certificate as a request hands it on: copied out of the Buffers, frozen. */
5
+ const certificate = (leaf, chain) => Object.freeze({
6
+ kind: "certificate",
7
+ leaf: new Uint8Array(leaf),
8
+ chain: Object.freeze(chain.map((part) => new Uint8Array(part))),
9
+ });
10
+ const rawLimit = 16_384;
11
+ /** One structured-field byte sequence (`:base64:`). */
12
+ const byteSequence = (item) => {
13
+ const found = /^:([A-Za-z0-9+/=]*):$/.exec(item.trim());
14
+ return found?.[1] && base64.test(found[1]) ? Buffer.from(found[1], "base64") : undefined;
15
+ };
16
+ /** The one certificate block of a PEM text that holds nothing else. */
17
+ function pemBlock(text) {
18
+ const blocks = certificateBlocks(text);
19
+ return blocks?.length === 1 ? blocks[0] : undefined;
20
+ }
21
+ /**
22
+ * The client certificate a trusted proxy forwards, decoded from its transport form only: what the
23
+ * certificate holds, and whether it is any good, is for its reader to judge.
24
+ */
25
+ export function certificateOf(headers, at) {
26
+ const leafText = headers.get(at.header);
27
+ if (leafText === undefined)
28
+ return { kind: "none" };
29
+ const chainText = at["chain-header"] ? headers.get(at["chain-header"]) : undefined;
30
+ if (leafText.length > rawLimit || (chainText?.length ?? 0) > rawLimit)
31
+ return malformed;
32
+ if (at.dialect === "xfcc") {
33
+ const found = xfccOf(leafText);
34
+ return found ? certificate(found.leaf, found.chain) : malformed;
35
+ }
36
+ const leaf = at.dialect === "rfc9440" ? byteSequence(leafText) : pemBlock(leafText);
37
+ const chain = chainText?.split(",").map(byteSequence) ?? [];
38
+ const whole = (part) => (part?.length ?? 0) > 0;
39
+ return whole(leaf) && chain.every(whole) ? certificate(leaf, chain) : malformed;
40
+ }
@@ -0,0 +1,23 @@
1
+ export { type CertificateRead, certificateOf } from "./certificate.js";
2
+ export { certificateSettings } from "./settings.js";
3
+ export { ignoredCertificates } from "./warnings.js";
4
+ /** Where a trusted proxy puts the client's certificate, and in what form. */
5
+ export interface CertificateHeaders {
6
+ /** "rfc9440": byte sequences; "pem": one PEM block; "xfcc": Envoy's, the chain in the same header. */
7
+ readonly dialect: "rfc9440" | "pem" | "xfcc";
8
+ readonly header: string;
9
+ readonly "chain-header"?: string;
10
+ }
11
+ /** What a trusted proxy forwarded of the client's certificate: it, or that it was malformed. */
12
+ export type ForwardedCertificate = {
13
+ readonly kind: "certificate";
14
+ readonly leaf: Uint8Array;
15
+ readonly chain: readonly Uint8Array[];
16
+ } | {
17
+ readonly kind: "malformed";
18
+ };
19
+ /** Whether the operator named where a trusted proxy forwards the client's certificate. */
20
+ export interface CertificateForwarding {
21
+ readonly configured: boolean;
22
+ }
23
+ export declare const certificateForwarding: import("@o3co/auth-provider-foundation/kernel").Contract<CertificateForwarding>;
@@ -0,0 +1,5 @@
1
+ import { contract } from "@o3co/auth-provider-foundation/kernel";
2
+ export { certificateOf } from "./certificate.js";
3
+ export { certificateSettings } from "./settings.js";
4
+ export { ignoredCertificates } from "./warnings.js";
5
+ export const certificateForwarding = contract("client-certificate.certificate-forwarding");
@@ -0,0 +1,4 @@
1
+ /** Padded standard base64. */
2
+ export declare const base64: RegExp;
3
+ /** The certificate blocks a PEM text holds, URL-encoded or not, when it holds nothing else. */
4
+ export declare function certificateBlocks(encoded: string): Buffer[] | undefined;
@@ -0,0 +1,19 @@
1
+ /** Padded standard base64. */
2
+ export const base64 = /^[A-Za-z0-9+/]+={0,2}$/;
3
+ const block = /-----BEGIN CERTIFICATE-----([^-]*)-----END CERTIFICATE-----/g;
4
+ /** The certificate blocks a PEM text holds, URL-encoded or not, when it holds nothing else. */
5
+ export function certificateBlocks(encoded) {
6
+ let text;
7
+ try {
8
+ text = decodeURIComponent(encoded);
9
+ }
10
+ catch {
11
+ return undefined;
12
+ }
13
+ const bodies = [...text.matchAll(block)].map((found) => found[1]?.replace(/\s+/g, "") ?? "");
14
+ if (text.replace(block, "").trim() !== "" || bodies.length === 0)
15
+ return undefined;
16
+ if (bodies.some((body) => !base64.test(body)))
17
+ return undefined;
18
+ return bodies.map((body) => Buffer.from(body, "base64"));
19
+ }
@@ -0,0 +1,13 @@
1
+ import { z } from "zod";
2
+ /** Where a trusted proxy forwards the client's certificate, and in what form. */
3
+ export declare const certificateSettings: z.ZodDiscriminatedUnion<[z.ZodObject<{
4
+ dialect: z.ZodLiteral<"rfc9440">;
5
+ header: z.ZodDefault<z.ZodString>;
6
+ "chain-header": z.ZodDefault<z.ZodString>;
7
+ }, z.core.$strict>, z.ZodObject<{
8
+ dialect: z.ZodLiteral<"pem">;
9
+ header: z.ZodString;
10
+ }, z.core.$strict>, z.ZodObject<{
11
+ dialect: z.ZodLiteral<"xfcc">;
12
+ header: z.ZodDefault<z.ZodString>;
13
+ }, z.core.$strict>], "dialect">;
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+ const headerName = z.string().regex(/^[a-z0-9-]+$/);
3
+ const headerText = "The header holding the client's certificate: lower-case letters, digits and `-`; `pem` has no default header.";
4
+ const dialectText = "The form a trusted proxy forwards the client's certificate in: `rfc9440` (byte sequences) with `header` and `chain-header`, `pem` (one PEM block, URL-encoded or not) with `header`, or `xfcc` (Envoy's form, the chain in the same header) with `header`.";
5
+ /** Where a trusted proxy forwards the client's certificate, and in what form. */
6
+ export const certificateSettings = z.discriminatedUnion("dialect", [
7
+ z.strictObject({
8
+ dialect: z.literal("rfc9440").describe(dialectText),
9
+ header: headerName.default("client-cert").describe(headerText),
10
+ "chain-header": headerName
11
+ .default("client-cert-chain")
12
+ .describe("The header holding the chain behind the certificate, for `rfc9440`: lower-case letters, digits and `-`."),
13
+ }),
14
+ z.strictObject({
15
+ dialect: z.literal("pem").describe(dialectText),
16
+ header: headerName.describe(headerText),
17
+ }),
18
+ z.strictObject({
19
+ dialect: z.literal("xfcc").describe(dialectText),
20
+ header: headerName.default("x-forwarded-client-cert").describe(headerText),
21
+ }),
22
+ ]);
@@ -0,0 +1,7 @@
1
+ import type { Clock } from "@o3co/auth-provider-foundation/kernel";
2
+ import type { CertificateHeaders, ForwardedCertificate } from "./contract.js";
3
+ /**
4
+ * Whether to warn now of a certificate header that was sent but not taken (its peer is no trusted
5
+ * proxy): at most once a minute.
6
+ */
7
+ export declare function ignoredCertificates(at: CertificateHeaders | undefined, clock: Clock): (headers: ReadonlyMap<string, string>, taken: ForwardedCertificate | undefined) => boolean;
@@ -0,0 +1,17 @@
1
+ const minute = 60_000;
2
+ /**
3
+ * Whether to warn now of a certificate header that was sent but not taken (its peer is no trusted
4
+ * proxy): at most once a minute.
5
+ */
6
+ export function ignoredCertificates(at, clock) {
7
+ let last = Number.NEGATIVE_INFINITY;
8
+ return (headers, taken) => {
9
+ if (!at || !headers.has(at.header) || taken)
10
+ return false;
11
+ const now = clock.now();
12
+ if (now - last <= minute)
13
+ return false;
14
+ last = now;
15
+ return true;
16
+ };
17
+ }
@@ -0,0 +1,5 @@
1
+ /** The client certificate in Envoy's `x-forwarded-client-cert`, held to its one element's Hash. */
2
+ export declare function xfccOf(text: string): {
3
+ leaf: Buffer;
4
+ chain: Buffer[];
5
+ } | undefined;
@@ -0,0 +1,72 @@
1
+ import { createHash } from "node:crypto";
2
+ import { certificateBlocks } from "./pem.js";
3
+ /** The elements of an XFCC header, each its key=value fields; undefined where its grammar breaks. */
4
+ function elementsOf(text) {
5
+ const elements = [[]];
6
+ let at = 0;
7
+ for (;;) {
8
+ const end = text.slice(at).search(/[=;,"]/);
9
+ if (end === -1 || text[at + end] !== "=")
10
+ return undefined;
11
+ const key = text.slice(at, at + end).trim();
12
+ if (key === "")
13
+ return undefined;
14
+ at += end + 1;
15
+ while (text[at] === " " || text[at] === "\t")
16
+ at++;
17
+ let value = "";
18
+ if (text[at] === '"') {
19
+ for (at++; text[at] !== '"'; at++) {
20
+ if (text[at] === "\\")
21
+ at++;
22
+ if (at >= text.length)
23
+ return undefined;
24
+ value += text[at];
25
+ }
26
+ at++;
27
+ while (text[at] === " " || text[at] === "\t")
28
+ at++;
29
+ if (at < text.length && text[at] !== ";" && text[at] !== ",")
30
+ return undefined;
31
+ }
32
+ else {
33
+ const stop = text.slice(at).search(/[;,]/);
34
+ const raw = stop === -1 ? text.slice(at) : text.slice(at, at + stop);
35
+ if (raw.includes('"'))
36
+ return undefined;
37
+ value = raw.trim();
38
+ at += raw.length;
39
+ }
40
+ elements[elements.length - 1]?.push({ key, value });
41
+ if (at >= text.length)
42
+ return elements;
43
+ if (text[at] === ",")
44
+ elements.push([]);
45
+ at++;
46
+ }
47
+ }
48
+ /** Fields that count, each at most once in an element, in any case. */
49
+ const counted = new Set(["hash", "cert", "chain", "subject"]);
50
+ /** The client certificate in Envoy's `x-forwarded-client-cert`, held to its one element's Hash. */
51
+ export function xfccOf(text) {
52
+ const elements = elementsOf(text);
53
+ if (elements?.length !== 1)
54
+ return undefined;
55
+ const fields = new Map();
56
+ for (const { key, value } of elements[0] ?? []) {
57
+ const name = key.toLowerCase();
58
+ if (!counted.has(name))
59
+ continue;
60
+ if (fields.has(name))
61
+ return undefined;
62
+ fields.set(name, value);
63
+ }
64
+ const leaf = certificateBlocks(fields.get("cert") ?? "");
65
+ const chain = fields.has("chain") ? certificateBlocks(fields.get("chain") ?? "") : [];
66
+ if (leaf?.length !== 1 || !leaf[0] || !chain)
67
+ return undefined;
68
+ const sum = createHash("sha256").update(leaf[0]).digest("hex");
69
+ if (fields.get("hash")?.toLowerCase() !== sum)
70
+ return undefined;
71
+ return { leaf: leaf[0], chain };
72
+ }
@@ -0,0 +1,34 @@
1
+ # cookies
2
+
3
+ ## Responsibilities
4
+ - Builds `Set-Cookie` values for the names routes give, by one rule for this deployment.
5
+ - Reads a request's cookies by those names, by the same rule.
6
+
7
+ ## Not here
8
+ - Whether cookies can be made secure (`http`'s `site`, from the public origin).
9
+ - A request's scheme (`request-source`).
10
+ - What a cookie holds (the route's).
11
+
12
+ ## Offers
13
+ - `cookies.write`: a name, a value, a lifetime in seconds, and whether the cookie is cross-site → a `Set-Cookie` value.
14
+ - `cookies.read`: a request and a name → the cookie's value.
15
+ - `cookiesFor`: whether cookies can be made secure → cookies by the same rule, for a stand-in that builds them without composing this module.
16
+
17
+ ## Invariants
18
+ - Where cookies can be made secure, a cookie's name gets the `__Host-` prefix.
19
+ - Where cookies can be made secure, a cookie gets Secure.
20
+ - Every cookie gets Path=/ and HttpOnly.
21
+ - No cookie gets a Domain.
22
+ - A cookie that is not cross-site gets SameSite=Lax.
23
+ - A lifetime of zero seconds removes a cookie.
24
+ - A cross-site cookie gets SameSite=None.
25
+ - A cross-site cookie is only ever `__Host-`, Secure, Path=/ and without Domain, so no related site can set it.
26
+ - Where cookies cannot be made secure, writing a cross-site cookie throws.
27
+ - Writing a name that is not an HTTP token, or a value with anything but cookie-octets (RFC 6265), throws, so a write sets one cookie and no attribute of its own.
28
+ - Each write takes its attributes from the rule of the deployment writing it; nothing of a cookie is kept between writes.
29
+ - A request's cookie is read only when exactly one cookie has that name.
30
+ - Where cookies can be made secure, the cookies of a request not over HTTPS are not read.
31
+ - The cookies offered are frozen.
32
+
33
+ ## Depends on
34
+ - `kernel`, `http` (the request type, `site`), `request-source` (the scheme).