@blamejs/core 0.7.107 → 0.8.4

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 (100) hide show
  1. package/CHANGELOG.md +41 -1
  2. package/NOTICE +17 -1
  3. package/README.md +4 -3
  4. package/index.js +15 -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-sign.js +1 -1
  9. package/lib/audit.js +68 -2
  10. package/lib/auth/acr-vocabulary.js +265 -0
  11. package/lib/auth/auth-time-tracker.js +111 -0
  12. package/lib/auth/elevation-grant.js +306 -0
  13. package/lib/auth/jwt.js +13 -0
  14. package/lib/auth/lockout.js +16 -3
  15. package/lib/auth/oauth.js +15 -1
  16. package/lib/auth/password.js +22 -2
  17. package/lib/auth/sd-jwt-vc-issuer.js +2 -2
  18. package/lib/auth/sd-jwt-vc.js +7 -2
  19. package/lib/auth/step-up-policy.js +335 -0
  20. package/lib/auth/step-up.js +445 -0
  21. package/lib/break-glass.js +53 -14
  22. package/lib/cache-redis.js +1 -1
  23. package/lib/cache.js +6 -1
  24. package/lib/cli.js +3 -3
  25. package/lib/cluster.js +24 -1
  26. package/lib/compliance-ai-act-logging.js +190 -0
  27. package/lib/compliance-ai-act-prohibited.js +205 -0
  28. package/lib/compliance-ai-act-risk.js +189 -0
  29. package/lib/compliance-ai-act-transparency.js +200 -0
  30. package/lib/compliance-ai-act.js +558 -0
  31. package/lib/compliance.js +12 -2
  32. package/lib/config-drift.js +2 -2
  33. package/lib/crypto-field.js +21 -1
  34. package/lib/crypto.js +114 -1
  35. package/lib/db.js +35 -4
  36. package/lib/dev.js +30 -3
  37. package/lib/dual-control.js +19 -1
  38. package/lib/external-db.js +10 -0
  39. package/lib/file-upload.js +30 -3
  40. package/lib/flag-cache.js +136 -0
  41. package/lib/flag-evaluation-context.js +135 -0
  42. package/lib/flag-providers.js +279 -0
  43. package/lib/flag-targeting.js +210 -0
  44. package/lib/flag.js +284 -0
  45. package/lib/guard-all.js +33 -16
  46. package/lib/guard-csv.js +16 -2
  47. package/lib/guard-html.js +35 -0
  48. package/lib/guard-svg.js +20 -0
  49. package/lib/http-client.js +57 -11
  50. package/lib/inbox.js +391 -0
  51. package/lib/log-stream-syslog.js +8 -0
  52. package/lib/log-stream.js +1 -1
  53. package/lib/mail-arc-sign.js +372 -0
  54. package/lib/mail-auth.js +2 -0
  55. package/lib/mail.js +40 -0
  56. package/lib/middleware/ai-act-disclosure.js +166 -0
  57. package/lib/middleware/asyncapi-serve.js +136 -0
  58. package/lib/middleware/attach-user.js +25 -2
  59. package/lib/middleware/bearer-auth.js +71 -6
  60. package/lib/middleware/body-parser.js +13 -0
  61. package/lib/middleware/cors.js +10 -0
  62. package/lib/middleware/csrf-protect.js +34 -3
  63. package/lib/middleware/dpop.js +3 -3
  64. package/lib/middleware/flag-context.js +76 -0
  65. package/lib/middleware/host-allowlist.js +1 -1
  66. package/lib/middleware/index.js +15 -0
  67. package/lib/middleware/openapi-serve.js +143 -0
  68. package/lib/middleware/require-aal.js +2 -2
  69. package/lib/middleware/require-step-up.js +186 -0
  70. package/lib/middleware/trace-propagate.js +1 -1
  71. package/lib/mtls-ca.js +23 -29
  72. package/lib/mtls-engine-default.js +21 -1
  73. package/lib/network-tls.js +21 -6
  74. package/lib/object-store/sigv4-bucket-ops.js +41 -0
  75. package/lib/observability-otlp-exporter.js +35 -2
  76. package/lib/openapi-paths-builder.js +248 -0
  77. package/lib/openapi-schema-walk.js +192 -0
  78. package/lib/openapi-security.js +169 -0
  79. package/lib/openapi-yaml.js +154 -0
  80. package/lib/openapi.js +443 -0
  81. package/lib/outbox.js +3 -3
  82. package/lib/permissions.js +10 -1
  83. package/lib/pqc-agent.js +22 -1
  84. package/lib/pqc-software.js +195 -0
  85. package/lib/pubsub.js +8 -4
  86. package/lib/redact.js +26 -1
  87. package/lib/retention.js +26 -0
  88. package/lib/router.js +1 -0
  89. package/lib/scheduler.js +57 -1
  90. package/lib/session.js +3 -3
  91. package/lib/ssrf-guard.js +19 -4
  92. package/lib/static.js +12 -0
  93. package/lib/totp.js +16 -0
  94. package/lib/vault/index.js +3 -0
  95. package/lib/vault-aad.js +259 -0
  96. package/lib/vendor/MANIFEST.json +29 -0
  97. package/lib/vendor/noble-post-quantum.cjs +18 -0
  98. package/lib/ws-client.js +978 -0
  99. package/package.json +1 -1
  100. package/sbom.cyclonedx.json +6 -6
