@serfab/cadre-host 0.9.0 → 0.11.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 (213) hide show
  1. package/dist/auth/index.d.ts +2 -0
  2. package/dist/auth/index.js +1 -0
  3. package/dist/auth/index.js.map +1 -1
  4. package/dist/auth/self-label.d.ts +36 -0
  5. package/dist/auth/self-label.js +57 -0
  6. package/dist/auth/self-label.js.map +1 -0
  7. package/dist/auth/trust-circle.d.ts +32 -5
  8. package/dist/auth/trust-circle.js +31 -11
  9. package/dist/auth/trust-circle.js.map +1 -1
  10. package/dist/bin/host.js +41 -5
  11. package/dist/bin/host.js.map +1 -1
  12. package/dist/donation/__tests__/fake-orchestrator.d.ts +103 -0
  13. package/dist/donation/__tests__/fake-orchestrator.js +180 -0
  14. package/dist/donation/__tests__/fake-orchestrator.js.map +1 -0
  15. package/dist/donation/donation-service.d.ts +237 -12
  16. package/dist/donation/donation-service.js +491 -34
  17. package/dist/donation/donation-service.js.map +1 -1
  18. package/dist/donation/donation-store.d.ts +7 -0
  19. package/dist/donation/donation-store.js +7 -0
  20. package/dist/donation/donation-store.js.map +1 -1
  21. package/dist/donation/donation-supervisor.d.ts +167 -0
  22. package/dist/donation/donation-supervisor.js +341 -0
  23. package/dist/donation/donation-supervisor.js.map +1 -0
  24. package/dist/donation/index.d.ts +4 -2
  25. package/dist/donation/index.js +2 -1
  26. package/dist/donation/index.js.map +1 -1
  27. package/dist/donation/types.d.ts +26 -0
  28. package/dist/donation/types.js.map +1 -1
  29. package/dist/index.d.ts +3 -1
  30. package/dist/index.js +3 -1
  31. package/dist/index.js.map +1 -1
  32. package/dist/orchestrator/host-process-orchestrator.d.ts +109 -5
  33. package/dist/orchestrator/host-process-orchestrator.js +341 -98
  34. package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
  35. package/dist/orchestrator/node-identity.d.ts +26 -0
  36. package/dist/orchestrator/node-identity.js +36 -0
  37. package/dist/orchestrator/node-identity.js.map +1 -0
  38. package/dist/orchestrator/port-allocator.d.ts +14 -0
  39. package/dist/orchestrator/port-allocator.js +45 -0
  40. package/dist/orchestrator/port-allocator.js.map +1 -1
  41. package/dist/owner/owner-node-client.d.ts +20 -5
  42. package/dist/owner/owner-node-client.js +21 -4
  43. package/dist/owner/owner-node-client.js.map +1 -1
  44. package/dist/server/error-handler.d.ts +7 -1
  45. package/dist/server/error-handler.js +23 -1
  46. package/dist/server/error-handler.js.map +1 -1
  47. package/dist/server/events/types.d.ts +3 -0
  48. package/dist/server/index.d.ts +7 -0
  49. package/dist/server/index.js +9 -3
  50. package/dist/server/index.js.map +1 -1
  51. package/dist/server/routes/grants.js +22 -3
  52. package/dist/server/routes/grants.js.map +1 -1
  53. package/dist/server/routes/strands.d.ts +20 -0
  54. package/dist/server/routes/strands.js +38 -0
  55. package/dist/server/routes/strands.js.map +1 -0
  56. package/dist/server/static.js +38 -38
  57. package/dist/strands/index.d.ts +7 -0
  58. package/dist/strands/index.js +6 -0
  59. package/dist/strands/index.js.map +1 -0
  60. package/dist/strands/strand-service.d.ts +50 -0
  61. package/dist/strands/strand-service.js +103 -0
  62. package/dist/strands/strand-service.js.map +1 -0
  63. package/dist/strands/types.d.ts +93 -0
  64. package/dist/strands/types.js +23 -0
  65. package/dist/strands/types.js.map +1 -0
  66. package/dist/ui/assets/index-BVoqLjyX.css +1 -0
  67. package/dist/ui/assets/index-C-iks41M.js +17 -0
  68. package/dist/ui/assets/index-C-iks41M.js.map +1 -0
  69. package/dist/ui/index.html +14 -14
  70. package/dist/update/release-key.d.ts +1 -1
  71. package/dist/update/release-key.js +1 -1
  72. package/package.json +5 -5
  73. package/service/cadre-host.service.tmpl +16 -16
  74. package/service/com.serfab.cadre-host.plist.tmpl +35 -35
  75. package/service/install-service.ps1 +98 -98
  76. package/service/uninstall-service.ps1 +19 -19
  77. package/src/__tests__/build-targets.test.ts +26 -0
  78. package/src/__tests__/cli-invite.smoke.test.ts +192 -192
  79. package/src/__tests__/cli-nat.smoke.test.ts +212 -212
  80. package/src/__tests__/cli.smoke.test.ts +46 -46
  81. package/src/__tests__/global-setup.ts +51 -0
  82. package/src/__tests__/host-owner.smoke.test.ts +123 -123
  83. package/src/__tests__/orchestrator-env-scrub.test.ts +140 -0
  84. package/src/__tests__/orchestrator-node-identity.test.ts +204 -0
  85. package/src/__tests__/orchestrator-owner.test.ts +198 -198
  86. package/src/__tests__/orchestrator-pin-keys.test.ts +153 -153
  87. package/src/__tests__/orchestrator-push.test.ts +145 -145
  88. package/src/__tests__/orchestrator.test.ts +951 -570
  89. package/src/auth/__tests__/duration.test.ts +28 -28
  90. package/src/auth/__tests__/self-label.test.ts +85 -0
  91. package/src/auth/__tests__/trust-circle-integration.test.ts +152 -147
  92. package/src/auth/__tests__/trust-circle-store.test.ts +115 -115
  93. package/src/auth/__tests__/trust-circle.test.ts +445 -401
  94. package/src/auth/duration.ts +33 -33
  95. package/src/auth/index.ts +21 -19
  96. package/src/auth/self-label.ts +74 -0
  97. package/src/auth/trust-circle-store.ts +155 -155
  98. package/src/auth/trust-circle.ts +45 -13
  99. package/src/auth/types.ts +99 -99
  100. package/src/bin/host.ts +1299 -1258
  101. package/src/donation/__tests__/donation-service.test.ts +1068 -290
  102. package/src/donation/__tests__/donation-supervisor.test.ts +510 -0
  103. package/src/donation/__tests__/fake-orchestrator.test.ts +168 -0
  104. package/src/donation/__tests__/fake-orchestrator.ts +225 -0
  105. package/src/donation/__tests__/grant-service.test.ts +213 -213
  106. package/src/donation/__tests__/grant-store.test.ts +113 -113
  107. package/src/donation/donation-service.ts +962 -392
  108. package/src/donation/donation-store.ts +171 -164
  109. package/src/donation/donation-supervisor.ts +388 -0
  110. package/src/donation/grant-service.ts +200 -200
  111. package/src/donation/grant-store.ts +152 -152
  112. package/src/donation/index.ts +64 -49
  113. package/src/donation/types.ts +243 -220
  114. package/src/index.ts +14 -0
  115. package/src/installer/__tests__/identity.test.ts +34 -34
  116. package/src/installer/__tests__/paths.test.ts +47 -47
  117. package/src/installer/__tests__/service-host.test.ts +62 -62
  118. package/src/installer/__tests__/wizard.test.ts +64 -64
  119. package/src/installer/identity.ts +49 -49
  120. package/src/installer/paths.ts +109 -109
  121. package/src/installer/platform.ts +27 -27
  122. package/src/installer/service-host/index.ts +21 -21
  123. package/src/installer/service-host/template.ts +21 -21
  124. package/src/installer/wizard.ts +103 -103
  125. package/src/nat/__tests__/address-resolver.test.ts +82 -82
  126. package/src/nat/__tests__/ddns-duckdns.test.ts +83 -83
  127. package/src/nat/__tests__/ddns-updater.test.ts +253 -253
  128. package/src/nat/__tests__/external-ip.test.ts +124 -124
  129. package/src/nat/__tests__/nat-service.test.ts +497 -497
  130. package/src/nat/__tests__/nat-store.test.ts +93 -93
  131. package/src/nat/__tests__/port-mapper.test.ts +191 -191
  132. package/src/nat/__tests__/reachability.test.ts +24 -24
  133. package/src/nat/__tests__/secrets.test.ts +178 -178
  134. package/src/nat/address-resolver.ts +51 -51
  135. package/src/nat/ddns/duckdns.ts +76 -76
  136. package/src/nat/ddns/index.ts +55 -55
  137. package/src/nat/ddns/updater.ts +243 -243
  138. package/src/nat/external-ip.ts +154 -154
  139. package/src/nat/index.ts +71 -71
  140. package/src/nat/nat-service.ts +501 -501
  141. package/src/nat/nat-store.ts +154 -154
  142. package/src/nat/port-mapper.ts +329 -329
  143. package/src/nat/reachability.ts +34 -34
  144. package/src/nat/secrets/file-store.ts +130 -130
  145. package/src/nat/secrets/index.ts +112 -112
  146. package/src/nat/secrets/keytar-store.ts +67 -67
  147. package/src/nat/types.ts +159 -159
  148. package/src/orchestrator/__tests__/node-identity.test.ts +62 -0
  149. package/src/orchestrator/host-process-orchestrator.ts +1133 -894
  150. package/src/orchestrator/index.ts +10 -10
  151. package/src/orchestrator/log-rotator.ts +53 -53
  152. package/src/orchestrator/node-identity.ts +40 -0
  153. package/src/orchestrator/pid-liveness.ts +24 -24
  154. package/src/orchestrator/port-allocator.ts +91 -43
  155. package/src/orchestrator/state-store.ts +90 -90
  156. package/src/orchestrator/types.ts +141 -141
  157. package/src/owner/__tests__/owner-node-client.test.ts +194 -194
  158. package/src/owner/index.ts +12 -12
  159. package/src/owner/owner-node-client.ts +259 -233
  160. package/src/push/__tests__/push.test.ts +90 -90
  161. package/src/push/index.ts +146 -146
  162. package/src/server/__tests__/bus.test.ts +38 -38
  163. package/src/server/__tests__/error-handler.test.ts +118 -118
  164. package/src/server/__tests__/grants-admin-route.test.ts +73 -73
  165. package/src/server/__tests__/grants-route.test.ts +263 -189
  166. package/src/server/__tests__/nodes-route.test.ts +259 -259
  167. package/src/server/__tests__/origin-guard.test.ts +72 -72
  168. package/src/server/__tests__/publishers.test.ts +216 -216
  169. package/src/server/__tests__/server.smoke.test.ts +253 -253
  170. package/src/server/__tests__/settings-route.test.ts +194 -194
  171. package/src/server/__tests__/sse-route.test.ts +119 -119
  172. package/src/server/__tests__/static.test.ts +145 -145
  173. package/src/server/__tests__/status-route.test.ts +134 -134
  174. package/src/server/__tests__/strands-route.test.ts +233 -0
  175. package/src/server/error-handler.ts +178 -155
  176. package/src/server/events/bus.ts +42 -42
  177. package/src/server/events/sse-route.ts +81 -81
  178. package/src/server/events/types.ts +23 -22
  179. package/src/server/index.ts +267 -254
  180. package/src/server/origin-guard.ts +84 -84
  181. package/src/server/routes/grants-admin.ts +41 -41
  182. package/src/server/routes/grants.ts +210 -186
  183. package/src/server/routes/nat.ts +63 -63
  184. package/src/server/routes/nodes.ts +170 -170
  185. package/src/server/routes/settings.ts +123 -123
  186. package/src/server/routes/status.ts +118 -118
  187. package/src/server/routes/strands.ts +54 -0
  188. package/src/server/routes/trust-circle.ts +49 -49
  189. package/src/server/routes/update.ts +43 -43
  190. package/src/server/server.ts +122 -122
  191. package/src/server/settings-store.ts +37 -37
  192. package/src/server/static.ts +207 -207
  193. package/src/strands/__tests__/strand-service.test.ts +165 -0
  194. package/src/strands/index.ts +14 -0
  195. package/src/strands/strand-service.ts +135 -0
  196. package/src/strands/types.ts +102 -0
  197. package/src/update/__tests__/apply.test.ts +158 -158
  198. package/src/update/__tests__/manifest.test.ts +128 -128
  199. package/src/update/__tests__/release-key.test.ts +42 -42
  200. package/src/update/__tests__/sign.test.ts +78 -78
  201. package/src/update/__tests__/store.test.ts +81 -81
  202. package/src/update/__tests__/update-service.integration.test.ts +256 -256
  203. package/src/update/__tests__/version.test.ts +31 -31
  204. package/src/update/apply.ts +190 -190
  205. package/src/update/index.ts +341 -341
  206. package/src/update/release-key.ts +71 -71
  207. package/src/update/sign.ts +74 -74
  208. package/src/update/store.ts +101 -101
  209. package/src/update/types.ts +109 -109
  210. package/src/update/version.ts +80 -80
  211. package/dist/ui/assets/index-BlMXQPSw.js +0 -14
  212. package/dist/ui/assets/index-BlMXQPSw.js.map +0 -1
  213. package/dist/ui/assets/index-BwU5mJGF.css +0 -1
