@serfab/quereus-plugin-sereus 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cached-storage.d.ts +45 -16
- package/dist/cached-storage.js +87 -34
- package/dist/cached-storage.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/plugin-browser.js +76848 -77386
- package/dist/plugin-browser.js.map +4 -4
- package/dist/strand-schema.d.ts +1 -1
- package/dist/strand-schema.js +62 -21
- package/dist/strand-schema.js.map +1 -1
- package/package.json +8 -8
- package/src/cached-storage.ts +105 -34
- package/src/compose-strand.ts +396 -323
- package/src/connect-browser.ts +58 -57
- package/src/connect.ts +39 -38
- package/src/strand-schema.ts +62 -21
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/strand-schema.ts
CHANGED
|
@@ -108,6 +108,18 @@ export const STRAND_SCHEMA = ` table Header (
|
|
|
108
108
|
-- rows — no caller-supplied context decides admission (unlike NotExpired's Now).
|
|
109
109
|
constraint NotCancelled check on insert (
|
|
110
110
|
not exists (select 1 from CancelledInvite C where C.InviteKey = new.InviteKey)
|
|
111
|
+
),
|
|
112
|
+
-- A SEALED strand (a closed strand whose Manager table its sole manager has
|
|
113
|
+
-- deliberately emptied — see the Manager table comment) admits nobody, and that
|
|
114
|
+
-- includes the holder of an unspent invitation issued BEFORE the seal.
|
|
115
|
+
-- cancelInvite itself needs a manager, so after sealing nobody could kill such
|
|
116
|
+
-- an invitation — this gate is what kills it. The subquery reads the LIVE
|
|
117
|
+
-- Manager table and defers to commit, so it sees the post-image: at least one
|
|
118
|
+
-- manager must survive the transaction, and sealing plus consuming in the SAME
|
|
119
|
+
-- transaction is rejected too. Blocking this insert blocks the whole join, for
|
|
120
|
+
-- exactly the reason NotCancelled gives above.
|
|
121
|
+
constraint NotSealed check on insert (
|
|
122
|
+
exists (select 1 from Manager)
|
|
111
123
|
)
|
|
112
124
|
) with context (InviteSignature text null, Now datetime null);
|
|
113
125
|
|
|
@@ -136,7 +148,8 @@ export const STRAND_SCHEMA = ` table Header (
|
|
|
136
148
|
-- "forever".
|
|
137
149
|
-- NOTE: a cancellation must PROPAGATE before the holder redeems — a holder consuming
|
|
138
150
|
-- on a node that has not yet seen this row still gets in. Same convergence class as
|
|
139
|
-
-- the NotRevoked / MinOneMember
|
|
151
|
+
-- the NotRevoked / MinOneMember local-visibility notes and the Manager table's
|
|
152
|
+
-- seal-propagation note; not solved here.
|
|
140
153
|
table CancelledInvite (
|
|
141
154
|
InviteKey text primary key,
|
|
142
155
|
constraint OnlyClosed check (
|
|
@@ -382,7 +395,7 @@ export const STRAND_SCHEMA = ` table Header (
|
|
|
382
395
|
-- NOTE: a per-transaction check against locally visible rows. Two partitioned nodes
|
|
383
396
|
-- — one promoting X, one removing X's Member and Manager rows — can each pass
|
|
384
397
|
-- locally and converge to a Manager row with no Member row. Same convergence class
|
|
385
|
-
-- as the MinOneMember /
|
|
398
|
+
-- as the MinOneMember / NotRevoked / seal-propagation notes; not solved here.
|
|
386
399
|
constraint MemberExists check on insert (
|
|
387
400
|
exists (select 1 from Member M where M.Key = new.MemberKey)
|
|
388
401
|
),
|
|
@@ -390,17 +403,21 @@ export const STRAND_SCHEMA = ` table Header (
|
|
|
390
403
|
-- (which proves only that old.MemberKey consented) double as a hand-off: the
|
|
391
404
|
-- former-manager branch would accept re-pointing the row at any new key.
|
|
392
405
|
constraint NoUpdate check on update (false),
|
|
393
|
-
-- A closed strand
|
|
394
|
-
--
|
|
395
|
-
--
|
|
396
|
-
--
|
|
397
|
-
--
|
|
398
|
-
--
|
|
399
|
-
--
|
|
400
|
-
-- the
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
406
|
+
-- A closed strand loses its LAST manager only by SEALING — a distinct,
|
|
407
|
+
-- self-signed act (the 'seal' branch of Authorized below), never an ordinary
|
|
408
|
+
-- resignation and never an admin removal. There is deliberately NO
|
|
409
|
+
-- min-one-manager floor: a strand with no managers is frozen, not bricked.
|
|
410
|
+
-- Every admission path (Invite, addMemberByManager, addManager) requires a
|
|
411
|
+
-- Manager row, so a sealed strand's membership can never grow again — and that
|
|
412
|
+
-- freeze is the privacy guarantee its remaining members are buying: nobody
|
|
413
|
+
-- holds the power to admit a party who would then read the strand's whole
|
|
414
|
+
-- history. Sealing is irreversible: the founding branch of Authorized closes
|
|
415
|
+
-- for good once any manager stamp has been retired into Revocation.
|
|
416
|
+
-- 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.
|
|
404
421
|
constraint Authorized check on insert, delete (
|
|
405
422
|
-- Every signed branch below is a domain/action-tagged digest bound to the
|
|
406
423
|
-- row's StampId — the same single-use idiom as Member.Authorized: an
|
|
@@ -415,23 +432,47 @@ export const STRAND_SCHEMA = ` table Header (
|
|
|
415
432
|
-- is belt-and-braces since MemberExists above enforces it on EVERY insert —
|
|
416
433
|
-- kept so this branch's own claim ("this manager IS that member") reads in
|
|
417
434
|
-- place rather than by cross-reference.
|
|
435
|
+
-- The Revocation gate closes this branch FOREVER once any manager seat has
|
|
436
|
+
-- been retired: founding happens exactly once per strand (Header, then
|
|
437
|
+
-- Member, then Manager, with no Revocation rows in existence), so on a
|
|
438
|
+
-- SEALED strand a lone surviving member cannot re-seat itself as a
|
|
439
|
+
-- generation-0 founder and start admitting. Accepted, deliberate: between
|
|
440
|
+
-- its own founding Member and Manager inserts a founder could, with its own
|
|
441
|
+
-- key, file a junk Revocation('Manager', ...) tombstone (RowIsGone permits
|
|
442
|
+
-- stamps naming no real row) and permanently block its own founding — pure
|
|
443
|
+
-- self-harm needing the founder's private key; no other party can reach
|
|
444
|
+
-- that window (there are no other committed members yet).
|
|
418
445
|
(old.MemberKey is null
|
|
419
446
|
and new.Generation = 0
|
|
420
447
|
and (select count(1) from Manager) <= 1
|
|
421
448
|
and (select count(1) from Member) <= 1
|
|
422
|
-
and exists (select 1 from Member M where M.Key = new.MemberKey)
|
|
449
|
+
and exists (select 1 from Member M where M.Key = new.MemberKey)
|
|
450
|
+
and not exists (select 1 from Revocation R where R.TableName = 'Manager'))
|
|
423
451
|
|
|
424
|
-
-- or
|
|
425
|
-
--
|
|
426
|
-
--
|
|
427
|
-
--
|
|
428
|
-
--
|
|
429
|
-
--
|
|
452
|
+
-- or removed by the departing manager ITSELF, over one of two
|
|
453
|
+
-- self-signed, action-tagged digests bound to its own row's stamp. The
|
|
454
|
+
-- POST-image manager count is what distinguishes the two: 'resign' is
|
|
455
|
+
-- an ordinary departure that leaves at least one manager standing,
|
|
456
|
+
-- while 'seal' is the delete that empties the table and permanently
|
|
457
|
+
-- freezes admission (see the table comment above). The tag is what
|
|
458
|
+
-- makes each a signed statement of THAT intent — a captured
|
|
459
|
+
-- resignation approval can never seal, nor a seal approval merely
|
|
460
|
+
-- resign — and the stamp is still what makes each single-use: once
|
|
461
|
+
-- this manager is re-promoted under a fresh stamp, neither captured
|
|
462
|
+
-- approval removes the new row. Both counts are subqueries, so the
|
|
463
|
+
-- whole check defers to commit and the counts are the post-image.
|
|
430
464
|
or (
|
|
431
465
|
old.MemberKey is not null
|
|
432
466
|
and old.MemberKey = context.ManagerKey
|
|
467
|
+
and (select count(1) from Manager) >= 1
|
|
433
468
|
and verify(digest('Strand.Manager', 'resign', old.MemberKey, old.StampId), context.Signature, old.MemberKey, 'ed25519')
|
|
434
469
|
)
|
|
470
|
+
or (
|
|
471
|
+
old.MemberKey is not null
|
|
472
|
+
and old.MemberKey = context.ManagerKey
|
|
473
|
+
and (select count(1) from Manager) = 0
|
|
474
|
+
and verify(digest('Strand.Manager', 'seal', old.MemberKey, old.StampId), context.Signature, old.MemberKey, 'ed25519')
|
|
475
|
+
)
|
|
435
476
|
|
|
436
477
|
-- or a promotion signed by an EARLIER-generation manager, over the
|
|
437
478
|
-- add-tagged digest binding (MemberKey, Generation, StampId) — so a
|
|
@@ -483,7 +524,7 @@ export const STRAND_SCHEMA = ` table Header (
|
|
|
483
524
|
-- NOTE: the guarded tables' NotRevoked CHECK runs against locally visible rows, so
|
|
484
525
|
-- a node that has not yet converged on a Revocation row can still accept a replayed
|
|
485
526
|
-- add and the resurrected row coexists with the tombstone after merge — the same
|
|
486
|
-
-- convergence class as the MinOneMember /
|
|
527
|
+
-- convergence class as the MinOneMember / Manager seal-propagation notes.
|
|
487
528
|
-- NOTE: this table only grows — one row per membership deletion, plus any junk any
|
|
488
529
|
-- member chooses to file, and Immutable means rows never go away. Fine at strand
|
|
489
530
|
-- scale; if membership churn ever gets large it needs pruning bounded by something
|