dsh-zen-remote 2.3.6 → 2.3.8
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 -5
- package/CHANGELOG.md +83 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/docs/interface.md +10 -4
- package/docs/remote-access.en.md +6 -0
- package/docs/remote-access.md +6 -0
- package/lan-gate.mjs +7 -0
- package/lib/client-pairing.js +10 -0
- package/lib/client-pairing.js.map +1 -1
- package/lib/client-routes.js +183 -94
- package/lib/client-routes.js.map +1 -1
- package/lib/client.js +505 -78
- package/lib/debug-redact.js +253 -0
- package/lib/debug-redact.js.map +1 -0
- package/lib/debug-report.js +60 -0
- package/lib/debug-report.js.map +1 -0
- package/lib/fetch-route-intercept.js +23 -0
- package/lib/fetch-route-intercept.js.map +1 -1
- package/lib/index.js +293 -45
- package/lib/index.js.map +1 -1
- package/lib/log-ring.js +76 -0
- package/lib/log-ring.js.map +1 -0
- package/lib/merge-streams.js +55 -19
- package/lib/merge-streams.js.map +1 -1
- package/lib/relay-filter.js +4 -3
- package/lib/relay-filter.js.map +1 -1
- package/lib/relay-server.js +5 -3
- package/lib/relay-server.js.map +1 -1
- package/lib/types/client/effects/remote-drop.d.ts +20 -8
- package/lib/types/client/effects/remote-drop.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +8 -1
- 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/misc.css.d.ts +1 -1
- package/lib/types/client/styles/misc.css.d.ts.map +1 -1
- package/lib/types/client-data/remote-drop.d.ts +64 -0
- package/lib/types/client-data/remote-drop.d.ts.map +1 -1
- package/lib/types/client-data/settings-form.d.ts +3 -0
- package/lib/types/client-data/settings-form.d.ts.map +1 -1
- package/lib/types/client-pairing.d.ts +8 -0
- package/lib/types/client-pairing.d.ts.map +1 -1
- package/lib/types/client-routes.d.ts +23 -0
- package/lib/types/client-routes.d.ts.map +1 -1
- package/lib/types/debug-redact.d.ts +68 -0
- package/lib/types/debug-redact.d.ts.map +1 -0
- package/lib/types/debug-report.d.ts +58 -0
- package/lib/types/debug-report.d.ts.map +1 -0
- package/lib/types/fetch-route-intercept.d.ts +26 -0
- package/lib/types/fetch-route-intercept.d.ts.map +1 -1
- package/lib/types/index.d.ts +7 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/log-ring.d.ts +57 -0
- package/lib/types/log-ring.d.ts.map +1 -0
- package/lib/types/merge-streams.d.ts +27 -0
- package/lib/types/merge-streams.d.ts.map +1 -1
- package/lib/types/relay-filter.d.ts +4 -3
- package/lib/types/relay-filter.d.ts.map +1 -1
- package/lib/types/relay-server.d.ts.map +1 -1
- package/package.json +6 -3
- package/pwa/app.css +13 -2
- package/scripts/check-debug-report.mjs +277 -0
- package/scripts/check-remote-drop.mjs +130 -0
- package/scripts/check-tablet-safe-area.mjs +49 -0
- package/src/client/effects/remote-drop.ts +271 -24
- package/src/client/locales.ts +20 -4
- package/src/client/settings/SettingsSection.tsx +120 -3
- package/src/client/styles/misc.css.ts +38 -0
- package/src/client-data/remote-drop.ts +103 -0
- package/src/client-data/settings-form.ts +3 -0
- package/src/client-pairing.ts +11 -0
- package/src/client-routes.ts +196 -90
- package/src/debug-redact.ts +290 -0
- package/src/debug-report.ts +93 -0
- package/src/fetch-route-intercept.ts +29 -0
- package/src/index.ts +282 -43
- package/src/log-ring.ts +101 -0
- package/src/merge-streams.ts +71 -16
- package/src/relay-filter.ts +4 -3
- package/src/relay-server.ts +5 -3
package/src/log-ring.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CU-1: the plugin's in-process diagnostic log ring (SPEC §方案 C). A bounded
|
|
3
|
+
* FIFO of the plugin row's own log lines, kept in memory only — the desktop
|
|
4
|
+
* app writes no log file, so this is the one artifact the debug report (CU-2)
|
|
5
|
+
* can read on either role. Deliberately NOT a file, and deliberately only
|
|
6
|
+
* this plugin's lines: DSH's own log stream stays untouched.
|
|
7
|
+
*
|
|
8
|
+
* Pure module: no DSH imports, no I/O, no clocks beyond the timestamp it
|
|
9
|
+
* stamps entries with. The wiring lives in src/index.ts, which funnels its
|
|
10
|
+
* `logWarn` / `logInfo` through {@link createPluginLog} so existing call
|
|
11
|
+
* sites enter the ring unchanged (double write: logger first, ring second),
|
|
12
|
+
* and pushes the handful of lifecycle events (row apply/reload, gateway
|
|
13
|
+
* spawn/exit, relay link flips, fetch-route installs) by hand.
|
|
14
|
+
*
|
|
15
|
+
* One discipline the debug report leans on: ring lines carry event codes,
|
|
16
|
+
* id shapes and counters — never payloads. The redactor (debug-redact.ts)
|
|
17
|
+
* is the second layer, not the license to log secrets here.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { format } from 'node:util'
|
|
21
|
+
|
|
22
|
+
/** Severity of one ring entry. The ring only distinguishes what the report
|
|
23
|
+
* renders: info lines and warnings. */
|
|
24
|
+
export type LogLevel = 'info' | 'warn'
|
|
25
|
+
|
|
26
|
+
/** One ring entry: ISO timestamp, level, fully interpolated message. */
|
|
27
|
+
export interface LogRingEntry {
|
|
28
|
+
t: string
|
|
29
|
+
level: LogLevel
|
|
30
|
+
msg: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Longest message kept, in characters (UTF-16 code units). Debug lines are
|
|
34
|
+
* codes and shapes, not dumps — anything longer was the wrong thing to log. */
|
|
35
|
+
const MAX_MSG_CHARS = 512
|
|
36
|
+
|
|
37
|
+
/** The ring's face. `push` never throws and never returns anything; callers
|
|
38
|
+
* log through it exactly like they would through the context logger. */
|
|
39
|
+
export interface LogRing {
|
|
40
|
+
push(level: LogLevel, formatText: string, ...args: unknown[]): void
|
|
41
|
+
/** Oldest-last copy of the ring. Mutating the returned array or its
|
|
42
|
+
* entries never reaches the ring's own state. */
|
|
43
|
+
snapshot(): LogRingEntry[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create one bounded log ring. `cap` defaults to 300 entries — a full debug
|
|
48
|
+
* session of lifecycle lines plus a healthy share of warnings, while keeping
|
|
49
|
+
* the report a fixed small size. A cap below 1 is clamped to 1.
|
|
50
|
+
*/
|
|
51
|
+
export function createLogRing(cap = 300): LogRing {
|
|
52
|
+
const capacity = Number.isFinite(cap) && cap >= 1 ? Math.floor(cap) : 1
|
|
53
|
+
const entries: LogRingEntry[] = []
|
|
54
|
+
return {
|
|
55
|
+
push(level, formatText, ...args) {
|
|
56
|
+
let msg: string
|
|
57
|
+
try {
|
|
58
|
+
msg = args.length === 0 ? String(formatText) : format(formatText, ...args)
|
|
59
|
+
} catch {
|
|
60
|
+
// A malformed format string must never break the caller's flow —
|
|
61
|
+
// keep the raw template as the message.
|
|
62
|
+
msg = String(formatText)
|
|
63
|
+
}
|
|
64
|
+
if (msg.length > MAX_MSG_CHARS) msg = msg.slice(0, MAX_MSG_CHARS)
|
|
65
|
+
entries.push({ t: new Date().toISOString(), level, msg })
|
|
66
|
+
if (entries.length > capacity) entries.splice(0, entries.length - capacity)
|
|
67
|
+
},
|
|
68
|
+
snapshot() {
|
|
69
|
+
return entries.map((entry) => ({ ...entry }))
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The logger face this module needs: the cordis context logger's two
|
|
75
|
+
* methods, each optional (test fakes carry none). */
|
|
76
|
+
export interface PluginLogLike {
|
|
77
|
+
info?(format: string, ...args: unknown[]): void
|
|
78
|
+
warn?(format: string, ...args: unknown[]): void
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The double-writer the apply() wiring funnels its logging through: every
|
|
82
|
+
* line reaches the context logger (unchanged behavior, unchanged call
|
|
83
|
+
* sites) AND the ring (the debug report's source). A ring failure is
|
|
84
|
+
* swallowed; a logger failure propagates exactly as it did before this
|
|
85
|
+
* module existed. */
|
|
86
|
+
export function createPluginLog(ring: LogRing, logger: PluginLogLike | undefined): { info(format: string, ...args: unknown[]): void, warn(format: string, ...args: unknown[]): void } {
|
|
87
|
+
return {
|
|
88
|
+
info(formatText, ...args) {
|
|
89
|
+
logger?.info?.(formatText, ...args)
|
|
90
|
+
try {
|
|
91
|
+
ring.push('info', formatText, ...args)
|
|
92
|
+
} catch { /* the ring must never break logging */ }
|
|
93
|
+
},
|
|
94
|
+
warn(formatText, ...args) {
|
|
95
|
+
logger?.warn?.(formatText, ...args)
|
|
96
|
+
try {
|
|
97
|
+
ring.push('warn', formatText, ...args)
|
|
98
|
+
} catch { /* the ring must never break logging */ }
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
}
|
package/src/merge-streams.ts
CHANGED
|
@@ -968,6 +968,49 @@ export interface ControlMerger {
|
|
|
968
968
|
retarget(identity: MergerIdentity): void
|
|
969
969
|
}
|
|
970
970
|
|
|
971
|
+
/** One `session/control` projection change, independent of the wire shape. */
|
|
972
|
+
export interface ControlProjectionChange {
|
|
973
|
+
key: string
|
|
974
|
+
value: unknown
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Read the changes of one live `session/control` `projection` frame in either
|
|
979
|
+
* wire shape. DSH 0.2.2-alpha.3 batched the frame per committed event —
|
|
980
|
+
* `{type:'projection', sessionId, seq, changes:[{key, val}]}` (its client
|
|
981
|
+
* face's handleControlFrame iterates `frame.changes`) — where 0.2.1 and
|
|
982
|
+
* 0.2.2-alpha.2 send one key per frame, `{type:'projection', sessionId, key,
|
|
983
|
+
* value, seq}` (that client face reads `frame.key` / `frame.value`). A
|
|
984
|
+
* `changes` array wins when present; `undefined` means neither shape is
|
|
985
|
+
* usable (a malformed entry spoils the whole frame, like every other
|
|
986
|
+
* malformed frame here).
|
|
987
|
+
*/
|
|
988
|
+
export function controlProjectionChanges(frame: Record<string, unknown>): ControlProjectionChange[] | undefined {
|
|
989
|
+
if (Array.isArray(frame.changes)) {
|
|
990
|
+
const out: ControlProjectionChange[] = []
|
|
991
|
+
for (const change of frame.changes) {
|
|
992
|
+
if (!isPlainObject(change) || typeof change.key !== 'string' || !('val' in change)) return undefined
|
|
993
|
+
out.push({ key: change.key, value: change.val })
|
|
994
|
+
}
|
|
995
|
+
return out
|
|
996
|
+
}
|
|
997
|
+
if (typeof frame.key === 'string' && 'value' in frame) return [{ key: frame.key, value: frame.value }]
|
|
998
|
+
return undefined
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* One single-key `session/control` projection frame that BOTH client
|
|
1003
|
+
* generations apply: the per-key `key` / `value` fields for DSH ≤ 0.2.2-alpha.2
|
|
1004
|
+
* and a one-entry `changes` list for ≥ 0.2.2-alpha.3. Neither client face
|
|
1005
|
+
* validates stream frames (the gateway client only runs a binary `decode`),
|
|
1006
|
+
* and each reads only its own fields, so the extra ones are inert. Splitting a
|
|
1007
|
+
* batched frame into one frame per key at the same seq is lossless: the
|
|
1008
|
+
* projection store applies per key (`seq <= row.seq` per row).
|
|
1009
|
+
*/
|
|
1010
|
+
export function controlProjectionFrame(sessionId: string, key: string, value: unknown, seq: number): Record<string, unknown> {
|
|
1011
|
+
return { type: 'projection', sessionId, key, value, seq, changes: [{ key, val: value }] }
|
|
1012
|
+
}
|
|
1013
|
+
|
|
971
1014
|
/**
|
|
972
1015
|
* The session/control merger. The control stream is key-value projection
|
|
973
1016
|
* traffic with no ordering constraints between sessions, so it needs no
|
|
@@ -1016,13 +1059,14 @@ export function createControlMerger(options: ControlMergerOptions): ControlMerge
|
|
|
1016
1059
|
// UNCONDITIONALLY (T52-fix3): the host's projection store is
|
|
1017
1060
|
// first-write-wins per seq, so a catalog-gated original value would
|
|
1018
1061
|
// occupy the seq and the later virtual rewrite could never land.
|
|
1019
|
-
out.push(
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1062
|
+
out.push(
|
|
1063
|
+
controlProjectionFrame(
|
|
1064
|
+
virtualize(sessionId),
|
|
1065
|
+
key,
|
|
1066
|
+
key === 'modelSelection' ? virtualizeModelSelectionValue(values[key], identity.serverId) : values[key],
|
|
1067
|
+
asOfSeq,
|
|
1068
|
+
),
|
|
1069
|
+
)
|
|
1026
1070
|
}
|
|
1027
1071
|
}
|
|
1028
1072
|
return out
|
|
@@ -1038,18 +1082,29 @@ export function createControlMerger(options: ControlMergerOptions): ControlMerge
|
|
|
1038
1082
|
onDiagnostic?.(`dropped a session/control projection frame with unusable seq ${String(frame.seq)}`)
|
|
1039
1083
|
return []
|
|
1040
1084
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1085
|
+
// DSH 0.2.2-alpha.3 batches one event's changes into `changes[]`;
|
|
1086
|
+
// older hosts send one key per frame. Read either, then emit one
|
|
1087
|
+
// dual-shape frame per key so a local UI of EITHER generation applies
|
|
1088
|
+
// it (a mixed local/server pair is only a fingerprint warning, the
|
|
1089
|
+
// stream still flows).
|
|
1090
|
+
const changes = controlProjectionChanges(frame)
|
|
1091
|
+
if (changes === undefined) {
|
|
1092
|
+
onDiagnostic?.('dropped a session/control projection frame with neither key/value nor a changes list')
|
|
1093
|
+
return []
|
|
1094
|
+
}
|
|
1095
|
+
const sessionId = virtualize(frame.sessionId)
|
|
1096
|
+
const seq = frame.seq
|
|
1097
|
+
return changes.map(({ key, value }) =>
|
|
1098
|
+
controlProjectionFrame(
|
|
1099
|
+
sessionId,
|
|
1100
|
+
key,
|
|
1045
1101
|
// The modelSelection update's providers go virtual with the session
|
|
1046
1102
|
// id (T52) — same unconditional rewrite as the exploded baseline
|
|
1047
1103
|
// above (T52-fix3: first-write-wins per seq forbids a catalog gate).
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
]
|
|
1104
|
+
key === 'modelSelection' ? virtualizeModelSelectionValue(value, identity.serverId) : value,
|
|
1105
|
+
seq,
|
|
1106
|
+
),
|
|
1107
|
+
)
|
|
1053
1108
|
}
|
|
1054
1109
|
onDiagnostic?.(`dropped a session/control frame of unknown type ${String(frame.type)}`)
|
|
1055
1110
|
return []
|
package/src/relay-filter.ts
CHANGED
|
@@ -88,9 +88,10 @@ export function filterWorkspaceFrame(frame: unknown, isAccessible: Accessibility
|
|
|
88
88
|
/**
|
|
89
89
|
* Filter one `session/control` frame. Exactly two frame types exist:
|
|
90
90
|
* `baseline{value:{projections: Record<sessionId, {asOfSeq, values}>}}` — the
|
|
91
|
-
* per-session map keeps only accessible keys — and `projection{sessionId,
|
|
92
|
-
* value, seq
|
|
93
|
-
* dropped
|
|
91
|
+
* per-session map keeps only accessible keys — and `projection{sessionId, …}`
|
|
92
|
+
* (per-key `key, value, seq` up to DSH 0.2.2-alpha.2, batched `seq,
|
|
93
|
+
* changes[]` from alpha.3) — kept whole or dropped by its session id, so either
|
|
94
|
+
* shape passes untouched. Anything else is dropped.
|
|
94
95
|
*
|
|
95
96
|
* The baseline record is rebuilt with `defineProperty` because a session id of
|
|
96
97
|
* `__proto__` must become an own data property of the new record, not fire the
|
package/src/relay-server.ts
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
import { withPeer } from './owner-peer.js'
|
|
30
|
+
import { controlProjectionFrame } from './merge-streams.js'
|
|
30
31
|
import { randomBytes, timingSafeEqual } from 'node:crypto'
|
|
31
32
|
import { Readable, Transform } from 'node:stream'
|
|
32
33
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
|
|
@@ -2508,8 +2509,9 @@ export function createRelayHandler(options: RelayHandlerOptions): RelayHandler {
|
|
|
2508
2509
|
})
|
|
2509
2510
|
} else if (decision.filter === 'control') {
|
|
2510
2511
|
// A newly shared session needs its current projections: fetch
|
|
2511
|
-
// them once and emit one synthesized `projection` frame per key
|
|
2512
|
-
//
|
|
2512
|
+
// them once and emit one synthesized `projection` frame per key in
|
|
2513
|
+
// the dual shape both DSH generations read (per-key key/value for
|
|
2514
|
+
// ≤ 0.2.2-alpha.2, a one-entry `changes` list for ≥ alpha.3). An unshared session
|
|
2513
2515
|
// synthesizes nothing — the client drops it via the workspace
|
|
2514
2516
|
// stream's re-filtered upserts.
|
|
2515
2517
|
const syncProjections = async (sessionId: string): Promise<void> => {
|
|
@@ -2528,7 +2530,7 @@ export function createRelayHandler(options: RelayHandlerOptions): RelayHandler {
|
|
|
2528
2530
|
const values = isPlainObject(result.values) ? result.values : {}
|
|
2529
2531
|
const seq = typeof result.asOfSeq === 'number' ? result.asOfSeq : 0
|
|
2530
2532
|
for (const key of Object.keys(values)) {
|
|
2531
|
-
void writeLine({ type: 'frame', frame:
|
|
2533
|
+
void writeLine({ type: 'frame', frame: controlProjectionFrame(sessionId, key, values[key], seq) })
|
|
2532
2534
|
}
|
|
2533
2535
|
}
|
|
2534
2536
|
unsubscribe = store.subscribe((event) => {
|