@serfab/cadre-host 1.12.0 → 1.14.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 (337) hide show
  1. package/README.md +332 -423
  2. package/dist/bin/host.d.ts +6 -3
  3. package/dist/bin/host.js +301 -505
  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 +49 -0
  9. package/dist/bin/nat-output.js +127 -0
  10. package/dist/bin/nat-output.js.map +1 -0
  11. package/dist/hosted/claim-details.d.ts +44 -0
  12. package/dist/hosted/claim-details.js +66 -0
  13. package/dist/hosted/claim-details.js.map +1 -0
  14. package/dist/hosted/hosted-node-service.d.ts +318 -0
  15. package/dist/hosted/hosted-node-service.js +666 -0
  16. package/dist/hosted/hosted-node-service.js.map +1 -0
  17. package/dist/hosted/hosted-node-store.d.ts +40 -0
  18. package/dist/hosted/hosted-node-store.js +126 -0
  19. package/dist/hosted/hosted-node-store.js.map +1 -0
  20. package/dist/{donation/donation-supervisor.d.ts → hosted/hosted-node-supervisor.d.ts} +70 -53
  21. package/dist/hosted/hosted-node-supervisor.js +396 -0
  22. package/dist/hosted/hosted-node-supervisor.js.map +1 -0
  23. package/dist/hosted/hosted-node-watcher.d.ts +83 -0
  24. package/dist/hosted/hosted-node-watcher.js +224 -0
  25. package/dist/hosted/hosted-node-watcher.js.map +1 -0
  26. package/dist/hosted/index.d.ts +16 -0
  27. package/dist/hosted/index.js +13 -0
  28. package/dist/hosted/index.js.map +1 -0
  29. package/dist/hosted/node-status.d.ts +35 -0
  30. package/dist/hosted/node-status.js +61 -0
  31. package/dist/hosted/node-status.js.map +1 -0
  32. package/dist/hosted/types.d.ts +134 -0
  33. package/dist/hosted/types.js +22 -0
  34. package/dist/hosted/types.js.map +1 -0
  35. package/dist/index.d.ts +12 -24
  36. package/dist/index.js +9 -19
  37. package/dist/index.js.map +1 -1
  38. package/dist/installer/config.d.ts +4 -34
  39. package/dist/installer/config.js +3 -18
  40. package/dist/installer/config.js.map +1 -1
  41. package/dist/installer/index.d.ts +7 -19
  42. package/dist/installer/index.js +14 -69
  43. package/dist/installer/index.js.map +1 -1
  44. package/dist/installer/paths.d.ts +0 -2
  45. package/dist/installer/paths.js +0 -4
  46. package/dist/installer/paths.js.map +1 -1
  47. package/dist/installer/wizard.d.ts +0 -7
  48. package/dist/installer/wizard.js +1 -8
  49. package/dist/installer/wizard.js.map +1 -1
  50. package/dist/nat/address-resolver.d.ts +18 -17
  51. package/dist/nat/address-resolver.js +79 -25
  52. package/dist/nat/address-resolver.js.map +1 -1
  53. package/dist/nat/address-watch.d.ts +37 -0
  54. package/dist/nat/address-watch.js +84 -0
  55. package/dist/nat/address-watch.js.map +1 -0
  56. package/dist/nat/ddns/duckdns.d.ts +2 -1
  57. package/dist/nat/ddns/duckdns.js +5 -1
  58. package/dist/nat/ddns/duckdns.js.map +1 -1
  59. package/dist/nat/ddns/index.d.ts +6 -0
  60. package/dist/nat/ddns/index.js.map +1 -1
  61. package/dist/nat/index.d.ts +12 -9
  62. package/dist/nat/index.js +6 -5
  63. package/dist/nat/index.js.map +1 -1
  64. package/dist/nat/nat-service.d.ts +203 -77
  65. package/dist/nat/nat-service.js +602 -237
  66. package/dist/nat/nat-service.js.map +1 -1
  67. package/dist/nat/nat-store.d.ts +12 -5
  68. package/dist/nat/nat-store.js +114 -30
  69. package/dist/nat/nat-store.js.map +1 -1
  70. package/dist/nat/port-mapper.d.ts +83 -79
  71. package/dist/nat/port-mapper.js +148 -189
  72. package/dist/nat/port-mapper.js.map +1 -1
  73. package/dist/nat/reachability.d.ts +31 -15
  74. package/dist/nat/reachability.js +97 -22
  75. package/dist/nat/reachability.js.map +1 -1
  76. package/dist/nat/types.d.ts +78 -27
  77. package/dist/nat/types.js +8 -5
  78. package/dist/nat/types.js.map +1 -1
  79. package/dist/orchestrator/host-process-orchestrator.d.ts +29 -71
  80. package/dist/orchestrator/host-process-orchestrator.js +78 -218
  81. package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
  82. package/dist/{installer/identity.d.ts → orchestrator/identity-file.d.ts} +5 -5
  83. package/dist/{installer/identity.js → orchestrator/identity-file.js} +6 -6
  84. package/dist/orchestrator/identity-file.js.map +1 -0
  85. package/dist/orchestrator/index.d.ts +2 -3
  86. package/dist/orchestrator/index.js +1 -1
  87. package/dist/orchestrator/index.js.map +1 -1
  88. package/dist/orchestrator/node-identity.d.ts +6 -6
  89. package/dist/orchestrator/node-identity.js +7 -7
  90. package/dist/orchestrator/node-identity.js.map +1 -1
  91. package/dist/orchestrator/port-allocator.d.ts +5 -6
  92. package/dist/orchestrator/port-allocator.js +9 -11
  93. package/dist/orchestrator/port-allocator.js.map +1 -1
  94. package/dist/orchestrator/state-store.d.ts +3 -5
  95. package/dist/orchestrator/state-store.js +2 -10
  96. package/dist/orchestrator/state-store.js.map +1 -1
  97. package/dist/orchestrator/types.d.ts +33 -22
  98. package/dist/orchestrator/types.js.map +1 -1
  99. package/dist/server/error-handler.d.ts +5 -25
  100. package/dist/server/error-handler.js +11 -74
  101. package/dist/server/error-handler.js.map +1 -1
  102. package/dist/server/events/types.d.ts +7 -11
  103. package/dist/server/events/types.js +2 -1
  104. package/dist/server/events/types.js.map +1 -1
  105. package/dist/server/index.d.ts +11 -37
  106. package/dist/server/index.js +30 -48
  107. package/dist/server/index.js.map +1 -1
  108. package/dist/server/routes/hosted-nodes.d.ts +28 -0
  109. package/dist/server/routes/hosted-nodes.js +68 -0
  110. package/dist/server/routes/hosted-nodes.js.map +1 -0
  111. package/dist/server/routes/nat.d.ts +4 -1
  112. package/dist/server/routes/nat.js +13 -2
  113. package/dist/server/routes/nat.js.map +1 -1
  114. package/dist/server/routes/nodes.d.ts +6 -20
  115. package/dist/server/routes/nodes.js +7 -86
  116. package/dist/server/routes/nodes.js.map +1 -1
  117. package/dist/server/routes/settings.d.ts +2 -7
  118. package/dist/server/routes/settings.js +3 -7
  119. package/dist/server/routes/settings.js.map +1 -1
  120. package/dist/server/routes/status.d.ts +2 -25
  121. package/dist/server/routes/status.js +1 -11
  122. package/dist/server/routes/status.js.map +1 -1
  123. package/dist/server/server.js +8 -8
  124. package/dist/server/settings-store.d.ts +2 -2
  125. package/dist/server/settings-store.js +2 -2
  126. package/dist/server/static.js +1 -2
  127. package/dist/server/static.js.map +1 -1
  128. package/dist/ui/assets/index-4FWuU8ye.js +19 -0
  129. package/dist/ui/assets/index-4FWuU8ye.js.map +1 -0
  130. package/dist/ui/assets/index-Di9Q4d01.css +1 -0
  131. package/dist/ui/index.html +2 -2
  132. package/dist/update/index.d.ts +2 -2
  133. package/dist/update/index.js +2 -2
  134. package/dist/update/store.d.ts +1 -1
  135. package/dist/update/store.js +1 -1
  136. package/dist/update/types.d.ts +1 -1
  137. package/dist/update/types.js +1 -1
  138. package/package.json +5 -6
  139. package/src/__tests__/cli-nat.smoke.test.ts +219 -212
  140. package/src/__tests__/cli.smoke.test.ts +1 -1
  141. package/src/__tests__/global-setup.ts +2 -3
  142. package/src/__tests__/orchestrator-claim-secret.test.ts +112 -0
  143. package/src/__tests__/orchestrator-handle-contract.ts +134 -134
  144. package/src/__tests__/orchestrator-node-identity.test.ts +182 -203
  145. package/src/__tests__/orchestrator-ports.test.ts +124 -143
  146. package/src/__tests__/orchestrator-push.test.ts +138 -146
  147. package/src/__tests__/orchestrator.test.ts +1019 -1063
  148. package/src/bin/host.ts +368 -553
  149. package/src/bin/join-output.ts +120 -0
  150. package/src/bin/nat-output.ts +177 -0
  151. package/src/{donation → hosted}/__tests__/fake-orchestrator.test.ts +173 -173
  152. package/src/{donation → hosted}/__tests__/fake-orchestrator.ts +265 -243
  153. package/src/hosted/__tests__/hosted-node-service.test.ts +609 -0
  154. package/src/hosted/__tests__/hosted-node-supervisor.test.ts +474 -0
  155. package/src/hosted/__tests__/test-invitation.ts +33 -0
  156. package/src/hosted/claim-details.ts +96 -0
  157. package/src/hosted/hosted-node-service.ts +795 -0
  158. package/src/hosted/hosted-node-store.ts +144 -0
  159. package/src/hosted/hosted-node-supervisor.ts +439 -0
  160. package/src/hosted/hosted-node-watcher.ts +250 -0
  161. package/src/hosted/index.ts +53 -0
  162. package/src/hosted/node-status.ts +71 -0
  163. package/src/hosted/types.ts +153 -0
  164. package/src/index.ts +72 -91
  165. package/src/installer/__tests__/config.test.ts +15 -40
  166. package/src/installer/__tests__/installer.smoke.test.ts +5 -86
  167. package/src/installer/__tests__/wizard.test.ts +56 -64
  168. package/src/installer/config.ts +135 -175
  169. package/src/installer/index.ts +15 -83
  170. package/src/installer/paths.ts +104 -109
  171. package/src/installer/wizard.ts +87 -103
  172. package/src/nat/__tests__/address-resolver.test.ts +107 -82
  173. package/src/nat/__tests__/nat-service.test.ts +433 -408
  174. package/src/nat/__tests__/nat-store.test.ts +127 -93
  175. package/src/nat/__tests__/port-mapper.test.ts +30 -191
  176. package/src/nat/__tests__/reachability.test.ts +96 -13
  177. package/src/nat/address-resolver.ts +94 -52
  178. package/src/nat/address-watch.ts +92 -0
  179. package/src/nat/ddns/duckdns.ts +5 -1
  180. package/src/nat/ddns/index.ts +6 -0
  181. package/src/nat/index.ts +87 -71
  182. package/src/nat/nat-service.ts +889 -501
  183. package/src/nat/nat-store.ts +237 -154
  184. package/src/nat/port-mapper.ts +241 -329
  185. package/src/nat/reachability.ts +134 -25
  186. package/src/nat/types.ts +209 -159
  187. package/src/{installer/__tests__/identity.test.ts → orchestrator/__tests__/identity-file.test.ts} +34 -34
  188. package/src/orchestrator/__tests__/node-identity.test.ts +62 -62
  189. package/src/orchestrator/host-process-orchestrator.ts +81 -243
  190. package/src/{installer/identity.ts → orchestrator/identity-file.ts} +56 -56
  191. package/src/orchestrator/index.ts +9 -10
  192. package/src/orchestrator/node-identity.ts +41 -41
  193. package/src/orchestrator/port-allocator.ts +71 -73
  194. package/src/orchestrator/state-store.ts +76 -90
  195. package/src/orchestrator/types.ts +34 -23
  196. package/src/server/__tests__/bus.test.ts +38 -38
  197. package/src/server/__tests__/error-handler.test.ts +81 -118
  198. package/src/server/__tests__/fakes.ts +40 -44
  199. package/src/server/__tests__/hosted-nodes-route.test.ts +160 -0
  200. package/src/server/__tests__/nodes-route.test.ts +114 -283
  201. package/src/server/__tests__/publishers.test.ts +193 -180
  202. package/src/server/__tests__/server.smoke.test.ts +134 -223
  203. package/src/server/__tests__/settings-route.test.ts +179 -195
  204. package/src/server/__tests__/sse-route.test.ts +119 -119
  205. package/src/server/__tests__/status-route.test.ts +82 -121
  206. package/src/server/error-handler.ts +112 -178
  207. package/src/server/events/types.ts +20 -24
  208. package/src/server/index.ts +223 -267
  209. package/src/server/routes/hosted-nodes.ts +83 -0
  210. package/src/server/routes/nat.ts +75 -63
  211. package/src/server/routes/nodes.ts +96 -188
  212. package/src/server/routes/settings.ts +115 -123
  213. package/src/server/routes/status.ts +102 -134
  214. package/src/server/server.ts +8 -8
  215. package/src/server/settings-store.ts +2 -2
  216. package/src/server/static.ts +1 -2
  217. package/src/update/index.ts +2 -2
  218. package/src/update/store.ts +1 -1
  219. package/src/update/types.ts +1 -1
  220. package/dist/auth/duration.d.ts +0 -1
  221. package/dist/auth/duration.js +0 -32
  222. package/dist/auth/duration.js.map +0 -1
  223. package/dist/auth/index.d.ts +0 -11
  224. package/dist/auth/index.js +0 -9
  225. package/dist/auth/index.js.map +0 -1
  226. package/dist/auth/self-label.d.ts +0 -36
  227. package/dist/auth/self-label.js +0 -57
  228. package/dist/auth/self-label.js.map +0 -1
  229. package/dist/auth/trust-circle-store.d.ts +0 -26
  230. package/dist/auth/trust-circle-store.js +0 -129
  231. package/dist/auth/trust-circle-store.js.map +0 -1
  232. package/dist/auth/trust-circle.d.ts +0 -156
  233. package/dist/auth/trust-circle.js +0 -307
  234. package/dist/auth/trust-circle.js.map +0 -1
  235. package/dist/auth/types.d.ts +0 -84
  236. package/dist/auth/types.js +0 -24
  237. package/dist/auth/types.js.map +0 -1
  238. package/dist/donation/donation-service.d.ts +0 -389
  239. package/dist/donation/donation-service.js +0 -828
  240. package/dist/donation/donation-service.js.map +0 -1
  241. package/dist/donation/donation-store.d.ts +0 -51
  242. package/dist/donation/donation-store.js +0 -152
  243. package/dist/donation/donation-store.js.map +0 -1
  244. package/dist/donation/donation-supervisor.js +0 -364
  245. package/dist/donation/donation-supervisor.js.map +0 -1
  246. package/dist/donation/grant-service.d.ts +0 -80
  247. package/dist/donation/grant-service.js +0 -210
  248. package/dist/donation/grant-service.js.map +0 -1
  249. package/dist/donation/grant-store.d.ts +0 -38
  250. package/dist/donation/grant-store.js +0 -135
  251. package/dist/donation/grant-store.js.map +0 -1
  252. package/dist/donation/index.d.ts +0 -20
  253. package/dist/donation/index.js +0 -17
  254. package/dist/donation/index.js.map +0 -1
  255. package/dist/donation/types.d.ts +0 -224
  256. package/dist/donation/types.js +0 -40
  257. package/dist/donation/types.js.map +0 -1
  258. package/dist/installer/identity.js.map +0 -1
  259. package/dist/owner/index.d.ts +0 -11
  260. package/dist/owner/index.js +0 -11
  261. package/dist/owner/index.js.map +0 -1
  262. package/dist/owner/owner-node-client.d.ts +0 -110
  263. package/dist/owner/owner-node-client.js +0 -184
  264. package/dist/owner/owner-node-client.js.map +0 -1
  265. package/dist/server/routes/bootstrap-node-validation.d.ts +0 -115
  266. package/dist/server/routes/bootstrap-node-validation.js +0 -197
  267. package/dist/server/routes/bootstrap-node-validation.js.map +0 -1
  268. package/dist/server/routes/grants-admin.d.ts +0 -29
  269. package/dist/server/routes/grants-admin.js +0 -58
  270. package/dist/server/routes/grants-admin.js.map +0 -1
  271. package/dist/server/routes/grants.d.ts +0 -28
  272. package/dist/server/routes/grants.js +0 -165
  273. package/dist/server/routes/grants.js.map +0 -1
  274. package/dist/server/routes/provision-request-validation.d.ts +0 -45
  275. package/dist/server/routes/provision-request-validation.js +0 -97
  276. package/dist/server/routes/provision-request-validation.js.map +0 -1
  277. package/dist/server/routes/strands.d.ts +0 -20
  278. package/dist/server/routes/strands.js +0 -38
  279. package/dist/server/routes/strands.js.map +0 -1
  280. package/dist/server/routes/trust-circle.d.ts +0 -17
  281. package/dist/server/routes/trust-circle.js +0 -35
  282. package/dist/server/routes/trust-circle.js.map +0 -1
  283. package/dist/strands/index.d.ts +0 -7
  284. package/dist/strands/index.js +0 -6
  285. package/dist/strands/index.js.map +0 -1
  286. package/dist/strands/strand-service.d.ts +0 -50
  287. package/dist/strands/strand-service.js +0 -103
  288. package/dist/strands/strand-service.js.map +0 -1
  289. package/dist/strands/types.d.ts +0 -93
  290. package/dist/strands/types.js +0 -23
  291. package/dist/strands/types.js.map +0 -1
  292. package/dist/ui/assets/index-C6nELNfC.js +0 -23
  293. package/dist/ui/assets/index-C6nELNfC.js.map +0 -1
  294. package/dist/ui/assets/index-TtIYSeZO.css +0 -1
  295. package/src/__tests__/cli-invite.smoke.test.ts +0 -192
  296. package/src/__tests__/host-owner.smoke.test.ts +0 -123
  297. package/src/__tests__/orchestrator-owner.test.ts +0 -234
  298. package/src/__tests__/orchestrator-pin-keys.test.ts +0 -152
  299. package/src/auth/__tests__/duration.test.ts +0 -28
  300. package/src/auth/__tests__/self-label.test.ts +0 -85
  301. package/src/auth/__tests__/trust-circle-integration.test.ts +0 -152
  302. package/src/auth/__tests__/trust-circle-store.test.ts +0 -115
  303. package/src/auth/__tests__/trust-circle.test.ts +0 -445
  304. package/src/auth/duration.ts +0 -33
  305. package/src/auth/index.ts +0 -21
  306. package/src/auth/self-label.ts +0 -74
  307. package/src/auth/trust-circle-store.ts +0 -155
  308. package/src/auth/trust-circle.ts +0 -395
  309. package/src/auth/types.ts +0 -99
  310. package/src/donation/__tests__/donation-service.test.ts +0 -1193
  311. package/src/donation/__tests__/donation-supervisor.test.ts +0 -548
  312. package/src/donation/__tests__/grant-service.test.ts +0 -213
  313. package/src/donation/__tests__/grant-store.test.ts +0 -113
  314. package/src/donation/donation-service.ts +0 -1033
  315. package/src/donation/donation-store.ts +0 -176
  316. package/src/donation/donation-supervisor.ts +0 -411
  317. package/src/donation/grant-service.ts +0 -252
  318. package/src/donation/grant-store.ts +0 -152
  319. package/src/donation/index.ts +0 -65
  320. package/src/donation/types.ts +0 -260
  321. package/src/owner/__tests__/owner-node-client.test.ts +0 -194
  322. package/src/owner/index.ts +0 -12
  323. package/src/owner/owner-node-client.ts +0 -259
  324. package/src/server/__tests__/bootstrap-node-validation.test.ts +0 -138
  325. package/src/server/__tests__/grants-admin-route.test.ts +0 -171
  326. package/src/server/__tests__/grants-route.test.ts +0 -394
  327. package/src/server/__tests__/strands-route.test.ts +0 -234
  328. package/src/server/routes/bootstrap-node-validation.ts +0 -206
  329. package/src/server/routes/grants-admin.ts +0 -74
  330. package/src/server/routes/grants.ts +0 -193
  331. package/src/server/routes/provision-request-validation.ts +0 -105
  332. package/src/server/routes/strands.ts +0 -54
  333. package/src/server/routes/trust-circle.ts +0 -49
  334. package/src/strands/__tests__/strand-service.test.ts +0 -165
  335. package/src/strands/index.ts +0 -14
  336. package/src/strands/strand-service.ts +0 -135
  337. package/src/strands/types.ts +0 -102
