@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
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Human-readable output of the `cadre-host join` and `cadre-host node` commands:
3
+ * the claim payload as a terminal QR code and as text, the warning when the node
4
+ * is not reachable from outside, the claimed line, the outcome of a join by
5
+ * invitation, and the node list.
6
+ *
7
+ * The types are loose mirrors of `ClaimDetails` and `HostedNodeView`
8
+ * (`../hosted/`): the body arrives over HTTP from whichever cadre-host version is
9
+ * running, so every field is optional and a missing one prints as `?`.
10
+ *
11
+ * Streams: the payload text goes to stdout ALONE, so `cadre-host join --no-qr`
12
+ * can be piped or copied; everything else (the QR, the instructions, the
13
+ * warning, the claimed line) goes to stderr. The payload carries the claim
14
+ * secret by design; nothing else printed here contains it.
15
+ */
16
+
17
+ import { createRequire } from 'node:module';
18
+
19
+ export interface ClaimDetailsLike {
20
+ payload?: string;
21
+ peerId?: string;
22
+ multiaddrs?: string[];
23
+ reachability?: { verdict?: string; reason?: string | null } | null;
24
+ }
25
+
26
+ export interface HostedNodeLike {
27
+ id?: string;
28
+ join?: { kind?: string };
29
+ status?: string;
30
+ partyId?: string;
31
+ ownerKey?: string;
32
+ memberPeerId?: string;
33
+ connected?: boolean;
34
+ error?: string;
35
+ retryable?: boolean;
36
+ }
37
+
38
+ const requireForQr = createRequire(import.meta.url);
39
+
40
+ /** Print the QR code (unless `qr` is false) and the instructions on stderr, then the payload text on stdout. */
41
+ export function printClaimPayload(details: ClaimDetailsLike, opts: { qr: boolean }): void {
42
+ console.error('Scan this with the Sereus app on the phone that owns the cadre, or paste the text into it.');
43
+ if (opts.qr && details.payload) renderQr(details.payload);
44
+ console.error(`Node ${details.peerId ?? '?'} at:`);
45
+ for (const addr of details.multiaddrs ?? []) console.error(` ${addr}`);
46
+ printReachabilityWarning(details);
47
+ console.log(details.payload ?? '');
48
+ }
49
+
50
+ /**
51
+ * One line on stderr when the NAT layer says the node cannot be reached from
52
+ * outside: the claim still works from the home network over the LAN address.
53
+ */
54
+ export function printReachabilityWarning(details: ClaimDetailsLike): void {
55
+ const reachability = details.reachability;
56
+ if (!reachability || reachability.verdict !== 'unreachable') return;
57
+ console.error(
58
+ 'Warning: this node cannot be reached from outside your home network yet'
59
+ + `${reachability.reason ? ` — ${reachability.reason}` : ''}. `
60
+ + 'A phone on your home network can still claim it; run `cadre-host nat status` for the ports to forward.',
61
+ );
62
+ }
63
+
64
+ /** The line printed once the watcher reports the claim. */
65
+ export function printClaimed(node: HostedNodeLike): void {
66
+ console.error(`✓ Claimed by owner ${ownerFingerprint(node.ownerKey)} into cadre ${node.partyId ?? '?'}`);
67
+ }
68
+
69
+ /**
70
+ * How a join by invitation ended: the member that admitted the node, or the failure. A
71
+ * retryable failure means no member could be reached, which is the usual case for a cadre's
72
+ * first always-on node: its only members are phones, which nothing can dial.
73
+ */
74
+ export function printInvitationOutcome(node: HostedNodeLike): void {
75
+ if (node.status === 'joined') {
76
+ console.error(`✓ Joined cadre ${node.partyId ?? '?'} at member ${node.memberPeerId ?? '(unnamed)'}`);
77
+ return;
78
+ }
79
+ console.error(`✗ Could not join: ${node.error ?? 'unknown error'}`);
80
+ if (!node.retryable) return;
81
+ console.error(
82
+ 'If this is the cadre\'s first always-on node, no member can be reached yet: use `cadre-host join` without '
83
+ + '`--invitation` and scan the QR code from the phone instead. Otherwise, once a member is online, run '
84
+ + `\`cadre-host node retry ${node.id ?? '<id>'}\`.`,
85
+ );
86
+ }
87
+
88
+ /** One line per hosted node: id, status, party, owner fingerprint, connected. */
89
+ export function printNodeList(nodes: HostedNodeLike[]): void {
90
+ if (nodes.length === 0) {
91
+ console.log('Hosted nodes: none. Run `cadre-host join` to start one.');
92
+ return;
93
+ }
94
+ console.log('Hosted nodes:');
95
+ for (const node of nodes) {
96
+ // An invitation node names its cadre from the start; a claim node only once claimed.
97
+ const party = node.status === 'joined' || node.join?.kind === 'invitation' ? `cadre ${node.partyId ?? '?'}` : (node.status ?? '?');
98
+ const owner = node.ownerKey ? ` owner ${ownerFingerprint(node.ownerKey)}` : '';
99
+ const connected = node.status === 'joined' ? ` ${node.connected ? 'connected' : 'not connected'}` : '';
100
+ const error = node.error ? ` (${node.error})` : '';
101
+ console.log(` ${node.id ?? '?'} ${node.status ?? '?'} ${party}${owner}${connected}${error}`);
102
+ }
103
+ }
104
+
105
+ /** The first 8 characters of an owner key, as the UI shows it. */
106
+ function ownerFingerprint(ownerKey: string | undefined): string {
107
+ return ownerKey ? ownerKey.slice(0, 8) : '?';
108
+ }
109
+
110
+ /** Best-effort QR render on stderr — fall back to the bare text if the lib chokes. */
111
+ function renderQr(text: string): void {
112
+ try {
113
+ const qr = requireForQr('qrcode-terminal') as {
114
+ generate: (text: string, opts?: { small?: boolean }, cb?: (s: string) => void) => void;
115
+ };
116
+ qr.generate(text, { small: true }, (rendered) => { console.error(rendered); });
117
+ } catch (err) {
118
+ console.error(`(qrcode-terminal unavailable: ${(err as Error).message})`);
119
+ }
120
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Human-readable output of the `cadre-host nat` commands: the host-level NAT
3
+ * state, then one block per hosted node, with the forward to make on the
4
+ * router when a node cannot be reached from outside.
5
+ *
6
+ * The types are loose mirrors of `NatStatusSnapshot` (`../nat/types.ts`): the
7
+ * body arrives over HTTP from whichever cadre-host version is running, so every
8
+ * field is optional and a missing one prints as `?`.
9
+ */
10
+
11
+ export interface NatPortRouteLike {
12
+ internalPort?: number;
13
+ externalPort?: number | null;
14
+ source?: string | null;
15
+ error?: string | null;
16
+ }
17
+
18
+ export interface NatNodeLike {
19
+ nodeId?: string;
20
+ running?: boolean;
21
+ verdict?: string;
22
+ reason?: string | null;
23
+ tcp?: NatPortRouteLike;
24
+ ws?: NatPortRouteLike | null;
25
+ publicAddrs?: string[];
26
+ }
27
+
28
+ export interface NatStatusLike {
29
+ network?: { setting?: string; mode?: string; publicInterfaceIp?: string | null };
30
+ upnpEnabled?: boolean;
31
+ gateway?: {
32
+ found?: boolean;
33
+ lanAddress?: string | null;
34
+ routerExternalIp?: string | null;
35
+ lastError?: string | null;
36
+ };
37
+ externalIp?: string | null;
38
+ cgnatDetected?: boolean;
39
+ directReachability?: string;
40
+ lastTestedAt?: string | null;
41
+ nodes?: NatNodeLike[];
42
+ ddns?: {
43
+ providerId?: string | null;
44
+ hostname?: string | null;
45
+ externallyManaged?: boolean;
46
+ lastUpdateAt?: string | null;
47
+ lastUpdateOk?: boolean | null;
48
+ lastError?: string | null;
49
+ };
50
+ }
51
+
52
+ type PortKind = 'tcp' | 'ws';
53
+
54
+ interface PortEntry {
55
+ kind: PortKind;
56
+ route: NatPortRouteLike;
57
+ }
58
+
59
+ const KIND_LABEL: Record<PortKind, string> = { tcp: 'TCP', ws: 'WebSocket' };
60
+
61
+ /** Width of the label column inside a node block. */
62
+ const FIELD_WIDTH = 11;
63
+
64
+ /**
65
+ * A forward changes the node's public addresses only when it changes the
66
+ * external port or makes a port routable, and the restart that follows is
67
+ * rate-limited per node (`NAT_ADDRESS_RESTART_MIN_INTERVAL_MS`).
68
+ */
69
+ const FORWARD_RESTART_NOTE =
70
+ 'If its public addresses changed, the node restarts to announce them; a node already restarted ' +
71
+ 'for an address change in the last 10 minutes waits until those 10 minutes are up.';
72
+
73
+ export function printNatStatus(s: NatStatusLike): void {
74
+ printHostLines(s);
75
+ printDdns(s);
76
+ const nodes = s.nodes ?? [];
77
+ console.log(nodes.length === 0 ? '\nNodes: none' : '\nNodes:');
78
+ for (const n of nodes) printNodeReachability(n, s);
79
+ }
80
+
81
+ /** The block for the node a forward was saved for, then what happens next. */
82
+ export function printForwardResult(nodeId: string, s: NatStatusLike): void {
83
+ const node = s.nodes?.find((n) => n.nodeId === nodeId);
84
+ if (node) printNodeReachability(node, s);
85
+ console.log(`\n${FORWARD_RESTART_NOTE}`);
86
+ }
87
+
88
+ function printHostLines(s: NatStatusLike): void {
89
+ const network = s.network ?? {};
90
+ const how = network.setting === 'auto' || network.setting === undefined ? 'detected' : 'set';
91
+ if (network.mode === 'public') {
92
+ console.log(`Network: public IP ${network.publicInterfaceIp ?? s.externalIp ?? '?'} on this machine (${how}); no router, so no UPnP`);
93
+ console.log(`External IP: ${s.externalIp ?? network.publicInterfaceIp ?? '(unknown)'}`);
94
+ console.log(`Reachability: ${s.directReachability ?? 'unknown'}${s.lastTestedAt ? ` (tested ${s.lastTestedAt})` : ''}`);
95
+ return;
96
+ }
97
+ console.log(`Network: behind a router (${how})`);
98
+ const gateway = s.gateway ?? {};
99
+ const gatewayLine = gateway.found
100
+ ? `found (this machine is ${gateway.lanAddress ?? '?'} on its network)`
101
+ : `not found${gateway.lastError ? ` — ${gateway.lastError}` : ''}`;
102
+ console.log(`UPnP: ${s.upnpEnabled === false ? 'off' : 'on'}; router ${gatewayLine}`);
103
+ if (gateway.routerExternalIp) {
104
+ console.log(`Router IP: ${gateway.routerExternalIp}`);
105
+ }
106
+ console.log(`External IP: ${s.externalIp ?? '(unknown)'}${s.cgnatDetected ? ' [CGNAT detected]' : ''}`);
107
+ console.log(`Reachability: ${s.directReachability ?? 'unknown'}${s.lastTestedAt ? ` (tested ${s.lastTestedAt})` : ''}`);
108
+ }
109
+
110
+ function printDdns(s: NatStatusLike): void {
111
+ const ddns = s.ddns ?? {};
112
+ if (!ddns.providerId && !ddns.hostname) {
113
+ console.log('DDNS: not configured');
114
+ return;
115
+ }
116
+ console.log(`DDNS: ${ddns.hostname ?? '(no hostname)'} via ${ddns.providerId ?? '(no provider)'}${ddns.externallyManaged ? ' [externally managed]' : ''}`);
117
+ if (ddns.lastUpdateAt) {
118
+ const ok = ddns.lastUpdateOk ? 'OK' : 'FAIL';
119
+ console.log(`DDNS update: ${ok} at ${ddns.lastUpdateAt}${ddns.lastError ? ` — ${ddns.lastError}` : ''}`);
120
+ }
121
+ }
122
+
123
+ function printNodeReachability(n: NatNodeLike, s: NatStatusLike): void {
124
+ console.log(` ${n.nodeId ?? '?'}: ${n.verdict ?? '?'}${n.running === false ? ' [stopped]' : ''}`);
125
+ printField('TCP', [formatRoute(n.tcp)]);
126
+ printField('WebSocket', [n.ws ? formatRoute(n.ws) : 'not available']);
127
+ printField('Public', n.publicAddrs?.length ? n.publicAddrs : ['none']);
128
+ if (n.reason) printField(n.verdict === 'direct' ? 'Firewall' : 'Why', [n.reason]);
129
+ const advice = forwardAdvice(n, s);
130
+ if (advice.length > 0) printField('To fix', advice);
131
+ }
132
+
133
+ function printField(label: string, values: string[]): void {
134
+ values.forEach((value, i) => console.log(` ${(i === 0 ? label : '').padEnd(FIELD_WIDTH)}${value}`));
135
+ }
136
+
137
+ function formatRoute(r: NatPortRouteLike | undefined): string {
138
+ if (!r) return '?';
139
+ const internal = `internal ${r.internalPort ?? '?'}`;
140
+ if (r.externalPort == null) return `${internal} → not mapped${r.error ? ` (${r.error})` : ''}`;
141
+ const failed = r.error ? `; last attempt failed: ${r.error}` : '';
142
+ if (r.source === 'direct') return `${internal} → public, as-is${failed}`;
143
+ return `${internal} → external ${r.externalPort} (${r.source === 'manual' ? 'forwarded by hand' : 'UPnP'}${failed})`;
144
+ }
145
+
146
+ /**
147
+ * What to do for an unreachable node, one line per array entry. Behind CGNAT a
148
+ * router forward does not help, but the detection can misfire, so the forward
149
+ * command is still offered for every port. With every port routed there is
150
+ * nothing to forward: what is missing is the host's public address, which the
151
+ * reason already explains.
152
+ *
153
+ * NOTE: the UI builds the same sentence (`forwardInstruction` in
154
+ * `ui/src/lib/reachability.ts`); if the rule for which ports to forward
155
+ * changes, have the server send the port list in `NodeReachability` instead
156
+ * of changing both copies.
157
+ */
158
+ function forwardAdvice(n: NatNodeLike, s: NatStatusLike): string[] {
159
+ if (n.verdict !== 'unreachable') return [];
160
+ const nodeId = n.nodeId ?? '<node-id>';
161
+ const ports = portsOf(n);
162
+ if (s.cgnatDetected) {
163
+ return [
164
+ 'Carrier-grade NAT detected: a forward on your router will not help, and cadre-host cannot reserve a relay for its nodes yet.',
165
+ 'If the detection is wrong and you did forward ports, tell cadre-host with:',
166
+ ` ${forwardCommand(nodeId, ports)}`,
167
+ ];
168
+ }
169
+ const unrouted = ports.filter(({ route }) => route.externalPort == null);
170
+ if (unrouted.length === 0) return [];
171
+ const lan = s.gateway?.lanAddress ?? null;
172
+ const forwards = unrouted.map(({ kind, route }) => {
173
+ const port = route.internalPort ?? '?';
174
+ return `${KIND_LABEL[kind]} port ${port} to ${lan ? `${lan}:${port}` : `port ${port} on this machine's LAN address`}`;
175
+ });
176
+ return [`On your router, forward ${forwards.join(' and ')}, then run:`, ` ${forwardCommand(nodeId, unrouted)}`];
177
+ }
178
+
179
+ function portsOf(n: NatNodeLike): PortEntry[] {
180
+ const out: PortEntry[] = [];
181
+ if (n.tcp) out.push({ kind: 'tcp', route: n.tcp });
182
+ if (n.ws) out.push({ kind: 'ws', route: n.ws });
183
+ return out;
184
+ }
185
+
186
+ function forwardCommand(nodeId: string, ports: PortEntry[]): string {
187
+ return ['cadre-host nat forward', nodeId, ...ports.map(({ kind }) => `--${kind} <external port>`)].join(' ');
188
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * How to reach the local UI from where the admin is sitting. On a machine with a desktop, open
3
+ * the URL; over SSH or on a headless server, forward the port from the admin's own computer
4
+ * first. The UI binds 127.0.0.1 only and checks the Host header, so the local port must equal
5
+ * the remote one.
6
+ */
7
+
8
+ import { hostname, userInfo } from 'node:os';
9
+
10
+ /** Over SSH, or Linux with no display server: a browser here would show nothing to the admin. */
11
+ export function isHeadless(platform: NodeJS.Platform = process.platform, env: NodeJS.ProcessEnv = process.env): boolean {
12
+ if (env.SSH_CONNECTION || env.SSH_TTY) return true;
13
+ return platform === 'linux' && !env.DISPLAY && !env.WAYLAND_DISPLAY;
14
+ }
15
+
16
+ /** Who to SSH in as, and where; `sshPort` only when it is not 22. */
17
+ export interface SshLogin {
18
+ user: string;
19
+ host: string;
20
+ sshPort?: number;
21
+ }
22
+
23
+ /** The lines telling the admin how to open the UI at `port`. */
24
+ export function uiAccessLines(port: number, headless: boolean, who: SshLogin = currentLogin()): string[] {
25
+ const url = `http://127.0.0.1:${port}`;
26
+ if (!headless) return [`Open the UI: ${url}`];
27
+ const portFlag = who.sshPort !== undefined && who.sshPort !== 22 ? ` -p ${who.sshPort}` : '';
28
+ return [
29
+ 'Open the UI from your own computer (this session has no screen):',
30
+ ` ssh${portFlag} -L ${port}:127.0.0.1:${port} ${who.user}@${who.host}`,
31
+ ` then browse to ${url}`,
32
+ ];
33
+ }
34
+
35
+ /**
36
+ * Over SSH, the server address and port the admin's client connected to (`SSH_CONNECTION` is
37
+ * "client-ip client-port server-ip server-port"): a VPS's hostname rarely resolves from the
38
+ * admin's computer, the address they just reached does. Otherwise this machine's hostname.
39
+ */
40
+ export function currentLogin(env: NodeJS.ProcessEnv = process.env): SshLogin {
41
+ let user = '<you>';
42
+ try {
43
+ user = userInfo().username;
44
+ } catch {
45
+ // no passwd entry (some containers): keep the placeholder
46
+ }
47
+ const [, , serverIp, serverPort] = (env.SSH_CONNECTION ?? '').trim().split(/\s+/);
48
+ if (serverIp && serverPort) return { user, host: serverIp, sshPort: Number(serverPort) };
49
+ return { user, host: hostname() };
50
+ }