dsh-plugin-mobile-gateway 0.7.7 → 0.8.1

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.
@@ -0,0 +1,114 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import crypto from 'node:crypto'
4
+ import { createReadStream } from 'node:fs'
5
+ import { execFile } from 'node:child_process'
6
+ import { promisify } from 'node:util'
7
+
8
+ const execFileAsync = promisify(execFile)
9
+ const CLOUDFLARED_VERSION = '2026.9.3'
10
+ const MAX_RELEASE_BYTES = 80 * 1024 * 1024
11
+ // GitHub's asset digests for the pinned Cloudflare release. The release API may
12
+ // return 403 even when the public release assets remain available.
13
+ const RELEASE_DIGESTS = Object.freeze({
14
+ 'cloudflared-darwin-amd64.tgz': 'd1155d0837487f261183b15c1eab6c4ebcad9dc49b94675f1524c3564cea3977',
15
+ 'cloudflared-darwin-arm64.tgz': '587c2cfb1c230fe36c7fa7727da78be459dae028cabe8c001291999350f07095',
16
+ 'cloudflared-windows-386.exe': '9b95ddc2eba67b86ed3dc4cc2a15881960563031b52ce564376af41fb91ad402',
17
+ 'cloudflared-windows-amd64.exe': 'f096265ec2fcbe9bb6e2d64268db167ced3fcbb83d894bdb9e2fcdb26f2ea7e2',
18
+ })
19
+ const BINARY_DIGESTS = Object.freeze({
20
+ 'cloudflared-darwin-amd64.tgz': 'ab588b3b4db9cdb4476c30a3db2a72635b1d8327d44741fee6799a0f37b0ec07',
21
+ 'cloudflared-darwin-arm64.tgz': '5472c1a01c84bc31b3021056a73b4e5774ddddefc572124ea8fdf6c340639f32',
22
+ 'cloudflared-windows-386.exe': RELEASE_DIGESTS['cloudflared-windows-386.exe'],
23
+ 'cloudflared-windows-amd64.exe': RELEASE_DIGESTS['cloudflared-windows-amd64.exe'],
24
+ })
25
+
26
+ export function cloudflaredAsset(platform = process.platform, arch = process.arch) {
27
+ if (platform === 'darwin' && (arch === 'arm64' || arch === 'x64')) {
28
+ return `cloudflared-darwin-${arch === 'x64' ? 'amd64' : 'arm64'}.tgz`
29
+ }
30
+ if (platform === 'win32' && arch === 'x64') return 'cloudflared-windows-amd64.exe'
31
+ if (platform === 'win32' && arch === 'ia32') return 'cloudflared-windows-386.exe'
32
+ throw new Error(`unsupported Cloudflare PC architecture: ${platform}/${arch}`)
33
+ }
34
+
35
+ async function readBounded(response, limit) {
36
+ if (!response.ok) throw new Error(`Cloudflare binary download returned HTTP ${response.status} from GitHub Releases`)
37
+ const length = Number(response.headers.get('content-length'))
38
+ if (length > limit) throw new Error('Cloudflare binary download exceeds the size limit')
39
+ const chunks = []
40
+ let size = 0
41
+ for await (const chunk of response.body) {
42
+ size += chunk.length
43
+ if (size > limit) throw new Error('Cloudflare binary download exceeds the size limit')
44
+ chunks.push(Buffer.from(chunk))
45
+ }
46
+ return Buffer.concat(chunks, size)
47
+ }
48
+
49
+ async function regularExecutable(file) {
50
+ try { return (await fs.lstat(file)).isFile() } catch { return false }
51
+ }
52
+
53
+ async function sha256File(file) {
54
+ const hash = crypto.createHash('sha256')
55
+ for await (const chunk of createReadStream(file)) hash.update(chunk)
56
+ return hash.digest('hex')
57
+ }
58
+
59
+ // The default executable stays in DSH's private cache. External binaries are
60
+ // used only when an explicit path is configured.
61
+ export async function ensureCloudflared({ cacheDir, executable = 'cloudflared',
62
+ platform = process.platform, arch = process.arch, fetchImpl = fetch,
63
+ releaseDigests = RELEASE_DIGESTS, binaryDigests = BINARY_DIGESTS } = {}) {
64
+ if (executable !== 'cloudflared') return executable
65
+ const assetName = cloudflaredAsset(platform, arch)
66
+ const destination = path.join(cacheDir, `${CLOUDFLARED_VERSION}-${platform}-${arch}${platform === 'win32' ? '.exe' : ''}`)
67
+ await fs.mkdir(cacheDir, { recursive: true, mode: 0o700 })
68
+ await fs.chmod(cacheDir, 0o700)
69
+ const expectedDigest = releaseDigests[assetName]
70
+ const expectedBinaryDigest = binaryDigests[assetName]
71
+ if (!/^[0-9a-f]{64}$/.test(expectedDigest || '') ||
72
+ !/^[0-9a-f]{64}$/.test(expectedBinaryDigest || '')) {
73
+ throw new Error('Cloudflare release has no verifiable binary for this PC')
74
+ }
75
+ if (await regularExecutable(destination)) {
76
+ if (await sha256File(destination) === expectedBinaryDigest) {
77
+ await fs.chmod(destination, 0o700)
78
+ return destination
79
+ }
80
+ await fs.rm(destination)
81
+ }
82
+
83
+ const expectedUrl = `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/${assetName}`
84
+ const response = await fetchImpl(expectedUrl, {
85
+ headers: { 'User-Agent': 'dsh-mobile-gateway' },
86
+ signal: AbortSignal.timeout(120_000),
87
+ })
88
+ const archive = await readBounded(response, MAX_RELEASE_BYTES)
89
+ const actual = crypto.createHash('sha256').update(archive).digest('hex')
90
+ if (actual !== expectedDigest) {
91
+ throw new Error('Cloudflare binary checksum mismatch')
92
+ }
93
+
94
+ const stage = await fs.mkdtemp(path.join(cacheDir, '.cloudflared-'))
95
+ try {
96
+ let source
97
+ if (assetName.endsWith('.tgz')) {
98
+ const archiveFile = path.join(stage, assetName)
99
+ await fs.writeFile(archiveFile, archive, { mode: 0o600 })
100
+ await execFileAsync('tar', ['-xzf', archiveFile, '-C', stage], { timeout: 30_000 })
101
+ source = path.join(stage, 'cloudflared')
102
+ if (!await regularExecutable(source)) throw new Error('Cloudflare archive contains no cloudflared executable')
103
+ } else {
104
+ source = path.join(stage, 'cloudflared.exe')
105
+ await fs.writeFile(source, archive, { mode: 0o700 })
106
+ }
107
+ if (await sha256File(source) !== expectedBinaryDigest) throw new Error('Cloudflare executable checksum mismatch')
108
+ await fs.chmod(source, 0o700)
109
+ if (!await regularExecutable(destination)) await fs.rename(source, destination)
110
+ return destination
111
+ } finally {
112
+ await fs.rm(stage, { recursive: true, force: true })
113
+ }
114
+ }
@@ -13,13 +13,13 @@ function requireArray(value, endpoint) {
13
13
  return value
14
14
  }
