@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/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
- agent: { orchestrator: agentOrchestrator, idempotency: agentIdempotency, stream: agentStream, eventBus: agentEventBus, tenant: agentTenant, saga: agentSaga, postureChain: agentPostureChain },
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
+ };
package/lib/guard-all.js CHANGED
@@ -83,6 +83,7 @@ var STANDALONE_GUARDS = [
83
83
  require("./guard-image"),
84
84
  require("./guard-pdf"),
85
85
  require("./guard-auth"),
86
+ require("./guard-smtp-command"),
86
87
  ];
87
88
 
88
89
  // Framework-wide profile + posture vocabulary that every guard MUST