@serfab/cadre-core 1.16.0 → 1.18.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 +4 -3
- package/dist/bootstrap-peer-store-file.js +1 -1
- package/dist/bootstrap-peer-store.d.ts +1 -1
- package/dist/bootstrap-peer-store.js +4 -4
- package/dist/cadre-invite-protocol.d.ts +81 -142
- package/dist/cadre-invite-protocol.js +168 -458
- package/dist/cadre-invite-protocol.js.map +1 -1
- package/dist/cadre-join-request.d.ts +15 -2
- package/dist/cadre-join-request.js +22 -3
- package/dist/cadre-join-request.js.map +1 -1
- package/dist/cadre-node.d.ts +1089 -475
- package/dist/cadre-node.js +2170 -1037
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-authorization.d.ts +36 -14
- package/dist/control-authorization.js +20 -3
- package/dist/control-authorization.js.map +1 -1
- package/dist/control-cohort.d.ts +88 -32
- package/dist/control-cohort.js +91 -33
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +856 -318
- package/dist/control-database.js +1422 -701
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +46 -43
- package/dist/control-formation-recorder.js +77 -78
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-protocol-guard.d.ts +11 -4
- package/dist/control-protocol-guard.js +20 -5
- package/dist/control-protocol-guard.js.map +1 -1
- package/dist/control-schema-version.d.ts +97 -0
- package/dist/control-schema-version.js +225 -0
- package/dist/control-schema-version.js.map +1 -0
- package/dist/control-schema.d.ts +45 -5
- package/dist/control-schema.js +262 -78
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +45 -10
- package/dist/control-write-retry.js +92 -22
- package/dist/control-write-retry.js.map +1 -1
- package/dist/delegate-admission.d.ts +13 -0
- package/dist/delegate-admission.js +27 -0
- package/dist/delegate-admission.js.map +1 -1
- package/dist/ed25519-key.d.ts +10 -7
- package/dist/ed25519-key.js +23 -20
- package/dist/ed25519-key.js.map +1 -1
- package/dist/edge-node/config.d.ts +122 -0
- package/dist/edge-node/config.js +199 -0
- package/dist/edge-node/config.js.map +1 -0
- package/dist/edge-node/dissolve.d.ts +95 -0
- package/dist/edge-node/dissolve.js +184 -0
- package/dist/edge-node/dissolve.js.map +1 -0
- package/dist/edge-node/edge-join.d.ts +44 -0
- package/dist/edge-node/edge-join.js +227 -0
- package/dist/edge-node/edge-join.js.map +1 -0
- package/dist/edge-node/edge-node.d.ts +295 -0
- package/dist/edge-node/edge-node.js +704 -0
- package/dist/edge-node/edge-node.js.map +1 -0
- package/dist/edge-node/index.d.ts +18 -0
- package/dist/edge-node/index.js +18 -0
- package/dist/edge-node/index.js.map +1 -0
- package/dist/edge-node/join-credential.d.ts +11 -0
- package/dist/edge-node/join-credential.js +11 -0
- package/dist/edge-node/join-credential.js.map +1 -0
- package/dist/edge-node/lazy-raw-storage.d.ts +40 -0
- package/dist/edge-node/lazy-raw-storage.js +134 -0
- package/dist/edge-node/lazy-raw-storage.js.map +1 -0
- package/dist/edge-node/node-local.d.ts +60 -0
- package/dist/edge-node/node-local.js +58 -0
- package/dist/edge-node/node-local.js.map +1 -0
- package/dist/edge-node/options.d.ts +83 -0
- package/dist/edge-node/options.js +110 -0
- package/dist/edge-node/options.js.map +1 -0
- package/dist/edge-node/owner-grant.d.ts +41 -0
- package/dist/edge-node/owner-grant.js +38 -0
- package/dist/edge-node/owner-grant.js.map +1 -0
- package/dist/edge-node/platform.d.ts +140 -0
- package/dist/edge-node/platform.js +12 -0
- package/dist/edge-node/platform.js.map +1 -0
- package/dist/edge-node/strand-move.d.ts +115 -0
- package/dist/edge-node/strand-move.js +223 -0
- package/dist/edge-node/strand-move.js.map +1 -0
- package/dist/edge-node/strands.d.ts +28 -0
- package/dist/edge-node/strands.js +60 -0
- package/dist/edge-node/strands.js.map +1 -0
- package/dist/enrolled-machine-store.d.ts +3 -3
- package/dist/enrolled-machine-store.js +3 -3
- package/dist/formation-token.d.ts +48 -0
- package/dist/formation-token.js +26 -0
- package/dist/formation-token.js.map +1 -0
- package/dist/index.d.ts +27 -17
- package/dist/index.js +39 -17
- package/dist/index.js.map +1 -1
- package/dist/joined-strand-store.d.ts +29 -12
- package/dist/joined-strand-store.js +66 -27
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/key-store.d.ts +9 -4
- package/dist/key-store.js +9 -4
- package/dist/key-store.js.map +1 -1
- package/dist/link-budget.d.ts +9 -7
- package/dist/link-budget.js +9 -7
- package/dist/link-budget.js.map +1 -1
- package/dist/machine-removal.d.ts +19 -39
- package/dist/machine-removal.js +24 -98
- package/dist/machine-removal.js.map +1 -1
- package/dist/member-redemption.d.ts +256 -0
- package/dist/member-redemption.js +490 -0
- package/dist/member-redemption.js.map +1 -0
- package/dist/membership-view.d.ts +91 -0
- package/dist/membership-view.js +110 -0
- package/dist/membership-view.js.map +1 -0
- package/dist/owner-anchor-sync.d.ts +4 -4
- package/dist/owner-anchor-sync.js +16 -15
- package/dist/owner-anchor-sync.js.map +1 -1
- package/dist/owner-grant-protocol.d.ts +251 -0
- package/dist/owner-grant-protocol.js +344 -0
- package/dist/owner-grant-protocol.js.map +1 -0
- package/dist/owner-key-export.d.ts +70 -0
- package/dist/owner-key-export.js +279 -0
- package/dist/owner-key-export.js.map +1 -0
- package/dist/owner-key-flows.d.ts +55 -0
- package/dist/owner-key-flows.js +60 -0
- package/dist/owner-key-flows.js.map +1 -0
- package/dist/owner-key-store.d.ts +31 -0
- package/dist/owner-key-store.js +81 -0
- package/dist/owner-key-store.js.map +1 -0
- package/dist/owner-keys.d.ts +78 -0
- package/dist/owner-keys.js +133 -0
- package/dist/owner-keys.js.map +1 -0
- package/dist/owner-signer.d.ts +54 -0
- package/dist/owner-signer.js +51 -0
- package/dist/owner-signer.js.map +1 -0
- package/dist/peer-addr-book.d.ts +10 -1
- package/dist/peer-addr-book.js +4 -3
- package/dist/peer-addr-book.js.map +1 -1
- package/dist/peer-authorization.d.ts +83 -18
- package/dist/peer-authorization.js +123 -53
- package/dist/peer-authorization.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +89 -20
- package/dist/peer-join-backfill.js +265 -108
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/pending-join-runner.d.ts +48 -18
- package/dist/pending-join-runner.js +74 -56
- package/dist/pending-join-runner.js.map +1 -1
- package/dist/provider/rent-node.js +1 -1
- package/dist/provider/rent-node.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +162 -133
- package/dist/seed-bootstrap.js +300 -283
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/seed-trust-policy.js +4 -4
- package/dist/strand-addr-protocol.d.ts +4 -1
- package/dist/strand-addr-protocol.js +8 -2
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-cohort.d.ts +3 -0
- package/dist/strand-cohort.js.map +1 -1
- package/dist/strand-database.d.ts +24 -2
- package/dist/strand-database.js +29 -6
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +176 -88
- package/dist/strand-first-sync-gate.js +288 -121
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-formation-deadlines.d.ts +9 -9
- package/dist/strand-formation-deadlines.js +9 -9
- package/dist/strand-formation-manager.d.ts +23 -17
- package/dist/strand-formation-manager.js +46 -44
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +5 -8
- package/dist/strand-formation-protocol.js +101 -16
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +27 -18
- package/dist/strand-instance-manager.js +119 -50
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-membership-reconciler.d.ts +16 -13
- package/dist/strand-membership-reconciler.js +30 -22
- package/dist/strand-membership-reconciler.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +45 -22
- package/dist/strand-membership-writer.js +226 -131
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-move.d.ts +6 -5
- package/dist/strand-move.js +36 -32
- package/dist/strand-move.js.map +1 -1
- package/dist/strand-peer-redial.js +2 -3
- package/dist/strand-peer-redial.js.map +1 -1
- package/dist/strand-revocation-enforcer.d.ts +134 -60
- package/dist/strand-revocation-enforcer.js +144 -71
- package/dist/strand-revocation-enforcer.js.map +1 -1
- package/dist/strand-solicitation.d.ts +50 -39
- package/dist/strand-solicitation.js +10 -3
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-watcher.d.ts +80 -13
- package/dist/strand-watcher.js +152 -48
- package/dist/strand-watcher.js.map +1 -1
- package/dist/table-change-watch.d.ts +63 -0
- package/dist/table-change-watch.js +209 -0
- package/dist/table-change-watch.js.map +1 -0
- package/dist/timeout-scheduler.d.ts +4 -3
- package/dist/timeout-scheduler.js +4 -3
- package/dist/timeout-scheduler.js.map +1 -1
- package/dist/types.d.ts +374 -150
- package/dist/types.js +4 -4
- package/dist/types.js.map +1 -1
- package/package.json +17 -8
- package/src/bootstrap-peer-store-file.ts +44 -44
- package/src/bootstrap-peer-store.ts +214 -214
- package/src/cadre-invite-protocol.ts +740 -1052
- package/src/cadre-join-request.ts +24 -3
- package/src/cadre-node.ts +2402 -1079
- package/src/control-authorization.ts +47 -14
- package/src/control-cohort.ts +204 -128
- package/src/control-database.ts +1749 -820
- package/src/control-formation-recorder.ts +359 -353
- package/src/control-protocol-guard.ts +23 -6
- package/src/control-schema-version.ts +301 -0
- package/src/control-schema.ts +273 -78
- package/src/control-write-retry.ts +641 -567
- package/src/delegate-admission.ts +351 -314
- package/src/ed25519-key.ts +174 -171
- package/src/edge-node/config.ts +277 -0
- package/src/edge-node/dissolve.ts +251 -0
- package/src/edge-node/edge-join.ts +319 -0
- package/src/edge-node/edge-node.ts +1045 -0
- package/src/edge-node/index.ts +59 -0
- package/src/edge-node/join-credential.ts +16 -0
- package/src/edge-node/lazy-raw-storage.ts +178 -0
- package/src/edge-node/node-local.ts +84 -0
- package/src/edge-node/options.ts +179 -0
- package/src/edge-node/owner-grant.ts +63 -0
- package/src/edge-node/platform.ts +151 -0
- package/src/edge-node/strand-move.ts +270 -0
- package/src/edge-node/strands.ts +72 -0
- package/src/enrolled-machine-store.ts +3 -3
- package/src/formation-token.ts +63 -0
- package/src/index.ts +899 -789
- package/src/joined-strand-store.ts +73 -28
- package/src/key-store.ts +10 -4
- package/src/link-budget.ts +9 -7
- package/src/machine-removal.ts +26 -111
- package/src/member-redemption.ts +649 -0
- package/src/membership-view.ts +144 -0
- package/src/owner-anchor-sync.ts +19 -18
- package/src/owner-grant-protocol.ts +547 -0
- package/src/owner-key-export.ts +348 -0
- package/src/owner-key-flows.ts +109 -0
- package/src/owner-key-store.ts +85 -0
- package/src/owner-keys.ts +178 -0
- package/src/owner-signer.ts +94 -0
- package/src/peer-addr-book.ts +4 -3
- package/src/peer-authorization.ts +558 -488
- package/src/peer-join-backfill.ts +971 -761
- package/src/pending-join-runner.ts +76 -53
- package/src/provider/rent-node.ts +1 -1
- package/src/seed-bootstrap.ts +343 -349
- package/src/seed-trust-policy.ts +4 -4
- package/src/strand-addr-protocol.ts +547 -538
- package/src/strand-cohort.ts +4 -0
- package/src/strand-database.ts +352 -319
- package/src/strand-first-sync-gate.ts +334 -133
- package/src/strand-formation-deadlines.ts +227 -227
- package/src/strand-formation-manager.ts +731 -725
- package/src/strand-formation-protocol.ts +122 -24
- package/src/strand-instance-manager.ts +2092 -2026
- package/src/strand-membership-reconciler.ts +33 -25
- package/src/strand-membership-writer.ts +2093 -1974
- package/src/strand-move.ts +50 -38
- package/src/strand-peer-redial.ts +2 -3
- package/src/strand-revocation-enforcer.ts +740 -654
- package/src/strand-solicitation.ts +61 -34
- package/src/strand-watcher.ts +175 -56
- package/src/table-change-watch.ts +285 -0
- package/src/timeout-scheduler.ts +29 -28
- package/src/types.ts +391 -152
|
@@ -2,9 +2,12 @@ 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, JoinRequestRow, JoinOutcome, PendingJoin, NodeProfile, PeerAddressRecord, CadrePeerRow, OwnerKeyRow, RevocationRow,
|
|
6
|
-
import type { ControlTable, RevocableTable, BoundControlDomain, ControlAction, CadreInviteSignedFields } from './control-authorization.js';
|
|
5
|
+
import type { StrandRow, JoinRequestRow, JoinOutcome, PendingJoin, NodeProfile, PeerAddressRecord, CadrePeerRow, OwnerKeyRow, RevocationRow, DeviceTokenRecord, DeviceTokenRow, CadreInviteRow, CadreInviteUsageRow, CadreInviteRedemptionResult, CadreInviteStatus, OwnerGrantRow, OwnerGrantUsageRow, OwnerGrantRedemptionResult, OwnerGrantStatus, SeedPeer, MachineRemovalResult } from './types.js';
|
|
6
|
+
import type { ControlTable, RevocableTable, BoundControlDomain, ControlAction, CadreInviteSignedFields, OwnerGrantSignedFields } from './control-authorization.js';
|
|
7
|
+
import type { ControlWriteRetryOptions } from './control-write-retry.js';
|
|
7
8
|
import type { ControlRetryAbandonment } from './control-retry.js';
|
|
9
|
+
import type { TableChangeWatch, TableChangeWatchOptions } from './table-change-watch.js';
|
|
10
|
+
import type { FormationInviteFacts } from './formation-token.js';
|
|
8
11
|
export type { ControlTable, RevocableTable, ControlDomain, ControlAction } from './control-authorization.js';
|
|
9
12
|
/**
|
|
10
13
|
* Generate a unique stamp ID for transaction authorization.
|
|
@@ -18,12 +21,11 @@ export declare function generateStampId(peerId: string): string;
|
|
|
18
21
|
* present locally.
|
|
19
22
|
*
|
|
20
23
|
* Consent is bound to the strand's one-off `StampId` (see the consent branch of
|
|
21
|
-
* `Strand.AuthorizedInsert`), so
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* CHECK at commit (which would drop the write with a far less legible error).
|
|
24
|
+
* `Strand.AuthorizedInsert`), so a writer not handed the stamp reads the live row before
|
|
25
|
+
* inserting, and throws this when the row is absent rather than leaving it to fail the
|
|
26
|
+
* deferred `StrandExists` CHECK at commit (a far less legible error). The formation
|
|
27
|
+
* responder hands the stamp over from `ControlFormationUsageRecorder.resolveStrand`, which
|
|
28
|
+
* reports an absent row as `missing`; a row removed after that read fails the CHECK instead.
|
|
27
29
|
*/
|
|
28
30
|
export declare class MissingHostStrandError extends Error {
|
|
29
31
|
readonly strandId: string;
|
|
@@ -52,22 +54,23 @@ export declare class FormationAbortedError extends Error {
|
|
|
52
54
|
/**
|
|
53
55
|
* The invitation's seat budget is already spent: the count of recorded usage rows for the
|
|
54
56
|
* invitation has reached its `TotalUses`, so this redemption cannot be given a seat. `token`
|
|
55
|
-
* is the `FormationInvite.Token` or the `
|
|
56
|
-
* redeemed against.
|
|
57
|
+
* is the `FormationInvite.Token`, the `CadreInvite.Key` or the `OwnerGrant.Key`, whichever
|
|
58
|
+
* table the caller redeemed against.
|
|
57
59
|
*
|
|
58
60
|
* Raised off the committed-count check ({@link ControlDatabase.assertSeatRemains}) ahead of
|
|
59
61
|
* the write, inside {@link ControlDatabase.redeemInvitation} /
|
|
60
62
|
* {@link ControlDatabase.recordFormationUsage}, off the same check for a cadre invitation
|
|
61
|
-
* ({@link ControlDatabase.redeemCadreInvite})
|
|
62
|
-
*
|
|
63
|
-
*
|
|
63
|
+
* ({@link ControlDatabase.redeemCadreInvite}) and an owner grant
|
|
64
|
+
* ({@link ControlDatabase.redeemOwnerGrant}), and off the unlocked re-check
|
|
65
|
+
* `ControlFormationUsageRecorder.authorizeUsage` runs after an outside approval, before the
|
|
66
|
+
* manager issues a membership pass — and off nothing else. It exists because
|
|
64
67
|
* without it the refusal surfaces as a generic `CHECK constraint failed: Authorized` from the
|
|
65
68
|
* schema's own count-based cap clause, which the manager reports as a retryable
|
|
66
69
|
* `Formation conflict, retry` — telling the joiner to retry something that can never succeed.
|
|
67
70
|
* Two redemptions of one token racing on the SAME node serialize behind the local write
|
|
68
|
-
* queue, so the loser reads the winner's committed row and is refused here by name —
|
|
69
|
-
* `StrandFormationManager.validateToken`
|
|
70
|
-
*
|
|
71
|
+
* queue, so the loser reads the winner's committed row and is refused here by name — the
|
|
72
|
+
* use count `StrandFormationManager.validateToken` reads cannot catch that, since it was
|
|
73
|
+
* read before either redemption wrote anything.
|
|
71
74
|
*/
|
|
72
75
|
export declare class InvitationExhaustedError extends Error {
|
|
73
76
|
readonly token: string;
|
|
@@ -89,6 +92,35 @@ export declare class CadreInviteIssuerUnknownError extends Error {
|
|
|
89
92
|
cause?: unknown;
|
|
90
93
|
});
|
|
91
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* {@link ControlDatabase.seatOwnerGrant} was refused by `OwnerGrant.AuthorizedInsert`: the
|
|
97
|
+
* grant twin of {@link CadreInviteIssuerUnknownError}, for the same two causes (the issuer's
|
|
98
|
+
* `OwnerKey` row has not replicated here yet or has since been removed; or a tampered row).
|
|
99
|
+
* Its own class rather than a shared one so a caller that handles invitation and grant
|
|
100
|
+
* bundles can tell by `instanceof` which credential was refused.
|
|
101
|
+
*/
|
|
102
|
+
export declare class OwnerGrantIssuerUnknownError extends Error {
|
|
103
|
+
readonly grantKey: string;
|
|
104
|
+
readonly issuerKey: string;
|
|
105
|
+
constructor(grantKey: string, issuerKey: string, options?: {
|
|
106
|
+
cause?: unknown;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* An issued row (a `CadreInvite` or an `OwnerGrant`) the cadre holds that this node cannot read
|
|
111
|
+
* yet: {@link ControlDatabase.seatCadreInvite} / {@link ControlDatabase.seatOwnerGrant} had its
|
|
112
|
+
* insert refused by a cohort member already holding the row, and a refreshed read here still
|
|
113
|
+
* finds nothing; or the redemption that follows the seat finds no row. It clears once this
|
|
114
|
+
* node's store catches up, which is why the redemption paths answer it with the retryable
|
|
115
|
+
* `conflict` code.
|
|
116
|
+
*/
|
|
117
|
+
export declare class IssuedRowNotHereYetError extends Error {
|
|
118
|
+
readonly table: IssuedTable;
|
|
119
|
+
readonly key: string;
|
|
120
|
+
constructor(table: IssuedTable, key: string, options?: {
|
|
121
|
+
cause?: unknown;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
92
124
|
/**
|
|
93
125
|
* What a recorded `FormationUsage` row came out as.
|
|
94
126
|
*
|
|
@@ -238,15 +270,133 @@ export declare function cadreInviteRedeemMessage(partyId: string, fields: CadreI
|
|
|
238
270
|
* distinct action tag, so neither signature can stand in for the other.
|
|
239
271
|
*/
|
|
240
272
|
export declare function cadreInviteConsentMessage(partyId: string, fields: CadreInviteRedemptionFields): Uint8Array;
|
|
273
|
+
/**
|
|
274
|
+
* The exact bytes an owner signs to seat an `OwnerGrant` row — the TS mirror of the `'add'`
|
|
275
|
+
* digest in `OwnerGrant.AuthorizedInsert`, over the whole row in the schema's order with
|
|
276
|
+
* nullable columns as `''` (`ownerGrantRowFields`, shared with the base64url verifier
|
|
277
|
+
* `peer-authorization.ts`'s `ownerGrantAddDigest`). `expiresAt` must already be the stored
|
|
278
|
+
* canonical `datetime` string, as {@link ControlDatabase.insertOwnerGrant} produces it, for
|
|
279
|
+
* the reason on {@link cadreInviteAddMessage}.
|
|
280
|
+
*/
|
|
281
|
+
export declare function ownerGrantAddMessage(partyId: string, row: OwnerGrantSignedFields): Uint8Array;
|
|
282
|
+
/** The fields both owner-grant redemption signatures cover; see {@link ownerGrantRedeemMessage}. */
|
|
283
|
+
export interface OwnerGrantRedemptionFields {
|
|
284
|
+
grantKey: string;
|
|
285
|
+
/** The single-use nonce the holder minted for this redemption (`generateStampId`). */
|
|
286
|
+
usageStampId: string;
|
|
287
|
+
/** The owner key being seated (base64url ed25519). */
|
|
288
|
+
seatedKey: string;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* The exact bytes the HOLDER of an owner grant signs, with the grant's private key, to redeem
|
|
292
|
+
* it once — the TS mirror of the `'redeem'` digest `OwnerGrantUsage.GrantPossessed` verifies
|
|
293
|
+
* against the grant's public key. Binding the seated key and the nonce means a captured
|
|
294
|
+
* redemption can seat no other key and spend no other use.
|
|
295
|
+
*/
|
|
296
|
+
export declare function ownerGrantRedeemMessage(partyId: string, fields: OwnerGrantRedemptionFields): Uint8Array;
|
|
297
|
+
/**
|
|
298
|
+
* The exact bytes the SEATED key signs, proving the holder holds it — the TS mirror of the
|
|
299
|
+
* `'consent'` digest `OwnerGrantUsage.OwnerConsented` verifies against `SeatedKey`. Same
|
|
300
|
+
* fields as {@link ownerGrantRedeemMessage} under a distinct action tag, so neither
|
|
301
|
+
* signature can stand in for the other.
|
|
302
|
+
*/
|
|
303
|
+
export declare function ownerGrantConsentMessage(partyId: string, fields: OwnerGrantRedemptionFields): Uint8Array;
|
|
304
|
+
/**
|
|
305
|
+
* A guarded table of owner-issued credentials: signed rows that travel inside a bundle, are
|
|
306
|
+
* seated verbatim by any member ({@link ControlDatabase.seatCadreInvite},
|
|
307
|
+
* {@link ControlDatabase.seatOwnerGrant}), and are never deleted (`NoDelete`) because the
|
|
308
|
+
* rows they admitted prove themselves through them. Withdrawing one is a tombstone alone
|
|
309
|
+
* ({@link ControlDatabase.withdrawCadreInvite}, {@link ControlDatabase.withdrawOwnerGrant}).
|
|
310
|
+
*/
|
|
311
|
+
type IssuedTable = Extract<RevocableTable, 'CadreInvite' | 'OwnerGrant'>;
|
|
312
|
+
/**
|
|
313
|
+
* An owner key's label as its row stores it: trimmed, a blank one left out, and refused past
|
|
314
|
+
* {@link OWNER_KEY_MAX_LABEL} by name rather than by the schema's `LabelLength` check.
|
|
315
|
+
* Idempotent, so a caller that checks before opening an owner approval can pass the result on.
|
|
316
|
+
*/
|
|
317
|
+
export declare function ownerKeyLabel(label: string | null | undefined): string | null;
|
|
318
|
+
/**
|
|
319
|
+
* Signs one owner digest's raw bytes (no pre-hash) and returns a base64url Ed25519 signature:
|
|
320
|
+
* the callback every owner-signed writer takes. An `OwnerSign` from an open
|
|
321
|
+
* `OwnerSigner.approve` fits it, and so does a synchronous signer.
|
|
322
|
+
*
|
|
323
|
+
* NOTE: writers that read the stamp they sign do so inside the write lock, so the lock is held
|
|
324
|
+
* across these calls, and a `lockedWithRetry` re-run signs again inside the same approval.
|
|
325
|
+
* Free for a software signer. If a prompting signer ever holds the lock long enough to stall
|
|
326
|
+
* other control writes, sign outside the lock and re-check the stamp inside it.
|
|
327
|
+
*/
|
|
328
|
+
export type ControlSign = (message: Uint8Array) => string | Promise<string>;
|
|
329
|
+
/**
|
|
330
|
+
* Signs this node's own address record over its `CadrePeer` row as it stands —
|
|
331
|
+
* {@link ControlDatabase.updateSelfPeerRecord}'s callback, run inside the locked write body on
|
|
332
|
+
* every attempt, so the record written is always stamped past the row that was actually read.
|
|
333
|
+
*/
|
|
334
|
+
export type SelfRecordSigner = (current: PeerAddressRecord) => PeerAddressRecord;
|
|
335
|
+
/**
|
|
336
|
+
* Which owner re-touch {@link ControlDatabase.reauthorizeCadrePeer} makes over a `CadrePeer` row:
|
|
337
|
+
*
|
|
338
|
+
* - `'rebroadcast'` — the write-while-alone drain re-emitting a row that committed local-only. It
|
|
339
|
+
* bumps `UpdatedAt` so the row goes out as a fresh transaction, and writes only while the row's
|
|
340
|
+
* `Sig` is null: once the machine has self-published, the row is its to refresh, and a bump
|
|
341
|
+
* without a re-sign would leave its signature unverifiable.
|
|
342
|
+
* - `'revouch'` — an owner rebinding the row's voucher to its own key before the vouching key is
|
|
343
|
+
* removed (`CadreNode.removeOwner`). Written whether or not the machine has self-published, and
|
|
344
|
+
* leaves `UpdatedAt` untouched, so a self-signed record stays verifiable.
|
|
345
|
+
*/
|
|
346
|
+
export type CadrePeerRetouchMode = 'rebroadcast' | 'revouch';
|
|
347
|
+
/**
|
|
348
|
+
* What {@link ControlDatabase.reauthorizeCadrePeer} found and did: the row was re-touched; no
|
|
349
|
+
* readable row exists (absent, or its stamp retired); or — `'rebroadcast'` only — the machine has
|
|
350
|
+
* self-published since, so the row is no longer the owner's to re-emit. The two no-write outcomes
|
|
351
|
+
* are decided over the row as read inside the locked body, so a caller can act on them.
|
|
352
|
+
*/
|
|
353
|
+
export type CadrePeerRetouchOutcome = 'retouched' | 'absent' | 'self-published';
|
|
354
|
+
/** A formation invitation written with a founding ({@link FoundingRows}), bound to the strand founded. */
|
|
355
|
+
export interface FoundingInvite {
|
|
356
|
+
/** The invitation token, the `FormationInvite` primary key. */
|
|
357
|
+
token: string;
|
|
358
|
+
/** The sApp a redeemed strand will use. */
|
|
359
|
+
sAppId: string;
|
|
360
|
+
/** Epoch ms after which it cannot be redeemed; omit for never. */
|
|
361
|
+
expiresAtMs?: number;
|
|
362
|
+
/** How many redemptions it allows; omit for unlimited. */
|
|
363
|
+
totalUses?: number;
|
|
364
|
+
/** Web hook sent the disclosure; a `ValidationKey` then signs off each redemption. */
|
|
365
|
+
validationUrl?: string;
|
|
366
|
+
}
|
|
367
|
+
/** The control rows one founding writes together; see {@link ControlDatabase.insertFoundingRows}. */
|
|
368
|
+
export interface FoundingRows {
|
|
369
|
+
strand: {
|
|
370
|
+
id: string;
|
|
371
|
+
type: 'o' | 'c';
|
|
372
|
+
memberPrivateKey?: string;
|
|
373
|
+
};
|
|
374
|
+
/** This party's membership identity key for a closed strand; absent for an open strand, or when a stored key is adopted. */
|
|
375
|
+
partyKey?: string;
|
|
376
|
+
/** A formation invitation bound to this strand (its `StrandId` is `strand.id`). */
|
|
377
|
+
formationInvite?: FoundingInvite;
|
|
378
|
+
}
|
|
379
|
+
/** What an owner asks {@link ControlDatabase.insertCadreInvite} (or {@link ControlDatabase.signCadreInvite}) to sign. */
|
|
380
|
+
export interface CadreInviteFields {
|
|
381
|
+
/** The invitation public key (base64url ed25519), the row's primary key. */
|
|
382
|
+
key: string;
|
|
383
|
+
/** The device the invitation is for, or null for whoever redeems first. */
|
|
384
|
+
peerId?: string | null;
|
|
385
|
+
/** Epoch ms after which it cannot be redeemed; omit for never. */
|
|
386
|
+
expiresAtMs?: number | null;
|
|
387
|
+
/** How many redemptions it allows; omit for unlimited. */
|
|
388
|
+
totalUses?: number | null;
|
|
389
|
+
}
|
|
241
390
|
/** The control tables keyed by a strand id whose repeat insert a caller may treat as idempotent. */
|
|
242
|
-
type StrandIdTable = Extract<RevocableTable, 'Strand' | 'JoinedStrand'>;
|
|
391
|
+
type StrandIdTable = Extract<RevocableTable, 'Strand' | 'StrandPartyKey' | 'JoinedStrand'>;
|
|
243
392
|
/**
|
|
244
393
|
* Did this write to `table` fail because the strand id is already seated there?
|
|
245
394
|
*
|
|
246
395
|
* {@link CadreNode.publishStrand} uses this to tell "my own earlier publish already
|
|
247
396
|
* landed" (re-read, and no-op when the row matches) apart from every other rejection —
|
|
248
397
|
* an unauthorized signer, a retired stamp — which must keep surfacing. For `JoinedStrand`
|
|
249
|
-
* the same test tells "another machine of this party already published this join" apart
|
|
398
|
+
* the same test tells "another machine of this party already published this join" apart, and
|
|
399
|
+
* for `StrandPartyKey` it tells a founding that this party's key for the id is already seated.
|
|
250
400
|
*
|
|
251
401
|
* Matched by TEXT, not by type: the typed engine error does not survive the trip out of
|
|
252
402
|
* optimystic (same constraint the retry classifiers in `control-write-retry.ts` document).
|
|
@@ -256,6 +406,13 @@ type StrandIdTable = Extract<RevocableTable, 'Strand' | 'JoinedStrand'>;
|
|
|
256
406
|
* reword reddens there.
|
|
257
407
|
*/
|
|
258
408
|
export declare function isStrandIdConflict(error: unknown, table: StrandIdTable): boolean;
|
|
409
|
+
/**
|
|
410
|
+
* Did a seat of an issued row fail because a cohort member already holds a row under its key?
|
|
411
|
+
* {@link ControlDatabase.seatCadreInvite} / {@link ControlDatabase.seatOwnerGrant} reach this
|
|
412
|
+
* on a node whose store lags the cohort: its guard read missed the row, and the member that
|
|
413
|
+
* checked the commit refused the insert. Text-matched, failing closed, like {@link isStrandIdConflict}.
|
|
414
|
+
*/
|
|
415
|
+
export declare function isIssuedRowKeyConflict(error: unknown, table: IssuedTable): boolean;
|
|
259
416
|
/**
|
|
260
417
|
* Did {@link ControlDatabase.insertJoinRequest} fail because the party already holds a
|
|
261
418
|
* `JoinRequest` row for this invitation? The caller adopts the existing request instead.
|
|
@@ -286,35 +443,19 @@ export declare class PendingJoinChangedError extends Error {
|
|
|
286
443
|
readonly live: PendingJoin | null;
|
|
287
444
|
constructor(id: string, live: PendingJoin | null);
|
|
288
445
|
}
|
|
289
|
-
/**
|
|
290
|
-
* The one `CadreControl.Revocation` row that retires nothing, filed once by an owner
|
|
291
|
-
* ({@link ControlDatabase.openRevocationLedger}) so the table is never a never-written
|
|
292
|
-
* block. The schema's table comment says why that matters and why the row can never read
|
|
293
|
-
* as a retirement; `RowIsGone` pins this exact triple.
|
|
294
|
-
*/
|
|
295
|
-
export declare const REVOCATION_LEDGER_MARKER: {
|
|
296
|
-
readonly tableName: "Revocation";
|
|
297
|
-
readonly rowKey: "ledger";
|
|
298
|
-
readonly stampId: "opened";
|
|
299
|
-
};
|
|
300
446
|
/**
|
|
301
447
|
* 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.
|
|
448
|
+
* redemption reply, carries. `minterPeerId` is the machine minting the seed or answering the
|
|
449
|
+
* redemption; it contributes an entry only through its own `CadrePeer` row.
|
|
309
450
|
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
451
|
+
* A peer is `preferred` when it is the minter, the one machine certain to be reachable by the
|
|
452
|
+
* receiver at that moment, since it is delivering the seed or the reply, or an always-on
|
|
453
|
+
* machine (`Profile = 'storage'`), the ones that stay dialable. Preference is a dial hint and
|
|
454
|
+
* reads no `OwnerKey` row: owner keys are not machines, and a receiver derives trust from its
|
|
455
|
+
* own anchor, never from this list. A `Profile` is self-declared and not checked here, so a
|
|
456
|
+
* machine misdeclaring one costs the receiver a wasted dial at most.
|
|
316
457
|
*/
|
|
317
|
-
export declare function projectSeedPeers(rows: readonly CadrePeerRow[],
|
|
458
|
+
export declare function projectSeedPeers(rows: readonly CadrePeerRow[], minterPeerId: string): SeedPeer[];
|
|
318
459
|
/**
|
|
319
460
|
* Guarded tables a node may reap locally once their tombstone has committed — the
|
|
320
461
|
* tables whose `AuthorizedDelete` carries the REAP branch (see the constraint comment on
|
|
@@ -326,22 +467,14 @@ export declare function projectSeedPeers(rows: readonly CadrePeerRow[], ownerKey
|
|
|
326
467
|
* hold, so a reaped row is recoverable by re-forming. `JoinRequest` is reapable for the same
|
|
327
468
|
* reason: the inviting party and the user also hold its invitation. `OwnerKey` has no
|
|
328
469
|
* production removal path and `MinOneOwner` makes an automated owner-key reap a
|
|
329
|
-
* party-bricking hazard. `CadreInvite`
|
|
330
|
-
*
|
|
470
|
+
* party-bricking hazard. `CadreInvite` and `OwnerGrant` are never deleted at all (their
|
|
471
|
+
* tombstones are withdrawals over rows that stay), so there is nothing to reap.
|
|
331
472
|
*/
|
|
332
473
|
export declare const REAPABLE_TABLES: readonly ["CadrePeer", "DeviceToken", "ValidationKey", "JoinedStrand", "JoinRequest"];
|
|
333
474
|
export type ReapableTable = (typeof REAPABLE_TABLES)[number];
|
|
334
475
|
/** A `FormationInvite` row as {@link ControlDatabase.queryFormationInvite} reads it. */
|
|
335
|
-
export interface FormationInviteRow {
|
|
476
|
+
export interface FormationInviteRow extends FormationInviteFacts {
|
|
336
477
|
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
478
|
}
|
|
346
479
|
/**
|
|
347
480
|
* Notified after a membership write (a `CadreControl.CadrePeer` or `OwnerKey` row, see
|
|
@@ -361,8 +494,6 @@ export type MembershipChangeListener = (reason: string) => Promise<void>;
|
|
|
361
494
|
* the writer that files the row supplies it.
|
|
362
495
|
*/
|
|
363
496
|
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'>;
|
|
366
497
|
/**
|
|
367
498
|
* Notified after a guarded-table delete — and the `CadreControl.Revocation`
|
|
368
499
|
* tombstone it writes in the same transaction — has COMMITTED.
|
|
@@ -400,6 +531,8 @@ export interface ControlDatabaseConfig {
|
|
|
400
531
|
* Optional path to the control schema file.
|
|
401
532
|
* If not provided, uses the embedded schema for cross-platform compatibility.
|
|
402
533
|
* Only use this if you need to override the default schema (e.g., for testing).
|
|
534
|
+
* The file has the shape of `schemas/control.qsql`: one `declare schema CadreControl`
|
|
535
|
+
* followed by one plain `apply schema CadreControl` ({@link controlSchemaFileDeclaration}).
|
|
403
536
|
*/
|
|
404
537
|
schemaPath?: string;
|
|
405
538
|
/** Libp2p node for the control network (injected) */
|
|
@@ -414,6 +547,7 @@ export interface ControlDatabaseConfig {
|
|
|
414
547
|
export declare class ControlDatabase {
|
|
415
548
|
private db;
|
|
416
549
|
private collectionFactory;
|
|
550
|
+
private txnBridge;
|
|
417
551
|
private readonly config;
|
|
418
552
|
private initialized;
|
|
419
553
|
private membershipListener;
|
|
@@ -421,6 +555,18 @@ export declare class ControlDatabase {
|
|
|
421
555
|
private controlWriteAbandonedListener;
|
|
422
556
|
/** Tail of the local-write chain — see {@link withWriteLock}. */
|
|
423
557
|
private writeQueue;
|
|
558
|
+
/** Subscriptions handed out by {@link watchTables}. */
|
|
559
|
+
private readonly tableWatches;
|
|
560
|
+
/** The cached membership tables; see {@link readMembershipRows}. */
|
|
561
|
+
private readonly revocationView;
|
|
562
|
+
private readonly cadrePeerView;
|
|
563
|
+
private readonly ownerKeyView;
|
|
564
|
+
private readonly cadreInviteView;
|
|
565
|
+
private readonly cadreInviteUsageView;
|
|
566
|
+
private readonly ownerGrantView;
|
|
567
|
+
private readonly ownerGrantUsageView;
|
|
568
|
+
/** Each membership view by the qualified table it copies, for {@link watchMembershipViews}, {@link watchMembershipTables} and {@link invalidateMembershipViews}. */
|
|
569
|
+
private readonly membershipViews;
|
|
424
570
|
/**
|
|
425
571
|
* Locked write bodies currently running (see {@link runWriteBody}); what
|
|
426
572
|
* {@link readRowsOnce} consults to route an unlocked read around a write that has
|
|
@@ -447,7 +593,33 @@ export declare class ControlDatabase {
|
|
|
447
593
|
* Initialize the database - load schema and register plugins
|
|
448
594
|
*/
|
|
449
595
|
initialize(): Promise<void>;
|
|
596
|
+
/**
|
|
597
|
+
* Drop a membership view whenever its table changes: a commit made here, or a change this
|
|
598
|
+
* node's storage takes in from elsewhere. `onInvalidate` is the whole consumer, so the run
|
|
599
|
+
* does nothing; no backstop, since {@link MEMBERSHIP_VIEW_MAX_AGE_MS} bounds what the
|
|
600
|
+
* subscription cannot hear. {@link close} disposes it with the rest.
|
|
601
|
+
*/
|
|
602
|
+
private watchMembershipViews;
|
|
603
|
+
/**
|
|
604
|
+
* {@link watchTables} on the membership tables: the seven the membership views copy
|
|
605
|
+
* (`CadrePeer`, `OwnerKey`, `Revocation`, `CadreInvite`, `CadreInviteUsage`, `OwnerGrant`,
|
|
606
|
+
* `OwnerGrantUsage`), which are exactly what the membership gate and the owner-anchor sync
|
|
607
|
+
* read. `CadreNode.start` takes its gate watch here, so the gate hears every table its
|
|
608
|
+
* reads can depend on and no list of them lives outside this class.
|
|
609
|
+
*/
|
|
610
|
+
watchMembershipTables(onChange: () => Promise<void>, options: TableChangeWatchOptions): TableChangeWatch;
|
|
611
|
+
/**
|
|
612
|
+
* Empty every membership view, so the next read of each loads afresh and no load in
|
|
613
|
+
* flight installs. Besides {@link initialize} and {@link close}, `CadreNode` calls it when
|
|
614
|
+
* a sibling becomes reachable (its identify on a new connection): a machine that was offline
|
|
615
|
+
* while that sibling committed learns of the commit only when it next reads the block from
|
|
616
|
+
* storage, and dropping the views makes the pass that starts then read now rather than after
|
|
617
|
+
* `MEMBERSHIP_VIEW_MAX_AGE_MS`.
|
|
618
|
+
*/
|
|
619
|
+
invalidateMembershipViews(): void;
|
|
450
620
|
private loadSchema;
|
|
621
|
+
/** Apply the declared control schema, retrying transient cohort failures. */
|
|
622
|
+
private applyControlSchema;
|
|
451
623
|
/**
|
|
452
624
|
* One UNRETRIED drain of a control read to rows — every read in this class bottoms out
|
|
453
625
|
* here (almost all via {@link readRows}, which adds the transient-failure retry), and
|
|
@@ -578,6 +750,39 @@ export declare class ControlDatabase {
|
|
|
578
750
|
* under-lock callback seam is ever added, audit its read graph the same way.
|
|
579
751
|
*/
|
|
580
752
|
private readRows;
|
|
753
|
+
/**
|
|
754
|
+
* {@link readRows}, also saying whether the attempt that answered took the committed-read
|
|
755
|
+
* path — which a {@link MembershipView} load needs, since it may not keep such rows (see
|
|
756
|
+
* {@link membershipView}).
|
|
757
|
+
*/
|
|
758
|
+
private readRowsRouted;
|
|
759
|
+
/**
|
|
760
|
+
* A view over one membership table, loaded by its full scan `sql` through {@link readRows}'s
|
|
761
|
+
* funnel. Rows a committed read served are never kept: that read may initialize an untouched
|
|
762
|
+
* table provisionally, and the Optimystic plugin arms a table's replicated-change bridge only
|
|
763
|
+
* on its full initialization (`OptimysticTable.initializeForCommittedRead`), so no later change
|
|
764
|
+
* event would ever drop them.
|
|
765
|
+
*/
|
|
766
|
+
private membershipView;
|
|
767
|
+
/**
|
|
768
|
+
* A membership table's raw rows, from its {@link MembershipView}: `Revocation`, `CadrePeer`
|
|
769
|
+
* and `OwnerKey`, the tables every membership, owner and peer-record read starts from, and
|
|
770
|
+
* `CadreInvite`, `CadreInviteUsage`, `OwnerGrant` and `OwnerGrantUsage`, the chains every
|
|
771
|
+
* membership refresh verifies invitation-admitted machines and grant-seated owner keys through.
|
|
772
|
+
* Callers build fresh result objects from them and never mutate them.
|
|
773
|
+
*
|
|
774
|
+
* Reads the table directly instead when a caller under the write lock (`retry: false`) has an
|
|
775
|
+
* explicit transaction open: such a read sees the transaction's own uncommitted rows, which
|
|
776
|
+
* the view does not hold, and a rollback fires no change event, so keeping them would cache
|
|
777
|
+
* rows that never existed. No reader of these tables runs inside {@link inTransaction} today;
|
|
778
|
+
* this keeps one added later, or a spec's raw-handle transaction, correct.
|
|
779
|
+
*
|
|
780
|
+
* Reads that stay plain SQL on purpose: {@link queryStampId} (sees retired rows), the
|
|
781
|
+
* schema's CHECK constraints, which read inside the committing transaction, the redemption's
|
|
782
|
+
* one read of the held credential row ({@link queryCadreInvite},
|
|
783
|
+
* {@link queryOwnerGrant}), and the indexed usage counts.
|
|
784
|
+
*/
|
|
785
|
+
private readMembershipRows;
|
|
581
786
|
/**
|
|
582
787
|
* Query all strands from the control database
|
|
583
788
|
*/
|
|
@@ -589,10 +794,18 @@ export declare class ControlDatabase {
|
|
|
589
794
|
* validated invitee during provision-then-record formation.
|
|
590
795
|
*/
|
|
591
796
|
queryStrand(strandId: string): Promise<StrandRow | null>;
|
|
797
|
+
/**
|
|
798
|
+
* {@link queryStrand} plus the row's `StampId`, in one read: the formation responder
|
|
799
|
+
* resolves a bound invite's host strand with it and later records consent against that
|
|
800
|
+
* stamp ({@link recordFormationUsage}'s `strandStampId`) without reading the row again.
|
|
801
|
+
*/
|
|
802
|
+
queryStrandWithStamp(strandId: string): Promise<(StrandRow & {
|
|
803
|
+
StampId: string;
|
|
804
|
+
}) | null>;
|
|
592
805
|
/**
|
|
593
806
|
* Every strand this party joined from another party (`CadreControl.JoinedStrand`), shaped
|
|
594
807
|
* as a {@link StrandRow} so a reader can treat it like one of the party's own. A joiner
|
|
595
|
-
* never founds, so `FounderOwnerKey`
|
|
808
|
+
* never founds, so `FounderOwnerKey` and `FounderPeerId` are always null. Read raw, like {@link queryStrands}:
|
|
596
809
|
* no retired-stamp filter.
|
|
597
810
|
*/
|
|
598
811
|
queryJoinedStrands(): Promise<StrandRow[]>;
|
|
@@ -634,14 +847,54 @@ export declare class ControlDatabase {
|
|
|
634
847
|
*
|
|
635
848
|
* Deliberately RAW (no retired-stamp filter, unlike {@link getOwnerKeys}): the genesis
|
|
636
849
|
* insert is refused by the schema whenever any physical row exists, so this must see one.
|
|
850
|
+
* Served from the `OwnerKey` membership view ({@link readMembershipRows}), which holds every
|
|
851
|
+
* physical row, so a genesis caller's pre-check and {@link ensureOwnerKey}'s re-check share
|
|
852
|
+
* one storage read.
|
|
637
853
|
*/
|
|
638
854
|
hasOwnerKey(): Promise<boolean>;
|
|
639
855
|
/**
|
|
640
856
|
* Idempotent genesis: insert `key` as the founding owner key only when
|
|
641
857
|
* the party has none yet. Returns true if it inserted, false if an owner
|
|
642
858
|
* key already existed (so a repeat `--owner` start is a no-op).
|
|
643
|
-
|
|
644
|
-
|
|
859
|
+
*
|
|
860
|
+
* Before the insert, gives every control table its empty collection
|
|
861
|
+
* ({@link commitEmptyControlCollections}). A collection committed while a machine is alone
|
|
862
|
+
* forks if another machine creates the same collection independently; before genesis the
|
|
863
|
+
* party has no owner, so no other machine can have been admitted and none can hold any of
|
|
864
|
+
* its collections. The order makes the step repeatable: a crash between it and the insert
|
|
865
|
+
* leaves no `OwnerKey` row, so the next start runs genesis again and commits only the
|
|
866
|
+
* collections still missing, while a founder restart finds the row and commits nothing, so
|
|
867
|
+
* it can never write an empty second history beside a table another machine has since
|
|
868
|
+
* written. A failure propagates with nothing inserted.
|
|
869
|
+
*
|
|
870
|
+
* @param label - the user's name for the key ({@link insertOwnerKey}).
|
|
871
|
+
*/
|
|
872
|
+
ensureOwnerKey(key: string, label?: string): Promise<boolean>;
|
|
873
|
+
/**
|
|
874
|
+
* Commit an empty collection for every `CadreControl` table, index tree and unique-enforcement
|
|
875
|
+
* tree that has none, so a read of a table nobody has written is answered from this machine's
|
|
876
|
+
* storage instead of asking the block's cohort to prove the collection absent — a round trip
|
|
877
|
+
* per read on a party of several machines, and an error or a wait while they are offline.
|
|
878
|
+
* Joining machines get these collections the ordinary way, by reading them from the cohort.
|
|
879
|
+
*
|
|
880
|
+
* Under the write lock: the helper commits whatever is staged in those collections outside any
|
|
881
|
+
* SQL transaction, which is sound only while nothing else writes.
|
|
882
|
+
*
|
|
883
|
+
* Parties founded before this step, and tables a later control schema version adds, have no
|
|
884
|
+
* such collections, and nothing creates them later: the start-time schema apply runs on every
|
|
885
|
+
* machine, possibly alone, where committing one would be exactly the fork genesis avoids.
|
|
886
|
+
*
|
|
887
|
+
* NOTE: one `sync()` per collection, in sequence: 32 collections for today's schema (17 tables,
|
|
888
|
+
* 3 indexes, 12 `unique` columns). Measured 2026-10-20 on a lone `CadreNode` over the network
|
|
889
|
+
* transactor (started, then `ensureOwnerKey` alone, 3 runs each): 64–105 ms in memory and
|
|
890
|
+
* 750–820 ms on `FileRawStorage`, about 25 ms per collection on disk, roughly 1/75 of the edge
|
|
891
|
+
* node's 60 s `DEFAULT_OWNER_GENESIS_TIMEOUT_MS`. Under the whole cadre-core suite's load (151
|
|
892
|
+
* geneses) it took a median of 159 ms and at most 4.6 s. If genesis latency starts to matter
|
|
893
|
+
* (phone flash under launch contention runs 50–90 ms per storage operation, see the NOTE in
|
|
894
|
+
* `initialize`), ask Optimystic for an API that commits them as one batch, the one strand
|
|
895
|
+
* founding would use too; do not build a private batching path.
|
|
896
|
+
*/
|
|
897
|
+
private commitEmptyControlCollections;
|
|
645
898
|
/**
|
|
646
899
|
* Collect every owner key (`CadreControl.OwnerKey.Key`) as a set.
|
|
647
900
|
*
|
|
@@ -666,8 +919,9 @@ export declare class ControlDatabase {
|
|
|
666
919
|
*/
|
|
667
920
|
getOwnerKeys(retry?: boolean): Promise<Set<string>>;
|
|
668
921
|
/**
|
|
669
|
-
* Every live `OwnerKey` row with its persisted proof ({@link OwnerKeyRow}) — what
|
|
670
|
-
* trusted-owner anchor sync derives from (`CadreNode.syncOwnerAnchor`)
|
|
922
|
+
* Every live `OwnerKey` row with its label and persisted proof ({@link OwnerKeyRow}) — what
|
|
923
|
+
* the trusted-owner anchor sync derives from (`CadreNode.syncOwnerAnchor`) and the owner-key
|
|
924
|
+
* listing shows (`CadreNode.listOwnerKeys`). Rows whose
|
|
671
925
|
* `StampId` is retired in `CadreControl.Revocation` are excluded here, as
|
|
672
926
|
* {@link queryCadrePeers} excludes them and for the reason on {@link getOwnerKeys}, which
|
|
673
927
|
* reads through this. A row with no stamp (the schema permits it; no writer produces one)
|
|
@@ -679,9 +933,8 @@ export declare class ControlDatabase {
|
|
|
679
933
|
*
|
|
680
934
|
* @param retired - the retired `OwnerKey` stamps, when the caller has already read the
|
|
681
935
|
* tombstones (`CadreNode.syncOwnerAnchor` reads {@link queryRevocations} for their
|
|
682
|
-
* signers); omitted, they are read here.
|
|
683
|
-
*
|
|
684
|
-
* membership refresh at one such read for the anchor.
|
|
936
|
+
* signers); omitted, they are read here. Both come from the same `Revocation` view
|
|
937
|
+
* ({@link readMembershipRows}), so passing the set saves only the filtering.
|
|
685
938
|
*/
|
|
686
939
|
queryOwnerKeyRows(retry?: boolean, retired?: ReadonlySet<string>): Promise<OwnerKeyRow[]>;
|
|
687
940
|
/**
|
|
@@ -696,12 +949,8 @@ export declare class ControlDatabase {
|
|
|
696
949
|
* {@link queryRevokedStamps} rather than inlining the SQL, so tests can interpose on
|
|
697
950
|
* that seam to model the replicated live-row-plus-tombstone merge state.
|
|
698
951
|
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
* while `Revocation` has never been written every call consults the cohort about it, and
|
|
702
|
-
* the ledger marker ({@link openRevocationLedger}) is what makes it a held block. Folding
|
|
703
|
-
* the exclusion into one statement would not help (it reads the same block), and caching
|
|
704
|
-
* the set would hide a tombstone arriving by replication.
|
|
952
|
+
* Both tables come from their membership views ({@link readMembershipRows}), so a call after
|
|
953
|
+
* the first costs no storage read until one of the two tables changes.
|
|
705
954
|
*
|
|
706
955
|
* NOTE: this filter is HAND-MIRRORED by the gate suites' fake control databases
|
|
707
956
|
* (`test/membership-gate-helpers.ts` and `test/cadre-node-authorized-surface.spec.ts`),
|
|
@@ -723,15 +972,15 @@ export declare class ControlDatabase {
|
|
|
723
972
|
queryCadrePeers(retry?: boolean): Promise<CadrePeerRow[]>;
|
|
724
973
|
/**
|
|
725
974
|
* The cadre's peers as a seed or a redemption reply carries them: every live `CadrePeer`
|
|
726
|
-
* row with its addresses,
|
|
727
|
-
*
|
|
975
|
+
* row with its addresses, the minter and every always-on machine marked preferred
|
|
976
|
+
* ({@link projectSeedPeers}).
|
|
728
977
|
*
|
|
729
978
|
* Read through {@link queryCadrePeers}, never a raw `CadrePeer` select, so a removed
|
|
730
979
|
* member's addresses are never packed into a seed or handed to a redeeming device as a
|
|
731
980
|
* dial hint: a revoked peer is off the addressable surface everywhere, and this is one of
|
|
732
981
|
* its exits.
|
|
733
982
|
*/
|
|
734
|
-
querySeedPeers(): Promise<SeedPeer[]>;
|
|
983
|
+
querySeedPeers(minterPeerId: string): Promise<SeedPeer[]>;
|
|
735
984
|
/**
|
|
736
985
|
* Every approver public key currently enrolled in `CadreControl.ValidationKey` — the
|
|
737
986
|
* set a `ValidationUrl` redemption's approval signature must be signed by (see
|
|
@@ -762,17 +1011,18 @@ export declare class ControlDatabase {
|
|
|
762
1011
|
* guard. A read retrying there would sleep its backoff HOLDING the write lock
|
|
763
1012
|
* (`retryControlWrite`'s contract is that backoff sleeps happen with no lock held), and
|
|
764
1013
|
* the write funnel already re-runs the body's reads when it re-runs the body. The
|
|
765
|
-
* unlocked callers — the public stamp readers
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
1014
|
+
* unlocked callers — the public stamp readers and {@link reapRevokedRow}, whose stamp
|
|
1015
|
+
* read runs BEFORE its lock is taken — keep the default retried path. Explicit per call
|
|
1016
|
+
* rather than an ambient flag: unlocked reads run concurrently with a locked body, so
|
|
1017
|
+
* `this`-state cannot tell the two apart. The same flag threads through
|
|
1018
|
+
* {@link findCadrePeerRow} for the two writers that read the whole row inside their body.
|
|
769
1019
|
*/
|
|
770
1020
|
private queryStampId;
|
|
771
1021
|
/**
|
|
772
1022
|
* `CadrePeer` stamp nonce — read by {@link SeedBootstrapService.removePeer} as its
|
|
773
1023
|
* row-present gate before it delegates to {@link deleteCadrePeer}. The in-class
|
|
774
|
-
* `CadrePeer` writers
|
|
775
|
-
*
|
|
1024
|
+
* `CadrePeer` writers go straight to {@link queryStampId} ({@link insertCadrePeer}) or read
|
|
1025
|
+
* the whole row ({@link reauthorizeCadrePeer}, through {@link findCadrePeerRow}).
|
|
776
1026
|
*/
|
|
777
1027
|
queryCadrePeerStampId(peerId: string): Promise<string | null>;
|
|
778
1028
|
/**
|
|
@@ -809,33 +1059,34 @@ export declare class ControlDatabase {
|
|
|
809
1059
|
* ({@link queryCadrePeers}, {@link queryPeerRecord},
|
|
810
1060
|
* {@link CadreNode.resolveDeviceToken}) drop any row whose stamp appears here.
|
|
811
1061
|
*
|
|
812
|
-
*
|
|
813
|
-
*
|
|
814
|
-
*
|
|
815
|
-
*
|
|
816
|
-
*
|
|
817
|
-
*
|
|
818
|
-
*
|
|
819
|
-
* row count only grows (append-only) but stays small while removals are rare; revisit if
|
|
1062
|
+
* Served from the `Revocation` membership view ({@link readMembershipRows}), dropped by
|
|
1063
|
+
* every change to the table, local or replicated into this node's storage, so a tombstone
|
|
1064
|
+
* arriving by replication is seen on the next call. The cost that matters is the storage
|
|
1065
|
+
* layer's, paid by a cold view's load: while `Revocation` has never been written, this node
|
|
1066
|
+
* does not hold its block, and Optimystic consults a block's cohort on every read of a block
|
|
1067
|
+
* it does not hold, each a round trip to every other member on a multi-machine party. The row
|
|
1068
|
+
* count only grows (append-only) but stays small while removals are rare; revisit if
|
|
820
1069
|
* removals become routine and the scan itself shows up in a profile.
|
|
821
1070
|
*
|
|
822
1071
|
* An isolated node that has never received the `Revocation` block reads it as holding no
|
|
823
|
-
* revocations instead of throwing — see {@link
|
|
1072
|
+
* revocations instead of throwing — see {@link readRevocationRowsOrNone}.
|
|
824
1073
|
*
|
|
825
1074
|
* `retry: false` is forwarded by {@link queryCadrePeers} for the one caller that reads
|
|
826
1075
|
* under the write lock — see that method's note.
|
|
827
1076
|
*/
|
|
828
1077
|
queryRevokedStamps(tableName: RevocableTable, retry?: boolean): Promise<Set<string>>;
|
|
829
1078
|
/**
|
|
830
|
-
* The `Revocation`
|
|
831
|
-
* fails `cohort-unreachable` ({@link isCohortUnreachableRead}): this node does not hold
|
|
1079
|
+
* The `Revocation` rows behind {@link queryRevokedStamps}, answering NO ROWS when the view's
|
|
1080
|
+
* load fails `cohort-unreachable` ({@link isCohortUnreachableRead}): this node does not hold
|
|
832
1081
|
* the block and could ask no other cohort member about it. Every other failure rethrows.
|
|
833
1082
|
* The retry in {@link readRows} runs first, so a node whose connectivity returns within
|
|
834
|
-
* the read budget still gets the real answer.
|
|
1083
|
+
* the read budget still gets the real answer. The empty answer is never installed in the
|
|
1084
|
+
* view (a failed load keeps nothing), so the next call asks again.
|
|
835
1085
|
*
|
|
836
1086
|
* Without this, a machine cut off from every other machine in its party before it ever
|
|
837
|
-
* received the block —
|
|
838
|
-
*
|
|
1087
|
+
* received the block — a joining machine gets it only by reading it from the cohort, and
|
|
1088
|
+
* on a party founded before genesis created every control collection it exists nowhere
|
|
1089
|
+
* until the first tombstone or the marker ({@link REVOCATION_LEDGER_MARKER}) — cannot
|
|
839
1090
|
* answer ANY membership, peer-record or device-token lookup, although it holds those
|
|
840
1091
|
* rows locally.
|
|
841
1092
|
*
|
|
@@ -854,12 +1105,12 @@ export declare class ControlDatabase {
|
|
|
854
1105
|
*
|
|
855
1106
|
* NOTE: an isolated node pays the whole read retry (up to
|
|
856
1107
|
* `CONTROL_READ_RETRY_BUDGET_MS`, plus the slow attempt that exhausted it) before this
|
|
857
|
-
* answers,
|
|
858
|
-
* node's lookups are seen exceeding the
|
|
859
|
-
* (`ADMISSION_DECISION_TIMEOUT_MS`), skip the retry for this
|
|
860
|
-
* fails `cohort-unreachable`.
|
|
1108
|
+
* answers, on every call since the answer is not kept, and {@link queryPeerRecord} may then
|
|
1109
|
+
* load the `CadrePeer` view on top. If an isolated node's lookups are seen exceeding the
|
|
1110
|
+
* admission gate's 2 s deadline (`ADMISSION_DECISION_TIMEOUT_MS`), skip the retry for this
|
|
1111
|
+
* read when the first attempt fails `cohort-unreachable`.
|
|
861
1112
|
*/
|
|
862
|
-
private
|
|
1113
|
+
private readRevocationRowsOrNone;
|
|
863
1114
|
/**
|
|
864
1115
|
* Every locally-held `CadreControl.Revocation` tombstone — identity triple, its
|
|
865
1116
|
* `ReissuedAt` counter and the stored signer pair. Consumed by the cohort-growth re-issue
|
|
@@ -868,17 +1119,18 @@ export declare class ControlDatabase {
|
|
|
868
1119
|
* `where`, so the composite-primary-key point-lookup hazard (see the statement comment in
|
|
869
1120
|
* {@link reissueRevocations}) does not arise. Unlocked, like every other read.
|
|
870
1121
|
*
|
|
871
|
-
* Skips the ledger marker ({@link REVOCATION_LEDGER_MARKER})
|
|
872
|
-
* neither sweep may reap or re-sign it, and skipping it keeps
|
|
873
|
-
* a {@link RevocableTable}. Filtered on `TableName` alone, in
|
|
874
|
-
* admits no other row under `'Revocation'`.
|
|
1122
|
+
* Skips the ledger marker ({@link REVOCATION_LEDGER_MARKER}) an older party may hold: it
|
|
1123
|
+
* retires nothing, so neither sweep may reap or re-sign it, and skipping it keeps
|
|
1124
|
+
* `RevocationRow.tableName` a {@link RevocableTable}. Filtered on `TableName` alone, in
|
|
1125
|
+
* TypeScript: `RowIsGone` admits no other row under `'Revocation'`.
|
|
875
1126
|
*
|
|
876
1127
|
* `retry: false` from a caller that reads under the write lock, for the reason on
|
|
877
1128
|
* {@link queryCadrePeers}.
|
|
878
1129
|
*/
|
|
879
1130
|
queryRevocations(retry?: boolean): Promise<RevocationRow[]>;
|
|
880
1131
|
/**
|
|
881
|
-
* Read a single peer's address record (the full `CadrePeer` row) by PeerId
|
|
1132
|
+
* Read a single peer's address record (the full `CadrePeer` row) by PeerId, found in the
|
|
1133
|
+
* `CadrePeer` membership view ({@link readMembershipRows}).
|
|
882
1134
|
*
|
|
883
1135
|
* Returns null when no row exists, or when the row's `StampId` is retired in
|
|
884
1136
|
* `CadreControl.Revocation` — a revoked peer reads as absent, so the resolver
|
|
@@ -894,10 +1146,22 @@ export declare class ControlDatabase {
|
|
|
894
1146
|
* The split `addrs` re-join to the exact stored `Multiaddr` (split-on-`,` is
|
|
895
1147
|
* the inverse of join-on-`,`), so the resolver re-verifies over the same bytes
|
|
896
1148
|
* the publisher signed.
|
|
1149
|
+
*
|
|
1150
|
+
* `retry: false` from the one caller that reads under the write lock,
|
|
1151
|
+
* {@link updateSelfPeerRecord}'s body, for the reason on {@link queryCadrePeers}.
|
|
1152
|
+
*/
|
|
1153
|
+
queryPeerRecord(peerId: string, retry?: boolean): Promise<PeerAddressRecord | null>;
|
|
1154
|
+
/**
|
|
1155
|
+
* One readable `CadrePeer` row by PeerId from the membership view, or null when no row exists
|
|
1156
|
+
* or its stamp is retired in `Revocation` — the single-row read behind {@link queryPeerRecord}
|
|
1157
|
+
* and the two writers that decide over the row inside their locked body
|
|
1158
|
+
* ({@link updateSelfPeerRecord}, {@link reauthorizeCadrePeer}). Same retired-stamp rule as
|
|
1159
|
+
* {@link queryCadrePeers}, and the same `retry` contract.
|
|
897
1160
|
*/
|
|
898
|
-
|
|
1161
|
+
private findCadrePeerRow;
|
|
899
1162
|
/**
|
|
900
|
-
* Apply a peer's own self-signed address-record update to
|
|
1163
|
+
* Apply a peer's own self-signed address-record update to its existing row: read the row
|
|
1164
|
+
* under the write lock, have `sign` build the record over it, and write that record.
|
|
901
1165
|
*
|
|
902
1166
|
* Authorization is carried entirely by the record: the `Sig` column (verified
|
|
903
1167
|
* by the `AuthorizedUpdate` self-branch against the stored `PublicKey`) plus
|
|
@@ -906,6 +1170,21 @@ export declare class ControlDatabase {
|
|
|
906
1170
|
* exists. `PublicKey` is intentionally not in the SET list (it is immutable on
|
|
907
1171
|
* self-update and the constraint enforces `new.PublicKey = old.PublicKey`).
|
|
908
1172
|
*
|
|
1173
|
+
* The read, the decision and the signature all run INSIDE the locked body, which is what
|
|
1174
|
+
* lets it opt into {@link LOST_UPDATE_RETRY_POLICY}: the row has a second writer, the owner's
|
|
1175
|
+
* re-touch ({@link reauthorizeCadrePeer}), and the two fire together while a cadre starts.
|
|
1176
|
+
* When this write loses that race the commit is refused as a lost update and re-run, and the
|
|
1177
|
+
* re-run reads the row the rival left — including its bumped `UpdatedAt`, which a record
|
|
1178
|
+
* signed before the race could no longer exceed — and signs past it. A record signed outside
|
|
1179
|
+
* the body would be re-presented unchanged and refused again, this time by the schema's
|
|
1180
|
+
* monotonic rule, which is correctly never retried. The row's read inside is `retry: false`
|
|
1181
|
+
* (see {@link queryStampId}'s NOTE).
|
|
1182
|
+
*
|
|
1183
|
+
* Returns the record actually written, so the caller's bookkeeping describes the row rather
|
|
1184
|
+
* than the first record it signed; null, and nothing written, when no readable row exists:
|
|
1185
|
+
* the row is absent, or its stamp is retired in `Revocation` (a revoked node reads its own row
|
|
1186
|
+
* as absent every heartbeat, and must not refresh it).
|
|
1187
|
+
*
|
|
909
1188
|
* Deliberately NOT wrapped in {@link mutateCadrePeer}, the one `CadrePeer` mutator that
|
|
910
1189
|
* is not: it only ever touches THIS node's own row (sole caller
|
|
911
1190
|
* `CadreNode.publishSelfRecord`), and `CadreNode.listAuthorizedMembers` filters self out
|
|
@@ -918,8 +1197,11 @@ export declare class ControlDatabase {
|
|
|
918
1197
|
* row, and a self insert happens once at startup, so the wasted refresh is a single read
|
|
919
1198
|
* — cheaper than a conditional carve-out inside the shared writer. Only the repeating
|
|
920
1199
|
* path is worth exempting.
|
|
1200
|
+
*
|
|
1201
|
+
* @param peerId - the row to refresh: this node's own.
|
|
1202
|
+
* @param sign - builds the signed record over the row as read; called once per attempt.
|
|
921
1203
|
*/
|
|
922
|
-
updateSelfPeerRecord(
|
|
1204
|
+
updateSelfPeerRecord(peerId: string, sign: SelfRecordSigner): Promise<PeerAddressRecord | null>;
|
|
923
1205
|
/**
|
|
924
1206
|
* Read a single peer's device push token (the full `DeviceToken` row) by PeerId.
|
|
925
1207
|
*
|
|
@@ -948,19 +1230,34 @@ export declare class ControlDatabase {
|
|
|
948
1230
|
updateSelfDeviceToken(record: DeviceTokenRecord): Promise<void>;
|
|
949
1231
|
/**
|
|
950
1232
|
* Insert the initial owner key (bootstrap - no existing owners required)
|
|
1233
|
+
*
|
|
1234
|
+
* @param label - the user's name for the key, at most {@link OWNER_KEY_MAX_LABEL}
|
|
1235
|
+
* characters once trimmed; blank is stored as none.
|
|
951
1236
|
*/
|
|
952
|
-
insertOwnerKey(key: string): Promise<void>;
|
|
1237
|
+
insertOwnerKey(key: string, label?: string): Promise<void>;
|
|
953
1238
|
/**
|
|
954
1239
|
* Owner-signed INSERT of a further `OwnerKey` row, notifying the membership listener once
|
|
955
1240
|
* it has committed. The signed-add branch of `OwnerKey.Authorized`: the signer must be an
|
|
956
1241
|
* 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
|
|
958
|
-
* can later judge the add against its own anchor (`verifyOwnerKeyVoucher`).
|
|
1242
|
+
* (Key, Label, StampId), and the stored `VouchOwner`/`VouchSig` are that same pair, so a
|
|
1243
|
+
* reader can later judge the add against its own anchor (`verifyOwnerKeyVoucher`).
|
|
959
1244
|
*
|
|
960
1245
|
* Through {@link mutateCadrePeer}, the membership hub. The liveness read, the stamp and the
|
|
961
1246
|
* signature all sit inside the locked body: a {@link lockedWithRetry} re-run starts over a
|
|
962
1247
|
* rolled-back attempt with a fresh stamp, as {@link redeemCadreInvite} does.
|
|
963
1248
|
*
|
|
1249
|
+
* @param label - the user's name for the key, as {@link insertOwnerKey}'s.
|
|
1250
|
+
* @returns `true` when this call seated the row, `false` when `key` is already a live owner
|
|
1251
|
+
* here (nothing written, whatever its label).
|
|
1252
|
+
* @throws when this node holds `key`'s row under a retired stamp
|
|
1253
|
+
* ({@link liveOwnerKeyStamp}), or when the schema refuses the signer.
|
|
1254
|
+
*/
|
|
1255
|
+
insertOwnerKeyVouched(key: string, ownerKey: string, signMessage: ControlSign, label?: string | null): Promise<boolean>;
|
|
1256
|
+
/**
|
|
1257
|
+
* The stamp of `key`'s live `OwnerKey` row here, or null when this node holds none: the
|
|
1258
|
+
* insert-if-absent read of {@link insertOwnerKeyVouched} and {@link redeemOwnerGrant}. Inside
|
|
1259
|
+
* the locked body (both reads `retry: false`; see queryStampId's NOTE).
|
|
1260
|
+
*
|
|
964
1261
|
* NOTE: re-adding a removed owner from a machine that still holds its retired physical row
|
|
965
1262
|
* is refused here by name: `OwnerKey` is not reaped, so a machine that received the
|
|
966
1263
|
* tombstone without the delete (see {@link getOwnerKeys}) keeps the row, and an insert there
|
|
@@ -968,12 +1265,15 @@ export declare class ControlDatabase {
|
|
|
968
1265
|
* machine the delete reached; if re-adding removed owners becomes routine, the fix is an
|
|
969
1266
|
* `OwnerKey` reap branch, gated so it can never take the table below `MinOneOwner`.
|
|
970
1267
|
*
|
|
971
|
-
* @
|
|
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.
|
|
1268
|
+
* @throws when this node holds `key`'s row under a retired stamp (above).
|
|
975
1269
|
*/
|
|
976
|
-
|
|
1270
|
+
private liveOwnerKeyStamp;
|
|
1271
|
+
/**
|
|
1272
|
+
* The signed-add statement of {@link insertOwnerKeyVouched} and {@link revouchOwnerKey}, the
|
|
1273
|
+
* stored voucher being the signing pair. Bare `exec`: callers hold the write lock, which is
|
|
1274
|
+
* NOT re-entrant.
|
|
1275
|
+
*/
|
|
1276
|
+
private execOwnerKeyVouchedInsert;
|
|
977
1277
|
/**
|
|
978
1278
|
* Owner-signed removal of one `OwnerKey` row plus the `Revocation` tombstone retiring its
|
|
979
1279
|
* stamp, notifying the membership listener once it has committed. The remove branch of
|
|
@@ -991,56 +1291,58 @@ export declare class ControlDatabase {
|
|
|
991
1291
|
* (nothing written, no tombstone).
|
|
992
1292
|
* @throws when `key` is `ownerKey`, or is the only live owner.
|
|
993
1293
|
*/
|
|
994
|
-
deleteOwnerKey(key: string, ownerKey: string, signMessage:
|
|
1294
|
+
deleteOwnerKey(key: string, ownerKey: string, signMessage: ControlSign): Promise<boolean>;
|
|
995
1295
|
/**
|
|
996
1296
|
* Re-vouch a live `OwnerKey` row under this owner's signature: in ONE transaction, the
|
|
997
1297
|
* 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`
|
|
999
|
-
* body), as {@link rewritePendingJoin} re-seats a request. `OwnerKey` has `NoUpdate`, so
|
|
1298
|
+
* of the same key and label under a fresh stamp, signed by `ownerKey`
|
|
1299
|
+
* ({@link insertOwnerKeyVouched}'s body), as {@link rewritePendingJoin} re-seats a request. `OwnerKey` has `NoUpdate`, so
|
|
1000
1300
|
* this is the only way to rebind a row's voucher; the anchor derivation lets the re-add
|
|
1001
1301
|
* under a fresh stamp beat the older tombstone (`deriveOwnerAnchor`). For
|
|
1002
|
-
*
|
|
1003
|
-
*
|
|
1302
|
+
* `CadreNode.removeOwner`: the owners a key being removed had added keep their anchor place
|
|
1303
|
+
* everywhere once their row stands on a remaining owner's signature.
|
|
1004
1304
|
*
|
|
1005
|
-
* Through {@link mutateCadrePeer}, the membership hub. The
|
|
1305
|
+
* Through {@link mutateCadrePeer}, the membership hub. The row read runs inside the locked
|
|
1006
1306
|
* body; a row whose stamp is already retired (held without its delete) is reported as absent
|
|
1007
1307
|
* rather than tombstoned twice.
|
|
1008
1308
|
*
|
|
1009
1309
|
* @returns `true` when the row was re-vouched, `false` when `key` is not a live owner here.
|
|
1010
1310
|
* @throws when `key` is `ownerKey` (an owner cannot sign its own row's removal).
|
|
1011
1311
|
*/
|
|
1012
|
-
revouchOwnerKey(key: string, ownerKey: string, signMessage:
|
|
1312
|
+
revouchOwnerKey(key: string, ownerKey: string, signMessage: ControlSign): Promise<boolean>;
|
|
1013
1313
|
/**
|
|
1014
|
-
* Remove one machine completely, in ONE transaction: its `CadrePeer` row
|
|
1015
|
-
*
|
|
1016
|
-
*
|
|
1017
|
-
*
|
|
1018
|
-
*
|
|
1019
|
-
*
|
|
1020
|
-
*
|
|
1021
|
-
*
|
|
1314
|
+
* Remove one machine completely, in ONE transaction: its `CadrePeer` row and its
|
|
1315
|
+
* `DeviceToken` row when it has one, each with the `Revocation` tombstone retiring its stamp
|
|
1316
|
+
* ({@link deleteGuardedRow}'s statement pair, composed bare as {@link deleteStrandAndPartyKey}
|
|
1317
|
+
* composes it), plus a withdrawal tombstone for every `CadreInvite` in `target.inviteKeys`
|
|
1318
|
+
* that is still live here ({@link withdrawCadreInvite}'s body). One transaction so no reader
|
|
1319
|
+
* sees the machine half removed: a row gone but its push token resolvable, or an invitation
|
|
1320
|
+
* it could redeem again still open. Owner keys are never touched: they are the person's, and
|
|
1321
|
+
* removing one is {@link deleteOwnerKey}.
|
|
1022
1322
|
*
|
|
1023
1323
|
* Through {@link mutateCadrePeer}, the membership hub. Every stamp is read inside the locked
|
|
1024
1324
|
* body, and a row whose stamp is already retired (held without its delete) is skipped rather
|
|
1025
|
-
* than tombstoned twice.
|
|
1026
|
-
* owner-key arm. Nothing to remove writes nothing and returns all false.
|
|
1325
|
+
* than tombstoned twice. Nothing to remove writes nothing and returns all false.
|
|
1027
1326
|
*
|
|
1028
|
-
*
|
|
1029
|
-
*
|
|
1030
|
-
*
|
|
1327
|
+
* Opted into {@link LOST_UPDATE_RETRY_POLICY} for the reason on {@link deleteCadrePeer}: every
|
|
1328
|
+
* stamp and signature is minted inside the body, and the invitation keys come from the
|
|
1329
|
+
* caller's plan, which a self-publish does not touch (a withdrawal of an invitation withdrawn
|
|
1330
|
+
* since collides on `Revocation`'s primary key — a constraint failure, never retried, as
|
|
1331
|
+
* before). A lost update after a sibling tree of the transaction already synced arrives as a
|
|
1332
|
+
* partial commit and stays declined.
|
|
1031
1333
|
*
|
|
1032
|
-
*
|
|
1334
|
+
* The caller decides which invitations the machine could use, from its plan
|
|
1335
|
+
* (`CadreNode.planMachineRemoval`).
|
|
1033
1336
|
*/
|
|
1034
1337
|
removeMachine(target: {
|
|
1035
1338
|
peerId: string;
|
|
1036
|
-
ownerKey: string | null;
|
|
1037
1339
|
inviteKeys: readonly string[];
|
|
1038
|
-
}, ownerKey: string, signMessage:
|
|
1340
|
+
}, ownerKey: string, signMessage: ControlSign): Promise<MachineRemovalResult>;
|
|
1039
1341
|
/**
|
|
1040
1342
|
* The guarded removals {@link removeMachine} runs, signed over each row's current stamp:
|
|
1041
|
-
* the `CadrePeer` row
|
|
1042
|
-
*
|
|
1043
|
-
*
|
|
1343
|
+
* the `CadrePeer` row and the `DeviceToken` row, each when present. Inside the locked body
|
|
1344
|
+
* (every read `retry: false`); one `Revocation` scan serves the retired-stamp check for both
|
|
1345
|
+
* tables.
|
|
1044
1346
|
*/
|
|
1045
1347
|
private signMachineRemovals;
|
|
1046
1348
|
/**
|
|
@@ -1062,7 +1364,29 @@ export declare class ControlDatabase {
|
|
|
1062
1364
|
* with the owner's private key, returning a base64url signature
|
|
1063
1365
|
* @param memberPrivateKey - Optional private key for membership in closed strands
|
|
1064
1366
|
*/
|
|
1065
|
-
insertStrand(strandId: string, type: 'o' | 'c', ownerKey: string, signMessage:
|
|
1367
|
+
insertStrand(strandId: string, type: 'o' | 'c', ownerKey: string, signMessage: ControlSign, memberPrivateKey?: string): Promise<void>;
|
|
1368
|
+
/** The stamped, owner-signed `Strand` insert that {@link insertStrand} and {@link insertFoundingRows} run. */
|
|
1369
|
+
private signStrandInsert;
|
|
1370
|
+
/**
|
|
1371
|
+
* Insert one founding's control rows — the `Strand` row, a closed strand's `StrandPartyKey`
|
|
1372
|
+
* row, and a formation invitation bound to the strand — in ONE transaction: one commit, and
|
|
1373
|
+
* never a strand row without the key or invitation written beside it.
|
|
1374
|
+
*
|
|
1375
|
+
* Each row is stamped and signed exactly as its single-row writer ({@link insertStrand},
|
|
1376
|
+
* {@link insertStrandPartyKey}, {@link insertFormationInvite}) does it, and all of that — the
|
|
1377
|
+
* invitation's {@link canonicalDatetime} read included — happens BEFORE the write lock is
|
|
1378
|
+
* taken: signing may prompt the user, and the lock held across a prompt would stall every
|
|
1379
|
+
* other control write. A {@link lockedWithRetry} re-run re-presents the same signed rows.
|
|
1380
|
+
*
|
|
1381
|
+
* No row's `AuthorizedInsert` reads another of these rows (`StrandPartyKey` and
|
|
1382
|
+
* `FormationInvite` check only `OwnerKey` and `Revocation`), so their order is free; the
|
|
1383
|
+
* `Strand` row goes first, so a founding repeated over a stored row is refused on `Strand.Id`.
|
|
1384
|
+
*
|
|
1385
|
+
* A uniqueness rejection rolls the whole transaction back. The caller resolves it and writes
|
|
1386
|
+
* again: a `Strand.Id` conflict is another founder's row, a `StrandPartyKey.Id` conflict a key
|
|
1387
|
+
* stored with no strand row ({@link isStrandIdConflict}; see `CadreNode.publishStrand`).
|
|
1388
|
+
*/
|
|
1389
|
+
insertFoundingRows(rows: FoundingRows, ownerKey: string, signMessage: ControlSign): Promise<void>;
|
|
1066
1390
|
/**
|
|
1067
1391
|
* Delete a strand from the control database using an owner signature.
|
|
1068
1392
|
*
|
|
@@ -1090,7 +1414,7 @@ export declare class ControlDatabase {
|
|
|
1090
1414
|
* A no-op (no throw, no tombstone) when the row does not exist — `false` then, `true`
|
|
1091
1415
|
* when a row was actually removed.
|
|
1092
1416
|
*/
|
|
1093
|
-
deleteStrand(strandId: string, ownerKey: string, signMessage:
|
|
1417
|
+
deleteStrand(strandId: string, ownerKey: string, signMessage: ControlSign): Promise<boolean>;
|
|
1094
1418
|
/**
|
|
1095
1419
|
* The locked body of {@link deleteStrand}: the `Strand` delete + tombstone, plus — when
|
|
1096
1420
|
* one exists — the strand's `StrandPartyKey` delete + tombstone, all in ONE
|
|
@@ -1120,7 +1444,9 @@ export declare class ControlDatabase {
|
|
|
1120
1444
|
* @param signMessage - Function that ed25519-signs the raw message bytes (no pre-hash)
|
|
1121
1445
|
* with the owner's private key, returning a base64url signature.
|
|
1122
1446
|
*/
|
|
1123
|
-
insertStrandPartyKey(strandId: string, privateKey: string, ownerKey: string, signMessage:
|
|
1447
|
+
insertStrandPartyKey(strandId: string, privateKey: string, ownerKey: string, signMessage: ControlSign): Promise<void>;
|
|
1448
|
+
/** The stamped, owner-signed `StrandPartyKey` insert that {@link insertStrandPartyKey} and {@link insertFoundingRows} run. */
|
|
1449
|
+
private signStrandPartyKeyInsert;
|
|
1124
1450
|
/**
|
|
1125
1451
|
* Owner-signed removal of one `StrandPartyKey` row — the party's own membership
|
|
1126
1452
|
* identity for that strand, stored nowhere else, so this is as destructive as
|
|
@@ -1133,7 +1459,7 @@ export declare class ControlDatabase {
|
|
|
1133
1459
|
* `Revocation` tombstone in the same transaction. A no-op (no throw, no tombstone)
|
|
1134
1460
|
* when the row does not exist — `false` then, `true` when a row was actually removed.
|
|
1135
1461
|
*/
|
|
1136
|
-
deleteStrandPartyKey(strandId: string, ownerKey: string, signMessage:
|
|
1462
|
+
deleteStrandPartyKey(strandId: string, ownerKey: string, signMessage: ControlSign): Promise<boolean>;
|
|
1137
1463
|
/**
|
|
1138
1464
|
* Record, party-wide, a strand this party joined from another party (`JoinedStrand` row),
|
|
1139
1465
|
* using an owner signature.
|
|
@@ -1146,14 +1472,14 @@ export declare class ControlDatabase {
|
|
|
1146
1472
|
* Fails with a `JoinedStrand.Id` uniqueness violation when the id is already recorded;
|
|
1147
1473
|
* `isStrandIdConflict(error, 'JoinedStrand')` identifies that case.
|
|
1148
1474
|
*/
|
|
1149
|
-
insertJoinedStrand(row: Pick<StrandRow, 'Id' | 'Type' | 'MemberPrivateKey'>, ownerKey: string, signMessage:
|
|
1475
|
+
insertJoinedStrand(row: Pick<StrandRow, 'Id' | 'Type' | 'MemberPrivateKey'>, ownerKey: string, signMessage: ControlSign): Promise<void>;
|
|
1150
1476
|
/**
|
|
1151
1477
|
* Owner-signed removal of one `JoinedStrand` row. Mirrors {@link deleteStrandPartyKey}:
|
|
1152
1478
|
* `'remove'`-tagged digest over (Id, StampId), `Revocation` tombstone in the same
|
|
1153
1479
|
* transaction. A no-op (no throw, no tombstone) when the row does not exist — `false`
|
|
1154
1480
|
* then, `true` when a row was actually removed.
|
|
1155
1481
|
*/
|
|
1156
|
-
deleteJoinedStrand(strandId: string, ownerKey: string, signMessage:
|
|
1482
|
+
deleteJoinedStrand(strandId: string, ownerKey: string, signMessage: ControlSign): Promise<boolean>;
|
|
1157
1483
|
/**
|
|
1158
1484
|
* Record, party-wide, a join this party asked for: a `JoinRequest` row under a fresh stamp,
|
|
1159
1485
|
* owner-signed over every column. Returns the pending join as written.
|
|
@@ -1161,7 +1487,7 @@ export declare class ControlDatabase {
|
|
|
1161
1487
|
* Fails with a `JoinRequest.Id` uniqueness violation when the party already holds a request
|
|
1162
1488
|
* for this invitation; {@link isPendingJoinConflict} identifies that case.
|
|
1163
1489
|
*/
|
|
1164
|
-
insertJoinRequest(request: JoinRequestFields, ownerKey: string, signMessage:
|
|
1490
|
+
insertJoinRequest(request: JoinRequestFields, ownerKey: string, signMessage: ControlSign): Promise<PendingJoin>;
|
|
1165
1491
|
/**
|
|
1166
1492
|
* Record the outcome of the pending join `expected`: its owner-signed `JoinSuccess` /
|
|
1167
1493
|
* `JoinFailure` row, keyed by the request's stamp. A join recorded over a failure deletes the
|
|
@@ -1175,7 +1501,7 @@ export declare class ControlDatabase {
|
|
|
1175
1501
|
* transaction instead (its outcome row collides on the primary key, or its tombstone retires
|
|
1176
1502
|
* the stamp), and the caller re-reads either way.
|
|
1177
1503
|
*/
|
|
1178
|
-
recordJoinOutcome(expected: PendingJoin, outcome: JoinOutcome, ownerKey: string, signMessage:
|
|
1504
|
+
recordJoinOutcome(expected: PendingJoin, outcome: JoinOutcome, ownerKey: string, signMessage: ControlSign): Promise<PendingJoin>;
|
|
1179
1505
|
/**
|
|
1180
1506
|
* Re-write the pending join `next` (request and outcome) under a fresh stamp, retiring the
|
|
1181
1507
|
* live request incarnation `expectedStampId`: how a finished request is asked for again, and
|
|
@@ -1190,14 +1516,14 @@ export declare class ControlDatabase {
|
|
|
1190
1516
|
* that committed but reported failure throws {@link PendingJoinChangedError} whose live join
|
|
1191
1517
|
* carries the returned stamp, so a caller that re-reads finds its write.
|
|
1192
1518
|
*/
|
|
1193
|
-
rewritePendingJoin(expectedStampId: string, next: PendingJoinFields, ownerKey: string, signMessage:
|
|
1519
|
+
rewritePendingJoin(expectedStampId: string, next: PendingJoinFields, ownerKey: string, signMessage: ControlSign): Promise<PendingJoin>;
|
|
1194
1520
|
/**
|
|
1195
1521
|
* Owner-signed removal of one pending join, as {@link deleteJoinedStrand} removes its row:
|
|
1196
1522
|
* `'remove'`-tagged digest over (Id, StampId), `Revocation` tombstone in the same transaction,
|
|
1197
1523
|
* and the request's outcome row with it. A no-op (no throw, no tombstone) when there is no
|
|
1198
1524
|
* request — `false` then, `true` when one was actually removed.
|
|
1199
1525
|
*/
|
|
1200
|
-
deletePendingJoin(id: string, ownerKey: string, signMessage:
|
|
1526
|
+
deletePendingJoin(id: string, ownerKey: string, signMessage: ControlSign): Promise<boolean>;
|
|
1201
1527
|
/**
|
|
1202
1528
|
* Mint a fresh stamp for `request` and the owner signature `JoinRequest.AuthorizedInsert`
|
|
1203
1529
|
* verifies, returning the row as it will be written and its insert.
|
|
@@ -1224,7 +1550,7 @@ export declare class ControlDatabase {
|
|
|
1224
1550
|
* @param signMessage - Function that ed25519-signs the raw message bytes (no pre-hash)
|
|
1225
1551
|
* with the owner's private key, returning a base64url signature
|
|
1226
1552
|
*/
|
|
1227
|
-
insertValidationKey(key: string, ownerKey: string, signMessage:
|
|
1553
|
+
insertValidationKey(key: string, ownerKey: string, signMessage: ControlSign): Promise<void>;
|
|
1228
1554
|
/**
|
|
1229
1555
|
* Delete a validation key from the control database using an owner signature.
|
|
1230
1556
|
*
|
|
@@ -1241,7 +1567,7 @@ export declare class ControlDatabase {
|
|
|
1241
1567
|
* A no-op (no throw, no tombstone) when the row does not exist — `false` then, `true`
|
|
1242
1568
|
* when a row was actually removed.
|
|
1243
1569
|
*/
|
|
1244
|
-
deleteValidationKey(key: string, ownerKey: string, signMessage:
|
|
1570
|
+
deleteValidationKey(key: string, ownerKey: string, signMessage: ControlSign): Promise<boolean>;
|
|
1245
1571
|
/**
|
|
1246
1572
|
* Owner-vouched INSERT of one `CadrePeer` membership row, notifying the membership
|
|
1247
1573
|
* listener once it has committed.
|
|
@@ -1286,34 +1612,46 @@ export declare class ControlDatabase {
|
|
|
1286
1612
|
updatedAt: number;
|
|
1287
1613
|
sig: string | null;
|
|
1288
1614
|
profile: NodeProfile | null;
|
|
1289
|
-
}, ownerKey: string, signMessage:
|
|
1290
|
-
/**
|
|
1291
|
-
* Owner "re-touch" of an existing `CadrePeer` membership row:
|
|
1292
|
-
*
|
|
1293
|
-
*
|
|
1294
|
-
* the row is re-emitted as a fresh, broadcasting
|
|
1295
|
-
*
|
|
1296
|
-
*
|
|
1297
|
-
*
|
|
1298
|
-
*
|
|
1299
|
-
*
|
|
1300
|
-
*
|
|
1301
|
-
*
|
|
1302
|
-
*
|
|
1303
|
-
*
|
|
1304
|
-
* `
|
|
1305
|
-
*
|
|
1306
|
-
*
|
|
1307
|
-
*
|
|
1308
|
-
*
|
|
1309
|
-
*
|
|
1310
|
-
*
|
|
1311
|
-
*
|
|
1312
|
-
*
|
|
1313
|
-
*
|
|
1314
|
-
*
|
|
1315
|
-
*
|
|
1316
|
-
*
|
|
1615
|
+
}, ownerKey: string, signMessage: ControlSign): Promise<boolean>;
|
|
1616
|
+
/**
|
|
1617
|
+
* Owner "re-touch" of an existing `CadrePeer` membership row: rewrite `VouchOwner`/`VouchSig`
|
|
1618
|
+
* (and, re-broadcasting, bump `UpdatedAt`) under the owner branch of
|
|
1619
|
+
* `CadrePeer.AuthorizedUpdate` — a signature over the SAME `'vouch'`-tagged digest
|
|
1620
|
+
* {@link insertCadrePeer} builds — so the row is re-emitted as a fresh, broadcasting
|
|
1621
|
+
* transaction, or stands on a different owner's voucher.
|
|
1622
|
+
*
|
|
1623
|
+
* Re-broadcasting is the write-while-alone re-replication primitive: a membership row that
|
|
1624
|
+
* committed local-only (its block's cluster ≤1 at insert) is pushed to the cohort once it
|
|
1625
|
+
* grows, by re-issuing this monotonic bump. It is an UPDATE (not the original INSERT) because
|
|
1626
|
+
* the row already exists locally; a re-INSERT would hit the `PeerId` PK. Only the freshness
|
|
1627
|
+
* stamp and voucher change — `PublicKey` / `Multiaddr` / `Sig` / `Profile` are left intact
|
|
1628
|
+
* (the owner branch requires them kept: they are the machine's own claim) — so it is safe
|
|
1629
|
+
* over a row whose peer has not self-published (`Sig` null), and this method itself skips a
|
|
1630
|
+
* row that already carries a self-`Sig` (that row is the owning peer's to refresh, and
|
|
1631
|
+
* bumping `UpdatedAt` without re-signing would invalidate its self-signature).
|
|
1632
|
+
*
|
|
1633
|
+
* The two modes ({@link CadrePeerRetouchMode}) differ in what they write: `'rebroadcast'`
|
|
1634
|
+
* bumps `UpdatedAt` to `max(now, current + 1)` from the row read inside the body and writes
|
|
1635
|
+
* only while the row's `Sig` is null; `'revouch'` leaves `UpdatedAt` out of the `set` list and
|
|
1636
|
+
* writes whether or not the machine has self-published (the owner branch has no monotonic
|
|
1637
|
+
* rule, so the column simply keeps its value).
|
|
1638
|
+
*
|
|
1639
|
+
* The row read (stamp, `Sig`, `UpdatedAt`), the mode's decision and the signature over the
|
|
1640
|
+
* row's CURRENT `StampId` (unchanged by the re-touch) all run INSIDE the locked body, which is
|
|
1641
|
+
* what lets both modes opt into {@link LOST_UPDATE_RETRY_POLICY}: the row's other writer is
|
|
1642
|
+
* the machine's own publish ({@link updateSelfPeerRecord}), and the drain's re-broadcast fires
|
|
1643
|
+
* at the moment that publish does. A re-run after losing to it reads the self-signed row and
|
|
1644
|
+
* reports `'self-published'` instead of writing the owner branch over it — which would rewrite
|
|
1645
|
+
* `UpdatedAt` under a `Sig` signed over the old value and make the record unverifiable to
|
|
1646
|
+
* resolvers until the machine's next heartbeat. The decision has to be the body's for that;
|
|
1647
|
+
* a caller's pre-check outside the lock is only a fast path (see below).
|
|
1648
|
+
*
|
|
1649
|
+
* An absent row — none, or its stamp retired, or a stampless row no writer produces — is
|
|
1650
|
+
* `'absent'`. Checked once outside the lock too, as `'self-published'` is: the common case
|
|
1651
|
+
* writes nothing, takes no lock and does NOT notify, so an absent target stays quiet
|
|
1652
|
+
* ({@link SeedBootstrapService.removePeer} keeps the same gate for the same reason). The
|
|
1653
|
+
* in-body re-check covers the race; a no-write outcome reached there notifies spuriously,
|
|
1654
|
+
* which only re-reads the member set.
|
|
1317
1655
|
*
|
|
1318
1656
|
* Notifies like the insert/remove paths even though this is "only" a re-touch: it rewrites
|
|
1319
1657
|
* VouchOwner/VouchSig, which the authorized-membership predicate judges on, so it CAN
|
|
@@ -1327,21 +1665,22 @@ export declare class ControlDatabase {
|
|
|
1327
1665
|
*
|
|
1328
1666
|
* NOTE: this rebinds VouchOwner to the CALLING owner's key, and the authorized-membership
|
|
1329
1667
|
* predicate (`CadreNode.listAuthorizedMembers`) now judges rows by that column against each
|
|
1330
|
-
* reader's node-local anchor.
|
|
1331
|
-
*
|
|
1332
|
-
*
|
|
1333
|
-
*
|
|
1334
|
-
* anchor the original owner but not this one would
|
|
1335
|
-
* must re-vouch deliberately (or preserve the existing
|
|
1336
|
-
* inherit this rebinding.
|
|
1668
|
+
* reader's node-local anchor. The two callers rebind on purpose: the write-while-alone drain
|
|
1669
|
+
* re-touches solely rows this node itself authored (`pendingPeerWrites`), so the voucher is
|
|
1670
|
+
* rewritten to the key that already signed it, and `CadreNode.removeOwner` moves rows off a
|
|
1671
|
+
* key it is about to remove. A new caller that re-touches a row a DIFFERENT owner vouched
|
|
1672
|
+
* flips the voucher silently: readers that anchor the original owner but not this one would
|
|
1673
|
+
* drop a legitimate member. It must re-vouch deliberately (or preserve the existing
|
|
1674
|
+
* VouchOwner/VouchSig) rather than inherit this rebinding.
|
|
1337
1675
|
*
|
|
1338
1676
|
* @param peerId - the membership row to re-touch.
|
|
1339
|
-
* @param
|
|
1677
|
+
* @param mode - re-broadcast or re-vouch ({@link CadrePeerRetouchMode}).
|
|
1340
1678
|
* @param ownerKey - public key of the re-vouching owner, rewritten into `VouchOwner`.
|
|
1341
|
-
* @param signMessage - ed25519-signs the raw message bytes with that owner's private key
|
|
1342
|
-
*
|
|
1679
|
+
* @param signMessage - ed25519-signs the raw message bytes with that owner's private key;
|
|
1680
|
+
* called inside the lock, once per attempt (the NOTE on {@link ControlSign}).
|
|
1681
|
+
* @returns what was found and done ({@link CadrePeerRetouchOutcome}).
|
|
1343
1682
|
*/
|
|
1344
|
-
reauthorizeCadrePeer(peerId: string,
|
|
1683
|
+
reauthorizeCadrePeer(peerId: string, mode: CadrePeerRetouchMode, ownerKey: string, signMessage: ControlSign): Promise<CadrePeerRetouchOutcome>;
|
|
1345
1684
|
/**
|
|
1346
1685
|
* Owner-signed removal of one `CadrePeer` membership row, notifying the membership
|
|
1347
1686
|
* listener once it has committed.
|
|
@@ -1361,8 +1700,14 @@ export declare class ControlDatabase {
|
|
|
1361
1700
|
*
|
|
1362
1701
|
* A no-op (no throw, no tombstone) when the row does not exist — `false` then, `true`
|
|
1363
1702
|
* when a row was actually removed.
|
|
1703
|
+
*
|
|
1704
|
+
* Opted into {@link LOST_UPDATE_RETRY_POLICY}: the row's machine may publish its own record
|
|
1705
|
+
* at the moment of the removal (its first publish lands about a second after it starts), and
|
|
1706
|
+
* the removal that loses is refused as a lost update. {@link deleteGuardedRow} reads the stamp
|
|
1707
|
+
* and signs inside the body, and a self-publish never changes the stamp the `'remove'` digest
|
|
1708
|
+
* binds, so the re-run reads the published row and removes it.
|
|
1364
1709
|
*/
|
|
1365
|
-
deleteCadrePeer(peerId: string, ownerKey: string, signMessage:
|
|
1710
|
+
deleteCadrePeer(peerId: string, ownerKey: string, signMessage: ControlSign): Promise<boolean>;
|
|
1366
1711
|
/**
|
|
1367
1712
|
* Owner-signed removal of one peer's `DeviceToken` row (logout / token invalidation).
|
|
1368
1713
|
*
|
|
@@ -1376,7 +1721,7 @@ export declare class ControlDatabase {
|
|
|
1376
1721
|
* A no-op (no throw, no tombstone) when the row does not exist — `false` then, `true`
|
|
1377
1722
|
* when a row was actually removed.
|
|
1378
1723
|
*/
|
|
1379
|
-
deleteDeviceToken(peerId: string, ownerKey: string, signMessage:
|
|
1724
|
+
deleteDeviceToken(peerId: string, ownerKey: string, signMessage: ControlSign): Promise<boolean>;
|
|
1380
1725
|
/**
|
|
1381
1726
|
* Owner-signed delete of one guarded row plus the `Revocation` tombstone retiring its
|
|
1382
1727
|
* stamp, in ONE transaction. The single body behind EVERY guarded delete —
|
|
@@ -1421,10 +1766,9 @@ export declare class ControlDatabase {
|
|
|
1421
1766
|
private execGuardedRemoval;
|
|
1422
1767
|
/**
|
|
1423
1768
|
* File the owner-signed `Revocation` tombstone retiring one row incarnation — the second
|
|
1424
|
-
* statement of every guarded removal ({@link execGuardedRemoval}) and the whole of
|
|
1425
|
-
*
|
|
1426
|
-
*
|
|
1427
|
-
* lock and, where one is needed, the transaction.
|
|
1769
|
+
* statement of every guarded removal ({@link execGuardedRemoval}) and the whole of an
|
|
1770
|
+
* issued row's withdrawal ({@link withdrawIssuedRow}), whose row stays. The caller
|
|
1771
|
+
* supplies the lock and, where one is needed, the transaction.
|
|
1428
1772
|
*
|
|
1429
1773
|
* `ReissuedAt` is named explicitly at 0 (the only value `FreshTombstone` accepts) rather
|
|
1430
1774
|
* than leaning on the column default — the seat-at-zero rule is load-bearing for
|
|
@@ -1462,9 +1806,10 @@ export declare class ControlDatabase {
|
|
|
1462
1806
|
* statement, and without the clause the reap would delete the owner's brand-new row.
|
|
1463
1807
|
* With it the statement matches nothing and the reap is a silent no-op (the schema
|
|
1464
1808
|
* branch would also refuse that delete, but as a constraint error thrown into a
|
|
1465
|
-
* background sweep — matched-nothing is the better failure mode).
|
|
1466
|
-
* {@link reauthorizeCadrePeer},
|
|
1467
|
-
* harmless — nothing was deleted, and the next sweep
|
|
1809
|
+
* background sweep — matched-nothing is the better failure mode). A race lost AFTER the
|
|
1810
|
+
* guard still returns `true` here (unlike {@link reauthorizeCadrePeer}, which reads inside
|
|
1811
|
+
* its lock and reports what it found); harmless — nothing was deleted, and the next sweep
|
|
1812
|
+
* re-reads.
|
|
1468
1813
|
*
|
|
1469
1814
|
* Writes NOTHING to `Revocation`: `RevocationRecorded` is satisfied by the same
|
|
1470
1815
|
* committed tombstone that authorizes the reap, so a reap files no second tombstone.
|
|
@@ -1491,7 +1836,7 @@ export declare class ControlDatabase {
|
|
|
1491
1836
|
* present clause are the shape {@link redeemInvitation}'s consent-branch insert ships).
|
|
1492
1837
|
*
|
|
1493
1838
|
* Driven automatically by {@link reapRevokedRows}, which the periodic control-cohort
|
|
1494
|
-
* reconcile pass runs while
|
|
1839
|
+
* reconcile pass runs while a sibling is reachable (`CadreNode.hasReachableControlSibling`).
|
|
1495
1840
|
*/
|
|
1496
1841
|
reapRevokedRow(table: ReapableTable, rowKey: string, stampId: string): Promise<boolean>;
|
|
1497
1842
|
/**
|
|
@@ -1502,8 +1847,8 @@ export declare class ControlDatabase {
|
|
|
1502
1847
|
* carry the delete (replaying a delete of an already-absent row is a no-op), so a node
|
|
1503
1848
|
* that converged on a removal while holding the removed row keeps that row as inert
|
|
1504
1849
|
* garbage until something walks the tombstones and drops it. `CadreNode`'s periodic
|
|
1505
|
-
* control-cohort reconcile pass is that something, gated on
|
|
1506
|
-
*
|
|
1850
|
+
* control-cohort reconcile pass is that something, gated on a sibling being reachable
|
|
1851
|
+
* (a reap is a write, and a write committed alone is local-only).
|
|
1507
1852
|
*
|
|
1508
1853
|
* Enumerated UNLOCKED, deleted per-row LOCKED: {@link reapRevokedRow} takes the write
|
|
1509
1854
|
* lock itself (via {@link mutateCadrePeer} / {@link execWrite}) and the lock is not
|
|
@@ -1536,12 +1881,11 @@ export declare class ControlDatabase {
|
|
|
1536
1881
|
*
|
|
1537
1882
|
* NOTE: cost is O(tombstones), not O(live rows) — one {@link queryRevocations} scan plus
|
|
1538
1883
|
* one {@link queryStampId} point lookup per tombstone, and the empty early return makes
|
|
1539
|
-
* the common case (a party that has never revoked anyone
|
|
1540
|
-
*
|
|
1541
|
-
*
|
|
1542
|
-
* ({@link openRevocationLedger}), filed by the same connected pass, makes it a held block.
|
|
1884
|
+
* the common case (a party that has never revoked anyone) a single scan per pass, served
|
|
1885
|
+
* from the `Revocation` membership view
|
|
1886
|
+
* ({@link readMembershipRows}) unless the table changed since the last read.
|
|
1543
1887
|
* But `Revocation` is append-only and unbounded, so this is O(all tombstones ever) point
|
|
1544
|
-
* lookups on every reconcile
|
|
1888
|
+
* lookups on every connected reconcile pass. Fine while revocations stay rare for a cadre-sized
|
|
1545
1889
|
* party; if the table ever grows, persist a node-local high-water mark of what has
|
|
1546
1890
|
* already been reaped instead of re-scanning everything (the same bound the sweep in
|
|
1547
1891
|
* `CadreNode.drainPendingRevocations` wants).
|
|
@@ -1575,45 +1919,7 @@ export declare class ControlDatabase {
|
|
|
1575
1919
|
*
|
|
1576
1920
|
* Returns how many UPDATE statements ran (`rows.length` on success).
|
|
1577
1921
|
*/
|
|
1578
|
-
reissueRevocations(rows: readonly RevocationRow[], reissuedAt: number, ownerKey: string, signMessage:
|
|
1579
|
-
/**
|
|
1580
|
-
* Owner-signed filing of the singleton `Revocation` ledger marker
|
|
1581
|
-
* ({@link REVOCATION_LEDGER_MARKER}), the one row in that table that retires nothing.
|
|
1582
|
-
*
|
|
1583
|
-
* Why it exists: while `Revocation` has never been written, this node does not hold its
|
|
1584
|
-
* block, and Optimystic consults a block's cohort on EVERY read of a block it does not
|
|
1585
|
-
* hold — and every membership lookup, and every guarded insert's `NotRevoked` check,
|
|
1586
|
-
* reads this table. Once any row exists the block is held and re-checked at most once per
|
|
1587
|
-
* read-repair window, like every other populated table. The schema's table comment says
|
|
1588
|
-
* why the row can never read as a retirement.
|
|
1589
|
-
*
|
|
1590
|
-
* Insert-if-absent, shaped like {@link reissueRevocations}: the signature is minted
|
|
1591
|
-
* OUTSIDE the locked body so a retried attempt re-presents the same one, and the guard
|
|
1592
|
-
* runs INSIDE it so it sees a concurrent local writer's committed row. The guard scans
|
|
1593
|
-
* the `'Revocation'` rows and compares the stamp in TypeScript rather than seeking the
|
|
1594
|
-
* full composite primary key, which is served as a point lookup that can miss an existing
|
|
1595
|
-
* row on a networked database (tickets/backlog/debt-composite-pk-point-lookup-unreliable-untracked).
|
|
1596
|
-
*
|
|
1597
|
-
* Two owners filing at once: the loser either sees the marker in its guard or is refused
|
|
1598
|
-
* on the primary key, and both answer `'already-open'`.
|
|
1599
|
-
*
|
|
1600
|
-
* The caller decides WHEN. A marker committed while the node is alone is local-only and
|
|
1601
|
-
* can fork the collection, so {@link CadreNode} files it only while connected; this
|
|
1602
|
-
* method does not look at connectivity.
|
|
1603
|
-
*
|
|
1604
|
-
* @param ownerKey - owner public key for the write context (`Revocation.Authorized`).
|
|
1605
|
-
* @param signMessage - ed25519-signs the raw message bytes (no pre-hash) with that owner's
|
|
1606
|
-
* private key, returning a base64url signature.
|
|
1607
|
-
* @returns `'opened'` when this call filed the marker, `'already-open'` when it was
|
|
1608
|
-
* already there.
|
|
1609
|
-
*/
|
|
1610
|
-
openRevocationLedger(ownerKey: string, signMessage: (message: Uint8Array) => string): Promise<RevocationLedgerOpenResult>;
|
|
1611
|
-
/**
|
|
1612
|
-
* Whether the ledger marker is present locally: a scan of the `'Revocation'` rows with the
|
|
1613
|
-
* stamp compared in TypeScript (see {@link openRevocationLedger} for why not a seek).
|
|
1614
|
-
* Called only inside the locked write body, so never retried (see queryStampId's NOTE).
|
|
1615
|
-
*/
|
|
1616
|
-
private revocationLedgerFiled;
|
|
1922
|
+
reissueRevocations(rows: readonly RevocationRow[], reissuedAt: number, ownerKey: string, signMessage: ControlSign): Promise<number>;
|
|
1617
1923
|
/**
|
|
1618
1924
|
* Run `body` between `beginTransaction` and `commit`, rolling back on failure.
|
|
1619
1925
|
*
|
|
@@ -1645,8 +1951,21 @@ export declare class ControlDatabase {
|
|
|
1645
1951
|
* {@link CadreNode}, which wires this in `start()` and clears it on teardown. A
|
|
1646
1952
|
* second call replaces the first rather than fanning out, so a stale node can
|
|
1647
1953
|
* never keep receiving notifications for a database it no longer owns.
|
|
1954
|
+
*
|
|
1955
|
+
* Only this database's own writes notify here, awaited inside the write so the gate is
|
|
1956
|
+
* current before the write returns. A membership change replicated in from another
|
|
1957
|
+
* machine reaches the gate through the subscription `CadreNode.start` takes on the
|
|
1958
|
+
* membership tables ({@link watchTables}), a coalesced run up to 50 ms after the change
|
|
1959
|
+
* lands in storage; one storage never hears of waits for the reconcile pass's backstop.
|
|
1648
1960
|
*/
|
|
1649
1961
|
setMembershipChangeListener(listener: MembershipChangeListener | null): void;
|
|
1962
|
+
/**
|
|
1963
|
+
* Subscribe `onChange` to changes in control tables (`'CadreControl.Strand'`, …): commits
|
|
1964
|
+
* made here and changes replicated into this node's storage alike, on the single-flight
|
|
1965
|
+
* schedule `table-change-watch.ts` describes. {@link close} disposes every subscription
|
|
1966
|
+
* still live before it drains the write queue.
|
|
1967
|
+
*/
|
|
1968
|
+
watchTables(tables: readonly string[], onChange: () => Promise<void>, options: TableChangeWatchOptions): TableChangeWatch;
|
|
1650
1969
|
/**
|
|
1651
1970
|
* Wire (or clear, with null) the single listener notified after every committed
|
|
1652
1971
|
* guarded-table delete. Same ownership contract as
|
|
@@ -1672,8 +1991,9 @@ export declare class ControlDatabase {
|
|
|
1672
1991
|
* Covers every write that can change who is a member or an owner: EVERY `CadrePeer`
|
|
1673
1992
|
* writer, with one documented exception ({@link updateSelfPeerRecord}, which cannot
|
|
1674
1993
|
* change the snapshot), the `OwnerKey` writers ({@link insertOwnerKeyVouched},
|
|
1675
|
-
* {@link deleteOwnerKey})
|
|
1676
|
-
*
|
|
1994
|
+
* {@link deleteOwnerKey}), the invitation redemption ({@link redeemCadreInvite}), which
|
|
1995
|
+
* seats both, and the owner-grant redemption ({@link redeemOwnerGrant}). That is what
|
|
1996
|
+
* makes the party-membership snapshot refresh automatic
|
|
1677
1997
|
* rather than a caller obligation. The name predates the `OwnerKey` writers. A
|
|
1678
1998
|
* writer necessarily holds the target node's ControlDatabase (the `SeedBootstrapService`'s
|
|
1679
1999
|
* event callbacks are NOT a viable seam: the temp service `CadreNode.applySeed`
|
|
@@ -1700,8 +2020,11 @@ export declare class ControlDatabase {
|
|
|
1700
2020
|
* exhaustion.
|
|
1701
2021
|
*
|
|
1702
2022
|
* @param reason - Label for the log line only (e.g. `'peer-insert'`).
|
|
2023
|
+
* @param policy - The retry policy, default unless the body has earned a wider one: the three
|
|
2024
|
+
* `CadrePeer` writers whose bodies re-read the row and decide inside pass
|
|
2025
|
+
* `LOST_UPDATE_RETRY_POLICY` (its `NOTE:` names them and the rule).
|
|
1703
2026
|
*/
|
|
1704
|
-
mutateCadrePeer<T>(reason: string, body: () => Promise<T
|
|
2027
|
+
mutateCadrePeer<T>(reason: string, body: () => Promise<T>, policy?: ControlWriteRetryOptions): Promise<T>;
|
|
1705
2028
|
/**
|
|
1706
2029
|
* Run one local write under the database-wide write lock, serializing it against
|
|
1707
2030
|
* every other local writer on this ControlDatabase.
|
|
@@ -1714,8 +2037,8 @@ export declare class ControlDatabase {
|
|
|
1714
2037
|
* class: every public write method of this class runs its statement(s) under it, and
|
|
1715
2038
|
* `SeedBootstrapService` reaches it through {@link execWrite} rather than touching the
|
|
1716
2039
|
* `Database` directly, so it is covered too. The race is
|
|
1717
|
-
* real, not theoretical: the background self-record publish
|
|
1718
|
-
*
|
|
2040
|
+
* real, not theoretical: the background self-record publish when a sibling becomes
|
|
2041
|
+
* reachable (`CadreNode.drainPendingControlReplication` → `registerSelf`) collided with
|
|
1719
2042
|
* a foreground `authorizePeer` — both `CadrePeer` inserts — and tripped the boundary
|
|
1720
2043
|
* assert.
|
|
1721
2044
|
*
|
|
@@ -1739,6 +2062,11 @@ export declare class ControlDatabase {
|
|
|
1739
2062
|
* re-runnable means a retry re-runs the body's reads too, which is what makes it safe.
|
|
1740
2063
|
* Signatures and stamp ids minted OUTSIDE the body are deliberately NOT re-minted per
|
|
1741
2064
|
* attempt: a retry re-presents the exact signed message the first attempt presented.
|
|
2065
|
+
* A body opted into `LOST_UPDATE_RETRY_POLICY` is the exception, and must mint INSIDE:
|
|
2066
|
+
* its re-run is a re-decision over a row another writer changed, not a re-presentation of
|
|
2067
|
+
* the same write, so its read of the row, its decision and its signature all run in the
|
|
2068
|
+
* body ({@link updateSelfPeerRecord}, {@link reauthorizeCadrePeer}, {@link deleteGuardedRow}
|
|
2069
|
+
* under {@link deleteCadrePeer}, {@link removeMachine}).
|
|
1742
2070
|
*/
|
|
1743
2071
|
withWriteLock<T>(fn: () => Promise<T>): Promise<T>;
|
|
1744
2072
|
/**
|
|
@@ -1760,13 +2088,18 @@ export declare class ControlDatabase {
|
|
|
1760
2088
|
* backoff never stalls the other local writers queued behind it. Safe because every
|
|
1761
2089
|
* locked body is atomic and re-runnable (the contract on {@link withWriteLock}).
|
|
1762
2090
|
*
|
|
1763
|
-
* Also carries {@link
|
|
2091
|
+
* Also carries {@link applyControlSchema}'s distributed DDL, the one caller that runs BEFORE
|
|
1764
2092
|
* `initialized` is set — so it reaches this method directly rather than through
|
|
1765
2093
|
* {@link execWrite}, whose {@link ensureInitialized} would reject it. The write queue is
|
|
1766
2094
|
* necessarily empty at that point, so the lock is uncontended there; it is taken anyway
|
|
1767
|
-
* rather than carving out a lockless path.
|
|
1768
|
-
*
|
|
1769
|
-
*
|
|
2095
|
+
* rather than carving out a lockless path.
|
|
2096
|
+
*
|
|
2097
|
+
* Two policies reach here besides the default: `SCHEMA_INIT_RETRY_POLICY` from that one
|
|
2098
|
+
* call site, and `LOST_UPDATE_RETRY_POLICY` from the `CadrePeer` writers that re-read the
|
|
2099
|
+
* row inside their body ({@link updateSelfPeerRecord} directly; {@link reauthorizeCadrePeer},
|
|
2100
|
+
* {@link deleteCadrePeer} and {@link removeMachine} through {@link mutateCadrePeer}'s
|
|
2101
|
+
* policy parameter). Everything else takes the default, whose classifier and attempt count
|
|
2102
|
+
* are unchanged by those call sites existing.
|
|
1770
2103
|
*
|
|
1771
2104
|
* `label` names the operation in this loop's debug lines and nothing else
|
|
1772
2105
|
* ({@link ControlWriteRetryOptions.label}). Every call site in this class supplies one:
|
|
@@ -1863,12 +2196,17 @@ export declare class ControlDatabase {
|
|
|
1863
2196
|
* @param options - Optional `expiresAtMs` (epoch ms), `totalUses`, `validationUrl`,
|
|
1864
2197
|
* `strandId` (bind to a pre-existing host strand for provision-then-record)
|
|
1865
2198
|
*/
|
|
1866
|
-
insertFormationInvite(token: string, sAppId: string, ownerKey: string, signMessage:
|
|
2199
|
+
insertFormationInvite(token: string, sAppId: string, ownerKey: string, signMessage: ControlSign, options?: {
|
|
1867
2200
|
expiresAtMs?: number;
|
|
1868
2201
|
totalUses?: number;
|
|
1869
2202
|
validationUrl?: string;
|
|
1870
2203
|
strandId?: string;
|
|
1871
2204
|
}): Promise<void>;
|
|
2205
|
+
/**
|
|
2206
|
+
* The stamped, owner-signed `FormationInvite` insert that {@link insertFormationInvite} and
|
|
2207
|
+
* {@link insertFoundingRows} run. Reads the engine once, for the canonical `ExpiresAt`.
|
|
2208
|
+
*/
|
|
2209
|
+
private signFormationInviteInsert;
|
|
1872
2210
|
/**
|
|
1873
2211
|
* Redeem a `FormationInvite` by inserting the `Strand` row and a matching
|
|
1874
2212
|
* `FormationUsage` row **atomically, in one transaction**.
|
|
@@ -1939,14 +2277,13 @@ export declare class ControlDatabase {
|
|
|
1939
2277
|
* Use {@link redeemInvitation} instead when the strand must be created by
|
|
1940
2278
|
* consent atomically with the usage.
|
|
1941
2279
|
*
|
|
1942
|
-
* The strand's live `StampId` is
|
|
2280
|
+
* The strand's live `StampId` is written onto the usage row:
|
|
1943
2281
|
* `FormationUsage.StrandExists` matches the (id, stamp) PAIR, and
|
|
1944
2282
|
* `Strand.AuthorizedInsert`'s consent branch reads the same pair back, so a consent
|
|
1945
|
-
* record authorizes exactly the strand ROW it was recorded against. A
|
|
1946
|
-
*
|
|
1947
|
-
*
|
|
1948
|
-
*
|
|
1949
|
-
* point is a genuine race and deserves a named error, not a silent rollback.
|
|
2283
|
+
* record authorizes exactly the strand ROW it was recorded against. A caller that already
|
|
2284
|
+
* read the stamp passes it (`strandStampId`); otherwise it is read here first, and a
|
|
2285
|
+
* missing strand THROWS {@link MissingHostStrandError} rather than being left to the
|
|
2286
|
+
* deferred `StrandExists` CHECK.
|
|
1950
2287
|
*
|
|
1951
2288
|
* The row is keyed by the joiner's own nonce (`UsageStampId` is the primary key), so this
|
|
1952
2289
|
* path — the one production actually races on (a bound invite published by `cadre-web` /
|
|
@@ -1981,6 +2318,13 @@ export declare class ControlDatabase {
|
|
|
1981
2318
|
* {@link queryFormationInvite} read; `null` means "no invite row" / unlimited.
|
|
1982
2319
|
*/
|
|
1983
2320
|
totalUses?: number | null;
|
|
2321
|
+
/**
|
|
2322
|
+
* The host strand's `StampId`, when the caller already read the row
|
|
2323
|
+
* ({@link queryStrandWithStamp}); `undefined` reads it here. A stamp is fixed for the life
|
|
2324
|
+
* of its row, so one read earlier is the stamp `StrandExists` compares, unless the row was
|
|
2325
|
+
* removed (and re-seated) in between, which that CHECK refuses at commit.
|
|
2326
|
+
*/
|
|
2327
|
+
strandStampId?: string;
|
|
1984
2328
|
}): Promise<FormationUsageResult>;
|
|
1985
2329
|
/**
|
|
1986
2330
|
* Refuse a redemption that would consume a seat the invite does not have.
|
|
@@ -2034,7 +2378,7 @@ export declare class ControlDatabase {
|
|
|
2034
2378
|
* permissive PRE-check that runs ahead of it, and a transiently short read costs each of
|
|
2035
2379
|
* them only a worse outcome for the ATTEMPT, never a seat the invitation did not pay for:
|
|
2036
2380
|
* {@link assertSeatRemains} loses its named exhaustion error and falls back to the CHECK's
|
|
2037
|
-
* generic refusal; `ControlFormationUsageRecorder.
|
|
2381
|
+
* generic refusal; `ControlFormationUsageRecorder.readToken` reports a seat free and lets
|
|
2038
2382
|
* the redemption proceed to the CHECK, which decides; {@link hasOutstandingFormationInvite}
|
|
2039
2383
|
* holds the stranger-admission door open slightly longer.
|
|
2040
2384
|
*
|
|
@@ -2060,12 +2404,12 @@ export declare class ControlDatabase {
|
|
|
2060
2404
|
/**
|
|
2061
2405
|
* Is any `FormationInvite` row still redeemable — unexpired AND with usage
|
|
2062
2406
|
* below its `TotalUses`? A null `ExpiresAt` never expires and a null
|
|
2063
|
-
* `TotalUses` is unlimited, matching
|
|
2064
|
-
*
|
|
2407
|
+
* `TotalUses` is unlimited, matching the per-token semantics of `classifyTokenRead`
|
|
2408
|
+
* (`formation-token.ts`).
|
|
2065
2409
|
*
|
|
2066
2410
|
* Answers the control-network connection gate's coarse "does this node expect
|
|
2067
2411
|
* a stranger?" question, which has no token to ask about. The expiry
|
|
2068
|
-
* comparison is `expiresAtMs <= now` — identical to `
|
|
2412
|
+
* comparison is `expiresAtMs <= now` — identical to `isInviteExpired`'s — so an
|
|
2069
2413
|
* invite the formation handler would reject can never hold the gate open.
|
|
2070
2414
|
*
|
|
2071
2415
|
* The scan is deliberately not pushed into SQL: nothing else here compares a
|
|
@@ -2091,31 +2435,53 @@ export declare class ControlDatabase {
|
|
|
2091
2435
|
* private half to the invitee out of band. It is never stored here (`Strand.Invite` has the
|
|
2092
2436
|
* same shape).
|
|
2093
2437
|
*
|
|
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
2438
|
* @returns the row as stored, `expiresAt` in its canonical form
|
|
2100
2439
|
*/
|
|
2101
|
-
insertCadreInvite(invite:
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2440
|
+
insertCadreInvite(invite: CadreInviteFields, ownerKey: string, signMessage: ControlSign): Promise<CadreInviteRow>;
|
|
2441
|
+
/**
|
|
2442
|
+
* Build and owner-sign a `CadreInvite` row WITHOUT inserting it: the signing half of
|
|
2443
|
+
* {@link insertCadreInvite}, which `CadreNode.joinWithOwnerGrant` uses alone for the
|
|
2444
|
+
* invitation a node issues to itself, since a store that holds no `OwnerKey` row for the
|
|
2445
|
+
* issuer would refuse the local insert and the row is seated at a member from the bundle
|
|
2446
|
+
* instead. `ExpiresAt` is canonicalized by this database's engine, which is a format, not a
|
|
2447
|
+
* store, so the row reads the same wherever it is seated.
|
|
2448
|
+
*/
|
|
2449
|
+
signCadreInvite(invite: CadreInviteFields, ownerKey: string, signMessage: ControlSign): Promise<CadreInviteRow>;
|
|
2108
2450
|
/**
|
|
2109
2451
|
* Seat an owner-signed `CadreInvite` row received inside an invitation bundle, under its own
|
|
2110
2452
|
* 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
|
|
2453
|
+
* already holds the row (by replication, or from an earlier redemption attempt) is a no-op,
|
|
2454
|
+
* and so is one whose insert the cohort refuses because a member there holds it.
|
|
2112
2455
|
*
|
|
2113
2456
|
* @returns `true` when this call inserted the row, `false` when it was already present.
|
|
2114
2457
|
* @throws {CadreInviteIssuerUnknownError} when `CadreInvite.AuthorizedInsert` refuses it: the
|
|
2115
2458
|
* issuer is not an owner on this node (not replicated yet, or removed), or the signature does
|
|
2116
|
-
* not verify.
|
|
2459
|
+
* not verify. {@link IssuedRowNotHereYetError} when the cohort holds the row and this node
|
|
2460
|
+
* cannot read it yet. Every other refusal (a withdrawn stamp, `NotRevoked`) propagates as is.
|
|
2117
2461
|
*/
|
|
2118
2462
|
seatCadreInvite(row: CadreInviteRow): Promise<boolean>;
|
|
2463
|
+
/**
|
|
2464
|
+
* Insert-if-absent of an issued row from a bundle's copy, under the write lock: the body of
|
|
2465
|
+
* {@link seatCadreInvite} and {@link seatOwnerGrant}. `exec` runs the bare insert under the
|
|
2466
|
+
* row's own stored issuer pair.
|
|
2467
|
+
*
|
|
2468
|
+
* @returns `true` when this call inserted the row, `false` when it was already present —
|
|
2469
|
+
* here, or at the cohort member that refused the insert for holding it
|
|
2470
|
+
* ({@link adoptCohortHeldRow}).
|
|
2471
|
+
* @throws `refuse(cause)` when the table's `AuthorizedInsert` refuses the row;
|
|
2472
|
+
* {@link IssuedRowNotHereYetError} when the cohort holds the row and this node still cannot
|
|
2473
|
+
* read it; every other refusal (a withdrawn stamp, `NotRevoked`) propagates as is.
|
|
2474
|
+
*/
|
|
2475
|
+
private seatIssuedRow;
|
|
2476
|
+
/**
|
|
2477
|
+
* The seat's insert was refused because a cohort member already holds a row under `key`, which
|
|
2478
|
+
* this node's guard read missed because its store lags the cohort. A bundle's copy is verbatim
|
|
2479
|
+
* and every column is under the issuer's signature, so the held row is this one: it counts as
|
|
2480
|
+
* seated once a refreshing read (unlocked, so retried as any read) finds it here.
|
|
2481
|
+
*
|
|
2482
|
+
* @throws {IssuedRowNotHereYetError} when the read still does not find it.
|
|
2483
|
+
*/
|
|
2484
|
+
private adoptCohortHeldRow;
|
|
2119
2485
|
/** Bare `CadreInvite` insert shared by {@link insertCadreInvite} and {@link seatCadreInvite}; the caller holds the lock. */
|
|
2120
2486
|
private execCadreInviteInsert;
|
|
2121
2487
|
/**
|
|
@@ -2123,8 +2489,10 @@ export declare class ControlDatabase {
|
|
|
2123
2489
|
* the row is the holder's proof and never goes away; liveness is {@link hasLiveCadreInvite}'s
|
|
2124
2490
|
* question, and `CadreInviteUsage.Authorized`'s at redemption.
|
|
2125
2491
|
*
|
|
2126
|
-
*
|
|
2127
|
-
*
|
|
2492
|
+
* A redemption reads the row once, before its write, and carries it into
|
|
2493
|
+
* {@link isCadreInviteLive} and {@link redeemCadreInvite}. `retry: false` is passed by
|
|
2494
|
+
* {@link redeemCadreInvite} when it was handed none and reads INSIDE its locked write body —
|
|
2495
|
+
* the same per-call opt-out, for the same reason, as {@link queryStampId}'s.
|
|
2128
2496
|
*/
|
|
2129
2497
|
queryCadreInvite(key: string, retry?: boolean): Promise<CadreInviteRow | null>;
|
|
2130
2498
|
/**
|
|
@@ -2138,7 +2506,7 @@ export declare class ControlDatabase {
|
|
|
2138
2506
|
/**
|
|
2139
2507
|
* Every `CadreInviteUsage` row this node holds — the redemption records the membership
|
|
2140
2508
|
* predicate verifies invitation-admitted rows against. Same `retry` contract as
|
|
2141
|
-
* {@link queryCadreInvites}.
|
|
2509
|
+
* {@link queryCadreInvites}; both come from their membership views ({@link readMembershipRows}).
|
|
2142
2510
|
*/
|
|
2143
2511
|
queryCadreInviteUsages(retry?: boolean): Promise<CadreInviteUsageRow[]>;
|
|
2144
2512
|
/**
|
|
@@ -2149,6 +2517,8 @@ export declare class ControlDatabase {
|
|
|
2149
2517
|
* `retry: false` is passed by {@link assertCadreInviteSeatRemains}, inside the locked body.
|
|
2150
2518
|
*/
|
|
2151
2519
|
countCadreInviteUsage(key: string, retry?: boolean): Promise<number>;
|
|
2520
|
+
/** The one-row count read behind {@link countCadreInviteUsage} and {@link countOwnerGrantUsage}. */
|
|
2521
|
+
private countUsageRows;
|
|
2152
2522
|
/**
|
|
2153
2523
|
* Is any cadre invitation still redeemable — not withdrawn, unexpired, with uses below its
|
|
2154
2524
|
* `TotalUses`, and issued by a current owner? The conditions `CadreInviteUsage.Authorized`
|
|
@@ -2164,14 +2534,24 @@ export declare class ControlDatabase {
|
|
|
2164
2534
|
hasLiveCadreInvite(nowMs?: number): Promise<boolean>;
|
|
2165
2535
|
/**
|
|
2166
2536
|
* Is this one cadre invitation still redeemable here — the conditions
|
|
2167
|
-
* {@link hasLiveCadreInvite} applies, for one invitation? The row judged is the one this
|
|
2168
|
-
* holds under `invite.key
|
|
2169
|
-
* yet), but only if its issuer signature
|
|
2170
|
-
* judged by whichever owner it names as
|
|
2171
|
-
* {@link countCadreInviteUsage}: {@link seatCadreInvite}
|
|
2172
|
-
* what enforce every condition.
|
|
2537
|
+
* {@link hasLiveCadreInvite} applies, for one invitation? The row judged is `held`, the one this
|
|
2538
|
+
* node holds under `invite.key` ({@link queryCadreInvite}, read by the caller); when it holds
|
|
2539
|
+
* none, `invite` itself (a bundle's copy, not seated yet), but only if its issuer signature
|
|
2540
|
+
* verifies, because an unsigned copy would otherwise be judged by whichever owner it names as
|
|
2541
|
+
* issuer. A permissive pre-check like {@link countCadreInviteUsage}: {@link seatCadreInvite}
|
|
2542
|
+
* and the redemption's own write are what enforce every condition.
|
|
2543
|
+
*
|
|
2544
|
+
* The use count read here is read again inside the redemption's write lock
|
|
2545
|
+
* ({@link assertCadreInviteSeatRemains}), on purpose: this one decides whether the device is
|
|
2546
|
+
* pushed the control store before the write, so the holder of a spent invitation is refused
|
|
2547
|
+
* without being sent anything; the in-lock one decides the seat, against a concurrent redemption.
|
|
2548
|
+
*/
|
|
2549
|
+
isCadreInviteLive(invite: CadreInviteRow, held: CadreInviteRow | null, nowMs?: number): Promise<boolean>;
|
|
2550
|
+
/**
|
|
2551
|
+
* The body of {@link isCadreInviteLive} and {@link isOwnerGrantLive}: is `row` (the one held,
|
|
2552
|
+
* or a verified bundle copy; null for neither) still open and under its use cap?
|
|
2173
2553
|
*/
|
|
2174
|
-
|
|
2554
|
+
private issuedRowLive;
|
|
2175
2555
|
/**
|
|
2176
2556
|
* Every `CadreInvite` row this node holds with its standing: how many redemptions are
|
|
2177
2557
|
* recorded, whether it is withdrawn, and whether it is still redeemable — the same
|
|
@@ -2180,6 +2560,11 @@ export declare class ControlDatabase {
|
|
|
2180
2560
|
* occasional read, not a gate's.
|
|
2181
2561
|
*/
|
|
2182
2562
|
listCadreInviteStatuses(nowMs?: number): Promise<CadreInviteStatus[]>;
|
|
2563
|
+
/**
|
|
2564
|
+
* The body of {@link listCadreInviteStatuses} and {@link listOwnerGrantStatuses}: each of
|
|
2565
|
+
* `rows` with its withdrawal, its recorded uses and whether it is still redeemable.
|
|
2566
|
+
*/
|
|
2567
|
+
private issuedRowStandings;
|
|
2183
2568
|
/**
|
|
2184
2569
|
* Owner-signed WITHDRAWAL of a cadre invitation: file the `Revocation` tombstone retiring its
|
|
2185
2570
|
* stamp, with the row kept. The row stays because every device the invitation admitted
|
|
@@ -2197,24 +2582,29 @@ export declare class ControlDatabase {
|
|
|
2197
2582
|
* Fires the guarded-delete listener like a removal does, so a withdrawal committed while the
|
|
2198
2583
|
* node was alone is queued for re-issue on cohort growth ({@link CadreNode.noteGuardedDelete}).
|
|
2199
2584
|
*/
|
|
2200
|
-
withdrawCadreInvite(key: string, ownerKey: string, signMessage:
|
|
2585
|
+
withdrawCadreInvite(key: string, ownerKey: string, signMessage: ControlSign): Promise<boolean>;
|
|
2586
|
+
/**
|
|
2587
|
+
* The body of {@link withdrawCadreInvite} and {@link withdrawOwnerGrant}: under the write
|
|
2588
|
+
* lock, file the owner-signed tombstone retiring the row's stamp with the row kept, and fire
|
|
2589
|
+
* the guarded-delete listener for it (so a withdrawal made while alone is re-issued).
|
|
2590
|
+
*/
|
|
2591
|
+
private withdrawIssuedRow;
|
|
2201
2592
|
/**
|
|
2202
|
-
* The withdrawal tombstones for the `
|
|
2203
|
-
*
|
|
2204
|
-
* {@link
|
|
2593
|
+
* The withdrawal tombstones for the `table` rows among `keys` that are held here and not
|
|
2594
|
+
* yet withdrawn, signed and ready for {@link execTombstone}: the body
|
|
2595
|
+
* {@link withdrawIssuedRow} files one of and {@link removeMachine} files several of in one
|
|
2205
2596
|
* transaction. Inside the locked body (every read `retry: false`); an absent or already
|
|
2206
2597
|
* withdrawn key is logged and left out.
|
|
2207
2598
|
*/
|
|
2208
|
-
private
|
|
2599
|
+
private signWithdrawals;
|
|
2209
2600
|
/**
|
|
2210
|
-
* Redeem a seated `CadreInvite` on the owner's behalf: admit `peerId` as a cadre member
|
|
2211
|
-
*
|
|
2212
|
-
* transaction writes the `CadrePeer` row (consent branch of `AuthorizedInsert`)
|
|
2213
|
-
* `
|
|
2214
|
-
*
|
|
2215
|
-
*
|
|
2216
|
-
*
|
|
2217
|
-
* ({@link seatCadreInvite}): every consent branch reads `committed.CadreInvite`.
|
|
2601
|
+
* Redeem a seated `CadreInvite` on the owner's behalf: admit `peerId` as a cadre member —
|
|
2602
|
+
* with NO owner signature, and as a machine only (ownership is {@link redeemOwnerGrant}'s).
|
|
2603
|
+
* One transaction writes the `CadrePeer` row (consent branch of `AuthorizedInsert`) and then
|
|
2604
|
+
* the `CadreInviteUsage` row that authorizes it; the usage row's own check (`PeerExists`)
|
|
2605
|
+
* reads the peer row back, so the two are mutually bound to this exact redemption and
|
|
2606
|
+
* neither can be held in reserve. The invitation must already be committed
|
|
2607
|
+
* ({@link seatCadreInvite}): the consent branch reads `committed.CadreInvite`.
|
|
2218
2608
|
*
|
|
2219
2609
|
* The two signatures are the holder's and the device's, minted on the device over
|
|
2220
2610
|
* {@link cadreInviteRedeemMessage} / {@link cadreInviteConsentMessage}; this machine only
|
|
@@ -2223,26 +2613,41 @@ export declare class ControlDatabase {
|
|
|
2223
2613
|
* (`verifyInvitationAdmission`) — a mismatched pair is written and then never trusted.
|
|
2224
2614
|
*
|
|
2225
2615
|
* Idempotent for a retry after a dropped reply: when the device's row is already present
|
|
2226
|
-
*
|
|
2227
|
-
*
|
|
2228
|
-
*
|
|
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.
|
|
2616
|
+
* nothing is written and no seat is spent — `alreadyMember: true`. The seat check
|
|
2617
|
+
* ({@link assertCadreInviteSeatRemains}) runs after that, so the retry of a single-use
|
|
2618
|
+
* redemption is answered `alreadyMember` rather than refused as exhausted.
|
|
2231
2619
|
*
|
|
2232
2620
|
* Through {@link mutateCadrePeer}, so the membership hub is notified; inside the locked body
|
|
2233
2621
|
* every read passes `retry: false` and every stamp is minted per attempt, so a
|
|
2234
2622
|
* {@link lockedWithRetry} re-run starts clean.
|
|
2235
2623
|
*
|
|
2236
2624
|
* @throws {InvitationExhaustedError} when the invitation's seats are all spent (by name,
|
|
2237
|
-
* ahead of the schema's generic `Authorized` refusal — see {@link assertSeatRemains})
|
|
2625
|
+
* ahead of the schema's generic `Authorized` refusal — see {@link assertSeatRemains});
|
|
2626
|
+
* {@link IssuedRowNotHereYetError} when no `CadreInvite` row is readable here.
|
|
2238
2627
|
*/
|
|
2239
2628
|
redeemCadreInvite(params: {
|
|
2240
2629
|
inviteKey: string;
|
|
2630
|
+
/**
|
|
2631
|
+
* The row this node holds under `inviteKey`, read before the call ({@link queryCadreInvite});
|
|
2632
|
+
* omitted, it is read inside the locked body. Never a bundle's copy: the requester chooses a
|
|
2633
|
+
* copy's `TotalUses` and `IssuerKey`, and the seat check must judge the stored cap. The
|
|
2634
|
+
* schema refuses every update and delete of the row (`NoUpdate`, `NoDelete`), so the earlier
|
|
2635
|
+
* read is the row a locked read would return; withdrawal is a `Revocation` row, which the schema's
|
|
2636
|
+
* `NotRevoked` and `Authorized` checks still read at commit.
|
|
2637
|
+
*/
|
|
2638
|
+
held?: CadreInviteRow;
|
|
2241
2639
|
peerId: string;
|
|
2242
2640
|
/** The device's ed25519 public key (base64url); `peerId` is its identity multihash. */
|
|
2243
2641
|
peerKey: string;
|
|
2244
2642
|
/** The device's dialable addresses, if known; stored comma-joined as `authorizePeer` does. */
|
|
2245
2643
|
multiaddrs?: string[];
|
|
2644
|
+
/**
|
|
2645
|
+
* The rest of the device's own signed address record over `multiaddrs`, already verified by
|
|
2646
|
+
* the caller (`admittedRecord` in `cadre-invite-protocol.ts`): stored as the row's
|
|
2647
|
+
* `UpdatedAt`, `Sig` and `Profile`, so the row resolves to the device's addresses from its
|
|
2648
|
+
* admission. Omitted, the row is written unsigned, `UpdatedAt` at this machine's clock.
|
|
2649
|
+
*/
|
|
2650
|
+
record?: Pick<PeerAddressRecord, 'updatedAt' | 'profile' | 'sig'>;
|
|
2246
2651
|
/** Single-use nonce minted by the device; both signatures cover it. */
|
|
2247
2652
|
usageStampId: string;
|
|
2248
2653
|
/** The holder's signature with the invitation private key over the `'redeem'` digest. */
|
|
@@ -2251,13 +2656,6 @@ export declare class ControlDatabase {
|
|
|
2251
2656
|
peerSig: string;
|
|
2252
2657
|
nowMs?: number;
|
|
2253
2658
|
}): 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
2659
|
/**
|
|
2262
2660
|
* Refuse a redemption that would consume a seat the cadre invitation does not have, by
|
|
2263
2661
|
* name ({@link InvitationExhaustedError}) — the cadre-invitation twin of
|
|
@@ -2266,10 +2664,150 @@ export declare class ControlDatabase {
|
|
|
2266
2664
|
* committed count inside the locked body, so on a same-node race the loser is refused here.
|
|
2267
2665
|
*/
|
|
2268
2666
|
private assertCadreInviteSeatRemains;
|
|
2667
|
+
/** The body of {@link assertCadreInviteSeatRemains} and {@link assertOwnerGrantSeatRemains}; `countUsage` reads inside the locked body. */
|
|
2668
|
+
private assertIssuedSeatRemains;
|
|
2669
|
+
/**
|
|
2670
|
+
* Mint and insert an owner-signed `OwnerGrant`: a grant of ownership to whoever holds the
|
|
2671
|
+
* grant's private half, redeemable through any member machine ({@link redeemOwnerGrant})
|
|
2672
|
+
* to seat an owner key the holder generates or already holds. No machine is admitted and no
|
|
2673
|
+
* peer id is named. The grant twin of {@link insertCadreInvite}: the owner signs the whole
|
|
2674
|
+
* row ({@link ownerGrantAddMessage}), `ExpiresAt` through {@link canonicalDatetime}, and
|
|
2675
|
+
* the returned row is what the grant bundle carries, re-inserted verbatim by
|
|
2676
|
+
* {@link seatOwnerGrant} on a member that has not received it by replication.
|
|
2677
|
+
*
|
|
2678
|
+
* @param grant.key - the grant's ed25519 PUBLIC key (base64url), the row's primary key; the
|
|
2679
|
+
* caller minted the pair and hands the private half to the holder out of band
|
|
2680
|
+
* @param grant.label - the label every key this grant seats will carry
|
|
2681
|
+
* ({@link ownerKeyLabel}'s rule: trimmed, at most 64 characters, blank meaning none); the
|
|
2682
|
+
* issuer's choice, which the holder cannot change
|
|
2683
|
+
* @param grant.expiresAtMs - epoch ms after which it cannot be redeemed; omit or null for
|
|
2684
|
+
* never. The short default lifetime is the node API's to apply, not this writer's.
|
|
2685
|
+
* @param grant.totalUses - how many redemptions it allows; omit for unlimited
|
|
2686
|
+
* @returns the row as stored, `expiresAt` in its canonical form
|
|
2687
|
+
*/
|
|
2688
|
+
insertOwnerGrant(grant: {
|
|
2689
|
+
key: string;
|
|
2690
|
+
label?: string | null;
|
|
2691
|
+
expiresAtMs?: number | null;
|
|
2692
|
+
totalUses?: number | null;
|
|
2693
|
+
}, ownerKey: string, signMessage: ControlSign): Promise<OwnerGrantRow>;
|
|
2694
|
+
/**
|
|
2695
|
+
* Seat an owner-signed `OwnerGrant` row received inside a grant bundle, under its own stored
|
|
2696
|
+
* `IssuerKey`/`IssuerSig` as the write context — insert-if-absent, as {@link seatCadreInvite}.
|
|
2697
|
+
*
|
|
2698
|
+
* @returns `true` when this call inserted the row, `false` when it was already present.
|
|
2699
|
+
* @throws {OwnerGrantIssuerUnknownError} when `OwnerGrant.AuthorizedInsert` refuses it: the
|
|
2700
|
+
* issuer is not an owner on this node (not replicated yet, or removed), or the signature does
|
|
2701
|
+
* not verify. {@link IssuedRowNotHereYetError} when the cohort holds the row and this node
|
|
2702
|
+
* cannot read it yet. Every other refusal (a withdrawn stamp, `NotRevoked`) propagates as is.
|
|
2703
|
+
*/
|
|
2704
|
+
seatOwnerGrant(row: OwnerGrantRow): Promise<boolean>;
|
|
2705
|
+
/** Bare `OwnerGrant` insert shared by {@link insertOwnerGrant} and {@link seatOwnerGrant}; the caller holds the lock. */
|
|
2706
|
+
private execOwnerGrantInsert;
|
|
2707
|
+
/**
|
|
2708
|
+
* Read one `OwnerGrant` row by key, or null when absent. Withdrawn rows are returned too,
|
|
2709
|
+
* as {@link queryCadreInvite} returns them: the row is the holder's proof and never goes
|
|
2710
|
+
* away. Read once per redemption and carried into {@link isOwnerGrantLive} and
|
|
2711
|
+
* {@link redeemOwnerGrant}; `retry: false` is passed by {@link redeemOwnerGrant} when it was
|
|
2712
|
+
* handed none and reads inside its locked body.
|
|
2713
|
+
*/
|
|
2714
|
+
queryOwnerGrant(key: string, retry?: boolean): Promise<OwnerGrantRow | null>;
|
|
2715
|
+
/**
|
|
2716
|
+
* Every `OwnerGrant` row this node holds, withdrawn and expired ones included. Read by the
|
|
2717
|
+
* anchor sync when at least one `OwnerKey` row is consent-seated (`CadreNode.loadOwnerGrantChain`);
|
|
2718
|
+
* `retry: false` is forwarded from the membership-gate refresh for {@link queryCadrePeers}' reason.
|
|
2719
|
+
*/
|
|
2720
|
+
queryOwnerGrants(retry?: boolean): Promise<OwnerGrantRow[]>;
|
|
2721
|
+
/** Every `OwnerGrantUsage` row this node holds — the redemption records a grant-seated `OwnerKey` row is verified against. Same `retry` contract as {@link queryOwnerGrants}; both come from their membership views ({@link readMembershipRows}). */
|
|
2722
|
+
queryOwnerGrantUsages(retry?: boolean): Promise<OwnerGrantUsageRow[]>;
|
|
2723
|
+
/**
|
|
2724
|
+
* How many times an owner grant has been redeemed (its `OwnerGrantUsage` rows), served
|
|
2725
|
+
* through the `OwnerGrantUsageByGrant` index. A permissive PRE-check like
|
|
2726
|
+
* {@link countCadreInviteUsage}: the cap is `OwnerGrantUsage.Authorized`'s own committed count.
|
|
2727
|
+
*/
|
|
2728
|
+
countOwnerGrantUsage(key: string, retry?: boolean): Promise<number>;
|
|
2729
|
+
/**
|
|
2730
|
+
* Is this one owner grant still redeemable here — not withdrawn, unexpired, uses left, issuer
|
|
2731
|
+
* still an owner — judged as {@link isCadreInviteLive} judges an invitation: by `held`, the row
|
|
2732
|
+
* this node holds under `grant.key` ({@link queryOwnerGrant}, read by the caller), else by
|
|
2733
|
+
* `grant` itself (a bundle's copy, not seated yet) when its issuer signature verifies. A
|
|
2734
|
+
* permissive pre-check; {@link seatOwnerGrant} and the redemption's own write enforce every
|
|
2735
|
+
* condition. Its use count is read again inside the write lock, for
|
|
2736
|
+
* {@link isCadreInviteLive}'s reason.
|
|
2737
|
+
*/
|
|
2738
|
+
isOwnerGrantLive(grant: OwnerGrantRow, held: OwnerGrantRow | null, nowMs?: number): Promise<boolean>;
|
|
2739
|
+
/**
|
|
2740
|
+
* Every `OwnerGrant` row this node holds with its standing — recorded redemptions, withdrawal,
|
|
2741
|
+
* and whether it is still redeemable — for an owner's listing, as {@link listCadreInviteStatuses}.
|
|
2742
|
+
*/
|
|
2743
|
+
listOwnerGrantStatuses(nowMs?: number): Promise<OwnerGrantStatus[]>;
|
|
2744
|
+
/**
|
|
2745
|
+
* Owner-signed WITHDRAWAL of an owner grant: file the `Revocation` tombstone retiring its
|
|
2746
|
+
* stamp, with the row kept, exactly as {@link withdrawCadreInvite} withdraws an invitation
|
|
2747
|
+
* and for the same reason (every key the grant seated proves itself through the row;
|
|
2748
|
+
* `OwnerGrant.NoDelete`, `Revocation.RowIsGone`). Afterwards `OwnerGrantUsage.Authorized`
|
|
2749
|
+
* refuses every further redemption and `OwnerGrant.NotRevoked` refuses re-seating the
|
|
2750
|
+
* holder's copy on any node holding the tombstone. Keys already seated stay owners;
|
|
2751
|
+
* removing one is {@link deleteOwnerKey}.
|
|
2752
|
+
*
|
|
2753
|
+
* Idempotent: `false` (nothing written) when the row is absent here or already withdrawn,
|
|
2754
|
+
* `true` when this call filed the tombstone. Fires the guarded-delete listener, so a
|
|
2755
|
+
* withdrawal committed while alone is re-issued on cohort growth.
|
|
2756
|
+
*/
|
|
2757
|
+
withdrawOwnerGrant(key: string, ownerKey: string, signMessage: ControlSign): Promise<boolean>;
|
|
2758
|
+
/**
|
|
2759
|
+
* Redeem a seated `OwnerGrant` on the issuer's behalf: seat `seatedKey` as an owner with NO
|
|
2760
|
+
* owner signature. One transaction writes the `OwnerKey` row (the grant branch of
|
|
2761
|
+
* `Authorized`: `VouchOwner` the issuer, `VouchSig` null, `VouchUsage` the usage stamp,
|
|
2762
|
+
* `Label` the grant's) and then the `OwnerGrantUsage` row that authorizes it; the usage
|
|
2763
|
+
* row's `SeatedExists` reads the owner row back, so the two are bound to this exact
|
|
2764
|
+
* redemption and neither can be held in reserve. The grant must already be committed
|
|
2765
|
+
* ({@link seatOwnerGrant}): both consent reads are of `committed.OwnerGrant`. No machine is
|
|
2766
|
+
* admitted and no peer id is named.
|
|
2767
|
+
*
|
|
2768
|
+
* The two signatures are minted where the private keys are (the grant's and the seated
|
|
2769
|
+
* key's), over {@link ownerGrantRedeemMessage} / {@link ownerGrantConsentMessage}; this
|
|
2770
|
+
* machine only verifies them, by writing the row.
|
|
2771
|
+
*
|
|
2772
|
+
* Idempotent, and a no-op for a key that is already an owner: when `seatedKey` has a live
|
|
2773
|
+
* `OwnerKey` row here nothing is written and no use is spent — `alreadyOwner: true` with
|
|
2774
|
+
* that row's stamp. The seat check ({@link assertOwnerGrantSeatRemains}) runs after that, so
|
|
2775
|
+
* the retry of a single-use redemption is answered `alreadyOwner` rather than refused as
|
|
2776
|
+
* exhausted. A key removed earlier is seated afresh under a new stamp (`NotRevoked` passes),
|
|
2777
|
+
* except on a machine that still holds its retired row, which refuses by name
|
|
2778
|
+
* ({@link liveOwnerKeyStamp}).
|
|
2779
|
+
*
|
|
2780
|
+
* Through {@link mutateCadrePeer}, so the membership hub is notified and the anchors follow;
|
|
2781
|
+
* inside the locked body every read passes `retry: false` and the stamp is minted per
|
|
2782
|
+
* attempt, so a {@link lockedWithRetry} re-run starts clean.
|
|
2783
|
+
*
|
|
2784
|
+
* @throws {InvitationExhaustedError} when the grant's uses are all spent (by name, ahead of
|
|
2785
|
+
* the schema's generic `Authorized` refusal); its `token` is the grant key.
|
|
2786
|
+
* {@link IssuedRowNotHereYetError} when no `OwnerGrant` row is readable here.
|
|
2787
|
+
*/
|
|
2788
|
+
redeemOwnerGrant(params: {
|
|
2789
|
+
grantKey: string;
|
|
2790
|
+
/** The row this node holds under `grantKey`, read before the call; omitted, it is read inside the locked body. Never a bundle's copy, for {@link redeemCadreInvite}'s reason. */
|
|
2791
|
+
held?: OwnerGrantRow;
|
|
2792
|
+
/** The owner key to seat (base64url ed25519): the holder generated it, or already holds it. */
|
|
2793
|
+
seatedKey: string;
|
|
2794
|
+
/** Single-use nonce minted by the holder; both signatures cover it. */
|
|
2795
|
+
usageStampId: string;
|
|
2796
|
+
/** The holder's signature with the grant private key over the `'redeem'` digest. */
|
|
2797
|
+
grantSig: string;
|
|
2798
|
+
/** The seated key's signature over the `'consent'` digest. */
|
|
2799
|
+
ownerSig: string;
|
|
2800
|
+
nowMs?: number;
|
|
2801
|
+
}): Promise<OwnerGrantRedemptionResult>;
|
|
2802
|
+
/** {@link assertCadreInviteSeatRemains} for an owner grant: refuse an exhausted grant by name, ahead of the write. */
|
|
2803
|
+
private assertOwnerGrantSeatRemains;
|
|
2269
2804
|
/**
|
|
2270
2805
|
* Close the database and cleanup resources.
|
|
2271
2806
|
*
|
|
2272
|
-
*
|
|
2807
|
+
* Disposes every {@link watchTables} subscription before anything else, so a commit that
|
|
2808
|
+
* lands while the write chain drains schedules no run, and again before dropping the handle.
|
|
2809
|
+
*
|
|
2810
|
+
* Then drains the local-write chain: {@link withWriteLock} can park a write behind
|
|
2273
2811
|
* others across an await, and a queued closure evaluates `this.db!` only when it
|
|
2274
2812
|
* finally runs — so nulling the handle out from under it would throw a TypeError on a
|
|
2275
2813
|
* null handle instead of committing. The tail never rejects (it swallows both
|