@@ -1,155 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
3
- import debug from 'debug';
4
-
5
- import type {
6
- PendingInvite,
7
- TrustCircleFile,
8
- TrustCircleMember,
9
- } from './types.js';
10
- import { TrustCircleError } from './types.js';
11
-
12
- const log = debug('cadre:host:trust-circle-store');
13
-
14
- const FILE_VERSION = 1;
15
-
16
- /**
17
- * Atomic JSON store for trust-circle.json. Mirrors the pattern in
18
- * `orchestrator/state-store.ts`: write to `<path>.tmp`, then rename.
19
- *
20
- * Concurrency assumption: only one cadre-host process owns a given rootDir
21
- * (the orchestrator already enforces this). No file-locking primitives.
22
- */
23
- export class TrustCircleStore {
24
- private readonly path: string;
25
- private cache: TrustCircleFile | null = null;
26
-
27
- constructor(rootDir: string) {
28
- mkdirSync(rootDir, { recursive: true });
29
- this.path = join(rootDir, 'trust-circle.json');
30
- }
31
-
32
- filePath(): string {
33
- return this.path;
34
- }
35
-
36
- /** Load (with caching). A missing file returns an empty store. */
37
- load(): TrustCircleFile {
38
- if (this.cache) return this.cache;
39
- if (!existsSync(this.path)) {
40
- this.cache = emptyFile();
41
- return this.cache;
42
- }
43
- let raw: string;
44
- try {
45
- raw = readFileSync(this.path, 'utf8');
46
- } catch (err) {
47
- throw new TrustCircleError(
48
- 'storage_error',
49
- `failed to read trust-circle file at ${this.path}: ${(err as Error).message}`,
50
- );
51
- }
52
- let parsed: unknown;
53
- try {
54
- parsed = JSON.parse(raw);
55
- } catch (err) {
56
- throw new TrustCircleError(
57
- 'storage_error',
58
- `trust-circle file at ${this.path} is not valid JSON: ${(err as Error).message}`,
59
- );
60
- }
61
- if (!isTrustCircleFile(parsed)) {
62
- throw new TrustCircleError(
63
- 'storage_error',
64
- `trust-circle file at ${this.path} has unexpected shape`,
65
- );
66
- }
67
- this.cache = parsed;
68
- return this.cache;
69
- }
70
-
71
- /** Persist the current state atomically (write then rename). */
72
- save(state: TrustCircleFile): void {
73
- mkdirSync(dirname(this.path), { recursive: true });
74
- const payload = JSON.stringify({ ...state, version: FILE_VERSION }, null, 2);
75
- const tmp = `${this.path}.tmp`;
76
- writeFileSync(tmp, payload, { encoding: 'utf8' });
77
- renameSync(tmp, this.path);
78
- this.cache = { ...state, version: FILE_VERSION };
79
- log('saved trust-circle (%d members, %d pending) to %s',
80
- Object.keys(state.members).length,
81
- Object.keys(state.pendingInvites).length,
82
- this.path);
83
- }
84
-
85
- // --- Member ops ---
86
-
87
- addMember(member: TrustCircleMember): void {
88
- const state = this.load();
89
- const { peerId, ...rest } = member;
90
- state.members[peerId] = rest;
91
- this.save(state);
92
- }
93
-
94
- removeMember(peerId: string): boolean {
95
- const state = this.load();
96
- if (!(peerId in state.members)) return false;
97
- delete state.members[peerId];
98
- this.save(state);
99
- return true;
100
- }
101
-
102
- getMember(peerId: string): TrustCircleMember | undefined {
103
- const state = this.load();
104
- const row = state.members[peerId];
105
- if (!row) return undefined;
106
- return { peerId, ...row };
107
- }
108
-
109
- listMembers(): TrustCircleMember[] {
110
- const state = this.load();
111
- return Object.entries(state.members).map(([peerId, row]) => ({ peerId, ...row }));
112
- }
113
-
114
- // --- Pending invite ops ---
115
-
116
- addPending(invite: PendingInvite): void {
117
- const state = this.load();
118
- const { token, ...rest } = invite;
119
- state.pendingInvites[token] = rest;
120
- this.save(state);
121
- }
122
-
123
- removePending(token: string): boolean {
124
- const state = this.load();
125
- if (!(token in state.pendingInvites)) return false;
126
- delete state.pendingInvites[token];
127
- this.save(state);
128
- return true;
129
- }
130
-
131
- getPending(token: string): PendingInvite | undefined {
132
- const state = this.load();
133
- const row = state.pendingInvites[token];
134
- if (!row) return undefined;
135
- return { token, ...row };
136
- }
137
-
138
- listPending(): PendingInvite[] {
139
- const state = this.load();
140
- return Object.entries(state.pendingInvites).map(([token, row]) => ({ token, ...row }));
141
- }
142
- }
143
-
144
- function emptyFile(): TrustCircleFile {
145
- return { version: FILE_VERSION, members: {}, pendingInvites: {} };
146
- }
147
-
148
- function isTrustCircleFile(v: unknown): v is TrustCircleFile {
149
- if (!v || typeof v !== 'object') return false;
150
- const obj = v as Record<string, unknown>;
151
- if (obj.version !== FILE_VERSION) return false;
152
- if (!obj.members || typeof obj.members !== 'object') return false;
153
- if (!obj.pendingInvites || typeof obj.pendingInvites !== 'object') return false;
154
- return true;
155
- }
@@ -1,395 +0,0 @@
1
- import { randomBytes } from 'node:crypto';
2
- import debug from 'debug';
3
-
4
- import type { CadreInvite } from '@serfab/cadre-core';
5
-
6
- import { OwnerNodeUnavailableError } from '../owner/owner-node-client.js';
7
-
8
- import type { TrustCircleStore } from './trust-circle-store.js';
9
- import type {
10
- PendingInvite,
11
- TrustCircleHandlers,
12
- TrustCircleMember,
13
- TrustCircleSnapshot,
14
- } from './types.js';
15
- import { TrustCircleError } from './types.js';
16
-
17
- const log = debug('cadre:host:trust-circle');
18
-
19
- /** Default invite lifetime: 24 hours. */
20
- export const DEFAULT_INVITE_TTL_MS = 24 * 60 * 60 * 1000;
21
-
22
- /** Token byte length (base64url-encoded; ~43 chars). */
23
- const TOKEN_BYTES = 32;
24
-
25
- /** Constructor options. */
26
- export interface TrustCircleServiceOptions {
27
- /** The host's cadre node — provides createInvite / acceptPhone / removePeer. */
28
- cadreNode: CadreNodeLike;
29
- /** Local labels + pending-invites store. */
30
- store: TrustCircleStore;
31
- /** Clock override for tests. */
32
- now?: () => Date;
33
- }
34
-
35
- /**
36
- * Minimal slice of CadreNode that the service uses. Defined as an interface
37
- * (rather than reaching into CadreNode directly) so unit tests can inject
38
- * a mock without standing up a real libp2p node.
39
- */
40
- export interface CadreNodeLike {
41
- createInvite(token?: string, expiresIn?: number): Promise<{ invite: CadreInvite; encodedInvite: string }>;
42
- acceptPhone(options: { phonePeerId: string; token?: string }, issuedInvite?: CadreInvite): Promise<void>;
43
- removePeer(peerId: string): Promise<void>;
44
- encodeInvite(invite: CadreInvite): string;
45
- /**
46
- * Enumerate the cadre's *addressable* peers (over the admin channel) —
47
- * anyone with an address record, including devices no owner vouched for.
48
- * Used only for {@link removeMember}, where "the row exists" is the point.
49
- */
50
- listMembers(): Promise<Array<{ peerId: string; multiaddr: string | null }>>;
51
- /** Probe whether a peer is *addressable* (over the admin channel). See {@link listMembers}. */
52
- isMember(peerId: string): Promise<boolean>;
53
- /**
54
- * Enumerate the cadre's *authorized* membership (over the admin channel) —
55
- * devices an owner key vouched for. Excludes the node's own self-published
56
- * row. This is the real membership test the trust-circle listing shows.
57
- */
58
- listAuthorizedMembers(): Promise<Array<{ peerId: string; multiaddr: string | null }>>;
59
- /** Probe whether a peer is *authorized*. See {@link listAuthorizedMembers}. */
60
- isAuthorizedMember(peerId: string): Promise<boolean>;
61
- }
62
-
63
- /**
64
- * TrustCircleService — orchestrates invite issuance, redemption, and
65
- * membership management.
66
- *
67
- * Membership is canonical in the cadre control DB (`CadrePeer` table),
68
- * reached over the owner node's admin channel; labels and pending tokens
69
- * live in the host-local `TrustCircleStore`. When the node is unreachable,
70
- * owner operations surface `node_unavailable` (→ 503) and `list()`
71
- * degrades to the local labels file.
72
- */
73
- export class TrustCircleService {
74
- private readonly cadreNode: CadreNodeLike;
75
- private readonly store: TrustCircleStore;
76
- private readonly now: () => Date;
77
- /**
78
- * In-flight redemption claims (by token). Serialises concurrent redeems
79
- * for the same token without burning the durable pending row before
80
- * `acceptPhone` succeeds. Intra-process only — the orchestrator enforces
81
- * single-process ownership of a given rootDir, so no file lock is needed.
82
- */
83
- private readonly inFlightRedemptions = new Set<string>();
84
-
85
- constructor(opts: TrustCircleServiceOptions) {
86
- this.cadreNode = opts.cadreNode;
87
- this.store = opts.store;
88
- this.now = opts.now ?? (() => new Date());
89
- }
90
-
91
- /**
92
- * Issue a new invite. Generates a token, persists a pending row, and
93
- * delegates to cadre-core to embed the host's owner addresses.
94
- */
95
- async issueInvite(opts: { label: string; ttlMs?: number }): Promise<{
96
- encodedInvite: string;
97
- invite: CadreInvite;
98
- token: string;
99
- expiresAt?: Date;
100
- }> {
101
- const label = normaliseLabel(opts.label);
102
- const ttlMs = opts.ttlMs ?? DEFAULT_INVITE_TTL_MS;
103
- if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
104
- throw new TrustCircleError('invalid_token', `Invalid ttlMs: ${ttlMs}`);
105
- }
106
-
107
- const token = generateToken();
108
- const created = this.now();
109
- const expiresAt = new Date(created.getTime() + ttlMs);
110
-
111
- const { invite, encodedInvite } = await this.cadreNode
112
- .createInvite(token, ttlMs)
113
- .catch((err) => this.toDomainError(err));
114
-
115
- const pending: PendingInvite = {
116
- token,
117
- label,
118
- createdAt: created.toISOString(),
119
- expiresAt: expiresAt.toISOString(),
120
- };
121
- this.store.addPending(pending);
122
-
123
- log('issued invite for "%s" (token=%s, expires=%s)', label, token, expiresAt.toISOString());
124
-
125
- return { encodedInvite, invite, token, expiresAt };
126
- }
127
-
128
- /**
129
- * Redeem an invite for an incoming peer ID. On success:
130
- * - cadre-core authorizes the peer (inserts CadrePeer row).
131
- * - The pending row is consumed (one-time enforcement).
132
- * - A labelled member row is written locally.
133
- *
134
- * Ordering: the synchronous *claim* against `inFlightRedemptions` serialises
135
- * concurrent redeems for the same token (second concurrent caller rejects
136
- * synchronously before its first await). The durable `removePending` only
137
- * runs *after* `acceptPhone` succeeds — so a transient `node_unavailable`
138
- * leaves the token re-redeemable rather than permanently burning it.
139
- *
140
- * Crash-safety tradeoff: if the host crashes between `acceptPhone` success
141
- * and the durable `removePending`, a retry will dial in fine but the
142
- * owner node's `acceptPhone` will reject the second `CadrePeer` insert
143
- * with a PK constraint error (cadre-core does not upsert). That surfaces to
144
- * the redeemer as a non-`node_unavailable` failure; the admin can revoke
145
- * the lingering pending row. This is a narrower window than the prior
146
- * behaviour, where *any* transient node outage burned the token.
147
- */
148
- async redeemInvite(opts: { token: string; peerId: string }): Promise<{ peerId: string; label: string }> {
149
- const { token, peerId } = opts;
150
- if (!token || !peerId) {
151
- throw new TrustCircleError('invalid_token', 'token and peerId are required');
152
- }
153
-
154
- const pending = this.store.getPending(token);
155
- if (!pending) {
156
- throw new TrustCircleError('already_redeemed', 'Invite not found or already redeemed');
157
- }
158
-
159
- if (pending.expiresAt) {
160
- const expiresAt = new Date(pending.expiresAt);
161
- if (Number.isFinite(expiresAt.getTime()) && this.now() > expiresAt) {
162
- // Expiry is permanent — reap the pending row durably.
163
- this.store.removePending(token);
164
- throw new TrustCircleError('expired', 'Invite has expired');
165
- }
166
- }
167
-
168
- // Synchronous in-memory claim. Closes the race between two concurrent
169
- // redemption requests for the same token — the loser sees the token in
170
- // the set and rejects synchronously before its first `await`.
171
- if (this.inFlightRedemptions.has(token)) {
172
- throw new TrustCircleError('already_redeemed', 'Invite not found or already redeemed');
173
- }
174
- this.inFlightRedemptions.add(token);
175
-
176
- try {
177
- // Reconstruct the CadreInvite that was originally issued. cadre-core's
178
- // acceptPhone validates `issuedInvite.token === options.token` (already
179
- // matches by construction) and re-checks expiration.
180
- const reconstructed: CadreInvite = {
181
- partyId: '',
182
- ownerAddrs: [],
183
- token: pending.token,
184
- createdAt: new Date(pending.createdAt).getTime(),
185
- ...(pending.expiresAt ? { expiresAt: new Date(pending.expiresAt).getTime() } : {}),
186
- };
187
-
188
- await this.cadreNode
189
- .acceptPhone({ phonePeerId: peerId, token }, reconstructed)
190
- .catch((err) => this.toDomainError(err));
191
-
192
- // acceptPhone succeeded → durably consume the pending row. If we crash
193
- // between here and the next line, the token stays pending (re-redeem
194
- // would fail at acceptPhone with a duplicate-CadrePeer error, not
195
- // node_unavailable, and the admin can revoke).
196
- this.store.removePending(token);
197
-
198
- // If the member write fails the peer is still authorized in CadrePeer —
199
- // the next call to list() will pick it up unlabeled, which is the
200
- // documented graceful degradation.
201
- const member: TrustCircleMember = {
202
- peerId,
203
- label: pending.label,
204
- addedAt: this.now().toISOString(),
205
- };
206
- this.store.addMember(member);
207
-
208
- log('redeemed invite for "%s" (peerId=%s, token=%s)', pending.label, peerId, token);
209
-
210
- return { peerId, label: pending.label };
211
- } finally {
212
- this.inFlightRedemptions.delete(token);
213
- }
214
- }
215
-
216
- /** Revoke a pending invite by token. No-op if not found. */
217
- async revokePending(token: string): Promise<void> {
218
- if (!this.store.removePending(token)) {
219
- throw new TrustCircleError('not_found', `Unknown pending invite: ${token}`);
220
- }
221
- log('revoked pending invite (token=%s)', token);
222
- }
223
-
224
- /**
225
- * Remove an authorised member. Deletes the CadrePeer row and the local
226
- * label. Throws not_found when the peer is in neither place.
227
- *
228
- * NOTE: no special case for the host's own peer ID — the local UI hides the
229
- * Remove button on the `self` row, but `DELETE /auth/members/<ownPeerId>`
230
- * (or `cadre-host trust revoke <ownPeerId>`) will delete the node's own
231
- * `CadrePeer` row. cadre-core re-registers self on node start, so a restart
232
- * heals it. If that stops being true, or the removal path grows a
233
- * non-loopback caller, guard `self` rows here.
234
- */
235
- async removeMember(peerId: string): Promise<void> {
236
- const inLocal = this.store.getMember(peerId);
237
- const inControl = await this.isMember(peerId);
238
-
239
- if (!inLocal && !inControl) {
240
- throw new TrustCircleError('not_found', `Unknown member: ${peerId}`);
241
- }
242
-
243
- if (inControl) {
244
- await this.cadreNode.removePeer(peerId).catch((err) => this.toDomainError(err));
245
- }
246
- if (inLocal) {
247
- this.store.removeMember(peerId);
248
- }
249
- log('removed member peerId=%s', peerId);
250
- }
251
-
252
- /** Whether the peer is present in the cadre's CadrePeer table. */
253
- async isMember(peerId: string): Promise<boolean> {
254
- try {
255
- return await this.cadreNode.isMember(peerId);
256
- } catch (err) {
257
- this.toDomainError(err);
258
- }
259
- }
260
-
261
- /**
262
- * UI snapshot. Joins the *authorized* membership (fetched over the admin
263
- * channel) with local labels; prunes any orphan labels (peer is no longer
264
- * authorised). Addressable-but-unauthorized peers — devices that merely
265
- * published an address record without an owner voucher — are deliberately
266
- * excluded; this is the listing an operator uses to decide who belongs.
267
- * The node's own self-published row is spliced back in from the local
268
- * label, since the authorized set excludes self by design. When the owner
269
- * node is unreachable, degrades to the local labels file so listing keeps
270
- * working while the node is down.
271
- */
272
- async list(): Promise<TrustCircleSnapshot> {
273
- const labels = new Map<string, Omit<TrustCircleMember, 'peerId'>>();
274
- for (const m of this.store.listMembers()) {
275
- labels.set(m.peerId, { label: m.label, addedAt: m.addedAt, ...(m.self ? { self: true } : {}) });
276
- }
277
-
278
- const members: TrustCircleMember[] = [];
279
-
280
- // Consult the control DB over the admin channel. A node-unavailable error
281
- // is non-fatal — fall back to the local labels rather than 503-ing a
282
- // read-only listing.
283
- let authorizedMembers: Array<{ peerId: string }> | null;
284
- try {
285
- authorizedMembers = await this.cadreNode.listAuthorizedMembers();
286
- } catch (err) {
287
- if (!(err instanceof OwnerNodeUnavailableError)) throw err;
288
- authorizedMembers = null;
289
- }
290
-
291
- if (authorizedMembers) {
292
- const seenPeerIds = new Set<string>();
293
- for (const { peerId } of authorizedMembers) {
294
- seenPeerIds.add(peerId);
295
- const label = labels.get(peerId);
296
- members.push({
297
- peerId,
298
- label: label?.label ?? peerId,
299
- addedAt: label?.addedAt ?? '',
300
- ...(label?.self ? { self: true } : {}),
301
- });
302
- }
303
- // The authorized set excludes the node's own self-published row —
304
- // splice it back in from the local label rather than dropping it.
305
- for (const [peerId, row] of labels) {
306
- if (row.self && !seenPeerIds.has(peerId)) {
307
- seenPeerIds.add(peerId);
308
- members.push({ peerId, ...row });
309
- }
310
- }
311
- // Prune labels for peers that are neither authorized nor self. Only
312
- // when we successfully consulted the node — otherwise a transient
313
- // outage would wipe labels.
314
- for (const peerId of labels.keys()) {
315
- if (!seenPeerIds.has(peerId)) {
316
- this.store.removeMember(peerId);
317
- }
318
- }
319
- } else {
320
- // Node down: return labels as-is (degradation; no pruning).
321
- for (const [peerId, row] of labels) {
322
- members.push({ peerId, ...row });
323
- }
324
- }
325
-
326
- const pending = this.store.listPending();
327
-
328
- // Sort for stable UI ordering.
329
- members.sort((a, b) => a.peerId.localeCompare(b.peerId));
330
- pending.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
331
-
332
- return { members, pending };
333
- }
334
-
335
- /**
336
- * Translate an admin-channel transport failure into a `node_unavailable`
337
- * TrustCircleError (→ 503). Re-throws anything else unchanged. Returns
338
- * `never` so callers can `.catch(err => this.toDomainError(err))`.
339
- */
340
- private toDomainError(err: unknown): never {
341
- if (err instanceof OwnerNodeUnavailableError) {
342
- throw new TrustCircleError('node_unavailable', `Owner node unavailable: ${err.message}`);
343
- }
344
- throw err instanceof Error ? err : new Error(String(err));
345
- }
346
- }
347
-
348
- /**
349
- * Wrap a TrustCircleService into the typed handler shape consumed by
350
- * `cadre-host-local-ui`. Errors propagate as TrustCircleError; the UI ticket
351
- * maps `.code` → HTTP status.
352
- */
353
- export function createTrustCircleHandlers(service: TrustCircleService): TrustCircleHandlers {
354
- return {
355
- async postInvite(body) {
356
- if (!body || typeof body.label !== 'string') {
357
- throw new TrustCircleError('invalid_label', 'label is required');
358
- }
359
- const result = await service.issueInvite({ label: body.label, ttlMs: body.ttlMs });
360
- return {
361
- encodedInvite: result.encodedInvite,
362
- token: result.token,
363
- ...(result.expiresAt ? { expiresAt: result.expiresAt.toISOString() } : {}),
364
- };
365
- },
366
- async deleteInvite(token) {
367
- await service.revokePending(token);
368
- },
369
- async deleteMember(peerId) {
370
- await service.removeMember(peerId);
371
- },
372
- async listTrustCircle() {
373
- return await service.list();
374
- },
375
- };
376
- }
377
-
378
- function generateToken(): string {
379
- // base64url so it survives in URLs, QR codes, and shell arguments.
380
- return randomBytes(TOKEN_BYTES).toString('base64url');
381
- }
382
-
383
- function normaliseLabel(label: unknown): string {
384
- if (typeof label !== 'string') {
385
- throw new TrustCircleError('invalid_label', 'label must be a string');
386
- }
387
- const trimmed = label.trim();
388
- if (trimmed.length === 0) {
389
- throw new TrustCircleError('invalid_label', 'label must not be empty');
390
- }
391
- if (trimmed.length > 200) {
392
- throw new TrustCircleError('invalid_label', 'label must be 200 characters or fewer');
393
- }
394
- return trimmed;
395
- }
package/src/auth/types.ts DELETED
@@ -1,99 +0,0 @@
1
- /**
2
- * Trust-circle types for cadre-host.
3
- *
4
- * The trust circle is the set of devices (peers) authorised to participate
5
- * in the host's cadre. Membership is stored canonically in the cadre control
6
- * network's `CadrePeer` table (via cadre-core). This module layers two pieces
7
- * of *host-local* state on top:
8
- *
9
- * - **Labels**: human-readable names ("Mom's phone") assigned by the host
10
- * admin. Display-only; loss just shows the bare peer ID.
11
- * - **Pending invites**: tokens that have been issued but not yet redeemed.
12
- * Operational state — lives on the issuing node, doesn't replicate.
13
- *
14
- * Both live in `<rootDir>/trust-circle.json`, written atomically.
15
- */
16
-
17
- /** An authorised member of the trust circle (matches a `CadrePeer` row). */
18
- export interface TrustCircleMember {
19
- /** libp2p peer ID. */
20
- peerId: string;
21
- /** Display label assigned by the host admin. */
22
- label: string;
23
- /** ISO timestamp when the peer was added. */
24
- addedAt: string;
25
- /** True if this row represents the host's own cadre node (self-enrolment). */
26
- self?: boolean;
27
- }
28
-
29
- /** An invite that has been issued but not yet redeemed. */
30
- export interface PendingInvite {
31
- /** Random token (base64url) used to match the redemption. */
32
- token: string;
33
- /** Display label that will be assigned to the redeemer on success. */
34
- label: string;
35
- /** ISO timestamp when the invite was issued. */
36
- createdAt: string;
37
- /** Optional ISO expiration timestamp. */
38
- expiresAt?: string;
39
- }
40
-
41
- /** On-disk shape of `trust-circle.json`. */
42
- export interface TrustCircleFile {
43
- version: 1;
44
- /** keyed by peer ID. */
45
- members: Record<string, Omit<TrustCircleMember, 'peerId'>>;
46
- /** keyed by token. */
47
- pendingInvites: Record<string, Omit<PendingInvite, 'token'>>;
48
- }
49
-
50
- /** Snapshot returned to the management UI. */
51
- export interface TrustCircleSnapshot {
52
- members: TrustCircleMember[];
53
- pending: PendingInvite[];
54
- }
55
-
56
- /**
57
- * Typed HTTP handlers exposed to `cadre-host-local-ui` for Fastify wiring.
58
- *
59
- * Handlers take typed objects and either return typed results or throw a
60
- * `TrustCircleError` whose `.code` the UI ticket maps to an HTTP status:
61
- *
62
- * - `not_found` → 404 (unknown token / unknown peer)
63
- * - `expired` → 410 (invite past expiresAt)
64
- * - `already_redeemed` → 409 (token was used and is gone)
65
- * - `node_unavailable` → 503 (owner node admin channel down/not ready)
66
- * - default → 500
67
- */
68
- export interface TrustCircleHandlers {
69
- postInvite(body: { label: string; ttlMs?: number }): Promise<{
70
- encodedInvite: string;
71
- token: string;
72
- expiresAt?: string;
73
- }>;
74
- deleteInvite(token: string): Promise<void>;
75
- deleteMember(peerId: string): Promise<void>;
76
- listTrustCircle(): Promise<TrustCircleSnapshot>;
77
- }
78
-
79
- /** Error codes thrown by TrustCircleService. */
80
- export type TrustCircleErrorCode =
81
- | 'not_found'
82
- | 'expired'
83
- | 'already_redeemed'
84
- | 'invalid_label'
85
- | 'invalid_token'
86
- | 'storage_error'
87
- /** The owner node's admin channel is unreachable / not ready. */
88
- | 'node_unavailable';
89
-
90
- /** Typed error carrying a stable `code` for HTTP mapping. */
91
- export class TrustCircleError extends Error {
92
- readonly code: TrustCircleErrorCode;
93
-
94
- constructor(code: TrustCircleErrorCode, message: string) {
95
- super(message);
96
- this.name = 'TrustCircleError';
97
- this.code = code;
98
- }
99
- }