@sparkelf/dsh-patch-web-base-path 0.1.0-rc.10

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write patches/npm/web-base-path/README.md
5
+ README.md: 01ce2dd51187b11a7cf24b40115e0620465a1cd3
6
+ README.zh.md: 01156396482f6c55f1f6bfc13641e1f8983c6cb6
package/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # @sparkelf/dsh-patch-web-base-path
2
+
3
+ English | [中文](README.zh.md)
4
+
5
+ This data-only package adds complete reverse-proxy mount-prefix support to the exact official DSH source base. One `--base-path /prefix` or `DSH_WEB_BASE_PATH=/prefix` value is normalized by WebServer, stripped before HTTP and upgrade route dispatch, injected as the document `<base>`, propagated to startup/browser URLs, and consumed by Connection RPC and Session export. Vite assets, parser preloads, PWA metadata, Backup, and DataOps browser requests resolve below the same document base.
6
+
7
+ The payload intentionally spans the WebServer, frontend static owner, Web app startup and shell, Connection browser transport, Session export, and Web source assets. Splitting those files would permit partially mounted deployments, so they share one patch lifecycle. Default empty-prefix behavior remains byte-for-byte root-relative from the user's perspective.
8
+
9
+ The target is official source base `cd5ef8148158c3a752a658978873241fdf8e2bbc`; the package has no JavaScript entry, lifecycle script, compatibility fallback, or credential data. Retire it when official DSH ships equivalent end-to-end base-path behavior.
package/README.zh.md ADDED
@@ -0,0 +1,9 @@
1
+ # @sparkelf/dsh-patch-web-base-path
2
+
3
+ [English](README.md) | 中文
4
+
5
+ 该data-only package为exact official DSH source base增加完整reverse-proxy mount-prefix support。一个`--base-path /prefix`或`DSH_WEB_BASE_PATH=/prefix`值由WebServer normalize,在HTTP与upgrade route dispatch前strip,作为document `<base>`注入,并传到startup/browser URLs,由Connection RPC与Session export消费。Vite assets、parser preloads、PWA metadata、Backup与DataOps browser requests都在同一document base下解析。
6
+
7
+ payload有意跨越WebServer、frontend static owner、Web app startup/shell、Connection browser transport、Session export与Web source assets。拆分这些files会允许partial mounted deployments,因此它们共享一个patch lifecycle。empty-prefix default behavior从用户视角保持root-relative不变。
8
+
9
+ target是official source base `cd5ef8148158c3a752a658978873241fdf8e2bbc`;package没有JavaScript entry、lifecycle script、compatibility fallback或credential data。official DSH发布等价end-to-end base-path behavior后retire。
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "description": "Data-only exact official-source patch for complete reverse-proxy Web base-path support",
3
+ "dshPatch": {
4
+ "formatVersion": 1,
5
+ "variants": [
6
+ {
7
+ "dsh": ">=0.1.2-alpha.1",
8
+ "file": "./patches/web-base-path.patch",
9
+ "id": "web-reverse-proxy-base-path",
10
+ "target": {
11
+ "baseRevision": "cd5ef8148158c3a752a658978873241fdf8e2bbc",
12
+ "kind": "dsh-source",
13
+ "paths": [
14
+ "apps/web/",
15
+ "packages/bundle/web-app/",
16
+ "packages/client/connection/",
17
+ "packages/host/frontend-static/",
18
+ "packages/host/webserver/",
19
+ "packages/session-query/session-log-export/"
20
+ ]
21
+ }
22
+ }
23
+ ]
24
+ },
25
+ "files": [
26
+ "patches/*.patch"
27
+ ],
28
+ "license": "MIT",
29
+ "name": "@sparkelf/dsh-patch-web-base-path",
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "repository": {
34
+ "directory": "patches/npm/web-base-path",
35
+ "type": "git",
36
+ "url": "git+https://github.com/SparkElf/deepseek-harness-plus.git"
37
+ },
38
+ "type": "module",
39
+ "version": "0.1.0-rc.10"
40
+ }
@@ -0,0 +1,339 @@
1
+ diff --git a/apps/web/index.html b/apps/web/index.html
2
+ index 75c4e5bac6..27024bbef4 100644
3
+ --- a/apps/web/index.html
4
+ +++ b/apps/web/index.html
5
+ @@ -3,12 +3,12 @@
6
+ <head>
7
+ <meta charset="utf-8" />
8
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
9
+ - <link rel="manifest" href="/manifest.webmanifest" />
10
+ - <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
11
+ + <link rel="manifest" href="./manifest.webmanifest" />
12
+ + <link rel="icon" type="image/svg+xml" href="./favicon.svg" />
13
+ <title>DSH Local Build</title>
14
+ </head>
15
+ <body>
16
+ <div id="root"></div>
17
+ - <script type="module" src="/src/main.ts"></script>
18
+ + <script type="module" src="./src/main.ts"></script>
19
+ </body>
20
+ </html>
21
+ diff --git a/apps/web/public/manifest.webmanifest b/apps/web/public/manifest.webmanifest
22
+ index 20a428fee6..a7cc973d9e 100644
23
+ --- a/apps/web/public/manifest.webmanifest
24
+ +++ b/apps/web/public/manifest.webmanifest
25
+ @@ -1,13 +1,13 @@
26
+ {
27
+ - "id": "/",
28
+ + "id": ".",
29
+ "name": "DeepSeek Harness",
30
+ "short_name": "DSH",
31
+ - "start_url": "/",
32
+ - "scope": "/",
33
+ + "start_url": ".",
34
+ + "scope": ".",
35
+ "display": "fullscreen",
36
+ "icons": [
37
+ {
38
+ - "src": "/favicon.svg",
39
+ + "src": "./favicon.svg",
40
+ "sizes": "any",
41
+ "type": "image/svg+xml",
42
+ "purpose": "any"
43
+ diff --git a/packages/bundle/web-app/cordis.patch.yml b/packages/bundle/web-app/cordis.patch.yml
44
+ index 9ee698044e..57757915ba 100644
45
+ --- a/packages/bundle/web-app/cordis.patch.yml
46
+ +++ b/packages/bundle/web-app/cordis.patch.yml
47
+ @@ -114,6 +114,7 @@
48
+ config:
49
+ host: !!js ctx.webStartup.host ?? '127.0.0.1'
50
+ port: !!js ctx.webStartup.port ?? 3080
51
+ + basePath: !!js ctx.webStartup.basePath ?? process.env.DSH_WEB_BASE_PATH ?? ''
52
+ compression: gzip
53
+ compressionLevel: 1
54
+ compressionThresholdBytes: 1024
55
+ diff --git a/packages/bundle/web-app/src/index.ts b/packages/bundle/web-app/src/index.ts
56
+ index d0b2635f8e..669dc756ea 100644
57
+ --- a/packages/bundle/web-app/src/index.ts
58
+ +++ b/packages/bundle/web-app/src/index.ts
59
+ @@ -157,9 +157,9 @@ function webSurfacePrompt(webUrl: string): string {
60
+
61
+ /** Resolve the canonical loopback URL from the active Web server. */
62
+ function localWebUrl(ctx: Context): string {
63
+ - const port = ctx.get('webServer')?.port
64
+ - if (port === undefined) throw new Error('web-app: webServer service missing while resolving Web runtime')
65
+ - return `http://${LOOPBACK_HOST}:${String(port)}`
66
+ + const webServer = ctx.get('webServer')
67
+ + if (webServer === undefined) throw new Error('web-app: webServer service missing while resolving Web runtime')
68
+ + return `http://${LOOPBACK_HOST}:${String(webServer.port)}${webServer.basePath}`
69
+ }
70
+
71
+ /**
72
+ @@ -275,7 +275,7 @@ export function apply(ctx: Context, config: Config): void {
73
+ const port = connectionCtx.webServer.port
74
+ const lanUrl = lanCandidate === undefined
75
+ ? undefined
76
+ - : connectionCtx.connection.authenticatedUrl(`http://${lanCandidate}:${String(port)}`)
77
+ + : connectionCtx.connection.authenticatedUrl(`http://${lanCandidate}:${String(port)}${connectionCtx.webServer.basePath}`)
78
+ ANNOUNCED_ROOTS.add(connectionCtx.root)
79
+ if (config.printUrl) {
80
+ console.log(`dsh web: ${authenticatedUrl}${lanUrl === undefined ? '' : ` (LAN: ${lanUrl})`}`)
81
+ diff --git a/packages/bundle/web-app/src/startup.ts b/packages/bundle/web-app/src/startup.ts
82
+ index 9faad244dd..e203b432a7 100644
83
+ --- a/packages/bundle/web-app/src/startup.ts
84
+ +++ b/packages/bundle/web-app/src/startup.ts
85
+ @@ -1,6 +1,6 @@
86
+ /**
87
+ * The web app's command-line provider: it parses the `dsh --profile web` flag
88
+ - * family (`--host`, `--port`, `--trusted-host`, `--no-open`) and its `--help`
89
+ + * family (`--host`, `--port`, `--base-path`, `--trusted-host`, `--no-open`) and its `--help`
90
+ * text, then provides the immutable values as {@link WEB_STARTUP_SERVICE}.
91
+ * Ordinary rows inject that service before reading it from lazy config.
92
+ * @module @deepseek-ai/dsh-web-app/startup
93
+ @@ -27,12 +27,15 @@ export interface WebStartupValues {
94
+ host?: string
95
+ /** `--port`, absent when the invocation did not name one. */
96
+ port?: number
97
+ + /** `--base-path`, absent when the invocation did not name one. */
98
+ + basePath?: string
99
+ /** Explicit `--trusted-host` authorities, in argument order. */
100
+ trustedHosts: string[]
101
+ }
102
+
103
+ /** The web flag family, as commander parsed it. */
104
+ interface WebOptions {
105
+ + basePath?: string
106
+ host?: string
107
+ open: boolean
108
+ port?: string
109
+ @@ -49,12 +52,14 @@ function webCommand(): Command {
110
+ .description('Serve the DeepSeek Harness browser UI.')
111
+ .helpOption('-h, --help', 'show this help')
112
+ .option('--host <host>', 'bind host')
113
+ + .option('--base-path <path>', 'mount the Web UI below a reverse-proxy path prefix (for example /dsh)')
114
+ .option('--no-open', 'do not open the Web UI in the default browser')
115
+ .option('--port <port>', 'listen port; pass 0 to let the OS pick a free one')
116
+ .option('--trusted-host <authority...>', 'extra authority the /api browser-trust fence accepts (host or host:port; repeatable)')
117
+ .addHelpText('after', `
118
+ Examples:
119
+ dsh --profile web serve on the composed host and port
120
+ + dsh --profile web --base-path /dsh serve below a reverse-proxy prefix
121
+ dsh --profile web --no-open serve without opening a browser
122
+ dsh --profile web --port 8080 serve on another port
123
+ `)
124
+ @@ -79,6 +84,7 @@ export function apply(ctx: Context): void {
125
+ }
126
+ ctx.provide(WEB_STARTUP_SERVICE, {
127
+ openBrowser: options.open,
128
+ + ...options.basePath !== undefined && { basePath: options.basePath },
129
+ ...options.host !== undefined && { host: options.host },
130
+ ...options.port !== undefined && { port: Number(options.port) },
131
+ trustedHosts: options.trustedHost ?? [],
132
+ diff --git a/packages/client/connection/src/client/rpc.ts b/packages/client/connection/src/client/rpc.ts
133
+ index 2c3f7d3e28..1c32c56d9e 100644
134
+ --- a/packages/client/connection/src/client/rpc.ts
135
+ +++ b/packages/client/connection/src/client/rpc.ts
136
+ @@ -7,8 +7,7 @@ import {
137
+ } from '../rpc.ts'
138
+ import type { ClientConnectionRpc, ConnectionRpcResult } from '../rpc.ts'
139
+ import { randomUuid } from './random-uuid.ts'
140
+ -
141
+ -const INTERNAL_BASE = 'http://dsh.internal'
142
+ +const INTERNAL_BASE = 'http://dsh.internal/'
143
+ const CHANNEL_PATTERN = /^\/[A-Za-z0-9._~-]+$/
144
+ const ENDPOINT_SEGMENT_PATTERN = /^[A-Za-z0-9_$.-]+$/
145
+
146
+ @@ -22,6 +21,15 @@ export type RpcStreamOpen = (
147
+ signal: AbortSignal,
148
+ ) => AsyncIterable<unknown>
149
+
150
+ +/** Resolve one logical route below the runtime-injected document base. */
151
+ +function resolveWebUrl(path: string): URL {
152
+ + const documentBase = typeof document === 'undefined' ? undefined : document.baseURI
153
+ + if (documentBase !== undefined) return new URL(path.replace(/^\/+/, ''), documentBase)
154
+ + const origin = (globalThis as { location?: { origin?: string } }).location?.origin
155
+ + const base = origin !== undefined && origin !== 'null' ? `${origin}/` : INTERNAL_BASE
156
+ + return new URL(path.replace(/^\/+/, ''), base)
157
+ +}
158
+ +
159
+ /**
160
+ * Create the browser-backed generic RPC caller.
161
+ * @param doFetch - transport override; defaults to the page's global fetch.
162
+ @@ -41,7 +49,7 @@ export function createWebConnectionRpc(doFetch?: RpcFetch, openStream?: RpcStrea
163
+ payload,
164
+ }
165
+ const response = await send(
166
+ - new URL(`${channel}/${endpoint}`, resolveBase()),
167
+ + resolveWebUrl(`${channel}/${endpoint}`),
168
+ {
169
+ method: 'POST',
170
+ headers: { 'content-type': 'application/json' },
171
+ @@ -105,11 +113,6 @@ function isRecord(value: unknown): value is Record<PropertyKey, unknown> {
172
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
173
+ }
174
+
175
+ -function resolveBase(): string {
176
+ - const location = (globalThis as { location?: { origin?: string } }).location
177
+ - return location?.origin !== undefined && location.origin !== 'null' ? location.origin : INTERNAL_BASE
178
+ -}
179
+ -
180
+ function assertTarget(channel: string, endpoint: string): void {
181
+ const segments = endpoint.split('/')
182
+ if (!CHANNEL_PATTERN.test(channel)
183
+ diff --git a/packages/host/frontend-static/src/index.ts b/packages/host/frontend-static/src/index.ts
184
+ index 3d218f31e6..4f717338a7 100644
185
+ --- a/packages/host/frontend-static/src/index.ts
186
+ +++ b/packages/host/frontend-static/src/index.ts
187
+ @@ -119,7 +119,10 @@ export function apply(ctx: Context, config: Config): void {
188
+ // served form anchors them at the site root ahead of every URL-bearing tag.
189
+ const renderIndex = async (): Promise<string> => {
190
+ const body = ctx.webServer.renderIndex(await readFile(distIndex, 'utf8'))
191
+ - return body.replace(/<head(?:\s[^>]*)?>/i, open => `${open}<base href="/">`)
192
+ + const href = `${ctx.webServer.basePath}/`
193
+ + return body
194
+ + .replace(/<head(?:\s[^>]*)?>/i, open => `${open}<base href="${href}">`)
195
+ + .replaceAll('src="/plugins/', 'src="plugins/')
196
+ }
197
+ ctx.effect(() => ctx.webServer.registerFallback(async (req, res) => {
198
+ // Non-GET/HEAD without a matching named route is 405 (fallback-only
199
+ diff --git a/packages/host/webserver/src/index.ts b/packages/host/webserver/src/index.ts
200
+ index bc41100de9..205f170baa 100644
201
+ --- a/packages/host/webserver/src/index.ts
202
+ +++ b/packages/host/webserver/src/index.ts
203
+ @@ -61,6 +61,8 @@ export interface Config {
204
+ host: '127.0.0.1' | '0.0.0.0'
205
+ /** Listen port; zero requests an OS-assigned port. */
206
+ port: number
207
+ + /** External URL prefix; empty means site root, otherwise an absolute path without a trailing slash. */
208
+ + basePath?: string
209
+ /** Response compression for socket-backed HTTP requests. @default 'none' */
210
+ compression?: 'none' | 'gzip'
211
+ /** Gzip DEFLATE level from 0 through 9. @default 1 */
212
+ @@ -69,6 +71,20 @@ export interface Config {
213
+ compressionThresholdBytes?: number
214
+ }
215
+
216
+ +/** Validate and canonicalize the external reverse-proxy mount prefix. */
217
+ +export function normalizeWebBasePath(value: string | undefined): string {
218
+ + const candidate = value?.trim() ?? ''
219
+ + if (candidate === '' || candidate === '/') return ''
220
+ + if (!candidate.startsWith('/') || candidate.endsWith('/') || candidate.includes('?') || candidate.includes('#')) {
221
+ + throw new Error('webserver: basePath must be empty or an absolute path without query, fragment, or trailing slash')
222
+ + }
223
+ + const segments = candidate.slice(1).split('/')
224
+ + if (segments.some(segment => segment === '' || segment === '.' || segment === '..')) {
225
+ + throw new Error('webserver: basePath must contain only non-empty path segments and no dot segments')
226
+ + }
227
+ + return candidate
228
+ +}
229
+ +
230
+ const DEFAULT_COMPRESSION = 'none' as const
231
+ const DEFAULT_COMPRESSION_LEVEL = 1
232
+ const DEFAULT_COMPRESSION_THRESHOLD_BYTES = 1024
233
+ @@ -125,6 +141,7 @@ export class WebServer extends Service {
234
+ static Config: z<Config> = z.object({
235
+ host: z.union([z.const('127.0.0.1'), z.const('0.0.0.0')]).required(),
236
+ port: z.natural().max(65535).required(),
237
+ + basePath: z.string().default(''),
238
+ compression: z.union([z.const('none'), z.const('gzip')]).default(DEFAULT_COMPRESSION),
239
+ compressionLevel: z.number().step(1).min(0).max(9).default(DEFAULT_COMPRESSION_LEVEL),
240
+ compressionThresholdBytes: z.natural().default(DEFAULT_COMPRESSION_THRESHOLD_BYTES),
241
+ @@ -138,11 +155,13 @@ export class WebServer extends Service {
242
+ private fallback: WebRoute['handler'] | undefined
243
+ private server!: Server
244
+ private listenedPort!: number
245
+ + private readonly mountedBasePath: string
246
+ private readonly gzip: NodeMiddleware | undefined
247
+
248
+ constructor(ctx: Context, private config: Config) {
249
+ super(ctx, 'webServer')
250
+ const resolved = config as ResolvedConfig
251
+ + this.mountedBasePath = normalizeWebBasePath(config.basePath)
252
+ this.gzip = resolved.compression === 'gzip' ? createGzipMiddleware(resolved) : undefined
253
+ }
254
+
255
+ @@ -156,6 +175,11 @@ export class WebServer extends Service {
256
+ return this.config.host
257
+ }
258
+
259
+ + /** External reverse-proxy mount prefix; empty means the site root. */
260
+ + get basePath(): string {
261
+ + return this.mountedBasePath
262
+ + }
263
+ +
264
+ /**
265
+ * Register a named route. Duplicate (kind, path) throws — route patterns are
266
+ * a composition-level contract, so a collision is a misconfiguration.
267
+ @@ -240,6 +264,13 @@ export class WebServer extends Service {
268
+ // client dropping mid-body). Per-request failures log and answer 400 —
269
+ // never a process exit.
270
+ this.server = createServer((req, res) => {
271
+ + const logicalUrl = this.logicalUrl(req.url ?? '/')
272
+ + if (logicalUrl === null) {
273
+ + res.writeHead(404)
274
+ + res.end()
275
+ + return
276
+ + }
277
+ + req.url = logicalUrl
278
+ const next = (): void => {
279
+ void handle(req, res).catch((err: unknown) => {
280
+ this.ctx.logger.warn(err instanceof Error ? err : new Error(String(err)))
281
+ @@ -266,8 +297,13 @@ export class WebServer extends Service {
282
+ })
283
+ let route: WebUpgradeRoute | undefined
284
+ try {
285
+ - /* v8 ignore next -- node:http always sets url on server requests. */
286
+ - route = this.upgrades.get(new URL(req.url ?? '/', 'http://x').pathname)
287
+ + const logicalUrl = this.logicalUrl(req.url ?? '/')
288
+ + if (logicalUrl === null) {
289
+ + socket.destroy()
290
+ + return
291
+ + }
292
+ + req.url = logicalUrl
293
+ + route = this.upgrades.get(new URL(logicalUrl, 'http://x').pathname)
294
+ } catch (error) {
295
+ this.ctx.logger.warn(error instanceof Error ? error : new Error(String(error)))
296
+ socket.destroy()
297
+ @@ -314,6 +350,16 @@ export class WebServer extends Service {
298
+ }, 'webServer.listen')
299
+ }
300
+
301
+ + /** Strip the external mount prefix while retaining query text. */
302
+ + private logicalUrl(rawUrl: string): string | null {
303
+ + const parsed = new URL(rawUrl, 'http://x')
304
+ + const basePath = this.mountedBasePath
305
+ + if (basePath === '') return `${parsed.pathname}${parsed.search}`
306
+ + if (parsed.pathname === basePath) return `/${parsed.search}`
307
+ + if (!parsed.pathname.startsWith(`${basePath}/`)) return null
308
+ + return `${parsed.pathname.slice(basePath.length)}${parsed.search}`
309
+ + }
310
+ +
311
+ /** Longest-prefix-wins over the prefix table after an exact-table miss. */
312
+ private match(pathname: string): WebRoute | undefined {
313
+ const exact = this.exact.get(pathname)
314
+ diff --git a/packages/session-query/session-log-export/src/client/controller.ts b/packages/session-query/session-log-export/src/client/controller.ts
315
+ index f7f01cceb9..f69be84de1 100644
316
+ --- a/packages/session-query/session-log-export/src/client/controller.ts
317
+ +++ b/packages/session-query/session-log-export/src/client/controller.ts
318
+ @@ -44,10 +44,9 @@ export function downloadUrl(url: string, filename: string): void {
319
+ anchor.click()
320
+ }
321
+
322
+ -/** Resolve the browser's Host base with the connection carrier's null-origin fallback. */
323
+ +/** Resolve the browser document base. */
324
+ function hostBase(): string {
325
+ - const origin = (globalThis as { location?: { origin?: string } }).location?.origin
326
+ - return origin !== undefined && origin !== 'null' ? origin : 'http://dsh.internal'
327
+ + return document.baseURI
328
+ }
329
+
330
+ function messageOf(error: unknown): string {
331
+ @@ -112,7 +111,7 @@ export class SessionLogDownloadController {
332
+ private async run(sessionId: SessionId, signal: AbortSignal): Promise<void> {
333
+ this.publish(sessionId, { open: true, status: 'downloading', error: null })
334
+ try {
335
+ - const url = new URL('/api/session.export', hostBase())
336
+ + const url = new URL('api/session.export', hostBase())
337
+ url.searchParams.set('sessionId', sessionId)
338
+ url.searchParams.set('includeDescendants', 'true')
339
+ const response = await this.fetcher(url, { method: 'HEAD', signal })