dsh-mobile-web 0.1.2 → 0.1.4

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.
@@ -1,226 +1,226 @@
1
- /**
2
- * 设备表与配对相关:持久化(devices.json)、公网基址、受保护会话清单。
3
- * 纯文件/JSON 读写(从原 index.js 顶层原样搬出);内存中的设备 Map 与配对码
4
- * 状态仍由 core 的 createHost 闭包持有,等 RPC 注册表那轮再显式化。
5
- */
6
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
7
- import { randomBytes, randomInt } from 'node:crypto'
8
- import { homedir } from 'node:os'
9
- import { join } from 'node:path'
10
- import { MAX_DEVICES, TOKEN_TTL_MS, COOKIE_NAME } from './config.js'
11
- export function normalizePublicBase(value) {
12
- const raw = typeof value === 'string' ? value.trim() : ''
13
- if (raw === '') return ''
14
- try {
15
- const url = new URL(raw)
16
- if (url.protocol !== 'http:' && url.protocol !== 'https:') return null
17
- if (url.hostname === '') return null
18
- url.hash = ''
19
- url.search = ''
20
- return url.toString().replace(/\/+$/, '')
21
- } catch {
22
- return null
23
- }
24
- }
25
-
26
- export function loadPublicBase(file) {
27
- try {
28
- if (!existsSync(file)) return ''
29
- const saved = JSON.parse(readFileSync(file, 'utf8'))
30
- const raw = saved && typeof saved === 'object' ? saved.publicBaseUrl : saved
31
- const normalized = normalizePublicBase(raw)
32
- return normalized === null ? '' : normalized
33
- } catch {
34
- return ''
35
- }
36
- }
37
-
38
- export function savePublicBase(file, value) {
39
- try {
40
- const dir = join(file, '..')
41
- if (dir && !existsSync(dir)) mkdirSync(dir, { recursive: true })
42
- writeFileSync(file, JSON.stringify({ publicBaseUrl: value }, null, 2))
43
- } catch (err) {
44
- console.warn('[dsh-mobile] failed to save public domain:', err && err.message)
45
- }
46
- }
47
-
48
- export function loadDevices(file) {
49
- const out = new Map()
50
- try {
51
- if (!existsSync(file)) return out
52
- const saved = JSON.parse(readFileSync(file, 'utf8'))
53
- if (saved && typeof saved === 'object') {
54
- for (const [id, d] of Object.entries(saved)) {
55
- if (d && typeof d === 'object' && typeof d.token === 'string') out.set(id, d)
56
- }
57
- }
58
- } catch {}
59
- return out
60
- }
61
-
62
- export function persistDevices(file, devices) {
63
- try {
64
- const dir = join(file, '..')
65
- if (dir && !existsSync(dir)) mkdirSync(dir, { recursive: true })
66
- const obj = {}
67
- for (const [id, d] of devices) obj[id] = d
68
- writeFileSync(file, JSON.stringify(obj, null, 2))
69
- } catch (err) {
70
- console.error('[dsh-mobile] failed to persist device store:', err && err.message)
71
- }
72
- }
73
-
74
- export function protectedSessionsFile() {
75
- return join(homedir(), '.dsh', 'dsh-mobile-protected.json')
76
- }
77
-
78
- export function loadProtectedSessionIds() {
79
- try {
80
- const raw = readFileSync(protectedSessionsFile(), 'utf8')
81
- const parsed = JSON.parse(raw)
82
- const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed.sessionIds) ? parsed.sessionIds : []
83
- return new Set(list.filter((id) => typeof id === 'string' && id !== ''))
84
- } catch {
85
- return new Set()
86
- }
87
- }
88
-
89
-
90
- /**
91
- * 有状态的设备/配对运行时:设备表与配对码都在 state bag 上,持久化复用本模块的 persistDevices。
92
- * core 创建一次后解构使用(纯搬迁,行为不变)。
93
- */
94
- export function createDeviceAuth({ state, cfg }) {
95
- const devices = state.devices
96
- const cookieName = COOKIE_NAME
97
- function deviceTokensOf(req) {
98
- const header = req.headers.cookie
99
- if (!header) return []
100
- const out = []
101
- for (const part of String(header).split(';')) {
102
- const i = part.indexOf('=')
103
- if (i < 0) continue
104
- if (part.slice(0, i).trim() !== cookieName) continue
105
- out.push(decodeURIComponent(part.slice(i + 1).trim()))
106
- }
107
- return out
108
- }
109
-
110
- function authenticatedDevice(req) {
111
- // 容忍历史遗留:Path=/ 与 Path=/m 可能同时存在同名 cookie,任一有效即通过。
112
- for (const token of deviceTokensOf(req)) {
113
- for (const [deviceId, d] of devices) {
114
- if (d.token === token) {
115
- d.lastSeenAt = Date.now()
116
- return { deviceId, ...d }
117
- }
118
- }
119
- }
120
- return null
121
- }
122
-
123
- function describeDeviceUA(userAgent) {
124
- const ua = String(userAgent || '')
125
- const grab = (re) => {
126
- const hit = re.exec(ua)
127
- return hit && typeof hit[1] === 'string' ? hit[1] : ''
128
- }
129
- let platform = ''
130
- if (/OpenHarmony|HarmonyOS/i.test(ua)) {
131
- const v = grab(/OpenHarmony\s+([\d.]+)/i) || grab(/HarmonyOS\s+([\d.]+)/i)
132
- platform = v === '' ? 'HarmonyOS' : 'HarmonyOS ' + v
133
- } else if (/Android/i.test(ua)) {
134
- const v = grab(/Android\s+([\d.]+)/i)
135
- platform = v === '' ? 'Android' : 'Android ' + v
136
- } else if (/iPhone|iPad|iPod/i.test(ua)) {
137
- platform = 'iOS'
138
- } else if (/Windows NT 10/i.test(ua)) {
139
- platform = 'Windows'
140
- } else if (/Macintosh|Mac OS X/i.test(ua)) {
141
- platform = 'macOS'
142
- } else if (/Linux/i.test(ua)) {
143
- platform = 'Linux'
144
- }
145
- let browser = ''
146
- const browsers = [
147
- [/HuaweiBrowser\/([\d.]+)/i, '华为浏览器'],
148
- [/Quark\/([\d.]+)/i, '夸克'],
149
- [/MicroMessenger\/([\d.]+)/i, '微信'],
150
- [/Edg\/([\d.]+)/i, 'Edge'],
151
- [/Firefox\/([\d.]+)/i, 'Firefox'],
152
- [/Chrome\/([\d.]+)/i, 'Chrome'],
153
- [/Version\/([\d.]+)[\s\S]*Safari/i, 'Safari'],
154
- ]
155
- for (const [re, name] of browsers) {
156
- const v = grab(re)
157
- if (v !== '') {
158
- browser = name + ' ' + v.split('.')[0]
159
- break
160
- }
161
- }
162
- let model = ''
163
- if (!/OpenHarmony|HarmonyOS/i.test(ua)) {
164
- if (/iPhone/i.test(ua)) model = 'iPhone'
165
- else if (/iPad/i.test(ua)) model = 'iPad'
166
- else if (/Android/i.test(ua)) {
167
- const hit = /Android[^;)]*;\s*(?:[A-Za-z]{2}-[A-Za-z]{2};\s*)?([^;)]+?)\s*(?:Build\/|\))/i.exec(ua)
168
- const token = hit && typeof hit[1] === 'string' ? hit[1].replace(/^U;\s*/i, '').trim() : ''
169
- if (token.length >= 2 && !/^wv$/i.test(token) && !/^Android$/i.test(token)) model = token.slice(0, 40)
170
- }
171
- }
172
- const label = [platform, browser].filter((s) => s !== '').join(' · ')
173
- return { model, platform, browser, label }
174
- }
175
-
176
- function createDevice(userAgent) {
177
- if (devices.size >= MAX_DEVICES) return null
178
- const now = Date.now()
179
- const token = randomBytes(24).toString('hex')
180
- const deviceId = randomBytes(8).toString('hex')
181
- devices.set(deviceId, {
182
- token,
183
- name: '手机设备',
184
- createdAt: now,
185
- lastSeenAt: now,
186
- userAgent: String(userAgent || '').slice(0, 180),
187
- })
188
- persistDevices(cfg.devicesFile, devices)
189
- return { deviceId, token }
190
- }
191
-
192
- function fixedPairCode() {
193
- return String(cfg.pairCode == null ? '' : cfg.pairCode).trim()
194
- }
195
-
196
- function issuePairToken() {
197
- const fixed = fixedPairCode()
198
- if (fixed !== '') {
199
- // 固定码:永不过期,随时可配(不再随机、不再 5 分钟失效)。
200
- state.pairToken = { code: fixed, expiresAt: Number.MAX_SAFE_INTEGER, fixed: true }
201
- return state.pairToken
202
- }
203
- state.pairToken = { code: String(randomInt(0, 1e6)).padStart(6, '0'), expiresAt: Date.now() + TOKEN_TTL_MS, fixed: false }
204
- return state.pairToken
205
- }
206
-
207
- function acceptPairCode(code, userAgent) {
208
- const input = String(code == null ? '' : code).trim()
209
- const fixed = fixedPairCode()
210
- if (fixed !== '') {
211
- if (input !== fixed) return null
212
- return createDevice(userAgent)
213
- }
214
- const now = Date.now()
215
- if (!state.pairToken || state.pairToken.code !== input || now > state.pairToken.expiresAt) {
216
- if (state.pairToken && now > state.pairToken.expiresAt) state.pairToken = null
217
- return null
218
- }
219
- const created = createDevice(userAgent)
220
- if (created) state.pairToken = null
221
- return created
222
- }
223
-
224
- return { deviceTokensOf, authenticatedDevice, describeDeviceUA, createDevice,
225
- fixedPairCode, issuePairToken, acceptPairCode }
226
- }
1
+ /**
2
+ * 设备表与配对相关:持久化(devices.json)、公网基址、受保护会话清单。
3
+ * 纯文件/JSON 读写(从原 index.js 顶层原样搬出);内存中的设备 Map 与配对码
4
+ * 状态仍由 core 的 createHost 闭包持有,等 RPC 注册表那轮再显式化。
5
+ */
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
7
+ import { randomBytes, randomInt } from 'node:crypto'
8
+ import { homedir } from 'node:os'
9
+ import { join } from 'node:path'
10
+ import { MAX_DEVICES, TOKEN_TTL_MS, COOKIE_NAME } from './config.js'
11
+ export function normalizePublicBase(value) {
12
+ const raw = typeof value === 'string' ? value.trim() : ''
13
+ if (raw === '') return ''
14
+ try {
15
+ const url = new URL(raw)
16
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') return null
17
+ if (url.hostname === '') return null
18
+ url.hash = ''
19
+ url.search = ''
20
+ return url.toString().replace(/\/+$/, '')
21
+ } catch {
22
+ return null
23
+ }
24
+ }
25
+
26
+ export function loadPublicBase(file) {
27
+ try {
28
+ if (!existsSync(file)) return ''
29
+ const saved = JSON.parse(readFileSync(file, 'utf8'))
30
+ const raw = saved && typeof saved === 'object' ? saved.publicBaseUrl : saved
31
+ const normalized = normalizePublicBase(raw)
32
+ return normalized === null ? '' : normalized
33
+ } catch {
34
+ return ''
35
+ }
36
+ }
37
+
38
+ export function savePublicBase(file, value) {
39
+ try {
40
+ const dir = join(file, '..')
41
+ if (dir && !existsSync(dir)) mkdirSync(dir, { recursive: true })
42
+ writeFileSync(file, JSON.stringify({ publicBaseUrl: value }, null, 2))
43
+ } catch (err) {
44
+ console.warn('[dsh-mobile] failed to save public domain:', err && err.message)
45
+ }
46
+ }
47
+
48
+ export function loadDevices(file) {
49
+ const out = new Map()
50
+ try {
51
+ if (!existsSync(file)) return out
52
+ const saved = JSON.parse(readFileSync(file, 'utf8'))
53
+ if (saved && typeof saved === 'object') {
54
+ for (const [id, d] of Object.entries(saved)) {
55
+ if (d && typeof d === 'object' && typeof d.token === 'string') out.set(id, d)
56
+ }
57
+ }
58
+ } catch {}
59
+ return out
60
+ }
61
+
62
+ export function persistDevices(file, devices) {
63
+ try {
64
+ const dir = join(file, '..')
65
+ if (dir && !existsSync(dir)) mkdirSync(dir, { recursive: true })
66
+ const obj = {}
67
+ for (const [id, d] of devices) obj[id] = d
68
+ writeFileSync(file, JSON.stringify(obj, null, 2))
69
+ } catch (err) {
70
+ console.error('[dsh-mobile] failed to persist device store:', err && err.message)
71
+ }
72
+ }
73
+
74
+ export function protectedSessionsFile() {
75
+ return join(homedir(), '.dsh', 'dsh-mobile-protected.json')
76
+ }
77
+
78
+ export function loadProtectedSessionIds() {
79
+ try {
80
+ const raw = readFileSync(protectedSessionsFile(), 'utf8')
81
+ const parsed = JSON.parse(raw)
82
+ const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed.sessionIds) ? parsed.sessionIds : []
83
+ return new Set(list.filter((id) => typeof id === 'string' && id !== ''))
84
+ } catch {
85
+ return new Set()
86
+ }
87
+ }
88
+
89
+
90
+ /**
91
+ * 有状态的设备/配对运行时:设备表与配对码都在 state bag 上,持久化复用本模块的 persistDevices。
92
+ * core 创建一次后解构使用(纯搬迁,行为不变)。
93
+ */
94
+ export function createDeviceAuth({ state, cfg }) {
95
+ const devices = state.devices
96
+ const cookieName = COOKIE_NAME
97
+ function deviceTokensOf(req) {
98
+ const header = req.headers.cookie
99
+ if (!header) return []
100
+ const out = []
101
+ for (const part of String(header).split(';')) {
102
+ const i = part.indexOf('=')
103
+ if (i < 0) continue
104
+ if (part.slice(0, i).trim() !== cookieName) continue
105
+ out.push(decodeURIComponent(part.slice(i + 1).trim()))
106
+ }
107
+ return out
108
+ }
109
+
110
+ function authenticatedDevice(req) {
111
+ // 容忍历史遗留:Path=/ 与 Path=/m 可能同时存在同名 cookie,任一有效即通过。
112
+ for (const token of deviceTokensOf(req)) {
113
+ for (const [deviceId, d] of devices) {
114
+ if (d.token === token) {
115
+ d.lastSeenAt = Date.now()
116
+ return { deviceId, ...d }
117
+ }
118
+ }
119
+ }
120
+ return null
121
+ }
122
+
123
+ function describeDeviceUA(userAgent) {
124
+ const ua = String(userAgent || '')
125
+ const grab = (re) => {
126
+ const hit = re.exec(ua)
127
+ return hit && typeof hit[1] === 'string' ? hit[1] : ''
128
+ }
129
+ let platform = ''
130
+ if (/OpenHarmony|HarmonyOS/i.test(ua)) {
131
+ const v = grab(/OpenHarmony\s+([\d.]+)/i) || grab(/HarmonyOS\s+([\d.]+)/i)
132
+ platform = v === '' ? 'HarmonyOS' : 'HarmonyOS ' + v
133
+ } else if (/Android/i.test(ua)) {
134
+ const v = grab(/Android\s+([\d.]+)/i)
135
+ platform = v === '' ? 'Android' : 'Android ' + v
136
+ } else if (/iPhone|iPad|iPod/i.test(ua)) {
137
+ platform = 'iOS'
138
+ } else if (/Windows NT 10/i.test(ua)) {
139
+ platform = 'Windows'
140
+ } else if (/Macintosh|Mac OS X/i.test(ua)) {
141
+ platform = 'macOS'
142
+ } else if (/Linux/i.test(ua)) {
143
+ platform = 'Linux'
144
+ }
145
+ let browser = ''
146
+ const browsers = [
147
+ [/HuaweiBrowser\/([\d.]+)/i, '华为浏览器'],
148
+ [/Quark\/([\d.]+)/i, '夸克'],
149
+ [/MicroMessenger\/([\d.]+)/i, '微信'],
150
+ [/Edg\/([\d.]+)/i, 'Edge'],
151
+ [/Firefox\/([\d.]+)/i, 'Firefox'],
152
+ [/Chrome\/([\d.]+)/i, 'Chrome'],
153
+ [/Version\/([\d.]+)[\s\S]*Safari/i, 'Safari'],
154
+ ]
155
+ for (const [re, name] of browsers) {
156
+ const v = grab(re)
157
+ if (v !== '') {
158
+ browser = name + ' ' + v.split('.')[0]
159
+ break
160
+ }
161
+ }
162
+ let model = ''
163
+ if (!/OpenHarmony|HarmonyOS/i.test(ua)) {
164
+ if (/iPhone/i.test(ua)) model = 'iPhone'
165
+ else if (/iPad/i.test(ua)) model = 'iPad'
166
+ else if (/Android/i.test(ua)) {
167
+ const hit = /Android[^;)]*;\s*(?:[A-Za-z]{2}-[A-Za-z]{2};\s*)?([^;)]+?)\s*(?:Build\/|\))/i.exec(ua)
168
+ const token = hit && typeof hit[1] === 'string' ? hit[1].replace(/^U;\s*/i, '').trim() : ''
169
+ if (token.length >= 2 && !/^wv$/i.test(token) && !/^Android$/i.test(token)) model = token.slice(0, 40)
170
+ }
171
+ }
172
+ const label = [platform, browser].filter((s) => s !== '').join(' · ')
173
+ return { model, platform, browser, label }
174
+ }
175
+
176
+ function createDevice(userAgent) {
177
+ if (devices.size >= MAX_DEVICES) return null
178
+ const now = Date.now()
179
+ const token = randomBytes(24).toString('hex')
180
+ const deviceId = randomBytes(8).toString('hex')
181
+ devices.set(deviceId, {
182
+ token,
183
+ name: '手机设备',
184
+ createdAt: now,
185
+ lastSeenAt: now,
186
+ userAgent: String(userAgent || '').slice(0, 180),
187
+ })
188
+ persistDevices(cfg.devicesFile, devices)
189
+ return { deviceId, token }
190
+ }
191
+
192
+ function fixedPairCode() {
193
+ return String(cfg.pairCode == null ? '' : cfg.pairCode).trim()
194
+ }
195
+
196
+ function issuePairToken() {
197
+ const fixed = fixedPairCode()
198
+ if (fixed !== '') {
199
+ // 固定码:永不过期,随时可配(不再随机、不再 5 分钟失效)。
200
+ state.pairToken = { code: fixed, expiresAt: Number.MAX_SAFE_INTEGER, fixed: true }
201
+ return state.pairToken
202
+ }
203
+ state.pairToken = { code: String(randomInt(0, 1e6)).padStart(6, '0'), expiresAt: Date.now() + TOKEN_TTL_MS, fixed: false }
204
+ return state.pairToken
205
+ }
206
+
207
+ function acceptPairCode(code, userAgent) {
208
+ const input = String(code == null ? '' : code).trim()
209
+ const fixed = fixedPairCode()
210
+ if (fixed !== '') {
211
+ if (input !== fixed) return null
212
+ return createDevice(userAgent)
213
+ }
214
+ const now = Date.now()
215
+ if (!state.pairToken || state.pairToken.code !== input || now > state.pairToken.expiresAt) {
216
+ if (state.pairToken && now > state.pairToken.expiresAt) state.pairToken = null
217
+ return null
218
+ }
219
+ const created = createDevice(userAgent)
220
+ if (created) state.pairToken = null
221
+ return created
222
+ }
223
+
224
+ return { deviceTokensOf, authenticatedDevice, describeDeviceUA, createDevice,
225
+ fixedPairCode, issuePairToken, acceptPairCode }
226
+ }
package/src/host/http.js CHANGED
@@ -1,11 +1,11 @@
1
- /**
2
- * HTTP 基础:响应写入、gzip/brotli、请求体读取、Cookie、回环/局域网地址。
3
- * 无闭包依赖(从原 index.js 顶层原样搬出)。
4
- */
5
- import { networkInterfaces } from 'node:os'
6
- import { gzipSync, brotliCompressSync, constants as zlibConstants } from 'node:zlib'
7
- import { COMPRESSIBLE_TYPE, MIN_COMPRESS_BYTES, SMALL_BODY } from './config.js'
8
-
1
+ /**
2
+ * HTTP 基础:响应写入、gzip/brotli、请求体读取、Cookie、回环/局域网地址。
3
+ * 无闭包依赖(从原 index.js 顶层原样搬出)。
4
+ */
5
+ import { networkInterfaces } from 'node:os'
6
+ import { gzipSync, brotliCompressSync, constants as zlibConstants } from 'node:zlib'
7
+ import { COMPRESSIBLE_TYPE, MIN_COMPRESS_BYTES, SMALL_BODY } from './config.js'
8
+
9
9
  export const compressCache = new WeakMap()
10
10
 
11
11
  export function compressFor(req, headers, body) {