sliccy 6.154.1 → 6.154.3
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/dist/node-server/browser-shutdown.d.ts +6 -1
- package/dist/node-server/browser-shutdown.js +3 -3
- package/dist/node-server/chrome-launch.d.ts +3 -0
- package/dist/node-server/chrome-launch.js +2 -1
- package/dist/node-server/electron-controller.d.ts +63 -2
- package/dist/node-server/electron-controller.js +86 -48
- package/dist/node-server/electron-runtime.d.ts +15 -1
- package/dist/node-server/electron-runtime.js +10 -10
- package/dist/node-server/fetch-proxy-gzip.js +5 -7
- package/dist/node-server/hosted-bootstrap.d.ts +1 -0
- package/dist/node-server/hosted-bootstrap.js +12 -14
- package/dist/node-server/hostfs.d.ts +37 -1
- package/dist/node-server/hostfs.js +5 -5
- package/dist/node-server/install-cli.d.ts +1 -0
- package/dist/node-server/install-cli.js +1 -1
- package/dist/node-server/launch-url.js +2 -6
- package/dist/node-server/leader-restart.d.ts +9 -0
- package/dist/node-server/leader-restart.js +1 -1
- package/dist/node-server/publish-chrome-web-store-main.d.ts +1 -0
- package/dist/node-server/publish-chrome-web-store-main.js +13 -0
- package/dist/node-server/publish-chrome-web-store.d.ts +2 -0
- package/dist/node-server/publish-chrome-web-store.js +2 -15
- package/dist/node-server/release-package-main.d.ts +1 -0
- package/dist/node-server/release-package-main.js +17 -0
- package/dist/node-server/release-package.d.ts +38 -3
- package/dist/node-server/release-package.js +46 -50
- package/dist/node-server/routes/fetch-proxy.d.ts +40 -1
- package/dist/node-server/routes/fetch-proxy.js +20 -12
- package/dist/node-server/routes/secrets.d.ts +23 -1
- package/dist/node-server/routes/secrets.js +34 -30
- package/dist/node-server/secrets/proxy-manager.d.ts +2 -1
- package/dist/node-server/secrets/proxy-manager.js +27 -26
- package/dist/node-server/secrets/session-id-file.d.ts +5 -1
- package/dist/node-server/secrets/session-id-file.js +4 -4
- package/dist/node-server/secrets/sign-and-forward.d.ts +8 -0
- package/dist/node-server/secrets/sign-and-forward.js +1 -1
- package/dist/node-server/sudo/select.d.ts +2 -0
- package/dist/node-server/sudo/select.js +1 -1
- package/dist/node-server/sudo/tty-backend.d.ts +1 -0
- package/dist/node-server/sudo/tty-backend.js +1 -1
- package/dist/ui/.vite/manifest.json +171 -171
- package/dist/ui/assets/SKILL-BGww2dFz.js +1 -0
- package/dist/ui/assets/{SKILL-cqUV8C7U.js → SKILL-Cl9PVarg.js} +1 -1
- package/dist/ui/assets/{account-store-BqdqUcgS.js → account-store-BYDUpeqo.js} +1 -1
- package/dist/ui/assets/{account-store-BgFuxoIt.js → account-store-BvEgWXDi.js} +1 -1
- package/dist/ui/assets/{adobe-BWEqvpfE.js → adobe-Bf44Mdem.js} +1 -1
- package/dist/ui/assets/{adobe-De7VHe-t.js → adobe-DIeHS2Yx.js} +1 -1
- package/dist/ui/assets/{agent-bridge-EXV0qNBB.js → agent-bridge-pmiSNwoc.js} +1 -1
- package/dist/ui/assets/{agent-panels-dcldnH3V.js → agent-panels-CWBbnFvW.js} +1 -1
- package/dist/ui/assets/api-endpoint-3qEcn7BY.js +1 -0
- package/dist/ui/assets/{apply-layout-DVgK-NlB.js → apply-layout-Eft8a3mq.js} +1 -1
- package/dist/ui/assets/{apps-BqrASr5C.js → apps-BklKPyC6.js} +1 -1
- package/dist/ui/assets/{auto-mount-table-0jMTi9rV.js → auto-mount-table-BDyGIOIb.js} +1 -1
- package/dist/ui/assets/{azure-openai-vXGFZfPE.js → azure-openai-7VfJn11R.js} +1 -1
- package/dist/ui/assets/{azure-openai-94kdulWT.js → azure-openai-o6cG2UDZ.js} +1 -1
- package/dist/ui/assets/{backend-hostfs-BxfjZHzO.js → backend-hostfs-BJU2QL9q.js} +1 -1
- package/dist/ui/assets/{budget-usage-source-BNEsxDHX.js → budget-usage-source-B5GSAynq.js} +1 -1
- package/dist/ui/assets/{cdp-D0GOzfX9.js → cdp-Di9nU0k7.js} +1 -1
- package/dist/ui/assets/{cerebras-eckgXNEU.js → cerebras-BRpdCI1J.js} +1 -1
- package/dist/ui/assets/{cerebras-v41u2mpJ.js → cerebras-DuUgRhfz.js} +1 -1
- package/dist/ui/assets/{client-BOSdqiLb.js → client-B6iKb_ye.js} +1 -1
- package/dist/ui/assets/{composer-speech-ButVgF9_.js → composer-speech-Chlrvbhg.js} +1 -1
- package/dist/ui/assets/{connect-surface-C0WXwJHl.js → connect-surface-DBXtjfUP.js} +1 -1
- package/dist/ui/assets/{core-D59qaXEb.js → core-C9qgaWgE.js} +2 -2
- package/dist/ui/assets/{directed-approval-D1CLB6cG.js → directed-approval-C2OTs8pv.js} +1 -1
- package/dist/ui/assets/dist-BPPG9gF6.js +12 -0
- package/dist/ui/assets/dist-BnmWpUNI.js +18 -0
- package/dist/ui/assets/{dist-DCrgApJQ.js → dist-CFI3XeIO.js} +1 -1
- package/dist/ui/assets/{dist-UJah3hVh.js → dist-Ciikigd5.js} +1 -1
- package/dist/ui/assets/{ensure-speech-assets-uGHvkmNC.js → ensure-speech-assets-DOu9EHnU.js} +1 -1
- package/dist/ui/assets/{extension-ops-B9_1VB7W.js → extension-ops-B05mAXsq.js} +1 -1
- package/dist/ui/assets/{fs-DK3k_wQh.js → fs-Dqh17T5J.js} +2 -2
- package/dist/ui/assets/{fs-mC_RRTZZ.js → fs-T4T9WqhX.js} +1 -1
- package/dist/ui/assets/{github-CT-Oyj_0.js → github-CL_7yoHL.js} +1 -1
- package/dist/ui/assets/github-auth-A6Mopmg8.js +1 -0
- package/dist/ui/assets/{github-copilot-CACLYChL.js → github-copilot-B2ugBoRh.js} +1 -1
- package/dist/ui/assets/{github-copilot-D2H-jvYu.js → github-copilot-_5qoxGCT.js} +1 -1
- package/dist/ui/assets/github-install-8yCqacop.js +1 -0
- package/dist/ui/assets/{github-BLOM522m.js → github-koUNqi7Y.js} +2 -2
- package/dist/ui/assets/github-zip-CV41OIqx.js +1 -0
- package/dist/ui/assets/{guest-tool-gate-6uzbOlEb.js → guest-tool-gate-_NuvhpkB.js} +1 -1
- package/dist/ui/assets/{hear-BXcOoxVl.js → hear-DScAQlnK.js} +1 -1
- package/dist/ui/assets/{help-BAqixBa0.js → help-BvOxNE_5.js} +1 -1
- package/dist/ui/assets/{js-realm-worker-CRY5Fv-C.js → js-realm-worker-fE5Mi5F2.js} +1 -1
- package/dist/ui/assets/{kernel-worker-C1DLWYBM.js → kernel-worker-DURzH7I0.js} +586 -585
- package/dist/ui/assets/{lick-urls-HYeTtmbf.js → lick-urls-BZVq_P3x.js} +1 -1
- package/dist/ui/assets/{lick-ws-bridge-DqmithfF.js → lick-ws-bridge-CDToX4F0.js} +1 -1
- package/dist/ui/assets/{local-llm-D723hPLf.js → local-llm-D_HQ1XG8.js} +1 -1
- package/dist/ui/assets/{local-llm-Cv98D1SL.js → local-llm-QN3Afs2P.js} +1 -1
- package/dist/ui/assets/{main-C-pVQBFE.js → main-DyPgoSsm.js} +3 -3
- package/dist/ui/assets/{main-cherry-BdH-nz0p.js → main-cherry-v6ksBa29.js} +1 -1
- package/dist/ui/assets/{memory-curation-seam-eUVjWT6W.js → memory-curation-seam-DXGqoWuF.js} +1 -1
- package/dist/ui/assets/{mount-3VFe3Pa7.js → mount-CdTXGHvm.js} +3 -3
- package/dist/ui/assets/{mount-Dh4tJ6XQ.js → mount-Drg9K5Dc.js} +1 -1
- package/dist/ui/assets/{mount-bridge-client-B5qLR9Xe.js → mount-bridge-client-DWY67yip.js} +1 -1
- package/dist/ui/assets/{mount-recovery-Cp7KYqTq.js → mount-recovery-CUhmg6UT.js} +1 -1
- package/dist/ui/assets/{new-session-hwwKqrO5.js → new-session-BBCvIUGf.js} +2 -2
- package/dist/ui/assets/{oauth-bootstrap-BP7bAgwJ.js → oauth-bootstrap-37qdAFNO.js} +2 -2
- package/dist/ui/assets/{oauth-service-BZl-9EoE.js → oauth-service-IlBYPvwr.js} +1 -1
- package/dist/ui/assets/{onboarding-orchestrator-DOOTJ9OG.js → onboarding-orchestrator-CgzN2Wut.js} +1 -1
- package/dist/ui/assets/{openai-codex--3foibzj.js → openai-codex-CByg7ExV.js} +1 -1
- package/dist/ui/assets/{openai-codex-vGpH6Ldz.js → openai-codex-yA937LOi.js} +1 -1
- package/dist/ui/assets/{openrouter-D17vOiK5.js → openrouter-Bq9TiHqe.js} +1 -1
- package/dist/ui/assets/{openrouter-B3v5OQPe.js → openrouter-D1YZxJcz.js} +1 -1
- package/dist/ui/assets/{openrouter-free-Mha-ykI6.js → openrouter-free-BwEyNzqx.js} +1 -1
- package/dist/ui/assets/{openrouter-free-DndyeHH8.js → openrouter-free-Db1fart9.js} +1 -1
- package/dist/ui/assets/{openrouter-oauth-C4aGXNeF.js → openrouter-oauth-CFlNuCUA.js} +1 -1
- package/dist/ui/assets/{openrouter-oauth-ov7dnUnh.js → openrouter-oauth-Cj61jTQa.js} +1 -1
- package/dist/ui/assets/{panel-rpc-handlers-CDsWA3s8.js → panel-rpc-handlers-CLsTGXPT.js} +2 -2
- package/dist/ui/assets/{panelize-shell-CXpZWzgn.js → panelize-shell-Bf9yr7Qf.js} +1 -1
- package/dist/ui/assets/{port-bridge-client-CzOd_VUf.js → port-bridge-client-Ds3xwapn.js} +1 -1
- package/dist/ui/assets/provenance-DdmsQ3Nr.js +1 -0
- package/dist/ui/assets/{provider-BtT9_ZsY.js → provider-BD-0pPN8.js} +1 -1
- package/dist/ui/assets/{provider-2fDgJArv.js → provider-C66yENxs.js} +2 -2
- package/dist/ui/assets/provider-settings-DysstkNf.js +1 -0
- package/dist/ui/assets/provider-store-access-BeX0OGEs.js +1 -0
- package/dist/ui/assets/provider-store-access-Cc45yVjV.js +1 -0
- package/dist/ui/assets/{providers-BWmPj6ob.js → providers-BtR_Oc-m.js} +2 -2
- package/dist/ui/assets/{providers-D_mm-oKv.js → providers-C2bj3jKv.js} +1 -1
- package/dist/ui/assets/proxied-fetch-DEK8DT-z.js +1 -0
- package/dist/ui/assets/{quick-llm-Ch_X9AH9.js → quick-llm-D-nM71kO.js} +1 -1
- package/dist/ui/assets/{recovery-screen-Ldb8xS5S.js → recovery-screen-YuMG99R0.js} +1 -1
- package/dist/ui/assets/{redirect-uri-C6BjkDE0.js → redirect-uri-CgKdq7-K.js} +1 -1
- package/dist/ui/assets/rest-ops-Bps3Ay-k.js +1 -0
- package/dist/ui/assets/{run-DXUToOLa.js → run-CDicdmnK.js} +1 -1
- package/dist/ui/assets/{run-Bmb6g7U2.js → run-CdzvBpS0.js} +3 -3
- package/dist/ui/assets/{run-xG3-BOwR.js → run-D5LoTYwZ.js} +1 -1
- package/dist/ui/assets/{secrets-bridge-client-DBeju3mP.js → secrets-bridge-client-ZpBL1Zhl.js} +1 -1
- package/dist/ui/assets/{session-jsonl-DlNZajGM.js → session-jsonl-By2mMQqb.js} +1 -1
- package/dist/ui/assets/{setup-feature-flags-remote-D-IPtvuE.js → setup-feature-flags-remote-BuBwyibG.js} +1 -1
- package/dist/ui/assets/sidecar-repair-DRkuMyU_.js +1 -0
- package/dist/ui/assets/{silent-renew-backoff-BocHwLdw.js → silent-renew-backoff-CS_htRXY.js} +1 -1
- package/dist/ui/assets/{skills-CTDnQTUW.js → skills-Cg34jB8I.js} +1 -1
- package/dist/ui/assets/slicc-version-DbT116fI.js +1 -0
- package/dist/ui/assets/{snapshot-BZ-_n6uU.js → snapshot-B6SF0dpr.js} +1 -1
- package/dist/ui/assets/{snapshot-features-BWzc1ERL.js → snapshot-features-A-jmNbR9.js} +1 -1
- package/dist/ui/assets/{soundscape-CowYlYSr.js → soundscape-smt9sQsm.js} +1 -1
- package/dist/ui/assets/{speak-rCiisrK7.js → speak-DeYQY3R8.js} +1 -1
- package/dist/ui/assets/{sprinkle-manager-DaVvW4Pl.js → sprinkle-manager-q3Ku7Zvz.js} +1 -1
- package/dist/ui/assets/{sprinkle-renderer-CDcbztf1.js → sprinkle-renderer-B9Re-_rl.js} +1 -1
- package/dist/ui/assets/{state-DHftNsPN.js → state-NPdZC8rd.js} +1 -1
- package/dist/ui/assets/{store-CTM4679Z.js → store-BbRPb7Di.js} +1 -1
- package/dist/ui/assets/{store-MZJeSOYF.js → store-BfdKxUK3.js} +1 -1
- package/dist/ui/assets/{sudo-DLRz2gLx.js → sudo-CtUlj7iw.js} +1 -1
- package/dist/ui/assets/{sync-dialog-BUFCYPxf.js → sync-dialog-CXgM1Uvh.js} +1 -1
- package/dist/ui/assets/{transformers-env-BfjOXz9f.js → transformers-env-C7dJtjmn.js} +1 -1
- package/dist/ui/assets/{tray-runtime-config--rtR6Gxd.js → tray-runtime-config-CRZxBChd.js} +1 -1
- package/dist/ui/assets/{tray-sidecar-BP70hrzZ.js → tray-sidecar-BtFzGslJ.js} +1 -1
- package/dist/ui/assets/types-Dzr0qYl3.js +1 -0
- package/dist/ui/assets/{upgrade-detection-Be8A7nY7.js → upgrade-detection-BWpOvoVx.js} +1 -1
- package/dist/ui/assets/upskill-lick-skip-ZGNBIEXS.js +1 -0
- package/dist/ui/assets/{voice-reply-BtqQr2DX.js → voice-reply-CCkglCEp.js} +1 -1
- package/dist/ui/assets/{wc-attach-zW7DuRo7.js → wc-attach-C8CgBet1.js} +2 -2
- package/dist/ui/assets/{wc-extension-DGYx9jMG.js → wc-extension-CHb4P34s.js} +2 -2
- package/dist/ui/assets/{wc-follower-Bc6Mmmda.js → wc-follower-DmIZM7ao.js} +3 -3
- package/dist/ui/assets/{wc-follower-oauth-DOaUEVw8.js → wc-follower-oauth-CQgoqG9v.js} +1 -1
- package/dist/ui/assets/{wc-gelatiere-CdqOnO6V.js → wc-gelatiere-B8JhDjog.js} +1 -1
- package/dist/ui/assets/{wc-live-BrKyGs-7.js → wc-live-gCevk2Vh.js} +5 -5
- package/dist/ui/assets/{wc-nav-dSAbBsA5.js → wc-nav-CpHEdfwe.js} +2 -2
- package/dist/ui/assets/{wc-onboarding-C1uyya9g.js → wc-onboarding-iwpabkC3.js} +2 -2
- package/dist/ui/assets/{wc-placeholder-Cw7uGVuJ.js → wc-placeholder-Cnl3OQfj.js} +3 -3
- package/dist/ui/assets/{wc-secret-request-CwA6DiWC.js → wc-secret-request-B9oB6kAt.js} +1 -1
- package/dist/ui/assets/{wc-settings-DJEhDr23.js → wc-settings-_0roTDdf.js} +3 -3
- package/dist/ui/assets/{wc-shell-C-DwVQCG.js → wc-shell-DkJPUxGT.js} +3 -3
- package/dist/ui/assets/{wc-shortcut-config-D9HBEfqv.js → wc-shortcut-config-CIup4nE8.js} +1 -1
- package/dist/ui/assets/{wc-shortcuts-7i4KncH2.js → wc-shortcuts-C9SNe-vm.js} +1 -1
- package/dist/ui/assets/{wc-sprinkles-DsRJYYRI.js → wc-sprinkles-C7_z9Yfm.js} +2 -2
- package/dist/ui/assets/{wc-transcript-export-Dq9HNkpA.js → wc-transcript-export-DnNvjokD.js} +2 -2
- package/dist/ui/assets/{wc-tray-f6xi76Bq.js → wc-tray-DhWe0-Ie.js} +4 -4
- package/dist/ui/assets/welcome-detection-BbyyaOna.js +1 -0
- package/dist/ui/assets/{whisper-session-B6knWfce.js → whisper-session-CMXLPRdV.js} +2 -2
- package/dist/ui/assets/{xai-grok-B0KRInrx.js → xai-grok-BNRMntfw.js} +1 -1
- package/dist/ui/assets/{xai-grok-BIfBT2BL.js → xai-grok-BiF6I0r6.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/ui/packages/webapp/index.html +3 -3
- package/package.json +3 -3
- package/dist/ui/assets/SKILL-CuHGVKei.js +0 -1
- package/dist/ui/assets/api-endpoint-Lx_FU5cP.js +0 -1
- package/dist/ui/assets/dist-B3W7EgGh.js +0 -12
- package/dist/ui/assets/dist-CmI2JIly.js +0 -18
- package/dist/ui/assets/github-install-Bk-KTJD-.js +0 -1
- package/dist/ui/assets/github-zip-Dvb4zHOl.js +0 -1
- package/dist/ui/assets/provider-settings-D2AYwHOa.js +0 -1
- package/dist/ui/assets/provider-store-access-BdvWm9Kj.js +0 -1
- package/dist/ui/assets/provider-store-access-C5A5Se3D.js +0 -1
- package/dist/ui/assets/proxied-fetch-B57RqgUb.js +0 -1
- package/dist/ui/assets/rest-ops-CB2up_K0.js +0 -1
- package/dist/ui/assets/sidecar-repair-DRUvDMSe.js +0 -1
- package/dist/ui/assets/slicc-version-CWl4uhV3.js +0 -1
- package/dist/ui/assets/welcome-detection-DmqQ-hO4.js +0 -1
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { ChildProcess } from 'node:child_process';
|
|
2
|
+
import WebSocket from 'ws';
|
|
2
3
|
export interface LaunchedBrowserHandle {
|
|
3
4
|
launchedBrowserProcess: ChildProcess | null;
|
|
4
5
|
launchedBrowserLabel: string;
|
|
5
6
|
}
|
|
7
|
+
export interface BrowserShutdownDependencies {
|
|
8
|
+
fetchImpl?: typeof fetch;
|
|
9
|
+
createWebSocket?: (url: string) => WebSocket;
|
|
10
|
+
}
|
|
6
11
|
/** Best-effort graceful close of the launched browser, escalating to SIGKILL. */
|
|
7
|
-
export declare function closeLaunchedBrowserGracefully(state: LaunchedBrowserHandle, cdpPort: number): Promise<void>;
|
|
12
|
+
export declare function closeLaunchedBrowserGracefully(state: LaunchedBrowserHandle, cdpPort: number, dependencies?: BrowserShutdownDependencies): Promise<void>;
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import WebSocket from 'ws';
|
|
2
2
|
import { probeCdpAlive } from './chrome-launch.js';
|
|
3
3
|
/** Best-effort graceful close of the launched browser, escalating to SIGKILL. */
|
|
4
|
-
export async function closeLaunchedBrowserGracefully(state, cdpPort) {
|
|
4
|
+
export async function closeLaunchedBrowserGracefully(state, cdpPort, dependencies = {}) {
|
|
5
5
|
const browser = state.launchedBrowserProcess;
|
|
6
6
|
if (!browser)
|
|
7
7
|
return;
|
|
8
8
|
try {
|
|
9
|
-
const res = await fetch(`http://127.0.0.1:${cdpPort}/json/version`, {
|
|
9
|
+
const res = await (dependencies.fetchImpl ?? fetch)(`http://127.0.0.1:${cdpPort}/json/version`, {
|
|
10
10
|
signal: AbortSignal.timeout(500),
|
|
11
11
|
});
|
|
12
12
|
const json = (await res.json());
|
|
13
|
-
const browserWs = new WebSocket(json.webSocketDebuggerUrl);
|
|
13
|
+
const browserWs = (dependencies.createWebSocket ?? ((url) => new WebSocket(url)))(json.webSocketDebuggerUrl);
|
|
14
14
|
await new Promise((resolve, reject) => {
|
|
15
15
|
browserWs.on('open', () => {
|
|
16
16
|
try {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ChildProcess } from 'child_process';
|
|
2
2
|
import { existsSync, readdirSync } from 'fs';
|
|
3
|
+
import { request as httpRequest } from 'http';
|
|
3
4
|
/**
|
|
4
5
|
* Default startup timeout for Chrome's CDP listener. Overridable via the
|
|
5
6
|
* `SLICC_CDP_LAUNCH_TIMEOUT_MS` environment variable so cold/contended CI
|
|
@@ -235,6 +236,8 @@ export interface ProbeCdpAliveOptions {
|
|
|
235
236
|
* for ours.
|
|
236
237
|
*/
|
|
237
238
|
expectedWebSocketPath?: string | null;
|
|
239
|
+
/** Test seam for synchronous request-construction failures. */
|
|
240
|
+
requestImpl?: typeof httpRequest;
|
|
238
241
|
}
|
|
239
242
|
/**
|
|
240
243
|
* Single-shot HTTP probe of Chrome's `/json/version` endpoint. Resolves
|
|
@@ -800,6 +800,7 @@ function defaultPidIsAlive(pid) {
|
|
|
800
800
|
export function probeCdpAlive(port, options = {}) {
|
|
801
801
|
const timeoutMs = options.timeoutMs ?? 500;
|
|
802
802
|
const expectedWebSocketPath = options.expectedWebSocketPath ?? null;
|
|
803
|
+
const requestImpl = options.requestImpl ?? httpRequest;
|
|
803
804
|
return new Promise((resolve) => {
|
|
804
805
|
let resolved = false;
|
|
805
806
|
const settle = (alive) => {
|
|
@@ -819,7 +820,7 @@ export function probeCdpAlive(port, options = {}) {
|
|
|
819
820
|
}
|
|
820
821
|
let req;
|
|
821
822
|
try {
|
|
822
|
-
req =
|
|
823
|
+
req = requestImpl({
|
|
823
824
|
host: '127.0.0.1',
|
|
824
825
|
port,
|
|
825
826
|
path: '/json/version',
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { BRIDGE_ROLE_FOLLOWER, BRIDGE_ROLE_LEADER, BRIDGE_ROLE_QUERY_PARAM } from './_shared/index.js';
|
|
2
|
-
import { type ChildProcess } from 'child_process';
|
|
1
|
+
import { BRIDGE_ROLE_FOLLOWER, BRIDGE_ROLE_LEADER, BRIDGE_ROLE_QUERY_PARAM, type CDPPayload } from './_shared/index.js';
|
|
2
|
+
import { type ChildProcess, type SpawnOptions } from 'child_process';
|
|
3
|
+
import * as http from 'http';
|
|
4
|
+
import { WebSocket } from 'ws';
|
|
3
5
|
import { type ElectronInspectableTarget } from './electron-runtime.js';
|
|
4
6
|
export { BRIDGE_ROLE_FOLLOWER, BRIDGE_ROLE_LEADER, BRIDGE_ROLE_QUERY_PARAM };
|
|
5
7
|
/**
|
|
@@ -72,6 +74,8 @@ interface RunningProcessInfo {
|
|
|
72
74
|
commandLine: string;
|
|
73
75
|
executablePath: string | null;
|
|
74
76
|
}
|
|
77
|
+
/** Send a CDP command over a target WebSocket; returns the message id. */
|
|
78
|
+
export type CdpSend = (method: string, params?: CDPPayload) => number;
|
|
75
79
|
/**
|
|
76
80
|
* One element of a CDP `Network.Request.postDataEntries` array. `bytes` is the
|
|
77
81
|
* protocol `binary` type — base64 in the JSON wire form, so it survives bodies
|
|
@@ -86,11 +90,31 @@ export declare function findMatchingElectronAppPids(runningProcesses: RunningPro
|
|
|
86
90
|
export declare class ElectronAppAlreadyRunningError extends Error {
|
|
87
91
|
constructor(message: string);
|
|
88
92
|
}
|
|
93
|
+
export declare function parseUnixProcessList(stdout: string): RunningProcessInfo[];
|
|
94
|
+
export declare function parseWindowsProcessList(stdout: string): RunningProcessInfo[];
|
|
95
|
+
export declare function listRunningProcesses(platform?: NodeJS.Platform, run?: (command: string, args: string[]) => Promise<{
|
|
96
|
+
stdout: string;
|
|
97
|
+
}>): Promise<RunningProcessInfo[]>;
|
|
98
|
+
export declare function isPidAlive(pid: number, signal?: (pid: number, signal: 0) => void): boolean;
|
|
99
|
+
interface ProcessLifecycle {
|
|
100
|
+
isAlive: (pid: number) => boolean;
|
|
101
|
+
kill: (pid: number, signal?: NodeJS.Signals) => void;
|
|
102
|
+
now: () => number;
|
|
103
|
+
sleep: (milliseconds: number) => Promise<void>;
|
|
104
|
+
}
|
|
105
|
+
export declare function waitForPidsToExit(pids: number[], timeoutMs?: number, lifecycle?: ProcessLifecycle): Promise<boolean>;
|
|
106
|
+
export declare function terminateRunningApp(pids: number[], lifecycle?: ProcessLifecycle): Promise<void>;
|
|
107
|
+
export declare function findRunningElectronAppPids(appPath: string, platform?: NodeJS.Platform): Promise<number[]>;
|
|
89
108
|
export declare function launchElectronApp(options: {
|
|
90
109
|
appPath: string;
|
|
91
110
|
cdpPort: number;
|
|
92
111
|
kill: boolean;
|
|
93
112
|
platform?: NodeJS.Platform;
|
|
113
|
+
}, dependencies?: {
|
|
114
|
+
exists?: (path: string) => boolean;
|
|
115
|
+
findRunningPids?: (appPath: string, platform?: NodeJS.Platform) => Promise<number[]>;
|
|
116
|
+
terminate?: (pids: number[]) => Promise<void>;
|
|
117
|
+
spawn?: (command: string, args: string[], options: SpawnOptions) => ChildProcess;
|
|
94
118
|
}): Promise<{
|
|
95
119
|
child: ChildProcess;
|
|
96
120
|
displayName: string;
|
|
@@ -109,6 +133,12 @@ export declare function decodePngPixels(base64Data: string): {
|
|
|
109
133
|
* sampling a grid of pixels for performance.
|
|
110
134
|
*/
|
|
111
135
|
export declare function computeAverageLuminance(pixels: Buffer, width: number, height: number, sampleStep?: number): number;
|
|
136
|
+
/**
|
|
137
|
+
* Detect whether the target app is using a light or dark theme by taking
|
|
138
|
+
* a CDP screenshot and analyzing the average luminance.
|
|
139
|
+
* Returns 'light' or 'dark'.
|
|
140
|
+
*/
|
|
141
|
+
export declare function detectAppThemeFromScreenshot(ws: WebSocket, send: CdpSend): Promise<'light' | 'dark'>;
|
|
112
142
|
/**
|
|
113
143
|
* Resolve the `Fetch.enable` origin pattern for CSP-bypass escalation. Mirrors
|
|
114
144
|
* swift-server's `OverlayTargetSession.fetchProxyOrigin` (Wave 5): prefer the
|
|
@@ -159,6 +189,20 @@ export declare function decodeCdpRequestPostBody(request: {
|
|
|
159
189
|
hasPostData?: boolean;
|
|
160
190
|
postDataEntries?: CdpPostDataEntry[];
|
|
161
191
|
}): CdpPostBody;
|
|
192
|
+
/**
|
|
193
|
+
* Translate a Node http(s) response's headers into the array shape required by
|
|
194
|
+
* `Fetch.fulfillRequest`, stripping CSP and other hop-by-hop headers that are
|
|
195
|
+
* invalid in fulfill responses and rewriting `content-length` to match the
|
|
196
|
+
* actually-buffered body length. Returns whether any CSP header was stripped
|
|
197
|
+
* so the caller can log it.
|
|
198
|
+
*/
|
|
199
|
+
export declare function buildFulfillResponseHeaders(rawHeaders: http.IncomingHttpHeaders, contentLength: number): {
|
|
200
|
+
responseHeaders: Array<{
|
|
201
|
+
name: string;
|
|
202
|
+
value: string;
|
|
203
|
+
}>;
|
|
204
|
+
strippedCSP: boolean;
|
|
205
|
+
};
|
|
162
206
|
/**
|
|
163
207
|
* Pre-built bootstrap scripts for thin-mode injection — one per overlay
|
|
164
208
|
* role. The injector picks `leader` for the first injected target and
|
|
@@ -175,6 +219,9 @@ export interface ThinBootstrapSet {
|
|
|
175
219
|
*/
|
|
176
220
|
status: string;
|
|
177
221
|
}
|
|
222
|
+
export declare function logOverlayReinjectionFailure(context: string, error: unknown): void;
|
|
223
|
+
export declare const logPresenceReinjectionFailure: (error: unknown) => void;
|
|
224
|
+
export declare const logNavigationReinjectionFailure: (error: unknown) => void;
|
|
178
225
|
/**
|
|
179
226
|
* Empty-viewport message shown by the status-only overlay for an app that
|
|
180
227
|
* denies the overlay iframe's network egress. Kept in sync with swift-server's
|
|
@@ -265,6 +312,7 @@ export declare class ElectronOverlayInjector {
|
|
|
265
312
|
private readonly onEgressBlocked?;
|
|
266
313
|
private syncTimer;
|
|
267
314
|
private syncing;
|
|
315
|
+
private readonly runScheduledSync;
|
|
268
316
|
private constructor();
|
|
269
317
|
static create(options: {
|
|
270
318
|
cdpPort: number;
|
|
@@ -318,6 +366,18 @@ export declare class ElectronOverlayInjector {
|
|
|
318
366
|
_testingEgressBlockedTargets(): ReadonlySet<string>;
|
|
319
367
|
/** Test-only: seed a target as already egress-blocked. */
|
|
320
368
|
_testingSeedEgressBlockedTarget(url: string): void;
|
|
369
|
+
/** Test-only: seed a connection so sync/stop cleanup can be driven deterministically. */
|
|
370
|
+
_testingSeedConnection(targetId: string, connection: Pick<WebSocket, 'close'>): void;
|
|
371
|
+
/** Test-only: drive the identity-guarded connection cleanup. */
|
|
372
|
+
_testingDropConnection(targetId: string, connection: WebSocket): void;
|
|
373
|
+
/** Test-only: invoke the same callback installed by `start()`. */
|
|
374
|
+
_testingRunScheduledSync(): Promise<void>;
|
|
375
|
+
/** Test-only: exercise the overlay-loaded timeout classifier directly. */
|
|
376
|
+
_testingProbeOverlayIframeLoaded(ws: WebSocket, send: CdpSend): Promise<boolean>;
|
|
377
|
+
/** Test-only: exercise the overlay-eviction timeout classifier directly. */
|
|
378
|
+
_testingProbeOverlayEvicted(ws: WebSocket, send: CdpSend): Promise<boolean>;
|
|
379
|
+
/** Test-only: drive a paused Fetch event without requiring the full escalation flow. */
|
|
380
|
+
_testingHandleFetchRequestPaused(ws: WebSocket, send: CdpSend, msg: unknown): void;
|
|
321
381
|
/** Test-only: drive a single `syncTargets` pass without `start()`'s interval. */
|
|
322
382
|
_testingSyncTargets(): Promise<void>;
|
|
323
383
|
/** Test-only: close any sockets opened by `_testingConnectToTarget`. */
|
|
@@ -325,6 +385,7 @@ export declare class ElectronOverlayInjector {
|
|
|
325
385
|
start(): Promise<void>;
|
|
326
386
|
stop(): void;
|
|
327
387
|
private syncTargets;
|
|
388
|
+
private dropConnectionIfCurrent;
|
|
328
389
|
/**
|
|
329
390
|
* Check if the overlay iframe loaded successfully by evaluating a probe
|
|
330
391
|
* script. Walks the `<slicc-launcher>` host's (open) shadow root to find the
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BRIDGE_ROLE_FOLLOWER, BRIDGE_ROLE_LEADER, BRIDGE_ROLE_QUERY_PARAM, ELECTRON_OVERLAY_APP_PATH, SLICC_HOSTED_ORIGIN, TRAY_QUERY_PARAM, } from './_shared/index.js';
|
|
2
|
-
import { execFile as nodeExecFile, spawn } from 'child_process';
|
|
2
|
+
import { execFile as nodeExecFile, spawn, } from 'child_process';
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
4
|
import { readFile } from 'fs/promises';
|
|
5
5
|
import * as http from 'http';
|
|
@@ -105,7 +105,7 @@ export class ElectronAppAlreadyRunningError extends Error {
|
|
|
105
105
|
this.name = 'ElectronAppAlreadyRunningError';
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
|
-
function parseUnixProcessList(stdout) {
|
|
108
|
+
export function parseUnixProcessList(stdout) {
|
|
109
109
|
const processes = [];
|
|
110
110
|
for (const rawLine of stdout.split('\n')) {
|
|
111
111
|
const line = rawLine.trim();
|
|
@@ -125,7 +125,7 @@ function parseUnixProcessList(stdout) {
|
|
|
125
125
|
}
|
|
126
126
|
return processes;
|
|
127
127
|
}
|
|
128
|
-
function parseWindowsProcessList(stdout) {
|
|
128
|
+
export function parseWindowsProcessList(stdout) {
|
|
129
129
|
const trimmed = stdout.trim();
|
|
130
130
|
if (!trimmed)
|
|
131
131
|
return [];
|
|
@@ -139,93 +139,101 @@ function parseWindowsProcessList(stdout) {
|
|
|
139
139
|
}))
|
|
140
140
|
.filter((processInfo) => Number.isFinite(processInfo.pid) && processInfo.pid > 0);
|
|
141
141
|
}
|
|
142
|
-
async function listRunningProcesses(platform = process.platform) {
|
|
142
|
+
export async function listRunningProcesses(platform = process.platform, run = execFile) {
|
|
143
143
|
if (platform === 'win32') {
|
|
144
|
-
const { stdout } = await
|
|
144
|
+
const { stdout } = await run('powershell', [
|
|
145
145
|
'-NoProfile',
|
|
146
146
|
'-Command',
|
|
147
147
|
'Get-CimInstance Win32_Process | Select-Object ProcessId, ExecutablePath, CommandLine | ConvertTo-Json -Compress',
|
|
148
148
|
]);
|
|
149
149
|
return parseWindowsProcessList(stdout);
|
|
150
150
|
}
|
|
151
|
-
const { stdout } = await
|
|
151
|
+
const { stdout } = await run('ps', ['-ax', '-o', 'pid=', '-o', 'command=']);
|
|
152
152
|
return parseUnixProcessList(stdout);
|
|
153
153
|
}
|
|
154
|
-
function isPidAlive(pid) {
|
|
154
|
+
export function isPidAlive(pid, signal = process.kill.bind(process)) {
|
|
155
155
|
try {
|
|
156
|
-
|
|
156
|
+
signal(pid, 0);
|
|
157
157
|
return true;
|
|
158
158
|
}
|
|
159
159
|
catch {
|
|
160
160
|
return false;
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
163
|
+
const defaultProcessLifecycle = {
|
|
164
|
+
isAlive: isPidAlive,
|
|
165
|
+
kill: (pid, signal) => process.kill(pid, signal),
|
|
166
|
+
now: Date.now,
|
|
167
|
+
sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
168
|
+
};
|
|
169
|
+
export async function waitForPidsToExit(pids, timeoutMs = 5000, lifecycle = defaultProcessLifecycle) {
|
|
170
|
+
const deadline = lifecycle.now() + timeoutMs;
|
|
171
|
+
while (lifecycle.now() < deadline) {
|
|
172
|
+
if (pids.every((pid) => !lifecycle.isAlive(pid)))
|
|
167
173
|
return true;
|
|
168
|
-
await
|
|
174
|
+
await lifecycle.sleep(100);
|
|
169
175
|
}
|
|
170
|
-
return pids.every((pid) => !
|
|
176
|
+
return pids.every((pid) => !lifecycle.isAlive(pid));
|
|
171
177
|
}
|
|
172
|
-
async function terminateRunningApp(pids) {
|
|
178
|
+
export async function terminateRunningApp(pids, lifecycle = defaultProcessLifecycle) {
|
|
173
179
|
for (const pid of pids) {
|
|
174
|
-
if (!
|
|
180
|
+
if (!lifecycle.isAlive(pid))
|
|
175
181
|
continue;
|
|
176
182
|
try {
|
|
177
|
-
|
|
183
|
+
lifecycle.kill(pid);
|
|
178
184
|
}
|
|
179
185
|
catch {
|
|
180
186
|
// Ignore individual termination failures and fall back to force-kill below if needed.
|
|
181
187
|
}
|
|
182
188
|
}
|
|
183
|
-
if (await waitForPidsToExit(pids))
|
|
189
|
+
if (await waitForPidsToExit(pids, 5000, lifecycle))
|
|
184
190
|
return;
|
|
185
191
|
for (const pid of pids) {
|
|
186
|
-
if (!
|
|
192
|
+
if (!lifecycle.isAlive(pid))
|
|
187
193
|
continue;
|
|
188
194
|
try {
|
|
189
|
-
|
|
195
|
+
lifecycle.kill(pid, 'SIGKILL');
|
|
190
196
|
}
|
|
191
197
|
catch {
|
|
192
198
|
// Ignore final cleanup failures.
|
|
193
199
|
}
|
|
194
200
|
}
|
|
195
|
-
await waitForPidsToExit(pids, 3000);
|
|
201
|
+
await waitForPidsToExit(pids, 3000, lifecycle);
|
|
196
202
|
}
|
|
197
|
-
async function findRunningElectronAppPids(appPath, platform = process.platform) {
|
|
203
|
+
export async function findRunningElectronAppPids(appPath, platform = process.platform) {
|
|
198
204
|
const { processMatchPatterns } = buildElectronAppLaunchSpec(appPath, { cdpPort: 0, platform });
|
|
199
205
|
const runningProcesses = await listRunningProcesses(platform);
|
|
200
206
|
return findMatchingElectronAppPids(runningProcesses, processMatchPatterns);
|
|
201
207
|
}
|
|
202
|
-
export async function launchElectronApp(options) {
|
|
208
|
+
export async function launchElectronApp(options, dependencies = {}) {
|
|
203
209
|
const launchSpec = buildElectronAppLaunchSpec(options.appPath, {
|
|
204
210
|
cdpPort: options.cdpPort,
|
|
205
211
|
platform: options.platform,
|
|
206
212
|
});
|
|
207
|
-
|
|
213
|
+
const exists = dependencies.exists ?? existsSync;
|
|
214
|
+
if (!exists(launchSpec.resolvedAppPath)) {
|
|
208
215
|
throw new Error(`Electron app not found at ${launchSpec.resolvedAppPath}`);
|
|
209
216
|
}
|
|
210
|
-
if (!
|
|
217
|
+
if (!exists(launchSpec.command)) {
|
|
211
218
|
throw new Error(`Electron executable not found at ${launchSpec.command}. Pass the app executable path directly if needed.`);
|
|
212
219
|
}
|
|
213
|
-
const runningPids = await findRunningElectronAppPids(launchSpec.resolvedAppPath, options.platform);
|
|
220
|
+
const runningPids = await (dependencies.findRunningPids ?? findRunningElectronAppPids)(launchSpec.resolvedAppPath, options.platform);
|
|
214
221
|
const platform = options.platform ?? process.platform;
|
|
215
222
|
const isMacAppBundle = platform === 'darwin' && launchSpec.resolvedAppPath.toLowerCase().endsWith('.app');
|
|
216
223
|
if (runningPids.length > 0 && !options.kill) {
|
|
217
224
|
throw new ElectronAppAlreadyRunningError(`${launchSpec.displayName} is already running. Re-run with --kill to relaunch it with remote debugging enabled.`);
|
|
218
225
|
}
|
|
219
226
|
if (runningPids.length > 0) {
|
|
220
|
-
await terminateRunningApp(runningPids);
|
|
227
|
+
await (dependencies.terminate ?? terminateRunningApp)(runningPids);
|
|
221
228
|
}
|
|
229
|
+
const spawnProcess = dependencies.spawn ?? spawn;
|
|
222
230
|
const child = isMacAppBundle
|
|
223
|
-
?
|
|
231
|
+
? spawnProcess('open', ['-n', '-a', launchSpec.resolvedAppPath, '-W', '--args', ...launchSpec.args], {
|
|
224
232
|
env: process.env,
|
|
225
233
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
226
234
|
detached: false,
|
|
227
235
|
})
|
|
228
|
-
:
|
|
236
|
+
: spawnProcess(launchSpec.command, launchSpec.args, {
|
|
229
237
|
env: process.env,
|
|
230
238
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
231
239
|
detached: false,
|
|
@@ -369,7 +377,7 @@ export function computeAverageLuminance(pixels, width, height, sampleStep = 4) {
|
|
|
369
377
|
* a CDP screenshot and analyzing the average luminance.
|
|
370
378
|
* Returns 'light' or 'dark'.
|
|
371
379
|
*/
|
|
372
|
-
function detectAppThemeFromScreenshot(ws, send) {
|
|
380
|
+
export function detectAppThemeFromScreenshot(ws, send) {
|
|
373
381
|
return new Promise((resolve) => {
|
|
374
382
|
// Take a small JPEG screenshot for speed — we only need luminance
|
|
375
383
|
const screenshotId = send('Page.captureScreenshot', {
|
|
@@ -524,7 +532,7 @@ function buildProxyRequestHeaders(requestHeaders, bodyLength) {
|
|
|
524
532
|
* actually-buffered body length. Returns whether any CSP header was stripped
|
|
525
533
|
* so the caller can log it.
|
|
526
534
|
*/
|
|
527
|
-
function buildFulfillResponseHeaders(rawHeaders, contentLength) {
|
|
535
|
+
export function buildFulfillResponseHeaders(rawHeaders, contentLength) {
|
|
528
536
|
const HOP_BY_HOP = new Set([
|
|
529
537
|
'content-security-policy',
|
|
530
538
|
'content-security-policy-report-only',
|
|
@@ -558,6 +566,16 @@ function buildFulfillResponseHeaders(rawHeaders, contentLength) {
|
|
|
558
566
|
}
|
|
559
567
|
return { responseHeaders, strippedCSP };
|
|
560
568
|
}
|
|
569
|
+
export function logOverlayReinjectionFailure(context, error) {
|
|
570
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
571
|
+
console.warn(`[electron-float] ${context} re-injection failed: ${message}`);
|
|
572
|
+
}
|
|
573
|
+
export const logPresenceReinjectionFailure = (error) => {
|
|
574
|
+
logOverlayReinjectionFailure('Presence-check', error);
|
|
575
|
+
};
|
|
576
|
+
export const logNavigationReinjectionFailure = (error) => {
|
|
577
|
+
logOverlayReinjectionFailure('Navigation', error);
|
|
578
|
+
};
|
|
561
579
|
/**
|
|
562
580
|
* Empty-viewport message shown by the status-only overlay for an app that
|
|
563
581
|
* denies the overlay iframe's network egress. Kept in sync with swift-server's
|
|
@@ -680,6 +698,9 @@ export class ElectronOverlayInjector {
|
|
|
680
698
|
onEgressBlocked;
|
|
681
699
|
syncTimer = null;
|
|
682
700
|
syncing = false;
|
|
701
|
+
runScheduledSync = async () => {
|
|
702
|
+
await this.syncTargets();
|
|
703
|
+
};
|
|
683
704
|
constructor(cdpPort, servePort, thinBootstraps, bridgeToken, onEgressBlocked, probeDelayMs = 1500, presenceCheckIntervalMs = ELECTRON_OVERLAY_PRESENCE_CHECK_INTERVAL_MS) {
|
|
684
705
|
this.cdpPort = cdpPort;
|
|
685
706
|
this.servePort = servePort;
|
|
@@ -761,6 +782,30 @@ export class ElectronOverlayInjector {
|
|
|
761
782
|
_testingSeedEgressBlockedTarget(url) {
|
|
762
783
|
this.egressBlockedTargets.add(url);
|
|
763
784
|
}
|
|
785
|
+
/** Test-only: seed a connection so sync/stop cleanup can be driven deterministically. */
|
|
786
|
+
_testingSeedConnection(targetId, connection) {
|
|
787
|
+
this.connections.set(targetId, connection);
|
|
788
|
+
}
|
|
789
|
+
/** Test-only: drive the identity-guarded connection cleanup. */
|
|
790
|
+
_testingDropConnection(targetId, connection) {
|
|
791
|
+
this.dropConnectionIfCurrent(targetId, connection);
|
|
792
|
+
}
|
|
793
|
+
/** Test-only: invoke the same callback installed by `start()`. */
|
|
794
|
+
async _testingRunScheduledSync() {
|
|
795
|
+
await this.runScheduledSync();
|
|
796
|
+
}
|
|
797
|
+
/** Test-only: exercise the overlay-loaded timeout classifier directly. */
|
|
798
|
+
_testingProbeOverlayIframeLoaded(ws, send) {
|
|
799
|
+
return this.probeOverlayIframeLoaded(ws, send);
|
|
800
|
+
}
|
|
801
|
+
/** Test-only: exercise the overlay-eviction timeout classifier directly. */
|
|
802
|
+
_testingProbeOverlayEvicted(ws, send) {
|
|
803
|
+
return this.probeOverlayEvicted(ws, send);
|
|
804
|
+
}
|
|
805
|
+
/** Test-only: drive a paused Fetch event without requiring the full escalation flow. */
|
|
806
|
+
_testingHandleFetchRequestPaused(ws, send, msg) {
|
|
807
|
+
this.handleFetchRequestPaused(ws, send, msg);
|
|
808
|
+
}
|
|
764
809
|
/** Test-only: drive a single `syncTargets` pass without `start()`'s interval. */
|
|
765
810
|
async _testingSyncTargets() {
|
|
766
811
|
await this.syncTargets();
|
|
@@ -779,9 +824,7 @@ export class ElectronOverlayInjector {
|
|
|
779
824
|
}
|
|
780
825
|
async start() {
|
|
781
826
|
await this.syncTargets();
|
|
782
|
-
this.syncTimer = setInterval(
|
|
783
|
-
void this.syncTargets();
|
|
784
|
-
}, ELECTRON_OVERLAY_SYNC_INTERVAL_MS);
|
|
827
|
+
this.syncTimer = setInterval(this.runScheduledSync, ELECTRON_OVERLAY_SYNC_INTERVAL_MS);
|
|
785
828
|
}
|
|
786
829
|
stop() {
|
|
787
830
|
if (this.syncTimer) {
|
|
@@ -853,6 +896,11 @@ export class ElectronOverlayInjector {
|
|
|
853
896
|
this.syncing = false;
|
|
854
897
|
}
|
|
855
898
|
}
|
|
899
|
+
dropConnectionIfCurrent(targetId, connection) {
|
|
900
|
+
if (this.connections.get(targetId) === connection) {
|
|
901
|
+
this.connections.delete(targetId);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
856
904
|
/**
|
|
857
905
|
* Check if the overlay iframe loaded successfully by evaluating a probe
|
|
858
906
|
* script. Walks the `<slicc-launcher>` host's (open) shadow root to find the
|
|
@@ -1310,10 +1358,7 @@ export class ElectronOverlayInjector {
|
|
|
1310
1358
|
ws.on('open', () => {
|
|
1311
1359
|
this.handleSocketOpen(ws, send, target, state);
|
|
1312
1360
|
presenceTimer = setInterval(() => {
|
|
1313
|
-
void this.reinjectIfEvicted(ws, send, target, state).catch(
|
|
1314
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1315
|
-
console.warn(`[electron-float] Presence-check re-injection failed: ${message}`);
|
|
1316
|
-
});
|
|
1361
|
+
void this.reinjectIfEvicted(ws, send, target, state).catch(logPresenceReinjectionFailure);
|
|
1317
1362
|
}, this.presenceCheckIntervalMs);
|
|
1318
1363
|
});
|
|
1319
1364
|
// Handle CDP events: lifecycle events and Fetch interception
|
|
@@ -1332,10 +1377,7 @@ export class ElectronOverlayInjector {
|
|
|
1332
1377
|
// navigation a no-op (its marker is wiped, so the new-document hook owns it).
|
|
1333
1378
|
const isMainFrameNavigated = msg.method === 'Page.frameNavigated' && !msg.params?.frame?.parentId;
|
|
1334
1379
|
if (msg.method === 'Page.navigatedWithinDocument' || isMainFrameNavigated) {
|
|
1335
|
-
void this.reinjectIfEvicted(ws, send, target, state).catch(
|
|
1336
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1337
|
-
console.warn(`[electron-float] Navigation re-injection failed: ${message}`);
|
|
1338
|
-
});
|
|
1380
|
+
void this.reinjectIfEvicted(ws, send, target, state).catch(logNavigationReinjectionFailure);
|
|
1339
1381
|
}
|
|
1340
1382
|
if (msg.method === 'Fetch.requestPaused' && state.fetchProxyActive) {
|
|
1341
1383
|
this.handleFetchRequestPaused(ws, send, msg);
|
|
@@ -1352,17 +1394,13 @@ export class ElectronOverlayInjector {
|
|
|
1352
1394
|
});
|
|
1353
1395
|
ws.on('close', () => {
|
|
1354
1396
|
clearPresenceTimer();
|
|
1355
|
-
|
|
1356
|
-
this.connections.delete(targetId);
|
|
1357
|
-
}
|
|
1397
|
+
this.dropConnectionIfCurrent(targetId, ws);
|
|
1358
1398
|
});
|
|
1359
1399
|
ws.on('error', (error) => {
|
|
1360
1400
|
clearPresenceTimer();
|
|
1361
1401
|
const message = error instanceof Error ? error.message : String(error);
|
|
1362
1402
|
console.error(`[electron-float] Overlay target connection failed for ${target.url}:`, message);
|
|
1363
|
-
|
|
1364
|
-
this.connections.delete(targetId);
|
|
1365
|
-
}
|
|
1403
|
+
this.dropConnectionIfCurrent(targetId, ws);
|
|
1366
1404
|
});
|
|
1367
1405
|
}
|
|
1368
1406
|
}
|
|
@@ -32,12 +32,25 @@ export declare const PORT_HASH_RANGE = 40;
|
|
|
32
32
|
* Exported for testing.
|
|
33
33
|
*/
|
|
34
34
|
export declare function hashString(str: string, max: number): number;
|
|
35
|
+
/**
|
|
36
|
+
* Try to listen on a specific port and host, returning the assigned port.
|
|
37
|
+
*/
|
|
38
|
+
export declare function tryListenOnPort(port: number, host: string): Promise<number>;
|
|
39
|
+
/**
|
|
40
|
+
* Check if a port is available on both IPv4 (127.0.0.1) and IPv6 (::1).
|
|
41
|
+
* On macOS, `localhost` resolves to `::1`, so we need to check both.
|
|
42
|
+
*/
|
|
43
|
+
export declare function isPortAvailable(port: number, listen?: (port: number, host: string) => Promise<number>): Promise<boolean>;
|
|
44
|
+
/**
|
|
45
|
+
* Find an available port starting from the given port.
|
|
46
|
+
*/
|
|
47
|
+
export declare function findAvailablePort(startPort: number, maxAttempts?: number, available?: (port: number) => Promise<boolean>): Promise<number>;
|
|
35
48
|
/**
|
|
36
49
|
* Get a port for an Electron app based on its path.
|
|
37
50
|
* Uses hash-based offset from base port, with fallback to next available port
|
|
38
51
|
* starting from the preferred port (to stay in the app's "slot" range).
|
|
39
52
|
*/
|
|
40
|
-
export declare function getElectronAppPort(appPath: string, basePort: number): Promise<number>;
|
|
53
|
+
export declare function getElectronAppPort(appPath: string, basePort: number, available?: (port: number) => Promise<boolean>): Promise<number>;
|
|
41
54
|
/**
|
|
42
55
|
* Get both CDP and serve ports for an Electron app.
|
|
43
56
|
*/
|
|
@@ -46,6 +59,7 @@ export declare function getElectronAppPorts(appPath: string): Promise<{
|
|
|
46
59
|
servePort: number;
|
|
47
60
|
}>;
|
|
48
61
|
export declare function getElectronAppDisplayName(appPath: string): string;
|
|
62
|
+
export declare function isExecutableFile(path: string): boolean;
|
|
49
63
|
export declare function resolveElectronAppExecutablePath(appPath: string, platform?: NodeJS.Platform): string;
|
|
50
64
|
export declare function buildElectronAppProcessMatchPatterns(appPath: string, platform?: NodeJS.Platform): string[];
|
|
51
65
|
export declare function buildElectronAppLaunchSpec(appPath: string, options: {
|
|
@@ -22,7 +22,7 @@ export function hashString(str, max) {
|
|
|
22
22
|
/**
|
|
23
23
|
* Try to listen on a specific port and host, returning the assigned port.
|
|
24
24
|
*/
|
|
25
|
-
async function tryListenOnPort(port, host) {
|
|
25
|
+
export async function tryListenOnPort(port, host) {
|
|
26
26
|
const { createServer } = await import('net');
|
|
27
27
|
return new Promise((resolve, reject) => {
|
|
28
28
|
const server = createServer();
|
|
@@ -38,11 +38,11 @@ async function tryListenOnPort(port, host) {
|
|
|
38
38
|
* Check if a port is available on both IPv4 (127.0.0.1) and IPv6 (::1).
|
|
39
39
|
* On macOS, `localhost` resolves to `::1`, so we need to check both.
|
|
40
40
|
*/
|
|
41
|
-
async function isPortAvailable(port) {
|
|
41
|
+
export async function isPortAvailable(port, listen = tryListenOnPort) {
|
|
42
42
|
try {
|
|
43
|
-
await
|
|
43
|
+
await listen(port, '127.0.0.1');
|
|
44
44
|
try {
|
|
45
|
-
await
|
|
45
|
+
await listen(port, '::1');
|
|
46
46
|
}
|
|
47
47
|
catch (err) {
|
|
48
48
|
// ::1 may not be available on some systems — only fail on EADDRINUSE
|
|
@@ -59,10 +59,10 @@ async function isPortAvailable(port) {
|
|
|
59
59
|
/**
|
|
60
60
|
* Find an available port starting from the given port.
|
|
61
61
|
*/
|
|
62
|
-
async function findAvailablePort(startPort, maxAttempts = 100) {
|
|
62
|
+
export async function findAvailablePort(startPort, maxAttempts = 100, available = isPortAvailable) {
|
|
63
63
|
for (let i = 0; i < maxAttempts; i++) {
|
|
64
64
|
const port = startPort + i;
|
|
65
|
-
if (await
|
|
65
|
+
if (await available(port)) {
|
|
66
66
|
return port;
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -73,14 +73,14 @@ async function findAvailablePort(startPort, maxAttempts = 100) {
|
|
|
73
73
|
* Uses hash-based offset from base port, with fallback to next available port
|
|
74
74
|
* starting from the preferred port (to stay in the app's "slot" range).
|
|
75
75
|
*/
|
|
76
|
-
export async function getElectronAppPort(appPath, basePort) {
|
|
76
|
+
export async function getElectronAppPort(appPath, basePort, available = isPortAvailable) {
|
|
77
77
|
const offset = hashString(appPath, PORT_HASH_RANGE);
|
|
78
78
|
const preferredPort = basePort + offset;
|
|
79
|
-
if (await
|
|
79
|
+
if (await available(preferredPort)) {
|
|
80
80
|
return preferredPort;
|
|
81
81
|
}
|
|
82
82
|
// Fallback: find next available port starting from preferred (stay in slot range)
|
|
83
|
-
return findAvailablePort(preferredPort + 1);
|
|
83
|
+
return findAvailablePort(preferredPort + 1, 100, available);
|
|
84
84
|
}
|
|
85
85
|
/**
|
|
86
86
|
* Get both CDP and serve ports for an Electron app.
|
|
@@ -98,7 +98,7 @@ export function getElectronAppDisplayName(appPath) {
|
|
|
98
98
|
}
|
|
99
99
|
return fileName || trimmedPath;
|
|
100
100
|
}
|
|
101
|
-
function isExecutableFile(path) {
|
|
101
|
+
export function isExecutableFile(path) {
|
|
102
102
|
try {
|
|
103
103
|
accessSync(path, constants.X_OK);
|
|
104
104
|
return true;
|
|
@@ -69,13 +69,11 @@ class MaybeGunzipTransform extends Transform {
|
|
|
69
69
|
cb();
|
|
70
70
|
return;
|
|
71
71
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
this.#endGunzipOrFinish(cb);
|
|
78
|
-
});
|
|
72
|
+
// A non-empty undecided head can only be one byte: two bytes decide in
|
|
73
|
+
// `_transform`. One byte cannot be gzip magic, so flush it directly.
|
|
74
|
+
this.#decided = true;
|
|
75
|
+
this.#onDecided?.(false);
|
|
76
|
+
cb(null, this.#head);
|
|
79
77
|
return;
|
|
80
78
|
}
|
|
81
79
|
this.#endGunzipOrFinish(cb);
|
|
@@ -2,6 +2,7 @@ import { type Account, imsTokenExpiry } from './_cloud_core/src/index.js';
|
|
|
2
2
|
import type { Express } from 'express';
|
|
3
3
|
import type { SecretStore } from './secrets/types.js';
|
|
4
4
|
export { imsTokenExpiry };
|
|
5
|
+
export declare function readHostedConeConfig(path?: string, read?: (path: string, encoding: BufferEncoding) => string, warn?: (...args: unknown[]) => void): string | null;
|
|
5
6
|
export interface HostedBootstrapPayload {
|
|
6
7
|
model?: string;
|
|
7
8
|
effortLevel?: string;
|
|
@@ -24,6 +24,17 @@ import { requireLoopback } from './cloud-status.js';
|
|
|
24
24
|
// Buffer-free) so the worker's back-compat + resume paths reuse the same logic.
|
|
25
25
|
export { imsTokenExpiry };
|
|
26
26
|
const CONE_CONFIG_PATH = '/slicc/cone-config.json';
|
|
27
|
+
export function readHostedConeConfig(path = CONE_CONFIG_PATH, read = readFileSync, warn = console.warn) {
|
|
28
|
+
try {
|
|
29
|
+
return read(path, 'utf-8');
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
if (err.code !== 'ENOENT') {
|
|
33
|
+
warn('[hosted-bootstrap] failed to read cone-config.json:', err);
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
27
38
|
export function buildHostedBootstrapPayload(sources) {
|
|
28
39
|
const raw = sources.readConeConfig();
|
|
29
40
|
if (raw) {
|
|
@@ -55,20 +66,7 @@ export function buildHostedBootstrapPayload(sources) {
|
|
|
55
66
|
export function registerHostedBootstrapEndpoint(app, options) {
|
|
56
67
|
app.get('/api/hosted-bootstrap', requireLoopback, (_req, res) => {
|
|
57
68
|
const payload = buildHostedBootstrapPayload({
|
|
58
|
-
readConeConfig: () =>
|
|
59
|
-
try {
|
|
60
|
-
return readFileSync(CONE_CONFIG_PATH, 'utf-8');
|
|
61
|
-
}
|
|
62
|
-
catch (err) {
|
|
63
|
-
// A missing file is the normal pre-feature/back-compat case. Any other
|
|
64
|
-
// error (EACCES, EIO, …) means the config exists but is unreadable —
|
|
65
|
-
// log it so a silent fallback to legacy/empty doesn't hide the cause.
|
|
66
|
-
if (err.code !== 'ENOENT') {
|
|
67
|
-
console.warn('[hosted-bootstrap] failed to read cone-config.json:', err);
|
|
68
|
-
}
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
},
|
|
69
|
+
readConeConfig: () => readHostedConeConfig(),
|
|
72
70
|
getLegacyAdobeToken: () => options.secretStore.get('ADOBE_IMS_TOKEN')?.value,
|
|
73
71
|
});
|
|
74
72
|
res.json(payload);
|