@wenathlan/saddle 2.0.151 → 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 (293) hide show
  1. package/README.md +3 -3
  2. package/biome.jsonc +12 -5
  3. package/dist/App.d.ts +3 -0
  4. package/dist/App.js +50 -0
  5. package/dist/Sol/Sol.d.ts +3 -0
  6. package/dist/Sol/Sol.js +46 -0
  7. package/dist/Sol/agentbrowser/agentbrowser.d.ts +9 -0
  8. package/dist/Sol/agentbrowser/agentbrowser.js +121 -0
  9. package/dist/Sol/architecture/architecture.d.ts +9 -0
  10. package/dist/Sol/architecture/architecture.js +93 -0
  11. package/dist/Sol/compute/compute.d.ts +9 -0
  12. package/dist/Sol/compute/compute.js +22 -0
  13. package/dist/Sol/console/console.d.ts +15 -0
  14. package/dist/Sol/console/console.js +292 -0
  15. package/dist/Sol/dashboard/dashboard.d.ts +10 -0
  16. package/dist/Sol/dashboard/dashboard.js +181 -0
  17. package/dist/Sol/docs/docs.d.ts +9 -0
  18. package/dist/Sol/docs/docs.js +28 -0
  19. package/dist/Sol/home/home.d.ts +35 -0
  20. package/dist/Sol/home/home.js +239 -0
  21. package/dist/Sol/integrations/integrations.d.ts +9 -0
  22. package/dist/Sol/integrations/integrations.js +83 -0
  23. package/dist/Sol/intro/intro.d.ts +7 -0
  24. package/dist/Sol/intro/intro.js +102 -0
  25. package/dist/Sol/login/login.d.ts +16 -0
  26. package/dist/Sol/login/login.js +49 -0
  27. package/dist/Sol/notfound/notfound.d.ts +9 -0
  28. package/dist/Sol/notfound/notfound.js +15 -0
  29. package/dist/Sol/onboarding/onboarding.d.ts +25 -0
  30. package/dist/Sol/onboarding/onboarding.js +60 -0
  31. package/dist/Sol/playground/playground.d.ts +9 -0
  32. package/dist/Sol/playground/playground.js +127 -0
  33. package/dist/Sol/register/register.d.ts +16 -0
  34. package/dist/Sol/register/register.js +193 -0
  35. package/dist/Sol/shell/Shell.d.ts +124 -0
  36. package/dist/Sol/shell/Shell.js +335 -0
  37. package/dist/Sol/wingets/ComputeBootladder.d.ts +6 -0
  38. package/dist/Sol/wingets/ComputeBootladder.js +71 -0
  39. package/dist/Sol/wingets/ComputeModalitycatalog.d.ts +7 -0
  40. package/dist/Sol/wingets/ComputeModalitycatalog.js +71 -0
  41. package/dist/Sol/wingets/ConsoleApibadge.d.ts +7 -0
  42. package/dist/Sol/wingets/ConsoleApibadge.js +5 -0
  43. package/dist/Sol/wingets/ConsoleEdgelaw.d.ts +6 -0
  44. package/dist/Sol/wingets/ConsoleEdgelaw.js +36 -0
  45. package/dist/Sol/wingets/ConsoleEventstimeline.d.ts +19 -0
  46. package/dist/Sol/wingets/ConsoleEventstimeline.js +12 -0
  47. package/dist/Sol/wingets/ConsoleSnapshotcontrols.d.ts +8 -0
  48. package/dist/Sol/wingets/ConsoleSnapshotcontrols.js +8 -0
  49. package/dist/Sol/wingets/ConsoleSpecpanel.d.ts +21 -0
  50. package/dist/Sol/wingets/ConsoleSpecpanel.js +75 -0
  51. package/dist/Sol/wingets/ConsoleTerminal.d.ts +22 -0
  52. package/dist/Sol/wingets/ConsoleTerminal.js +77 -0
  53. package/dist/Sol/wingets/DashboardAccountpanel.d.ts +18 -0
  54. package/dist/Sol/wingets/DashboardAccountpanel.js +65 -0
  55. package/dist/Sol/wingets/DashboardAdminpanel.d.ts +6 -0
  56. package/dist/Sol/wingets/DashboardAdminpanel.js +90 -0
  57. package/dist/Sol/wingets/DashboardAuditlog.d.ts +8 -0
  58. package/dist/Sol/wingets/DashboardAuditlog.js +50 -0
  59. package/dist/Sol/wingets/DashboardCreatesandboxform.d.ts +10 -0
  60. package/dist/Sol/wingets/DashboardCreatesandboxform.js +71 -0
  61. package/dist/Sol/wingets/DashboardEventstimeline.d.ts +8 -0
  62. package/dist/Sol/wingets/DashboardEventstimeline.js +83 -0
  63. package/dist/Sol/wingets/DashboardGlobalsandboxestable.d.ts +8 -0
  64. package/dist/Sol/wingets/DashboardGlobalsandboxestable.js +57 -0
  65. package/dist/Sol/wingets/DashboardMeshnodestable.d.ts +8 -0
  66. package/dist/Sol/wingets/DashboardMeshnodestable.js +103 -0
  67. package/dist/Sol/wingets/DashboardSandboxlist.d.ts +10 -0
  68. package/dist/Sol/wingets/DashboardSandboxlist.js +163 -0
  69. package/dist/Sol/wingets/DashboardUserstable.d.ts +8 -0
  70. package/dist/Sol/wingets/DashboardUserstable.js +78 -0
  71. package/dist/Sol/wingets/HomeEntrygate.d.ts +23 -0
  72. package/dist/Sol/wingets/HomeEntrygate.js +73 -0
  73. package/dist/Sol/wingets/HomeMetricstrip.d.ts +9 -0
  74. package/dist/Sol/wingets/HomeMetricstrip.js +4 -0
  75. package/dist/Sol/wingets/HomeRuntimediagram.d.ts +1 -0
  76. package/dist/Sol/wingets/HomeRuntimediagram.js +22 -0
  77. package/dist/Sol/wingets/LoginForm.d.ts +19 -0
  78. package/dist/Sol/wingets/LoginForm.js +141 -0
  79. package/dist/Sol/wingets/RegisterPasswordconfirm.d.ts +13 -0
  80. package/dist/Sol/wingets/RegisterPasswordconfirm.js +16 -0
  81. package/dist/Sol/wingets/RegisterPasswordmeter.d.ts +19 -0
  82. package/dist/Sol/wingets/RegisterPasswordmeter.js +65 -0
  83. package/dist/Sol/wingets/RegisterUsernamerules.d.ts +11 -0
  84. package/dist/Sol/wingets/RegisterUsernamerules.js +16 -0
  85. package/dist/Sol/wingets/index.d.ts +39 -0
  86. package/dist/Sol/wingets/index.js +39 -0
  87. package/dist/alternatives.d.ts +508 -0
  88. package/dist/alternatives.js +950 -0
  89. package/dist/api.d.ts +154 -0
  90. package/dist/api.js +174 -0
  91. package/dist/auth.d.ts +268 -0
  92. package/dist/auth.js +476 -0
  93. package/dist/automation.d.ts +425 -0
  94. package/dist/automation.js +566 -0
  95. package/dist/browser.d.ts +561 -0
  96. package/dist/browser.js +1058 -0
  97. package/dist/capacitor.config.d.ts +45 -0
  98. package/dist/capacitor.config.js +48 -0
  99. package/dist/catalog.d.ts +43 -0
  100. package/dist/catalog.js +44 -0
  101. package/dist/cli.d.ts +16 -0
  102. package/dist/cli.js +168 -0
  103. package/dist/communication.d.ts +181 -0
  104. package/dist/communication.js +376 -0
  105. package/dist/compute.d.ts +1160 -0
  106. package/dist/compute.js +2272 -0
  107. package/dist/dashboardlib.d.ts +58 -0
  108. package/dist/dashboardlib.js +161 -0
  109. package/dist/db.d.ts +569 -0
  110. package/dist/db.js +1080 -0
  111. package/dist/distribution.d.ts +528 -0
  112. package/dist/distribution.js +810 -0
  113. package/dist/drizzle.config.d.ts +20 -0
  114. package/dist/drizzle.config.js +18 -0
  115. package/dist/edgelandes.d.ts +70 -0
  116. package/dist/edgelandes.host.d.ts +26 -0
  117. package/dist/edgelandes.host.js +1089 -0
  118. package/dist/edgelandes.inproc.d.ts +27 -0
  119. package/dist/edgelandes.inproc.js +952 -0
  120. package/dist/edgelandes.js +1133 -0
  121. package/dist/edgelandes.ninety.d.ts +31 -0
  122. package/dist/edgelandes.ninety.js +174 -0
  123. package/dist/edgelandes.virtual.d.ts +25 -0
  124. package/dist/edgelandes.virtual.js +629 -0
  125. package/dist/edgeplane.d.ts +114 -0
  126. package/dist/edgeplane.js +305 -0
  127. package/dist/examplelocaljob.d.ts +1 -0
  128. package/{docs/example-localjob.ts → dist/examplelocaljob.js} +0 -1
  129. package/dist/execution.d.ts +529 -0
  130. package/dist/execution.js +760 -0
  131. package/dist/family.d.ts +16 -0
  132. package/dist/family.js +42 -0
  133. package/dist/familyurl.d.ts +21 -0
  134. package/dist/familyurl.js +24 -0
  135. package/dist/format.d.ts +18 -0
  136. package/dist/format.js +59 -0
  137. package/dist/foundation.d.ts +172 -0
  138. package/dist/foundation.js +312 -0
  139. package/dist/index.d.ts +735 -0
  140. package/dist/index.js +1020 -0
  141. package/dist/integration.d.ts +235 -0
  142. package/dist/integration.js +228 -0
  143. package/dist/intelligence.d.ts +93 -0
  144. package/dist/intelligence.js +144 -0
  145. package/dist/isolation.d.ts +128 -0
  146. package/dist/isolation.js +95 -0
  147. package/dist/labels.d.ts +665 -0
  148. package/dist/labels.js +751 -0
  149. package/dist/localauth.d.ts +127 -0
  150. package/dist/localauth.js +413 -0
  151. package/dist/media.d.ts +343 -0
  152. package/dist/media.js +985 -0
  153. package/dist/mesh.d.ts +177 -0
  154. package/dist/mesh.js +468 -0
  155. package/dist/modalityregistry.d.ts +109 -0
  156. package/dist/modalityregistry.js +1386 -0
  157. package/dist/modes.d.ts +358 -0
  158. package/dist/modes.js +133 -0
  159. package/dist/onboardingstate.d.ts +79 -0
  160. package/dist/onboardingstate.js +282 -0
  161. package/dist/orchestrator.d.ts +1338 -0
  162. package/dist/orchestrator.js +4310 -0
  163. package/dist/packager/targetcli.js +4 -0
  164. package/dist/paths.d.ts +2 -0
  165. package/dist/paths.js +14 -0
  166. package/dist/performance.d.ts +415 -0
  167. package/dist/performance.js +826 -0
  168. package/dist/quantum.d.ts +809 -0
  169. package/dist/quantum.js +2072 -0
  170. package/dist/render.d.ts +380 -0
  171. package/dist/render.js +1072 -0
  172. package/dist/runall.d.ts +2 -0
  173. package/dist/runall.js +39 -0
  174. package/dist/runtimeprobe.d.ts +4 -0
  175. package/dist/runtimeprobe.js +8 -0
  176. package/dist/sandbox.d.ts +355 -0
  177. package/dist/sandbox.js +1721 -0
  178. package/dist/sandboxenvprofile.d.ts +29 -0
  179. package/dist/sandboxenvprofile.js +89 -0
  180. package/dist/sandboxprofile.d.ts +137 -0
  181. package/dist/sandboxprofile.js +241 -0
  182. package/dist/sandboxqueue.d.ts +108 -0
  183. package/dist/sandboxqueue.js +201 -0
  184. package/dist/sandboxquota.d.ts +121 -0
  185. package/dist/sandboxquota.js +236 -0
  186. package/dist/scheduler.d.ts +595 -0
  187. package/dist/scheduler.js +1258 -0
  188. package/dist/scriptlanes.d.ts +182 -0
  189. package/dist/scriptlanes.js +660 -0
  190. package/dist/scripts.buildengine.d.ts +1 -0
  191. package/dist/scripts.buildengine.js +39 -0
  192. package/dist/scripts.securitygate.d.ts +1 -0
  193. package/dist/scripts.securitygate.js +68 -0
  194. package/dist/scripts.validateflatnative.d.ts +1 -0
  195. package/dist/scripts.validateflatnative.js +72 -0
  196. package/dist/security.d.ts +500 -0
  197. package/dist/security.js +1169 -0
  198. package/dist/seedcatalog.d.ts +13 -0
  199. package/dist/seedcatalog.js +46 -0
  200. package/dist/server.d.ts +20 -0
  201. package/dist/server.js +53 -0
  202. package/dist/speccatalog.d.ts +31 -0
  203. package/dist/speccatalog.js +163 -0
  204. package/dist/tiers.d.ts +1001 -0
  205. package/dist/tiers.js +2169 -0
  206. package/dist/utils.d.ts +2 -0
  207. package/dist/utils.js +5 -0
  208. package/dist/vaulthandoff.d.ts +141 -0
  209. package/dist/vaulthandoff.js +444 -0
  210. package/dist/virtual.d.ts +640 -0
  211. package/dist/virtual.js +1284 -0
  212. package/dist/virtualcpu.d.ts +315 -0
  213. package/dist/virtualcpu.js +1180 -0
  214. package/dist/virtualgpu.d.ts +344 -0
  215. package/dist/virtualgpu.js +1000 -0
  216. package/dist/virtualization.d.ts +606 -0
  217. package/dist/virtualization.js +1186 -0
  218. package/dist/virtualmemory.d.ts +817 -0
  219. package/dist/virtualmemory.js +1795 -0
  220. package/dist/vite.config.d.ts +3 -0
  221. package/dist/vite.config.js +43 -0
  222. package/dist/vitest.config.d.ts +2 -0
  223. package/dist/vitest.config.js +25 -0
  224. package/dist/webserver.d.ts +166 -0
  225. package/dist/webserver.js +2226 -0
  226. package/dist/workloadlaw.d.ts +133 -0
  227. package/dist/workloadlaw.js +193 -0
  228. package/docs/artifactavailability.md +2 -2
  229. package/docs/{containerplatforms-2.1.0.md → containerplatforms.2.1.0.md} +1 -1
  230. package/docs/{documentmap-1.8.12.md → documentmap.1.8.12.md} +1 -1
  231. package/docs/ninety.modalities.md +108 -0
  232. package/docs/planning.ninety.md +391 -0
  233. package/docs/{releasenotes-1.8.13.md → releasenotes.1.8.13.md} +2 -2
  234. package/docs/{releasenotes-1.8.14.md → releasenotes.1.8.14.md} +1 -1
  235. package/docs/{repo-synthesis-1.8.16.md → repo.synthesis.1.8.16.md} +1 -1
  236. package/docs/{todo-1.8.15.md → todo.1.8.15.md} +1 -1
  237. package/docs/{todo-1.8.16.md → todo.1.8.16.md} +5 -5
  238. package/docs/todo.md +2 -2
  239. package/docs/workloadlaw.md +104 -0
  240. package/index.ts +1 -1
  241. package/manifest.json +1 -1
  242. package/modalityregistry.ts +1371 -34
  243. package/orchestrator.ts +1 -1
  244. package/package.json +73 -33
  245. package/docs/CONVERSA.txt +0 -1523
  246. package/docs/architecture-1.8.19.png +0 -0
  247. package/docs/assets/architecture.svg +0 -45
  248. package/docs/assets/saddle-browser-trace.webp +0 -0
  249. package/docs/assets/saddle-hero-bridge.webp +0 -0
  250. package/docs/assets/saddle-mark.webp +0 -0
  251. package/docs/assets/saddle-runtime-map.webp +0 -0
  252. package/docs/assets/saddlemark.svg +0 -13
  253. package/docs/saddle.archive.1.8.17.tar.gz.gpg +0 -0
  254. /package/docs/{architecture-1.8.19.md → architecture.1.8.19.md} +0 -0
  255. /package/docs/{architecture-1.8.19.mmd → architecture.1.8.19.mmd} +0 -0
  256. /package/docs/{artifactresearch-1.8.9.md → artifactresearch.1.8.9.md} +0 -0
  257. /package/docs/{batches-1816-1820.md → batches.1816.to.1820.md} +0 -0
  258. /package/docs/{brancharchive-2026-08-18.md → brancharchive.2026.08.18.md} +0 -0
  259. /package/docs/{containerplatforms-1.8.17.md → containerplatforms.1.8.17.md} +0 -0
  260. /package/docs/{e2ugh-engine.md → e2ugh.engine.md} +0 -0
  261. /package/docs/{flatbuildresearch-1.8.11.md → flatbuildresearch.1.8.11.md} +0 -0
  262. /package/docs/{licenseaudit-1.8.12.md → licenseaudit.1.8.12.md} +0 -0
  263. /package/docs/{mobileconversionresearch-1.8.10.md → mobileconversionresearch.1.8.10.md} +0 -0
  264. /package/docs/{native-wrappers.md → native.wrappers.md} +0 -0
  265. /package/docs/{nativeidentityresearch-1.8.11.md → nativeidentityresearch.1.8.11.md} +0 -0
  266. /package/docs/{releasenotes-1.8.12.md → releasenotes.1.8.12.md} +0 -0
  267. /package/docs/{releasenotes-1.8.15.md → releasenotes.1.8.15.md} +0 -0
  268. /package/docs/{releasenotes-1.8.16.md → releasenotes.1.8.16.md} +0 -0
  269. /package/docs/{releasenotes-1.8.17.md → releasenotes.1.8.17.md} +0 -0
  270. /package/docs/{releasenotes-1.8.18.md → releasenotes.1.8.18.md} +0 -0
  271. /package/docs/{releasenotes-1.8.19.md → releasenotes.1.8.19.md} +0 -0
  272. /package/docs/{reorganization-1.8.8.md → reorganization.1.8.8.md} +0 -0
  273. /package/docs/{reorganization-1.8.9.md → reorganization.1.8.9.md} +0 -0
  274. /package/docs/{repo-analysis-1.8.21.json → repo.analysis.1.8.21.json} +0 -0
  275. /package/docs/{repo-candidates-1.8.21.jsonl → repo.candidates.1.8.21.jsonl} +0 -0
  276. /package/docs/{repo-research-1.8.16.md → repo.research.1.8.16.md} +0 -0
  277. /package/docs/{repo-research-1.8.21.md → repo.research.1.8.21.md} +0 -0
  278. /package/docs/{repo-selected-1.8.21.json → repo.selected.1.8.21.json} +0 -0
  279. /package/docs/{repo-synthesis-1.8.21.md → repo.synthesis.1.8.21.md} +0 -0
  280. /package/docs/{research-1.8.18-isolation.md → research.1.8.18.isolation.md} +0 -0
  281. /package/docs/{research-1.8.19-virtual-browser.md → research.1.8.19.virtual.browser.md} +0 -0
  282. /package/docs/{scanningresearch-1.8.11.md → scanningresearch.1.8.11.md} +0 -0
  283. /package/docs/{securityaudit-1.8.7.md → securityaudit.1.8.7.md} +0 -0
  284. /package/docs/{securityreferences-1.8.11.md → securityreferences.1.8.11.md} +0 -0
  285. /package/docs/{securityresearch-1.8.11.md → securityresearch.1.8.11.md} +0 -0
  286. /package/docs/{signingresearch-1.8.12.md → signingresearch.1.8.12.md} +0 -0
  287. /package/docs/{tauri-glib-risk-1.8.11.md → tauri.glib.risk.1.8.11.md} +0 -0
  288. /package/docs/{todo-1.8.18.md → todo.1.8.18.md} +0 -0
  289. /package/docs/{todo-1.8.19.md → todo.1.8.19.md} +0 -0
  290. /package/docs/{toolchainresearch-1.8.9.md → toolchainresearch.1.8.9.md} +0 -0
  291. /package/docs/{web-duplicatecleanup.md → web.duplicatecleanup.md} +0 -0
  292. /package/docs/{web-ideas.md → web.ideas.md} +0 -0
  293. /package/docs/{workflowimprovements-2026-08-19.md → workflowimprovements.2026.08.19.md} +0 -0
