@btraut/browser-bridge 0.3.0 → 0.4.2

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.
@@ -1,3 +1,97 @@
1
+ // packages/extension/src/error-sanitizer.ts
2
+ var TRAILING_PUNCTUATION_RE = /[.,;:!?]+$/;
3
+ var stripTrailingPunctuation = (value) => {
4
+ const match = value.match(TRAILING_PUNCTUATION_RE);
5
+ if (!match) {
6
+ return { core: value, trailing: "" };
7
+ }
8
+ const trailing = match[0] ?? "";
9
+ return { core: value.slice(0, -trailing.length), trailing };
10
+ };
11
+ var sanitizeUrlToken = (token) => {
12
+ const { core, trailing } = stripTrailingPunctuation(token);
13
+ try {
14
+ const parsed = new URL(core);
15
+ if (parsed.protocol === "file:") {
16
+ return `file://[redacted]${trailing}`;
17
+ }
18
+ if (parsed.origin && parsed.origin !== "null") {
19
+ return `${parsed.origin}${trailing}`;
20
+ }
21
+ if (parsed.protocol && parsed.host) {
22
+ return `${parsed.protocol}//${parsed.host}${trailing}`;
23
+ }
24
+ if (parsed.protocol) {
25
+ return `${parsed.protocol}${trailing}`;
26
+ }
27
+ } catch {
28
+ }
29
+ const fallback = core.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^/\s]+)/);
30
+ if (fallback?.[1] && fallback?.[2]) {
31
+ return `${fallback[1]}://${fallback[2]}${trailing}`;
32
+ }
33
+ return `[redacted url]${trailing}`;
34
+ };
35
+ var URL_TOKEN_RE = /\b(?:https?|wss?|chrome-extension|chrome|chrome-devtools|devtools|edge|brave|file):\/\/[^\s"'<>)}\]]+/gi;
36
+ var VIEW_SOURCE_RE = /\bview-source:(https?:\/\/[^\s"'<>)}\]]+)/gi;
37
+ var sanitizeUrls = (message) => {
38
+ let next = message.replace(VIEW_SOURCE_RE, (_match, inner) => {
39
+ return `view-source:${sanitizeUrlToken(inner)}`;
40
+ });
41
+ next = next.replace(URL_TOKEN_RE, (match) => sanitizeUrlToken(match));
42
+ return next;
43
+ };
44
+ var extractBasename = (value) => {
45
+ const trimmed = value.trim();
46
+ const lastSlash = Math.max(
47
+ trimmed.lastIndexOf("/"),
48
+ trimmed.lastIndexOf("\\")
49
+ );
50
+ if (lastSlash < 0) {
51
+ return trimmed;
52
+ }
53
+ return trimmed.slice(lastSlash + 1);
54
+ };
55
+ var sanitizeWindowsPaths = (message) => {
56
+ const windowsPathRe = /\b[A-Za-z]:\\(?:[^\s"')\]}]+\\)+[^\s"')\]}]+(?::\d+(?::\d+)?)?/g;
57
+ return message.replace(windowsPathRe, (match) => extractBasename(match));
58
+ };
59
+ var sanitizeUnixPaths = (message) => {
60
+ const unixPathRe = /\/(?:Users|home|var|private|tmp|opt|etc|Library|Applications|Volumes|System)(?:\/[^\s"')\]}]+)+(?::\d+(?::\d+)?)?/g;
61
+ return message.replace(unixPathRe, (match) => extractBasename(match));
62
+ };
63
+ var sanitizeChromeErrorMessage = (message) => {
64
+ let next = message;
65
+ next = sanitizeUrls(next);
66
+ next = sanitizeWindowsPaths(next);
67
+ next = sanitizeUnixPaths(next);
68
+ return next;
69
+ };
70
+ var sanitizeUnknown = (value) => {
71
+ if (typeof value === "string") {
72
+ return sanitizeChromeErrorMessage(value);
73
+ }
74
+ if (Array.isArray(value)) {
75
+ return value.map((entry) => sanitizeUnknown(entry));
76
+ }
77
+ if (!value || typeof value !== "object") {
78
+ return value;
79
+ }
80
+ const record = value;
81
+ const next = {};
82
+ for (const [key, entry] of Object.entries(record)) {
83
+ next[key] = sanitizeUnknown(entry);
84
+ }
85
+ return next;
86
+ };
87
+ var sanitizeDriveErrorInfo = (error) => {
88
+ return {
89
+ ...error,
90
+ message: sanitizeChromeErrorMessage(error.message),
91
+ ...error.details ? { details: sanitizeUnknown(error.details) } : {}
92
+ };
93
+ };
94
+
1
95
  // packages/extension/src/background.ts
2
96
  var DEFAULT_CORE_PORT = 3210;
3
97
  var CORE_PORT_KEY = "corePort";
@@ -48,6 +142,59 @@ var wrapChromeVoid = (invoker) => {
48
142
  });
49
143
  });
50
144
  };
145
+ var delayMs = async (ms) => {
146
+ if (!Number.isFinite(ms) || ms <= 0) {
147
+ return;
148
+ }
149
+ await new Promise((resolve) => {
150
+ self.setTimeout(resolve, ms);
151
+ });
152
+ };
153
+ var parseDataUrl = (dataUrl) => {
154
+ const match = /^data:([^;]+);base64,(.*)$/s.exec(dataUrl);
155
+ if (!match) {
156
+ return null;
157
+ }
158
+ return { mime: match[1] ?? "application/octet-stream", base64: match[2] };
159
+ };
160
+ var arrayBufferToBase64 = (buffer) => {
161
+ const bytes = new Uint8Array(buffer);
162
+ const chunkSize = 32768;
163
+ let binary = "";
164
+ for (let i = 0; i < bytes.length; i += chunkSize) {
165
+ const chunk = bytes.subarray(i, i + chunkSize);
166
+ binary += String.fromCharCode(...chunk);
167
+ }
168
+ return btoa(binary);
169
+ };
170
+ var renderDataUrlToFormat = async (dataUrl, format, quality) => {
171
+ const parsed = parseDataUrl(dataUrl);
172
+ if (!parsed) {
173
+ throw new Error("Invalid screenshot data URL.");
174
+ }
175
+ const blob = await (await fetch(dataUrl)).blob();
176
+ const bitmap = await createImageBitmap(blob);
177
+ try {
178
+ const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
179
+ const ctx = canvas.getContext("2d");
180
+ if (!ctx) {
181
+ throw new Error("Canvas context unavailable.");
182
+ }
183
+ ctx.drawImage(bitmap, 0, 0);
184
+ const mime = format === "jpeg" ? "image/jpeg" : format === "webp" ? "image/webp" : "image/png";
185
+ const q = typeof quality === "number" && Number.isFinite(quality) ? Math.max(0, Math.min(1, quality / 100)) : void 0;
186
+ const out = format === "png" ? await canvas.convertToBlob({ type: mime }) : await canvas.convertToBlob({ type: mime, quality: q });
187
+ const base64 = arrayBufferToBase64(await out.arrayBuffer());
188
+ return {
189
+ mime,
190
+ data_base64: base64,
191
+ width_px: bitmap.width,
192
+ height_px: bitmap.height
193
+ };
194
+ } finally {
195
+ bitmap.close();
196
+ }
197
+ };
51
198
  var readCorePort = async () => {
52
199
  return await new Promise((resolve) => {
53
200
  chrome.storage.local.get(
@@ -446,7 +593,7 @@ var DriveSocket = class {
446
593
  id: driveMessage.id,
447
594
  action: driveMessage.action,
448
595
  status: "error",
449
- error
596
+ error: sanitizeDriveErrorInfo(error)
450
597
  };
451
598
  this.sendMessage(response);
452
599
  };
@@ -703,6 +850,366 @@ var DriveSocket = class {
703
850
  }
704
851
  return;
705
852
  }
853
+ case "drive.screenshot": {
854
+ const params = message.params ?? {};
855
+ let tabId = params.tab_id;
856
+ if (tabId !== void 0 && typeof tabId !== "number") {
857
+ respondError({
858
+ code: "INVALID_ARGUMENT",
859
+ message: "tab_id must be a number when provided.",
860
+ retryable: false
861
+ });
862
+ return;
863
+ }
864
+ if (tabId === void 0) {
865
+ tabId = await getActiveTabId();
866
+ }
867
+ const mode = params.mode === "full_page" || params.mode === "viewport" || params.mode === "element" ? params.mode : "viewport";
868
+ const format = params.format === "jpeg" || params.format === "webp" ? params.format : "png";
869
+ const quality = typeof params.quality === "number" && Number.isFinite(params.quality) ? Math.max(0, Math.min(100, Math.floor(params.quality))) : void 0;
870
+ const tab = await getTab(tabId);
871
+ const url = tab.url;
872
+ if (typeof url === "string" && isRestrictedUrl(url)) {
873
+ respondError({
874
+ code: "NOT_SUPPORTED",
875
+ message: "Screenshots are not supported for this URL.",
876
+ retryable: false,
877
+ details: { url }
878
+ });
879
+ return;
880
+ }
881
+ const windowId = tab.windowId;
882
+ if (typeof windowId !== "number") {
883
+ respondError({
884
+ code: "TAB_NOT_FOUND",
885
+ message: "window_id missing for tab.",
886
+ retryable: false
887
+ });
888
+ return;
889
+ }
890
+ if (typeof OffscreenCanvas === "undefined") {
891
+ respondError({
892
+ code: "NOT_SUPPORTED",
893
+ message: "OffscreenCanvas is unavailable in this extension host.",
894
+ retryable: false
895
+ });
896
+ return;
897
+ }
898
+ const captureVisible = async () => {
899
+ return await wrapChromeCallback(
900
+ (callback) => chrome.tabs.captureVisibleTab(
901
+ windowId,
902
+ { format: "png" },
903
+ callback
904
+ )
905
+ );
906
+ };
907
+ const scrollTo = async (top, left) => {
908
+ const result = await sendToTab(tabId, "drive.scroll", {
909
+ top,
910
+ left,
911
+ behavior: "auto",
912
+ tab_id: tabId
913
+ });
914
+ if (!result.ok) {
915
+ throw new Error(result.error.message);
916
+ }
917
+ };
918
+ const getMetaInfo = async () => {
919
+ const meta = await sendToTab(
920
+ tabId,
921
+ "drive.screenshot_meta"
922
+ );
923
+ if (!meta.ok) {
924
+ throw new Error(meta.error.message);
925
+ }
926
+ const payload = meta.result;
927
+ if (!payload || typeof payload !== "object") {
928
+ throw new Error("Invalid screenshot metadata response.");
929
+ }
930
+ const record = payload;
931
+ const viewportHeight = record.viewportHeight;
932
+ const scrollHeight = record.scrollHeight;
933
+ const scrollY = record.scrollY;
934
+ const scrollX = record.scrollX;
935
+ const dpr = record.devicePixelRatio;
936
+ if (typeof viewportHeight !== "number" || !Number.isFinite(viewportHeight) || viewportHeight <= 0) {
937
+ throw new Error(
938
+ "viewportHeight missing from screenshot metadata."
939
+ );
940
+ }
941
+ if (typeof scrollHeight !== "number" || !Number.isFinite(scrollHeight) || scrollHeight <= 0) {
942
+ throw new Error("scrollHeight missing from screenshot metadata.");
943
+ }
944
+ const devicePixelRatio = typeof dpr === "number" && Number.isFinite(dpr) && dpr > 0 ? dpr : 1;
945
+ const fullHeightPx = Math.round(scrollHeight * devicePixelRatio);
946
+ const maxHeightPx = 5e4;
947
+ if (fullHeightPx > maxHeightPx) {
948
+ throw new Error(
949
+ `Page is too tall to capture (max ${maxHeightPx}px).`
950
+ );
951
+ }
952
+ const maxScrollY = Math.max(0, scrollHeight - viewportHeight);
953
+ const step = viewportHeight;
954
+ const positions = [];
955
+ for (let y = 0; y < maxScrollY; y += step) {
956
+ positions.push(y);
957
+ }
958
+ positions.push(maxScrollY);
959
+ const maxTiles = 200;
960
+ if (positions.length > maxTiles) {
961
+ throw new Error(
962
+ `Page requires too many tiles to capture (${positions.length}).`
963
+ );
964
+ }
965
+ return {
966
+ viewportHeight,
967
+ scrollHeight,
968
+ scrollY: typeof scrollY === "number" && Number.isFinite(scrollY) ? scrollY : 0,
969
+ scrollX: typeof scrollX === "number" && Number.isFinite(scrollX) ? scrollX : 0,
970
+ devicePixelRatio,
971
+ fullHeightPx,
972
+ positions
973
+ };
974
+ };
975
+ const canvasToResult = async (canvas) => {
976
+ const mime = format === "jpeg" ? "image/jpeg" : format === "webp" ? "image/webp" : "image/png";
977
+ const q = typeof quality === "number" && Number.isFinite(quality) ? Math.max(0, Math.min(1, quality / 100)) : void 0;
978
+ const blob = format === "png" ? await canvas.convertToBlob({ type: mime }) : await canvas.convertToBlob({ type: mime, quality: q });
979
+ const base64 = arrayBufferToBase64(await blob.arrayBuffer());
980
+ return {
981
+ mime,
982
+ data_base64: base64,
983
+ width_px: canvas.width,
984
+ height_px: canvas.height
985
+ };
986
+ };
987
+ const captureFullPageCanvas = async (metaInfo) => {
988
+ try {
989
+ await scrollTo(0, 0);
990
+ await delayMs(100);
991
+ const firstDataUrl = await captureVisible();
992
+ const firstBlob = await (await fetch(firstDataUrl)).blob();
993
+ const firstBitmap = await createImageBitmap(firstBlob);
994
+ const canvas = new OffscreenCanvas(
995
+ firstBitmap.width,
996
+ metaInfo.fullHeightPx
997
+ );
998
+ const ctx = canvas.getContext("2d");
999
+ if (!ctx) {
1000
+ firstBitmap.close();
1001
+ throw new Error("Canvas context unavailable.");
1002
+ }
1003
+ const drawTile = (bitmap, yCss) => {
1004
+ const destY = Math.round(yCss * metaInfo.devicePixelRatio);
1005
+ const remaining = metaInfo.fullHeightPx - destY;
1006
+ if (remaining <= 0) {
1007
+ return;
1008
+ }
1009
+ const drawHeight = Math.min(bitmap.height, remaining);
1010
+ ctx.drawImage(
1011
+ bitmap,
1012
+ 0,
1013
+ 0,
1014
+ bitmap.width,
1015
+ drawHeight,
1016
+ 0,
1017
+ destY,
1018
+ bitmap.width,
1019
+ drawHeight
1020
+ );
1021
+ };
1022
+ drawTile(firstBitmap, 0);
1023
+ firstBitmap.close();
1024
+ for (const y of metaInfo.positions.slice(1)) {
1025
+ await scrollTo(y, 0);
1026
+ await delayMs(100);
1027
+ const dataUrl = await captureVisible();
1028
+ const blob = await (await fetch(dataUrl)).blob();
1029
+ const bitmap = await createImageBitmap(blob);
1030
+ drawTile(bitmap, y);
1031
+ bitmap.close();
1032
+ }
1033
+ return canvas;
1034
+ } finally {
1035
+ try {
1036
+ await scrollTo(metaInfo.scrollY, metaInfo.scrollX);
1037
+ } catch {
1038
+ }
1039
+ }
1040
+ };
1041
+ if (mode === "viewport") {
1042
+ try {
1043
+ const dataUrl = await captureVisible();
1044
+ const rendered = await renderDataUrlToFormat(
1045
+ dataUrl,
1046
+ format,
1047
+ quality
1048
+ );
1049
+ respondOk(rendered);
1050
+ } catch (error) {
1051
+ respondError({
1052
+ code: "ARTIFACT_IO_ERROR",
1053
+ message: error instanceof Error ? error.message : "Failed to capture screenshot.",
1054
+ retryable: false
1055
+ });
1056
+ }
1057
+ return;
1058
+ }
1059
+ if (mode === "element") {
1060
+ const selector = params.selector;
1061
+ if (typeof selector !== "string" || selector.trim().length === 0) {
1062
+ respondError({
1063
+ code: "INVALID_ARGUMENT",
1064
+ message: "selector must be a non-empty string.",
1065
+ retryable: false
1066
+ });
1067
+ return;
1068
+ }
1069
+ let metaInfo;
1070
+ try {
1071
+ metaInfo = await getMetaInfo();
1072
+ } catch (error) {
1073
+ respondError({
1074
+ code: "EVALUATION_FAILED",
1075
+ message: error instanceof Error ? error.message : "Failed to read screenshot metadata.",
1076
+ retryable: false
1077
+ });
1078
+ return;
1079
+ }
1080
+ const element = await sendToTab(
1081
+ tabId,
1082
+ "drive.screenshot_element",
1083
+ { selector }
1084
+ );
1085
+ if (!element.ok) {
1086
+ respondError(element.error);
1087
+ return;
1088
+ }
1089
+ const payload = element.result;
1090
+ if (!payload || typeof payload !== "object") {
1091
+ respondError({
1092
+ code: "EVALUATION_FAILED",
1093
+ message: "Invalid element metadata response.",
1094
+ retryable: false
1095
+ });
1096
+ return;
1097
+ }
1098
+ const record = payload;
1099
+ const viewportLeft = record.viewportLeft;
1100
+ const viewportTop = record.viewportTop;
1101
+ const viewportWidth = record.viewportWidth;
1102
+ const viewportHeight = record.viewportHeight;
1103
+ const width = record.width;
1104
+ const height = record.height;
1105
+ const pageX = record.pageX;
1106
+ const pageY = record.pageY;
1107
+ const dpr = record.devicePixelRatio;
1108
+ const devicePixelRatio = typeof dpr === "number" && Number.isFinite(dpr) && dpr > 0 ? dpr : metaInfo.devicePixelRatio;
1109
+ if (typeof viewportLeft !== "number" || typeof viewportTop !== "number" || typeof viewportWidth !== "number" || typeof viewportHeight !== "number" || typeof width !== "number" || typeof height !== "number" || typeof pageX !== "number" || typeof pageY !== "number" || !Number.isFinite(viewportLeft) || !Number.isFinite(viewportTop) || !Number.isFinite(viewportWidth) || !Number.isFinite(viewportHeight) || !Number.isFinite(width) || !Number.isFinite(height) || !Number.isFinite(pageX) || !Number.isFinite(pageY)) {
1110
+ respondError({
1111
+ code: "EVALUATION_FAILED",
1112
+ message: "Invalid element bounding box metadata.",
1113
+ retryable: false
1114
+ });
1115
+ return;
1116
+ }
1117
+ const fitsInViewport = viewportLeft >= 0 && viewportTop >= 0 && viewportLeft + width <= viewportWidth && viewportTop + height <= viewportHeight;
1118
+ const cropW = Math.max(1, Math.round(width * devicePixelRatio));
1119
+ const cropH = Math.max(1, Math.round(height * devicePixelRatio));
1120
+ try {
1121
+ if (fitsInViewport) {
1122
+ const dataUrl = await captureVisible();
1123
+ const blob = await (await fetch(dataUrl)).blob();
1124
+ const bitmap = await createImageBitmap(blob);
1125
+ try {
1126
+ const cropX2 = Math.max(
1127
+ 0,
1128
+ Math.round(viewportLeft * devicePixelRatio)
1129
+ );
1130
+ const cropY2 = Math.max(
1131
+ 0,
1132
+ Math.round(viewportTop * devicePixelRatio)
1133
+ );
1134
+ const srcW2 = Math.min(cropW, bitmap.width - cropX2);
1135
+ const srcH2 = Math.min(cropH, bitmap.height - cropY2);
1136
+ if (srcW2 <= 0 || srcH2 <= 0) {
1137
+ throw new Error("Element is outside screenshot bounds.");
1138
+ }
1139
+ const cropCanvas2 = new OffscreenCanvas(srcW2, srcH2);
1140
+ const ctx2 = cropCanvas2.getContext("2d");
1141
+ if (!ctx2) {
1142
+ throw new Error("Canvas context unavailable.");
1143
+ }
1144
+ ctx2.drawImage(
1145
+ bitmap,
1146
+ cropX2,
1147
+ cropY2,
1148
+ srcW2,
1149
+ srcH2,
1150
+ 0,
1151
+ 0,
1152
+ srcW2,
1153
+ srcH2
1154
+ );
1155
+ respondOk(await canvasToResult(cropCanvas2));
1156
+ } finally {
1157
+ bitmap.close();
1158
+ }
1159
+ return;
1160
+ }
1161
+ const fullCanvas = await captureFullPageCanvas(metaInfo);
1162
+ const cropX = Math.max(0, Math.round(pageX * devicePixelRatio));
1163
+ const cropY = Math.max(0, Math.round(pageY * devicePixelRatio));
1164
+ const srcW = Math.min(cropW, fullCanvas.width - cropX);
1165
+ const srcH = Math.min(cropH, fullCanvas.height - cropY);
1166
+ if (srcW <= 0 || srcH <= 0) {
1167
+ throw new Error("Element is outside screenshot bounds.");
1168
+ }
1169
+ const cropCanvas = new OffscreenCanvas(srcW, srcH);
1170
+ const ctx = cropCanvas.getContext("2d");
1171
+ if (!ctx) {
1172
+ throw new Error("Canvas context unavailable.");
1173
+ }
1174
+ ctx.drawImage(
1175
+ fullCanvas,
1176
+ cropX,
1177
+ cropY,
1178
+ srcW,
1179
+ srcH,
1180
+ 0,
1181
+ 0,
1182
+ srcW,
1183
+ srcH
1184
+ );
1185
+ respondOk(await canvasToResult(cropCanvas));
1186
+ } catch (error) {
1187
+ respondError({
1188
+ code: "ARTIFACT_IO_ERROR",
1189
+ message: error instanceof Error ? error.message : "Failed to capture element screenshot.",
1190
+ retryable: false
1191
+ });
1192
+ } finally {
1193
+ try {
1194
+ await scrollTo(metaInfo.scrollY, metaInfo.scrollX);
1195
+ } catch {
1196
+ }
1197
+ }
1198
+ return;
1199
+ }
1200
+ try {
1201
+ const metaInfo = await getMetaInfo();
1202
+ const canvas = await captureFullPageCanvas(metaInfo);
1203
+ respondOk(await canvasToResult(canvas));
1204
+ } catch (error) {
1205
+ respondError({
1206
+ code: "ARTIFACT_IO_ERROR",
1207
+ message: error instanceof Error ? error.message : "Failed to capture full page screenshot.",
1208
+ retryable: false
1209
+ });
1210
+ }
1211
+ return;
1212
+ }
706
1213
  default:
707
1214
  respondError({
708
1215
  code: "NOT_IMPLEMENTED",
@@ -733,7 +1240,7 @@ var DriveSocket = class {
733
1240
  id: message.id,
734
1241
  action: message.action,
735
1242
  status: "error",
736
- error
1243
+ error: sanitizeDriveErrorInfo(error)
737
1244
  });
738
1245
  };
739
1246
  try {