dsh-zen-remote 2.2.2 → 2.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/README.zh-CN.md +5 -1
- package/lib/admin-routes.js +3 -2
- package/lib/admin-routes.js.map +1 -1
- package/lib/index.js +16 -1
- package/lib/index.js.map +1 -1
- package/lib/intercept-shape.js +10 -4
- package/lib/intercept-shape.js.map +1 -1
- package/lib/intercept.js +26 -11
- package/lib/intercept.js.map +1 -1
- package/lib/owner-peer.js +72 -0
- package/lib/owner-peer.js.map +1 -0
- package/lib/relay-server.js +10 -9
- package/lib/relay-server.js.map +1 -1
- package/lib/types/admin-routes.d.ts +2 -0
- package/lib/types/admin-routes.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/intercept-shape.d.ts.map +1 -1
- package/lib/types/intercept.d.ts +9 -0
- package/lib/types/intercept.d.ts.map +1 -1
- package/lib/types/owner-peer.d.ts +57 -0
- package/lib/types/owner-peer.d.ts.map +1 -0
- package/lib/types/relay-server.d.ts +8 -0
- package/lib/types/relay-server.d.ts.map +1 -1
- package/package.json +36 -36
- package/src/admin-routes.ts +5 -2
- package/src/index.ts +16 -1
- package/src/intercept-shape.ts +10 -3
- package/src/intercept.ts +33 -7
- package/src/owner-peer.ts +81 -0
- package/src/relay-server.ts +18 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-zen-remote",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.3",
|
|
4
4
|
"description": "一个插件、两种角色:主服务端把 DeepSeek Harness 变成手机能安全访问的 PWA(移动端界面、配对码网关、Web Push),并把选定会话共享给已配对的桌面端;子客户端在另一台 DSH 桌面端的侧边栏里远程使用这些会话。One DSH plugin, two roles: the host turns DeepSeek Harness into a phone-reachable PWA (mobile UI, pairing-code gateway behind your own reverse proxy, Web Push) and shares chosen sessions to paired desktops; the client uses those sessions remotely from another DSH desktop app's sidebar.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -115,42 +115,42 @@
|
|
|
115
115
|
}
|
|
116
116
|
},
|
|
117
117
|
"devDependencies": {
|
|
118
|
-
"@deepseek-ai/cordis": "4.0.5-alpha.
|
|
118
|
+
"@deepseek-ai/cordis": "4.0.5-alpha.2",
|
|
119
119
|
"@deepseek-ai/cosmokit": "1.8.5",
|
|
120
|
-
"@deepseek-ai/dsh-agent": "0.2.1-alpha.
|
|
121
|
-
"@deepseek-ai/dsh-api-remotes": "0.2.1-alpha.
|
|
122
|
-
"@deepseek-ai/dsh-api-session-controller": "0.2.1-alpha.
|
|
123
|
-
"@deepseek-ai/dsh-api-workspace-controller": "0.2.1-alpha.
|
|
124
|
-
"@deepseek-ai/dsh-brand": "0.2.1-alpha.
|
|
125
|
-
"@deepseek-ai/dsh-client-connection": "0.2.1-alpha.
|
|
126
|
-
"@deepseek-ai/dsh-client-locale": "0.2.1-alpha.
|
|
127
|
-
"@deepseek-ai/dsh-client-store": "0.2.1-alpha.
|
|
128
|
-
"@deepseek-ai/dsh-client-ui-conversation": "0.2.1-alpha.
|
|
129
|
-
"@deepseek-ai/dsh-client-ui-layout": "0.2.1-alpha.
|
|
130
|
-
"@deepseek-ai/dsh-client-ui-primitives": "0.2.1-alpha.
|
|
131
|
-
"@deepseek-ai/dsh-client-ui-renderer": "0.2.1-alpha.
|
|
132
|
-
"@deepseek-ai/dsh-client-ui-session": "0.2.1-alpha.
|
|
133
|
-
"@deepseek-ai/dsh-client-ui-sidebar": "0.2.1-alpha.
|
|
134
|
-
"@deepseek-ai/dsh-client-ui-slots": "0.2.1-alpha.
|
|
135
|
-
"@deepseek-ai/dsh-host-webserver": "0.2.1-alpha.
|
|
136
|
-
"@deepseek-ai/dsh-llm": "0.2.1-alpha.
|
|
137
|
-
"@deepseek-ai/dsh-ptc-runtime": "0.2.1-alpha.
|
|
138
|
-
"@deepseek-ai/dsh-sandbox": "0.2.1-alpha.
|
|
139
|
-
"@deepseek-ai/dsh-sandbox-policy": "0.2.1-alpha.
|
|
140
|
-
"@deepseek-ai/dsh-scope": "0.2.1-alpha.
|
|
141
|
-
"@deepseek-ai/dsh-session": "0.2.1-alpha.
|
|
142
|
-
"@deepseek-ai/dsh-session-log-export": "0.2.1-alpha.
|
|
143
|
-
"@deepseek-ai/dsh-session-projection": "0.2.1-alpha.
|
|
144
|
-
"@deepseek-ai/dsh-session-query": "0.2.1-alpha.
|
|
145
|
-
"@deepseek-ai/dsh-session-stats": "0.2.1-alpha.
|
|
146
|
-
"@deepseek-ai/dsh-subagent": "0.2.1-alpha.
|
|
147
|
-
"@deepseek-ai/dsh-timeout": "0.2.1-alpha.
|
|
148
|
-
"@deepseek-ai/dsh-token-meter": "0.2.1-alpha.
|
|
149
|
-
"@deepseek-ai/dsh-tools": "0.2.1-alpha.
|
|
150
|
-
"@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.
|
|
151
|
-
"@deepseek-ai/dsh-util-crypto": "0.2.1-alpha.
|
|
152
|
-
"@deepseek-ai/dsh-util-values": "0.2.1-alpha.
|
|
153
|
-
"@deepseek-ai/dsh-workspace": "0.2.1-alpha.
|
|
120
|
+
"@deepseek-ai/dsh-agent": "0.2.1-alpha.3",
|
|
121
|
+
"@deepseek-ai/dsh-api-remotes": "0.2.1-alpha.3",
|
|
122
|
+
"@deepseek-ai/dsh-api-session-controller": "0.2.1-alpha.3",
|
|
123
|
+
"@deepseek-ai/dsh-api-workspace-controller": "0.2.1-alpha.3",
|
|
124
|
+
"@deepseek-ai/dsh-brand": "0.2.1-alpha.3",
|
|
125
|
+
"@deepseek-ai/dsh-client-connection": "0.2.1-alpha.3",
|
|
126
|
+
"@deepseek-ai/dsh-client-locale": "0.2.1-alpha.3",
|
|
127
|
+
"@deepseek-ai/dsh-client-store": "0.2.1-alpha.3",
|
|
128
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.2.1-alpha.3",
|
|
129
|
+
"@deepseek-ai/dsh-client-ui-layout": "0.2.1-alpha.3",
|
|
130
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.2.1-alpha.3",
|
|
131
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.2.1-alpha.3",
|
|
132
|
+
"@deepseek-ai/dsh-client-ui-session": "0.2.1-alpha.3",
|
|
133
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "0.2.1-alpha.3",
|
|
134
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.2.1-alpha.3",
|
|
135
|
+
"@deepseek-ai/dsh-host-webserver": "0.2.1-alpha.3",
|
|
136
|
+
"@deepseek-ai/dsh-llm": "0.2.1-alpha.3",
|
|
137
|
+
"@deepseek-ai/dsh-ptc-runtime": "0.2.1-alpha.3",
|
|
138
|
+
"@deepseek-ai/dsh-sandbox": "0.2.1-alpha.3",
|
|
139
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.2.1-alpha.3",
|
|
140
|
+
"@deepseek-ai/dsh-scope": "0.2.1-alpha.3",
|
|
141
|
+
"@deepseek-ai/dsh-session": "0.2.1-alpha.3",
|
|
142
|
+
"@deepseek-ai/dsh-session-log-export": "0.2.1-alpha.3",
|
|
143
|
+
"@deepseek-ai/dsh-session-projection": "0.2.1-alpha.3",
|
|
144
|
+
"@deepseek-ai/dsh-session-query": "0.2.1-alpha.3",
|
|
145
|
+
"@deepseek-ai/dsh-session-stats": "0.2.1-alpha.3",
|
|
146
|
+
"@deepseek-ai/dsh-subagent": "0.2.1-alpha.3",
|
|
147
|
+
"@deepseek-ai/dsh-timeout": "0.2.1-alpha.3",
|
|
148
|
+
"@deepseek-ai/dsh-token-meter": "0.2.1-alpha.3",
|
|
149
|
+
"@deepseek-ai/dsh-tools": "0.2.1-alpha.3",
|
|
150
|
+
"@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.3",
|
|
151
|
+
"@deepseek-ai/dsh-util-crypto": "0.2.1-alpha.3",
|
|
152
|
+
"@deepseek-ai/dsh-util-values": "0.2.1-alpha.3",
|
|
153
|
+
"@deepseek-ai/dsh-workspace": "0.2.1-alpha.3",
|
|
154
154
|
"@deepseek-ai/schemastery": "3.18.4",
|
|
155
155
|
"@types/node": "^26.2.0",
|
|
156
156
|
"@types/react": "~18.3.1",
|
package/src/admin-routes.ts
CHANGED
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
* drive it over a real socket with a mock gateway and a mock admit.
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
|
+
import { withPeer } from './owner-peer.js'
|
|
40
41
|
import { request as httpRequest } from 'node:http'
|
|
41
42
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
42
43
|
import type { ConfigSource, ZenRemoteConfig } from './config.js'
|
|
@@ -150,6 +151,8 @@ export interface AdminHandlerOptions {
|
|
|
150
151
|
* share trusts the table alone. The lookup itself must never be allowed to
|
|
151
152
|
* throw into the route, so callers wrap it. */
|
|
152
153
|
typert?: () => RelayGateway | undefined
|
|
154
|
+
/** The owner Peer gateway calls are made as (DSH 0.2.1-alpha.3+); see owner-peer.ts. */
|
|
155
|
+
getPeer?: () => unknown
|
|
153
156
|
/** Live agent roster for the busy restore a successful `share` performs.
|
|
154
157
|
* Absent or throwing means "no information": nothing is marked busy. */
|
|
155
158
|
listAgents?: () => readonly AgentStatusLike[]
|
|
@@ -358,12 +361,12 @@ async function projectionOf(options: AdminHandlerOptions, sessionId: string): Pr
|
|
|
358
361
|
return { kind: 'no-gateway' }
|
|
359
362
|
}
|
|
360
363
|
if (gateway === undefined) return { kind: 'no-gateway' }
|
|
361
|
-
const value = await gateway.invoke({
|
|
364
|
+
const value = await gateway.invoke(withPeer({
|
|
362
365
|
namespace: 'session',
|
|
363
366
|
method: 'projections',
|
|
364
367
|
args: { request: { sessionId } },
|
|
365
368
|
signal: AbortSignal.timeout(options.projectionTimeoutMs ?? PROJECTION_TIMEOUT_MS),
|
|
366
|
-
})
|
|
369
|
+
}, options.getPeer))
|
|
367
370
|
if (value === null || value === undefined) return { kind: 'no-session' }
|
|
368
371
|
const values = (value as { values?: unknown }).values
|
|
369
372
|
return {
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* the package.json dsh.client declaration.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { createOwnerPeerSource } from './owner-peer.js'
|
|
18
19
|
import { lstat, mkdir, open, realpath, rm } from 'node:fs/promises'
|
|
19
20
|
import type { FileHandle } from 'node:fs/promises'
|
|
20
21
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
@@ -715,6 +716,7 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
715
716
|
// which would turn every shares request into a 500.
|
|
716
717
|
store,
|
|
717
718
|
typert: () => webCtx.reflect.get('typertGateway') as RelayGateway | undefined,
|
|
719
|
+
getPeer: createOwnerPeerSource(() => webCtx.connection),
|
|
718
720
|
listAgents: () => (webCtx.reflect.get('agents') as { list(): readonly AgentStatusLike[] } | undefined)?.list() ?? [],
|
|
719
721
|
// The "who is looking" badge (T33a2): read through whichever relay
|
|
720
722
|
// handler is live right now. sessionQuery likewise — the share
|
|
@@ -780,6 +782,7 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
780
782
|
// registry and pin those throwaway values for the row's lifetime.
|
|
781
783
|
// ~7 ms per computation against a full registry; a failed compute
|
|
782
784
|
// lands as the empty map inside the relay handler's own guard.
|
|
785
|
+
const relayPeer = createOwnerPeerSource(() => relayCtx.reflect.get('connection'))
|
|
783
786
|
relayCtx.effect(() => {
|
|
784
787
|
// The plugin bridge's loopback facts (PB-3), both read PER REQUEST
|
|
785
788
|
// through the reflection layer: the web server may have restarted on
|
|
@@ -812,6 +815,10 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
812
815
|
store,
|
|
813
816
|
gateway: gatewayService,
|
|
814
817
|
getApiFetch,
|
|
818
|
+
// DSH 0.2.1-alpha.3: one cached owner/answer Peer for every relay
|
|
819
|
+
// gateway call — `$events/result` must come from the Peer that
|
|
820
|
+
// opened `$events`.
|
|
821
|
+
getPeer: relayPeer,
|
|
815
822
|
// Subagent reachability (T22c-fix): isAccessible walks the same
|
|
816
823
|
// child→parent chain the activity tracker keeps fresh.
|
|
817
824
|
parentOf: parentIndex.parentOf,
|
|
@@ -1068,7 +1075,15 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
1068
1075
|
// second failure uninstalls (a half-trusted interception is worse than
|
|
1069
1076
|
// none). Failure handling lives in runSelfCheck, which records the
|
|
1070
1077
|
// verdict via noteSelfCheck.
|
|
1071
|
-
|
|
1078
|
+
// DSH 0.2.1-alpha.3: the probe needs an owner Peer of its own (the
|
|
1079
|
+
// gateway's operatorPeer fallback is gone) — read `connection` through
|
|
1080
|
+
// the reflection layer so a composition without it still installs.
|
|
1081
|
+
void runSelfCheck(raw, {
|
|
1082
|
+
handle,
|
|
1083
|
+
log: logWarn,
|
|
1084
|
+
info: (format, ...args) => { ctx.logger?.info(`dsh-zen-remote ${format}`, ...args) },
|
|
1085
|
+
getPeer: createOwnerPeerSource(() => gatewayCtx.reflect.get('connection')),
|
|
1086
|
+
})
|
|
1072
1087
|
})
|
|
1073
1088
|
// The exact-fetch-route interception (T51): the local backend's
|
|
1074
1089
|
// `/api/session/uploadFileBinary` entry forwards REMOTE sessions'
|
package/src/intercept-shape.ts
CHANGED
|
@@ -23,6 +23,11 @@ export type GatewayShapeCheck = { ok: true; notes: string[] } | { ok: false; rea
|
|
|
23
23
|
* §2.2/§4.1). If these strings vanish from the constructor source, own
|
|
24
24
|
* properties would silently stop intercepting. */
|
|
25
25
|
const DYNAMIC_OPEN_CALL = 'this.openWireStream(endpoint, payload, uplink, peer, control.signal, control)'
|
|
26
|
+
/** DSH 0.2.1-alpha.3 threads the mux's per-stream uplink window through as a
|
|
27
|
+
* seventh argument (`uplinkWindow`); the call site is otherwise unchanged. */
|
|
28
|
+
const DYNAMIC_OPEN_CALL_WINDOW = 'this.openWireStream(endpoint, payload, uplink, peer, control.signal, control, window)'
|
|
29
|
+
/** Accepted `openWireStream` arities: 6 through 0.2.1-alpha.2, 7 from alpha.3. */
|
|
30
|
+
const OPEN_ARITIES = [6, 7]
|
|
26
31
|
const DYNAMIC_RPC_CALL = 'this.dispatchRpc(endpoint, payload, signal, peer)'
|
|
27
32
|
|
|
28
33
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -48,7 +53,9 @@ export function checkGatewayShape(raw: unknown): GatewayShapeCheck {
|
|
|
48
53
|
// already installed.
|
|
49
54
|
const open = proto === null ? undefined : proto.openWireStream
|
|
50
55
|
if (typeof open !== 'function') reasons.push('prototype: openWireStream is not a function')
|
|
51
|
-
else if (open.length
|
|
56
|
+
else if (!OPEN_ARITIES.includes(open.length)) {
|
|
57
|
+
reasons.push(`prototype: openWireStream has ${open.length} parameters, expected ${OPEN_ARITIES.join(' or ')}`)
|
|
58
|
+
}
|
|
52
59
|
const dispatch = proto === null ? undefined : proto.dispatchRpc
|
|
53
60
|
if (typeof dispatch !== 'function') reasons.push('prototype: dispatchRpc is not a function')
|
|
54
61
|
else if (dispatch.length !== 4) reasons.push(`prototype: dispatchRpc has ${dispatch.length} parameters, expected 4`)
|
|
@@ -65,8 +72,8 @@ export function checkGatewayShape(raw: unknown): GatewayShapeCheck {
|
|
|
65
72
|
if (constructorSource === '') {
|
|
66
73
|
reasons.push('constructor: the prototype has no constructor to inspect')
|
|
67
74
|
} else {
|
|
68
|
-
if (!constructorSource.includes(DYNAMIC_OPEN_CALL)) {
|
|
69
|
-
reasons.push(`constructor: source does not contain "${DYNAMIC_OPEN_CALL}"`)
|
|
75
|
+
if (!constructorSource.includes(DYNAMIC_OPEN_CALL) && !constructorSource.includes(DYNAMIC_OPEN_CALL_WINDOW)) {
|
|
76
|
+
reasons.push(`constructor: source does not contain "${DYNAMIC_OPEN_CALL}" or "${DYNAMIC_OPEN_CALL_WINDOW}"`)
|
|
70
77
|
}
|
|
71
78
|
if (!constructorSource.includes(DYNAMIC_RPC_CALL)) {
|
|
72
79
|
reasons.push(`constructor: source does not contain "${DYNAMIC_RPC_CALL}"`)
|
package/src/intercept.ts
CHANGED
|
@@ -3212,6 +3212,9 @@ export function installIntercept(options: InstallInterceptOptions): InterceptHan
|
|
|
3212
3212
|
peer: unknown,
|
|
3213
3213
|
signal: AbortSignal | undefined,
|
|
3214
3214
|
control: unknown,
|
|
3215
|
+
// DSH 0.2.1-alpha.3: the mux's uplink window, passed through verbatim
|
|
3216
|
+
// (undefined on older hosts and on the 5-argument wireStream.open path).
|
|
3217
|
+
uplinkWindow?: unknown,
|
|
3215
3218
|
): unknown {
|
|
3216
3219
|
if (!installed) {
|
|
3217
3220
|
return (chainTarget.openWireStream as (...callArgs: unknown[]) => unknown).call(
|
|
@@ -3222,6 +3225,7 @@ export function installIntercept(options: InstallInterceptOptions): InterceptHan
|
|
|
3222
3225
|
peer,
|
|
3223
3226
|
signal,
|
|
3224
3227
|
control,
|
|
3228
|
+
uplinkWindow,
|
|
3225
3229
|
)
|
|
3226
3230
|
}
|
|
3227
3231
|
counters.openWireStream += 1
|
|
@@ -3235,7 +3239,7 @@ export function installIntercept(options: InstallInterceptOptions): InterceptHan
|
|
|
3235
3239
|
if (endpoint === '$events') {
|
|
3236
3240
|
const open = chainTarget.openWireStream as (this: unknown, ...callArgs: unknown[]) => unknown
|
|
3237
3241
|
return (async (): Promise<unknown> => {
|
|
3238
|
-
const local = await open.call(raw, endpoint, payload, uplink, peer, signal, control)
|
|
3242
|
+
const local = await open.call(raw, endpoint, payload, uplink, peer, signal, control, uplinkWindow)
|
|
3239
3243
|
if (!isAsyncIterable(local)) return local
|
|
3240
3244
|
return mergedEventsStream({ local, relay, signal, recordFailure, log, clock, summarySync })
|
|
3241
3245
|
})()
|
|
@@ -3256,6 +3260,7 @@ export function installIntercept(options: InstallInterceptOptions): InterceptHan
|
|
|
3256
3260
|
peer,
|
|
3257
3261
|
signal,
|
|
3258
3262
|
control,
|
|
3263
|
+
uplinkWindow,
|
|
3259
3264
|
)
|
|
3260
3265
|
}
|
|
3261
3266
|
// The T23b-2 global merge. The 0.2.0 openWireStream is an ASYNC
|
|
@@ -3269,7 +3274,7 @@ export function installIntercept(options: InstallInterceptOptions): InterceptHan
|
|
|
3269
3274
|
return (async (): Promise<unknown> => {
|
|
3270
3275
|
// The uplink rides to the LOCAL method verbatim (it may consume
|
|
3271
3276
|
// it); the remote leg carries no uplink.
|
|
3272
|
-
const local = await open.call(raw, endpoint, payload, uplink, peer, signal, control)
|
|
3277
|
+
const local = await open.call(raw, endpoint, payload, uplink, peer, signal, control, uplinkWindow)
|
|
3273
3278
|
if (!isAsyncIterable(local)) return local
|
|
3274
3279
|
return mergedGlobalStream({
|
|
3275
3280
|
endpoint,
|
|
@@ -3295,6 +3300,7 @@ export function installIntercept(options: InstallInterceptOptions): InterceptHan
|
|
|
3295
3300
|
peer,
|
|
3296
3301
|
signal,
|
|
3297
3302
|
control,
|
|
3303
|
+
uplinkWindow,
|
|
3298
3304
|
)
|
|
3299
3305
|
}
|
|
3300
3306
|
const verdict = validate(endpoint, virtuals)
|
|
@@ -3399,6 +3405,11 @@ export function installIntercept(options: InstallInterceptOptions): InterceptHan
|
|
|
3399
3405
|
export interface SelfCheckOptions {
|
|
3400
3406
|
/** How long the probe may wait for its first frame; default 5000 ms. */
|
|
3401
3407
|
timeoutMs?: number
|
|
3408
|
+
/** The owner Peer the probe opens as. DSH 0.2.1-alpha.3 removed
|
|
3409
|
+
* `gateway.operatorPeer()` and requires a live Peer on every stream; see
|
|
3410
|
+
* owner-peer.ts. Undefined falls back to `operatorPeer()` where it still
|
|
3411
|
+
* exists (≤ alpha.2), else to no Peer. */
|
|
3412
|
+
getPeer?: () => unknown
|
|
3402
3413
|
}
|
|
3403
3414
|
|
|
3404
3415
|
/**
|
|
@@ -3430,7 +3441,15 @@ export async function behaviorSelfCheck(raw: object, options?: SelfCheckOptions)
|
|
|
3430
3441
|
const wireStream = gateway.wireStream
|
|
3431
3442
|
const open = isPlainObject(wireStream) ? wireStream.open : undefined
|
|
3432
3443
|
if (typeof open !== 'function') return { ok: false, reason: 'wireStream.open disappeared before the self-check' }
|
|
3433
|
-
|
|
3444
|
+
// ≤ 0.2.1-alpha.2 only: alpha.3 deleted operatorPeer (and connection.operator).
|
|
3445
|
+
const legacyOperatorPeer = typeof gateway.operatorPeer === 'function' ? gateway.operatorPeer : undefined
|
|
3446
|
+
let peer: unknown
|
|
3447
|
+
try {
|
|
3448
|
+
peer = options?.getPeer?.()
|
|
3449
|
+
} catch (error) {
|
|
3450
|
+
return { ok: false, reason: `the self-check could not obtain an owner Peer: ${messageOf(error)}` }
|
|
3451
|
+
}
|
|
3452
|
+
if (peer === undefined && legacyOperatorPeer !== undefined) peer = legacyOperatorPeer.call(raw)
|
|
3434
3453
|
const controller = new AbortController()
|
|
3435
3454
|
// NOT unref'd: an unref'd timer lets the process exit (or a test runner
|
|
3436
3455
|
// judge the loop empty) before the timeout ever fires when this probe is
|
|
@@ -3458,7 +3477,7 @@ export async function behaviorSelfCheck(raw: object, options?: SelfCheckOptions)
|
|
|
3458
3477
|
'workspace/follow',
|
|
3459
3478
|
payload,
|
|
3460
3479
|
undefined,
|
|
3461
|
-
|
|
3480
|
+
peer,
|
|
3462
3481
|
controller.signal,
|
|
3463
3482
|
),
|
|
3464
3483
|
),
|
|
@@ -3502,6 +3521,10 @@ export interface RunSelfCheckOptions {
|
|
|
3502
3521
|
retryDelayMs?: number
|
|
3503
3522
|
/** Failure logging, wired to the context logger by index.ts. */
|
|
3504
3523
|
log?: (format: string, ...args: unknown[]) => void
|
|
3524
|
+
/** Success logging (one line once the probe passes). */
|
|
3525
|
+
info?: (format: string, ...args: unknown[]) => void
|
|
3526
|
+
/** Forwarded to {@link behaviorSelfCheck}. */
|
|
3527
|
+
getPeer?: () => unknown
|
|
3505
3528
|
}
|
|
3506
3529
|
|
|
3507
3530
|
/**
|
|
@@ -3521,7 +3544,7 @@ export async function runSelfCheck(raw: object, options: RunSelfCheckOptions): P
|
|
|
3521
3544
|
const { handle, log } = options
|
|
3522
3545
|
const retryDelayMs = options.retryDelayMs ?? 3_000
|
|
3523
3546
|
const stillInstalled = (): boolean => handle.diagnostics().installed
|
|
3524
|
-
let result = await behaviorSelfCheck(raw)
|
|
3547
|
+
let result = await behaviorSelfCheck(raw, { getPeer: options.getPeer })
|
|
3525
3548
|
if (!result.ok && stillInstalled()) {
|
|
3526
3549
|
await new Promise<void>((resolve) => {
|
|
3527
3550
|
const timer = setTimeout(resolve, retryDelayMs)
|
|
@@ -3529,11 +3552,14 @@ export async function runSelfCheck(raw: object, options: RunSelfCheckOptions): P
|
|
|
3529
3552
|
// loop-empty judgement) open for the retry delay.
|
|
3530
3553
|
if (typeof timer.unref === 'function') timer.unref()
|
|
3531
3554
|
})
|
|
3532
|
-
if (stillInstalled()) result = await behaviorSelfCheck(raw)
|
|
3555
|
+
if (stillInstalled()) result = await behaviorSelfCheck(raw, { getPeer: options.getPeer })
|
|
3533
3556
|
}
|
|
3534
3557
|
if (!stillInstalled()) return result
|
|
3535
3558
|
handle.noteSelfCheck(result)
|
|
3536
|
-
if (result.ok)
|
|
3559
|
+
if (result.ok) {
|
|
3560
|
+
options.info?.('client intercept self-check passed; interception active')
|
|
3561
|
+
return result
|
|
3562
|
+
}
|
|
3537
3563
|
handle.uninstall()
|
|
3538
3564
|
log?.('client intercept self-check failed (%s), interception removed', result.reason)
|
|
3539
3565
|
return result
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The owner Peer this plugin speaks as when it calls the typert gateway on
|
|
3
|
+
* its own behalf (relay routes, admin projections, the intercept
|
|
4
|
+
* self-check, the `$events` relay subscription).
|
|
5
|
+
*
|
|
6
|
+
* DSH 0.2.1-alpha.3 removed the gateway's implicit operator fallback
|
|
7
|
+
* (`gateway.operatorPeer()`, `connection.operator` and the `OperatorPeer`
|
|
8
|
+
* export are gone): every gateway call — `invoke`, `stream`,
|
|
9
|
+
* `openWireStream`, `dispatchRpc` — now asserts a live Peer, and `$events` /
|
|
10
|
+
* `$events/result` additionally demand `principal.kind === 'owner'` and
|
|
11
|
+
* `interactions === 'answer'`, with the result answered by the SAME Peer that
|
|
12
|
+
* opened the stream. The sanctioned in-process door is
|
|
13
|
+
* `connection.createPeer({ principal: { kind: 'owner' }, interactions:
|
|
14
|
+
* 'answer' })` (docs/upgrade-guide/v0.2.1-alpha.2/operator-connect), whose
|
|
15
|
+
* Peer is disposed with the Context that read `connection`.
|
|
16
|
+
*
|
|
17
|
+
* Resolution order: a cached `createPeer` Peer (recreated once its scope is
|
|
18
|
+
* disposed), else `connection.browserPeer` (also an owner/answer Peer),
|
|
19
|
+
* else `undefined` — the pre-alpha.3 behavior, where the gateway itself
|
|
20
|
+
* defaulted a missing Peer to its operator.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Minimal structural view of a typert `PeerScope`. */
|
|
24
|
+
export interface PeerLike {
|
|
25
|
+
readonly signal?: AbortSignal
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The connection-service members this module reads. */
|
|
29
|
+
export interface PeerConnectionLike {
|
|
30
|
+
createPeer?: (options: { principal: { kind: 'owner' }; interactions?: 'answer' | 'none' }) => unknown
|
|
31
|
+
browserPeer?: unknown
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isLive(peer: unknown): boolean {
|
|
35
|
+
if (peer === null || typeof peer !== 'object') return false
|
|
36
|
+
const signal = (peer as PeerLike).signal
|
|
37
|
+
return !(signal instanceof AbortSignal) || !signal.aborted
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build a lazy, caching owner-Peer source over a connection-service reader.
|
|
42
|
+
* The reader runs per call (the service may be absent or restarted). The
|
|
43
|
+
* cache is keyed on the Peer's LIVENESS, not on the connection object's
|
|
44
|
+
* identity: `ctx.reflect.get('connection')` hands out a fresh proxy per read,
|
|
45
|
+
* so an identity key would mint a new Peer per call — and the gateway
|
|
46
|
+
* refuses a `$events/result` from any Peer but the one that opened
|
|
47
|
+
* `$events` (found by the alpha.3 live smoke). A restarted connection
|
|
48
|
+
* service disposes its Peers, which is exactly when a new one is minted.
|
|
49
|
+
* @param getConnection - live reader of `ctx.connection` (may return undefined).
|
|
50
|
+
* @returns a function answering the Peer to pass, or undefined to let the
|
|
51
|
+
* host default it (pre-alpha.3 hosts).
|
|
52
|
+
*/
|
|
53
|
+
export function createOwnerPeerSource(getConnection: () => unknown): () => unknown {
|
|
54
|
+
let cached: unknown
|
|
55
|
+
return () => {
|
|
56
|
+
let connection: PeerConnectionLike | undefined
|
|
57
|
+
try {
|
|
58
|
+
const value = getConnection()
|
|
59
|
+
connection = value !== null && typeof value === 'object' ? (value as PeerConnectionLike) : undefined
|
|
60
|
+
} catch {
|
|
61
|
+
connection = undefined
|
|
62
|
+
}
|
|
63
|
+
if (connection === undefined) return undefined
|
|
64
|
+
if (typeof connection.createPeer === 'function') {
|
|
65
|
+
if (isLive(cached)) return cached
|
|
66
|
+
cached = connection.createPeer({ principal: { kind: 'owner' }, interactions: 'answer' })
|
|
67
|
+
return cached
|
|
68
|
+
}
|
|
69
|
+
const browser = connection.browserPeer
|
|
70
|
+
return browser !== null && typeof browser === 'object' ? browser : undefined
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Attach `peer` to one gateway request object when a Peer is available;
|
|
76
|
+
* otherwise return the request unchanged (pre-alpha.3 shape).
|
|
77
|
+
*/
|
|
78
|
+
export function withPeer<T extends object>(request: T, getPeer: (() => unknown) | undefined): T & { peer?: unknown } {
|
|
79
|
+
const peer = getPeer?.()
|
|
80
|
+
return peer === undefined ? request : { ...request, peer }
|
|
81
|
+
}
|
package/src/relay-server.ts
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* a 404.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
+
import { withPeer } from './owner-peer.js'
|
|
29
30
|
import { randomBytes, timingSafeEqual } from 'node:crypto'
|
|
30
31
|
import { Readable, Transform } from 'node:stream'
|
|
31
32
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
|
|
@@ -208,12 +209,13 @@ const JOB_CACHE_LIMIT = 1024
|
|
|
208
209
|
* before its typert dispatch — and returns the `{ok,…}` envelope itself.
|
|
209
210
|
*/
|
|
210
211
|
export interface RelayGateway {
|
|
211
|
-
invoke(call: { namespace: string; method: string; args: unknown; signal?: AbortSignal }): Promise<unknown>
|
|
212
|
+
invoke(call: { namespace: string; method: string; args: unknown; signal?: AbortSignal; peer?: unknown }): Promise<unknown>
|
|
212
213
|
stream(call: {
|
|
213
214
|
namespace: string
|
|
214
215
|
method: string
|
|
215
216
|
args: unknown
|
|
216
217
|
signal?: AbortSignal
|
|
218
|
+
peer?: unknown
|
|
217
219
|
}): Promise<AsyncIterable<unknown>>
|
|
218
220
|
/** The host's wire adapter, shared by the WebSocket mux and the local
|
|
219
221
|
* transports. `open` is the async 0.2.0 method. */
|
|
@@ -268,6 +270,12 @@ export interface RelayHandlerOptions {
|
|
|
268
270
|
* synthetic Request to the same exact-fetch route table the browser's
|
|
269
271
|
* `/api` transport uses — no loopback HTTP, no login state. */
|
|
270
272
|
getApiFetch?: () => ((request: Request) => Promise<Response>) | undefined
|
|
273
|
+
/** The owner Peer every gateway call is made as (DSH 0.2.1-alpha.3+
|
|
274
|
+
* requires one; `$events` and `$events/result` need an owner Peer with
|
|
275
|
+
* `interactions: 'answer'`, the SAME one for both). Built by
|
|
276
|
+
* {@link createOwnerPeerSource}; absent or answering undefined keeps the
|
|
277
|
+
* pre-alpha.3 behavior (the host defaults the Peer). */
|
|
278
|
+
getPeer?: () => unknown
|
|
271
279
|
/** Stream heartbeat interval in ms (a `{"type":"ping"}` line that keeps
|
|
272
280
|
* reverse proxies from timing the idle stream away); defaults to 15000.
|
|
273
281
|
* Tests inject a small value. */
|
|
@@ -875,12 +883,12 @@ export function createRelayHandler(options: RelayHandlerOptions): RelayHandler {
|
|
|
875
883
|
const cached = recentJobs.get(sessionId)
|
|
876
884
|
if (cached !== undefined && cached.has(jobId)) return cached.get(jobId) === sessionId
|
|
877
885
|
try {
|
|
878
|
-
const iterable = await gateway.stream({
|
|
886
|
+
const iterable = await gateway.stream(withPeer({
|
|
879
887
|
namespace: 'job',
|
|
880
888
|
method: 'list',
|
|
881
889
|
args: { request: { sessionId } },
|
|
882
890
|
signal,
|
|
883
|
-
})
|
|
891
|
+
}, options.getPeer))
|
|
884
892
|
for await (const frame of iterable) {
|
|
885
893
|
rememberJobs(sessionId, frame)
|
|
886
894
|
const jobs = recentJobs.get(sessionId)
|
|
@@ -906,7 +914,7 @@ export function createRelayHandler(options: RelayHandlerOptions): RelayHandler {
|
|
|
906
914
|
*/
|
|
907
915
|
const workspaceExists = async (workspaceId: string, signal: AbortSignal): Promise<boolean> => {
|
|
908
916
|
try {
|
|
909
|
-
const iterable = await gateway.stream({ namespace: 'workspace', method: 'follow', args: {}, signal })
|
|
917
|
+
const iterable = await gateway.stream(withPeer({ namespace: 'workspace', method: 'follow', args: {}, signal }, options.getPeer))
|
|
910
918
|
for await (const frame of iterable) {
|
|
911
919
|
if (!isPlainObject(frame) || frame.type !== 'baseline') return false
|
|
912
920
|
const value = isPlainObject(frame.value) ? frame.value : undefined
|
|
@@ -939,7 +947,7 @@ export function createRelayHandler(options: RelayHandlerOptions): RelayHandler {
|
|
|
939
947
|
'$events',
|
|
940
948
|
{ args: {} },
|
|
941
949
|
undefined,
|
|
942
|
-
|
|
950
|
+
options.getPeer?.(),
|
|
943
951
|
signal,
|
|
944
952
|
)) as unknown
|
|
945
953
|
if (
|
|
@@ -1217,7 +1225,7 @@ export function createRelayHandler(options: RelayHandlerOptions): RelayHandler {
|
|
|
1217
1225
|
'$events/result',
|
|
1218
1226
|
{ args: { clientId: owner.clientId, eventId: originalEventId, outcome: parsed.result } },
|
|
1219
1227
|
hangUp.signal,
|
|
1220
|
-
|
|
1228
|
+
options.getPeer?.(),
|
|
1221
1229
|
)) as unknown
|
|
1222
1230
|
responseJson(res, 200, isPlainObject(envelope) ? envelope : { ok: false, error: { code: 'internal' } })
|
|
1223
1231
|
} catch (error) {
|
|
@@ -1341,7 +1349,7 @@ export function createRelayHandler(options: RelayHandlerOptions): RelayHandler {
|
|
|
1341
1349
|
responseJson(res, 403, { ok: false, error: { code: 'not-shared' } })
|
|
1342
1350
|
return
|
|
1343
1351
|
}
|
|
1344
|
-
const value = await gateway.invoke({ namespace, method, args, signal: hangUp.signal })
|
|
1352
|
+
const value = await gateway.invoke(withPeer({ namespace, method, args, signal: hangUp.signal }, options.getPeer))
|
|
1345
1353
|
// T31: a session created or forked THROUGH the relay is shared
|
|
1346
1354
|
// automatically — the client only ever sees shared sessions, so an
|
|
1347
1355
|
// unshared result would be born unreachable. Re-sharing (a fork the
|
|
@@ -2471,12 +2479,12 @@ export function createRelayHandler(options: RelayHandlerOptions): RelayHandler {
|
|
|
2471
2479
|
const syncProjections = async (sessionId: string): Promise<void> => {
|
|
2472
2480
|
let result: unknown
|
|
2473
2481
|
try {
|
|
2474
|
-
result = await gateway.invoke({
|
|
2482
|
+
result = await gateway.invoke(withPeer({
|
|
2475
2483
|
namespace: 'session',
|
|
2476
2484
|
method: 'projections',
|
|
2477
2485
|
args: { request: { sessionId } },
|
|
2478
2486
|
signal: hangUp.signal,
|
|
2479
|
-
})
|
|
2487
|
+
}, options.getPeer))
|
|
2480
2488
|
} catch {
|
|
2481
2489
|
return // projections unavailable — the next live projection frame will flow anyway
|
|
2482
2490
|
}
|
|
@@ -2596,7 +2604,7 @@ export function createRelayHandler(options: RelayHandlerOptions): RelayHandler {
|
|
|
2596
2604
|
const iterable =
|
|
2597
2605
|
eventsSub !== undefined
|
|
2598
2606
|
? await openEventsStream(hangUp.signal)
|
|
2599
|
-
: await gateway.stream({ namespace, method, args, signal: hangUp.signal })
|
|
2607
|
+
: await gateway.stream(withPeer({ namespace, method, args, signal: hangUp.signal }, options.getPeer))
|
|
2600
2608
|
for await (const frame of iterable) {
|
|
2601
2609
|
if (finished || clientGone) break
|
|
2602
2610
|
if (namespace === 'job' && method === 'list') rememberJobs(ownerSessionIdOf(args), frame)
|