@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,50 @@
|
|
|
1
|
+
import { type OwnIdentity } from '@kokuin/token';
|
|
2
|
+
import type { CatalogRecord } from '@kubun/protocol';
|
|
3
|
+
type SetPayload = {
|
|
4
|
+
op: 'set';
|
|
5
|
+
catalog: CatalogRecord;
|
|
6
|
+
};
|
|
7
|
+
type UpdatePayload = {
|
|
8
|
+
op: 'update';
|
|
9
|
+
catalogID: string;
|
|
10
|
+
update: {
|
|
11
|
+
name?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
filterCriteria?: CatalogRecord['filterCriteria'];
|
|
14
|
+
};
|
|
15
|
+
hlc: string;
|
|
16
|
+
};
|
|
17
|
+
type RemovePayload = {
|
|
18
|
+
op: 'remove';
|
|
19
|
+
catalogID: string;
|
|
20
|
+
hlc: string;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* The verified claim behind a catalog control broadcast. `issuer` is the
|
|
24
|
+
* authenticated author (the verified token `iss`). Every authoritative field
|
|
25
|
+
* comes from the signed payload, so a receiver never trusts the surrounding
|
|
26
|
+
* plaintext frame. Sender-bound: the receiver applies a set only when the
|
|
27
|
+
* catalog's `ownerDID` equals this issuer, and an update/remove only when the
|
|
28
|
+
* locally-stored catalog is owned by this issuer.
|
|
29
|
+
*/
|
|
30
|
+
export type VerifiedCatalog = {
|
|
31
|
+
issuer: string;
|
|
32
|
+
claim: SetPayload;
|
|
33
|
+
} | {
|
|
34
|
+
issuer: string;
|
|
35
|
+
claim: UpdatePayload;
|
|
36
|
+
} | {
|
|
37
|
+
issuer: string;
|
|
38
|
+
claim: RemovePayload;
|
|
39
|
+
};
|
|
40
|
+
export declare function signCatalogSet(identity: OwnIdentity, catalog: CatalogRecord): Promise<string>;
|
|
41
|
+
export declare function signCatalogUpdate(identity: OwnIdentity, claim: Omit<UpdatePayload, 'op'>): Promise<string>;
|
|
42
|
+
export declare function signCatalogRemove(identity: OwnIdentity, claim: Omit<RemovePayload, 'op'>): Promise<string>;
|
|
43
|
+
/**
|
|
44
|
+
* Verify a signed catalog token and extract its claim. Returns `null` (never
|
|
45
|
+
* throws) when the token is unparseable, unsigned (`alg: 'none'`), or
|
|
46
|
+
* structurally malformed — an attacker cannot forge an `iss` this way. The
|
|
47
|
+
* issuer is the normalized verified `iss`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function verifyCatalog(token: string): Promise<VerifiedCatalog | null>;
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { isVerifiedToken, normalizeDID, stringifyToken, verifyToken } from '@kokuin/token';
|
|
2
|
+
export async function signCatalogSet(identity, catalog) {
|
|
3
|
+
const signed = await identity.signToken({
|
|
4
|
+
op: 'set',
|
|
5
|
+
catalog
|
|
6
|
+
}, {
|
|
7
|
+
embedLongForm: true
|
|
8
|
+
});
|
|
9
|
+
return stringifyToken(signed);
|
|
10
|
+
}
|
|
11
|
+
export async function signCatalogUpdate(identity, claim) {
|
|
12
|
+
const signed = await identity.signToken({
|
|
13
|
+
op: 'update',
|
|
14
|
+
...claim
|
|
15
|
+
}, {
|
|
16
|
+
embedLongForm: true
|
|
17
|
+
});
|
|
18
|
+
return stringifyToken(signed);
|
|
19
|
+
}
|
|
20
|
+
export async function signCatalogRemove(identity, claim) {
|
|
21
|
+
const signed = await identity.signToken({
|
|
22
|
+
op: 'remove',
|
|
23
|
+
...claim
|
|
24
|
+
}, {
|
|
25
|
+
embedLongForm: true
|
|
26
|
+
});
|
|
27
|
+
return stringifyToken(signed);
|
|
28
|
+
}
|
|
29
|
+
function isCatalogRecord(value) {
|
|
30
|
+
if (value == null || typeof value !== 'object') {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const record = value;
|
|
34
|
+
return typeof record.id === 'string' && typeof record.ownerDID === 'string' && typeof record.name === 'string' && typeof record.hlc === 'string';
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Verify a signed catalog token and extract its claim. Returns `null` (never
|
|
38
|
+
* throws) when the token is unparseable, unsigned (`alg: 'none'`), or
|
|
39
|
+
* structurally malformed — an attacker cannot forge an `iss` this way. The
|
|
40
|
+
* issuer is the normalized verified `iss`.
|
|
41
|
+
*/ export async function verifyCatalog(token) {
|
|
42
|
+
let verified;
|
|
43
|
+
try {
|
|
44
|
+
verified = await verifyToken(token);
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (!isVerifiedToken(verified)) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const { iss, op } = verified.payload;
|
|
52
|
+
const issuer = normalizeDID(iss);
|
|
53
|
+
if (op === 'set') {
|
|
54
|
+
const { catalog } = verified.payload;
|
|
55
|
+
if (!isCatalogRecord(catalog)) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
issuer,
|
|
60
|
+
claim: {
|
|
61
|
+
op: 'set',
|
|
62
|
+
catalog
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (op === 'update') {
|
|
67
|
+
const { catalogID, update, hlc } = verified.payload;
|
|
68
|
+
if (typeof catalogID !== 'string' || typeof hlc !== 'string' || update == null) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
issuer,
|
|
73
|
+
claim: {
|
|
74
|
+
op: 'update',
|
|
75
|
+
catalogID,
|
|
76
|
+
update: update,
|
|
77
|
+
hlc
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (op === 'remove') {
|
|
82
|
+
const { catalogID, hlc } = verified.payload;
|
|
83
|
+
if (typeof catalogID !== 'string' || typeof hlc !== 'string') {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
issuer,
|
|
88
|
+
claim: {
|
|
89
|
+
op: 'remove',
|
|
90
|
+
catalogID,
|
|
91
|
+
hlc
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
@@ -1,20 +1,9 @@
|
|
|
1
1
|
import type { Logger } from '@kubun/logger';
|
|
2
2
|
import type { P2PStoreAPI } from '@kubun/store-p2p';
|
|
3
|
-
import
|
|
4
|
-
import type
|
|
5
|
-
import { type
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Projection keys, one per entry type — the watermark slot a reproject /
|
|
9
|
-
* incremental advances. Mirrors `ADMIN_ROSTER_PROJECTION`: a single projection
|
|
10
|
-
* per ledger type, so the key is the type itself.
|
|
11
|
-
*/
|
|
12
|
-
export declare const CIRCLE_DEF_PROJECTION = "circle.def";
|
|
13
|
-
export declare const CIRCLE_MEMBER_PROJECTION = "circle.member";
|
|
14
|
-
export declare const GROUP_SETTINGS_PROJECTION = "group.settings";
|
|
15
|
-
export declare const loadVerifiedCircleDefEntries: (store: P2PStoreAPI, groupID: string, logger?: Logger) => Promise<FoldInput<CircleDefValue>[]>;
|
|
16
|
-
export declare const loadVerifiedCircleMemberEntries: (store: P2PStoreAPI, groupID: string, logger?: Logger) => Promise<FoldInput<CircleMemberValue>[]>;
|
|
17
|
-
export declare const loadVerifiedGroupSettingsEntries: (store: P2PStoreAPI, groupID: string, logger?: Logger) => Promise<FoldInput<GroupSettingsValue>[]>;
|
|
3
|
+
import type { GroupAnchor, LedgerLogEntry } from '@kumiai/mls';
|
|
4
|
+
import { type CircleDefValue, type CircleMemberValue, type GroupSettingsValue, type IsAdminAtPosition } from './circle-reducers.js';
|
|
5
|
+
import { type PositionedFoldInput } from './ledger.js';
|
|
6
|
+
export declare const selectCircleMemberEntries: (ledger: ReadonlyArray<LedgerLogEntry>) => PositionedFoldInput<CircleMemberValue>[];
|
|
18
7
|
/**
|
|
19
8
|
* One projected circle row as the fold decides it. `hlc` is the winning active
|
|
20
9
|
* entry's HLC (the row's `hlc` column); `removedAtHLC` is the winning tombstone
|
|
@@ -33,14 +22,14 @@ export type CircleDefProjectionRow = {
|
|
|
33
22
|
/**
|
|
34
23
|
* Fold the circle-definition ledger into a per-circle projection covering ALL
|
|
35
24
|
* subjects, active and removed — the active reducer drops removed circles, but a
|
|
36
|
-
* projection must keep a tombstone row for them. Replays in
|
|
37
|
-
* the same admin-authority gate as the reducer: an active entry sets the
|
|
25
|
+
* projection must keep a tombstone row for them. Replays in the ledger's own
|
|
26
|
+
* enactment order under the same admin-authority gate as the reducer: an active entry sets the
|
|
38
27
|
* definition and clears any tombstone (re-add), a tombstone marks the row
|
|
39
28
|
* removed while keeping its last active value. A circle whose only entry is a
|
|
40
29
|
* tombstone (no prior active value) produces no row, matching the LWW store's
|
|
41
30
|
* ignore-if-absent removal.
|
|
42
31
|
*/
|
|
43
|
-
export declare function projectCircleDefs(entries: Array<
|
|
32
|
+
export declare function projectCircleDefs(entries: Array<PositionedFoldInput<CircleDefValue>>, isAdmin: IsAdminAtPosition): Map<string, CircleDefProjectionRow>;
|
|
44
33
|
/** One projected circle-member row, keyed by the composite ledger subject. */
|
|
45
34
|
export type CircleMemberProjectionRow = {
|
|
46
35
|
circleID: string;
|
|
@@ -54,22 +43,31 @@ export type CircleMemberProjectionRow = {
|
|
|
54
43
|
* subjects, active and removed. Same structure as {@link projectCircleDefs}: an
|
|
55
44
|
* active entry sets the role and clears any tombstone, a tombstone marks the row
|
|
56
45
|
* removed while keeping its last active role. A malformed subject is skipped, as
|
|
57
|
-
* is a tombstone with no prior active value.
|
|
46
|
+
* is a tombstone with no prior active value. Gates each entry on the same
|
|
47
|
+
* admin-authority check as the reducer's fold — an entry from a non-admin issuer
|
|
48
|
+
* never projects.
|
|
58
49
|
*/
|
|
59
|
-
export declare function projectCircleMembers(entries: Array<
|
|
50
|
+
export declare function projectCircleMembers(entries: Array<PositionedFoldInput<CircleMemberValue>>, isAdmin: IsAdminAtPosition): Map<string, CircleMemberProjectionRow>;
|
|
60
51
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
52
|
+
* Rebuild a circle/settings projection from the whole ledger.
|
|
53
|
+
*
|
|
54
|
+
* The only projection path there is: an entry is judged against the ledger prefix
|
|
55
|
+
* through its own slot, so folding the whole log is the same answer as folding it
|
|
56
|
+
* incrementally, and there is no cached-projection-is-stale case to detect.
|
|
57
|
+
* Authority is the admin fold (`adminAuthorityFromEntries`), so every projection
|
|
58
|
+
* honors one gate. Each `reconcile*` write is unconditional, so the row converges
|
|
59
|
+
* to exactly the fold's decision regardless of prior contents.
|
|
68
60
|
*/
|
|
69
61
|
export type ReprojectCircleParams = {
|
|
70
62
|
store: P2PStoreAPI;
|
|
71
63
|
groupID: string;
|
|
72
64
|
anchor: GroupAnchor;
|
|
65
|
+
/**
|
|
66
|
+
* The group's control ledger, in the handle's enactment order. Passed in rather
|
|
67
|
+
* than read: these projections run where the handle is already open, and every
|
|
68
|
+
* entry they fold is one the handle verified on the way in.
|
|
69
|
+
*/
|
|
70
|
+
ledger: ReadonlyArray<LedgerLogEntry>;
|
|
73
71
|
logger?: Logger;
|
|
74
72
|
};
|
|
75
73
|
export declare function reprojectCircleDefs(params: ReprojectCircleParams): Promise<void>;
|
|
@@ -81,42 +79,12 @@ export declare function reprojectCircleMembers(params: ReprojectCircleParams): P
|
|
|
81
79
|
* unconditional `updateGroup`. A field no entry ever set stays at the group's
|
|
82
80
|
* original value (creation-time name/description).
|
|
83
81
|
*/
|
|
84
|
-
export declare function reprojectGroupSettings(params: ReprojectCircleParams): Promise<void>;
|
|
85
82
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* the projection itself — circle authority lives in the separate admin
|
|
92
|
-
* sub-ledger, so the admin entries are loaded here.
|
|
93
|
-
*
|
|
94
|
-
* The watermark is advanced by the caller (the ingest handler), not here —
|
|
95
|
-
* mirroring the admin roster's split of responsibility — so the projection
|
|
96
|
-
* mutation and the watermark advance stay in the handler's single transaction
|
|
97
|
-
* even when the entry is dropped (it is still durable and accounted for).
|
|
83
|
+
* Fold the group's settings ledger under the admin gate and return the merged
|
|
84
|
+
* per-field value, or `undefined` when no authorized settings entry has been
|
|
85
|
+
* folded. This is the same fold {@link reprojectGroupSettings} materializes,
|
|
86
|
+
* exposed so a producer can read the CURRENT folded designation before authoring
|
|
87
|
+
* — what a device projects and what it checks before writing cannot then drift.
|
|
98
88
|
*/
|
|
99
|
-
export
|
|
100
|
-
|
|
101
|
-
groupID: string;
|
|
102
|
-
anchor: GroupAnchor;
|
|
103
|
-
verified: VerifiedLedgerEntry<CircleDefValue>;
|
|
104
|
-
logger?: Logger;
|
|
105
|
-
};
|
|
106
|
-
export declare function applyCircleDefEntryIncremental(params: ApplyCircleDefEntryIncrementalParams): Promise<void>;
|
|
107
|
-
export type ApplyCircleMemberEntryIncrementalParams = {
|
|
108
|
-
store: P2PStoreAPI;
|
|
109
|
-
groupID: string;
|
|
110
|
-
anchor: GroupAnchor;
|
|
111
|
-
verified: VerifiedLedgerEntry<CircleMemberValue>;
|
|
112
|
-
logger?: Logger;
|
|
113
|
-
};
|
|
114
|
-
export declare function applyCircleMemberEntryIncremental(params: ApplyCircleMemberEntryIncrementalParams): Promise<void>;
|
|
115
|
-
export type ApplyGroupSettingsEntryIncrementalParams = {
|
|
116
|
-
store: P2PStoreAPI;
|
|
117
|
-
groupID: string;
|
|
118
|
-
anchor: GroupAnchor;
|
|
119
|
-
verified: VerifiedLedgerEntry<GroupSettingsValue>;
|
|
120
|
-
logger?: Logger;
|
|
121
|
-
};
|
|
122
|
-
export declare function applyGroupSettingsEntryIncremental(params: ApplyGroupSettingsEntryIncrementalParams): Promise<void>;
|
|
89
|
+
export declare function foldGroupSettings(ledger: ReadonlyArray<LedgerLogEntry>, groupID: string, anchor: GroupAnchor): GroupSettingsValue | undefined;
|
|
90
|
+
export declare function reprojectGroupSettings(params: ReprojectCircleParams): Promise<void>;
|
|
@@ -1,95 +1,63 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { adminAuthorityFromEntries, CIRCLE_DEF_ENTRY_TYPE, CIRCLE_MEMBER_ENTRY_TYPE, createGroupSettingsReducer, decodeCircleMemberSubject, GROUP_SETTINGS_ENTRY_TYPE } from './circle-reducers.js';
|
|
3
|
-
import {
|
|
4
|
-
import { foldLedger } from './ledger-fold.js';
|
|
5
|
-
/**
|
|
6
|
-
* Projection keys, one per entry type — the watermark slot a reproject /
|
|
7
|
-
* incremental advances. Mirrors `ADMIN_ROSTER_PROJECTION`: a single projection
|
|
8
|
-
* per ledger type, so the key is the type itself.
|
|
9
|
-
*/ export const CIRCLE_DEF_PROJECTION = CIRCLE_DEF_ENTRY_TYPE;
|
|
10
|
-
export const CIRCLE_MEMBER_PROJECTION = CIRCLE_MEMBER_ENTRY_TYPE;
|
|
11
|
-
export const GROUP_SETTINGS_PROJECTION = GROUP_SETTINGS_ENTRY_TYPE;
|
|
1
|
+
import { selectRoleEntries } from './admin-roster.js';
|
|
2
|
+
import { adminAuthorityFromEntries, authorizedEntries, CIRCLE_DEF_ENTRY_TYPE, CIRCLE_MEMBER_ENTRY_TYPE, createGroupSettingsReducer, decodeCircleMemberSubject, GROUP_SETTINGS_ENTRY_TYPE } from './circle-reducers.js';
|
|
3
|
+
import { entryOrd, foldLedgerEntries } from './ledger.js';
|
|
12
4
|
function isRemovedDef(value) {
|
|
13
5
|
return 'removed' in value && value.removed === true;
|
|
14
6
|
}
|
|
15
7
|
function isRemovedMember(value) {
|
|
16
8
|
return 'removed' in value && value.removed === true;
|
|
17
9
|
}
|
|
18
|
-
/** Total order over the ledger: HLC first, content-addressed id breaks ties. */ function byTotalOrder(a, b) {
|
|
19
|
-
if (a.verified.entry.hlc !== b.verified.entry.hlc) {
|
|
20
|
-
return a.verified.entry.hlc < b.verified.entry.hlc ? -1 : 1;
|
|
21
|
-
}
|
|
22
|
-
if (a.entryID === b.entryID) return 0;
|
|
23
|
-
return a.entryID < b.entryID ? -1 : 1;
|
|
24
|
-
}
|
|
25
|
-
function maxHLC(entries) {
|
|
26
|
-
let max = null;
|
|
27
|
-
for (const { verified } of entries){
|
|
28
|
-
if (max == null || verified.entry.hlc > max) max = verified.entry.hlc;
|
|
29
|
-
}
|
|
30
|
-
return max;
|
|
31
|
-
}
|
|
32
|
-
// --- Per-type verified-entry loaders ---
|
|
33
10
|
/**
|
|
34
|
-
* Build a
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (cached != null && cached.key === key) {
|
|
49
|
-
return cached.entries;
|
|
50
|
-
}
|
|
11
|
+
* Build a selector for one ledger type, carrying each entry's slot in the WHOLE
|
|
12
|
+
* ledger so its authority can be judged against the prefix that precedes it.
|
|
13
|
+
*
|
|
14
|
+
* Entries come from the handle's own log, and every door into it verifies —
|
|
15
|
+
* `GroupHandle.applyLedgerEntries`, `bootstrapLedger` and an accepted commit
|
|
16
|
+
* each re-verify the tokens they admit — so these are pure filters over an
|
|
17
|
+
* in-memory array: no re-verification, and nothing to memoize. They replaced
|
|
18
|
+
* loaders that re-verified every stored `signed_token` on each call, which was
|
|
19
|
+
* needed only because the SQL row they read could not vouch for itself.
|
|
20
|
+
*
|
|
21
|
+
* Selecting by `type` is what licenses the cast: the log is typed over an unknown
|
|
22
|
+
* claim value, and the type is the claim about what that value is.
|
|
23
|
+
*/ function createEntrySelector(entryType) {
|
|
24
|
+
return (ledger)=>{
|
|
51
25
|
const entries = [];
|
|
52
|
-
for (const
|
|
53
|
-
|
|
54
|
-
if (verified == null) {
|
|
55
|
-
logger?.warn('circle projection: dropped stored entry whose token no longer verifies', {
|
|
56
|
-
groupID,
|
|
57
|
-
entryType,
|
|
58
|
-
entryID: row.entry_id
|
|
59
|
-
});
|
|
26
|
+
for (const [position, held] of ledger.entries()){
|
|
27
|
+
if (held.verified.entry.type !== entryType) {
|
|
60
28
|
continue;
|
|
61
29
|
}
|
|
62
30
|
entries.push({
|
|
63
|
-
verified,
|
|
64
|
-
entryID:
|
|
31
|
+
verified: held.verified,
|
|
32
|
+
entryID: held.entryID,
|
|
33
|
+
position
|
|
65
34
|
});
|
|
66
35
|
}
|
|
67
|
-
cache.set(groupID, {
|
|
68
|
-
key,
|
|
69
|
-
entries
|
|
70
|
-
});
|
|
71
36
|
return entries;
|
|
72
37
|
};
|
|
73
38
|
}
|
|
74
|
-
|
|
75
|
-
export const
|
|
76
|
-
|
|
39
|
+
const selectCircleDefEntries = createEntrySelector(CIRCLE_DEF_ENTRY_TYPE);
|
|
40
|
+
export const selectCircleMemberEntries = createEntrySelector(CIRCLE_MEMBER_ENTRY_TYPE);
|
|
41
|
+
const selectGroupSettingsEntries = createEntrySelector(GROUP_SETTINGS_ENTRY_TYPE);
|
|
77
42
|
/**
|
|
78
43
|
* Fold the circle-definition ledger into a per-circle projection covering ALL
|
|
79
44
|
* subjects, active and removed — the active reducer drops removed circles, but a
|
|
80
|
-
* projection must keep a tombstone row for them. Replays in
|
|
81
|
-
* the same admin-authority gate as the reducer: an active entry sets the
|
|
45
|
+
* projection must keep a tombstone row for them. Replays in the ledger's own
|
|
46
|
+
* enactment order under the same admin-authority gate as the reducer: an active entry sets the
|
|
82
47
|
* definition and clears any tombstone (re-add), a tombstone marks the row
|
|
83
48
|
* removed while keeping its last active value. A circle whose only entry is a
|
|
84
49
|
* tombstone (no prior active value) produces no row, matching the LWW store's
|
|
85
50
|
* ignore-if-absent removal.
|
|
86
|
-
*/ export function projectCircleDefs(entries,
|
|
51
|
+
*/ export function projectCircleDefs(entries, isAdmin) {
|
|
87
52
|
const rows = new Map();
|
|
88
|
-
for (const { verified } of
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
53
|
+
for (const { verified } of authorizedEntries(entries, isAdmin)){
|
|
54
|
+
const { subject, value } = verified.entry;
|
|
55
|
+
const hlc = entryOrd(verified.entry);
|
|
56
|
+
// Kubun stamps `ord` on every circle entry it signs; one without is foreign
|
|
57
|
+
// or malformed. Skipping is fail-closed — projecting it would have to invent
|
|
58
|
+
// an HLC, and on a tombstone a `null` `removed_at_hlc` reads as ACTIVE and
|
|
59
|
+
// silently revives the row.
|
|
60
|
+
if (hlc == null) continue;
|
|
93
61
|
if (isRemovedDef(value)) {
|
|
94
62
|
const existing = rows.get(subject);
|
|
95
63
|
if (existing == null) continue;
|
|
@@ -114,14 +82,17 @@ export const loadVerifiedGroupSettingsEntries = createVerifiedEntryLoader(GROUP_
|
|
|
114
82
|
* subjects, active and removed. Same structure as {@link projectCircleDefs}: an
|
|
115
83
|
* active entry sets the role and clears any tombstone, a tombstone marks the row
|
|
116
84
|
* removed while keeping its last active role. A malformed subject is skipped, as
|
|
117
|
-
* is a tombstone with no prior active value.
|
|
118
|
-
|
|
85
|
+
* is a tombstone with no prior active value. Gates each entry on the same
|
|
86
|
+
* admin-authority check as the reducer's fold — an entry from a non-admin issuer
|
|
87
|
+
* never projects.
|
|
88
|
+
*/ export function projectCircleMembers(entries, isAdmin) {
|
|
119
89
|
const rows = new Map();
|
|
120
|
-
for (const { verified } of
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
90
|
+
for (const { verified } of authorizedEntries(entries, isAdmin)){
|
|
91
|
+
const { subject, value } = verified.entry;
|
|
92
|
+
const hlc = entryOrd(verified.entry);
|
|
93
|
+
// See {@link projectCircleDefs}: no `ord` means foreign or malformed, and a
|
|
94
|
+
// null tombstone HLC would read as active.
|
|
95
|
+
if (hlc == null) continue;
|
|
125
96
|
if (isRemovedMember(value)) {
|
|
126
97
|
const existing = rows.get(subject);
|
|
127
98
|
if (existing == null) continue;
|
|
@@ -143,16 +114,25 @@ export const loadVerifiedGroupSettingsEntries = createVerifiedEntryLoader(GROUP_
|
|
|
143
114
|
}
|
|
144
115
|
return rows;
|
|
145
116
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
117
|
+
function adminAuthorityFor(params) {
|
|
118
|
+
return adminAuthorityFromEntries(selectRoleEntries(params.ledger), params.anchor, params.groupID);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The group's CURRENT open-circle set, folded from the settings ledger under the
|
|
122
|
+
* supplied admin authority. `open` is a derived cache of this set on each circle
|
|
123
|
+
* row, so both the settings→circles and circles←settings reprojection paths
|
|
124
|
+
* derive it from the one authoritative fold and converge regardless of which
|
|
125
|
+
* entry (the `circle.def` or the `group.settings` opening it) arrives first.
|
|
126
|
+
*/ function foldOpenCircleIDs(ledger, groupID, anchor, isAdmin) {
|
|
127
|
+
const entries = selectGroupSettingsEntries(ledger);
|
|
128
|
+
const { settings } = foldLedgerEntries(authorizedEntries(entries, isAdmin), anchor, createGroupSettingsReducer());
|
|
129
|
+
return new Set(settings.get(groupID)?.openCircleIDs ?? []);
|
|
149
130
|
}
|
|
150
131
|
export async function reprojectCircleDefs(params) {
|
|
151
|
-
const { store, groupID } = params;
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
]);
|
|
132
|
+
const { store, groupID, anchor, ledger } = params;
|
|
133
|
+
const isAdmin = adminAuthorityFor(params);
|
|
134
|
+
const entries = selectCircleDefEntries(ledger);
|
|
135
|
+
const openIDs = foldOpenCircleIDs(ledger, groupID, anchor, isAdmin);
|
|
156
136
|
const rows = projectCircleDefs(entries, isAdmin);
|
|
157
137
|
for (const [circleID, row] of rows){
|
|
158
138
|
await store.reconcileCircleProjection({
|
|
@@ -162,20 +142,15 @@ export async function reprojectCircleDefs(params) {
|
|
|
162
142
|
description: row.description,
|
|
163
143
|
catalogIDs: row.catalogIDs,
|
|
164
144
|
hlc: row.hlc,
|
|
165
|
-
removedAtHLC: row.removedAtHLC
|
|
145
|
+
removedAtHLC: row.removedAtHLC,
|
|
146
|
+
open: openIDs.has(circleID)
|
|
166
147
|
});
|
|
167
148
|
}
|
|
168
|
-
const max = maxHLC(entries);
|
|
169
|
-
if (max != null) {
|
|
170
|
-
await store.setProjectionWatermark(groupID, CIRCLE_DEF_PROJECTION, max);
|
|
171
|
-
}
|
|
172
149
|
}
|
|
173
150
|
export async function reprojectCircleMembers(params) {
|
|
174
|
-
const { store,
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
loadVerifiedCircleMemberEntries(store, groupID, params.logger)
|
|
178
|
-
]);
|
|
151
|
+
const { store, ledger } = params;
|
|
152
|
+
const isAdmin = adminAuthorityFor(params);
|
|
153
|
+
const entries = selectCircleMemberEntries(ledger);
|
|
179
154
|
const rows = projectCircleMembers(entries, isAdmin);
|
|
180
155
|
for (const row of rows.values()){
|
|
181
156
|
await store.reconcileCircleMemberProjection({
|
|
@@ -186,10 +161,6 @@ export async function reprojectCircleMembers(params) {
|
|
|
186
161
|
removedAtHLC: row.removedAtHLC
|
|
187
162
|
});
|
|
188
163
|
}
|
|
189
|
-
const max = maxHLC(entries);
|
|
190
|
-
if (max != null) {
|
|
191
|
-
await store.setProjectionWatermark(groupID, CIRCLE_MEMBER_PROJECTION, max);
|
|
192
|
-
}
|
|
193
164
|
}
|
|
194
165
|
/**
|
|
195
166
|
* Reproject group settings onto the group row. Settings are PARTIAL per-field
|
|
@@ -197,92 +168,35 @@ export async function reprojectCircleMembers(params) {
|
|
|
197
168
|
* fold it under the admin gate, then write the merged fields via the
|
|
198
169
|
* unconditional `updateGroup`. A field no entry ever set stays at the group's
|
|
199
170
|
* original value (creation-time name/description).
|
|
200
|
-
*/
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
const max = maxHLC(entries);
|
|
212
|
-
if (max != null) {
|
|
213
|
-
await store.setProjectionWatermark(groupID, GROUP_SETTINGS_PROJECTION, max);
|
|
214
|
-
}
|
|
171
|
+
*/ /**
|
|
172
|
+
* Fold the group's settings ledger under the admin gate and return the merged
|
|
173
|
+
* per-field value, or `undefined` when no authorized settings entry has been
|
|
174
|
+
* folded. This is the same fold {@link reprojectGroupSettings} materializes,
|
|
175
|
+
* exposed so a producer can read the CURRENT folded designation before authoring
|
|
176
|
+
* — what a device projects and what it checks before writing cannot then drift.
|
|
177
|
+
*/ export function foldGroupSettings(ledger, groupID, anchor) {
|
|
178
|
+
const isAdmin = adminAuthorityFromEntries(selectRoleEntries(ledger), anchor, groupID);
|
|
179
|
+
const { settings } = foldLedgerEntries(authorizedEntries(selectGroupSettingsEntries(ledger), isAdmin), anchor, createGroupSettingsReducer());
|
|
180
|
+
return settings.get(groupID);
|
|
215
181
|
}
|
|
216
|
-
export async function
|
|
217
|
-
const { store, groupID, anchor,
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (!isAdmin(verified.issuer, hlc)) {
|
|
222
|
-
logger?.warn('circle projection: dropped incremental circle.def from a non-admin issuer', {
|
|
223
|
-
groupID,
|
|
224
|
-
issuer: verified.issuer
|
|
225
|
-
});
|
|
226
|
-
} else if (isRemovedDef(value)) {
|
|
227
|
-
// Tombstone an existing active row, leaving its name/description intact —
|
|
228
|
-
// the LWW removal write, correct for an in-order entry on the active row.
|
|
229
|
-
await store.markCircleRemoved(subject, hlc);
|
|
230
|
-
} else {
|
|
231
|
-
await store.reconcileCircleProjection({
|
|
232
|
-
id: subject,
|
|
233
|
-
groupID,
|
|
234
|
-
name: value.name,
|
|
235
|
-
description: value.description,
|
|
236
|
-
catalogIDs: value.catalogIDs,
|
|
237
|
-
hlc,
|
|
238
|
-
removedAtHLC: null
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
export async function applyCircleMemberEntryIncremental(params) {
|
|
243
|
-
const { store, groupID, anchor, verified, logger } = params;
|
|
244
|
-
const adminEntries = await loadVerifiedAdminEntries(store, groupID, logger);
|
|
245
|
-
const isAdmin = adminAuthorityFromEntries(adminEntries, anchor);
|
|
246
|
-
const { subject, value, hlc } = verified.entry;
|
|
247
|
-
if (!isAdmin(verified.issuer, hlc)) {
|
|
248
|
-
logger?.warn('circle projection: dropped incremental circle.member from a non-admin issuer', {
|
|
249
|
-
groupID,
|
|
250
|
-
issuer: verified.issuer
|
|
251
|
-
});
|
|
252
|
-
} else if (isRemovedMember(value)) {
|
|
253
|
-
const decoded = decodeCircleMemberSubject(subject);
|
|
254
|
-
if (decoded != null) {
|
|
255
|
-
await store.removeCircleMember(decoded.circleID, decoded.memberDID, hlc);
|
|
256
|
-
}
|
|
257
|
-
} else {
|
|
258
|
-
const decoded = decodeCircleMemberSubject(subject);
|
|
259
|
-
if (decoded != null) {
|
|
260
|
-
await store.reconcileCircleMemberProjection({
|
|
261
|
-
circleID: decoded.circleID,
|
|
262
|
-
memberDID: decoded.memberDID,
|
|
263
|
-
role: value.role,
|
|
264
|
-
hlc,
|
|
265
|
-
removedAtHLC: null
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
export async function applyGroupSettingsEntryIncremental(params) {
|
|
271
|
-
const { store, groupID, anchor, verified, logger } = params;
|
|
272
|
-
const adminEntries = await loadVerifiedAdminEntries(store, groupID, logger);
|
|
273
|
-
const isAdmin = adminAuthorityFromEntries(adminEntries, anchor);
|
|
274
|
-
const { value, hlc } = verified.entry;
|
|
275
|
-
if (!isAdmin(verified.issuer, hlc)) {
|
|
276
|
-
logger?.warn('circle projection: dropped incremental group.settings from a non-admin issuer', {
|
|
277
|
-
groupID,
|
|
278
|
-
issuer: verified.issuer
|
|
279
|
-
});
|
|
280
|
-
} else {
|
|
281
|
-
// Per-field merge onto the group row: `updateGroup` sets only the fields the
|
|
282
|
-
// entry carries (an explicit `''` clears; an omitted field is left alone).
|
|
182
|
+
export async function reprojectGroupSettings(params) {
|
|
183
|
+
const { store, groupID, anchor, ledger } = params;
|
|
184
|
+
const merged = foldGroupSettings(ledger, groupID, anchor);
|
|
185
|
+
if (merged != null) {
|
|
186
|
+
// name/description live on the group row directly.
|
|
283
187
|
await store.updateGroup(groupID, {
|
|
284
|
-
name:
|
|
285
|
-
description:
|
|
188
|
+
name: merged.name,
|
|
189
|
+
description: merged.description
|
|
286
190
|
});
|
|
287
191
|
}
|
|
192
|
+
// `openCircleIDs` is the authoritative open set; project it onto each circle's
|
|
193
|
+
// `open` flag (settings → circles). Runs unconditionally so a fold with no open
|
|
194
|
+
// circles closes every circle in the group.
|
|
195
|
+
await store.reconcileCircleOpenFlags(groupID, merged?.openCircleIDs ?? []);
|
|
196
|
+
// `commitHub` is the group's consensus commit-hub designation; materialize it
|
|
197
|
+
// onto the local `is_commit_hub` flag so the fold, not each device's first
|
|
198
|
+
// hub-bind, decides which bound hub the group commits through. A fold carrying
|
|
199
|
+
// no designation is a no-op here, leaving the local auto-designated flag in
|
|
200
|
+
// place until an admin authors one.
|
|
201
|
+
await store.reconcileCommitHub(groupID, merged?.commitHub);
|
|
288
202
|
}
|