dsh-plugin-mobile-link 0.3.1 → 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.md +29 -3
- package/cordis.patch.yml +0 -1
- package/lib/cli.js +7 -3
- package/lib/dlp.js +21 -1
- package/lib/dsh-client.js +162 -61
- package/lib/files-out.js +425 -0
- package/lib/files.js +43 -14
- package/lib/handoff.js +53 -0
- package/lib/hello.js +8 -0
- package/lib/index.js +65 -11
- package/lib/link.js +90 -55
- package/lib/pairing.js +1 -1
- package/lib/router.js +190 -32
- package/lib/routes.js +1 -1
- package/lib/state.js +5 -14
- package/lib/status.js +101 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,11 +38,32 @@ plugins/mobile-link/
|
|
|
38
38
|
|
|
39
39
|
## Install
|
|
40
40
|
|
|
41
|
+
The connector runs in whichever host runs your DSH. There are two supported hosts today —
|
|
42
|
+
**the official desktop app** and **the command-line web host** — and each keeps its own
|
|
43
|
+
profile, so you install the plugin into the one you actually use. The connector discovers
|
|
44
|
+
the local DSH by itself in both (the host hands it the authenticated loopback address);
|
|
45
|
+
you never type an address or a token.
|
|
46
|
+
|
|
47
|
+
**Command-line web host**
|
|
48
|
+
|
|
41
49
|
```bash
|
|
42
50
|
dsh plugin --profile web add dsh-plugin-mobile-link
|
|
43
51
|
```
|
|
44
52
|
|
|
45
|
-
|
|
53
|
+
**Official desktop app** — install through the app's own plugin page, or with the CLI that
|
|
54
|
+
the app ships (`…/Resources/runtime/cli/bin/dsh`, installable from its settings). The npm
|
|
55
|
+
`dsh` refuses this profile on purpose (`profile "desktop" is managed exclusively by the
|
|
56
|
+
Electron application`). Two prerequisites matter in practice: let the app start once so it
|
|
57
|
+
initializes its profile, and **quit it completely** before installing — otherwise the
|
|
58
|
+
profile is locked.
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# quoting the app's bundled CLI; see docs/ONBOARDING.md for the section titles
|
|
62
|
+
"<DeepSeek Harness.app>/Contents/Resources/runtime/cli/bin/dsh" \
|
|
63
|
+
plugin --profile desktop add dsh-plugin-mobile-link
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Then restart the host (or reload the profile). From a checkout the same command takes
|
|
46
67
|
a path — the leading `./` matters, because DSH anchors a relative spec against
|
|
47
68
|
your current directory instead of resolving it inside the profile:
|
|
48
69
|
|
|
@@ -86,10 +107,15 @@ plugin's config block (`cordis.patch.yml`), not by editing this file.
|
|
|
86
107
|
| `relayUrl` | from `agent.json` | Relay base URL; a bare deployment dials `<relay>/link/agent` |
|
|
87
108
|
| `agentId` / `agentSecret` | from `agent.json` | Override the enrolled identity |
|
|
88
109
|
| `stateFile` | `~/.dsh/mobile-link/agent.json` | Where the identity lives |
|
|
89
|
-
| `dshUrl` | discovery | Explicit local DSH base URL |
|
|
90
|
-
| `endpointFile` | `$DSH_HOME/desktop-shell/endpoint.json` | DSH endpoint handoff file |
|
|
110
|
+
| `dshUrl` | discovery | Explicit local DSH base URL (usually unnecessary — the host hands the connector its own address) |
|
|
91
111
|
| `heartbeatMs` | `20000` | DLP ping interval |
|
|
92
112
|
|
|
113
|
+
The connector does **not** read an endpoint handoff file any more: discovery happens inside
|
|
114
|
+
the host process (`ctx.inject(['webServer','connection'])`), which is what makes one npm
|
|
115
|
+
package work on both the desktop app and the command-line host. It **writes** one instead —
|
|
116
|
+
`$DSH_HOME/mobile-link/endpoint.json`, mode 0600 — for tools that run *outside* the host
|
|
117
|
+
(our dev/test scripts and the iOS integration helpers).
|
|
118
|
+
|
|
93
119
|
Address resolution order: this config block, then `DSH_RELAY_URL` (or
|
|
94
120
|
`DSH_MOBILE_LINK_RELAY`), then the enrolled identity file, then the repository
|
|
95
121
|
placeholder `wss://relay.example.com/dsh-link` — the placeholder being the
|
package/cordis.patch.yml
CHANGED
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
# agentSecret overrides ~/.dsh/mobile-link/agent.json
|
|
22
22
|
# stateFile agent identity file (default ~/.dsh/mobile-link/agent.json)
|
|
23
23
|
# dshUrl explicit local DSH base URL override (default: discovery)
|
|
24
|
-
# endpointFile DSH endpoint handoff file (default $DSH_HOME/desktop-shell/endpoint.json)
|
|
25
24
|
#
|
|
26
25
|
# Address resolution order: this config block, then DSH_RELAY_URL (or
|
|
27
26
|
# DSH_MOBILE_LINK_RELAY), then the enrolled identity file, then the repository
|
package/lib/cli.js
CHANGED
|
@@ -66,9 +66,14 @@ async function main() {
|
|
|
66
66
|
The invite may also arrive as ${INVITE_ENV}. Exits non-zero on failure.
|
|
67
67
|
|
|
68
68
|
cli.js --relay <ws url> --agent-id <id> --agent-secret <secret>
|
|
69
|
-
[--state-file <path>] [--dsh-url <url>]
|
|
69
|
+
[--state-file <path>] [--dsh-url <url>]
|
|
70
70
|
[--status-file <path>] [--heartbeat-ms <n>] [--log-level debug|info|warn]
|
|
71
71
|
|
|
72
|
+
Note: run this inside a DSH host (plugin) when you can. A standalone process has no
|
|
73
|
+
in-process context, so on the official desktop app it cannot learn the local address by
|
|
74
|
+
itself — pass one explicitly with --dsh-url (a tokenized URL only stays valid until the
|
|
75
|
+
host restarts). Supported hosts: the official desktop app and the command-line web host.
|
|
76
|
+
|
|
72
77
|
cli.js --mint-pair-code --relay <ws url> --agent-id <id> --agent-secret <secret>
|
|
73
78
|
[--ttl-ms <n>] print one pairing code as JSON and exit
|
|
74
79
|
`)
|
|
@@ -106,7 +111,7 @@ async function main() {
|
|
|
106
111
|
process.stdout.write('\n' + qrTerminal(minted.qrPayload) + '\n')
|
|
107
112
|
process.stdout.write(`配对码 ${minted.code}(约 ${minutes} 分钟有效,一次性)\n`)
|
|
108
113
|
process.stdout.write('也可以在这台电脑上打开 http://127.0.0.1:<DSH 端口>/mobile-link/qr 取新码;\n'
|
|
109
|
-
+ '端口在 DSH
|
|
114
|
+
+ '端口在 DSH 启动时打印;连接器还会把带令牌地址写进 ~/.dsh/mobile-link/endpoint.json。\n')
|
|
110
115
|
} catch (error) {
|
|
111
116
|
process.stdout.write('(这次没能预先申请配对码:' + (error?.message ?? error) + ')\n'
|
|
112
117
|
+ '重启 DSH 后,在这台电脑的浏览器里打开 http://127.0.0.1:<DSH 端口>/mobile-link/qr,'
|
|
@@ -148,7 +153,6 @@ async function main() {
|
|
|
148
153
|
agentSecret: args.agentSecret,
|
|
149
154
|
stateFile: typeof args.stateFile === 'string' ? args.stateFile : undefined,
|
|
150
155
|
dshUrl: typeof args.dshUrl === 'string' ? args.dshUrl : undefined,
|
|
151
|
-
endpointFile: typeof args.endpointFile === 'string' ? args.endpointFile : undefined,
|
|
152
156
|
heartbeatMs: Number(args.heartbeatMs) || 20000,
|
|
153
157
|
logger,
|
|
154
158
|
})
|
package/lib/dlp.js
CHANGED
|
@@ -11,8 +11,28 @@ export const MAX_FRAME_BYTES = 32 * 1024 * 1024
|
|
|
11
11
|
|
|
12
12
|
export const DEVICE_TO_AGENT = new Set(['req', 'open', 'cancel', 'eventResult', 'ping', 'hello'])
|
|
13
13
|
export const AGENT_TO_DEVICE = new Set(['res', 'item', 'end', 'streamError', 'event', 'hostStatus', 'pong', 'error'])
|
|
14
|
-
/** Relay-internal control frames carrying device lifecycle (see
|
|
14
|
+
/** Relay-internal control frames carrying device lifecycle (see docs/RELAY-NOTES.md). */
|
|
15
15
|
export const RELAY_CONTROL = new Set(['deviceAttach', 'deviceDetach'])
|
|
16
|
+
/**
|
|
17
|
+
* Agent-side control frames: sent by the connector to the relay and **never
|
|
18
|
+
* forwarded to a device** (they are not part of DLP — a phone neither sends nor
|
|
19
|
+
* receives them). `notify` is the "a run finished / something needs you" ping the
|
|
20
|
+
* relay turns into an APNs push when the device is offline (R-1 C-02/C-06).
|
|
21
|
+
*/
|
|
22
|
+
export const AGENT_CONTROL = new Set([
|
|
23
|
+
'notify',
|
|
24
|
+
// 后台大文件上传(`PUT /files/up`)与下载(`GET /files/down`)的桥接帧。
|
|
25
|
+
'fsPutBegin', 'fsPutChunk', 'fsPutEnd', 'fsPutAck', 'fsPutDone',
|
|
26
|
+
'fsGetBegin', 'fsGetChunk', 'fsGetEnd', 'fsGetAck',
|
|
27
|
+
'fsErr',
|
|
28
|
+
])
|
|
29
|
+
/**
|
|
30
|
+
* Control frames the relay sends **to** the connector. Same family as
|
|
31
|
+
* `AGENT_CONTROL` but the opposite direction, which is why it is its own set:
|
|
32
|
+
* that one is what this end *produces*, and a router that switched on the union
|
|
33
|
+
* of the two would dispatch its own outgoing frames if they ever came back.
|
|
34
|
+
*/
|
|
35
|
+
export const RELAY_TO_AGENT_CONTROL = new Set(['fsGetCancel'])
|
|
16
36
|
|
|
17
37
|
const ID_FRAMES = new Set(['req', 'open', 'cancel', 'res', 'item', 'end', 'streamError', 'eventResult'])
|
|
18
38
|
|
package/lib/dsh-client.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Client for the *local* DSH instance.
|
|
3
3
|
*
|
|
4
4
|
* Responsibilities (spec §4.1):
|
|
5
|
-
* 1. Discover the endpoint:
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* 1. Discover the endpoint as an ordered candidate list: explicit config, then the
|
|
6
|
+
* host's own in-process resolver, then `DSH_WEB_URL`, then `http://127.0.0.1:54499`.
|
|
7
|
+
* The authenticated value changes on every host restart, so it is re-resolved on
|
|
8
|
+
* every (re)connect.
|
|
8
9
|
* 2. Exchange `?token=` for a `dsh-auth-*` cookie without following the 303.
|
|
9
10
|
* The cookie is bound to the authority, so the client always dials
|
|
10
11
|
* `127.0.0.1:<port>` and never rewrites `Host` (see NOTES.md on spec §7).
|
|
@@ -13,9 +14,7 @@
|
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
import { EventEmitter } from 'node:events'
|
|
16
|
-
import { readFile } from 'node:fs/promises'
|
|
17
17
|
|
|
18
|
-
import { defaultEndpointFile, defaultShellLogFile } from './state.js'
|
|
19
18
|
import { connect as wsConnect } from './ws.js'
|
|
20
19
|
|
|
21
20
|
export class DshUnavailable extends Error {}
|
|
@@ -42,56 +41,123 @@ function parseBase(raw, { forceLoopback = true } = {}) {
|
|
|
42
41
|
}
|
|
43
42
|
|
|
44
43
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
44
|
+
* Whether this answer is the binary form of an RPC result.
|
|
45
|
+
*
|
|
46
|
+
* A result that carries bytes **cannot** be JSON, so a Host that supports them
|
|
47
|
+
* answers `multipart/form-data` instead. A client that only reads JSON sees an
|
|
48
|
+
* unparsable body and reports `bad-response` — which is exactly how a Host
|
|
49
|
+
* generation that started doing this broke every file read at once.
|
|
47
50
|
*/
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
function isBinaryAnswer(response) {
|
|
52
|
+
const type = response.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase()
|
|
53
|
+
return type === 'multipart/form-data'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Decode that form into the envelope shape the JSON path also produces.
|
|
58
|
+
*
|
|
59
|
+
* Field for field the same contract the Host's own client implements: a
|
|
60
|
+
* `metadata` part holds the envelope — with `null` wherever the bytes were —
|
|
61
|
+
* plus one attachment per binary field, naming the part that carries it and the
|
|
62
|
+
* path to walk from `result.value` to that `null`.
|
|
63
|
+
*/
|
|
64
|
+
async function decodeBinaryAnswer(response) {
|
|
65
|
+
const body = await response.formData()
|
|
66
|
+
const metadata = body.get('metadata')
|
|
67
|
+
if (typeof metadata !== 'string') throw new TypeError('binary answer without metadata')
|
|
68
|
+
const envelope = JSON.parse(metadata)
|
|
69
|
+
const root = { value: envelope?.result?.value }
|
|
70
|
+
for (const attachment of envelope?.attachments ?? []) {
|
|
71
|
+
const part = attachment?.part
|
|
72
|
+
const bytes = typeof part === 'string' ? body.get(part) : null
|
|
73
|
+
if (!bytes || typeof bytes.arrayBuffer !== 'function') {
|
|
74
|
+
throw new TypeError('binary answer with an unreadable part')
|
|
75
|
+
}
|
|
76
|
+
let parent = root
|
|
77
|
+
let key = 'value'
|
|
78
|
+
for (const segment of attachment?.path ?? []) {
|
|
79
|
+
const value = parent[key]
|
|
80
|
+
if (typeof value !== 'object' || value === null) {
|
|
81
|
+
throw new TypeError('binary answer with an unreachable path')
|
|
82
|
+
}
|
|
83
|
+
parent = value
|
|
84
|
+
key = segment
|
|
85
|
+
}
|
|
86
|
+
parent[key] = new Uint8Array(await bytes.arrayBuffer())
|
|
87
|
+
}
|
|
88
|
+
return envelope
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Resolve **all** ways this process might reach the local DSH, in the order they
|
|
93
|
+
* must be tried (contract: `contracts/endpoint-discovery.md` §1).
|
|
94
|
+
*
|
|
95
|
+
* Pure and injectable on purpose: it only *produces candidates*. Authentication
|
|
96
|
+
* happens in `DshClient`, because "this candidate works" must be decided by an
|
|
97
|
+
* actual token → cookie exchange, not by the shape of the value.
|
|
98
|
+
*
|
|
99
|
+
* @param {object} [options]
|
|
100
|
+
* @param {string} [options.explicitUrl] user-configured address (may carry a token)
|
|
101
|
+
* @param {() => string | undefined} [options.hostService] in-process resolver: the host
|
|
102
|
+
* process hands us its current authenticated loopback URL (desktop + web hosts)
|
|
103
|
+
* @param {NodeJS.ProcessEnv} [options.env]
|
|
104
|
+
* @returns {Array<{base: string, wsBase: string, port: number, token: string, source: string}>}
|
|
105
|
+
*/
|
|
106
|
+
export function discoverCandidates({ explicitUrl, hostService, env = process.env } = {}) {
|
|
107
|
+
const candidates = []
|
|
53
108
|
if (explicitUrl) {
|
|
54
109
|
const parsed = parseBase(explicitUrl, { forceLoopback: false })
|
|
55
110
|
parsed.source = 'config'
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
try {
|
|
59
|
-
const raw = JSON.parse(await readFile(endpointFile, 'utf8'))
|
|
60
|
-
const url = typeof raw?.url === 'string' && raw.url ? raw.url : `http://127.0.0.1:${raw?.port ?? FALLBACK_PORT}`
|
|
61
|
-
const parsed = parseBase(url)
|
|
62
|
-
if (!parsed.port) parsed.port = Number(raw?.port) || FALLBACK_PORT
|
|
63
|
-
parsed.base = `http://127.0.0.1:${parsed.port}`
|
|
64
|
-
parsed.wsBase = `ws://127.0.0.1:${parsed.port}`
|
|
65
|
-
parsed.token = parsed.token || url.searchParams?.get?.('token') || ''
|
|
66
|
-
parsed.source = `endpoint.json:${endpointFile}`
|
|
67
|
-
return parsed
|
|
68
|
-
} catch {
|
|
69
|
-
/* fall through to the log, the environment and the built-in default */
|
|
111
|
+
candidates.push(parsed)
|
|
70
112
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
parsed.source = `dsh-shell.log:${shellLogFile}`
|
|
82
|
-
return parsed
|
|
113
|
+
if (typeof hostService === 'function') {
|
|
114
|
+
try {
|
|
115
|
+
const raw = hostService()
|
|
116
|
+
if (typeof raw === 'string' && raw !== '') {
|
|
117
|
+
const parsed = parseBase(raw)
|
|
118
|
+
parsed.source = 'host-service'
|
|
119
|
+
candidates.push(parsed)
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
/* the service is not usable in this host; fall through to the older sources */
|
|
83
123
|
}
|
|
84
|
-
} catch {
|
|
85
|
-
/* fall through to the environment and the built-in default */
|
|
86
124
|
}
|
|
87
|
-
if (
|
|
88
|
-
const parsed = parseBase(
|
|
125
|
+
if (env.DSH_WEB_URL) {
|
|
126
|
+
const parsed = parseBase(env.DSH_WEB_URL)
|
|
89
127
|
parsed.source = 'DSH_WEB_URL'
|
|
90
|
-
|
|
128
|
+
candidates.push(parsed)
|
|
129
|
+
}
|
|
130
|
+
const fallback = parseBase(`http://127.0.0.1:${FALLBACK_PORT}`)
|
|
131
|
+
fallback.source = 'default'
|
|
132
|
+
candidates.push(fallback)
|
|
133
|
+
return candidates
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The first candidate, for callers that only need one address.
|
|
138
|
+
* @returns {Promise<{base: string, wsBase: string, port: number, token: string, source: string}>}
|
|
139
|
+
*/
|
|
140
|
+
export async function discoverEndpoint(options = {}) {
|
|
141
|
+
return discoverCandidates(options)[0]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Classify why discovery/authentication failed, for the status payload and the
|
|
146
|
+
* user-facing hint table (`contracts/status-endpoint.md` §3).
|
|
147
|
+
*
|
|
148
|
+
* The rule is "the candidate that got furthest": one that carried a token and
|
|
149
|
+
* still failed says more about the situation than the bare fallback port.
|
|
150
|
+
*
|
|
151
|
+
* @param {{source?: string} | undefined} endpoint
|
|
152
|
+
* @returns {'config' | 'host-service' | 'DSH_WEB_URL' | 'unreachable'}
|
|
153
|
+
*/
|
|
154
|
+
function kindFor(endpoint) {
|
|
155
|
+
switch (endpoint?.source) {
|
|
156
|
+
case 'config': return 'config'
|
|
157
|
+
case 'host-service': return 'host-service'
|
|
158
|
+
case 'DSH_WEB_URL': return 'DSH_WEB_URL'
|
|
159
|
+
default: return 'unreachable'
|
|
91
160
|
}
|
|
92
|
-
const parsed = parseBase(`http://127.0.0.1:${FALLBACK_PORT}`)
|
|
93
|
-
parsed.source = 'default'
|
|
94
|
-
return parsed
|
|
95
161
|
}
|
|
96
162
|
|
|
97
163
|
function cookieFrom(response) {
|
|
@@ -105,10 +171,11 @@ function cookieFrom(response) {
|
|
|
105
171
|
}
|
|
106
172
|
|
|
107
173
|
export class DshClient extends EventEmitter {
|
|
108
|
-
constructor({
|
|
174
|
+
constructor({ explicitUrl, hostService, logger = console, fetchImpl = fetch, connectImpl = wsConnect, env = process.env } = {}) {
|
|
109
175
|
super()
|
|
110
|
-
this.endpointFile = endpointFile || defaultEndpointFile()
|
|
111
176
|
this.explicitUrl = explicitUrl
|
|
177
|
+
this.hostService = typeof hostService === 'function' ? hostService : undefined
|
|
178
|
+
this.env = env
|
|
112
179
|
this.logger = logger
|
|
113
180
|
this.fetchImpl = fetchImpl
|
|
114
181
|
this.connectImpl = connectImpl
|
|
@@ -119,25 +186,59 @@ export class DshClient extends EventEmitter {
|
|
|
119
186
|
this.muxReady = false
|
|
120
187
|
this.pendingOpens = []
|
|
121
188
|
this.lastError = undefined
|
|
189
|
+
/** Failure classification for the status payload; see contracts/status-endpoint.md §3. */
|
|
190
|
+
this.errorKind = undefined
|
|
122
191
|
}
|
|
123
192
|
|
|
124
|
-
/**
|
|
193
|
+
/**
|
|
194
|
+
* Wire (or replace) the in-process resolver after construction.
|
|
195
|
+
*
|
|
196
|
+
* The plugin learns about the host's services asynchronously — `ctx.inject`
|
|
197
|
+
* callbacks fire once the service exists — so the agent cannot pass this in
|
|
198
|
+
* the constructor.
|
|
199
|
+
*
|
|
200
|
+
* @param {(() => string | undefined) | undefined} resolver
|
|
201
|
+
*/
|
|
202
|
+
setHostService(resolver) {
|
|
203
|
+
this.hostService = typeof resolver === 'function' ? resolver : undefined
|
|
204
|
+
return this
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Re-read endpoint discovery and re-do the token -> cookie exchange.
|
|
209
|
+
*
|
|
210
|
+
* Candidates are tried in order and the first one that *authenticates* wins:
|
|
211
|
+
* a host that can hand us its current URL beats the environment variable and
|
|
212
|
+
* the built-in default, while an older source is still used if the new one
|
|
213
|
+
* turns out not to work (constitution §IV).
|
|
214
|
+
*/
|
|
125
215
|
async refresh({ force = false } = {}) {
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
216
|
+
const candidates = discoverCandidates({ explicitUrl: this.explicitUrl, hostService: this.hostService, env: this.env })
|
|
217
|
+
let lastError
|
|
218
|
+
let attemptedWithToken
|
|
219
|
+
for (const candidate of candidates) {
|
|
220
|
+
if (force || !this.endpoint || this.endpoint.base !== candidate.base) this.cookie = ''
|
|
221
|
+
this.endpoint = candidate
|
|
222
|
+
try {
|
|
223
|
+
await this.authenticate()
|
|
224
|
+
this.errorKind = undefined
|
|
225
|
+
return this.endpoint
|
|
226
|
+
} catch (error) {
|
|
227
|
+
lastError = error
|
|
228
|
+
if (candidate.token) attemptedWithToken = candidate
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
this.cookie = ''
|
|
232
|
+
this.errorKind = kindFor(attemptedWithToken ?? this.endpoint)
|
|
233
|
+
throw lastError ?? new DshUnavailable('DSH is not reachable (no candidate answered)')
|
|
131
234
|
}
|
|
132
235
|
|
|
133
236
|
async authenticate() {
|
|
134
|
-
const endpoint = this.endpoint
|
|
135
|
-
|
|
136
|
-
})
|
|
137
|
-
this.endpoint = endpoint
|
|
237
|
+
const endpoint = this.endpoint
|
|
238
|
+
if (!endpoint) throw new DshUnavailable('no DSH endpoint discovered yet')
|
|
138
239
|
if (!endpoint.token) {
|
|
139
240
|
throw new DshUnavailable(
|
|
140
|
-
`no DSH launch token found (looked at ${
|
|
241
|
+
`no DSH launch token found (looked at ${endpoint.source}); restart the host or set dshUrl in the plugin config`,
|
|
141
242
|
)
|
|
142
243
|
}
|
|
143
244
|
const url = `${endpoint.base}/?token=${encodeURIComponent(endpoint.token)}`
|
|
@@ -152,7 +253,7 @@ export class DshClient extends EventEmitter {
|
|
|
152
253
|
throw new DshUnavailable(`DSH auth handshake returned HTTP ${response.status} without a dsh-auth cookie`)
|
|
153
254
|
}
|
|
154
255
|
this.cookie = cookie
|
|
155
|
-
this.logger.debug?.(`mobile-link: authenticated with DSH at ${endpoint.base}`)
|
|
256
|
+
this.logger.debug?.(`mobile-link: authenticated with DSH at ${endpoint.base} (source: ${endpoint.source})`)
|
|
156
257
|
return cookie
|
|
157
258
|
}
|
|
158
259
|
|
|
@@ -189,7 +290,7 @@ export class DshClient extends EventEmitter {
|
|
|
189
290
|
}
|
|
190
291
|
let json
|
|
191
292
|
try {
|
|
192
|
-
json = await response.json()
|
|
293
|
+
json = isBinaryAnswer(response) ? await decodeBinaryAnswer(response) : await response.json()
|
|
193
294
|
} catch {
|
|
194
295
|
return { ok: false, error: { code: 'gateway/bad-response', message: `DSH returned HTTP ${response.status}`, details: {} } }
|
|
195
296
|
}
|