@serfab/cadre-host 0.7.1 → 0.9.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 (193) hide show
  1. package/README.md +296 -101
  2. package/dist/auth/trust-circle.d.ts +45 -7
  3. package/dist/auth/trust-circle.js +116 -59
  4. package/dist/auth/trust-circle.js.map +1 -1
  5. package/dist/auth/types.d.ts +4 -1
  6. package/dist/auth/types.js.map +1 -1
  7. package/dist/bin/host.js +426 -112
  8. package/dist/bin/host.js.map +1 -1
  9. package/dist/donation/donation-service.d.ts +136 -0
  10. package/dist/donation/donation-service.js +306 -0
  11. package/dist/donation/donation-service.js.map +1 -0
  12. package/dist/donation/donation-store.d.ts +42 -0
  13. package/dist/donation/donation-store.js +141 -0
  14. package/dist/donation/donation-store.js.map +1 -0
  15. package/dist/donation/grant-service.d.ts +71 -0
  16. package/dist/donation/grant-service.js +163 -0
  17. package/dist/donation/grant-service.js.map +1 -0
  18. package/dist/donation/grant-store.d.ts +38 -0
  19. package/dist/donation/grant-store.js +135 -0
  20. package/dist/donation/grant-store.js.map +1 -0
  21. package/dist/donation/index.d.ts +18 -0
  22. package/dist/donation/index.js +16 -0
  23. package/dist/donation/index.js.map +1 -0
  24. package/dist/donation/types.d.ts +175 -0
  25. package/dist/donation/types.js +40 -0
  26. package/dist/donation/types.js.map +1 -0
  27. package/dist/index.d.ts +12 -6
  28. package/dist/index.js +8 -3
  29. package/dist/index.js.map +1 -1
  30. package/dist/installer/browser.js +16 -17
  31. package/dist/installer/browser.js.map +1 -1
  32. package/dist/installer/config.d.ts +51 -0
  33. package/dist/installer/config.js +14 -1
  34. package/dist/installer/config.js.map +1 -1
  35. package/dist/installer/index.d.ts +7 -2
  36. package/dist/installer/index.js +17 -10
  37. package/dist/installer/index.js.map +1 -1
  38. package/dist/installer/service-host/launchd.js +1 -1
  39. package/dist/installer/service-host/launchd.js.map +1 -1
  40. package/dist/installer/service-host/systemd.js +1 -1
  41. package/dist/installer/service-host/systemd.js.map +1 -1
  42. package/dist/installer/wizard.d.ts +4 -0
  43. package/dist/installer/wizard.js +5 -1
  44. package/dist/installer/wizard.js.map +1 -1
  45. package/dist/nat/index.d.ts +1 -1
  46. package/dist/nat/nat-service.d.ts +45 -3
  47. package/dist/nat/nat-service.js +116 -3
  48. package/dist/nat/nat-service.js.map +1 -1
  49. package/dist/nat/secrets/index.d.ts +7 -0
  50. package/dist/nat/secrets/index.js +28 -2
  51. package/dist/nat/secrets/index.js.map +1 -1
  52. package/dist/nat/types.d.ts +3 -1
  53. package/dist/nat/types.js.map +1 -1
  54. package/dist/orchestrator/host-process-orchestrator.d.ts +68 -1
  55. package/dist/orchestrator/host-process-orchestrator.js +272 -39
  56. package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
  57. package/dist/orchestrator/index.d.ts +3 -2
  58. package/dist/orchestrator/index.js +1 -1
  59. package/dist/orchestrator/index.js.map +1 -1
  60. package/dist/orchestrator/state-store.d.ts +6 -5
  61. package/dist/orchestrator/state-store.js +10 -2
  62. package/dist/orchestrator/state-store.js.map +1 -1
  63. package/dist/orchestrator/types.d.ts +45 -10
  64. package/dist/orchestrator/types.js.map +1 -1
  65. package/dist/owner/index.d.ts +11 -0
  66. package/dist/owner/index.js +11 -0
  67. package/dist/owner/index.js.map +1 -0
  68. package/dist/owner/owner-node-client.d.ts +95 -0
  69. package/dist/owner/owner-node-client.js +167 -0
  70. package/dist/owner/owner-node-client.js.map +1 -0
  71. package/dist/push/index.d.ts +67 -0
  72. package/dist/push/index.js +101 -0
  73. package/dist/push/index.js.map +1 -0
  74. package/dist/server/error-handler.d.ts +17 -0
  75. package/dist/server/error-handler.js +45 -0
  76. package/dist/server/error-handler.js.map +1 -1
  77. package/dist/server/index.d.ts +26 -2
  78. package/dist/server/index.js +37 -16
  79. package/dist/server/index.js.map +1 -1
  80. package/dist/server/routes/grants-admin.d.ts +17 -0
  81. package/dist/server/routes/grants-admin.js +32 -0
  82. package/dist/server/routes/grants-admin.js.map +1 -0
  83. package/dist/server/routes/grants.d.ts +28 -0
  84. package/dist/server/routes/grants.js +163 -0
  85. package/dist/server/routes/grants.js.map +1 -0
  86. package/dist/server/routes/nodes.d.ts +5 -3
  87. package/dist/server/routes/nodes.js +39 -24
  88. package/dist/server/routes/nodes.js.map +1 -1
  89. package/dist/server/routes/settings.d.ts +6 -1
  90. package/dist/server/routes/settings.js +5 -2
  91. package/dist/server/routes/settings.js.map +1 -1
  92. package/dist/server/routes/status.d.ts +8 -4
  93. package/dist/server/routes/status.js +9 -4
  94. package/dist/server/routes/status.js.map +1 -1
  95. package/dist/server/static.d.ts +12 -0
  96. package/dist/server/static.js +24 -7
  97. package/dist/server/static.js.map +1 -1
  98. package/dist/ui/assets/index-BlMXQPSw.js +14 -0
  99. package/dist/ui/assets/{index-C9uChk2P.js.map → index-BlMXQPSw.js.map} +1 -1
  100. package/dist/ui/index.html +1 -1
  101. package/dist/update/apply.d.ts +23 -1
  102. package/dist/update/apply.js +46 -4
  103. package/dist/update/apply.js.map +1 -1
  104. package/dist/update/index.d.ts +14 -3
  105. package/dist/update/index.js +35 -2
  106. package/dist/update/index.js.map +1 -1
  107. package/dist/update/manifest.d.ts +28 -9
  108. package/dist/update/manifest.js +56 -29
  109. package/dist/update/manifest.js.map +1 -1
  110. package/dist/update/release-key.d.ts +18 -7
  111. package/dist/update/release-key.js +29 -14
  112. package/dist/update/release-key.js.map +1 -1
  113. package/dist/update/sign.d.ts +46 -0
  114. package/dist/update/sign.js +53 -0
  115. package/dist/update/sign.js.map +1 -0
  116. package/package.json +9 -4
  117. package/service/README.md +95 -86
  118. package/src/__tests__/cli-invite.smoke.test.ts +6 -1
  119. package/src/__tests__/cli-nat.smoke.test.ts +6 -1
  120. package/src/__tests__/cli.smoke.test.ts +6 -1
  121. package/src/__tests__/host-owner.smoke.test.ts +123 -0
  122. package/src/__tests__/orchestrator-owner.test.ts +198 -0
  123. package/src/__tests__/orchestrator-pin-keys.test.ts +153 -0
  124. package/src/__tests__/orchestrator-push.test.ts +145 -0
  125. package/src/auth/__tests__/trust-circle-integration.test.ts +44 -25
  126. package/src/auth/__tests__/trust-circle.test.ts +91 -24
  127. package/src/auth/trust-circle.ts +363 -304
  128. package/src/auth/types.ts +4 -1
  129. package/src/bin/host.ts +1258 -905
  130. package/src/donation/__tests__/donation-service.test.ts +290 -0
  131. package/src/donation/__tests__/grant-service.test.ts +213 -0
  132. package/src/donation/__tests__/grant-store.test.ts +113 -0
  133. package/src/donation/donation-service.ts +392 -0
  134. package/src/donation/donation-store.ts +164 -0
  135. package/src/donation/grant-service.ts +200 -0
  136. package/src/donation/grant-store.ts +152 -0
  137. package/src/donation/index.ts +49 -0
  138. package/src/donation/types.ts +220 -0
  139. package/src/index.ts +66 -3
  140. package/src/installer/__tests__/config.test.ts +27 -1
  141. package/src/installer/__tests__/installer.smoke.test.ts +94 -0
  142. package/src/installer/__tests__/wizard.test.ts +5 -1
  143. package/src/installer/browser.ts +40 -42
  144. package/src/installer/config.ts +61 -1
  145. package/src/installer/index.ts +23 -12
  146. package/src/installer/service-host/launchd.ts +1 -1
  147. package/src/installer/service-host/systemd.ts +1 -1
  148. package/src/installer/wizard.ts +12 -1
  149. package/src/nat/__tests__/ddns-updater.test.ts +1 -1
  150. package/src/nat/__tests__/nat-service.test.ts +192 -0
  151. package/src/nat/__tests__/port-mapper.test.ts +1 -1
  152. package/src/nat/__tests__/secrets.test.ts +23 -1
  153. package/src/nat/index.ts +1 -1
  154. package/src/nat/nat-service.ts +131 -5
  155. package/src/nat/secrets/index.ts +32 -2
  156. package/src/nat/types.ts +3 -1
  157. package/src/orchestrator/host-process-orchestrator.ts +322 -54
  158. package/src/orchestrator/index.ts +9 -2
  159. package/src/orchestrator/state-store.ts +21 -3
  160. package/src/orchestrator/types.ts +48 -2
  161. package/src/owner/__tests__/owner-node-client.test.ts +194 -0
  162. package/src/owner/index.ts +12 -0
  163. package/src/owner/owner-node-client.ts +233 -0
  164. package/src/push/__tests__/push.test.ts +90 -0
  165. package/src/push/index.ts +146 -0
  166. package/src/server/__tests__/error-handler.test.ts +26 -4
  167. package/src/server/__tests__/grants-admin-route.test.ts +73 -0
  168. package/src/server/__tests__/grants-route.test.ts +189 -0
  169. package/src/server/__tests__/nodes-route.test.ts +90 -10
  170. package/src/server/__tests__/publishers.test.ts +1 -1
  171. package/src/server/__tests__/server.smoke.test.ts +75 -0
  172. package/src/server/__tests__/settings-route.test.ts +17 -0
  173. package/src/server/__tests__/static.test.ts +145 -0
  174. package/src/server/__tests__/status-route.test.ts +23 -2
  175. package/src/server/error-handler.ts +47 -0
  176. package/src/server/index.ts +62 -17
  177. package/src/server/routes/grants-admin.ts +41 -0
  178. package/src/server/routes/grants.ts +186 -0
  179. package/src/server/routes/nodes.ts +49 -26
  180. package/src/server/routes/settings.ts +11 -3
  181. package/src/server/routes/status.ts +18 -8
  182. package/src/server/static.ts +24 -8
  183. package/src/update/__tests__/apply.test.ts +58 -1
  184. package/src/update/__tests__/manifest.test.ts +41 -6
  185. package/src/update/__tests__/release-key.test.ts +42 -0
  186. package/src/update/__tests__/sign.test.ts +78 -0
  187. package/src/update/__tests__/update-service.integration.test.ts +44 -9
  188. package/src/update/apply.ts +46 -4
  189. package/src/update/index.ts +41 -3
  190. package/src/update/manifest.ts +214 -172
  191. package/src/update/release-key.ts +31 -15
  192. package/src/update/sign.ts +74 -0
  193. package/dist/ui/assets/index-C9uChk2P.js +0 -14
