agent-first-data 0.15.0 → 0.16.1

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,391 @@
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
+ payload;
180
+ traceValue = {};
181
+ issues = [];
182
+ constructor(payload) {
183
+ this.payload = payload;
184
+ }
185
+ trace(value) {
186
+ if (value !== undefined) {
187
+ if (!isObject(value)) {
188
+ this.issues.push("trace must be a JSON object");
189
+ }
190
+ else {
191
+ this.traceValue = value;
192
+ }
193
+ }
194
+ return this;
195
+ }
196
+ build() {
197
+ if (this.issues.length > 0) {
198
+ throw new EventBuildError(`Failed to build progress event: ${this.issues.join("; ")}`, this.issues);
199
+ }
200
+ const m = { kind: "progress", progress: this.payload, trace: this.traceValue };
201
+ return new Event(m);
202
+ }
203
+ }
204
+ export class JsonLogBuilder {
205
+ payload;
206
+ traceValue = {};
207
+ issues = [];
208
+ constructor(payload) {
209
+ this.payload = payload;
210
+ }
211
+ trace(value) {
212
+ if (value !== undefined) {
213
+ if (!isObject(value)) {
214
+ this.issues.push("trace must be a JSON object");
215
+ }
216
+ else {
217
+ this.traceValue = value;
218
+ }
219
+ }
220
+ return this;
221
+ }
222
+ build() {
223
+ if (this.issues.length > 0) {
224
+ throw new EventBuildError(`Failed to build log event: ${this.issues.join("; ")}`, this.issues);
225
+ }
226
+ const m = { kind: "log", log: this.payload, trace: this.traceValue };
227
+ return new Event(m);
228
+ }
229
+ }
230
+ /** Fluent builder for result events. */
231
+ export function jsonResult(payload) {
232
+ return new JsonResultBuilder(payload);
233
+ }
234
+ /** Fluent builder for error events. */
235
+ export function jsonError(code, message) {
236
+ return new JsonErrorBuilder(code, message);
237
+ }
238
+ /** Fluent builder for progress events. */
239
+ export function jsonProgress(payload) {
240
+ return new JsonProgressBuilder(payload);
241
+ }
242
+ /** Fluent builder for log events. */
243
+ export function jsonLog(payload) {
244
+ return new JsonLogBuilder(payload);
245
+ }
246
+ /** Validate one protocol event envelope. `strict` also enforces the recommended strict protocol profile (default true). */
247
+ export function validateProtocolEvent(event, strict = true) {
248
+ if (!isObject(event)) {
249
+ throw new Error("event must be a JSON object");
250
+ }
251
+ const kind = event.kind;
252
+ if (kind !== "result" && kind !== "error" && kind !== "progress" && kind !== "log") {
253
+ throw new Error("event.kind must be one of result, error, progress, log");
254
+ }
255
+ if (!(kind in event)) {
256
+ throw new Error(`event payload field ${JSON.stringify(kind)} is required`);
257
+ }
258
+ for (const key of Object.keys(event)) {
259
+ if (key !== "kind" && key !== kind && key !== "trace") {
260
+ throw new Error(`unexpected top-level field ${JSON.stringify(key)}`);
261
+ }
262
+ }
263
+ if ("trace" in event && !isObject(event.trace)) {
264
+ throw new Error("event.trace must be a JSON object when present");
265
+ }
266
+ if (kind === "error") {
267
+ validateErrorPayload(event.error);
268
+ }
269
+ if (!strict)
270
+ return;
271
+ if (!isObject(event.trace))
272
+ throw new Error("event.trace is required in strict mode");
273
+ if (kind === "error")
274
+ validateStrictError(event.error);
275
+ }
276
+ function validateErrorPayload(error) {
277
+ if (!isObject(error)) {
278
+ throw new Error("event.error must be a JSON object");
279
+ }
280
+ if (typeof error.code !== "string" || error.code === "") {
281
+ throw new Error("event.error.code must be a non-empty string");
282
+ }
283
+ if (typeof error.message !== "string" || error.message === "") {
284
+ throw new Error("event.error.message must be a non-empty string");
285
+ }
286
+ if ("hint" in error && typeof error.hint !== "string") {
287
+ throw new Error("event.error.hint must be a string when present");
288
+ }
289
+ }
290
+ /** Validate finite CLI lifecycle: (log | progress)* -> exactly one terminal. `strict` also enforces the recommended strict protocol profile (default true). */
291
+ export function validateProtocolStream(events, strict = true) {
292
+ let terminalSeen = false;
293
+ events.forEach((event, idx) => {
294
+ try {
295
+ validateProtocolEvent(event, strict);
296
+ }
297
+ catch (e) {
298
+ throw new Error(`event ${idx}: ${e.message}`);
299
+ }
300
+ const kind = event.kind;
301
+ if (kind === "log" || kind === "progress") {
302
+ if (terminalSeen)
303
+ throw new Error(`event ${idx}: non-terminal event after terminal`);
304
+ }
305
+ else if (kind === "result" || kind === "error") {
306
+ if (terminalSeen)
307
+ throw new Error(`event ${idx}: duplicate terminal event`);
308
+ terminalSeen = true;
309
+ }
310
+ });
311
+ if (!terminalSeen) {
312
+ throw new Error("event stream must contain exactly one terminal result or error");
313
+ }
314
+ }
315
+ function validateStrictError(error) {
316
+ if (!isObject(error))
317
+ throw new Error("event.error must be a JSON object in strict mode");
318
+ if (typeof error.code !== "string" || error.code === "") {
319
+ throw new Error("event.error.code must be a non-empty string in strict mode");
320
+ }
321
+ if (typeof error.message !== "string" || error.message === "") {
322
+ throw new Error("event.error.message must be a non-empty string in strict mode");
323
+ }
324
+ if (typeof error.retryable !== "boolean") {
325
+ throw new Error("event.error.retryable must be a boolean in strict mode");
326
+ }
327
+ if ("hint" in error && typeof error.hint !== "string") {
328
+ throw new Error("event.error.hint must be a string when present in strict mode");
329
+ }
330
+ }
331
+ /** Thrown by decodeProtocolEvent for malformed JSON or a strict-invalid event. */
332
+ export class EventDecodeError extends Error {
333
+ constructor(message) {
334
+ super(message);
335
+ this.name = "EventDecodeError";
336
+ }
337
+ }
338
+ const ERROR_RESERVED_FIELDS = new Set(["code", "message", "hint", "retryable"]);
339
+ /**
340
+ * Parse one protocol line (a single JSON text value), strict-validate it, and
341
+ * return a typed decoded event.
342
+ */
343
+ export function decodeProtocolEvent(text) {
344
+ let parsed;
345
+ try {
346
+ parsed = JSON.parse(text);
347
+ }
348
+ catch (e) {
349
+ throw new EventDecodeError(`invalid JSON: ${e.message}`);
350
+ }
351
+ try {
352
+ validateProtocolEvent(parsed, true);
353
+ }
354
+ catch (e) {
355
+ throw new EventDecodeError(e.message);
356
+ }
357
+ const event = parsed;
358
+ const trace = isObject(event.trace) ? event.trace : undefined;
359
+ switch (event.kind) {
360
+ case "result":
361
+ return { kind: "result", result: event.result, trace };
362
+ case "error": {
363
+ const error = event.error;
364
+ const fields = {};
365
+ for (const [k, v] of Object.entries(error)) {
366
+ if (!ERROR_RESERVED_FIELDS.has(k))
367
+ fields[k] = v;
368
+ }
369
+ const decoded = {
370
+ kind: "error",
371
+ code: error.code,
372
+ message: error.message,
373
+ retryable: error.retryable,
374
+ fields,
375
+ trace,
376
+ };
377
+ if (typeof error.hint === "string")
378
+ decoded.hint = error.hint;
379
+ return decoded;
380
+ }
381
+ case "progress":
382
+ return { kind: "progress", progress: event.progress, trace };
383
+ case "log":
384
+ return { kind: "log", log: event.log, trace };
385
+ default:
386
+ // Unreachable: validateProtocolEvent already constrains event.kind.
387
+ throw new EventDecodeError(`unsupported event kind ${JSON.stringify(event.kind)}`);
388
+ }
36
389
  }
