@wenathlan/saddle 2.0.150 → 2.0.152

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 (296) hide show
  1. package/Dockerfile +43 -23
  2. package/README.md +4 -4
  3. package/biome.jsonc +134 -0
  4. package/dist/App.d.ts +3 -0
  5. package/dist/App.js +50 -0
  6. package/dist/Sol/Sol.d.ts +3 -0
  7. package/dist/Sol/Sol.js +46 -0
  8. package/dist/Sol/agentbrowser/agentbrowser.d.ts +9 -0
  9. package/dist/Sol/agentbrowser/agentbrowser.js +121 -0
  10. package/dist/Sol/architecture/architecture.d.ts +9 -0
  11. package/dist/Sol/architecture/architecture.js +93 -0
  12. package/dist/Sol/compute/compute.d.ts +9 -0
  13. package/dist/Sol/compute/compute.js +22 -0
  14. package/dist/Sol/console/console.d.ts +15 -0
  15. package/dist/Sol/console/console.js +292 -0
  16. package/dist/Sol/dashboard/dashboard.d.ts +10 -0
  17. package/dist/Sol/dashboard/dashboard.js +181 -0
  18. package/dist/Sol/docs/docs.d.ts +9 -0
  19. package/dist/Sol/docs/docs.js +28 -0
  20. package/dist/Sol/home/home.d.ts +35 -0
  21. package/dist/Sol/home/home.js +239 -0
  22. package/dist/Sol/integrations/integrations.d.ts +9 -0
  23. package/dist/Sol/integrations/integrations.js +83 -0
  24. package/dist/Sol/intro/intro.d.ts +7 -0
  25. package/dist/Sol/intro/intro.js +102 -0
  26. package/dist/Sol/login/login.d.ts +16 -0
  27. package/dist/Sol/login/login.js +49 -0
  28. package/dist/Sol/notfound/notfound.d.ts +9 -0
  29. package/dist/Sol/notfound/notfound.js +15 -0
  30. package/dist/Sol/onboarding/onboarding.d.ts +25 -0
  31. package/dist/Sol/onboarding/onboarding.js +60 -0
  32. package/dist/Sol/playground/playground.d.ts +9 -0
  33. package/dist/Sol/playground/playground.js +127 -0
  34. package/dist/Sol/register/register.d.ts +16 -0
  35. package/dist/Sol/register/register.js +193 -0
  36. package/dist/Sol/shell/Shell.d.ts +124 -0
  37. package/dist/Sol/shell/Shell.js +335 -0
  38. package/dist/Sol/wingets/ComputeBootladder.d.ts +6 -0
  39. package/dist/Sol/wingets/ComputeBootladder.js +71 -0
  40. package/dist/Sol/wingets/ComputeModalitycatalog.d.ts +7 -0
  41. package/dist/Sol/wingets/ComputeModalitycatalog.js +71 -0
  42. package/dist/Sol/wingets/ConsoleApibadge.d.ts +7 -0
  43. package/dist/Sol/wingets/ConsoleApibadge.js +5 -0
  44. package/dist/Sol/wingets/ConsoleEdgelaw.d.ts +6 -0
  45. package/dist/Sol/wingets/ConsoleEdgelaw.js +36 -0
  46. package/dist/Sol/wingets/ConsoleEventstimeline.d.ts +19 -0
  47. package/dist/Sol/wingets/ConsoleEventstimeline.js +12 -0
  48. package/dist/Sol/wingets/ConsoleSnapshotcontrols.d.ts +8 -0
  49. package/dist/Sol/wingets/ConsoleSnapshotcontrols.js +8 -0
  50. package/dist/Sol/wingets/ConsoleSpecpanel.d.ts +21 -0
  51. package/dist/Sol/wingets/ConsoleSpecpanel.js +75 -0
  52. package/dist/Sol/wingets/ConsoleTerminal.d.ts +22 -0
  53. package/dist/Sol/wingets/ConsoleTerminal.js +77 -0
  54. package/dist/Sol/wingets/DashboardAccountpanel.d.ts +18 -0
  55. package/dist/Sol/wingets/DashboardAccountpanel.js +65 -0
  56. package/dist/Sol/wingets/DashboardAdminpanel.d.ts +6 -0
  57. package/dist/Sol/wingets/DashboardAdminpanel.js +90 -0
  58. package/dist/Sol/wingets/DashboardAuditlog.d.ts +8 -0
  59. package/dist/Sol/wingets/DashboardAuditlog.js +50 -0
  60. package/dist/Sol/wingets/DashboardCreatesandboxform.d.ts +10 -0
  61. package/dist/Sol/wingets/DashboardCreatesandboxform.js +71 -0
  62. package/dist/Sol/wingets/DashboardEventstimeline.d.ts +8 -0
  63. package/dist/Sol/wingets/DashboardEventstimeline.js +83 -0
  64. package/dist/Sol/wingets/DashboardGlobalsandboxestable.d.ts +8 -0
  65. package/dist/Sol/wingets/DashboardGlobalsandboxestable.js +57 -0
  66. package/dist/Sol/wingets/DashboardMeshnodestable.d.ts +8 -0
  67. package/dist/Sol/wingets/DashboardMeshnodestable.js +103 -0
  68. package/dist/Sol/wingets/DashboardSandboxlist.d.ts +10 -0
  69. package/dist/Sol/wingets/DashboardSandboxlist.js +163 -0
  70. package/dist/Sol/wingets/DashboardUserstable.d.ts +8 -0
  71. package/dist/Sol/wingets/DashboardUserstable.js +78 -0
  72. package/dist/Sol/wingets/HomeEntrygate.d.ts +23 -0
  73. package/dist/Sol/wingets/HomeEntrygate.js +73 -0
  74. package/dist/Sol/wingets/HomeMetricstrip.d.ts +9 -0
  75. package/dist/Sol/wingets/HomeMetricstrip.js +4 -0
  76. package/dist/Sol/wingets/HomeRuntimediagram.d.ts +1 -0
  77. package/dist/Sol/wingets/HomeRuntimediagram.js +22 -0
  78. package/dist/Sol/wingets/LoginForm.d.ts +19 -0
  79. package/dist/Sol/wingets/LoginForm.js +141 -0
  80. package/dist/Sol/wingets/RegisterPasswordconfirm.d.ts +13 -0
  81. package/dist/Sol/wingets/RegisterPasswordconfirm.js +16 -0
  82. package/dist/Sol/wingets/RegisterPasswordmeter.d.ts +19 -0
  83. package/dist/Sol/wingets/RegisterPasswordmeter.js +65 -0
  84. package/dist/Sol/wingets/RegisterUsernamerules.d.ts +11 -0
  85. package/dist/Sol/wingets/RegisterUsernamerules.js +16 -0
  86. package/dist/Sol/wingets/index.d.ts +39 -0
  87. package/dist/Sol/wingets/index.js +39 -0
  88. package/dist/alternatives.d.ts +508 -0
  89. package/dist/alternatives.js +950 -0
  90. package/dist/api.d.ts +154 -0
  91. package/dist/api.js +174 -0
  92. package/dist/auth.d.ts +268 -0
  93. package/dist/auth.js +476 -0
  94. package/dist/automation.d.ts +425 -0
  95. package/dist/automation.js +566 -0
  96. package/dist/browser.d.ts +561 -0
  97. package/dist/browser.js +1058 -0
  98. package/dist/capacitor.config.d.ts +45 -0
  99. package/dist/capacitor.config.js +48 -0
  100. package/dist/catalog.d.ts +43 -0
  101. package/dist/catalog.js +44 -0
  102. package/dist/cli.d.ts +16 -0
  103. package/dist/cli.js +168 -0
  104. package/dist/communication.d.ts +181 -0
  105. package/dist/communication.js +376 -0
  106. package/dist/compute.d.ts +1160 -0
  107. package/dist/compute.js +2272 -0
  108. package/dist/dashboardlib.d.ts +58 -0
  109. package/dist/dashboardlib.js +161 -0
  110. package/dist/db.d.ts +569 -0
  111. package/dist/db.js +1080 -0
  112. package/dist/distribution.d.ts +528 -0
  113. package/dist/distribution.js +810 -0
  114. package/dist/drizzle.config.d.ts +20 -0
  115. package/dist/drizzle.config.js +18 -0
  116. package/dist/edgelandes.d.ts +70 -0
  117. package/dist/edgelandes.host.d.ts +26 -0
  118. package/dist/edgelandes.host.js +1089 -0
  119. package/dist/edgelandes.inproc.d.ts +27 -0
  120. package/dist/edgelandes.inproc.js +952 -0
  121. package/dist/edgelandes.js +1133 -0
  122. package/dist/edgelandes.ninety.d.ts +31 -0
  123. package/dist/edgelandes.ninety.js +174 -0
  124. package/dist/edgelandes.virtual.d.ts +25 -0
  125. package/dist/edgelandes.virtual.js +629 -0
  126. package/dist/edgeplane.d.ts +114 -0
  127. package/dist/edgeplane.js +305 -0
  128. package/dist/examplelocaljob.d.ts +1 -0
  129. package/{docs/example-localjob.ts → dist/examplelocaljob.js} +0 -1
  130. package/dist/execution.d.ts +529 -0
  131. package/dist/execution.js +760 -0
  132. package/dist/family.d.ts +16 -0
  133. package/dist/family.js +42 -0
  134. package/dist/familyurl.d.ts +21 -0
  135. package/dist/familyurl.js +24 -0
  136. package/dist/format.d.ts +18 -0
  137. package/dist/format.js +59 -0
  138. package/dist/foundation.d.ts +172 -0
  139. package/dist/foundation.js +312 -0
  140. package/dist/index.d.ts +735 -0
  141. package/dist/index.js +1020 -0
  142. package/dist/integration.d.ts +235 -0
  143. package/dist/integration.js +228 -0
  144. package/dist/intelligence.d.ts +93 -0
  145. package/dist/intelligence.js +144 -0
  146. package/dist/isolation.d.ts +128 -0
  147. package/dist/isolation.js +95 -0
  148. package/dist/labels.d.ts +665 -0
  149. package/dist/labels.js +751 -0
  150. package/dist/localauth.d.ts +127 -0
  151. package/dist/localauth.js +413 -0
  152. package/dist/media.d.ts +343 -0
  153. package/dist/media.js +985 -0
  154. package/dist/mesh.d.ts +177 -0
  155. package/dist/mesh.js +468 -0
  156. package/dist/modalityregistry.d.ts +109 -0
  157. package/dist/modalityregistry.js +1386 -0
  158. package/dist/modes.d.ts +358 -0
  159. package/dist/modes.js +133 -0
  160. package/dist/onboardingstate.d.ts +79 -0
  161. package/dist/onboardingstate.js +282 -0
  162. package/dist/orchestrator.d.ts +1338 -0
  163. package/dist/orchestrator.js +4310 -0
  164. package/dist/packager/targetcli.js +4 -0
  165. package/dist/paths.d.ts +2 -0
  166. package/dist/paths.js +14 -0
  167. package/dist/performance.d.ts +415 -0
  168. package/dist/performance.js +826 -0
  169. package/dist/quantum.d.ts +809 -0
  170. package/dist/quantum.js +2072 -0
  171. package/dist/render.d.ts +380 -0
  172. package/dist/render.js +1072 -0
  173. package/dist/runall.d.ts +2 -0
  174. package/dist/runall.js +39 -0
  175. package/dist/runtimeprobe.d.ts +4 -0
  176. package/dist/runtimeprobe.js +8 -0
  177. package/dist/sandbox.d.ts +355 -0
  178. package/dist/sandbox.js +1721 -0
  179. package/dist/sandboxenvprofile.d.ts +29 -0
  180. package/dist/sandboxenvprofile.js +89 -0
  181. package/dist/sandboxprofile.d.ts +137 -0
  182. package/dist/sandboxprofile.js +241 -0
  183. package/dist/sandboxqueue.d.ts +108 -0
  184. package/dist/sandboxqueue.js +201 -0
  185. package/dist/sandboxquota.d.ts +121 -0
  186. package/dist/sandboxquota.js +236 -0
  187. package/dist/scheduler.d.ts +595 -0
  188. package/dist/scheduler.js +1258 -0
  189. package/dist/scriptlanes.d.ts +182 -0
  190. package/dist/scriptlanes.js +660 -0
  191. package/dist/scripts.buildengine.d.ts +1 -0
  192. package/dist/scripts.buildengine.js +39 -0
  193. package/dist/scripts.securitygate.d.ts +1 -0
  194. package/dist/scripts.securitygate.js +68 -0
  195. package/dist/scripts.validateflatnative.d.ts +1 -0
  196. package/dist/scripts.validateflatnative.js +72 -0
  197. package/dist/security.d.ts +500 -0
  198. package/dist/security.js +1169 -0
  199. package/dist/seedcatalog.d.ts +13 -0
  200. package/dist/seedcatalog.js +46 -0
  201. package/dist/server.d.ts +20 -0
  202. package/dist/server.js +53 -0
  203. package/dist/speccatalog.d.ts +31 -0
  204. package/dist/speccatalog.js +163 -0
  205. package/dist/tiers.d.ts +1001 -0
  206. package/dist/tiers.js +2169 -0
  207. package/dist/utils.d.ts +2 -0
  208. package/dist/utils.js +5 -0
  209. package/dist/vaulthandoff.d.ts +141 -0
  210. package/dist/vaulthandoff.js +444 -0
  211. package/dist/virtual.d.ts +640 -0
  212. package/dist/virtual.js +1284 -0
  213. package/dist/virtualcpu.d.ts +315 -0
  214. package/dist/virtualcpu.js +1180 -0
  215. package/dist/virtualgpu.d.ts +344 -0
  216. package/dist/virtualgpu.js +1000 -0
  217. package/dist/virtualization.d.ts +606 -0
  218. package/dist/virtualization.js +1186 -0
  219. package/dist/virtualmemory.d.ts +817 -0
  220. package/dist/virtualmemory.js +1795 -0
  221. package/dist/vite.config.d.ts +3 -0
  222. package/dist/vite.config.js +43 -0
  223. package/dist/vitest.config.d.ts +2 -0
  224. package/dist/vitest.config.js +25 -0
  225. package/dist/webserver.d.ts +166 -0
  226. package/dist/webserver.js +2226 -0
  227. package/dist/workloadlaw.d.ts +133 -0
  228. package/dist/workloadlaw.js +193 -0
  229. package/docker.config +13 -6
  230. package/docs/artifactavailability.md +2 -2
  231. package/docs/{containerplatforms-2.1.0.md → containerplatforms.2.1.0.md} +1 -1
  232. package/docs/{documentmap-1.8.12.md → documentmap.1.8.12.md} +1 -1
  233. package/docs/ninety.modalities.md +108 -0
  234. package/docs/planning.ninety.md +391 -0
  235. package/docs/{releasenotes-1.8.13.md → releasenotes.1.8.13.md} +2 -2
  236. package/docs/{releasenotes-1.8.14.md → releasenotes.1.8.14.md} +1 -1
  237. package/docs/{repo-synthesis-1.8.16.md → repo.synthesis.1.8.16.md} +1 -1
  238. package/docs/{todo-1.8.15.md → todo.1.8.15.md} +1 -1
  239. package/docs/{todo-1.8.16.md → todo.1.8.16.md} +5 -5
  240. package/docs/todo.md +2 -2
  241. package/docs/workloadlaw.md +104 -0
  242. package/index.ts +4 -3
  243. package/manifest.json +1 -1
  244. package/modalityregistry.ts +1493 -0
  245. package/orchestrator.ts +1 -1
  246. package/package.json +76 -37
  247. package/docs/CONVERSA.txt +0 -1523
  248. package/docs/architecture-1.8.19.png +0 -0
  249. package/docs/assets/architecture.svg +0 -45
  250. package/docs/assets/saddle-browser-trace.webp +0 -0
  251. package/docs/assets/saddle-hero-bridge.webp +0 -0
  252. package/docs/assets/saddle-mark.webp +0 -0
  253. package/docs/assets/saddle-runtime-map.webp +0 -0
  254. package/docs/assets/saddlemark.svg +0 -13
  255. package/docs/example-publicapi.ts +0 -7
  256. package/docs/saddle.archive.1.8.17.tar.gz.gpg +0 -0
  257. /package/docs/{architecture-1.8.19.md → architecture.1.8.19.md} +0 -0
  258. /package/docs/{architecture-1.8.19.mmd → architecture.1.8.19.mmd} +0 -0
  259. /package/docs/{artifactresearch-1.8.9.md → artifactresearch.1.8.9.md} +0 -0
  260. /package/docs/{batches-1816-1820.md → batches.1816.to.1820.md} +0 -0
  261. /package/docs/{brancharchive-2026-08-18.md → brancharchive.2026.08.18.md} +0 -0
  262. /package/docs/{containerplatforms-1.8.17.md → containerplatforms.1.8.17.md} +0 -0
  263. /package/docs/{e2ugh-engine.md → e2ugh.engine.md} +0 -0
  264. /package/docs/{flatbuildresearch-1.8.11.md → flatbuildresearch.1.8.11.md} +0 -0
  265. /package/docs/{licenseaudit-1.8.12.md → licenseaudit.1.8.12.md} +0 -0
  266. /package/docs/{mobileconversionresearch-1.8.10.md → mobileconversionresearch.1.8.10.md} +0 -0
  267. /package/docs/{native-wrappers.md → native.wrappers.md} +0 -0
  268. /package/docs/{nativeidentityresearch-1.8.11.md → nativeidentityresearch.1.8.11.md} +0 -0
  269. /package/docs/{releasenotes-1.8.12.md → releasenotes.1.8.12.md} +0 -0
  270. /package/docs/{releasenotes-1.8.15.md → releasenotes.1.8.15.md} +0 -0
  271. /package/docs/{releasenotes-1.8.16.md → releasenotes.1.8.16.md} +0 -0
  272. /package/docs/{releasenotes-1.8.17.md → releasenotes.1.8.17.md} +0 -0
  273. /package/docs/{releasenotes-1.8.18.md → releasenotes.1.8.18.md} +0 -0
  274. /package/docs/{releasenotes-1.8.19.md → releasenotes.1.8.19.md} +0 -0
  275. /package/docs/{reorganization-1.8.8.md → reorganization.1.8.8.md} +0 -0
  276. /package/docs/{reorganization-1.8.9.md → reorganization.1.8.9.md} +0 -0
  277. /package/docs/{repo-analysis-1.8.21.json → repo.analysis.1.8.21.json} +0 -0
  278. /package/docs/{repo-candidates-1.8.21.jsonl → repo.candidates.1.8.21.jsonl} +0 -0
  279. /package/docs/{repo-research-1.8.16.md → repo.research.1.8.16.md} +0 -0
  280. /package/docs/{repo-research-1.8.21.md → repo.research.1.8.21.md} +0 -0
  281. /package/docs/{repo-selected-1.8.21.json → repo.selected.1.8.21.json} +0 -0
  282. /package/docs/{repo-synthesis-1.8.21.md → repo.synthesis.1.8.21.md} +0 -0
  283. /package/docs/{research-1.8.18-isolation.md → research.1.8.18.isolation.md} +0 -0
  284. /package/docs/{research-1.8.19-virtual-browser.md → research.1.8.19.virtual.browser.md} +0 -0
  285. /package/docs/{scanningresearch-1.8.11.md → scanningresearch.1.8.11.md} +0 -0
  286. /package/docs/{securityaudit-1.8.7.md → securityaudit.1.8.7.md} +0 -0
  287. /package/docs/{securityreferences-1.8.11.md → securityreferences.1.8.11.md} +0 -0
  288. /package/docs/{securityresearch-1.8.11.md → securityresearch.1.8.11.md} +0 -0
  289. /package/docs/{signingresearch-1.8.12.md → signingresearch.1.8.12.md} +0 -0
  290. /package/docs/{tauri-glib-risk-1.8.11.md → tauri.glib.risk.1.8.11.md} +0 -0
  291. /package/docs/{todo-1.8.18.md → todo.1.8.18.md} +0 -0
  292. /package/docs/{todo-1.8.19.md → todo.1.8.19.md} +0 -0
  293. /package/docs/{toolchainresearch-1.8.9.md → toolchainresearch.1.8.9.md} +0 -0
  294. /package/docs/{web-duplicatecleanup.md → web.duplicatecleanup.md} +0 -0
  295. /package/docs/{web-ideas.md → web.ideas.md} +0 -0
  296. /package/docs/{workflowimprovements-2026-08-19.md → workflowimprovements.2026.08.19.md} +0 -0
