sliccy 4.22.1 → 4.24.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/dist/node-server/bridge-security.d.ts +135 -0
- package/dist/node-server/bridge-security.js +245 -0
- package/dist/node-server/index.js +290 -48
- package/dist/node-server/launch-url.d.ts +7 -0
- package/dist/node-server/launch-url.js +12 -3
- package/dist/ui/assets/{account-store-ibgiPI6V.js → account-store-BXT0RMpF.js} +2 -2
- package/dist/ui/assets/{account-store-4ECV8DGT.js → account-store-CcqPXFk-.js} +2 -2
- package/dist/ui/assets/{adobe--TTRq2If.js → adobe-Dvkc5_oM.js} +1 -1
- package/dist/ui/assets/adobe-Dyvxqy9s.js +1 -0
- package/dist/ui/assets/{agent-message-to-chat-biPexDDJ.js → agent-message-to-chat-Cihra740.js} +1 -1
- package/dist/ui/assets/{apps-BOvmrPiX.js → apps-CPyXd9Mk.js} +1 -1
- package/dist/ui/assets/{azure-openai-Bf3TmJ0y.js → azure-openai-BEec8FHP.js} +1 -1
- package/dist/ui/assets/{azure-openai-DjY0l719.js → azure-openai-lMn0o-9C.js} +1 -1
- package/dist/ui/assets/{bsh-watchdog-BVbry18Y.js → bsh-watchdog-Dgr_OA1N.js} +1 -1
- package/dist/ui/assets/{cdp-Cbi5Katl.js → cdp-D4JkCTey.js} +3 -3
- package/dist/ui/assets/{connect-surface-DSA8Cz3z.js → connect-surface-BzhaxP0L.js} +1 -1
- package/dist/ui/assets/dip-CIhLisNq.js +1 -0
- package/dist/ui/assets/{dist-BElcUj_V.js → dist-ChTQy8at.js} +1 -1
- package/dist/ui/assets/{dist-DP3_V4hR.js → dist-DIjck1q7.js} +1 -1
- package/dist/ui/assets/{es-DcI8bAtA.js → es-B8GxLLfB.js} +1 -1
- package/dist/ui/assets/{fs-DKIJdRIl.js → fs-ATai1P5Q.js} +2 -2
- package/dist/ui/assets/{fs-BQEkHWJV.js → fs-xQUivn-8.js} +1 -1
- package/dist/ui/assets/{github-BQBIaABC.js → github-C6vLSHER.js} +2 -2
- package/dist/ui/assets/{github-0YLv6nLk.js → github-CUVBUETF.js} +1 -1
- package/dist/ui/assets/{github-copilot-DwyTNkwX.js → github-copilot-BqMS3_4S.js} +1 -1
- package/dist/ui/assets/{github-copilot-Co_dxCXe.js → github-copilot-ggchxbZd.js} +1 -1
- package/dist/ui/assets/{hear-DyUkFgOW.js → hear-Bm8tL0z2.js} +1 -1
- package/dist/ui/assets/{kernel-worker-DvmZjl24.js → kernel-worker-CqATTm08.js} +1026 -1026
- package/dist/ui/assets/{kokoro-engine-BnI4fL_p.js → kokoro-engine-BzgKX1A1.js} +1 -1
- package/dist/ui/assets/{lick-ws-bridge-B0_yyU80.js → lick-ws-bridge-DFQu2sM8.js} +1 -1
- package/dist/ui/assets/{local-llm-DnPuhfxf.js → local-llm-D09l_nRy.js} +1 -1
- package/dist/ui/assets/{main-cherry-Bap_NTtV.js → main-cherry-B1mI8ArD.js} +1 -1
- package/dist/ui/assets/{main-DRGnmneg.js → main-sWMjBmPA.js} +3 -3
- package/dist/ui/assets/{mount-ByUTNmzv.js → mount-D7RXkT90.js} +1 -1
- package/dist/ui/assets/{mount-CguScujU.js → mount-sEjqfa02.js} +2 -2
- package/dist/ui/assets/{new-session-CwnHjnd0.js → new-session-B0xwp5hO.js} +2 -2
- package/dist/ui/assets/{oauth-bootstrap-Py4nRtkM.js → oauth-bootstrap-Uz3oksBR.js} +2 -2
- package/dist/ui/assets/{openai-codex-B2qaIik0.js → openai-codex-BMr_sYj2.js} +1 -1
- package/dist/ui/assets/{openai-codex-0L44_lSb.js → openai-codex-d2uBt8CN.js} +1 -1
- package/dist/ui/assets/{panel-rpc-handlers-D7cbHQYp.js → panel-rpc-handlers-1U8M-tQU.js} +1 -1
- package/dist/ui/assets/{provider-BI7Y3RBy.js → provider-B_3MhsjL.js} +2 -2
- package/dist/ui/assets/{provider-riIj2TBx.js → provider-BbwD43pe.js} +1 -1
- package/dist/ui/assets/provider-store-access-Cop2VuvO.js +1 -0
- package/dist/ui/assets/provider-store-access-_8NADMDP.js +1 -0
- package/dist/ui/assets/{providers-fz0Sm6ju.js → providers-DRQ-n4v0.js} +1 -1
- package/dist/ui/assets/proxied-fetch-3ooUUAsF.js +1 -0
- package/dist/ui/assets/{quick-llm-B9FBP8Bs.js → quick-llm-CHWi-jSD.js} +1 -1
- package/dist/ui/assets/session-freezer-mzV5cASX.js +1 -0
- package/dist/ui/assets/setup-sudo-DSt9eI8G.js +1 -0
- package/dist/ui/assets/{speak-DVyJlZaU.js → speak-Cr05k3rj.js} +1 -1
- package/dist/ui/assets/{sprinkle-manager-O6TFBdwJ.js → sprinkle-manager-B7xN0WFf.js} +1 -1
- package/dist/ui/assets/{store-BsGfQ4o5.js → store-CfuBHukr.js} +1 -1
- package/dist/ui/assets/{sudo-C-n2ZT92.js → sudo-CnG9mh3w.js} +1 -1
- package/dist/ui/assets/{transformers-env-Dpnex-yX.js → transformers-env-BEM0HkoC.js} +1 -1
- package/dist/ui/assets/{tray-leave-runtime-MYPnSSR_.js → tray-leave-runtime-CD-xjbXc.js} +1 -1
- package/dist/ui/assets/{upgrade-detection-uSF5N6Ew.js → upgrade-detection-CwNs_M6E.js} +1 -1
- package/dist/ui/assets/{wc-attach--tUlEIYg.js → wc-attach-D7JTvNE3.js} +2 -2
- package/dist/ui/assets/{wc-detached-D6RhlJwc.js → wc-detached-DfgIMcuk.js} +1 -1
- package/dist/ui/assets/{wc-extension-DeqnwDO0.js → wc-extension-BNpR2nAA.js} +2 -2
- package/dist/ui/assets/{wc-live-DIowT4eF.js → wc-live-cbtCpYYl.js} +12 -12
- package/dist/ui/assets/{wc-nav-DI67gu77.js → wc-nav-BVFjm1zp.js} +2 -2
- package/dist/ui/assets/{wc-onboarding-BIG01YkV.js → wc-onboarding-C-b3gfT3.js} +2 -2
- package/dist/ui/assets/{wc-placeholder-Nry9IKS2.js → wc-placeholder-CLNwZTGs.js} +3 -3
- package/dist/ui/assets/{wc-settings-C8nodqL5.js → wc-settings-BRCPA5ys.js} +2 -2
- package/dist/ui/assets/{wc-shell-Ng-YAcjd.js → wc-shell-CeWID7O0.js} +3 -3
- package/dist/ui/assets/{wc-sprinkles-BWzmaQEm.js → wc-sprinkles-DYTck3a-.js} +2 -2
- package/dist/ui/assets/{wc-tray-CbkvHS3n.js → wc-tray-ByE1fpIQ.js} +3 -3
- package/dist/ui/assets/{xai-grok-uc4Un-5T.js → xai-grok-BcQs9t1L.js} +1 -1
- package/dist/ui/assets/{xai-grok-CZfi469b.js → xai-grok-Bx-LJDfy.js} +1 -1
- package/dist/ui/index.html +2 -2
- package/dist/ui/packages/webapp/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/adobe-CQr1UAcb.js +0 -1
- package/dist/ui/assets/dip-6KkNyCHS.js +0 -1
- package/dist/ui/assets/provider-store-access-BHYTqSuW.js +0 -1
- package/dist/ui/assets/provider-store-access-BP2Nfrp_.js +0 -1
- package/dist/ui/assets/proxied-fetch-Dg114MVx.js +0 -1
- package/dist/ui/assets/proxy-error-9UvXMpzG.js +0 -1
- package/dist/ui/assets/session-freezer-BAK6UVLW.js +0 -1
- package/dist/ui/assets/setup-sudo-BMcUrWtK.js +0 -1
|
@@ -10,6 +10,7 @@ import { homedir } from 'os';
|
|
|
10
10
|
import { basename, dirname, join, resolve } from 'path';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
12
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
13
|
+
import { BRIDGE_TOKEN_HEADER, buildCorsHeaders, buildPnaPreflightHeaders, isLoopbackBridgeOrigin, mintBridgeToken, selectBridgeSubprotocol, validateBridgeToken, validateBridgeUpgrade, } from './bridge-security.js';
|
|
13
14
|
import { applyCdpUnmask } from './cdp-proxy/cdp-unmask.js';
|
|
14
15
|
import { createCdpSessionUrlTracker } from './cdp-proxy/session-url-tracker.js';
|
|
15
16
|
import { buildChromeLaunchArgs, clearStaleDevToolsActivePort, ensureQaProfileScaffold, findChromeExecutable, legacyChromeCandidates, migrateLegacyDefaultChromeProfile, planChromeSpawn, resolveChromeLaunchProfile, waitForCdpPort, } from './chrome-launch.js';
|
|
@@ -65,6 +66,13 @@ const SERVE_ONLY = RUNTIME_FLAGS.serveOnly;
|
|
|
65
66
|
const ELECTRON_MODE = RUNTIME_FLAGS.electron;
|
|
66
67
|
const ELECTRON_APP = RUNTIME_FLAGS.electronApp;
|
|
67
68
|
const KILL_EXISTING_ELECTRON_APP = RUNTIME_FLAGS.kill;
|
|
69
|
+
/**
|
|
70
|
+
* Standalone thin-bridge mode: production CLI with no bundled UI. The
|
|
71
|
+
* launched Chrome opens a sliccy.ai-hosted leader; node-server is a thin
|
|
72
|
+
* /cdp bridge + /api surface. Dev / electron / serve-only / hosted all
|
|
73
|
+
* keep the bundled UI path (a separate path B / hosted task covers those).
|
|
74
|
+
*/
|
|
75
|
+
const THIN_BRIDGE_MODE = !DEV_MODE && !SERVE_ONLY && !ELECTRON_MODE && !RUNTIME_FLAGS.hosted;
|
|
68
76
|
// ---------------------------------------------------------------------------
|
|
69
77
|
// File logger — persistent log file in ~/.slicc/logs/
|
|
70
78
|
// ---------------------------------------------------------------------------
|
|
@@ -300,6 +308,7 @@ function createServerState() {
|
|
|
300
308
|
overlayInjector: null,
|
|
301
309
|
shuttingDown: false,
|
|
302
310
|
discoveredTrayJoinUrl: RUNTIME_FLAGS.joinUrl ?? null,
|
|
311
|
+
bridgeToken: THIN_BRIDGE_MODE ? mintBridgeToken() : null,
|
|
303
312
|
cdpUrl: null,
|
|
304
313
|
chromeWs: null,
|
|
305
314
|
activeClientWs: null,
|
|
@@ -465,15 +474,33 @@ async function launchElectronTarget(state) {
|
|
|
465
474
|
throw error;
|
|
466
475
|
}
|
|
467
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* Resolve the leader origin Chrome should open in thin-bridge mode. Prefers
|
|
479
|
+
* an explicit `--lead <url>` / `WORKER_BASE_URL` so dev can point at staging;
|
|
480
|
+
* defaults to production sliccy.ai.
|
|
481
|
+
*/
|
|
482
|
+
function resolveThinLeaderOrigin() {
|
|
483
|
+
const explicit = RUNTIME_FLAGS.leadWorkerBaseUrl ?? process.env['WORKER_BASE_URL'] ?? null;
|
|
484
|
+
if (explicit) {
|
|
485
|
+
return explicit.replace(/\/+$/, '');
|
|
486
|
+
}
|
|
487
|
+
return 'https://www.sliccy.ai';
|
|
488
|
+
}
|
|
468
489
|
/** Build the Chrome launch URL, appending hosted-runtime + prompt query params. */
|
|
469
490
|
function buildBrowserLaunchUrl(state) {
|
|
491
|
+
// Thin-bridge standalone: Chrome opens the hosted leader directly; the
|
|
492
|
+
// local node-server serves no UI at all. Bridge coordinates ride as
|
|
493
|
+
// query params so the leader can discover + authenticate /cdp.
|
|
494
|
+
const serveOriginForLaunch = THIN_BRIDGE_MODE && state.bridgeToken ? resolveThinLeaderOrigin() : state.serveOrigin;
|
|
470
495
|
let url = resolveCliBrowserLaunchUrl({
|
|
471
|
-
serveOrigin:
|
|
496
|
+
serveOrigin: serveOriginForLaunch,
|
|
472
497
|
lead: RUNTIME_FLAGS.lead,
|
|
473
498
|
leadWorkerBaseUrl: RUNTIME_FLAGS.leadWorkerBaseUrl,
|
|
474
499
|
envWorkerBaseUrl: process.env['WORKER_BASE_URL'] ?? null,
|
|
475
500
|
join: RUNTIME_FLAGS.join,
|
|
476
501
|
joinUrl: RUNTIME_FLAGS.joinUrl,
|
|
502
|
+
bridgeWsUrl: THIN_BRIDGE_MODE && state.bridgeToken ? `ws://localhost:${state.servePort}/cdp` : null,
|
|
503
|
+
bridgeToken: state.bridgeToken,
|
|
477
504
|
});
|
|
478
505
|
if (RUNTIME_FLAGS.hosted) {
|
|
479
506
|
url += `${url.includes('?') ? '&' : '?'}runtime=hosted-leader`;
|
|
@@ -487,6 +514,11 @@ function buildBrowserLaunchUrl(state) {
|
|
|
487
514
|
else if (RUNTIME_FLAGS.lead) {
|
|
488
515
|
console.log(`Lead launch URL: ${url}`);
|
|
489
516
|
}
|
|
517
|
+
else if (THIN_BRIDGE_MODE) {
|
|
518
|
+
// Print WITHOUT the bridgeToken — it's a session capability.
|
|
519
|
+
const sanitized = url.replace(/([?&])bridgeToken=[^&]+/, '$1bridgeToken=<redacted>');
|
|
520
|
+
console.log(`Thin-bridge launch URL: ${sanitized}`);
|
|
521
|
+
}
|
|
490
522
|
return url;
|
|
491
523
|
}
|
|
492
524
|
function resolveChromeProfileOrExit(state) {
|
|
@@ -602,11 +634,56 @@ function closeWebSocketQuietly(ws) {
|
|
|
602
634
|
/* ignore */
|
|
603
635
|
}
|
|
604
636
|
}
|
|
605
|
-
/**
|
|
606
|
-
|
|
637
|
+
/**
|
|
638
|
+
* Reject a WebSocket upgrade before `handleUpgrade` runs. Writes a minimal
|
|
639
|
+
* HTTP/1.1 response and destroys the underlying socket. RFC 6455 allows 401
|
|
640
|
+
* / 403 here; we use 401 so a caller that retries with the correct
|
|
641
|
+
* subprotocol/origin doesn't trip permanent CORS-style caches.
|
|
642
|
+
*/
|
|
643
|
+
function rejectUpgradeUnauthorized(socket, reason) {
|
|
644
|
+
try {
|
|
645
|
+
socket.write(`HTTP/1.1 401 Unauthorized\r\n` +
|
|
646
|
+
`Content-Type: text/plain\r\n` +
|
|
647
|
+
`Connection: close\r\n` +
|
|
648
|
+
`Content-Length: ${Buffer.byteLength(reason)}\r\n` +
|
|
649
|
+
`\r\n${reason}`);
|
|
650
|
+
}
|
|
651
|
+
catch {
|
|
652
|
+
/* socket may already be gone */
|
|
653
|
+
}
|
|
654
|
+
try {
|
|
655
|
+
socket.destroy();
|
|
656
|
+
}
|
|
657
|
+
catch {
|
|
658
|
+
/* ignore */
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Route `/cdp` and `/licks-ws` upgrades to their servers; leave others for Vite HMR.
|
|
663
|
+
*
|
|
664
|
+
* When `bridgeToken !== null` (thin standalone mode) the `/cdp` upgrade is
|
|
665
|
+
* gated by `validateBridgeUpgrade`: bad origin or missing/wrong
|
|
666
|
+
* `Sec-WebSocket-Protocol` token = socket destroyed before
|
|
667
|
+
* `wss.emit('connection', ...)` ever fires. Legacy modes (dev / electron /
|
|
668
|
+
* serve-only / hosted) pass `null` to keep same-origin behavior unchanged.
|
|
669
|
+
* `/licks-ws` is loopback-only and stays ungated.
|
|
670
|
+
*/
|
|
671
|
+
function attachCdpUpgradeRouting(server, wss, lickWss, bridgeToken) {
|
|
607
672
|
server.on('upgrade', (request, socket, head) => {
|
|
608
673
|
const { pathname } = new URL(request.url, `http://${request.headers.host}`);
|
|
609
674
|
if (pathname === '/cdp') {
|
|
675
|
+
if (bridgeToken !== null) {
|
|
676
|
+
const gate = validateBridgeUpgrade({
|
|
677
|
+
origin: request.headers.origin,
|
|
678
|
+
subprotocolHeader: request.headers['sec-websocket-protocol'],
|
|
679
|
+
expectedToken: bridgeToken,
|
|
680
|
+
});
|
|
681
|
+
if (!gate.ok) {
|
|
682
|
+
console.warn(`[cdp-proxy] /cdp upgrade rejected: ${gate.reason}`);
|
|
683
|
+
rejectUpgradeUnauthorized(socket, gate.reason ?? 'rejected');
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
610
687
|
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
611
688
|
wss.emit('connection', ws, request);
|
|
612
689
|
});
|
|
@@ -736,6 +813,27 @@ function forwardClientFrame(state, data, ctx) {
|
|
|
736
813
|
console.log(`[cdp-proxy] Client→Chrome (DROPPED — no connection): ${preview}`);
|
|
737
814
|
}
|
|
738
815
|
}
|
|
816
|
+
/**
|
|
817
|
+
* Wait for `state.cdpPort` to be populated (i.e. `launchChromeTarget`'s
|
|
818
|
+
* `waitForCdpPort` has resolved). Used by clients that race ahead of the
|
|
819
|
+
* browser launch — primarily the thin-bridge case where `server.listen()`
|
|
820
|
+
* runs BEFORE `launchBrowser` so the hosted leader can connect to /cdp
|
|
821
|
+
* the moment Chrome opens it, even before Chrome's own CDP port is known.
|
|
822
|
+
*/
|
|
823
|
+
async function waitForServerCdpPort(state, timeoutMs = 30_000) {
|
|
824
|
+
if (state.cdpPort > 0)
|
|
825
|
+
return state.cdpPort;
|
|
826
|
+
const startedAt = Date.now();
|
|
827
|
+
while (state.cdpPort === 0 && Date.now() - startedAt < timeoutMs) {
|
|
828
|
+
if (state.shuttingDown)
|
|
829
|
+
throw new Error('Server shutting down');
|
|
830
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
831
|
+
}
|
|
832
|
+
if (state.cdpPort === 0) {
|
|
833
|
+
throw new Error('Chrome CDP port did not become available in time');
|
|
834
|
+
}
|
|
835
|
+
return state.cdpPort;
|
|
836
|
+
}
|
|
739
837
|
async function handleCdpClient(state, clientWs, ctx, cdpPort) {
|
|
740
838
|
try {
|
|
741
839
|
// Only one client active at a time — close the previous one.
|
|
@@ -766,7 +864,8 @@ async function handleCdpClient(state, clientWs, ctx, cdpPort) {
|
|
|
766
864
|
});
|
|
767
865
|
// NOW do async work — messages arriving during these awaits are buffered.
|
|
768
866
|
if (!state.cdpUrl) {
|
|
769
|
-
|
|
867
|
+
const port = cdpPort && cdpPort > 0 ? cdpPort : await waitForServerCdpPort(state);
|
|
868
|
+
state.cdpUrl = await waitForCDP(port);
|
|
770
869
|
console.log(`[cdp-proxy] CDP available at: ${state.cdpUrl}`);
|
|
771
870
|
}
|
|
772
871
|
await ensureChromeConnection(state, state.cdpUrl, ctx);
|
|
@@ -834,13 +933,16 @@ function createGracefulShutdown(state, deps) {
|
|
|
834
933
|
deps.wss.close();
|
|
835
934
|
// Stop accepting new HTTP connections.
|
|
836
935
|
deps.server.close();
|
|
837
|
-
|
|
936
|
+
// Read cdpPort from state — populated by `launchChromeTarget`; may be 0
|
|
937
|
+
// if shutdown fires before the browser ever launched.
|
|
938
|
+
await closeLaunchedBrowserGracefully(state, state.cdpPort);
|
|
838
939
|
process.exit(0);
|
|
839
940
|
};
|
|
840
941
|
}
|
|
841
942
|
/** Pre-connect to Chrome so the proxy is warm before the first client; hosted mode registers leader-restart once CDP is ready. */
|
|
842
|
-
async function preconnectCdp(state, ctx, app, servePort
|
|
943
|
+
async function preconnectCdp(state, ctx, app, servePort) {
|
|
843
944
|
try {
|
|
945
|
+
const cdpPort = state.cdpPort > 0 ? state.cdpPort : await waitForServerCdpPort(state);
|
|
844
946
|
state.cdpUrl = await waitForCDP(cdpPort);
|
|
845
947
|
console.log(`[cdp-proxy] Pre-connected: CDP available at ${state.cdpUrl}`);
|
|
846
948
|
await ensureChromeConnection(state, state.cdpUrl, ctx);
|
|
@@ -873,31 +975,61 @@ async function startOverlayInjector(state, cdpPort, servePort) {
|
|
|
873
975
|
console.error('[electron-float] Failed to start overlay injector:', message);
|
|
874
976
|
}
|
|
875
977
|
}
|
|
876
|
-
/**
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
978
|
+
/**
|
|
979
|
+
* Bind the HTTP server (and the /cdp WS upgrade handler attached to it) so
|
|
980
|
+
* incoming `ws://localhost/cdp` connections are accepted. Resolves when
|
|
981
|
+
* `server.listen` fires its 'listening' callback.
|
|
982
|
+
*
|
|
983
|
+
* Split out of the old `startCdpServer` so `main()` can `await` listening
|
|
984
|
+
* BEFORE `launchBrowser(state)` runs — without this ordering the hosted
|
|
985
|
+
* leader's eager `browser.connect()` (fired the moment Chrome opens
|
|
986
|
+
* sliccy.ai in thin-bridge mode) can race ahead of the bridge and fail
|
|
987
|
+
* its one-shot WebSocket handshake.
|
|
988
|
+
*/
|
|
989
|
+
function startListening(deps) {
|
|
990
|
+
const { server, fileLogger, servePort, serveOrigin, cdpPort } = deps;
|
|
991
|
+
return new Promise((resolve) => {
|
|
992
|
+
server.listen(servePort, '127.0.0.1', () => {
|
|
993
|
+
if (THIN_BRIDGE_MODE) {
|
|
994
|
+
console.log(`Thin /cdp bridge + /api at ${serveOrigin}`);
|
|
995
|
+
}
|
|
996
|
+
else {
|
|
997
|
+
console.log(`Serving UI at ${serveOrigin}`);
|
|
998
|
+
}
|
|
999
|
+
console.log(`CDP proxy at ws://localhost:${servePort}/cdp`);
|
|
1000
|
+
fileLogger.log('info', 'CLI server started', {
|
|
1001
|
+
port: servePort,
|
|
1002
|
+
cdpPort,
|
|
1003
|
+
devMode: DEV_MODE,
|
|
1004
|
+
electronMode: ELECTRON_MODE,
|
|
1005
|
+
});
|
|
1006
|
+
resolve();
|
|
887
1007
|
});
|
|
888
|
-
void preconnectCdp(state, ctx, app, servePort, cdpPort);
|
|
889
|
-
if (ELECTRON_MODE) {
|
|
890
|
-
void startOverlayInjector(state, cdpPort, servePort);
|
|
891
|
-
}
|
|
892
|
-
else {
|
|
893
|
-
setTimeout(() => {
|
|
894
|
-
attachConsoleForwarder(cdpPort, String(servePort)).catch((err) => {
|
|
895
|
-
console.error('[page] Console forwarder error:', err);
|
|
896
|
-
});
|
|
897
|
-
}, 2500);
|
|
898
|
-
}
|
|
899
1008
|
});
|
|
900
1009
|
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Post-listen warmup: pre-connects to Chrome's CDP (so the proxy is warm
|
|
1012
|
+
* before the first client), starts the Electron overlay injector, and
|
|
1013
|
+
* attaches the console forwarder. Reads `state.cdpPort` dynamically so
|
|
1014
|
+
* the value populated by `launchChromeTarget` is used regardless of when
|
|
1015
|
+
* this runs relative to the listen callback.
|
|
1016
|
+
*/
|
|
1017
|
+
function runCdpProxyWarmup(state, deps) {
|
|
1018
|
+
const { app, ctx, servePort } = deps;
|
|
1019
|
+
void preconnectCdp(state, ctx, app, servePort);
|
|
1020
|
+
if (ELECTRON_MODE) {
|
|
1021
|
+
void startOverlayInjector(state, state.cdpPort, servePort);
|
|
1022
|
+
}
|
|
1023
|
+
else if (!THIN_BRIDGE_MODE) {
|
|
1024
|
+
// Thin-bridge mode has no local UI page to forward console output from.
|
|
1025
|
+
const cdpPort = state.cdpPort;
|
|
1026
|
+
setTimeout(() => {
|
|
1027
|
+
attachConsoleForwarder(cdpPort, String(servePort)).catch((err) => {
|
|
1028
|
+
console.error('[page] Console forwarder error:', err);
|
|
1029
|
+
});
|
|
1030
|
+
}, 2500);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
901
1033
|
/**
|
|
902
1034
|
* Build the secret stores + masking pipeline shared by the fetch-proxy, the
|
|
903
1035
|
* /api/secrets routes, and the S3 sign-and-forward handler. Secret-load
|
|
@@ -924,20 +1056,98 @@ async function bootstrapSecrets() {
|
|
|
924
1056
|
}
|
|
925
1057
|
return { secretStore, secretProxy, oauthStore };
|
|
926
1058
|
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Thin-bridge CORS + PNA middleware. The hosted leader at sliccy.ai is a
|
|
1061
|
+
* cross-origin caller, so headers go on every response for allowlisted
|
|
1062
|
+
* origins (so /cdp's pre-WS preflight and every /api/* call succeed);
|
|
1063
|
+
* OPTIONS from an allowlisted origin short-circuits to 204 with the PNA
|
|
1064
|
+
* opt-in. Non-allowlisted origins fall through with no CORS headers, which
|
|
1065
|
+
* preserves same-origin (localhost) behavior unchanged.
|
|
1066
|
+
*
|
|
1067
|
+
* Additionally enforces the per-process bridge token on cross-origin
|
|
1068
|
+
* `/api/*` requests from REMOTE allowlisted origins (sliccy.ai) — the
|
|
1069
|
+
* origin allowlist alone is insufficient because any script on
|
|
1070
|
+
* `https://www.sliccy.ai` could otherwise reach the local node-server's
|
|
1071
|
+
* /api surface (secrets, fetch-proxy, etc.). Loopback allowlisted
|
|
1072
|
+
* origins (e.g. the locally-served OAuth callback at
|
|
1073
|
+
* `http://localhost:5710/auth/callback` POSTing to `/api/oauth-result`)
|
|
1074
|
+
* are exempt — they originate from this same server. OPTIONS preflights
|
|
1075
|
+
* are exempt because browsers strip custom headers from preflights.
|
|
1076
|
+
*/
|
|
1077
|
+
function createThinBridgeCorsMiddleware(bridgeToken) {
|
|
1078
|
+
return (req, res, next) => {
|
|
1079
|
+
// Reflect the requested headers so the agent's `bash curl -H …` (which
|
|
1080
|
+
// can carry arbitrary upstream headers through /api/fetch-proxy) is not
|
|
1081
|
+
// blocked by a hardcoded allowlist. Cross-origin preflights advertise
|
|
1082
|
+
// those headers via `Access-Control-Request-Headers`.
|
|
1083
|
+
const origin = req.headers.origin;
|
|
1084
|
+
const cors = buildCorsHeaders(origin, req.headers['access-control-request-headers']);
|
|
1085
|
+
if (cors) {
|
|
1086
|
+
for (const [k, v] of Object.entries(cors))
|
|
1087
|
+
res.setHeader(k, v);
|
|
1088
|
+
}
|
|
1089
|
+
if (req.method === 'OPTIONS' && cors) {
|
|
1090
|
+
for (const [k, v] of Object.entries(buildPnaPreflightHeaders()))
|
|
1091
|
+
res.setHeader(k, v);
|
|
1092
|
+
res.setHeader('Access-Control-Max-Age', '600');
|
|
1093
|
+
res.status(204).end();
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
// Token gate on /api/* from remote allowlisted origins. `cors` is only
|
|
1097
|
+
// truthy when the Origin is in the allowlist; loopback callers
|
|
1098
|
+
// (localhost/127.0.0.1) and no-Origin curl-style callers fall through
|
|
1099
|
+
// unchanged.
|
|
1100
|
+
if (cors &&
|
|
1101
|
+
req.path.startsWith('/api/') &&
|
|
1102
|
+
!isLoopbackBridgeOrigin(origin) &&
|
|
1103
|
+
!validateBridgeToken(req.headers[BRIDGE_TOKEN_HEADER.toLowerCase()], bridgeToken)) {
|
|
1104
|
+
res.status(403).json({ error: 'bridge-token-required' });
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
next();
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Build the `/cdp` `WebSocketServer`. `handleProtocols` echoes the matched
|
|
1112
|
+
* bridge subprotocol back in the 101 response (RFC 6455 §1.9). Returning
|
|
1113
|
+
* `false` makes the server omit the header, which makes the browser fail
|
|
1114
|
+
* the upgrade when a subprotocol was offered. Legacy modes (token=null)
|
|
1115
|
+
* pass the first offered subprotocol through unchanged.
|
|
1116
|
+
*/
|
|
1117
|
+
function createCdpWebSocketServer(bridgeToken) {
|
|
1118
|
+
return new WebSocketServer({
|
|
1119
|
+
noServer: true,
|
|
1120
|
+
handleProtocols: (protocols) => {
|
|
1121
|
+
if (bridgeToken !== null) {
|
|
1122
|
+
return selectBridgeSubprotocol([...protocols], bridgeToken) ?? false;
|
|
1123
|
+
}
|
|
1124
|
+
const first = protocols.values().next();
|
|
1125
|
+
return first.done ? false : first.value;
|
|
1126
|
+
},
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
927
1129
|
async function main() {
|
|
928
|
-
// Resolve ports
|
|
929
|
-
//
|
|
1130
|
+
// Resolve ports first; `launchBrowser` is deferred until AFTER
|
|
1131
|
+
// `server.listen()` so the /cdp bridge is accepting connections before
|
|
1132
|
+
// Chrome opens its target URL (the packaged-CLI thin-bridge mode opens
|
|
1133
|
+
// the hosted leader at sliccy.ai, whose eager `browser.connect()`
|
|
1134
|
+
// otherwise races ahead of the bridge listener and fails its one-shot
|
|
1135
|
+
// WebSocket handshake). `state.cdpPort` becomes valid inside
|
|
1136
|
+
// `launchChromeTarget` once Chrome announces its CDP port; consumers
|
|
1137
|
+
// that need it (`handleCdpClient`, `preconnectCdp`, the graceful-
|
|
1138
|
+
// shutdown handler) read it dynamically from `state`.
|
|
930
1139
|
const state = createServerState();
|
|
931
1140
|
await resolvePorts(state);
|
|
932
|
-
|
|
933
|
-
const { servePort: SERVE_PORT, cdpPort: CDP_PORT, serveOrigin: SERVE_ORIGIN } = state;
|
|
934
|
-
const discoveredTrayJoinUrl = state.discoveredTrayJoinUrl;
|
|
1141
|
+
const { servePort: SERVE_PORT, serveOrigin: SERVE_ORIGIN } = state;
|
|
935
1142
|
// 3. Set up express app with request logging
|
|
936
1143
|
const { secretStore, secretProxy, oauthStore } = await bootstrapSecrets();
|
|
937
1144
|
const app = express();
|
|
938
1145
|
app.use(requestLogger);
|
|
939
1146
|
// Append SLICC's standard RFC 8288 Link header set on every /api/* response.
|
|
940
1147
|
app.use(sliccLinksMiddleware());
|
|
1148
|
+
if (THIN_BRIDGE_MODE) {
|
|
1149
|
+
app.use(createThinBridgeCorsMiddleware(state.bridgeToken));
|
|
1150
|
+
}
|
|
941
1151
|
// ---------------------------------------------------------------------------
|
|
942
1152
|
// Lick system — WebSocket bridge for webhooks/crontasks (all logic in browser)
|
|
943
1153
|
// ---------------------------------------------------------------------------
|
|
@@ -964,7 +1174,11 @@ async function main() {
|
|
|
964
1174
|
(DEV_MODE
|
|
965
1175
|
? 'https://slicc-tray-hub-staging.minivelos.workers.dev'
|
|
966
1176
|
: 'https://www.sliccy.ai'),
|
|
967
|
-
|
|
1177
|
+
// Read dynamically from state — populated by `launchElectronTarget`
|
|
1178
|
+
// when an existing leader is discovered. `launchBrowser` now runs
|
|
1179
|
+
// after `server.listen()`, so this endpoint must NOT close over a
|
|
1180
|
+
// pre-launch snapshot or it would always return null.
|
|
1181
|
+
trayJoinUrl: state.discoveredTrayJoinUrl ?? null,
|
|
968
1182
|
});
|
|
969
1183
|
});
|
|
970
1184
|
// Localhost API descriptor — the discoverable surface advertised by the
|
|
@@ -1012,17 +1226,21 @@ async function main() {
|
|
|
1012
1226
|
registerFetchProxyRoute(app, { secretProxy });
|
|
1013
1227
|
// Create the HTTP server BEFORE Vite so we can register our upgrade handler first
|
|
1014
1228
|
const server = createServer(app);
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1229
|
+
// Thin-bridge mode: skip UI serving entirely. node-server becomes a
|
|
1230
|
+
// pure /cdp bridge + /api surface; Chrome opens the sliccy.ai-hosted
|
|
1231
|
+
// leader which talks to /cdp + /api cross-origin.
|
|
1232
|
+
if (!THIN_BRIDGE_MODE) {
|
|
1233
|
+
await attachUiServing(app, server, {
|
|
1234
|
+
devMode: DEV_MODE,
|
|
1235
|
+
hosted: RUNTIME_FLAGS.hosted,
|
|
1236
|
+
serveOrigin: SERVE_ORIGIN,
|
|
1237
|
+
uiDir: resolve(Dirname, '..', 'ui'),
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1021
1240
|
// 4. CDP WebSocket proxy at /cdp — noServer mode so Vite's dev middleware
|
|
1022
|
-
// doesn't intercept the upgrade
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
attachCdpUpgradeRouting(server, wss, lickWss);
|
|
1241
|
+
// doesn't intercept the upgrade.
|
|
1242
|
+
const wss = createCdpWebSocketServer(state.bridgeToken);
|
|
1243
|
+
attachCdpUpgradeRouting(server, wss, lickWss, state.bridgeToken);
|
|
1026
1244
|
const cdpCtx = {
|
|
1027
1245
|
wss,
|
|
1028
1246
|
secretProxy,
|
|
@@ -1033,7 +1251,6 @@ async function main() {
|
|
|
1033
1251
|
fileLogger,
|
|
1034
1252
|
wss,
|
|
1035
1253
|
server,
|
|
1036
|
-
cdpPort: CDP_PORT,
|
|
1037
1254
|
});
|
|
1038
1255
|
process.on('SIGINT', () => {
|
|
1039
1256
|
gracefulShutdown();
|
|
@@ -1053,17 +1270,42 @@ async function main() {
|
|
|
1053
1270
|
}
|
|
1054
1271
|
}
|
|
1055
1272
|
});
|
|
1273
|
+
await startCdpStack(state, { app, server, wss, cdpCtx, fileLogger });
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Bottom half of `main()`: bind the /cdp WS handler, start listening on
|
|
1277
|
+
* the HTTP server BEFORE launching the browser so the /cdp bridge is
|
|
1278
|
+
* accepting the hosted leader's eager connect the instant Chrome opens
|
|
1279
|
+
* sliccy.ai, then launch the browser (populates `state.cdpPort`), then
|
|
1280
|
+
* run the post-launch warmup (preconnect / overlay / console forwarder).
|
|
1281
|
+
*
|
|
1282
|
+
* Extracted purely to keep `main()` under the lint-enforced function-size
|
|
1283
|
+
* cap; not a meaningful boundary, just a co-locating helper.
|
|
1284
|
+
*/
|
|
1285
|
+
async function startCdpStack(state, deps) {
|
|
1286
|
+
const { app, server, wss, cdpCtx, fileLogger } = deps;
|
|
1287
|
+
const { servePort: SERVE_PORT, serveOrigin: SERVE_ORIGIN } = state;
|
|
1288
|
+
// Read `state.cdpPort` dynamically — populated by `launchChromeTarget`
|
|
1289
|
+
// AFTER `server.listen()` so we can't capture it here.
|
|
1056
1290
|
wss.on('connection', (clientWs) => {
|
|
1057
|
-
void handleCdpClient(state, clientWs, cdpCtx,
|
|
1291
|
+
void handleCdpClient(state, clientWs, cdpCtx, state.cdpPort);
|
|
1292
|
+
});
|
|
1293
|
+
await startListening({
|
|
1294
|
+
server,
|
|
1295
|
+
fileLogger,
|
|
1296
|
+
servePort: SERVE_PORT,
|
|
1297
|
+
serveOrigin: SERVE_ORIGIN,
|
|
1298
|
+
cdpPort: state.cdpPort,
|
|
1058
1299
|
});
|
|
1059
|
-
|
|
1300
|
+
await launchBrowser(state);
|
|
1301
|
+
runCdpProxyWarmup(state, {
|
|
1060
1302
|
app,
|
|
1061
1303
|
server,
|
|
1062
1304
|
ctx: cdpCtx,
|
|
1063
1305
|
fileLogger,
|
|
1064
1306
|
servePort: SERVE_PORT,
|
|
1065
1307
|
serveOrigin: SERVE_ORIGIN,
|
|
1066
|
-
cdpPort:
|
|
1308
|
+
cdpPort: state.cdpPort,
|
|
1067
1309
|
});
|
|
1068
1310
|
}
|
|
1069
1311
|
// ---------------------------------------------------------------------------
|
|
@@ -5,5 +5,12 @@ export interface CliLaunchUrlOptions {
|
|
|
5
5
|
envWorkerBaseUrl?: string | null;
|
|
6
6
|
join: boolean;
|
|
7
7
|
joinUrl?: string | null;
|
|
8
|
+
/**
|
|
9
|
+
* Thin-bridge coordinates appended as query params so the hosted leader can
|
|
10
|
+
* discover + authenticate the local /cdp WebSocket. Both must be set
|
|
11
|
+
* together; both unset = no bridge params (legacy / bundled UI path).
|
|
12
|
+
*/
|
|
13
|
+
bridgeWsUrl?: string | null;
|
|
14
|
+
bridgeToken?: string | null;
|
|
8
15
|
}
|
|
9
16
|
export declare function resolveCliBrowserLaunchUrl(options: CliLaunchUrlOptions): string;
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
+
import { BRIDGE_TOKEN_QUERY_PARAM, BRIDGE_WS_QUERY_PARAM } from './bridge-security.js';
|
|
1
2
|
import { buildCanonicalTrayLaunchUrl, normalizeTrayWorkerBaseUrl, parseTrayJoinUrl, } from './tray-url-shared.js';
|
|
3
|
+
function appendBridgeParams(url, opts) {
|
|
4
|
+
if (!opts.bridgeWsUrl || !opts.bridgeToken)
|
|
5
|
+
return url;
|
|
6
|
+
const params = new URLSearchParams();
|
|
7
|
+
params.set(BRIDGE_WS_QUERY_PARAM, opts.bridgeWsUrl);
|
|
8
|
+
params.set(BRIDGE_TOKEN_QUERY_PARAM, opts.bridgeToken);
|
|
9
|
+
return `${url}${url.includes('?') ? '&' : '?'}${params.toString()}`;
|
|
10
|
+
}
|
|
2
11
|
function buildTrayJoinLaunchUrl(locationHref, joinUrl) {
|
|
3
12
|
const parsedJoinUrl = parseTrayJoinUrl(joinUrl);
|
|
4
13
|
if (!parsedJoinUrl) {
|
|
@@ -21,14 +30,14 @@ export function resolveCliBrowserLaunchUrl(options) {
|
|
|
21
30
|
if (!options.joinUrl) {
|
|
22
31
|
throw new Error('The --join launch flow requires a tray join URL via --join <url> or --join=<url>.');
|
|
23
32
|
}
|
|
24
|
-
return buildTrayJoinLaunchUrl(options.serveOrigin, options.joinUrl);
|
|
33
|
+
return appendBridgeParams(buildTrayJoinLaunchUrl(options.serveOrigin, options.joinUrl), options);
|
|
25
34
|
}
|
|
26
35
|
if (!options.lead) {
|
|
27
|
-
return options.serveOrigin;
|
|
36
|
+
return appendBridgeParams(options.serveOrigin, options);
|
|
28
37
|
}
|
|
29
38
|
const workerBaseUrl = normalizeTrayWorkerBaseUrl(options.leadWorkerBaseUrl ?? options.envWorkerBaseUrl ?? null);
|
|
30
39
|
if (!workerBaseUrl) {
|
|
31
40
|
throw new Error('The --lead launch flow requires a tray worker base URL via --lead <url>, --lead=<url>, or WORKER_BASE_URL.');
|
|
32
41
|
}
|
|
33
|
-
return buildTrayLeadLaunchUrl(options.serveOrigin, workerBaseUrl);
|
|
42
|
+
return appendBridgeParams(buildTrayLeadLaunchUrl(options.serveOrigin, workerBaseUrl), options);
|
|
34
43
|
}
|