@serfab/cadre-host 0.8.1 → 0.10.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.
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.js +1 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/self-label.d.ts +36 -0
- package/dist/auth/self-label.js +57 -0
- package/dist/auth/self-label.js.map +1 -0
- package/dist/auth/trust-circle.d.ts +36 -9
- package/dist/auth/trust-circle.js +40 -20
- package/dist/auth/trust-circle.js.map +1 -1
- package/dist/auth/types.d.ts +2 -2
- package/dist/bin/host.js +291 -54
- package/dist/bin/host.js.map +1 -1
- package/dist/donation/__tests__/fake-orchestrator.d.ts +103 -0
- package/dist/donation/__tests__/fake-orchestrator.js +180 -0
- package/dist/donation/__tests__/fake-orchestrator.js.map +1 -0
- package/dist/donation/donation-service.d.ts +361 -0
- package/dist/donation/donation-service.js +763 -0
- package/dist/donation/donation-service.js.map +1 -0
- package/dist/donation/donation-store.d.ts +49 -0
- package/dist/donation/donation-store.js +148 -0
- package/dist/donation/donation-store.js.map +1 -0
- package/dist/donation/donation-supervisor.d.ts +167 -0
- package/dist/donation/donation-supervisor.js +341 -0
- package/dist/donation/donation-supervisor.js.map +1 -0
- package/dist/donation/grant-service.d.ts +71 -0
- package/dist/donation/grant-service.js +163 -0
- package/dist/donation/grant-service.js.map +1 -0
- package/dist/donation/grant-store.d.ts +38 -0
- package/dist/donation/grant-store.js +135 -0
- package/dist/donation/grant-store.js.map +1 -0
- package/dist/donation/index.d.ts +20 -0
- package/dist/donation/index.js +17 -0
- package/dist/donation/index.js.map +1 -0
- package/dist/donation/types.d.ts +201 -0
- package/dist/donation/types.js +40 -0
- package/dist/donation/types.js.map +1 -0
- package/dist/index.d.ts +9 -5
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/installer/config.d.ts +26 -0
- package/dist/installer/config.js +13 -0
- package/dist/installer/config.js.map +1 -1
- package/dist/installer/index.d.ts +2 -0
- package/dist/installer/index.js +3 -0
- package/dist/installer/index.js.map +1 -1
- package/dist/installer/wizard.d.ts +4 -0
- package/dist/installer/wizard.js +5 -1
- package/dist/installer/wizard.js.map +1 -1
- package/dist/nat/nat-service.d.ts +5 -5
- package/dist/nat/nat-service.js +9 -9
- package/dist/nat/nat-service.js.map +1 -1
- package/dist/nat/types.d.ts +1 -1
- package/dist/orchestrator/host-process-orchestrator.d.ts +144 -32
- package/dist/orchestrator/host-process-orchestrator.js +397 -128
- package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
- package/dist/orchestrator/index.d.ts +3 -3
- package/dist/orchestrator/index.js +1 -1
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/node-identity.d.ts +26 -0
- package/dist/orchestrator/node-identity.js +36 -0
- package/dist/orchestrator/node-identity.js.map +1 -0
- package/dist/orchestrator/port-allocator.d.ts +14 -0
- package/dist/orchestrator/port-allocator.js +45 -0
- package/dist/orchestrator/port-allocator.js.map +1 -1
- package/dist/orchestrator/state-store.d.ts +5 -5
- package/dist/orchestrator/state-store.js +2 -2
- package/dist/orchestrator/state-store.js.map +1 -1
- package/dist/orchestrator/types.d.ts +8 -8
- package/dist/owner/index.d.ts +11 -0
- package/dist/owner/index.js +11 -0
- package/dist/owner/index.js.map +1 -0
- package/dist/owner/owner-node-client.d.ts +110 -0
- package/dist/{authority/authority-node-client.js → owner/owner-node-client.js} +58 -17
- package/dist/owner/owner-node-client.js.map +1 -0
- package/dist/server/error-handler.d.ts +22 -1
- package/dist/server/error-handler.js +64 -1
- package/dist/server/error-handler.js.map +1 -1
- package/dist/server/events/types.d.ts +3 -0
- package/dist/server/index.d.ts +33 -2
- package/dist/server/index.js +43 -16
- package/dist/server/index.js.map +1 -1
- package/dist/server/routes/grants-admin.d.ts +17 -0
- package/dist/server/routes/grants-admin.js +32 -0
- package/dist/server/routes/grants-admin.js.map +1 -0
- package/dist/server/routes/grants.d.ts +28 -0
- package/dist/server/routes/grants.js +182 -0
- package/dist/server/routes/grants.js.map +1 -0
- package/dist/server/routes/nodes.d.ts +5 -4
- package/dist/server/routes/nodes.js +19 -18
- package/dist/server/routes/nodes.js.map +1 -1
- package/dist/server/routes/settings.d.ts +6 -1
- package/dist/server/routes/settings.js +5 -2
- package/dist/server/routes/settings.js.map +1 -1
- package/dist/server/routes/status.d.ts +8 -4
- package/dist/server/routes/status.js +9 -4
- package/dist/server/routes/status.js.map +1 -1
- package/dist/server/routes/strands.d.ts +20 -0
- package/dist/server/routes/strands.js +38 -0
- package/dist/server/routes/strands.js.map +1 -0
- package/dist/strands/index.d.ts +7 -0
- package/dist/strands/index.js +6 -0
- package/dist/strands/index.js.map +1 -0
- package/dist/strands/strand-service.d.ts +50 -0
- package/dist/strands/strand-service.js +103 -0
- package/dist/strands/strand-service.js.map +1 -0
- package/dist/strands/types.d.ts +93 -0
- package/dist/strands/types.js +23 -0
- package/dist/strands/types.js.map +1 -0
- package/dist/ui/assets/index-BVoqLjyX.css +1 -0
- package/dist/ui/assets/index-C-iks41M.js +17 -0
- package/dist/ui/assets/index-C-iks41M.js.map +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/update/release-key.d.ts +1 -1
- package/dist/update/release-key.js +8 -6
- package/dist/update/release-key.js.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/build-targets.test.ts +26 -0
- package/src/__tests__/global-setup.ts +51 -0
- package/src/__tests__/{host-authority.smoke.test.ts → host-owner.smoke.test.ts} +7 -7
- package/src/__tests__/orchestrator-env-scrub.test.ts +140 -0
- package/src/__tests__/orchestrator-node-identity.test.ts +204 -0
- package/src/__tests__/{orchestrator-authority.test.ts → orchestrator-owner.test.ts} +33 -33
- package/src/__tests__/orchestrator-pin-keys.test.ts +153 -0
- package/src/__tests__/orchestrator-push.test.ts +10 -10
- package/src/__tests__/orchestrator.test.ts +387 -6
- package/src/auth/__tests__/self-label.test.ts +85 -0
- package/src/auth/__tests__/trust-circle-integration.test.ts +152 -147
- package/src/auth/__tests__/trust-circle.test.ts +55 -11
- package/src/auth/index.ts +2 -0
- package/src/auth/self-label.ts +74 -0
- package/src/auth/trust-circle.ts +54 -22
- package/src/auth/types.ts +2 -2
- package/src/bin/host.ts +1299 -1038
- package/src/donation/__tests__/donation-service.test.ts +1068 -0
- package/src/donation/__tests__/donation-supervisor.test.ts +510 -0
- package/src/donation/__tests__/fake-orchestrator.test.ts +168 -0
- package/src/donation/__tests__/fake-orchestrator.ts +225 -0
- package/src/donation/__tests__/grant-service.test.ts +213 -0
- package/src/donation/__tests__/grant-store.test.ts +113 -0
- package/src/donation/donation-service.ts +962 -0
- package/src/donation/donation-store.ts +171 -0
- package/src/donation/donation-supervisor.ts +388 -0
- package/src/donation/grant-service.ts +200 -0
- package/src/donation/grant-store.ts +152 -0
- package/src/donation/index.ts +64 -0
- package/src/donation/types.ts +243 -0
- package/src/index.ts +51 -5
- package/src/installer/__tests__/config.test.ts +27 -1
- package/src/installer/__tests__/installer.smoke.test.ts +31 -1
- package/src/installer/__tests__/wizard.test.ts +5 -1
- package/src/installer/config.ts +34 -0
- package/src/installer/index.ts +5 -0
- package/src/installer/wizard.ts +12 -1
- package/src/nat/__tests__/nat-service.test.ts +5 -5
- package/src/nat/nat-service.ts +10 -10
- package/src/nat/types.ts +1 -1
- package/src/orchestrator/__tests__/node-identity.test.ts +62 -0
- package/src/orchestrator/host-process-orchestrator.ts +401 -134
- package/src/orchestrator/index.ts +3 -3
- package/src/orchestrator/node-identity.ts +40 -0
- package/src/orchestrator/port-allocator.ts +48 -0
- package/src/orchestrator/state-store.ts +7 -7
- package/src/orchestrator/types.ts +8 -8
- package/src/{authority/__tests__/authority-node-client.test.ts → owner/__tests__/owner-node-client.test.ts} +45 -17
- package/src/owner/index.ts +12 -0
- package/src/{authority/authority-node-client.ts → owner/owner-node-client.ts} +83 -23
- package/src/server/__tests__/error-handler.test.ts +22 -0
- package/src/server/__tests__/grants-admin-route.test.ts +73 -0
- package/src/server/__tests__/grants-route.test.ts +263 -0
- package/src/server/__tests__/nodes-route.test.ts +25 -25
- package/src/server/__tests__/server.smoke.test.ts +75 -0
- package/src/server/__tests__/settings-route.test.ts +17 -0
- package/src/server/__tests__/status-route.test.ts +21 -0
- package/src/server/__tests__/strands-route.test.ts +233 -0
- package/src/server/error-handler.ts +67 -1
- package/src/server/events/types.ts +1 -0
- package/src/server/index.ts +75 -17
- package/src/server/routes/grants-admin.ts +41 -0
- package/src/server/routes/grants.ts +210 -0
- package/src/server/routes/nodes.ts +19 -18
- package/src/server/routes/settings.ts +11 -3
- package/src/server/routes/status.ts +18 -8
- package/src/server/routes/strands.ts +54 -0
- package/src/strands/__tests__/strand-service.test.ts +165 -0
- package/src/strands/index.ts +14 -0
- package/src/strands/strand-service.ts +135 -0
- package/src/strands/types.ts +102 -0
- package/src/update/__tests__/release-key.test.ts +16 -3
- package/src/update/release-key.ts +8 -6
- package/dist/authority/authority-node-client.d.ts +0 -77
- package/dist/authority/authority-node-client.js.map +0 -1
- package/dist/authority/index.d.ts +0 -11
- package/dist/authority/index.js +0 -11
- package/dist/authority/index.js.map +0 -1
- package/dist/ui/assets/index-BlMXQPSw.js +0 -14
- package/dist/ui/assets/index-BlMXQPSw.js.map +0 -1
- package/dist/ui/assets/index-BwU5mJGF.css +0 -1
- package/src/authority/index.ts +0 -12
|
@@ -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,
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* - `
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
let
|
|
30
|
-
let
|
|
31
|
-
let
|
|
32
|
-
let
|
|
33
|
-
let
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
expect(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
expect(snap.members
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
expect(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
expect(
|
|
132
|
-
expect(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
+
}
|
|
@@ -9,29 +9,37 @@ import { TrustCircleService, createTrustCircleHandlers } from '../trust-circle.j
|
|
|
9
9
|
import { TrustCircleStore } from '../trust-circle-store.js';
|
|
10
10
|
import { TrustCircleError } from '../types.js';
|
|
11
11
|
import type { CadreNodeLike } from '../trust-circle.js';
|
|
12
|
-
import {
|
|
12
|
+
import { OwnerNodeUnavailableError } from '../../owner/owner-node-client.js';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Minimal in-memory mock of the
|
|
15
|
+
* Minimal in-memory mock of the owner-node channel adapter. Records calls
|
|
16
16
|
* so assertions can verify the auth + redemption flow without standing up a
|
|
17
17
|
* real libp2p control network or admin channel.
|
|
18
18
|
*/
|
|
19
19
|
class MockCadreNode implements CadreNodeLike {
|
|
20
|
+
/** CadrePeer rows — the real (authorized) membership test. */
|
|
20
21
|
readonly authorizedPeers = new Set<string>();
|
|
22
|
+
/**
|
|
23
|
+
* Addressable-only rows — peers that published an address record without
|
|
24
|
+
* ever being vouched for. Models the "stranger writes its own row" bug:
|
|
25
|
+
* present in `listMembers`/`isMember` (addressable), absent from
|
|
26
|
+
* `listAuthorizedMembers`/`isAuthorizedMember` (authorized).
|
|
27
|
+
*/
|
|
28
|
+
readonly unauthorizedAddressablePeers = new Set<string>();
|
|
21
29
|
readonly issuedInvites: Array<{ token?: string; expiresIn?: number }> = [];
|
|
22
30
|
readonly accepted: Array<{ phonePeerId: string; token?: string }> = [];
|
|
23
31
|
readonly removed: string[] = [];
|
|
24
32
|
|
|
25
33
|
/** Toggle to make acceptPhone reject. */
|
|
26
34
|
acceptShouldThrow: Error | null = null;
|
|
27
|
-
/** When false, membership calls throw
|
|
35
|
+
/** When false, membership calls throw OwnerNodeUnavailableError. */
|
|
28
36
|
nodeAvailable = true;
|
|
29
37
|
|
|
30
38
|
async createInvite(token?: string, expiresIn?: number) {
|
|
31
39
|
this.issuedInvites.push({ token, expiresIn });
|
|
32
40
|
const invite: CadreInvite = {
|
|
33
41
|
partyId: 'party-test',
|
|
34
|
-
|
|
42
|
+
ownerAddrs: ['/ip4/127.0.0.1/tcp/4001'],
|
|
35
43
|
token,
|
|
36
44
|
createdAt: Date.now(),
|
|
37
45
|
...(expiresIn ? { expiresAt: Date.now() + expiresIn } : {}),
|
|
@@ -48,6 +56,7 @@ class MockCadreNode implements CadreNodeLike {
|
|
|
48
56
|
async removePeer(peerId: string) {
|
|
49
57
|
this.removed.push(peerId);
|
|
50
58
|
this.authorizedPeers.delete(peerId);
|
|
59
|
+
this.unauthorizedAddressablePeers.delete(peerId);
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
encodeInvite(invite: CadreInvite): string {
|
|
@@ -55,12 +64,23 @@ class MockCadreNode implements CadreNodeLike {
|
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
async listMembers(): Promise<Array<{ peerId: string; multiaddr: string | null }>> {
|
|
58
|
-
if (!this.nodeAvailable) throw new
|
|
59
|
-
return [...this.authorizedPeers
|
|
67
|
+
if (!this.nodeAvailable) throw new OwnerNodeUnavailableError('node down');
|
|
68
|
+
return [...this.authorizedPeers, ...this.unauthorizedAddressablePeers]
|
|
69
|
+
.map((peerId) => ({ peerId, multiaddr: null }));
|
|
60
70
|
}
|
|
61
71
|
|
|
62
72
|
async isMember(peerId: string): Promise<boolean> {
|
|
63
|
-
if (!this.nodeAvailable) throw new
|
|
73
|
+
if (!this.nodeAvailable) throw new OwnerNodeUnavailableError('node down');
|
|
74
|
+
return this.authorizedPeers.has(peerId) || this.unauthorizedAddressablePeers.has(peerId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async listAuthorizedMembers(): Promise<Array<{ peerId: string; multiaddr: string | null }>> {
|
|
78
|
+
if (!this.nodeAvailable) throw new OwnerNodeUnavailableError('node down');
|
|
79
|
+
return [...this.authorizedPeers].map((peerId) => ({ peerId, multiaddr: null }));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async isAuthorizedMember(peerId: string): Promise<boolean> {
|
|
83
|
+
if (!this.nodeAvailable) throw new OwnerNodeUnavailableError('node down');
|
|
64
84
|
return this.authorizedPeers.has(peerId);
|
|
65
85
|
}
|
|
66
86
|
}
|
|
@@ -201,7 +221,7 @@ describe('TrustCircleService.redeemInvite', () => {
|
|
|
201
221
|
|
|
202
222
|
it('preserves the pending token when acceptPhone fails with node_unavailable', async () => {
|
|
203
223
|
const { token } = await service.issueInvite({ label: 'Transient' });
|
|
204
|
-
cadreNode.acceptShouldThrow = new
|
|
224
|
+
cadreNode.acceptShouldThrow = new OwnerNodeUnavailableError('node down');
|
|
205
225
|
|
|
206
226
|
await expect(service.redeemInvite({ token, peerId: 'pTrans' }))
|
|
207
227
|
.rejects.toMatchObject({ code: 'node_unavailable' });
|
|
@@ -225,7 +245,7 @@ describe('TrustCircleService.redeemInvite', () => {
|
|
|
225
245
|
// first redeem hits node_unavailable. The second redeem should still
|
|
226
246
|
// bounce synchronously while the gate is held.
|
|
227
247
|
let release!: () => void;
|
|
228
|
-
const gate = new Promise<void>((_, reject) => { release = () => reject(new
|
|
248
|
+
const gate = new Promise<void>((_, reject) => { release = () => reject(new OwnerNodeUnavailableError('node down')); });
|
|
229
249
|
cadreNode.acceptPhone = async () => {
|
|
230
250
|
await gate;
|
|
231
251
|
};
|
|
@@ -349,7 +369,7 @@ describe('TrustCircleService.list', () => {
|
|
|
349
369
|
expect(store.getMember('orphan')).toBeUndefined();
|
|
350
370
|
});
|
|
351
371
|
|
|
352
|
-
it('falls back to local labels when the
|
|
372
|
+
it('falls back to local labels when the owner node is unavailable', async () => {
|
|
353
373
|
store.addMember({ peerId: 'pZ', label: 'Zoe', addedAt: 't' });
|
|
354
374
|
cadreNode.nodeAvailable = false;
|
|
355
375
|
|
|
@@ -361,7 +381,7 @@ describe('TrustCircleService.list', () => {
|
|
|
361
381
|
|
|
362
382
|
it('issueInvite surfaces node_unavailable when the channel is down', async () => {
|
|
363
383
|
const downNode = new MockCadreNode();
|
|
364
|
-
downNode.createInvite = async () => { throw new
|
|
384
|
+
downNode.createInvite = async () => { throw new OwnerNodeUnavailableError('refused'); };
|
|
365
385
|
const svc = new TrustCircleService({ cadreNode: downNode, store });
|
|
366
386
|
await expect(svc.issueInvite({ label: 'Nina' }))
|
|
367
387
|
.rejects.toMatchObject({ code: 'node_unavailable' });
|
|
@@ -373,6 +393,30 @@ describe('TrustCircleService.list', () => {
|
|
|
373
393
|
const entry = snap.members.find(m => m.peerId === 'unlabeled-peer');
|
|
374
394
|
expect(entry?.label).toBe('unlabeled-peer');
|
|
375
395
|
});
|
|
396
|
+
|
|
397
|
+
it('excludes addressable-only peers nobody authorized', async () => {
|
|
398
|
+
cadreNode.authorizedPeers.add('invited-peer');
|
|
399
|
+
cadreNode.unauthorizedAddressablePeers.add('stranger-peer');
|
|
400
|
+
|
|
401
|
+
const snap = await service.list();
|
|
402
|
+
|
|
403
|
+
expect(snap.members.map(m => m.peerId)).toContain('invited-peer');
|
|
404
|
+
expect(snap.members.map(m => m.peerId)).not.toContain('stranger-peer');
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('keeps the self row even though the authorized set excludes self', async () => {
|
|
408
|
+
store.addMember({ peerId: 'self-peer', label: 'This device', addedAt: 't', self: true });
|
|
409
|
+
// Self is addressable (would show under listMembers) but never authorized.
|
|
410
|
+
|
|
411
|
+
const snap = await service.list();
|
|
412
|
+
|
|
413
|
+
const self = snap.members.find(m => m.peerId === 'self-peer');
|
|
414
|
+
expect(self).toBeDefined();
|
|
415
|
+
expect(self?.self).toBe(true);
|
|
416
|
+
expect(self?.label).toBe('This device');
|
|
417
|
+
// Not pruned as an orphan label despite being absent from the authorized set.
|
|
418
|
+
expect(store.getMember('self-peer')).toBeDefined();
|
|
419
|
+
});
|
|
376
420
|
});
|
|
377
421
|
|
|
378
422
|
describe('createTrustCircleHandlers', () => {
|
package/src/auth/index.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
export { TrustCircleService, createTrustCircleHandlers, DEFAULT_INVITE_TTL_MS } from './trust-circle.js';
|
|
6
6
|
export type { TrustCircleServiceOptions, CadreNodeLike } from './trust-circle.js';
|
|
7
7
|
export { TrustCircleStore } from './trust-circle-store.js';
|
|
8
|
+
export { ensureSelfLabel, SELF_LABEL } from './self-label.js';
|
|
9
|
+
export type { EnsureSelfLabelOptions } from './self-label.js';
|
|
8
10
|
export { parseDuration } from './duration.js';
|
|
9
11
|
export {
|
|
10
12
|
TrustCircleError,
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-labelling for the trust-circle listing.
|
|
3
|
+
*
|
|
4
|
+
* `TrustCircleService.list()` shows the cadre's *authorized* membership, which
|
|
5
|
+
* deliberately excludes the node's own self-published row — a node's own
|
|
6
|
+
* address record isn't something an owner key vouched for. To keep the owner's
|
|
7
|
+
* own device visible in the listing, cadre-host records a local
|
|
8
|
+
* `self: true` label for it and `list()` splices that row back in.
|
|
9
|
+
*
|
|
10
|
+
* Writing that label is this module's job. Kept out of `bin/host.ts` so it is
|
|
11
|
+
* unit-testable without standing up the CLI.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import debug from 'debug';
|
|
15
|
+
|
|
16
|
+
import type { TrustCircleStore } from './trust-circle-store.js';
|
|
17
|
+
|
|
18
|
+
const log = debug('cadre:host:trust-circle-self');
|
|
19
|
+
|
|
20
|
+
/** Label written for the owner's own device when none exists yet. */
|
|
21
|
+
export const SELF_LABEL = 'This device';
|
|
22
|
+
|
|
23
|
+
export interface EnsureSelfLabelOptions {
|
|
24
|
+
/** Local labels store to write into. */
|
|
25
|
+
store: TrustCircleStore;
|
|
26
|
+
/** Resolves the owner node's peer ID; '' when the node isn't ready yet. */
|
|
27
|
+
getPeerId: () => Promise<string>;
|
|
28
|
+
/** Clock override for tests. */
|
|
29
|
+
now?: () => Date;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Record the owner node's own peer ID as a `self: true` local label.
|
|
34
|
+
*
|
|
35
|
+
* Idempotent: an existing row for that peer ID is left alone, so an admin's
|
|
36
|
+
* rename survives restarts. Stale `self` rows (a different peer ID — the node
|
|
37
|
+
* identity was replaced) are dropped, since `list()` splices every `self` row
|
|
38
|
+
* in unconditionally and would otherwise show a device that no longer exists.
|
|
39
|
+
*
|
|
40
|
+
* Returns the peer ID that is now labelled, or `undefined` when the node
|
|
41
|
+
* wasn't ready. Throws whatever `getPeerId` throws — callers treat a failure
|
|
42
|
+
* as best-effort (the next start heals it).
|
|
43
|
+
*/
|
|
44
|
+
export async function ensureSelfLabel(opts: EnsureSelfLabelOptions): Promise<string | undefined> {
|
|
45
|
+
const { store, getPeerId } = opts;
|
|
46
|
+
const now = opts.now ?? (() => new Date());
|
|
47
|
+
|
|
48
|
+
const peerId = await getPeerId();
|
|
49
|
+
if (!peerId) {
|
|
50
|
+
log('owner node not ready — no self label written');
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const member of store.listMembers()) {
|
|
55
|
+
if (member.self && member.peerId !== peerId) {
|
|
56
|
+
log('dropping stale self label for %s (current identity is %s)', member.peerId, peerId);
|
|
57
|
+
store.removeMember(member.peerId);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const existing = store.getMember(peerId);
|
|
62
|
+
if (!existing) {
|
|
63
|
+
store.addMember({ peerId, label: SELF_LABEL, addedAt: now().toISOString(), self: true });
|
|
64
|
+
log('labelled own device %s as "%s"', peerId, SELF_LABEL);
|
|
65
|
+
} else if (!existing.self) {
|
|
66
|
+
// Row predates self-labelling (or was written by a redemption). Stamp the
|
|
67
|
+
// flag, keeping the label: without it `list()` prunes the row, since the
|
|
68
|
+
// authorized set never contains self.
|
|
69
|
+
store.addMember({ ...existing, self: true });
|
|
70
|
+
log('stamped existing label for own device %s as self', peerId);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return peerId;
|
|
74
|
+
}
|