@serfab/cadre-host 0.7.1

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 (300) hide show
  1. package/README.md +101 -0
  2. package/dist/auth/duration.d.ts +1 -0
  3. package/dist/auth/duration.js +32 -0
  4. package/dist/auth/duration.js.map +1 -0
  5. package/dist/auth/index.d.ts +9 -0
  6. package/dist/auth/index.js +8 -0
  7. package/dist/auth/index.js.map +1 -0
  8. package/dist/auth/trust-circle-store.d.ts +26 -0
  9. package/dist/auth/trust-circle-store.js +129 -0
  10. package/dist/auth/trust-circle-store.js.map +1 -0
  11. package/dist/auth/trust-circle.d.ts +91 -0
  12. package/dist/auth/trust-circle.js +230 -0
  13. package/dist/auth/trust-circle.js.map +1 -0
  14. package/dist/auth/types.d.ts +81 -0
  15. package/dist/auth/types.js +24 -0
  16. package/dist/auth/types.js.map +1 -0
  17. package/dist/bin/host.d.ts +13 -0
  18. package/dist/bin/host.js +826 -0
  19. package/dist/bin/host.js.map +1 -0
  20. package/dist/index.d.ts +32 -0
  21. package/dist/index.js +27 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/installer/browser.d.ts +14 -0
  24. package/dist/installer/browser.js +35 -0
  25. package/dist/installer/browser.js.map +1 -0
  26. package/dist/installer/config.d.ts +43 -0
  27. package/dist/installer/config.js +113 -0
  28. package/dist/installer/config.js.map +1 -0
  29. package/dist/installer/identity.d.ts +23 -0
  30. package/dist/installer/identity.js +36 -0
  31. package/dist/installer/identity.js.map +1 -0
  32. package/dist/installer/index.d.ts +83 -0
  33. package/dist/installer/index.js +204 -0
  34. package/dist/installer/index.js.map +1 -0
  35. package/dist/installer/paths.d.ts +33 -0
  36. package/dist/installer/paths.js +84 -0
  37. package/dist/installer/paths.js.map +1 -0
  38. package/dist/installer/platform.d.ts +9 -0
  39. package/dist/installer/platform.js +21 -0
  40. package/dist/installer/platform.js.map +1 -0
  41. package/dist/installer/service-host/index.d.ts +11 -0
  42. package/dist/installer/service-host/index.js +16 -0
  43. package/dist/installer/service-host/index.js.map +1 -0
  44. package/dist/installer/service-host/launchd.d.ts +16 -0
  45. package/dist/installer/service-host/launchd.js +79 -0
  46. package/dist/installer/service-host/launchd.js.map +1 -0
  47. package/dist/installer/service-host/nssm.d.ts +20 -0
  48. package/dist/installer/service-host/nssm.js +88 -0
  49. package/dist/installer/service-host/nssm.js.map +1 -0
  50. package/dist/installer/service-host/systemd.d.ts +16 -0
  51. package/dist/installer/service-host/systemd.js +78 -0
  52. package/dist/installer/service-host/systemd.js.map +1 -0
  53. package/dist/installer/service-host/template.d.ts +6 -0
  54. package/dist/installer/service-host/template.js +20 -0
  55. package/dist/installer/service-host/template.js.map +1 -0
  56. package/dist/installer/service-host/types.d.ts +40 -0
  57. package/dist/installer/service-host/types.js +6 -0
  58. package/dist/installer/service-host/types.js.map +1 -0
  59. package/dist/installer/wizard.d.ts +31 -0
  60. package/dist/installer/wizard.js +67 -0
  61. package/dist/installer/wizard.js.map +1 -0
  62. package/dist/nat/address-resolver.d.ts +22 -0
  63. package/dist/nat/address-resolver.js +31 -0
  64. package/dist/nat/address-resolver.js.map +1 -0
  65. package/dist/nat/ddns/duckdns.d.ts +12 -0
  66. package/dist/nat/ddns/duckdns.js +57 -0
  67. package/dist/nat/ddns/duckdns.js.map +1 -0
  68. package/dist/nat/ddns/index.d.ts +30 -0
  69. package/dist/nat/ddns/index.js +21 -0
  70. package/dist/nat/ddns/index.js.map +1 -0
  71. package/dist/nat/ddns/updater.d.ts +68 -0
  72. package/dist/nat/ddns/updater.js +194 -0
  73. package/dist/nat/ddns/updater.js.map +1 -0
  74. package/dist/nat/external-ip.d.ts +62 -0
  75. package/dist/nat/external-ip.js +114 -0
  76. package/dist/nat/external-ip.js.map +1 -0
  77. package/dist/nat/index.d.ts +22 -0
  78. package/dist/nat/index.js +16 -0
  79. package/dist/nat/index.js.map +1 -0
  80. package/dist/nat/nat-service.d.ts +101 -0
  81. package/dist/nat/nat-service.js +284 -0
  82. package/dist/nat/nat-service.js.map +1 -0
  83. package/dist/nat/nat-store.d.ts +19 -0
  84. package/dist/nat/nat-store.js +136 -0
  85. package/dist/nat/nat-store.js.map +1 -0
  86. package/dist/nat/port-mapper.d.ts +98 -0
  87. package/dist/nat/port-mapper.js +226 -0
  88. package/dist/nat/port-mapper.js.map +1 -0
  89. package/dist/nat/reachability.d.ts +19 -0
  90. package/dist/nat/reachability.js +29 -0
  91. package/dist/nat/reachability.js.map +1 -0
  92. package/dist/nat/secrets/file-store.d.ts +23 -0
  93. package/dist/nat/secrets/file-store.js +102 -0
  94. package/dist/nat/secrets/file-store.js.map +1 -0
  95. package/dist/nat/secrets/index.d.ts +36 -0
  96. package/dist/nat/secrets/index.js +56 -0
  97. package/dist/nat/secrets/index.js.map +1 -0
  98. package/dist/nat/secrets/keytar-store.d.ts +18 -0
  99. package/dist/nat/secrets/keytar-store.js +52 -0
  100. package/dist/nat/secrets/keytar-store.js.map +1 -0
  101. package/dist/nat/types.d.ts +120 -0
  102. package/dist/nat/types.js +24 -0
  103. package/dist/nat/types.js.map +1 -0
  104. package/dist/orchestrator/host-process-orchestrator.d.ts +87 -0
  105. package/dist/orchestrator/host-process-orchestrator.js +557 -0
  106. package/dist/orchestrator/host-process-orchestrator.js.map +1 -0
  107. package/dist/orchestrator/index.d.ts +3 -0
  108. package/dist/orchestrator/index.js +2 -0
  109. package/dist/orchestrator/index.js.map +1 -0
  110. package/dist/orchestrator/log-rotator.d.ts +25 -0
  111. package/dist/orchestrator/log-rotator.js +56 -0
  112. package/dist/orchestrator/log-rotator.js.map +1 -0
  113. package/dist/orchestrator/pid-liveness.d.ts +11 -0
  114. package/dist/orchestrator/pid-liveness.js +26 -0
  115. package/dist/orchestrator/pid-liveness.js.map +1 -0
  116. package/dist/orchestrator/port-allocator.d.ts +16 -0
  117. package/dist/orchestrator/port-allocator.js +38 -0
  118. package/dist/orchestrator/port-allocator.js.map +1 -0
  119. package/dist/orchestrator/state-store.d.ts +32 -0
  120. package/dist/orchestrator/state-store.js +44 -0
  121. package/dist/orchestrator/state-store.js.map +1 -0
  122. package/dist/orchestrator/types.d.ts +85 -0
  123. package/dist/orchestrator/types.js +31 -0
  124. package/dist/orchestrator/types.js.map +1 -0
  125. package/dist/server/error-handler.d.ts +26 -0
  126. package/dist/server/error-handler.js +95 -0
  127. package/dist/server/error-handler.js.map +1 -0
  128. package/dist/server/events/bus.d.ts +17 -0
  129. package/dist/server/events/bus.js +36 -0
  130. package/dist/server/events/bus.js.map +1 -0
  131. package/dist/server/events/sse-route.d.ts +22 -0
  132. package/dist/server/events/sse-route.js +65 -0
  133. package/dist/server/events/sse-route.js.map +1 -0
  134. package/dist/server/events/types.d.ts +25 -0
  135. package/dist/server/events/types.js +8 -0
  136. package/dist/server/events/types.js.map +1 -0
  137. package/dist/server/index.d.ts +67 -0
  138. package/dist/server/index.js +149 -0
  139. package/dist/server/index.js.map +1 -0
  140. package/dist/server/origin-guard.d.ts +24 -0
  141. package/dist/server/origin-guard.js +72 -0
  142. package/dist/server/origin-guard.js.map +1 -0
  143. package/dist/server/routes/nat.d.ts +16 -0
  144. package/dist/server/routes/nat.js +42 -0
  145. package/dist/server/routes/nat.js.map +1 -0
  146. package/dist/server/routes/nodes.d.ts +23 -0
  147. package/dist/server/routes/nodes.js +128 -0
  148. package/dist/server/routes/nodes.js.map +1 -0
  149. package/dist/server/routes/settings.d.ts +26 -0
  150. package/dist/server/routes/settings.js +97 -0
  151. package/dist/server/routes/settings.js.map +1 -0
  152. package/dist/server/routes/status.d.ts +42 -0
  153. package/dist/server/routes/status.js +76 -0
  154. package/dist/server/routes/status.js.map +1 -0
  155. package/dist/server/routes/trust-circle.d.ts +17 -0
  156. package/dist/server/routes/trust-circle.js +35 -0
  157. package/dist/server/routes/trust-circle.js.map +1 -0
  158. package/dist/server/routes/update.d.ts +16 -0
  159. package/dist/server/routes/update.js +29 -0
  160. package/dist/server/routes/update.js.map +1 -0
  161. package/dist/server/server.d.ts +40 -0
  162. package/dist/server/server.js +90 -0
  163. package/dist/server/server.js.map +1 -0
  164. package/dist/server/settings-store.d.ts +24 -0
  165. package/dist/server/settings-store.js +27 -0
  166. package/dist/server/settings-store.js.map +1 -0
  167. package/dist/server/static.d.ts +25 -0
  168. package/dist/server/static.js +177 -0
  169. package/dist/server/static.js.map +1 -0
  170. package/dist/ui/assets/index-BwU5mJGF.css +1 -0
  171. package/dist/ui/assets/index-C9uChk2P.js +14 -0
  172. package/dist/ui/assets/index-C9uChk2P.js.map +1 -0
  173. package/dist/ui/index.html +14 -0
  174. package/dist/update/apply.d.ts +37 -0
  175. package/dist/update/apply.js +102 -0
  176. package/dist/update/apply.js.map +1 -0
  177. package/dist/update/index.d.ts +101 -0
  178. package/dist/update/index.js +228 -0
  179. package/dist/update/index.js.map +1 -0
  180. package/dist/update/manifest.d.ts +48 -0
  181. package/dist/update/manifest.js +152 -0
  182. package/dist/update/manifest.js.map +1 -0
  183. package/dist/update/release-key.d.ts +24 -0
  184. package/dist/update/release-key.js +52 -0
  185. package/dist/update/release-key.js.map +1 -0
  186. package/dist/update/store.d.ts +22 -0
  187. package/dist/update/store.js +81 -0
  188. package/dist/update/store.js.map +1 -0
  189. package/dist/update/types.d.ts +90 -0
  190. package/dist/update/types.js +22 -0
  191. package/dist/update/types.js.map +1 -0
  192. package/dist/update/version.d.ts +18 -0
  193. package/dist/update/version.js +76 -0
  194. package/dist/update/version.js.map +1 -0
  195. package/package.json +80 -0
  196. package/service/README.md +86 -0
  197. package/service/cadre-host.service.tmpl +16 -0
  198. package/service/com.serfab.cadre-host.plist.tmpl +35 -0
  199. package/service/install-service.ps1 +98 -0
  200. package/service/uninstall-service.ps1 +19 -0
  201. package/src/__tests__/cli-invite.smoke.test.ts +187 -0
  202. package/src/__tests__/cli-nat.smoke.test.ts +207 -0
  203. package/src/__tests__/cli.smoke.test.ts +41 -0
  204. package/src/__tests__/orchestrator.test.ts +570 -0
  205. package/src/auth/__tests__/duration.test.ts +28 -0
  206. package/src/auth/__tests__/trust-circle-integration.test.ts +128 -0
  207. package/src/auth/__tests__/trust-circle-store.test.ts +115 -0
  208. package/src/auth/__tests__/trust-circle.test.ts +334 -0
  209. package/src/auth/duration.ts +33 -0
  210. package/src/auth/index.ts +19 -0
  211. package/src/auth/trust-circle-store.ts +155 -0
  212. package/src/auth/trust-circle.ts +304 -0
  213. package/src/auth/types.ts +96 -0
  214. package/src/bin/host.ts +905 -0
  215. package/src/index.ts +167 -0
  216. package/src/installer/__tests__/config.test.ts +86 -0
  217. package/src/installer/__tests__/identity.test.ts +34 -0
  218. package/src/installer/__tests__/installer.smoke.test.ts +124 -0
  219. package/src/installer/__tests__/paths.test.ts +47 -0
  220. package/src/installer/__tests__/service-host.test.ts +62 -0
  221. package/src/installer/__tests__/wizard.test.ts +60 -0
  222. package/src/installer/browser.ts +42 -0
  223. package/src/installer/config.ts +153 -0
  224. package/src/installer/identity.ts +49 -0
  225. package/src/installer/index.ts +303 -0
  226. package/src/installer/paths.ts +109 -0
  227. package/src/installer/platform.ts +27 -0
  228. package/src/installer/service-host/index.ts +21 -0
  229. package/src/installer/service-host/launchd.ts +90 -0
  230. package/src/installer/service-host/nssm.ts +100 -0
  231. package/src/installer/service-host/systemd.ts +88 -0
  232. package/src/installer/service-host/template.ts +21 -0
  233. package/src/installer/service-host/types.ts +48 -0
  234. package/src/installer/wizard.ts +92 -0
  235. package/src/nat/__tests__/address-resolver.test.ts +82 -0
  236. package/src/nat/__tests__/ddns-duckdns.test.ts +83 -0
  237. package/src/nat/__tests__/ddns-updater.test.ts +253 -0
  238. package/src/nat/__tests__/external-ip.test.ts +124 -0
  239. package/src/nat/__tests__/nat-service.test.ts +305 -0
  240. package/src/nat/__tests__/nat-store.test.ts +93 -0
  241. package/src/nat/__tests__/port-mapper.test.ts +191 -0
  242. package/src/nat/__tests__/reachability.test.ts +24 -0
  243. package/src/nat/__tests__/secrets.test.ts +156 -0
  244. package/src/nat/address-resolver.ts +51 -0
  245. package/src/nat/ddns/duckdns.ts +76 -0
  246. package/src/nat/ddns/index.ts +55 -0
  247. package/src/nat/ddns/updater.ts +243 -0
  248. package/src/nat/external-ip.ts +154 -0
  249. package/src/nat/index.ts +71 -0
  250. package/src/nat/nat-service.ts +375 -0
  251. package/src/nat/nat-store.ts +154 -0
  252. package/src/nat/port-mapper.ts +329 -0
  253. package/src/nat/reachability.ts +34 -0
  254. package/src/nat/secrets/file-store.ts +130 -0
  255. package/src/nat/secrets/index.ts +82 -0
  256. package/src/nat/secrets/keytar-store.ts +67 -0
  257. package/src/nat/types.ts +157 -0
  258. package/src/orchestrator/host-process-orchestrator.ts +626 -0
  259. package/src/orchestrator/index.ts +3 -0
  260. package/src/orchestrator/log-rotator.ts +53 -0
  261. package/src/orchestrator/pid-liveness.ts +24 -0
  262. package/src/orchestrator/port-allocator.ts +43 -0
  263. package/src/orchestrator/state-store.ts +72 -0
  264. package/src/orchestrator/types.ts +95 -0
  265. package/src/server/__tests__/bus.test.ts +38 -0
  266. package/src/server/__tests__/error-handler.test.ts +96 -0
  267. package/src/server/__tests__/nodes-route.test.ts +179 -0
  268. package/src/server/__tests__/origin-guard.test.ts +72 -0
  269. package/src/server/__tests__/publishers.test.ts +216 -0
  270. package/src/server/__tests__/server.smoke.test.ts +178 -0
  271. package/src/server/__tests__/settings-route.test.ts +177 -0
  272. package/src/server/__tests__/sse-route.test.ts +119 -0
  273. package/src/server/__tests__/status-route.test.ts +113 -0
  274. package/src/server/error-handler.ts +108 -0
  275. package/src/server/events/bus.ts +42 -0
  276. package/src/server/events/sse-route.ts +81 -0
  277. package/src/server/events/types.ts +22 -0
  278. package/src/server/index.ts +209 -0
  279. package/src/server/origin-guard.ts +84 -0
  280. package/src/server/routes/nat.ts +63 -0
  281. package/src/server/routes/nodes.ts +147 -0
  282. package/src/server/routes/settings.ts +115 -0
  283. package/src/server/routes/status.ts +108 -0
  284. package/src/server/routes/trust-circle.ts +49 -0
  285. package/src/server/routes/update.ts +43 -0
  286. package/src/server/server.ts +122 -0
  287. package/src/server/settings-store.ts +37 -0
  288. package/src/server/static.ts +191 -0
  289. package/src/update/__tests__/apply.test.ts +101 -0
  290. package/src/update/__tests__/manifest.test.ts +93 -0
  291. package/src/update/__tests__/store.test.ts +81 -0
  292. package/src/update/__tests__/update-service.integration.test.ts +221 -0
  293. package/src/update/__tests__/version.test.ts +31 -0
  294. package/src/update/apply.ts +148 -0
  295. package/src/update/index.ts +303 -0
  296. package/src/update/manifest.ts +172 -0
  297. package/src/update/release-key.ts +55 -0
  298. package/src/update/store.ts +101 -0
  299. package/src/update/types.ts +109 -0
  300. package/src/update/version.ts +80 -0
