@serfab/cadre-core 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.
- package/README.md +255 -253
- package/dist/bootstrap-peer-store-file.d.ts +13 -0
- package/dist/bootstrap-peer-store-file.js +42 -0
- package/dist/bootstrap-peer-store-file.js.map +1 -0
- package/dist/bootstrap-peer-store.d.ts +73 -0
- package/dist/bootstrap-peer-store.js +128 -0
- package/dist/bootstrap-peer-store.js.map +1 -0
- package/dist/cadre-node.d.ts +1121 -81
- package/dist/cadre-node.js +2276 -262
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-authorization.d.ts +93 -0
- package/dist/control-authorization.js +57 -0
- package/dist/control-authorization.js.map +1 -0
- package/dist/control-cohort.d.ts +26 -0
- package/dist/control-cohort.js +26 -0
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +959 -54
- package/dist/control-database.js +1421 -148
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +90 -10
- package/dist/control-formation-recorder.js +130 -14
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +651 -72
- package/dist/control-schema.js.map +1 -1
- package/dist/control-stream.d.ts +38 -4
- package/dist/control-stream.js +70 -4
- package/dist/control-stream.js.map +1 -1
- package/dist/control-write-retry.d.ts +161 -0
- package/dist/control-write-retry.js +412 -0
- package/dist/control-write-retry.js.map +1 -0
- package/dist/delegate-admission.d.ts +134 -0
- package/dist/delegate-admission.js +244 -0
- package/dist/delegate-admission.js.map +1 -0
- package/dist/device-token.d.ts +11 -11
- package/dist/device-token.js +14 -13
- package/dist/device-token.js.map +1 -1
- package/dist/ed25519-key.d.ts +26 -0
- package/dist/ed25519-key.js +56 -1
- package/dist/ed25519-key.js.map +1 -1
- package/dist/file-durable-slot.d.ts +20 -0
- package/dist/file-durable-slot.js +53 -0
- package/dist/file-durable-slot.js.map +1 -0
- package/dist/formation-approval.d.ts +157 -0
- package/dist/formation-approval.js +375 -0
- package/dist/formation-approval.js.map +1 -0
- package/dist/fs-atomic.d.ts +38 -0
- package/dist/fs-atomic.js +97 -0
- package/dist/fs-atomic.js.map +1 -0
- package/dist/identity-key.d.ts +49 -0
- package/dist/identity-key.js +81 -0
- package/dist/identity-key.js.map +1 -0
- package/dist/index.d.ts +17 -7
- package/dist/index.js +40 -6
- package/dist/index.js.map +1 -1
- package/dist/key-store-file.d.ts +2 -24
- package/dist/key-store-file.js +10 -77
- package/dist/key-store-file.js.map +1 -1
- package/dist/membership-connection-gater.d.ts +146 -0
- package/dist/membership-connection-gater.js +180 -0
- package/dist/membership-connection-gater.js.map +1 -0
- package/dist/node-local-snapshot.d.ts +109 -0
- package/dist/node-local-snapshot.js +182 -0
- package/dist/node-local-snapshot.js.map +1 -0
- package/dist/peer-addr-book.d.ts +92 -0
- package/dist/peer-addr-book.js +220 -0
- package/dist/peer-addr-book.js.map +1 -0
- package/dist/peer-authorization.d.ts +135 -18
- package/dist/peer-authorization.js +152 -21
- package/dist/peer-authorization.js.map +1 -1
- package/dist/peer-record.d.ts +21 -8
- package/dist/peer-record.js +26 -10
- package/dist/peer-record.js.map +1 -1
- package/dist/relay-addrs.d.ts +45 -0
- package/dist/relay-addrs.js +103 -0
- package/dist/relay-addrs.js.map +1 -0
- package/dist/relay-reservation.d.ts +266 -0
- package/dist/relay-reservation.js +613 -0
- package/dist/relay-reservation.js.map +1 -0
- package/dist/seed-bootstrap.d.ts +228 -56
- package/dist/seed-bootstrap.js +340 -198
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/seed-trust-policy.d.ts +47 -21
- package/dist/seed-trust-policy.js +24 -15
- package/dist/seed-trust-policy.js.map +1 -1
- package/dist/strand-addr-protocol.d.ts +42 -13
- package/dist/strand-addr-protocol.js +58 -52
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-backfill.d.ts +109 -0
- package/dist/strand-backfill.js +279 -0
- package/dist/strand-backfill.js.map +1 -0
- package/dist/strand-cohort.d.ts +4 -37
- package/dist/strand-cohort.js +4 -15
- package/dist/strand-cohort.js.map +1 -1
- package/dist/strand-database.d.ts +11 -16
- package/dist/strand-database.js +19 -7
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-formation-manager.d.ts +36 -7
- package/dist/strand-formation-manager.js +115 -15
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +103 -4
- package/dist/strand-formation-protocol.js +232 -4
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +88 -19
- package/dist/strand-instance-manager.js +178 -34
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-member-registry.d.ts +5 -5
- package/dist/strand-member-registry.js +18 -10
- package/dist/strand-member-registry.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +484 -88
- package/dist/strand-membership-writer.js +751 -128
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-solicitation.d.ts +99 -33
- package/dist/strand-solicitation.js +100 -52
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-transport-key.d.ts +40 -0
- package/dist/strand-transport-key.js +56 -0
- package/dist/strand-transport-key.js.map +1 -0
- package/dist/strand-wake-protocol.d.ts +9 -7
- package/dist/strand-wake-protocol.js +22 -46
- package/dist/strand-wake-protocol.js.map +1 -1
- package/dist/strand-watcher.d.ts +17 -2
- package/dist/strand-watcher.js +47 -3
- package/dist/strand-watcher.js.map +1 -1
- package/dist/trusted-owner-store-file.d.ts +12 -0
- package/dist/trusted-owner-store-file.js +39 -0
- package/dist/trusted-owner-store-file.js.map +1 -0
- package/dist/trusted-owner-store.d.ts +64 -0
- package/dist/trusted-owner-store.js +126 -0
- package/dist/trusted-owner-store.js.map +1 -0
- package/dist/types.d.ts +307 -44
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -1
- package/package.json +20 -8
- package/src/arachnode-stub.ts +159 -159
- package/src/bootstrap-peer-store-file.ts +44 -0
- package/src/bootstrap-peer-store.ts +172 -0
- package/src/cadre-node.ts +5158 -2981
- package/src/canonical-json.ts +30 -30
- package/src/control-authorization.ts +114 -0
- package/src/control-cohort.ts +130 -103
- package/src/control-database.ts +2406 -948
- package/src/control-formation-recorder.ts +305 -151
- package/src/control-schema.ts +651 -72
- package/src/control-stream.ts +205 -128
- package/src/control-write-retry.ts +483 -0
- package/src/delegate-admission.ts +302 -0
- package/src/device-token.ts +94 -89
- package/src/diagnostics/connection-path.ts +277 -277
- package/src/diagnostics/webrtc-turn-tracker.ts +222 -222
- package/src/ed25519-key.ts +66 -1
- package/src/enrollment.ts +199 -199
- package/src/file-durable-slot.ts +61 -0
- package/src/formation-approval.ts +604 -0
- package/src/fs-atomic.ts +107 -0
- package/src/hibernation-manager.ts +437 -437
- package/src/identity-key.ts +109 -0
- package/src/index.ts +137 -5
- package/src/key-store-file.ts +132 -203
- package/src/key-store.ts +99 -99
- package/src/membership-connection-gater.ts +204 -0
- package/src/node-local-snapshot.ts +260 -0
- package/src/peer-addr-book.ts +250 -0
- package/src/peer-authorization.ts +259 -81
- package/src/peer-record.ts +164 -146
- package/src/push-credentials.ts +84 -84
- package/src/push-node.ts +64 -64
- package/src/push-notifier-apns.ts +247 -247
- package/src/push-notifier-fcm.ts +220 -220
- package/src/push-notifier-shared.ts +28 -28
- package/src/push-notifier.ts +52 -52
- package/src/relay-addrs.ts +111 -0
- package/src/relay-reservation.ts +823 -0
- package/src/seed-bootstrap.ts +409 -209
- package/src/seed-trust-policy.ts +140 -107
- package/src/strand-addr-protocol.ts +441 -410
- package/src/strand-backfill.ts +366 -0
- package/src/strand-cohort.ts +38 -77
- package/src/strand-database.ts +202 -204
- package/src/strand-formation-manager.ts +491 -368
- package/src/strand-formation-protocol.ts +692 -391
- package/src/strand-instance-manager.ts +201 -46
- package/src/strand-member-key.ts +39 -39
- package/src/strand-member-registry.ts +182 -177
- package/src/strand-membership-writer.ts +938 -146
- package/src/strand-solicitation.ts +472 -407
- package/src/strand-transport-key.ts +58 -0
- package/src/strand-wake-payload.ts +32 -32
- package/src/strand-wake-protocol.ts +314 -331
- package/src/strand-watcher.ts +314 -251
- package/src/trusted-owner-store-file.ts +41 -0
- package/src/trusted-owner-store.ts +171 -0
- package/src/types.ts +1309 -1032
|
@@ -1,177 +1,182 @@
|
|
|
1
|
-
import debug from 'debug';
|
|
2
|
-
import type { Database, SqlValue } from '@quereus/quereus';
|
|
3
|
-
import type { MemberRegistration } from './types.js';
|
|
4
|
-
import type { MemberRegistry, MemberVerifier } from './enrollment.js';
|
|
5
|
-
import type { Ed25519KeyPair } from './ed25519-key.js';
|
|
6
|
-
import {
|
|
7
|
-
verifyStrandPayload,
|
|
8
|
-
consumeInvite,
|
|
9
|
-
addMemberByManager,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const log = debug('sereus:cadre:strand-member-registry');
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* The canonical payload a joining member signs to prove ownership of its strand
|
|
17
|
-
* member key, verified by {@link StrandMemberVerifier.verifyMember}.
|
|
18
|
-
*
|
|
19
|
-
* Binds the member key to the strand id so a registration signature cannot be
|
|
20
|
-
* replayed against a different strand. Peer-id binding is intentionally NOT part
|
|
21
|
-
* of this payload — `MemberPeer` rows carry their OWN per-peer self-proofs
|
|
22
|
-
* (`MemberKey || '|' || PeerId`, signed by the member) and are written by the
|
|
23
|
-
* dedicated `registerMemberPeer` writer; see {@link StrandMemberRegistry}.
|
|
24
|
-
*/
|
|
25
|
-
export function memberRegistrationPayload(registration: MemberRegistration): string {
|
|
26
|
-
return `${registration.strandId}|${registration.key}`;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** How a {@link StrandMemberRegistry} admits a member into the strand. */
|
|
30
|
-
export type StrandAdmission =
|
|
31
|
-
| {
|
|
32
|
-
/** Redeem a specific single-use invite (the invitee-side join). */
|
|
33
|
-
readonly mode: 'invite';
|
|
34
|
-
/** The `Invite.Key` (invite public key, base64url) to redeem. */
|
|
35
|
-
readonly inviteKey: string;
|
|
36
|
-
/** The matching invite private seed (base64url), received out-of-band. */
|
|
37
|
-
readonly invitePrivateKey: string;
|
|
38
|
-
}
|
|
39
|
-
| {
|
|
40
|
-
/** Admit directly by manager signature (the manager-side join). */
|
|
41
|
-
readonly mode: 'manager';
|
|
42
|
-
/** The admitting manager's strand keypair. */
|
|
43
|
-
readonly managerKeyPair: Ed25519KeyPair;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Strand-`Database`-backed {@link MemberVerifier} — the reconciliation that lets
|
|
48
|
-
* {@link EnrollmentService} make real authorization decisions against a strand's
|
|
49
|
-
* `Invite`/`ConsumedInvite`/`Member` tables instead of a placeholder.
|
|
50
|
-
*
|
|
51
|
-
* Each instance is scoped to ONE strand's database (the `Strand.*` tables live
|
|
52
|
-
* inside the connected strand DB), so the `strandId` arguments on the interface
|
|
53
|
-
* are accepted for signature compatibility but the queries run against this db.
|
|
54
|
-
*/
|
|
55
|
-
export class StrandMemberVerifier implements MemberVerifier {
|
|
56
|
-
constructor(private readonly db: Database) {}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Verify the member's self-proof over {@link memberRegistrationPayload} against
|
|
60
|
-
* the registration's own `key` (the member's ed25519 public key). This is the
|
|
61
|
-
* off-engine counterpart to the on-engine signature checks — a pre-flight gate
|
|
62
|
-
* before the constraint-enforced write.
|
|
63
|
-
*/
|
|
64
|
-
async verifyMember(registration: MemberRegistration, signature: string): Promise<boolean> {
|
|
65
|
-
const ok = verifyStrandPayload(memberRegistrationPayload(registration), signature, registration.key);
|
|
66
|
-
if (!ok) {
|
|
67
|
-
log('verifyMember: bad self-proof for member %s on strand %s', registration.key, registration.strandId);
|
|
68
|
-
}
|
|
69
|
-
return ok;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* A member is authorized to join when either it has already consumed an invite
|
|
74
|
-
* (a `ConsumedInvite` row bears its key) OR the strand has at least one
|
|
75
|
-
* outstanding `Invite` it could redeem.
|
|
76
|
-
*
|
|
77
|
-
* Strand invites are anonymous — `Invite.Key` is an invite public key, not a
|
|
78
|
-
* member key, so an un-consumed invite cannot be bound to a specific member
|
|
79
|
-
* here. This check is therefore a "door is open" pre-flight; the binding,
|
|
80
|
-
* single-use, and cryptographic gates are all enforced by the deferred
|
|
81
|
-
* `Strand.*` constraints when the member is actually written.
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*/
|
|
89
|
-
async isAuthorizedToJoin(_strandId: string, memberKey: string): Promise<boolean> {
|
|
90
|
-
const consumed = await scalarCount(
|
|
91
|
-
this.db,
|
|
92
|
-
'select count(1) as c from Strand.ConsumedInvite where MemberKey = ?',
|
|
93
|
-
[memberKey],
|
|
94
|
-
);
|
|
95
|
-
if (consumed > 0) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
this
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
*
|
|
115
|
-
* `
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
1
|
+
import debug from 'debug';
|
|
2
|
+
import type { Database, SqlValue } from '@quereus/quereus';
|
|
3
|
+
import type { MemberRegistration } from './types.js';
|
|
4
|
+
import type { MemberRegistry, MemberVerifier } from './enrollment.js';
|
|
5
|
+
import type { Ed25519KeyPair } from './ed25519-key.js';
|
|
6
|
+
import {
|
|
7
|
+
verifyStrandPayload,
|
|
8
|
+
consumeInvite,
|
|
9
|
+
addMemberByManager,
|
|
10
|
+
listOutstandingInvites,
|
|
11
|
+
} from './strand-membership-writer.js';
|
|
12
|
+
|
|
13
|
+
const log = debug('sereus:cadre:strand-member-registry');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The canonical payload a joining member signs to prove ownership of its strand
|
|
17
|
+
* member key, verified by {@link StrandMemberVerifier.verifyMember}.
|
|
18
|
+
*
|
|
19
|
+
* Binds the member key to the strand id so a registration signature cannot be
|
|
20
|
+
* replayed against a different strand. Peer-id binding is intentionally NOT part
|
|
21
|
+
* of this payload — `MemberPeer` rows carry their OWN per-peer self-proofs
|
|
22
|
+
* (`MemberKey || '|' || PeerId`, signed by the member) and are written by the
|
|
23
|
+
* dedicated `registerMemberPeer` writer; see {@link StrandMemberRegistry}.
|
|
24
|
+
*/
|
|
25
|
+
export function memberRegistrationPayload(registration: MemberRegistration): string {
|
|
26
|
+
return `${registration.strandId}|${registration.key}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** How a {@link StrandMemberRegistry} admits a member into the strand. */
|
|
30
|
+
export type StrandAdmission =
|
|
31
|
+
| {
|
|
32
|
+
/** Redeem a specific single-use invite (the invitee-side join). */
|
|
33
|
+
readonly mode: 'invite';
|
|
34
|
+
/** The `Invite.Key` (invite public key, base64url) to redeem. */
|
|
35
|
+
readonly inviteKey: string;
|
|
36
|
+
/** The matching invite private seed (base64url), received out-of-band. */
|
|
37
|
+
readonly invitePrivateKey: string;
|
|
38
|
+
}
|
|
39
|
+
| {
|
|
40
|
+
/** Admit directly by manager signature (the manager-side join). */
|
|
41
|
+
readonly mode: 'manager';
|
|
42
|
+
/** The admitting manager's strand keypair. */
|
|
43
|
+
readonly managerKeyPair: Ed25519KeyPair;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Strand-`Database`-backed {@link MemberVerifier} — the reconciliation that lets
|
|
48
|
+
* {@link EnrollmentService} make real authorization decisions against a strand's
|
|
49
|
+
* `Invite`/`ConsumedInvite`/`Member` tables instead of a placeholder.
|
|
50
|
+
*
|
|
51
|
+
* Each instance is scoped to ONE strand's database (the `Strand.*` tables live
|
|
52
|
+
* inside the connected strand DB), so the `strandId` arguments on the interface
|
|
53
|
+
* are accepted for signature compatibility but the queries run against this db.
|
|
54
|
+
*/
|
|
55
|
+
export class StrandMemberVerifier implements MemberVerifier {
|
|
56
|
+
constructor(private readonly db: Database) {}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Verify the member's self-proof over {@link memberRegistrationPayload} against
|
|
60
|
+
* the registration's own `key` (the member's ed25519 public key). This is the
|
|
61
|
+
* off-engine counterpart to the on-engine signature checks — a pre-flight gate
|
|
62
|
+
* before the constraint-enforced write.
|
|
63
|
+
*/
|
|
64
|
+
async verifyMember(registration: MemberRegistration, signature: string): Promise<boolean> {
|
|
65
|
+
const ok = verifyStrandPayload(memberRegistrationPayload(registration), signature, registration.key);
|
|
66
|
+
if (!ok) {
|
|
67
|
+
log('verifyMember: bad self-proof for member %s on strand %s', registration.key, registration.strandId);
|
|
68
|
+
}
|
|
69
|
+
return ok;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A member is authorized to join when either it has already consumed an invite
|
|
74
|
+
* (a `ConsumedInvite` row bears its key) OR the strand has at least one
|
|
75
|
+
* outstanding `Invite` it could redeem.
|
|
76
|
+
*
|
|
77
|
+
* Strand invites are anonymous — `Invite.Key` is an invite public key, not a
|
|
78
|
+
* member key, so an un-consumed invite cannot be bound to a specific member
|
|
79
|
+
* here. This check is therefore a "door is open" pre-flight; the binding,
|
|
80
|
+
* single-use, and cryptographic gates are all enforced by the deferred
|
|
81
|
+
* `Strand.*` constraints when the member is actually written.
|
|
82
|
+
*
|
|
83
|
+
* "Outstanding" is {@link listOutstandingInvites}: an `Invite` that is neither
|
|
84
|
+
* already consumed, nor cancelled by a manager, nor expired. All three exclusions
|
|
85
|
+
* mirror gates the actual write would hit (`ConsumedInvite`'s primary key,
|
|
86
|
+
* `NotCancelled`, `NotExpired`), so this pre-flight does not report a door the
|
|
87
|
+
* constraints will slam. A raw `Invite` count would over-report on all three.
|
|
88
|
+
*/
|
|
89
|
+
async isAuthorizedToJoin(_strandId: string, memberKey: string): Promise<boolean> {
|
|
90
|
+
const consumed = await scalarCount(
|
|
91
|
+
this.db,
|
|
92
|
+
'select count(1) as c from Strand.ConsumedInvite where MemberKey = ?',
|
|
93
|
+
[memberKey],
|
|
94
|
+
);
|
|
95
|
+
if (consumed > 0) {
|
|
96
|
+
// NOTE: a REVOKED member still bears its stale ConsumedInvite row (the table is
|
|
97
|
+
// insert-only, and revocation does not delete it), so this short-circuit says
|
|
98
|
+
// "authorized" for a party that can no longer actually join. Harmless because the
|
|
99
|
+
// on-engine constraints are the real gate — Member.Authorized's invite branch
|
|
100
|
+
// requires a FRESH consumption row, which a stale one is not — and this pre-flight
|
|
101
|
+
// only decides whether attempting the write is worthwhile.
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
// NOTE: materialises the whole outstanding set to answer a non-empty question (the
|
|
105
|
+
// former code was a single `select count(1)`). Deliberate — sharing one definition of
|
|
106
|
+
// "outstanding" with the manager-facing enumeration beats a second, drifting copy of
|
|
107
|
+
// the three exclusions. If this pre-flight ever runs hot on a strand with many
|
|
108
|
+
// invitations, add a short-circuiting variant rather than re-inlining the rules.
|
|
109
|
+
return (await listOutstandingInvites(this.db)).length > 0;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Strand-`Database`-backed {@link MemberRegistry} — writes real `Strand.Member`
|
|
115
|
+
* (and, in invite mode, `Strand.ConsumedInvite`) rows through the signed writer
|
|
116
|
+
* primitives, so {@link EnrollmentService.registerMember} performs the actual
|
|
117
|
+
* per-strand join handshake rather than returning "MemberRegistry not configured".
|
|
118
|
+
*
|
|
119
|
+
* The {@link StrandAdmission} chosen at construction selects which branch of
|
|
120
|
+
* `Member.Authorized` is taken:
|
|
121
|
+
* - `invite` → {@link consumeInvite} (atomic `Member` + `ConsumedInvite`), the
|
|
122
|
+
* invitee-side flow that redeems a single-use invite.
|
|
123
|
+
* - `manager` → {@link addMemberByManager}, the manager-side flow that
|
|
124
|
+
* seats a member already trusted by a manager.
|
|
125
|
+
*
|
|
126
|
+
* Scoped to one strand's db; the `strandId` argument is accepted for interface
|
|
127
|
+
* compatibility but writes target this db's `Strand.*` tables.
|
|
128
|
+
*
|
|
129
|
+
* NOTE: `peerIds` are NOT written as `MemberPeer` rows here. The signed peer path
|
|
130
|
+
* now exists as the standalone `registerMemberPeer` writer (each peer self-proof is
|
|
131
|
+
* signed by the member's own key), but wiring the enrollment `peerIds` through it is
|
|
132
|
+
* a deliberate follow-on — the member self-proof available here does not carry the
|
|
133
|
+
* per-peer signatures `MemberPeer.Authorized` requires. A non-empty `peerIds` is
|
|
134
|
+
* logged and otherwise ignored so the member still lands; peer rows are reconciled
|
|
135
|
+
* separately via `registerMemberPeer`.
|
|
136
|
+
*/
|
|
137
|
+
export class StrandMemberRegistry implements MemberRegistry {
|
|
138
|
+
constructor(
|
|
139
|
+
private readonly db: Database,
|
|
140
|
+
private readonly admission: StrandAdmission,
|
|
141
|
+
) {}
|
|
142
|
+
|
|
143
|
+
async registerMember(strandId: string, memberKey: string, peerIds: string[]): Promise<void> {
|
|
144
|
+
if (peerIds.length > 0) {
|
|
145
|
+
log(
|
|
146
|
+
'registerMember: %d peerId(s) for member %s on strand %s deferred to the registerMemberPeer path (needs per-peer self-proofs); registering member only',
|
|
147
|
+
peerIds.length, memberKey, strandId,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (this.admission.mode === 'invite') {
|
|
152
|
+
await consumeInvite(this.db, {
|
|
153
|
+
inviteKey: this.admission.inviteKey,
|
|
154
|
+
invitePrivateKey: this.admission.invitePrivateKey,
|
|
155
|
+
memberKey,
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
await addMemberByManager(this.db, {
|
|
161
|
+
managerKeyPair: this.admission.managerKeyPair,
|
|
162
|
+
memberKey,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async isMemberRegistered(_strandId: string, memberKey: string): Promise<boolean> {
|
|
167
|
+
const count = await scalarCount(
|
|
168
|
+
this.db,
|
|
169
|
+
'select count(1) as c from Strand.Member where Key = ?',
|
|
170
|
+
[memberKey],
|
|
171
|
+
);
|
|
172
|
+
return count > 0;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Run a `select count(1) as c ...` and return the scalar (0 if no row). */
|
|
177
|
+
async function scalarCount(db: Database, sql: string, params: SqlValue[]): Promise<number> {
|
|
178
|
+
for await (const row of db.eval(sql, params)) {
|
|
179
|
+
return (row.c as number) ?? 0;
|
|
180
|
+
}
|
|
181
|
+
return 0;
|
|
182
|
+
}
|