@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.
- package/CHANGELOG.md +41 -1
- package/NOTICE +17 -1
- package/README.md +4 -3
- package/index.js +15 -0
- package/lib/asyncapi-bindings.js +160 -0
- package/lib/asyncapi-traits.js +143 -0
- package/lib/asyncapi.js +531 -0
- package/lib/audit-sign.js +1 -1
- package/lib/audit.js +68 -2
- package/lib/auth/acr-vocabulary.js +265 -0
- package/lib/auth/auth-time-tracker.js +111 -0
- package/lib/auth/elevation-grant.js +306 -0
- package/lib/auth/jwt.js +13 -0
- package/lib/auth/lockout.js +16 -3
- package/lib/auth/oauth.js +15 -1
- package/lib/auth/password.js +22 -2
- package/lib/auth/sd-jwt-vc-issuer.js +2 -2
- package/lib/auth/sd-jwt-vc.js +7 -2
- package/lib/auth/step-up-policy.js +335 -0
- package/lib/auth/step-up.js +445 -0
- package/lib/break-glass.js +53 -14
- package/lib/cache-redis.js +1 -1
- package/lib/cache.js +6 -1
- package/lib/cli.js +3 -3
- package/lib/cluster.js +24 -1
- package/lib/compliance-ai-act-logging.js +190 -0
- package/lib/compliance-ai-act-prohibited.js +205 -0
- package/lib/compliance-ai-act-risk.js +189 -0
- package/lib/compliance-ai-act-transparency.js +200 -0
- package/lib/compliance-ai-act.js +558 -0
- package/lib/compliance.js +12 -2
- package/lib/config-drift.js +2 -2
- package/lib/crypto-field.js +21 -1
- package/lib/crypto.js +114 -1
- package/lib/db.js +35 -4
- package/lib/dev.js +30 -3
- package/lib/dual-control.js +19 -1
- package/lib/external-db.js +10 -0
- package/lib/file-upload.js +30 -3
- package/lib/flag-cache.js +136 -0
- package/lib/flag-evaluation-context.js +135 -0
- package/lib/flag-providers.js +279 -0
- package/lib/flag-targeting.js +210 -0
- package/lib/flag.js +284 -0
- package/lib/guard-all.js +33 -16
- package/lib/guard-csv.js +16 -2
- package/lib/guard-html.js +35 -0
- package/lib/guard-svg.js +20 -0
- package/lib/http-client.js +57 -11
- package/lib/inbox.js +391 -0
- package/lib/log-stream-syslog.js +8 -0
- package/lib/log-stream.js +1 -1
- package/lib/mail-arc-sign.js +372 -0
- package/lib/mail-auth.js +2 -0
- package/lib/mail.js +40 -0
- package/lib/middleware/ai-act-disclosure.js +166 -0
- package/lib/middleware/asyncapi-serve.js +136 -0
- package/lib/middleware/attach-user.js +25 -2
- package/lib/middleware/bearer-auth.js +71 -6
- package/lib/middleware/body-parser.js +13 -0
- package/lib/middleware/cors.js +10 -0
- package/lib/middleware/csrf-protect.js +34 -3
- package/lib/middleware/dpop.js +3 -3
- package/lib/middleware/flag-context.js +76 -0
- package/lib/middleware/host-allowlist.js +1 -1
- package/lib/middleware/index.js +15 -0
- package/lib/middleware/openapi-serve.js +143 -0
- package/lib/middleware/require-aal.js +2 -2
- package/lib/middleware/require-step-up.js +186 -0
- package/lib/middleware/trace-propagate.js +1 -1
- package/lib/mtls-ca.js +23 -29
- package/lib/mtls-engine-default.js +21 -1
- package/lib/network-tls.js +21 -6
- package/lib/object-store/sigv4-bucket-ops.js +41 -0
- package/lib/observability-otlp-exporter.js +35 -2
- package/lib/openapi-paths-builder.js +248 -0
- package/lib/openapi-schema-walk.js +192 -0
- package/lib/openapi-security.js +169 -0
- package/lib/openapi-yaml.js +154 -0
- package/lib/openapi.js +443 -0
- package/lib/outbox.js +3 -3
- package/lib/permissions.js +10 -1
- package/lib/pqc-agent.js +22 -1
- package/lib/pqc-software.js +195 -0
- package/lib/pubsub.js +8 -4
- package/lib/redact.js +26 -1
- package/lib/retention.js +26 -0
- package/lib/router.js +1 -0
- package/lib/scheduler.js +57 -1
- package/lib/session.js +3 -3
- package/lib/ssrf-guard.js +19 -4
- package/lib/static.js +12 -0
- package/lib/totp.js +16 -0
- package/lib/vault/index.js +3 -0
- package/lib/vault-aad.js +259 -0
- package/lib/vendor/MANIFEST.json +29 -0
- package/lib/vendor/noble-post-quantum.cjs +18 -0
- package/lib/ws-client.js +978 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
package/lib/asyncapi.js
ADDED
|
@@ -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" }, //
|
|
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
|
|
826
|
+
outcome: _normalizeOutcome(event.outcome),
|
|
761
827
|
reason: reason,
|
|
762
828
|
metadata: metadata,
|
|
763
829
|
requestId: event.requestId || null,
|