package/src/index.ts ADDED
@@ -0,0 +1,167 @@
1
+ /**
2
+ * @serfab/cadre-host — self-hosted cadre node manager.
3
+ *
4
+ * This package is the sibling of @serfab/cadre-provider for self-hosted
5
+ * basement-PC deployments. It depends on @serfab/cadre-provider for the
6
+ * orchestration interface and container lifecycle types but ships its own
7
+ * orchestrator (host processes, not Docker), auth (trust circle, not API
8
+ * keys), installer, NAT layer, and local management UI.
9
+ *
10
+ * Implementations of HostProcessOrchestrator, TrustCircleAuth, the NAT
11
+ * layer, installer scripts, and the local UI live in sibling subdirectories
12
+ * under src/ and are added by their respective tickets:
13
+ *
14
+ * - cadre-host-process-orchestrator [DONE]
15
+ * - cadre-host-trust-circle [DONE]
16
+ * - cadre-host-nat
17
+ * - cadre-host-installer
18
+ * - cadre-host-local-ui
19
+ */
20
+
21
+ export type {
22
+ Orchestrator,
23
+ OrchestratorCreateRequest,
24
+ OrchestratorCreateResult,
25
+ OrchestratorStats,
26
+ ContainerStatus,
27
+ ContainerResources,
28
+ } from '@serfab/cadre-provider';
29
+
30
+ export { HostProcessOrchestrator } from './orchestrator/index.js';
31
+ export type {
32
+ HostProcessConfig,
33
+ PersistedHandle,
34
+ ManagedNodeInfo,
35
+ NodeStateListener,
36
+ } from './orchestrator/index.js';
37
+
38
+ export {
39
+ TrustCircleService,
40
+ TrustCircleStore,
41
+ TrustCircleError,
42
+ createTrustCircleHandlers,
43
+ parseDuration,
44
+ DEFAULT_INVITE_TTL_MS,
45
+ } from './auth/index.js';
46
+ export type {
47
+ TrustCircleServiceOptions,
48
+ CadreNodeLike,
49
+ TrustCircleMember,
50
+ PendingInvite,
51
+ TrustCircleFile,
52
+ TrustCircleSnapshot,
53
+ TrustCircleHandlers,
54
+ TrustCircleErrorCode,
55
+ } from './auth/index.js';
56
+
57
+ export { Installer } from './installer/index.js';
58
+ export type {
59
+ InstallOptions,
60
+ InstallResult,
61
+ UninstallOptions,
62
+ InstallerOptions,
63
+ HostConfigFile,
64
+ WizardAnswers,
65
+ WizardDefaults,
66
+ ServiceHost,
67
+ ServiceHostContext,
68
+ ServiceHostStatus,
69
+ } from './installer/index.js';
70
+
71
+ export {
72
+ UpdateService,
73
+ createUpdateHandlers,
74
+ UpdateStateStore,
75
+ UpdateErrorException,
76
+ applyUpdate,
77
+ defaultNpmExecutor,
78
+ fetchManifest,
79
+ verifyManifest,
80
+ canonicalJson,
81
+ signManifestForTesting,
82
+ compareVersions,
83
+ parseVersion,
84
+ getReleasePublicKey,
85
+ getReleasePublicKeyBase64,
86
+ ed25519FromRaw,
87
+ } from './update/index.js';
88
+ export type {
89
+ UpdateServiceOptions,
90
+ UpdateHandlers,
91
+ UpdateState,
92
+ UpdateApplyResult,
93
+ UpdateSettings,
94
+ UpdateManifest,
95
+ UpdateErrorCode,
96
+ NpmExecutor,
97
+ ServiceRestarter,
98
+ } from './update/index.js';
99
+
100
+ export {
101
+ NatService,
102
+ NatStore,
103
+ NatError,
104
+ createNatHandlers,
105
+ ExternalIpDetector,
106
+ PortMapperService,
107
+ createDefaultPortMapper,
108
+ evaluateReachability,
109
+ buildInviteAddresses,
110
+ BUILTIN_PROVIDERS,
111
+ getProvider,
112
+ listProviders,
113
+ duckDnsProvider,
114
+ DdnsUpdater,
115
+ KeytarSecretsStore,
116
+ FileSecretsStore,
117
+ createSecretsStore,
118
+ ddnsAccount,
119
+ KEYTAR_SERVICE,
120
+ DEFAULT_LEASE_TTL_MS,
121
+ DEFAULT_REFRESH_MS,
122
+ isPlausibleIp,
123
+ } from './nat/index.js';
124
+ export type {
125
+ NatServiceOptions,
126
+ CadreNodeLike as NatCadreNodeLike,
127
+ NatStatusSnapshot,
128
+ NatDdnsStatus,
129
+ NatSettingsFile,
130
+ NatDdnsSettings,
131
+ NatHandlers,
132
+ NatErrorCode,
133
+ PortForwardMode,
134
+ DirectReachability,
135
+ DdnsProviderInfo,
136
+ DdnsConfigField,
137
+ ExternalIpResult,
138
+ RouterIpProbe,
139
+ ExternalIpDetectorOptions,
140
+ PortMapper,
141
+ PortMappingResult,
142
+ PortMapOptions,
143
+ PortMapperState,
144
+ PortMapperServiceOptions,
145
+ BuildInviteAddressesInput,
146
+ DdnsProvider,
147
+ DdnsProviderDeps,
148
+ DdnsUpdaterOptions,
149
+ DdnsUpdateOutcome,
150
+ SecretsStore,
151
+ KeytarLike,
152
+ } from './nat/index.js';
153
+
154
+ /* ──────────────── local UI server (6.5.1) ──────────────── */
155
+
156
+ export {
157
+ createLocalUiServer,
158
+ EventBus,
159
+ HostSettingsStore,
160
+ } from './server/index.js';
161
+ export type {
162
+ LocalUiServer,
163
+ LocalUiServerOptions,
164
+ LocalUiEvent,
165
+ LocalUiEventType,
166
+ LocalUiEventListener,
167
+ } from './server/index.js';
@@ -0,0 +1,86 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ import { readHostConfig, writeHostConfig, type HostConfigFile } from '../config.js';
7
+
8
+ describe('host.config.json round-trip', () => {
9
+ let tmp: string;
10
+
11
+ beforeEach(() => {
12
+ tmp = mkdtempSync(join(tmpdir(), 'cadre-host-cfg-'));
13
+ });
14
+
15
+ afterEach(() => {
16
+ rmSync(tmp, { recursive: true, force: true });
17
+ });
18
+
19
+ function makeCfg(overrides: Partial<HostConfigFile> = {}): HostConfigFile {
20
+ return {
21
+ version: 2,
22
+ installId: 'abc123',
23
+ uiPort: 8765,
24
+ libp2pPort: 4001,
25
+ dataDir: tmp,
26
+ identityPath: join(tmp, 'identity.key'),
27
+ upnpEnabled: true,
28
+ installedAt: '2026-01-01T00:00:00.000Z',
29
+ installerVersion: '0.6.0',
30
+ updates: { autoApply: false },
31
+ ...overrides,
32
+ };
33
+ }
34
+
35
+ it('writes and reads the same shape', () => {
36
+ const cfg = makeCfg();
37
+ const path = join(tmp, 'host.config.json');
38
+ writeHostConfig(path, cfg);
39
+ expect(readHostConfig(path)).toEqual(cfg);
40
+ });
41
+
42
+ it('upgrades v1 files in place on read', () => {
43
+ const path = join(tmp, 'host.config.json');
44
+ const v1 = {
45
+ version: 1,
46
+ installId: 'abc123',
47
+ uiPort: 8765,
48
+ libp2pPort: 4001,
49
+ dataDir: tmp,
50
+ identityPath: join(tmp, 'identity.key'),
51
+ upnpEnabled: true,
52
+ installedAt: '2026-01-01T00:00:00.000Z',
53
+ installerVersion: '0.6.0',
54
+ };
55
+ writeFileSync(path, JSON.stringify(v1));
56
+ const upgraded = readHostConfig(path);
57
+ expect(upgraded.version).toBe(2);
58
+ expect(upgraded.updates).toEqual({ autoApply: false });
59
+ // Reading again should be idempotent and stay v2.
60
+ const reRead = readHostConfig(path);
61
+ expect(reRead.version).toBe(2);
62
+ });
63
+
64
+ it('rejects unknown versions', () => {
65
+ const path = join(tmp, 'host.config.json');
66
+ writeFileSync(path, JSON.stringify({ ...makeCfg(), version: 99 }));
67
+ expect(() => readHostConfig(path)).toThrow(/unsupported version=99/);
68
+ });
69
+
70
+ it('rejects malformed JSON', () => {
71
+ const path = join(tmp, 'host.config.json');
72
+ writeFileSync(path, 'not json');
73
+ expect(() => readHostConfig(path)).toThrow(/Failed to parse/);
74
+ });
75
+
76
+ it('rejects missing required fields', () => {
77
+ const path = join(tmp, 'host.config.json');
78
+ writeFileSync(path, JSON.stringify({ version: 2, uiPort: 1234 }));
79
+ expect(() => readHostConfig(path)).toThrow(/missing required fields/);
80
+ });
81
+
82
+ it('refuses to write the wrong version', () => {
83
+ const path = join(tmp, 'host.config.json');
84
+ expect(() => writeHostConfig(path, { ...makeCfg(), version: 3 as never })).toThrow(/version=3/);
85
+ });
86
+ });
@@ -0,0 +1,34 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+ import { mkdtempSync, rmSync, statSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ import { generateIdentity, loadIdentity } from '../identity.js';
7
+
8
+ describe('installer identity', () => {
9
+ let tmp: string;
10
+
11
+ beforeEach(() => {
12
+ tmp = mkdtempSync(join(tmpdir(), 'cadre-host-id-'));
13
+ });
14
+
15
+ afterEach(() => {
16
+ rmSync(tmp, { recursive: true, force: true });
17
+ });
18
+
19
+ it('round-trips an Ed25519 keypair through the file', async () => {
20
+ const path = join(tmp, 'identity.key');
21
+ const generated = await generateIdentity(path);
22
+ expect(generated.peerId).toMatch(/^12D3Koo/);
23
+
24
+ const loaded = loadIdentity(path);
25
+ expect(loaded.peerId).toBe(generated.peerId);
26
+ });
27
+
28
+ it.runIf(process.platform !== 'win32')('persists with mode 0600 on POSIX', async () => {
29
+ const path = join(tmp, 'identity.key');
30
+ await generateIdentity(path);
31
+ const mode = statSync(path).mode & 0o777;
32
+ expect(mode).toBe(0o600);
33
+ });
34
+ });
@@ -0,0 +1,124 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+ import { existsSync, mkdtempSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ import { Installer } from '../index.js';
7
+ import type { ServiceHost, ServiceHostContext, ServiceHostStatus } from '../service-host/types.js';
8
+
9
+ class FakeServiceHost implements ServiceHost {
10
+ readonly name = 'cadre-host';
11
+ installCalls: ServiceHostContext[] = [];
12
+ uninstallCalls: ServiceHostContext[] = [];
13
+ installed = false;
14
+ running = false;
15
+
16
+ async install(ctx: ServiceHostContext): Promise<void> {
17
+ this.installCalls.push(ctx);
18
+ this.installed = true;
19
+ this.running = true;
20
+ }
21
+ async uninstall(ctx: ServiceHostContext): Promise<void> {
22
+ this.uninstallCalls.push(ctx);
23
+ this.installed = false;
24
+ this.running = false;
25
+ }
26
+ async restart(_ctx: ServiceHostContext): Promise<void> {
27
+ this.running = true;
28
+ }
29
+ async status(_ctx: ServiceHostContext): Promise<ServiceHostStatus> {
30
+ return { installed: this.installed, running: this.running };
31
+ }
32
+ renderUnit(_ctx: ServiceHostContext): string | null {
33
+ return 'rendered-unit';
34
+ }
35
+ }
36
+
37
+ describe('Installer smoke', () => {
38
+ let tmp: string;
39
+
40
+ beforeEach(() => {
41
+ tmp = mkdtempSync(join(tmpdir(), 'cadre-host-installer-smoke-'));
42
+ });
43
+
44
+ afterEach(() => {
45
+ rmSync(tmp, { recursive: true, force: true });
46
+ });
47
+
48
+ it('install creates config + identity + invokes service-host', async () => {
49
+ const fake = new FakeServiceHost();
50
+ const installer = new Installer({ platform: 'linux', installerVersion: 'test-1.0.0' });
51
+ const result = await installer.install({
52
+ nonInteractive: true,
53
+ dataDir: tmp,
54
+ uiPort: 19999,
55
+ libp2pPort: 14001,
56
+ openBrowser: false,
57
+ noInvite: true,
58
+ serviceHost: fake,
59
+ });
60
+
61
+ expect(result.dataDir).toBe(tmp);
62
+ expect(result.uiUrl).toBe('http://127.0.0.1:19999/');
63
+ expect(result.serviceName).toBe('cadre-host');
64
+ expect(result.configPath).toBe(join(tmp, 'host.config.json'));
65
+
66
+ expect(existsSync(join(tmp, 'host.config.json'))).toBe(true);
67
+ expect(existsSync(join(tmp, 'identity.key'))).toBe(true);
68
+ expect(existsSync(join(tmp, 'nat.json'))).toBe(true);
69
+ expect(existsSync(join(tmp, 'logs'))).toBe(true);
70
+
71
+ expect(fake.installCalls).toHaveLength(1);
72
+ expect(fake.installCalls[0]!.dataDir).toBe(tmp);
73
+ });
74
+
75
+ it('uninstall removes data only when --remove-data is set', async () => {
76
+ const fake = new FakeServiceHost();
77
+ const installer = new Installer({ platform: 'linux' });
78
+ await installer.install({
79
+ nonInteractive: true,
80
+ dataDir: tmp,
81
+ uiPort: 19998,
82
+ libp2pPort: 14002,
83
+ openBrowser: false,
84
+ noInvite: true,
85
+ serviceHost: fake,
86
+ });
87
+ expect(existsSync(join(tmp, 'host.config.json'))).toBe(true);
88
+
89
+ await installer.uninstall({ yes: true, removeData: false, dataDir: tmp, serviceHost: fake });
90
+ expect(fake.uninstallCalls).toHaveLength(1);
91
+ expect(existsSync(join(tmp, 'host.config.json'))).toBe(true);
92
+
93
+ await installer.uninstall({ yes: true, removeData: true, dataDir: tmp, serviceHost: fake });
94
+ expect(existsSync(tmp)).toBe(false);
95
+ });
96
+
97
+ it('rejects --system in v1', async () => {
98
+ const fake = new FakeServiceHost();
99
+ const installer = new Installer({ platform: 'linux' });
100
+ await expect(installer.install({
101
+ nonInteractive: true,
102
+ dataDir: tmp,
103
+ system: true,
104
+ serviceHost: fake,
105
+ openBrowser: false,
106
+ noInvite: true,
107
+ })).rejects.toThrow(/system.*not yet supported/);
108
+ expect(fake.installCalls).toHaveLength(0);
109
+ });
110
+
111
+ it('re-running install preserves the existing identity', async () => {
112
+ const fake = new FakeServiceHost();
113
+ const installer = new Installer({ platform: 'linux' });
114
+ await installer.install({
115
+ nonInteractive: true, dataDir: tmp, openBrowser: false, noInvite: true, serviceHost: fake,
116
+ });
117
+ const idBefore = (await import('node:fs')).readFileSync(join(tmp, 'identity.key'));
118
+ await installer.install({
119
+ nonInteractive: true, dataDir: tmp, openBrowser: false, noInvite: true, serviceHost: fake,
120
+ });
121
+ const idAfter = (await import('node:fs')).readFileSync(join(tmp, 'identity.key'));
122
+ expect(Buffer.from(idBefore).equals(Buffer.from(idAfter))).toBe(true);
123
+ });
124
+ });
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { defaultDataDir, serviceInstallPath } from '../paths.js';
4
+
5
+ describe('defaultDataDir', () => {
6
+ it('honors XDG_DATA_HOME on linux', () => {
7
+ expect(defaultDataDir('linux', { XDG_DATA_HOME: '/tmp/xdg', HOME: '/home/me' }, '/home/me'))
8
+ .toBe('/tmp/xdg/cadre-host');
9
+ });
10
+
11
+ it('falls back to ~/.local/share on linux', () => {
12
+ expect(defaultDataDir('linux', { HOME: '/home/me' }, '/home/me'))
13
+ .toBe('/home/me/.local/share/cadre-host');
14
+ });
15
+
16
+ it('uses Application Support on darwin', () => {
17
+ expect(defaultDataDir('darwin', { HOME: '/Users/me' }, '/Users/me'))
18
+ .toBe('/Users/me/Library/Application Support/CadreHost');
19
+ });
20
+
21
+ it('uses LocalAppData on win32', () => {
22
+ expect(defaultDataDir('win32', { LocalAppData: 'C:\\Users\\me\\AppData\\Local' }, 'C:\\Users\\me'))
23
+ .toBe('C:\\Users\\me\\AppData\\Local\\CadreHost');
24
+ });
25
+
26
+ it('falls back to ~/AppData/Local on win32 without LocalAppData', () => {
27
+ expect(defaultDataDir('win32', {}, 'C:\\Users\\me'))
28
+ .toBe('C:\\Users\\me\\AppData\\Local\\CadreHost');
29
+ });
30
+ });
31
+
32
+ describe('serviceInstallPath', () => {
33
+ it('honors XDG_CONFIG_HOME on linux', () => {
34
+ expect(serviceInstallPath('linux', { XDG_CONFIG_HOME: '/tmp/cfg', HOME: '/home/me' }, '/home/me'))
35
+ .toBe('/tmp/cfg/systemd/user/cadre-host.service');
36
+ });
37
+
38
+ it('uses LaunchAgents on darwin', () => {
39
+ expect(serviceInstallPath('darwin', { HOME: '/Users/me' }, '/Users/me'))
40
+ .toBe('/Users/me/Library/LaunchAgents/com.serfab.cadre-host.plist');
41
+ });
42
+
43
+ it('returns a registry-style path on win32 (for logging only)', () => {
44
+ expect(serviceInstallPath('win32', {}, 'C:\\Users\\me'))
45
+ .toMatch(/CadreHost/);
46
+ });
47
+ });
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { LaunchdServiceHost } from '../service-host/launchd.js';
6
+ import { SystemdServiceHost } from '../service-host/systemd.js';
7
+ import { NssmServiceHost } from '../service-host/nssm.js';
8
+ import { renderTemplate } from '../service-host/template.js';
9
+ import type { ServiceHostContext } from '../service-host/types.js';
10
+
11
+ const here = fileURLToPath(new URL('.', import.meta.url));
12
+ const serviceDir = resolve(here, '..', '..', '..', 'service');
13
+
14
+ function ctx(): ServiceHostContext {
15
+ return {
16
+ nodePath: '/usr/local/bin/node',
17
+ hostJs: '/opt/cadre-host/dist/bin/host.js',
18
+ dataDir: '/var/data/cadre-host',
19
+ serviceDir,
20
+ };
21
+ }
22
+
23
+ describe('renderTemplate', () => {
24
+ it('substitutes @TOKEN@ values', () => {
25
+ expect(renderTemplate('@A@ x @B@', { A: 'foo', B: 'bar' })).toBe('foo x bar');
26
+ });
27
+
28
+ it('throws on unknown tokens', () => {
29
+ expect(() => renderTemplate('@A@ @MISSING@', { A: 'foo' })).toThrow(/missing value for token @MISSING@/);
30
+ });
31
+ });
32
+
33
+ describe('SystemdServiceHost.renderUnit', () => {
34
+ it('produces an enable-able [Install] section and substitutes ExecStart', () => {
35
+ const unit = new SystemdServiceHost().renderUnit(ctx());
36
+ expect(unit).toContain('[Install]');
37
+ expect(unit).toContain('WantedBy=default.target');
38
+ expect(unit).toContain('ExecStart=/usr/local/bin/node /opt/cadre-host/dist/bin/host.js start --no-tui --data-dir /var/data/cadre-host');
39
+ expect(unit).toContain('RestartPreventExitStatus=0');
40
+ });
41
+ });
42
+
43
+ describe('LaunchdServiceHost.renderUnit', () => {
44
+ it('produces a plist with the substituted ProgramArguments', () => {
45
+ const unit = new LaunchdServiceHost().renderUnit(ctx());
46
+ expect(unit).toContain('<key>Label</key>');
47
+ expect(unit).toContain('<string>com.serfab.cadre-host</string>');
48
+ expect(unit).toContain('<string>/usr/local/bin/node</string>');
49
+ expect(unit).toContain('<string>/opt/cadre-host/dist/bin/host.js</string>');
50
+ expect(unit).toContain('<string>--data-dir</string>');
51
+ expect(unit).toContain('<string>/var/data/cadre-host</string>');
52
+ expect(unit).toContain('<key>KeepAlive</key>');
53
+ expect(unit).toContain('<key>SuccessfulExit</key>');
54
+ expect(unit).toContain('<key>Crashed</key>');
55
+ });
56
+ });
57
+
58
+ describe('NssmServiceHost', () => {
59
+ it('returns null from renderUnit (NSSM stores config in the registry)', () => {
60
+ expect(new NssmServiceHost().renderUnit(ctx())).toBeNull();
61
+ });
62
+ });
@@ -0,0 +1,60 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { runWizardWith, type WizardDefaults } from '../wizard.js';
4
+
5
+ const defaults: WizardDefaults = {
6
+ dataDir: '/data/cadre-host',
7
+ uiPort: 8765,
8
+ libp2pPort: 4001,
9
+ upnpEnabled: true,
10
+ };
11
+
12
+ function scripted(answers: ReadonlyArray<string>): (label: string, fallback: string) => Promise<string> {
13
+ let i = 0;
14
+ return async (_label, _fallback) => {
15
+ if (i >= answers.length) throw new Error(`wizard asked more questions than scripted (i=${i})`);
16
+ return answers[i++]!;
17
+ };
18
+ }
19
+
20
+ describe('runWizardWith', () => {
21
+ it('returns the defaults when every answer is blank', async () => {
22
+ const out = await runWizardWith(defaults, scripted(['', '', '', '', '']));
23
+ expect(out).toEqual({
24
+ dataDir: '/data/cadre-host',
25
+ uiPort: 8765,
26
+ libp2pPort: 4001,
27
+ upnpEnabled: true,
28
+ configureDdns: false,
29
+ });
30
+ });
31
+
32
+ it('respects overrides', async () => {
33
+ const out = await runWizardWith(defaults, scripted([
34
+ '/srv/host',
35
+ '9000',
36
+ '5000',
37
+ 'n',
38
+ 'y',
39
+ ]));
40
+ expect(out).toEqual({
41
+ dataDir: '/srv/host',
42
+ uiPort: 9000,
43
+ libp2pPort: 5000,
44
+ upnpEnabled: false,
45
+ configureDdns: true,
46
+ });
47
+ });
48
+
49
+ it('rejects invalid port input', async () => {
50
+ await expect(
51
+ runWizardWith(defaults, scripted(['', 'banana', '', '', ''])),
52
+ ).rejects.toThrow(/Invalid port/);
53
+ });
54
+
55
+ it('rejects unrecognized yes/no answers', async () => {
56
+ await expect(
57
+ runWizardWith(defaults, scripted(['', '', '', 'maybe', ''])),
58
+ ).rejects.toThrow(/Invalid yes\/no/);
59
+ });
60
+ });
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Best-effort URL opener. Never throws — if the platform launcher isn't
3
+ * available (headless box, missing xdg-utils, etc.) we just log the URL.
4
+ *
5
+ * Implemented inline rather than pulling in the `open` npm dep so the
6
+ * installer keeps its dependency tree small.
7
+ */
8
+
9
+ import { spawn } from 'node:child_process';
10
+
11
+ export interface OpenBrowserResult {
12
+ attempted: boolean;
13
+ spawned: boolean;
14
+ /** Set when the spawn attempt errored synchronously. */
15
+ error?: string;
16
+ }
17
+
18
+ export function openBrowser(url: string, platform: NodeJS.Platform = process.platform): OpenBrowserResult {
19
+ try {
20
+ if (platform === 'darwin') {
21
+ const child = spawn('open', [url], { detached: true, stdio: 'ignore' });
22
+ child.unref();
23
+ return { attempted: true, spawned: true };
24
+ }
25
+ if (platform === 'win32') {
26
+ // rundll32 is the most portable way to invoke the registered handler
27
+ // without going through cmd.exe (which would require escaping URLs).
28
+ const child = spawn('rundll32', ['url.dll,FileProtocolHandler', url], {
29
+ detached: true,
30
+ stdio: 'ignore',
31
+ });
32
+ child.unref();
33
+ return { attempted: true, spawned: true };
34
+ }
35
+ // linux + everything else POSIX-ish
36
+ const child = spawn('xdg-open', [url], { detached: true, stdio: 'ignore' });
37
+ child.unref();
38
+ return { attempted: true, spawned: true };
39
+ } catch (err) {
40
+ return { attempted: true, spawned: false, error: (err as Error).message };
41
+ }
42
+ }