@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
@@ -2,21 +2,28 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from '
2
2
  import { dirname, join } from 'node:path';
3
3
  import debug from 'debug';
4
4
 
5
- import { NatError, type NatDdnsSettings, type NatSettingsFile } from './types.js';
5
+ import {
6
+ NatError,
7
+ type ManualForward,
8
+ type ManualForwardPatch,
9
+ type NatDdnsSettings,
10
+ type NatSettingsFile,
11
+ type NetworkSetting,
12
+ type PortKind,
13
+ } from './types.js';
14
+ import { isDnsHostname } from './address-resolver.js';
6
15
 
7
16
  const log = debug('cadre:host:nat-store');
8
17
 
9
18
  const FILE_VERSION = 1;
10
19
 
11
- /** Default port (libp2p TCP default). */
12
- const DEFAULT_PORT = 4001;
13
-
14
20
  /** Default DDNS update interval (5 minutes). */
15
21
  const DEFAULT_DDNS_INTERVAL_MS = 5 * 60 * 1000;
16
22
 
23
+ const PORT_KINDS: ReadonlyArray<PortKind> = ['tcp', 'ws'];
24
+
17
25
  /**
18
- * Atomic JSON store for `nat.json`. Mirrors `TrustCircleStore` — write to
19
- * `<path>.tmp`, then rename.
26
+ * Atomic JSON store for `nat.json` — write to `<path>.tmp`, then rename.
20
27
  *
21
28
  * Concurrency assumption: only one cadre-host process owns a given rootDir.
22
29
  */
@@ -58,13 +65,14 @@ export class NatStore {
58
65
  `nat file at ${this.path} is not valid JSON: ${(err as Error).message}`,
59
66
  );
60
67
  }
61
- if (!isNatSettingsFile(parsed)) {
68
+ const settings = fromParsed(parsed);
69
+ if (!settings) {
62
70
  throw new NatError(
63
71
  'storage_error',
64
72
  `nat file at ${this.path} has unexpected shape`,
65
73
  );
66
74
  }
67
- this.cache = parsed;
75
+ this.cache = settings;
68
76
  return this.cache;
69
77
  }
70
78
 
@@ -77,33 +85,69 @@ export class NatStore {
77
85
  writeFileSync(tmp, payload, { encoding: 'utf8' });
78
86
  renameSync(tmp, this.path);
79
87
  this.cache = next;
80
- log('saved nat.json (port=%d, ddns=%s) to %s',
81
- next.externalPort,
88
+ log('saved nat.json (upnp=%s, forwards=%d, ddns=%s) to %s',
89
+ next.upnpEnabled,
90
+ Object.keys(next.forwards).length,
82
91
  next.ddns.providerId ?? '(none)',
83
92
  this.path);
84
93
  }
85
94
 
86
- /** Patch and persist (validation lives here). */
87
- update(patch: Partial<Omit<NatSettingsFile, 'version'>>): NatSettingsFile {
95
+ /** Patch and persist (validation lives here). `forwards` is edited through `setForward`. */
96
+ update(patch: Partial<Omit<NatSettingsFile, 'version' | 'forwards'>>): NatSettingsFile {
88
97
  const current = this.load();
89
98
  const merged: NatSettingsFile = {
90
99
  ...current,
91
100
  ...patch,
92
101
  ddns: { ...current.ddns, ...(patch.ddns ?? {}) },
102
+ forwards: current.forwards,
93
103
  version: FILE_VERSION,
94
104
  };
95
105
  validate(merged);
96
106
  this.save(merged);
97
107
  return merged;
98
108
  }
109
+
110
+ /**
111
+ * Apply a manual-forward patch for one node: a number sets that port's
112
+ * external port, `null` clears it, an absent key leaves it alone. An entry
113
+ * with no ports left is removed.
114
+ */
115
+ setForward(nodeId: string, patch: ManualForwardPatch): NatSettingsFile {
116
+ const current = this.load();
117
+ const next: ManualForward = { ...(current.forwards[nodeId] ?? {}) };
118
+ for (const kind of PORT_KINDS) {
119
+ const value = patch[kind];
120
+ if (value === undefined) continue;
121
+ if (value === null) delete next[kind];
122
+ else next[kind] = value;
123
+ }
124
+ const forwards = { ...current.forwards };
125
+ if (Object.keys(next).length === 0) delete forwards[nodeId];
126
+ else forwards[nodeId] = next;
127
+ const merged: NatSettingsFile = { ...current, forwards };
128
+ validate(merged);
129
+ this.save(merged);
130
+ return merged;
131
+ }
132
+
133
+ /** Drop a node's manual forward entry. No-op (no write) when there is none. */
134
+ deleteForward(nodeId: string): NatSettingsFile {
135
+ const current = this.load();
136
+ if (!(nodeId in current.forwards)) return current;
137
+ const forwards = { ...current.forwards };
138
+ delete forwards[nodeId];
139
+ const merged: NatSettingsFile = { ...current, forwards };
140
+ this.save(merged);
141
+ return merged;
142
+ }
99
143
  }
100
144
 
101
145
  function defaultSettings(): NatSettingsFile {
102
146
  return {
103
147
  version: FILE_VERSION,
104
- externalPort: DEFAULT_PORT,
105
- internalPort: DEFAULT_PORT,
148
+ network: 'auto',
106
149
  upnpEnabled: true,
150
+ forwards: {},
107
151
  ddns: defaultDdns(),
108
152
  };
109
153
  }
@@ -117,27 +161,78 @@ function defaultDdns(): NatDdnsSettings {
117
161
  };
118
162
  }
