dsh-cherry-bridge 0.1.10 → 0.2.0
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.en.md +12 -5
- package/README.md +11 -5
- package/dist/index.d.mts +31 -3
- package/dist/index.mjs +1363 -170
- package/docs/e2e-checklist.md +9 -2
- package/lib/client.js +47 -0
- package/package.json +4 -3
- package/src/AGENTS.md +860 -0
- package/src/connection.ts +114 -10
- package/src/deviceStore.ts +204 -5
- package/src/diagnostics.ts +30 -0
- package/src/httpApi.ts +129 -8
- package/src/index.ts +190 -8
- package/src/pairing.ts +504 -112
- package/src/rpc/jsonRpcClient.ts +19 -1
- package/src/subscriptions.ts +10 -2
- package/src/text.ts +158 -0
- package/src/tools.ts +803 -56
- package/static/settings-devices.jpg +0 -0
- package/static/settings-pairing-scope.jpg +0 -0
- package/ui/AGENTS.md +23 -0
- package/ui/src/DeviceCard.tsx +35 -0
- package/ui/src/i18n.ts +10 -0
- package/ui/src/types.ts +9 -1
- package/src/urlHost.ts +0 -15
package/src/rpc/jsonRpcClient.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
remoteFailureSchema,
|
|
6
6
|
} from '@cherrystudio/remote-protocol'
|
|
7
7
|
import type { SecureChannel } from '@cherrystudio/remote-transport'
|
|
8
|
+
import { failureText } from '../text.js'
|
|
8
9
|
|
|
9
10
|
/** Default cap on business requests in flight on a single connection. */
|
|
10
11
|
const DEFAULT_MAX_IN_FLIGHT = 8
|
|
@@ -558,7 +559,24 @@ export class JsonRpcClient {
|
|
|
558
559
|
try {
|
|
559
560
|
handler(method, params)
|
|
560
561
|
} catch (error) {
|
|
561
|
-
|
|
562
|
+
// BOUNDED, and ONE STRING (issue #155; rendered by the shared
|
|
563
|
+
// `failureText`, issue #161). The old form passed the error OBJECT as
|
|
564
|
+
// a second argument, so the console chose the rendering: the stack
|
|
565
|
+
// plus every own property, with nothing bounding it. A handler
|
|
566
|
+
// failure is not a payload, so the line carries what an operator
|
|
567
|
+
// needs and nothing else — the METHOD and the MESSAGE under
|
|
568
|
+
// LOG_TEXT_MAX. The method is already bounded twice over, without
|
|
569
|
+
// clipText: the notification schema caps `method` at 128 characters
|
|
570
|
+
// and `bounded()` (the frame-drop path's older, count-less form,
|
|
571
|
+
// deliberately kept: folding it into clipText would change its bare
|
|
572
|
+
// `…` marker and the caller's asserted text) clips to 120 + an
|
|
573
|
+
// ellipsis.
|
|
574
|
+
// Ceiling: `[cherry-remote] notification handler for "` (42) +
|
|
575
|
+
// bounded method (≤ 121: FRAME_FIELD_LIMIT 120 + the ellipsis) +
|
|
576
|
+
// `" threw: ` (9) + LOG_TEXT_MAX (240) = 412.
|
|
577
|
+
console.warn(
|
|
578
|
+
`[cherry-remote] notification handler for "${bounded(method)}" threw: ${failureText(error)}`,
|
|
579
|
+
)
|
|
562
580
|
}
|
|
563
581
|
}
|
|
564
582
|
return
|
package/src/subscriptions.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { JsonRpcRequestError } from './rpc/jsonRpcClient.js'
|
|
2
2
|
import type { JsonRpcClient } from './rpc/jsonRpcClient.js'
|
|
3
3
|
import { DEFAULT_PER_SESSION_CACHE_BYTES } from './defaults.js'
|
|
4
|
+
import { failureText } from './text.js'
|
|
4
5
|
import type { AgentCursor, AgentEvent } from '@cherrystudio/remote-protocol/agent'
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -760,8 +761,15 @@ export class SessionSubscriptions {
|
|
|
760
761
|
await this.#rpc.request('agent.subscriptions.close', { subscriptionId })
|
|
761
762
|
} catch (error) {
|
|
762
763
|
// Closing is best-effort: the peer may already have reset it, and the
|
|
763
|
-
// caller is about to establish a replacement either way.
|
|
764
|
-
|
|
764
|
+
// caller is about to establish a replacement either way. ONE bounded
|
|
765
|
+
// STRING (issue #161): the refusal is the PEER's JSON-RPC
|
|
766
|
+
// `error.message` verbatim, so the old object print let the console
|
|
767
|
+
// render peer text with no local bound. Ceiling:
|
|
768
|
+
// `[cherry-remote] closing subscription ` (37) + subscriptionId +
|
|
769
|
+
// ` failed: ` (9) + LOG_TEXT_MAX (240).
|
|
770
|
+
console.warn(
|
|
771
|
+
`[cherry-remote] closing subscription ${subscriptionId} failed: ${failureText(error)}`,
|
|
772
|
+
)
|
|
765
773
|
}
|
|
766
774
|
}
|
|
767
775
|
|
package/src/text.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded rendering for peer- and payload-controlled text (issue #155).
|
|
3
|
+
*
|
|
4
|
+
* The ONE helper this audit STANDARDISED ON — not the first attempt at the
|
|
5
|
+
* idea: `src/diagnostics.ts`'s `truncateErrorMessage` (the diagnostics error
|
|
6
|
+
* digest) and `src/rpc/jsonRpcClient.ts`'s `bounded()` (the frame-drop
|
|
7
|
+
* warning's classification fields) predate it and stay where they are, for
|
|
8
|
+
* their own callers. #155 is where the rendering rule was stated once and
|
|
9
|
+
* applied to the SIX sites the audit found — four files: pairing's unknown-key
|
|
10
|
+
* warning and stage verdict, the link-recovery line, the two connection error
|
|
11
|
+
* lines, and the JSON-RPC notification-handler line. Each was unbounded for a
|
|
12
|
+
* different "why not" (a 10 KiB payload cap, a hello-advertised field, the
|
|
13
|
+
* 64 KiB record budget), which is exactly how the class of defect recurs: each
|
|
14
|
+
* site answered the question for itself.
|
|
15
|
+
*
|
|
16
|
+
* The rule these helpers establish is stated once, here: bound the text where
|
|
17
|
+
* it is RENDERED (never where it is carried — an error message is a value
|
|
18
|
+
* until a log line or a verdict makes it text), name the amount removed with
|
|
19
|
+
* the `… (+N chars)` marker, and never drop a list ENTRY silently (the
|
|
20
|
+
* `(+K more)` count is explicit).
|
|
21
|
+
*
|
|
22
|
+
* Issue #161 swept the SAME SHAPE at the sites #155 did not name — a catch
|
|
23
|
+
* handing an error OBJECT to `console.warn`'s second argument — on the paths
|
|
24
|
+
* PEER traffic reaches. One renderer states the answer there, {@link
|
|
25
|
+
* failureText}: the message alone, bounded. A purely LOCAL catch keeps the
|
|
26
|
+
* object print, deliberately: its stack is the useful part and no peer text
|
|
27
|
+
* can reach it (fs quarantine, the host registry, our own listener / settle
|
|
28
|
+
* fan-out, a REST route's synchronous construction). The two earlier forms it
|
|
29
|
+
* did NOT absorb — `truncateErrorMessage` (the diagnostics digest) and
|
|
30
|
+
* `bounded()` (the frame-drop path) — are left alone: each emits a different
|
|
31
|
+
* marker (`…[truncated]`, a bare `…`) that callers and docs depend on, so
|
|
32
|
+
* folding would change output rather than converge on the rule.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Clips `text` into `max` characters, the `… (+N chars)` marker included in
|
|
37
|
+
* the count. `kept` is a fixed point — the number of removed characters sets
|
|
38
|
+
* the marker's own digits — and it is the LARGEST prefix that fits, so a
|
|
39
|
+
* caller's ceiling is used without ever being exceeded.
|
|
40
|
+
*
|
|
41
|
+
* The final guard never ends the kept prefix between a UTF-16 surrogate pair: a
|
|
42
|
+
* clip there would emit a lone surrogate, which is not valid unicode text (the
|
|
43
|
+
* peer's `unicodeText` params face refuses it) and would travel in a
|
|
44
|
+
* user-visible verdict. Dropping one char pays for a marker that can grow by
|
|
45
|
+
* one digit at a 9→10 boundary, and the marker is re-derived from the new
|
|
46
|
+
* dropped count. Pinned by `tests/unit/text.test.ts` — the branch used to be a
|
|
47
|
+
* declared coverage gap while the clip point was private to pairing.
|
|
48
|
+
*/
|
|
49
|
+
export function clipText(text: string, max: number): string {
|
|
50
|
+
if (text.length <= max) return text
|
|
51
|
+
let kept = max
|
|
52
|
+
for (;;) {
|
|
53
|
+
const next = max - `… (+${text.length - kept} chars)`.length
|
|
54
|
+
if (next === kept) break
|
|
55
|
+
// A room too small for the marker cannot carry both; admit the loss with
|
|
56
|
+
// the bare ellipsis rather than overrun the caller's bound.
|
|
57
|
+
if (next < 0) return '…'.slice(0, max)
|
|
58
|
+
kept = next
|
|
59
|
+
}
|
|
60
|
+
const before = text.charCodeAt(kept - 1)
|
|
61
|
+
const after = text.charCodeAt(kept)
|
|
62
|
+
if (kept > 0 && before >= 0xd800 && before <= 0xdbff && after >= 0xdc00 && after <= 0xdfff) {
|
|
63
|
+
kept -= 1
|
|
64
|
+
}
|
|
65
|
+
return `${text.slice(0, kept)}… (+${text.length - kept} chars)`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Renders `items` as a comma-joined list under `limits.max`, each item clipped
|
|
70
|
+
* to `limits.itemMax` FIRST. When anything is left out the list says so:
|
|
71
|
+
* ` (+K more)` where K is the number of items NOT named.
|
|
72
|
+
*
|
|
73
|
+
* Caller contract: `itemMax <= max`, so the first item always fits and a list
|
|
74
|
+
* carrying a single over-long entry never renders an empty one. The marker
|
|
75
|
+
* rides ON TOP of `limits.max` — it is a count, not an item, so the named text
|
|
76
|
+
* never has to absorb its length. `[]` renders as `''`.
|
|
77
|
+
*
|
|
78
|
+
* Pairing's progress line (`stageHosts`) deliberately does NOT use this helper:
|
|
79
|
+
* it always shows the FIRST host in full even above its own budget, because
|
|
80
|
+
* that host is the run's identity. That is not an oversight, and it is not a
|
|
81
|
+
* reason to weaken the contract here.
|
|
82
|
+
*/
|
|
83
|
+
export function boundedJoin(
|
|
84
|
+
items: readonly string[],
|
|
85
|
+
limits: { max: number; itemMax: number },
|
|
86
|
+
): string {
|
|
87
|
+
const rendered = items.map((item) => clipText(item, limits.itemMax))
|
|
88
|
+
const first = rendered[0]
|
|
89
|
+
if (first === undefined) return ''
|
|
90
|
+
let text = first
|
|
91
|
+
let shown = 1
|
|
92
|
+
for (; shown < rendered.length; shown += 1) {
|
|
93
|
+
// The separator is charged to the same budget as the item: a list that
|
|
94
|
+
// stops two characters early on paper would still refuse an entry it could
|
|
95
|
+
// have named.
|
|
96
|
+
if (text.length + 2 + rendered[shown]!.length > limits.max) break
|
|
97
|
+
text += `, ${rendered[shown]}`
|
|
98
|
+
}
|
|
99
|
+
const omitted = items.length - shown
|
|
100
|
+
return omitted > 0 ? `${text} (+${omitted} more)` : text
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The ceiling for ONE peer- or payload-controlled detail interpolated into a
|
|
105
|
+
* log line: 240 characters, the `… (+N chars)` marker included.
|
|
106
|
+
*
|
|
107
|
+
* It is deliberately NOT the same budget as pairing's `FAILURE_SEGMENT_MAX`,
|
|
108
|
+
* although both read 240: that one bounds the WHOLE `<label>: <detail>`, so
|
|
109
|
+
* the label eats from it and the detail there is at most 218 (a
|
|
110
|
+
* `pairing hello failed` label — 20 characters — plus its `': '`) and often
|
|
111
|
+
* less. Same number, different quantity: this one is the detail alone.
|
|
112
|
+
*
|
|
113
|
+
* Call sites. Two shapes are rendered with it:
|
|
114
|
+
*
|
|
115
|
+
* - peer-controlled DETAIL through {@link clipText}: `src/connection.ts`'s
|
|
116
|
+
* protocol-version console.error (the cause is the peer's own hello-echo
|
|
117
|
+
* text) and transient-failure console.warn (the transport's own message).
|
|
118
|
+
* - a caught FAILURE through {@link failureText} (issue #161), on the catches
|
|
119
|
+
* peer traffic reaches: `src/index.ts`'s notification routing and
|
|
120
|
+
* subscription recovery, `src/tools.ts`'s ack and both background re-ensure
|
|
121
|
+
* arms plus its `#assemblyFailed` log line, `src/subscriptions.ts`'s
|
|
122
|
+
* subscription-close catch, `src/httpApi.ts`'s `warnConnect` rejection, and
|
|
123
|
+
* the JSON-RPC notification-handler catch in `src/rpc/jsonRpcClient.ts`
|
|
124
|
+
* (#155). None of them passes the error object.
|
|
125
|
+
*
|
|
126
|
+
* Other bounded faces state their own value beside the line they bound
|
|
127
|
+
* (`UNKNOWN_FIELDS_MAX` / `RECOVERY_SESSIONS_MAX`), because what they bound is
|
|
128
|
+
* a rendered LIST or a joined field, not one detail.
|
|
129
|
+
*/
|
|
130
|
+
export const LOG_TEXT_MAX = 240
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Renders ONE caught failure for a log line: the message alone, clipped to
|
|
134
|
+
* {@link LOG_TEXT_MAX}.
|
|
135
|
+
*
|
|
136
|
+
* This is the ONE form a caught failure takes in a PEER-TRIGGERED catch (issue
|
|
137
|
+
* #161): a catch reached from peer traffic or a peer-triggered callback. The
|
|
138
|
+
* old shape — `console.warn(prefix, error)` — handed the console the OBJECT.
|
|
139
|
+
* On Node the console prints the error's name, message and stack (own
|
|
140
|
+
* properties only when inspected), so the peer-controlled message and the
|
|
141
|
+
* stack were both unbounded; for a defensive catch the stack is the noise.
|
|
142
|
+
*
|
|
143
|
+
* A caught failure from a purely LOCAL path (an fs copy, the host registry, our
|
|
144
|
+
* own observers) keeps the object print: its stack is the useful part and no
|
|
145
|
+
* peer text can reach it.
|
|
146
|
+
*
|
|
147
|
+
* TOTAL by construction: it runs INSIDE catch blocks, so a captured value whose
|
|
148
|
+
* `toString` (or an Error subclass whose `message` getter) throws must not
|
|
149
|
+
* escape the catch. The extraction is guarded and falls back to the fixed token
|
|
150
|
+
* `<unrenderable failure>`.
|
|
151
|
+
*/
|
|
152
|
+
export function failureText(error: unknown): string {
|
|
153
|
+
try {
|
|
154
|
+
return clipText(error instanceof Error ? error.message : String(error), LOG_TEXT_MAX)
|
|
155
|
+
} catch {
|
|
156
|
+
return '<unrenderable failure>'
|
|
157
|
+
}
|
|
158
|
+
}
|