@mercuryworkshop/proxy-bootstrap 0.0.1

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.
Files changed (102) hide show
  1. package/dist/.downloads/controller/package/dist/controller.api.js +44 -0
  2. package/dist/.downloads/controller/package/dist/controller.api.js.map +1 -0
  3. package/dist/.downloads/controller/package/dist/controller.inject.js +2 -0
  4. package/dist/.downloads/controller/package/dist/controller.inject.js.map +1 -0
  5. package/dist/.downloads/controller/package/dist/controller.sw.js +2 -0
  6. package/dist/.downloads/controller/package/dist/controller.sw.js.map +1 -0
  7. package/dist/.downloads/controller/package/dist/types/cache.d.ts +39 -0
  8. package/dist/.downloads/controller/package/dist/types/index.d.ts +74 -0
  9. package/dist/.downloads/controller/package/dist/types/inject.d.ts +16 -0
  10. package/dist/.downloads/controller/package/dist/types/sw.d.ts +2 -0
  11. package/dist/.downloads/controller/package/dist/types/symbols.d.ts +1 -0
  12. package/dist/.downloads/controller/package/dist/types/typesEntry.d.ts +5 -0
  13. package/dist/.downloads/controller/package/package.json +16 -0
  14. package/dist/.downloads/controller/package/src/cache.ts +473 -0
  15. package/dist/.downloads/controller/package/src/index.ts +809 -0
  16. package/dist/.downloads/controller/package/src/inject.ts +370 -0
  17. package/dist/.downloads/controller/package/src/sw.ts +231 -0
  18. package/dist/.downloads/controller/package/src/symbols.ts +1 -0
  19. package/dist/.downloads/controller/package/src/types.d.ts +139 -0
  20. package/dist/.downloads/controller/package/src/typesEntry.ts +6 -0
  21. package/dist/.downloads/controller/package/tsconfig.json +24 -0
  22. package/dist/.downloads/controller/package/tsconfig.types.json +16 -0
  23. package/dist/.downloads/libcurl-transport/package/LICENSE +661 -0
  24. package/dist/.downloads/libcurl-transport/package/README.md +52 -0
  25. package/dist/.downloads/libcurl-transport/package/dist/index.d.ts +25 -0
  26. package/dist/.downloads/libcurl-transport/package/dist/index.js +6500 -0
  27. package/dist/.downloads/libcurl-transport/package/dist/index.mjs +6481 -0
  28. package/dist/.downloads/libcurl-transport/package/package.json +37 -0
  29. package/dist/.downloads/scramjet/package/dist/167400cb144aab22.wasm +0 -0
  30. package/dist/.downloads/scramjet/package/dist/2919e49b986edf8c.wasm +0 -0
  31. package/dist/.downloads/scramjet/package/dist/5aed1d5e48aab205.wasm +0 -0
  32. package/dist/.downloads/scramjet/package/dist/882d77912a3c8e3a.wasm +0 -0
  33. package/dist/.downloads/scramjet/package/dist/ac6aa30297a80464.wasm +0 -0
  34. package/dist/.downloads/scramjet/package/dist/c10a57758af882c8.wasm +0 -0
  35. package/dist/.downloads/scramjet/package/dist/cfd04aaae6955b67.wasm +0 -0
  36. package/dist/.downloads/scramjet/package/dist/d06a90fd413b36cf.wasm +0 -0
  37. package/dist/.downloads/scramjet/package/dist/dda06914899a6c28.wasm +0 -0
  38. package/dist/.downloads/scramjet/package/dist/scramjet.js +34 -0
  39. package/dist/.downloads/scramjet/package/dist/scramjet.js.map +1 -0
  40. package/dist/.downloads/scramjet/package/dist/scramjet.mjs +34 -0
  41. package/dist/.downloads/scramjet/package/dist/scramjet.mjs.map +1 -0
  42. package/dist/.downloads/scramjet/package/dist/scramjet.wasm +0 -0
  43. package/dist/.downloads/scramjet/package/dist/scramjet_bundled.js +34 -0
  44. package/dist/.downloads/scramjet/package/dist/scramjet_bundled.js.map +1 -0
  45. package/dist/.downloads/scramjet/package/dist/scramjet_bundled.mjs +34 -0
  46. package/dist/.downloads/scramjet/package/dist/scramjet_bundled.mjs.map +1 -0
  47. package/dist/.downloads/scramjet/package/dist/types/Tap.d.ts +32 -0
  48. package/dist/.downloads/scramjet/package/dist/types/client/client.d.ts +115 -0
  49. package/dist/.downloads/scramjet/package/dist/types/client/entry.d.ts +5 -0
  50. package/dist/.downloads/scramjet/package/dist/types/client/events.d.ts +10 -0
  51. package/dist/.downloads/scramjet/package/dist/types/client/global.d.ts +4 -0
  52. package/dist/.downloads/scramjet/package/dist/types/client/helpers.d.ts +1 -0
  53. package/dist/.downloads/scramjet/package/dist/types/client/index.d.ts +7 -0
  54. package/dist/.downloads/scramjet/package/dist/types/client/location.d.ts +2 -0
  55. package/dist/.downloads/scramjet/package/dist/types/client/shared/eval.d.ts +3 -0
  56. package/dist/.downloads/scramjet/package/dist/types/client/shared/sourcemaps.d.ts +19 -0
  57. package/dist/.downloads/scramjet/package/dist/types/client/shared/unproxy.d.ts +19 -0
  58. package/dist/.downloads/scramjet/package/dist/types/client/shared/wrap.d.ts +4 -0
  59. package/dist/.downloads/scramjet/package/dist/types/client/singletonbox.d.ts +16 -0
  60. package/dist/.downloads/scramjet/package/dist/types/client/unproxy.generated.d.ts +50 -0
  61. package/dist/.downloads/scramjet/package/dist/types/fetch/body.d.ts +3 -0
  62. package/dist/.downloads/scramjet/package/dist/types/fetch/fetch.d.ts +7 -0
  63. package/dist/.downloads/scramjet/package/dist/types/fetch/headers.d.ts +19 -0
  64. package/dist/.downloads/scramjet/package/dist/types/fetch/index.d.ts +128 -0
  65. package/dist/.downloads/scramjet/package/dist/types/fetch/parse.d.ts +22 -0
  66. package/dist/.downloads/scramjet/package/dist/types/fetch/util.d.ts +7 -0
  67. package/dist/.downloads/scramjet/package/dist/types/index.d.ts +11 -0
  68. package/dist/.downloads/scramjet/package/dist/types/shared/cookie.d.ts +26 -0
  69. package/dist/.downloads/scramjet/package/dist/types/shared/headers.d.ts +13 -0
  70. package/dist/.downloads/scramjet/package/dist/types/shared/htmlRules.d.ts +6 -0
  71. package/dist/.downloads/scramjet/package/dist/types/shared/index.d.ts +51 -0
  72. package/dist/.downloads/scramjet/package/dist/types/shared/mime.d.ts +39 -0
  73. package/dist/.downloads/scramjet/package/dist/types/shared/refresh.d.ts +7 -0
  74. package/dist/.downloads/scramjet/package/dist/types/shared/rewriters/css.d.ts +4 -0
  75. package/dist/.downloads/scramjet/package/dist/types/shared/rewriters/html.d.ts +33 -0
  76. package/dist/.downloads/scramjet/package/dist/types/shared/rewriters/index.d.ts +6 -0
  77. package/dist/.downloads/scramjet/package/dist/types/shared/rewriters/js.d.ts +11 -0
  78. package/dist/.downloads/scramjet/package/dist/types/shared/rewriters/url.d.ts +25 -0
  79. package/dist/.downloads/scramjet/package/dist/types/shared/rewriters/wasm.d.ts +7 -0
  80. package/dist/.downloads/scramjet/package/dist/types/shared/rewriters/worker.d.ts +3 -0
  81. package/dist/.downloads/scramjet/package/dist/types/shared/set-cookie-parser.d.ts +20 -0
  82. package/dist/.downloads/scramjet/package/dist/types/shared/snapshot.d.ts +236 -0
  83. package/dist/.downloads/scramjet/package/dist/types/shared/sniffEncoding.d.ts +65 -0
  84. package/dist/.downloads/scramjet/package/dist/types/shared/util.d.ts +2 -0
  85. package/dist/.downloads/scramjet/package/dist/types/symbols.d.ts +6 -0
  86. package/dist/.downloads/scramjet/package/dist/types/types.d.ts +68 -0
  87. package/dist/.downloads/scramjet/package/lib/index.cjs +7 -0
  88. package/dist/.downloads/scramjet/package/lib/index.d.ts +8 -0
  89. package/dist/.downloads/scramjet/package/lib/types.d.ts +20 -0
  90. package/dist/.downloads/scramjet/package/package.json +93 -0
  91. package/dist/bootstrap-client.js +169 -0
  92. package/dist/bootstrap-client.js.map +1 -0
  93. package/dist/bootstrap-server.js +406 -0
  94. package/dist/bootstrap-server.js.map +1 -0
  95. package/dist/bootstrap-static.js +476 -0
  96. package/dist/bootstrap-static.js.map +1 -0
  97. package/dist/types/client.d.ts +4 -0
  98. package/dist/types/clientcommon.d.ts +2 -0
  99. package/dist/types/common.d.ts +30 -0
  100. package/dist/types/server.d.ts +24 -0
  101. package/dist/types/static.d.ts +1 -0
  102. package/package.json +30 -0
