sparkbun 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.
- package/bin/sparkbun.cjs +18 -0
- package/dist-linux-arm64/bsdiff +0 -0
- package/dist-linux-arm64/bspatch +0 -0
- package/dist-linux-arm64/libElectrobunCore.so +0 -0
- package/dist-linux-arm64/libNativeWrapper.so +0 -0
- package/dist-linux-arm64/libasar.so +0 -0
- package/dist-linux-x64/bsdiff +0 -0
- package/dist-linux-x64/bspatch +0 -0
- package/dist-linux-x64/libElectrobunCore.so +0 -0
- package/dist-linux-x64/libNativeWrapper.so +0 -0
- package/dist-linux-x64/libasar.so +0 -0
- package/dist-macos-arm64/bsdiff +0 -0
- package/dist-macos-arm64/bspatch +0 -0
- package/dist-macos-arm64/libElectrobunCore.dylib +0 -0
- package/dist-macos-arm64/libNativeWrapper.dylib +0 -0
- package/dist-macos-arm64/libasar.dylib +0 -0
- package/dist-macos-arm64/libwebgpu_dawn.dylib +0 -0
- package/dist-macos-arm64/preload-full.js +885 -0
- package/dist-macos-arm64/preload-sandboxed.js +111 -0
- package/dist-macos-arm64/process_helper +0 -0
- package/dist-win-x64/ElectrobunCore.dll +0 -0
- package/dist-win-x64/WebView2Loader.dll +0 -0
- package/dist-win-x64/bsdiff.exe +0 -0
- package/dist-win-x64/bspatch.exe +0 -0
- package/dist-win-x64/libNativeWrapper.dll +0 -0
- package/dist-win-x64/zig-asar/arm64/libasar.dll +0 -0
- package/dist-win-x64/zig-asar/x64/libasar.dll +0 -0
- package/package.json +47 -0
- package/scripts/build-and-upload-artifacts.js +207 -0
- package/scripts/gen-webgpu-ffi.mjs +162 -0
- package/scripts/install-windows-deps.ps1 +80 -0
- package/scripts/package-release.js +237 -0
- package/scripts/push-version.js +84 -0
- package/scripts/update-bun-version.ts +122 -0
- package/scripts/update-cef-version.ts +145 -0
- package/src/browser/builtinrpcSchema.ts +19 -0
- package/src/browser/global.d.ts +36 -0
- package/src/browser/index.ts +234 -0
- package/src/browser/webviewtag.ts +88 -0
- package/src/browser/wgputag.ts +48 -0
- package/src/bun/SparkBunConfig.ts +497 -0
- package/src/bun/__tests__/ffi-contract.test.ts +105 -0
- package/src/bun/core/ApplicationMenu.ts +70 -0
- package/src/bun/core/BrowserView.ts +416 -0
- package/src/bun/core/BrowserWindow.ts +396 -0
- package/src/bun/core/BuildConfig.ts +71 -0
- package/src/bun/core/ContextMenu.ts +75 -0
- package/src/bun/core/GpuWindow.ts +289 -0
- package/src/bun/core/Paths.ts +5 -0
- package/src/bun/core/Socket.ts +22 -0
- package/src/bun/core/Tray.ts +197 -0
- package/src/bun/core/Updater.ts +1131 -0
- package/src/bun/core/Utils.ts +487 -0
- package/src/bun/core/WGPUView.ts +167 -0
- package/src/bun/core/menuRoles.ts +181 -0
- package/src/bun/events/ApplicationEvents.ts +22 -0
- package/src/bun/events/event.ts +27 -0
- package/src/bun/events/eventEmitter.ts +45 -0
- package/src/bun/events/trayEvents.ts +11 -0
- package/src/bun/events/webviewEvents.ts +39 -0
- package/src/bun/events/windowEvents.ts +23 -0
- package/src/bun/index.ts +120 -0
- package/src/bun/preload/.generated/compiled.ts +2 -0
- package/src/bun/preload/build.ts +65 -0
- package/src/bun/preload/dragRegions.ts +41 -0
- package/src/bun/preload/encryption.ts +86 -0
- package/src/bun/preload/events.ts +171 -0
- package/src/bun/preload/globals.d.ts +45 -0
- package/src/bun/preload/index-sandboxed.ts +28 -0
- package/src/bun/preload/index.ts +77 -0
- package/src/bun/preload/internalRpc.ts +80 -0
- package/src/bun/preload/overlaySync.ts +107 -0
- package/src/bun/preload/webviewTag.ts +451 -0
- package/src/bun/preload/wgpuTag.ts +246 -0
- package/src/bun/proc/linux.md +43 -0
- package/src/bun/proc/native.ts +3253 -0
- package/src/bun/webGPU.ts +346 -0
- package/src/bun/webgpuAdapter.ts +3011 -0
- package/src/cli/bun.lockb +0 -0
- package/src/cli/index.ts +4653 -0
- package/src/cli/package-lock.json +81 -0
- package/src/cli/package.json +11 -0
- package/src/cli/templates/embedded.ts +2 -0
- package/src/core/build.zig +16 -0
- package/src/core/main.zig +3378 -0
- package/src/extractor/build.zig +22 -0
- package/src/installer/installer-template.ts +216 -0
- package/src/launcher/main.ts +221 -0
- package/src/native/build/libNativeWrapper.so +0 -0
- package/src/native/linux/build/nativeWrapper.o +0 -0
- package/src/native/linux/cef_loader.cpp +110 -0
- package/src/native/linux/cef_loader.h +28 -0
- package/src/native/linux/cef_process_helper_linux.cpp +160 -0
- package/src/native/linux/nativeWrapper.cpp +11768 -0
- package/src/native/macos/cef_process_helper_mac.cc +160 -0
- package/src/native/macos/nativeWrapper.mm +9172 -0
- package/src/native/shared/accelerator_parser.h +72 -0
- package/src/native/shared/app_paths.h +110 -0
- package/src/native/shared/asar.h +35 -0
- package/src/native/shared/cache_migration.h +244 -0
- package/src/native/shared/callbacks.h +57 -0
- package/src/native/shared/cef_response_filter.h +189 -0
- package/src/native/shared/chromium_flags.h +181 -0
- package/src/native/shared/config.h +66 -0
- package/src/native/shared/download_event.h +197 -0
- package/src/native/shared/ffi_helpers.h +139 -0
- package/src/native/shared/glob_match.h +59 -0
- package/src/native/shared/json_menu_parser.h +223 -0
- package/src/native/shared/mime_types.h +101 -0
- package/src/native/shared/navigation_rules.h +98 -0
- package/src/native/shared/partition_context.h +137 -0
- package/src/native/shared/pending_resize_queue.h +45 -0
- package/src/native/shared/permissions.h +118 -0
- package/src/native/shared/permissions_cef.h +74 -0
- package/src/native/shared/preload_script.h +71 -0
- package/src/native/shared/shutdown_guard.h +134 -0
- package/src/native/shared/thread_safe_map.h +138 -0
- package/src/native/shared/webview_storage.h +91 -0
- package/src/native/win/cef_process_helper_win.cpp +143 -0
- package/src/native/win/dcomp_compositor.h +352 -0
- package/src/native/win/nativeWrapper.cpp +12434 -0
- package/src/npmbin/index.js +34 -0
- package/src/shared/bun-version.ts +3 -0
- package/src/shared/cef-version.ts +5 -0
- package/src/shared/naming.test.ts +327 -0
- package/src/shared/naming.ts +188 -0
- package/src/shared/platform.ts +48 -0
- package/src/shared/rpc.ts +541 -0
- package/src/shared/sparkbun-version.ts +2 -0
- package/src/types/three.d.ts +1 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Encryption/Decryption for secure RPC
|
|
2
|
+
// Uses per-webview secret key set in window.__sparkbunSecretKeyBytes
|
|
3
|
+
|
|
4
|
+
import "./globals.d.ts";
|
|
5
|
+
|
|
6
|
+
function base64ToUint8Array(base64: string): Uint8Array {
|
|
7
|
+
return new Uint8Array(
|
|
8
|
+
atob(base64)
|
|
9
|
+
.split("")
|
|
10
|
+
.map((char) => char.charCodeAt(0)),
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function uint8ArrayToBase64(uint8Array: Uint8Array): string {
|
|
15
|
+
let binary = "";
|
|
16
|
+
for (let i = 0; i < uint8Array.length; i++) {
|
|
17
|
+
binary += String.fromCharCode(uint8Array[i]!);
|
|
18
|
+
}
|
|
19
|
+
return btoa(binary);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function generateKeyFromBytes(rawKey: Uint8Array): Promise<CryptoKey> {
|
|
23
|
+
return await window.crypto.subtle.importKey(
|
|
24
|
+
"raw",
|
|
25
|
+
rawKey as unknown as ArrayBuffer,
|
|
26
|
+
{ name: "AES-GCM" },
|
|
27
|
+
true,
|
|
28
|
+
["encrypt", "decrypt"],
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function initEncryption(): Promise<void> {
|
|
33
|
+
const secretKey = await generateKeyFromBytes(
|
|
34
|
+
new Uint8Array(window.__sparkbunSecretKeyBytes),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const encryptString = async (
|
|
38
|
+
plaintext: string,
|
|
39
|
+
): Promise<{ encryptedData: string; iv: string; tag: string }> => {
|
|
40
|
+
const encoder = new TextEncoder();
|
|
41
|
+
const encodedText = encoder.encode(plaintext);
|
|
42
|
+
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
|
43
|
+
const encryptedBuffer = await window.crypto.subtle.encrypt(
|
|
44
|
+
{ name: "AES-GCM", iv },
|
|
45
|
+
secretKey,
|
|
46
|
+
encodedText,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// Split the tag (last 16 bytes) from the ciphertext
|
|
50
|
+
const encryptedData = new Uint8Array(encryptedBuffer.slice(0, -16));
|
|
51
|
+
const tag = new Uint8Array(encryptedBuffer.slice(-16));
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
encryptedData: uint8ArrayToBase64(encryptedData),
|
|
55
|
+
iv: uint8ArrayToBase64(iv),
|
|
56
|
+
tag: uint8ArrayToBase64(tag),
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const decryptString = async (
|
|
61
|
+
encryptedDataB64: string,
|
|
62
|
+
ivB64: string,
|
|
63
|
+
tagB64: string,
|
|
64
|
+
): Promise<string> => {
|
|
65
|
+
const encryptedData = base64ToUint8Array(encryptedDataB64);
|
|
66
|
+
const iv = base64ToUint8Array(ivB64);
|
|
67
|
+
const tag = base64ToUint8Array(tagB64);
|
|
68
|
+
|
|
69
|
+
// Combine encrypted data and tag to match the format expected by SubtleCrypto
|
|
70
|
+
const combinedData = new Uint8Array(encryptedData.length + tag.length);
|
|
71
|
+
combinedData.set(encryptedData);
|
|
72
|
+
combinedData.set(tag, encryptedData.length);
|
|
73
|
+
|
|
74
|
+
const decryptedBuffer = await window.crypto.subtle.decrypt(
|
|
75
|
+
{ name: "AES-GCM", iv: iv as unknown as ArrayBuffer },
|
|
76
|
+
secretKey,
|
|
77
|
+
combinedData as unknown as ArrayBuffer,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const decoder = new TextDecoder();
|
|
81
|
+
return decoder.decode(decryptedBuffer);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
window.__sparkbun_encrypt = encryptString;
|
|
85
|
+
window.__sparkbun_decrypt = decryptString;
|
|
86
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Shared Event Emission for webview lifecycle events
|
|
2
|
+
// Uses __sparkbunEventBridge which is available on ALL webviews (including sandboxed)
|
|
3
|
+
// Falls back to __sparkbunInternalBridge for backwards compatibility until native code
|
|
4
|
+
// is updated to include the eventBridge handler
|
|
5
|
+
// This is a one-way channel for emitting events to native/bun - no RPC capability
|
|
6
|
+
|
|
7
|
+
import "./globals.d.ts";
|
|
8
|
+
|
|
9
|
+
// Emit a webview event to native code
|
|
10
|
+
export function emitWebviewEvent(eventName: string, detail: string) {
|
|
11
|
+
// setTimeout works around a race condition with Bun FFI
|
|
12
|
+
setTimeout(() => {
|
|
13
|
+
// Prefer eventBridge (available on all webviews), fall back to internalBridge
|
|
14
|
+
// (for backwards compatibility until native code adds eventBridge handler)
|
|
15
|
+
const bridge =
|
|
16
|
+
window.__sparkbunEventBridge || window.__sparkbunInternalBridge;
|
|
17
|
+
bridge?.postMessage(
|
|
18
|
+
JSON.stringify({
|
|
19
|
+
id: "webviewEvent",
|
|
20
|
+
type: "message",
|
|
21
|
+
payload: {
|
|
22
|
+
id: window.__sparkbunWebviewId,
|
|
23
|
+
eventName,
|
|
24
|
+
detail,
|
|
25
|
+
},
|
|
26
|
+
}),
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Set up standard lifecycle event listeners
|
|
32
|
+
export function initLifecycleEvents() {
|
|
33
|
+
// Emit dom-ready when page loads (top-level window only)
|
|
34
|
+
window.addEventListener("load", () => {
|
|
35
|
+
if (window === window.top) {
|
|
36
|
+
emitWebviewEvent("dom-ready", document.location.href);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Track in-page navigation
|
|
41
|
+
window.addEventListener("popstate", () => {
|
|
42
|
+
emitWebviewEvent("did-navigate-in-page", window.location.href);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
window.addEventListener("hashchange", () => {
|
|
46
|
+
emitWebviewEvent("did-navigate-in-page", window.location.href);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Track cmd key state for SPA navigation detection
|
|
51
|
+
let cmdKeyHeld = false;
|
|
52
|
+
let cmdKeyTimestamp = 0;
|
|
53
|
+
const CMD_KEY_THRESHOLD_MS = 500;
|
|
54
|
+
|
|
55
|
+
export function isCmdHeld(): boolean {
|
|
56
|
+
if (cmdKeyHeld) return true;
|
|
57
|
+
return (
|
|
58
|
+
Date.now() - cmdKeyTimestamp < CMD_KEY_THRESHOLD_MS && cmdKeyTimestamp > 0
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Set up cmd+click detection for opening links in new windows
|
|
63
|
+
export function initCmdClickHandling() {
|
|
64
|
+
window.addEventListener(
|
|
65
|
+
"keydown",
|
|
66
|
+
(event) => {
|
|
67
|
+
if (event.key === "Meta" || event.metaKey) {
|
|
68
|
+
cmdKeyHeld = true;
|
|
69
|
+
cmdKeyTimestamp = Date.now();
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
true,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
window.addEventListener(
|
|
76
|
+
"keyup",
|
|
77
|
+
(event) => {
|
|
78
|
+
if (event.key === "Meta") {
|
|
79
|
+
cmdKeyHeld = false;
|
|
80
|
+
cmdKeyTimestamp = Date.now();
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
true,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
window.addEventListener("blur", () => {
|
|
87
|
+
cmdKeyHeld = false;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Intercept cmd+clicks on anchors before SPA frameworks can handle them
|
|
91
|
+
window.addEventListener(
|
|
92
|
+
"click",
|
|
93
|
+
(event) => {
|
|
94
|
+
if (event.metaKey || event.ctrlKey) {
|
|
95
|
+
const anchor = (event.target as HTMLElement)?.closest?.("a");
|
|
96
|
+
if (anchor && (anchor as HTMLAnchorElement).href) {
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
event.stopPropagation();
|
|
99
|
+
event.stopImmediatePropagation();
|
|
100
|
+
emitWebviewEvent(
|
|
101
|
+
"new-window-open",
|
|
102
|
+
JSON.stringify({
|
|
103
|
+
url: (anchor as HTMLAnchorElement).href,
|
|
104
|
+
isCmdClick: true,
|
|
105
|
+
isSPANavigation: false,
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
true,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Intercept SPA navigation (history.pushState/replaceState) when cmd is held
|
|
116
|
+
export function initSPANavigationInterception() {
|
|
117
|
+
const originalPushState = history.pushState;
|
|
118
|
+
const originalReplaceState = history.replaceState;
|
|
119
|
+
|
|
120
|
+
history.pushState = function (
|
|
121
|
+
state: unknown,
|
|
122
|
+
title: string,
|
|
123
|
+
url?: string | URL | null,
|
|
124
|
+
) {
|
|
125
|
+
if (isCmdHeld() && url) {
|
|
126
|
+
const resolvedUrl = new URL(String(url), window.location.href).href;
|
|
127
|
+
emitWebviewEvent(
|
|
128
|
+
"new-window-open",
|
|
129
|
+
JSON.stringify({
|
|
130
|
+
url: resolvedUrl,
|
|
131
|
+
isCmdClick: true,
|
|
132
|
+
isSPANavigation: true,
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
return originalPushState.apply(this, [state, title, url]);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
history.replaceState = function (
|
|
141
|
+
state: unknown,
|
|
142
|
+
title: string,
|
|
143
|
+
url?: string | URL | null,
|
|
144
|
+
) {
|
|
145
|
+
if (isCmdHeld() && url) {
|
|
146
|
+
const resolvedUrl = new URL(String(url), window.location.href).href;
|
|
147
|
+
emitWebviewEvent(
|
|
148
|
+
"new-window-open",
|
|
149
|
+
JSON.stringify({
|
|
150
|
+
url: resolvedUrl,
|
|
151
|
+
isCmdClick: true,
|
|
152
|
+
isSPANavigation: true,
|
|
153
|
+
}),
|
|
154
|
+
);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
return originalReplaceState.apply(this, [state, title, url]);
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Prevent overscroll bounce effect
|
|
162
|
+
export function initOverscrollPrevention() {
|
|
163
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
164
|
+
const style = document.createElement("style");
|
|
165
|
+
style.type = "text/css";
|
|
166
|
+
style.appendChild(
|
|
167
|
+
document.createTextNode("html, body { overscroll-behavior: none; }"),
|
|
168
|
+
);
|
|
169
|
+
document.head.appendChild(style);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Type declarations for SparkBun preload globals
|
|
2
|
+
// These are set dynamically per-webview before the preload script runs
|
|
3
|
+
|
|
4
|
+
declare global {
|
|
5
|
+
interface Window {
|
|
6
|
+
__sparkbunWebviewId: number;
|
|
7
|
+
__sparkbunWindowId: number;
|
|
8
|
+
__sparkbunRpcSocketPort: number;
|
|
9
|
+
__sparkbunHostSocketPort?: number;
|
|
10
|
+
__sparkbunSecretKeyBytes: number[];
|
|
11
|
+
// Event-only bridge (all webviews, including sandboxed)
|
|
12
|
+
__sparkbunEventBridge?: {
|
|
13
|
+
postMessage: (message: string) => void;
|
|
14
|
+
};
|
|
15
|
+
// Internal RPC bridge (trusted webviews only)
|
|
16
|
+
__sparkbunInternalBridge?: {
|
|
17
|
+
postMessage: (message: string) => void;
|
|
18
|
+
};
|
|
19
|
+
// User RPC bridge (trusted webviews only)
|
|
20
|
+
__sparkbunHostBridge?: {
|
|
21
|
+
postMessage: (message: string) => void;
|
|
22
|
+
};
|
|
23
|
+
__sparkbunBunBridge?: {
|
|
24
|
+
postMessage: (message: string) => void;
|
|
25
|
+
};
|
|
26
|
+
__sparkbun_encrypt: (
|
|
27
|
+
plaintext: string,
|
|
28
|
+
) => Promise<{ encryptedData: string; iv: string; tag: string }>;
|
|
29
|
+
__sparkbun_decrypt: (
|
|
30
|
+
encryptedData: string,
|
|
31
|
+
iv: string,
|
|
32
|
+
tag: string,
|
|
33
|
+
) => Promise<string>;
|
|
34
|
+
__sparkbunSendToHost: (message: unknown) => void;
|
|
35
|
+
__sparkbunPendingHostMessages?: unknown[];
|
|
36
|
+
__sparkbun: {
|
|
37
|
+
receiveMessageFromHost: (msg: unknown) => void;
|
|
38
|
+
receiveInternalMessageFromHost: (msg: unknown) => void;
|
|
39
|
+
receiveMessageFromBun: (msg: unknown) => void;
|
|
40
|
+
receiveInternalMessageFromBun: (msg: unknown) => void;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// SparkBun Sandboxed Preload Script (for untrusted webviews)
|
|
2
|
+
// This is compiled to JS and injected into webviews that ARE sandboxed
|
|
3
|
+
//
|
|
4
|
+
// Minimal functionality for security: NO RPC, NO encryption, NO webview tags
|
|
5
|
+
// Only includes: lifecycle events, cmd+click handling, overscroll prevention
|
|
6
|
+
//
|
|
7
|
+
// Before this script runs, the following must be set:
|
|
8
|
+
// - window.__sparkbunWebviewId
|
|
9
|
+
// - window.__sparkbunWindowId
|
|
10
|
+
// - window.__sparkbunEventBridge (event emission only)
|
|
11
|
+
|
|
12
|
+
import "./globals.d.ts";
|
|
13
|
+
import {
|
|
14
|
+
initLifecycleEvents,
|
|
15
|
+
initCmdClickHandling,
|
|
16
|
+
initSPANavigationInterception,
|
|
17
|
+
initOverscrollPrevention,
|
|
18
|
+
} from "./events";
|
|
19
|
+
|
|
20
|
+
// Initialize minimal features for sandboxed webviews
|
|
21
|
+
// No RPC handlers - sandboxed webviews cannot communicate with Bun
|
|
22
|
+
// No drag regions - sandboxed content shouldn't control window movement
|
|
23
|
+
// No webview tags - sandboxed content cannot create OOPIFs
|
|
24
|
+
|
|
25
|
+
initLifecycleEvents();
|
|
26
|
+
initCmdClickHandling();
|
|
27
|
+
initSPANavigationInterception();
|
|
28
|
+
initOverscrollPrevention();
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// SparkBun Full Preload Script (for trusted webviews)
|
|
2
|
+
// This is compiled to JS and injected into webviews that are NOT sandboxed
|
|
3
|
+
//
|
|
4
|
+
// Includes: RPC, encryption, drag regions, webview tags, lifecycle events
|
|
5
|
+
//
|
|
6
|
+
// Before this script runs, the following must be set:
|
|
7
|
+
// - window.__sparkbunWebviewId
|
|
8
|
+
// - window.__sparkbunWindowId
|
|
9
|
+
// - window.__sparkbunRpcSocketPort
|
|
10
|
+
// - window.__sparkbunHostSocketPort (optional alias)
|
|
11
|
+
// - window.__sparkbunSecretKeyBytes
|
|
12
|
+
// - window.__sparkbunEventBridge (event emission - all webviews)
|
|
13
|
+
// - window.__sparkbunInternalBridge (internal RPC - trusted only)
|
|
14
|
+
// - window.__sparkbunHostBridge (user RPC - trusted only)
|
|
15
|
+
// - window.__sparkbunBunBridge (legacy alias)
|
|
16
|
+
|
|
17
|
+
import "./globals.d.ts";
|
|
18
|
+
import { initEncryption } from "./encryption";
|
|
19
|
+
import { handleResponse } from "./internalRpc";
|
|
20
|
+
import { initDragRegions } from "./dragRegions";
|
|
21
|
+
import { initWebviewTag } from "./webviewTag";
|
|
22
|
+
import { initWgpuTag } from "./wgpuTag";
|
|
23
|
+
import {
|
|
24
|
+
emitWebviewEvent,
|
|
25
|
+
initLifecycleEvents,
|
|
26
|
+
initCmdClickHandling,
|
|
27
|
+
initSPANavigationInterception,
|
|
28
|
+
initOverscrollPrevention,
|
|
29
|
+
} from "./events";
|
|
30
|
+
|
|
31
|
+
// Initialize encryption first (async)
|
|
32
|
+
initEncryption().catch((err) =>
|
|
33
|
+
console.error("Failed to initialize encryption:", err),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// Set up global handlers for bun to call back
|
|
37
|
+
// Wrapper to satisfy the (msg: unknown) => void type
|
|
38
|
+
const internalMessageHandler = (msg: unknown) => {
|
|
39
|
+
handleResponse(msg as { type: string; id: string; success: boolean; payload: unknown });
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const defaultUserMessageHandler = (msg: unknown) => {
|
|
43
|
+
// Buffer user RPC packets that arrive before the page-specific Electroview
|
|
44
|
+
// instance installs the real handler.
|
|
45
|
+
if (!window.__sparkbunPendingHostMessages) {
|
|
46
|
+
window.__sparkbunPendingHostMessages = [];
|
|
47
|
+
}
|
|
48
|
+
window.__sparkbunPendingHostMessages.push(msg);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (!window.__sparkbun) {
|
|
52
|
+
window.__sparkbun = {
|
|
53
|
+
receiveInternalMessageFromHost: internalMessageHandler,
|
|
54
|
+
receiveMessageFromHost: defaultUserMessageHandler,
|
|
55
|
+
receiveInternalMessageFromBun: internalMessageHandler,
|
|
56
|
+
receiveMessageFromBun: defaultUserMessageHandler,
|
|
57
|
+
};
|
|
58
|
+
} else {
|
|
59
|
+
window.__sparkbun.receiveInternalMessageFromHost = internalMessageHandler;
|
|
60
|
+
window.__sparkbun.receiveMessageFromHost = defaultUserMessageHandler;
|
|
61
|
+
window.__sparkbun.receiveInternalMessageFromBun = internalMessageHandler;
|
|
62
|
+
window.__sparkbun.receiveMessageFromBun = defaultUserMessageHandler;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Allow preload scripts to send custom messages to the host webview
|
|
66
|
+
window.__sparkbunSendToHost = (message: unknown) => {
|
|
67
|
+
emitWebviewEvent("host-message", JSON.stringify(message));
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Initialize all features
|
|
71
|
+
initLifecycleEvents();
|
|
72
|
+
initCmdClickHandling();
|
|
73
|
+
initSPANavigationInterception();
|
|
74
|
+
initOverscrollPrevention();
|
|
75
|
+
initDragRegions();
|
|
76
|
+
initWebviewTag();
|
|
77
|
+
initWgpuTag();
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Internal RPC System for webview tags, drag regions, etc.
|
|
2
|
+
// Communicates with Bun via __sparkbunInternalBridge
|
|
3
|
+
|
|
4
|
+
import "./globals.d.ts";
|
|
5
|
+
|
|
6
|
+
interface PendingRequest {
|
|
7
|
+
resolve: (value: unknown) => void;
|
|
8
|
+
reject: (reason: unknown) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const pendingRequests: Record<string, PendingRequest> = {};
|
|
12
|
+
let requestId = 0;
|
|
13
|
+
let isProcessingQueue = false;
|
|
14
|
+
const sendQueue: string[] = [];
|
|
15
|
+
|
|
16
|
+
function processQueue() {
|
|
17
|
+
if (isProcessingQueue) {
|
|
18
|
+
setTimeout(processQueue);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (sendQueue.length === 0) return;
|
|
22
|
+
|
|
23
|
+
isProcessingQueue = true;
|
|
24
|
+
const batch = JSON.stringify(sendQueue);
|
|
25
|
+
sendQueue.length = 0;
|
|
26
|
+
window.__sparkbunInternalBridge?.postMessage(batch);
|
|
27
|
+
|
|
28
|
+
// 2ms delay to work around Bun JSCallback threading issue
|
|
29
|
+
setTimeout(() => {
|
|
30
|
+
isProcessingQueue = false;
|
|
31
|
+
}, 2);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function send(type: string, payload: unknown) {
|
|
35
|
+
// Format: { type: 'message', id: handlerName, payload: data }
|
|
36
|
+
sendQueue.push(JSON.stringify({ type: "message", id: type, payload }));
|
|
37
|
+
processQueue();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function request(type: string, payload: unknown): Promise<unknown> {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const id = `req_${++requestId}_${Date.now()}`;
|
|
43
|
+
pendingRequests[id] = { resolve, reject };
|
|
44
|
+
// Format: { type: 'request', method: handlerName, id: requestId, params: data, hostWebviewId: ... }
|
|
45
|
+
sendQueue.push(
|
|
46
|
+
JSON.stringify({
|
|
47
|
+
type: "request",
|
|
48
|
+
method: type,
|
|
49
|
+
id,
|
|
50
|
+
params: payload,
|
|
51
|
+
hostWebviewId: window.__sparkbunWebviewId,
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
processQueue();
|
|
55
|
+
// Timeout after 10s
|
|
56
|
+
setTimeout(() => {
|
|
57
|
+
if (pendingRequests[id]) {
|
|
58
|
+
delete pendingRequests[id];
|
|
59
|
+
reject(new Error(`Request timeout: ${type}`));
|
|
60
|
+
}
|
|
61
|
+
}, 10000);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function handleResponse(msg: {
|
|
66
|
+
type: string;
|
|
67
|
+
id: string;
|
|
68
|
+
success: boolean;
|
|
69
|
+
payload: unknown;
|
|
70
|
+
}) {
|
|
71
|
+
// msg format: { type: 'response', id: requestId, success: bool, payload: data }
|
|
72
|
+
if (msg && msg.type === "response" && msg.id) {
|
|
73
|
+
const pending = pendingRequests[msg.id];
|
|
74
|
+
if (pending) {
|
|
75
|
+
delete pendingRequests[msg.id];
|
|
76
|
+
if (msg.success) pending.resolve(msg.payload);
|
|
77
|
+
else pending.reject(msg.payload);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import "./globals.d.ts";
|
|
2
|
+
|
|
3
|
+
export interface Rect {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type MaskRect = Rect;
|
|
11
|
+
|
|
12
|
+
export type OverlaySyncOptions = {
|
|
13
|
+
onSync: (rect: Rect, masksJson: string) => void;
|
|
14
|
+
getMasks?: () => MaskRect[];
|
|
15
|
+
burstIntervalMs?: number;
|
|
16
|
+
baseIntervalMs?: number;
|
|
17
|
+
burstDurationMs?: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export class OverlaySyncController {
|
|
21
|
+
private element: HTMLElement;
|
|
22
|
+
private options: Required<OverlaySyncOptions>;
|
|
23
|
+
private lastRect: Rect = { x: 0, y: 0, width: 0, height: 0 };
|
|
24
|
+
private resizeObserver: ResizeObserver | null = null;
|
|
25
|
+
private positionLoop: ReturnType<typeof setTimeout> | null = null;
|
|
26
|
+
private resizeHandler: (() => void) | null = null;
|
|
27
|
+
private burstUntil = 0;
|
|
28
|
+
|
|
29
|
+
constructor(element: HTMLElement, options: OverlaySyncOptions) {
|
|
30
|
+
this.element = element;
|
|
31
|
+
this.options = {
|
|
32
|
+
onSync: options.onSync,
|
|
33
|
+
getMasks: options.getMasks ?? (() => []),
|
|
34
|
+
burstIntervalMs: options.burstIntervalMs ?? 50,
|
|
35
|
+
baseIntervalMs: options.baseIntervalMs ?? 100,
|
|
36
|
+
burstDurationMs: options.burstDurationMs ?? 500,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
start() {
|
|
41
|
+
this.resizeObserver = new ResizeObserver(() => this.sync());
|
|
42
|
+
this.resizeObserver.observe(this.element);
|
|
43
|
+
|
|
44
|
+
const loop = () => {
|
|
45
|
+
this.sync();
|
|
46
|
+
const now = performance.now();
|
|
47
|
+
const interval =
|
|
48
|
+
now < this.burstUntil
|
|
49
|
+
? this.options.burstIntervalMs
|
|
50
|
+
: this.options.baseIntervalMs;
|
|
51
|
+
this.positionLoop = setTimeout(loop, interval);
|
|
52
|
+
};
|
|
53
|
+
this.positionLoop = setTimeout(loop, this.options.baseIntervalMs);
|
|
54
|
+
|
|
55
|
+
this.resizeHandler = () => this.sync(true);
|
|
56
|
+
window.addEventListener("resize", this.resizeHandler);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
stop() {
|
|
60
|
+
if (this.resizeObserver) this.resizeObserver.disconnect();
|
|
61
|
+
if (this.positionLoop) clearTimeout(this.positionLoop);
|
|
62
|
+
if (this.resizeHandler) {
|
|
63
|
+
window.removeEventListener("resize", this.resizeHandler);
|
|
64
|
+
}
|
|
65
|
+
this.resizeObserver = null;
|
|
66
|
+
this.positionLoop = null;
|
|
67
|
+
this.resizeHandler = null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
forceSync() {
|
|
71
|
+
this.sync(true);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
setLastRect(rect: Rect) {
|
|
75
|
+
this.lastRect = rect;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private sync(force = false) {
|
|
79
|
+
const rect = this.element.getBoundingClientRect();
|
|
80
|
+
const newRect: Rect = {
|
|
81
|
+
x: rect.x,
|
|
82
|
+
y: rect.y,
|
|
83
|
+
width: rect.width,
|
|
84
|
+
height: rect.height,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (newRect.width === 0 && newRect.height === 0) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (
|
|
92
|
+
!force &&
|
|
93
|
+
newRect.x === this.lastRect.x &&
|
|
94
|
+
newRect.y === this.lastRect.y &&
|
|
95
|
+
newRect.width === this.lastRect.width &&
|
|
96
|
+
newRect.height === this.lastRect.height
|
|
97
|
+
) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.burstUntil = performance.now() + this.options.burstDurationMs;
|
|
102
|
+
this.lastRect = newRect;
|
|
103
|
+
|
|
104
|
+
const masks = this.options.getMasks();
|
|
105
|
+
this.options.onSync(newRect, JSON.stringify(masks));
|
|
106
|
+
}
|
|
107
|
+
}
|