@serfab/cadre-host 1.13.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,445 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
- import { mkdtempSync, rmSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
-
6
- import type { CadreInvite } from '@serfab/cadre-core';
7
-
8
- import { TrustCircleService, createTrustCircleHandlers } from '../trust-circle.js';
9
- import { TrustCircleStore } from '../trust-circle-store.js';
10
- import { TrustCircleError } from '../types.js';
11
- import type { CadreNodeLike } from '../trust-circle.js';
12
- import { OwnerNodeUnavailableError } from '../../owner/owner-node-client.js';
13
-
14
- /**
15
- * Minimal in-memory mock of the owner-node channel adapter. Records calls
16
- * so assertions can verify the auth + redemption flow without standing up a
17
- * real libp2p control network or admin channel.
18
- */
19
- class MockCadreNode implements CadreNodeLike {
20
- /** CadrePeer rows — the real (authorized) membership test. */
21
- readonly authorizedPeers = new Set<string>();
22
- /**
23
- * Addressable-only rows — peers that published an address record without
24
- * ever being vouched for. Models the "stranger writes its own row" bug:
25
- * present in `listMembers`/`isMember` (addressable), absent from
26
- * `listAuthorizedMembers`/`isAuthorizedMember` (authorized).
27
- */
28
- readonly unauthorizedAddressablePeers = new Set<string>();
29
- readonly issuedInvites: Array<{ token?: string; expiresIn?: number }> = [];
30
- readonly accepted: Array<{ phonePeerId: string; token?: string }> = [];
31
- readonly removed: string[] = [];
32
-
33
- /** Toggle to make acceptPhone reject. */
34
- acceptShouldThrow: Error | null = null;
35
- /** When false, membership calls throw OwnerNodeUnavailableError. */
36
- nodeAvailable = true;
37
-
38
- async createInvite(token?: string, expiresIn?: number) {
39
- this.issuedInvites.push({ token, expiresIn });
40
- const invite: CadreInvite = {
41
- partyId: 'party-test',
42
- ownerAddrs: ['/ip4/127.0.0.1/tcp/4001'],
43
- token,
44
- createdAt: Date.now(),
45
- ...(expiresIn ? { expiresAt: Date.now() + expiresIn } : {}),
46
- };
47
- return { invite, encodedInvite: 'enc:' + JSON.stringify(invite) };
48
- }
49
-
50
- async acceptPhone(options: { phonePeerId: string; token?: string }, _issuedInvite?: CadreInvite) {
51
- if (this.acceptShouldThrow) throw this.acceptShouldThrow;
52
- this.accepted.push({ ...options });
53
- this.authorizedPeers.add(options.phonePeerId);
54
- }
55
-
56
- async removePeer(peerId: string) {
57
- this.removed.push(peerId);
58
- this.authorizedPeers.delete(peerId);
59
- this.unauthorizedAddressablePeers.delete(peerId);
60
- }
61
-
62
- encodeInvite(invite: CadreInvite): string {
63
- return 'enc:' + JSON.stringify(invite);
64
- }
65
-
66
- async listMembers(): Promise<Array<{ peerId: string; multiaddr: string | null }>> {
67
- if (!this.nodeAvailable) throw new OwnerNodeUnavailableError('node down');
68
- return [...this.authorizedPeers, ...this.unauthorizedAddressablePeers]
69
- .map((peerId) => ({ peerId, multiaddr: null }));
70
- }
71
-
72
- async isMember(peerId: string): Promise<boolean> {
73
- if (!this.nodeAvailable) throw new OwnerNodeUnavailableError('node down');
74
- return this.authorizedPeers.has(peerId) || this.unauthorizedAddressablePeers.has(peerId);
75
- }
76
-
77
- async listAuthorizedMembers(): Promise<Array<{ peerId: string; multiaddr: string | null }>> {
78
- if (!this.nodeAvailable) throw new OwnerNodeUnavailableError('node down');
79
- return [...this.authorizedPeers].map((peerId) => ({ peerId, multiaddr: null }));
80
- }
81
-
82
- async isAuthorizedMember(peerId: string): Promise<boolean> {
83
- if (!this.nodeAvailable) throw new OwnerNodeUnavailableError('node down');
84
- return this.authorizedPeers.has(peerId);
85
- }
86
- }
87
-
88
- let tmpRoot: string;
89
- let cadreNode: MockCadreNode;
90
- let store: TrustCircleStore;
91
- let service: TrustCircleService;
92
-
93
- beforeEach(() => {
94
- tmpRoot = mkdtempSync(join(tmpdir(), 'cadre-host-tc-svc-'));
95
- cadreNode = new MockCadreNode();
96
- store = new TrustCircleStore(tmpRoot);
97
- service = new TrustCircleService({ cadreNode, store });
98
- });
99
-
100
- afterEach(() => {
101
- try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
102
- });
103
-
104
- describe('TrustCircleService.issueInvite', () => {
105
- it('creates a pending row, returns an encoded invite, and uses default TTL', async () => {
106
- const result = await service.issueInvite({ label: "Mom's phone" });
107
-
108
- expect(result.token).toBeTruthy();
109
- expect(result.encodedInvite.startsWith('enc:')).toBe(true);
110
- expect(result.expiresAt).toBeInstanceOf(Date);
111
-
112
- const pending = store.getPending(result.token);
113
- expect(pending).toBeDefined();
114
- expect(pending!.label).toBe("Mom's phone");
115
- expect(pending!.expiresAt).toBe(result.expiresAt!.toISOString());
116
-
117
- expect(cadreNode.issuedInvites).toHaveLength(1);
118
- expect(cadreNode.issuedInvites[0]!.token).toBe(result.token);
119
- });
120
-
121
- it('rejects empty / whitespace labels', async () => {
122
- await expect(service.issueInvite({ label: ' ' })).rejects.toBeInstanceOf(TrustCircleError);
123
- await expect(service.issueInvite({ label: '' })).rejects.toBeInstanceOf(TrustCircleError);
124
- });
125
-
126
- it('honours custom ttl', async () => {
127
- const start = Date.now();
128
- const ttlMs = 5 * 60 * 1000;
129
- const result = await service.issueInvite({ label: 'Carol', ttlMs });
130
- const expiresMs = result.expiresAt!.getTime();
131
- expect(expiresMs - start).toBeGreaterThanOrEqual(ttlMs - 50);
132
- expect(expiresMs - start).toBeLessThanOrEqual(ttlMs + 500);
133
- });
134
-
135
- it('rejects non-positive ttl', async () => {
136
- await expect(service.issueInvite({ label: 'X', ttlMs: 0 })).rejects.toBeInstanceOf(TrustCircleError);
137
- await expect(service.issueInvite({ label: 'X', ttlMs: -1 })).rejects.toBeInstanceOf(TrustCircleError);
138
- });
139
- });
140
-
141
- describe('TrustCircleService.redeemInvite', () => {
142
- it('happy path: authorises peer, consumes pending, writes member', async () => {
143
- const { token } = await service.issueInvite({ label: "Mom's phone" });
144
-
145
- const result = await service.redeemInvite({ token, peerId: '12D3KooWMom' });
146
- expect(result).toEqual({ peerId: '12D3KooWMom', label: "Mom's phone" });
147
-
148
- expect(cadreNode.accepted).toHaveLength(1);
149
- expect(cadreNode.accepted[0]).toMatchObject({ phonePeerId: '12D3KooWMom', token });
150
- expect(cadreNode.authorizedPeers.has('12D3KooWMom')).toBe(true);
151
-
152
- expect(store.getPending(token)).toBeUndefined();
153
- expect(store.getMember('12D3KooWMom')?.label).toBe("Mom's phone");
154
- });
155
-
156
- it('rejects unknown token', async () => {
157
- await expect(service.redeemInvite({ token: 'nope', peerId: 'p1' }))
158
- .rejects.toMatchObject({ code: 'already_redeemed' });
159
- expect(cadreNode.accepted).toHaveLength(0);
160
- });
161
-
162
- it('rejects expired invite and reaps the pending row', async () => {
163
- let clock = new Date('2025-01-01T00:00:00Z');
164
- const svc = new TrustCircleService({ cadreNode, store, now: () => clock });
165
- const { token } = await svc.issueInvite({ label: 'Dad', ttlMs: 60_000 });
166
-
167
- // Move clock past expiry.
168
- clock = new Date('2025-01-02T00:00:00Z');
169
-
170
- await expect(svc.redeemInvite({ token, peerId: 'p1' }))
171
- .rejects.toMatchObject({ code: 'expired' });
172
- expect(store.getPending(token)).toBeUndefined();
173
- expect(cadreNode.accepted).toHaveLength(0);
174
- });
175
-
176
- it('double-redeem: second attempt is rejected because pending was consumed', async () => {
177
- const { token } = await service.issueInvite({ label: 'Carol' });
178
-
179
- await service.redeemInvite({ token, peerId: 'pA' });
180
- await expect(service.redeemInvite({ token, peerId: 'pB' }))
181
- .rejects.toMatchObject({ code: 'already_redeemed' });
182
-
183
- // The second attempt must NOT have authorised pB.
184
- expect(cadreNode.authorizedPeers.has('pB')).toBe(false);
185
- });
186
-
187
- it('requires both token and peerId', async () => {
188
- await expect(service.redeemInvite({ token: '', peerId: 'p1' }))
189
- .rejects.toBeInstanceOf(TrustCircleError);
190
- await expect(service.redeemInvite({ token: 't', peerId: '' }))
191
- .rejects.toBeInstanceOf(TrustCircleError);
192
- });
193
-
194
- it('claims pending atomically so a concurrent second call cannot also redeem', async () => {
195
- const { token } = await service.issueInvite({ label: 'Race' });
196
-
197
- // Block acceptPhone until we release it, simulating the async window
198
- // between getPending() and removePending() under naive ordering.
199
- let release!: () => void;
200
- const gate = new Promise<void>((r) => { release = r; });
201
- const originalAccept = cadreNode.acceptPhone.bind(cadreNode);
202
- cadreNode.acceptPhone = async (options, issued) => {
203
- await gate;
204
- await originalAccept(options, issued);
205
- };
206
-
207
- const first = service.redeemInvite({ token, peerId: 'pA' });
208
- const second = service.redeemInvite({ token, peerId: 'pB' });
209
-
210
- // The second call must be rejected synchronously (before the gate opens)
211
- // because the pending row was already claimed by `first`.
212
- await expect(second).rejects.toMatchObject({ code: 'already_redeemed' });
213
-
214
- release();
215
- await expect(first).resolves.toMatchObject({ peerId: 'pA' });
216
-
217
- // Only one peer was authorized.
218
- expect(cadreNode.authorizedPeers.has('pA')).toBe(true);
219
- expect(cadreNode.authorizedPeers.has('pB')).toBe(false);
220
- });
221
-
222
- it('preserves the pending token when acceptPhone fails with node_unavailable', async () => {
223
- const { token } = await service.issueInvite({ label: 'Transient' });
224
- cadreNode.acceptShouldThrow = new OwnerNodeUnavailableError('node down');
225
-
226
- await expect(service.redeemInvite({ token, peerId: 'pTrans' }))
227
- .rejects.toMatchObject({ code: 'node_unavailable' });
228
-
229
- // Token must still be redeemable.
230
- expect(store.getPending(token)).toBeDefined();
231
- expect(cadreNode.authorizedPeers.has('pTrans')).toBe(false);
232
-
233
- // Retry after the node recovers.
234
- cadreNode.acceptShouldThrow = null;
235
- await expect(service.redeemInvite({ token, peerId: 'pTrans' }))
236
- .resolves.toMatchObject({ peerId: 'pTrans', label: 'Transient' });
237
- expect(cadreNode.authorizedPeers.has('pTrans')).toBe(true);
238
- expect(store.getPending(token)).toBeUndefined();
239
- });
240
-
241
- it('concurrent redeem during a gated node_unavailable leaves the token re-redeemable', async () => {
242
- const { token } = await service.issueInvite({ label: 'GatedFail' });
243
-
244
- // Gate acceptPhone — reject on release with a transient error so the
245
- // first redeem hits node_unavailable. The second redeem should still
246
- // bounce synchronously while the gate is held.
247
- let release!: () => void;
248
- const gate = new Promise<void>((_, reject) => { release = () => reject(new OwnerNodeUnavailableError('node down')); });
249
- cadreNode.acceptPhone = async () => {
250
- await gate;
251
- };
252
-
253
- const first = service.redeemInvite({ token, peerId: 'pA' });
254
- const second = service.redeemInvite({ token, peerId: 'pB' });
255
-
256
- await expect(second).rejects.toMatchObject({ code: 'already_redeemed' });
257
-
258
- release();
259
- await expect(first).rejects.toMatchObject({ code: 'node_unavailable' });
260
-
261
- // Pending row preserved; in-flight Set cleared via finally.
262
- expect(store.getPending(token)).toBeDefined();
263
- expect(cadreNode.authorizedPeers.has('pA')).toBe(false);
264
- expect(cadreNode.authorizedPeers.has('pB')).toBe(false);
265
-
266
- // Third redeem (after in-flight slot freed) succeeds against a healthy node.
267
- cadreNode.acceptPhone = async (options) => {
268
- cadreNode.authorizedPeers.add(options.phonePeerId);
269
- };
270
- await expect(service.redeemInvite({ token, peerId: 'pC' }))
271
- .resolves.toMatchObject({ peerId: 'pC', label: 'GatedFail' });
272
- expect(store.getPending(token)).toBeUndefined();
273
- expect(cadreNode.authorizedPeers.has('pC')).toBe(true);
274
- });
275
-
276
- it('preserves the pending token on non-transient acceptPhone failures (admin must revoke)', async () => {
277
- const { token } = await service.issueInvite({ label: 'OtherFail' });
278
- cadreNode.acceptShouldThrow = new Error('cadre-core rejected the peer');
279
-
280
- await expect(service.redeemInvite({ token, peerId: 'pX' }))
281
- .rejects.toThrow('cadre-core rejected the peer');
282
-
283
- expect(store.getPending(token)).toBeDefined();
284
- expect(cadreNode.authorizedPeers.has('pX')).toBe(false);
285
- });
286
- });
287
-
288
- describe('TrustCircleService.issueInvite — label length', () => {
289
- it('rejects labels over 200 characters', async () => {
290
- await expect(service.issueInvite({ label: 'a'.repeat(201) }))
291
- .rejects.toMatchObject({ code: 'invalid_label' });
292
- });
293
-
294
- it('accepts labels at the 200-character limit', async () => {
295
- await expect(service.issueInvite({ label: 'a'.repeat(200) }))
296
- .resolves.toBeDefined();
297
- });
298
- });
299
-
300
- describe('TrustCircleService.revokePending', () => {
301
- it('removes a pending row and prevents subsequent redemption', async () => {
302
- const { token } = await service.issueInvite({ label: 'Eve' });
303
- await service.revokePending(token);
304
- expect(store.getPending(token)).toBeUndefined();
305
-
306
- await expect(service.redeemInvite({ token, peerId: 'p1' }))
307
- .rejects.toMatchObject({ code: 'already_redeemed' });
308
- });
309
-
310
- it('throws not_found for an unknown token', async () => {
311
- await expect(service.revokePending('nope'))
312
- .rejects.toMatchObject({ code: 'not_found' });
313
- });
314
- });
315
-
316
- describe('TrustCircleService.removeMember', () => {
317
- it('removes from CadrePeer and the local label store', async () => {
318
- const { token } = await service.issueInvite({ label: 'Frank' });
319
- await service.redeemInvite({ token, peerId: 'pF' });
320
-
321
- await service.removeMember('pF');
322
- expect(cadreNode.removed).toEqual(['pF']);
323
- expect(cadreNode.authorizedPeers.has('pF')).toBe(false);
324
- expect(store.getMember('pF')).toBeUndefined();
325
- });
326
-
327
- it('throws not_found for an unknown peer', async () => {
328
- await expect(service.removeMember('unknown'))
329
- .rejects.toMatchObject({ code: 'not_found' });
330
- });
331
-
332
- it('removes a control-only peer (CadrePeer row without a local label)', async () => {
333
- cadreNode.authorizedPeers.add('pCtrlOnly');
334
- expect(store.getMember('pCtrlOnly')).toBeUndefined();
335
-
336
- await service.removeMember('pCtrlOnly');
337
- expect(cadreNode.removed).toEqual(['pCtrlOnly']);
338
- expect(cadreNode.authorizedPeers.has('pCtrlOnly')).toBe(false);
339
- });
340
- });
341
-
342
- describe('TrustCircleService.isMember', () => {
343
- it('reads from the CadrePeer table', async () => {
344
- expect(await service.isMember('pX')).toBe(false);
345
- cadreNode.authorizedPeers.add('pX');
346
- expect(await service.isMember('pX')).toBe(true);
347
- });
348
- });
349
-
350
- describe('TrustCircleService.list', () => {
351
- it('joins CadrePeer with local labels and prunes orphan labels', async () => {
352
- // Pre-seed a label whose peer is NOT in CadrePeer — should be pruned.
353
- store.addMember({ peerId: 'orphan', label: 'ghost', addedAt: 't' });
354
-
355
- const r1 = await service.issueInvite({ label: 'Alice' });
356
- await service.redeemInvite({ token: r1.token, peerId: 'pA' });
357
- const r2 = await service.issueInvite({ label: 'Bob' });
358
- await service.redeemInvite({ token: r2.token, peerId: 'pB' });
359
- // A still-pending invite that should appear in `pending`.
360
- const r3 = await service.issueInvite({ label: 'Carol' });
361
-
362
- const snap = await service.list();
363
-
364
- expect(snap.members.map(m => m.peerId).sort()).toEqual(['pA', 'pB']);
365
- expect(snap.members.find(m => m.peerId === 'pA')?.label).toBe('Alice');
366
- expect(snap.members.find(m => m.peerId === 'pB')?.label).toBe('Bob');
367
- expect(snap.pending.map(p => p.token)).toContain(r3.token);
368
-
369
- expect(store.getMember('orphan')).toBeUndefined();
370
- });
371
-
372
- it('falls back to local labels when the owner node is unavailable', async () => {
373
- store.addMember({ peerId: 'pZ', label: 'Zoe', addedAt: 't' });
374
- cadreNode.nodeAvailable = false;
375
-
376
- const snap = await service.list();
377
- expect(snap.members.some(m => m.peerId === 'pZ' && m.label === 'Zoe')).toBe(true);
378
- // Orphan check is skipped when the node is unavailable.
379
- expect(store.getMember('pZ')).toBeDefined();
380
- });
381
-
382
- it('issueInvite surfaces node_unavailable when the channel is down', async () => {
383
- const downNode = new MockCadreNode();
384
- downNode.createInvite = async () => { throw new OwnerNodeUnavailableError('refused'); };
385
- const svc = new TrustCircleService({ cadreNode: downNode, store });
386
- await expect(svc.issueInvite({ label: 'Nina' }))
387
- .rejects.toMatchObject({ code: 'node_unavailable' });
388
- });
389
-
390
- it('shows unlabelled CadrePeer rows with peerId as label', async () => {
391
- cadreNode.authorizedPeers.add('unlabeled-peer');
392
- const snap = await service.list();
393
- const entry = snap.members.find(m => m.peerId === 'unlabeled-peer');
394
- expect(entry?.label).toBe('unlabeled-peer');
395
- });
396
-
397
- it('excludes addressable-only peers nobody authorized', async () => {
398
- cadreNode.authorizedPeers.add('invited-peer');
399
- cadreNode.unauthorizedAddressablePeers.add('stranger-peer');
400
-
401
- const snap = await service.list();
402
-
403
- expect(snap.members.map(m => m.peerId)).toContain('invited-peer');
404
- expect(snap.members.map(m => m.peerId)).not.toContain('stranger-peer');
405
- });
406
-
407
- it('keeps the self row even though the authorized set excludes self', async () => {
408
- store.addMember({ peerId: 'self-peer', label: 'This device', addedAt: 't', self: true });
409
- // Self is addressable (would show under listMembers) but never authorized.
410
-
411
- const snap = await service.list();
412
-
413
- const self = snap.members.find(m => m.peerId === 'self-peer');
414
- expect(self).toBeDefined();
415
- expect(self?.self).toBe(true);
416
- expect(self?.label).toBe('This device');
417
- // Not pruned as an orphan label despite being absent from the authorized set.
418
- expect(store.getMember('self-peer')).toBeDefined();
419
- });
420
- });
421
-
422
- describe('createTrustCircleHandlers', () => {
423
- it('exposes the HTTP-handler surface', async () => {
424
- const handlers = createTrustCircleHandlers(service);
425
-
426
- const issued = await handlers.postInvite({ label: 'Pat' });
427
- expect(issued.encodedInvite.startsWith('enc:')).toBe(true);
428
- expect(typeof issued.token).toBe('string');
429
- expect(issued.expiresAt).toBeTruthy();
430
-
431
- let snap = await handlers.listTrustCircle();
432
- expect(snap.pending.map(p => p.token)).toContain(issued.token);
433
- expect(snap.members).toEqual([]);
434
-
435
- await handlers.deleteInvite(issued.token);
436
- snap = await handlers.listTrustCircle();
437
- expect(snap.pending).toEqual([]);
438
- });
439
-
440
- it('postInvite rejects missing label', async () => {
441
- const handlers = createTrustCircleHandlers(service);
442
- await expect(handlers.postInvite({ label: undefined as unknown as string }))
443
- .rejects.toMatchObject({ code: 'invalid_label' });
444
- });
445
- });
@@ -1,33 +0,0 @@
1
- /**
2
- * Tiny duration parser for invite TTLs.
3
- *
4
- * Accepts a single value+unit token: `30s`, `5m`, `24h`, `7d`, `2w`.
5
- * Whitespace is allowed around it. Bare numbers are treated as milliseconds.
6
- *
7
- * Returns the duration in milliseconds. Throws on unparseable input.
8
- */
9
- const UNIT_MULTIPLIERS: Record<string, number> = {
10
- ms: 1,
11
- s: 1000,
12
- m: 60 * 1000,
13
- h: 60 * 60 * 1000,
14
- d: 24 * 60 * 60 * 1000,
15
- w: 7 * 24 * 60 * 60 * 1000,
16
- };
17
-
18
- const DURATION_RE = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d|w)?$/;
19
-
20
- export function parseDuration(input: string): number {
21
- const trimmed = input.trim();
22
- const match = DURATION_RE.exec(trimmed);
23
- if (!match) {
24
- throw new Error(`Invalid duration: ${JSON.stringify(input)} (expected e.g. "24h", "7d", "30m")`);
25
- }
26
- const value = parseFloat(match[1]!);
27
- const unit = match[2] ?? 'ms';
28
- const multiplier = UNIT_MULTIPLIERS[unit];
29
- if (multiplier === undefined) {
30
- throw new Error(`Unknown duration unit: ${unit}`);
31
- }
32
- return Math.round(value * multiplier);
33
- }
package/src/auth/index.ts DELETED
@@ -1,21 +0,0 @@
1
- /**
2
- * Trust-circle auth for cadre-host. See ./trust-circle.ts and ./types.ts.
3
- */
4
-
5
- export { TrustCircleService, createTrustCircleHandlers, DEFAULT_INVITE_TTL_MS } from './trust-circle.js';
6
- export type { TrustCircleServiceOptions, CadreNodeLike } from './trust-circle.js';
7
- export { TrustCircleStore } from './trust-circle-store.js';
8
- export { ensureSelfLabel, SELF_LABEL } from './self-label.js';
9
- export type { EnsureSelfLabelOptions } from './self-label.js';
10
- export { parseDuration } from './duration.js';
11
- export {
12
- TrustCircleError,
13
- } from './types.js';
14
- export type {
15
- TrustCircleMember,
16
- PendingInvite,
17
- TrustCircleFile,
18
- TrustCircleSnapshot,
19
- TrustCircleHandlers,
20
- TrustCircleErrorCode,
21
- } from './types.js';
@@ -1,74 +0,0 @@
1
- /**
2
- * Self-labelling for the trust-circle listing.
3
- *
4
- * `TrustCircleService.list()` shows the cadre's *authorized* membership, which
5
- * deliberately excludes the node's own self-published row — a node's own
6
- * address record isn't something an owner key vouched for. To keep the owner's
7
- * own device visible in the listing, cadre-host records a local
8
- * `self: true` label for it and `list()` splices that row back in.
9
- *
10
- * Writing that label is this module's job. Kept out of `bin/host.ts` so it is
11
- * unit-testable without standing up the CLI.
12
- */
13
-
14
- import debug from 'debug';
15
-
16
- import type { TrustCircleStore } from './trust-circle-store.js';
17
-
18
- const log = debug('cadre:host:trust-circle-self');
19
-
20
- /** Label written for the owner's own device when none exists yet. */
21
- export const SELF_LABEL = 'This device';
22
-
23
- export interface EnsureSelfLabelOptions {
24
- /** Local labels store to write into. */
25
- store: TrustCircleStore;
26
- /** Resolves the owner node's peer ID; '' when the node isn't ready yet. */
27
- getPeerId: () => Promise<string>;
28
- /** Clock override for tests. */
29
- now?: () => Date;
30
- }
31
-
32
- /**
33
- * Record the owner node's own peer ID as a `self: true` local label.
34
- *
35
- * Idempotent: an existing row for that peer ID is left alone, so an admin's
36
- * rename survives restarts. Stale `self` rows (a different peer ID — the node
37
- * identity was replaced) are dropped, since `list()` splices every `self` row
38
- * in unconditionally and would otherwise show a device that no longer exists.
39
- *
40
- * Returns the peer ID that is now labelled, or `undefined` when the node
41
- * wasn't ready. Throws whatever `getPeerId` throws — callers treat a failure
42
- * as best-effort (the next start heals it).
43
- */
44
- export async function ensureSelfLabel(opts: EnsureSelfLabelOptions): Promise<string | undefined> {
45
- const { store, getPeerId } = opts;
46
- const now = opts.now ?? (() => new Date());
47
-
48
- const peerId = await getPeerId();
49
- if (!peerId) {
50
- log('owner node not ready — no self label written');
51
- return undefined;
52
- }
53
-
54
- for (const member of store.listMembers()) {
55
- if (member.self && member.peerId !== peerId) {
56
- log('dropping stale self label for %s (current identity is %s)', member.peerId, peerId);
57
- store.removeMember(member.peerId);
58
- }
59
- }
60
-
61
- const existing = store.getMember(peerId);
62
- if (!existing) {
63
- store.addMember({ peerId, label: SELF_LABEL, addedAt: now().toISOString(), self: true });
64
- log('labelled own device %s as "%s"', peerId, SELF_LABEL);
65
- } else if (!existing.self) {
66
- // Row predates self-labelling (or was written by a redemption). Stamp the
67
- // flag, keeping the label: without it `list()` prunes the row, since the
68
- // authorized set never contains self.
69
- store.addMember({ ...existing, self: true });
70
- log('stamped existing label for own device %s as self', peerId);
71
- }
72
-
73
- return peerId;
74
- }