@@ -0,0 +1,127 @@
1
+ /**
2
+ * browser-local account fallback for the static edge (github pages,
3
+ * netlify, vercel clones, file://): when no api backend answers at the
4
+ * resolved base, the auth pages switch to local browser accounts so the
5
+ * full interface still works on hosting without a server. local
6
+ * accounts live ONLY in this browser (localstorage, pbkdf2-hashed
7
+ * passwords through webcrypto); they never sync to the main node and
8
+ * never hold real authority - the self-hosted node keeps the real
9
+ * scrypt/cookie sessions (see the saddle readme, static-first design).
10
+ *
11
+ * typed ES module conversion of web/localauth.js (the window-global
12
+ * IIFE): same storage keys, same seed, same keyfile format, same
13
+ * returns. the module is shared by the Login, Register and Dashboard
14
+ * pages and exposes:
15
+ * probe(base) - true when the api answers
16
+ * available() - local accounts exist
17
+ * register(u, p) - creates a local account (throws on conflict)
18
+ * login(u, p) - verifies and opens a local session
19
+ * session() - the open local session or null
20
+ * logout() - closes the local session
21
+ * exportaccounts() - keyfile backup download
22
+ * importaccounts(file) - keyfile restore (merge)
23
+ * roleof(u) - "admin" only for the CODEOWNERS names
24
+ *
25
+ * built-in admins: the CODEOWNERS accounts (iakadion, inathlan,
26
+ * aasblor, nasblor) are seeded as local admin accounts when the
27
+ * bootstrap seed rides the build env (VITE_SADDLE_ADMIN_SEED_PASSWORD).
28
+ * the public repository carries no hardcoded credential — the seed
29
+ * password is a pipeline secret injected at build time, and when the
30
+ * env is absent the seed is skipped (locally created accounts still
31
+ * work; only the CODEOWNERS accounts carry the admin role anywhere).
32
+ * clearing the browser storage never locks the interface out: the seed
33
+ * is re-applied whenever the env is present and the store is missing
34
+ * the admin entries, so "the account never comes back" cannot happen
35
+ * on the static edge (vercel / netlify / github pages).
36
+ */
37
+ /** role of a local account: "admin" only for the CODEOWNERS names. */
38
+ export type LocalAccountRole = 'admin' | 'user';
39
+ /** one stored local account (salt + hash, never the plaintext). */
40
+ export type LocalAccountRecord = {
41
+ salt: string;
42
+ hash: string;
43
+ iterations: number;
44
+ algorithm: 'pbkdf2-sha256';
45
+ createdat: string;
46
+ role: LocalAccountRole;
47
+ seeded?: boolean;
48
+ };
49
+ /** the open local session (12h lifetime, localstorage only). */
50
+ export type LocalSession = {
51
+ user: string;
52
+ mode: 'local';
53
+ role: LocalAccountRole;
54
+ issuedat: number;
55
+ expiresat: number;
56
+ };
57
+ /** the backup keyfile payload produced by exportaccounts. */
58
+ export type AccountsBackupPayload = {
59
+ format: 'saddle-local-accounts/1';
60
+ exportedat: string;
61
+ accounts: Record<string, LocalAccountRecord>;
62
+ };
63
+ /** the exported admin allowlist (a defensive copy). */
64
+ export declare const adminusers: readonly string[];
65
+ /**
66
+ * exports every account (salt + hash only, never plaintext passwords)
67
+ * as a downloadable backup keyfile.
68
+ *
69
+ * @returns the number of exported accounts.
70
+ */
71
+ export declare function exportaccounts(): Promise<number>;
72
+ /**
73
+ * imports accounts from a backup keyfile (merges; the seed and
74
+ * operator-owned entries are never overwritten).
75
+ *
76
+ * @param file the keyfile chosen by the operator.
77
+ * @returns the number of imported accounts.
78
+ * @throws when the file is not a saddle accounts backup.
79
+ */
80
+ export declare function importaccounts(file: File): Promise<number>;
81
+ /**
82
+ * probes the api health endpoint with a short timeout.
83
+ *
84
+ * @param base the api base to probe ("" means same-origin).
85
+ * @returns true when the api answers ok.
86
+ */
87
+ export declare function probe(base: string): Promise<boolean>;
88
+ /**
89
+ * creates a local browser account and opens the local session.
90
+ *
91
+ * @param username the account name (validated by the caller).
92
+ * @param password the plaintext password (hashed before storage).
93
+ * @returns true on success.
94
+ * @throws when the username already exists on this browser.
95
+ */
96
+ export declare function register(username: string, password: string): Promise<boolean>;
97
+ /**
98
+ * verifies the password against the stored account and opens the
99
+ * local session.
100
+ *
101
+ * @param username the account name.
102
+ * @param password the plaintext password.
103
+ * @returns true on success.
104
+ * @throws when the account is missing or the password is wrong.
105
+ */
106
+ export declare function login(username: string, password: string): Promise<boolean>;
107
+ /**
108
+ * reads the open local session.
109
+ *
110
+ * @returns the session when valid and unexpired, null otherwise.
111
+ */
112
+ export declare function session(): LocalSession | null;
113
+ /** closes the local session. */
114
+ export declare function logout(): void;
115
+ /**
116
+ * checks whether local accounts exist on this browser.
117
+ *
118
+ * @returns true when at least one account is stored.
119
+ */
120
+ export declare function available(): boolean;
121
+ /**
122
+ * the role of an account: "admin" only for the CODEOWNERS names.
123
+ *
124
+ * @param username the account name.
125
+ * @returns "admin" or "user".
126
+ */
127
+ export declare function roleof(username: string): LocalAccountRole;
@@ -0,0 +1,413 @@
1
+ /**
2
+ * browser-local account fallback for the static edge (github pages,
3
+ * netlify, vercel clones, file://): when no api backend answers at the
4
+ * resolved base, the auth pages switch to local browser accounts so the
5
+ * full interface still works on hosting without a server. local
6
+ * accounts live ONLY in this browser (localstorage, pbkdf2-hashed
7
+ * passwords through webcrypto); they never sync to the main node and
8
+ * never hold real authority - the self-hosted node keeps the real
9
+ * scrypt/cookie sessions (see the saddle readme, static-first design).
10
+ *
11
+ * typed ES module conversion of web/localauth.js (the window-global
12
+ * IIFE): same storage keys, same seed, same keyfile format, same
13
+ * returns. the module is shared by the Login, Register and Dashboard
14
+ * pages and exposes:
15
+ * probe(base) - true when the api answers
16
+ * available() - local accounts exist
17
+ * register(u, p) - creates a local account (throws on conflict)
18
+ * login(u, p) - verifies and opens a local session
19
+ * session() - the open local session or null
20
+ * logout() - closes the local session
21
+ * exportaccounts() - keyfile backup download
22
+ * importaccounts(file) - keyfile restore (merge)
23
+ * roleof(u) - "admin" only for the CODEOWNERS names
24
+ *
25
+ * built-in admins: the CODEOWNERS accounts (iakadion, inathlan,
26
+ * aasblor, nasblor) are seeded as local admin accounts when the
27
+ * bootstrap seed rides the build env (VITE_SADDLE_ADMIN_SEED_PASSWORD).
28
+ * the public repository carries no hardcoded credential — the seed
29
+ * password is a pipeline secret injected at build time, and when the
30
+ * env is absent the seed is skipped (locally created accounts still
31
+ * work; only the CODEOWNERS accounts carry the admin role anywhere).
32
+ * clearing the browser storage never locks the interface out: the seed
33
+ * is re-applied whenever the env is present and the store is missing
34
+ * the admin entries, so "the account never comes back" cannot happen
35
+ * on the static edge (vercel / netlify / github pages).
36
+ */
37
+ /** localstorage key carrying the local account map. */
38
+ const userskey = 'saddle_local_users';
39
+ /** localstorage key carrying the open local session. */
40
+ const sessionkey = 'saddle_local_session';
41
+ /** pbkdf2 iteration count, kept in lockstep with the stored records. */
42
+ const iterations = 150000;
43
+ /** IndexedDB mirror coordinates (database / object store names). */
44
+ const idbname = 'saddle-auth';
45
+ const idbstore = 'accounts';
46
+ /** the ONLY admin usernames: the CODEOWNERS list. mirror changes to
47
+ * .github CODEOWNERS here (the two files are the same contract). */
48
+ const adminlist = ['iakadion', 'inathlan', 'aasblor', 'nasblor'];
49
+ /** bootstrap credentials for the seeded admins: the CODEOWNERS shared
50
+ * password rides the build env as a pipeline secret (never hardcoded
51
+ * in the public repository) and stays in lockstep with the self-hosted
52
+ * node (auth.ts resolves SADDLE_ADMIN_SEED_PASSWORD the same way this
53
+ * module resolves the build twin) and the database seed. when the env
54
+ * is absent the seed is skipped entirely. */
55
+ const adminseedpassword = import.meta.env.VITE_SADDLE_ADMIN_SEED_PASSWORD ?? '';
56
+ /** the exported admin allowlist (a defensive copy). */
57
+ export const adminusers = adminlist.slice();
58
+ function readusers() {
59
+ try {
60
+ const raw = window.localStorage.getItem(userskey);
61
+ const parsed = raw ? JSON.parse(raw) : {};
62
+ return parsed && typeof parsed === 'object'
63
+ ? parsed
64
+ : {};
65
+ }
66
+ catch {
67
+ return {};
68
+ }
69
+ }
70
+ function writeusers(map) {
71
+ window.localStorage.setItem(userskey, JSON.stringify(map));
72
+ void mirrorusers(map);
73
+ }
74
+ /**
75
+ * re-seeds the built-in admin accounts whenever the store is missing
76
+ * them (first visit, cleared storage, private mode). the seed is
77
+ * idempotent: an admin the operator re-registered with a custom
78
+ * password is never overwritten. skipped entirely when the build env
79
+ * carries no seed password (the public repository never hardcodes one).
80
+ */
81
+ async function ensureadminseed() {
82
+ if (!adminseedpassword)
83
+ return;
84
+ const users = readusers();
85
+ let changed = false;
86
+ for (const name of adminlist) {
87
+ if (!Object.hasOwn(users, name)) {
88
+ const salt = randomsalt();
89
+ const password = adminseedpassword;
90
+ users[name] = {
91
+ salt,
92
+ hash: await derive(password, salt),
93
+ iterations,
94
+ algorithm: 'pbkdf2-sha256',
95
+ createdat: new Date().toISOString(),
96
+ role: 'admin',
97
+ seeded: true,
98
+ };
99
+ changed = true;
100
+ }
101
+ }
102
+ if (changed)
103
+ writeusers(users);
104
+ }
105
+ /* ---------------- persistence: the IndexedDB mirror ----------------
106
+ * user accounts live in localStorage AND in an IndexedDB store; the
107
+ * mirror is repaired in both directions on load, so a tool that
108
+ * clears only one of them (or a partial eviction) never loses the
109
+ * accounts - "the account never comes back" cannot happen. the
110
+ * mirror also powers the explicit backup file: accounts can be
111
+ * exported to a json keyfile and re-imported after a full wipe. */
112
+ function openidb() {
113
+ return new Promise((resolve) => {
114
+ try {
115
+ if (!window.indexedDB) {
116
+ resolve(null);
117
+ return;
118
+ }
119
+ const request = window.indexedDB.open(idbname, 1);
120
+ request.onupgradeneeded = () => {
121
+ request.result.createObjectStore(idbstore);
122
+ };
123
+ request.onsuccess = () => {
124
+ resolve(request.result);
125
+ };
126
+ request.onerror = () => {
127
+ resolve(null);
128
+ };
129
+ }
130
+ catch {
131
+ resolve(null);
132
+ }
133
+ });
134
+ }
135
+ function idbread(db) {
136
+ return new Promise((resolve) => {
137
+ try {
138
+ const tx = db.transaction(idbstore, 'readonly');
139
+ const get = tx.objectStore(idbstore).get('users');
140
+ get.onsuccess = () => {
141
+ const result = get.result;
142
+ resolve(result ?? null);
143
+ };
144
+ get.onerror = () => {
145
+ resolve(null);
146
+ };
147
+ }
148
+ catch {
149
+ resolve(null);
150
+ }
151
+ });
152
+ }
153
+ function idbwrite(db, map) {
154
+ return new Promise((resolve) => {
155
+ try {
156
+ const tx = db.transaction(idbstore, 'readwrite');
157
+ tx.objectStore(idbstore).put(map, 'users');
158
+ tx.oncomplete = () => {
159
+ resolve(true);
160
+ };
161
+ tx.onerror = () => {
162
+ resolve(false);
163
+ };
164
+ }
165
+ catch {
166
+ resolve(false);
167
+ }
168
+ });
169
+ }
170
+ /** mirrors the account map into IndexedDB (fire and forget). */
171
+ async function mirrorusers(map) {
172
+ const db = await openidb();
173
+ if (db)
174
+ await idbwrite(db, map);
175
+ }
176
+ /** restores localStorage from the IndexedDB mirror when the primary
177
+ * store lost the accounts (partial clear). */
178
+ async function restorefrommirror() {
179
+ try {
180
+ const raw = window.localStorage.getItem(userskey);
181
+ if (raw !== null && Object.keys(JSON.parse(raw)).length > 0) {
182
+ return; /* primary store still carries accounts */
183
+ }
184
+ }
185
+ catch {
186
+ /* fall through to the mirror */
187
+ }
188
+ const db = await openidb();
189
+ if (!db)
190
+ return;
191
+ const mirrored = await idbread(db);
192
+ if (mirrored && typeof mirrored === 'object' && Object.keys(mirrored).length > 0) {
193
+ window.localStorage.setItem(userskey, JSON.stringify(mirrored));
194
+ }
195
+ }
196
+ /**
197
+ * exports every account (salt + hash only, never plaintext passwords)
198
+ * as a downloadable backup keyfile.
199
+ *
200
+ * @returns the number of exported accounts.
201
+ */
202
+ export async function exportaccounts() {
203
+ const map = readusers();
204
+ const payload = {
205
+ format: 'saddle-local-accounts/1',
206
+ exportedat: new Date().toISOString(),
207
+ accounts: map,
208
+ };
209
+ const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
210
+ const url = URL.createObjectURL(blob);
211
+ const link = document.createElement('a');
212
+ link.href = url;
213
+ link.download = 'saddle-accounts-backup.json';
214
+ document.body.append(link);
215
+ link.click();
216
+ link.remove();
217
+ setTimeout(() => URL.revokeObjectURL(url), 2000);
218
+ return Object.keys(map).length;
219
+ }
220
+ /**
221
+ * imports accounts from a backup keyfile (merges; the seed and
222
+ * operator-owned entries are never overwritten).
223
+ *
224
+ * @param file the keyfile chosen by the operator.
225
+ * @returns the number of imported accounts.
226
+ * @throws when the file is not a saddle accounts backup.
227
+ */
228
+ export async function importaccounts(file) {
229
+ const text = await file.text();
230
+ const payload = JSON.parse(text);
231
+ if (payload?.format !== 'saddle-local-accounts/1' || typeof payload.accounts !== 'object') {
232
+ throw new Error('not a saddle accounts backup file.');
233
+ }
234
+ const users = readusers();
235
+ let imported = 0;
236
+ for (const [name, record] of Object.entries(payload.accounts)) {
237
+ if (!/^[a-z0-9.-]{3,32}$/.test(name))
238
+ continue;
239
+ if (!record || typeof record.salt !== 'string' || typeof record.hash !== 'string')
240
+ continue;
241
+ if (Object.hasOwn(users, name))
242
+ continue;
243
+ users[name] = record;
244
+ imported += 1;
245
+ }
246
+ writeusers(users);
247
+ return imported;
248
+ }
249
+ function randomsalt() {
250
+ const bytes = new Uint8Array(16);
251
+ crypto.getRandomValues(bytes);
252
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
253
+ }
254
+ function tohex(buffer) {
255
+ return Array.from(new Uint8Array(buffer), (b) => b.toString(16).padStart(2, '0')).join('');
256
+ }
257
+ /** pbkdf2-sha256 hash of the password with the account salt. */
258
+ async function derive(password, salt) {
259
+ const encoder = new TextEncoder();
260
+ const key = await crypto.subtle.importKey('raw', encoder.encode(password), 'PBKDF2', false, [
261
+ 'deriveBits',
262
+ ]);
263
+ const bits = await crypto.subtle.deriveBits({
264
+ name: 'PBKDF2',
265
+ salt: encoder.encode(salt),
266
+ iterations,
267
+ hash: 'SHA-256',
268
+ }, key, 256);
269
+ return tohex(bits);
270
+ }
271
+ /** constant-time-ish string compare (length-safe). */
272
+ function safecompare(a, b) {
273
+ if (typeof a !== 'string' || typeof b !== 'string' || a.length !== b.length)
274
+ return false;
275
+ let diff = 0;
276
+ for (let i = 0; i < a.length; i += 1)
277
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
278
+ return diff === 0;
279
+ }
280
+ /**
281
+ * probes the api health endpoint with a short timeout.
282
+ *
283
+ * @param base the api base to probe ("" means same-origin).
284
+ * @returns true when the api answers ok.
285
+ */
286
+ export async function probe(base) {
287
+ try {
288
+ const url = `${base ? base.replace(/\/+$/, '') : ''}/api/v1/health`;
289
+ const controller = new AbortController();
290
+ const timer = setTimeout(() => controller.abort(), 2500);
291
+ const response = await fetch(url, { signal: controller.signal });
292
+ clearTimeout(timer);
293
+ if (!response.ok)
294
+ return false;
295
+ const body = (await response.json());
296
+ return Boolean(body && body.ok === true);
297
+ }
298
+ catch {
299
+ return false;
300
+ }
301
+ }
302
+ /**
303
+ * creates a local browser account and opens the local session.
304
+ *
305
+ * @param username the account name (validated by the caller).
306
+ * @param password the plaintext password (hashed before storage).
307
+ * @returns true on success.
308
+ * @throws when the username already exists on this browser.
309
+ */
310
+ export async function register(username, password) {
311
+ const users = readusers();
312
+ if (Object.hasOwn(users, username)) {
313
+ throw new Error('that username is already registered on this browser.');
314
+ }
315
+ const salt = randomsalt();
316
+ const hash = await derive(password, salt);
317
+ users[username] = {
318
+ salt,
319
+ hash,
320
+ iterations,
321
+ algorithm: 'pbkdf2-sha256',
322
+ createdat: new Date().toISOString(),
323
+ role: roleof(username),
324
+ };
325
+ writeusers(users);
326
+ opensession(username);
327
+ return true;
328
+ }
329
+ /**
330
+ * verifies the password against the stored account and opens the
331
+ * local session.
332
+ *
333
+ * @param username the account name.
334
+ * @param password the plaintext password.
335
+ * @returns true on success.
336
+ * @throws when the account is missing or the password is wrong.
337
+ */
338
+ export async function login(username, password) {
339
+ const users = readusers();
340
+ const account = users[username];
341
+ if (!account)
342
+ throw new Error('no local account for that username on this browser.');
343
+ const hash = await derive(password, account.salt);
344
+ if (!safecompare(hash, account.hash))
345
+ throw new Error('wrong password.');
346
+ opensession(username);
347
+ return true;
348
+ }
349
+ function opensession(username) {
350
+ const session = {
351
+ user: username,
352
+ mode: 'local',
353
+ role: roleof(username),
354
+ issuedat: Date.now(),
355
+ expiresat: Date.now() + 12 * 60 * 60 * 1000,
356
+ };
357
+ window.localStorage.setItem(sessionkey, JSON.stringify(session));
358
+ }
359
+ /**
360
+ * reads the open local session.
361
+ *
362
+ * @returns the session when valid and unexpired, null otherwise.
363
+ */
364
+ export function session() {
365
+ try {
366
+ const raw = window.localStorage.getItem(sessionkey);
367
+ if (!raw)
368
+ return null;
369
+ const parsed = JSON.parse(raw);
370
+ if (parsed?.mode !== 'local' || typeof parsed.user !== 'string')
371
+ return null;
372
+ if (typeof parsed.expiresat !== 'number' || parsed.expiresat < Date.now()) {
373
+ window.localStorage.removeItem(sessionkey);
374
+ return null;
375
+ }
376
+ return parsed;
377
+ }
378
+ catch {
379
+ return null;
380
+ }
381
+ }
382
+ /** closes the local session. */
383
+ export function logout() {
384
+ window.localStorage.removeItem(sessionkey);
385
+ }
386
+ /**
387
+ * checks whether local accounts exist on this browser.
388
+ *
389
+ * @returns true when at least one account is stored.
390
+ */
391
+ export function available() {
392
+ return Object.keys(readusers()).length > 0;
393
+ }
394
+ /**
395
+ * the role of an account: "admin" only for the CODEOWNERS names.
396
+ *
397
+ * @param username the account name.
398
+ * @returns "admin" or "user".
399
+ */
400
+ export function roleof(username) {
401
+ return adminlist.includes(String(username).toLowerCase()) ? 'admin' : 'user';
402
+ }
403
+ /**
404
+ * boot order: restore from the mirror first (so the seed sees the
405
+ * accounts that survived a partial clear), then re-seed the admin
406
+ * allowlist when missing. runs once at module load, exactly like the
407
+ * window-global original.
408
+ */
409
+ async function boot() {
410
+ await restorefrommirror();
411
+ await ensureadminseed();
412
+ }
413
+ void boot();