agent-first-data 0.15.0 → 0.16.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.
package/dist/format.js CHANGED
@@ -1,38 +1,533 @@
1
1
  /**
2
2
  * AFDATA output formatting and protocol templates.
3
3
  *
4
- * 21 public APIs and 4 types: protocol builders, value redactors (copy and
5
- * in-place; cover _secret and _url fields), output formatters, URL-string
6
- * redactors (redactUrlSecrets / WithOptions), parseSize, normalizeUtcOffset,
7
- * isValidRfc3339Date, isValidRfc3339Time, RedactionPolicy, RedactionOptions,
8
- * OutputStyle, and OutputOptions.
4
+ * Public APIs include protocol event builders, redactedValue (covers _secret
5
+ * and _url fields), output formatters, redactUrlSecrets, parseSize,
6
+ * normalizeUtcOffset, isValidRfc3339Date, isValidRfc3339Time,
7
+ * RedactionPolicy, OutputStyle, and OutputOptions.
9
8
  */
10
9
  // ═══════════════════════════════════════════
11
- // Public API: Protocol Builders
10
+ // Event type: opaque branded wrapper
12
11
  // ═══════════════════════════════════════════
13
- /** Build {code: "ok", result, trace?}. */
14
- export function buildJsonOk(result, trace) {
15
- const m = { code: "ok", result };
16
- if (trace !== undefined)
17
- m.trace = trace;
18
- return m;
19
- }
20
- /** Build {code: "error", error: message, hint?, trace?}. */
21
- export function buildJsonError(message, hint, trace) {
22
- const m = { code: "error", error: message };
23
- if (hint !== undefined)
24
- m.hint = hint;
25
- if (trace !== undefined)
26
- m.trace = trace;
27
- return m;
28
- }
29
- /** Build {code: "<custom>", ...fields, trace?}. */
30
- export function buildJson(code, fields, trace) {
31
- const result = isObject(fields) ? { ...fields } : {};
32
- result.code = code;
33
- if (trace !== undefined)
34
- result.trace = trace;
35
- return result;
12
+ /**
13
+ * Event is an opaque type wrapping a protocol event envelope.
14
+ * Guaranteed to be strict-valid by construction.
15
+ */
16
+ export class Event {
17
+ value;
18
+ __brand = "Event";
19
+ constructor(value) {
20
+ this.value = value;
21
+ }
22
+ toJSON() {
23
+ return this.value;
24
+ }
25
+ valueOf() {
26
+ return this.value;
27
+ }
28
+ }
29
+ // ═══════════════════════════════════════════
30
+ // EventBuildError: typed error from .build()
31
+ // ═══════════════════════════════════════════
32
+ export class EventBuildError extends Error {
33
+ issues;
34
+ constructor(message, issues) {
35
+ super(message);
36
+ this.issues = issues;
37
+ this.name = "EventBuildError";
38
+ }
39
+ }
40
+ // ═══════════════════════════════════════════
41
+ // Public API: Protocol v1 Builders and Validation
42
+ // ═══════════════════════════════════════════
43
+ // ═══════════════════════════════════════════
44
+ // Fluent builders
45
+ // ═══════════════════════════════════════════
46
+ export class JsonResultBuilder {
47
+ payload;
48
+ traceValue = {};
49
+ issues = [];
50
+ constructor(payload) {
51
+ this.payload = payload;
52
+ }
53
+ trace(value) {
54
+ if (value !== undefined) {
55
+ if (!isObject(value)) {
56
+ this.issues.push("trace must be a JSON object");
57
+ }
58
+ else {
59
+ this.traceValue = value;
60
+ }
61
+ }
62
+ return this;
63
+ }
64
+ build() {
65
+ if (this.issues.length > 0) {
66
+ throw new EventBuildError(`Failed to build result event: ${this.issues.join("; ")}`, this.issues);
67
+ }
68
+ const m = { kind: "result", result: this.payload, trace: this.traceValue };
69
+ return new Event(m);
70
+ }
71
+ }
72
+ export class JsonErrorBuilder {
73
+ code;
74
+ message;
75
+ retryableValue = false;
76
+ hintValue;
77
+ extensionFields = {};
78
+ traceValue = {};
79
+ issues = [];
80
+ constructor(code, message) {
81
+ this.code = code;
82
+ this.message = message;
83
+ if (!code || code === "")
84
+ this.issues.push("code must be a non-empty string");
85
+ if (!message || message === "")
86
+ this.issues.push("message must be a non-empty string");
87
+ }
88
+ retryable() {
89
+ this.retryableValue = true;
90
+ return this;
91
+ }
92
+ retryableIf(flag) {
93
+ this.retryableValue = flag;
94
+ return this;
95
+ }
96
+ hint(value) {
97
+ if (!value || value === "") {
98
+ this.issues.push("hint must be a non-empty string");
99
+ }
100
+ else {
101
+ this.hintValue = value;
102
+ }
103
+ return this;
104
+ }
105
+ hintIfSome(value) {
106
+ if (value) {
107
+ return this.hint(value);
108
+ }
109
+ return this;
110
+ }
111
+ field(name, value) {
112
+ if (this.isReservedErrorField(name)) {
113
+ this.issues.push(`cannot write reserved error field ${JSON.stringify(name)}`);
114
+ }
115
+ else {
116
+ this.extensionFields[name] = value;
117
+ }
118
+ return this;
119
+ }
120
+ fields(obj) {
121
+ if (!isObject(obj)) {
122
+ this.issues.push("fields must be a JSON object");
123
+ return this;
124
+ }
125
+ for (const [key, val] of Object.entries(obj)) {
126
+ if (this.isReservedErrorField(key)) {
127
+ this.issues.push(`cannot write reserved error field ${JSON.stringify(key)}`);
128
+ }
129
+ else {
130
+ this.extensionFields[key] = val;
131
+ }
132
+ }
133
+ return this;
134
+ }
135
+ extend(obj) {
136
+ if (!isObject(obj)) {
137
+ this.issues.push("extend must be a JSON object");
138
+ return this;
139
+ }
140
+ for (const [key, val] of Object.entries(obj)) {
141
+ if (this.isReservedErrorField(key)) {
142
+ this.issues.push(`cannot write reserved error field ${JSON.stringify(key)}`);
143
+ }
144
+ else {
145
+ this.extensionFields[key] = val;
146
+ }
147
+ }
148
+ return this;
149
+ }
150
+ trace(value) {
151
+ if (value !== undefined) {
152
+ if (!isObject(value)) {
153
+ this.issues.push("trace must be a JSON object");
154
+ }
155
+ else {
156
+ this.traceValue = value;
157
+ }
158
+ }
159
+ return this;
160
+ }
161
+ isReservedErrorField(name) {
162
+ return name === "code" || name === "message" || name === "hint" || name === "retryable";
163
+ }
164
+ build() {
165
+ if (this.issues.length > 0) {
166
+ throw new EventBuildError(`Failed to build error event: ${this.issues.join("; ")}`, this.issues);
167
+ }
168
+ const error = { ...this.extensionFields };
169
+ error.code = this.code;
170
+ error.message = this.message;
171
+ error.retryable = this.retryableValue;
172
+ if (this.hintValue !== undefined)
173
+ error.hint = this.hintValue;
174
+ const m = { kind: "error", error, trace: this.traceValue };
175
+ return new Event(m);
176
+ }
177
+ }
178
+ export class JsonProgressBuilder {
179
+ message;
180
+ extensionFields = {};
181
+ traceValue = {};
182
+ issues = [];
183
+ constructor(message) {
184
+ this.message = message;
185
+ if (!message || message === "")
186
+ this.issues.push("message must be a non-empty string");
187
+ }
188
+ field(name, value) {
189
+ if (this.isReservedProgressField(name)) {
190
+ this.issues.push(`cannot write reserved progress field ${JSON.stringify(name)}`);
191
+ }
192
+ else {
193
+ this.extensionFields[name] = value;
194
+ }
195
+ return this;
196
+ }
197
+ fields(obj) {
198
+ if (!isObject(obj)) {
199
+ this.issues.push("fields must be a JSON object");
200
+ return this;
201
+ }
202
+ for (const [key, val] of Object.entries(obj)) {
203
+ if (this.isReservedProgressField(key)) {
204
+ this.issues.push(`cannot write reserved progress field ${JSON.stringify(key)}`);
205
+ }
206
+ else {
207
+ this.extensionFields[key] = val;
208
+ }
209
+ }
210
+ return this;
211
+ }
212
+ extend(obj) {
213
+ if (!isObject(obj)) {
214
+ this.issues.push("extend must be a JSON object");
215
+ return this;
216
+ }
217
+ for (const [key, val] of Object.entries(obj)) {
218
+ if (this.isReservedProgressField(key)) {
219
+ this.issues.push(`cannot write reserved progress field ${JSON.stringify(key)}`);
220
+ }
221
+ else {
222
+ this.extensionFields[key] = val;
223
+ }
224
+ }
225
+ return this;
226
+ }
227
+ trace(value) {
228
+ if (value !== undefined) {
229
+ if (!isObject(value)) {
230
+ this.issues.push("trace must be a JSON object");
231
+ }
232
+ else {
233
+ this.traceValue = value;
234
+ }
235
+ }
236
+ return this;
237
+ }
238
+ isReservedProgressField(name) {
239
+ return name === "message";
240
+ }
241
+ build() {
242
+ if (this.issues.length > 0) {
243
+ throw new EventBuildError(`Failed to build progress event: ${this.issues.join("; ")}`, this.issues);
244
+ }
245
+ const progress = { ...this.extensionFields };
246
+ progress.message = this.message;
247
+ const m = { kind: "progress", progress, trace: this.traceValue };
248
+ return new Event(m);
249
+ }
250
+ }
251
+ export class JsonLogBuilder {
252
+ level;
253
+ message;
254
+ extensionFields = {};
255
+ traceValue = {};
256
+ issues = [];
257
+ constructor(level, message) {
258
+ this.level = level;
259
+ this.message = message;
260
+ if (!message || message === "")
261
+ this.issues.push("message must be a non-empty string");
262
+ if (!isValidLogLevel(level))
263
+ this.issues.push(`level must be one of debug, info, warn, error`);
264
+ }
265
+ field(name, value) {
266
+ if (this.isReservedLogField(name)) {
267
+ this.issues.push(`cannot write reserved log field ${JSON.stringify(name)}`);
268
+ }
269
+ else {
270
+ this.extensionFields[name] = value;
271
+ }
272
+ return this;
273
+ }
274
+ fields(obj) {
275
+ if (!isObject(obj)) {
276
+ this.issues.push("fields must be a JSON object");
277
+ return this;
278
+ }
279
+ for (const [key, val] of Object.entries(obj)) {
280
+ if (this.isReservedLogField(key)) {
281
+ this.issues.push(`cannot write reserved log field ${JSON.stringify(key)}`);
282
+ }
283
+ else {
284
+ this.extensionFields[key] = val;
285
+ }
286
+ }
287
+ return this;
288
+ }
289
+ extend(obj) {
290
+ if (!isObject(obj)) {
291
+ this.issues.push("extend must be a JSON object");
292
+ return this;
293
+ }
294
+ for (const [key, val] of Object.entries(obj)) {
295
+ if (this.isReservedLogField(key)) {
296
+ this.issues.push(`cannot write reserved log field ${JSON.stringify(key)}`);
297
+ }
298
+ else {
299
+ this.extensionFields[key] = val;
300
+ }
301
+ }
302
+ return this;
303
+ }
304
+ trace(value) {
305
+ if (value !== undefined) {
306
+ if (!isObject(value)) {
307
+ this.issues.push("trace must be a JSON object");
308
+ }
309
+ else {
310
+ this.traceValue = value;
311
+ }
312
+ }
313
+ return this;
314
+ }
315
+ isReservedLogField(name) {
316
+ return name === "message" || name === "level" || name === "code";
317
+ }
318
+ build() {
319
+ if (this.issues.length > 0) {
320
+ throw new EventBuildError(`Failed to build log event: ${this.issues.join("; ")}`, this.issues);
321
+ }
322
+ const log = { ...this.extensionFields };
323
+ log.level = this.level;
324
+ log.message = this.message;
325
+ const m = { kind: "log", log, trace: this.traceValue };
326
+ return new Event(m);
327
+ }
328
+ }
329
+ /** Fluent builder for result events. */
330
+ export function jsonResult(payload) {
331
+ return new JsonResultBuilder(payload);
332
+ }
333
+ /** Fluent builder for error events. */
334
+ export function jsonError(code, message) {
335
+ return new JsonErrorBuilder(code, message);
336
+ }
337
+ /** Fluent builder for progress events. */
338
+ export function jsonProgress(message) {
339
+ return new JsonProgressBuilder(message);
340
+ }
341
+ /** Fluent builder for log events. */
342
+ export function jsonLog(level, message) {
343
+ return new JsonLogBuilder(level, message);
344
+ }
345
+ function isValidLogLevel(level) {
346
+ return level === "debug" || level === "info" || level === "warn" || level === "error";
347
+ }
348
+ /** Validate one protocol event envelope. `strict` also enforces the recommended strict protocol profile (default true). */
349
+ export function validateProtocolEvent(event, strict = true) {
350
+ if (!isObject(event)) {
351
+ throw new Error("event must be a JSON object");
352
+ }
353
+ const kind = event.kind;
354
+ if (kind !== "result" && kind !== "error" && kind !== "progress" && kind !== "log") {
355
+ throw new Error("event.kind must be one of result, error, progress, log");
356
+ }
357
+ if (!(kind in event)) {
358
+ throw new Error(`event payload field ${JSON.stringify(kind)} is required`);
359
+ }
360
+ for (const key of Object.keys(event)) {
361
+ if (key !== "kind" && key !== kind && key !== "trace") {
362
+ throw new Error(`unexpected top-level field ${JSON.stringify(key)}`);
363
+ }
364
+ }
365
+ if ("trace" in event && !isObject(event.trace)) {
366
+ throw new Error("event.trace must be a JSON object when present");
367
+ }
368
+ if (kind === "error") {
369
+ validateErrorPayload(event.error);
370
+ }
371
+ if (!strict)
372
+ return;
373
+ if (!isObject(event.trace))
374
+ throw new Error("event.trace is required in strict mode");
375
+ if (kind === "log")
376
+ validateStrictLog(event.log);
377
+ if (kind === "progress")
378
+ validateStrictProgress(event.progress);
379
+ if (kind === "error")
380
+ validateStrictError(event.error);
381
+ }
382
+ function validateErrorPayload(error) {
383
+ if (!isObject(error)) {
384
+ throw new Error("event.error must be a JSON object");
385
+ }
386
+ if (typeof error.code !== "string" || error.code === "") {
387
+ throw new Error("event.error.code must be a non-empty string");
388
+ }
389
+ if (typeof error.message !== "string" || error.message === "") {
390
+ throw new Error("event.error.message must be a non-empty string");
391
+ }
392
+ if ("hint" in error && typeof error.hint !== "string") {
393
+ throw new Error("event.error.hint must be a string when present");
394
+ }
395
+ }
396
+ /** Validate finite CLI lifecycle: (log | progress)* -> exactly one terminal. `strict` also enforces the recommended strict protocol profile (default true). */
397
+ export function validateProtocolStream(events, strict = true) {
398
+ let terminalSeen = false;
399
+ events.forEach((event, idx) => {
400
+ try {
401
+ validateProtocolEvent(event, strict);
402
+ }
403
+ catch (e) {
404
+ throw new Error(`event ${idx}: ${e.message}`);
405
+ }
406
+ const kind = event.kind;
407
+ if (kind === "log" || kind === "progress") {
408
+ if (terminalSeen)
409
+ throw new Error(`event ${idx}: non-terminal event after terminal`);
410
+ }
411
+ else if (kind === "result" || kind === "error") {
412
+ if (terminalSeen)
413
+ throw new Error(`event ${idx}: duplicate terminal event`);
414
+ terminalSeen = true;
415
+ }
416
+ });
417
+ if (!terminalSeen) {
418
+ throw new Error("event stream must contain exactly one terminal result or error");
419
+ }
420
+ }
421
+ function validateStrictLog(payload) {
422
+ if (!isObject(payload))
423
+ throw new Error("event.log must be a JSON object in strict mode");
424
+ // 0.16: log payload MUST NOT contain code field
425
+ if ("code" in payload) {
426
+ throw new Error("event.log.code is not allowed in strict mode");
427
+ }
428
+ if (typeof payload.message !== "string" || payload.message === "") {
429
+ throw new Error("event.log.message must be a non-empty string in strict mode");
430
+ }
431
+ if (!['debug', 'info', 'warn', 'error'].includes(payload.level)) {
432
+ throw new Error("event.log.level must be one of debug, info, warn, error in strict mode");
433
+ }
434
+ }
435
+ function validateStrictProgress(payload) {
436
+ if (!isObject(payload) || typeof payload.message !== "string" || payload.message === "") {
437
+ throw new Error("event.progress.message must be a non-empty string in strict mode");
438
+ }
439
+ }
440
+ function validateStrictError(error) {
441
+ if (!isObject(error))
442
+ throw new Error("event.error must be a JSON object in strict mode");
443
+ if (typeof error.code !== "string" || error.code === "") {
444
+ throw new Error("event.error.code must be a non-empty string in strict mode");
445
+ }
446
+ if (typeof error.message !== "string" || error.message === "") {
447
+ throw new Error("event.error.message must be a non-empty string in strict mode");
448
+ }
449
+ if (typeof error.retryable !== "boolean") {
450
+ throw new Error("event.error.retryable must be a boolean in strict mode");
451
+ }
452
+ if ("hint" in error && typeof error.hint !== "string") {
453
+ throw new Error("event.error.hint must be a string when present in strict mode");
454
+ }
455
+ }
456
+ /** Thrown by decodeProtocolEvent for malformed JSON or a strict-invalid event. */
457
+ export class EventDecodeError extends Error {
458
+ constructor(message) {
459
+ super(message);
460
+ this.name = "EventDecodeError";
461
+ }
462
+ }
463
+ const ERROR_RESERVED_FIELDS = new Set(["code", "message", "hint", "retryable"]);
464
+ const PROGRESS_RESERVED_FIELDS = new Set(["message"]);
465
+ const LOG_RESERVED_FIELDS = new Set(["level", "message"]);
466
+ /**
467
+ * Parse one protocol line (a single JSON text value), strict-validate it, and
468
+ * return a typed decoded event. Extension fields are every payload key beyond
469
+ * the required ones, collected into `fields`.
470
+ */
471
+ export function decodeProtocolEvent(text) {
472
+ let parsed;
473
+ try {
474
+ parsed = JSON.parse(text);
475
+ }
476
+ catch (e) {
477
+ throw new EventDecodeError(`invalid JSON: ${e.message}`);
478
+ }
479
+ try {
480
+ validateProtocolEvent(parsed, true);
481
+ }
482
+ catch (e) {
483
+ throw new EventDecodeError(e.message);
484
+ }
485
+ const event = parsed;
486
+ const trace = isObject(event.trace) ? event.trace : undefined;
487
+ switch (event.kind) {
488
+ case "result":
489
+ return { kind: "result", result: event.result, trace };
490
+ case "error": {
491
+ const error = event.error;
492
+ const fields = {};
493
+ for (const [k, v] of Object.entries(error)) {
494
+ if (!ERROR_RESERVED_FIELDS.has(k))
495
+ fields[k] = v;
496
+ }
497
+ const decoded = {
498
+ kind: "error",
499
+ code: error.code,
500
+ message: error.message,
501
+ retryable: error.retryable,
502
+ fields,
503
+ trace,
504
+ };
505
+ if (typeof error.hint === "string")
506
+ decoded.hint = error.hint;
507
+ return decoded;
508
+ }
509
+ case "progress": {
510
+ const progress = event.progress;
511
+ const fields = {};
512
+ for (const [k, v] of Object.entries(progress)) {
513
+ if (!PROGRESS_RESERVED_FIELDS.has(k))
514
+ fields[k] = v;
515
+ }
516
+ return { kind: "progress", message: progress.message, fields, trace };
517
+ }
518
+ case "log": {
519
+ const log = event.log;
520
+ const fields = {};
521
+ for (const [k, v] of Object.entries(log)) {
522
+ if (!LOG_RESERVED_FIELDS.has(k))
523
+ fields[k] = v;
524
+ }
525
+ return { kind: "log", level: log.level, message: log.message, fields, trace };
526
+ }
527
+ default:
528
+ // Unreachable: validateProtocolEvent already constrains event.kind.
529
+ throw new EventDecodeError(`unsupported event kind ${JSON.stringify(event.kind)}`);
530
+ }
36
531
  }
37
532
  // ═══════════════════════════════════════════
38
533
  // Public API: Output Formatters
@@ -47,47 +542,38 @@ export var OutputStyle;
47
542
  OutputStyle["Readable"] = "Readable";
48
543
  OutputStyle["Raw"] = "Raw";
49
544
  })(OutputStyle || (OutputStyle = {}));
