dsh-zen-remote 2.2.8 → 2.3.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/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/docs/interface.md +1 -1
- package/docs/remote-access.md +20 -0
- package/lan-gate.mjs +11 -1
- package/lib/adapters/dsh-better-sidebar/index.js +6 -2
- package/lib/adapters/dsh-better-sidebar/index.js.map +1 -1
- package/lib/adapters/dsh-better-sidebar/methods.js +3 -0
- package/lib/adapters/dsh-better-sidebar/methods.js.map +1 -1
- package/lib/admin-routes.js +33 -2
- package/lib/admin-routes.js.map +1 -1
- package/lib/client-routes.js +52 -0
- package/lib/client-routes.js.map +1 -1
- package/lib/client.js +3390 -10
- package/lib/config.js +70 -0
- package/lib/config.js.map +1 -1
- package/lib/index.js +232 -2
- package/lib/index.js.map +1 -1
- package/lib/intercept.js +43 -3
- package/lib/intercept.js.map +1 -1
- package/lib/lan-gate-server.cjs +29 -3
- package/lib/merge-streams.js +42 -12
- package/lib/merge-streams.js.map +1 -1
- package/lib/relay-client.js +71 -0
- package/lib/relay-client.js.map +1 -1
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts +1 -1
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -1
- package/lib/types/adapters/dsh-better-sidebar/methods.d.ts.map +1 -1
- package/lib/types/admin-routes.d.ts +13 -0
- package/lib/types/admin-routes.d.ts.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +204 -1
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/wizard/GatewayWizard.d.ts +29 -0
- package/lib/types/client/wizard/GatewayWizard.d.ts.map +1 -0
- package/lib/types/client/wizard/WizardCard.d.ts +30 -0
- package/lib/types/client/wizard/WizardCard.d.ts.map +1 -0
- package/lib/types/client/wizard/cards/MigrateCard.d.ts +15 -0
- package/lib/types/client/wizard/cards/MigrateCard.d.ts.map +1 -0
- package/lib/types/client/wizard/cards/PortForwardCard.d.ts +15 -0
- package/lib/types/client/wizard/cards/PortForwardCard.d.ts.map +1 -0
- package/lib/types/client/wizard/cards/ProxyCard.d.ts +19 -0
- package/lib/types/client/wizard/cards/ProxyCard.d.ts.map +1 -0
- package/lib/types/client/wizard/cards/TokenCard.d.ts +20 -0
- package/lib/types/client/wizard/cards/TokenCard.d.ts.map +1 -0
- package/lib/types/client/wizard/register-wizard.d.ts +51 -0
- package/lib/types/client/wizard/register-wizard.d.ts.map +1 -0
- package/lib/types/client/wizard/wizard-css.d.ts +10 -0
- package/lib/types/client/wizard/wizard-css.d.ts.map +1 -0
- package/lib/types/client-data/qr.d.ts +20 -0
- package/lib/types/client-data/qr.d.ts.map +1 -0
- package/lib/types/client-data/remote-status.d.ts +39 -2
- package/lib/types/client-data/remote-status.d.ts.map +1 -1
- package/lib/types/client-data/settings-form.d.ts +7 -2
- package/lib/types/client-data/settings-form.d.ts.map +1 -1
- package/lib/types/client-data/wizard.d.ts +247 -0
- package/lib/types/client-data/wizard.d.ts.map +1 -0
- package/lib/types/client-routes.d.ts +19 -0
- package/lib/types/client-routes.d.ts.map +1 -1
- package/lib/types/config.d.ts +27 -0
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/intercept.d.ts.map +1 -1
- package/lib/types/merge-streams.d.ts.map +1 -1
- package/lib/types/relay-client.d.ts +10 -0
- package/lib/types/relay-client.d.ts.map +1 -1
- package/lib/types/wizard/binary-io.d.ts +111 -0
- package/lib/types/wizard/binary-io.d.ts.map +1 -0
- package/lib/types/wizard/binary.d.ts +118 -0
- package/lib/types/wizard/binary.d.ts.map +1 -0
- package/lib/types/wizard/caddy-process.d.ts +101 -0
- package/lib/types/wizard/caddy-process.d.ts.map +1 -0
- package/lib/types/wizard/caddy.d.ts +53 -0
- package/lib/types/wizard/caddy.d.ts.map +1 -0
- package/lib/types/wizard/cloudflare.d.ts +112 -0
- package/lib/types/wizard/cloudflare.d.ts.map +1 -0
- package/lib/types/wizard/cloudflared.d.ts +71 -0
- package/lib/types/wizard/cloudflared.d.ts.map +1 -0
- package/lib/types/wizard/probe.d.ts +66 -0
- package/lib/types/wizard/probe.d.ts.map +1 -0
- package/lib/types/wizard/proxy-snippets.d.ts +60 -0
- package/lib/types/wizard/proxy-snippets.d.ts.map +1 -0
- package/lib/types/wizard/routes.d.ts +152 -0
- package/lib/types/wizard/routes.d.ts.map +1 -0
- package/lib/types/wizard/source-detect.d.ts +53 -0
- package/lib/types/wizard/source-detect.d.ts.map +1 -0
- package/lib/types/wizard/status.d.ts +115 -0
- package/lib/types/wizard/status.d.ts.map +1 -0
- package/lib/types/wizard/token-store.d.ts +27 -0
- package/lib/types/wizard/token-store.d.ts.map +1 -0
- package/lib/types/wizard/tunnel-orchestrator.d.ts +90 -0
- package/lib/types/wizard/tunnel-orchestrator.d.ts.map +1 -0
- package/lib/types/wizard/verify.d.ts +97 -0
- package/lib/types/wizard/verify.d.ts.map +1 -0
- package/lib/wizard/binary-io.js +386 -0
- package/lib/wizard/binary-io.js.map +1 -0
- package/lib/wizard/binary.js +138 -0
- package/lib/wizard/binary.js.map +1 -0
- package/lib/wizard/caddy-process.js +162 -0
- package/lib/wizard/caddy-process.js.map +1 -0
- package/lib/wizard/caddy.js +80 -0
- package/lib/wizard/caddy.js.map +1 -0
- package/lib/wizard/cloudflare.js +334 -0
- package/lib/wizard/cloudflare.js.map +1 -0
- package/lib/wizard/cloudflared.js +158 -0
- package/lib/wizard/cloudflared.js.map +1 -0
- package/lib/wizard/probe.js +118 -0
- package/lib/wizard/probe.js.map +1 -0
- package/lib/wizard/proxy-snippets.js +112 -0
- package/lib/wizard/proxy-snippets.js.map +1 -0
- package/lib/wizard/routes.js +462 -0
- package/lib/wizard/routes.js.map +1 -0
- package/lib/wizard/source-detect.js +77 -0
- package/lib/wizard/source-detect.js.map +1 -0
- package/lib/wizard/status.js +94 -0
- package/lib/wizard/status.js.map +1 -0
- package/lib/wizard/token-store.js +71 -0
- package/lib/wizard/token-store.js.map +1 -0
- package/lib/wizard/tunnel-orchestrator.js +155 -0
- package/lib/wizard/tunnel-orchestrator.js.map +1 -0
- package/lib/wizard/verify.js +171 -0
- package/lib/wizard/verify.js.map +1 -0
- package/package.json +37 -37
- package/scripts/check-settings-form.mjs +5 -4
- package/scripts/check-wizard-ui.mjs +690 -0
- package/src/adapters/dsh-better-sidebar/index.ts +6 -2
- package/src/adapters/dsh-better-sidebar/methods.ts +3 -0
- package/src/admin-routes.ts +44 -2
- package/src/client/index.tsx +9 -0
- package/src/client/locales.ts +410 -2
- package/src/client/wizard/GatewayWizard.tsx +2220 -0
- package/src/client/wizard/WizardCard.tsx +81 -0
- package/src/client/wizard/cards/MigrateCard.tsx +42 -0
- package/src/client/wizard/cards/PortForwardCard.tsx +37 -0
- package/src/client/wizard/cards/ProxyCard.tsx +56 -0
- package/src/client/wizard/cards/TokenCard.tsx +81 -0
- package/src/client/wizard/register-wizard.ts +99 -0
- package/src/client/wizard/wizard-css.ts +948 -0
- package/src/client-data/qr.ts +284 -0
- package/src/client-data/remote-status.ts +128 -2
- package/src/client-data/settings-form.ts +12 -3
- package/src/client-data/wizard.ts +432 -0
- package/src/client-routes.ts +53 -0
- package/src/config.ts +87 -0
- package/src/index.ts +227 -2
- package/src/intercept.ts +43 -3
- package/src/merge-streams.ts +41 -12
- package/src/relay-client.ts +76 -0
- package/src/wizard/binary-io.ts +449 -0
- package/src/wizard/binary.ts +177 -0
- package/src/wizard/caddy-process.ts +228 -0
- package/src/wizard/caddy.ts +97 -0
- package/src/wizard/cloudflare.ts +413 -0
- package/src/wizard/cloudflared.ts +196 -0
- package/src/wizard/probe.ts +161 -0
- package/src/wizard/proxy-snippets.ts +128 -0
- package/src/wizard/routes.ts +537 -0
- package/src/wizard/source-detect.ts +87 -0
- package/src/wizard/status.ts +180 -0
- package/src/wizard/token-store.ts +73 -0
- package/src/wizard/tunnel-orchestrator.ts +197 -0
- package/src/wizard/verify.ts +236 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wizard status projection (GW-1): declared ingress config + observed
|
|
3
|
+
* runtime facts → one sanitized JSON shape the browser may see.
|
|
4
|
+
*
|
|
5
|
+
* This module is the SECURITY BOUNDARY for everything the wizard surfaces
|
|
6
|
+
* on `GET /_dsh/zen-remote/admin/wizard/status`. The two properties it must
|
|
7
|
+
* hold, regardless of what the callers hand it:
|
|
8
|
+
*
|
|
9
|
+
* 1. No secret material. The Cloudflare API token and the cloudflared
|
|
10
|
+
* tunnel token never enter this module — the runtime side reports only
|
|
11
|
+
* `*Configured` booleans (the files live 0600 under
|
|
12
|
+
* `<DSH_HOME>/zen-remote-ingress/`, and their contents stay there).
|
|
13
|
+
* 2. No file paths. The runtime side knows where binaries, token files
|
|
14
|
+
* and the Caddyfile live; the projection carries none of it — every
|
|
15
|
+
* output field is built from a whitelist below, and input objects are
|
|
16
|
+
* never spread through.
|
|
17
|
+
*
|
|
18
|
+
* Pure logic on purpose: zero imports, so the check scripts and the tests
|
|
19
|
+
* can drive it through Node's strip-only type stripping, and the wizard
|
|
20
|
+
* routes can call it per request without re-resolving anything.
|
|
21
|
+
*
|
|
22
|
+
* Every input is coerced defensively. The declared side normally arrives
|
|
23
|
+
* from `resolveConfig(...).values` (already legal), but the projection is
|
|
24
|
+
* also the last line of defense for the wire: an off-enum mode answers
|
|
25
|
+
* `'off'`, a junk port answers the 443 default, a non-string domain answers
|
|
26
|
+
* `''` — same transparency discipline as the config resolver, applied at
|
|
27
|
+
* the rendering edge.
|
|
28
|
+
*/
|
|
29
|
+
/** The ingress modes `resolveConfig` can legally produce. */
|
|
30
|
+
export const WIZARD_INGRESS_MODES = ['off', 'caddy', 'external', 'cloudflared'];
|
|
31
|
+
function modeOf(value) {
|
|
32
|
+
return typeof value === 'string' && WIZARD_INGRESS_MODES.includes(value)
|
|
33
|
+
? value
|
|
34
|
+
: 'off';
|
|
35
|
+
}
|
|
36
|
+
function stringOf(value) {
|
|
37
|
+
return typeof value === 'string' ? value : '';
|
|
38
|
+
}
|
|
39
|
+
function portOf(value) {
|
|
40
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= 65535 ? value : null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Project one probe / verification memory down to its three wire fields.
|
|
44
|
+
* A record without a string `result` is not a memory — it answers null, so
|
|
45
|
+
* a corrupt runtime entry can never render as a half-valid verdict.
|
|
46
|
+
*/
|
|
47
|
+
function recordOf(input) {
|
|
48
|
+
if (input === null || input === undefined || typeof input !== 'object')
|
|
49
|
+
return null;
|
|
50
|
+
const { result, deviceId, at } = input;
|
|
51
|
+
if (typeof result !== 'string' || result === '')
|
|
52
|
+
return null;
|
|
53
|
+
return {
|
|
54
|
+
result,
|
|
55
|
+
deviceId: typeof deviceId === 'string' && deviceId !== '' ? deviceId : null,
|
|
56
|
+
at: typeof at === 'number' && Number.isFinite(at) ? at : null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** Project one untrusted-proxy detection down to its two wire fields. */
|
|
60
|
+
function forwardedSourceOf(input) {
|
|
61
|
+
if (input === null || input === undefined || typeof input !== 'object')
|
|
62
|
+
return null;
|
|
63
|
+
const { ip, at } = input;
|
|
64
|
+
if (typeof ip !== 'string' || ip === '')
|
|
65
|
+
return null;
|
|
66
|
+
if (typeof at !== 'number' || !Number.isFinite(at))
|
|
67
|
+
return null;
|
|
68
|
+
return { ip, at };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Build the sanitized wizard status. Both arguments are optional so a
|
|
72
|
+
* composition can answer before anything is configured; the all-empty
|
|
73
|
+
* answer is the honest "wizard not started" state (`mode: 'off'`, no ports,
|
|
74
|
+
* no tokens configured, no memories).
|
|
75
|
+
*/
|
|
76
|
+
export function projectWizardStatus(declared = {}, runtime = {}) {
|
|
77
|
+
return {
|
|
78
|
+
ok: true,
|
|
79
|
+
mode: modeOf(declared.ingressMode),
|
|
80
|
+
domain: stringOf(declared.ingressDomain),
|
|
81
|
+
httpsPort: portOf(declared.ingressHttpsPort) ?? 443,
|
|
82
|
+
publicUrl: stringOf(declared.ingressPublicUrl),
|
|
83
|
+
cfTokenConfigured: runtime.cfTokenConfigured === true,
|
|
84
|
+
tunnelTokenConfigured: runtime.tunnelTokenConfigured === true,
|
|
85
|
+
caddyRunning: runtime.caddyRunning === true,
|
|
86
|
+
cloudflaredRunning: runtime.cloudflaredRunning === true,
|
|
87
|
+
gatewayActualPort: portOf(runtime.gatewayActualPort),
|
|
88
|
+
...(typeof runtime.lanAddress === 'string' && runtime.lanAddress !== '' ? { lanAddress: runtime.lanAddress } : {}),
|
|
89
|
+
lastHostProbe: recordOf(runtime.lastHostProbe),
|
|
90
|
+
lastExternalVerification: recordOf(runtime.lastExternalVerification),
|
|
91
|
+
lastForwardedSource: forwardedSourceOf(runtime.lastForwardedSource),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/wizard/status.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wizard token storage (GW-13a): the secrets that must never enter the
|
|
3
|
+
* plugin row or any wire response live as 0600 files under
|
|
4
|
+
* `<DSH_HOME>/zen-remote-ingress/`, written through a temp-file rename so
|
|
5
|
+
* a crash mid-write can never leave a half token at the real path. The
|
|
6
|
+
* tunnel-token half lives in cloudflared.ts (it is the cloudflared
|
|
7
|
+
* process's own input); this module owns the shared Cloudflare API token.
|
|
8
|
+
*
|
|
9
|
+
* Reads answer presence or hand the raw string ONLY to in-process callers
|
|
10
|
+
* (the Caddy manager's env) — nothing here is ever logged or serialized
|
|
11
|
+
* into a response.
|
|
12
|
+
*/
|
|
13
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { ingressDir } from './binary.js';
|
|
16
|
+
/** `<DSH_HOME>/zen-remote-ingress/cf-api-token`. */
|
|
17
|
+
export function cfTokenPath(dshHome) {
|
|
18
|
+
return join(ingressDir(dshHome), 'cf-api-token');
|
|
19
|
+
}
|
|
20
|
+
/** Atomic 0600 write (temp sibling + rename), with a direct-write fallback
|
|
21
|
+
* on Windows if an existing file or antivirus scan refuses `renameSync` with
|
|
22
|
+
* EPERM/EACCES. */
|
|
23
|
+
export function writeCfTokenFile(dshHome, token) {
|
|
24
|
+
const finalPath = cfTokenPath(dshHome);
|
|
25
|
+
const partPath = `${finalPath}.part`;
|
|
26
|
+
try {
|
|
27
|
+
mkdirSync(ingressDir(dshHome), { recursive: true });
|
|
28
|
+
writeFileSync(partPath, token, { mode: 0o600 });
|
|
29
|
+
try {
|
|
30
|
+
renameSync(partPath, finalPath);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
writeFileSync(finalPath, token, { mode: 0o600 });
|
|
34
|
+
rmSync(partPath, { force: true });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
rmSync(partPath, { force: true });
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
return finalPath;
|
|
42
|
+
}
|
|
43
|
+
/** The stored token, or undefined when absent/empty. Callers feed it to the
|
|
44
|
+
* Caddy manager's environment; it must never reach a response or a log. */
|
|
45
|
+
export function readCfTokenFile(dshHome) {
|
|
46
|
+
try {
|
|
47
|
+
const token = readRaw(dshHome);
|
|
48
|
+
return token.trim() === '' ? undefined : token;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Whether a non-empty token file exists (the projection's boolean only). */
|
|
55
|
+
export function cfTokenConfigured(dshHome) {
|
|
56
|
+
try {
|
|
57
|
+
return statSync(cfTokenPath(dshHome)).isFile() && readRaw(dshHome).trim() !== '';
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Delete the stored token (the DELETE route; callers stop the processes
|
|
64
|
+
* that consume it first). */
|
|
65
|
+
export function deleteCfTokenFile(dshHome) {
|
|
66
|
+
rmSync(cfTokenPath(dshHome), { force: true });
|
|
67
|
+
}
|
|
68
|
+
function readRaw(dshHome) {
|
|
69
|
+
return readFileSync(cfTokenPath(dshHome), 'utf8');
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=token-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-store.js","sourceRoot":"","sources":["../../src/wizard/token-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAC9F,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC,oDAAoD;AACpD,MAAM,UAAU,WAAW,CAAC,OAAe;IACzC,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC,CAAA;AAClD,CAAC;AAED;;mBAEmB;AACnB,MAAM,UAAU,gBAAgB,CAAC,OAAe,EAAE,KAAa;IAC7D,MAAM,SAAS,GAAG,WAAW,CAAC,OAAO,CAAC,CAAA;IACtC,MAAM,QAAQ,GAAG,GAAG,SAAS,OAAO,CAAA;IACpC,IAAI,CAAC;QACH,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QACnD,aAAa,CAAC,QAAQ,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;QAC/C,IAAI,CAAC;YACH,UAAU,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAA;QACjC,CAAC;QAAC,MAAM,CAAC;YACP,aAAa,CAAC,SAAS,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;YAChD,MAAM,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QACnC,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QACjC,MAAM,KAAK,CAAA;IACb,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED;2EAC2E;AAC3E,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;QAC9B,OAAO,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAA;IAChD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,iBAAiB,CAAC,OAAe;IAC/C,IAAI,CAAC;QACH,OAAO,QAAQ,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAA;IAClF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED;6BAC6B;AAC7B,MAAM,UAAU,iBAAiB,CAAC,OAAe;IAC/C,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;AAC/C,CAAC;AAED,SAAS,OAAO,CAAC,OAAe;IAC9B,OAAO,YAAY,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,CAAA;AACnD,CAAC"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tunnel orchestration (GW-13b, 路 C's autopilot): one call drives the
|
|
3
|
+
* whole Cloudflare-side setup the design promises — zone/account lookup
|
|
4
|
+
* (through the ancestor-chain check), tunnel create-or-adopt, ingress
|
|
5
|
+
* configuration against the gateway's ACTUAL port, the proxied CNAME,
|
|
6
|
+
* and the cloudflared child.
|
|
7
|
+
*
|
|
8
|
+
* Rules the review and the design pin down:
|
|
9
|
+
*
|
|
10
|
+
* - **已有同名隧道**:the first setup pass answers `tunnel-conflict`
|
|
11
|
+
* with the same-name list; the UI lets the user pick one and re-runs
|
|
12
|
+
* with `tunnelId`, which adopts the existing tunnel through the token
|
|
13
|
+
* endpoint instead of creating anything.
|
|
14
|
+
* - **模式切换**:starting the tunnel stops the managed Caddy first
|
|
15
|
+
* (`stopCaddy`) — never two ingress processes at once.
|
|
16
|
+
* - The connector token travels from the Cloudflare answer into the
|
|
17
|
+
* 0600 token file and the child's `--token-file`; it never reaches a
|
|
18
|
+
* response or a log.
|
|
19
|
+
* - Every failure is a VALUE with a stable `reason` the UI maps to
|
|
20
|
+
* locale copy; CloudflareError codes map losslessly (`cf-*` → the
|
|
21
|
+
* orchestration's own reasons), unknown ones degrade to `api`.
|
|
22
|
+
*/
|
|
23
|
+
import { hostOf } from '../config.js';
|
|
24
|
+
import { createCloudflareApi } from './cloudflare.js';
|
|
25
|
+
import { writeTunnelTokenFile } from './cloudflared.js';
|
|
26
|
+
/** The tunnel name the wizard mints and looks for. */
|
|
27
|
+
export const TUNNEL_NAME = 'dsh-zen-remote';
|
|
28
|
+
/** CloudflareError.code → the orchestration's reason vocabulary. */
|
|
29
|
+
function mapCloudflareError(error) {
|
|
30
|
+
if (error !== null && typeof error === 'object' && 'code' in error) {
|
|
31
|
+
const code = error.code;
|
|
32
|
+
const detail = error.message;
|
|
33
|
+
if (code === 'cf-auth-failed')
|
|
34
|
+
return { ok: false, reason: 'auth', detail: String(detail ?? '') };
|
|
35
|
+
if (code === 'cf-network')
|
|
36
|
+
return { ok: false, reason: 'network', detail: String(detail ?? '') };
|
|
37
|
+
if (code === 'cf-zone-not-found')
|
|
38
|
+
return { ok: false, reason: 'zone-not-found' };
|
|
39
|
+
}
|
|
40
|
+
return { ok: false, reason: 'api', detail: String(error?.message ?? error) };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Drive the whole tunnel setup. Throws nothing — every outcome is a value.
|
|
44
|
+
*/
|
|
45
|
+
export async function setupTunnel(options, input) {
|
|
46
|
+
const token = options.readToken();
|
|
47
|
+
if (token === undefined || token.trim() === '')
|
|
48
|
+
return { ok: false, reason: 'no-token' };
|
|
49
|
+
const host = hostOf(input.domain);
|
|
50
|
+
if (host === undefined)
|
|
51
|
+
return { ok: false, reason: 'bad-domain' };
|
|
52
|
+
const api = options.apiFactory !== undefined
|
|
53
|
+
? options.apiFactory(token)
|
|
54
|
+
: createCloudflareApi({ token, fetchImpl: options.fetchImpl });
|
|
55
|
+
// Zone + account: the ancestor-chain check answers both in one verdict.
|
|
56
|
+
let zoneId;
|
|
57
|
+
let accountId;
|
|
58
|
+
try {
|
|
59
|
+
const check = await api.checkDomainOnCloudflare(host);
|
|
60
|
+
if (!check.onCloudflare || check.zoneId === null || check.accountId === null) {
|
|
61
|
+
return { ok: false, reason: 'zone-not-found' };
|
|
62
|
+
}
|
|
63
|
+
zoneId = check.zoneId;
|
|
64
|
+
accountId = check.accountId;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
return mapCloudflareError(error);
|
|
68
|
+
}
|
|
69
|
+
// Tunnel: adopt the user's choice, or create a fresh one — but a fresh
|
|
70
|
+
// create refuses when same-name tunnels already exist (the UI offers the
|
|
71
|
+
// list; the re-run carries `tunnelId`).
|
|
72
|
+
let tunnelId;
|
|
73
|
+
let tunnelToken;
|
|
74
|
+
try {
|
|
75
|
+
if (input.tunnelId !== undefined && input.tunnelId !== '') {
|
|
76
|
+
tunnelId = input.tunnelId;
|
|
77
|
+
tunnelToken = await api.getTunnelToken(accountId, tunnelId);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
const existing = await api.listTunnels(accountId);
|
|
81
|
+
const sameName = existing.filter((tunnel) => tunnel.name === TUNNEL_NAME);
|
|
82
|
+
if (sameName.length > 0)
|
|
83
|
+
return { ok: false, reason: 'tunnel-conflict', existing: sameName };
|
|
84
|
+
const created = await api.createTunnel(accountId, TUNNEL_NAME);
|
|
85
|
+
tunnelId = created.id;
|
|
86
|
+
tunnelToken = created.token;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
return mapCloudflareError(error);
|
|
91
|
+
}
|
|
92
|
+
const gatewayPort = await options.gatewayPort();
|
|
93
|
+
// The ingress mapping: the public hostname serves the local gateway.
|
|
94
|
+
try {
|
|
95
|
+
await api.configureIngress(accountId, tunnelId, host, `http://127.0.0.1:${gatewayPort}`);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
return mapCloudflareError(error);
|
|
99
|
+
}
|
|
100
|
+
// DNS idempotence next (GW-fix-4/M3): an existing CNAME for this name
|
|
101
|
+
// pointing at the SAME tunnel is a no-op; pointing elsewhere is a real
|
|
102
|
+
// conflict we refuse instead of silently overwriting.
|
|
103
|
+
try {
|
|
104
|
+
const existing = await api.listDnsRecords(zoneId, host);
|
|
105
|
+
const prior = existing.find((record) => record.name === host);
|
|
106
|
+
if (prior !== undefined) {
|
|
107
|
+
const expected = `${tunnelId}.cfargotunnel.com`;
|
|
108
|
+
if (prior.content !== expected) {
|
|
109
|
+
return { ok: false, reason: 'dns-conflict', detail: prior.content };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
await api.createDnsRecord(zoneId, host, `${tunnelId}.cfargotunnel.com`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
return mapCloudflareError(error);
|
|
118
|
+
}
|
|
119
|
+
// L1: the Cloudflare half is DONE — only now stop the managed Caddy and
|
|
120
|
+
// start the tunnel, so a failed API call leaves the old path untouched.
|
|
121
|
+
options.cloudflared.stopCaddy();
|
|
122
|
+
// Connector secret → 0600 file → child.
|
|
123
|
+
let tokenFile;
|
|
124
|
+
try {
|
|
125
|
+
tokenFile = writeTunnelTokenFile(options.dshHome, tunnelToken);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
return { ok: false, reason: 'spawn-failed', detail: String(error?.message ?? error) };
|
|
129
|
+
}
|
|
130
|
+
const started = await options.cloudflared.start({ tunnelToken });
|
|
131
|
+
if (!started.ok) {
|
|
132
|
+
return { ok: false, reason: 'spawn-failed', detail: started.error };
|
|
133
|
+
}
|
|
134
|
+
return { ok: true, tunnelId, publicUrl: `https://${host}`, tokenFile };
|
|
135
|
+
}
|
|
136
|
+
export async function checkDomainOnCloudflare(options, domain) {
|
|
137
|
+
const token = options.readToken();
|
|
138
|
+
if (token === undefined || token.trim() === '')
|
|
139
|
+
return { ok: false, reason: 'no-token' };
|
|
140
|
+
const host = hostOf(domain);
|
|
141
|
+
if (host === undefined)
|
|
142
|
+
return { ok: false, reason: 'bad-domain' };
|
|
143
|
+
const api = options.apiFactory !== undefined
|
|
144
|
+
? options.apiFactory(token)
|
|
145
|
+
: createCloudflareApi({ token, fetchImpl: options.fetchImpl });
|
|
146
|
+
try {
|
|
147
|
+
const check = await api.checkDomainOnCloudflare(host);
|
|
148
|
+
return { ok: true, onCloudflare: check.onCloudflare, zoneId: check.zoneId, accountId: check.accountId };
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
const mapped = mapCloudflareError(error);
|
|
152
|
+
return { ok: false, reason: mapped.reason === 'auth' ? 'auth' : mapped.reason === 'network' ? 'network' : 'api', detail: mapped.detail };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=tunnel-orchestrator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tunnel-orchestrator.js","sourceRoot":"","sources":["../../src/wizard/tunnel-orchestrator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Final-verification orchestration (GW-9): the wizard's last mile, where
|
|
3
|
+
* 「完成」 means a real phone really paired through the real public URL —
|
|
4
|
+
* never a green light from internal checks alone (设计: 不做已联通的假绿灯).
|
|
5
|
+
*
|
|
6
|
+
* The flow, spread over two entry points the UI drives:
|
|
7
|
+
*
|
|
8
|
+
* start({ publicUrl }): runtime ready? → walk the public URL
|
|
9
|
+
* (GW-8's probe; BOTH the pairing page and the admin-403 shape must be
|
|
10
|
+
* right) → snapshot the gateway's device ids (the baseline) → mint a WEB
|
|
11
|
+
* pairing code → remember the session IN MEMORY and answer with the
|
|
12
|
+
* code, the URL and the expiry. The code never touches the row config,
|
|
13
|
+
* and minting it restarts nothing — trustedProxies changes are what
|
|
14
|
+
* restart the gateway, and any such change invalidates the session
|
|
15
|
+
* instead (the config fingerprint below).
|
|
16
|
+
*
|
|
17
|
+
* check(): re-read the gateway status. A device id the baseline did not
|
|
18
|
+
* have is a freshly paired phone; the gateway's lastWebClaim record must
|
|
19
|
+
* name THAT device with secure === true (the proxy passed
|
|
20
|
+
* X-Forwarded-Proto: https through, so the issued cookie carries the
|
|
21
|
+
* Secure flag). All green → pass. The claim recorded an insecure cookie →
|
|
22
|
+
* cookie-insecure. Past the code's expiry → code-expired. Anything else →
|
|
23
|
+
* pending, keep polling.
|
|
24
|
+
*
|
|
25
|
+
* The session binds to a CONFIG FINGERPRINT (ingressMode, domain,
|
|
26
|
+
* httpsPort, publicUrl, the gateway's ACTUAL port, host, trustedProxies):
|
|
27
|
+
* any of those moving between start and check answers stale and discards
|
|
28
|
+
* the session, because the verification proved a URL that no longer exists.
|
|
29
|
+
* The session is deliberately memory-only — a row reload (which a
|
|
30
|
+
* trustedProxies change causes) builds a fresh manager and the session is
|
|
31
|
+
* gone, exactly the same verdict the fingerprint would have produced.
|
|
32
|
+
*/
|
|
33
|
+
import { normalizeServerUrl } from '../client-pairing.js';
|
|
34
|
+
import { probeHostPath } from './probe.js';
|
|
35
|
+
/**
|
|
36
|
+
* The config fingerprint a session binds to: any drift between start and
|
|
37
|
+
* check means the verified URL is no longer the deployed URL.
|
|
38
|
+
*/
|
|
39
|
+
export function verificationFingerprint(values, gatewayPort) {
|
|
40
|
+
return JSON.stringify({
|
|
41
|
+
ingressMode: values.ingressMode,
|
|
42
|
+
ingressDomain: values.ingressDomain,
|
|
43
|
+
ingressHttpsPort: values.ingressHttpsPort,
|
|
44
|
+
ingressPublicUrl: values.ingressPublicUrl,
|
|
45
|
+
gatewayPort,
|
|
46
|
+
host: values.host,
|
|
47
|
+
trustedProxies: values.trustedProxies,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export function createVerificationManager(options) {
|
|
51
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
52
|
+
const timeoutMs = options.timeoutMs ?? 5000;
|
|
53
|
+
const now = options.now ?? Date.now;
|
|
54
|
+
let session = null;
|
|
55
|
+
/** One loopback gateway call AS the local machine. Answer status+body or
|
|
56
|
+
* null on any failure — every caller degrades, none throws. */
|
|
57
|
+
const callGateway = async (method, path, payload) => {
|
|
58
|
+
try {
|
|
59
|
+
const response = await fetchImpl(`${options.gatewayBase}${path}`, {
|
|
60
|
+
method,
|
|
61
|
+
headers: payload === undefined ? undefined : { 'Content-Type': 'application/json' },
|
|
62
|
+
...(payload === undefined ? {} : { body: JSON.stringify(payload) }),
|
|
63
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
64
|
+
});
|
|
65
|
+
const body = await response.json();
|
|
66
|
+
return { status: response.status, body };
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const readStatus = async () => {
|
|
73
|
+
const reply = await callGateway('GET', '/lan-gate/status');
|
|
74
|
+
if (reply === null || reply.status < 200 || reply.status >= 300)
|
|
75
|
+
return null;
|
|
76
|
+
const body = reply.body;
|
|
77
|
+
if (body === null || typeof body !== 'object')
|
|
78
|
+
return null;
|
|
79
|
+
const deviceIds = Array.isArray(body.devices)
|
|
80
|
+
? body.devices.map((device) => (device !== null && typeof device === 'object' ? device.id : undefined))
|
|
81
|
+
.filter((id) => typeof id === 'string')
|
|
82
|
+
: [];
|
|
83
|
+
const port = typeof body.port === 'number' ? body.port : null;
|
|
84
|
+
const claim = body.lastWebClaim;
|
|
85
|
+
const lastWebClaim = claim !== null && typeof claim === 'object'
|
|
86
|
+
? {
|
|
87
|
+
id: typeof claim.id === 'string' ? claim.id : '',
|
|
88
|
+
secure: claim.secure === true,
|
|
89
|
+
at: typeof claim.at === 'number' ? claim.at : 0,
|
|
90
|
+
}
|
|
91
|
+
: null;
|
|
92
|
+
return { deviceIds, port, lastWebClaim };
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
async start({ publicUrl, probeOnly }) {
|
|
96
|
+
if (typeof publicUrl !== 'string' || publicUrl.trim() === '')
|
|
97
|
+
return { ok: false, reason: 'no-public-url' };
|
|
98
|
+
// Same admission as the probe: https anywhere, plain http only into
|
|
99
|
+
// the private-network allowances. The URL the phone will open must
|
|
100
|
+
// be one this machine is willing to verify at all.
|
|
101
|
+
const normalized = normalizeServerUrl(publicUrl);
|
|
102
|
+
if (!normalized.ok)
|
|
103
|
+
return { ok: false, reason: 'bad-public-url', detail: normalized.reason };
|
|
104
|
+
if ((await options.runtimeReady?.()) === false)
|
|
105
|
+
return { ok: false, reason: 'runtime-not-ready' };
|
|
106
|
+
const statusBefore = await readStatus();
|
|
107
|
+
if (statusBefore === null)
|
|
108
|
+
return { ok: false, reason: 'gateway-unreachable' };
|
|
109
|
+
const outcome = await (options.probe ?? probeHostPath)(normalized.url);
|
|
110
|
+
if (outcome.kind !== 'reached' || outcome.root !== 'gateway-pairing-page' || outcome.admin !== 'gateway-admin-403') {
|
|
111
|
+
return { ok: false, reason: 'probe-failed', detail: outcome };
|
|
112
|
+
}
|
|
113
|
+
if (probeOnly === true) {
|
|
114
|
+
return { ok: true, pairingCode: '', publicUrl: normalized.url, expiresAt: 0 };
|
|
115
|
+
}
|
|
116
|
+
const pair = await callGateway('POST', '/lan-gate/pair', { role: 'web' });
|
|
117
|
+
const pairBody = pair?.body;
|
|
118
|
+
if (pair === null || pair.status < 200 || pair.status >= 300 || pairBody?.ok !== true || typeof pairBody.code !== 'string' || typeof pairBody.expiresAt !== 'number') {
|
|
119
|
+
return { ok: false, reason: 'pair-refused', detail: pair === null ? null : pair.status };
|
|
120
|
+
}
|
|
121
|
+
session = {
|
|
122
|
+
publicUrl: normalized.url,
|
|
123
|
+
code: pairBody.code,
|
|
124
|
+
expiresAt: pairBody.expiresAt,
|
|
125
|
+
knownDeviceIds: new Set(statusBefore.deviceIds),
|
|
126
|
+
fingerprint: verificationFingerprint(options.getValues(), statusBefore.port),
|
|
127
|
+
};
|
|
128
|
+
return { ok: true, pairingCode: session.code, publicUrl: session.publicUrl, expiresAt: session.expiresAt };
|
|
129
|
+
},
|
|
130
|
+
async check() {
|
|
131
|
+
if (session === null)
|
|
132
|
+
return { result: 'not-started' };
|
|
133
|
+
const status = await readStatus();
|
|
134
|
+
if (status === null)
|
|
135
|
+
return { result: 'pending' };
|
|
136
|
+
// The verified URL must still BE the deployed URL: a config or
|
|
137
|
+
// gateway-port drift discards the session outright.
|
|
138
|
+
if (verificationFingerprint(options.getValues(), status.port) !== session.fingerprint) {
|
|
139
|
+
session = null;
|
|
140
|
+
return { result: 'stale' };
|
|
141
|
+
}
|
|
142
|
+
// New devices first, expiry second: the claim itself proves the code
|
|
143
|
+
// was still valid when it was redeemed, so a poll that lands moments
|
|
144
|
+
// past the expiry must not swallow a pass.
|
|
145
|
+
const newcomers = status.deviceIds.filter((id) => !session.knownDeviceIds.has(id));
|
|
146
|
+
if (newcomers.length > 0) {
|
|
147
|
+
const deviceId = newcomers[newcomers.length - 1];
|
|
148
|
+
const claim = status.lastWebClaim;
|
|
149
|
+
if (claim !== null && claim.id === deviceId) {
|
|
150
|
+
const isHttps = session.publicUrl.startsWith('https://');
|
|
151
|
+
session = null;
|
|
152
|
+
// Plain http:// LAN/Tailscale pairing does not sit behind a TLS
|
|
153
|
+
// reverse proxy and intentionally issues a non-Secure cookie.
|
|
154
|
+
if (isHttps && !claim.secure)
|
|
155
|
+
return { result: 'cookie-insecure', deviceId };
|
|
156
|
+
return { result: 'pass', deviceId, at: claim.at };
|
|
157
|
+
}
|
|
158
|
+
// A claim record that has not surfaced (or names a different,
|
|
159
|
+
// later claim): keep polling rather than bless an unverified cookie.
|
|
160
|
+
return { result: 'pending' };
|
|
161
|
+
}
|
|
162
|
+
if (now() >= session.expiresAt) {
|
|
163
|
+
session = null;
|
|
164
|
+
return { result: 'code-expired' };
|
|
165
|
+
}
|
|
166
|
+
return { result: 'pending' };
|
|
167
|
+
},
|
|
168
|
+
hasSession: () => session !== null,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../../src/wizard/verify.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-zen-remote",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"description": "一个插件、两种角色:主服务端把 DeepSeek Harness 变成手机能安全访问的 PWA(移动端界面、配对码网关、Web Push),并把选定会话共享给已配对的桌面端;子客户端在另一台 DSH 桌面端的侧边栏里远程使用这些会话。One DSH plugin, two roles: the host turns DeepSeek Harness into a phone-reachable PWA (mobile UI, pairing-code gateway behind your own reverse proxy, Web Push) and shares chosen sessions to paired desktops; the client uses those sessions remotely from another DSH desktop app's sidebar.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -117,41 +117,41 @@
|
|
|
117
117
|
"devDependencies": {
|
|
118
118
|
"@deepseek-ai/cordis": "4.0.5",
|
|
119
119
|
"@deepseek-ai/cosmokit": "1.8.5",
|
|
120
|
-
"@deepseek-ai/dsh-agent": "0.2.1-rc.
|
|
121
|
-
"@deepseek-ai/dsh-api-remotes": "0.2.1-rc.
|
|
122
|
-
"@deepseek-ai/dsh-api-session-controller": "0.2.1-rc.
|
|
123
|
-
"@deepseek-ai/dsh-api-workspace-controller": "0.2.1-rc.
|
|
124
|
-
"@deepseek-ai/dsh-brand": "0.2.1-rc.
|
|
125
|
-
"@deepseek-ai/dsh-client-connection": "0.2.1-rc.
|
|
126
|
-
"@deepseek-ai/dsh-client-locale": "0.2.1-rc.
|
|
127
|
-
"@deepseek-ai/dsh-client-store": "0.2.1-rc.
|
|
128
|
-
"@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.
|
|
129
|
-
"@deepseek-ai/dsh-client-ui-layout": "0.2.1-rc.
|
|
130
|
-
"@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.
|
|
131
|
-
"@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.
|
|
132
|
-
"@deepseek-ai/dsh-client-ui-session": "0.2.1-rc.
|
|
133
|
-
"@deepseek-ai/dsh-client-ui-sidebar": "0.2.1-rc.
|
|
134
|
-
"@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.
|
|
135
|
-
"@deepseek-ai/dsh-host-webserver": "0.2.1-rc.
|
|
136
|
-
"@deepseek-ai/dsh-llm": "0.2.1-rc.
|
|
137
|
-
"@deepseek-ai/dsh-ptc-runtime": "0.2.1-rc.
|
|
138
|
-
"@deepseek-ai/dsh-sandbox": "0.2.1-rc.
|
|
139
|
-
"@deepseek-ai/dsh-sandbox-policy": "0.2.1-rc.
|
|
140
|
-
"@deepseek-ai/dsh-scope": "0.2.1-rc.
|
|
141
|
-
"@deepseek-ai/dsh-session": "0.2.1-rc.
|
|
142
|
-
"@deepseek-ai/dsh-session-log-export": "0.2.1-rc.
|
|
143
|
-
"@deepseek-ai/dsh-session-projection": "0.2.1-rc.
|
|
144
|
-
"@deepseek-ai/dsh-session-query": "0.2.1-rc.
|
|
145
|
-
"@deepseek-ai/dsh-session-stats": "0.2.1-rc.
|
|
146
|
-
"@deepseek-ai/dsh-subagent": "0.2.1-rc.
|
|
147
|
-
"@deepseek-ai/dsh-timeout": "0.2.1-rc.
|
|
148
|
-
"@deepseek-ai/dsh-token-meter": "0.2.1-rc.
|
|
149
|
-
"@deepseek-ai/dsh-tools": "0.2.1-rc.
|
|
150
|
-
"@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.
|
|
151
|
-
"@deepseek-ai/dsh-util-crypto": "0.2.1-rc.
|
|
152
|
-
"@deepseek-ai/dsh-util-values": "0.2.1-rc.
|
|
153
|
-
"@deepseek-ai/dsh-workspace": "0.2.1-rc.
|
|
154
|
-
"@deepseek-ai/schemastery": "3.18.
|
|
120
|
+
"@deepseek-ai/dsh-agent": "0.2.1-rc.4",
|
|
121
|
+
"@deepseek-ai/dsh-api-remotes": "0.2.1-rc.4",
|
|
122
|
+
"@deepseek-ai/dsh-api-session-controller": "0.2.1-rc.4",
|
|
123
|
+
"@deepseek-ai/dsh-api-workspace-controller": "0.2.1-rc.4",
|
|
124
|
+
"@deepseek-ai/dsh-brand": "0.2.1-rc.4",
|
|
125
|
+
"@deepseek-ai/dsh-client-connection": "0.2.1-rc.4",
|
|
126
|
+
"@deepseek-ai/dsh-client-locale": "0.2.1-rc.4",
|
|
127
|
+
"@deepseek-ai/dsh-client-store": "0.2.1-rc.4",
|
|
128
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.4",
|
|
129
|
+
"@deepseek-ai/dsh-client-ui-layout": "0.2.1-rc.4",
|
|
130
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.4",
|
|
131
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.4",
|
|
132
|
+
"@deepseek-ai/dsh-client-ui-session": "0.2.1-rc.4",
|
|
133
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "0.2.1-rc.4",
|
|
134
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.4",
|
|
135
|
+
"@deepseek-ai/dsh-host-webserver": "0.2.1-rc.4",
|
|
136
|
+
"@deepseek-ai/dsh-llm": "0.2.1-rc.4",
|
|
137
|
+
"@deepseek-ai/dsh-ptc-runtime": "0.2.1-rc.4",
|
|
138
|
+
"@deepseek-ai/dsh-sandbox": "0.2.1-rc.4",
|
|
139
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.2.1-rc.4",
|
|
140
|
+
"@deepseek-ai/dsh-scope": "0.2.1-rc.4",
|
|
141
|
+
"@deepseek-ai/dsh-session": "0.2.1-rc.4",
|
|
142
|
+
"@deepseek-ai/dsh-session-log-export": "0.2.1-rc.4",
|
|
143
|
+
"@deepseek-ai/dsh-session-projection": "0.2.1-rc.4",
|
|
144
|
+
"@deepseek-ai/dsh-session-query": "0.2.1-rc.4",
|
|
145
|
+
"@deepseek-ai/dsh-session-stats": "0.2.1-rc.4",
|
|
146
|
+
"@deepseek-ai/dsh-subagent": "0.2.1-rc.4",
|
|
147
|
+
"@deepseek-ai/dsh-timeout": "0.2.1-rc.4",
|
|
148
|
+
"@deepseek-ai/dsh-token-meter": "0.2.1-rc.4",
|
|
149
|
+
"@deepseek-ai/dsh-tools": "0.2.1-rc.4",
|
|
150
|
+
"@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.4",
|
|
151
|
+
"@deepseek-ai/dsh-util-crypto": "0.2.1-rc.4",
|
|
152
|
+
"@deepseek-ai/dsh-util-values": "0.2.1-rc.4",
|
|
153
|
+
"@deepseek-ai/dsh-workspace": "0.2.1-rc.4",
|
|
154
|
+
"@deepseek-ai/schemastery": "3.18.5",
|
|
155
155
|
"@types/node": "^26.2.0",
|
|
156
156
|
"@types/react": "~18.3.1",
|
|
157
157
|
"@types/react-dom": "~18.3.0",
|
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
"scripts": {
|
|
161
161
|
"build": "tsc -p tsconfig.json && tsc -p tsconfig.client.json && node scripts/build-client.mjs",
|
|
162
162
|
"verify": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
|
|
163
|
-
"test": "node --test \"test/*.test.cjs\" && node scripts/check-sunk-viewport.mjs && node scripts/check-keyboard-avoid.mjs && node scripts/check-attach-upload.mjs && node scripts/check-share-image.mjs && node scripts/check-upload-endpoint.mjs && node scripts/check-host-shadow.mjs && node scripts/check-compat-store.mjs && node scripts/check-settings-form.mjs && node scripts/check-shares.mjs && node scripts/check-plugin-bridge-ui.mjs && node scripts/check-remote-drop.mjs && node scripts/check-client-externals.mjs && node scripts/check-info-sheet-stacking.mjs && node scripts/sync-doc-version.mjs --check",
|
|
163
|
+
"test": "node --test \"test/*.test.cjs\" && node scripts/check-sunk-viewport.mjs && node scripts/check-keyboard-avoid.mjs && node scripts/check-attach-upload.mjs && node scripts/check-share-image.mjs && node scripts/check-upload-endpoint.mjs && node scripts/check-host-shadow.mjs && node scripts/check-compat-store.mjs && node scripts/check-settings-form.mjs && node scripts/check-shares.mjs && node scripts/check-plugin-bridge-ui.mjs && node scripts/check-remote-drop.mjs && node scripts/check-client-externals.mjs && node scripts/check-info-sheet-stacking.mjs && node scripts/check-wizard-ui.mjs && node scripts/sync-doc-version.mjs --check",
|
|
164
164
|
"version": "node scripts/sync-doc-version.mjs && git add README.md README.zh-CN.md"
|
|
165
165
|
}
|
|
166
166
|
}
|
|
@@ -619,10 +619,11 @@ test('CLIENT_RECONNECT_ROUTE is the one reconnect endpoint', () => {
|
|
|
619
619
|
assert.equal(CLIENT_RECONNECT_ROUTE, '/_dsh/zen-remote/client/reconnect')
|
|
620
620
|
})
|
|
621
621
|
|
|
622
|
-
test('normalizePairingCode uppercases
|
|
623
|
-
assert.equal(normalizePairingCode('ab cd-
|
|
624
|
-
assert.equal(normalizePairingCode('
|
|
625
|
-
assert.equal(normalizePairingCode('abcd-
|
|
622
|
+
test('normalizePairingCode uppercases, filters to the 32-char code alphabet (no 0/O/1/I/L or symbols), and caps at 8 chars', () => {
|
|
623
|
+
assert.equal(normalizePairingCode('ab cd-23'), 'ABCD23')
|
|
624
|
+
assert.equal(normalizePairingCode(' abcd23ef '), 'ABCD23EF')
|
|
625
|
+
assert.equal(normalizePairingCode('abcd-23-ef-gh'), 'ABCD23EF')
|
|
626
|
+
assert.equal(normalizePairingCode('0O1IL!@#$%^&*()_+=abcd2345'), 'ABCD2345')
|
|
626
627
|
assert.equal(normalizePairingCode(''), '')
|
|
627
628
|
})
|
|
628
629
|
|