codex-webstrapper 0.1.0

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,669 @@
1
+ (() => {
2
+ const BRIDGE_PATH = "/__webstrapper/bridge";
3
+ const RECONNECT_MS = 1000;
4
+
5
+ const queuedEnvelopes = [];
6
+ const workerSubscribers = new Map();
7
+ const mainMessageHistory = [];
8
+ const CONTEXT_MENU_ROOT_ID = "__codex-webstrap-context-menu-root";
9
+ const CONTEXT_MENU_STYLE_ID = "__codex-webstrap-context-menu-style";
10
+
11
+ let ws = null;
12
+ let connected = false;
13
+ let reconnectTimer = null;
14
+ let activeContextMenu = null;
15
+ let lastContextMenuPoint = {
16
+ x: Math.floor(window.innerWidth / 2),
17
+ y: Math.floor(window.innerHeight / 2)
18
+ };
19
+
20
+ function isSentryIpcUrl(input) {
21
+ if (typeof input === "string") {
22
+ return input.startsWith("sentry-ipc://");
23
+ }
24
+ if (input && typeof input.url === "string") {
25
+ return input.url.startsWith("sentry-ipc://");
26
+ }
27
+ return false;
28
+ }
29
+
30
+ function resolveUrlString(input) {
31
+ if (typeof input === "string") {
32
+ return input;
33
+ }
34
+ if (input && typeof input.url === "string") {
35
+ return input.url;
36
+ }
37
+ return "";
38
+ }
39
+
40
+ function isStatsigRegistryUrl(input) {
41
+ const raw = resolveUrlString(input);
42
+ if (!raw) {
43
+ return false;
44
+ }
45
+ try {
46
+ const parsed = new URL(raw, window.location.href);
47
+ return parsed.hostname === "ab.chatgpt.com" && parsed.pathname.startsWith("/v1/rgstr");
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+
53
+ function installBrowserCompatibilityShims() {
54
+ if (typeof window.fetch === "function") {
55
+ const originalFetch = window.fetch.bind(window);
56
+ window.fetch = (input, init) => {
57
+ if (isSentryIpcUrl(input)) {
58
+ return Promise.resolve(new Response(null, { status: 204 }));
59
+ }
60
+ if (isStatsigRegistryUrl(input)) {
61
+ return Promise.resolve(
62
+ new Response(
63
+ JSON.stringify({
64
+ has_updates: false,
65
+ feature_gates: {},
66
+ dynamic_configs: {},
67
+ layer_configs: {},
68
+ time: Date.now()
69
+ }),
70
+ {
71
+ status: 200,
72
+ headers: {
73
+ "content-type": "application/json"
74
+ }
75
+ }
76
+ )
77
+ );
78
+ }
79
+ return originalFetch(input, init);
80
+ };
81
+ }
82
+
83
+ if (typeof navigator.sendBeacon === "function") {
84
+ const originalSendBeacon = navigator.sendBeacon.bind(navigator);
85
+ navigator.sendBeacon = (url, data) => {
86
+ if (typeof url === "string" && url.startsWith("sentry-ipc://")) {
87
+ return true;
88
+ }
89
+ if (isStatsigRegistryUrl(url)) {
90
+ return true;
91
+ }
92
+ return originalSendBeacon(url, data);
93
+ };
94
+ }
95
+
96
+ const fallbackCopyText = (text) => {
97
+ if (typeof document === "undefined" || !document.body) {
98
+ return false;
99
+ }
100
+ const value = typeof text === "string" ? text : String(text ?? "");
101
+ const textarea = document.createElement("textarea");
102
+ textarea.value = value;
103
+ textarea.setAttribute("readonly", "true");
104
+ textarea.style.position = "fixed";
105
+ textarea.style.opacity = "0";
106
+ textarea.style.pointerEvents = "none";
107
+ textarea.style.left = "-9999px";
108
+ textarea.style.top = "0";
109
+ document.body.appendChild(textarea);
110
+ textarea.select();
111
+ textarea.setSelectionRange(0, textarea.value.length);
112
+
113
+ let copied = false;
114
+ try {
115
+ copied = document.execCommand("copy");
116
+ } catch {
117
+ copied = false;
118
+ }
119
+ textarea.remove();
120
+ return copied;
121
+ };
122
+
123
+ const clipboard = navigator.clipboard;
124
+ if (clipboard && typeof clipboard.writeText === "function") {
125
+ const originalWriteText = clipboard.writeText.bind(clipboard);
126
+ try {
127
+ clipboard.writeText = async (text) => {
128
+ try {
129
+ return await originalWriteText(text);
130
+ } catch (error) {
131
+ if (fallbackCopyText(text)) {
132
+ return;
133
+ }
134
+ throw error;
135
+ }
136
+ };
137
+ } catch {
138
+ // Ignore immutable clipboard implementations.
139
+ }
140
+ }
141
+
142
+ if (clipboard && typeof clipboard.write === "function") {
143
+ const originalWrite = clipboard.write.bind(clipboard);
144
+ try {
145
+ clipboard.write = async (items) => {
146
+ try {
147
+ return await originalWrite(items);
148
+ } catch (error) {
149
+ try {
150
+ const firstItem = Array.isArray(items) ? items[0] : null;
151
+ if (!firstItem || typeof firstItem.getType !== "function") {
152
+ throw error;
153
+ }
154
+ const blob = await firstItem.getType("text/plain");
155
+ const text = await blob.text();
156
+ if (fallbackCopyText(text)) {
157
+ return;
158
+ }
159
+ } catch {
160
+ // Fall through to throw the original clipboard error.
161
+ }
162
+ throw error;
163
+ }
164
+ };
165
+ } catch {
166
+ // Ignore immutable clipboard implementations.
167
+ }
168
+ }
169
+
170
+ if (typeof console.error === "function") {
171
+ const originalConsoleError = console.error.bind(console);
172
+ console.error = (...args) => {
173
+ const text = args
174
+ .map((arg) => {
175
+ if (typeof arg === "string") {
176
+ return arg;
177
+ }
178
+ if (arg && typeof arg.message === "string") {
179
+ return arg.message;
180
+ }
181
+ return "";
182
+ })
183
+ .join(" ");
184
+
185
+ if (text.includes("Sentry SDK failed to establish connection with the Electron main process")) {
186
+ return;
187
+ }
188
+ if (text.includes("sentry-ipc://")) {
189
+ return;
190
+ }
191
+ if (text.includes("ab.chatgpt.com/v1/rgstr")) {
192
+ return;
193
+ }
194
+ if (text.includes("`DialogContent` requires a `DialogTitle`")) {
195
+ return;
196
+ }
197
+ originalConsoleError(...args);
198
+ };
199
+ }
200
+
201
+ if (typeof console.warn === "function") {
202
+ const originalConsoleWarn = console.warn.bind(console);
203
+ console.warn = (...args) => {
204
+ const text = args
205
+ .map((arg) => {
206
+ if (typeof arg === "string") {
207
+ return arg;
208
+ }
209
+ if (arg && typeof arg.message === "string") {
210
+ return arg.message;
211
+ }
212
+ return "";
213
+ })
214
+ .join(" ");
215
+
216
+ if (text.includes("Missing `Description` or `aria-describedby={undefined}` for {DialogContent}")) {
217
+ return;
218
+ }
219
+ originalConsoleWarn(...args);
220
+ };
221
+ }
222
+ }
223
+
224
+ function rememberContextMenuPosition(event) {
225
+ if (!event || !Number.isFinite(event.clientX) || !Number.isFinite(event.clientY)) {
226
+ return;
227
+ }
228
+ lastContextMenuPoint = {
229
+ x: event.clientX,
230
+ y: event.clientY
231
+ };
232
+ }
233
+
234
+ function ensureContextMenuStyles() {
235
+ if (document.getElementById(CONTEXT_MENU_STYLE_ID)) {
236
+ return;
237
+ }
238
+
239
+ const style = document.createElement("style");
240
+ style.id = CONTEXT_MENU_STYLE_ID;
241
+ style.textContent = `
242
+ #${CONTEXT_MENU_ROOT_ID} {
243
+ position: fixed;
244
+ inset: 0;
245
+ z-index: 2147483647;
246
+ }
247
+
248
+ #${CONTEXT_MENU_ROOT_ID} .cw-menu {
249
+ position: fixed;
250
+ min-width: 220px;
251
+ max-width: 320px;
252
+ border: 1px solid rgba(255, 255, 255, 0.16);
253
+ border-radius: 12px;
254
+ background: rgba(26, 27, 31, 0.97);
255
+ box-shadow: 0 14px 40px rgba(0, 0, 0, 0.35);
256
+ padding: 6px;
257
+ display: flex;
258
+ flex-direction: column;
259
+ gap: 2px;
260
+ color: #f4f4f5;
261
+ font-size: 14px;
262
+ line-height: 1.35;
263
+ backdrop-filter: blur(16px);
264
+ }
265
+
266
+ #${CONTEXT_MENU_ROOT_ID} .cw-item {
267
+ position: relative;
268
+ }
269
+
270
+ #${CONTEXT_MENU_ROOT_ID} .cw-item-btn {
271
+ width: 100%;
272
+ appearance: none;
273
+ border: 0;
274
+ border-radius: 8px;
275
+ background: transparent;
276
+ color: inherit;
277
+ padding: 9px 10px;
278
+ text-align: left;
279
+ display: flex;
280
+ align-items: center;
281
+ justify-content: space-between;
282
+ gap: 12px;
283
+ cursor: pointer;
284
+ }
285
+
286
+ #${CONTEXT_MENU_ROOT_ID} .cw-item-btn:hover {
287
+ background: rgba(255, 255, 255, 0.1);
288
+ }
289
+
290
+ #${CONTEXT_MENU_ROOT_ID} .cw-item-btn:disabled {
291
+ opacity: 0.45;
292
+ cursor: default;
293
+ }
294
+
295
+ #${CONTEXT_MENU_ROOT_ID} .cw-separator {
296
+ height: 1px;
297
+ margin: 5px 2px;
298
+ background: rgba(255, 255, 255, 0.14);
299
+ }
300
+
301
+ #${CONTEXT_MENU_ROOT_ID} .cw-item--submenu > .cw-menu {
302
+ display: none;
303
+ position: absolute;
304
+ top: -6px;
305
+ left: calc(100% + 4px);
306
+ }
307
+
308
+ #${CONTEXT_MENU_ROOT_ID} .cw-item--submenu:hover > .cw-menu,
309
+ #${CONTEXT_MENU_ROOT_ID} .cw-item--submenu:focus-within > .cw-menu {
310
+ display: flex;
311
+ }
312
+
313
+ #${CONTEXT_MENU_ROOT_ID} .cw-submenu-arrow {
314
+ opacity: 0.7;
315
+ }
316
+ `;
317
+ document.head.appendChild(style);
318
+ }
319
+
320
+ function normalizeContextMenuItems(items) {
321
+ if (!Array.isArray(items)) {
322
+ return [];
323
+ }
324
+ return items.filter((item) => item && typeof item === "object");
325
+ }
326
+
327
+ function contextMenuAnchor() {
328
+ const margin = 8;
329
+ const x = Number.isFinite(lastContextMenuPoint.x) ? lastContextMenuPoint.x : window.innerWidth / 2;
330
+ const y = Number.isFinite(lastContextMenuPoint.y) ? lastContextMenuPoint.y : window.innerHeight / 2;
331
+ return {
332
+ x: Math.min(Math.max(margin, x), Math.max(margin, window.innerWidth - margin)),
333
+ y: Math.min(Math.max(margin, y), Math.max(margin, window.innerHeight - margin))
334
+ };
335
+ }
336
+
337
+ function closeContextMenu(result) {
338
+ if (!activeContextMenu) {
339
+ return;
340
+ }
341
+
342
+ const current = activeContextMenu;
343
+ activeContextMenu = null;
344
+
345
+ window.removeEventListener("keydown", current.onKeyDown, true);
346
+ window.removeEventListener("resize", current.onWindowChange, true);
347
+
348
+ current.root.remove();
349
+ current.resolve(result ?? null);
350
+ }
351
+
352
+ function positionContextMenu(menu, anchor) {
353
+ const margin = 8;
354
+ const rect = menu.getBoundingClientRect();
355
+ const maxLeft = Math.max(margin, window.innerWidth - rect.width - margin);
356
+ const maxTop = Math.max(margin, window.innerHeight - rect.height - margin);
357
+ const left = Math.min(Math.max(margin, anchor.x), maxLeft);
358
+ const top = Math.min(Math.max(margin, anchor.y), maxTop);
359
+ menu.style.left = `${Math.round(left)}px`;
360
+ menu.style.top = `${Math.round(top)}px`;
361
+ }
362
+
363
+ function buildContextMenu(items, onSelect, nested = false) {
364
+ const menu = document.createElement("div");
365
+ menu.className = "cw-menu";
366
+ menu.setAttribute("role", "menu");
367
+ if (!nested) {
368
+ menu.tabIndex = -1;
369
+ }
370
+
371
+ items.forEach((item, index) => {
372
+ if (item.type === "separator") {
373
+ const separator = document.createElement("div");
374
+ separator.className = "cw-separator";
375
+ separator.setAttribute("role", "separator");
376
+ separator.dataset.index = String(index);
377
+ menu.appendChild(separator);
378
+ return;
379
+ }
380
+
381
+ const container = document.createElement("div");
382
+ container.className = "cw-item";
383
+ container.dataset.itemId = String(item.id ?? "");
384
+
385
+ const button = document.createElement("button");
386
+ button.type = "button";
387
+ button.className = "cw-item-btn";
388
+ button.setAttribute("role", "menuitem");
389
+ button.textContent = String(item.label ?? item.nativeLabel ?? item.id ?? "Menu item");
390
+
391
+ const submenuItems = normalizeContextMenuItems(item.submenu);
392
+ if (submenuItems.length > 0) {
393
+ container.classList.add("cw-item--submenu");
394
+ const arrow = document.createElement("span");
395
+ arrow.className = "cw-submenu-arrow";
396
+ arrow.textContent = ">";
397
+ button.appendChild(arrow);
398
+ container.appendChild(button);
399
+ container.appendChild(buildContextMenu(submenuItems, onSelect, true));
400
+ } else {
401
+ const enabled = item.enabled !== false;
402
+ if (!enabled) {
403
+ button.disabled = true;
404
+ } else if (item.id) {
405
+ button.addEventListener("click", (event) => {
406
+ event.preventDefault();
407
+ event.stopPropagation();
408
+ onSelect(String(item.id));
409
+ });
410
+ }
411
+ container.appendChild(button);
412
+ }
413
+
414
+ menu.appendChild(container);
415
+ });
416
+
417
+ return menu;
418
+ }
419
+
420
+ function showBrowserContextMenu(items) {
421
+ const normalizedItems = normalizeContextMenuItems(items);
422
+ if (normalizedItems.length === 0) {
423
+ return Promise.resolve(null);
424
+ }
425
+
426
+ ensureContextMenuStyles();
427
+ closeContextMenu(null);
428
+
429
+ return new Promise((resolve) => {
430
+ const root = document.createElement("div");
431
+ root.id = CONTEXT_MENU_ROOT_ID;
432
+
433
+ const menu = buildContextMenu(normalizedItems, (id) => {
434
+ closeContextMenu({ id });
435
+ });
436
+ root.appendChild(menu);
437
+
438
+ const onRootMouseDown = (event) => {
439
+ if (event.target === root && event.button === 0) {
440
+ event.preventDefault();
441
+ closeContextMenu(null);
442
+ }
443
+ };
444
+
445
+ const onKeyDown = (event) => {
446
+ if (event.key === "Escape") {
447
+ event.preventDefault();
448
+ closeContextMenu(null);
449
+ }
450
+ };
451
+
452
+ const onWindowChange = () => {
453
+ closeContextMenu(null);
454
+ };
455
+
456
+ root.addEventListener("mousedown", onRootMouseDown);
457
+ root.addEventListener("contextmenu", (event) => {
458
+ event.preventDefault();
459
+ });
460
+
461
+ activeContextMenu = {
462
+ root,
463
+ resolve,
464
+ onKeyDown,
465
+ onWindowChange
466
+ };
467
+
468
+ window.addEventListener("keydown", onKeyDown, true);
469
+ window.addEventListener("resize", onWindowChange, true);
470
+
471
+ document.body.appendChild(root);
472
+ const anchor = contextMenuAnchor();
473
+ positionContextMenu(menu, anchor);
474
+ menu.focus();
475
+ });
476
+ }
477
+
478
+ function bridgeUrl() {
479
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
480
+ return `${protocol}//${window.location.host}${BRIDGE_PATH}`;
481
+ }
482
+
483
+ function scheduleReconnect() {
484
+ if (reconnectTimer) {
485
+ return;
486
+ }
487
+ reconnectTimer = window.setTimeout(() => {
488
+ reconnectTimer = null;
489
+ connect();
490
+ }, RECONNECT_MS);
491
+ }
492
+
493
+ function sendEnvelope(envelope) {
494
+ if (connected && ws && ws.readyState === WebSocket.OPEN) {
495
+ ws.send(JSON.stringify(envelope));
496
+ return;
497
+ }
498
+ queuedEnvelopes.push(envelope);
499
+ }
500
+
501
+ function flushQueue() {
502
+ while (queuedEnvelopes.length > 0 && connected && ws && ws.readyState === WebSocket.OPEN) {
503
+ ws.send(JSON.stringify(queuedEnvelopes.shift()));
504
+ }
505
+ }
506
+
507
+ function handleMainMessage(payload) {
508
+ const record = {
509
+ ts: Date.now(),
510
+ type: payload?.type || null,
511
+ payload
512
+ };
513
+ mainMessageHistory.push(record);
514
+ if (mainMessageHistory.length > 200) {
515
+ mainMessageHistory.shift();
516
+ }
517
+ window.__codexWebstrapMainMessages = mainMessageHistory;
518
+
519
+ try {
520
+ window.dispatchEvent(new MessageEvent("message", { data: payload }));
521
+ } catch (error) {
522
+ console.error("codex-webstrap main-message dispatch failed", {
523
+ type: payload?.type || null,
524
+ error: String(error)
525
+ });
526
+ throw error;
527
+ }
528
+ }
529
+
530
+ function handleWorkerEvent(workerId, payload) {
531
+ const subscribers = workerSubscribers.get(workerId);
532
+ if (!subscribers) {
533
+ return;
534
+ }
535
+ subscribers.forEach((callback) => {
536
+ try {
537
+ callback(payload);
538
+ } catch (error) {
539
+ console.warn("worker subscriber callback failed", error);
540
+ }
541
+ });
542
+ }
543
+
544
+ function handleBridgeError(message) {
545
+ window.__codexWebstrapLastBridgeError = message;
546
+ const printable = {
547
+ code: message?.code || null,
548
+ message: message?.message || null
549
+ };
550
+ console.warn("codex-webstrap bridge error", printable);
551
+ }
552
+
553
+ function connect() {
554
+ if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
555
+ return;
556
+ }
557
+
558
+ ws = new WebSocket(bridgeUrl());
559
+
560
+ ws.addEventListener("open", () => {
561
+ connected = true;
562
+ flushQueue();
563
+ });
564
+
565
+ ws.addEventListener("close", () => {
566
+ connected = false;
567
+ scheduleReconnect();
568
+ });
569
+
570
+ ws.addEventListener("error", () => {
571
+ connected = false;
572
+ scheduleReconnect();
573
+ });
574
+
575
+ ws.addEventListener("message", (event) => {
576
+ let envelope;
577
+ try {
578
+ envelope = JSON.parse(String(event.data));
579
+ } catch {
580
+ return;
581
+ }
582
+
583
+ switch (envelope?.type) {
584
+ case "bridge-ready":
585
+ return;
586
+ case "main-message":
587
+ handleMainMessage(envelope.payload);
588
+ return;
589
+ case "worker-event":
590
+ handleWorkerEvent(envelope.workerId, envelope.payload);
591
+ return;
592
+ case "bridge-error":
593
+ handleBridgeError(envelope);
594
+ return;
595
+ default:
596
+ return;
597
+ }
598
+ });
599
+ }
600
+
601
+ function sendMessageFromView(payload) {
602
+ sendEnvelope({
603
+ type: "view-message",
604
+ payload
605
+ });
606
+ return Promise.resolve();
607
+ }
608
+
609
+ function sendWorkerMessageFromView(workerId, payload) {
610
+ sendEnvelope({
611
+ type: "worker-message",
612
+ workerId,
613
+ payload
614
+ });
615
+ return Promise.resolve();
616
+ }
617
+
618
+ function subscribeToWorkerMessages(workerId, callback) {
619
+ let subscribers = workerSubscribers.get(workerId);
620
+ if (!subscribers) {
621
+ subscribers = new Set();
622
+ workerSubscribers.set(workerId, subscribers);
623
+ }
624
+ subscribers.add(callback);
625
+
626
+ return () => {
627
+ const existing = workerSubscribers.get(workerId);
628
+ if (!existing) {
629
+ return;
630
+ }
631
+ existing.delete(callback);
632
+ if (existing.size === 0) {
633
+ workerSubscribers.delete(workerId);
634
+ }
635
+ };
636
+ }
637
+
638
+ const electronBridge = {
639
+ windowType: "electron",
640
+ sendMessageFromView,
641
+ sendWorkerMessageFromView,
642
+ subscribeToWorkerMessages,
643
+ showContextMenu: async (payload) => {
644
+ return showBrowserContextMenu(payload);
645
+ },
646
+ triggerSentryTestError: async () => {
647
+ await sendMessageFromView({ type: "trigger-sentry-test" });
648
+ },
649
+ getPathForFile: (file) => {
650
+ if (file && typeof file.path === "string") {
651
+ return file.path;
652
+ }
653
+ return null;
654
+ },
655
+ getSentryInitOptions: () => ({
656
+ dsn: null,
657
+ codexAppSessionId: null
658
+ }),
659
+ getAppSessionId: () => null,
660
+ getBuildFlavor: () => "prod"
661
+ };
662
+
663
+ window.codexWindowType = "electron";
664
+ window.electronBridge = electronBridge;
665
+ installBrowserCompatibilityShims();
666
+ window.addEventListener("contextmenu", rememberContextMenuPosition, true);
667
+
668
+ connect();
669
+ })();