@serfab/cadre-host 1.13.0 → 1.15.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 (352) hide show
  1. package/README.md +347 -423
  2. package/dist/bin/host.d.ts +6 -3
  3. package/dist/bin/host.js +332 -524
  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 +54 -0
  9. package/dist/bin/nat-output.js +138 -0
  10. package/dist/bin/nat-output.js.map +1 -0
  11. package/dist/bin/ui-access.d.ts +22 -0
  12. package/dist/bin/ui-access.js +44 -0
  13. package/dist/bin/ui-access.js.map +1 -0
  14. package/dist/hosted/claim-details.d.ts +66 -0
  15. package/dist/hosted/claim-details.js +60 -0
  16. package/dist/hosted/claim-details.js.map +1 -0
  17. package/dist/hosted/hosted-node-service.d.ts +325 -0
  18. package/dist/hosted/hosted-node-service.js +678 -0
  19. package/dist/hosted/hosted-node-service.js.map +1 -0
  20. package/dist/hosted/hosted-node-store.d.ts +40 -0
  21. package/dist/hosted/hosted-node-store.js +126 -0
  22. package/dist/hosted/hosted-node-store.js.map +1 -0
  23. package/dist/{donation/donation-supervisor.d.ts → hosted/hosted-node-supervisor.d.ts} +70 -53
  24. package/dist/hosted/hosted-node-supervisor.js +396 -0
  25. package/dist/hosted/hosted-node-supervisor.js.map +1 -0
  26. package/dist/hosted/hosted-node-watcher.d.ts +95 -0
  27. package/dist/hosted/hosted-node-watcher.js +260 -0
  28. package/dist/hosted/hosted-node-watcher.js.map +1 -0
  29. package/dist/hosted/index.d.ts +16 -0
  30. package/dist/hosted/index.js +13 -0
  31. package/dist/hosted/index.js.map +1 -0
  32. package/dist/hosted/node-status.d.ts +51 -0
  33. package/dist/hosted/node-status.js +77 -0
  34. package/dist/hosted/node-status.js.map +1 -0
  35. package/dist/hosted/types.d.ts +141 -0
  36. package/dist/hosted/types.js +22 -0
  37. package/dist/hosted/types.js.map +1 -0
  38. package/dist/index.d.ts +12 -24
  39. package/dist/index.js +9 -19
  40. package/dist/index.js.map +1 -1
  41. package/dist/installer/config.d.ts +27 -34
  42. package/dist/installer/config.js +46 -15
  43. package/dist/installer/config.js.map +1 -1
  44. package/dist/installer/index.d.ts +9 -19
  45. package/dist/installer/index.js +21 -85
  46. package/dist/installer/index.js.map +1 -1
  47. package/dist/installer/paths.d.ts +0 -2
  48. package/dist/installer/paths.js +0 -4
  49. package/dist/installer/paths.js.map +1 -1
  50. package/dist/installer/wizard.d.ts +12 -9
  51. package/dist/installer/wizard.js +15 -14
  52. package/dist/installer/wizard.js.map +1 -1
  53. package/dist/nat/address-resolver.d.ts +18 -17
  54. package/dist/nat/address-resolver.js +79 -25
  55. package/dist/nat/address-resolver.js.map +1 -1
  56. package/dist/nat/address-watch.d.ts +37 -0
  57. package/dist/nat/address-watch.js +84 -0
  58. package/dist/nat/address-watch.js.map +1 -0
  59. package/dist/nat/ddns/duckdns.d.ts +2 -1
  60. package/dist/nat/ddns/duckdns.js +5 -1
  61. package/dist/nat/ddns/duckdns.js.map +1 -1
  62. package/dist/nat/ddns/index.d.ts +6 -0
  63. package/dist/nat/ddns/index.js.map +1 -1
  64. package/dist/nat/index.d.ts +13 -9
  65. package/dist/nat/index.js +7 -5
  66. package/dist/nat/index.js.map +1 -1
  67. package/dist/nat/nat-service.d.ts +220 -77
  68. package/dist/nat/nat-service.js +653 -239
  69. package/dist/nat/nat-service.js.map +1 -1
  70. package/dist/nat/nat-store.d.ts +12 -5
  71. package/dist/nat/nat-store.js +124 -29
  72. package/dist/nat/nat-store.js.map +1 -1
  73. package/dist/nat/network-mode.d.ts +19 -0
  74. package/dist/nat/network-mode.js +32 -0
  75. package/dist/nat/network-mode.js.map +1 -0
  76. package/dist/nat/port-mapper.d.ts +83 -79
  77. package/dist/nat/port-mapper.js +148 -189
  78. package/dist/nat/port-mapper.js.map +1 -1
  79. package/dist/nat/reachability.d.ts +32 -15
  80. package/dist/nat/reachability.js +110 -22
  81. package/dist/nat/reachability.js.map +1 -1
  82. package/dist/nat/types.d.ts +98 -27
  83. package/dist/nat/types.js +8 -5
  84. package/dist/nat/types.js.map +1 -1
  85. package/dist/orchestrator/host-process-orchestrator.d.ts +32 -74
  86. package/dist/orchestrator/host-process-orchestrator.js +84 -233
  87. package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
  88. package/dist/{installer/identity.d.ts → orchestrator/identity-file.d.ts} +5 -5
  89. package/dist/{installer/identity.js → orchestrator/identity-file.js} +6 -6
  90. package/dist/orchestrator/identity-file.js.map +1 -0
  91. package/dist/orchestrator/index.d.ts +2 -3
  92. package/dist/orchestrator/index.js +1 -1
  93. package/dist/orchestrator/index.js.map +1 -1
  94. package/dist/orchestrator/node-identity.d.ts +6 -6
  95. package/dist/orchestrator/node-identity.js +7 -7
  96. package/dist/orchestrator/node-identity.js.map +1 -1
  97. package/dist/orchestrator/port-allocator.d.ts +5 -6
  98. package/dist/orchestrator/port-allocator.js +9 -11
  99. package/dist/orchestrator/port-allocator.js.map +1 -1
  100. package/dist/orchestrator/state-store.d.ts +3 -5
  101. package/dist/orchestrator/state-store.js +2 -10
  102. package/dist/orchestrator/state-store.js.map +1 -1
  103. package/dist/orchestrator/types.d.ts +33 -22
  104. package/dist/orchestrator/types.js.map +1 -1
  105. package/dist/package-version.d.ts +1 -0
  106. package/dist/package-version.js +4 -0
  107. package/dist/package-version.js.map +1 -0
  108. package/dist/server/error-handler.d.ts +5 -25
  109. package/dist/server/error-handler.js +11 -74
  110. package/dist/server/error-handler.js.map +1 -1
  111. package/dist/server/events/types.d.ts +7 -11
  112. package/dist/server/events/types.js +2 -1
  113. package/dist/server/events/types.js.map +1 -1
  114. package/dist/server/index.d.ts +11 -37
  115. package/dist/server/index.js +30 -48
  116. package/dist/server/index.js.map +1 -1
  117. package/dist/server/routes/hosted-nodes.d.ts +28 -0
  118. package/dist/server/routes/hosted-nodes.js +68 -0
  119. package/dist/server/routes/hosted-nodes.js.map +1 -0
  120. package/dist/server/routes/nat.d.ts +4 -1
  121. package/dist/server/routes/nat.js +13 -2
  122. package/dist/server/routes/nat.js.map +1 -1
  123. package/dist/server/routes/nodes.d.ts +6 -20
  124. package/dist/server/routes/nodes.js +7 -86
  125. package/dist/server/routes/nodes.js.map +1 -1
  126. package/dist/server/routes/settings.d.ts +3 -7
  127. package/dist/server/routes/settings.js +18 -8
  128. package/dist/server/routes/settings.js.map +1 -1
  129. package/dist/server/routes/status.d.ts +2 -25
  130. package/dist/server/routes/status.js +3 -43
  131. package/dist/server/routes/status.js.map +1 -1
  132. package/dist/server/server.js +8 -8
  133. package/dist/server/settings-store.d.ts +2 -2
  134. package/dist/server/settings-store.js +2 -2
  135. package/dist/server/static.js +1 -2
  136. package/dist/server/static.js.map +1 -1
  137. package/dist/ui/assets/index-BHd8CWZA.css +1 -0
  138. package/dist/ui/assets/index-Ccr2GjPT.js +23 -0
  139. package/dist/ui/assets/index-Ccr2GjPT.js.map +1 -0
  140. package/dist/ui/index.html +2 -2
  141. package/dist/update/index.d.ts +2 -2
  142. package/dist/update/index.js +2 -2
  143. package/dist/update/store.d.ts +1 -1
  144. package/dist/update/store.js +1 -1
  145. package/dist/update/types.d.ts +1 -1
  146. package/dist/update/types.js +1 -1
  147. package/package.json +5 -7
  148. package/src/__tests__/cli-nat.smoke.test.ts +219 -212
  149. package/src/__tests__/cli.smoke.test.ts +2 -2
  150. package/src/__tests__/global-setup.ts +2 -3
  151. package/src/__tests__/orchestrator-claim-secret.test.ts +111 -0
  152. package/src/__tests__/orchestrator-env-scrub.test.ts +160 -163
  153. package/src/__tests__/orchestrator-handle-contract.ts +134 -134
  154. package/src/__tests__/orchestrator-node-identity.test.ts +177 -203
  155. package/src/__tests__/orchestrator-ports.test.ts +124 -143
  156. package/src/__tests__/orchestrator-push.test.ts +138 -146
  157. package/src/__tests__/orchestrator.test.ts +1017 -1063
  158. package/src/__tests__/ui-access.test.ts +30 -0
  159. package/src/bin/host.ts +397 -572
  160. package/src/bin/join-output.ts +120 -0
  161. package/src/bin/nat-output.ts +188 -0
  162. package/src/bin/ui-access.ts +50 -0
  163. package/src/{donation → hosted}/__tests__/fake-orchestrator.test.ts +172 -173
  164. package/src/{donation → hosted}/__tests__/fake-orchestrator.ts +43 -23
  165. package/src/hosted/__tests__/hosted-node-service.test.ts +708 -0
  166. package/src/hosted/__tests__/hosted-node-supervisor.test.ts +475 -0
  167. package/src/hosted/__tests__/test-invitation.ts +33 -0
  168. package/src/hosted/claim-details.ts +103 -0
  169. package/src/hosted/hosted-node-service.ts +814 -0
  170. package/src/hosted/hosted-node-store.ts +144 -0
  171. package/src/hosted/hosted-node-supervisor.ts +439 -0
  172. package/src/hosted/hosted-node-watcher.ts +288 -0
  173. package/src/hosted/index.ts +53 -0
  174. package/src/hosted/node-status.ts +97 -0
  175. package/src/hosted/types.ts +160 -0
  176. package/src/index.ts +72 -91
  177. package/src/installer/__tests__/config.test.ts +15 -40
  178. package/src/installer/__tests__/installer.smoke.test.ts +184 -263
  179. package/src/installer/__tests__/wizard.test.ts +25 -13
  180. package/src/installer/config.ts +201 -175
  181. package/src/installer/index.ts +252 -327
  182. package/src/installer/paths.ts +104 -109
  183. package/src/installer/wizard.ts +24 -26
  184. package/src/nat/__tests__/address-resolver.test.ts +107 -82
  185. package/src/nat/__tests__/nat-service.test.ts +608 -515
  186. package/src/nat/__tests__/nat-store.test.ts +49 -12
  187. package/src/nat/__tests__/network-mode.test.ts +38 -0
  188. package/src/nat/__tests__/port-mapper.test.ts +30 -191
  189. package/src/nat/__tests__/reachability.test.ts +96 -13
  190. package/src/nat/address-resolver.ts +94 -52
  191. package/src/nat/address-watch.ts +92 -0
  192. package/src/nat/ddns/duckdns.ts +5 -1
  193. package/src/nat/ddns/index.ts +6 -0
  194. package/src/nat/index.ts +33 -13
  195. package/src/nat/nat-service.ts +942 -501
  196. package/src/nat/nat-store.ts +126 -31
  197. package/src/nat/network-mode.ts +36 -0
  198. package/src/nat/port-mapper.ts +241 -329
  199. package/src/nat/reachability.ts +155 -34
  200. package/src/nat/types.ts +110 -37
  201. package/src/{installer/__tests__/identity.test.ts → orchestrator/__tests__/identity-file.test.ts} +34 -34
  202. package/src/orchestrator/__tests__/node-identity.test.ts +62 -62
  203. package/src/orchestrator/host-process-orchestrator.ts +1089 -1260
  204. package/src/{installer/identity.ts → orchestrator/identity-file.ts} +56 -56
  205. package/src/orchestrator/index.ts +9 -10
  206. package/src/orchestrator/node-identity.ts +41 -41
  207. package/src/orchestrator/port-allocator.ts +71 -73
  208. package/src/orchestrator/state-store.ts +76 -90
  209. package/src/orchestrator/types.ts +34 -23
  210. package/src/package-version.ts +4 -0
  211. package/src/server/__tests__/bus.test.ts +38 -38
  212. package/src/server/__tests__/error-handler.test.ts +81 -118
  213. package/src/server/__tests__/fakes.ts +15 -18
  214. package/src/server/__tests__/hosted-nodes-route.test.ts +161 -0
  215. package/src/server/__tests__/nodes-route.test.ts +114 -283
  216. package/src/server/__tests__/publishers.test.ts +193 -180
  217. package/src/server/__tests__/server.smoke.test.ts +134 -223
  218. package/src/server/__tests__/settings-route.test.ts +210 -195
  219. package/src/server/__tests__/sse-route.test.ts +119 -119
  220. package/src/server/__tests__/status-route.test.ts +82 -121
  221. package/src/server/error-handler.ts +112 -178
  222. package/src/server/events/types.ts +20 -24
  223. package/src/server/index.ts +223 -267
  224. package/src/server/routes/hosted-nodes.ts +83 -0
  225. package/src/server/routes/nat.ts +75 -63
  226. package/src/server/routes/nodes.ts +96 -188
  227. package/src/server/routes/settings.ts +20 -15
  228. package/src/server/routes/status.ts +5 -67
  229. package/src/server/server.ts +8 -8
  230. package/src/server/settings-store.ts +2 -2
  231. package/src/server/static.ts +1 -2
  232. package/src/update/index.ts +2 -2
  233. package/src/update/store.ts +1 -1
  234. package/src/update/types.ts +1 -1
  235. package/dist/auth/duration.d.ts +0 -1
  236. package/dist/auth/duration.js +0 -32
  237. package/dist/auth/duration.js.map +0 -1
  238. package/dist/auth/index.d.ts +0 -11
  239. package/dist/auth/index.js +0 -9
  240. package/dist/auth/index.js.map +0 -1
  241. package/dist/auth/self-label.d.ts +0 -36
  242. package/dist/auth/self-label.js +0 -57
  243. package/dist/auth/self-label.js.map +0 -1
  244. package/dist/auth/trust-circle-store.d.ts +0 -26
  245. package/dist/auth/trust-circle-store.js +0 -129
  246. package/dist/auth/trust-circle-store.js.map +0 -1
  247. package/dist/auth/trust-circle.d.ts +0 -156
  248. package/dist/auth/trust-circle.js +0 -307
  249. package/dist/auth/trust-circle.js.map +0 -1
  250. package/dist/auth/types.d.ts +0 -84
  251. package/dist/auth/types.js +0 -24
  252. package/dist/auth/types.js.map +0 -1
  253. package/dist/donation/donation-service.d.ts +0 -389
  254. package/dist/donation/donation-service.js +0 -828
  255. package/dist/donation/donation-service.js.map +0 -1
  256. package/dist/donation/donation-store.d.ts +0 -51
  257. package/dist/donation/donation-store.js +0 -152
  258. package/dist/donation/donation-store.js.map +0 -1
  259. package/dist/donation/donation-supervisor.js +0 -364
  260. package/dist/donation/donation-supervisor.js.map +0 -1
  261. package/dist/donation/grant-service.d.ts +0 -80
  262. package/dist/donation/grant-service.js +0 -210
  263. package/dist/donation/grant-service.js.map +0 -1
  264. package/dist/donation/grant-store.d.ts +0 -38
  265. package/dist/donation/grant-store.js +0 -135
  266. package/dist/donation/grant-store.js.map +0 -1
  267. package/dist/donation/index.d.ts +0 -20
  268. package/dist/donation/index.js +0 -17
  269. package/dist/donation/index.js.map +0 -1
  270. package/dist/donation/types.d.ts +0 -224
  271. package/dist/donation/types.js +0 -40
  272. package/dist/donation/types.js.map +0 -1
  273. package/dist/installer/identity.js.map +0 -1
  274. package/dist/owner/index.d.ts +0 -11
  275. package/dist/owner/index.js +0 -11
  276. package/dist/owner/index.js.map +0 -1
  277. package/dist/owner/owner-node-client.d.ts +0 -110
  278. package/dist/owner/owner-node-client.js +0 -184
  279. package/dist/owner/owner-node-client.js.map +0 -1
  280. package/dist/server/routes/bootstrap-node-validation.d.ts +0 -115
  281. package/dist/server/routes/bootstrap-node-validation.js +0 -197
  282. package/dist/server/routes/bootstrap-node-validation.js.map +0 -1
  283. package/dist/server/routes/grants-admin.d.ts +0 -29
  284. package/dist/server/routes/grants-admin.js +0 -58
  285. package/dist/server/routes/grants-admin.js.map +0 -1
  286. package/dist/server/routes/grants.d.ts +0 -28
  287. package/dist/server/routes/grants.js +0 -165
  288. package/dist/server/routes/grants.js.map +0 -1
  289. package/dist/server/routes/provision-request-validation.d.ts +0 -45
  290. package/dist/server/routes/provision-request-validation.js +0 -97
  291. package/dist/server/routes/provision-request-validation.js.map +0 -1
  292. package/dist/server/routes/strands.d.ts +0 -20
  293. package/dist/server/routes/strands.js +0 -38
  294. package/dist/server/routes/strands.js.map +0 -1
  295. package/dist/server/routes/trust-circle.d.ts +0 -17
  296. package/dist/server/routes/trust-circle.js +0 -35
  297. package/dist/server/routes/trust-circle.js.map +0 -1
  298. package/dist/strands/index.d.ts +0 -7
  299. package/dist/strands/index.js +0 -6
  300. package/dist/strands/index.js.map +0 -1
  301. package/dist/strands/strand-service.d.ts +0 -50
  302. package/dist/strands/strand-service.js +0 -103
  303. package/dist/strands/strand-service.js.map +0 -1
  304. package/dist/strands/types.d.ts +0 -93
  305. package/dist/strands/types.js +0 -23
  306. package/dist/strands/types.js.map +0 -1
  307. package/dist/ui/assets/index-C6nELNfC.js +0 -23
  308. package/dist/ui/assets/index-C6nELNfC.js.map +0 -1
  309. package/dist/ui/assets/index-TtIYSeZO.css +0 -1
  310. package/src/__tests__/cli-invite.smoke.test.ts +0 -192
  311. package/src/__tests__/host-owner.smoke.test.ts +0 -123
  312. package/src/__tests__/orchestrator-owner.test.ts +0 -234
  313. package/src/__tests__/orchestrator-pin-keys.test.ts +0 -152
  314. package/src/auth/__tests__/duration.test.ts +0 -28
  315. package/src/auth/__tests__/self-label.test.ts +0 -85
  316. package/src/auth/__tests__/trust-circle-integration.test.ts +0 -152
  317. package/src/auth/__tests__/trust-circle-store.test.ts +0 -115
  318. package/src/auth/__tests__/trust-circle.test.ts +0 -445
  319. package/src/auth/duration.ts +0 -33
  320. package/src/auth/index.ts +0 -21
  321. package/src/auth/self-label.ts +0 -74
  322. package/src/auth/trust-circle-store.ts +0 -155
  323. package/src/auth/trust-circle.ts +0 -395
  324. package/src/auth/types.ts +0 -99
  325. package/src/donation/__tests__/donation-service.test.ts +0 -1193
  326. package/src/donation/__tests__/donation-supervisor.test.ts +0 -548
  327. package/src/donation/__tests__/grant-service.test.ts +0 -213
  328. package/src/donation/__tests__/grant-store.test.ts +0 -113
  329. package/src/donation/donation-service.ts +0 -1033
  330. package/src/donation/donation-store.ts +0 -176
  331. package/src/donation/donation-supervisor.ts +0 -411
  332. package/src/donation/grant-service.ts +0 -252
  333. package/src/donation/grant-store.ts +0 -152
  334. package/src/donation/index.ts +0 -65
  335. package/src/donation/types.ts +0 -260
  336. package/src/owner/__tests__/owner-node-client.test.ts +0 -194
  337. package/src/owner/index.ts +0 -12
  338. package/src/owner/owner-node-client.ts +0 -259
  339. package/src/server/__tests__/bootstrap-node-validation.test.ts +0 -138
  340. package/src/server/__tests__/grants-admin-route.test.ts +0 -171
  341. package/src/server/__tests__/grants-route.test.ts +0 -394
  342. package/src/server/__tests__/strands-route.test.ts +0 -234
  343. package/src/server/routes/bootstrap-node-validation.ts +0 -206
  344. package/src/server/routes/grants-admin.ts +0 -74
  345. package/src/server/routes/grants.ts +0 -193
  346. package/src/server/routes/provision-request-validation.ts +0 -105
  347. package/src/server/routes/strands.ts +0 -54
  348. package/src/server/routes/trust-circle.ts +0 -49
  349. package/src/strands/__tests__/strand-service.test.ts +0 -165
  350. package/src/strands/index.ts +0 -14
  351. package/src/strands/strand-service.ts +0 -135
  352. package/src/strands/types.ts +0 -102
