@lievclouds/dsh-cortex 0.4.4 → 1.0.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.

Potentially problematic release.


This version of @lievclouds/dsh-cortex might be problematic. Click here for more details.

Files changed (58) hide show
  1. package/README.md +1 -1
  2. package/client.js +428 -406
  3. package/index.js +7 -0
  4. package/locale/en.json +6 -6
  5. package/locale/zh.json +6 -6
  6. package/package.json +115 -106
  7. package/src/cortex-ui/dsh/api.ts +5 -2
  8. package/src/cortex-ui/dsh/css-overrides.css +8 -0
  9. package/src/cortex-ui/renderer/App.tsx +39 -0
  10. package/src/cortex-ui/renderer/components/asset/AssetDetail.tsx +9 -1
  11. package/src/cortex-ui/renderer/components/miniapp/FileTree.tsx +161 -0
  12. package/src/cortex-ui/renderer/components/miniapp/MiniappCard.tsx +263 -0
  13. package/src/cortex-ui/renderer/components/miniapp/MiniappDetail.tsx +1493 -0
  14. package/src/cortex-ui/renderer/components/retest/CurlImportDialog.tsx +158 -0
  15. package/src/cortex-ui/renderer/components/retest/RepeaterCollectionsDialog.tsx +327 -0
  16. package/src/cortex-ui/renderer/components/retest/RepeaterHistoryDialog.tsx +242 -0
  17. package/src/cortex-ui/renderer/components/retest/RepeaterPanel.tsx +333 -148
  18. package/src/cortex-ui/renderer/components/retest/YakitSendPanel.tsx +411 -0
  19. package/src/cortex-ui/renderer/components/vuln/DirectoryPickerDialog.tsx +187 -0
  20. package/src/cortex-ui/renderer/components/vuln/ExportReportDialog.tsx +173 -59
  21. package/src/cortex-ui/renderer/components/vuln/VulnDetail.tsx +16 -4
  22. package/src/cortex-ui/renderer/i18n/messages/common.ts +2 -0
  23. package/src/cortex-ui/renderer/i18n/messages/fuzzer.ts +32 -0
  24. package/src/cortex-ui/renderer/i18n/messages/index.ts +5 -1
  25. package/src/cortex-ui/renderer/i18n/messages/miniapp.ts +392 -0
  26. package/src/cortex-ui/renderer/i18n/messages/retest.ts +146 -2
  27. package/src/cortex-ui/renderer/i18n/messages/vuln.ts +26 -6
  28. package/src/cortex-ui/renderer/lib/curl.ts +635 -0
  29. package/src/cortex-ui/renderer/lib/miniappApi.ts +459 -0
  30. package/src/cortex-ui/renderer/lib/navigation.ts +13 -1
  31. package/src/cortex-ui/renderer/lib/repeaterRaw.ts +102 -0
  32. package/src/cortex-ui/renderer/lib/repeaterStoreApi.ts +163 -0
  33. package/src/cortex-ui/renderer/lib/retestResponse.ts +64 -0
  34. package/src/cortex-ui/renderer/lib/transport/httpClient.ts +1 -1
  35. package/src/cortex-ui/renderer/pages/DashboardPage.tsx +3 -3
  36. package/src/cortex-ui/renderer/pages/FuzzerPage.tsx +212 -0
  37. package/src/cortex-ui/renderer/pages/MiniappPage.tsx +664 -0
  38. package/src/cortex-ui/shared/ipc.ts +3 -4
  39. package/src/cortex-ui/shared/types.ts +9 -0
  40. package/src/host/cortex/fs.js +72 -0
  41. package/src/host/cortex/index.js +49 -1
  42. package/src/host/cortex/repeater-store.js +411 -0
  43. package/src/host/cortex/repeater.js +15 -2
  44. package/src/host/cortex/settings.js +54 -0
  45. package/src/host/cortex/tools.js +2 -0
  46. package/src/host/cortex/vulns.js +36 -13
  47. package/src/host/miniapp/analyze.js +1078 -0
  48. package/src/host/miniapp/assets-import.js +294 -0
  49. package/src/host/miniapp/detect.js +426 -0
  50. package/src/host/miniapp/format.js +263 -0
  51. package/src/host/miniapp/roots.js +360 -0
  52. package/src/host/miniapp/routes.js +304 -0
  53. package/src/host/miniapp/service.js +559 -0
  54. package/src/host/miniapp/store.js +756 -0
  55. package/src/host/miniapp/tools.js +335 -0
  56. package/src/host/miniapp/unpack.js +1010 -0
  57. package/src/host/miniapp/wxapkg.js +416 -0
  58. package/src/host/reports-office.js +341 -0
