@serfab/cadre-core 1.13.0 → 1.15.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 +293 -274
- package/dist/cadre-invite-protocol.d.ts +383 -0
- package/dist/cadre-invite-protocol.js +774 -0
- package/dist/cadre-invite-protocol.js.map +1 -0
- package/dist/cadre-join-request.d.ts +42 -0
- package/dist/cadre-join-request.js +144 -0
- package/dist/cadre-join-request.js.map +1 -0
- package/dist/cadre-node.d.ts +849 -210
- package/dist/cadre-node.js +1574 -374
- package/dist/cadre-node.js.map +1 -1
- package/dist/claim-proof.d.ts +45 -0
- package/dist/claim-proof.js +132 -0
- package/dist/claim-proof.js.map +1 -0
- package/dist/control-authorization.d.ts +80 -19
- package/dist/control-authorization.js +55 -13
- package/dist/control-authorization.js.map +1 -1
- package/dist/control-cohort.js +1 -1
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +605 -108
- package/dist/control-database.js +1299 -280
- package/dist/control-database.js.map +1 -1
- package/dist/control-protocol-guard.d.ts +142 -0
- package/dist/control-protocol-guard.js +293 -0
- package/dist/control-protocol-guard.js.map +1 -0
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +817 -1067
- package/dist/control-schema.js.map +1 -1
- package/dist/control-stream.d.ts +4 -0
- package/dist/control-stream.js +22 -7
- package/dist/control-stream.js.map +1 -1
- package/dist/delegate-admission.d.ts +1 -1
- package/dist/delegate-admission.js +1 -1
- package/dist/device-token.d.ts +11 -9
- package/dist/device-token.js +14 -12
- package/dist/device-token.js.map +1 -1
- package/dist/ed25519-key.d.ts +22 -9
- package/dist/ed25519-key.js +47 -10
- package/dist/ed25519-key.js.map +1 -1
- package/dist/fetch-impl.d.ts +12 -0
- package/dist/fetch-impl.js +23 -0
- package/dist/fetch-impl.js.map +1 -0
- package/dist/file-durable-slot.js +2 -1
- package/dist/file-durable-slot.js.map +1 -1
- package/dist/file-safe-name.d.ts +25 -0
- package/dist/file-safe-name.js +29 -0
- package/dist/file-safe-name.js.map +1 -0
- package/dist/formation-approval.js +5 -11
- package/dist/formation-approval.js.map +1 -1
- package/dist/fs-atomic.d.ts +0 -19
- package/dist/fs-atomic.js +3 -24
- package/dist/fs-atomic.js.map +1 -1
- package/dist/index.d.ts +22 -10
- package/dist/index.js +54 -11
- package/dist/index.js.map +1 -1
- package/dist/invitation-bootstrap.d.ts +50 -12
- package/dist/invitation-bootstrap.js +76 -13
- package/dist/invitation-bootstrap.js.map +1 -1
- package/dist/joined-strand-store.d.ts +7 -0
- package/dist/joined-strand-store.js +9 -0
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/key-store-file.js +2 -1
- package/dist/key-store-file.js.map +1 -1
- package/dist/link-budget.d.ts +25 -33
- package/dist/link-budget.js +25 -35
- package/dist/link-budget.js.map +1 -1
- package/dist/machine-removal.d.ts +78 -0
- package/dist/machine-removal.js +166 -0
- package/dist/machine-removal.js.map +1 -0
- package/dist/membership-connection-gater.d.ts +191 -151
- package/dist/membership-connection-gater.js +236 -173
- package/dist/membership-connection-gater.js.map +1 -1
- package/dist/node-claim-payload.d.ts +74 -0
- package/dist/node-claim-payload.js +224 -0
- package/dist/node-claim-payload.js.map +1 -0
- package/dist/node-default-transports.d.ts +27 -0
- package/dist/node-default-transports.js +35 -0
- package/dist/node-default-transports.js.map +1 -0
- package/dist/node-transports.d.ts +30 -0
- package/dist/node-transports.js +40 -0
- package/dist/node-transports.js.map +1 -0
- package/dist/owner-anchor-sync.d.ts +22 -0
- package/dist/owner-anchor-sync.js +143 -0
- package/dist/owner-anchor-sync.js.map +1 -0
- package/dist/peer-authorization.d.ts +113 -18
- package/dist/peer-authorization.js +225 -29
- package/dist/peer-authorization.js.map +1 -1
- package/dist/peer-dial.d.ts +143 -27
- package/dist/peer-dial.js +276 -31
- package/dist/peer-dial.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +26 -2
- package/dist/peer-join-backfill.js +60 -12
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/peer-record.d.ts +17 -52
- package/dist/peer-record.js +20 -58
- package/dist/peer-record.js.map +1 -1
- package/dist/pending-join-runner.d.ts +66 -58
- package/dist/pending-join-runner.js +136 -145
- package/dist/pending-join-runner.js.map +1 -1
- package/dist/primary-lan-address.d.ts +8 -0
- package/dist/primary-lan-address.js +46 -0
- package/dist/primary-lan-address.js.map +1 -0
- package/dist/provider/client.d.ts +131 -0
- package/dist/provider/client.js +226 -0
- package/dist/provider/client.js.map +1 -0
- package/dist/provider/descriptor.d.ts +41 -0
- package/dist/provider/descriptor.js +103 -0
- package/dist/provider/descriptor.js.map +1 -0
- package/dist/provider/index.d.ts +9 -0
- package/dist/provider/index.js +10 -0
- package/dist/provider/index.js.map +1 -0
- package/dist/provider/oauth.d.ts +125 -0
- package/dist/provider/oauth.js +199 -0
- package/dist/provider/oauth.js.map +1 -0
- package/dist/provider/rent-node.d.ts +101 -0
- package/dist/provider/rent-node.js +220 -0
- package/dist/provider/rent-node.js.map +1 -0
- package/dist/push-fanout.d.ts +8 -2
- package/dist/push-fanout.js +8 -2
- package/dist/push-fanout.js.map +1 -1
- package/dist/relay-addrs.d.ts +9 -4
- package/dist/relay-addrs.js +9 -4
- package/dist/relay-addrs.js.map +1 -1
- package/dist/relay-reservation.d.ts +51 -16
- package/dist/relay-reservation.js +107 -24
- package/dist/relay-reservation.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +165 -123
- package/dist/seed-bootstrap.js +313 -275
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/seed-trust-policy.d.ts +98 -37
- package/dist/seed-trust-policy.js +188 -2
- package/dist/seed-trust-policy.js.map +1 -1
- package/dist/strand-addr-protocol.d.ts +24 -26
- package/dist/strand-addr-protocol.js +19 -26
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-commit-durability.d.ts +54 -0
- package/dist/strand-commit-durability.js +74 -0
- package/dist/strand-commit-durability.js.map +1 -0
- package/dist/strand-database.d.ts +23 -8
- package/dist/strand-database.js +41 -13
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +7 -1
- package/dist/strand-first-sync-gate.js +7 -1
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-formation-protocol.js +5 -2
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-fret-addrs.d.ts +19 -1
- package/dist/strand-fret-addrs.js +27 -9
- package/dist/strand-fret-addrs.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +81 -19
- package/dist/strand-instance-manager.js +216 -105
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-move.d.ts +124 -0
- package/dist/strand-move.js +438 -0
- package/dist/strand-move.js.map +1 -0
- package/dist/strand-network-config.js +7 -6
- package/dist/strand-network-config.js.map +1 -1
- package/dist/strand-peer-redial.d.ts +113 -0
- package/dist/strand-peer-redial.js +236 -0
- package/dist/strand-peer-redial.js.map +1 -0
- package/dist/strand-revocation-enforcer.d.ts +11 -11
- package/dist/strand-revocation-enforcer.js +11 -11
- package/dist/strand-wake-protocol.d.ts +13 -16
- package/dist/strand-wake-protocol.js +23 -27
- package/dist/strand-wake-protocol.js.map +1 -1
- package/dist/trusted-owner-store.d.ts +35 -5
- package/dist/trusted-owner-store.js +50 -8
- package/dist/trusted-owner-store.js.map +1 -1
- package/dist/types.d.ts +512 -89
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -1
- package/package.json +19 -10
- package/src/cadre-invite-protocol.ts +1052 -0
- package/src/cadre-join-request.ts +167 -0
- package/src/cadre-node.ts +1703 -386
- package/src/claim-proof.ts +147 -0
- package/src/control-authorization.ts +203 -117
- package/src/control-cohort.ts +1 -1
- package/src/control-database.ts +4663 -3453
- package/src/control-protocol-guard.ts +348 -0
- package/src/control-schema.ts +828 -1078
- package/src/control-stream.ts +26 -7
- package/src/delegate-admission.ts +1 -1
- package/src/device-token.ts +14 -10
- package/src/ed25519-key.ts +47 -10
- package/src/fetch-impl.ts +23 -0
- package/src/file-durable-slot.ts +64 -63
- package/src/file-safe-name.ts +32 -0
- package/src/formation-approval.ts +604 -610
- package/src/fs-atomic.ts +92 -117
- package/src/index.ts +789 -648
- package/src/invitation-bootstrap.ts +91 -13
- package/src/joined-strand-store.ts +508 -498
- package/src/key-store-file.ts +142 -141
- package/src/link-budget.ts +25 -36
- package/src/machine-removal.ts +216 -0
- package/src/membership-connection-gater.ts +259 -191
- package/src/node-claim-payload.ts +259 -0
- package/src/node-default-transports.ts +38 -0
- package/src/node-transports.ts +49 -0
- package/src/owner-anchor-sync.ts +171 -0
- package/src/peer-authorization.ts +258 -29
- package/src/peer-dial.ts +604 -315
- package/src/peer-join-backfill.ts +62 -11
- package/src/peer-record.ts +159 -197
- package/src/pending-join-runner.ts +729 -735
- package/src/primary-lan-address.ts +47 -0
- package/src/provider/client.ts +326 -0
- package/src/provider/descriptor.ts +129 -0
- package/src/provider/index.ts +59 -0
- package/src/provider/oauth.ts +285 -0
- package/src/provider/rent-node.ts +275 -0
- package/src/push-fanout.ts +8 -2
- package/src/relay-addrs.ts +9 -4
- package/src/relay-reservation.ts +1211 -1121
- package/src/seed-bootstrap.ts +1502 -1448
- package/src/seed-trust-policy.ts +353 -140
- package/src/strand-addr-protocol.ts +26 -35
- package/src/strand-commit-durability.ts +116 -0
- package/src/strand-database.ts +46 -14
- package/src/strand-first-sync-gate.ts +7 -1
- package/src/strand-formation-protocol.ts +1039 -1036
- package/src/strand-fret-addrs.ts +179 -145
- package/src/strand-instance-manager.ts +2026 -1883
- package/src/strand-move.ts +585 -0
- package/src/strand-network-config.ts +7 -6
- package/src/strand-peer-redial.ts +307 -0
- package/src/strand-revocation-enforcer.ts +11 -11
- package/src/strand-wake-protocol.ts +23 -30
- package/src/trusted-owner-store.ts +242 -171
- package/src/types.ts +2687 -2242
|
@@ -2,8 +2,8 @@ import { Database } from '@quereus/quereus';
|
|
|
2
2
|
import type { SqlParameters } from '@quereus/quereus';
|
|
3
3
|
import type { Libp2p } from '@libp2p/interface';
|
|
4
4
|
import type { IRepo } from '@optimystic/db-core';
|
|
5
|
-
import type { StrandRow,
|
|
6
|
-
import type { ControlTable, RevocableTable,
|
|
5
|
+
import type { StrandRow, JoinRequestRow, JoinOutcome, PendingJoin, NodeProfile, PeerAddressRecord, CadrePeerRow, OwnerKeyRow, RevocationRow, RevocationLedgerOpenResult, DeviceTokenRecord, DeviceTokenRow, CadreInviteRow, CadreInviteUsageRow, CadreInviteRedemptionResult, CadreInviteStatus, SeedPeer, MachineRemovalResult } from './types.js';
|
|
6
|
+
import type { ControlTable, RevocableTable, BoundControlDomain, ControlAction, CadreInviteSignedFields } from './control-authorization.js';
|
|
7
7
|
import type { ControlRetryAbandonment } from './control-retry.js';
|
|
8
8
|
export type { ControlTable, RevocableTable, ControlDomain, ControlAction } from './control-authorization.js';
|
|
9
9
|
/**
|
|
@@ -50,13 +50,15 @@ export declare class FormationAbortedError extends Error {
|
|
|
50
50
|
});
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
|
-
* The invitation's seat budget is already spent: the count of recorded
|
|
54
|
-
*
|
|
55
|
-
*
|
|
53
|
+
* The invitation's seat budget is already spent: the count of recorded usage rows for the
|
|
54
|
+
* invitation has reached its `TotalUses`, so this redemption cannot be given a seat. `token`
|
|
55
|
+
* is the `FormationInvite.Token` or the `CadreInvite.Key`, whichever table the caller
|
|
56
|
+
* redeemed against.
|
|
56
57
|
*
|
|
57
58
|
* Raised off the committed-count check ({@link ControlDatabase.assertSeatRemains}) ahead of
|
|
58
59
|
* the write, inside {@link ControlDatabase.redeemInvitation} /
|
|
59
|
-
* {@link ControlDatabase.recordFormationUsage},
|
|
60
|
+
* {@link ControlDatabase.recordFormationUsage}, off the same check for a cadre invitation
|
|
61
|
+
* ({@link ControlDatabase.redeemCadreInvite}), and off the unlocked pre-check
|
|
60
62
|
* `ControlFormationUsageRecorder.authorizeUsage` runs before the manager issues a membership
|
|
61
63
|
* pass — and off nothing else. It exists because
|
|
62
64
|
* without it the refusal surfaces as a generic `CHECK constraint failed: Authorized` from the
|
|
@@ -73,6 +75,20 @@ export declare class InvitationExhaustedError extends Error {
|
|
|
73
75
|
readonly totalUses: number;
|
|
74
76
|
constructor(token: string, usesRecorded: number, totalUses: number);
|
|
75
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* {@link ControlDatabase.seatCadreInvite} was refused by `CadreInvite.AuthorizedInsert`: the
|
|
80
|
+
* row's `IssuerKey` is not an owner on THIS node, or its `IssuerSig` does not verify. The
|
|
81
|
+
* common cause is the first — the issuing owner's `OwnerKey` row has not replicated here yet,
|
|
82
|
+
* or that owner has since been removed — which is why the redemption protocol maps this to a
|
|
83
|
+
* retryable code; a tampered row fails the same way and simply never succeeds.
|
|
84
|
+
*/
|
|
85
|
+
export declare class CadreInviteIssuerUnknownError extends Error {
|
|
86
|
+
readonly inviteKey: string;
|
|
87
|
+
readonly issuerKey: string;
|
|
88
|
+
constructor(inviteKey: string, issuerKey: string, options?: {
|
|
89
|
+
cause?: unknown;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
76
92
|
/**
|
|
77
93
|
* What a recorded `FormationUsage` row came out as.
|
|
78
94
|
*
|
|
@@ -84,20 +100,26 @@ export interface FormationUsageResult {
|
|
|
84
100
|
/** The single-use nonce written to `UsageStampId` (the row's primary key) — the caller's, or a freshly minted one. */
|
|
85
101
|
usageStampId: string;
|
|
86
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Parse a nullable stored `datetime` column into epoch ms, mapping both "absent"
|
|
105
|
+
* and "unparseable" to null — i.e. "no expiry". Shared by every `ExpiresAt`
|
|
106
|
+
* reader so the NaN guard cannot drift between them.
|
|
107
|
+
*/
|
|
108
|
+
export declare function parseNullableStoredDatetimeMs(value: string | number | null | undefined): number | null;
|
|
87
109
|
/**
|
|
88
110
|
* Build the canonical authorization message that owner signatures are bound to.
|
|
89
111
|
*
|
|
90
112
|
* The message is a SINGLE framed SHA-256 digest over the ordered field vector from
|
|
91
113
|
* {@link controlAuthorizationFields} (the crypto plugin's injective multi-field
|
|
92
114
|
* encoding): two fixed literals — the domain tag naming the table rule and the action
|
|
93
|
-
* tag —
|
|
94
|
-
* StampId as the final field where the table has one:
|
|
115
|
+
* tag — then the party id, then the row fields in the schema's fixed order, with the
|
|
116
|
+
* single-use StampId as the final field where the table has one:
|
|
95
117
|
*
|
|
96
|
-
* message = sha256(encodeFields([domain, action, field_1, ..., StampId])) // raw digest bytes
|
|
118
|
+
* message = sha256(encodeFields([domain, action, partyId, field_1, ..., StampId])) // raw digest bytes
|
|
97
119
|
*
|
|
98
120
|
* ed25519 signs these raw digest bytes DIRECTLY (no second hash). The SQL constraints
|
|
99
121
|
* verify the identical bytes with one variadic call
|
|
100
|
-
* (`verify(digest('CadreControl.X', 'add', field_1, ...), context.Signature, A.Key, 'ed25519')`):
|
|
122
|
+
* (`verify(digest('CadreControl.X', 'add', party_id(), field_1, ...), context.Signature, A.Key, 'ed25519')`):
|
|
101
123
|
* SQL `digest(...)` returns the base64url string of the same digest, which `verify`'s default
|
|
102
124
|
* base64url input encoding decodes back to those raw bytes — so signer and verifier
|
|
103
125
|
* operate on the same bytes. Every field is TEXT on both sides (the SQL columns are
|
|
@@ -105,11 +127,33 @@ export interface FormationUsageResult {
|
|
|
105
127
|
* type tags agree. Binding the row contents closes captured-stamp replay; the leading
|
|
106
128
|
* domain/action tags scope the signature to ONE table rule, so an approval minted for
|
|
107
129
|
* one constraint can never satisfy another (e.g. a ValidationKey enrollment can no
|
|
108
|
-
* longer double as an OwnerKey enrollment)
|
|
109
|
-
*
|
|
110
|
-
*
|
|
130
|
+
* longer double as an OwnerKey enrollment); the party id scopes it to ONE party, so two
|
|
131
|
+
* parties sharing an owner key never accept each other's approvals. `partyId` is the
|
|
132
|
+
* party the signer is configured for, and the SQL side reads its own through the
|
|
133
|
+
* node-local `party_id()` ({@link registerPartyIdFunction}). Single source of truth:
|
|
134
|
+
* every signed writer (and every test/harness signer) MUST build the message through
|
|
135
|
+
* this function with the schema's tags and field order, or `verify` will reject the row.
|
|
136
|
+
*/
|
|
137
|
+
export declare function buildAuthorizationMessage(domain: BoundControlDomain, action: ControlAction, partyId: string, rowFields: string[]): Uint8Array;
|
|
138
|
+
/**
|
|
139
|
+
* Register the zero-argument scalar `party_id()` on `db`: the party id this machine is
|
|
140
|
+
* configured for, which every party-bound `digest(...)` in the control schema puts third.
|
|
141
|
+
* Node-local by design — a `context` value would be the writer's choice (a cohort member
|
|
142
|
+
* that re-ran the statement would replay it as a literal), and a stored `Party` table would
|
|
143
|
+
* be replicated data the schema already treats as untrusted. A cohort member that re-runs a
|
|
144
|
+
* statement therefore evaluates its OWN party id, and the schema text stays identical for
|
|
145
|
+
* every party. Each `Database` carries its own registration, so a process serving two
|
|
146
|
+
* parties holds two.
|
|
147
|
+
*
|
|
148
|
+
* NOTE: flagged deterministic, so the planner may fold the call into a plan; plans are
|
|
149
|
+
* per `Database` today. If Quereus ever shares plans across `Database` instances, drop
|
|
150
|
+
* the DETERMINISTIC flag or one party's id would be folded into another's checks.
|
|
151
|
+
*
|
|
152
|
+
* `ControlDatabase.initialize` calls this before hydrating persisted schemas, because a
|
|
153
|
+
* warm restart parses the persisted CHECK text, which calls it. Exported for specs that
|
|
154
|
+
* build their own `Database` around a copy of a bound constraint.
|
|
111
155
|
*/
|
|
112
|
-
export declare function
|
|
156
|
+
export declare function registerPartyIdFunction(db: Database, partyId: string): void;
|
|
113
157
|
/**
|
|
114
158
|
* The exact bytes an outside approver signs to authorize ONE redemption of a
|
|
115
159
|
* `ValidationUrl`-bearing `FormationInvite` — the TS mirror of the `'vouch'` digest in
|
|
@@ -125,6 +169,10 @@ export declare function buildAuthorizationMessage(domain: ControlDomain, action:
|
|
|
125
169
|
* that same nonce, then pass BOTH the nonce and the signature to
|
|
126
170
|
* {@link ControlDatabase.redeemInvitation} / {@link ControlDatabase.recordFormationUsage}:
|
|
127
171
|
* signing one nonce and inserting another fails the CHECK.
|
|
172
|
+
*
|
|
173
|
+
* No party id, unlike every owner digest: the approver is sent only these five fields
|
|
174
|
+
* (`formation-approval.ts`), and the `token` it binds is a random secret seated in one
|
|
175
|
+
* party's `FormationInvite` table (the schema's `FormationUsage.Authorized` comment).
|
|
128
176
|
*/
|
|
129
177
|
export declare function formationVouchMessage(fields: {
|
|
130
178
|
token: string;
|
|
@@ -149,7 +197,9 @@ export declare function formationVouchMessage(fields: {
|
|
|
149
197
|
* the strand when it signs (a bound invite's host strand arrives only in the result
|
|
150
198
|
* frame; an unbound strand is minted by the responder). The responder cannot
|
|
151
199
|
* substitute one anyway — a bound invite is pinned to its own strand by `Authorized`,
|
|
152
|
-
* and an unbound redemption mints a fresh strand, so there is no victim to name.
|
|
200
|
+
* and an unbound redemption mints a fresh strand, so there is no victim to name. The
|
|
201
|
+
* party id is absent for the same reason (it too arrives only in the result frame) and
|
|
202
|
+
* the `token` scopes the digest to one party instead, as for {@link formationVouchMessage}.
|
|
153
203
|
*/
|
|
154
204
|
export declare function formationConsentMessage(fields: {
|
|
155
205
|
token: string;
|
|
@@ -157,6 +207,37 @@ export declare function formationConsentMessage(fields: {
|
|
|
157
207
|
peerKey: string;
|
|
158
208
|
disclosure: string;
|
|
159
209
|
}): Uint8Array;
|
|
210
|
+
/**
|
|
211
|
+
* The exact bytes an owner signs to seat a `CadreInvite` row — the TS mirror of the `'add'`
|
|
212
|
+
* digest in `CadreInvite.AuthorizedInsert`, over the whole row in the schema's order with
|
|
213
|
+
* nullable columns as `''` (`cadreInviteRowFields`, shared with the base64url verifier
|
|
214
|
+
* `peer-authorization.ts`'s `cadreInviteAddDigest`). `expiresAt` must already be the stored
|
|
215
|
+
* canonical `datetime` string ({@link canonicalDatetime}), as {@link ControlDatabase.insertCadreInvite}
|
|
216
|
+
* produces it, or the signed segment will not match what the CHECK sees after coercion.
|
|
217
|
+
*/
|
|
218
|
+
export declare function cadreInviteAddMessage(partyId: string, row: CadreInviteSignedFields): Uint8Array;
|
|
219
|
+
/** The fields both redemption signatures cover; see {@link cadreInviteRedeemMessage}. */
|
|
220
|
+
export interface CadreInviteRedemptionFields {
|
|
221
|
+
inviteKey: string;
|
|
222
|
+
/** The single-use nonce the redeeming device minted for this redemption (`generateStampId`). */
|
|
223
|
+
usageStampId: string;
|
|
224
|
+
/** The redeeming device's own ed25519 public key (base64url). */
|
|
225
|
+
peerKey: string;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* The exact bytes the HOLDER of a cadre invitation signs, with the invitation's private key,
|
|
229
|
+
* to redeem it once — the TS mirror of the `'redeem'` digest `CadreInviteUsage.InvitePossessed`
|
|
230
|
+
* verifies against the invitation's public key. Binding the device and the nonce means a
|
|
231
|
+
* captured redemption can admit no other device and spend no other use.
|
|
232
|
+
*/
|
|
233
|
+
export declare function cadreInviteRedeemMessage(partyId: string, fields: CadreInviteRedemptionFields): Uint8Array;
|
|
234
|
+
/**
|
|
235
|
+
* The exact bytes the DEVICE signs, with its own key, to consent to being admitted by one
|
|
236
|
+
* redemption — the TS mirror of the `'consent'` digest `CadreInviteUsage.PeerConsented`
|
|
237
|
+
* verifies against `PeerKey`. Same fields as {@link cadreInviteRedeemMessage} under a
|
|
238
|
+
* distinct action tag, so neither signature can stand in for the other.
|
|
239
|
+
*/
|
|
240
|
+
export declare function cadreInviteConsentMessage(partyId: string, fields: CadreInviteRedemptionFields): Uint8Array;
|
|
160
241
|
/** The control tables keyed by a strand id whose repeat insert a caller may treat as idempotent. */
|
|
161
242
|
type StrandIdTable = Extract<RevocableTable, 'Strand' | 'JoinedStrand'>;
|
|
162
243
|
/**
|
|
@@ -176,8 +257,8 @@ type StrandIdTable = Extract<RevocableTable, 'Strand' | 'JoinedStrand'>;
|
|
|
176
257
|
*/
|
|
177
258
|
export declare function isStrandIdConflict(error: unknown, table: StrandIdTable): boolean;
|
|
178
259
|
/**
|
|
179
|
-
* Did {@link ControlDatabase.
|
|
180
|
-
* `
|
|
260
|
+
* Did {@link ControlDatabase.insertJoinRequest} fail because the party already holds a
|
|
261
|
+
* `JoinRequest` row for this invitation? The caller adopts the existing request instead.
|
|
181
262
|
* Text-matched, failing closed, like {@link isStrandIdConflict}.
|
|
182
263
|
*
|
|
183
264
|
* NOTE: the existing row may be one {@link ControlDatabase.queryPendingJoin} hides (its stamp
|
|
@@ -186,22 +267,24 @@ export declare function isStrandIdConflict(error: unknown, table: StrandIdTable)
|
|
|
186
267
|
*/
|
|
187
268
|
export declare function isPendingJoinConflict(error: unknown): boolean;
|
|
188
269
|
/**
|
|
189
|
-
* The `
|
|
270
|
+
* The `JoinRequest.Id` of an invitation: the base64url sha256 `digest` of its token. One row
|
|
190
271
|
* per invitation, and the `Revocation` tombstone that names a removed row carries this, never
|
|
191
272
|
* the bearer token itself.
|
|
192
273
|
*/
|
|
193
274
|
export declare function pendingJoinId(token: string): string;
|
|
275
|
+
/** A `JoinRequest` row as written: every column but the stamp, which each write mints. */
|
|
276
|
+
export type JoinRequestFields = Omit<JoinRequestRow, 'StampId'>;
|
|
277
|
+
/** A pending join as re-written: its request fields and its outcome, under a stamp the write mints. */
|
|
278
|
+
export type PendingJoinFields = Omit<PendingJoin, 'StampId'>;
|
|
194
279
|
/**
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* re-reads and decides again. `liveStampId` is null when the row is gone.
|
|
280
|
+
* A pending-join write found that the live join is not the one the caller read: another write
|
|
281
|
+
* recorded an outcome, asked again or dismissed it first. Nothing was written; the caller
|
|
282
|
+
* decides again from `live`, which is null when the request is gone.
|
|
199
283
|
*/
|
|
200
284
|
export declare class PendingJoinChangedError extends Error {
|
|
201
285
|
readonly id: string;
|
|
202
|
-
readonly
|
|
203
|
-
|
|
204
|
-
constructor(id: string, expectedStampId: string, liveStampId: string | null);
|
|
286
|
+
readonly live: PendingJoin | null;
|
|
287
|
+
constructor(id: string, live: PendingJoin | null);
|
|
205
288
|
}
|
|
206
289
|
/**
|
|
207
290
|
* The one `CadreControl.Revocation` row that retires nothing, filed once by an owner
|
|
@@ -214,6 +297,24 @@ export declare const REVOCATION_LEDGER_MARKER: {
|
|
|
214
297
|
readonly rowKey: "ledger";
|
|
215
298
|
readonly stampId: "opened";
|
|
216
299
|
};
|
|
300
|
+
/**
|
|
301
|
+
* Project `CadrePeer` rows into the {@link SeedPeer} list a seed, or a cadre invitation
|
|
302
|
+
* redemption reply, carries.
|
|
303
|
+
*
|
|
304
|
+
* Owner identity is sourced from the `OwnerKey` table, not from the transport peer id. An
|
|
305
|
+
* Ed25519 libp2p peer id embeds its public key (identity multihash), so each peer's ed25519
|
|
306
|
+
* key is derivable from its `PeerId`; a peer is an owner iff that derived key is in
|
|
307
|
+
* `ownerKeys`. This makes any owner node markable — not just the local one. A non-Ed25519
|
|
308
|
+
* or unparsable id yields a non-owner rather than failing the whole projection.
|
|
309
|
+
*
|
|
310
|
+
* NOTE: this is the one owner lookup deliberately left on the REPLICATED table rather than
|
|
311
|
+
* the node-local anchor. `SeedPeer.isOwner` is a dial hint — the receiver dials owner-flagged
|
|
312
|
+
* peers first — not a trust decision, and the receiver re-derives real trust from its own
|
|
313
|
+
* anchor. So a polluted table costs at most a wasted dial, while anchoring here would
|
|
314
|
+
* silently drop legitimate co-owners this node never pinned. If `isOwner` ever gates
|
|
315
|
+
* anything the receiver TRUSTS, move it to the anchor.
|
|
316
|
+
*/
|
|
317
|
+
export declare function projectSeedPeers(rows: readonly CadrePeerRow[], ownerKeys: ReadonlySet<string>): SeedPeer[];
|
|
217
318
|
/**
|
|
218
319
|
* Guarded tables a node may reap locally once their tombstone has committed — the
|
|
219
320
|
* tables whose `AuthorizedDelete` carries the REAP branch (see the constraint comment on
|
|
@@ -222,15 +323,29 @@ export declare const REVOCATION_LEDGER_MARKER: {
|
|
|
222
323
|
* secrets stored nowhere else (tickets/backlog/debt-strand-tombstone-reap.md owns any
|
|
223
324
|
* future change). `JoinedStrand` also carries a `MemberPrivateKey` but IS reapable: that
|
|
224
325
|
* secret is the joined strand's shared read key, which its founding party and every member
|
|
225
|
-
* hold, so a reaped row is recoverable by re-forming. `
|
|
326
|
+
* hold, so a reaped row is recoverable by re-forming. `JoinRequest` is reapable for the same
|
|
226
327
|
* reason: the inviting party and the user also hold its invitation. `OwnerKey` has no
|
|
227
328
|
* production removal path and `MinOneOwner` makes an automated owner-key reap a
|
|
228
|
-
* party-bricking hazard.
|
|
329
|
+
* party-bricking hazard. `CadreInvite` is never deleted at all (its tombstone is a
|
|
330
|
+
* withdrawal over a row that stays), so there is nothing to reap.
|
|
229
331
|
*/
|
|
230
|
-
export declare const REAPABLE_TABLES: readonly ["CadrePeer", "DeviceToken", "ValidationKey", "JoinedStrand", "
|
|
332
|
+
export declare const REAPABLE_TABLES: readonly ["CadrePeer", "DeviceToken", "ValidationKey", "JoinedStrand", "JoinRequest"];
|
|
231
333
|
export type ReapableTable = (typeof REAPABLE_TABLES)[number];
|
|
334
|
+
/** A `FormationInvite` row as {@link ControlDatabase.queryFormationInvite} reads it. */
|
|
335
|
+
export interface FormationInviteRow {
|
|
336
|
+
token: string;
|
|
337
|
+
sAppId: string;
|
|
338
|
+
/** Parsed epoch ms of the stored `datetime`; null when the invitation never expires. */
|
|
339
|
+
expiresAtMs: number | null;
|
|
340
|
+
/** Null is unlimited. */
|
|
341
|
+
totalUses: number | null;
|
|
342
|
+
validationUrl: string | null;
|
|
343
|
+
/** A bound invitation's host strand; null on the responder-provisions path. */
|
|
344
|
+
strandId: string | null;
|
|
345
|
+
}
|
|
232
346
|
/**
|
|
233
|
-
* Notified after a `CadreControl.CadrePeer`
|
|
347
|
+
* Notified after a membership write (a `CadreControl.CadrePeer` or `OwnerKey` row, see
|
|
348
|
+
* {@link ControlDatabase.mutateCadrePeer}) has COMMITTED.
|
|
234
349
|
*
|
|
235
350
|
* The one hook the party-membership snapshot a node admits control-DB traffic
|
|
236
351
|
* against ({@link CadreNode.refreshMembershipGate}) hangs off, so that snapshot
|
|
@@ -240,19 +355,22 @@ export type ReapableTable = (typeof REAPABLE_TABLES)[number];
|
|
|
240
355
|
*/
|
|
241
356
|
export type MembershipChangeListener = (reason: string) => Promise<void>;
|
|
242
357
|
/**
|
|
243
|
-
* Identity of the tombstone a committed guarded delete wrote — a
|
|
244
|
-
* {@link RevocationRow}
|
|
245
|
-
* always seats at 0 (`FreshTombstone`) and
|
|
358
|
+
* Identity of the tombstone a committed guarded delete wrote — the triple a
|
|
359
|
+
* {@link RevocationRow} is keyed and signed over. The counter is left out because a
|
|
360
|
+
* fresh tombstone always seats it at 0 (`FreshTombstone`), and the signer pair because
|
|
361
|
+
* the writer that files the row supplies it.
|
|
246
362
|
*/
|
|
247
|
-
export type RevokedRowRef =
|
|
363
|
+
export type RevokedRowRef = Pick<RevocationRow, 'tableName' | 'rowKey' | 'stampId'>;
|
|
364
|
+
/** What {@link ControlDatabase.removeMachine} wrote: `CadreNode.removeMachine`'s result, less the re-vouches it made first. */
|
|
365
|
+
export type MachineRemovalWrites = Pick<MachineRemovalResult, 'removedPeer' | 'removedOwnerKey' | 'removedDeviceToken' | 'withdrawnInvitations'>;
|
|
248
366
|
/**
|
|
249
367
|
* Notified after a guarded-table delete — and the `CadreControl.Revocation`
|
|
250
368
|
* tombstone it writes in the same transaction — has COMMITTED.
|
|
251
369
|
*
|
|
252
370
|
* The seam the write-while-alone re-replication queue hangs off
|
|
253
|
-
* ({@link CadreNode.noteGuardedDelete}): one listener covers
|
|
254
|
-
*
|
|
255
|
-
*
|
|
371
|
+
* ({@link CadreNode.noteGuardedDelete}): one listener covers every guarded table
|
|
372
|
+
* because every owner delete funnels through {@link deleteGuardedRow} (or the
|
|
373
|
+
* two-table `deleteStrandAndPartyKey`). Synchronous — the
|
|
256
374
|
* handler only records the tombstone's identity; it must not throw (the notifier
|
|
257
375
|
* swallows and logs anyway — a committed delete never fails because bookkeeping did).
|
|
258
376
|
*/
|
|
@@ -271,7 +389,12 @@ export type GuardedDeleteListener = (revocation: RevokedRowRef) => void;
|
|
|
271
389
|
*/
|
|
272
390
|
export type ControlWriteAbandonedListener = (abandonment: ControlRetryAbandonment) => void;
|
|
273
391
|
export interface ControlDatabaseConfig {
|
|
274
|
-
/**
|
|
392
|
+
/**
|
|
393
|
+
* Party ID for the control network. Also the party every owner and peer signature over a
|
|
394
|
+
* control row binds ({@link buildAuthorizationMessage}), read on the SQL side through
|
|
395
|
+
* `party_id()` ({@link registerPartyIdFunction}). Must be non-empty: parties with an empty
|
|
396
|
+
* id would share approvals.
|
|
397
|
+
*/
|
|
275
398
|
partyId: string;
|
|
276
399
|
/**
|
|
277
400
|
* Optional path to the control schema file.
|
|
@@ -476,17 +599,19 @@ export declare class ControlDatabase {
|
|
|
476
599
|
/** Single-row sibling of {@link queryJoinedStrands}: the joined strand with this id, or null. */
|
|
477
600
|
queryJoinedStrand(strandId: string): Promise<StrandRow | null>;
|
|
478
601
|
/**
|
|
479
|
-
* Every join this party asked for and has not dismissed
|
|
480
|
-
*
|
|
602
|
+
* Every join this party asked for and has not dismissed, pending or finished: each
|
|
603
|
+
* `CadreControl.JoinRequest` row with its `JoinSuccess` / `JoinFailure` row.
|
|
481
604
|
*
|
|
482
|
-
*
|
|
483
|
-
* drops
|
|
484
|
-
*
|
|
485
|
-
*
|
|
605
|
+
* A request whose `StampId` is retired in `Revocation` is dropped, as {@link queryCadrePeers}
|
|
606
|
+
* drops such rows: it is a re-asked or dismissed incarnation this node still holds until the
|
|
607
|
+
* reap sweep removes it, or one re-seated by a replayed approval on a node that had not yet
|
|
608
|
+
* seen the tombstone. Either way it must not bring a dismissed join back.
|
|
486
609
|
*/
|
|
487
|
-
queryPendingJoins(): Promise<
|
|
488
|
-
/** Single-row sibling of {@link queryPendingJoins}, with the same retired-stamp filter: the
|
|
489
|
-
queryPendingJoin(id: string): Promise<
|
|
610
|
+
queryPendingJoins(): Promise<PendingJoin[]>;
|
|
611
|
+
/** Single-row sibling of {@link queryPendingJoins}, with the same retired-stamp filter: the join with this id, or null. */
|
|
612
|
+
queryPendingJoin(id: string): Promise<PendingJoin | null>;
|
|
613
|
+
/** The raw read behind {@link queryPendingJoin}, with no retired-stamp filter. `retry: false` inside a locked write body. */
|
|
614
|
+
private readPendingJoin;
|
|
490
615
|
/**
|
|
491
616
|
* Count rows in a CadreControl table as seen by THIS database instance.
|
|
492
617
|
*
|
|
@@ -506,6 +631,9 @@ export declare class ControlDatabase {
|
|
|
506
631
|
/**
|
|
507
632
|
* Check whether any owner key exists in the control database.
|
|
508
633
|
* Used to decide whether a fresh-party genesis insert is required.
|
|
634
|
+
*
|
|
635
|
+
* Deliberately RAW (no retired-stamp filter, unlike {@link getOwnerKeys}): the genesis
|
|
636
|
+
* insert is refused by the schema whenever any physical row exists, so this must see one.
|
|
509
637
|
*/
|
|
510
638
|
hasOwnerKey(): Promise<boolean>;
|
|
511
639
|
/**
|
|
@@ -521,8 +649,41 @@ export declare class ControlDatabase {
|
|
|
521
649
|
* trusted only if it is already enrolled here (see `SeedTrustPolicy`). It is
|
|
522
650
|
* also the owner-identity source for `queryPeers`, decoupling owner
|
|
523
651
|
* status from the libp2p transport peer ID.
|
|
524
|
-
|
|
525
|
-
|
|
652
|
+
*
|
|
653
|
+
* Rows whose `StampId` is retired in `CadreControl.Revocation` are excluded, as
|
|
654
|
+
* {@link queryCadrePeers} excludes them: `OwnerKey` is never reaped (`MinOneOwner` makes
|
|
655
|
+
* an automated reap able to empty the table), so a node that received the tombstone
|
|
656
|
+
* without the delete (a removal committed while the remover was alone, re-issued on growth)
|
|
657
|
+
* keeps the removed owner's physical row next to it. Without the filter a removed owner
|
|
658
|
+
* would still count as an owner in every membership snapshot and seed-trust read, still
|
|
659
|
+
* read as the issuer of a live invitation and still be preferred as a dial target. The
|
|
660
|
+
* write side filters for itself: every owner lookup in the schema disqualifies a row whose
|
|
661
|
+
* stamp is retired (`schemas/control.qsql` header), so a removed owner cannot sign here
|
|
662
|
+
* once its tombstone has arrived, row or no row.
|
|
663
|
+
*
|
|
664
|
+
* `retry: false` from a caller that reads under the write lock, for the reason on
|
|
665
|
+
* {@link queryCadrePeers}.
|
|
666
|
+
*/
|
|
667
|
+
getOwnerKeys(retry?: boolean): Promise<Set<string>>;
|
|
668
|
+
/**
|
|
669
|
+
* Every live `OwnerKey` row with its persisted proof ({@link OwnerKeyRow}) — what the
|
|
670
|
+
* trusted-owner anchor sync derives from (`CadreNode.syncOwnerAnchor`). Rows whose
|
|
671
|
+
* `StampId` is retired in `CadreControl.Revocation` are excluded here, as
|
|
672
|
+
* {@link queryCadrePeers} excludes them and for the reason on {@link getOwnerKeys}, which
|
|
673
|
+
* reads through this. A row with no stamp (the schema permits it; no writer produces one)
|
|
674
|
+
* is dropped too: no proof can bind to it, so no reader could ever derive it.
|
|
675
|
+
*
|
|
676
|
+
* `retry: false` from the membership-gate refresh, which runs as the control database's
|
|
677
|
+
* membership listener with that database's write lock held — the reason on
|
|
678
|
+
* {@link queryCadrePeers}; both reads here take the flag.
|
|
679
|
+
*
|
|
680
|
+
* @param retired - the retired `OwnerKey` stamps, when the caller has already read the
|
|
681
|
+
* tombstones (`CadreNode.syncOwnerAnchor` reads {@link queryRevocations} for their
|
|
682
|
+
* signers); omitted, they are read here. Before the ledger marker is filed every
|
|
683
|
+
* `Revocation` read is a cohort consult, so the sync passing its own set keeps the
|
|
684
|
+
* membership refresh at one such read for the anchor.
|
|
685
|
+
*/
|
|
686
|
+
queryOwnerKeyRows(retry?: boolean, retired?: ReadonlySet<string>): Promise<OwnerKeyRow[]>;
|
|
526
687
|
/**
|
|
527
688
|
* Enumerate the CadrePeer rows (cadre membership) for admin/membership reads.
|
|
528
689
|
* Includes the persisted voucher columns, which
|
|
@@ -560,6 +721,17 @@ export declare class ControlDatabase {
|
|
|
560
721
|
* lock is the same defect.
|
|
561
722
|
*/
|
|
562
723
|
queryCadrePeers(retry?: boolean): Promise<CadrePeerRow[]>;
|
|
724
|
+
/**
|
|
725
|
+
* The cadre's peers as a seed or a redemption reply carries them: every live `CadrePeer`
|
|
726
|
+
* row with its addresses, each flagged `isOwner` when the key behind its peer id is in
|
|
727
|
+
* the replicated `OwnerKey` table ({@link projectSeedPeers}).
|
|
728
|
+
*
|
|
729
|
+
* Read through {@link queryCadrePeers}, never a raw `CadrePeer` select, so a removed
|
|
730
|
+
* member's addresses are never packed into a seed or handed to a redeeming device as a
|
|
731
|
+
* dial hint: a revoked peer is off the addressable surface everywhere, and this is one of
|
|
732
|
+
* its exits.
|
|
733
|
+
*/
|
|
734
|
+
querySeedPeers(): Promise<SeedPeer[]>;
|
|
563
735
|
/**
|
|
564
736
|
* Every approver public key currently enrolled in `CadreControl.ValidationKey` — the
|
|
565
737
|
* set a `ValidationUrl` redemption's approval signature must be signed by (see
|
|
@@ -689,19 +861,22 @@ export declare class ControlDatabase {
|
|
|
689
861
|
*/
|
|
690
862
|
private readRevokedStampRows;
|
|
691
863
|
/**
|
|
692
|
-
* Every locally-held `CadreControl.Revocation` tombstone — identity triple
|
|
693
|
-
* `ReissuedAt` counter. Consumed by the cohort-growth re-issue
|
|
694
|
-
* enumerates what this node holds before {@link reissueRevocations}
|
|
695
|
-
* it, and by the reap sweep ({@link reapRevokedRows}). Plain scan with no
|
|
696
|
-
* the composite-primary-key point-lookup hazard (see the statement comment in
|
|
864
|
+
* Every locally-held `CadreControl.Revocation` tombstone — identity triple, its
|
|
865
|
+
* `ReissuedAt` counter and the stored signer pair. Consumed by the cohort-growth re-issue
|
|
866
|
+
* sweep, which enumerates what this node holds before {@link reissueRevocations}
|
|
867
|
+
* re-broadcasts it, and by the reap sweep ({@link reapRevokedRows}). Plain scan with no
|
|
868
|
+
* `where`, so the composite-primary-key point-lookup hazard (see the statement comment in
|
|
697
869
|
* {@link reissueRevocations}) does not arise. Unlocked, like every other read.
|
|
698
870
|
*
|
|
699
871
|
* Skips the ledger marker ({@link REVOCATION_LEDGER_MARKER}): it retires nothing, so
|
|
700
872
|
* neither sweep may reap or re-sign it, and skipping it keeps `RevocationRow.tableName`
|
|
701
873
|
* a {@link RevocableTable}. Filtered on `TableName` alone, in TypeScript: `RowIsGone`
|
|
702
874
|
* admits no other row under `'Revocation'`.
|
|
875
|
+
*
|
|
876
|
+
* `retry: false` from a caller that reads under the write lock, for the reason on
|
|
877
|
+
* {@link queryCadrePeers}.
|
|
703
878
|
*/
|
|
704
|
-
queryRevocations(): Promise<RevocationRow[]>;
|
|
879
|
+
queryRevocations(retry?: boolean): Promise<RevocationRow[]>;
|
|
705
880
|
/**
|
|
706
881
|
* Read a single peer's address record (the full `CadrePeer` row) by PeerId.
|
|
707
882
|
*
|
|
@@ -775,6 +950,99 @@ export declare class ControlDatabase {
|
|
|
775
950
|
* Insert the initial owner key (bootstrap - no existing owners required)
|
|
776
951
|
*/
|
|
777
952
|
insertOwnerKey(key: string): Promise<void>;
|
|
953
|
+
/**
|
|
954
|
+
* Owner-signed INSERT of a further `OwnerKey` row, notifying the membership listener once
|
|
955
|
+
* it has committed. The signed-add branch of `OwnerKey.Authorized`: the signer must be an
|
|
956
|
+
* owner in the pre-transaction snapshot, its signature covers the `'add'` digest over
|
|
957
|
+
* (Key, StampId), and the stored `VouchOwner`/`VouchSig` are that same pair, so a reader
|
|
958
|
+
* can later judge the add against its own anchor (`verifyOwnerKeyVoucher`).
|
|
959
|
+
*
|
|
960
|
+
* Through {@link mutateCadrePeer}, the membership hub. The liveness read, the stamp and the
|
|
961
|
+
* signature all sit inside the locked body: a {@link lockedWithRetry} re-run starts over a
|
|
962
|
+
* rolled-back attempt with a fresh stamp, as {@link redeemCadreInvite} does.
|
|
963
|
+
*
|
|
964
|
+
* NOTE: re-adding a removed owner from a machine that still holds its retired physical row
|
|
965
|
+
* is refused here by name: `OwnerKey` is not reaped, so a machine that received the
|
|
966
|
+
* tombstone without the delete (see {@link getOwnerKeys}) keeps the row, and an insert there
|
|
967
|
+
* would collide on the primary key. Re-add from the machine that removed the key, or any
|
|
968
|
+
* machine the delete reached; if re-adding removed owners becomes routine, the fix is an
|
|
969
|
+
* `OwnerKey` reap branch, gated so it can never take the table below `MinOneOwner`.
|
|
970
|
+
*
|
|
971
|
+
* @returns `true` when this call seated the row, `false` when `key` is already a live owner
|
|
972
|
+
* here (nothing written).
|
|
973
|
+
* @throws when this node holds `key`'s row under a retired stamp (above), or when the
|
|
974
|
+
* schema refuses the signer.
|
|
975
|
+
*/
|
|
976
|
+
insertOwnerKeyVouched(key: string, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<boolean>;
|
|
977
|
+
/**
|
|
978
|
+
* Owner-signed removal of one `OwnerKey` row plus the `Revocation` tombstone retiring its
|
|
979
|
+
* stamp, notifying the membership listener once it has committed. The remove branch of
|
|
980
|
+
* `OwnerKey.Authorized` requires a signer other than the removed key, and `MinOneOwner`
|
|
981
|
+
* keeps the table from emptying; both are refused by name here first, read from this
|
|
982
|
+
* node's live owners ({@link getOwnerKeys}), so a caller gets a clear error rather than a
|
|
983
|
+
* constraint failure. The schema stays the authority: a concurrent removal elsewhere can
|
|
984
|
+
* make the local count wrong in either direction, and then `MinOneOwner` or the tombstone's
|
|
985
|
+
* primary key decides (see the NOTE on {@link deleteGuardedRow}).
|
|
986
|
+
*
|
|
987
|
+
* Through {@link mutateCadrePeer}, the membership hub, which notifies whenever the body
|
|
988
|
+
* resolves, the absent-key answer included.
|
|
989
|
+
*
|
|
990
|
+
* @returns `true` when the row was removed, `false` when `key` is not a live owner here
|
|
991
|
+
* (nothing written, no tombstone).
|
|
992
|
+
* @throws when `key` is `ownerKey`, or is the only live owner.
|
|
993
|
+
*/
|
|
994
|
+
deleteOwnerKey(key: string, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<boolean>;
|
|
995
|
+
/**
|
|
996
|
+
* Re-vouch a live `OwnerKey` row under this owner's signature: in ONE transaction, the
|
|
997
|
+
* current row's guarded delete plus tombstone ({@link deleteOwnerKey}'s body) and an insert
|
|
998
|
+
* of the same key under a fresh stamp, signed by `ownerKey` ({@link insertOwnerKeyVouched}'s
|
|
999
|
+
* body), as {@link rewritePendingJoin} re-seats a request. `OwnerKey` has `NoUpdate`, so
|
|
1000
|
+
* this is the only way to rebind a row's voucher; the anchor derivation lets the re-add
|
|
1001
|
+
* under a fresh stamp beat the older tombstone (`deriveOwnerAnchor`). For
|
|
1002
|
+
* {@link removeMachine}: the owners a machine being removed had added keep their anchor
|
|
1003
|
+
* place everywhere once their row stands on a remaining owner's signature.
|
|
1004
|
+
*
|
|
1005
|
+
* Through {@link mutateCadrePeer}, the membership hub. The stamp read runs inside the locked
|
|
1006
|
+
* body; a row whose stamp is already retired (held without its delete) is reported as absent
|
|
1007
|
+
* rather than tombstoned twice.
|
|
1008
|
+
*
|
|
1009
|
+
* @returns `true` when the row was re-vouched, `false` when `key` is not a live owner here.
|
|
1010
|
+
* @throws when `key` is `ownerKey` (an owner cannot sign its own row's removal).
|
|
1011
|
+
*/
|
|
1012
|
+
revouchOwnerKey(key: string, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<boolean>;
|
|
1013
|
+
/**
|
|
1014
|
+
* Remove one machine completely, in ONE transaction: its `CadrePeer` row, its `OwnerKey`
|
|
1015
|
+
* row when `target.ownerKey` names one, its `DeviceToken` row when it has one, each with
|
|
1016
|
+
* the `Revocation` tombstone retiring its stamp ({@link deleteGuardedRow}'s statement pair,
|
|
1017
|
+
* composed bare as {@link deleteStrandAndPartyKey} composes it), plus a withdrawal tombstone
|
|
1018
|
+
* for every `CadreInvite` in `target.inviteKeys` that is still live here
|
|
1019
|
+
* ({@link withdrawCadreInvite}'s body). One transaction so no reader sees the machine half
|
|
1020
|
+
* removed: a row gone but its push token resolvable, or its owner key gone but an invitation
|
|
1021
|
+
* it could redeem again still open.
|
|
1022
|
+
*
|
|
1023
|
+
* Through {@link mutateCadrePeer}, the membership hub. Every stamp is read inside the locked
|
|
1024
|
+
* body, and a row whose stamp is already retired (held without its delete) is skipped rather
|
|
1025
|
+
* than tombstoned twice. The by-name owner refusals of {@link deleteOwnerKey} apply to the
|
|
1026
|
+
* owner-key arm. Nothing to remove writes nothing and returns all false.
|
|
1027
|
+
*
|
|
1028
|
+
* The caller decides WHAT to remove, which owner key is the machine's and which invitations
|
|
1029
|
+
* it could use, from its plan (`CadreNode.planMachineRemoval`); this method only refuses to
|
|
1030
|
+
* write an inconsistent set.
|
|
1031
|
+
*
|
|
1032
|
+
* @throws when `target.ownerKey` is `ownerKey`, or is the only live owner here.
|
|
1033
|
+
*/
|
|
1034
|
+
removeMachine(target: {
|
|
1035
|
+
peerId: string;
|
|
1036
|
+
ownerKey: string | null;
|
|
1037
|
+
inviteKeys: readonly string[];
|
|
1038
|
+
}, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<MachineRemovalWrites>;
|
|
1039
|
+
/**
|
|
1040
|
+
* The guarded removals {@link removeMachine} runs, signed over each row's current stamp:
|
|
1041
|
+
* the `CadrePeer` row, the `OwnerKey` row when named, the `DeviceToken` row when present.
|
|
1042
|
+
* Inside the locked body (every read `retry: false`); one `Revocation` scan serves the
|
|
1043
|
+
* retired-stamp check for all three tables and the last-owner refusal.
|
|
1044
|
+
*/
|
|
1045
|
+
private signMachineRemovals;
|
|
778
1046
|
/**
|
|
779
1047
|
* Insert a strand into the control database using an owner signature.
|
|
780
1048
|
*
|
|
@@ -887,45 +1155,62 @@ export declare class ControlDatabase {
|
|
|
887
1155
|
*/
|
|
888
1156
|
deleteJoinedStrand(strandId: string, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<boolean>;
|
|
889
1157
|
/**
|
|
890
|
-
* Record, party-wide, a join this party asked for
|
|
891
|
-
*
|
|
892
|
-
* row as written, with the fresh `StampId` a later {@link replacePendingJoin} names.
|
|
1158
|
+
* Record, party-wide, a join this party asked for: a `JoinRequest` row under a fresh stamp,
|
|
1159
|
+
* owner-signed over every column. Returns the pending join as written.
|
|
893
1160
|
*
|
|
894
|
-
* Fails with a `
|
|
895
|
-
* this invitation; {@link isPendingJoinConflict} identifies that case.
|
|
1161
|
+
* Fails with a `JoinRequest.Id` uniqueness violation when the party already holds a request
|
|
1162
|
+
* for this invitation; {@link isPendingJoinConflict} identifies that case.
|
|
896
1163
|
*/
|
|
897
|
-
|
|
1164
|
+
insertJoinRequest(request: JoinRequestFields, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<PendingJoin>;
|
|
898
1165
|
/**
|
|
899
|
-
*
|
|
900
|
-
*
|
|
1166
|
+
* Record the outcome of the pending join `expected`: its owner-signed `JoinSuccess` /
|
|
1167
|
+
* `JoinFailure` row, keyed by the request's stamp. A join recorded over a failure deletes the
|
|
1168
|
+
* failure row in the same transaction (the schema authorizes that delete by the join, and
|
|
1169
|
+
* leaves the one-outcome rule in that direction to this writer). Returns the pending join as
|
|
1170
|
+
* written.
|
|
901
1171
|
*
|
|
902
|
-
*
|
|
903
|
-
*
|
|
904
|
-
*
|
|
905
|
-
*
|
|
906
|
-
*
|
|
907
|
-
|
|
908
|
-
|
|
1172
|
+
* The live join must still be what the caller read: the same request incarnation with the
|
|
1173
|
+
* same outcome kind. Otherwise this throws {@link PendingJoinChangedError} and writes nothing.
|
|
1174
|
+
* That check is local; a sibling machine's write landing between it and the commit fails the
|
|
1175
|
+
* transaction instead (its outcome row collides on the primary key, or its tombstone retires
|
|
1176
|
+
* the stamp), and the caller re-reads either way.
|
|
1177
|
+
*/
|
|
1178
|
+
recordJoinOutcome(expected: PendingJoin, outcome: JoinOutcome, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<PendingJoin>;
|
|
1179
|
+
/**
|
|
1180
|
+
* Re-write the pending join `next` (request and outcome) under a fresh stamp, retiring the
|
|
1181
|
+
* live request incarnation `expectedStampId`: how a finished request is asked for again, and
|
|
1182
|
+
* how a join written while this machine was alone is re-broadcast. One locked transaction
|
|
1183
|
+
* deletes the outcome row and the request (owner-signed `'remove'` digest, with its
|
|
1184
|
+
* `Revocation` tombstone) and inserts the successors, so no reader ever sees the id missing in
|
|
1185
|
+
* between. Throws {@link PendingJoinChangedError} and writes nothing when the live request is
|
|
1186
|
+
* not `expectedStampId`.
|
|
909
1187
|
*
|
|
910
1188
|
* Signatures and the new stamp are minted before the lock, so a retried attempt re-presents
|
|
911
1189
|
* the same signed messages (the contract on {@link withWriteLock}). A retry after an attempt
|
|
912
|
-
* that committed but reported failure throws {@link PendingJoinChangedError} whose
|
|
913
|
-
*
|
|
1190
|
+
* that committed but reported failure throws {@link PendingJoinChangedError} whose live join
|
|
1191
|
+
* carries the returned stamp, so a caller that re-reads finds its write.
|
|
914
1192
|
*/
|
|
915
|
-
|
|
1193
|
+
rewritePendingJoin(expectedStampId: string, next: PendingJoinFields, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<PendingJoin>;
|
|
916
1194
|
/**
|
|
917
|
-
* Owner-signed removal of one
|
|
918
|
-
*
|
|
919
|
-
*
|
|
920
|
-
* `true` when
|
|
1195
|
+
* Owner-signed removal of one pending join, as {@link deleteJoinedStrand} removes its row:
|
|
1196
|
+
* `'remove'`-tagged digest over (Id, StampId), `Revocation` tombstone in the same transaction,
|
|
1197
|
+
* and the request's outcome row with it. A no-op (no throw, no tombstone) when there is no
|
|
1198
|
+
* request — `false` then, `true` when one was actually removed.
|
|
921
1199
|
*/
|
|
922
1200
|
deletePendingJoin(id: string, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<boolean>;
|
|
923
1201
|
/**
|
|
924
|
-
* Mint a fresh stamp for `
|
|
925
|
-
* verifies, returning the row as it will be written and
|
|
926
|
-
|
|
1202
|
+
* Mint a fresh stamp for `request` and the owner signature `JoinRequest.AuthorizedInsert`
|
|
1203
|
+
* verifies, returning the row as it will be written and its insert.
|
|
1204
|
+
*/
|
|
1205
|
+
private signJoinRequestInsert;
|
|
1206
|
+
/**
|
|
1207
|
+
* Remove the outcome row of the request incarnation `requestStampId`, whichever table holds
|
|
1208
|
+
* it. Unsigned: the schema authorizes the delete by the tombstone retiring that stamp, filed
|
|
1209
|
+
* in the same transaction or already committed (a reap).
|
|
927
1210
|
*/
|
|
928
|
-
private
|
|
1211
|
+
private deleteJoinOutcomes;
|
|
1212
|
+
/** One unsigned outcome delete (see {@link deleteJoinOutcomes}; a failure's is also authorized by the join superseding it). */
|
|
1213
|
+
private deleteJoinOutcome;
|
|
929
1214
|
/**
|
|
930
1215
|
* Insert a validation key into the control database using an owner signature.
|
|
931
1216
|
*
|
|
@@ -1000,6 +1285,7 @@ export declare class ControlDatabase {
|
|
|
1000
1285
|
multiaddr: string;
|
|
1001
1286
|
updatedAt: number;
|
|
1002
1287
|
sig: string | null;
|
|
1288
|
+
profile: NodeProfile | null;
|
|
1003
1289
|
}, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<boolean>;
|
|
1004
1290
|
/**
|
|
1005
1291
|
* Owner "re-touch" of an existing `CadrePeer` membership row: bump `UpdatedAt` and
|
|
@@ -1011,7 +1297,8 @@ export declare class ControlDatabase {
|
|
|
1011
1297
|
* local-only (its block's cluster ≤1 at insert) is pushed to the cohort once it grows, by
|
|
1012
1298
|
* re-issuing this monotonic bump. It is an UPDATE (not the original INSERT) because the row
|
|
1013
1299
|
* already exists locally; a re-INSERT would hit the `PeerId` PK. Only the freshness stamp
|
|
1014
|
-
* and voucher change — `PublicKey` / `Multiaddr` / `Sig` are left intact
|
|
1300
|
+
* and voucher change — `PublicKey` / `Multiaddr` / `Sig` / `Profile` are left intact (the
|
|
1301
|
+
* owner branch requires them kept: they are the machine's own claim) — so it is safe
|
|
1015
1302
|
* over a row whose peer has not self-published (`Sig` null); the caller must skip a row
|
|
1016
1303
|
* that already carries a self-`Sig` (that row is the owning peer's to refresh, and bumping
|
|
1017
1304
|
* `UpdatedAt` without re-signing would invalidate its self-signature).
|
|
@@ -1033,6 +1320,11 @@ export declare class ControlDatabase {
|
|
|
1033
1320
|
* change the member set. Keeping the rule uniform ("every CadrePeer mutator notifies")
|
|
1034
1321
|
* beats a per-method exception the next reader has to relearn.
|
|
1035
1322
|
*
|
|
1323
|
+
* Also clears `VouchUsage`: a row carries exactly one proof (the owner branch of
|
|
1324
|
+
* `AuthorizedUpdate` requires it null), so re-vouching an invitation-admitted row moves it
|
|
1325
|
+
* onto the signature and its invitation is no longer consulted for it — the way an owner
|
|
1326
|
+
* retires a device's dependency on an invitation it may later withdraw.
|
|
1327
|
+
*
|
|
1036
1328
|
* NOTE: this rebinds VouchOwner to the CALLING owner's key, and the authorized-membership
|
|
1037
1329
|
* predicate (`CadreNode.listAuthorizedMembers`) now judges rows by that column against each
|
|
1038
1330
|
* reader's node-local anchor. Benign today because the only caller — the write-while-alone
|
|
@@ -1089,15 +1381,9 @@ export declare class ControlDatabase {
|
|
|
1089
1381
|
* Owner-signed delete of one guarded row plus the `Revocation` tombstone retiring its
|
|
1090
1382
|
* stamp, in ONE transaction. The single body behind EVERY guarded delete —
|
|
1091
1383
|
* {@link deleteStrand}, {@link deleteValidationKey}, {@link deleteCadrePeer},
|
|
1092
|
-
* {@link deleteDeviceToken}; see any of them for the per-table
|
|
1093
|
-
* of those is a thin named wrapper, so callers never pass a
|
|
1094
|
-
* generic shape stays off the public surface.
|
|
1095
|
-
*
|
|
1096
|
-
* `OwnerKey` is excluded from `table` only because no owner-key removal path exists in
|
|
1097
|
-
* production yet — the schema has the `'remove'` branch (`OwnerKey.Authorized`, which
|
|
1098
|
-
* requires a DIFFERENT owner as signer) and `control-revocation-replay.spec.ts` drives
|
|
1099
|
-
* it with hand-rolled SQL. Widen this and add a wrapper when that path lands; the body
|
|
1100
|
-
* needs no change.
|
|
1384
|
+
* {@link deleteDeviceToken}, {@link deleteOwnerKey}; see any of them for the per-table
|
|
1385
|
+
* security rationale. Each of those is a thin named wrapper, so callers never pass a
|
|
1386
|
+
* table name and this generic shape stays off the public surface.
|
|
1101
1387
|
*
|
|
1102
1388
|
* The row's CURRENT stamp is read first and signed over, so the remove digest binds to
|
|
1103
1389
|
* this exact row instance. A no-op (no throw, no tombstone) when the row is absent —
|
|
@@ -1105,6 +1391,9 @@ export declare class ControlDatabase {
|
|
|
1105
1391
|
* second read (e.g. {@link CadreNode.unpublishStrand}'s committed-while-alone warning,
|
|
1106
1392
|
* which must not fire for a no-op).
|
|
1107
1393
|
*
|
|
1394
|
+
* `alongside`, when given, runs inside the transaction before the removal, for a table whose
|
|
1395
|
+
* row has dependants the schema requires removed with it ({@link deletePendingJoin}).
|
|
1396
|
+
*
|
|
1108
1397
|
* NOTE: the stamp read is outside the transaction. A concurrent writer that removes
|
|
1109
1398
|
* the row in between makes the signature bind a stamp that is no longer live; the
|
|
1110
1399
|
* delete then matches nothing and the tombstone insert collides with the other
|
|
@@ -1119,7 +1408,7 @@ export declare class ControlDatabase {
|
|
|
1119
1408
|
/**
|
|
1120
1409
|
* Delete the row incarnation `removal` names and file its tombstone — the statement pair
|
|
1121
1410
|
* every owner removal of a guarded row runs ({@link deleteGuardedRow},
|
|
1122
|
-
* {@link deleteStrandAndPartyKey}, {@link
|
|
1411
|
+
* {@link deleteStrandAndPartyKey}, {@link rewritePendingJoin}). The caller supplies the
|
|
1123
1412
|
* transaction: the schema's `RevocationRecorded` check refuses the delete unless the
|
|
1124
1413
|
* tombstone commits with it.
|
|
1125
1414
|
*
|
|
@@ -1130,6 +1419,21 @@ export declare class ControlDatabase {
|
|
|
1130
1419
|
* come from closed literal unions — no caller-supplied string reaches the statement.
|
|
1131
1420
|
*/
|
|
1132
1421
|
private execGuardedRemoval;
|
|
1422
|
+
/**
|
|
1423
|
+
* File the owner-signed `Revocation` tombstone retiring one row incarnation — the second
|
|
1424
|
+
* statement of every guarded removal ({@link execGuardedRemoval}) and the whole of a
|
|
1425
|
+
* `CadreInvite` withdrawal ({@link withdrawCadreInvite}), whose row stays — or the ledger
|
|
1426
|
+
* marker ({@link openRevocationLedger}), which retires nothing. The caller supplies the
|
|
1427
|
+
* lock and, where one is needed, the transaction.
|
|
1428
|
+
*
|
|
1429
|
+
* `ReissuedAt` is named explicitly at 0 (the only value `FreshTombstone` accepts) rather
|
|
1430
|
+
* than leaning on the column default — the seat-at-zero rule is load-bearing for
|
|
1431
|
+
* {@link reissueRevocations}' monotonic bump, so state it at the write site.
|
|
1432
|
+
*
|
|
1433
|
+
* `SignerKey` / `SignerSig` store the context pair (`Revocation.Authorized` pins them
|
|
1434
|
+
* equal), so a node that receives the row by replication can tell which owner filed it.
|
|
1435
|
+
*/
|
|
1436
|
+
private execTombstone;
|
|
1133
1437
|
/**
|
|
1134
1438
|
* Fire the guarded-delete listener for one committed tombstone, swallowing (and
|
|
1135
1439
|
* logging) a listener throw — a committed delete never fails because bookkeeping did.
|
|
@@ -1218,9 +1522,9 @@ export declare class ControlDatabase {
|
|
|
1218
1522
|
* a retired stamp, and which a revoked drone cannot produce at all. The trade is
|
|
1219
1523
|
* recurring failure noise every cycle against removing the one stale row nobody else
|
|
1220
1524
|
* reads from this node (every other node already filters it by tombstone). A revoked
|
|
1221
|
-
* node therefore keeps its own copy of its own row;
|
|
1222
|
-
*
|
|
1223
|
-
* `ValidationKey`, `JoinedStrand` and `
|
|
1525
|
+
* node therefore keeps its own copy of its own row; learning that it was removed is a
|
|
1526
|
+
* separate behaviour (`CadreNode.removedFromCadre`, the `cadre:removed` event).
|
|
1527
|
+
* `ValidationKey`, `JoinedStrand` and `JoinRequest` have no self notion and are not
|
|
1224
1528
|
* special-cased.
|
|
1225
1529
|
*
|
|
1226
1530
|
* A per-row failure is logged and skipped rather than aborting the sweep: an abort would
|
|
@@ -1362,15 +1666,18 @@ export declare class ControlDatabase {
|
|
|
1362
1666
|
*/
|
|
1363
1667
|
setControlWriteAbandonedListener(listener: ControlWriteAbandonedListener | null): void;
|
|
1364
1668
|
/**
|
|
1365
|
-
* Run a
|
|
1669
|
+
* Run a membership-relevant mutation and notify the membership listener once it has
|
|
1366
1670
|
* COMMITTED.
|
|
1367
1671
|
*
|
|
1368
|
-
*
|
|
1369
|
-
* ({@link updateSelfPeerRecord}, which cannot
|
|
1370
|
-
* the
|
|
1672
|
+
* Covers every write that can change who is a member or an owner: EVERY `CadrePeer`
|
|
1673
|
+
* writer, with one documented exception ({@link updateSelfPeerRecord}, which cannot
|
|
1674
|
+
* change the snapshot), the `OwnerKey` writers ({@link insertOwnerKeyVouched},
|
|
1675
|
+
* {@link deleteOwnerKey}) and the invitation redemption ({@link redeemCadreInvite}),
|
|
1676
|
+
* which seats both. That is what makes the party-membership snapshot refresh automatic
|
|
1677
|
+
* rather than a caller obligation. The name predates the `OwnerKey` writers. A
|
|
1371
1678
|
* writer necessarily holds the target node's ControlDatabase (the `SeedBootstrapService`'s
|
|
1372
|
-
* event callbacks are NOT a viable seam: the temp
|
|
1373
|
-
*
|
|
1679
|
+
* event callbacks are NOT a viable seam: the temp service `CadreNode.applySeed`
|
|
1680
|
+
* builds, and services constructed outside `CadreNode` entirely, never get
|
|
1374
1681
|
* callbacks wired), so this is the one point on the write path that cannot be bypassed.
|
|
1375
1682
|
*
|
|
1376
1683
|
* `body` owns its own transaction if it needs one and must COMMIT before returning, so
|
|
@@ -1707,14 +2014,13 @@ export declare class ControlDatabase {
|
|
|
1707
2014
|
* locked write body — same per-call opt-out, and for the same reason, as
|
|
1708
2015
|
* {@link queryStampId}'s. Every unlocked caller keeps the default retried path.
|
|
1709
2016
|
*/
|
|
1710
|
-
queryFormationInvite(token: string, retry?: boolean): Promise<
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
} | null>;
|
|
2017
|
+
queryFormationInvite(token: string, retry?: boolean): Promise<FormationInviteRow | null>;
|
|
2018
|
+
/**
|
|
2019
|
+
* Every `FormationInvite` row, expired and spent ones included, in the shape
|
|
2020
|
+
* {@link queryFormationInvite} returns one. Read by a strand move
|
|
2021
|
+
* (`exportStrandBundle`), which decides per row what is still redeemable.
|
|
2022
|
+
*/
|
|
2023
|
+
queryFormationInvites(): Promise<FormationInviteRow[]>;
|
|
1718
2024
|
/**
|
|
1719
2025
|
* Count `FormationUsage` rows recorded against a token (uses consumed so far).
|
|
1720
2026
|
*
|
|
@@ -1769,6 +2075,197 @@ export declare class ControlDatabase {
|
|
|
1769
2075
|
* unlimited-use invite anywhere in the scan short-circuits all of them.
|
|
1770
2076
|
*/
|
|
1771
2077
|
hasOutstandingFormationInvite(nowMs?: number): Promise<boolean>;
|
|
2078
|
+
/**
|
|
2079
|
+
* Mint and insert an owner-signed `CadreInvite`: an invitation to join THIS cadre that any
|
|
2080
|
+
* member machine can later redeem on the owner's behalf ({@link redeemCadreInvite}).
|
|
2081
|
+
*
|
|
2082
|
+
* The owner signs the whole row ({@link cadreInviteAddMessage}) — `ExpiresAt` through
|
|
2083
|
+
* {@link canonicalDatetime} so the signed segment byte-matches the stored `datetime`, as
|
|
2084
|
+
* {@link insertFormationInvite} does — and the stored `IssuerKey`/`IssuerSig` are pinned to
|
|
2085
|
+
* that signer by `CadreInvite.AuthorizedInsert`. The signature is part of the row because
|
|
2086
|
+
* the row travels: the returned {@link CadreInviteRow} is what the invitation bundle
|
|
2087
|
+
* carries to the holder, and {@link seatCadreInvite} re-inserts it verbatim on a member
|
|
2088
|
+
* that has not received it by replication.
|
|
2089
|
+
*
|
|
2090
|
+
* `key` is the invitation's ed25519 PUBLIC key; the caller minted the pair and hands the
|
|
2091
|
+
* private half to the invitee out of band. It is never stored here (`Strand.Invite` has the
|
|
2092
|
+
* same shape).
|
|
2093
|
+
*
|
|
2094
|
+
* @param invite.key - the invitation public key (base64url ed25519), the row's primary key
|
|
2095
|
+
* @param invite.peerId - the device the invitation is for, or null for whoever redeems first
|
|
2096
|
+
* @param invite.grantsOwner - whether redemption also seats an `OwnerKey` row for the device
|
|
2097
|
+
* @param invite.expiresAtMs - epoch ms after which it cannot be redeemed; omit for never
|
|
2098
|
+
* @param invite.totalUses - how many redemptions it allows; omit for unlimited
|
|
2099
|
+
* @returns the row as stored, `expiresAt` in its canonical form
|
|
2100
|
+
*/
|
|
2101
|
+
insertCadreInvite(invite: {
|
|
2102
|
+
key: string;
|
|
2103
|
+
peerId?: string | null;
|
|
2104
|
+
grantsOwner: boolean;
|
|
2105
|
+
expiresAtMs?: number | null;
|
|
2106
|
+
totalUses?: number | null;
|
|
2107
|
+
}, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<CadreInviteRow>;
|
|
2108
|
+
/**
|
|
2109
|
+
* Seat an owner-signed `CadreInvite` row received inside an invitation bundle, under its own
|
|
2110
|
+
* stored `IssuerKey`/`IssuerSig` as the write context — insert-if-absent, so a member that
|
|
2111
|
+
* already holds the row (by replication, or from an earlier redemption attempt) is a no-op.
|
|
2112
|
+
*
|
|
2113
|
+
* @returns `true` when this call inserted the row, `false` when it was already present.
|
|
2114
|
+
* @throws {CadreInviteIssuerUnknownError} when `CadreInvite.AuthorizedInsert` refuses it: the
|
|
2115
|
+
* issuer is not an owner on this node (not replicated yet, or removed), or the signature does
|
|
2116
|
+
* not verify. Every other refusal (a withdrawn stamp, `NotRevoked`) propagates as is.
|
|
2117
|
+
*/
|
|
2118
|
+
seatCadreInvite(row: CadreInviteRow): Promise<boolean>;
|
|
2119
|
+
/** Bare `CadreInvite` insert shared by {@link insertCadreInvite} and {@link seatCadreInvite}; the caller holds the lock. */
|
|
2120
|
+
private execCadreInviteInsert;
|
|
2121
|
+
/**
|
|
2122
|
+
* Read one `CadreInvite` row by key, or null when absent. Withdrawn rows are returned too —
|
|
2123
|
+
* the row is the holder's proof and never goes away; liveness is {@link hasLiveCadreInvite}'s
|
|
2124
|
+
* question, and `CadreInviteUsage.Authorized`'s at redemption.
|
|
2125
|
+
*
|
|
2126
|
+
* `retry: false` is passed by {@link redeemCadreInvite}, which reads INSIDE its locked write
|
|
2127
|
+
* body — the same per-call opt-out, for the same reason, as {@link queryStampId}'s.
|
|
2128
|
+
*/
|
|
2129
|
+
queryCadreInvite(key: string, retry?: boolean): Promise<CadreInviteRow | null>;
|
|
2130
|
+
/**
|
|
2131
|
+
* Every `CadreInvite` row this node holds, withdrawn and expired ones included (see
|
|
2132
|
+
* {@link queryCadreInvite}). Read by the membership predicate when at least one `CadrePeer`
|
|
2133
|
+
* row is invitation-admitted; `retry: false` is forwarded from
|
|
2134
|
+
* {@link CadreNode.listAuthorizedMembers} on the membership-gate refresh path for
|
|
2135
|
+
* {@link queryCadrePeers}' reason.
|
|
2136
|
+
*/
|
|
2137
|
+
queryCadreInvites(retry?: boolean): Promise<CadreInviteRow[]>;
|
|
2138
|
+
/**
|
|
2139
|
+
* Every `CadreInviteUsage` row this node holds — the redemption records the membership
|
|
2140
|
+
* predicate verifies invitation-admitted rows against. Same `retry` contract as
|
|
2141
|
+
* {@link queryCadreInvites}.
|
|
2142
|
+
*/
|
|
2143
|
+
queryCadreInviteUsages(retry?: boolean): Promise<CadreInviteUsageRow[]>;
|
|
2144
|
+
/**
|
|
2145
|
+
* How many times a cadre invitation has been redeemed (its `CadreInviteUsage` rows), served
|
|
2146
|
+
* through the `CadreInviteUsageByInvite` index. As {@link countFormationUsage}: a permissive
|
|
2147
|
+
* PRE-check, not the cap — the cap is `CadreInviteUsage.Authorized`'s own committed count.
|
|
2148
|
+
*
|
|
2149
|
+
* `retry: false` is passed by {@link assertCadreInviteSeatRemains}, inside the locked body.
|
|
2150
|
+
*/
|
|
2151
|
+
countCadreInviteUsage(key: string, retry?: boolean): Promise<number>;
|
|
2152
|
+
/**
|
|
2153
|
+
* Is any cadre invitation still redeemable — not withdrawn, unexpired, with uses below its
|
|
2154
|
+
* `TotalUses`, and issued by a current owner? The conditions `CadreInviteUsage.Authorized`
|
|
2155
|
+
* applies, read ahead of time so the control-network connection gate can answer "does this
|
|
2156
|
+
* node expect a stranger?" with no key to ask about, exactly as
|
|
2157
|
+
* {@link hasOutstandingFormationInvite} answers it for strand formation. The expiry
|
|
2158
|
+
* comparison is `expiresAtMs <= now`, so an invitation the redemption would refuse never
|
|
2159
|
+
* holds the gate open.
|
|
2160
|
+
*
|
|
2161
|
+
* Withdrawal is read through {@link queryRevokedStamps}, the per-table retired-stamp seam
|
|
2162
|
+
* every other revocation-aware reader uses (and the one an isolated node can still answer).
|
|
2163
|
+
*/
|
|
2164
|
+
hasLiveCadreInvite(nowMs?: number): Promise<boolean>;
|
|
2165
|
+
/**
|
|
2166
|
+
* Is this one cadre invitation still redeemable here — the conditions
|
|
2167
|
+
* {@link hasLiveCadreInvite} applies, for one invitation? The row judged is the one this node
|
|
2168
|
+
* holds under `invite.key`; when it holds none, `invite` itself (a bundle's copy, not seated
|
|
2169
|
+
* yet), but only if its issuer signature verifies, because an unsigned copy would otherwise be
|
|
2170
|
+
* judged by whichever owner it names as issuer. A permissive pre-check like
|
|
2171
|
+
* {@link countCadreInviteUsage}: {@link seatCadreInvite} and the redemption's own write are
|
|
2172
|
+
* what enforce every condition.
|
|
2173
|
+
*/
|
|
2174
|
+
isCadreInviteLive(invite: CadreInviteRow, nowMs?: number): Promise<boolean>;
|
|
2175
|
+
/**
|
|
2176
|
+
* Every `CadreInvite` row this node holds with its standing: how many redemptions are
|
|
2177
|
+
* recorded, whether it is withdrawn, and whether it is still redeemable — the same
|
|
2178
|
+
* conditions {@link hasLiveCadreInvite} applies, answered per row for an owner's listing
|
|
2179
|
+
* rather than short-circuited. One usage count per row, so this is the owner's
|
|
2180
|
+
* occasional read, not a gate's.
|
|
2181
|
+
*/
|
|
2182
|
+
listCadreInviteStatuses(nowMs?: number): Promise<CadreInviteStatus[]>;
|
|
2183
|
+
/**
|
|
2184
|
+
* Owner-signed WITHDRAWAL of a cadre invitation: file the `Revocation` tombstone retiring its
|
|
2185
|
+
* stamp, with the row kept. The row stays because every device the invitation admitted
|
|
2186
|
+
* proves its membership through it (`CadrePeer.AuthorizedInsert`'s consent branch,
|
|
2187
|
+
* `verifyInvitationAdmission`); `CadreInvite.NoDelete` refuses a delete, and
|
|
2188
|
+
* `Revocation.RowIsGone` admits a `'CadreInvite'` tombstone while the row lives. Afterwards
|
|
2189
|
+
* `CadreInviteUsage.Authorized` refuses every further redemption, and `CadreInvite.NotRevoked`
|
|
2190
|
+
* refuses re-seating the row from the holder's signed copy on any node holding the
|
|
2191
|
+
* tombstone.
|
|
2192
|
+
*
|
|
2193
|
+
* Idempotent: a no-op (no throw, nothing written) when the row is absent locally or already
|
|
2194
|
+
* withdrawn — `false` then, `true` when this call filed the tombstone. The owner must hold the
|
|
2195
|
+
* row to withdraw it, since the tombstone names the row's stamp.
|
|
2196
|
+
*
|
|
2197
|
+
* Fires the guarded-delete listener like a removal does, so a withdrawal committed while the
|
|
2198
|
+
* node was alone is queued for re-issue on cohort growth ({@link CadreNode.noteGuardedDelete}).
|
|
2199
|
+
*/
|
|
2200
|
+
withdrawCadreInvite(key: string, ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<boolean>;
|
|
2201
|
+
/**
|
|
2202
|
+
* The withdrawal tombstones for the `CadreInvite` rows among `keys` that are held here and
|
|
2203
|
+
* not yet withdrawn, signed and ready for {@link execTombstone}: the body
|
|
2204
|
+
* {@link withdrawCadreInvite} files one of and {@link removeMachine} files several of in one
|
|
2205
|
+
* transaction. Inside the locked body (every read `retry: false`); an absent or already
|
|
2206
|
+
* withdrawn key is logged and left out.
|
|
2207
|
+
*/
|
|
2208
|
+
private signCadreInviteWithdrawals;
|
|
2209
|
+
/**
|
|
2210
|
+
* Redeem a seated `CadreInvite` on the owner's behalf: admit `peerId` as a cadre member and,
|
|
2211
|
+
* when the invitation grants ownership, as an owner — with NO owner signature. One
|
|
2212
|
+
* transaction writes the `CadrePeer` row (consent branch of `AuthorizedInsert`), the
|
|
2213
|
+
* `OwnerKey` row when granted (consent branch of `Authorized`), and last the
|
|
2214
|
+
* `CadreInviteUsage` row that authorizes both; the usage row's own checks (`PeerExists`,
|
|
2215
|
+
* `OwnerExists`) read the rows back, so the three are mutually bound to this exact
|
|
2216
|
+
* redemption and none can be held in reserve. The invitation must already be committed
|
|
2217
|
+
* ({@link seatCadreInvite}): every consent branch reads `committed.CadreInvite`.
|
|
2218
|
+
*
|
|
2219
|
+
* The two signatures are the holder's and the device's, minted on the device over
|
|
2220
|
+
* {@link cadreInviteRedeemMessage} / {@link cadreInviteConsentMessage}; this machine only
|
|
2221
|
+
* verifies them (by writing the row). `peerId` must be the identity multihash of `peerKey`:
|
|
2222
|
+
* the schema cannot derive it, so a writer asserts the pair and every reader re-derives it
|
|
2223
|
+
* (`verifyInvitationAdmission`) — a mismatched pair is written and then never trusted.
|
|
2224
|
+
*
|
|
2225
|
+
* Idempotent for a retry after a dropped reply: when the device's row is already present
|
|
2226
|
+
* (and its `OwnerKey` row too, when granted) nothing is written and no seat is spent —
|
|
2227
|
+
* `alreadyMember: true`. A present row whose `OwnerKey` row is missing under an
|
|
2228
|
+
* owner-granting invitation is completed: the usage row then names the existing peer stamp.
|
|
2229
|
+
* The seat check ({@link assertCadreInviteSeatRemains}) runs after that, so the retry of a
|
|
2230
|
+
* single-use redemption is answered `alreadyMember` rather than refused as exhausted.
|
|
2231
|
+
*
|
|
2232
|
+
* Through {@link mutateCadrePeer}, so the membership hub is notified; inside the locked body
|
|
2233
|
+
* every read passes `retry: false` and every stamp is minted per attempt, so a
|
|
2234
|
+
* {@link lockedWithRetry} re-run starts clean.
|
|
2235
|
+
*
|
|
2236
|
+
* @throws {InvitationExhaustedError} when the invitation's seats are all spent (by name,
|
|
2237
|
+
* ahead of the schema's generic `Authorized` refusal — see {@link assertSeatRemains}).
|
|
2238
|
+
*/
|
|
2239
|
+
redeemCadreInvite(params: {
|
|
2240
|
+
inviteKey: string;
|
|
2241
|
+
peerId: string;
|
|
2242
|
+
/** The device's ed25519 public key (base64url); `peerId` is its identity multihash. */
|
|
2243
|
+
peerKey: string;
|
|
2244
|
+
/** The device's dialable addresses, if known; stored comma-joined as `authorizePeer` does. */
|
|
2245
|
+
multiaddrs?: string[];
|
|
2246
|
+
/** Single-use nonce minted by the device; both signatures cover it. */
|
|
2247
|
+
usageStampId: string;
|
|
2248
|
+
/** The holder's signature with the invitation private key over the `'redeem'` digest. */
|
|
2249
|
+
inviteSig: string;
|
|
2250
|
+
/** The device's signature with its own key over the `'consent'` digest. */
|
|
2251
|
+
peerSig: string;
|
|
2252
|
+
nowMs?: number;
|
|
2253
|
+
}): Promise<CadreInviteRedemptionResult>;
|
|
2254
|
+
/**
|
|
2255
|
+
* The rows a redemption would seat that are already present: the device's `CadrePeer`
|
|
2256
|
+
* stamp, and — only when the invitation grants ownership — its `OwnerKey` stamp. Raw stamp
|
|
2257
|
+
* reads ({@link queryStampId}), so a physically present row counts; inside the locked
|
|
2258
|
+
* body, so never retried.
|
|
2259
|
+
*/
|
|
2260
|
+
private readSeatedAdmission;
|
|
2261
|
+
/**
|
|
2262
|
+
* Refuse a redemption that would consume a seat the cadre invitation does not have, by
|
|
2263
|
+
* name ({@link InvitationExhaustedError}) — the cadre-invitation twin of
|
|
2264
|
+
* {@link assertSeatRemains}, for the same reason: the schema's cap clause refuses as a
|
|
2265
|
+
* generic `Authorized` failure a caller would otherwise report as retryable. Reads the
|
|
2266
|
+
* committed count inside the locked body, so on a same-node race the loser is refused here.
|
|
2267
|
+
*/
|
|
2268
|
+
private assertCadreInviteSeatRemains;
|
|
1772
2269
|
/**
|
|
1773
2270
|
* Close the database and cleanup resources.
|
|
1774
2271
|
*
|