@kubun/plugin-p2p 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/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 +56 -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,103 @@
|
|
|
1
|
+
import { isVerifiedToken, normalizeDID, stringifyToken, verifyToken } from '@kokuin/token';
|
|
2
|
+
export async function signAccessDefaultSet(identity, claim) {
|
|
3
|
+
const signed = await identity.signToken({
|
|
4
|
+
op: 'set',
|
|
5
|
+
...claim
|
|
6
|
+
}, {
|
|
7
|
+
embedLongForm: true
|
|
8
|
+
});
|
|
9
|
+
return stringifyToken(signed);
|
|
10
|
+
}
|
|
11
|
+
export async function signAccessDefaultRemove(identity, claim) {
|
|
12
|
+
const signed = await identity.signToken({
|
|
13
|
+
op: 'remove',
|
|
14
|
+
...claim
|
|
15
|
+
}, {
|
|
16
|
+
embedLongForm: true
|
|
17
|
+
});
|
|
18
|
+
return stringifyToken(signed);
|
|
19
|
+
}
|
|
20
|
+
function isPermissionType(value) {
|
|
21
|
+
return value === 'read' || value === 'write';
|
|
22
|
+
}
|
|
23
|
+
/** The levels the store's read predicate is written for, and no others. */ const ACCESS_LEVELS = new Set([
|
|
24
|
+
'only_owner',
|
|
25
|
+
'anyone',
|
|
26
|
+
'restricted'
|
|
27
|
+
]);
|
|
28
|
+
function isDIDList(value) {
|
|
29
|
+
return value == null || Array.isArray(value) && value.every((entry)=>typeof entry === 'string');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Every field, not just `level`.
|
|
33
|
+
*
|
|
34
|
+
* This is the only gate between the wire and `setUserModelAccessDefault`: the
|
|
35
|
+
* applier writes the rule verbatim. Checking `level` alone let a member land an
|
|
36
|
+
* arbitrary string as an access level, and non-array values in the allow-lists,
|
|
37
|
+
* in its own rows on every peer — shapes the receiving device's SQL read
|
|
38
|
+
* predicate was never written for. The blast radius is the sender's own rows
|
|
39
|
+
* (the applier is sender-bound), which is a reason it is not urgent, not a
|
|
40
|
+
* reason to accept the shape.
|
|
41
|
+
*/ function isRule(value) {
|
|
42
|
+
if (value == null || typeof value !== 'object') {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const rule = value;
|
|
46
|
+
return ACCESS_LEVELS.has(rule.level) && isDIDList(rule.allowedDIDs) && isDIDList(rule.allowedCircles) && // Accepted from the wire but never minted locally: `setModelAccessDefaults`
|
|
47
|
+
// exposes circles only. Refusing it here would drop a frame a peer on an
|
|
48
|
+
// older build can still legitimately send, so it is validated and kept.
|
|
49
|
+
isDIDList(rule.allowedGroups);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Verify a signed access-default token and extract its claim. Returns `null`
|
|
53
|
+
* (never throws) when the token is unparseable, unsigned (`alg: 'none'`), or
|
|
54
|
+
* structurally malformed — an attacker cannot forge an `iss` this way. The
|
|
55
|
+
* issuer is the normalized verified `iss`.
|
|
56
|
+
*/ export async function verifyAccessDefault(token) {
|
|
57
|
+
let verified;
|
|
58
|
+
try {
|
|
59
|
+
verified = await verifyToken(token);
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
if (!isVerifiedToken(verified)) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const { iss, op, modelID, hlc } = verified.payload;
|
|
67
|
+
if (typeof modelID !== 'string' || typeof hlc !== 'string') {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const issuer = normalizeDID(iss);
|
|
71
|
+
if (op === 'set') {
|
|
72
|
+
const { permissionType, rule } = verified.payload;
|
|
73
|
+
if (!isPermissionType(permissionType) || !isRule(rule)) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
issuer,
|
|
78
|
+
claim: {
|
|
79
|
+
op: 'set',
|
|
80
|
+
modelID,
|
|
81
|
+
permissionType,
|
|
82
|
+
rule,
|
|
83
|
+
hlc
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (op === 'remove') {
|
|
88
|
+
const { permissionTypes } = verified.payload;
|
|
89
|
+
if (!Array.isArray(permissionTypes) || !permissionTypes.every(isPermissionType)) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
issuer,
|
|
94
|
+
claim: {
|
|
95
|
+
op: 'remove',
|
|
96
|
+
modelID,
|
|
97
|
+
permissionTypes,
|
|
98
|
+
hlc
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
@@ -1,130 +1,55 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type {
|
|
3
|
-
import type { GroupAnchor } from './group-anchor.js';
|
|
4
|
-
import { type VerifiedLedgerEntry } from './ledger-entry.js';
|
|
5
|
-
import { type FoldInput, type LedgerReducer } from './ledger-fold.js';
|
|
6
|
-
/** Ledger entry `type` for an admin-role grant or revocation. */
|
|
7
|
-
export declare const ADMIN_ROLE_ENTRY_TYPE = "group.role.admin";
|
|
1
|
+
import { type GroupAnchor, type LedgerLogEntry, type RoleValue } from '@kumiai/mls';
|
|
2
|
+
import type { PositionedFoldInput } from './ledger.js';
|
|
8
3
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* Ledger entry `type` for a role grant or demotion. This is `@kumiai/mls`'s own
|
|
5
|
+
* interpreted type: the same signed entry the library folds into the roster its
|
|
6
|
+
* commit policy enforces, so kubun's projection and the MLS authority that
|
|
7
|
+
* accepts or refuses a commit can never disagree about who is an admin.
|
|
11
8
|
*/
|
|
12
|
-
export declare const
|
|
9
|
+
export declare const ADMIN_ROLE_ENTRY_TYPE = "kumiai.role";
|
|
13
10
|
/**
|
|
14
|
-
* The claim carried by
|
|
15
|
-
* member
|
|
16
|
-
* admin
|
|
11
|
+
* The claim carried by a role entry: the permission the subject holds. Demotion
|
|
12
|
+
* is `member`, not a distinct "revoked" value — the library's roster is a
|
|
13
|
+
* DID→permission map, and an absent admin is simply a member.
|
|
17
14
|
*/
|
|
18
|
-
export type AdminRoleValue =
|
|
15
|
+
export type AdminRoleValue = RoleValue;
|
|
19
16
|
/** Folded admin set: the normalized DIDs currently holding the admin role. */
|
|
20
17
|
export type AdminRosterState = {
|
|
21
18
|
admins: Set<string>;
|
|
22
19
|
};
|
|
23
20
|
/**
|
|
24
|
-
*
|
|
25
|
-
* creator is the epoch-0 admin) and grows only through admins-so-far: every
|
|
26
|
-
* grant or revocation must be issued by a DID already in the set accumulated
|
|
27
|
-
* from strictly-earlier entries. That state-so-far check is what makes rotation
|
|
28
|
-
* sound — an admin can revoke the very creator that granted them without
|
|
29
|
-
* retroactively voiding their own earlier grants.
|
|
30
|
-
*/
|
|
31
|
-
export declare const adminRosterReducer: LedgerReducer<AdminRoleValue, AdminRosterState>;
|
|
32
|
-
/**
|
|
33
|
-
* Fold the admin-role ledger into the current admin set. With `atHLC`, only
|
|
34
|
-
* entries at or before that HLC are folded, yielding the exact was-admin-at-HLC
|
|
35
|
-
* answer the current-only role column cannot express. The string `<=` compare on
|
|
36
|
-
* the serialized HLC is the total order (zero-padded, lexicographic), matching
|
|
37
|
-
* the membership-removal LWW elsewhere in the store.
|
|
38
|
-
*/
|
|
39
|
-
export declare function foldAdminRoster(entries: Array<FoldInput<AdminRoleValue>>, anchor: GroupAnchor, atHLC?: string): AdminRosterState;
|
|
40
|
-
export type ProjectAdminRosterParams = {
|
|
41
|
-
store: P2PStoreAPI;
|
|
42
|
-
groupID: string;
|
|
43
|
-
anchor: GroupAnchor;
|
|
44
|
-
/** The group's admin-role ledger entries, in any order. */
|
|
45
|
-
entries: Array<FoldInput<AdminRoleValue>>;
|
|
46
|
-
/** Current active members whose role overlay should be reconciled. */
|
|
47
|
-
memberDIDs: Array<string>;
|
|
48
|
-
};
|
|
49
|
-
/**
|
|
50
|
-
* Project the folded admin set onto the store's `role` column. Membership stays
|
|
51
|
-
* MLS-native — this only overlays a role on members that already exist: each
|
|
52
|
-
* current member is set to `admin` when the fold places them in the set and
|
|
53
|
-
* `member` otherwise. `setGroupMemberRole` is update-only, so a DID in the
|
|
54
|
-
* admin set with no membership row is silently skipped.
|
|
55
|
-
*/
|
|
56
|
-
export declare function projectAdminRoster(params: ProjectAdminRosterParams): Promise<void>;
|
|
57
|
-
/**
|
|
58
|
-
* Load the group's admin-role ledger entries and re-verify each `signed_token`
|
|
59
|
-
* into a fold input. The stored token is re-verified rather than trusting the
|
|
60
|
-
* decoded `value` column: the ledger is the source of truth and must stay
|
|
61
|
-
* self-verifying, so a stored token that no longer verifies is dropped and the
|
|
62
|
-
* rest survive. Returned in store order; the fold sorts by total order. Results
|
|
63
|
-
* are memoized per group on the stored id set (see {@link verifiedAdminEntryCache}).
|
|
64
|
-
*/
|
|
65
|
-
export declare function loadVerifiedAdminEntries(store: P2PStoreAPI, groupID: string, logger?: Logger): Promise<Array<FoldInput<AdminRoleValue>>>;
|
|
66
|
-
/**
|
|
67
|
-
* Re-project the admin overlay from the stored ledger. The active member set
|
|
68
|
-
* bounds the overlay — `projectAdminRoster` only touches rows that already
|
|
69
|
-
* exist, so an admin DID with no membership is silently skipped.
|
|
70
|
-
*/
|
|
71
|
-
export type ProjectAdminRosterFromStoreParams = {
|
|
72
|
-
store: P2PStoreAPI;
|
|
73
|
-
groupID: string;
|
|
74
|
-
anchor: GroupAnchor;
|
|
75
|
-
logger?: Logger;
|
|
76
|
-
};
|
|
77
|
-
export declare function projectAdminRosterFromStore(params: ProjectAdminRosterFromStoreParams): Promise<void>;
|
|
78
|
-
/**
|
|
79
|
-
* Was `did` a group admin at `atHLC`? Authority is folded from the
|
|
80
|
-
* anchor-seeded ledger evaluated at the operation's HLC (rotation-aware:
|
|
81
|
-
* was-admin-at-HLC, not is-admin-now), conjoined with membership at that HLC.
|
|
82
|
-
* Folding from the genesis anchor — not the local `role` column — is what makes
|
|
83
|
-
* a receiver agree with the author's peer: the creator is the epoch-0 admin on
|
|
84
|
-
* every peer regardless of how its membership row's role was written.
|
|
21
|
+
* Fold the role ledger into the admin set.
|
|
85
22
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
export declare function isLedgerAdminAtHLC(params: IsLedgerAdminAtHLCParams): Promise<boolean>;
|
|
98
|
-
/**
|
|
99
|
-
* Authoritatively rebuild the admin overlay from the whole ledger and reset the
|
|
100
|
-
* watermark to the newest folded entry. Used on a first projection, an
|
|
101
|
-
* out-of-order arrival, or drift repair — anywhere the cached projection can no
|
|
102
|
-
* longer be trusted as a fold-through-watermark. Seeds the admin set from the
|
|
103
|
-
* genesis anchor (the creator is the epoch-0 admin), so it never relies on a
|
|
104
|
-
* pre-existing `role` value in the store.
|
|
23
|
+
* With `throughPosition`, only entries enacted at or before that ledger slot are
|
|
24
|
+
* folded — the roster as it stood at that point in the group's history. That is
|
|
25
|
+
* the was-admin-then answer the current-only `role` column cannot express, and
|
|
26
|
+
* unlike the HLC it used to be keyed on, a position is assigned by the commit
|
|
27
|
+
* chain rather than asserted by the signer, so it cannot be backdated.
|
|
28
|
+
*
|
|
29
|
+
* The entries are folded in the order given, which is the order the handle's log
|
|
30
|
+
* holds them. Authority is rooted at the genesis anchor (the creator is the
|
|
31
|
+
* epoch-0 admin) and grows only through admins-so-far — `foldRoster` also drops
|
|
32
|
+
* an entry signed for another group and one that would leave the roster with
|
|
33
|
+
* zero admins.
|
|
105
34
|
*/
|
|
106
|
-
export
|
|
107
|
-
store: P2PStoreAPI;
|
|
108
|
-
groupID: string;
|
|
109
|
-
anchor: GroupAnchor;
|
|
110
|
-
logger?: Logger;
|
|
111
|
-
};
|
|
112
|
-
export declare function reprojectAdminRoster(params: ReprojectAdminRosterParams): Promise<void>;
|
|
35
|
+
export declare function foldAdminRoster(entries: Array<PositionedFoldInput<RoleValue>>, anchor: GroupAnchor, groupID: string, throughPosition?: number): AdminRosterState;
|
|
113
36
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
37
|
+
* Select the role entries out of a group's control ledger, as fold inputs
|
|
38
|
+
* carrying the ledger slot each one occupies.
|
|
39
|
+
*
|
|
40
|
+
* Every entry is already verified: the log is the handle's own, and each of its
|
|
41
|
+
* doors — `GroupHandle.applyLedgerEntries`, `bootstrapLedger`, an accepted
|
|
42
|
+
* commit — re-verifies what it admits. Nothing left to check, nothing to
|
|
43
|
+
* memoize; this replaced a loader that re-verified every stored `signed_token`
|
|
44
|
+
* per authority check, needed only because the SQL row could not vouch for
|
|
45
|
+
* itself.
|
|
46
|
+
*
|
|
47
|
+
* The position indexes the WHOLE ledger, not the selected subset, so a role
|
|
48
|
+
* entry's slot is comparable with a circle entry's — which is what makes "was
|
|
49
|
+
* this issuer an admin when that entry was enacted" answerable at all.
|
|
50
|
+
*
|
|
51
|
+
* The cast is unavoidable: the log is typed over an unknown claim value, and the
|
|
52
|
+
* entry `type` is what says the value is a role, so selecting by that type is
|
|
53
|
+
* what makes the narrowing true.
|
|
123
54
|
*/
|
|
124
|
-
export
|
|
125
|
-
store: P2PStoreAPI;
|
|
126
|
-
groupID: string;
|
|
127
|
-
verified: VerifiedLedgerEntry<AdminRoleValue>;
|
|
128
|
-
logger?: Logger;
|
|
129
|
-
};
|
|
130
|
-
export declare function applyAdminEntryIncremental(params: ApplyAdminEntryIncrementalParams): Promise<void>;
|
|
55
|
+
export declare function selectRoleEntries(ledger: ReadonlyArray<LedgerLogEntry>): Array<PositionedFoldInput<RoleValue>>;
|
|
@@ -1,184 +1,69 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { verifyLedgerEntry } from './ledger-entry.js';
|
|
3
|
-
import { foldLedger } from './ledger-fold.js';
|
|
4
|
-
/** Ledger entry `type` for an admin-role grant or revocation. */ export const ADMIN_ROLE_ENTRY_TYPE = 'group.role.admin';
|
|
1
|
+
import { foldRoster, ROLE_ENTRY_TYPE } from '@kumiai/mls';
|
|
5
2
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
*/ export const adminRosterReducer = {
|
|
17
|
-
type: ADMIN_ROLE_ENTRY_TYPE,
|
|
18
|
-
seed: (anchor)=>({
|
|
19
|
-
admins: new Set([
|
|
20
|
-
normalizeDID(anchor.creatorDID)
|
|
21
|
-
])
|
|
22
|
-
}),
|
|
23
|
-
verifyAuthority: (verified, state)=>state.admins.has(verified.issuer),
|
|
24
|
-
apply: (verified, state)=>{
|
|
25
|
-
const admins = new Set(state.admins);
|
|
26
|
-
const subject = normalizeDID(verified.entry.subject);
|
|
27
|
-
if (verified.entry.value === 'admin') {
|
|
28
|
-
admins.add(subject);
|
|
29
|
-
} else if (verified.entry.value === 'revoked') {
|
|
30
|
-
admins.delete(subject);
|
|
31
|
-
} else {
|
|
32
|
-
// Unknown claim value: leave the set untouched rather than guess intent.
|
|
33
|
-
return state;
|
|
3
|
+
* Ledger entry `type` for a role grant or demotion. This is `@kumiai/mls`'s own
|
|
4
|
+
* interpreted type: the same signed entry the library folds into the roster its
|
|
5
|
+
* commit policy enforces, so kubun's projection and the MLS authority that
|
|
6
|
+
* accepts or refuses a commit can never disagree about who is an admin.
|
|
7
|
+
*/ export const ADMIN_ROLE_ENTRY_TYPE = ROLE_ENTRY_TYPE;
|
|
8
|
+
function adminsFromRoles(roles) {
|
|
9
|
+
const admins = new Set();
|
|
10
|
+
for (const [did, permission] of roles){
|
|
11
|
+
if (permission === 'admin') {
|
|
12
|
+
admins.add(did);
|
|
34
13
|
}
|
|
35
|
-
return {
|
|
36
|
-
admins
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
/**
|
|
41
|
-
* Fold the admin-role ledger into the current admin set. With `atHLC`, only
|
|
42
|
-
* entries at or before that HLC are folded, yielding the exact was-admin-at-HLC
|
|
43
|
-
* answer the current-only role column cannot express. The string `<=` compare on
|
|
44
|
-
* the serialized HLC is the total order (zero-padded, lexicographic), matching
|
|
45
|
-
* the membership-removal LWW elsewhere in the store.
|
|
46
|
-
*/ export function foldAdminRoster(entries, anchor, atHLC) {
|
|
47
|
-
const scoped = atHLC == null ? entries : entries.filter(({ verified })=>verified.entry.hlc <= atHLC);
|
|
48
|
-
return foldLedger(scoped, anchor, adminRosterReducer);
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Project the folded admin set onto the store's `role` column. Membership stays
|
|
52
|
-
* MLS-native — this only overlays a role on members that already exist: each
|
|
53
|
-
* current member is set to `admin` when the fold places them in the set and
|
|
54
|
-
* `member` otherwise. `setGroupMemberRole` is update-only, so a DID in the
|
|
55
|
-
* admin set with no membership row is silently skipped.
|
|
56
|
-
*/ export async function projectAdminRoster(params) {
|
|
57
|
-
const { store, groupID, anchor, entries, memberDIDs } = params;
|
|
58
|
-
const { admins } = foldAdminRoster(entries, anchor);
|
|
59
|
-
for (const memberDID of memberDIDs){
|
|
60
|
-
const role = admins.has(normalizeDID(memberDID)) ? 'admin' : 'member';
|
|
61
|
-
await store.setGroupMemberRole(groupID, memberDID, role);
|
|
62
14
|
}
|
|
15
|
+
return admins;
|
|
63
16
|
}
|
|
64
17
|
/**
|
|
65
|
-
*
|
|
66
|
-
* stored entry ids. Verifying every stored token is the dominant cost of an
|
|
67
|
-
* authority check, and the gate folds on every control op, so re-verifying an
|
|
68
|
-
* unchanged ledger each time is pure waste. Reading the id set stays cheap (an
|
|
69
|
-
* indexed scan); signatures are re-verified only when the set changes. The key
|
|
70
|
-
* is derived purely from content-addressed entry ids — never the `role` column —
|
|
71
|
-
* so the cache can never reintroduce the column-trust bypass: a forged or
|
|
72
|
-
* altered entry produces a different id set, a different key, and a full
|
|
73
|
-
* re-verification. One slot per group bounds memory and self-invalidates, since
|
|
74
|
-
* the ledger is append-only and every append changes the id set.
|
|
18
|
+
* Fold the role ledger into the admin set.
|
|
75
19
|
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
20
|
+
* With `throughPosition`, only entries enacted at or before that ledger slot are
|
|
21
|
+
* folded — the roster as it stood at that point in the group's history. That is
|
|
22
|
+
* the was-admin-then answer the current-only `role` column cannot express, and
|
|
23
|
+
* unlike the HLC it used to be keyed on, a position is assigned by the commit
|
|
24
|
+
* chain rather than asserted by the signer, so it cannot be backdated.
|
|
25
|
+
*
|
|
26
|
+
* The entries are folded in the order given, which is the order the handle's log
|
|
27
|
+
* holds them. Authority is rooted at the genesis anchor (the creator is the
|
|
28
|
+
* epoch-0 admin) and grows only through admins-so-far — `foldRoster` also drops
|
|
29
|
+
* an entry signed for another group and one that would leave the roster with
|
|
30
|
+
* zero admins.
|
|
31
|
+
*/ export function foldAdminRoster(entries, anchor, groupID, throughPosition) {
|
|
32
|
+
const scoped = throughPosition == null ? entries : entries.filter(({ position })=>position <= throughPosition);
|
|
33
|
+
const roster = foldRoster(scoped, anchor, groupID);
|
|
34
|
+
return {
|
|
35
|
+
admins: adminsFromRoles(roster.roles)
|
|
36
|
+
};
|
|
87
37
|
}
|
|
88
38
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
39
|
+
* Select the role entries out of a group's control ledger, as fold inputs
|
|
40
|
+
* carrying the ledger slot each one occupies.
|
|
41
|
+
*
|
|
42
|
+
* Every entry is already verified: the log is the handle's own, and each of its
|
|
43
|
+
* doors — `GroupHandle.applyLedgerEntries`, `bootstrapLedger`, an accepted
|
|
44
|
+
* commit — re-verifies what it admits. Nothing left to check, nothing to
|
|
45
|
+
* memoize; this replaced a loader that re-verified every stored `signed_token`
|
|
46
|
+
* per authority check, needed only because the SQL row could not vouch for
|
|
47
|
+
* itself.
|
|
48
|
+
*
|
|
49
|
+
* The position indexes the WHOLE ledger, not the selected subset, so a role
|
|
50
|
+
* entry's slot is comparable with a circle entry's — which is what makes "was
|
|
51
|
+
* this issuer an admin when that entry was enacted" answerable at all.
|
|
52
|
+
*
|
|
53
|
+
* The cast is unavoidable: the log is typed over an unknown claim value, and the
|
|
54
|
+
* entry `type` is what says the value is a role, so selecting by that type is
|
|
55
|
+
* what makes the narrowing true.
|
|
56
|
+
*/ export function selectRoleEntries(ledger) {
|
|
102
57
|
const entries = [];
|
|
103
|
-
for (const
|
|
104
|
-
|
|
105
|
-
if (verified == null) {
|
|
106
|
-
logger?.warn('admin roster: dropped stored entry whose token no longer verifies', {
|
|
107
|
-
groupID,
|
|
108
|
-
entryID: row.entry_id
|
|
109
|
-
});
|
|
58
|
+
for (const [position, held] of ledger.entries()){
|
|
59
|
+
if (held.verified.entry.type !== ADMIN_ROLE_ENTRY_TYPE) {
|
|
110
60
|
continue;
|
|
111
61
|
}
|
|
112
62
|
entries.push({
|
|
113
|
-
verified,
|
|
114
|
-
entryID:
|
|
63
|
+
verified: held.verified,
|
|
64
|
+
entryID: held.entryID,
|
|
65
|
+
position
|
|
115
66
|
});
|
|
116
67
|
}
|
|
117
|
-
verifiedAdminEntryCache.set(groupID, {
|
|
118
|
-
key,
|
|
119
|
-
entries
|
|
120
|
-
});
|
|
121
68
|
return entries;
|
|
122
69
|
}
|
|
123
|
-
export async function projectAdminRosterFromStore(params) {
|
|
124
|
-
const { store, groupID, anchor, logger } = params;
|
|
125
|
-
const entries = await loadVerifiedAdminEntries(store, groupID, logger);
|
|
126
|
-
const memberDIDs = (await store.listGroupMembers(groupID)).map((member)=>member.member_did);
|
|
127
|
-
await projectAdminRoster({
|
|
128
|
-
store,
|
|
129
|
-
groupID,
|
|
130
|
-
anchor,
|
|
131
|
-
entries,
|
|
132
|
-
memberDIDs
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
export async function isLedgerAdminAtHLC(params) {
|
|
136
|
-
const { store, groupID, anchor, did, atHLC, logger } = params;
|
|
137
|
-
if (!await store.isGroupMemberAtHLC(groupID, did, atHLC)) return false;
|
|
138
|
-
const entries = await loadVerifiedAdminEntries(store, groupID, logger);
|
|
139
|
-
const { admins } = foldAdminRoster(entries, anchor, atHLC);
|
|
140
|
-
return admins.has(normalizeDID(did));
|
|
141
|
-
}
|
|
142
|
-
export async function reprojectAdminRoster(params) {
|
|
143
|
-
const { store, groupID, anchor, logger } = params;
|
|
144
|
-
const entries = await loadVerifiedAdminEntries(store, groupID, logger);
|
|
145
|
-
const memberDIDs = (await store.listGroupMembers(groupID)).map((member)=>member.member_did);
|
|
146
|
-
await projectAdminRoster({
|
|
147
|
-
store,
|
|
148
|
-
groupID,
|
|
149
|
-
anchor,
|
|
150
|
-
entries,
|
|
151
|
-
memberDIDs
|
|
152
|
-
});
|
|
153
|
-
// Watermark through the newest folded entry: the projection now equals the
|
|
154
|
-
// fold of every verified entry, so a later in-order arrival (strictly newer
|
|
155
|
-
// HLC) can be applied incrementally instead of forcing a full reproject.
|
|
156
|
-
let maxHLC = null;
|
|
157
|
-
for (const { verified } of entries){
|
|
158
|
-
if (maxHLC == null || verified.entry.hlc > maxHLC) {
|
|
159
|
-
maxHLC = verified.entry.hlc;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
if (maxHLC != null) {
|
|
163
|
-
await store.setProjectionWatermark(groupID, ADMIN_ROSTER_PROJECTION, maxHLC);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
export async function applyAdminEntryIncremental(params) {
|
|
167
|
-
const { store, groupID, verified, logger } = params;
|
|
168
|
-
const members = await store.listGroupMembers(groupID);
|
|
169
|
-
const currentAdmins = new Set(members.filter((m)=>m.role === 'admin').map((m)=>normalizeDID(m.member_did)));
|
|
170
|
-
if (!currentAdmins.has(verified.issuer)) {
|
|
171
|
-
logger?.warn('admin roster: dropped incremental entry from a non-admin issuer', {
|
|
172
|
-
groupID,
|
|
173
|
-
issuer: verified.issuer
|
|
174
|
-
});
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
const subject = normalizeDID(verified.entry.subject);
|
|
178
|
-
if (verified.entry.value === 'admin') {
|
|
179
|
-
await store.setGroupMemberRole(groupID, subject, 'admin');
|
|
180
|
-
} else if (verified.entry.value === 'revoked') {
|
|
181
|
-
await store.setGroupMemberRole(groupID, subject, 'member');
|
|
182
|
-
}
|
|
183
|
-
// Unknown value: no-op, mirroring the reducer's leave-untouched policy.
|
|
184
|
-
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { StoreProvider } from '@kubun/db';
|
|
2
|
+
import type { AnchorStore } from '@kumiai/rpc';
|
|
3
|
+
/**
|
|
4
|
+
* The `@kumiai/rpc` `AnchorStore` port for one group, backed by the device's
|
|
5
|
+
* single-slot anchor store. The port carries no group scope — the group a peer
|
|
6
|
+
* belongs to is fixed for its lifetime — so the adapter closes over `groupID`
|
|
7
|
+
* and threads it into every store call.
|
|
8
|
+
*
|
|
9
|
+
* `null` from `load` MEANS FIRST BOOT AND ONLY FIRST BOOT — the peer answers it by
|
|
10
|
+
* seeding the anchor from its live handle, and MLS ratchets forward so no earlier
|
|
11
|
+
* epoch's secret can be re-exported. A read error must therefore PROPAGATE, never
|
|
12
|
+
* be coerced to `null`: a `null` for a group that HAS an anchor silently derives
|
|
13
|
+
* different topic IDs and partitions this device with nothing reported.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createAnchorStore(stores: StoreProvider, groupID: string): AnchorStore;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { getP2PStore } from '@kubun/store-p2p';
|
|
2
|
+
/**
|
|
3
|
+
* The `@kumiai/rpc` `AnchorStore` port for one group, backed by the device's
|
|
4
|
+
* single-slot anchor store. The port carries no group scope — the group a peer
|
|
5
|
+
* belongs to is fixed for its lifetime — so the adapter closes over `groupID`
|
|
6
|
+
* and threads it into every store call.
|
|
7
|
+
*
|
|
8
|
+
* `null` from `load` MEANS FIRST BOOT AND ONLY FIRST BOOT — the peer answers it by
|
|
9
|
+
* seeding the anchor from its live handle, and MLS ratchets forward so no earlier
|
|
10
|
+
* epoch's secret can be re-exported. A read error must therefore PROPAGATE, never
|
|
11
|
+
* be coerced to `null`: a `null` for a group that HAS an anchor silently derives
|
|
12
|
+
* different topic IDs and partitions this device with nothing reported.
|
|
13
|
+
*/ export function createAnchorStore(stores, groupID) {
|
|
14
|
+
return {
|
|
15
|
+
async load () {
|
|
16
|
+
const store = await getP2PStore(stores);
|
|
17
|
+
const row = await store.getGroupAnchor(groupID);
|
|
18
|
+
if (row == null) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
// Copied into a fresh `Uint8Array` the port owns, so nothing downstream
|
|
22
|
+
// holds a view onto the adapter's decoded row.
|
|
23
|
+
return {
|
|
24
|
+
secret: new Uint8Array(row.secret),
|
|
25
|
+
epoch: row.epoch
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
async save (anchor) {
|
|
29
|
+
const store = await getP2PStore(stores);
|
|
30
|
+
await store.putGroupAnchor({
|
|
31
|
+
groupID,
|
|
32
|
+
secret: anchor.secret,
|
|
33
|
+
epoch: anchor.epoch
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { StoreProvider } from '@kubun/db';
|
|
2
|
+
import type { AppCursorStore } from '@kumiai/rpc';
|
|
3
|
+
/**
|
|
4
|
+
* The `@kumiai/rpc` `AppCursorStore` port for one group, backed by the device's
|
|
5
|
+
* per-topic cursor store. The port keys positions by topic ID alone — a topic
|
|
6
|
+
* ID is already globally unique — so the adapter closes over `groupID` purely
|
|
7
|
+
* to give each row an owner: a deleted group's cursors go with it, rather than
|
|
8
|
+
* staying behind as unreachable rows nothing accounts for.
|
|
9
|
+
*
|
|
10
|
+
* `null` here means this device has never processed a frame on the topic, and
|
|
11
|
+
* the drain answers it by reading from the hub's oldest retained frame. Unlike
|
|
12
|
+
* the anchor, a wrong `null` costs a re-read, not a partition.
|
|
13
|
+
*
|
|
14
|
+
* Positions are opaque log positions: stored and returned verbatim, never
|
|
15
|
+
* compared or parsed here.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createAppCursorStore(stores: StoreProvider, groupID: string): AppCursorStore;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { getP2PStore } from '@kubun/store-p2p';
|
|
2
|
+
/**
|
|
3
|
+
* The `@kumiai/rpc` `AppCursorStore` port for one group, backed by the device's
|
|
4
|
+
* per-topic cursor store. The port keys positions by topic ID alone — a topic
|
|
5
|
+
* ID is already globally unique — so the adapter closes over `groupID` purely
|
|
6
|
+
* to give each row an owner: a deleted group's cursors go with it, rather than
|
|
7
|
+
* staying behind as unreachable rows nothing accounts for.
|
|
8
|
+
*
|
|
9
|
+
* `null` here means this device has never processed a frame on the topic, and
|
|
10
|
+
* the drain answers it by reading from the hub's oldest retained frame. Unlike
|
|
11
|
+
* the anchor, a wrong `null` costs a re-read, not a partition.
|
|
12
|
+
*
|
|
13
|
+
* Positions are opaque log positions: stored and returned verbatim, never
|
|
14
|
+
* compared or parsed here.
|
|
15
|
+
*/ export function createAppCursorStore(stores, groupID) {
|
|
16
|
+
return {
|
|
17
|
+
async load (topicID) {
|
|
18
|
+
const store = await getP2PStore(stores);
|
|
19
|
+
const row = await store.getAppCursor({
|
|
20
|
+
groupID,
|
|
21
|
+
topicID
|
|
22
|
+
});
|
|
23
|
+
return row?.position ?? null;
|
|
24
|
+
},
|
|
25
|
+
async save (topicID, position) {
|
|
26
|
+
const store = await getP2PStore(stores);
|
|
27
|
+
await store.putAppCursor({
|
|
28
|
+
groupID,
|
|
29
|
+
topicID,
|
|
30
|
+
position
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|