@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,252 +0,0 @@
1
- import { randomBytes } from 'node:crypto';
2
- import debug from 'debug';
3
-
4
- import type { DonationService } from './donation-service.js';
5
- import { isLiveDonationStatus } from './donation-store.js';
6
- import type { GrantStore } from './grant-store.js';
7
- import type {
8
- DonationView,
9
- Grant,
10
- GrantAdminHandlers,
11
- GrantListing,
12
- GrantValidation,
13
- GrantValidator,
14
- } from './types.js';
15
- import { GrantError } from './types.js';
16
-
17
- const log = debug('cadre:host:grant-service');
18
-
19
- /** Token byte length (base64url-encoded; ~43 chars). Matches trust-circle. */
20
- const TOKEN_BYTES = 32;
21
-
22
- /** Default node cap when the admin doesn't specify one. */
23
- export const DEFAULT_MAX_NODES = 1;
24
-
25
- /** Constructor options. */
26
- export interface GrantServiceOptions {
27
- /** Persistent grant store (`grants.json`). */
28
- store: GrantStore;
29
- /** Clock override for tests. */
30
- now?: () => Date;
31
- }
32
-
33
- /**
34
- * GrantService — issues, validates, lists, and revokes donation grant tokens.
35
- *
36
- * It is the authority for a grant's *identity, expiry, and revocation*, and
37
- * for the per-grantee *node cap* (`maxNodes`). It is deliberately **not** the
38
- * authority for how many nodes are currently live under a grant — that tally
39
- * belongs to the donation service (`2-donation-service`), which persists the
40
- * grant→node records and can see terminations. `validateForProvision` takes
41
- * the count as a parameter so the two never drift.
42
- *
43
- * All methods are synchronous: issuance/validation/revocation are pure local
44
- * store operations with no node round-trip (unlike the trust-circle flow,
45
- * which delegates to the owner node).
46
- */
47
- export class GrantService implements GrantValidator {
48
- private readonly store: GrantStore;
49
- private readonly now: () => Date;
50
-
51
- constructor(opts: GrantServiceOptions) {
52
- this.store = opts.store;
53
- this.now = opts.now ?? (() => new Date());
54
- }
55
-
56
- /**
57
- * Issue a new grant. Generates a high-entropy base64url token, persists it,
58
- * and returns the whole `Grant` (the CLI/UI prints/QR-encodes the token).
59
- */
60
- issue(opts: { label: string; maxNodes?: number; ttlMs?: number }): Grant {
61
- const label = normaliseLabel(opts.label);
62
- const maxNodes = normaliseMaxNodes(opts.maxNodes);
63
-
64
- const created = this.now();
65
- const grant: Grant = {
66
- token: generateToken(),
67
- label,
68
- maxNodes,
69
- createdAt: created.toISOString(),
70
- };
71
-
72
- if (opts.ttlMs !== undefined) {
73
- if (!Number.isFinite(opts.ttlMs) || opts.ttlMs <= 0) {
74
- throw new GrantError('invalid_ttl', `Invalid ttlMs: ${opts.ttlMs}`);
75
- }
76
- grant.expiresAt = new Date(created.getTime() + opts.ttlMs).toISOString();
77
- }
78
-
79
- this.store.add(grant);
80
- log('issued grant "%s" (token=%s, maxNodes=%d, expires=%s)',
81
- label, grant.token, maxNodes, grant.expiresAt ?? 'never');
82
- return grant;
83
- }
84
-
85
- /**
86
- * Validate a presented bearer token for a non-provisioning op: it must name a
87
- * known grant that is neither revoked nor expired. No quota check. Never
88
- * throws for a bad token — returns a denial.
89
- */
90
- validate(token: string): GrantValidation {
91
- if (!token) return { ok: false, reason: 'unknown_token' };
92
- const grant = this.store.get(token);
93
- if (!grant) return { ok: false, reason: 'unknown_token' };
94
-
95
- // Revocation is an explicit admin action; check it before expiry so a
96
- // grant that is both revoked and expired reports the stronger signal.
97
- if (grant.revokedAt) return { ok: false, grant, reason: 'revoked' };
98
-
99
- if (grant.expiresAt) {
100
- const expiresAt = new Date(grant.expiresAt);
101
- if (Number.isFinite(expiresAt.getTime()) && this.now() > expiresAt) {
102
- return { ok: false, grant, reason: 'expired' };
103
- }
104
- }
105
-
106
- return { ok: true, grant };
107
- }
108
-
109
- /**
110
- * Validate for a *new* provision request. Runs the identity/expiry/revocation
111
- * checks of `validate`, then — only if those pass — consults the
112
- * caller-supplied live-node tally against the grant's `maxNodes`.
113
- *
114
- * Concurrency boundary: this is a pure function of the passed count. Two
115
- * provision requests racing at the quota edge (both seeing count = N-1)
116
- * would both pass here; serialising them so only one wins is the donation
117
- * service's responsibility (`2-donation-service` owns the grant→node lock).
118
- */
119
- validateForProvision(
120
- token: string,
121
- liveNodeCount: (token: string) => number,
122
- ): GrantValidation {
123
- const base = this.validate(token);
124
- if (!base.ok || !base.grant) return base;
125
-
126
- const live = liveNodeCount(token);
127
- if (live >= base.grant.maxNodes) {
128
- return { ok: false, grant: base.grant, reason: 'quota_exceeded' };
129
- }
130
- return { ok: true, grant: base.grant };
131
- }
132
-
133
- /**
134
- * Revoke a grant by token: mark it revoked (denies future requests).
135
- * Idempotent on an already-revoked grant; throws not_found when the token is
136
- * unknown.
137
- *
138
- * This only marks the grant. Tearing down the nodes already donated under it
139
- * is `DonationService.terminateGrant`, which the admin handler
140
- * ({@link createGrantAdminHandlers}) runs after this unless asked to keep them.
141
- */
142
- revoke(token: string): void {
143
- if (!this.store.markRevoked(token, this.now().toISOString())) {
144
- throw new GrantError('not_found', `Unknown grant: ${token}`);
145
- }
146
- log('revoked grant (token=%s)', token);
147
- }
148
-
149
- /** Enumerate all grants (for the CLI / UI). */
150
- list(): Grant[] {
151
- return this.store.list();
152
- }
153
- }
154
-
155
- /**
156
- * Wrap a GrantService into the typed handler shape consumed by the loopback
157
- * management server's `/grants-admin` routes. Errors propagate as GrantError /
158
- * DonationError; the server maps `.code` → HTTP status.
159
- *
160
- * `donations` is where revoke's node teardown, the single-donation terminate and
161
- * the listing's per-grant node counts come from. Without it there can be no
162
- * donated nodes, so revoke only marks the grant, every grant lists with no
163
- * nodes, and `terminateDonation` is left off.
164
- */
165
- export function createGrantAdminHandlers(
166
- service: GrantService,
167
- donations?: Pick<DonationService, 'terminate' | 'terminateGrant' | 'list'>,
168
- ): GrantAdminHandlers {
169
- const handlers: GrantAdminHandlers = {
170
- async postGrant(body) {
171
- if (!body || typeof body.label !== 'string') {
172
- throw new GrantError('invalid_label', 'label is required');
173
- }
174
- const grant = service.issue({
175
- label: body.label,
176
- ...(body.maxNodes !== undefined ? { maxNodes: body.maxNodes } : {}),
177
- ...(body.ttlMs !== undefined ? { ttlMs: body.ttlMs } : {}),
178
- });
179
- return { grant };
180
- },
181
- async listGrants() {
182
- const byGrant = groupByGrant(donations?.list() ?? []);
183
- return { grants: service.list().map((grant) => toListing(grant, byGrant.get(grant.token) ?? [])) };
184
- },
185
- async deleteGrant(token, { keepNodes }) {
186
- // Revoke first: it throws not_found for an unknown token before any
187
- // teardown, and a provision racing this call is then refused as revoked.
188
- service.revoke(token);
189
- // NOTE: the teardown runs inside the HTTP request, one stop + reclaim per
190
- // node in series. Fine for a handful of nodes per grant; if grants ever
191
- // carry many, answer after the revoke and tear down in the background.
192
- if (keepNodes || !donations) return { terminated: [] };
193
- return { terminated: await donations.terminateGrant(token) };
194
- },
195
- };
196
- if (donations) {
197
- handlers.terminateDonation = (id) => donations.terminate(id);
198
- }
199
- return handlers;
200
- }
201
-
202
- /** One read of the donation store, bucketed by grant token. */
203
- function groupByGrant(donations: DonationView[]): Map<string, DonationView[]> {
204
- const byGrant = new Map<string, DonationView[]>();
205
- for (const donation of donations) {
206
- const bucket = byGrant.get(donation.grantToken);
207
- if (bucket) bucket.push(donation);
208
- else byGrant.set(donation.grantToken, [donation]);
209
- }
210
- return byGrant;
211
- }
212
-
213
- /**
214
- * `liveNodes` uses the quota's own rule; `donations` is the set a revoke tears
215
- * down (every record not yet `terminated`, `error` included), so the admin can be
216
- * told the real count before confirming.
217
- */
218
- function toListing(grant: Grant, donations: DonationView[]): GrantListing {
219
- const unterminated = donations.filter((d) => d.status !== 'terminated');
220
- return {
221
- ...grant,
222
- liveNodes: unterminated.filter((d) => isLiveDonationStatus(d.status)).length,
223
- donations: unterminated.map(({ id, status }) => ({ id, status })),
224
- };
225
- }
226
-
227
- function generateToken(): string {
228
- // base64url so it survives in URLs, QR codes, and shell arguments.
229
- return randomBytes(TOKEN_BYTES).toString('base64url');
230
- }
231
-
232
- function normaliseLabel(label: unknown): string {
233
- if (typeof label !== 'string') {
234
- throw new GrantError('invalid_label', 'label must be a string');
235
- }
236
- const trimmed = label.trim();
237
- if (trimmed.length === 0) {
238
- throw new GrantError('invalid_label', 'label must not be empty');
239
- }
240
- if (trimmed.length > 200) {
241
- throw new GrantError('invalid_label', 'label must be 200 characters or fewer');
242
- }
243
- return trimmed;
244
- }
245
-
246
- function normaliseMaxNodes(maxNodes: unknown): number {
247
- if (maxNodes === undefined) return DEFAULT_MAX_NODES;
248
- if (typeof maxNodes !== 'number' || !Number.isInteger(maxNodes) || maxNodes < 1) {
249
- throw new GrantError('invalid_max_nodes', `maxNodes must be a positive integer (got ${JSON.stringify(maxNodes)})`);
250
- }
251
- return maxNodes;
252
- }
@@ -1,152 +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 { Grant, GrantFile } from './types.js';
6
- import { GrantError } from './types.js';
7
-
8
- const log = debug('cadre:host:grant-store');
9
-
10
- const FILE_VERSION = 1;
11
-
12
- /**
13
- * Atomic JSON store for `grants.json`. Modelled on `auth/trust-circle-store.ts`:
14
- * a single file, written to `<path>.tmp` then renamed, with an in-memory cache
15
- * keyed by token.
16
- *
17
- * Unlike the trust-circle pending rows, grants are **not** self-reaping: a
18
- * grant may legitimately outlive many nodes, so expiry is evaluated at
19
- * validate time (in `GrantService`) and the admin removes stale grants
20
- * explicitly. This store keeps no clock.
21
- *
22
- * Concurrency assumption: only one cadre-host process owns a given rootDir
23
- * (the orchestrator already enforces this). No file-locking primitives.
24
- */
25
- export class GrantStore {
26
- private readonly path: string;
27
- private cache: GrantFile | null = null;
28
-
29
- constructor(rootDir: string) {
30
- mkdirSync(rootDir, { recursive: true });
31
- this.path = join(rootDir, 'grants.json');
32
- }
33
-
34
- filePath(): string {
35
- return this.path;
36
- }
37
-
38
- /**
39
- * Load (with caching). A missing file returns an empty store (fresh
40
- * install). A present-but-malformed file throws — silently wiping it would
41
- * revoke every grantee at once.
42
- */
43
- load(): GrantFile {
44
- if (this.cache) return this.cache;
45
- if (!existsSync(this.path)) {
46
- this.cache = emptyFile();
47
- return this.cache;
48
- }
49
- let raw: string;
50
- try {
51
- raw = readFileSync(this.path, 'utf8');
52
- } catch (err) {
53
- throw new GrantError(
54
- 'storage_error',
55
- `failed to read grants file at ${this.path}: ${(err as Error).message}`,
56
- );
57
- }
58
- let parsed: unknown;
59
- try {
60
- parsed = JSON.parse(raw);
61
- } catch (err) {
62
- throw new GrantError(
63
- 'storage_error',
64
- `grants file at ${this.path} is not valid JSON: ${(err as Error).message}`,
65
- );
66
- }
67
- if (!isGrantFile(parsed)) {
68
- throw new GrantError(
69
- 'storage_error',
70
- `grants file at ${this.path} has unexpected shape`,
71
- );
72
- }
73
- this.cache = parsed;
74
- return this.cache;
75
- }
76
-
77
- /** Persist the current state atomically (write then rename). */
78
- save(state: GrantFile): void {
79
- // NOTE: rewrites the whole grants.json on every mutation. Fine at household
80
- // scale (a handful of grantees); if grant counts ever grow large, switch to
81
- // an append/compact log or per-token files.
82
- mkdirSync(dirname(this.path), { recursive: true });
83
- const payload = JSON.stringify({ ...state, version: FILE_VERSION }, null, 2);
84
- const tmp = `${this.path}.tmp`;
85
- try {
86
- writeFileSync(tmp, payload, { encoding: 'utf8' });
87
- renameSync(tmp, this.path);
88
- } catch (err) {
89
- // Surface as storage_error (→ 500) like load(), not a bare Error (→
90
- // "internal"); a write failure on issue/revoke should read consistently.
91
- throw new GrantError(
92
- 'storage_error',
93
- `failed to write grants file at ${this.path}: ${(err as Error).message}`,
94
- );
95
- }
96
- this.cache = { ...state, version: FILE_VERSION };
97
- log('saved grants (%d) to %s', Object.keys(state.grants).length, this.path);
98
- }
99
-
100
- add(grant: Grant): void {
101
- const state = this.load();
102
- const { token, ...rest } = grant;
103
- state.grants[token] = rest;
104
- this.save(state);
105
- }
106
-
107
- get(token: string): Grant | undefined {
108
- const state = this.load();
109
- const row = state.grants[token];
110
- if (!row) return undefined;
111
- return { token, ...row };
112
- }
113
-
114
- list(): Grant[] {
115
- const state = this.load();
116
- return Object.entries(state.grants).map(([token, row]) => ({ token, ...row }));
117
- }
118
-
119
- remove(token: string): boolean {
120
- const state = this.load();
121
- if (!(token in state.grants)) return false;
122
- delete state.grants[token];
123
- this.save(state);
124
- return true;
125
- }
126
-
127
- /**
128
- * Mark a grant revoked at the given ISO instant. Idempotent: an
129
- * already-revoked grant keeps its original `revokedAt`. Returns false when
130
- * the token is unknown.
131
- */
132
- markRevoked(token: string, at: string): boolean {
133
- const state = this.load();
134
- const row = state.grants[token];
135
- if (!row) return false;
136
- if (!row.revokedAt) row.revokedAt = at;
137
- this.save(state);
138
- return true;
139
- }
140
- }
141
-
142
- function emptyFile(): GrantFile {
143
- return { version: FILE_VERSION, grants: {} };
144
- }
145
-
146
- function isGrantFile(v: unknown): v is GrantFile {
147
- if (!v || typeof v !== 'object') return false;
148
- const obj = v as Record<string, unknown>;
149
- if (obj.version !== FILE_VERSION) return false;
150
- if (!obj.grants || typeof obj.grants !== 'object') return false;
151
- return true;
152
- }
@@ -1,65 +0,0 @@
1
- /**
2
- * Donation grant-token layer for cadre-host. See ./types.ts, ./grant-store.ts,
3
- * and ./grant-service.ts.
4
- *
5
- * This layer gates *who may ask this host to donate a node*. The lifecycle
6
- * service that consumes a validated grant to actually provision a node lands in
7
- * the `2-donation-service` ticket, which imports the `GrantValidator` interface
8
- * defined here.
9
- */
10
-
11
- export { GrantStore } from './grant-store.js';
12
- export {
13
- GrantService,
14
- createGrantAdminHandlers,
15
- DEFAULT_MAX_NODES,
16
- } from './grant-service.js';
17
- export type { GrantServiceOptions } from './grant-service.js';
18
- export { GrantError, DonationError } from './types.js';
19
- export type {
20
- Grant,
21
- GrantListing,
22
- GrantDenyReason,
23
- GrantValidation,
24
- GrantValidator,
25
- GrantFile,
26
- GrantAdminHandlers,
27
- GrantErrorCode,
28
- } from './types.js';
29
-
30
- /* ──────────────── donation lifecycle (2-donation-service) ──────────────── */
31
-
32
- export { DonationStore, isLiveDonationStatus } from './donation-store.js';
33
- export {
34
- DonationService,
35
- DONATION_AWAITING_SEED_TTL_MS,
36
- DONATION_PROVISIONING_TTL_MS,
37
- DONATION_REAP_SWEEP_MS,
38
- } from './donation-service.js';
39
- export type {
40
- DonationOrchestrator,
41
- DonationProvisionRequest,
42
- DonationSeedResult,
43
- DonationPeerInfo,
44
- DonationServiceOptions,
45
- RespawnResult,
46
- } from './donation-service.js';
47
- export {
48
- DonationSupervisor,
49
- DONATION_RESPAWN_BACKOFF_BASE_MS,
50
- DONATION_RESPAWN_BACKOFF_MAX_MS,
51
- DONATION_RESPAWN_MAX_ATTEMPTS,
52
- DONATION_RESPAWN_HEALTHY_MS,
53
- DONATION_RESPAWN_SWEEP_MS,
54
- } from './donation-supervisor.js';
55
- export type {
56
- DonationSupervisorOptions,
57
- SupervisedOrchestrator,
58
- } from './donation-supervisor.js';
59
- export type {
60
- Donation,
61
- DonationView,
62
- DonationStatus,
63
- DonationFile,
64
- DonationErrorCode,
65
- } from './types.js';