@marktrue/dsh-mobile-remote 3.2.4-alpha.3-app → 3.2.4-alpha.3

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.
Files changed (352) hide show
  1. package/CHANGELOG.md +1439 -0
  2. package/CONTRIBUTING.md +59 -0
  3. package/FAQ.md +111 -0
  4. package/README.md +254 -6
  5. package/cordis.patch.yml +14 -0
  6. package/docs/00-/345/274/200/345/217/221/346/200/273/347/272/262.md +125 -0
  7. package/docs/01-PRD.md +148 -0
  8. package/docs/02-architecture.md +249 -0
  9. package/docs/03-api.md +1154 -0
  10. package/docs/04-security.md +100 -0
  11. package/docs/05-test-cases.md +405 -0
  12. package/docs/06-install-run.md +372 -0
  13. package/docs/07-user-manual.md +70 -0
  14. package/docs/08-/346/211/251/345/261/225/345/274/200/345/217/221/346/214/207/345/215/227.md +86 -0
  15. package/docs/09-compatibility.md +227 -0
  16. package/docs/10-release.md +1067 -0
  17. package/docs/screenshots/01-home.png +0 -0
  18. package/docs/screenshots/02-notifications.png +0 -0
  19. package/docs/screenshots/03-sessions.png +0 -0
  20. package/docs/screenshots/04-chat.png +0 -0
  21. package/docs/screenshots/05-settings.png +0 -0
  22. package/docs/screenshots/06-new-session.png +0 -0
  23. package/docs/screenshots/pgy-appstore.jpg +0 -0
  24. package/docs/screenshots/pgy-mobile-app.jpg +0 -0
  25. package/docs/screenshots/pgy-pc-client.jpg +0 -0
  26. package/lib/account-usage.js +145 -0
  27. package/lib/app-alignment.js +112 -0
  28. package/lib/app-carrier-store.js +1972 -0
  29. package/lib/app-carrier.js +609 -0
  30. package/lib/client.js +1174 -0
  31. package/lib/desktop-form.js +738 -0
  32. package/lib/endpoint-registry.js +167 -0
  33. package/lib/git-read-bundled-provider.js +450 -0
  34. package/lib/git-read-service.js +601 -0
  35. package/lib/index.js +9622 -0
  36. package/lib/npm-update-source.js +226 -0
  37. package/lib/plugin-content-digest.js +80 -0
  38. package/lib/plugin-self-update.js +997 -0
  39. package/lib/plugin-update-controller.js +274 -0
  40. package/lib/plugin-update-log.js +164 -0
  41. package/lib/plugin-update-transaction.js +322 -0
  42. package/lib/plugin-update.js +309 -0
  43. package/lib/runtime-dependency-snapshot.js +345 -0
  44. package/lib/session-title-cache.js +123 -0
  45. package/lib/session-title-refresh.js +261 -0
  46. package/lib/tool-process.js +20 -0
  47. package/lib/update-fetch.js +24 -0
  48. package/lib/update-lock-arbiter.js +170 -0
  49. package/lib/update-supervisor.js +346 -0
  50. package/node_modules/ansi-regex/index.d.ts +37 -0
  51. package/node_modules/ansi-regex/index.js +10 -0
  52. package/node_modules/ansi-regex/license +9 -0
  53. package/node_modules/ansi-regex/package.json +55 -0
  54. package/node_modules/ansi-regex/readme.md +78 -0
  55. package/node_modules/ansi-styles/index.d.ts +345 -0
  56. package/node_modules/ansi-styles/index.js +163 -0
  57. package/node_modules/ansi-styles/license +9 -0
  58. package/node_modules/ansi-styles/package.json +56 -0
  59. package/node_modules/ansi-styles/readme.md +152 -0
  60. package/node_modules/camelcase/index.d.ts +63 -0
  61. package/node_modules/camelcase/index.js +76 -0
  62. package/node_modules/camelcase/license +9 -0
  63. package/node_modules/camelcase/package.json +43 -0
  64. package/node_modules/camelcase/readme.md +99 -0
  65. package/node_modules/cliui/CHANGELOG.md +76 -0
  66. package/node_modules/cliui/LICENSE.txt +14 -0
  67. package/node_modules/cliui/README.md +115 -0
  68. package/node_modules/cliui/index.js +354 -0
  69. package/node_modules/cliui/package.json +65 -0
  70. package/node_modules/color-convert/CHANGELOG.md +54 -0
  71. package/node_modules/color-convert/LICENSE +21 -0
  72. package/node_modules/color-convert/README.md +68 -0
  73. package/node_modules/color-convert/conversions.js +839 -0
  74. package/node_modules/color-convert/index.js +81 -0
  75. package/node_modules/color-convert/package.json +48 -0
  76. package/node_modules/color-convert/route.js +97 -0
  77. package/node_modules/color-name/LICENSE +8 -0
  78. package/node_modules/color-name/README.md +11 -0
  79. package/node_modules/color-name/index.js +152 -0
  80. package/node_modules/color-name/package.json +28 -0
  81. package/node_modules/decamelize/index.js +13 -0
  82. package/node_modules/decamelize/license +21 -0
  83. package/node_modules/decamelize/package.json +38 -0
  84. package/node_modules/decamelize/readme.md +48 -0
  85. package/node_modules/dijkstrajs/.travis.yml +4 -0
  86. package/node_modules/dijkstrajs/CONTRIBUTING.md +8 -0
  87. package/node_modules/dijkstrajs/LICENSE.md +19 -0
  88. package/node_modules/dijkstrajs/README.md +18 -0
  89. package/node_modules/dijkstrajs/dijkstra.js +165 -0
  90. package/node_modules/dijkstrajs/package.json +60 -0
  91. package/node_modules/dijkstrajs/test/dijkstra.test.js +96 -0
  92. package/node_modules/emoji-regex/LICENSE-MIT.txt +20 -0
  93. package/node_modules/emoji-regex/README.md +73 -0
  94. package/node_modules/emoji-regex/es2015/index.js +6 -0
  95. package/node_modules/emoji-regex/es2015/text.js +6 -0
  96. package/node_modules/emoji-regex/index.d.ts +23 -0
  97. package/node_modules/emoji-regex/index.js +6 -0
  98. package/node_modules/emoji-regex/package.json +50 -0
  99. package/node_modules/emoji-regex/text.js +6 -0
  100. package/node_modules/find-up/index.d.ts +137 -0
  101. package/node_modules/find-up/index.js +89 -0
  102. package/node_modules/find-up/license +9 -0
  103. package/node_modules/find-up/package.json +53 -0
  104. package/node_modules/find-up/readme.md +156 -0
  105. package/node_modules/get-caller-file/LICENSE.md +6 -0
  106. package/node_modules/get-caller-file/README.md +41 -0
  107. package/node_modules/get-caller-file/index.d.ts +2 -0
  108. package/node_modules/get-caller-file/index.js +22 -0
  109. package/node_modules/get-caller-file/index.js.map +1 -0
  110. package/node_modules/get-caller-file/package.json +42 -0
  111. package/node_modules/is-fullwidth-code-point/index.d.ts +17 -0
  112. package/node_modules/is-fullwidth-code-point/index.js +50 -0
  113. package/node_modules/is-fullwidth-code-point/license +9 -0
  114. package/node_modules/is-fullwidth-code-point/package.json +42 -0
  115. package/node_modules/is-fullwidth-code-point/readme.md +39 -0
  116. package/node_modules/locate-path/index.d.ts +83 -0
  117. package/node_modules/locate-path/index.js +65 -0
  118. package/node_modules/locate-path/license +9 -0
  119. package/node_modules/locate-path/package.json +45 -0
  120. package/node_modules/locate-path/readme.md +122 -0
  121. package/node_modules/p-limit/index.d.ts +38 -0
  122. package/node_modules/p-limit/index.js +57 -0
  123. package/node_modules/p-limit/license +9 -0
  124. package/node_modules/p-limit/package.json +52 -0
  125. package/node_modules/p-limit/readme.md +101 -0
  126. package/node_modules/p-locate/index.d.ts +64 -0
  127. package/node_modules/p-locate/index.js +52 -0
  128. package/node_modules/p-locate/license +9 -0
  129. package/node_modules/p-locate/package.json +53 -0
  130. package/node_modules/p-locate/readme.md +90 -0
  131. package/node_modules/p-try/index.d.ts +39 -0
  132. package/node_modules/p-try/index.js +9 -0
  133. package/node_modules/p-try/license +9 -0
  134. package/node_modules/p-try/package.json +42 -0
  135. package/node_modules/p-try/readme.md +58 -0
  136. package/node_modules/path-exists/index.d.ts +28 -0
  137. package/node_modules/path-exists/index.js +23 -0
  138. package/node_modules/path-exists/license +9 -0
  139. package/node_modules/path-exists/package.json +39 -0
  140. package/node_modules/path-exists/readme.md +52 -0
  141. package/node_modules/pngjs/.eslintignore +2 -0
  142. package/node_modules/pngjs/.eslintrc.json +19 -0
  143. package/node_modules/pngjs/.prettierignore +3 -0
  144. package/node_modules/pngjs/LICENSE +20 -0
  145. package/node_modules/pngjs/README.md +429 -0
  146. package/node_modules/pngjs/browser.js +16096 -0
  147. package/node_modules/pngjs/coverage/lcov-report/base.css +354 -0
  148. package/node_modules/pngjs/coverage/lcov-report/bitmapper.js.html +881 -0
  149. package/node_modules/pngjs/coverage/lcov-report/bitpacker.js.html +554 -0
  150. package/node_modules/pngjs/coverage/lcov-report/block-navigation.js +77 -0
  151. package/node_modules/pngjs/coverage/lcov-report/chunkstream.js.html +647 -0
  152. package/node_modules/pngjs/coverage/lcov-report/constants.js.html +176 -0
  153. package/node_modules/pngjs/coverage/lcov-report/crc.js.html +200 -0
  154. package/node_modules/pngjs/coverage/lcov-report/favicon.png +0 -0
  155. package/node_modules/pngjs/coverage/lcov-report/filter-pack.js.html +593 -0
  156. package/node_modules/pngjs/coverage/lcov-report/filter-parse-async.js.html +152 -0
  157. package/node_modules/pngjs/coverage/lcov-report/filter-parse-sync.js.html +143 -0
  158. package/node_modules/pngjs/coverage/lcov-report/filter-parse.js.html +611 -0
  159. package/node_modules/pngjs/coverage/lcov-report/format-normaliser.js.html +359 -0
  160. package/node_modules/pngjs/coverage/lcov-report/index.html +426 -0
  161. package/node_modules/pngjs/coverage/lcov-report/interlace.js.html +365 -0
  162. package/node_modules/pngjs/coverage/lcov-report/packer-async.js.html +230 -0
  163. package/node_modules/pngjs/coverage/lcov-report/packer-sync.js.html +248 -0
  164. package/node_modules/pngjs/coverage/lcov-report/packer.js.html +467 -0
  165. package/node_modules/pngjs/coverage/lcov-report/paeth-predictor.js.html +128 -0
  166. package/node_modules/pngjs/coverage/lcov-report/parser-async.js.html +575 -0
  167. package/node_modules/pngjs/coverage/lcov-report/parser-sync.js.html +404 -0
  168. package/node_modules/pngjs/coverage/lcov-report/parser.js.html +950 -0
  169. package/node_modules/pngjs/coverage/lcov-report/png-sync.js.html +116 -0
  170. package/node_modules/pngjs/coverage/lcov-report/png.js.html +662 -0
  171. package/node_modules/pngjs/coverage/lcov-report/prettify.css +101 -0
  172. package/node_modules/pngjs/coverage/lcov-report/prettify.js +1007 -0
  173. package/node_modules/pngjs/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  174. package/node_modules/pngjs/coverage/lcov-report/sorter.js +169 -0
  175. package/node_modules/pngjs/coverage/lcov-report/sync-inflate.js.html +584 -0
  176. package/node_modules/pngjs/coverage/lcov-report/sync-reader.js.html +215 -0
  177. package/node_modules/pngjs/coverage/lcov.info +2177 -0
  178. package/node_modules/pngjs/lib/bitmapper.js +267 -0
  179. package/node_modules/pngjs/lib/bitpacker.js +158 -0
  180. package/node_modules/pngjs/lib/chunkstream.js +189 -0
  181. package/node_modules/pngjs/lib/constants.js +32 -0
  182. package/node_modules/pngjs/lib/crc.js +40 -0
  183. package/node_modules/pngjs/lib/filter-pack.js +171 -0
  184. package/node_modules/pngjs/lib/filter-parse-async.js +24 -0
  185. package/node_modules/pngjs/lib/filter-parse-sync.js +21 -0
  186. package/node_modules/pngjs/lib/filter-parse.js +177 -0
  187. package/node_modules/pngjs/lib/format-normaliser.js +93 -0
  188. package/node_modules/pngjs/lib/interlace.js +95 -0
  189. package/node_modules/pngjs/lib/packer-async.js +50 -0
  190. package/node_modules/pngjs/lib/packer-sync.js +56 -0
  191. package/node_modules/pngjs/lib/packer.js +129 -0
  192. package/node_modules/pngjs/lib/paeth-predictor.js +16 -0
  193. package/node_modules/pngjs/lib/parser-async.js +165 -0
  194. package/node_modules/pngjs/lib/parser-sync.js +108 -0
  195. package/node_modules/pngjs/lib/parser.js +290 -0
  196. package/node_modules/pngjs/lib/png-sync.js +12 -0
  197. package/node_modules/pngjs/lib/png.js +194 -0
  198. package/node_modules/pngjs/lib/sync-inflate.js +168 -0
  199. package/node_modules/pngjs/lib/sync-reader.js +45 -0
  200. package/node_modules/pngjs/package.json +73 -0
  201. package/node_modules/qrcode/README.md +770 -0
  202. package/node_modules/qrcode/bin/qrcode +159 -0
  203. package/node_modules/qrcode/helper/to-sjis-browser.js +2 -0
  204. package/node_modules/qrcode/helper/to-sjis.js +105 -0
  205. package/node_modules/qrcode/lib/browser.js +76 -0
  206. package/node_modules/qrcode/lib/can-promise.js +7 -0
  207. package/node_modules/qrcode/lib/core/alignment-pattern.js +83 -0
  208. package/node_modules/qrcode/lib/core/alphanumeric-data.js +59 -0
  209. package/node_modules/qrcode/lib/core/bit-buffer.js +37 -0
  210. package/node_modules/qrcode/lib/core/bit-matrix.js +65 -0
  211. package/node_modules/qrcode/lib/core/byte-data.js +30 -0
  212. package/node_modules/qrcode/lib/core/error-correction-code.js +135 -0
  213. package/node_modules/qrcode/lib/core/error-correction-level.js +50 -0
  214. package/node_modules/qrcode/lib/core/finder-pattern.js +22 -0
  215. package/node_modules/qrcode/lib/core/format-info.js +29 -0
  216. package/node_modules/qrcode/lib/core/galois-field.js +69 -0
  217. package/node_modules/qrcode/lib/core/kanji-data.js +54 -0
  218. package/node_modules/qrcode/lib/core/mask-pattern.js +234 -0
  219. package/node_modules/qrcode/lib/core/mode.js +167 -0
  220. package/node_modules/qrcode/lib/core/numeric-data.js +43 -0
  221. package/node_modules/qrcode/lib/core/polynomial.js +62 -0
  222. package/node_modules/qrcode/lib/core/qrcode.js +495 -0
  223. package/node_modules/qrcode/lib/core/reed-solomon-encoder.js +56 -0
  224. package/node_modules/qrcode/lib/core/regex.js +31 -0
  225. package/node_modules/qrcode/lib/core/segments.js +330 -0
  226. package/node_modules/qrcode/lib/core/utils.js +63 -0
  227. package/node_modules/qrcode/lib/core/version-check.js +9 -0
  228. package/node_modules/qrcode/lib/core/version.js +163 -0
  229. package/node_modules/qrcode/lib/index.js +12 -0
  230. package/node_modules/qrcode/lib/renderer/canvas.js +63 -0
  231. package/node_modules/qrcode/lib/renderer/png.js +78 -0
  232. package/node_modules/qrcode/lib/renderer/svg-tag.js +81 -0
  233. package/node_modules/qrcode/lib/renderer/svg.js +19 -0
  234. package/node_modules/qrcode/lib/renderer/terminal/terminal-small.js +85 -0
  235. package/node_modules/qrcode/lib/renderer/terminal/terminal.js +49 -0
  236. package/node_modules/qrcode/lib/renderer/terminal.js +9 -0
  237. package/node_modules/qrcode/lib/renderer/utf8.js +71 -0
  238. package/node_modules/qrcode/lib/renderer/utils.js +99 -0
  239. package/node_modules/qrcode/lib/server.js +138 -0
  240. package/node_modules/qrcode/license +10 -0
  241. package/node_modules/qrcode/package.json +77 -0
  242. package/node_modules/require-directory/.jshintrc +67 -0
  243. package/node_modules/require-directory/.npmignore +1 -0
  244. package/node_modules/require-directory/.travis.yml +3 -0
  245. package/node_modules/require-directory/LICENSE +22 -0
  246. package/node_modules/require-directory/README.markdown +184 -0
  247. package/node_modules/require-directory/index.js +86 -0
  248. package/node_modules/require-directory/package.json +40 -0
  249. package/node_modules/require-main-filename/CHANGELOG.md +26 -0
  250. package/node_modules/require-main-filename/LICENSE.txt +14 -0
  251. package/node_modules/require-main-filename/README.md +26 -0
  252. package/node_modules/require-main-filename/index.js +18 -0
  253. package/node_modules/require-main-filename/package.json +35 -0
  254. package/node_modules/set-blocking/CHANGELOG.md +26 -0
  255. package/node_modules/set-blocking/LICENSE.txt +14 -0
  256. package/node_modules/set-blocking/README.md +31 -0
  257. package/node_modules/set-blocking/index.js +7 -0
  258. package/node_modules/set-blocking/package.json +42 -0
  259. package/node_modules/string-width/index.d.ts +29 -0
  260. package/node_modules/string-width/index.js +47 -0
  261. package/node_modules/string-width/license +9 -0
  262. package/node_modules/string-width/package.json +56 -0
  263. package/node_modules/string-width/readme.md +50 -0
  264. package/node_modules/strip-ansi/index.d.ts +17 -0
  265. package/node_modules/strip-ansi/index.js +4 -0
  266. package/node_modules/strip-ansi/license +9 -0
  267. package/node_modules/strip-ansi/package.json +54 -0
  268. package/node_modules/strip-ansi/readme.md +46 -0
  269. package/node_modules/which-module/LICENSE +13 -0
  270. package/node_modules/which-module/README.md +58 -0
  271. package/node_modules/which-module/index.js +9 -0
  272. package/node_modules/which-module/package.json +41 -0
  273. package/node_modules/wrap-ansi/index.js +186 -0
  274. package/node_modules/wrap-ansi/license +9 -0
  275. package/node_modules/wrap-ansi/package.json +61 -0
  276. package/node_modules/wrap-ansi/readme.md +97 -0
  277. package/node_modules/y18n/CHANGELOG.md +35 -0
  278. package/node_modules/y18n/LICENSE +13 -0
  279. package/node_modules/y18n/README.md +109 -0
  280. package/node_modules/y18n/index.js +188 -0
  281. package/node_modules/y18n/package.json +39 -0
  282. package/node_modules/yargs/CHANGELOG.md +420 -0
  283. package/node_modules/yargs/LICENSE +21 -0
  284. package/node_modules/yargs/README.md +140 -0
  285. package/node_modules/yargs/build/lib/apply-extends.d.ts +2 -0
  286. package/node_modules/yargs/build/lib/apply-extends.js +65 -0
  287. package/node_modules/yargs/build/lib/argsert.d.ts +2 -0
  288. package/node_modules/yargs/build/lib/argsert.js +65 -0
  289. package/node_modules/yargs/build/lib/command.d.ts +64 -0
  290. package/node_modules/yargs/build/lib/command.js +416 -0
  291. package/node_modules/yargs/build/lib/common-types.d.ts +36 -0
  292. package/node_modules/yargs/build/lib/common-types.js +25 -0
  293. package/node_modules/yargs/build/lib/completion-templates.d.ts +2 -0
  294. package/node_modules/yargs/build/lib/completion-templates.js +50 -0
  295. package/node_modules/yargs/build/lib/completion.d.ts +21 -0
  296. package/node_modules/yargs/build/lib/completion.js +135 -0
  297. package/node_modules/yargs/build/lib/is-promise.d.ts +1 -0
  298. package/node_modules/yargs/build/lib/is-promise.js +9 -0
  299. package/node_modules/yargs/build/lib/levenshtein.d.ts +1 -0
  300. package/node_modules/yargs/build/lib/levenshtein.js +58 -0
  301. package/node_modules/yargs/build/lib/middleware.d.ts +10 -0
  302. package/node_modules/yargs/build/lib/middleware.js +57 -0
  303. package/node_modules/yargs/build/lib/obj-filter.d.ts +1 -0
  304. package/node_modules/yargs/build/lib/obj-filter.js +14 -0
  305. package/node_modules/yargs/build/lib/parse-command.d.ts +11 -0
  306. package/node_modules/yargs/build/lib/parse-command.js +36 -0
  307. package/node_modules/yargs/build/lib/process-argv.d.ts +2 -0
  308. package/node_modules/yargs/build/lib/process-argv.js +31 -0
  309. package/node_modules/yargs/build/lib/usage.d.ts +49 -0
  310. package/node_modules/yargs/build/lib/usage.js +540 -0
  311. package/node_modules/yargs/build/lib/validation.d.ts +34 -0
  312. package/node_modules/yargs/build/lib/validation.js +330 -0
  313. package/node_modules/yargs/build/lib/yargs.d.ts +274 -0
  314. package/node_modules/yargs/build/lib/yargs.js +1190 -0
  315. package/node_modules/yargs/build/lib/yerror.d.ts +4 -0
  316. package/node_modules/yargs/build/lib/yerror.js +11 -0
  317. package/node_modules/yargs/index.js +40 -0
  318. package/node_modules/yargs/locales/be.json +46 -0
  319. package/node_modules/yargs/locales/de.json +46 -0
  320. package/node_modules/yargs/locales/en.json +51 -0
  321. package/node_modules/yargs/locales/es.json +46 -0
  322. package/node_modules/yargs/locales/fi.json +49 -0
  323. package/node_modules/yargs/locales/fr.json +53 -0
  324. package/node_modules/yargs/locales/hi.json +49 -0
  325. package/node_modules/yargs/locales/hu.json +46 -0
  326. package/node_modules/yargs/locales/id.json +50 -0
  327. package/node_modules/yargs/locales/it.json +46 -0
  328. package/node_modules/yargs/locales/ja.json +51 -0
  329. package/node_modules/yargs/locales/ko.json +49 -0
  330. package/node_modules/yargs/locales/nb.json +44 -0
  331. package/node_modules/yargs/locales/nl.json +49 -0
  332. package/node_modules/yargs/locales/nn.json +44 -0
  333. package/node_modules/yargs/locales/pirate.json +13 -0
  334. package/node_modules/yargs/locales/pl.json +49 -0
  335. package/node_modules/yargs/locales/pt.json +45 -0
  336. package/node_modules/yargs/locales/pt_BR.json +48 -0
  337. package/node_modules/yargs/locales/ru.json +46 -0
  338. package/node_modules/yargs/locales/th.json +46 -0
  339. package/node_modules/yargs/locales/tr.json +48 -0
  340. package/node_modules/yargs/locales/zh_CN.json +48 -0
  341. package/node_modules/yargs/locales/zh_TW.json +47 -0
  342. package/node_modules/yargs/package.json +92 -0
  343. package/node_modules/yargs/yargs.js +14 -0
  344. package/node_modules/yargs-parser/CHANGELOG.md +601 -0
  345. package/node_modules/yargs-parser/LICENSE.txt +14 -0
  346. package/node_modules/yargs-parser/README.md +449 -0
  347. package/node_modules/yargs-parser/index.js +1032 -0
  348. package/node_modules/yargs-parser/lib/tokenize-arg-string.js +40 -0
  349. package/node_modules/yargs-parser/package.json +46 -0
  350. package/package.json +65 -12
  351. package/DSH-Remote-v3.2.4.apk +0 -4
  352. package/manifest.json +0 -21
