@kubun/plugin-p2p 0.11.0 → 0.12.1
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/lib/context/group.js +1073 -746
- package/lib/context/hub.js +2 -1
- package/lib/context/join.js +50 -15
- package/lib/context/peer.d.ts +92 -0
- package/lib/context/peer.js +1216 -0
- package/lib/context/require-admin.d.ts +57 -0
- package/lib/context/require-admin.js +57 -0
- package/lib/context/sync.d.ts +12 -0
- package/lib/context/sync.js +234 -1
- package/lib/context/types.d.ts +56 -20
- package/lib/context/types.js +23 -3
- package/lib/groups/access-default-token.d.ts +44 -0
- package/lib/groups/access-default-token.js +103 -0
- package/lib/groups/admin-roster.d.ts +42 -117
- package/lib/groups/admin-roster.js +53 -168
- package/lib/groups/anchor-store.d.ts +15 -0
- package/lib/groups/anchor-store.js +37 -0
- package/lib/groups/app-cursor-store.d.ts +17 -0
- package/lib/groups/app-cursor-store.js +34 -0
- package/lib/groups/broadcast.d.ts +199 -81
- package/lib/groups/broadcast.js +443 -368
- package/lib/groups/catalog-token.d.ts +50 -0
- package/lib/groups/catalog-token.js +96 -0
- package/lib/groups/circle-projection.d.ts +32 -64
- package/lib/groups/circle-projection.js +95 -181
- package/lib/groups/circle-reducers.d.ts +71 -110
- package/lib/groups/circle-reducers.js +57 -127
- package/lib/groups/commit-adoption.d.ts +77 -0
- package/lib/groups/commit-adoption.js +108 -0
- package/lib/groups/commit-journal.d.ts +14 -0
- package/lib/groups/commit-journal.js +65 -0
- package/lib/groups/control-request.d.ts +66 -0
- package/lib/groups/control-request.js +101 -0
- package/lib/groups/events.d.ts +14 -1
- package/lib/groups/group-anchor.d.ts +18 -57
- package/lib/groups/group-anchor.js +15 -100
- package/lib/groups/group-crypto.d.ts +36 -13
- package/lib/groups/group-crypto.js +124 -50
- package/lib/groups/group-handle-registry.d.ts +106 -36
- package/lib/groups/group-handle-registry.js +351 -61
- package/lib/groups/group-handlers.d.ts +5 -4
- package/lib/groups/group-handlers.js +155 -29
- package/lib/groups/group-health-monitor.d.ts +1 -1
- package/lib/groups/group-health-monitor.js +1 -1
- package/lib/groups/group-mls.d.ts +35 -4
- package/lib/groups/group-mls.js +594 -62
- package/lib/groups/group-peer-manager.d.ts +212 -19
- package/lib/groups/group-peer-manager.js +1061 -69
- package/lib/groups/group-protocols.d.ts +485 -28
- package/lib/groups/group-protocols.js +289 -15
- package/lib/groups/invite-payload.d.ts +48 -0
- package/lib/groups/join-request-identity.d.ts +21 -0
- package/lib/groups/join-request-identity.js +43 -0
- package/lib/groups/join-utils.d.ts +60 -0
- package/lib/groups/join-utils.js +262 -0
- package/lib/groups/ledger-affected-events.d.ts +11 -6
- package/lib/groups/ledger-affected-events.js +15 -26
- package/lib/groups/ledger-commit-fold.d.ts +107 -0
- package/lib/groups/ledger-commit-fold.js +167 -0
- package/lib/groups/ledger-ingest.d.ts +124 -0
- package/lib/groups/ledger-ingest.js +212 -0
- package/lib/groups/ledger.d.ts +44 -0
- package/lib/groups/ledger.js +30 -0
- package/lib/groups/manager.d.ts +202 -19
- package/lib/groups/manager.js +623 -106
- package/lib/groups/mls-codec.d.ts +27 -6
- package/lib/groups/mls-codec.js +30 -15
- package/lib/groups/mls-encryptor.js +9 -6
- package/lib/groups/mls-group-handle.d.ts +37 -2
- package/lib/groups/mls-group-handle.js +12 -4
- package/lib/groups/mls-receive-errors.d.ts +0 -2
- package/lib/groups/mls-receive-errors.js +3 -6
- package/lib/groups/mls-state.d.ts +5 -3
- package/lib/groups/mls-state.js +27 -4
- package/lib/groups/peer-presence.d.ts +90 -0
- package/lib/groups/peer-presence.js +295 -0
- package/lib/groups/peer-selection.d.ts +37 -0
- package/lib/groups/peer-selection.js +72 -0
- package/lib/groups/roster-projection.d.ts +53 -0
- package/lib/groups/roster-projection.js +65 -0
- package/lib/groups/store-received-grant.d.ts +4 -3
- package/lib/groups/store-received-grant.js +80 -35
- package/lib/groups/store-received-revocation.d.ts +16 -13
- package/lib/groups/store-received-revocation.js +23 -22
- package/lib/hub/http-client.d.ts +10 -27
- package/lib/hub/http-client.js +2 -19
- package/lib/hub/hub-like.d.ts +57 -6
- package/lib/hub/hub-like.js +539 -52
- package/lib/hub/loopback-log-hub.d.ts +20 -0
- package/lib/hub/loopback-log-hub.js +178 -0
- package/lib/hub/manager.d.ts +25 -40
- package/lib/hub/manager.js +51 -94
- package/lib/hub/peer-scoped-hub-view.d.ts +35 -0
- package/lib/hub/peer-scoped-hub-view.js +92 -0
- package/lib/hub/sender-scoped-hub-view.d.ts +41 -0
- package/lib/hub/sender-scoped-hub-view.js +59 -0
- package/lib/hub/server-did.d.ts +67 -0
- package/lib/hub/server-did.js +131 -0
- package/lib/hub/wiring.d.ts +71 -14
- package/lib/hub/wiring.js +80 -23
- package/lib/index.d.ts +48 -13
- package/lib/index.js +257 -86
- package/lib/peer/connection-registry.d.ts +45 -0
- package/lib/peer/connection-registry.js +52 -0
- package/lib/peer/handlers.d.ts +69 -0
- package/lib/peer/handlers.js +110 -0
- package/lib/protocol.d.ts +213 -0
- package/lib/protocol.js +268 -0
- package/lib/schema.d.ts +2 -1
- package/lib/schema.js +805 -131
- package/lib/sync/access-default-sender.d.ts +23 -0
- package/lib/sync/access-default-sender.js +80 -0
- package/lib/sync/authorize.d.ts +57 -3
- package/lib/sync/authorize.js +45 -4
- package/lib/sync/broadcast-queue.d.ts +3 -4
- package/lib/sync/broadcast-queue.js +2 -3
- package/lib/sync/catalog-scope.d.ts +8 -7
- package/lib/sync/catalog-scope.js +55 -39
- package/lib/sync/errors.d.ts +9 -0
- package/lib/sync/errors.js +10 -0
- package/lib/sync/forwarder.d.ts +13 -19
- package/lib/sync/forwarder.js +13 -19
- package/lib/sync/handlers.js +54 -14
- package/lib/sync/hub-tunnel-sync-listener.d.ts +16 -6
- package/lib/sync/hub-tunnel-sync-listener.js +126 -37
- package/lib/sync/hub-tunnel-sync-provider.d.ts +31 -11
- package/lib/sync/hub-tunnel-sync-provider.js +68 -19
- package/lib/sync/merkle-channel.d.ts +9 -1
- package/lib/sync/merkle-channel.js +0 -0
- package/lib/sync/peer-registry.d.ts +14 -10
- package/lib/sync/peer-registry.js +0 -25
- package/lib/sync/sync-client.d.ts +11 -1
- package/lib/sync/sync-client.js +72 -10
- package/lib/sync/sync-manager.d.ts +22 -4
- package/lib/sync/sync-manager.js +39 -10
- package/lib/sync/tunnel-listeners.d.ts +52 -0
- package/lib/sync/tunnel-listeners.js +175 -0
- package/lib/sync/tunnel-observability.d.ts +25 -0
- package/lib/sync/tunnel-observability.js +63 -0
- package/lib/sync/tunnel-topics.d.ts +20 -0
- package/lib/sync/tunnel-topics.js +28 -0
- package/lib/types.d.ts +699 -28
- package/lib/types.js +0 -1
- package/lib/util/handler-error.d.ts +16 -0
- package/lib/util/handler-error.js +64 -0
- package/lib/util/mutex.d.ts +14 -1
- package/lib/util/mutex.js +37 -3
- package/package.json +59 -52
- package/lib/groups/ledger-entry.d.ts +0 -37
- package/lib/groups/ledger-entry.js +0 -56
- package/lib/groups/ledger-fold.d.ts +0 -56
- package/lib/groups/ledger-fold.js +0 -53
- package/lib/hub/did-observing-transport.d.ts +0 -75
- package/lib/hub/did-observing-transport.js +0 -126
- package/lib/hub/errors.d.ts +0 -24
- package/lib/hub/errors.js +0 -33
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { StoreProvider } from '@kubun/db';
|
|
2
|
+
import type { CommitJournal } from '@kumiai/rpc';
|
|
3
|
+
/**
|
|
4
|
+
* The `@kumiai/rpc` `CommitJournal` port for one group, backed by the device's
|
|
5
|
+
* single-slot commit-journal store. The port carries no group scope — the group
|
|
6
|
+
* a peer commits to is fixed for its lifetime — so the adapter closes over
|
|
7
|
+
* `groupID` and threads it into every store call.
|
|
8
|
+
*
|
|
9
|
+
* `get` maps the store's raw selectable (snake_case, decoded blobs) to the port's
|
|
10
|
+
* `JournalEntry`: an absent row is `null`, a null `accepted_as` omits `acceptedAs`
|
|
11
|
+
* (the outcome is unknown until the hub answers), and `commit`/`journal` are
|
|
12
|
+
* copied into fresh `Uint8Array`s the port owns.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createCommitJournal(stores: StoreProvider, groupID: string): CommitJournal;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { getP2PStore } from '@kubun/store-p2p';
|
|
2
|
+
/**
|
|
3
|
+
* The `@kumiai/rpc` `CommitJournal` port for one group, backed by the device's
|
|
4
|
+
* single-slot commit-journal store. The port carries no group scope — the group
|
|
5
|
+
* a peer commits to is fixed for its lifetime — so the adapter closes over
|
|
6
|
+
* `groupID` and threads it into every store call.
|
|
7
|
+
*
|
|
8
|
+
* `get` maps the store's raw selectable (snake_case, decoded blobs) to the port's
|
|
9
|
+
* `JournalEntry`: an absent row is `null`, a null `accepted_as` omits `acceptedAs`
|
|
10
|
+
* (the outcome is unknown until the hub answers), and `commit`/`journal` are
|
|
11
|
+
* copied into fresh `Uint8Array`s the port owns.
|
|
12
|
+
*/ export function createCommitJournal(stores, groupID) {
|
|
13
|
+
return {
|
|
14
|
+
async put (entry) {
|
|
15
|
+
const store = await getP2PStore(stores);
|
|
16
|
+
await store.putCommitJournalEntry({
|
|
17
|
+
groupID,
|
|
18
|
+
publishID: entry.publishID,
|
|
19
|
+
expectedHead: entry.expectedHead,
|
|
20
|
+
epoch: entry.epoch,
|
|
21
|
+
...entry.acceptedAs != null ? {
|
|
22
|
+
acceptedAs: entry.acceptedAs
|
|
23
|
+
} : {},
|
|
24
|
+
commit: entry.commit,
|
|
25
|
+
bodies: entry.bodies,
|
|
26
|
+
kind: entry.kind,
|
|
27
|
+
journal: entry.journal
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
async markAccepted (publishID, sequenceID) {
|
|
31
|
+
const store = await getP2PStore(stores);
|
|
32
|
+
await store.markCommitJournalAccepted({
|
|
33
|
+
groupID,
|
|
34
|
+
publishID,
|
|
35
|
+
sequenceID
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
async get () {
|
|
39
|
+
const store = await getP2PStore(stores);
|
|
40
|
+
const row = await store.getCommitJournalEntry(groupID);
|
|
41
|
+
if (row == null) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
publishID: row.publish_id,
|
|
46
|
+
expectedHead: row.expected_head,
|
|
47
|
+
epoch: row.epoch,
|
|
48
|
+
...row.accepted_as != null ? {
|
|
49
|
+
acceptedAs: row.accepted_as
|
|
50
|
+
} : {},
|
|
51
|
+
commit: new Uint8Array(row.commit),
|
|
52
|
+
bodies: row.bodies,
|
|
53
|
+
kind: row.kind,
|
|
54
|
+
journal: new Uint8Array(row.journal)
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
async clear (publishID) {
|
|
58
|
+
const store = await getP2PStore(stores);
|
|
59
|
+
await store.clearCommitJournalEntry({
|
|
60
|
+
groupID,
|
|
61
|
+
publishID
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { StoreProvider } from '@kubun/db';
|
|
2
|
+
import type { Logger } from '@kubun/logger';
|
|
3
|
+
import { type ControlRequest, type ControlRequestOutcome } from '@kubun/store-p2p';
|
|
4
|
+
import type { ControlRequestData } from '../types.js';
|
|
5
|
+
import type { P2PEventEmitter } from './events.js';
|
|
6
|
+
/**
|
|
7
|
+
* Project a stored control-request row into its API-facing shape. `kind` and
|
|
8
|
+
* `status` are stored as bare text (no CHECK constraint), so the store types
|
|
9
|
+
* them as `string`; only this store's own writers ever produce them, and they
|
|
10
|
+
* write nothing outside the two unions.
|
|
11
|
+
*/
|
|
12
|
+
export declare function toControlRequestData(row: ControlRequest): ControlRequestData;
|
|
13
|
+
export type SettleControlRequestParams = {
|
|
14
|
+
stores: StoreProvider;
|
|
15
|
+
requestID: string;
|
|
16
|
+
outcome: ControlRequestOutcome;
|
|
17
|
+
error?: string;
|
|
18
|
+
/**
|
|
19
|
+
* The artifact this request produced. Written in the same update as the
|
|
20
|
+
* status and BEFORE the announcement, so a caller woken by the settle always
|
|
21
|
+
* finds it there.
|
|
22
|
+
*/
|
|
23
|
+
result?: string;
|
|
24
|
+
emitter?: P2PEventEmitter;
|
|
25
|
+
logger?: Logger;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Record a control request's terminal outcome and announce it.
|
|
29
|
+
*
|
|
30
|
+
* The row is RETAINED — a client holding the id must be able to read the
|
|
31
|
+
* outcome back — so this only moves `status`/`error`/`settledAt`; the row
|
|
32
|
+
* leaves on ack alone. The announcement carries the row as it was just
|
|
33
|
+
* written, read back rather than reconstructed, so a subscriber and a later
|
|
34
|
+
* query never disagree about the outcome.
|
|
35
|
+
*
|
|
36
|
+
* Settling an id with no row (already acked, or never enqueued) is a silent
|
|
37
|
+
* no-op that announces nothing: there is no outcome for anyone to observe.
|
|
38
|
+
*
|
|
39
|
+
* Never throws. Callers are the commit lane and the drive-failure path, where
|
|
40
|
+
* the outcome has already happened — a bookkeeping fault here cannot un-happen
|
|
41
|
+
* it, and must not turn an accepted commit into a lane error.
|
|
42
|
+
*/
|
|
43
|
+
export declare function settleControlRequest(params: SettleControlRequestParams): Promise<void>;
|
|
44
|
+
export type AwaitControlRequestSettledParams = {
|
|
45
|
+
stores: StoreProvider;
|
|
46
|
+
emitter: P2PEventEmitter;
|
|
47
|
+
requestID: string;
|
|
48
|
+
/** Rejects once this many milliseconds pass with the request still pending. */
|
|
49
|
+
timeoutMs: number;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Resolve when a control request reaches a terminal status.
|
|
53
|
+
*
|
|
54
|
+
* Both halves are load-bearing. The subscription alone loses every request that
|
|
55
|
+
* settles before the caller gets here — the common case for a fast lane — and
|
|
56
|
+
* the store read alone cannot observe a settle that has not happened yet. The
|
|
57
|
+
* listener is registered BEFORE the read so there is no window between them
|
|
58
|
+
* where a settle is neither in the row nor in the stream.
|
|
59
|
+
*
|
|
60
|
+
* The timeout is mandatory and REJECTS: callers await this inside a mutation,
|
|
61
|
+
* where an unbounded wait is a request that never answers. A request with no
|
|
62
|
+
* row (never enqueued, or already acked) is indistinguishable from one whose
|
|
63
|
+
* enqueue has not committed yet, so it waits rather than failing fast, and the
|
|
64
|
+
* timeout is what ends it.
|
|
65
|
+
*/
|
|
66
|
+
export declare function awaitControlRequestSettled(params: AwaitControlRequestSettledParams): Promise<ControlRequestData>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { getP2PStore } from '@kubun/store-p2p';
|
|
2
|
+
/**
|
|
3
|
+
* Project a stored control-request row into its API-facing shape. `kind` and
|
|
4
|
+
* `status` are stored as bare text (no CHECK constraint), so the store types
|
|
5
|
+
* them as `string`; only this store's own writers ever produce them, and they
|
|
6
|
+
* write nothing outside the two unions.
|
|
7
|
+
*/ export function toControlRequestData(row) {
|
|
8
|
+
return {
|
|
9
|
+
id: row.id,
|
|
10
|
+
groupID: row.group_id,
|
|
11
|
+
kind: row.kind,
|
|
12
|
+
status: row.status,
|
|
13
|
+
error: row.error,
|
|
14
|
+
enqueuedAt: row.enqueued_at,
|
|
15
|
+
settledAt: row.settled_at
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Record a control request's terminal outcome and announce it.
|
|
20
|
+
*
|
|
21
|
+
* The row is RETAINED — a client holding the id must be able to read the
|
|
22
|
+
* outcome back — so this only moves `status`/`error`/`settledAt`; the row
|
|
23
|
+
* leaves on ack alone. The announcement carries the row as it was just
|
|
24
|
+
* written, read back rather than reconstructed, so a subscriber and a later
|
|
25
|
+
* query never disagree about the outcome.
|
|
26
|
+
*
|
|
27
|
+
* Settling an id with no row (already acked, or never enqueued) is a silent
|
|
28
|
+
* no-op that announces nothing: there is no outcome for anyone to observe.
|
|
29
|
+
*
|
|
30
|
+
* Never throws. Callers are the commit lane and the drive-failure path, where
|
|
31
|
+
* the outcome has already happened — a bookkeeping fault here cannot un-happen
|
|
32
|
+
* it, and must not turn an accepted commit into a lane error.
|
|
33
|
+
*/ export async function settleControlRequest(params) {
|
|
34
|
+
try {
|
|
35
|
+
const store = await getP2PStore(params.stores);
|
|
36
|
+
await store.settleControlRequest({
|
|
37
|
+
id: params.requestID,
|
|
38
|
+
outcome: params.outcome,
|
|
39
|
+
...params.error != null ? {
|
|
40
|
+
error: params.error
|
|
41
|
+
} : {},
|
|
42
|
+
...params.result != null ? {
|
|
43
|
+
result: params.result
|
|
44
|
+
} : {},
|
|
45
|
+
settledAt: new Date().toISOString()
|
|
46
|
+
});
|
|
47
|
+
const row = await store.getControlRequest(params.requestID);
|
|
48
|
+
if (row == null || params.emitter == null) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
await params.emitter.emit('controlRequestSettled', toControlRequestData(row));
|
|
52
|
+
} catch (error) {
|
|
53
|
+
params.logger?.error('settling a control request failed', {
|
|
54
|
+
requestID: params.requestID,
|
|
55
|
+
outcome: params.outcome,
|
|
56
|
+
error
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolve when a control request reaches a terminal status.
|
|
62
|
+
*
|
|
63
|
+
* Both halves are load-bearing. The subscription alone loses every request that
|
|
64
|
+
* settles before the caller gets here — the common case for a fast lane — and
|
|
65
|
+
* the store read alone cannot observe a settle that has not happened yet. The
|
|
66
|
+
* listener is registered BEFORE the read so there is no window between them
|
|
67
|
+
* where a settle is neither in the row nor in the stream.
|
|
68
|
+
*
|
|
69
|
+
* The timeout is mandatory and REJECTS: callers await this inside a mutation,
|
|
70
|
+
* where an unbounded wait is a request that never answers. A request with no
|
|
71
|
+
* row (never enqueued, or already acked) is indistinguishable from one whose
|
|
72
|
+
* enqueue has not committed yet, so it waits rather than failing fast, and the
|
|
73
|
+
* timeout is what ends it.
|
|
74
|
+
*/ export async function awaitControlRequestSettled(params) {
|
|
75
|
+
const { requestID } = params;
|
|
76
|
+
let unsubscribe;
|
|
77
|
+
let timer;
|
|
78
|
+
try {
|
|
79
|
+
const settled = new Promise((resolve, reject)=>{
|
|
80
|
+
unsubscribe = params.emitter.on('controlRequestSettled', (data)=>{
|
|
81
|
+
if (data.id === requestID) {
|
|
82
|
+
resolve(data);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
timer = setTimeout(()=>{
|
|
86
|
+
reject(new Error(`control request ${requestID} did not settle in ${params.timeoutMs}ms`));
|
|
87
|
+
}, params.timeoutMs);
|
|
88
|
+
});
|
|
89
|
+
const store = await getP2PStore(params.stores);
|
|
90
|
+
const row = await store.getControlRequest(requestID);
|
|
91
|
+
if (row != null && row.status !== 'pending') {
|
|
92
|
+
return toControlRequestData(row);
|
|
93
|
+
}
|
|
94
|
+
return await settled;
|
|
95
|
+
} finally{
|
|
96
|
+
unsubscribe?.();
|
|
97
|
+
if (timer != null) {
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
package/lib/groups/events.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from '@sozai/event';
|
|
2
|
-
import type { CircleData, CircleMemberData, DelegationData, GroupData, GroupHealthData, GroupMemberData, RevokedDelegationData } from '../types.js';
|
|
2
|
+
import type { CircleData, CircleMemberData, ControlRequestData, DelegationData, GroupData, GroupHealthData, GroupMemberData, HubServerDIDChangedData, RevokedDelegationData } from '../types.js';
|
|
3
3
|
export type P2PEventMap = {
|
|
4
4
|
groupJoined: GroupData;
|
|
5
5
|
groupLeft: GroupData & {
|
|
@@ -55,9 +55,22 @@ export type P2PEventMap = {
|
|
|
55
55
|
hubID: string;
|
|
56
56
|
hubURL: string;
|
|
57
57
|
};
|
|
58
|
+
/**
|
|
59
|
+
* The hub at this URL answers under a different DID than the device had
|
|
60
|
+
* recorded; the new one is adopted and the connection re-dialled with it.
|
|
61
|
+
*/
|
|
62
|
+
hubServerDIDChanged: HubServerDIDChangedData;
|
|
58
63
|
delegationTokenAdded: DelegationData;
|
|
59
64
|
delegationTokenRevoked: RevokedDelegationData;
|
|
60
65
|
groupHealthChanged: GroupHealthData;
|
|
66
|
+
/**
|
|
67
|
+
* A control request reached a terminal status; carries the settled request as
|
|
68
|
+
* it was written, so a subscriber and a later query agree on the outcome.
|
|
69
|
+
* Emitted by the commit lane when a commit lands, by the drive path when the
|
|
70
|
+
* lane refuses one, and by the restart adopt path for a commit whose process
|
|
71
|
+
* died before it could report.
|
|
72
|
+
*/
|
|
73
|
+
controlRequestSettled: ControlRequestData;
|
|
61
74
|
};
|
|
62
75
|
export type P2PEventEmitter = EventEmitter<P2PEventMap>;
|
|
63
76
|
export declare function createP2PEventEmitter(): P2PEventEmitter;
|
|
@@ -1,63 +1,24 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { GroupContextExtension } from '@kumiai/mls';
|
|
2
|
+
import { type GroupAnchor } from '@kumiai/mls';
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* The anchor's opaque `app` slot as kubun writes it. `@kumiai/mls` owns the
|
|
5
|
+
* anchor container (creator DID, version) and never reads this payload.
|
|
6
|
+
*
|
|
7
|
+
* `recoverySecret` is base64 of 32 cryptographically-random bytes minted once at
|
|
8
|
+
* group creation. It is the epoch-independent seed for the non-rotating
|
|
9
|
+
* handshake/recovery topic: baked into the MLS GroupContext, authenticated by
|
|
10
|
+
* the GroupInfo signature, so every member on every epoch derives the same
|
|
11
|
+
* rendezvous and a peer stranded on a stale epoch can always rejoin.
|
|
6
12
|
*/
|
|
7
|
-
export
|
|
8
|
-
/**
|
|
9
|
-
* Genesis anchor baked into the MLS GroupContext at group creation: the creator
|
|
10
|
-
* DID is the epoch-0 admin. It survives every epoch and is authenticated by the
|
|
11
|
-
* GroupInfo signature, so every joiner reads the same value. Treated as
|
|
12
|
-
* immutable for the lifetime of the group.
|
|
13
|
-
*/
|
|
14
|
-
export type GroupAnchor = {
|
|
15
|
-
creatorDID: string;
|
|
16
|
-
version: number;
|
|
17
|
-
/**
|
|
18
|
-
* Base64 of 32 cryptographically-random bytes minted once at group creation.
|
|
19
|
-
* It is the epoch-independent seed for the non-rotating handshake/recovery
|
|
20
|
-
* topic: baked into the MLS GroupContext, authenticated by the GroupInfo
|
|
21
|
-
* signature, so every member on every epoch derives the same rendezvous and a
|
|
22
|
-
* peer stranded on a stale epoch can always rejoin. Immutable for the group's
|
|
23
|
-
* whole life.
|
|
24
|
-
*/
|
|
13
|
+
export type GroupAnchorApp = {
|
|
25
14
|
recoverySecret: string;
|
|
26
15
|
};
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
* Tolerant decode: returns null on malformed bytes or wrong shape. The
|
|
30
|
-
* `recoverySecret` is required and must be a non-empty string — an anchor
|
|
31
|
-
* without it cannot seed the recovery rendezvous, so a missing/empty value is a
|
|
32
|
-
* malformed anchor, not a tolerated absence.
|
|
33
|
-
*/
|
|
34
|
-
export declare function decodeGroupAnchor(bytes: Uint8Array): GroupAnchor | null;
|
|
35
|
-
export declare function buildGroupAnchorExtension(anchor: GroupAnchor): GroupContextExtension;
|
|
36
|
-
export declare function buildCurrentGroupAnchorExtension(creatorDID: string, recoverySecret: string): GroupContextExtension;
|
|
37
|
-
/**
|
|
38
|
-
* Leaf-node capabilities advertising the genesis-anchor extension. RFC 9420
|
|
39
|
-
* requires every member leaf to advertise each custom GroupContext extension
|
|
40
|
-
* type, or `commitInvite` rejects the added leaf. Pass these at both
|
|
41
|
-
* `createGroup` (creator leaf) and `createKeyPackageBundle` (invitee leaf) so an
|
|
42
|
-
* anchored group can be joined.
|
|
43
|
-
*/
|
|
44
|
-
export declare function groupAnchorCapabilities(): Capabilities;
|
|
45
|
-
/**
|
|
46
|
-
* Commit policy enforcing genesis-anchor immutability. The anchor lives in the
|
|
47
|
-
* GroupContext extensions and is written once at group creation; kubun issues no
|
|
48
|
-
* group-context-extensions proposal through any normal path, so any incoming
|
|
49
|
-
* commit or proposal carrying one is anomalous and is refused before it can
|
|
50
|
-
* alter the anchor. Refine to inspect the specific extension type if a future
|
|
51
|
-
* feature legitimately mutates non-anchor group-context extensions.
|
|
52
|
-
*/
|
|
53
|
-
export declare const anchorImmutabilityPolicy: IncomingMessageCallback;
|
|
16
|
+
/** Build the genesis-anchor extension for a group kubun is creating. */
|
|
17
|
+
export declare function buildKubunGroupAnchorExtension(creatorDID: string, recoverySecret: string): GroupContextExtension;
|
|
54
18
|
/**
|
|
55
|
-
* Read the
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* rather than silently downgrading to the forgeable role-column gate. (The anchor
|
|
60
|
-
* is kubun-written, frozen by the immutability commit policy, and authenticated
|
|
61
|
-
* by the GroupInfo signature, so this is a corruption guard, not a forgery path.)
|
|
19
|
+
* Read the recovery seed out of an anchor's `app` slot. Throws when the payload
|
|
20
|
+
* is missing or malformed: the anchor is written once by kubun and authenticated
|
|
21
|
+
* by the GroupInfo signature, so an unreadable seed is corruption, not absence,
|
|
22
|
+
* and a group whose seed cannot be read cannot participate in recovery.
|
|
62
23
|
*/
|
|
63
|
-
export declare function
|
|
24
|
+
export declare function readRecoverySecret(anchor: GroupAnchor): string;
|
|
@@ -1,107 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
* the MLS default extension types (1–5) and clear of every reserved GREASE
|
|
5
|
-
* value, so it can never collide with a ts-mls built-in or a probing extension.
|
|
6
|
-
*/ export const GROUP_ANCHOR_EXTENSION_TYPE = 0xf100;
|
|
7
|
-
const CURRENT_VERSION = 1;
|
|
8
|
-
const encoder = new TextEncoder();
|
|
9
|
-
const decoder = new TextDecoder();
|
|
10
|
-
export function encodeGroupAnchor(anchor) {
|
|
11
|
-
return encoder.encode(JSON.stringify(anchor));
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Tolerant decode: returns null on malformed bytes or wrong shape. The
|
|
15
|
-
* `recoverySecret` is required and must be a non-empty string — an anchor
|
|
16
|
-
* without it cannot seed the recovery rendezvous, so a missing/empty value is a
|
|
17
|
-
* malformed anchor, not a tolerated absence.
|
|
18
|
-
*/ export function decodeGroupAnchor(bytes) {
|
|
19
|
-
let parsed;
|
|
20
|
-
try {
|
|
21
|
-
parsed = JSON.parse(decoder.decode(bytes));
|
|
22
|
-
} catch {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
if (parsed == null || typeof parsed !== 'object') {
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
const record = parsed;
|
|
29
|
-
if (typeof record.creatorDID !== 'string' || typeof record.version !== 'number') {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
if (typeof record.recoverySecret !== 'string' || record.recoverySecret.length === 0) {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
return {
|
|
36
|
-
creatorDID: record.creatorDID,
|
|
37
|
-
version: record.version,
|
|
38
|
-
recoverySecret: record.recoverySecret
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
export function buildGroupAnchorExtension(anchor) {
|
|
42
|
-
return makeCustomExtension({
|
|
43
|
-
extensionType: GROUP_ANCHOR_EXTENSION_TYPE,
|
|
44
|
-
extensionData: encodeGroupAnchor(anchor)
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
export function buildCurrentGroupAnchorExtension(creatorDID, recoverySecret) {
|
|
48
|
-
return buildGroupAnchorExtension({
|
|
49
|
-
creatorDID,
|
|
50
|
-
version: CURRENT_VERSION,
|
|
1
|
+
import { buildCurrentGroupAnchorExtension } from '@kumiai/mls';
|
|
2
|
+
/** Build the genesis-anchor extension for a group kubun is creating. */ export function buildKubunGroupAnchorExtension(creatorDID, recoverySecret) {
|
|
3
|
+
return buildCurrentGroupAnchorExtension(creatorDID, {
|
|
51
4
|
recoverySecret
|
|
52
5
|
});
|
|
53
6
|
}
|
|
54
7
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
...base,
|
|
64
|
-
extensions: [
|
|
65
|
-
...base.extensions,
|
|
66
|
-
GROUP_ANCHOR_EXTENSION_TYPE
|
|
67
|
-
]
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Commit policy enforcing genesis-anchor immutability. The anchor lives in the
|
|
72
|
-
* GroupContext extensions and is written once at group creation; kubun issues no
|
|
73
|
-
* group-context-extensions proposal through any normal path, so any incoming
|
|
74
|
-
* commit or proposal carrying one is anomalous and is refused before it can
|
|
75
|
-
* alter the anchor. Refine to inspect the specific extension type if a future
|
|
76
|
-
* feature legitimately mutates non-anchor group-context extensions.
|
|
77
|
-
*/ export const anchorImmutabilityPolicy = (incoming)=>{
|
|
78
|
-
const proposals = incoming.kind === 'commit' ? incoming.proposals : [
|
|
79
|
-
incoming.proposal
|
|
80
|
-
];
|
|
81
|
-
for (const { proposal } of proposals){
|
|
82
|
-
if (proposal.proposalType === defaultProposalTypes.group_context_extensions) {
|
|
83
|
-
return 'reject';
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return 'accept';
|
|
87
|
-
};
|
|
88
|
-
/**
|
|
89
|
-
* Read the genesis anchor from a group handle. Returns null only when the anchor
|
|
90
|
-
* extension is genuinely absent (a group created before the anchor, or
|
|
91
|
-
* external). A present-but-undecodable extension is corruption, not absence, and
|
|
92
|
-
* throws — so the control gate treats it as "anchor unreadable" and fails closed
|
|
93
|
-
* rather than silently downgrading to the forgeable role-column gate. (The anchor
|
|
94
|
-
* is kubun-written, frozen by the immutability commit policy, and authenticated
|
|
95
|
-
* by the GroupInfo signature, so this is a corruption guard, not a forgery path.)
|
|
96
|
-
*/ export function readGroupAnchor(handle) {
|
|
97
|
-
const extension = handle.state.groupContext.extensions.find((ext)=>ext.extensionType === GROUP_ANCHOR_EXTENSION_TYPE);
|
|
98
|
-
if (extension == null) {
|
|
99
|
-
return null;
|
|
8
|
+
* Read the recovery seed out of an anchor's `app` slot. Throws when the payload
|
|
9
|
+
* is missing or malformed: the anchor is written once by kubun and authenticated
|
|
10
|
+
* by the GroupInfo signature, so an unreadable seed is corruption, not absence,
|
|
11
|
+
* and a group whose seed cannot be read cannot participate in recovery.
|
|
12
|
+
*/ export function readRecoverySecret(anchor) {
|
|
13
|
+
const app = anchor.app;
|
|
14
|
+
if (app == null || typeof app !== 'object') {
|
|
15
|
+
throw new Error('group anchor carries no application payload; cannot derive recovery secret');
|
|
100
16
|
}
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
throw new Error('group anchor extension present but could not be decoded');
|
|
17
|
+
const { recoverySecret } = app;
|
|
18
|
+
if (typeof recoverySecret !== 'string' || recoverySecret.length === 0) {
|
|
19
|
+
throw new Error('group anchor carries no recovery secret');
|
|
105
20
|
}
|
|
106
|
-
return
|
|
21
|
+
return recoverySecret;
|
|
107
22
|
}
|
|
@@ -1,19 +1,42 @@
|
|
|
1
1
|
import type { GroupCrypto } from '@kumiai/rpc';
|
|
2
|
+
import { type Runtime } from '@sozai/runtime';
|
|
2
3
|
import type { GroupHandleRegistry } from './group-handle-registry.js';
|
|
3
4
|
/**
|
|
4
|
-
* Adapts a `GroupHandleRegistry` group into
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Adapts a `GroupHandleRegistry` group into `@kumiai/rpc`'s {@link GroupCrypto}
|
|
6
|
+
* port: epoch, an epoch-bound topic-derivation secret, and byte-level
|
|
7
|
+
* encrypt/decrypt over the live MLS handle.
|
|
7
8
|
*
|
|
8
|
-
* `wrap`/`unwrap`/`exportSecret` route through `registry.readHandle
|
|
9
|
-
*
|
|
10
|
-
* `
|
|
9
|
+
* `wrap`/`unwrap`/`exportSecret` route through `registry.readHandle`, taking the
|
|
10
|
+
* per-group mutex and observing the canonical handle, framed as in
|
|
11
|
+
* `mls-codec.ts`. `exportSecret` passes the caller's label through untouched:
|
|
12
|
+
* the labels belong to the package deriving topics from the result, so
|
|
13
|
+
* substituting one moves every topic ID away from what the caller addressed.
|
|
11
14
|
*
|
|
12
|
-
* `
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* `sealEntries`/`openEntries` are a SECOND seal, NOT interchangeable with
|
|
16
|
+
* `wrap`/`unwrap`: those consume a ratchet generation and mutate the handle, so
|
|
17
|
+
* they cannot serve an open running inside the apply of the commit carrying the
|
|
18
|
+
* blob. The bytes must match `@kumiai/mls-rpc` exactly — the seal is agreed
|
|
19
|
+
* without exchange, so a divergence is not a decode error but a group whose
|
|
20
|
+
* members silently stop reading each other's commits.
|
|
21
|
+
*
|
|
22
|
+
* The epoch is READ FROM THE REGISTRY, never cached: applying someone else's
|
|
23
|
+
* commit advances the handle without touching this file, and a stale number
|
|
24
|
+
* classifies every later frame as `ahead`, so the cursor skips it forever.
|
|
25
|
+
* `initialEpoch` covers only construction before the registry has observed a
|
|
26
|
+
* handle — the caller MUST read the handle's epoch first, or a returning peer
|
|
27
|
+
* answering `0` steps over every commit it missed. Construction is synchronous
|
|
28
|
+
* and eager, leaving no window to prime it afterwards.
|
|
18
29
|
*/
|
|
19
|
-
export
|
|
30
|
+
export type GroupCryptoParams = {
|
|
31
|
+
registry: GroupHandleRegistry;
|
|
32
|
+
groupID: string;
|
|
33
|
+
/** The handle's CURRENT epoch — see the note above on why `0` is not a safe placeholder. */
|
|
34
|
+
initialEpoch: number;
|
|
35
|
+
/**
|
|
36
|
+
* Host runtime supplying the seal's randomness, so the source follows the
|
|
37
|
+
* platform the host is on. Pass the host's — the default constructs a SECOND
|
|
38
|
+
* runtime rather than propagating one.
|
|
39
|
+
*/
|
|
40
|
+
runtime?: Runtime;
|
|
41
|
+
};
|
|
42
|
+
export declare function createGroupCrypto(params: GroupCryptoParams): GroupCrypto;
|