@serfab/cadre-core 1.17.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 +3 -2
- package/dist/cadre-invite-protocol.d.ts +35 -13
- package/dist/cadre-invite-protocol.js +90 -25
- package/dist/cadre-invite-protocol.js.map +1 -1
- package/dist/cadre-node.d.ts +586 -255
- package/dist/cadre-node.js +1176 -564
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-cohort.d.ts +66 -9
- package/dist/control-cohort.js +67 -9
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +451 -193
- package/dist/control-database.js +701 -359
- 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 +6 -0
- package/dist/control-protocol-guard.js +14 -2
- package/dist/control-protocol-guard.js.map +1 -1
- package/dist/control-schema-version.d.ts +48 -31
- package/dist/control-schema-version.js +67 -45
- package/dist/control-schema-version.js.map +1 -1
- package/dist/control-schema.d.ts +8 -8
- package/dist/control-schema.js +16 -16
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +42 -7
- package/dist/control-write-retry.js +87 -19
- package/dist/control-write-retry.js.map +1 -1
- 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 +14 -13
- package/dist/index.js +16 -13
- package/dist/index.js.map +1 -1
- package/dist/joined-strand-store.d.ts +26 -9
- package/dist/joined-strand-store.js +63 -24
- package/dist/joined-strand-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/member-redemption.d.ts +15 -9
- package/dist/member-redemption.js +13 -8
- package/dist/member-redemption.js.map +1 -1
- 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-grant-protocol.d.ts +11 -7
- package/dist/owner-grant-protocol.js +31 -17
- package/dist/owner-grant-protocol.js.map +1 -1
- 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-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/seed-bootstrap.d.ts +47 -40
- package/dist/seed-bootstrap.js +66 -61
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/strand-database.d.ts +23 -2
- package/dist/strand-database.js +26 -6
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +176 -89
- package/dist/strand-first-sync-gate.js +288 -122
- 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 +26 -17
- 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 +40 -28
- package/dist/strand-membership-writer.js +212 -153
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-move.js +5 -2
- package/dist/strand-move.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 +90 -49
- package/dist/types.js +4 -4
- package/dist/types.js.map +1 -1
- package/package.json +8 -8
- package/src/cadre-invite-protocol.ts +119 -31
- package/src/cadre-node.ts +1282 -584
- package/src/control-cohort.ts +86 -9
- package/src/control-database.ts +848 -403
- package/src/control-formation-recorder.ts +359 -353
- package/src/control-protocol-guard.ts +16 -2
- package/src/control-schema-version.ts +101 -51
- package/src/control-schema.ts +16 -16
- package/src/control-write-retry.ts +91 -19
- package/src/delegate-admission.ts +351 -351
- package/src/enrolled-machine-store.ts +3 -3
- package/src/formation-token.ts +63 -0
- package/src/index.ts +30 -17
- package/src/joined-strand-store.ts +69 -24
- package/src/link-budget.ts +9 -7
- package/src/member-redemption.ts +20 -13
- package/src/membership-view.ts +144 -0
- package/src/owner-grant-protocol.ts +33 -18
- package/src/peer-addr-book.ts +4 -3
- package/src/peer-join-backfill.ts +971 -761
- package/src/pending-join-runner.ts +76 -53
- package/src/seed-bootstrap.ts +72 -67
- package/src/strand-database.ts +352 -322
- package/src/strand-first-sync-gate.ts +334 -134
- 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 -2019
- package/src/strand-move.ts +5 -2
- 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 +89 -51
|
@@ -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,
|
|
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
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;
|
|
@@ -59,16 +61,16 @@ export declare class FormationAbortedError extends Error {
|
|
|
59
61
|
* the write, inside {@link ControlDatabase.redeemInvitation} /
|
|
60
62
|
* {@link ControlDatabase.recordFormationUsage}, off the same check for a cadre invitation
|
|
61
63
|
* ({@link ControlDatabase.redeemCadreInvite}) and an owner grant
|
|
62
|
-
* ({@link ControlDatabase.redeemOwnerGrant}), and off the unlocked
|
|
63
|
-
* `ControlFormationUsageRecorder.authorizeUsage` runs
|
|
64
|
-
* pass — and off nothing else. It exists because
|
|
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
|
|
65
67
|
* without it the refusal surfaces as a generic `CHECK constraint failed: Authorized` from the
|
|
66
68
|
* schema's own count-based cap clause, which the manager reports as a retryable
|
|
67
69
|
* `Formation conflict, retry` — telling the joiner to retry something that can never succeed.
|
|
68
70
|
* Two redemptions of one token racing on the SAME node serialize behind the local write
|
|
69
|
-
* queue, so the loser reads the winner's committed row and is refused here by name —
|
|
70
|
-
* `StrandFormationManager.validateToken`
|
|
71
|
-
*
|
|
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.
|
|
72
74
|
*/
|
|
73
75
|
export declare class InvitationExhaustedError extends Error {
|
|
74
76
|
readonly token: string;
|
|
@@ -324,6 +326,56 @@ export declare function ownerKeyLabel(label: string | null | undefined): string
|
|
|
324
326
|
* other control writes, sign outside the lock and re-check the stamp inside it.
|
|
325
327
|
*/
|
|
326
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
|
+
}
|
|
327
379
|
/** What an owner asks {@link ControlDatabase.insertCadreInvite} (or {@link ControlDatabase.signCadreInvite}) to sign. */
|
|
328
380
|
export interface CadreInviteFields {
|
|
329
381
|
/** The invitation public key (base64url ed25519), the row's primary key. */
|
|
@@ -336,14 +388,15 @@ export interface CadreInviteFields {
|
|
|
336
388
|
totalUses?: number | null;
|
|
337
389
|
}
|
|
338
390
|
/** The control tables keyed by a strand id whose repeat insert a caller may treat as idempotent. */
|
|
339
|
-
type StrandIdTable = Extract<RevocableTable, 'Strand' | 'JoinedStrand'>;
|
|
391
|
+
type StrandIdTable = Extract<RevocableTable, 'Strand' | 'StrandPartyKey' | 'JoinedStrand'>;
|
|
340
392
|
/**
|
|
341
393
|
* Did this write to `table` fail because the strand id is already seated there?
|
|
342
394
|
*
|
|
343
395
|
* {@link CadreNode.publishStrand} uses this to tell "my own earlier publish already
|
|
344
396
|
* landed" (re-read, and no-op when the row matches) apart from every other rejection —
|
|
345
397
|
* an unauthorized signer, a retired stamp — which must keep surfacing. For `JoinedStrand`
|
|
346
|
-
* 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.
|
|
347
400
|
*
|
|
348
401
|
* Matched by TEXT, not by type: the typed engine error does not survive the trip out of
|
|
349
402
|
* optimystic (same constraint the retry classifiers in `control-write-retry.ts` document).
|
|
@@ -390,17 +443,6 @@ export declare class PendingJoinChangedError extends Error {
|
|
|
390
443
|
readonly live: PendingJoin | null;
|
|
391
444
|
constructor(id: string, live: PendingJoin | null);
|
|
392
445
|
}
|
|
393
|
-
/**
|
|
394
|
-
* The one `CadreControl.Revocation` row that retires nothing, filed once by an owner
|
|
395
|
-
* ({@link ControlDatabase.openRevocationLedger}) so the table is never a never-written
|
|
396
|
-
* block. The schema's table comment says why that matters and why the row can never read
|
|
397
|
-
* as a retirement; `RowIsGone` pins this exact triple.
|
|
398
|
-
*/
|
|
399
|
-
export declare const REVOCATION_LEDGER_MARKER: {
|
|
400
|
-
readonly tableName: "Revocation";
|
|
401
|
-
readonly rowKey: "ledger";
|
|
402
|
-
readonly stampId: "opened";
|
|
403
|
-
};
|
|
404
446
|
/**
|
|
405
447
|
* Project `CadrePeer` rows into the {@link SeedPeer} list a seed, or a cadre invitation
|
|
406
448
|
* redemption reply, carries. `minterPeerId` is the machine minting the seed or answering the
|
|
@@ -431,16 +473,8 @@ export declare function projectSeedPeers(rows: readonly CadrePeerRow[], minterPe
|
|
|
431
473
|
export declare const REAPABLE_TABLES: readonly ["CadrePeer", "DeviceToken", "ValidationKey", "JoinedStrand", "JoinRequest"];
|
|
432
474
|
export type ReapableTable = (typeof REAPABLE_TABLES)[number];
|
|
433
475
|
/** A `FormationInvite` row as {@link ControlDatabase.queryFormationInvite} reads it. */
|
|
434
|
-
export interface FormationInviteRow {
|
|
476
|
+
export interface FormationInviteRow extends FormationInviteFacts {
|
|
435
477
|
token: string;
|
|
436
|
-
sAppId: string;
|
|
437
|
-
/** Parsed epoch ms of the stored `datetime`; null when the invitation never expires. */
|
|
438
|
-
expiresAtMs: number | null;
|
|
439
|
-
/** Null is unlimited. */
|
|
440
|
-
totalUses: number | null;
|
|
441
|
-
validationUrl: string | null;
|
|
442
|
-
/** A bound invitation's host strand; null on the responder-provisions path. */
|
|
443
|
-
strandId: string | null;
|
|
444
478
|
}
|
|
445
479
|
/**
|
|
446
480
|
* Notified after a membership write (a `CadreControl.CadrePeer` or `OwnerKey` row, see
|
|
@@ -513,6 +547,7 @@ export interface ControlDatabaseConfig {
|
|
|
513
547
|
export declare class ControlDatabase {
|
|
514
548
|
private db;
|
|
515
549
|
private collectionFactory;
|
|
550
|
+
private txnBridge;
|
|
516
551
|
private readonly config;
|
|
517
552
|
private initialized;
|
|
518
553
|
private membershipListener;
|
|
@@ -520,6 +555,18 @@ export declare class ControlDatabase {
|
|
|
520
555
|
private controlWriteAbandonedListener;
|
|
521
556
|
/** Tail of the local-write chain — see {@link withWriteLock}. */
|
|
522
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;
|
|
523
570
|
/**
|
|
524
571
|
* Locked write bodies currently running (see {@link runWriteBody}); what
|
|
525
572
|
* {@link readRowsOnce} consults to route an unlocked read around a write that has
|
|
@@ -546,6 +593,30 @@ export declare class ControlDatabase {
|
|
|
546
593
|
* Initialize the database - load schema and register plugins
|
|
547
594
|
*/
|
|
548
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;
|
|
549
620
|
private loadSchema;
|
|
550
621
|
/** Apply the declared control schema, retrying transient cohort failures. */
|
|
551
622
|
private applyControlSchema;
|
|
@@ -679,6 +750,39 @@ export declare class ControlDatabase {
|
|
|
679
750
|
* under-lock callback seam is ever added, audit its read graph the same way.
|
|
680
751
|
*/
|
|
681
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;
|
|
682
786
|
/**
|
|
683
787
|
* Query all strands from the control database
|
|
684
788
|
*/
|
|
@@ -690,6 +794,14 @@ export declare class ControlDatabase {
|
|
|
690
794
|
* validated invitee during provision-then-record formation.
|
|
691
795
|
*/
|
|
692
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>;
|
|
693
805
|
/**
|
|
694
806
|
* Every strand this party joined from another party (`CadreControl.JoinedStrand`), shaped
|
|
695
807
|
* as a {@link StrandRow} so a reader can treat it like one of the party's own. A joiner
|
|
@@ -735,6 +847,9 @@ export declare class ControlDatabase {
|
|
|
735
847
|
*
|
|
736
848
|
* Deliberately RAW (no retired-stamp filter, unlike {@link getOwnerKeys}): the genesis
|
|
737
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.
|
|
738
853
|
*/
|
|
739
854
|
hasOwnerKey(): Promise<boolean>;
|
|
740
855
|
/**
|
|
@@ -742,9 +857,44 @@ export declare class ControlDatabase {
|
|
|
742
857
|
* the party has none yet. Returns true if it inserted, false if an owner
|
|
743
858
|
* key already existed (so a repeat `--owner` start is a no-op).
|
|
744
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
|
+
*
|
|
745
870
|
* @param label - the user's name for the key ({@link insertOwnerKey}).
|
|
746
871
|
*/
|
|
747
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;
|
|
748
898
|
/**
|
|
749
899
|
* Collect every owner key (`CadreControl.OwnerKey.Key`) as a set.
|
|
750
900
|
*
|
|
@@ -783,9 +933,8 @@ export declare class ControlDatabase {
|
|
|
783
933
|
*
|
|
784
934
|
* @param retired - the retired `OwnerKey` stamps, when the caller has already read the
|
|
785
935
|
* tombstones (`CadreNode.syncOwnerAnchor` reads {@link queryRevocations} for their
|
|
786
|
-
* signers); omitted, they are read here.
|
|
787
|
-
*
|
|
788
|
-
* 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.
|
|
789
938
|
*/
|
|
790
939
|
queryOwnerKeyRows(retry?: boolean, retired?: ReadonlySet<string>): Promise<OwnerKeyRow[]>;
|
|
791
940
|
/**
|
|
@@ -800,12 +949,8 @@ export declare class ControlDatabase {
|
|
|
800
949
|
* {@link queryRevokedStamps} rather than inlining the SQL, so tests can interpose on
|
|
801
950
|
* that seam to model the replicated live-row-plus-tombstone merge state.
|
|
802
951
|
*
|
|
803
|
-
*
|
|
804
|
-
*
|
|
805
|
-
* while `Revocation` has never been written every call consults the cohort about it, and
|
|
806
|
-
* the ledger marker ({@link openRevocationLedger}) is what makes it a held block. Folding
|
|
807
|
-
* the exclusion into one statement would not help (it reads the same block), and caching
|
|
808
|
-
* 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.
|
|
809
954
|
*
|
|
810
955
|
* NOTE: this filter is HAND-MIRRORED by the gate suites' fake control databases
|
|
811
956
|
* (`test/membership-gate-helpers.ts` and `test/cadre-node-authorized-surface.spec.ts`),
|
|
@@ -866,17 +1011,18 @@ export declare class ControlDatabase {
|
|
|
866
1011
|
* guard. A read retrying there would sleep its backoff HOLDING the write lock
|
|
867
1012
|
* (`retryControlWrite`'s contract is that backoff sleeps happen with no lock held), and
|
|
868
1013
|
* the write funnel already re-runs the body's reads when it re-runs the body. The
|
|
869
|
-
* unlocked callers — the public stamp readers
|
|
870
|
-
*
|
|
871
|
-
*
|
|
872
|
-
*
|
|
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.
|
|
873
1019
|
*/
|
|
874
1020
|
private queryStampId;
|
|
875
1021
|
/**
|
|
876
1022
|
* `CadrePeer` stamp nonce — read by {@link SeedBootstrapService.removePeer} as its
|
|
877
1023
|
* row-present gate before it delegates to {@link deleteCadrePeer}. The in-class
|
|
878
|
-
* `CadrePeer` writers
|
|
879
|
-
*
|
|
1024
|
+
* `CadrePeer` writers go straight to {@link queryStampId} ({@link insertCadrePeer}) or read
|
|
1025
|
+
* the whole row ({@link reauthorizeCadrePeer}, through {@link findCadrePeerRow}).
|
|
880
1026
|
*/
|
|
881
1027
|
queryCadrePeerStampId(peerId: string): Promise<string | null>;
|
|
882
1028
|
/**
|
|
@@ -913,33 +1059,34 @@ export declare class ControlDatabase {
|
|
|
913
1059
|
* ({@link queryCadrePeers}, {@link queryPeerRecord},
|
|
914
1060
|
* {@link CadreNode.resolveDeviceToken}) drop any row whose stamp appears here.
|
|
915
1061
|
*
|
|
916
|
-
*
|
|
917
|
-
*
|
|
918
|
-
*
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
*
|
|
922
|
-
*
|
|
923
|
-
* 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
|
|
924
1069
|
* removals become routine and the scan itself shows up in a profile.
|
|
925
1070
|
*
|
|
926
1071
|
* An isolated node that has never received the `Revocation` block reads it as holding no
|
|
927
|
-
* revocations instead of throwing — see {@link
|
|
1072
|
+
* revocations instead of throwing — see {@link readRevocationRowsOrNone}.
|
|
928
1073
|
*
|
|
929
1074
|
* `retry: false` is forwarded by {@link queryCadrePeers} for the one caller that reads
|
|
930
1075
|
* under the write lock — see that method's note.
|
|
931
1076
|
*/
|
|
932
1077
|
queryRevokedStamps(tableName: RevocableTable, retry?: boolean): Promise<Set<string>>;
|
|
933
1078
|
/**
|
|
934
|
-
* The `Revocation`
|
|
935
|
-
* 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
|
|
936
1081
|
* the block and could ask no other cohort member about it. Every other failure rethrows.
|
|
937
1082
|
* The retry in {@link readRows} runs first, so a node whose connectivity returns within
|
|
938
|
-
* 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.
|
|
939
1085
|
*
|
|
940
1086
|
* Without this, a machine cut off from every other machine in its party before it ever
|
|
941
|
-
* received the block —
|
|
942
|
-
*
|
|
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
|
|
943
1090
|
* answer ANY membership, peer-record or device-token lookup, although it holds those
|
|
944
1091
|
* rows locally.
|
|
945
1092
|
*
|
|
@@ -958,12 +1105,12 @@ export declare class ControlDatabase {
|
|
|
958
1105
|
*
|
|
959
1106
|
* NOTE: an isolated node pays the whole read retry (up to
|
|
960
1107
|
* `CONTROL_READ_RETRY_BUDGET_MS`, plus the slow attempt that exhausted it) before this
|
|
961
|
-
* answers,
|
|
962
|
-
* node's lookups are seen exceeding the
|
|
963
|
-
* (`ADMISSION_DECISION_TIMEOUT_MS`), skip the retry for this
|
|
964
|
-
* 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`.
|
|
965
1112
|
*/
|
|
966
|
-
private
|
|
1113
|
+
private readRevocationRowsOrNone;
|
|
967
1114
|
/**
|
|
968
1115
|
* Every locally-held `CadreControl.Revocation` tombstone — identity triple, its
|
|
969
1116
|
* `ReissuedAt` counter and the stored signer pair. Consumed by the cohort-growth re-issue
|
|
@@ -972,17 +1119,18 @@ export declare class ControlDatabase {
|
|
|
972
1119
|
* `where`, so the composite-primary-key point-lookup hazard (see the statement comment in
|
|
973
1120
|
* {@link reissueRevocations}) does not arise. Unlocked, like every other read.
|
|
974
1121
|
*
|
|
975
|
-
* Skips the ledger marker ({@link REVOCATION_LEDGER_MARKER})
|
|
976
|
-
* neither sweep may reap or re-sign it, and skipping it keeps
|
|
977
|
-
* a {@link RevocableTable}. Filtered on `TableName` alone, in
|
|
978
|
-
* 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'`.
|
|
979
1126
|
*
|
|
980
1127
|
* `retry: false` from a caller that reads under the write lock, for the reason on
|
|
981
1128
|
* {@link queryCadrePeers}.
|
|
982
1129
|
*/
|
|
983
1130
|
queryRevocations(retry?: boolean): Promise<RevocationRow[]>;
|
|
984
1131
|
/**
|
|
985
|
-
* 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}).
|
|
986
1134
|
*
|
|
987
1135
|
* Returns null when no row exists, or when the row's `StampId` is retired in
|
|
988
1136
|
* `CadreControl.Revocation` — a revoked peer reads as absent, so the resolver
|
|
@@ -998,10 +1146,22 @@ export declare class ControlDatabase {
|
|
|
998
1146
|
* The split `addrs` re-join to the exact stored `Multiaddr` (split-on-`,` is
|
|
999
1147
|
* the inverse of join-on-`,`), so the resolver re-verifies over the same bytes
|
|
1000
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.
|
|
1001
1160
|
*/
|
|
1002
|
-
|
|
1161
|
+
private findCadrePeerRow;
|
|
1003
1162
|
/**
|
|
1004
|
-
* 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.
|
|
1005
1165
|
*
|
|
1006
1166
|
* Authorization is carried entirely by the record: the `Sig` column (verified
|
|
1007
1167
|
* by the `AuthorizedUpdate` self-branch against the stored `PublicKey`) plus
|
|
@@ -1010,6 +1170,21 @@ export declare class ControlDatabase {
|
|
|
1010
1170
|
* exists. `PublicKey` is intentionally not in the SET list (it is immutable on
|
|
1011
1171
|
* self-update and the constraint enforces `new.PublicKey = old.PublicKey`).
|
|
1012
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
|
+
*
|
|
1013
1188
|
* Deliberately NOT wrapped in {@link mutateCadrePeer}, the one `CadrePeer` mutator that
|
|
1014
1189
|
* is not: it only ever touches THIS node's own row (sole caller
|
|
1015
1190
|
* `CadreNode.publishSelfRecord`), and `CadreNode.listAuthorizedMembers` filters self out
|
|
@@ -1022,8 +1197,11 @@ export declare class ControlDatabase {
|
|
|
1022
1197
|
* row, and a self insert happens once at startup, so the wasted refresh is a single read
|
|
1023
1198
|
* — cheaper than a conditional carve-out inside the shared writer. Only the repeating
|
|
1024
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.
|
|
1025
1203
|
*/
|
|
1026
|
-
updateSelfPeerRecord(
|
|
1204
|
+
updateSelfPeerRecord(peerId: string, sign: SelfRecordSigner): Promise<PeerAddressRecord | null>;
|
|
1027
1205
|
/**
|
|
1028
1206
|
* Read a single peer's device push token (the full `DeviceToken` row) by PeerId.
|
|
1029
1207
|
*
|
|
@@ -1146,6 +1324,13 @@ export declare class ControlDatabase {
|
|
|
1146
1324
|
* body, and a row whose stamp is already retired (held without its delete) is skipped rather
|
|
1147
1325
|
* than tombstoned twice. Nothing to remove writes nothing and returns all false.
|
|
1148
1326
|
*
|
|
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.
|
|
1333
|
+
*
|
|
1149
1334
|
* The caller decides which invitations the machine could use, from its plan
|
|
1150
1335
|
* (`CadreNode.planMachineRemoval`).
|
|
1151
1336
|
*/
|
|
@@ -1180,6 +1365,28 @@ export declare class ControlDatabase {
|
|
|
1180
1365
|
* @param memberPrivateKey - Optional private key for membership in closed strands
|
|
1181
1366
|
*/
|
|
1182
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>;
|
|
1183
1390
|
/**
|
|
1184
1391
|
* Delete a strand from the control database using an owner signature.
|
|
1185
1392
|
*
|
|
@@ -1238,6 +1445,8 @@ export declare class ControlDatabase {
|
|
|
1238
1445
|
* with the owner's private key, returning a base64url signature.
|
|
1239
1446
|
*/
|
|
1240
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;
|
|
1241
1450
|
/**
|
|
1242
1451
|
* Owner-signed removal of one `StrandPartyKey` row — the party's own membership
|
|
1243
1452
|
* identity for that strand, stored nowhere else, so this is as destructive as
|
|
@@ -1405,32 +1614,44 @@ export declare class ControlDatabase {
|
|
|
1405
1614
|
profile: NodeProfile | null;
|
|
1406
1615
|
}, ownerKey: string, signMessage: ControlSign): Promise<boolean>;
|
|
1407
1616
|
/**
|
|
1408
|
-
* Owner "re-touch" of an existing `CadrePeer` membership row:
|
|
1409
|
-
*
|
|
1410
|
-
*
|
|
1411
|
-
* the row is re-emitted as a fresh, broadcasting
|
|
1412
|
-
*
|
|
1413
|
-
*
|
|
1414
|
-
*
|
|
1415
|
-
*
|
|
1416
|
-
*
|
|
1417
|
-
*
|
|
1418
|
-
*
|
|
1419
|
-
*
|
|
1420
|
-
*
|
|
1421
|
-
* `
|
|
1422
|
-
*
|
|
1423
|
-
*
|
|
1424
|
-
*
|
|
1425
|
-
*
|
|
1426
|
-
*
|
|
1427
|
-
*
|
|
1428
|
-
*
|
|
1429
|
-
*
|
|
1430
|
-
*
|
|
1431
|
-
*
|
|
1432
|
-
*
|
|
1433
|
-
*
|
|
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.
|
|
1434
1655
|
*
|
|
1435
1656
|
* Notifies like the insert/remove paths even though this is "only" a re-touch: it rewrites
|
|
1436
1657
|
* VouchOwner/VouchSig, which the authorized-membership predicate judges on, so it CAN
|
|
@@ -1453,12 +1674,13 @@ export declare class ControlDatabase {
|
|
|
1453
1674
|
* VouchOwner/VouchSig) rather than inherit this rebinding.
|
|
1454
1675
|
*
|
|
1455
1676
|
* @param peerId - the membership row to re-touch.
|
|
1456
|
-
* @param
|
|
1677
|
+
* @param mode - re-broadcast or re-vouch ({@link CadrePeerRetouchMode}).
|
|
1457
1678
|
* @param ownerKey - public key of the re-vouching owner, rewritten into `VouchOwner`.
|
|
1458
|
-
* @param signMessage - ed25519-signs the raw message bytes with that owner's private key
|
|
1459
|
-
*
|
|
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}).
|
|
1460
1682
|
*/
|
|
1461
|
-
reauthorizeCadrePeer(peerId: string,
|
|
1683
|
+
reauthorizeCadrePeer(peerId: string, mode: CadrePeerRetouchMode, ownerKey: string, signMessage: ControlSign): Promise<CadrePeerRetouchOutcome>;
|
|
1462
1684
|
/**
|
|
1463
1685
|
* Owner-signed removal of one `CadrePeer` membership row, notifying the membership
|
|
1464
1686
|
* listener once it has committed.
|
|
@@ -1478,6 +1700,12 @@ export declare class ControlDatabase {
|
|
|
1478
1700
|
*
|
|
1479
1701
|
* A no-op (no throw, no tombstone) when the row does not exist — `false` then, `true`
|
|
1480
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.
|
|
1481
1709
|
*/
|
|
1482
1710
|
deleteCadrePeer(peerId: string, ownerKey: string, signMessage: ControlSign): Promise<boolean>;
|
|
1483
1711
|
/**
|
|
@@ -1539,9 +1767,8 @@ export declare class ControlDatabase {
|
|
|
1539
1767
|
/**
|
|
1540
1768
|
* File the owner-signed `Revocation` tombstone retiring one row incarnation — the second
|
|
1541
1769
|
* statement of every guarded removal ({@link execGuardedRemoval}) and the whole of an
|
|
1542
|
-
* issued row's withdrawal ({@link withdrawIssuedRow}), whose row stays
|
|
1543
|
-
*
|
|
1544
|
-
* lock and, where one is needed, the transaction.
|
|
1770
|
+
* issued row's withdrawal ({@link withdrawIssuedRow}), whose row stays. The caller
|
|
1771
|
+
* supplies the lock and, where one is needed, the transaction.
|
|
1545
1772
|
*
|
|
1546
1773
|
* `ReissuedAt` is named explicitly at 0 (the only value `FreshTombstone` accepts) rather
|
|
1547
1774
|
* than leaning on the column default — the seat-at-zero rule is load-bearing for
|
|
@@ -1579,9 +1806,10 @@ export declare class ControlDatabase {
|
|
|
1579
1806
|
* statement, and without the clause the reap would delete the owner's brand-new row.
|
|
1580
1807
|
* With it the statement matches nothing and the reap is a silent no-op (the schema
|
|
1581
1808
|
* branch would also refuse that delete, but as a constraint error thrown into a
|
|
1582
|
-
* background sweep — matched-nothing is the better failure mode).
|
|
1583
|
-
* {@link reauthorizeCadrePeer},
|
|
1584
|
-
* 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.
|
|
1585
1813
|
*
|
|
1586
1814
|
* Writes NOTHING to `Revocation`: `RevocationRecorded` is satisfied by the same
|
|
1587
1815
|
* committed tombstone that authorizes the reap, so a reap files no second tombstone.
|
|
@@ -1608,7 +1836,7 @@ export declare class ControlDatabase {
|
|
|
1608
1836
|
* present clause are the shape {@link redeemInvitation}'s consent-branch insert ships).
|
|
1609
1837
|
*
|
|
1610
1838
|
* Driven automatically by {@link reapRevokedRows}, which the periodic control-cohort
|
|
1611
|
-
* reconcile pass runs while
|
|
1839
|
+
* reconcile pass runs while a sibling is reachable (`CadreNode.hasReachableControlSibling`).
|
|
1612
1840
|
*/
|
|
1613
1841
|
reapRevokedRow(table: ReapableTable, rowKey: string, stampId: string): Promise<boolean>;
|
|
1614
1842
|
/**
|
|
@@ -1619,8 +1847,8 @@ export declare class ControlDatabase {
|
|
|
1619
1847
|
* carry the delete (replaying a delete of an already-absent row is a no-op), so a node
|
|
1620
1848
|
* that converged on a removal while holding the removed row keeps that row as inert
|
|
1621
1849
|
* garbage until something walks the tombstones and drops it. `CadreNode`'s periodic
|
|
1622
|
-
* control-cohort reconcile pass is that something, gated on
|
|
1623
|
-
*
|
|
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).
|
|
1624
1852
|
*
|
|
1625
1853
|
* Enumerated UNLOCKED, deleted per-row LOCKED: {@link reapRevokedRow} takes the write
|
|
1626
1854
|
* lock itself (via {@link mutateCadrePeer} / {@link execWrite}) and the lock is not
|
|
@@ -1653,12 +1881,11 @@ export declare class ControlDatabase {
|
|
|
1653
1881
|
*
|
|
1654
1882
|
* NOTE: cost is O(tombstones), not O(live rows) — one {@link queryRevocations} scan plus
|
|
1655
1883
|
* one {@link queryStampId} point lookup per tombstone, and the empty early return makes
|
|
1656
|
-
* the common case (a party that has never revoked anyone
|
|
1657
|
-
*
|
|
1658
|
-
*
|
|
1659
|
-
* ({@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.
|
|
1660
1887
|
* But `Revocation` is append-only and unbounded, so this is O(all tombstones ever) point
|
|
1661
|
-
* lookups on every reconcile
|
|
1888
|
+
* lookups on every connected reconcile pass. Fine while revocations stay rare for a cadre-sized
|
|
1662
1889
|
* party; if the table ever grows, persist a node-local high-water mark of what has
|
|
1663
1890
|
* already been reaped instead of re-scanning everything (the same bound the sweep in
|
|
1664
1891
|
* `CadreNode.drainPendingRevocations` wants).
|
|
@@ -1693,44 +1920,6 @@ export declare class ControlDatabase {
|
|
|
1693
1920
|
* Returns how many UPDATE statements ran (`rows.length` on success).
|
|
1694
1921
|
*/
|
|
1695
1922
|
reissueRevocations(rows: readonly RevocationRow[], reissuedAt: number, ownerKey: string, signMessage: ControlSign): Promise<number>;
|
|
1696
|
-
/**
|
|
1697
|
-
* Owner-signed filing of the singleton `Revocation` ledger marker
|
|
1698
|
-
* ({@link REVOCATION_LEDGER_MARKER}), the one row in that table that retires nothing.
|
|
1699
|
-
*
|
|
1700
|
-
* Why it exists: while `Revocation` has never been written, this node does not hold its
|
|
1701
|
-
* block, and Optimystic consults a block's cohort on EVERY read of a block it does not
|
|
1702
|
-
* hold — and every membership lookup, and every guarded insert's `NotRevoked` check,
|
|
1703
|
-
* reads this table. Once any row exists the block is held and re-checked at most once per
|
|
1704
|
-
* read-repair window, like every other populated table. The schema's table comment says
|
|
1705
|
-
* why the row can never read as a retirement.
|
|
1706
|
-
*
|
|
1707
|
-
* Insert-if-absent, shaped like {@link reissueRevocations}: the signature is minted
|
|
1708
|
-
* OUTSIDE the locked body so a retried attempt re-presents the same one, and the guard
|
|
1709
|
-
* runs INSIDE it so it sees a concurrent local writer's committed row. The guard scans
|
|
1710
|
-
* the `'Revocation'` rows and compares the stamp in TypeScript rather than seeking the
|
|
1711
|
-
* full composite primary key, which is served as a point lookup that can miss an existing
|
|
1712
|
-
* row on a networked database (tickets/backlog/debt-composite-pk-point-lookup-unreliable-untracked).
|
|
1713
|
-
*
|
|
1714
|
-
* Two owners filing at once: the loser either sees the marker in its guard or is refused
|
|
1715
|
-
* on the primary key, and both answer `'already-open'`.
|
|
1716
|
-
*
|
|
1717
|
-
* The caller decides WHEN. A marker committed while the node is alone is local-only and
|
|
1718
|
-
* can fork the collection, so {@link CadreNode} files it only while connected; this
|
|
1719
|
-
* method does not look at connectivity.
|
|
1720
|
-
*
|
|
1721
|
-
* @param ownerKey - owner public key for the write context (`Revocation.Authorized`).
|
|
1722
|
-
* @param signMessage - ed25519-signs the raw message bytes (no pre-hash) with that owner's
|
|
1723
|
-
* private key, returning a base64url signature.
|
|
1724
|
-
* @returns `'opened'` when this call filed the marker, `'already-open'` when it was
|
|
1725
|
-
* already there.
|
|
1726
|
-
*/
|
|
1727
|
-
openRevocationLedger(ownerKey: string, signMessage: ControlSign): Promise<RevocationLedgerOpenResult>;
|
|
1728
|
-
/**
|
|
1729
|
-
* Whether the ledger marker is present locally: a scan of the `'Revocation'` rows with the
|
|
1730
|
-
* stamp compared in TypeScript (see {@link openRevocationLedger} for why not a seek).
|
|
1731
|
-
* Called only inside the locked write body, so never retried (see queryStampId's NOTE).
|
|
1732
|
-
*/
|
|
1733
|
-
private revocationLedgerFiled;
|
|
1734
1923
|
/**
|
|
1735
1924
|
* Run `body` between `beginTransaction` and `commit`, rolling back on failure.
|
|
1736
1925
|
*
|
|
@@ -1762,8 +1951,21 @@ export declare class ControlDatabase {
|
|
|
1762
1951
|
* {@link CadreNode}, which wires this in `start()` and clears it on teardown. A
|
|
1763
1952
|
* second call replaces the first rather than fanning out, so a stale node can
|
|
1764
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.
|
|
1765
1960
|
*/
|
|
1766
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;
|
|
1767
1969
|
/**
|
|
1768
1970
|
* Wire (or clear, with null) the single listener notified after every committed
|
|
1769
1971
|
* guarded-table delete. Same ownership contract as
|
|
@@ -1818,8 +2020,11 @@ export declare class ControlDatabase {
|
|
|
1818
2020
|
* exhaustion.
|
|
1819
2021
|
*
|
|
1820
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).
|
|
1821
2026
|
*/
|
|
1822
|
-
mutateCadrePeer<T>(reason: string, body: () => Promise<T
|
|
2027
|
+
mutateCadrePeer<T>(reason: string, body: () => Promise<T>, policy?: ControlWriteRetryOptions): Promise<T>;
|
|
1823
2028
|
/**
|
|
1824
2029
|
* Run one local write under the database-wide write lock, serializing it against
|
|
1825
2030
|
* every other local writer on this ControlDatabase.
|
|
@@ -1832,8 +2037,8 @@ export declare class ControlDatabase {
|
|
|
1832
2037
|
* class: every public write method of this class runs its statement(s) under it, and
|
|
1833
2038
|
* `SeedBootstrapService` reaches it through {@link execWrite} rather than touching the
|
|
1834
2039
|
* `Database` directly, so it is covered too. The race is
|
|
1835
|
-
* real, not theoretical: the background self-record publish
|
|
1836
|
-
*
|
|
2040
|
+
* real, not theoretical: the background self-record publish when a sibling becomes
|
|
2041
|
+
* reachable (`CadreNode.drainPendingControlReplication` → `registerSelf`) collided with
|
|
1837
2042
|
* a foreground `authorizePeer` — both `CadrePeer` inserts — and tripped the boundary
|
|
1838
2043
|
* assert.
|
|
1839
2044
|
*
|
|
@@ -1857,6 +2062,11 @@ export declare class ControlDatabase {
|
|
|
1857
2062
|
* re-runnable means a retry re-runs the body's reads too, which is what makes it safe.
|
|
1858
2063
|
* Signatures and stamp ids minted OUTSIDE the body are deliberately NOT re-minted per
|
|
1859
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}).
|
|
1860
2070
|
*/
|
|
1861
2071
|
withWriteLock<T>(fn: () => Promise<T>): Promise<T>;
|
|
1862
2072
|
/**
|
|
@@ -1882,9 +2092,14 @@ export declare class ControlDatabase {
|
|
|
1882
2092
|
* `initialized` is set — so it reaches this method directly rather than through
|
|
1883
2093
|
* {@link execWrite}, whose {@link ensureInitialized} would reject it. The write queue is
|
|
1884
2094
|
* necessarily empty at that point, so the lock is uncontended there; it is taken anyway
|
|
1885
|
-
* rather than carving out a lockless path.
|
|
1886
|
-
*
|
|
1887
|
-
*
|
|
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.
|
|
1888
2103
|
*
|
|
1889
2104
|
* `label` names the operation in this loop's debug lines and nothing else
|
|
1890
2105
|
* ({@link ControlWriteRetryOptions.label}). Every call site in this class supplies one:
|
|
@@ -1987,6 +2202,11 @@ export declare class ControlDatabase {
|
|
|
1987
2202
|
validationUrl?: string;
|
|
1988
2203
|
strandId?: string;
|
|
1989
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;
|
|
1990
2210
|
/**
|
|
1991
2211
|
* Redeem a `FormationInvite` by inserting the `Strand` row and a matching
|
|
1992
2212
|
* `FormationUsage` row **atomically, in one transaction**.
|
|
@@ -2057,14 +2277,13 @@ export declare class ControlDatabase {
|
|
|
2057
2277
|
* Use {@link redeemInvitation} instead when the strand must be created by
|
|
2058
2278
|
* consent atomically with the usage.
|
|
2059
2279
|
*
|
|
2060
|
-
* The strand's live `StampId` is
|
|
2280
|
+
* The strand's live `StampId` is written onto the usage row:
|
|
2061
2281
|
* `FormationUsage.StrandExists` matches the (id, stamp) PAIR, and
|
|
2062
2282
|
* `Strand.AuthorizedInsert`'s consent branch reads the same pair back, so a consent
|
|
2063
|
-
* record authorizes exactly the strand ROW it was recorded against. A
|
|
2064
|
-
*
|
|
2065
|
-
*
|
|
2066
|
-
*
|
|
2067
|
-
* 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.
|
|
2068
2287
|
*
|
|
2069
2288
|
* The row is keyed by the joiner's own nonce (`UsageStampId` is the primary key), so this
|
|
2070
2289
|
* path — the one production actually races on (a bound invite published by `cadre-web` /
|
|
@@ -2099,6 +2318,13 @@ export declare class ControlDatabase {
|
|
|
2099
2318
|
* {@link queryFormationInvite} read; `null` means "no invite row" / unlimited.
|
|
2100
2319
|
*/
|
|
2101
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;
|
|
2102
2328
|
}): Promise<FormationUsageResult>;
|
|
2103
2329
|
/**
|
|
2104
2330
|
* Refuse a redemption that would consume a seat the invite does not have.
|
|
@@ -2152,7 +2378,7 @@ export declare class ControlDatabase {
|
|
|
2152
2378
|
* permissive PRE-check that runs ahead of it, and a transiently short read costs each of
|
|
2153
2379
|
* them only a worse outcome for the ATTEMPT, never a seat the invitation did not pay for:
|
|
2154
2380
|
* {@link assertSeatRemains} loses its named exhaustion error and falls back to the CHECK's
|
|
2155
|
-
* generic refusal; `ControlFormationUsageRecorder.
|
|
2381
|
+
* generic refusal; `ControlFormationUsageRecorder.readToken` reports a seat free and lets
|
|
2156
2382
|
* the redemption proceed to the CHECK, which decides; {@link hasOutstandingFormationInvite}
|
|
2157
2383
|
* holds the stranger-admission door open slightly longer.
|
|
2158
2384
|
*
|
|
@@ -2178,12 +2404,12 @@ export declare class ControlDatabase {
|
|
|
2178
2404
|
/**
|
|
2179
2405
|
* Is any `FormationInvite` row still redeemable — unexpired AND with usage
|
|
2180
2406
|
* below its `TotalUses`? A null `ExpiresAt` never expires and a null
|
|
2181
|
-
* `TotalUses` is unlimited, matching
|
|
2182
|
-
*
|
|
2407
|
+
* `TotalUses` is unlimited, matching the per-token semantics of `classifyTokenRead`
|
|
2408
|
+
* (`formation-token.ts`).
|
|
2183
2409
|
*
|
|
2184
2410
|
* Answers the control-network connection gate's coarse "does this node expect
|
|
2185
2411
|
* a stranger?" question, which has no token to ask about. The expiry
|
|
2186
|
-
* comparison is `expiresAtMs <= now` — identical to `
|
|
2412
|
+
* comparison is `expiresAtMs <= now` — identical to `isInviteExpired`'s — so an
|
|
2187
2413
|
* invite the formation handler would reject can never hold the gate open.
|
|
2188
2414
|
*
|
|
2189
2415
|
* The scan is deliberately not pushed into SQL: nothing else here compares a
|
|
@@ -2263,8 +2489,10 @@ export declare class ControlDatabase {
|
|
|
2263
2489
|
* the row is the holder's proof and never goes away; liveness is {@link hasLiveCadreInvite}'s
|
|
2264
2490
|
* question, and `CadreInviteUsage.Authorized`'s at redemption.
|
|
2265
2491
|
*
|
|
2266
|
-
*
|
|
2267
|
-
*
|
|
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.
|
|
2268
2496
|
*/
|
|
2269
2497
|
queryCadreInvite(key: string, retry?: boolean): Promise<CadreInviteRow | null>;
|
|
2270
2498
|
/**
|
|
@@ -2278,7 +2506,7 @@ export declare class ControlDatabase {
|
|
|
2278
2506
|
/**
|
|
2279
2507
|
* Every `CadreInviteUsage` row this node holds — the redemption records the membership
|
|
2280
2508
|
* predicate verifies invitation-admitted rows against. Same `retry` contract as
|
|
2281
|
-
* {@link queryCadreInvites}.
|
|
2509
|
+
* {@link queryCadreInvites}; both come from their membership views ({@link readMembershipRows}).
|
|
2282
2510
|
*/
|
|
2283
2511
|
queryCadreInviteUsages(retry?: boolean): Promise<CadreInviteUsageRow[]>;
|
|
2284
2512
|
/**
|
|
@@ -2306,14 +2534,19 @@ export declare class ControlDatabase {
|
|
|
2306
2534
|
hasLiveCadreInvite(nowMs?: number): Promise<boolean>;
|
|
2307
2535
|
/**
|
|
2308
2536
|
* Is this one cadre invitation still redeemable here — the conditions
|
|
2309
|
-
* {@link hasLiveCadreInvite} applies, for one invitation? The row judged is the one this
|
|
2310
|
-
* holds under `invite.key
|
|
2311
|
-
* yet), but only if its issuer signature
|
|
2312
|
-
* judged by whichever owner it names as
|
|
2313
|
-
* {@link countCadreInviteUsage}: {@link seatCadreInvite}
|
|
2314
|
-
* 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.
|
|
2315
2548
|
*/
|
|
2316
|
-
isCadreInviteLive(invite: CadreInviteRow, nowMs?: number): Promise<boolean>;
|
|
2549
|
+
isCadreInviteLive(invite: CadreInviteRow, held: CadreInviteRow | null, nowMs?: number): Promise<boolean>;
|
|
2317
2550
|
/**
|
|
2318
2551
|
* The body of {@link isCadreInviteLive} and {@link isOwnerGrantLive}: is `row` (the one held,
|
|
2319
2552
|
* or a verified bundle copy; null for neither) still open and under its use cap?
|
|
@@ -2394,11 +2627,27 @@ export declare class ControlDatabase {
|
|
|
2394
2627
|
*/
|
|
2395
2628
|
redeemCadreInvite(params: {
|
|
2396
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;
|
|
2397
2639
|
peerId: string;
|
|
2398
2640
|
/** The device's ed25519 public key (base64url); `peerId` is its identity multihash. */
|
|
2399
2641
|
peerKey: string;
|
|
2400
2642
|
/** The device's dialable addresses, if known; stored comma-joined as `authorizePeer` does. */
|
|
2401
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'>;
|
|
2402
2651
|
/** Single-use nonce minted by the device; both signatures cover it. */
|
|
2403
2652
|
usageStampId: string;
|
|
2404
2653
|
/** The holder's signature with the invitation private key over the `'redeem'` digest. */
|
|
@@ -2458,7 +2707,9 @@ export declare class ControlDatabase {
|
|
|
2458
2707
|
/**
|
|
2459
2708
|
* Read one `OwnerGrant` row by key, or null when absent. Withdrawn rows are returned too,
|
|
2460
2709
|
* as {@link queryCadreInvite} returns them: the row is the holder's proof and never goes
|
|
2461
|
-
* away.
|
|
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.
|
|
2462
2713
|
*/
|
|
2463
2714
|
queryOwnerGrant(key: string, retry?: boolean): Promise<OwnerGrantRow | null>;
|
|
2464
2715
|
/**
|
|
@@ -2467,7 +2718,7 @@ export declare class ControlDatabase {
|
|
|
2467
2718
|
* `retry: false` is forwarded from the membership-gate refresh for {@link queryCadrePeers}' reason.
|
|
2468
2719
|
*/
|
|
2469
2720
|
queryOwnerGrants(retry?: boolean): Promise<OwnerGrantRow[]>;
|
|
2470
|
-
/** Every `OwnerGrantUsage` row this node holds — the redemption records a grant-seated `OwnerKey` row is verified against. Same `retry` contract as {@link queryOwnerGrants}. */
|
|
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}). */
|
|
2471
2722
|
queryOwnerGrantUsages(retry?: boolean): Promise<OwnerGrantUsageRow[]>;
|
|
2472
2723
|
/**
|
|
2473
2724
|
* How many times an owner grant has been redeemed (its `OwnerGrantUsage` rows), served
|
|
@@ -2477,12 +2728,14 @@ export declare class ControlDatabase {
|
|
|
2477
2728
|
countOwnerGrantUsage(key: string, retry?: boolean): Promise<number>;
|
|
2478
2729
|
/**
|
|
2479
2730
|
* Is this one owner grant still redeemable here — not withdrawn, unexpired, uses left, issuer
|
|
2480
|
-
* still an owner — judged as {@link isCadreInviteLive} judges an invitation: by the row
|
|
2481
|
-
* node holds under `grant.key
|
|
2482
|
-
* when its issuer signature verifies. A
|
|
2483
|
-
* redemption's own write enforce every
|
|
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.
|
|
2484
2737
|
*/
|
|
2485
|
-
isOwnerGrantLive(grant: OwnerGrantRow, nowMs?: number): Promise<boolean>;
|
|
2738
|
+
isOwnerGrantLive(grant: OwnerGrantRow, held: OwnerGrantRow | null, nowMs?: number): Promise<boolean>;
|
|
2486
2739
|
/**
|
|
2487
2740
|
* Every `OwnerGrant` row this node holds with its standing — recorded redemptions, withdrawal,
|
|
2488
2741
|
* and whether it is still redeemable — for an owner's listing, as {@link listCadreInviteStatuses}.
|
|
@@ -2534,6 +2787,8 @@ export declare class ControlDatabase {
|
|
|
2534
2787
|
*/
|
|
2535
2788
|
redeemOwnerGrant(params: {
|
|
2536
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;
|
|
2537
2792
|
/** The owner key to seat (base64url ed25519): the holder generated it, or already holds it. */
|
|
2538
2793
|
seatedKey: string;
|
|
2539
2794
|
/** Single-use nonce minted by the holder; both signatures cover it. */
|
|
@@ -2549,7 +2804,10 @@ export declare class ControlDatabase {
|
|
|
2549
2804
|
/**
|
|
2550
2805
|
* Close the database and cleanup resources.
|
|
2551
2806
|
*
|
|
2552
|
-
*
|
|
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
|
|
2553
2811
|
* others across an await, and a queued closure evaluates `this.db!` only when it
|
|
2554
2812
|
* finally runs — so nulling the handle out from under it would throw a TypeError on a
|
|
2555
2813
|
* null handle instead of committing. The tail never rejects (it swallows both
|