waybetter 0.1.0 → 0.6.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.
Files changed (515) hide show
  1. package/README.md +143 -9
  2. package/bin/waybetter.mjs +46 -0
  3. package/build/runtime.mjs +105 -0
  4. package/cli/buildenv.mjs +130 -0
  5. package/cli/dev.mjs +97 -0
  6. package/cli/i18n.mjs +382 -0
  7. package/cli/manifests.mjs +383 -0
  8. package/cli/privacy.mjs +768 -0
  9. package/cli/release.mjs +444 -0
  10. package/cli/remote.mjs +260 -0
  11. package/cli/store.mjs +326 -0
  12. package/cli/strings.mjs +121 -0
  13. package/dist/runtime/column.d.ts +93 -0
  14. package/dist/runtime/column.js +451 -0
  15. package/dist/runtime/column.js.map +1 -0
  16. package/dist/runtime/commandReport.d.ts +19 -0
  17. package/dist/runtime/commandReport.js +47 -0
  18. package/dist/runtime/commandReport.js.map +1 -0
  19. package/dist/runtime/commands.d.ts +72 -0
  20. package/dist/runtime/commands.js +214 -0
  21. package/dist/runtime/commands.js.map +1 -0
  22. package/dist/runtime/devErrors.d.ts +16 -0
  23. package/dist/runtime/devErrors.js +30 -0
  24. package/dist/runtime/devErrors.js.map +1 -0
  25. package/dist/runtime/domHub.d.ts +21 -0
  26. package/dist/runtime/domHub.js +139 -4
  27. package/dist/runtime/domHub.js.map +1 -1
  28. package/dist/runtime/featureModules.d.ts +157 -0
  29. package/dist/runtime/featureModules.js +484 -0
  30. package/dist/runtime/featureModules.js.map +1 -0
  31. package/dist/runtime/featureTable.d.ts +144 -0
  32. package/dist/runtime/featureTable.js +223 -0
  33. package/dist/runtime/featureTable.js.map +1 -0
  34. package/dist/runtime/fonts.d.ts +22 -0
  35. package/dist/runtime/fonts.js +72 -0
  36. package/dist/runtime/fonts.js.map +1 -0
  37. package/dist/runtime/i18n.d.ts +138 -0
  38. package/dist/runtime/i18n.js +614 -0
  39. package/dist/runtime/i18n.js.map +1 -0
  40. package/dist/runtime/i18nCheck.d.ts +30 -0
  41. package/dist/runtime/i18nCheck.js +224 -0
  42. package/dist/runtime/i18nCheck.js.map +1 -0
  43. package/dist/runtime/pageLang.d.ts +8 -0
  44. package/dist/runtime/pageLang.js +64 -0
  45. package/dist/runtime/pageLang.js.map +1 -0
  46. package/dist/runtime/pageStrings.d.ts +35 -0
  47. package/dist/runtime/pageStrings.js +76 -0
  48. package/dist/runtime/pageStrings.js.map +1 -0
  49. package/dist/runtime/panelButton.d.ts +33 -0
  50. package/dist/runtime/panelButton.js +83 -0
  51. package/dist/runtime/panelButton.js.map +1 -0
  52. package/dist/runtime/permissions.d.ts +47 -0
  53. package/dist/runtime/permissions.js +141 -0
  54. package/dist/runtime/permissions.js.map +1 -0
  55. package/dist/runtime/pluginApi.d.ts +51 -2
  56. package/dist/runtime/pluginApi.js +137 -22
  57. package/dist/runtime/pluginApi.js.map +1 -1
  58. package/dist/runtime/pluginI18n.d.ts +29 -0
  59. package/dist/runtime/pluginI18n.js +119 -0
  60. package/dist/runtime/pluginI18n.js.map +1 -0
  61. package/dist/runtime/settingsForm.d.ts +66 -0
  62. package/dist/runtime/settingsForm.js +114 -0
  63. package/dist/runtime/settingsForm.js.map +1 -0
  64. package/dist/runtime/site.d.ts +5 -0
  65. package/dist/runtime/site.js +1 -1
  66. package/dist/runtime/site.js.map +1 -1
  67. package/dist/runtime/siteRuntime.d.ts +144 -0
  68. package/dist/runtime/siteRuntime.js +175 -0
  69. package/dist/runtime/siteRuntime.js.map +1 -0
  70. package/dist/runtime/stringPackHook.d.ts +84 -0
  71. package/dist/runtime/stringPackHook.js +220 -0
  72. package/dist/runtime/stringPackHook.js.map +1 -0
  73. package/dist/runtime/stringPackTranslate.d.ts +59 -0
  74. package/dist/runtime/stringPackTranslate.js +264 -0
  75. package/dist/runtime/stringPackTranslate.js.map +1 -0
  76. package/dist/runtime/stringPacks.d.ts +200 -0
  77. package/dist/runtime/stringPacks.js +474 -0
  78. package/dist/runtime/stringPacks.js.map +1 -0
  79. package/dist/runtime/styles.d.ts +15 -0
  80. package/dist/runtime/styles.js +81 -0
  81. package/dist/runtime/styles.js.map +1 -0
  82. package/dist/runtime/textNet.d.ts +15 -0
  83. package/dist/runtime/textNet.js +131 -0
  84. package/dist/runtime/textNet.js.map +1 -0
  85. package/dist/runtime/themeApi.d.ts +121 -0
  86. package/dist/runtime/themeApi.js +220 -0
  87. package/dist/runtime/themeApi.js.map +1 -0
  88. package/dist/runtime/themeChoice.d.ts +52 -0
  89. package/dist/runtime/themeChoice.js +69 -0
  90. package/dist/runtime/themeChoice.js.map +1 -0
  91. package/dist/runtime/themeComponents.d.ts +435 -0
  92. package/dist/runtime/themeComponents.js +463 -0
  93. package/dist/runtime/themeComponents.js.map +1 -0
  94. package/dist/runtime/themeDecor.d.ts +73 -0
  95. package/dist/runtime/themeDecor.js +131 -0
  96. package/dist/runtime/themeDecor.js.map +1 -0
  97. package/dist/runtime/themeDoctor.d.ts +11 -0
  98. package/dist/runtime/themeDoctor.js +51 -0
  99. package/dist/runtime/themeDoctor.js.map +1 -0
  100. package/dist/runtime/themeIcons.d.ts +130 -0
  101. package/dist/runtime/themeIcons.js +263 -0
  102. package/dist/runtime/themeIcons.js.map +1 -0
  103. package/dist/runtime/themeRuntime.d.ts +100 -0
  104. package/dist/runtime/themeRuntime.js +497 -0
  105. package/dist/runtime/themeRuntime.js.map +1 -0
  106. package/dist/runtime/themeSchema.d.ts +10 -0
  107. package/dist/runtime/themeSchema.js +150 -0
  108. package/dist/runtime/themeSchema.js.map +1 -0
  109. package/dist/runtime/themeSpec.d.ts +259 -0
  110. package/dist/runtime/themeSpec.js +439 -0
  111. package/dist/runtime/themeSpec.js.map +1 -0
  112. package/dist/runtime/themeTokens.d.ts +12 -0
  113. package/dist/runtime/themeTokens.js +14 -6
  114. package/dist/runtime/themeTokens.js.map +1 -1
  115. package/dist/runtime/themeVariants.d.ts +30 -0
  116. package/dist/runtime/themeVariants.js +53 -0
  117. package/dist/runtime/themeVariants.js.map +1 -0
  118. package/dist/runtime/ui.d.ts +192 -0
  119. package/dist/runtime/ui.js +926 -0
  120. package/dist/runtime/ui.js.map +1 -0
  121. package/dist/shell/InstallDialog.d.ts +40 -0
  122. package/dist/shell/InstallDialog.js +170 -0
  123. package/dist/shell/InstallDialog.js.map +1 -0
  124. package/dist/shell/StoreView.d.ts +66 -0
  125. package/dist/shell/StoreView.js +103 -0
  126. package/dist/shell/StoreView.js.map +1 -0
  127. package/dist/shell/addressModel.js.map +1 -1
  128. package/dist/shell/chrome.js.map +1 -1
  129. package/dist/shell/deckModel.d.ts +117 -0
  130. package/dist/shell/deckModel.js +267 -0
  131. package/dist/shell/deckModel.js.map +1 -0
  132. package/dist/shell/frame/AccountSwitcher.d.ts +42 -0
  133. package/dist/shell/frame/AccountSwitcher.js +243 -0
  134. package/dist/shell/frame/AccountSwitcher.js.map +1 -0
  135. package/dist/shell/frame/AddColumnDialog.d.ts +8 -0
  136. package/dist/shell/frame/AddColumnDialog.js +81 -0
  137. package/dist/shell/frame/AddColumnDialog.js.map +1 -0
  138. package/dist/shell/frame/AppMenu.d.ts +21 -0
  139. package/dist/shell/frame/AppMenu.js +642 -0
  140. package/dist/shell/frame/AppMenu.js.map +1 -0
  141. package/dist/shell/frame/CommandPalette.d.ts +7 -0
  142. package/dist/shell/frame/CommandPalette.js +118 -0
  143. package/dist/shell/frame/CommandPalette.js.map +1 -0
  144. package/dist/shell/frame/DeckView.d.ts +13 -0
  145. package/dist/shell/frame/DeckView.js +287 -0
  146. package/dist/shell/frame/DeckView.js.map +1 -0
  147. package/dist/shell/frame/FindBar.d.ts +10 -0
  148. package/dist/shell/frame/FindBar.js +108 -0
  149. package/dist/shell/frame/FindBar.js.map +1 -0
  150. package/dist/shell/frame/MediaPanel.d.ts +8 -0
  151. package/dist/shell/frame/MediaPanel.js +76 -0
  152. package/dist/shell/frame/MediaPanel.js.map +1 -0
  153. package/dist/shell/frame/NewTabPage.d.ts +9 -0
  154. package/dist/shell/frame/NewTabPage.js +71 -0
  155. package/dist/shell/frame/NewTabPage.js.map +1 -0
  156. package/dist/shell/frame/PaneTheme.d.ts +16 -0
  157. package/dist/shell/frame/PaneTheme.js +67 -0
  158. package/dist/shell/frame/PaneTheme.js.map +1 -0
  159. package/dist/shell/frame/ReportDialog.d.ts +3 -0
  160. package/dist/shell/frame/ReportDialog.js +95 -0
  161. package/dist/shell/frame/ReportDialog.js.map +1 -0
  162. package/dist/shell/frame/Shell.d.ts +8 -0
  163. package/dist/shell/frame/Shell.js +1131 -0
  164. package/dist/shell/frame/Shell.js.map +1 -0
  165. package/dist/shell/frame/Titlebar.d.ts +16 -0
  166. package/dist/shell/frame/Titlebar.js +77 -0
  167. package/dist/shell/frame/Titlebar.js.map +1 -0
  168. package/dist/shell/frame/TitlebarExtras.d.ts +4 -0
  169. package/dist/shell/frame/TitlebarExtras.js +202 -0
  170. package/dist/shell/frame/TitlebarExtras.js.map +1 -0
  171. package/dist/shell/frame/WebPanel.d.ts +4 -0
  172. package/dist/shell/frame/WebPanel.js +182 -0
  173. package/dist/shell/frame/WebPanel.js.map +1 -0
  174. package/dist/shell/frame/accountModel.d.ts +49 -0
  175. package/dist/shell/frame/accountModel.js +67 -0
  176. package/dist/shell/frame/accountModel.js.map +1 -0
  177. package/dist/shell/frame/accountSwitcher.css +91 -0
  178. package/dist/shell/frame/appI18n.d.ts +44 -0
  179. package/dist/shell/frame/appI18n.js +64 -0
  180. package/dist/shell/frame/appI18n.js.map +1 -0
  181. package/dist/shell/frame/appMenu.css +121 -0
  182. package/dist/shell/frame/backupModel.d.ts +38 -0
  183. package/dist/shell/frame/backupModel.js +192 -0
  184. package/dist/shell/frame/backupModel.js.map +1 -0
  185. package/dist/shell/frame/boot.d.ts +8 -0
  186. package/dist/shell/frame/boot.js +35 -0
  187. package/dist/shell/frame/boot.js.map +1 -0
  188. package/dist/shell/frame/brand.d.ts +1 -0
  189. package/dist/shell/frame/brand.js +4 -0
  190. package/dist/shell/frame/brand.js.map +1 -0
  191. package/dist/shell/frame/bridge.d.ts +35 -0
  192. package/dist/shell/frame/bridge.js +70 -0
  193. package/dist/shell/frame/bridge.js.map +1 -0
  194. package/dist/shell/frame/chromeTheme.d.ts +29 -0
  195. package/dist/shell/frame/chromeTheme.js +81 -0
  196. package/dist/shell/frame/chromeTheme.js.map +1 -0
  197. package/dist/shell/frame/deck.css +209 -0
  198. package/dist/shell/frame/deckStore.d.ts +89 -0
  199. package/dist/shell/frame/deckStore.js +239 -0
  200. package/dist/shell/frame/deckStore.js.map +1 -0
  201. package/dist/shell/frame/devHost.d.ts +35 -0
  202. package/dist/shell/frame/devHost.js +80 -0
  203. package/dist/shell/frame/devHost.js.map +1 -0
  204. package/dist/shell/frame/devModel.d.ts +7 -0
  205. package/dist/shell/frame/devModel.js +17 -0
  206. package/dist/shell/frame/devModel.js.map +1 -0
  207. package/dist/shell/frame/diag.d.ts +7 -0
  208. package/dist/shell/frame/diag.js +39 -0
  209. package/dist/shell/frame/diag.js.map +1 -0
  210. package/dist/shell/frame/findBar.css +14 -0
  211. package/dist/shell/frame/frameMessages.d.ts +248 -0
  212. package/dist/shell/frame/frameMessages.js +252 -0
  213. package/dist/shell/frame/frameMessages.js.map +1 -0
  214. package/dist/shell/frame/hooks.d.ts +39 -0
  215. package/dist/shell/frame/hooks.js +97 -0
  216. package/dist/shell/frame/hooks.js.map +1 -0
  217. package/dist/shell/frame/i18n.d.ts +27 -0
  218. package/dist/shell/frame/i18n.js +66 -0
  219. package/dist/shell/frame/i18n.js.map +1 -0
  220. package/dist/shell/frame/iconPack.d.ts +11 -0
  221. package/dist/shell/frame/iconPack.js +45 -0
  222. package/dist/shell/frame/iconPack.js.map +1 -0
  223. package/dist/shell/frame/icons.d.ts +71 -0
  224. package/dist/shell/frame/icons.js +75 -0
  225. package/dist/shell/frame/icons.js.map +1 -0
  226. package/dist/shell/frame/index.d.ts +34 -0
  227. package/dist/shell/frame/index.js +43 -0
  228. package/dist/shell/frame/index.js.map +1 -0
  229. package/dist/shell/frame/language.d.ts +58 -0
  230. package/dist/shell/frame/language.js +165 -0
  231. package/dist/shell/frame/language.js.map +1 -0
  232. package/dist/shell/frame/mediaModel.d.ts +34 -0
  233. package/dist/shell/frame/mediaModel.js +53 -0
  234. package/dist/shell/frame/mediaModel.js.map +1 -0
  235. package/dist/shell/frame/menuMessages.en.json +54 -0
  236. package/dist/shell/frame/menuModel.d.ts +277 -0
  237. package/dist/shell/frame/menuModel.js +293 -0
  238. package/dist/shell/frame/menuModel.js.map +1 -0
  239. package/dist/shell/frame/paletteModel.d.ts +139 -0
  240. package/dist/shell/frame/paletteModel.js +112 -0
  241. package/dist/shell/frame/paletteModel.js.map +1 -0
  242. package/dist/shell/frame/paneTheme.css +9 -0
  243. package/dist/shell/frame/pluginCommandStore.d.ts +23 -0
  244. package/dist/shell/frame/pluginCommandStore.js +70 -0
  245. package/dist/shell/frame/pluginCommandStore.js.map +1 -0
  246. package/dist/shell/frame/reportModel.d.ts +52 -0
  247. package/dist/shell/frame/reportModel.js +105 -0
  248. package/dist/shell/frame/reportModel.js.map +1 -0
  249. package/dist/shell/frame/safeStore.d.ts +7 -0
  250. package/dist/shell/frame/safeStore.js +40 -0
  251. package/dist/shell/frame/safeStore.js.map +1 -0
  252. package/dist/shell/frame/searchInput.d.ts +2 -0
  253. package/dist/shell/frame/searchInput.js +11 -0
  254. package/dist/shell/frame/searchInput.js.map +1 -0
  255. package/dist/shell/frame/shell.css +629 -0
  256. package/dist/shell/frame/site.d.ts +206 -0
  257. package/dist/shell/frame/site.js +22 -0
  258. package/dist/shell/frame/site.js.map +1 -0
  259. package/dist/shell/frame/state.d.ts +99 -0
  260. package/dist/shell/frame/state.js +284 -0
  261. package/dist/shell/frame/state.js.map +1 -0
  262. package/dist/shell/frame/syncGate.d.ts +10 -0
  263. package/dist/shell/frame/syncGate.js +36 -0
  264. package/dist/shell/frame/syncGate.js.map +1 -0
  265. package/dist/shell/frame/syncHost.d.ts +35 -0
  266. package/dist/shell/frame/syncHost.js +85 -0
  267. package/dist/shell/frame/syncHost.js.map +1 -0
  268. package/dist/shell/frame/tabStill.d.ts +2 -0
  269. package/dist/shell/frame/tabStill.js +62 -0
  270. package/dist/shell/frame/tabStill.js.map +1 -0
  271. package/dist/shell/frame/themeScopes.d.ts +29 -0
  272. package/dist/shell/frame/themeScopes.js +61 -0
  273. package/dist/shell/frame/themeScopes.js.map +1 -0
  274. package/dist/shell/frame/themeStore.d.ts +99 -0
  275. package/dist/shell/frame/themeStore.js +252 -0
  276. package/dist/shell/frame/themeStore.js.map +1 -0
  277. package/dist/shell/frame/windowBinding.d.ts +2 -0
  278. package/dist/shell/frame/windowBinding.js +7 -0
  279. package/dist/shell/frame/windowBinding.js.map +1 -0
  280. package/dist/shell/frame/workspaceModel.d.ts +82 -0
  281. package/dist/shell/frame/workspaceModel.js +250 -0
  282. package/dist/shell/frame/workspaceModel.js.map +1 -0
  283. package/dist/shell/frame/workspaces.css +84 -0
  284. package/dist/shell/frame/workspaces.d.ts +22 -0
  285. package/dist/shell/frame/workspaces.js +125 -0
  286. package/dist/shell/frame/workspaces.js.map +1 -0
  287. package/dist/shell/frame/zoomStore.d.ts +10 -0
  288. package/dist/shell/frame/zoomStore.js +65 -0
  289. package/dist/shell/frame/zoomStore.js.map +1 -0
  290. package/dist/shell/installController.d.ts +122 -0
  291. package/dist/shell/installController.js +146 -0
  292. package/dist/shell/installController.js.map +1 -0
  293. package/dist/shell/installerMessages.d.ts +30 -0
  294. package/dist/shell/installerMessages.js +36 -0
  295. package/dist/shell/installerMessages.js.map +1 -0
  296. package/dist/shell/linkDrop.js.map +1 -1
  297. package/dist/shell/messages.d.ts +16 -0
  298. package/dist/shell/messages.js +27 -0
  299. package/dist/shell/messages.js.map +1 -0
  300. package/dist/shell/palette.js.map +1 -1
  301. package/dist/shell/paneModel.d.ts +22 -0
  302. package/dist/shell/paneModel.js +24 -1
  303. package/dist/shell/paneModel.js.map +1 -1
  304. package/dist/shell/panel/Appearance.d.ts +8 -0
  305. package/dist/shell/panel/Appearance.js +256 -0
  306. package/dist/shell/panel/Appearance.js.map +1 -0
  307. package/dist/shell/panel/BackupPanel.d.ts +5 -0
  308. package/dist/shell/panel/BackupPanel.js +198 -0
  309. package/dist/shell/panel/BackupPanel.js.map +1 -0
  310. package/dist/shell/panel/DeveloperPanel.d.ts +4 -0
  311. package/dist/shell/panel/DeveloperPanel.js +51 -0
  312. package/dist/shell/panel/DeveloperPanel.js.map +1 -0
  313. package/dist/shell/panel/FeatureToggles.d.ts +13 -0
  314. package/dist/shell/panel/FeatureToggles.js +42 -0
  315. package/dist/shell/panel/FeatureToggles.js.map +1 -0
  316. package/dist/shell/panel/Home.d.ts +3 -0
  317. package/dist/shell/panel/Home.js +23 -0
  318. package/dist/shell/panel/Home.js.map +1 -0
  319. package/dist/shell/panel/IconPackInstall.d.ts +15 -0
  320. package/dist/shell/panel/IconPackInstall.js +41 -0
  321. package/dist/shell/panel/IconPackInstall.js.map +1 -0
  322. package/dist/shell/panel/LanguageSettings.d.ts +1 -0
  323. package/dist/shell/panel/LanguageSettings.js +54 -0
  324. package/dist/shell/panel/LanguageSettings.js.map +1 -0
  325. package/dist/shell/panel/MakePanel.d.ts +3 -0
  326. package/dist/shell/panel/MakePanel.js +237 -0
  327. package/dist/shell/panel/MakePanel.js.map +1 -0
  328. package/dist/shell/panel/MarkdownDoc.d.ts +6 -0
  329. package/dist/shell/panel/MarkdownDoc.js +35 -0
  330. package/dist/shell/panel/MarkdownDoc.js.map +1 -0
  331. package/dist/shell/panel/Modes.d.ts +40 -0
  332. package/dist/shell/panel/Modes.js +270 -0
  333. package/dist/shell/panel/Modes.js.map +1 -0
  334. package/dist/shell/panel/NavIcon.d.ts +4 -0
  335. package/dist/shell/panel/NavIcon.js +6 -0
  336. package/dist/shell/panel/NavIcon.js.map +1 -0
  337. package/dist/shell/panel/Panel.d.ts +7 -0
  338. package/dist/shell/panel/Panel.js +139 -0
  339. package/dist/shell/panel/Panel.js.map +1 -0
  340. package/dist/shell/panel/PluginFeatures.d.ts +24 -0
  341. package/dist/shell/panel/PluginFeatures.js +165 -0
  342. package/dist/shell/panel/PluginFeatures.js.map +1 -0
  343. package/dist/shell/panel/Plugins.d.ts +33 -0
  344. package/dist/shell/panel/Plugins.js +112 -0
  345. package/dist/shell/panel/Plugins.js.map +1 -0
  346. package/dist/shell/panel/PowerSettings.d.ts +4 -0
  347. package/dist/shell/panel/PowerSettings.js +52 -0
  348. package/dist/shell/panel/PowerSettings.js.map +1 -0
  349. package/dist/shell/panel/Settings.d.ts +23 -0
  350. package/dist/shell/panel/Settings.js +116 -0
  351. package/dist/shell/panel/Settings.js.map +1 -0
  352. package/dist/shell/panel/ShareSetup.d.ts +8 -0
  353. package/dist/shell/panel/ShareSetup.js +130 -0
  354. package/dist/shell/panel/ShareSetup.js.map +1 -0
  355. package/dist/shell/panel/StoreSection.d.ts +6 -0
  356. package/dist/shell/panel/StoreSection.js +48 -0
  357. package/dist/shell/panel/StoreSection.js.map +1 -0
  358. package/dist/shell/panel/StringPacks.d.ts +11 -0
  359. package/dist/shell/panel/StringPacks.js +106 -0
  360. package/dist/shell/panel/StringPacks.js.map +1 -0
  361. package/dist/shell/panel/ThemeBuilder.d.ts +5 -0
  362. package/dist/shell/panel/ThemeBuilder.js +205 -0
  363. package/dist/shell/panel/ThemeBuilder.js.map +1 -0
  364. package/dist/shell/panel/backup.css +34 -0
  365. package/dist/shell/panel/builder.css +232 -0
  366. package/dist/shell/panel/builderModel.d.ts +55 -0
  367. package/dist/shell/panel/builderModel.js +95 -0
  368. package/dist/shell/panel/builderModel.js.map +1 -0
  369. package/dist/shell/panel/compare.d.ts +10 -0
  370. package/dist/shell/panel/compare.js +51 -0
  371. package/dist/shell/panel/compare.js.map +1 -0
  372. package/dist/shell/panel/controls.d.ts +135 -0
  373. package/dist/shell/panel/controls.js +213 -0
  374. package/dist/shell/panel/controls.js.map +1 -0
  375. package/dist/shell/panel/featureModel.d.ts +69 -0
  376. package/dist/shell/panel/featureModel.js +79 -0
  377. package/dist/shell/panel/featureModel.js.map +1 -0
  378. package/dist/shell/panel/fonts/FontBrowser.d.ts +10 -0
  379. package/dist/shell/panel/fonts/FontBrowser.js +189 -0
  380. package/dist/shell/panel/fonts/FontBrowser.js.map +1 -0
  381. package/dist/shell/panel/fonts/FontRoles.d.ts +20 -0
  382. package/dist/shell/panel/fonts/FontRoles.js +77 -0
  383. package/dist/shell/panel/fonts/FontRoles.js.map +1 -0
  384. package/dist/shell/panel/fonts/catalog.d.ts +37 -0
  385. package/dist/shell/panel/fonts/catalog.js +68 -0
  386. package/dist/shell/panel/fonts/catalog.js.map +1 -0
  387. package/dist/shell/panel/fonts/googleFontsCatalog.json +1 -0
  388. package/dist/shell/panel/index.d.ts +20 -0
  389. package/dist/shell/panel/index.js +34 -0
  390. package/dist/shell/panel/index.js.map +1 -0
  391. package/dist/shell/panel/languageModel.d.ts +5 -0
  392. package/dist/shell/panel/languageModel.js +7 -0
  393. package/dist/shell/panel/languageModel.js.map +1 -0
  394. package/dist/shell/panel/make.css +72 -0
  395. package/dist/shell/panel/makeModel.d.ts +32 -0
  396. package/dist/shell/panel/makeModel.js +84 -0
  397. package/dist/shell/panel/makeModel.js.map +1 -0
  398. package/dist/shell/panel/markdown.d.ts +46 -0
  399. package/dist/shell/panel/markdown.js +100 -0
  400. package/dist/shell/panel/markdown.js.map +1 -0
  401. package/dist/shell/panel/nav.d.ts +12 -0
  402. package/dist/shell/panel/nav.js +50 -0
  403. package/dist/shell/panel/nav.js.map +1 -0
  404. package/dist/shell/panel/panel.css +401 -0
  405. package/dist/shell/panel/panelMessages.d.ts +763 -0
  406. package/dist/shell/panel/panelMessages.js +767 -0
  407. package/dist/shell/panel/panelMessages.js.map +1 -0
  408. package/dist/shell/panel/plugins.css +59 -0
  409. package/dist/shell/panel/setupModel.d.ts +87 -0
  410. package/dist/shell/panel/setupModel.js +152 -0
  411. package/dist/shell/panel/setupModel.js.map +1 -0
  412. package/dist/shell/panel/site.d.ts +259 -0
  413. package/dist/shell/panel/site.js +25 -0
  414. package/dist/shell/panel/site.js.map +1 -0
  415. package/dist/shell/panel/strings.css +7 -0
  416. package/dist/shell/panel/themes.d.ts +10 -0
  417. package/dist/shell/panel/themes.js +8 -0
  418. package/dist/shell/panel/themes.js.map +1 -0
  419. package/dist/shell/profileModel.d.ts +3 -2
  420. package/dist/shell/profileModel.js +7 -5
  421. package/dist/shell/profileModel.js.map +1 -1
  422. package/dist/shell/store.css +230 -0
  423. package/dist/shell/storeMessages.d.ts +169 -0
  424. package/dist/shell/storeMessages.js +172 -0
  425. package/dist/shell/storeMessages.js.map +1 -0
  426. package/dist/shell/storeModel.d.ts +181 -0
  427. package/dist/shell/storeModel.js +133 -0
  428. package/dist/shell/storeModel.js.map +1 -0
  429. package/dist/shell/stringPacksModel.d.ts +44 -0
  430. package/dist/shell/stringPacksModel.js +94 -0
  431. package/dist/shell/stringPacksModel.js.map +1 -0
  432. package/dist/shell/updateModel.d.ts +2 -1
  433. package/dist/shell/updateModel.js +5 -4
  434. package/dist/shell/updateModel.js.map +1 -1
  435. package/dist/shell/windowControls.d.ts +4 -0
  436. package/dist/shell/windowControls.js +5 -0
  437. package/dist/shell/windowControls.js.map +1 -1
  438. package/i18n-config.schema.json +29 -0
  439. package/i18n-meta.schema.json +26 -0
  440. package/i18n.schema.json +42 -0
  441. package/icons.schema.json +646 -0
  442. package/languages.schema.json +23 -0
  443. package/locales/ar-x-fm.json +11 -0
  444. package/locales/ar.json +1281 -0
  445. package/locales/bg.json +1280 -0
  446. package/locales/bn.json +1280 -0
  447. package/locales/ca.json +1280 -0
  448. package/locales/cs.json +1280 -0
  449. package/locales/da.json +1280 -0
  450. package/locales/de.json +1280 -0
  451. package/locales/el.json +1280 -0
  452. package/locales/en-GB.json +27 -0
  453. package/locales/en.json +1280 -0
  454. package/locales/en.meta.json +51 -0
  455. package/locales/es.json +1280 -0
  456. package/locales/eu.json +1280 -0
  457. package/locales/fa.json +1281 -0
  458. package/locales/fi.json +1280 -0
  459. package/locales/fil.json +1280 -0
  460. package/locales/fr.json +1280 -0
  461. package/locales/ga.json +1280 -0
  462. package/locales/gl.json +1280 -0
  463. package/locales/gu.json +1280 -0
  464. package/locales/ha.json +1280 -0
  465. package/locales/he.json +1281 -0
  466. package/locales/hi.json +1280 -0
  467. package/locales/hr.json +1280 -0
  468. package/locales/hu.json +1279 -0
  469. package/locales/id.json +1280 -0
  470. package/locales/ig.json +1280 -0
  471. package/locales/it.json +1280 -0
  472. package/locales/ja.json +1280 -0
  473. package/locales/kn.json +1280 -0
  474. package/locales/ko.json +1280 -0
  475. package/locales/mr.json +1280 -0
  476. package/locales/ms.json +1280 -0
  477. package/locales/nb.json +1280 -0
  478. package/locales/nl.json +1280 -0
  479. package/locales/pl.json +1280 -0
  480. package/locales/pt.json +1280 -0
  481. package/locales/ro.json +1280 -0
  482. package/locales/ru.json +1280 -0
  483. package/locales/sk.json +1280 -0
  484. package/locales/sr.json +1280 -0
  485. package/locales/sv.json +1280 -0
  486. package/locales/ta.json +1280 -0
  487. package/locales/th.json +1280 -0
  488. package/locales/tr.json +1280 -0
  489. package/locales/uk.json +1280 -0
  490. package/locales/ur.json +1281 -0
  491. package/locales/vi.json +1280 -0
  492. package/locales/yo.json +1280 -0
  493. package/locales/zh-Hans.json +1280 -0
  494. package/locales/zh-Hant.json +1280 -0
  495. package/package.json +148 -6
  496. package/store-catalog.schema.json +65 -0
  497. package/theme.schema.json +4659 -0
  498. package/windows/installer.nsi +926 -0
  499. package/windows/languages/Basque.nsh +28 -0
  500. package/windows/languages/Catalan.nsh +28 -0
  501. package/windows/languages/Croatian.nsh +28 -0
  502. package/windows/languages/Czech.nsh +28 -0
  503. package/windows/languages/Danish.nsh +28 -0
  504. package/windows/languages/Finnish.nsh +28 -0
  505. package/windows/languages/Galician.nsh +28 -0
  506. package/windows/languages/Greek.nsh +28 -0
  507. package/windows/languages/Hungarian.nsh +28 -0
  508. package/windows/languages/Indonesian.nsh +28 -0
  509. package/windows/languages/Irish.nsh +28 -0
  510. package/windows/languages/Malay.nsh +28 -0
  511. package/windows/languages/Polish.nsh +28 -0
  512. package/windows/languages/Romanian.nsh +28 -0
  513. package/windows/languages/Serbian.nsh +28 -0
  514. package/windows/languages/Slovak.nsh +28 -0
  515. package/windows/languages/Thai.nsh +28 -0