15
15
 
16
- export const DSH_VERSION = '0.1.5-rc.2'
17
- export const SESSION_FORMAT_VERSION = 3
16
+ export const DSH_VERSION = '0.1.7-rc.2'
17
+ export const SESSION_FORMAT_VERSION = 4
18
18
 
19
19
  export function readHistoryRecords(records) {
20
20
  return requireArray(records, 'session history').map(record => {
21
21
  const value = requireRecord(record, 'session history record')
22
- if (value.type !== 'event') throw new Error('DSH 0.1.5-rc.2 history requires event records')
22
+ if (value.type !== 'event') throw new Error('DSH Session V4 history requires event records')
23
23
  const event = requireRecord(value.event, 'session history event')
24
24
  if (!Number.isSafeInteger(event.seq) || event.seq < 0) throw new Error('invalid history sequence')
25
25
  return event
@@ -31,7 +31,7 @@ export function readSessionSnapshot(frame, sessionId) {
31
31
  if (frame.type !== 'snapshot' || !Number.isSafeInteger(frame.cursor) || frame.cursor < -1
32
32
  || frame.header?.id !== sessionId) throw new Error('session/follow returned an invalid snapshot')
33
33
  if (frame.header.version !== SESSION_FORMAT_VERSION) {
34
- throw Object.assign(new Error('mobile-gateway requires DSH 0.1.5-rc.2 Session format 3'), {
34
+ throw Object.assign(new Error(`mobile-gateway requires Session format ${SESSION_FORMAT_VERSION}; received ${frame.header.version}`), {
35
35
  code: 'unsupported-session-format',
36
36
  })
37
37
  }
@@ -66,7 +66,7 @@ function requestArgs(request) {
66
66
  return { request }
67
67
  }
68
68
 
69
- // The 0.1.5-rc.2 SessionController names its reserved list argument
69
+ // The SessionController names its reserved list argument
70
70
  // `_request`. Typert descriptors preserve that source parameter name exactly,
71
71
  // so this endpoint cannot share the normal `{ request }` wrapper.
72
72
  function sessionListArgs(request) {
@@ -160,7 +160,7 @@ export function createDshHostAdapter(typertGateway) {
160
160
 
161
161
  const commands = {
162
162
  list: (sessionId, signal) => invoke('commands', 'list', { agentId: sessionId }, signal),
163
- // DSH 0.1.5-rc.2 accepts `submittedAttachments`. The parsed
163
+ // DSH accepts `submittedAttachments`. The parsed
164
164
  // wire images already carry the { type: 'image', ... } shape it expects, so
165
165
  // only the field name changes.
166
166
  execute: (sessionId, line, attachments, signal) => invoke(
@@ -253,6 +253,20 @@ export function createDshHostAdapter(typertGateway) {
253
253
  ...(payload.expectedRevision === undefined ? {} : { expectedRevision: payload.expectedRevision }),
254
254
  }, signal),
255
255
  },
256
+ permissionPresets: {
257
+ catalog: (_payload = {}, signal) => invoke('permissionPresets', 'catalog', {}, signal),
258
+ },
259
+ schedule: {
260
+ async catalog(_payload = {}, signal) {
261
+ return { items: requireArray(await invoke('schedule', 'catalog', {}, signal), 'schedule/catalog') }
262
+ },
263
+ async list(payload, signal) {
264
+ return { items: requireArray(await invoke('schedule', 'list', requestArgs(payload), signal), 'schedule/list') }
265
+ },
266
+ history: (payload, signal) => invoke('schedule', 'history', requestArgs(payload), signal),
267
+ update: (payload, signal) => invoke('schedule', 'update', requestArgs(payload), signal),
268
+ delete: (payload, signal) => invoke('schedule', 'delete', requestArgs(payload), signal),
269
+ },
256
270
  skills: {
257
271
  list: (payload, signal) => invoke('skills', 'list', requestArgs(payload), signal),
258
272
  },
@@ -0,0 +1,36 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { realpathSync } from 'node:fs'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ export function webRestartAvailable(profile, runtime = process) {
6
+ if (profile?.name !== 'web' || !runtime.argv?.[1]) return false
7
+ try {
8
+ return /[/\\]@deepseek-ai[/\\]dsh[/\\]lib[/\\]bin\.js$/.test(realpathSync(runtime.argv[1]))
9
+ } catch {
10
+ return false
11
+ }
12
+ }
13
+
14
+ export async function prepareWebRestart(runtime = process, spawnChild = spawn) {
15
+ const argv = runtime.argv.slice(1)
16
+ // DSH Web opens a browser on every boot by default. This restart was
17
+ // initiated from an existing tab, so keep that tab and suppress the handoff.
18
+ const terminator = argv.indexOf('--')
19
+ const flagEnd = terminator < 0 ? argv.length : terminator
20
+ if (!argv.slice(0, flagEnd).includes('--no-open')) argv.splice(flagEnd, 0, '--no-open')
21
+ const payload = Buffer.from(JSON.stringify({
22
+ pid: runtime.pid,
23
+ execPath: runtime.execPath,
24
+ execArgv: runtime.execArgv,
25
+ argv,
26
+ cwd: runtime.cwd(),
27
+ })).toString('base64url')
28
+ const helper = spawnChild(runtime.execPath, [fileURLToPath(new URL('./web-restart.mjs', import.meta.url)), payload], {
29
+ cwd: runtime.cwd(), env: runtime.env, detached: true, stdio: 'ignore',
30
+ })
31
+ await new Promise((resolve, reject) => {
32
+ helper.once('spawn', resolve)
33
+ helper.once('error', reject)
34
+ })
35
+ helper.unref()
36
+ }