dsh-plugin-dshm 0.0.0-stage → 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.
Files changed (82) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +21 -0
  3. package/README.md +232 -2
  4. package/README.zh-CN.md +194 -0
  5. package/cordis.patch.yml +7 -0
  6. package/lib/auth/signature.d.ts +4 -0
  7. package/lib/auth/signature.js +24 -0
  8. package/lib/cli/index.d.ts +4 -0
  9. package/lib/cli/index.js +41 -0
  10. package/lib/client.js +419 -0
  11. package/lib/config.d.ts +10 -0
  12. package/lib/config.js +27 -0
  13. package/lib/discovery/advertiser.d.ts +45 -0
  14. package/lib/discovery/advertiser.js +141 -0
  15. package/lib/discovery/interfaces.d.ts +5 -0
  16. package/lib/discovery/interfaces.js +24 -0
  17. package/lib/errors.d.ts +5 -0
  18. package/lib/errors.js +10 -0
  19. package/lib/host/local.d.ts +5 -0
  20. package/lib/host/local.js +122 -0
  21. package/lib/host/tailscale.d.ts +43 -0
  22. package/lib/host/tailscale.js +137 -0
  23. package/lib/host/types.d.ts +40 -0
  24. package/lib/host/types.js +1 -0
  25. package/lib/index.d.ts +30 -0
  26. package/lib/index.js +98 -0
  27. package/lib/methods/index.d.ts +9 -0
  28. package/lib/methods/index.js +206 -0
  29. package/lib/pairing/state.d.ts +26 -0
  30. package/lib/pairing/state.js +89 -0
  31. package/lib/pairing/window.d.ts +18 -0
  32. package/lib/pairing/window.js +57 -0
  33. package/lib/projection/client.d.mts +2 -0
  34. package/lib/projection/client.mjs +11 -0
  35. package/lib/projection/compatibility.d.mts +2 -0
  36. package/lib/projection/compatibility.mjs +4 -0
  37. package/lib/projection/files.d.ts +33 -0
  38. package/lib/projection/files.js +278 -0
  39. package/lib/projection/fold-recording.d.mts +50 -0
  40. package/lib/projection/fold-recording.mjs +440 -0
  41. package/lib/projection/host.d.ts +6 -0
  42. package/lib/projection/host.js +86 -0
  43. package/lib/projection/ids.d.mts +1 -0
  44. package/lib/projection/ids.mjs +1 -0
  45. package/lib/projection/index.d.ts +2 -0
  46. package/lib/projection/index.js +2 -0
  47. package/lib/projection/interactions.d.ts +10 -0
  48. package/lib/projection/interactions.js +73 -0
  49. package/lib/projection/native.d.ts +115 -0
  50. package/lib/projection/native.js +915 -0
  51. package/lib/protocol/THIRD_PARTY_LICENSES.txt +135 -0
  52. package/lib/protocol/dmp.d.ts +402 -0
  53. package/lib/protocol/dmp.js +1 -0
  54. package/lib/protocol/schema/dmp-v1/common.schema.json +88 -0
  55. package/lib/protocol/schema/dmp-v1/events.schema.json +107 -0
  56. package/lib/protocol/schema/dmp-v1/frames.schema.json +280 -0
  57. package/lib/protocol/schema/dmp-v1/methods.schema.json +584 -0
  58. package/lib/protocol/schema/dmp-v1/models.schema.json +674 -0
  59. package/lib/protocol/scripts/contract.mjs +8106 -0
  60. package/lib/push/controller.d.ts +64 -0
  61. package/lib/push/controller.js +290 -0
  62. package/lib/push/observer.d.ts +51 -0
  63. package/lib/push/observer.js +266 -0
  64. package/lib/push/payload.d.ts +15 -0
  65. package/lib/push/payload.js +18 -0
  66. package/lib/push/store.d.ts +17 -0
  67. package/lib/push/store.js +75 -0
  68. package/lib/server/index.d.ts +30 -0
  69. package/lib/server/index.js +500 -0
  70. package/lib/server/rate-limit.d.ts +7 -0
  71. package/lib/server/rate-limit.js +19 -0
  72. package/lib/server/uploads.d.ts +21 -0
  73. package/lib/server/uploads.js +74 -0
  74. package/lib/types.d.ts +92 -0
  75. package/lib/types.js +1 -0
  76. package/lib/ui/card.d.mts +3 -0
  77. package/lib/ui/card.mjs +59 -0
  78. package/lib/ui/copy.d.mts +146 -0
  79. package/lib/ui/copy.mjs +106 -0
  80. package/lib/ui/remote-setup.d.mts +81 -0
  81. package/lib/ui/remote-setup.mjs +75 -0
  82. package/package.json +67 -4
