@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/issue-stack.cjs CHANGED
@@ -1,276 +1,239 @@
1
1
  "use strict";
2
2
 
3
+ const { SdkError } = require("./sdk-error.cjs");
4
+ const { isRecord } = require("./validation.cjs");
5
+ const {
6
+ SAFE_DEBUG_ID_PATTERN,
7
+ basename,
8
+ frameCoordinateFromText,
9
+ frameIdentity,
10
+ sanitizeFrameFilename
11
+ } = require("./issue-stack-text.cjs");
12
+ const { validateIssueStackFrames, validateNativeStackFrames } = require("./issue-stack-frames.cjs");
13
+
14
+ /**
15
+ * @import { IssueStackFrame } from "./index.cjs"
16
+ * @typedef {{ frames: IssueStackFrame[], truncated: boolean }} StackEvidence
17
+ * @typedef {readonly (readonly [string, unknown])[]} DebugIdEntries
18
+ * @typedef {{ functionName: string | undefined, location: string }} FrameLocation
19
+ * @typedef {{ readonly debugId: string, readonly path: string }} DebugIdCandidate
20
+ */
21
+
3
22
  const MAX_ISSUE_STACK_FRAMES = 32;
4
23
  const MAX_ISSUE_STACK_FUNCTION_LENGTH = 256;
5
- const MAX_ISSUE_STACK_MODULE_LENGTH = 512;
6
- const SAFE_DEBUG_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
7
- const NATIVE_IMAGE_UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u;
8
- const NATIVE_OFFSET_PATTERN = /^[0-9a-f]{16}$/u;
9
- const NATIVE_ARCHITECTURES = new Set(["arm", "arm64", "arm64e", "x86", "x86_64"]);
10
- const LOCAL_ABSOLUTE_PATH_PATTERN = /(?:^|\s)(?:\/(?:Users|home|private|tmp|var|Volumes)\/|[A-Za-z]:[\\/])/u;
24
+ const MAX_RUNTIME_DEBUG_IDS = 512;
25
+ const MIN_FRAME_LOCATION_PARTS = 3;
26
+ const NO_OFFSET = 0;
27
+ const HERMES_BYTECODE_OFFSET = 1;
28
+ const HERMES_BYTECODE_LINE = 1;
29
+ const UNIQUE_MATCH = 1;
30
+ const TEXT_START = 0;
31
+ const MARKER_WIDTH = 1;
32
+ const LAST_CHARACTER = -1;
33
+ const CHROME_FRAME_PREFIX = "at ";
34
+ const HERMES_BYTECODE_PREFIX = "address at ";
35
+ const LINE_BREAK_PATTERN = /\r?\n/u;
36
+ const LEADING_SLASHES_PATTERN = /^\/+/u;
11
37
  const DEBUG_ID_REGISTRY = Symbol.for("logbrew.release-artifact.debug-ids");
12
38
 