37
390
  // ═══════════════════════════════════════════
38
391
  // Public API: Output Formatters
@@ -47,47 +400,38 @@ export var OutputStyle;
47
400
  OutputStyle["Readable"] = "Readable";
48
401
  OutputStyle["Raw"] = "Raw";
49
402
  })(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));
403
+ /** Convenience: build OutputOptions scoped to a RedactionPolicy. */
404
+ export function outputOptionsForPolicy(policy) {
405
+ return { redaction: { policy } };
53
406
  }
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 ?? {}));
407
+ /** Format as single-line JSON. Secrets redacted, original keys, raw values. */
408
+ export function outputJson(value, options = {}) {
409
+ const unwrapped = value instanceof Event ? value.toJSON() : value;
410
+ return JSON.stringify(redactedValue(unwrapped, options.redaction ?? {}));
61
411
  }
62
412
  /** 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 ?? {});
413
+ export function outputYaml(value, options = {}) {
414
+ const unwrapped = value instanceof Event ? value.toJSON() : value;
415
+ const redacted = redactedValue(unwrapped, options.redaction ?? {});
69
416
  const lines = ["---"];
70
- if (outputOptions.style === OutputStyle.Raw) {
71
- renderYamlRaw(value, 0, lines);
417
+ if (options.style === OutputStyle.Raw) {
418
+ renderYamlRaw(redacted, 0, lines);
72
419
  }
73
420
  else {
74
- renderYamlProcessed(value, 0, lines);
421
+ renderYamlProcessed(redacted, 0, lines);
75
422
  }
76
423
  return lines.join("\n");
77
424
  }
78
425
  /** 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 ?? {});
426
+ export function outputPlain(value, options = {}) {
427
+ const unwrapped = value instanceof Event ? value.toJSON() : value;
428
+ const redacted = redactedValue(unwrapped, options.redaction ?? {});
85
429
  const pairs = [];
86
- if (outputOptions.style === OutputStyle.Raw) {
87
- collectPlainPairsRaw(value, "", pairs);
430
+ if (options.style === OutputStyle.Raw) {
431
+ collectPlainPairsRaw(redacted, "", pairs);
88
432
  }
89
433
  else {
90
- collectPlainPairs(value, "", pairs);
434
+ collectPlainPairs(redacted, "", pairs);
91
435
  }
92
436
  pairs.sort(([a], [b]) => jcsCompare(a, b));
93
437
  return pairs
@@ -97,41 +441,12 @@ export function outputPlainWithOptions(value, outputOptions) {
97
441
  // ═══════════════════════════════════════════
98
442
  // Public API: Redaction & Utility
99
443
  // ═══════════════════════════════════════════
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) {
444
+ /** Return a JSON-safe copy with redaction options applied (default: full _secret redaction). */
445
+ export function redactedValue(value, options) {
110
446
  const v = sanitizeForJson(value);
111
- redactSecrets(v);
447
+ applyRedactionOptions(v, options ?? {});
112
448
  return v;
113
449
  }