@@ -0,0 +1,768 @@
1
+ // `waybetter privacy-scan`: the release gate that keeps personal names and account names out of
2
+ // everything an app publishes. It reads a list of forbidden terms (one per line, kept OUTSIDE every
3
+ // repository), always adds this machine's account name and home folder name, and searches files,
4
+ // folders, archives, git-tracked files and (with --history) a whole repository for them.
5
+ //
6
+ // - Text and binary alike: every file is searched as bytes, in UTF-8, UTF-16LE and UTF-16BE.
7
+ // - Containers are opened: zip (and .nupkg/.jar), tar, gzip, zstd, ar/.deb, and with an external
8
+ // tool xz/bzip2 (xz, bzip2 or 7-Zip), NSIS installers, AppImage/squashfs (7-Zip or unsquashfs)
9
+ // and DMG (zlib images natively, others with 7-Zip). A container that could not be opened is an
10
+ // error, not a pass.
11
+ // - A term is never printed. A hit shows the file, line:column or byte offset, and the term as its
12
+ // first letter plus `***`; the context shown has every term masked the same way.
13
+ //
14
+ // Matching: a term is a hit as a whole word, case-insensitively (letters with diacritics count as
15
+ // their base letter). A term of three characters or fewer is usually also an ordinary word fragment
16
+ // (`max(`, `max-width`), so it is a hit only in a path (`Users\<it>`, `/home/<it>`, `\<it>\`,
17
+ // `/<it>/`, `Users-<it>-`) or as the capitalized standalone word.
18
+ //
19
+ // Reviewed false positives go in an allowlist file inside the repository: lines of
20
+ // `<file glob>: <regular expression>`, where the expression is tested against the hit's context with
21
+ // the term replaced by `<term>` (another hit nearby reads `<other>`). The file never contains a term (the scan refuses one that does).
22
+ import { spawnSync } from 'node:child_process';
23
+ import { closeSync, existsSync, lstatSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
24
+ import { homedir, tmpdir, userInfo } from 'node:os';
25
+ import { basename, dirname, join, relative, resolve } from 'node:path';
26
+ import * as zlib from 'node:zlib';
27
+
28
+ export const TERMS_ENV = 'WAYBETTER_FORBIDDEN_TERMS';
29
+ export const SEVEN_ZIP_ENV = 'WAYBETTER_7Z';
30
+ const CONFIG_FILE = 'waybetter.release.json';
31
+
32
+ export const PRIVACY_USAGE = `Usage:
33
+ waybetter privacy-scan <paths...> [--terms <file>] [--allow <file>] [--tracked] [--untracked]
34
+ [--history] [--all] [--json] [--if-terms] [--no-auto]
35
+ Searches files, folders and archives for forbidden terms (personal names, account names) and
36
+ exits 1 on a hit, 2 when something could not be opened. Terms: --terms, else the file named by
37
+ ${TERMS_ENV}, else "forbiddenTerms" in ${CONFIG_FILE}; this machine's account name and
38
+ home folder name are always added (--no-auto: not). A term is never printed.
39
+ --tracked each path is inside a git work tree: only the files git tracks (and their names)
40
+ --untracked with --tracked: also files git neither tracks nor ignores
41
+ --history each path is a repository: every blob, path, commit message, tag and identity
42
+ --allow reviewed false positives: lines of "<file glob>: <regex over the context, the
43
+ term written <term>>" (default: "privacyAllowlist" in ${CONFIG_FILE})
44
+ --all list every hit (default: the first 5 of each file)
45
+ --if-terms do nothing, quietly, when no terms file exists (for a contributor's test run)`;
46
+
47
+ class PrivacyError extends Error {}
48
+ const fail = message => { throw new PrivacyError(message); };
49
+
50
+ // ---------------------------------------------------------------------------------------------
51
+ // Terms
52
+
53
+ /** Account names that say nothing about a person: never added automatically. */
54
+ const GENERIC_ACCOUNTS = new Set(['root', 'user', 'users', 'admin', 'administrator', 'runner', 'runneradmin', 'ubuntu', 'build',
55
+ 'builder', 'ci', 'docker', 'node', 'vagrant', 'default', 'nobody', 'home', 'dev', 'developer', 'owner', 'guest', 'test',
56
+ 'app', 'work', 'vscode', 'codespace', 'gitpod', 'circleci', 'jenkins', 'buildkite', 'ec2-user', 'azureuser', 'pi', 'public']);
57
+
58
+ /** The lines of a terms file: one term each, `#` comments and blank lines skipped. */
59
+ export function parseTerms(text) {
60
+ return text.split(/\r?\n/).map(line => line.trim()).filter(line => line && !line.startsWith('#'));
61
+ }
62
+
63
+ const expandHome = value => value.replace(/^~(?=$|[\\/])/, homedir());
64
+
65
+ /** The nearest waybetter.release.json's privacy settings (paths made absolute), or {}. */
66
+ export function privacyConfig(cwd) {
67
+ for (let dir = resolve(cwd); ; dir = dirname(dir)) {
68
+ const file = join(dir, CONFIG_FILE);
69
+ if (existsSync(file)) {
70
+ let raw;
71
+ try { raw = JSON.parse(readFileSync(file, 'utf8')); } catch { return {}; }
72
+ const at = value => (typeof value === 'string' && value.trim() ? resolve(dir, expandHome(value.trim())) : undefined);
73
+ return { root: dir, forbiddenTerms: at(raw.forbiddenTerms), privacyAllowlist: at(raw.privacyAllowlist) };
74
+ }
75
+ if (dirname(dir) === dir) return {};
76
+ }
77
+ }
78
+
79
+ /**
80
+ * The terms to search for: the file's lines plus (unless `auto` is false) this machine's account
81
+ * name and home folder name. `file` may be missing (then only the automatic ones).
82
+ * Returns `{ terms: [{ text, rule }], fromFile }`; rule is "word" or "account" (see the header).
83
+ */
84
+ export function collectTerms({ file, auto = true, extra = [] } = {}) {
85
+ const seen = new Map();
86
+ const add = (text, automatic) => {
87
+ const value = text.trim();
88
+ const key = value.toLowerCase();
89
+ if (value.length < 2 || seen.has(key)) return;
90
+ if (automatic && GENERIC_ACCOUNTS.has(key)) return;
91
+ seen.set(key, { text: value, rule: [...value].length <= 3 ? 'account' : 'word' });
92
+ };
93
+ let fromFile = 0;
94
+ if (file) {
95
+ for (const term of parseTerms(readFileSync(file, 'utf8'))) add(term, false);
96
+ fromFile = seen.size;
97
+ }
98
+ for (const term of extra) add(term, false);
99
+ if (auto) {
100
+ let user = '';
101
+ try { user = userInfo().username; } catch { /* no passwd entry */ }
102
+ add(user, true);
103
+ add(basename(homedir()), true);
104
+ }
105
+ return { terms: [...seen.values()], fromFile };
106
+ }
107
+
108
+ // ---------------------------------------------------------------------------------------------
109
+ // Matching (on bytes, read as a latin1 string so one regular expression sees every encoding)
110
+
111
+ const ENCODINGS = ['utf8', 'utf16le', 'utf16be'];
112
+ const FOLDS = { a: 'àáâãäåā', c: 'çćč', e: 'èéêëēęě', g: 'ğ', i: 'ìíîïıī', l: 'ł', n: 'ñńň', o: 'òóôõöøō', r: 'ř', s: 'şšśș', t: 'țť', u: 'ùúûüūů', y: 'ýÿ', z: 'žźż' };
113
+
114
+ function bytesOf(text, encoding) {
115
+ if (encoding === 'utf8') return Buffer.from(text, 'utf8');
116
+ const bytes = Buffer.from(text, 'utf16le');
117
+ return encoding === 'utf16be' ? bytes.swap16() : bytes;
118
+ }
119
+ const hex = bytes => [...bytes].map(byte => `\\x${byte.toString(16).padStart(2, '0')}`).join('');
120
+
121
+ /** One character in one encoding; `fold`: either case, and the letter's diacritic forms. */
122
+ function unit(char, encoding, fold) {
123
+ const forms = new Set([char]);
124
+ if (fold) {
125
+ const lower = char.toLowerCase();
126
+ for (const form of [lower, char.toUpperCase(), ...(FOLDS[lower] ?? '')]) {
127
+ forms.add(form);
128
+ forms.add(form.toUpperCase());
129
+ }
130
+ if (lower === 'i') forms.add('İ');
131
+ }
132
+ const encoded = [...new Set([...forms].filter(form => [...form].length === 1).map(form => hex(bytesOf(form, encoding))))];
133
+ return encoded.length === 1 ? encoded[0] : `(?:${encoded.join('|')})`;
134
+ }
135
+ const literal = (text, encoding, fold) => [...text].map(char => unit(char, encoding, fold)).join('');
136
+ const oneOf = (texts, encoding, fold) => `(?:${texts.map(text => literal(text, encoding, fold)).join('|')})`;
137
+
138
+ const ALNUM = 'A-Za-z0-9';
139
+ const BEFORE = { utf8: `(?<![${ALNUM}])`, utf16le: `(?<![${ALNUM}]\\x00)`, utf16be: `(?<!\\x00[${ALNUM}])` };
140
+ const AFTER = { utf8: `(?![${ALNUM}])`, utf16le: `(?![${ALNUM}]\\x00)`, utf16be: `(?!\\x00[${ALNUM}])` };
141
+ const SEPARATORS = ['\\', '/', '%5C', '%2F'];
142
+
143
+ /**
144
+ * The alternatives (regular expression sources) that make `term` a hit. `firm` leaves out the one
145
+ * that three random bytes satisfy (the capitalized word in UTF-8), see `stringLike`.
146
+ */
147
+ function alternatives(term, firm = false) {
148
+ const out = [];
149
+ for (const encoding of ENCODINGS) {
150
+ const word = literal(term.text, encoding, true);
151
+ if (term.rule === 'word') {
152
+ out.push(`${BEFORE[encoding]}${word}${AFTER[encoding]}`);
153
+ continue;
154
+ }
155
+ const sep = oneOf(SEPARATORS, encoding, true);
156
+ // In a path: between separators, right after a Users/home folder, or in a dash-flattened path.
157
+ out.push(`(?<=${sep})${word}(?=${sep})`);
158
+ out.push(`(?<=${oneOf(['Users', 'home'], encoding, true)}${sep}+)${word}${AFTER[encoding]}`);
159
+ out.push(`(?<=${literal('Users-', encoding, true)})${word}(?=${literal('-', encoding, false)})`);
160
+ // The capitalized standalone word.
161
+ const [first, ...rest] = [...term.text];
162
+ const capitalized = literal(first.toUpperCase(), encoding, false) + literal(rest.join('').toLowerCase(), encoding, false);
163
+ if (!firm || encoding !== 'utf8') out.push(`${BEFORE[encoding]}${capitalized}${AFTER[encoding]}`);
164
+ }
165
+ return out;
166
+ }
167
+
168
+ /**
169
+ * Whether the bytes at [start, end) sit in text rather than in compressed or random data: part of a
170
+ * run of 12 or more printable characters, or a field between two NUL bytes. Megabytes of media hold
171
+ * every three-letter sequence by chance; they do not hold it inside a sentence.
172
+ */
173
+ function stringLike(text, start, end) {
174
+ const printable = code => (code >= 0x20 && code <= 0x7e) || code === 9 || code === 10 || code === 13;
175
+ let run = end - start;
176
+ for (let at = start - 1; at >= 0 && run < 12 && printable(text.charCodeAt(at)); at -= 1) run += 1;
177
+ for (let at = end; at < text.length && run < 12 && printable(text.charCodeAt(at)); at += 1) run += 1;
178
+ if (run >= 12) return true;
179
+ return (start === 0 || text.charCodeAt(start - 1) === 0) && (end === text.length || text.charCodeAt(end) === 0);
180
+ }
181
+
182
+ /** First letter + `***`: the only form of a term that is ever printed. */
183
+ export const maskTerm = text => `${[...text][0]}***`;
184
+
185
+ /** A matcher over bytes for a term list. */
186
+ export function createMatcher(terms) {
187
+ const each = terms.map(term => ({ term, mask: maskTerm(term.text), source: alternatives(term).join('|') }));
188
+ const all = each.length ? new RegExp(each.map(entry => `(?:${entry.source})`).join('|'), 'g') : undefined;
189
+ const single = each.map(entry => new RegExp(entry.source, 'g'));
190
+ const firm = each.map(entry => (entry.term.rule === 'account' ? new RegExp(alternatives(entry.term, true).join('|'), 'g') : undefined));
191
+ const startsAt = (expression, window, at) => {
192
+ expression.lastIndex = at;
193
+ return expression.exec(window)?.index === at;
194
+ };
195
+
196
+ /** Every hit in `buffer`: `{ offset, length, mask, encoding }`, in order. `binary`: not a text file. */
197
+ function find(buffer, binary = false) {
198
+ if (!all || !buffer.length) return [];
199
+ const text = buffer.latin1Slice(0, buffer.length);
200
+ const hits = [];
201
+ all.lastIndex = 0;
202
+ let end = -1;
203
+ for (let match = all.exec(text); match; match = all.exec(text)) {
204
+ if (match.index >= end) {
205
+ end = match.index + match[0].length;
206
+ // Which term: the one whose own expression matches at this offset (seen with its context).
207
+ const from = Math.max(0, match.index - 64);
208
+ const window = text.slice(from, end + 8);
209
+ const which = single.findIndex(expression => startsAt(expression, window, match.index - from));
210
+ const mask = each[which]?.mask ?? '?***';
211
+ if (binary && firm[which] && !startsAt(firm[which], window, match.index - from) && !stringLike(text, match.index, end)) continue;
212
+ // (UTF-16 text matches in one byte order or, a byte earlier, in the other: the label names neither.)
213
+ hits.push({ offset: match.index, length: match[0].length, mask, encoding: match[0].includes('\x00') ? 'utf16' : 'utf8' });
214
+ }
215
+ if (match[0].length === 0) all.lastIndex += 1;
216
+ }
217
+ return hits;
218
+ }
219
+
220
+ /** `text` (a latin1 view of bytes) with every hit replaced by `mark` (default: the term's mask). */
221
+ function redact(text, mark) {
222
+ let out = text;
223
+ for (let i = 0; i < single.length; i += 1) {
224
+ single[i].lastIndex = 0;
225
+ out = out.replace(single[i], mark ?? each[i].mask);
226
+ }
227
+ return out;
228
+ }
229
+
230
+ return { find, redact, size: each.length };
231
+ }
232
+
233
+ // ---------------------------------------------------------------------------------------------
234
+ // Allowlist
235
+
236
+ const globToRegExp = glob => new RegExp(`^${glob.split('**').map(part => part.split('*').map(piece =>
237
+ piece.replace(/[.+^${}()|[\]\\]/g, '\\$&').replaceAll('?', '[^/]')).join('[^/]*')).join('.*')}$`);
238
+
239
+ /** Reads an allowlist file: `<file glob>: <regex>` lines. It must not contain a term itself. */
240
+ export function loadAllowlist(file, matcher) {
241
+ if (!file || !existsSync(file)) return [];
242
+ const raw = readFileSync(file);
243
+ if (matcher.find(raw).length) fail(`${file}: the allowlist must not contain a term (write <term> in its place)`);
244
+ const rules = [];
245
+ raw.toString('utf8').split(/\r?\n/).forEach((line, index) => {
246
+ const text = line.trim();
247
+ if (!text || text.startsWith('#')) return;
248
+ const cut = text.indexOf(': ');
249
+ if (cut <= 0) fail(`${file}:${index + 1}: expected "<file glob>: <regex>"`);
250
+ let pattern;
251
+ try { pattern = new RegExp(text.slice(cut + 2).trim()); } catch (error) { fail(`${file}:${index + 1}: ${error.message}`); }
252
+ rules.push({ glob: globToRegExp(text.slice(0, cut).trim()), pattern, line: index + 1, used: 0 });
253
+ });
254
+ return rules;
255
+ }
256
+
257
+ // ---------------------------------------------------------------------------------------------
258
+ // Containers
259
+
260
+ const startsWith = (buffer, bytes, at = 0) => buffer.length >= at + bytes.length && bytes.every((byte, i) => buffer[at + i] === byte);
261
+ const ascii = (buffer, from, length) => buffer.latin1Slice(Math.min(from, buffer.length), Math.min(from + length, buffer.length)).replace(/\0.*$/s, '');
262
+
263
+ function* tarEntries(buffer) {
264
+ let at = 0;
265
+ let longName;
266
+ while (at + 512 <= buffer.length) {
267
+ const header = buffer.subarray(at, at + 512);
268
+ if (header.every(byte => byte === 0)) break;
269
+ const sizeField = header.subarray(124, 136);
270
+ const size = sizeField[0] & 0x80
271
+ ? Number(sizeField.subarray(4).readBigUInt64BE(0))
272
+ : parseInt(ascii(header, 124, 12).trim() || '0', 8) || 0;
273
+ const type = String.fromCharCode(header[156] || 48);
274
+ const data = buffer.subarray(at + 512, at + 512 + size);
275
+ at += 512 + Math.ceil(size / 512) * 512;
276
+ if (type === 'L') { longName = data.toString('utf8').replace(/\0+$/, ''); continue; }
277
+ if (type === 'x' || type === 'g') {
278
+ const path = /\d+ path=([^\n]*)\n/.exec(data.toString('utf8'));
279
+ if (type === 'x' && path) longName = path[1];
280
+ yield { name: `${longName ?? 'pax'} (pax header)`, data };
281
+ continue;
282
+ }
283
+ const prefix = ascii(header, 345, 155);
284
+ const name = longName ?? (prefix ? `${prefix}/${ascii(header, 0, 100)}` : ascii(header, 0, 100));
285
+ longName = undefined;
286
+ // The header also carries a link target and the owner's user and group names.
287
+ yield { name, data: type === '0' || type === '7' ? data : undefined, meta: [ascii(header, 157, 100), ascii(header, 265, 32), ascii(header, 297, 32)] };
288
+ }
289
+ }
290
+
291
+ function* zipEntries(buffer, note) {
292
+ const end = buffer.lastIndexOf(Buffer.from([0x50, 0x4b, 0x05, 0x06]));
293
+ if (end < 0) { note('zip: no central directory'); return; }
294
+ let count = buffer.readUInt16LE(end + 10);
295
+ let at = buffer.readUInt32LE(end + 16);
296
+ if (at === 0xffffffff || count === 0xffff) {
297
+ const locator = end - 20;
298
+ if (locator < 0 || !startsWith(buffer, [0x50, 0x4b, 0x06, 0x07], locator)) { note('zip64: no locator'); return; }
299
+ const record = Number(buffer.readBigUInt64LE(locator + 8));
300
+ count = Number(buffer.readBigUInt64LE(record + 32));
301
+ at = Number(buffer.readBigUInt64LE(record + 48));
302
+ }
303
+ yield { name: '(zip directory)', data: buffer.subarray(at, end + 22 + buffer.readUInt16LE(end + 20)), leaf: true };
304
+ for (let i = 0; i < count; i += 1) {
305
+ if (!startsWith(buffer, [0x50, 0x4b, 0x01, 0x02], at)) { note('zip: broken central directory'); return; }
306
+ const method = buffer.readUInt16LE(at + 10);
307
+ let size = buffer.readUInt32LE(at + 20);
308
+ let full = buffer.readUInt32LE(at + 24);
309
+ const nameLength = buffer.readUInt16LE(at + 28);
310
+ const extraLength = buffer.readUInt16LE(at + 30);
311
+ const commentLength = buffer.readUInt16LE(at + 32);
312
+ let local = buffer.readUInt32LE(at + 42);
313
+ const name = buffer.toString('utf8', at + 46, at + 46 + nameLength);
314
+ for (let extra = at + 46 + nameLength; extra + 4 <= at + 46 + nameLength + extraLength;) {
315
+ const id = buffer.readUInt16LE(extra);
316
+ const length = buffer.readUInt16LE(extra + 2);
317
+ if (id === 1) {
318
+ let field = extra + 4;
319
+ if (full === 0xffffffff) { full = Number(buffer.readBigUInt64LE(field)); field += 8; }
320
+ if (size === 0xffffffff) { size = Number(buffer.readBigUInt64LE(field)); field += 8; }
321
+ if (local === 0xffffffff) local = Number(buffer.readBigUInt64LE(field));
322
+ }
323
+ extra += 4 + length;
324
+ }
325
+ at += 46 + nameLength + extraLength + commentLength;
326
+ if (name.endsWith('/')) continue;
327
+ const start = local + 30 + buffer.readUInt16LE(local + 26) + buffer.readUInt16LE(local + 28);
328
+ const packed = buffer.subarray(start, start + size);
329
+ if (method === 0) yield { name, data: packed };
330
+ else if (method === 8) {
331
+ try { yield { name, data: zlib.inflateRawSync(packed) }; } catch (error) { note(`zip: ${name}: ${error.message}`); }
332
+ } else note(`zip: ${name}: compression method ${method}`);
333
+ }
334
+ }
335
+
336
+ function* arEntries(buffer) {
337
+ let at = 8;
338
+ while (at + 60 <= buffer.length) {
339
+ const name = ascii(buffer, at, 16).trim().replace(/\/$/, '');
340
+ const size = parseInt(ascii(buffer, at + 48, 10).trim(), 10) || 0;
341
+ yield { name: `${name} (ar header)`, data: buffer.subarray(at, at + 60), leaf: true };
342
+ yield { name, data: buffer.subarray(at + 60, at + 60 + size) };
343
+ at += 60 + size + (size % 2);
344
+ }
345
+ }
346
+
347
+ /** The decompressed partitions of a DMG whose chunks are raw or zlib; undefined when it has others. */
348
+ function dmgPartitions(buffer) {
349
+ const trailer = buffer.subarray(buffer.length - 512);
350
+ const dataFork = Number(trailer.readBigUInt64BE(24));
351
+ const xmlAt = Number(trailer.readBigUInt64BE(216));
352
+ const xml = buffer.toString('latin1', xmlAt, xmlAt + Number(trailer.readBigUInt64BE(224)));
353
+ const blocks = /<key>blkx<\/key>([\s\S]*?)<\/array>/.exec(xml)?.[1] ?? '';
354
+ const partitions = [];
355
+ for (const [, encoded] of blocks.matchAll(/<data>([\s\S]*?)<\/data>/g)) {
356
+ const table = Buffer.from(encoded.replace(/\s+/g, ''), 'base64');
357
+ if (ascii(table, 0, 4) !== 'mish') continue;
358
+ const parts = [];
359
+ for (let i = 0; i < table.readUInt32BE(200); i += 1) {
360
+ const entry = 204 + i * 40;
361
+ const type = table.readUInt32BE(entry);
362
+ const from = dataFork + Number(table.readBigUInt64BE(entry + 24));
363
+ const packed = buffer.subarray(from, from + Number(table.readBigUInt64BE(entry + 32)));
364
+ if (type === 1) parts.push(packed);
365
+ else if (type === 0x80000005) parts.push(zlib.inflateSync(packed));
366
+ else if (type === 0x80000006 || type === 0x80000007 || type === 0x80000008) return undefined;
367
+ }
368
+ partitions.push(Buffer.concat(parts));
369
+ }
370
+ return partitions;
371
+ }
372
+
373
+ /** The offset where an ELF file ends (an AppImage's squashfs payload starts there). */
374
+ function elfEnd(buffer) {
375
+ if (buffer[4] !== 2 || buffer[5] !== 1) return undefined;
376
+ return Number(buffer.readBigUInt64LE(0x28)) + buffer.readUInt16LE(0x3a) * buffer.readUInt16LE(0x3c);
377
+ }
378
+
379
+ /** Whether bytes start like a container this scan would open. */
380
+ const isContainer = head => startsWith(head, [0x1f, 0x8b]) || startsWith(head, [0x50, 0x4b, 0x03, 0x04]) || ascii(head, 257, 5) === 'ustar'
381
+ || ascii(head, 0, 8) === '!<arch>\n' || startsWith(head, [0x28, 0xb5, 0x2f, 0xfd]) || startsWith(head, [0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00])
382
+ || ascii(head, 0, 4) === 'hsqs' || startsWith(head, [0x4d, 0x5a]) || startsWith(head, [0x7f, 0x45, 0x4c, 0x46]);
383
+
384
+ let sevenZipPath;
385
+ function sevenZip(env) {
386
+ if (sevenZipPath !== undefined) return sevenZipPath;
387
+ const candidates = [env[SEVEN_ZIP_ENV], '7z', '7zz', '7za'];
388
+ if (process.platform === 'win32') {
389
+ for (const base of [env.ProgramFiles, env['ProgramFiles(x86)'], env.ProgramW6432]) if (base) candidates.push(join(base, '7-Zip', '7z.exe'));
390
+ }
391
+ sevenZipPath = candidates.filter(Boolean).find(file => spawnSync(file, ['i'], { stdio: 'ignore', windowsHide: true }).status === 0) ?? '';
392
+ return sevenZipPath;
393
+ }
394
+
395
+ /** Runs a filter (`xz -dc`, `bzip2 -dc`) over bytes; undefined when the tool is missing or fails. */
396
+ function pipe(tool, args, input) {
397
+ const result = spawnSync(tool, args, { input, maxBuffer: 2 ** 31 - 1, windowsHide: true });
398
+ return result.status === 0 ? result.stdout : undefined;
399
+ }
400
+
401
+ // ---------------------------------------------------------------------------------------------
402
+ // The scan
403
+
404
+ const CHUNK = 64 * 1024 * 1024;
405
+ const OVERLAP = 4096;
406
+ const utf8 = latin1 => Buffer.from(latin1, 'latin1').toString('utf8');
407
+ const printable = text => text.replace(/\0/g, '').replace(/[\x01-\x1f\x7f-\x9f]+/g, ' ').trim();
408
+
409
+ /**
410
+ * A scan over any number of inputs. `report()` gives
411
+ * `{ hits, files: [{ file, count, masks, hits: [{ where, mask, encoding, context }] }], unopened, scanned, bytes }`.
412
+ */
413
+ export function createScan({ terms, allow = [], env = process.env } = {}) {
414
+ const matcher = createMatcher(terms);
415
+ const files = new Map();
416
+ const unopened = [];
417
+ let scanned = 0;
418
+ let bytes = 0;
419
+
420
+ function record(name, hit, where, context, allowContext) {
421
+ const file = safeName(name);
422
+ for (const rule of allow) {
423
+ if (rule.glob.test(file) && rule.pattern.test(allowContext)) { rule.used += 1; return; }
424
+ }
425
+ let entry = files.get(file);
426
+ if (!entry) files.set(file, entry = { file, count: 0, masks: {}, hits: [] });
427
+ entry.count += 1;
428
+ entry.masks[hit.mask] = (entry.masks[hit.mask] ?? 0) + 1;
429
+ entry.hits.push({ where, mask: hit.mask, encoding: hit.encoding, context });
430
+ }
431
+
432
+ const safeName = name => printable(utf8(matcher.redact(Buffer.from(name, 'utf8').latin1Slice())));
433
+
434
+ /** The allowlist's view of a hit: its surroundings with this hit written <term>, any other <other>. */
435
+ const marked = (buffer, from, to, hit) => printable(`${matcher.redact(buffer.latin1Slice(from, hit.offset), '<other>')}<term>${matcher.redact(buffer.latin1Slice(hit.offset + hit.length, to), '<other>')}`);
436
+
437
+ /** Searches bytes; `base`: their offset inside the file, `skip`: hits below this offset were seen. */
438
+ function bytesOfFile(file, buffer, { base = 0, skip = 0, textual } = {}) {
439
+ const isText = textual ?? !buffer.subarray(0, 8000).includes(0);
440
+ const hits = matcher.find(buffer, !isText);
441
+ if (!hits.length) return;
442
+ let line = 1;
443
+ let lineStart = 0;
444
+ let cursor = 0;
445
+ for (const hit of hits) {
446
+ if (hit.offset < skip) continue;
447
+ let where;
448
+ let from;
449
+ let to;
450
+ if (isText && hit.encoding === 'utf8') {
451
+ for (let next = buffer.indexOf(10, cursor); next >= 0 && next < hit.offset; next = buffer.indexOf(10, cursor)) {
452
+ line += 1;
453
+ lineStart = next + 1;
454
+ cursor = next + 1;
455
+ }
456
+ where = `${line}:${hit.offset - lineStart + 1}`;
457
+ const lineEnd = buffer.indexOf(10, hit.offset);
458
+ from = Math.max(lineStart, hit.offset - 100);
459
+ to = Math.min(lineEnd < 0 ? buffer.length : lineEnd, hit.offset + hit.length + 100);
460
+ } else {
461
+ where = `@0x${(base + hit.offset).toString(16)}`;
462
+ from = Math.max(0, hit.offset - 160);
463
+ to = Math.min(buffer.length, hit.offset + hit.length + 160);
464
+ }
465
+ const near = buffer.latin1Slice(Math.max(from, hit.offset - 60), Math.min(to, hit.offset + hit.length + 60));
466
+ record(file, hit, where, printable(matcher.redact(near)), marked(buffer, from, to, hit));
467
+ }
468
+ }
469
+
470
+ /** A name (a path inside an archive, a git path, an identity) searched as text. */
471
+ function text(file, value, label = 'name') {
472
+ const buffer = Buffer.from(value, 'utf8');
473
+ for (const hit of matcher.find(buffer)) {
474
+ const shown = printable(utf8(matcher.redact(buffer.latin1Slice(0, buffer.length))));
475
+ record(file, hit, label, shown.slice(0, 200), marked(buffer, 0, buffer.length, hit));
476
+ }
477
+ }
478
+
479
+ /** Extracts with 7-Zip (or unsquashfs) into a temporary folder, scans it, removes it. */
480
+ function extracted(file, buffer, depth, { type, tool = 'container' } = {}) {
481
+ const dir = mkdtempSync(join(tmpdir(), 'wb-privacy-'));
482
+ try {
483
+ const source = join(dir, 'in');
484
+ const out = join(dir, 'out');
485
+ writeFileSync(source, buffer);
486
+ const seven = sevenZip(env);
487
+ let done = false;
488
+ if (seven) {
489
+ done = spawnSync(seven, ['x', '-y', '-bd', `-o${out}`, ...(type ? [`-t${type}`] : []), source], { stdio: 'ignore', windowsHide: true }).status === 0;
490
+ }
491
+ if (!done && type === 'SquashFS') {
492
+ done = spawnSync('unsquashfs', ['-no-progress', '-d', out, source], { stdio: 'ignore' }).status === 0;
493
+ }
494
+ if (!done || !existsSync(out)) {
495
+ unopened.push(`${file}: ${tool} not opened (install 7-Zip, or set ${SEVEN_ZIP_ENV})`);
496
+ return;
497
+ }
498
+ folder(out, `${file}!`, depth + 1);
499
+ } finally {
500
+ rmSync(dir, { recursive: true, force: true });
501
+ }
502
+ }
503
+
504
+ /** One file's bytes: searched, and opened when they are a container. */
505
+ function content(file, buffer, depth = 0) {
506
+ const note = message => unopened.push(`${file}: ${message}`);
507
+ const inner = (entries) => {
508
+ for (const entry of entries) {
509
+ const name = `${file}!/${safeName(entry.name)}`;
510
+ text(name, entry.name);
511
+ for (const value of entry.meta ?? []) text(`${name} (tar header)`, value, 'owner');
512
+ if (entry.leaf) bytesOfFile(name, entry.data, { textual: false });
513
+ else if (entry.data) content(name, entry.data, depth + 1);
514
+ }
515
+ };
516
+ const filtered = (data, what) => {
517
+ if (data) content(file, data, depth + 1);
518
+ else note(`${what} not opened`);
519
+ };
520
+ if (depth === 0) { scanned += 1; bytes += buffer.length; }
521
+ if (depth > 8) { bytesOfFile(file, buffer); return; }
522
+ try {
523
+ if (startsWith(buffer, [0x1f, 0x8b])) {
524
+ bytesOfFile(`${file} (gzip header)`, buffer.subarray(0, 512), { textual: false });
525
+ let data;
526
+ try { data = zlib.gunzipSync(buffer); } catch (error) { note(`gzip: ${error.message}`); }
527
+ if (data) content(file, data, depth + 1);
528
+ } else if (startsWith(buffer, [0x50, 0x4b, 0x03, 0x04])) {
529
+ inner(zipEntries(buffer, note));
530
+ } else if (buffer.length > 512 && ascii(buffer, 257, 5) === 'ustar') {
531
+ inner(tarEntries(buffer));
532
+ } else if (ascii(buffer, 0, 8) === '!<arch>\n') {
533
+ inner(arEntries(buffer));
534
+ } else if (startsWith(buffer, [0x28, 0xb5, 0x2f, 0xfd])) {
535
+ let data;
536
+ try { data = zlib.zstdDecompressSync?.(buffer); } catch { /* reported below */ }
537
+ filtered(data ?? pipe('zstd', ['-dc'], buffer), 'zstd');
538
+ } else if (startsWith(buffer, [0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00])) {
539
+ filtered(pipe('xz', ['-dc'], buffer), 'xz (install xz)');
540
+ } else if (startsWith(buffer, [0x42, 0x5a, 0x68]) && buffer[3] >= 0x31 && buffer[3] <= 0x39 && startsWith(buffer, [0x31, 0x41, 0x59, 0x26, 0x53, 0x59], 4)) {
541
+ filtered(pipe('bzip2', ['-dc'], buffer), 'bzip2 (install bzip2)');
542
+ } else if (ascii(buffer, 0, 4) === 'hsqs') {
543
+ extracted(file, buffer, depth, { type: 'SquashFS', tool: 'squashfs' });
544
+ } else if (buffer.length > 512 && ascii(buffer, buffer.length - 512, 4) === 'koly') {
545
+ const partitions = dmgPartitions(buffer);
546
+ if (partitions) partitions.forEach((data, i) => bytesOfFile(`${file}!/partition ${i}`, data, { textual: false }));
547
+ else extracted(file, buffer, depth, { tool: 'dmg' });
548
+ } else if (startsWith(buffer, [0x7f, 0x45, 0x4c, 0x46]) && ascii(buffer, 8, 2) === 'AI' && buffer[10] === 2) {
549
+ bytesOfFile(file, buffer);
550
+ const end = elfEnd(buffer);
551
+ if (end && ascii(buffer, end, 4) === 'hsqs') extracted(file, buffer.subarray(end), depth, { type: 'SquashFS', tool: 'AppImage' });
552
+ else note('AppImage: payload not found');
553
+ } else if (startsWith(buffer, [0x4d, 0x5a]) && buffer.includes('NullsoftInst')) {
554
+ bytesOfFile(file, buffer);
555
+ extracted(file, buffer, depth, { type: 'Nsis', tool: 'NSIS installer' });
556
+ } else {
557
+ bytesOfFile(file, buffer);
558
+ }
559
+ } catch (error) {
560
+ if (error instanceof PrivacyError) throw error;
561
+ note(`not opened (${error.message})`);
562
+ bytesOfFile(file, buffer);
563
+ }
564
+ }
565
+
566
+ /** A file on disk under the name `file`. Large plain files are read in overlapping chunks. */
567
+ function diskFile(path, file, depth = 0) {
568
+ const size = lstatSync(path).size;
569
+ if (size <= CHUNK * 4) { content(file, readFileSync(path), depth); return; }
570
+ scanned += 1;
571
+ bytes += size;
572
+ const handle = openSync(path, 'r');
573
+ const buffer = Buffer.alloc(CHUNK + OVERLAP);
574
+ let container = false;
575
+ try {
576
+ container = isContainer(buffer.subarray(0, readSync(handle, buffer, 0, 512, 0)));
577
+ for (let at = 0; at < size; at += CHUNK) {
578
+ const from = Math.max(0, at - OVERLAP);
579
+ const read = readSync(handle, buffer, 0, Math.min(CHUNK + (at - from), size - from), from);
580
+ bytesOfFile(file, buffer.subarray(0, read), { base: from, skip: at - from, textual: false });
581
+ }
582
+ } finally {
583
+ closeSync(handle);
584
+ }
585
+ if (container) unopened.push(`${file}: a container larger than ${(CHUNK * 4) / 2 ** 20} MB, searched as plain bytes only`);
586
+ }
587
+
588
+ function folder(path, prefix, depth = 0) {
589
+ for (const entry of readdirSync(path, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : 1))) {
590
+ const full = join(path, entry.name);
591
+ const file = `${prefix}/${safeName(entry.name)}`;
592
+ text(file, entry.name);
593
+ if (entry.isDirectory()) { if (entry.name !== '.git') folder(full, file, depth); }
594
+ else if (entry.isFile()) diskFile(full, file, depth);
595
+ }
596
+ }
597
+
598
+ /** A file, folder or archive, shown relative to `cwd`. */
599
+ function path(target, cwd = process.cwd()) {
600
+ const full = resolve(cwd, target);
601
+ if (!existsSync(full)) fail(`not found: ${safeName(target)}`);
602
+ // Outside the working directory only the name is shown (the folders above it are not published).
603
+ const inside = relative(cwd, full);
604
+ const outside = inside.startsWith('..') || resolve(cwd, inside) !== full;
605
+ const shown = safeName(outside ? basename(full) : inside.replaceAll('\\', '/') || '.');
606
+ text(shown, outside ? basename(full) : inside);
607
+ if (lstatSync(full).isDirectory()) folder(full, shown === '.' ? '' : shown);
608
+ else diskFile(full, shown);
609
+ }
610
+
611
+ const git = (repo, args, options = {}) => {
612
+ const result = spawnSync('git', ['-C', repo, ...args], { maxBuffer: 2 ** 31 - 1, windowsHide: true, ...options });
613
+ if (result.status !== 0) fail(`git ${args[0]} failed in ${safeName(repo)}: ${String(result.stderr ?? '').trim().split('\n')[0]}`);
614
+ return result.stdout;
615
+ };
616
+
617
+ /** The files git tracks under `target` (and, with `untracked`, the ones it neither tracks nor ignores). */
618
+ function tracked(target, { cwd = process.cwd(), untracked = false } = {}) {
619
+ const dir = resolve(cwd, target);
620
+ const list = git(dir, ['ls-files', '-z', ...(untracked ? ['--cached', '--others', '--exclude-standard'] : [])]).toString('utf8').split('\0').filter(Boolean);
621
+ for (const name of [...new Set(list)]) {
622
+ const full = join(dir, name);
623
+ const shown = safeName(relative(cwd, full).replaceAll('\\', '/'));
624
+ text(shown, name);
625
+ let stat;
626
+ try { stat = lstatSync(full); } catch { continue; } // deleted in the work tree
627
+ if (stat.isFile()) diskFile(full, shown);
628
+ }
629
+ }
630
+
631
+ /** Everything a repository would publish: blobs, paths, commit and tag messages, identities, ref names. */
632
+ function history(target, { cwd = process.cwd() } = {}) {
633
+ const dir = resolve(cwd, target);
634
+ const label = safeName(relative(cwd, dir).replaceAll('\\', '/') || '.');
635
+ const names = new Map();
636
+ for (const line of git(dir, ['rev-list', '--all', '--objects']).toString('utf8').split('\n')) {
637
+ const space = line.indexOf(' ');
638
+ if (space > 0 && line.length > space + 1 && !names.has(line.slice(0, space))) names.set(line.slice(0, space), line.slice(space + 1));
639
+ }
640
+ for (const [, name] of names) text(`${label} history: ${safeName(name)}`, name, 'path');
641
+ const ids = [...names.keys()];
642
+ const kinds = git(dir, ['cat-file', '--batch-check=%(objectname) %(objecttype) %(objectsize)'], { input: `${ids.join('\n')}\n` }).toString('utf8').split('\n');
643
+ const blobs = kinds.map(line => line.split(' ')).filter(parts => parts[1] === 'blob');
644
+ // In batches, so a repository with large media never sits in memory whole.
645
+ for (let i = 0; i < blobs.length;) {
646
+ const batch = [];
647
+ for (let size = 0; i < blobs.length && (size < CHUNK * 4 || !batch.length); i += 1) {
648
+ batch.push(blobs[i]);
649
+ size += Number(blobs[i][2]);
650
+ }
651
+ const out = git(dir, ['cat-file', '--batch'], { input: `${batch.map(parts => parts[0]).join('\n')}\n` });
652
+ let at = 0;
653
+ for (const [id, , size] of batch) {
654
+ at = out.indexOf(10, at) + 1;
655
+ content(`${label} history: ${safeName(names.get(id))} (blob ${id.slice(0, 10)})`, out.subarray(at, at + Number(size)));
656
+ at += Number(size) + 1;
657
+ }
658
+ }
659
+ const log = git(dir, ['log', '--all', '--format=%H%x00%an%x00%ae%x00%cn%x00%ce%x00%B%x01']).toString('utf8');
660
+ for (const entry of log.split('\x01')) {
661
+ const [id, author, authorMail, committer, committerMail, message] = entry.replace(/^\n/, '').split('\0');
662
+ if (!id) continue;
663
+ const commit = `${label} history: commit ${id.slice(0, 10)}`;
664
+ text(commit, `${author} <${authorMail}>`, 'author');
665
+ text(commit, `${committer} <${committerMail}>`, 'committer');
666
+ text(commit, message ?? '', 'message');
667
+ }
668
+ const refs = git(dir, ['for-each-ref', '--format=%(refname)%00%(taggername) %(taggeremail)%00%(contents)%01']).toString('utf8');
669
+ for (const entry of refs.split('\x01')) {
670
+ const [ref, tagger, message] = entry.replace(/^\n/, '').split('\0');
671
+ if (!ref) continue;
672
+ text(`${label} history: ${safeName(ref)}`, ref, 'ref');
673
+ if (tagger?.trim()) text(`${label} history: ${safeName(ref)}`, tagger, 'tagger');
674
+ if (ref.startsWith('refs/tags/') && message) text(`${label} history: ${safeName(ref)}`, message, 'tag message');
675
+ }
676
+ }
677
+
678
+ function report() {
679
+ const list = [...files.values()];
680
+ return { hits: list.reduce((sum, entry) => sum + entry.count, 0), files: list, unopened: [...unopened], scanned, bytes, terms: matcher.size };
681
+ }
682
+
683
+ return { path, tracked, history, content, text, report };
684
+ }
685
+
686
+ /** The report as lines (hits masked). `all`: every hit instead of the first five of each file. */
687
+ export function formatReport(report, { all = false, label = 'privacy-scan' } = {}) {
688
+ const lines = [];
689
+ for (const entry of report.files) {
690
+ const masks = Object.entries(entry.masks).map(([mask, count]) => `${mask} x${count}`).join(', ');
691
+ lines.push(`${entry.file}: ${entry.count} ${entry.count === 1 ? 'hit' : 'hits'} (${masks})`);
692
+ for (const hit of all ? entry.hits : entry.hits.slice(0, 5)) {
693
+ lines.push(` ${hit.where} ${hit.mask}${hit.encoding === 'utf8' ? '' : ` [${hit.encoding}]`} ${hit.context.slice(0, 160)}`);
694
+ }
695
+ if (!all && entry.hits.length > 5) lines.push(` ... ${entry.hits.length - 5} more (--all)`);
696
+ }
697
+ for (const item of report.unopened) lines.push(`not opened: ${item}`);
698
+ const size = report.bytes >= 2 ** 20 ? `${(report.bytes / 2 ** 20).toFixed(1)} MB` : `${report.bytes} bytes`;
699
+ lines.push(`${label}: ${report.hits} ${report.hits === 1 ? 'hit' : 'hits'} in ${report.files.length} ${report.files.length === 1 ? 'file' : 'files'}`
700
+ + `${report.unopened.length ? `, ${report.unopened.length} not opened` : ''} (${report.scanned} files, ${size}, ${report.terms} terms)`);
701
+ return lines;
702
+ }
703
+
704
+ /**
705
+ * Scans artifacts and throws when one holds a term or could not be opened (the release gate).
706
+ * `paths`: files or folders; `terms`/`allowlist`: files (both optional).
707
+ */
708
+ export function assertClean(paths, { terms, allowlist, cwd = process.cwd(), env = process.env, log = console.log, label = 'privacy-scan' } = {}) {
709
+ const file = env[TERMS_ENV] || terms;
710
+ if (file && !existsSync(file)) fail(`terms file not found (${TERMS_ENV} or "forbiddenTerms"): fix the path, it is the release gate`);
711
+ const collected = collectTerms({ file });
712
+ const matcher = createMatcher(collected.terms);
713
+ const scan = createScan({ terms: collected.terms, allow: loadAllowlist(allowlist, matcher), env });
714
+ for (const target of paths) scan.path(target, cwd);
715
+ const report = scan.report();
716
+ for (const line of formatReport(report, { label })) log(line);
717
+ if (report.hits) fail(`${report.hits} forbidden ${report.hits === 1 ? 'term' : 'terms'} in the artifacts: nothing was released`);
718
+ if (report.unopened.length) fail(`${report.unopened.length} ${report.unopened.length === 1 ? 'container' : 'containers'} could not be opened, so the artifacts are not proven clean`);
719
+ return report;
720
+ }
721
+
722
+ function flag(args, name) {
723
+ const i = args.indexOf(name);
724
+ if (i < 0) return undefined;
725
+ const [, value] = args.splice(i, 2);
726
+ if (value === undefined) fail(`${name} needs a value`);
727
+ return value;
728
+ }
729
+ function toggle(args, name) {
730
+ const i = args.indexOf(name);
731
+ if (i >= 0) args.splice(i, 1);
732
+ return i >= 0;
733
+ }
734
+
735
+ /** `waybetter privacy-scan ...`; returns the exit code (0 clean, 1 hits, 2 something not opened). */
736
+ export function privacyCommand(argv, { cwd = process.cwd(), env = process.env, log = console.log } = {}) {
737
+ const args = [...argv];
738
+ if (toggle(args, '--help') || toggle(args, '-h')) { log(PRIVACY_USAGE); return 0; }
739
+ const config = privacyConfig(cwd);
740
+ const explicit = flag(args, '--terms');
741
+ const allowFile = flag(args, '--allow');
742
+ const options = Object.fromEntries(['--tracked', '--untracked', '--history', '--all', '--json', '--if-terms', '--no-auto'].map(name => [name, toggle(args, name)]));
743
+ const unknown = args.find(arg => arg.startsWith('--'));
744
+ if (unknown) fail(`unknown option ${unknown}`);
745
+
746
+ const candidate = explicit ? resolve(cwd, expandHome(explicit)) : env[TERMS_ENV] ? resolve(cwd, expandHome(env[TERMS_ENV])) : config.forbiddenTerms;
747
+ const file = candidate && existsSync(candidate) ? candidate : undefined;
748
+ if (!file && options['--if-terms']) return 0;
749
+ if (explicit && !file) fail('--terms: file not found');
750
+ const collected = collectTerms({ file, auto: !options['--no-auto'] });
751
+ if (!collected.terms.length) fail('no terms: pass --terms <file> or set ' + TERMS_ENV);
752
+ const matcher = createMatcher(collected.terms);
753
+ const allow = loadAllowlist(allowFile ? resolve(cwd, allowFile) : config.privacyAllowlist, matcher);
754
+ const scan = createScan({ terms: collected.terms, allow, env });
755
+ const targets = args.length ? args : ['.'];
756
+ for (const target of targets) {
757
+ if (options['--history']) scan.history(target, { cwd });
758
+ if (options['--tracked']) scan.tracked(target, { cwd, untracked: options['--untracked'] });
759
+ if (!options['--history'] && !options['--tracked']) scan.path(target, cwd);
760
+ }
761
+ const report = scan.report();
762
+ if (options['--json']) log(JSON.stringify(report, null, 2));
763
+ else {
764
+ for (const line of formatReport(report, { all: options['--all'] })) log(line);
765
+ if (!file) log(`privacy-scan: no terms file, only this machine's account name was searched (--terms, ${TERMS_ENV})`);
766
+ }
767
+ return report.hits ? 1 : report.unopened.length ? 2 : 0;
768
+ }