@linxin666/dsh-remote-web-ui 0.2.7 → 0.2.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/README.i18n.yaml +2 -2
- package/README.md +4 -0
- package/README.zh.md +1 -0
- package/lib/client.js +40 -8
- package/lib/client.js.map +1 -1
- package/lib/index.js +12 -8
- package/lib/mobile.js +39 -21
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/RemoteEntry.js +3 -3
- package/lib/types/client/UpdateEntry.d.ts.map +1 -1
- package/lib/types/client/UpdateEntry.js +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +28 -14
- package/lib/types/client/pair-api.d.ts +6 -0
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/pair-api.js +11 -0
- package/lib/types/client/remote-channel.d.ts +10 -0
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +11 -0
- package/lib/types/index.js +1 -1
- package/lib/types/mobile/messages.d.ts.map +1 -1
- package/lib/types/mobile/messages.js +5 -3
- package/lib/types/mobile/messages.test.js +20 -0
- package/lib/types/mobile/mux.d.ts +7 -4
- package/lib/types/mobile/mux.d.ts.map +1 -1
- package/lib/types/mobile/mux.js +43 -15
- package/lib/types/mobile/mux.test.js +57 -0
- package/lib/types/routes.d.ts +2 -0
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +5 -2
- package/package.json +15 -12
- package/src/client/RemoteEntry.tsx +3 -2
- package/src/client/UpdateEntry.tsx +1 -0
- package/src/client/index.ts +30 -13
- package/src/client/pair-api.ts +16 -0
- package/src/client/remote-channel.ts +21 -0
- package/src/index.ts +1 -1
- package/src/mobile/messages.test.ts +23 -0
- package/src/mobile/messages.ts +5 -3
- package/src/mobile/mux.test.ts +64 -0
- package/src/mobile/mux.ts +44 -17
- package/src/routes.ts +7 -2
|
@@ -204,9 +204,9 @@ export function RemoteEntry({ wide, useWorkspaces, t }) {
|
|
|
204
204
|
window.setTimeout(() => { setCopied(undefined); }, 1500);
|
|
205
205
|
});
|
|
206
206
|
}, []);
|
|
207
|
-
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.entryRow, "data-rail": wide ? undefined : 'rail', children: [_jsx(UpdateEntry, { wide: wide, t: t }), _jsx(TooltipAnchor, { wide: wide, label: t('entry.label'), onClick: openPanel })] }), open && createPortal((_jsxs("div", { className: css.overlay, role: "presentation", children: [_jsx("div", { className: css.mask, "aria-hidden": "true", onClick: closePanel }), _jsx(RemotePanel, { t: t, state: state, copied: copied, onClose: closePanel, onStop: handleStop, onRefresh: handleRefresh, onCopy: handleCopy, onPickAddress: handlePickAddress, onPickPublic: handlePickPublic, onRevoke: handleRevoke })] })), document.body)] }));
|
|
207
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.entryRow, "data-rail": wide ? undefined : 'rail', children: [_jsx(UpdateEntry, { wide: wide, t: t }), _jsx(TooltipAnchor, { wide: wide, label: t('entry.label'), onClick: openPanel, expanded: open })] }), open && createPortal((_jsxs("div", { className: css.overlay, role: "presentation", children: [_jsx("div", { className: css.mask, "aria-hidden": "true", onClick: closePanel }), _jsx(RemotePanel, { t: t, state: state, copied: copied, onClose: closePanel, onStop: handleStop, onRefresh: handleRefresh, onCopy: handleCopy, onPickAddress: handlePickAddress, onPickPublic: handlePickPublic, onRevoke: handleRevoke })] })), document.body)] }));
|
|
208
208
|
}
|
|
209
209
|
/** The trigger: an icon-only control with a persistent accessible label. */
|
|
210
|
-
function TooltipAnchor({ wide, label, onClick }) {
|
|
211
|
-
return (_jsx("button", { type: "button", className: css.trigger, "data-wide": wide ? 'wide' : 'rail', "aria-label": label, title: label, onClick: onClick, children: _jsx(PhoneIcon, { size: wide ? 16 : 18 }) }));
|
|
210
|
+
function TooltipAnchor({ wide, label, onClick, expanded }) {
|
|
211
|
+
return (_jsx("button", { type: "button", className: css.trigger, "data-wide": wide ? 'wide' : 'rail', "aria-label": label, "aria-expanded": expanded, title: label, onClick: onClick, children: _jsx(PhoneIcon, { size: wide ? 16 : 18 }) }));
|
|
212
212
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UpdateEntry.d.ts","sourceRoot":"","sources":["../../../src/client/UpdateEntry.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AAOnE,wEAAwE;AACxE,MAAM,WAAW,gBAAgB;IAC/B,oEAAoE;IACpE,IAAI,EAAE,OAAO,CAAA;IACb,CAAC,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;CACzB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,gBAAgB,+
|
|
1
|
+
{"version":3,"file":"UpdateEntry.d.ts","sourceRoot":"","sources":["../../../src/client/UpdateEntry.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AAOnE,wEAAwE;AACxE,MAAM,WAAW,gBAAgB;IAC/B,oEAAoE;IACpE,IAAI,EAAE,OAAO,CAAA;IACb,CAAC,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;CACzB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,gBAAgB,+BA+GxD"}
|
|
@@ -108,5 +108,5 @@ export function UpdateEntry({ wide, t }) {
|
|
|
108
108
|
};
|
|
109
109
|
}, [probeAvailability]);
|
|
110
110
|
const updateLabel = updateAvailable ? t("update.availableLabel") : t("update.label");
|
|
111
|
-
return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: css.trigger, "data-wide": wide ? undefined : "rail", "data-update-available": updateAvailable ? "true" : undefined, "aria-label": updateLabel, title: updateLabel, onClick: openPanel, children: _jsx(IconDownloadOutline16, { size: wide ? 16 : 18 }) }), open && createPortal((_jsxs("div", { className: css.overlay, role: "presentation", children: [_jsx("div", { className: css.mask, "aria-hidden": "true", onClick: closePanel }), _jsx(UpdatePanel, { t: t, view: view, onClose: closePanel, onRecheck: () => { void check(); }, onStartUpdate: (status) => { void startUpdate(status); } })] })), document.body)] }));
|
|
111
|
+
return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: css.trigger, "data-wide": wide ? undefined : "rail", "data-update-available": updateAvailable ? "true" : undefined, "aria-label": updateLabel, "aria-expanded": open, title: updateLabel, onClick: openPanel, children: _jsx(IconDownloadOutline16, { size: wide ? 16 : 18 }) }), open && createPortal((_jsxs("div", { className: css.overlay, role: "presentation", children: [_jsx("div", { className: css.mask, "aria-hidden": "true", onClick: closePanel }), _jsx(UpdatePanel, { t: t, view: view, onClose: closePanel, onRecheck: () => { void check(); }, onStartUpdate: (status) => { void startUpdate(status); } })] })), document.body)] }));
|
|
112
112
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAA;AAa7G,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,cAAc,CAAA;AAMrD,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACzD,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACrE,YAAY,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AACnE,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAC7C,YAAY,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAA;AAC/F,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACzD,YAAY,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAErE,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,0CAA0C;QAC1C,MAAM,EAAE,SAAS,CAAA;KAClB;IAED,UAAU,OAAO;QACf;;;;WAIG;QACH,gBAAgB,EAAE;YAAE,IAAI,EAAE,QAAQ,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,uBAAuB,CAAA;SAAE,CAAA;QACnF;;;;;WAKG;QACH,oBAAoB,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,4BAA4B,CAAA;SAAE,CAAA;KAC3F;CACF;AAED,4GAA4G;AAC5G,MAAM,WAAW,uBAAuB;IACtC,oEAAoE;IACpE,IAAI,EAAE,OAAO,CAAA;CACd;AAED,mEAAmE;AACnE,MAAM,WAAW,4BAA4B;IAC3C,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,KAAK,CAAA;CACjB;AAED,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf;;;;WAIG;QACH,aAAa,CAAC,EAAE;YAAE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;SAAE,CAAA;KAC1E;CACF;AAYD,wCAAwC;AACxC,eAAO,MAAM,MAAM,UAA+D,CAAA;AAElF;;;GAGG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAA;AAa7G,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,cAAc,CAAA;AAMrD,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACzD,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACrE,YAAY,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AACnE,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAC7C,YAAY,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAA;AAC/F,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACzD,YAAY,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAErE,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,0CAA0C;QAC1C,MAAM,EAAE,SAAS,CAAA;KAClB;IAED,UAAU,OAAO;QACf;;;;WAIG;QACH,gBAAgB,EAAE;YAAE,IAAI,EAAE,QAAQ,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,uBAAuB,CAAA;SAAE,CAAA;QACnF;;;;;WAKG;QACH,oBAAoB,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,4BAA4B,CAAA;SAAE,CAAA;KAC3F;CACF;AAED,4GAA4G;AAC5G,MAAM,WAAW,uBAAuB;IACtC,oEAAoE;IACpE,IAAI,EAAE,OAAO,CAAA;CACd;AAED,mEAAmE;AACnE,MAAM,WAAW,4BAA4B;IAC3C,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,KAAK,CAAA;CACjB;AAED,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf;;;;WAIG;QACH,aAAa,CAAC,EAAE;YAAE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;SAAE,CAAA;KAC1E;CACF;AAYD,wCAAwC;AACxC,eAAO,MAAM,MAAM,UAA+D,CAAA;AAElF;;;GAGG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAqL9C"}
|
|
@@ -14,8 +14,8 @@ import { PairFailedNotice } from "./PairFailedNotice.js";
|
|
|
14
14
|
import { RemoteSettingsCard, RemoteSettingsCardController } from "./RemoteSettingsCard.js";
|
|
15
15
|
import { en, zh } from "./locales.js";
|
|
16
16
|
import { PAIR_FAILED_MARKER, runPairBootFlow } from "./deep-link.js";
|
|
17
|
-
import { sendHeartbeat } from "./pair-api.js";
|
|
18
|
-
import { channelTransition, installRemoteChannel, isLoopbackHostname } from "./remote-channel.js";
|
|
17
|
+
import { readPairGatePolicy, sendHeartbeat } from "./pair-api.js";
|
|
18
|
+
import { channelTransition, installRemoteChannel, isLoopbackHostname, remoteChannelRequired } from "./remote-channel.js";
|
|
19
19
|
import { FenceNotice } from "./FenceNotice.js";
|
|
20
20
|
/** Dictionary namespace owned by this plugin. */
|
|
21
21
|
const NS = 'remote';
|
|
@@ -130,6 +130,8 @@ export function apply(ctx) {
|
|
|
130
130
|
// rewritten onto this plugin's gated /remote/api prefix (remote-channel.ts)
|
|
131
131
|
// while the fence setting demands it. Loopback origins are untouched.
|
|
132
132
|
let disposeChannel;
|
|
133
|
+
let hostPairingPolicy;
|
|
134
|
+
let unpairedWhilePolicyPending = false;
|
|
133
135
|
let fenceNotice;
|
|
134
136
|
const showFenceNotice = () => {
|
|
135
137
|
if (fenceNotice !== undefined)
|
|
@@ -144,23 +146,19 @@ export function apply(ctx) {
|
|
|
144
146
|
fenceNotice?.unmount();
|
|
145
147
|
fenceNotice = undefined;
|
|
146
148
|
};
|
|
147
|
-
const
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
// what the channel gates), so waiting for 'ready' here would deadlock
|
|
154
|
-
// the channel off exactly where it is needed most.
|
|
155
|
-
const snapshot = settingsScope.getSnapshot();
|
|
156
|
-
const value = snapshot.status === 'ready' ? snapshot.value : undefined;
|
|
157
|
-
return (value?.enabled ?? true) && (value?.requirePairingForLan ?? true);
|
|
149
|
+
const handleUnpaired = () => {
|
|
150
|
+
if (settingsScope.getSnapshot().status !== 'ready' && hostPairingPolicy === undefined) {
|
|
151
|
+
unpairedWhilePolicyPending = true;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
showFenceNotice();
|
|
158
155
|
};
|
|
156
|
+
const channelActive = () => remoteChannelRequired(window.location.hostname, settingsScope.getSnapshot(), hostPairingPolicy);
|
|
159
157
|
const syncChannel = () => {
|
|
160
158
|
const transition = channelTransition(channelActive(), disposeChannel !== undefined);
|
|
161
159
|
if (transition === 'install') {
|
|
162
160
|
disposeChannel = ctx.effect(() => {
|
|
163
|
-
const restore = installRemoteChannel(window, { onUnpaired:
|
|
161
|
+
const restore = installRemoteChannel(window, { onUnpaired: handleUnpaired, onPaired: hideFenceNotice });
|
|
164
162
|
return restore;
|
|
165
163
|
}, 'remote-web-ui: remote desktop channel');
|
|
166
164
|
}
|
|
@@ -178,6 +176,22 @@ export function apply(ctx) {
|
|
|
178
176
|
};
|
|
179
177
|
settingsScope.subscribe(syncChannel);
|
|
180
178
|
syncChannel();
|
|
179
|
+
if (!isLoopbackHostname(window.location.hostname) && settingsScope.getSnapshot().status !== 'ready') {
|
|
180
|
+
void readPairGatePolicy().then((policy) => {
|
|
181
|
+
hostPairingPolicy = policy.requirePairingForLan;
|
|
182
|
+
syncChannel();
|
|
183
|
+
if (hostPairingPolicy && unpairedWhilePolicyPending)
|
|
184
|
+
showFenceNotice();
|
|
185
|
+
unpairedWhilePolicyPending = false;
|
|
186
|
+
}).catch(() => {
|
|
187
|
+
// Fail closed when the policy endpoint is unavailable or malformed.
|
|
188
|
+
hostPairingPolicy = true;
|
|
189
|
+
syncChannel();
|
|
190
|
+
if (unpairedWhilePolicyPending)
|
|
191
|
+
showFenceNotice();
|
|
192
|
+
unpairedWhilePolicyPending = false;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
181
195
|
// One-time failed-pair toast. The accept result lands asynchronously, so
|
|
182
196
|
// the marker check is deferred past the accept round trip.
|
|
183
197
|
ctx.effect(() => {
|
|
@@ -30,6 +30,12 @@ export interface IssueLoopbackRequired {
|
|
|
30
30
|
code: 'forbidden';
|
|
31
31
|
}
|
|
32
32
|
export type IssueResponse = IssueResult | IssueLanRequired | IssueUnknownAddress | IssueLoopbackRequired;
|
|
33
|
+
/** Public policy fields returned by the pairing status endpoint. */
|
|
34
|
+
export interface PairGatePolicy {
|
|
35
|
+
requirePairingForLan: boolean;
|
|
36
|
+
}
|
|
37
|
+
/** Read the host-authoritative desktop pairing policy. */
|
|
38
|
+
export declare function readPairGatePolicy(): Promise<PairGatePolicy>;
|
|
33
39
|
/** accept() refusal codes. */
|
|
34
40
|
export type AcceptFailure = {
|
|
35
41
|
ok: false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pair-api.d.ts","sourceRoot":"","sources":["../../../src/client/pair-api.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pair-api.d.ts","sourceRoot":"","sources":["../../../src/client/pair-api.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
* same-origin /api (like the connection client); JSON bodies, string
|
|
4
4
|
* responses for the error codes.
|
|
5
5
|
*/
|
|
6
|
+
/** Read the host-authoritative desktop pairing policy. */
|
|
7
|
+
export async function readPairGatePolicy() {
|
|
8
|
+
const response = await fetch('/api/pair/status');
|
|
9
|
+
if (!response.ok)
|
|
10
|
+
throw new Error(`remote-web-ui: status failed with ${String(response.status)}`);
|
|
11
|
+
const value = await response.json();
|
|
12
|
+
if (typeof value.requirePairingForLan !== 'boolean') {
|
|
13
|
+
throw new Error('remote-web-ui: status omitted requirePairingForLan');
|
|
14
|
+
}
|
|
15
|
+
return { requirePairingForLan: value.requirePairingForLan };
|
|
16
|
+
}
|
|
6
17
|
/**
|
|
7
18
|
* Mint a fresh pairing token (one active token at a time — this invalidates
|
|
8
19
|
* any previous link).
|
|
@@ -27,6 +27,16 @@
|
|
|
27
27
|
export declare const REMOTE_PREFIX = "/remote";
|
|
28
28
|
/** Connection-plugin method prefix under the gated channel. */
|
|
29
29
|
export declare const REMOTE_API_PREFIX = "/remote/api";
|
|
30
|
+
/** Minimal settings snapshot used by the remote channel decision. */
|
|
31
|
+
export interface RemoteChannelSettingsSnapshot {
|
|
32
|
+
status: 'ready' | 'loading' | 'unavailable' | string;
|
|
33
|
+
value?: {
|
|
34
|
+
enabled?: boolean;
|
|
35
|
+
requirePairingForLan?: boolean;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Decide whether a remote desktop channel is required from local or host policy. */
|
|
39
|
+
export declare function remoteChannelRequired(hostname: string, snapshot: RemoteChannelSettingsSnapshot, hostPairingPolicy: boolean | undefined): boolean;
|
|
30
40
|
/**
|
|
31
41
|
* Browser-safe loopback classification for the page origin (the SDK client
|
|
32
42
|
* exports its own; this copy keeps the module dependency-free).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-channel.d.ts","sourceRoot":"","sources":["../../../src/client/remote-channel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,kEAAkE;AAClE,eAAO,MAAM,aAAa,YAAY,CAAA;AAEtC,+DAA+D;AAC/D,eAAO,MAAM,iBAAiB,gBAAyB,CAAA;AAkBvD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAI5D;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAUhE;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;CAC3C;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,CAwE1G;AAED;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAA;AAE7D;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,GAAG,iBAAiB,CAIxF"}
|
|
1
|
+
{"version":3,"file":"remote-channel.d.ts","sourceRoot":"","sources":["../../../src/client/remote-channel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,kEAAkE;AAClE,eAAO,MAAM,aAAa,YAAY,CAAA;AAEtC,+DAA+D;AAC/D,eAAO,MAAM,iBAAiB,gBAAyB,CAAA;AAkBvD,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,CAUhE;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;CAC3C;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,CAwE1G;AAED;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAA;AAE7D;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,GAAG,iBAAiB,CAIxF"}
|
|
@@ -42,6 +42,17 @@ const WS_PATHS = new Set([
|
|
|
42
42
|
'/sidebar/ws/agent-terminals',
|
|
43
43
|
'/api/dsh-ssh/terminal',
|
|
44
44
|
]);
|
|
45
|
+
/** Decide whether a remote desktop channel is required from local or host policy. */
|
|
46
|
+
export function remoteChannelRequired(hostname, snapshot, hostPairingPolicy) {
|
|
47
|
+
if (isLoopbackHostname(hostname))
|
|
48
|
+
return false;
|
|
49
|
+
if (snapshot.status === 'ready') {
|
|
50
|
+
return (snapshot.value?.enabled ?? true) && (snapshot.value?.requirePairingForLan ?? true);
|
|
51
|
+
}
|
|
52
|
+
// Install provisionally while the host probe is pending so early SDK calls
|
|
53
|
+
// cannot escape onto the plain remote origin. A confirmed false retires it.
|
|
54
|
+
return hostPairingPolicy !== false;
|
|
55
|
+
}
|
|
45
56
|
/**
|
|
46
57
|
* Browser-safe loopback classification for the page origin (the SDK client
|
|
47
58
|
* exports its own; this copy keeps the module dependency-free).
|
package/lib/types/index.js
CHANGED
|
@@ -248,7 +248,7 @@ function applyImpl(ctx, config) {
|
|
|
248
248
|
},
|
|
249
249
|
});
|
|
250
250
|
const routes = [
|
|
251
|
-
...makeRoutes({ service, lanAddresses }),
|
|
251
|
+
...makeRoutes({ service, lanAddresses, requirePairingForLan: () => resolve().requirePairingForLan }),
|
|
252
252
|
...makeMobileRoutes(),
|
|
253
253
|
...(apiProxy !== undefined
|
|
254
254
|
? makeMobileApiRoutes({ service, apiProxy, mobileEnterToSend: () => resolve().mobileEnterToSend })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../../src/mobile/messages.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,MAAM,WAAW,aAAa;IAC5B,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAA;IACnC,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,EAAE,CAAA;IAC/B,8EAA8E;IAC9E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,oCAAoC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,kFAAkF;IAClF,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B,sFAAsF;IACtF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,kDAAkD;IAClD,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE;QACf,WAAW,EAAE,MAAM,CAAA;QACnB,YAAY,EAAE,MAAM,CAAA;QACpB,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAC1B,CAAA;IACD,sFAAsF;IACtF,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;IAC/B,gEAAgE;IAChE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B;AAED,+EAA+E;AAC/E,MAAM,WAAW,YAAY;IAC3B,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,8BAA8B;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,qDAAqD;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;IACnC,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,IAAI,CAAA;CAC1B;
|
|
1
|
+
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../../src/mobile/messages.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,MAAM,WAAW,aAAa;IAC5B,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAA;IACnC,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,EAAE,CAAA;IAC/B,8EAA8E;IAC9E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,oCAAoC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,kFAAkF;IAClF,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B,sFAAsF;IACtF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,kDAAkD;IAClD,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE;QACf,WAAW,EAAE,MAAM,CAAA;QACnB,YAAY,EAAE,MAAM,CAAA;QACpB,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAC1B,CAAA;IACD,sFAAsF;IACtF,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;IAC/B,gEAAgE;IAChE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B;AAED,+EAA+E;AAC/E,MAAM,WAAW,YAAY;IAC3B,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,8BAA8B;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,qDAAqD;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;IACnC,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,IAAI,CAAA;CAC1B;AAyeD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,EAAE,QAAQ,CAAC,EAAE,SAAS,aAAa,EAAE,GAAG,aAAa,EAAE,CAE7G;AAED;;;;;;;;;GASG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,KAAK,CAAW;IACxB,OAAO,CAAC,YAAY,CAA6B;IAEjD,gFAAgF;gBACpE,OAAO,CAAC,EAAE,SAAS,aAAa,EAAE;IAI9C,uEAAuE;IACvE,IAAI,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,GAAG,aAAa,EAAE;IAanD,kEAAkE;IAClE,IAAI,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,GAAG,IAAI;IAK9C,uEAAuE;IACvE,OAAO,CAAC,KAAK,EAAE,SAAS,aAAa,EAAE,GAAG,IAAI;IAK9C,uEAAuE;IACvE,QAAQ,IAAI,aAAa,EAAE;CAK5B"}
|
|
@@ -470,7 +470,8 @@ function applyTurnEnd(state, event) {
|
|
|
470
470
|
...message,
|
|
471
471
|
...(wasPending ? { pending: false } : {}),
|
|
472
472
|
...(failed ? { failed: true } : {}),
|
|
473
|
-
|
|
473
|
+
// Preserve each step's own final-event seq. Collapsing every message
|
|
474
|
+
// onto turn/end makes same-turn ordering depend on arbitrary ids.
|
|
474
475
|
time: event.time,
|
|
475
476
|
});
|
|
476
477
|
}
|
|
@@ -542,10 +543,11 @@ function snapshotOf(state) {
|
|
|
542
543
|
for (let index = 1; index < out.length; index += 1) {
|
|
543
544
|
const prev = out[index - 1];
|
|
544
545
|
const current = out[index];
|
|
545
|
-
if (prev.seq > current.seq
|
|
546
|
+
if (prev.seq > current.seq) {
|
|
546
547
|
ordered = false;
|
|
547
548
|
break;
|
|
548
549
|
}
|
|
549
550
|
}
|
|
550
|
-
|
|
551
|
+
// Array.sort is stable: equal-seq rows keep their event insertion order.
|
|
552
|
+
return ordered ? out : out.sort((a, b) => a.seq - b.seq);
|
|
551
553
|
}
|
|
@@ -152,6 +152,26 @@ describe('foldEvents', () => {
|
|
|
152
152
|
expect(assistant?.failed).toBeFalsy();
|
|
153
153
|
expect(assistant?.pending).toBeFalsy();
|
|
154
154
|
});
|
|
155
|
+
it('keeps same-turn steps in final-event order after turn/end', () => {
|
|
156
|
+
const result = foldEvents([
|
|
157
|
+
makeEvent('user/message', userMessageData('u-1', 'multi-step'), 0),
|
|
158
|
+
makeEvent('assistant/message', assistantMessageData('step-z', 0, 0, 'first'), 2),
|
|
159
|
+
makeEvent('assistant/message', assistantMessageData('step-a', 0, 1, 'second'), 4),
|
|
160
|
+
makeEvent('assistant/message', assistantMessageData('step-m', 0, 2, 'third'), 6),
|
|
161
|
+
makeEvent('turn/end', { turn: 0, reason: { kind: 'completed' } }, 7),
|
|
162
|
+
]);
|
|
163
|
+
const assistants = result.filter(message => message.kind === 'assistant');
|
|
164
|
+
expect(assistants.map(message => message.id)).toEqual(['step-z', 'step-a', 'step-m']);
|
|
165
|
+
expect(assistants.map(message => message.seq)).toEqual([2, 4, 6]);
|
|
166
|
+
expect(assistants.every(message => message.pending !== true)).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
it('keeps stable insertion order when legacy rows share a seq', () => {
|
|
169
|
+
const folder = new EventFolder([
|
|
170
|
+
{ id: 'z-last-lexically', kind: 'assistant', text: 'first', seq: 5, time: 5_000 },
|
|
171
|
+
{ id: 'a-first-lexically', kind: 'assistant', text: 'second', seq: 5, time: 5_000 },
|
|
172
|
+
]);
|
|
173
|
+
expect(folder.snapshot().map(message => message.id)).toEqual(['z-last-lexically', 'a-first-lexically']);
|
|
174
|
+
});
|
|
155
175
|
it('is idempotent: applying the same batch twice yields an identical list', () => {
|
|
156
176
|
const events = [
|
|
157
177
|
makeEvent('user/message', userMessageData('u-1', 'hi'), 0),
|
|
@@ -35,9 +35,9 @@ export interface MuxClientOptions {
|
|
|
35
35
|
* the ordinary HTTP channel (unaffected by SSE-impairing tunnels).
|
|
36
36
|
*/
|
|
37
37
|
pollLatest?: (sessionId: string) => Promise<HistoryPage>;
|
|
38
|
-
/**
|
|
38
|
+
/** Initial poll cadence while SSE is stalled (default 3000 ms). Empty polls back off to 60000 ms. */
|
|
39
39
|
pollIntervalMs?: number;
|
|
40
|
-
/**
|
|
40
|
+
/** Initial SSE stall window (default 12000 ms); a previously-live stream gets three windows before fallback. */
|
|
41
41
|
stallThresholdMs?: number;
|
|
42
42
|
/** Clock seam for tests (defaults to Date.now). */
|
|
43
43
|
now?: () => number;
|
|
@@ -72,8 +72,8 @@ export declare class MuxClient {
|
|
|
72
72
|
private lastDataAt;
|
|
73
73
|
/**
|
|
74
74
|
* Whether the SSE channel has ever delivered a frame in this stream (a
|
|
75
|
-
* delivered frame proves the tunnel
|
|
76
|
-
*
|
|
75
|
+
* delivered frame proves the tunnel can forward SSE; sustained silence may
|
|
76
|
+
* still mean a suspended mobile tunnel, so polling re-arms after 3 windows).
|
|
77
77
|
*/
|
|
78
78
|
private sseAlive;
|
|
79
79
|
/** Per-session highest event seq already emitted, for poll dedup. */
|
|
@@ -83,6 +83,8 @@ export declare class MuxClient {
|
|
|
83
83
|
private polling;
|
|
84
84
|
/** Epoch ms of the next due poll while polling (kept on the same tick timer). */
|
|
85
85
|
private nextPollAt;
|
|
86
|
+
/** Adaptive delay: productive polls reset it; empty/error polls add one base interval up to one minute. */
|
|
87
|
+
private pollDelayMs;
|
|
86
88
|
/**
|
|
87
89
|
* @param url - the mobile events endpoint (browser-relative).
|
|
88
90
|
* @param options - seams.
|
|
@@ -110,6 +112,7 @@ export declare class MuxClient {
|
|
|
110
112
|
private startTick;
|
|
111
113
|
private stopTick;
|
|
112
114
|
private tick;
|
|
115
|
+
private isSseStalled;
|
|
113
116
|
private startPolling;
|
|
114
117
|
private stopPolling;
|
|
115
118
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mux.d.ts","sourceRoot":"","sources":["../../../src/mobile/mux.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2CAA2C,CAAA;AAGzE,OAAO,EAA2B,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AAEpE,kCAAkC;AAClC,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,eAAe,CAAA;IAChD;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAA;IACxD,
|
|
1
|
+
{"version":3,"file":"mux.d.ts","sourceRoot":"","sources":["../../../src/mobile/mux.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2CAA2C,CAAA;AAGzE,OAAO,EAA2B,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AAEpE,kCAAkC;AAClC,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,eAAe,CAAA;IAChD;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAA;IACxD,qGAAqG;IACrG,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,gHAAgH;IAChH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,mDAAmD;IACnD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED,0EAA0E;AAC1E,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,CAAC,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,IAAI,CAAA;IACrD,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,IAAI,CAAA;IAC1C,KAAK,IAAI,IAAI,CAAA;CACd;AAyBD;;;;;GAKG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAkC;IAChE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6C;IACxE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAQ;IACvC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAQ;IACzC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAuC;IACjE,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;IAE5B,wEAAwE;IACxE,OAAO,CAAC,gBAAgB,CAAoB;IAC5C,6EAA6E;IAC7E,OAAO,CAAC,UAAU,CAAI;IACtB;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAQ;IACxB,qEAAqE;IACrE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA4B;IAC1D,+FAA+F;IAC/F,OAAO,CAAC,SAAS,CAA4C;IAC7D,OAAO,CAAC,OAAO,CAAQ;IACvB,iFAAiF;IACjF,OAAO,CAAC,UAAU,CAAI;IACtB,2GAA2G;IAC3G,OAAO,CAAC,WAAW,CAAQ;IAE3B;;;OAGG;gBACS,GAAG,SAAsB,EAAE,OAAO,GAAE,gBAAqB;IAUrE,+EAA+E;IAC/E,KAAK,IAAI,IAAI;IAOb,sBAAsB;IACtB,IAAI,IAAI,IAAI;IASZ,sEAAsE;IACtE,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,GAAG,MAAM,IAAI;IAKxD;;;;OAIG;IACH,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAU5C,OAAO,CAAC,OAAO;IAqBf;;;;;OAKG;IACH,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,IAAI;IAgBZ,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,YAAY;IAQpB,OAAO,CAAC,WAAW;IAMnB;;;;OAIG;YACW,QAAQ;IAsCtB,OAAO,CAAC,aAAa;IAuBrB,OAAO,CAAC,IAAI;IAUZ,OAAO,CAAC,WAAW;CAapB"}
|
package/lib/types/mobile/mux.js
CHANGED
|
@@ -34,6 +34,8 @@ function browserSource(url) {
|
|
|
34
34
|
}
|
|
35
35
|
const DEFAULT_POLL_INTERVAL_MS = 3000;
|
|
36
36
|
const DEFAULT_STALL_THRESHOLD_MS = 12000;
|
|
37
|
+
const LIVE_SSE_STALL_MULTIPLIER = 3;
|
|
38
|
+
const MAX_POLL_BACKOFF_MS = 60000;
|
|
37
39
|
/**
|
|
38
40
|
* Stall-check granularity: the single scheduler tick runs at least this
|
|
39
41
|
* often so fallback arms within a second of the stall threshold passing,
|
|
@@ -64,8 +66,8 @@ export class MuxClient {
|
|
|
64
66
|
lastDataAt = 0;
|
|
65
67
|
/**
|
|
66
68
|
* Whether the SSE channel has ever delivered a frame in this stream (a
|
|
67
|
-
* delivered frame proves the tunnel
|
|
68
|
-
*
|
|
69
|
+
* delivered frame proves the tunnel can forward SSE; sustained silence may
|
|
70
|
+
* still mean a suspended mobile tunnel, so polling re-arms after 3 windows).
|
|
69
71
|
*/
|
|
70
72
|
sseAlive = false;
|
|
71
73
|
/** Per-session highest event seq already emitted, for poll dedup. */
|
|
@@ -75,6 +77,8 @@ export class MuxClient {
|
|
|
75
77
|
polling = false;
|
|
76
78
|
/** Epoch ms of the next due poll while polling (kept on the same tick timer). */
|
|
77
79
|
nextPollAt = 0;
|
|
80
|
+
/** Adaptive delay: productive polls reset it; empty/error polls add one base interval up to one minute. */
|
|
81
|
+
pollDelayMs;
|
|
78
82
|
/**
|
|
79
83
|
* @param url - the mobile events endpoint (browser-relative).
|
|
80
84
|
* @param options - seams.
|
|
@@ -84,6 +88,7 @@ export class MuxClient {
|
|
|
84
88
|
this.sourceFactory = options.sourceFactory ?? browserSource;
|
|
85
89
|
this.pollLatest = options.pollLatest ?? ((sessionId) => fetchHistory(sessionId, undefined, DEFAULT_POLL_PAGE_SIZE));
|
|
86
90
|
this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
91
|
+
this.pollDelayMs = this.pollIntervalMs;
|
|
87
92
|
this.stallThresholdMs = options.stallThresholdMs ?? DEFAULT_STALL_THRESHOLD_MS;
|
|
88
93
|
this.now = options.now ?? (() => Date.now());
|
|
89
94
|
}
|
|
@@ -121,9 +126,8 @@ export class MuxClient {
|
|
|
121
126
|
return;
|
|
122
127
|
}
|
|
123
128
|
// If SSE is already stalled for this session, start patching right away.
|
|
124
|
-
if (!this.polling && !this.stopped &&
|
|
129
|
+
if (!this.polling && !this.stopped && this.isSseStalled())
|
|
125
130
|
this.startPolling();
|
|
126
|
-
}
|
|
127
131
|
}
|
|
128
132
|
connect() {
|
|
129
133
|
// A fresh stream starts unknown; only a delivered frame proves it works.
|
|
@@ -170,30 +174,36 @@ export class MuxClient {
|
|
|
170
174
|
if (this.observeSessionId === undefined)
|
|
171
175
|
return;
|
|
172
176
|
if (this.polling) {
|
|
173
|
-
// The stall phase has ended; the same tick now paces the polls.
|
|
177
|
+
// The stall phase has ended; the same tick now paces the adaptive polls.
|
|
174
178
|
if (this.now() >= this.nextPollAt) {
|
|
175
|
-
|
|
179
|
+
// pollTick schedules the next run after it settles, so slow requests
|
|
180
|
+
// cannot overlap with another scheduler tick.
|
|
181
|
+
this.nextPollAt = Number.POSITIVE_INFINITY;
|
|
176
182
|
void this.pollTick();
|
|
177
183
|
}
|
|
178
184
|
return;
|
|
179
185
|
}
|
|
180
|
-
|
|
181
|
-
// against it. Fallback arms again only via onerror or a stream that has
|
|
182
|
-
// never delivered.
|
|
183
|
-
if (this.sseAlive)
|
|
184
|
-
return;
|
|
185
|
-
if ((this.now() - this.lastDataAt) > this.stallThresholdMs)
|
|
186
|
+
if (this.isSseStalled())
|
|
186
187
|
this.startPolling();
|
|
187
188
|
}
|
|
189
|
+
isSseStalled() {
|
|
190
|
+
const windowMs = this.sseAlive
|
|
191
|
+
? this.stallThresholdMs * LIVE_SSE_STALL_MULTIPLIER
|
|
192
|
+
: this.stallThresholdMs;
|
|
193
|
+
return (this.now() - this.lastDataAt) > windowMs;
|
|
194
|
+
}
|
|
188
195
|
startPolling() {
|
|
189
196
|
if (this.polling || this.stopped)
|
|
190
197
|
return;
|
|
191
198
|
this.polling = true;
|
|
192
|
-
this.
|
|
199
|
+
this.pollDelayMs = this.pollIntervalMs;
|
|
200
|
+
this.nextPollAt = Number.POSITIVE_INFINITY;
|
|
193
201
|
void this.pollTick();
|
|
194
202
|
}
|
|
195
203
|
stopPolling() {
|
|
196
204
|
this.polling = false;
|
|
205
|
+
this.pollDelayMs = this.pollIntervalMs;
|
|
206
|
+
this.nextPollAt = 0;
|
|
197
207
|
}
|
|
198
208
|
/**
|
|
199
209
|
* Fetch the latest history page for the observed session and re-emit any
|
|
@@ -206,21 +216,39 @@ export class MuxClient {
|
|
|
206
216
|
this.stopPolling();
|
|
207
217
|
return;
|
|
208
218
|
}
|
|
219
|
+
let emitted = 0;
|
|
209
220
|
try {
|
|
210
221
|
const page = await this.pollLatest(sessionId);
|
|
211
222
|
let maxSeq = this.pollWatermark.get(sessionId) ?? -1;
|
|
212
|
-
|
|
223
|
+
const ordered = [...page.events].sort((left, right) => {
|
|
224
|
+
const leftSeq = typeof left.event?.seq === 'number' ? left.event.seq : -1;
|
|
225
|
+
const rightSeq = typeof right.event?.seq === 'number' ? right.event.seq : -1;
|
|
226
|
+
return leftSeq - rightSeq;
|
|
227
|
+
});
|
|
228
|
+
for (const entry of ordered) {
|
|
213
229
|
const event = entry.event;
|
|
214
230
|
const seq = typeof event?.seq === 'number' ? event.seq : -1;
|
|
215
231
|
if (seq <= maxSeq)
|
|
216
232
|
continue;
|
|
217
233
|
maxSeq = seq;
|
|
234
|
+
emitted += 1;
|
|
218
235
|
this.emit({ type: 'session/event', sessionId: sessionId, event });
|
|
219
236
|
}
|
|
220
237
|
this.pollWatermark.set(sessionId, maxSeq);
|
|
221
238
|
}
|
|
222
239
|
catch {
|
|
223
|
-
// Transient (network, pairing, history paging);
|
|
240
|
+
// Transient (network, pairing, history paging); retry with backoff.
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
if (emitted > 0) {
|
|
244
|
+
this.pollDelayMs = this.pollIntervalMs;
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
this.pollDelayMs = Math.min(MAX_POLL_BACKOFF_MS, this.pollDelayMs + this.pollIntervalMs);
|
|
248
|
+
}
|
|
249
|
+
if (this.polling && this.observeSessionId === sessionId) {
|
|
250
|
+
this.nextPollAt = this.now() + this.pollDelayMs;
|
|
251
|
+
}
|
|
224
252
|
}
|
|
225
253
|
}
|
|
226
254
|
handleMessage(data) {
|
|
@@ -76,6 +76,21 @@ describe('MuxClient polling fallback', () => {
|
|
|
76
76
|
expect(frames[1]?.event).toMatchObject({ seq: 1 });
|
|
77
77
|
client.stop();
|
|
78
78
|
});
|
|
79
|
+
it('sorts an out-of-order history page before advancing the watermark', async () => {
|
|
80
|
+
const { factory } = makeSources();
|
|
81
|
+
const pollLatest = vi.fn(async (_sessionId) => pageOf([2, 1, 3]));
|
|
82
|
+
const client = new MuxClient('/m/api/events.mux', baseOptions(pollLatest, factory));
|
|
83
|
+
const seqs = [];
|
|
84
|
+
client.onFrame((frame) => {
|
|
85
|
+
if (frame.type === 'session/event' && typeof frame.event.seq === 'number')
|
|
86
|
+
seqs.push(frame.event.seq);
|
|
87
|
+
});
|
|
88
|
+
client.start();
|
|
89
|
+
client.observe('s1');
|
|
90
|
+
await vi.advanceTimersByTimeAsync(1200);
|
|
91
|
+
expect(seqs).toEqual([1, 2, 3]);
|
|
92
|
+
client.stop();
|
|
93
|
+
});
|
|
79
94
|
it('keeps the watermark so a repeated page never re-emits old events', async () => {
|
|
80
95
|
const { factory } = makeSources();
|
|
81
96
|
// Two calls return the same page: the second must emit nothing.
|
|
@@ -139,6 +154,48 @@ describe('MuxClient polling fallback', () => {
|
|
|
139
154
|
expect(live[0]).toMatchObject({ type: 'session/subscribed', sessionId: 's1' });
|
|
140
155
|
client.stop();
|
|
141
156
|
});
|
|
157
|
+
it('recovers when a previously-live SSE stream becomes silently stalled', async () => {
|
|
158
|
+
const { factory, sources } = makeSources();
|
|
159
|
+
const pollLatest = vi.fn(async (_sessionId) => pageOf([5]));
|
|
160
|
+
const client = new MuxClient('/m/api/events.mux', baseOptions(pollLatest, factory));
|
|
161
|
+
const frames = [];
|
|
162
|
+
client.onFrame(frame => { frames.push(frame); });
|
|
163
|
+
client.start();
|
|
164
|
+
client.observe('s1');
|
|
165
|
+
sources[0]?.onmessage?.({ data: envelopeWith({ type: 'session/subscribed', sessionId: 's1', lastSeq: 4 }) });
|
|
166
|
+
await vi.advanceTimersByTimeAsync(2400);
|
|
167
|
+
expect(pollLatest).not.toHaveBeenCalled();
|
|
168
|
+
// A once-live stream gets three stall windows; the next scheduler tick
|
|
169
|
+
// crosses that boundary and starts the ordinary-HTTP recovery path.
|
|
170
|
+
await vi.advanceTimersByTimeAsync(400);
|
|
171
|
+
expect(pollLatest).toHaveBeenCalledTimes(1);
|
|
172
|
+
expect(frames.at(-1)?.event).toMatchObject({ seq: 5 });
|
|
173
|
+
client.stop();
|
|
174
|
+
});
|
|
175
|
+
it('backs empty polls off and resets to the base cadence after progress', async () => {
|
|
176
|
+
const { factory } = makeSources();
|
|
177
|
+
const pages = [pageOf([]), pageOf([1]), pageOf([1, 2])];
|
|
178
|
+
const pollLatest = vi.fn(async (_sessionId) => pages.shift() ?? pageOf([]));
|
|
179
|
+
const client = new MuxClient('/m/api/events.mux', baseOptions(pollLatest, factory));
|
|
180
|
+
client.start();
|
|
181
|
+
client.observe('s1');
|
|
182
|
+
await vi.advanceTimersByTimeAsync(1200);
|
|
183
|
+
expect(pollLatest).toHaveBeenCalledTimes(1);
|
|
184
|
+
await vi.advanceTimersByTimeAsync(400);
|
|
185
|
+
expect(pollLatest).toHaveBeenCalledTimes(1);
|
|
186
|
+
await vi.advanceTimersByTimeAsync(400);
|
|
187
|
+
expect(pollLatest).toHaveBeenCalledTimes(2);
|
|
188
|
+
// The productive second poll resets the next delay from 800 ms to 400 ms.
|
|
189
|
+
await vi.advanceTimersByTimeAsync(400);
|
|
190
|
+
expect(pollLatest).toHaveBeenCalledTimes(3);
|
|
191
|
+
await vi.advanceTimersByTimeAsync(400);
|
|
192
|
+
expect(pollLatest).toHaveBeenCalledTimes(4);
|
|
193
|
+
await vi.advanceTimersByTimeAsync(400);
|
|
194
|
+
expect(pollLatest).toHaveBeenCalledTimes(4);
|
|
195
|
+
await vi.advanceTimersByTimeAsync(400);
|
|
196
|
+
expect(pollLatest).toHaveBeenCalledTimes(5);
|
|
197
|
+
client.stop();
|
|
198
|
+
});
|
|
142
199
|
it('drives the whole lifecycle on a single scheduler tick (one interval)', async () => {
|
|
143
200
|
const { factory } = makeSources();
|
|
144
201
|
const setIntervalSpy = vi.spyOn(globalThis, 'setInterval');
|
package/lib/types/routes.d.ts
CHANGED
|
@@ -84,6 +84,8 @@ export interface PairRoutesDeps {
|
|
|
84
84
|
service: PairingService;
|
|
85
85
|
/** The LAN IP literals the fence accepts (derived from the bind host). */
|
|
86
86
|
lanAddresses: readonly string[];
|
|
87
|
+
/** Current desktop gate policy, re-read for every status response. */
|
|
88
|
+
requirePairingForLan?: boolean | (() => boolean);
|
|
87
89
|
}
|
|
88
90
|
/**
|
|
89
91
|
* Build the /api/pair route family.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,EAAE,CAAC,EAAgB,MAAM,KAAK,CAAA;AACrC,OAAO,EAA0B,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;AAIhG;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CA6BtG;AAKD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOxE;AAKD,8CAA8C;AAC9C,eAAO,MAAM,UAAU;;;;;;;;CAQb,CAAA;AAEV;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB;;;iBAG7B,CAAA;AACF,eAAO,MAAM,mBAAmB;;iBAE9B,CAAA;AACF,eAAO,MAAM,mBAAmB;;iBAE9B,CAAA;AACF,eAAO,MAAM,uBAAuB,gCAA6B,CAAA;AA6BjE,gDAAgD;AAChD,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAElD;;OAEG;gBACS,OAAO,EAAE,cAAc;IAInC,4DAA4D;IAC5D,IAAI,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI;IAiBrD,kEAAkE;IAClE,IAAI,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI;IAYrC,wCAAwC;IACxC,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF;AAED,6CAA6C;AAC7C,MAAM,WAAW,cAAc;IAC7B,2BAA2B;IAC3B,OAAO,EAAE,cAAc,CAAA;IACvB,0EAA0E;IAC1E,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,EAAE,CAAC,EAAgB,MAAM,KAAK,CAAA;AACrC,OAAO,EAA0B,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;AAIhG;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CA6BtG;AAKD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOxE;AAKD,8CAA8C;AAC9C,eAAO,MAAM,UAAU;;;;;;;;CAQb,CAAA;AAEV;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB;;;iBAG7B,CAAA;AACF,eAAO,MAAM,mBAAmB;;iBAE9B,CAAA;AACF,eAAO,MAAM,mBAAmB;;iBAE9B,CAAA;AACF,eAAO,MAAM,uBAAuB,gCAA6B,CAAA;AA6BjE,gDAAgD;AAChD,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAElD;;OAEG;gBACS,OAAO,EAAE,cAAc;IAInC,4DAA4D;IAC5D,IAAI,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI;IAiBrD,kEAAkE;IAClE,IAAI,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI;IAYrC,wCAAwC;IACxC,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF;AAED,6CAA6C;AAC7C,MAAM,WAAW,cAAc;IAC7B,2BAA2B;IAC3B,OAAO,EAAE,cAAc,CAAA;IACvB,0EAA0E;IAC1E,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;IAC/B,sEAAsE;IACtE,oBAAoB,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,CAAA;CACjD;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,QAAQ,EAAE,CAoO3D"}
|
package/lib/types/routes.js
CHANGED
|
@@ -183,7 +183,10 @@ export class PairingEventsStream {
|
|
|
183
183
|
* @returns the exact routes to register on webServer.
|
|
184
184
|
*/
|
|
185
185
|
export function makeRoutes(deps) {
|
|
186
|
-
const { service, lanAddresses } = deps;
|
|
186
|
+
const { service, lanAddresses, requirePairingForLan = true } = deps;
|
|
187
|
+
const pairingRequired = () => typeof requirePairingForLan === 'function'
|
|
188
|
+
? requirePairingForLan()
|
|
189
|
+
: requirePairingForLan;
|
|
187
190
|
const events = new PairingEventsStream(service);
|
|
188
191
|
/** Loopback-only fence: the desktop panel's control endpoints. */
|
|
189
192
|
const loopbackFence = (req) => isTrustedApiRequest(req, []);
|
|
@@ -385,7 +388,7 @@ export function makeRoutes(deps) {
|
|
|
385
388
|
const visible = paired
|
|
386
389
|
? rest
|
|
387
390
|
: { phase: snapshot.phase, lanAvailable: snapshot.lanAvailable, lanAddresses: snapshot.lanAddresses };
|
|
388
|
-
writeJson(res, 200, { ok: true, paired, ...visible });
|
|
391
|
+
writeJson(res, 200, { ok: true, paired, requirePairingForLan: pairingRequired(), ...visible });
|
|
389
392
|
};
|
|
390
393
|
const handleEvents = (req, res) => {
|
|
391
394
|
if (!requireMethod(req, res, 'GET'))
|