@logbrew/sdk 0.1.3 → 0.1.5

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,149 @@
1
+ "use strict";
2
+
3
+ const MAX_ISSUE_STACK_FRAMES = 32;
4
+ 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;
5
+ const LOCAL_ABSOLUTE_PATH_PATTERN = /^(?:\/(?:Users|home|private|tmp|var|Volumes)\/|[A-Za-z]:[\\/])/u;
6
+
7
+ function buildIssueStackHelpers({ SdkError }) {
8
+ function javascriptStackFrames(stack, debugIdMap) {
9
+ if (typeof stack !== "string" || stack.trim() === "") {
10
+ return [];
11
+ }
12
+ const frames = [];
13
+ for (const rawLine of stack.split(/\r?\n/u)) {
14
+ const parsed = parseJavaScriptStackFrame(rawLine);
15
+ if (parsed) {
16
+ const debugId = debugIdForFrame(parsed.filename, debugIdMap, SdkError);
17
+ frames.push({ ...parsed, ...(debugId ? { debugId } : {}) });
18
+ if (frames.length === MAX_ISSUE_STACK_FRAMES) {
19
+ break;
20
+ }
21
+ }
22
+ }
23
+ return frames;
24
+ }
25
+
26
+ function validateIssueStackFrames(stackFrames) {
27
+ if (stackFrames === undefined) {
28
+ return undefined;
29
+ }
30
+ if (!Array.isArray(stackFrames) || stackFrames.length === 0 || stackFrames.length > MAX_ISSUE_STACK_FRAMES) {
31
+ throw new SdkError("validation_error", `issue stackFrames must contain 1-${MAX_ISSUE_STACK_FRAMES} frames`);
32
+ }
33
+ return stackFrames.map((frame) => {
34
+ if (!frame || Array.isArray(frame) || typeof frame !== "object") {
35
+ throw new SdkError("validation_error", "issue stack frame must be an object");
36
+ }
37
+ if (typeof frame.filename !== "string") {
38
+ throw new SdkError("validation_error", "issue stack frame filename is invalid");
39
+ }
40
+ const filename = sanitizeFrameFilename(frame.filename);
41
+ if (!filename || filename.length > 2048 || hasControlCharacter(filename)) {
42
+ throw new SdkError("validation_error", "issue stack frame filename is invalid");
43
+ }
44
+ const line = positiveIntegerFromText(frame.line);
45
+ const column = positiveIntegerFromText(frame.column);
46
+ if (line === null || column === null || line > 2147483647 || column > 2147483647) {
47
+ throw new SdkError("validation_error", "issue stack frame coordinates must be positive integers");
48
+ }
49
+ const debugId = frame.debugId === undefined
50
+ ? undefined
51
+ : typeof frame.debugId === "string" && SAFE_DEBUG_ID_PATTERN.test(frame.debugId.trim())
52
+ ? frame.debugId.trim().toLowerCase()
53
+ : null;
54
+ if (debugId === null) {
55
+ throw new SdkError("validation_error", "issue stack frame debugId is invalid");
56
+ }
57
+ return { filename, line, column, ...(debugId ? { debugId } : {}) };
58
+ });
59
+ }
60
+
61
+ return { javascriptStackFrames, validateIssueStackFrames };
62
+ }
63
+
64
+ function parseJavaScriptStackFrame(rawLine) {
65
+ const line = typeof rawLine === "string" ? rawLine.trim() : "";
66
+ if (!line) {
67
+ return null;
68
+ }
69
+ let location = line;
70
+ if (location.startsWith("at ")) {
71
+ location = location.slice(3).trim();
72
+ if (location.endsWith(")") && location.includes("(")) {
73
+ location = location.slice(location.lastIndexOf("(") + 1, -1);
74
+ }
75
+ } else if (location.includes("@")) {
76
+ location = location.slice(location.lastIndexOf("@") + 1);
77
+ }
78
+ const parts = location.split(":");
79
+ if (parts.length < 3) {
80
+ return null;
81
+ }
82
+ const column = positiveIntegerFromText(parts.pop());
83
+ const lineNumber = positiveIntegerFromText(parts.pop());
84
+ const filename = sanitizeFrameFilename(parts.join(":"));
85
+ if (!filename || lineNumber === null || column === null) {
86
+ return null;
87
+ }
88
+ return { filename, line: lineNumber, column };
89
+ }
90
+
91
+ function positiveIntegerFromText(value) {
92
+ const text = String(value);
93
+ if (!/^[1-9][0-9]*$/u.test(text)) {
94
+ return null;
95
+ }
96
+ const parsed = Number(text);
97
+ return Number.isSafeInteger(parsed) ? parsed : null;
98
+ }
99
+
100
+ function sanitizeFrameFilename(value) {
101
+ let filename = String(value ?? "").trim();
102
+ if (!filename) {
103
+ return "";
104
+ }
105
+ filename = filename.split("?", 1)[0].split("#", 1)[0];
106
+ if (filename.startsWith("file://")) {
107
+ filename = filename.slice("file://".length);
108
+ }
109
+ if (LOCAL_ABSOLUTE_PATH_PATTERN.test(filename)) {
110
+ return basename(filename);
111
+ }
112
+ return filename;
113
+ }
114
+
115
+ function debugIdForFrame(filename, debugIdMap, SdkError) {
116
+ if (debugIdMap === undefined || debugIdMap === null) {
117
+ return null;
118
+ }
119
+ if (!debugIdMap || Array.isArray(debugIdMap) || typeof debugIdMap !== "object") {
120
+ throw new SdkError("validation_error", "debugIdMap must be an object");
121
+ }
122
+ const normalizedFilename = sanitizeFrameFilename(filename);
123
+ const aliases = new Set([normalizedFilename, basename(normalizedFilename)].filter(Boolean));
124
+ for (const [candidate, debugId] of Object.entries(debugIdMap)) {
125
+ if (typeof debugId !== "string" || !SAFE_DEBUG_ID_PATTERN.test(debugId.trim())) {
126
+ continue;
127
+ }
128
+ const normalizedCandidate = sanitizeFrameFilename(candidate);
129
+ if (aliases.has(normalizedCandidate) || aliases.has(basename(normalizedCandidate))) {
130
+ return debugId.trim().toLowerCase();
131
+ }
132
+ }
133
+ return null;
134
+ }
135
+
136
+ function basename(value) {
137
+ const normalized = String(value).replace(/\\/gu, "/").replace(/\/+$/u, "");
138
+ const marker = normalized.lastIndexOf("/");
139
+ return marker === -1 ? normalized : normalized.slice(marker + 1);
140
+ }
141
+
142
+ function hasControlCharacter(value) {
143
+ return Array.from(value).some((character) => {
144
+ const code = character.codePointAt(0);
145
+ return code !== undefined && (code <= 31 || code === 127);
146
+ });
147
+ }
148
+
149
+ module.exports = { buildIssueStackHelpers };
@@ -0,0 +1,95 @@
1
+ const ZERO_TRACE_ID = "00000000000000000000000000000000";
2
+ const ZERO_SPAN_ID = "0000000000000000";
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
+ }
20
+
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
+ }
41
+
42
+ function isMetadataValue(value) {
43
+ return (
44
+ value === null
45
+ || typeof value === "string"
46
+ || typeof value === "number" && Number.isFinite(value)
47
+ || typeof value === "boolean"
48
+ );
49
+ }
50
+
51
+ function traceMetadataFromLogContext(trace) {
52
+ const normalized = normalizeLogTraceContext(trace);
53
+ if (!normalized) {
54
+ return {};
55
+ }
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
+ };
62
+ }
63
+
64
+ function normalizeLogTraceContext(trace) {
65
+ if (!trace || Array.isArray(trace) || typeof trace !== "object") {
66
+ return undefined;
67
+ }
68
+ const traceId = normalizeHexId(trace.traceId, 32, ZERO_TRACE_ID);
69
+ const spanId = normalizeHexId(trace.spanId, 16, ZERO_SPAN_ID);
70
+ if (!traceId || !spanId) {
71
+ return undefined;
72
+ }
73
+ const parentSpanId = normalizeHexId(trace.parentSpanId, 16, ZERO_SPAN_ID);
74
+ return {
75
+ traceId,
76
+ spanId,
77
+ ...(parentSpanId !== undefined ? { parentSpanId } : {}),
78
+ ...(typeof trace.sampled === "boolean" ? { sampled: trace.sampled } : {})
79
+ };
80
+ }
81
+
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();
93
+ }
94
+
95
+ module.exports = { buildLogContextHelpers };