@blamejs/core 0.7.106 → 0.8.0

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 (51) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/NOTICE +17 -1
  3. package/README.md +4 -3
  4. package/index.js +16 -0
  5. package/lib/asyncapi-bindings.js +160 -0
  6. package/lib/asyncapi-traits.js +143 -0
  7. package/lib/asyncapi.js +531 -0
  8. package/lib/audit.js +6 -0
  9. package/lib/auth/acr-vocabulary.js +265 -0
  10. package/lib/auth/auth-time-tracker.js +111 -0
  11. package/lib/auth/elevation-grant.js +306 -0
  12. package/lib/auth/sd-jwt-vc-disclosure.js +95 -0
  13. package/lib/auth/sd-jwt-vc-holder.js +203 -0
  14. package/lib/auth/sd-jwt-vc-issuer.js +197 -0
  15. package/lib/auth/sd-jwt-vc.js +526 -0
  16. package/lib/auth/step-up-policy.js +335 -0
  17. package/lib/auth/step-up.js +445 -0
  18. package/lib/compliance-ai-act-logging.js +186 -0
  19. package/lib/compliance-ai-act-prohibited.js +205 -0
  20. package/lib/compliance-ai-act-risk.js +189 -0
  21. package/lib/compliance-ai-act-transparency.js +200 -0
  22. package/lib/compliance-ai-act.js +558 -0
  23. package/lib/compliance.js +2 -0
  24. package/lib/crypto.js +32 -0
  25. package/lib/flag-cache.js +136 -0
  26. package/lib/flag-evaluation-context.js +135 -0
  27. package/lib/flag-providers.js +279 -0
  28. package/lib/flag-targeting.js +210 -0
  29. package/lib/flag.js +284 -0
  30. package/lib/inbox.js +367 -0
  31. package/lib/mail-arc-sign.js +372 -0
  32. package/lib/mail-auth.js +2 -0
  33. package/lib/middleware/ai-act-disclosure.js +166 -0
  34. package/lib/middleware/asyncapi-serve.js +136 -0
  35. package/lib/middleware/flag-context.js +76 -0
  36. package/lib/middleware/index.js +15 -0
  37. package/lib/middleware/openapi-serve.js +143 -0
  38. package/lib/middleware/require-step-up.js +186 -0
  39. package/lib/openapi-paths-builder.js +248 -0
  40. package/lib/openapi-schema-walk.js +192 -0
  41. package/lib/openapi-security.js +169 -0
  42. package/lib/openapi-yaml.js +154 -0
  43. package/lib/openapi.js +443 -0
  44. package/lib/pqc-software.js +195 -0
  45. package/lib/vault/index.js +3 -0
  46. package/lib/vault-aad.js +259 -0
  47. package/lib/vendor/MANIFEST.json +29 -0
  48. package/lib/vendor/noble-post-quantum.cjs +18 -0
  49. package/lib/ws-client.js +829 -0
  50. package/package.json +1 -1
  51. package/sbom.cyclonedx.json +6 -6
