@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,52 +1,94 @@
1
- /**
2
- * Build the multiaddrs that should be embedded in invites.
3
- *
4
- * Inputs are the host's currently-best understanding of its dialable surface:
5
- * - DDNS hostname (`/dns4/<hostname>/...`) when configured AND reachable.
6
- * - Raw external IP (`/ip4/<ip>/...`) when no DDNS but reachable.
7
- * - Whatever libp2p reports otherwise — which would include a `/p2p-circuit/`
8
- * address once cadre-host passes a relay to its owner node; it passes none
9
- * yet (docs/cadre-host.md → "NAT and DDNS", item 2).
10
- *
11
- * This is intentionally a pure function so it's trivial to unit-test, and so
12
- * the NatService can be the only place that knows about settings → multiaddrs.
13
- */
14
- export interface BuildInviteAddressesInput {
15
- peerId: string;
16
- externalPort: number;
17
- ddnsHostname: string | null;
18
- externalIp: string | null;
19
- reachable: boolean;
20
- /** Multiaddrs reported by the underlying libp2p node. */
21
- libp2pAddrs: string[];
22
- }
23
-
24
- export function buildInviteAddresses(input: BuildInviteAddressesInput): string[] {
25
- const suffix = `/p2p/${input.peerId}`;
26
- // DNS path-of-truth: stable across IP changes.
27
- if (input.reachable && input.ddnsHostname) {
28
- return [`/dns4/${input.ddnsHostname}/tcp/${input.externalPort}${suffix}`];
29
- }
30
- // Raw IPv4: works when we know our external IP.
31
- if (input.reachable && input.externalIp && isIpv4(input.externalIp)) {
32
- return [`/ip4/${input.externalIp}/tcp/${input.externalPort}${suffix}`];
33
- }
34
- // Fallback: whatever libp2p sees. Strip any addresses that lack the
35
- // peer suffix and append it for clarity, but otherwise pass through.
36
- return input.libp2pAddrs.map((a) => withPeerSuffix(a, input.peerId));
37
- }
38
-
39
- function withPeerSuffix(addr: string, peerId: string): string {
40
- if (addr.includes(`/p2p/${peerId}`)) return addr;
41
- if (addr.includes('/p2p/')) return addr; // already has *some* peer
42
- return `${addr}/p2p/${peerId}`;
43
- }
44
-
45
- function isIpv4(s: string): boolean {
46
- const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(s);
47
- if (!m) return false;
48
- return m.slice(1).every((octet) => {
49
- const n = Number(octet);
50
- return n >= 0 && n <= 255;
51
- });
52
- }
1
+ /**
2
+ * Build the public multiaddrs of one hosted node from the host's current
3
+ * understanding of its dialable surface.
4
+ *
5
+ * Pure so it's trivial to unit-test, and so `NatService` is the only place
6
+ * that turns settings and routes into addresses. The addresses carry no
7
+ * `/p2p/` suffix: the node appends its own (cadre-core `normalizeSelfAddrs`,
8
+ * libp2p for announce addresses). The node's own LAN addresses are not here
9
+ * either — libp2p reports those itself.
10
+ */
11
+
12
+ import type { PortRoute } from './types.js';
13
+
14
+ export interface PublicAddressInput {
15
+ ddnsHostname: string | null;
16
+ /** Last known public IPv4; null when unknown. */
17
+ externalIp: string | null;
18
+ cgnatDetected: boolean;
19
+ tcp: PortRoute;
20
+ ws: PortRoute | null;
21
+ }
22
+
23
+ export function buildPublicAddresses(input: PublicAddressInput): string[] {
24
+ const host = publicHost(input);
25
+ if (!host) return [];
26
+ const out: string[] = [];
27
+ const tcpPort = routedPort(input.tcp, input.cgnatDetected);
28
+ if (tcpPort !== null) out.push(`${host}/tcp/${tcpPort}`);
29
+ const wsPort = input.ws ? routedPort(input.ws, input.cgnatDetected) : null;
30
+ if (wsPort !== null) out.push(`${host}/tcp/${wsPort}/ws`);
31
+ return out;
32
+ }
33
+
34
+ /**
35
+ * `/dns4/<hostname>` when a DDNS hostname is configured (externally managed
36
+ * included), else `/ip4/<externalIp>` when that is a public IPv4, else none.
37
+ *
38
+ * A hostname that is not a DNS name is skipped rather than used: these addresses
39
+ * reach each node as `CADRE_APPEND_ANNOUNCE_ADDRS`, a comma-separated list that
40
+ * cadre-core validates at start, so one bad entry (a pasted URL, a comma) would
41
+ * stop every hosted node from starting.
42
+ */
43
+ function publicHost(input: PublicAddressInput): string | null {
44
+ if (input.ddnsHostname && isDnsHostname(input.ddnsHostname)) return `/dns4/${input.ddnsHostname}`;
45
+ if (input.externalIp && isPublicIpv4(input.externalIp)) return `/ip4/${input.externalIp}`;
46
+ return null;
47
+ }
48
+
49
+ /** One RFC 1123 label: letters, digits and inner hyphens, 1–63 characters. */
50
+ const DNS_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;
51
+
52
+ /** True for a hostname of dot-separated RFC 1123 labels, at most 253 characters. */
53
+ export function isDnsHostname(s: string): boolean {
54
+ return s.length <= 253 && s.split('.').every((label) => DNS_LABEL.test(label));
55
+ }
56
+
57
+ /**
58
+ * Under CGNAT a UPnP route produces nothing: the router's mapping is on a
59
+ * carrier-private address. A manual route still does, since the user asserted
60
+ * the forward (the CGNAT check can misfire).
61
+ */
62
+ function routedPort(route: PortRoute, cgnatDetected: boolean): number | null {
63
+ if (route.externalPort === null || route.source === null) return null;
64
+ if (cgnatDetected && route.source === 'upnp') return null;
65
+ return route.externalPort;
66
+ }
67
+
68
+ /** Private, carrier-grade, loopback, link-local, multicast and reserved IPv4 ranges. */
69
+ const NON_PUBLIC_IPV4: ReadonlyArray<[number, number]> = [
70
+ [0x00000000, 8], // 0.0.0.0/8
71
+ [0x0a000000, 8], // 10.0.0.0/8
72
+ [0x64400000, 10], // 100.64.0.0/10 (carrier-grade NAT)
73
+ [0x7f000000, 8], // 127.0.0.0/8
74
+ [0xa9fe0000, 16], // 169.254.0.0/16
75
+ [0xac100000, 12], // 172.16.0.0/12
76
+ [0xc0a80000, 16], // 192.168.0.0/16
77
+ [0xe0000000, 4], // 224.0.0.0/4 (multicast)
78
+ [0xf0000000, 4], // 240.0.0.0/4 (reserved, broadcast)
79
+ ];
80
+
81
+ /** True for a dotted-quad IPv4 outside every non-public range above. */
82
+ export function isPublicIpv4(s: string): boolean {
83
+ const octets = parseIpv4(s);
84
+ if (!octets) return false;
85
+ const value = ((octets[0]! << 24) | (octets[1]! << 16) | (octets[2]! << 8) | octets[3]!) >>> 0;
86
+ return !NON_PUBLIC_IPV4.some(([base, prefix]) => (value >>> (32 - prefix)) === (base >>> (32 - prefix)));
87
+ }
88
+
89
+ function parseIpv4(s: string): number[] | null {
90
+ const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(s);
91
+ if (!m) return null;
92
+ const octets = m.slice(1).map(Number);
93
+ return octets.every((n) => n >= 0 && n <= 255) ? octets : null;
94
+ }
@@ -0,0 +1,92 @@
1
+ import debug from 'debug';
2
+
3
+ import type { ManagedNodeInfo } from '../orchestrator/types.js';
4
+
5
+ const log = debug('cadre:host:nat-address-watch');
6
+
7
+ /**
8
+ * The shortest gap between two address restarts of one node. Bounds a flapping
9
+ * router (a mapping that keeps failing and coming back) to one restart per node
10
+ * per window; a difference found inside the window waits for the first check
11
+ * after it ends.
12
+ */
13
+ export const NAT_ADDRESS_RESTART_MIN_INTERVAL_MS = 10 * 60_000;
14
+
15
+ /** Asked to restart one node so it starts announcing its current public addresses. */
16
+ export type NodeAddressesStaleListener = (nodeId: string) => void | Promise<void>;
17
+
18
+ /**
19
+ * Notices a running node whose announced public addresses differ from the ones
20
+ * it should announce now, and asks its listeners to restart that node. A node
21
+ * learns its public addresses only at start (`CADRE_APPEND_ANNOUNCE_ADDRS`), so
22
+ * a restart is how a changed mapping, forward, DDNS hostname or external IP
23
+ * reaches it.
24
+ */
25
+ export class AddressWatch {
26
+ private readonly listeners = new Set<NodeAddressesStaleListener>();
27
+ /** When each node was last asked to restart (ms since epoch). */
28
+ private readonly lastRestartAt = new Map<string, number>();
29
+
30
+ constructor(private readonly now: () => number) {}
31
+
32
+ /** Register a restart listener. Returns an unsubscribe fn. */
33
+ subscribe(listener: NodeAddressesStaleListener): () => void {
34
+ this.listeners.add(listener);
35
+ return () => { this.listeners.delete(listener); };
36
+ }
37
+
38
+ /**
39
+ * Compare every running node's announced addresses with `expected(node)`, as
40
+ * sets, and ask for a restart of each that differs and was not restarted
41
+ * within {@link NAT_ADDRESS_RESTART_MIN_INTERVAL_MS}.
42
+ */
43
+ check(nodes: ManagedNodeInfo[], expected: (node: ManagedNodeInfo) => string[]): void {
44
+ this.forgetMissing(nodes);
45
+ if (this.listeners.size === 0) return;
46
+ for (const node of nodes) {
47
+ if (node.status !== 'running') continue;
48
+ if (sameAddressSet(node.announcedAddrs, expected(node))) continue;
49
+ if (this.restartedRecently(node.id)) {
50
+ log('node %s announces stale addresses; restart deferred by the rate limit', node.id);
51
+ continue;
52
+ }
53
+ this.lastRestartAt.set(node.id, this.now());
54
+ log('node %s announces stale addresses; asking for a restart', node.id);
55
+ this.notify(node.id);
56
+ }
57
+ }
58
+
59
+ /** A terminated node's id may come back as a fresh node, which owes nothing to the old window. */
60
+ private forgetMissing(nodes: ManagedNodeInfo[]): void {
61
+ const listed = new Set(nodes.map((n) => n.id));
62
+ for (const id of this.lastRestartAt.keys()) {
63
+ if (!listed.has(id)) this.lastRestartAt.delete(id);
64
+ }
65
+ }
66
+
67
+ private restartedRecently(nodeId: string): boolean {
68
+ const last = this.lastRestartAt.get(nodeId);
69
+ return last !== undefined && this.now() - last < NAT_ADDRESS_RESTART_MIN_INTERVAL_MS;
70
+ }
71
+
72
+ /** Fire-and-forget: a restart takes seconds and must not hold up the NAT pass that found it. */
73
+ private notify(nodeId: string): void {
74
+ for (const listener of this.listeners) {
75
+ try {
76
+ void Promise.resolve(listener(nodeId)).catch((err: unknown) => { logRestartFailure(nodeId, err); });
77
+ } catch (err) {
78
+ logRestartFailure(nodeId, err);
79
+ }
80
+ }
81
+ }
82
+ }
83
+
84
+ function sameAddressSet(a: readonly string[], b: readonly string[]): boolean {
85
+ const left = new Set(a);
86
+ const right = new Set(b);
87
+ return left.size === right.size && [...left].every((addr) => right.has(addr));
88
+ }
89
+
90
+ function logRestartFailure(nodeId: string, err: unknown): void {
91
+ log('address restart of node %s failed: %s', nodeId, err instanceof Error ? err.message : String(err));
92
+ }
@@ -13,7 +13,8 @@ const log = debug('cadre:host:nat-ddns:duckdns');
13
13
  * Response is plain text — `OK` on success, `KO` on failure.
