@logbrew/sdk 0.1.36 → 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.
@@ -0,0 +1,220 @@
1
+ "use strict";
2
+
3
+ const { SdkError } = require("./sdk-error.cjs");
4
+ const { isRecord } = require("./validation.cjs");
5
+ const {
6
+ SAFE_DEBUG_ID_PATTERN,
7
+ frameCoordinateFromText,
8
+ frameIdentity,
9
+ hasControlCharacter,
10
+ sanitizeFrameFilename
11
+ } = require("./issue-stack-text.cjs");
12
+
13
+ /**
14
+ * @import { IssueStackFrame, NativeStackArchitecture, NativeStackFrame } from "./index.cjs"
15
+ */
16
+
17
+ const MAX_ISSUE_STACK_FRAMES = 32;
18
+ const MAX_ISSUE_STACK_FUNCTION_LENGTH = 256;
19
+ const MAX_ISSUE_STACK_MODULE_LENGTH = 512;
20
+ const MAX_ISSUE_STACK_FILENAME_LENGTH = 2048;
21
+ const MAX_FRAME_COORDINATE = 2_147_483_647;
22
+ const NATIVE_FRAME_KEY_COUNT = 3;
23
+ const NO_FRAMES = 0;
24
+ const NATIVE_FRAME_KEYS = new Set(["imageUuid", "architecture", "instructionOffset"]);
25
+ 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;
26
+ const NATIVE_OFFSET_PATTERN = /^[0-9a-f]{16}$/u;
27
+ /** @type {ReadonlySet<string>} */
28
+ const NATIVE_ARCHITECTURES = new Set(["arm", "arm64", "arm64e", "x86", "x86_64"]);
29
+
30
+ /**
31
+ * @param {unknown} value
32
+ * @returns {value is NativeStackArchitecture}
33
+ */
34
+ function isNativeArchitecture(value) {
35
+ return typeof value === "string" && NATIVE_ARCHITECTURES.has(value);
36
+ }
37
+
38
+ /**
39
+ * @param {unknown} value
40
+ * @returns {string}
41
+ */
42
+ function requireFrameFilename(value) {
43
+ if (typeof value !== "string") {
44
+ throw new SdkError("validation_error", "issue stack frame filename is invalid");
45
+ }
46
+ const filename = sanitizeFrameFilename(value);
47
+ if (filename === "" || filename.length > MAX_ISSUE_STACK_FILENAME_LENGTH || hasControlCharacter(filename)) {
48
+ throw new SdkError("validation_error", "issue stack frame filename is invalid");
49
+ }
50
+ return filename;
51
+ }
52
+
53
+ /**
54
+ * @param {unknown} value
55
+ * @returns {number | undefined}
56
+ */
57
+ function boundedFrameCoordinate(value) {
58
+ const coordinate = frameCoordinateFromText(value);
59
+ return coordinate !== undefined && coordinate <= MAX_FRAME_COORDINATE ? coordinate : undefined;
60
+ }
61
+
62
+ /**
63
+ * @param {unknown} value
64
+ * @returns {string | undefined}
65
+ */
66
+ function optionalFrameDebugId(value) {
67
+ if (value === undefined) {
68
+ return undefined;
69
+ }
70
+ if (typeof value !== "string" || !SAFE_DEBUG_ID_PATTERN.test(value.trim())) {
71
+ throw new SdkError("validation_error", "issue stack frame debugId is invalid");
72
+ }
73
+ return value.trim().toLowerCase();
74
+ }
75
+
76
+ /**
77
+ * @param {unknown} value
78
+ * @returns {string | undefined}
79
+ */
80
+ function optionalFrameFunction(value) {
81
+ if (value === undefined) {
82
+ return undefined;
83
+ }
84
+ const functionName = typeof value === "string" ? frameIdentity(value, MAX_ISSUE_STACK_FUNCTION_LENGTH, false) : undefined;
85
+ if (functionName === undefined) {
86
+ throw new SdkError("validation_error", "issue stack frame function is invalid");
87
+ }
88
+ return functionName;
89
+ }
90
+
91
+ /**
92
+ * @param {unknown} value
93
+ * @returns {string | undefined}
94
+ */
95
+ function optionalFrameModule(value) {
96
+ if (value === undefined) {
97
+ return undefined;
98
+ }
99
+ const moduleName = typeof value === "string" ? frameIdentity(value, MAX_ISSUE_STACK_MODULE_LENGTH, true) : undefined;
100
+ if (moduleName === undefined) {
101
+ throw new SdkError("validation_error", "issue stack frame module is invalid");
102
+ }
103
+ return moduleName;
104
+ }
105
+
106
+ /**
107
+ * @param {unknown} value
108
+ * @returns {boolean | undefined}
109
+ */
110
+ function optionalFrameInApp(value) {
111
+ if (value === undefined || typeof value === "boolean") {
112
+ return value;
113
+ }
114
+ throw new SdkError("validation_error", "issue stack frame inApp is invalid");
115
+ }
116
+
117
+ /**
118
+ * @param {Readonly<Record<string, unknown>>} frame
119
+ * @returns {Pick<IssueStackFrame, "debugId" | "function" | "inApp" | "module">}
120
+ */
121
+ function optionalFrameFields(frame) {
122
+ const debugId = optionalFrameDebugId(frame["debugId"]);
123
+ const functionName = optionalFrameFunction(frame["function"]);
124
+ const moduleName = optionalFrameModule(frame["module"]);
125
+ const inApp = optionalFrameInApp(frame["inApp"]);
126
+ return {
127
+ ...(functionName === undefined ? {} : { function: functionName }),
128
+ ...(moduleName === undefined ? {} : { module: moduleName }),
129
+ ...(inApp === undefined ? {} : { inApp }),
130
+ ...(debugId === undefined ? {} : { debugId })
131
+ };
132
+ }
133
+
134
+ /**
135
+ * @param {unknown} frame
136
+ * @returns {IssueStackFrame}
137
+ */
138
+ function validateIssueStackFrame(frame) {
139
+ if (!isRecord(frame)) {
140
+ throw new SdkError("validation_error", "issue stack frame must be an object");
141
+ }
142
+ const filename = requireFrameFilename(frame["filename"]);
143
+ const line = boundedFrameCoordinate(frame["line"]);
144
+ const column = boundedFrameCoordinate(frame["column"]);
145
+ if (line === undefined || column === undefined) {
146
+ throw new SdkError("validation_error", "issue stack frame coordinates must be positive integers");
147
+ }
148
+ return {
149
+ filename,
150
+ line,
151
+
152
+ column,
153
+ ...optionalFrameFields(frame)
154
+ };
155
+ }
156
+
157
+ /**
158
+ * @param {unknown} stackFrames
159
+ * @returns {IssueStackFrame[] | undefined}
160
+ */
161
+ function validateIssueStackFrames(stackFrames) {
162
+ if (stackFrames === undefined) {
163
+ return undefined;
164
+ }
165
+ if (!Array.isArray(stackFrames) || stackFrames.length === NO_FRAMES || stackFrames.length > MAX_ISSUE_STACK_FRAMES) {
166
+ throw new SdkError("validation_error", `issue stackFrames must contain 1-${MAX_ISSUE_STACK_FRAMES} frames`);
167
+ }
168
+ /** @type {readonly unknown[]} */
169
+ const frames = stackFrames;
170
+ return frames.map((frame) => validateIssueStackFrame(frame));
171
+ }
172
+
173
+ /**
174
+ * @param {unknown} frame
175
+ * @returns {NativeStackFrame}
176
+ */
177
+ function validateNativeStackFrame(frame) {
178
+ const keys = isRecord(frame) ? Object.keys(frame) : [];
179
+ if (
180
+ !isRecord(frame)
181
+ || keys.length !== NATIVE_FRAME_KEY_COUNT
182
+ || !keys.every((key) => NATIVE_FRAME_KEYS.has(key))
183
+ ) {
184
+ throw new SdkError("validation_error", "issue native stack frame is invalid");
185
+ }
186
+ const { architecture, imageUuid, instructionOffset } = frame;
187
+ if (
188
+ typeof imageUuid !== "string"
189
+ || !NATIVE_IMAGE_UUID_PATTERN.test(imageUuid)
190
+ || !isNativeArchitecture(architecture)
191
+ || typeof instructionOffset !== "string"
192
+ || !NATIVE_OFFSET_PATTERN.test(instructionOffset)
193
+ ) {
194
+ throw new SdkError("validation_error", "issue native stack frame is invalid");
195
+ }
196
+ return {
197
+ imageUuid,
198
+
199
+ architecture,
200
+ instructionOffset
201
+ };
202
+ }
203
+
204
+ /**
205
+ * @param {unknown} frames
206
+ * @returns {NativeStackFrame[] | undefined}
207
+ */
208
+ function validateNativeStackFrames(frames) {
209
+ if (frames === undefined) {
210
+ return undefined;
211
+ }
212
+ if (!Array.isArray(frames) || frames.length === NO_FRAMES || frames.length > MAX_ISSUE_STACK_FRAMES) {
213
+ throw new SdkError("validation_error", `issue nativeStackFrames must contain 1-${MAX_ISSUE_STACK_FRAMES} frames`);
214
+ }
215
+ /** @type {readonly unknown[]} */
216
+ const list = frames;
217
+ return list.map((frame) => validateNativeStackFrame(frame));
218
+ }
219
+
220
+ module.exports = { validateIssueStackFrames, validateNativeStackFrames };
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+
3
+ const NOT_FOUND = -1;
4
+ const TEXT_START = 0;
5
+ const MARKER_WIDTH = 1;
6
+ const NO_OFFSET = 0;
7
+ const MIN_FRAME_COORDINATE = 1;
8
+ const LAST_CONTROL_CHARACTER = 31;
9
+ const DELETE_CHARACTER = 127;
10
+ const FILE_URL_PREFIX = "file://";
11
+ 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;
12
+ const FRAME_COORDINATE_PATTERN = /^(?:0|[1-9][0-9]*)$/u;
13
+ const TRAILING_SLASHES_PATTERN = /\/+$/u;
14
+ const LOCAL_ABSOLUTE_PATH_PATTERN = /(?:^|\s)(?:\/(?:Users|home|private|tmp|var|Volumes)\/|[A-Za-z]:[\\/])/u;
15
+
16
+ /**
17
+ * @param {string} value
18
+ * @returns {boolean}
19
+ */
20
+ function hasControlCharacter(value) {
21
+ return [...value].some((character) => {
22
+ const code = character.codePointAt(TEXT_START);
23
+ return code !== undefined && (code <= LAST_CONTROL_CHARACTER || code === DELETE_CHARACTER);
24
+ });
25
+ }
26
+
27
+ /**
28
+ * @param {string} value
29
+ * @returns {string}
30
+ */
31
+ function basename(value) {
32
+ const normalized = value.replaceAll("\\", "/").replace(TRAILING_SLASHES_PATTERN, "");
33
+ const marker = normalized.lastIndexOf("/");
34
+ return marker === NOT_FOUND ? normalized : normalized.slice(marker + MARKER_WIDTH);
35
+ }
36
+
37
+ /**
38
+ * @param {string} text
39
+ * @param {string} marker
40
+ * @returns {string}
41
+ */
42
+ function beforeFirst(text, marker) {
43
+ const index = text.indexOf(marker);
44
+ return index === NOT_FOUND ? text : text.slice(TEXT_START, index);
45
+ }
46
+
47
+ /**
48
+ * @param {string} value
49
+ * @returns {string}
50
+ */
51
+ function sanitizeFrameFilename(value) {
52
+ const trimmed = value.trim();
53
+ if (trimmed === "") {
54
+ return "";
55
+ }
56
+ const withoutQuery = beforeFirst(beforeFirst(trimmed, "?"), "#");
57
+ const filename = withoutQuery.startsWith(FILE_URL_PREFIX) ? withoutQuery.slice(FILE_URL_PREFIX.length) : withoutQuery;
58
+ return LOCAL_ABSOLUTE_PATH_PATTERN.test(filename) ? basename(filename) : filename;
59
+ }
60
+
61
+ /**
62
+ * @param {unknown} value
63
+ * @param {number} [offset]
64
+ * @returns {number | undefined}
65
+ */
66
+ function frameCoordinateFromText(value, offset = NO_OFFSET) {
67
+ const text = String(value);
68
+ if (!FRAME_COORDINATE_PATTERN.test(text)) {
69
+ return undefined;
70
+ }
71
+ const parsed = Number(text) + offset;
72
+ return Number.isSafeInteger(parsed) && parsed >= MIN_FRAME_COORDINATE ? parsed : undefined;
73
+ }
74
+
75
+ /**
76
+ * @param {string} value
77
+ * @param {number} maxLength
78
+ * @param {boolean} rejectLocationText
79
+ * @returns {string | undefined}
80
+ */
81
+ function frameIdentity(value, maxLength, rejectLocationText) {
82
+ const identity = value.trim();
83
+ if (
84
+ identity === ""
85
+ || [...identity].length > maxLength
86
+ || hasControlCharacter(identity)
87
+ || (rejectLocationText && (identity.includes("?") || identity.includes("#")))
88
+ ) {
89
+ return undefined;
90
+ }
91
+ return identity;
92
+ }
93
+
94
+ module.exports = {
95
+ SAFE_DEBUG_ID_PATTERN,
96
+ basename,
97
+ frameCoordinateFromText,
98
+ frameIdentity,
99
+ hasControlCharacter,
100
+ sanitizeFrameFilename
101
+ };