@@ -0,0 +1,531 @@
1
+ "use strict";
2
+ /**
3
+ * b.asyncapi — AsyncAPI 3.0 schema-document builder.
4
+ *
5
+ * AsyncAPI is the event-driven sibling to OpenAPI. Operators describe
6
+ * pubsub / websocket / kafka / mqtt surfaces as a single document the
7
+ * framework can serve at /asyncapi.json (or /asyncapi.yaml) for
8
+ * downstream tooling.
9
+ *
10
+ * var aapi = b.asyncapi.create({
11
+ * info: { title: "Acme Events API", version: "1.0.0" },
12
+ * servers: {
13
+ * production: { host: "broker.acme.example.com:9092",
14
+ * protocol: "kafka",
15
+ * description: "Kafka broker" },
16
+ * },
17
+ * });
18
+ *
19
+ * aapi.channel("orders.created", {
20
+ * address: "orders.created",
21
+ * messages: {
22
+ * OrderCreated: {
23
+ * payload: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
24
+ * contentType: "application/json",
25
+ * },
26
+ * },
27
+ * bindings: { kafka: b.asyncapi.bindings.kafka({ topic: "orders.created", partitions: 4 }) },
28
+ * });
29
+ *
30
+ * aapi.operation("publishOrderCreated", {
31
+ * action: "send",
32
+ * channel: "orders.created",
33
+ * summary: "Publish an order-created event",
34
+ * });
35
+ *
36
+ * var doc = aapi.toJson(); // AsyncAPI 3.0 JSON document
37
+ * var yaml = aapi.toYaml(); // YAML serialisation
38
+ *
39
+ * The builder is FRAMEWORK-FACING: it produces a valid AsyncAPI 3.0
40
+ * document, but the operator's hand-written contract is the source of
41
+ * truth — it does NOT auto-walk b.pubsub topics or b.websocketChannels
42
+ * subscriptions (operators frequently want a smaller / different
43
+ * surface published than what is in-process).
44
+ *
45
+ * Public surface (b.asyncapi.*):
46
+ *
47
+ * .create({ info, servers, defaultContentType, security, externalDocs })
48
+ * -> builder
49
+ *
50
+ * builder.channel(channelId, opts) // register channel
51
+ * builder.operation(operationId, opts) // register operation
52
+ * builder.schema(name, schemaSpec) // reusable schema
53
+ * builder.message(name, messageSpec) // reusable message
54
+ * builder.security.add(name, scheme)
55
+ * builder.tag({ name, description })
56
+ * builder.toJson() / toJsonString() / toYaml()
57
+ *
58
+ * b.asyncapi.bindings.{websockets, kafka, amqp, mqtt, http}
59
+ * -> typed binding builders.
60
+ */
61
+
62
+ var validateOpts = require("./validate-opts");
63
+ var lazyRequire = require("./lazy-require");
64
+ var schemaWalk = require("./openapi-schema-walk");
65
+ var openapiSecurity = require("./openapi-security");
66
+ var openapiYaml = require("./openapi-yaml");
67
+ var bindingsMod = require("./asyncapi-bindings");
68
+ var traitsMod = require("./asyncapi-traits");
69
+ var { defineClass } = require("./framework-error");
70
+ var AsyncApiError = defineClass("AsyncApiError", { alwaysPermanent: true });
71
+
72
+ var audit = lazyRequire(function () { return require("./audit"); });
73
+
74
+ var ASYNCAPI_VERSION = "3.0.0";
75
+
76
+ var VALID_OPERATION_ACTIONS = ["send", "receive"];
77
+
78
+ function create(opts) {
79
+ opts = opts || {};
80
+ validateOpts(opts, [
81
+ "info", "servers", "defaultContentType", "security",
82
+ "externalDocs", "tags", "id",
83
+ ], "asyncapi.create");
84
+ if (!opts.info || typeof opts.info !== "object") {
85
+ throw new AsyncApiError("asyncapi/bad-info",
86
+ "create: info object is required (title + version)");
87
+ }
88
+ validateOpts.requireNonEmptyString(opts.info.title,
89
+ "create: info.title", AsyncApiError, "asyncapi/bad-info");
90
+ validateOpts.requireNonEmptyString(opts.info.version,
91
+ "create: info.version", AsyncApiError, "asyncapi/bad-info");
92
+
93
+ var info = Object.assign({}, opts.info);
94
+ var servers = _validateServers(opts.servers);
95
+ var channels = {};
96
+ var operations = {};
97
+ var components = {
98
+ schemas: {},
99
+ messages: {},
100
+ parameters: {},
101
+ securitySchemes: {},
102
+ serverVariables: {},
103
+ correlationIds: {},
104
+ operationTraits: {},
105
+ messageTraits: {},
106
+ replies: {},
107
+ replyAddresses: {},
108
+ };
109
+ var docTags = Array.isArray(opts.tags) ? opts.tags.slice() : [];
110
+ var docSecurity = Array.isArray(opts.security) ? opts.security.slice() : [];
111
+ var defaultContentType = (typeof opts.defaultContentType === "string" && opts.defaultContentType.length > 0)
112
+ ? opts.defaultContentType : "application/json";
113
+ var externalDocs = opts.externalDocs || null;
114
+ var docId = opts.id || null;
115
+
116
+ function _addChannel(channelId, channelOpts) {
117
+ validateOpts.requireNonEmptyString(channelId, "channel: channelId",
118
+ AsyncApiError, "asyncapi/bad-channel");
119
+ if (Object.prototype.hasOwnProperty.call(channels, channelId)) {
120
+ throw new AsyncApiError("asyncapi/duplicate-channel",
121
+ "channel: " + channelId + " already registered");
122
+ }
123
+ channelOpts = channelOpts || {};
124
+ validateOpts(channelOpts, [
125
+ "address", "messages", "title", "summary", "description",
126
+ "servers", "parameters", "tags", "bindings", "externalDocs",
127
+ ], "channel");
128
+ var ch = {};
129
+ if (channelOpts.address) ch.address = channelOpts.address;
130
+ if (channelOpts.title) ch.title = channelOpts.title;
131
+ if (channelOpts.summary) ch.summary = channelOpts.summary;
132
+ if (channelOpts.description) ch.description = channelOpts.description;
133
+ if (Array.isArray(channelOpts.servers)) ch.servers = channelOpts.servers.slice();
134
+ if (channelOpts.bindings) ch.bindings = channelOpts.bindings;
135
+ if (channelOpts.externalDocs) ch.externalDocs = channelOpts.externalDocs;
136
+ if (Array.isArray(channelOpts.tags)) ch.tags = channelOpts.tags.slice();
137
+ if (channelOpts.parameters && typeof channelOpts.parameters === "object") {
138
+ ch.parameters = {};
139
+ for (var pn in channelOpts.parameters) {
140
+ if (!Object.prototype.hasOwnProperty.call(channelOpts.parameters, pn)) continue;
141
+ ch.parameters[pn] = channelOpts.parameters[pn];
142
+ }
143
+ }
144
+ if (channelOpts.messages && typeof channelOpts.messages === "object") {
145
+ ch.messages = {};
146
+ for (var mn in channelOpts.messages) {
147
+ if (!Object.prototype.hasOwnProperty.call(channelOpts.messages, mn)) continue;
148
+ ch.messages[mn] = _normaliseMessage(channelOpts.messages[mn],
149
+ "channel " + channelId + ".messages." + mn);
150
+ }
151
+ }
152
+ channels[channelId] = ch;
153
+ return ch;
154
+ }
155
+
156
+ function _addOperation(operationId, opOpts) {
157
+ validateOpts.requireNonEmptyString(operationId, "operation: operationId",
158
+ AsyncApiError, "asyncapi/bad-operation");
159
+ if (Object.prototype.hasOwnProperty.call(operations, operationId)) {
160
+ throw new AsyncApiError("asyncapi/duplicate-operation",
161
+ "operation: " + operationId + " already registered");
162
+ }
163
+ opOpts = opOpts || {};
164
+ validateOpts(opOpts, [
165
+ "action", "channel", "messages", "summary", "description",
166
+ "tags", "bindings", "security", "externalDocs", "reply",
167
+ ], "operation");
168
+ if (VALID_OPERATION_ACTIONS.indexOf(opOpts.action) === -1) {
169
+ throw new AsyncApiError("asyncapi/bad-operation",
170
+ "operation: action must be 'send' or 'receive' - got " +
171
+ JSON.stringify(opOpts.action));
172
+ }
173
+ validateOpts.requireNonEmptyString(opOpts.channel, "operation: channel",
174
+ AsyncApiError, "asyncapi/bad-operation");
175
+ if (!Object.prototype.hasOwnProperty.call(channels, opOpts.channel)) {
176
+ throw new AsyncApiError("asyncapi/dangling-channel",
177
+ "operation " + operationId + ": channel " + JSON.stringify(opOpts.channel) +
178
+ " is not registered (declare it via builder.channel() first)");
179
+ }
180
+ var op = {
181
+ action: opOpts.action,
182
+ channel: { "$ref": "#/channels/" + opOpts.channel },
183
+ };
184
+ if (opOpts.summary) op.summary = opOpts.summary;
185
+ if (opOpts.description) op.description = opOpts.description;
186
+ if (Array.isArray(opOpts.tags)) op.tags = opOpts.tags.slice();
187
+ if (opOpts.bindings) op.bindings = opOpts.bindings;
188
+ if (Array.isArray(opOpts.security)) op.security = opOpts.security.slice();
189
+ if (opOpts.externalDocs) op.externalDocs = opOpts.externalDocs;
190
+ if (opOpts.reply) op.reply = opOpts.reply;
191
+ if (Array.isArray(opOpts.messages) && opOpts.messages.length > 0) {
192
+ op.messages = opOpts.messages.map(function (m, idx) {
193
+ if (typeof m === "string") {
194
+ return { "$ref": "#/channels/" + opOpts.channel + "/messages/" + m };
195
+ }
196
+ if (m && typeof m === "object" && typeof m["$ref"] === "string") {
197
+ return { "$ref": m["$ref"] };
198
+ }
199
+ throw new AsyncApiError("asyncapi/bad-operation",
200
+ "operation " + operationId + ".messages[" + idx +
201
+ "]: must be a message name string or an object with $ref");
202
+ });
203
+ }
204
+ operations[operationId] = op;
205
+ return op;
206
+ }
207
+
208
+ function _normaliseMessage(input, label) {
209
+ if (!input || typeof input !== "object") {
210
+ throw new AsyncApiError("asyncapi/bad-message",
211
+ label + ": message must be an object");
212
+ }
213
+ var m = {};
214
+ if (input.name) m.name = input.name;
215
+ if (input.title) m.title = input.title;
216
+ if (input.summary) m.summary = input.summary;
217
+ if (input.description) m.description = input.description;
218
+ if (input.contentType) m.contentType = input.contentType;
219
+ if (input.headers != null) m.headers = schemaWalk.walk(input.headers);
220
+ if (input.payload != null) m.payload = schemaWalk.walk(input.payload);
221
+ if (input.correlationId) m.correlationId = input.correlationId;
222
+ if (input.bindings) m.bindings = input.bindings;
223
+ if (input.tags) m.tags = input.tags;
224
+ if (Array.isArray(input.examples)) m.examples = input.examples.slice();
225
+ if (input.traits) m.traits = input.traits;
226
+ if (input.externalDocs) m.externalDocs = input.externalDocs;
227
+ return m;
228
+ }
229
+
230
+ return {
231
+ info: info,
232
+ asyncapi: ASYNCAPI_VERSION,
233
+
234
+ channel: function (id, channelOpts) { _addChannel(id, channelOpts); return this; },
235
+ operation: function (id, opOpts) { _addOperation(id, opOpts); return this; },
236
+
237
+ schema: function (name, schemaSpec) {
238
+ validateOpts.requireNonEmptyString(name, "schema: name",
239
+ AsyncApiError, "asyncapi/bad-component");
240
+ if (Object.prototype.hasOwnProperty.call(components.schemas, name)) {
241
+ throw new AsyncApiError("asyncapi/duplicate-component",
242
+ "schema: components.schemas." + name + " already registered");
243
+ }
244
+ components.schemas[name] = schemaWalk.walk(schemaSpec);
245
+ return this;
246
+ },
247
+
248
+ message: function (name, messageSpec) {
249
+ validateOpts.requireNonEmptyString(name, "message: name",
250
+ AsyncApiError, "asyncapi/bad-component");
251
+ components.messages[name] = _normaliseMessage(messageSpec, "message: " + name);
252
+ return this;
253
+ },
254
+
255
+ parameter: function (name, paramSpec) {
256
+ validateOpts.requireNonEmptyString(name, "parameter: name",
257
+ AsyncApiError, "asyncapi/bad-component");
258
+ components.parameters[name] = paramSpec;
259
+ return this;
260
+ },
261
+
262
+ correlationId: function (name, idSpec) {
263
+ validateOpts.requireNonEmptyString(name, "correlationId: name",
264
+ AsyncApiError, "asyncapi/bad-component");
265
+ if (!idSpec || typeof idSpec !== "object" || typeof idSpec.location !== "string") {
266
+ throw new AsyncApiError("asyncapi/bad-correlation-id",
267
+ "correlationId: spec must be { location: 'runtime expression', description? }");
268
+ }
269
+ components.correlationIds[name] = idSpec;
270
+ return this;
271
+ },
272
+
273
+ security: {
274
+ add: function (name, scheme) {
275
+ validateOpts.requireNonEmptyString(name, "security.add: name",
276
+ AsyncApiError, "asyncapi/bad-security");
277
+ if (!scheme || typeof scheme !== "object" || typeof scheme.type !== "string") {
278
+ throw new AsyncApiError("asyncapi/bad-security",
279
+ "security.add: scheme must be a securityScheme object with a type");
280
+ }
281
+ components.securitySchemes[name] = scheme;
282
+ return this;
283
+ },
284
+ require: function (requirement) {
285
+ if (!requirement || typeof requirement !== "object") {
286
+ throw new AsyncApiError("asyncapi/bad-security",
287
+ "security.require: requirement must be an object like { schemeName: ['scope'] }");
288
+ }
289
+ docSecurity.push(requirement);
290
+ return this;
291
+ },
292
+ },
293
+
294
+ tag: function (tagSpec) {
295
+ if (!tagSpec || typeof tagSpec !== "object" ||
296
+ typeof tagSpec.name !== "string" || tagSpec.name.length === 0) {
297
+ throw new AsyncApiError("asyncapi/bad-tag",
298
+ "tag: tagSpec.name is required");
299
+ }
300
+ docTags.push(tagSpec);
301
+ return this;
302
+ },
303
+
304
+ server: function (serverId, serverSpec) {
305
+ validateOpts.requireNonEmptyString(serverId, "server: serverId",
306
+ AsyncApiError, "asyncapi/bad-server");
307
+ _validateServerEntry(serverSpec, "server " + serverId);
308
+ servers[serverId] = Object.assign({}, serverSpec);
309
+ return this;
310
+ },
311
+
312
+ toJson: function () {
313
+ var doc = {
314
+ asyncapi: ASYNCAPI_VERSION,
315
+ info: info,
316
+ };
317
+ if (docId) doc.id = docId;
318
+ if (defaultContentType) doc.defaultContentType = defaultContentType;
319
+ if (Object.keys(servers).length > 0) doc.servers = servers;
320
+
321
+ doc.channels = channels;
322
+ doc.operations = operations;
323
+
324
+ var anyComponent = false;
325
+ var componentsOut = {};
326
+ var keys = ["schemas", "messages", "parameters", "securitySchemes",
327
+ "serverVariables", "correlationIds", "operationTraits",
328
+ "messageTraits", "replies", "replyAddresses"];
329
+ for (var k = 0; k < keys.length; k += 1) {
330
+ var key = keys[k];
331
+ if (Object.keys(components[key]).length > 0) {
332
+ componentsOut[key] = components[key];
333
+ anyComponent = true;
334
+ }
335
+ }
336
+ if (anyComponent) doc.components = componentsOut;
337
+
338
+ // Validate doc-level security references.
339
+ for (var r = 0; r < docSecurity.length; r += 1) {
340
+ for (var schemeName in docSecurity[r]) {
341
+ if (!Object.prototype.hasOwnProperty.call(docSecurity[r], schemeName)) continue;
342
+ if (!components.securitySchemes[schemeName]) {
343
+ throw new AsyncApiError("asyncapi/dangling-security",
344
+ "toJson: doc-level security references undefined scheme " +
345
+ JSON.stringify(schemeName));
346
+ }
347
+ }
348
+ }
349
+ // Operation-level security
350
+ for (var opId in operations) {
351
+ if (!Object.prototype.hasOwnProperty.call(operations, opId)) continue;
352
+ var opSec = operations[opId].security;
353
+ if (Array.isArray(opSec)) {
354
+ for (var os = 0; os < opSec.length; os += 1) {
355
+ for (var sn in opSec[os]) {
356
+ if (!Object.prototype.hasOwnProperty.call(opSec[os], sn)) continue;
357
+ if (!components.securitySchemes[sn]) {
358
+ throw new AsyncApiError("asyncapi/dangling-security",
359
+ "toJson: operation " + opId + " references undefined security scheme " +
360
+ JSON.stringify(sn));
361
+ }
362
+ }
363
+ }
364
+ }
365
+ }
366
+
367
+ if (docSecurity.length > 0) doc.security = docSecurity.slice();
368
+ if (docTags.length > 0) doc.tags = docTags.slice();
369
+ if (externalDocs) doc.externalDocs = externalDocs;
370
+
371
+ try {
372
+ audit().safeEmit({
373
+ action: "asyncapi.document.built",
374
+ outcome: "success",
375
+ actor: null,
376
+ metadata: {
377
+ title: info.title,
378
+ version: info.version,
379
+ channelCount: Object.keys(channels).length,
380
+ operationCount: Object.keys(operations).length,
381
+ schemaCount: Object.keys(components.schemas).length,
382
+ messageCount: Object.keys(components.messages).length,
383
+ },
384
+ });
385
+ } catch (_e) { /* drop-silent */ }
386
+
387
+ return doc;
388
+ },
389
+
390
+ toJsonString: function (indent) {
391
+ return JSON.stringify(this.toJson(), null, indent || 2);
392
+ },
393
+
394
+ toYaml: function () {
395
+ return openapiYaml.toYaml(this.toJson());
396
+ },
397
+ };
398
+ }
399
+
400
+ function _validateServers(input) {
401
+ if (input == null) return {};
402
+ if (typeof input !== "object" || Array.isArray(input)) {
403
+ throw new AsyncApiError("asyncapi/bad-server",
404
+ "create: servers must be a map { serverId: spec, ... }");
405
+ }
406
+ var out = {};
407
+ for (var key in input) {
408
+ if (!Object.prototype.hasOwnProperty.call(input, key)) continue;
409
+ _validateServerEntry(input[key], "create: servers." + key);
410
+ out[key] = Object.assign({}, input[key]);
411
+ }
412
+ return out;
413
+ }
414
+
415
+ function _validateServerEntry(entry, label) {
416
+ if (!entry || typeof entry !== "object") {
417
+ throw new AsyncApiError("asyncapi/bad-server",
418
+ label + ": server must be an object");
419
+ }
420
+ validateOpts.requireNonEmptyString(entry.host,
421
+ label + ": host", AsyncApiError, "asyncapi/bad-server");
422
+ validateOpts.requireNonEmptyString(entry.protocol,
423
+ label + ": protocol", AsyncApiError, "asyncapi/bad-server");
424
+ }
425
+
426
+ // Parse + validate an external AsyncAPI 3.0 document. Like openapi.parse:
427
+ // returns `{ doc, errors[], valid }`. Throws on invalid JSON.
428
+ function parse(jsonStringOrObject) {
429
+ var doc;
430
+ if (typeof jsonStringOrObject === "string") {
431
+ try { doc = JSON.parse(jsonStringOrObject); } // allow:bare-json-parse — operator-supplied AsyncAPI doc; size-bounded by caller
432
+ catch (e) {
433
+ throw new AsyncApiError("asyncapi/bad-json",
434
+ "asyncapi.parse: invalid JSON — " + e.message);
435
+ }
436
+ } else if (jsonStringOrObject != null && typeof jsonStringOrObject === "object") {
437
+ doc = jsonStringOrObject;
438
+ } else {
439
+ throw new AsyncApiError("asyncapi/bad-input",
440
+ "asyncapi.parse: input must be a JSON string or a plain object");
441
+ }
442
+ var errors = [];
443
+ if (typeof doc.asyncapi !== "string") {
444
+ errors.push("missing or non-string `asyncapi` version field (must be 3.0.x)");
445
+ } else if (doc.asyncapi.indexOf("3.0") !== 0) {
446
+ errors.push("`asyncapi` version must be 3.0.x — got " + JSON.stringify(doc.asyncapi));
447
+ }
448
+ if (!doc.info || typeof doc.info !== "object") {
449
+ errors.push("missing or non-object `info`");
450
+ } else {
451
+ if (typeof doc.info.title !== "string" || doc.info.title.length === 0) {
452
+ errors.push("info.title must be a non-empty string");
453
+ }
454
+ if (typeof doc.info.version !== "string" || doc.info.version.length === 0) {
455
+ errors.push("info.version must be a non-empty string");
456
+ }
457
+ }
458
+ // Channels must be an object when present.
459
+ if (doc.channels != null && typeof doc.channels !== "object") {
460
+ errors.push("`channels` must be an object when present");
461
+ }
462
+ // Operations must reference declared channels.
463
+ var channels = doc.channels || {};
464
+ if (doc.operations && typeof doc.operations === "object") {
465
+ for (var opId in doc.operations) {
466
+ if (!Object.prototype.hasOwnProperty.call(doc.operations, opId)) continue;
467
+ var op = doc.operations[opId];
468
+ if (!op || typeof op !== "object") {
469
+ errors.push("operations." + opId + ": must be an object");
470
+ continue;
471
+ }
472
+ if (op.action !== "send" && op.action !== "receive") {
473
+ errors.push("operations." + opId + ".action must be 'send' or 'receive' — got " +
474
+ JSON.stringify(op.action));
475
+ }
476
+ var channelRef = op.channel;
477
+ if (channelRef && typeof channelRef === "object" && typeof channelRef["$ref"] === "string") {
478
+ var refMatch = channelRef["$ref"].match(/^#\/channels\/(.+)$/);
479
+ if (refMatch && !Object.prototype.hasOwnProperty.call(channels, refMatch[1])) {
480
+ errors.push("operations." + opId + ".channel: $ref " +
481
+ JSON.stringify(channelRef["$ref"]) +
482
+ " does not resolve to a declared channel");
483
+ }
484
+ } else {
485
+ errors.push("operations." + opId + ".channel: must be a $ref to #/channels/<id>");
486
+ }
487
+ }
488
+ }
489
+ // Server entries must carry host + protocol when present.
490
+ if (doc.servers && typeof doc.servers === "object") {
491
+ for (var sid in doc.servers) {
492
+ if (!Object.prototype.hasOwnProperty.call(doc.servers, sid)) continue;
493
+ var entry = doc.servers[sid];
494
+ if (!entry || typeof entry !== "object") {
495
+ errors.push("servers." + sid + ": must be an object");
496
+ continue;
497
+ }
498
+ if (typeof entry.host !== "string" || entry.host.length === 0) {
499
+ errors.push("servers." + sid + ".host must be a non-empty string");
500
+ }
501
+ if (typeof entry.protocol !== "string" || entry.protocol.length === 0) {
502
+ errors.push("servers." + sid + ".protocol must be a non-empty string");
503
+ }
504
+ }
505
+ }
506
+ // Dangling doc-level security references.
507
+ var schemes = (doc.components && doc.components.securitySchemes) || {};
508
+ if (Array.isArray(doc.security)) {
509
+ for (var s = 0; s < doc.security.length; s += 1) {
510
+ for (var sn in doc.security[s]) {
511
+ if (!Object.prototype.hasOwnProperty.call(doc.security[s], sn)) continue;
512
+ if (!schemes[sn]) {
513
+ errors.push("doc-level security references undefined scheme " + JSON.stringify(sn));
514
+ }
515
+ }
516
+ }
517
+ }
518
+ return { doc: doc, errors: errors, valid: errors.length === 0 };
519
+ }
520
+
521
+ module.exports = {
522
+ create: create,
523
+ parse: parse,
524
+ bindings: bindingsMod,
525
+ traits: traitsMod,
526
+ schemaWalk: schemaWalk.walk,
527
+ security: openapiSecurity,
528
+ toYaml: openapiYaml.toYaml,
529
+ VERSION: ASYNCAPI_VERSION,
530
+ AsyncApiError: AsyncApiError,
531
+ };
package/lib/audit-sign.js CHANGED
@@ -90,7 +90,7 @@ var SIGNING_KEY_SCHEMA = {
90
90
  properties: {
91
91
  publicKey: { type: "string" },
92
92
  privateKey: { type: "string" },
93
- algorithm: { type: "string" }, // optional; missing = legacy ml-dsa-87
93
+ algorithm: { type: "string" }, // load-time-required _initPlaintext + _initWrapped both throw KEY_FILE_MISSING_ALG / UNWRAPPED_MISSING_ALG when the field is absent (legacy implicit-default-to-ml-dsa-87 was removed in the pre-v1 compat-shim sweep). Schema's `required` keeps publicKey + privateKey only so the runtime checks fire with the precise error codes operators have wired alerting on.
94
94
  },
95
95
  };
96
96
 
package/lib/audit.js CHANGED
@@ -206,6 +206,8 @@ var FRAMEWORK_NAMESPACES = [
206
206
  "cache", // b.cache
207
207
  "compliance", // b.compliance (compliance.posture.set / cleared)
208
208
  "config", // b.configDrift (config.baseline.captured / config.drift.detected / config.baseline.tamper / config.baseline.unreadable)
209
+ "csrf", // b.middleware.csrfProtect (csrf.bad_cookie_value)
210
+ // (system.crypto.hybrid_disabled rides under "system" so no separate namespace)
209
211
  "db", // b.db / b.middleware.dbRoleFor / b.externalDb.runAs
210
212
  // (role-switching, RLS-shaped events)
211
213
  "dkim", // b.mail.dkim (DKIM-Signature generation events)
@@ -213,9 +215,16 @@ var FRAMEWORK_NAMESPACES = [
213
215
  "dsr", // b.dsr (Data Subject Rights workflow: dsr.ticket.* / dsr.source.*)
214
216
  "dual", // b.dualControl (dual.grant.requested / approved / denied / consumed / expired / self_approval_denied)
215
217
  "mail", // b.mail (b.mail-bounce uses "system.mail.*")
218
+ "mtls", // b.mtlsCa engine algorithm-selection audit (mtls.engine.algorithm_selected)
216
219
  "network", // b.middleware.networkAllowlist (network.gate.denied)
217
220
  "notify", // b.notify
218
221
  "objectstore", // b.objectStore.bucketOps (objectstore.bucket.* / objectstore.object.*)
222
+ "openapi", // b.openapi (openapi.document.built / openapi.document.served)
223
+ "asyncapi", // b.asyncapi (asyncapi.document.built)
224
+ "vault", // b.vault.aad (vault.aad.sealed / vault.aad.unseal_failed)
225
+ "wsclient", // b.wsClient (wsclient.connected / closed / error)
226
+ "inbox", // b.inbox (inbox.received / handled / handle_failed / swept)
227
+ "flag", // b.flag (flag.evaluated / flag.evaluation.error / flag.cache.bust)
219
228
  "permissions", // b.permissions
220
229
  "restore", // b.restore
221
230
  "retention", // b.retention (retention.rule.declared / sweep.started / row.processed / sweep.completed / sweep.failed)
@@ -696,10 +705,12 @@ function _ensureHandler() {
696
705
  // into a database that no longer represents the chain those
697
706
  // items were emitted against. Early-exit drops them; the
698
707
  // alternative is silent corruption of the next chain.
708
+ var droppedThisBatch = 0;
699
709
  for (var i = 0; i < batch.length; i++) {
700
710
  if (ctx && ctx.isShutdown && ctx.isShutdown()) return;
701
711
  try { await record(batch[i]); }
702
712
  catch (e) {
713
+ droppedThisBatch += 1;
703
714
  // Per-item failure shouldn't drop the whole batch; log and
704
715
  // continue. The handler's onError gets called for batch-
705
716
  // wide failures only.
@@ -708,6 +719,14 @@ function _ensureHandler() {
708
719
  " (action=" + (batch[i] && batch[i].action) + ")");
709
720
  }
710
721
  }
722
+ // Surface chain-write integrity failures via observability so
723
+ // operators alerting on rate-drop see something. The audit
724
+ // chain itself can't carry the signal — the chain is what's
725
+ // broken — so observability is the only sink left.
726
+ if (droppedThisBatch > 0) {
727
+ observability.safeEvent("system.audit.chain_write_dropped",
728
+ droppedThisBatch, { batchSize: batch.length });
729
+ }
711
730
  },
712
731
  });
713
732
  return _auditHandler;
@@ -717,6 +736,53 @@ function emit(event) {
717
736
  _ensureHandler().emit(event);
718
737
  }
719
738
 
739
+ // Outcome normalization — drop-silent on a strict `outcome` mismatch
740
+ // dropped a class of audit rows across the framework (every
741
+ // non-{success, failure, denied} outcome from b.flag / b.outbox /
742
+ // b.inbox / b.session / b.db / b.config-drift / b.compliance-aiAct
743
+ // landed in the handler's catch-and-log path instead of the chain).
744
+ // safeEmit owns the normalization; record() stays strict so direct
745
+ // callers see the typo loudly.
746
+ var OUTCOME_NORMALIZE = {
747
+ ok: "success",
748
+ okay: "success",
749
+ pass: "success",
750
+ passed: "success",
751
+ success: "success",
752
+ succeeded: "success",
753
+ warn: "success",
754
+ warning: "success",
755
+ duplicate: "success",
756
+ skip: "success",
757
+ skipped: "success",
758
+ fail: "failure",
759
+ failed: "failure",
760
+ failure: "failure",
761
+ err: "failure",
762
+ error: "failure",
763
+ denied: "denied",
764
+ refused: "denied",
765
+ deny: "denied",
766
+ };
767
+
768
+ function _normalizeOutcome(o) {
769
+ if (typeof o !== "string") return "success";
770
+ var n = OUTCOME_NORMALIZE[o.toLowerCase()];
771
+ return n || "success";
772
+ }
773
+
774
+ // Hyphens in action segments fall outside the underscore-only
775
+ // regex enforced by record(). Replace at the segment boundary so
776
+ // "compliance.aiact.biometric-id-categorisation" lands as
777
+ // "compliance.aiact.biometric_id_categorisation" and reaches the
778
+ // chain instead of dropping. The action namespace prefix (the part
779
+ // before the first dot) is left strict — namespaces are
780
+ // operator-registered and should be plain identifiers.
781
+ function _normalizeAction(action) {
782
+ if (typeof action !== "string") return action;
783
+ return action.replace(/-/g, "_");
784
+ }
785
+
720
786
  // safeEmit — fire-and-forget audit emit with safe defaults + try/catch.
721
787
  //
722
788
  // Most modules wrap emit() in their own _emit helper that fills in
@@ -755,9 +821,9 @@ function safeEmit(event) {
755
821
  } catch (_e) { /* fall through with original values */ }
756
822
  _ensureHandler().emit({
757
823
  actor: actor,
758
- action: event.action,
824
+ action: _normalizeAction(event.action),
759
825
  resource: event.resource || null,
760
- outcome: event.outcome || "success",
826
+ outcome: _normalizeOutcome(event.outcome),
761
827
  reason: reason,
762
828
  metadata: metadata,
763
829
  requestId: event.requestId || null,