114
- /** Return a JSON-safe copy with an explicit redaction policy applied. */
115
- export function redactedValueWith(value, redactionPolicy) {
116
- 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);
124
- return v;
125
- }
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
450
  /**
136
451
  * Redact secret components of a single URL string.
137
452
  *
@@ -142,36 +457,36 @@ export function redactUrlSecrets(url) {
142
457
  * A string that is not a single, whitespace-free, scheme-prefixed URL
143
458
  * (including a URL embedded in surrounding prose) is returned unchanged.
144
459
  */
145
- export function redactUrlSecretsWithOptions(url, redactionOptions) {
146
- const redacted = redactUrlInStr(url, secretNameSet(redactionOptions));
460
+ export function redactUrlSecrets(url, options) {
461
+ const redacted = redactUrlInStr(url, secretNameSet(options ?? {}));
147
462
  return redacted ?? url;
148
463
  }
149
464
  /**
150
465
  * 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.
466
+ * Accepts numbers followed by explicit units. Decimal units are
467
+ * B/kB/MB/GB/TB; binary units are KiB/MiB/GiB/TiB.
468
+ * Trims whitespace and rejects ambiguous K/M/G/T units.
153
469
  */
154
470
  export function parseSize(s) {
155
471
  s = s.trim();
156
472
  if (!s)
157
473
  return null;
158
474
  const multipliers = {
159
- b: 1, k: 1024, m: 1024 ** 2, g: 1024 ** 3, t: 1024 ** 4,
475
+ KiB: 1024,
476
+ MiB: 1024 ** 2,
477
+ GiB: 1024 ** 3,
478
+ TiB: 1024 ** 4,
479
+ kB: 1000,
480
+ MB: 1000 ** 2,
481
+ GB: 1000 ** 3,
482
+ TB: 1000 ** 4,
483
+ B: 1,
160
484
  };
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 {
485
+ const unit = Object.keys(multipliers).find((candidate) => s.endsWith(candidate));
486
+ if (unit === undefined)
173
487
  return null;
174
- }
488
+ const numStr = s.slice(0, -unit.length);
489
+ const mult = multipliers[unit];
175
490
  if (!numStr || !/^(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(numStr))
176
491
  return null;
177
492
  const n = Number(numStr);
@@ -287,6 +602,7 @@ function isSecretKey(key, secretNames) {
287
602
  return keyHasSecretSuffix(key) || secretNames.has(key);
288
603
  }
289
604
  const MAX_DEPTH = 256;
605
+ const MAX_DEPTH_MARKER = "<afdata:max-depth>";
290
606
  function redactSecrets(value, context = DEFAULT_CONTEXT, depth = 0) {
291
607
  if (depth >= MAX_DEPTH)
292
608
  return;
@@ -301,26 +617,23 @@ function redactSecrets(value, context = DEFAULT_CONTEXT, depth = 0) {
301
617
  value[k] = redactUrlFieldValue(v, context.secretNames);
302
618
  }
303
619
  else {
304
- value[k] = depth + 1 >= MAX_DEPTH ? "***" : (redactSecrets(v, context, depth + 1), v);
620
+ value[k] = depth + 1 >= MAX_DEPTH ? MAX_DEPTH_MARKER : (redactSecrets(v, context, depth + 1), v);
305
621
  }
306
622
  }
307
623
  else {
308
- value[k] = depth + 1 >= MAX_DEPTH ? "***" : (redactSecrets(v, context, depth + 1), v);
624
+ value[k] = depth + 1 >= MAX_DEPTH ? MAX_DEPTH_MARKER : (redactSecrets(v, context, depth + 1), v);
309
625
  }
310
626
  }
311
627
  }
312
628
  else if (Array.isArray(value)) {
313
629
  for (let i = 0; i < value.length; i++) {
314
630
  if (depth + 1 >= MAX_DEPTH)
315
- value[i] = "***";
631
+ value[i] = MAX_DEPTH_MARKER;
316
632
  else
317
633
  redactSecrets(value[i], context, depth + 1);
318
634
  }
319
635
  }
320
636
  }