@@ -0,0 +1,42 @@
1
+ import type { Donation, DonationFile } from './types.js';
2
+ /**
3
+ * Atomic JSON store for `donations.json` — one row per donated node. Modelled on
4
+ * `grant-store.ts`: a single file written to `<path>.tmp` then renamed, with an
5
+ * in-memory cache keyed by donation id.
6
+ *
7
+ * **Persists `seedToken`.** Unlike the orchestrator handle (whose `toPersisted`
8
+ * deliberately omits the freshly-minted-per-spawn seed token), this record keeps
9
+ * it: if the host restarts in the request→seed gap, only the donation row can
10
+ * reproduce the bearer needed to present the seed to the node. See
11
+ * `tickets/complete/cadre-provider-seed-endpoint-never-populated.md`.
12
+ *
13
+ * Concurrency assumption: only one cadre-host process owns a given rootDir
14
+ * (the orchestrator already enforces this). No file-locking primitives.
15
+ */
16
+ export declare class DonationStore {
17
+ private readonly path;
18
+ private cache;
19
+ constructor(rootDir: string);
20
+ filePath(): string;
21
+ /**
22
+ * Load (with caching). A missing file returns an empty store. A
23
+ * present-but-malformed file throws rather than silently wiping — losing the
24
+ * records would orphan live child processes (their seed tokens gone).
25
+ */
26
+ load(): DonationFile;
27
+ /** Persist the current state atomically (write then rename). */
28
+ save(state: DonationFile): void;
29
+ /** Insert or replace a donation row. */
30
+ put(donation: Donation): void;
31
+ get(id: string): Donation | undefined;
32
+ list(): Donation[];
33
+ /** All donations authorized by a given grant token. */
34
+ listByGrant(grantToken: string): Donation[];
35
+ remove(id: string): boolean;
36
+ /**
37
+ * The authoritative live-node tally the grant validator consumes: donations
38
+ * under `grantToken` in a live status (`provisioning` / `awaiting_seed` /
39
+ * `seeded`). `error` and `terminated` rows are excluded.
40
+ */
41
+ liveNodeCount(grantToken: string): number;
42
+ }
@@ -0,0 +1,141 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import debug from 'debug';
4
+ import { DonationError } from './types.js';
5
+ const log = debug('cadre:host:donation-store');
6
+ const FILE_VERSION = 1;
7
+ /**
8
+ * Statuses that count a donation as "live" against a grant's `maxNodes` quota.
9
+ * `error` and `terminated` donations are kept for audit but do NOT count.
10
+ */
11
+ const LIVE_STATUSES = new Set([
12
+ 'provisioning',
13
+ 'awaiting_seed',
14
+ 'seeded',
15
+ ]);
16
+ /**
17
+ * Atomic JSON store for `donations.json` — one row per donated node. Modelled on
18
+ * `grant-store.ts`: a single file written to `<path>.tmp` then renamed, with an
19
+ * in-memory cache keyed by donation id.
20
+ *
21
+ * **Persists `seedToken`.** Unlike the orchestrator handle (whose `toPersisted`
22
+ * deliberately omits the freshly-minted-per-spawn seed token), this record keeps
23
+ * it: if the host restarts in the request→seed gap, only the donation row can
24
+ * reproduce the bearer needed to present the seed to the node. See
25
+ * `tickets/complete/cadre-provider-seed-endpoint-never-populated.md`.
26
+ *
27
+ * Concurrency assumption: only one cadre-host process owns a given rootDir
28
+ * (the orchestrator already enforces this). No file-locking primitives.
29
+ */
30
+ export class DonationStore {
31
+ constructor(rootDir) {
32
+ this.cache = null;
33
+ mkdirSync(rootDir, { recursive: true });
34
+ this.path = join(rootDir, 'donations.json');
35
+ }
36
+ filePath() {
37
+ return this.path;
38
+ }
39
+ /**
40
+ * Load (with caching). A missing file returns an empty store. A
41
+ * present-but-malformed file throws rather than silently wiping — losing the
42
+ * records would orphan live child processes (their seed tokens gone).
43
+ */
44
+ load() {
45
+ if (this.cache)
46
+ return this.cache;
47
+ if (!existsSync(this.path)) {
48
+ this.cache = emptyFile();
49
+ return this.cache;
50
+ }
51
+ let raw;
52
+ try {
53
+ raw = readFileSync(this.path, 'utf8');
54
+ }
55
+ catch (err) {
56
+ throw new DonationError('storage_error', `failed to read donations file at ${this.path}: ${err.message}`);
57
+ }
58
+ let parsed;
59
+ try {
60
+ parsed = JSON.parse(raw);
61
+ }
62
+ catch (err) {
63
+ throw new DonationError('storage_error', `donations file at ${this.path} is not valid JSON: ${err.message}`);
64
+ }
65
+ if (!isDonationFile(parsed)) {
66
+ throw new DonationError('storage_error', `donations file at ${this.path} has unexpected shape`);
67
+ }
68
+ this.cache = parsed;
69
+ return this.cache;
70
+ }
71
+ /** Persist the current state atomically (write then rename). */
72
+ save(state) {
73
+ // NOTE: rewrites the whole donations.json on every mutation. Fine at
74
+ // household scale (a handful of donated nodes); if counts ever grow large,
75
+ // switch to an append/compact log or per-id files.
76
+ mkdirSync(dirname(this.path), { recursive: true });
77
+ const payload = JSON.stringify({ ...state, version: FILE_VERSION }, null, 2);
78
+ const tmp = `${this.path}.tmp`;
79
+ try {
80
+ writeFileSync(tmp, payload, { encoding: 'utf8' });
81
+ renameSync(tmp, this.path);
82
+ }
83
+ catch (err) {
84
+ throw new DonationError('storage_error', `failed to write donations file at ${this.path}: ${err.message}`);
85
+ }
86
+ this.cache = { ...state, version: FILE_VERSION };
87
+ log('saved donations (%d) to %s', Object.keys(state.donations).length, this.path);
88
+ }
89
+ /** Insert or replace a donation row. */
90
+ put(donation) {
91
+ const state = this.load();
92
+ const { id, ...rest } = donation;
93
+ state.donations[id] = rest;
94
+ this.save(state);
95
+ }
96
+ get(id) {
97
+ const state = this.load();
98
+ const row = state.donations[id];
99
+ if (!row)
100
+ return undefined;
101
+ return { id, ...row };
102
+ }
103
+ list() {
104
+ const state = this.load();
105
+ return Object.entries(state.donations).map(([id, row]) => ({ id, ...row }));
106
+ }
107
+ /** All donations authorized by a given grant token. */
108
+ listByGrant(grantToken) {
109
+ return this.list().filter((d) => d.grantToken === grantToken);
110
+ }
111
+ remove(id) {
112
+ const state = this.load();
113
+ if (!(id in state.donations))
114
+ return false;
115
+ delete state.donations[id];
116
+ this.save(state);
117
+ return true;
118
+ }
119
+ /**
120
+ * The authoritative live-node tally the grant validator consumes: donations
121
+ * under `grantToken` in a live status (`provisioning` / `awaiting_seed` /
122
+ * `seeded`). `error` and `terminated` rows are excluded.
123
+ */
124
+ liveNodeCount(grantToken) {
125
+ return this.list().filter((d) => d.grantToken === grantToken && LIVE_STATUSES.has(d.status)).length;
126
+ }
127
+ }
128
+ function emptyFile() {
129
+ return { version: FILE_VERSION, donations: {} };
130
+ }
131
+ function isDonationFile(v) {
132
+ if (!v || typeof v !== 'object')
133
+ return false;
134
+ const obj = v;
135
+ if (obj.version !== FILE_VERSION)
136
+ return false;
137
+ if (!obj.donations || typeof obj.donations !== 'object')
138
+ return false;
139
+ return true;
140
+ }
141
+ //# sourceMappingURL=donation-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"donation-store.js","sourceRoot":"","sources":["../../src/donation/donation-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,71 @@
1
+ import type { GrantStore } from './grant-store.js';
2
+ import type { Grant, GrantAdminHandlers, GrantValidation, GrantValidator } from './types.js';
3
+ /** Default node cap when the admin doesn't specify one. */
4
+ export declare const DEFAULT_MAX_NODES = 1;
5
+ /** Constructor options. */
6
+ export interface GrantServiceOptions {
7
+ /** Persistent grant store (`grants.json`). */
8
+ store: GrantStore;
9
+ /** Clock override for tests. */
10
+ now?: () => Date;
11
+ }
12
+ /**
13
+ * GrantService — issues, validates, lists, and revokes donation grant tokens.
14
+ *
15
+ * It is the authority for a grant's *identity, expiry, and revocation*, and
16
+ * for the per-grantee *node cap* (`maxNodes`). It is deliberately **not** the
17
+ * authority for how many nodes are currently live under a grant — that tally
18
+ * belongs to the donation service (`2-donation-service`), which persists the
19
+ * grant→node records and can see terminations. `validateForProvision` takes
20
+ * the count as a parameter so the two never drift.
21
+ *
22
+ * All methods are synchronous: issuance/validation/revocation are pure local
23
+ * store operations with no node round-trip (unlike the trust-circle flow,
24
+ * which delegates to the owner node).
25
+ */
26
+ export declare class GrantService implements GrantValidator {
27
+ private readonly store;
28
+ private readonly now;
29
+ constructor(opts: GrantServiceOptions);
30
+ /**
31
+ * Issue a new grant. Generates a high-entropy base64url token, persists it,
32
+ * and returns the whole `Grant` (the CLI/UI prints/QR-encodes the token).
33
+ */
34
+ issue(opts: {
35
+ label: string;
36
+ maxNodes?: number;
37
+ ttlMs?: number;
38
+ }): Grant;
39
+ /**
40
+ * Validate a presented bearer token for a non-provisioning op: it must name a
41
+ * known grant that is neither revoked nor expired. No quota check. Never
42
+ * throws for a bad token — returns a denial.
43
+ */
44
+ validate(token: string): GrantValidation;
45
+ /**
46
+ * Validate for a *new* provision request. Runs the identity/expiry/revocation
47
+ * checks of `validate`, then — only if those pass — consults the
48
+ * caller-supplied live-node tally against the grant's `maxNodes`.
49
+ *
50
+ * Concurrency boundary: this is a pure function of the passed count. Two
51
+ * provision requests racing at the quota edge (both seeing count = N-1)
52
+ * would both pass here; serialising them so only one wins is the donation
53
+ * service's responsibility (`2-donation-service` owns the grant→node lock).
54
+ */
55
+ validateForProvision(token: string, liveNodeCount: (token: string) => number): GrantValidation;
56
+ /**
57
+ * Revoke a grant by token: mark it revoked (denies future requests). Existing
58
+ * live nodes are **not** torn down here — that is a separate admin action via
59
+ * the donation service's terminate (`2-donation-service`). Throws not_found
60
+ * when the token is unknown.
61
+ */
62
+ revoke(token: string): void;
63
+ /** Enumerate all grants (for the CLI / UI). */
64
+ list(): Grant[];
65
+ }
66
+ /**
67
+ * Wrap a GrantService into the typed handler shape consumed by the loopback
68
+ * management server's `/grants-admin` routes. Errors propagate as GrantError;
69
+ * the server maps `.code` → HTTP status.
70
+ */
71
+ export declare function createGrantAdminHandlers(service: GrantService): GrantAdminHandlers;
@@ -0,0 +1,163 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import debug from 'debug';
3
+ import { GrantError } from './types.js';
4
+ const log = debug('cadre:host:grant-service');
5
+ /** Token byte length (base64url-encoded; ~43 chars). Matches trust-circle. */
6
+ const TOKEN_BYTES = 32;
7
+ /** Default node cap when the admin doesn't specify one. */
8
+ export const DEFAULT_MAX_NODES = 1;
9
+ /**
10
+ * GrantService — issues, validates, lists, and revokes donation grant tokens.
11
+ *
12
+ * It is the authority for a grant's *identity, expiry, and revocation*, and
13
+ * for the per-grantee *node cap* (`maxNodes`). It is deliberately **not** the
14
+ * authority for how many nodes are currently live under a grant — that tally
15
+ * belongs to the donation service (`2-donation-service`), which persists the
16
+ * grant→node records and can see terminations. `validateForProvision` takes
17
+ * the count as a parameter so the two never drift.
18
+ *
19
+ * All methods are synchronous: issuance/validation/revocation are pure local
20
+ * store operations with no node round-trip (unlike the trust-circle flow,
21
+ * which delegates to the owner node).
22
+ */
23
+ export class GrantService {
24
+ constructor(opts) {
25
+ this.store = opts.store;
26
+ this.now = opts.now ?? (() => new Date());
27
+ }
28
+ /**
29
+ * Issue a new grant. Generates a high-entropy base64url token, persists it,
30
+ * and returns the whole `Grant` (the CLI/UI prints/QR-encodes the token).
31
+ */
32
+ issue(opts) {
33
+ const label = normaliseLabel(opts.label);
34
+ const maxNodes = normaliseMaxNodes(opts.maxNodes);
35
+ const created = this.now();
36
+ const grant = {
37
+ token: generateToken(),
38
+ label,
39
+ maxNodes,
40
+ createdAt: created.toISOString(),
41
+ };
42
+ if (opts.ttlMs !== undefined) {
43
+ if (!Number.isFinite(opts.ttlMs) || opts.ttlMs <= 0) {
44
+ throw new GrantError('invalid_ttl', `Invalid ttlMs: ${opts.ttlMs}`);
45
+ }
46
+ grant.expiresAt = new Date(created.getTime() + opts.ttlMs).toISOString();
47
+ }
48
+ this.store.add(grant);
49
+ log('issued grant "%s" (token=%s, maxNodes=%d, expires=%s)', label, grant.token, maxNodes, grant.expiresAt ?? 'never');
50
+ return grant;
51
+ }
52
+ /**
53
+ * Validate a presented bearer token for a non-provisioning op: it must name a
54
+ * known grant that is neither revoked nor expired. No quota check. Never
55
+ * throws for a bad token — returns a denial.
56
+ */
57
+ validate(token) {
58
+ if (!token)
59
+ return { ok: false, reason: 'unknown_token' };
60
+ const grant = this.store.get(token);
61
+ if (!grant)
62
+ return { ok: false, reason: 'unknown_token' };
63
+ // Revocation is an explicit admin action; check it before expiry so a
64
+ // grant that is both revoked and expired reports the stronger signal.
65
+ if (grant.revokedAt)
66
+ return { ok: false, grant, reason: 'revoked' };
67
+ if (grant.expiresAt) {
68
+ const expiresAt = new Date(grant.expiresAt);
69
+ if (Number.isFinite(expiresAt.getTime()) && this.now() > expiresAt) {
70
+ return { ok: false, grant, reason: 'expired' };
71
+ }
72
+ }
73
+ return { ok: true, grant };
74
+ }
75
+ /**
76
+ * Validate for a *new* provision request. Runs the identity/expiry/revocation
77
+ * checks of `validate`, then — only if those pass — consults the
78
+ * caller-supplied live-node tally against the grant's `maxNodes`.
79
+ *
80
+ * Concurrency boundary: this is a pure function of the passed count. Two
81
+ * provision requests racing at the quota edge (both seeing count = N-1)
82
+ * would both pass here; serialising them so only one wins is the donation
83
+ * service's responsibility (`2-donation-service` owns the grant→node lock).
84
+ */
85
+ validateForProvision(token, liveNodeCount) {
86
+ const base = this.validate(token);
87
+ if (!base.ok || !base.grant)
88
+ return base;
89
+ const live = liveNodeCount(token);
90
+ if (live >= base.grant.maxNodes) {
91
+ return { ok: false, grant: base.grant, reason: 'quota_exceeded' };
92
+ }
93
+ return { ok: true, grant: base.grant };
94
+ }
95
+ /**
96
+ * Revoke a grant by token: mark it revoked (denies future requests). Existing
97
+ * live nodes are **not** torn down here — that is a separate admin action via
98
+ * the donation service's terminate (`2-donation-service`). Throws not_found
99
+ * when the token is unknown.
100
+ */
101
+ revoke(token) {
102
+ if (!this.store.markRevoked(token, this.now().toISOString())) {
103
+ throw new GrantError('not_found', `Unknown grant: ${token}`);
104
+ }
105
+ log('revoked grant (token=%s)', token);
106
+ }
107
+ /** Enumerate all grants (for the CLI / UI). */
108
+ list() {
109
+ return this.store.list();
110
+ }
111
+ }
112
+ /**
113
+ * Wrap a GrantService into the typed handler shape consumed by the loopback
114
+ * management server's `/grants-admin` routes. Errors propagate as GrantError;
115
+ * the server maps `.code` → HTTP status.
116
+ */
117
+ export function createGrantAdminHandlers(service) {
118
+ return {
119
+ async postGrant(body) {
120
+ if (!body || typeof body.label !== 'string') {
121
+ throw new GrantError('invalid_label', 'label is required');
122
+ }
123
+ const grant = service.issue({
124
+ label: body.label,
125
+ ...(body.maxNodes !== undefined ? { maxNodes: body.maxNodes } : {}),
126
+ ...(body.ttlMs !== undefined ? { ttlMs: body.ttlMs } : {}),
127
+ });
128
+ return { grant };
129
+ },
130
+ async listGrants() {
131
+ return { grants: service.list() };
132
+ },
133
+ async deleteGrant(token) {
134
+ service.revoke(token);
135
+ },
136
+ };
137
+ }
138
+ function generateToken() {
139
+ // base64url so it survives in URLs, QR codes, and shell arguments.
140
+ return randomBytes(TOKEN_BYTES).toString('base64url');
141
+ }
142
+ function normaliseLabel(label) {
143
+ if (typeof label !== 'string') {
144
+ throw new GrantError('invalid_label', 'label must be a string');
145
+ }
146
+ const trimmed = label.trim();
147
+ if (trimmed.length === 0) {
148
+ throw new GrantError('invalid_label', 'label must not be empty');
149
+ }
150
+ if (trimmed.length > 200) {
151
+ throw new GrantError('invalid_label', 'label must be 200 characters or fewer');
152
+ }
153
+ return trimmed;
154
+ }
155
+ function normaliseMaxNodes(maxNodes) {
156
+ if (maxNodes === undefined)
157
+ return DEFAULT_MAX_NODES;
158
+ if (typeof maxNodes !== 'number' || !Number.isInteger(maxNodes) || maxNodes < 1) {
159
+ throw new GrantError('invalid_max_nodes', `maxNodes must be a positive integer (got ${JSON.stringify(maxNodes)})`);
160
+ }
161
+ return maxNodes;
162
+ }
163
+ //# sourceMappingURL=grant-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grant-service.js","sourceRoot":"","sources":["../../src/donation/grant-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import type { Grant, GrantFile } from './types.js';
2
+ /**
3
+ * Atomic JSON store for `grants.json`. Modelled on `auth/trust-circle-store.ts`:
4
+ * a single file, written to `<path>.tmp` then renamed, with an in-memory cache
5
+ * keyed by token.
6
+ *
7
+ * Unlike the trust-circle pending rows, grants are **not** self-reaping: a
8
+ * grant may legitimately outlive many nodes, so expiry is evaluated at
9
+ * validate time (in `GrantService`) and the admin removes stale grants
10
+ * explicitly. This store keeps no clock.
11
+ *
12
+ * Concurrency assumption: only one cadre-host process owns a given rootDir
13
+ * (the orchestrator already enforces this). No file-locking primitives.
14
+ */
15
+ export declare class GrantStore {
16
+ private readonly path;
17
+ private cache;
18
+ constructor(rootDir: string);
19
+ filePath(): string;
20
+ /**
21
+ * Load (with caching). A missing file returns an empty store (fresh
22
+ * install). A present-but-malformed file throws — silently wiping it would
23
+ * revoke every grantee at once.
24
+ */
25
+ load(): GrantFile;
26
+ /** Persist the current state atomically (write then rename). */
27
+ save(state: GrantFile): void;
28
+ add(grant: Grant): void;
29
+ get(token: string): Grant | undefined;
30
+ list(): Grant[];
31
+ remove(token: string): boolean;
32
+ /**
33
+ * Mark a grant revoked at the given ISO instant. Idempotent: an
34
+ * already-revoked grant keeps its original `revokedAt`. Returns false when
35
+ * the token is unknown.
36
+ */
37
+ markRevoked(token: string, at: string): boolean;
38
+ }
@@ -0,0 +1,135 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import debug from 'debug';
4
+ import { GrantError } from './types.js';
5
+ const log = debug('cadre:host:grant-store');
6
+ const FILE_VERSION = 1;
7
+ /**
8
+ * Atomic JSON store for `grants.json`. Modelled on `auth/trust-circle-store.ts`:
9
+ * a single file, written to `<path>.tmp` then renamed, with an in-memory cache
10
+ * keyed by token.
11
+ *
12
+ * Unlike the trust-circle pending rows, grants are **not** self-reaping: a
13
+ * grant may legitimately outlive many nodes, so expiry is evaluated at
14
+ * validate time (in `GrantService`) and the admin removes stale grants
15
+ * explicitly. This store keeps no clock.
16
+ *
17
+ * Concurrency assumption: only one cadre-host process owns a given rootDir
18
+ * (the orchestrator already enforces this). No file-locking primitives.
19
+ */
20
+ export class GrantStore {
21
+ constructor(rootDir) {
22
+ this.cache = null;
23
+ mkdirSync(rootDir, { recursive: true });
24
+ this.path = join(rootDir, 'grants.json');
25
+ }
26
+ filePath() {
27
+ return this.path;
28
+ }
29
+ /**
30
+ * Load (with caching). A missing file returns an empty store (fresh
31
+ * install). A present-but-malformed file throws — silently wiping it would
32
+ * revoke every grantee at once.
33
+ */
34
+ load() {
35
+ if (this.cache)
36
+ return this.cache;
37
+ if (!existsSync(this.path)) {
38
+ this.cache = emptyFile();
39
+ return this.cache;
40
+ }
41
+ let raw;
42
+ try {
43
+ raw = readFileSync(this.path, 'utf8');
44
+ }
45
+ catch (err) {
46
+ throw new GrantError('storage_error', `failed to read grants file at ${this.path}: ${err.message}`);
47
+ }
48
+ let parsed;
49
+ try {
50
+ parsed = JSON.parse(raw);
51
+ }
52
+ catch (err) {
53
+ throw new GrantError('storage_error', `grants file at ${this.path} is not valid JSON: ${err.message}`);
54
+ }
55
+ if (!isGrantFile(parsed)) {
56
+ throw new GrantError('storage_error', `grants file at ${this.path} has unexpected shape`);
57
+ }
58
+ this.cache = parsed;
59
+ return this.cache;
60
+ }
61
+ /** Persist the current state atomically (write then rename). */
62
+ save(state) {
63
+ // NOTE: rewrites the whole grants.json on every mutation. Fine at household
64
+ // scale (a handful of grantees); if grant counts ever grow large, switch to
65
+ // an append/compact log or per-token files.
66
+ mkdirSync(dirname(this.path), { recursive: true });
67
+ const payload = JSON.stringify({ ...state, version: FILE_VERSION }, null, 2);
68
+ const tmp = `${this.path}.tmp`;
69
+ try {
70
+ writeFileSync(tmp, payload, { encoding: 'utf8' });
71
+ renameSync(tmp, this.path);
72
+ }
73
+ catch (err) {
74
+ // Surface as storage_error (→ 500) like load(), not a bare Error (→
75
+ // "internal"); a write failure on issue/revoke should read consistently.
76
+ throw new GrantError('storage_error', `failed to write grants file at ${this.path}: ${err.message}`);
77
+ }
78
+ this.cache = { ...state, version: FILE_VERSION };
79
+ log('saved grants (%d) to %s', Object.keys(state.grants).length, this.path);
80
+ }
81
+ add(grant) {
82
+ const state = this.load();
83
+ const { token, ...rest } = grant;
84
+ state.grants[token] = rest;
85
+ this.save(state);
86
+ }
87
+ get(token) {
88
+ const state = this.load();
89
+ const row = state.grants[token];
90
+ if (!row)
91
+ return undefined;
92
+ return { token, ...row };
93
+ }
94
+ list() {
95
+ const state = this.load();
96
+ return Object.entries(state.grants).map(([token, row]) => ({ token, ...row }));
97
+ }
98
+ remove(token) {
99
+ const state = this.load();
100
+ if (!(token in state.grants))
101
+ return false;
102
+ delete state.grants[token];
103
+ this.save(state);
104
+ return true;
105
+ }
106
+ /**
107
+ * Mark a grant revoked at the given ISO instant. Idempotent: an
108
+ * already-revoked grant keeps its original `revokedAt`. Returns false when
109
+ * the token is unknown.
110
+ */
111
+ markRevoked(token, at) {
112
+ const state = this.load();
113
+ const row = state.grants[token];
114
+ if (!row)
115
+ return false;
116
+ if (!row.revokedAt)
117
+ row.revokedAt = at;
118
+ this.save(state);
119
+ return true;
120
+ }
121
+ }
122
+ function emptyFile() {
123
+ return { version: FILE_VERSION, grants: {} };
124
+ }
125
+ function isGrantFile(v) {
126
+ if (!v || typeof v !== 'object')
127
+ return false;
128
+ const obj = v;
129
+ if (obj.version !== FILE_VERSION)
130
+ return false;
131
+ if (!obj.grants || typeof obj.grants !== 'object')
132
+ return false;
133
+ return true;
134
+ }
135
+ //# sourceMappingURL=grant-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grant-store.js","sourceRoot":"","sources":["../../src/donation/grant-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
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
+ export { GrantStore } from './grant-store.js';
11
+ export { GrantService, createGrantAdminHandlers, DEFAULT_MAX_NODES, } from './grant-service.js';
12
+ export type { GrantServiceOptions } from './grant-service.js';
13
+ export { GrantError, DonationError } from './types.js';
14
+ export type { Grant, GrantDenyReason, GrantValidation, GrantValidator, GrantFile, GrantAdminHandlers, GrantErrorCode, } from './types.js';
15
+ export { DonationStore } from './donation-store.js';
16
+ export { DonationService, DONATION_AWAITING_SEED_TTL_MS, DONATION_REAP_SWEEP_MS, } from './donation-service.js';
17
+ export type { DonationProvisionRequest, DonationSeedResult, DonationPeerInfo, DonationServiceOptions, } from './donation-service.js';
18
+ export type { Donation, DonationView, DonationStatus, DonationFile, DonationErrorCode, } from './types.js';
@@ -0,0 +1,16 @@
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
+ export { GrantStore } from './grant-store.js';
11
+ export { GrantService, createGrantAdminHandlers, DEFAULT_MAX_NODES, } from './grant-service.js';
12
+ export { GrantError, DonationError } from './types.js';
13
+ /* ──────────────── donation lifecycle (2-donation-service) ──────────────── */
14
+ export { DonationStore } from './donation-store.js';
15
+ export { DonationService, DONATION_AWAITING_SEED_TTL_MS, DONATION_REAP_SWEEP_MS, } from './donation-service.js';
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/donation/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EACL,YAAY,EACZ,wBAAwB,EACxB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAWvD,+EAA+E;AAE/E,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EACL,eAAe,EACf,6BAA6B,EAC7B,sBAAsB,GACvB,MAAM,uBAAuB,CAAC"}