package/dist/mesh.d.ts ADDED
@@ -0,0 +1,177 @@
1
+ /**
2
+ * mesh.ts — signed node-to-node communication for the saddle web mesh
3
+ * (v7-BACK).
4
+ *
5
+ * the mesh connects clone nodes to the main authority: requests are
6
+ * signed with HMAC-SHA256 over `${timestamp}.${method}.${path}.${bodyhash}`,
7
+ * protected by a 60-second anti-replay window with a 1024-entry nonce
8
+ * cache, and may optionally encrypt payloads with AES-256-GCM using the
9
+ * SADDLE_MESH_KEY hex secret. the module also carries the node role
10
+ * (SADDLE_ROLE=main|clone|standalone, default standalone), the signed
11
+ * fetch client towards SADDLE_MAIN_URL and the 60-second heartbeat
12
+ * loop clones run against the main registry.
13
+ *
14
+ * contexts (9): types, noderole, requestsigning, antireplay,
15
+ * payloadcrypto, meshmessages, mainclient, heartbeat, sessionforwarding.
16
+ *
17
+ * rules: lowercase identifiers, english jsdoc in third person, no emoji,
18
+ * try/catch on every fallible path, node:* modules plus the global
19
+ * fetch only, zero dependencies, no hardcoded localhost (the main url
20
+ * always comes from the environment).
21
+ */
22
+ import type { IncomingMessage } from 'node:http';
23
+ /** the node role resolved from saddle_role. */
24
+ export type NodeRole = 'main' | 'clone' | 'standalone';
25
+ /** the mesh verification outcome: the ok arm or the rejection payload
26
+ * consumed by writeerror. */
27
+ export type MeshVerdict = {
28
+ ok: true;
29
+ } | {
30
+ ok: false;
31
+ status: number;
32
+ code: string;
33
+ message: string;
34
+ };
35
+ /** the mesh wire envelope {type, from, data, ts}; when the mesh key is
36
+ * configured the data field is replaced by {enc: '<hex>'}. */
37
+ export type MeshEnvelope = {
38
+ type: string;
39
+ from: string;
40
+ data: unknown;
41
+ ts: string;
42
+ };
43
+ /** the opened mesh message produced by openmeshmessage. */
44
+ export type MeshMessage = {
45
+ type: string;
46
+ from: string;
47
+ data: unknown;
48
+ ts: string;
49
+ };
50
+ /** the main node response relayed by the signed client: status, the
51
+ * parsed json body (null when not json) and the set-cookie relay. */
52
+ export type MainResponse = {
53
+ status: number;
54
+ body: unknown;
55
+ setcookie: string[];
56
+ };
57
+ /** the announcing node description of the clone heartbeat loop. */
58
+ export type HeartbeatSelf = {
59
+ url: string;
60
+ region?: string;
61
+ rolename?: string;
62
+ };
63
+ /** the node role: main, clone or standalone. */
64
+ export declare const role: NodeRole;
65
+ /**
66
+ * reads the shared mesh secret from the environment.
67
+ *
68
+ * @returns the secret or an empty string when unconfigured.
69
+ */
70
+ export declare function meshsecret(): string;
71
+ /**
72
+ * reads the main node base url (no trailing slash) from the
73
+ * environment; never a hardcoded address.
74
+ *
75
+ * @returns the trimmed base url or an empty string.
76
+ */
77
+ export declare function mainurl(): string;
78
+ /**
79
+ * computes the sha256 hex digest of one request body.
80
+ *
81
+ * @param body the exact raw body string ('' for GET).
82
+ * @returns the hex body hash.
83
+ */
84
+ export declare function bodyhash(body: string): string;
85
+ /**
86
+ * signs one mesh request: HMAC-SHA256 hex over the string
87
+ * `${timestamp}.${method}.${path}.${bodyhash}`.
88
+ *
89
+ * @param method the upper-case http method.
90
+ * @param path the full request path (e.g. /api/v1/mesh/register).
91
+ * @param body the exact raw body string ('' for GET).
92
+ * @param secret the shared mesh secret.
93
+ * @param timestamp the millisecond timestamp string
94
+ * (defaults to now).
95
+ * @returns the hex signature.
96
+ */
97
+ export declare function sign(method: string, path: string, body: string, secret: string, timestamp?: string): string;
98
+ /**
99
+ * verifies one incoming mesh request: the x-saddle-timestamp header must
100
+ * sit inside the 60-second window, the x-saddle-signature header must
101
+ * match the recomputed HMAC over the raw body, and the signature must
102
+ * not have been seen before (replay protection).
103
+ *
104
+ * @param req the incoming request.
105
+ * @param rawbody the exact raw request body ('' for GET).
106
+ * @param secret the shared mesh secret.
107
+ * @returns the verification outcome.
108
+ */
109
+ export declare function verifymesh(req: IncomingMessage, rawbody: string, secret: string): MeshVerdict;
110
+ /**
111
+ * encrypts one utf-8 payload with AES-256-GCM; the output is hex
112
+ * `iv(12) || ciphertext || tag(16)`.
113
+ *
114
+ * @param plaintext the payload to encrypt.
115
+ * @returns the hex envelope or null when the key is
116
+ * not configured.
117
+ */
118
+ export declare function encrypt(plaintext: string): string | null;
119
+ /**
120
+ * decrypts one AES-256-GCM hex envelope produced by encrypt.
121
+ *
122
+ * @param envelope the hex iv || ciphertext || tag payload.
123
+ * @returns the utf-8 plaintext or null on any failure.
124
+ */
125
+ export declare function decrypt(envelope: string): string | null;
126
+ /**
127
+ * builds one mesh message envelope {type, from, data, ts}; when the
128
+ * mesh key is configured the data field is replaced by the GCM
129
+ * envelope {enc: '<hex>'}.
130
+ *
131
+ * @param type the message type (e.g. 'heartbeat').
132
+ * @param from the sender node id or url.
133
+ * @param data the message payload.
134
+ * @returns the wire envelope.
135
+ */
136
+ export declare function meshmessage(type: string, from: string, data: unknown): MeshEnvelope;
137
+ /**
138
+ * opens one mesh message envelope produced by meshmessage, decrypting
139
+ * the data field when needed.
140
+ *
141
+ * @param envelope the wire envelope.
142
+ * @returns the opened message or null on tampering.
143
+ */
144
+ export declare function openmeshmessage(envelope: unknown): MeshMessage | null;
145
+ /**
146
+ * performs one signed POST towards the main node described by
147
+ * SADDLE_MAIN_URL using the global fetch; the signature headers are
148
+ * computed with the shared SADDLE_MESH_SECRET.
149
+ *
150
+ * @param path the full api path (e.g. /api/v1/auth/login).
151
+ * @param body the json-serializable payload.
152
+ * @returns the response status, the parsed json body and the
153
+ * set-cookie relay.
154
+ * @throws when the mesh is unconfigured, the
155
+ * request fails or the body is not json.
156
+ */
157
+ export declare function postmain(path: string, body: unknown): Promise<MainResponse>;
158
+ /**
159
+ * runs the clone registry loop: one immediate registration followed by
160
+ * heartbeats every 60 seconds, all signed towards the main node. the
161
+ * interval is unref'd so the process can still exit cleanly.
162
+ *
163
+ * @param self the announcing node description.
164
+ * @returns the heartbeat timer or null when the
165
+ * node is not a clone or the mesh is unconfigured.
166
+ */
167
+ export declare function startheartbeat(self: HeartbeatSelf): NodeJS.Timeout | null;
168
+ /**
169
+ * forwards one auth request (register or login) from a clone to the
170
+ * main authority; the caller caches the returned session locally when
171
+ * the main node accepts it.
172
+ *
173
+ * @param action the auth action.
174
+ * @param body the credentials payload.
175
+ * @returns the main response verbatim.
176
+ */
177
+ export declare function forwardauth(action: 'register' | 'login', body: unknown): Promise<MainResponse>;
package/dist/mesh.js ADDED
@@ -0,0 +1,468 @@
1
+ /**
2
+ * mesh.ts — signed node-to-node communication for the saddle web mesh
3
+ * (v7-BACK).
4
+ *
5
+ * the mesh connects clone nodes to the main authority: requests are
6
+ * signed with HMAC-SHA256 over `${timestamp}.${method}.${path}.${bodyhash}`,
7
+ * protected by a 60-second anti-replay window with a 1024-entry nonce
8
+ * cache, and may optionally encrypt payloads with AES-256-GCM using the
9
+ * SADDLE_MESH_KEY hex secret. the module also carries the node role
10
+ * (SADDLE_ROLE=main|clone|standalone, default standalone), the signed
11
+ * fetch client towards SADDLE_MAIN_URL and the 60-second heartbeat
12
+ * loop clones run against the main registry.
13
+ *
14
+ * contexts (9): types, noderole, requestsigning, antireplay,
15
+ * payloadcrypto, meshmessages, mainclient, heartbeat, sessionforwarding.
16
+ *
17
+ * rules: lowercase identifiers, english jsdoc in third person, no emoji,
18
+ * try/catch on every fallible path, node:* modules plus the global
19
+ * fetch only, zero dependencies, no hardcoded localhost (the main url
20
+ * always comes from the environment).
21
+ */
22
+ import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, timingSafeEqual, } from 'node:crypto';
23
+ import process from 'node:process';
24
+ /* ------------------------------------------------------------------ */
25
+ /* context: noderole */
26
+ /* ------------------------------------------------------------------ */
27
+ /**
28
+ * resolves the node role once at import time: saddle_role accepts the
29
+ * values main, clone and standalone; anything else (including unset)
30
+ * falls back to standalone so a bare `node web/server.ts` boot behaves
31
+ * as a self-contained authority.
32
+ *
33
+ * @returns the resolved role.
34
+ */
35
+ function resolverole() {
36
+ const value = String(process.env.SADDLE_ROLE ?? 'standalone').toLowerCase();
37
+ if (value === 'main' || value === 'clone' || value === 'standalone') {
38
+ return value;
39
+ }
40
+ return 'standalone';
41
+ }
42
+ /** the node role: main, clone or standalone. */
43
+ export const role = resolverole();
44
+ /**
45
+ * reads the shared mesh secret from the environment.
46
+ *
47
+ * @returns the secret or an empty string when unconfigured.
48
+ */
49
+ export function meshsecret() {
50
+ return String(process.env.SADDLE_MESH_SECRET ?? '');
51
+ }
52
+ /**
53
+ * reads the main node base url (no trailing slash) from the
54
+ * environment; never a hardcoded address.
55
+ *
56
+ * @returns the trimmed base url or an empty string.
57
+ */
58
+ export function mainurl() {
59
+ return String(process.env.SADDLE_MAIN_URL ?? '').replace(/\/+$/, '');
60
+ }
61
+ /* ------------------------------------------------------------------ */
62
+ /* context: requestsigning */
63
+ /* ------------------------------------------------------------------ */
64
+ /** the anti-replay window in milliseconds. */
65
+ const replaywindowms = 60 * 1000;
66
+ /** the nonce cache ceiling (simple LRU eviction past this size). */
67
+ const noncelimit = 1024;
68
+ /**
69
+ * computes the sha256 hex digest of one request body.
70
+ *
71
+ * @param body the exact raw body string ('' for GET).
72
+ * @returns the hex body hash.
73
+ */
74
+ export function bodyhash(body) {
75
+ return createHash('sha256').update(String(body)).digest('hex');
76
+ }
77
+ /**
78
+ * signs one mesh request: HMAC-SHA256 hex over the string
79
+ * `${timestamp}.${method}.${path}.${bodyhash}`.
80
+ *
81
+ * @param method the upper-case http method.
82
+ * @param path the full request path (e.g. /api/v1/mesh/register).
83
+ * @param body the exact raw body string ('' for GET).
84
+ * @param secret the shared mesh secret.
85
+ * @param timestamp the millisecond timestamp string
86
+ * (defaults to now).
87
+ * @returns the hex signature.
88
+ */
89
+ export function sign(method, path, body, secret, timestamp = String(Date.now())) {
90
+ return createHmac('sha256', String(secret))
91
+ .update(`${timestamp}.${String(method).toUpperCase()}.${path}.${bodyhash(body)}`)
92
+ .digest('hex');
93
+ }
94
+ /* ------------------------------------------------------------------ */
95
+ /* context: antireplay */
96
+ /* ------------------------------------------------------------------ */
97
+ /** seen signature nonces with insertion order for LRU eviction. */
98
+ const nonces = new Map();
99
+ /**
100
+ * records one signature in the nonce cache, evicting the oldest entry
101
+ * past the 1024-entry ceiling.
102
+ *
103
+ * @param signature the verified signature hex.
104
+ * @returns void.
105
+ */
106
+ function recordnonce(signature) {
107
+ if (nonces.has(signature)) {
108
+ return;
109
+ }
110
+ nonces.set(signature, Date.now());
111
+ while (nonces.size > noncelimit) {
112
+ const oldest = nonces.keys().next().value;
113
+ if (oldest === undefined) {
114
+ break;
115
+ }
116
+ nonces.delete(oldest);
117
+ }
118
+ }
119
+ /**
120
+ * verifies one incoming mesh request: the x-saddle-timestamp header must
121
+ * sit inside the 60-second window, the x-saddle-signature header must
122
+ * match the recomputed HMAC over the raw body, and the signature must
123
+ * not have been seen before (replay protection).
124
+ *
125
+ * @param req the incoming request.
126
+ * @param rawbody the exact raw request body ('' for GET).
127
+ * @param secret the shared mesh secret.
128
+ * @returns the verification outcome.
129
+ */
130
+ export function verifymesh(req, rawbody, secret) {
131
+ try {
132
+ const timestamp = req.headers?.['x-saddle-timestamp'];
133
+ const signature = req.headers?.['x-saddle-signature'];
134
+ if (typeof timestamp !== 'string' || typeof signature !== 'string') {
135
+ return {
136
+ ok: false,
137
+ status: 401,
138
+ code: 'mesh-unauthenticated',
139
+ message: 'mesh requests require x-saddle-timestamp and x-saddle-signature headers',
140
+ };
141
+ }
142
+ const skew = Math.abs(Date.now() - Number.parseInt(timestamp, 10));
143
+ if (!Number.isFinite(skew) || skew > replaywindowms) {
144
+ return {
145
+ ok: false,
146
+ status: 401,
147
+ code: 'mesh-stale-timestamp',
148
+ message: 'mesh timestamp outside the 60 second window',
149
+ };
150
+ }
151
+ const path = String(req.url ?? '/').split('?')[0];
152
+ const expected = sign(req.method ?? 'GET', path, String(rawbody), secret, timestamp);
153
+ const left = Buffer.from(expected, 'hex');
154
+ const right = Buffer.from(signature, 'hex');
155
+ if (left.length !== right.length || !timingSafeEqual(left, right)) {
156
+ return {
157
+ ok: false,
158
+ status: 401,
159
+ code: 'mesh-bad-signature',
160
+ message: 'mesh signature mismatch',
161
+ };
162
+ }
163
+ if (nonces.has(signature)) {
164
+ return {
165
+ ok: false,
166
+ status: 401,
167
+ code: 'mesh-replay',
168
+ message: 'mesh signature was already used',
169
+ };
170
+ }
171
+ recordnonce(signature);
172
+ return { ok: true };
173
+ }
174
+ catch (error) {
175
+ return {
176
+ ok: false,
177
+ status: 401,
178
+ code: 'mesh-verify-failed',
179
+ message: error instanceof Error ? error.message : String(error),
180
+ };
181
+ }
182
+ }
183
+ /* ------------------------------------------------------------------ */
184
+ /* context: payloadcrypto (optional AES-256-GCM) */
185
+ /* ------------------------------------------------------------------ */
186
+ /** the authenticated payload key length in bytes. */
187
+ const meshkeylen = 32;
188
+ /** the GCM initialization vector length in bytes. */
189
+ const ivlen = 12;
190
+ /** the GCM authentication tag length in bytes. */
191
+ const taglen = 16;
192
+ /**
193
+ * resolves the optional SADDLE_MESH_KEY (32 bytes hex) used for payload
194
+ * encryption between nodes.
195
+ *
196
+ * @returns the key or null when unset/invalid.
197
+ */
198
+ function meshkey() {
199
+ try {
200
+ const raw = String(process.env.SADDLE_MESH_KEY ?? '');
201
+ if (raw.length === 0) {
202
+ return null;
203
+ }
204
+ const key = Buffer.from(raw, 'hex');
205
+ return key.length === meshkeylen ? key : null;
206
+ }
207
+ catch {
208
+ return null;
209
+ }
210
+ }
211
+ /**
212
+ * encrypts one utf-8 payload with AES-256-GCM; the output is hex
213
+ * `iv(12) || ciphertext || tag(16)`.
214
+ *
215
+ * @param plaintext the payload to encrypt.
216
+ * @returns the hex envelope or null when the key is
217
+ * not configured.
218
+ */
219
+ export function encrypt(plaintext) {
220
+ try {
221
+ const key = meshkey();
222
+ if (key === null) {
223
+ return null;
224
+ }
225
+ const iv = randomBytes(ivlen);
226
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
227
+ const ciphertext = Buffer.concat([
228
+ cipher.update(String(plaintext), 'utf8'),
229
+ cipher.final(),
230
+ ]);
231
+ const tag = cipher.getAuthTag();
232
+ return Buffer.concat([iv, ciphertext, tag]).toString('hex');
233
+ }
234
+ catch {
235
+ return null;
236
+ }
237
+ }
238
+ /**
239
+ * decrypts one AES-256-GCM hex envelope produced by encrypt.
240
+ *
241
+ * @param envelope the hex iv || ciphertext || tag payload.
242
+ * @returns the utf-8 plaintext or null on any failure.
243
+ */
244
+ export function decrypt(envelope) {
245
+ try {
246
+ const key = meshkey();
247
+ if (key === null) {
248
+ return null;
249
+ }
250
+ const raw = Buffer.from(String(envelope), 'hex');
251
+ if (raw.length <= ivlen + taglen) {
252
+ return null;
253
+ }
254
+ const iv = raw.subarray(0, ivlen);
255
+ const tag = raw.subarray(raw.length - taglen);
256
+ const ciphertext = raw.subarray(ivlen, raw.length - taglen);
257
+ const decipher = createDecipheriv('aes-256-gcm', key, iv);
258
+ decipher.setAuthTag(tag);
259
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
260
+ }
261
+ catch {
262
+ return null;
263
+ }
264
+ }
265
+ /* ------------------------------------------------------------------ */
266
+ /* context: meshmessages */
267
+ /* ------------------------------------------------------------------ */
268
+ /**
269
+ * builds one mesh message envelope {type, from, data, ts}; when the
270
+ * mesh key is configured the data field is replaced by the GCM
271
+ * envelope {enc: '<hex>'}.
272
+ *
273
+ * @param type the message type (e.g. 'heartbeat').
274
+ * @param from the sender node id or url.
275
+ * @param data the message payload.
276
+ * @returns the wire envelope.
277
+ */
278
+ export function meshmessage(type, from, data) {
279
+ const envelope = { type, from, data, ts: new Date().toISOString() };
280
+ const key = meshkey();
281
+ if (key !== null) {
282
+ const sealed = encrypt(JSON.stringify(envelope.data));
283
+ if (sealed !== null) {
284
+ return { ...envelope, data: { enc: sealed } };
285
+ }
286
+ }
287
+ return envelope;
288
+ }
289
+ /**
290
+ * opens one mesh message envelope produced by meshmessage, decrypting
291
+ * the data field when needed.
292
+ *
293
+ * @param envelope the wire envelope.
294
+ * @returns the opened message or null on tampering.
295
+ */
296
+ export function openmeshmessage(envelope) {
297
+ try {
298
+ if (envelope === null || typeof envelope !== 'object') {
299
+ return null;
300
+ }
301
+ const message = envelope;
302
+ let data = message.data;
303
+ if (data !== null &&
304
+ typeof data === 'object' &&
305
+ typeof data.enc === 'string') {
306
+ const opened = decrypt(data.enc);
307
+ if (opened === null) {
308
+ return null;
309
+ }
310
+ data = JSON.parse(opened);
311
+ }
312
+ return {
313
+ type: String(message.type ?? ''),
314
+ from: String(message.from ?? ''),
315
+ data,
316
+ ts: String(message.ts ?? ''),
317
+ };
318
+ }
319
+ catch {
320
+ return null;
321
+ }
322
+ }
323
+ /* ------------------------------------------------------------------ */
324
+ /* context: mainclient */
325
+ /* ------------------------------------------------------------------ */
326
+ /** the signed request timeout in milliseconds. */
327
+ const requesttimeoutms = 10 * 1000;
328
+ /**
329
+ * performs one signed POST towards the main node described by
330
+ * SADDLE_MAIN_URL using the global fetch; the signature headers are
331
+ * computed with the shared SADDLE_MESH_SECRET.
332
+ *
333
+ * @param path the full api path (e.g. /api/v1/auth/login).
334
+ * @param body the json-serializable payload.
335
+ * @returns the response status, the parsed json body and the
336
+ * set-cookie relay.
337
+ * @throws when the mesh is unconfigured, the
338
+ * request fails or the body is not json.
339
+ */
340
+ export async function postmain(path, body) {
341
+ const base = mainurl();
342
+ const secret = meshsecret();
343
+ if (base.length === 0 || secret.length === 0) {
344
+ throw Object.assign(new Error('SADDLE_MAIN_URL and SADDLE_MESH_SECRET are required for mesh forwarding'), { code: 'mesh-unconfigured' });
345
+ }
346
+ const payload = JSON.stringify(body ?? {});
347
+ const timestamp = String(Date.now());
348
+ const signature = sign('POST', path, payload, secret, timestamp);
349
+ let response;
350
+ try {
351
+ response = await fetch(`${base}${path}`, {
352
+ method: 'POST',
353
+ headers: {
354
+ 'content-type': 'application/json',
355
+ 'x-saddle-timestamp': timestamp,
356
+ 'x-saddle-signature': signature,
357
+ },
358
+ body: payload,
359
+ signal: AbortSignal.timeout(requesttimeoutms),
360
+ });
361
+ }
362
+ catch (error) {
363
+ throw Object.assign(new Error(`mesh request to ${path} failed: ${error instanceof Error ? error.message : String(error)}`), { code: 'mesh-unreachable' });
364
+ }
365
+ let parsed = null;
366
+ try {
367
+ parsed = await response.json();
368
+ }
369
+ catch {
370
+ parsed = null;
371
+ }
372
+ let setcookie = [];
373
+ try {
374
+ setcookie =
375
+ typeof response.headers.getSetCookie === 'function'
376
+ ? response.headers.getSetCookie()
377
+ : [];
378
+ }
379
+ catch {
380
+ setcookie = [];
381
+ }
382
+ return { status: response.status, body: parsed, setcookie };
383
+ }
384
+ /* ------------------------------------------------------------------ */
385
+ /* context: heartbeat */
386
+ /* ------------------------------------------------------------------ */
387
+ /** the clone heartbeat period in milliseconds. */
388
+ const heartbeatperiodms = 60 * 1000;
389
+ /**
390
+ * runs the clone registry loop: one immediate registration followed by
391
+ * heartbeats every 60 seconds, all signed towards the main node. the
392
+ * interval is unref'd so the process can still exit cleanly.
393
+ *
394
+ * @param self the announcing node description.
395
+ * @returns the heartbeat timer or null when the
396
+ * node is not a clone or the mesh is unconfigured.
397
+ */
398
+ export function startheartbeat(self) {
399
+ if (role !== 'clone' ||
400
+ mainurl().length === 0 ||
401
+ meshsecret().length === 0 ||
402
+ String(self?.url ?? '').length === 0) {
403
+ return null;
404
+ }
405
+ const beat = async () => {
406
+ try {
407
+ const registered = await postmain('/api/v1/mesh/register', self);
408
+ if (registered.status !== 200 && registered.status !== 201) {
409
+ return false;
410
+ }
411
+ const nodeid = registered.body !== null && typeof registered.body === 'object'
412
+ ? String(registered.body?.nodeid ??
413
+ registered.body?.id ??
414
+ '')
415
+ : '';
416
+ if (nodeid.length === 0) {
417
+ return false;
418
+ }
419
+ const pulsed = await postmain('/api/v1/mesh/heartbeat', { nodeid });
420
+ return pulsed.status === 200;
421
+ }
422
+ catch {
423
+ /* the next tick retries; the clone keeps serving locally */
424
+ return false;
425
+ }
426
+ };
427
+ /* boot ladder: the immediate beat may race the main node's own boot,
428
+ * so short retries run until the first success; afterwards the steady
429
+ * 60 second interval keeps the registry fresh. */
430
+ let established = false;
431
+ const runbeat = async () => {
432
+ const ok = await beat();
433
+ if (ok) {
434
+ established = true;
435
+ }
436
+ return ok;
437
+ };
438
+ runbeat();
439
+ for (const delay of [2000, 5000, 10000, 20000]) {
440
+ const retry = setTimeout(() => {
441
+ if (!established) {
442
+ runbeat();
443
+ }
444
+ }, delay);
445
+ retry.unref?.();
446
+ }
447
+ const timer = setInterval(() => {
448
+ runbeat();
449
+ }, heartbeatperiodms);
450
+ timer.unref?.();
451
+ return timer;
452
+ }
453
+ /* ------------------------------------------------------------------ */
454
+ /* context: sessionforwarding */
455
+ /* ------------------------------------------------------------------ */
456
+ /**
457
+ * forwards one auth request (register or login) from a clone to the
458
+ * main authority; the caller caches the returned session locally when
459
+ * the main node accepts it.
460
+ *
461
+ * @param action the auth action.
462
+ * @param body the credentials payload.
463
+ * @returns the main response verbatim.
464
+ */
465
+ export function forwardauth(action, body) {
466
+ const path = action === 'register' ? '/api/v1/auth/register' : '/api/v1/auth/login';
467
+ return postmain(path, body);
468
+ }