@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.
@@ -0,0 +1,508 @@
1
+ const MAXIMUM_DIAGNOSTIC_BYTES = 16 * 1024;
2
+ const MAXIMUM_SOURCE_CHARACTERS = 8 * 1024;
3
+ const MAXIMUM_CAUSAL_DEPTH = 8;
4
+ const MAXIMUM_CAUSAL_NODES = 32;
5
+ const SOURCE_TRUNCATION_SUFFIX = " [truncated]";
6
+ const SENSITIVE_FIELD_NAME_SOURCE = "(?:authorization|proxy(?:[_-]|\\s+)?authorization|body|payload"
7
+ + "|request(?:[_-]|\\s+)?(?:body|payload)"
8
+ + "|response(?:[_-]|\\s+)?(?:body|payload)"
9
+ + "|observation(?:[_-]|\\s+)?(?:body|payload)"
10
+ + "|runner(?:[_-]|\\s+)?key"
11
+ + "|api(?:[_-]|\\s+)?(?:key|token)"
12
+ + "|x(?:[_-]|\\s+)?api(?:[_-]|\\s+)?(?:key|token)"
13
+ + "|run(?:[_-]|\\s+)?token|access(?:[_-]|\\s+)?token"
14
+ + "|refresh(?:[_-]|\\s+)?token|id(?:[_-]|\\s+)?token"
15
+ + "|token|password|passwd|pwd|client(?:[_-]|\\s+)?secret"
16
+ + "|connection(?:[_-]|\\s+)?string|secret)";
17
+ const SENSITIVE_FIELD_NAME_PATTERN = new RegExp(`^${SENSITIVE_FIELD_NAME_SOURCE}$`, "iu");
18
+ const SENSITIVE_FIELD_PATTERN = new RegExp(`(^|[\\s{(,;=.]|\\[\\s*)(["']?)(${SENSITIVE_FIELD_NAME_SOURCE})\\2((?:\\s*\\[\\s*\\d+\\s*\\])*)(\\s*\\])?\\s*[:=]\\s*`, "giu");
19
+ const NEXT_SENSITIVE_FIELD_PATTERN = new RegExp(`\\s+(?=(?:(?:[a-z_$][\\w$.-]*\\s*)?\\[\\s*)?["']?${SENSITIVE_FIELD_NAME_SOURCE}["']?(?:\\s*\\[\\s*\\d+\\s*\\])*\\s*\\]?\\s*[:=])`, "giu");
20
+ const XML_ELEMENT_TAG_PATTERN = /<\s*(\/?)\s*([a-z_][\w.:-]*)(?:\s[^<>]*?)?\s*(\/?)>/giu;
21
+ const DIAGNOSTIC_FIELD_BOUNDARY_PATTERN = /\s+(?=(?:stack|code|errno|syscall|status|statuscode|statustext)\s*[:=])/giu;
22
+ const CAUSAL_BOUNDARY_PATTERN = /\s+\|\s+/gu;
23
+ const SOURCE_TRUNCATION_BOUNDARY_PATTERN = /\s+\[truncated\](?=\s|$)/giu;
24
+ const STANDALONE_CREDENTIAL_BOUNDARY_PATTERN = /\s+(?=(?:bearer|basic)\s+(?:["']|\[REDACTED\]|[a-z0-9._~+/=-]+))/giu;
25
+ const STANDALONE_CREDENTIAL_PREFIX_PATTERN = /\b(bearer|basic)\s+/giu;
26
+ export function logIterationObservationFailure(logger, level, context, error) {
27
+ const event = formatIterationObservationDiagnostic("observation sink callback failed", context, error);
28
+ safeLog(logger, level, event);
29
+ }
30
+ export function logIterationObservationRecovery(logger, context) {
31
+ const event = formatIterationObservationDiagnostic("observation sink callback recovered", context);
32
+ safeLog(logger, "debug", event);
33
+ }
34
+ function formatIterationObservationDiagnostic(event, context, error) {
35
+ const fields = [
36
+ `event=${event}`,
37
+ `sink=${safeText(context.sinkName)}`,
38
+ `operation=${context.operation}`,
39
+ ...(context.phase ? [`phase=${safeText(context.phase)}`] : []),
40
+ `runId=${safeText(context.runId)}`,
41
+ `resultOwnerId=${safeText(context.resultOwnerId)}`,
42
+ ...(context.batchId ? [`batchId=${safeText(context.batchId)}`] : []),
43
+ ...(context.batchSequence64
44
+ ? [`batchSequence64=${safeText(context.batchSequence64)}`]
45
+ : []),
46
+ `observationCount=${safeInteger(context.observationCount)}`,
47
+ `attempt=${safeInteger(context.attempt)}/${safeInteger(context.maximumAttempts)}`,
48
+ ...(context.nextDelayMs === undefined
49
+ ? []
50
+ : [`nextDelayMs=${safeInteger(context.nextDelayMs)}`])
51
+ ];
52
+ if (error !== undefined) {
53
+ fields.push(`errorChain=${formatErrorChain(error)}`);
54
+ }
55
+ return truncateUtf8(redactIterationObservationSecrets(normalizeText(fields.join(" "))), MAXIMUM_DIAGNOSTIC_BYTES);
56
+ }
57
+ function formatErrorChain(error) {
58
+ const lines = [];
59
+ const seen = new Set();
60
+ const state = { nodes: 0, nodeLimitWritten: false };
61
+ visitError(error, "error", 0, seen, state, lines);
62
+ return lines.join(" | ");
63
+ }
64
+ function visitError(value, path, depth, seen, state, lines) {
65
+ if (depth >= MAXIMUM_CAUSAL_DEPTH) {
66
+ lines.push(`${path}: [truncated at causal depth ${MAXIMUM_CAUSAL_DEPTH}]`);
67
+ return;
68
+ }
69
+ if (state.nodes >= MAXIMUM_CAUSAL_NODES) {
70
+ if (!state.nodeLimitWritten) {
71
+ state.nodeLimitWritten = true;
72
+ lines.push(`[truncated after ${MAXIMUM_CAUSAL_NODES} causal nodes]`);
73
+ }
74
+ return;
75
+ }
76
+ state.nodes += 1;
77
+ if ((typeof value !== "object" && typeof value !== "function") || value === null) {
78
+ lines.push(`${path}: ${primitiveType(value)} message=${safeText(value)}`);
79
+ return;
80
+ }
81
+ if (seen.has(value)) {
82
+ lines.push(`${path}: [cycle]`);
83
+ return;
84
+ }
85
+ seen.add(value);
86
+ const name = safePropertyText(value, "name") || objectType(value);
87
+ const message = safePropertyText(value, "message");
88
+ const details = [`${path}: ${name}`];
89
+ if (message) {
90
+ details.push(`message=${message}`);
91
+ }
92
+ for (const key of ["code", "errno", "syscall", "status", "statusCode", "statusText"]) {
93
+ const resolved = safePropertyText(value, key);
94
+ if (resolved) {
95
+ details.push(`${key}=${resolved}`);
96
+ }
97
+ }
98
+ const stack = safePropertyText(value, "stack", 4096);
99
+ if (stack) {
100
+ details.push(`stack=${stack}`);
101
+ }
102
+ lines.push(details.join(" "));
103
+ const cause = safeProperty(value, "cause");
104
+ if (cause.available && cause.value !== undefined && cause.value !== null) {
105
+ visitError(cause.value, `${path}.cause`, depth + 1, seen, state, lines);
106
+ }
107
+ else if (!cause.available) {
108
+ lines.push(`${path}.cause: [unavailable]`);
109
+ }
110
+ const aggregate = safeProperty(value, "errors");
111
+ if (!aggregate.available) {
112
+ lines.push(`${path}.errors: [unavailable]`);
113
+ return;
114
+ }
115
+ const members = safeArrayMembers(aggregate.value);
116
+ for (let index = 0; index < members.length; index += 1) {
117
+ visitError(members[index], `${path}.errors[${index}]`, depth + 1, seen, state, lines);
118
+ }
119
+ }
120
+ function safeArrayMembers(value) {
121
+ try {
122
+ if (!Array.isArray(value)) {
123
+ return [];
124
+ }
125
+ const length = Math.min(value.length, MAXIMUM_CAUSAL_NODES);
126
+ const result = [];
127
+ for (let index = 0; index < length; index += 1) {
128
+ try {
129
+ result.push(value[index]);
130
+ }
131
+ catch {
132
+ result.push("[unavailable aggregate member]");
133
+ }
134
+ }
135
+ return result;
136
+ }
137
+ catch {
138
+ return [];
139
+ }
140
+ }
141
+ function safeProperty(value, key) {
142
+ try {
143
+ return { available: true, value: Reflect.get(value, key) };
144
+ }
145
+ catch {
146
+ return { available: false, value: undefined };
147
+ }
148
+ }
149
+ function safePropertyText(value, key, maximumCharacters = MAXIMUM_SOURCE_CHARACTERS) {
150
+ const property = safeProperty(value, key);
151
+ return property.available ? safeText(property.value, maximumCharacters) : "";
152
+ }
153
+ function objectType(value) {
154
+ try {
155
+ const tag = Object.prototype.toString.call(value);
156
+ const match = /^\[object ([^\]]+)\]$/u.exec(tag);
157
+ return match?.[1] || "Object";
158
+ }
159
+ catch {
160
+ return "Object";
161
+ }
162
+ }
163
+ function primitiveType(value) {
164
+ return value === null ? "null" : typeof value;
165
+ }
166
+ function safeText(value, maximumCharacters = MAXIMUM_SOURCE_CHARACTERS) {
167
+ let source;
168
+ try {
169
+ source = typeof value === "string" ? value : String(value ?? "");
170
+ }
171
+ catch {
172
+ source = "[unavailable]";
173
+ }
174
+ return normalizeText(boundSourceCharacters(source, maximumCharacters));
175
+ }
176
+ function normalizeText(value) {
177
+ const validUtf8 = Buffer.from(value, "utf8").toString("utf8");
178
+ return validUtf8
179
+ .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/gu, " ")
180
+ .replace(/[\r\n\t]+/gu, " ")
181
+ .replace(/\s{2,}/gu, " ")
182
+ .trim();
183
+ }
184
+ export function redactIterationObservationSecrets(value) {
185
+ let output = boundSourceCharacters(value, MAXIMUM_SOURCE_CHARACTERS * 2);
186
+ output = redactSensitiveXmlElements(output);
187
+ output = redactBoundedSensitiveFields(output);
188
+ output = redactStandaloneCredentials(output);
189
+ 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]");
190
+ output = output.replace(/(\b[a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@/giu, "$1[REDACTED]@");
191
+ output = output.replace(/(\b[a-z][a-z0-9+.-]*:\/\/)[^\s/@]+(?=\s+\[truncated\](?:\s|$))/giu, "$1[REDACTED]");
192
+ return output;
193
+ }
194
+ function redactStandaloneCredentials(value) {
195
+ let output = "";
196
+ let cursor = 0;
197
+ STANDALONE_CREDENTIAL_PREFIX_PATTERN.lastIndex = 0;
198
+ for (let match = STANDALONE_CREDENTIAL_PREFIX_PATTERN.exec(value); match; match = STANDALONE_CREDENTIAL_PREFIX_PATTERN.exec(value)) {
199
+ if (match.index < cursor) {
200
+ continue;
201
+ }
202
+ const valueStart = STANDALONE_CREDENTIAL_PREFIX_PATTERN.lastIndex;
203
+ const valueEnd = standaloneCredentialValueEnd(value, valueStart);
204
+ output += value.slice(cursor, match.index);
205
+ output += `${match[1]} [REDACTED]`;
206
+ cursor = Math.max(valueEnd, valueStart);
207
+ STANDALONE_CREDENTIAL_PREFIX_PATTERN.lastIndex = cursor;
208
+ }
209
+ output += value.slice(cursor);
210
+ return output;
211
+ }
212
+ function standaloneCredentialValueEnd(source, valueStart) {
213
+ if (valueStart >= source.length) {
214
+ return source.length;
215
+ }
216
+ const hardBoundary = earliestPatternIndex(source, valueStart, CAUSAL_BOUNDARY_PATTERN, DIAGNOSTIC_FIELD_BOUNDARY_PATTERN, SOURCE_TRUNCATION_BOUNDARY_PATTERN);
217
+ const quote = source[valueStart];
218
+ if (quote === "\"" || quote === "'") {
219
+ const quotedEnd = quotedValueEnd(source, valueStart);
220
+ if (quotedEnd !== null
221
+ && (hardBoundary === null || quotedEnd <= hardBoundary)) {
222
+ if (isStandaloneCredentialBoundary(source, quotedEnd)) {
223
+ return quotedEnd;
224
+ }
225
+ return standaloneCredentialScalarEnd(source, quotedEnd, hardBoundary);
226
+ }
227
+ return hardBoundary ?? source.length;
228
+ }
229
+ return standaloneCredentialScalarEnd(source, valueStart, hardBoundary);
230
+ }
231
+ function standaloneCredentialScalarEnd(source, valueStart, hardBoundary) {
232
+ const scalarBoundary = earliestCharacterIndex(source, valueStart, " \t\r\n,;|");
233
+ return minimumIndex(hardBoundary, scalarBoundary) ?? source.length;
234
+ }
235
+ function isStandaloneCredentialBoundary(source, index) {
236
+ return index >= source.length || /^[\s,;|]/u.test(source.slice(index));
237
+ }
238
+ function boundSourceCharacters(value, maximumCharacters) {
239
+ const limit = Math.max(Math.trunc(maximumCharacters), 0);
240
+ if (value.length <= limit) {
241
+ return value;
242
+ }
243
+ if (limit <= SOURCE_TRUNCATION_SUFFIX.length) {
244
+ return SOURCE_TRUNCATION_SUFFIX.slice(0, limit);
245
+ }
246
+ return `${value.slice(0, limit - SOURCE_TRUNCATION_SUFFIX.length)}${SOURCE_TRUNCATION_SUFFIX}`;
247
+ }
248
+ function redactBoundedSensitiveFields(value) {
249
+ let output = "";
250
+ let cursor = 0;
251
+ SENSITIVE_FIELD_PATTERN.lastIndex = 0;
252
+ for (let match = SENSITIVE_FIELD_PATTERN.exec(value); match; match = SENSITIVE_FIELD_PATTERN.exec(value)) {
253
+ if (match.index < cursor) {
254
+ continue;
255
+ }
256
+ const prefix = match[1] ?? "";
257
+ const key = match[3] ?? "";
258
+ const indexSuffix = match[4] ?? "";
259
+ const bracketSuffix = match[5] ?? "";
260
+ const valueStart = SENSITIVE_FIELD_PATTERN.lastIndex;
261
+ const valueEnd = sensitiveFieldValueEnd(value, valueStart, key);
262
+ output += value.slice(cursor, match.index);
263
+ output += `${prefix}${key}${indexSuffix}${bracketSuffix}=[REDACTED]`;
264
+ cursor = Math.max(valueEnd, valueStart);
265
+ SENSITIVE_FIELD_PATTERN.lastIndex = cursor;
266
+ }
267
+ output += value.slice(cursor);
268
+ return output;
269
+ }
270
+ function redactSensitiveXmlElements(value) {
271
+ let output = "";
272
+ let cursor = 0;
273
+ let sensitiveElementName = null;
274
+ let sensitiveElementDepth = 0;
275
+ XML_ELEMENT_TAG_PATTERN.lastIndex = 0;
276
+ for (let match = XML_ELEMENT_TAG_PATTERN.exec(value); match; match = XML_ELEMENT_TAG_PATTERN.exec(value)) {
277
+ const isClosingTag = match[1] === "/";
278
+ const elementName = (match[2] ?? "").toLowerCase();
279
+ const isSelfClosingTag = match[3] === "/";
280
+ if (sensitiveElementName === null) {
281
+ if (isClosingTag
282
+ || isSelfClosingTag
283
+ || !isSensitiveXmlElementName(elementName)) {
284
+ continue;
285
+ }
286
+ output += value.slice(cursor, XML_ELEMENT_TAG_PATTERN.lastIndex);
287
+ output += "[REDACTED]";
288
+ cursor = XML_ELEMENT_TAG_PATTERN.lastIndex;
289
+ sensitiveElementName = elementName;
290
+ sensitiveElementDepth = 1;
291
+ continue;
292
+ }
293
+ if (elementName !== sensitiveElementName) {
294
+ continue;
295
+ }
296
+ if (!isClosingTag && !isSelfClosingTag) {
297
+ sensitiveElementDepth += 1;
298
+ continue;
299
+ }
300
+ if (!isClosingTag) {
301
+ continue;
302
+ }
303
+ sensitiveElementDepth -= 1;
304
+ if (sensitiveElementDepth === 0) {
305
+ output += match[0];
306
+ cursor = XML_ELEMENT_TAG_PATTERN.lastIndex;
307
+ sensitiveElementName = null;
308
+ }
309
+ }
310
+ if (sensitiveElementName === null) {
311
+ output += value.slice(cursor);
312
+ }
313
+ return output;
314
+ }
315
+ function isSensitiveXmlElementName(elementName) {
316
+ const namespaceSeparator = elementName.lastIndexOf(":");
317
+ const localName = namespaceSeparator < 0
318
+ ? elementName
319
+ : elementName.slice(namespaceSeparator + 1);
320
+ return SENSITIVE_FIELD_NAME_PATTERN.test(localName);
321
+ }
322
+ function sensitiveFieldValueEnd(source, valueStart, key) {
323
+ if (valueStart >= source.length) {
324
+ return source.length;
325
+ }
326
+ const normalizedKey = key.replace(/[_\s-]/gu, "").toLowerCase();
327
+ const hardBoundary = earliestPatternIndex(source, valueStart, CAUSAL_BOUNDARY_PATTERN, DIAGNOSTIC_FIELD_BOUNDARY_PATTERN);
328
+ if (normalizedKey === "authorization"
329
+ || normalizedKey === "proxyauthorization") {
330
+ return hardBoundary ?? source.length;
331
+ }
332
+ const markerEnd = redactionMarkerPrefixEnd(source, valueStart);
333
+ let boundarySearchStart = markerEnd ?? valueStart;
334
+ if (markerEnd === null) {
335
+ const quotedEnd = quotedValueEnd(source, valueStart);
336
+ if (quotedEnd !== null) {
337
+ if (isRecognizedSensitiveValueBoundary(source, quotedEnd)) {
338
+ return quotedEnd;
339
+ }
340
+ boundarySearchStart = quotedEnd;
341
+ }
342
+ else {
343
+ const structuredEnd = structuredValueEnd(source, valueStart);
344
+ if (structuredEnd !== null) {
345
+ if (isRecognizedSensitiveValueBoundary(source, structuredEnd)) {
346
+ return structuredEnd;
347
+ }
348
+ boundarySearchStart = structuredEnd;
349
+ }
350
+ }
351
+ }
352
+ if (normalizedKey === "connectionstring") {
353
+ return hardBoundary ?? source.length;
354
+ }
355
+ const nextFieldBoundary = earliestPatternIndex(source, boundarySearchStart, NEXT_SENSITIVE_FIELD_PATTERN, STANDALONE_CREDENTIAL_BOUNDARY_PATTERN);
356
+ const fieldBoundary = minimumIndex(hardBoundary, nextFieldBoundary);
357
+ if (normalizedKey === "body"
358
+ || normalizedKey === "payload"
359
+ || normalizedKey.endsWith("body")
360
+ || normalizedKey.endsWith("payload")) {
361
+ return fieldBoundary ?? source.length;
362
+ }
363
+ const scalarBoundary = earliestCharacterIndex(source, boundarySearchStart, ";,&}]");
364
+ return minimumIndex(fieldBoundary, scalarBoundary) ?? source.length;
365
+ }
366
+ function redactionMarkerPrefixEnd(source, valueStart) {
367
+ const marker = /^(?:\[REDACTED\])+/iu.exec(source.slice(valueStart));
368
+ return marker ? valueStart + marker[0].length : null;
369
+ }
370
+ function isRecognizedSensitiveValueBoundary(source, index) {
371
+ if (index >= source.length) {
372
+ return true;
373
+ }
374
+ if (/^[;,&}\]]/u.test(source.slice(index))) {
375
+ return true;
376
+ }
377
+ return earliestPatternIndex(source, index, CAUSAL_BOUNDARY_PATTERN, DIAGNOSTIC_FIELD_BOUNDARY_PATTERN, NEXT_SENSITIVE_FIELD_PATTERN, STANDALONE_CREDENTIAL_BOUNDARY_PATTERN) === index;
378
+ }
379
+ function quotedValueEnd(source, valueStart) {
380
+ const quote = source[valueStart];
381
+ if (quote !== "\"" && quote !== "'") {
382
+ return null;
383
+ }
384
+ let escaped = false;
385
+ for (let index = valueStart + 1; index < source.length; index += 1) {
386
+ const token = source[index];
387
+ if (escaped) {
388
+ escaped = false;
389
+ continue;
390
+ }
391
+ if (token === "\\") {
392
+ escaped = true;
393
+ continue;
394
+ }
395
+ if (token === quote) {
396
+ return index + 1;
397
+ }
398
+ }
399
+ return null;
400
+ }
401
+ function structuredValueEnd(source, valueStart) {
402
+ const opening = source[valueStart];
403
+ if (opening !== "{" && opening !== "[") {
404
+ return null;
405
+ }
406
+ const stack = [opening];
407
+ let quote = "";
408
+ let escaped = false;
409
+ for (let index = valueStart + 1; index < source.length; index += 1) {
410
+ const token = source[index];
411
+ if (quote) {
412
+ if (escaped) {
413
+ escaped = false;
414
+ }
415
+ else if (token === "\\") {
416
+ escaped = true;
417
+ }
418
+ else if (token === quote) {
419
+ quote = "";
420
+ }
421
+ continue;
422
+ }
423
+ if (token === "\"" || token === "'") {
424
+ quote = token;
425
+ continue;
426
+ }
427
+ if (token === "{" || token === "[") {
428
+ stack.push(token);
429
+ continue;
430
+ }
431
+ if (token === "}" || token === "]") {
432
+ const expected = token === "}" ? "{" : "[";
433
+ if (stack.at(-1) !== expected) {
434
+ return null;
435
+ }
436
+ stack.pop();
437
+ if (!stack.length) {
438
+ return index + 1;
439
+ }
440
+ }
441
+ }
442
+ return null;
443
+ }
444
+ function earliestPatternIndex(source, start, ...patterns) {
445
+ let earliest = null;
446
+ for (const pattern of patterns) {
447
+ pattern.lastIndex = start;
448
+ const match = pattern.exec(source);
449
+ if (match && (earliest === null || match.index < earliest)) {
450
+ earliest = match.index;
451
+ }
452
+ }
453
+ return earliest;
454
+ }
455
+ function earliestCharacterIndex(source, start, characters) {
456
+ let earliest = null;
457
+ for (const character of characters) {
458
+ const index = source.indexOf(character, start);
459
+ if (index >= 0 && (earliest === null || index < earliest)) {
460
+ earliest = index;
461
+ }
462
+ }
463
+ return earliest;
464
+ }
465
+ function minimumIndex(left, right) {
466
+ if (left === null)
467
+ return right;
468
+ if (right === null)
469
+ return left;
470
+ return Math.min(left, right);
471
+ }
472
+ function safeInteger(value) {
473
+ return Number.isFinite(value) ? String(Math.max(Math.trunc(value), 0)) : "0";
474
+ }
475
+ function truncateUtf8(value, maximumBytes) {
476
+ if (Buffer.byteLength(value, "utf8") <= maximumBytes) {
477
+ return value;
478
+ }
479
+ const suffix = " [truncated]";
480
+ const limit = Math.max(maximumBytes - Buffer.byteLength(suffix, "utf8"), 0);
481
+ let output = "";
482
+ let bytes = 0;
483
+ for (const token of value) {
484
+ const tokenBytes = Buffer.byteLength(token, "utf8");
485
+ if (bytes + tokenBytes > limit) {
486
+ break;
487
+ }
488
+ output += token;
489
+ bytes += tokenBytes;
490
+ }
491
+ return `${output}${suffix}`;
492
+ }
493
+ function safeLog(logger, level, message) {
494
+ if (!logger)
495
+ return;
496
+ try {
497
+ const method = logger[level];
498
+ if (typeof method === "function") {
499
+ const result = method.call(logger, message);
500
+ if (result && typeof result.then === "function") {
501
+ void Promise.resolve(result).catch(() => { });
502
+ }
503
+ }
504
+ }
505
+ catch {
506
+ // Operational diagnostics must never change retry or run behavior.
507
+ }
508
+ }