@celestia-island/hikari 0.4.4 → 0.30.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 (385) hide show
  1. package/README.md +1 -1
  2. package/package.json +22 -7
  3. package/src/DemoApp.tsx +491 -0
  4. package/src/animation/registerAnimations.test.ts +94 -0
  5. package/src/animation/registerAnimations.ts +103 -0
  6. package/src/components/HkAboutModal.scss +95 -0
  7. package/src/components/HkAboutModal.tsx +114 -0
  8. package/src/components/HkAdaptiveDialog.scss +22 -0
  9. package/src/components/HkAdaptiveDialog.test.tsx +271 -0
  10. package/src/components/HkAdaptiveDialog.tsx +167 -0
  11. package/src/components/HkAdminHeader.test.tsx +185 -0
  12. package/src/components/HkAdminHeader.tsx +308 -0
  13. package/src/components/HkAdminShell.test.tsx +237 -0
  14. package/src/components/HkAdminShell.tsx +129 -0
  15. package/src/components/HkAdminTablePage.tsx +70 -0
  16. package/src/components/HkAdminWidgets.test.ts +89 -0
  17. package/src/components/HkAlert.tsx +1 -1
  18. package/src/components/HkAltSignIn.scss +55 -0
  19. package/src/components/HkAltSignIn.test.tsx +88 -0
  20. package/src/components/HkAltSignIn.tsx +92 -0
  21. package/src/components/HkAttachmentModal.scss +49 -0
  22. package/src/components/HkAttachmentModal.tsx +318 -0
  23. package/src/components/HkAuthCard.tsx +50 -0
  24. package/src/components/HkAuthSubmitButton.scss +12 -0
  25. package/src/components/HkAuthSubmitButton.tsx +94 -0
  26. package/src/components/HkAvatar.scss +93 -0
  27. package/src/components/HkAvatar.tsx +2 -2
  28. package/src/components/HkBlockingToast.scss +123 -0
  29. package/src/components/HkBlockingToast.test.tsx +221 -0
  30. package/src/components/HkBlockingToast.tsx +182 -0
  31. package/src/components/HkBreadcrumb.scss +624 -0
  32. package/src/components/HkBreadcrumb.tsx +2 -2
  33. package/src/components/HkButton.scss +16 -2
  34. package/src/components/HkCaptchaModal.scss +14 -0
  35. package/src/components/HkCaptchaModal.tsx +62 -0
  36. package/src/components/HkCaptchaWidget.scss +30 -0
  37. package/src/components/HkCaptchaWidget.tsx +181 -0
  38. package/src/components/HkCard.scss +1 -1
  39. package/src/components/HkChatTypes.ts +65 -0
  40. package/src/components/HkCheckbox.scss +4 -9
  41. package/src/components/HkCheckbox.tsx +18 -6
  42. package/src/components/HkColorPicker.scss +120 -3
  43. package/src/components/HkColorPicker.tsx +130 -19
  44. package/src/components/HkColorPickerTouch.test.tsx +145 -0
  45. package/src/components/HkColorSchemeDialog.scss +52 -0
  46. package/src/components/HkColorSchemeDialog.test.ts +201 -0
  47. package/src/components/HkColorSchemeDialog.tsx +96 -0
  48. package/src/components/HkColorSchemeEditor.test.ts +270 -0
  49. package/src/components/HkColorSchemeEditor.tsx +410 -0
  50. package/src/components/HkConfirmDialog.scss +43 -3
  51. package/src/components/HkConfirmDialog.tsx +7 -4
  52. package/src/components/HkConnectionInfo.ts +80 -0
  53. package/src/components/HkConnectionStatus.tsx +158 -0
  54. package/src/components/HkCookieConsent.scss +22 -0
  55. package/src/components/HkCookieConsent.test.tsx +70 -0
  56. package/src/components/HkCookieConsent.tsx +56 -0
  57. package/src/components/HkCountdownDigit.tsx +105 -0
  58. package/src/components/HkDatePicker.scss +361 -0
  59. package/src/components/HkDatePicker.test.ts +459 -0
  60. package/src/components/HkDatePicker.tsx +568 -0
  61. package/src/components/HkDateTimePicker.scss +65 -37
  62. package/src/components/HkDateTimePicker.test.ts +344 -0
  63. package/src/components/HkDateTimePicker.tsx +249 -82
  64. package/src/components/HkDraggableGrid.scss +4 -1
  65. package/src/components/HkDraggableList.scss +4 -1
  66. package/src/components/HkDrawer.scss +27 -13
  67. package/src/components/HkDrawer.test.tsx +73 -0
  68. package/src/components/HkDrawer.tsx +157 -10
  69. package/src/components/HkErrorBoundary.scss +5 -2
  70. package/src/components/HkErrorBoundary.tsx +7 -4
  71. package/src/components/HkExpansionPanel.scss +143 -0
  72. package/src/components/HkExpansionPanel.test.tsx +109 -0
  73. package/src/components/HkExpansionPanel.tsx +101 -0
  74. package/src/components/HkFab.scss +162 -0
  75. package/src/components/HkFab.test.tsx +217 -0
  76. package/src/components/HkFab.tsx +214 -0
  77. package/src/components/HkHoverRevealAction.scss +6 -0
  78. package/src/components/HkHoverRevealAction.test.ts +68 -0
  79. package/src/components/HkHoverRevealAction.tsx +84 -6
  80. package/src/components/HkIcon.scss +123 -0
  81. package/src/components/HkIcon.tsx +7 -23
  82. package/src/components/HkIconButton.scss +170 -0
  83. package/src/components/HkIconButton.test.tsx +75 -0
  84. package/src/components/HkIconButton.tsx +4 -3
  85. package/src/components/HkIconButtonVars.scss +180 -0
  86. package/src/components/HkInput.affix.test.ts +253 -0
  87. package/src/components/HkInput.enter.test.ts +88 -0
  88. package/src/components/HkInput.scss +127 -8
  89. package/src/components/HkInput.tsx +245 -13
  90. package/src/components/HkKeywordSearchModal.scss +13 -0
  91. package/src/components/HkKeywordSearchModal.tsx +46 -9
  92. package/src/components/HkLabel.scss +34 -0
  93. package/src/components/HkLabel.test.tsx +61 -0
  94. package/src/components/HkLabel.tsx +45 -0
  95. package/src/components/HkListTransition.scss +26 -0
  96. package/src/components/HkListTransition.tsx +57 -0
  97. package/src/components/HkLocalePicker.scss +37 -0
  98. package/src/components/HkLocalePicker.tsx +133 -0
  99. package/src/components/HkLocalePickerPopup.tsx +35 -19
  100. package/src/components/HkLocalizedInput.scss +277 -0
  101. package/src/components/HkLocalizedInput.test.tsx +693 -0
  102. package/src/components/HkLocalizedInput.tsx +495 -0
  103. package/src/components/HkLogWindow.scss +121 -0
  104. package/src/components/HkLogWindow.tsx +195 -0
  105. package/src/components/HkLogo.scss +1 -1
  106. package/src/components/HkLogo.tsx +1 -1
  107. package/src/components/HkMarkdownRenderer.scss +22 -2
  108. package/src/components/HkMarkdownRenderer.tsx +33 -3
  109. package/src/components/HkMediaControlBar.tsx +20 -13
  110. package/src/components/HkMenu.scss +200 -0
  111. package/src/components/HkMenu.test.ts +779 -0
  112. package/src/components/HkMenu.tsx +770 -0
  113. package/src/components/HkMenuActionItem.scss +58 -0
  114. package/src/components/HkMenuActionItem.tsx +41 -0
  115. package/src/components/HkMenuIdentityItem.scss +70 -0
  116. package/src/components/HkMenuIdentityItem.tsx +42 -0
  117. package/src/components/HkMenuPanel.scss +20 -0
  118. package/src/components/HkMenuPanel.test.tsx +118 -0
  119. package/src/components/HkMenuPanel.tsx +32 -0
  120. package/src/components/HkMinimap.tsx +16 -8
  121. package/src/components/HkModal.autofollow.test.ts +44 -0
  122. package/src/components/HkModal.scss +170 -53
  123. package/src/components/HkModal.sheetgap.test.ts +82 -0
  124. package/src/components/HkModal.test.tsx +334 -0
  125. package/src/components/HkModal.tsx +314 -68
  126. package/src/components/HkModalBreadcrumb.scss +4 -1
  127. package/src/components/HkModalBreadcrumb.test.tsx +111 -0
  128. package/src/components/HkModalBreadcrumb.tsx +34 -11
  129. package/src/components/HkModelCatalog.ts +98 -0
  130. package/src/components/HkModelTag.scss +130 -0
  131. package/src/components/HkModelTag.test.tsx +111 -0
  132. package/src/components/HkModelTag.tsx +190 -0
  133. package/src/components/HkNavItem.scss +13 -2
  134. package/src/components/HkNavItem.tsx +2 -1
  135. package/src/components/HkNavSidebar.tsx +99 -0
  136. package/src/components/HkNumberInput.scss +12 -0
  137. package/src/components/HkNumberInput.tsx +15 -8
  138. package/src/components/HkPageHeader.scss +79 -0
  139. package/src/components/HkPageHeader.test.ts +67 -0
  140. package/src/components/HkPageHeader.tsx +51 -0
  141. package/src/components/HkPasswordInput.scss +29 -17
  142. package/src/components/HkPasswordInput.test.ts +281 -0
  143. package/src/components/HkPasswordInput.tsx +337 -59
  144. package/src/components/HkPickerPane.scss +158 -0
  145. package/src/components/HkPlaceholderMarquee.scss +109 -0
  146. package/src/components/HkPlaceholderMarquee.test.ts +305 -0
  147. package/src/components/HkPlaceholderMarquee.tsx +170 -0
  148. package/src/components/HkPopover.scss +134 -10
  149. package/src/components/HkPopover.sheetdock.test.ts +34 -0
  150. package/src/components/HkPopover.surfacevars.test.ts +20 -0
  151. package/src/components/HkPopover.titleSuppress.test.tsx +265 -0
  152. package/src/components/HkPopover.tsx +225 -50
  153. package/src/components/HkPopoverSheet.test.tsx +130 -0
  154. package/src/components/HkPopupSelect.scss +30 -10
  155. package/src/components/HkPopupSelect.tsx +21 -1
  156. package/src/components/HkProgressDialog.scss +31 -0
  157. package/src/components/HkProgressDialog.tsx +37 -19
  158. package/src/components/HkProtocolModal.scss +26 -0
  159. package/src/components/HkProtocolModal.tsx +112 -0
  160. package/src/components/HkRadio.scss +1 -16
  161. package/src/components/HkRadio.tsx +5 -2
  162. package/src/components/HkRichInput.scss +162 -0
  163. package/src/components/HkRichInput.tsx +335 -0
  164. package/src/components/HkRollingNumber.test.ts +90 -0
  165. package/src/components/HkRollingNumber.tsx +39 -1
  166. package/src/components/HkScrollContainer.scss +71 -54
  167. package/src/components/HkScrollContainer.test.ts +160 -0
  168. package/src/components/HkScrollContainer.tsx +187 -160
  169. package/src/components/HkSearchInput.tsx +3 -3
  170. package/src/components/HkSecretRevealModal.scss +61 -0
  171. package/src/components/HkSecretRevealModal.tsx +91 -0
  172. package/src/components/HkSelect.scss +263 -12
  173. package/src/components/HkSelect.sheetdock.test.ts +30 -0
  174. package/src/components/HkSelect.test.tsx +88 -0
  175. package/src/components/HkSelect.tsx +59 -74
  176. package/src/components/HkSelectPanel.test.tsx +666 -0
  177. package/src/components/HkSelectPanel.tsx +595 -0
  178. package/src/components/HkSelectSheet.test.tsx +132 -0
  179. package/src/components/HkSelectionGrid.scss +1 -1
  180. package/src/components/HkSelectionGrid.tsx +2 -1
  181. package/src/components/HkSelectionWaterfall.scss +1 -1
  182. package/src/components/HkShareBar.scss +40 -0
  183. package/src/components/HkShareBar.tsx +50 -0
  184. package/src/components/HkSidebar.scss +15 -10
  185. package/src/components/HkSidebar.tsx +26 -2
  186. package/src/components/HkSignInCard.test.ts +154 -0
  187. package/src/components/HkSignInCard.tsx +155 -0
  188. package/src/components/HkSlider.scss +99 -0
  189. package/src/components/HkSlider.test.tsx +155 -0
  190. package/src/components/HkSlider.tsx +173 -0
  191. package/src/components/HkSpinner.scss +36 -1
  192. package/src/components/HkSpinner.test.tsx +63 -0
  193. package/src/components/HkSpinner.tsx +16 -1
  194. package/src/components/HkSplash.scss +1 -1
  195. package/src/components/HkSplash.tsx +3 -1
  196. package/src/components/HkStatCard.scss +73 -0
  197. package/src/components/HkStatCard.tsx +46 -0
  198. package/src/components/HkStatusBar.test.tsx +178 -0
  199. package/src/components/HkStatusBar.tsx +379 -0
  200. package/src/components/HkStatusPill.scss +73 -0
  201. package/src/components/HkStatusPill.tsx +41 -0
  202. package/src/components/HkStepFlow.scss +75 -0
  203. package/src/components/HkStepFlow.test.tsx +246 -0
  204. package/src/components/HkStepFlow.tsx +115 -0
  205. package/src/components/HkSwitch.scss +5 -17
  206. package/src/components/HkSwitch.tsx +6 -3
  207. package/src/components/HkTable.scss +20 -1
  208. package/src/components/HkTable.tsx +28 -3
  209. package/src/components/HkTabs.scss +222 -48
  210. package/src/components/HkTabs.test.ts +517 -0
  211. package/src/components/HkTabs.tsx +567 -56
  212. package/src/components/HkTag.tsx +2 -1
  213. package/src/components/HkTextarea.tsx +5 -0
  214. package/src/components/HkThemeToggle.scss +232 -0
  215. package/src/components/HkThemeToggle.test.ts +231 -0
  216. package/src/components/HkThemeToggle.tsx +289 -0
  217. package/src/components/HkTimeline.scss +134 -1
  218. package/src/components/HkTimeline.test.tsx +319 -0
  219. package/src/components/HkTimeline.tsx +303 -54
  220. package/src/components/HkToast.scss +34 -5
  221. package/src/components/HkToast.test.tsx +109 -0
  222. package/src/components/HkToast.tsx +57 -8
  223. package/src/components/HkTokenUsageBadge.scss +53 -0
  224. package/src/components/HkTokenUsageBadge.test.tsx +59 -0
  225. package/src/components/HkTokenUsageBadge.tsx +54 -0
  226. package/src/components/HkTokenUsagePanel.scss +95 -0
  227. package/src/components/HkTokenUsagePanel.tsx +140 -0
  228. package/src/components/HkToolBlock.scss +428 -0
  229. package/src/components/HkToolBlock.tsx +639 -0
  230. package/src/components/HkTooltip.scss +14 -2
  231. package/src/components/HkTooltip.tsx +25 -2
  232. package/src/components/HkTrendChart.tsx +27 -16
  233. package/src/components/HkVoiceInputPopup.scss +77 -0
  234. package/src/components/HkVoiceInputPopup.tsx +97 -0
  235. package/src/components/HkZoomToolbar.scss +4 -1
  236. package/src/components/HkZoomToolbar.tsx +7 -4
  237. package/src/components/_hikari-vars.scss +6 -0
  238. package/src/composables/highlightLanguages.ts +109 -0
  239. package/src/composables/iconRegistry.ts +127 -0
  240. package/src/composables/messaging/browserTransport.ts +96 -0
  241. package/src/composables/messaging/index.ts +204 -0
  242. package/src/composables/messaging/messaging.test.ts +256 -0
  243. package/src/composables/messaging/toastTransport.ts +26 -0
  244. package/src/composables/messaging/types.ts +115 -0
  245. package/src/composables/useActionBar.ts +66 -0
  246. package/src/composables/useAttachments.ts +154 -0
  247. package/src/composables/useConnectionProbe.ts +142 -0
  248. package/src/composables/useDeferredTransition.ts +16 -0
  249. package/src/composables/useEngineHealth.ts +69 -0
  250. package/src/composables/useHighlight.ts +124 -0
  251. package/src/composables/useMeasuredHighlight.ts +150 -0
  252. package/src/composables/useOverlayScrollbar.test.ts +221 -0
  253. package/src/composables/useOverlayScrollbar.ts +296 -0
  254. package/src/composables/usePageTitle.ts +37 -0
  255. package/src/composables/useReducedMotion.ts +34 -0
  256. package/src/composables/useResourceListModal.tsx +147 -0
  257. package/src/composables/useSafeArea.ts +137 -0
  258. package/src/composables/useSendShortcut.ts +35 -0
  259. package/src/composables/useSizeMorph.test.ts +156 -0
  260. package/src/composables/useSizeMorph.ts +149 -0
  261. package/src/composables/useSurfaceTransition.test.ts +96 -0
  262. package/src/composables/useSurfaceTransition.ts +71 -0
  263. package/src/composables/useZoomPan.ts +192 -0
  264. package/src/demo.scss +9 -1
  265. package/src/demo.ts +5 -1
  266. package/src/i18n/locales/ar/chat.json +38 -0
  267. package/src/i18n/locales/ar/components.json +141 -40
  268. package/src/i18n/locales/ar/components.toml +59 -0
  269. package/src/i18n/locales/ar/platform.json +65 -0
  270. package/src/i18n/locales/de/chat.json +38 -0
  271. package/src/i18n/locales/de/components.json +139 -38
  272. package/src/i18n/locales/de/components.toml +59 -0
  273. package/src/i18n/locales/de/platform.json +65 -0
  274. package/src/i18n/locales/en/chat.json +38 -0
  275. package/src/i18n/locales/en/components.json +104 -3
  276. package/src/i18n/locales/en/components.toml +59 -0
  277. package/src/i18n/locales/en/platform.json +65 -0
  278. package/src/i18n/locales/es/chat.json +38 -0
  279. package/src/i18n/locales/es/components.json +141 -40
  280. package/src/i18n/locales/es/components.toml +59 -0
  281. package/src/i18n/locales/es/platform.json +65 -0
  282. package/src/i18n/locales/fr/chat.json +38 -0
  283. package/src/i18n/locales/fr/components.json +137 -36
  284. package/src/i18n/locales/fr/components.toml +59 -0
  285. package/src/i18n/locales/fr/platform.json +65 -0
  286. package/src/i18n/locales/ja/chat.json +38 -0
  287. package/src/i18n/locales/ja/components.json +104 -3
  288. package/src/i18n/locales/ja/components.toml +59 -0
  289. package/src/i18n/locales/ja/platform.json +65 -0
  290. package/src/i18n/locales/ko/chat.json +38 -0
  291. package/src/i18n/locales/ko/components.json +104 -3
  292. package/src/i18n/locales/ko/components.toml +59 -0
  293. package/src/i18n/locales/ko/platform.json +65 -0
  294. package/src/i18n/locales/pt/chat.json +38 -0
  295. package/src/i18n/locales/pt/components.json +141 -40
  296. package/src/i18n/locales/pt/components.toml +59 -0
  297. package/src/i18n/locales/pt/platform.json +65 -0
  298. package/src/i18n/locales/ru/chat.json +38 -0
  299. package/src/i18n/locales/ru/components.json +141 -40
  300. package/src/i18n/locales/ru/components.toml +59 -0
  301. package/src/i18n/locales/ru/platform.json +65 -0
  302. package/src/i18n/locales/zh-Hans/chat.json +38 -0
  303. package/src/i18n/locales/zh-Hans/components.json +109 -4
  304. package/src/i18n/locales/zh-Hans/components.toml +59 -0
  305. package/src/i18n/locales/zh-Hans/platform.json +65 -0
  306. package/src/i18n/locales/zh-Hant/chat.json +38 -0
  307. package/src/i18n/locales/zh-Hant/components.json +109 -4
  308. package/src/i18n/locales/zh-Hant/components.toml +59 -0
  309. package/src/i18n/locales/zh-Hant/platform.json +65 -0
  310. package/src/index.ts +225 -6
  311. package/src/plugins/bootScriptsPlugin.ts +47 -0
  312. package/src/plugins/buildHashPlugin.ts +67 -0
  313. package/src/plugins/index.ts +2 -0
  314. package/src/render.ts +1 -1
  315. package/src/runtime/animationBus.test.ts +112 -0
  316. package/src/runtime/animationBus.ts +33 -2
  317. package/src/runtime/backStack.test.ts +334 -0
  318. package/src/runtime/backStack.ts +377 -0
  319. package/src/runtime/env.ts +4 -0
  320. package/src/runtime/index.ts +15 -3
  321. package/src/runtime/intervalBus.test.ts +118 -0
  322. package/src/runtime/intervalBus.ts +189 -0
  323. package/src/runtime/mobileViewport.test.ts +132 -0
  324. package/src/runtime/mobileViewport.ts +157 -0
  325. package/src/runtime/pageLifecycle.ts +91 -0
  326. package/src/runtime/useBlockingToast.ts +112 -0
  327. package/src/runtime/useClipboard.ts +28 -0
  328. package/src/runtime/useConfirm.ts +15 -7
  329. package/src/runtime/useMediaQuery.test.ts +67 -0
  330. package/src/runtime/useMediaQuery.ts +89 -0
  331. package/src/runtime/useOverlay.test.ts +160 -0
  332. package/src/runtime/useOverlay.ts +59 -12
  333. package/src/runtime/usePopupManager.test.ts +200 -0
  334. package/src/runtime/usePopupManager.ts +106 -10
  335. package/src/runtime/useToast.test.ts +90 -0
  336. package/src/runtime/useToast.ts +20 -2
  337. package/src/styles/admin-tokens.scss +744 -0
  338. package/src/styles/index.scss +5 -3
  339. package/src/styles/theme/_glass.scss +33 -0
  340. package/src/styles/theme/_layout.scss +123 -0
  341. package/src/styles/theme/_scrollbar.scss +73 -0
  342. package/src/styles/theme/_tokens.scss +69 -0
  343. package/src/styles/theme/base.scss +617 -0
  344. package/src/styles/theme/foundation.scss +219 -0
  345. package/src/styles/theme/mixins.scss +793 -0
  346. package/src/styles/theme/themes.scss +25 -0
  347. package/src/styles/theme/variables.scss +125 -0
  348. package/src/theme/_field.scss +116 -0
  349. package/src/theme/fontContext.test.ts +179 -0
  350. package/src/theme/fontContext.ts +169 -0
  351. package/src/theme/index.ts +19 -2
  352. package/src/theme/presets.ts +86 -18
  353. package/src/theme/theme.scss +61 -1
  354. package/src/theme/tokenGroups.test.ts +534 -0
  355. package/src/theme/tokenGroups.ts +552 -0
  356. package/src/theme/useSolarTime.test.ts +113 -0
  357. package/src/theme/useSolarTime.ts +95 -20
  358. package/src/theme/useTheme.test.ts +72 -0
  359. package/src/theme/useTheme.ts +144 -9
  360. package/src/tokens.scss +110 -0
  361. package/src/utils/base64.ts +31 -0
  362. package/src/utils/connectivity.ts +51 -0
  363. package/src/utils/dom.test.ts +131 -0
  364. package/src/utils/dom.ts +133 -0
  365. package/src/utils/download.ts +22 -0
  366. package/src/utils/fileType.ts +127 -0
  367. package/src/utils/format.ts +98 -0
  368. package/src/utils/fuzzy.ts +77 -0
  369. package/src/utils/fuzzySearch.test.ts +87 -0
  370. package/src/utils/healthProbe.ts +64 -0
  371. package/src/utils/objectPath.ts +59 -0
  372. package/src/utils/password.ts +53 -0
  373. package/src/utils/pow.test.ts +45 -0
  374. package/src/utils/pow.ts +226 -0
  375. package/src/utils/powNonce.ts +95 -0
  376. package/src/utils/validation.test.ts +78 -0
  377. package/src/DemoApp.vue +0 -400
  378. package/src/components/HkCollapse.tsx +0 -48
  379. package/src/components/HkMorphingTabs.scss +0 -141
  380. package/src/components/HkMorphingTabs.tsx +0 -113
  381. package/src/composables/useBreakpoint.ts +0 -41
  382. package/src/composables/useClipboard.ts +0 -37
  383. package/src/composables/useConfirm.ts +0 -35
  384. package/src/composables/useToast.ts +0 -2
  385. package/src/runtime/useAsyncData.ts +0 -30
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Shared one-shot health-endpoint probe (P59-W2).
3
+ *
4
+ * `/api/health` (or any health-style endpoint) was fetched independently
5
+ * by every app with its own timeout/body handling — shittim-chest's
6
+ * useHealthProbe, plana-legacy's useEngineHealth, erp's status poll —
7
+ * drifting apart in policy. This is the single primitive they all call:
8
+ * one fetch, one AbortSignal timeout, one canonical minimal body shape.
9
+ * Pure function, no app config — callers bring the URL.
10
+ */
11
+
12
+ /** Minimal subset shared by all `/api/health` consumers. */
13
+ export interface HealthProbeBody {
14
+ status: string;
15
+ version: string;
16
+ product: string;
17
+ engine_version: string | null;
18
+ build_hash: string | null;
19
+ network?: unknown;
20
+ [key: string]: unknown;
21
+ }
22
+
23
+ export interface HealthProbeResult {
24
+ ok: boolean;
25
+ /** HTTP round-trip latency in ms (present even when the body is not ok). */
26
+ latencyMs: number;
27
+ body: HealthProbeBody | null;
28
+ /** Human-readable failure reason when !ok. */
29
+ reason: string | null;
30
+ }
31
+
32
+ const DEFAULT_TIMEOUT_MS = 8_000;
33
+
34
+ /**
35
+ * Probe a health endpoint once. Never throws — returns a result.
36
+ * `ok` means: HTTP 2xx AND parseable body (callers decide how to treat
37
+ * `status` values like "degraded").
38
+ */
39
+ export async function probeHealthEndpoint(
40
+ url: string,
41
+ opts: { timeoutMs?: number; fetchFn?: typeof fetch } = {},
42
+ ): Promise<HealthProbeResult> {
43
+ const fetchFn = opts.fetchFn ?? fetch;
44
+ const started = performance.now();
45
+ try {
46
+ const resp = await fetchFn(url, {
47
+ method: "GET",
48
+ credentials: "include",
49
+ signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
50
+ });
51
+ const latencyMs = Math.round(performance.now() - started);
52
+ if (!resp.ok) {
53
+ return { ok: false, latencyMs, body: null, reason: `HTTP ${resp.status}` };
54
+ }
55
+ const body = (await resp.json()) as HealthProbeBody;
56
+ return { ok: true, latencyMs, body, reason: null };
57
+ } catch (e) {
58
+ const latencyMs = Math.round(performance.now() - started);
59
+ const reason = e instanceof DOMException && e.name === "TimeoutError"
60
+ ? "timeout"
61
+ : e instanceof Error ? e.message : String(e);
62
+ return { ok: false, latencyMs, body: null, reason };
63
+ }
64
+ }
@@ -0,0 +1,59 @@
1
+ /** Deep merge following RFC 7396 semantics: plain objects merge
2
+ * recursively, anything else replaces. `null` in `patch` deletes. */
3
+ export function deepMerge<T extends Record<string, unknown>>(base: T, patch: unknown): T {
4
+ if (!isPlainObject(patch)) return patch as T;
5
+ if (!isPlainObject(base)) base = {} as T;
6
+ const out: Record<string, unknown> = { ...base };
7
+ for (const key of Object.keys(patch as Record<string, unknown>)) {
8
+ const v = (patch as Record<string, unknown>)[key];
9
+ if (v === null) {
10
+ delete out[key];
11
+ } else if (isPlainObject(v) && isPlainObject(out[key])) {
12
+ out[key] = deepMerge(out[key] as Record<string, unknown>, v);
13
+ } else {
14
+ out[key] = v;
15
+ }
16
+ }
17
+ return out as T;
18
+ }
19
+
20
+ export function isPlainObject(v: unknown): v is Record<string, unknown> {
21
+ return typeof v === "object" && v !== null && !Array.isArray(v) &&
22
+ (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null);
23
+ }
24
+
25
+ /** Read a dot-separated path (e.g. "a.b.c") from a nested object. */
26
+ export function getPath(obj: unknown, path: string): unknown {
27
+ let cur: unknown = obj;
28
+ for (const seg of path.split(".")) {
29
+ if (cur == null || typeof cur !== "object") return undefined;
30
+ cur = (cur as Record<string, unknown>)[seg];
31
+ }
32
+ return cur;
33
+ }
34
+
35
+ /** Write a dot-separated path into a nested object (mutating, creating
36
+ * intermediate plain objects). */
37
+ export function setPath<T extends Record<string, unknown>>(obj: T, path: string, value: unknown): T {
38
+ const segs = path.split(".");
39
+ let cur: Record<string, unknown> = obj;
40
+ for (let i = 0; i < segs.length - 1; i++) {
41
+ const seg = segs[i];
42
+ if (!isPlainObject(cur[seg])) cur[seg] = {};
43
+ cur = cur[seg] as Record<string, unknown>;
44
+ }
45
+ cur[segs[segs.length - 1]] = value;
46
+ return obj;
47
+ }
48
+
49
+ /** Remove a dot-separated path from a nested object (mutating). */
50
+ export function delPath(obj: Record<string, unknown>, path: string): void {
51
+ const segs = path.split(".");
52
+ let cur: Record<string, unknown> = obj;
53
+ for (let i = 0; i < segs.length - 1; i++) {
54
+ const next = cur[segs[i]];
55
+ if (!isPlainObject(next)) return;
56
+ cur = next;
57
+ }
58
+ delete cur[segs[segs.length - 1]];
59
+ }
@@ -0,0 +1,53 @@
1
+ export interface PasswordValidationResult {
2
+ valid: boolean;
3
+ errorKey: string | null;
4
+ }
5
+
6
+ /**
7
+ * Password policy validation (upstreamed from shittim-chest).
8
+ *
9
+ * Rules: match with the confirmation, ≥ 8 chars, letters + digits.
10
+ * `errorKey` is an i18n key the caller resolves with its own catalog
11
+ * (override the prefix via the second arg if your catalog differs).
12
+ */
13
+ export function validatePassword(
14
+ password: string,
15
+ confirmPassword: string,
16
+ keyPrefix = "common.validation",
17
+ ): PasswordValidationResult {
18
+ if (password !== confirmPassword) {
19
+ return { valid: false, errorKey: `${keyPrefix}.passwordMismatch` };
20
+ }
21
+ if (password.length < 8) {
22
+ return { valid: false, errorKey: `${keyPrefix}.minLength` };
23
+ }
24
+ if (!/[a-zA-Z]/.test(password) || !/\d/.test(password)) {
25
+ return { valid: false, errorKey: `${keyPrefix}.passwordNeedsVariety` };
26
+ }
27
+ return { valid: true, errorKey: null };
28
+ }
29
+
30
+ export type PasswordLevel = "weak" | "fair" | "strong";
31
+
32
+ /**
33
+ * Password strength level for a traffic-light indicator:
34
+ * - "strong": ≥ 10 chars + upper + lower + special (green)
35
+ * - "fair": ≥ 8 chars + letters + digits (yellow)
36
+ * - "weak": below fair (red)
37
+ * - null: empty (no indicator)
38
+ */
39
+ export function passwordLevel(password: string): PasswordLevel | null {
40
+ if (!password) return null;
41
+ const hasLetter = /[a-zA-Z]/.test(password);
42
+ const hasDigit = /\d/.test(password);
43
+ const hasUpper = /[A-Z]/.test(password);
44
+ const hasLower = /[a-z]/.test(password);
45
+ const hasSpecial = /[^a-zA-Z0-9]/.test(password);
46
+ if (password.length >= 10 && hasUpper && hasLower && hasSpecial) {
47
+ return "strong";
48
+ }
49
+ if (password.length >= 8 && hasLetter && hasDigit) {
50
+ return "fair";
51
+ }
52
+ return "weak";
53
+ }
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { leadingZeroBits, sha256, solvePow, solvePowSync, verifyPow } from "./pow";
3
+
4
+ describe("pow", () => {
5
+ it("leadingZeroBits counts correctly", () => {
6
+ // Two zero bytes (16) plus the 0x01 byte's seven leading zeros = 23.
7
+ // The upstream plana expectation of 16 missed the final byte.
8
+ expect(leadingZeroBits(new Uint8Array([0, 0, 1, 0]))).toBe(23);
9
+ expect(leadingZeroBits(new Uint8Array([0x80, 0, 0]))).toBe(0);
10
+ expect(leadingZeroBits(new Uint8Array([0x01, 0, 0]))).toBe(7);
11
+ });
12
+
13
+ it("solvePow finds a counter meeting the difficulty", async () => {
14
+ const counter = await solvePow({ seed: "test-seed", bits: 8 });
15
+ expect(await verifyPow({ seed: "test-seed", bits: 8 }, counter)).toBe(true);
16
+ });
17
+
18
+ it("matches the NIST SHA-256 known-answer vectors (sync core)", () => {
19
+ const enc = new TextEncoder();
20
+ const hex = (b: Uint8Array) => [...b].map((x) => x.toString(16).padStart(2, "0")).join("");
21
+ const vectors: Array<[string, string]> = [
22
+ ["abc", "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"],
23
+ ["", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"],
24
+ ["abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"],
25
+ ];
26
+ for (const [input, expected] of vectors) {
27
+ expect(hex(sha256(enc.encode(input)))).toBe(expected);
28
+ }
29
+ });
30
+
31
+ it("the sync solver matches the subtle path (same wire contract)", async () => {
32
+ const counter = solvePowSync("test-seed", 8);
33
+ expect(await verifyPow({ seed: "test-seed", bits: 8 }, counter)).toBe(true);
34
+ const c1 = solvePowSync("fixed-seed", 12);
35
+ const c2 = solvePowSync("fixed-seed", 12);
36
+ expect(c1).toBe(c2);
37
+ });
38
+
39
+ it("the hash layout is deterministic (wire contract)", async () => {
40
+ const c1 = await solvePow({ seed: "fixed-seed", bits: 12 });
41
+ const c2 = await solvePow({ seed: "fixed-seed", bits: 12 });
42
+ expect(c1).toBe(c2);
43
+ expect(await verifyPow({ seed: "fixed-seed", bits: 12 }, c1)).toBe(true);
44
+ });
45
+ });
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Hashcash-style proof-of-work helpers (upstreamed from shittim-chest,
3
+ * P5 auth-kit consolidation).
4
+ *
5
+ * Given a `{ seed, bits }` challenge, `solvePow` finds a `counter` such
6
+ * that `SHA-256(UTF-8(seed) || UTF-8(decimal counter))` has at least
7
+ * `bits` leading zero bits. The byte layout is part of the wire contract
8
+ * and must match the verifying backend exactly.
9
+ */
10
+
11
+ const BATCH = 64;
12
+
13
+ // ── Synchronous SHA-256 (dependency-free) ─────────────────────────────
14
+ // Upstreamed from shittim-chest's PoW worker; runs when `crypto.subtle`
15
+ // is unavailable (plain-HTTP non-localhost origins are not a secure
16
+ // context and have no WebCrypto at all). Verified against crypto.subtle
17
+ // by the pow contract tests.
18
+ const K = new Uint32Array([
19
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
20
+ 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
21
+ 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
22
+ 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
23
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
24
+ 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
25
+ 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
26
+ 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
27
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
28
+ 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
29
+ 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
30
+ ]);
31
+
32
+ function rotr(x: number, n: number): number {
33
+ return (x >>> n) | (x << (32 - n));
34
+ }
35
+
36
+ /** SHA-256 of `data` written into `out` (8 x u32, reuses the buffer). */
37
+ function sha256Into(data: Uint8Array, out: Uint32Array): void {
38
+ const len = data.length;
39
+ const bitLen = len * 8;
40
+ const paddedLen = Math.ceil((len + 9) / 64) * 64;
41
+ const padded = new Uint8Array(paddedLen);
42
+ padded.set(data);
43
+ padded[len] = 0x80;
44
+ const dv = new DataView(padded.buffer);
45
+ dv.setUint32(paddedLen - 4, bitLen >>> 0, false);
46
+ dv.setUint32(paddedLen - 8, Math.floor(bitLen / 0x100000000), false);
47
+
48
+ let h0 = 0x6a09e667, h1 = 0xbb67ae85, h2 = 0x3c6ef372, h3 = 0xa54ff53a;
49
+ let h4 = 0x510e527f, h5 = 0x9b05688c, h6 = 0x1f83d9ab, h7 = 0x5be0cd19;
50
+
51
+ const w = new Uint32Array(64);
52
+ for (let off = 0; off < paddedLen; off += 64) {
53
+ for (let i = 0; i < 16; i++) {
54
+ w[i] = dv.getUint32(off + i * 4, false);
55
+ }
56
+ for (let i = 16; i < 64; i++) {
57
+ const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3);
58
+ const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10);
59
+ w[i] = (w[i - 16] + s0 + w[i - 7] + s1) | 0;
60
+ }
61
+ let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7;
62
+ for (let i = 0; i < 64; i++) {
63
+ const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
64
+ const ch = (e & f) ^ (~e & g);
65
+ const temp1 = (h + S1 + ch + K[i] + w[i]) | 0;
66
+ const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
67
+ const maj = (a & b) ^ (a & c) ^ (b & c);
68
+ const temp2 = (S0 + maj) | 0;
69
+ h = g; g = f; f = e;
70
+ e = (d + temp1) | 0;
71
+ d = c; c = b; b = a;
72
+ a = (temp1 + temp2) | 0;
73
+ }
74
+ h0 = (h0 + a) | 0; h1 = (h1 + b) | 0; h2 = (h2 + c) | 0; h3 = (h3 + d) | 0;
75
+ h4 = (h4 + e) | 0; h5 = (h5 + f) | 0; h6 = (h6 + g) | 0; h7 = (h7 + h) | 0;
76
+ }
77
+ out[0] = h0; out[1] = h1; out[2] = h2; out[3] = h3;
78
+ out[4] = h4; out[5] = h5; out[6] = h6; out[7] = h7;
79
+ }
80
+
81
+ /** Leading zero bits of a SHA-256 digest stored as 8 x u32 words. */
82
+ function leadingZeroBits32(hash: Uint32Array): number {
83
+ let count = 0;
84
+ for (let i = 0; i < 8; i++) {
85
+ const word = hash[i];
86
+ if (word === 0) {
87
+ count += 32;
88
+ } else {
89
+ count += Math.clz32(word);
90
+ break;
91
+ }
92
+ }
93
+ return count;
94
+ }
95
+
96
+ /**
97
+ * SHA-256 digest of `data` (32 bytes) via the dependency-free sync core.
98
+ * Exported for contract tests (NIST known-answer vectors).
99
+ */
100
+ export function sha256(data: Uint8Array): Uint8Array {
101
+ const out = new Uint32Array(8);
102
+ sha256Into(data, out);
103
+ const bytes = new Uint8Array(32);
104
+ const dv = new DataView(bytes.buffer);
105
+ for (let i = 0; i < 8; i++) dv.setUint32(i * 4, out[i], false);
106
+ return bytes;
107
+ }
108
+
109
+ /** Synchronous solver for non-WebCrypto contexts (plain-HTTP origins). */
110
+ export function solvePowSync(
111
+ seed: string,
112
+ bits: number,
113
+ onProgress?: (hashed: number) => void,
114
+ ): number {
115
+ const enc = new TextEncoder();
116
+ const seedBytes = enc.encode(seed);
117
+ const hashOut = new Uint32Array(8);
118
+ let counter = 0;
119
+ const MAX = 268435456;
120
+ for (;;) {
121
+ const counterBytes = enc.encode(String(counter));
122
+ const data = new Uint8Array(seedBytes.length + counterBytes.length);
123
+ data.set(seedBytes, 0);
124
+ data.set(counterBytes, seedBytes.length);
125
+ sha256Into(data, hashOut);
126
+ if (leadingZeroBits32(hashOut) >= bits) {
127
+ return counter;
128
+ }
129
+ counter++;
130
+ if (counter > MAX) {
131
+ throw new Error("PoW exceeded max iterations");
132
+ }
133
+ if (counter % BATCH === 0) onProgress?.(counter);
134
+ }
135
+ }
136
+
137
+ export interface PowChallenge {
138
+ seed: string;
139
+ bits: number;
140
+ }
141
+
142
+ export interface PowSolution {
143
+ seed: string;
144
+ bits: number;
145
+ counter: number;
146
+ }
147
+
148
+ /**
149
+ * Solve a PoW challenge via `crypto.subtle.digest` in batches (correct
150
+ * everywhere, async per batch — the pure-logic path without a worker).
151
+ */
152
+ export async function solvePow(
153
+ challenge: PowChallenge,
154
+ onProgress?: (hashed: number) => void,
155
+ ): Promise<number> {
156
+ const { seed, bits } = challenge;
157
+ const encoder = new TextEncoder();
158
+ const seedBytes = encoder.encode(seed);
159
+ const digest = typeof crypto !== "undefined" && crypto.subtle
160
+ ? crypto.subtle.digest.bind(crypto.subtle)
161
+ : null;
162
+ if (!digest) {
163
+ // Not a secure context (plain-HTTP non-localhost): no WebCrypto at all.
164
+ return solvePowSync(seed, bits, onProgress);
165
+ }
166
+
167
+ let base = 0;
168
+ for (;;) {
169
+ const batch = Math.min(BATCH, 1 << 20 - base.toString().length);
170
+ const tasks: Array<Promise<{ counter: number; hash: Uint8Array }>> = [];
171
+ for (let i = 0; i < batch; i++) {
172
+ const counter = base + i;
173
+ tasks.push(
174
+ digest("SHA-256", concat(seedBytes, encoder.encode(String(counter))).buffer).then(
175
+ (buf) => ({ counter, hash: new Uint8Array(buf) }),
176
+ ),
177
+ );
178
+ }
179
+ const results = await Promise.all(tasks);
180
+ for (const { counter, hash } of results) {
181
+ if (leadingZeroBits(hash) >= bits) {
182
+ return counter;
183
+ }
184
+ }
185
+ base += batch;
186
+ onProgress?.(base);
187
+ }
188
+ }
189
+
190
+ function concat(a: Uint8Array, b: Uint8Array): Uint8Array<ArrayBuffer> {
191
+ const out = new Uint8Array(a.length + b.length);
192
+ out.set(a, 0);
193
+ out.set(b, a.length);
194
+ return out;
195
+ }
196
+
197
+ /** Number of leading zero bits of a 32-byte SHA-256 digest. */
198
+ export function leadingZeroBits(hash: Uint8Array): number {
199
+ let count = 0;
200
+ for (let i = 0; i < hash.length; i++) {
201
+ const byte = hash[i];
202
+ if (byte === 0) {
203
+ count += 8;
204
+ } else {
205
+ count += Math.clz32(byte) - 24;
206
+ break;
207
+ }
208
+ }
209
+ return count;
210
+ }
211
+
212
+ /** Verify a solution against the challenge (async; mirrors the backend check). */
213
+ export async function verifyPow(
214
+ challenge: PowChallenge,
215
+ counter: number,
216
+ ): Promise<boolean> {
217
+ const encoder = new TextEncoder();
218
+ const input = concat(encoder.encode(challenge.seed), encoder.encode(String(counter)));
219
+ if (typeof crypto !== "undefined" && crypto.subtle) {
220
+ const buf = await crypto.subtle.digest("SHA-256", input.buffer);
221
+ return leadingZeroBits(new Uint8Array(buf)) >= challenge.bits;
222
+ }
223
+ const out = new Uint32Array(8);
224
+ sha256Into(input, out);
225
+ return leadingZeroBits32(out) >= challenge.bits;
226
+ }
@@ -0,0 +1,95 @@
1
+ import { solvePow, type PowChallenge } from "./pow";
2
+
3
+ export type ChallengeDescriptor =
4
+ | { type: "pow"; seed: string; bits: number }
5
+ | { type: "captcha"; provider: string; sitekey: string; script_url: string }
6
+ | null;
7
+
8
+ /**
9
+ * Normalize the API base before it is interpolated into request URLs.
10
+ * Both exported functions are public API: a caller that passes an
11
+ * undefined/missed config value would otherwise fetch `undefined/health`
12
+ * — a relative URL resolving to `<origin>/undefined/health` — and fail
13
+ * with a 404 that looks like a backend problem. Same pattern as
14
+ * useEngineHealth's baseUrl guard.
15
+ */
16
+ function normalizeBase(baseUrl: string): string {
17
+ return (baseUrl ?? "").replace(/\/+$/, "");
18
+ }
19
+
20
+ /**
21
+ * Fetch the anti-bot challenge descriptor from a public `/health`-style
22
+ * endpoint (upstreamed from shittim-chest's api/pow.ts).
23
+ *
24
+ * Returns:
25
+ * - the descriptor when the challenge gate is active,
26
+ * - `null` when the backend answered but has no challenge configured,
27
+ * - `undefined` when the fetch itself failed (network/transport error), so
28
+ * callers can distinguish "gate off" from "could not reach the backend".
29
+ */
30
+ export async function fetchChallenge(
31
+ baseUrl: string,
32
+ fetchFn: typeof fetch = fetch,
33
+ ): Promise<ChallengeDescriptor | undefined> {
34
+ let resp: Response;
35
+ try {
36
+ resp = await fetchFn(`${normalizeBase(baseUrl)}/health`, { credentials: "same-origin" });
37
+ } catch {
38
+ return undefined;
39
+ }
40
+ if (!resp.ok) return undefined;
41
+ const json = (await resp.json().catch(() => null)) as {
42
+ challenge?: ChallengeDescriptor;
43
+ } | null;
44
+ return json?.challenge ?? null;
45
+ }
46
+
47
+ /**
48
+ * Negotiate a single-use X-Nonce by solving the PoW challenge (or passing
49
+ * a captcha token). Returns the nonce string for the X-Nonce header, or
50
+ * undefined when the exchange fails — including when a captcha challenge
51
+ * is configured but no token was provided.
52
+ */
53
+ export async function negotiateNonce(
54
+ baseUrl: string,
55
+ opts?: {
56
+ captchaToken?: string;
57
+ solve?: (challenge: PowChallenge) => Promise<number>;
58
+ fetchFn?: typeof fetch;
59
+ },
60
+ ): Promise<string | undefined> {
61
+ try {
62
+ const fetchFn = opts?.fetchFn ?? fetch;
63
+ let body: Record<string, unknown> | undefined;
64
+ if (opts?.captchaToken) {
65
+ body = { type: "captcha", token: opts.captchaToken };
66
+ } else {
67
+ const challenge = await fetchChallenge(baseUrl, fetchFn);
68
+ if (challenge === undefined) return undefined;
69
+ if (challenge?.type === "pow") {
70
+ const solve = opts?.solve ?? solvePow;
71
+ const counter = await solve({ seed: challenge.seed, bits: challenge.bits });
72
+ body = { type: "pow", seed: challenge.seed, counter };
73
+ } else if (challenge?.type === "captcha") {
74
+ // A captcha is required but no token was provided — fail cleanly
75
+ // instead of posting a garbage solution.
76
+ return undefined;
77
+ }
78
+ // challenge === null: the gate is disabled and the backend issues a
79
+ // nonce without proof, so an empty body is the correct request.
80
+ }
81
+ const resp = await fetchFn(`${normalizeBase(baseUrl)}/auth/nonce`, {
82
+ method: "POST",
83
+ headers: { "Content-Type": "application/json" },
84
+ body: body ? JSON.stringify(body) : undefined,
85
+ credentials: "same-origin",
86
+ });
87
+ if (!resp.ok) return undefined;
88
+ const json = (await resp.json().catch(() => null)) as {
89
+ nonce?: string;
90
+ } | null;
91
+ return json?.nonce;
92
+ } catch {
93
+ return undefined;
94
+ }
95
+ }
@@ -0,0 +1,78 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { passwordLevel, validatePassword } from "./password";
4
+
5
+ describe("validatePassword", () => {
6
+ it("returns valid for matching passwords >= 8 chars with letters and digits", () => {
7
+ const result = validatePassword("password123", "password123");
8
+ expect(result.valid).toBe(true);
9
+ expect(result.errorKey).toBeNull();
10
+ });
11
+
12
+ it("returns mismatch when passwords differ", () => {
13
+ const result = validatePassword("password1", "password2");
14
+ expect(result.valid).toBe(false);
15
+ expect(result.errorKey).toBe("common.validation.passwordMismatch");
16
+ });
17
+
18
+ it("returns minLength when password < 8 chars", () => {
19
+ const result = validatePassword("short", "short");
20
+ expect(result.valid).toBe(false);
21
+ expect(result.errorKey).toBe("common.validation.minLength");
22
+ });
23
+
24
+ it("prioritises mismatch over minLength", () => {
25
+ const result = validatePassword("abc", "xyz");
26
+ expect(result.valid).toBe(false);
27
+ expect(result.errorKey).toBe("common.validation.passwordMismatch");
28
+ });
29
+
30
+ it("accepts an exactly 8-char password with letters and digits", () => {
31
+ const result = validatePassword("pass1234", "pass1234");
32
+ expect(result.valid).toBe(true);
33
+ });
34
+
35
+ it("rejects digits-only password as needing variety", () => {
36
+ const result = validatePassword("12345678", "12345678");
37
+ expect(result.valid).toBe(false);
38
+ expect(result.errorKey).toBe("common.validation.passwordNeedsVariety");
39
+ });
40
+
41
+ it("rejects letters-only password as needing variety", () => {
42
+ const result = validatePassword("password", "password");
43
+ expect(result.valid).toBe(false);
44
+ expect(result.errorKey).toBe("common.validation.passwordNeedsVariety");
45
+ });
46
+
47
+ it("rejects empty passwords with minLength", () => {
48
+ const result = validatePassword("", "");
49
+ expect(result.valid).toBe(false);
50
+ expect(result.errorKey).toBe("common.validation.minLength");
51
+ });
52
+ });
53
+
54
+ describe("passwordLevel", () => {
55
+ it("returns null for empty password", () => {
56
+ expect(passwordLevel("")).toBeNull();
57
+ });
58
+
59
+ it("returns weak when below 8 chars", () => {
60
+ expect(passwordLevel("Ab1")).toBe("weak");
61
+ });
62
+
63
+ it("returns weak for 8 chars without digits", () => {
64
+ expect(passwordLevel("password")).toBe("weak");
65
+ });
66
+
67
+ it("returns fair for 8 chars with letters and digits", () => {
68
+ expect(passwordLevel("pass1234")).toBe("fair");
69
+ });
70
+
71
+ it("returns strong for 10+ chars with upper, lower, and special", () => {
72
+ expect(passwordLevel("Pass1234!x")).toBe("strong");
73
+ });
74
+
75
+ it("returns fair (not strong) when missing special char", () => {
76
+ expect(passwordLevel("Password1234")).toBe("fair");
77
+ });
78
+ });