50
- /** Format as single-line JSON. Secrets redacted, original keys, raw values. */
51
- export function outputJson(value) {
52
- return JSON.stringify(redactedValue(value));
545
+ /** Convenience: build OutputOptions scoped to a RedactionPolicy. */
546
+ export function outputOptionsForPolicy(policy) {
547
+ return { redaction: { policy } };
53
548
  }
54
- /** Format as single-line JSON with explicit redaction policy. */
55
- export function outputJsonWith(value, redactionPolicy) {
56
- return JSON.stringify(redactedValueWith(value, redactionPolicy));
57
- }
58
- /** Format as single-line JSON with explicit output options. */
59
- export function outputJsonWithOptions(value, outputOptions) {
60
- return JSON.stringify(redactedValueWithOptions(value, outputOptions.redaction ?? {}));
549
+ /** Format as single-line JSON. Secrets redacted, original keys, raw values. */
550
+ export function outputJson(value, options = {}) {
551
+ const unwrapped = value instanceof Event ? value.toJSON() : value;
552
+ return JSON.stringify(redactedValue(unwrapped, options.redaction ?? {}));
61
553
  }
62
554
  /** Format as multi-line YAML. Keys stripped, values formatted, secrets redacted. */
63
- export function outputYaml(value) {
64
- return outputYamlWithOptions(value, {});
65
- }
66
- /** Format as multi-line YAML with explicit output options. */
67
- export function outputYamlWithOptions(value, outputOptions) {
68
- value = redactedValueWithOptions(value, outputOptions.redaction ?? {});
555
+ export function outputYaml(value, options = {}) {
556
+ const unwrapped = value instanceof Event ? value.toJSON() : value;
557
+ const redacted = redactedValue(unwrapped, options.redaction ?? {});
69
558
  const lines = ["---"];
70
- if (outputOptions.style === OutputStyle.Raw) {
71
- renderYamlRaw(value, 0, lines);
559
+ if (options.style === OutputStyle.Raw) {
560
+ renderYamlRaw(redacted, 0, lines);
72
561
  }
73
562
  else {
74
- renderYamlProcessed(value, 0, lines);
563
+ renderYamlProcessed(redacted, 0, lines);
75
564
  }
76
565
  return lines.join("\n");
77
566
  }
78
567
  /** Format as single-line logfmt. Keys stripped, values formatted, secrets redacted. */
79
- export function outputPlain(value) {
80
- return outputPlainWithOptions(value, {});
81
- }
82
- /** Format as single-line logfmt with explicit output options. */
83
- export function outputPlainWithOptions(value, outputOptions) {
84
- value = redactedValueWithOptions(value, outputOptions.redaction ?? {});
568
+ export function outputPlain(value, options = {}) {
569
+ const unwrapped = value instanceof Event ? value.toJSON() : value;
570
+ const redacted = redactedValue(unwrapped, options.redaction ?? {});
85
571
  const pairs = [];
86
- if (outputOptions.style === OutputStyle.Raw) {
87
- collectPlainPairsRaw(value, "", pairs);
572
+ if (options.style === OutputStyle.Raw) {
573
+ collectPlainPairsRaw(redacted, "", pairs);
88
574
  }
89
575
  else {
90
- collectPlainPairs(value, "", pairs);
576
+ collectPlainPairs(redacted, "", pairs);
91
577
  }
92
578
  pairs.sort(([a], [b]) => jcsCompare(a, b));
93
579
  return pairs
@@ -97,41 +583,12 @@ export function outputPlainWithOptions(value, outputOptions) {
97
583
  // ═══════════════════════════════════════════
98
584
  // Public API: Redaction & Utility
99
585
  // ═══════════════════════════════════════════
100
- /** Redact _secret fields in-place. */
101
- export function redactSecretsInPlace(value) {
102
- redactSecrets(value);
103
- }
104
- /** Redact secret fields in-place using explicit redaction options. */
105
- export function redactSecretsInPlaceWithOptions(value, redactionOptions) {
106
- applyRedactionOptions(value, redactionOptions);
107
- }
108
- /** Return a JSON-safe copy with default _secret redaction applied. */
109
- export function redactedValue(value) {
110
- const v = sanitizeForJson(value);
111
- redactSecrets(v);
112
- return v;
113
- }
114
- /** Return a JSON-safe copy with an explicit redaction policy applied. */
115
- export function redactedValueWith(value, redactionPolicy) {
586
+ /** Return a JSON-safe copy with redaction options applied (default: full _secret redaction). */
587
+ export function redactedValue(value, options) {
116
588
  const v = sanitizeForJson(value);
117
- applyRedactionPolicy(v, redactionPolicy);
118
- return v;
119
- }
120
- /** Return a JSON-safe copy with explicit redaction options applied. */
121
- export function redactedValueWithOptions(value, redactionOptions) {
122
- const v = sanitizeForJson(value);
123
- applyRedactionOptions(v, redactionOptions);
589
+ applyRedactionOptions(v, options ?? {});
124
590
  return v;
125
591
  }
126
- /**
127
- * Redact secret components of a single URL string, using default options.
128
- *
129
- * Returns url with its userinfo password and any _secret-suffixed query
130
- * parameter values replaced by "***". See redactUrlSecretsWithOptions.
131
- */
132
- export function redactUrlSecrets(url) {
133
- return redactUrlSecretsWithOptions(url, {});
134
- }
135
592
  /**
136
593
  * Redact secret components of a single URL string.
137
594
  *
@@ -142,36 +599,36 @@ export function redactUrlSecrets(url) {
142
599
  * A string that is not a single, whitespace-free, scheme-prefixed URL
143
600
  * (including a URL embedded in surrounding prose) is returned unchanged.
144
601
  */
145
- export function redactUrlSecretsWithOptions(url, redactionOptions) {
146
- const redacted = redactUrlInStr(url, secretNameSet(redactionOptions));
602
+ export function redactUrlSecrets(url, options) {
603
+ const redacted = redactUrlInStr(url, secretNameSet(options ?? {}));
147
604
  return redacted ?? url;
148
605
  }
149
606
  /**
150
607
  * Parse a human-readable size string into bytes.
151
- * Accepts bare numbers or numbers followed by a unit letter (B/K/M/G/T).
152
- * Case-insensitive. Trims whitespace. Returns null for invalid input.
608
+ * Accepts numbers followed by explicit units. Decimal units are
609
+ * B/kB/MB/GB/TB; binary units are KiB/MiB/GiB/TiB.
610
+ * Trims whitespace and rejects ambiguous K/M/G/T units.
153
611
  */
154
612
  export function parseSize(s) {
155
613
  s = s.trim();
156
614
  if (!s)
157
615
  return null;
158
616
  const multipliers = {
159
- b: 1, k: 1024, m: 1024 ** 2, g: 1024 ** 3, t: 1024 ** 4,
617
+ KiB: 1024,
618
+ MiB: 1024 ** 2,
619
+ GiB: 1024 ** 3,
620
+ TiB: 1024 ** 4,
621
+ kB: 1000,
622
+ MB: 1000 ** 2,
623
+ GB: 1000 ** 3,
624
+ TB: 1000 ** 4,
625
+ B: 1,
160
626
  };
161
- const last = s[s.length - 1].toLowerCase();
162
- let numStr;
163
- let mult;
164
- if (last in multipliers) {
165
- numStr = s.slice(0, -1);
166
- mult = multipliers[last];
167
- }
168
- else if ((last >= "0" && last <= "9") || last === ".") {
169
- numStr = s;
170
- mult = 1;
171
- }
172
- else {
627
+ const unit = Object.keys(multipliers).find((candidate) => s.endsWith(candidate));
628
+ if (unit === undefined)
173
629
  return null;
174
- }
630
+ const numStr = s.slice(0, -unit.length);
631
+ const mult = multipliers[unit];
175
632
  if (!numStr || !/^(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(numStr))
176
633
  return null;
177
634
  const n = Number(numStr);
@@ -287,6 +744,7 @@ function isSecretKey(key, secretNames) {
287
744
  return keyHasSecretSuffix(key) || secretNames.has(key);
288
745
  }
289
746
  const MAX_DEPTH = 256;
747
+ const MAX_DEPTH_MARKER = "<afdata:max-depth>";
290
748
  function redactSecrets(value, context = DEFAULT_CONTEXT, depth = 0) {
291
749
  if (depth >= MAX_DEPTH)
292
750
  return;
@@ -301,26 +759,23 @@ function redactSecrets(value, context = DEFAULT_CONTEXT, depth = 0) {
301
759
  value[k] = redactUrlFieldValue(v, context.secretNames);
302
760
  }
303
761
  else {
304
- value[k] = depth + 1 >= MAX_DEPTH ? "***" : (redactSecrets(v, context, depth + 1), v);
762
+ value[k] = depth + 1 >= MAX_DEPTH ? MAX_DEPTH_MARKER : (redactSecrets(v, context, depth + 1), v);
305
763
  }
306
764
  }
307
765
  else {
308
- value[k] = depth + 1 >= MAX_DEPTH ? "***" : (redactSecrets(v, context, depth + 1), v);
766
+ value[k] = depth + 1 >= MAX_DEPTH ? MAX_DEPTH_MARKER : (redactSecrets(v, context, depth + 1), v);
309
767
  }
310
768
  }
311
769
  }
312
770
  else if (Array.isArray(value)) {
313
771
  for (let i = 0; i < value.length; i++) {
314
772
  if (depth + 1 >= MAX_DEPTH)
315
- value[i] = "***";
773
+ value[i] = MAX_DEPTH_MARKER;
316
774
  else
317
775
  redactSecrets(value[i], context, depth + 1);
318
776
  }
319
777
  }
320
778
  }
321
- function applyRedactionPolicy(value, redactionPolicy) {
322
- applyRedactionPolicyWithContext(value, redactionPolicy, DEFAULT_CONTEXT);
323
- }
324
779
  function applyRedactionOptions(value, redactionOptions) {
325
780
  applyRedactionPolicyWithContext(value, redactionOptions.policy, contextFromOptions(redactionOptions));
326
781
  }
@@ -516,9 +971,45 @@ function tryStripGenericCents(key) {
516
971
  return null;
517
972
  return [stripped, code];
518
973
  }
974
+ function tryStripGenericMicro(key) {
975
+ const code = extractCurrencyCodeMicro(key);
976
+ if (code === null)
977
+ return null;
978
+ const suffixLen = code.length + "_micro".length + 1; // _{code}_micro
979
+ const stripped = key.slice(0, -suffixLen);
980
+ if (!stripped)
981
+ return null;
982
+ return [stripped, code];
983
+ }
519
984
  function isInt(value) {
520
985
  return typeof value === "number" && Number.isInteger(value);
521
986
  }
987
+ function decimalIntText(value) {
988
+ if (typeof value === "string" && /^-?\d+$/.test(value))
989
+ return value;
990
+ if (isInt(value))
991
+ return String(value);
992
+ return null;
993
+ }
994
+ function epochNsToMs(value) {
995
+ const text = decimalIntText(value);
996
+ if (text === null)
997
+ return null;
998
+ try {
999
+ const ns = BigInt(text);
1000
+ const divisor = 1000000n;
1001
+ let ms = ns / divisor;
1002
+ if (ns < 0n && ns % divisor !== 0n)
1003
+ ms -= 1n;
1004
+ const asNumber = Number(ms);
1005
+ if (!Number.isSafeInteger(asNumber))
1006
+ return null;
1007
+ return asNumber;
1008
+ }
1009
+ catch {
1010
+ return null;
1011
+ }
1012
+ }
522
1013
  function isNum(value) {
523
1014
  return typeof value === "number";
524
1015
  }
@@ -545,8 +1036,9 @@ function tryProcessField(key, value) {
545
1036
  }
546
1037
  stripped = stripSuffixCI(key, "_epoch_ns");
547
1038
  if (stripped !== null) {
548
- if (isInt(value)) {
549
- const formatted = formatRfc3339Ms(Math.floor(value / 1_000_000));
1039
+ const ms = epochNsToMs(value);
1040
+ if (ms !== null) {
1041
+ const formatted = formatRfc3339Ms(ms);
550
1042
  if (formatted !== null)
551
1043
  return [stripped, formatted];
552
1044
  }
@@ -572,6 +1064,13 @@ function tryProcessField(key, value) {
572
1064
  return [gcStripped, `${Math.floor(value / 100)}.${String(value % 100).padStart(2, "0")} ${code.toUpperCase()}`];
573
1065
  return null;
574
1066
  }
1067
+ const gm = tryStripGenericMicro(key);
1068
+ if (gm !== null) {
1069
+ const [gmStripped, code] = gm;
1070
+ if (isInt(value) && value >= 0)
1071
+ return [gmStripped, `${Math.floor(value / 1_000_000)}.${String(value % 1_000_000).padStart(6, "0")} ${code.toUpperCase()}`];
1072
+ return null;
1073
+ }
575
1074
  // Group 3: multi-char suffixes
576
1075
  stripped = stripSuffixCI(key, "_rfc3339");
577
1076
  if (stripped !== null) {
@@ -600,19 +1099,21 @@ function tryProcessField(key, value) {
600
1099
  // Group 4: single-unit suffixes
601
1100
  stripped = stripSuffixCI(key, "_msats");
602
1101
  if (stripped !== null) {
603
- if (isNum(value))
604
- return [stripped, `${plainScalar(value)}msats`];
1102
+ const text = decimalIntText(value);
1103
+ if (text !== null)
1104
+ return [stripped, `${text}msats`];
605
1105
  return null;
606
1106
  }
607
1107
  stripped = stripSuffixCI(key, "_sats");
608
1108
  if (stripped !== null) {
609
- if (isNum(value))
610
- return [stripped, `${plainScalar(value)}sats`];
1109
+ const text = decimalIntText(value);
1110
+ if (text !== null)
1111
+ return [stripped, `${text}sats`];
611
1112
  return null;
612
1113
  }
613
1114
  stripped = stripSuffixCI(key, "_bytes");
614
1115
  if (stripped !== null) {
615
- if (isInt(value))
1116
+ if (isInt(value) && value >= 0)
616
1117
  return [stripped, formatBytesHuman(value)];
617
1118
  return null;
618
1119
  }
@@ -623,12 +1124,6 @@ function tryProcessField(key, value) {
623
1124
  return null;
624
1125
  }
625
1126
  // Group 5: short suffixes (last to avoid false positives)
626
- stripped = stripSuffixCI(key, "_btc");
627
- if (stripped !== null) {
628
- if (isNum(value))
629
- return [stripped, `${plainScalar(value)} BTC`];
630
- return null;
631
- }
632
1127
  stripped = stripSuffixCI(key, "_jpy");
633
1128
  if (stripped !== null) {
634
1129
  if (isInt(value) && value >= 0)
@@ -725,7 +1220,7 @@ function formatRfc3339Ms(ms) {
725
1220
  * Round to `digits` decimals using round-half-to-even (banker's rounding) and
726
1221
  * format with a fixed decimal count. `Number.toFixed` rounds half away from
727
1222
  * zero, which diverges from the printf `%.1f` used by Rust/Go/Python on exact
728
- * ties (e.g. 1280 bytes = 1.25 KB → "1.2KB", not "1.3KB").
1223
+ * ties (e.g. 1280 bytes = 1.25 KiB → "1.2KiB", not "1.3KiB").
729
1224
  */
730
1225
  function toFixedHalfEven(x, digits) {
731
1226
  const factor = 10 ** digits;
@@ -743,20 +1238,19 @@ function toFixedHalfEven(x, digits) {
743
1238
  return (rounded / factor).toFixed(digits);
744
1239
  }
745
1240
  function formatBytesHuman(bytes) {
746
- const KB = 1024;
747
- const MB = KB * 1024;
748
- const GB = MB * 1024;
749
- const TB = GB * 1024;
750
- const sign = bytes < 0 ? "-" : "";
751
- const b = Math.abs(bytes);
752
- if (b >= TB)
753
- return `${sign}${toFixedHalfEven(b / TB, 1)}TB`;
754
- if (b >= GB)
755
- return `${sign}${toFixedHalfEven(b / GB, 1)}GB`;
756
- if (b >= MB)
757
- return `${sign}${toFixedHalfEven(b / MB, 1)}MB`;
758
- if (b >= KB)
759
- return `${sign}${toFixedHalfEven(b / KB, 1)}KB`;
1241
+ const KiB = 1024;
1242
+ const MiB = KiB * 1024;
1243
+ const GiB = MiB * 1024;
1244
+ const TiB = GiB * 1024;
1245
+ const b = bytes;
1246
+ if (b >= TiB)
1247
+ return `${toFixedHalfEven(b / TiB, 1)}TiB`;
1248
+ if (b >= GiB)
1249
+ return `${toFixedHalfEven(b / GiB, 1)}GiB`;
1250
+ if (b >= MiB)
1251
+ return `${toFixedHalfEven(b / MiB, 1)}MiB`;
1252
+ if (b >= KiB)
1253
+ return `${toFixedHalfEven(b / KiB, 1)}KiB`;
760
1254
  return `${bytes}B`;
761
1255
  }
762
1256
  function formatWithCommas(n) {
@@ -778,10 +1272,24 @@ function extractCurrencyCode(key) {
778
1272
  withoutCents = key.slice(0, -6);
779
1273
  else
780
1274
  return null;
781
- const idx = withoutCents.lastIndexOf("_");
1275
+ return extractCurrencyCodeFromStem(withoutCents);
1276
+ }
1277
+ /** Extract currency code from _{code}_micro / _{CODE}_MICRO suffix. */
1278
+ function extractCurrencyCodeMicro(key) {
1279
+ let withoutMicro;
1280
+ if (key.endsWith("_micro"))
1281
+ withoutMicro = key.slice(0, -6);
1282
+ else if (key.endsWith("_MICRO"))
1283
+ withoutMicro = key.slice(0, -6);
1284
+ else
1285
+ return null;
1286
+ return extractCurrencyCodeFromStem(withoutMicro);
1287
+ }
1288
+ function extractCurrencyCodeFromStem(stem) {
1289
+ const idx = stem.lastIndexOf("_");
782
1290
  if (idx < 0)
783
1291
  return null;
784
- const code = withoutCents.slice(idx + 1);
1292
+ const code = stem.slice(idx + 1);
785
1293
  if (!/^[A-Za-z]{3,4}$/.test(code))
786
1294
  return null;
787
1295
  return code;
@@ -1002,7 +1510,7 @@ function quoteLogfmtKey(key) {
1002
1510
  // ═══════════════════════════════════════════
1003
1511
  function sanitizeForJson(value, stack = new WeakSet(), depth = 0) {
1004
1512
  if (depth >= MAX_DEPTH)
1005
- return "***";
1513
+ return MAX_DEPTH_MARKER;
1006
1514
  if (value === null)
1007
1515
  return null;
1008
1516
  const t = typeof value;