@serfab/quereus-plugin-sereus 0.12.0 → 1.0.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.
@@ -13,8 +13,9 @@
13
13
  *
14
14
  * Drift discipline: the table declarations here MUST stay byte-equivalent to the
15
15
  * body of `schemas/strand.qsql` (the on-disk canonical copy). Any edit here must
16
- * be mirrored there and vice versa — the same invariant the planned
17
- * `control-schema-drift-guard` enforces for the control schema.
16
+ * be mirrored there and vice versa. `test/strand-schema-drift.spec.ts` enforces it
17
+ * — but only from THIS package, so an edit made in `schemas/strand.qsql` alone
18
+ * stays green everywhere its author is likely to look; run that spec after either.
18
19
  *
19
20
  * Crypto idiom (matches `schemas/control.qsql` and the sApp RBAC fixture
20
21
  * `packages/integration-tests/fixtures/simple-sapp.qsql`): every signed write
@@ -27,4 +28,4 @@
27
28
  * the founding `Manager`/`Member`, and the invite/peer flows is owned by
28
29
  * `strand-membership-lifecycle-population`.
29
30
  */
30
- export declare const STRAND_SCHEMA = " table Header (\n Id text, -- UUID of this network - generated at inception - immutable\n\n -- Types: 'o' = Open, 'c' = Closed\n -- Any peers can join an open network, but only members can join a closed network\n -- Open can still control writes in the app, but only Closed prevents reads\n Type text check (Type in ('o', 'c')),\n\n -- The app that this strand faciliates - public key from author to prevent tampering\n sAppId text,\n\n -- The version of the sApp that this strand faciliates\n sAppVersion text,\n\n -- The schema of the sApp that this strand faciliates\n sAppSchema text,\n\n -- The author's signature on the schema to guarantee authorship\n sAppSignature text,\n\n -- The engine (rules logic system) that is assumed to manage this strand\n Engine text,\n\n -- The version of the engine that is assumed to manage this strand\n EngineVersion text,\n\n primary key (/* empty - singleton */),\n constraint InsertOnly check on update, delete (false), -- One-time insert only for now - TODO: revisit for versioning\n );\n\n -- An invitation to join a strand as a member\n table Invite (\n Key text primary key,\n Expiration datetime null,\n -- Deactivation is a CancelledInvite tombstone (below), not a delete of this row\n -- or a state column on it \u2014 rationale on that table.\n constraint InsertOnly check on update, delete (false),\n constraint OnlyClosed check (\n exists (select 1 from Header H where H.Type = 'c')\n ),\n constraint InviteValid check on insert (\n -- Can only be inserted by a manager,\n exists (select 1 from Manager A\n where A.MemberKey = context.ManagerKey\n and verify(digest(new.Key || '|' || coalesce(new.Expiration, '')), context.ManagerSignature, A.MemberKey, 'ed25519')\n )\n -- and must also prove invite private key held by issuing manager\n and verify(digest(new.Key || '|' || coalesce(new.Expiration, '')), context.InviteSignature, new.Key, 'ed25519')\n )\n ) with context (ManagerKey text null, ManagerSignature text null, InviteSignature text null);\n\n -- Invite [InviteKey] has been used to add [MemberKey] as a member\n -- No StampId here or on Invite: both tables are insert-only and their primary keys are\n -- minted fresh per issuance/consumption, so a replayed insert collides on the PK.\n table ConsumedInvite (\n InviteKey text primary key,\n MemberKey text,\n constraint InsertOnly check on update, delete (false),\n constraint InviteExists check (exists (select 1 from Invite I where I.Key = new.InviteKey)),\n constraint MemberExists check (exists (select 1 from Member M where M.Key = new.MemberKey)),\n constraint ValidUsage check on insert (\n exists (select 1 from Invite I where I.Key = new.InviteKey and verify(digest(new.InviteKey || '|' || new.MemberKey), context.InviteSignature, new.InviteKey, 'ed25519'))\n ),\n -- An invite with a non-null Expiration may only be consumed while it is still\n -- in the future. context.Now is the canonical-datetime \"now\" supplied by the\n -- consumeInvite writer (same canonicalDatetime() transform used to store\n -- Invite.Expiration), so both sides of the comparison are byte-identical\n -- canonical strings and the lexical \">\" comparison orders chronologically. A\n -- null Expiration never expires. Mirrors CadreControl.FormationUsage's\n -- \"FI.ExpiresAt is null or FI.ExpiresAt > context.Now\" gate.\n constraint NotExpired check on insert (\n exists (select 1 from Invite I where I.Key = new.InviteKey and (I.Expiration is null or I.Expiration > context.Now))\n ),\n -- A cancelled invitation may never be consumed. Blocking the ConsumedInvite\n -- insert is sufficient to block the JOIN: Member.Authorized's invite branch\n -- requires a same-transaction FRESH ConsumedInvite row, and consumeInvite writes\n -- both rows in one transaction, so the whole join rolls back. The rule lives here\n -- alone rather than being duplicated into Member.Authorized. Reads only stored\n -- rows \u2014 no caller-supplied context decides admission (unlike NotExpired's Now).\n constraint NotCancelled check on insert (\n not exists (select 1 from CancelledInvite C where C.InviteKey = new.InviteKey)\n ),\n -- A SEALED strand (a closed strand whose Manager table its sole manager has\n -- deliberately emptied \u2014 see the Manager table comment) admits nobody, and that\n -- includes the holder of an unspent invitation issued BEFORE the seal.\n -- cancelInvite itself needs a manager, so after sealing nobody could kill such\n -- an invitation \u2014 this gate is what kills it. The subquery reads the LIVE\n -- Manager table and defers to commit, so it sees the post-image: at least one\n -- manager must survive the transaction, and sealing plus consuming in the SAME\n -- transaction is rejected too. Blocking this insert blocks the whole join, for\n -- exactly the reason NotCancelled gives above.\n constraint NotSealed check on insert (\n exists (select 1 from Manager)\n )\n ) with context (InviteSignature text null, Now datetime null);\n\n -- A manager has cancelled invitation [InviteKey]: it may never be consumed.\n --\n -- A monotone TOMBSTONE, not a delete of the Invite row and not a state column on it:\n -- 1. Deleting the Invite row would re-open a replay surface. InviteValid signs only\n -- (Key, Expiration) \u2014 there is no per-row StampId on Invite \u2014 so a deleted Invite\n -- row can be re-created by replaying the captured issue signatures. Closing that\n -- would mean adding Invite.StampId and widening Revocation.RowIsGone to a fourth\n -- table. (CadreControl.FormationInvite IS deletable \u2014 it carries a StampId for\n -- exactly this reason.)\n -- 2. A state column needs an UPDATE, and Invite is insert-only on purpose.\n -- 3. Insert-only converges: a growing set of dead markers merges cleanly across\n -- nodes, a delete/re-insert pair does not. Same rationale as Strand.Revocation.\n -- No StampId here: the table is insert-only and its primary key IS the invite key, so\n -- a replayed insert collides on the PK \u2014 the argument the comment above ConsumedInvite\n -- makes for Invite/ConsumedInvite.\n -- Deliberately NO InviteExists gate: a node that has not yet converged on the Invite\n -- row must still be able to record the cancellation. Refusing it would fail OPEN on a\n -- security control, which is strictly worse than accepting a junk row.\n -- NOTE: this table only grows \u2014 one row per cancellation, plus any junk a manager\n -- chooses to file, and Immutable means rows never go away (the tradeoff\n -- Revocation.RowIsGone already documents for junk stamps). Fine at strand scale; if\n -- invite churn ever gets large it needs pruning bounded by something other than\n -- \"forever\".\n -- NOTE: a cancellation must PROPAGATE before the holder redeems \u2014 a holder consuming\n -- on a node that has not yet seen this row still gets in. Same convergence class as\n -- the NotRevoked / MinOneMember local-visibility notes and the Manager table's\n -- seal-propagation note; not solved here.\n table CancelledInvite (\n InviteKey text primary key,\n constraint OnlyClosed check (\n exists (select 1 from Header H where H.Type = 'c')\n ),\n -- Cancellation is permanent: un-cancelling would restore the re-entry it closed.\n -- To re-invite a party, issue a fresh invitation.\n constraint Immutable check on update, delete (false),\n -- committed.Manager, not Manager: this check has a subquery so it defers to\n -- commit, by which point a manager seated in the SAME transaction would otherwise\n -- be able to authorize its own cancellation. Member, MemberPeer and Revocation\n -- read committed.* for the same reason. (Manager.Authorized reads the LIVE table\n -- instead \u2014 there it is the strict Generation ordering, not a snapshot, that\n -- excludes an authorizer seated in the same transaction.)\n constraint Authorized check on insert (\n exists (select 1 from committed.Manager A\n where A.MemberKey = context.ManagerKey\n and verify(digest('Strand.CancelledInvite', 'cancel', new.InviteKey),\n context.ManagerSignature, A.MemberKey, 'ed25519'))\n )\n ) with context (ManagerKey text, ManagerSignature text);\n\n -- A party in the closed strand network\n table Member (\n Key text primary key,\n StampId text not null unique, -- per-row authorization nonce, bound into the signed digests below.\n -- unique holds over LIVE rows only; a removed row's stamp is retired\n -- permanently into Revocation (NotRevoked below), so the never-expiring\n -- admission signature cannot resurrect a revoked member.\n -- A removed row's StampId is retired into Revocation, and this refuses any insert\n -- naming a retired stamp: the approval that seated this row can never re-seat it\n -- after removal. unique alone did not do this \u2014 it only holds over LIVE rows, so\n -- a delete freed the stamp and the original never-expiring signature verified\n -- again. A legitimate re-add mints a FRESH StampId and a fresh signature, so it is\n -- unaffected. Mirrors CadreControl.OwnerKey.NotRevoked (rationale in full there).\n constraint NotRevoked check on insert (\n not exists (select 1 from Revocation R where R.TableName = 'Member' and R.StampId = new.StampId)\n ),\n -- Retirement is MANDATORY: a delete must carry the matching Revocation row in the\n -- same transaction, or the stamp would free up again. Deferred (subquery), so the\n -- sibling insert is visible at commit regardless of statement order.\n constraint RevocationRecorded check on delete (\n exists (select 1 from Revocation R where R.TableName = 'Member' and R.StampId = old.StampId)\n ),\n -- Membership is insert + delete only; a re-keyed member is a remove + fresh add.\n constraint NoUpdate check on update (false),\n -- Mask is explicit on all three operations: a bare check defaults to\n -- insert|update and silently never runs on DELETE.\n constraint OnlyClosed check on insert, update, delete (\n exists (select 1 from Header H where H.Type = 'c')\n ),\n -- A closed strand must never lose its last member: Member is the strand's read\n -- gate, and the founding Manager is only seatable beside an existing Member, so\n -- an empty member set is a permanent denial of the whole strand. Deferred\n -- (subquery), so the count it sees is the POST-delete count.\n -- NOTE: this is a per-transaction check against locally visible rows. Two nodes\n -- that concurrently remove different members can each see a surviving one and\n -- still converge to zero; if partitioned removal ever becomes a real workflow,\n -- the floor needs a cross-node guard, not a local count.\n constraint MinOneMember check on delete (\n (select count(1) from Member) >= 1\n ),\n -- A member still holding a Manager row cannot be un-membered \u2014 it must resign\n -- its Manager row first. Deferred (subquery), so it reads the POST-image: a\n -- single transaction deleting BOTH the Manager and the Member row passes.\n constraint NotAManager check on delete (\n not exists (select 1 from Manager A where A.MemberKey = old.Key)\n ),\n constraint Authorized check on insert, delete (\n -- Every authorizer is read from the PRE-transaction snapshot (committed.*):\n -- this CHECK auto-defers to commit (it has a subquery), by which point rows\n -- inserted alongside are live, so a plain from-Manager would let a manager\n -- seated in this same transaction authorize a removal. committed.* states\n -- the rule directly: the authorizer existed BEFORE this transaction.\n\n -- Every signed digest below leads with two fixed literals \u2014 the\n -- 'Strand.Member' domain tag and an action tag \u2014 so an approval verifies\n -- ONLY against the one rule it was minted for (the idiom of CadreControl's\n -- control-authorization.ts), and binds the row's StampId, so it authorizes\n -- exactly one INCARNATION of the row: once the row is deleted (retiring the\n -- stamp) or re-created under a fresh stamp, the captured approval matches\n -- nothing.\n\n -- Bootstrap: the FOUNDING transaction \u2014 one whose pre-transaction member\n -- set is empty \u2014 seats the first member, AND ONLY THAT ONE, with no\n -- authorization. Both counts are load-bearing. The committed (PRE-image)\n -- count keeps the branch off for a DELETE and for the insert half of a\n -- same-transaction wipe-then-seat (the wiped rows were committed). The\n -- live (POST-image) count keeps a single founding transaction from\n -- seating an unbounded unauthenticated member set \u2014 which would also\n -- brick the strand, since Manager's own bootstrap branch requires at\n -- most one Member.\n (old.Key is null\n and (select count(1) from committed.Member) = 0\n and (select count(1) from Member) <= 1)\n\n -- or added directly by a pre-existing manager signing the add-tagged\n -- digest over (Key, StampId) \u2014 bound to this row's fresh stamp, so the\n -- approval seats exactly this incarnation and dies with it.\n or (old.Key is null and exists (\n select 1 from committed.Manager A\n where A.MemberKey = context.ManagerKey\n and verify(digest('Strand.Member', 'add', new.Key, new.StampId), context.ManagerSignature, A.MemberKey, 'ed25519')\n ))\n\n -- or added by consuming an invite. The ConsumedInvite admitting this\n -- member must be same-transaction FRESH \u2014 its InviteKey absent from the\n -- committed snapshot. consumeInvite inserts Member + ConsumedInvite in\n -- one transaction, so a genuine join passes; a revoked member's STALE\n -- committed row no longer re-admits it, while a fresh manager-issued\n -- invite still does (new InviteKey \u2192 uncommitted row). InviteExists +\n -- ValidUsage on ConsumedInvite keep this branch mintable only via a\n -- real invite and possession of its private key.\n or (old.Key is null and exists (\n select 1 from ConsumedInvite CI\n where CI.MemberKey = new.Key\n and not exists (select 1 from committed.ConsumedInvite CC where CC.InviteKey = CI.InviteKey)\n ))\n\n -- or a removal authorized by a pre-existing manager over the DISTINCT\n -- remove-tagged digest bound to the departing row's (Key, StampId).\n or (new.Key is null and exists (\n select 1 from committed.Manager A\n where A.MemberKey = context.ManagerKey\n and verify(digest('Strand.Member', 'remove', old.Key, old.StampId), context.ManagerSignature, A.MemberKey, 'ed25519')\n ))\n\n -- or the member removes ITSELF (leaving): the leave-tagged signature\n -- verifies against old.Key itself, so only the departing key's holder\n -- can produce it \u2014 no manager involved. The 'leave' tag (vs the manager\n -- branch's 'remove') is belt-and-braces ONLY: the two branches verify\n -- against DIFFERENT keys, so the former shared tag was never\n -- exploitable \u2014 the anti-replay fix here is the StampId binding, not\n -- this tag.\n or (new.Key is null\n and verify(digest('Strand.Member', 'leave', old.Key, old.StampId), context.MemberSignature, old.Key, 'ed25519'))\n ),\n ) with context (ManagerKey text null, ManagerSignature text null, MemberSignature text null);\n\n -- A member-associated peer (node)\n --\n -- NOTE: a MemberPeer row can OUTLIVE the Member row it names. MemberExists runs on\n -- insert only, and nothing cascades a Member delete, so revoking a member leaves its\n -- peer bindings behind as orphans until a manager clears them (removeMemberPeer).\n -- Any consumer that treats a MemberPeer row as evidence of membership MUST join to\n -- Member rather than trusting the row alone.\n table MemberPeer (\n MemberKey text,\n PeerId text,\n StampId text not null unique, -- per-row authorization nonce, bound into the signed\n -- digests below; retired into Revocation on delete\n -- (same idiom as Member.StampId, rationale there).\n primary key (MemberKey, PeerId),\n -- Refuses any insert naming a retired stamp \u2014 see Member.NotRevoked.\n constraint NotRevoked check on insert (\n not exists (select 1 from Revocation R where R.TableName = 'MemberPeer' and R.StampId = new.StampId)\n ),\n -- A delete must retire its stamp in the same transaction \u2014 see Member.RevocationRecorded.\n constraint RevocationRecorded check on delete (\n exists (select 1 from Revocation R where R.TableName = 'MemberPeer' and R.StampId = old.StampId)\n ),\n -- Binding is insert + delete only, as on Member and Manager; a re-binding is a\n -- remove plus a fresh register. The whole visible identity IS the primary key, so\n -- an update is never an edit \u2014 and Authorized reads the NEW image, so an update\n -- would let ANY member re-point another member's row at its OWN key, signing only\n -- over its own new values, and thereby clear a binding it has no signature to\n -- delete.\n constraint NoUpdate check on update (false),\n -- Mask is explicit: a bare check defaults to insert|update and silently never runs on\n -- DELETE. Insert is the only reachable case \u2014 update is forbidden above, and a delete\n -- leaves no new row image to validate.\n constraint MemberExists check on insert (exists (select 1 from Member M where M.Key = new.MemberKey)),\n constraint Authorized check on insert, delete (\n -- The member itself signs its own binding, verified against the MemberKey the\n -- row names \u2014 so only that member registers (or removes) its own peers. The\n -- registration and removal approvals are DISTINCT action-tagged digests, each\n -- bound to the row's StampId, so a captured registration approval neither\n -- re-registers a cleared binding (its stamp is retired) nor authorizes a\n -- removal (wrong tag).\n (old.MemberKey is null\n and verify(digest('Strand.MemberPeer', 'add', new.MemberKey, new.PeerId, new.StampId), context.Signature, new.MemberKey, 'ed25519'))\n\n -- or the member clears its OWN binding.\n or (new.MemberKey is null\n and verify(digest('Strand.MemberPeer', 'remove', old.MemberKey, old.PeerId, old.StampId), context.Signature, old.MemberKey, 'ed25519'))\n\n -- or a manager clears ANOTHER member's binding \u2014 the cleanup path after a\n -- revocation, which the departing member has no reason to cooperate with.\n -- Gated to DELETE and bound to a manager-remove-tagged digest over the exact\n -- row + stamp. The 'manager-remove' tag (vs the self branch's 'remove') is\n -- belt-and-braces ONLY \u2014 the two branches verify against different keys.\n -- The authorizer is read from the PRE-transaction snapshot\n -- (committed.Manager) for the same reason Member.Authorized does: this check\n -- defers to commit, by which point a manager seated in the SAME transaction\n -- would otherwise be able to authorize its own cleanup.\n or (new.MemberKey is null and exists (\n select 1 from committed.Manager A\n where A.MemberKey = context.ManagerKey\n and verify(digest('Strand.MemberPeer', 'manager-remove', old.MemberKey, old.PeerId, old.StampId), context.ManagerSignature, A.MemberKey, 'ed25519')\n ))\n ),\n ) with context (Signature text null, ManagerKey text null, ManagerSignature text null);\n\n -- A manager is a member that can issue invites, authorize members, and rotate managers\n table Manager (\n MemberKey text primary key,\n -- Lineage ordering. The founding manager is generation 0; every later manager is\n -- seated strictly after (greater than) the manager that appointed it. This is what\n -- makes same-transaction mutual promotion impossible: a deferred CHECK sees only\n -- the post-image, so \"did my authorizer exist before this transaction?\" is not\n -- directly askable \u2014 but the minimum-generation row of any inserted set must find\n -- its authorizer among rows of strictly smaller generation, which can only be a\n -- pre-existing one. Generation is NOT a privilege level: a generation-5 manager\n -- has exactly the same powers as a generation-1 manager.\n Generation integer not null,\n StampId text not null unique, -- per-row authorization nonce, bound into the signed\n -- digests below; retired into Revocation on delete\n -- (same idiom as Member.StampId, rationale there).\n -- Refuses any insert naming a retired stamp \u2014 see Member.NotRevoked. This is what\n -- makes a captured promotion approval single-use: once the manager is removed (its\n -- stamp retired), the approval that seated it can never re-seat it.\n constraint NotRevoked check on insert (\n not exists (select 1 from Revocation R where R.TableName = 'Manager' and R.StampId = new.StampId)\n ),\n -- A delete must retire its stamp in the same transaction \u2014 see Member.RevocationRecorded.\n constraint RevocationRecorded check on delete (\n exists (select 1 from Revocation R where R.TableName = 'Manager' and R.StampId = old.StampId)\n ),\n constraint OnlyClosed check (\n exists (select 1 from Header H where H.Type = 'c')\n ),\n -- A manager IS a member: the table comment has always said so, and since the\n -- single-use-approval work it is load-bearing \u2014 Revocation.Authorized verifies a\n -- delete's tombstone against a committed Member row, so a manager with no Member\n -- row cannot revoke, clear a peer binding, or even resign. Reads the LIVE Member\n -- table, not committed.Member, so an admit-then-promote in ONE transaction passes\n -- (the same shape ConsumedInvite.MemberExists allows for the invite join); the\n -- sibling Member insert is authorized at that same commit, so nothing is waived.\n -- Mask is explicit: a bare check defaults to insert|update, and update is\n -- forbidden outright by NoUpdate below. Nothing is needed on DELETE \u2014 the other\n -- half of the invariant is Member.NotAManager, which refuses to un-member a key\n -- that still holds a Manager row.\n -- NOTE: a per-transaction check against locally visible rows. Two partitioned nodes\n -- \u2014 one promoting X, one removing X's Member and Manager rows \u2014 can each pass\n -- locally and converge to a Manager row with no Member row. Same convergence class\n -- as the MinOneMember / NotRevoked / seal-propagation notes; not solved here.\n constraint MemberExists check on insert (\n exists (select 1 from Member M where M.Key = new.MemberKey)\n ),\n -- Rotation is insert + delete only. An update would let a resignation signature\n -- (which proves only that old.MemberKey consented) double as a hand-off: the\n -- former-manager branch would accept re-pointing the row at any new key.\n constraint NoUpdate check on update (false),\n -- A closed strand loses its LAST manager only by SEALING \u2014 a distinct,\n -- self-signed act (the 'seal' branch of Authorized below), never an ordinary\n -- resignation and never an admin removal. There is deliberately NO\n -- min-one-manager floor: a strand with no managers is frozen, not bricked.\n -- Every admission path (Invite, addMemberByManager, addManager) requires a\n -- Manager row, so a sealed strand's membership can never grow again \u2014 and that\n -- freeze is the privacy guarantee its remaining members are buying: nobody\n -- holds the power to admit a party who would then read the strand's whole\n -- history. Sealing is irreversible: the founding branch of Authorized closes\n -- for good once any manager stamp has been retired into Revocation.\n -- NOTE: a seal must PROPAGATE before it binds a given node \u2014 a node that has\n -- not yet converged on the delete still shows the manager row, so the\n -- ex-manager's OWN key could still admit there until the delete arrives. Only\n -- that one key; no stranger gains anything. Same convergence class as the\n -- MinOneMember / NotRevoked local-visibility notes; not solved here.\n constraint Authorized check on insert, delete (\n -- Every signed branch below is a domain/action-tagged digest bound to the\n -- row's StampId \u2014 the same single-use idiom as Member.Authorized: an\n -- approval seats (or removes) exactly one incarnation of one row.\n\n -- Bootstrap: the founding manager is seated with no prior signer, at\n -- generation 0. Gated to INSERT (old.MemberKey is null) AND to the founding\n -- state \u2014 at most one Member, and this manager IS that member. Deferred\n -- checks see post-image state, so an ungated count test is also true for a\n -- DELETE that drops the count to <= 1, and for the INSERT half of a\n -- same-transaction swap of the sole manager. The trailing Member-exists test\n -- is belt-and-braces since MemberExists above enforces it on EVERY insert \u2014\n -- kept so this branch's own claim (\"this manager IS that member\") reads in\n -- place rather than by cross-reference.\n -- The Revocation gate closes this branch FOREVER once any manager seat has\n -- been retired: founding happens exactly once per strand (Header, then\n -- Member, then Manager, with no Revocation rows in existence), so on a\n -- SEALED strand a lone surviving member cannot re-seat itself as a\n -- generation-0 founder and start admitting. Accepted, deliberate: between\n -- its own founding Member and Manager inserts a founder could, with its own\n -- key, file a junk Revocation('Manager', ...) tombstone (RowIsGone permits\n -- stamps naming no real row) and permanently block its own founding \u2014 pure\n -- self-harm needing the founder's private key; no other party can reach\n -- that window (there are no other committed members yet).\n (old.MemberKey is null\n and new.Generation = 0\n and (select count(1) from Manager) <= 1\n and (select count(1) from Member) <= 1\n and exists (select 1 from Member M where M.Key = new.MemberKey)\n and not exists (select 1 from Revocation R where R.TableName = 'Manager'))\n\n -- or removed by the departing manager ITSELF, over one of two\n -- self-signed, action-tagged digests bound to its own row's stamp. The\n -- POST-image manager count is what distinguishes the two: 'resign' is\n -- an ordinary departure that leaves at least one manager standing,\n -- while 'seal' is the delete that empties the table and permanently\n -- freezes admission (see the table comment above). The tag is what\n -- makes each a signed statement of THAT intent \u2014 a captured\n -- resignation approval can never seal, nor a seal approval merely\n -- resign \u2014 and the stamp is still what makes each single-use: once\n -- this manager is re-promoted under a fresh stamp, neither captured\n -- approval removes the new row. Both counts are subqueries, so the\n -- whole check defers to commit and the counts are the post-image.\n or (\n old.MemberKey is not null\n and old.MemberKey = context.ManagerKey\n and (select count(1) from Manager) >= 1\n and verify(digest('Strand.Manager', 'resign', old.MemberKey, old.StampId), context.Signature, old.MemberKey, 'ed25519')\n )\n or (\n old.MemberKey is not null\n and old.MemberKey = context.ManagerKey\n and (select count(1) from Manager) = 0\n and verify(digest('Strand.Manager', 'seal', old.MemberKey, old.StampId), context.Signature, old.MemberKey, 'ed25519')\n )\n\n -- or a promotion signed by an EARLIER-generation manager, over the\n -- add-tagged digest binding (MemberKey, Generation, StampId) \u2014 so a\n -- captured promotion replays at neither a different generation nor a\n -- second incarnation of the row. The strict\n -- A.Generation < new.Generation is what closes same-transaction mutual\n -- promotion: this subquery runs at commit against the post-insert row\n -- set, so sibling rows inserted in the same transaction are visible \u2014\n -- but the minimum-generation row of any inserted set cannot find its\n -- authorizer among its siblings (that would contradict minimality), so\n -- that authorizer must be a pre-existing manager who really signed.\n -- A mutual pair or ring needs each generation strictly below another's\n -- in a cycle; impossible. Nothing sits below the founder's 0, so a\n -- chain cannot duck underneath either. The <> is subsumed by the\n -- ordering (a row's generation is never below its own) but kept to\n -- state the no-self-promotion intent locally.\n or (old.MemberKey is null and exists (\n select 1 from Manager A\n where A.MemberKey = context.ManagerKey\n and A.MemberKey <> new.MemberKey\n and A.Generation < new.Generation\n and verify(digest('Strand.Manager', 'add', new.MemberKey, new.Generation, new.StampId), context.Signature, A.MemberKey, 'ed25519')))\n\n -- or a removal authorized by ANOTHER existing manager, over the\n -- remove-tagged digest bound to (MemberKey, StampId). Deliberately no\n -- generation condition here: deletes are safe once inserts are (every\n -- accepting branch requires a Manager row in the post-image, and no\n -- attacker row can get there), and a generation gate would break a\n -- later-generation manager removing an earlier-generation one.\n or (new.MemberKey is null and exists (\n select 1 from Manager A\n where A.MemberKey = context.ManagerKey\n and A.MemberKey <> old.MemberKey\n and verify(digest('Strand.Manager', 'remove', old.MemberKey, old.StampId), context.Signature, A.MemberKey, 'ed25519')))\n )\n ) with context (ManagerKey text null, Signature text null);\n\n -- Permanent tombstones for retired per-row authorization nonces (StampIds) \u2014 the\n -- delete half of the retired-stamp anti-replay idiom on Member / Manager /\n -- MemberPeer. Adapted from CadreControl.Revocation (the idiom is stated in full on\n -- CadreControl.OwnerKey); two deliberate differences:\n -- 1. TableName is confined to the three guarded STRAND tables (RowIsGone below).\n -- 2. Authorized verifies against a committed MEMBER row, not an owner key: every\n -- delete path in this schema is driven by a manager or by the departing party,\n -- and both are members. The action tag is 'retire' where the control layer\n -- says 'remove'; the domain tag already keeps the two schemas' approvals\n -- disjoint, so the divergence is cosmetic \u2014 kept because 'retire' names the\n -- act (retiring a stamp), not a row operation.\n -- NOTE: the guarded tables' NotRevoked CHECK runs against locally visible rows, so\n -- a node that has not yet converged on a Revocation row can still accept a replayed\n -- add and the resurrected row coexists with the tombstone after merge \u2014 the same\n -- convergence class as the MinOneMember / Manager seal-propagation notes.\n -- NOTE: this table only grows \u2014 one row per membership deletion, plus any junk any\n -- member chooses to file, and Immutable means rows never go away. Fine at strand\n -- scale; if membership churn ever gets large it needs pruning bounded by something\n -- other than \"forever\".\n -- NOTE: nothing on the strand read side drops a row because its stamp is retired\n -- (NotRevoked is insert-only), which is why a tombstone filed against a live but\n -- not-yet-converged row is inert here. If a read path ever starts filtering on\n -- Revocation (as the control layer's listAuthorizedMembers does), filing a tombstone\n -- becomes a remote eviction primitive and Authorized below must narrow from \"any\n -- member\" to \"a manager, or the row's own owner\".\n table Revocation (\n TableName text, -- 'Member' | 'Manager' | 'MemberPeer' (confined by RowIsGone below)\n StampId text, -- the retired nonce\n primary key (TableName, StampId),\n constraint OnlyClosed check (\n exists (select 1 from Header H where H.Type = 'c')\n ),\n -- Retirement is permanent: clearing or re-pointing a tombstone would restore the replay.\n constraint Immutable check on update, delete (false),\n -- A stamp may only be retired once its row is actually gone. Deferred (subquery), so a\n -- delete in the same transaction has already landed. Retiring a stamp that never\n -- existed is permitted and harmless: a stamp carries 256 bits of CSPRNG output\n -- (strand-membership-writer.ts generateStrandStampId), so a future legitimate one\n -- cannot be guessed and pre-planted \u2014 junk stamps only grow the table.\n constraint RowIsGone check on insert (\n (new.TableName = 'Member' and not exists (select 1 from Member M where M.StampId = new.StampId))\n or (new.TableName = 'Manager' and not exists (select 1 from Manager A where A.StampId = new.StampId))\n or (new.TableName = 'MemberPeer' and not exists (select 1 from MemberPeer P where P.StampId = new.StampId))\n ),\n -- Filing a tombstone is a MEMBER action: every legitimate delete in this schema is\n -- driven by a manager or by the departing party \u2014 both members \u2014 and the tombstone\n -- rides the delete's own transaction. The authorizer is read from the\n -- PRE-transaction snapshot (committed.Member) for the same reason the sibling\n -- tables read committed.*: this check defers to commit, by which point a member\n -- seated in the same transaction is live. The digest binds the WHOLE row\n -- (TableName, StampId) under its own domain tag, so it is disjoint from every\n -- other rule \u2014 including the remove-tagged approval the same party signs in the\n -- SAME transaction for the delete this tombstone accompanies. RowIsGone and\n -- Immutable guard what authorization does not: retiring a stamp early, or\n -- withdrawing one later.\n constraint Authorized check on insert (\n exists (select 1 from committed.Member M\n where M.Key = context.MemberKey\n and verify(digest('Strand.Revocation', 'retire', new.TableName, new.StampId), context.Signature, M.Key, 'ed25519'))\n )\n ) with context (MemberKey text, Signature text);\n";
31
+ export declare const STRAND_SCHEMA = " table Header (\n Id text, -- UUID of this network - generated at inception - immutable\n\n -- Types: 'o' = Open, 'c' = Closed\n -- Any peers can join an open network, but only members can join a closed network\n -- Open can still control writes in the app, but only Closed prevents reads\n Type text check (Type in ('o', 'c')),\n\n -- The app that this strand faciliates - public key from author to prevent tampering\n sAppId text,\n\n -- The version of the sApp that this strand faciliates\n sAppVersion text,\n\n -- The schema of the sApp that this strand faciliates\n sAppSchema text,\n\n -- The author's signature on the schema to guarantee authorship\n sAppSignature text,\n\n -- The engine (rules logic system) that is assumed to manage this strand\n Engine text,\n\n -- The version of the engine that is assumed to manage this strand\n EngineVersion text,\n\n primary key (/* empty - singleton */),\n constraint InsertOnly check on update, delete (false), -- One-time insert only for now - TODO: revisit for versioning\n );\n\n -- An invitation to join a strand as a member\n table Invite (\n Key text primary key,\n Expiration datetime null,\n -- Deactivation is a CancelledInvite tombstone (below), not a delete of this row\n -- or a state column on it \u2014 rationale on that table.\n constraint InsertOnly check on update, delete (false),\n constraint OnlyClosed check (\n exists (select 1 from Header H where H.Type = 'c')\n ),\n constraint InviteValid check on insert (\n -- Can only be inserted by a manager,\n exists (select 1 from Manager A\n where A.MemberKey = context.ManagerKey\n and verify(digest(new.Key || '|' || coalesce(new.Expiration, '')), context.ManagerSignature, A.MemberKey, 'ed25519')\n )\n -- and must also prove invite private key held by issuing manager\n and verify(digest(new.Key || '|' || coalesce(new.Expiration, '')), context.InviteSignature, new.Key, 'ed25519')\n )\n ) with context (ManagerKey text null, ManagerSignature text null, InviteSignature text null);\n\n -- Invite [InviteKey] has been used to add [MemberKey] as a member\n -- No StampId here or on Invite: both tables are insert-only and their primary keys are\n -- minted fresh per issuance/consumption, so a replayed insert collides on the PK.\n table ConsumedInvite (\n InviteKey text primary key,\n MemberKey text,\n constraint InsertOnly check on update, delete (false),\n constraint InviteExists check (exists (select 1 from Invite I where I.Key = new.InviteKey)),\n constraint MemberExists check (exists (select 1 from Member M where M.Key = new.MemberKey)),\n constraint ValidUsage check on insert (\n exists (select 1 from Invite I where I.Key = new.InviteKey and verify(digest(new.InviteKey || '|' || new.MemberKey), context.InviteSignature, new.InviteKey, 'ed25519'))\n ),\n -- An invite with a non-null Expiration may only be consumed while it is still\n -- in the future. context.Now is the canonical-datetime \"now\" supplied by the\n -- consumeInvite writer (same canonicalDatetime() transform used to store\n -- Invite.Expiration), so both sides of the comparison are byte-identical\n -- canonical strings and the lexical \">\" comparison orders chronologically. A\n -- null Expiration never expires. Mirrors CadreControl.FormationUsage's\n -- \"FI.ExpiresAt is null or FI.ExpiresAt > context.Now\" gate.\n constraint NotExpired check on insert (\n exists (select 1 from Invite I where I.Key = new.InviteKey and (I.Expiration is null or I.Expiration > context.Now))\n ),\n -- A cancelled invitation may never be consumed. Blocking the ConsumedInvite\n -- insert is sufficient to block the JOIN: Member.Authorized's invite branch\n -- requires a same-transaction FRESH ConsumedInvite row, and consumeInvite writes\n -- both rows in one transaction, so the whole join rolls back. The rule lives here\n -- alone rather than being duplicated into Member.Authorized. Reads only stored\n -- rows \u2014 no caller-supplied context decides admission (unlike NotExpired's Now).\n constraint NotCancelled check on insert (\n not exists (select 1 from CancelledInvite C where C.InviteKey = new.InviteKey)\n ),\n -- A SEALED strand (a closed strand whose Manager table its sole manager has\n -- deliberately emptied \u2014 see the Manager table comment) admits nobody, and that\n -- includes the holder of an unspent invitation issued BEFORE the seal.\n -- cancelInvite itself needs a manager, so after sealing nobody could kill such\n -- an invitation \u2014 this gate is what kills it. The subquery reads the LIVE\n -- Manager table and defers to commit, so it sees the post-image: at least one\n -- manager must survive the transaction, and sealing plus consuming in the SAME\n -- transaction is rejected too. Blocking this insert blocks the whole join, for\n -- exactly the reason NotCancelled gives above.\n -- The Manager rows this reads are the ones THIS NODE can see, so the gate binds\n -- only once the seal has reached here \u2014 on a node still behind on the seal it\n -- passes, and a stranger holding a pre-seal invitation joins (the Manager table's\n -- seal-propagation NOTE).\n constraint NotSealed check on insert (\n exists (select 1 from Manager)\n )\n ) with context (InviteSignature text null, Now datetime null);\n\n -- A manager has cancelled invitation [InviteKey]: it may never be consumed.\n --\n -- A monotone TOMBSTONE, not a delete of the Invite row and not a state column on it:\n -- 1. Deleting the Invite row would re-open a replay surface. InviteValid signs only\n -- (Key, Expiration) \u2014 there is no per-row StampId on Invite \u2014 so a deleted Invite\n -- row can be re-created by replaying the captured issue signatures. Closing that\n -- would mean adding Invite.StampId and widening Revocation.RowIsGone to a fourth\n -- table. (CadreControl.FormationInvite IS deletable \u2014 it carries a StampId for\n -- exactly this reason.)\n -- 2. A state column needs an UPDATE, and Invite is insert-only on purpose.\n -- 3. Insert-only converges: a growing set of dead markers merges cleanly across\n -- nodes, a delete/re-insert pair does not. Same rationale as Strand.Revocation.\n -- No StampId here: the table is insert-only and its primary key IS the invite key, so\n -- a replayed insert collides on the PK \u2014 the argument the comment above ConsumedInvite\n -- makes for Invite/ConsumedInvite.\n -- Deliberately NO InviteExists gate: a node that has not yet converged on the Invite\n -- row must still be able to record the cancellation. Refusing it would fail OPEN on a\n -- security control, which is strictly worse than accepting a junk row.\n -- NOTE: this table only grows \u2014 one row per cancellation, plus any junk a manager\n -- chooses to file, and Immutable means rows never go away (the tradeoff\n -- Revocation.RowIsGone already documents for junk stamps). Fine at strand scale; if\n -- invite churn ever gets large it needs pruning bounded by something other than\n -- \"forever\".\n -- NOTE: a cancellation must PROPAGATE before the holder redeems \u2014 a holder consuming\n -- on a node that has not yet seen this row still gets in. Same convergence class as\n -- the NotRevoked / MinOneMember local-visibility notes and the Manager table's\n -- seal-propagation note; not solved here.\n table CancelledInvite (\n InviteKey text primary key,\n constraint OnlyClosed check (\n exists (select 1 from Header H where H.Type = 'c')\n ),\n -- Cancellation is permanent: un-cancelling would restore the re-entry it closed.\n -- To re-invite a party, issue a fresh invitation.\n constraint Immutable check on update, delete (false),\n -- committed.Manager, not Manager: this check has a subquery so it defers to\n -- commit, by which point a manager seated in the SAME transaction would otherwise\n -- be able to authorize its own cancellation. Member, MemberPeer and Revocation\n -- read committed.* for the same reason. (Manager.Authorized reads the LIVE table\n -- instead \u2014 there it is the strict Generation ordering, not a snapshot, that\n -- excludes an authorizer seated in the same transaction.)\n constraint Authorized check on insert (\n exists (select 1 from committed.Manager A\n where A.MemberKey = context.ManagerKey\n and verify(digest('Strand.CancelledInvite', 'cancel', new.InviteKey),\n context.ManagerSignature, A.MemberKey, 'ed25519'))\n )\n ) with context (ManagerKey text, ManagerSignature text);\n\n -- A party in the closed strand network\n table Member (\n Key text primary key,\n StampId text not null unique, -- per-row authorization nonce, bound into the signed digests below.\n -- unique holds over LIVE rows only; a removed row's stamp is retired\n -- permanently into Revocation (NotRevoked below), so the never-expiring\n -- admission signature cannot resurrect a revoked member.\n -- A removed row's StampId is retired into Revocation, and this refuses any insert\n -- naming a retired stamp: the approval that seated this row can never re-seat it\n -- after removal. unique alone did not do this \u2014 it only holds over LIVE rows, so\n -- a delete freed the stamp and the original never-expiring signature verified\n -- again. A legitimate re-add mints a FRESH StampId and a fresh signature, so it is\n -- unaffected. Mirrors CadreControl.OwnerKey.NotRevoked (rationale in full there).\n constraint NotRevoked check on insert (\n not exists (select 1 from Revocation R where R.TableName = 'Member' and R.StampId = new.StampId)\n ),\n -- Retirement is MANDATORY: a delete must carry the matching Revocation row in the\n -- same transaction, or the stamp would free up again. Deferred (subquery), so the\n -- sibling insert is visible at commit regardless of statement order.\n constraint RevocationRecorded check on delete (\n exists (select 1 from Revocation R where R.TableName = 'Member' and R.StampId = old.StampId)\n ),\n -- Membership is insert + delete only; a re-keyed member is a remove + fresh add.\n constraint NoUpdate check on update (false),\n -- Mask is explicit on all three operations: a bare check defaults to\n -- insert|update and silently never runs on DELETE.\n constraint OnlyClosed check on insert, update, delete (\n exists (select 1 from Header H where H.Type = 'c')\n ),\n -- A closed strand must never lose its last member: Member is the strand's read\n -- gate, and the founding Manager is only seatable beside an existing Member, so\n -- an empty member set is a permanent denial of the whole strand. Deferred\n -- (subquery), so the count it sees is the POST-delete count.\n -- NOTE: this is a per-transaction check against locally visible rows. Two nodes\n -- that concurrently remove different members can each see a surviving one and\n -- still converge to zero; if partitioned removal ever becomes a real workflow,\n -- the floor needs a cross-node guard, not a local count.\n constraint MinOneMember check on delete (\n (select count(1) from Member) >= 1\n ),\n -- A member still holding a Manager row cannot be un-membered \u2014 it must resign\n -- its Manager row first. Deferred (subquery), so it reads the POST-image: a\n -- single transaction deleting BOTH the Manager and the Member row passes.\n constraint NotAManager check on delete (\n not exists (select 1 from Manager A where A.MemberKey = old.Key)\n ),\n constraint Authorized check on insert, delete (\n -- Every authorizer is read from the PRE-transaction snapshot (committed.*):\n -- this CHECK auto-defers to commit (it has a subquery), by which point rows\n -- inserted alongside are live, so a plain from-Manager would let a manager\n -- seated in this same transaction authorize a removal. committed.* states\n -- the rule directly: the authorizer existed BEFORE this transaction.\n\n -- Every signed digest below leads with two fixed literals \u2014 the\n -- 'Strand.Member' domain tag and an action tag \u2014 so an approval verifies\n -- ONLY against the one rule it was minted for (the idiom of CadreControl's\n -- control-authorization.ts), and binds the row's StampId, so it authorizes\n -- exactly one INCARNATION of the row: once the row is deleted (retiring the\n -- stamp) or re-created under a fresh stamp, the captured approval matches\n -- nothing.\n\n -- Bootstrap: the FOUNDING transaction \u2014 one whose pre-transaction member\n -- set is empty \u2014 seats the first member, AND ONLY THAT ONE, with no\n -- authorization. Both counts are load-bearing. The committed (PRE-image)\n -- count keeps the branch off for a DELETE and for the insert half of a\n -- same-transaction wipe-then-seat (the wiped rows were committed). The\n -- live (POST-image) count keeps a single founding transaction from\n -- seating an unbounded unauthenticated member set \u2014 which would also\n -- brick the strand, since Manager's own bootstrap branch requires at\n -- most one Member.\n (old.Key is null\n and (select count(1) from committed.Member) = 0\n and (select count(1) from Member) <= 1)\n\n -- or added directly by a pre-existing manager signing the add-tagged\n -- digest over (Key, StampId) \u2014 bound to this row's fresh stamp, so the\n -- approval seats exactly this incarnation and dies with it.\n or (old.Key is null and exists (\n select 1 from committed.Manager A\n where A.MemberKey = context.ManagerKey\n and verify(digest('Strand.Member', 'add', new.Key, new.StampId), context.ManagerSignature, A.MemberKey, 'ed25519')\n ))\n\n -- or added by consuming an invite. The ConsumedInvite admitting this\n -- member must be same-transaction FRESH \u2014 its InviteKey absent from the\n -- committed snapshot. consumeInvite inserts Member + ConsumedInvite in\n -- one transaction, so a genuine join passes; a revoked member's STALE\n -- committed row no longer re-admits it, while a fresh manager-issued\n -- invite still does (new InviteKey \u2192 uncommitted row). InviteExists +\n -- ValidUsage on ConsumedInvite keep this branch mintable only via a\n -- real invite and possession of its private key.\n or (old.Key is null and exists (\n select 1 from ConsumedInvite CI\n where CI.MemberKey = new.Key\n and not exists (select 1 from committed.ConsumedInvite CC where CC.InviteKey = CI.InviteKey)\n ))\n\n -- or a removal authorized by a pre-existing manager over the DISTINCT\n -- remove-tagged digest bound to the departing row's (Key, StampId).\n or (new.Key is null and exists (\n select 1 from committed.Manager A\n where A.MemberKey = context.ManagerKey\n and verify(digest('Strand.Member', 'remove', old.Key, old.StampId), context.ManagerSignature, A.MemberKey, 'ed25519')\n ))\n\n -- or the member removes ITSELF (leaving): the leave-tagged signature\n -- verifies against old.Key itself, so only the departing key's holder\n -- can produce it \u2014 no manager involved. The 'leave' tag (vs the manager\n -- branch's 'remove') is belt-and-braces ONLY: the two branches verify\n -- against DIFFERENT keys, so the former shared tag was never\n -- exploitable \u2014 the anti-replay fix here is the StampId binding, not\n -- this tag.\n or (new.Key is null\n and verify(digest('Strand.Member', 'leave', old.Key, old.StampId), context.MemberSignature, old.Key, 'ed25519'))\n ),\n ) with context (ManagerKey text null, ManagerSignature text null, MemberSignature text null);\n\n -- A member-associated peer (node)\n --\n -- NOTE: a MemberPeer row can OUTLIVE the Member row it names. MemberExists runs on\n -- insert only, and nothing cascades a Member delete, so revoking a member leaves its\n -- peer bindings behind as orphans until a manager clears them (removeMemberPeer).\n -- Any consumer that treats a MemberPeer row as evidence of membership MUST join to\n -- Member rather than trusting the row alone.\n table MemberPeer (\n MemberKey text,\n PeerId text,\n StampId text not null unique, -- per-row authorization nonce, bound into the signed\n -- digests below; retired into Revocation on delete\n -- (same idiom as Member.StampId, rationale there).\n primary key (MemberKey, PeerId),\n -- Refuses any insert naming a retired stamp \u2014 see Member.NotRevoked.\n constraint NotRevoked check on insert (\n not exists (select 1 from Revocation R where R.TableName = 'MemberPeer' and R.StampId = new.StampId)\n ),\n -- A delete must retire its stamp in the same transaction \u2014 see Member.RevocationRecorded.\n constraint RevocationRecorded check on delete (\n exists (select 1 from Revocation R where R.TableName = 'MemberPeer' and R.StampId = old.StampId)\n ),\n -- Binding is insert + delete only, as on Member and Manager; a re-binding is a\n -- remove plus a fresh register. The whole visible identity IS the primary key, so\n -- an update is never an edit \u2014 and Authorized reads the NEW image, so an update\n -- would let ANY member re-point another member's row at its OWN key, signing only\n -- over its own new values, and thereby clear a binding it has no signature to\n -- delete.\n constraint NoUpdate check on update (false),\n -- Mask is explicit: a bare check defaults to insert|update and silently never runs on\n -- DELETE. Insert is the only reachable case \u2014 update is forbidden above, and a delete\n -- leaves no new row image to validate.\n constraint MemberExists check on insert (exists (select 1 from Member M where M.Key = new.MemberKey)),\n constraint Authorized check on insert, delete (\n -- The member itself signs its own binding, verified against the MemberKey the\n -- row names \u2014 so only that member registers (or removes) its own peers. The\n -- registration and removal approvals are DISTINCT action-tagged digests, each\n -- bound to the row's StampId, so a captured registration approval neither\n -- re-registers a cleared binding (its stamp is retired) nor authorizes a\n -- removal (wrong tag).\n (old.MemberKey is null\n and verify(digest('Strand.MemberPeer', 'add', new.MemberKey, new.PeerId, new.StampId), context.Signature, new.MemberKey, 'ed25519'))\n\n -- or the member clears its OWN binding.\n or (new.MemberKey is null\n and verify(digest('Strand.MemberPeer', 'remove', old.MemberKey, old.PeerId, old.StampId), context.Signature, old.MemberKey, 'ed25519'))\n\n -- or a manager clears ANOTHER member's binding \u2014 the cleanup path after a\n -- revocation, which the departing member has no reason to cooperate with.\n -- Gated to DELETE and bound to a manager-remove-tagged digest over the exact\n -- row + stamp. The 'manager-remove' tag (vs the self branch's 'remove') is\n -- belt-and-braces ONLY \u2014 the two branches verify against different keys.\n -- The authorizer is read from the PRE-transaction snapshot\n -- (committed.Manager) for the same reason Member.Authorized does: this check\n -- defers to commit, by which point a manager seated in the SAME transaction\n -- would otherwise be able to authorize its own cleanup.\n or (new.MemberKey is null and exists (\n select 1 from committed.Manager A\n where A.MemberKey = context.ManagerKey\n and verify(digest('Strand.MemberPeer', 'manager-remove', old.MemberKey, old.PeerId, old.StampId), context.ManagerSignature, A.MemberKey, 'ed25519')\n ))\n ),\n ) with context (Signature text null, ManagerKey text null, ManagerSignature text null);\n\n -- A manager is a member that can issue invites, authorize members, and rotate managers\n table Manager (\n MemberKey text primary key,\n -- Lineage ordering. The founding manager is generation 0; every later manager is\n -- seated strictly after (greater than) the manager that appointed it. This is what\n -- makes same-transaction mutual promotion impossible: a deferred CHECK sees only\n -- the post-image, so \"did my authorizer exist before this transaction?\" is not\n -- directly askable \u2014 but the minimum-generation row of any inserted set must find\n -- its authorizer among rows of strictly smaller generation, which can only be a\n -- pre-existing one. Generation is NOT a privilege level: a generation-5 manager\n -- has exactly the same powers as a generation-1 manager.\n Generation integer not null,\n StampId text not null unique, -- per-row authorization nonce, bound into the signed\n -- digests below; retired into Revocation on delete\n -- (same idiom as Member.StampId, rationale there).\n -- Refuses any insert naming a retired stamp \u2014 see Member.NotRevoked. This is what\n -- makes a captured promotion approval single-use: once the manager is removed (its\n -- stamp retired), the approval that seated it can never re-seat it.\n constraint NotRevoked check on insert (\n not exists (select 1 from Revocation R where R.TableName = 'Manager' and R.StampId = new.StampId)\n ),\n -- A delete must retire its stamp in the same transaction \u2014 see Member.RevocationRecorded.\n constraint RevocationRecorded check on delete (\n exists (select 1 from Revocation R where R.TableName = 'Manager' and R.StampId = old.StampId)\n ),\n constraint OnlyClosed check (\n exists (select 1 from Header H where H.Type = 'c')\n ),\n -- A manager IS a member: the table comment has always said so, and since the\n -- single-use-approval work it is load-bearing \u2014 Revocation.Authorized verifies a\n -- delete's tombstone against a committed Member row, so a manager with no Member\n -- row cannot revoke, clear a peer binding, or even resign. Reads the LIVE Member\n -- table, not committed.Member, so an admit-then-promote in ONE transaction passes\n -- (the same shape ConsumedInvite.MemberExists allows for the invite join); the\n -- sibling Member insert is authorized at that same commit, so nothing is waived.\n -- Mask is explicit: a bare check defaults to insert|update, and update is\n -- forbidden outright by NoUpdate below. Nothing is needed on DELETE \u2014 the other\n -- half of the invariant is Member.NotAManager, which refuses to un-member a key\n -- that still holds a Manager row.\n -- NOTE: a per-transaction check against locally visible rows. Two partitioned nodes\n -- \u2014 one promoting X, one removing X's Member and Manager rows \u2014 can each pass\n -- locally and converge to a Manager row with no Member row. Same convergence class\n -- as the MinOneMember / NotRevoked / seal-propagation notes; not solved here.\n constraint MemberExists check on insert (\n exists (select 1 from Member M where M.Key = new.MemberKey)\n ),\n -- Rotation is insert + delete only. An update would let a resignation signature\n -- (which proves only that old.MemberKey consented) double as a hand-off: the\n -- former-manager branch would accept re-pointing the row at any new key.\n constraint NoUpdate check on update (false),\n -- A closed strand loses its LAST manager only by SEALING \u2014 a distinct,\n -- self-signed act (the 'seal' branch of Authorized below), never an ordinary\n -- resignation and never an admin removal. There is deliberately NO\n -- min-one-manager floor: a strand with no managers is frozen, not bricked.\n -- Every admission path (Invite, addMemberByManager, addManager) requires a\n -- Manager row, so a sealed strand's membership can never grow again \u2014 and that\n -- freeze is the privacy guarantee its remaining members are buying: nobody\n -- holds the power to admit a party who would then read the strand's whole\n -- history. Sealing is irreversible: the founding branch of Authorized closes\n -- for good once any manager stamp has been retired into Revocation.\n -- NOTE: a seal must PROPAGATE before it binds a given node \u2014 a node that has\n -- not yet converged on the delete still shows the manager row, so until the\n -- delete arrives that node admits as if unsealed. It is NOT only the\n -- ex-manager's own key that gains: every gate here reads the LOCALLY visible\n -- Manager rows, and ConsumedInvite.NotSealed is one of them, so a STRANGER\n -- holding an invitation issued before the seal can redeem it on such a node.\n -- Same convergence class as the MinOneMember / NotRevoked local-visibility\n -- notes; not solved here. Arrival IS covered, on the converged side:\n -- integration-tests' strand-membership-closed-strand-e2e gates a second node on\n -- the seal and then requires it to refuse every admission path.\n constraint Authorized check on insert, delete (\n -- Every signed branch below is a domain/action-tagged digest bound to the\n -- row's StampId \u2014 the same single-use idiom as Member.Authorized: an\n -- approval seats (or removes) exactly one incarnation of one row.\n\n -- Bootstrap: the founding manager is seated with no prior signer, at\n -- generation 0. Gated to INSERT (old.MemberKey is null) AND to the founding\n -- state \u2014 at most one Member, and this manager IS that member. Deferred\n -- checks see post-image state, so an ungated count test is also true for a\n -- DELETE that drops the count to <= 1, and for the INSERT half of a\n -- same-transaction swap of the sole manager. The trailing Member-exists test\n -- is belt-and-braces since MemberExists above enforces it on EVERY insert \u2014\n -- kept so this branch's own claim (\"this manager IS that member\") reads in\n -- place rather than by cross-reference.\n -- The Revocation gate closes this branch FOREVER once any manager seat has\n -- been retired: founding happens exactly once per strand (Header, then\n -- Member, then Manager, with no Revocation rows in existence), so on a\n -- SEALED strand a lone surviving member cannot re-seat itself as a\n -- generation-0 founder and start admitting. Accepted, deliberate: between\n -- its own founding Member and Manager inserts a founder could, with its own\n -- key, file a junk Revocation('Manager', ...) tombstone (RowIsGone permits\n -- stamps naming no real row) and permanently block its own founding \u2014 pure\n -- self-harm needing the founder's private key; no other party can reach\n -- that window (there are no other committed members yet).\n (old.MemberKey is null\n and new.Generation = 0\n and (select count(1) from Manager) <= 1\n and (select count(1) from Member) <= 1\n and exists (select 1 from Member M where M.Key = new.MemberKey)\n and not exists (select 1 from Revocation R where R.TableName = 'Manager'))\n\n -- or removed by the departing manager ITSELF, over one of two\n -- self-signed, action-tagged digests bound to its own row's stamp. The\n -- POST-image manager count is what distinguishes the two: 'resign' is\n -- an ordinary departure that leaves at least one manager standing,\n -- while 'seal' is the delete that empties the table and permanently\n -- freezes admission (see the table comment above). The tag is what\n -- makes each a signed statement of THAT intent \u2014 a captured\n -- resignation approval can never seal, nor a seal approval merely\n -- resign \u2014 and the stamp is still what makes each single-use: once\n -- this manager is re-promoted under a fresh stamp, neither captured\n -- approval removes the new row. Both counts are subqueries, so the\n -- whole check defers to commit and the counts are the post-image.\n or (\n old.MemberKey is not null\n and old.MemberKey = context.ManagerKey\n and (select count(1) from Manager) >= 1\n and verify(digest('Strand.Manager', 'resign', old.MemberKey, old.StampId), context.Signature, old.MemberKey, 'ed25519')\n )\n or (\n old.MemberKey is not null\n and old.MemberKey = context.ManagerKey\n and (select count(1) from Manager) = 0\n and verify(digest('Strand.Manager', 'seal', old.MemberKey, old.StampId), context.Signature, old.MemberKey, 'ed25519')\n )\n\n -- or a promotion signed by an EARLIER-generation manager, over the\n -- add-tagged digest binding (MemberKey, Generation, StampId) \u2014 so a\n -- captured promotion replays at neither a different generation nor a\n -- second incarnation of the row. The strict\n -- A.Generation < new.Generation is what closes same-transaction mutual\n -- promotion: this subquery runs at commit against the post-insert row\n -- set, so sibling rows inserted in the same transaction are visible \u2014\n -- but the minimum-generation row of any inserted set cannot find its\n -- authorizer among its siblings (that would contradict minimality), so\n -- that authorizer must be a pre-existing manager who really signed.\n -- A mutual pair or ring needs each generation strictly below another's\n -- in a cycle; impossible. Nothing sits below the founder's 0, so a\n -- chain cannot duck underneath either. The <> is subsumed by the\n -- ordering (a row's generation is never below its own) but kept to\n -- state the no-self-promotion intent locally.\n or (old.MemberKey is null and exists (\n select 1 from Manager A\n where A.MemberKey = context.ManagerKey\n and A.MemberKey <> new.MemberKey\n and A.Generation < new.Generation\n and verify(digest('Strand.Manager', 'add', new.MemberKey, new.Generation, new.StampId), context.Signature, A.MemberKey, 'ed25519')))\n\n -- or a removal authorized by ANOTHER existing manager, over the\n -- remove-tagged digest bound to (MemberKey, StampId). Deliberately no\n -- generation condition here: deletes are safe once inserts are (every\n -- accepting branch requires a Manager row in the post-image, and no\n -- attacker row can get there), and a generation gate would break a\n -- later-generation manager removing an earlier-generation one.\n or (new.MemberKey is null and exists (\n select 1 from Manager A\n where A.MemberKey = context.ManagerKey\n and A.MemberKey <> old.MemberKey\n and verify(digest('Strand.Manager', 'remove', old.MemberKey, old.StampId), context.Signature, A.MemberKey, 'ed25519')))\n )\n ) with context (ManagerKey text null, Signature text null);\n\n -- Permanent tombstones for retired per-row authorization nonces (StampIds) \u2014 the\n -- delete half of the retired-stamp anti-replay idiom on Member / Manager /\n -- MemberPeer. Adapted from CadreControl.Revocation (the idiom is stated in full on\n -- CadreControl.OwnerKey); two deliberate differences:\n -- 1. TableName is confined to the three guarded STRAND tables (RowIsGone below).\n -- 2. Authorized verifies against a committed MEMBER row, not an owner key: every\n -- delete path in this schema is driven by a manager or by the departing party,\n -- and both are members. The action tag is 'retire' where the control layer\n -- says 'remove'; the domain tag already keeps the two schemas' approvals\n -- disjoint, so the divergence is cosmetic \u2014 kept because 'retire' names the\n -- act (retiring a stamp), not a row operation.\n -- NOTE: the guarded tables' NotRevoked CHECK runs against locally visible rows, so\n -- a node that has not yet converged on a Revocation row can still accept a replayed\n -- add and the resurrected row coexists with the tombstone after merge \u2014 the same\n -- convergence class as the MinOneMember / Manager seal-propagation notes.\n -- NOTE: this table only grows \u2014 one row per membership deletion, plus any junk any\n -- member chooses to file, and Immutable means rows never go away. Fine at strand\n -- scale; if membership churn ever gets large it needs pruning bounded by something\n -- other than \"forever\".\n -- NOTE: nothing on the strand read side drops a row because its stamp is retired\n -- (NotRevoked is insert-only), which is why a tombstone filed against a live but\n -- not-yet-converged row is inert here. If a read path ever starts filtering on\n -- Revocation (as the control layer's listAuthorizedMembers does), filing a tombstone\n -- becomes a remote eviction primitive and Authorized below must narrow from \"any\n -- member\" to \"a manager, or the row's own owner\".\n table Revocation (\n TableName text, -- 'Member' | 'Manager' | 'MemberPeer' (confined by RowIsGone below)\n StampId text, -- the retired nonce\n primary key (TableName, StampId),\n constraint OnlyClosed check (\n exists (select 1 from Header H where H.Type = 'c')\n ),\n -- Retirement is permanent: clearing or re-pointing a tombstone would restore the replay.\n constraint Immutable check on update, delete (false),\n -- A stamp may only be retired once its row is actually gone. Deferred (subquery), so a\n -- delete in the same transaction has already landed. Retiring a stamp that never\n -- existed is permitted and harmless: a stamp carries 256 bits of CSPRNG output\n -- (strand-membership-writer.ts generateStrandStampId), so a future legitimate one\n -- cannot be guessed and pre-planted \u2014 junk stamps only grow the table.\n constraint RowIsGone check on insert (\n (new.TableName = 'Member' and not exists (select 1 from Member M where M.StampId = new.StampId))\n or (new.TableName = 'Manager' and not exists (select 1 from Manager A where A.StampId = new.StampId))\n or (new.TableName = 'MemberPeer' and not exists (select 1 from MemberPeer P where P.StampId = new.StampId))\n ),\n -- Filing a tombstone is a MEMBER action: every legitimate delete in this schema is\n -- driven by a manager or by the departing party \u2014 both members \u2014 and the tombstone\n -- rides the delete's own transaction. The authorizer is read from the\n -- PRE-transaction snapshot (committed.Member) for the same reason the sibling\n -- tables read committed.*: this check defers to commit, by which point a member\n -- seated in the same transaction is live. The digest binds the WHOLE row\n -- (TableName, StampId) under its own domain tag, so it is disjoint from every\n -- other rule \u2014 including the remove-tagged approval the same party signs in the\n -- SAME transaction for the delete this tombstone accompanies. RowIsGone and\n -- Immutable guard what authorization does not: retiring a stamp early, or\n -- withdrawing one later.\n constraint Authorized check on insert (\n exists (select 1 from committed.Member M\n where M.Key = context.MemberKey\n and verify(digest('Strand.Revocation', 'retire', new.TableName, new.StampId), context.Signature, M.Key, 'ed25519'))\n )\n ) with context (MemberKey text, Signature text);\n";
@@ -13,8 +13,9 @@
13
13
  *
