@logbrew/sdk 0.1.37 → 0.1.38

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/log-context.cjs CHANGED
@@ -1,95 +1,115 @@
1
- const ZERO_TRACE_ID = "00000000000000000000000000000000";
2
- const ZERO_SPAN_ID = "0000000000000000";
1
+ const { SdkError } = require("./sdk-error.cjs");
2
+ const { ZERO_SPAN_ID, ZERO_TRACE_ID, isRecord } = require("./validation.cjs");
3
3
 
4
- function buildLogContextHelpers({ SdkError }) {
5
- function compactMetadata(metadata) {
6
- if (metadata === undefined) {
7
- return {};
8
- }
9
- if (!metadata || Array.isArray(metadata) || typeof metadata !== "object") {
10
- throw new SdkError("validation_error", "metadata must be an object");
11
- }
12
- const safeMetadata = {};
13
- for (const [key, value] of Object.entries(metadata)) {
14
- if (isMetadataValue(value)) {
15
- safeMetadata[key] = value;
16
- }
17
- }
18
- return safeMetadata;
19
- }
4
+ /**
5
+ * @import { LogCorrelationTraceContext, Metadata, MetadataValue } from "./index.cjs"
6
+ */
20
7
 
21
- function traceFromProvider(provider, onError) {
22
- if (!provider) {
23
- return undefined;
24
- }
25
- try {
26
- return provider();
27
- } catch (error) {
28
- onError(error);
29
- return undefined;
30
- }
31
- }
32
-
33
- return {
34
- compactMetadata,
35
- isMetadataValue,
36
- normalizeLogTraceContext,
37
- traceFromProvider,
38
- traceMetadataFromLogContext
39
- };
40
- }
8
+ const TRACE_ID_PATTERN = /^[0-9a-fA-F]{32}$/u;
9
+ const SPAN_ID_PATTERN = /^[0-9a-fA-F]{16}$/u;
41
10
 
11
+ /**
12
+ * @param {unknown} value
13
+ * @returns {value is MetadataValue}
14
+ */
42
15
  function isMetadataValue(value) {
43
16
  return (
44
17
  value === null
45
18
  || typeof value === "string"
46
- || typeof value === "number" && Number.isFinite(value)
19
+ || (typeof value === "number" && Number.isFinite(value))
47
20
  || typeof value === "boolean"
48
21
  );
49
22
  }
50
23
 
