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