@serfab/cadre-host 1.13.0 → 1.14.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 (337) hide show
  1. package/README.md +332 -423
  2. package/dist/bin/host.d.ts +6 -3
  3. package/dist/bin/host.js +301 -505
  4. package/dist/bin/host.js.map +1 -1
  5. package/dist/bin/join-output.d.ts +56 -0
  6. package/dist/bin/join-output.js +92 -0
  7. package/dist/bin/join-output.js.map +1 -0
  8. package/dist/bin/nat-output.d.ts +49 -0
  9. package/dist/bin/nat-output.js +127 -0
  10. package/dist/bin/nat-output.js.map +1 -0
  11. package/dist/hosted/claim-details.d.ts +44 -0
  12. package/dist/hosted/claim-details.js +66 -0
  13. package/dist/hosted/claim-details.js.map +1 -0
  14. package/dist/hosted/hosted-node-service.d.ts +318 -0
  15. package/dist/hosted/hosted-node-service.js +666 -0
  16. package/dist/hosted/hosted-node-service.js.map +1 -0
  17. package/dist/hosted/hosted-node-store.d.ts +40 -0
  18. package/dist/hosted/hosted-node-store.js +126 -0
  19. package/dist/hosted/hosted-node-store.js.map +1 -0
  20. package/dist/{donation/donation-supervisor.d.ts → hosted/hosted-node-supervisor.d.ts} +70 -53
  21. package/dist/hosted/hosted-node-supervisor.js +396 -0
  22. package/dist/hosted/hosted-node-supervisor.js.map +1 -0
  23. package/dist/hosted/hosted-node-watcher.d.ts +83 -0
  24. package/dist/hosted/hosted-node-watcher.js +224 -0
  25. package/dist/hosted/hosted-node-watcher.js.map +1 -0
  26. package/dist/hosted/index.d.ts +16 -0
  27. package/dist/hosted/index.js +13 -0
  28. package/dist/hosted/index.js.map +1 -0
  29. package/dist/hosted/node-status.d.ts +35 -0
  30. package/dist/hosted/node-status.js +61 -0
  31. package/dist/hosted/node-status.js.map +1 -0
  32. package/dist/hosted/types.d.ts +134 -0
  33. package/dist/hosted/types.js +22 -0
  34. package/dist/hosted/types.js.map +1 -0
  35. package/dist/index.d.ts +12 -24
  36. package/dist/index.js +9 -19
  37. package/dist/index.js.map +1 -1
  38. package/dist/installer/config.d.ts +4 -34
  39. package/dist/installer/config.js +3 -18
  40. package/dist/installer/config.js.map +1 -1
  41. package/dist/installer/index.d.ts +7 -19
  42. package/dist/installer/index.js +14 -69
  43. package/dist/installer/index.js.map +1 -1
  44. package/dist/installer/paths.d.ts +0 -2
  45. package/dist/installer/paths.js +0 -4
  46. package/dist/installer/paths.js.map +1 -1
  47. package/dist/installer/wizard.d.ts +0 -7
  48. package/dist/installer/wizard.js +1 -8
  49. package/dist/installer/wizard.js.map +1 -1
  50. package/dist/nat/address-resolver.d.ts +18 -17
  51. package/dist/nat/address-resolver.js +79 -25
  52. package/dist/nat/address-resolver.js.map +1 -1
  53. package/dist/nat/address-watch.d.ts +37 -0
  54. package/dist/nat/address-watch.js +84 -0
  55. package/dist/nat/address-watch.js.map +1 -0
  56. package/dist/nat/ddns/duckdns.d.ts +2 -1
  57. package/dist/nat/ddns/duckdns.js +5 -1
  58. package/dist/nat/ddns/duckdns.js.map +1 -1
  59. package/dist/nat/ddns/index.d.ts +6 -0
  60. package/dist/nat/ddns/index.js.map +1 -1
  61. package/dist/nat/index.d.ts +12 -9
  62. package/dist/nat/index.js +6 -5
  63. package/dist/nat/index.js.map +1 -1
  64. package/dist/nat/nat-service.d.ts +203 -77
  65. package/dist/nat/nat-service.js +602 -237
  66. package/dist/nat/nat-service.js.map +1 -1
  67. package/dist/nat/nat-store.d.ts +12 -5
  68. package/dist/nat/nat-store.js +114 -30
  69. package/dist/nat/nat-store.js.map +1 -1
  70. package/dist/nat/port-mapper.d.ts +83 -79
  71. package/dist/nat/port-mapper.js +148 -189
  72. package/dist/nat/port-mapper.js.map +1 -1
  73. package/dist/nat/reachability.d.ts +31 -15
  74. package/dist/nat/reachability.js +97 -22
  75. package/dist/nat/reachability.js.map +1 -1
  76. package/dist/nat/types.d.ts +78 -27
  77. package/dist/nat/types.js +8 -5
  78. package/dist/nat/types.js.map +1 -1
  79. package/dist/orchestrator/host-process-orchestrator.d.ts +29 -71
  80. package/dist/orchestrator/host-process-orchestrator.js +78 -218
  81. package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
  82. package/dist/{installer/identity.d.ts → orchestrator/identity-file.d.ts} +5 -5
  83. package/dist/{installer/identity.js → orchestrator/identity-file.js} +6 -6
  84. package/dist/orchestrator/identity-file.js.map +1 -0
  85. package/dist/orchestrator/index.d.ts +2 -3
  86. package/dist/orchestrator/index.js +1 -1
  87. package/dist/orchestrator/index.js.map +1 -1
  88. package/dist/orchestrator/node-identity.d.ts +6 -6
  89. package/dist/orchestrator/node-identity.js +7 -7
  90. package/dist/orchestrator/node-identity.js.map +1 -1
  91. package/dist/orchestrator/port-allocator.d.ts +5 -6
  92. package/dist/orchestrator/port-allocator.js +9 -11
  93. package/dist/orchestrator/port-allocator.js.map +1 -1
  94. package/dist/orchestrator/state-store.d.ts +3 -5
  95. package/dist/orchestrator/state-store.js +2 -10
  96. package/dist/orchestrator/state-store.js.map +1 -1
  97. package/dist/orchestrator/types.d.ts +33 -22
  98. package/dist/orchestrator/types.js.map +1 -1
  99. package/dist/server/error-handler.d.ts +5 -25
  100. package/dist/server/error-handler.js +11 -74
  101. package/dist/server/error-handler.js.map +1 -1
  102. package/dist/server/events/types.d.ts +7 -11
  103. package/dist/server/events/types.js +2 -1
  104. package/dist/server/events/types.js.map +1 -1
  105. package/dist/server/index.d.ts +11 -37
  106. package/dist/server/index.js +30 -48
  107. package/dist/server/index.js.map +1 -1
  108. package/dist/server/routes/hosted-nodes.d.ts +28 -0
  109. package/dist/server/routes/hosted-nodes.js +68 -0
  110. package/dist/server/routes/hosted-nodes.js.map +1 -0
  111. package/dist/server/routes/nat.d.ts +4 -1
  112. package/dist/server/routes/nat.js +13 -2
  113. package/dist/server/routes/nat.js.map +1 -1
  114. package/dist/server/routes/nodes.d.ts +6 -20
  115. package/dist/server/routes/nodes.js +7 -86
  116. package/dist/server/routes/nodes.js.map +1 -1
  117. package/dist/server/routes/settings.d.ts +2 -7
  118. package/dist/server/routes/settings.js +3 -7
  119. package/dist/server/routes/settings.js.map +1 -1
  120. package/dist/server/routes/status.d.ts +2 -25
  121. package/dist/server/routes/status.js +1 -11
  122. package/dist/server/routes/status.js.map +1 -1
  123. package/dist/server/server.js +8 -8
  124. package/dist/server/settings-store.d.ts +2 -2
  125. package/dist/server/settings-store.js +2 -2
  126. package/dist/server/static.js +1 -2
  127. package/dist/server/static.js.map +1 -1
  128. package/dist/ui/assets/index-4FWuU8ye.js +19 -0
  129. package/dist/ui/assets/index-4FWuU8ye.js.map +1 -0
  130. package/dist/ui/assets/index-Di9Q4d01.css +1 -0
  131. package/dist/ui/index.html +2 -2
  132. package/dist/update/index.d.ts +2 -2
  133. package/dist/update/index.js +2 -2
  134. package/dist/update/store.d.ts +1 -1
  135. package/dist/update/store.js +1 -1
  136. package/dist/update/types.d.ts +1 -1
  137. package/dist/update/types.js +1 -1
  138. package/package.json +5 -6
  139. package/src/__tests__/cli-nat.smoke.test.ts +219 -212
  140. package/src/__tests__/cli.smoke.test.ts +1 -1
  141. package/src/__tests__/global-setup.ts +2 -3
  142. package/src/__tests__/orchestrator-claim-secret.test.ts +112 -0
  143. package/src/__tests__/orchestrator-handle-contract.ts +134 -134
  144. package/src/__tests__/orchestrator-node-identity.test.ts +182 -203
  145. package/src/__tests__/orchestrator-ports.test.ts +124 -143
  146. package/src/__tests__/orchestrator-push.test.ts +138 -146
  147. package/src/__tests__/orchestrator.test.ts +1019 -1063
  148. package/src/bin/host.ts +368 -553
  149. package/src/bin/join-output.ts +120 -0
  150. package/src/bin/nat-output.ts +177 -0
  151. package/src/{donation → hosted}/__tests__/fake-orchestrator.test.ts +173 -173
  152. package/src/{donation → hosted}/__tests__/fake-orchestrator.ts +265 -243
  153. package/src/hosted/__tests__/hosted-node-service.test.ts +609 -0
  154. package/src/hosted/__tests__/hosted-node-supervisor.test.ts +474 -0
  155. package/src/hosted/__tests__/test-invitation.ts +33 -0
  156. package/src/hosted/claim-details.ts +96 -0
  157. package/src/hosted/hosted-node-service.ts +795 -0
  158. package/src/hosted/hosted-node-store.ts +144 -0
  159. package/src/hosted/hosted-node-supervisor.ts +439 -0
  160. package/src/hosted/hosted-node-watcher.ts +250 -0
  161. package/src/hosted/index.ts +53 -0
  162. package/src/hosted/node-status.ts +71 -0
  163. package/src/hosted/types.ts +153 -0
  164. package/src/index.ts +72 -91
  165. package/src/installer/__tests__/config.test.ts +15 -40
  166. package/src/installer/__tests__/installer.smoke.test.ts +5 -86
  167. package/src/installer/__tests__/wizard.test.ts +56 -64
  168. package/src/installer/config.ts +135 -175
  169. package/src/installer/index.ts +15 -83
  170. package/src/installer/paths.ts +104 -109
  171. package/src/installer/wizard.ts +87 -103
  172. package/src/nat/__tests__/address-resolver.test.ts +107 -82
  173. package/src/nat/__tests__/nat-service.test.ts +433 -408
  174. package/src/nat/__tests__/nat-store.test.ts +127 -93
  175. package/src/nat/__tests__/port-mapper.test.ts +30 -191
  176. package/src/nat/__tests__/reachability.test.ts +96 -13
  177. package/src/nat/address-resolver.ts +94 -52
  178. package/src/nat/address-watch.ts +92 -0
  179. package/src/nat/ddns/duckdns.ts +5 -1
  180. package/src/nat/ddns/index.ts +6 -0
  181. package/src/nat/index.ts +87 -71
  182. package/src/nat/nat-service.ts +889 -501
  183. package/src/nat/nat-store.ts +237 -154
  184. package/src/nat/port-mapper.ts +241 -329
  185. package/src/nat/reachability.ts +134 -25
  186. package/src/nat/types.ts +209 -159
  187. package/src/{installer/__tests__/identity.test.ts → orchestrator/__tests__/identity-file.test.ts} +34 -34
  188. package/src/orchestrator/__tests__/node-identity.test.ts +62 -62
  189. package/src/orchestrator/host-process-orchestrator.ts +81 -243
  190. package/src/{installer/identity.ts → orchestrator/identity-file.ts} +56 -56
  191. package/src/orchestrator/index.ts +9 -10
  192. package/src/orchestrator/node-identity.ts +41 -41
  193. package/src/orchestrator/port-allocator.ts +71 -73
  194. package/src/orchestrator/state-store.ts +76 -90
  195. package/src/orchestrator/types.ts +34 -23
  196. package/src/server/__tests__/bus.test.ts +38 -38
  197. package/src/server/__tests__/error-handler.test.ts +81 -118
  198. package/src/server/__tests__/fakes.ts +40 -44
  199. package/src/server/__tests__/hosted-nodes-route.test.ts +160 -0
  200. package/src/server/__tests__/nodes-route.test.ts +114 -283
  201. package/src/server/__tests__/publishers.test.ts +193 -180
  202. package/src/server/__tests__/server.smoke.test.ts +134 -223
  203. package/src/server/__tests__/settings-route.test.ts +179 -195
  204. package/src/server/__tests__/sse-route.test.ts +119 -119
  205. package/src/server/__tests__/status-route.test.ts +82 -121
  206. package/src/server/error-handler.ts +112 -178
  207. package/src/server/events/types.ts +20 -24
  208. package/src/server/index.ts +223 -267
  209. package/src/server/routes/hosted-nodes.ts +83 -0
  210. package/src/server/routes/nat.ts +75 -63
  211. package/src/server/routes/nodes.ts +96 -188
  212. package/src/server/routes/settings.ts +115 -123
  213. package/src/server/routes/status.ts +102 -134
  214. package/src/server/server.ts +8 -8
  215. package/src/server/settings-store.ts +2 -2
  216. package/src/server/static.ts +1 -2
  217. package/src/update/index.ts +2 -2
  218. package/src/update/store.ts +1 -1
  219. package/src/update/types.ts +1 -1
  220. package/dist/auth/duration.d.ts +0 -1
  221. package/dist/auth/duration.js +0 -32
  222. package/dist/auth/duration.js.map +0 -1
  223. package/dist/auth/index.d.ts +0 -11
  224. package/dist/auth/index.js +0 -9
  225. package/dist/auth/index.js.map +0 -1
  226. package/dist/auth/self-label.d.ts +0 -36
  227. package/dist/auth/self-label.js +0 -57
  228. package/dist/auth/self-label.js.map +0 -1
  229. package/dist/auth/trust-circle-store.d.ts +0 -26
  230. package/dist/auth/trust-circle-store.js +0 -129
  231. package/dist/auth/trust-circle-store.js.map +0 -1
  232. package/dist/auth/trust-circle.d.ts +0 -156
  233. package/dist/auth/trust-circle.js +0 -307
  234. package/dist/auth/trust-circle.js.map +0 -1
  235. package/dist/auth/types.d.ts +0 -84
  236. package/dist/auth/types.js +0 -24
  237. package/dist/auth/types.js.map +0 -1
  238. package/dist/donation/donation-service.d.ts +0 -389
  239. package/dist/donation/donation-service.js +0 -828
  240. package/dist/donation/donation-service.js.map +0 -1
  241. package/dist/donation/donation-store.d.ts +0 -51
  242. package/dist/donation/donation-store.js +0 -152
  243. package/dist/donation/donation-store.js.map +0 -1
  244. package/dist/donation/donation-supervisor.js +0 -364
  245. package/dist/donation/donation-supervisor.js.map +0 -1
  246. package/dist/donation/grant-service.d.ts +0 -80
  247. package/dist/donation/grant-service.js +0 -210
  248. package/dist/donation/grant-service.js.map +0 -1
  249. package/dist/donation/grant-store.d.ts +0 -38
  250. package/dist/donation/grant-store.js +0 -135
  251. package/dist/donation/grant-store.js.map +0 -1
  252. package/dist/donation/index.d.ts +0 -20
  253. package/dist/donation/index.js +0 -17
  254. package/dist/donation/index.js.map +0 -1
  255. package/dist/donation/types.d.ts +0 -224
  256. package/dist/donation/types.js +0 -40
  257. package/dist/donation/types.js.map +0 -1
  258. package/dist/installer/identity.js.map +0 -1
  259. package/dist/owner/index.d.ts +0 -11
  260. package/dist/owner/index.js +0 -11
  261. package/dist/owner/index.js.map +0 -1
  262. package/dist/owner/owner-node-client.d.ts +0 -110
  263. package/dist/owner/owner-node-client.js +0 -184
  264. package/dist/owner/owner-node-client.js.map +0 -1
  265. package/dist/server/routes/bootstrap-node-validation.d.ts +0 -115
  266. package/dist/server/routes/bootstrap-node-validation.js +0 -197
  267. package/dist/server/routes/bootstrap-node-validation.js.map +0 -1
  268. package/dist/server/routes/grants-admin.d.ts +0 -29
  269. package/dist/server/routes/grants-admin.js +0 -58
  270. package/dist/server/routes/grants-admin.js.map +0 -1
  271. package/dist/server/routes/grants.d.ts +0 -28
  272. package/dist/server/routes/grants.js +0 -165
  273. package/dist/server/routes/grants.js.map +0 -1
  274. package/dist/server/routes/provision-request-validation.d.ts +0 -45
  275. package/dist/server/routes/provision-request-validation.js +0 -97
  276. package/dist/server/routes/provision-request-validation.js.map +0 -1
  277. package/dist/server/routes/strands.d.ts +0 -20
  278. package/dist/server/routes/strands.js +0 -38
  279. package/dist/server/routes/strands.js.map +0 -1
  280. package/dist/server/routes/trust-circle.d.ts +0 -17
  281. package/dist/server/routes/trust-circle.js +0 -35
  282. package/dist/server/routes/trust-circle.js.map +0 -1
  283. package/dist/strands/index.d.ts +0 -7
  284. package/dist/strands/index.js +0 -6
  285. package/dist/strands/index.js.map +0 -1
  286. package/dist/strands/strand-service.d.ts +0 -50
  287. package/dist/strands/strand-service.js +0 -103
  288. package/dist/strands/strand-service.js.map +0 -1
  289. package/dist/strands/types.d.ts +0 -93
  290. package/dist/strands/types.js +0 -23
  291. package/dist/strands/types.js.map +0 -1
  292. package/dist/ui/assets/index-C6nELNfC.js +0 -23
  293. package/dist/ui/assets/index-C6nELNfC.js.map +0 -1
  294. package/dist/ui/assets/index-TtIYSeZO.css +0 -1
  295. package/src/__tests__/cli-invite.smoke.test.ts +0 -192
  296. package/src/__tests__/host-owner.smoke.test.ts +0 -123
  297. package/src/__tests__/orchestrator-owner.test.ts +0 -234
  298. package/src/__tests__/orchestrator-pin-keys.test.ts +0 -152
  299. package/src/auth/__tests__/duration.test.ts +0 -28
  300. package/src/auth/__tests__/self-label.test.ts +0 -85
  301. package/src/auth/__tests__/trust-circle-integration.test.ts +0 -152
  302. package/src/auth/__tests__/trust-circle-store.test.ts +0 -115
  303. package/src/auth/__tests__/trust-circle.test.ts +0 -445
  304. package/src/auth/duration.ts +0 -33
  305. package/src/auth/index.ts +0 -21
  306. package/src/auth/self-label.ts +0 -74
  307. package/src/auth/trust-circle-store.ts +0 -155
  308. package/src/auth/trust-circle.ts +0 -395
  309. package/src/auth/types.ts +0 -99
  310. package/src/donation/__tests__/donation-service.test.ts +0 -1193
  311. package/src/donation/__tests__/donation-supervisor.test.ts +0 -548
  312. package/src/donation/__tests__/grant-service.test.ts +0 -213
  313. package/src/donation/__tests__/grant-store.test.ts +0 -113
  314. package/src/donation/donation-service.ts +0 -1033
  315. package/src/donation/donation-store.ts +0 -176
  316. package/src/donation/donation-supervisor.ts +0 -411
  317. package/src/donation/grant-service.ts +0 -252
  318. package/src/donation/grant-store.ts +0 -152
  319. package/src/donation/index.ts +0 -65
  320. package/src/donation/types.ts +0 -260
  321. package/src/owner/__tests__/owner-node-client.test.ts +0 -194
  322. package/src/owner/index.ts +0 -12
  323. package/src/owner/owner-node-client.ts +0 -259
  324. package/src/server/__tests__/bootstrap-node-validation.test.ts +0 -138
  325. package/src/server/__tests__/grants-admin-route.test.ts +0 -171
  326. package/src/server/__tests__/grants-route.test.ts +0 -394
  327. package/src/server/__tests__/strands-route.test.ts +0 -234
  328. package/src/server/routes/bootstrap-node-validation.ts +0 -206
  329. package/src/server/routes/grants-admin.ts +0 -74
  330. package/src/server/routes/grants.ts +0 -193
  331. package/src/server/routes/provision-request-validation.ts +0 -105
  332. package/src/server/routes/strands.ts +0 -54
  333. package/src/server/routes/trust-circle.ts +0 -49
  334. package/src/strands/__tests__/strand-service.test.ts +0 -165
  335. package/src/strands/index.ts +0 -14
  336. package/src/strands/strand-service.ts +0 -135
  337. package/src/strands/types.ts +0 -102
