sliccy 3.44.0 → 3.46.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/README.md +1 -1
- package/dist/node-server/electron-controller.d.ts +66 -0
- package/dist/node-server/electron-controller.js +351 -227
- package/dist/ui/assets/{adobe-DfX5Y34s.js → adobe-CSxcey5r.js} +2 -2
- package/dist/ui/assets/{adobe-ChVGhtP9.js → adobe-Da115bHB.js} +1 -1
- package/dist/ui/assets/{agent-bridge-C5enj1WV.js → agent-bridge-ZRdlTYxI.js} +1 -1
- package/dist/ui/assets/agent-message-to-chat-B5VQCgc6.js +7 -0
- package/dist/ui/assets/{apps-Crb9N_ly.js → apps-LCRJf-Bj.js} +1 -1
- package/dist/ui/assets/{azure-openai-C4klGbBq.js → azure-openai-DrpoflZr.js} +1 -1
- package/dist/ui/assets/{azure-openai-z_V4V786.js → azure-openai-e68lAEyj.js} +1 -1
- package/dist/ui/assets/{bsh-watchdog-X8O_f3CL.js → bsh-watchdog-B1zIY2sM.js} +1 -1
- package/dist/ui/assets/{cdp-rsA_ALer.js → cdp-Cy4upOhT.js} +3 -3
- package/dist/ui/assets/{connect-surface-KLfqgi-g.js → connect-surface-BahfcMFA.js} +1 -1
- package/dist/ui/assets/cost-command-B1SUEdVQ.js +1 -0
- package/dist/ui/assets/{dist-LiJ_YnhF.js → dist-DLO6_i_i.js} +1 -1
- package/dist/ui/assets/{dist-Bm5Z71wh.js → dist-DUVVOtT3.js} +1 -1
- package/dist/ui/assets/{es-BJGCk_7v.js → es-CBKhaKNo.js} +1 -1
- package/dist/ui/assets/{follower-sprinkle-bridge-Q2unDn_f.js → follower-sprinkle-bridge-CknbkmDw.js} +1 -1
- package/dist/ui/assets/{fs-OBeSd3iT.js → fs-jtOU7if2.js} +1 -1
- package/dist/ui/assets/{github-kiWl2pok.js → github-B4cLbBmQ.js} +1 -1
- package/dist/ui/assets/{github-Ca90QHu7.js → github-BSaD-jMI.js} +2 -2
- package/dist/ui/assets/{github-copilot-CwAq3lpN.js → github-copilot-BsW7hhJu.js} +1 -1
- package/dist/ui/assets/{github-copilot-BrwsrJ0a.js → github-copilot-DodI4DS1.js} +1 -1
- package/dist/ui/assets/{kernel-worker-D7kDM6z5.js → kernel-worker-BTrLFznM.js} +919 -891
- package/dist/ui/assets/lick-ws-bridge-E1g3lRps.js +1 -0
- package/dist/ui/assets/{local-llm-BlEhi-EE.js → local-llm-CwKk0-sh.js} +1 -1
- package/dist/ui/assets/{magick-wasm-1dtYEyLr.js → magick-wasm-Muqi5nQe.js} +1 -1
- package/dist/ui/assets/{main-DQNfRwQ_.js → main-BAt3GJms.js} +169 -169
- package/dist/ui/assets/{main-cherry-zUoPE3cg.js → main-cherry-TgxLxBGI.js} +1 -1
- package/dist/ui/assets/{migration-run--a8o7LGm.js → migration-run-BDogYJcC.js} +1 -1
- package/dist/ui/assets/{mount-B7zmIwVP.js → mount-CSayJ5xj.js} +1 -1
- package/dist/ui/assets/{nuke-command-DlkLMgeD.js → nuke-command-B1G-iwiX.js} +1 -1
- package/dist/ui/assets/{oauth-bootstrap--CDdXr74.js → oauth-bootstrap-CJO5Uc9n.js} +2 -2
- package/dist/ui/assets/{oauth-service-BcS-bhoB.js → oauth-service-DB4fWEIw.js} +1 -1
- package/dist/ui/assets/{onboarding-orchestrator-Dpk9ts40.js → onboarding-orchestrator-xRYzOs-B.js} +1 -1
- package/dist/ui/assets/{openai-codex-DzqY9hsC.js → openai-codex-B5qZu8bN.js} +1 -1
- package/dist/ui/assets/{openai-codex-D1-QVrzy.js → openai-codex-CgxGUy87.js} +1 -1
- package/dist/ui/assets/{panel-rpc-handlers-CnJYQP27.js → panel-rpc-handlers-DbDQuWjm.js} +1 -1
- package/dist/ui/assets/{provider-BiMU6jfy.js → provider-BbqMcfYp.js} +2 -2
- package/dist/ui/assets/{provider-BBaMH9k3.js → provider-j_DAU9YR.js} +1 -1
- package/dist/ui/assets/{provider-settings-Cg_hqZuj.js → provider-settings-BfWBLG45.js} +2 -2
- package/dist/ui/assets/provider-store-access-DREH5Rn0.js +1 -0
- package/dist/ui/assets/provider-store-access-Dvt6Obmo.js +1 -0
- package/dist/ui/assets/{providers-BHEqlrX_.js → providers-B-eUneAQ.js} +1 -1
- package/dist/ui/assets/{proxied-fetch-Czuq7h2B.js → proxied-fetch-DLNoZln5.js} +1 -1
- package/dist/ui/assets/{remote-terminal-view-DkDiLkBM.js → remote-terminal-view-8J05xBYs.js} +1 -1
- package/dist/ui/assets/{store-DFdPyEaa.js → store-s3uG1ved.js} +1 -1
- package/dist/ui/assets/{sudo-CDWHE-XG.js → sudo-6xMv5Je5.js} +1 -1
- package/dist/ui/assets/{tray-leave-runtime-CedcHQX5.js → tray-leave-runtime-DnWFIWXy.js} +1 -1
- package/dist/ui/assets/{upgrade-detection-Bxb3OurB.js → upgrade-detection-366FmFL3.js} +1 -1
- package/dist/ui/assets/{xai-grok-CaWgmHbx.js → xai-grok-BG6yWHMj.js} +1 -1
- package/dist/ui/assets/{xai-grok-BrDEN2gc.js → xai-grok-hGf99Tc7.js} +1 -1
- package/dist/ui/electron-overlay-entry.js +127 -10
- package/dist/ui/index.html +1 -1
- package/dist/ui/packages/webapp/index.html +1 -1
- package/package.json +1 -1
- package/dist/ui/assets/agent-message-to-chat-FbjZc5O-.js +0 -7
- package/dist/ui/assets/cost-command-JwQpMwfk.js +0 -1
- package/dist/ui/assets/lick-ws-bridge-X9yJaxZX.js +0 -1
- package/dist/ui/assets/provider-store-access-DumMYBkr.js +0 -1
- package/dist/ui/assets/provider-store-access-k0kSINjn.js +0 -1
|
@@ -175,16 +175,10 @@ export async function launchElectronApp(options) {
|
|
|
175
175
|
// Theme detection — screenshot-based luminance analysis
|
|
176
176
|
// ---------------------------------------------------------------------------
|
|
177
177
|
/**
|
|
178
|
-
*
|
|
179
|
-
*
|
|
178
|
+
* Parse the IHDR/IDAT/IEND chunks of a PNG buffer (signature already validated).
|
|
179
|
+
* Other chunk types are intentionally skipped.
|
|
180
180
|
*/
|
|
181
|
-
|
|
182
|
-
const buf = Buffer.from(base64Data, 'base64');
|
|
183
|
-
// Validate PNG signature
|
|
184
|
-
const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
185
|
-
if (buf.subarray(0, 8).compare(PNG_SIGNATURE) !== 0) {
|
|
186
|
-
throw new Error('Not a valid PNG');
|
|
187
|
-
}
|
|
181
|
+
function parsePngChunks(buf) {
|
|
188
182
|
let width = 0;
|
|
189
183
|
let height = 0;
|
|
190
184
|
let bitDepth = 0;
|
|
@@ -209,6 +203,52 @@ export function decodePngPixels(base64Data) {
|
|
|
209
203
|
}
|
|
210
204
|
offset += 12 + chunkLength; // 4 (length) + 4 (type) + data + 4 (CRC)
|
|
211
205
|
}
|
|
206
|
+
return { width, height, bitDepth, colorType, idatChunks };
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Apply a single PNG row filter in place. Filter 0 (None) is a no-op so this
|
|
210
|
+
* helper is not called for it. See the PNG spec §9 Filtering for details.
|
|
211
|
+
*/
|
|
212
|
+
function applyPngRowFilter(row, prevRow, filter, bytesPerPixel, rowBytes) {
|
|
213
|
+
for (let i = 0; i < rowBytes; i++) {
|
|
214
|
+
const a = i >= bytesPerPixel ? row[i - bytesPerPixel] : 0;
|
|
215
|
+
const b = prevRow[i];
|
|
216
|
+
const c = i >= bytesPerPixel ? prevRow[i - bytesPerPixel] : 0;
|
|
217
|
+
switch (filter) {
|
|
218
|
+
case 1: // Sub
|
|
219
|
+
row[i] = (row[i] + a) & 0xff;
|
|
220
|
+
break;
|
|
221
|
+
case 2: // Up
|
|
222
|
+
row[i] = (row[i] + b) & 0xff;
|
|
223
|
+
break;
|
|
224
|
+
case 3: // Average
|
|
225
|
+
row[i] = (row[i] + ((a + b) >>> 1)) & 0xff;
|
|
226
|
+
break;
|
|
227
|
+
case 4: {
|
|
228
|
+
// Paeth
|
|
229
|
+
const p = a + b - c;
|
|
230
|
+
const pa = Math.abs(p - a);
|
|
231
|
+
const pb = Math.abs(p - b);
|
|
232
|
+
const pc = Math.abs(p - c);
|
|
233
|
+
row[i] = (row[i] + (pa <= pb && pa <= pc ? a : pb <= pc ? b : c)) & 0xff;
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
// case 0: None — no transformation needed
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Decode a base64 PNG into raw RGBA pixel data by parsing chunks and inflating.
|
|
242
|
+
* Returns { width, height, pixels } where pixels is a Buffer of RGBA bytes.
|
|
243
|
+
*/
|
|
244
|
+
export function decodePngPixels(base64Data) {
|
|
245
|
+
const buf = Buffer.from(base64Data, 'base64');
|
|
246
|
+
// Validate PNG signature
|
|
247
|
+
const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
248
|
+
if (buf.subarray(0, 8).compare(PNG_SIGNATURE) !== 0) {
|
|
249
|
+
throw new Error('Not a valid PNG');
|
|
250
|
+
}
|
|
251
|
+
const { width, height, bitDepth, colorType, idatChunks } = parsePngChunks(buf);
|
|
212
252
|
if (width === 0 || height === 0)
|
|
213
253
|
throw new Error('Missing IHDR chunk');
|
|
214
254
|
if (bitDepth !== 8)
|
|
@@ -227,33 +267,7 @@ export function decodePngPixels(base64Data) {
|
|
|
227
267
|
const rowStart = y * (1 + rowBytes);
|
|
228
268
|
const filter = inflated[rowStart];
|
|
229
269
|
const row = Buffer.from(inflated.subarray(rowStart + 1, rowStart + 1 + rowBytes));
|
|
230
|
-
|
|
231
|
-
for (let i = 0; i < rowBytes; i++) {
|
|
232
|
-
const a = i >= bytesPerPixel ? row[i - bytesPerPixel] : 0;
|
|
233
|
-
const b = prevRow[i];
|
|
234
|
-
const c = i >= bytesPerPixel ? prevRow[i - bytesPerPixel] : 0;
|
|
235
|
-
switch (filter) {
|
|
236
|
-
case 1: // Sub
|
|
237
|
-
row[i] = (row[i] + a) & 0xff;
|
|
238
|
-
break;
|
|
239
|
-
case 2: // Up
|
|
240
|
-
row[i] = (row[i] + b) & 0xff;
|
|
241
|
-
break;
|
|
242
|
-
case 3: // Average
|
|
243
|
-
row[i] = (row[i] + ((a + b) >>> 1)) & 0xff;
|
|
244
|
-
break;
|
|
245
|
-
case 4: {
|
|
246
|
-
// Paeth
|
|
247
|
-
const p = a + b - c;
|
|
248
|
-
const pa = Math.abs(p - a);
|
|
249
|
-
const pb = Math.abs(p - b);
|
|
250
|
-
const pc = Math.abs(p - c);
|
|
251
|
-
row[i] = (row[i] + (pa <= pb && pa <= pc ? a : pb <= pc ? b : c)) & 0xff;
|
|
252
|
-
break;
|
|
253
|
-
}
|
|
254
|
-
// case 0: None — no transformation needed
|
|
255
|
-
}
|
|
256
|
-
}
|
|
270
|
+
applyPngRowFilter(row, prevRow, filter, bytesPerPixel, rowBytes);
|
|
257
271
|
for (let x = 0; x < width; x++) {
|
|
258
272
|
const srcIdx = x * bytesPerPixel;
|
|
259
273
|
const dstIdx = (y * width + x) * 4;
|
|
@@ -352,16 +366,81 @@ async function loadElectronOverlayBundleSource(options) {
|
|
|
352
366
|
}
|
|
353
367
|
return await readFile(getElectronOverlayEntryDistPath(options.projectRoot), 'utf8');
|
|
354
368
|
}
|
|
369
|
+
/**
|
|
370
|
+
* Resolve the `Fetch.enable` origin pattern for CSP-bypass escalation. Mirrors
|
|
371
|
+
* swift-server's `OverlayTargetSession.fetchProxyOrigin` (Wave 5): prefer the
|
|
372
|
+
* parent page's http(s) origin so interception is byte-for-byte the same as
|
|
373
|
+
* before, but for `file://` (or other no-http-origin) targets fall back to the
|
|
374
|
+
* overlay iframe's own `http://localhost:<servePort>` origin — that is what
|
|
375
|
+
* actually needs unblocking when the parent is a local file.
|
|
376
|
+
*/
|
|
377
|
+
export function resolveFetchProxyOrigin(targetUrl, servePort) {
|
|
378
|
+
try {
|
|
379
|
+
const url = new URL(targetUrl);
|
|
380
|
+
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
|
381
|
+
return url.origin;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
// Fall through to the localhost fallback below.
|
|
386
|
+
}
|
|
387
|
+
return `http://localhost:${servePort}`;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Translate a Node http(s) response's headers into the array shape required by
|
|
391
|
+
* `Fetch.fulfillRequest`, stripping CSP and other hop-by-hop headers that are
|
|
392
|
+
* invalid in fulfill responses and rewriting `content-length` to match the
|
|
393
|
+
* actually-buffered body length. Returns whether any CSP header was stripped
|
|
394
|
+
* so the caller can log it.
|
|
395
|
+
*/
|
|
396
|
+
function buildFulfillResponseHeaders(rawHeaders, contentLength) {
|
|
397
|
+
const HOP_BY_HOP = new Set([
|
|
398
|
+
'content-security-policy',
|
|
399
|
+
'content-security-policy-report-only',
|
|
400
|
+
'transfer-encoding',
|
|
401
|
+
'connection',
|
|
402
|
+
'keep-alive',
|
|
403
|
+
]);
|
|
404
|
+
const responseHeaders = [];
|
|
405
|
+
let strippedCSP = false;
|
|
406
|
+
for (const [name, value] of Object.entries(rawHeaders)) {
|
|
407
|
+
const lower = name.toLowerCase();
|
|
408
|
+
if (lower.includes('content-security-policy')) {
|
|
409
|
+
strippedCSP = true;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (HOP_BY_HOP.has(lower))
|
|
413
|
+
continue;
|
|
414
|
+
// Update content-length to match actual body size
|
|
415
|
+
if (lower === 'content-length') {
|
|
416
|
+
responseHeaders.push({ name, value: String(contentLength) });
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if (Array.isArray(value)) {
|
|
420
|
+
value.forEach((v) => {
|
|
421
|
+
responseHeaders.push({ name, value: v });
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
else if (value) {
|
|
425
|
+
responseHeaders.push({ name, value });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return { responseHeaders, strippedCSP };
|
|
429
|
+
}
|
|
355
430
|
export class ElectronOverlayInjector {
|
|
356
431
|
cdpPort;
|
|
432
|
+
servePort;
|
|
357
433
|
bootstrapScript;
|
|
434
|
+
probeDelayMs;
|
|
358
435
|
connections = new Map();
|
|
359
436
|
cspBypassedTargets = new Set();
|
|
360
437
|
syncTimer = null;
|
|
361
438
|
syncing = false;
|
|
362
|
-
constructor(cdpPort, bootstrapScript) {
|
|
439
|
+
constructor(cdpPort, servePort, bootstrapScript, probeDelayMs = 1500) {
|
|
363
440
|
this.cdpPort = cdpPort;
|
|
441
|
+
this.servePort = servePort;
|
|
364
442
|
this.bootstrapScript = bootstrapScript;
|
|
443
|
+
this.probeDelayMs = probeDelayMs;
|
|
365
444
|
}
|
|
366
445
|
static async create(options) {
|
|
367
446
|
const bundleSource = await loadElectronOverlayBundleSource(options);
|
|
@@ -369,7 +448,39 @@ export class ElectronOverlayInjector {
|
|
|
369
448
|
bundleSource,
|
|
370
449
|
appUrl: buildElectronOverlayAppUrl(getElectronServeOrigin(options.servePort)),
|
|
371
450
|
});
|
|
372
|
-
return new ElectronOverlayInjector(options.cdpPort, bootstrapScript);
|
|
451
|
+
return new ElectronOverlayInjector(options.cdpPort, options.servePort, bootstrapScript);
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Test-only factory: skips bundle loading and lets tests drive the per-target
|
|
455
|
+
* connect flow directly with a controllable probe delay. Mirrors swift-server's
|
|
456
|
+
* `_testing_*` hooks on `ElectronOverlayInjector`.
|
|
457
|
+
*/
|
|
458
|
+
static _createForTesting(options) {
|
|
459
|
+
return new ElectronOverlayInjector(options.cdpPort ?? 9223, options.servePort, options.bootstrapScript ?? '/* test-noop */', options.probeDelayMs ?? 1500);
|
|
460
|
+
}
|
|
461
|
+
/** Test-only: drive the per-target connect flow without going through `start`. */
|
|
462
|
+
_testingConnectToTarget(target) {
|
|
463
|
+
this.connectToTarget(target);
|
|
464
|
+
}
|
|
465
|
+
/** Test-only: seed the per-target "already bypassed" guard. */
|
|
466
|
+
_testingSeedBypassedTarget(url) {
|
|
467
|
+
this.cspBypassedTargets.add(url);
|
|
468
|
+
}
|
|
469
|
+
/** Test-only: snapshot the per-target "already bypassed" guard set. */
|
|
470
|
+
_testingBypassedTargets() {
|
|
471
|
+
return new Set(this.cspBypassedTargets);
|
|
472
|
+
}
|
|
473
|
+
/** Test-only: close any sockets opened by `_testingConnectToTarget`. */
|
|
474
|
+
_testingCloseConnections() {
|
|
475
|
+
for (const connection of this.connections.values()) {
|
|
476
|
+
try {
|
|
477
|
+
connection.close();
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
// Ignore connection cleanup failures.
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
this.connections.clear();
|
|
373
484
|
}
|
|
374
485
|
async start() {
|
|
375
486
|
await this.syncTargets();
|
|
@@ -481,6 +592,199 @@ export class ElectronOverlayInjector {
|
|
|
481
592
|
ws.on('message', onMessage);
|
|
482
593
|
});
|
|
483
594
|
}
|
|
595
|
+
/**
|
|
596
|
+
* Build a script that sets the SLICC theme preference in localStorage to
|
|
597
|
+
* match the target app's detected theme, then runs the bootstrap.
|
|
598
|
+
*/
|
|
599
|
+
buildThemedBootstrap(theme) {
|
|
600
|
+
const themeScript = `try{localStorage.setItem('slicc-theme',${JSON.stringify(theme)})}catch(e){}`;
|
|
601
|
+
return `${themeScript}\n${this.bootstrapScript}`;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Handle the initial CDP `ws.on('open', ...)` event for a target: enable
|
|
605
|
+
* Runtime/Page, set CSP bypass, detect theme, inject the overlay, and (on a
|
|
606
|
+
* first connect) probe whether the overlay iframe actually loaded — falling
|
|
607
|
+
* back to a CSP-bypass reload by setting `state.pendingReload` and
|
|
608
|
+
* `state.pendingCspEscalation` for the message handler to continue from.
|
|
609
|
+
*
|
|
610
|
+
* Mutating flow flags (`pendingReload`, `pendingCspEscalation`,
|
|
611
|
+
* `fetchProxyActive`) live on the shared `state` object so this helper
|
|
612
|
+
* preserves the original closure-driven control flow exactly.
|
|
613
|
+
*/
|
|
614
|
+
handleSocketOpen(ws, send, target, state) {
|
|
615
|
+
const alreadyBypassed = this.cspBypassedTargets.has(target.url);
|
|
616
|
+
console.log(`[electron-float] Connected to target, bypassed=${alreadyBypassed}, url=${target.url}`);
|
|
617
|
+
send('Runtime.enable');
|
|
618
|
+
send('Page.enable');
|
|
619
|
+
// Set CSP bypass — affects future resource loads on the current page
|
|
620
|
+
send('Page.setBypassCSP', { enabled: true });
|
|
621
|
+
if (alreadyBypassed) {
|
|
622
|
+
// Already reloaded with CSP bypass previously — detect theme and inject
|
|
623
|
+
console.log(`[electron-float] Detecting theme and injecting overlay (CSP already bypassed)...`);
|
|
624
|
+
void detectAppThemeFromScreenshot(ws, send).then((theme) => {
|
|
625
|
+
if (ws.readyState !== WebSocket.OPEN)
|
|
626
|
+
return;
|
|
627
|
+
send('Runtime.evaluate', {
|
|
628
|
+
expression: this.buildThemedBootstrap(theme),
|
|
629
|
+
awaitPromise: false,
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
// First connection to this target URL: detect theme, then inject overlay.
|
|
635
|
+
// After injection, probe whether the iframe loaded; if CSP blocked it, fall
|
|
636
|
+
// back to reload+proxy. We probe/escalate regardless of URL scheme — file://
|
|
637
|
+
// (and app://-style local) Electron renderers can still ship a meta CSP
|
|
638
|
+
// (e.g. AEM Desktop's `default-src 'self'`) that blocks the overlay iframe.
|
|
639
|
+
console.log(`[electron-float] Detecting theme before first overlay injection...`);
|
|
640
|
+
void detectAppThemeFromScreenshot(ws, send).then((theme) => {
|
|
641
|
+
if (ws.readyState !== WebSocket.OPEN)
|
|
642
|
+
return;
|
|
643
|
+
console.log(`[electron-float] Injecting overlay (first attempt, theme=${theme})...`);
|
|
644
|
+
send('Runtime.evaluate', {
|
|
645
|
+
expression: this.buildThemedBootstrap(theme),
|
|
646
|
+
awaitPromise: false,
|
|
647
|
+
});
|
|
648
|
+
// After a short delay, probe whether the overlay iframe loaded.
|
|
649
|
+
// If CSP blocked it, reload the page so Page.setBypassCSP takes effect.
|
|
650
|
+
// If that still doesn't work, escalate to the Fetch proxy.
|
|
651
|
+
setTimeout(async () => {
|
|
652
|
+
if (ws.readyState !== WebSocket.OPEN)
|
|
653
|
+
return;
|
|
654
|
+
const loaded = await this.probeOverlayIframeLoaded(ws, send);
|
|
655
|
+
if (loaded) {
|
|
656
|
+
console.log(`[electron-float] Overlay iframe loaded successfully — no CSP reload needed`);
|
|
657
|
+
this.cspBypassedTargets.add(target.url);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
// Phase 2: Page.setBypassCSP was already set — a simple reload should
|
|
661
|
+
// make the browser ignore CSP headers on the fresh navigation.
|
|
662
|
+
// Deliberately do NOT recordBypassed yet — if the CDP session
|
|
663
|
+
// disconnects mid-reload (AEM Desktop's bootstrap recreates the
|
|
664
|
+
// execution context, which closes our WS), the next reconnect
|
|
665
|
+
// needs to re-run the reload path. Only record once the post-reload
|
|
666
|
+
// probe confirms the iframe loaded. Mirrors swift-server d1c9f14d
|
|
667
|
+
// (`shouldRecordBypassedAfter(probeAction:)` returns false for
|
|
668
|
+
// `.reloadWithBypass`).
|
|
669
|
+
console.log(`[electron-float] Overlay iframe blocked by CSP, reloading with bypass: ${target.url}`);
|
|
670
|
+
state.pendingReload = true;
|
|
671
|
+
state.pendingCspEscalation = true;
|
|
672
|
+
send('Page.reload', { ignoreCache: true });
|
|
673
|
+
}, this.probeDelayMs);
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Handle `Page.loadEventFired` after a CSP-bypass reload: re-inject the
|
|
678
|
+
* themed overlay, then (if this load came from the simple-reload path) probe
|
|
679
|
+
* the iframe again and, if still blocked, escalate to the Fetch HTTP proxy
|
|
680
|
+
* which strips CSP from the document response. The proxy reload also sets
|
|
681
|
+
* `pendingReload` again so the next `loadEventFired` re-injects on top of
|
|
682
|
+
* the stripped response.
|
|
683
|
+
*/
|
|
684
|
+
handlePageLoadAfterReload(ws, send, target, state) {
|
|
685
|
+
state.pendingReload = false;
|
|
686
|
+
console.log(`[electron-float] Page loaded after CSP reload, detecting theme and injecting overlay...`);
|
|
687
|
+
if (ws.readyState !== WebSocket.OPEN)
|
|
688
|
+
return;
|
|
689
|
+
void detectAppThemeFromScreenshot(ws, send).then((theme) => {
|
|
690
|
+
if (ws.readyState !== WebSocket.OPEN)
|
|
691
|
+
return;
|
|
692
|
+
send('Runtime.evaluate', {
|
|
693
|
+
expression: this.buildThemedBootstrap(theme),
|
|
694
|
+
awaitPromise: false,
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
// If this was a simple reload (no proxy), check if the iframe loads now.
|
|
698
|
+
// If it still doesn't, escalate to the Fetch proxy as a last resort.
|
|
699
|
+
if (state.pendingCspEscalation) {
|
|
700
|
+
state.pendingCspEscalation = false;
|
|
701
|
+
setTimeout(async () => {
|
|
702
|
+
if (ws.readyState !== WebSocket.OPEN)
|
|
703
|
+
return;
|
|
704
|
+
const loaded = await this.probeOverlayIframeLoaded(ws, send);
|
|
705
|
+
if (loaded) {
|
|
706
|
+
console.log(`[electron-float] Overlay iframe loaded after CSP reload — no proxy needed`);
|
|
707
|
+
this.cspBypassedTargets.add(target.url);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
const fetchOrigin = resolveFetchProxyOrigin(target.url, this.servePort);
|
|
711
|
+
console.log(`[electron-float] CSP reload insufficient, escalating to Fetch proxy: target=${target.url} origin=${fetchOrigin}`);
|
|
712
|
+
state.fetchProxyActive = true;
|
|
713
|
+
send('Fetch.enable', {
|
|
714
|
+
patterns: [{ urlPattern: `${fetchOrigin}/*`, requestStage: 'Request' }],
|
|
715
|
+
});
|
|
716
|
+
state.pendingReload = true;
|
|
717
|
+
send('Page.reload', { ignoreCache: true });
|
|
718
|
+
}, this.probeDelayMs);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Handle a single `Fetch.requestPaused` event under the active Fetch proxy:
|
|
723
|
+
* pass non-HTML requests straight through with `Fetch.continueRequest`, and
|
|
724
|
+
* proxy HTML document requests through Node http/https so the response can
|
|
725
|
+
* be returned via `Fetch.fulfillRequest` with CSP and hop-by-hop headers
|
|
726
|
+
* stripped. `Fetch.fulfillRequest` is intentionally fire-and-forget — there
|
|
727
|
+
* is no CDP reply for fulfill, and the response body is the document body.
|
|
728
|
+
*/
|
|
729
|
+
handleFetchRequestPaused(ws, send, msg) {
|
|
730
|
+
const requestId = msg.params?.requestId;
|
|
731
|
+
if (!requestId) {
|
|
732
|
+
console.warn('[electron-float] Fetch.requestPaused without requestId, skipping');
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
const request = (msg.params?.request ?? {});
|
|
736
|
+
const url = request.url || '';
|
|
737
|
+
const method = request.method || 'GET';
|
|
738
|
+
const requestHeaders = request.headers || {};
|
|
739
|
+
const postData = request.postData;
|
|
740
|
+
// Only proxy HTML document requests (Accept header contains text/html)
|
|
741
|
+
const acceptHeader = requestHeaders['Accept'] || requestHeaders['accept'] || '';
|
|
742
|
+
if (!acceptHeader.includes('text/html')) {
|
|
743
|
+
send('Fetch.continueRequest', { requestId });
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
console.log(`[electron-float] Proxying request to strip CSP: ${url.substring(0, 60)}`);
|
|
747
|
+
// Make the request ourselves using Node.js http/https
|
|
748
|
+
const parsedUrl = new URL(url);
|
|
749
|
+
const transport = parsedUrl.protocol === 'https:' ? https : http;
|
|
750
|
+
const options = {
|
|
751
|
+
hostname: parsedUrl.hostname,
|
|
752
|
+
port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
|
|
753
|
+
path: parsedUrl.pathname + parsedUrl.search,
|
|
754
|
+
method: method,
|
|
755
|
+
headers: requestHeaders,
|
|
756
|
+
};
|
|
757
|
+
const proxyReq = transport.request(options, (proxyRes) => {
|
|
758
|
+
const bodyChunks = [];
|
|
759
|
+
proxyRes.on('data', (chunk) => bodyChunks.push(chunk));
|
|
760
|
+
proxyRes.on('end', () => {
|
|
761
|
+
if (ws.readyState !== WebSocket.OPEN)
|
|
762
|
+
return;
|
|
763
|
+
const fullBody = Buffer.concat(bodyChunks);
|
|
764
|
+
const { responseHeaders, strippedCSP } = buildFulfillResponseHeaders(proxyRes.headers, fullBody.length);
|
|
765
|
+
if (strippedCSP) {
|
|
766
|
+
console.log(`[electron-float] Stripped CSP from: ${url.substring(0, 60)}`);
|
|
767
|
+
}
|
|
768
|
+
send('Fetch.fulfillRequest', {
|
|
769
|
+
requestId,
|
|
770
|
+
responseCode: proxyRes.statusCode || 200,
|
|
771
|
+
responseHeaders,
|
|
772
|
+
body: fullBody.toString('base64'),
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
});
|
|
776
|
+
proxyReq.on('error', (err) => {
|
|
777
|
+
console.error(`[electron-float] Proxy request failed for ${url.substring(0, 60)}:`, err.message);
|
|
778
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
779
|
+
send('Fetch.failRequest', { requestId, errorReason: 'Failed' });
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
// Forward request body if present (for POST/PUT requests)
|
|
783
|
+
if (postData) {
|
|
784
|
+
proxyReq.write(postData);
|
|
785
|
+
}
|
|
786
|
+
proxyReq.end();
|
|
787
|
+
}
|
|
484
788
|
connectToTarget(target) {
|
|
485
789
|
const targetId = target.webSocketDebuggerUrl;
|
|
486
790
|
const ws = new WebSocket(targetId);
|
|
@@ -491,204 +795,24 @@ export class ElectronOverlayInjector {
|
|
|
491
795
|
ws.send(JSON.stringify({ id, method, params }));
|
|
492
796
|
return id;
|
|
493
797
|
};
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
let fetchProxyActive = false;
|
|
499
|
-
/**
|
|
500
|
-
* Build a script that sets the SLICC theme preference in localStorage
|
|
501
|
-
* to match the target app's detected theme, then runs the bootstrap.
|
|
502
|
-
*/
|
|
503
|
-
const buildThemedBootstrap = (theme) => {
|
|
504
|
-
const themeScript = `try{localStorage.setItem('slicc-theme',${JSON.stringify(theme)})}catch(e){}`;
|
|
505
|
-
return `${themeScript}\n${bootstrapScript}`;
|
|
798
|
+
const state = {
|
|
799
|
+
pendingReload: false,
|
|
800
|
+
pendingCspEscalation: false,
|
|
801
|
+
fetchProxyActive: false,
|
|
506
802
|
};
|
|
507
803
|
ws.on('open', () => {
|
|
508
|
-
|
|
509
|
-
const alreadyBypassed = cspBypassedTargets.has(target.url);
|
|
510
|
-
console.log(`[electron-float] Connected to target, web=${isWebContent}, bypassed=${alreadyBypassed}, url=${target.url}`);
|
|
511
|
-
send('Runtime.enable');
|
|
512
|
-
send('Page.enable');
|
|
513
|
-
// Set CSP bypass — affects future resource loads on the current page
|
|
514
|
-
send('Page.setBypassCSP', { enabled: true });
|
|
515
|
-
if (alreadyBypassed) {
|
|
516
|
-
// Already reloaded with CSP bypass previously — detect theme and inject
|
|
517
|
-
console.log(`[electron-float] Detecting theme and injecting overlay (CSP already bypassed)...`);
|
|
518
|
-
void detectAppThemeFromScreenshot(ws, send).then((theme) => {
|
|
519
|
-
if (ws.readyState !== WebSocket.OPEN)
|
|
520
|
-
return;
|
|
521
|
-
send('Runtime.evaluate', {
|
|
522
|
-
expression: buildThemedBootstrap(theme),
|
|
523
|
-
awaitPromise: false,
|
|
524
|
-
});
|
|
525
|
-
});
|
|
526
|
-
return;
|
|
527
|
-
}
|
|
528
|
-
// First connection to this target URL: detect theme, then inject overlay.
|
|
529
|
-
// After injection, check if the iframe loaded. If CSP blocked it, fall back to reload+proxy.
|
|
530
|
-
console.log(`[electron-float] Detecting theme before first overlay injection...`);
|
|
531
|
-
void detectAppThemeFromScreenshot(ws, send).then((theme) => {
|
|
532
|
-
if (ws.readyState !== WebSocket.OPEN)
|
|
533
|
-
return;
|
|
534
|
-
console.log(`[electron-float] Injecting overlay (first attempt, theme=${theme})...`);
|
|
535
|
-
send('Runtime.evaluate', { expression: buildThemedBootstrap(theme), awaitPromise: false });
|
|
536
|
-
if (!isWebContent) {
|
|
537
|
-
// Local content (file://, app protocol) — CSP is not an issue
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
// After a short delay, probe whether the overlay iframe loaded.
|
|
541
|
-
// If CSP blocked it, reload the page so Page.setBypassCSP takes effect.
|
|
542
|
-
// If that still doesn't work, escalate to the Fetch proxy.
|
|
543
|
-
setTimeout(async () => {
|
|
544
|
-
if (ws.readyState !== WebSocket.OPEN)
|
|
545
|
-
return;
|
|
546
|
-
const loaded = await this.probeOverlayIframeLoaded(ws, send);
|
|
547
|
-
if (loaded) {
|
|
548
|
-
console.log(`[electron-float] Overlay iframe loaded successfully — no CSP reload needed`);
|
|
549
|
-
cspBypassedTargets.add(target.url);
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
552
|
-
// Phase 2: Page.setBypassCSP was already set — a simple reload should
|
|
553
|
-
// make the browser ignore CSP headers on the fresh navigation.
|
|
554
|
-
console.log(`[electron-float] Overlay iframe blocked by CSP, reloading with bypass: ${target.url}`);
|
|
555
|
-
cspBypassedTargets.add(target.url);
|
|
556
|
-
pendingReload = true;
|
|
557
|
-
pendingCspEscalation = true;
|
|
558
|
-
send('Page.reload', { ignoreCache: true });
|
|
559
|
-
}, 1500);
|
|
560
|
-
});
|
|
804
|
+
this.handleSocketOpen(ws, send, target, state);
|
|
561
805
|
});
|
|
562
806
|
// Handle CDP events: lifecycle events and Fetch interception
|
|
563
807
|
ws.on('message', (data) => {
|
|
564
808
|
try {
|
|
565
809
|
const msg = JSON.parse(data.toString());
|
|
566
810
|
// Inject overlay after page load completes (after CSP-bypass reload)
|
|
567
|
-
if (msg.method === 'Page.loadEventFired' && pendingReload) {
|
|
568
|
-
|
|
569
|
-
console.log(`[electron-float] Page loaded after CSP reload, detecting theme and injecting overlay...`);
|
|
570
|
-
if (ws.readyState !== WebSocket.OPEN)
|
|
571
|
-
return;
|
|
572
|
-
void detectAppThemeFromScreenshot(ws, send).then((theme) => {
|
|
573
|
-
if (ws.readyState !== WebSocket.OPEN)
|
|
574
|
-
return;
|
|
575
|
-
send('Runtime.evaluate', {
|
|
576
|
-
expression: buildThemedBootstrap(theme),
|
|
577
|
-
awaitPromise: false,
|
|
578
|
-
});
|
|
579
|
-
});
|
|
580
|
-
// If this was a simple reload (no proxy), check if the iframe loads now.
|
|
581
|
-
// If it still doesn't, escalate to the Fetch proxy as a last resort.
|
|
582
|
-
if (pendingCspEscalation) {
|
|
583
|
-
pendingCspEscalation = false;
|
|
584
|
-
setTimeout(async () => {
|
|
585
|
-
if (ws.readyState !== WebSocket.OPEN)
|
|
586
|
-
return;
|
|
587
|
-
const loaded = await this.probeOverlayIframeLoaded(ws, send);
|
|
588
|
-
if (loaded) {
|
|
589
|
-
console.log(`[electron-float] Overlay iframe loaded after CSP reload — no proxy needed`);
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
console.log(`[electron-float] CSP reload insufficient, escalating to Fetch proxy: ${target.url}`);
|
|
593
|
-
fetchProxyActive = true;
|
|
594
|
-
const urlOrigin = new URL(target.url).origin;
|
|
595
|
-
send('Fetch.enable', {
|
|
596
|
-
patterns: [{ urlPattern: `${urlOrigin}/*`, requestStage: 'Request' }],
|
|
597
|
-
});
|
|
598
|
-
pendingReload = true;
|
|
599
|
-
send('Page.reload', { ignoreCache: true });
|
|
600
|
-
}, 1500);
|
|
601
|
-
}
|
|
811
|
+
if (msg.method === 'Page.loadEventFired' && state.pendingReload) {
|
|
812
|
+
this.handlePageLoadAfterReload(ws, send, target, state);
|
|
602
813
|
}
|
|
603
|
-
if (msg.method === 'Fetch.requestPaused' && fetchProxyActive) {
|
|
604
|
-
|
|
605
|
-
if (!requestId) {
|
|
606
|
-
console.warn('[electron-float] Fetch.requestPaused without requestId, skipping');
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
const url = msg.params?.request?.url || '';
|
|
610
|
-
const method = msg.params?.request?.method || 'GET';
|
|
611
|
-
const requestHeaders = msg.params?.request?.headers || {};
|
|
612
|
-
const postData = msg.params?.request?.postData;
|
|
613
|
-
// Only proxy HTML document requests (Accept header contains text/html)
|
|
614
|
-
const acceptHeader = requestHeaders['Accept'] || requestHeaders['accept'] || '';
|
|
615
|
-
if (!acceptHeader.includes('text/html')) {
|
|
616
|
-
send('Fetch.continueRequest', { requestId });
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
619
|
-
console.log(`[electron-float] Proxying request to strip CSP: ${url.substring(0, 60)}`);
|
|
620
|
-
// Make the request ourselves using Node.js http/https
|
|
621
|
-
const parsedUrl = new URL(url);
|
|
622
|
-
const transport = parsedUrl.protocol === 'https:' ? https : http;
|
|
623
|
-
const options = {
|
|
624
|
-
hostname: parsedUrl.hostname,
|
|
625
|
-
port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
|
|
626
|
-
path: parsedUrl.pathname + parsedUrl.search,
|
|
627
|
-
method: method,
|
|
628
|
-
headers: requestHeaders,
|
|
629
|
-
};
|
|
630
|
-
const proxyReq = transport.request(options, (proxyRes) => {
|
|
631
|
-
const bodyChunks = [];
|
|
632
|
-
proxyRes.on('data', (chunk) => bodyChunks.push(chunk));
|
|
633
|
-
proxyRes.on('end', () => {
|
|
634
|
-
if (ws.readyState !== WebSocket.OPEN)
|
|
635
|
-
return;
|
|
636
|
-
const fullBody = Buffer.concat(bodyChunks);
|
|
637
|
-
// Build response headers, stripping CSP and hop-by-hop headers
|
|
638
|
-
// that are invalid in Fetch.fulfillRequest responses
|
|
639
|
-
const HOP_BY_HOP = new Set([
|
|
640
|
-
'content-security-policy',
|
|
641
|
-
'content-security-policy-report-only',
|
|
642
|
-
'transfer-encoding',
|
|
643
|
-
'connection',
|
|
644
|
-
'keep-alive',
|
|
645
|
-
]);
|
|
646
|
-
const responseHeaders = [];
|
|
647
|
-
let strippedCSP = false;
|
|
648
|
-
for (const [name, value] of Object.entries(proxyRes.headers)) {
|
|
649
|
-
const lower = name.toLowerCase();
|
|
650
|
-
if (lower.includes('content-security-policy')) {
|
|
651
|
-
strippedCSP = true;
|
|
652
|
-
continue;
|
|
653
|
-
}
|
|
654
|
-
if (HOP_BY_HOP.has(lower))
|
|
655
|
-
continue;
|
|
656
|
-
// Update content-length to match actual body size
|
|
657
|
-
if (lower === 'content-length') {
|
|
658
|
-
responseHeaders.push({ name, value: String(fullBody.length) });
|
|
659
|
-
continue;
|
|
660
|
-
}
|
|
661
|
-
if (Array.isArray(value)) {
|
|
662
|
-
value.forEach((v) => {
|
|
663
|
-
responseHeaders.push({ name, value: v });
|
|
664
|
-
});
|
|
665
|
-
}
|
|
666
|
-
else if (value) {
|
|
667
|
-
responseHeaders.push({ name, value });
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
if (strippedCSP) {
|
|
671
|
-
console.log(`[electron-float] Stripped CSP from: ${url.substring(0, 60)}`);
|
|
672
|
-
}
|
|
673
|
-
send('Fetch.fulfillRequest', {
|
|
674
|
-
requestId,
|
|
675
|
-
responseCode: proxyRes.statusCode || 200,
|
|
676
|
-
responseHeaders,
|
|
677
|
-
body: fullBody.toString('base64'),
|
|
678
|
-
});
|
|
679
|
-
});
|
|
680
|
-
});
|
|
681
|
-
proxyReq.on('error', (err) => {
|
|
682
|
-
console.error(`[electron-float] Proxy request failed for ${url.substring(0, 60)}:`, err.message);
|
|
683
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
684
|
-
send('Fetch.failRequest', { requestId, errorReason: 'Failed' });
|
|
685
|
-
}
|
|
686
|
-
});
|
|
687
|
-
// Forward request body if present (for POST/PUT requests)
|
|
688
|
-
if (postData) {
|
|
689
|
-
proxyReq.write(postData);
|
|
690
|
-
}
|
|
691
|
-
proxyReq.end();
|
|
814
|
+
if (msg.method === 'Fetch.requestPaused' && state.fetchProxyActive) {
|
|
815
|
+
this.handleFetchRequestPaused(ws, send, msg);
|
|
692
816
|
}
|
|
693
817
|
}
|
|
694
818
|
catch {
|