@@ -0,0 +1,445 @@
1
+ "use strict";
2
+ /**
3
+ * RFC 9470 — OAuth 2.0 Step-Up Authentication Challenge.
4
+ *
5
+ * Step-up flows let a resource server demand a stronger or fresher
6
+ * authentication ceremony before serving a particular request. The
7
+ * challenge shape is fixed by RFC 9470:
8
+ *
9
+ * HTTP/1.1 401 Unauthorized
10
+ * WWW-Authenticate: Bearer error="insufficient_user_authentication",
11
+ * error_description="A higher level of authentication is required",
12
+ * acr_values="urn:mace:incommon:iap:silver",
13
+ * max_age="300"
14
+ *
15
+ * The corresponding error code, `insufficient_user_authentication`, is
16
+ * registered in the OAuth Extensions Error Registry; clients MUST
17
+ * recognise it and re-trigger the auth-flow with `acr_values` and/or
18
+ * `max_age` propagated to the IdP.
19
+ *
20
+ * Public surface (b.auth.stepUp.*):
21
+ *
22
+ * .evaluate({ claims, requirement, now? })
23
+ * → { ok: true } | { ok: false, error, requirement }
24
+ *
25
+ * .buildChallenge({ requirement, realm?, error?, errorDescription? })
26
+ * → "Bearer error=\"insufficient_user_authentication\", ..."
27
+ *
28
+ * .acr.register({ value, rank }) (delegates to acr-vocabulary)
29
+ * .acr.meets(presented, required)
30
+ *
31
+ * .grant.create({ subject, scope, acr, amr, evidence?, ttlSec? })
32
+ * → { token, expiresAt, jti }
33
+ * .grant.verify(token, { audience?, scope? })
34
+ * → claims object
35
+ *
36
+ * .parseAuthorizationDetails(value) (RFC 9396 helper)
37
+ *
38
+ * Requirement object shape:
39
+ * {
40
+ * acr: "urn:..." (optional; one acr to require)
41
+ * acrValues: [ "...", "..." ] (optional; ANY satisfies)
42
+ * maxAge: 300 (optional, seconds — RFC 9470)
43
+ * requiredAmr: [ "hwk", "pop" ] (optional; AMR must include all)
44
+ * phishingResistant: true (optional; AMR must include any
45
+ * phishing-resistant method)
46
+ * authorizationDetails: [ {...} ] (optional; RFC 9396 fine-grained)
47
+ * }
48
+ *
49
+ * Per the validation-tier policy: configuration entry-points (.buildChallenge,
50
+ * .grant.create, .acr.register) THROW on bad input — operator catches the
51
+ * typo at boot. The hot-path (.evaluate) never throws — it returns the
52
+ * structured failure so the middleware can emit a 401.
53
+ *
54
+ * Audit emissions on every state transition:
55
+ * - auth.stepUp.required (challenge emitted)
56
+ * - auth.stepUp.satisfied (request passed evaluation)
57
+ * - auth.stepUp.denied (request failed)
58
+ * - auth.stepUp.grant.issued (elevation grant minted)
59
+ * - auth.stepUp.grant.consumed (elevation grant used)
60
+ * - auth.stepUp.grant.revoked (elevation grant revoked)
61
+ */
62
+
63
+ var lazyRequire = require("../lazy-require");
64
+ var validateOpts = require("../validate-opts");
65
+ var safeJson = require("../safe-json");
66
+ var C = require("../constants");
67
+ var { AuthError } = require("../framework-error");
68
+
69
+ var acr = require("./acr-vocabulary");
70
+ var authTime = require("./auth-time-tracker");
71
+ var elevation = lazyRequire(function () { return require("./elevation-grant"); });
72
+ var audit = lazyRequire(function () { return require("../audit"); });
73
+
74
+ var INSUFFICIENT_USER_AUTHENTICATION = "insufficient_user_authentication";
75
+ var DEFAULT_REALM = "api";
76
+
77
+ function _readPresentedClaims(claims) {
78
+ return authTime.readClaims(claims);
79
+ }
80
+
81
+ // Quote a value for inclusion in a WWW-Authenticate parameter per RFC
82
+ // 7235 §2.2 and RFC 9470 §3 (uses `quoted-string` for all values).
83
+ function _quote(value) {
84
+ if (typeof value !== "string") value = String(value);
85
+ // Reject CTLs and quote-injecting characters.
86
+ for (var i = 0; i < value.length; i += 1) {
87
+ var code = value.charCodeAt(i);
88
+ if (code < 32 || code === 127) { // allow:raw-byte-literal — ASCII control codepoints
89
+ throw new AuthError("auth-stepUp/bad-challenge",
90
+ "challenge value contains control character at index " + i);
91
+ }
92
+ if (value.charAt(i) === '"' || value.charAt(i) === "\\") {
93
+ throw new AuthError("auth-stepUp/bad-challenge",
94
+ "challenge value contains illegal character " +
95
+ JSON.stringify(value.charAt(i)) + " at index " + i);
96
+ }
97
+ }
98
+ return '"' + value + '"';
99
+ }
100
+
101
+ function _validateRequirement(requirement, label) {
102
+ if (!requirement || typeof requirement !== "object") {
103
+ throw new AuthError("auth-stepUp/bad-requirement",
104
+ label + ": requirement must be an object — got " +
105
+ JSON.stringify(requirement));
106
+ }
107
+ validateOpts(requirement, [
108
+ "acr", "acrValues", "maxAge", "requiredAmr", "phishingResistant",
109
+ "authorizationDetails",
110
+ ], label);
111
+ if (requirement.acr != null) {
112
+ validateOpts.requireNonEmptyString(requirement.acr,
113
+ label + ": acr", AuthError, "auth-stepUp/bad-acr");
114
+ }
115
+ if (requirement.acrValues != null) {
116
+ if (!Array.isArray(requirement.acrValues) || requirement.acrValues.length === 0) {
117
+ throw new AuthError("auth-stepUp/bad-acr",
118
+ label + ": acrValues must be a non-empty string array");
119
+ }
120
+ for (var i = 0; i < requirement.acrValues.length; i += 1) {
121
+ validateOpts.requireNonEmptyString(requirement.acrValues[i],
122
+ label + ": acrValues[" + i + "]", AuthError, "auth-stepUp/bad-acr");
123
+ }
124
+ }
125
+ if (requirement.maxAge != null) {
126
+ if (typeof requirement.maxAge !== "number" || !isFinite(requirement.maxAge) ||
127
+ requirement.maxAge < 0) {
128
+ throw new AuthError("auth-stepUp/bad-max-age",
129
+ label + ": maxAge must be a finite number >= 0 — got " +
130
+ JSON.stringify(requirement.maxAge));
131
+ }
132
+ }
133
+ if (requirement.requiredAmr != null) {
134
+ if (!Array.isArray(requirement.requiredAmr)) {
135
+ throw new AuthError("auth-stepUp/bad-amr",
136
+ label + ": requiredAmr must be a string array");
137
+ }
138
+ for (var j = 0; j < requirement.requiredAmr.length; j += 1) {
139
+ validateOpts.requireNonEmptyString(requirement.requiredAmr[j],
140
+ label + ": requiredAmr[" + j + "]", AuthError, "auth-stepUp/bad-amr");
141
+ }
142
+ }
143
+ if (requirement.phishingResistant != null &&
144
+ typeof requirement.phishingResistant !== "boolean") {
145
+ throw new AuthError("auth-stepUp/bad-requirement",
146
+ label + ": phishingResistant must be boolean — got " +
147
+ JSON.stringify(requirement.phishingResistant));
148
+ }
149
+ }
150
+
151
+ function evaluate(opts) {
152
+ opts = opts || {};
153
+ var claims = opts.claims;
154
+ var requirement = opts.requirement;
155
+ if (!requirement || typeof requirement !== "object") {
156
+ return { ok: false, error: "no_requirement", reason: "evaluate: requirement object missing" };
157
+ }
158
+ // Hot-path drop-silent: do not throw on typo — return structured
159
+ // failure. But surface unregistered-acr because that's an operator-
160
+ // side typo that should bubble up.
161
+ try { _validateRequirement(requirement, "auth.stepUp.evaluate"); }
162
+ catch (err) { return { ok: false, error: "bad_requirement", reason: err.message }; }
163
+
164
+ var presented = _readPresentedClaims(claims);
165
+ var now = (typeof opts.now === "number") ? opts.now : Math.floor(Date.now() / C.TIME.seconds(1));
166
+
167
+ // 1. ACR check (single)
168
+ if (typeof requirement.acr === "string") {
169
+ if (!acr.isRegistered(requirement.acr)) {
170
+ return {
171
+ ok: false, error: "unknown_acr",
172
+ reason: "evaluate: required acr is not registered: " + requirement.acr,
173
+ requirement: requirement,
174
+ };
175
+ }
176
+ if (!acr.meets(presented.acr, requirement.acr)) {
177
+ return {
178
+ ok: false, error: INSUFFICIENT_USER_AUTHENTICATION,
179
+ reason: "presented acr " + JSON.stringify(presented.acr) +
180
+ " does not meet required " + JSON.stringify(requirement.acr),
181
+ requirement: requirement, presented: presented,
182
+ };
183
+ }
184
+ }
185
+ // 2. ACR-values list (any one suffices)
186
+ if (Array.isArray(requirement.acrValues) && requirement.acrValues.length > 0) {
187
+ if (!acr.meetsAny(presented.acr, requirement.acrValues)) {
188
+ return {
189
+ ok: false, error: INSUFFICIENT_USER_AUTHENTICATION,
190
+ reason: "presented acr " + JSON.stringify(presented.acr) +
191
+ " does not meet any of " + JSON.stringify(requirement.acrValues),
192
+ requirement: requirement, presented: presented,
193
+ };
194
+ }
195
+ }
196
+ // 3. max_age freshness
197
+ if (typeof requirement.maxAge === "number") {
198
+ if (!authTime.freshEnough(claims, requirement.maxAge, now)) {
199
+ return {
200
+ ok: false, error: INSUFFICIENT_USER_AUTHENTICATION,
201
+ reason: "auth_time stale or missing — required max_age=" +
202
+ requirement.maxAge + "s, age=" + authTime.ageSec(claims, now),
203
+ requirement: requirement, presented: presented,
204
+ };
205
+ }
206
+ }
207
+ // 4. AMR — required methods
208
+ if (Array.isArray(requirement.requiredAmr) && requirement.requiredAmr.length > 0) {
209
+ if (!acr.amrSatisfiesRequiredList(presented.amr, requirement.requiredAmr)) {
210
+ return {
211
+ ok: false, error: INSUFFICIENT_USER_AUTHENTICATION,
212
+ reason: "presented amr " + JSON.stringify(presented.amr) +
213
+ " does not include all required " + JSON.stringify(requirement.requiredAmr),
214
+ requirement: requirement, presented: presented,
215
+ };
216
+ }
217
+ }
218
+ // 5. AMR — phishing resistance
219
+ if (requirement.phishingResistant === true) {
220
+ if (!acr.amrIncludesPhishingResistant(presented.amr)) {
221
+ return {
222
+ ok: false, error: INSUFFICIENT_USER_AUTHENTICATION,
223
+ reason: "presented amr " + JSON.stringify(presented.amr) +
224
+ " does not include any phishing-resistant method",
225
+ requirement: requirement, presented: presented,
226
+ };
227
+ }
228
+ }
229
+ return { ok: true, presented: presented };
230
+ }
231
+
232
+ function buildChallenge(opts) {
233
+ opts = opts || {};
234
+ validateOpts(opts, [
235
+ "requirement", "realm", "error", "errorDescription", "scope",
236
+ ], "auth.stepUp.buildChallenge");
237
+ _validateRequirement(opts.requirement, "auth.stepUp.buildChallenge");
238
+ var realm = (typeof opts.realm === "string" && opts.realm.length > 0) ? opts.realm : DEFAULT_REALM;
239
+ var errCode = (typeof opts.error === "string" && opts.error.length > 0)
240
+ ? opts.error : INSUFFICIENT_USER_AUTHENTICATION;
241
+ var errDesc = (typeof opts.errorDescription === "string" && opts.errorDescription.length > 0)
242
+ ? opts.errorDescription : "A higher level of authentication is required";
243
+
244
+ var parts = [];
245
+ parts.push('realm=' + _quote(realm));
246
+ parts.push('error=' + _quote(errCode));
247
+ parts.push('error_description=' + _quote(errDesc));
248
+ if (typeof opts.scope === "string" && opts.scope.length > 0) {
249
+ parts.push('scope=' + _quote(opts.scope));
250
+ }
251
+
252
+ var req = opts.requirement;
253
+ // Per RFC 9470 §3: emit acr_values as space-separated string per RFC 6749.
254
+ if (typeof req.acr === "string" && req.acr.length > 0) {
255
+ parts.push('acr_values=' + _quote(req.acr));
256
+ } else if (Array.isArray(req.acrValues) && req.acrValues.length > 0) {
257
+ parts.push('acr_values=' + _quote(req.acrValues.join(" ")));
258
+ }
259
+ if (typeof req.maxAge === "number") {
260
+ parts.push('max_age=' + _quote(String(req.maxAge)));
261
+ }
262
+ if (Array.isArray(req.requiredAmr) && req.requiredAmr.length > 0) {
263
+ parts.push('amr_values=' + _quote(req.requiredAmr.join(" ")));
264
+ }
265
+ if (Array.isArray(req.authorizationDetails) && req.authorizationDetails.length > 0) {
266
+ parts.push('authorization_details=' + _quote(JSON.stringify(req.authorizationDetails)));
267
+ }
268
+ return "Bearer " + parts.join(", ");
269
+ }
270
+
271
+ // RFC 9396 helper — parse the JSON-array authorization_details parameter.
272
+ // Throws on malformed payload at config time (operator typo at boot).
273
+ // Hot-path callers wrap this in try/catch.
274
+ function parseAuthorizationDetails(value) {
275
+ if (typeof value !== "string") {
276
+ throw new AuthError("auth-stepUp/bad-rar",
277
+ "parseAuthorizationDetails: value must be a JSON string — got " +
278
+ typeof value);
279
+ }
280
+ var parsed;
281
+ try { parsed = safeJson.parse(value, { maxBytes: C.BYTES.kib(64) }); }
282
+ catch (e) {
283
+ throw new AuthError("auth-stepUp/bad-rar",
284
+ "parseAuthorizationDetails: invalid JSON — " + e.message);
285
+ }
286
+ if (!Array.isArray(parsed)) {
287
+ throw new AuthError("auth-stepUp/bad-rar",
288
+ "parseAuthorizationDetails: value must be a JSON array — got " +
289
+ typeof parsed);
290
+ }
291
+ for (var i = 0; i < parsed.length; i += 1) {
292
+ var entry = parsed[i];
293
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
294
+ throw new AuthError("auth-stepUp/bad-rar",
295
+ "parseAuthorizationDetails[" + i + "]: must be an object");
296
+ }
297
+ if (typeof entry.type !== "string" || entry.type.length === 0) {
298
+ throw new AuthError("auth-stepUp/bad-rar",
299
+ "parseAuthorizationDetails[" + i + "]: missing required 'type' field");
300
+ }
301
+ }
302
+ return parsed;
303
+ }
304
+
305
+ function emitAuditRequired(label, requirement, presented, req) {
306
+ try {
307
+ audit().safeEmit({
308
+ action: "auth.stepup.required",
309
+ outcome: "denied",
310
+ actor: { route: req && (req.url || req.pathname) || null,
311
+ userId: req && req.user && req.user.id || null },
312
+ metadata: {
313
+ label: label || "stepUp",
314
+ requirement: _summarizeRequirement(requirement),
315
+ presented: _summarizePresented(presented),
316
+ },
317
+ });
318
+ } catch (_e) { /* drop-silent */ }
319
+ }
320
+
321
+ function emitAuditSatisfied(label, requirement, presented, req) {
322
+ try {
323
+ audit().safeEmit({
324
+ action: "auth.stepup.satisfied",
325
+ outcome: "success",
326
+ actor: { route: req && (req.url || req.pathname) || null,
327
+ userId: req && req.user && req.user.id || null },
328
+ metadata: {
329
+ label: label || "stepUp",
330
+ requirement: _summarizeRequirement(requirement),
331
+ presented: _summarizePresented(presented),
332
+ },
333
+ });
334
+ } catch (_e) { /* drop-silent */ }
335
+ }
336
+
337
+ function _summarizeRequirement(req) {
338
+ if (!req || typeof req !== "object") return null;
339
+ return {
340
+ acr: req.acr || null,
341
+ acrValues: Array.isArray(req.acrValues) ? req.acrValues.slice() : null,
342
+ maxAge: (typeof req.maxAge === "number") ? req.maxAge : null,
343
+ requiredAmr: Array.isArray(req.requiredAmr) ? req.requiredAmr.slice() : null,
344
+ phishingResistant: req.phishingResistant === true ? true : false,
345
+ };
346
+ }
347
+
348
+ function _summarizePresented(presented) {
349
+ if (!presented || typeof presented !== "object") return null;
350
+ return {
351
+ acr: presented.acr || null,
352
+ amr: Array.isArray(presented.amr) ? presented.amr.slice() : null,
353
+ auth_time: presented.auth_time || null,
354
+ };
355
+ }
356
+
357
+ // ---- Bearer-challenge parser (RFC 7235 §2.1, RFC 9470 §3) ----
358
+ //
359
+ // Operator-side helper to inspect what an upstream RS challenged with.
360
+ // Returns null when the header doesn't carry a Bearer challenge or
361
+ // doesn't carry the insufficient_user_authentication error.
362
+
363
+ function parseChallenge(headerValue) {
364
+ if (typeof headerValue !== "string") return null;
365
+ // Tolerate "Bearer " prefix in any case; reject anything else.
366
+ var idx = headerValue.toLowerCase().indexOf("bearer");
367
+ if (idx === -1) return null;
368
+ var rest = headerValue.slice(idx + "bearer".length).trim();
369
+ if (rest.length === 0) return null;
370
+ var out = { error: null, scope: null, acrValues: null, maxAge: null, raw: {} };
371
+ // Split on commas at top level, but respect quoted strings.
372
+ var tokens = _splitWwwAuth(rest);
373
+ for (var i = 0; i < tokens.length; i += 1) {
374
+ var token = tokens[i].trim();
375
+ var eq = token.indexOf("=");
376
+ if (eq === -1) continue;
377
+ var key = token.slice(0, eq).trim().toLowerCase();
378
+ var val = token.slice(eq + 1).trim();
379
+ if (val.length >= 2 && val.charAt(0) === '"' && val.charAt(val.length - 1) === '"') {
380
+ val = val.slice(1, val.length - 1);
381
+ }
382
+ out.raw[key] = val;
383
+ if (key === "error") out.error = val;
384
+ else if (key === "scope") out.scope = val;
385
+ else if (key === "acr_values") out.acrValues = val.split(/\s+/);
386
+ else if (key === "max_age") out.maxAge = parseInt(val, 10);
387
+ }
388
+ return out;
389
+ }
390
+
391
+ function _splitWwwAuth(raw) {
392
+ var tokens = [];
393
+ var cursor = 0;
394
+ var inQuoted = false;
395
+ var current = "";
396
+ while (cursor < raw.length) {
397
+ var ch = raw.charAt(cursor);
398
+ if (inQuoted) {
399
+ current += ch;
400
+ if (ch === "\\" && cursor + 1 < raw.length) {
401
+ current += raw.charAt(cursor + 1);
402
+ cursor += 2;
403
+ continue;
404
+ }
405
+ if (ch === '"') inQuoted = false;
406
+ cursor += 1;
407
+ continue;
408
+ }
409
+ if (ch === '"') { inQuoted = true; current += ch; cursor += 1; continue; }
410
+ if (ch === ",") {
411
+ tokens.push(current);
412
+ current = "";
413
+ cursor += 1;
414
+ continue;
415
+ }
416
+ current += ch;
417
+ cursor += 1;
418
+ }
419
+ if (current.length > 0) tokens.push(current);
420
+ return tokens;
421
+ }
422
+
423
+ var policy = lazyRequire(function () { return require("./step-up-policy"); });
424
+
425
+ module.exports = {
426
+ evaluate: evaluate,
427
+ buildChallenge: buildChallenge,
428
+ parseChallenge: parseChallenge,
429
+ parseAuthorizationDetails: parseAuthorizationDetails,
430
+ acr: acr,
431
+ authTime: authTime,
432
+ get policy() { return policy(); },
433
+ grant: {
434
+ create: function (opts) { return elevation().create(opts); },
435
+ verify: function (token, opts) { return elevation().verify(token, opts); },
436
+ revoke: function (jti, opts) { return elevation().revoke(jti, opts); },
437
+ isRevoked: function (jti) { return elevation().isRevoked(jti); },
438
+ list: function () { return elevation().list(); },
439
+ setSigningKey: function (key) { return elevation().setSigningKey(key); },
440
+ _resetForTests: function () { return elevation()._resetForTests(); },
441
+ },
442
+ emitAuditRequired: emitAuditRequired,
443
+ emitAuditSatisfied: emitAuditSatisfied,
444
+ INSUFFICIENT_USER_AUTHENTICATION: INSUFFICIENT_USER_AUTHENTICATION,
445
+ };
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ /**
3
+ * EU AI Act Article 12 — automatic logging requirements for high-risk
4
+ * AI systems.
5
+ *
6
+ * Per Regulation (EU) 2024/1689 Art. 12, providers of high-risk AI
7
+ * systems MUST design the system to automatically record events
8
+ * ("logs") over its lifetime, sufficient to:
9
+ *
10
+ * (a) identify situations that may result in the AI system
11
+ * presenting a risk under Art. 79(1) (post-market monitoring);
12
+ * (b) facilitate post-market monitoring per Art. 72;
13
+ * (c) monitor the operation of the high-risk AI systems referred
14
+ * to in Art. 26(5) (deployer obligations).
15
+ *
16
+ * For Annex III §1(a) (remote biometric identification systems), the
17
+ * minimum required logged fields are explicitly enumerated in
18
+ * Art. 12(3):
19
+ *
20
+ * - period of each use (start time, end time)
21
+ * - reference database against which input data was checked
22
+ * - input data for which the search led to a match
23
+ * - identification of natural persons involved in result verification
24
+ *
25
+ * The framework provides a typed event-builder that produces records
26
+ * conforming to these requirements, plus a serialiser that funnels the
27
+ * records into the framework's audit-chain (b.audit) so they ride the
28
+ * tamper-evident PQC-signed chain.
29
+ *
30
+ * Logs are operator-retained per Art. 19 (provider must keep them at
31
+ * least 6 months unless local law requires longer; for high-risk
32
+ * systems used in financial services + employment + law enforcement
33
+ * the retention floor is 1 year). The retention floor cross-walks
34
+ * into b.retention.complianceFloor.
35
+ */
36
+
37
+ var validateOpts = require("./validate-opts");
38
+ var lazyRequire = require("./lazy-require");
39
+ var C = require("./constants");
40
+ var { ComplianceError } = require("./framework-error");
41
+
42
+ var audit = lazyRequire(function () { return require("./audit"); });
43
+
44
+ // Retention floors per Art. 19. Operator's b.retention.complianceFloor
45
+ // applies the more-stringent of: AI Act floor, sectoral law, internal
46
+ // retention policy.
47
+ var RETENTION_FLOORS = Object.freeze({
48
+ default: C.TIME.days(180),
49
+ "high-risk-financial": C.TIME.days(365),
50
+ "high-risk-employment": C.TIME.days(365),
51
+ "high-risk-law-enforcement": C.TIME.days(365),
52
+ });
53
+
54
+ var MIN_BIOMETRIC_FIELDS = Object.freeze([
55
+ "periodStart", "periodEnd", "referenceDatabase",
56
+ "matchedInputRef", "verifiers",
57
+ ]);
58
+
59
+ function buildEvent(opts) {
60
+ opts = opts || {};
61
+ validateOpts(opts, [
62
+ "systemId", "kind", "actor", "timestamp",
63
+ "periodStart", "periodEnd", "referenceDatabase",
64
+ "matchedInputRef", "verifiers",
65
+ "outcome", "metadata", "annexIII",
66
+ ], "compliance.aiAct.logging.buildEvent");
67
+
68
+ validateOpts.requireNonEmptyString(opts.systemId,
69
+ "buildEvent: systemId", ComplianceError, "compliance-ai-act/bad-event");
70
+ validateOpts.requireNonEmptyString(opts.kind,
71
+ "buildEvent: kind", ComplianceError, "compliance-ai-act/bad-event");
72
+
73
+ var nowMs = (typeof opts.timestamp === "number" && isFinite(opts.timestamp))
74
+ ? opts.timestamp : Date.now();
75
+
76
+ var record = {
77
+ aiActArticle: "Art. 12",
78
+ systemId: opts.systemId,
79
+ kind: opts.kind,
80
+ timestamp: new Date(nowMs).toISOString(),
81
+ actor: opts.actor || null,
82
+ annexIII: opts.annexIII || null,
83
+ outcome: opts.outcome || "ok",
84
+ };
85
+
86
+ // Annex III §1(a) biometric-id systems require specific fields.
87
+ if (opts.annexIII === "biometric-id-categorisation") {
88
+ var missing = [];
89
+ for (var i = 0; i < MIN_BIOMETRIC_FIELDS.length; i += 1) {
90
+ var field = MIN_BIOMETRIC_FIELDS[i];
91
+ if (opts[field] == null) missing.push(field);
92
+ }
93
+ if (missing.length > 0) {
94
+ throw new ComplianceError("compliance-ai-act/missing-biometric-fields",
95
+ "buildEvent: biometric-id event missing required fields per Art. 12(3): " +
96
+ missing.join(", "));
97
+ }
98
+ record.periodStart = _toIsoString(opts.periodStart);
99
+ record.periodEnd = _toIsoString(opts.periodEnd);
100
+ record.referenceDatabase = opts.referenceDatabase;
101
+ record.matchedInputRef = opts.matchedInputRef;
102
+ record.verifiers = Array.isArray(opts.verifiers)
103
+ ? opts.verifiers.slice() : [opts.verifiers];
104
+ }
105
+
106
+ if (opts.metadata && typeof opts.metadata === "object") {
107
+ record.metadata = opts.metadata;
108
+ }
109
+ return record;
110
+ }
111
+
112
+ function _toIsoString(value) {
113
+ if (value == null) return null;
114
+ if (typeof value === "string") return value;
115
+ if (typeof value === "number" && isFinite(value)) {
116
+ return new Date(value).toISOString();
117
+ }
118
+ if (value instanceof Date) return value.toISOString();
119
+ return null;
120
+ }
121
+
122
+ function emit(event) {
123
+ if (!event || typeof event !== "object") {
124
+ throw new ComplianceError("compliance-ai-act/bad-event",
125
+ "compliance.aiAct.logging.emit: event must be an object");
126
+ }
127
+ // Funnel into the framework audit chain so the record rides the
128
+ // tamper-evident PQC-signed chain.
129
+ try {
130
+ audit().safeEmit({
131
+ action: "compliance.aiact." + (event.kind || "log"),
132
+ outcome: event.outcome === "ok" ? "success" : (event.outcome || "success"),
133
+ actor: event.actor || null,
134
+ metadata: event,
135
+ });
136
+ } catch (_e) { /* drop-silent */ }
137
+ return event;
138
+ }
139
+
140
+ function logEvent(opts) {
141
+ var record = buildEvent(opts);
142
+ return emit(record);
143
+ }
144
+
145
+ function retentionFloorMs(opts) {
146
+ opts = opts || {};
147
+ validateOpts(opts, ["domain"], "compliance.aiAct.logging.retentionFloorMs");
148
+ var key = opts.domain || "default";
149
+ if (Object.prototype.hasOwnProperty.call(RETENTION_FLOORS, key)) {
150
+ return RETENTION_FLOORS[key];
151
+ }
152
+ return RETENTION_FLOORS.default;
153
+ }
154
+
155
+ // Build a request-attached logger pre-bound to a system context. The
156
+ // returned function accepts a partial event and merges it with the
157
+ // preset (systemId, annexIII, deployer).
158
+ function loggerFor(systemContext) {
159
+ if (!systemContext || typeof systemContext !== "object") {
160
+ throw new ComplianceError("compliance-ai-act/bad-system-context",
161
+ "loggerFor: systemContext must be an object");
162
+ }
163
+ validateOpts.requireNonEmptyString(systemContext.systemId,
164
+ "loggerFor: systemContext.systemId", ComplianceError, "compliance-ai-act/bad-system-context");
165
+ return function (eventPartial) {
166
+ var merged = Object.assign({}, eventPartial || {});
167
+ merged.systemId = systemContext.systemId;
168
+ if (systemContext.annexIII && !merged.annexIII) {
169
+ merged.annexIII = systemContext.annexIII;
170
+ }
171
+ if (systemContext.deployer && !merged.actor) {
172
+ merged.actor = { deployer: systemContext.deployer };
173
+ }
174
+ return logEvent(merged);
175
+ };
176
+ }
177
+
178
+ module.exports = {
179
+ buildEvent: buildEvent,
180
+ emit: emit,
181
+ logEvent: logEvent,
182
+ retentionFloorMs: retentionFloorMs,
183
+ loggerFor: loggerFor,
184
+ RETENTION_FLOORS: RETENTION_FLOORS,
185
+ MIN_BIOMETRIC_FIELDS: MIN_BIOMETRIC_FIELDS,
186
+ };