@linxin666/dsh-remote-web-ui 0.3.22 → 0.3.24
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.i18n.yaml +2 -2
- package/README.md +4 -0
- package/README.zh.md +4 -0
- package/lib/client.js +193 -20
- package/lib/client.js.map +1 -1
- package/lib/index.js +181 -34
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +13 -18
- package/lib/types/client/mobile-adapt.d.ts.map +1 -1
- package/lib/types/client/mobile-adapt.js +40 -6
- package/lib/types/client/plugin-card-seat.d.ts +97 -0
- package/lib/types/client/plugin-card-seat.d.ts.map +1 -0
- package/lib/types/client/plugin-card-seat.js +141 -0
- package/lib/types/client/remote-channel.d.ts +35 -0
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +55 -0
- package/lib/types/console-output.d.ts +41 -0
- package/lib/types/console-output.d.ts.map +1 -0
- package/lib/types/console-output.js +89 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +26 -22
- package/lib/types/public-base.d.ts +67 -0
- package/lib/types/public-base.d.ts.map +1 -0
- package/lib/types/public-base.js +110 -0
- package/lib/types/remote-channel-boot.d.ts +5 -2
- package/lib/types/remote-channel-boot.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.js +28 -2
- package/lib/types/remote-channel-rules.d.ts +4 -0
- package/lib/types/remote-channel-rules.d.ts.map +1 -1
- package/lib/types/remote-channel-rules.js +2 -0
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +5 -12
- package/package.json +1 -1
- package/src/client/index.ts +13 -17
- package/src/client/mobile-adapt.ts +39 -5
- package/src/client/plugin-card-seat.ts +185 -0
- package/src/client/remote-channel.ts +64 -0
- package/src/console-output.ts +97 -0
- package/src/index.ts +26 -22
- package/src/public-base.ts +122 -0
- package/src/remote-channel-boot.ts +28 -2
- package/src/remote-channel-rules.ts +6 -0
- package/src/update.ts +5 -10
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Generated by scripts/sync-shared.mjs from shared/client/settings/plugin-card-seat.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
2
|
+
/**
|
|
3
|
+
* Family plugin-card seat.
|
|
4
|
+
*
|
|
5
|
+
* A family plugin contributes its settings card to whichever plugin-card seat
|
|
6
|
+
* the running host actually renders:
|
|
7
|
+
*
|
|
8
|
+
* - `web-ui.plugin.item` — the list seat declared by the dsh-web-settings
|
|
9
|
+
* group section (this family's own first-level "Web UI plugins" section);
|
|
10
|
+
* - `settings.plugin.item` — the official keyed seat of the harness's
|
|
11
|
+
* `ui-settings-plugins` tab, keyed by the settings namespace the card edits.
|
|
12
|
+
*
|
|
13
|
+
* SEAT SELECTION IS NOT A DECLARATION PROBE. The official `ui-settings-plugins`
|
|
14
|
+
* row belongs to the harness bundle and its `configurable` tab always declares
|
|
15
|
+
* `settings.plugin.item` before any external plugin's `apply()` runs, so
|
|
16
|
+
* "is the official seat declared?" answers yes even in the deployment whose
|
|
17
|
+
* whole point is the family group. Choosing on that probe sends every family
|
|
18
|
+
* card to the official Plugins tab and leaves the group's own section
|
|
19
|
+
* permanently empty — the family of reports where the section renders its
|
|
20
|
+
* heading and zero cards.
|
|
21
|
+
*
|
|
22
|
+
* The signal that actually distinguishes the two deployments is whether
|
|
23
|
+
* dsh-web-settings is loaded: it is the package that owns the group section and
|
|
24
|
+
* it publishes the `webUiSettings` service during `apply()`, which every
|
|
25
|
+
* family plugin already reads for its settings scope. Group loaded -> the family
|
|
26
|
+
* seat; group absent -> the official seat.
|
|
27
|
+
*
|
|
28
|
+
* The decision is re-evaluated on every `slots/changed` because the group may
|
|
29
|
+
* apply after this plugin (the family aggregate orders it first, a profile that
|
|
30
|
+
* installs the group separately need not): the initial contribution goes to the
|
|
31
|
+
* official seat, then moves to the family seat the moment the group's section
|
|
32
|
+
* registers. The entry is disposed before the replacement is registered, so a
|
|
33
|
+
* card is never in two seats at once.
|
|
34
|
+
*
|
|
35
|
+
* The shared tree has no client-SDK dependency, so this module reads its
|
|
36
|
+
* context through the structural shape below; callers pass the plugin's own
|
|
37
|
+
* `ctx`.
|
|
38
|
+
*/
|
|
39
|
+
/** The family list seat key. */
|
|
40
|
+
export const FAMILY_PLUGIN_CARD_SEAT = 'web-ui.plugin.item';
|
|
41
|
+
/** The official keyed plugin-card seat key. */
|
|
42
|
+
export const OFFICIAL_PLUGIN_CARD_SEAT = 'settings.plugin.item';
|
|
43
|
+
/** The service dsh-web-settings publishes while it is loaded. */
|
|
44
|
+
export const FAMILY_GROUP_SERVICE = 'webUiSettings';
|
|
45
|
+
/**
|
|
46
|
+
* Whether the family group (dsh-web-settings) is loaded in this page. The
|
|
47
|
+
* service is the group package's own contract, so the probe cannot be fooled
|
|
48
|
+
* by a harness release that starts declaring the official seat differently.
|
|
49
|
+
*/
|
|
50
|
+
export function familyGroupLoaded(ctx) {
|
|
51
|
+
const get = ctx.get;
|
|
52
|
+
if (typeof get !== 'function')
|
|
53
|
+
return false;
|
|
54
|
+
try {
|
|
55
|
+
return get.call(ctx, FAMILY_GROUP_SERVICE) !== undefined;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// A context that refuses the lookup: treat the group as absent, which
|
|
59
|
+
// falls back to the official seat the harness declares.
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Report a refused registration instead of leaving the user with no card. */
|
|
64
|
+
function warnRefusedSeat(seat, error) {
|
|
65
|
+
try {
|
|
66
|
+
console.warn(`[dsh-web] plugin card registration into "${seat}" was refused; the card will not render`, error);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Best-effort console write; a failed warning must not break the plugin.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Contribute one family plugin card to the seat this host renders, following
|
|
74
|
+
* the group if it loads later. The entry is disposed and re-registered on a
|
|
75
|
+
* seat change, never duplicated.
|
|
76
|
+
* @param ctx - client context (its slot registry decides the seat).
|
|
77
|
+
* @param seat - the card contribution.
|
|
78
|
+
*/
|
|
79
|
+
export function installPluginCard(ctx, seat) {
|
|
80
|
+
const slots = ctx.slots;
|
|
81
|
+
const component = seat.component;
|
|
82
|
+
const inject = seat.inject;
|
|
83
|
+
let dispose;
|
|
84
|
+
let current;
|
|
85
|
+
/**
|
|
86
|
+
* Re-entrancy latch. The registry emits a change event synchronously from
|
|
87
|
+
* inside both `register` and the previous entry's disposer, so an unguarded
|
|
88
|
+
* reconcile would re-enter itself mid-move and register the card twice into
|
|
89
|
+
* the seat it is leaving ("already has an entry for key ...").
|
|
90
|
+
*/
|
|
91
|
+
let reconciling = false;
|
|
92
|
+
/** Reconcile the contribution with the currently live seat (no-op when unchanged). */
|
|
93
|
+
const reconcile = () => {
|
|
94
|
+
if (reconciling)
|
|
95
|
+
return;
|
|
96
|
+
const target = familyGroupLoaded(ctx) ? FAMILY_PLUGIN_CARD_SEAT : OFFICIAL_PLUGIN_CARD_SEAT;
|
|
97
|
+
if (current === target)
|
|
98
|
+
return;
|
|
99
|
+
reconciling = true;
|
|
100
|
+
const previous = dispose;
|
|
101
|
+
dispose = undefined;
|
|
102
|
+
current = undefined;
|
|
103
|
+
previous?.();
|
|
104
|
+
try {
|
|
105
|
+
dispose = slots.register((target === FAMILY_PLUGIN_CARD_SEAT
|
|
106
|
+
? {
|
|
107
|
+
name: FAMILY_PLUGIN_CARD_SEAT,
|
|
108
|
+
id: seat.id,
|
|
109
|
+
...(seat.order === undefined ? {} : { order: seat.order }),
|
|
110
|
+
...(seat.label === undefined ? {} : { label: seat.label }),
|
|
111
|
+
locale: seat.locale,
|
|
112
|
+
...(seat.inject === undefined ? {} : { inject }),
|
|
113
|
+
}
|
|
114
|
+
: {
|
|
115
|
+
name: OFFICIAL_PLUGIN_CARD_SEAT,
|
|
116
|
+
key: seat.namespace,
|
|
117
|
+
locale: seat.locale,
|
|
118
|
+
...(seat.inject === undefined ? {} : { inject }),
|
|
119
|
+
}), component);
|
|
120
|
+
current = target;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
warnRefusedSeat(target, error);
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
reconciling = false;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
if (typeof ctx.on === 'function') {
|
|
130
|
+
try {
|
|
131
|
+
ctx.on('slots/changed', () => { reconcile(); });
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// An event seat that refuses the subscription leaves the initial
|
|
135
|
+
// decision in place; the card still renders in the seat chosen below.
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// The initial decision: the group, when it is already loaded (the aggregate
|
|
139
|
+
// order), otherwise the official seat the harness declares.
|
|
140
|
+
reconcile();
|
|
141
|
+
}
|
|
@@ -100,6 +100,41 @@ export declare function isUnpairedDenied(response: Response): Promise<boolean>;
|
|
|
100
100
|
* @returns a function restoring the originals.
|
|
101
101
|
*/
|
|
102
102
|
export declare function installRemoteChannel(window: ChannelWindow, options?: RemoteChannelOptions): () => void;
|
|
103
|
+
/** Page global the official pre-Cordis upload hook is published under. */
|
|
104
|
+
export declare const FILE_UPLOAD_HOOK_GLOBAL = "__DSH_FILE_UPLOAD__";
|
|
105
|
+
/** The subset of window the upload hook needs (injectable for tests). */
|
|
106
|
+
export interface UploadHookWindow {
|
|
107
|
+
fetch: typeof globalThis.fetch;
|
|
108
|
+
location: {
|
|
109
|
+
href: string;
|
|
110
|
+
origin: string;
|
|
111
|
+
};
|
|
112
|
+
/** Pre-existing hook owner; left untouched when present. */
|
|
113
|
+
__DSH_FILE_UPLOAD__?: {
|
|
114
|
+
fetch: (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Publish the official pre-Cordis upload hook so background uploads keep
|
|
119
|
+
* riding the patched main-thread fetch (issue #1580).
|
|
120
|
+
*
|
|
121
|
+
* `@deepseek-ai/dsh-client-file-upload` reads `globalThis.__DSH_FILE_UPLOAD__`
|
|
122
|
+
* once when its runtime is constructed; without it the carrier is a Web
|
|
123
|
+
* Worker, whose own globals no main-thread patch reaches. That worker's XHR
|
|
124
|
+
* goes straight to `<origin>/api/session/uploadFileBinary` with neither the
|
|
125
|
+
* `/remote` rewrite nor the cookieless device credential, so the harness
|
|
126
|
+
* browser-auth fence answers 401 and every upload from a paired browser
|
|
127
|
+
* fails. Rewriting the worker URL cannot fix it: a worker context carries
|
|
128
|
+
* neither the pairing cookie nor the device header.
|
|
129
|
+
*
|
|
130
|
+
* The hook hands the runtime the same transport the rest of the page uses -
|
|
131
|
+
* the boot script publishes an identical one (remote-channel-boot.ts), and
|
|
132
|
+
* this is the fallback for pages served without it.
|
|
133
|
+
*
|
|
134
|
+
* @param window - the browser window (or a test double), BEFORE the channel patch.
|
|
135
|
+
* @returns a function retiring the hook (a pre-existing one is left alone).
|
|
136
|
+
*/
|
|
137
|
+
export declare function installFileUploadHook(window: UploadHookWindow): () => void;
|
|
103
138
|
/**
|
|
104
139
|
* The remote-channel lifecycle transition between two steady states:
|
|
105
140
|
* running (active + installed) and retired (inactive + not installed).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-channel.d.ts","sourceRoot":"","sources":["../../../src/client/remote-channel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EACL,iBAAiB,EAEjB,aAAa,EACd,MAAM,4BAA4B,CAAA;AAEnC,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,CAAA;AAC3C,YAAY,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAA;AACvE,OAAO,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAA;AAIvE,qEAAqE;AACrE,MAAM,WAAW,6BAA6B;IAC5C,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,aAAa,GAAG,MAAM,CAAA;IACpD,KAAK,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,oBAAoB,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;CAC9D;AAED,qFAAqF;AACrF,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,6BAA6B,EACvC,iBAAiB,EAAE,OAAO,GAAG,SAAS,GACrC,OAAO,CAQT;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAI5D;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAShE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE7D;AAED,yEAAyE;AACzE,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAcnF;AAED,wEAAwE;AACxE,UAAU,cAAc;IACtB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,qEAAqE;AACrE,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;IAC9B,SAAS,EAAE,OAAO,SAAS,CAAA;IAC3B,WAAW,CAAC,EAAE,OAAO,WAAW,CAAA;IAChC,gBAAgB,CAAC,EAAE,cAAc,CAAA;IACjC,iBAAiB,CAAC,EAAE,cAAc,CAAA;IAClC,iBAAiB,CAAC,EAAE,cAAc,CAAA;IAClC,QAAQ,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IAC1C,cAAc,CAAC,EAAE;QAAE,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;KAAE,CAAA;CACzD;AAED,gDAAgD;AAChD,MAAM,WAAW,oBAAoB;IACnC,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,IAAI,CAAA;IACvB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAoBD;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAO3E;AA0BD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,GAAE,oBAAyB,GAAG,MAAM,IAAI,
|
|
1
|
+
{"version":3,"file":"remote-channel.d.ts","sourceRoot":"","sources":["../../../src/client/remote-channel.ts"],"names":[],"mappings":"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"}
|
|
@@ -172,6 +172,9 @@ export function installRemoteChannel(window, options = {}) {
|
|
|
172
172
|
const originalFetch = window.fetch;
|
|
173
173
|
const OriginalWebSocket = window.WebSocket;
|
|
174
174
|
const OriginalEventSource = window.EventSource;
|
|
175
|
+
// Publish the official upload hook before the file-upload service reads it
|
|
176
|
+
// (issue #1580); removed again by the restore below.
|
|
177
|
+
const restoreUploadHook = installFileUploadHook(window);
|
|
175
178
|
const sameOrigin = (url) => url.origin === window.location.origin;
|
|
176
179
|
const rewrite = (raw) => rewriteRawUrl(raw, window.location.href, window.location.origin);
|
|
177
180
|
// The cookieless device credential (set by the /pair-app capture script):
|
|
@@ -266,6 +269,58 @@ export function installRemoteChannel(window, options = {}) {
|
|
|
266
269
|
window.EventSource = OriginalEventSource;
|
|
267
270
|
for (const restore of restoreSrc)
|
|
268
271
|
restore();
|
|
272
|
+
restoreUploadHook();
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/** Page global the official pre-Cordis upload hook is published under. */
|
|
276
|
+
export const FILE_UPLOAD_HOOK_GLOBAL = '__DSH_FILE_UPLOAD__';
|
|
277
|
+
/**
|
|
278
|
+
* Publish the official pre-Cordis upload hook so background uploads keep
|
|
279
|
+
* riding the patched main-thread fetch (issue #1580).
|
|
280
|
+
*
|
|
281
|
+
* `@deepseek-ai/dsh-client-file-upload` reads `globalThis.__DSH_FILE_UPLOAD__`
|
|
282
|
+
* once when its runtime is constructed; without it the carrier is a Web
|
|
283
|
+
* Worker, whose own globals no main-thread patch reaches. That worker's XHR
|
|
284
|
+
* goes straight to `<origin>/api/session/uploadFileBinary` with neither the
|
|
285
|
+
* `/remote` rewrite nor the cookieless device credential, so the harness
|
|
286
|
+
* browser-auth fence answers 401 and every upload from a paired browser
|
|
287
|
+
* fails. Rewriting the worker URL cannot fix it: a worker context carries
|
|
288
|
+
* neither the pairing cookie nor the device header.
|
|
289
|
+
*
|
|
290
|
+
* The hook hands the runtime the same transport the rest of the page uses -
|
|
291
|
+
* the boot script publishes an identical one (remote-channel-boot.ts), and
|
|
292
|
+
* this is the fallback for pages served without it.
|
|
293
|
+
*
|
|
294
|
+
* @param window - the browser window (or a test double), BEFORE the channel patch.
|
|
295
|
+
* @returns a function retiring the hook (a pre-existing one is left alone).
|
|
296
|
+
*/
|
|
297
|
+
export function installFileUploadHook(window) {
|
|
298
|
+
if (window.__DSH_FILE_UPLOAD__ !== undefined)
|
|
299
|
+
return () => { };
|
|
300
|
+
const originalFetch = window.fetch;
|
|
301
|
+
const hook = {
|
|
302
|
+
fetch: (input, init) => {
|
|
303
|
+
const raw = typeof input === 'string' ? input : input.href;
|
|
304
|
+
let url;
|
|
305
|
+
try {
|
|
306
|
+
url = new URL(raw, window.location.href);
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
return originalFetch.call(window, input, init);
|
|
310
|
+
}
|
|
311
|
+
// Delegate the whole decision to the patched fetch: rewriting the path
|
|
312
|
+
// here first would make it skip its own rewrite branch and drop the
|
|
313
|
+
// device credential the fence requires.
|
|
314
|
+
if (url.origin === window.location.origin && url.pathname === RULES.uploadPath) {
|
|
315
|
+
return window.fetch.call(window, raw, init);
|
|
316
|
+
}
|
|
317
|
+
return originalFetch.call(window, input, init);
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
window.__DSH_FILE_UPLOAD__ = hook;
|
|
321
|
+
return () => {
|
|
322
|
+
if (window.__DSH_FILE_UPLOAD__ === hook)
|
|
323
|
+
delete window.__DSH_FILE_UPLOAD__;
|
|
269
324
|
};
|
|
270
325
|
}
|
|
271
326
|
/**
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console-output capture shared by the family's Host halves.
|
|
3
|
+
*
|
|
4
|
+
* A child process's stdout/stderr are raw bytes, and on Windows a console
|
|
5
|
+
* program writes the OEM code page (CP936/GBK on a zh-CN system), not UTF-8.
|
|
6
|
+
* Decoding each chunk on its own corrupts the text twice over: the
|
|
7
|
+
* encoding is guessed wrong, and a multi-byte character split across two reads
|
|
8
|
+
* decodes into two replacement characters. This capture accumulates raw bytes
|
|
9
|
+
* under a byte budget and decodes exactly once, at read time.
|
|
10
|
+
*/
|
|
11
|
+
/** Bounded byte accumulator for one child process's interleaved stdout/stderr. */
|
|
12
|
+
export interface OutputCapture {
|
|
13
|
+
/** Append one raw chunk (the bytes are copied into the budget). */
|
|
14
|
+
push(chunk: Uint8Array): void;
|
|
15
|
+
/**
|
|
16
|
+
* Decode everything captured so far. Idempotent, so a caller may read the
|
|
17
|
+
* tail both after a failure branch and at settlement.
|
|
18
|
+
*/
|
|
19
|
+
read(): string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Decode one console byte buffer. Strict UTF-8 wins whenever it is valid;
|
|
23
|
+
* otherwise the bytes are not UTF-8 at all and the console wrote its own code
|
|
24
|
+
* page, so the candidate that loses the least text is taken.
|
|
25
|
+
* @param bytes - the captured bytes.
|
|
26
|
+
* @returns the decoded text.
|
|
27
|
+
*/
|
|
28
|
+
export declare function decodeConsoleBytes(bytes: Uint8Array): string;
|
|
29
|
+
/**
|
|
30
|
+
* Create a capture bounded to a byte budget. Overflow drops from the FRONT
|
|
31
|
+
* (the tail is what carries a failure reason), and the cut never lands inside
|
|
32
|
+
* a character: leading UTF-8 continuation bytes are dropped with it, and a
|
|
33
|
+
* legacy code page's dangling lead byte — indistinguishable from a real byte
|
|
34
|
+
* without knowing the encoding until the whole buffer is decoded — is removed
|
|
35
|
+
* with the one replacement character it produced, so an over-long stream still
|
|
36
|
+
* reads cleanly at the seam.
|
|
37
|
+
* @param maxBytes - maximum retained bytes.
|
|
38
|
+
* @returns the capture.
|
|
39
|
+
*/
|
|
40
|
+
export declare function createOutputCapture(maxBytes: number): OutputCapture;
|
|
41
|
+
//# sourceMappingURL=console-output.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"console-output.d.ts","sourceRoot":"","sources":["../../src/console-output.ts"],"names":[],"mappings":"AACA;;;;;;;;;GASG;AAEH,kFAAkF;AAClF,MAAM,WAAW,aAAa;IAC5B,mEAAmE;IACnE,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAA;IAC7B;;;OAGG;IACH,IAAI,IAAI,MAAM,CAAA;CACf;AAWD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CA2B5D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,CAiBnE"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Generated by scripts/sync-shared.mjs from shared/host/console-output.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
2
|
+
/**
|
|
3
|
+
* Console-output capture shared by the family's Host halves.
|
|
4
|
+
*
|
|
5
|
+
* A child process's stdout/stderr are raw bytes, and on Windows a console
|
|
6
|
+
* program writes the OEM code page (CP936/GBK on a zh-CN system), not UTF-8.
|
|
7
|
+
* Decoding each chunk on its own corrupts the text twice over: the
|
|
8
|
+
* encoding is guessed wrong, and a multi-byte character split across two reads
|
|
9
|
+
* decodes into two replacement characters. This capture accumulates raw bytes
|
|
10
|
+
* under a byte budget and decodes exactly once, at read time.
|
|
11
|
+
*/
|
|
12
|
+
/** Count the U+FFFD replacement characters one candidate decode produced. */
|
|
13
|
+
function replacementCount(text) {
|
|
14
|
+
let count = 0;
|
|
15
|
+
for (const character of text) {
|
|
16
|
+
if (character === '\uFFFD')
|
|
17
|
+
count += 1;
|
|
18
|
+
}
|
|
19
|
+
return count;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Decode one console byte buffer. Strict UTF-8 wins whenever it is valid;
|
|
23
|
+
* otherwise the bytes are not UTF-8 at all and the console wrote its own code
|
|
24
|
+
* page, so the candidate that loses the least text is taken.
|
|
25
|
+
* @param bytes - the captured bytes.
|
|
26
|
+
* @returns the decoded text.
|
|
27
|
+
*/
|
|
28
|
+
export function decodeConsoleBytes(bytes) {
|
|
29
|
+
try {
|
|
30
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Not valid UTF-8. Prefer whichever candidate leaves the fewest
|
|
34
|
+
// replacement characters so real UTF-8 text carrying one bad byte is not
|
|
35
|
+
// rewritten wholesale as GBK gibberish; without the legacy code page
|
|
36
|
+
// (small-ICU builds) the lenient UTF-8 pass is all that is left.
|
|
37
|
+
let best;
|
|
38
|
+
let bestLoss = Number.POSITIVE_INFINITY;
|
|
39
|
+
for (const label of ['gbk', 'utf-8']) {
|
|
40
|
+
let text;
|
|
41
|
+
try {
|
|
42
|
+
text = new TextDecoder(label).decode(bytes);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const loss = replacementCount(text);
|
|
48
|
+
if (loss < bestLoss) {
|
|
49
|
+
best = text;
|
|
50
|
+
bestLoss = loss;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/* v8 ignore next 3 -- reachable only without the legacy code page AND without the lenient pass */
|
|
54
|
+
if (best === undefined)
|
|
55
|
+
return Buffer.from(bytes).toString('utf8');
|
|
56
|
+
return best;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Create a capture bounded to a byte budget. Overflow drops from the FRONT
|
|
61
|
+
* (the tail is what carries a failure reason), and the cut never lands inside
|
|
62
|
+
* a character: leading UTF-8 continuation bytes are dropped with it, and a
|
|
63
|
+
* legacy code page's dangling lead byte — indistinguishable from a real byte
|
|
64
|
+
* without knowing the encoding until the whole buffer is decoded — is removed
|
|
65
|
+
* with the one replacement character it produced, so an over-long stream still
|
|
66
|
+
* reads cleanly at the seam.
|
|
67
|
+
* @param maxBytes - maximum retained bytes.
|
|
68
|
+
* @returns the capture.
|
|
69
|
+
*/
|
|
70
|
+
export function createOutputCapture(maxBytes) {
|
|
71
|
+
let buffer = Buffer.alloc(0);
|
|
72
|
+
let trimmed = false;
|
|
73
|
+
return {
|
|
74
|
+
push(chunk) {
|
|
75
|
+
buffer = buffer.length === 0 ? Buffer.from(chunk) : Buffer.concat([buffer, Buffer.from(chunk)]);
|
|
76
|
+
if (buffer.length <= maxBytes)
|
|
77
|
+
return;
|
|
78
|
+
let start = buffer.length - maxBytes;
|
|
79
|
+
while (start < buffer.length && (buffer[start] & 0xc0) === 0x80)
|
|
80
|
+
start += 1;
|
|
81
|
+
buffer = Buffer.from(buffer.subarray(start));
|
|
82
|
+
trimmed = true;
|
|
83
|
+
},
|
|
84
|
+
read() {
|
|
85
|
+
const text = decodeConsoleBytes(buffer);
|
|
86
|
+
return trimmed && text.startsWith('\uFFFD') ? text.slice(1) : text;
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAClE,OAAO,CAAC,MAAM,aAAa,CAAA;AAO3B,OAAO,EAA0C,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|
package/lib/types/index.js
CHANGED
|
@@ -27,6 +27,7 @@ import { loadRelayIdentity, RelayRegistrar } from "./relay-registry.js";
|
|
|
27
27
|
import { desiredBindHost, desiredBindPort, firewallActionNeeded, pendingRestartOf } from "./lan-bind-plan.js";
|
|
28
28
|
import { createInnerAuth } from "./inner-auth.js";
|
|
29
29
|
import { TunnelManager } from "./tunnel.js";
|
|
30
|
+
import { PublicBaseKeeper } from "./public-base.js";
|
|
30
31
|
import { checkUpdates, fetchGitHubReleaseNotes, fetchLatestVersion, RELEASE_NOTES_CACHE_TTL_MS, resolveAnchorManifest, resolveUpdateTarget, runUpdateVerified, } from "./update.js";
|
|
31
32
|
import { makeUpdateRoutes } from "./update-routes.js";
|
|
32
33
|
import { mountOnce } from "./mount-once.js";
|
|
@@ -162,17 +163,18 @@ function applyImpl(ctx, config) {
|
|
|
162
163
|
// raw quick URL is used, exactly as before. Named tunnels keep their fixed
|
|
163
164
|
// dashboard hostname and never touch the relay.
|
|
164
165
|
let relayRegistrar;
|
|
165
|
-
|
|
166
|
-
|
|
166
|
+
/**
|
|
167
|
+
* The public base the pairing fence trusts. A tunnel reconnect must not
|
|
168
|
+
* strip the host the QR still shows: named and relay hosts never change,
|
|
169
|
+
* and a quick tunnel's host survives a bounded grace window (issue #1547).
|
|
170
|
+
*/
|
|
171
|
+
const publicBase = new PublicBaseKeeper((base) => { service.setPublicBaseUrl(base); });
|
|
167
172
|
/** The tunnel target the registrar last announced (dedupes sync re-runs). */
|
|
168
173
|
let relayAnnouncedFor;
|
|
169
|
-
const setPublicBase = () => {
|
|
170
|
-
service.setPublicBaseUrl(relayUrl ?? rawTunnelUrl);
|
|
171
|
-
};
|
|
172
174
|
const disposeRelayRegistrar = (unregister = false) => {
|
|
173
175
|
const registrar = relayRegistrar;
|
|
174
176
|
relayRegistrar = undefined;
|
|
175
|
-
|
|
177
|
+
publicBase.setRelay(undefined);
|
|
176
178
|
relayAnnouncedFor = undefined;
|
|
177
179
|
if (registrar === undefined)
|
|
178
180
|
return;
|
|
@@ -193,10 +195,10 @@ function applyImpl(ctx, config) {
|
|
|
193
195
|
relayRegistrar = new RelayRegistrar(identity, (state) => {
|
|
194
196
|
service.setRelayStatus(state.state === 'off' ? undefined : state);
|
|
195
197
|
if (state.state === 'running') {
|
|
196
|
-
|
|
198
|
+
publicBase.setRelay(state.url);
|
|
197
199
|
}
|
|
198
200
|
else if (state.state === 'off') {
|
|
199
|
-
|
|
201
|
+
publicBase.setRelay(undefined);
|
|
200
202
|
}
|
|
201
203
|
else if (state.state === 'failed') {
|
|
202
204
|
// Keep the last relay URL on failures: the phone origin only
|
|
@@ -204,7 +206,6 @@ function applyImpl(ctx, config) {
|
|
|
204
206
|
// refresh call fails. The registrar retries with backoff.
|
|
205
207
|
console.warn(`remote-web-ui: relay registration failed (${state.error}) — the stable origin may serve its offline page until the retry lands`);
|
|
206
208
|
}
|
|
207
|
-
setPublicBase();
|
|
208
209
|
});
|
|
209
210
|
}
|
|
210
211
|
catch (error) {
|
|
@@ -226,8 +227,7 @@ function applyImpl(ctx, config) {
|
|
|
226
227
|
if (tunnelMode === 'off')
|
|
227
228
|
return;
|
|
228
229
|
if (info.phase === 'running' && info.url !== undefined) {
|
|
229
|
-
|
|
230
|
-
setPublicBase();
|
|
230
|
+
publicBase.markRunning(info.url);
|
|
231
231
|
service.setTunnelStatus({ state: 'running', url: info.url });
|
|
232
232
|
const registrar = tunnelMode === 'quick' ? ensureRelayRegistrar() : undefined;
|
|
233
233
|
if (registrar !== undefined)
|
|
@@ -235,21 +235,23 @@ function applyImpl(ctx, config) {
|
|
|
235
235
|
runPostureProbe();
|
|
236
236
|
}
|
|
237
237
|
else if (info.phase === 'starting') {
|
|
238
|
-
// A restart mints a
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
238
|
+
// A quick-tunnel restart mints a new hostname, but the old one is not
|
|
239
|
+
// dropped at once: the edge may still deliver a connection the phone
|
|
240
|
+
// already opened, and the reconnect usually lands inside the grace
|
|
241
|
+
// window (issue #1547). A named tunnel keeps its fixed hostname, and a
|
|
242
|
+
// registered relay its stable subdomain, so neither is ever dropped
|
|
243
|
+
// here — the previous code cleared the relay base on every restart.
|
|
244
|
+
publicBase.markReconnecting();
|
|
243
245
|
service.setTunnelStatus({ state: 'starting' });
|
|
244
246
|
}
|
|
245
247
|
else if (info.phase === 'failed') {
|
|
246
|
-
|
|
247
|
-
setPublicBase();
|
|
248
|
+
publicBase.markReconnecting();
|
|
248
249
|
service.setTunnelStatus(info.error === undefined ? { state: 'failed' } : { state: 'failed', error: info.error });
|
|
249
250
|
}
|
|
250
251
|
});
|
|
251
252
|
ctx.effect(() => () => {
|
|
252
253
|
disposeRelayRegistrar();
|
|
254
|
+
publicBase.dispose();
|
|
253
255
|
tunnel.dispose();
|
|
254
256
|
}, 'remote-web-ui: auto tunnel');
|
|
255
257
|
// The bind facts are known by now (webServer is an inject edge): the LAN
|
|
@@ -613,26 +615,28 @@ function applyImpl(ctx, config) {
|
|
|
613
615
|
// publicBaseUrl applies only when no tunnel runs.
|
|
614
616
|
const plan = tunnelPlanOf(value, ctx.webServer.port);
|
|
615
617
|
tunnelMode = plan.mode;
|
|
618
|
+
publicBase.setMode(plan.mode);
|
|
619
|
+
const liveTunnelUrl = publicBase.quickUrl();
|
|
616
620
|
if (plan.mode !== 'quick') {
|
|
617
621
|
// The relay only fronts the quick tunnel; named mode owns its fixed
|
|
618
622
|
// dashboard hostname and the off mode has no public base at all.
|
|
619
623
|
disposeRelayRegistrar();
|
|
620
624
|
if (plan.mode !== 'named')
|
|
621
|
-
|
|
625
|
+
publicBase.refresh();
|
|
622
626
|
}
|
|
623
627
|
else if (value.relay === false) {
|
|
624
628
|
// The relay toggle is off: no stable origin, the raw quick URL is the
|
|
625
629
|
// QR base exactly as before the relay existed.
|
|
626
630
|
disposeRelayRegistrar(true);
|
|
627
|
-
|
|
631
|
+
publicBase.refresh();
|
|
628
632
|
}
|
|
629
|
-
else if (
|
|
633
|
+
else if (liveTunnelUrl !== undefined) {
|
|
630
634
|
// The relay just turned on (or the registrar is new) while the tunnel
|
|
631
635
|
// already runs: announce now — no phase event will fire for an
|
|
632
636
|
// unchanged target.
|
|
633
637
|
const registrar = ensureRelayRegistrar();
|
|
634
638
|
if (registrar !== undefined)
|
|
635
|
-
announceRelay(registrar,
|
|
639
|
+
announceRelay(registrar, liveTunnelUrl);
|
|
636
640
|
}
|
|
637
641
|
if (plan.mode === 'quick') {
|
|
638
642
|
for (const ignored of plan.ignored) {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The public base the pairing fence trusts while a tunnel is not running
|
|
3
|
+
* (issue #1547).
|
|
4
|
+
*
|
|
5
|
+
* The fence reads exactly one public host: `service.publicBaseUrl`. Dropping
|
|
6
|
+
* it the instant the tunnel leaves `running` makes every public request from
|
|
7
|
+
* the address still printed in the QR code answer 403 — including
|
|
8
|
+
* `/api/pair/status`, which exists to be callable before pairing. Two of the
|
|
9
|
+
* three tunnel modes never lose their host: a named tunnel keeps its fixed
|
|
10
|
+
* dashboard hostname, and a registered relay keeps its stable subdomain. A
|
|
11
|
+
* quick tunnel does mint a new hostname on restart, so its previous host is
|
|
12
|
+
* kept only for a bounded grace window: Cloudflare's edge can still deliver a
|
|
13
|
+
* connection the phone already opened, and a mobile network reconnects
|
|
14
|
+
* constantly, so an instant drop turns the phone's retry into a silent 403.
|
|
15
|
+
* @module @linxin666/dsh-remote-web-ui/public-base
|
|
16
|
+
*/
|
|
17
|
+
/** How long a quick-tunnel host stays trusted after the tunnel stops running. */
|
|
18
|
+
export declare const PUBLIC_BASE_GRACE_MS = 60000;
|
|
19
|
+
/** Tunnel ownership of the public base, as resolved from the settings document. */
|
|
20
|
+
export type TunnelBaseMode = 'off' | 'quick' | 'named';
|
|
21
|
+
/**
|
|
22
|
+
* Owns the trusted public base across tunnel and relay transitions.
|
|
23
|
+
*
|
|
24
|
+
* The keeper never decides on its own to widen trust: it only postpones
|
|
25
|
+
* dropping an address the operator (named/relay) or the tunnel (quick) had
|
|
26
|
+
* already published, and it forgets it once the tunnel comes back with the
|
|
27
|
+
* address that actually serves the QR.
|
|
28
|
+
*/
|
|
29
|
+
export declare class PublicBaseKeeper {
|
|
30
|
+
private readonly onChange;
|
|
31
|
+
private readonly graceMs;
|
|
32
|
+
private mode;
|
|
33
|
+
private tunnelUrl;
|
|
34
|
+
private relayUrl;
|
|
35
|
+
private graceTimer;
|
|
36
|
+
/**
|
|
37
|
+
* @param onChange - receives the base the fence should trust from now on.
|
|
38
|
+
* @param graceMs - how long a quick-tunnel host outlives its tunnel.
|
|
39
|
+
*/
|
|
40
|
+
constructor(onChange: (base: string | undefined) => void, graceMs?: number);
|
|
41
|
+
/** The base a fresh fence read should trust: the stable relay origin wins. */
|
|
42
|
+
current(): string | undefined;
|
|
43
|
+
/** The mode that currently owns the public base. */
|
|
44
|
+
setMode(mode: TunnelBaseMode): void;
|
|
45
|
+
/** The last quick-tunnel URL, for callers that must announce it downstream. */
|
|
46
|
+
quickUrl(): string | undefined;
|
|
47
|
+
/** A relay registration result; undefined withdraws the stable origin. */
|
|
48
|
+
setRelay(url: string | undefined): void;
|
|
49
|
+
/** The tunnel reported a live URL: it supersedes any pending grace. */
|
|
50
|
+
markRunning(url: string): void;
|
|
51
|
+
/**
|
|
52
|
+
* The tunnel left `running` (starting again, or failed). A named tunnel's
|
|
53
|
+
* fixed hostname stays trusted; a quick tunnel's previous host stays
|
|
54
|
+
* trusted for the grace window, then is dropped if nothing replaced it.
|
|
55
|
+
*/
|
|
56
|
+
markReconnecting(): void;
|
|
57
|
+
/** Re-publish the current base after a branch change that did not move it. */
|
|
58
|
+
refresh(): void;
|
|
59
|
+
/** Forget everything (teardown, or a mode change that stops the tunnel). */
|
|
60
|
+
reset(): void;
|
|
61
|
+
/** Cancel the pending grace timer; the keeper keeps its last base. */
|
|
62
|
+
dispose(): void;
|
|
63
|
+
private startGrace;
|
|
64
|
+
private cancelGrace;
|
|
65
|
+
private emit;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=public-base.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"public-base.d.ts","sourceRoot":"","sources":["../../src/public-base.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,iFAAiF;AACjF,eAAO,MAAM,oBAAoB,QAAS,CAAA;AAE1C,mFAAmF;AACnF,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,OAAO,GAAG,OAAO,CAAA;AAEtD;;;;;;;GAOG;AACH,qBAAa,gBAAgB;IAWzB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAX1B,OAAO,CAAC,IAAI,CAAwB;IACpC,OAAO,CAAC,SAAS,CAAoB;IACrC,OAAO,CAAC,QAAQ,CAAoB;IACpC,OAAO,CAAC,UAAU,CAA2C;IAE7D;;;OAGG;gBAEgB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,EAC5C,OAAO,GAAE,MAA6B;IAGzD,8EAA8E;IAC9E,OAAO,IAAI,MAAM,GAAG,SAAS;IAI7B,oDAAoD;IACpD,OAAO,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI;IAInC,+EAA+E;IAC/E,QAAQ,IAAI,MAAM,GAAG,SAAS;IAI9B,0EAA0E;IAC1E,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAKvC,uEAAuE;IACvE,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAM9B;;;;OAIG;IACH,gBAAgB,IAAI,IAAI;IAKxB,8EAA8E;IAC9E,OAAO,IAAI,IAAI;IAIf,4EAA4E;IAC5E,KAAK,IAAI,IAAI;IAOb,sEAAsE;IACtE,OAAO,IAAI,IAAI;IAIf,OAAO,CAAC,UAAU;IAUlB,OAAO,CAAC,WAAW;IAMnB,OAAO,CAAC,IAAI;CAGb"}
|