agent-first-data 0.14.2 → 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/README.md +18 -21
- package/assets/protocol-v1.schema.json +84 -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 +152 -55
- package/dist/format.js +652 -144
- package/dist/index.d.ts +2 -4
- package/dist/index.js +2 -4
- package/dist/stream_redirect.d.ts +21 -0
- package/dist/stream_redirect.js +128 -0
- package/package.json +12 -7
- package/dist/afdata_logging.d.ts +0 -44
- package/dist/afdata_logging.js +0 -90
package/dist/format.js
CHANGED
|
@@ -1,38 +1,533 @@
|
|
|
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
|
+
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
|
-
/**
|
|
51
|
-
export function
|
|
52
|
-
return
|
|
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
|
|
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 ?? {}));
|
|
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
|
-
|
|
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 (
|
|
71
|
-
renderYamlRaw(
|
|
559
|
+
if (options.style === OutputStyle.Raw) {
|
|
560
|
+
renderYamlRaw(redacted, 0, lines);
|
|
72
561
|
}
|
|
73
562
|
else {
|
|
74
|
-
renderYamlProcessed(
|
|
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
|
-
|
|
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 (
|
|
87
|
-
collectPlainPairsRaw(
|
|
572
|
+
if (options.style === OutputStyle.Raw) {
|
|
573
|
+
collectPlainPairsRaw(redacted, "", pairs);
|
|
88
574
|
}
|
|
89
575
|
else {
|
|
90
|
-
collectPlainPairs(
|
|
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
|
-
/**
|
|
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) {
|
|
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
|
-
|
|
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
|
|
146
|
-
const redacted = redactUrlInStr(url, secretNameSet(
|
|
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
|
|
152
|
-
*
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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 ?
|
|
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 ?
|
|
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
|
-
|
|
549
|
-
|
|
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
|
-
|
|
604
|
-
|
|
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
|
-
|
|
610
|
-
|
|
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
|
|
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
|
|
747
|
-
const
|
|
748
|
-
const
|
|
749
|
-
const
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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
|
-
|
|
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 =
|
|
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;
|