@@ -0,0 +1,809 @@
1
+ import { type MethodsDefinition, RpcHelper } from "@mercuryworkshop/rpc";
2
+ import {
3
+ BareResponse,
4
+ type ProxyTransport,
5
+ } from "@mercuryworkshop/proxy-transports";
6
+ import type * as ScramjetGlobal from "@mercuryworkshop/scramjet";
7
+ declare const $scramjet: typeof ScramjetGlobal;
8
+ import { deepmerge } from "@fastify/deepmerge";
9
+ import { CONTROLLERFRAME } from "./symbols";
10
+ import type {
11
+ FrameInitHooks,
12
+ SerializedCookieSyncEntry,
13
+ TransportToController,
14
+ Controllerbound,
15
+ ControllerToTransport,
16
+ SWbound,
17
+ WebSocketMessage,
18
+ FrameErrorHooks,
19
+ } from "./types";
20
+
21
+ export { HttpCachePlugin, type HttpCachePluginOptions } from "./cache";
22
+
23
+ export type Config = {
24
+ prefix: string;
25
+ scramjetPath: string;
26
+ injectPath: string;
27
+ wasmPath: string;
28
+ virtualWasmPath: string;
29
+ codec: Record<"encode" | "decode", (input: string) => string>;
30
+ };
31
+
32
+ export const config: Config = {
33
+ prefix: "/~/sj/",
34
+ scramjetPath: "/scramjet/scramjet.js",
35
+ injectPath: "/controller/controller.inject.js",
36
+ wasmPath: "/scramjet/scramjet.wasm",
37
+ virtualWasmPath: "scramjet.wasm.js",
38
+ codec: {
39
+ encode: (url: string) => {
40
+ if (!url) return url;
41
+
42
+ return encodeURIComponent(url);
43
+ },
44
+ decode: (url: string) => {
45
+ if (!url) return url;
46
+
47
+ return decodeURIComponent(url);
48
+ },
49
+ },
50
+ };
51
+
52
+ const scramjetConfig: Partial<ScramjetGlobal.ScramjetConfig> = {
53
+ flags: {
54
+ ...$scramjet.defaultConfig.flags,
55
+ allowFailedIntercepts: true,
56
+ },
57
+ maskedfiles: ["inject.js", "scramjet.wasm.js"],
58
+ };
59
+
60
+ type PersistedCookieState = {
61
+ updatedAt: number;
62
+ cookies: string;
63
+ };
64
+
65
+ const COOKIE_DB_NAME = "__scramjet_controller";
66
+ const COOKIE_STORE_NAME = "state";
67
+ const COOKIE_STATE_KEY = "cookies";
68
+ const BROADCASTCHANNEL_NAME = "__scramjet_controller_channel";
69
+
70
+ let cookieDbPromise: Promise<IDBDatabase> | null = null;
71
+
72
+ function parsePersistedCookieState(
73
+ value: unknown
74
+ ): PersistedCookieState | null {
75
+ if (
76
+ typeof value !== "object" ||
77
+ value === null ||
78
+ typeof (value as PersistedCookieState).updatedAt !== "number" ||
79
+ !Number.isFinite((value as PersistedCookieState).updatedAt) ||
80
+ typeof (value as PersistedCookieState).cookies !== "string"
81
+ ) {
82
+ return null;
83
+ }
84
+
85
+ return value as PersistedCookieState;
86
+ }
87
+
88
+ function requestToPromise<T>(request: IDBRequest<T>): Promise<T> {
89
+ return new Promise<T>((resolve, reject) => {
90
+ request.onsuccess = () => resolve(request.result);
91
+ request.onerror = () =>
92
+ reject(request.error ?? new Error("IndexedDB request failed"));
93
+ });
94
+ }
95
+
96
+ function transactionToPromise(transaction: IDBTransaction): Promise<void> {
97
+ return new Promise<void>((resolve, reject) => {
98
+ transaction.oncomplete = () => resolve();
99
+ transaction.onabort = () =>
100
+ reject(transaction.error ?? new Error("IndexedDB transaction aborted"));
101
+ transaction.onerror = () =>
102
+ reject(transaction.error ?? new Error("IndexedDB transaction failed"));
103
+ });
104
+ }
105
+
106
+ function openCookieDatabase(): Promise<IDBDatabase> {
107
+ if (cookieDbPromise) {
108
+ return cookieDbPromise;
109
+ }
110
+
111
+ cookieDbPromise = new Promise<IDBDatabase>((resolve, reject) => {
112
+ const request = indexedDB.open(COOKIE_DB_NAME, 1);
113
+ request.onupgradeneeded = () => {
114
+ const db = request.result;
115
+ if (!db.objectStoreNames.contains(COOKIE_STORE_NAME)) {
116
+ db.createObjectStore(COOKIE_STORE_NAME);
117
+ }
118
+ };
119
+ request.onsuccess = () => resolve(request.result);
120
+ request.onerror = () =>
121
+ reject(request.error ?? new Error("Failed to open cookie database"));
122
+ });
123
+
124
+ return cookieDbPromise;
125
+ }
126
+
127
+ async function readCookieState(): Promise<PersistedCookieState | null> {
128
+ try {
129
+ const db = await openCookieDatabase();
130
+ const transaction = db.transaction(COOKIE_STORE_NAME, "readonly");
131
+ const store = transaction.objectStore(COOKIE_STORE_NAME);
132
+ const value = await requestToPromise(store.get(COOKIE_STATE_KEY));
133
+ await transactionToPromise(transaction);
134
+ return parsePersistedCookieState(value);
135
+ } catch (error) {
136
+ console.error("Failed to read persisted controller cookies:", error);
137
+ return null;
138
+ }
139
+ }
140
+
141
+ async function writeCookieState(
142
+ cookies: string,
143
+ currentUpdatedAt: number
144
+ ): Promise<number> {
145
+ try {
146
+ const db = await openCookieDatabase();
147
+ const transaction = db.transaction(COOKIE_STORE_NAME, "readwrite");
148
+ const store = transaction.objectStore(COOKIE_STORE_NAME);
149
+ const existing = parsePersistedCookieState(
150
+ await requestToPromise(store.get(COOKIE_STATE_KEY))
151
+ );
152
+ const updatedAt = Math.max(
153
+ Date.now(),
154
+ currentUpdatedAt + 1,
155
+ (existing?.updatedAt ?? 0) + 1
156
+ );
157
+ const state: PersistedCookieState = {
158
+ updatedAt,
159
+ cookies,
160
+ };
161
+ store.put(state, COOKIE_STATE_KEY);
162
+ await transactionToPromise(transaction);
163
+ return updatedAt;
164
+ } catch (error) {
165
+ console.error("Failed to persist controller cookies:", error);
166
+ return currentUpdatedAt;
167
+ }
168
+ }
169
+
170
+ function makeId(): string {
171
+ return Math.random().toString(36).substring(2, 10);
172
+ }
173
+
174
+ const deepMerge = deepmerge();
175
+
176
+ type ControllerInit = {
177
+ serviceworker: ServiceWorker;
178
+ transport: ProxyTransport;
179
+ config?: Partial<Config>;
180
+ scramjetConfig?: Partial<ScramjetGlobal.ScramjetConfig>;
181
+ };
182
+
183
+ export class Controller {
184
+ id: string;
185
+ config: Config;
186
+ scramjetConfig: ScramjetGlobal.ScramjetConfig;
187
+ prefix: string;
188
+ cookieJar = new $scramjet.CookieJar();
189
+ frames: Frame[] = [];
190
+ serviceWorkerController: ServiceWorker;
191
+ guardServiceWorkerRevive = true;
192
+
193
+ private ready: Promise<void>;
194
+ private readyResolve!: () => void;
195
+ public isReady: boolean = false;
196
+ rpc: RpcHelper<Controllerbound, SWbound>;
197
+ private port: MessagePort | null = null;
198
+
199
+ transport: ProxyTransport;
200
+ private cookieUpdatedAt = 0;
201
+ private cookieSyncPromise: Promise<void> | null = null;
202
+ private cookieSyncDirty = true;
203
+ private cookieSyncChannel = new BroadcastChannel(BROADCASTCHANNEL_NAME);
204
+
205
+ private wasmAlreadyFetched = false;
206
+ private wasmPayload: string | null = null;
207
+ private onTabChannelMessage: (e: MessageEvent) => void = (e) => {
208
+ this.rpc.recieve(e.data);
209
+ };
210
+ private onCookieSyncMessage = (event: MessageEvent) => {
211
+ const updatedAt =
212
+ typeof event.data === "object" && event.data !== null
213
+ ? (event.data as { updatedAt?: unknown }).updatedAt
214
+ : undefined;
215
+ if (typeof updatedAt !== "number" || updatedAt <= this.cookieUpdatedAt) {
216
+ return;
217
+ }
218
+
219
+ this.cookieSyncDirty = true;
220
+ void this.loadSavedCookies();
221
+ };
222
+
223
+ private async loadScramjetWasm() {
224
+ if (this.wasmAlreadyFetched) {
225
+ return;
226
+ }
227
+
228
+ const resp = await fetch(this.config.wasmPath);
229
+ $scramjet.setWasm(await resp.arrayBuffer());
230
+ this.wasmAlreadyFetched = true;
231
+ }
232
+
233
+ private methods: MethodsDefinition<Controllerbound> = {
234
+ ready: async () => {
235
+ this.readyResolve();
236
+ setTimeout(() => {
237
+ this.guardServiceWorkerRevive = false;
238
+ }, 5000);
239
+ },
240
+ request: async (data) => {
241
+ const path = new URL(data.rawUrl).pathname;
242
+ const frame = this.frames.find((f) => path.startsWith(f.prefix));
243
+ if (!frame) throw new Error("No frame found for request");
244
+ try {
245
+ // doesn't actually *load* every request, but hold up requests until the promise finishes
246
+ await this.loadSavedCookies();
247
+
248
+ if (path === frame.prefix + this.config.virtualWasmPath) {
249
+ if (!this.wasmPayload) {
250
+ const resp = await fetch(this.config.wasmPath);
251
+ const buf = await resp.arrayBuffer();
252
+ const b64 = btoa(
253
+ new Uint8Array(buf)
254
+ .reduce(
255
+ (data, byte) => (data.push(String.fromCharCode(byte)), data),
256
+ [] as any
257
+ )
258
+ .join("")
259
+ );
260
+
261
+ this.wasmPayload = `self.WASM = '${b64}';`;
262
+ }
263
+
264
+ return [
265
+ {
266
+ body: this.wasmPayload,
267
+ status: 200,
268
+ statusText: "OK",
269
+ headers: [["Content-Type", "application/javascript"]],
270
+ },
271
+ [],
272
+ ];
273
+ }
274
+
275
+ const sjheaders = $scramjet.ScramjetHeaders.fromRawHeaders(
276
+ data.initialHeaders
277
+ );
278
+
279
+ const fetchresponse = await frame.fetchHandler.handleFetch({
280
+ initialHeaders: sjheaders,
281
+ rawClientUrl: data.rawClientUrl
282
+ ? new URL(data.rawClientUrl)
283
+ : undefined,
284
+ rawUrl: new URL(data.rawUrl),
285
+ rawReferrer: data.rawReferrer,
286
+ rawDestination: data.destination,
287
+ method: data.method,
288
+ mode: data.mode,
289
+ referrer: data.referrer,
290
+ body: data.body,
291
+ cache: data.cache,
292
+ clientId: data.clientId,
293
+ });
294
+
295
+ return [
296
+ {
297
+ body: fetchresponse.body,
298
+ status: fetchresponse.status,
299
+ statusText: fetchresponse.statusText,
300
+ headers: fetchresponse.headers.toRawHeaders(),
301
+ },
302
+ fetchresponse.body instanceof ReadableStream ||
303
+ fetchresponse.body instanceof ArrayBuffer
304
+ ? [fetchresponse.body]
305
+ : [],
306
+ ];
307
+ } catch (e) {
308
+ const reqcontext: typeof frame.hooks.error.request.context = {
309
+ rawrequest: data,
310
+ };
311
+ const reqprops: typeof frame.hooks.error.request.props = {
312
+ setResponse: undefined,
313
+ suppressError: false,
314
+ };
315
+ await $scramjet.Tap.dispatch(
316
+ frame.hooks.error.request,
317
+ reqcontext,
318
+ reqprops
319
+ );
320
+ if (!reqprops.suppressError) {
321
+ console.error("Error in controller request handler:", e);
322
+ }
323
+ if (reqprops.setResponse) {
324
+ return [reqprops.setResponse, []];
325
+ }
326
+ throw e;
327
+ }
328
+ },
329
+ initRemoteTransport: async (port) => {
330
+ const rpc = new RpcHelper<TransportToController, ControllerToTransport>(
331
+ {
332
+ request: async ({ remote, method, body, headers }) => {
333
+ const response = await this.transport.request(
334
+ new URL(remote),
335
+ method,
336
+ body,
337
+ headers,
338
+ undefined
339
+ );
340
+ return [response, [response.body]];
341
+ },
342
+ sendSetCookie: async ({ cookies, options }) => {
343
+ await this.loadSavedCookies(true);
344
+ if (options?.clear) {
345
+ this.cookieJar.clear();
346
+ }
347
+ this.applyCookieSyncEntries(cookies);
348
+ await this.persistCookies();
349
+ await this.propagateCookieSync(cookies, options);
350
+ },
351
+ connect: async ({ url, protocols, requestHeaders, port }) => {
352
+ let resolve: (arg: TransportToController["connect"][1]) => void;
353
+ const promise = new Promise<TransportToController["connect"][1]>(
354
+ (res) => (resolve = res)
355
+ );
356
+ const [send, close] = this.transport.connect(
357
+ new URL(url),
358
+ protocols,
359
+ requestHeaders,
360
+ (protocol, extensions) => {
361
+ resolve({
362
+ result: "success",
363
+ protocol: protocol,
364
+ extensions: extensions,
365
+ });
366
+ },
367
+ (data) => {
368
+ port.postMessage(
369
+ {
370
+ type: "data",
371
+ data: data,
372
+ } as WebSocketMessage,
373
+ data instanceof ArrayBuffer ? [data] : []
374
+ );
375
+ },
376
+ (close, reason) => {
377
+ port.postMessage({
378
+ type: "close",
379
+ code: close,
380
+ reason: reason,
381
+ } as WebSocketMessage);
382
+ },
383
+ (error) => {
384
+ resolve({
385
+ result: "failure",
386
+ error: error,
387
+ });
388
+ }
389
+ );
390
+ port.onmessageerror = (ev) => {
391
+ console.error(
392
+ "Transport port messageerror (this should never happen!)",
393
+ ev
394
+ );
395
+ };
396
+ port.onmessage = ({ data }: { data: WebSocketMessage }) => {
397
+ if (data.type === "data") {
398
+ send(data.data);
399
+ } else if (data.type === "close") {
400
+ close(data.code, data.reason);
401
+ }
402
+ };
403
+
404
+ return [await promise, []];
405
+ },
406
+ },
407
+ "transport",
408
+ (data, transfer) => port.postMessage(data, transfer)
409
+ );
410
+ port.onmessageerror = (ev) => {
411
+ console.error(
412
+ "Transport port messageerror (this should never happen!)",
413
+ ev
414
+ );
415
+ };
416
+ port.onmessage = (e) => {
417
+ rpc.recieve(e.data);
418
+ };
419
+ rpc.call("ready", undefined, []);
420
+ },
421
+ };
422
+
423
+ constructor(public init: ControllerInit) {
424
+ this.id = makeId();
425
+ this.config = deepMerge(config, init.config || {}) as Config;
426
+ this.scramjetConfig = deepMerge(scramjetConfig, $scramjet.defaultConfig);
427
+ this.scramjetConfig = deepMerge(
428
+ this.scramjetConfig,
429
+ init.scramjetConfig || {}
430
+ ) as ScramjetGlobal.ScramjetConfig;
431
+ this.prefix = this.config.prefix + this.id + "/";
432
+ this.serviceWorkerController = init.serviceworker;
433
+
434
+ this.ready = Promise.all([
435
+ new Promise<void>((resolve) => {
436
+ this.readyResolve = resolve;
437
+ }),
438
+ this.loadScramjetWasm(),
439
+ this.loadSavedCookies(true),
440
+ ]).then(() => undefined);
441
+
442
+ this.rpc = new RpcHelper<Controllerbound, SWbound>(
443
+ this.methods,
444
+ "tabchannel-" + this.id,
445
+ (data, transfer) => {
446
+ if (!this.port) {
447
+ throw new Error("Port not found");
448
+ }
449
+ this.port.postMessage(data, transfer);
450
+ }
451
+ );
452
+ this.transport = init.transport;
453
+
454
+ this.cookieSyncChannel.addEventListener(
455
+ "message",
456
+ this.onCookieSyncMessage
457
+ );
458
+ this.setupMessagePort();
459
+
460
+ navigator.serviceWorker.addEventListener("message", (e) => {
461
+ if (
462
+ e.data?.$controller$setCookie &&
463
+ typeof e.data.$controller$setCookie === "object"
464
+ ) {
465
+ const payload = e.data.$controller$setCookie as {
466
+ cookies?: SerializedCookieSyncEntry[];
467
+ options?: ScramjetGlobal.CookieSyncOptions;
468
+ id?: string;
469
+ };
470
+
471
+ if (payload.options?.clear) {
472
+ this.cookieJar.clear();
473
+ }
474
+ this.applyCookieSyncEntries(payload.cookies);
475
+
476
+ if (typeof payload.id === "string") {
477
+ this.serviceWorkerController.postMessage({
478
+ $sw$setCookieDone: {
479
+ id: payload.id,
480
+ },
481
+ });
482
+ }
483
+
484
+ return;
485
+ }
486
+
487
+ if (e.data.$controller$swrevive) {
488
+ // if we just spawned the service worker, it will send this even though it's not actually dead
489
+ // TODO: pretty jank, fix at some point
490
+ if (this.guardServiceWorkerRevive) {
491
+ return;
492
+ }
493
+ this.setupMessagePort();
494
+ }
495
+ });
496
+ }
497
+
498
+ private setupMessagePort() {
499
+ if (this.port) {
500
+ this.port.removeEventListener("message", this.onTabChannelMessage);
501
+ try {
502
+ this.port.close();
503
+ } catch {
504
+ // ignore
505
+ }
506
+ this.port = null;
507
+ }
508
+
509
+ const channel = new MessageChannel();
510
+ this.port = channel.port1;
511
+ this.port.addEventListener("message", this.onTabChannelMessage);
512
+ this.port.start();
513
+
514
+ this.serviceWorkerController.postMessage(
515
+ {
516
+ $controller$init: {
517
+ prefix: this.prefix,
518
+ id: this.id,
519
+ },
520
+ },
521
+ [channel.port2]
522
+ );
523
+ }
524
+
525
+ // TODO: should this be a method on the cookie jar?
526
+ private applyCookieSyncEntries(
527
+ cookies: SerializedCookieSyncEntry[] | undefined
528
+ ) {
529
+ if (!Array.isArray(cookies)) {
530
+ return;
531
+ }
532
+
533
+ for (const entry of cookies) {
534
+ if (typeof entry?.url !== "string" || typeof entry.cookie !== "string") {
535
+ continue;
536
+ }
537
+
538
+ this.cookieJar.setCookies(entry.cookie, new URL(entry.url));
539
+ }
540
+ }
541
+
542
+ async propagateCookieSync(
543
+ cookies: SerializedCookieSyncEntry[],
544
+ options: ScramjetGlobal.CookieSyncOptions = {}
545
+ ): Promise<void> {
546
+ if (!this.port) {
547
+ return;
548
+ }
549
+
550
+ await this.rpc.call("sendSetCookie", {
551
+ cookies,
552
+ options,
553
+ });
554
+ }
555
+
556
+ private async loadSavedCookies(force = false): Promise<void> {
557
+ if (!force && !this.cookieSyncDirty) {
558
+ return;
559
+ }
560
+
561
+ if (this.cookieSyncPromise) {
562
+ return this.cookieSyncPromise;
563
+ }
564
+
565
+ this.cookieSyncPromise = (async () => {
566
+ const persisted = await readCookieState();
567
+ if (persisted && persisted.updatedAt > this.cookieUpdatedAt) {
568
+ this.cookieJar.load(persisted.cookies);
569
+ this.cookieUpdatedAt = persisted.updatedAt;
570
+ }
571
+ this.cookieSyncDirty = false;
572
+ })().finally(() => {
573
+ this.cookieSyncPromise = null;
574
+ });
575
+
576
+ return this.cookieSyncPromise;
577
+ }
578
+
579
+ async persistCookies(): Promise<void> {
580
+ const updatedAt = await writeCookieState(
581
+ this.cookieJar.dump(),
582
+ this.cookieUpdatedAt
583
+ );
584
+ if (updatedAt <= this.cookieUpdatedAt) {
585
+ return;
586
+ }
587
+
588
+ this.cookieUpdatedAt = updatedAt;
589
+ this.cookieSyncDirty = false;
590
+ this.cookieSyncChannel.postMessage({
591
+ updatedAt,
592
+ });
593
+ }
594
+
595
+ setTransport(transport: ProxyTransport) {
596
+ this.transport = transport;
597
+ for (const frame of this.frames) {
598
+ frame.controller.transport = transport;
599
+ frame.fetchHandler.client.transport = transport;
600
+ }
601
+ }
602
+
603
+ createFrame(element?: HTMLIFrameElement): Frame {
604
+ if (!this.ready) {
605
+ throw new Error(
606
+ "Controller is not ready! Try awaiting controller.wait()"
607
+ );
608
+ }
609
+ element ??= document.createElement("iframe");
610
+ const frame = new Frame(this, element);
611
+ this.frames.push(frame);
612
+ return frame;
613
+ }
614
+
615
+ async wait(): Promise<void> {
616
+ await this.ready;
617
+ }
618
+ }
619
+
620
+ function base64Encode(text: string) {
621
+ return btoa(
622
+ new TextEncoder()
623
+ .encode(text)
624
+ .reduce(
625
+ (data, byte) => (data.push(String.fromCharCode(byte)), data),
626
+ [] as any
627
+ )
628
+ .join("")
629
+ );
630
+ }
631
+
632
+ function yieldGetInjectScripts(
633
+ config: Config,
634
+ sjconfig: ScramjetGlobal.ScramjetConfig,
635
+ prefix: URL,
636
+ cookieJar: ScramjetGlobal.CookieJar,
637
+ codecEncode: (input: string) => string,
638
+ codecDecode: (input: string) => string
639
+ ) {
640
+ const getInjectScripts: ScramjetGlobal.ScramjetInterface["getInjectScripts"] =
641
+ (meta, handler, htmlcontext, script) => {
642
+ function base64Encode(text: string) {
643
+ return btoa(
644
+ new TextEncoder()
645
+ .encode(text)
646
+ .reduce(
647
+ (data, byte) => (data.push(String.fromCharCode(byte)), data),
648
+ [] as any
649
+ )
650
+ .join("")
651
+ );
652
+ }
653
+ return [
654
+ script(config.scramjetPath),
655
+ script(prefix.href + config.virtualWasmPath),
656
+ script(config.injectPath),
657
+ script(
658
+ "data:text/javascript;charset=utf-8;base64," +
659
+ base64Encode(`
660
+ document.querySelectorAll("script[scramjet-injected]").forEach(script => script.remove());
661
+ $scramjetController.load({
662
+ config: ${JSON.stringify(config)},
663
+ sjconfig: ${JSON.stringify(sjconfig)},
664
+ prefix: new URL("${prefix.href}"),
665
+ cookies: ${JSON.stringify(cookieJar.dump())},
666
+ yieldGetInjectScripts: ${yieldGetInjectScripts.toString()},
667
+ codecEncode: ${codecEncode.toString()},
668
+ codecDecode: ${codecDecode.toString()},
669
+ initHeaders: ${JSON.stringify(htmlcontext.headers ?? [])},
670
+ history: ${JSON.stringify(htmlcontext.history ?? [])},
671
+ })
672
+ `)
673
+ ),
674
+ ];
675
+ };
676
+ return getInjectScripts;
677
+ }
678
+
679
+ export class Frame {
680
+ id: string;
681
+ prefix: string;
682
+ fetchHandler: ScramjetGlobal.ScramjetFetchHandler;
683
+ hooks: {
684
+ fetch: ScramjetGlobal.FetchHooks;
685
+ init: FrameInitHooks;
686
+ error: FrameErrorHooks;
687
+ };
688
+
689
+ get context(): ScramjetGlobal.ScramjetContext {
690
+ return {
691
+ config: this.controller.scramjetConfig,
692
+ prefix: new URL(this.prefix, location.href),
693
+ cookieJar: this.controller.cookieJar,
694
+ interface: {
695
+ getInjectScripts: yieldGetInjectScripts(
696
+ this.controller.config,
697
+ this.controller.scramjetConfig,
698
+ new URL(this.prefix, location.href),
699
+ this.controller.cookieJar,
700
+ this.controller.config.codec.encode,
701
+ this.controller.config.codec.decode
702
+ ),
703
+ getWorkerInjectScripts: (meta, type, script) => {
704
+ let str = "";
705
+
706
+ str += script(this.controller.config.scramjetPath);
707
+ str += script(this.prefix + this.controller.config.virtualWasmPath);
708
+ str += script(
709
+ "data:text/javascript;charset=utf-8;base64," +
710
+ base64Encode(`
711
+ (()=>{
712
+ const { ScramjetClient, CookieJar, setWasm } = $scramjet;
713
+
714
+ setWasm(Uint8Array.from(atob(self.WASM), (c) => c.charCodeAt(0)));
715
+ delete self.WASM;
716
+
717
+ const sjconfig = ${JSON.stringify(this.controller.scramjetConfig)};
718
+ const prefix = new URL("${this.prefix}", location.href);
719
+
720
+ const context = {
721
+ config: sjconfig,
722
+ prefix,
723
+ interface: {
724
+ codecEncode: ${this.controller.config.codec.encode.toString()},
725
+ codecDecode: ${this.controller.config.codec.decode.toString()},
726
+ },
727
+ };
728
+
729
+ const client = new ScramjetClient(globalThis, {
730
+ context,
731
+ transport: null,
732
+ shouldPassthroughWebsocket: (url) => {
733
+ return false;
734
+ }
735
+ });
736
+
737
+ client.hook();
738
+ })();
739
+ `)
740
+ );
741
+
742
+ return str;
743
+ },
744
+ codecEncode: this.controller.config.codec.encode,
745
+ codecDecode: this.controller.config.codec.decode,
746
+ },
747
+ };
748
+ }
749
+
750
+ constructor(
751
+ public controller: Controller,
752
+ public element: HTMLIFrameElement
753
+ ) {
754
+ this.id = makeId();
755
+ this.prefix = this.controller.prefix + this.id + "/";
756
+
757
+ this.fetchHandler = new $scramjet.ScramjetFetchHandler({
758
+ crossOriginIsolated: self.crossOriginIsolated,
759
+ context: this.context,
760
+ transport: controller.transport,
761
+ async sendSetCookie(cookies, options) {
762
+ await controller.persistCookies();
763
+ await controller.propagateCookieSync(
764
+ cookies.map(({ url, cookie }) => ({
765
+ url: url.href,
766
+ cookie,
767
+ })),
768
+ options
769
+ );
770
+ },
771
+ async fetchBlobUrl(url) {
772
+ return BareResponse.fromNativeResponse(await fetch(url));
773
+ },
774
+ async fetchDataUrl(url) {
775
+ return BareResponse.fromNativeResponse(await fetch(url));
776
+ },
777
+ });
778
+
779
+ this.hooks = {
780
+ fetch: this.fetchHandler.hooks.fetch,
781
+ init: $scramjet.Tap.create<FrameInitHooks>(),
782
+ error: $scramjet.Tap.create<FrameErrorHooks>(),
783
+ };
784
+
785
+ element[CONTROLLERFRAME] = this;
786
+ }
787
+
788
+ back() {
789
+ this.element.contentWindow?.history.back();
790
+ }
791
+
792
+ forward() {
793
+ this.element.contentWindow?.history.forward();
794
+ }
795
+
796
+ reload() {
797
+ this.element.contentWindow?.location.reload();
798
+ }
799
+
800
+ go(url: string) {
801
+ const encoded = $scramjet.rewriteUrl(url, this.context, {
802
+ //@ts-expect-error
803
+ origin: new URL(location.href),
804
+ //@ts-expect-error
805
+ base: new URL(location.href),
806
+ });
807
+ this.element.src = encoded;
808
+ }
809
+ }