dsh-zen-remote 2.2.9 → 2.3.2
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.js +3293 -6
- 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 +58 -3
- package/lib/merge-streams.js +42 -12
- package/lib/merge-streams.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/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/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/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 +2224 -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/settings-form.ts +12 -3
- package/src/client-data/wizard.ts +432 -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/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,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reverse-proxy source detection (GW-5), pure logic — the browser-facing
|
|
3
|
+
* half of the 「检测到反代从 {ip} 连过来,自动信任?」flow.
|
|
4
|
+
*
|
|
5
|
+
* The gateway (lib/lan-gate-server.cjs) records the last request that
|
|
6
|
+
* carried proxy headers from a socket it does NOT trust — almost always
|
|
7
|
+
* the user's own reverse proxy whose IP never made it into
|
|
8
|
+
* `trustedProxies` — as `lastForwardedSource: { ip, at }` in its
|
|
9
|
+
* `/lan-gate/status` answer. {@link detectForwardedSource} lifts that
|
|
10
|
+
* field out (defensively: the gateway may be old and send nothing), and
|
|
11
|
+
* {@link mergeProxyList} / {@link normalizeTrustedIp} build the merged
|
|
12
|
+
* trust list the trust-proxy route persists.
|
|
13
|
+
*
|
|
14
|
+
* Pure: no imports, no I/O — the check scripts can drive it directly.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Extract the untrusted-proxy detection from one `/lan-gate/status` body.
|
|
18
|
+
* Returns null when the gateway predates the field or the field is
|
|
19
|
+
* malformed — a missing detection is the common case, never an error.
|
|
20
|
+
*/
|
|
21
|
+
export function detectForwardedSource(gatewayStatus) {
|
|
22
|
+
if (gatewayStatus === null || typeof gatewayStatus !== 'object')
|
|
23
|
+
return null;
|
|
24
|
+
const source = gatewayStatus.lastForwardedSource;
|
|
25
|
+
if (source === null || typeof source !== 'object')
|
|
26
|
+
return null;
|
|
27
|
+
const { ip, at } = source;
|
|
28
|
+
if (typeof ip !== 'string' || ip === '')
|
|
29
|
+
return null;
|
|
30
|
+
if (typeof at !== 'number' || !Number.isFinite(at))
|
|
31
|
+
return null;
|
|
32
|
+
return { ip, at };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* One trust-list entry: an IP literal, normalized the way the gateway
|
|
36
|
+
* normalizes socket peers (`::ffff:` IPv4-in-IPv6 unwrapped). Refuses
|
|
37
|
+
* anything that is not a plausible IPv4/IPv6 literal — the value goes
|
|
38
|
+
* into persisted config and later into an exact string comparison
|
|
39
|
+
* against socket peers, where a hostname or a CIDR would silently never
|
|
40
|
+
* match.
|
|
41
|
+
*/
|
|
42
|
+
export function normalizeTrustedIp(raw) {
|
|
43
|
+
if (typeof raw !== 'string')
|
|
44
|
+
return undefined;
|
|
45
|
+
const value = raw.trim();
|
|
46
|
+
if (value === '')
|
|
47
|
+
return undefined;
|
|
48
|
+
// IPv4-in-IPv6 form the gateway itself strips from socket peers.
|
|
49
|
+
const unwrapped = value.toLowerCase().startsWith('::ffff:') ? value.slice(7) : value;
|
|
50
|
+
if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(unwrapped)) {
|
|
51
|
+
return unwrapped.split('.').every((part) => Number(part) <= 255) ? unwrapped : undefined;
|
|
52
|
+
}
|
|
53
|
+
// IPv6: hex groups and colons only, at least one colon, no zone index.
|
|
54
|
+
if (/^[0-9a-f:]+$/i.test(unwrapped) && unwrapped.includes(':') && !unwrapped.includes('%')) {
|
|
55
|
+
return unwrapped.toLowerCase();
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
/** Split the comma-separated trustedProxies value into entries. */
|
|
60
|
+
export function parseProxyList(value) {
|
|
61
|
+
if (typeof value !== 'string')
|
|
62
|
+
return [];
|
|
63
|
+
return value.split(',').map((entry) => entry.trim()).filter((entry) => entry !== '');
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Append one IP to the comma-separated list, preserving order, skipping
|
|
67
|
+
* duplicates (the trust-proxy route answers "already trusted" instead of
|
|
68
|
+
* rewriting). Normalization is the caller's job — the route normalizes
|
|
69
|
+
* before merging.
|
|
70
|
+
*/
|
|
71
|
+
export function mergeProxyList(current, ip) {
|
|
72
|
+
const entries = parseProxyList(current);
|
|
73
|
+
if (entries.includes(ip))
|
|
74
|
+
return entries.join(',');
|
|
75
|
+
return [...entries, ip].join(',');
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=source-detect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source-detect.js","sourceRoot":"","sources":["../../src/wizard/source-detect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAiBH;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,aAAsB;IAC1D,IAAI,aAAa,KAAK,IAAI,IAAI,OAAO,aAAa,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAA;IAC5E,MAAM,MAAM,GAAI,aAAmD,CAAC,mBAAmB,CAAA;IACvF,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAA;IAC9D,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,MAA8B,CAAA;IACjD,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,IAAI,CAAA;IACpD,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QAAE,OAAO,IAAI,CAAA;IAC/D,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,CAAA;AACnB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAY;IAC7C,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAA;IAC7C,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,EAAE,CAAA;IACxB,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,SAAS,CAAA;IAClC,iEAAiE;IACjE,MAAM,SAAS,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAA;IACpF,IAAI,2BAA2B,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QAChD,OAAO,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAA;IAC1F,CAAC;IACD,uEAAuE;IACvE,IAAI,eAAe,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QAC3F,OAAO,SAAS,CAAC,WAAW,EAAE,CAAA;IAChC,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAA;IACxC,OAAO,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,EAAE,CAAC,CAAA;AACtF,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,OAAgB,EAAE,EAAU;IACzD,MAAM,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC,CAAA;IACvC,IAAI,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QAAE,OAAO,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAClD,OAAO,CAAC,GAAG,OAAO,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AACnC,CAAC"}
|
|
@@ -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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"}
|