opencode-feature-factory 0.2.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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +877 -0
  3. package/assets/agent/backend-builder.md +73 -0
  4. package/assets/agent/codebase-researcher.md +56 -0
  5. package/assets/agent/design-interpreter.md +37 -0
  6. package/assets/agent/frontend-builder.md +74 -0
  7. package/assets/agent/implementation-validator.md +73 -0
  8. package/assets/agent/security-reviewer.md +60 -0
  9. package/assets/agent/spec-writer.md +94 -0
  10. package/assets/agent/story-reader.md +38 -0
  11. package/assets/agent/story-writer.md +39 -0
  12. package/assets/agent/test-verifier.md +73 -0
  13. package/assets/agent/work-decomposer.md +102 -0
  14. package/assets/agent/work-reviewer.md +77 -0
  15. package/assets/command/feature.md +68 -0
  16. package/assets/skills/feature/SCHEMA.md +990 -0
  17. package/assets/skills/feature/SKILL.md +620 -0
  18. package/dist/tui.js +3641 -0
  19. package/package.json +65 -0
  20. package/src/cleanup-sweep-command.js +75 -0
  21. package/src/cleanup-sweep-eligibility.js +581 -0
  22. package/src/cleanup-sweep-output.js +139 -0
  23. package/src/cleanup-sweep-report.js +548 -0
  24. package/src/cleanup-sweep.js +546 -0
  25. package/src/cli-output.js +251 -0
  26. package/src/cli.js +1152 -0
  27. package/src/config.js +231 -0
  28. package/src/cost-attribution.js +327 -0
  29. package/src/cost-report.js +185 -0
  30. package/src/detached-log-supervisor.js +178 -0
  31. package/src/doctor.js +598 -0
  32. package/src/env-snapshot.js +211 -0
  33. package/src/factory-diagnostics.js +429 -0
  34. package/src/factory-paths.js +40 -0
  35. package/src/factory.js +4769 -0
  36. package/src/feature-command-payload.js +378 -0
  37. package/src/git.js +110 -0
  38. package/src/github.js +252 -0
  39. package/src/hardening/atomic-write.js +954 -0
  40. package/src/hardening/line-output.js +139 -0
  41. package/src/hardening/output-policy.js +365 -0
  42. package/src/hardening/process-verification.js +542 -0
  43. package/src/hardening/sensitive-data.js +341 -0
  44. package/src/hardening/terminal-encoding.js +224 -0
  45. package/src/plugin.js +246 -0
  46. package/src/post-pr-ci.js +754 -0
  47. package/src/process-evidence.js +1139 -0
  48. package/src/refs.js +144 -0
  49. package/src/run-state.js +2411 -0
  50. package/src/steering-conflicts.js +77 -0
  51. package/src/telemetry.js +419 -0
  52. package/src/tui-data.js +499 -0
  53. package/src/tui-rendering.js +148 -0
  54. package/src/utils.js +35 -0
  55. package/src/validate.js +1655 -0
  56. package/src/worktrees.js +56 -0
