@serfab/cadre-core 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +255 -255
- package/dist/announce-addrs.d.ts +49 -0
- package/dist/announce-addrs.js +78 -0
- package/dist/announce-addrs.js.map +1 -0
- package/dist/cadre-node.d.ts +484 -58
- package/dist/cadre-node.js +953 -182
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-cohort.d.ts +26 -0
- package/dist/control-cohort.js +26 -0
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +222 -125
- package/dist/control-database.js +356 -266
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +8 -7
- package/dist/control-formation-recorder.js +8 -7
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-read-retry.d.ts +103 -0
- package/dist/control-read-retry.js +145 -0
- package/dist/control-read-retry.js.map +1 -0
- package/dist/control-retry.d.ts +101 -0
- package/dist/control-retry.js +108 -0
- package/dist/control-retry.js.map +1 -0
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +805 -774
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +65 -31
- package/dist/control-write-retry.js +33 -103
- package/dist/control-write-retry.js.map +1 -1
- package/dist/delegate-admission.d.ts +17 -15
- package/dist/delegate-admission.js +17 -15
- package/dist/delegate-admission.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.js +18 -9
- package/dist/index.js.map +1 -1
- package/dist/membership-connection-gater.d.ts +211 -25
- package/dist/membership-connection-gater.js +309 -29
- package/dist/membership-connection-gater.js.map +1 -1
- package/dist/peer-addr-book.d.ts +92 -0
- package/dist/peer-addr-book.js +220 -0
- package/dist/peer-addr-book.js.map +1 -0
- package/dist/peer-record.d.ts +26 -0
- package/dist/peer-record.js +32 -0
- package/dist/peer-record.js.map +1 -1
- package/dist/relay-addrs.d.ts +76 -26
- package/dist/relay-addrs.js +115 -27
- package/dist/relay-addrs.js.map +1 -1
- package/dist/relay-reservation.d.ts +20 -15
- package/dist/relay-reservation.js +18 -14
- package/dist/relay-reservation.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +7 -0
- package/dist/seed-bootstrap.js +15 -13
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/strand-addr-protocol.js +16 -0
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-backfill.js +13 -4
- package/dist/strand-backfill.js.map +1 -1
- package/dist/strand-cohort.d.ts +4 -37
- package/dist/strand-cohort.js +4 -15
- package/dist/strand-cohort.js.map +1 -1
- package/dist/strand-database.d.ts +11 -16
- package/dist/strand-database.js +19 -7
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-formation-manager.d.ts +18 -17
- package/dist/strand-formation-manager.js +25 -27
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +7 -7
- package/dist/strand-formation-protocol.js +8 -8
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +53 -21
- package/dist/strand-instance-manager.js +128 -37
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +113 -19
- package/dist/strand-membership-writer.js +219 -51
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-solicitation.d.ts +3 -3
- package/dist/strand-solicitation.js +4 -5
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-wake-protocol.d.ts +47 -2
- package/dist/strand-wake-protocol.js +76 -6
- package/dist/strand-wake-protocol.js.map +1 -1
- package/dist/types.d.ts +107 -54
- package/dist/types.js.map +1 -1
- package/package.json +8 -7
- package/src/announce-addrs.ts +93 -0
- package/src/arachnode-stub.ts +159 -159
- package/src/bootstrap-peer-store-file.ts +44 -44
- package/src/bootstrap-peer-store.ts +172 -172
- package/src/cadre-node.ts +1026 -194
- package/src/canonical-json.ts +30 -30
- package/src/control-authorization.ts +114 -114
- package/src/control-cohort.ts +130 -103
- package/src/control-database.ts +376 -298
- package/src/control-formation-recorder.ts +305 -304
- package/src/control-read-retry.ts +167 -0
- package/src/control-retry.ts +190 -0
- package/src/control-schema.ts +817 -786
- package/src/control-stream.ts +205 -205
- package/src/control-write-retry.ts +383 -473
- package/src/delegate-admission.ts +304 -302
- package/src/device-token.ts +94 -94
- package/src/diagnostics/connection-path.ts +277 -277
- package/src/diagnostics/webrtc-turn-tracker.ts +222 -222
- package/src/ed25519-key.ts +134 -134
- package/src/enrollment.ts +199 -199
- package/src/file-durable-slot.ts +61 -61
- package/src/formation-approval.ts +604 -604
- package/src/fs-atomic.ts +107 -107
- package/src/hibernation-manager.ts +437 -437
- package/src/identity-key.ts +109 -109
- package/src/index.ts +435 -406
- package/src/key-store-file.ts +132 -132
- package/src/key-store.ts +99 -99
- package/src/membership-connection-gater.ts +368 -39
- package/src/node-local-snapshot.ts +260 -260
- package/src/peer-addr-book.ts +250 -0
- package/src/peer-authorization.ts +259 -259
- package/src/peer-record.ts +197 -164
- package/src/push-credentials.ts +84 -84
- package/src/push-node.ts +64 -64
- package/src/push-notifier-apns.ts +247 -247
- package/src/push-notifier-fcm.ts +220 -220
- package/src/push-notifier-shared.ts +28 -28
- package/src/push-notifier.ts +52 -52
- package/src/relay-addrs.ts +134 -27
- package/src/relay-reservation.ts +22 -17
- package/src/seed-bootstrap.ts +15 -15
- package/src/seed-trust-policy.ts +140 -140
- package/src/strand-addr-protocol.ts +458 -441
- package/src/strand-backfill.ts +28 -5
- package/src/strand-cohort.ts +38 -77
- package/src/strand-database.ts +202 -204
- package/src/strand-formation-manager.ts +26 -28
- package/src/strand-formation-protocol.ts +692 -692
- package/src/strand-instance-manager.ts +636 -553
- package/src/strand-member-key.ts +39 -39
- package/src/strand-member-registry.ts +182 -182
- package/src/strand-membership-writer.ts +1627 -1430
- package/src/strand-solicitation.ts +471 -472
- package/src/strand-transport-key.ts +58 -58
- package/src/strand-wake-payload.ts +32 -32
- package/src/strand-wake-protocol.ts +400 -314
- package/src/strand-watcher.ts +314 -314
- package/src/trusted-owner-store-file.ts +41 -41
- package/src/trusted-owner-store.ts +171 -171
- package/src/types.ts +1354 -1303
|
@@ -414,8 +414,8 @@ export interface RevokeMemberParams {
|
|
|
414
414
|
* A manager must resign its `Manager` row before (or in the same transaction
|
|
415
415
|
* as) losing membership — `Member.NotAManager` rejects un-membering a key that
|
|
416
416
|
* still holds a Manager row. And the strand never drops to zero members
|
|
417
|
-
* (`Member.MinOneMember`, a local-count floor with the
|
|
418
|
-
*
|
|
417
|
+
* (`Member.MinOneMember`, a local-count floor with the cross-node caveat its
|
|
418
|
+
* schema NOTE states).
|
|
419
419
|
*
|
|
420
420
|
* @param db - The closed strand's database.
|
|
421
421
|
* @param params - The revoking manager keypair and the target member key.
|
|
@@ -722,18 +722,26 @@ export interface RemoveManagerParams {
|
|
|
722
722
|
* The caller selects the case purely by which keypair it passes; the only
|
|
723
723
|
* branching here is the action tag, chosen by comparing the signer to the target.
|
|
724
724
|
*
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
725
|
+
* Deferred (subquery-bearing) CHECK constraints are evaluated on DELETE as well
|
|
726
|
+
* as INSERT by Quereus plus the Optimystic vtab session — enforcement lives above
|
|
727
|
+
* the transactor (proven on the network transactor by
|
|
728
|
+
* `strand-membership-network-transactor-parity.spec.ts`), so `Manager.Authorized`
|
|
729
|
+
* — deferred — IS enforced here: a signer that is neither an existing manager nor
|
|
730
|
+
* the target itself is rejected at commit.
|
|
731
|
+
*
|
|
732
|
+
* THE SOLE MANAGER CANNOT ORDINARILY RESIGN — it SEALS instead. The `'resign'`
|
|
733
|
+
* branch of `Manager.Authorized` requires at least one manager in the POST-delete
|
|
734
|
+
* image, so a resignation that would empty the `Manager` table is schema-rejected;
|
|
735
|
+
* emptying it deliberately is a distinct, self-signed act — {@link sealStrand} —
|
|
736
|
+
* that permanently freezes admission. This writer refuses the sole-manager
|
|
737
|
+
* self-resignation up front with an error naming `sealStrand`; that TS guard is UX,
|
|
738
|
+
* not the trust boundary — the schema rejects a mis-counted `'resign'` approval
|
|
739
|
+
* regardless. Reading the count inside a caller-joined transaction is correct: an
|
|
740
|
+
* add-then-resign hand-off composed in one transaction sees 2 and passes, while
|
|
741
|
+
* the already-rejected delete-then-add swap sees 1 and is refused here as well as
|
|
742
|
+
* by the schema. The bootstrap branch of `Manager.Authorized` is gated to INSERTs
|
|
735
743
|
* (`old.MemberKey is null`), so it does not waive the signature check on a delete
|
|
736
|
-
* that drops the count toward
|
|
744
|
+
* that drops the count toward zero: a second-to-last removal is authorized
|
|
737
745
|
* exactly like any other.
|
|
738
746
|
*
|
|
739
747
|
* HAND-OFF ORDER: a sole manager transferring control must ADD the successor FIRST
|
|
@@ -742,14 +750,100 @@ export interface RemoveManagerParams {
|
|
|
742
750
|
* successor's insert has no other manager to authorize it once the sole manager's
|
|
743
751
|
* row is gone.
|
|
744
752
|
*
|
|
745
|
-
* Cross-node caveat: `
|
|
746
|
-
* nodes concurrently removing different managers can each see
|
|
747
|
-
* converge to zero. Noted in the schema
|
|
753
|
+
* Cross-node caveat: the `'resign'`/`'seal'` branches count rows visible to THIS
|
|
754
|
+
* transaction, so two nodes concurrently removing different managers can each see
|
|
755
|
+
* a survivor and still converge to zero — an unintended seal. Noted in the schema
|
|
756
|
+
* (the `Manager` table's seal-propagation NOTE); a cross-node floor is not
|
|
757
|
+
* attempted here.
|
|
748
758
|
*
|
|
749
759
|
* @param db - The closed strand's database.
|
|
750
760
|
* @param params - The authorizing keypair and the target manager key.
|
|
751
|
-
* @throws If
|
|
752
|
-
*
|
|
753
|
-
*
|
|
761
|
+
* @throws If the signer IS the target and it is the sole manager (before writing —
|
|
762
|
+
* the operation the caller wants is {@link sealStrand}), or if `Manager.Authorized`
|
|
763
|
+
* rejects (a signer that is neither another existing manager nor the target
|
|
764
|
+
* itself, or a raw sole-manager `'resign'` whose post-image count is zero); the
|
|
765
|
+
* whole delete+tombstone transaction rolls back.
|
|
754
766
|
*/
|
|
755
767
|
export declare function removeManager(db: Database, params: RemoveManagerParams): Promise<void>;
|
|
768
|
+
/** Parameters for {@link sealStrand}. */
|
|
769
|
+
export interface SealStrandParams {
|
|
770
|
+
/**
|
|
771
|
+
* The SOLE manager's strand keypair. Its `publicKeyB64` must be the one live
|
|
772
|
+
* `Strand.Manager` row; it signs the seal-tagged digest over its own key and
|
|
773
|
+
* live stamp, and files the accompanying tombstone (it is still a committed
|
|
774
|
+
* member, which is what `Revocation.Authorized` checks).
|
|
775
|
+
*/
|
|
776
|
+
managerKeyPair: Ed25519KeyPair;
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Seal the strand: the SOLE manager deliberately steps down, permanently freezing
|
|
780
|
+
* membership.
|
|
781
|
+
*
|
|
782
|
+
* A sealed strand has zero `Manager` rows, and every admission path — issuing or
|
|
783
|
+
* cancelling an invitation, consuming one issued BEFORE the seal
|
|
784
|
+
* (`ConsumedInvite.NotSealed`), admitting a member, promoting a manager — requires
|
|
785
|
+
* one, so nobody can ever be let in again. That freeze is a privacy guarantee to
|
|
786
|
+
* everyone who contributed: no key holds the power to admit a party who would then
|
|
787
|
+
* read the strand's whole history. Sealing is irreversible — the founding branch
|
|
788
|
+
* of `Manager.Authorized` closes for good once any manager stamp is retired into
|
|
789
|
+
* `Strand.Revocation`. What remains possible: members may {@link leaveStrand}
|
|
790
|
+
* (floored by `MinOneMember`), manage their own `MemberPeer` rows, and file
|
|
791
|
+
* `Revocation` tombstones.
|
|
792
|
+
*
|
|
793
|
+
* The `'seal'` branch of `Manager.Authorized` accepts a self-signed delete only
|
|
794
|
+
* when the POST-image manager count is ZERO — a distinct action tag from
|
|
795
|
+
* `'resign'`, so a captured resignation approval can never seal and a raw writer
|
|
796
|
+
* cannot seal by accident. Like {@link removeManager}, the delete pairs with its
|
|
797
|
+
* `Strand.Revocation` tombstone in one transaction.
|
|
798
|
+
*
|
|
799
|
+
* The TS count/identity checks below are UX guards, not the trust boundary — the
|
|
800
|
+
* schema rejects a mis-tagged or mis-counted approval regardless of what this
|
|
801
|
+
* writer (or any raw writer) presents.
|
|
802
|
+
*
|
|
803
|
+
* NOTE: a seal approval is a bearer token for its row incarnation, like every other
|
|
804
|
+
* self-signed approval here — but the `'seal'` branch's post-image count of 0 is
|
|
805
|
+
* satisfied for EVERY row deleted in the same transaction, so N managers each
|
|
806
|
+
* presenting a valid `'seal'` approval seal jointly (pinned as an accepted case in
|
|
807
|
+
* `strand-seal.spec.ts`). Unreachable today: this writer mints a seal signature only
|
|
808
|
+
* while its caller is the SOLE manager, and spends it immediately, so a second
|
|
809
|
+
* manager never has one to combine. If seal-approval MINTING is ever split from
|
|
810
|
+
* spending — offline signing, a two-phase seal, a queued approval — one manager
|
|
811
|
+
* holding another's unspent seal approval could freeze the strand irreversibly
|
|
812
|
+
* without its current consent; re-gate here (or narrow the schema branch) then.
|
|
813
|
+
*
|
|
814
|
+
* @param db - The closed strand's database.
|
|
815
|
+
* @param params - The sole manager's own keypair.
|
|
816
|
+
* @throws If more than one manager exists (the caller wants {@link removeManager}),
|
|
817
|
+
* or the supplied keypair holds no `Manager` row while one exists, or the strand
|
|
818
|
+
* holds no `Manager` row and has never retired one (not founded yet — see
|
|
819
|
+
* {@link isStrandSealed}), or any schema constraint rejects; the whole
|
|
820
|
+
* delete+tombstone transaction rolls back. A strand that is ALREADY sealed logs
|
|
821
|
+
* and returns quietly (restart-safe, matching {@link bootstrapFounderMembership}
|
|
822
|
+
* and {@link removeManager}'s absent-row no-op).
|
|
823
|
+
*/
|
|
824
|
+
export declare function sealStrand(db: Database, params: SealStrandParams): Promise<void>;
|
|
825
|
+
/**
|
|
826
|
+
* Whether the strand is sealed: a CLOSED strand that holds no `Manager` rows AND
|
|
827
|
+
* has retired at least one `Manager` stamp into `Strand.Revocation`.
|
|
828
|
+
*
|
|
829
|
+
* All three conjuncts are load-bearing:
|
|
830
|
+
* - `Header.Type` — an OPEN strand never holds `Manager` rows at all
|
|
831
|
+
* (`Manager.OnlyClosed`), so a bare manager-count test would report every open
|
|
832
|
+
* strand as sealed.
|
|
833
|
+
* - the manager count — sealing IS the empty `Manager` table.
|
|
834
|
+
* - the retired `Manager` stamp — "sealed" means admission is frozen FOREVER, and
|
|
835
|
+
* what makes it permanent is the founding branch of `Manager.Authorized`
|
|
836
|
+
* refusing to re-seat a generation-0 manager once any `Manager` stamp has been
|
|
837
|
+
* retired. Without this conjunct a closed strand that is merely NOT FOUNDED YET
|
|
838
|
+
* reads as sealed: {@link bootstrapFounderMembership} commits `Header`,
|
|
839
|
+
* `Member` and `Manager` as three sequential statements, so the window between
|
|
840
|
+
* the first and the last is exactly "closed, zero managers, still foundable".
|
|
841
|
+
*
|
|
842
|
+
* Reads locally visible rows — a node that has not yet converged on the seal
|
|
843
|
+
* still reports `false` (the schema's seal-propagation NOTE).
|
|
844
|
+
*
|
|
845
|
+
* @param db - The strand's database.
|
|
846
|
+
* @returns `true` iff the strand is closed, holds zero `Manager` rows, and has
|
|
847
|
+
* permanently closed its founding branch.
|
|
848
|
+
*/
|
|
849
|
+
export declare function isStrandSealed(db: Database): Promise<boolean>;
|
|
@@ -175,8 +175,8 @@ async function inStrandTransaction(db, fn) {
|
|
|
175
175
|
*/
|
|
176
176
|
async function insertRevocation(db, tableName, stampId, retiree) {
|
|
177
177
|
const signature = signStrandApproval(['Strand.Revocation', 'retire', tableName, stampId], retiree.privateKeyB64);
|
|
178
|
-
await db.exec(`insert into Strand.Revocation (TableName, StampId)
|
|
179
|
-
with context MemberKey = ?, Signature = ?
|
|
178
|
+
await db.exec(`insert into Strand.Revocation (TableName, StampId)
|
|
179
|
+
with context MemberKey = ?, Signature = ?
|
|
180
180
|
values (?, ?)`, [retiree.publicKeyB64, signature, tableName, stampId]);
|
|
181
181
|
}
|
|
182
182
|
/**
|
|
@@ -192,6 +192,27 @@ async function strandTableCount(db, table) {
|
|
|
192
192
|
}
|
|
193
193
|
return 0;
|
|
194
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Whether any `Strand.Revocation` row has ever retired a `Manager` seat — the
|
|
197
|
+
* exact condition that permanently closes the founding branch of
|
|
198
|
+
* `Manager.Authorized` (see the schema's bootstrap-branch comment). Unfiltered
|
|
199
|
+
* scan + JavaScript comparison, the module's scan-not-seek idiom (rationale on
|
|
200
|
+
* {@link scanMemberPeers}).
|
|
201
|
+
*
|
|
202
|
+
* NOTE: walks the whole append-only `Strand.Revocation` table when no `Manager`
|
|
203
|
+
* tombstone is present (it returns on the first one it finds). Cheap at strand
|
|
204
|
+
* scale; if membership churn ever makes that table large — the growth the
|
|
205
|
+
* `Revocation` schema comment already flags, tracked by `debt-strand-tombstone-reap`
|
|
206
|
+
* — this wants a stored "founding closed" marker rather than a scan per call.
|
|
207
|
+
*/
|
|
208
|
+
async function strandHasManagerRevocation(db) {
|
|
209
|
+
for await (const row of db.eval('select TableName from Strand.Revocation')) {
|
|
210
|
+
if (row.TableName === 'Manager') {
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
195
216
|
/**
|
|
196
217
|
* Insert the singleton `Strand.Header` if absent.
|
|
197
218
|
*
|
|
@@ -206,8 +227,8 @@ async function insertHeaderIfAbsent(db, params) {
|
|
|
206
227
|
log('Header already present for strand %s; skipping', params.strandId);
|
|
207
228
|
return;
|
|
208
229
|
}
|
|
209
|
-
await db.exec(`insert into Strand.Header
|
|
210
|
-
(Id, Type, sAppId, sAppVersion, sAppSchema, sAppSignature, Engine, EngineVersion)
|
|
230
|
+
await db.exec(`insert into Strand.Header
|
|
231
|
+
(Id, Type, sAppId, sAppVersion, sAppSchema, sAppSignature, Engine, EngineVersion)
|
|
211
232
|
values (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
212
233
|
params.strandId,
|
|
213
234
|
params.type,
|
|
@@ -237,8 +258,8 @@ async function insertFounderMemberIfAbsent(db, memberKey, strandId) {
|
|
|
237
258
|
log('Member already present for strand %s; skipping founder Member', strandId);
|
|
238
259
|
return;
|
|
239
260
|
}
|
|
240
|
-
await db.exec(`insert into Strand.Member (Key, StampId)
|
|
241
|
-
with context ManagerKey = null, ManagerSignature = null, MemberSignature = null
|
|
261
|
+
await db.exec(`insert into Strand.Member (Key, StampId)
|
|
262
|
+
with context ManagerKey = null, ManagerSignature = null, MemberSignature = null
|
|
242
263
|
values (?, ?)`, [memberKey, generateStrandStampId()]);
|
|
243
264
|
log('Inserted founding Member for strand %s', strandId);
|
|
244
265
|
}
|
|
@@ -260,14 +281,27 @@ async function insertFounderMemberIfAbsent(db, memberKey, strandId) {
|
|
|
260
281
|
* `Manager.OnlyClosed` is a deferred (subquery) check evaluated at commit. The
|
|
261
282
|
* sequential auto-commit Header→Member→Manager order in
|
|
262
283
|
* {@link bootstrapFounderMembership} satisfies both.
|
|
284
|
+
*
|
|
285
|
+
* SEALED strands are skipped, not re-founded: a strand whose sole manager has
|
|
286
|
+
* {@link sealStrand}ed also has zero `Manager` rows, but its founding branch is
|
|
287
|
+
* closed for good (the schema's bootstrap branch requires no `Manager`-table
|
|
288
|
+
* `Revocation` row to exist). Attempting the founding insert here would be
|
|
289
|
+
* schema-rejected and turn every founder restart of a sealed strand into a
|
|
290
|
+
* loud failure — so this mirrors the schema's own gate and returns quietly.
|
|
291
|
+
* A crash BETWEEN the founding Member and Manager inserts leaves no Revocation
|
|
292
|
+
* rows at all, so that restart still completes the founding.
|
|
263
293
|
*/
|
|
264
294
|
async function insertFounderManagerIfAbsent(db, memberKey, strandId) {
|
|
265
295
|
if (await strandTableCount(db, 'Manager') > 0) {
|
|
266
296
|
log('Manager already present for strand %s; skipping founder Manager', strandId);
|
|
267
297
|
return;
|
|
268
298
|
}
|
|
269
|
-
|
|
270
|
-
|
|
299
|
+
if (await strandHasManagerRevocation(db)) {
|
|
300
|
+
log('Strand %s has retired a manager seat (sealed); skipping founder Manager', strandId);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
await db.exec(`insert into Strand.Manager (MemberKey, Generation, StampId)
|
|
304
|
+
with context ManagerKey = null, Signature = null
|
|
271
305
|
values (?, 0, ?)`, [memberKey, generateStrandStampId()]);
|
|
272
306
|
log('Inserted founding Manager for strand %s', strandId);
|
|
273
307
|
}
|
|
@@ -348,8 +382,8 @@ export async function issueInvite(db, params) {
|
|
|
348
382
|
const payload = `${inviteKey}|${expirationSegment}`;
|
|
349
383
|
const managerSignature = signStrandPayload(payload, managerKeyPair.privateKeyB64);
|
|
350
384
|
const inviteSignature = signStrandPayload(payload, invitePrivateKey);
|
|
351
|
-
await db.exec(`insert into Strand.Invite (Key, Expiration)
|
|
352
|
-
with context ManagerKey = ?, ManagerSignature = ?, InviteSignature = ?
|
|
385
|
+
await db.exec(`insert into Strand.Invite (Key, Expiration)
|
|
386
|
+
with context ManagerKey = ?, ManagerSignature = ?, InviteSignature = ?
|
|
353
387
|
values (?, ?)`, [managerKeyPair.publicKeyB64, managerSignature, inviteSignature, inviteKey, expirationColumn]);
|
|
354
388
|
log('Issued invite %s (expires=%s)', inviteKey, expirationColumn ?? 'never');
|
|
355
389
|
return { inviteKey, invitePrivateKey };
|
|
@@ -409,13 +443,13 @@ export async function consumeInvite(db, params) {
|
|
|
409
443
|
await inStrandTransaction(db, async () => {
|
|
410
444
|
// 1. Member — admitted by the deferred invite branch (the matching
|
|
411
445
|
// ConsumedInvite below), so no manager signature is supplied.
|
|
412
|
-
await db.exec(`insert into Strand.Member (Key, StampId)
|
|
413
|
-
with context ManagerKey = null, ManagerSignature = null, MemberSignature = null
|
|
446
|
+
await db.exec(`insert into Strand.Member (Key, StampId)
|
|
447
|
+
with context ManagerKey = null, ManagerSignature = null, MemberSignature = null
|
|
414
448
|
values (?, ?)`, [memberKey, generateStrandStampId()]);
|
|
415
449
|
// 2. ConsumedInvite — proves possession of the invite private key and that the
|
|
416
450
|
// invite has not expired (NotExpired gate against the canonical Now).
|
|
417
|
-
await db.exec(`insert into Strand.ConsumedInvite (InviteKey, MemberKey)
|
|
418
|
-
with context InviteSignature = ?, Now = ?
|
|
451
|
+
await db.exec(`insert into Strand.ConsumedInvite (InviteKey, MemberKey)
|
|
452
|
+
with context InviteSignature = ?, Now = ?
|
|
419
453
|
values (?, ?)`, [inviteSignature, nowCanonical, inviteKey, memberKey]);
|
|
420
454
|
});
|
|
421
455
|
log('Consumed invite %s -> member %s', inviteKey, memberKey);
|
|
@@ -485,8 +519,8 @@ export async function cancelInvite(db, params) {
|
|
|
485
519
|
return;
|
|
486
520
|
}
|
|
487
521
|
const signature = signStrandApproval(['Strand.CancelledInvite', 'cancel', inviteKey], managerKeyPair.privateKeyB64);
|
|
488
|
-
await db.exec(`insert into Strand.CancelledInvite (InviteKey)
|
|
489
|
-
with context ManagerKey = ?, ManagerSignature = ?
|
|
522
|
+
await db.exec(`insert into Strand.CancelledInvite (InviteKey)
|
|
523
|
+
with context ManagerKey = ?, ManagerSignature = ?
|
|
490
524
|
values (?)`, [managerKeyPair.publicKeyB64, signature, inviteKey]);
|
|
491
525
|
log('Cancelled invite %s by manager %s', inviteKey, managerKeyPair.publicKeyB64);
|
|
492
526
|
}
|
|
@@ -581,8 +615,8 @@ export async function addMemberByManager(db, params) {
|
|
|
581
615
|
const { managerKeyPair, memberKey } = params;
|
|
582
616
|
const stampId = generateStrandStampId();
|
|
583
617
|
const signature = signStrandApproval(['Strand.Member', 'add', memberKey, stampId], managerKeyPair.privateKeyB64);
|
|
584
|
-
await db.exec(`insert into Strand.Member (Key, StampId)
|
|
585
|
-
with context ManagerKey = ?, ManagerSignature = ?, MemberSignature = null
|
|
618
|
+
await db.exec(`insert into Strand.Member (Key, StampId)
|
|
619
|
+
with context ManagerKey = ?, ManagerSignature = ?, MemberSignature = null
|
|
586
620
|
values (?, ?)`, [managerKeyPair.publicKeyB64, signature, memberKey, stampId]);
|
|
587
621
|
log('Admitted member %s by manager %s', memberKey, managerKeyPair.publicKeyB64);
|
|
588
622
|
}
|
|
@@ -619,8 +653,8 @@ export async function addMemberByManager(db, params) {
|
|
|
619
653
|
* A manager must resign its `Manager` row before (or in the same transaction
|
|
620
654
|
* as) losing membership — `Member.NotAManager` rejects un-membering a key that
|
|
621
655
|
* still holds a Manager row. And the strand never drops to zero members
|
|
622
|
-
* (`Member.MinOneMember`, a local-count floor with the
|
|
623
|
-
*
|
|
656
|
+
* (`Member.MinOneMember`, a local-count floor with the cross-node caveat its
|
|
657
|
+
* schema NOTE states).
|
|
624
658
|
*
|
|
625
659
|
* @param db - The closed strand's database.
|
|
626
660
|
* @param params - The revoking manager keypair and the target member key.
|
|
@@ -638,8 +672,8 @@ export async function revokeMember(db, params) {
|
|
|
638
672
|
}
|
|
639
673
|
const signature = signStrandApproval(['Strand.Member', 'remove', memberKey, stampId], managerKeyPair.privateKeyB64);
|
|
640
674
|
await inStrandTransaction(db, async () => {
|
|
641
|
-
await db.exec(`delete from Strand.Member
|
|
642
|
-
with context ManagerKey = ?, ManagerSignature = ?, MemberSignature = null
|
|
675
|
+
await db.exec(`delete from Strand.Member
|
|
676
|
+
with context ManagerKey = ?, ManagerSignature = ?, MemberSignature = null
|
|
643
677
|
where Key = ?`, [managerKeyPair.publicKeyB64, signature, memberKey]);
|
|
644
678
|
await insertRevocation(db, 'Member', stampId, managerKeyPair);
|
|
645
679
|
});
|
|
@@ -674,8 +708,8 @@ export async function leaveStrand(db, params) {
|
|
|
674
708
|
}
|
|
675
709
|
const signature = signStrandApproval(['Strand.Member', 'leave', memberKey, stampId], memberKeyPair.privateKeyB64);
|
|
676
710
|
await inStrandTransaction(db, async () => {
|
|
677
|
-
await db.exec(`delete from Strand.Member
|
|
678
|
-
with context ManagerKey = null, ManagerSignature = null, MemberSignature = ?
|
|
711
|
+
await db.exec(`delete from Strand.Member
|
|
712
|
+
with context ManagerKey = null, ManagerSignature = null, MemberSignature = ?
|
|
679
713
|
where Key = ?`, [signature, memberKey]);
|
|
680
714
|
await insertRevocation(db, 'Member', stampId, memberKeyPair);
|
|
681
715
|
});
|
|
@@ -723,8 +757,8 @@ export async function registerMemberPeer(db, params) {
|
|
|
723
757
|
}
|
|
724
758
|
const stampId = generateStrandStampId();
|
|
725
759
|
const signature = signStrandApproval(['Strand.MemberPeer', 'add', memberKey, peerId, stampId], memberKeyPair.privateKeyB64);
|
|
726
|
-
await db.exec(`insert into Strand.MemberPeer (MemberKey, PeerId, StampId)
|
|
727
|
-
with context Signature = ?, ManagerKey = null, ManagerSignature = null
|
|
760
|
+
await db.exec(`insert into Strand.MemberPeer (MemberKey, PeerId, StampId)
|
|
761
|
+
with context Signature = ?, ManagerKey = null, ManagerSignature = null
|
|
728
762
|
values (?, ?, ?)`, [signature, memberKey, peerId, stampId]);
|
|
729
763
|
log('Registered MemberPeer (%s, %s)', memberKey, peerId);
|
|
730
764
|
}
|
|
@@ -869,8 +903,8 @@ async function deleteOwnMemberPeer(db, params, stampId) {
|
|
|
869
903
|
const { memberKeyPair, peerId } = params;
|
|
870
904
|
const memberKey = memberKeyPair.publicKeyB64;
|
|
871
905
|
const signature = signStrandApproval(['Strand.MemberPeer', 'remove', memberKey, peerId, stampId], memberKeyPair.privateKeyB64);
|
|
872
|
-
await db.exec(`delete from Strand.MemberPeer
|
|
873
|
-
with context Signature = ?, ManagerKey = null, ManagerSignature = null
|
|
906
|
+
await db.exec(`delete from Strand.MemberPeer
|
|
907
|
+
with context Signature = ?, ManagerKey = null, ManagerSignature = null
|
|
874
908
|
where MemberKey = ? and PeerId = ?`, [signature, memberKey, peerId]);
|
|
875
909
|
await insertRevocation(db, 'MemberPeer', stampId, memberKeyPair);
|
|
876
910
|
}
|
|
@@ -878,8 +912,8 @@ async function deleteOwnMemberPeer(db, params, stampId) {
|
|
|
878
912
|
async function deleteMemberPeerByManager(db, params, stampId) {
|
|
879
913
|
const { managerKeyPair, memberKey, peerId } = params;
|
|
880
914
|
const signature = signStrandApproval(['Strand.MemberPeer', 'manager-remove', memberKey, peerId, stampId], managerKeyPair.privateKeyB64);
|
|
881
|
-
await db.exec(`delete from Strand.MemberPeer
|
|
882
|
-
with context Signature = null, ManagerKey = ?, ManagerSignature = ?
|
|
915
|
+
await db.exec(`delete from Strand.MemberPeer
|
|
916
|
+
with context Signature = null, ManagerKey = ?, ManagerSignature = ?
|
|
883
917
|
where MemberKey = ? and PeerId = ?`, [managerKeyPair.publicKeyB64, signature, memberKey, peerId]);
|
|
884
918
|
await insertRevocation(db, 'MemberPeer', stampId, managerKeyPair);
|
|
885
919
|
}
|
|
@@ -947,8 +981,8 @@ export async function addManager(db, params) {
|
|
|
947
981
|
const generation = authorizer == null ? 1 : authorizer.generation + 1;
|
|
948
982
|
const stampId = generateStrandStampId();
|
|
949
983
|
const signature = signStrandApproval(['Strand.Manager', 'add', newManagerKey, generation, stampId], byManagerKeyPair.privateKeyB64);
|
|
950
|
-
await db.exec(`insert into Strand.Manager (MemberKey, Generation, StampId)
|
|
951
|
-
with context ManagerKey = ?, Signature = ?
|
|
984
|
+
await db.exec(`insert into Strand.Manager (MemberKey, Generation, StampId)
|
|
985
|
+
with context ManagerKey = ?, Signature = ?
|
|
952
986
|
values (?, ?, ?)`, [byManagerKeyPair.publicKeyB64, signature, newManagerKey, generation, stampId]);
|
|
953
987
|
log('Added manager %s (generation %d) by %s', newManagerKey, generation, byManagerKeyPair.publicKeyB64);
|
|
954
988
|
}
|
|
@@ -1020,18 +1054,26 @@ export async function admitManager(db, params) {
|
|
|
1020
1054
|
* The caller selects the case purely by which keypair it passes; the only
|
|
1021
1055
|
* branching here is the action tag, chosen by comparing the signer to the target.
|
|
1022
1056
|
*
|
|
1023
|
-
*
|
|
1024
|
-
*
|
|
1025
|
-
*
|
|
1026
|
-
*
|
|
1027
|
-
*
|
|
1028
|
-
*
|
|
1029
|
-
*
|
|
1030
|
-
*
|
|
1031
|
-
*
|
|
1032
|
-
*
|
|
1057
|
+
* Deferred (subquery-bearing) CHECK constraints are evaluated on DELETE as well
|
|
1058
|
+
* as INSERT by Quereus plus the Optimystic vtab session — enforcement lives above
|
|
1059
|
+
* the transactor (proven on the network transactor by
|
|
1060
|
+
* `strand-membership-network-transactor-parity.spec.ts`), so `Manager.Authorized`
|
|
1061
|
+
* — deferred — IS enforced here: a signer that is neither an existing manager nor
|
|
1062
|
+
* the target itself is rejected at commit.
|
|
1063
|
+
*
|
|
1064
|
+
* THE SOLE MANAGER CANNOT ORDINARILY RESIGN — it SEALS instead. The `'resign'`
|
|
1065
|
+
* branch of `Manager.Authorized` requires at least one manager in the POST-delete
|
|
1066
|
+
* image, so a resignation that would empty the `Manager` table is schema-rejected;
|
|
1067
|
+
* emptying it deliberately is a distinct, self-signed act — {@link sealStrand} —
|
|
1068
|
+
* that permanently freezes admission. This writer refuses the sole-manager
|
|
1069
|
+
* self-resignation up front with an error naming `sealStrand`; that TS guard is UX,
|
|
1070
|
+
* not the trust boundary — the schema rejects a mis-counted `'resign'` approval
|
|
1071
|
+
* regardless. Reading the count inside a caller-joined transaction is correct: an
|
|
1072
|
+
* add-then-resign hand-off composed in one transaction sees 2 and passes, while
|
|
1073
|
+
* the already-rejected delete-then-add swap sees 1 and is refused here as well as
|
|
1074
|
+
* by the schema. The bootstrap branch of `Manager.Authorized` is gated to INSERTs
|
|
1033
1075
|
* (`old.MemberKey is null`), so it does not waive the signature check on a delete
|
|
1034
|
-
* that drops the count toward
|
|
1076
|
+
* that drops the count toward zero: a second-to-last removal is authorized
|
|
1035
1077
|
* exactly like any other.
|
|
1036
1078
|
*
|
|
1037
1079
|
* HAND-OFF ORDER: a sole manager transferring control must ADD the successor FIRST
|
|
@@ -1040,15 +1082,19 @@ export async function admitManager(db, params) {
|
|
|
1040
1082
|
* successor's insert has no other manager to authorize it once the sole manager's
|
|
1041
1083
|
* row is gone.
|
|
1042
1084
|
*
|
|
1043
|
-
* Cross-node caveat: `
|
|
1044
|
-
* nodes concurrently removing different managers can each see
|
|
1045
|
-
* converge to zero. Noted in the schema
|
|
1085
|
+
* Cross-node caveat: the `'resign'`/`'seal'` branches count rows visible to THIS
|
|
1086
|
+
* transaction, so two nodes concurrently removing different managers can each see
|
|
1087
|
+
* a survivor and still converge to zero — an unintended seal. Noted in the schema
|
|
1088
|
+
* (the `Manager` table's seal-propagation NOTE); a cross-node floor is not
|
|
1089
|
+
* attempted here.
|
|
1046
1090
|
*
|
|
1047
1091
|
* @param db - The closed strand's database.
|
|
1048
1092
|
* @param params - The authorizing keypair and the target manager key.
|
|
1049
|
-
* @throws If
|
|
1050
|
-
*
|
|
1051
|
-
*
|
|
1093
|
+
* @throws If the signer IS the target and it is the sole manager (before writing —
|
|
1094
|
+
* the operation the caller wants is {@link sealStrand}), or if `Manager.Authorized`
|
|
1095
|
+
* rejects (a signer that is neither another existing manager nor the target
|
|
1096
|
+
* itself, or a raw sole-manager `'resign'` whose post-image count is zero); the
|
|
1097
|
+
* whole delete+tombstone transaction rolls back.
|
|
1052
1098
|
*/
|
|
1053
1099
|
export async function removeManager(db, params) {
|
|
1054
1100
|
const { byManagerKeyPair, targetManagerKey } = params;
|
|
@@ -1058,13 +1104,135 @@ export async function removeManager(db, params) {
|
|
|
1058
1104
|
return;
|
|
1059
1105
|
}
|
|
1060
1106
|
const tag = byManagerKeyPair.publicKeyB64 === targetManagerKey ? 'resign' : 'remove';
|
|
1107
|
+
if (tag === 'resign' && await strandTableCount(db, 'Manager') === 1) {
|
|
1108
|
+
throw new Error('Cannot resign as the SOLE manager: an ordinary resignation never empties the '
|
|
1109
|
+
+ 'Manager table. To permanently freeze admission instead, call sealStrand.');
|
|
1110
|
+
}
|
|
1061
1111
|
const signature = signStrandApproval(['Strand.Manager', tag, targetManagerKey, target.stampId], byManagerKeyPair.privateKeyB64);
|
|
1062
1112
|
await inStrandTransaction(db, async () => {
|
|
1063
|
-
await db.exec(`delete from Strand.Manager
|
|
1064
|
-
with context ManagerKey = ?, Signature = ?
|
|
1113
|
+
await db.exec(`delete from Strand.Manager
|
|
1114
|
+
with context ManagerKey = ?, Signature = ?
|
|
1065
1115
|
where MemberKey = ?`, [byManagerKeyPair.publicKeyB64, signature, targetManagerKey]);
|
|
1066
1116
|
await insertRevocation(db, 'Manager', target.stampId, byManagerKeyPair);
|
|
1067
1117
|
});
|
|
1068
1118
|
log('Removed manager %s by %s', targetManagerKey, byManagerKeyPair.publicKeyB64);
|
|
1069
1119
|
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Seal the strand: the SOLE manager deliberately steps down, permanently freezing
|
|
1122
|
+
* membership.
|
|
1123
|
+
*
|
|
1124
|
+
* A sealed strand has zero `Manager` rows, and every admission path — issuing or
|
|
1125
|
+
* cancelling an invitation, consuming one issued BEFORE the seal
|
|
1126
|
+
* (`ConsumedInvite.NotSealed`), admitting a member, promoting a manager — requires
|
|
1127
|
+
* one, so nobody can ever be let in again. That freeze is a privacy guarantee to
|
|
1128
|
+
* everyone who contributed: no key holds the power to admit a party who would then
|
|
1129
|
+
* read the strand's whole history. Sealing is irreversible — the founding branch
|
|
1130
|
+
* of `Manager.Authorized` closes for good once any manager stamp is retired into
|
|
1131
|
+
* `Strand.Revocation`. What remains possible: members may {@link leaveStrand}
|
|
1132
|
+
* (floored by `MinOneMember`), manage their own `MemberPeer` rows, and file
|
|
1133
|
+
* `Revocation` tombstones.
|
|
1134
|
+
*
|
|
1135
|
+
* The `'seal'` branch of `Manager.Authorized` accepts a self-signed delete only
|
|
1136
|
+
* when the POST-image manager count is ZERO — a distinct action tag from
|
|
1137
|
+
* `'resign'`, so a captured resignation approval can never seal and a raw writer
|
|
1138
|
+
* cannot seal by accident. Like {@link removeManager}, the delete pairs with its
|
|
1139
|
+
* `Strand.Revocation` tombstone in one transaction.
|
|
1140
|
+
*
|
|
1141
|
+
* The TS count/identity checks below are UX guards, not the trust boundary — the
|
|
1142
|
+
* schema rejects a mis-tagged or mis-counted approval regardless of what this
|
|
1143
|
+
* writer (or any raw writer) presents.
|
|
1144
|
+
*
|
|
1145
|
+
* NOTE: a seal approval is a bearer token for its row incarnation, like every other
|
|
1146
|
+
* self-signed approval here — but the `'seal'` branch's post-image count of 0 is
|
|
1147
|
+
* satisfied for EVERY row deleted in the same transaction, so N managers each
|
|
1148
|
+
* presenting a valid `'seal'` approval seal jointly (pinned as an accepted case in
|
|
1149
|
+
* `strand-seal.spec.ts`). Unreachable today: this writer mints a seal signature only
|
|
1150
|
+
* while its caller is the SOLE manager, and spends it immediately, so a second
|
|
1151
|
+
* manager never has one to combine. If seal-approval MINTING is ever split from
|
|
1152
|
+
* spending — offline signing, a two-phase seal, a queued approval — one manager
|
|
1153
|
+
* holding another's unspent seal approval could freeze the strand irreversibly
|
|
1154
|
+
* without its current consent; re-gate here (or narrow the schema branch) then.
|
|
1155
|
+
*
|
|
1156
|
+
* @param db - The closed strand's database.
|
|
1157
|
+
* @param params - The sole manager's own keypair.
|
|
1158
|
+
* @throws If more than one manager exists (the caller wants {@link removeManager}),
|
|
1159
|
+
* or the supplied keypair holds no `Manager` row while one exists, or the strand
|
|
1160
|
+
* holds no `Manager` row and has never retired one (not founded yet — see
|
|
1161
|
+
* {@link isStrandSealed}), or any schema constraint rejects; the whole
|
|
1162
|
+
* delete+tombstone transaction rolls back. A strand that is ALREADY sealed logs
|
|
1163
|
+
* and returns quietly (restart-safe, matching {@link bootstrapFounderMembership}
|
|
1164
|
+
* and {@link removeManager}'s absent-row no-op).
|
|
1165
|
+
*/
|
|
1166
|
+
export async function sealStrand(db, params) {
|
|
1167
|
+
const { managerKeyPair } = params;
|
|
1168
|
+
const managerCount = await strandTableCount(db, 'Manager');
|
|
1169
|
+
if (managerCount === 0) {
|
|
1170
|
+
// Zero managers is NOT by itself a seal — a closed strand between its Header
|
|
1171
|
+
// and Manager founding inserts looks identical. The retired `Manager` stamp is
|
|
1172
|
+
// what separates "frozen forever" from "still foundable"; returning quietly on
|
|
1173
|
+
// the latter would report a seal that never happened.
|
|
1174
|
+
if (await strandHasManagerRevocation(db)) {
|
|
1175
|
+
log('Strand already sealed (no Manager rows, manager stamp retired); skipping seal');
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
throw new Error('Cannot seal: the strand holds no Manager row and has never retired one — it is '
|
|
1179
|
+
+ 'not founded yet, not sealed. Bootstrap the founding manager first.');
|
|
1180
|
+
}
|
|
1181
|
+
if (managerCount > 1) {
|
|
1182
|
+
throw new Error(`Cannot seal: ${managerCount} managers exist and sealing is the SOLE manager's act. `
|
|
1183
|
+
+ 'The other managers must step down first (removeManager).');
|
|
1184
|
+
}
|
|
1185
|
+
const self = await managerRow(db, managerKeyPair.publicKeyB64);
|
|
1186
|
+
if (self == null) {
|
|
1187
|
+
throw new Error('Cannot seal: the supplied keypair does not hold the sole Manager row.');
|
|
1188
|
+
}
|
|
1189
|
+
const signature = signStrandApproval(['Strand.Manager', 'seal', managerKeyPair.publicKeyB64, self.stampId], managerKeyPair.privateKeyB64);
|
|
1190
|
+
await inStrandTransaction(db, async () => {
|
|
1191
|
+
await db.exec(`delete from Strand.Manager
|
|
1192
|
+
with context ManagerKey = ?, Signature = ?
|
|
1193
|
+
where MemberKey = ?`, [managerKeyPair.publicKeyB64, signature, managerKeyPair.publicKeyB64]);
|
|
1194
|
+
await insertRevocation(db, 'Manager', self.stampId, managerKeyPair);
|
|
1195
|
+
});
|
|
1196
|
+
log('Sealed strand: sole manager %s stepped down', managerKeyPair.publicKeyB64);
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Whether the strand is `Type = 'c'` (closed). Reads the singleton `Header` via
|
|
1200
|
+
* the same `db.eval` scan idiom as {@link strandTableCount}; a strand with no
|
|
1201
|
+
* `Header` row yet reports `false`.
|
|
1202
|
+
*/
|
|
1203
|
+
async function strandIsClosed(db) {
|
|
1204
|
+
for await (const row of db.eval('select Type from Strand.Header')) {
|
|
1205
|
+
return row.Type === 'c';
|
|
1206
|
+
}
|
|
1207
|
+
return false;
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Whether the strand is sealed: a CLOSED strand that holds no `Manager` rows AND
|
|
1211
|
+
* has retired at least one `Manager` stamp into `Strand.Revocation`.
|
|
1212
|
+
*
|
|
1213
|
+
* All three conjuncts are load-bearing:
|
|
1214
|
+
* - `Header.Type` — an OPEN strand never holds `Manager` rows at all
|
|
1215
|
+
* (`Manager.OnlyClosed`), so a bare manager-count test would report every open
|
|
1216
|
+
* strand as sealed.
|
|
1217
|
+
* - the manager count — sealing IS the empty `Manager` table.
|
|
1218
|
+
* - the retired `Manager` stamp — "sealed" means admission is frozen FOREVER, and
|
|
1219
|
+
* what makes it permanent is the founding branch of `Manager.Authorized`
|
|
1220
|
+
* refusing to re-seat a generation-0 manager once any `Manager` stamp has been
|
|
1221
|
+
* retired. Without this conjunct a closed strand that is merely NOT FOUNDED YET
|
|
1222
|
+
* reads as sealed: {@link bootstrapFounderMembership} commits `Header`,
|
|
1223
|
+
* `Member` and `Manager` as three sequential statements, so the window between
|
|
1224
|
+
* the first and the last is exactly "closed, zero managers, still foundable".
|
|
1225
|
+
*
|
|
1226
|
+
* Reads locally visible rows — a node that has not yet converged on the seal
|
|
1227
|
+
* still reports `false` (the schema's seal-propagation NOTE).
|
|
1228
|
+
*
|
|
1229
|
+
* @param db - The strand's database.
|
|
1230
|
+
* @returns `true` iff the strand is closed, holds zero `Manager` rows, and has
|
|
1231
|
+
* permanently closed its founding branch.
|
|
1232
|
+
*/
|
|
1233
|
+
export async function isStrandSealed(db) {
|
|
1234
|
+
return await strandIsClosed(db)
|
|
1235
|
+
&& await strandTableCount(db, 'Manager') === 0
|
|
1236
|
+
&& await strandHasManagerRevocation(db);
|
|
1237
|
+
}
|
|
1070
1238
|
//# sourceMappingURL=strand-membership-writer.js.map
|