321
- function applyRedactionPolicy(value, redactionPolicy) {
322
- applyRedactionPolicyWithContext(value, redactionPolicy, DEFAULT_CONTEXT);
323
- }
324
637
  function applyRedactionOptions(value, redactionOptions) {
325
638
  applyRedactionPolicyWithContext(value, redactionOptions.policy, contextFromOptions(redactionOptions));
326
639
  }
@@ -516,9 +829,45 @@ function tryStripGenericCents(key) {
516
829
  return null;
517
830
  return [stripped, code];
518
831
  }
832
+ function tryStripGenericMicro(key) {
833
+ const code = extractCurrencyCodeMicro(key);
834
+ if (code === null)
835
+ return null;
836
+ const suffixLen = code.length + "_micro".length + 1; // _{code}_micro
837
+ const stripped = key.slice(0, -suffixLen);
838
+ if (!stripped)
839
+ return null;
840
+ return [stripped, code];
841
+ }
519
842
  function isInt(value) {
520
843
  return typeof value === "number" && Number.isInteger(value);
521
844
  }
845
+ function decimalIntText(value) {
846
+ if (typeof value === "string" && /^-?\d+$/.test(value))
847
+ return value;
848
+ if (isInt(value))
849
+ return String(value);
850
+ return null;
851
+ }
852
+ function epochNsToMs(value) {
853
+ const text = decimalIntText(value);
854
+ if (text === null)
855
+ return null;
856
+ try {
857
+ const ns = BigInt(text);
858
+ const divisor = 1000000n;
859
+ let ms = ns / divisor;
860
+ if (ns < 0n && ns % divisor !== 0n)
861
+ ms -= 1n;
862
+ const asNumber = Number(ms);
863
+ if (!Number.isSafeInteger(asNumber))
864
+ return null;
865
+ return asNumber;
866
+ }
867
+ catch {
868
+ return null;
869
+ }
870
+ }
522
871
  function isNum(value) {
523
872
  return typeof value === "number";
524
873
  }