51
- function traceMetadataFromLogContext(trace) {
52
- const normalized = normalizeLogTraceContext(trace);
53
- if (!normalized) {
24
+ /**
25
+ * @param {unknown} metadata
26
+ * @returns {Metadata}
27
+ */
28
+ function compactMetadata(metadata) {
29
+ if (metadata === undefined) {
54
30
  return {};
55
31
  }
56
- return {
57
- traceId: normalized.traceId,
58
- spanId: normalized.spanId,
59
- ...(normalized.parentSpanId !== undefined ? { parentSpanId: normalized.parentSpanId } : {}),
60
- ...(normalized.sampled !== undefined ? { sampled: normalized.sampled } : {})
61
- };
32
+ if (!isRecord(metadata)) {
33
+ throw new SdkError("validation_error", "metadata must be an object");
34
+ }
35
+ /** @type {Metadata} */
36
+ const safeMetadata = {};
37
+ for (const [key, value] of Object.entries(metadata)) {
38
+ if (isMetadataValue(value)) {
39
+ safeMetadata[key] = value;
40
+ }
41
+ }
42
+ return safeMetadata;
62
43
  }
63
44
 
45
+ /**
46
+ * @template Trace
47
+ * @param {(() => Trace) | null | undefined} provider
48
+ * @param {(error: unknown) => void} onError
49
+ * @returns {Trace | undefined}
50
+ */
51
+ function traceFromProvider(provider, onError) {
52
+ if (!provider) {
53
+ return undefined;
54
+ }
55
+ try {
56
+ return provider();
57
+ } catch (error) {
58
+ onError(error);
59
+ return undefined;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * @param {unknown} value
65
+ * @param {Readonly<RegExp>} pattern
66
+ * @param {string} zeroValue
67
+ * @returns {string | undefined}
68
+ */
69
+ function normalizeHexId(value, pattern, zeroValue) {
70
+ if (typeof value !== "string" || !pattern.test(value) || value.toLowerCase() === zeroValue) {
71
+ return undefined;
72
+ }
73
+ return value.toLowerCase();
74
+ }
75
+
76
+ /**
77
+ * @param {unknown} trace
78
+ * @returns {LogCorrelationTraceContext | undefined}
79
+ */
64
80
  function normalizeLogTraceContext(trace) {
65
- if (!trace || Array.isArray(trace) || typeof trace !== "object") {
81
+ if (!isRecord(trace)) {
66
82
  return undefined;
67
83
  }
68
- const traceId = normalizeHexId(trace.traceId, 32, ZERO_TRACE_ID);
69
- const spanId = normalizeHexId(trace.spanId, 16, ZERO_SPAN_ID);
70
- if (!traceId || !spanId) {
84
+ const traceId = normalizeHexId(trace["traceId"], TRACE_ID_PATTERN, ZERO_TRACE_ID);
85
+ const spanId = normalizeHexId(trace["spanId"], SPAN_ID_PATTERN, ZERO_SPAN_ID);
86
+ if (traceId === undefined || spanId === undefined) {
71
87
  return undefined;
72
88
  }
73
- const parentSpanId = normalizeHexId(trace.parentSpanId, 16, ZERO_SPAN_ID);
89
+ const parentSpanId = normalizeHexId(trace["parentSpanId"], SPAN_ID_PATTERN, ZERO_SPAN_ID);
90
+ const { sampled } = trace;
74
91
  return {
75
92
  traceId,
93
+
76
94
  spanId,
77
- ...(parentSpanId !== undefined ? { parentSpanId } : {}),
78
- ...(typeof trace.sampled === "boolean" ? { sampled: trace.sampled } : {})
95
+ ...(parentSpanId === undefined ? {} : { parentSpanId }),
96
+ ...(typeof sampled === "boolean" ? { sampled } : {})
79
97
  };
80
98
  }
81
99
 
82
- function normalizeHexId(value, length, zeroValue) {
83
- if (typeof value !== "string") {
84
- return undefined;
85
- }
86
- const pattern = length === 32
87
- ? /^[0-9a-fA-F]{32}$/u
88
- : /^[0-9a-fA-F]{16}$/u;
89
- if (!pattern.test(value) || value.toLowerCase() === zeroValue) {
90
- return undefined;
91
- }
92
- return value.toLowerCase();
100
+ /**
101
+ * @param {unknown} trace
102
+ * @returns {Metadata}
103
+ */
104
+ function traceMetadataFromLogContext(trace) {
105
+ const normalized = normalizeLogTraceContext(trace);
106
+ return normalized === undefined ? {} : { ...normalized };
93
107
  }
94
108
 
95
- module.exports = { buildLogContextHelpers };
109
+ module.exports = {
110
+ compactMetadata,
111
+ isMetadataValue,
112
+ normalizeLogTraceContext,
113
+ traceFromProvider,
114
+ traceMetadataFromLogContext
115
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logbrew/sdk",
3
- "version": "0.1.37",
3
+ "version": "0.1.38",
4
4
  "description": "Public LogBrew JavaScript SDK for building, validating, and flushing event batches.",
5
5
  "type": "module",
6
6
  "main": "./index.cjs",
@@ -48,15 +48,24 @@
48
48
  "index.js",
49
49
  "index.d.ts",
50
50
  "index.d.cts",
51
+ "issue-breadcrumbs.cjs",
51
52
  "issue-diagnostics.cjs",
53
+ "issue-evidence.cjs",
54
+ "issue-exception.cjs",
52
55
  "issue-stack.cjs",
56
+ "issue-stack-frames.cjs",
57
+ "issue-stack-text.cjs",
58
+ "issue-text.cjs",
53
59
  "log-context.cjs",
54
60
  "opentelemetry.cjs",
55
61
  "react-native.js",
56
62
  "react-native.d.ts",
63
+ "sdk-error.cjs",
57
64
  "support-ticket.cjs",
58
65
  "telemetry-context.cjs",
59
66
  "trace-context.cjs",
67
+ "transport-error.cjs",
68
+ "validation.cjs",
60
69
  "winston.cjs",
61
70
  "release-artifacts-common.js",
62
71
  "release-artifacts-build.cjs",
package/sdk-error.cjs ADDED
@@ -0,0 +1,36 @@
1
+ const MIN_RETRY_AFTER_MS = 0;
2
+
3
+ /**
4
+ * @param {unknown} value
5
+ * @returns {number | undefined}
6
+ */
7
+ function retryAfterMsOrUndefined(value) {
8
+ if (value === undefined) {
9
+ return undefined;
10
+ }
11
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= MIN_RETRY_AFTER_MS ? value : undefined;
12
+ }
13
+
14
+ class SdkError extends Error {
15
+ /**
16
+ * @param {string} code
17
+ * @param {string} message
18
+ * @param {Readonly<{ retryAfterMs?: unknown, retryable?: unknown }> | null} [details]
19
+ */
20
+ constructor(code, message, details = {}) {
21
+ super(message);
22
+ this.name = "SdkError";
23
+ this.code = code;
24
+ const retryAfterMs = retryAfterMsOrUndefined(details?.retryAfterMs);
25
+ if (retryAfterMs !== undefined) {
26
+ /** @type {number | undefined} */
27
+ this.retryAfterMs = retryAfterMs;
28
+ }
29
+ if (details?.retryable === true) {
30
+ /** @type {boolean | undefined} */
31
+ this.retryable = true;
32
+ }
33
+ }
34
+ }
35
+
36
+ module.exports = { SdkError, retryAfterMsOrUndefined };
@@ -1,4 +1,18 @@
1
+ const { SdkError } = require("./sdk-error.cjs");
2
+ const { isRecord, requireAllowedValue, requireNonEmpty, requireTraceId } = require("./validation.cjs");
3
+
4
+ /**
5
+ * @import { SupportTicketCategory, SupportTicketDraft, SupportTicketSource } from "./index.cjs"
6
+ * @typedef {string | number | boolean | null | undefined | SanitizedDiagnosticsValue[] | SanitizedDiagnostics} SanitizedDiagnosticsValue
7
+ * @typedef {{ [key: string]: SanitizedDiagnosticsValue }} SanitizedDiagnostics
8
+ * @typedef {Omit<SupportTicketDraft, "diagnostics"> & { diagnostics?: SanitizedDiagnostics }} SanitizedSupportTicketDraft
9
+ * @typedef {Omit<SanitizedSupportTicketDraft, "category" | "description" | "source" | "title">} SupportTicketContext
10
+ * @typedef {(child: unknown, childKey: string) => SanitizedDiagnosticsValue} SanitizeChild
11
+ */
12
+
13
+ /** @type {ReadonlySet<SupportTicketSource>} */
1
14
  const SUPPORT_TICKET_SOURCES = new Set(["cli", "sdk", "website", "docs", "mobile"]);
15
+ /** @type {ReadonlySet<SupportTicketCategory>} */
2
16
  const SUPPORT_TICKET_CATEGORIES = new Set([
3
17
  "sdk_install_failure",
4
18
  "ingest_failure",
@@ -11,165 +25,261 @@ const SUPPORT_TICKET_CATEGORIES = new Set([
11
25
  "billing_question",
12
26
  "other"
13
27
  ]);
28
+ const SENSITIVE_KEY_PARTS = [
29
+ "apikey",
30
+ "auth",
31
+ "authorization",
32
+ "authtoken",
33
+ "bearer",
34
+ "clientsecret",
35
+ "connectionstring",
36
+ "cookie",
37
+ "credential",
38
+ "dsn",
39
+ "email",
40
+ "password",
41
+ "passwd",
42
+ "privatekey",
43
+ "refreshtoken",
44
+ "secret",
45
+ "session",
46
+ "setcookie",
47
+ "token"
48
+ ];
14
49
  const SUPPORT_DIAGNOSTICS_MAX_DEPTH = 5;
15
50
  const SUPPORT_DIAGNOSTICS_MAX_ARRAY_LENGTH = 20;
16
51
  const SUPPORT_DIAGNOSTICS_MAX_STRING_LENGTH = 500;
52
+ const ROOT_DEPTH = 0;
53
+ const DEPTH_STEP = 1;
54
+ const TEXT_START = 0;
55
+ const KEY_SEPARATOR_PATTERN = /[^a-z0-9]/giu;
56
+ const SENSITIVE_ASSIGNMENT_PATTERN = /(?:authorization|api[_-]?key|token|secret|password|passwd|cookie)\s*[:=]/iu;
57
+ const BEARER_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/-]+=*/iu;
58
+ const LOGBREW_KEY_PATTERN = /\blbw_(?:ingest|client|api)_[A-Za-z0-9._-]+/iu;
59
+ const VENDOR_KEY_PATTERN = /\b(?:github_pat|ghp|gho|npm|pypi|sk_live|sk_test|xox[baprs]|AKIA)[A-Za-z0-9._-]+/u;
60
+ const LOCAL_PATH_PATTERN = /^(?:\/Users\/|\/home\/|\/var\/folders\/|[A-Za-z]:\\)/u;
61
+ const QUERY_OR_FRAGMENT_PATTERN = /[?#]/u;
17
62
 
18
- function buildCreateSupportTicketDraft({ SdkError, requireAllowedValue, requireNonEmpty, requireTraceId }) {
19
- return function createSupportTicketDraft(input) {
20
- if (!input || Array.isArray(input) || typeof input !== "object") {
21
- throw new SdkError("validation_error", "support ticket draft input must be an object");
22
- }
63
+ /**
64
+ * @param {string} key
65
+ * @returns {boolean}
66
+ */
67
+ function isSensitiveSupportKey(key) {
68
+ if (key === "") {
69
+ return false;
70
+ }
71
+ const normalized = key.replaceAll(KEY_SEPARATOR_PATTERN, "").toLowerCase();
72
+ return SENSITIVE_KEY_PARTS.some((needle) => normalized.includes(needle));
73
+ }
23
74
 
24
- requireAllowedValue("support ticket source", input.source, SUPPORT_TICKET_SOURCES);
25
- requireAllowedValue("support ticket category", input.category, SUPPORT_TICKET_CATEGORIES);
26
- const draft = {
27
- source: input.source,
28
- category: input.category,
29
- title: requiredSupportString(requireNonEmpty, "support ticket title", input.title),
30
- description: requiredSupportString(requireNonEmpty, "support ticket description", input.description)
31
- };
32
-
33
- addOptionalSupportString(draft, "project_id", "support ticket projectId", input.projectId, requireNonEmpty);
34
- addOptionalSupportString(draft, "environment", "support ticket environment", input.environment, requireNonEmpty);
35
- addOptionalSupportString(draft, "runtime", "support ticket runtime", input.runtime, requireNonEmpty);
36
- addOptionalSupportString(draft, "framework", "support ticket framework", input.framework, requireNonEmpty);
37
- addOptionalSupportString(draft, "sdk_package", "support ticket sdkPackage", input.sdkPackage, requireNonEmpty);
38
- addOptionalSupportString(draft, "sdk_version", "support ticket sdkVersion", input.sdkVersion, requireNonEmpty);
39
- addOptionalSupportString(draft, "release", "support ticket release", input.release, requireNonEmpty);
40
- if (input.traceId !== undefined) {
41
- requireTraceId(input.traceId);
42
- draft.trace_id = input.traceId.toLowerCase();
43
- }
44
- addOptionalSupportString(draft, "event_id", "support ticket eventId", input.eventId, requireNonEmpty);
45
- if (input.diagnostics !== undefined) {
46
- draft.diagnostics = sanitizeSupportDiagnostics(input.diagnostics, SdkError);
47
- }
75
+ /**
76
+ * @param {string} value
77
+ * @returns {boolean}
78
+ */
79
+ function isSensitiveSupportString(value) {
80
+ return SENSITIVE_ASSIGNMENT_PATTERN.test(value)
81
+ || BEARER_PATTERN.test(value)
82
+ || LOGBREW_KEY_PATTERN.test(value)
83
+ || VENDOR_KEY_PATTERN.test(value);
84
+ }
48
85
 
49
- return draft;
50
- };
86
+ /**
87
+ * @param {string} value
88
+ * @returns {string}
89
+ */
90
+ function redactUrl(value) {
91
+ try {
92
+ const url = new URL(value);
93
+ return `[redacted-url]${url.pathname || "/"}`;
94
+ } catch {
95
+ const [beforeQuery = value] = value.split(QUERY_OR_FRAGMENT_PATTERN);
96
+ return beforeQuery;
97
+ }
51
98
  }
52
99
 
53
- function requiredSupportString(requireNonEmpty, label, value) {
54
- requireNonEmpty(label, value);
55
- return value.trim();
100
+ /**
101
+ * @param {string} value
102
+ * @returns {string}
103
+ */
104
+ function sanitizeSupportDiagnosticString(value) {
105
+ const trimmed = value.trim();
106
+ if (trimmed === "") {
107
+ return "";
108
+ }
109
+ if (isSensitiveSupportString(trimmed)) {
110
+ return "[redacted]";
111
+ }
112
+ const urlRedacted = redactUrl(LOCAL_PATH_PATTERN.test(trimmed) ? "[redacted-path]" : trimmed);
113
+ return urlRedacted.length > SUPPORT_DIAGNOSTICS_MAX_STRING_LENGTH
114
+ ? `${urlRedacted.slice(TEXT_START, SUPPORT_DIAGNOSTICS_MAX_STRING_LENGTH)}...`
115
+ : urlRedacted;
116
+ }
117
+
118
+ /**
119
+ * @param {Readonly<Error>} error
120
+ * @returns {SanitizedDiagnostics}
121
+ */
122
+ function errorDiagnostics(error) {
123
+ return { name: error.name || "Error" };
56
124
  }
57
125
 
58
- function addOptionalSupportString(target, key, label, value, requireNonEmpty) {
59
- if (value === undefined) {
60
- return;
126
+ /**
127
+ * @param {unknown} value
128
+ * @returns {SanitizedDiagnosticsValue}
129
+ */
130
+ function sanitizeSupportDiagnosticScalar(value) {
131
+ if (value === null || (typeof value === "number" && Number.isFinite(value)) || typeof value === "boolean") {
132
+ return value;
61
133
  }
62
- target[key] = requiredSupportString(requireNonEmpty, label, value);
134
+ return typeof value === "string" ? sanitizeSupportDiagnosticString(value) : undefined;
63
135
  }
64
136
 
65
- function sanitizeSupportDiagnostics(diagnostics, SdkError) {
66
- if (!diagnostics || Array.isArray(diagnostics) || typeof diagnostics !== "object") {
67
- throw new SdkError("validation_error", "support ticket diagnostics must be an object");
137
+ /**
138
+ * @param {readonly unknown[]} value
139
+ * @param {SanitizeChild} sanitizeChild
140
+ * @returns {SanitizedDiagnosticsValue[]}
141
+ */
142
+ function sanitizeSupportDiagnosticList(value, sanitizeChild) {
143
+ return value.slice(TEXT_START, SUPPORT_DIAGNOSTICS_MAX_ARRAY_LENGTH).map((item) => sanitizeChild(item, ""));
144
+ }
145
+
146
+ /**
147
+ * @param {Readonly<Record<string, unknown>>} value
148
+ * @param {SanitizeChild} sanitizeChild
149
+ * @returns {SanitizedDiagnostics}
150
+ */
151
+ function sanitizeSupportDiagnosticRecord(value, sanitizeChild) {
152
+ /** @type {SanitizedDiagnostics} */
153
+ const safe = {};
154
+ for (const [childKey, childValue] of Object.entries(value)) {
155
+ const sanitized = sanitizeChild(childValue, childKey);
156
+ if (sanitized !== undefined) {
157
+ safe[childKey] = sanitized;
158
+ }
68
159
  }
69
- return sanitizeSupportDiagnosticValue(diagnostics, 0, "");
160
+ return safe;
70
161
  }
71
162
 
163
+ /**
164
+ * @param {unknown} value
165
+ * @param {number} depth
166
+ * @param {string} key
167
+ * @returns {SanitizedDiagnosticsValue}
168
+ */
72
169
  function sanitizeSupportDiagnosticValue(value, depth, key) {
73
170
  if (isSensitiveSupportKey(key)) {
74
171
  return "[redacted]";
75
172
  }
76
173
  if (value instanceof Error) {
77
- return { name: value.name || "Error" };
174
+ return errorDiagnostics(value);
78
175
  }
79
- if (value === null || typeof value === "number" && Number.isFinite(value) || typeof value === "boolean") {
80
- return value;
176
+ if (!Array.isArray(value) && !isRecord(value)) {
177
+ return sanitizeSupportDiagnosticScalar(value);
81
178
  }
82
- if (typeof value === "string") {
83
- return sanitizeSupportDiagnosticString(value);
179
+ if (depth >= SUPPORT_DIAGNOSTICS_MAX_DEPTH) {
180
+ return "[max-depth]";
84
181
  }
85
- if (Array.isArray(value)) {
86
- if (depth >= SUPPORT_DIAGNOSTICS_MAX_DEPTH) {
87
- return "[max-depth]";
88
- }
89
- return value
90
- .slice(0, SUPPORT_DIAGNOSTICS_MAX_ARRAY_LENGTH)
91
- .map((item) => sanitizeSupportDiagnosticValue(item, depth + 1, ""));
182
+ const childDepth = depth + DEPTH_STEP;
183
+ return isRecord(value)
184
+ ? sanitizeSupportDiagnosticRecord(value, (child, childKey) => sanitizeSupportDiagnosticValue(child, childDepth, childKey))
185
+ : sanitizeSupportDiagnosticList(value, (child, childKey) => sanitizeSupportDiagnosticValue(child, childDepth, childKey));
186
+ }
187
+
188
+ /**
189
+ * @param {unknown} diagnostics
190
+ * @returns {{ diagnostics?: SanitizedDiagnostics }}
191
+ */
192
+ function optionalDiagnostics(diagnostics) {
193
+ if (diagnostics === undefined) {
194
+ return {};
92
195
  }
93
- if (value && typeof value === "object") {
94
- if (depth >= SUPPORT_DIAGNOSTICS_MAX_DEPTH) {
95
- return "[max-depth]";
96
- }
97
- const safe = {};
98
- for (const [childKey, childValue] of Object.entries(value)) {
99
- const sanitized = sanitizeSupportDiagnosticValue(childValue, depth + 1, childKey);
100
- if (sanitized !== undefined) {
101
- safe[childKey] = sanitized;
102
- }
103
- }
104
- return safe;
196
+ if (!isRecord(diagnostics)) {
197
+ throw new SdkError("validation_error", "support ticket diagnostics must be an object");
198
+ }
199
+ if (diagnostics instanceof Error) {
200
+ return { diagnostics: errorDiagnostics(diagnostics) };
105
201
  }
106
- return undefined;
202
+ return {
203
+ diagnostics: sanitizeSupportDiagnosticRecord(
204
+ diagnostics,
205
+ (child, childKey) => sanitizeSupportDiagnosticValue(child, ROOT_DEPTH + DEPTH_STEP, childKey)
206
+ )
207
+ };
107
208
  }
108
209
 
109
- function isSensitiveSupportKey(key) {
110
- if (typeof key !== "string" || key === "") {
111
- return false;
112
- }
113
- const normalized = key.replace(/[^a-z0-9]/giu, "").toLowerCase();
114
- return [
115
- "apikey",
116
- "auth",
117
- "authorization",
118
- "authtoken",
119
- "bearer",
120
- "clientsecret",
121
- "connectionstring",
122
- "cookie",
123
- "credential",
124
- "dsn",
125
- "email",
126
- "password",
127
- "passwd",
128
- "privatekey",
129
- "refreshtoken",
130
- "secret",
131
- "session",
132
- "setcookie",
133
- "token"
134
- ].some((needle) => normalized.includes(needle));
210
+ /**
211
+ * @param {string} label
212
+ * @param {unknown} value
213
+ * @returns {string}
214
+ */
215
+ function requiredSupportString(label, value) {
216
+ return requireNonEmpty(label, value).trim();
135
217
  }
136
218
 
137
- function sanitizeSupportDiagnosticString(value) {
138
- const trimmed = value.trim();
139
- if (trimmed === "") {
140
- return "";
219
+ /**
220
+ * @template {string} Key
221
+ * @param {Key} key
222
+ * @param {string} label
223
+ * @param {unknown} value
224
+ * @returns {{ [Field in Key]?: string }}
225
+ */
226
+ function optionalSupportString(key, label, value) {
227
+ /** @type {{ [Field in Key]?: string }} */
228
+ const field = {};
229
+ if (value !== undefined) {
230
+ field[key] = requiredSupportString(label, value);
141
231
  }
142
- if (isSensitiveSupportString(trimmed)) {
143
- return "[redacted]";
144
- }
145
- const pathRedacted = redactLocalPath(trimmed);
146
- const urlRedacted = redactUrl(pathRedacted);
147
- return urlRedacted.length > SUPPORT_DIAGNOSTICS_MAX_STRING_LENGTH
148
- ? `${urlRedacted.slice(0, SUPPORT_DIAGNOSTICS_MAX_STRING_LENGTH)}...`
149
- : urlRedacted;
232
+ return field;
150
233
  }
151
234
 
152
- function isSensitiveSupportString(value) {
153
- return /(?:authorization|api[_-]?key|token|secret|password|passwd|cookie)\s*[:=]/iu.test(value)
154
- || /\bBearer\s+[A-Za-z0-9._~+/-]+=*/iu.test(value)
155
- || /\blbw_(?:ingest|client|api)_[A-Za-z0-9._-]+/iu.test(value)
156
- || /\b(?:github_pat|ghp|gho|npm|pypi|sk_live|sk_test|xox[baprs]|AKIA)[A-Za-z0-9._-]+/u.test(value);
235
+ /**
236
+ * @param {unknown} traceId
237
+ * @returns {{ trace_id?: string }}
238
+ */
239
+ function optionalTraceId(traceId) {
240
+ return traceId === undefined ? {} : { trace_id: requireTraceId(traceId).toLowerCase() };
157
241
  }
158
242
 
159
- function redactLocalPath(value) {
160
- if (/^(?:\/Users\/|\/home\/|\/var\/folders\/|[A-Za-z]:\\)/u.test(value)) {
161
- return "[redacted-path]";
162
- }
163
- return value;
243
+ /**
244
+ * @param {Readonly<Record<string, unknown>>} input
245
+ * @returns {SupportTicketContext}
246
+ */
247
+ function supportTicketContext(input) {
248
+ return {
249
+ ...optionalSupportString("project_id", "support ticket projectId", input["projectId"]),
250
+ ...optionalSupportString("environment", "support ticket environment", input["environment"]),
251
+ ...optionalSupportString("runtime", "support ticket runtime", input["runtime"]),
252
+ ...optionalSupportString("framework", "support ticket framework", input["framework"]),
253
+ ...optionalSupportString("sdk_package", "support ticket sdkPackage", input["sdkPackage"]),
254
+ ...optionalSupportString("sdk_version", "support ticket sdkVersion", input["sdkVersion"]),
255
+ ...optionalSupportString("release", "support ticket release", input["release"]),
256
+ ...optionalTraceId(input["traceId"]),
257
+ ...optionalSupportString("event_id", "support ticket eventId", input["eventId"]),
258
+ ...optionalDiagnostics(input["diagnostics"])
259
+ };
164
260
  }
165
261
 
166
- function redactUrl(value) {
167
- try {
168
- const url = new URL(value);
169
- return `[redacted-url]${url.pathname || "/"}`;
170
- } catch {
171
- return value.split(/[?#]/u)[0];
262
+ /**
263
+ * @param {unknown} input
264
+ * @returns {SanitizedSupportTicketDraft}
265
+ */
266
+ function createSupportTicketDraft(input) {
267
+ if (!isRecord(input)) {
268
+ throw new SdkError("validation_error", "support ticket draft input must be an object");
172
269
  }
270
+ const source = requireAllowedValue("support ticket source", input["source"], SUPPORT_TICKET_SOURCES);
271
+ const category = requireAllowedValue("support ticket category", input["category"], SUPPORT_TICKET_CATEGORIES);
272
+ const title = requiredSupportString("support ticket title", input["title"]);
273
+ const description = requiredSupportString("support ticket description", input["description"]);
274
+ return {
275
+ source,
276
+
277
+ category,
278
+ title,
279
+
280
+ description,
281
+ ...supportTicketContext(input)
282
+ };
173
283
  }
174
284
 
175
- module.exports = { buildCreateSupportTicketDraft };
285
+ module.exports = { createSupportTicketDraft };