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/README.md +18 -22
- package/assets/protocol-v1.schema.json +67 -0
- package/assets/registry.json +260 -0
- package/assets/skills/agent-first-data/SKILL.md +64 -0
- package/assets/skills/agent-first-data/references/protocol-v1.schema.json +66 -0
- package/assets/skills/agent-first-data/references/registry.json +260 -0
- package/assets/skills/agent-first-data/references/rules.md +347 -0
- package/dist/cli.d.ts +48 -14
- package/dist/cli.js +134 -36
- package/dist/format.d.ts +137 -55
- package/dist/format.js +510 -144
- package/dist/index.d.ts +2 -5
- package/dist/index.js +2 -5
- package/dist/stream_redirect.js +17 -3
- package/package.json +9 -8
- package/dist/afdata_logging.d.ts +0 -44
- package/dist/afdata_logging.js +0 -90
package/dist/format.js
CHANGED
|
@@ -1,38 +1,391 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AFDATA output formatting and protocol templates.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
//
|
|
10
|
+
// Event type: opaque branded wrapper
|
|
12
11
|
// ═══════════════════════════════════════════
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
/**
|
|
51
|
-
export function
|
|
52
|
-
return
|
|
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
|
|
55
|
-
export function
|
|
56
|
-
|
|
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
|
-
|
|
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 (
|
|
71
|
-
renderYamlRaw(
|
|
417
|
+
if (options.style === OutputStyle.Raw) {
|
|
418
|
+
renderYamlRaw(redacted, 0, lines);
|
|
72
419
|
}
|
|
73
420
|
else {
|
|
74
|
-
renderYamlProcessed(
|
|
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
|
-
|
|
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 (
|
|
87
|
-
collectPlainPairsRaw(
|
|
430
|
+
if (options.style === OutputStyle.Raw) {
|
|
431
|
+
collectPlainPairsRaw(redacted, "", pairs);
|
|
88
432
|
}
|
|
89
433
|
else {
|
|
90
|
-
collectPlainPairs(
|
|
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
|
-
/**
|
|
101
|
-
export function
|
|
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
|
-
|
|
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
|
|
146
|
-
const redacted = redactUrlInStr(url, secretNameSet(
|
|
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
|
|
152
|
-
*
|
|
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
|
-
|
|
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
|
|
162
|
-
|
|
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 ?
|
|
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 ?
|
|
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
|
-
|
|
549
|
-
|
|
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
|
-
|
|
604
|
-
|
|
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
|
-
|
|
610
|
-
|
|
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
|
|
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
|
|
747
|
-
const
|
|
748
|
-
const
|
|
749
|
-
const
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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
|
-
|
|
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 =
|
|
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;
|