@linxin666/dsh-remote-web-ui 0.1.1
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/LICENSE +28 -0
- package/README.md +346 -0
- package/cordis.patch.yml +10 -0
- package/lib/client.js +2817 -0
- package/lib/index.js +1455 -0
- package/lib/invariant.js +21 -0
- package/lib/types/client/PairFailedNotice.d.ts +12 -0
- package/lib/types/client/PairFailedNotice.d.ts.map +1 -0
- package/lib/types/client/PhoneIcon.d.ts +19 -0
- package/lib/types/client/PhoneIcon.d.ts.map +1 -0
- package/lib/types/client/PluginSettingsCard.d.ts +78 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -0
- package/lib/types/client/RemoteEntry.d.ts +10 -0
- package/lib/types/client/RemoteEntry.d.ts.map +1 -0
- package/lib/types/client/RemotePanel.d.ts +50 -0
- package/lib/types/client/RemotePanel.d.ts.map +1 -0
- package/lib/types/client/RemoteSettingsCard.d.ts +75 -0
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -0
- package/lib/types/client/deep-link.d.ts +40 -0
- package/lib/types/client/deep-link.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +54 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/locales.d.ts +144 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/client/pair-api.d.ts +100 -0
- package/lib/types/client/pair-api.d.ts.map +1 -0
- package/lib/types/client/settings-form.d.ts +119 -0
- package/lib/types/client/settings-form.d.ts.map +1 -0
- package/lib/types/gate.d.ts +58 -0
- package/lib/types/gate.d.ts.map +1 -0
- package/lib/types/index.d.ts +78 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.d.ts.map +1 -0
- package/lib/types/lan.d.ts +15 -0
- package/lib/types/lan.d.ts.map +1 -0
- package/lib/types/mobile/api.d.ts +64 -0
- package/lib/types/mobile/api.d.ts.map +1 -0
- package/lib/types/mobile/index.d.ts +10 -0
- package/lib/types/mobile/index.d.ts.map +1 -0
- package/lib/types/mobile/messages.d.ts +103 -0
- package/lib/types/mobile/messages.d.ts.map +1 -0
- package/lib/types/mobile/messages.test.d.ts +2 -0
- package/lib/types/mobile/messages.test.d.ts.map +1 -0
- package/lib/types/mobile/mobile-styles.d.ts +6 -0
- package/lib/types/mobile/mobile-styles.d.ts.map +1 -0
- package/lib/types/mobile/mobile-theme.d.ts +22 -0
- package/lib/types/mobile/mobile-theme.d.ts.map +1 -0
- package/lib/types/mobile/mobile-theme.test.d.ts +2 -0
- package/lib/types/mobile/mobile-theme.test.d.ts.map +1 -0
- package/lib/types/mobile/mux.d.ts +52 -0
- package/lib/types/mobile/mux.d.ts.map +1 -0
- package/lib/types/mobile/rpc.d.ts +38 -0
- package/lib/types/mobile/rpc.d.ts.map +1 -0
- package/lib/types/mobile/theme-toggle.d.ts +12 -0
- package/lib/types/mobile/theme-toggle.d.ts.map +1 -0
- package/lib/types/mobile/views/App.d.ts +59 -0
- package/lib/types/mobile/views/App.d.ts.map +1 -0
- package/lib/types/mobile/views/ChatView.d.ts +39 -0
- package/lib/types/mobile/views/ChatView.d.ts.map +1 -0
- package/lib/types/mobile/views/ChatView.test.d.ts +2 -0
- package/lib/types/mobile/views/ChatView.test.d.ts.map +1 -0
- package/lib/types/mobile/views/SessionListView.d.ts +27 -0
- package/lib/types/mobile/views/SessionListView.d.ts.map +1 -0
- package/lib/types/mobile/views/SessionListView.test.d.ts +2 -0
- package/lib/types/mobile/views/SessionListView.test.d.ts.map +1 -0
- package/lib/types/mobile/views/WorkspaceView.d.ts +19 -0
- package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -0
- package/lib/types/mobile-api.d.ts +41 -0
- package/lib/types/mobile-api.d.ts.map +1 -0
- package/lib/types/mobile-routes.d.ts +16 -0
- package/lib/types/mobile-routes.d.ts.map +1 -0
- package/lib/types/pairing.d.ts +215 -0
- package/lib/types/pairing.d.ts.map +1 -0
- package/lib/types/routes.d.ts +58 -0
- package/lib/types/routes.d.ts.map +1 -0
- package/lib/types/tunnel.d.ts +99 -0
- package/lib/types/tunnel.d.ts.map +1 -0
- package/package.json +104 -0
- package/src/client/PairFailedNotice.tsx +33 -0
- package/src/client/PhoneIcon.tsx +31 -0
- package/src/client/PluginSettingsCard.tsx +207 -0
- package/src/client/RemoteEntry.tsx +202 -0
- package/src/client/RemotePanel.tsx +186 -0
- package/src/client/RemoteSettingsCard.tsx +220 -0
- package/src/client/deep-link.ts +137 -0
- package/src/client/index.ts +167 -0
- package/src/client/locales.ts +149 -0
- package/src/client/pair-api.ts +176 -0
- package/src/client/remote.module.css +422 -0
- package/src/client/settings-card.module.css +296 -0
- package/src/client/settings-form.ts +294 -0
- package/src/css-modules.d.ts +6 -0
- package/src/gate.ts +121 -0
- package/src/index.ts +299 -0
- package/src/invariant.ts +32 -0
- package/src/lan.ts +21 -0
- package/src/mobile/api.ts +106 -0
- package/src/mobile/index.tsx +27 -0
- package/src/mobile/messages.test.ts +222 -0
- package/src/mobile/messages.ts +555 -0
- package/src/mobile/mobile-styles.ts +853 -0
- package/src/mobile/mobile-theme.test.ts +84 -0
- package/src/mobile/mobile-theme.ts +76 -0
- package/src/mobile/mux.ts +126 -0
- package/src/mobile/rpc.ts +90 -0
- package/src/mobile/theme-toggle.tsx +47 -0
- package/src/mobile/views/App.tsx +152 -0
- package/src/mobile/views/ChatView.test.tsx +243 -0
- package/src/mobile/views/ChatView.tsx +650 -0
- package/src/mobile/views/SessionListView.test.tsx +139 -0
- package/src/mobile/views/SessionListView.tsx +186 -0
- package/src/mobile/views/WorkspaceView.tsx +108 -0
- package/src/mobile-api.ts +271 -0
- package/src/mobile-routes.ts +77 -0
- package/src/pairing.ts +390 -0
- package/src/routes.ts +346 -0
- package/src/tunnel.ts +264 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mobile-surface live-event client: the plugin's `/m/api/events.mux` SSE
|
|
3
|
+
* channel (Server-Sent Events — the host bridges the mux stream onto it, so
|
|
4
|
+
* no WebSocket handshake or framing is needed on this side). The host
|
|
5
|
+
* pushes mux frames (subscribed baselines, session events, approvals,
|
|
6
|
+
* questions, queue snapshots, tasks, projections) as soon as the stream
|
|
7
|
+
* opens — no subscription handshake is needed. Frames arrive as
|
|
8
|
+
* server-request envelopes whose payload is the mux frame; unknown frame
|
|
9
|
+
* types are dropped so a newer host never breaks this client.
|
|
10
|
+
* EventSource reconnects automatically.
|
|
11
|
+
*/
|
|
12
|
+
import type { MuxFrame } from '@deepseek-ai/dsh-host-apiproxy/api/events';
|
|
13
|
+
/** Injectable seams for tests. */
|
|
14
|
+
export interface MuxClientOptions {
|
|
15
|
+
/** EventSource factory (defaults to the browser EventSource). */
|
|
16
|
+
sourceFactory?: (url: string) => EventSourceLike;
|
|
17
|
+
}
|
|
18
|
+
/** The EventSource subset this client uses (browser EventSource fits). */
|
|
19
|
+
export interface EventSourceLike {
|
|
20
|
+
onmessage: ((event: {
|
|
21
|
+
data: string;
|
|
22
|
+
}) => void) | null;
|
|
23
|
+
onerror: ((event: unknown) => void) | null;
|
|
24
|
+
close(): void;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Keep one SSE subscription open, fanning validated frames out to
|
|
28
|
+
* subscribers. EventSource owns reconnection (with its own backoff); this
|
|
29
|
+
* class only manages the subscription lifecycle.
|
|
30
|
+
*/
|
|
31
|
+
export declare class MuxClient {
|
|
32
|
+
private readonly sourceFactory;
|
|
33
|
+
private readonly listeners;
|
|
34
|
+
private source;
|
|
35
|
+
private stopped;
|
|
36
|
+
private readonly url;
|
|
37
|
+
/**
|
|
38
|
+
* @param url - the mobile events endpoint (browser-relative).
|
|
39
|
+
* @param options - seams.
|
|
40
|
+
*/
|
|
41
|
+
constructor(url?: string, options?: MuxClientOptions);
|
|
42
|
+
/** Open the stream (idempotent; EventSource reconnects until {@link stop}). */
|
|
43
|
+
start(): void;
|
|
44
|
+
/** Close for good. */
|
|
45
|
+
stop(): void;
|
|
46
|
+
/** Subscribe to validated frames; returns an unsubscribe function. */
|
|
47
|
+
onFrame(listener: (frame: MuxFrame) => void): () => void;
|
|
48
|
+
private connect;
|
|
49
|
+
private handleMessage;
|
|
50
|
+
private closeSource;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=mux.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mux.d.ts","sourceRoot":"","sources":["../../../src/mobile/mux.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2CAA2C,CAAA;AAIzE,kCAAkC;AAClC,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,eAAe,CAAA;CACjD;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;AASD;;;;GAIG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAkC;IAChE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAuC;IACjE,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;IAE5B;;;OAGG;gBACS,GAAG,SAAsB,EAAE,OAAO,GAAE,gBAAqB;IAKrE,+EAA+E;IAC/E,KAAK,IAAI,IAAI;IAMb,sBAAsB;IACtB,IAAI,IAAI,IAAI;IAKZ,sEAAsE;IACtE,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,GAAG,MAAM,IAAI;IAKxD,OAAO,CAAC,OAAO;IAaf,OAAO,CAAC,aAAa;IAuBrB,OAAO,CAAC,WAAW;CAapB"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mobile-surface unary RPC over the shared /api transport: the four-quadrant
|
|
3
|
+
* envelope (client-request → server-response), minted rpcIds, and typed
|
|
4
|
+
* error mapping. This is a thin, self-contained slice of the harness
|
|
5
|
+
* apiproxy fetch carrier — the mobile page is an independent bundle and must
|
|
6
|
+
* not depend on the main UI's module loader, so the wire contract is
|
|
7
|
+
* reimplemented here over plain fetch.
|
|
8
|
+
*/
|
|
9
|
+
/** Transport-level failure (network, HTTP status, malformed envelope). */
|
|
10
|
+
export declare class RpcTransportError extends Error {
|
|
11
|
+
constructor(message: string);
|
|
12
|
+
}
|
|
13
|
+
/** A business error the host answered with (200 + err result). */
|
|
14
|
+
export declare class RpcCallError extends Error {
|
|
15
|
+
/** The wire error (code + message + details). */
|
|
16
|
+
readonly error: {
|
|
17
|
+
code: string;
|
|
18
|
+
message: string;
|
|
19
|
+
};
|
|
20
|
+
constructor(error: {
|
|
21
|
+
code: string;
|
|
22
|
+
message: string;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/** Mint one process-unique rpcId (stable under crypto.randomUUID absence). */
|
|
26
|
+
export declare function mintRpcId(): string;
|
|
27
|
+
/**
|
|
28
|
+
* One unary call: POST /m/api/<method> (the plugin's own mobile channel —
|
|
29
|
+
* NOT the connection plugin's /api prefix, so the tunneled Host never needs
|
|
30
|
+
* to enter the transport trust fence) with the client-request envelope,
|
|
31
|
+
* resolve the server-response value, reject with the mapped error classes.
|
|
32
|
+
* @param method - the dotted RPC method, e.g. `session.list`.
|
|
33
|
+
* @param payload - the business payload.
|
|
34
|
+
* @param signal - optional abort.
|
|
35
|
+
* @returns the response value.
|
|
36
|
+
*/
|
|
37
|
+
export declare function callUnary<T>(method: string, payload: unknown, signal?: AbortSignal): Promise<T>;
|
|
38
|
+
//# sourceMappingURL=rpc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rpc.d.ts","sourceRoot":"","sources":["../../../src/mobile/rpc.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,0EAA0E;AAC1E,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAI5B;AAED,kEAAkE;AAClE,qBAAa,YAAa,SAAQ,KAAK;IACrC,iDAAiD;IACjD,QAAQ,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;gBAErC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE;CAKrD;AAID,8EAA8E;AAC9E,wBAAgB,SAAS,IAAI,MAAM,CAMlC;AAED;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAC/B,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,OAAO,EAChB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,CAAC,CAAC,CAmCZ"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme toggle button: sits at the right end of every view header and flips
|
|
3
|
+
* the mobile surface between the light (default) and dark palettes. The
|
|
4
|
+
* glyph shows the theme you will switch TO (moon in light mode, sun in dark
|
|
5
|
+
* mode); the choice persists across visits.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Render the header theme toggle.
|
|
9
|
+
* @returns the toggle button.
|
|
10
|
+
*/
|
|
11
|
+
export declare function ThemeToggle(): import("react").JSX.Element;
|
|
12
|
+
//# sourceMappingURL=theme-toggle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme-toggle.d.ts","sourceRoot":"","sources":["../../../src/mobile/theme-toggle.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAwBH;;;GAGG;AACH,wBAAgB,WAAW,gCAa1B"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mobile surface root: the view state machine (workspaces → sessions →
|
|
3
|
+
* chat) and the top-level data flows. Deliberately plain React state — no
|
|
4
|
+
* router, no state library: the surface is three fixed levels with a back
|
|
5
|
+
* affordance, and every piece of data is fetched on demand.
|
|
6
|
+
*/
|
|
7
|
+
import { listSessions, listWorkspaces, prompt } from '../api.ts';
|
|
8
|
+
/** The session-list row model (list + chat share it). */
|
|
9
|
+
export interface SessionView {
|
|
10
|
+
sessionId: string;
|
|
11
|
+
title: string;
|
|
12
|
+
cwd?: string;
|
|
13
|
+
updatedAt: number;
|
|
14
|
+
running: boolean;
|
|
15
|
+
blank: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** One rendered chat message (produced by the fold). */
|
|
18
|
+
export interface RenderMessage {
|
|
19
|
+
id: string;
|
|
20
|
+
kind: 'user' | 'assistant';
|
|
21
|
+
text: string;
|
|
22
|
+
seq: number;
|
|
23
|
+
time: number;
|
|
24
|
+
pending?: boolean;
|
|
25
|
+
failed?: boolean;
|
|
26
|
+
toolSummary?: string;
|
|
27
|
+
}
|
|
28
|
+
/** Map a list row to the surface model; the title comes from projections when present. */
|
|
29
|
+
export declare function toSessionView(item: {
|
|
30
|
+
sessionId: string;
|
|
31
|
+
updatedAt: number;
|
|
32
|
+
running: boolean;
|
|
33
|
+
blank: boolean;
|
|
34
|
+
cwd?: string;
|
|
35
|
+
projections?: {
|
|
36
|
+
values?: Record<string, unknown>;
|
|
37
|
+
};
|
|
38
|
+
}): SessionView;
|
|
39
|
+
/** Human clock, e.g. "14:05" or "昨天 20:31". */
|
|
40
|
+
export declare function formatTime(epochMs: number): string;
|
|
41
|
+
/**
|
|
42
|
+
* The surface root.
|
|
43
|
+
* @returns the app tree.
|
|
44
|
+
*/
|
|
45
|
+
export declare function App(): import("react").JSX.Element;
|
|
46
|
+
/** Shared error text for the surface's small failure affordances. */
|
|
47
|
+
export declare function errorText(error: unknown): string;
|
|
48
|
+
/**
|
|
49
|
+
* Actionable hint for transport-level 403s on host-gated channels (model
|
|
50
|
+
* picker, session creation): the phone's UI bundle is served fresh from disk
|
|
51
|
+
* per request, while the host-side allowlist lives in the long-running
|
|
52
|
+
* process — a rebuild without a restart shows the new surface against the
|
|
53
|
+
* old allowlist (HTTP 403 forbidden).
|
|
54
|
+
*/
|
|
55
|
+
export declare function staleHostHint(message: string): string | undefined;
|
|
56
|
+
/** Fetch one history page (tail by default) — thin wrapper so views share the call shape. */
|
|
57
|
+
export declare function loadHistory(sessionId: string, beforeSeq?: number): Promise<import("../api.ts").HistoryPage>;
|
|
58
|
+
export { listSessions, listWorkspaces, prompt };
|
|
59
|
+
//# sourceMappingURL=App.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/App.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAA2B,YAAY,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,WAAW,CAAA;AAazF,yDAAyD;AACzD,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,OAAO,CAAA;CACf;AAED,wDAAwD;AACxD,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,GAAG,WAAW,CAAA;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,0FAA0F;AAC1F,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAClC,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,OAAO,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAA;CACnD,GAAG,WAAW,CAad;AAED,+CAA+C;AAC/C,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CASlD;AAED;;;GAGG;AACH,wBAAgB,GAAG,gCAwClB;AAED,qEAAqE;AACrE,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAKhD;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAIjE;AAED,6FAA6F;AAC7F,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,4CAEhE;AAED,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,EAAE,CAAA"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat level: one session. Loads the history tail page on open, appends
|
|
3
|
+
* pages upward (loadOlder), folds live mux frames in as they arrive, and
|
|
4
|
+
* sends prompts through session.prompt.
|
|
5
|
+
*
|
|
6
|
+
* Rendering mirrors the desktop web UI's fold discipline on a small screen:
|
|
7
|
+
* - reasoning text hides behind a collapsed "深度思考" disclosure,
|
|
8
|
+
* - tool calls behind a collapsed tool disclosure (name + arguments),
|
|
9
|
+
* - very long assistant text collapses with an explicit expand toggle,
|
|
10
|
+
* - a toolbar above the composer carries the model (+ thinking effort) and
|
|
11
|
+
* permission pickers, both as bottom sheets.
|
|
12
|
+
*/
|
|
13
|
+
import { type SessionView } from './App.tsx';
|
|
14
|
+
import { MuxClient } from '../mux.ts';
|
|
15
|
+
/** Props for the chat view. */
|
|
16
|
+
export interface ChatViewProps {
|
|
17
|
+
session: SessionView;
|
|
18
|
+
/** The page-lifetime mux client (undefined before the first effect tick). */
|
|
19
|
+
mux?: MuxClient | undefined;
|
|
20
|
+
onBack(): void;
|
|
21
|
+
}
|
|
22
|
+
/** One switchable permission preset (the `permissions` projection shape). */
|
|
23
|
+
export interface PermissionOption {
|
|
24
|
+
value: string;
|
|
25
|
+
name: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
}
|
|
28
|
+
/** The `permissions` projection value: options + the effective current value. */
|
|
29
|
+
export interface PermissionSelectValue {
|
|
30
|
+
options: PermissionOption[];
|
|
31
|
+
currentValue: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Render one session's chat.
|
|
35
|
+
* @param props - the session, the mux client, and the back action.
|
|
36
|
+
* @returns the chat surface.
|
|
37
|
+
*/
|
|
38
|
+
export declare function ChatView({ session, mux, onBack }: ChatViewProps): import("react").JSX.Element;
|
|
39
|
+
//# sourceMappingURL=ChatView.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatView.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/ChatView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,WAAW,CAAA;AAIjE,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAGrC,+BAA+B;AAC/B,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,WAAW,CAAA;IACpB,6EAA6E;IAC7E,GAAG,CAAC,EAAE,SAAS,GAAG,SAAS,CAAA;IAC3B,MAAM,IAAI,IAAI,CAAA;CACf;AAYD,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,iFAAiF;AACjF,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,YAAY,EAAE,MAAM,CAAA;CACrB;AAqCD;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,aAAa,+BA6M/D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatView.test.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/ChatView.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sessions level: one workspace's sessions, loaded incrementally — the
|
|
3
|
+
* first page fetches session.list with a cursor, scrolling appends further
|
|
4
|
+
* pages (never the whole list at once). Rows are filtered to the opened
|
|
5
|
+
* workspace by its owned session ids; extra pages are pulled on demand so
|
|
6
|
+
* a workspace with many sessions converges without a full transfer.
|
|
7
|
+
*
|
|
8
|
+
* Creating a session is this level's other action: the workspace's id is
|
|
9
|
+
* sent to session.create (the host attaches the new session to it), the
|
|
10
|
+
* fresh row is prepended optimistically, and the user lands straight in
|
|
11
|
+
* the new chat — the same "new session opens" flow as the desktop UI.
|
|
12
|
+
*/
|
|
13
|
+
import type { WorkspaceView as WorkspaceRow } from '@deepseek-ai/dsh-host-apiproxy/api/workspace';
|
|
14
|
+
import { type SessionView } from './App.tsx';
|
|
15
|
+
/** Props for the session list. */
|
|
16
|
+
export interface SessionListViewProps {
|
|
17
|
+
workspace: WorkspaceRow;
|
|
18
|
+
onBack(): void;
|
|
19
|
+
onPick(session: SessionView): void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Render one workspace's paged session list.
|
|
23
|
+
* @param props - the workspace, back action, and pick action.
|
|
24
|
+
* @returns the session list.
|
|
25
|
+
*/
|
|
26
|
+
export declare function SessionListView({ workspace, onBack, onPick }: SessionListViewProps): import("react").JSX.Element;
|
|
27
|
+
//# sourceMappingURL=SessionListView.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SessionListView.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/SessionListView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,aAAa,IAAI,YAAY,EAAE,MAAM,8CAA8C,CAAA;AAGjG,OAAO,EAAuD,KAAK,WAAW,EAAE,MAAM,WAAW,CAAA;AAGjG,kCAAkC;AAClC,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,YAAY,CAAA;IACvB,MAAM,IAAI,IAAI,CAAA;IACd,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAAA;CACnC;AAUD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,oBAAoB,+BAiJlF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SessionListView.test.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/SessionListView.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Landing level: the workspace roster. The mobile surface opens straight
|
|
3
|
+
* here — no new-session homepage — and every workspace row is a thin
|
|
4
|
+
* fetch from workspace.list (the roster is small; sessions are not loaded
|
|
5
|
+
* until a workspace is opened).
|
|
6
|
+
*/
|
|
7
|
+
import type { WorkspaceView as WorkspaceRow } from '@deepseek-ai/dsh-host-apiproxy/api/workspace';
|
|
8
|
+
/** Props for the workspace roster. */
|
|
9
|
+
export interface WorkspaceViewProps {
|
|
10
|
+
/** Open one workspace's session list. */
|
|
11
|
+
onPick(workspace: WorkspaceRow): void;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Render the workspace roster.
|
|
15
|
+
* @param props - the pick action.
|
|
16
|
+
* @returns the roster.
|
|
17
|
+
*/
|
|
18
|
+
export declare function WorkspaceView({ onPick }: WorkspaceViewProps): import("react").JSX.Element;
|
|
19
|
+
//# sourceMappingURL=WorkspaceView.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WorkspaceView.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/WorkspaceView.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,aAAa,IAAI,YAAY,EAAE,MAAM,8CAA8C,CAAA;AAKjG,sCAAsC;AACtC,MAAM,WAAW,kBAAkB;IACjC,yCAAyC;IACzC,MAAM,CAAC,SAAS,EAAE,YAAY,GAAG,IAAI,CAAA;CACtC;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,EAAE,kBAAkB,+BAmF3D"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mobile surface's data channel: `/m/api` proxies the host ApiProxy
|
|
3
|
+
* service for the standalone phone page. The phone's RPC calls ride THIS
|
|
4
|
+
* prefix instead of the connection plugin's `/api` — so the tunneled Host
|
|
5
|
+
* never needs to enter the connection trust fence (a distributable plugin
|
|
6
|
+
* cannot change that fence), and this plugin's own pairing gate is the
|
|
7
|
+
* access control instead.
|
|
8
|
+
*
|
|
9
|
+
* Security model:
|
|
10
|
+
* - Every request must carry a live paired-device cookie (the same gate
|
|
11
|
+
* semantic as the LAN fence), enforced before any host call.
|
|
12
|
+
* - Only an explicit allowlist of methods is proxied; privileged domains
|
|
13
|
+
* (settings, credentials, host actions, goals, subagents, …) are never
|
|
14
|
+
* reachable from the phone.
|
|
15
|
+
* - `session.list` is paged here (the host API returns everything; this
|
|
16
|
+
* layer slices stable pages) so the phone never transfers the whole list.
|
|
17
|
+
* - The live mux stream is bridged over Server-Sent Events on the same
|
|
18
|
+
* prefix (one-directional push; answers to questions/approvals ride the
|
|
19
|
+
* unary channel), gated identically.
|
|
20
|
+
*/
|
|
21
|
+
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
|
|
22
|
+
import type { ApiProxy } from '@deepseek-ai/dsh-host-apiproxy';
|
|
23
|
+
import type { PairingService } from './pairing.ts';
|
|
24
|
+
/** Route-family dependencies. */
|
|
25
|
+
export interface MobileApiDeps {
|
|
26
|
+
/** The pairing service (device gate + cookie name). */
|
|
27
|
+
service: PairingService;
|
|
28
|
+
/** The host ApiProxy service (injected by the plugin). */
|
|
29
|
+
apiProxy: ApiProxy;
|
|
30
|
+
}
|
|
31
|
+
/** Mobile API route paths. */
|
|
32
|
+
export declare const MOBILE_API_PATHS: {
|
|
33
|
+
readonly events: "/m/api/events.mux";
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Build the mobile data-channel routes.
|
|
37
|
+
* @param deps - pairing service + apiProxy.
|
|
38
|
+
* @returns the routes to register on webServer.
|
|
39
|
+
*/
|
|
40
|
+
export declare function makeMobileApiRoutes(deps: MobileApiDeps): WebRoute[];
|
|
41
|
+
//# sourceMappingURL=mobile-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mobile-api.d.ts","sourceRoot":"","sources":["../../src/mobile-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAA;AAG9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAuClD,iCAAiC;AACjC,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,OAAO,EAAE,cAAc,CAAA;IACvB,0DAA0D;IAC1D,QAAQ,EAAE,QAAQ,CAAA;CACnB;AAED,8BAA8B;AAC9B,eAAO,MAAM,gBAAgB;;CAEnB,CAAA;AAOV;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,CAkHnE"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mobile surface's page routes: `/m` serves the standalone phone UI
|
|
3
|
+
* (an independent bundle, built to lib/mobile.js by the mobile tsdown
|
|
4
|
+
* entry), `/m/mobile.js` serves the bundle itself. The page talks to the
|
|
5
|
+
* host exclusively through the shared /api transport (paired-device cookie
|
|
6
|
+
* already crosses the api/gate fence), so no host-side data plumbing is
|
|
7
|
+
* needed here — only static serving, loopback+paired-fence via the normal
|
|
8
|
+
* webserver route registration.
|
|
9
|
+
*/
|
|
10
|
+
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
|
|
11
|
+
/**
|
|
12
|
+
* Build the mobile page routes.
|
|
13
|
+
* @returns the two exact routes to register on webServer.
|
|
14
|
+
*/
|
|
15
|
+
export declare function makeMobileRoutes(): WebRoute[];
|
|
16
|
+
//# sourceMappingURL=mobile-routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mobile-routes.d.ts","sourceRoot":"","sources":["../../src/mobile-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAqC/D;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,QAAQ,EAAE,CAqB7C"}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pairing state machine: one active one-time token, a device-session table,
|
|
3
|
+
* and presence tracking. Pure TypeScript with injected clock/randomness so
|
|
4
|
+
* the whole security semantics are unit-testable without cordis. The
|
|
5
|
+
* cordis-facing surfaces (routes, the api/gate listener) live next door.
|
|
6
|
+
*
|
|
7
|
+
* Security invariants:
|
|
8
|
+
* - One active token at a time; `issue()` replaces it, so a refreshed QR
|
|
9
|
+
* immediately invalidates the previous link.
|
|
10
|
+
* - A token is consumed by the first successful `accept()` — reuse is
|
|
11
|
+
* refused with `'used'`.
|
|
12
|
+
* - Tokens expire; `accept()` on an expired token is refused like an
|
|
13
|
+
* unknown one (no oracle for validity).
|
|
14
|
+
* - `stop()` revokes every device session and clears the token, so paired
|
|
15
|
+
* devices are cut off on their next gated request.
|
|
16
|
+
*/
|
|
17
|
+
/** The observable pairing phases the panel renders. */
|
|
18
|
+
export type PairingPhase =
|
|
19
|
+
/** The server is not bound all-interfaces: no usable QR exists. */
|
|
20
|
+
'lan-required'
|
|
21
|
+
/** Remote control was stopped; a fresh QR (issue) re-enables it. */
|
|
22
|
+
| 'stopped'
|
|
23
|
+
/** A token is live and no device has paired with it yet. */
|
|
24
|
+
| 'waiting'
|
|
25
|
+
/** At least one paired device was active recently. */
|
|
26
|
+
| 'connected'
|
|
27
|
+
/** Devices are paired but none has been active within the offline window. */
|
|
28
|
+
| 'disconnected';
|
|
29
|
+
/** One issued pairing token (keyed by its secret). */
|
|
30
|
+
export interface TokenRecord {
|
|
31
|
+
/** Monotonic issue time (ms epoch), drives refresh ordering. */
|
|
32
|
+
issuedAt: number;
|
|
33
|
+
/** Absolute expiry (ms epoch); accept() past this is refused. */
|
|
34
|
+
expiresAt: number;
|
|
35
|
+
/** Consumed by the first successful accept(). */
|
|
36
|
+
consumed: boolean;
|
|
37
|
+
/** Opaque, non-secret identifier surfaced in snapshots (never the pairing secret). */
|
|
38
|
+
id: string;
|
|
39
|
+
/** Workspace the QR link should land the phone in (optional). */
|
|
40
|
+
workspaceId?: string;
|
|
41
|
+
/** LAN IP literal the QR link was built from (optional; default first). */
|
|
42
|
+
address?: string;
|
|
43
|
+
}
|
|
44
|
+
/** One paired device session, keyed by the device id stored in its cookie. */
|
|
45
|
+
export interface DeviceSession {
|
|
46
|
+
/** Pairing time (ms epoch). */
|
|
47
|
+
createdAt: number;
|
|
48
|
+
/** Last time the device passed a gated request or heartbeat. */
|
|
49
|
+
lastSeenAt: number;
|
|
50
|
+
}
|
|
51
|
+
/** One tunnel status frame (auto-tunnel only; undefined when disabled). */
|
|
52
|
+
export interface TunnelStatus {
|
|
53
|
+
/** starting: binary/process warming up; running: URL minted; failed: no URL. */
|
|
54
|
+
state: 'starting' | 'running' | 'failed';
|
|
55
|
+
/** The minted public URL, once the tunnel reports it. */
|
|
56
|
+
url?: string;
|
|
57
|
+
/** Human-readable failure detail. */
|
|
58
|
+
error?: string;
|
|
59
|
+
}
|
|
60
|
+
/** One snapshot frame pushed to desktop status streams. */
|
|
61
|
+
export interface PairingSnapshot {
|
|
62
|
+
phase: PairingPhase;
|
|
63
|
+
/** Whether the server bind is all-interfaces (a QR is constructible). */
|
|
64
|
+
lanAvailable: boolean;
|
|
65
|
+
/** The LAN IP literals a QR can be built from (interface order). */
|
|
66
|
+
lanAddresses: string[];
|
|
67
|
+
/** Configured public (tunneled) base URL, when present. */
|
|
68
|
+
publicUrl?: string;
|
|
69
|
+
/** Auto-tunnel status, while the auto-tunnel feature is active. */
|
|
70
|
+
tunnel?: TunnelStatus;
|
|
71
|
+
/** Opaque (non-secret) id of the active token (undefined when stopped/lan-required). */
|
|
72
|
+
tokenId?: string;
|
|
73
|
+
/** Absolute expiry of the active token. */
|
|
74
|
+
tokenExpiresAt?: number;
|
|
75
|
+
/** Count of ever-paired devices. */
|
|
76
|
+
deviceCount: number;
|
|
77
|
+
/** Count of devices active within the offline window. */
|
|
78
|
+
onlineCount: number;
|
|
79
|
+
}
|
|
80
|
+
/** Service tunables (config-validated upstream; plain numbers here). */
|
|
81
|
+
export interface PairingConfig {
|
|
82
|
+
/** Token lifetime; the QR stops working after this. */
|
|
83
|
+
tokenTtlMs: number;
|
|
84
|
+
/** A device is "online" while its lastSeenAt is newer than this. */
|
|
85
|
+
offlineAfterMs: number;
|
|
86
|
+
/** Hard cap on paired device sessions (oldest-evicted when full). */
|
|
87
|
+
maxDevices: number;
|
|
88
|
+
/** Cookie name carrying the device id. */
|
|
89
|
+
cookieName: string;
|
|
90
|
+
}
|
|
91
|
+
/** Result of one accept() attempt. */
|
|
92
|
+
export type AcceptResult = {
|
|
93
|
+
ok: true;
|
|
94
|
+
deviceId: string;
|
|
95
|
+
} | {
|
|
96
|
+
ok: false;
|
|
97
|
+
code: 'invalid' | 'used';
|
|
98
|
+
};
|
|
99
|
+
/** Thrown by issue() for an address outside the sampled LAN literals. */
|
|
100
|
+
export declare class UnknownLanAddressError extends Error {
|
|
101
|
+
/**
|
|
102
|
+
* @param address - the offending literal.
|
|
103
|
+
*/
|
|
104
|
+
constructor(address: string);
|
|
105
|
+
}
|
|
106
|
+
/** Clock and entropy injection for tests. */
|
|
107
|
+
export interface PairingClock {
|
|
108
|
+
now(): number;
|
|
109
|
+
randomToken(): string;
|
|
110
|
+
}
|
|
111
|
+
/** Real clock/entropy: 32 random hex chars per token. */
|
|
112
|
+
export declare const defaultClock: PairingClock;
|
|
113
|
+
/**
|
|
114
|
+
* The pairing state machine. All mutations notify state listeners after the
|
|
115
|
+
* commit point that makes them true, and notification dedupes against the
|
|
116
|
+
* last emitted snapshot — time-driven transitions (a device aging offline)
|
|
117
|
+
* surface on the next sweep without any mutation.
|
|
118
|
+
*/
|
|
119
|
+
export declare class PairingService {
|
|
120
|
+
config: PairingConfig;
|
|
121
|
+
private readonly clock;
|
|
122
|
+
private readonly tokens;
|
|
123
|
+
private readonly devices;
|
|
124
|
+
private readonly listeners;
|
|
125
|
+
private lastEmitted;
|
|
126
|
+
private stopped;
|
|
127
|
+
private tokenSerial;
|
|
128
|
+
/** LAN base URLs keyed by the advertised IP literal (interface order). */
|
|
129
|
+
private lanBases;
|
|
130
|
+
/** Public (tunneled) base URL, e.g. a Cloudflare Tunnel quick URL. */
|
|
131
|
+
private publicBase;
|
|
132
|
+
/** Auto-tunnel status, while the auto-tunnel feature is active. */
|
|
133
|
+
private tunnelStatus;
|
|
134
|
+
/**
|
|
135
|
+
* @param config - tunables. The settings surface replaces the object (a
|
|
136
|
+
* fresh literal) when a committed section changes; every operation reads
|
|
137
|
+
* the current one.
|
|
138
|
+
* @param clock - clock/entropy source (injectable for tests).
|
|
139
|
+
*/
|
|
140
|
+
constructor(config: PairingConfig, clock?: PairingClock);
|
|
141
|
+
/** The default LAN base URL (the first interface; undefined when not LAN-reachable). */
|
|
142
|
+
get lanBaseUrl(): string | undefined;
|
|
143
|
+
/** The LAN base URL for one specific literal (undefined when not constructible). */
|
|
144
|
+
lanBaseUrlFor(address: string): string | undefined;
|
|
145
|
+
/** The LAN IP literals QR links can be built from (interface order). */
|
|
146
|
+
get lanAddresses(): string[];
|
|
147
|
+
/** Set the LAN base URLs once the server bind is known (interface order). */
|
|
148
|
+
setLanBases(entries: readonly {
|
|
149
|
+
address: string;
|
|
150
|
+
base: string;
|
|
151
|
+
}[]): void;
|
|
152
|
+
/** The configured public (tunneled) base URL, when present. */
|
|
153
|
+
get publicBaseUrl(): string | undefined;
|
|
154
|
+
/** Set or clear the public base URL (a tunnel in front of this server). */
|
|
155
|
+
setPublicBaseUrl(url: string | undefined): void;
|
|
156
|
+
/** Set or clear the auto-tunnel status frame (undefined when the feature is off). */
|
|
157
|
+
setTunnelStatus(status: TunnelStatus | undefined): void;
|
|
158
|
+
/**
|
|
159
|
+
* Issue a fresh token, replacing (invalidating) any previous one. A
|
|
160
|
+
* stopped service re-arms through this call (the panel's refresh button).
|
|
161
|
+
* @param workspaceId - optional workspace the QR link should land in.
|
|
162
|
+
* @param address - optional LAN IP literal the QR must be built from; the
|
|
163
|
+
* default is the public base (when configured) or the first interface.
|
|
164
|
+
* Unknown addresses are refused.
|
|
165
|
+
* @returns the token secret and its expiry.
|
|
166
|
+
* @throws {Error} when no reachable base exists (no all-interfaces bind and
|
|
167
|
+
* no public base) — callers surface this as the lan-required state instead
|
|
168
|
+
* of minting an unusable QR.
|
|
169
|
+
*/
|
|
170
|
+
issue(workspaceId?: string, address?: string): {
|
|
171
|
+
token: string;
|
|
172
|
+
expiresAt: number;
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* Consume a token and bind a device session. One-time: the second
|
|
176
|
+
* successful call for the same token is impossible because the first
|
|
177
|
+
* consumes it.
|
|
178
|
+
* @param token - the token secret from the QR link.
|
|
179
|
+
* @returns the new device id, or a refusal code.
|
|
180
|
+
*/
|
|
181
|
+
accept(token: string): AcceptResult;
|
|
182
|
+
/**
|
|
183
|
+
* Stop remote control: revoke every device session and clear the token.
|
|
184
|
+
* The phone's next gated /api request 403s; the panel falls back to
|
|
185
|
+
* stopped until a fresh QR is issued.
|
|
186
|
+
*/
|
|
187
|
+
stop(): void;
|
|
188
|
+
/**
|
|
189
|
+
* The api/gate path: record activity for a device id and report whether
|
|
190
|
+
* the request may proceed. Unknown or revoked ids (including any device
|
|
191
|
+
* after stop()) are refused.
|
|
192
|
+
* @param deviceId - the cookie value of the requesting device.
|
|
193
|
+
* @returns true when the device session is live and was refreshed.
|
|
194
|
+
*/
|
|
195
|
+
touchDevice(deviceId: string): boolean;
|
|
196
|
+
/** Explicit presence heartbeat (the phone's client sends these). */
|
|
197
|
+
heartbeat(deviceId: string): boolean;
|
|
198
|
+
/**
|
|
199
|
+
* Periodic sweep: re-evaluate the derived snapshot (a device aging past
|
|
200
|
+
* the offline window flips the phase to disconnected). Emits only when
|
|
201
|
+
* the snapshot actually changed.
|
|
202
|
+
*/
|
|
203
|
+
sweep(): void;
|
|
204
|
+
/** The current snapshot (fresh object per call — stable between emits). */
|
|
205
|
+
snapshot(): PairingSnapshot;
|
|
206
|
+
/** Whether a cookie value names a currently live device session. */
|
|
207
|
+
hasDevice(deviceId: string): boolean;
|
|
208
|
+
/** Subscribe to snapshot changes (each emit passes a fresh snapshot). */
|
|
209
|
+
onState(listener: (snapshot: PairingSnapshot) => void): () => void;
|
|
210
|
+
private activeToken;
|
|
211
|
+
private derivePhase;
|
|
212
|
+
private isOnlineAt;
|
|
213
|
+
private notify;
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=pairing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../src/pairing.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The /api/pair route family + the desktop status stream. Exact routes
|
|
3
|
+
* under /api: the webserver matches exact paths before the connection
|
|
4
|
+
* plugin's /api prefix, so these handlers own the full response lifecycle
|
|
5
|
+
* and apply their own trust fence (loopback-only for control endpoints;
|
|
6
|
+
* loopback-or-LAN for the phone-facing accept/heartbeat/status). The
|
|
7
|
+
* cookie set on accept is the device identity the api/gate listener checks
|
|
8
|
+
* on every other /api request.
|
|
9
|
+
*/
|
|
10
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
11
|
+
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
|
|
12
|
+
import { type PairingService, type PairingSnapshot } from './pairing.ts';
|
|
13
|
+
/**
|
|
14
|
+
* The host authority of a configured public base URL, e.g. `foo.trycloudflare.com`
|
|
15
|
+
* from `https://foo.trycloudflare.com`. Undefined when the URL does not parse —
|
|
16
|
+
* a malformed config then simply contributes no fence entry (and the panel
|
|
17
|
+
* falls back to LAN-only URLs).
|
|
18
|
+
* @param url - the configured public base URL (or undefined).
|
|
19
|
+
* @returns the `host[:port]` authority the fence should trust.
|
|
20
|
+
*/
|
|
21
|
+
export declare function publicHostOf(url: string | undefined): string | undefined;
|
|
22
|
+
/** Route paths (exact matches under /api). */
|
|
23
|
+
export declare const PAIR_PATHS: {
|
|
24
|
+
readonly issue: "/api/pair/issue";
|
|
25
|
+
readonly accept: "/api/pair/accept";
|
|
26
|
+
readonly stop: "/api/pair/stop";
|
|
27
|
+
readonly heartbeat: "/api/pair/heartbeat";
|
|
28
|
+
readonly status: "/api/pair/status";
|
|
29
|
+
readonly events: "/api/pair/events";
|
|
30
|
+
};
|
|
31
|
+
/** The SSE fan-out for desktop panel status. */
|
|
32
|
+
export declare class PairingEventsStream {
|
|
33
|
+
private readonly streams;
|
|
34
|
+
/**
|
|
35
|
+
* @param service - the pairing service whose snapshots are fanned out.
|
|
36
|
+
*/
|
|
37
|
+
constructor(service: PairingService);
|
|
38
|
+
/** Open one stream; the response is owned to completion. */
|
|
39
|
+
open(req: IncomingMessage, res: ServerResponse): void;
|
|
40
|
+
/** Push one frame to every open stream (contained per stream). */
|
|
41
|
+
push(snapshot: PairingSnapshot): void;
|
|
42
|
+
/** Stream count (tests/diagnostics). */
|
|
43
|
+
get size(): number;
|
|
44
|
+
}
|
|
45
|
+
/** Route-family dependencies (test seam). */
|
|
46
|
+
export interface PairRoutesDeps {
|
|
47
|
+
/** The pairing service. */
|
|
48
|
+
service: PairingService;
|
|
49
|
+
/** The LAN IP literals the fence accepts (derived from the bind host). */
|
|
50
|
+
lanAddresses: readonly string[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Build the /api/pair route family.
|
|
54
|
+
* @param deps - service + fence inputs.
|
|
55
|
+
* @returns the exact routes to register on webServer.
|
|
56
|
+
*/
|
|
57
|
+
export declare function makeRoutes(deps: PairRoutesDeps): WebRoute[];
|
|
58
|
+
//# sourceMappingURL=routes.d.ts.map
|