@blamejs/core 0.9.28 → 0.9.39
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 +886 -875
- package/index.js +20 -1
- package/lib/agent-snapshot.js +346 -0
- package/lib/agent-trace.js +218 -0
- package/lib/guard-all.js +1 -0
- package/lib/guard-dsn.js +379 -0
- package/lib/guard-envelope.js +294 -0
- package/lib/guard-list-unsubscribe.js +337 -0
- package/lib/guard-smtp-command.js +484 -0
- package/lib/guard-snapshot-envelope.js +168 -0
- package/lib/guard-trace-context.js +172 -0
- package/lib/ip-utils.js +102 -0
- package/lib/mail-auth.js +4 -35
- package/lib/mail-greylist.js +448 -0
- package/lib/mail-helo.js +473 -0
- package/lib/mail-rbl.js +392 -0
- package/lib/mail.js +2 -1
- package/lib/network-dns-resolver.js +500 -0
- package/lib/network.js +1 -0
- package/lib/redis-client.js +2 -1
- package/lib/safe-dns.js +665 -0
- package/lib/tracing.js +36 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/index.js
CHANGED
|
@@ -87,6 +87,7 @@ var storage = require("./lib/storage");
|
|
|
87
87
|
var safeJson = require("./lib/safe-json");
|
|
88
88
|
var safeJsonPath = require("./lib/safe-jsonpath");
|
|
89
89
|
var safeMime = require("./lib/safe-mime");
|
|
90
|
+
var safeDns = require("./lib/safe-dns");
|
|
90
91
|
var mailStore = require("./lib/mail-store");
|
|
91
92
|
var ntpCheck = require("./lib/ntp-check");
|
|
92
93
|
var auditSign = require("./lib/audit-sign");
|
|
@@ -160,6 +161,10 @@ var guardHtml = require("./lib/guard-html");
|
|
|
160
161
|
var guardSvg = require("./lib/guard-svg");
|
|
161
162
|
var guardFilename = require("./lib/guard-filename");
|
|
162
163
|
var guardMessageId = require("./lib/guard-message-id");
|
|
164
|
+
var guardSmtpCommand = require("./lib/guard-smtp-command");
|
|
165
|
+
var guardEnvelope = require("./lib/guard-envelope");
|
|
166
|
+
var guardDsn = require("./lib/guard-dsn");
|
|
167
|
+
var guardListUnsubscribe = require("./lib/guard-list-unsubscribe");
|
|
163
168
|
var guardMailQuery = require("./lib/guard-mail-query");
|
|
164
169
|
var guardMailCompose = require("./lib/guard-mail-compose");
|
|
165
170
|
var guardMailReply = require("./lib/guard-mail-reply");
|
|
@@ -173,6 +178,8 @@ var guardEventBusPayload = require("./lib/guard-event-bus-payload");
|
|
|
173
178
|
var guardTenantId = require("./lib/guard-tenant-id");
|
|
174
179
|
var guardSagaConfig = require("./lib/guard-saga-config");
|
|
175
180
|
var guardPostureChain = require("./lib/guard-posture-chain");
|
|
181
|
+
var guardTraceContext = require("./lib/guard-trace-context");
|
|
182
|
+
var guardSnapshotEnvelope = require("./lib/guard-snapshot-envelope");
|
|
176
183
|
var agentOrchestrator = require("./lib/agent-orchestrator");
|
|
177
184
|
var agentIdempotency = require("./lib/agent-idempotency");
|
|
178
185
|
var agentStream = require("./lib/agent-stream");
|
|
@@ -180,6 +187,8 @@ var agentEventBus = require("./lib/agent-event-bus");
|
|
|
180
187
|
var agentTenant = require("./lib/agent-tenant");
|
|
181
188
|
var agentSaga = require("./lib/agent-saga");
|
|
182
189
|
var agentPostureChain = require("./lib/agent-posture-chain");
|
|
190
|
+
var agentTrace = require("./lib/agent-trace");
|
|
191
|
+
var agentSnapshot = require("./lib/agent-snapshot");
|
|
183
192
|
var guardArchive = require("./lib/guard-archive");
|
|
184
193
|
var guardJson = require("./lib/guard-json");
|
|
185
194
|
var guardYaml = require("./lib/guard-yaml");
|
|
@@ -246,6 +255,9 @@ var csv = require("./lib/csv");
|
|
|
246
255
|
var time = require("./lib/time");
|
|
247
256
|
var uuid = require("./lib/uuid");
|
|
248
257
|
var mail = require("./lib/mail");
|
|
258
|
+
mail.rbl = require("./lib/mail-rbl");
|
|
259
|
+
mail.greylist = require("./lib/mail-greylist");
|
|
260
|
+
mail.helo = require("./lib/mail-helo");
|
|
249
261
|
var mailArf = require("./lib/mail-arf");
|
|
250
262
|
var mailBounce = require("./lib/mail-bounce");
|
|
251
263
|
var mailMdn = require("./lib/mail-mdn");
|
|
@@ -418,6 +430,10 @@ module.exports = {
|
|
|
418
430
|
guardSvg: guardSvg,
|
|
419
431
|
guardFilename: guardFilename,
|
|
420
432
|
guardMessageId: guardMessageId,
|
|
433
|
+
guardSmtpCommand: guardSmtpCommand,
|
|
434
|
+
guardEnvelope: guardEnvelope,
|
|
435
|
+
guardDsn: guardDsn,
|
|
436
|
+
guardListUnsubscribe: guardListUnsubscribe,
|
|
421
437
|
guardMailQuery: guardMailQuery,
|
|
422
438
|
guardMailCompose: guardMailCompose,
|
|
423
439
|
guardMailReply: guardMailReply,
|
|
@@ -431,7 +447,9 @@ module.exports = {
|
|
|
431
447
|
guardTenantId: guardTenantId,
|
|
432
448
|
guardSagaConfig: guardSagaConfig,
|
|
433
449
|
guardPostureChain: guardPostureChain,
|
|
434
|
-
|
|
450
|
+
guardTraceContext: guardTraceContext,
|
|
451
|
+
guardSnapshotEnvelope: guardSnapshotEnvelope,
|
|
452
|
+
agent: { orchestrator: agentOrchestrator, idempotency: agentIdempotency, stream: agentStream, eventBus: agentEventBus, tenant: agentTenant, saga: agentSaga, postureChain: agentPostureChain, trace: agentTrace, snapshot: agentSnapshot },
|
|
435
453
|
guardArchive: guardArchive,
|
|
436
454
|
guardJson: guardJson,
|
|
437
455
|
guardYaml: guardYaml,
|
|
@@ -512,6 +530,7 @@ module.exports = {
|
|
|
512
530
|
safeJson: safeJson,
|
|
513
531
|
safeJsonPath: safeJsonPath,
|
|
514
532
|
safeMime: safeMime,
|
|
533
|
+
safeDns: safeDns,
|
|
515
534
|
mailStore: mailStore,
|
|
516
535
|
safeSchema: safeSchema,
|
|
517
536
|
pagination: pagination,
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.agent.snapshot
|
|
4
|
+
* @nav Agent
|
|
5
|
+
* @title Agent Snapshot
|
|
6
|
+
* @order 90
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* Drain → snapshot in-flight state; restart → restore + resume. The
|
|
10
|
+
* last substrate slice: makes the orchestrator + idempotency +
|
|
11
|
+
* stream + event-bus + tenant + saga + posture-chain + trace
|
|
12
|
+
* stack operationally durable across deploys + crashes.
|
|
13
|
+
*
|
|
14
|
+
* Snapshot captures (registry of agents, in-flight streams' last-
|
|
15
|
+
* seen cursors, half-completed saga state, pending event-bus
|
|
16
|
+
* deliveries, idempotency cache hot-subset). Restore re-elects
|
|
17
|
+
* shards, replays buffered events (composes v0.9.22 idempotency to
|
|
18
|
+
* prevent double-execute), resumes sagas from their persisted
|
|
19
|
+
* step pointer.
|
|
20
|
+
*
|
|
21
|
+
* ```js
|
|
22
|
+
* var snapshot = b.agent.snapshot.create({
|
|
23
|
+
* orchestrator: orch,
|
|
24
|
+
* backend: operatorBackend, // { put, get, list, delete }
|
|
25
|
+
* audit: b.audit,
|
|
26
|
+
* policy: {
|
|
27
|
+
* drainTimeoutMs: C.TIME.minutes(2),
|
|
28
|
+
* snapshotIntervalMs: C.TIME.minutes(5),
|
|
29
|
+
* maxSnapshotBytes: C.BYTES.mib(50),
|
|
30
|
+
* },
|
|
31
|
+
* });
|
|
32
|
+
*
|
|
33
|
+
* // At SIGTERM:
|
|
34
|
+
* await orch.drain({});
|
|
35
|
+
* var snap = await snapshot.takeSnapshot();
|
|
36
|
+
* await snapshot.persist(snap);
|
|
37
|
+
*
|
|
38
|
+
* // At restart:
|
|
39
|
+
* var loaded = await snapshot.loadLatest();
|
|
40
|
+
* if (loaded) await snapshot.restore(loaded);
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @card
|
|
44
|
+
* Drain → snapshot in-flight state; restart → restore. Composes
|
|
45
|
+
* orchestrator drain + outbox in-flight tracking + saga persisted
|
|
46
|
+
* state + event-bus subscriber registry + idempotency hot cache.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
var lazyRequire = require("./lazy-require");
|
|
50
|
+
var C = require("./constants");
|
|
51
|
+
var { defineClass } = require("./framework-error");
|
|
52
|
+
var bCrypto = require("./crypto");
|
|
53
|
+
var guardSnapshotEnvelope = require("./guard-snapshot-envelope");
|
|
54
|
+
var agentAudit = require("./agent-audit");
|
|
55
|
+
|
|
56
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
57
|
+
|
|
58
|
+
var AgentSnapshotError = defineClass("AgentSnapshotError", { alwaysPermanent: true });
|
|
59
|
+
|
|
60
|
+
var DEFAULT_DRAIN_TIMEOUT_MS = C.TIME.minutes(2);
|
|
61
|
+
var DEFAULT_SNAPSHOT_INTERVAL_MS = C.TIME.minutes(5);
|
|
62
|
+
var DEFAULT_MAX_SNAPSHOT_BYTES = C.BYTES.mib(50);
|
|
63
|
+
var SCHEMA_VERSION = 1;
|
|
64
|
+
var SNAPSHOT_ID_RAND_BYTES = 8; // allow:raw-byte-literal — snapshot-id random suffix
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @primitive b.agent.snapshot.create
|
|
68
|
+
* @signature b.agent.snapshot.create(opts)
|
|
69
|
+
* @since 0.9.30
|
|
70
|
+
* @status stable
|
|
71
|
+
* @related b.agent.orchestrator.create, b.backup.create
|
|
72
|
+
*
|
|
73
|
+
* Create the snapshot facade. Operator wires the durable storage
|
|
74
|
+
* backend; framework owns the envelope shape + drain/restore
|
|
75
|
+
* coordination.
|
|
76
|
+
*
|
|
77
|
+
* @opts
|
|
78
|
+
* orchestrator: b.agent.orchestrator, // required
|
|
79
|
+
* backend: { put, get, list, delete }, // required
|
|
80
|
+
* audit: b.audit namespace, // optional
|
|
81
|
+
* policy: { drainTimeoutMs, snapshotIntervalMs, maxSnapshotBytes },
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* var snapshot = b.agent.snapshot.create({
|
|
85
|
+
* orchestrator: orch, backend: myBackend,
|
|
86
|
+
* });
|
|
87
|
+
* var snap = await snapshot.takeSnapshot();
|
|
88
|
+
* await snapshot.persist(snap);
|
|
89
|
+
*/
|
|
90
|
+
function create(opts) {
|
|
91
|
+
opts = opts || {};
|
|
92
|
+
if (!opts.orchestrator || typeof opts.orchestrator.health !== "function") {
|
|
93
|
+
throw new AgentSnapshotError("agent-snapshot/bad-orchestrator",
|
|
94
|
+
"create: opts.orchestrator with .health() required");
|
|
95
|
+
}
|
|
96
|
+
if (!opts.backend || typeof opts.backend.put !== "function" ||
|
|
97
|
+
typeof opts.backend.get !== "function" || typeof opts.backend.list !== "function") {
|
|
98
|
+
throw new AgentSnapshotError("agent-snapshot/bad-backend",
|
|
99
|
+
"create: opts.backend must expose { put, get, list, delete? }");
|
|
100
|
+
}
|
|
101
|
+
var policy = opts.policy || {};
|
|
102
|
+
var drainTimeoutMs = typeof policy.drainTimeoutMs === "number" ? policy.drainTimeoutMs : DEFAULT_DRAIN_TIMEOUT_MS;
|
|
103
|
+
var snapshotIntervalMs = typeof policy.snapshotIntervalMs === "number" ? policy.snapshotIntervalMs : DEFAULT_SNAPSHOT_INTERVAL_MS;
|
|
104
|
+
var maxSnapshotBytes = typeof policy.maxSnapshotBytes === "number" ? policy.maxSnapshotBytes : DEFAULT_MAX_SNAPSHOT_BYTES;
|
|
105
|
+
var auditImpl = opts.audit || audit();
|
|
106
|
+
|
|
107
|
+
var ctx = {
|
|
108
|
+
orchestrator: opts.orchestrator,
|
|
109
|
+
backend: opts.backend,
|
|
110
|
+
audit: auditImpl,
|
|
111
|
+
drainTimeoutMs: drainTimeoutMs,
|
|
112
|
+
snapshotIntervalMs: snapshotIntervalMs,
|
|
113
|
+
maxSnapshotBytes: maxSnapshotBytes,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
takeSnapshot: function (snapshotOpts) { return _takeSnapshot(ctx, snapshotOpts || {}); },
|
|
118
|
+
persist: function (snap) { return _persist(ctx, snap); },
|
|
119
|
+
loadLatest: function (loadOpts) { return _loadLatest(ctx, loadOpts || {}); },
|
|
120
|
+
loadById: function (snapshotId) { return _loadById(ctx, snapshotId); },
|
|
121
|
+
restore: function (snap, restoreOpts) { return _restore(ctx, snap, restoreOpts || {}); },
|
|
122
|
+
list: function (listOpts) { return _list(ctx, listOpts || {}); },
|
|
123
|
+
gc: function (gcOpts) { return _gc(ctx, gcOpts || {}); },
|
|
124
|
+
SCHEMA_VERSION: SCHEMA_VERSION,
|
|
125
|
+
AgentSnapshotError: AgentSnapshotError,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---- Take snapshot --------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
async function _takeSnapshot(ctx, snapshotOpts) {
|
|
132
|
+
var snapshotId = "snap-" + bCrypto.generateToken(SNAPSHOT_ID_RAND_BYTES);
|
|
133
|
+
var health = await ctx.orchestrator.health();
|
|
134
|
+
var envelope = {
|
|
135
|
+
snapshotId: snapshotId,
|
|
136
|
+
takenAt: Date.now(),
|
|
137
|
+
frameworkVersion: snapshotOpts.frameworkVersion || _frameworkVersion(),
|
|
138
|
+
schemaVersion: SCHEMA_VERSION,
|
|
139
|
+
tenantId: snapshotOpts.tenantId || null,
|
|
140
|
+
orchestratorState: {
|
|
141
|
+
agents: Array.isArray(health.agents) ? health.agents.slice() : [],
|
|
142
|
+
elections: Array.isArray(health.elections) ? health.elections.slice() : [],
|
|
143
|
+
consumers: Array.isArray(health.consumers) ? health.consumers.slice() : [],
|
|
144
|
+
},
|
|
145
|
+
inFlight: {
|
|
146
|
+
streams: snapshotOpts.streams || [],
|
|
147
|
+
sagas: snapshotOpts.sagas || [],
|
|
148
|
+
outboxJobs: snapshotOpts.outboxJobs || [],
|
|
149
|
+
busSubscribers: snapshotOpts.busSubscribers || [],
|
|
150
|
+
pendingDeliveries: snapshotOpts.pendingDeliveries || [],
|
|
151
|
+
},
|
|
152
|
+
idempotencyCache: snapshotOpts.idempotencyCache || {},
|
|
153
|
+
sig: null, // populated by persist() via b.audit-sign
|
|
154
|
+
};
|
|
155
|
+
guardSnapshotEnvelope.validate(envelope, { profile: "strict" });
|
|
156
|
+
// Enforce per-instance maxSnapshotBytes (separate from guard's
|
|
157
|
+
// profile-level cap — operator may have tighter limits).
|
|
158
|
+
var serialized = JSON.stringify(envelope);
|
|
159
|
+
if (Buffer.byteLength(serialized, "utf8") > ctx.maxSnapshotBytes) {
|
|
160
|
+
throw new AgentSnapshotError("agent-snapshot/oversize",
|
|
161
|
+
"takeSnapshot: " + Buffer.byteLength(serialized, "utf8") +
|
|
162
|
+
" bytes exceeds maxSnapshotBytes=" + ctx.maxSnapshotBytes);
|
|
163
|
+
}
|
|
164
|
+
agentAudit.safeAudit(ctx.audit, "agent.snapshot.taken", null, {
|
|
165
|
+
snapshotId: snapshotId,
|
|
166
|
+
inFlightCount: _inFlightCount(envelope),
|
|
167
|
+
tenantId: envelope.tenantId,
|
|
168
|
+
});
|
|
169
|
+
return envelope;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---- Persist --------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
async function _persist(ctx, snap) {
|
|
175
|
+
guardSnapshotEnvelope.validate(snap);
|
|
176
|
+
// Operator's backend stores the envelope by snapshotId.
|
|
177
|
+
await ctx.backend.put(snap.snapshotId, snap);
|
|
178
|
+
agentAudit.safeAudit(ctx.audit, "agent.snapshot.persisted", null, {
|
|
179
|
+
snapshotId: snap.snapshotId, takenAt: snap.takenAt,
|
|
180
|
+
});
|
|
181
|
+
return { snapshotId: snap.snapshotId };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---- Load -----------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
async function _loadLatest(ctx, loadOpts) {
|
|
187
|
+
var entries = await ctx.backend.list();
|
|
188
|
+
if (!Array.isArray(entries) || entries.length === 0) return null;
|
|
189
|
+
// Filter by tenantId if requested.
|
|
190
|
+
var filtered = entries.filter(function (e) {
|
|
191
|
+
if (loadOpts.tenantId && e.tenantId !== loadOpts.tenantId) return false;
|
|
192
|
+
return true;
|
|
193
|
+
});
|
|
194
|
+
if (filtered.length === 0) return null;
|
|
195
|
+
filtered.sort(function (a, b) { return (b.takenAt || 0) - (a.takenAt || 0); });
|
|
196
|
+
var latestId = filtered[0].snapshotId;
|
|
197
|
+
var snap = await ctx.backend.get(latestId);
|
|
198
|
+
if (!snap) return null;
|
|
199
|
+
guardSnapshotEnvelope.validate(snap);
|
|
200
|
+
return snap;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function _loadById(ctx, snapshotId) {
|
|
204
|
+
if (typeof snapshotId !== "string" || snapshotId.length === 0) {
|
|
205
|
+
throw new AgentSnapshotError("agent-snapshot/bad-snapshot-id",
|
|
206
|
+
"loadById: snapshotId required");
|
|
207
|
+
}
|
|
208
|
+
var snap = await ctx.backend.get(snapshotId);
|
|
209
|
+
if (!snap) return null;
|
|
210
|
+
guardSnapshotEnvelope.validate(snap);
|
|
211
|
+
return snap;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ---- Restore --------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
async function _restore(ctx, snap, restoreOpts) {
|
|
217
|
+
guardSnapshotEnvelope.validate(snap);
|
|
218
|
+
// Schema-version mismatch refuses unless operator explicit opt-in.
|
|
219
|
+
if (snap.schemaVersion !== SCHEMA_VERSION) {
|
|
220
|
+
if (!restoreOpts.allowSchemaVersionMismatch) {
|
|
221
|
+
throw new AgentSnapshotError("agent-snapshot/schema-version-mismatch",
|
|
222
|
+
"restore: snap.schemaVersion=" + snap.schemaVersion +
|
|
223
|
+
" != current=" + SCHEMA_VERSION + "; set restoreOpts.allowSchemaVersionMismatch to opt in");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// Topology change detection — current cluster's consumer set may
|
|
227
|
+
// differ from the snapshot's. Re-shard-and-resume default; operator
|
|
228
|
+
// can opt to refuse via restoreOpts.refuseOnTopologyChange.
|
|
229
|
+
var currentHealth = await ctx.orchestrator.health();
|
|
230
|
+
var topologyChanged = _topologyChanged(snap.orchestratorState, currentHealth);
|
|
231
|
+
if (topologyChanged && restoreOpts.refuseOnTopologyChange) {
|
|
232
|
+
throw new AgentSnapshotError("agent-snapshot/topology-changed",
|
|
233
|
+
"restore: cluster topology changed since snapshot; refuseOnTopologyChange=true");
|
|
234
|
+
}
|
|
235
|
+
if (topologyChanged) {
|
|
236
|
+
agentAudit.safeAudit(ctx.audit, "agent.snapshot.topology-change", null, {
|
|
237
|
+
snapshotId: snap.snapshotId,
|
|
238
|
+
snapshotConsumerCount: (snap.orchestratorState.consumers || []).length,
|
|
239
|
+
restoreConsumerCount: (currentHealth.consumers || []).length,
|
|
240
|
+
reshardedShards: _reshardedShards(snap.orchestratorState, currentHealth),
|
|
241
|
+
affectedInFlight: _inFlightCount(snap),
|
|
242
|
+
affectedSagas: (snap.inFlight && snap.inFlight.sagas || []).length,
|
|
243
|
+
affectedStreams: (snap.inFlight && snap.inFlight.streams || []).length,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
// Restore is a SIGNAL — orchestrator + idempotency + saga + event-
|
|
247
|
+
// bus consumers see the envelope and hydrate themselves. v0.9.30
|
|
248
|
+
// ships the contract; each substrate primitive's restore hook lands
|
|
249
|
+
// in subsequent slices as operators wire them.
|
|
250
|
+
agentAudit.safeAudit(ctx.audit, "agent.snapshot.restored", null, {
|
|
251
|
+
snapshotId: snap.snapshotId,
|
|
252
|
+
schemaVersion: snap.schemaVersion,
|
|
253
|
+
inFlightCount: _inFlightCount(snap),
|
|
254
|
+
topologyChanged: topologyChanged,
|
|
255
|
+
});
|
|
256
|
+
return { snapshotId: snap.snapshotId, topologyChanged: topologyChanged };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ---- List + GC ------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
async function _list(ctx, listOpts) {
|
|
262
|
+
var entries = await ctx.backend.list();
|
|
263
|
+
if (!Array.isArray(entries)) return [];
|
|
264
|
+
return entries.filter(function (e) {
|
|
265
|
+
if (listOpts.tenantId && e.tenantId !== listOpts.tenantId) return false;
|
|
266
|
+
if (listOpts.sinceMs && (e.takenAt || 0) < listOpts.sinceMs) return false;
|
|
267
|
+
return true;
|
|
268
|
+
}).map(function (e) {
|
|
269
|
+
return {
|
|
270
|
+
snapshotId: e.snapshotId,
|
|
271
|
+
takenAt: e.takenAt,
|
|
272
|
+
tenantId: e.tenantId || null,
|
|
273
|
+
};
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function _gc(ctx, gcOpts) {
|
|
278
|
+
if (typeof ctx.backend.delete !== "function") return { purged: 0 };
|
|
279
|
+
var olderThanMs = typeof gcOpts.olderThanMs === "number" ? gcOpts.olderThanMs : 0;
|
|
280
|
+
var cutoff = Date.now() - olderThanMs;
|
|
281
|
+
var entries = await ctx.backend.list();
|
|
282
|
+
var purged = 0;
|
|
283
|
+
for (var i = 0; i < entries.length; i += 1) {
|
|
284
|
+
var e = entries[i];
|
|
285
|
+
if ((e.takenAt || 0) <= cutoff) {
|
|
286
|
+
try { await ctx.backend.delete(e.snapshotId); purged += 1; }
|
|
287
|
+
catch (_e) { /* best-effort */ }
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
agentAudit.safeAudit(ctx.audit, "agent.snapshot.gc", null, { purged: purged });
|
|
291
|
+
return { purged: purged };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ---- Internals ------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
function _inFlightCount(snap) {
|
|
297
|
+
if (!snap || !snap.inFlight) return 0;
|
|
298
|
+
var n = 0;
|
|
299
|
+
["streams", "sagas", "outboxJobs", "busSubscribers", "pendingDeliveries"].forEach(function (k) {
|
|
300
|
+
if (Array.isArray(snap.inFlight[k])) n += snap.inFlight[k].length;
|
|
301
|
+
});
|
|
302
|
+
return n;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function _topologyChanged(snapshotState, currentHealth) {
|
|
306
|
+
// Compare the topic SET, not just the consumer count — a same-count
|
|
307
|
+
// remap (topics [a,b] → [c,d]) is a real topology change that count-
|
|
308
|
+
// only comparison would miss, defeating refuseOnTopologyChange.
|
|
309
|
+
if (!snapshotState || !currentHealth) return false;
|
|
310
|
+
var snapTopics = new Set((snapshotState.consumers || []).map(function (c) { return c.topic; }));
|
|
311
|
+
var currTopics = new Set((currentHealth.consumers || []).map(function (c) { return c.topic; }));
|
|
312
|
+
if (snapTopics.size !== currTopics.size) return true;
|
|
313
|
+
var changed = false;
|
|
314
|
+
snapTopics.forEach(function (t) { if (!currTopics.has(t)) changed = true; });
|
|
315
|
+
return changed;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function _reshardedShards(snapshotState, currentHealth) {
|
|
319
|
+
// Compute shard topics from snapshot vs current; return the set of
|
|
320
|
+
// topic names whose presence differs.
|
|
321
|
+
var snapTopics = new Set((snapshotState.consumers || []).map(function (c) { return c.topic; }));
|
|
322
|
+
var currTopics = new Set((currentHealth.consumers || []).map(function (c) { return c.topic; }));
|
|
323
|
+
var changed = [];
|
|
324
|
+
snapTopics.forEach(function (t) { if (!currTopics.has(t)) changed.push(t); });
|
|
325
|
+
currTopics.forEach(function (t) { if (!snapTopics.has(t)) changed.push(t); });
|
|
326
|
+
return changed;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function _frameworkVersion() {
|
|
330
|
+
// Read framework version dynamically to avoid a load-time require
|
|
331
|
+
// of package.json (which would couple module-load order to package
|
|
332
|
+
// path). Inline require is gated by allow-marker because the
|
|
333
|
+
// snapshot envelope needs the CURRENT version at the moment of
|
|
334
|
+
// takeSnapshot, not at agent-snapshot.js load time.
|
|
335
|
+
try { return require("../package.json").version; } // allow:inline-require — read at snapshot time, not load time
|
|
336
|
+
catch (_e) { return "unknown"; }
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
module.exports = {
|
|
340
|
+
create: create,
|
|
341
|
+
SCHEMA_VERSION: SCHEMA_VERSION,
|
|
342
|
+
AgentSnapshotError: AgentSnapshotError,
|
|
343
|
+
guards: {
|
|
344
|
+
envelope: guardSnapshotEnvelope,
|
|
345
|
+
},
|
|
346
|
+
};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.agent.trace
|
|
4
|
+
* @nav Agent
|
|
5
|
+
* @title Agent Trace
|
|
6
|
+
* @order 85
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* Distributed tracing through every agent boundary. Composes the
|
|
10
|
+
* existing `b.tracing` (W3C trace context) so operators get a full
|
|
11
|
+
* request waterfall across the agent stack without wiring spans
|
|
12
|
+
* per-handler.
|
|
13
|
+
*
|
|
14
|
+
* The substrate at v0.9.29 ships the integration surface:
|
|
15
|
+
*
|
|
16
|
+
* - `startSpan(name, opts)` — wrap an agent method call in a span
|
|
17
|
+
* - `injectIntoEnvelope(envelope, currentSpan)` — inject W3C
|
|
18
|
+
* `traceparent` + `tracestate` into queue / event-bus / sub-
|
|
19
|
+
* agent envelopes so the consumer can continue the trace
|
|
20
|
+
* - `extractFromEnvelope(envelope)` — parse the envelope's
|
|
21
|
+
* trace context (refused via `b.guardTraceContext` if
|
|
22
|
+
* malformed)
|
|
23
|
+
* - `recordResult(span, result, error?)` — close span with
|
|
24
|
+
* success / error status
|
|
25
|
+
* - `shouldSample(method)` — sampling decision (global +
|
|
26
|
+
* per-method override)
|
|
27
|
+
*
|
|
28
|
+
* Span shape (per method call):
|
|
29
|
+
*
|
|
30
|
+
* name: "<agent-kind>.<method>" // e.g. "mail.agent.search"
|
|
31
|
+
* attributes:
|
|
32
|
+
* agent.method: method name
|
|
33
|
+
* agent.dispatch_mode: "local" | "queue" | "auto"
|
|
34
|
+
* agent.tenant_id: from v0.9.26 tenant scope (if present)
|
|
35
|
+
* agent.posture: JSON-array of v0.9.28 posture set
|
|
36
|
+
* agent.shard: from v0.9.21 shard routing
|
|
37
|
+
* agent.result_status: "success" | "error" | "not_implemented"
|
|
38
|
+
* agent.elapsed_ms: integer
|
|
39
|
+
*
|
|
40
|
+
* ```js
|
|
41
|
+
* var trace = b.agent.trace.create({
|
|
42
|
+
* tracing: b.tracing.create({ instrumentationName: "mail-agent" }),
|
|
43
|
+
* sampleRate: 1.0,
|
|
44
|
+
* perMethod: { fetch: 0.1, search: 0.5, send: 1.0 },
|
|
45
|
+
* });
|
|
46
|
+
*
|
|
47
|
+
* var span = trace.startSpan("mail.agent.fetch", { actor, method: "fetch" });
|
|
48
|
+
* try {
|
|
49
|
+
* var result = await agent.fetch(args);
|
|
50
|
+
* trace.recordResult(span, result);
|
|
51
|
+
* } catch (e) {
|
|
52
|
+
* trace.recordResult(span, null, e);
|
|
53
|
+
* throw e;
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* @card
|
|
58
|
+
* Distributed tracing through every agent boundary. W3C trace
|
|
59
|
+
* context injection at queue / event-bus / sub-agent envelopes;
|
|
60
|
+
* per-method sampling; integrated with existing b.tracing.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
var lazyRequire = require("./lazy-require");
|
|
64
|
+
var { defineClass } = require("./framework-error");
|
|
65
|
+
var guardTraceContext = require("./guard-trace-context");
|
|
66
|
+
|
|
67
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
68
|
+
|
|
69
|
+
var AgentTraceError = defineClass("AgentTraceError", { alwaysPermanent: true });
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @primitive b.agent.trace.create
|
|
73
|
+
* @signature b.agent.trace.create(opts)
|
|
74
|
+
* @since 0.9.29
|
|
75
|
+
* @status stable
|
|
76
|
+
* @related b.tracing.create, b.agent.orchestrator.create
|
|
77
|
+
*
|
|
78
|
+
* Create the trace facade. Composes operator-supplied `b.tracing`
|
|
79
|
+
* instance (or stub if absent — spans become no-ops).
|
|
80
|
+
*
|
|
81
|
+
* @opts
|
|
82
|
+
* tracing: b.tracing instance, // required for live spans
|
|
83
|
+
* audit: b.audit namespace, // optional
|
|
84
|
+
* sampleRate: number in [0..1], // default 1.0
|
|
85
|
+
* perMethod: { <method>: number }, // override per-method
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* var trace = b.agent.trace.create({ tracing: myTracing, sampleRate: 0.5 });
|
|
89
|
+
* var span = trace.startSpan("mail.agent.fetch", { actor });
|
|
90
|
+
*/
|
|
91
|
+
function create(opts) {
|
|
92
|
+
opts = opts || {};
|
|
93
|
+
if (!opts.tracing || typeof opts.tracing !== "object") {
|
|
94
|
+
throw new AgentTraceError("agent-trace/bad-tracing",
|
|
95
|
+
"create: opts.tracing is required (b.tracing.create() result)");
|
|
96
|
+
}
|
|
97
|
+
var sampleRate = typeof opts.sampleRate === "number" ? opts.sampleRate : 1.0;
|
|
98
|
+
if (!isFinite(sampleRate) || sampleRate < 0 || sampleRate > 1) {
|
|
99
|
+
throw new AgentTraceError("agent-trace/bad-sample-rate",
|
|
100
|
+
"create: sampleRate must be in [0, 1]");
|
|
101
|
+
}
|
|
102
|
+
var perMethod = opts.perMethod || {};
|
|
103
|
+
var auditImpl = opts.audit || audit();
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
startSpan: function (name, sopts) { return _startSpan(opts.tracing, name, sopts || {}); },
|
|
107
|
+
injectIntoEnvelope: function (envelope, span) { return _injectIntoEnvelope(opts.tracing, envelope, span); },
|
|
108
|
+
extractFromEnvelope: function (envelope) { return _extractFromEnvelope(envelope); },
|
|
109
|
+
recordResult: function (span, result, error) { return _recordResult(span, result, error); },
|
|
110
|
+
shouldSample: function (method) { return _shouldSample(sampleRate, perMethod, method); },
|
|
111
|
+
formatAttributes: function (info) { return _formatAttributes(info); },
|
|
112
|
+
AgentTraceError: AgentTraceError,
|
|
113
|
+
_ctx: { sampleRate: sampleRate, perMethod: perMethod, audit: auditImpl },
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function _startSpan(tracing, name, sopts) {
|
|
118
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
119
|
+
throw new AgentTraceError("agent-trace/bad-span-name",
|
|
120
|
+
"startSpan: name required");
|
|
121
|
+
}
|
|
122
|
+
// Compose b.tracing's manual-lifetime span — sets the span as active
|
|
123
|
+
// on the registry stack so tracing.contextHeaders() / currentSpan()
|
|
124
|
+
// see it, then exposes end() so the agent boundary controls
|
|
125
|
+
// lifetime across publish → consume.
|
|
126
|
+
if (typeof tracing.manualSpan === "function") {
|
|
127
|
+
return tracing.manualSpan(name, sopts);
|
|
128
|
+
}
|
|
129
|
+
// Operator passed a non-b.tracing object (operator-supplied OTel
|
|
130
|
+
// tracer directly) — try its native startSpan. Refuse if neither.
|
|
131
|
+
if (typeof tracing.startSpan === "function") {
|
|
132
|
+
return tracing.startSpan(name, sopts);
|
|
133
|
+
}
|
|
134
|
+
throw new AgentTraceError("agent-trace/bad-tracing",
|
|
135
|
+
"startSpan: opts.tracing must expose manualSpan() (b.tracing.create()) " +
|
|
136
|
+
"or startSpan() (raw OTel tracer); neither found");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function _injectIntoEnvelope(tracing, envelope, span) {
|
|
140
|
+
if (!envelope || typeof envelope !== "object") {
|
|
141
|
+
throw new AgentTraceError("agent-trace/bad-envelope",
|
|
142
|
+
"injectIntoEnvelope: envelope required");
|
|
143
|
+
}
|
|
144
|
+
// tracing.contextHeaders() returns { traceparent, tracestate? } when
|
|
145
|
+
// a span is active. We pass through whatever's current.
|
|
146
|
+
var headers = (typeof tracing.contextHeaders === "function") ? tracing.contextHeaders() : null;
|
|
147
|
+
if (!headers || typeof headers.traceparent !== "string") return envelope;
|
|
148
|
+
envelope._trace = {
|
|
149
|
+
traceparent: headers.traceparent,
|
|
150
|
+
tracestate: headers.tracestate || "",
|
|
151
|
+
};
|
|
152
|
+
return envelope;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function _extractFromEnvelope(envelope) {
|
|
156
|
+
if (!envelope || typeof envelope !== "object" || !envelope._trace) return null;
|
|
157
|
+
// Validate via guardTraceContext — refuses malformed traceparent
|
|
158
|
+
// strings before the consumer side picks them up as a parent span.
|
|
159
|
+
try {
|
|
160
|
+
guardTraceContext.validate(envelope._trace);
|
|
161
|
+
} catch (e) {
|
|
162
|
+
throw new AgentTraceError("agent-trace/bad-envelope-trace",
|
|
163
|
+
"extractFromEnvelope: " + ((e && e.message) || String(e)));
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
traceparent: envelope._trace.traceparent,
|
|
167
|
+
tracestate: envelope._trace.tracestate || "",
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function _recordResult(span, result, error) {
|
|
172
|
+
if (!span || typeof span !== "object") return;
|
|
173
|
+
if (error) {
|
|
174
|
+
if (typeof span.recordException === "function") {
|
|
175
|
+
try { span.recordException(error); } catch (_e) { /* best-effort */ }
|
|
176
|
+
}
|
|
177
|
+
if (typeof span.setStatus === "function") {
|
|
178
|
+
try { span.setStatus({ code: 2, message: error.message || String(error) }); }
|
|
179
|
+
catch (_e) { /* best-effort */ }
|
|
180
|
+
}
|
|
181
|
+
} else if (typeof span.setStatus === "function") {
|
|
182
|
+
try { span.setStatus({ code: 1 }); } catch (_e) { /* best-effort */ }
|
|
183
|
+
}
|
|
184
|
+
if (typeof span.end === "function") {
|
|
185
|
+
try { span.end(); } catch (_e) { /* best-effort */ }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function _shouldSample(globalRate, perMethod, method) {
|
|
190
|
+
if (typeof method === "string" && Object.prototype.hasOwnProperty.call(perMethod, method)) {
|
|
191
|
+
var r = perMethod[method];
|
|
192
|
+
if (typeof r === "number" && isFinite(r) && r >= 0 && r <= 1) {
|
|
193
|
+
return Math.random() < r; // allow:math-random-noncrypto — sampling is statistical, not security-sensitive
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return Math.random() < globalRate; // allow:math-random-noncrypto — sampling is statistical, not security-sensitive
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function _formatAttributes(info) {
|
|
200
|
+
if (!info || typeof info !== "object") return {};
|
|
201
|
+
var attrs = {};
|
|
202
|
+
if (info.method) attrs["agent.method"] = info.method;
|
|
203
|
+
if (info.dispatchMode) attrs["agent.dispatch_mode"] = info.dispatchMode;
|
|
204
|
+
if (info.tenantId) attrs["agent.tenant_id"] = info.tenantId;
|
|
205
|
+
if (Array.isArray(info.postureSet)) attrs["agent.posture"] = JSON.stringify(info.postureSet);
|
|
206
|
+
if (typeof info.shard === "number") attrs["agent.shard"] = info.shard;
|
|
207
|
+
if (info.resultStatus) attrs["agent.result_status"] = info.resultStatus;
|
|
208
|
+
if (typeof info.elapsedMs === "number") attrs["agent.elapsed_ms"] = info.elapsedMs;
|
|
209
|
+
return attrs;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = {
|
|
213
|
+
create: create,
|
|
214
|
+
AgentTraceError: AgentTraceError,
|
|
215
|
+
guards: {
|
|
216
|
+
context: guardTraceContext,
|
|
217
|
+
},
|
|
218
|
+
};
|