sliccy 6.112.2 → 6.112.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/bridge-security.d.ts +9 -0
- package/dist/node-server/bridge-security.js +11 -0
- package/dist/node-server/fetch-proxy-headers.d.ts +13 -0
- package/dist/node-server/fetch-proxy-headers.js +19 -0
- package/dist/node-server/hostfs.d.ts +57 -1
- package/dist/node-server/hostfs.js +197 -38
- package/dist/node-server/index.js +6 -11
- package/dist/ui/.vite/manifest.json +141 -141
- package/dist/ui/assets/{account-store-DFb7ccri.js → account-store-DKGiIfmq.js} +1 -1
- package/dist/ui/assets/{account-store-BEm3ZOf4.js → account-store-Drm8M57M.js} +1 -1
- package/dist/ui/assets/{adobe-CrYs-_-w.js → adobe-CS7hK6MS.js} +1 -1
- package/dist/ui/assets/{adobe-D4NDqSKt.js → adobe-CsMz2rsC.js} +1 -1
- package/dist/ui/assets/{agent-bridge-D2uQgl3Y.js → agent-bridge-Diq7l6-Z.js} +1 -1
- package/dist/ui/assets/{agent-panels-SlP4qUD9.js → agent-panels-B9c8k1kc.js} +1 -1
- package/dist/ui/assets/{apply-layout-Btd25a-I.js → apply-layout-Cr_Qp7ts.js} +1 -1
- package/dist/ui/assets/{apps-DvB62CHa.js → apps-DQTe9Hdt.js} +1 -1
- package/dist/ui/assets/{auto-mount-table-B5XJxoBU.js → auto-mount-table-BRFqHPcB.js} +1 -1
- package/dist/ui/assets/{azure-openai-pZTdx_lF.js → azure-openai-BXn6Q7yb.js} +1 -1
- package/dist/ui/assets/{azure-openai-CwiVZb3U.js → azure-openai-CzFCMFS3.js} +1 -1
- package/dist/ui/assets/backend-hostfs-CwOm0hxZ.js +1 -0
- package/dist/ui/assets/backend-hostfs-DHZa_yjt.js +1 -0
- package/dist/ui/assets/{cerebras-Kr8CGPGL.js → cerebras-B6ESuqhH.js} +1 -1
- package/dist/ui/assets/{cerebras-BBG5B3l5.js → cerebras-C-6yRmj5.js} +1 -1
- package/dist/ui/assets/{connect-surface-CZmki_ui.js → connect-surface-C6Z5eEC-.js} +1 -1
- package/dist/ui/assets/{directed-approval-DJkc0X5A.js → directed-approval-LOfGNkxz.js} +1 -1
- package/dist/ui/assets/{fs-DrhcgSjY.js → fs-CH0Ljn4a.js} +1 -1
- package/dist/ui/assets/{fs-C7Q7Gwx0.js → fs-CjSSfDYh.js} +2 -2
- package/dist/ui/assets/{github-DLvfbIxr.js → github-C4S66iqT.js} +1 -1
- package/dist/ui/assets/{github-2ePt7Y4O.js → github-CGmhboTB.js} +2 -2
- package/dist/ui/assets/{github-copilot-KpwsmwEN.js → github-copilot-Cys8f2bl.js} +1 -1
- package/dist/ui/assets/{github-copilot-COHzkV9u.js → github-copilot-DVPbVtKI.js} +1 -1
- package/dist/ui/assets/{github-install-ZepK9sNf.js → github-install-DL98HSJZ.js} +1 -1
- package/dist/ui/assets/{github-zip-DdnZ7Mnj.js → github-zip-COa4Q2lq.js} +1 -1
- package/dist/ui/assets/{js-realm-worker-DB2Y7zuK.js → js-realm-worker-DPFyND8C.js} +1 -1
- package/dist/ui/assets/{kernel-worker-CMjt7tHc.js → kernel-worker-C0uy1TbG.js} +530 -530
- package/dist/ui/assets/{local-llm-CtJOiUkB.js → local-llm-D-9YzYyO.js} +1 -1
- package/dist/ui/assets/{local-llm-CHmYak89.js → local-llm-DWgPIy_K.js} +1 -1
- package/dist/ui/assets/{main-CbyCXNIn.js → main-B4SwrphU.js} +3 -3
- package/dist/ui/assets/{mount-Dodh2D8V.js → mount-C_4aO72D.js} +3 -3
- package/dist/ui/assets/{mount-DO3EG6PG.js → mount-D2bzwfnP.js} +1 -1
- package/dist/ui/assets/{mount-recovery-DixhqwBC.js → mount-recovery-DX5ZMNPV.js} +1 -1
- package/dist/ui/assets/{new-session-B8nhZDEn.js → new-session-CobplCqm.js} +2 -2
- package/dist/ui/assets/{oauth-bootstrap-BkzTKBfF.js → oauth-bootstrap-Da8vG0Ce.js} +2 -2
- package/dist/ui/assets/{openai-codex-DJr9IgkK.js → openai-codex-BQy2GRvd.js} +1 -1
- package/dist/ui/assets/{openai-codex-BIKDqKu_.js → openai-codex-BxXGZbZ2.js} +1 -1
- package/dist/ui/assets/{openrouter-DH4eLV-K.js → openrouter-BCGVjBV9.js} +1 -1
- package/dist/ui/assets/{openrouter-B9IxwYeV.js → openrouter-CIP6PZdI.js} +1 -1
- package/dist/ui/assets/{openrouter-oauth-CQJY8i6t.js → openrouter-oauth-BSFzZC1g.js} +1 -1
- package/dist/ui/assets/{openrouter-oauth-BWsz3-ge.js → openrouter-oauth-DnkEkrDp.js} +1 -1
- package/dist/ui/assets/{panel-rpc-handlers-DH7xx1M0.js → panel-rpc-handlers-9GTNZnKe.js} +2 -2
- package/dist/ui/assets/{panelize-shell-k--mebzt.js → panelize-shell-DfteHska.js} +1 -1
- package/dist/ui/assets/{provider-CDbrFirS.js → provider-DNg8cWzc.js} +2 -2
- package/dist/ui/assets/{provider-ClUAbw1j.js → provider-kpBMt5XH.js} +1 -1
- package/dist/ui/assets/{provider-settings-IGWxggS0.js → provider-settings-BFKSyu38.js} +1 -1
- package/dist/ui/assets/{provider-settings-Dca6XryJ.js → provider-settings-CXxoEx6k.js} +1 -1
- package/dist/ui/assets/provider-store-access-BTyiaWJG.js +1 -0
- package/dist/ui/assets/{provider-store-access-CxUkiz_V.js → provider-store-access-Dcy8pc8z.js} +2 -2
- package/dist/ui/assets/{providers-W21iBf-y.js → providers-90_avw00.js} +1 -1
- package/dist/ui/assets/{providers-CVZc7zSe.js → providers-C7JLZFjM.js} +2 -2
- package/dist/ui/assets/{quick-llm-BO0uuygb.js → quick-llm-D59bKtv9.js} +1 -1
- package/dist/ui/assets/{recovery-screen-0xv_XYuL.js → recovery-screen-C51ohvOQ.js} +1 -1
- package/dist/ui/assets/{run-CcUi-QR8.js → run-CSH5VzF0.js} +2 -2
- package/dist/ui/assets/{run-oqzFNdad.js → run-Fu1DWOPZ.js} +1 -1
- package/dist/ui/assets/{session-freezer-D6A9ph34.js → session-freezer-BUkrQCDA.js} +3 -3
- package/dist/ui/assets/{setup-feature-flags-remote-DQ12tLeR.js → setup-feature-flags-remote-C_x2aqvQ.js} +1 -1
- package/dist/ui/assets/{setup-standalone-prelude-C4hTP-NY.js → setup-standalone-prelude-D1hf5svM.js} +1 -1
- package/dist/ui/assets/{setup-sudo-Bv52MwOC.js → setup-sudo-BeQ_wWvx.js} +2 -2
- package/dist/ui/assets/{skills-CRrnKp2n.js → skills-c_2geZLF.js} +1 -1
- package/dist/ui/assets/slicc-version-Bmv6W3YW.js +1 -0
- package/dist/ui/assets/{state-BlkDfTVY.js → state-CxYcgIEC.js} +1 -1
- package/dist/ui/assets/{store-ljIGz9wi.js → store-DDRMLUbL.js} +1 -1
- package/dist/ui/assets/{store-za51L75Y.js → store-hz-L7qro.js} +1 -1
- package/dist/ui/assets/{sudo-JK66KVfq.js → sudo-EuNE7Rcp.js} +1 -1
- package/dist/ui/assets/{upgrade-detection-DayCJLDc.js → upgrade-detection-tKoUqNBL.js} +1 -1
- package/dist/ui/assets/{wc-attach-CCbv56td.js → wc-attach-u_LmkqY-.js} +2 -2
- package/dist/ui/assets/{wc-extension-daiiHAwe.js → wc-extension-DIm4QLFM.js} +2 -2
- package/dist/ui/assets/{wc-float-label-CYQ1O9IG.js → wc-float-label-CtNoKDwq.js} +1 -1
- package/dist/ui/assets/{wc-floatbar-online-DOvI2Gz9.js → wc-floatbar-online-CPLQs77u.js} +1 -1
- package/dist/ui/assets/{wc-follower-CmtSqM4T.js → wc-follower-D6gXrBmp.js} +3 -3
- package/dist/ui/assets/{wc-follower-oauth-CfVGAzoz.js → wc-follower-oauth-B9IGaduR.js} +1 -1
- package/dist/ui/assets/{wc-live-CEb85_ni.js → wc-live-CRYDgY8_.js} +4 -4
- package/dist/ui/assets/{wc-nav-9JsFLg8Q.js → wc-nav-BBBCCIWN.js} +2 -2
- package/dist/ui/assets/{wc-onboarding-INB8m-An.js → wc-onboarding-DQQ2cJsn.js} +2 -2
- package/dist/ui/assets/{wc-placeholder-KVfEJPvC.js → wc-placeholder-_lRw8rmV.js} +2 -2
- package/dist/ui/assets/{wc-settings-Dh7v_Gm7.js → wc-settings-CQAUL3mr.js} +2 -2
- package/dist/ui/assets/{wc-shell-vKd9u5Qb.js → wc-shell-DCeYY9cZ.js} +2 -2
- package/dist/ui/assets/{wc-shortcut-config-DNWWZUqP.js → wc-shortcut-config-Tfio7pdk.js} +1 -1
- package/dist/ui/assets/{wc-sprinkles-BbjnvtOw.js → wc-sprinkles-CnZmKYuh.js} +2 -2
- package/dist/ui/assets/{wc-tray-B-HO43dD.js → wc-tray-Q3ELQQvC.js} +4 -4
- package/dist/ui/assets/{xai-grok-BAMOZLLE.js → xai-grok-B0MEGouW.js} +1 -1
- package/dist/ui/assets/{xai-grok-S5ay2Ab2.js → xai-grok-fDgioGoH.js} +1 -1
- package/dist/ui/index.html +6 -6
- package/dist/ui/packages/webapp/index.html +6 -6
- package/package.json +1 -1
- package/dist/ui/assets/backend-hostfs-Com-zF5W.js +0 -1
- package/dist/ui/assets/backend-hostfs-bQQx-S0W.js +0 -1
- package/dist/ui/assets/provider-store-access-CjkPfBXV.js +0 -1
- package/dist/ui/assets/slicc-version-0JCDQACw.js +0 -1
|
@@ -161,3 +161,12 @@ export declare function buildCorsHeaders(origin: string | undefined | null, requ
|
|
|
161
161
|
* — Chrome blocks public→private fetch / WS otherwise.
|
|
162
162
|
*/
|
|
163
163
|
export declare function buildPnaPreflightHeaders(): Record<string, string>;
|
|
164
|
+
/**
|
|
165
|
+
* Preflight cache lifetime. `/api/hostfs/*` gets Chrome's cap (7200s) because
|
|
166
|
+
* the mounted-filesystem traffic is thousands of requests per command and
|
|
167
|
+
* every one of them is non-simple (bridge-token header) plus public→loopback
|
|
168
|
+
* (Chrome's Private Network Access preflight) — see #2715. The rest of /api
|
|
169
|
+
* keeps the conservative 10 minutes: those routes are low-volume, and a short
|
|
170
|
+
* window bounds how long a stale allowed-header set can linger.
|
|
171
|
+
*/
|
|
172
|
+
export declare function preflightMaxAge(path: string): string;
|
|
@@ -332,3 +332,14 @@ export function buildCorsHeaders(origin, requestHeadersHeader) {
|
|
|
332
332
|
export function buildPnaPreflightHeaders() {
|
|
333
333
|
return { 'Access-Control-Allow-Private-Network': 'true' };
|
|
334
334
|
}
|
|
335
|
+
/**
|
|
336
|
+
* Preflight cache lifetime. `/api/hostfs/*` gets Chrome's cap (7200s) because
|
|
337
|
+
* the mounted-filesystem traffic is thousands of requests per command and
|
|
338
|
+
* every one of them is non-simple (bridge-token header) plus public→loopback
|
|
339
|
+
* (Chrome's Private Network Access preflight) — see #2715. The rest of /api
|
|
340
|
+
* keeps the conservative 10 minutes: those routes are low-volume, and a short
|
|
341
|
+
* window bounds how long a stale allowed-header set can linger.
|
|
342
|
+
*/
|
|
343
|
+
export function preflightMaxAge(path) {
|
|
344
|
+
return path === '/api/hostfs' || path.startsWith('/api/hostfs/') ? '7200' : '600';
|
|
345
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'http';
|
|
1
2
|
/**
|
|
2
3
|
* Headers the `/api/fetch-proxy` route does NOT forward upstream.
|
|
3
4
|
*
|
|
@@ -66,3 +67,15 @@ export declare const FETCH_PROXY_BASE_EXPOSE_HEADERS: readonly string[];
|
|
|
66
67
|
* Dedupes case-insensitively while preserving the first-seen spelling.
|
|
67
68
|
*/
|
|
68
69
|
export declare function buildFetchProxyExposeHeaders(forwardedHeaderNames: Iterable<string>): string;
|
|
70
|
+
/**
|
|
71
|
+
* Which requests the global `express.json()` consumes. Two exclusions:
|
|
72
|
+
*
|
|
73
|
+
* - `X-Slicc-Raw-Body: 1` — SigV4-signed bodies must reach the
|
|
74
|
+
* /api/fetch-proxy handler byte-for-byte (the parser would re-serialize
|
|
75
|
+
* them via JSON.stringify and break the signature);
|
|
76
|
+
* - the stable hostfs dispatcher — it owns a bounded 1 MiB parser and an
|
|
77
|
+
* errno error adapter, and this parser is mounted ahead of
|
|
78
|
+
* `registerHostFsRoutes`, so without the exclusion it would consume the
|
|
79
|
+
* body first and answer a malformed one with code-less HTML.
|
|
80
|
+
*/
|
|
81
|
+
export declare function shouldParseGlobalJson(req: IncomingMessage): boolean;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isHostFsStableBodyRequest } from './hostfs.js';
|
|
1
2
|
/**
|
|
2
3
|
* Headers the `/api/fetch-proxy` route does NOT forward upstream.
|
|
3
4
|
*
|
|
@@ -111,3 +112,21 @@ export function buildFetchProxyExposeHeaders(forwardedHeaderNames) {
|
|
|
111
112
|
}
|
|
112
113
|
return out.join(', ');
|
|
113
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Which requests the global `express.json()` consumes. Two exclusions:
|
|
117
|
+
*
|
|
118
|
+
* - `X-Slicc-Raw-Body: 1` — SigV4-signed bodies must reach the
|
|
119
|
+
* /api/fetch-proxy handler byte-for-byte (the parser would re-serialize
|
|
120
|
+
* them via JSON.stringify and break the signature);
|
|
121
|
+
* - the stable hostfs dispatcher — it owns a bounded 1 MiB parser and an
|
|
122
|
+
* errno error adapter, and this parser is mounted ahead of
|
|
123
|
+
* `registerHostFsRoutes`, so without the exclusion it would consume the
|
|
124
|
+
* body first and answer a malformed one with code-less HTML.
|
|
125
|
+
*/
|
|
126
|
+
export function shouldParseGlobalJson(req) {
|
|
127
|
+
if (req.headers['x-slicc-raw-body'] === '1')
|
|
128
|
+
return false;
|
|
129
|
+
if (isHostFsStableBodyRequest(req))
|
|
130
|
+
return false;
|
|
131
|
+
return (req.headers['content-type'] ?? '').includes('application/json');
|
|
132
|
+
}
|
|
@@ -14,6 +14,31 @@
|
|
|
14
14
|
* Protected like every other /api route: same-origin / loopback callers
|
|
15
15
|
* pass, cross-origin callers need the bridge token (see
|
|
16
16
|
* `createThinBridgeCorsMiddleware` in index.ts).
|
|
17
|
+
*
|
|
18
|
+
* Two request shapes reach the same handlers:
|
|
19
|
+
*
|
|
20
|
+
* - the per-op routes (`GET /api/hostfs/list?mount=&path=` …), kept for
|
|
21
|
+
* compatibility and used for the body ops (`read`, `write`);
|
|
22
|
+
* - a single stable `POST /api/hostfs` whose JSON body carries
|
|
23
|
+
* `{ op, mount, path, … }`.
|
|
24
|
+
*
|
|
25
|
+
* The stable endpoint exists purely to make CORS preflights cacheable. The
|
|
26
|
+
* bridge token travels as a custom header, so every cross-origin hostfs call
|
|
27
|
+
* is a non-simple request, and Chrome's Private Network Access preflights
|
|
28
|
+
* public→loopback traffic regardless — but the preflight cache is keyed by
|
|
29
|
+
* URL, and the per-op URLs are unique per path, so `Access-Control-Max-Age`
|
|
30
|
+
* never got a chance to help (issue #2715: 246,893 `OPTIONS` for 385,033
|
|
31
|
+
* `GET`s in one session). One stable URL collapses that to one preflight per
|
|
32
|
+
* max-age. `read` deliberately stays a `GET` on a per-file URL so the
|
|
33
|
+
* browser HTTP cache can still revalidate large blobs with a 304.
|
|
34
|
+
*
|
|
35
|
+
* The hostfs surface owns its own body parsing end to end. `index.ts` skips
|
|
36
|
+
* the stable dispatcher in its global `express.json()` filter (via
|
|
37
|
+
* `isHostFsStableBodyRequest`) so the bounded 1 MiB parser here is the one
|
|
38
|
+
* that runs, and `hostFsBodyErrorHandler` maps body-parser failures to the
|
|
39
|
+
* same `{ code, message }` errno shape as every other error on this surface —
|
|
40
|
+
* otherwise express's default handler answers malformed JSON with code-less
|
|
41
|
+
* HTML and the webapp cannot rethrow a faithful `FsError`.
|
|
17
42
|
*/
|
|
18
43
|
import type { Express } from 'express';
|
|
19
44
|
import type { HostMountMapping } from './runtime-flags.js';
|
|
@@ -40,19 +65,50 @@ interface HostMountRoot {
|
|
|
40
65
|
* webapp will surface the absent mount as "not mounted".
|
|
41
66
|
*/
|
|
42
67
|
export declare function resolveHostMountRoots(mounts: readonly HostMountMapping[], warn?: (msg: string) => void): Promise<HostMountRoot[]>;
|
|
68
|
+
/**
|
|
69
|
+
* Bounded body cap for the stable dispatcher — it only ever carries a small
|
|
70
|
+
* JSON envelope, so it has no business inheriting the global 50 MiB limit.
|
|
71
|
+
*/
|
|
72
|
+
export declare const HOSTFS_STABLE_MAX_BODY_BYTES: number;
|
|
73
|
+
/**
|
|
74
|
+
* True for a request `index.ts`'s global `express.json()` must NOT consume.
|
|
75
|
+
* The global parser is mounted before these routes with a 50 MiB limit, so
|
|
76
|
+
* without this exclusion it would swallow the stable dispatcher's body first:
|
|
77
|
+
* the route-local 1 MiB cap would never apply, and a malformed body would
|
|
78
|
+
* fail in the global parser and reach express's default (code-less HTML)
|
|
79
|
+
* handler instead of the errno shape every hostfs caller expects. Exported so
|
|
80
|
+
* `index.ts` and the middleware-order test filter identically.
|
|
81
|
+
*/
|
|
82
|
+
export declare function isHostFsStableBodyRequest(req: {
|
|
83
|
+
method?: string;
|
|
84
|
+
url?: string;
|
|
85
|
+
}): boolean;
|
|
43
86
|
/**
|
|
44
87
|
* Register the /api/hostfs routes for the resolved mount roots. The route
|
|
45
88
|
* surface mirrors `MountBackend` 1:1 (webapp `backend-hostfs.ts`):
|
|
46
89
|
*
|
|
90
|
+
* POST /api/hostfs ← { op, mount, path, to?, recursive? }
|
|
47
91
|
* GET /api/hostfs/list?mount=&path= → { entries: [...] }
|
|
48
92
|
* GET /api/hostfs/stat?mount=&path= → { kind, size, mtime }
|
|
49
93
|
* GET /api/hostfs/read?mount=&path= → octet-stream body
|
|
50
94
|
* PUT /api/hostfs/write?mount=&path= ← octet-stream body
|
|
51
95
|
* POST /api/hostfs/mkdir?mount=&path=
|
|
96
|
+
* POST /api/hostfs/rename?mount=&path=&to=
|
|
52
97
|
* DELETE /api/hostfs/remove?mount=&path=[&recursive=1]
|
|
53
98
|
*
|
|
99
|
+
* `POST /api/hostfs` is the preflight-cacheable form of `list`/`stat`/
|
|
100
|
+
* `mkdir`/`rename`/`remove` — one URL for all of them, so one CORS preflight
|
|
101
|
+
* covers the whole `Access-Control-Max-Age` window instead of one per path
|
|
102
|
+
* (#2715). It is purely a transport change: same handlers, same payloads,
|
|
103
|
+
* same errno JSON. The per-op routes stay for compatibility (older webapps,
|
|
104
|
+
* `curl`) and remain the only form for the two body ops.
|
|
105
|
+
*
|
|
54
106
|
* Errors are `{ code, message }` JSON with an errno-derived status so the
|
|
55
|
-
* webapp backend can rethrow faithful `FsError`s
|
|
107
|
+
* webapp backend can rethrow faithful `FsError`s — including body-parser
|
|
108
|
+
* failures, via `hostFsBodyErrorHandler` registered at the end. A bridge that
|
|
109
|
+
* predates the stable endpoint answers `POST /api/hostfs` with a framework 404
|
|
110
|
+
* that has no `code`, which is how the webapp detects it and falls back to the
|
|
111
|
+
* per-op routes — so every error this route emits MUST carry a `code`.
|
|
56
112
|
*/
|
|
57
113
|
export declare function registerHostFsRoutes(app: Express, roots: readonly HostMountRoot[]): void;
|
|
58
114
|
export {};
|
|
@@ -14,6 +14,31 @@
|
|
|
14
14
|
* Protected like every other /api route: same-origin / loopback callers
|
|
15
15
|
* pass, cross-origin callers need the bridge token (see
|
|
16
16
|
* `createThinBridgeCorsMiddleware` in index.ts).
|
|
17
|
+
*
|
|
18
|
+
* Two request shapes reach the same handlers:
|
|
19
|
+
*
|
|
20
|
+
* - the per-op routes (`GET /api/hostfs/list?mount=&path=` …), kept for
|
|
21
|
+
* compatibility and used for the body ops (`read`, `write`);
|
|
22
|
+
* - a single stable `POST /api/hostfs` whose JSON body carries
|
|
23
|
+
* `{ op, mount, path, … }`.
|
|
24
|
+
*
|
|
25
|
+
* The stable endpoint exists purely to make CORS preflights cacheable. The
|
|
26
|
+
* bridge token travels as a custom header, so every cross-origin hostfs call
|
|
27
|
+
* is a non-simple request, and Chrome's Private Network Access preflights
|
|
28
|
+
* public→loopback traffic regardless — but the preflight cache is keyed by
|
|
29
|
+
* URL, and the per-op URLs are unique per path, so `Access-Control-Max-Age`
|
|
30
|
+
* never got a chance to help (issue #2715: 246,893 `OPTIONS` for 385,033
|
|
31
|
+
* `GET`s in one session). One stable URL collapses that to one preflight per
|
|
32
|
+
* max-age. `read` deliberately stays a `GET` on a per-file URL so the
|
|
33
|
+
* browser HTTP cache can still revalidate large blobs with a 304.
|
|
34
|
+
*
|
|
35
|
+
* The hostfs surface owns its own body parsing end to end. `index.ts` skips
|
|
36
|
+
* the stable dispatcher in its global `express.json()` filter (via
|
|
37
|
+
* `isHostFsStableBodyRequest`) so the bounded 1 MiB parser here is the one
|
|
38
|
+
* that runs, and `hostFsBodyErrorHandler` maps body-parser failures to the
|
|
39
|
+
* same `{ code, message }` errno shape as every other error on this surface —
|
|
40
|
+
* otherwise express's default handler answers malformed JSON with code-less
|
|
41
|
+
* HTML and the webapp cannot rethrow a faithful `FsError`.
|
|
17
42
|
*/
|
|
18
43
|
import express from 'express';
|
|
19
44
|
import { lstat, mkdir, readdir, readFile, realpath, rename, rm, stat, writeFile, } from 'fs/promises';
|
|
@@ -124,19 +149,138 @@ function queryString(req, name) {
|
|
|
124
149
|
const value = req.query[name];
|
|
125
150
|
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
126
151
|
}
|
|
152
|
+
/** Path of the stable dispatcher. */
|
|
153
|
+
const HOSTFS_STABLE_PATH = '/api/hostfs';
|
|
154
|
+
/**
|
|
155
|
+
* Bounded body cap for the stable dispatcher — it only ever carries a small
|
|
156
|
+
* JSON envelope, so it has no business inheriting the global 50 MiB limit.
|
|
157
|
+
*/
|
|
158
|
+
export const HOSTFS_STABLE_MAX_BODY_BYTES = 1024 * 1024;
|
|
159
|
+
function isHostFsPath(path) {
|
|
160
|
+
return path === HOSTFS_STABLE_PATH || path.startsWith(`${HOSTFS_STABLE_PATH}/`);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* True for a request `index.ts`'s global `express.json()` must NOT consume.
|
|
164
|
+
* The global parser is mounted before these routes with a 50 MiB limit, so
|
|
165
|
+
* without this exclusion it would swallow the stable dispatcher's body first:
|
|
166
|
+
* the route-local 1 MiB cap would never apply, and a malformed body would
|
|
167
|
+
* fail in the global parser and reach express's default (code-less HTML)
|
|
168
|
+
* handler instead of the errno shape every hostfs caller expects. Exported so
|
|
169
|
+
* `index.ts` and the middleware-order test filter identically.
|
|
170
|
+
*/
|
|
171
|
+
export function isHostFsStableBodyRequest(req) {
|
|
172
|
+
if (req.method !== 'POST')
|
|
173
|
+
return false;
|
|
174
|
+
const path = (req.url ?? '').split('?')[0];
|
|
175
|
+
return path === HOSTFS_STABLE_PATH || path === `${HOSTFS_STABLE_PATH}/`;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Map body-parser failures on the hostfs surface to `{ code, message }`.
|
|
179
|
+
* Registered after the routes (express only runs an error handler for
|
|
180
|
+
* middleware mounted before it), so it catches the stable dispatcher's JSON
|
|
181
|
+
* parser AND `PUT /write`'s raw parser. Mirrors swift-server, which answers
|
|
182
|
+
* 400 `EINVAL` for an unparseable body and 413 `EFBIG` for an oversized one.
|
|
183
|
+
* Anything that is not a body-parser error, and every non-hostfs path, is
|
|
184
|
+
* passed through untouched.
|
|
185
|
+
*/
|
|
186
|
+
const hostFsBodyErrorHandler = (err, req, res, next) => {
|
|
187
|
+
const type = err?.type;
|
|
188
|
+
if (typeof type !== 'string' || res.headersSent || !isHostFsPath(req.path)) {
|
|
189
|
+
next(err);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
193
|
+
if (type === 'entity.too.large') {
|
|
194
|
+
res.status(413).json({ code: 'EFBIG', message: `hostfs body too large: ${message}` });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
res.status(400).json({ code: 'EINVAL', message: `hostfs body rejected: ${message}` });
|
|
198
|
+
};
|
|
199
|
+
/** Ops reachable through the stable `POST /api/hostfs` endpoint. */
|
|
200
|
+
const STABLE_OPS = ['list', 'stat', 'mkdir', 'rename', 'remove'];
|
|
201
|
+
function isStableOp(value) {
|
|
202
|
+
return typeof value === 'string' && STABLE_OPS.includes(value);
|
|
203
|
+
}
|
|
204
|
+
function bodyString(body, name) {
|
|
205
|
+
const value = body[name];
|
|
206
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
207
|
+
}
|
|
208
|
+
/** `recursive` accepts the query-string `'1'` and a JSON `true` alike. */
|
|
209
|
+
function isRecursive(value) {
|
|
210
|
+
return value === true || value === '1' || value === 1;
|
|
211
|
+
}
|
|
212
|
+
/** `readdir` + a stat per entry, so consumers get size/mtime without an N+1. */
|
|
213
|
+
async function listOp(target) {
|
|
214
|
+
const dirents = await readdir(target, { withFileTypes: true });
|
|
215
|
+
const entries = await Promise.all(dirents.map(async (d) => {
|
|
216
|
+
if (d.isDirectory())
|
|
217
|
+
return { name: d.name, kind: 'directory' };
|
|
218
|
+
try {
|
|
219
|
+
// stat() follows symlinks, so a link to a directory classifies as
|
|
220
|
+
// a directory — matching what every subsequent access sees (and
|
|
221
|
+
// the swift server, whose fileExists follows links). Escaping
|
|
222
|
+
// links are still refused at access time by resolveWithinRoot.
|
|
223
|
+
const s = await stat(resolve(target, d.name));
|
|
224
|
+
if (s.isDirectory())
|
|
225
|
+
return { name: d.name, kind: 'directory' };
|
|
226
|
+
return {
|
|
227
|
+
name: d.name,
|
|
228
|
+
kind: 'file',
|
|
229
|
+
size: s.size,
|
|
230
|
+
lastModified: Math.round(s.mtimeMs),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
// Raced deletion / dangling link / unreadable: name only.
|
|
235
|
+
return { name: d.name, kind: 'file' };
|
|
236
|
+
}
|
|
237
|
+
}));
|
|
238
|
+
return { entries };
|
|
239
|
+
}
|
|
240
|
+
async function statOp(target) {
|
|
241
|
+
return statPayload(await stat(target));
|
|
242
|
+
}
|
|
243
|
+
async function mkdirOp(target) {
|
|
244
|
+
await mkdir(target, { recursive: true });
|
|
245
|
+
return { ok: true };
|
|
246
|
+
}
|
|
247
|
+
async function renameOp(from, to) {
|
|
248
|
+
await rename(from, to);
|
|
249
|
+
return { ok: true };
|
|
250
|
+
}
|
|
251
|
+
async function removeOp(target, recursive, roots) {
|
|
252
|
+
if (roots.some((r) => r.root === target)) {
|
|
253
|
+
throw Object.assign(new Error('refusing to remove a mount root'), { code: 'EACCES' });
|
|
254
|
+
}
|
|
255
|
+
await rm(target, { recursive, force: false });
|
|
256
|
+
return { ok: true };
|
|
257
|
+
}
|
|
127
258
|
/**
|
|
128
259
|
* Register the /api/hostfs routes for the resolved mount roots. The route
|
|
129
260
|
* surface mirrors `MountBackend` 1:1 (webapp `backend-hostfs.ts`):
|
|
130
261
|
*
|
|
262
|
+
* POST /api/hostfs ← { op, mount, path, to?, recursive? }
|
|
131
263
|
* GET /api/hostfs/list?mount=&path= → { entries: [...] }
|
|
132
264
|
* GET /api/hostfs/stat?mount=&path= → { kind, size, mtime }
|
|
133
265
|
* GET /api/hostfs/read?mount=&path= → octet-stream body
|
|
134
266
|
* PUT /api/hostfs/write?mount=&path= ← octet-stream body
|
|
135
267
|
* POST /api/hostfs/mkdir?mount=&path=
|
|
268
|
+
* POST /api/hostfs/rename?mount=&path=&to=
|
|
136
269
|
* DELETE /api/hostfs/remove?mount=&path=[&recursive=1]
|
|
137
270
|
*
|
|
271
|
+
* `POST /api/hostfs` is the preflight-cacheable form of `list`/`stat`/
|
|
272
|
+
* `mkdir`/`rename`/`remove` — one URL for all of them, so one CORS preflight
|
|
273
|
+
* covers the whole `Access-Control-Max-Age` window instead of one per path
|
|
274
|
+
* (#2715). It is purely a transport change: same handlers, same payloads,
|
|
275
|
+
* same errno JSON. The per-op routes stay for compatibility (older webapps,
|
|
276
|
+
* `curl`) and remain the only form for the two body ops.
|
|
277
|
+
*
|
|
138
278
|
* Errors are `{ code, message }` JSON with an errno-derived status so the
|
|
139
|
-
* webapp backend can rethrow faithful `FsError`s
|
|
279
|
+
* webapp backend can rethrow faithful `FsError`s — including body-parser
|
|
280
|
+
* failures, via `hostFsBodyErrorHandler` registered at the end. A bridge that
|
|
281
|
+
* predates the stable endpoint answers `POST /api/hostfs` with a framework 404
|
|
282
|
+
* that has no `code`, which is how the webapp detects it and falls back to the
|
|
283
|
+
* per-op routes — so every error this route emits MUST carry a `code`.
|
|
140
284
|
*/
|
|
141
285
|
export function registerHostFsRoutes(app, roots) {
|
|
142
286
|
const byPath = new Map(roots.map((r) => [r.path, r]));
|
|
@@ -160,35 +304,58 @@ export function registerHostFsRoutes(app, roots) {
|
|
|
160
304
|
})();
|
|
161
305
|
};
|
|
162
306
|
};
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
307
|
+
// Stable-URL dispatcher. Mounted before the per-op routes so `/api/hostfs`
|
|
308
|
+
// is matched exactly; express keeps `/api/hostfs/mkdir` distinct.
|
|
309
|
+
app.post(HOSTFS_STABLE_PATH, express.json({ limit: HOSTFS_STABLE_MAX_BODY_BYTES }), (req, res) => {
|
|
310
|
+
void (async () => {
|
|
311
|
+
const body = (req.body ?? {});
|
|
312
|
+
const op = body.op;
|
|
313
|
+
if (!isStableOp(op)) {
|
|
314
|
+
res.status(400).json({ code: 'EINVAL', message: `unsupported hostfs op: ${String(op)}` });
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const mount = bodyString(body, 'mount');
|
|
318
|
+
const entry = mount ? byPath.get(mount) : undefined;
|
|
319
|
+
if (!entry) {
|
|
320
|
+
res.status(404).json({ code: 'ENOENT', message: `no such mount: ${mount ?? ''}` });
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
168
323
|
try {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
324
|
+
const target = await resolveWithinRoot(entry.root, bodyString(body, 'path') ?? '');
|
|
325
|
+
switch (op) {
|
|
326
|
+
case 'list':
|
|
327
|
+
res.json(await listOp(target));
|
|
328
|
+
return;
|
|
329
|
+
case 'stat':
|
|
330
|
+
res.json(await statOp(target));
|
|
331
|
+
return;
|
|
332
|
+
case 'mkdir':
|
|
333
|
+
res.json(await mkdirOp(target));
|
|
334
|
+
return;
|
|
335
|
+
case 'rename': {
|
|
336
|
+
const toRel = bodyString(body, 'to');
|
|
337
|
+
if (!toRel) {
|
|
338
|
+
res.status(400).json({ code: 'EINVAL', message: 'rename requires to' });
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
res.json(await renameOp(target, await resolveWithinRoot(entry.root, toRel)));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
case 'remove':
|
|
345
|
+
res.json(await removeOp(target, isRecursive(body.recursive), roots));
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
182
348
|
}
|
|
183
|
-
catch {
|
|
184
|
-
|
|
185
|
-
return { name: d.name, kind: 'file' };
|
|
349
|
+
catch (err) {
|
|
350
|
+
sendFsError(res, err);
|
|
186
351
|
}
|
|
187
|
-
}));
|
|
188
|
-
|
|
352
|
+
})();
|
|
353
|
+
});
|
|
354
|
+
app.get('/api/hostfs/list', withTarget(async (target, _req, res) => {
|
|
355
|
+
res.json(await listOp(target));
|
|
189
356
|
}));
|
|
190
357
|
app.get('/api/hostfs/stat', withTarget(async (target, _req, res) => {
|
|
191
|
-
res.json(
|
|
358
|
+
res.json(await statOp(target));
|
|
192
359
|
}));
|
|
193
360
|
app.get('/api/hostfs/read', withTarget(async (target, _req, res) => {
|
|
194
361
|
// lstat first so a dangling symlink is ENOENT, a directory is EISDIR.
|
|
@@ -229,8 +396,7 @@ export function registerHostFsRoutes(app, roots) {
|
|
|
229
396
|
res.json({ ok: true });
|
|
230
397
|
}));
|
|
231
398
|
app.post('/api/hostfs/mkdir', withTarget(async (target, _req, res) => {
|
|
232
|
-
await
|
|
233
|
-
res.json({ ok: true });
|
|
399
|
+
res.json(await mkdirOp(target));
|
|
234
400
|
}));
|
|
235
401
|
app.post('/api/hostfs/rename', withTarget(async (target, req, res) => {
|
|
236
402
|
const mount = queryString(req, 'mount');
|
|
@@ -240,18 +406,11 @@ export function registerHostFsRoutes(app, roots) {
|
|
|
240
406
|
res.status(400).json({ code: 'EINVAL', message: 'rename requires mount and to' });
|
|
241
407
|
return;
|
|
242
408
|
}
|
|
243
|
-
|
|
244
|
-
await rename(target, to);
|
|
245
|
-
res.json({ ok: true });
|
|
409
|
+
res.json(await renameOp(target, await resolveWithinRoot(entry.root, toRel)));
|
|
246
410
|
}));
|
|
247
411
|
app.delete('/api/hostfs/remove', withTarget(async (target, req, res) => {
|
|
248
|
-
|
|
249
|
-
if (isRoot) {
|
|
250
|
-
res.status(403).json({ code: 'EACCES', message: 'refusing to remove a mount root' });
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
const recursive = queryString(req, 'recursive') === '1';
|
|
254
|
-
await rm(target, { recursive, force: false });
|
|
255
|
-
res.json({ ok: true });
|
|
412
|
+
res.json(await removeOp(target, queryString(req, 'recursive') === '1', roots));
|
|
256
413
|
}));
|
|
414
|
+
// Last: only reachable for errors raised by middleware mounted above it.
|
|
415
|
+
app.use(hostFsBodyErrorHandler);
|
|
257
416
|
}
|
|
@@ -11,7 +11,7 @@ import { homedir } from 'os';
|
|
|
11
11
|
import { basename, dirname, join, resolve } from 'path';
|
|
12
12
|
import { fileURLToPath } from 'url';
|
|
13
13
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
14
|
-
import { BRIDGE_TOKEN_HEADER, buildCorsHeaders, buildPnaPreflightHeaders, isLoopbackBridgeOrigin, resolveServerBridgeToken, selectBridgeSubprotocol, shouldMountThinBridgeCors, validateBridgeToken, validateBridgeUpgrade, } from './bridge-security.js';
|
|
14
|
+
import { BRIDGE_TOKEN_HEADER, buildCorsHeaders, buildPnaPreflightHeaders, isLoopbackBridgeOrigin, preflightMaxAge, resolveServerBridgeToken, selectBridgeSubprotocol, shouldMountThinBridgeCors, validateBridgeToken, validateBridgeUpgrade, } from './bridge-security.js';
|
|
15
15
|
import { closeLaunchedBrowserGracefully } from './browser-shutdown.js';
|
|
16
16
|
import { applyCdpUnmask } from './cdp-proxy/cdp-unmask.js';
|
|
17
17
|
import { createCdpSessionUrlTracker } from './cdp-proxy/session-url-tracker.js';
|
|
@@ -28,6 +28,7 @@ import { registerCloudStatusEndpoint } from './cloud-status.js';
|
|
|
28
28
|
import { ElectronAppAlreadyRunningError, ElectronOverlayInjector, launchElectronApp, resolveOverlayThinBridge, } from './electron-controller.js';
|
|
29
29
|
import { getElectronAppPorts } from './electron-runtime.js';
|
|
30
30
|
import { ElectronTrayFollower } from './electron-tray-follower.js';
|
|
31
|
+
import { shouldParseGlobalJson } from './fetch-proxy-headers.js';
|
|
31
32
|
import { FileLogger } from './file-logger.js';
|
|
32
33
|
import { registerHostedBootstrapEndpoint } from './hosted-bootstrap.js';
|
|
33
34
|
import { registerHostFsRoutes, resolveHostMountRoots } from './hostfs.js';
|
|
@@ -1018,7 +1019,7 @@ function createThinBridgeCorsMiddleware(bridgeToken) {
|
|
|
1018
1019
|
if (req.method === 'OPTIONS' && cors) {
|
|
1019
1020
|
for (const [k, v] of Object.entries(buildPnaPreflightHeaders()))
|
|
1020
1021
|
res.setHeader(k, v);
|
|
1021
|
-
res.setHeader('Access-Control-Max-Age',
|
|
1022
|
+
res.setHeader('Access-Control-Max-Age', preflightMaxAge(req.path));
|
|
1022
1023
|
res.status(204).end();
|
|
1023
1024
|
return;
|
|
1024
1025
|
}
|
|
@@ -1084,15 +1085,9 @@ async function main() {
|
|
|
1084
1085
|
const { lickWss, broadcastLickEvent } = lickBridge;
|
|
1085
1086
|
// OAuth callback — generic redirect target for OAuth providers (implicit + PKCE)
|
|
1086
1087
|
registerOAuthCallbackRoutes(app);
|
|
1087
|
-
// Global JSON body parser
|
|
1088
|
-
//
|
|
1089
|
-
|
|
1090
|
-
// re-serialize them via JSON.stringify, breaking the signature).
|
|
1091
|
-
app.use(express.json({
|
|
1092
|
-
limit: '50mb',
|
|
1093
|
-
type: (req) => req.headers['x-slicc-raw-body'] !== '1' &&
|
|
1094
|
-
(req.headers['content-type'] ?? '').includes('application/json'),
|
|
1095
|
-
}));
|
|
1088
|
+
// Global JSON body parser; `shouldParseGlobalJson` owns the exclusions
|
|
1089
|
+
// (SigV4 raw bodies, the stable hostfs dispatcher).
|
|
1090
|
+
app.use(express.json({ limit: '50mb', type: shouldParseGlobalJson }));
|
|
1096
1091
|
app.get('/api/runtime-config', (_req, res) => {
|
|
1097
1092
|
res.json({
|
|
1098
1093
|
trayWorkerBaseUrl:
|