@linxin666/dsh-remote-web-ui 0.3.23 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +13 -10
- package/README.zh.md +13 -10
- package/icon.svg +3 -0
- package/lib/client.js +396 -132
- package/lib/client.js.map +1 -1
- package/lib/index.js +604 -262
- package/lib/types/client/FenceNotice.d.ts.map +1 -1
- package/lib/types/client/FenceNotice.js +0 -3
- package/lib/types/client/PluginSettingsCard.d.ts +4 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +3 -2
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +14 -9
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +13 -4
- package/lib/types/client/RemoteSettingsCard.d.ts +6 -6
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +14 -8
- package/lib/types/client/UpdateEntry.js +2 -2
- package/lib/types/client/UpdatePanel.js +2 -2
- package/lib/types/client/deep-link.js +2 -2
- package/lib/types/client/index.d.ts +16 -8
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +56 -32
- package/lib/types/client/locales.d.ts +0 -2
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +6 -8
- package/lib/types/client/mobile-adapt.d.ts.map +1 -1
- package/lib/types/client/mobile-adapt.js +143 -28
- package/lib/types/client/pair-api.d.ts +32 -3
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/pair-api.js +44 -6
- package/lib/types/client/plugin-card-seat.d.ts +104 -0
- package/lib/types/client/plugin-card-seat.d.ts.map +1 -0
- package/lib/types/client/plugin-card-seat.js +143 -0
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +18 -5
- package/lib/types/client/settings-form.d.ts +17 -17
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +30 -27
- package/lib/types/console-output.d.ts +41 -0
- package/lib/types/console-output.d.ts.map +1 -0
- package/lib/types/console-output.js +89 -0
- package/lib/types/http.d.ts +18 -1
- package/lib/types/http.d.ts.map +1 -1
- package/lib/types/http.js +22 -1
- package/lib/types/index.d.ts +19 -11
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +91 -76
- package/lib/types/inner-auth.d.ts +3 -2
- package/lib/types/inner-auth.d.ts.map +1 -1
- package/lib/types/inner-auth.js +3 -2
- package/lib/types/pair-grant.d.ts +66 -0
- package/lib/types/pair-grant.d.ts.map +1 -0
- package/lib/types/pair-grant.js +95 -0
- package/lib/types/pairing.d.ts +44 -12
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +69 -20
- package/lib/types/pairing.persist.test.js +26 -0
- package/lib/types/posture.d.ts +11 -3
- package/lib/types/posture.d.ts.map +1 -1
- package/lib/types/posture.js +8 -3
- package/lib/types/remote-api.d.ts +10 -13
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-api.js +34 -34
- package/lib/types/remote-channel-boot.d.ts +6 -1
- package/lib/types/remote-channel-boot.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.js +16 -8
- package/lib/types/remote-channel-rules.d.ts +14 -0
- package/lib/types/remote-channel-rules.d.ts.map +1 -1
- package/lib/types/remote-channel-rules.js +8 -0
- package/lib/types/remote-methods.d.ts +1 -1
- package/lib/types/remote-methods.d.ts.map +1 -1
- package/lib/types/remote-methods.js +1 -0
- package/lib/types/routes.d.ts +47 -5
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +160 -58
- package/lib/types/tunnel.d.ts +21 -6
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +58 -11
- package/lib/types/update.d.ts +4 -0
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +74 -33
- package/package.json +23 -20
- package/src/client/FenceNotice.tsx +0 -2
- package/src/client/PluginSettingsCard.tsx +7 -2
- package/src/client/RemoteEntry.tsx +14 -9
- package/src/client/RemotePanel.tsx +17 -7
- package/src/client/RemoteSettingsCard.tsx +16 -9
- package/src/client/UpdateEntry.tsx +2 -2
- package/src/client/UpdatePanel.tsx +3 -3
- package/src/client/deep-link.ts +2 -2
- package/src/client/index.ts +70 -40
- package/src/client/locales.ts +6 -8
- package/src/client/mobile-adapt.ts +136 -25
- package/src/client/pair-api.ts +47 -7
- package/src/client/plugin-card-seat.ts +192 -0
- package/src/client/remote-channel.ts +19 -4
- package/src/client/remote.module.css +38 -0
- package/src/client/settings-form.ts +34 -31
- package/src/console-output.ts +97 -0
- package/src/http.ts +23 -1
- package/src/index.ts +102 -79
- package/src/inner-auth.ts +3 -2
- package/src/pair-grant.ts +131 -0
- package/src/pairing.persist.test.ts +36 -0
- package/src/pairing.ts +75 -24
- package/src/posture.ts +12 -4
- package/src/remote-api.ts +34 -41
- package/src/remote-channel-boot.ts +16 -8
- package/src/remote-channel-rules.ts +17 -0
- package/src/remote-methods.ts +1 -0
- package/src/routes.ts +165 -57
- package/src/tunnel.ts +56 -11
- package/src/update.ts +74 -29
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Generated by scripts/sync-shared.mjs from shared/host/console-output.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
2
|
+
/**
|
|
3
|
+
* Console-output capture shared by the family's Host halves.
|
|
4
|
+
*
|
|
5
|
+
* A child process's stdout/stderr are raw bytes, and on Windows a console
|
|
6
|
+
* program writes the OEM code page (CP936/GBK on a zh-CN system), not UTF-8.
|
|
7
|
+
* Decoding each chunk on its own corrupts the text twice over: the
|
|
8
|
+
* encoding is guessed wrong, and a multi-byte character split across two reads
|
|
9
|
+
* decodes into two replacement characters. This capture accumulates raw bytes
|
|
10
|
+
* under a byte budget and decodes exactly once, at read time.
|
|
11
|
+
*/
|
|
12
|
+
/** Count the U+FFFD replacement characters one candidate decode produced. */
|
|
13
|
+
function replacementCount(text) {
|
|
14
|
+
let count = 0;
|
|
15
|
+
for (const character of text) {
|
|
16
|
+
if (character === '\uFFFD')
|
|
17
|
+
count += 1;
|
|
18
|
+
}
|
|
19
|
+
return count;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Decode one console byte buffer. Strict UTF-8 wins whenever it is valid;
|
|
23
|
+
* otherwise the bytes are not UTF-8 at all and the console wrote its own code
|
|
24
|
+
* page, so the candidate that loses the least text is taken.
|
|
25
|
+
* @param bytes - the captured bytes.
|
|
26
|
+
* @returns the decoded text.
|
|
27
|
+
*/
|
|
28
|
+
export function decodeConsoleBytes(bytes) {
|
|
29
|
+
try {
|
|
30
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Not valid UTF-8. Prefer whichever candidate leaves the fewest
|
|
34
|
+
// replacement characters so real UTF-8 text carrying one bad byte is not
|
|
35
|
+
// rewritten wholesale as GBK gibberish; without the legacy code page
|
|
36
|
+
// (small-ICU builds) the lenient UTF-8 pass is all that is left.
|
|
37
|
+
let best;
|
|
38
|
+
let bestLoss = Number.POSITIVE_INFINITY;
|
|
39
|
+
for (const label of ['gbk', 'utf-8']) {
|
|
40
|
+
let text;
|
|
41
|
+
try {
|
|
42
|
+
text = new TextDecoder(label).decode(bytes);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const loss = replacementCount(text);
|
|
48
|
+
if (loss < bestLoss) {
|
|
49
|
+
best = text;
|
|
50
|
+
bestLoss = loss;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/* v8 ignore next 3 -- reachable only without the legacy code page AND without the lenient pass */
|
|
54
|
+
if (best === undefined)
|
|
55
|
+
return Buffer.from(bytes).toString('utf8');
|
|
56
|
+
return best;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Create a capture bounded to a byte budget. Overflow drops from the FRONT
|
|
61
|
+
* (the tail is what carries a failure reason), and the cut never lands inside
|
|
62
|
+
* a character: leading UTF-8 continuation bytes are dropped with it, and a
|
|
63
|
+
* legacy code page's dangling lead byte — indistinguishable from a real byte
|
|
64
|
+
* without knowing the encoding until the whole buffer is decoded — is removed
|
|
65
|
+
* with the one replacement character it produced, so an over-long stream still
|
|
66
|
+
* reads cleanly at the seam.
|
|
67
|
+
* @param maxBytes - maximum retained bytes.
|
|
68
|
+
* @returns the capture.
|
|
69
|
+
*/
|
|
70
|
+
export function createOutputCapture(maxBytes) {
|
|
71
|
+
let buffer = Buffer.alloc(0);
|
|
72
|
+
let trimmed = false;
|
|
73
|
+
return {
|
|
74
|
+
push(chunk) {
|
|
75
|
+
buffer = buffer.length === 0 ? Buffer.from(chunk) : Buffer.concat([buffer, Buffer.from(chunk)]);
|
|
76
|
+
if (buffer.length <= maxBytes)
|
|
77
|
+
return;
|
|
78
|
+
let start = buffer.length - maxBytes;
|
|
79
|
+
while (start < buffer.length && (buffer[start] & 0xc0) === 0x80)
|
|
80
|
+
start += 1;
|
|
81
|
+
buffer = Buffer.from(buffer.subarray(start));
|
|
82
|
+
trimmed = true;
|
|
83
|
+
},
|
|
84
|
+
read() {
|
|
85
|
+
const text = decodeConsoleBytes(buffer);
|
|
86
|
+
return trimmed && text.startsWith('\uFFFD') ? text.slice(1) : text;
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
package/lib/types/http.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared JSON body/response helpers for the host route families: one strict
|
|
3
3
|
* bounded body reader, one lenient bounded body reader, one JSON object
|
|
4
|
-
* narrow,
|
|
4
|
+
* narrow, one JSON writer, and the outbound request-init helper that keeps a
|
|
5
|
+
* host fetch body decodable (withIdentityEncoding). Previously the readers
|
|
6
|
+
* were copy-pasted across the
|
|
5
7
|
* package route files (routes.ts, update-routes.ts, mobile-api.ts, and each
|
|
6
8
|
* family's route module) with drifting contracts: body caps ranging 4 KiB to
|
|
7
9
|
* 1 MiB and four distinct overflow behaviors (reject, undefined, null, throw).
|
|
@@ -34,6 +36,21 @@ export declare function readJsonBody(req: IncomingMessage, opts?: {
|
|
|
34
36
|
}): Promise<unknown | null>;
|
|
35
37
|
/** Narrow a value to a JSON object, or undefined when it is not one. */
|
|
36
38
|
export declare function asJsonObject(value: unknown): Record<string, unknown> | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Request init that asks a remote origin for an uncompressed body.
|
|
41
|
+
*
|
|
42
|
+
* The DSH host boots `@deepseek-ai/dsh-http-proxy`, whose top-level import of
|
|
43
|
+
* the npm `undici` copy replaces the legacy `undici.globalDispatcher.1` slot
|
|
44
|
+
* Node's built-in `fetch()` reads. That cross-major wrapper drops the
|
|
45
|
+
* `content-encoding` header and automatic decompression, so a plain fetch
|
|
46
|
+
* resolves to raw gzip/brotli/zstd bytes — compressed noise no JSON or text
|
|
47
|
+
* consumer can read. Every host fetch that parses a remote body must request
|
|
48
|
+
* identity encoding. Caller headers survive; `accept-encoding` is forced to
|
|
49
|
+
* `identity` because a caller value has no decoder behind it in this host.
|
|
50
|
+
* @param init - the caller's request init (signal, headers, method, ...).
|
|
51
|
+
* @returns a copy with `accept-encoding: identity` merged in.
|
|
52
|
+
*/
|
|
53
|
+
export declare function withIdentityEncoding(init?: RequestInit): RequestInit;
|
|
37
54
|
/**
|
|
38
55
|
* Write one JSON response. Default headers are the family defaults
|
|
39
56
|
* (content-type and referrer-policy); caller headers are appended or
|
package/lib/types/http.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/http.ts"],"names":[],"mappings":"AACA
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/http.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAWrF;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAU9F;AAED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,eAAe,EACpB,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAO,GACrD,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAsBzB;AAOD,wEAAwE;AACxE,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAEhF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,GAAE,WAAgB,GAAG,WAAW,CAIxE;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,mBAAwB,GAChC,IAAI,CAIN"}
|
package/lib/types/http.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Shared JSON body/response helpers for the host route families: one strict
|
|
4
4
|
* bounded body reader, one lenient bounded body reader, one JSON object
|
|
5
|
-
* narrow,
|
|
5
|
+
* narrow, one JSON writer, and the outbound request-init helper that keeps a
|
|
6
|
+
* host fetch body decodable (withIdentityEncoding). Previously the readers
|
|
7
|
+
* were copy-pasted across the
|
|
6
8
|
* package route files (routes.ts, update-routes.ts, mobile-api.ts, and each
|
|
7
9
|
* family's route module) with drifting contracts: body caps ranging 4 KiB to
|
|
8
10
|
* 1 MiB and four distinct overflow behaviors (reject, undefined, null, throw).
|
|
@@ -80,6 +82,25 @@ function isJsonObject(value) {
|
|
|
80
82
|
export function asJsonObject(value) {
|
|
81
83
|
return isJsonObject(value) ? value : undefined;
|
|
82
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Request init that asks a remote origin for an uncompressed body.
|
|
87
|
+
*
|
|
88
|
+
* The DSH host boots `@deepseek-ai/dsh-http-proxy`, whose top-level import of
|
|
89
|
+
* the npm `undici` copy replaces the legacy `undici.globalDispatcher.1` slot
|
|
90
|
+
* Node's built-in `fetch()` reads. That cross-major wrapper drops the
|
|
91
|
+
* `content-encoding` header and automatic decompression, so a plain fetch
|
|
92
|
+
* resolves to raw gzip/brotli/zstd bytes — compressed noise no JSON or text
|
|
93
|
+
* consumer can read. Every host fetch that parses a remote body must request
|
|
94
|
+
* identity encoding. Caller headers survive; `accept-encoding` is forced to
|
|
95
|
+
* `identity` because a caller value has no decoder behind it in this host.
|
|
96
|
+
* @param init - the caller's request init (signal, headers, method, ...).
|
|
97
|
+
* @returns a copy with `accept-encoding: identity` merged in.
|
|
98
|
+
*/
|
|
99
|
+
export function withIdentityEncoding(init = {}) {
|
|
100
|
+
const headers = new Headers(init.headers);
|
|
101
|
+
headers.set('accept-encoding', 'identity');
|
|
102
|
+
return { ...init, headers };
|
|
103
|
+
}
|
|
83
104
|
/**
|
|
84
105
|
* Write one JSON response. Default headers are the family defaults
|
|
85
106
|
* (content-type and referrer-policy); caller headers are appended or
|
package/lib/types/index.d.ts
CHANGED
|
@@ -34,9 +34,12 @@ export declare const name = "remote-web-ui";
|
|
|
34
34
|
/** Services required before the pairing surfaces can mount. */
|
|
35
35
|
export declare const inject: string[];
|
|
36
36
|
/**
|
|
37
|
-
* Settings namespace of the remote-control capability
|
|
38
|
-
* settings
|
|
39
|
-
*
|
|
37
|
+
* Settings namespace of the remote-control capability. Under the 0.1.7
|
|
38
|
+
* settings model the namespace IS the Host profile entry id, so this is also
|
|
39
|
+
* the id the browser half asks `ctx.configForms` for (the family binder
|
|
40
|
+
* resolves the same value to the owning entry). Spelled here rather than
|
|
41
|
+
* imported: the browser half spells the same value and must not depend on a
|
|
42
|
+
* Host package.
|
|
40
43
|
*/
|
|
41
44
|
export declare const REMOTE_WEB_UI_SETTINGS_NAMESPACE: SettingsNamespace;
|
|
42
45
|
/** Plugin config, validated by the same-named schemastery schema. */
|
|
@@ -49,22 +52,28 @@ export interface Config {
|
|
|
49
52
|
maxDevices?: number;
|
|
50
53
|
/**
|
|
51
54
|
* Idle sessions older than this (ms) are deleted from memory and disk.
|
|
52
|
-
* Default is
|
|
55
|
+
* Default is 30 days; a leftover cookie no longer authorizes after expiry.
|
|
53
56
|
*/
|
|
54
57
|
idleExpireMs?: number;
|
|
55
58
|
/** Cookie name carrying the paired device id. */
|
|
56
59
|
cookieName?: string;
|
|
57
60
|
/**
|
|
58
61
|
* When true (default), a desktop Web GUI opened at a non-loopback origin
|
|
59
|
-
* rides the gated `/remote/api` channel
|
|
60
|
-
* cookie — the QR is the only way into remote desktop, and
|
|
61
|
-
* cut the
|
|
62
|
+
* rides the gated `/remote/api` channel, whose requests must carry a live
|
|
63
|
+
* paired-device cookie — the QR is the only way into remote desktop, and
|
|
64
|
+
* stop()/revoke() cut the channel and the pairing cookie off immediately.
|
|
65
|
+
* This policy selects the transport; it never loosens the channel, whose
|
|
66
|
+
* gate is unconditional (issue #1665). Scope
|
|
62
67
|
* note for this cohort: direct /api is governed by the harness fence +
|
|
63
68
|
* browser-auth cookie (the api/gate seam has no emitter on 0.1.2-alpha.2),
|
|
64
69
|
* so a harness browser credential a device has already redeemed is not
|
|
65
70
|
* invalidated by stop() — see the README security model. Set false to keep
|
|
66
71
|
* the desktop on plain `/api` (only useful when that origin is already
|
|
67
|
-
* trusted for `/api`).
|
|
72
|
+
* trusted for `/api`). Turning the policy off does NOT open the channel:
|
|
73
|
+
* the /remote proxy attaches the process's own browser-auth credential only
|
|
74
|
+
* when the request itself presents a live paired-device credential, so an
|
|
75
|
+
* unpaired LAN/tunnel caller is forwarded without it and the inner route
|
|
76
|
+
* answers 401.
|
|
68
77
|
*/
|
|
69
78
|
requirePairingForLan?: boolean;
|
|
70
79
|
/**
|
|
@@ -164,9 +173,8 @@ type ResolvedConfig = Required<Omit<Config, 'publicBaseUrl' | 'trustedHosts' | '
|
|
|
164
173
|
};
|
|
165
174
|
/**
|
|
166
175
|
* The single mapping from resolved plugin config to the pairing service
|
|
167
|
-
* config. Both the constructed service and every
|
|
168
|
-
* it, so no field can be silently dropped
|
|
169
|
-
* pushes a new value into the running service.
|
|
176
|
+
* config. Both the constructed service and every later apply of this row
|
|
177
|
+
* reuse it, so no field can be silently dropped.
|
|
170
178
|
*/
|
|
171
179
|
export declare function pairingConfigOf(resolved: Pick<ResolvedConfig, 'tokenTtlMs' | 'offlineAfterMs' | 'maxDevices' | 'idleExpireMs' | 'cookieName' | 'devicesFile'>): PairingConfig;
|
|
172
180
|
/** Default paired-session store: `$DSH_HOME/remote-web-ui-devices.json`. */
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAClE,OAAO,CAAC,MAAM,aAAa,CAAA;AAO3B,OAAO,EAA0C,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
|
package/lib/types/index.js
CHANGED
|
@@ -26,6 +26,7 @@ import { isHttpUrl, tunnelPlanOf } from "./tunnel-plan.js";
|
|
|
26
26
|
import { loadRelayIdentity, RelayRegistrar } from "./relay-registry.js";
|
|
27
27
|
import { desiredBindHost, desiredBindPort, firewallActionNeeded, pendingRestartOf } from "./lan-bind-plan.js";
|
|
28
28
|
import { createInnerAuth } from "./inner-auth.js";
|
|
29
|
+
import { withIdentityEncoding } from "./http.js";
|
|
29
30
|
import { TunnelManager } from "./tunnel.js";
|
|
30
31
|
import { PublicBaseKeeper } from "./public-base.js";
|
|
31
32
|
import { checkUpdates, fetchGitHubReleaseNotes, fetchLatestVersion, RELEASE_NOTES_CACHE_TTL_MS, resolveAnchorManifest, resolveUpdateTarget, runUpdateVerified, } from "./update.js";
|
|
@@ -38,9 +39,12 @@ export const name = 'remote-web-ui';
|
|
|
38
39
|
/** Services required before the pairing surfaces can mount. */
|
|
39
40
|
export const inject = ['webServer', 'typertGateway', 'connection'];
|
|
40
41
|
/**
|
|
41
|
-
* Settings namespace of the remote-control capability
|
|
42
|
-
* settings
|
|
43
|
-
*
|
|
42
|
+
* Settings namespace of the remote-control capability. Under the 0.1.7
|
|
43
|
+
* settings model the namespace IS the Host profile entry id, so this is also
|
|
44
|
+
* the id the browser half asks `ctx.configForms` for (the family binder
|
|
45
|
+
* resolves the same value to the owning entry). Spelled here rather than
|
|
46
|
+
* imported: the browser half spells the same value and must not depend on a
|
|
47
|
+
* Host package.
|
|
44
48
|
*/
|
|
45
49
|
export const REMOTE_WEB_UI_SETTINGS_NAMESPACE = 'remote-web-ui';
|
|
46
50
|
export const Config = z.object({
|
|
@@ -62,11 +66,16 @@ export const Config = z.object({
|
|
|
62
66
|
});
|
|
63
67
|
/** Presence sweep cadence (a stale device flips to disconnected within two sweeps). */
|
|
64
68
|
const SWEEP_INTERVAL_MS = 10_000;
|
|
69
|
+
/**
|
|
70
|
+
* How long one update-status probe answer is reused. The probe fans out one
|
|
71
|
+
* registry GET per family package plus a GitHub call, and the sidebar update
|
|
72
|
+
* entry asks for the status on every GUI page load.
|
|
73
|
+
*/
|
|
74
|
+
const UPDATE_STATUS_TTL_MS = 60_000;
|
|
65
75
|
/**
|
|
66
76
|
* The single mapping from resolved plugin config to the pairing service
|
|
67
|
-
* config. Both the constructed service and every
|
|
68
|
-
* it, so no field can be silently dropped
|
|
69
|
-
* pushes a new value into the running service.
|
|
77
|
+
* config. Both the constructed service and every later apply of this row
|
|
78
|
+
* reuse it, so no field can be silently dropped.
|
|
70
79
|
*/
|
|
71
80
|
export function pairingConfigOf(resolved) {
|
|
72
81
|
return {
|
|
@@ -125,30 +134,12 @@ function applyImpl(ctx, config) {
|
|
|
125
134
|
profile: config?.profile ?? process.env.DSH_PROFILE ?? DEFAULTS.profile,
|
|
126
135
|
enabled: config?.enabled ?? DEFAULTS.enabled,
|
|
127
136
|
};
|
|
128
|
-
// The
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return {
|
|
135
|
-
tokenTtlMs: value.tokenTtlMs ?? DEFAULTS.tokenTtlMs,
|
|
136
|
-
offlineAfterMs: value.offlineAfterMs ?? DEFAULTS.offlineAfterMs,
|
|
137
|
-
maxDevices: value.maxDevices ?? DEFAULTS.maxDevices,
|
|
138
|
-
idleExpireMs: value.idleExpireMs ?? DEFAULTS.idleExpireMs,
|
|
139
|
-
cookieName: value.cookieName ?? DEFAULTS.cookieName,
|
|
140
|
-
requirePairingForLan: value.requirePairingForLan ?? DEFAULTS.requirePairingForLan,
|
|
141
|
-
publicBaseUrl: value.publicBaseUrl ?? envPublicBase,
|
|
142
|
-
trustedHosts: value.trustedHosts,
|
|
143
|
-
devicesFile: value.devicesFile ?? DEFAULTS.devicesFile,
|
|
144
|
-
autoTunnel: value.autoTunnel ?? DEFAULTS.autoTunnel,
|
|
145
|
-
tunnelToken: value.tunnelToken,
|
|
146
|
-
relay: value.relay ?? DEFAULTS.relay,
|
|
147
|
-
lanBind: value.lanBind,
|
|
148
|
-
profile: value.profile ?? process.env.DSH_PROFILE ?? DEFAULTS.profile,
|
|
149
|
-
enabled: value.enabled ?? DEFAULTS.enabled,
|
|
150
|
-
};
|
|
151
|
-
};
|
|
137
|
+
// The effective configuration this activation runs on: under the 0.1.7
|
|
138
|
+
// settings model the plugin's own Config IS its settings document, so the
|
|
139
|
+
// Host serves the settings page from this schema and reloads the row after a
|
|
140
|
+
// save — a live edit re-enters apply() with the new value instead of being
|
|
141
|
+
// pushed into a running instance.
|
|
142
|
+
const resolve = () => resolved;
|
|
152
143
|
const service = new PairingService(pairingConfigOf(resolved));
|
|
153
144
|
// ── auto tunnel ─────────────────────────────────────────────────────────
|
|
154
145
|
// The minted public URL becomes the QR base (and the pairing fence's
|
|
@@ -271,13 +262,14 @@ function applyImpl(ctx, config) {
|
|
|
271
262
|
: [];
|
|
272
263
|
service.setLanBases(lanBases);
|
|
273
264
|
}
|
|
274
|
-
// Push
|
|
265
|
+
// Push the effective configuration into the service and gate. The service
|
|
275
266
|
// config object is read per operation (token mint, touch, sweep), and the
|
|
276
|
-
// gate re-reads its fence flag per request, so a
|
|
277
|
-
// without a restart
|
|
278
|
-
// sweep timer are dropped and all
|
|
279
|
-
// gate listener stays mounted so a
|
|
280
|
-
// (now vetoing every non-loopback
|
|
267
|
+
// gate re-reads its fence flag per request, so a saved edit takes effect
|
|
268
|
+
// without a restart (the Host reloads this row, and apply re-runs). When
|
|
269
|
+
// `enabled` is off, the pairing routes and sweep timer are dropped and all
|
|
270
|
+
// device/token state is revoked, but the gate listener stays mounted so a
|
|
271
|
+
// LAN-exposed /api stays behind pairing (now vetoing every non-loopback
|
|
272
|
+
// request) instead of opening the fence.
|
|
281
273
|
let disposeRoutes;
|
|
282
274
|
let disposeSweep;
|
|
283
275
|
// ── remote update ────────────────────────────────────────────────────────
|
|
@@ -308,16 +300,34 @@ function applyImpl(ctx, config) {
|
|
|
308
300
|
releaseNotesCache.set(version, { at: Date.now(), notes });
|
|
309
301
|
return notes;
|
|
310
302
|
};
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
303
|
+
// The status probe fans out one registry GET per family package (plus a
|
|
304
|
+
// GitHub release-notes call), and the sidebar update entry asks for it on
|
|
305
|
+
// every GUI page load — so the answer is memoized briefly and concurrent
|
|
306
|
+
// callers share one probe. A completed update invalidates it.
|
|
307
|
+
let updateStatusCache;
|
|
308
|
+
let updateStatusInFlight;
|
|
309
|
+
const checkStatusCached = () => {
|
|
310
|
+
const cached = updateStatusCache;
|
|
311
|
+
if (cached !== undefined && Date.now() - cached.at < UPDATE_STATUS_TTL_MS)
|
|
312
|
+
return Promise.resolve(cached.value);
|
|
313
|
+
if (updateStatusInFlight !== undefined)
|
|
314
|
+
return updateStatusInFlight;
|
|
315
|
+
updateStatusInFlight = checkUpdates({
|
|
316
316
|
anchorManifestPath: resolveAnchorPath(),
|
|
317
317
|
resolve: hostResolve,
|
|
318
318
|
fetchLatest: name => fetchLatestVersion(name, fetch),
|
|
319
319
|
fetchReleaseNotes: fetchReleaseNotesCached,
|
|
320
|
-
})
|
|
320
|
+
}).then((value) => {
|
|
321
|
+
updateStatusCache = { at: Date.now(), value };
|
|
322
|
+
return value;
|
|
323
|
+
}).finally(() => { updateStatusInFlight = undefined; });
|
|
324
|
+
return updateStatusInFlight;
|
|
325
|
+
};
|
|
326
|
+
const updateRoutes = makeUpdateRoutes({
|
|
327
|
+
// Control endpoints are host-surface only: a LAN/phone origin must never
|
|
328
|
+
// trigger a real install on this machine.
|
|
329
|
+
fence: request => isTrustedApiRequest(request, []),
|
|
330
|
+
check: () => checkStatusCached(),
|
|
321
331
|
run: async () => {
|
|
322
332
|
const target = resolveUpdateTarget({ anchorManifestPath: resolveAnchorPath() });
|
|
323
333
|
if ('error' in target) {
|
|
@@ -334,7 +344,7 @@ function applyImpl(ctx, config) {
|
|
|
334
344
|
// 11 minimumReleaseAge gate can silently keep the installed versions
|
|
335
345
|
// (same-day releases), which a plain exit-0 check would report as
|
|
336
346
|
// success — the user then restarts and nothing changed.
|
|
337
|
-
|
|
347
|
+
const result = await runUpdateVerified({
|
|
338
348
|
run: { profileDir: target.profileDir, packages: target.packages },
|
|
339
349
|
check: {
|
|
340
350
|
anchorManifestPath: resolveAnchorPath(),
|
|
@@ -343,6 +353,9 @@ function applyImpl(ctx, config) {
|
|
|
343
353
|
fetchReleaseNotes: fetchReleaseNotesCached,
|
|
344
354
|
},
|
|
345
355
|
});
|
|
356
|
+
// The install moved (or failed) the versions the cached status reported.
|
|
357
|
+
updateStatusCache = undefined;
|
|
358
|
+
return result;
|
|
346
359
|
},
|
|
347
360
|
});
|
|
348
361
|
// LAN-bind facts for the settings card, re-read per request so a hot
|
|
@@ -416,11 +429,17 @@ function applyImpl(ctx, config) {
|
|
|
416
429
|
return appShellCache.html;
|
|
417
430
|
const cookie = await innerAuth.ready();
|
|
418
431
|
try {
|
|
419
|
-
const response = await fetch(`http://127.0.0.1:${String(ctx.webServer.port)}/`, {
|
|
432
|
+
const response = await fetch(`http://127.0.0.1:${String(ctx.webServer.port)}/`, withIdentityEncoding({
|
|
420
433
|
headers: cookie !== undefined ? { cookie } : undefined,
|
|
421
|
-
});
|
|
422
|
-
if (!response.ok)
|
|
434
|
+
}));
|
|
435
|
+
if (!response.ok) {
|
|
436
|
+
// A stale credential (secret rotation, 30-day TTL) must not wedge every
|
|
437
|
+
// later landing: drop it so the next attempt re-redeems, exactly as the
|
|
438
|
+
// proxy path does on an upstream 401.
|
|
439
|
+
if (response.status === 401 || response.status === 403)
|
|
440
|
+
innerAuth.invalidate();
|
|
423
441
|
return undefined;
|
|
442
|
+
}
|
|
424
443
|
const html = await response.text();
|
|
425
444
|
appShellCache = { at: Date.now(), html };
|
|
426
445
|
return html;
|
|
@@ -458,15 +477,15 @@ function applyImpl(ctx, config) {
|
|
|
458
477
|
return list;
|
|
459
478
|
},
|
|
460
479
|
}),
|
|
461
|
-
// The remote desktop channel:
|
|
462
|
-
// re-issues fenced paths to loopback (see remote-api.ts). The
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
//
|
|
480
|
+
// The remote desktop channel: a paired-credential-gated `/remote` prefix
|
|
481
|
+
// that re-issues fenced paths to loopback (see remote-api.ts). The gate is
|
|
482
|
+
// unconditional — requirePairingForLan governs the plain /api surface and
|
|
483
|
+
// the desktop's client-side rewrite only; it is never an authorization
|
|
484
|
+
// input here, because this channel attaches the process's own browser
|
|
485
|
+
// credential to everything it forwards (issue #1665).
|
|
466
486
|
...makeRemoteApiRoutes({
|
|
467
487
|
service,
|
|
468
488
|
port: ctx.webServer.port,
|
|
469
|
-
requirePairingForLan: () => resolve().requirePairingForLan,
|
|
470
489
|
auth: innerAuth,
|
|
471
490
|
}),
|
|
472
491
|
...updateRoutes,
|
|
@@ -474,7 +493,6 @@ function applyImpl(ctx, config) {
|
|
|
474
493
|
const upgrades = makeRemoteApiUpgradeRoutes({
|
|
475
494
|
service,
|
|
476
495
|
port: ctx.webServer.port,
|
|
477
|
-
requirePairingForLan: () => resolve().requirePairingForLan,
|
|
478
496
|
auth: innerAuth,
|
|
479
497
|
});
|
|
480
498
|
const gate = makeGateListener(service, () => resolve().requirePairingForLan, () => resolve().enabled);
|
|
@@ -559,6 +577,9 @@ function applyImpl(ctx, config) {
|
|
|
559
577
|
if (ctx.webServer.host === '0.0.0.0') {
|
|
560
578
|
console.warn('remote-web-ui: LAN-exposed bind — pairing gates the /remote channel; direct /api stays under the harness fence + browser auth (stop() does not revoke an already-redeemed browser credential)');
|
|
561
579
|
}
|
|
580
|
+
// Apply the effective configuration to the running surfaces once per
|
|
581
|
+
// activation: the service tunables, the LAN-bind block/firewall, the tunnel
|
|
582
|
+
// plan, the pairing routes, the presence sweep, and the posture probe.
|
|
562
583
|
const sync = () => {
|
|
563
584
|
const value = resolve();
|
|
564
585
|
service.config = pairingConfigOf(value);
|
|
@@ -655,6 +676,10 @@ function applyImpl(ctx, config) {
|
|
|
655
676
|
: { kind: 'quick', targetUrl: plan.targetUrl, originHostHeader });
|
|
656
677
|
}
|
|
657
678
|
else if (plan.mode === 'named') {
|
|
679
|
+
// The hostname is fixed and known before the process runs, so publish it
|
|
680
|
+
// now: a named tunnel that fails to start must not leave the previous
|
|
681
|
+
// mode's ephemeral host as the QR and fence base.
|
|
682
|
+
publicBase.markRunning(plan.publicUrl);
|
|
658
683
|
tunnel.start({ kind: 'named', token: plan.token, publicUrl: plan.publicUrl });
|
|
659
684
|
}
|
|
660
685
|
else {
|
|
@@ -676,8 +701,17 @@ function applyImpl(ctx, config) {
|
|
|
676
701
|
}
|
|
677
702
|
}
|
|
678
703
|
const enabled = value.enabled;
|
|
679
|
-
if (!enabled)
|
|
704
|
+
if (!enabled) {
|
|
680
705
|
service.stop();
|
|
706
|
+
// The tunnel branch above follows the plan, not the master switch, so an
|
|
707
|
+
// explicit "off" must also drop the public ingress it started: otherwise
|
|
708
|
+
// the cloudflared child keeps a live public URL (and the relay keeps its
|
|
709
|
+
// stable row) while the panel says remote control is stopped. The relay
|
|
710
|
+
// row is deliberately kept so re-enabling reuses the same origin.
|
|
711
|
+
tunnel.stop();
|
|
712
|
+
disposeRelayRegistrar();
|
|
713
|
+
publicBase.reset();
|
|
714
|
+
}
|
|
681
715
|
if (disposeRoutes === undefined && enabled) {
|
|
682
716
|
disposeRoutes = ctx.effect(() => {
|
|
683
717
|
const disposers = [
|
|
@@ -726,27 +760,8 @@ function applyImpl(ctx, config) {
|
|
|
726
760
|
return;
|
|
727
761
|
table.push({ kind: 'script', placement: 'head', text: REMOTE_CHANNEL_BOOT_SCRIPT });
|
|
728
762
|
}), 'remote-web-ui: remote channel boot patch');
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
settingsCtx.settings.installSection(ctx, REMOTE_WEB_UI_SETTINGS_NAMESPACE, Config, config ?? {}, {
|
|
733
|
-
setSource: (source) => {
|
|
734
|
-
current = source;
|
|
735
|
-
sync();
|
|
736
|
-
},
|
|
737
|
-
onChange: sync,
|
|
738
|
-
});
|
|
739
|
-
}
|
|
740
|
-
else if (typeof settingsCtx.settings?.register === 'function') {
|
|
741
|
-
const scope = settingsCtx.settings.register(REMOTE_WEB_UI_SETTINGS_NAMESPACE, Config, { base: config ?? {} });
|
|
742
|
-
current = () => scope?.get?.() ?? (config ?? {});
|
|
743
|
-
scope?.watch?.(() => { sync(); });
|
|
744
|
-
sync();
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
catch {
|
|
748
|
-
// Defensive fallback against settings registration differences
|
|
749
|
-
}
|
|
750
|
-
});
|
|
763
|
+
// No settings registration: the Host derives this plugin's settings page
|
|
764
|
+
// from the exported `Config` schema and reloads the row after a save, so
|
|
765
|
+
// every write re-enters apply() with the new config (see `resolve`).
|
|
751
766
|
sync();
|
|
752
767
|
}
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* therefore redeems its own launch token (the same one-time exchange every
|
|
8
8
|
* paired device's first navigation performs) and attaches the resulting
|
|
9
9
|
* cookie to inner requests. The credential is only ever exercised behind the
|
|
10
|
-
* pairing gate:
|
|
11
|
-
*
|
|
10
|
+
* pairing gate: the `/remote` channel always requires a live paired-device
|
|
11
|
+
* credential before this module is consulted, regardless of the
|
|
12
|
+
* requirePairingForLan LAN policy (issue #1665).
|
|
12
13
|
*/
|
|
13
14
|
/** Browser-auth credential the proxy attaches to loopback-bound requests. */
|
|
14
15
|
export interface InnerAuth {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inner-auth.d.ts","sourceRoot":"","sources":["../../src/inner-auth.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"inner-auth.d.ts","sourceRoot":"","sources":["../../src/inner-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,6EAA6E;AAC7E,MAAM,WAAW,SAAS;IACxB;;;;OAIG;IACH,KAAK,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;IACpC,+DAA+D;IAC/D,UAAU,IAAI,IAAI,CAAA;CACnB;AAKD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,MAAM,GAAG,SAAS,EACnC,SAAS,GAAE,OAAO,KAAa,GAC9B,SAAS,CA8CX"}
|
package/lib/types/inner-auth.js
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* therefore redeems its own launch token (the same one-time exchange every
|
|
8
8
|
* paired device's first navigation performs) and attaches the resulting
|
|
9
9
|
* cookie to inner requests. The credential is only ever exercised behind the
|
|
10
|
-
* pairing gate:
|
|
11
|
-
*
|
|
10
|
+
* pairing gate: the `/remote` channel always requires a live paired-device
|
|
11
|
+
* credential before this module is consulted, regardless of the
|
|
12
|
+
* requirePairingForLan LAN policy (issue #1665).
|
|
12
13
|
*/
|
|
13
14
|
/** The harness browser-auth cookie name prefix (see dsh-client-connection). */
|
|
14
15
|
const BROWSER_AUTH_COOKIE_PREFIX = 'dsh-auth-';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-time pairing grants for the /pair-app landing.
|
|
3
|
+
*
|
|
4
|
+
* The QR entry (`/pair-accept?pair=<token>`) used to hand the freshly minted
|
|
5
|
+
* device id to the app landing in its own URL (`/pair-app?device=<id>`). A
|
|
6
|
+
* device id is a live session credential: the channel gate authorizes every
|
|
7
|
+
* gated request by it and the cookie it mirrors lives a year, so a URL carrying
|
|
8
|
+
* it leaks that credential into browser history, the address bar, and every log
|
|
9
|
+
* on the path. The landing is reached with a one-time grant instead — 256 bits
|
|
10
|
+
* of entropy, a short TTL, and single-consume semantics — and the device id
|
|
11
|
+
* reaches the client only inside the server-rendered shell (the capture script
|
|
12
|
+
* the landing patches in).
|
|
13
|
+
*
|
|
14
|
+
* Consume semantics follow the reference implementation's one-time host
|
|
15
|
+
* capability (zcode `packages/server/src/hostCapability.ts`): the record is
|
|
16
|
+
* deleted on EVERY consume attempt — success, expiry, or replay — so a captured
|
|
17
|
+
* grant is worthless after its first use and a replayed one can never resolve,
|
|
18
|
+
* and only a first use inside the TTL yields a device.
|
|
19
|
+
*
|
|
20
|
+
* @module @linxin666/dsh-remote-web-ui/pair-grant
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Default grant lifetime. The grant only has to survive one redirect chain
|
|
24
|
+
* from the accept page to the app landing, tunnel round trips included, so it
|
|
25
|
+
* is far shorter than the device session it stands in for.
|
|
26
|
+
*/
|
|
27
|
+
export declare const DEFAULT_PAIR_GRANT_TTL_MS = 60000;
|
|
28
|
+
/**
|
|
29
|
+
* Cap on live grants. The accept page is rate-limited per source, but the
|
|
30
|
+
* table is still caller-driven, so it stays bounded (oldest evicted first)
|
|
31
|
+
* exactly like the device table.
|
|
32
|
+
*/
|
|
33
|
+
export declare const MAX_LIVE_GRANTS = 256;
|
|
34
|
+
/** Clock/entropy injection for tests. */
|
|
35
|
+
export interface PairGrantStoreOptions {
|
|
36
|
+
/** Grant lifetime in ms (default {@link DEFAULT_PAIR_GRANT_TTL_MS}). */
|
|
37
|
+
ttlMs?: number;
|
|
38
|
+
/** Clock seam (default Date.now). */
|
|
39
|
+
now?: () => number;
|
|
40
|
+
/** Entropy seam (default 32 random bytes, base64url). */
|
|
41
|
+
createGrant?: () => string;
|
|
42
|
+
}
|
|
43
|
+
/** One issued grant and its absolute expiry (both non-secret to the issuer). */
|
|
44
|
+
export interface PairGrantRecord {
|
|
45
|
+
/** The one-time secret carried in the /pair-app URL. */
|
|
46
|
+
grant: string;
|
|
47
|
+
/** Absolute expiry (ms epoch); a consume past this resolves to nothing. */
|
|
48
|
+
expiresAt: number;
|
|
49
|
+
}
|
|
50
|
+
/** The grant table surface the routes consume. */
|
|
51
|
+
export interface PairGrantStore {
|
|
52
|
+
/** Mint one grant bound to a device session. */
|
|
53
|
+
issue(deviceId: string): PairGrantRecord;
|
|
54
|
+
/** Redeem a grant once, returning its device id (undefined when unusable). */
|
|
55
|
+
consume(grant: string | undefined): string | undefined;
|
|
56
|
+
/** Liveness check that does not spend the grant (the fence fallback). */
|
|
57
|
+
peek(grant: string | undefined): string | undefined;
|
|
58
|
+
/** Live grant count (tests/diagnostics). */
|
|
59
|
+
readonly size: number;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Build the grant table. Pure TypeScript with injected clock/randomness so the
|
|
63
|
+
* whole one-time semantics are unit-testable without a server.
|
|
64
|
+
*/
|
|
65
|
+
export declare function createPairGrantStore(options?: PairGrantStoreOptions): PairGrantStore;
|
|
66
|
+
//# sourceMappingURL=pair-grant.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pair-grant.d.ts","sourceRoot":"","sources":["../../src/pair-grant.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,QAAS,CAAA;AAE/C;;;;GAIG;AACH,eAAO,MAAM,eAAe,MAAM,CAAA;AAElC,yCAAyC;AACzC,MAAM,WAAW,qBAAqB;IACpC,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,qCAAqC;IACrC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,MAAM,CAAA;CAC3B;AAED,gFAAgF;AAChF,MAAM,WAAW,eAAe;IAC9B,wDAAwD;IACxD,KAAK,EAAE,MAAM,CAAA;IACb,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,kDAAkD;AAClD,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,CAAA;IACxC,8EAA8E;IAC9E,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAAA;IACtD,yEAAyE;IACzE,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAAA;IACnD,4CAA4C;IAC5C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAQD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,qBAA0B,GAAG,cAAc,CAoDxF"}
|