@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
@@ -0,0 +1,16 @@
1
+ /**
2
+ * systemd `--user` unit registration.
3
+ *
4
+ * Renders the template into `~/.config/systemd/user/cadre-host.service`,
5
+ * runs `systemctl --user daemon-reload`, `enable --now cadre-host`, and
6
+ * `loginctl enable-linger <user>` so the unit survives logout / reboot.
7
+ */
8
+ import type { ServiceHost, ServiceHostContext, ServiceHostStatus } from './types.js';
9
+ export declare class SystemdServiceHost implements ServiceHost {
10
+ readonly name = "cadre-host";
11
+ renderUnit(ctx: ServiceHostContext): string;
12
+ install(ctx: ServiceHostContext): Promise<void>;
13
+ uninstall(ctx: ServiceHostContext): Promise<void>;
14
+ restart(ctx: ServiceHostContext): Promise<void>;
15
+ status(ctx: ServiceHostContext): Promise<ServiceHostStatus>;
16
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * systemd `--user` unit registration.
3
+ *
4
+ * Renders the template into `~/.config/systemd/user/cadre-host.service`,
5
+ * runs `systemctl --user daemon-reload`, `enable --now cadre-host`, and
6
+ * `loginctl enable-linger <user>` so the unit survives logout / reboot.
7
+ */
8
+ import { mkdirSync, readFileSync, rmSync, writeFileSync, existsSync } from 'node:fs';
9
+ import { dirname } from 'node:path';
10
+ import { execFileSync, spawnSync } from 'node:child_process';
11
+ import { userInfo } from 'node:os';
12
+ import debug from 'debug';
13
+ import { renderTemplate } from './template.js';
14
+ import { serviceInstallPath } from '../paths.js';
15
+ const log = debug('cadre:host:installer:systemd');
16
+ const SERVICE_NAME = 'cadre-host';
17
+ export class SystemdServiceHost {
18
+ constructor() {
19
+ this.name = SERVICE_NAME;
20
+ }
21
+ renderUnit(ctx) {
22
+ const tmpl = readFileSync(`${ctx.serviceDir}/cadre-host.service.tmpl`, 'utf8');
23
+ return renderTemplate(tmpl, {
24
+ NODE_PATH: ctx.nodePath,
25
+ HOST_JS: ctx.hostJs,
26
+ DATA_DIR: ctx.dataDir,
27
+ });
28
+ }
29
+ async install(ctx) {
30
+ const unitPath = serviceInstallPath('linux');
31
+ const unit = this.renderUnit(ctx);
32
+ mkdirSync(dirname(unitPath), { recursive: true });
33
+ writeFileSync(unitPath, unit, { mode: 0o644 });
34
+ log('wrote %s', unitPath);
35
+ run('systemctl', ['--user', 'daemon-reload']);
36
+ run('systemctl', ['--user', 'enable', '--now', SERVICE_NAME]);
37
+ // enable-linger is opportunistic — it requires `loginctl` and may
38
+ // fail in containers / minimal images. Don't kill the install for it.
39
+ const user = userInfo().username;
40
+ const linger = spawnSync('loginctl', ['enable-linger', user], { stdio: 'inherit' });
41
+ if (linger.status !== 0) {
42
+ log('loginctl enable-linger %s failed (status=%s) — service may not run after logout', user, linger.status);
43
+ }
44
+ }
45
+ async uninstall(ctx) {
46
+ const unitPath = serviceInstallPath('linux');
47
+ // Stop + disable even if the unit file is missing — covers manual cleanup.
48
+ spawnSync('systemctl', ['--user', 'stop', SERVICE_NAME], { stdio: 'inherit' });
49
+ spawnSync('systemctl', ['--user', 'disable', SERVICE_NAME], { stdio: 'inherit' });
50
+ if (existsSync(unitPath)) {
51
+ rmSync(unitPath, { force: true });
52
+ log('removed %s', unitPath);
53
+ }
54
+ spawnSync('systemctl', ['--user', 'daemon-reload'], { stdio: 'inherit' });
55
+ void ctx;
56
+ }
57
+ async restart(ctx) {
58
+ void ctx;
59
+ run('systemctl', ['--user', 'restart', SERVICE_NAME]);
60
+ }
61
+ async status(ctx) {
62
+ void ctx;
63
+ const unitPath = serviceInstallPath('linux');
64
+ const installed = existsSync(unitPath);
65
+ const result = spawnSync('systemctl', ['--user', 'is-active', SERVICE_NAME], { encoding: 'utf8' });
66
+ const running = result.status === 0 && result.stdout.trim() === 'active';
67
+ return { installed, running };
68
+ }
69
+ }
70
+ function run(cmd, args) {
71
+ try {
72
+ execFileSync(cmd, args, { stdio: 'inherit' });
73
+ }
74
+ catch (err) {
75
+ throw new Error(`${cmd} ${args.join(' ')} failed: ${err.message}`);
76
+ }
77
+ }
78
+ //# sourceMappingURL=systemd.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"systemd.js","sourceRoot":"","sources":["../../../src/installer/service-host/systemd.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Tiny `@TOKEN@` substitution. Used to render systemd / launchd unit
3
+ * files. Throws if any tokens are left after substitution to catch
4
+ * typos at install time rather than at service-startup time.
5
+ */
6
+ export declare function renderTemplate(template: string, tokens: Readonly<Record<string, string>>): string;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Tiny `@TOKEN@` substitution. Used to render systemd / launchd unit
3
+ * files. Throws if any tokens are left after substitution to catch
4
+ * typos at install time rather than at service-startup time.
5
+ */
6
+ const TOKEN_RE = /@([A-Z][A-Z0-9_]*)@/g;
7
+ export function renderTemplate(template, tokens) {
8
+ const rendered = template.replace(TOKEN_RE, (full, key) => {
9
+ if (!Object.prototype.hasOwnProperty.call(tokens, key)) {
10
+ throw new Error(`renderTemplate: missing value for token ${full}`);
11
+ }
12
+ return tokens[key];
13
+ });
14
+ const leftover = rendered.match(TOKEN_RE);
15
+ if (leftover) {
16
+ throw new Error(`renderTemplate: unresolved tokens: ${leftover.join(', ')}`);
17
+ }
18
+ return rendered;
19
+ }
20
+ //# sourceMappingURL=template.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template.js","sourceRoot":"","sources":["../../../src/installer/service-host/template.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,QAAQ,GAAG,sBAAsB,CAAC;AAExC,MAAM,UAAU,cAAc,CAAC,QAAgB,EAAE,MAAwC;IACvF,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAW,EAAE,EAAE;QAChE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC;YACvD,MAAM,IAAI,KAAK,CAAC,2CAA2C,IAAI,EAAE,CAAC,CAAC;QACrE,CAAC;QACD,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC;IACrB,CAAC,CAAC,CAAC;IACH,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC1C,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,sCAAsC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC/E,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Service-host abstraction. The wizard picks a concrete impl by platform
3
+ * (systemd / launchd / NSSM) and drives it through this common interface.
4
+ */
5
+ export interface ServiceHostContext {
6
+ /** Absolute path to node.exe / node. */
7
+ nodePath: string;
8
+ /** Absolute path to the cadre-host CLI entrypoint (dist/bin/host.js). */
9
+ hostJs: string;
10
+ /** Absolute path to the cadre-host data dir. */
11
+ dataDir: string;
12
+ /** Directory containing template files / PowerShell scripts. */
13
+ serviceDir: string;
14
+ }
15
+ export interface ServiceHostStatus {
16
+ installed: boolean;
17
+ running: boolean;
18
+ }
19
+ export interface ServiceHost {
20
+ /** Stable service name (used for status / logging). */
21
+ readonly name: string;
22
+ /** Render unit file(s) and register with the OS service manager. */
23
+ install(ctx: ServiceHostContext): Promise<void>;
24
+ /** Stop + deregister. Idempotent: no-op when not installed. */
25
+ uninstall(ctx: ServiceHostContext): Promise<void>;
26
+ /**
27
+ * Restart the running service. Used by the update flow after `npm install -g`
28
+ * to pick up the new binary. Throws if the OS service manager reports a
29
+ * non-zero exit — callers are responsible for surfacing that as a warning
30
+ * (the binary swap already succeeded).
31
+ */
32
+ restart(ctx: ServiceHostContext): Promise<void>;
33
+ /** Cheap status read. */
34
+ status(ctx: ServiceHostContext): Promise<ServiceHostStatus>;
35
+ /**
36
+ * Render the unit-file text without touching the OS. Surfaced for unit
37
+ * tests; the install() path uses this internally too.
38
+ */
39
+ renderUnit(ctx: ServiceHostContext): string | null;
40
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Service-host abstraction. The wizard picks a concrete impl by platform
3
+ * (systemd / launchd / NSSM) and drives it through this common interface.
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/installer/service-host/types.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Interactive install wizard.
3
+ *
4
+ * Pure readline — no extra deps. Each prompt is decomposed into a small
5
+ * `Prompt` so the test can drive the wizard with a scripted Iterable of
6
+ * answers. Production code wires the prompts to stdin.
7
+ */
8
+ import type { SupportedPlatform } from './platform.js';
9
+ export interface WizardDefaults {
10
+ dataDir: string;
11
+ uiPort: number;
12
+ libp2pPort: number;
13
+ upnpEnabled: boolean;
14
+ }
15
+ export interface WizardAnswers {
16
+ dataDir: string;
17
+ uiPort: number;
18
+ libp2pPort: number;
19
+ upnpEnabled: boolean;
20
+ configureDdns: boolean;
21
+ }
22
+ export declare const DEFAULT_UI_PORT = 8765;
23
+ export declare const DEFAULT_LIBP2P_PORT = 4001;
24
+ export declare function defaultsForPlatform(platform: SupportedPlatform): WizardDefaults;
25
+ /**
26
+ * Drive the wizard with a function that returns the next user answer for
27
+ * a prompt. Used both by the readline-backed `runWizard` and by tests.
28
+ */
29
+ export declare function runWizardWith(defaults: WizardDefaults, ask: (label: string, fallback: string) => Promise<string>): Promise<WizardAnswers>;
30
+ /** Production entrypoint — drives the wizard over stdin/stdout. */
31
+ export declare function runWizard(defaults: WizardDefaults): Promise<WizardAnswers>;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Interactive install wizard.
3
+ *
4
+ * Pure readline — no extra deps. Each prompt is decomposed into a small
5
+ * `Prompt` so the test can drive the wizard with a scripted Iterable of
6
+ * answers. Production code wires the prompts to stdin.
7
+ */
8
+ import { createInterface } from 'node:readline';
9
+ import { defaultDataDir } from './paths.js';
10
+ export const DEFAULT_UI_PORT = 8765;
11
+ export const DEFAULT_LIBP2P_PORT = 4001;
12
+ export function defaultsForPlatform(platform) {
13
+ return {
14
+ dataDir: defaultDataDir(platform),
15
+ uiPort: DEFAULT_UI_PORT,
16
+ libp2pPort: DEFAULT_LIBP2P_PORT,
17
+ upnpEnabled: true,
18
+ };
19
+ }
20
+ /**
21
+ * Drive the wizard with a function that returns the next user answer for
22
+ * a prompt. Used both by the readline-backed `runWizard` and by tests.
23
+ */
24
+ export async function runWizardWith(defaults, ask) {
25
+ const dataDir = (await ask('Data directory', defaults.dataDir)).trim() || defaults.dataDir;
26
+ const uiPort = parsePort(await ask('UI port (localhost only)', String(defaults.uiPort)), defaults.uiPort);
27
+ const libp2pPort = parsePort(await ask('Cadre libp2p port', String(defaults.libp2pPort)), defaults.libp2pPort);
28
+ const upnpEnabled = parseBool(await ask('Attempt UPnP port mapping on this network? [Y/n]', defaults.upnpEnabled ? 'Y' : 'n'), defaults.upnpEnabled);
29
+ const configureDdns = parseBool(await ask('Configure DDNS now? [y/N]', 'N'), false);
30
+ return { dataDir, uiPort, libp2pPort, upnpEnabled, configureDdns };
31
+ }
32
+ /** Production entrypoint — drives the wizard over stdin/stdout. */
33
+ export async function runWizard(defaults) {
34
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
35
+ try {
36
+ return await runWizardWith(defaults, async (label, fallback) => prompt(rl, label, fallback));
37
+ }
38
+ finally {
39
+ rl.close();
40
+ }
41
+ }
42
+ function prompt(rl, label, fallback) {
43
+ return new Promise((resolve) => {
44
+ rl.question(`${label} [${fallback}]: `, (answer) => resolve(answer));
45
+ });
46
+ }
47
+ function parsePort(input, fallback) {
48
+ const trimmed = input.trim();
49
+ if (!trimmed)
50
+ return fallback;
51
+ const n = Number(trimmed);
52
+ if (!Number.isInteger(n) || n <= 0 || n > 65535) {
53
+ throw new Error(`Invalid port: ${input}`);
54
+ }
55
+ return n;
56
+ }
57
+ function parseBool(input, fallback) {
58
+ const trimmed = input.trim().toLowerCase();
59
+ if (!trimmed)
60
+ return fallback;
61
+ if (['y', 'yes', 'true', '1'].includes(trimmed))
62
+ return true;
63
+ if (['n', 'no', 'false', '0'].includes(trimmed))
64
+ return false;
65
+ throw new Error(`Invalid yes/no answer: ${input}`);
66
+ }
67
+ //# sourceMappingURL=wizard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wizard.js","sourceRoot":"","sources":["../../src/installer/wizard.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Build the multiaddrs that should be embedded in invites.
3
+ *
4
+ * Inputs are the host's currently-best understanding of its dialable surface:
5
+ * - DDNS hostname (`/dns4/<hostname>/...`) when configured AND reachable.
6
+ * - Raw external IP (`/ip4/<ip>/...`) when no DDNS but reachable.
7
+ * - Whatever libp2p reports otherwise — including any `/p2p-circuit/`
8
+ * addresses, once relay-client wiring lands (see follow-up tickets).
9
+ *
10
+ * This is intentionally a pure function so it's trivial to unit-test, and so
11
+ * the NatService can be the only place that knows about settings → multiaddrs.
12
+ */
13
+ export interface BuildInviteAddressesInput {
14
+ peerId: string;
15
+ externalPort: number;
16
+ ddnsHostname: string | null;
17
+ externalIp: string | null;
18
+ reachable: boolean;
19
+ /** Multiaddrs reported by the underlying libp2p node. */
20
+ libp2pAddrs: string[];
21
+ }
22
+ export declare function buildInviteAddresses(input: BuildInviteAddressesInput): string[];
@@ -0,0 +1,31 @@
1
+ export function buildInviteAddresses(input) {
2
+ const suffix = `/p2p/${input.peerId}`;
3
+ // DNS path-of-truth: stable across IP changes.
4
+ if (input.reachable && input.ddnsHostname) {
5
+ return [`/dns4/${input.ddnsHostname}/tcp/${input.externalPort}${suffix}`];
6
+ }
7
+ // Raw IPv4: works when we know our external IP.
8
+ if (input.reachable && input.externalIp && isIpv4(input.externalIp)) {
9
+ return [`/ip4/${input.externalIp}/tcp/${input.externalPort}${suffix}`];
10
+ }
11
+ // Fallback: whatever libp2p sees. Strip any addresses that lack the
12
+ // peer suffix and append it for clarity, but otherwise pass through.
13
+ return input.libp2pAddrs.map((a) => withPeerSuffix(a, input.peerId));
14
+ }
15
+ function withPeerSuffix(addr, peerId) {
16
+ if (addr.includes(`/p2p/${peerId}`))
17
+ return addr;
18
+ if (addr.includes('/p2p/'))
19
+ return addr; // already has *some* peer
20
+ return `${addr}/p2p/${peerId}`;
21
+ }
22
+ function isIpv4(s) {
23
+ const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(s);
24
+ if (!m)
25
+ return false;
26
+ return m.slice(1).every((octet) => {
27
+ const n = Number(octet);
28
+ return n >= 0 && n <= 255;
29
+ });
30
+ }
31
+ //# sourceMappingURL=address-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"address-resolver.js","sourceRoot":"","sources":["../../src/nat/address-resolver.ts"],"names":[],"mappings":"AAsBA,MAAM,UAAU,oBAAoB,CAAC,KAAgC;IACnE,MAAM,MAAM,GAAG,QAAQ,KAAK,CAAC,MAAM,EAAE,CAAC;IACtC,+CAA+C;IAC/C,IAAI,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,YAAY,EAAE,CAAC;QAC1C,OAAO,CAAC,SAAS,KAAK,CAAC,YAAY,QAAQ,KAAK,CAAC,YAAY,GAAG,MAAM,EAAE,CAAC,CAAC;IAC5E,CAAC;IACD,gDAAgD;IAChD,IAAI,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,UAAU,IAAI,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;QACpE,OAAO,CAAC,QAAQ,KAAK,CAAC,UAAU,QAAQ,KAAK,CAAC,YAAY,GAAG,MAAM,EAAE,CAAC,CAAC;IACzE,CAAC;IACD,oEAAoE;IACpE,qEAAqE;IACrE,OAAO,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;AACvE,CAAC;AAED,SAAS,cAAc,CAAC,IAAY,EAAE,MAAc;IAClD,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,MAAM,EAAE,CAAC;QAAE,OAAO,IAAI,CAAC;IACjD,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC,CAAE,0BAA0B;IACpE,OAAO,GAAG,IAAI,QAAQ,MAAM,EAAE,CAAC;AACjC,CAAC;AAED,SAAS,MAAM,CAAC,CAAS;IACvB,MAAM,CAAC,GAAG,8CAA8C,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACjE,IAAI,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IACrB,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;QAChC,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;QACxB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC;IAC5B,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { DdnsProvider } from './index.js';
2
+ /**
3
+ * DuckDNS provider.
4
+ *
5
+ * Update API: HTTPS GET
6
+ * https://www.duckdns.org/update?domains=<subdomain>&token=<token>&ip=<ip>
7
+ * Response is plain text — `OK` on success, `KO` on failure.
8
+ *
9
+ * `hostname` may be supplied as the bare subdomain (`foo`) or the FQDN
10
+ * (`foo.duckdns.org`); we strip the suffix.
11
+ */
12
+ export declare const duckDnsProvider: DdnsProvider;
@@ -0,0 +1,57 @@
1
+ import debug from 'debug';
2
+ import { NatError } from '../types.js';
3
+ const log = debug('cadre:host:nat-ddns:duckdns');
4
+ /**
5
+ * DuckDNS provider.
6
+ *
7
+ * Update API: HTTPS GET
8
+ * https://www.duckdns.org/update?domains=<subdomain>&token=<token>&ip=<ip>
9
+ * Response is plain text — `OK` on success, `KO` on failure.
10
+ *
11
+ * `hostname` may be supplied as the bare subdomain (`foo`) or the FQDN
12
+ * (`foo.duckdns.org`); we strip the suffix.
13
+ */
14
+ export const duckDnsProvider = {
15
+ id: 'duckdns',
16
+ displayName: 'DuckDNS',
17
+ configFields: [
18
+ { key: 'token', label: 'DuckDNS token', secret: true },
19
+ ],
20
+ async update(config, hostname, ip, deps) {
21
+ const token = config['token'];
22
+ if (!token) {
23
+ throw new NatError('ddns_credentials_missing', 'DuckDNS requires a `token` config value');
24
+ }
25
+ const subdomain = stripSuffix(hostname);
26
+ if (!subdomain) {
27
+ throw new NatError('invalid_config', `DuckDNS hostname must be a valid subdomain (got: "${hostname}")`);
28
+ }
29
+ const url = `https://www.duckdns.org/update?domains=${encodeURIComponent(subdomain)}&token=${encodeURIComponent(token)}&ip=${encodeURIComponent(ip)}`;
30
+ log('DuckDNS update %s → %s', subdomain, ip);
31
+ let res;
32
+ try {
33
+ res = await deps.fetch(url);
34
+ }
35
+ catch (err) {
36
+ throw new NatError('ddns_update_failed', `DuckDNS request failed: ${err.message}`);
37
+ }
38
+ if (!res.ok) {
39
+ throw new NatError('ddns_update_failed', `DuckDNS HTTP ${res.status}: ${res.statusText}`);
40
+ }
41
+ const text = (await res.text()).trim();
42
+ if (text !== 'OK') {
43
+ throw new NatError('ddns_update_failed', `DuckDNS returned "${text || '(empty)'}" (expected "OK")`);
44
+ }
45
+ },
46
+ };
47
+ const SUFFIX = '.duckdns.org';
48
+ function stripSuffix(hostname) {
49
+ const trimmed = hostname.trim().toLowerCase();
50
+ if (trimmed.length === 0)
51
+ return '';
52
+ if (trimmed.endsWith(SUFFIX)) {
53
+ return trimmed.slice(0, -SUFFIX.length);
54
+ }
55
+ return trimmed;
56
+ }
57
+ //# sourceMappingURL=duckdns.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duckdns.js","sourceRoot":"","sources":["../../../src/nat/ddns/duckdns.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAGvC,MAAM,GAAG,GAAG,KAAK,CAAC,6BAA6B,CAAC,CAAC;AAEjD;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,eAAe,GAAiB;IAC3C,EAAE,EAAE,SAAS;IACb,WAAW,EAAE,SAAS;IACtB,YAAY,EAAE;QACZ,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,IAAI,EAAE;KACvD;IACD,KAAK,CAAC,MAAM,CACV,MAA8B,EAC9B,QAAgB,EAChB,EAAU,EACV,IAAsB;QAEtB,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;QAC9B,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,IAAI,QAAQ,CAAC,0BAA0B,EAAE,yCAAyC,CAAC,CAAC;QAC5F,CAAC;QACD,MAAM,SAAS,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;QACxC,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,MAAM,IAAI,QAAQ,CAAC,gBAAgB,EAAE,qDAAqD,QAAQ,IAAI,CAAC,CAAC;QAC1G,CAAC;QAED,MAAM,GAAG,GAAG,0CAA0C,kBAAkB,CAAC,SAAS,CAAC,UAAU,kBAAkB,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,EAAE,CAAC,EAAE,CAAC;QACtJ,GAAG,CAAC,wBAAwB,EAAE,SAAS,EAAE,EAAE,CAAC,CAAC;QAE7C,IAAI,GAAa,CAAC;QAClB,IAAI,CAAC;YACH,GAAG,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC9B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,IAAI,QAAQ,CAChB,oBAAoB,EACpB,2BAA4B,GAAa,CAAC,OAAO,EAAE,CACpD,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,IAAI,QAAQ,CAChB,oBAAoB,EACpB,gBAAgB,GAAG,CAAC,MAAM,KAAK,GAAG,CAAC,UAAU,EAAE,CAChD,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACvC,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAClB,MAAM,IAAI,QAAQ,CAChB,oBAAoB,EACpB,qBAAqB,IAAI,IAAI,SAAS,mBAAmB,CAC1D,CAAC;QACJ,CAAC;IACH,CAAC;CACF,CAAC;AAEF,MAAM,MAAM,GAAG,cAAc,CAAC;AAE9B,SAAS,WAAW,CAAC,QAAgB;IACnC,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC9C,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACpC,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC7B,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,30 @@
1
+ import { type DdnsConfigField } from '../types.js';
2
+ /** Per-provider runtime dependencies (fetch override for tests). */
3
+ export interface DdnsProviderDeps {
4
+ fetch: typeof fetch;
5
+ }
6
+ /**
7
+ * DDNS provider interface. Each provider knows:
8
+ * - Its display name + the fields needed to configure it (which fields are
9
+ * secrets vs. plain config goes into the SecretsStore / nat.json split).
10
+ * - How to push an IP update to the provider's API.
11
+ *
12
+ * v1 ships DuckDNS only. New providers drop in by exporting a `DdnsProvider`
13
+ * and adding an entry to `BUILTIN_PROVIDERS`.
14
+ */
15
+ export interface DdnsProvider {
16
+ readonly id: string;
17
+ readonly displayName: string;
18
+ readonly configFields: ReadonlyArray<DdnsConfigField>;
19
+ /** Push `ip` for `hostname`. Throws NatError(ddns_update_failed) on failure. */
20
+ update(config: Record<string, string>, hostname: string, ip: string, deps: DdnsProviderDeps): Promise<void>;
21
+ }
22
+ /** Built-in DDNS providers. */
23
+ export declare const BUILTIN_PROVIDERS: Readonly<Record<string, DdnsProvider>>;
24
+ /** Look up a provider by ID, throwing NatError(ddns_provider_unknown) if missing. */
25
+ export declare function getProvider(providerId: string): DdnsProvider;
26
+ /** List all built-in providers. */
27
+ export declare function listProviders(): DdnsProvider[];
28
+ export { duckDnsProvider } from './duckdns.js';
29
+ export { DdnsUpdater } from './updater.js';
30
+ export type { DdnsUpdaterOptions, DdnsUpdateOutcome } from './updater.js';
@@ -0,0 +1,21 @@
1
+ import { NatError } from '../types.js';
2
+ import { duckDnsProvider } from './duckdns.js';
3
+ /** Built-in DDNS providers. */
4
+ export const BUILTIN_PROVIDERS = {
5
+ duckdns: duckDnsProvider,
6
+ };
7
+ /** Look up a provider by ID, throwing NatError(ddns_provider_unknown) if missing. */
8
+ export function getProvider(providerId) {
9
+ const p = BUILTIN_PROVIDERS[providerId];
10
+ if (!p) {
11
+ throw new NatError('ddns_provider_unknown', `Unknown DDNS provider: ${providerId}. Built-in: ${Object.keys(BUILTIN_PROVIDERS).join(', ')}`);
12
+ }
13
+ return p;
14
+ }
15
+ /** List all built-in providers. */
16
+ export function listProviders() {
17
+ return Object.values(BUILTIN_PROVIDERS);
18
+ }
19
+ export { duckDnsProvider } from './duckdns.js';
20
+ export { DdnsUpdater } from './updater.js';
21
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/nat/ddns/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAwB,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AA6B/C,+BAA+B;AAC/B,MAAM,CAAC,MAAM,iBAAiB,GAA2C;IACvE,OAAO,EAAE,eAAe;CACzB,CAAC;AAEF,qFAAqF;AACrF,MAAM,UAAU,WAAW,CAAC,UAAkB;IAC5C,MAAM,CAAC,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAC;IACxC,IAAI,CAAC,CAAC,EAAE,CAAC;QACP,MAAM,IAAI,QAAQ,CAChB,uBAAuB,EACvB,0BAA0B,UAAU,eAAe,MAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAC/F,CAAC;IACJ,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,mCAAmC;AACnC,MAAM,UAAU,aAAa;IAC3B,OAAO,MAAM,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC;AAC1C,CAAC;AAED,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,68 @@
1
+ import type { SecretsStore } from '../secrets/index.js';
2
+ import { type NatDdnsStatus, type NatDdnsSettings } from '../types.js';
3
+ import type { DdnsProvider } from './index.js';
4
+ /** Outcome of a single DDNS update attempt. */
5
+ export interface DdnsUpdateOutcome {
6
+ attempted: boolean;
7
+ ok: boolean;
8
+ ip: string | null;
9
+ error: string | null;
10
+ at: Date;
11
+ }
12
+ export interface DdnsUpdaterOptions {
13
+ /** Current DDNS settings (provider, hostname, externally-managed flag). */
14
+ settings: NatDdnsSettings;
15
+ /** Backing credential store. */
16
+ secrets: SecretsStore;
17
+ /** Returns the most recently observed external IP (null when unknown). */
18
+ getExternalIp: () => string | null;
19
+ /** Fetch override for tests. */
20
+ fetch?: typeof fetch;
21
+ /** Provider resolver override (tests can inject fakes). */
22
+ resolveProvider?: (providerId: string) => DdnsProvider;
23
+ /** Clock override for tests. */
24
+ now?: () => Date;
25
+ /** Timer overrides for tests. */
26
+ setInterval?: (fn: () => void, ms: number) => NodeJS.Timeout;
27
+ clearInterval?: (h: NodeJS.Timeout) => void;
28
+ }
29
+ /**
30
+ * DDNS update loop.
31
+ *
32
+ * Lifecycle:
33
+ * - `start()`: if a provider is configured AND not externally managed, runs
34
+ * an immediate update, then schedules `setInterval(intervalMs)`.
35
+ * - On each tick: re-evaluates the IP. If unchanged from the last successful
36
+ * update, no provider call. If changed, calls `provider.update`. Errors
37
+ * are recorded but the timer keeps ticking.
38
+ * - `stop()`: clears the timer. Idempotent.
39
+ * - `updateSettings(...)`: swap in new settings, re-starting if needed.
40
+ * - `forceUpdate()`: bypass the unchanged-IP check (used by `testReachability`).
41
+ */
42
+ export declare class DdnsUpdater {
43
+ private settings;
44
+ private readonly secrets;
45
+ private readonly getExternalIp;
46
+ private readonly fetchImpl;
47
+ private readonly resolveProvider;
48
+ private readonly nowFn;
49
+ private readonly setIntervalFn;
50
+ private readonly clearIntervalFn;
51
+ private timer;
52
+ private lastPushedIp;
53
+ private lastOutcome;
54
+ private started;
55
+ constructor(opts: DdnsUpdaterOptions);
56
+ /** Current status (for the NatService snapshot). */
57
+ getStatus(): NatDdnsStatus;
58
+ start(): Promise<DdnsUpdateOutcome>;
59
+ stop(): void;
60
+ /** Replace settings; restart the loop with the new configuration. */
61
+ updateSettings(next: NatDdnsSettings): Promise<DdnsUpdateOutcome>;
62
+ /** Force an immediate update, bypassing the unchanged-IP skip. */
63
+ forceUpdate(): Promise<DdnsUpdateOutcome>;
64
+ private shouldUpdate;
65
+ private scheduleNext;
66
+ private runOnce;
67
+ private loadConfig;
68
+ }