@@ -1,28 +1,28 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
- import { parseDuration } from '../duration.js';
4
-
5
- describe('parseDuration', () => {
6
- it('parses common units', () => {
7
- expect(parseDuration('500ms')).toBe(500);
8
- expect(parseDuration('30s')).toBe(30_000);
9
- expect(parseDuration('5m')).toBe(5 * 60 * 1000);
10
- expect(parseDuration('24h')).toBe(24 * 60 * 60 * 1000);
11
- expect(parseDuration('7d')).toBe(7 * 24 * 60 * 60 * 1000);
12
- expect(parseDuration('2w')).toBe(2 * 7 * 24 * 60 * 60 * 1000);
13
- });
14
-
15
- it('treats bare numbers as milliseconds', () => {
16
- expect(parseDuration('1234')).toBe(1234);
17
- });
18
-
19
- it('accepts whitespace and decimals', () => {
20
- expect(parseDuration(' 1.5h ')).toBe(Math.round(1.5 * 60 * 60 * 1000));
21
- });
22
-
23
- it('throws on garbage input', () => {
24
- expect(() => parseDuration('abc')).toThrow();
25
- expect(() => parseDuration('1y')).toThrow();
26
- expect(() => parseDuration('')).toThrow();
27
- });
28
- });
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { parseDuration } from '../duration.js';
4
+
5
+ describe('parseDuration', () => {
6
+ it('parses common units', () => {
7
+ expect(parseDuration('500ms')).toBe(500);
8
+ expect(parseDuration('30s')).toBe(30_000);
9
+ expect(parseDuration('5m')).toBe(5 * 60 * 1000);
10
+ expect(parseDuration('24h')).toBe(24 * 60 * 60 * 1000);
11
+ expect(parseDuration('7d')).toBe(7 * 24 * 60 * 60 * 1000);
12
+ expect(parseDuration('2w')).toBe(2 * 7 * 24 * 60 * 60 * 1000);
13
+ });
14
+
15
+ it('treats bare numbers as milliseconds', () => {
16
+ expect(parseDuration('1234')).toBe(1234);
17
+ });
18
+
19
+ it('accepts whitespace and decimals', () => {
20
+ expect(parseDuration(' 1.5h ')).toBe(Math.round(1.5 * 60 * 60 * 1000));
21
+ });
22
+
23
+ it('throws on garbage input', () => {
24
+ expect(() => parseDuration('abc')).toThrow();
25
+ expect(() => parseDuration('1y')).toThrow();
26
+ expect(() => parseDuration('')).toThrow();
27
+ });
28
+ });
@@ -0,0 +1,85 @@
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 { SELF_LABEL, ensureSelfLabel } from '../self-label.js';
7
+ import { TrustCircleStore } from '../trust-circle-store.js';
8
+
9
+ let tmpRoot: string;
10
+ let store: TrustCircleStore;
11
+
12
+ const NOW = new Date('2025-06-01T12:00:00.000Z');
13
+ const now = (): Date => NOW;
14
+
15
+ beforeEach(() => {
16
+ tmpRoot = mkdtempSync(join(tmpdir(), 'cadre-host-self-label-'));
17
+ store = new TrustCircleStore(tmpRoot);
18
+ });
19
+
20
+ afterEach(() => {
21
+ try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
22
+ });
23
+
24
+ describe('ensureSelfLabel', () => {
25
+ it('writes a self-flagged label for the owner peer ID', async () => {
26
+ const peerId = await ensureSelfLabel({ store, getPeerId: async () => '12D3KooWOwner', now });
27
+
28
+ expect(peerId).toBe('12D3KooWOwner');
29
+ expect(store.getMember('12D3KooWOwner')).toEqual({
30
+ peerId: '12D3KooWOwner',
31
+ label: SELF_LABEL,
32
+ addedAt: NOW.toISOString(),
33
+ self: true,
34
+ });
35
+ });
36
+
37
+ it('keeps an admin-renamed label on a second run', async () => {
38
+ store.addMember({ peerId: '12D3KooWOwner', label: 'Basement PC', addedAt: 't0', self: true });
39
+
40
+ await ensureSelfLabel({ store, getPeerId: async () => '12D3KooWOwner', now });
41
+
42
+ expect(store.getMember('12D3KooWOwner')).toMatchObject({ label: 'Basement PC', addedAt: 't0', self: true });
43
+ });
44
+
45
+ it('stamps the self flag onto a pre-existing unflagged row', async () => {
46
+ store.addMember({ peerId: '12D3KooWOwner', label: 'Basement PC', addedAt: 't0' });
47
+
48
+ await ensureSelfLabel({ store, getPeerId: async () => '12D3KooWOwner', now });
49
+
50
+ expect(store.getMember('12D3KooWOwner')).toEqual({
51
+ peerId: '12D3KooWOwner',
52
+ label: 'Basement PC',
53
+ addedAt: 't0',
54
+ self: true,
55
+ });
56
+ });
57
+
58
+ it('drops a stale self label after the node identity changes', async () => {
59
+ store.addMember({ peerId: '12D3KooWOld', label: SELF_LABEL, addedAt: 't0', self: true });
60
+ store.addMember({ peerId: '12D3KooWPhone', label: "Mom's phone", addedAt: 't1' });
61
+
62
+ await ensureSelfLabel({ store, getPeerId: async () => '12D3KooWNew', now });
63
+
64
+ expect(store.getMember('12D3KooWOld')).toBeUndefined();
65
+ expect(store.getMember('12D3KooWNew')).toMatchObject({ label: SELF_LABEL, self: true });
66
+ // Non-self labels are untouched.
67
+ expect(store.getMember('12D3KooWPhone')).toMatchObject({ label: "Mom's phone" });
68
+ });
69
+
70
+ it('writes nothing when the node is not ready yet (empty peer ID)', async () => {
71
+ const peerId = await ensureSelfLabel({ store, getPeerId: async () => '', now });
72
+
73
+ expect(peerId).toBeUndefined();
74
+ expect(store.listMembers()).toEqual([]);
75
+ });
76
+
77
+ it('propagates a getPeerId failure to the caller', async () => {
78
+ await expect(ensureSelfLabel({
79
+ store,
80
+ getPeerId: async () => { throw new Error('owner node unavailable'); },
81
+ now,
82
+ })).rejects.toThrow('owner node unavailable');
83
+ expect(store.listMembers()).toEqual([]);
84
+ });
85
+ });
@@ -1,147 +1,152 @@
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
- import { generateKeyPair } from '@libp2p/crypto/keys';
6
- import { peerIdFromPrivateKey } from '@libp2p/peer-id';
7
- import { CadreNode, ed25519KeyPairFromLibp2p } from '@serfab/cadre-core';
8
-
9
- import { TrustCircleService } from '../trust-circle.js';
10
- import { TrustCircleStore } from '../trust-circle-store.js';
11
-
12
- /**
13
- * End-to-end integration test: stand up a real host CadreNode with a real
14
- * Quereus control DB and exercise the full TrustCircleService issue → redeem
15
- * → list → remove cycle. The "phone" peerId is derived from a fresh Ed25519
16
- * key (no second libp2p node) since the test only validates host-side state.
17
- *
18
- * This replaces the mock CadreNodeLike in trust-circle.test.ts with the real
19
- * CadreNode to validate that:
20
- * - `acceptPhone` (called from redeemInvite with a sparse reconstructed
21
- * invite) actually inserts a CadrePeer row;
22
- * - `list()` picks up the new peer via the canonical CadrePeer query;
23
- * - `removeMember` deletes the row from CadrePeer via the
24
- * DELETE-with-context syntax that cadre-core trusts from
25
- * `quereus/docs/sql.md`.
26
- */
27
- describe('TrustCircleService — real CadreNode integration', () => {
28
- let tmpRoot: string;
29
- let host: CadreNode;
30
- let store: TrustCircleStore;
31
- let service: TrustCircleService;
32
- let hostOwnerPrivateKey: string;
33
- let hostOwnerPublicKey: string;
34
-
35
- beforeEach(async () => {
36
- tmpRoot = mkdtempSync(join(tmpdir(), 'cadre-host-tc-int-'));
37
-
38
- // Own-owner host: the node's libp2p identity key IS its owner key
39
- // (the cadre-cli `--owner` shape), so registerSelf can owner-sign the
40
- // INSERT of its own CadrePeer row.
41
- const hostKey = await generateKeyPair('Ed25519');
42
- const owner = ed25519KeyPairFromLibp2p(hostKey);
43
- hostOwnerPrivateKey = owner.privateKeyB64;
44
- hostOwnerPublicKey = owner.publicKeyB64;
45
-
46
- const baseId = Math.random().toString(36).slice(2);
47
-
48
- host = new CadreNode({
49
- controlNetwork: { partyId: `host-${baseId}`, bootstrapNodes: [] },
50
- privateKey: hostKey,
51
- profile: 'transaction'
52
- });
53
-
54
- await host.start();
55
-
56
- const db = host.getControlDatabase();
57
- expect(db).not.toBeNull();
58
- await db!.insertOwnerKey(hostOwnerPublicKey);
59
-
60
- host.initializeSeedBootstrap(hostOwnerPrivateKey);
61
-
62
- // The owner writes its own CadrePeer row up-front (the implement ticket's
63
- // CLI change). After this the host's own peerId is a member, so it shows up
64
- // alongside any redeemed peers in the trust-circle listing.
65
- await host.registerSelf();
66
-
67
- store = new TrustCircleStore(tmpRoot);
68
- service = new TrustCircleService({ cadreNode: host, store });
69
- }, 60_000);
70
-
71
- afterEach(async () => {
72
- try { await host.stop(); } catch { /* ignore */ }
73
- try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
74
- });
75
-
76
- it('issues → redeems → lists against the real control DB (listMembers path)', async () => {
77
- // Derive a shape-valid peerId from a fresh Ed25519 key — same pattern as
78
- // packages/cadre-core/test/seed-bootstrap.spec.ts. The AuthorizedInsert
79
- // constraint only checks the signature over digest(PeerId,…); peerId
80
- // *liveness* doesn't matter for the host-side CRUD path under test.
81
- const phoneKey = await generateKeyPair('Ed25519');
82
- const phonePeerId = peerIdFromPrivateKey(phoneKey).toString();
83
-
84
- // Issue
85
- const issued = await service.issueInvite({ label: "Mom's phone" });
86
- expect(issued.token).toBeTruthy();
87
- expect(issued.encodedInvite).toBeTruthy();
88
-
89
- // Redeem — this calls host.acceptPhone() with the sparse reconstructed
90
- // invite, which in turn calls authorizePeer and inserts CadrePeer.
91
- const redeemed = await service.redeemInvite({
92
- token: issued.token,
93
- peerId: phonePeerId
94
- });
95
- expect(redeemed).toEqual({ peerId: phonePeerId, label: "Mom's phone" });
96
-
97
- // Verify directly against the control DB: the row is there.
98
- expect(await isInCadrePeer(host, phonePeerId)).toBe(true);
99
-
100
- // List — exercises the CadreNode.listMembers() path. CadrePeer now carries
101
- // the host's own self-registered row in addition to the redeemed phone.
102
- const snap = await service.list();
103
- expect(snap.members).toHaveLength(2);
104
- const phoneMember = snap.members.find((m) => m.peerId === phonePeerId);
105
- expect(phoneMember).toBeDefined();
106
- expect(phoneMember!.label).toBe("Mom's phone");
107
- // The host's own peerId is present too (unlabeled → bare peerId as label).
108
- expect(snap.members.some((m) => m.peerId === host.peerId!.toString())).toBe(true);
109
- expect(snap.pending).toHaveLength(0);
110
- }, 90_000);
111
-
112
- // The signed `CadrePeer` DELETE previously threw a Quereus deferred-constraint
113
- // error ("No row context found for column PeerId") inside cadre-core's
114
- // removePeer. That upstream bug is fixed (`quereus-cadrepeer-delete-no-row-context`,
115
- // landed), so the full issue → redeem → remove cycle is now exercised here.
116
- it('removes a member from CadrePeer', async () => {
117
- const phoneKey = await generateKeyPair('Ed25519');
118
- const phonePeerId = peerIdFromPrivateKey(phoneKey).toString();
119
-
120
- const issued = await service.issueInvite({ label: "Mom's phone" });
121
- await service.redeemInvite({ token: issued.token, peerId: phonePeerId });
122
- expect(await isInCadrePeer(host, phonePeerId)).toBe(true);
123
-
124
- await service.removeMember(phonePeerId);
125
-
126
- expect(await isInCadrePeer(host, phonePeerId)).toBe(false);
127
- expect(store.getMember(phonePeerId)).toBeUndefined();
128
- const afterRemove = await service.list();
129
- // The phone is gone; the host's own self-registered row remains.
130
- expect(afterRemove.members.map((m) => m.peerId)).not.toContain(phonePeerId);
131
- expect(afterRemove.members).toHaveLength(1);
132
- expect(afterRemove.members[0]!.peerId).toBe(host.peerId!.toString());
133
- }, 90_000);
134
- });
135
-
136
- async function isInCadrePeer(node: CadreNode, peerId: string): Promise<boolean> {
137
- const db = node.getControlDatabase();
138
- if (!db) return false;
139
- const inner = db.getDatabase();
140
- for await (const row of inner.eval(
141
- 'select PeerId from CadreControl.CadrePeer where PeerId = ?',
142
- [peerId]
143
- )) {
144
- if (row.PeerId === peerId) return true;
145
- }
146
- return false;
147
- }
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
+ import { generateKeyPair } from '@libp2p/crypto/keys';
6
+ import { peerIdFromPrivateKey } from '@libp2p/peer-id';
7
+ import { CadreNode, ed25519KeyPairFromLibp2p } from '@serfab/cadre-core';
8
+
9
+ import { ensureSelfLabel } from '../self-label.js';
10
+ import { TrustCircleService } from '../trust-circle.js';
11
+ import { TrustCircleStore } from '../trust-circle-store.js';
12
+
13
+ /**
14
+ * End-to-end integration test: stand up a real host CadreNode with a real
15
+ * Quereus control DB and exercise the full TrustCircleService issue → redeem
16
+ * → list → remove cycle. The "phone" peerId is derived from a fresh Ed25519
17
+ * key (no second libp2p node) since the test only validates host-side state.
18
+ *
19
+ * This replaces the mock CadreNodeLike in trust-circle.test.ts with the real
20
+ * CadreNode to validate that:
21
+ * - `acceptPhone` (called from redeemInvite with a sparse reconstructed
22
+ * invite) actually inserts a CadrePeer row;
23
+ * - `list()` picks up the new peer via the canonical CadrePeer query;
24
+ * - `removeMember` deletes the row from CadrePeer via the
25
+ * DELETE-with-context syntax that cadre-core trusts from
26
+ * `quereus/docs/sql.md`.
27
+ */
28
+ describe('TrustCircleService — real CadreNode integration', () => {
29
+ let tmpRoot: string;
30
+ let host: CadreNode;
31
+ let store: TrustCircleStore;
32
+ let service: TrustCircleService;
33
+ let hostOwnerPrivateKey: string;
34
+ let hostOwnerPublicKey: string;
35
+
36
+ beforeEach(async () => {
37
+ tmpRoot = mkdtempSync(join(tmpdir(), 'cadre-host-tc-int-'));
38
+
39
+ // Own-owner host: the node's libp2p identity key IS its owner key
40
+ // (the cadre-cli `--owner` shape), so registerSelf can owner-sign the
41
+ // INSERT of its own CadrePeer row.
42
+ const hostKey = await generateKeyPair('Ed25519');
43
+ const owner = ed25519KeyPairFromLibp2p(hostKey);
44
+ hostOwnerPrivateKey = owner.privateKeyB64;
45
+ hostOwnerPublicKey = owner.publicKeyB64;
46
+
47
+ const baseId = Math.random().toString(36).slice(2);
48
+
49
+ host = new CadreNode({
50
+ controlNetwork: { partyId: `host-${baseId}`, bootstrapNodes: [] },
51
+ privateKey: hostKey,
52
+ profile: 'transaction'
53
+ });
54
+
55
+ await host.start();
56
+
57
+ const db = host.getControlDatabase();
58
+ expect(db).not.toBeNull();
59
+ await db!.insertOwnerKey(hostOwnerPublicKey);
60
+
61
+ host.initializeSeedBootstrap(hostOwnerPrivateKey);
62
+
63
+ // The owner writes its own CadrePeer row up-front (the implement ticket's
64
+ // CLI change). After this the host's own peerId is a member, so it shows up
65
+ // alongside any redeemed peers in the trust-circle listing.
66
+ await host.registerSelf();
67
+
68
+ store = new TrustCircleStore(tmpRoot);
69
+ // Same self-labelling call bin/host.ts makes at startup, wired to the real
70
+ // node's peer ID instead of an OwnerNodeClient (this test constructs
71
+ // TrustCircleService directly against a CadreNode, so no client is in play).
72
+ await ensureSelfLabel({ store, getPeerId: async () => host.peerId!.toString() });
73
+ service = new TrustCircleService({ cadreNode: host, store });
74
+ }, 60_000);
75
+
76
+ afterEach(async () => {
77
+ try { await host.stop(); } catch { /* ignore */ }
78
+ try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
79
+ });
80
+
81
+ it('issues → redeems → lists against the real control DB (listMembers path)', async () => {
82
+ // Derive a shape-valid peerId from a fresh Ed25519 key — same pattern as
83
+ // packages/cadre-core/test/seed-bootstrap.spec.ts. The AuthorizedInsert
84
+ // constraint only checks the signature over digest(PeerId,…); peerId
85
+ // *liveness* doesn't matter for the host-side CRUD path under test.
86
+ const phoneKey = await generateKeyPair('Ed25519');
87
+ const phonePeerId = peerIdFromPrivateKey(phoneKey).toString();
88
+
89
+ // Issue
90
+ const issued = await service.issueInvite({ label: "Mom's phone" });
91
+ expect(issued.token).toBeTruthy();
92
+ expect(issued.encodedInvite).toBeTruthy();
93
+
94
+ // Redeem — this calls host.acceptPhone() with the sparse reconstructed
95
+ // invite, which in turn calls authorizePeer and inserts CadrePeer.
96
+ const redeemed = await service.redeemInvite({
97
+ token: issued.token,
98
+ peerId: phonePeerId
99
+ });
100
+ expect(redeemed).toEqual({ peerId: phonePeerId, label: "Mom's phone" });
101
+
102
+ // Verify directly against the control DB: the row is there.
103
+ expect(await isInCadrePeer(host, phonePeerId)).toBe(true);
104
+
105
+ // List — exercises the CadreNode.listMembers() path. CadrePeer now carries
106
+ // the host's own self-registered row in addition to the redeemed phone.
107
+ const snap = await service.list();
108
+ expect(snap.members).toHaveLength(2);
109
+ const phoneMember = snap.members.find((m) => m.peerId === phonePeerId);
110
+ expect(phoneMember).toBeDefined();
111
+ expect(phoneMember!.label).toBe("Mom's phone");
112
+ // The host's own peerId is present too (unlabeled → bare peerId as label).
113
+ expect(snap.members.some((m) => m.peerId === host.peerId!.toString())).toBe(true);
114
+ expect(snap.pending).toHaveLength(0);
115
+ }, 90_000);
116
+
117
+ // The signed `CadrePeer` DELETE previously threw a Quereus deferred-constraint
118
+ // error ("No row context found for column PeerId") inside cadre-core's
119
+ // removePeer. That upstream bug is fixed (`quereus-cadrepeer-delete-no-row-context`,
120
+ // landed), so the full issue → redeem → remove cycle is now exercised here.
121
+ it('removes a member from CadrePeer', async () => {
122
+ const phoneKey = await generateKeyPair('Ed25519');
123
+ const phonePeerId = peerIdFromPrivateKey(phoneKey).toString();
124
+
125
+ const issued = await service.issueInvite({ label: "Mom's phone" });
126
+ await service.redeemInvite({ token: issued.token, peerId: phonePeerId });
127
+ expect(await isInCadrePeer(host, phonePeerId)).toBe(true);
128
+
129
+ await service.removeMember(phonePeerId);
130
+
131
+ expect(await isInCadrePeer(host, phonePeerId)).toBe(false);
132
+ expect(store.getMember(phonePeerId)).toBeUndefined();
133
+ const afterRemove = await service.list();
134
+ // The phone is gone; the host's own self-registered row remains.
135
+ expect(afterRemove.members.map((m) => m.peerId)).not.toContain(phonePeerId);
136
+ expect(afterRemove.members).toHaveLength(1);
137
+ expect(afterRemove.members[0]!.peerId).toBe(host.peerId!.toString());
138
+ }, 90_000);
139
+ });
140
+
141
+ async function isInCadrePeer(node: CadreNode, peerId: string): Promise<boolean> {
142
+ const db = node.getControlDatabase();
143
+ if (!db) return false;
144
+ const inner = db.getDatabase();
145
+ for await (const row of inner.eval(
146
+ 'select PeerId from CadreControl.CadrePeer where PeerId = ?',
147
+ [peerId]
148
+ )) {
149
+ if (row.PeerId === peerId) return true;
150
+ }
151
+ return false;
152
+ }