agent-first-data 0.12.2 → 0.13.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 +26 -557
- package/dist/afdata_logging.d.ts +7 -3
- package/dist/afdata_logging.js +16 -14
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +63 -4
- package/dist/format.d.ts +12 -7
- package/dist/format.js +149 -81
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/skill.js +155 -43
- package/package.json +2 -1
package/dist/afdata_logging.js
CHANGED
|
@@ -17,19 +17,26 @@
|
|
|
17
17
|
* });
|
|
18
18
|
*/
|
|
19
19
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
20
|
-
import {
|
|
20
|
+
import { outputJsonWithOptions, outputYamlWithOptions, outputPlainWithOptions, } from "./format.js";
|
|
21
21
|
const spanStore = new AsyncLocalStorage();
|
|
22
22
|
let currentFormat = "json";
|
|
23
|
+
let currentRedaction = {};
|
|
24
|
+
function setFormat(format, options) {
|
|
25
|
+
currentFormat = format;
|
|
26
|
+
currentRedaction = options?.secretNames ? { secretNames: options.secretNames } : {};
|
|
27
|
+
}
|
|
23
28
|
/** Set log output to single-line JSONL (secrets redacted, original keys). */
|
|
24
|
-
export function initJson() {
|
|
29
|
+
export function initJson(options) { setFormat("json", options); }
|
|
25
30
|
/** Set log output to single-line logfmt (keys stripped, values formatted). */
|
|
26
|
-
export function initPlain() {
|
|
31
|
+
export function initPlain(options) { setFormat("plain", options); }
|
|
27
32
|
/** Set log output to multi-line YAML (keys stripped, values formatted). */
|
|
28
|
-
export function initYaml() {
|
|
33
|
+
export function initYaml(options) { setFormat("yaml", options); }
|
|
29
34
|
function emit(level, message, fields) {
|
|
30
35
|
const entry = {
|
|
31
36
|
timestamp_epoch_ms: Date.now(),
|
|
32
37
|
message,
|
|
38
|
+
code: "log",
|
|
39
|
+
level,
|
|
33
40
|
};
|
|
34
41
|
// Span fields (from AsyncLocalStorage)
|
|
35
42
|
const spanFields = spanStore.getStore();
|
|
@@ -38,31 +45,26 @@ function emit(level, message, fields) {
|
|
|
38
45
|
entry[k] = v;
|
|
39
46
|
}
|
|
40
47
|
}
|
|
41
|
-
// Event fields
|
|
42
|
-
let hasCode = false;
|
|
48
|
+
// Event fields override span fields, except protocol code is always "log".
|
|
43
49
|
if (fields) {
|
|
44
50
|
for (const [k, v] of Object.entries(fields)) {
|
|
45
51
|
if (k === "code")
|
|
46
|
-
|
|
52
|
+
continue;
|
|
47
53
|
entry[k] = v;
|
|
48
54
|
}
|
|
49
55
|
}
|
|
50
|
-
// Default code from level
|
|
51
|
-
if (!hasCode) {
|
|
52
|
-
entry["code"] = level;
|
|
53
|
-
}
|
|
54
56
|
// Format using the library's own output functions
|
|
55
57
|
const value = entry;
|
|
56
58
|
let line;
|
|
57
59
|
switch (currentFormat) {
|
|
58
60
|
case "plain":
|
|
59
|
-
line =
|
|
61
|
+
line = outputPlainWithOptions(value, { redaction: currentRedaction });
|
|
60
62
|
break;
|
|
61
63
|
case "yaml":
|
|
62
|
-
line =
|
|
64
|
+
line = outputYamlWithOptions(value, { redaction: currentRedaction });
|
|
63
65
|
break;
|
|
64
66
|
default:
|
|
65
|
-
line =
|
|
67
|
+
line = outputJsonWithOptions(value, { redaction: currentRedaction });
|
|
66
68
|
break;
|
|
67
69
|
}
|
|
68
70
|
process.stdout.write(line + "\n");
|
package/dist/cli.d.ts
CHANGED
|
@@ -34,6 +34,19 @@ export declare function cliOutput(value: JsonValue, format: OutputFormat): strin
|
|
|
34
34
|
* JSON ignores OutputStyle and preserves original keys and values after redaction.
|
|
35
35
|
*/
|
|
36
36
|
export declare function cliOutputWithOptions(value: JsonValue, format: OutputFormat, outputOptions: OutputOptions): string;
|
|
37
|
+
/** Build a standard CLI version value. */
|
|
38
|
+
export declare function buildCliVersion(version: string): JsonValue;
|
|
39
|
+
/**
|
|
40
|
+
* Render CLI version output.
|
|
41
|
+
* Pass an OutputFormat for AFDATA JSON/YAML/plain. Pass null to preserve
|
|
42
|
+
* conventional "<name> <version>" text.
|
|
43
|
+
*/
|
|
44
|
+
export declare function cliRenderVersion(name: string, version: string, format?: OutputFormat | null): string;
|
|
45
|
+
/**
|
|
46
|
+
* Render version output if --version/-V is present; otherwise return undefined.
|
|
47
|
+
* Throws for malformed version requests, for example `--version --output xml`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function cliHandleVersionOrContinue(rawArgs: string[], name: string, version: string, defaultOutput?: OutputFormat | null, outputFlag?: string, allowOutputFormat?: boolean): string | undefined;
|
|
37
50
|
/**
|
|
38
51
|
* Build a standard CLI parse error value.
|
|
39
52
|
* Use when flag parsing fails or a flag value is invalid.
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AFDATA CLI helpers — output format parsing, log filter normalization, error building.
|
|
3
3
|
*/
|
|
4
|
-
import { outputJson, outputJsonWithOptions, outputYaml, outputYamlWithOptions, outputPlain, outputPlainWithOptions, } from "./format.js";
|
|
4
|
+
import { buildJson, outputJson, outputJsonWithOptions, outputYaml, outputYamlWithOptions, outputPlain, outputPlainWithOptions, } from "./format.js";
|
|
5
5
|
/**
|
|
6
6
|
* Parse the --output flag value into an OutputFormat.
|
|
7
7
|
* Throws on unknown values; catch and pass message to buildCliError.
|
|
@@ -58,6 +58,68 @@ export function cliOutputWithOptions(value, format, outputOptions) {
|
|
|
58
58
|
return outputPlainWithOptions(value, outputOptions);
|
|
59
59
|
return outputJsonWithOptions(value, outputOptions);
|
|
60
60
|
}
|
|
61
|
+
/** Build a standard CLI version value. */
|
|
62
|
+
export function buildCliVersion(version) {
|
|
63
|
+
return buildJson("version", { version });
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Render CLI version output.
|
|
67
|
+
* Pass an OutputFormat for AFDATA JSON/YAML/plain. Pass null to preserve
|
|
68
|
+
* conventional "<name> <version>" text.
|
|
69
|
+
*/
|
|
70
|
+
export function cliRenderVersion(name, version, format = null) {
|
|
71
|
+
const rendered = format === null ? `${name} ${version}` : cliOutput(buildCliVersion(version), format);
|
|
72
|
+
return `${rendered.replace(/\n+$/u, "")}\n`;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Render version output if --version/-V is present; otherwise return undefined.
|
|
76
|
+
* Throws for malformed version requests, for example `--version --output xml`.
|
|
77
|
+
*/
|
|
78
|
+
export function cliHandleVersionOrContinue(rawArgs, name, version, defaultOutput = null, outputFlag = "--output", allowOutputFormat = true) {
|
|
79
|
+
let versionRequested = false;
|
|
80
|
+
let outputFormat;
|
|
81
|
+
let outputError;
|
|
82
|
+
for (let i = 0; i < rawArgs.length;) {
|
|
83
|
+
const arg = rawArgs[i];
|
|
84
|
+
if (arg === "--")
|
|
85
|
+
break;
|
|
86
|
+
if (arg === "--version" || arg === "-V") {
|
|
87
|
+
versionRequested = true;
|
|
88
|
+
i += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (allowOutputFormat && (arg === outputFlag || arg.startsWith(`${outputFlag}=`))) {
|
|
92
|
+
let value;
|
|
93
|
+
let step = 1;
|
|
94
|
+
if (arg.startsWith(`${outputFlag}=`)) {
|
|
95
|
+
value = arg.slice(outputFlag.length + 1);
|
|
96
|
+
}
|
|
97
|
+
else if (rawArgs[i + 1] !== undefined && !rawArgs[i + 1].startsWith("-")) {
|
|
98
|
+
value = rawArgs[i + 1];
|
|
99
|
+
step = 2;
|
|
100
|
+
}
|
|
101
|
+
if (value === undefined) {
|
|
102
|
+
outputError = new Error(`missing value for ${outputFlag}: expected json, yaml, or plain`);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
try {
|
|
106
|
+
outputFormat = cliParseOutput(value);
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
outputError = e;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
i += step;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
i += 1;
|
|
116
|
+
}
|
|
117
|
+
if (!versionRequested)
|
|
118
|
+
return undefined;
|
|
119
|
+
if (outputError !== undefined)
|
|
120
|
+
throw outputError;
|
|
121
|
+
return cliRenderVersion(name, version, allowOutputFormat && outputFormat !== undefined ? outputFormat : defaultOutput);
|
|
122
|
+
}
|
|
61
123
|
/**
|
|
62
124
|
* Build a standard CLI parse error value.
|
|
63
125
|
* Use when flag parsing fails or a flag value is invalid.
|
|
@@ -71,10 +133,7 @@ export function cliOutputWithOptions(value, format, outputOptions) {
|
|
|
71
133
|
export function buildCliError(message, hint) {
|
|
72
134
|
const m = {
|
|
73
135
|
code: "error",
|
|
74
|
-
error_code: "invalid_request",
|
|
75
136
|
error: message,
|
|
76
|
-
retryable: false,
|
|
77
|
-
trace: { duration_ms: 0 },
|
|
78
137
|
};
|
|
79
138
|
if (hint !== undefined)
|
|
80
139
|
m.hint = hint;
|
package/dist/format.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AFDATA output formatting and protocol templates.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 19 public APIs and 4 types: protocol builders, value redactors (copy and
|
|
5
5
|
* in-place; cover _secret and _url fields), output formatters, URL-string
|
|
6
|
-
* redactors (redactUrlSecrets / WithOptions), parseSize,
|
|
7
|
-
* RedactionOptions, OutputStyle, and OutputOptions.
|
|
6
|
+
* redactors (redactUrlSecrets / WithOptions), parseSize, normalizeUtcOffset,
|
|
7
|
+
* RedactionPolicy, RedactionOptions, OutputStyle, and OutputOptions.
|
|
8
8
|
*/
|
|
9
9
|
export type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
10
10
|
[key: string]: JsonValue;
|
|
@@ -17,8 +17,7 @@ export declare function buildJsonError(message: string, hint?: string, trace?: J
|
|
|
17
17
|
export declare function buildJson(code: string, fields: JsonValue, trace?: JsonValue): JsonValue;
|
|
18
18
|
export declare enum RedactionPolicy {
|
|
19
19
|
RedactionTraceOnly = "RedactionTraceOnly",
|
|
20
|
-
RedactionNone = "RedactionNone"
|
|
21
|
-
RedactionStrict = "RedactionStrict"
|
|
20
|
+
RedactionNone = "RedactionNone"
|
|
22
21
|
}
|
|
23
22
|
export type RedactionOptions = {
|
|
24
23
|
/** Optional scoped policy. Omitted means default full redaction. */
|
|
@@ -56,9 +55,9 @@ export declare function outputPlain(value: JsonValue): string;
|
|
|
56
55
|
/** Format as single-line logfmt with explicit output options. */
|
|
57
56
|
export declare function outputPlainWithOptions(value: JsonValue, outputOptions: OutputOptions): string;
|
|
58
57
|
/** Redact _secret fields in-place. */
|
|
59
|
-
export declare function
|
|
58
|
+
export declare function redactSecretsInPlace(value: JsonValue): void;
|
|
60
59
|
/** Redact secret fields in-place using explicit redaction options. */
|
|
61
|
-
export declare function
|
|
60
|
+
export declare function redactSecretsInPlaceWithOptions(value: JsonValue, redactionOptions: RedactionOptions): void;
|
|
62
61
|
/** Return a JSON-safe copy with default _secret redaction applied. */
|
|
63
62
|
export declare function redactedValue(value: unknown): JsonValue;
|
|
64
63
|
/** Return a JSON-safe copy with an explicit redaction policy applied. */
|
|
@@ -89,3 +88,9 @@ export declare function redactUrlSecretsWithOptions(url: string, redactionOption
|
|
|
89
88
|
* Case-insensitive. Trims whitespace. Returns null for invalid input.
|
|
90
89
|
*/
|
|
91
90
|
export declare function parseSize(s: string): number | null;
|
|
91
|
+
/**
|
|
92
|
+
* Normalize a fixed UTC offset string to "UTC" or ±HH:MM.
|
|
93
|
+
* This helper handles fixed offsets only; IANA timezone names and DST rules
|
|
94
|
+
* are intentionally out of scope.
|
|
95
|
+
*/
|
|
96
|
+
export declare function normalizeUtcOffset(value: string): string | null;
|
package/dist/format.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AFDATA output formatting and protocol templates.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 19 public APIs and 4 types: protocol builders, value redactors (copy and
|
|
5
5
|
* in-place; cover _secret and _url fields), output formatters, URL-string
|
|
6
|
-
* redactors (redactUrlSecrets / WithOptions), parseSize,
|
|
7
|
-
* RedactionOptions, OutputStyle, and OutputOptions.
|
|
6
|
+
* redactors (redactUrlSecrets / WithOptions), parseSize, normalizeUtcOffset,
|
|
7
|
+
* RedactionPolicy, RedactionOptions, OutputStyle, and OutputOptions.
|
|
8
8
|
*/
|
|
9
9
|
// ═══════════════════════════════════════════
|
|
10
10
|
// Public API: Protocol Builders
|
|
@@ -40,7 +40,6 @@ export var RedactionPolicy;
|
|
|
40
40
|
(function (RedactionPolicy) {
|
|
41
41
|
RedactionPolicy["RedactionTraceOnly"] = "RedactionTraceOnly";
|
|
42
42
|
RedactionPolicy["RedactionNone"] = "RedactionNone";
|
|
43
|
-
RedactionPolicy["RedactionStrict"] = "RedactionStrict";
|
|
44
43
|
})(RedactionPolicy || (RedactionPolicy = {}));
|
|
45
44
|
export var OutputStyle;
|
|
46
45
|
(function (OutputStyle) {
|
|
@@ -91,18 +90,18 @@ export function outputPlainWithOptions(value, outputOptions) {
|
|
|
91
90
|
}
|
|
92
91
|
pairs.sort(([a], [b]) => jcsCompare(a, b));
|
|
93
92
|
return pairs
|
|
94
|
-
.map(([k, v]) => `${k}=${quoteLogfmtValue(v)}`)
|
|
93
|
+
.map(([k, v]) => `${quoteLogfmtKey(k)}=${quoteLogfmtValue(v)}`)
|
|
95
94
|
.join(" ");
|
|
96
95
|
}
|
|
97
96
|
// ═══════════════════════════════════════════
|
|
98
97
|
// Public API: Redaction & Utility
|
|
99
98
|
// ═══════════════════════════════════════════
|
|
100
99
|
/** Redact _secret fields in-place. */
|
|
101
|
-
export function
|
|
100
|
+
export function redactSecretsInPlace(value) {
|
|
102
101
|
redactSecrets(value);
|
|
103
102
|
}
|
|
104
103
|
/** Redact secret fields in-place using explicit redaction options. */
|
|
105
|
-
export function
|
|
104
|
+
export function redactSecretsInPlaceWithOptions(value, redactionOptions) {
|
|
106
105
|
applyRedactionOptions(value, redactionOptions);
|
|
107
106
|
}
|
|
108
107
|
/** Return a JSON-safe copy with default _secret redaction applied. */
|
|
@@ -172,7 +171,7 @@ export function parseSize(s) {
|
|
|
172
171
|
else {
|
|
173
172
|
return null;
|
|
174
173
|
}
|
|
175
|
-
if (!numStr)
|
|
174
|
+
if (!numStr || !/^(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(numStr))
|
|
176
175
|
return null;
|
|
177
176
|
const n = Number(numStr);
|
|
178
177
|
if (isNaN(n) || n < 0 || !isFinite(n))
|
|
@@ -182,6 +181,49 @@ export function parseSize(s) {
|
|
|
182
181
|
return null;
|
|
183
182
|
return result;
|
|
184
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Normalize a fixed UTC offset string to "UTC" or ±HH:MM.
|
|
186
|
+
* This helper handles fixed offsets only; IANA timezone names and DST rules
|
|
187
|
+
* are intentionally out of scope.
|
|
188
|
+
*/
|
|
189
|
+
export function normalizeUtcOffset(value) {
|
|
190
|
+
const s = value.trim();
|
|
191
|
+
if (s.toLowerCase() === "utc" || s.toLowerCase() === "z")
|
|
192
|
+
return "UTC";
|
|
193
|
+
if (!s || (s[0] !== "+" && s[0] !== "-"))
|
|
194
|
+
return null;
|
|
195
|
+
const parsed = parseUtcOffsetBody(s.slice(1));
|
|
196
|
+
if (parsed === null)
|
|
197
|
+
return null;
|
|
198
|
+
const [hours, minutes] = parsed;
|
|
199
|
+
if (hours > 23 || minutes > 59)
|
|
200
|
+
return null;
|
|
201
|
+
if (hours === 0 && minutes === 0)
|
|
202
|
+
return "UTC";
|
|
203
|
+
return `${s[0]}${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
|
|
204
|
+
}
|
|
205
|
+
function parseUtcOffsetBody(body) {
|
|
206
|
+
if (!body)
|
|
207
|
+
return null;
|
|
208
|
+
if (body.includes(":")) {
|
|
209
|
+
const parts = body.split(":");
|
|
210
|
+
if (parts.length !== 2)
|
|
211
|
+
return null;
|
|
212
|
+
const [hours, minutes] = parts;
|
|
213
|
+
if (!hours || hours.length > 2 || minutes.length !== 2)
|
|
214
|
+
return null;
|
|
215
|
+
if (!/^[0-9]+$/.test(hours) || !/^[0-9]+$/.test(minutes))
|
|
216
|
+
return null;
|
|
217
|
+
return [Number(hours), Number(minutes)];
|
|
218
|
+
}
|
|
219
|
+
if (!/^[0-9]+$/.test(body))
|
|
220
|
+
return null;
|
|
221
|
+
if (body.length === 1 || body.length === 2)
|
|
222
|
+
return [Number(body), 0];
|
|
223
|
+
if (body.length === 4)
|
|
224
|
+
return [Number(body.slice(0, 2)), Number(body.slice(2))];
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
185
227
|
const DEFAULT_CONTEXT = {
|
|
186
228
|
secretNames: new Set(),
|
|
187
229
|
};
|
|
@@ -200,54 +242,35 @@ function keyHasUrlSuffix(key) {
|
|
|
200
242
|
function isSecretKey(key, secretNames) {
|
|
201
243
|
return keyHasSecretSuffix(key) || secretNames.has(key);
|
|
202
244
|
}
|
|
203
|
-
|
|
245
|
+
const MAX_DEPTH = 256;
|
|
246
|
+
function redactSecrets(value, context = DEFAULT_CONTEXT, depth = 0) {
|
|
247
|
+
if (depth >= MAX_DEPTH)
|
|
248
|
+
return;
|
|
204
249
|
if (isObject(value)) {
|
|
205
250
|
for (const k of Object.keys(value)) {
|
|
206
251
|
const v = value[k];
|
|
207
252
|
if (isSecretKey(k, context.secretNames)) {
|
|
208
|
-
|
|
209
|
-
|
|
253
|
+
value[k] = "***";
|
|
254
|
+
}
|
|
255
|
+
else if (keyHasUrlSuffix(k)) {
|
|
256
|
+
if (typeof v === "string") {
|
|
257
|
+
value[k] = redactUrlFieldValue(v, context.secretNames);
|
|
210
258
|
}
|
|
211
259
|
else {
|
|
212
|
-
value[k] = "***";
|
|
260
|
+
value[k] = depth + 1 >= MAX_DEPTH ? "***" : (redactSecrets(v, context, depth + 1), v);
|
|
213
261
|
}
|
|
214
262
|
}
|
|
215
|
-
else if (keyHasUrlSuffix(k) && typeof v === "string") {
|
|
216
|
-
const redacted = redactUrlInStr(v, context.secretNames);
|
|
217
|
-
if (redacted !== null)
|
|
218
|
-
value[k] = redacted;
|
|
219
|
-
}
|
|
220
263
|
else {
|
|
221
|
-
redactSecrets(v, context);
|
|
264
|
+
value[k] = depth + 1 >= MAX_DEPTH ? "***" : (redactSecrets(v, context, depth + 1), v);
|
|
222
265
|
}
|
|
223
266
|
}
|
|
224
267
|
}
|
|
225
268
|
else if (Array.isArray(value)) {
|
|
226
269
|
for (let i = 0; i < value.length; i++) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
function redactSecretsStrict(value, context = DEFAULT_CONTEXT) {
|
|
232
|
-
if (isObject(value)) {
|
|
233
|
-
for (const k of Object.keys(value)) {
|
|
234
|
-
const v = value[k];
|
|
235
|
-
if (isSecretKey(k, context.secretNames)) {
|
|
236
|
-
value[k] = "***";
|
|
237
|
-
}
|
|
238
|
-
else if (keyHasUrlSuffix(k) && typeof v === "string") {
|
|
239
|
-
const redacted = redactUrlInStr(v, context.secretNames);
|
|
240
|
-
if (redacted !== null)
|
|
241
|
-
value[k] = redacted;
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
244
|
-
redactSecretsStrict(v, context);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
else if (Array.isArray(value)) {
|
|
249
|
-
for (let i = 0; i < value.length; i++) {
|
|
250
|
-
redactSecretsStrict(value[i], context);
|
|
270
|
+
if (depth + 1 >= MAX_DEPTH)
|
|
271
|
+
value[i] = "***";
|
|
272
|
+
else
|
|
273
|
+
redactSecrets(value[i], context, depth + 1);
|
|
251
274
|
}
|
|
252
275
|
}
|
|
253
276
|
}
|
|
@@ -266,9 +289,6 @@ function applyRedactionPolicyWithContext(value, redactionPolicy, context) {
|
|
|
266
289
|
break;
|
|
267
290
|
case RedactionPolicy.RedactionNone:
|
|
268
291
|
break;
|
|
269
|
-
case RedactionPolicy.RedactionStrict:
|
|
270
|
-
redactSecretsStrict(value, context);
|
|
271
|
-
break;
|
|
272
292
|
default:
|
|
273
293
|
// Empty/unknown policy falls back to default full redaction.
|
|
274
294
|
redactSecrets(value, context);
|
|
@@ -332,12 +352,31 @@ function redactUrlInStr(s, secretNames) {
|
|
|
332
352
|
}
|
|
333
353
|
return `${scheme}://${newAuthority}${newRemainder}`;
|
|
334
354
|
}
|
|
355
|
+
function redactUrlFieldValue(s, secretNames) {
|
|
356
|
+
const redacted = redactUrlInStr(s, secretNames);
|
|
357
|
+
if (redacted !== null)
|
|
358
|
+
return redacted;
|
|
359
|
+
const trimmed = s.trim();
|
|
360
|
+
if (trimmed !== s) {
|
|
361
|
+
const trimmedRedacted = redactUrlInStr(trimmed, secretNames);
|
|
362
|
+
if (trimmedRedacted !== null)
|
|
363
|
+
return trimmedRedacted;
|
|
364
|
+
}
|
|
365
|
+
// Fail closed: a _url value we could not parse as a clean scheme-prefixed
|
|
366
|
+
// URL, yet which carries a credential sigil ('@' userinfo) or internal
|
|
367
|
+
// whitespace, is redacted wholesale rather than passed through. A schemeless
|
|
368
|
+
// connection string like user:pass@host/db has no scheme anchor for the
|
|
369
|
+
// surgical span logic above, so blanket redaction is the safe default.
|
|
370
|
+
if (/\s/.test(s) || s.includes("@"))
|
|
371
|
+
return "***";
|
|
372
|
+
return s;
|
|
373
|
+
}
|
|
335
374
|
/**
|
|
336
375
|
* Replace the userinfo password (user:pass@) with "***", preserving the
|
|
337
376
|
* username. Authority without '@', or userinfo without ':', is unchanged.
|
|
338
377
|
*/
|
|
339
378
|
function redactUserinfoPassword(authority) {
|
|
340
|
-
const at = authority.
|
|
379
|
+
const at = authority.lastIndexOf("@");
|
|
341
380
|
if (at < 0)
|
|
342
381
|
return authority;
|
|
343
382
|
const userinfo = authority.slice(0, at);
|
|
@@ -444,20 +483,29 @@ function tryProcessField(key, value) {
|
|
|
444
483
|
// Group 1: compound timestamp suffixes
|
|
445
484
|
stripped = stripSuffixCI(key, "_epoch_ms");
|
|
446
485
|
if (stripped !== null) {
|
|
447
|
-
if (isInt(value))
|
|
448
|
-
|
|
486
|
+
if (isInt(value)) {
|
|
487
|
+
const formatted = formatRfc3339Ms(value);
|
|
488
|
+
if (formatted !== null)
|
|
489
|
+
return [stripped, formatted];
|
|
490
|
+
}
|
|
449
491
|
return null;
|
|
450
492
|
}
|
|
451
493
|
stripped = stripSuffixCI(key, "_epoch_s");
|
|
452
494
|
if (stripped !== null) {
|
|
453
|
-
if (isInt(value))
|
|
454
|
-
|
|
495
|
+
if (isInt(value)) {
|
|
496
|
+
const formatted = formatRfc3339Ms(value * 1000);
|
|
497
|
+
if (formatted !== null)
|
|
498
|
+
return [stripped, formatted];
|
|
499
|
+
}
|
|
455
500
|
return null;
|
|
456
501
|
}
|
|
457
502
|
stripped = stripSuffixCI(key, "_epoch_ns");
|
|
458
503
|
if (stripped !== null) {
|
|
459
|
-
if (isInt(value))
|
|
460
|
-
|
|
504
|
+
if (isInt(value)) {
|
|
505
|
+
const formatted = formatRfc3339Ms(Math.floor(value / 1_000_000));
|
|
506
|
+
if (formatted !== null)
|
|
507
|
+
return [stripped, formatted];
|
|
508
|
+
}
|
|
461
509
|
return null;
|
|
462
510
|
}
|
|
463
511
|
// Group 2: compound currency suffixes
|
|
@@ -530,9 +578,6 @@ function tryProcessField(key, value) {
|
|
|
530
578
|
return [stripped, `${plainScalar(value)}%`];
|
|
531
579
|
return null;
|
|
532
580
|
}
|
|
533
|
-
stripped = stripSuffixCI(key, "_secret");
|
|
534
|
-
if (stripped !== null)
|
|
535
|
-
return [stripped, "***"];
|
|
536
581
|
// Group 5: short suffixes (last to avoid false positives)
|
|
537
582
|
stripped = stripSuffixCI(key, "_btc");
|
|
538
583
|
if (stripped !== null) {
|
|
@@ -576,6 +621,11 @@ function tryProcessField(key, value) {
|
|
|
576
621
|
function processObjectFields(obj) {
|
|
577
622
|
const entries = [];
|
|
578
623
|
for (const [k, v] of Object.entries(obj)) {
|
|
624
|
+
const secretStripped = stripSuffixCI(k, "_secret");
|
|
625
|
+
if (secretStripped !== null) {
|
|
626
|
+
entries.push({ stripped: secretStripped, original: k, value: v, formatted: null });
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
579
629
|
const result = tryProcessField(k, v);
|
|
580
630
|
if (result !== null) {
|
|
581
631
|
entries.push({ stripped: result[0], original: k, value: v, formatted: result[1] });
|
|
@@ -617,14 +667,15 @@ function formatMsValue(value) {
|
|
|
617
667
|
return formatMsAsSeconds(value);
|
|
618
668
|
return `${plainScalar(value)}ms`;
|
|
619
669
|
}
|
|
670
|
+
const MIN_RFC3339_MS = -62135596800000;
|
|
671
|
+
const MAX_RFC3339_MS = 253402300799999;
|
|
620
672
|
function formatRfc3339Ms(ms) {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
}
|
|
673
|
+
if (ms < MIN_RFC3339_MS || ms > MAX_RFC3339_MS)
|
|
674
|
+
return null;
|
|
675
|
+
const d = new Date(ms);
|
|
676
|
+
if (Number.isNaN(d.getTime()))
|
|
677
|
+
return null;
|
|
678
|
+
return d.toISOString().replace(/(\.\d{3})\d*Z$/, "$1Z");
|
|
628
679
|
}
|
|
629
680
|
/**
|
|
630
681
|
* Round to `digits` decimals using round-half-to-even (banker's rounding) and
|
|
@@ -687,7 +738,7 @@ function extractCurrencyCode(key) {
|
|
|
687
738
|
if (idx < 0)
|
|
688
739
|
return null;
|
|
689
740
|
const code = withoutCents.slice(idx + 1);
|
|
690
|
-
if (
|
|
741
|
+
if (!/^[A-Za-z]{3,4}$/.test(code))
|
|
691
742
|
return null;
|
|
692
743
|
return code;
|
|
693
744
|
}
|
|
@@ -702,23 +753,23 @@ function renderYamlProcessed(value, indent, lines) {
|
|
|
702
753
|
}
|
|
703
754
|
for (const pf of processObjectFields(value)) {
|
|
704
755
|
if (pf.formatted !== null) {
|
|
705
|
-
lines.push(`${prefix}${pf.key}: "${escapeYamlStr(pf.formatted)}"`);
|
|
756
|
+
lines.push(`${prefix}${yamlKey(pf.key)}: "${escapeYamlStr(pf.formatted)}"`);
|
|
706
757
|
}
|
|
707
758
|
else if (isObject(pf.value)) {
|
|
708
759
|
if (Object.keys(pf.value).length > 0) {
|
|
709
|
-
lines.push(`${prefix}${pf.key}:`);
|
|
760
|
+
lines.push(`${prefix}${yamlKey(pf.key)}:`);
|
|
710
761
|
renderYamlProcessed(pf.value, indent + 1, lines);
|
|
711
762
|
}
|
|
712
763
|
else {
|
|
713
|
-
lines.push(`${prefix}${pf.key}: {}`);
|
|
764
|
+
lines.push(`${prefix}${yamlKey(pf.key)}: {}`);
|
|
714
765
|
}
|
|
715
766
|
}
|
|
716
767
|
else if (Array.isArray(pf.value)) {
|
|
717
768
|
if (pf.value.length === 0) {
|
|
718
|
-
lines.push(`${prefix}${pf.key}: []`);
|
|
769
|
+
lines.push(`${prefix}${yamlKey(pf.key)}: []`);
|
|
719
770
|
}
|
|
720
771
|
else {
|
|
721
|
-
lines.push(`${prefix}${pf.key}:`);
|
|
772
|
+
lines.push(`${prefix}${yamlKey(pf.key)}:`);
|
|
722
773
|
for (const item of pf.value) {
|
|
723
774
|
if (isObject(item)) {
|
|
724
775
|
lines.push(`${prefix} -`);
|
|
@@ -731,7 +782,7 @@ function renderYamlProcessed(value, indent, lines) {
|
|
|
731
782
|
}
|
|
732
783
|
}
|
|
733
784
|
else {
|
|
734
|
-
lines.push(`${prefix}${pf.key}: ${yamlScalar(pf.value)}`);
|
|
785
|
+
lines.push(`${prefix}${yamlKey(pf.key)}: ${yamlScalar(pf.value)}`);
|
|
735
786
|
}
|
|
736
787
|
}
|
|
737
788
|
}
|
|
@@ -752,24 +803,24 @@ function renderYamlRaw(value, indent, lines) {
|
|
|
752
803
|
function renderYamlFieldRaw(prefix, key, value, indent, lines) {
|
|
753
804
|
if (isObject(value)) {
|
|
754
805
|
if (Object.keys(value).length > 0) {
|
|
755
|
-
lines.push(`${prefix}${key}:`);
|
|
806
|
+
lines.push(`${prefix}${yamlKey(key)}:`);
|
|
756
807
|
renderYamlRaw(value, indent + 1, lines);
|
|
757
808
|
}
|
|
758
809
|
else {
|
|
759
|
-
lines.push(`${prefix}${key}: {}`);
|
|
810
|
+
lines.push(`${prefix}${yamlKey(key)}: {}`);
|
|
760
811
|
}
|
|
761
812
|
}
|
|
762
813
|
else if (Array.isArray(value)) {
|
|
763
814
|
if (value.length > 0) {
|
|
764
|
-
lines.push(`${prefix}${key}:`);
|
|
815
|
+
lines.push(`${prefix}${yamlKey(key)}:`);
|
|
765
816
|
renderYamlArrayRaw(value, indent + 1, lines);
|
|
766
817
|
}
|
|
767
818
|
else {
|
|
768
|
-
lines.push(`${prefix}${key}: []`);
|
|
819
|
+
lines.push(`${prefix}${yamlKey(key)}: []`);
|
|
769
820
|
}
|
|
770
821
|
}
|
|
771
822
|
else {
|
|
772
|
-
lines.push(`${prefix}${key}: ${yamlScalar(value)}`);
|
|
823
|
+
lines.push(`${prefix}${yamlKey(key)}: ${yamlScalar(value)}`);
|
|
773
824
|
}
|
|
774
825
|
}
|
|
775
826
|
function renderYamlArrayRaw(arr, indent, lines) {
|
|
@@ -799,7 +850,17 @@ function renderYamlArrayRaw(arr, indent, lines) {
|
|
|
799
850
|
}
|
|
800
851
|
}
|
|
801
852
|
function escapeYamlStr(s) {
|
|
802
|
-
return s
|
|
853
|
+
return s
|
|
854
|
+
.replace(/\\/g, "\\\\")
|
|
855
|
+
.replace(/"/g, '\\"')
|
|
856
|
+
.replace(/\n/g, "\\n")
|
|
857
|
+
.replace(/\r/g, "\\r")
|
|
858
|
+
.replace(/\t/g, "\\t")
|
|
859
|
+
.replace(/\f/g, "\\f")
|
|
860
|
+
.replace(/\v/g, "\\v");
|
|
861
|
+
}
|
|
862
|
+
function yamlKey(key) {
|
|
863
|
+
return /^[A-Za-z0-9_.-]+$/.test(key) ? key : `"${escapeYamlStr(key)}"`;
|
|
803
864
|
}
|
|
804
865
|
function yamlScalar(value) {
|
|
805
866
|
if (typeof value === "string")
|
|
@@ -810,7 +871,7 @@ function yamlScalar(value) {
|
|
|
810
871
|
return value.toString();
|
|
811
872
|
if (typeof value === "number")
|
|
812
873
|
return value.toString();
|
|
813
|
-
return `"${
|
|
874
|
+
return `"${escapeYamlStr(JSON.stringify(sortJsonValue(value)))}"`;
|
|
814
875
|
}
|
|
815
876
|
// ═══════════════════════════════════════════
|
|
816
877
|
// Plain Rendering (logfmt)
|
|
@@ -866,7 +927,7 @@ function plainScalar(value) {
|
|
|
866
927
|
return value.toString();
|
|
867
928
|
if (typeof value === "number")
|
|
868
929
|
return value.toString();
|
|
869
|
-
return
|
|
930
|
+
return JSON.stringify(sortJsonValue(value));
|
|
870
931
|
}
|
|
871
932
|
function plainScalarRaw(value) {
|
|
872
933
|
if (isObject(value) || Array.isArray(value)) {
|
|
@@ -884,13 +945,20 @@ function quoteLogfmtValue(value) {
|
|
|
884
945
|
.replace(/"/g, '\\"')
|
|
885
946
|
.replace(/\n/g, "\\n")
|
|
886
947
|
.replace(/\r/g, "\\r")
|
|
887
|
-
.replace(/\t/g, "\\t")
|
|
948
|
+
.replace(/\t/g, "\\t")
|
|
949
|
+
.replace(/\f/g, "\\f")
|
|
950
|
+
.replace(/\v/g, "\\v");
|
|
888
951
|
return `"${escaped}"`;
|
|
889
952
|
}
|
|
953
|
+
function quoteLogfmtKey(key) {
|
|
954
|
+
return /^[A-Za-z0-9_.-]+$/.test(key) ? key : quoteLogfmtValue(key);
|
|
955
|
+
}
|
|
890
956
|
// ═══════════════════════════════════════════
|
|
891
957
|
// Utilities
|
|
892
958
|
// ═══════════════════════════════════════════
|
|
893
|
-
function sanitizeForJson(value, stack = new WeakSet()) {
|
|
959
|
+
function sanitizeForJson(value, stack = new WeakSet(), depth = 0) {
|
|
960
|
+
if (depth >= MAX_DEPTH)
|
|
961
|
+
return "***";
|
|
894
962
|
if (value === null)
|
|
895
963
|
return null;
|
|
896
964
|
const t = typeof value;
|
|
@@ -918,7 +986,7 @@ function sanitizeForJson(value, stack = new WeakSet()) {
|
|
|
918
986
|
if (stack.has(value))
|
|
919
987
|
return "<unsupported:circular>";
|
|
920
988
|
stack.add(value);
|
|
921
|
-
const out = value.map((item) => sanitizeForJson(item, stack));
|
|
989
|
+
const out = value.map((item) => sanitizeForJson(item, stack, depth + 1));
|
|
922
990
|
stack.delete(value);
|
|
923
991
|
return out;
|
|
924
992
|
}
|
|
@@ -929,7 +997,7 @@ function sanitizeForJson(value, stack = new WeakSet()) {
|
|
|
929
997
|
stack.add(obj);
|
|
930
998
|
const out = {};
|
|
931
999
|
for (const [k, v] of Object.entries(obj)) {
|
|
932
|
-
out[k] = sanitizeForJson(v, stack);
|
|
1000
|
+
out[k] = sanitizeForJson(v, stack, depth + 1);
|
|
933
1001
|
}
|
|
934
1002
|
stack.delete(obj);
|
|
935
1003
|
return out;
|