@serfab/quereus-plugin-sereus 0.11.0 → 0.13.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/dist/cached-storage.d.ts +45 -16
- package/dist/cached-storage.js +87 -34
- package/dist/cached-storage.js.map +1 -1
- package/dist/cluster-size.d.ts +175 -7
- package/dist/cluster-size.js +207 -7
- package/dist/cluster-size.js.map +1 -1
- package/dist/compose-strand.d.ts +14 -1
- package/dist/compose-strand.js +83 -33
- package/dist/compose-strand.js.map +1 -1
- package/dist/connect-browser.js +2 -1
- package/dist/connect-browser.js.map +1 -1
- package/dist/connect.js +2 -1
- package/dist/connect.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/plugin-browser.js +78614 -78119
- package/dist/plugin-browser.js.map +4 -4
- package/dist/strand-schema.d.ts +4 -3
- package/dist/strand-schema.js +74 -23
- package/dist/strand-schema.js.map +1 -1
- package/package.json +8 -8
- package/src/cached-storage.ts +105 -34
- package/src/cluster-size.ts +455 -248
- package/src/compose-strand.ts +396 -323
- package/src/connect-browser.ts +58 -57
- package/src/connect.ts +39 -38
- package/src/index.ts +4 -1
- package/src/strand-schema.ts +74 -23
package/src/connect-browser.ts
CHANGED
|
@@ -1,57 +1,58 @@
|
|
|
1
|
-
import type { Database, VTablePluginInfo, FunctionPluginInfo, CollationPluginInfo } from '@quereus/quereus';
|
|
2
|
-
import cryptoPlugin from '@optimystic/quereus-plugin-crypto/plugin';
|
|
3
|
-
import { createLibp2pNode } from '@optimystic/db-p2p/rn';
|
|
4
|
-
import { webSockets } from '@libp2p/websockets';
|
|
5
|
-
import { circuitRelayTransport } from '@libp2p/circuit-relay-v2';
|
|
6
|
-
import { IndexedDBRawStorage, openOptimysticWebDb } from '@optimystic/db-p2p-storage-web';
|
|
7
|
-
import type { Libp2p } from '@libp2p/interface';
|
|
8
|
-
import type { StrandConnectionOptions, SereusPluginResult } from './types.js';
|
|
9
|
-
import { composeStrand, applyRegistrations } from './compose-strand.js';
|
|
10
|
-
|
|
11
|
-
/** The subset of the crypto plugin result we register inline in the browser. */
|
|
12
|
-
interface CryptoPluginResult {
|
|
13
|
-
vtables: VTablePluginInfo[];
|
|
14
|
-
functions: FunctionPluginInfo[];
|
|
15
|
-
collations: CollationPluginInfo[];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Connect a Quereus Database to a Sereus strand from a browser/worker.
|
|
20
|
-
*
|
|
21
|
-
* Thin adapter over the shared {@link composeStrand} composition, supplying the
|
|
22
|
-
* browser platform seams:
|
|
23
|
-
* - crypto registered inline (no runtime `@quereus/quereus` import, so the
|
|
24
|
-
* bundle does not duplicate the host's instance),
|
|
25
|
-
* - storage defaulting to `IndexedDBRawStorage` keyed by `sereus-strand-<id>`,
|
|
26
|
-
* - a node created via the TCP-free `@optimystic/db-p2p/rn` entry with explicit
|
|
27
|
-
* `webSockets()` + `circuitRelayTransport()` transports.
|
|
28
|
-
*/
|
|
29
|
-
export async function connectToStrandBrowser(
|
|
30
|
-
db: Database,
|
|
31
|
-
options: StrandConnectionOptions,
|
|
32
|
-
): Promise<SereusPluginResult> {
|
|
33
|
-
return composeStrand(db, options, {
|
|
34
|
-
registerCrypto(database) {
|
|
35
|
-
applyRegistrations(database, cryptoPlugin(database, {}) as unknown as CryptoPluginResult);
|
|
36
|
-
},
|
|
37
|
-
async resolveStorage({ strandId, resolvedTransactor, requestedStorage }) {
|
|
38
|
-
if (requestedStorage) return requestedStorage;
|
|
39
|
-
// Browsers always default to IndexedDB so a reload survives; the
|
|
40
|
-
// unit-test fake transactor needs no storage.
|
|
41
|
-
if (resolvedTransactor === 'test') return undefined;
|
|
42
|
-
const dbHandle = await openOptimysticWebDb(`sereus-strand-${strandId}`);
|
|
43
|
-
return new IndexedDBRawStorage(dbHandle);
|
|
44
|
-
},
|
|
45
|
-
async createNode({ networkName, bootstrapNodes, fretProfile, clusterSize, storage }) {
|
|
46
|
-
return createLibp2pNode({
|
|
47
|
-
transports: [webSockets(), circuitRelayTransport()],
|
|
48
|
-
listenAddrs: [],
|
|
49
|
-
bootstrapNodes,
|
|
50
|
-
networkName,
|
|
51
|
-
fretProfile,
|
|
52
|
-
clusterSize,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
1
|
+
import type { Database, VTablePluginInfo, FunctionPluginInfo, CollationPluginInfo } from '@quereus/quereus';
|
|
2
|
+
import cryptoPlugin from '@optimystic/quereus-plugin-crypto/plugin';
|
|
3
|
+
import { createLibp2pNode } from '@optimystic/db-p2p/rn';
|
|
4
|
+
import { webSockets } from '@libp2p/websockets';
|
|
5
|
+
import { circuitRelayTransport } from '@libp2p/circuit-relay-v2';
|
|
6
|
+
import { IndexedDBRawStorage, openOptimysticWebDb } from '@optimystic/db-p2p-storage-web';
|
|
7
|
+
import type { Libp2p } from '@libp2p/interface';
|
|
8
|
+
import type { StrandConnectionOptions, SereusPluginResult } from './types.js';
|
|
9
|
+
import { composeStrand, applyRegistrations } from './compose-strand.js';
|
|
10
|
+
|
|
11
|
+
/** The subset of the crypto plugin result we register inline in the browser. */
|
|
12
|
+
interface CryptoPluginResult {
|
|
13
|
+
vtables: VTablePluginInfo[];
|
|
14
|
+
functions: FunctionPluginInfo[];
|
|
15
|
+
collations: CollationPluginInfo[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Connect a Quereus Database to a Sereus strand from a browser/worker.
|
|
20
|
+
*
|
|
21
|
+
* Thin adapter over the shared {@link composeStrand} composition, supplying the
|
|
22
|
+
* browser platform seams:
|
|
23
|
+
* - crypto registered inline (no runtime `@quereus/quereus` import, so the
|
|
24
|
+
* bundle does not duplicate the host's instance),
|
|
25
|
+
* - storage defaulting to `IndexedDBRawStorage` keyed by `sereus-strand-<id>`,
|
|
26
|
+
* - a node created via the TCP-free `@optimystic/db-p2p/rn` entry with explicit
|
|
27
|
+
* `webSockets()` + `circuitRelayTransport()` transports.
|
|
28
|
+
*/
|
|
29
|
+
export async function connectToStrandBrowser(
|
|
30
|
+
db: Database,
|
|
31
|
+
options: StrandConnectionOptions,
|
|
32
|
+
): Promise<SereusPluginResult> {
|
|
33
|
+
return composeStrand(db, options, {
|
|
34
|
+
registerCrypto(database) {
|
|
35
|
+
applyRegistrations(database, cryptoPlugin(database, {}) as unknown as CryptoPluginResult);
|
|
36
|
+
},
|
|
37
|
+
async resolveStorage({ strandId, resolvedTransactor, requestedStorage }) {
|
|
38
|
+
if (requestedStorage) return requestedStorage;
|
|
39
|
+
// Browsers always default to IndexedDB so a reload survives; the
|
|
40
|
+
// unit-test fake transactor needs no storage.
|
|
41
|
+
if (resolvedTransactor === 'test') return undefined;
|
|
42
|
+
const dbHandle = await openOptimysticWebDb(`sereus-strand-${strandId}`);
|
|
43
|
+
return new IndexedDBRawStorage(dbHandle);
|
|
44
|
+
},
|
|
45
|
+
async createNode({ networkName, bootstrapNodes, fretProfile, clusterSize, clusterPolicy, storage }) {
|
|
46
|
+
return createLibp2pNode({
|
|
47
|
+
transports: [webSockets(), circuitRelayTransport()],
|
|
48
|
+
listenAddrs: [],
|
|
49
|
+
bootstrapNodes,
|
|
50
|
+
networkName,
|
|
51
|
+
fretProfile,
|
|
52
|
+
clusterSize,
|
|
53
|
+
clusterPolicy,
|
|
54
|
+
...(storage && { storage }),
|
|
55
|
+
}) as Promise<Libp2p>;
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
package/src/connect.ts
CHANGED
|
@@ -1,38 +1,39 @@
|
|
|
1
|
-
import type { Database } from '@quereus/quereus';
|
|
2
|
-
import { registerPlugin } from '@quereus/quereus';
|
|
3
|
-
import cryptoPlugin from '@optimystic/quereus-plugin-crypto/plugin';
|
|
4
|
-
import type { Libp2p } from '@libp2p/interface';
|
|
5
|
-
import type { StrandConnectionOptions, SereusPluginResult } from './types.js';
|
|
6
|
-
import { composeStrand } from './compose-strand.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Connect a Quereus Database to a Sereus strand (Node).
|
|
10
|
-
*
|
|
11
|
-
* Thin adapter over the shared {@link composeStrand} composition, supplying the
|
|
12
|
-
* Node platform seams: crypto via `@quereus/quereus`'s `registerPlugin`, and a
|
|
13
|
-
* TCP libp2p node created (only when not injected) via `@optimystic/db-p2p`.
|
|
14
|
-
*/
|
|
15
|
-
export async function connectToStrand(
|
|
16
|
-
db: Database,
|
|
17
|
-
options: StrandConnectionOptions,
|
|
18
|
-
): Promise<SereusPluginResult> {
|
|
19
|
-
return composeStrand(db, options, {
|
|
20
|
-
async registerCrypto(database) {
|
|
21
|
-
await registerPlugin(database, cryptoPlugin);
|
|
22
|
-
},
|
|
23
|
-
async createNode({ networkName, bootstrapNodes, fretProfile, port, clusterSize, storage }) {
|
|
24
|
-
// Dynamically import to keep the module cross-platform friendly: this
|
|
25
|
-
// pulls the Node-only `@optimystic/db-p2p` (TCP) entry, and is only
|
|
26
|
-
// reached when actually creating a node (never for injected nodes).
|
|
27
|
-
const { createLibp2pNode } = await import('@optimystic/db-p2p');
|
|
28
|
-
return createLibp2pNode({
|
|
29
|
-
port,
|
|
30
|
-
bootstrapNodes,
|
|
31
|
-
networkName,
|
|
32
|
-
fretProfile,
|
|
33
|
-
clusterSize,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
1
|
+
import type { Database } from '@quereus/quereus';
|
|
2
|
+
import { registerPlugin } from '@quereus/quereus';
|
|
3
|
+
import cryptoPlugin from '@optimystic/quereus-plugin-crypto/plugin';
|
|
4
|
+
import type { Libp2p } from '@libp2p/interface';
|
|
5
|
+
import type { StrandConnectionOptions, SereusPluginResult } from './types.js';
|
|
6
|
+
import { composeStrand } from './compose-strand.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Connect a Quereus Database to a Sereus strand (Node).
|
|
10
|
+
*
|
|
11
|
+
* Thin adapter over the shared {@link composeStrand} composition, supplying the
|
|
12
|
+
* Node platform seams: crypto via `@quereus/quereus`'s `registerPlugin`, and a
|
|
13
|
+
* TCP libp2p node created (only when not injected) via `@optimystic/db-p2p`.
|
|
14
|
+
*/
|
|
15
|
+
export async function connectToStrand(
|
|
16
|
+
db: Database,
|
|
17
|
+
options: StrandConnectionOptions,
|
|
18
|
+
): Promise<SereusPluginResult> {
|
|
19
|
+
return composeStrand(db, options, {
|
|
20
|
+
async registerCrypto(database) {
|
|
21
|
+
await registerPlugin(database, cryptoPlugin);
|
|
22
|
+
},
|
|
23
|
+
async createNode({ networkName, bootstrapNodes, fretProfile, port, clusterSize, clusterPolicy, storage }) {
|
|
24
|
+
// Dynamically import to keep the module cross-platform friendly: this
|
|
25
|
+
// pulls the Node-only `@optimystic/db-p2p` (TCP) entry, and is only
|
|
26
|
+
// reached when actually creating a node (never for injected nodes).
|
|
27
|
+
const { createLibp2pNode } = await import('@optimystic/db-p2p');
|
|
28
|
+
return createLibp2pNode({
|
|
29
|
+
port,
|
|
30
|
+
bootstrapNodes,
|
|
31
|
+
networkName,
|
|
32
|
+
fretProfile,
|
|
33
|
+
clusterSize,
|
|
34
|
+
clusterPolicy,
|
|
35
|
+
...(storage && { storage }),
|
|
36
|
+
}) as Promise<Libp2p>;
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,10 @@ export {
|
|
|
5
5
|
CONTROL_CLUSTER_POLICY,
|
|
6
6
|
DEFAULT_STRAND_CLUSTER_SIZE,
|
|
7
7
|
STRAND_CLUSTER_POLICY,
|
|
8
|
-
resolveStrandClusterSize
|
|
8
|
+
resolveStrandClusterSize,
|
|
9
|
+
resolveRepairYardstick,
|
|
10
|
+
controlClusterPolicy,
|
|
11
|
+
strandClusterPolicy
|
|
9
12
|
} from './cluster-size.js';
|
|
10
13
|
export { wrapStorageWithCache, disposeStorageCache } from './cached-storage.js';
|
|
11
14
|
export type { StrandConnectionOptions, SereusPluginResult, StrandTransactor } from './types.js';
|
package/src/strand-schema.ts
CHANGED
|
@@ -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
|
|
17
|
-
*
|
|
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
|
|
@@ -108,6 +109,22 @@ export const STRAND_SCHEMA = ` table Header (
|
|
|
108
109
|
-- rows — no caller-supplied context decides admission (unlike NotExpired's Now).
|
|
109
110
|
constraint NotCancelled check on insert (
|
|
110
111
|
not exists (select 1 from CancelledInvite C where C.InviteKey = new.InviteKey)
|
|
112
|
+
),
|
|
113
|
+
-- A SEALED strand (a closed strand whose Manager table its sole manager has
|
|
114
|
+
-- deliberately emptied — see the Manager table comment) admits nobody, and that
|
|
115
|
+
-- includes the holder of an unspent invitation issued BEFORE the seal.
|
|
116
|
+
-- cancelInvite itself needs a manager, so after sealing nobody could kill such
|
|
117
|
+
-- an invitation — this gate is what kills it. The subquery reads the LIVE
|
|
118
|
+
-- Manager table and defers to commit, so it sees the post-image: at least one
|
|
119
|
+
-- manager must survive the transaction, and sealing plus consuming in the SAME
|
|
120
|
+
-- transaction is rejected too. Blocking this insert blocks the whole join, for
|
|
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).
|
|
126
|
+
constraint NotSealed check on insert (
|
|
127
|
+
exists (select 1 from Manager)
|
|
111
128
|
)
|
|
112
129
|
) with context (InviteSignature text null, Now datetime null);
|
|
113
130
|
|
|
@@ -136,7 +153,8 @@ export const STRAND_SCHEMA = ` table Header (
|
|
|
136
153
|
-- "forever".
|
|
137
154
|
-- NOTE: a cancellation must PROPAGATE before the holder redeems — a holder consuming
|
|
138
155
|
-- on a node that has not yet seen this row still gets in. Same convergence class as
|
|
139
|
-
-- the NotRevoked / MinOneMember
|
|
156
|
+
-- the NotRevoked / MinOneMember local-visibility notes and the Manager table's
|
|
157
|
+
-- seal-propagation note; not solved here.
|
|
140
158
|
table CancelledInvite (
|
|
141
159
|
InviteKey text primary key,
|
|
142
160
|
constraint OnlyClosed check (
|
|
@@ -382,7 +400,7 @@ export const STRAND_SCHEMA = ` table Header (
|
|
|
382
400
|
-- NOTE: a per-transaction check against locally visible rows. Two partitioned nodes
|
|
383
401
|
-- — one promoting X, one removing X's Member and Manager rows — can each pass
|
|
384
402
|
-- locally and converge to a Manager row with no Member row. Same convergence class
|
|
385
|
-
-- as the MinOneMember /
|
|
403
|
+
-- as the MinOneMember / NotRevoked / seal-propagation notes; not solved here.
|
|
386
404
|
constraint MemberExists check on insert (
|
|
387
405
|
exists (select 1 from Member M where M.Key = new.MemberKey)
|
|
388
406
|
),
|
|
@@ -390,17 +408,26 @@ export const STRAND_SCHEMA = ` table Header (
|
|
|
390
408
|
-- (which proves only that old.MemberKey consented) double as a hand-off: the
|
|
391
409
|
-- former-manager branch would accept re-pointing the row at any new key.
|
|
392
410
|
constraint NoUpdate check on update (false),
|
|
393
|
-
-- A closed strand
|
|
394
|
-
--
|
|
395
|
-
--
|
|
396
|
-
--
|
|
397
|
-
--
|
|
398
|
-
--
|
|
399
|
-
--
|
|
400
|
-
-- the
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
411
|
+
-- A closed strand loses its LAST manager only by SEALING — a distinct,
|
|
412
|
+
-- self-signed act (the 'seal' branch of Authorized below), never an ordinary
|
|
413
|
+
-- resignation and never an admin removal. There is deliberately NO
|
|
414
|
+
-- min-one-manager floor: a strand with no managers is frozen, not bricked.
|
|
415
|
+
-- Every admission path (Invite, addMemberByManager, addManager) requires a
|
|
416
|
+
-- Manager row, so a sealed strand's membership can never grow again — and that
|
|
417
|
+
-- freeze is the privacy guarantee its remaining members are buying: nobody
|
|
418
|
+
-- holds the power to admit a party who would then read the strand's whole
|
|
419
|
+
-- history. Sealing is irreversible: the founding branch of Authorized closes
|
|
420
|
+
-- for good once any manager stamp has been retired into Revocation.
|
|
421
|
+
-- NOTE: a seal must PROPAGATE before it binds a given node — a node that has
|
|
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.
|
|
404
431
|
constraint Authorized check on insert, delete (
|
|
405
432
|
-- Every signed branch below is a domain/action-tagged digest bound to the
|
|
406
433
|
-- row's StampId — the same single-use idiom as Member.Authorized: an
|
|
@@ -415,23 +442,47 @@ export const STRAND_SCHEMA = ` table Header (
|
|
|
415
442
|
-- is belt-and-braces since MemberExists above enforces it on EVERY insert —
|
|
416
443
|
-- kept so this branch's own claim ("this manager IS that member") reads in
|
|
417
444
|
-- place rather than by cross-reference.
|
|
445
|
+
-- The Revocation gate closes this branch FOREVER once any manager seat has
|
|
446
|
+
-- been retired: founding happens exactly once per strand (Header, then
|
|
447
|
+
-- Member, then Manager, with no Revocation rows in existence), so on a
|
|
448
|
+
-- SEALED strand a lone surviving member cannot re-seat itself as a
|
|
449
|
+
-- generation-0 founder and start admitting. Accepted, deliberate: between
|
|
450
|
+
-- its own founding Member and Manager inserts a founder could, with its own
|
|
451
|
+
-- key, file a junk Revocation('Manager', ...) tombstone (RowIsGone permits
|
|
452
|
+
-- stamps naming no real row) and permanently block its own founding — pure
|
|
453
|
+
-- self-harm needing the founder's private key; no other party can reach
|
|
454
|
+
-- that window (there are no other committed members yet).
|
|
418
455
|
(old.MemberKey is null
|
|
419
456
|
and new.Generation = 0
|
|
420
457
|
and (select count(1) from Manager) <= 1
|
|
421
458
|
and (select count(1) from Member) <= 1
|
|
422
|
-
and exists (select 1 from Member M where M.Key = new.MemberKey)
|
|
459
|
+
and exists (select 1 from Member M where M.Key = new.MemberKey)
|
|
460
|
+
and not exists (select 1 from Revocation R where R.TableName = 'Manager'))
|
|
423
461
|
|
|
424
|
-
-- or
|
|
425
|
-
--
|
|
426
|
-
--
|
|
427
|
-
--
|
|
428
|
-
--
|
|
429
|
-
--
|
|
462
|
+
-- or removed by the departing manager ITSELF, over one of two
|
|
463
|
+
-- self-signed, action-tagged digests bound to its own row's stamp. The
|
|
464
|
+
-- POST-image manager count is what distinguishes the two: 'resign' is
|
|
465
|
+
-- an ordinary departure that leaves at least one manager standing,
|
|
466
|
+
-- while 'seal' is the delete that empties the table and permanently
|
|
467
|
+
-- freezes admission (see the table comment above). The tag is what
|
|
468
|
+
-- makes each a signed statement of THAT intent — a captured
|
|
469
|
+
-- resignation approval can never seal, nor a seal approval merely
|
|
470
|
+
-- resign — and the stamp is still what makes each single-use: once
|
|
471
|
+
-- this manager is re-promoted under a fresh stamp, neither captured
|
|
472
|
+
-- approval removes the new row. Both counts are subqueries, so the
|
|
473
|
+
-- whole check defers to commit and the counts are the post-image.
|
|
430
474
|
or (
|
|
431
475
|
old.MemberKey is not null
|
|
432
476
|
and old.MemberKey = context.ManagerKey
|
|
477
|
+
and (select count(1) from Manager) >= 1
|
|
433
478
|
and verify(digest('Strand.Manager', 'resign', old.MemberKey, old.StampId), context.Signature, old.MemberKey, 'ed25519')
|
|
434
479
|
)
|
|
480
|
+
or (
|
|
481
|
+
old.MemberKey is not null
|
|
482
|
+
and old.MemberKey = context.ManagerKey
|
|
483
|
+
and (select count(1) from Manager) = 0
|
|
484
|
+
and verify(digest('Strand.Manager', 'seal', old.MemberKey, old.StampId), context.Signature, old.MemberKey, 'ed25519')
|
|
485
|
+
)
|
|
435
486
|
|
|
436
487
|
-- or a promotion signed by an EARLIER-generation manager, over the
|
|
437
488
|
-- add-tagged digest binding (MemberKey, Generation, StampId) — so a
|
|
@@ -483,7 +534,7 @@ export const STRAND_SCHEMA = ` table Header (
|
|
|
483
534
|
-- NOTE: the guarded tables' NotRevoked CHECK runs against locally visible rows, so
|
|
484
535
|
-- a node that has not yet converged on a Revocation row can still accept a replayed
|
|
485
536
|
-- add and the resurrected row coexists with the tombstone after merge — the same
|
|
486
|
-
-- convergence class as the MinOneMember /
|
|
537
|
+
-- convergence class as the MinOneMember / Manager seal-propagation notes.
|
|
487
538
|
-- NOTE: this table only grows — one row per membership deletion, plus any junk any
|
|
488
539
|
-- member chooses to file, and Immutable means rows never go away. Fine at strand
|
|
489
540
|
-- scale; if membership churn ever gets large it needs pruning bounded by something
|