dsh-zen-remote 2.0.3 → 2.2.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/AGENTS.md +15 -1
- package/README.md +11 -5
- package/README.zh-CN.md +11 -5
- package/docs/adr/0001-plugin-bridge-forwards-plugin-routes.md +15 -0
- package/docs/adr/0002-relay-emulates-websocket.md +8 -0
- package/docs/interface.md +3 -1
- package/docs/plugin-bridge.md +140 -0
- package/docs/remote-access.en.md +57 -0
- package/docs/remote-access.md +57 -0
- package/lib/activity.js +25 -0
- package/lib/activity.js.map +1 -1
- package/lib/adapters/dsh-better-sidebar/index.js +144 -0
- package/lib/adapters/dsh-better-sidebar/index.js.map +1 -0
- package/lib/adapters/dsh-better-sidebar/methods.js +221 -0
- package/lib/adapters/dsh-better-sidebar/methods.js.map +1 -0
- package/lib/adapters/index.js +17 -0
- package/lib/adapters/index.js.map +1 -0
- package/lib/adapters/types.js +17 -0
- package/lib/adapters/types.js.map +1 -0
- package/lib/admin-routes.js +51 -0
- package/lib/admin-routes.js.map +1 -1
- package/lib/client-routes.js +18 -1
- package/lib/client-routes.js.map +1 -1
- package/lib/client.js +449 -45
- package/lib/config.js +27 -0
- package/lib/config.js.map +1 -1
- package/lib/http.js +10 -0
- package/lib/http.js.map +1 -1
- package/lib/index.js +273 -1
- package/lib/index.js.map +1 -1
- package/lib/plugin-adapter-core.js +385 -0
- package/lib/plugin-adapter-core.js.map +1 -0
- package/lib/plugin-bridge.js +318 -0
- package/lib/plugin-bridge.js.map +1 -0
- package/lib/plugin-loopback.js +207 -0
- package/lib/plugin-loopback.js.map +1 -0
- package/lib/plugin-route-intercept.js +1103 -0
- package/lib/plugin-route-intercept.js.map +1 -0
- package/lib/plugin-tunnel.js +416 -0
- package/lib/plugin-tunnel.js.map +1 -0
- package/lib/relay-client.js +319 -2
- package/lib/relay-client.js.map +1 -1
- package/lib/relay-server.js +633 -0
- package/lib/relay-server.js.map +1 -1
- package/lib/types/activity.d.ts +22 -0
- package/lib/types/activity.d.ts.map +1 -1
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts +27 -0
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -0
- package/lib/types/adapters/dsh-better-sidebar/methods.d.ts +58 -0
- package/lib/types/adapters/dsh-better-sidebar/methods.d.ts.map +1 -0
- package/lib/types/adapters/index.d.ts +17 -0
- package/lib/types/adapters/index.d.ts.map +1 -0
- package/lib/types/adapters/types.d.ts +42 -0
- package/lib/types/adapters/types.d.ts.map +1 -0
- package/lib/types/admin-routes.d.ts +16 -0
- package/lib/types/admin-routes.d.ts.map +1 -1
- package/lib/types/client/effects/remote-session.d.ts +0 -22
- package/lib/types/client/effects/remote-session.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +16 -0
- 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/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-data/plugin-bridge.d.ts +79 -0
- package/lib/types/client-data/plugin-bridge.d.ts.map +1 -0
- package/lib/types/client-data/remote-status.d.ts +20 -0
- package/lib/types/client-data/remote-status.d.ts.map +1 -1
- package/lib/types/client-data/settings-form.d.ts +37 -1
- package/lib/types/client-data/settings-form.d.ts.map +1 -1
- package/lib/types/client-routes.d.ts +13 -0
- package/lib/types/client-routes.d.ts.map +1 -1
- package/lib/types/config.d.ts +8 -0
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/http.d.ts +7 -0
- package/lib/types/http.d.ts.map +1 -1
- package/lib/types/index.d.ts +4 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/plugin-adapter-core.d.ts +263 -0
- package/lib/types/plugin-adapter-core.d.ts.map +1 -0
- package/lib/types/plugin-bridge.d.ts +187 -0
- package/lib/types/plugin-bridge.d.ts.map +1 -0
- package/lib/types/plugin-loopback.d.ts +104 -0
- package/lib/types/plugin-loopback.d.ts.map +1 -0
- package/lib/types/plugin-route-intercept.d.ts +114 -0
- package/lib/types/plugin-route-intercept.d.ts.map +1 -0
- package/lib/types/plugin-tunnel.d.ts +96 -0
- package/lib/types/plugin-tunnel.d.ts.map +1 -0
- package/lib/types/relay-client.d.ts +76 -0
- package/lib/types/relay-client.d.ts.map +1 -1
- package/lib/types/relay-server.d.ts +32 -1
- package/lib/types/relay-server.d.ts.map +1 -1
- package/llms.txt +1 -0
- package/package.json +2 -2
- package/scripts/check-plugin-bridge-ui.mjs +168 -0
- package/scripts/check-settings-form.mjs +3 -1
- package/src/activity.ts +32 -0
- package/src/adapters/dsh-better-sidebar/index.ts +144 -0
- package/src/adapters/dsh-better-sidebar/methods.ts +244 -0
- package/src/adapters/index.ts +19 -0
- package/src/adapters/types.ts +44 -0
- package/src/admin-routes.ts +64 -0
- package/src/client/effects/remote-session.ts +82 -10
- package/src/client/locales.ts +36 -0
- package/src/client/settings/SettingsSection.tsx +107 -3
- package/src/client/styles/remote-session.css.ts +70 -31
- package/src/client-data/plugin-bridge.ts +150 -0
- package/src/client-data/remote-status.ts +30 -0
- package/src/client-data/settings-form.ts +78 -0
- package/src/client-routes.ts +29 -1
- package/src/config.ts +32 -0
- package/src/http.ts +12 -0
- package/src/index.ts +279 -1
- package/src/plugin-adapter-core.ts +530 -0
- package/src/plugin-bridge.ts +435 -0
- package/src/plugin-loopback.ts +299 -0
- package/src/plugin-route-intercept.ts +1193 -0
- package/src/plugin-tunnel.ts +470 -0
- package/src/relay-client.ts +394 -2
- package/src/relay-server.ts +656 -1
package/src/index.ts
CHANGED
|
@@ -38,7 +38,7 @@ declare module '@deepseek-ai/cordis' {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
import { readFileConfig, resolveConfig, unwrapVolatile } from './config.js'
|
|
41
|
-
import { createActivityTracker, createParentIndex, startSweeper } from './activity.js'
|
|
41
|
+
import { createActivityTracker, createParentIndex, seedParentIndexFromSnapshots, startSweeper, type PersistedSessionSnapshot } from './activity.js'
|
|
42
42
|
import { ADMIN_ROUTE_PREFIX, createAdminHandler } from './admin-routes.js'
|
|
43
43
|
import type { SessionQueryLike } from './admin-routes.js'
|
|
44
44
|
import { normalizedRowServerUrl } from './client-pairing.js'
|
|
@@ -59,8 +59,22 @@ import { checkGatewayShape } from './intercept-shape.js'
|
|
|
59
59
|
import type { GatewayShapeCheck } from './intercept-shape.js'
|
|
60
60
|
import { checkFetchRouteShape, installFetchRouteIntercept } from './fetch-route-intercept.js'
|
|
61
61
|
import type { FetchRouteInterceptDiagnostics, FetchRouteInterceptHandle, FetchRouteShapeCheck } from './fetch-route-intercept.js'
|
|
62
|
+
import { checkWebServerShape, installPluginRouteIntercept } from './plugin-route-intercept.js'
|
|
63
|
+
import type { PluginRouteInterceptDiagnostics, PluginRouteInterceptHandle, WebServerShapeCheck } from './plugin-route-intercept.js'
|
|
64
|
+
import { normalizeAdapter, adapterOwnsRegistryEntry } from './plugin-adapter-core.js'
|
|
65
|
+
import type { NormalizedAdapter } from './plugin-adapter-core.js'
|
|
66
|
+
import { ADAPTERS } from './adapters/index.js'
|
|
62
67
|
import { createRelayHandler, loadServerId, RELAY_PREFIX, resolveDshVersion } from './relay-server.js'
|
|
63
68
|
import type { RelayGateway, RelayHandler } from './relay-server.js'
|
|
69
|
+
import { createBridgeRegistry, compareBridges } from './plugin-bridge.js'
|
|
70
|
+
import type { BridgeDeclaration, BridgeRegistry, BridgeStatus, ServerPluginEntry, ZenRemoteService } from './plugin-bridge.js'
|
|
71
|
+
import { createPluginLoopback } from './plugin-loopback.js'
|
|
72
|
+
import { createPluginTunnel } from './plugin-tunnel.js'
|
|
73
|
+
|
|
74
|
+
// The plugin bridge's public types (PB-1), re-exported so another plugin's
|
|
75
|
+
// author can `import type { … } from 'dsh-zen-remote'` without reaching into
|
|
76
|
+
// lib/types. Type-only: no runtime code, no Context augmentation.
|
|
77
|
+
export type { BridgeDeclaration, ZenRemoteService } from './plugin-bridge.js'
|
|
64
78
|
|
|
65
79
|
// The loader's config schema (settings form) and the role normalizer live in
|
|
66
80
|
// src/config.ts next to the resolution they describe; re-exported so the
|
|
@@ -126,6 +140,22 @@ export function getRelayClient(): RelayClient | undefined {
|
|
|
126
140
|
return clientRelayClient
|
|
127
141
|
}
|
|
128
142
|
|
|
143
|
+
/**
|
|
144
|
+
* The plugin bridge's declaration registry (PB-1), created inside apply() on
|
|
145
|
+
* BOTH roles and kept on the module so later tasks (the handshake's plugins
|
|
146
|
+
* table, the client-side rewrite) can reach it without reaching into a
|
|
147
|
+
* context. Undefined in compositions that never ran this module's apply, and
|
|
148
|
+
* — identity-guarded below — after the row that owned a registry was
|
|
149
|
+
* unloaded or reloaded, so a reloaded row never leaves a stale table where
|
|
150
|
+
* the getter can hand it out (the T23a-fix discipline).
|
|
151
|
+
*/
|
|
152
|
+
let bridgeRegistry: BridgeRegistry | undefined
|
|
153
|
+
|
|
154
|
+
/** The live bridge registry, if a plugin row is currently applied. */
|
|
155
|
+
export function getBridgeRegistry(): BridgeRegistry | undefined {
|
|
156
|
+
return bridgeRegistry
|
|
157
|
+
}
|
|
158
|
+
|
|
129
159
|
/** Host half config. Every field below arrives through the loader as a
|
|
130
160
|
* volatile live reference — a `{ get() }` wrapper over the stored value, not
|
|
131
161
|
* the value itself (src/config.ts explains why every field is volatile) — so
|
|
@@ -461,6 +491,76 @@ export async function handleUpload(
|
|
|
461
491
|
*/
|
|
462
492
|
export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
463
493
|
const effective = resolveConfig(config, readFileConfig(), process.env)
|
|
494
|
+
// The plugin bridge's zenRemote service (PB-1), provided on BOTH roles
|
|
495
|
+
// before any role branch: a plugin that wants its panel data forwarded in
|
|
496
|
+
// remote sessions declares that against THIS process whichever role it
|
|
497
|
+
// runs as, and the declaration registry is the one piece of PB-1 both the
|
|
498
|
+
// server half (handshake plugins table) and the client half (route
|
|
499
|
+
// rewrite) will read. ctx.provide (cordis 4) registers the service owned
|
|
500
|
+
// by THIS row's fiber: visible to `inject(['zenRemote'])` dependents once
|
|
501
|
+
// the row is active, withdrawn when the row unloads — so a composition
|
|
502
|
+
// without zen-remote never fires those injects and a row reload swaps the
|
|
503
|
+
// service wholesale. The registry goes into the module slot BEFORE the
|
|
504
|
+
// provide so this apply's consumers never see a window without it; the
|
|
505
|
+
// effect's identity guard then clears the slot exactly when THIS row
|
|
506
|
+
// unloads (a reload installs the new registry before the old disposer
|
|
507
|
+
// runs, same shape as clientRelayClient). provide() throwing is not worth
|
|
508
|
+
// failing the row over — the bridge stays off, everything else keeps
|
|
509
|
+
// working — so it degrades to a warning like the session feeds do.
|
|
510
|
+
const registry = createBridgeRegistry({
|
|
511
|
+
warn: (text) => { ctx.logger?.warn('dsh-zen-remote plugin bridge: %s', text) },
|
|
512
|
+
})
|
|
513
|
+
bridgeRegistry = registry
|
|
514
|
+
try {
|
|
515
|
+
const service: ZenRemoteService = {
|
|
516
|
+
bridge: (declaration: BridgeDeclaration) => registry.register(declaration),
|
|
517
|
+
}
|
|
518
|
+
ctx.provide('zenRemote', service)
|
|
519
|
+
} catch (error) {
|
|
520
|
+
ctx.logger?.warn('dsh-zen-remote: cannot provide the "zenRemote" service: %s', message(error))
|
|
521
|
+
}
|
|
522
|
+
ctx.effect(() => () => {
|
|
523
|
+
if (bridgeRegistry === registry) bridgeRegistry = undefined
|
|
524
|
+
}, 'dsh-zen-remote: plugin bridge registry disposal')
|
|
525
|
+
// The adapters (PB-11): each one registers ONLY against its target's
|
|
526
|
+
// EXACT installed version, probed off the host's `pluginPackages` service
|
|
527
|
+
// (`packageOf(name, ctx.baseUrl)` — verified on 0.2.0-rc.2 to answer
|
|
528
|
+
// `{name, version, dir, …}` or undefined; any other shape, a missing
|
|
529
|
+
// service or a probe failure reads as "not installed" and the adapter
|
|
530
|
+
// stays out). A plugin that registers its OWN cooperative declaration
|
|
531
|
+
// always wins: the adapter yields at registration time (logged), and the
|
|
532
|
+
// runtime re-checks the registry digest on every request. Both roles run
|
|
533
|
+
// this loop — the registry entries drive the handshake table, the bridge
|
|
534
|
+
// verdicts and the settings listing on either side, while the returned
|
|
535
|
+
// array feeds the two request halves.
|
|
536
|
+
const adapters: NormalizedAdapter[] = []
|
|
537
|
+
{
|
|
538
|
+
const adapterWarn = (text: string): void => { ctx.logger?.warn('dsh-zen-remote adapter: %s', text) }
|
|
539
|
+
for (const adapter of ADAPTERS) {
|
|
540
|
+
try {
|
|
541
|
+
const holder = ctx as unknown as { get?: (name: string) => unknown; baseUrl?: string }
|
|
542
|
+
const packages = holder.get?.('pluginPackages') as { packageOf?: (specifier: string, parentURL: string) => unknown } | undefined
|
|
543
|
+
const found = packages?.packageOf?.(adapter.plugin, holder.baseUrl ?? '') as { version?: unknown } | undefined
|
|
544
|
+
const installedVersion = found !== null && typeof found === 'object' && typeof (found as { version?: unknown }).version === 'string'
|
|
545
|
+
? (found as { version: string }).version
|
|
546
|
+
: undefined
|
|
547
|
+
if (installedVersion === undefined || installedVersion === '') {
|
|
548
|
+
adapterWarn(`${adapter.plugin}: target package (or its version) not found; not registered`)
|
|
549
|
+
continue
|
|
550
|
+
}
|
|
551
|
+
const normalized = normalizeAdapter(adapter, installedVersion)
|
|
552
|
+
if (registry.get(normalized.plugin) !== undefined) {
|
|
553
|
+
adapterWarn(`${normalized.plugin}: the plugin registers its own bridge declaration; the adapter yields`)
|
|
554
|
+
continue
|
|
555
|
+
}
|
|
556
|
+
registry.registerNormalized(normalized.declaration)
|
|
557
|
+
adapters.push(normalized)
|
|
558
|
+
ctx.logger?.info('dsh-zen-remote adapter: %s adapted at version %s', normalized.plugin, installedVersion)
|
|
559
|
+
} catch (error) {
|
|
560
|
+
adapterWarn(`${adapter.plugin} refused: ${error instanceof Error ? error.message : String(error)}`)
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
464
564
|
if (effective.values.role === 'host') {
|
|
465
565
|
// A row that switched from client to host must not leave a stale relay
|
|
466
566
|
// client where getRelayClient() can find it (T23a-fix).
|
|
@@ -557,6 +657,32 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
557
657
|
const agents = (agentsCtx as Context & { agents: { list(): readonly AgentStatusLike[] } }).agents
|
|
558
658
|
restoreBusy(store, () => agents.list())
|
|
559
659
|
})
|
|
660
|
+
// Restart repair (2026-10-13): the parent index rebuilds only from LIVE
|
|
661
|
+
// session/event traffic, so after a host restart every already-stored
|
|
662
|
+
// sidechat / subagent child lost its parent link and the relay answered
|
|
663
|
+
// `not-shared` for a session whose share it used to inherit through the
|
|
664
|
+
// ancestor chain. Seed the index once from the persistence service's
|
|
665
|
+
// snapshot list — headers only, event logs are never read.
|
|
666
|
+
try {
|
|
667
|
+
ctx.inject(['sessionPersistence'], (persistenceCtx) => {
|
|
668
|
+
const persistence = (persistenceCtx as Context & {
|
|
669
|
+
sessionPersistence?: { list(): Promise<readonly PersistedSessionSnapshot[]> } | undefined
|
|
670
|
+
}).sessionPersistence
|
|
671
|
+
if (persistence === undefined) {
|
|
672
|
+
ctx.logger.warn('dsh-zen-remote: no sessionPersistence service — pre-restart subagent sessions stay not-shared until they emit an event')
|
|
673
|
+
return
|
|
674
|
+
}
|
|
675
|
+
void persistence
|
|
676
|
+
.list()
|
|
677
|
+
.then((snapshots) => {
|
|
678
|
+
const recorded = seedParentIndexFromSnapshots(parentIndex, snapshots)
|
|
679
|
+
if (recorded > 0) ctx.logger.info?.('dsh-zen-remote: restored %d persisted parent links', recorded)
|
|
680
|
+
})
|
|
681
|
+
.catch((error: unknown) => ctx.logger.warn('dsh-zen-remote cannot seed parent links from persisted sessions: %s', message(error)))
|
|
682
|
+
})
|
|
683
|
+
} catch (error) {
|
|
684
|
+
ctx.logger.warn('dsh-zen-remote cannot listen on "sessionPersistence": %s', message(error))
|
|
685
|
+
}
|
|
560
686
|
// Settings-surface admin routes (T14). The browser cannot call the
|
|
561
687
|
// gateway's loopback-only admin API from DSH's origin, so the host
|
|
562
688
|
// process re-exposes it same-origin and the handler calls the gateway AS
|
|
@@ -597,6 +723,16 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
597
723
|
// composition lacks the service.
|
|
598
724
|
viewerCount: (id) => relayHandler?.viewerCount(id) ?? 0,
|
|
599
725
|
sessionQuery: () => webCtx.reflect.get('sessionQuery') as SessionQueryLike | undefined,
|
|
726
|
+
// The plugin bridge listing (PB-5): the SAME registry this apply
|
|
727
|
+
// built and the SAME live consent reader the relay's plugins table
|
|
728
|
+
// uses — one source of truth per question. PB-11: the listing
|
|
729
|
+
// marks the plugins an adapter stands in for (live — a plugin
|
|
730
|
+
// that registered its own declaration stops being marked).
|
|
731
|
+
pluginBridge: {
|
|
732
|
+
registry,
|
|
733
|
+
getConsent: () => resolveConfig(config, readFileConfig(), process.env).values.pluginBridgeConsent,
|
|
734
|
+
adaptedPlugins: () => new Set(adapters.filter((adapter) => adapterOwnsRegistryEntry(adapter, registry.get(adapter.plugin))).map((adapter) => adapter.plugin)),
|
|
735
|
+
},
|
|
600
736
|
}),
|
|
601
737
|
}), 'dsh-zen-remote: admin routes')
|
|
602
738
|
})
|
|
@@ -645,6 +781,32 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
645
781
|
// ~7 ms per computation against a full registry; a failed compute
|
|
646
782
|
// lands as the empty map inside the relay handler's own guard.
|
|
647
783
|
relayCtx.effect(() => {
|
|
784
|
+
// The plugin bridge's loopback facts (PB-3), both read PER REQUEST
|
|
785
|
+
// through the reflection layer: the web server may have restarted on
|
|
786
|
+
// another port or protocol, and the connection service's
|
|
787
|
+
// authenticatedUrl — the token-exchange entry the lan-gate wiring
|
|
788
|
+
// also reads (0.1.1 hosts lack the method entirely) — lives on a
|
|
789
|
+
// service deliberately NOT in this inject's declaration list (same
|
|
790
|
+
// reason as getApiFetch above: adding it would keep the whole relay
|
|
791
|
+
// prefix off compositions that lack it).
|
|
792
|
+
const getLoopbackTarget = () => {
|
|
793
|
+
const webServer = relayCtx.reflect.get('webServer') as { protocol?: unknown; port?: unknown } | undefined
|
|
794
|
+
const port = typeof webServer?.port === 'number' && webServer.port > 0 ? webServer.port : undefined
|
|
795
|
+
if (port === undefined) throw new Error('the web server exposes no loopback port')
|
|
796
|
+
return { protocol: webServer?.protocol === 'https:' ? ('https:' as const) : ('http:' as const), port }
|
|
797
|
+
}
|
|
798
|
+
const getTokenUrl = () => {
|
|
799
|
+
const connection = relayCtx.reflect.get('connection') as
|
|
800
|
+
| { authenticatedUrl?: (base: string) => string | undefined }
|
|
801
|
+
| undefined
|
|
802
|
+
if (typeof connection?.authenticatedUrl !== 'function') return undefined
|
|
803
|
+
const { protocol, port } = getLoopbackTarget()
|
|
804
|
+
return connection.authenticatedUrl(`${protocol}//127.0.0.1:${port}`)
|
|
805
|
+
}
|
|
806
|
+
// One loopback executor for BOTH plugin halves (the HTTP forward and
|
|
807
|
+
// the PB-10 tunnel): a single cookie cache behind two consumers.
|
|
808
|
+
const pluginLoopback = createPluginLoopback({ getTarget: getLoopbackTarget, getTokenUrl })
|
|
809
|
+
const pluginConsent = () => resolveConfig(config, readFileConfig(), process.env).values.pluginBridgeConsent
|
|
648
810
|
const handler = createRelayHandler({
|
|
649
811
|
secret: relaySecret,
|
|
650
812
|
store,
|
|
@@ -662,6 +824,43 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
662
824
|
serverName: () => resolveConfig(config, readFileConfig(), process.env).values.serverName,
|
|
663
825
|
dshVersion: resolveDshVersion(),
|
|
664
826
|
fingerprints: () => computeFingerprints(relayCtx),
|
|
827
|
+
// PB-2: the plugin bridge table, rebuilt FRESH at every handshake
|
|
828
|
+
// from the PB-1 registry and the row's consent map — both live:
|
|
829
|
+
// the registry through this apply's own closure, the consent
|
|
830
|
+
// through resolveConfig on the SAME row object (a volatile field,
|
|
831
|
+
// read at use time per the hard constraint, never snapshotted).
|
|
832
|
+
plugins: () => {
|
|
833
|
+
const consent = resolveConfig(config, readFileConfig(), process.env).values.pluginBridgeConsent
|
|
834
|
+
const table: Record<string, ServerPluginEntry> = {}
|
|
835
|
+
for (const declaration of registry.list()) {
|
|
836
|
+
table[declaration.plugin] = {
|
|
837
|
+
version: declaration.version,
|
|
838
|
+
digest: declaration.digest,
|
|
839
|
+
serverScopeConsent: consent[declaration.plugin] === true,
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return table
|
|
843
|
+
},
|
|
844
|
+
},
|
|
845
|
+
// PB-3: the forwarding half of the plugin bridge — the registry
|
|
846
|
+
// this apply built (the zenRemote service's table), the consent
|
|
847
|
+
// map read LIVE per request (a volatile row field), and the
|
|
848
|
+
// loopback executor over the wiring's two reflection getters.
|
|
849
|
+
// PB-10: the WebSocket byte tunnel's server half shares the SAME
|
|
850
|
+
// loopback (one cookie cache) and the same live consent; its
|
|
851
|
+
// reachability judgment rides the store directly (the handler's
|
|
852
|
+
// own isAccessible closure is not visible out here).
|
|
853
|
+
pluginBridge: {
|
|
854
|
+
registry,
|
|
855
|
+
getConsent: pluginConsent,
|
|
856
|
+
loopback: pluginLoopback,
|
|
857
|
+
adapters,
|
|
858
|
+
tunnel: createPluginTunnel({
|
|
859
|
+
bridge: { registry, getConsent: pluginConsent, loopback: pluginLoopback },
|
|
860
|
+
getTarget: getLoopbackTarget,
|
|
861
|
+
isAccessible: (sessionId) => store.isAccessible(sessionId, parentIndex.parentOf),
|
|
862
|
+
heartbeatMs: 15_000,
|
|
863
|
+
}),
|
|
665
864
|
},
|
|
666
865
|
})
|
|
667
866
|
const unregister = relayCtx.webServer.register({
|
|
@@ -735,6 +934,15 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
735
934
|
computeOwnFingerprints: () => computeFingerprints(ctx),
|
|
736
935
|
})
|
|
737
936
|
clientRelayClient = relayClient
|
|
937
|
+
// The plugin bridge's live verdict (PB-2), the CLIENT role's job — a host
|
|
938
|
+
// has no remote sessions of its own to bridge anything into. Computed PER
|
|
939
|
+
// CALL (never a stored conclusion): the local registry is this apply's
|
|
940
|
+
// own closure, the server's table is the LAST handshake's `plugins`
|
|
941
|
+
// (undefined until one succeeded — an old server reads server-too-old),
|
|
942
|
+
// and `online` is the client's CURRENT state — so a registry change, a
|
|
943
|
+
// fresh handshake or a link flip shows up at the very next read.
|
|
944
|
+
const pluginBridgeStatus = (): BridgeStatus[] =>
|
|
945
|
+
compareBridges(registry.list(), relayClient.handshakeInfo?.plugins, relayClient.state === 'online')
|
|
738
946
|
// A stopped or reloaded row must not leave a stale instance where the
|
|
739
947
|
// module getter can hand it out (T23a-fix), and its reconnect machinery
|
|
740
948
|
// (T43) must not outlive the row either: stop() cancels the armed retry
|
|
@@ -931,6 +1139,74 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
931
1139
|
if (fetchRouteHandle === handle) fetchRouteHandle = undefined
|
|
932
1140
|
}, 'dsh-zen-remote: fetch-route intercept')
|
|
933
1141
|
})
|
|
1142
|
+
// The plugin bridge's route interception (PB-4): the local webServer's
|
|
1143
|
+
// `match` wrap forwarding bridged-plugin requests for REMOTE sessions
|
|
1144
|
+
// through the relay. Injected on `webServer` alone — the only service
|
|
1145
|
+
// it reads — and the raw instance comes off the reflection layer's
|
|
1146
|
+
// symbols lookup, the same face the typert and fetch-route wraps use.
|
|
1147
|
+
// The diagnostics flow to the client status route like theirs, refused
|
|
1148
|
+
// installs included.
|
|
1149
|
+
let pluginRouteHandle: PluginRouteInterceptHandle | undefined
|
|
1150
|
+
let pluginRouteRefused: WebServerShapeCheck | undefined
|
|
1151
|
+
const pluginRouteDiagnostics = (): PluginRouteInterceptDiagnostics | undefined => {
|
|
1152
|
+
if (pluginRouteHandle !== undefined) return pluginRouteHandle.diagnostics()
|
|
1153
|
+
if (pluginRouteRefused !== undefined) {
|
|
1154
|
+
return {
|
|
1155
|
+
installed: false,
|
|
1156
|
+
shape: pluginRouteRefused,
|
|
1157
|
+
forwarded: 0,
|
|
1158
|
+
refused: 0,
|
|
1159
|
+
refusalsByReason: {},
|
|
1160
|
+
recentFailures: [],
|
|
1161
|
+
adapterForwarded: 0,
|
|
1162
|
+
adapterResponseRewrites: 0,
|
|
1163
|
+
adapterResponsePassthroughs: 0,
|
|
1164
|
+
tunnelsCurrent: 0,
|
|
1165
|
+
tunnelsOpened: 0,
|
|
1166
|
+
tunnelsClosed: 0,
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
return undefined
|
|
1170
|
+
}
|
|
1171
|
+
ctx.inject(['webServer'], (wsCtx) => {
|
|
1172
|
+
const logWarn = (...args: Parameters<Context['logger']['warn']>): void => { ctx.logger?.warn(...args) }
|
|
1173
|
+
const proxy = (wsCtx as Context & { webServer?: unknown }).webServer
|
|
1174
|
+
const raw = proxy !== null && typeof proxy === 'object'
|
|
1175
|
+
? (proxy as unknown as Record<PropertyKey, unknown>)[symbols.original]
|
|
1176
|
+
: undefined
|
|
1177
|
+
if (raw === null || typeof raw !== 'object') {
|
|
1178
|
+
pluginRouteRefused = { ok: false, reasons: ['original: webServer[symbols.original] is not an object'] }
|
|
1179
|
+
logWarn('dsh-zen-remote plugin-route intercept not installed: %s', pluginRouteRefused.reasons[0])
|
|
1180
|
+
return
|
|
1181
|
+
}
|
|
1182
|
+
// Shape first, like every wrap: a failed check leaves the webServer
|
|
1183
|
+
// exactly as it was — local routing untouched, remote bridging off.
|
|
1184
|
+
const shape = checkWebServerShape(raw)
|
|
1185
|
+
if (!shape.ok) {
|
|
1186
|
+
pluginRouteRefused = shape
|
|
1187
|
+
logWarn('dsh-zen-remote plugin-route intercept not installed: %s', shape.reasons.join('; '))
|
|
1188
|
+
return
|
|
1189
|
+
}
|
|
1190
|
+
const handle = installPluginRouteIntercept({
|
|
1191
|
+
webServer: raw,
|
|
1192
|
+
registry,
|
|
1193
|
+
relay: relayClient,
|
|
1194
|
+
getServerId: () => relayClient.handshakeInfo?.serverId,
|
|
1195
|
+
getStatus: pluginBridgeStatus,
|
|
1196
|
+
adapters,
|
|
1197
|
+
// PB-12: the browser's currently open session — a VIRTUAL id means
|
|
1198
|
+
// a remote panel is the request's origin, the signal the adapter
|
|
1199
|
+
// routes' deny hooks read (better-sidebar's open.external carries
|
|
1200
|
+
// no session field of its own).
|
|
1201
|
+
getCurrentSession: () => (reportedCurrentSession.kind === 'open' ? reportedCurrentSession.sessionId : undefined),
|
|
1202
|
+
log: (format, ...args) => { ctx.logger?.info(`dsh-zen-remote ${format}`, ...args) },
|
|
1203
|
+
})
|
|
1204
|
+
pluginRouteHandle = handle
|
|
1205
|
+
wsCtx.effect(() => () => {
|
|
1206
|
+
handle.uninstall()
|
|
1207
|
+
if (pluginRouteHandle === handle) pluginRouteHandle = undefined
|
|
1208
|
+
}, 'dsh-zen-remote: plugin route intercept')
|
|
1209
|
+
})
|
|
934
1210
|
ctx.inject(['webServer', 'connection'], (clientCtx) => {
|
|
935
1211
|
clientCtx.effect(() => clientCtx.webServer.register({
|
|
936
1212
|
kind: 'prefix',
|
|
@@ -941,6 +1217,8 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
941
1217
|
getRelayClient: () => relayClient,
|
|
942
1218
|
getIntercept: interceptDiagnostics,
|
|
943
1219
|
getFetchRouteIntercept: fetchRouteDiagnostics,
|
|
1220
|
+
getPluginRouteIntercept: pluginRouteDiagnostics,
|
|
1221
|
+
getPluginBridgeStatus: pluginBridgeStatus,
|
|
944
1222
|
onCurrentSession,
|
|
945
1223
|
}),
|
|
946
1224
|
}), 'dsh-zen-remote: client routes')
|