dsh-zen-remote 2.3.11 → 2.4.1
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/CHANGELOG.md +45 -78
- package/README.md +94 -84
- package/README.zh-CN.md +90 -81
- package/bin/zen-remote.mjs +5 -0
- package/docs/interface.md +6 -6
- package/docs/remote-access.en.md +6 -4
- package/docs/remote-access.md +6 -4
- package/dsh-push.mjs +96 -10
- package/lib/adapters/dsh-better-sidebar/index.js +7 -3
- package/lib/adapters/dsh-better-sidebar/index.js.map +1 -1
- package/lib/admin-routes.js +10 -2
- package/lib/admin-routes.js.map +1 -1
- package/lib/cli/args.js +251 -0
- package/lib/cli/args.js.map +1 -0
- package/lib/cli/bootstrap.js +449 -0
- package/lib/cli/bootstrap.js.map +1 -0
- package/lib/cli/command-setup.js +484 -0
- package/lib/cli/command-setup.js.map +1 -0
- package/lib/cli/commands-client.js +359 -0
- package/lib/cli/commands-client.js.map +1 -0
- package/lib/cli/commands-host.js +1108 -0
- package/lib/cli/commands-host.js.map +1 -0
- package/lib/cli/commands-status.js +279 -0
- package/lib/cli/commands-status.js.map +1 -0
- package/lib/cli/config-writer.js +334 -0
- package/lib/cli/config-writer.js.map +1 -0
- package/lib/cli/context.js +10 -0
- package/lib/cli/context.js.map +1 -0
- package/lib/cli/i18n.js +620 -0
- package/lib/cli/i18n.js.map +1 -0
- package/lib/cli/ipc-client.js +160 -0
- package/lib/cli/ipc-client.js.map +1 -0
- package/lib/cli/ipc-server.js +464 -0
- package/lib/cli/ipc-server.js.map +1 -0
- package/lib/cli/main.js +245 -0
- package/lib/cli/main.js.map +1 -0
- package/lib/cli/qr-terminal.js +48 -0
- package/lib/cli/qr-terminal.js.map +1 -0
- package/lib/client-data/qr.js +286 -0
- package/lib/client-data/qr.js.map +1 -0
- package/lib/client-routes.js +308 -25
- package/lib/client-routes.js.map +1 -1
- package/lib/client.js +1013 -229
- package/lib/config.js +50 -12
- package/lib/config.js.map +1 -1
- package/lib/debug-report.js +28 -1
- package/lib/debug-report.js.map +1 -1
- package/lib/fetch-route-intercept.js +334 -26
- package/lib/fetch-route-intercept.js.map +1 -1
- package/lib/index.js +608 -375
- package/lib/index.js.map +1 -1
- package/lib/intercept.js +1833 -280
- package/lib/intercept.js.map +1 -1
- package/lib/lan-gate-server.cjs +36 -10
- package/lib/merge-streams.js +103 -11
- package/lib/merge-streams.js.map +1 -1
- package/lib/plugin-route-intercept.js +93 -63
- package/lib/plugin-route-intercept.js.map +1 -1
- package/lib/relay-access.js +8 -1
- package/lib/relay-access.js.map +1 -1
- package/lib/relay-client.js +133 -4
- package/lib/relay-client.js.map +1 -1
- package/lib/relay-pool.js +275 -0
- package/lib/relay-pool.js.map +1 -0
- package/lib/relay-server.js +269 -17
- package/lib/relay-server.js.map +1 -1
- package/lib/restart-fields.js +6 -4
- package/lib/restart-fields.js.map +1 -1
- package/lib/share-ops.js +68 -0
- package/lib/share-ops.js.map +1 -1
- package/lib/share-store.js +65 -27
- package/lib/share-store.js.map +1 -1
- package/lib/state-store.js +400 -0
- package/lib/state-store.js.map +1 -0
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts +3 -2
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -1
- package/lib/types/admin-routes.d.ts.map +1 -1
- package/lib/types/cli/args.d.ts +78 -0
- package/lib/types/cli/args.d.ts.map +1 -0
- package/lib/types/cli/bootstrap.d.ts +170 -0
- package/lib/types/cli/bootstrap.d.ts.map +1 -0
- package/lib/types/cli/command-setup.d.ts +72 -0
- package/lib/types/cli/command-setup.d.ts.map +1 -0
- package/lib/types/cli/commands-client.d.ts +36 -0
- package/lib/types/cli/commands-client.d.ts.map +1 -0
- package/lib/types/cli/commands-host.d.ts +82 -0
- package/lib/types/cli/commands-host.d.ts.map +1 -0
- package/lib/types/cli/commands-status.d.ts +46 -0
- package/lib/types/cli/commands-status.d.ts.map +1 -0
- package/lib/types/cli/config-writer.d.ts +92 -0
- package/lib/types/cli/config-writer.d.ts.map +1 -0
- package/lib/types/cli/context.d.ts +39 -0
- package/lib/types/cli/context.d.ts.map +1 -0
- package/lib/types/cli/i18n.d.ts +32 -0
- package/lib/types/cli/i18n.d.ts.map +1 -0
- package/lib/types/cli/ipc-client.d.ts +83 -0
- package/lib/types/cli/ipc-client.d.ts.map +1 -0
- package/lib/types/cli/ipc-server.d.ts +113 -0
- package/lib/types/cli/ipc-server.d.ts.map +1 -0
- package/lib/types/cli/main.d.ts +57 -0
- package/lib/types/cli/main.d.ts.map +1 -0
- package/lib/types/cli/qr-terminal.d.ts +17 -0
- package/lib/types/cli/qr-terminal.d.ts.map +1 -0
- package/lib/types/client/RemoteStatusIcon.d.ts.map +1 -1
- package/lib/types/client/current-session-report.d.ts +4 -0
- package/lib/types/client/current-session-report.d.ts.map +1 -1
- package/lib/types/client/effects/model-group-side.d.ts.map +1 -1
- package/lib/types/client/effects/remote-session.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +47 -3
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/settings/SettingsSection.d.ts.map +1 -1
- package/lib/types/client/settings/settings-css.d.ts +1 -1
- package/lib/types/client/settings/settings-css.d.ts.map +1 -1
- package/lib/types/client/styles/remote-session.css.d.ts +1 -1
- package/lib/types/client/styles/remote-session.css.d.ts.map +1 -1
- package/lib/types/client/wizard/GatewayWizard.d.ts.map +1 -1
- package/lib/types/client/wizard/cards/ProxyCard.d.ts.map +1 -1
- package/lib/types/client/wizard/wizard-css.d.ts +1 -1
- package/lib/types/client/wizard/wizard-css.d.ts.map +1 -1
- package/lib/types/client-data/model-group-side.d.ts +23 -2
- package/lib/types/client-data/model-group-side.d.ts.map +1 -1
- package/lib/types/client-data/remote-status.d.ts +47 -1
- package/lib/types/client-data/remote-status.d.ts.map +1 -1
- package/lib/types/client-data/settings-form.d.ts +85 -1
- package/lib/types/client-data/settings-form.d.ts.map +1 -1
- package/lib/types/client-data/wizard.d.ts +56 -1
- package/lib/types/client-data/wizard.d.ts.map +1 -1
- package/lib/types/client-routes.d.ts +25 -1
- package/lib/types/client-routes.d.ts.map +1 -1
- package/lib/types/config.d.ts +40 -14
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/debug-report.d.ts.map +1 -1
- package/lib/types/fetch-route-intercept.d.ts +66 -8
- package/lib/types/fetch-route-intercept.d.ts.map +1 -1
- package/lib/types/index.d.ts +9 -2
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/intercept.d.ts +72 -10
- package/lib/types/intercept.d.ts.map +1 -1
- package/lib/types/merge-streams.d.ts +79 -1
- package/lib/types/merge-streams.d.ts.map +1 -1
- package/lib/types/plugin-route-intercept.d.ts +25 -9
- package/lib/types/plugin-route-intercept.d.ts.map +1 -1
- package/lib/types/relay-access.d.ts +3 -1
- package/lib/types/relay-access.d.ts.map +1 -1
- package/lib/types/relay-client.d.ts +23 -2
- package/lib/types/relay-client.d.ts.map +1 -1
- package/lib/types/relay-pool.d.ts +148 -0
- package/lib/types/relay-pool.d.ts.map +1 -0
- package/lib/types/relay-server.d.ts +30 -3
- package/lib/types/relay-server.d.ts.map +1 -1
- package/lib/types/restart-fields.d.ts +5 -5
- package/lib/types/restart-fields.d.ts.map +1 -1
- package/lib/types/share-ops.d.ts +47 -1
- package/lib/types/share-ops.d.ts.map +1 -1
- package/lib/types/share-store.d.ts.map +1 -1
- package/lib/types/state-store.d.ts +179 -0
- package/lib/types/state-store.d.ts.map +1 -0
- package/lib/types/upload-limits.d.ts +15 -0
- package/lib/types/upload-limits.d.ts.map +1 -1
- package/lib/types/virtual-id.d.ts +21 -0
- package/lib/types/virtual-id.d.ts.map +1 -1
- package/lib/types/wizard/probe.d.ts.map +1 -1
- package/lib/types/wizard/proxy-snippets.d.ts +3 -0
- package/lib/types/wizard/proxy-snippets.d.ts.map +1 -1
- package/lib/types/wizard/routes.d.ts +1 -0
- package/lib/types/wizard/routes.d.ts.map +1 -1
- package/lib/types/wizard/status.d.ts +9 -0
- package/lib/types/wizard/status.d.ts.map +1 -1
- package/lib/types/wizard/verify.d.ts +3 -0
- package/lib/types/wizard/verify.d.ts.map +1 -1
- package/lib/upload-limits.js +15 -0
- package/lib/upload-limits.js.map +1 -1
- package/lib/virtual-id.js +60 -0
- package/lib/virtual-id.js.map +1 -1
- package/lib/wizard/probe.js +46 -1
- package/lib/wizard/probe.js.map +1 -1
- package/lib/wizard/proxy-snippets.js +14 -1
- package/lib/wizard/proxy-snippets.js.map +1 -1
- package/lib/wizard/routes.js +23 -4
- package/lib/wizard/routes.js.map +1 -1
- package/lib/wizard/status.js +21 -0
- package/lib/wizard/status.js.map +1 -1
- package/lib/wizard/verify.js +31 -8
- package/lib/wizard/verify.js.map +1 -1
- package/package.json +8 -4
- package/pwa/icons/badge-96.png +0 -0
- package/pwa/icons/badge.svg +1 -0
- package/pwa/manifest.json +2 -1
- package/pwa/sw.js +3 -1
- package/scripts/check-debug-report.mjs +41 -1
- package/scripts/check-settings-form.mjs +14 -3
- package/scripts/check-settings-view.mjs +247 -0
- package/scripts/check-wizard-ui.mjs +37 -0
- package/src/adapters/dsh-better-sidebar/index.ts +7 -3
- package/src/admin-routes.ts +10 -2
- package/src/cli/args.ts +289 -0
- package/src/cli/bootstrap.ts +569 -0
- package/src/cli/command-setup.ts +540 -0
- package/src/cli/commands-client.ts +369 -0
- package/src/cli/commands-host.ts +1127 -0
- package/src/cli/commands-status.ts +296 -0
- package/src/cli/config-writer.ts +354 -0
- package/src/cli/context.ts +38 -0
- package/src/cli/i18n.ts +645 -0
- package/src/cli/ipc-client.ts +216 -0
- package/src/cli/ipc-server.ts +531 -0
- package/src/cli/main.ts +285 -0
- package/src/cli/qr-terminal.ts +49 -0
- package/src/client/RemoteStatusIcon.tsx +4 -1
- package/src/client/current-session-report.ts +28 -4
- package/src/client/effects/model-group-side.ts +19 -5
- package/src/client/effects/remote-session.ts +26 -6
- package/src/client/locales.ts +96 -6
- package/src/client/settings/SettingsSection.tsx +287 -92
- package/src/client/settings/settings-css.ts +18 -0
- package/src/client/styles/remote-session.css.ts +15 -1
- package/src/client/wizard/GatewayWizard.tsx +470 -109
- package/src/client/wizard/cards/ProxyCard.tsx +2 -1
- package/src/client/wizard/wizard-css.ts +23 -0
- package/src/client-data/model-group-side.ts +37 -10
- package/src/client-data/remote-status.ts +112 -17
- package/src/client-data/settings-form.ts +164 -8
- package/src/client-data/wizard.ts +112 -2
- package/src/client-routes.ts +334 -26
- package/src/config.ts +62 -16
- package/src/debug-report.ts +27 -1
- package/src/fetch-route-intercept.ts +381 -32
- package/src/index.ts +608 -380
- package/src/intercept.ts +1866 -286
- package/src/merge-streams.ts +155 -10
- package/src/plugin-route-intercept.ts +120 -71
- package/src/relay-access.ts +14 -3
- package/src/relay-client.ts +163 -5
- package/src/relay-pool.ts +387 -0
- package/src/relay-server.ts +312 -16
- package/src/restart-fields.ts +6 -4
- package/src/share-ops.ts +82 -1
- package/src/share-store.ts +59 -26
- package/src/state-store.ts +428 -0
- package/src/upload-limits.ts +16 -0
- package/src/virtual-id.ts +57 -0
- package/src/wizard/probe.ts +54 -2
- package/src/wizard/proxy-snippets.ts +17 -2
- package/src/wizard/routes.ts +24 -5
- package/src/wizard/status.ts +28 -0
- package/src/wizard/verify.ts +33 -11
- package/AGENTS.md +0 -185
- package/docs/adr/0001-plugin-bridge-forwards-plugin-routes.md +0 -15
- package/docs/adr/0002-relay-emulates-websocket.md +0 -8
package/src/merge-streams.ts
CHANGED
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
* `zr~<serverId>~provider/model` fallback string (the pre-fix2 display).
|
|
143
143
|
*/
|
|
144
144
|
|
|
145
|
-
import { fromVirtual, isVirtual, toVirtual
|
|
145
|
+
import { encodeServerIdZeroWidth, fromVirtual, isVirtual, toVirtual } from './virtual-id.js'
|
|
146
146
|
|
|
147
147
|
/** The (serverId, serverName) pair a merger virtualizes with. A re-handshake
|
|
148
148
|
* with a different server retargets the SAME merger ({@link retarget}) so the
|
|
@@ -162,12 +162,26 @@ export interface MergerIdentity {
|
|
|
162
162
|
* English forms live in src/client/locales.ts, `remoteGroup*` keys).
|
|
163
163
|
*/
|
|
164
164
|
export type MergerAnnotation = 'none' | 'offline' | 'revoked' | 'unpaired' | 'mismatch'
|
|
165
|
+
export type MergerLang = 'zh' | 'en'
|
|
165
166
|
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
167
|
+
const ANNOTATION_SUFFIX_BY_LANG: Record<MergerLang, Record<Exclude<MergerAnnotation, 'none'>, string>> = {
|
|
168
|
+
zh: {
|
|
169
|
+
offline: '(离线)',
|
|
170
|
+
revoked: '(令牌已吊销)',
|
|
171
|
+
unpaired: '(已解除配对)',
|
|
172
|
+
mismatch: '(版本有差异)',
|
|
173
|
+
},
|
|
174
|
+
en: {
|
|
175
|
+
offline: ' (offline)',
|
|
176
|
+
revoked: ' (token revoked)',
|
|
177
|
+
unpaired: ' (unpaired)',
|
|
178
|
+
mismatch: ' (version mismatch)',
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function resolveMergerLang(lang: MergerLang | (() => MergerLang) | undefined): MergerLang {
|
|
183
|
+
const value = typeof lang === 'function' ? lang() : lang
|
|
184
|
+
return value === 'en' ? 'en' : 'zh'
|
|
171
185
|
}
|
|
172
186
|
|
|
173
187
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
@@ -199,12 +213,13 @@ function virtualizeWorkspace(
|
|
|
199
213
|
record: Record<string, unknown>,
|
|
200
214
|
identity: MergerIdentity,
|
|
201
215
|
annotation: MergerAnnotation,
|
|
216
|
+
lang: MergerLang = 'zh',
|
|
202
217
|
): Record<string, unknown> {
|
|
203
218
|
const virtualize = (id: string): string => toVirtual(identity.serverId, id)
|
|
204
219
|
const out: Record<string, unknown> = { ...record }
|
|
205
220
|
if (typeof record.workspaceId === 'string') out.workspaceId = virtualize(record.workspaceId)
|
|
206
221
|
if (typeof record.title === 'string') {
|
|
207
|
-
const suffix = annotation === 'none' ? '' :
|
|
222
|
+
const suffix = annotation === 'none' ? '' : ANNOTATION_SUFFIX_BY_LANG[lang][annotation]
|
|
208
223
|
out.title = `${identity.serverName} · ${record.title}${suffix}`
|
|
209
224
|
}
|
|
210
225
|
if (Array.isArray(record.sessionIds)) {
|
|
@@ -220,6 +235,9 @@ export interface WorkspaceMergerOptions extends MergerIdentity {
|
|
|
220
235
|
/** T65: the clock the tombstone hide-grace runs on. Default Date.now;
|
|
221
236
|
* tests advance an injectable fake to cross the grace. */
|
|
222
237
|
now?: () => number
|
|
238
|
+
/** Language for status annotation suffixes on group titles (`'zh'` default,
|
|
239
|
+
* `'en'` English). Accepts a getter for live resolution. */
|
|
240
|
+
lang?: MergerLang | (() => MergerLang) | undefined
|
|
223
241
|
/** PB-25b: the known-real-session inventory report — every time the
|
|
224
242
|
* session registry changes, the FULL current set of real ids this merger
|
|
225
243
|
* knows (the sessionHome keys: live ids and tombstones alike, each one the
|
|
@@ -234,6 +252,22 @@ export interface WorkspaceMergerOptions extends MergerIdentity {
|
|
|
234
252
|
onKnownSessions?: (ids: ReadonlySet<string>) => void
|
|
235
253
|
}
|
|
236
254
|
|
|
255
|
+
/** The lists a workspace merger contributes to the MS-3 multi-server
|
|
256
|
+
* composition: the locally observed truth (identical across every slot fed
|
|
257
|
+
* the same local stream) and this server's virtualized remote contribution
|
|
258
|
+
* (shown workspaces in remote order, archived = remote archived + the T58
|
|
259
|
+
* hidden closed sessions, pinned). Snapshots — the caller may mutate them
|
|
260
|
+
* freely; nothing here hands out a live reference. */
|
|
261
|
+
export interface WorkspaceMergerParts {
|
|
262
|
+
local: { orderIds: string[]; archivedIds: string[]; pinnedIds: string[] }
|
|
263
|
+
remote: {
|
|
264
|
+
orderIds: string[]
|
|
265
|
+
archivedIds: string[]
|
|
266
|
+
pinnedIds: string[]
|
|
267
|
+
workspaces: Record<string, unknown>[]
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
237
271
|
export interface WorkspaceMerger {
|
|
238
272
|
readonly serverId: string
|
|
239
273
|
readonly serverName: string
|
|
@@ -287,6 +321,13 @@ export interface WorkspaceMerger {
|
|
|
287
321
|
* for a serverId change the remote state must be gone first (onRemoteGone
|
|
288
322
|
* first); for a rename it may stay. */
|
|
289
323
|
retarget(identity: MergerIdentity): void
|
|
324
|
+
/** MS-3: the composition inputs the multi-server orchestrator builds the
|
|
325
|
+
* canonical merged frames from — the locally observed lists and this
|
|
326
|
+
* server's virtualized remote contribution, as snapshots. Reading the
|
|
327
|
+
* workspaces runs the same {@link forwardVisible} visibility gate the
|
|
328
|
+
* frame paths use (T56/T58), so the parts can never expose a group the
|
|
329
|
+
* frames would withhold. */
|
|
330
|
+
parts(): WorkspaceMergerParts
|
|
290
331
|
}
|
|
291
332
|
|
|
292
333
|
/**
|
|
@@ -518,7 +559,7 @@ export function createWorkspaceMerger(options: WorkspaceMergerOptions): Workspac
|
|
|
518
559
|
* session stays in its group so the host's「显示已归档」filter can show it
|
|
519
560
|
* in place — hiding is purely the archived set. */
|
|
520
561
|
function forwardWorkspace(record: Record<string, unknown>): Record<string, unknown> {
|
|
521
|
-
const out = virtualizeWorkspace(record, identity, annotation)
|
|
562
|
+
const out = virtualizeWorkspace(record, identity, annotation, resolveMergerLang(options.lang))
|
|
522
563
|
const workspaceId = typeof record.workspaceId === 'string' ? record.workspaceId : undefined
|
|
523
564
|
const live = stringList(record.sessionIds)
|
|
524
565
|
if (workspaceId === undefined || !Array.isArray(out.sessionIds)) return out
|
|
@@ -627,6 +668,22 @@ export function createWorkspaceMerger(options: WorkspaceMergerOptions): Workspac
|
|
|
627
668
|
identity = { serverId: next.serverId, serverName: next.serverName }
|
|
628
669
|
},
|
|
629
670
|
|
|
671
|
+
parts(): WorkspaceMergerParts {
|
|
672
|
+
return {
|
|
673
|
+
local: {
|
|
674
|
+
orderIds: [...localOrder],
|
|
675
|
+
archivedIds: [...localArchived],
|
|
676
|
+
pinnedIds: [...localPinned],
|
|
677
|
+
},
|
|
678
|
+
remote: {
|
|
679
|
+
orderIds: remoteIds(),
|
|
680
|
+
archivedIds: [...remoteArchivedVirtual(), ...[...hiddenSessions].map(virtualize)],
|
|
681
|
+
pinnedIds: remotePinnedVirtual(),
|
|
682
|
+
workspaces: virtualWorkspaces(),
|
|
683
|
+
},
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
|
|
630
687
|
onLocal(frame: unknown): unknown[] {
|
|
631
688
|
if (!isPlainObject(frame)) return [frame]
|
|
632
689
|
observeLocal(frame)
|
|
@@ -1243,7 +1300,9 @@ export function mergeSessionList(localResult: unknown, remoteResult: unknown, se
|
|
|
1243
1300
|
* order, RT dsh-client-ui-model-selection lib/client.js:510 — appended
|
|
1244
1301
|
* groups render last); each group id becomes the virtual group id
|
|
1245
1302
|
* `zr~<serverId>~<original>` and its name is prefixed
|
|
1246
|
-
* `${
|
|
1303
|
+
* `${encodeServerIdZeroWidth(serverId)}${serverName} · ${name}` (MS-4: the
|
|
1304
|
+
* WORD JOINER mark plus the serverId as 32 zero-width bits, so the browser
|
|
1305
|
+
* can hide OTHER paired servers' groups too) — the same workspace
|
|
1247
1306
|
* title discipline the merger uses, PLUS the invisible WORD JOINER (T74):
|
|
1248
1307
|
* the model menu's groups stopped carrying their id in the markup (0.2.0's
|
|
1249
1308
|
* MenuGroup renders `aria-labelledby={useId()}` only), so the browser
|
|
@@ -1289,7 +1348,7 @@ export function mergeModelCatalogs(localValue: unknown, remoteValue: unknown, id
|
|
|
1289
1348
|
...group,
|
|
1290
1349
|
id,
|
|
1291
1350
|
...(typeof group.name === 'string'
|
|
1292
|
-
? { name: `${
|
|
1351
|
+
? { name: `${encodeServerIdZeroWidth(identity.serverId)}${identity.serverName} · ${group.name}` }
|
|
1293
1352
|
: {}),
|
|
1294
1353
|
})
|
|
1295
1354
|
}
|
|
@@ -1401,3 +1460,89 @@ export function sessionSummaryAddedFrames(remoteResult: unknown, serverId: strin
|
|
|
1401
1460
|
}
|
|
1402
1461
|
return frames
|
|
1403
1462
|
}
|
|
1463
|
+
|
|
1464
|
+
// ---- MS-3: multi-server composition -----------------------------------------
|
|
1465
|
+
|
|
1466
|
+
/** The cumulative LOCAL lists one workspace merger's `local` side holds. */
|
|
1467
|
+
export interface LocalWorkspaceLists {
|
|
1468
|
+
orderIds: string[]
|
|
1469
|
+
archivedIds: string[]
|
|
1470
|
+
pinnedIds: string[]
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
/**
|
|
1474
|
+
* MS-3: fold one LOCAL `workspace/follow` frame into the orchestrator's
|
|
1475
|
+
* cumulative local lists — the baseline replaces all three, `order` /
|
|
1476
|
+
* `archived` / `pinned` each replace their own list, and every other frame
|
|
1477
|
+
* (upsert / remove / junk) observes nothing. The parsing is the merger's own
|
|
1478
|
+
* `observeLocal` reading, extracted so the multi-server orchestrator keeps
|
|
1479
|
+
* the same truth WITHOUT a second shape definition drifting away from it.
|
|
1480
|
+
*/
|
|
1481
|
+
export function localWorkspaceLists(frame: unknown, current: LocalWorkspaceLists): LocalWorkspaceLists {
|
|
1482
|
+
if (!isPlainObject(frame)) return current
|
|
1483
|
+
if (frame.type === 'baseline') {
|
|
1484
|
+
const value = isPlainObject(frame.value) ? frame.value : {}
|
|
1485
|
+
return {
|
|
1486
|
+
orderIds: idList(value.items),
|
|
1487
|
+
archivedIds: stringList(value.archivedSessionIds),
|
|
1488
|
+
pinnedIds: stringList(value.pinnedSessionIds),
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
if (frame.type === 'order') return { ...current, orderIds: stringList(frame.workspaceIds) }
|
|
1492
|
+
if (frame.type === 'archived') return { ...current, archivedIds: stringList(frame.archivedSessionIds) }
|
|
1493
|
+
if (frame.type === 'pinned') return { ...current, pinnedIds: stringList(frame.pinnedSessionIds) }
|
|
1494
|
+
return current
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
/** One slot's remote contribution for {@link composeMergedWorkspaceFrames}
|
|
1498
|
+
* — the `remote` half of {@link WorkspaceMergerParts}. */
|
|
1499
|
+
export interface RemoteWorkspaceContribution {
|
|
1500
|
+
orderIds: string[]
|
|
1501
|
+
archivedIds: string[]
|
|
1502
|
+
pinnedIds: string[]
|
|
1503
|
+
workspaces: Record<string, unknown>[]
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
/**
|
|
1507
|
+
* MS-3: the canonical merged frames over N servers — local first, then each
|
|
1508
|
+
* server's contribution in the given (pairing) order. The order list is
|
|
1509
|
+
* plain concatenation, the archived set is DEDUPLICATED in that order (the
|
|
1510
|
+
* single-server merger dedupes the same way), and pinned concatenates (the
|
|
1511
|
+
* single-server pinned frame never deduped either). With no remotes the
|
|
1512
|
+
* frames collapse to the local-only form.
|
|
1513
|
+
*/
|
|
1514
|
+
export function composeMergedWorkspaceFrames(
|
|
1515
|
+
local: LocalWorkspaceLists,
|
|
1516
|
+
remotes: readonly RemoteWorkspaceContribution[],
|
|
1517
|
+
): {
|
|
1518
|
+
order: { type: 'order'; workspaceIds: string[] }
|
|
1519
|
+
archived: { type: 'archived'; archivedSessionIds: string[] }
|
|
1520
|
+
pinned: { type: 'pinned'; pinnedSessionIds: string[] }
|
|
1521
|
+
workspaces: Record<string, unknown>[]
|
|
1522
|
+
} {
|
|
1523
|
+
const orderIds = [...local.orderIds]
|
|
1524
|
+
const archived: string[] = []
|
|
1525
|
+
const seenArchived = new Set<string>()
|
|
1526
|
+
const addArchived = (ids: readonly string[]): void => {
|
|
1527
|
+
for (const id of ids) {
|
|
1528
|
+
if (seenArchived.has(id)) continue
|
|
1529
|
+
seenArchived.add(id)
|
|
1530
|
+
archived.push(id)
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
addArchived(local.archivedIds)
|
|
1534
|
+
const pinnedIds = [...local.pinnedIds]
|
|
1535
|
+
const workspaces: Record<string, unknown>[] = []
|
|
1536
|
+
for (const remote of remotes) {
|
|
1537
|
+
orderIds.push(...remote.orderIds)
|
|
1538
|
+
addArchived(remote.archivedIds)
|
|
1539
|
+
pinnedIds.push(...remote.pinnedIds)
|
|
1540
|
+
workspaces.push(...remote.workspaces)
|
|
1541
|
+
}
|
|
1542
|
+
return {
|
|
1543
|
+
order: { type: 'order', workspaceIds: orderIds },
|
|
1544
|
+
archived: { type: 'archived', archivedSessionIds: archived },
|
|
1545
|
+
pinned: { type: 'pinned', pinnedSessionIds: pinnedIds },
|
|
1546
|
+
workspaces,
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
@@ -340,15 +340,19 @@ export interface InstallPluginRouteInterceptOptions {
|
|
|
340
340
|
webServer: object
|
|
341
341
|
/** The local declaration registry (the `zenRemote` service's table). */
|
|
342
342
|
registry: BridgeRegistry
|
|
343
|
-
/**
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
*
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
*
|
|
351
|
-
|
|
343
|
+
/** SINGLE-SERVER mode: the client relay client; `pluginRequest` is the
|
|
344
|
+
* forwarding door. Required together with {@link getServerId} and
|
|
345
|
+
* {@link getStatus} unless the MS-4 per-server faces below are given
|
|
346
|
+
* (MS-5's pool wiring). */
|
|
347
|
+
relay?: RelayClient
|
|
348
|
+
/** SINGLE-SERVER mode: the CURRENT handshake's server id, read live per
|
|
349
|
+
* call; a virtual id minted for another server (or before any handshake)
|
|
350
|
+
* refuses `server-changed`. */
|
|
351
|
+
getServerId?: () => string | undefined
|
|
352
|
+
/** SINGLE-SERVER mode: the PB-2 bridge verdicts, computed live per call; a
|
|
353
|
+
* plugin that is not bridged right now refuses with the status's own
|
|
354
|
+
* reason. */
|
|
355
|
+
getStatus?: () => BridgeStatus[]
|
|
352
356
|
/** The validated adapters (PB-11), as the wiring registered them.
|
|
353
357
|
* Absent/empty — the pluggable default — means no adapter routes exist
|
|
354
358
|
* and this interception behaves exactly as before. */
|
|
@@ -374,6 +378,18 @@ export interface InstallPluginRouteInterceptOptions {
|
|
|
374
378
|
* consumption: nothing here mutates merge state. Absent → the scan is
|
|
375
379
|
* exactly the request-local PB-22 behavior. */
|
|
376
380
|
getKnownRealIds?: () => ReadonlySet<string>
|
|
381
|
+
/** MS-4: the per-server faces of the connection pool. A request whose
|
|
382
|
+
* located ids are `zr~<serverId>~…` is validated, forwarded and
|
|
383
|
+
* response-rewritten against THAT server alone: its relay client
|
|
384
|
+
* (`undefined` — a server the pool does not hold — refuses
|
|
385
|
+
* `server-changed`), its bridge verdicts and its known-real-id inventory.
|
|
386
|
+
* Each is optional and falls back on its own to the single-server face
|
|
387
|
+
* above: no `getRelayForServer` → `relay` exactly when the id belongs to
|
|
388
|
+
* `getServerId()`; no `getStatusForServer` → `getStatus()`; no
|
|
389
|
+
* `getKnownRealIdsForServer` → `getKnownRealIds()`. All read live. */
|
|
390
|
+
getRelayForServer?: (serverId: string) => RelayClient | undefined
|
|
391
|
+
getStatusForServer?: (serverId: string) => BridgeStatus[]
|
|
392
|
+
getKnownRealIdsForServer?: (serverId: string) => ReadonlySet<string>
|
|
377
393
|
/** PB-26a: called with the virtual session ids a response rewrite
|
|
378
394
|
* PRODUCED — a `rewriteIds.fields` route's wrapped positions, or the ids
|
|
379
395
|
* an adapter's `client.response` hook passed through `virtualize` (e.g.
|
|
@@ -485,11 +501,55 @@ export function installPluginRouteIntercept(options: InstallPluginRouteIntercept
|
|
|
485
501
|
},
|
|
486
502
|
}
|
|
487
503
|
}
|
|
488
|
-
const { webServer, registry,
|
|
504
|
+
const { webServer, registry, log } = options
|
|
489
505
|
const adapters = options.adapters ?? []
|
|
490
506
|
const getCurrentSession = options.getCurrentSession
|
|
491
507
|
const getKnownRealIds = options.getKnownRealIds
|
|
492
508
|
const onMintedVirtualIds = options.onMintedVirtualIds
|
|
509
|
+
const { relay, getServerId, getStatus } = options
|
|
510
|
+
const { getRelayForServer, getStatusForServer, getKnownRealIdsForServer } = options
|
|
511
|
+
// Exactly one mode, like installIntercept: the pool faces or the single
|
|
512
|
+
// row client — a wiring that passes neither would refuse every forwarded
|
|
513
|
+
// request as server-changed / not-bridged, so it is refused here instead.
|
|
514
|
+
if (
|
|
515
|
+
(getRelayForServer === undefined && (relay === undefined || getServerId === undefined))
|
|
516
|
+
|| (getStatusForServer === undefined && getStatus === undefined)
|
|
517
|
+
) {
|
|
518
|
+
throw new Error(
|
|
519
|
+
'installPluginRouteIntercept requires either `relay` + `getServerId` + `getStatus` (single server) or `getRelayForServer` + `getStatusForServer` (pool)',
|
|
520
|
+
)
|
|
521
|
+
}
|
|
522
|
+
// The single-server facts, proven present by the guard above.
|
|
523
|
+
const singleRelay = relay as RelayClient
|
|
524
|
+
const singleGetServerId = getServerId as () => string | undefined
|
|
525
|
+
const singleGetStatus = getStatus as () => BridgeStatus[]
|
|
526
|
+
/** MS-4: the relay client serving one server id, or `undefined` (the
|
|
527
|
+
* `server-changed` refusal). */
|
|
528
|
+
const relayOfServer = (serverId: string): RelayClient | undefined =>
|
|
529
|
+
getRelayForServer !== undefined ? getRelayForServer(serverId) : serverId === singleGetServerId() ? singleRelay : undefined
|
|
530
|
+
const statusesOfServer = (serverId: string): BridgeStatus[] =>
|
|
531
|
+
getStatusForServer !== undefined ? getStatusForServer(serverId) : singleGetStatus()
|
|
532
|
+
const knownRealIdsOfServer = (serverId: string): ReadonlySet<string> | undefined =>
|
|
533
|
+
getKnownRealIdsForServer !== undefined ? getKnownRealIdsForServer(serverId) : getKnownRealIds?.()
|
|
534
|
+
/** MS-4: the one server every located virtual id of a request belongs to,
|
|
535
|
+
* with its relay client — or the refusal reason, judged id by id in hit
|
|
536
|
+
* order (the single-server walls' order). A request naming ids of two
|
|
537
|
+
* servers is refused like a foreign one: it has no single home. */
|
|
538
|
+
const serverOfHits = (hits: readonly AdapterIdHit[]): { serverId: string; relay: RelayClient } | 'bad-query' | 'server-changed' => {
|
|
539
|
+
let located: { serverId: string; relay: RelayClient } | undefined
|
|
540
|
+
for (const idHit of hits) {
|
|
541
|
+
const parts = fromVirtual(idHit.value)
|
|
542
|
+
if (parts === undefined || parts.id === '') return 'bad-query'
|
|
543
|
+
if (located === undefined) {
|
|
544
|
+
const target = relayOfServer(parts.serverId)
|
|
545
|
+
if (target === undefined) return 'server-changed'
|
|
546
|
+
located = { serverId: parts.serverId, relay: target }
|
|
547
|
+
} else if (parts.serverId !== located.serverId) {
|
|
548
|
+
return 'server-changed'
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return located ?? 'bad-query'
|
|
552
|
+
}
|
|
493
553
|
const target = webServer as { match: (this: unknown, pathname: string) => unknown }
|
|
494
554
|
const savedMatch = target.match
|
|
495
555
|
|
|
@@ -661,7 +721,8 @@ export function installPluginRouteIntercept(options: InstallPluginRouteIntercept
|
|
|
661
721
|
answerUnbridged(res, declared.plugin, 'bad-query')
|
|
662
722
|
return
|
|
663
723
|
}
|
|
664
|
-
|
|
724
|
+
const targetRelay = relayOfServer(parts.serverId)
|
|
725
|
+
if (targetRelay === undefined) {
|
|
665
726
|
answerUnbridged(res, declared.plugin, 'server-changed')
|
|
666
727
|
return
|
|
667
728
|
}
|
|
@@ -687,7 +748,7 @@ export function installPluginRouteIntercept(options: InstallPluginRouteIntercept
|
|
|
687
748
|
log?.('plugin request for %s refused locally: body-too-large (%s declared)', declared.path, req.headers['content-length'])
|
|
688
749
|
return
|
|
689
750
|
}
|
|
690
|
-
const status =
|
|
751
|
+
const status = statusesOfServer(parts.serverId).find((entry) => entry.plugin === declared.plugin)
|
|
691
752
|
if (status === undefined || !status.bridged) {
|
|
692
753
|
answerUnbridged(res, declared.plugin, status?.reason ?? 'not-on-server')
|
|
693
754
|
return
|
|
@@ -757,7 +818,7 @@ export function installPluginRouteIntercept(options: InstallPluginRouteIntercept
|
|
|
757
818
|
bodyStream = Readable.toWeb(gate) as ReadableStream<Uint8Array>
|
|
758
819
|
req.pipe(gate)
|
|
759
820
|
}
|
|
760
|
-
const response = await
|
|
821
|
+
const response = await targetRelay.pluginRequest(
|
|
761
822
|
{
|
|
762
823
|
plugin: declared.plugin,
|
|
763
824
|
version: declared.version,
|
|
@@ -794,7 +855,7 @@ export function installPluginRouteIntercept(options: InstallPluginRouteIntercept
|
|
|
794
855
|
// known inventory, read live right here (undefined when the wiring
|
|
795
856
|
// passed no getter — the scan stays request-local).
|
|
796
857
|
if (declared.rewriteIds) {
|
|
797
|
-
await answerRewriteIds(res, response, idMapping, declared.rewriteFields, parts.serverId,
|
|
858
|
+
await answerRewriteIds(res, response, idMapping, declared.rewriteFields, parts.serverId, knownRealIdsOfServer(parts.serverId), onMintedVirtualIds)
|
|
798
859
|
return
|
|
799
860
|
}
|
|
800
861
|
const contentType = response.headers.get('content-type')
|
|
@@ -989,7 +1050,6 @@ export function installPluginRouteIntercept(options: InstallPluginRouteIntercept
|
|
|
989
1050
|
answerUnbridged(res, plugin, 'method-not-bridged')
|
|
990
1051
|
return
|
|
991
1052
|
}
|
|
992
|
-
const serverId = getServerId()
|
|
993
1053
|
// PB-22: the request's own real→virtual pairs, collected while the
|
|
994
1054
|
// wall validates each hit — the READ-ONLY mapping a rewriteIds
|
|
995
1055
|
// route's answer scan consults (`zr~` carries the real id inside, so
|
|
@@ -1001,24 +1061,19 @@ export function installPluginRouteIntercept(options: InstallPluginRouteIntercept
|
|
|
1001
1061
|
// ONE more source beside this mapping — the known-real-session
|
|
1002
1062
|
// inventory, read live at the answer (never copied here) and
|
|
1003
1063
|
// consumed read-only; nothing here writes any session table.
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
return
|
|
1011
|
-
}
|
|
1012
|
-
if (parts.serverId !== serverId) {
|
|
1013
|
-
answerUnbridged(res, plugin, 'server-changed')
|
|
1014
|
-
return
|
|
1015
|
-
}
|
|
1016
|
-
idMapping.set(parts.id, idHit.value)
|
|
1017
|
-
locatedServerId = parts.serverId
|
|
1064
|
+
// MS-4: every located id must name ONE server the wiring serves;
|
|
1065
|
+
// that server's relay, verdicts and inventory carry the rest.
|
|
1066
|
+
const home = serverOfHits(verdict.hits)
|
|
1067
|
+
if (home === 'bad-query' || home === 'server-changed') {
|
|
1068
|
+
answerUnbridged(res, plugin, home)
|
|
1069
|
+
return
|
|
1018
1070
|
}
|
|
1071
|
+
const locatedServerId = home.serverId
|
|
1072
|
+
const idMapping = new Map<string, string>()
|
|
1073
|
+
for (const idHit of verdict.hits) idMapping.set(fromVirtual(idHit.value)!.id, idHit.value)
|
|
1019
1074
|
// The bridge verdicts judged from the SAME live table the query
|
|
1020
1075
|
// half reads.
|
|
1021
|
-
const status =
|
|
1076
|
+
const status = statusesOfServer(locatedServerId).find((entry) => entry.plugin === plugin)
|
|
1022
1077
|
if (status === undefined || !status.bridged) {
|
|
1023
1078
|
answerUnbridged(res, plugin, status?.reason ?? 'not-on-server')
|
|
1024
1079
|
return
|
|
@@ -1078,7 +1133,7 @@ export function installPluginRouteIntercept(options: InstallPluginRouteIntercept
|
|
|
1078
1133
|
req.pipe(gate)
|
|
1079
1134
|
if (Number.isFinite(declaredLength) && declaredLength > 0) contentLength = declaredLength
|
|
1080
1135
|
}
|
|
1081
|
-
const response = await relay.pluginRequest(
|
|
1136
|
+
const response = await home.relay.pluginRequest(
|
|
1082
1137
|
{
|
|
1083
1138
|
plugin,
|
|
1084
1139
|
version: declared.version,
|
|
@@ -1104,7 +1159,7 @@ export function installPluginRouteIntercept(options: InstallPluginRouteIntercept
|
|
|
1104
1159
|
return
|
|
1105
1160
|
}
|
|
1106
1161
|
if (declared.rewriteIds) {
|
|
1107
|
-
await answerRewriteIds(res, response, idMapping, declared.rewriteFields, locatedServerId
|
|
1162
|
+
await answerRewriteIds(res, response, idMapping, declared.rewriteFields, locatedServerId, knownRealIdsOfServer(locatedServerId), onMintedVirtualIds)
|
|
1108
1163
|
return
|
|
1109
1164
|
}
|
|
1110
1165
|
const contentType = response.headers.get('content-type')
|
|
@@ -1300,23 +1355,18 @@ export function installPluginRouteIntercept(options: InstallPluginRouteIntercept
|
|
|
1300
1355
|
answerUnbridged(res, plugin, 'method-not-bridged')
|
|
1301
1356
|
return
|
|
1302
1357
|
}
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
return
|
|
1309
|
-
}
|
|
1310
|
-
if (parts.serverId !== serverId) {
|
|
1311
|
-
answerUnbridged(res, plugin, 'server-changed')
|
|
1312
|
-
return
|
|
1313
|
-
}
|
|
1358
|
+
// MS-4: one home server for every located id.
|
|
1359
|
+
const home = serverOfHits(verdict.hits)
|
|
1360
|
+
if (home === 'bad-query' || home === 'server-changed') {
|
|
1361
|
+
answerUnbridged(res, plugin, home)
|
|
1362
|
+
return
|
|
1314
1363
|
}
|
|
1364
|
+
const serverId = home.serverId
|
|
1315
1365
|
// The bridge verdicts (version skew, consent, offline…) judged from
|
|
1316
1366
|
// the SAME live table the cooperative surrogate reads — the
|
|
1317
1367
|
// adapter's registry entry makes the plugin indistinguishable from
|
|
1318
1368
|
// a cooperative one here.
|
|
1319
|
-
const status =
|
|
1369
|
+
const status = statusesOfServer(serverId).find((entry) => entry.plugin === plugin)
|
|
1320
1370
|
if (status === undefined || !status.bridged) {
|
|
1321
1371
|
answerUnbridged(res, plugin, status?.reason ?? 'not-on-server')
|
|
1322
1372
|
return
|
|
@@ -1375,7 +1425,7 @@ export function installPluginRouteIntercept(options: InstallPluginRouteIntercept
|
|
|
1375
1425
|
bodyStream = Readable.toWeb(gate) as ReadableStream<Uint8Array>
|
|
1376
1426
|
req.pipe(gate)
|
|
1377
1427
|
}
|
|
1378
|
-
const response = await relay.pluginRequest(
|
|
1428
|
+
const response = await home.relay.pluginRequest(
|
|
1379
1429
|
{
|
|
1380
1430
|
plugin,
|
|
1381
1431
|
version: adapter.declaration.version,
|
|
@@ -1480,7 +1530,7 @@ export function installPluginRouteIntercept(options: InstallPluginRouteIntercept
|
|
|
1480
1530
|
pathname,
|
|
1481
1531
|
body: JSON.parse(text),
|
|
1482
1532
|
virtualize: (id: string) => {
|
|
1483
|
-
const virtualId = toVirtual(serverId
|
|
1533
|
+
const virtualId = toVirtual(serverId, id)
|
|
1484
1534
|
if (isVirtual(virtualId)) minted.add(virtualId)
|
|
1485
1535
|
return virtualId
|
|
1486
1536
|
},
|
|
@@ -1744,6 +1794,9 @@ export function installPluginRouteIntercept(options: InstallPluginRouteIntercept
|
|
|
1744
1794
|
|
|
1745
1795
|
// ---- the remote pipe: the PB-10 tunnel, frame by frame -------------
|
|
1746
1796
|
let tunnelHandle: { tunnel: string; frames: AsyncIterable<Uint8Array> } | undefined
|
|
1797
|
+
// MS-4: the server the tunnel was opened on — set together with
|
|
1798
|
+
// tunnelHandle, the relay every later send and the close travel.
|
|
1799
|
+
let tunnelServer: { serverId: string; relay: RelayClient } | undefined
|
|
1747
1800
|
// The connection-wide real→virtual mapping the rewriteIds deep scan
|
|
1748
1801
|
// consults: EVERY validated remote frame's ids merge in (a watch list
|
|
1749
1802
|
// that grows mid-connection makes the later answers rewritable too —
|
|
@@ -1764,7 +1817,7 @@ export function installPluginRouteIntercept(options: InstallPluginRouteIntercept
|
|
|
1764
1817
|
* never a silent broken flag: that would leak `tunnelsCurrent` and
|
|
1765
1818
|
* leave the browser hanging on a dead tunnel. */
|
|
1766
1819
|
const sendTextUpstream = async (frame: Rfc6455Frame, parsedJson: unknown, hits: AdapterIdHit[]): Promise<void> => {
|
|
1767
|
-
if (upstreamBroken || tunnelHandle === undefined) return
|
|
1820
|
+
if (upstreamBroken || tunnelHandle === undefined || tunnelServer === undefined) return
|
|
1768
1821
|
const rewrite = hits.length > 0
|
|
1769
1822
|
? rewriteAdapterRequest(declared.view, declared.path, new URLSearchParams(), parsedJson, hits, (virtualId) => fromVirtual(virtualId)!.id)
|
|
1770
1823
|
: undefined
|
|
@@ -1772,7 +1825,7 @@ export function installPluginRouteIntercept(options: InstallPluginRouteIntercept
|
|
|
1772
1825
|
const seq = upstreamSeq
|
|
1773
1826
|
upstreamSeq += 1
|
|
1774
1827
|
try {
|
|
1775
|
-
await relay.pluginTunnelSend(tunnelHandle.tunnel, seq, encodeRfc6455Frame(0x1, payload, true, frame.fin))
|
|
1828
|
+
await tunnelServer.relay.pluginTunnelSend(tunnelHandle.tunnel, seq, encodeRfc6455Frame(0x1, payload, true, frame.fin))
|
|
1776
1829
|
} catch {
|
|
1777
1830
|
killExchange()
|
|
1778
1831
|
}
|
|
@@ -1819,7 +1872,7 @@ export function installPluginRouteIntercept(options: InstallPluginRouteIntercept
|
|
|
1819
1872
|
})), 'utf8')
|
|
1820
1873
|
hooked = true
|
|
1821
1874
|
} else if (declared.rewriteIds) {
|
|
1822
|
-
const scanned = deepRewriteIds(parsedJson, idMapping,
|
|
1875
|
+
const scanned = deepRewriteIds(parsedJson, idMapping, knownRealIdsOfServer(serverId), serverId)
|
|
1823
1876
|
if (declared.rewriteFields !== undefined) rewriteIdsAtFields(scanned, declared.rewriteFields, serverId)
|
|
1824
1877
|
outPayload = Buffer.from(JSON.stringify(scanned), 'utf8')
|
|
1825
1878
|
hooked = true
|
|
@@ -1917,24 +1970,18 @@ export function installPluginRouteIntercept(options: InstallPluginRouteIntercept
|
|
|
1917
1970
|
}
|
|
1918
1971
|
return
|
|
1919
1972
|
}
|
|
1920
|
-
|
|
1921
|
-
|
|
1973
|
+
// MS-4: the frame's ids name one served server, and once the tunnel
|
|
1974
|
+
// is open every later frame must name the tunnel's own server.
|
|
1975
|
+
const home = serverOfHits(verdict.hits)
|
|
1976
|
+
if (home === 'bad-query' || home === 'server-changed' || (tunnelServer !== undefined && home.serverId !== tunnelServer.serverId)) {
|
|
1922
1977
|
refuseAndDie('server-changed')
|
|
1923
1978
|
return
|
|
1924
1979
|
}
|
|
1925
|
-
const
|
|
1926
|
-
for (const idHit of verdict.hits) {
|
|
1927
|
-
const parts = fromVirtual(idHit.value)
|
|
1928
|
-
if (parts === undefined || parts.id === '' || parts.serverId !== serverId) {
|
|
1929
|
-
refuseAndDie('server-changed')
|
|
1930
|
-
return
|
|
1931
|
-
}
|
|
1932
|
-
mapping.set(parts.id, idHit.value)
|
|
1933
|
-
}
|
|
1980
|
+
const serverId = home.serverId
|
|
1934
1981
|
// This frame's ids join the connection-wide mapping (see idMapping).
|
|
1935
|
-
for (const
|
|
1982
|
+
for (const idHit of verdict.hits) idMapping.set(fromVirtual(idHit.value)!.id, idHit.value)
|
|
1936
1983
|
if (tunnelHandle === undefined) {
|
|
1937
|
-
const status =
|
|
1984
|
+
const status = statusesOfServer(serverId).find((entry) => entry.plugin === plugin)
|
|
1938
1985
|
if (status === undefined || !status.bridged) {
|
|
1939
1986
|
refuseAndDie(status?.reason ?? 'not-on-server')
|
|
1940
1987
|
return
|
|
@@ -1944,7 +1991,7 @@ export function installPluginRouteIntercept(options: InstallPluginRouteIntercept
|
|
|
1944
1991
|
let opened: { tunnel: string; frames: AsyncIterable<Uint8Array> }
|
|
1945
1992
|
try {
|
|
1946
1993
|
log?.('opening tunnel for %s', declared.path)
|
|
1947
|
-
opened = await relay.pluginTunnelOpen(
|
|
1994
|
+
opened = await home.relay.pluginTunnelOpen(
|
|
1948
1995
|
{
|
|
1949
1996
|
plugin,
|
|
1950
1997
|
version: declared.version,
|
|
@@ -1962,6 +2009,7 @@ export function installPluginRouteIntercept(options: InstallPluginRouteIntercept
|
|
|
1962
2009
|
}
|
|
1963
2010
|
tunnelsCurrent += 1
|
|
1964
2011
|
tunnelsOpened += 1
|
|
2012
|
+
tunnelServer = home
|
|
1965
2013
|
tunnelHandle = opened
|
|
1966
2014
|
log?.('tunnel open for %s: %s', declared.path, opened.tunnel)
|
|
1967
2015
|
teardownTunnel = (): void => {
|
|
@@ -1969,7 +2017,7 @@ export function installPluginRouteIntercept(options: InstallPluginRouteIntercept
|
|
|
1969
2017
|
upstreamBroken = true
|
|
1970
2018
|
tunnelsCurrent -= 1
|
|
1971
2019
|
tunnelsClosed += 1
|
|
1972
|
-
void relay.pluginTunnelClose(opened.tunnel).catch(() => {})
|
|
2020
|
+
void home.relay.pluginTunnelClose(opened.tunnel).catch(() => {})
|
|
1973
2021
|
}
|
|
1974
2022
|
socket.on('close', teardownTunnel)
|
|
1975
2023
|
socket.on('error', teardownTunnel)
|
|
@@ -1988,11 +2036,11 @@ export function installPluginRouteIntercept(options: InstallPluginRouteIntercept
|
|
|
1988
2036
|
localSocket.push(frame.raw)
|
|
1989
2037
|
return
|
|
1990
2038
|
}
|
|
1991
|
-
if (tunnelHandle !== undefined && !upstreamBroken) {
|
|
2039
|
+
if (tunnelHandle !== undefined && tunnelServer !== undefined && !upstreamBroken) {
|
|
1992
2040
|
const seq = upstreamSeq
|
|
1993
2041
|
upstreamSeq += 1
|
|
1994
2042
|
try {
|
|
1995
|
-
await relay.pluginTunnelSend(tunnelHandle.tunnel, seq, frame.raw)
|
|
2043
|
+
await tunnelServer.relay.pluginTunnelSend(tunnelHandle.tunnel, seq, frame.raw)
|
|
1996
2044
|
} catch {
|
|
1997
2045
|
// The send died: tear the exchange down COMPLETELY — the
|
|
1998
2046
|
// counter, the close, the browser socket (a bare broken flag
|
|
@@ -2100,7 +2148,8 @@ export function installPluginRouteIntercept(options: InstallPluginRouteIntercept
|
|
|
2100
2148
|
answerUnbridgedSocket(socket, declared.plugin, 'bad-query')
|
|
2101
2149
|
return
|
|
2102
2150
|
}
|
|
2103
|
-
|
|
2151
|
+
const tunnelRelay = relayOfServer(parts.serverId)
|
|
2152
|
+
if (tunnelRelay === undefined) {
|
|
2104
2153
|
answerUnbridgedSocket(socket, declared.plugin, 'server-changed')
|
|
2105
2154
|
return
|
|
2106
2155
|
}
|
|
@@ -2108,7 +2157,7 @@ export function installPluginRouteIntercept(options: InstallPluginRouteIntercept
|
|
|
2108
2157
|
// the tunnel opens only for a plugin bridged RIGHT NOW — an absent
|
|
2109
2158
|
// or unbridged status refuses with its own reason, and the local
|
|
2110
2159
|
// plugin handler is never reached.
|
|
2111
|
-
const bridgeStatus =
|
|
2160
|
+
const bridgeStatus = statusesOfServer(parts.serverId).find((entry) => entry.plugin === declared.plugin)
|
|
2112
2161
|
if (bridgeStatus === undefined || !bridgeStatus.bridged) {
|
|
2113
2162
|
answerUnbridgedSocket(socket, declared.plugin, bridgeStatus?.reason ?? 'not-on-server')
|
|
2114
2163
|
return
|
|
@@ -2129,7 +2178,7 @@ export function installPluginRouteIntercept(options: InstallPluginRouteIntercept
|
|
|
2129
2178
|
let tunnel: { tunnel: string; frames: AsyncIterable<Uint8Array> }
|
|
2130
2179
|
try {
|
|
2131
2180
|
log?.('opening tunnel for %s', declared.path)
|
|
2132
|
-
tunnel = await
|
|
2181
|
+
tunnel = await tunnelRelay.pluginTunnelOpen(
|
|
2133
2182
|
{
|
|
2134
2183
|
plugin: declared.plugin,
|
|
2135
2184
|
version: declared.version,
|
|
@@ -2207,7 +2256,7 @@ export function installPluginRouteIntercept(options: InstallPluginRouteIntercept
|
|
|
2207
2256
|
pending = pending.then(async () => {
|
|
2208
2257
|
if (upstreamBroken) return
|
|
2209
2258
|
try {
|
|
2210
|
-
await
|
|
2259
|
+
await tunnelRelay.pluginTunnelSend(tunnel.tunnel, seq, bytes, controller.signal)
|
|
2211
2260
|
} catch {
|
|
2212
2261
|
upstreamBroken = true
|
|
2213
2262
|
}
|
|
@@ -2218,7 +2267,7 @@ export function installPluginRouteIntercept(options: InstallPluginRouteIntercept
|
|
|
2218
2267
|
upstreamBroken = true
|
|
2219
2268
|
tunnelsCurrent -= 1
|
|
2220
2269
|
tunnelsClosed += 1
|
|
2221
|
-
void
|
|
2270
|
+
void tunnelRelay.pluginTunnelClose(tunnel.tunnel).catch(() => {})
|
|
2222
2271
|
}
|
|
2223
2272
|
socket.on('close', teardownTunnel)
|
|
2224
2273
|
socket.on('error', teardownTunnel)
|