@serfab/cadre-core 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/README.md +6 -4
- 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 +1006 -101
- package/dist/cadre-node.js +1977 -283
- 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 +15 -15
- package/dist/control-cohort.js +14 -14
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +1033 -76
- package/dist/control-database.js +1520 -153
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +91 -12
- package/dist/control-formation-recorder.js +131 -16
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +774 -186
- 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 +151 -0
- package/dist/control-write-retry.js +411 -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 +12 -12
- package/dist/device-token.js +15 -14
- package/dist/device-token.js.map +1 -1
- package/dist/ed25519-key.d.ts +68 -0
- package/dist/ed25519-key.js +106 -0
- package/dist/ed25519-key.js.map +1 -0
- 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 +16 -6
- package/dist/index.js +44 -10
- 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/key-store.d.ts +1 -1
- package/dist/key-store.js +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-authorization.d.ts +150 -11
- package/dist/peer-authorization.js +170 -13
- package/dist/peer-authorization.js.map +1 -1
- package/dist/peer-record.d.ts +24 -11
- package/dist/peer-record.js +29 -13
- package/dist/peer-record.js.map +1 -1
- package/dist/push-node.d.ts +38 -0
- package/dist/push-node.js +48 -0
- package/dist/push-node.js.map +1 -0
- package/dist/push-notifier-apns.d.ts +10 -2
- package/dist/push-notifier-apns.js +14 -10
- package/dist/push-notifier-apns.js.map +1 -1
- package/dist/push-notifier-fcm.d.ts +7 -1
- package/dist/push-notifier-fcm.js +9 -10
- package/dist/push-notifier-fcm.js.map +1 -1
- package/dist/push-notifier.d.ts +16 -35
- package/dist/push-notifier.js +16 -44
- package/dist/push-notifier.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 +263 -79
- package/dist/seed-bootstrap.js +413 -211
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/seed-trust-policy.d.ts +50 -24
- package/dist/seed-trust-policy.js +28 -19
- 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-database.d.ts +5 -5
- package/dist/strand-database.js +4 -4
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-formation-manager.d.ts +38 -9
- package/dist/strand-formation-manager.js +120 -17
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +104 -5
- package/dist/strand-formation-protocol.js +233 -5
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +40 -2
- package/dist/strand-instance-manager.js +93 -15
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-member-key.d.ts +6 -6
- package/dist/strand-member-key.js +6 -6
- package/dist/strand-member-key.js.map +1 -1
- package/dist/strand-member-registry.d.ts +12 -12
- package/dist/strand-member-registry.js +22 -14
- package/dist/strand-member-registry.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +532 -138
- package/dist/strand-membership-writer.js +800 -179
- 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 +325 -50
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -1
- package/package.json +23 -8
- package/src/bootstrap-peer-store-file.ts +44 -0
- package/src/bootstrap-peer-store.ts +172 -0
- package/src/cadre-node.ts +2129 -295
- package/src/control-authorization.ts +114 -0
- package/src/control-cohort.ts +17 -17
- package/src/control-database.ts +1766 -191
- package/src/control-formation-recorder.ts +174 -21
- package/src/control-schema.ts +786 -198
- package/src/control-stream.ts +81 -4
- package/src/control-write-retry.ts +473 -0
- package/src/delegate-admission.ts +302 -0
- package/src/device-token.ts +19 -14
- package/src/ed25519-key.ts +134 -0
- package/src/file-durable-slot.ts +61 -0
- package/src/formation-approval.ts +604 -0
- package/src/fs-atomic.ts +107 -0
- package/src/identity-key.ts +109 -0
- package/src/index.ts +157 -24
- package/src/key-store-file.ts +10 -81
- package/src/key-store.ts +1 -1
- package/src/membership-connection-gater.ts +204 -0
- package/src/node-local-snapshot.ts +260 -0
- package/src/peer-authorization.ts +219 -13
- package/src/peer-record.ts +31 -13
- package/src/push-node.ts +64 -0
- package/src/push-notifier-apns.ts +16 -12
- package/src/push-notifier-fcm.ts +9 -10
- package/src/push-notifier.ts +16 -57
- package/src/relay-addrs.ts +111 -0
- package/src/relay-reservation.ts +823 -0
- package/src/seed-bootstrap.ts +490 -226
- package/src/seed-trust-policy.ts +65 -32
- package/src/strand-addr-protocol.ts +84 -53
- package/src/strand-backfill.ts +366 -0
- package/src/strand-database.ts +6 -6
- package/src/strand-formation-manager.ts +145 -19
- package/src/strand-formation-protocol.ts +310 -9
- package/src/strand-instance-manager.ts +113 -16
- package/src/strand-member-key.ts +7 -7
- package/src/strand-member-registry.ts +28 -23
- package/src/strand-membership-writer.ts +1430 -640
- package/src/strand-solicitation.ts +155 -90
- package/src/strand-transport-key.ts +58 -0
- package/src/strand-wake-protocol.ts +29 -46
- package/src/strand-watcher.ts +66 -3
- package/src/trusted-owner-store-file.ts +41 -0
- package/src/trusted-owner-store.ts +171 -0
- package/src/types.ts +342 -51
- package/dist/authority-key.d.ts +0 -41
- package/dist/authority-key.js +0 -52
- package/dist/authority-key.js.map +0 -1
- package/src/authority-key.ts +0 -68
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Database } from '@quereus/quereus';
|
|
2
2
|
import type { SAppConfig } from './types.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { Ed25519KeyPair } from './ed25519-key.js';
|
|
4
4
|
/**
|
|
5
5
|
* The engine (rules-logic system) assumed to manage every strand. Persisted in
|
|
6
6
|
* `Strand.Header.Engine`.
|
|
@@ -18,18 +18,17 @@ export declare const STRAND_ENGINE = "quereus";
|
|
|
18
18
|
*/
|
|
19
19
|
export declare const STRAND_ENGINE_VERSION = "0.1.0";
|
|
20
20
|
/**
|
|
21
|
-
* The single-
|
|
22
|
-
* verify against.
|
|
21
|
+
* The single-payload ed25519 signer the `Strand.Invite`/`Strand.ConsumedInvite`
|
|
22
|
+
* handshake constraints verify against.
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
* (e.g. `
|
|
26
|
-
* `verify(digest(new.Key
|
|
24
|
+
* Those constraints sign a single SHA-256 digest over a `'|'`-joined payload
|
|
25
|
+
* (e.g. `InviteValid` verifies
|
|
26
|
+
* `verify(digest(new.Key || '|' || coalesce(new.Expiration, '')), ...)`).
|
|
27
27
|
* So the signer hashes the payload to raw bytes and ed25519-signs *those bytes*:
|
|
28
28
|
* SQL `digest(...)`'s default output is base64url and `verify(...)`'s default
|
|
29
29
|
* `inputEncoding` is base64url, so signer and verifier operate on identical bytes.
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* joined payload field.
|
|
30
|
+
* The membership/RBAC tables (Member/Manager/MemberPeer/Revocation) instead use
|
|
31
|
+
* domain/action-tagged VARIADIC digests — see {@link signStrandApproval}.
|
|
33
32
|
*
|
|
34
33
|
* All strand keys are ed25519, so the explicit `'ed25519'` curve arg is mandatory
|
|
35
34
|
* (the crypto plugin otherwise defaults to secp256k1). Mirrors the proven
|
|
@@ -43,7 +42,7 @@ export declare function signStrandPayload(payload: string, privateKeyB64: string
|
|
|
43
42
|
/**
|
|
44
43
|
* The verifier counterpart to {@link signStrandPayload}.
|
|
45
44
|
*
|
|
46
|
-
* Mirrors what
|
|
45
|
+
* Mirrors what the single-payload `Strand.*` constraints compute:
|
|
47
46
|
* `verify(digest(payload), signature, publicKey, 'ed25519')`.
|
|
48
47
|
* SQL `digest`'s default output and `verify`'s default `inputEncoding`/`sigEncoding`/
|
|
49
48
|
* `keyEncoding` are all base64url, so the off-engine check operates on the exact
|
|
@@ -57,20 +56,64 @@ export declare function signStrandPayload(payload: string, privateKeyB64: string
|
|
|
57
56
|
* @returns `true` iff the signature is valid for the payload under that key.
|
|
58
57
|
*/
|
|
59
58
|
export declare function verifyStrandPayload(payload: string, signatureB64: string, publicKeyB64: string): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Sign a domain/action-tagged `Strand.*` membership/RBAC approval.
|
|
61
|
+
*
|
|
62
|
+
* Every `Authorized` branch on `Member`/`Manager`/`MemberPeer`/`Revocation`
|
|
63
|
+
* verifies a VARIADIC digest — `digest('<domain>', '<action>', <field>, ...,
|
|
64
|
+
* <StampId>)` — whose leading two elements are fixed literals (the domain tag,
|
|
65
|
+
* e.g. `'Strand.Member'`, and an action tag, e.g. `'add'`) and whose trailing
|
|
66
|
+
* element is the row's per-incarnation StampId. The tags keep an approval from
|
|
67
|
+
* verifying against any rule but the one it was minted for (the idiom of the
|
|
68
|
+
* control layer's `buildAuthorizationMessage`); the StampId binding makes it
|
|
69
|
+
* single-use — it authorizes exactly one incarnation of one row, and dies when
|
|
70
|
+
* that row's stamp is retired into `Strand.Revocation`.
|
|
71
|
+
*
|
|
72
|
+
* The caller passes the digest elements exactly as the matching SQL constraint
|
|
73
|
+
* lists them (each SQL argument = one array element), PRESERVING TYPE: the
|
|
74
|
+
* crypto plugin's digest framing is type-tagged (INTEGER 1 and TEXT '1' encode
|
|
75
|
+
* differently, deliberately), so `Manager.Authorized`'s promotion branch —
|
|
76
|
+
* whose digest includes the INTEGER `new.Generation` — must be signed over the
|
|
77
|
+
* NUMBER, not its string form. Integer `number` and `bigint` encode
|
|
78
|
+
* identically in that framing, so JS-side numbers match however the engine
|
|
79
|
+
* represents the SQL integer.
|
|
80
|
+
*
|
|
81
|
+
* Like {@link signStrandPayload}, the raw digest BYTES are ed25519-signed
|
|
82
|
+
* directly, matching SQL `verify(...)`'s default base64url input decoding.
|
|
83
|
+
*
|
|
84
|
+
* @param fields - The digest elements, in constraint order (tags first, StampId last).
|
|
85
|
+
* @param privateKeyB64 - The signer's base64url ed25519 private seed.
|
|
86
|
+
* @returns The base64url ed25519 signature over the tagged digest.
|
|
87
|
+
*/
|
|
88
|
+
export declare function signStrandApproval(fields: ReadonlyArray<string | number>, privateKeyB64: string): string;
|
|
89
|
+
/**
|
|
90
|
+
* Mint a fresh per-row authorization nonce (StampId) for a `Strand.Member`,
|
|
91
|
+
* `Strand.Manager`, or `Strand.MemberPeer` row.
|
|
92
|
+
*
|
|
93
|
+
* 256 bits of CSPRNG output, base64url-encoded — enough entropy that a future
|
|
94
|
+
* legitimate stamp cannot be guessed and pre-planted into `Strand.Revocation`
|
|
95
|
+
* (the `RowIsGone` comment in `schemas/strand.qsql` leans on exactly this).
|
|
96
|
+
* Plain randomness, deliberately NOT the control layer's peer-derived
|
|
97
|
+
* `generateStampId`: strand rows are not per-peer, so entropy alone carries
|
|
98
|
+
* the uniqueness.
|
|
99
|
+
*
|
|
100
|
+
* @returns A fresh base64url stamp (43 chars for 32 bytes).
|
|
101
|
+
*/
|
|
102
|
+
export declare function generateStrandStampId(): string;
|
|
60
103
|
/** Parameters for {@link bootstrapFounderMembership}. */
|
|
61
104
|
export interface FounderBootstrapParams {
|
|
62
105
|
/** The strand id — written to `Header.Id`. */
|
|
63
106
|
strandId: string;
|
|
64
|
-
/** Strand type: `'o'` (open, Header only) or `'c'` (closed, Header+Member+
|
|
107
|
+
/** Strand type: `'o'` (open, Header only) or `'c'` (closed, Header+Member+Manager). */
|
|
65
108
|
type: 'o' | 'c';
|
|
66
109
|
/** The sApp config whose id/version/schema/signature populate the Header. */
|
|
67
110
|
sApp: SAppConfig;
|
|
68
111
|
/**
|
|
69
112
|
* The founder's derived strand keypair (from {@link strandMemberKeyPair}). Its
|
|
70
|
-
* `publicKeyB64` becomes the founding `Member.Key` and `
|
|
113
|
+
* `publicKeyB64` becomes the founding `Member.Key` and `Manager.MemberKey`.
|
|
71
114
|
* Required for a closed strand; ignored for an open strand.
|
|
72
115
|
*/
|
|
73
|
-
founderKeyPair?:
|
|
116
|
+
founderKeyPair?: Ed25519KeyPair;
|
|
74
117
|
}
|
|
75
118
|
/**
|
|
76
119
|
* Founder-only one-time bootstrap of a strand's `Strand.*` membership/RBAC rows.
|
|
@@ -81,16 +124,16 @@ export interface FounderBootstrapParams {
|
|
|
81
124
|
* restart / founder re-`addStrand` is a no-op and never double-inserts.
|
|
82
125
|
*
|
|
83
126
|
* Behavior by strand type:
|
|
84
|
-
* - **Open (`'o'`)**: insert `Header` only. `Member`/`
|
|
127
|
+
* - **Open (`'o'`)**: insert `Header` only. `Member`/`Manager`/`Invite` are
|
|
85
128
|
* `OnlyClosed` and would trip that constraint — they are skipped entirely.
|
|
86
129
|
* - **Closed (`'c'`)**: insert `Header(Type='c')`, then the founding `Member`
|
|
87
|
-
* (`Key = founderKeyPair.publicKeyB64`), then the founding `
|
|
130
|
+
* (`Key = founderKeyPair.publicKeyB64`), then the founding `Manager`
|
|
88
131
|
* (`MemberKey = founderKeyPair.publicKeyB64`). Insert order matters: the
|
|
89
|
-
* Header must commit before the deferred `OnlyClosed` checks on Member/
|
|
132
|
+
* Header must commit before the deferred `OnlyClosed` checks on Member/Manager
|
|
90
133
|
* evaluate at commit.
|
|
91
134
|
*
|
|
92
135
|
* A closed strand with no `founderKeyPair` throws: a closed strand with no founding
|
|
93
|
-
*
|
|
136
|
+
* Manager could never admit anyone, so failing loudly here (which propagates out
|
|
94
137
|
* of `StrandDatabase.initialize()` and triggers the runtime's rollback) is correct.
|
|
95
138
|
*
|
|
96
139
|
* @param db - The strand's Quereus database (schema already applied).
|
|
@@ -102,10 +145,10 @@ export declare function bootstrapFounderMembership(db: Database, params: Founder
|
|
|
102
145
|
/** Parameters for {@link issueInvite}. */
|
|
103
146
|
export interface IssueInviteParams {
|
|
104
147
|
/**
|
|
105
|
-
* The issuing
|
|
106
|
-
* `Strand.
|
|
148
|
+
* The issuing manager's strand keypair. Its `publicKeyB64` must already be a
|
|
149
|
+
* `Strand.Manager` row (the `InviteValid` constraint rejects a non-manager).
|
|
107
150
|
*/
|
|
108
|
-
|
|
151
|
+
managerKeyPair: Ed25519KeyPair;
|
|
109
152
|
/**
|
|
110
153
|
* Optional invite expiry as epoch milliseconds. When set, it is canonicalised
|
|
111
154
|
* via {@link canonicalDatetime} so the signed payload segment byte-matches the
|
|
@@ -131,10 +174,10 @@ export interface IssuedInvite {
|
|
|
131
174
|
*
|
|
132
175
|
* Generates a fresh invite ed25519 keypair (the public key becomes `Invite.Key`),
|
|
133
176
|
* builds the constraint's payload (`Key || '|' || coalesce(Expiration, '')`), and
|
|
134
|
-
* signs it TWICE: with the
|
|
135
|
-
*
|
|
177
|
+
* signs it TWICE: with the manager private key (→ `ManagerSignature`, proving
|
|
178
|
+
* a manager issued it) and with the invite private key (→ `InviteSignature`,
|
|
136
179
|
* proving the issuer actually holds the invite secret). Both signatures plus the
|
|
137
|
-
*
|
|
180
|
+
* manager public key are bound as constraint context for the `Invite` insert.
|
|
138
181
|
*
|
|
139
182
|
* The returned `invitePrivateKey` is the only secret the invitee needs to redeem
|
|
140
183
|
* the invite — it is NOT stored in the strand (only `Invite.Key`, the public half,
|
|
@@ -142,7 +185,7 @@ export interface IssuedInvite {
|
|
|
142
185
|
* is `InviteKey`, so a given invite can be consumed at most once.
|
|
143
186
|
*
|
|
144
187
|
* @param db - The closed strand's database (founder already bootstrapped).
|
|
145
|
-
* @param params - The issuing
|
|
188
|
+
* @param params - The issuing manager keypair and optional expiry.
|
|
146
189
|
* @returns The invite public key and the out-of-band private seed.
|
|
147
190
|
*/
|
|
148
191
|
export declare function issueInvite(db: Database, params: IssueInviteParams): Promise<IssuedInvite>;
|
|
@@ -165,8 +208,8 @@ export interface ConsumeInviteParams {
|
|
|
165
208
|
* Redeem an invite to admit a new `Member`, atomically.
|
|
166
209
|
*
|
|
167
210
|
* `Member.Authorized`'s invite branch needs a `ConsumedInvite` row, while
|
|
168
|
-
* `ConsumedInvite`'s `MemberExists
|
|
169
|
-
*
|
|
211
|
+
* `ConsumedInvite`'s `MemberExists` needs the `Member` row — a circular
|
|
212
|
+
* dependency. Both are deferred (subquery-bearing) checks that evaluate
|
|
170
213
|
* at COMMIT, so inserting `Member` then `ConsumedInvite` inside one explicit
|
|
171
214
|
* transaction lets both rows exist when the deferred checks fire. This mirrors
|
|
172
215
|
* `ControlDatabase.redeemInvitation` (Strand + FormationUsage in one txn).
|
|
@@ -174,7 +217,9 @@ export interface ConsumeInviteParams {
|
|
|
174
217
|
* The `ConsumedInvite` insert carries an `InviteSignature` over
|
|
175
218
|
* `InviteKey || '|' || MemberKey`, proving the consumer holds the invite private
|
|
176
219
|
* key (the on-engine `ValidUsage` gate). The `Member` insert needs no member
|
|
177
|
-
* signature — its admission is the existence of the matching `ConsumedInvite
|
|
220
|
+
* signature — its admission is the existence of the matching `ConsumedInvite` —
|
|
221
|
+
* but still mints a fresh StampId (NOT NULL on every path; a revoked-then-
|
|
222
|
+
* re-invited member is a fresh incarnation under a fresh stamp).
|
|
178
223
|
*
|
|
179
224
|
* Single-use: `ConsumedInvite`'s primary key is `InviteKey`, so a second consume
|
|
180
225
|
* of the same invite is rejected by the PK (a distinct layer from the control
|
|
@@ -204,67 +249,257 @@ export interface ConsumeInviteParams {
|
|
|
204
249
|
* `ConsumedInvite` row survives).
|
|
205
250
|
*/
|
|
206
251
|
export declare function consumeInvite(db: Database, params: ConsumeInviteParams): Promise<void>;
|
|
207
|
-
/** Parameters for {@link
|
|
208
|
-
export interface
|
|
252
|
+
/** Parameters for {@link cancelInvite}. */
|
|
253
|
+
export interface CancelInviteParams {
|
|
209
254
|
/**
|
|
210
|
-
* The
|
|
211
|
-
* `Strand.
|
|
255
|
+
* The cancelling manager's strand keypair. Its `publicKeyB64` must be a
|
|
256
|
+
* PRE-EXISTING `Strand.Manager` row — `CancelledInvite.Authorized` reads
|
|
257
|
+
* `committed.Manager`, so a manager seated in the SAME transaction cannot cancel.
|
|
212
258
|
*/
|
|
213
|
-
|
|
259
|
+
managerKeyPair: Ed25519KeyPair;
|
|
260
|
+
/** The `Invite.Key` to kill. */
|
|
261
|
+
inviteKey: string;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Cancel an outstanding invitation, permanently: it may never be consumed.
|
|
265
|
+
*
|
|
266
|
+
* Files a `Strand.CancelledInvite` tombstone carrying a manager signature over
|
|
267
|
+
* `digest('Strand.CancelledInvite', 'cancel', InviteKey)`. The digest binds the exact
|
|
268
|
+
* invite key, so an approval minted for one invitation cannot cancel another.
|
|
269
|
+
*
|
|
270
|
+
* One row, one statement — no transaction needed. `CancelledInvite.Authorized` is a
|
|
271
|
+
* deferred (subquery-bearing) check, so it fires at this statement's own auto-commit.
|
|
272
|
+
*
|
|
273
|
+
* Insert-if-absent, matching {@link registerMemberPeer}'s and {@link revokeMember}'s
|
|
274
|
+
* restart-safe shape: a repeat cancellation logs and returns rather than throwing on the
|
|
275
|
+
* primary key. (Check-then-write is not atomic; the primary key is the real backstop —
|
|
276
|
+
* the guard is for the sequential repeat/restart path.)
|
|
277
|
+
*
|
|
278
|
+
* Cancellation is what makes removal a re-entry gate: it is the `ConsumedInvite` insert
|
|
279
|
+
* that `NotCancelled` blocks, and `Member.Authorized`'s invite branch needs a
|
|
280
|
+
* same-transaction FRESH `ConsumedInvite` row, so a cancelled invitation rolls the whole
|
|
281
|
+
* join back. Un-cancelling is impossible (`CancelledInvite.Immutable`) — to re-invite a
|
|
282
|
+
* party, {@link issueInvite} a fresh invitation.
|
|
283
|
+
*
|
|
284
|
+
* Enumerate what there is to cancel with {@link listOutstandingInvites}: an invitation
|
|
285
|
+
* names no invitee, so the strand cannot tell a manager WHICH invitations a departing
|
|
286
|
+
* member holds. Invitee binding is tracked as `feat-strand-invitee-bound-invites`.
|
|
287
|
+
*
|
|
288
|
+
* @param db - The closed strand's database.
|
|
289
|
+
* @param params - The cancelling manager's keypair and the invite key to kill.
|
|
290
|
+
* @throws If `CancelledInvite.Authorized` rejects (a non-manager, a manager seated in
|
|
291
|
+
* this transaction, or an approval minted for a different invite key) or `OnlyClosed`
|
|
292
|
+
* rejects (an open strand); the insert rolls back, leaving no tombstone.
|
|
293
|
+
*/
|
|
294
|
+
export declare function cancelInvite(db: Database, params: CancelInviteParams): Promise<void>;
|
|
295
|
+
/** One redeemable invitation, as reported by {@link listOutstandingInvites}. */
|
|
296
|
+
export interface OutstandingInvite {
|
|
297
|
+
/** The `Invite.Key` (invite public key, base64url). */
|
|
298
|
+
inviteKey: string;
|
|
299
|
+
/**
|
|
300
|
+
* The expiry as the engine's canonical `datetime` string
|
|
301
|
+
* (`YYYY-MM-DDTHH:MM:SS[.frac]`, no zone suffix — see {@link canonicalDatetime}), or
|
|
302
|
+
* `null` for never-expires.
|
|
303
|
+
*
|
|
304
|
+
* Deliberately the STORED form, not the epoch-ms {@link IssueInviteParams.expiration}
|
|
305
|
+
* takes: it is what compares like-for-like against another canonicalised instant, and
|
|
306
|
+
* what the on-engine `NotExpired` gate sees. It is NOT a JS-parseable ISO instant —
|
|
307
|
+
* pass it through the engine (or `canonicalDatetime`) rather than `Date.parse`.
|
|
308
|
+
*/
|
|
309
|
+
expiration: string | null;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* The invitations that are still redeemable: neither consumed, nor cancelled, nor
|
|
313
|
+
* expired at `nowMs`.
|
|
314
|
+
*
|
|
315
|
+
* `Strand.Invite` is insert-only and carries no state column, so "outstanding" is not a
|
|
316
|
+
* property of the row — it is the `Invite` set minus the `ConsumedInvite` keys, minus
|
|
317
|
+
* the `CancelledInvite` keys, minus anything whose `Expiration` has passed. All three
|
|
318
|
+
* exclusions mirror gates a consume would hit anyway (`ConsumedInvite`'s primary key,
|
|
319
|
+
* `NotCancelled`, `NotExpired`).
|
|
320
|
+
*
|
|
321
|
+
* Reads via unfiltered scans and compares in JavaScript — see {@link scanInviteKeys} for
|
|
322
|
+
* why a full-PK where-equality is not reliable on a networked strand.
|
|
323
|
+
*
|
|
324
|
+
* Expiry is compared as `expiration > canonicalDatetime(nowMs)`: the same transform
|
|
325
|
+
* {@link issueInvite} uses to STORE `Invite.Expiration`, so the string comparison orders
|
|
326
|
+
* chronologically, and the same strict `>` the on-engine `NotExpired` gate uses (expiry
|
|
327
|
+
* is exclusive — an invitation is dead at its expiry instant, not after it).
|
|
328
|
+
*
|
|
329
|
+
* This is the manager's enumeration side of {@link cancelInvite}, and the "door is open"
|
|
330
|
+
* pre-flight behind `StrandMemberVerifier.isAuthorizedToJoin`.
|
|
331
|
+
*
|
|
332
|
+
* @param db - The closed strand's database.
|
|
333
|
+
* @param nowMs - The instant (epoch ms) to compare expiries against; defaults to
|
|
334
|
+
* `Date.now()`. Tests pin it so the comparison is deterministic, mirroring
|
|
335
|
+
* {@link consumeInvite}'s `nowMs` convention.
|
|
336
|
+
* @returns The redeemable invitations, in NO guaranteed order (the scans promise only a
|
|
337
|
+
* superset of the live rows, not a stable sequence — sort by `expiration` if order
|
|
338
|
+
* matters); empty if none.
|
|
339
|
+
*/
|
|
340
|
+
export declare function listOutstandingInvites(db: Database, nowMs?: number): Promise<OutstandingInvite[]>;
|
|
341
|
+
/** Parameters for {@link addMemberByManager}. */
|
|
342
|
+
export interface AddMemberByManagerParams {
|
|
343
|
+
/**
|
|
344
|
+
* The admitting manager's strand keypair. Its `publicKeyB64` must be a
|
|
345
|
+
* `Strand.Manager` row; it signs the add-tagged digest over the new member key
|
|
346
|
+
* and its fresh stamp.
|
|
347
|
+
*/
|
|
348
|
+
managerKeyPair: Ed25519KeyPair;
|
|
214
349
|
/** The joining member's ed25519 PUBLIC key (base64url) — the new `Member.Key`. */
|
|
215
350
|
memberKey: string;
|
|
216
351
|
}
|
|
217
352
|
/**
|
|
218
|
-
* Admit a `Member` directly by
|
|
219
|
-
* path on `Member.Authorized`'s direct-
|
|
220
|
-
*
|
|
221
|
-
* The constraint verifies
|
|
222
|
-
* `
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
353
|
+
* Admit a `Member` directly by manager signature — the sibling of the invite
|
|
354
|
+
* path on `Member.Authorized`'s direct-manager branch.
|
|
355
|
+
*
|
|
356
|
+
* The constraint verifies the add-tagged digest
|
|
357
|
+
* `digest('Strand.Member', 'add', new.Key, new.StampId)` against a pre-existing
|
|
358
|
+
* (`committed.Manager`) row matching `context.ManagerKey`. The stamp is minted
|
|
359
|
+
* fresh here and bound inside the signed digest, so the approval seats exactly
|
|
360
|
+
* this incarnation: once the member is removed (retiring the stamp), the captured
|
|
361
|
+
* approval can never re-seat it. No `ConsumedInvite` is involved: this is the
|
|
362
|
+
* path a manager uses to seat a member it already trusts (e.g. a manager-side
|
|
363
|
+
* enrollment that admits a party already authorised out-of-band).
|
|
226
364
|
*
|
|
227
365
|
* @param db - The closed strand's database.
|
|
228
|
-
* @param params - The admitting
|
|
229
|
-
* @throws If `Member.Authorized` rejects (e.g. a non-
|
|
366
|
+
* @param params - The admitting manager keypair and the new member key.
|
|
367
|
+
* @throws If `Member.Authorized` rejects (e.g. a non-manager key); the insert
|
|
230
368
|
* rolls back, leaving no `Member` row.
|
|
231
369
|
*/
|
|
232
|
-
export declare function
|
|
370
|
+
export declare function addMemberByManager(db: Database, params: AddMemberByManagerParams): Promise<void>;
|
|
371
|
+
/** Parameters for {@link revokeMember}. */
|
|
372
|
+
export interface RevokeMemberParams {
|
|
373
|
+
/**
|
|
374
|
+
* The revoking manager's strand keypair. Its `publicKeyB64` must be a
|
|
375
|
+
* PRE-EXISTING `Strand.Manager` row (the manager-removal branch of
|
|
376
|
+
* `Member.Authorized` reads `committed.Manager` — a manager seated in the same
|
|
377
|
+
* transaction cannot authorize); it signs the remove-tagged digest over the
|
|
378
|
+
* target row's key and live stamp, and files the accompanying tombstone.
|
|
379
|
+
*/
|
|
380
|
+
managerKeyPair: Ed25519KeyPair;
|
|
381
|
+
/** The `Member.Key` row to delete. */
|
|
382
|
+
memberKey: string;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Revoke a `Member` on the signature of an existing manager.
|
|
386
|
+
*
|
|
387
|
+
* Reads the target row's live StampId first (a quiet no-op if the row is already
|
|
388
|
+
* absent — a repeated or restarted revocation should not throw), then runs ONE
|
|
389
|
+
* transaction pairing the delete with its `Strand.Revocation` tombstone: the
|
|
390
|
+
* schema's `Member.RevocationRecorded` requires the tombstone in the same
|
|
391
|
+
* transaction, and the tombstone is what permanently retires the stamp so the
|
|
392
|
+
* captured admission approval can never re-seat this incarnation.
|
|
393
|
+
*
|
|
394
|
+
* The manager-removal branch of `Member.Authorized` verifies the remove-tagged
|
|
395
|
+
* digest `digest('Strand.Member', 'remove', old.Key, old.StampId)` against a
|
|
396
|
+
* `committed.Manager` row matching `context.ManagerKey` — distinct from the
|
|
397
|
+
* add-tagged admission payload, and bound to the live stamp, so neither a
|
|
398
|
+
* captured admission nor a captured PRIOR removal (whose stamp differs) replays.
|
|
399
|
+
*
|
|
400
|
+
* A revoked member cannot re-admit itself off the invite it ALREADY SPENT: that
|
|
401
|
+
* `ConsumedInvite` row is stale, and the invite branch requires a
|
|
402
|
+
* same-transaction FRESH consumption. Re-admission normally takes a fresh
|
|
403
|
+
* manager action ({@link addMemberByManager} or a new invite).
|
|
404
|
+
*
|
|
405
|
+
* An UNSPENT invite the revoked party holds is killed EXPLICITLY, by the manager
|
|
406
|
+
* calling {@link cancelInvite} on it (enumerate the candidates with
|
|
407
|
+
* {@link listOutstandingInvites}); a cancelled invitation can never be consumed,
|
|
408
|
+
* and cancellation is permanent. This removal does NOT cascade into cancellation:
|
|
409
|
+
* an invitation names no invitee, so there is nothing to match the removed member
|
|
410
|
+
* against — the strand cannot tell which invitations were meant for it, or whether
|
|
411
|
+
* it holds any. Binding an invitation to its invitee is tracked as
|
|
412
|
+
* `feat-strand-invitee-bound-invites`.
|
|
413
|
+
*
|
|
414
|
+
* A manager must resign its `Manager` row before (or in the same transaction
|
|
415
|
+
* as) losing membership — `Member.NotAManager` rejects un-membering a key that
|
|
416
|
+
* still holds a Manager row. And the strand never drops to zero members
|
|
417
|
+
* (`Member.MinOneMember`, a local-count floor with the same cross-node caveat
|
|
418
|
+
* as `MinOneManager`).
|
|
419
|
+
*
|
|
420
|
+
* @param db - The closed strand's database.
|
|
421
|
+
* @param params - The revoking manager keypair and the target member key.
|
|
422
|
+
* @throws If `Member.Authorized` rejects (a non-manager or same-transaction
|
|
423
|
+
* signer), `Member.NotAManager` rejects (the target still holds a Manager
|
|
424
|
+
* row), or `Member.MinOneMember` rejects (the removal would empty the member
|
|
425
|
+
* set); the whole delete+tombstone transaction rolls back.
|
|
426
|
+
*/
|
|
427
|
+
export declare function revokeMember(db: Database, params: RevokeMemberParams): Promise<void>;
|
|
428
|
+
/** Parameters for {@link leaveStrand}. */
|
|
429
|
+
export interface LeaveStrandParams {
|
|
430
|
+
/**
|
|
431
|
+
* The departing member's OWN strand keypair. Its `publicKeyB64` is the
|
|
432
|
+
* `Member.Key` row being deleted; it self-signs the leave-tagged digest —
|
|
433
|
+
* the self-departure branch verifies the signature against `old.Key` itself,
|
|
434
|
+
* so only the departing key's holder can produce it — and files the
|
|
435
|
+
* accompanying tombstone (it is still a committed member until this very
|
|
436
|
+
* transaction removes it, which is what `Revocation.Authorized` checks). No
|
|
437
|
+
* manager is involved.
|
|
438
|
+
*/
|
|
439
|
+
memberKeyPair: Ed25519KeyPair;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* A member leaves the strand by deleting its own `Member` row.
|
|
443
|
+
*
|
|
444
|
+
* The self-departure branch of `Member.Authorized` verifies
|
|
445
|
+
* `digest('Strand.Member', 'leave', old.Key, old.StampId)` against `old.Key`
|
|
446
|
+
* itself via `context.MemberSignature`. The `'leave'` tag (vs the manager
|
|
447
|
+
* branch's `'remove'`) is belt-and-braces only — the two branches verify against
|
|
448
|
+
* different keys; the anti-replay fix is the stamp binding. Like
|
|
449
|
+
* {@link revokeMember}, the live stamp is read first (absent row → quiet no-op)
|
|
450
|
+
* and the delete pairs with its `Strand.Revocation` tombstone in one transaction.
|
|
451
|
+
*
|
|
452
|
+
* The same floors as {@link revokeMember} apply: a manager must resign first
|
|
453
|
+
* (`NotAManager`) and the last member cannot leave (`MinOneMember`).
|
|
454
|
+
*
|
|
455
|
+
* @param db - The closed strand's database.
|
|
456
|
+
* @param params - The departing member's own keypair.
|
|
457
|
+
* @throws If `Member.Authorized`, `Member.NotAManager`, or `Member.MinOneMember`
|
|
458
|
+
* rejects; the whole delete+tombstone transaction rolls back.
|
|
459
|
+
*/
|
|
460
|
+
export declare function leaveStrand(db: Database, params: LeaveStrandParams): Promise<void>;
|
|
233
461
|
/** Parameters for {@link registerMemberPeer}. */
|
|
234
462
|
export interface RegisterMemberPeerParams {
|
|
235
463
|
/**
|
|
236
464
|
* The member's OWN strand keypair (`{ privateKeyB64, publicKeyB64 }`). Its
|
|
237
465
|
* `publicKeyB64` becomes `MemberPeer.MemberKey` and it self-signs the binding —
|
|
238
466
|
* `MemberPeer.Authorized` verifies the signature against `MemberKey` itself, so a
|
|
239
|
-
* peer can only be registered by the very member it belongs to (no
|
|
467
|
+
* peer can only be registered by the very member it belongs to (no manager
|
|
240
468
|
* involved). The founder passes its `strandMemberKeyPair`; an invited member
|
|
241
|
-
* passes its own keypair. Typed as {@link
|
|
242
|
-
* base64url keypair shape — no
|
|
469
|
+
* passes its own keypair. Typed as {@link Ed25519KeyPair} only for the shared
|
|
470
|
+
* base64url keypair shape — no manager privilege is implied.
|
|
243
471
|
*/
|
|
244
|
-
memberKeyPair:
|
|
472
|
+
memberKeyPair: Ed25519KeyPair;
|
|
245
473
|
/** The peer/node id (libp2p peer id string) to associate with the member. */
|
|
246
474
|
peerId: string;
|
|
247
475
|
}
|
|
248
476
|
/**
|
|
249
477
|
* Register a network node (`PeerId`) as acting on behalf of a member.
|
|
250
478
|
*
|
|
251
|
-
* The member self-signs
|
|
252
|
-
* `MemberPeer
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
479
|
+
* The member self-signs the add-tagged digest
|
|
480
|
+
* `digest('Strand.MemberPeer', 'add', new.MemberKey, new.PeerId, new.StampId)`
|
|
481
|
+
* with its OWN key; the schema's `MemberPeer.Authorized` verifies that signature
|
|
482
|
+
* against `new.MemberKey` — i.e. the member key itself — so only the member that
|
|
483
|
+
* owns the key can register peers for it. The stamp is minted fresh here and
|
|
484
|
+
* bound inside the digest, so a captured registration approval neither
|
|
485
|
+
* re-registers a cleared binding (its stamp is retired) nor authorizes a removal
|
|
486
|
+
* (wrong tag). A deferred `MemberExists` additionally requires the `Member` row
|
|
487
|
+
* to already exist, so a peer for a non-member is rejected at commit.
|
|
488
|
+
*
|
|
489
|
+
* Insert-if-absent: a re-register on restart (or a redundant call) is a no-op. The
|
|
490
|
+
* platform DOES reject a duplicate-PK insert, but a restart-safe re-register should
|
|
491
|
+
* succeed quietly rather than throw, so the writer guards on existence instead of
|
|
492
|
+
* catching. The guard ({@link memberPeerStampId}) scans the member's peers and compares
|
|
493
|
+
* both key columns in JavaScript rather than seeking the composite primary key — see
|
|
494
|
+
* {@link scanMemberPeers} for why a full-PK point lookup is not reliable on a networked
|
|
495
|
+
* strand. A member may register multiple DISTINCT `PeerId`s (multi-device); each is its
|
|
496
|
+
* own row (and its own stamp) under the same `MemberKey`.
|
|
497
|
+
*
|
|
498
|
+
* Peer DELETION is signature-checked too, not rejected outright: `Authorized` carries
|
|
499
|
+
* distinct delete branches (self + manager), each over its own action-tagged digest
|
|
500
|
+
* bound to the row's stamp. Both removal paths live in {@link removeMemberPeer}.
|
|
501
|
+
* UPDATE is refused outright by `NoUpdate`: a re-binding is a remove plus a fresh
|
|
502
|
+
* register (under a fresh stamp).
|
|
268
503
|
*
|
|
269
504
|
* @param db - The closed strand's database (the member already exists).
|
|
270
505
|
* @param params - The member's own keypair and the peer id to bind.
|
|
@@ -272,90 +507,249 @@ export interface RegisterMemberPeerParams {
|
|
|
272
507
|
* matching `Member` row); the insert rolls back, leaving no `MemberPeer` row.
|
|
273
508
|
*/
|
|
274
509
|
export declare function registerMemberPeer(db: Database, params: RegisterMemberPeerParams): Promise<void>;
|
|
275
|
-
/**
|
|
276
|
-
|
|
510
|
+
/**
|
|
511
|
+
* Every `PeerId` currently bound to `memberKey`, in storage order.
|
|
512
|
+
*
|
|
513
|
+
* The enumeration side of {@link removeMemberPeer}: a manager cleaning up after a
|
|
514
|
+
* revocation knows only the departed member's key, never which devices it registered, so
|
|
515
|
+
* cleanup is `listMemberPeers` then one `removeMemberPeer` per id. Reads only — the rows
|
|
516
|
+
* it returns may name a `MemberKey` with no `Member` row (see the `MemberPeer` table NOTE
|
|
517
|
+
* in the schema), so this is NOT a membership test.
|
|
518
|
+
*
|
|
519
|
+
* @param db - The closed strand's database.
|
|
520
|
+
* @param memberKey - The `MemberPeer.MemberKey` to enumerate.
|
|
521
|
+
* @returns The bound peer ids, empty if the member has none.
|
|
522
|
+
*/
|
|
523
|
+
export declare function listMemberPeers(db: Database, memberKey: string): Promise<string[]>;
|
|
524
|
+
/** Parameters for the SELF branch of {@link removeMemberPeer}. */
|
|
525
|
+
export interface RemoveOwnPeerParams {
|
|
526
|
+
/**
|
|
527
|
+
* The member's OWN strand keypair. Its `publicKeyB64` is the `MemberPeer.MemberKey`
|
|
528
|
+
* of the row being deleted, and it signs the remove-tagged digest
|
|
529
|
+
* `digest('Strand.MemberPeer', 'remove', MemberKey, PeerId, StampId)` — the self
|
|
530
|
+
* branch of `MemberPeer.Authorized` verifies against the row's own member key, so
|
|
531
|
+
* no OTHER member's row is reachable this way, and the tag + stamp binding keep a
|
|
532
|
+
* captured registration approval from doubling as a removal (and vice versa).
|
|
533
|
+
*/
|
|
534
|
+
memberKeyPair: Ed25519KeyPair;
|
|
535
|
+
/** The `PeerId` of the binding to delete. */
|
|
536
|
+
peerId: string;
|
|
537
|
+
}
|
|
538
|
+
/** Parameters for the MANAGER branch of {@link removeMemberPeer}. */
|
|
539
|
+
export interface RemoveMemberPeerByManagerParams {
|
|
540
|
+
/**
|
|
541
|
+
* The clearing manager's strand keypair. Its `publicKeyB64` must be a PRE-EXISTING
|
|
542
|
+
* `Strand.Manager` row (the manager branch reads `committed.Manager`, so a manager
|
|
543
|
+
* seated in the same transaction cannot authorize); it signs the
|
|
544
|
+
* manager-remove-tagged digest over the exact row and its live stamp, and files
|
|
545
|
+
* the accompanying tombstone.
|
|
546
|
+
*/
|
|
547
|
+
managerKeyPair: Ed25519KeyPair;
|
|
548
|
+
/** The `MemberPeer.MemberKey` whose binding is being cleared. */
|
|
549
|
+
memberKey: string;
|
|
550
|
+
/** The `PeerId` of the binding to delete. */
|
|
551
|
+
peerId: string;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Either shape accepted by {@link removeMemberPeer}. A discriminated union rather than
|
|
555
|
+
* one opaque keypair: the two branches sign DIFFERENT action tags (`'remove'` self vs
|
|
556
|
+
* `'manager-remove'`), so each case carries exactly the fields its payload needs.
|
|
557
|
+
*/
|
|
558
|
+
export type RemoveMemberPeerParams = RemoveOwnPeerParams | RemoveMemberPeerByManagerParams;
|
|
559
|
+
/**
|
|
560
|
+
* Delete a `MemberPeer` binding — either the member clearing its own, or a manager
|
|
561
|
+
* clearing another member's.
|
|
562
|
+
*
|
|
563
|
+
* Reads the row's live StampId first (absent → quiet no-op, mirroring
|
|
564
|
+
* {@link registerMemberPeer}'s insert-if-absent, so a repeated or restarted cleanup
|
|
565
|
+
* is not a silent zero-row "success" the caller cannot distinguish from a real one),
|
|
566
|
+
* then runs ONE transaction pairing the delete with its `Strand.Revocation`
|
|
567
|
+
* tombstone — `MemberPeer.RevocationRecorded` requires it, and the retirement is
|
|
568
|
+
* what keeps the captured registration approval from re-binding the cleared row.
|
|
569
|
+
*
|
|
570
|
+
* `MemberPeer` rows do NOT cascade when the member is revoked (`MemberExists` runs on
|
|
571
|
+
* insert only, and nothing deletes them), so a removed member's peer bindings survive
|
|
572
|
+
* as orphans — enumerate them with {@link listMemberPeers}, which is how a manager
|
|
573
|
+
* discovers what to clear. Only that member can sign the self branch, and a member being
|
|
574
|
+
* removed against its will has no reason to cooperate — hence the manager branch, which
|
|
575
|
+
* is the cleanup path after a revocation.
|
|
576
|
+
*
|
|
577
|
+
* The manager branch is deliberately NOT gated on the member already being gone: a
|
|
578
|
+
* manager may clear a still-present member's binding too, matching "any manager can
|
|
579
|
+
* remove any member". It verifies the manager-remove-tagged
|
|
580
|
+
* `digest('Strand.MemberPeer', 'manager-remove', old.MemberKey, old.PeerId, old.StampId)`
|
|
581
|
+
* against a `committed.Manager` row; the self branch verifies the remove-tagged
|
|
582
|
+
* sibling against the row's own member key.
|
|
583
|
+
*
|
|
584
|
+
* Cleanup is an explicit follow-up call by the revoking manager, never a cascade inside
|
|
585
|
+
* {@link revokeMember}: a revocation that also had to clear an unbounded number of peer
|
|
586
|
+
* rows in the same transaction would couple two concerns and make its failure modes
|
|
587
|
+
* harder to reason about.
|
|
588
|
+
*
|
|
589
|
+
* @param db - The closed strand's database.
|
|
590
|
+
* @param params - Either the owning member's keypair, or a manager's keypair plus the
|
|
591
|
+
* target member key.
|
|
592
|
+
* @throws If `MemberPeer.Authorized` rejects (neither branch satisfied), or if the row
|
|
593
|
+
* is still present after the delete (see the point-lookup note below).
|
|
594
|
+
*/
|
|
595
|
+
export declare function removeMemberPeer(db: Database, params: RemoveMemberPeerParams): Promise<void>;
|
|
596
|
+
/** Parameters for {@link addManager}. */
|
|
597
|
+
export interface AddManagerParams {
|
|
277
598
|
/**
|
|
278
|
-
* An EXISTING
|
|
279
|
-
* `Strand.
|
|
280
|
-
* rejects a non-
|
|
281
|
-
*
|
|
599
|
+
* An EXISTING manager's strand keypair. Its `publicKeyB64` must already be a
|
|
600
|
+
* `Strand.Manager` row (the existing-manager branch of `Manager.Authorized`
|
|
601
|
+
* rejects a non-manager once the founder manager exists); it signs the
|
|
602
|
+
* add-tagged digest over the new manager key, generation, and fresh stamp,
|
|
603
|
+
* and is bound as `context.ManagerKey`.
|
|
282
604
|
*/
|
|
283
|
-
|
|
284
|
-
/** The member key to promote — the new `
|
|
285
|
-
|
|
605
|
+
byManagerKeyPair: Ed25519KeyPair;
|
|
606
|
+
/** The member key to promote — the new `Manager.MemberKey` row. */
|
|
607
|
+
newManagerKey: string;
|
|
286
608
|
}
|
|
287
609
|
/**
|
|
288
|
-
* Promote a
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
* `
|
|
293
|
-
* `
|
|
294
|
-
* `
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
610
|
+
* Promote a key that is ALREADY a `Strand.Member` to `Manager` on the signature of
|
|
611
|
+
* an existing manager — use {@link admitManager} to admit and promote atomically.
|
|
612
|
+
*
|
|
613
|
+
* The promotion branch of `Manager.Authorized` requires the authorizer to be a
|
|
614
|
+
* `Manager` row of STRICTLY SMALLER `Generation` than the new row, verifying
|
|
615
|
+
* `digest('Strand.Manager', 'add', new.MemberKey, new.Generation, new.StampId)`
|
|
616
|
+
* against `context.ManagerKey`. So this writer reads the authorizer's generation,
|
|
617
|
+
* seats the new manager at that value + 1 (the natural successor; the schema
|
|
618
|
+
* enforces only the strict ordering), mints a fresh stamp, and signs the
|
|
619
|
+
* add-tagged digest over all three: the generation inside the signed digest keeps
|
|
620
|
+
* a captured promotion from replaying at a different generation, and the stamp
|
|
621
|
+
* keeps it from re-seating a second incarnation once this one is removed.
|
|
622
|
+
* `new.Generation` is an SQL INTEGER digest arg — the digest framing is
|
|
623
|
+
* type-tagged, so the TS side signs over the NUMBER itself
|
|
624
|
+
* (see {@link signStrandApproval}).
|
|
625
|
+
*
|
|
626
|
+
* The strict ordering — not this writer — is what makes a same-transaction takeover
|
|
627
|
+
* impossible; the `Manager.Generation` column comment in `schemas/strand.qsql` carries
|
|
628
|
+
* that argument. All this writer owes it is a generation strictly above the
|
|
629
|
+
* authorizer's, inside the signed payload.
|
|
630
|
+
*
|
|
631
|
+
* When the authorizer has NO `Manager` row (a non-manager signer, or an open
|
|
632
|
+
* strand with no managers at all), the scan finds nothing and the writer falls
|
|
633
|
+
* back to generation 1 and issues the insert anyway — deliberately letting the
|
|
634
|
+
* SCHEMA be the rejector (`Manager.Authorized` / `OnlyClosed`), not a
|
|
635
|
+
* writer-thrown error, so enforcement is pinned where it actually lives.
|
|
636
|
+
*
|
|
637
|
+
* @param db - The closed strand's database (founder manager already seated).
|
|
638
|
+
* @param params - The authorizing manager's keypair and the new manager key.
|
|
639
|
+
* @throws If `Manager.Authorized` rejects, or if `Manager.MemberExists` rejects because
|
|
640
|
+
* `newManagerKey` holds no `Member` row (admit it first, or use
|
|
641
|
+
* {@link admitManager}); the insert rolls back.
|
|
642
|
+
*/
|
|
643
|
+
export declare function addManager(db: Database, params: AddManagerParams): Promise<void>;
|
|
644
|
+
/** Parameters for {@link admitManager} — the same shape as {@link AddManagerParams}. */
|
|
645
|
+
export type AdmitManagerParams = AddManagerParams;
|
|
646
|
+
/**
|
|
647
|
+
* Admit a key as a `Member` AND promote it to `Manager`, atomically — the one-step
|
|
648
|
+
* pair for a key that is not in the strand yet. {@link addManager} alone promotes a
|
|
649
|
+
* key that is already a member.
|
|
650
|
+
*
|
|
651
|
+
* ONE transaction, because `Manager.MemberExists` is a deferred check reading the LIVE
|
|
652
|
+
* `Member` table: the sibling `Member` insert satisfies it at the shared commit, and a
|
|
653
|
+
* rejection of either half rolls BOTH rows back — never a `Member` row seated by a
|
|
654
|
+
* failed promotion, nor a `Manager` row with no member behind it.
|
|
655
|
+
*
|
|
656
|
+
* NO new authority: both halves are signed by the SAME manager, over two distinct
|
|
657
|
+
* action-tagged digests — `digest('Strand.Member', 'add', …)` for the admission and
|
|
658
|
+
* `digest('Strand.Manager', 'add', …)` for the promotion — each bound to its own row's
|
|
659
|
+
* fresh stamp. This composes exactly the two approvals that manager could already
|
|
660
|
+
* issue separately.
|
|
661
|
+
*
|
|
662
|
+
* The promoting manager must be a PRE-transaction manager: the direct-admit branch of
|
|
663
|
+
* `Member.Authorized` reads `committed.Manager`, so `admitManager` cannot be chained —
|
|
664
|
+
* a manager seated by an `admitManager` earlier in the SAME transaction cannot admit
|
|
665
|
+
* the next one. (A same-transaction promotion CHAIN rooted at a pre-existing manager
|
|
666
|
+
* is fine; only the `Member` half needs the committed authorizer.)
|
|
667
|
+
*
|
|
668
|
+
* NOT insert-if-absent: a repeat call for a key that is already a member fails on the
|
|
669
|
+
* `Member` primary key, matching {@link addMemberByManager}'s unguarded shape.
|
|
670
|
+
*
|
|
671
|
+
* @param db - The closed strand's database (founder manager already seated).
|
|
672
|
+
* @param params - The authorizing manager's keypair and the key to admit + promote.
|
|
673
|
+
* @throws If either half is rejected (`Member.Authorized`, `Manager.Authorized`, a
|
|
674
|
+
* duplicate `Member` key, …); the whole transaction rolls back, leaving neither row.
|
|
305
675
|
*/
|
|
306
|
-
export declare function
|
|
307
|
-
/** Parameters for {@link
|
|
308
|
-
export interface
|
|
676
|
+
export declare function admitManager(db: Database, params: AdmitManagerParams): Promise<void>;
|
|
677
|
+
/** Parameters for {@link removeManager}. */
|
|
678
|
+
export interface RemoveManagerParams {
|
|
309
679
|
/**
|
|
310
|
-
* The keypair authorizing the removal, bound as `context.
|
|
311
|
-
*
|
|
312
|
-
*
|
|
680
|
+
* The keypair authorizing the removal, bound as `context.ManagerKey`. For an
|
|
681
|
+
* ADMIN removal this is a DIFFERENT existing manager, signing the
|
|
682
|
+
* remove-tagged digest (satisfying the existing-manager branch); for a
|
|
313
683
|
* SELF-resignation it is the target's OWN keypair (`publicKeyB64 ===
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
* {@link
|
|
684
|
+
* targetManagerKey`), signing the resign-tagged digest (satisfying the
|
|
685
|
+
* former-manager self branch). The writer picks the tag by comparing the keys —
|
|
686
|
+
* see {@link removeManager}. Either way this keypair also files the
|
|
687
|
+
* accompanying tombstone, which requires it to be a PRE-transaction MEMBER
|
|
688
|
+
* (`Revocation.Authorized` reads `committed.Member`) — true for every manager,
|
|
689
|
+
* since managers are members.
|
|
317
690
|
*/
|
|
318
|
-
|
|
319
|
-
/** The `
|
|
320
|
-
|
|
691
|
+
byManagerKeyPair: Ed25519KeyPair;
|
|
692
|
+
/** The `Manager.MemberKey` row to delete. */
|
|
693
|
+
targetManagerKey: string;
|
|
321
694
|
}
|
|
322
695
|
/**
|
|
323
|
-
* Remove
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
696
|
+
* Remove a `Manager` row — either an admin removing a different manager or a
|
|
697
|
+
* manager resigning itself.
|
|
698
|
+
*
|
|
699
|
+
* Reads the target row's live `(Generation, StampId)` first (absent → quiet
|
|
700
|
+
* no-op, same restart-safe shape as {@link revokeMember}), then runs ONE
|
|
701
|
+
* transaction pairing the delete with its `Strand.Revocation` tombstone
|
|
702
|
+
* (`Manager.RevocationRecorded`). The two delete-side branches of
|
|
703
|
+
* `Manager.Authorized` verify DIFFERENT action-tagged digests, each bound to
|
|
704
|
+
* `(old.MemberKey, old.StampId)`:
|
|
705
|
+
* - **Admin removal** (`byManagerKeyPair` is a different existing manager): the
|
|
706
|
+
* removal branch verifies `digest('Strand.Manager', 'remove', old.MemberKey,
|
|
707
|
+
* old.StampId)` against `A.MemberKey = context.ManagerKey`. Deliberately NO
|
|
708
|
+
* generation condition: a later-generation manager may remove an
|
|
709
|
+
* earlier-generation one (generation is lineage, not privilege), and deletes
|
|
710
|
+
* are safe once inserts are — every accepting branch requires a `Manager` row
|
|
711
|
+
* in the post-image, and the promotion branch's generation ordering keeps
|
|
712
|
+
* attacker rows out of it. The stamp binding is what makes a captured removal
|
|
713
|
+
* single-use: a re-promoted manager carries a fresh stamp the old approval
|
|
714
|
+
* does not match — closing the removal half of
|
|
715
|
+
* `bug-strand-manager-authority-antireplay` (the insert half was already
|
|
716
|
+
* closed by the generation-bearing promotion payload).
|
|
717
|
+
* - **Self-resignation** (`byManagerKeyPair` IS the target): the former-manager
|
|
718
|
+
* self branch verifies `digest('Strand.Manager', 'resign', old.MemberKey,
|
|
719
|
+
* old.StampId)` against `old.MemberKey` itself. The distinct `'resign'` tag is
|
|
720
|
+
* belt-and-braces (different verifying keys); the anti-replay fix is the stamp.
|
|
721
|
+
*
|
|
722
|
+
* The caller selects the case purely by which keypair it passes; the only
|
|
723
|
+
* branching here is the action tag, chosen by comparing the signer to the target.
|
|
724
|
+
*
|
|
725
|
+
* The optimystic bootstrap-mode transactor evaluates deferred (subquery-bearing)
|
|
726
|
+
* CHECK constraints on DELETE as well as INSERT, so `Manager.Authorized` — deferred —
|
|
727
|
+
* IS enforced here: a signer that is neither an existing manager nor the target
|
|
728
|
+
* itself is rejected at commit.
|
|
729
|
+
*
|
|
730
|
+
* THE LAST MANAGER CANNOT BE REMOVED. `Manager.MinOneManager` (`check on delete`,
|
|
731
|
+
* deferred, so it sees the POST-delete count) rejects any delete that would leave the
|
|
732
|
+
* strand with zero managers — an admin-less closed strand could never admit anyone
|
|
733
|
+
* again, since `Invite`, `addMemberByManager`, and `addManager` all require a
|
|
734
|
+
* `Manager` row. The bootstrap branch of `Manager.Authorized` is gated to INSERTs
|
|
735
|
+
* (`old.MemberKey is null`), so it does not waive the signature check on a delete
|
|
736
|
+
* that drops the count toward the floor: a second-to-last removal is authorized
|
|
737
|
+
* exactly like any other.
|
|
738
|
+
*
|
|
739
|
+
* HAND-OFF ORDER: a sole manager transferring control must ADD the successor FIRST
|
|
740
|
+
* and resign SECOND. A same-transaction delete-then-insert swap is rejected — the
|
|
741
|
+
* bootstrap branch is gated to the founding state (at most one `Member`), and the
|
|
742
|
+
* successor's insert has no other manager to authorize it once the sole manager's
|
|
743
|
+
* row is gone.
|
|
744
|
+
*
|
|
745
|
+
* Cross-node caveat: `MinOneManager` counts rows visible to THIS transaction, so two
|
|
746
|
+
* nodes concurrently removing different managers can each see a survivor and still
|
|
747
|
+
* converge to zero. Noted in the schema; a cross-node floor is not attempted here.
|
|
355
748
|
*
|
|
356
749
|
* @param db - The closed strand's database.
|
|
357
|
-
* @param params - The authorizing keypair and the target
|
|
358
|
-
* @throws If `
|
|
359
|
-
*
|
|
750
|
+
* @param params - The authorizing keypair and the target manager key.
|
|
751
|
+
* @throws If `Manager.Authorized` rejects (a signer that is neither another existing
|
|
752
|
+
* manager nor the target itself) or `Manager.MinOneManager` rejects (the removal
|
|
753
|
+
* would leave no managers); the whole delete+tombstone transaction rolls back.
|
|
360
754
|
*/
|
|
361
|
-
export declare function
|
|
755
|
+
export declare function removeManager(db: Database, params: RemoveManagerParams): Promise<void>;
|