@kinocode/dsh-mobile-bridge 0.0.0-stage → 0.3.2
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 -0
- package/README.md +47 -2
- package/cordis.patch.yml +16 -0
- package/lib/auth.mjs +180 -0
- package/lib/client.js +219 -0
- package/lib/control.mjs +226 -0
- package/lib/desktop-api.mjs +102 -0
- package/lib/gateway-client.mjs +236 -0
- package/lib/gateway.mjs +319 -0
- package/lib/index.mjs +246 -0
- package/lib/preview.mjs +149 -0
- package/lib/push.mjs +173 -0
- package/lib/relay-tunnel.mjs +256 -0
- package/lib/sessions.mjs +131 -0
- package/lib/state.mjs +187 -0
- package/lib/tls-store.mjs +107 -0
- package/lib/upstream.mjs +61 -0
- package/lib/vendor/qrcode.mjs +2291 -0
- package/lib/web-sessions.mjs +113 -0
- package/lib/x509.mjs +228 -0
- package/package.json +63 -4
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// WebView sessions for the gateway: a signed cookie value bound to one registered device.
|
|
2
|
+
// The value is base64url(`deviceId.expiresAt`) followed by a 43-character HMAC-SHA256 (key derived
|
|
3
|
+
// from the host secret), only [A-Za-z0-9_-] as the app expects,
|
|
4
|
+
// so sessions survive a DSH restart; a removed device's sessions stop working at once because
|
|
5
|
+
// `isActive` consults the device registry and `revokeDevice` remembers it for this process.
|
|
6
|
+
|
|
7
|
+
import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto'
|
|
8
|
+
|
|
9
|
+
export const SESSION_TTL_MS = 12 * 60 * 60 * 1000
|
|
10
|
+
export const SESSION_COOKIE = 'dshmb_session'
|
|
11
|
+
const VALUE = /^([A-Za-z0-9_-]{4,120})([A-Za-z0-9_-]{43})$/
|
|
12
|
+
const PAYLOAD = /^([A-Za-z0-9_-]{1,64})\.(\d{1,15})$/
|
|
13
|
+
|
|
14
|
+
export class WebSessions {
|
|
15
|
+
/**
|
|
16
|
+
* @param {{now?: () => number, secret?: () => string, isActive?: (deviceId: string) => boolean}} [options]
|
|
17
|
+
* secret - host secret the signing key is derived from (default: a random per-process key)
|
|
18
|
+
* isActive - whether a device is still registered (default: always)
|
|
19
|
+
*/
|
|
20
|
+
constructor(options = {}) {
|
|
21
|
+
this.now = options.now ?? Date.now
|
|
22
|
+
const fallback = randomBytes(32).toString('base64url')
|
|
23
|
+
this.secret = options.secret ?? (() => fallback)
|
|
24
|
+
this.isActive = options.isActive ?? (() => true)
|
|
25
|
+
/** devices removed while this process runs */
|
|
26
|
+
this.revoked = new Set()
|
|
27
|
+
/** deviceId -> sockets to close when the device is removed */
|
|
28
|
+
this.sockets = new Map()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** @param {string} payload */
|
|
32
|
+
mac(payload) {
|
|
33
|
+
const key = createHash('sha256').update(`dshmb-web-session\n${this.secret()}`).digest()
|
|
34
|
+
return createHmac('sha256', key).update(payload).digest('base64url')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} deviceId
|
|
39
|
+
* @returns {{cookieName: string, value: string, expiresAt: number}}
|
|
40
|
+
*/
|
|
41
|
+
create(deviceId) {
|
|
42
|
+
this.revoked.delete(deviceId)
|
|
43
|
+
const expiresAt = this.now() + SESSION_TTL_MS
|
|
44
|
+
const payload = `${deviceId}.${expiresAt}`
|
|
45
|
+
return { cookieName: SESSION_COOKIE, value: `${Buffer.from(payload).toString('base64url')}${this.mac(payload)}`, expiresAt }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Device id for a Cookie header, or undefined.
|
|
50
|
+
* @param {string|undefined} cookieHeader
|
|
51
|
+
*/
|
|
52
|
+
check(cookieHeader) {
|
|
53
|
+
const value = readCookie(cookieHeader, SESSION_COOKIE)
|
|
54
|
+
const match = value === undefined ? null : VALUE.exec(value)
|
|
55
|
+
if (match === null) return undefined
|
|
56
|
+
const payload = Buffer.from(match[1], 'base64url').toString()
|
|
57
|
+
const parts = PAYLOAD.exec(payload)
|
|
58
|
+
if (parts === null) return undefined
|
|
59
|
+
const [, deviceId, expires] = parts
|
|
60
|
+
const mac = match[2]
|
|
61
|
+
const expected = Buffer.from(this.mac(payload))
|
|
62
|
+
const given = Buffer.from(mac)
|
|
63
|
+
if (given.length !== expected.length || !timingSafeEqual(given, expected)) return undefined
|
|
64
|
+
if (Number(expires) <= this.now()) return undefined
|
|
65
|
+
if (this.revoked.has(deviceId) || !this.isActive(deviceId)) return undefined
|
|
66
|
+
return deviceId
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Track a proxied WebSocket so removing the device closes it.
|
|
71
|
+
* @param {string} deviceId
|
|
72
|
+
* @param {import('node:net').Socket} socket
|
|
73
|
+
*/
|
|
74
|
+
track(deviceId, socket) {
|
|
75
|
+
let set = this.sockets.get(deviceId)
|
|
76
|
+
if (set === undefined) {
|
|
77
|
+
set = new Set()
|
|
78
|
+
this.sockets.set(deviceId, set)
|
|
79
|
+
}
|
|
80
|
+
set.add(socket)
|
|
81
|
+
socket.once('close', () => set.delete(socket))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** End every session and socket of a device. @param {string} deviceId */
|
|
85
|
+
revokeDevice(deviceId) {
|
|
86
|
+
this.revoked.add(deviceId)
|
|
87
|
+
for (const socket of this.sockets.get(deviceId) ?? []) socket.destroy()
|
|
88
|
+
this.sockets.delete(deviceId)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {string|undefined} header
|
|
94
|
+
* @param {string} name
|
|
95
|
+
*/
|
|
96
|
+
export function readCookie(header, name) {
|
|
97
|
+
if (typeof header !== 'string') return undefined
|
|
98
|
+
for (const part of header.split(';')) {
|
|
99
|
+
const eq = part.indexOf('=')
|
|
100
|
+
if (eq > 0 && part.slice(0, eq).trim() === name) return part.slice(eq + 1).trim()
|
|
101
|
+
}
|
|
102
|
+
return undefined
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Cookie header without one cookie.
|
|
107
|
+
* @param {string|undefined} header
|
|
108
|
+
* @param {string} name
|
|
109
|
+
*/
|
|
110
|
+
export function withoutCookie(header, name) {
|
|
111
|
+
if (typeof header !== 'string') return ''
|
|
112
|
+
return header.split(';').map(p => p.trim()).filter(p => p !== '' && !p.startsWith(`${name}=`)).join('; ')
|
|
113
|
+
}
|
package/lib/x509.mjs
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// Minimal X.509 v3 certificate builder for the gateway's private CA and server certificate.
|
|
2
|
+
// Only what the gateway needs: EC P-256 keys, ECDSA-SHA256 signatures, basicConstraints,
|
|
3
|
+
// keyUsage, extKeyUsage(serverAuth), subjectAltName (IP + DNS), subject/authority key ids.
|
|
4
|
+
// Verified with node:crypto X509Certificate after creation.
|
|
5
|
+
|
|
6
|
+
import { createHash, generateKeyPairSync, randomBytes, sign, X509Certificate } from 'node:crypto'
|
|
7
|
+
import { isIP, isIPv4 } from 'node:net'
|
|
8
|
+
|
|
9
|
+
// ---- DER primitives ---------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
function length(n) {
|
|
12
|
+
if (n < 0x80) return Buffer.from([n])
|
|
13
|
+
const bytes = []
|
|
14
|
+
while (n > 0) {
|
|
15
|
+
bytes.unshift(n & 0xff)
|
|
16
|
+
n >>>= 8
|
|
17
|
+
}
|
|
18
|
+
return Buffer.from([0x80 | bytes.length, ...bytes])
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function tlv(tag, content) {
|
|
22
|
+
const body = Buffer.isBuffer(content) ? content : Buffer.concat(content)
|
|
23
|
+
return Buffer.concat([Buffer.from([tag]), length(body.length), body])
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const seq = (...items) => tlv(0x30, items)
|
|
27
|
+
const set = (...items) => tlv(0x31, items)
|
|
28
|
+
const octets = (buf) => tlv(0x04, buf)
|
|
29
|
+
const bool = (value) => tlv(0x01, Buffer.from([value ? 0xff : 0x00]))
|
|
30
|
+
const utf8 = (text) => tlv(0x0c, Buffer.from(text, 'utf8'))
|
|
31
|
+
const ia5 = (text, tag = 0x16) => tlv(tag, Buffer.from(text, 'ascii'))
|
|
32
|
+
const bitString = (buf, unusedBits = 0) => tlv(0x03, Buffer.concat([Buffer.from([unusedBits]), buf]))
|
|
33
|
+
const explicit = (n, inner) => tlv(0xa0 | n, inner)
|
|
34
|
+
|
|
35
|
+
function integer(buf) {
|
|
36
|
+
let bytes = Buffer.from(buf)
|
|
37
|
+
while (bytes.length > 1 && bytes[0] === 0 && (bytes[1] & 0x80) === 0) bytes = bytes.subarray(1)
|
|
38
|
+
if (bytes[0] & 0x80) bytes = Buffer.concat([Buffer.from([0]), bytes])
|
|
39
|
+
return tlv(0x02, bytes)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function oid(dotted) {
|
|
43
|
+
const parts = dotted.split('.').map(Number)
|
|
44
|
+
const out = [40 * parts[0] + parts[1]]
|
|
45
|
+
for (const part of parts.slice(2)) {
|
|
46
|
+
const stack = [part & 0x7f]
|
|
47
|
+
let value = part >>> 7
|
|
48
|
+
while (value > 0) {
|
|
49
|
+
stack.unshift((value & 0x7f) | 0x80)
|
|
50
|
+
value >>>= 7
|
|
51
|
+
}
|
|
52
|
+
out.push(...stack)
|
|
53
|
+
}
|
|
54
|
+
return tlv(0x06, Buffer.from(out))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function time(date) {
|
|
58
|
+
const iso = date.toISOString().replace(/[-:T]/g, '').slice(0, 14) // YYYYMMDDHHMMSS
|
|
59
|
+
return date.getUTCFullYear() < 2050 ? ia5(`${iso.slice(2)}Z`, 0x17) : ia5(`${iso}Z`, 0x18)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---- OIDs ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
const OID = {
|
|
65
|
+
ecdsaWithSHA256: '1.2.840.10045.4.3.2',
|
|
66
|
+
commonName: '2.5.4.3',
|
|
67
|
+
organization: '2.5.4.10',
|
|
68
|
+
subjectKeyIdentifier: '2.5.29.14',
|
|
69
|
+
keyUsage: '2.5.29.15',
|
|
70
|
+
subjectAltName: '2.5.29.17',
|
|
71
|
+
basicConstraints: '2.5.29.19',
|
|
72
|
+
authorityKeyIdentifier: '2.5.29.35',
|
|
73
|
+
extKeyUsage: '2.5.29.37',
|
|
74
|
+
serverAuth: '1.3.6.1.5.5.7.3.1',
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const signatureAlgorithm = () => seq(oid(OID.ecdsaWithSHA256))
|
|
78
|
+
|
|
79
|
+
function name(commonName) {
|
|
80
|
+
return seq(
|
|
81
|
+
set(seq(oid(OID.organization), utf8('DSH Mobile'))),
|
|
82
|
+
set(seq(oid(OID.commonName), utf8(commonName))),
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function extension(id, critical, valueDer) {
|
|
87
|
+
return critical ? seq(oid(id), bool(true), octets(valueDer)) : seq(oid(id), octets(valueDer))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** keyUsage bits: digitalSignature=0, keyCertSign=5, cRLSign=6. */
|
|
91
|
+
function keyUsage(bits) {
|
|
92
|
+
let byte = 0
|
|
93
|
+
for (const bit of bits) byte |= 0x80 >> bit
|
|
94
|
+
let unused = 0
|
|
95
|
+
while (unused < 7 && ((byte >> unused) & 1) === 0) unused += 1
|
|
96
|
+
return bitString(Buffer.from([byte]), unused)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function keyIdentifier(spkiDer) {
|
|
100
|
+
// RFC 5280 method 1: SHA-1 of the subjectPublicKey BIT STRING contents. For P-256 SPKI the
|
|
101
|
+
// uncompressed point is the last 65 bytes.
|
|
102
|
+
return createHash('sha1').update(spkiDer.subarray(spkiDer.length - 65)).digest()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function ipBytes(ip) {
|
|
106
|
+
if (isIPv4(ip)) return Buffer.from(ip.split('.').map(Number))
|
|
107
|
+
throw new Error(`unsupported IP SAN ${ip}`)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function serial() {
|
|
111
|
+
const bytes = randomBytes(16)
|
|
112
|
+
bytes[0] &= 0x7f
|
|
113
|
+
if (bytes[0] === 0) bytes[0] = 1
|
|
114
|
+
return bytes
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @param {object} o
|
|
119
|
+
* @param {string} o.subjectCN
|
|
120
|
+
* @param {string} o.issuerCN
|
|
121
|
+
* @param {Buffer} o.subjectSpki - SPKI DER of the subject key
|
|
122
|
+
* @param {Buffer} o.issuerSpki - SPKI DER of the issuer key (for the authority key id)
|
|
123
|
+
* @param {import('node:crypto').KeyObject} o.issuerKey - private key that signs
|
|
124
|
+
* @param {Date} o.notBefore
|
|
125
|
+
* @param {Date} o.notAfter
|
|
126
|
+
* @param {Buffer[]} o.extensions
|
|
127
|
+
* @returns {Buffer} certificate DER
|
|
128
|
+
*/
|
|
129
|
+
function build(o) {
|
|
130
|
+
const tbs = seq(
|
|
131
|
+
explicit(0, integer(Buffer.from([2]))),
|
|
132
|
+
integer(serial()),
|
|
133
|
+
signatureAlgorithm(),
|
|
134
|
+
name(o.issuerCN),
|
|
135
|
+
seq(time(o.notBefore), time(o.notAfter)),
|
|
136
|
+
name(o.subjectCN),
|
|
137
|
+
o.subjectSpki,
|
|
138
|
+
explicit(3, seq(...o.extensions)),
|
|
139
|
+
)
|
|
140
|
+
const signature = sign('sha256', tbs, { key: o.issuerKey, dsaEncoding: 'der' })
|
|
141
|
+
return seq(tbs, signatureAlgorithm(), bitString(signature))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const DAY = 24 * 60 * 60 * 1000
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Create a self-signed EC P-256 CA.
|
|
148
|
+
* @param {{commonName?: string, days?: number, now?: Date}} [options]
|
|
149
|
+
* @returns {{certDer: Buffer, certPem: string, keyPem: string, fingerprint: string}}
|
|
150
|
+
*/
|
|
151
|
+
export function createCa(options = {}) {
|
|
152
|
+
const now = options.now ?? new Date()
|
|
153
|
+
const { publicKey, privateKey } = generateKeyPairSync('ec', { namedCurve: 'prime256v1' })
|
|
154
|
+
const spki = publicKey.export({ format: 'der', type: 'spki' })
|
|
155
|
+
const cn = options.commonName ?? 'DSH Mobile Gateway CA'
|
|
156
|
+
const certDer = build({
|
|
157
|
+
subjectCN: cn,
|
|
158
|
+
issuerCN: cn,
|
|
159
|
+
subjectSpki: spki,
|
|
160
|
+
issuerSpki: spki,
|
|
161
|
+
issuerKey: privateKey,
|
|
162
|
+
notBefore: new Date(now.getTime() - DAY),
|
|
163
|
+
notAfter: new Date(now.getTime() + (options.days ?? 3650) * DAY),
|
|
164
|
+
extensions: [
|
|
165
|
+
extension(OID.basicConstraints, true, seq(bool(true), integer(Buffer.from([0])))),
|
|
166
|
+
extension(OID.keyUsage, true, keyUsage([5, 6])),
|
|
167
|
+
extension(OID.subjectKeyIdentifier, false, octets(keyIdentifier(spki))),
|
|
168
|
+
],
|
|
169
|
+
})
|
|
170
|
+
const cert = new X509Certificate(certDer)
|
|
171
|
+
if (!cert.ca || !cert.verify(publicKey)) throw new Error('generated CA failed verification')
|
|
172
|
+
return { certDer, certPem: toPem(certDer), keyPem: privateKey.export({ format: 'pem', type: 'pkcs8' }), fingerprint: fingerprintOf(certDer) }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Issue a server certificate signed by the CA.
|
|
177
|
+
* @param {object} o
|
|
178
|
+
* @param {string} o.caCertPem
|
|
179
|
+
* @param {import('node:crypto').KeyObject} o.caKey
|
|
180
|
+
* @param {string[]} o.ips - IPv4 addresses for IP SANs
|
|
181
|
+
* @param {string[]} o.dnsNames
|
|
182
|
+
* @param {number} [o.days]
|
|
183
|
+
* @param {Date} [o.now]
|
|
184
|
+
* @returns {{certPem: string, keyPem: string}}
|
|
185
|
+
*/
|
|
186
|
+
export function issueServerCertificate(o) {
|
|
187
|
+
const now = o.now ?? new Date()
|
|
188
|
+
const ca = new X509Certificate(o.caCertPem)
|
|
189
|
+
const caSpki = ca.publicKey.export({ format: 'der', type: 'spki' })
|
|
190
|
+
const { publicKey, privateKey } = generateKeyPairSync('ec', { namedCurve: 'prime256v1' })
|
|
191
|
+
const spki = publicKey.export({ format: 'der', type: 'spki' })
|
|
192
|
+
const ips = [...new Set(o.ips.filter(ip => isIP(ip) === 4))]
|
|
193
|
+
const dnsNames = [...new Set(o.dnsNames.filter(n => /^[A-Za-z0-9.-]{1,253}$/.test(n)))]
|
|
194
|
+
if (ips.length + dnsNames.length === 0) throw new Error('server certificate needs at least one SAN')
|
|
195
|
+
const sans = [...ips.map(ip => tlv(0x87, ipBytes(ip))), ...dnsNames.map(n => ia5(n, 0x82))]
|
|
196
|
+
const certDer = build({
|
|
197
|
+
subjectCN: ips[0] ?? dnsNames[0],
|
|
198
|
+
issuerCN: ca.subject.split('\n').find(l => l.startsWith('CN='))?.slice(3) ?? 'DSH Mobile Gateway CA',
|
|
199
|
+
subjectSpki: spki,
|
|
200
|
+
issuerSpki: caSpki,
|
|
201
|
+
issuerKey: o.caKey,
|
|
202
|
+
notBefore: new Date(now.getTime() - DAY),
|
|
203
|
+
notAfter: new Date(now.getTime() + (o.days ?? 397) * DAY),
|
|
204
|
+
extensions: [
|
|
205
|
+
extension(OID.basicConstraints, true, seq()),
|
|
206
|
+
extension(OID.keyUsage, true, keyUsage([0])),
|
|
207
|
+
extension(OID.extKeyUsage, false, seq(oid(OID.serverAuth))),
|
|
208
|
+
extension(OID.subjectAltName, false, seq(...sans)),
|
|
209
|
+
extension(OID.subjectKeyIdentifier, false, octets(keyIdentifier(spki))),
|
|
210
|
+
extension(OID.authorityKeyIdentifier, false, seq(tlv(0x80, keyIdentifier(caSpki)))),
|
|
211
|
+
],
|
|
212
|
+
})
|
|
213
|
+
const cert = new X509Certificate(certDer)
|
|
214
|
+
if (!cert.verify(ca.publicKey) || cert.ca) throw new Error('generated server certificate failed verification')
|
|
215
|
+
for (const ip of ips) if (cert.checkIP(ip) === undefined) throw new Error(`server certificate missing IP SAN ${ip}`)
|
|
216
|
+
return { certPem: toPem(certDer), keyPem: privateKey.export({ format: 'pem', type: 'pkcs8' }) }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** @param {Buffer} der */
|
|
220
|
+
export function fingerprintOf(der) {
|
|
221
|
+
return createHash('sha256').update(der).digest('hex')
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** @param {Buffer} der */
|
|
225
|
+
export function toPem(der) {
|
|
226
|
+
const b64 = der.toString('base64').match(/.{1,64}/g).join('\n')
|
|
227
|
+
return `-----BEGIN CERTIFICATE-----\n${b64}\n-----END CERTIFICATE-----\n`
|
|
228
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,65 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kinocode/dsh-mobile-bridge",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.2",
|
|
4
|
+
"description": "DeepSeek Harness host plugin for the DSH Mobile Android app: pair with a QR, use DSH on the phone through its own HTTPS gateway, answer approvals and questions from notifications, push turn completion.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"deepseek-harness",
|
|
7
|
+
"dsh",
|
|
8
|
+
"dsh-plugin",
|
|
9
|
+
"plugin",
|
|
10
|
+
"mobile",
|
|
11
|
+
"android",
|
|
12
|
+
"push-notifications",
|
|
13
|
+
"approval",
|
|
14
|
+
"relay"
|
|
15
|
+
],
|
|
16
|
+
"author": "kinocode",
|
|
17
|
+
"homepage": "https://www.npmjs.com/package/@kinocode/dsh-mobile-bridge",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "lib/index.mjs",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": "./lib/index.mjs",
|
|
22
|
+
"./client": "./lib/client.js",
|
|
23
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
24
|
+
"./package.json": "./package.json"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"LICENSE",
|
|
28
|
+
"README.md",
|
|
29
|
+
"cordis.patch.yml",
|
|
30
|
+
"lib/**/*.mjs",
|
|
31
|
+
"lib/client.js",
|
|
32
|
+
"lib/vendor/*.mjs"
|
|
33
|
+
],
|
|
34
|
+
"dsh": {
|
|
35
|
+
"bundle": {
|
|
36
|
+
"patch": "./cordis.patch.yml"
|
|
37
|
+
},
|
|
38
|
+
"client": {
|
|
39
|
+
"inject": [
|
|
40
|
+
"@deepseek-ai/dsh-client-ui-workspace",
|
|
41
|
+
"@deepseek-ai/dsh-client-ui-sidebar"
|
|
42
|
+
],
|
|
43
|
+
"platform": "web"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@deepseek-ai/cordis": "~4.0.4"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"@deepseek-ai/cordis": {
|
|
51
|
+
"optional": true
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=22.4",
|
|
56
|
+
"dsh": ">=0.2.1-rc.2 <0.3.0"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"test": "node --test test/*.test.mjs"
|
|
60
|
+
},
|
|
61
|
+
"license": "MIT",
|
|
62
|
+
"publishConfig": {
|
|
63
|
+
"access": "public"
|
|
64
|
+
}
|
|
65
|
+
}
|