@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
package/lib/inbox.js ADDED
@@ -0,0 +1,367 @@
1
+ "use strict";
2
+ /**
3
+ * b.inbox — transactional dedupe-on-receive.
4
+ *
5
+ * Companion to `b.outbox`. Where outbox guarantees at-least-once
6
+ * delivery, inbox lets the receiver guarantee exactly-once handling
7
+ * by recording every (source, messageId) pair in the same transaction
8
+ * as the business state change. If the same event is delivered twice
9
+ * (network retry, replay, broker re-dispatch on consumer failure),
10
+ * the second handler refuses with a duplicate-key constraint and the
11
+ * application sees a clean short-circuit.
12
+ *
13
+ * var inbox = b.inbox.create({
14
+ * externalDb: b.externalDb,
15
+ * table: "inbox_events",
16
+ * retentionDays: 30, // sweep older rows
17
+ * audit: true,
18
+ * });
19
+ *
20
+ * // High-level API — recommended for most callers:
21
+ * await inbox.handle({
22
+ * messageId: kafkaEvent.headers["x-event-id"],
23
+ * source: "kafka:orders.created.v1",
24
+ * payload: kafkaEvent.payload, // optional, audit only
25
+ * }, async function (xdb) {
26
+ * // Business state change runs exactly once per (source, messageId).
27
+ * await xdb.query("INSERT INTO orders ...", [...]);
28
+ * });
29
+ *
30
+ * // Low-level API — operator manages the transaction directly:
31
+ * await b.externalDb.transaction(async function (xdb) {
32
+ * var fresh = await inbox.recordReceive({
33
+ * messageId: id, source: "kafka:orders.created",
34
+ * }, xdb);
35
+ * if (!fresh) return; // duplicate; skip
36
+ * await xdb.query("INSERT INTO orders ...", [...]);
37
+ * });
38
+ *
39
+ * // Schema:
40
+ * await inbox.declareSchema(b.externalDb);
41
+ *
42
+ * // Periodic retention sweep (operator wires their scheduler):
43
+ * await inbox.sweep();
44
+ *
45
+ * Schema columns:
46
+ *
47
+ * message_id TEXT — primary part of the dedupe tuple
48
+ * source TEXT — namespace (kafka topic, queue name, ...)
49
+ * received_at TIMESTAMP
50
+ * processed_at TIMESTAMP NULL — set when handle() commits
51
+ * metadata_json JSONB / TEXT (operator-supplied audit blob)
52
+ *
53
+ * PRIMARY KEY (source, message_id) — enforces idempotence.
54
+ *
55
+ * Picking semantics:
56
+ * - Postgres backends: ON CONFLICT (source, message_id) DO NOTHING
57
+ * RETURNING * lets `recordReceive` decide fresh vs duplicate in
58
+ * a single round-trip.
59
+ * - SQLite: INSERT OR IGNORE + SELECT changes() to test fresh-ness.
60
+ */
61
+
62
+ var C = require("./constants");
63
+ var lazyRequire = require("./lazy-require");
64
+ var safeJson = require("./safe-json");
65
+ var safeSql = require("./safe-sql");
66
+ var validateOpts = require("./validate-opts");
67
+ var { defineClass } = require("./framework-error");
68
+
69
+ var InboxError = defineClass("InboxError", { alwaysPermanent: true });
70
+
71
+ var audit = lazyRequire(function () { return require("./audit"); });
72
+ var observability = lazyRequire(function () { return require("./observability"); });
73
+
74
+ function _validateTableName(name) {
75
+ try { safeSql.validateIdentifier(name); }
76
+ catch (e) {
77
+ throw new InboxError("inbox/bad-table",
78
+ "inbox.create: table " + JSON.stringify(name) +
79
+ " is not a safe SQL identifier — " + e.message);
80
+ }
81
+ }
82
+
83
+ function _utcNowExpr(externalDb) {
84
+ // Both backends accept this expression; SQLite returns ISO-8601,
85
+ // Postgres returns timestamptz.
86
+ if (externalDb && typeof externalDb.dialect === "string" &&
87
+ externalDb.dialect === "postgres") {
88
+ return "NOW()";
89
+ }
90
+ return "CURRENT_TIMESTAMP";
91
+ }
92
+
93
+ function create(opts) {
94
+ opts = opts || {};
95
+ validateOpts(opts, [
96
+ "externalDb", "table", "retentionDays", "audit",
97
+ "maxPayloadBytes", "messageIdMaxLen", "sourceMaxLen",
98
+ ], "inbox.create");
99
+ if (!opts.externalDb || typeof opts.externalDb.transaction !== "function") {
100
+ throw new InboxError("inbox/bad-externaldb",
101
+ "inbox.create: externalDb must be a b.externalDb instance");
102
+ }
103
+ validateOpts.requireNonEmptyString(opts.table,
104
+ "inbox.create: table", InboxError, "inbox/bad-table");
105
+ _validateTableName(opts.table);
106
+
107
+ var externalDb = opts.externalDb;
108
+ var table = opts.table;
109
+ var retentionDays = (typeof opts.retentionDays === "number" && opts.retentionDays > 0) // allow:numeric-opt-Infinity
110
+ ? opts.retentionDays : 30; // allow:raw-byte-literal — default retention days
111
+ var auditOn = opts.audit !== false;
112
+ var maxPayloadBytes = (typeof opts.maxPayloadBytes === "number" && opts.maxPayloadBytes > 0) // allow:numeric-opt-Infinity
113
+ ? opts.maxPayloadBytes : C.BYTES.kib(64);
114
+ var messageIdMaxLen = (typeof opts.messageIdMaxLen === "number" && opts.messageIdMaxLen > 0) // allow:numeric-opt-Infinity
115
+ ? opts.messageIdMaxLen : 256; // allow:raw-byte-literal — message-id length cap
116
+ var sourceMaxLen = (typeof opts.sourceMaxLen === "number" && opts.sourceMaxLen > 0) // allow:numeric-opt-Infinity
117
+ ? opts.sourceMaxLen : 256; // allow:raw-byte-literal — source length cap
118
+
119
+ function _emitAudit(action, outcome, metadata) {
120
+ if (!auditOn) return;
121
+ try {
122
+ audit().safeEmit({
123
+ action: action,
124
+ outcome: outcome,
125
+ actor: null,
126
+ metadata: metadata || {},
127
+ });
128
+ } catch (_e) { /* drop-silent */ }
129
+ }
130
+
131
+ function _validateReceiveOpts(receiveOpts, label) {
132
+ if (!receiveOpts || typeof receiveOpts !== "object") {
133
+ throw new InboxError("inbox/bad-receive",
134
+ label + ": receiveOpts must be an object");
135
+ }
136
+ validateOpts.requireNonEmptyString(receiveOpts.messageId,
137
+ label + ": messageId", InboxError, "inbox/bad-receive");
138
+ validateOpts.requireNonEmptyString(receiveOpts.source,
139
+ label + ": source", InboxError, "inbox/bad-receive");
140
+ if (receiveOpts.messageId.length > messageIdMaxLen) {
141
+ throw new InboxError("inbox/bad-receive",
142
+ label + ": messageId exceeds " + messageIdMaxLen + " chars");
143
+ }
144
+ if (receiveOpts.source.length > sourceMaxLen) {
145
+ throw new InboxError("inbox/bad-receive",
146
+ label + ": source exceeds " + sourceMaxLen + " chars");
147
+ }
148
+ }
149
+
150
+ async function recordReceive(receiveOpts, txn) {
151
+ if (!txn || typeof txn.query !== "function") {
152
+ throw new InboxError("inbox/bad-txn",
153
+ "recordReceive: txn must be a transaction handle (call inside externalDb.transaction)");
154
+ }
155
+ _validateReceiveOpts(receiveOpts, "recordReceive");
156
+ var metaJson = null;
157
+ if (receiveOpts.metadata != null) {
158
+ var serialized = safeJson.stringify(receiveOpts.metadata);
159
+ if (serialized.length > maxPayloadBytes) {
160
+ throw new InboxError("inbox/bad-receive",
161
+ "recordReceive: metadata serialized exceeds maxPayloadBytes (" +
162
+ maxPayloadBytes + " bytes)");
163
+ }
164
+ metaJson = serialized;
165
+ }
166
+ var nowExpr = _utcNowExpr(externalDb);
167
+ var dialect = (externalDb.dialect === "postgres") ? "postgres" : "sqlite";
168
+
169
+ if (dialect === "postgres") {
170
+ var rs = await txn.query(
171
+ "INSERT INTO " + table +
172
+ " (message_id, source, received_at, metadata_json) " +
173
+ " VALUES ($1, $2, " + nowExpr + ", $3::jsonb) " +
174
+ " ON CONFLICT (source, message_id) DO NOTHING " +
175
+ " RETURNING message_id",
176
+ [receiveOpts.messageId, receiveOpts.source, metaJson]);
177
+ var fresh = rs && rs.rows && rs.rows.length === 1;
178
+ _emitAudit("inbox.received", fresh ? "success" : "duplicate", {
179
+ source: receiveOpts.source, messageId: receiveOpts.messageId,
180
+ fresh: fresh,
181
+ });
182
+ return fresh;
183
+ }
184
+
185
+ // SQLite path — INSERT OR IGNORE + check changes()
186
+ await txn.query(
187
+ "INSERT OR IGNORE INTO " + table +
188
+ " (message_id, source, received_at, metadata_json) " +
189
+ " VALUES (?, ?, " + nowExpr + ", ?)",
190
+ [receiveOpts.messageId, receiveOpts.source, metaJson]);
191
+ var changedResult = await txn.query("SELECT changes() AS c");
192
+ var changedRow = changedResult.rows && changedResult.rows[0];
193
+ var sqlFresh = !!(changedRow && Number(changedRow.c) === 1);
194
+ _emitAudit("inbox.received", sqlFresh ? "success" : "duplicate", {
195
+ source: receiveOpts.source, messageId: receiveOpts.messageId,
196
+ fresh: sqlFresh,
197
+ });
198
+ return sqlFresh;
199
+ }
200
+
201
+ async function markProcessed(receiveOpts, txn) {
202
+ if (!txn || typeof txn.query !== "function") {
203
+ throw new InboxError("inbox/bad-txn",
204
+ "markProcessed: txn must be a transaction handle");
205
+ }
206
+ _validateReceiveOpts(receiveOpts, "markProcessed");
207
+ var nowExpr = _utcNowExpr(externalDb);
208
+ var dialect = (externalDb.dialect === "postgres") ? "postgres" : "sqlite";
209
+ var sql = "UPDATE " + table +
210
+ " SET processed_at = " + nowExpr +
211
+ " WHERE source = " + (dialect === "postgres" ? "$1" : "?") +
212
+ " AND message_id = " + (dialect === "postgres" ? "$2" : "?");
213
+ await txn.query(sql, [receiveOpts.source, receiveOpts.messageId]);
214
+ }
215
+
216
+ async function handle(receiveOpts, handler) {
217
+ if (typeof handler !== "function") {
218
+ throw new InboxError("inbox/bad-handler",
219
+ "handle: handler must be an async function (xdb) → void");
220
+ }
221
+ var startMs = Date.now();
222
+ var fresh = false;
223
+ var handlerErr = null;
224
+ var result;
225
+ try {
226
+ result = await externalDb.transaction(async function (xdb) {
227
+ fresh = await recordReceive(receiveOpts, xdb);
228
+ if (!fresh) return null;
229
+ var inner = await handler(xdb);
230
+ await markProcessed(receiveOpts, xdb);
231
+ return inner;
232
+ });
233
+ } catch (e) {
234
+ handlerErr = e;
235
+ _emitAudit("inbox.handle_failed", "fail", {
236
+ source: receiveOpts.source, messageId: receiveOpts.messageId,
237
+ message: e && e.message || String(e),
238
+ });
239
+ throw e;
240
+ }
241
+ _emitAudit("inbox.handled", fresh ? "success" : "duplicate", {
242
+ source: receiveOpts.source, messageId: receiveOpts.messageId,
243
+ fresh: fresh, elapsedMs: Date.now() - startMs,
244
+ });
245
+ if (!handlerErr) {
246
+ try {
247
+ observability().safeEvent("inbox.message_handled", {
248
+ source: receiveOpts.source, fresh: fresh,
249
+ });
250
+ } catch (_e) { /* drop-silent */ }
251
+ }
252
+ return { fresh: fresh, result: fresh ? result : null };
253
+ }
254
+
255
+ async function declareSchema(xdb) {
256
+ var dialect = (xdb && xdb.dialect === "postgres") ? "postgres" : "sqlite";
257
+ if (dialect === "postgres") {
258
+ await xdb.query(
259
+ "CREATE TABLE IF NOT EXISTS " + table + " (" +
260
+ " message_id TEXT NOT NULL," +
261
+ " source TEXT NOT NULL," +
262
+ " received_at TIMESTAMPTZ NOT NULL DEFAULT NOW()," +
263
+ " processed_at TIMESTAMPTZ NULL," +
264
+ " metadata_json JSONB NULL," +
265
+ " PRIMARY KEY (source, message_id)" +
266
+ ")");
267
+ await xdb.query(
268
+ "CREATE INDEX IF NOT EXISTS " + table + "_received_at_idx " +
269
+ "ON " + table + " (received_at)");
270
+ } else {
271
+ await xdb.query(
272
+ "CREATE TABLE IF NOT EXISTS " + table + " (" +
273
+ " message_id TEXT NOT NULL," +
274
+ " source TEXT NOT NULL," +
275
+ " received_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP," +
276
+ " processed_at TEXT NULL," +
277
+ " metadata_json TEXT NULL," +
278
+ " PRIMARY KEY (source, message_id)" +
279
+ ")");
280
+ await xdb.query(
281
+ "CREATE INDEX IF NOT EXISTS " + table + "_received_at_idx " +
282
+ "ON " + table + " (received_at)");
283
+ }
284
+ }
285
+
286
+ async function sweep() {
287
+ var dialect = (externalDb.dialect === "postgres") ? "postgres" : "sqlite";
288
+ var deleted = 0;
289
+ await externalDb.transaction(async function (xdb) {
290
+ if (dialect === "postgres") {
291
+ var rs = await xdb.query(
292
+ "DELETE FROM " + table +
293
+ " WHERE received_at < NOW() - $1::interval " +
294
+ " AND (processed_at IS NOT NULL OR received_at < NOW() - $2::interval)",
295
+ [retentionDays + " days", (retentionDays * 2) + " days"]);
296
+ deleted = (rs && typeof rs.rowCount === "number") ? rs.rowCount : 0;
297
+ } else {
298
+ var staleDate = new Date(Date.now() - retentionDays * C.TIME.days(1)).toISOString();
299
+ var unprocStaleDate = new Date(Date.now() - retentionDays * 2 * C.TIME.days(1)).toISOString();
300
+ await xdb.query(
301
+ "DELETE FROM " + table +
302
+ " WHERE received_at < ? " +
303
+ " AND (processed_at IS NOT NULL OR received_at < ?)",
304
+ [staleDate, unprocStaleDate]);
305
+ var changedResult = await xdb.query("SELECT changes() AS c");
306
+ var changedRow = changedResult.rows && changedResult.rows[0];
307
+ deleted = changedRow ? Number(changedRow.c) : 0;
308
+ }
309
+ });
310
+ _emitAudit("inbox.swept", "success", {
311
+ deleted: deleted, retentionDays: retentionDays,
312
+ });
313
+ return deleted;
314
+ }
315
+
316
+ async function isFresh(receiveOpts) {
317
+ _validateReceiveOpts(receiveOpts, "isFresh");
318
+ var dialect = (externalDb.dialect === "postgres") ? "postgres" : "sqlite";
319
+ var sql = "SELECT 1 FROM " + table +
320
+ " WHERE source = " + (dialect === "postgres" ? "$1" : "?") +
321
+ " AND message_id = " + (dialect === "postgres" ? "$2" : "?");
322
+ var rs = await externalDb.transaction(async function (xdb) {
323
+ return await xdb.query(sql, [receiveOpts.source, receiveOpts.messageId]);
324
+ });
325
+ return !rs || !rs.rows || rs.rows.length === 0;
326
+ }
327
+
328
+ async function getReceiveStats(opts2) {
329
+ opts2 = opts2 || {};
330
+ var sourceFilter = (typeof opts2.source === "string" && opts2.source.length > 0)
331
+ ? opts2.source : null;
332
+ var dialect = (externalDb.dialect === "postgres") ? "postgres" : "sqlite";
333
+ var stats = await externalDb.transaction(async function (xdb) {
334
+ var sql = "SELECT COUNT(*) AS total," +
335
+ " COUNT(processed_at) AS processed " +
336
+ " FROM " + table +
337
+ (sourceFilter ? " WHERE source = " +
338
+ (dialect === "postgres" ? "$1" : "?") : "");
339
+ var args = sourceFilter ? [sourceFilter] : [];
340
+ var rs = await xdb.query(sql, args);
341
+ var row = rs.rows && rs.rows[0];
342
+ return {
343
+ total: row ? Number(row.total) : 0,
344
+ processed: row ? Number(row.processed) : 0,
345
+ };
346
+ });
347
+ stats.unprocessed = stats.total - stats.processed;
348
+ return stats;
349
+ }
350
+
351
+ return {
352
+ declareSchema: declareSchema,
353
+ recordReceive: recordReceive,
354
+ markProcessed: markProcessed,
355
+ handle: handle,
356
+ sweep: sweep,
357
+ isFresh: isFresh,
358
+ getStats: getReceiveStats,
359
+ table: table,
360
+ retentionDays: retentionDays,
361
+ };
362
+ }
363
+
364
+ module.exports = {
365
+ create: create,
366
+ InboxError: InboxError,
367
+ };