14
14
  *
15
15
  * `hostname` may be supplied as the bare subdomain (`foo`) or the FQDN
16
- * (`foo.duckdns.org`); we strip the suffix.
16
+ * (`foo.duckdns.org`); the update strips the suffix, and `qualifyHostname`
17
+ * adds it.
17
18
  */
18
19
  export const duckDnsProvider: DdnsProvider = {
19
20
  id: 'duckdns',
@@ -21,6 +22,9 @@ export const duckDnsProvider: DdnsProvider = {
21
22
  configFields: [
22
23
  { key: 'token', label: 'DuckDNS token', secret: true },
23
24
  ],
25
+ qualifyHostname(hostname: string): string {
26
+ return `${stripSuffix(hostname)}${SUFFIX}`;
27
+ },
24
28
  async update(
25
29
  config: Record<string, string>,
26
30
  hostname: string,
@@ -19,6 +19,12 @@ export interface DdnsProvider {
19
19
  readonly id: string;
20
20
  readonly displayName: string;
21
21
  readonly configFields: ReadonlyArray<DdnsConfigField>;
22
+ /**
23
+ * The fully qualified name the provider publishes for a hostname as the user
24
+ * typed it. Stored in place of the typed form because hosted nodes announce the
25
+ * stored hostname as `/dns4/<hostname>`, so it has to resolve on its own.
26
+ */
27
+ qualifyHostname(hostname: string): string;
22
28
  /** Push `ip` for `hostname`. Throws NatError(ddns_update_failed) on failure. */
23
29
  update(
24
30
  config: Record<string, string>,
package/src/nat/index.ts CHANGED
@@ -1,71 +1,87 @@
1
- /**
2
- * NAT / DDNS substrate for cadre-host.
3
- *
4
- * See ./nat-service.ts for the top-level orchestrator and `docs/cadre-host.md`
5
- * for integration notes (the long-running process owner is `cadre-host-local-ui`).
6
- */
7
-
8
- export { NatService, createNatHandlers } from './nat-service.js';
9
- export type { NatServiceOptions, CadreNodeLike, AddressesChangedListener } from './nat-service.js';
10
- export { NatStore } from './nat-store.js';
11
- export {
12
- NatError,
13
- } from './types.js';
14
- export type {
15
- NatStatusSnapshot,
16
- NatDdnsStatus,
17
- NatSettingsFile,
18
- NatDdnsSettings,
19
- NatHandlers,
20
- NatErrorCode,
21
- PortForwardMode,
22
- DirectReachability,
23
- DdnsProviderInfo,
24
- DdnsConfigField,
25
- } from './types.js';
26
- export {
27
- ExternalIpDetector,
28
- isPlausibleIp,
29
- } from './external-ip.js';
30
- export type {
31
- ExternalIpResult,
32
- RouterIpProbe,
33
- ExternalIpDetectorOptions,
34
- } from './external-ip.js';
35
- export {
36
- PortMapperService,
37
- createDefaultPortMapper,
38
- DEFAULT_LEASE_TTL_MS,
39
- DEFAULT_REFRESH_MS,
40
- } from './port-mapper.js';
41
- export type {
42
- PortMapper,
43
- PortMappingResult,
44
- PortMapOptions,
45
- PortMapperState,
46
- PortMapperServiceOptions,
47
- } from './port-mapper.js';
48
- export { evaluateReachability } from './reachability.js';
49
- export { buildInviteAddresses } from './address-resolver.js';
50
- export type { BuildInviteAddressesInput } from './address-resolver.js';
51
- export {
52
- BUILTIN_PROVIDERS,
53
- getProvider,
54
- listProviders,
55
- duckDnsProvider,
56
- DdnsUpdater,
57
- } from './ddns/index.js';
58
- export type {
59
- DdnsProvider,
60
- DdnsProviderDeps,
61
- DdnsUpdaterOptions,
62
- DdnsUpdateOutcome,
63
- } from './ddns/index.js';
64
- export {
65
- KeytarSecretsStore,
66
- FileSecretsStore,
67
- createSecretsStore,
68
- ddnsAccount,
69
- KEYTAR_SERVICE,
70
- } from './secrets/index.js';
71
- export type { SecretsStore, KeytarLike } from './secrets/index.js';
1
+ /**
2
+ * NAT / DDNS substrate for cadre-host.
3
+ *
4
+ * See ./nat-service.ts for the top-level orchestrator and `docs/cadre-host.md`
5
+ * for integration notes (the long-running process owner is `cadre-host start`).
6
+ */
7
+
8
+ export {
9
+ NatService,
10
+ createNatHandlers,
11
+ NAT_UNMAP_GRACE_MS,
12
+ NAT_RECONCILE_INTERVAL_MS,
13
+ NAT_IP_REDETECT_INTERVAL_MS,
14
+ } from './nat-service.js';
15
+ export type { NatServiceOptions, NatNodeSource, NatChangeListener } from './nat-service.js';
16
+ export { NAT_ADDRESS_RESTART_MIN_INTERVAL_MS } from './address-watch.js';
17
+ export type { NodeAddressesStaleListener } from './address-watch.js';
18
+ export { NatStore } from './nat-store.js';
19
+ export {
20
+ NatError,
21
+ } from './types.js';
22
+ export type {
23
+ NatStatusSnapshot,
24
+ NatGatewayStatus,
25
+ NatDdnsStatus,
26
+ NatSettingsFile,
27
+ NatDdnsSettings,
28
+ NatHandlers,
29
+ NatErrorCode,
30
+ DirectReachability,
31
+ NodeReachability,
32
+ NodeVerdict,
33
+ PortKind,
34
+ PortRoute,
35
+ PortRouteSource,
36
+ ManualForward,
37
+ ManualForwardPatch,
38
+ DdnsProviderInfo,
39
+ DdnsConfigField,
40
+ } from './types.js';
41
+ export {
42
+ ExternalIpDetector,
43
+ isPlausibleIp,
44
+ } from './external-ip.js';
45
+ export type {
46
+ ExternalIpResult,
47
+ RouterIpProbe,
48
+ ExternalIpDetectorOptions,
49
+ } from './external-ip.js';
50
+ export {
51
+ UpnpPortMapper,
52
+ pickLanAddress,
53
+ DEFAULT_LEASE_TTL_MS,
54
+ DEFAULT_REFRESH_MS,
55
+ GATEWAY_DISCOVERY_TIMEOUT_MS,
56
+ } from './port-mapper.js';
57
+ export type {
58
+ PortMapper,
59
+ GatewayInfo,
60
+ PortMapRequest,
61
+ PortMapResult,
62
+ } from './port-mapper.js';
63
+ export { evaluateNodeReachability, evaluateHostReachability } from './reachability.js';
64
+ export type { NodeVerdictInput, NodeVerdictResult } from './reachability.js';
65
+ export { buildPublicAddresses, isPublicIpv4 } from './address-resolver.js';
66
+ export type { PublicAddressInput } from './address-resolver.js';
67
+ export {
68
+ BUILTIN_PROVIDERS,
69
+ getProvider,
70
+ listProviders,
71
+ duckDnsProvider,
72
+ DdnsUpdater,
73
+ } from './ddns/index.js';
74
+ export type {
75
+ DdnsProvider,
76
+ DdnsProviderDeps,
77
+ DdnsUpdaterOptions,
78
+ DdnsUpdateOutcome,
79
+ } from './ddns/index.js';
80
+ export {
81
+ KeytarSecretsStore,
82
+ FileSecretsStore,
83
+ createSecretsStore,
84
+ ddnsAccount,
85
+ KEYTAR_SERVICE,
86
+ } from './secrets/index.js';
87
+ export type { SecretsStore, KeytarLike } from './secrets/index.js';