@@ -0,0 +1,24 @@
1
+ import net from 'node:net';
2
+ import os from 'node:os';
3
+ export function isTailscale(address) {
4
+ if (address.toLowerCase().startsWith('fd7a:115c:a1e0:'))
5
+ return true;
6
+ if (net.isIP(address) !== 4)
7
+ return false;
8
+ const [a, b] = address.split('.').map(Number);
9
+ return a === 100 && b >= 64 && b <= 127;
10
+ }
11
+ export function isPrivateAddress(address, allowLoopback = false) {
12
+ if (net.isIP(address) === 6)
13
+ return (allowLoopback && address === '::1') || /^(fc|fd)/i.test(address);
14
+ if (net.isIP(address) !== 4)
15
+ return false;
16
+ const [a, b] = address.split('.').map(Number);
17
+ return (allowLoopback && a === 127) || a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || isTailscale(address);
18
+ }
19
+ export function privateInterfaces(mode = 'all-private') {
20
+ return [...new Set(Object.values(os.networkInterfaces()).flatMap(entries => (entries ?? [])
21
+ .filter(entry => !entry.internal && isPrivateAddress(entry.address) && (mode === 'all-private' || (mode === 'tailscale' ? isTailscale(entry.address) : !isTailscale(entry.address))))
22
+ .map(entry => entry.address)))].sort();
23
+ }
24
+ export const endpoint = (address, port) => `${net.isIP(address) === 6 ? `[${address}]` : address}:${port}`;
@@ -0,0 +1,5 @@
1
+ export declare class DmpError extends Error {
2
+ readonly code: string;
3
+ readonly details?: Record<string, unknown> | undefined;
4
+ constructor(code: string, message?: string, details?: Record<string, unknown> | undefined);
5
+ }
package/lib/errors.js ADDED
@@ -0,0 +1,10 @@
1
+ export class DmpError extends Error {
2
+ code;
3
+ details;
4
+ constructor(code, message = code, details) {
5
+ super(message);
6
+ this.code = code;
7
+ this.details = details;
8
+ this.name = 'DmpError';
9
+ }
10
+ }
@@ -0,0 +1,5 @@
1
+ import type { HostControls } from './types.js';
2
+ export declare function startLocalControl(directory: string, controls: HostControls): Promise<{
3
+ close(): Promise<void>;
4
+ }>;
5
+ export declare function localRequest<T>(directory: string, method: 'GET' | 'POST', route: string, body?: unknown): Promise<T>;
@@ -0,0 +1,122 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import http from 'node:http';
4
+ import path from 'node:path';
5
+ import { writePrivate } from '../pairing/state.js';
6
+ function record(directory) {
7
+ const value = JSON.parse(fs.readFileSync(path.join(directory, 'control.json'), 'utf8'));
8
+ if (!Number.isInteger(value.pid) || typeof value.socket !== 'string' || typeof value.token !== 'string' || value.token.length !== 64)
9
+ throw new Error('Invalid local bridge control record');
10
+ return value;
11
+ }
12
+ function equal(left, right) {
13
+ const a = Buffer.from(left), b = Buffer.from(right);
14
+ return a.length === b.length && crypto.timingSafeEqual(a, b);
15
+ }
16
+ export async function startLocalControl(directory, controls) {
17
+ const file = path.join(directory, 'control.json');
18
+ if (fs.existsSync(file)) {
19
+ const old = record(directory);
20
+ let live = true;
21
+ try {
22
+ process.kill(old.pid, 0);
23
+ }
24
+ catch (error) {
25
+ if (error.code === 'ESRCH')
26
+ live = false;
27
+ }
28
+ if (live)
29
+ throw new Error('A bridge already owns this home; use its existing host controls');
30
+ }
31
+ const token = crypto.randomBytes(32).toString('hex');
32
+ const suffix = crypto.randomBytes(6).toString('hex');
33
+ const socket = process.platform === 'win32' ? `\\\\.\\pipe\\dshm-${suffix}` : path.join(directory, `control-${suffix}.sock`);
34
+ const server = http.createServer(async (req, res) => {
35
+ res.setHeader('Content-Type', 'application/json');
36
+ res.setHeader('Cache-Control', 'no-store');
37
+ if (!equal(req.headers.authorization ?? '', `Bearer ${token}`)) {
38
+ res.writeHead(401);
39
+ res.end('{"error":"unauthorized"}');
40
+ return;
41
+ }
42
+ const reply = (code, value) => { res.writeHead(code); res.end(JSON.stringify(value)); };
43
+ try {
44
+ if (req.method === 'GET' && req.url === '/status') {
45
+ reply(200, controls.status());
46
+ return;
47
+ }
48
+ if (req.method === 'GET' && req.url === '/devices') {
49
+ reply(200, { devices: controls.devices() });
50
+ return;
51
+ }
52
+ if (req.method !== 'POST') {
53
+ reply(404, { error: 'not-found' });
54
+ return;
55
+ }
56
+ let body = '';
57
+ for await (const chunk of req) {
58
+ body += String(chunk);
59
+ if (Buffer.byteLength(body) > 4096) {
60
+ reply(413, { error: 'too-large' });
61
+ return;
62
+ }
63
+ }
64
+ const input = JSON.parse(body || '{}');
65
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
66
+ reply(400, { error: 'invalid-params' });
67
+ return;
68
+ }
69
+ if (req.url === '/pair' && !Object.keys(input).length) {
70
+ reply(200, await controls.pair());
71
+ return;
72
+ }
73
+ if (req.url === '/cancel' && !Object.keys(input).length) {
74
+ controls.cancelPair();
75
+ reply(200, {});
76
+ return;
77
+ }
78
+ if (req.url === '/revoke' && Object.keys(input).length === 1 && typeof input.deviceId === 'string') {
79
+ reply(200, { revoked: controls.revoke(input.deviceId) });
80
+ return;
81
+ }
82
+ reply(400, { error: 'invalid-params' });
83
+ }
84
+ catch {
85
+ reply(500, { error: 'host-control-failed' });
86
+ }
87
+ });
88
+ server.requestTimeout = 5000;
89
+ server.headersTimeout = 5000;
90
+ await new Promise((resolve, reject) => { server.once('error', reject); server.listen(socket, () => { server.off('error', reject); resolve(); }); });
91
+ if (process.platform !== 'win32')
92
+ fs.chmodSync(socket, 0o600);
93
+ writePrivate(file, JSON.stringify({ pid: process.pid, socket, token }));
94
+ return { async close() {
95
+ await new Promise(resolve => server.close(() => resolve()));
96
+ if (fs.existsSync(file) && equal(record(directory).token, token))
97
+ fs.rmSync(file);
98
+ if (process.platform !== 'win32')
99
+ fs.rmSync(socket, { force: true });
100
+ } };
101
+ }
102
+ export async function localRequest(directory, method, route, body) {
103
+ const { socket, token } = record(directory);
104
+ return new Promise((resolve, reject) => {
105
+ const request = http.request({ socketPath: socket, path: route, method, headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, timeout: 6000 }, res => {
106
+ let text = '';
107
+ res.on('data', chunk => { text += String(chunk); if (Buffer.byteLength(text) > 1024 * 1024)
108
+ request.destroy(new Error('Oversized host control response')); });
109
+ res.on('end', () => { try {
110
+ if (res.statusCode !== 200)
111
+ throw new Error('Host control request failed');
112
+ resolve(JSON.parse(text));
113
+ }
114
+ catch (error) {
115
+ reject(error);
116
+ } });
117
+ });
118
+ request.on('timeout', () => request.destroy(new Error('Host control timed out')));
119
+ request.on('error', reject);
120
+ request.end(body === undefined ? undefined : JSON.stringify(body));
121
+ });
122
+ }
@@ -0,0 +1,43 @@
1
+ import { existsSync } from 'node:fs';
2
+ export type TailscaleState = 'unsupported' | 'not-installed' | 'needs-login' | 'needs-approval' | 'stopped' | 'starting' | 'running' | 'offline' | 'unavailable';
3
+ export interface TailscaleStatus {
4
+ platform: 'macos' | 'windows' | 'other';
5
+ state: TailscaleState;
6
+ canOpenApp: boolean;
7
+ addresses: string[];
8
+ }
9
+ export interface RemoteSetupStatus extends TailscaleStatus {
10
+ bridgeListening: boolean;
11
+ connectedDevices: number;
12
+ }
13
+ interface Installation {
14
+ cli: string;
15
+ app?: string;
16
+ }
17
+ interface Adapter {
18
+ platform: NodeJS.Platform;
19
+ installation(): Installation | undefined;
20
+ read(cli: string): Promise<string>;
21
+ open(app: string): Promise<void>;
22
+ now(): number;
23
+ }
24
+ export declare function findInstallation(platform: NodeJS.Platform, env?: NodeJS.ProcessEnv, exists?: typeof existsSync): Installation | undefined;
25
+ /** Only allowlisted state leaves this boundary; CLI data is never logged or stored. */
26
+ export declare function parseStatus(text: string): Pick<TailscaleStatus, 'state' | 'addresses'>;
27
+ export declare function createTailscaleControls(adapter?: Adapter): {
28
+ status(fresh?: boolean): Promise<TailscaleStatus>;
29
+ open(): Promise<void>;
30
+ };
31
+ /** Ready-to-pair is based on a current bound address, never just an installed app. */
32
+ export declare function createRemoteControls(server: {
33
+ addresses: string[];
34
+ devices(): {
35
+ connectedVia?: string;
36
+ }[];
37
+ refreshInterfaces(): Promise<void>;
38
+ }, tailscale: ReturnType<typeof createTailscaleControls>, pair: () => Promise<import('./types.js').HostPairing>): {
39
+ remoteSetup: (fresh?: boolean) => Promise<RemoteSetupStatus>;
40
+ openTailscale: () => Promise<void>;
41
+ pairRemote(): Promise<import("./types.js").HostPairing | undefined>;
42
+ };
43
+ export {};
@@ -0,0 +1,137 @@
1
+ import { execFile, spawn } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { isIP } from 'node:net';
4
+ import path from 'node:path';
5
+ import { isTailscale } from '../discovery/interfaces.js';
6
+ // Do not search cwd/PATH for a program invoked by privileged host UI controls.
7
+ export function findInstallation(platform, env = process.env, exists = existsSync) {
8
+ if (platform === 'darwin') {
9
+ const app = '/Applications/Tailscale.app', cli = `${app}/Contents/MacOS/Tailscale`;
10
+ return exists(cli) ? { cli, ...(exists(app) ? { app } : {}) } : undefined;
11
+ }
12
+ if (platform === 'win32') {
13
+ const root = env.ProgramFiles ?? 'C:\\Program Files';
14
+ if (!path.win32.isAbsolute(root) || root.startsWith('\\\\'))
15
+ return;
16
+ const cli = path.win32.join(root, 'Tailscale', 'tailscale.exe');
17
+ const app = path.win32.join(root, 'Tailscale', 'tailscale-ipn.exe');
18
+ return exists(cli) ? { cli, ...(exists(app) ? { app } : {}) } : undefined;
19
+ }
20
+ }
21
+ function readStatus(cli) {
22
+ return new Promise((resolve, reject) => {
23
+ execFile(cli, ['status', '--json', '--peers=false'], { encoding: 'utf8', timeout: 4000, maxBuffer: 256 * 1024, windowsHide: true }, (error, stdout) => {
24
+ // Even failures can contain a login URL. Never propagate native errors.
25
+ if (error)
26
+ reject(new Error('Tailscale status unavailable'));
27
+ else
28
+ resolve(stdout);
29
+ });
30
+ });
31
+ }
32
+ function openApp(app) {
33
+ return new Promise((resolve, reject) => {
34
+ if (process.platform === 'darwin') {
35
+ execFile('/usr/bin/open', ['-a', app], { timeout: 4000, maxBuffer: 4096 }, error => error ? reject(new Error('Could not open Tailscale')) : resolve());
36
+ }
37
+ else {
38
+ // The official tray app outlives this request; never kill it on a UI timeout.
39
+ const child = spawn(app, [], { detached: true, stdio: 'ignore', windowsHide: false });
40
+ child.once('error', () => reject(new Error('Could not open Tailscale')));
41
+ child.once('spawn', () => { child.unref(); resolve(); });
42
+ }
43
+ });
44
+ }
45
+ const object = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
46
+ /** Only allowlisted state leaves this boundary; CLI data is never logged or stored. */
47
+ export function parseStatus(text) {
48
+ const invalid = { state: 'unavailable', addresses: [] };
49
+ if (Buffer.byteLength(text) > 256 * 1024)
50
+ return { ...invalid, addresses: [] };
51
+ let value;
52
+ try {
53
+ value = object(JSON.parse(text));
54
+ }
55
+ catch {
56
+ return { ...invalid, addresses: [] };
57
+ }
58
+ if (!value)
59
+ return { ...invalid, addresses: [] };
60
+ const self = object(value.Self);
61
+ const known = { NeedsLogin: 'needs-login', NeedsMachineAuth: 'needs-approval', Stopped: 'stopped', Starting: 'starting', NoState: 'starting' };
62
+ if (typeof value.BackendState !== 'string')
63
+ return { ...invalid, addresses: [] };
64
+ if (value.BackendState !== 'Running')
65
+ return { state: Object.hasOwn(known, value.BackendState) ? known[value.BackendState] : 'unavailable', addresses: [] };
66
+ if (self?.KeyExpired === true)
67
+ return { state: 'needs-login', addresses: [] };
68
+ if (self?.Online === false)
69
+ return { state: 'offline', addresses: [] };
70
+ if (self?.Online !== true || !Array.isArray(value.TailscaleIPs))
71
+ return { ...invalid, addresses: [] };
72
+ const addresses = [...new Set(value.TailscaleIPs.filter((ip) => typeof ip === 'string' && Boolean(isIP(ip)) && isTailscale(ip)))].slice(0, 8);
73
+ return { state: addresses.length ? 'running' : 'starting', addresses };
74
+ }
75
+ export function createTailscaleControls(adapter = {
76
+ platform: process.platform, installation: () => findInstallation(process.platform), read: readStatus, open: openApp, now: Date.now,
77
+ }) {
78
+ let cached, expires = 0, pending;
79
+ const platform = adapter.platform === 'darwin' ? 'macos' : adapter.platform === 'win32' ? 'windows' : 'other';
80
+ return {
81
+ async status(fresh = false) {
82
+ if (!fresh && cached && adapter.now() < expires)
83
+ return cached;
84
+ if (pending)
85
+ return pending;
86
+ pending = (async () => {
87
+ const base = { platform, canOpenApp: false, addresses: [] };
88
+ if (platform === 'other')
89
+ return { ...base, state: 'unsupported' };
90
+ try {
91
+ const installation = adapter.installation();
92
+ if (!installation)
93
+ return { ...base, state: 'not-installed' };
94
+ const present = { ...base, canOpenApp: Boolean(installation.app) };
95
+ try {
96
+ return { ...present, ...parseStatus(await adapter.read(installation.cli)) };
97
+ }
98
+ catch {
99
+ return { ...present, state: 'unavailable' };
100
+ }
101
+ }
102
+ catch {
103
+ return { ...base, state: 'unavailable' };
104
+ }
105
+ })();
106
+ try {
107
+ cached = await pending;
108
+ expires = adapter.now() + 5000;
109
+ return cached;
110
+ }
111
+ finally {
112
+ pending = undefined;
113
+ }
114
+ },
115
+ async open() {
116
+ const installation = adapter.installation();
117
+ if (!installation?.app)
118
+ throw new Error('Tailscale app unavailable');
119
+ await adapter.open(installation.app);
120
+ expires = 0;
121
+ },
122
+ };
123
+ }
124
+ /** Ready-to-pair is based on a current bound address, never just an installed app. */
125
+ export function createRemoteControls(server, tailscale, pair) {
126
+ const remoteSetup = async (fresh = false) => {
127
+ const status = await tailscale.status(fresh);
128
+ return { ...status, bridgeListening: status.state === 'running' && status.addresses.some(address => server.addresses.includes(address)),
129
+ connectedDevices: server.devices().filter(device => device.connectedVia === 'tailscale').length };
130
+ };
131
+ return { remoteSetup, openTailscale: () => tailscale.open(), async pairRemote() {
132
+ await server.refreshInterfaces();
133
+ if (!(await remoteSetup(true)).bridgeListening)
134
+ return;
135
+ return pair();
136
+ } };
137
+ }
@@ -0,0 +1,40 @@
1
+ import type { Device, HostInfo } from '../protocol/dmp.js';
2
+ import type { RemoteSetupStatus } from './tailscale.js';
3
+ export type HostDevice = Omit<Device, 'current'> & {
4
+ connected: boolean;
5
+ connectedVia?: 'lan' | 'tailscale' | 'other';
6
+ };
7
+ export interface HostStatus {
8
+ host: HostInfo;
9
+ bridgeVersion: string;
10
+ dshVersion: string;
11
+ dshStatus: string;
12
+ addresses: string[];
13
+ port: number;
14
+ connectedDevices: number;
15
+ pairing: {
16
+ active: boolean;
17
+ expiresAt?: string;
18
+ };
19
+ discovery: {
20
+ state: string;
21
+ serviceName: string;
22
+ error?: string;
23
+ };
24
+ guidance: string[];
25
+ }
26
+ export interface HostPairing {
27
+ url: string;
28
+ svg: string;
29
+ expiresAt: string;
30
+ }
31
+ export interface HostControls {
32
+ status(): HostStatus;
33
+ pair(): Promise<HostPairing>;
34
+ cancelPair(): void;
35
+ devices(): HostDevice[];
36
+ revoke(id: string): boolean;
37
+ remoteSetup(): Promise<RemoteSetupStatus>;
38
+ openTailscale(): Promise<void>;
39
+ pairRemote(): Promise<HostPairing | undefined>;
40
+ }
@@ -0,0 +1 @@
1
+ export {};
package/lib/index.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ export { inject } from './projection/index.js';
2
+ export declare const name = "dshm-bridge";
3
+ /** Trusted in-process host controls; never exposed as an unauthenticated route. */
4
+ export declare function getRunningBridge(stateDir: string): ({
5
+ identity: import("./types.js").ServerIdentity;
6
+ fingerprint: string;
7
+ readonly port: number;
8
+ readonly addresses: string[];
9
+ setCapabilities: (caps: import("./types.js").ServerIdentity["caps"]) => void;
10
+ readonly pairingUrl: string | undefined;
11
+ readonly pairingExpiresAt: string | undefined;
12
+ cancelPairing: () => void;
13
+ devices: () => {
14
+ connectedVia?: "lan" | "tailscale" | "other" | undefined;
15
+ connected: boolean;
16
+ lastSeenAt?: string | undefined;
17
+ deviceId: string;
18
+ name: string;
19
+ model: string;
20
+ secureEnclave: boolean;
21
+ pairedAt: string;
22
+ }[];
23
+ openPairing: () => string;
24
+ revoke: (deviceId: string, defer?: boolean) => boolean;
25
+ refreshInterfaces: () => Promise<void>;
26
+ close(): Promise<void>;
27
+ } & {
28
+ diagnostics: () => unknown;
29
+ }) | undefined;
30
+ export declare function apply(context: unknown, input?: unknown): Promise<void>;
package/lib/index.js ADDED
@@ -0,0 +1,98 @@
1
+ import QRCode from 'qrcode';
2
+ import { createAdvertiser } from './discovery/advertiser.js';
3
+ import { startLocalControl } from './host/local.js';
4
+ import { createTailscaleControls, createRemoteControls } from './host/tailscale.js';
5
+ import { installHostRoutes } from './projection/host.js';
6
+ import { readFileSync } from 'node:fs';
7
+ import { DmpError } from './errors.js';
8
+ import { parseConfig } from './config.js';
9
+ import { createMethods, CreationLedger, capabilities } from './methods/index.js';
10
+ import { startServer } from './server/index.js';
11
+ import { createProjectionHost, onDispose } from './projection/index.js';
12
+ import { PushController } from './push/controller.js';
13
+ export { inject } from './projection/index.js';
14
+ export const name = 'dshm-bridge';
15
+ const packageVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
16
+ if (typeof packageVersion !== 'string' || !packageVersion.length)
17
+ throw new Error('Missing bridge package version');
18
+ const running = new Map();
19
+ /** Trusted in-process host controls; never exposed as an unauthenticated route. */
20
+ export function getRunningBridge(stateDir) { return running.get(stateDir); }
21
+ export async function apply(context, input) {
22
+ const config = parseConfig(input);
23
+ const projection = await createProjectionHost(context);
24
+ const ledger = new CreationLedger();
25
+ let server;
26
+ let discovery;
27
+ let local;
28
+ let push;
29
+ let pushSetupFailures = 0;
30
+ try {
31
+ server = await startServer({ stateDir: config.stateDir, port: config.port, bindMode: config.bindMode,
32
+ onAuthenticated: deviceId => push?.connected(deviceId),
33
+ onRevoked: deviceId => push?.revoke(deviceId),
34
+ onAddresses: async (addresses, port) => { await discovery?.update({ hostId: server.identity.host.hostId, name: server.identity.host.name, addresses, port }); },
35
+ addresses: config.addresses, pairing: config.pairOnStart, quiet: config.quiet,
36
+ version: packageVersion, dshVersion: projection.version, dshStatus: projection.status, caps: projection.status === 'ok' ? capabilities : [],
37
+ async connect(methodContext) {
38
+ if (projection.status !== 'ok')
39
+ return {
40
+ async request(method) {
41
+ if (method !== 'host.info')
42
+ throw new DmpError(projection.status === 'incompatible' ? 'dsh-incompatible' : 'dsh-unavailable');
43
+ return { result: { host: methodContext.identity.host, bridgeVersion: methodContext.identity.version,
44
+ dshVersion: projection.version, dshStatus: projection.status, caps: [], time: new Date().toISOString() } };
45
+ },
46
+ dispose() { },
47
+ };
48
+ const peer = await projection.connect(methodContext.deviceId);
49
+ return createMethods(peer, { ...methodContext, get push() { return push; } }, ledger);
50
+ }, });
51
+ if (config.pushProxyUrl && projection.status === 'ok') {
52
+ try {
53
+ push = new PushController({ stateDir: config.stateDir, proxyUrl: config.pushProxyUrl,
54
+ hostId: server.identity.host.hostId, projection,
55
+ connected: id => server.devices().some(device => device.deviceId === id && device.connected),
56
+ devices: () => server.devices().map(device => device.deviceId),
57
+ capability: enabled => server.setCapabilities(enabled ? [...capabilities, 'push'] : capabilities),
58
+ });
59
+ await push.start();
60
+ }
61
+ catch {
62
+ pushSetupFailures++;
63
+ await push?.close();
64
+ push = undefined;
65
+ }
66
+ }
67
+ discovery = await createAdvertiser({ hostId: server.identity.host.hostId, name: server.identity.host.name, addresses: server.addresses, port: server.port });
68
+ const tailscale = createTailscaleControls();
69
+ const pair = async () => { const url = server.openPairing(); return { url, svg: await QRCode.toString(url, { type: 'svg', errorCorrectionLevel: 'M', margin: 4 }), expiresAt: server.pairingExpiresAt }; };
70
+ const controls = {
71
+ status: () => ({ host: server.identity.host, bridgeVersion: packageVersion, dshVersion: projection.version, dshStatus: projection.status, addresses: server.addresses, port: server.port, connectedDevices: server.devices().filter(device => device.connected).length, pairing: { active: Boolean(server.pairingUrl), ...(server.pairingExpiresAt ? { expiresAt: server.pairingExpiresAt } : {}) }, discovery: discovery.status(), guidance: process.platform === 'darwin' ? ['Allow DeepSeek Harness under System Settings → Privacy & Security → Local Network. If you run dsh web in a terminal, allow that terminal too.'] : process.platform === 'win32' ? ['Allow DeepSeek Harness on Private networks when Windows Firewall asks. Keep Public networks disabled.'] : [] }),
72
+ pair,
73
+ cancelPair: () => server.cancelPairing(), devices: () => server.devices(), revoke: id => server.revoke(id),
74
+ ...createRemoteControls(server, tailscale, pair),
75
+ };
76
+ installHostRoutes(context, controls);
77
+ local = await startLocalControl(config.stateDir, controls);
78
+ }
79
+ catch (error) {
80
+ await push?.close();
81
+ await local?.close();
82
+ await discovery?.close();
83
+ if (server)
84
+ await server.close();
85
+ await projection.dispose();
86
+ throw error;
87
+ }
88
+ running.set(config.stateDir, { ...server, get port() { return server.port; }, get addresses() { return server.addresses; },
89
+ get pairingUrl() { return server.pairingUrl; }, diagnostics: () => ({ ...projection.diagnostics?.(), ...(config.pushProxyUrl ? { push: { ...push?.counters, setupFailures: pushSetupFailures } } : {}) }) });
90
+ onDispose(context, async () => {
91
+ running.delete(config.stateDir);
92
+ await push?.close();
93
+ await local?.close();
94
+ await discovery?.close();
95
+ await server.close();
96
+ await projection.dispose();
97
+ });
98
+ }
@@ -0,0 +1,9 @@
1
+ import type { ConnectionMethods, DmpObject, MethodContext, ProjectionPeer } from '../types.js';
2
+ export declare const capabilities: string[];
3
+ /** Remove oldest complete display groups until a snapshot/page fits. */
4
+ export declare function fitPage(result: DmpObject): DmpObject;
5
+ export declare class CreationLedger {
6
+ private entries;
7
+ create(deviceId: string, params: DmpObject, peer: ProjectionPeer): Promise<DmpObject>;
8
+ }
9
+ export declare function createMethods(peer: ProjectionPeer, context: MethodContext, ledger: CreationLedger): ConnectionMethods;