13
- function buildIssueStackHelpers({ SdkError }) {
14
- function javascriptStackEvidence(stack, debugIdMap) {
15
- if (typeof stack !== "string" || stack.trim() === "") {
16
- return { frames: [], truncated: false };
17
- }
18
- const frames = [];
19
- let truncated = false;
20
- for (const rawLine of stack.split(/\r?\n/u)) {
21
- const parsed = parseJavaScriptStackFrame(rawLine);
22
- if (parsed) {
23
- if (frames.length === MAX_ISSUE_STACK_FRAMES) {
24
- truncated = true;
25
- break;
26
- }
27
- const debugId = debugIdForFrame(parsed.filename, debugIdMap, SdkError);
28
- frames.push({ ...parsed, ...(debugId ? { debugId } : {}) });
29
- }
30
- }
31
- return { frames, truncated };
39
+ /**
40
+ * @param {string} value
41
+ * @returns {string | undefined}
42
+ */
43
+ function generatedFrameFunction(value) {
44
+ const functionName = frameIdentity(value, MAX_ISSUE_STACK_FUNCTION_LENGTH, false);
45
+ if (
46
+ functionName === undefined
47
+ || functionName.includes("@")
48
+ || functionName.includes("/")
49
+ || functionName.includes("\\")
50
+ || functionName.includes("?")
51
+ || functionName.includes("#")
52
+ ) {
53
+ return undefined;
32
54
  }
55
+ return functionName;
56
+ }
33
57
 
34
- function validateIssueStackFrames(stackFrames) {
35
- if (stackFrames === undefined) {
36
- return undefined;
58
+ /**
59
+ * @param {string} line
60
+ * @returns {FrameLocation}
61
+ */
62
+ function splitFrameFunction(line) {
63
+ if (line.startsWith(CHROME_FRAME_PREFIX)) {
64
+ const location = line.slice(CHROME_FRAME_PREFIX.length).trim();
65
+ if (!location.endsWith(")") || !location.includes("(")) {
66
+ return { functionName: undefined, location };
37
67
  }
38
- if (!Array.isArray(stackFrames) || stackFrames.length === 0 || stackFrames.length > MAX_ISSUE_STACK_FRAMES) {
39
- throw new SdkError("validation_error", `issue stackFrames must contain 1-${MAX_ISSUE_STACK_FRAMES} frames`);
40
- }
41
- return stackFrames.map((frame) => {
42
- if (!frame || Array.isArray(frame) || typeof frame !== "object") {
43
- throw new SdkError("validation_error", "issue stack frame must be an object");
44
- }
45
- if (typeof frame.filename !== "string") {
46
- throw new SdkError("validation_error", "issue stack frame filename is invalid");
47
- }
48
- const filename = sanitizeFrameFilename(frame.filename);
49
- if (!filename || filename.length > 2048 || hasControlCharacter(filename)) {
50
- throw new SdkError("validation_error", "issue stack frame filename is invalid");
51
- }
52
- const line = frameCoordinateFromText(frame.line);
53
- const column = frameCoordinateFromText(frame.column);
54
- if (line === null || column === null || line > 2147483647 || column > 2147483647) {
55
- throw new SdkError("validation_error", "issue stack frame coordinates must be positive integers");
56
- }
57
- const debugId = frame.debugId === undefined
58
- ? undefined
59
- : typeof frame.debugId === "string" && SAFE_DEBUG_ID_PATTERN.test(frame.debugId.trim())
60
- ? frame.debugId.trim().toLowerCase()
61
- : null;
62
- if (debugId === null) {
63
- throw new SdkError("validation_error", "issue stack frame debugId is invalid");
64
- }
65
- const functionName = optionalFrameIdentity(frame.function, MAX_ISSUE_STACK_FUNCTION_LENGTH);
66
- if (functionName === null) {
67
- throw new SdkError("validation_error", "issue stack frame function is invalid");
68
- }
69
- const moduleName = optionalFrameIdentity(frame.module, MAX_ISSUE_STACK_MODULE_LENGTH, true);
70
- if (moduleName === null) {
71
- throw new SdkError("validation_error", "issue stack frame module is invalid");
72
- }
73
- const inApp = frame.inApp === undefined
74
- ? undefined
75
- : typeof frame.inApp === "boolean"
76
- ? frame.inApp
77
- : null;
78
- if (inApp === null) {
79
- throw new SdkError("validation_error", "issue stack frame inApp is invalid");
80
- }
81
- return {
82
- filename,
83
- line,
84
- column,
85
- ...(functionName ? { function: functionName } : {}),
86
- ...(moduleName ? { module: moduleName } : {}),
87
- ...(inApp !== undefined ? { inApp } : {}),
88
- ...(debugId ? { debugId } : {})
89
- };
90
- });
68
+ const marker = location.lastIndexOf("(");
69
+ return {
70
+ functionName: generatedFrameFunction(location.slice(TEXT_START, marker)),
71
+ location: location.slice(marker + MARKER_WIDTH, LAST_CHARACTER)
72
+ };
91
73
  }
92
-
93
- function validateNativeStackFrames(frames) {
94
- if (frames === undefined) {
95
- return undefined;
96
- }
97
- if (!Array.isArray(frames) || frames.length === 0 || frames.length > MAX_ISSUE_STACK_FRAMES) {
98
- throw new SdkError("validation_error", `issue nativeStackFrames must contain 1-${MAX_ISSUE_STACK_FRAMES} frames`);
99
- }
100
- return frames.map((frame) => {
101
- const keys = frame && !Array.isArray(frame) && typeof frame === "object" ? Object.keys(frame) : [];
102
- if (keys.length !== 3
103
- || !keys.every((key) => ["imageUuid", "architecture", "instructionOffset"].includes(key))
104
- || typeof frame.imageUuid !== "string" || !NATIVE_IMAGE_UUID_PATTERN.test(frame.imageUuid)
105
- || !NATIVE_ARCHITECTURES.has(frame.architecture)
106
- || typeof frame.instructionOffset !== "string" || !NATIVE_OFFSET_PATTERN.test(frame.instructionOffset)) {
107
- throw new SdkError("validation_error", "issue native stack frame is invalid");
108
- }
109
- return {
110
- imageUuid: frame.imageUuid,
111
- architecture: frame.architecture,
112
- instructionOffset: frame.instructionOffset
113
- };
114
- });
74
+ if (!line.includes("@")) {
75
+ return { functionName: undefined, location: line };
115
76
  }
116
-
117
- return { javascriptStackEvidence, validateIssueStackFrames, validateNativeStackFrames };
77
+ const marker = line.lastIndexOf("@");
78
+ return {
79
+ functionName: generatedFrameFunction(line.slice(TEXT_START, marker)),
80
+ location: line.slice(marker + MARKER_WIDTH)
81
+ };
118
82
  }
119
83
 
120
- function parseJavaScriptStackFrame(rawLine) {
121
- const line = typeof rawLine === "string" ? rawLine.trim() : "";
122
- if (!line) {
123
- return null;
124
- }
125
- let location = line;
126
- let functionName;
127
- if (location.startsWith("at ")) {
128
- location = location.slice(3).trim();
129
- if (location.endsWith(")") && location.includes("(")) {
130
- const marker = location.lastIndexOf("(");
131
- functionName = generatedFrameFunction(location.slice(0, marker));
132
- location = location.slice(marker + 1, -1);
133
- }
134
- } else if (location.includes("@")) {
135
- const marker = location.lastIndexOf("@");
136
- functionName = generatedFrameFunction(location.slice(0, marker));
137
- location = location.slice(marker + 1);
138
- }
139
- const hermesBytecode = location.startsWith("address at ");
140
- location = location.replace(/^address at /u, "");
84
+ /**
85
+ * @param {string} location
86
+ * @param {boolean} hermesBytecode
87
+ * @returns {Omit<IssueStackFrame, "function"> | undefined}
88
+ */
89
+ function frameCoordinates(location, hermesBytecode) {
141
90
  const parts = location.split(":");
142
- if (parts.length < 3) {
143
- return null;
91
+ if (parts.length < MIN_FRAME_LOCATION_PARTS) {
92
+ return undefined;
144
93
  }
145
94
  const rawColumn = parts.pop();
146
95
  const lineNumber = frameCoordinateFromText(parts.pop());
147
- const column = frameCoordinateFromText(rawColumn, hermesBytecode && lineNumber === 1 ? 1 : 0);
96
+ const column = frameCoordinateFromText(
97
+ rawColumn,
98
+ hermesBytecode && lineNumber === HERMES_BYTECODE_LINE ? HERMES_BYTECODE_OFFSET : NO_OFFSET
99
+ );
148
100
  const filename = sanitizeFrameFilename(parts.join(":"));
149
- if (!filename || lineNumber === null || column === null) {
150
- return null;
101
+ if (filename === "" || lineNumber === undefined || column === undefined) {
102
+ return undefined;
151
103
  }
152
104
  return {
153
105
  filename,
154
106
  line: lineNumber,
155
- column,
156
- ...(functionName ? { function: functionName } : {})
107
+
108
+ column
157
109
  };
158
110
  }
159
111
 
160
- function optionalFrameIdentity(value, maxLength, rejectLocationText = false) {
161
- if (value === undefined) {
112
+ /**
113
+ * @param {string} rawLine
114
+ * @returns {IssueStackFrame | undefined}
115
+ */
116
+ function parseJavaScriptStackFrame(rawLine) {
117
+ const line = rawLine.trim();
118
+ if (line === "") {
162
119
  return undefined;
163
120
  }
164
- if (typeof value !== "string") {
165
- return null;
166
- }
167
- const identity = value.trim();
168
- if (!identity
169
- || Array.from(identity).length > maxLength
170
- || hasControlCharacter(identity)
171
- || (rejectLocationText && (identity.includes("?") || identity.includes("#")))) {
172
- return null;
173
- }
174
- return identity;
175
- }
176
-
177
- function generatedFrameFunction(value) {
178
- const functionName = optionalFrameIdentity(value, MAX_ISSUE_STACK_FUNCTION_LENGTH);
179
- if (!functionName
180
- || functionName.includes("@")
181
- || functionName.includes("/")
182
- || functionName.includes("\\")
183
- || functionName.includes("?")
184
- || functionName.includes("#")) {
121
+ const { functionName, location } = splitFrameFunction(line);
122
+ const hermesBytecode = location.startsWith(HERMES_BYTECODE_PREFIX);
123
+ const coordinates = frameCoordinates(
124
+ hermesBytecode ? location.slice(HERMES_BYTECODE_PREFIX.length) : location,
125
+ hermesBytecode
126
+ );
127
+ if (coordinates === undefined) {
185
128
  return undefined;
186
129
  }
187
- return functionName;
130
+ return functionName === undefined ? coordinates : { ...coordinates, function: functionName };
188
131
  }
189
132
 
190
- function frameCoordinateFromText(value, offset = 0) {
191
- const text = String(value);
192
- if (!/^(?:0|[1-9][0-9]*)$/u.test(text)) {
193
- return null;
194
- }
195
- const parsed = Number(text) + offset;
196
- return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
197
- }
198
-
199
- function sanitizeFrameFilename(value) {
200
- let filename = String(value ?? "").trim();
201
- if (!filename) {
202
- return "";
203
- }
204
- filename = filename.split("?", 1)[0].split("#", 1)[0];
205
- if (filename.startsWith("file://")) {
206
- filename = filename.slice("file://".length);
207
- }
208
- if (LOCAL_ABSOLUTE_PATH_PATTERN.test(filename)) {
209
- return basename(filename);
210
- }
211
- return filename;
212
- }
213
-
214
- function debugIdForFrame(filename, debugIdMap, SdkError) {
215
- if (debugIdMap !== undefined && debugIdMap !== null
216
- && (!debugIdMap || Array.isArray(debugIdMap) || typeof debugIdMap !== "object")) {
217
- throw new SdkError("validation_error", "debugIdMap must be an object");
218
- }
219
- return debugIdFromEntries(filename, debugIdMap ? Object.entries(debugIdMap) : [])
220
- ?? debugIdFromEntries(filename, runtimeDebugIdEntries());
133
+ /**
134
+ * @param {string} filename
135
+ * @param {string} candidate
136
+ * @returns {boolean}
137
+ */
138
+ function matchesFramePath(filename, candidate) {
139
+ return filename === candidate
140
+ || (candidate.includes("/") && filename.endsWith(`/${candidate.replace(LEADING_SLASHES_PATTERN, "")}`));
221
141
  }
222
142
 
143
+ /**
144
+ * @param {string} filename
145
+ * @param {DebugIdEntries} entries
146
+ * @returns {string | undefined}
147
+ */
223
148
  function debugIdFromEntries(filename, entries) {
224
149
  const normalizedFilename = sanitizeFrameFilename(filename);
225
150
  const frameBasename = basename(normalizedFilename);
226
- let basenameMatch = null;
227
- for (const [candidate, debugId] of entries) {
228
- if (typeof debugId !== "string" || !SAFE_DEBUG_ID_PATTERN.test(debugId.trim())) {
229
- continue;
230
- }
231
- const normalizedCandidate = sanitizeFrameFilename(candidate);
232
- const normalizedDebugId = debugId.trim().toLowerCase();
233
- if (normalizedFilename === normalizedCandidate
234
- || (normalizedCandidate.includes("/")
235
- && normalizedFilename.endsWith(`/${normalizedCandidate.replace(/^\/+/, "")}`))) {
236
- return normalizedDebugId;
237
- }
238
- if (frameBasename === basename(normalizedCandidate)) {
239
- basenameMatch = basenameMatch === null || basenameMatch === normalizedDebugId
240
- ? normalizedDebugId
241
- : false;
242
- }
243
- }
244
- return typeof basenameMatch === "string" ? basenameMatch : null;
151
+ /** @type {readonly DebugIdCandidate[]} */
152
+ const candidates = entries.flatMap(([candidate, debugId]) =>
153
+ typeof debugId === "string" && SAFE_DEBUG_ID_PATTERN.test(debugId.trim())
154
+ ? [{ debugId: debugId.trim().toLowerCase(), path: sanitizeFrameFilename(candidate) }]
155
+ : []);
156
+ const exact = candidates.find((candidate) => matchesFramePath(normalizedFilename, candidate.path));
157
+ if (exact !== undefined) {
158
+ return exact.debugId;
159
+ }
160
+ const basenameIds = new Set(
161
+ candidates.filter((candidate) => basename(candidate.path) === frameBasename).map((candidate) => candidate.debugId)
162
+ );
163
+ const [onlyId] = basenameIds;
164
+ return basenameIds.size === UNIQUE_MATCH ? onlyId : undefined;
245
165
  }
246
166
 
167
+ /**
168
+ * @returns {DebugIdEntries}
169
+ */
247
170
  function runtimeDebugIdEntries() {
248
171
  try {
249
- const registry = typeof globalThis === "object"
250
- ? Object.getOwnPropertyDescriptor(globalThis, DEBUG_ID_REGISTRY)?.value
251
- : null;
252
- if (!registry || Array.isArray(registry) || typeof registry !== "object") {
172
+ /** @type {unknown} */
173
+ const registry = Object.getOwnPropertyDescriptor(globalThis, DEBUG_ID_REGISTRY)?.value;
174
+ if (!isRecord(registry)) {
253
175
  return [];
254
176
  }
255
- return Object.entries(Object.getOwnPropertyDescriptors(registry)).slice(0, 512).flatMap(
256
- ([candidate, descriptor]) => "value" in descriptor ? [[candidate, descriptor.value]] : []
257
- );
177
+ return Object.getOwnPropertyNames(registry)
178
+ .slice(TEXT_START, MAX_RUNTIME_DEBUG_IDS)
179
+ .flatMap((candidate) => {
180
+ const descriptor = Object.getOwnPropertyDescriptor(registry, candidate);
181
+ /** @type {unknown} */
182
+ const value = descriptor?.value;
183
+ return descriptor !== undefined && "value" in descriptor ? [[candidate, value]] : [];
184
+ });
258
185
  } catch {
259
186
  return [];
260
187
  }
261
188
  }
262
189
 
263
- function basename(value) {
264
- const normalized = String(value).replace(/\\/gu, "/").replace(/\/+$/u, "");
265
- const marker = normalized.lastIndexOf("/");
266
- return marker === -1 ? normalized : normalized.slice(marker + 1);
190
+ /**
191
+ * @param {string} filename
192
+ * @param {unknown} debugIdMap
193
+ * @returns {string | undefined}
194
+ */
195
+ function debugIdForFrame(filename, debugIdMap) {
196
+ if (debugIdMap === undefined || debugIdMap === null) {
197
+ return debugIdFromEntries(filename, runtimeDebugIdEntries());
198
+ }
199
+ if (!isRecord(debugIdMap)) {
200
+ throw new SdkError("validation_error", "debugIdMap must be an object");
201
+ }
202
+ return debugIdFromEntries(filename, Object.entries(debugIdMap))
203
+ ?? debugIdFromEntries(filename, runtimeDebugIdEntries());
204
+ }
205
+
206
+ /**
207
+ * @param {Readonly<IssueStackFrame>} frame
208
+ * @param {unknown} debugIdMap
209
+ * @returns {IssueStackFrame}
210
+ */
211
+ function withDebugId(frame, debugIdMap) {
212
+ const debugId = debugIdForFrame(frame.filename, debugIdMap);
213
+ return debugId === undefined ? frame : { ...frame, debugId };
267
214
  }
268
215
 
269
- function hasControlCharacter(value) {
270
- return Array.from(value).some((character) => {
271
- const code = character.codePointAt(0);
272
- return code !== undefined && (code <= 31 || code === 127);
273
- });
216
+ /**
217
+ * @param {unknown} stack
218
+ * @param {unknown} debugIdMap
219
+ * @returns {StackEvidence}
220
+ */
221
+ function javascriptStackEvidence(stack, debugIdMap) {
222
+ if (typeof stack !== "string" || stack.trim() === "") {
223
+ return { frames: [], truncated: false };
224
+ }
225
+ /** @type {IssueStackFrame[]} */
226
+ const frames = [];
227
+ for (const rawLine of stack.split(LINE_BREAK_PATTERN)) {
228
+ const parsed = parseJavaScriptStackFrame(rawLine);
229
+ if (parsed !== undefined) {
230
+ if (frames.length === MAX_ISSUE_STACK_FRAMES) {
231
+ return { frames, truncated: true };
232
+ }
233
+ frames.push(withDebugId(parsed, debugIdMap));
234
+ }
235
+ }
236
+ return { frames, truncated: false };
274
237
  }
275
238
 
276
- module.exports = { buildIssueStackHelpers };
239
+ module.exports = { javascriptStackEvidence, validateIssueStackFrames, validateNativeStackFrames };
package/issue-text.cjs ADDED
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+
3
+ const { SdkError } = require("./sdk-error.cjs");
4
+ const { hasUnicodeControlCharacter } = require("./validation.cjs");
5
+
6
+ const MACHINE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_.:-]{0,63}$/u;
7
+ const LOCATION_TEXT_PATTERN = /[?#]/u;
8
+
9
+ /**
10
+ * @param {string} label
11
+ * @param {unknown} value
12
+ * @returns {string}
13
+ */
14
+ function requireMachineName(label, value) {
15
+ if (typeof value !== "string" || !MACHINE_NAME_PATTERN.test(value)) {
16
+ throw new SdkError("validation_error", `${label} must match ${MACHINE_NAME_PATTERN}`);
17
+ }
18
+ return value;
19
+ }
20
+
21
+ /**
22
+ * @param {string} label
23
+ * @param {unknown} value
24
+ * @param {number} maxLength
25
+ * @returns {string}
26
+ */
27
+ function boundedText(label, value, maxLength) {
28
+ const normalized = typeof value === "string" ? value.trim() : "";
29
+ if (normalized === "") {
30
+ throw new SdkError("validation_error", `${label} must be non-empty`);
31
+ }
32
+ if ([...normalized].length > maxLength || hasUnicodeControlCharacter(normalized)) {
33
+ throw new SdkError("validation_error", `${label} is invalid or exceeds ${maxLength} characters`);
34
+ }
35
+ return normalized;
36
+ }
37
+
38
+ /**
39
+ * @param {string} label
40
+ * @param {unknown} value
41
+ * @param {number} maxLength
42
+ * @returns {string}
43
+ */
44
+ function boundedIdentity(label, value, maxLength) {
45
+ const normalized = boundedText(label, value, maxLength);
46
+ if (LOCATION_TEXT_PATTERN.test(normalized)) {
47
+ throw new SdkError("validation_error", `${label} is invalid or exceeds ${maxLength} characters`);
48
+ }
49
+ return normalized;
50
+ }
51
+
52
+ /**
53
+ * @param {string} label
54
+ * @param {unknown} value
55
+ * @param {number} maxLength
56
+ * @returns {string | undefined}
57
+ */
58
+ function optionalText(label, value, maxLength) {
59
+ return value === undefined ? undefined : boundedText(label, value, maxLength);
60
+ }
61
+
62
+ /**
63
+ * @param {string} label
64
+ * @param {unknown} value
65
+ * @param {number} maxLength
66
+ * @returns {string | undefined}
67
+ */
68
+ function optionalIdentity(label, value, maxLength) {
69
+ return value === undefined ? undefined : boundedIdentity(label, value, maxLength);
70
+ }
71
+
72
+ module.exports = { boundedIdentity, boundedText, optionalIdentity, optionalText, requireMachineName };