@@ -1,329 +1,241 @@
1
- import debug from 'debug';
2
-
3
- import { NatError, type PortForwardMode } from './types.js';
4
-
5
- const log = debug('cadre:host:nat-port-mapper');
6
-
7
- /** Default lease TTL: 1 hour. The router must keep our mapping at least this long. */
8
- export const DEFAULT_LEASE_TTL_MS = 60 * 60 * 1000;
9
-
10
- /** Default refresh cadence: half the TTL, so two refreshes per lease. */
11
- export const DEFAULT_REFRESH_MS = 30 * 60 * 1000;
12
-
13
- /** Result of a successful map(). */
14
- export interface PortMappingResult {
15
- mode: 'auto-upnp' | 'auto-natpmp';
16
- externalIp: string | null;
17
- externalPort: number;
18
- internalPort: number;
19
- protocol: 'tcp' | 'udp';
20
- leaseExpiresAt: Date;
21
- }
22
-
23
- export interface PortMapOptions {
24
- externalPort: number;
25
- internalPort: number;
26
- protocol?: 'tcp' | 'udp';
27
- /** Lease TTL in milliseconds. Defaults to DEFAULT_LEASE_TTL_MS. */
28
- ttlMs?: number;
29
- }
30
-
31
- /**
32
- * Wrapper around the underlying UPnP/NAT-PMP client. Abstract so tests can
33
- * stub the network behavior without spinning up a real router.
34
- */
35
- export interface PortMapper {
36
- /** Attempt to install a port mapping. Throws NatError(mapping_failed) on failure. */
37
- map(opts: PortMapOptions): Promise<PortMappingResult>;
38
- /** Re-query the router; returns true if the mapping is still present. */
39
- verify(externalPort: number, protocol?: 'tcp' | 'udp'): Promise<boolean>;
40
- /** Query the router's WAN/external IP, if exposed. */
41
- externalIp(): Promise<string | null>;
42
- /** Remove a mapping installed by this mapper. */
43
- unmap(externalPort: number, protocol?: 'tcp' | 'udp'): Promise<void>;
44
- /** Release client resources (timers, sockets). Idempotent. */
45
- stop(): Promise<void>;
46
- }
47
-
48
- /** Constructor for the default mapper. Loads `@achingbrain/nat-port-mapper` dynamically. */
49
- export type PortMapperFactory = () => Promise<PortMapper>;
50
-
51
- interface NatPortMapperClient {
52
- externalIp(): Promise<string>;
53
- map(opts: Record<string, unknown>): Promise<unknown>;
54
- unmap(opts: Record<string, unknown>): Promise<void>;
55
- stop(): Promise<void>;
56
- }
57
-
58
- interface NatPortMapperModule {
59
- upnpNat(opts?: Record<string, unknown>): Promise<NatPortMapperClient>;
60
- }
61
-
62
- /**
63
- * Default factory: loads @achingbrain/nat-port-mapper. Wrapped in dynamic
64
- * import so a missing dep at install time doesn't crash the package.
65
- */
66
- export async function createDefaultPortMapper(opts: {
67
- description?: string;
68
- ttlMs?: number;
69
- } = {}): Promise<PortMapper> {
70
- let mod: NatPortMapperModule;
71
- try {
72
- mod = (await import('@achingbrain/nat-port-mapper' as string)) as NatPortMapperModule;
73
- } catch (err) {
74
- throw new NatError(
75
- 'mapping_failed',
76
- `@achingbrain/nat-port-mapper is not installed: ${(err as Error).message}`,
77
- );
78
- }
79
-
80
- let client: NatPortMapperClient;
81
- try {
82
- client = await mod.upnpNat({
83
- description: opts.description ?? 'sereus-cadre-host',
84
- ttl: Math.round((opts.ttlMs ?? DEFAULT_LEASE_TTL_MS) / 1000),
85
- });
86
- } catch (err) {
87
- throw new NatError(
88
- 'mapping_failed',
89
- `upnpNat() failed: ${(err as Error).message}`,
90
- );
91
- }
92
-
93
- return new NatPortMapperAdapter(client, opts.ttlMs ?? DEFAULT_LEASE_TTL_MS);
94
- }
95
-
96
- class NatPortMapperAdapter implements PortMapper {
97
- constructor(
98
- private readonly client: NatPortMapperClient,
99
- private readonly defaultTtlMs: number,
100
- ) {}
101
-
102
- async map(opts: PortMapOptions): Promise<PortMappingResult> {
103
- const ttlMs = opts.ttlMs ?? this.defaultTtlMs;
104
- const protocol = opts.protocol ?? 'tcp';
105
- let externalIp: string | null = null;
106
- try {
107
- externalIp = await this.client.externalIp();
108
- } catch (err) {
109
- log('externalIp() failed: %s', (err as Error).message);
110
- }
111
- try {
112
- await this.client.map({
113
- localPort: opts.internalPort,
114
- externalPort: opts.externalPort,
115
- protocol,
116
- ttl: Math.round(ttlMs / 1000),
117
- });
118
- } catch (err) {
119
- throw new NatError(
120
- 'mapping_failed',
121
- `router refused mapping for port ${opts.externalPort}: ${(err as Error).message}`,
122
- );
123
- }
124
- return {
125
- mode: 'auto-upnp',
126
- externalIp,
127
- externalPort: opts.externalPort,
128
- internalPort: opts.internalPort,
129
- protocol,
130
- leaseExpiresAt: new Date(Date.now() + ttlMs),
131
- };
132
- }
133
-
134
- async verify(externalPort: number, protocol: 'tcp' | 'udp' = 'tcp'): Promise<boolean> {
135
- void externalPort;
136
- void protocol;
137
- // The current library doesn't expose an introspection API. Treat a
138
- // working externalIp() as a proxy for "router still reachable" — refresh
139
- // failures will surface via re-running map().
140
- try {
141
- await this.client.externalIp();
142
- return true;
143
- } catch (err) {
144
- log('verify() failed: %s', (err as Error).message);
145
- return false;
146
- }
147
- }
148
-
149
- async externalIp(): Promise<string | null> {
150
- try {
151
- return await this.client.externalIp();
152
- } catch (err) {
153
- log('externalIp() failed: %s', (err as Error).message);
154
- return null;
155
- }
156
- }
157
-
158
- async unmap(externalPort: number, protocol: 'tcp' | 'udp' = 'tcp'): Promise<void> {
159
- try {
160
- await this.client.unmap({ externalPort, protocol });
161
- } catch (err) {
162
- log('unmap(%d) failed: %s', externalPort, (err as Error).message);
163
- }
164
- }
165
-
166
- async stop(): Promise<void> {
167
- try {
168
- await this.client.stop();
169
- } catch (err) {
170
- log('stop() failed: %s', (err as Error).message);
171
- }
172
- }
173
- }
174
-
175
- /** Public-facing port-mapper service: wraps a PortMapper with refresh + state. */
176
- export interface PortMapperServiceOptions {
177
- mapper: PortMapper;
178
- externalPort: number;
179
- internalPort: number;
180
- protocol?: 'tcp' | 'udp';
181
- ttlMs?: number;
182
- refreshMs?: number;
183
- /** Clock override for tests. */
184
- now?: () => Date;
185
- /** Timer overrides for tests (must return a handle usable by `clear`). */
186
- setTimer?: (fn: () => void, ms: number) => NodeJS.Timeout;
187
- clearTimer?: (h: NodeJS.Timeout) => void;
188
- }
189
-
190
- /** State snapshot returned by `PortMapperService.getStatus()`. */
191
- export interface PortMapperState {
192
- mode: PortForwardMode;
193
- externalPort: number;
194
- internalPort: number;
195
- routerExternalIp: string | null;
196
- leaseExpiresAt: Date | null;
197
- lastError: string | null;
198
- }
199
-
200
- /**
201
- * Long-running orchestrator over a PortMapper. Owns the refresh timer and
202
- * surfaces the latest state for the NatService snapshot.
203
- */
204
- export class PortMapperService {
205
- private readonly mapper: PortMapper;
206
- private readonly externalPort: number;
207
- private readonly internalPort: number;
208
- private readonly protocol: 'tcp' | 'udp';
209
- private readonly ttlMs: number;
210
- private readonly refreshMs: number;
211
- private readonly nowFn: () => Date;
212
- private readonly setTimer: (fn: () => void, ms: number) => NodeJS.Timeout;
213
- private readonly clearTimer: (h: NodeJS.Timeout) => void;
214
-
215
- private state: PortMapperState;
216
- private refreshHandle: NodeJS.Timeout | null = null;
217
- private started = false;
218
-
219
- constructor(opts: PortMapperServiceOptions) {
220
- this.mapper = opts.mapper;
221
- this.externalPort = opts.externalPort;
222
- this.internalPort = opts.internalPort;
223
- this.protocol = opts.protocol ?? 'tcp';
224
- this.ttlMs = opts.ttlMs ?? DEFAULT_LEASE_TTL_MS;
225
- this.refreshMs = opts.refreshMs ?? DEFAULT_REFRESH_MS;
226
- this.nowFn = opts.now ?? (() => new Date());
227
- this.setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
228
- this.clearTimer = opts.clearTimer ?? ((h) => clearTimeout(h));
229
-
230
- this.state = {
231
- mode: 'disabled',
232
- externalPort: this.externalPort,
233
- internalPort: this.internalPort,
234
- routerExternalIp: null,
235
- leaseExpiresAt: null,
236
- lastError: null,
237
- };
238
- }
239
-
240
- getState(): PortMapperState {
241
- return { ...this.state };
242
- }
243
-
244
- /** Install the mapping and schedule periodic refresh. */
245
- async start(): Promise<void> {
246
- if (this.started) return;
247
- this.started = true;
248
- await this.installMapping();
249
- this.scheduleRefresh();
250
- }
251
-
252
- /** Release the mapping and clear timers. Idempotent. */
253
- async stop(): Promise<void> {
254
- if (!this.started) return;
255
- this.started = false;
256
- if (this.refreshHandle) {
257
- this.clearTimer(this.refreshHandle);
258
- this.refreshHandle = null;
259
- }
260
- try {
261
- await this.mapper.unmap(this.externalPort, this.protocol);
262
- } catch (err) {
263
- log('unmap during stop failed: %s', (err as Error).message);
264
- }
265
- try {
266
- await this.mapper.stop();
267
- } catch (err) {
268
- log('mapper.stop() failed: %s', (err as Error).message);
269
- }
270
- this.state = {
271
- ...this.state,
272
- mode: 'disabled',
273
- routerExternalIp: null,
274
- leaseExpiresAt: null,
275
- };
276
- }
277
-
278
- /** Force an immediate verify+refresh cycle (used by testReachability). */
279
- async refresh(): Promise<PortMapperState> {
280
- if (!this.started) return this.getState();
281
- const ok = await this.mapper.verify(this.externalPort, this.protocol).catch(() => false);
282
- if (!ok) {
283
- log('verify() returned false; re-mapping');
284
- await this.installMapping();
285
- return this.getState();
286
- }
287
- // Refresh: re-issue the mapping to extend the lease.
288
- await this.installMapping();
289
- return this.getState();
290
- }
291
-
292
- private scheduleRefresh(): void {
293
- if (this.refreshHandle) this.clearTimer(this.refreshHandle);
294
- this.refreshHandle = this.setTimer(() => {
295
- void this.refresh().catch((err) => log('refresh timer error: %s', (err as Error).message));
296
- // Re-arm after the call to avoid drift.
297
- this.scheduleRefresh();
298
- }, this.refreshMs);
299
- }
300
-
301
- private async installMapping(): Promise<void> {
302
- try {
303
- const r = await this.mapper.map({
304
- externalPort: this.externalPort,
305
- internalPort: this.internalPort,
306
- protocol: this.protocol,
307
- ttlMs: this.ttlMs,
308
- });
309
- this.state = {
310
- mode: r.mode,
311
- externalPort: r.externalPort,
312
- internalPort: r.internalPort,
313
- routerExternalIp: r.externalIp,
314
- leaseExpiresAt: r.leaseExpiresAt,
315
- lastError: null,
316
- };
317
- log('mapped port %d (mode=%s, externalIp=%s)', r.externalPort, r.mode, r.externalIp ?? '?');
318
- } catch (err) {
319
- const message = (err as Error).message;
320
- this.state = {
321
- ...this.state,
322
- mode: 'failed',
323
- leaseExpiresAt: null,
324
- lastError: message,
325
- };
326
- log('mapping failed: %s', message);
327
- }
328
- }
329
- }
1
+ import { BlockList } from 'node:net';
2
+ import { networkInterfaces, type NetworkInterfaceInfo } from 'node:os';
3
+ import debug from 'debug';
4
+ import { upnpNat, type Gateway, type UPnPNAT } from '@achingbrain/nat-port-mapper';
5
+
6
+ import { NatError } from './types.js';
7
+
8
+ const log = debug('cadre:host:nat-port-mapper');
9
+
10
+ /**
11
+ * Default lease TTL: 1 hour. `@achingbrain/nat-port-mapper` takes the TTL in
12
+ * milliseconds and floors the lease it asks the router for at 3600 s, so a
13
+ * shorter value here would still lease an hour.
14
+ */
15
+ export const DEFAULT_LEASE_TTL_MS = 60 * 60 * 1000;
16
+
17
+ /** Default renewal cadence: half the TTL, so two renewals per lease. */
18
+ export const DEFAULT_REFRESH_MS = 30 * 60 * 1000;
19
+
20
+ /** How long to listen for a UPnP gateway's SSDP answer before giving up. */
21
+ export const GATEWAY_DISCOVERY_TIMEOUT_MS = 10_000;
22
+
23
+ /** Bound on one SOAP call to the router (map, unmap, external IP). */
24
+ export const GATEWAY_OPERATION_TIMEOUT_MS = 10_000;
25
+
26
+ /** What `discover()` learned about the router. */
27
+ export interface GatewayInfo {
28
+ /** This machine's address on the router's subnet — the address mappings point at. */
29
+ lanAddress: string;
30
+ /** The router's own LAN address. */
31
+ routerHost: string;
32
+ }
33
+
34
+ export interface PortMapRequest {
35
+ internalPort: number;
36
+ protocol: 'tcp' | 'udp';
37
+ /** Lease TTL in milliseconds. */
38
+ ttlMs: number;
39
+ }
40
+
41
+ /** Result of a successful map(). */
42
+ export interface PortMapResult {
43
+ /** The external port the router granted — may differ from the internal port. */
44
+ externalPort: number;
45
+ leaseExpiresAt: Date;
46
+ }
47
+
48
+ /**
49
+ * Wrapper around the UPnP client. An interface so tests inject a fake instead
50
+ * of a router.
51
+ */
52
+ export interface PortMapper {
53
+ /**
54
+ * Find the router. Resolves null when no gateway answered within the
55
+ * discovery timeout; throws `NatError('router_unreachable')` when one did but
56
+ * cannot be used (no local interface shares its subnet).
57
+ */
58
+ discover(): Promise<GatewayInfo | null>;
59
+ /**
60
+ * Ask the router to map `internalPort` to the same external port. The router
61
+ * may grant another; the returned `externalPort` is the one that counts.
62
+ * Throws `NatError('mapping_failed')` on refusal.
63
+ */
64
+ map(req: PortMapRequest): Promise<PortMapResult>;
65
+ /** Remove a mapping installed by this process. Never throws. */
66
+ unmap(internalPort: number, protocol: 'tcp' | 'udp'): Promise<void>;
67
+ /** The WAN address the router reports, or null when it cannot be asked. */
68
+ externalIp(): Promise<string | null>;
69
+ /** Release client resources. Does not unmap. Idempotent. */
70
+ stop(): Promise<void>;
71
+ }
72
+
73
+ /** Signal that aborts after `ms`; the timer is cleared by `done()` so it never holds the process open. */
74
+ function deadline(ms: number): { signal: AbortSignal; done(): void } {
75
+ const controller = new AbortController();
76
+ const timer = setTimeout(() => controller.abort(new Error(`timed out after ${ms} ms`)), ms);
77
+ timer.unref();
78
+ return { signal: controller.signal, done: () => clearTimeout(timer) };
79
+ }
80
+
81
+ /**
82
+ * Pick the IPv4 address of the local interface whose subnet contains the router.
83
+ *
84
+ * Not `mapAll`: that maps on every local address and leaves stray mappings for
85
+ * VPN and container interfaces. The address returned is the one a user is told
86
+ * to forward to, so it must be the one on the router's own network.
87
+ */
88
+ export function pickLanAddress(
89
+ routerHost: string,
90
+ interfaces: Record<string, NetworkInterfaceInfo[] | undefined> = networkInterfaces(),
91
+ ): string | null {
92
+ for (const entries of Object.values(interfaces)) {
93
+ for (const entry of entries ?? []) {
94
+ if (entry.family !== 'IPv4' || entry.internal || !entry.cidr) continue;
95
+ const prefix = Number(entry.cidr.split('/')[1]);
96
+ if (!Number.isInteger(prefix)) continue;
97
+ const subnet = new BlockList();
98
+ try {
99
+ subnet.addSubnet(entry.address, prefix, 'ipv4');
100
+ if (subnet.check(routerHost, 'ipv4')) return entry.address;
101
+ } catch (err) {
102
+ log('skipping interface entry %s (%s): %s', entry.address, entry.cidr, (err as Error).message);
103
+ }
104
+ }
105
+ }
106
+ return null;
107
+ }
108
+
109
+ /**
110
+ * UPnP port mapper over `@achingbrain/nat-port-mapper` v4: `upnpNat()` hands
111
+ * out a client that discovers gateways; mapping, unmapping and the external IP
112
+ * live on the discovered `Gateway`.
113
+ *
114
+ * The library's auto-refresh stays off: cadre-host renews leases itself so it
115
+ * can report lease expiry and per-mapping failures.
116
+ */
117
+ export class UpnpPortMapper implements PortMapper {
118
+ private readonly client: UPnPNAT;
119
+ private gateway: Gateway | null = null;
120
+ private lanAddress: string | null = null;
121
+
122
+ constructor(opts: { description?: string } = {}) {
123
+ this.client = upnpNat({
124
+ description: opts.description ?? 'sereus-cadre-host',
125
+ autoRefresh: false,
126
+ });
127
+ }
128
+
129
+ async discover(): Promise<GatewayInfo | null> {
130
+ const gateway = await this.findIpv4Gateway();
131
+ if (!gateway) return null;
132
+ const lanAddress = pickLanAddress(gateway.host);
133
+ if (!lanAddress) {
134
+ throw new NatError(
135
+ 'router_unreachable',
136
+ `found a UPnP router at ${gateway.host} but no local IPv4 interface shares its subnet`,
137
+ );
138
+ }
139
+ this.gateway = gateway;
140
+ this.lanAddress = lanAddress;
141
+ log('discovered gateway %s (%s), mapping from %s', gateway.id, gateway.host, lanAddress);
142
+ return { lanAddress, routerHost: gateway.host };
143
+ }
144
+
145
+ async map(req: PortMapRequest): Promise<PortMapResult> {
146
+ const { gateway, lanAddress } = this.requireGateway();
147
+ const op = deadline(GATEWAY_OPERATION_TIMEOUT_MS);
148
+ try {
149
+ const mapping = await gateway.map(req.internalPort, lanAddress, {
150
+ externalPort: req.internalPort,
151
+ protocol: req.protocol,
152
+ ttl: req.ttlMs,
153
+ autoRefresh: false,
154
+ signal: op.signal,
155
+ });
156
+ return { externalPort: mapping.externalPort, leaseExpiresAt: new Date(Date.now() + req.ttlMs) };
157
+ } catch (err) {
158
+ throw new NatError(
159
+ 'mapping_failed',
160
+ `router refused the mapping for port ${req.internalPort}: ${(err as Error).message}`,
161
+ );
162
+ } finally {
163
+ op.done();
164
+ }
165
+ }
166
+
167
+ // NOTE: the library tracks the mappings this process made and `unmap` only sends a
168
+ // delete for those, so after a host restart a port mapped by the previous process is
169
+ // left to expire with its lease rather than deleted. It also names the INTERNAL port
170
+ // as the external one in that delete, so a mapping the router granted on another
171
+ // external port is not deleted either; it, too, expires with its lease (1 h). And it
172
+ // appends one tracked entry per successful `map` of a port, so after N renewals an
173
+ // unmap sends N deletes (the first is the one that counts); they all run under the
174
+ // one 10 s bound, which caps the cost of a process that has renewed for days.
175
+ async unmap(internalPort: number, _protocol: 'tcp' | 'udp'): Promise<void> {
176
+ if (!this.gateway) return;
177
+ const op = deadline(GATEWAY_OPERATION_TIMEOUT_MS);
178
+ try {
179
+ await this.gateway.unmap(internalPort, { signal: op.signal });
180
+ } catch (err) {
181
+ log('unmap(%d) failed: %s', internalPort, (err as Error).message);
182
+ } finally {
183
+ op.done();
184
+ }
185
+ }
186
+
187
+ async externalIp(): Promise<string | null> {
188
+ if (!this.gateway) return null;
189
+ const op = deadline(GATEWAY_OPERATION_TIMEOUT_MS);
190
+ try {
191
+ return await this.gateway.externalIp({ signal: op.signal });
192
+ } catch (err) {
193
+ log('externalIp() failed: %s', (err as Error).message);
194
+ return null;
195
+ } finally {
196
+ op.done();
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Forget the gateway. Deliberately not `gateway.stop()`: that deletes every
202
+ * mapping this process made, and hosted children keep running across a host
203
+ * restart, so their mappings must outlive this process.
204
+ */
205
+ async stop(): Promise<void> {
206
+ this.gateway = null;
207
+ this.lanAddress = null;
208
+ }
209
+
210
+ private requireGateway(): { gateway: Gateway; lanAddress: string } {
211
+ if (!this.gateway || !this.lanAddress) {
212
+ throw new NatError('router_unreachable', 'no UPnP gateway discovered');
213
+ }
214
+ return { gateway: this.gateway, lanAddress: this.lanAddress };
215
+ }
216
+
217
+ /**
218
+ * First IPv4 gateway to answer the SSDP search, or null when none did within
219
+ * the timeout.
220
+ *
221
+ * NOTE: the library searches for `InternetGatewayDevice:2` only (its
222
+ * `upnp/discovery.js`), and a router that implements only version 1 does not
223
+ * answer a version-2 search, so such a router reads as "not found". There is
224
+ * no library option for it; if it shows up on real hosts, the fix is a fork or
225
+ * another library.
226
+ */
227
+ private async findIpv4Gateway(): Promise<Gateway | null> {
228
+ const search = deadline(GATEWAY_DISCOVERY_TIMEOUT_MS);
229
+ try {
230
+ for await (const gateway of this.client.findGateways({ signal: search.signal })) {
231
+ if (gateway.family === 'IPv4') return gateway;
232
+ }
233
+ return null;
234
+ } catch (err) {
235
+ if (search.signal.aborted) return null;
236
+ throw new NatError('router_unreachable', `UPnP discovery failed: ${(err as Error).message}`);
237
+ } finally {
238
+ search.done();
239
+ }
240
+ }
241
+ }