14
14
  * Drift discipline: the table declarations here MUST stay byte-equivalent to the
15
15
  * body of `schemas/strand.qsql` (the on-disk canonical copy). Any edit here must
16
- * be mirrored there and vice versa — the same invariant the planned
17
- * `control-schema-drift-guard` enforces for the control schema.
16
+ * be mirrored there and vice versa. `test/strand-schema-drift.spec.ts` enforces it
17
+ * — but only from THIS package, so an edit made in `schemas/strand.qsql` alone
18
+ * stays green everywhere its author is likely to look; run that spec after either.
18
19
  *
19
20
  * Crypto idiom (matches `schemas/control.qsql` and the sApp RBAC fixture
20
21
  * `packages/integration-tests/fixtures/simple-sapp.qsql`): every signed write
@@ -118,6 +119,10 @@ export const STRAND_SCHEMA = ` table Header (
118
119
  -- manager must survive the transaction, and sealing plus consuming in the SAME
119
120
  -- transaction is rejected too. Blocking this insert blocks the whole join, for
120
121
  -- exactly the reason NotCancelled gives above.
122
+ -- The Manager rows this reads are the ones THIS NODE can see, so the gate binds
123
+ -- only once the seal has reached here — on a node still behind on the seal it
124
+ -- passes, and a stranger holding a pre-seal invitation joins (the Manager table's
125
+ -- seal-propagation NOTE).
121
126
  constraint NotSealed check on insert (
122
127
  exists (select 1 from Manager)
123
128
  )
@@ -414,10 +419,15 @@ export const STRAND_SCHEMA = ` table Header (
414
419
  -- history. Sealing is irreversible: the founding branch of Authorized closes
415
420
  -- for good once any manager stamp has been retired into Revocation.
416
421
  -- NOTE: a seal must PROPAGATE before it binds a given node — a node that has
417
- -- not yet converged on the delete still shows the manager row, so the
418
- -- ex-manager's OWN key could still admit there until the delete arrives. Only
419
- -- that one key; no stranger gains anything. Same convergence class as the
420
- -- MinOneMember / NotRevoked local-visibility notes; not solved here.
422
+ -- not yet converged on the delete still shows the manager row, so until the
423
+ -- delete arrives that node admits as if unsealed. It is NOT only the
424
+ -- ex-manager's own key that gains: every gate here reads the LOCALLY visible
425
+ -- Manager rows, and ConsumedInvite.NotSealed is one of them, so a STRANGER
426
+ -- holding an invitation issued before the seal can redeem it on such a node.
427
+ -- Same convergence class as the MinOneMember / NotRevoked local-visibility
428
+ -- notes; not solved here. Arrival IS covered, on the converged side:
429
+ -- integration-tests' strand-membership-closed-strand-e2e gates a second node on
430
+ -- the seal and then requires it to refuse every admission path.
421
431
  constraint Authorized check on insert, delete (
422
432
  -- Every signed branch below is a domain/action-tagged digest bound to the
423
433
  -- row's StampId — the same single-use idiom as Member.Authorized: an
@@ -1 +1 @@
1
- {"version":3,"file":"strand-schema.js","sourceRoot":"","sources":["../src/strand-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgiB5B,CAAC"}
1
+ {"version":3,"file":"strand-schema.js","sourceRoot":"","sources":["../src/strand-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyiB5B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serfab/quereus-plugin-sereus",
3
- "version": "0.12.0",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "description": "Quereus plugin that connects to a Sereus strand for SQL access",
6
6
  "main": "dist/index.js",
@@ -67,13 +67,13 @@
67
67
  "@libp2p/circuit-relay-v2": "^4.1.3",
68
68
  "@libp2p/interface": "^3.1.0",
69
69
  "@libp2p/websockets": "^10.1.3",
70
- "@optimystic/db-core": "^0.27.0",
71
- "@optimystic/db-p2p": "^0.27.0",
72
- "@optimystic/db-p2p-storage-fs": "^0.27.0",
73
- "@optimystic/db-p2p-storage-web": "^0.27.0",
74
- "@optimystic/quereus-plugin-crypto": "^0.27.0",
75
- "@optimystic/quereus-plugin-optimystic": "^0.27.0",
76
- "@quereus/quereus": "^4.18.0",
70
+ "@optimystic/db-core": "^1.0.0",
71
+ "@optimystic/db-p2p": "^1.0.0",
72
+ "@optimystic/db-p2p-storage-fs": "^1.0.0",
73
+ "@optimystic/db-p2p-storage-web": "^1.0.0",
74
+ "@optimystic/quereus-plugin-crypto": "^1.0.0",
75
+ "@optimystic/quereus-plugin-optimystic": "^1.0.0",
76
+ "@quereus/quereus": "^4.19.4",
77
77
  "debug": "^4.4.3"
78
78
  },
79
79
  "quereus": {