119
163
 
120
- function isNatSettingsFile(v: unknown): v is NatSettingsFile {
121
- if (!v || typeof v !== 'object') return false;
164
+ /**
165
+ * Build the settings from the known fields of a parsed file, or null when the
166
+ * shape is wrong. Unknown fields — including the `externalPort`/`internalPort`
167
+ * an older build wrote — are dropped, so the next save removes them.
168
+ */
169
+ function fromParsed(v: unknown): NatSettingsFile | null {
170
+ if (!v || typeof v !== 'object') return null;
122
171
  const obj = v as Record<string, unknown>;
123
- if (obj.version !== FILE_VERSION) return false;
124
- if (typeof obj.externalPort !== 'number' || typeof obj.internalPort !== 'number') return false;
125
- if (typeof obj.upnpEnabled !== 'boolean') return false;
172
+ if (obj.version !== FILE_VERSION) return null;
173
+ if (typeof obj.upnpEnabled !== 'boolean') return null;
174
+ // Absent in a file written before network modes existed; that reads as `auto`.
175
+ if (obj.network !== undefined && !isNetworkSetting(obj.network)) return null;
126
176
  const ddns = obj.ddns as Record<string, unknown> | undefined;
127
- if (!ddns || typeof ddns !== 'object') return false;
128
- if (ddns.providerId !== null && typeof ddns.providerId !== 'string') return false;
129
- if (ddns.hostname !== null && typeof ddns.hostname !== 'string') return false;
130
- if (typeof ddns.externallyManaged !== 'boolean') return false;
131
- if (typeof ddns.intervalMs !== 'number') return false;
132
- return true;
177
+ if (!ddns || typeof ddns !== 'object') return null;
178
+ if (ddns.providerId !== null && typeof ddns.providerId !== 'string') return null;
179
+ if (ddns.hostname !== null && typeof ddns.hostname !== 'string') return null;
180
+ if (typeof ddns.externallyManaged !== 'boolean') return null;
181
+ if (typeof ddns.intervalMs !== 'number') return null;
182
+ const forwards = parseForwards(obj.forwards);
183
+ if (!forwards) return null;
184
+ return {
185
+ version: FILE_VERSION,
186
+ network: (obj.network as NetworkSetting | undefined) ?? 'auto',
187
+ upnpEnabled: obj.upnpEnabled,
188
+ forwards,
189
+ ddns: {
190
+ providerId: ddns.providerId as string | null,
191
+ hostname: ddns.hostname as string | null,
192
+ externallyManaged: ddns.externallyManaged,
193
+ intervalMs: ddns.intervalMs,
194
+ },
195
+ };
196
+ }
197
+
198
+ /** `forwards` is absent in a file written before manual forwards existed; that reads as none. */
199
+ function parseForwards(v: unknown): Record<string, ManualForward> | null {
200
+ if (v === undefined) return {};
201
+ if (!v || typeof v !== 'object' || Array.isArray(v)) return null;
202
+ const out: Record<string, ManualForward> = {};
203
+ for (const [nodeId, entry] of Object.entries(v as Record<string, unknown>)) {
204
+ if (!entry || typeof entry !== 'object') return null;
205
+ const rec = entry as Record<string, unknown>;
206
+ const forward: ManualForward = {};
207
+ for (const kind of PORT_KINDS) {
208
+ if (rec[kind] === undefined) continue;
209
+ if (typeof rec[kind] !== 'number') return null;
210
+ forward[kind] = rec[kind];
211
+ }
212
+ out[nodeId] = forward;
213
+ }
214
+ return out;
215
+ }
216
+
217
+ function isNetworkSetting(v: unknown): v is NetworkSetting {
218
+ return v === 'auto' || v === 'lan' || v === 'public';
133
219
  }
134
220
 
135
221
  function validate(s: NatSettingsFile): void {
136
- if (!Number.isInteger(s.externalPort) || s.externalPort < 1 || s.externalPort > 65535) {
137
- throw new NatError('invalid_config', `externalPort must be 1-65535, got ${s.externalPort}`);
222
+ if (!isNetworkSetting(s.network)) {
223
+ throw new NatError('invalid_config', `network must be "auto", "lan" or "public", got "${String(s.network)}"`);
138
224
  }
139
- if (!Number.isInteger(s.internalPort) || s.internalPort < 1 || s.internalPort > 65535) {
140
- throw new NatError('invalid_config', `internalPort must be 1-65535, got ${s.internalPort}`);
225
+ for (const [nodeId, forward] of Object.entries(s.forwards)) {
226
+ if (nodeId.length === 0) {
227
+ throw new NatError('invalid_config', 'a manual forward needs a node id');
228
+ }
229
+ for (const kind of PORT_KINDS) {
230
+ const port = forward[kind];
231
+ if (port === undefined) continue;
232
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
233
+ throw new NatError('invalid_config', `${kind} forward for ${nodeId} must be 1-65535, got ${port}`);
234
+ }
235
+ }
141
236
  }
142
237
  if (!Number.isFinite(s.ddns.intervalMs) || s.ddns.intervalMs < 1000) {
143
238
  throw new NatError(
@@ -148,7 +243,7 @@ function validate(s: NatSettingsFile): void {
148
243
  if (s.ddns.providerId !== null && s.ddns.providerId.length === 0) {
149
244
  throw new NatError('invalid_config', 'ddns.providerId must be a non-empty string or null');
150
245
  }
151
- if (s.ddns.hostname !== null && s.ddns.hostname.length === 0) {
152
- throw new NatError('invalid_config', 'ddns.hostname must be a non-empty string or null');
246
+ if (s.ddns.hostname !== null && !isDnsHostname(s.ddns.hostname)) {
247
+ throw new NatError('invalid_config', `ddns.hostname must be a DNS name such as foo.duckdns.org or null, got "${s.ddns.hostname}"`);
153
248
  }
154
249
  }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * LAN or public-IP mode: whether this machine sits behind a router or holds a public IPv4
3
+ * on one of its interfaces. See `NetworkMode` in `types.ts` for what each changes.
4
+ */
5
+
6
+ import { networkInterfaces, type NetworkInterfaceInfo } from 'node:os';
7
+
8
+ import { isPublicIpv4 } from './address-resolver.js';
9
+ import type { NetworkMode, NetworkSetting } from './types.js';
10
+
11
+ /**
12
+ * The first public IPv4 an interface holds, or null. A VPS's `eth0` holds its public address
13
+ * directly; a machine behind a router holds only private ones (its public address lives on the
14
+ * router). Docker bridges, VPNs and other private interfaces never qualify.
15
+ *
16
+ * NOTE: any interface counts, not only the one on the route to the internet, so a LAN machine
17
+ * with a public address on a second card or a tunnel reads as public (the `lan` setting fixes it);
18
+ * if that is reported, decide on the default route's source address instead (the UDP-connect probe
19
+ * in cadre-core's `primaryLanAddress`).
20
+ */
21
+ export function publicInterfaceAddress(
22
+ interfaces: Record<string, NetworkInterfaceInfo[] | undefined> = networkInterfaces(),
23
+ ): string | null {
24
+ for (const entries of Object.values(interfaces)) {
25
+ for (const entry of entries ?? []) {
26
+ if (entry.family === 'IPv4' && !entry.internal && isPublicIpv4(entry.address)) return entry.address;
27
+ }
28
+ }
29
+ return null;
30
+ }
31
+
32
+ /** The mode in effect: the setting, or for `auto`, public exactly when an interface holds a public IPv4. */
33
+ export function effectiveNetworkMode(setting: NetworkSetting, publicInterfaceIp: string | null): NetworkMode {
34
+ if (setting !== 'auto') return setting;
35
+ return publicInterfaceIp !== null ? 'public' : 'lan';
36
+ }