@x8r/sapphire 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,1390 @@
1
+ import { withMissingMemberFallback } from "./autoStub";
2
+ import { dbDelete, dbGet, dbGetAllKeys, dbPut, EXT_STORAGE_STORE } from "./db";
3
+ import { readExtFileText, readExtFileURL } from "./fileStore";
4
+ import { createRealmEvents, type PortRecord, type RealmEvents, type SapphireRegistry } from "./registry";
5
+ import { recomputeStaticRules } from "./dnr";
6
+ import type { SapphireHostBindings, TabInfo } from "./types";
7
+ import { EventHub } from "./eventHub";
8
+ import { injectScript } from "./htmlInject";
9
+ import { chromeExtensionUrl, decodeSapphireUrl } from "./urlScheme";
10
+
11
+ let sapphireTraceCallCounter = 0;
12
+
13
+ function sapphireTraceLog(...args: unknown[]): void {
14
+ if ((globalThis as { SAPPHIRE_TRACE?: boolean }).SAPPHIRE_TRACE) console.debug("[sapphire-trace]", ...args);
15
+ }
16
+
17
+ function traceCalls<T extends object>(obj: T, path = ""): T {
18
+ return new Proxy(obj, {
19
+ get(target, prop, receiver) {
20
+ const value = Reflect.get(target, prop, receiver);
21
+ if (typeof prop !== "string") return value;
22
+ const fullPath = path ? `${path}.${prop}` : prop;
23
+ if (typeof value === "function") {
24
+ return new Proxy(value, {
25
+ apply(fnTarget, thisArg, args) {
26
+ const callId = ++sapphireTraceCallCounter;
27
+ sapphireTraceLog(`#${callId} call ${fullPath}(`, args, `)`);
28
+ const result = Reflect.apply(fnTarget as (...a: unknown[]) => unknown, thisArg, args);
29
+ if (result && typeof (result as Promise<unknown>).then === "function") {
30
+ (result as Promise<unknown>).then(
31
+ (r) => sapphireTraceLog(`#${callId} resolved ${fullPath} =>`, r),
32
+ (e) => sapphireTraceLog(`#${callId} REJECTED ${fullPath} =>`, e),
33
+ );
34
+ }
35
+ return result;
36
+ },
37
+ get(fnTarget, fnProp, fnReceiver) {
38
+ const fnValue = Reflect.get(fnTarget, fnProp, fnReceiver);
39
+ if (typeof fnProp !== "string") return fnValue;
40
+ if (typeof fnValue === "object" && fnValue !== null) return traceCalls(fnValue, `${fullPath}.${fnProp}`);
41
+ return fnValue;
42
+ },
43
+ });
44
+ }
45
+ if (value && typeof value === "object") return traceCalls(value as object, fullPath);
46
+ return value;
47
+ },
48
+ }) as T;
49
+ }
50
+
51
+ export interface InstallChromeApiOptions {
52
+ extId: string;
53
+ tabId: number | null;
54
+ isBackground: boolean;
55
+ registry: SapphireRegistry;
56
+ host: SapphireHostBindings;
57
+ skipTabRegistration?: boolean;
58
+ senderUrl?: string;
59
+ senderFrameId?: number;
60
+ senderDocumentId?: string;
61
+ }
62
+
63
+ function cloneForRealm<T>(realm: Window | undefined, value: T): T {
64
+ if (!realm) return value;
65
+ try {
66
+ return (realm as unknown as { structuredClone: (v: T) => T }).structuredClone(value);
67
+ } catch {
68
+ return value;
69
+ }
70
+ }
71
+
72
+ export function buildTabObject(host: SapphireHostBindings, tabId: number | null, realm?: Window): unknown {
73
+ if (tabId === null) return null;
74
+ const tab: TabInfo | null = host.getTab(tabId);
75
+ if (!tab) return null;
76
+ const activeId = host.getActiveTabId?.() ?? null;
77
+ const obj = {
78
+ id: tab.id,
79
+ index: Math.max(0, tab.id - 1),
80
+ windowId: tab.windowId,
81
+ highlighted: tab.active,
82
+ active: tab.active || tab.id === activeId,
83
+ pinned: false,
84
+ audible: false,
85
+ discarded: false,
86
+ autoDiscardable: false,
87
+ mutedInfo: { muted: false },
88
+ url: tab.url,
89
+ title: tab.title,
90
+ favIconUrl: "",
91
+ status: "complete",
92
+ incognito: false,
93
+ width: 800,
94
+ height: 600,
95
+ };
96
+ return cloneForRealm(realm, obj);
97
+ }
98
+
99
+ function openUrlInTab(host: SapphireHostBindings, tabId: number | null, url: string): void {
100
+ const decoded = decodeSapphireUrl(url);
101
+ if (decoded) {
102
+ host.openExtensionTab?.(decoded.extId, decoded.path, tabId);
103
+ } else {
104
+ host.navigateTab?.(tabId, url);
105
+ }
106
+ }
107
+
108
+ async function captureVisibleTabViaDisplayMedia(targetWin: Window | null): Promise<string | null> {
109
+ if (!navigator.mediaDevices?.getDisplayMedia) return null;
110
+ let stream: MediaStream | null = null;
111
+ try {
112
+ stream = await navigator.mediaDevices.getDisplayMedia({
113
+ video: { displaySurface: "browser" },
114
+ preferCurrentTab: true,
115
+ } as DisplayMediaStreamOptions);
116
+ const track = stream.getVideoTracks()[0];
117
+ if (!track) return null;
118
+ const video = document.createElement("video");
119
+ video.srcObject = stream;
120
+ video.muted = true;
121
+ await video.play();
122
+ await new Promise<void>((resolve) => {
123
+ if (video.readyState >= 2) resolve();
124
+ else video.addEventListener("loadeddata", () => resolve(), { once: true });
125
+ });
126
+
127
+ const canvas = document.createElement("canvas");
128
+ canvas.width = video.videoWidth;
129
+ canvas.height = video.videoHeight;
130
+ const ctx = canvas.getContext("2d");
131
+ if (!ctx) return null;
132
+ ctx.drawImage(video, 0, 0);
133
+
134
+ const frameElement = (targetWin as unknown as { frameElement?: Element })?.frameElement;
135
+ if (frameElement) {
136
+ const rect = frameElement.getBoundingClientRect();
137
+ const scaleX = video.videoWidth / window.innerWidth;
138
+ const scaleY = video.videoHeight / window.innerHeight;
139
+ const cropCanvas = document.createElement("canvas");
140
+ cropCanvas.width = Math.max(1, Math.round(rect.width * scaleX));
141
+ cropCanvas.height = Math.max(1, Math.round(rect.height * scaleY));
142
+ const cropCtx = cropCanvas.getContext("2d");
143
+ if (cropCtx) {
144
+ cropCtx.drawImage(
145
+ canvas,
146
+ rect.left * scaleX,
147
+ rect.top * scaleY,
148
+ rect.width * scaleX,
149
+ rect.height * scaleY,
150
+ 0,
151
+ 0,
152
+ cropCanvas.width,
153
+ cropCanvas.height,
154
+ );
155
+ return cropCanvas.toDataURL("image/png");
156
+ }
157
+ }
158
+ return canvas.toDataURL("image/png");
159
+ } catch (e) {
160
+ console.warn("[sapphire] captureVisibleTab: getDisplayMedia failed or was cancelled", e);
161
+ return null;
162
+ } finally {
163
+ stream?.getTracks().forEach((t) => t.stop());
164
+ }
165
+ }
166
+
167
+ function generateDocumentId(): string {
168
+ return typeof crypto?.randomUUID === "function" ? crypto.randomUUID().replace(/-/g, "") : Math.random().toString(36).slice(2).padEnd(32, "0");
169
+ }
170
+
171
+ function originOf(url: string | undefined): string | undefined {
172
+ if (!url) return undefined;
173
+ try {
174
+ const parsed = new URL(url);
175
+ return parsed.protocol === "chrome-extension:" ? `chrome-extension://${parsed.hostname}` : parsed.origin;
176
+ } catch {
177
+ return undefined;
178
+ }
179
+ }
180
+
181
+ function dispatchMessage(hubs: EventHub[], message: unknown, sender: unknown): Promise<unknown> {
182
+ return new Promise((resolve) => {
183
+ queueMicrotask(() => dispatchMessageNow(hubs, message, sender, resolve));
184
+ });
185
+ }
186
+
187
+ function dispatchMessageNow(hubs: EventHub[], message: unknown, sender: unknown, resolve: (v: unknown) => void): void {
188
+ let responded = false;
189
+ const sendResponse = (resp: unknown) => {
190
+ sapphireTraceLog("sendResponse called with", resp, "already responded:", responded, "for", message);
191
+ if (responded) return;
192
+ responded = true;
193
+ resolve(resp);
194
+ };
195
+ const listeners = hubs.flatMap((hub) => hub.snapshot());
196
+ if (listeners.length === 0) {
197
+ console.warn("[sapphire] runtime.sendMessage: no onMessage listeners registered yet for this extension", message);
198
+ }
199
+ let anyAsync = false;
200
+ for (const [i, fn] of listeners.entries()) {
201
+ sapphireTraceLog(`dispatchMessage listener #${i} invoked for`, message);
202
+ const result = fn(message, sender, sendResponse);
203
+ sapphireTraceLog(`dispatchMessage listener #${i} returned`, result, "responded so far:", responded);
204
+ if (result === true) {
205
+ anyAsync = true;
206
+ } else if (result && typeof (result as Promise<unknown>).then === "function") {
207
+ anyAsync = true;
208
+ (result as Promise<unknown>).then(
209
+ (r) => {
210
+ sapphireTraceLog(`dispatchMessage listener #${i} thenable resolved`, r);
211
+ sendResponse(r);
212
+ },
213
+ (e) => {
214
+ sapphireTraceLog(`dispatchMessage listener #${i} thenable REJECTED`, e);
215
+ sendResponse(undefined);
216
+ },
217
+ );
218
+ } else if (!responded) {
219
+ console.debug("[sapphire] runtime.sendMessage: a listener ran but didn't signal it would respond (returned", result, ") — treating as not handled", message);
220
+ }
221
+ }
222
+ if (!anyAsync && !responded) sendResponse(undefined);
223
+ }
224
+
225
+ async function storageGet(extId: string, area: string, keys: unknown): Promise<Record<string, unknown>> {
226
+ const result: Record<string, unknown> = {};
227
+ const allKeys = await dbGetAllKeys(EXT_STORAGE_STORE);
228
+ const prefix = `${extId}/${area}/`;
229
+ const relevant = allKeys.filter((k) => typeof k === "string" && k.startsWith(prefix)) as string[];
230
+ for (const k of relevant) {
231
+ const shortKey = k.slice(prefix.length);
232
+ let include = false;
233
+ if (keys === null || keys === undefined) include = true;
234
+ else if (typeof keys === "string") include = shortKey === keys;
235
+ else if (Array.isArray(keys)) include = keys.includes(shortKey);
236
+ else if (typeof keys === "object") include = shortKey in (keys as object);
237
+ if (include) result[shortKey] = await dbGet(EXT_STORAGE_STORE, k);
238
+ }
239
+ if (typeof keys === "object" && keys !== null && !Array.isArray(keys)) {
240
+ for (const [k, v] of Object.entries(keys as Record<string, unknown>)) {
241
+ if (!(k in result)) result[k] = v;
242
+ }
243
+ }
244
+ return result;
245
+ }
246
+
247
+ async function storageSet(extId: string, area: string, items: Record<string, unknown>): Promise<void> {
248
+ for (const [key, value] of Object.entries(items)) {
249
+ await dbPut(EXT_STORAGE_STORE, `${extId}/${area}/${key}`, value);
250
+ }
251
+ }
252
+
253
+ async function storageRemove(extId: string, area: string, keys: string | string[]): Promise<void> {
254
+ const arr = Array.isArray(keys) ? keys : [keys];
255
+ for (const key of arr) {
256
+ await dbDelete(EXT_STORAGE_STORE, `${extId}/${area}/${key}`);
257
+ }
258
+ }
259
+
260
+ async function storageClear(extId: string, area: string): Promise<void> {
261
+ const allKeys = await dbGetAllKeys(EXT_STORAGE_STORE);
262
+ const prefix = `${extId}/${area}/`;
263
+ for (const k of allKeys.filter((key) => typeof key === "string" && key.startsWith(prefix))) {
264
+ await dbDelete(EXT_STORAGE_STORE, k);
265
+ }
266
+ }
267
+
268
+ async function storageGetKeys(extId: string, area: string): Promise<string[]> {
269
+ const allKeys = await dbGetAllKeys(EXT_STORAGE_STORE);
270
+ const prefix = `${extId}/${area}/`;
271
+ return (allKeys.filter((k) => typeof k === "string" && k.startsWith(prefix)) as string[]).map((k) => k.slice(prefix.length));
272
+ }
273
+
274
+ function makeStorageArea(extId: string, area: string, realm: Window) {
275
+ return {
276
+ get: (keys: unknown, cb?: (items: Record<string, unknown>) => void) => {
277
+ const p = storageGet(extId, area, keys).then((result) => cloneForRealm(realm, result));
278
+ if (cb) p.then(cb);
279
+ return p;
280
+ },
281
+ set: (items: Record<string, unknown>, cb?: () => void) => {
282
+ const p = storageSet(extId, area, items);
283
+ if (cb) p.then(cb);
284
+ return p;
285
+ },
286
+ remove: (keys: string | string[], cb?: () => void) => {
287
+ const p = storageRemove(extId, area, keys);
288
+ if (cb) p.then(cb);
289
+ return p;
290
+ },
291
+ clear: (cb?: () => void) => {
292
+ const p = storageClear(extId, area);
293
+ if (cb) p.then(cb);
294
+ return p;
295
+ },
296
+ getBytesInUse: (_keys: unknown, cb?: (n: number) => void) => {
297
+ const result = (0);
298
+ cb?.(result);
299
+ return Promise.resolve(result);
300
+ },
301
+ getKeys: (cb?: (keys: string[]) => void) => {
302
+ const p = storageGetKeys(extId, area);
303
+ if (cb) p.then(cb);
304
+ return p;
305
+ },
306
+ };
307
+ }
308
+
309
+ export function installChromeApi(realm: Window, opts: InstallChromeApiOptions): RealmEvents {
310
+ const { extId, tabId, isBackground, registry, host, skipTabRegistration, senderUrl, senderFrameId, senderDocumentId } = opts;
311
+ const ext = registry.get(extId);
312
+ if (!ext) throw new Error(`sapphire: installChromeApi called for unknown extension ${extId}`);
313
+
314
+ const events = createRealmEvents();
315
+ if (isBackground) {
316
+ } else if (tabId !== null && !skipTabRegistration) {
317
+ ext.tabEvents.set(tabId, events);
318
+ }
319
+
320
+ let portIdCounter = 0;
321
+
322
+ const runtime = {
323
+ id: extId,
324
+ getManifest: () => ext.manifest,
325
+ getURL: (path?: string) => chromeExtensionUrl(extId, path == null ? "" : String(path)),
326
+ sendMessage: (
327
+ extIdOrMsg: unknown,
328
+ msgOrOpts?: unknown,
329
+ optsOrCb?: unknown,
330
+ maybeCb?: unknown,
331
+ ) => {
332
+ let targetExt: string;
333
+ let message: unknown;
334
+ let callback: ((resp: unknown) => void) | undefined;
335
+ if (typeof extIdOrMsg === "object") {
336
+ message = extIdOrMsg;
337
+ callback = msgOrOpts as typeof callback;
338
+ targetExt = extId;
339
+ } else if (typeof extIdOrMsg === "string" && typeof msgOrOpts === "object") {
340
+ targetExt = extIdOrMsg;
341
+ message = msgOrOpts;
342
+ callback = (typeof optsOrCb === "function" ? optsOrCb : maybeCb) as typeof callback;
343
+ } else {
344
+ message = extIdOrMsg;
345
+ callback = msgOrOpts as typeof callback;
346
+ targetExt = extId;
347
+ }
348
+ const target = registry.get(targetExt);
349
+ const hubs = [target?.background?.events.runtimeOnMessage, target?.popupEvents?.runtimeOnMessage].filter(
350
+ (h): h is EventHub => h !== undefined,
351
+ );
352
+ if (hubs.length === 0) {
353
+ if (callback) {
354
+ callback(undefined);
355
+ return undefined;
356
+ }
357
+ return Promise.resolve(undefined);
358
+ }
359
+ const sender = {
360
+ id: extId,
361
+ url: senderUrl,
362
+ origin: originOf(senderUrl),
363
+ frameId: senderFrameId,
364
+ documentId: senderDocumentId,
365
+ tab: tabId !== null ? buildTabObject(host, tabId, realm) : undefined,
366
+ };
367
+ const responsePromise = dispatchMessage(hubs, message, sender).then((resp) => cloneForRealm(realm, resp));
368
+ if (callback) {
369
+ void responsePromise.then(callback);
370
+ return undefined;
371
+ }
372
+ return responsePromise;
373
+ },
374
+ onMessage: events.runtimeOnMessage.toApi(),
375
+ onInstalled: events.runtimeOnInstalled.toApi(),
376
+ onStartup: events.runtimeOnStartup.toApi(),
377
+ onConnect: events.runtimeOnConnect.toApi(),
378
+ connect: (extIdOrInfo?: unknown, maybeInfo?: unknown) => {
379
+ const targetExt = typeof extIdOrInfo === "string" ? extIdOrInfo : extId;
380
+ const connectInfo = (typeof extIdOrInfo === "string" ? maybeInfo : extIdOrInfo) as { name?: string } | undefined;
381
+ const name = connectInfo?.name ?? "";
382
+ const portId = `port_${++portIdCounter}`;
383
+
384
+ const callerSide = { onMessage: new EventHub(), onDisconnect: new EventHub() };
385
+ const remoteSide = { onMessage: new EventHub(), onDisconnect: new EventHub() };
386
+
387
+ const disconnectBoth = () => {
388
+ callerSide.onDisconnect.fire();
389
+ remoteSide.onDisconnect.fire();
390
+ ext.ports.delete(portId);
391
+ };
392
+
393
+ const sender = {
394
+ id: extId,
395
+ url: senderUrl,
396
+ origin: originOf(senderUrl),
397
+ frameId: senderFrameId,
398
+ documentId: senderDocumentId,
399
+ tab: tabId !== null ? buildTabObject(host, tabId, realm) : undefined,
400
+ };
401
+
402
+ let callerPort: typeof remotePort;
403
+ let remotePort: {
404
+ name: string;
405
+ sender: typeof sender;
406
+ postMessage: (msg: unknown) => void;
407
+ disconnect: () => void;
408
+ onMessage: ReturnType<EventHub["toApi"]>;
409
+ onDisconnect: ReturnType<EventHub["toApi"]>;
410
+ };
411
+
412
+ callerPort = {
413
+ name,
414
+ sender,
415
+ postMessage: (msg: unknown) => remoteSide.onMessage.fire(msg, remotePort),
416
+ disconnect: disconnectBoth,
417
+ onMessage: callerSide.onMessage.toApi(),
418
+ onDisconnect: callerSide.onDisconnect.toApi(),
419
+ };
420
+ remotePort = {
421
+ name,
422
+ sender,
423
+ postMessage: (msg: unknown) => callerSide.onMessage.fire(msg, callerPort),
424
+ disconnect: disconnectBoth,
425
+ onMessage: remoteSide.onMessage.toApi(),
426
+ onDisconnect: remoteSide.onDisconnect.toApi(),
427
+ };
428
+
429
+ const record: PortRecord = { id: portId, name, extId: targetExt, remote: remoteSide };
430
+ ext.ports.set(portId, record);
431
+
432
+ const target = registry.get(targetExt);
433
+ target?.background?.events.runtimeOnConnect.fire(remotePort);
434
+
435
+ return callerPort;
436
+ },
437
+ lastError: null as { message: string } | null,
438
+ getPlatformInfo: (cb?: (info: unknown) => void) => {
439
+ const result = ({ os: "linux", arch: "x86-64", nacl_arch: "x86_64" });
440
+ cb?.(result);
441
+ return Promise.resolve(result);
442
+ },
443
+ openOptionsPage: (cb?: () => void) => {
444
+ const page = ext.manifest.options_page ?? ext.manifest.options_ui?.page;
445
+ if (page) host.openExtensionTab?.(extId, page, tabId);
446
+ cb?.();
447
+ },
448
+ setUninstallURL: (_url?: string, cb?: () => void) => {
449
+ cb?.();
450
+ return Promise.resolve(undefined);
451
+ },
452
+ requestUpdateCheck: (cb?: (status: string, details: unknown) => void) => {
453
+ cb?.("no_update", {});
454
+ return Promise.resolve({ status: "no_update", details: {} });
455
+ },
456
+ };
457
+
458
+ const storage = {
459
+ local: makeStorageArea(extId, "local", realm),
460
+ sync: makeStorageArea(extId, "sync", realm),
461
+ session: makeStorageArea(extId, "session", realm),
462
+ managed: makeStorageArea(extId, "managed", realm),
463
+ onChanged: new EventHub().toApi(),
464
+ };
465
+
466
+ const tabs = {
467
+ query: (queryInfo: { active?: boolean; url?: string } = {}, cb?: (tabs: unknown[]) => void) => {
468
+ const active = host.getActiveTabId?.();
469
+ const result = host
470
+ .getAllTabs()
471
+ .filter((t) => {
472
+ if (queryInfo.active !== undefined && queryInfo.active !== (t.id === active)) return false;
473
+ if (queryInfo.url && !t.url.includes(queryInfo.url)) return false;
474
+ return true;
475
+ })
476
+ .map((t) => buildTabObject(host, t.id, realm));
477
+ cb?.(result);
478
+ return Promise.resolve(result);
479
+ },
480
+ get: (id: number, cb?: (tab: unknown) => void) => {
481
+ const tab = buildTabObject(host, id, realm);
482
+ cb?.(tab);
483
+ return Promise.resolve(tab);
484
+ },
485
+ getCurrent: (cb?: (tab: unknown) => void) => {
486
+ const tab = buildTabObject(host, tabId, realm);
487
+ cb?.(tab);
488
+ return Promise.resolve(tab);
489
+ },
490
+ create: (createProps: { url?: string } = {}, cb?: (tab: unknown) => void) => {
491
+ if (createProps.url) openUrlInTab(host, null, createProps.url);
492
+ const tab = buildTabObject(host, tabId, realm);
493
+ cb?.(tab);
494
+ return Promise.resolve(tab);
495
+ },
496
+ update: (tabIdOrProps: unknown, updatePropsOrCb?: unknown, maybeCb?: unknown) => {
497
+ const targetTabId = typeof tabIdOrProps === "number" ? tabIdOrProps : tabId;
498
+ const updateProps = (typeof tabIdOrProps === "number" ? updatePropsOrCb : tabIdOrProps) as { url?: string } | undefined;
499
+ const cb = (typeof updatePropsOrCb === "function" ? updatePropsOrCb : maybeCb) as ((tab: unknown) => void) | undefined;
500
+ if (updateProps?.url) openUrlInTab(host, targetTabId, updateProps.url);
501
+ const tab = buildTabObject(host, targetTabId, realm);
502
+ cb?.(tab);
503
+ return Promise.resolve(tab);
504
+ },
505
+ remove: (_tabIds: number | number[], cb?: () => void) => {
506
+ cb?.();
507
+ return Promise.resolve(undefined);
508
+ },
509
+ sendMessage: (targetTabId: number, message: unknown, optsOrCb?: unknown, maybeCb?: unknown) => {
510
+ const callback = (typeof optsOrCb === "function" ? optsOrCb : typeof maybeCb === "function" ? maybeCb : undefined) as
511
+ | ((resp: unknown) => void)
512
+ | undefined;
513
+ const targetEvents = ext.tabEvents.get(targetTabId);
514
+ if (!targetEvents) {
515
+ if (callback) {
516
+ callback(undefined);
517
+ return undefined;
518
+ }
519
+ return Promise.resolve(undefined);
520
+ }
521
+ const sender = {
522
+ id: extId,
523
+ url: senderUrl,
524
+ frameId: senderFrameId,
525
+ documentId: senderDocumentId,
526
+ tab: buildTabObject(host, tabId, realm),
527
+ };
528
+ const responsePromise = dispatchMessage([targetEvents.runtimeOnMessage], message, sender).then((resp) => cloneForRealm(realm, resp));
529
+ if (callback) {
530
+ void responsePromise.then(callback);
531
+ return undefined;
532
+ }
533
+ return responsePromise;
534
+ },
535
+ onCreated: events.tabsOnCreated.toApi(),
536
+ onUpdated: events.tabsOnUpdated.toApi(),
537
+ onRemoved: events.tabsOnRemoved.toApi(),
538
+ onActivated: events.tabsOnActivated.toApi(),
539
+ executeScript: (tabIdOrDetails: unknown, detailsOrCb?: unknown, maybeCb?: unknown) => {
540
+ const actualTabId = typeof tabIdOrDetails === "number" ? tabIdOrDetails : tabId;
541
+ const details = (typeof tabIdOrDetails === "object" ? tabIdOrDetails : detailsOrCb) as { code?: string; file?: string } | undefined;
542
+ const cb = (typeof detailsOrCb === "function" ? detailsOrCb : maybeCb) as ((r: unknown[]) => void) | undefined;
543
+ const win = actualTabId !== null ? host.getTabWindow?.(actualTabId) : null;
544
+ let result: unknown[] = [];
545
+ if (win && details?.code) {
546
+ try {
547
+ result = [(win as unknown as { eval: (s: string) => unknown }).eval(details.code)];
548
+ } catch {
549
+ result = [];
550
+ }
551
+ }
552
+ cb?.(result);
553
+ return Promise.resolve(result);
554
+ },
555
+ insertCSS: (tabIdOrDetails: unknown, detailsOrCb?: unknown, maybeCb?: unknown) => {
556
+ const actualTabId = typeof tabIdOrDetails === "number" ? tabIdOrDetails : tabId;
557
+ const details = (typeof tabIdOrDetails === "object" ? tabIdOrDetails : detailsOrCb) as { code?: string } | undefined;
558
+ const cb = (typeof detailsOrCb === "function" ? detailsOrCb : maybeCb) as (() => void) | undefined;
559
+ const win = actualTabId !== null ? host.getTabWindow?.(actualTabId) : null;
560
+ if (win && details?.code) injectCss(win, details.code);
561
+ cb?.();
562
+ return Promise.resolve(undefined);
563
+ },
564
+ captureVisibleTab: (windowIdOrOpts?: unknown, optsOrCb?: unknown, maybeCb?: unknown) => {
565
+ const cb = (typeof windowIdOrOpts === "function" ? windowIdOrOpts : typeof optsOrCb === "function" ? optsOrCb : maybeCb) as
566
+ | ((dataUrl: string | null) => void)
567
+ | undefined;
568
+ const targetWin = tabId !== null ? host.getTabWindow?.(tabId) ?? null : null;
569
+ const p = captureVisibleTabViaDisplayMedia(targetWin);
570
+ p.then(cb);
571
+ return p;
572
+ },
573
+ TAB_ID_NONE: -1,
574
+ };
575
+
576
+ const windows = {
577
+ getCurrent: (cb?: (w: unknown) => void) => {
578
+ const w = { id: 1, focused: true, type: "normal", state: "normal" };
579
+ cb?.(w);
580
+ return Promise.resolve(w);
581
+ },
582
+ getAll: (cb?: (w: unknown[]) => void) => {
583
+ const all = [{ id: 1, focused: true, type: "normal", state: "normal" }];
584
+ cb?.(all);
585
+ return Promise.resolve(all);
586
+ },
587
+ create: (createData: { url?: string } = {}, cb?: (w: unknown) => void) => {
588
+ if (createData.url) host.navigateTab?.(null, createData.url);
589
+ const w = { id: 1, focused: true, type: "normal", state: "normal" };
590
+ cb?.(w);
591
+ return Promise.resolve(w);
592
+ },
593
+ onFocusChanged: new EventHub().toApi(),
594
+ WINDOW_ID_NONE: -1,
595
+ WINDOW_ID_CURRENT: -2,
596
+ };
597
+
598
+ const i18n = {
599
+ getMessage: (messageName: string, substitutions?: string | string[]) => {
600
+ const msg = ext.messages[messageName];
601
+ if (!msg) return "";
602
+ let text = msg.message ?? "";
603
+ if (substitutions) {
604
+ const subs = Array.isArray(substitutions) ? substitutions : [substitutions];
605
+ subs.forEach((s, i) => {
606
+ text = text.replace(new RegExp(`\\$${i + 1}`, "g"), s);
607
+ });
608
+ }
609
+ return text;
610
+ },
611
+ getUILanguage: () => realm.navigator.language || "en",
612
+ detectLanguage: (_text: string, cb?: (r: unknown) => void) => {
613
+ const result = ({ isReliable: false, languages: [] });
614
+ cb?.(result);
615
+ return Promise.resolve(result);
616
+ },
617
+ getAcceptLanguage: (cb?: (langs: string[]) => void) => {
618
+ const result = ([realm.navigator.language || "en"]);
619
+ cb?.(result);
620
+ return Promise.resolve(result);
621
+ },
622
+ };
623
+
624
+ const contextMenus = {
625
+ create: (props: { id?: string; title: string; contexts?: string[] }, cb?: () => void) => {
626
+ const id = props.id ?? Math.random().toString(36).slice(2);
627
+ ext.contextMenuItems.push({ id, title: props.title, contexts: props.contexts ?? ["all"] });
628
+ cb?.();
629
+ return id;
630
+ },
631
+ update: (id: string, props: Partial<{ title: string; contexts: string[] }>, cb?: () => void) => {
632
+ const item = ext.contextMenuItems.find((i) => i.id === id);
633
+ if (item) Object.assign(item, props);
634
+ cb?.();
635
+ },
636
+ remove: (id: string, cb?: () => void) => {
637
+ const idx = ext.contextMenuItems.findIndex((i) => i.id === id);
638
+ if (idx > -1) ext.contextMenuItems.splice(idx, 1);
639
+ cb?.();
640
+ },
641
+ removeAll: (cb?: () => void) => {
642
+ ext.contextMenuItems.length = 0;
643
+ cb?.();
644
+ },
645
+ onClicked: events.contextMenusOnClicked.toApi(),
646
+ };
647
+
648
+ const notifications = {
649
+ create: (idOrOptions: unknown, optionsOrCb?: unknown, maybeCb?: unknown) => {
650
+ const notifId = typeof idOrOptions === "string" ? idOrOptions : `notif_${Date.now()}`;
651
+ const options = (typeof idOrOptions === "string" ? optionsOrCb : idOrOptions) as { title?: string; message?: string } | undefined;
652
+ const cb = (typeof optionsOrCb === "function" ? optionsOrCb : maybeCb) as ((id: string) => void) | undefined;
653
+ host.showNotification?.(options?.title ?? ext.manifest.name, options?.message ?? "");
654
+ cb?.(notifId);
655
+ return notifId;
656
+ },
657
+ update: (_id: string, _options: unknown, cb?: (wasUpdated: boolean) => void) => {
658
+ const result = (true);
659
+ cb?.(result);
660
+ return Promise.resolve(result);
661
+ },
662
+ clear: (_id: string, cb?: (wasCleared: boolean) => void) => {
663
+ const result = (true);
664
+ cb?.(result);
665
+ return Promise.resolve(result);
666
+ },
667
+ getAll: (cb?: (all: Record<string, unknown>) => void) => {
668
+ const result = ({});
669
+ cb?.(result);
670
+ return Promise.resolve(result);
671
+ },
672
+ onClicked: events.notificationsOnClicked.toApi(),
673
+ onClosed: events.notificationsOnClosed.toApi(),
674
+ onButtonClicked: events.notificationsOnButtonClicked.toApi(),
675
+ };
676
+
677
+ const cookies = {
678
+ get: (details: { name: string; domain?: string }, cb?: (cookie: unknown) => void) => {
679
+ const win = tabId !== null ? host.getTabWindow?.(tabId) : realm;
680
+ let val: unknown = null;
681
+ try {
682
+ const all = win?.document?.cookie?.split(";") ?? [];
683
+ const found = all.find((c) => c.trim().startsWith(`${details.name}=`));
684
+ if (found) {
685
+ const value = found.split("=").slice(1).join("=").trim();
686
+ val = { name: details.name, value, domain: details.domain ?? "", path: "/" };
687
+ }
688
+ } catch {
689
+ }
690
+ cb?.(val);
691
+ return val;
692
+ },
693
+ set: (details: { name: string; value: string; path?: string; domain?: string }, cb?: () => void) => {
694
+ const win = tabId !== null ? host.getTabWindow?.(tabId) : realm;
695
+ try {
696
+ let c = `${details.name}=${details.value}`;
697
+ if (details.path) c += `;path=${details.path}`;
698
+ if (details.domain) c += `;domain=${details.domain}`;
699
+ if (win) win.document.cookie = c;
700
+ } catch {
701
+ }
702
+ cb?.();
703
+ },
704
+ getAll: (_details: unknown, cb?: (cookies: unknown[]) => void) => {
705
+ const result: unknown[] = [];
706
+ cb?.(result);
707
+ return Promise.resolve(result);
708
+ },
709
+ remove: (_details: unknown, cb?: () => void) => {
710
+ cb?.();
711
+ return Promise.resolve(undefined);
712
+ },
713
+ onChanged: events.cookiesOnChanged.toApi(),
714
+ };
715
+
716
+ function mkWebRequestEvent() {
717
+ const hub = new EventHub();
718
+ return {
719
+ addListener: (fn: (...a: unknown[]) => unknown, _filter?: unknown, _extraInfoSpec?: unknown) => hub.addListener(fn),
720
+ removeListener: hub.removeListener,
721
+ hasListener: hub.hasListener,
722
+ };
723
+ }
724
+ const webRequest = {
725
+ onBeforeRequest: mkWebRequestEvent(),
726
+ onBeforeSendHeaders: mkWebRequestEvent(),
727
+ onSendHeaders: mkWebRequestEvent(),
728
+ onHeadersReceived: mkWebRequestEvent(),
729
+ onCompleted: mkWebRequestEvent(),
730
+ onErrorOccurred: mkWebRequestEvent(),
731
+ onBeforeRedirect: mkWebRequestEvent(),
732
+ handlerBehaviorChanged: (cb?: () => void) => {
733
+ cb?.();
734
+ return Promise.resolve(undefined);
735
+ },
736
+ };
737
+
738
+ const declarativeNetRequest = {
739
+ updateDynamicRules: (options: { addRules?: unknown[]; removeRuleIds?: number[] }, cb?: () => void) => {
740
+ if (options.removeRuleIds) {
741
+ ext.dynamicRules = ext.dynamicRules.filter((r) => !options.removeRuleIds!.includes(r.id));
742
+ }
743
+ if (options.addRules) ext.dynamicRules.push(...(options.addRules as typeof ext.dynamicRules));
744
+ cb?.();
745
+ return Promise.resolve(undefined);
746
+ },
747
+ getDynamicRules: (filterOrCb?: unknown, maybeCb?: (rules: unknown[]) => void) => {
748
+ const filter = typeof filterOrCb === "function" ? undefined : (filterOrCb as { ruleIds?: number[] } | undefined);
749
+ const cb = typeof filterOrCb === "function" ? (filterOrCb as (rules: unknown[]) => void) : maybeCb;
750
+ const rules = filter?.ruleIds ? ext.dynamicRules.filter((r) => filter.ruleIds!.includes(r.id)) : ext.dynamicRules;
751
+ cb?.(rules);
752
+ return Promise.resolve(rules);
753
+ },
754
+ updateSessionRules: (_options: unknown, cb?: () => void) => {
755
+ cb?.();
756
+ return Promise.resolve(undefined);
757
+ },
758
+ getSessionRules: (filterOrCb?: unknown, maybeCb?: (rules: unknown[]) => void) => {
759
+ const cb = typeof filterOrCb === "function" ? (filterOrCb as (rules: unknown[]) => void) : maybeCb;
760
+ cb?.([]);
761
+ return Promise.resolve([]);
762
+ },
763
+ isRegexSupported: (_opts: unknown, cb?: (r: { isSupported: boolean }) => void) => {
764
+ const result = { isSupported: true };
765
+ cb?.(result);
766
+ return Promise.resolve(result);
767
+ },
768
+ testMatchOutcome: (_req: unknown, cb?: (r: unknown) => void) => {
769
+ const result = { matchedRules: [] };
770
+ cb?.(result);
771
+ return Promise.resolve(result);
772
+ },
773
+ getAvailableRulesets: (cb?: (ids: string[]) => void) => {
774
+ const ids = [...ext.rulesetRules.keys()];
775
+ cb?.(ids);
776
+ return Promise.resolve(ids);
777
+ },
778
+ getEnabledRulesets: (cb?: (ids: string[]) => void) => {
779
+ const ids = [...ext.enabledRulesetIds];
780
+ cb?.(ids);
781
+ return Promise.resolve(ids);
782
+ },
783
+ updateEnabledRulesets: (options: { enableRulesetIds?: string[]; disableRulesetIds?: string[] }, cb?: () => void) => {
784
+ for (const id of options.disableRulesetIds ?? []) ext.enabledRulesetIds.delete(id);
785
+ for (const id of options.enableRulesetIds ?? []) {
786
+ if (ext.rulesetRules.has(id)) ext.enabledRulesetIds.add(id);
787
+ }
788
+ recomputeStaticRules(ext);
789
+ cb?.();
790
+ return Promise.resolve(undefined);
791
+ },
792
+ MAX_NUMBER_OF_RULES: 30000,
793
+ MAX_NUMBER_OF_DYNAMIC_RULES: 5000,
794
+ GUARANTEED_MINIMUM_STATIC_RULES: 30000,
795
+ };
796
+
797
+ function injectCss(win: Window, css: string) {
798
+ try {
799
+ const doc = win.document;
800
+ const style = doc.createElement("style");
801
+ style.textContent = css;
802
+ (doc.head || doc.documentElement).appendChild(style);
803
+ } catch {
804
+ }
805
+ }
806
+
807
+ const scripting = {
808
+ executeScript: async (
809
+ injection: { target?: { tabId?: number }; func?: (...a: unknown[]) => unknown; args?: unknown[]; files?: string[] },
810
+ cb?: (results: unknown[]) => void,
811
+ ) => {
812
+ const targetTabId = injection.target?.tabId ?? tabId;
813
+ const win = targetTabId !== null ? host.getTabWindow?.(targetTabId) : null;
814
+ if (win && targetTabId !== null && !(win as unknown as { chrome?: unknown }).chrome) {
815
+ installChromeApi(win, {
816
+ extId,
817
+ tabId: targetTabId,
818
+ isBackground: false,
819
+ registry,
820
+ host,
821
+ senderUrl: host.getTab(targetTabId)?.url,
822
+ senderFrameId: 0,
823
+ senderDocumentId: generateDocumentId(),
824
+ });
825
+ }
826
+ let results: unknown[] = [];
827
+ if (win && injection.func) {
828
+ try {
829
+ results = [{ result: injection.func(...(injection.args ?? [])) }];
830
+ } catch {
831
+ results = [];
832
+ }
833
+ } else if (win && injection.files?.length) {
834
+ for (const file of injection.files) {
835
+ const code = await readExtFileText(extId, file);
836
+ if (code !== null) injectScript(win, code);
837
+ }
838
+ results = injection.files.map(() => ({ result: undefined }));
839
+ }
840
+ cb?.(results);
841
+ return Promise.resolve(results);
842
+ },
843
+ insertCSS: (injection: { target?: { tabId?: number }; css?: string }, cb?: () => void) => {
844
+ const targetTabId = injection.target?.tabId ?? tabId;
845
+ const win = targetTabId !== null ? host.getTabWindow?.(targetTabId) : null;
846
+ if (win && injection.css) injectCss(win, injection.css);
847
+ cb?.();
848
+ return Promise.resolve(undefined);
849
+ },
850
+ removeCSS: (_injection: unknown, cb?: () => void) => {
851
+ cb?.();
852
+ return Promise.resolve(undefined);
853
+ },
854
+ registerContentScripts: (
855
+ scripts: { id: string; matches?: string[]; excludeMatches?: string[]; js?: string[]; css?: string[]; runAt?: string; allFrames?: boolean }[],
856
+ cb?: () => void,
857
+ ) => {
858
+ for (const s of scripts) {
859
+ const idx = registry.contentScripts.findIndex((cs) => cs.extId === extId && cs.id === s.id);
860
+ const entry = {
861
+ extId,
862
+ id: s.id,
863
+ matches: s.matches ?? [],
864
+ excludeMatches: s.excludeMatches ?? [],
865
+ js: s.js ?? [],
866
+ css: s.css ?? [],
867
+ runAt: (s.runAt as "document_start" | "document_end" | "document_idle" | undefined) ?? "document_idle",
868
+ allFrames: s.allFrames ?? false,
869
+ };
870
+ if (idx === -1) registry.contentScripts.push(entry);
871
+ else registry.contentScripts[idx] = entry;
872
+ }
873
+ cb?.();
874
+ return Promise.resolve(undefined);
875
+ },
876
+ unregisterContentScripts: (filter: { ids?: string[] } | undefined, cb?: () => void) => {
877
+ for (let i = registry.contentScripts.length - 1; i >= 0; i--) {
878
+ const cs = registry.contentScripts[i];
879
+ if (cs.extId !== extId || cs.id === undefined) continue;
880
+ if (filter?.ids && !filter.ids.includes(cs.id)) continue;
881
+ registry.contentScripts.splice(i, 1);
882
+ }
883
+ cb?.();
884
+ return Promise.resolve(undefined);
885
+ },
886
+ getRegisteredContentScripts: (filter: { ids?: string[] } | undefined, cb?: (scripts: unknown[]) => void) => {
887
+ const result = registry.contentScripts
888
+ .filter((cs) => cs.extId === extId && cs.id !== undefined && (!filter?.ids || filter.ids.includes(cs.id)))
889
+ .map((cs) => ({
890
+ id: cs.id,
891
+ matches: cs.matches,
892
+ excludeMatches: cs.excludeMatches,
893
+ js: cs.js,
894
+ css: cs.css,
895
+ runAt: cs.runAt,
896
+ allFrames: cs.allFrames,
897
+ }));
898
+ cb?.(result);
899
+ return Promise.resolve(result);
900
+ },
901
+ };
902
+
903
+ function makeAction(clickHub: EventHub) {
904
+ const extRef = ext!;
905
+ return {
906
+ setIcon: (details: { imageData?: unknown; path?: unknown }, cb?: () => void) => {
907
+ if (details.imageData) {
908
+ extRef.iconUrl = typeof details.imageData === "object" ? (Object.values(details.imageData as object)[0] as string) : (details.imageData as string);
909
+ registry.notifyChange();
910
+ cb?.();
911
+ } else if (details.path) {
912
+ const p = typeof details.path === "object" ? (Object.values(details.path as object)[0] as string) : (details.path as string);
913
+ readExtFileURL(extRef.id, p).then((url) => {
914
+ if (url) extRef.iconUrl = url;
915
+ registry.notifyChange();
916
+ cb?.();
917
+ });
918
+ } else {
919
+ cb?.();
920
+ }
921
+ },
922
+ setTitle: (details: { title: string }, cb?: () => void) => {
923
+ extRef.title = details.title;
924
+ registry.notifyChange();
925
+ cb?.();
926
+ },
927
+ setBadgeText: (details: { text: string }, cb?: () => void) => {
928
+ extRef.badgeText = details.text ?? "";
929
+ registry.notifyChange();
930
+ cb?.();
931
+ },
932
+ setBadgeBackgroundColor: (details: { color: string }, cb?: () => void) => {
933
+ extRef.badgeColor = details.color;
934
+ registry.notifyChange();
935
+ cb?.();
936
+ },
937
+ getBadgeText: (_details: unknown, cb?: (text: string) => void) => {
938
+ const result = (extRef.badgeText);
939
+ cb?.(result);
940
+ return Promise.resolve(result);
941
+ },
942
+ enable: (_tabId?: number, cb?: () => void) => {
943
+ cb?.();
944
+ return Promise.resolve(undefined);
945
+ },
946
+ disable: (_tabId?: number, cb?: () => void) => {
947
+ cb?.();
948
+ return Promise.resolve(undefined);
949
+ },
950
+ onClicked: clickHub.toApi(),
951
+ openPopup: (_options: unknown, cb?: () => void) => {
952
+ cb?.();
953
+ return Promise.resolve(undefined);
954
+ },
955
+ setPopup: (details: { popup: string }, cb?: () => void) => {
956
+ extRef.popupPage = details.popup;
957
+ registry.notifyChange();
958
+ cb?.();
959
+ },
960
+ getPopup: (_details: unknown, cb?: (popup: string) => void) => {
961
+ const result = (extRef.popupPage ?? "");
962
+ cb?.(result);
963
+ return Promise.resolve(result);
964
+ },
965
+ };
966
+ }
967
+ const action = makeAction(events.actionOnClicked);
968
+ const browserAction = makeAction(events.browserActionOnClicked);
969
+ const pageAction = makeAction(events.actionOnClicked);
970
+
971
+ const alarms = {
972
+ create: (nameOrInfo: unknown, maybeInfo?: unknown) => {
973
+ const name = typeof nameOrInfo === "string" ? nameOrInfo : "";
974
+ const alarmInfo = (typeof nameOrInfo === "string" ? maybeInfo : nameOrInfo) as
975
+ | { when?: number; delayInMinutes?: number; periodInMinutes?: number }
976
+ | undefined;
977
+ const existing = ext.alarms.get(name);
978
+ if (existing) clearTimeout(existing.timer);
979
+ const delayMs = alarmInfo?.when !== undefined ? Math.max(0, alarmInfo.when - Date.now()) : (alarmInfo?.delayInMinutes ?? 0) * 60000;
980
+ const periodMs = alarmInfo?.periodInMinutes ? alarmInfo.periodInMinutes * 60000 : null;
981
+ const scheduledTime = Date.now() + delayMs;
982
+ const fire = () => registry.broadcast(extId, (e) => e.alarmsOnAlarm, [{ name, scheduledTime, periodInMinutes: alarmInfo?.periodInMinutes }]);
983
+ const timer = periodMs
984
+ ? setTimeout(() => {
985
+ fire();
986
+ ext.alarms.set(name, { name, scheduledTime, periodInMinutes: alarmInfo?.periodInMinutes, timer: setInterval(fire, periodMs) });
987
+ }, delayMs)
988
+ : setTimeout(fire, delayMs);
989
+ ext.alarms.set(name, { name, scheduledTime, periodInMinutes: alarmInfo?.periodInMinutes, timer });
990
+ },
991
+ get: (name: string, cb?: (alarm: unknown) => void) => {
992
+ const alarm = ext.alarms.get(name);
993
+ cb?.(alarm ? { name, scheduledTime: alarm.scheduledTime, periodInMinutes: alarm.periodInMinutes } : null);
994
+ },
995
+ getAll: (cb?: (alarms: unknown[]) => void) => {
996
+ const result = ([...ext.alarms.values()].map((a) => ({ name: a.name, scheduledTime: a.scheduledTime, periodInMinutes: a.periodInMinutes })));
997
+ cb?.(result);
998
+ return Promise.resolve(result);
999
+ },
1000
+ clear: (name: string, cb?: (wasCleared: boolean) => void) => {
1001
+ const alarm = ext.alarms.get(name);
1002
+ if (alarm) clearTimeout(alarm.timer);
1003
+ const existed = ext.alarms.delete(name);
1004
+ cb?.(existed);
1005
+ },
1006
+ clearAll: (cb?: (wasCleared: boolean) => void) => {
1007
+ for (const alarm of ext.alarms.values()) clearTimeout(alarm.timer);
1008
+ ext.alarms.clear();
1009
+ cb?.(true);
1010
+ },
1011
+ onAlarm: events.alarmsOnAlarm.toApi(),
1012
+ };
1013
+
1014
+ const permissions = {
1015
+ request: (_perms: unknown, cb?: (granted: boolean) => void) => {
1016
+ cb?.(true);
1017
+ return Promise.resolve(true);
1018
+ },
1019
+ contains: (_perms: unknown, cb?: (has: boolean) => void) => {
1020
+ cb?.(true);
1021
+ return Promise.resolve(true);
1022
+ },
1023
+ getAll: (cb?: (perms: unknown) => void) => {
1024
+ const result = { permissions: ext.manifest.permissions ?? [], origins: ext.manifest.host_permissions ?? [] };
1025
+ cb?.(result);
1026
+ return Promise.resolve(result);
1027
+ },
1028
+ remove: (_perms: unknown, cb?: (removed: boolean) => void) => {
1029
+ cb?.(true);
1030
+ return Promise.resolve(true);
1031
+ },
1032
+ onAdded: new EventHub().toApi(),
1033
+ onRemoved: new EventHub().toApi(),
1034
+ };
1035
+
1036
+ const history = {
1037
+ search: (_query: { text?: string; maxResults?: number }, cb?: (results: unknown[]) => void) => {
1038
+ const result: unknown[] = [];
1039
+ cb?.(result);
1040
+ return Promise.resolve(result);
1041
+ },
1042
+ getVisits: (_details: unknown, cb?: (visits: unknown[]) => void) => {
1043
+ const result: unknown[] = [];
1044
+ cb?.(result);
1045
+ return Promise.resolve(result);
1046
+ },
1047
+ addUrl: (_details: unknown, cb?: () => void) => {
1048
+ cb?.();
1049
+ return Promise.resolve(undefined);
1050
+ },
1051
+ deleteUrl: (_details: unknown, cb?: () => void) => {
1052
+ cb?.();
1053
+ return Promise.resolve(undefined);
1054
+ },
1055
+ deleteAll: (cb?: () => void) => {
1056
+ cb?.();
1057
+ return Promise.resolve(undefined);
1058
+ },
1059
+ onVisited: new EventHub().toApi(),
1060
+ onVisitRemoved: new EventHub().toApi(),
1061
+ };
1062
+
1063
+ const bookmarks = {
1064
+ get: (_idOrList: unknown, cb?: (nodes: unknown[]) => void) => {
1065
+ const result: unknown[] = [];
1066
+ cb?.(result);
1067
+ return Promise.resolve(result);
1068
+ },
1069
+ getTree: (cb?: (tree: unknown[]) => void) => {
1070
+ const result = ([{ id: "0", title: "Bookmarks", children: [] }]);
1071
+ cb?.(result);
1072
+ return Promise.resolve(result);
1073
+ },
1074
+ search: (_query: unknown, cb?: (nodes: unknown[]) => void) => {
1075
+ const result: unknown[] = [];
1076
+ cb?.(result);
1077
+ return Promise.resolve(result);
1078
+ },
1079
+ create: (bookmark: { url?: string; title?: string }, cb?: (node: unknown) => void) => {
1080
+ const result = ({ id: "0", ...bookmark });
1081
+ cb?.(result);
1082
+ return Promise.resolve(result);
1083
+ },
1084
+ remove: (_id: string, cb?: () => void) => {
1085
+ cb?.();
1086
+ return Promise.resolve(undefined);
1087
+ },
1088
+ onCreated: new EventHub().toApi(),
1089
+ onRemoved: new EventHub().toApi(),
1090
+ onChanged: new EventHub().toApi(),
1091
+ };
1092
+
1093
+ const downloads = {
1094
+ download: (options: { url: string; filename?: string }, cb?: (id: number) => void) => {
1095
+ try {
1096
+ const doc = realm.document;
1097
+ const a = doc.createElement("a");
1098
+ a.href = options.url;
1099
+ a.download = options.filename ?? "";
1100
+ doc.body.appendChild(a);
1101
+ a.click();
1102
+ a.remove();
1103
+ } catch {
1104
+ }
1105
+ cb?.(1);
1106
+ },
1107
+ search: (_query: unknown, cb?: (items: unknown[]) => void) => {
1108
+ const result: unknown[] = [];
1109
+ cb?.(result);
1110
+ return Promise.resolve(result);
1111
+ },
1112
+ pause: (_id: number, cb?: () => void) => {
1113
+ cb?.();
1114
+ return Promise.resolve(undefined);
1115
+ },
1116
+ resume: (_id: number, cb?: () => void) => {
1117
+ cb?.();
1118
+ return Promise.resolve(undefined);
1119
+ },
1120
+ cancel: (_id: number, cb?: () => void) => {
1121
+ cb?.();
1122
+ return Promise.resolve(undefined);
1123
+ },
1124
+ onCreated: new EventHub().toApi(),
1125
+ onChanged: new EventHub().toApi(),
1126
+ };
1127
+
1128
+ const identity = {
1129
+ getAuthToken: (_details: unknown, cb?: (token: string | undefined) => void) => {
1130
+ const result = (undefined);
1131
+ cb?.(result);
1132
+ return Promise.resolve(result);
1133
+ },
1134
+ launchWebAuthFlow: (_details: unknown, cb?: (url: string | undefined) => void) => {
1135
+ const result = (undefined);
1136
+ cb?.(result);
1137
+ return Promise.resolve(result);
1138
+ },
1139
+ getRedirectURL: (path?: string) => `https://sapphire.invalid/${extId}/${path ?? ""}`,
1140
+ removeCachedAuthToken: (_details: unknown, cb?: () => void) => {
1141
+ cb?.();
1142
+ return Promise.resolve(undefined);
1143
+ },
1144
+ };
1145
+
1146
+ const commands = {
1147
+ getAll: (cb?: (commands: unknown[]) => void) => {
1148
+ const cmds = ext.manifest.commands ?? {};
1149
+ const list = Object.entries(cmds).map(([name, cmd]) => ({ name, description: cmd.description ?? "", shortcut: cmd.suggested_key?.default ?? "" }));
1150
+ cb?.(list);
1151
+ return Promise.resolve(list);
1152
+ },
1153
+ onCommand: events.commandsOnCommand.toApi(),
1154
+ };
1155
+
1156
+ const omnibox = {
1157
+ setDefaultSuggestion: (_suggestion: unknown) => {},
1158
+ onInputStarted: new EventHub().toApi(),
1159
+ onInputChanged: new EventHub().toApi(),
1160
+ onInputEntered: new EventHub().toApi(),
1161
+ onInputCancelled: new EventHub().toApi(),
1162
+ };
1163
+
1164
+ const proxy = {
1165
+ settings: {
1166
+ get: (_details: unknown, cb?: (r: unknown) => void) => {
1167
+ const result = ({ value: { mode: "direct" }, levelOfControl: "controlled_by_this_extension" });
1168
+ cb?.(result);
1169
+ return Promise.resolve(result);
1170
+ },
1171
+ set: (_details: unknown, cb?: () => void) => {
1172
+ cb?.();
1173
+ return Promise.resolve(undefined);
1174
+ },
1175
+ clear: (_details: unknown, cb?: () => void) => {
1176
+ cb?.();
1177
+ return Promise.resolve(undefined);
1178
+ },
1179
+ },
1180
+ onProxyError: new EventHub().toApi(),
1181
+ };
1182
+
1183
+ const system = {
1184
+ cpu: {
1185
+ getInfo: (cb?: (i: unknown) => void) => {
1186
+ const result = { numOfProcessors: 4, "arch-name": "x86-64", modelName: "Sapphire vCPU", features: [] };
1187
+ cb?.(result);
1188
+ return Promise.resolve(result);
1189
+ },
1190
+ },
1191
+ memory: {
1192
+ getInfo: (cb?: (i: unknown) => void) => {
1193
+ const result = { capacity: 8 * 1024 * 1024 * 1024, availableCapacity: 4 * 1024 * 1024 * 1024 };
1194
+ cb?.(result);
1195
+ return Promise.resolve(result);
1196
+ },
1197
+ },
1198
+ storage: {
1199
+ getInfo: (cb?: (i: unknown[]) => void) => {
1200
+ cb?.([]);
1201
+ return Promise.resolve([]);
1202
+ },
1203
+ },
1204
+ display: {
1205
+ getInfo: (cb?: (i: unknown[]) => void) =>
1206
+ cb?.([{ id: "0", isPrimary: true, isInternal: false, isEnabled: true, bounds: { left: 0, top: 0, width: realm.screen.width, height: realm.screen.height } }]),
1207
+ },
1208
+ };
1209
+
1210
+ const power = {
1211
+ requestKeepAwake: (_level?: string) => {},
1212
+ releaseKeepAwake: () => {},
1213
+ };
1214
+
1215
+ const management = {
1216
+ getSelf: (cb?: (info: unknown) => void) => {
1217
+ const result = ({ id: extId, name: ext.manifest.name, version: ext.manifest.version ?? "", enabled: ext.enabled, type: "extension" });
1218
+ cb?.(result);
1219
+ return Promise.resolve(result);
1220
+ },
1221
+ getAll: (cb?: (all: unknown[]) => void) =>
1222
+ cb?.(registry.list().map((e) => ({ id: e.id, name: e.manifest.name, version: e.manifest.version ?? "", enabled: e.enabled, type: "extension" }))),
1223
+ setEnabled: (_id: string, _enabled: boolean, cb?: () => void) => {
1224
+ cb?.();
1225
+ return Promise.resolve(undefined);
1226
+ },
1227
+ uninstallSelf: (_options: unknown, cb?: () => void) => {
1228
+ cb?.();
1229
+ return Promise.resolve(undefined);
1230
+ },
1231
+ onEnabled: new EventHub().toApi(),
1232
+ onDisabled: new EventHub().toApi(),
1233
+ };
1234
+
1235
+ const webNavigation = {
1236
+ getFrame: (_details: unknown, cb?: (frame: unknown) => void) => {
1237
+ const result = (null);
1238
+ cb?.(result);
1239
+ return Promise.resolve(result);
1240
+ },
1241
+ getAllFrames: (_details: unknown, cb?: (frames: unknown[]) => void) => {
1242
+ const result: unknown[] = [];
1243
+ cb?.(result);
1244
+ return Promise.resolve(result);
1245
+ },
1246
+ onBeforeNavigate: new EventHub().toApi(),
1247
+ onCommitted: new EventHub().toApi(),
1248
+ onCompleted: events.webNavigationOnCompleted.toApi(),
1249
+ onDOMContentLoaded: new EventHub().toApi(),
1250
+ onErrorOccurred: new EventHub().toApi(),
1251
+ onHistoryStateUpdated: new EventHub().toApi(),
1252
+ onReferenceFragmentUpdated: new EventHub().toApi(),
1253
+ };
1254
+
1255
+ const tts = {
1256
+ speak: (utterance: string, options?: { lang?: string; rate?: number; pitch?: number; volume?: number }, cb?: () => void) => {
1257
+ try {
1258
+ const SpeechSynthesisUtteranceCtor = (realm as unknown as { SpeechSynthesisUtterance: typeof SpeechSynthesisUtterance }).SpeechSynthesisUtterance;
1259
+ const u = new SpeechSynthesisUtteranceCtor(utterance);
1260
+ if (options?.lang) u.lang = options.lang;
1261
+ if (options?.rate) u.rate = options.rate;
1262
+ if (options?.pitch) u.pitch = options.pitch;
1263
+ if (options?.volume) u.volume = options.volume;
1264
+ realm.speechSynthesis.speak(u);
1265
+ } catch {
1266
+ }
1267
+ cb?.();
1268
+ },
1269
+ stop: () => realm.speechSynthesis?.cancel(),
1270
+ isSpeaking: (cb?: (speaking: boolean) => void) => {
1271
+ const result = (realm.speechSynthesis?.speaking ?? false);
1272
+ cb?.(result);
1273
+ return Promise.resolve(result);
1274
+ },
1275
+ getVoices: (cb?: (voices: unknown[]) => void) =>
1276
+ cb?.((realm.speechSynthesis?.getVoices() ?? []).map((v) => ({ voiceName: v.name, lang: v.lang, remote: false, extensionId: "" }))),
1277
+ onEvent: new EventHub().toApi(),
1278
+ };
1279
+
1280
+ const clipboard = {
1281
+ setImageData: (_imageData: unknown, _type: string, cb?: () => void) => {
1282
+ cb?.();
1283
+ return Promise.resolve(undefined);
1284
+ },
1285
+ };
1286
+
1287
+ const fontSettings = {
1288
+ getFont: (_details: unknown, cb?: (font: unknown) => void) => {
1289
+ const result = ({ fontId: "Arial", levelOfControl: "controllable_by_this_extension" });
1290
+ cb?.(result);
1291
+ return Promise.resolve(result);
1292
+ },
1293
+ setFont: (_details: unknown, cb?: () => void) => {
1294
+ cb?.();
1295
+ return Promise.resolve(undefined);
1296
+ },
1297
+ clearFont: (_details: unknown, cb?: () => void) => {
1298
+ cb?.();
1299
+ return Promise.resolve(undefined);
1300
+ },
1301
+ getFontList: (cb?: (fonts: { fontId: string; displayName: string }[]) => void) => {
1302
+ const fonts = [
1303
+ "Arial",
1304
+ "Arial Black",
1305
+ "Comic Sans MS",
1306
+ "Courier New",
1307
+ "Georgia",
1308
+ "Impact",
1309
+ "Segoe UI",
1310
+ "Tahoma",
1311
+ "Times New Roman",
1312
+ "Trebuchet MS",
1313
+ "Verdana",
1314
+ ].map((fontId) => ({ fontId, displayName: fontId }));
1315
+ cb?.(fonts);
1316
+ return Promise.resolve(fonts);
1317
+ },
1318
+ onFontChanged: new EventHub().toApi(),
1319
+ };
1320
+
1321
+ const extensionNs = {
1322
+ getURL: runtime.getURL,
1323
+ getBackgroundPage: () => null,
1324
+ isAllowedIncognitoAccess: (cb?: (allowed: boolean) => void) => {
1325
+ cb?.(false);
1326
+ return Promise.resolve(false);
1327
+ },
1328
+ isAllowedFileSchemeAccess: (cb?: (allowed: boolean) => void) => {
1329
+ cb?.(false);
1330
+ return Promise.resolve(false);
1331
+ },
1332
+ onMessage: runtime.onMessage,
1333
+ onMessageExternal: new EventHub().toApi(),
1334
+ sendMessage: runtime.sendMessage,
1335
+ };
1336
+
1337
+ const chromeApi = {
1338
+ runtime,
1339
+ storage,
1340
+ tabs,
1341
+ windows,
1342
+ extension: extensionNs,
1343
+ i18n,
1344
+ contextMenus,
1345
+ notifications,
1346
+ cookies,
1347
+ webRequest,
1348
+ declarativeNetRequest,
1349
+ scripting,
1350
+ action,
1351
+ browserAction,
1352
+ pageAction,
1353
+ alarms,
1354
+ permissions,
1355
+ history,
1356
+ bookmarks,
1357
+ downloads,
1358
+ identity,
1359
+ commands,
1360
+ omnibox,
1361
+ contentSettings: {},
1362
+ proxy,
1363
+ system,
1364
+ power,
1365
+ management,
1366
+ webNavigation,
1367
+ tts,
1368
+ clipboard,
1369
+ fontSettings,
1370
+ app: {
1371
+ getDetails: () => null,
1372
+ isInstalled: false,
1373
+ InstallState: { DISABLED: "disabled", INSTALLED: "installed", NOT_INSTALLED: "not_installed" },
1374
+ RunningState: { CANNOT_RUN: "cannot_run", READY_TO_RUN: "ready_to_run", RUNNING: "running" },
1375
+ },
1376
+ csi: () => ({}),
1377
+ loadTimes: () => ({}),
1378
+ };
1379
+
1380
+ const chromeApiWithFallback = (globalThis as { SAPPHIRE_TRACE?: boolean }).SAPPHIRE_TRACE
1381
+ ? traceCalls(withMissingMemberFallback(chromeApi))
1382
+ : withMissingMemberFallback(chromeApi);
1383
+
1384
+ (realm as unknown as { chrome: unknown }).chrome = chromeApiWithFallback;
1385
+ if (!(realm as unknown as { browser?: unknown }).browser) {
1386
+ (realm as unknown as { browser: unknown }).browser = chromeApiWithFallback;
1387
+ }
1388
+
1389
+ return events;
1390
+ }