@loadstrike/loadstrike-sdk 1.0.30401 → 1.0.31601
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 +6 -0
- package/dist/cjs/iteration-observation-diagnostics.js +513 -0
- package/dist/cjs/iteration-observations.js +195 -22
- package/dist/cjs/local-report-input.js +21 -0
- package/dist/cjs/local.js +55 -10
- package/dist/cjs/report-history.js +421 -0
- package/dist/cjs/reporting-svg.js +116 -0
- package/dist/cjs/reporting.js +436 -140
- package/dist/cjs/runtime.js +313 -85
- package/dist/cjs/sink-retry-policy.js +52 -0
- package/dist/cjs/sinks.js +112 -9
- package/dist/cjs/transports.js +78 -25
- package/dist/esm/iteration-observation-diagnostics.js +508 -0
- package/dist/esm/iteration-observations.js +195 -22
- package/dist/esm/local-report-input.js +17 -0
- package/dist/esm/local.js +55 -10
- package/dist/esm/report-history.js +413 -0
- package/dist/esm/reporting-svg.js +113 -0
- package/dist/esm/reporting.js +436 -140
- package/dist/esm/runtime.js +313 -85
- package/dist/esm/sink-retry-policy.js +44 -0
- package/dist/esm/sinks.js +112 -9
- package/dist/esm/transports.js +78 -25
- package/dist/types/iteration-observation-diagnostics.d.ts +21 -0
- package/dist/types/iteration-observations.d.ts +5 -0
- package/dist/types/local-report-input.d.ts +6 -0
- package/dist/types/report-history.d.ts +124 -0
- package/dist/types/reporting-svg.d.ts +2 -0
- package/dist/types/reporting.d.ts +6 -3
- package/dist/types/runtime.d.ts +25 -0
- package/dist/types/sink-retry-policy.d.ts +9 -0
- package/dist/types/sinks.d.ts +6 -0
- package/dist/types/transports.d.ts +4 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -68,12 +68,18 @@ Non-correlated V2 scenarios can run through the local-development cluster or a r
|
|
|
68
68
|
|
|
69
69
|
HTML reports include a Generator Delivery tab whenever scheduler delivery data, raw observation delivery statistics, generator or reporting warnings, or incomplete reporting are available. Results without reporting-completeness status show N/A rather than reporting loss. Application failures remain separate from generator and reporting warnings.
|
|
70
70
|
|
|
71
|
+
HTML charts are responsive SVG graphics embedded in the offline report. They provide exact-value pointer, touch, and keyboard tooltips; outcome legends; zoom, pan, and reset; an accessible expanded view; chart-title search; and compact, comfortable, or spacious grids. Successful and failed latency stay separate, while All appears only when the run has a genuine combined distribution. When temporal history is available, cumulative requests, achieved request rate, bytes, and per-scenario latency include the final partial reporting interval. Correlation charts retain scenario, destination, status, GatherBy selector/value, and all available percentile points without averaging groups.
|
|
72
|
+
|
|
71
73
|
## Raw Iteration Reporting
|
|
72
74
|
|
|
73
75
|
Observation-capable reporting sinks receive one compact record for every scenario attempt, including retry attempts and nested steps. Retries share a logical iteration ID while retaining distinct attempt indexes and final-attempt markers. Warm-up and bombing traffic, simulation and shard identity, UTC nanosecond timestamps, observed and reported latency, outcome, status code, and response size are included; reply messages, payloads, bodies, and headers are not.
|
|
74
76
|
|
|
77
|
+
If a fail-mode runtime policy callback fails after an attempt begins, the stream receives one final failed observation with status `runtime_policy_error` before the run terminates. The observation does not include the callback error text.
|
|
78
|
+
|
|
75
79
|
Portal reporting sends compressed batches, while the JSONL and generic webhook sinks preserve the canonical observation records. Custom sinks can opt in with `saveIterationBatch` or `SaveIterationBatch` while keeping all existing aggregate lifecycle callbacks unchanged. Batches flush every five seconds; the defaults and the portal-compatible common shape are 50,000 observations or 8 MiB before compression. Runs without a portal sink may select the documented larger limits. Capture and sink queues are memory bounded; buffer pressure, a single record that cannot fit a batch, and sink pressure drop only the affected reporting observations with explicit warnings instead of changing application failures. Metric-only destinations disclose their reduced observation shape. The final observation flush and optional stream-completion callback finish before `saveRunResult` or `SaveRunResult`.
|
|
76
80
|
|
|
81
|
+
Every reporting-sink callback—including initialization, start, realtime statistics and metrics, final statistics and metrics, raw batches, completion markers, stop, and dispose—is attempted once and then retried up to three times by default, after 250 ms, 500 ms, and 1 second. Set `sinkRetryCount` and `sinkRetryBackoffMs`, or the matching Pascal-case configuration keys, to select a bounded policy of zero through 100 retries. A recovered callback adds no final sink error or delivery-failed warning. Only an exhausted raw-observation delivery counts as sink observation loss; other exhausted callbacks are reported against that sink without failing the workload. Custom sinks have at-least-once delivery semantics and should deduplicate replay by stable batch or observation identity. Stop and dispose remain best-effort cleanup, and an exhausted stop callback does not prevent dispose. Sanitized nested error details stay in the local run log rather than generator warnings, portable results, portal payloads, or HTML reports.
|
|
82
|
+
|
|
77
83
|
Portal trend p50, p75, p95, and p99 values are calculated cumulatively from every final bombing-phase outcome received for that scenario and run. Separate successful and failed distributions remain available for diagnosis; SDK-calculated percentile fields are not sent as the authoritative portal or observation-capable sink result.
|
|
78
84
|
|
|
79
85
|
Use runner configuration fields such as `iterationObservationFlushIntervalSeconds`, `maxIterationObservationBufferBytes`, `maxIterationObservationsPerBatch`, `maxIterationObservationBatchBytes`, `iterationObservationSinkQueueDepth`, `iterationObservationSinkParallelism`, and `iterationObservationDrainTimeoutSeconds` when the defaults need to be adjusted within their documented bounds.
|
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.logIterationObservationFailure = logIterationObservationFailure;
|
|
4
|
+
exports.logIterationObservationRecovery = logIterationObservationRecovery;
|
|
5
|
+
exports.redactIterationObservationSecrets = redactIterationObservationSecrets;
|
|
6
|
+
const MAXIMUM_DIAGNOSTIC_BYTES = 16 * 1024;
|
|
7
|
+
const MAXIMUM_SOURCE_CHARACTERS = 8 * 1024;
|
|
8
|
+
const MAXIMUM_CAUSAL_DEPTH = 8;
|
|
9
|
+
const MAXIMUM_CAUSAL_NODES = 32;
|
|
10
|
+
const SOURCE_TRUNCATION_SUFFIX = " [truncated]";
|
|
11
|
+
const SENSITIVE_FIELD_NAME_SOURCE = "(?:authorization|proxy(?:[_-]|\\s+)?authorization|body|payload"
|
|
12
|
+
+ "|request(?:[_-]|\\s+)?(?:body|payload)"
|
|
13
|
+
+ "|response(?:[_-]|\\s+)?(?:body|payload)"
|
|
14
|
+
+ "|observation(?:[_-]|\\s+)?(?:body|payload)"
|
|
15
|
+
+ "|runner(?:[_-]|\\s+)?key"
|
|
16
|
+
+ "|api(?:[_-]|\\s+)?(?:key|token)"
|
|
17
|
+
+ "|x(?:[_-]|\\s+)?api(?:[_-]|\\s+)?(?:key|token)"
|
|
18
|
+
+ "|run(?:[_-]|\\s+)?token|access(?:[_-]|\\s+)?token"
|
|
19
|
+
+ "|refresh(?:[_-]|\\s+)?token|id(?:[_-]|\\s+)?token"
|
|
20
|
+
+ "|token|password|passwd|pwd|client(?:[_-]|\\s+)?secret"
|
|
21
|
+
+ "|connection(?:[_-]|\\s+)?string|secret)";
|
|
22
|
+
const SENSITIVE_FIELD_NAME_PATTERN = new RegExp(`^${SENSITIVE_FIELD_NAME_SOURCE}$`, "iu");
|
|
23
|
+
const SENSITIVE_FIELD_PATTERN = new RegExp(`(^|[\\s{(,;=.]|\\[\\s*)(["']?)(${SENSITIVE_FIELD_NAME_SOURCE})\\2((?:\\s*\\[\\s*\\d+\\s*\\])*)(\\s*\\])?\\s*[:=]\\s*`, "giu");
|
|
24
|
+
const NEXT_SENSITIVE_FIELD_PATTERN = new RegExp(`\\s+(?=(?:(?:[a-z_$][\\w$.-]*\\s*)?\\[\\s*)?["']?${SENSITIVE_FIELD_NAME_SOURCE}["']?(?:\\s*\\[\\s*\\d+\\s*\\])*\\s*\\]?\\s*[:=])`, "giu");
|
|
25
|
+
const XML_ELEMENT_TAG_PATTERN = /<\s*(\/?)\s*([a-z_][\w.:-]*)(?:\s[^<>]*?)?\s*(\/?)>/giu;
|
|
26
|
+
const DIAGNOSTIC_FIELD_BOUNDARY_PATTERN = /\s+(?=(?:stack|code|errno|syscall|status|statuscode|statustext)\s*[:=])/giu;
|
|
27
|
+
const CAUSAL_BOUNDARY_PATTERN = /\s+\|\s+/gu;
|
|
28
|
+
const SOURCE_TRUNCATION_BOUNDARY_PATTERN = /\s+\[truncated\](?=\s|$)/giu;
|
|
29
|
+
const STANDALONE_CREDENTIAL_BOUNDARY_PATTERN = /\s+(?=(?:bearer|basic)\s+(?:["']|\[REDACTED\]|[a-z0-9._~+/=-]+))/giu;
|
|
30
|
+
const STANDALONE_CREDENTIAL_PREFIX_PATTERN = /\b(bearer|basic)\s+/giu;
|
|
31
|
+
function logIterationObservationFailure(logger, level, context, error) {
|
|
32
|
+
const event = formatIterationObservationDiagnostic("observation sink callback failed", context, error);
|
|
33
|
+
safeLog(logger, level, event);
|
|
34
|
+
}
|
|
35
|
+
function logIterationObservationRecovery(logger, context) {
|
|
36
|
+
const event = formatIterationObservationDiagnostic("observation sink callback recovered", context);
|
|
37
|
+
safeLog(logger, "debug", event);
|
|
38
|
+
}
|
|
39
|
+
function formatIterationObservationDiagnostic(event, context, error) {
|
|
40
|
+
const fields = [
|
|
41
|
+
`event=${event}`,
|
|
42
|
+
`sink=${safeText(context.sinkName)}`,
|
|
43
|
+
`operation=${context.operation}`,
|
|
44
|
+
...(context.phase ? [`phase=${safeText(context.phase)}`] : []),
|
|
45
|
+
`runId=${safeText(context.runId)}`,
|
|
46
|
+
`resultOwnerId=${safeText(context.resultOwnerId)}`,
|
|
47
|
+
...(context.batchId ? [`batchId=${safeText(context.batchId)}`] : []),
|
|
48
|
+
...(context.batchSequence64
|
|
49
|
+
? [`batchSequence64=${safeText(context.batchSequence64)}`]
|
|
50
|
+
: []),
|
|
51
|
+
`observationCount=${safeInteger(context.observationCount)}`,
|
|
52
|
+
`attempt=${safeInteger(context.attempt)}/${safeInteger(context.maximumAttempts)}`,
|
|
53
|
+
...(context.nextDelayMs === undefined
|
|
54
|
+
? []
|
|
55
|
+
: [`nextDelayMs=${safeInteger(context.nextDelayMs)}`])
|
|
56
|
+
];
|
|
57
|
+
if (error !== undefined) {
|
|
58
|
+
fields.push(`errorChain=${formatErrorChain(error)}`);
|
|
59
|
+
}
|
|
60
|
+
return truncateUtf8(redactIterationObservationSecrets(normalizeText(fields.join(" "))), MAXIMUM_DIAGNOSTIC_BYTES);
|
|
61
|
+
}
|
|
62
|
+
function formatErrorChain(error) {
|
|
63
|
+
const lines = [];
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
const state = { nodes: 0, nodeLimitWritten: false };
|
|
66
|
+
visitError(error, "error", 0, seen, state, lines);
|
|
67
|
+
return lines.join(" | ");
|
|
68
|
+
}
|
|
69
|
+
function visitError(value, path, depth, seen, state, lines) {
|
|
70
|
+
if (depth >= MAXIMUM_CAUSAL_DEPTH) {
|
|
71
|
+
lines.push(`${path}: [truncated at causal depth ${MAXIMUM_CAUSAL_DEPTH}]`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (state.nodes >= MAXIMUM_CAUSAL_NODES) {
|
|
75
|
+
if (!state.nodeLimitWritten) {
|
|
76
|
+
state.nodeLimitWritten = true;
|
|
77
|
+
lines.push(`[truncated after ${MAXIMUM_CAUSAL_NODES} causal nodes]`);
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
state.nodes += 1;
|
|
82
|
+
if ((typeof value !== "object" && typeof value !== "function") || value === null) {
|
|
83
|
+
lines.push(`${path}: ${primitiveType(value)} message=${safeText(value)}`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (seen.has(value)) {
|
|
87
|
+
lines.push(`${path}: [cycle]`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
seen.add(value);
|
|
91
|
+
const name = safePropertyText(value, "name") || objectType(value);
|
|
92
|
+
const message = safePropertyText(value, "message");
|
|
93
|
+
const details = [`${path}: ${name}`];
|
|
94
|
+
if (message) {
|
|
95
|
+
details.push(`message=${message}`);
|
|
96
|
+
}
|
|
97
|
+
for (const key of ["code", "errno", "syscall", "status", "statusCode", "statusText"]) {
|
|
98
|
+
const resolved = safePropertyText(value, key);
|
|
99
|
+
if (resolved) {
|
|
100
|
+
details.push(`${key}=${resolved}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const stack = safePropertyText(value, "stack", 4096);
|
|
104
|
+
if (stack) {
|
|
105
|
+
details.push(`stack=${stack}`);
|
|
106
|
+
}
|
|
107
|
+
lines.push(details.join(" "));
|
|
108
|
+
const cause = safeProperty(value, "cause");
|
|
109
|
+
if (cause.available && cause.value !== undefined && cause.value !== null) {
|
|
110
|
+
visitError(cause.value, `${path}.cause`, depth + 1, seen, state, lines);
|
|
111
|
+
}
|
|
112
|
+
else if (!cause.available) {
|
|
113
|
+
lines.push(`${path}.cause: [unavailable]`);
|
|
114
|
+
}
|
|
115
|
+
const aggregate = safeProperty(value, "errors");
|
|
116
|
+
if (!aggregate.available) {
|
|
117
|
+
lines.push(`${path}.errors: [unavailable]`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const members = safeArrayMembers(aggregate.value);
|
|
121
|
+
for (let index = 0; index < members.length; index += 1) {
|
|
122
|
+
visitError(members[index], `${path}.errors[${index}]`, depth + 1, seen, state, lines);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function safeArrayMembers(value) {
|
|
126
|
+
try {
|
|
127
|
+
if (!Array.isArray(value)) {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
const length = Math.min(value.length, MAXIMUM_CAUSAL_NODES);
|
|
131
|
+
const result = [];
|
|
132
|
+
for (let index = 0; index < length; index += 1) {
|
|
133
|
+
try {
|
|
134
|
+
result.push(value[index]);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
result.push("[unavailable aggregate member]");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function safeProperty(value, key) {
|
|
147
|
+
try {
|
|
148
|
+
return { available: true, value: Reflect.get(value, key) };
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return { available: false, value: undefined };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function safePropertyText(value, key, maximumCharacters = MAXIMUM_SOURCE_CHARACTERS) {
|
|
155
|
+
const property = safeProperty(value, key);
|
|
156
|
+
return property.available ? safeText(property.value, maximumCharacters) : "";
|
|
157
|
+
}
|
|
158
|
+
function objectType(value) {
|
|
159
|
+
try {
|
|
160
|
+
const tag = Object.prototype.toString.call(value);
|
|
161
|
+
const match = /^\[object ([^\]]+)\]$/u.exec(tag);
|
|
162
|
+
return match?.[1] || "Object";
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return "Object";
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function primitiveType(value) {
|
|
169
|
+
return value === null ? "null" : typeof value;
|
|
170
|
+
}
|
|
171
|
+
function safeText(value, maximumCharacters = MAXIMUM_SOURCE_CHARACTERS) {
|
|
172
|
+
let source;
|
|
173
|
+
try {
|
|
174
|
+
source = typeof value === "string" ? value : String(value ?? "");
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
source = "[unavailable]";
|
|
178
|
+
}
|
|
179
|
+
return normalizeText(boundSourceCharacters(source, maximumCharacters));
|
|
180
|
+
}
|
|
181
|
+
function normalizeText(value) {
|
|
182
|
+
const validUtf8 = Buffer.from(value, "utf8").toString("utf8");
|
|
183
|
+
return validUtf8
|
|
184
|
+
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/gu, " ")
|
|
185
|
+
.replace(/[\r\n\t]+/gu, " ")
|
|
186
|
+
.replace(/\s{2,}/gu, " ")
|
|
187
|
+
.trim();
|
|
188
|
+
}
|
|
189
|
+
function redactIterationObservationSecrets(value) {
|
|
190
|
+
let output = boundSourceCharacters(value, MAXIMUM_SOURCE_CHARACTERS * 2);
|
|
191
|
+
output = redactSensitiveXmlElements(output);
|
|
192
|
+
output = redactBoundedSensitiveFields(output);
|
|
193
|
+
output = redactStandaloneCredentials(output);
|
|
194
|
+
output = output.replace(/([?&](?:authorization|runner[_-]?key|api[_-]?(?:key|token)|x[_-]?api[_-]?(?:key|token)|run[_-]?token|access[_-]?token|refresh[_-]?token|id[_-]?token|security[_-]?token|token|password|passwd|pwd|client[_-]?secret|connection[_-]?string|account[_-]?key|key|secret|signature|sig|credential|code|x-(?:amz|goog)-(?:credential|signature|security-token))=)[^&#\s]*/giu, "$1[REDACTED]");
|
|
195
|
+
output = output.replace(/(\b[a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@/giu, "$1[REDACTED]@");
|
|
196
|
+
output = output.replace(/(\b[a-z][a-z0-9+.-]*:\/\/)[^\s/@]+(?=\s+\[truncated\](?:\s|$))/giu, "$1[REDACTED]");
|
|
197
|
+
return output;
|
|
198
|
+
}
|
|
199
|
+
function redactStandaloneCredentials(value) {
|
|
200
|
+
let output = "";
|
|
201
|
+
let cursor = 0;
|
|
202
|
+
STANDALONE_CREDENTIAL_PREFIX_PATTERN.lastIndex = 0;
|
|
203
|
+
for (let match = STANDALONE_CREDENTIAL_PREFIX_PATTERN.exec(value); match; match = STANDALONE_CREDENTIAL_PREFIX_PATTERN.exec(value)) {
|
|
204
|
+
if (match.index < cursor) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const valueStart = STANDALONE_CREDENTIAL_PREFIX_PATTERN.lastIndex;
|
|
208
|
+
const valueEnd = standaloneCredentialValueEnd(value, valueStart);
|
|
209
|
+
output += value.slice(cursor, match.index);
|
|
210
|
+
output += `${match[1]} [REDACTED]`;
|
|
211
|
+
cursor = Math.max(valueEnd, valueStart);
|
|
212
|
+
STANDALONE_CREDENTIAL_PREFIX_PATTERN.lastIndex = cursor;
|
|
213
|
+
}
|
|
214
|
+
output += value.slice(cursor);
|
|
215
|
+
return output;
|
|
216
|
+
}
|
|
217
|
+
function standaloneCredentialValueEnd(source, valueStart) {
|
|
218
|
+
if (valueStart >= source.length) {
|
|
219
|
+
return source.length;
|
|
220
|
+
}
|
|
221
|
+
const hardBoundary = earliestPatternIndex(source, valueStart, CAUSAL_BOUNDARY_PATTERN, DIAGNOSTIC_FIELD_BOUNDARY_PATTERN, SOURCE_TRUNCATION_BOUNDARY_PATTERN);
|
|
222
|
+
const quote = source[valueStart];
|
|
223
|
+
if (quote === "\"" || quote === "'") {
|
|
224
|
+
const quotedEnd = quotedValueEnd(source, valueStart);
|
|
225
|
+
if (quotedEnd !== null
|
|
226
|
+
&& (hardBoundary === null || quotedEnd <= hardBoundary)) {
|
|
227
|
+
if (isStandaloneCredentialBoundary(source, quotedEnd)) {
|
|
228
|
+
return quotedEnd;
|
|
229
|
+
}
|
|
230
|
+
return standaloneCredentialScalarEnd(source, quotedEnd, hardBoundary);
|
|
231
|
+
}
|
|
232
|
+
return hardBoundary ?? source.length;
|
|
233
|
+
}
|
|
234
|
+
return standaloneCredentialScalarEnd(source, valueStart, hardBoundary);
|
|
235
|
+
}
|
|
236
|
+
function standaloneCredentialScalarEnd(source, valueStart, hardBoundary) {
|
|
237
|
+
const scalarBoundary = earliestCharacterIndex(source, valueStart, " \t\r\n,;|");
|
|
238
|
+
return minimumIndex(hardBoundary, scalarBoundary) ?? source.length;
|
|
239
|
+
}
|
|
240
|
+
function isStandaloneCredentialBoundary(source, index) {
|
|
241
|
+
return index >= source.length || /^[\s,;|]/u.test(source.slice(index));
|
|
242
|
+
}
|
|
243
|
+
function boundSourceCharacters(value, maximumCharacters) {
|
|
244
|
+
const limit = Math.max(Math.trunc(maximumCharacters), 0);
|
|
245
|
+
if (value.length <= limit) {
|
|
246
|
+
return value;
|
|
247
|
+
}
|
|
248
|
+
if (limit <= SOURCE_TRUNCATION_SUFFIX.length) {
|
|
249
|
+
return SOURCE_TRUNCATION_SUFFIX.slice(0, limit);
|
|
250
|
+
}
|
|
251
|
+
return `${value.slice(0, limit - SOURCE_TRUNCATION_SUFFIX.length)}${SOURCE_TRUNCATION_SUFFIX}`;
|
|
252
|
+
}
|
|
253
|
+
function redactBoundedSensitiveFields(value) {
|
|
254
|
+
let output = "";
|
|
255
|
+
let cursor = 0;
|
|
256
|
+
SENSITIVE_FIELD_PATTERN.lastIndex = 0;
|
|
257
|
+
for (let match = SENSITIVE_FIELD_PATTERN.exec(value); match; match = SENSITIVE_FIELD_PATTERN.exec(value)) {
|
|
258
|
+
if (match.index < cursor) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const prefix = match[1] ?? "";
|
|
262
|
+
const key = match[3] ?? "";
|
|
263
|
+
const indexSuffix = match[4] ?? "";
|
|
264
|
+
const bracketSuffix = match[5] ?? "";
|
|
265
|
+
const valueStart = SENSITIVE_FIELD_PATTERN.lastIndex;
|
|
266
|
+
const valueEnd = sensitiveFieldValueEnd(value, valueStart, key);
|
|
267
|
+
output += value.slice(cursor, match.index);
|
|
268
|
+
output += `${prefix}${key}${indexSuffix}${bracketSuffix}=[REDACTED]`;
|
|
269
|
+
cursor = Math.max(valueEnd, valueStart);
|
|
270
|
+
SENSITIVE_FIELD_PATTERN.lastIndex = cursor;
|
|
271
|
+
}
|
|
272
|
+
output += value.slice(cursor);
|
|
273
|
+
return output;
|
|
274
|
+
}
|
|
275
|
+
function redactSensitiveXmlElements(value) {
|
|
276
|
+
let output = "";
|
|
277
|
+
let cursor = 0;
|
|
278
|
+
let sensitiveElementName = null;
|
|
279
|
+
let sensitiveElementDepth = 0;
|
|
280
|
+
XML_ELEMENT_TAG_PATTERN.lastIndex = 0;
|
|
281
|
+
for (let match = XML_ELEMENT_TAG_PATTERN.exec(value); match; match = XML_ELEMENT_TAG_PATTERN.exec(value)) {
|
|
282
|
+
const isClosingTag = match[1] === "/";
|
|
283
|
+
const elementName = (match[2] ?? "").toLowerCase();
|
|
284
|
+
const isSelfClosingTag = match[3] === "/";
|
|
285
|
+
if (sensitiveElementName === null) {
|
|
286
|
+
if (isClosingTag
|
|
287
|
+
|| isSelfClosingTag
|
|
288
|
+
|| !isSensitiveXmlElementName(elementName)) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
output += value.slice(cursor, XML_ELEMENT_TAG_PATTERN.lastIndex);
|
|
292
|
+
output += "[REDACTED]";
|
|
293
|
+
cursor = XML_ELEMENT_TAG_PATTERN.lastIndex;
|
|
294
|
+
sensitiveElementName = elementName;
|
|
295
|
+
sensitiveElementDepth = 1;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (elementName !== sensitiveElementName) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (!isClosingTag && !isSelfClosingTag) {
|
|
302
|
+
sensitiveElementDepth += 1;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (!isClosingTag) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
sensitiveElementDepth -= 1;
|
|
309
|
+
if (sensitiveElementDepth === 0) {
|
|
310
|
+
output += match[0];
|
|
311
|
+
cursor = XML_ELEMENT_TAG_PATTERN.lastIndex;
|
|
312
|
+
sensitiveElementName = null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (sensitiveElementName === null) {
|
|
316
|
+
output += value.slice(cursor);
|
|
317
|
+
}
|
|
318
|
+
return output;
|
|
319
|
+
}
|
|
320
|
+
function isSensitiveXmlElementName(elementName) {
|
|
321
|
+
const namespaceSeparator = elementName.lastIndexOf(":");
|
|
322
|
+
const localName = namespaceSeparator < 0
|
|
323
|
+
? elementName
|
|
324
|
+
: elementName.slice(namespaceSeparator + 1);
|
|
325
|
+
return SENSITIVE_FIELD_NAME_PATTERN.test(localName);
|
|
326
|
+
}
|
|
327
|
+
function sensitiveFieldValueEnd(source, valueStart, key) {
|
|
328
|
+
if (valueStart >= source.length) {
|
|
329
|
+
return source.length;
|
|
330
|
+
}
|
|
331
|
+
const normalizedKey = key.replace(/[_\s-]/gu, "").toLowerCase();
|
|
332
|
+
const hardBoundary = earliestPatternIndex(source, valueStart, CAUSAL_BOUNDARY_PATTERN, DIAGNOSTIC_FIELD_BOUNDARY_PATTERN);
|
|
333
|
+
if (normalizedKey === "authorization"
|
|
334
|
+
|| normalizedKey === "proxyauthorization") {
|
|
335
|
+
return hardBoundary ?? source.length;
|
|
336
|
+
}
|
|
337
|
+
const markerEnd = redactionMarkerPrefixEnd(source, valueStart);
|
|
338
|
+
let boundarySearchStart = markerEnd ?? valueStart;
|
|
339
|
+
if (markerEnd === null) {
|
|
340
|
+
const quotedEnd = quotedValueEnd(source, valueStart);
|
|
341
|
+
if (quotedEnd !== null) {
|
|
342
|
+
if (isRecognizedSensitiveValueBoundary(source, quotedEnd)) {
|
|
343
|
+
return quotedEnd;
|
|
344
|
+
}
|
|
345
|
+
boundarySearchStart = quotedEnd;
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
const structuredEnd = structuredValueEnd(source, valueStart);
|
|
349
|
+
if (structuredEnd !== null) {
|
|
350
|
+
if (isRecognizedSensitiveValueBoundary(source, structuredEnd)) {
|
|
351
|
+
return structuredEnd;
|
|
352
|
+
}
|
|
353
|
+
boundarySearchStart = structuredEnd;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (normalizedKey === "connectionstring") {
|
|
358
|
+
return hardBoundary ?? source.length;
|
|
359
|
+
}
|
|
360
|
+
const nextFieldBoundary = earliestPatternIndex(source, boundarySearchStart, NEXT_SENSITIVE_FIELD_PATTERN, STANDALONE_CREDENTIAL_BOUNDARY_PATTERN);
|
|
361
|
+
const fieldBoundary = minimumIndex(hardBoundary, nextFieldBoundary);
|
|
362
|
+
if (normalizedKey === "body"
|
|
363
|
+
|| normalizedKey === "payload"
|
|
364
|
+
|| normalizedKey.endsWith("body")
|
|
365
|
+
|| normalizedKey.endsWith("payload")) {
|
|
366
|
+
return fieldBoundary ?? source.length;
|
|
367
|
+
}
|
|
368
|
+
const scalarBoundary = earliestCharacterIndex(source, boundarySearchStart, ";,&}]");
|
|
369
|
+
return minimumIndex(fieldBoundary, scalarBoundary) ?? source.length;
|
|
370
|
+
}
|
|
371
|
+
function redactionMarkerPrefixEnd(source, valueStart) {
|
|
372
|
+
const marker = /^(?:\[REDACTED\])+/iu.exec(source.slice(valueStart));
|
|
373
|
+
return marker ? valueStart + marker[0].length : null;
|
|
374
|
+
}
|
|
375
|
+
function isRecognizedSensitiveValueBoundary(source, index) {
|
|
376
|
+
if (index >= source.length) {
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
if (/^[;,&}\]]/u.test(source.slice(index))) {
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
return earliestPatternIndex(source, index, CAUSAL_BOUNDARY_PATTERN, DIAGNOSTIC_FIELD_BOUNDARY_PATTERN, NEXT_SENSITIVE_FIELD_PATTERN, STANDALONE_CREDENTIAL_BOUNDARY_PATTERN) === index;
|
|
383
|
+
}
|
|
384
|
+
function quotedValueEnd(source, valueStart) {
|
|
385
|
+
const quote = source[valueStart];
|
|
386
|
+
if (quote !== "\"" && quote !== "'") {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
let escaped = false;
|
|
390
|
+
for (let index = valueStart + 1; index < source.length; index += 1) {
|
|
391
|
+
const token = source[index];
|
|
392
|
+
if (escaped) {
|
|
393
|
+
escaped = false;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (token === "\\") {
|
|
397
|
+
escaped = true;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (token === quote) {
|
|
401
|
+
return index + 1;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
function structuredValueEnd(source, valueStart) {
|
|
407
|
+
const opening = source[valueStart];
|
|
408
|
+
if (opening !== "{" && opening !== "[") {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
const stack = [opening];
|
|
412
|
+
let quote = "";
|
|
413
|
+
let escaped = false;
|
|
414
|
+
for (let index = valueStart + 1; index < source.length; index += 1) {
|
|
415
|
+
const token = source[index];
|
|
416
|
+
if (quote) {
|
|
417
|
+
if (escaped) {
|
|
418
|
+
escaped = false;
|
|
419
|
+
}
|
|
420
|
+
else if (token === "\\") {
|
|
421
|
+
escaped = true;
|
|
422
|
+
}
|
|
423
|
+
else if (token === quote) {
|
|
424
|
+
quote = "";
|
|
425
|
+
}
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (token === "\"" || token === "'") {
|
|
429
|
+
quote = token;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (token === "{" || token === "[") {
|
|
433
|
+
stack.push(token);
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (token === "}" || token === "]") {
|
|
437
|
+
const expected = token === "}" ? "{" : "[";
|
|
438
|
+
if (stack.at(-1) !== expected) {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
stack.pop();
|
|
442
|
+
if (!stack.length) {
|
|
443
|
+
return index + 1;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
function earliestPatternIndex(source, start, ...patterns) {
|
|
450
|
+
let earliest = null;
|
|
451
|
+
for (const pattern of patterns) {
|
|
452
|
+
pattern.lastIndex = start;
|
|
453
|
+
const match = pattern.exec(source);
|
|
454
|
+
if (match && (earliest === null || match.index < earliest)) {
|
|
455
|
+
earliest = match.index;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return earliest;
|
|
459
|
+
}
|
|
460
|
+
function earliestCharacterIndex(source, start, characters) {
|
|
461
|
+
let earliest = null;
|
|
462
|
+
for (const character of characters) {
|
|
463
|
+
const index = source.indexOf(character, start);
|
|
464
|
+
if (index >= 0 && (earliest === null || index < earliest)) {
|
|
465
|
+
earliest = index;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return earliest;
|
|
469
|
+
}
|
|
470
|
+
function minimumIndex(left, right) {
|
|
471
|
+
if (left === null)
|
|
472
|
+
return right;
|
|
473
|
+
if (right === null)
|
|
474
|
+
return left;
|
|
475
|
+
return Math.min(left, right);
|
|
476
|
+
}
|
|
477
|
+
function safeInteger(value) {
|
|
478
|
+
return Number.isFinite(value) ? String(Math.max(Math.trunc(value), 0)) : "0";
|
|
479
|
+
}
|
|
480
|
+
function truncateUtf8(value, maximumBytes) {
|
|
481
|
+
if (Buffer.byteLength(value, "utf8") <= maximumBytes) {
|
|
482
|
+
return value;
|
|
483
|
+
}
|
|
484
|
+
const suffix = " [truncated]";
|
|
485
|
+
const limit = Math.max(maximumBytes - Buffer.byteLength(suffix, "utf8"), 0);
|
|
486
|
+
let output = "";
|
|
487
|
+
let bytes = 0;
|
|
488
|
+
for (const token of value) {
|
|
489
|
+
const tokenBytes = Buffer.byteLength(token, "utf8");
|
|
490
|
+
if (bytes + tokenBytes > limit) {
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
output += token;
|
|
494
|
+
bytes += tokenBytes;
|
|
495
|
+
}
|
|
496
|
+
return `${output}${suffix}`;
|
|
497
|
+
}
|
|
498
|
+
function safeLog(logger, level, message) {
|
|
499
|
+
if (!logger)
|
|
500
|
+
return;
|
|
501
|
+
try {
|
|
502
|
+
const method = logger[level];
|
|
503
|
+
if (typeof method === "function") {
|
|
504
|
+
const result = method.call(logger, message);
|
|
505
|
+
if (result && typeof result.then === "function") {
|
|
506
|
+
void Promise.resolve(result).catch(() => { });
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
// Operational diagnostics must never change retry or run behavior.
|
|
512
|
+
}
|
|
513
|
+
}
|