@@ -0,0 +1,609 @@
1
+ // App 载体发现与身份校验(设计稿 §5.5;事实 C9/C10,F15/F16)。
2
+ //
3
+ // 载体是什么:与主插件**同包名**的一个预发布版本(`<插件版本>-app`,插件为 prerelease 时
4
+ // 形如 `3.2.1-alpha.1-app`),只承担数据角色(APK + 完整 schema 1 发布清单),由主插件
5
+ // `package.json` 里的精确 npm 别名依赖随包安装。主插件需要找到它、验证它,再交给托管区
6
+ // 去落位;同时**不能**把它算进运行时依赖闭包(否则每次自更新都会把 72MB 复制进产物目录
7
+ // 与每个回滚快照)。
8
+ //
9
+ // 安全边界(缺一不可,任一不满足都不得当作可用载体):
10
+ // 1. 只从**本插件自身的解析作用域**定位载体别名下的包;自身作用域没有时才按 DSH
11
+ // profile 逐个精确回退,多个不同候选一律报 ambiguous,绝不"找到第一个就用"。
12
+ // 2. 不解析载体的裸包入口(数据包没有 main/exports);只读 `package.json`、
13
+ // 发布清单与 APK 字节,避免借"入口加载"执行任意代码。
14
+ // 3. 身份:真实包名 === 本插件包名、版本 === `<本插件版本>-app`、
15
+ // `dsh.appCarrier === true`、无运行角色字段、发布清单完整 schema 1(app+plugin
16
+ // 两段 + 顶层兼容镜像)、APK 文件名在清单允许范围内且大小/sha256 与清单一致。
17
+ // 4. 运行时闭包排除只接受「根包显式声明该载体别名 + 上述校验通过」;普通依赖自报
18
+ // `dsh.appCarrier` 不构成豁免。任何异常/多义一律按"不排除"处理(fail-closed:
19
+ // 宁可让闭包包含字节,也不放宽身份判据)。
20
+ // 5. 本模块不写任何文件;冻结快照原样保留根包 `package.json`,不靠删改依赖声明
21
+ // 改变内容标识。
22
+ import { createHash } from "node:crypto";
23
+ import {
24
+ closeSync, lstatSync, openSync, readFileSync, readSync, readdirSync, realpathSync, statSync,
25
+ } from "node:fs";
26
+ import { createRequire } from "node:module";
27
+ import { homedir } from "node:os";
28
+ import { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
29
+ import { fileURLToPath } from "node:url";
30
+ import { pluginContentDigest } from "./plugin-content-digest.js";
31
+ import { parseVersion } from "./plugin-update.js";
32
+
33
+ /** 载体别名 / 版本的固定后缀:`<包名>-app` 与 `<插件版本>-app`。 */
34
+ export const APP_CARRIER_SUFFIX = "-app";
35
+ /** 随载体到达的发布清单文件名(设计稿 §5.1)。 */
36
+ export const APP_CARRIER_MANIFEST_FILE = "manifest.json";
37
+ /** 发布清单 schema 版本(与 dsh-mobile-app/tools/gen-manifest.js 的 MANIFEST_SCHEMA 一致)。 */
38
+ export const APP_CARRIER_MANIFEST_SCHEMA = 1;
39
+
40
+ /** 发现结果状态:找到 / 未找到 / 多个候选 / 身份不符 / 加载失败。 */
41
+ export const APP_CARRIER_STATUS = Object.freeze({
42
+ FOUND: "found",
43
+ MISSING: "missing",
44
+ AMBIGUOUS: "ambiguous",
45
+ MISMATCH: "mismatch",
46
+ ERROR: "error",
47
+ });
48
+
49
+ const HASH_PATTERN = /^[a-f0-9]{64}$/;
50
+ // App 版本保持 `X.Y.Z+N`(App 不发 prerelease);插件版本可为 core + 可选 prerelease。
51
+ const APP_BUILD_VERSION_PATTERN = /^\d+\.\d+\.\d+\+\d+$/;
52
+ // 与运行时闭包 worker 同款依赖名判据:可选 scope,名字里不允许路径分隔符/空白。
53
+ const DEPENDENCY_NAME_PATTERN = /^(?:@[^/\s]+\/)?[^./\s][^/\s]*$/;
54
+ // 清单允许的 APK 文件名:只能是载体根下的一层普通文件名,且以 .apk 结尾。
55
+ const APK_FILE_PATTERN = /^[^/\\\x00]+\.apk$/i;
56
+ // 载体不得携带的运行角色 / 平台字段(设计稿 §5.1 红线)。
57
+ const FORBIDDEN_ROLE_FIELDS = ["bin", "main", "exports", "os", "cpu", "libc"];
58
+ const FORBIDDEN_ROLE_DSH_FIELDS = ["bundle", "client"];
59
+ const FORBIDDEN_SCRIPTS = ["preinstall", "install", "postinstall", "prepare"];
60
+
61
+ const MODULE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
62
+
63
+ /** 本模块所在插件的根目录(生产环境即实际加载的插件副本)。 */
64
+ export function defaultPluginRoot() {
65
+ return MODULE_ROOT;
66
+ }
67
+
68
+ /** 由插件包名推导载体别名:`@scope/name` → `@scope/name-app`。 */
69
+ export function appCarrierAlias(pluginName) {
70
+ if (typeof pluginName !== "string" || !DEPENDENCY_NAME_PATTERN.test(pluginName)) {
71
+ throw new Error("invalid-plugin-name");
72
+ }
73
+ return `${pluginName}${APP_CARRIER_SUFFIX}`;
74
+ }
75
+
76
+ /**
77
+ * 由插件版本推导载体版本:`X.Y.Z` → `X.Y.Z-app`。
78
+ * 插件为 prerelease 时用**全字串**加后缀(`3.2.1-alpha.1` → `3.2.1-alpha.1-app`),
79
+ * 载体因此仍是合法的 semver prerelease,也正是发布通道里精确别名依赖的那一个版本。
80
+ */
81
+ export function appCarrierVersion(pluginVersion) {
82
+ if (typeof pluginVersion !== "string" || pluginVersion === "") throw new Error("invalid-plugin-version");
83
+ return `${pluginVersion}${APP_CARRIER_SUFFIX}`;
84
+ }
85
+
86
+ /** 主包 `package.json` 里应有的精确 npm 别名依赖声明(设计稿 §5.1)。 */
87
+ export function appCarrierDependencySpec(pluginName, pluginVersion) {
88
+ return `npm:${pluginName}@${appCarrierVersion(pluginVersion)}`;
89
+ }
90
+
91
+ function contained(root, target) {
92
+ const rel = relative(root, target);
93
+ return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
94
+ }
95
+
96
+ function readJsonFile(file) {
97
+ try {
98
+ return { ok: true, value: JSON.parse(readFileSync(file, "utf8")) };
99
+ } catch (error) {
100
+ return { ok: false, code: error?.code === "ENOENT" ? "missing" : "unreadable", error };
101
+ }
102
+ }
103
+
104
+ function readPackageIdentity(packageRoot) {
105
+ const file = join(packageRoot, "package.json");
106
+ const parsed = readJsonFile(file);
107
+ if (!parsed.ok) return { ok: false, reason: parsed.code === "missing" ? "package-missing" : "package-unreadable" };
108
+ const pkg = parsed.value;
109
+ if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) return { ok: false, reason: "package-invalid" };
110
+ return { ok: true, file, pkg };
111
+ }
112
+
113
+ /** 同步、分块计算文件 sha256:72MB 级 APK 也不在事件循环里一次性读入内存。 */
114
+ function sha256FileSync(file, chunkSize = 1024 * 1024) {
115
+ const hash = createHash("sha256");
116
+ const fd = openSync(file, "r");
117
+ try {
118
+ const buffer = Buffer.allocUnsafe(chunkSize);
119
+ for (;;) {
120
+ const read = readSync(fd, buffer, 0, chunkSize, null);
121
+ if (read <= 0) break;
122
+ hash.update(buffer.subarray(0, read));
123
+ }
124
+ } finally {
125
+ closeSync(fd);
126
+ }
127
+ return hash.digest("hex");
128
+ }
129
+
130
+ function dshHomeFrom(options) {
131
+ if (typeof options?.dshHome === "string" && options.dshHome.trim() !== "") return options.dshHome;
132
+ const fromEnv = process.env.DSH_HOME;
133
+ if (typeof fromEnv === "string" && fromEnv.trim() !== "") return fromEnv;
134
+ return join(homedir(), ".dsh");
135
+ }
136
+
137
+ /** 读取根包对载体别名的显式声明(只认 optionalDependencies / dependencies 的精确键)。 */
138
+ function declaredCarrierDependency(pkg, alias) {
139
+ for (const field of ["optionalDependencies", "dependencies"]) {
140
+ const table = pkg?.[field];
141
+ if (table && typeof table === "object" && typeof table[alias] === "string") {
142
+ return { declared: true, field, spec: table[alias] };
143
+ }
144
+ }
145
+ return { declared: false, field: null, spec: null };
146
+ }
147
+
148
+ /**
149
+ * 声明是否可当作"精确绑定":`npm:<包名>@<版本>-app` 是正式发布形态;
150
+ * `file:` / `link:` / `portal:` / `workspace:` 是本地测试通道的目录绑定(P0/A10)。
151
+ * 其余(范围、latest、别的别名)一律不算精确绑定,不能据此排除闭包。
152
+ */
153
+ function declarationKind(spec, pluginName, pluginVersion) {
154
+ if (typeof spec !== "string") return "inexact";
155
+ if (spec === appCarrierDependencySpec(pluginName, pluginVersion)) return "npm-alias";
156
+ if (/^(?:file|link|portal|workspace):/.test(spec)) return "local";
157
+ return "inexact";
158
+ }
159
+
160
+ /** 某作用域下 Node 会查找该依赖名的目录顺序(与运行时 worker 的 binding 一致)。 */
161
+ function dependencyLookupDirs(scopeRoot, name) {
162
+ try {
163
+ const require = createRequire(join(scopeRoot, "package.json"));
164
+ return (require.resolve.paths(name) ?? []).filter((dir) => typeof dir === "string" && isAbsolute(dir));
165
+ } catch {
166
+ return [];
167
+ }
168
+ }
169
+
170
+ /**
171
+ * 在一个解析作用域里按精确别名找候选。
172
+ * 复核 scope 容器(`@scope`)的 broken symlink:不能被上层同名目录兜底后假绿。
173
+ * 返回 { candidates, errors };candidates 按真实路径去重。
174
+ */
175
+ function scanCarrierScope(scopeRoot, alias, via) {
176
+ const candidates = [];
177
+ const errors = [];
178
+ for (const lookup of dependencyLookupDirs(scopeRoot, alias)) {
179
+ const candidatePath = join(lookup, alias);
180
+ if (alias.startsWith("@")) {
181
+ const scopeDir = join(lookup, alias.split("/")[0]);
182
+ let scopeStat;
183
+ try {
184
+ scopeStat = lstatSync(scopeDir);
185
+ } catch (error) {
186
+ if (error?.code !== "ENOENT") errors.push({ via, path: scopeDir, reason: "carrier-lookup-failed" });
187
+ }
188
+ if (scopeStat?.isSymbolicLink()) {
189
+ try {
190
+ realpathSync(scopeDir);
191
+ } catch {
192
+ errors.push({ via, path: scopeDir, reason: "carrier-broken-link" });
193
+ continue;
194
+ }
195
+ }
196
+ }
197
+ try {
198
+ lstatSync(candidatePath);
199
+ } catch (error) {
200
+ if (error?.code === "ENOENT") continue;
201
+ errors.push({ via, path: candidatePath, reason: "carrier-lookup-failed" });
202
+ continue;
203
+ }
204
+ let source;
205
+ try {
206
+ source = realpathSync(candidatePath);
207
+ } catch {
208
+ errors.push({ via, path: candidatePath, reason: "carrier-broken-link" });
209
+ continue;
210
+ }
211
+ let stat;
212
+ try {
213
+ stat = statSync(source);
214
+ } catch {
215
+ errors.push({ via, path: candidatePath, reason: "carrier-broken-link" });
216
+ continue;
217
+ }
218
+ if (!stat.isDirectory()) {
219
+ errors.push({ via, path: candidatePath, reason: "carrier-not-directory" });
220
+ continue;
221
+ }
222
+ if (!candidates.some((entry) => entry.source === source)) {
223
+ candidates.push({ via, source, path: candidatePath });
224
+ }
225
+ }
226
+ return { candidates, errors };
227
+ }
228
+
229
+ /** DSH profile 目录:固定三个常见名字 + 实际存在的目录,逐个判断、不取第一个。 */
230
+ function profileDirectories(dshHome) {
231
+ const profilesRoot = join(dshHome, "profiles");
232
+ const names = new Set(["web", "desktop", "cli"]);
233
+ try {
234
+ for (const entry of readdirSync(profilesRoot, { withFileTypes: true })) {
235
+ if (entry.isDirectory() && !entry.name.startsWith(".")) names.add(entry.name);
236
+ }
237
+ } catch {
238
+ // 没有 profiles 目录(隔离测试/新机器):只按固定名字尝试。
239
+ }
240
+ return [...names].sort().map((name) => ({ name, dir: join(profilesRoot, name) }));
241
+ }
242
+
243
+ /** 从入口文件向上找到 package.json 名称为 expectedName 的包根。 */
244
+ function packageRootOfEntry(entry, expectedName) {
245
+ let dir = dirname(entry);
246
+ for (let depth = 0; depth < 8; depth++) {
247
+ const identity = readPackageIdentity(dir);
248
+ if (identity.ok && identity.pkg.name === expectedName) return dir;
249
+ const parent = dirname(dir);
250
+ if (parent === dir) break;
251
+ dir = parent;
252
+ }
253
+ return null;
254
+ }
255
+
256
+ /** 从 profile 里定位"本插件"自己的安装根(用于插件根被快照/链接到别处时的回退)。 */
257
+ function installedPluginRootInProfile(profileDir, pluginName) {
258
+ try {
259
+ const require = createRequire(join(profileDir, "package.json"));
260
+ return packageRootOfEntry(require.resolve(pluginName), pluginName);
261
+ } catch {
262
+ return null;
263
+ }
264
+ }
265
+
266
+ /**
267
+ * 收集候选:先看本插件自身作用域;自身作用域完全找不到时才逐 profile 回退。
268
+ * 回退会评估**所有** profile,返回全部不同候选交给上层判多义,绝不"找到第一个就用"。
269
+ */
270
+ function collectCarrierCandidates(pluginRoot, pluginName, alias, options) {
271
+ const own = scanCarrierScope(pluginRoot, alias, "plugin");
272
+ // 自身作用域已有候选或存在损坏链接:按自身作用域的结论处理,不去别的 profile 猜。
273
+ if (own.candidates.length > 0 || own.errors.length > 0) return own;
274
+ const candidates = [];
275
+ const errors = [];
276
+ // §5.5:**不得**扫描其它 profile 猜载体(真机验收里踩过:现网 profile 导入了另一个
277
+ // 测试 profile 的载体)。只认调用方显式给出的"当前 profile",没有就不做 profile 回退。
278
+ const explicitProfile = typeof options?.profileDir === "string" && options.profileDir.trim() !== ""
279
+ ? { name: basename(options.profileDir), dir: options.profileDir }
280
+ : null;
281
+ for (const profile of explicitProfile === null ? [] : [explicitProfile]) {
282
+ const via = `profile:${profile.name}`;
283
+ // (a) 被提升/平铺到 profile 直接 node_modules 的布局。
284
+ try {
285
+ const stat = lstatSync(join(profile.dir, "node_modules"));
286
+ if (stat.isDirectory() || stat.isSymbolicLink()) {
287
+ const hoistedPath = join(profile.dir, "node_modules", alias);
288
+ let source;
289
+ try {
290
+ source = realpathSync(hoistedPath);
291
+ if (statSync(source).isDirectory() && !candidates.some((entry) => entry.source === source)) {
292
+ candidates.push({ via, source, path: hoistedPath });
293
+ }
294
+ } catch {
295
+ // 该 profile 没有平铺载体:继续。
296
+ }
297
+ }
298
+ } catch {
299
+ // profile 未安装 node_modules:继续。
300
+ }
301
+ // (b) 从该 profile 解析出"本插件"的安装根,再按插件自身作用域找载体。
302
+ const installedRoot = installedPluginRootInProfile(profile.dir, pluginName);
303
+ if (!installedRoot) continue;
304
+ const scanned = scanCarrierScope(installedRoot, alias, via);
305
+ errors.push(...scanned.errors);
306
+ for (const candidate of scanned.candidates) {
307
+ if (!candidates.some((entry) => entry.source === candidate.source)) candidates.push(candidate);
308
+ }
309
+ }
310
+ return { candidates, errors };
311
+ }
312
+
313
+ /** 清单 app 段 / plugin 段的字段判据;返回稳定 reason 或 null。 */
314
+ function manifestProblem(manifest, plugin) {
315
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) return "carrier-manifest-invalid";
316
+ if (manifest.schema !== APP_CARRIER_MANIFEST_SCHEMA) return "carrier-manifest-schema";
317
+ const app = manifest.app;
318
+ const pluginSegment = manifest.plugin;
319
+ if (!app || typeof app !== "object" || Array.isArray(app)) return "carrier-manifest-incomplete";
320
+ if (!pluginSegment || typeof pluginSegment !== "object" || Array.isArray(pluginSegment)) return "carrier-manifest-incomplete";
321
+ // app 段:真实构建版本、APK 文件名、字节数与哈希。
322
+ if (typeof app.version !== "string" || !APP_BUILD_VERSION_PATTERN.test(app.version)) return "carrier-manifest-incomplete";
323
+ if (typeof app.file !== "string" || !APK_FILE_PATTERN.test(app.file)) return "carrier-apk-file-invalid";
324
+ if (!Number.isSafeInteger(app.size) || app.size < 0) return "carrier-manifest-incomplete";
325
+ if (typeof app.sha256 !== "string" || !HASH_PATTERN.test(app.sha256)) return "carrier-manifest-incomplete";
326
+ // plugin 段:发布 tgz 身份 + 插件内容标识(内容标识是配对选择依据,不是本模块的硬门禁)。
327
+ // 插件版本允许 core + 可选 SemVer prerelease;清单里必须与插件**全字串**一致。
328
+ if (typeof pluginSegment.version !== "string" || parseVersion(pluginSegment.version) === null) return "carrier-manifest-incomplete";
329
+ if (typeof pluginSegment.file !== "string" || pluginSegment.file === "" || pluginSegment.file === "." || pluginSegment.file === ".."
330
+ || /[\\/]/.test(pluginSegment.file)) return "carrier-manifest-incomplete";
331
+ if (!Number.isSafeInteger(pluginSegment.size) || pluginSegment.size < 0) return "carrier-manifest-incomplete";
332
+ if (typeof pluginSegment.sha256 !== "string" || !HASH_PATTERN.test(pluginSegment.sha256)) return "carrier-manifest-incomplete";
333
+ if (typeof pluginSegment.contentSha256 !== "string" || !HASH_PATTERN.test(pluginSegment.contentSha256)) return "carrier-manifest-incomplete";
334
+ // 顶层兼容镜像必须与 app 段同源(旧 App 读镜像,完整 schema 1 的一部分)。
335
+ if (manifest.version !== app.version || manifest.apk !== app.file
336
+ || manifest.size !== app.size || manifest.sha256 !== app.sha256) return "carrier-manifest-mirror-mismatch";
337
+ // 版本配对:manifest plugin 段与插件全字串一致(含 prerelease);App 段只按 **core** 与插件配对。
338
+ if (pluginSegment.version !== plugin.version) return "carrier-manifest-plugin-version-mismatch";
339
+ const pluginCore = parseVersion(plugin.version)?.version ?? "";
340
+ if (pluginCore === "" || app.version.split("+")[0] !== pluginCore) return "carrier-manifest-app-version-mismatch";
341
+ return null;
342
+ }
343
+
344
+ /** 校验 APK 位于允许位置,且大小/哈希与清单一致。 */
345
+ function validateCarrierApk(carrierRoot, app) {
346
+ const apkPath = join(carrierRoot, app.file);
347
+ let stat;
348
+ try {
349
+ stat = statSync(apkPath);
350
+ } catch (error) {
351
+ return { ok: false, reason: error?.code === "ENOENT" ? "carrier-apk-missing" : "carrier-apk-unreadable" };
352
+ }
353
+ if (!stat.isFile()) return { ok: false, reason: "carrier-apk-not-file" };
354
+ let real;
355
+ try {
356
+ real = realpathSync(apkPath);
357
+ } catch {
358
+ return { ok: false, reason: "carrier-apk-unreadable" };
359
+ }
360
+ if (!contained(carrierRoot, real)) return { ok: false, reason: "carrier-apk-outside-carrier" };
361
+ if (stat.size !== app.size) return { ok: false, reason: "carrier-apk-size-mismatch", actual: stat.size };
362
+ let sha256;
363
+ try {
364
+ sha256 = sha256FileSync(real);
365
+ } catch {
366
+ return { ok: false, reason: "carrier-apk-unreadable" };
367
+ }
368
+ if (sha256 !== app.sha256) return { ok: false, reason: "carrier-apk-sha256-mismatch", actual: sha256 };
369
+ return { ok: true, path: real, file: app.file, size: stat.size, sha256 };
370
+ }
371
+
372
+ /**
373
+ * 单个候选的完整身份校验。通过返回可用载体描述;否则返回稳定 reason。
374
+ * 注意:这里只看"这个包是不是本插件的载体",插件内容标识匹配属于当前发布选择(§5.3)。
375
+ */
376
+ function validateCarrierCandidate(candidate, plugin) {
377
+ const identity = readPackageIdentity(candidate.source);
378
+ if (!identity.ok) return { ok: false, reason: `carrier-${identity.reason}` };
379
+ const pkg = identity.pkg;
380
+ if (pkg.name !== plugin.name) return { ok: false, reason: "carrier-package-name-mismatch" };
381
+ if (pkg.version !== appCarrierVersion(plugin.version)) return { ok: false, reason: "carrier-version-mismatch" };
382
+ if (pkg.dsh?.appCarrier !== true) return { ok: false, reason: "carrier-role-marker-missing" };
383
+ // 载体只是数据:不得带运行入口、平台限定字段、bundle 角色或安装期钩子。
384
+ if (FORBIDDEN_ROLE_FIELDS.some((field) => pkg[field] !== undefined)) return { ok: false, reason: "carrier-runtime-role-forbidden" };
385
+ if (FORBIDDEN_ROLE_DSH_FIELDS.some((field) => pkg.dsh?.[field] !== undefined)) return { ok: false, reason: "carrier-runtime-role-forbidden" };
386
+ if (pkg.scripts && typeof pkg.scripts === "object" && FORBIDDEN_SCRIPTS.some((field) => typeof pkg.scripts[field] === "string")) {
387
+ return { ok: false, reason: "carrier-runtime-role-forbidden" };
388
+ }
389
+ const manifestPath = join(candidate.source, APP_CARRIER_MANIFEST_FILE);
390
+ const manifestFile = readJsonFile(manifestPath);
391
+ if (!manifestFile.ok) return { ok: false, reason: manifestFile.code === "missing" ? "carrier-manifest-missing" : "carrier-manifest-unreadable" };
392
+ const problem = manifestProblem(manifestFile.value, plugin);
393
+ if (problem) return { ok: false, reason: problem };
394
+ // New carriers expose this in NPM packument for lightweight checks; old carriers may omit it.
395
+ if (pkg.appVersion !== undefined && pkg.appVersion !== manifestFile.value.app.version) {
396
+ return { ok: false, reason: "carrier-app-version-metadata-mismatch" };
397
+ }
398
+ if (pkg.pluginContentSha256 !== undefined && pkg.pluginContentSha256 !== manifestFile.value.plugin.contentSha256) {
399
+ return { ok: false, reason: "carrier-plugin-content-sha256-metadata-mismatch" };
400
+ }
401
+ const apk = validateCarrierApk(candidate.source, manifestFile.value.app);
402
+ if (!apk.ok) return { ok: false, reason: apk.reason };
403
+ let contentSha256 = null;
404
+ try {
405
+ contentSha256 = pluginContentDigest(plugin.root);
406
+ } catch {
407
+ contentSha256 = null;
408
+ }
409
+ const manifest = manifestFile.value;
410
+ return {
411
+ ok: true,
412
+ carrier: {
413
+ source: candidate.source,
414
+ packageJson: identity.file,
415
+ name: pkg.name,
416
+ version: pkg.version,
417
+ appCarrier: true,
418
+ },
419
+ manifest: {
420
+ path: manifestPath,
421
+ schema: manifest.schema,
422
+ appVersion: manifest.app.version,
423
+ pluginVersion: manifest.plugin.version,
424
+ pluginFile: manifest.plugin.file,
425
+ pluginTgzSha256: manifest.plugin.sha256,
426
+ contentSha256: manifest.plugin.contentSha256,
427
+ },
428
+ apk,
429
+ pluginContent: contentSha256 === null ? null : {
430
+ actual: contentSha256,
431
+ manifest: manifest.plugin.contentSha256,
432
+ matches: contentSha256 === manifest.plugin.contentSha256,
433
+ },
434
+ discoveredVia: candidate.via,
435
+ };
436
+ }
437
+
438
+ function baseResult(status, reason, plugin, alias) {
439
+ return {
440
+ ok: status === APP_CARRIER_STATUS.FOUND,
441
+ status,
442
+ reason,
443
+ alias,
444
+ plugin: plugin ? { root: plugin.root, name: plugin.name, version: plugin.version } : null,
445
+ };
446
+ }
447
+
448
+ function pluginIdentityFor(pluginRoot) {
449
+ const resolvedRoot = (() => {
450
+ try {
451
+ return realpathSync(pluginRoot);
452
+ } catch {
453
+ return pluginRoot;
454
+ }
455
+ })();
456
+ const identity = readPackageIdentity(resolvedRoot);
457
+ if (!identity.ok) return { ok: false, reason: identity.reason === "package-missing" ? "plugin-package-missing" : "plugin-package-unreadable" };
458
+ const name = identity.pkg.name;
459
+ const version = identity.pkg.version;
460
+ if (typeof name !== "string" || !DEPENDENCY_NAME_PATTERN.test(name)) return { ok: false, reason: "plugin-package-name-invalid" };
461
+ // 插件版本:core `X.Y.Z` + 可选 SemVer prerelease(发布通道如 3.2.1-alpha.1)。
462
+ if (typeof version !== "string" || parseVersion(version) === null) return { ok: false, reason: "plugin-version-invalid" };
463
+ return { ok: true, root: resolvedRoot, name, version, pkg: identity.pkg };
464
+ }
465
+
466
+ /**
467
+ * 发现并校验随包安装的 App 载体。
468
+ *
469
+ * @param {object} [options]
470
+ * @param {string} [options.pluginRoot] 本插件根目录(默认本模块所在插件根)。
471
+ * @param {string} [options.dshHome] DSH 主目录(默认 `DSH_HOME` 或 `~/.dsh`);隔离测试用。
472
+ * @returns {object} 结构化结果:`{ ok, status, reason, alias, plugin, declaration,
473
+ * discoveredVia?, carrier?, manifest?, apk?, pluginContent?, candidates? }`。
474
+ * `status` 为 found/missing/ambiguous/mismatch/error,`!ok` 时 `reason` 是稳定原因码。
475
+ * 绝对路径只在插件内部使用;对外展示请用 `describeAppCarrier()` 脱敏。
476
+ */
477
+ export function discoverAppCarrier(options = {}) {
478
+ const pluginRoot = typeof options.pluginRoot === "string" && options.pluginRoot !== "" ? options.pluginRoot : defaultPluginRoot();
479
+ const plugin = pluginIdentityFor(pluginRoot);
480
+ if (!plugin.ok) {
481
+ return { ok: false, status: APP_CARRIER_STATUS.ERROR, reason: plugin.reason, alias: null, plugin: null };
482
+ }
483
+ const alias = appCarrierAlias(plugin.name);
484
+ const declaration = declaredCarrierDependency(plugin.pkg, alias);
485
+ const result = baseResult(APP_CARRIER_STATUS.MISSING, "carrier-not-installed", plugin, alias);
486
+ result.declaration = {
487
+ declared: declaration.declared,
488
+ field: declaration.field,
489
+ spec: declaration.spec,
490
+ kind: declaration.declared ? declarationKind(declaration.spec, plugin.name, plugin.version) : null,
491
+ };
492
+ const collected = collectCarrierCandidates(plugin.root, plugin.name, alias, options);
493
+ if (collected.errors.length > 0 && collected.candidates.length === 0) {
494
+ const error = collected.errors[0];
495
+ return {
496
+ ...result,
497
+ status: APP_CARRIER_STATUS.ERROR,
498
+ reason: error.reason,
499
+ discoveredVia: error.via,
500
+ };
501
+ }
502
+ if (collected.candidates.length === 0) return result;
503
+ if (collected.candidates.length > 1) {
504
+ return {
505
+ ...result,
506
+ status: APP_CARRIER_STATUS.AMBIGUOUS,
507
+ reason: "carrier-multiple-candidates",
508
+ // 只暴露来源与真实目录,供上层诊断;绝不替调用方挑一个。
509
+ candidates: collected.candidates.map((candidate) => ({ via: candidate.via, source: candidate.source })),
510
+ };
511
+ }
512
+ const candidate = collected.candidates[0];
513
+ const validated = validateCarrierCandidate(candidate, plugin);
514
+ if (!validated.ok) {
515
+ return {
516
+ ...result,
517
+ status: APP_CARRIER_STATUS.MISMATCH,
518
+ reason: validated.reason,
519
+ discoveredVia: candidate.via,
520
+ };
521
+ }
522
+ return {
523
+ ...result,
524
+ ...validated,
525
+ ok: true,
526
+ status: APP_CARRIER_STATUS.FOUND,
527
+ reason: "ok",
528
+ };
529
+ }
530
+
531
+ /**
532
+ * 运行时依赖闭包排除判定(设计稿 §5.5)。同步、fail-closed。
533
+ *
534
+ * 只有同时满足以下条件才返回 `exclude: true`:
535
+ * 1. 根包 `package.json` 在 `optionalDependencies`/`dependencies` 里**显式声明**了载体别名;
536
+ * 声明是精确绑定(精确 npm 别名,或本地测试通道的目录绑定);
537
+ * 2. 从根包自身作用域能唯一解析到该别名;
538
+ * 3. 该候选通过 `validateCarrierCandidate` 的全部身份校验。
539
+ * 普通依赖自报 `dsh.appCarrier`、范围声明、多候选、校验失败都返回 `exclude: false`。
540
+ *
541
+ * @param {{ root: string }} [input] `root` 为待建图/待冻结/待校验的包根目录。
542
+ * @returns {{ exclude: boolean, reason: string, hard?: boolean, alias?: string, source?: string }}
543
+ * `hard: true` 表示根包**确实**声明了载体且作用域里也确实存在候选,但候选身份校验失败
544
+ * 或多义。闭包调用方应据此 fail-closed(不能把无入口的数据包当运行依赖去解析)。
545
+ */
546
+ export function appCarrierExclusion(input = {}) {
547
+ const root = input?.root;
548
+ let alias = null;
549
+ try {
550
+ const plugin = pluginIdentityFor(root);
551
+ if (!plugin.ok) return { exclude: false, reason: plugin.reason };
552
+ alias = appCarrierAlias(plugin.name);
553
+ const declaration = declaredCarrierDependency(plugin.pkg, alias);
554
+ if (!declaration.declared) return { exclude: false, reason: "carrier-not-declared", alias };
555
+ if (declarationKind(declaration.spec, plugin.name, plugin.version) === "inexact") {
556
+ return { exclude: false, reason: "carrier-declaration-not-exact", alias };
557
+ }
558
+ const scanned = scanCarrierScope(plugin.root, alias, "plugin");
559
+ if (scanned.candidates.length === 0) {
560
+ return { exclude: false, reason: scanned.errors[0]?.reason ?? "carrier-not-installed", alias };
561
+ }
562
+ if (scanned.candidates.length > 1) return { exclude: false, hard: true, reason: "carrier-multiple-candidates", alias };
563
+ const validated = validateCarrierCandidate(scanned.candidates[0], plugin);
564
+ if (!validated.ok) return { exclude: false, hard: true, reason: validated.reason, alias };
565
+ return {
566
+ exclude: true,
567
+ reason: "ok",
568
+ alias,
569
+ source: validated.carrier.source,
570
+ carrier: validated.carrier,
571
+ apk: validated.apk,
572
+ };
573
+ } catch (error) {
574
+ // 任何意外都按"不排除"处理:宁可多复制字节,也不让伪造/损坏的包绕过校验。
575
+ return { exclude: false, reason: "carrier-check-failed", alias, detail: String(error?.message ?? error) };
576
+ }
577
+ }
578
+
579
+ /**
580
+ * 对外诊断视图(设计稿 §5.10:不含凭据与本机绝对路径)。
581
+ * 上层展示发现路径、别名、版本、校验结论与稳定 reason 时只用这个结果。
582
+ */
583
+ export function describeAppCarrier(result) {
584
+ if (!result || typeof result !== "object") return { status: "error", reason: "carrier-check-failed" };
585
+ return {
586
+ status: result.status ?? "error",
587
+ reason: result.reason ?? "carrier-check-failed",
588
+ alias: result.alias ?? null,
589
+ declared: result.declaration?.declared === true,
590
+ declarationKind: result.declaration?.kind ?? null,
591
+ locatedVia: result.discoveredVia ?? null,
592
+ plugin: result.plugin ? { name: result.plugin.name, version: result.plugin.version } : null,
593
+ carrier: result.carrier ? { name: result.carrier.name, version: result.carrier.version } : null,
594
+ manifest: result.manifest ? {
595
+ schema: result.manifest.schema,
596
+ appVersion: result.manifest.appVersion,
597
+ pluginVersion: result.manifest.pluginVersion,
598
+ } : null,
599
+ apk: result.apk ? { file: result.apk.file, size: result.apk.size, sha256: result.apk.sha256 } : null,
600
+ candidateCount: Array.isArray(result.candidates) ? result.candidates.length : (result.carrier ? 1 : 0),
601
+ };
602
+ }
603
+
604
+ // 供上层接线时拼装 APK 绝对路径(仅在插件内部使用,不直接展示)。
605
+ export function appCarrierApkPath(result) {
606
+ const source = result?.carrier?.source;
607
+ const file = result?.apk?.file;
608
+ return typeof source === "string" && typeof file === "string" ? join(source, file) : null;
609
+ }