@@ -1,34 +1,143 @@
1
- import type { DirectReachability, PortForwardMode } from './types.js';
1
+ /**
2
+ * Reachability verdicts: one per hosted node, and the host-level roll-up the
3
+ * dashboard badge shows. Heuristics over the mapping table, not a dial-back —
4
+ * libp2p AutoNAT is a later ticket.
5
+ */
6
+
7
+ import type {
8
+ DirectReachability,
9
+ NodeReachability,
10
+ NodeVerdict,
11
+ PortKind,
12
+ PortRoute,
13
+ } from './types.js';
2
14
 
3
- export interface ReachabilityInput {
4
- portMode: PortForwardMode;
15
+ export interface NodeVerdictInput {
16
+ tcp: PortRoute;
17
+ /** Null for a handle persisted by an older build without a WebSocket port. */
18
+ ws: PortRoute | null;
19
+ /** Whether a public host part exists: a DDNS hostname, or a public IPv4. */
20
+ hostKnown: boolean;
5
21
  cgnatDetected: boolean;
22
+ upnpEnabled: boolean;
23
+ gatewayFound: boolean;
24
+ /** The address a user forwards to; null when no gateway was found. */
25
+ lanAddress: string | null;
26
+ }
27
+
28
+ export interface NodeVerdictResult {
29
+ verdict: NodeVerdict;
30
+ reason: string | null;
6
31
  }
7
32
 
8
33
  /**
9
- * v1 reachability heuristic.
10
- *
11
- * Returns:
12
- * - 'cgnat' when CGNAT was detected (highest signal — UPnP succeeded but our
13
- * external IP isn't actually reachable from the open internet).
14
- * - 'unreachable' when port mapping failed.
15
- * - 'reachable' when an auto mapping succeeded.
16
- * - 'unknown' otherwise (manual config, disabled UPnP, etc).
17
- *
18
- * A future ticket will replace this with libp2p AutoNAT or a sereus-owned
19
- * dial-back probe (see the follow-up section of `6.3-cadre-host-nat`).
34
+ * `unreachable` when either port has no usable route or the host part is
35
+ * unknown; otherwise `manual` when either port is forwarded by hand, else
36
+ * `mapped`. A UPnP route under CGNAT is not usable: the router's mapping is on
37
+ * a carrier-private address.
20
38
  */
21
- export function evaluateReachability(input: ReachabilityInput): DirectReachability {
39
+ export function evaluateNodeReachability(input: NodeVerdictInput): NodeVerdictResult {
40
+ const ports = portRoutes(input);
41
+ const failing = ports.filter(({ route }) => !usable(route, input.cgnatDetected));
42
+ if (!input.hostKnown) {
43
+ return { verdict: 'unreachable', reason: HOST_UNKNOWN_REASON };
44
+ }
45
+ if (failing.length > 0) {
46
+ return { verdict: 'unreachable', reason: describeFailures(failing, input) };
47
+ }
48
+ const manual = ports.some(({ route }) => route.source === 'manual');
49
+ return { verdict: manual ? 'manual' : 'mapped', reason: null };
50
+ }
51
+
52
+ /**
53
+ * Host roll-up: `cgnat` when CGNAT is detected and no node is reachable
54
+ * through a manual route; `unknown` with no running nodes; `reachable` when
55
+ * every running node is `mapped`/`manual`; else `unreachable`.
56
+ */
57
+ export function evaluateHostReachability(
58
+ nodes: ReadonlyArray<Pick<NodeReachability, 'running' | 'verdict'>>,
59
+ cgnatDetected: boolean,
60
+ ): DirectReachability {
61
+ const running = nodes.filter((n) => n.running);
62
+ if (cgnatDetected && !running.some((n) => n.verdict === 'manual')) return 'cgnat';
63
+ if (running.length === 0) return 'unknown';
64
+ return running.every((n) => n.verdict !== 'unreachable') ? 'reachable' : 'unreachable';
65
+ }
66
+
67
+ const HOST_UNKNOWN_REASON =
68
+ "This host's public address is unknown: no dynamic DNS hostname is set and no public IPv4 address was detected. " +
69
+ 'Set a DDNS hostname, or check that this machine can reach the internet.';
70
+
71
+ interface PortEntry {
72
+ kind: PortKind;
73
+ route: PortRoute;
74
+ }
75
+
76
+ function portRoutes(input: NodeVerdictInput): PortEntry[] {
77
+ const out: PortEntry[] = [{ kind: 'tcp', route: input.tcp }];
78
+ if (input.ws) out.push({ kind: 'ws', route: input.ws });
79
+ return out;
80
+ }
81
+
82
+ function usable(route: PortRoute, cgnatDetected: boolean): boolean {
83
+ if (route.externalPort === null || route.source === null) return false;
84
+ return !(cgnatDetected && route.source === 'upnp');
85
+ }
86
+
87
+ type FailureCause = 'cgnat' | 'refused' | 'upnp_off' | 'no_gateway' | 'pending';
88
+
89
+ /** Behind CGNAT no router-side remedy helps, so CGNAT outranks whatever else failed the port. */
90
+ function causeOf(route: PortRoute, input: NodeVerdictInput): FailureCause {
22
91
  if (input.cgnatDetected) return 'cgnat';
23
- switch (input.portMode) {
24
- case 'auto-upnp':
25
- case 'auto-natpmp':
26
- return 'reachable';
27
- case 'failed':
28
- return 'unreachable';
29
- case 'manual':
30
- case 'disabled':
31
- default:
32
- return 'unknown';
92
+ if (route.error) return 'refused';
93
+ if (!input.upnpEnabled) return 'upnp_off';
94
+ if (!input.gatewayFound) return 'no_gateway';
95
+ return 'pending';
96
+ }
97
+
98
+ /** One sentence per distinct cause, each naming every port that failed for it. */
99
+ function describeFailures(failing: PortEntry[], input: NodeVerdictInput): string {
100
+ const byCause = new Map<FailureCause, PortEntry[]>();
101
+ for (const entry of failing) {
102
+ const cause = causeOf(entry.route, input);
103
+ byCause.set(cause, [...(byCause.get(cause) ?? []), entry]);
104
+ }
105
+ return [...byCause.entries()].map(([cause, entries]) => describeCause(cause, entries, input)).join(' ');
106
+ }
107
+
108
+ function describeCause(cause: FailureCause, entries: PortEntry[], input: NodeVerdictInput): string {
109
+ const names = entries.map(({ kind, route }) => `${kindLabel(kind)} port ${route.internalPort}`);
110
+ const lan = input.lanAddress ?? "this machine's LAN address";
111
+ const forwardHint = `Forward ${entries.length === 1 ? 'port' : 'ports'} ${listPorts(entries)} to ${lan} on your router, then tell cadre-host the external ${entries.length === 1 ? 'port' : 'ports'}.`;
112
+ switch (cause) {
113
+ case 'cgnat':
114
+ return `This host is behind carrier-grade NAT, so the ${joinNames(names)} cannot be reached from outside through your router, and neither UPnP nor a port forward on your router will help; a relay is needed.`;
115
+ case 'refused': {
116
+ const errors = entries.map(({ route }) => route.error).filter((e): e is string => !!e);
117
+ return `Router refused the ${joinNames(names)} mapping (${errors.join('; ')}). ${forwardHint}`;
118
+ }
119
+ case 'upnp_off':
120
+ return `UPnP is off, so the ${joinNames(names)} ${entries.length === 1 ? 'is' : 'are'} not mapped. Turn UPnP on, or: ${lowerFirst(forwardHint)}`;
121
+ case 'no_gateway':
122
+ return `No UPnP router answered, so the ${joinNames(names)} ${entries.length === 1 ? 'is' : 'are'} not mapped. Enable UPnP on your router, or: ${lowerFirst(forwardHint)}`;
123
+ case 'pending':
124
+ return `The ${joinNames(names)} ${entries.length === 1 ? 'is' : 'are'} not mapped yet; the router is being asked.`;
33
125
  }
34
126
  }
127
+
128
+ function kindLabel(kind: PortKind): string {
129
+ return kind === 'tcp' ? 'TCP' : 'WebSocket';
130
+ }
131
+
132
+ function listPorts(entries: PortEntry[]): string {
133
+ return joinNames(entries.map(({ route }) => String(route.internalPort)));
134
+ }
135
+
136
+ function joinNames(names: string[]): string {
137
+ if (names.length <= 1) return names[0] ?? '';
138
+ return `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`;
139
+ }
140
+
141
+ function lowerFirst(s: string): string {
142
+ return s.charAt(0).toLowerCase() + s.slice(1);
143
+ }
package/src/nat/types.ts CHANGED
@@ -1,159 +1,209 @@
1
- /**
2
- * NAT/DDNS types for cadre-host.
3
- *
4
- * cadre-host runs on home/SMB machines that are typically behind NAT. To be
5
- * dialable from the open internet the host must:
6
- * 1. Punch a port forward on the upstream router (UPnP/NAT-PMP).
7
- * 2. Detect its external IP (and whether it's behind CGNAT).
8
- * 3. Optionally publish a stable hostname via dynamic DNS.
9
- *
10
- * State for these three concerns lives in `<rootDir>/nat.json` (user settings)
11
- * and is exposed as a single `NatStatusSnapshot` to the management UI.
12
- *
13
- * Credentials for DDNS providers are NOT stored in `nat.json`. They go through
14
- * the SecretsStore (keytar, with a 0600 file fallback).
15
- */
16
-
17
- /** Port-forwarding mode currently in effect. */
18
- export type PortForwardMode =
19
- | 'auto-upnp'
20
- | 'auto-natpmp'
21
- | 'manual'
22
- | 'failed'
23
- | 'disabled';
24
-
25
- /** Reachability verdict — best-effort heuristic until AutoNAT lands. */
26
- export type DirectReachability = 'reachable' | 'unreachable' | 'unknown' | 'cgnat';
27
-
28
- /** Snapshot returned to the management UI / CLI. */
29
- export interface NatStatusSnapshot {
30
- // Port forwarding
31
- portMode: PortForwardMode;
32
- externalPort: number;
33
- internalPort: number;
34
- /** IP the router reports as its WAN address. Null when not auto-mapped. */
35
- routerExternalIp: string | null;
36
- /** ISO timestamp when the current mapping lease expires. */
37
- mappingLeaseExpiresAt: string | null;
38
-
39
- // External IP (from public probe; may differ from routerExternalIp under CGNAT)
40
- externalIp: string | null;
41
- externalIpDetectedAt: string | null;
42
- cgnatDetected: boolean;
43
-
44
- // Reachability verdict
45
- directReachability: DirectReachability;
46
- lastTestedAt: string | null;
47
-
48
- // DDNS
49
- ddns: NatDdnsStatus;
50
- }
51
-
52
- /** DDNS sub-section of the status snapshot. */
53
- export interface NatDdnsStatus {
54
- /** Provider ID (e.g. "duckdns"), null when no DDNS is configured. */
55
- providerId: string | null;
56
- /** Configured hostname (e.g. "foo.duckdns.org"), null when unconfigured. */
57
- hostname: string | null;
58
- /** True when the user opted out of cadre-host updating the record. */
59
- externallyManaged: boolean;
60
- /** Last attempt timestamp (ISO), null until first attempt. */
61
- lastUpdateAt: string | null;
62
- /** Whether the most recent attempt succeeded. */
63
- lastUpdateOk: boolean | null;
64
- /** Most recent error message, null when last attempt succeeded. */
65
- lastError: string | null;
66
- }
67
-
68
- /**
69
- * On-disk shape of `nat.json`. NEVER contains DDNS credentials — those live
70
- * in the SecretsStore.
71
- */
72
- export interface NatSettingsFile {
73
- version: 1;
74
- /** External port to map (and announce). Default 4001. */
75
- externalPort: number;
76
- /** Internal libp2p port. Default 4001. */
77
- internalPort: number;
78
- /** Whether to attempt UPnP/NAT-PMP at all. */
79
- upnpEnabled: boolean;
80
- ddns: NatDdnsSettings;
81
- }
82
-
83
- /** DDNS sub-section of `nat.json`. */
84
- export interface NatDdnsSettings {
85
- /** Provider ID (e.g. "duckdns"). Null = unconfigured. */
86
- providerId: string | null;
87
- /** Hostname to publish. Null = unconfigured. */
88
- hostname: string | null;
89
- /**
90
- * True means the user maintains the DNS record themselves (e.g. via the
91
- * provider's own auto-update agent on the router). cadre-host shows the
92
- * hostname but never tries to update it.
93
- */
94
- externallyManaged: boolean;
95
- /** DDNS update interval in milliseconds. Default 5 min. */
96
- intervalMs: number;
97
- }
98
-
99
- /** Typed error codes for NatError. */
100
- export type NatErrorCode =
101
- | 'mapping_failed'
102
- | 'router_unreachable'
103
- | 'ip_detection_failed'
104
- | 'ddns_provider_unknown'
105
- | 'ddns_credentials_missing'
106
- | 'ddns_update_failed'
107
- | 'secrets_unavailable'
108
- | 'storage_error'
109
- | 'invalid_config'
110
- /** The owner node's admin channel is unreachable / not ready. */
111
- | 'node_unavailable';
112
-
113
- /** Typed error carrying a stable `code` for HTTP mapping in the local-ui. */
114
- export class NatError extends Error {
115
- readonly code: NatErrorCode;
116
-
117
- constructor(code: NatErrorCode, message: string) {
118
- super(message);
119
- this.name = 'NatError';
120
- this.code = code;
121
- }
122
- }
123
-
124
- /** Description of a configurable field on a DDNS provider. */
125
- export interface DdnsConfigField {
126
- /** Field key (e.g. "token"). */
127
- key: string;
128
- /** Human-readable label (e.g. "DuckDNS token"). */
129
- label: string;
130
- /** True if the value is a credential that must be stored in the SecretsStore. */
131
- secret: boolean;
132
- }
133
-
134
- /** Summary of a DDNS provider for UI / CLI listing. */
135
- export interface DdnsProviderInfo {
136
- id: string;
137
- displayName: string;
138
- configFields: ReadonlyArray<DdnsConfigField>;
139
- }
140
-
141
- /**
142
- * Typed HTTP handlers exposed to `cadre-host-local-ui` for Fastify wiring.
143
- *
144
- * Like TrustCircleHandlers, these take typed objects and either return typed
145
- * results or throw a `NatError` whose `.code` the UI ticket maps to an HTTP
146
- * status code.
147
- */
148
- export interface NatHandlers {
149
- getStatus(): Promise<NatStatusSnapshot>;
150
- testReachability(): Promise<NatStatusSnapshot>;
151
- listDdnsProviders(): Promise<DdnsProviderInfo[]>;
152
- putDdns(body: {
153
- providerId: string;
154
- hostname: string;
155
- config: Record<string, string>;
156
- externallyManaged?: boolean;
157
- }): Promise<NatStatusSnapshot>;
158
- putSettings(body: Partial<Omit<NatSettingsFile, 'version'>>): Promise<NatStatusSnapshot>;
159
- }
1
+ /**
2
+ * NAT/DDNS types for cadre-host.
3
+ *
4
+ * cadre-host runs on home/SMB machines that are typically behind NAT. For the
5
+ * nodes it runs to be dialable from the open internet the host must:
6
+ * 1. Map every hosted node's two libp2p ports (TCP, and the WebSocket port a
7
+ * phone dials) through the upstream router, or let the user forward them
8
+ * by hand.
9
+ * 2. Detect its external IP (and whether it's behind CGNAT).
10
+ * 3. Optionally publish a stable hostname via dynamic DNS.
11
+ *
12
+ * State for these concerns lives in `<rootDir>/nat.json` (user settings) and is
13
+ * exposed as a single `NatStatusSnapshot` to the management UI, with one
14
+ * `NodeReachability` entry per hosted node.
15
+ *
16
+ * Credentials for DDNS providers are NOT stored in `nat.json`. They go through
17
+ * the SecretsStore (keytar, with a 0600 file fallback).
18
+ */
19
+
20
+ /** Reachability roll-up across the host's running nodes — see `reachability.ts`. */
21
+ export type DirectReachability = 'reachable' | 'unreachable' | 'unknown' | 'cgnat';
22
+
23
+ /** The two ports of a hosted node that are mapped: libp2p TCP and libp2p WebSocket. */
24
+ export type PortKind = 'tcp' | 'ws';
25
+
26
+ /** Where a port's external route came from. */
27
+ export type PortRouteSource = 'upnp' | 'manual';
28
+
29
+ /** How one of a node's ports is reached from outside the home network. */
30
+ export interface PortRoute {
31
+ internalPort: number;
32
+ /** Port reachable from outside; null when there is no route. */
33
+ externalPort: number | null;
34
+ source: PortRouteSource | null;
35
+ /** ISO timestamp when the router lease expires; UPnP routes only. */
36
+ leaseExpiresAt: string | null;
37
+ /** Last mapping failure, in plain language; null when the last attempt succeeded. */
38
+ error: string | null;
39
+ }
40
+
41
+ /** Per-node verdict. */
42
+ export type NodeVerdict = 'mapped' | 'manual' | 'unreachable';
43
+
44
+ /** Reachability of one hosted node, as reported in the status snapshot. */
45
+ export interface NodeReachability {
46
+ nodeId: string;
47
+ /** Whether the node's process is running; a stopped node keeps its last routes until the unmap grace elapses. */
48
+ running: boolean;
49
+ verdict: NodeVerdict;
50
+ /**
51
+ * Plain-language reason and remedy when unreachable, e.g. "Router refused
52
+ * the WebSocket port mapping. Forward port 10004 to 192.168.1.20 on your
53
+ * router, then tell cadre-host the external port."
54
+ */
55
+ reason: string | null;
56
+ tcp: PortRoute;
57
+ /** Null for a handle persisted by an older build without a WebSocket port. */
58
+ ws: PortRoute | null;
59
+ /** Multiaddrs (no `/p2p/` suffix) through which the node is reachable from outside. */
60
+ publicAddrs: string[];
61
+ }
62
+
63
+ /** The UPnP gateway as the host last saw it. */
64
+ export interface NatGatewayStatus {
65
+ found: boolean;
66
+ /** This machine's address on the router's subnet — the address a user forwards to. */
67
+ lanAddress: string | null;
68
+ /** The WAN address the router reports for itself. */
69
+ routerExternalIp: string | null;
70
+ /** Why discovery or the last gateway operation failed; null when it did not. */
71
+ lastError: string | null;
72
+ }
73
+
74
+ /** Snapshot returned to the management UI / CLI. */
75
+ export interface NatStatusSnapshot {
76
+ upnpEnabled: boolean;
77
+ gateway: NatGatewayStatus;
78
+
79
+ // External IP (from the public probe, falling back to the router's report).
80
+ externalIp: string | null;
81
+ externalIpDetectedAt: string | null;
82
+ cgnatDetected: boolean;
83
+
84
+ /** Roll-up across running nodes; see `reachability.ts`. */
85
+ directReachability: DirectReachability;
86
+ lastTestedAt: string | null;
87
+
88
+ ddns: NatDdnsStatus;
89
+ nodes: NodeReachability[];
90
+ }
91
+
92
+ /** DDNS sub-section of the status snapshot. */
93
+ export interface NatDdnsStatus {
94
+ /** Provider ID (e.g. "duckdns"), null when no DDNS is configured. */
95
+ providerId: string | null;
96
+ /** Configured hostname (e.g. "foo.duckdns.org"), null when unconfigured. */
97
+ hostname: string | null;
98
+ /** True when the user opted out of cadre-host updating the record. */
99
+ externallyManaged: boolean;
100
+ /** Last attempt timestamp (ISO), null until first attempt. */
101
+ lastUpdateAt: string | null;
102
+ /** Whether the most recent attempt succeeded. */
103
+ lastUpdateOk: boolean | null;
104
+ /** Most recent error message, null when last attempt succeeded. */
105
+ lastError: string | null;
106
+ }
107
+
108
+ /** The external ports a user forwarded on their router for one node, per port kind. */
109
+ export interface ManualForward {
110
+ tcp?: number;
111
+ ws?: number;
112
+ }
113
+
114
+ /** Patch applied by `PUT /nat/nodes/:nodeId/forward`: `null` clears that port. */
115
+ export interface ManualForwardPatch {
116
+ tcp?: number | null;
117
+ ws?: number | null;
118
+ }
119
+
120
+ /**
121
+ * On-disk shape of `nat.json`. NEVER contains DDNS credentials — those live
122
+ * in the SecretsStore.
123
+ */
124
+ export interface NatSettingsFile {
125
+ version: 1;
126
+ /** Whether to ask the router for port mappings at all. */
127
+ upnpEnabled: boolean;
128
+ /** Per node id: the external port the user forwarded on their router, per port kind. */
129
+ forwards: Record<string, ManualForward>;
130
+ ddns: NatDdnsSettings;
131
+ }
132
+
133
+ /** DDNS sub-section of `nat.json`. */
134
+ export interface NatDdnsSettings {
135
+ /** Provider ID (e.g. "duckdns"). Null = unconfigured. */
136
+ providerId: string | null;
137
+ /** Hostname to publish. Null = unconfigured. */
138
+ hostname: string | null;
139
+ /**
140
+ * True means the user maintains the DNS record themselves (e.g. via the
141
+ * provider's own auto-update agent on the router). cadre-host shows the
142
+ * hostname but never tries to update it.
143
+ */
144
+ externallyManaged: boolean;
145
+ /** DDNS update interval in milliseconds. Default 5 min. */
146
+ intervalMs: number;
147
+ }
148
+
149
+ /** Typed error codes for NatError. */
150
+ export type NatErrorCode =
151
+ | 'mapping_failed'
152
+ | 'router_unreachable'
153
+ | 'ip_detection_failed'
154
+ | 'ddns_provider_unknown'
155
+ | 'ddns_credentials_missing'
156
+ | 'ddns_update_failed'
157
+ | 'secrets_unavailable'
158
+ | 'storage_error'
159
+ | 'invalid_config'
160
+ /** A manual forward named a node id the host does not run. */
161
+ | 'unknown_node';
162
+
163
+ /** Typed error carrying a stable `code` for HTTP mapping in the local-ui. */
164
+ export class NatError extends Error {
165
+ readonly code: NatErrorCode;
166
+
167
+ constructor(code: NatErrorCode, message: string) {
168
+ super(message);
169
+ this.name = 'NatError';
170
+ this.code = code;
171
+ }
172
+ }
173
+
174
+ /** Description of a configurable field on a DDNS provider. */
175
+ export interface DdnsConfigField {
176
+ /** Field key (e.g. "token"). */
177
+ key: string;
178
+ /** Human-readable label (e.g. "DuckDNS token"). */
179
+ label: string;
180
+ /** True if the value is a credential that must be stored in the SecretsStore. */
181
+ secret: boolean;
182
+ }
183
+
184
+ /** Summary of a DDNS provider for UI / CLI listing. */
185
+ export interface DdnsProviderInfo {
186
+ id: string;
187
+ displayName: string;
188
+ configFields: ReadonlyArray<DdnsConfigField>;
189
+ }
190
+
191
+ /**
192
+ * Typed HTTP handlers exposed to the local UI server for Fastify wiring.
193
+ *
194
+ * These take typed objects and either return typed results or throw a
195
+ * `NatError` whose `.code` the error handler maps to an HTTP status code.
196
+ */
197
+ export interface NatHandlers {
198
+ getStatus(): Promise<NatStatusSnapshot>;
199
+ testReachability(): Promise<NatStatusSnapshot>;
200
+ listDdnsProviders(): Promise<DdnsProviderInfo[]>;
201
+ putDdns(body: {
202
+ providerId: string;
203
+ hostname: string;
204
+ config: Record<string, string>;
205
+ externallyManaged?: boolean;
206
+ }): Promise<NatStatusSnapshot>;
207
+ putSettings(body: Partial<Omit<NatSettingsFile, 'version' | 'forwards'>>): Promise<NatStatusSnapshot>;
208
+ putForward(nodeId: string, patch: ManualForwardPatch): Promise<NatStatusSnapshot>;
209
+ }
@@ -1,34 +1,34 @@
1
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
- import { mkdtempSync, rmSync, statSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
-
6
- import { generateIdentity, loadIdentity } from '../identity.js';
7
-
8
- describe('installer identity', () => {
9
- let tmp: string;
10
-
11
- beforeEach(() => {
12
- tmp = mkdtempSync(join(tmpdir(), 'cadre-host-id-'));
13
- });
14
-
15
- afterEach(() => {
16
- rmSync(tmp, { recursive: true, force: true });
17
- });
18
-
19
- it('round-trips an Ed25519 keypair through the file', async () => {
20
- const path = join(tmp, 'identity.key');
21
- const generated = await generateIdentity(path);
22
- expect(generated.peerId).toMatch(/^12D3Koo/);
23
-
24
- const loaded = loadIdentity(path);
25
- expect(loaded.peerId).toBe(generated.peerId);
26
- });
27
-
28
- it.runIf(process.platform !== 'win32')('persists with mode 0600 on POSIX', async () => {
29
- const path = join(tmp, 'identity.key');
30
- await generateIdentity(path);
31
- const mode = statSync(path).mode & 0o777;
32
- expect(mode).toBe(0o600);
33
- });
34
- });
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+ import { mkdtempSync, rmSync, statSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ import { generateIdentity, loadIdentity } from '../identity-file.js';
7
+
8
+ describe('identity file', () => {
9
+ let tmp: string;
10
+
11
+ beforeEach(() => {
12
+ tmp = mkdtempSync(join(tmpdir(), 'cadre-host-id-'));
13
+ });
14
+
15
+ afterEach(() => {
16
+ rmSync(tmp, { recursive: true, force: true });
17
+ });
18
+
19
+ it('round-trips an Ed25519 keypair through the file', async () => {
20
+ const path = join(tmp, 'identity.key');
21
+ const generated = await generateIdentity(path);
22
+ expect(generated.peerId).toMatch(/^12D3Koo/);
23
+
24
+ const loaded = loadIdentity(path);
25
+ expect(loaded.peerId).toBe(generated.peerId);
26
+ });
27
+
28
+ it.runIf(process.platform !== 'win32')('persists with mode 0600 on POSIX', async () => {
29
+ const path = join(tmp, 'identity.key');
30
+ await generateIdentity(path);
31
+ const mode = statSync(path).mode & 0o777;
32
+ expect(mode).toBe(0o600);
33
+ });
34
+ });