@@ -545,8 +894,9 @@ function tryProcessField(key, value) {
545
894
  }
546
895
  stripped = stripSuffixCI(key, "_epoch_ns");
547
896
  if (stripped !== null) {
548
- if (isInt(value)) {
549
- const formatted = formatRfc3339Ms(Math.floor(value / 1_000_000));
897
+ const ms = epochNsToMs(value);
898
+ if (ms !== null) {
899
+ const formatted = formatRfc3339Ms(ms);
550
900
  if (formatted !== null)
551
901
  return [stripped, formatted];
552
902
  }
@@ -572,6 +922,13 @@ function tryProcessField(key, value) {
572
922
  return [gcStripped, `${Math.floor(value / 100)}.${String(value % 100).padStart(2, "0")} ${code.toUpperCase()}`];
573
923
  return null;
574
924
  }
925
+ const gm = tryStripGenericMicro(key);
926
+ if (gm !== null) {
927
+ const [gmStripped, code] = gm;
928
+ if (isInt(value) && value >= 0)
929
+ return [gmStripped, `${Math.floor(value / 1_000_000)}.${String(value % 1_000_000).padStart(6, "0")} ${code.toUpperCase()}`];
930
+ return null;
931
+ }
575
932
  // Group 3: multi-char suffixes
576
933
  stripped = stripSuffixCI(key, "_rfc3339");
577
934
  if (stripped !== null) {
@@ -600,19 +957,21 @@ function tryProcessField(key, value) {
600
957
  // Group 4: single-unit suffixes
601
958
  stripped = stripSuffixCI(key, "_msats");
602
959
  if (stripped !== null) {
603
- if (isNum(value))
604
- return [stripped, `${plainScalar(value)}msats`];
960
+ const text = decimalIntText(value);
961
+ if (text !== null)
962
+ return [stripped, `${text}msats`];
605
963
  return null;
606
964
  }
607
965
  stripped = stripSuffixCI(key, "_sats");
608
966
  if (stripped !== null) {
609
- if (isNum(value))
610
- return [stripped, `${plainScalar(value)}sats`];
967
+ const text = decimalIntText(value);
968
+ if (text !== null)
969
+ return [stripped, `${text}sats`];
611
970
  return null;
612
971
  }
613
972
  stripped = stripSuffixCI(key, "_bytes");
614
973
  if (stripped !== null) {
615
- if (isInt(value))
974
+ if (isInt(value) && value >= 0)
616
975
  return [stripped, formatBytesHuman(value)];
617
976
  return null;
618
977
  }
@@ -623,12 +982,6 @@ function tryProcessField(key, value) {
623
982
  return null;
624
983
  }
625
984
  // 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
985
  stripped = stripSuffixCI(key, "_jpy");
633
986
  if (stripped !== null) {
634
987
  if (isInt(value) && value >= 0)
@@ -725,7 +1078,7 @@ function formatRfc3339Ms(ms) {
725
1078
  * Round to `digits` decimals using round-half-to-even (banker's rounding) and
726
1079
  * format with a fixed decimal count. `Number.toFixed` rounds half away from
727
1080
  * 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").
1081
+ * ties (e.g. 1280 bytes = 1.25 KiB → "1.2KiB", not "1.3KiB").
729
1082
  */
730
1083
  function toFixedHalfEven(x, digits) {
731
1084
  const factor = 10 ** digits;
@@ -743,20 +1096,19 @@ function toFixedHalfEven(x, digits) {
743
1096
  return (rounded / factor).toFixed(digits);
744
1097
  }
745
1098
  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`;
1099
+ const KiB = 1024;
1100
+ const MiB = KiB * 1024;
1101
+ const GiB = MiB * 1024;
1102
+ const TiB = GiB * 1024;
1103
+ const b = bytes;
1104
+ if (b >= TiB)
1105
+ return `${toFixedHalfEven(b / TiB, 1)}TiB`;
1106
+ if (b >= GiB)
1107
+ return `${toFixedHalfEven(b / GiB, 1)}GiB`;
1108
+ if (b >= MiB)
1109
+ return `${toFixedHalfEven(b / MiB, 1)}MiB`;
1110
+ if (b >= KiB)
1111
+ return `${toFixedHalfEven(b / KiB, 1)}KiB`;
760
1112
  return `${bytes}B`;
761
1113
  }
762
1114
  function formatWithCommas(n) {
@@ -778,10 +1130,24 @@ function extractCurrencyCode(key) {
778
1130
  withoutCents = key.slice(0, -6);
779
1131
  else
780
1132
  return null;
781
- const idx = withoutCents.lastIndexOf("_");
1133
+ return extractCurrencyCodeFromStem(withoutCents);
1134
+ }
1135
+ /** Extract currency code from _{code}_micro / _{CODE}_MICRO suffix. */
1136
+ function extractCurrencyCodeMicro(key) {
1137
+ let withoutMicro;
1138
+ if (key.endsWith("_micro"))
1139
+ withoutMicro = key.slice(0, -6);
1140
+ else if (key.endsWith("_MICRO"))
1141
+ withoutMicro = key.slice(0, -6);
1142
+ else
1143
+ return null;
1144
+ return extractCurrencyCodeFromStem(withoutMicro);
1145
+ }
1146
+ function extractCurrencyCodeFromStem(stem) {
1147
+ const idx = stem.lastIndexOf("_");
782
1148
  if (idx < 0)
783
1149
  return null;
784
- const code = withoutCents.slice(idx + 1);
1150
+ const code = stem.slice(idx + 1);
785
1151
  if (!/^[A-Za-z]{3,4}$/.test(code))
786
1152
  return null;
787
1153
  return code;
@@ -1002,7 +1368,7 @@ function quoteLogfmtKey(key) {
1002
1368
  // ═══════════════════════════════════════════
1003
1369
  function sanitizeForJson(value, stack = new WeakSet(), depth = 0) {
1004
1370
  if (depth >= MAX_DEPTH)
1005
- return "***";
1371
+ return MAX_DEPTH_MARKER;
1006
1372
  if (value === null)
1007
1373
  return null;
1008
1374
  const t = typeof value;