@linxin666/dsh-remote-web-ui 0.4.3 → 0.4.5
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 +23 -8
- package/README.zh.md +22 -7
- package/lib/client.js +194 -140
- package/lib/client.js.map +1 -1
- package/lib/index.js +231 -23
- package/lib/types/client/PluginSettingsCard.d.ts +7 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +2 -2
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +2 -1
- package/lib/types/client/index.js +3 -3
- package/lib/types/client/locales.d.ts +0 -4
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +5 -9
- package/lib/types/client/mobile-adapt.d.ts +2 -2
- package/lib/types/client/mobile-adapt.d.ts.map +1 -1
- package/lib/types/client/mobile-adapt.js +163 -136
- package/lib/types/client/settings-form.d.ts +16 -0
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +27 -1
- package/lib/types/detached-work.d.ts +45 -0
- package/lib/types/detached-work.d.ts.map +1 -0
- package/lib/types/detached-work.js +54 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +67 -12
- package/lib/types/lan-bind.d.ts +21 -4
- package/lib/types/lan-bind.d.ts.map +1 -1
- package/lib/types/lan-bind.js +103 -17
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +43 -0
- package/lib/types/remote-api.d.ts +13 -1
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-api.js +14 -1
- package/package.json +19 -19
- package/src/client/PluginSettingsCard.tsx +9 -0
- package/src/client/RemoteSettingsCard.tsx +2 -0
- package/src/client/index.ts +3 -3
- package/src/client/locales.ts +5 -9
- package/src/client/mobile-adapt.ts +167 -138
- package/src/client/settings-card.module.css +31 -5
- package/src/client/settings-form.ts +27 -1
- package/src/detached-work.ts +57 -0
- package/src/index.ts +67 -12
- package/src/lan-bind.ts +111 -17
- package/src/pairing.ts +44 -1
- package/src/remote-api.ts +14 -1
package/src/index.ts
CHANGED
|
@@ -39,6 +39,7 @@ import { withIdentityEncoding } from './http.ts'
|
|
|
39
39
|
import { TunnelManager, type TunnelInfo } from './tunnel.ts'
|
|
40
40
|
import { PublicBaseKeeper } from './public-base.ts'
|
|
41
41
|
import { mountOnce } from './mount-once.ts'
|
|
42
|
+
import { runDetached } from './detached-work.ts'
|
|
42
43
|
import { REMOTE_CHANNEL_BOOT_SCRIPT } from './remote-channel-boot.ts'
|
|
43
44
|
import { UUID_POLYFILL_SCRIPT } from './uuid-polyfill.ts'
|
|
44
45
|
|
|
@@ -736,18 +737,34 @@ function applyImpl(ctx: Context, config?: ResolvedConfigFields): void {
|
|
|
736
737
|
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)')
|
|
737
738
|
}
|
|
738
739
|
|
|
739
|
-
//
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
740
|
+
// LAN bind toggle: only write the managed patch block once the user has
|
|
741
|
+
// flipped it (undefined = untouched, never write). Desired host/port come
|
|
742
|
+
// from the CLI flags when given (flags win), else from the toggle and
|
|
743
|
+
// the currently bound port. The block takes effect on the next start, so
|
|
744
|
+
// the re-assert at every boot keeps it in sync with both the toggle and
|
|
745
|
+
// the flags.
|
|
746
|
+
//
|
|
747
|
+
// cordis.patch.yml is exactly the file the Host's HMR config watcher
|
|
748
|
+
// refreshes from, and a settings save runs inside the Host's exclusive HMR
|
|
749
|
+
// transaction. Writing that file while the caller's transaction is open makes
|
|
750
|
+
// the watcher's refresh re-enter it, which the Host refuses with "HMR
|
|
751
|
+
// transactions cannot be nested" and which surfaces on the user's save
|
|
752
|
+
// (#1751, #1754).
|
|
753
|
+
//
|
|
754
|
+
// Deferring alone does NOT escape that context, and the earlier belt rested
|
|
755
|
+
// on a premise that measurement disproves: AsyncLocalStorage is propagated
|
|
756
|
+
// into setImmediate, into node:timers, and into AsyncResource scopes (a
|
|
757
|
+
// plain resource built while the transaction runs inherits it), so
|
|
758
|
+
// "setImmediate starts a fresh store" is false. What does start clean is an
|
|
759
|
+
// AsyncResource created at module scope, before any transaction existed -
|
|
760
|
+
// see detached-work.ts. scheduleLanBindWork runs the scheduling through it,
|
|
761
|
+
// so the write lands outside the caller's transaction.
|
|
762
|
+
//
|
|
763
|
+
// The assertion is also idempotent on its own: the write is guarded by the
|
|
764
|
+
// current-vs-desired comparison below, so once the profile carries the block
|
|
765
|
+
// a coalesced follow-up sync() finds nothing to do. That guard is a second
|
|
766
|
+
// belt, not the fix.
|
|
767
|
+
const applyLanBindWork = (value: ResolvedConfig): void => {
|
|
751
768
|
if (value.lanBind !== undefined) {
|
|
752
769
|
const startup = ctx.get('webStartup') as StartupFacts | undefined
|
|
753
770
|
const desiredHost = desiredBindHost(value.lanBind === true, startup?.host)
|
|
@@ -785,6 +802,44 @@ function applyImpl(ctx: Context, config?: ResolvedConfigFields): void {
|
|
|
785
802
|
}
|
|
786
803
|
}
|
|
787
804
|
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// The LAN bind assertion never blocks the caller: one pending work item is
|
|
808
|
+
// enough, and it always re-reads the config through `resolve()` at run time
|
|
809
|
+
// so a toggle flipped twice before the tick lands on the last value.
|
|
810
|
+
//
|
|
811
|
+
// The scheduling itself runs through runDetached: the settings save that
|
|
812
|
+
// drives sync() holds the Host's exclusive HMR transaction in its async
|
|
813
|
+
// context, and a `setImmediate` scheduled directly from here would inherit
|
|
814
|
+
// that mark (measured on Node 24), so the write would still land inside the
|
|
815
|
+
// transaction the watcher then tries to re-enter. Scheduling from the
|
|
816
|
+
// module-scope resource starts the timer - and therefore the write - with no
|
|
817
|
+
// store at all.
|
|
818
|
+
let lanBindWorkPending = false
|
|
819
|
+
const scheduleLanBindWork = (): void => {
|
|
820
|
+
if (lanBindWorkPending) return
|
|
821
|
+
lanBindWorkPending = true
|
|
822
|
+
runDetached(() => setImmediate(() => {
|
|
823
|
+
lanBindWorkPending = false
|
|
824
|
+
try {
|
|
825
|
+
applyLanBindWork(resolve())
|
|
826
|
+
} catch (error) {
|
|
827
|
+
// A deferred assertion must never surface as an unhandled rejection:
|
|
828
|
+
// the settings save that triggered it already answered, and the card
|
|
829
|
+
// reads the live block state back on its own poll.
|
|
830
|
+
console.error(`remote-web-ui: the deferred lan-bind assertion failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
831
|
+
}
|
|
832
|
+
}))
|
|
833
|
+
}
|
|
834
|
+
ctx.effect(() => () => { lanBindWorkPending = false }, 'remote-web-ui: lan-bind work')
|
|
835
|
+
|
|
836
|
+
// Apply the effective configuration to the running surfaces once per
|
|
837
|
+
// activation: the service tunables, the LAN-bind block/firewall, the tunnel
|
|
838
|
+
// plan, the pairing routes, the presence sweep, and the posture probe.
|
|
839
|
+
const sync = (): void => {
|
|
840
|
+
const value = resolve()
|
|
841
|
+
service.config = pairingConfigOf(value)
|
|
842
|
+
scheduleLanBindWork()
|
|
788
843
|
// The plugin-managed tunnels own the public base while one runs: the URL
|
|
789
844
|
// lands in the service through the tunnel's phase listener (the minted
|
|
790
845
|
// quick URL, or the named tunnel's fixed public hostname). The manual
|
package/src/lan-bind.ts
CHANGED
|
@@ -57,17 +57,105 @@ function readPatchContent(file: string): string {
|
|
|
57
57
|
return readFileSync(file, 'utf8')
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
/** One top-level sequence item inside a managed-block region. */
|
|
61
|
+
interface BlockItem {
|
|
62
|
+
/** Offset of the item's first character inside the region. */
|
|
63
|
+
start: number
|
|
64
|
+
/** Offset just past the item's last character inside the region. */
|
|
65
|
+
end: number
|
|
66
|
+
/** The item's source text, line terminator included. */
|
|
67
|
+
text: string
|
|
63
68
|
}
|
|
64
69
|
|
|
65
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* Split a block-sequence region into its top-level items. Only a column-0 `-`
|
|
72
|
+
* starts an item: that is the shape every writer of this file produces - the
|
|
73
|
+
* managed block itself and the YAML document API's block-style re-emission.
|
|
74
|
+
* @param region - text between two line boundaries.
|
|
75
|
+
* @returns the items in source order; text outside them is not returned.
|
|
76
|
+
*/
|
|
77
|
+
function blockItems(region: string): BlockItem[] {
|
|
78
|
+
const items: BlockItem[] = []
|
|
79
|
+
let start = -1
|
|
80
|
+
let offset = 0
|
|
81
|
+
for (const line of region.split('\n')) {
|
|
82
|
+
if (line.startsWith('- ') || line === '-') {
|
|
83
|
+
if (start !== -1) items.push({ start, end: offset, text: region.slice(start, offset) })
|
|
84
|
+
start = offset
|
|
85
|
+
}
|
|
86
|
+
offset += line.length + 1
|
|
87
|
+
}
|
|
88
|
+
if (start !== -1) items.push({ start, end: region.length, text: region.slice(start) })
|
|
89
|
+
return items
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Whether one top-level item is the webserver row this plugin's block owns.
|
|
94
|
+
* The row id is the whole signal: the markers declare everything inside them
|
|
95
|
+
* managed, our block always writes `- id: webserver`, and a same-id row later
|
|
96
|
+
* in the patch replaces this one wholesale anyway - so an id-webserver row
|
|
97
|
+
* found inside our markers is this plugin's row (a hand-truncated block may
|
|
98
|
+
* have lost its `name` line).
|
|
99
|
+
* @param item - one top-level item's text.
|
|
100
|
+
* @returns true when the item is the managed webserver row.
|
|
101
|
+
*/
|
|
102
|
+
function isManagedWebserverItem(item: string): boolean {
|
|
103
|
+
return /^- id:\s*['"]?webserver['"]?[ \t]*(?:\r?\n|$)/.test(item)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The text between a block's BEGIN and END markers: from the character after
|
|
108
|
+
* the BEGIN line's terminator to the start of the END marker's line (or to
|
|
109
|
+
* the end of the content without an END marker).
|
|
110
|
+
* @param content - the patch file text.
|
|
111
|
+
* @param begin - index of the BEGIN marker.
|
|
112
|
+
* @param end - index of the END marker, or -1 when the block is unterminated.
|
|
113
|
+
* @returns the region's text.
|
|
114
|
+
*/
|
|
115
|
+
function managedRegion(content: string, begin: number, end: number): string {
|
|
116
|
+
const newlineAfterBegin = content.indexOf('\n', begin)
|
|
117
|
+
const innerStart = newlineAfterBegin === -1 ? content.length : newlineAfterBegin + 1
|
|
118
|
+
const endLineStart = end === -1 ? content.length : content.lastIndexOf('\n', end - 1) + 1
|
|
119
|
+
return content.slice(innerStart, Math.max(innerStart, endLineStart))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Remove the managed block (its markers and the webserver row this plugin
|
|
124
|
+
* wrote) from patch content, keeping every other row.
|
|
125
|
+
*
|
|
126
|
+
* The markers travel with the file through writers that append rows through
|
|
127
|
+
* the YAML document API: the appended rows land at the end of the sequence,
|
|
128
|
+
* which is BEFORE a trailing END comment, so the region between the markers
|
|
129
|
+
* can hold rows this plugin never wrote. Deleting the whole region silently
|
|
130
|
+
* removed the user's model, plugin and remote-control rows on the next rewrite
|
|
131
|
+
* - the remote-control row carries the LAN toggle itself, so the feature reset
|
|
132
|
+
* to `undefined` and the phone was locked out again after a restart (DSH
|
|
133
|
+
* Desktop report, 2026-09-30). Only the managed item is removed here; every
|
|
134
|
+
* foreign row is kept in place and the fresh block is appended after them (the
|
|
135
|
+
* same-id row last wins).
|
|
136
|
+
* @param content - the patch file text.
|
|
137
|
+
* @returns the text with the markers and the managed row removed.
|
|
138
|
+
*/
|
|
66
139
|
export function stripManagedBlock(content: string): string {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
140
|
+
let text = content
|
|
141
|
+
for (;;) {
|
|
142
|
+
const begin = text.indexOf(LAN_BIND_BLOCK_BEGIN)
|
|
143
|
+
if (begin === -1) return text
|
|
144
|
+
const end = text.indexOf(LAN_BIND_BLOCK_END, begin)
|
|
145
|
+
const blockStart = text.lastIndexOf('\n', begin - 1) + 1
|
|
146
|
+
const newlineAfterEnd = end === -1 ? -1 : text.indexOf('\n', end)
|
|
147
|
+
const resumeAt = end === -1 ? text.length : (newlineAfterEnd === -1 ? text.length : newlineAfterEnd + 1)
|
|
148
|
+
const region = managedRegion(text, begin, end)
|
|
149
|
+
let kept = ''
|
|
150
|
+
let cursor = 0
|
|
151
|
+
for (const item of blockItems(region)) {
|
|
152
|
+
if (!isManagedWebserverItem(item.text)) continue
|
|
153
|
+
kept += region.slice(cursor, item.start)
|
|
154
|
+
cursor = item.end
|
|
155
|
+
}
|
|
156
|
+
kept += region.slice(cursor)
|
|
157
|
+
text = text.slice(0, blockStart) + kept + text.slice(resumeAt)
|
|
158
|
+
}
|
|
71
159
|
}
|
|
72
160
|
|
|
73
161
|
/**
|
|
@@ -163,9 +251,16 @@ export function managedBindOf(content: string): ManagedBindState | undefined {
|
|
|
163
251
|
const begin = content.indexOf(LAN_BIND_BLOCK_BEGIN)
|
|
164
252
|
if (begin === -1) return undefined
|
|
165
253
|
const end = content.indexOf(LAN_BIND_BLOCK_END, begin)
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
254
|
+
const region = managedRegion(content, begin, end)
|
|
255
|
+
// Read the bind out of the managed row itself. A region a YAML writer
|
|
256
|
+
// displaced holds foreign rows too, and their `host`/`port` keys are not
|
|
257
|
+
// this block's bind - reading them made the boot re-assert
|
|
258
|
+
// believe the block was already applied. A region without a recognizable
|
|
259
|
+
// managed row (hand-edited literals) keeps the historical whole-region read.
|
|
260
|
+
const items = blockItems(region).filter(item => isManagedWebserverItem(item.text))
|
|
261
|
+
const scope = items.length > 0 ? items.map(item => item.text).join('') : region
|
|
262
|
+
const hostMatch = /host:\s*'([^']+)'/.exec(scope)
|
|
263
|
+
const portMatch = /port:\s*(\d+)/.exec(scope)
|
|
169
264
|
return {
|
|
170
265
|
host: hostMatch?.[1] ?? '',
|
|
171
266
|
port: portMatch !== null ? Number(portMatch[1]) : undefined,
|
|
@@ -184,9 +279,10 @@ export function lanBindState(profile: string, home: string = dshHome()): { block
|
|
|
184
279
|
* patch file is preserved; the block is rewritten atomically (unique temp
|
|
185
280
|
* file + rename, so concurrent writers can never rename a half-written
|
|
186
281
|
* file) with the original file's permissions preserved. A hand-truncated
|
|
187
|
-
* unterminated block (BEGIN marker without END
|
|
188
|
-
*
|
|
189
|
-
* never stack a second webserver row onto the
|
|
282
|
+
* unterminated block (BEGIN marker without END) is consumed by
|
|
283
|
+
* stripManagedBlock as well - markers and managed row removed, foreign rows
|
|
284
|
+
* kept - so the rewrite can never stack a second webserver row onto the
|
|
285
|
+
* orphan.
|
|
190
286
|
*
|
|
191
287
|
* The written file is always exactly one valid YAML document: a base the
|
|
192
288
|
* plugin manager left in flow style is re-emitted as a block sequence before
|
|
@@ -194,9 +290,7 @@ export function lanBindState(profile: string, home: string = dshHome()): { block
|
|
|
194
290
|
*/
|
|
195
291
|
export function writeLanBind(host: LanBindHost, port: number, profile: string, home: string = dshHome()): void {
|
|
196
292
|
const file = profilePatchFile(profile, home)
|
|
197
|
-
const
|
|
198
|
-
const orphanBegin = stripped.indexOf(LAN_BIND_BLOCK_BEGIN)
|
|
199
|
-
const rawBase = (orphanBegin === -1 ? stripped : stripped.slice(0, orphanBegin)).trimEnd()
|
|
293
|
+
const rawBase = stripManagedBlock(readPatchContent(file)).trimEnd()
|
|
200
294
|
const base = rawBase.replace(/\[\s*\]\s*$/, '').trimEnd()
|
|
201
295
|
const block = managedBlock(host, port)
|
|
202
296
|
const content = mergePatchContent(base, block, file)
|
package/src/pairing.ts
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
import { randomBytes } from 'node:crypto'
|
|
25
25
|
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
26
26
|
import { dirname } from 'node:path'
|
|
27
|
-
import type { PostureSnapshot } from './posture.ts'
|
|
27
|
+
import type { PostureHost, PostureSnapshot } from './posture.ts'
|
|
28
28
|
|
|
29
29
|
/** The observable pairing phases the panel renders. */
|
|
30
30
|
export type PairingPhase =
|
|
@@ -663,6 +663,7 @@ function snapshotsEqual(a: PairingSnapshot, b: PairingSnapshot): boolean {
|
|
|
663
663
|
&& a.publicUrl === b.publicUrl
|
|
664
664
|
&& tunnelEqual(a.tunnel, b.tunnel)
|
|
665
665
|
&& relayEqual(a.relay, b.relay)
|
|
666
|
+
&& postureEqual(a.posture, b.posture)
|
|
666
667
|
&& a.tokenId === b.tokenId
|
|
667
668
|
&& a.tokenExpiresAt === b.tokenExpiresAt
|
|
668
669
|
&& a.deviceCount === b.deviceCount
|
|
@@ -695,6 +696,48 @@ function relayEqual(a: RelayStatus | undefined, b: RelayStatus | undefined): boo
|
|
|
695
696
|
&& a.state === b.state && a.url === b.url && a.error === b.error)
|
|
696
697
|
}
|
|
697
698
|
|
|
699
|
+
/**
|
|
700
|
+
* Posture equality. A probe round can change this frame while every other field
|
|
701
|
+
* stays put — the common case, since the probe re-runs on a settings change and
|
|
702
|
+
* on a tunnel reaching running, and a quick-tunnel restart churns the probed
|
|
703
|
+
* host set. Leaving the frame out of the comparison suppressed those emits, so
|
|
704
|
+
* the loopback SSE stream and the desktop panel could sit on a stale "exposed"
|
|
705
|
+
* (or stale "clean") verdict for as long as the phase and roster stayed put.
|
|
706
|
+
*/
|
|
707
|
+
function postureEqual(a: PostureSnapshot | undefined, b: PostureSnapshot | undefined): boolean {
|
|
708
|
+
if (a === b) return true
|
|
709
|
+
// A frame that is not an object at all (a hand-built context can pass
|
|
710
|
+
// anything) counts as changed instead of throwing out of the setter.
|
|
711
|
+
if (!isPostureFrame(a) || !isPostureFrame(b)) return false
|
|
712
|
+
return a.checkedAt === b.checkedAt && hostsEqual(a.hosts, b.hosts)
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/** Whether a value has the shape a posture frame is compared on. */
|
|
716
|
+
function isPostureFrame(value: unknown): value is PostureSnapshot {
|
|
717
|
+
return typeof value === 'object' && value !== null
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Per-host posture equality (a round's verdicts, in probe order). Tolerates a
|
|
722
|
+
* frame that is not the expected shape: this runs inside the emit path, outside
|
|
723
|
+
* the listener error containment, so a malformed snapshot handed in by another
|
|
724
|
+
* host-fiber caller must degrade to "changed" instead of throwing out of the
|
|
725
|
+
* setter that produced it.
|
|
726
|
+
*/
|
|
727
|
+
function hostsEqual(a: readonly PostureHost[] | undefined, b: readonly PostureHost[] | undefined): boolean {
|
|
728
|
+
if (a === b) return true
|
|
729
|
+
if (!Array.isArray(a) || !Array.isArray(b)) return false
|
|
730
|
+
return a.length === b.length && a.every((host, index) => {
|
|
731
|
+
const other = b[index]
|
|
732
|
+
return isPostureHost(host) && isPostureHost(other) && host.host === other.host && host.exposed === other.exposed
|
|
733
|
+
})
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/** Whether a value has the shape one probed host is compared on. */
|
|
737
|
+
function isPostureHost(value: unknown): value is PostureHost {
|
|
738
|
+
return typeof value === 'object' && value !== null
|
|
739
|
+
}
|
|
740
|
+
|
|
698
741
|
/** Element-wise string list equality (interface order is meaningful). */
|
|
699
742
|
function sameStrings(a: readonly string[], b: readonly string[]): boolean {
|
|
700
743
|
return a.length === b.length && a.every((value, index) => value === b[index])
|
package/src/remote-api.ts
CHANGED
|
@@ -182,7 +182,19 @@ export function makeRemoteApiRoutes(deps: RemoteApiDeps): WebRoute[] {
|
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
/**
|
|
185
|
-
* Map one outer upgrade URL onto the loopback path
|
|
185
|
+
* Map one outer upgrade URL onto the loopback path, dropping the cookieless
|
|
186
|
+
* device credential on the way in.
|
|
187
|
+
*
|
|
188
|
+
* The outer handshake carries the device id as `?device=<id>` because WebSocket
|
|
189
|
+
* handshakes cannot carry headers from the Web API. That id is a live session
|
|
190
|
+
* credential — it opens the full host API through this same channel — and the
|
|
191
|
+
* inner leg is a request the rest of the loopback origin sees (on a WebSocket
|
|
192
|
+
* leg, a raw handshake request line written to a loopback socket), so forwarding
|
|
193
|
+
* it handed a working credential to every other observer of the local port for
|
|
194
|
+
* no benefit: the gate above has already authenticated the caller, and no
|
|
195
|
+
* upstream route reads the parameter (the gateway mux matches on path alone).
|
|
196
|
+
* The rest of the query rides through unchanged, so a caller's own parameters
|
|
197
|
+
* still reach the upstream.
|
|
186
198
|
*/
|
|
187
199
|
export function upgradeInnerPath(reqUrl: string | undefined, fallbackPath: string): string {
|
|
188
200
|
if (reqUrl === undefined || reqUrl === '') return fallbackPath
|
|
@@ -194,6 +206,7 @@ export function upgradeInnerPath(reqUrl: string | undefined, fallbackPath: strin
|
|
|
194
206
|
}
|
|
195
207
|
const inner = innerPathOf(url.pathname)
|
|
196
208
|
if (inner === undefined) return fallbackPath
|
|
209
|
+
url.searchParams.delete(REMOTE_DEVICE_QUERY)
|
|
197
210
|
return `${inner}${url.search}`
|
|
198
211
|
}
|
|
199
212
|
|