@@ -0,0 +1,416 @@
1
+ /**
2
+ * wxapkg container reader and classic `V1MMWX` decryptor.
3
+ *
4
+ * WeChat desktop stores mini-program packages as `*.wxapkg` files. Two formats
5
+ * show up locally:
6
+ *
7
+ * - `plain` byte 0 is `0xBE`; the index/table is in clear text.
8
+ * - `v1mmwx` 6-byte magic `V1MMWX`; the body is AES-256-CBC encrypted with
9
+ * a key derived from the appid (see `decryptV1MMWX`).
10
+ *
11
+ * The decryption algorithm below is the classic PC WeChat scheme verified
12
+ * against local WeChat 4.x samples (36/37; the remaining `publicLib` package
13
+ * uses an unknown key and therefore degrades to `unsupported`, never crashes).
14
+ *
15
+ * Newer client versions are expected to change the header/key derivation. Keep
16
+ * the tiny decryptor registry (`registerDecryptor`) so a future scheme can be
17
+ * appended without touching callers.
18
+ *
19
+ * This module only reads bytes; it never writes or executes package content.
20
+ */
21
+
22
+ import { createCipheriv, createDecipheriv, pbkdf2Sync } from 'node:crypto'
23
+ import { closeSync, openSync, readFileSync, readSync, statSync } from 'node:fs'
24
+
25
+ /** Encrypted package magic. */
26
+ export const WXAPKG_HEADER = 'V1MMWX'
27
+
28
+ /** Error codes surfaced to the unpack pipeline. */
29
+ export const ERROR_CODES = Object.freeze({
30
+ UNSUPPORTED_FORMAT: 'UNSUPPORTED_FORMAT',
31
+ DECRYPT_FAILED: 'DECRYPT_FAILED',
32
+ INVALID_PACKAGE: 'INVALID_PACKAGE',
33
+ LIMIT_EXCEEDED: 'LIMIT_EXCEEDED',
34
+ /** Path-jail violation (HTTP/workspace browsing, not package content). */
35
+ INVALID_PATH: 'INVALID_PATH',
36
+ /** Malformed search query (bad regular expression). */
37
+ INVALID_QUERY: 'INVALID_QUERY',
38
+ })
39
+
40
+ /** One `WxapkgError` carries a stable `code` so callers can map to statuses. */
41
+ export class WxapkgError extends Error {
42
+ /**
43
+ * @param {string} code one of `ERROR_CODES`
44
+ * @param {string} message human-readable detail
45
+ * @param {object} [details] optional diagnostic payload (paths, sizes, ...)
46
+ */
47
+ constructor(code, message, details = undefined) {
48
+ super(message)
49
+ this.name = 'WxapkgError'
50
+ this.code = code
51
+ if (details !== undefined) this.details = details
52
+ }
53
+ }
54
+
55
+ const HEADER_BYTES = WXAPKG_HEADER.length
56
+ const PLAIN_MAGIC = 0xbe
57
+ const PLAIN_INDEX_MARK = 0xed
58
+ const PLAIN_HEADER_BYTES = 18
59
+ const MAX_FILE_COUNT = 20000
60
+ const MAX_NAME_BYTES = 4096
61
+
62
+ const V1MMWX_SALT = 'saltiest'
63
+ const V1MMWX_ITERATIONS = 1000
64
+ const V1MMWX_KEY_BYTES = 32
65
+ /** 6-byte header + one 1024-byte AES block. */
66
+ const V1MMWX_MIN_BYTES = HEADER_BYTES + 1024
67
+ /** First 1023 plaintext bytes are carried inside the encrypted block. */
68
+ const V1MMWX_PLAIN_HEAD_BYTES = 1023
69
+ const V1MMWX_IV = Buffer.from('the iv: 16 bytes', 'ascii')
70
+ /** Fallback XOR byte when the appid is too short. */
71
+ const V1MMWX_FALLBACK_XOR = 0x66
72
+
73
+ function asBuffer(value) {
74
+ if (Buffer.isBuffer(value)) return value
75
+ if (value instanceof Uint8Array) return Buffer.from(value)
76
+ throw new TypeError('wxapkg buffer must be a Buffer or Uint8Array')
77
+ }
78
+
79
+ function detectFormatFromBytes(bytes) {
80
+ if (bytes.length >= HEADER_BYTES && bytes.subarray(0, HEADER_BYTES).toString('latin1') === WXAPKG_HEADER) {
81
+ return 'v1mmwx'
82
+ }
83
+ if (bytes.length >= 1 && bytes[0] === PLAIN_MAGIC) return 'plain'
84
+ return 'unknown'
85
+ }
86
+
87
+ /** PBKDF2-HMAC-SHA1 key for the classic V1MMWX scheme. */
88
+ function v1mmwxKey(appid) {
89
+ const passphrase = typeof appid === 'string' ? appid : ''
90
+ if (passphrase.length === 0) {
91
+ throw new WxapkgError(ERROR_CODES.DECRYPT_FAILED, 'appid is required to decrypt a V1MMWX wxapkg')
92
+ }
93
+ return pbkdf2Sync(passphrase, V1MMWX_SALT, V1MMWX_ITERATIONS, V1MMWX_KEY_BYTES, 'sha1')
94
+ }
95
+
96
+ /** XOR byte derived from the appid's second-to-last character. */
97
+ function v1mmwxXorByte(appid) {
98
+ const passphrase = typeof appid === 'string' ? appid : ''
99
+ if (passphrase.length < 2) return V1MMWX_FALLBACK_XOR
100
+ const code = passphrase.charCodeAt(passphrase.length - 2)
101
+ return Number.isFinite(code) ? code : V1MMWX_FALLBACK_XOR
102
+ }
103
+
104
+ function xorBytes(buffer, keyByte) {
105
+ const out = Buffer.allocUnsafe(buffer.length)
106
+ for (let index = 0; index < buffer.length; index += 1) out[index] = buffer[index] ^ keyByte
107
+ return out
108
+ }
109
+
110
+ /**
111
+ * Read package metadata without loading the whole file.
112
+ *
113
+ * @param {string} filePath
114
+ * @returns {{format:'plain'|'v1mmwx'|'unknown', size:number, headerBytes:Buffer}}
115
+ */
116
+ export function inspectPackage(filePath) {
117
+ const stats = statSync(filePath)
118
+ const length = Math.min(HEADER_BYTES, stats.size)
119
+ const headerBytes = Buffer.alloc(length)
120
+ if (length > 0) {
121
+ const fd = openSync(filePath, 'r')
122
+ try {
123
+ readSync(fd, headerBytes, 0, length, 0)
124
+ } finally {
125
+ closeSync(fd)
126
+ }
127
+ }
128
+ return { format: detectFormatFromBytes(headerBytes), size: stats.size, headerBytes }
129
+ }
130
+
131
+ /**
132
+ * Decrypt a classic `V1MMWX` package.
133
+ *
134
+ * ```
135
+ * key = PBKDF2-HMAC-SHA1(appid, 'saltiest', 1000, 32)
136
+ * iv = 'the iv: 16 bytes'
137
+ * head = AES-256-CBC-decrypt(file[6..1030]) (no auto padding)
138
+ * tail = file[1030..] XOR appid[len-2] (or 0x66)
139
+ * plain = head[0..1023] + tail
140
+ * ```
141
+ *
142
+ * @param {Buffer|Uint8Array} buffer encrypted package bytes
143
+ * @param {string} appid mini-program appid used as the PBKDF2 passphrase
144
+ * @returns {Buffer} the decrypted plain wxapkg bytes
145
+ * @throws {WxapkgError} code `DECRYPT_FAILED` when the input is too short, the
146
+ * header is wrong, or the key is wrong (no `0xBE` magic after decrypt).
147
+ */
148
+ export function decryptV1MMWX(buffer, appid) {
149
+ const source = asBuffer(buffer)
150
+ if (source.length < V1MMWX_MIN_BYTES) {
151
+ throw new WxapkgError(ERROR_CODES.DECRYPT_FAILED, 'encrypted wxapkg is truncated', {
152
+ size: source.length,
153
+ minSize: V1MMWX_MIN_BYTES,
154
+ })
155
+ }
156
+ if (source.subarray(0, HEADER_BYTES).toString('latin1') !== WXAPKG_HEADER) {
157
+ throw new WxapkgError(ERROR_CODES.DECRYPT_FAILED, 'not a V1MMWX wxapkg (unexpected header)')
158
+ }
159
+
160
+ const key = v1mmwxKey(appid)
161
+ let head
162
+ try {
163
+ const decipher = createDecipheriv('aes-256-cbc', key, V1MMWX_IV)
164
+ decipher.setAutoPadding(false)
165
+ head = Buffer.concat([
166
+ decipher.update(source.subarray(HEADER_BYTES, V1MMWX_MIN_BYTES)),
167
+ decipher.final(),
168
+ ])
169
+ } catch (error) {
170
+ throw new WxapkgError(ERROR_CODES.DECRYPT_FAILED, `V1MMWX AES decrypt failed: ${error instanceof Error ? error.message : String(error)}`)
171
+ }
172
+ if (head.length < 1024) {
173
+ throw new WxapkgError(ERROR_CODES.DECRYPT_FAILED, 'V1MMWX decrypt produced a short block')
174
+ }
175
+
176
+ const tail = xorBytes(source.subarray(V1MMWX_MIN_BYTES), v1mmwxXorByte(appid))
177
+ const plain = Buffer.concat([head.subarray(0, V1MMWX_PLAIN_HEAD_BYTES), tail])
178
+ if (plain.length === 0 || plain[0] !== PLAIN_MAGIC) {
179
+ throw new WxapkgError(
180
+ ERROR_CODES.DECRYPT_FAILED,
181
+ 'decrypted wxapkg has no 0xBE magic: wrong appid or unsupported client version',
182
+ )
183
+ }
184
+ return plain
185
+ }
186
+
187
+ /**
188
+ * Inverse of {@link decryptV1MMWX} — used by round-trip tests.
189
+ *
190
+ * The first 1023 plaintext bytes are padded with one zero byte, encrypted as a
191
+ * single 1024-byte AES block; the remainder is XOR-obfuscated. The synthetic
192
+ * zero byte is not part of the plaintext so the round trip is byte-exact.
193
+ *
194
+ * @param {Buffer|Uint8Array} buffer plain wxapkg bytes (>= 1023 bytes)
195
+ * @param {string} appid
196
+ * @returns {Buffer}
197
+ * @throws {WxapkgError} code `INVALID_PACKAGE` when the plaintext is too short
198
+ */
199
+ export function encryptV1MMWX(buffer, appid) {
200
+ const plain = asBuffer(buffer)
201
+ if (plain.length < V1MMWX_PLAIN_HEAD_BYTES) {
202
+ throw new WxapkgError(
203
+ ERROR_CODES.INVALID_PACKAGE,
204
+ `plain wxapkg is too short to encrypt (${plain.length} < ${V1MMWX_PLAIN_HEAD_BYTES})`,
205
+ )
206
+ }
207
+ const key = v1mmwxKey(appid)
208
+ const block = Buffer.alloc(1024)
209
+ plain.copy(block, 0, 0, V1MMWX_PLAIN_HEAD_BYTES)
210
+
211
+ const cipher = createCipheriv('aes-256-cbc', key, V1MMWX_IV)
212
+ cipher.setAutoPadding(false)
213
+ const head = Buffer.concat([cipher.update(block), cipher.final()])
214
+ const tail = xorBytes(plain.subarray(V1MMWX_PLAIN_HEAD_BYTES), v1mmwxXorByte(appid))
215
+ return Buffer.concat([Buffer.from(WXAPKG_HEADER, 'latin1'), head, tail])
216
+ }
217
+
218
+ /**
219
+ * Parse the plain wxapkg table.
220
+ *
221
+ * Layout:
222
+ * ```
223
+ * 0x00 0xBE
224
+ * 0x01 uint32BE info1
225
+ * 0x05 uint32BE indexLength
226
+ * 0x09 uint32BE bodyLength
227
+ * 0x0D 0xED
228
+ * 0x0E uint32BE fileCount
229
+ * 0x12 fileCount * { uint32BE nameLen, name(UTF-8), uint32BE offset, uint32BE size }
230
+ * ```
231
+ * Entry names are returned as stored (usually starting with `/`); use
232
+ * `safeRelativePath` from `unpack.js` before touching the filesystem.
233
+ *
234
+ * @param {Buffer|Uint8Array} buffer plain package bytes
235
+ * @returns {{format:'plain', files:Array<{name:string, offset:number, size:number}>, meta:{info1:number,indexLength:number,bodyLength:number,lastMark:number,fileCount:number}}}
236
+ * @throws {WxapkgError} `INVALID_PACKAGE` for structural corruption,
237
+ * `LIMIT_EXCEEDED` for absurd file counts / name lengths.
238
+ */
239
+ export function parseWxapkg(buffer) {
240
+ const source = asBuffer(buffer)
241
+ if (source.length < PLAIN_HEADER_BYTES) {
242
+ throw new WxapkgError(ERROR_CODES.INVALID_PACKAGE, 'wxapkg is truncated: header needs 18 bytes', {
243
+ size: source.length,
244
+ })
245
+ }
246
+ if (source[0] !== PLAIN_MAGIC) {
247
+ throw new WxapkgError(ERROR_CODES.INVALID_PACKAGE, 'not a plain wxapkg (missing 0xBE magic)')
248
+ }
249
+
250
+ const info1 = source.readUInt32BE(1)
251
+ const indexLength = source.readUInt32BE(5)
252
+ const bodyLength = source.readUInt32BE(9)
253
+ const lastMark = source[13]
254
+ if (lastMark !== PLAIN_INDEX_MARK) {
255
+ throw new WxapkgError(ERROR_CODES.INVALID_PACKAGE, 'not a plain wxapkg (missing 0xED index mark)')
256
+ }
257
+
258
+ const fileCount = source.readUInt32BE(14)
259
+ if (fileCount > MAX_FILE_COUNT) {
260
+ throw new WxapkgError(ERROR_CODES.LIMIT_EXCEEDED, `wxapkg declares too many files (${fileCount} > ${MAX_FILE_COUNT})`, {
261
+ fileCount,
262
+ max: MAX_FILE_COUNT,
263
+ })
264
+ }
265
+ if (indexLength > 0 && PLAIN_HEADER_BYTES + indexLength > source.length) {
266
+ throw new WxapkgError(ERROR_CODES.INVALID_PACKAGE, 'wxapkg file index exceeds the package size', {
267
+ indexLength,
268
+ size: source.length,
269
+ })
270
+ }
271
+
272
+ const files = []
273
+ let cursor = PLAIN_HEADER_BYTES
274
+ for (let index = 0; index < fileCount; index += 1) {
275
+ if (cursor + 4 > source.length) {
276
+ throw new WxapkgError(ERROR_CODES.INVALID_PACKAGE, `wxapkg file index is truncated at entry ${index}`)
277
+ }
278
+ const nameLength = source.readUInt32BE(cursor)
279
+ cursor += 4
280
+ if (nameLength > MAX_NAME_BYTES) {
281
+ throw new WxapkgError(ERROR_CODES.LIMIT_EXCEEDED, `wxapkg entry ${index} name is too long (${nameLength})`, {
282
+ max: MAX_NAME_BYTES,
283
+ })
284
+ }
285
+ if (cursor + nameLength + 8 > source.length) {
286
+ throw new WxapkgError(ERROR_CODES.INVALID_PACKAGE, `wxapkg file index is truncated at entry ${index}`)
287
+ }
288
+ const name = source.subarray(cursor, cursor + nameLength).toString('utf8')
289
+ cursor += nameLength
290
+ const offset = source.readUInt32BE(cursor)
291
+ cursor += 4
292
+ const size = source.readUInt32BE(cursor)
293
+ cursor += 4
294
+ if (offset > source.length || size > source.length - offset) {
295
+ throw new WxapkgError(
296
+ ERROR_CODES.INVALID_PACKAGE,
297
+ `wxapkg entry ${JSON.stringify(name)} points outside the package`,
298
+ { offset, size, packageSize: source.length },
299
+ )
300
+ }
301
+ files.push({ name, offset, size })
302
+ }
303
+
304
+ return {
305
+ format: 'plain',
306
+ files,
307
+ meta: { info1, indexLength, bodyLength, lastMark, fileCount },
308
+ }
309
+ }
310
+
311
+ /* ------------------------- decryptor registry ------------------------- */
312
+
313
+ /** @type {Array<{id:string, header?:string, detect:(buffer:Buffer)=>boolean, decrypt:(buffer:Buffer, appid:string)=>Buffer}>} */
314
+ const decryptors = []
315
+
316
+ /**
317
+ * Append a decryptor for a future client version. `detect` receives the raw
318
+ * package buffer and must return `true` for its format; `decrypt` returns the
319
+ * plain package bytes (starting with `0xBE`).
320
+ */
321
+ export function registerDecryptor(decryptor) {
322
+ if (
323
+ !decryptor ||
324
+ typeof decryptor.id !== 'string' ||
325
+ decryptor.id.length === 0 ||
326
+ typeof decryptor.detect !== 'function' ||
327
+ typeof decryptor.decrypt !== 'function'
328
+ ) {
329
+ throw new TypeError('decryptor must have { id, detect(buffer), decrypt(buffer, appid) }')
330
+ }
331
+ if (decryptors.some((entry) => entry.id === decryptor.id)) {
332
+ throw new WxapkgError(ERROR_CODES.INVALID_PACKAGE, `decryptor already registered: ${decryptor.id}`)
333
+ }
334
+ decryptors.push(decryptor)
335
+ return decryptor
336
+ }
337
+
338
+ /** Registered decryptor ids (no functions exposed). */
339
+ export function listDecryptors() {
340
+ return decryptors.map((entry) => ({ id: entry.id, header: entry.header || '' }))
341
+ }
342
+
343
+ function findDecryptor(buffer) {
344
+ for (const decryptor of decryptors) {
345
+ let matched = false
346
+ try {
347
+ matched = decryptor.detect(buffer) === true
348
+ } catch {
349
+ matched = false
350
+ }
351
+ if (matched) return decryptor
352
+ }
353
+ return null
354
+ }
355
+
356
+ registerDecryptor({
357
+ id: 'v1mmwx',
358
+ header: WXAPKG_HEADER,
359
+ detect: (buffer) =>
360
+ buffer.length >= HEADER_BYTES && buffer.subarray(0, HEADER_BYTES).toString('latin1') === WXAPKG_HEADER,
361
+ decrypt: (buffer, appid) => decryptV1MMWX(buffer, appid),
362
+ })
363
+
364
+ /* ------------------------------ readers ------------------------------ */
365
+
366
+ /**
367
+ * Read + decrypt + parse a package that is already in memory.
368
+ *
369
+ * @param {Buffer|Uint8Array} buffer raw package bytes
370
+ * @param {{appid?:string, filePath?:string}} [options]
371
+ * @returns {{format:'plain'|'v1mmwx', buffer:Buffer, files:Array<{name:string,offset:number,size:number}>, meta:object}}
372
+ */
373
+ export function readPackageBuffer(buffer, { appid = '', filePath = '' } = {}) {
374
+ const raw = asBuffer(buffer)
375
+ const decryptor = findDecryptor(raw)
376
+ let format
377
+ let plain
378
+ if (decryptor) {
379
+ format = decryptor.id
380
+ plain = decryptor.decrypt(raw, appid)
381
+ } else if (raw.length > 0 && raw[0] === PLAIN_MAGIC) {
382
+ format = 'plain'
383
+ plain = raw
384
+ } else {
385
+ const detected = detectFormatFromBytes(raw.subarray(0, HEADER_BYTES))
386
+ throw new WxapkgError(
387
+ ERROR_CODES.UNSUPPORTED_FORMAT,
388
+ `unsupported wxapkg format (${detected === 'unknown' ? 'unknown header' : detected})`,
389
+ { filePath: filePath || '', header: raw.subarray(0, 8).toString('hex') },
390
+ )
391
+ }
392
+ const parsed = parseWxapkg(plain)
393
+ return { format, buffer: plain, files: parsed.files, meta: parsed.meta }
394
+ }
395
+
396
+ /**
397
+ * Read one package from disk.
398
+ *
399
+ * @param {string} filePath
400
+ * @param {{appid?:string}} [options]
401
+ * @returns {{format:'plain'|'v1mmwx', buffer:Buffer, files:Array<{name:string,offset:number,size:number}>, meta:object}}
402
+ * @throws {WxapkgError} `UNSUPPORTED_FORMAT` / `DECRYPT_FAILED` / `INVALID_PACKAGE`
403
+ */
404
+ export function readPackage(filePath, { appid = '' } = {}) {
405
+ let raw
406
+ try {
407
+ raw = readFileSync(filePath)
408
+ } catch (error) {
409
+ throw new WxapkgError(
410
+ ERROR_CODES.INVALID_PACKAGE,
411
+ `cannot read wxapkg: ${error instanceof Error ? error.message : String(error)}`,
412
+ { filePath },
413
+ )
414
+ }
415
+ return readPackageBuffer(raw, { appid, filePath })
416
+ }