@@ -0,0 +1,77 @@
1
+ import { PASSING_SECURITY_VERDICTS, PASSING_VALIDATOR_VERDICTS } from "./validate.js";
2
+
3
+ const PROTECTED_GATE_STATUSES = new Set(["approved", "stopped"]);
4
+ const PROTECTED_SLICE_STATUSES = new Set(["merged", "blocked", "review"]);
5
+
6
+ export const STEERING_CONFLICT_SUMMARY = "Consumed untrusted steering would require changing accepted durable state; human reconciliation is required.";
7
+
8
+ export function collectProtectedSteeringState(runDir, run) {
9
+ void runDir;
10
+ if (!isRecord(run)) return [];
11
+
12
+ const protectedState = [];
13
+ for (const [gateName, gate] of Object.entries(isRecord(run.gates) ? run.gates : {})) {
14
+ if (isRecord(gate) && PROTECTED_GATE_STATUSES.has(gate.status)) protectedState.push(`gate:${gateName}`);
15
+ }
16
+
17
+ for (const step of Array.isArray(run.steps) ? run.steps : []) {
18
+ if (isRecord(step) && step.status === "accepted" && stringValue(step.agent)) protectedState.push(`step:${step.agent}`);
19
+ }
20
+
21
+ for (const slice of Array.isArray(run.slices) ? run.slices : []) {
22
+ if (isRecord(slice) && PROTECTED_SLICE_STATUSES.has(slice.status) && stringValue(slice.id)) protectedState.push(`slice:${slice.id}`);
23
+ }
24
+
25
+ if (isRecord(run.validator) && PASSING_VALIDATOR_VERDICTS.has(run.validator.verdict)) protectedState.push(`validator:${run.validator.verdict}`);
26
+ if (isRecord(run.security_review) && PASSING_SECURITY_VERDICTS.has(run.security_review.verdict)) protectedState.push(`security_review:${run.security_review.verdict}`);
27
+ if (stringValue(run.pr_url)) protectedState.push("pr_url");
28
+ if (isRecord(run.terminal_result)) protectedState.push("terminal_result");
29
+
30
+ return protectedState;
31
+ }
32
+
33
+ export function formatSteeringConflictReason(ref, protectedState) {
34
+ const steeringRef = requireNonEmptyString(ref, "steering ref");
35
+ const protectedText = normalizeProtectedState(protectedState).join(",") || "none";
36
+ return `operator steering conflicts with accepted durable state: steering=${steeringRef}; protected=${protectedText}; automatic rollback is forbidden`;
37
+ }
38
+
39
+ export function buildSteeringConflictTerminalResult(run, steering, protectedState, input = {}) {
40
+ const ref = requireNonEmptyString(steering?.ref, "steering ref");
41
+ const hash = requireNonEmptyString(steering?.hash, "steering hash");
42
+ const normalizedProtectedState = normalizeProtectedState(protectedState);
43
+ const artifacts = {
44
+ steering_ref: ref,
45
+ steering_hash: hash,
46
+ protected_state: normalizedProtectedState.join(","),
47
+ reason_code: "accepted-state-conflict",
48
+ };
49
+ void input;
50
+
51
+ return {
52
+ status: "needs-human",
53
+ run_id: requireNonEmptyString(run?.run_id, "run_id"),
54
+ pr_url: stringValue(run?.pr_url) ? run.pr_url : null,
55
+ reason: formatSteeringConflictReason(ref, normalizedProtectedState),
56
+ summary: STEERING_CONFLICT_SUMMARY,
57
+ artifacts,
58
+ };
59
+ }
60
+
61
+ function normalizeProtectedState(protectedState) {
62
+ if (!Array.isArray(protectedState)) return [];
63
+ return protectedState.filter(stringValue).map((value) => String(value).trim());
64
+ }
65
+
66
+ function requireNonEmptyString(value, label) {
67
+ if (!stringValue(value)) throw new Error(`${label} must be a non-empty string`);
68
+ return String(value).trim();
69
+ }
70
+
71
+ function isRecord(value) {
72
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
73
+ }
74
+
75
+ function stringValue(value) {
76
+ return typeof value === "string" && value.trim().length > 0;
77
+ }
@@ -0,0 +1,419 @@
1
+ import {
2
+ REDACTED_ENV_VALUE,
3
+ isSecretShapedEnvKey,
4
+ isSensitiveEnvKey,
5
+ isSensitiveEnvValue,
6
+ scrubSecretEnv,
7
+ } from "./env-snapshot.js";
8
+
9
+ export const TELEMETRY_TRACER_NAME = "opencode-feature-factory";
10
+ export const FEATURE_FACTORY_TRACEPARENT = "FEATURE_FACTORY_TRACEPARENT";
11
+ export const FEATURE_FACTORY_TRACESTATE = "FEATURE_FACTORY_TRACESTATE";
12
+ export const FEATURE_FACTORY_PARENT_SPAN_ID = "FEATURE_FACTORY_PARENT_SPAN_ID";
13
+
14
+ const TRACEPARENT_PATTERN = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/iu;
15
+ const ZERO_TRACE_ID = "0".repeat(32);
16
+ const ZERO_SPAN_ID = "0".repeat(16);
17
+ const PARENT_SPAN_ID_PATTERN = /^[0-9a-f]{16}$/iu;
18
+ const CONTROL_CHAR_PATTERN = /[\u0000-\u001f\u007f]/u;
19
+ const OTLP_ENV_PATTERN = /^OTEL_EXPORTER_OTLP(?:_[A-Z0-9_]+)?$/u;
20
+ const OTLP_ENDPOINT_PATTERN = /^OTEL_EXPORTER_OTLP(?:_[A-Z0-9_]+)?_ENDPOINT$/u;
21
+ const OTLP_HEADERS_PATTERN = /^OTEL_EXPORTER_OTLP(?:_[A-Z0-9_]+)?_HEADERS$/u;
22
+ // W3C trace-context caps tracestate at 512 characters; oversized values get
23
+ // truncated or dropped by downstream propagators, so reject them up front.
24
+ const MAX_TRACESTATE_LENGTH = 512;
25
+ const HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/u;
26
+ const SAFE_ATTRIBUTE_ARRAY_TYPES = new Set(["string", "number", "boolean"]);
27
+ // @opentelemetry/api SpanStatusCode.ERROR is the stable enum value 2. Inlined so
28
+ // this module never needs a static import of an optional runtime dependency.
29
+ const SPAN_STATUS_ERROR = 2;
30
+ // Lazy, cached load of the optional @opentelemetry/api package. `undefined` means
31
+ // not yet attempted, `null` means unavailable, an object is the loaded module.
32
+ let cachedOtelApi;
33
+ async function loadOtelApi(importer) {
34
+ if (typeof importer === "function") {
35
+ try {
36
+ return await importer();
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+ if (cachedOtelApi !== undefined) return cachedOtelApi;
42
+ try {
43
+ cachedOtelApi = await import("@opentelemetry/api");
44
+ } catch {
45
+ cachedOtelApi = null;
46
+ }
47
+ return cachedOtelApi;
48
+ }
49
+ function noopSpan() {
50
+ return {
51
+ recordException() {},
52
+ setStatus() {},
53
+ setAttribute() {},
54
+ end() {},
55
+ };
56
+ }
57
+ const DEFAULT_CONTENT_CAPTURE = Object.freeze({
58
+ captureMessages: false,
59
+ captureToolArguments: false,
60
+ captureToolResults: false,
61
+ captureReviews: false,
62
+ captureEvidence: false,
63
+ });
64
+
65
+ export async function checkOpenTelemetryApiLoadability(importer = () => import("@opentelemetry/api")) {
66
+ try {
67
+ const api = await importer();
68
+ const missing = ["trace", "context", "SpanStatusCode"].filter((name) => api?.[name] === undefined);
69
+ if (missing.length > 0) {
70
+ return {
71
+ ok: false,
72
+ package: "@opentelemetry/api",
73
+ error: `@opentelemetry/api missing expected export(s): ${missing.join(", ")}`,
74
+ };
75
+ }
76
+ return {
77
+ ok: true,
78
+ package: "@opentelemetry/api",
79
+ exports: ["trace", "context", "SpanStatusCode"],
80
+ };
81
+ } catch (error) {
82
+ return {
83
+ ok: false,
84
+ package: "@opentelemetry/api",
85
+ error: sanitizeDiagnosticMessage(error?.message || String(error)),
86
+ };
87
+ }
88
+ }
89
+
90
+ export function sanitizeOtlpEnv(env = process.env) {
91
+ const output = {};
92
+ for (const [key, value] of Object.entries(env || {})) {
93
+ if (!OTLP_ENV_PATTERN.test(key)) continue;
94
+ output[key] = OTLP_HEADERS_PATTERN.test(key)
95
+ ? sanitizeOtlpHeaders(value)
96
+ : sanitizeTelemetryValue(key, value);
97
+ }
98
+ return output;
99
+ }
100
+
101
+ export function sanitizeOtlpHeaders(value) {
102
+ if (!stringValue(value)) return [];
103
+ return String(value)
104
+ .split(",")
105
+ .map((entry) => sanitizeOtlpHeaderEntry(entry))
106
+ .filter(Boolean);
107
+ }
108
+
109
+ export function sanitizeTelemetryValue(key, value) {
110
+ if (value === undefined || value === null) return value;
111
+ const string = String(value);
112
+ if (isSensitiveEnvKey(key)) return REDACTED_ENV_VALUE;
113
+ if (OTLP_ENDPOINT_PATTERN.test(key)) return sanitizeOtlpEndpointValue(string);
114
+ if (isSensitiveEnvValue(string)) return REDACTED_ENV_VALUE;
115
+ return string;
116
+ }
117
+
118
+ export function evaluateContentCaptureRisk(options = {}) {
119
+ const telemetry = {
120
+ ...DEFAULT_CONTENT_CAPTURE,
121
+ ...(plainObject(options.telemetry) ? options.telemetry : {}),
122
+ ...(plainObject(options.config?.telemetry) ? options.config.telemetry : {}),
123
+ ...(plainObject(options.config?.plugin?.telemetry) ? options.config.plugin.telemetry : {}),
124
+ };
125
+ const nativeOpenTelemetry = options.nativeOpenTelemetry === true
126
+ || options.config?.experimental?.openTelemetry === true;
127
+ const enabledCaptureFlags = Object.keys(DEFAULT_CONTENT_CAPTURE)
128
+ .filter((key) => telemetry[key] === true);
129
+ const risks = [];
130
+
131
+ if (nativeOpenTelemetry) {
132
+ risks.push({
133
+ kind: "native-opencode-content-capture",
134
+ severity: "warning",
135
+ message: "Native opencode/AI SDK OpenTelemetry may capture prompts, completions, tool arguments, or tool results outside feature-factory redaction.",
136
+ });
137
+ }
138
+ if (enabledCaptureFlags.length > 0) {
139
+ risks.push({
140
+ kind: "feature-factory-content-capture",
141
+ severity: "warning",
142
+ flags: enabledCaptureFlags,
143
+ message: "Feature-factory content capture is enabled; redact and cap content before setting span attributes or events.",
144
+ });
145
+ }
146
+
147
+ return {
148
+ ok: risks.length === 0,
149
+ level: risks.length === 0 ? "ok" : "warning",
150
+ redactionActive: true,
151
+ capture: Object.fromEntries(Object.keys(DEFAULT_CONTENT_CAPTURE).map((key) => [key, telemetry[key] === true])),
152
+ risks,
153
+ };
154
+ }
155
+
156
+ export const contentCaptureRisk = evaluateContentCaptureRisk;
157
+
158
+ export function validateParentSpanId(value) {
159
+ if (!stringValue(value)) return validationError("parent span id is required");
160
+ const spanId = String(value).trim().toLowerCase();
161
+ if (!PARENT_SPAN_ID_PATTERN.test(spanId)) return validationError("parent span id must be 16 hex characters");
162
+ if (spanId === ZERO_SPAN_ID) return validationError("parent span id must not be all zeroes");
163
+ return { ok: true, value: spanId, spanId };
164
+ }
165
+
166
+ export function validateTraceparent(value) {
167
+ if (!stringValue(value)) return validationError("traceparent is required");
168
+ const traceparent = String(value).trim().toLowerCase();
169
+ const match = TRACEPARENT_PATTERN.exec(traceparent);
170
+ if (!match) return validationError("traceparent must match W3C format 00-<32hex trace id>-<16hex span id>-<2hex flags>");
171
+
172
+ const [, traceId, parentSpanId, flags] = match;
173
+ if (traceId === ZERO_TRACE_ID) return validationError("traceparent trace id must not be all zeroes");
174
+ if (parentSpanId === ZERO_SPAN_ID) return validationError("traceparent parent span id must not be all zeroes");
175
+ return { ok: true, value: traceparent, traceparent, traceId, parentSpanId, flags };
176
+ }
177
+
178
+ export function validateTracestate(value) {
179
+ if (value === undefined || value === null) return { ok: true, value: undefined };
180
+ const tracestate = String(value);
181
+ if (CONTROL_CHAR_PATTERN.test(tracestate)) return validationError("tracestate must not contain control characters or newlines");
182
+ if (tracestate.length > MAX_TRACESTATE_LENGTH) return validationError(`tracestate must be at most ${MAX_TRACESTATE_LENGTH} characters`);
183
+ return { ok: true, value: tracestate, tracestate };
184
+ }
185
+
186
+ export function validateTraceContext(input = {}) {
187
+ const output = {};
188
+ if (input.parentSpanId !== undefined && input.parentSpanId !== null) {
189
+ const parent = validateParentSpanId(input.parentSpanId);
190
+ if (!parent.ok) return parent;
191
+ output.parentSpanId = parent.spanId;
192
+ }
193
+ if (input.traceparent !== undefined && input.traceparent !== null) {
194
+ const parsed = validateTraceparent(input.traceparent);
195
+ if (!parsed.ok) return parsed;
196
+ output.traceparent = parsed.traceparent;
197
+ output.traceId = parsed.traceId;
198
+ output.traceparentSpanId = parsed.parentSpanId;
199
+ output.traceFlags = parsed.flags;
200
+ }
201
+ if (output.parentSpanId && output.traceparentSpanId && output.parentSpanId !== output.traceparentSpanId) {
202
+ return validationError("parent span id must match traceparent span id when both are supplied");
203
+ }
204
+ if (input.tracestate !== undefined && input.tracestate !== null) {
205
+ const state = validateTracestate(input.tracestate);
206
+ if (!state.ok) return state;
207
+ output.tracestate = state.tracestate;
208
+ }
209
+ return { ok: true, ...output };
210
+ }
211
+
212
+ const TRACE_CONTEXT_ENV_KEYS = Object.freeze([
213
+ "TRACEPARENT",
214
+ "TRACESTATE",
215
+ FEATURE_FACTORY_TRACEPARENT,
216
+ FEATURE_FACTORY_TRACESTATE,
217
+ FEATURE_FACTORY_PARENT_SPAN_ID,
218
+ ]);
219
+
220
+ export function prepareTelemetryEnv(baseEnv = process.env, traceContext = {}) {
221
+ const env = { ...(baseEnv || {}) };
222
+ const validated = validateTraceContext(traceContext);
223
+ if (!validated.ok) throw new Error(validated.error);
224
+
225
+ const hasExplicitContext = validated.traceparent !== undefined
226
+ || validated.tracestate !== undefined
227
+ || validated.parentSpanId !== undefined;
228
+ if (!hasExplicitContext) return env;
229
+
230
+ // An explicit override replaces the whole ambient trace context. Keeping any
231
+ // inherited field alongside supplied ones can pair a stale TRACESTATE with a
232
+ // new trace, or a contradictory inherited TRACEPARENT with an explicit parent
233
+ // span id, leaving the child process with an ambiguous context.
234
+ for (const key of TRACE_CONTEXT_ENV_KEYS) delete env[key];
235
+
236
+ if (validated.traceparent) {
237
+ env.TRACEPARENT = validated.traceparent;
238
+ env[FEATURE_FACTORY_TRACEPARENT] = validated.traceparent;
239
+ }
240
+ if (validated.tracestate !== undefined) {
241
+ env.TRACESTATE = validated.tracestate;
242
+ env[FEATURE_FACTORY_TRACESTATE] = validated.tracestate;
243
+ }
244
+ const parentSpanId = validated.parentSpanId || validated.traceparentSpanId;
245
+ if (parentSpanId) env[FEATURE_FACTORY_PARENT_SPAN_ID] = parentSpanId;
246
+
247
+ return env;
248
+ }
249
+
250
+ export async function withSpan(name, attributesOrCallback = {}, callbackOrOptions, maybeOptions = {}) {
251
+ const callback = typeof attributesOrCallback === "function" ? attributesOrCallback : callbackOrOptions;
252
+ if (typeof callback !== "function") throw new Error("withSpan requires a callback");
253
+ const attributes = typeof attributesOrCallback === "function" ? {} : runAttributes(attributesOrCallback);
254
+ const options = typeof attributesOrCallback === "function" ? (callbackOrOptions || {}) : maybeOptions;
255
+
256
+ const api = await loadOtelApi(options.importer);
257
+ if (!api) {
258
+ // @opentelemetry/api is not installed: run the work without emitting a span
259
+ // rather than crashing. Telemetry is strictly optional at runtime.
260
+ return callback(noopSpan());
261
+ }
262
+
263
+ const tracer = api.trace.getTracer(options.tracerName || TELEMETRY_TRACER_NAME);
264
+ return tracer.startActiveSpan(String(name), { attributes }, options.context || api.context.active(), async (span) => {
265
+ try {
266
+ return await callback(span);
267
+ } catch (error) {
268
+ recordError(span, error);
269
+ throw error;
270
+ } finally {
271
+ span.end();
272
+ }
273
+ });
274
+ }
275
+
276
+ export function recordError(span, error) {
277
+ if (!span || !error) return;
278
+ const message = error instanceof Error ? error.message : String(error);
279
+ const type = error instanceof Error && error.name ? error.name : typeof error;
280
+ span.recordException?.(sanitizeException(error));
281
+ span.setStatus?.({ code: SPAN_STATUS_ERROR, message: sanitizeDiagnosticMessage(message) });
282
+ span.setAttribute?.("error.type", sanitizeDiagnosticMessage(type));
283
+ }
284
+
285
+ export function runAttributes(input = {}) {
286
+ if (!plainObject(input)) return {};
287
+ const output = {};
288
+ for (const [key, value] of Object.entries(input)) {
289
+ if (value === undefined || value === null || typeof value === "function") continue;
290
+ if (isUnsafeAttributeKey(key)) continue;
291
+ output[key] = sanitizeAttribute(key, value);
292
+ }
293
+ return output;
294
+ }
295
+
296
+ function sanitizeOtlpHeaderEntry(entry) {
297
+ const index = String(entry).indexOf("=");
298
+ if (index <= 0) return null;
299
+ const rawName = String(entry).slice(0, index);
300
+ const name = sanitizeHeaderName(rawName);
301
+ if (!name) return null;
302
+ return { name, present: true, value: REDACTED_ENV_VALUE };
303
+ }
304
+
305
+ function sanitizeOtlpEndpointValue(value) {
306
+ const raw = String(value || "").trim();
307
+ if (!raw) return raw;
308
+
309
+ try {
310
+ const parsed = new URL(raw);
311
+ const safeHost = sanitizeEndpointHost(parsed);
312
+ const safePath = sanitizeEndpointPath(parsed.pathname, raw);
313
+ const safeQuery = endpointSearchHasValues(parsed.searchParams) ? `?${REDACTED_ENV_VALUE}` : "";
314
+ const safeHash = parsed.hash ? `#${REDACTED_ENV_VALUE}` : "";
315
+ return `${parsed.protocol}//${safeHost}${safePath}${safeQuery}${safeHash}`;
316
+ } catch {
317
+ return scrubSecretEnv(raw);
318
+ }
319
+ }
320
+
321
+ function sanitizeEndpointHost(parsed) {
322
+ const hostname = parsed.hostname;
323
+ const port = parsed.port ? `:${parsed.port}` : "";
324
+ if (!hostname) return scrubSecretEnv(parsed.host || "");
325
+ if (hostname.startsWith("[") && hostname.endsWith("]")) return `${hostname}${port}`;
326
+ const safeHostname = hostname
327
+ .split(".")
328
+ .map((label) => sanitizeEndpointComponent(label))
329
+ .join(".");
330
+ return `${safeHostname}${port}`;
331
+ }
332
+
333
+ function sanitizeEndpointPath(pathname, raw) {
334
+ const safePath = String(pathname || "/")
335
+ .split("/")
336
+ .map((segment) => sanitizeEndpointComponent(segment))
337
+ .join("/") || "/";
338
+ if (safePath === "/" && !/^[a-z][a-z0-9+.-]*:\/\/[^/?#]+\//iu.test(raw)) return "";
339
+ return safePath;
340
+ }
341
+
342
+ function sanitizeEndpointComponent(value) {
343
+ if (!value) return value;
344
+ const decoded = safeDecodeURIComponent(value);
345
+ if (isSensitiveEnvValue(decoded) || scrubSecretEnv(decoded) === REDACTED_ENV_VALUE) return REDACTED_ENV_VALUE;
346
+ return scrubSecretEnv(value);
347
+ }
348
+
349
+ function endpointSearchHasValues(searchParams) {
350
+ for (const [key, value] of searchParams.entries()) {
351
+ if (key || value) return true;
352
+ }
353
+ return false;
354
+ }
355
+
356
+ function safeDecodeURIComponent(value) {
357
+ try {
358
+ return decodeURIComponent(value);
359
+ } catch {
360
+ return value;
361
+ }
362
+ }
363
+
364
+ function sanitizeHeaderName(value) {
365
+ const name = String(value || "").trim();
366
+ if (!name) return null;
367
+ if (!HEADER_NAME_PATTERN.test(name)) return REDACTED_ENV_VALUE;
368
+ if (isSensitiveEnvKey(name) || isSensitiveEnvValue(name)) return REDACTED_ENV_VALUE;
369
+ return name.toLowerCase();
370
+ }
371
+
372
+ function sanitizeAttribute(key, value) {
373
+ if (isSensitiveEnvKey(key)) return REDACTED_ENV_VALUE;
374
+ if (typeof value === "string") return isSensitiveEnvValue(value) ? REDACTED_ENV_VALUE : value;
375
+ if (typeof value === "number" || typeof value === "boolean") return value;
376
+ if (Array.isArray(value)) {
377
+ const safe = value
378
+ .filter((item) => SAFE_ATTRIBUTE_ARRAY_TYPES.has(typeof item))
379
+ .map((item) => (typeof item === "string" && isSensitiveEnvValue(item) ? REDACTED_ENV_VALUE : item));
380
+ // OTel attribute arrays must be homogeneous; mixed primitive types get
381
+ // dropped or rejected by SDKs, so coerce them to a JSON string instead.
382
+ const homogeneous = new Set(safe.map((item) => typeof item)).size <= 1;
383
+ return safe.length === value.length && homogeneous ? safe : JSON.stringify(scrubSecretEnv(value));
384
+ }
385
+ return JSON.stringify(scrubSecretEnv(value));
386
+ }
387
+
388
+ function isUnsafeAttributeKey(key) {
389
+ return isSecretShapedEnvKey(key);
390
+ }
391
+
392
+ function sanitizeException(error) {
393
+ const message = error instanceof Error ? error.message : String(error);
394
+ const type = error instanceof Error && error.name ? error.name : typeof error;
395
+ const exception = {
396
+ name: sanitizeDiagnosticMessage(type),
397
+ message: sanitizeDiagnosticMessage(message),
398
+ };
399
+ if (error instanceof Error && typeof error.stack === "string") {
400
+ exception.stack = sanitizeDiagnosticMessage(error.stack);
401
+ }
402
+ return exception;
403
+ }
404
+
405
+ function sanitizeDiagnosticMessage(message) {
406
+ return scrubSecretEnv(String(message || ""));
407
+ }
408
+
409
+ function validationError(error) {
410
+ return { ok: false, error };
411
+ }
412
+
413
+ function plainObject(value) {
414
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
415
+ }
416
+
417
+ function stringValue(value) {
418
+ return typeof value === "string" && value.trim().length > 0;
419
+ }