dsh-plugin-weapp-gateway 0.8.23 → 0.8.34
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/LICENSE +21 -21
- package/PROTOCOL.md +1545 -1486
- package/README.md +503 -464
- package/THIRD-PARTY-NOTICES/dsh-cost-meter.txt +31 -31
- package/bin/setup-ip.mjs +474 -474
- package/cordis.patch.yml +50 -50
- package/docs/agent-team-app-integration.md +945 -0
- package/docs/blog-mobile-gateway.md +542 -542
- package/docs/dsh-0.1.5-rc.2-compatibility-audit.md +255 -255
- package/docs/dsh-0.1.6-alpha.1-compatibility-audit.md +122 -122
- package/docs/dsh-0.1.7-rc.1-compatibility-audit.md +52 -52
- package/docs/dsh-0.2.0-rc.2-plugin-management-mobile-integration.md +109 -109
- package/docs/dsh-rc2-mobile-integration.md +155 -155
- package/docs/file-upload-app-integration.md +250 -0
- package/docs/multi-gateway-app-integration.md +141 -125
- package/docs/multi-gateway-phase1-acceptance.md +179 -179
- package/docs/multi-gateway-todo.md +137 -137
- package/docs/remote-gateway-refactor-plan.md +52 -52
- package/docs/runtime-architecture.architecture.json +264 -264
- package/docs/runtime-architecture.html +15001 -15001
- package/docs/runtime-architecture.visual-check.html +32 -32
- package/docs/runtime-architecture.visual-check.json +548 -548
- package/docs/session-agent-preset-app-integration.md +93 -93
- package/docs/turn-outline-app-integration.md +196 -0
- package/docs/typert-remote-gateway-feature-checklist.md +294 -294
- package/docs/voice-input-implementation.md +219 -219
- package/docs/workspace-changes-mobile-integration.md +32 -32
- package/helper/dsh_mobile_gateway_helper.py +227 -227
- package/lib/account-bridge.mjs +57 -57
- package/lib/client.js +1409 -1361
- package/lib/cloudflare-tunnel.mjs +314 -314
- package/lib/cloudflared-binary.mjs +135 -135
- package/lib/context-bridge.mjs +3338 -1715
- package/lib/cost-meter-bridge.mjs +794 -794
- package/lib/cost-meter-dashboard-bridge.mjs +1655 -1449
- package/lib/devices.js +273 -245
- package/lib/dsh-host-adapter.mjs +508 -426
- package/lib/file-upload.mjs +205 -0
- package/lib/gateway-probe.mjs +6 -6
- package/lib/gateway-state.mjs +57 -57
- package/lib/gateway-tools.mjs +36 -36
- package/lib/history-cache.mjs +66 -0
- package/lib/index.mjs +4390 -3803
- package/lib/plugin-management.mjs +251 -233
- package/lib/public-endpoint-check.mjs +61 -61
- package/lib/relay-connector.mjs +213 -181
- package/lib/relay-state.mjs +83 -76
- package/lib/session-follower.mjs +140 -136
- package/lib/session-resume-bridge.mjs +86 -0
- package/lib/session-target.mjs +29 -0
- package/lib/speech-input.mjs +192 -192
- package/lib/vendor/cost-meter/aliyun-balance.mjs +123 -123
- package/lib/vendor/cost-meter/coding-plans.mjs +1668 -1668
- package/lib/vendor/cost-meter/credentials-shim.mjs +8 -8
- package/lib/vendor/cost-meter/custom-balance.mjs +281 -281
- package/lib/vendor/cost-meter/factory-quota.mjs +69 -69
- package/lib/vendor/cost-meter/gateway-quota-adapters.mjs +1006 -1006
- package/lib/vendor/cost-meter/gateway-quotas.mjs +664 -664
- package/lib/vendor/cost-meter/home-paths-shim.mjs +13 -13
- package/lib/vendor/cost-meter/net.mjs +183 -183
- package/lib/vendor/cost-meter/opendesign.mjs +204 -204
- package/lib/vendor/cost-meter/tokenrhythm-balance.mjs +95 -95
- package/lib/vendor/cost-meter/workbuddy-accounts.mjs +44 -44
- package/lib/vendor/cost-meter/workbuddy-transport.mjs +38 -38
- package/lib/vendor/cost-meter/workbuddy.mjs +356 -356
- package/lib/web-restart.mjs +47 -47
- package/lib/wire-json.mjs +7 -7
- package/lib/workspace-changes-bridge.mjs +32 -32
- package/package.json +54 -54
- package/relay/agent-connector.js +588 -569
- package/relay/vendor/tweetnacl.js +2391 -2391
package/lib/devices.js
CHANGED
|
@@ -1,245 +1,273 @@
|
|
|
1
|
-
// Persistent paired-device registry for weapp-gateway authentication.
|
|
2
|
-
//
|
|
3
|
-
// Long-lived device tokens are generated with 256 bits of entropy and are
|
|
4
|
-
// never written to disk. Only their SHA-256 digests are persisted. Pairing
|
|
5
|
-
// codes are one-time, short-lived, and memory-only, so a restart invalidates
|
|
6
|
-
// every outstanding QR code without affecting already paired devices.
|
|
7
|
-
'use strict'
|
|
8
|
-
|
|
9
|
-
const fs = require('node:fs')
|
|
10
|
-
const crypto = require('node:crypto')
|
|
11
|
-
const path = require('node:path')
|
|
12
|
-
|
|
13
|
-
const STORE_VERSION = 3
|
|
14
|
-
const DEFAULT_PAIRING_TTL_MS = 5 * 60 * 1000
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
1
|
+
// Persistent paired-device registry for weapp-gateway authentication.
|
|
2
|
+
//
|
|
3
|
+
// Long-lived device tokens are generated with 256 bits of entropy and are
|
|
4
|
+
// never written to disk. Only their SHA-256 digests are persisted. Pairing
|
|
5
|
+
// codes are one-time, short-lived, and memory-only, so a restart invalidates
|
|
6
|
+
// every outstanding QR code without affecting already paired devices.
|
|
7
|
+
'use strict'
|
|
8
|
+
|
|
9
|
+
const fs = require('node:fs')
|
|
10
|
+
const crypto = require('node:crypto')
|
|
11
|
+
const path = require('node:path')
|
|
12
|
+
|
|
13
|
+
const STORE_VERSION = 3
|
|
14
|
+
const DEFAULT_PAIRING_TTL_MS = 5 * 60 * 1000
|
|
15
|
+
|
|
16
|
+
// How a device's live connections reach this gateway. The trusted-device list
|
|
17
|
+
// shows one badge per active route so the operator can see whether the phone
|
|
18
|
+
// currently sits on the LAN, a public entry point, or the Cloudflare tunnel.
|
|
19
|
+
// lan — the private-network-only LAN listener
|
|
20
|
+
// public — the main listener (public server, reverse proxy, or the
|
|
21
|
+
// bundled relay connector dialling loopback on its behalf)
|
|
22
|
+
// cloudflare — the tunnel's dedicated loopback origin listener
|
|
23
|
+
const CONNECTION_ROUTES = ['lan', 'public', 'cloudflare']
|
|
24
|
+
|
|
25
|
+
function normalizeRoute(route) {
|
|
26
|
+
return CONNECTION_ROUTES.includes(route) ? route : 'public'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function digest(secret) {
|
|
30
|
+
return crypto.createHash('sha256').update(secret, 'utf8').digest('hex')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function safeEqualHex(left, right) {
|
|
34
|
+
if (typeof left !== 'string' || typeof right !== 'string') return false
|
|
35
|
+
const a = Buffer.from(left, 'hex')
|
|
36
|
+
const b = Buffer.from(right, 'hex')
|
|
37
|
+
return a.length === 32 && b.length === 32 && crypto.timingSafeEqual(a, b)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// routes: Map<route, connectionCount> for the device's live sockets. Derived
|
|
41
|
+
// totals keep one source of truth for online/connections/routes.
|
|
42
|
+
function publicDevice(device, routes) {
|
|
43
|
+
let connections = 0
|
|
44
|
+
for (const count of routes.values()) connections += count
|
|
45
|
+
return {
|
|
46
|
+
id: device.id,
|
|
47
|
+
name: device.name,
|
|
48
|
+
createdAt: device.createdAt,
|
|
49
|
+
lastSeenAt: device.lastSeenAt || null,
|
|
50
|
+
online: connections > 0,
|
|
51
|
+
connections,
|
|
52
|
+
routes: [...routes.keys()],
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeClientDeviceId(value) {
|
|
57
|
+
if (typeof value !== 'string') return undefined
|
|
58
|
+
const normalized = value.trim()
|
|
59
|
+
return /^[A-Za-z0-9._:-]{8,128}$/.test(normalized) ? normalized : undefined
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function createRegistry(file, options = {}) {
|
|
63
|
+
const pairingTtlMs = Number.isSafeInteger(options.pairingTtlMs) && options.pairingTtlMs > 0
|
|
64
|
+
? options.pairingTtlMs
|
|
65
|
+
: DEFAULT_PAIRING_TTL_MS
|
|
66
|
+
let devices = []
|
|
67
|
+
const pairings = new Map()
|
|
68
|
+
// deviceId -> Map<route, connectionCount>
|
|
69
|
+
const online = new Map()
|
|
70
|
+
|
|
71
|
+
const routesOf = (deviceId) => online.get(deviceId) || new Map()
|
|
72
|
+
|
|
73
|
+
const save = () => {
|
|
74
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 })
|
|
75
|
+
const tmp = `${file}.${process.pid}.tmp`
|
|
76
|
+
fs.writeFileSync(tmp, JSON.stringify({ version: STORE_VERSION, devices }, null, 2), { mode: 0o600 })
|
|
77
|
+
fs.chmodSync(tmp, 0o600)
|
|
78
|
+
fs.renameSync(tmp, file)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const load = () => {
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
84
|
+
if (!Array.isArray(parsed.devices)) return
|
|
85
|
+
let migrated = false
|
|
86
|
+
devices = parsed.devices.flatMap((row) => {
|
|
87
|
+
if (!row || typeof row.id !== 'string' || typeof row.name !== 'string') return []
|
|
88
|
+
let tokenHash = typeof row.tokenHash === 'string' ? row.tokenHash : undefined
|
|
89
|
+
// Migrate the v1 development format without invalidating an existing
|
|
90
|
+
// device. The plaintext is removed on the next atomic save.
|
|
91
|
+
if (!tokenHash && typeof row.token === 'string' && row.token !== '') {
|
|
92
|
+
tokenHash = digest(row.token)
|
|
93
|
+
migrated = true
|
|
94
|
+
}
|
|
95
|
+
if (!tokenHash || !/^[a-f0-9]{64}$/.test(tokenHash)) return []
|
|
96
|
+
return [{
|
|
97
|
+
id: row.id,
|
|
98
|
+
name: row.name.slice(0, 80),
|
|
99
|
+
clientDeviceId: normalizeClientDeviceId(row.clientDeviceId),
|
|
100
|
+
tokenHash,
|
|
101
|
+
createdAt: Number.isFinite(row.createdAt) ? row.createdAt : Date.now(),
|
|
102
|
+
lastSeenAt: Number.isFinite(row.lastSeenAt) ? row.lastSeenAt : null,
|
|
103
|
+
revokedAt: Number.isFinite(row.revokedAt) ? row.revokedAt : (row.revoked ? Date.now() : null),
|
|
104
|
+
}]
|
|
105
|
+
})
|
|
106
|
+
if (migrated || parsed.version !== STORE_VERSION) save()
|
|
107
|
+
else fs.chmodSync(file, 0o600)
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error && error.code !== 'ENOENT') throw new Error(`failed to load device registry: ${error.message}`, { cause: error })
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const prunePairings = (now = Date.now()) => {
|
|
114
|
+
for (const [codeHash, pairing] of pairings) {
|
|
115
|
+
if (pairing.expiresAt <= now) pairings.delete(codeHash)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
load()
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
list() {
|
|
123
|
+
const active = devices.filter((device) => !device.revokedAt)
|
|
124
|
+
const counts = new Map()
|
|
125
|
+
const indices = new Map()
|
|
126
|
+
for (const device of active) counts.set(device.name, (counts.get(device.name) || 0) + 1)
|
|
127
|
+
return active.map((device) => {
|
|
128
|
+
const index = (indices.get(device.name) || 0) + 1
|
|
129
|
+
indices.set(device.name, index)
|
|
130
|
+
return {
|
|
131
|
+
...publicDevice(device, routesOf(device.id)),
|
|
132
|
+
displayName: counts.get(device.name) > 1 ? `${device.name} (${index})` : device.name,
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
count() {
|
|
138
|
+
return devices.filter((device) => !device.revokedAt).length
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
// Ids of pairing codes that are still redeemable. The management panel
|
|
142
|
+
// polls this to close the QR dialog the moment its code is consumed (a
|
|
143
|
+
// successful scan removes it) instead of waiting for the 5-minute TTL.
|
|
144
|
+
pendingPairings() {
|
|
145
|
+
prunePairings()
|
|
146
|
+
return [...pairings.values()].map((pairing) => pairing.id)
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
createPairing(name) {
|
|
150
|
+
prunePairings()
|
|
151
|
+
const code = crypto.randomBytes(32).toString('base64url')
|
|
152
|
+
const pairing = {
|
|
153
|
+
id: crypto.randomUUID(),
|
|
154
|
+
name: typeof name === 'string' && name.trim() !== '' ? name.trim().slice(0, 80) : '未命名设备',
|
|
155
|
+
expiresAt: Date.now() + pairingTtlMs,
|
|
156
|
+
}
|
|
157
|
+
pairings.set(digest(code), pairing)
|
|
158
|
+
return { ...pairing, code }
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
hasPairing(code) {
|
|
162
|
+
prunePairings()
|
|
163
|
+
return typeof code === 'string' && pairings.has(digest(code))
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
claimPairing(code, clientDeviceId, clientDeviceName) {
|
|
167
|
+
prunePairings()
|
|
168
|
+
if (typeof code !== 'string' || code === '') return undefined
|
|
169
|
+
const codeHash = digest(code)
|
|
170
|
+
const pairing = pairings.get(codeHash)
|
|
171
|
+
if (!pairing) return undefined
|
|
172
|
+
// Delete before doing any further work: claiming is single-use even if a
|
|
173
|
+
// later socket upgrade fails.
|
|
174
|
+
pairings.delete(codeHash)
|
|
175
|
+
const token = crypto.randomBytes(32).toString('base64url')
|
|
176
|
+
const normalizedClientDeviceId = normalizeClientDeviceId(clientDeviceId)
|
|
177
|
+
let device = normalizedClientDeviceId
|
|
178
|
+
? devices.find((candidate) => !candidate.revokedAt && candidate.clientDeviceId === normalizedClientDeviceId)
|
|
179
|
+
: undefined
|
|
180
|
+
if (device) {
|
|
181
|
+
// A valid one-time pairing code authorizes credential rotation. Keep
|
|
182
|
+
// the stable server-side device record while replacing its token, so
|
|
183
|
+
// re-pairing one iOS installation never creates duplicate rows.
|
|
184
|
+
device.name = typeof clientDeviceName === 'string' && clientDeviceName.trim() ? clientDeviceName.trim().slice(0, 80) : '未命名设备'
|
|
185
|
+
device.tokenHash = digest(token)
|
|
186
|
+
device.clientDeviceId = normalizedClientDeviceId
|
|
187
|
+
} else {
|
|
188
|
+
device = {
|
|
189
|
+
id: pairing.id,
|
|
190
|
+
name: typeof clientDeviceName === 'string' && clientDeviceName.trim() ? clientDeviceName.trim().slice(0, 80) : '未命名设备',
|
|
191
|
+
clientDeviceId: normalizedClientDeviceId,
|
|
192
|
+
tokenHash: digest(token),
|
|
193
|
+
createdAt: Date.now(),
|
|
194
|
+
lastSeenAt: null,
|
|
195
|
+
revokedAt: null,
|
|
196
|
+
}
|
|
197
|
+
devices.push(device)
|
|
198
|
+
}
|
|
199
|
+
save()
|
|
200
|
+
return { device: publicDevice(device, new Map()), token }
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
authenticate(token, clientDeviceId, clientDeviceName) {
|
|
204
|
+
if (typeof token !== 'string' || token === '') return undefined
|
|
205
|
+
const tokenHash = digest(token)
|
|
206
|
+
const device = devices.find((candidate) => !candidate.revokedAt && safeEqualHex(candidate.tokenHash, tokenHash))
|
|
207
|
+
const normalizedClientDeviceId = normalizeClientDeviceId(clientDeviceId)
|
|
208
|
+
if (device && normalizedClientDeviceId && !device.clientDeviceId) {
|
|
209
|
+
// Migration path for devices paired before installation IDs existed:
|
|
210
|
+
// a valid long-lived token proves ownership of this record, so it is
|
|
211
|
+
// safe to bind the client's stable ID without creating a new device.
|
|
212
|
+
const conflict = devices.some((candidate) => candidate !== device && !candidate.revokedAt && candidate.clientDeviceId === normalizedClientDeviceId)
|
|
213
|
+
if (!conflict) {
|
|
214
|
+
device.clientDeviceId = normalizedClientDeviceId
|
|
215
|
+
save()
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (device && typeof clientDeviceName === 'string' && clientDeviceName.trim()) {
|
|
219
|
+
const name = clientDeviceName.trim().slice(0, 80)
|
|
220
|
+
if (device.name !== name) {
|
|
221
|
+
device.name = name
|
|
222
|
+
save()
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return device ? publicDevice(device, routesOf(device.id)) : undefined
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
connected(deviceId, route) {
|
|
229
|
+
const device = devices.find((candidate) => candidate.id === deviceId && !candidate.revokedAt)
|
|
230
|
+
if (!device) return false
|
|
231
|
+
const key = normalizeRoute(route)
|
|
232
|
+
const routes = online.get(deviceId) || new Map()
|
|
233
|
+
routes.set(key, (routes.get(key) || 0) + 1)
|
|
234
|
+
online.set(deviceId, routes)
|
|
235
|
+
device.lastSeenAt = Date.now()
|
|
236
|
+
save()
|
|
237
|
+
return true
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
disconnected(deviceId, route) {
|
|
241
|
+
const routes = online.get(deviceId)
|
|
242
|
+
if (!routes) return
|
|
243
|
+
const key = normalizeRoute(route)
|
|
244
|
+
const count = routes.get(key) || 0
|
|
245
|
+
if (count <= 1) routes.delete(key)
|
|
246
|
+
else routes.set(key, count - 1)
|
|
247
|
+
if (routes.size === 0) online.delete(deviceId)
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
revoke(deviceId) {
|
|
251
|
+
const index = devices.findIndex((candidate) => candidate.id === deviceId && !candidate.revokedAt)
|
|
252
|
+
if (index < 0) return false
|
|
253
|
+
devices.splice(index, 1)
|
|
254
|
+
online.delete(deviceId)
|
|
255
|
+
save()
|
|
256
|
+
return true
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
// A paired device can be relabelled later: the relay connector renames its
|
|
260
|
+
// entry once the phone reports its own model, so the trusted-device list
|
|
261
|
+
// shows "Xiaomi 14 Ultra" instead of the host running the connector.
|
|
262
|
+
rename(deviceId, name) {
|
|
263
|
+
const device = devices.find((candidate) => candidate.id === deviceId && !candidate.revokedAt)
|
|
264
|
+
if (!device) return null
|
|
265
|
+
if (typeof name !== 'string' || name.trim() === '') return null
|
|
266
|
+
device.name = name.trim().slice(0, 80)
|
|
267
|
+
save()
|
|
268
|
+
return publicDevice(device, routesOf(device.id))
|
|
269
|
+
},
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
module.exports = { createRegistry, digest, CONNECTION_ROUTES }
|