sliccy 4.23.0 → 4.25.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 +56 -2
- package/dist/node-server/bridge-security.js +186 -9
- package/dist/node-server/fetch-proxy-headers.d.ts +8 -2
- package/dist/node-server/fetch-proxy-headers.js +9 -2
- package/dist/node-server/index.js +153 -45
- package/dist/node-server/routes/fetch-proxy.d.ts +8 -0
- package/dist/node-server/routes/fetch-proxy.js +6 -1
- package/dist/ui/assets/{account-store-9y5Ab4vV.js → account-store-CCx84KSO.js} +2 -2
- package/dist/ui/assets/{account-store-CSLAx3pa.js → account-store-D6F5ht1K.js} +2 -2
- package/dist/ui/assets/adobe-B_gJbGrM.js +1 -0
- package/dist/ui/assets/adobe-D8jV1ibX.js +2 -0
- package/dist/ui/assets/{agent-message-to-chat-bEHX4iF4.js → agent-message-to-chat-Dt9iSVuQ.js} +1 -1
- package/dist/ui/assets/{apps-Dh-H9LYE.js → apps-CyKH98UU.js} +1 -1
- package/dist/ui/assets/{azure-openai-BpIYfrM-.js → azure-openai-CTCrw8ag.js} +1 -1
- package/dist/ui/assets/{azure-openai-C4YFzA3o.js → azure-openai-DHxbdJP2.js} +1 -1
- package/dist/ui/assets/{bsh-watchdog-CNmrTRAy.js → bsh-watchdog-DRf6YtLF.js} +1 -1
- package/dist/ui/assets/{cdp-BGgdiko1.js → cdp-D4JkCTey.js} +2 -2
- package/dist/ui/assets/{connect-surface-DTsPWNDo.js → connect-surface-BAkLQjYg.js} +1 -1
- package/dist/ui/assets/dip-CoQDOdRF.js +1 -0
- package/dist/ui/assets/{dist-CjQIX13Y.js → dist-DstXLyS2.js} +1 -1
- package/dist/ui/assets/{dist-B5wGuNlW.js → dist-Gjxc3CGe.js} +1 -1
- package/dist/ui/assets/{es-t4MB1uY0.js → es-BtePr83c.js} +1 -1
- package/dist/ui/assets/{fs-ry06ziaM.js → fs-CES5lb4o.js} +1 -1
- package/dist/ui/assets/{fs-BlhR8i_q.js → fs-D3A8iUb-.js} +2 -2
- package/dist/ui/assets/{github-Cr1bhcIq.js → github-BD9mji3m.js} +1 -1
- package/dist/ui/assets/{github-6R3ONWs4.js → github-BjJwZAID.js} +2 -2
- package/dist/ui/assets/{github-copilot-_vAY24oI.js → github-copilot-O5-ryiZR.js} +1 -1
- package/dist/ui/assets/{github-copilot-CPpSIAl_.js → github-copilot-yWe1M0K_.js} +1 -1
- package/dist/ui/assets/{hear-DLqP08Yk.js → hear-CFjk8o5A.js} +1 -1
- package/dist/ui/assets/{kernel-worker-ReGrzvdD.js → kernel-worker-DS4Sjo4b.js} +1029 -1029
- package/dist/ui/assets/{kokoro-engine-BeiKy8PB.js → kokoro-engine-TLc0ityO.js} +1 -1
- package/dist/ui/assets/{lick-ws-bridge-DZy9C70j.js → lick-ws-bridge-Fa3rBD7K.js} +1 -1
- package/dist/ui/assets/{local-llm-ehhQ9AeC.js → local-llm-77SlLHlX.js} +1 -1
- package/dist/ui/assets/main-RvuAuHo8.js +24 -0
- package/dist/ui/assets/{main-cherry-X1IxoqNY.js → main-cherry-B1mI8ArD.js} +1 -1
- package/dist/ui/assets/{mount-Crd-I-lS.js → mount-B3baMU-O.js} +1 -1
- package/dist/ui/assets/{mount-Cf2e3Vu8.js → mount-Dwoznret.js} +2 -2
- package/dist/ui/assets/{new-session-PJxD5xz-.js → new-session-BnW6Ia6h.js} +2 -2
- package/dist/ui/assets/{oauth-bootstrap-38UC9ABk.js → oauth-bootstrap-D4jfK89n.js} +2 -2
- package/dist/ui/assets/oauth-service-CoJ-pYVD.js +1 -0
- package/dist/ui/assets/oauth-service-hTyvduF1.js +1 -0
- package/dist/ui/assets/{openai-codex-CcWEg2Id.js → openai-codex-BG86bd3g.js} +1 -1
- package/dist/ui/assets/{openai-codex-4vY4IfjZ.js → openai-codex-W1k-olNJ.js} +1 -1
- package/dist/ui/assets/{panel-rpc-handlers-CyLUoTYQ.js → panel-rpc-handlers-BAwdZnUG.js} +1 -1
- package/dist/ui/assets/{provider-BRZxUH2o.js → provider-Ba8_qOfx.js} +1 -1
- package/dist/ui/assets/{provider-BGI7Ytuv.js → provider-fDZ2ppW0.js} +2 -2
- package/dist/ui/assets/provider-store-access-BVVboGQW.js +1 -0
- package/dist/ui/assets/provider-store-access-DLLL3FmZ.js +1 -0
- package/dist/ui/assets/{providers-CcDiXx1v.js → providers-DteOckdy.js} +1 -1
- package/dist/ui/assets/proxied-fetch-BBO8HwcV.js +1 -0
- package/dist/ui/assets/{quick-llm-CpcpayIL.js → quick-llm-BRbz1fDI.js} +1 -1
- package/dist/ui/assets/{remote-terminal-view-FVcjc7kw.js → remote-terminal-view-CSqiig2G.js} +1 -1
- package/dist/ui/assets/session-freezer-tVkiu4V5.js +1 -0
- package/dist/ui/assets/{setup-sprinkle-exec-BGXT8k4k.js → setup-sprinkle-exec-C7SBZY7y.js} +1 -1
- package/dist/ui/assets/setup-sudo-DWfiGlOf.js +1 -0
- package/dist/ui/assets/{speak-gvgsbAbr.js → speak-DhkLZ-am.js} +1 -1
- package/dist/ui/assets/{sprinkle-manager-am-b2nZK.js → sprinkle-manager-BLhwuVdT.js} +1 -1
- package/dist/ui/assets/{store-2Y1B359N.js → store-T-K8TeFw.js} +1 -1
- package/dist/ui/assets/{sudo-CXuD9JQi.js → sudo-qsDyNaAG.js} +1 -1
- package/dist/ui/assets/terminal-session-client-CbAduNfC.js +1 -0
- package/dist/ui/assets/{transformers-env-RCjmw9ix.js → transformers-env-DSAN95G3.js} +1 -1
- package/dist/ui/assets/{tray-leave-runtime-EjSnz0hB.js → tray-leave-runtime-tqIb9yl_.js} +1 -1
- package/dist/ui/assets/upgrade-detection-pjQXVRWP.js +1 -0
- package/dist/ui/assets/{wc-attach-BbBcc9d7.js → wc-attach-DwcZ5MaG.js} +2 -2
- package/dist/ui/assets/{wc-detached-BsGgVyQ2.js → wc-detached-BvZo_AId.js} +1 -1
- package/dist/ui/assets/{wc-extension-pjc1MPFN.js → wc-extension-CtHl8VdG.js} +2 -2
- package/dist/ui/assets/wc-live-BHTqp_8F.js +253 -0
- package/dist/ui/assets/{wc-nav-D1GUd0Mt.js → wc-nav-BJuQ2irW.js} +2 -2
- package/dist/ui/assets/wc-onboarding-84S404WJ.js +2 -0
- package/dist/ui/assets/{wc-placeholder-CEMVuHe3.js → wc-placeholder-DXEcgvyw.js} +3 -3
- package/dist/ui/assets/wc-settings-DWuDKTDp.js +24 -0
- package/dist/ui/assets/{wc-shell-tnKegXuB.js → wc-shell-B_z0VSKk.js} +3 -3
- package/dist/ui/assets/{wc-sprinkles-DEgP8Ihj.js → wc-sprinkles-BsYJVGg_.js} +2 -2
- package/dist/ui/assets/{wc-tray-BsL3c-FP.js → wc-tray-CWJphCmt.js} +3 -3
- package/dist/ui/assets/{xai-grok-BK-WZwtZ.js → xai-grok-D7ULF1Ef.js} +1 -1
- package/dist/ui/assets/{xai-grok-CqrCTXgB.js → xai-grok-DQ32cJt0.js} +1 -1
- package/dist/ui/index.html +2 -2
- package/dist/ui/llm-proxy-sw.js +1 -1
- package/dist/ui/packages/webapp/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/adobe-Ba8U6Soe.js +0 -2
- package/dist/ui/assets/adobe-BtvdR5I0.js +0 -1
- package/dist/ui/assets/dip-Fvfzs0-q.js +0 -1
- package/dist/ui/assets/main-zx9b4e1t.js +0 -24
- package/dist/ui/assets/oauth-service-CTlktxVj.js +0 -1
- package/dist/ui/assets/oauth-service-DQVciyk_.js +0 -1
- package/dist/ui/assets/provider-store-access-BiJN5bAM.js +0 -1
- package/dist/ui/assets/provider-store-access-Dp0MGqAc.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-DQ--9MwT.js +0 -1
- package/dist/ui/assets/setup-sudo-I4S39zW5.js +0 -1
- package/dist/ui/assets/terminal-session-client-C_tYJ1IB.js +0 -1
- package/dist/ui/assets/upgrade-detection-CHScEQnq.js +0 -1
- package/dist/ui/assets/wc-live-BAWrPhr9.js +0 -253
- package/dist/ui/assets/wc-onboarding-Bt02HM9-.js +0 -2
- package/dist/ui/assets/wc-settings-DP3TVlWk.js +0 -24
|
@@ -23,6 +23,12 @@
|
|
|
23
23
|
* origins (parallel to chrome-extension's `BRIDGE_DEV_ORIGINS`). Add a new
|
|
24
24
|
* origin here and in the extension allowlist together — they MUST stay in
|
|
25
25
|
* sync, otherwise extension and standalone disagree on what's a leader.
|
|
26
|
+
*
|
|
27
|
+
* Dev-only extra origins can be added at process start via the
|
|
28
|
+
* `BRIDGE_DEV_ALLOWED_ORIGINS` env var (comma-separated). Used by the
|
|
29
|
+
* local two-service harness (wrangler dev UI on :8787 + node-server
|
|
30
|
+
* bridge); see Wave 5c. When the env var is unset, the effective
|
|
31
|
+
* allowlist is byte-identical to this frozen base — prod is unaffected.
|
|
26
32
|
*/
|
|
27
33
|
export declare const BRIDGE_ALLOWED_ORIGINS: readonly string[];
|
|
28
34
|
/** Subprotocol prefix advertised by the leader; the per-process token is appended. */
|
|
@@ -31,8 +37,40 @@ export declare const BRIDGE_SUBPROTOCOL_PREFIX = "slicc.bridge.v1.";
|
|
|
31
37
|
export declare const BRIDGE_TOKEN_QUERY_PARAM = "bridgeToken";
|
|
32
38
|
/** Query-param name the launch URL uses to forward the local /cdp WebSocket URL. */
|
|
33
39
|
export declare const BRIDGE_WS_QUERY_PARAM = "bridge";
|
|
34
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Request header carrying the per-process bridge token on cross-origin
|
|
42
|
+
* /api/* calls from a remote allowlisted leader (e.g. sliccy.ai). The
|
|
43
|
+
* webapp's `proxied-fetch.ts` attaches it whenever a local API base
|
|
44
|
+
* origin is set; the thin-bridge CORS middleware validates it. Header
|
|
45
|
+
* is included in `CORS_BASE_ALLOW_HEADERS` so browsers don't strip it
|
|
46
|
+
* on the preflight.
|
|
47
|
+
*/
|
|
48
|
+
export declare const BRIDGE_TOKEN_HEADER = "X-Bridge-Token";
|
|
49
|
+
/**
|
|
50
|
+
* True iff `origin` is in the bridge allowlist. The frozen prod base
|
|
51
|
+
* (`BRIDGE_ALLOWED_ORIGINS`) is matched case-sensitively against the
|
|
52
|
+
* raw origin; the dev-only env-supplied extras (normalized lowercase
|
|
53
|
+
* + trailing-slash-stripped at load) are matched against a normalized
|
|
54
|
+
* copy of the input, so a leader sending `Origin: HTTP://Localhost:8787`
|
|
55
|
+
* (or with a stray trailing slash) still resolves correctly.
|
|
56
|
+
*/
|
|
35
57
|
export declare function isAllowedBridgeOrigin(origin: string | undefined | null): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* True iff `origin` is a loopback host (localhost / 127.0.0.1 / ::1).
|
|
60
|
+
* Loopback allowlisted origins (e.g. the locally-served OAuth callback
|
|
61
|
+
* page at `http://localhost:5710/auth/callback` posting to
|
|
62
|
+
* `/api/oauth-result`) are exempt from the bridge-token requirement —
|
|
63
|
+
* the token's threat model is "remote allowlisted origin (sliccy.ai)
|
|
64
|
+
* with a hostile script", not "local server talking to itself".
|
|
65
|
+
*/
|
|
66
|
+
export declare function isLoopbackBridgeOrigin(origin: string | undefined | null): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Constant-time compare for the bridge token. `presented` may be missing
|
|
69
|
+
* or shaped wrong (Express delivers headers as `string | string[] |
|
|
70
|
+
* undefined`). Returns `false` for any non-string, length mismatch, or
|
|
71
|
+
* empty expected — never throws.
|
|
72
|
+
*/
|
|
73
|
+
export declare function validateBridgeToken(presented: string | string[] | undefined, expected: string | null): boolean;
|
|
36
74
|
/**
|
|
37
75
|
* Mint a per-process bridge token. Embedded in the leader launch URL and
|
|
38
76
|
* required as the WebSocket subprotocol on /cdp. Use `crypto.randomUUID()`
|
|
@@ -77,6 +115,18 @@ export declare function validateBridgeUpgrade(input: {
|
|
|
77
115
|
subprotocolHeader: string | string[] | undefined;
|
|
78
116
|
expectedToken: string;
|
|
79
117
|
}): BridgeUpgradeGateResult;
|
|
118
|
+
/**
|
|
119
|
+
* Resolve the `Access-Control-Allow-Headers` value for a request. Starts
|
|
120
|
+
* from `CORS_BASE_ALLOW_HEADERS` (the static set covering the documented
|
|
121
|
+
* /api endpoints + the `/api/fetch-proxy` transport headers) and unions
|
|
122
|
+
* in any header names from the request's `Access-Control-Request-Headers`
|
|
123
|
+
* that aren't already listed. This is the reflect-headers pattern: the
|
|
124
|
+
* agent's `bash curl -H X-Custom: …` can route through `/api/fetch-proxy`
|
|
125
|
+
* cross-origin without us having to enumerate every possible upstream
|
|
126
|
+
* header in advance. Comparison is case-insensitive; the static set's
|
|
127
|
+
* canonical casing wins on duplicates.
|
|
128
|
+
*/
|
|
129
|
+
export declare function resolveCorsAllowHeaders(requestHeadersHeader: string | undefined | null): string;
|
|
80
130
|
/**
|
|
81
131
|
* CORS headers for an allowlisted `Origin`. Returns `null` when the origin
|
|
82
132
|
* is not in the allowlist (caller should NOT set CORS headers).
|
|
@@ -84,8 +134,12 @@ export declare function validateBridgeUpgrade(input: {
|
|
|
84
134
|
* `Access-Control-Allow-Credentials: true` is included so the hosted leader
|
|
85
135
|
* can carry cookies to /api/* (e.g. auth) when that's added later. Today
|
|
86
136
|
* the bridge token is the auth factor, not cookies.
|
|
137
|
+
*
|
|
138
|
+
* `requestHeadersHeader` should be the request's `Access-Control-Request-Headers`
|
|
139
|
+
* value (preflight only); on a non-preflight request pass `null` and the
|
|
140
|
+
* caller can omit it.
|
|
87
141
|
*/
|
|
88
|
-
export declare function buildCorsHeaders(origin: string | undefined | null): Record<string, string> | null;
|
|
142
|
+
export declare function buildCorsHeaders(origin: string | undefined | null, requestHeadersHeader?: string | string[] | null): Record<string, string> | null;
|
|
89
143
|
/**
|
|
90
144
|
* PNA preflight extras. Added on OPTIONS responses for allowlisted origins
|
|
91
145
|
* when the request carries `Access-Control-Request-Private-Network: true`
|
|
@@ -18,12 +18,18 @@
|
|
|
18
18
|
* Pure module (no node-only imports) so the WS gate is trivially unit
|
|
19
19
|
* testable from `tests/bridge-security.test.ts`.
|
|
20
20
|
*/
|
|
21
|
-
import { randomUUID } from 'node:crypto';
|
|
21
|
+
import { randomUUID, timingSafeEqual } from 'node:crypto';
|
|
22
22
|
/**
|
|
23
23
|
* Origin allowlist. Production + staging worker plus the dev-mode loopback
|
|
24
24
|
* origins (parallel to chrome-extension's `BRIDGE_DEV_ORIGINS`). Add a new
|
|
25
25
|
* origin here and in the extension allowlist together — they MUST stay in
|
|
26
26
|
* sync, otherwise extension and standalone disagree on what's a leader.
|
|
27
|
+
*
|
|
28
|
+
* Dev-only extra origins can be added at process start via the
|
|
29
|
+
* `BRIDGE_DEV_ALLOWED_ORIGINS` env var (comma-separated). Used by the
|
|
30
|
+
* local two-service harness (wrangler dev UI on :8787 + node-server
|
|
31
|
+
* bridge); see Wave 5c. When the env var is unset, the effective
|
|
32
|
+
* allowlist is byte-identical to this frozen base — prod is unaffected.
|
|
27
33
|
*/
|
|
28
34
|
export const BRIDGE_ALLOWED_ORIGINS = Object.freeze([
|
|
29
35
|
'https://www.sliccy.ai',
|
|
@@ -31,21 +37,155 @@ export const BRIDGE_ALLOWED_ORIGINS = Object.freeze([
|
|
|
31
37
|
'http://localhost:5710',
|
|
32
38
|
'http://127.0.0.1:5710',
|
|
33
39
|
]);
|
|
40
|
+
/**
|
|
41
|
+
* Normalize a single env-supplied origin: trim, lowercase, drop trailing
|
|
42
|
+
* slash. Returns `null` for blank/whitespace entries or anything that
|
|
43
|
+
* `URL` can't parse. Never throws.
|
|
44
|
+
*/
|
|
45
|
+
function normalizeDevOrigin(raw) {
|
|
46
|
+
const trimmed = raw.trim();
|
|
47
|
+
if (!trimmed)
|
|
48
|
+
return null;
|
|
49
|
+
let candidate = trimmed.toLowerCase();
|
|
50
|
+
while (candidate.endsWith('/')) {
|
|
51
|
+
candidate = candidate.slice(0, -1);
|
|
52
|
+
}
|
|
53
|
+
if (!candidate)
|
|
54
|
+
return null;
|
|
55
|
+
try {
|
|
56
|
+
// Sanity check: must parse as an absolute URL with scheme + host.
|
|
57
|
+
const parsed = new URL(candidate);
|
|
58
|
+
if (!parsed.protocol || !parsed.hostname)
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return candidate;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Dev-only extra origins parsed once from `BRIDGE_DEV_ALLOWED_ORIGINS` at
|
|
68
|
+
* module load. Comma-separated; blank entries ignored; malformed entries
|
|
69
|
+
* dropped. Frozen prod base above is left intact; `isAllowedBridgeOrigin`
|
|
70
|
+
* consults the union.
|
|
71
|
+
*/
|
|
72
|
+
const BRIDGE_DEV_ALLOWED_ORIGINS = (() => {
|
|
73
|
+
const raw = process.env.BRIDGE_DEV_ALLOWED_ORIGINS;
|
|
74
|
+
if (!raw)
|
|
75
|
+
return new Set();
|
|
76
|
+
const set = new Set();
|
|
77
|
+
for (const entry of raw.split(',')) {
|
|
78
|
+
const normalized = normalizeDevOrigin(entry);
|
|
79
|
+
if (normalized)
|
|
80
|
+
set.add(normalized);
|
|
81
|
+
}
|
|
82
|
+
return set;
|
|
83
|
+
})();
|
|
34
84
|
/** Subprotocol prefix advertised by the leader; the per-process token is appended. */
|
|
35
85
|
export const BRIDGE_SUBPROTOCOL_PREFIX = 'slicc.bridge.v1.';
|
|
36
86
|
/** Query-param name the launch URL uses to forward the subprotocol token to the leader. */
|
|
37
87
|
export const BRIDGE_TOKEN_QUERY_PARAM = 'bridgeToken';
|
|
38
88
|
/** Query-param name the launch URL uses to forward the local /cdp WebSocket URL. */
|
|
39
89
|
export const BRIDGE_WS_QUERY_PARAM = 'bridge';
|
|
40
|
-
/**
|
|
41
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Request header carrying the per-process bridge token on cross-origin
|
|
92
|
+
* /api/* calls from a remote allowlisted leader (e.g. sliccy.ai). The
|
|
93
|
+
* webapp's `proxied-fetch.ts` attaches it whenever a local API base
|
|
94
|
+
* origin is set; the thin-bridge CORS middleware validates it. Header
|
|
95
|
+
* is included in `CORS_BASE_ALLOW_HEADERS` so browsers don't strip it
|
|
96
|
+
* on the preflight.
|
|
97
|
+
*/
|
|
98
|
+
export const BRIDGE_TOKEN_HEADER = 'X-Bridge-Token';
|
|
99
|
+
/**
|
|
100
|
+
* Headers we allow on cross-origin /api requests from the hosted leader.
|
|
101
|
+
* Includes the `/api/fetch-proxy` transport headers (`X-Target-URL`, the
|
|
102
|
+
* forbidden-header `X-Proxy-*` bridges, `X-Slicc-Raw-Body`) so the
|
|
103
|
+
* webapp's `createProxiedFetch` can route through the local node-server
|
|
104
|
+
* cross-origin in thin-bridge mode. Custom upstream headers (any header
|
|
105
|
+
* the agent's `curl -H …` would pass through) are reflected via
|
|
106
|
+
* `Access-Control-Request-Headers` in `buildCorsHeaders` below.
|
|
107
|
+
*/
|
|
108
|
+
const CORS_BASE_ALLOW_HEADERS = [
|
|
109
|
+
'Content-Type',
|
|
110
|
+
'X-Slicc-Raw-Body',
|
|
111
|
+
'X-Session-Id',
|
|
112
|
+
'X-Bridge-Token',
|
|
113
|
+
'Authorization',
|
|
114
|
+
'X-Target-URL',
|
|
115
|
+
'X-Proxy-Cookie',
|
|
116
|
+
'X-Proxy-Origin',
|
|
117
|
+
'X-Proxy-Referer',
|
|
118
|
+
];
|
|
119
|
+
/**
|
|
120
|
+
* Response headers the browser is allowed to read after a cross-origin
|
|
121
|
+
* /api call — must include the proxy's infrastructure-error marker
|
|
122
|
+
* (`isProxyError` reads `X-Proxy-Error`) and the forbidden-response
|
|
123
|
+
* bridge (`decodeForbiddenResponseHeaders` reads `X-Proxy-Set-Cookie`).
|
|
124
|
+
*/
|
|
125
|
+
const CORS_EXPOSE_HEADERS = 'Link, X-Proxy-Error, X-Proxy-Set-Cookie';
|
|
42
126
|
/** Methods exposed to the hosted leader. */
|
|
43
127
|
const CORS_ALLOW_METHODS = 'GET, POST, PUT, DELETE, OPTIONS';
|
|
44
|
-
/**
|
|
128
|
+
/**
|
|
129
|
+
* True iff `origin` is in the bridge allowlist. The frozen prod base
|
|
130
|
+
* (`BRIDGE_ALLOWED_ORIGINS`) is matched case-sensitively against the
|
|
131
|
+
* raw origin; the dev-only env-supplied extras (normalized lowercase
|
|
132
|
+
* + trailing-slash-stripped at load) are matched against a normalized
|
|
133
|
+
* copy of the input, so a leader sending `Origin: HTTP://Localhost:8787`
|
|
134
|
+
* (or with a stray trailing slash) still resolves correctly.
|
|
135
|
+
*/
|
|
45
136
|
export function isAllowedBridgeOrigin(origin) {
|
|
46
137
|
if (!origin)
|
|
47
138
|
return false;
|
|
48
|
-
|
|
139
|
+
if (BRIDGE_ALLOWED_ORIGINS.includes(origin))
|
|
140
|
+
return true;
|
|
141
|
+
if (BRIDGE_DEV_ALLOWED_ORIGINS.size === 0)
|
|
142
|
+
return false;
|
|
143
|
+
const normalized = normalizeDevOrigin(origin);
|
|
144
|
+
if (!normalized)
|
|
145
|
+
return false;
|
|
146
|
+
return BRIDGE_DEV_ALLOWED_ORIGINS.has(normalized);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* True iff `origin` is a loopback host (localhost / 127.0.0.1 / ::1).
|
|
150
|
+
* Loopback allowlisted origins (e.g. the locally-served OAuth callback
|
|
151
|
+
* page at `http://localhost:5710/auth/callback` posting to
|
|
152
|
+
* `/api/oauth-result`) are exempt from the bridge-token requirement —
|
|
153
|
+
* the token's threat model is "remote allowlisted origin (sliccy.ai)
|
|
154
|
+
* with a hostile script", not "local server talking to itself".
|
|
155
|
+
*/
|
|
156
|
+
export function isLoopbackBridgeOrigin(origin) {
|
|
157
|
+
if (!origin)
|
|
158
|
+
return false;
|
|
159
|
+
try {
|
|
160
|
+
const url = new URL(origin);
|
|
161
|
+
// Node's WHATWG URL parser keeps the brackets on IPv6 hostnames
|
|
162
|
+
// (`http://[::1]:5710` → `[::1]`); accept both bracketed and bare.
|
|
163
|
+
return (url.hostname === 'localhost' ||
|
|
164
|
+
url.hostname === '127.0.0.1' ||
|
|
165
|
+
url.hostname === '::1' ||
|
|
166
|
+
url.hostname === '[::1]');
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Constant-time compare for the bridge token. `presented` may be missing
|
|
174
|
+
* or shaped wrong (Express delivers headers as `string | string[] |
|
|
175
|
+
* undefined`). Returns `false` for any non-string, length mismatch, or
|
|
176
|
+
* empty expected — never throws.
|
|
177
|
+
*/
|
|
178
|
+
export function validateBridgeToken(presented, expected) {
|
|
179
|
+
if (!expected)
|
|
180
|
+
return false;
|
|
181
|
+
const value = Array.isArray(presented) ? presented[0] : presented;
|
|
182
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
183
|
+
return false;
|
|
184
|
+
const a = Buffer.from(value);
|
|
185
|
+
const b = Buffer.from(expected);
|
|
186
|
+
if (a.length !== b.length)
|
|
187
|
+
return false;
|
|
188
|
+
return timingSafeEqual(a, b);
|
|
49
189
|
}
|
|
50
190
|
/**
|
|
51
191
|
* Mint a per-process bridge token. Embedded in the leader launch URL and
|
|
@@ -102,6 +242,36 @@ export function validateBridgeUpgrade(input) {
|
|
|
102
242
|
}
|
|
103
243
|
return { ok: true, acceptedSubprotocol: accepted };
|
|
104
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Resolve the `Access-Control-Allow-Headers` value for a request. Starts
|
|
247
|
+
* from `CORS_BASE_ALLOW_HEADERS` (the static set covering the documented
|
|
248
|
+
* /api endpoints + the `/api/fetch-proxy` transport headers) and unions
|
|
249
|
+
* in any header names from the request's `Access-Control-Request-Headers`
|
|
250
|
+
* that aren't already listed. This is the reflect-headers pattern: the
|
|
251
|
+
* agent's `bash curl -H X-Custom: …` can route through `/api/fetch-proxy`
|
|
252
|
+
* cross-origin without us having to enumerate every possible upstream
|
|
253
|
+
* header in advance. Comparison is case-insensitive; the static set's
|
|
254
|
+
* canonical casing wins on duplicates.
|
|
255
|
+
*/
|
|
256
|
+
export function resolveCorsAllowHeaders(requestHeadersHeader) {
|
|
257
|
+
if (!requestHeadersHeader)
|
|
258
|
+
return CORS_BASE_ALLOW_HEADERS.join(', ');
|
|
259
|
+
const seen = new Set(CORS_BASE_ALLOW_HEADERS.map((h) => h.toLowerCase()));
|
|
260
|
+
const extras = [];
|
|
261
|
+
for (const raw of requestHeadersHeader.split(',')) {
|
|
262
|
+
const name = raw.trim();
|
|
263
|
+
if (!name)
|
|
264
|
+
continue;
|
|
265
|
+
const lower = name.toLowerCase();
|
|
266
|
+
if (seen.has(lower))
|
|
267
|
+
continue;
|
|
268
|
+
seen.add(lower);
|
|
269
|
+
extras.push(name);
|
|
270
|
+
}
|
|
271
|
+
if (extras.length === 0)
|
|
272
|
+
return CORS_BASE_ALLOW_HEADERS.join(', ');
|
|
273
|
+
return [...CORS_BASE_ALLOW_HEADERS, ...extras].join(', ');
|
|
274
|
+
}
|
|
105
275
|
/**
|
|
106
276
|
* CORS headers for an allowlisted `Origin`. Returns `null` when the origin
|
|
107
277
|
* is not in the allowlist (caller should NOT set CORS headers).
|
|
@@ -109,17 +279,24 @@ export function validateBridgeUpgrade(input) {
|
|
|
109
279
|
* `Access-Control-Allow-Credentials: true` is included so the hosted leader
|
|
110
280
|
* can carry cookies to /api/* (e.g. auth) when that's added later. Today
|
|
111
281
|
* the bridge token is the auth factor, not cookies.
|
|
282
|
+
*
|
|
283
|
+
* `requestHeadersHeader` should be the request's `Access-Control-Request-Headers`
|
|
284
|
+
* value (preflight only); on a non-preflight request pass `null` and the
|
|
285
|
+
* caller can omit it.
|
|
112
286
|
*/
|
|
113
|
-
export function buildCorsHeaders(origin) {
|
|
287
|
+
export function buildCorsHeaders(origin, requestHeadersHeader) {
|
|
114
288
|
if (!isAllowedBridgeOrigin(origin))
|
|
115
289
|
return null;
|
|
290
|
+
const reqHeaders = Array.isArray(requestHeadersHeader)
|
|
291
|
+
? requestHeadersHeader.join(', ')
|
|
292
|
+
: (requestHeadersHeader ?? null);
|
|
116
293
|
return {
|
|
117
294
|
'Access-Control-Allow-Origin': origin,
|
|
118
295
|
'Access-Control-Allow-Credentials': 'true',
|
|
119
296
|
'Access-Control-Allow-Methods': CORS_ALLOW_METHODS,
|
|
120
|
-
'Access-Control-Allow-Headers':
|
|
121
|
-
'Access-Control-Expose-Headers':
|
|
122
|
-
Vary: 'Origin',
|
|
297
|
+
'Access-Control-Allow-Headers': resolveCorsAllowHeaders(reqHeaders),
|
|
298
|
+
'Access-Control-Expose-Headers': CORS_EXPOSE_HEADERS,
|
|
299
|
+
Vary: 'Origin, Access-Control-Request-Headers',
|
|
123
300
|
};
|
|
124
301
|
}
|
|
125
302
|
/**
|
|
@@ -3,9 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Includes hop-by-hop headers (`host`, `connection`, `transfer-encoding`,
|
|
5
5
|
* `content-length`), proxy-internal markers (`x-target-url`,
|
|
6
|
-
* `x-slicc-raw-body`),
|
|
6
|
+
* `x-slicc-raw-body`), forbidden-header transports the client uses
|
|
7
7
|
* to smuggle reserved names through `fetch()` (`x-proxy-cookie`,
|
|
8
|
-
* `x-proxy-origin`, `x-proxy-referer`)
|
|
8
|
+
* `x-proxy-origin`, `x-proxy-referer`), and the thin-bridge auth header
|
|
9
|
+
* (`x-bridge-token`) which authenticates the browser->local hop only
|
|
10
|
+
* and must not leak onward to `targetUrl`. The bridge-token middleware
|
|
11
|
+
* (`createThinBridgeCorsMiddleware` in `index.ts`) is mounted ahead of
|
|
12
|
+
* this route and reads `req.headers` directly, so token validation
|
|
13
|
+
* still sees the header - this Set only filters what gets COPIED into
|
|
14
|
+
* the forwarded request.
|
|
9
15
|
*
|
|
10
16
|
* Lives in its own module (rather than `index.ts`) so tests can import
|
|
11
17
|
* it without triggering the server bootstrap that runs at `index.ts`
|
|
@@ -3,9 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Includes hop-by-hop headers (`host`, `connection`, `transfer-encoding`,
|
|
5
5
|
* `content-length`), proxy-internal markers (`x-target-url`,
|
|
6
|
-
* `x-slicc-raw-body`),
|
|
6
|
+
* `x-slicc-raw-body`), forbidden-header transports the client uses
|
|
7
7
|
* to smuggle reserved names through `fetch()` (`x-proxy-cookie`,
|
|
8
|
-
* `x-proxy-origin`, `x-proxy-referer`)
|
|
8
|
+
* `x-proxy-origin`, `x-proxy-referer`), and the thin-bridge auth header
|
|
9
|
+
* (`x-bridge-token`) which authenticates the browser->local hop only
|
|
10
|
+
* and must not leak onward to `targetUrl`. The bridge-token middleware
|
|
11
|
+
* (`createThinBridgeCorsMiddleware` in `index.ts`) is mounted ahead of
|
|
12
|
+
* this route and reads `req.headers` directly, so token validation
|
|
13
|
+
* still sees the header - this Set only filters what gets COPIED into
|
|
14
|
+
* the forwarded request.
|
|
9
15
|
*
|
|
10
16
|
* Lives in its own module (rather than `index.ts`) so tests can import
|
|
11
17
|
* it without triggering the server bootstrap that runs at `index.ts`
|
|
@@ -21,4 +27,5 @@ export const FETCH_PROXY_SKIP_HEADERS = new Set([
|
|
|
21
27
|
'x-proxy-cookie',
|
|
22
28
|
'x-proxy-origin',
|
|
23
29
|
'x-proxy-referer',
|
|
30
|
+
'x-bridge-token',
|
|
24
31
|
]);
|