@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,23 @@
|
|
|
1
|
+
import type { OwnIdentity } from '@kokuin/token';
|
|
2
|
+
import type { KubunDB } from '@kubun/db';
|
|
3
|
+
import type { EngineEventBus, EngineEvents } from '@kubun/engine';
|
|
4
|
+
import type { Logger } from '@kubun/logger';
|
|
5
|
+
import type { GroupBroadcastMessage } from '../groups/broadcast.js';
|
|
6
|
+
export type AccessDefaultSenderParams = {
|
|
7
|
+
db: KubunDB;
|
|
8
|
+
eventBus: EngineEventBus<EngineEvents>;
|
|
9
|
+
identity: OwnIdentity;
|
|
10
|
+
/** Fire-and-forget hub broadcast trigger, one call per target group. */
|
|
11
|
+
scheduleBroadcast: (groupID: string, message: GroupBroadcastMessage) => void | Promise<void>;
|
|
12
|
+
logger?: Logger;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Subscribe to the engine's access-default events and replicate each rule to
|
|
16
|
+
* every MLS group the local peer belongs to. A model default is owner-global,
|
|
17
|
+
* not group-scoped: any co-member evaluating the owner's documents needs the
|
|
18
|
+
* rule, and the receiver's sender-bound apply keeps a forged owner out. The
|
|
19
|
+
* fan-out is a single tiny frame per group, so no scope resolution is needed.
|
|
20
|
+
*
|
|
21
|
+
* Returns a combined unsubscribe function for both listeners.
|
|
22
|
+
*/
|
|
23
|
+
export declare function wireAccessDefaultSender(params: AccessDefaultSenderParams): () => void;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { getP2PStore } from '@kubun/store-p2p';
|
|
2
|
+
import { signAccessDefaultRemove, signAccessDefaultSet } from '../groups/access-default-token.js';
|
|
3
|
+
/**
|
|
4
|
+
* Subscribe to the engine's access-default events and replicate each rule to
|
|
5
|
+
* every MLS group the local peer belongs to. A model default is owner-global,
|
|
6
|
+
* not group-scoped: any co-member evaluating the owner's documents needs the
|
|
7
|
+
* rule, and the receiver's sender-bound apply keeps a forged owner out. The
|
|
8
|
+
* fan-out is a single tiny frame per group, so no scope resolution is needed.
|
|
9
|
+
*
|
|
10
|
+
* Returns a combined unsubscribe function for both listeners.
|
|
11
|
+
*/ export function wireAccessDefaultSender(params) {
|
|
12
|
+
const broadcastToOwnGroups = async (message)=>{
|
|
13
|
+
const p2pStore = await getP2PStore(params.db);
|
|
14
|
+
const groups = await p2pStore.getGroupsForMember(params.identity.id);
|
|
15
|
+
// Distinct groups over distinct hub lanes with nothing shared between them,
|
|
16
|
+
// so the fan-out runs concurrently: in sequence one slow lane delays every
|
|
17
|
+
// group behind it.
|
|
18
|
+
await Promise.all(groups.map((group)=>params.scheduleBroadcast(group.id, message)));
|
|
19
|
+
};
|
|
20
|
+
const unsubscribeSet = params.eventBus.on('engine:access-default:set', async (event)=>{
|
|
21
|
+
try {
|
|
22
|
+
const auth = await signAccessDefaultSet(params.identity, {
|
|
23
|
+
modelID: event.modelID,
|
|
24
|
+
permissionType: event.permissionType,
|
|
25
|
+
rule: {
|
|
26
|
+
level: event.accessLevel,
|
|
27
|
+
allowedDIDs: event.allowedDIDs,
|
|
28
|
+
allowedCircles: event.allowedCircles,
|
|
29
|
+
allowedGroups: event.allowedGroups
|
|
30
|
+
},
|
|
31
|
+
hlc: event.hlc
|
|
32
|
+
});
|
|
33
|
+
await broadcastToOwnGroups({
|
|
34
|
+
type: 'access-default:set',
|
|
35
|
+
modelID: event.modelID,
|
|
36
|
+
permissionType: event.permissionType,
|
|
37
|
+
rule: {
|
|
38
|
+
level: event.accessLevel,
|
|
39
|
+
allowedDIDs: event.allowedDIDs,
|
|
40
|
+
allowedCircles: event.allowedCircles,
|
|
41
|
+
allowedGroups: event.allowedGroups
|
|
42
|
+
},
|
|
43
|
+
ownerDID: event.ownerDID,
|
|
44
|
+
hlc: event.hlc,
|
|
45
|
+
auth
|
|
46
|
+
});
|
|
47
|
+
} catch (error) {
|
|
48
|
+
params.logger?.warn('access-default-sender set handler failed', {
|
|
49
|
+
modelID: event.modelID,
|
|
50
|
+
error: String(error)
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
const unsubscribeRemove = params.eventBus.on('engine:access-default:removed', async (event)=>{
|
|
55
|
+
try {
|
|
56
|
+
const auth = await signAccessDefaultRemove(params.identity, {
|
|
57
|
+
modelID: event.modelID,
|
|
58
|
+
permissionTypes: event.permissionTypes,
|
|
59
|
+
hlc: event.hlc
|
|
60
|
+
});
|
|
61
|
+
await broadcastToOwnGroups({
|
|
62
|
+
type: 'access-default:remove',
|
|
63
|
+
modelID: event.modelID,
|
|
64
|
+
permissionTypes: event.permissionTypes,
|
|
65
|
+
ownerDID: event.ownerDID,
|
|
66
|
+
hlc: event.hlc,
|
|
67
|
+
auth
|
|
68
|
+
});
|
|
69
|
+
} catch (error) {
|
|
70
|
+
params.logger?.warn('access-default-sender remove handler failed', {
|
|
71
|
+
modelID: event.modelID,
|
|
72
|
+
error: String(error)
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
return ()=>{
|
|
77
|
+
unsubscribeSet();
|
|
78
|
+
unsubscribeRemove();
|
|
79
|
+
};
|
|
80
|
+
}
|
package/lib/sync/authorize.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type VerifyTokenHook } from '@kokuin/capability';
|
|
2
|
+
import type { StoredAccessRule } from '@kubun/store-graph';
|
|
2
3
|
/**
|
|
3
4
|
* Check if delegation tokens grant the viewer read access to a user's documents.
|
|
4
5
|
*
|
|
@@ -11,15 +12,68 @@ import { type VerifyTokenHook } from '@kokuin/capability';
|
|
|
11
12
|
* @returns true if any token grants read access
|
|
12
13
|
*/
|
|
13
14
|
export declare function checkSyncDelegation(viewerDID: string, ownerDID: string, delegationTokens: Array<string>, revocationChecker?: VerifyTokenHook): Promise<boolean>;
|
|
15
|
+
/**
|
|
16
|
+
* Resolver for circle-granted sync scopes. All reads run against the SERVING
|
|
17
|
+
* device's own state: the owner's tier-2 read default, the requester's circle
|
|
18
|
+
* membership, and the requester's group membership. The methods are dumb store
|
|
19
|
+
* reads — the authorization rule combining them lives in {@link authorizeScope}
|
|
20
|
+
* so implementations cannot drift on the security-critical conjunction.
|
|
21
|
+
*/
|
|
22
|
+
export type CircleReadGrantResolver = {
|
|
23
|
+
/** The owner's tier-2 read default for the model, or `null` if none exists. */
|
|
24
|
+
getUserModelReadDefault(ownerDID: string, modelID: string): Promise<StoredAccessRule | null>;
|
|
25
|
+
/**
|
|
26
|
+
* Which of `circleIDs` the viewer reaches, each with the group it belongs to.
|
|
27
|
+
* An unknown or tombstoned circle is absent, so the gate fails closed on it.
|
|
28
|
+
*
|
|
29
|
+
* Batched rather than per-circle so the rule below costs a fixed number of
|
|
30
|
+
* reads instead of one per named circle — and the group arrives with the id,
|
|
31
|
+
* which keeps the tombstone filter that decides "unknown circle" in one place
|
|
32
|
+
* instead of two reads that could disagree.
|
|
33
|
+
*/
|
|
34
|
+
listAccessibleCircles(viewerDID: string, circleIDs: Array<string>): Promise<Array<{
|
|
35
|
+
id: string;
|
|
36
|
+
groupID: string;
|
|
37
|
+
}>>;
|
|
38
|
+
/**
|
|
39
|
+
* Which of `groupIDs` the viewer is a member of, per the serving device's own
|
|
40
|
+
* membership view (rows grown only from local ops and verified MLS commits).
|
|
41
|
+
*/
|
|
42
|
+
listMemberGroupIDs(viewerDID: string, groupIDs: Array<string>): Promise<Array<string>>;
|
|
43
|
+
};
|
|
14
44
|
export type AuthorizeScopeParams = {
|
|
15
45
|
viewerDID: string;
|
|
16
46
|
ownerDID: string;
|
|
47
|
+
modelID: string;
|
|
17
48
|
delegationTokens: Array<string>;
|
|
18
49
|
revocationChecker?: VerifyTokenHook;
|
|
50
|
+
/**
|
|
51
|
+
* Optional resolver enabling owner-granted read. When set, a scope the owner
|
|
52
|
+
* and delegation arms reject is still accepted if the owner's tier-2 read
|
|
53
|
+
* default for the model is `anyone`, or is `restricted` and names a circle the
|
|
54
|
+
* viewer belongs to. Only an explicit owner grant widens: the server default is
|
|
55
|
+
* never consulted, so a model with no tier-2 read default stays owner/
|
|
56
|
+
* delegation only. Omitted — owner + delegation only, unchanged.
|
|
57
|
+
*/
|
|
58
|
+
circleReadGrant?: CircleReadGrantResolver;
|
|
19
59
|
};
|
|
20
60
|
/**
|
|
21
|
-
* Authorize a single sync scope: the viewer must be the owner,
|
|
22
|
-
*
|
|
23
|
-
*
|
|
61
|
+
* Authorize a single sync scope: the viewer must be the owner, hold a delegation
|
|
62
|
+
* token granting read access to the owner's documents, or — when a
|
|
63
|
+
* {@link CircleReadGrantResolver} is supplied — be reached by the owner's tier-2
|
|
64
|
+
* read default for the model, either because that default is `anyone` or because
|
|
65
|
+
* the viewer is a member of a circle it names AND a member of that circle's
|
|
66
|
+
* group per the serving device's own view. Shared by negotiate and merkle-sync
|
|
67
|
+
* so the two access gates cannot drift apart. Fail-closed: any arm that is not
|
|
68
|
+
* satisfiable denies.
|
|
69
|
+
*
|
|
70
|
+
* The group-membership conjunct is that half of the open-access definition
|
|
71
|
+
* (`explicitMember OR (open AND groupMember)`): for an open circle
|
|
72
|
+
* `isMemberOfAnyCircle` already implies it, so the outer check is
|
|
73
|
+
* redundant-but-consistent; for the explicit arm it is a data-plane backstop.
|
|
74
|
+
* The circle roster is a projection of admin-signed ledger entries, while the
|
|
75
|
+
* serving device's group-membership rows grow only from its own ops and verified
|
|
76
|
+
* MLS commits — so even a polluted circle projection cannot open the data plane
|
|
77
|
+
* without a matching local row. Unknown circle or unresolvable group → deny.
|
|
24
78
|
*/
|
|
25
79
|
export declare function authorizeScope(params: AuthorizeScopeParams): Promise<boolean>;
|
package/lib/sync/authorize.js
CHANGED
|
@@ -62,12 +62,53 @@ import { checkCapability } from '@kokuin/capability';
|
|
|
62
62
|
return false;
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
|
-
* Authorize a single sync scope: the viewer must be the owner,
|
|
66
|
-
*
|
|
67
|
-
*
|
|
65
|
+
* Authorize a single sync scope: the viewer must be the owner, hold a delegation
|
|
66
|
+
* token granting read access to the owner's documents, or — when a
|
|
67
|
+
* {@link CircleReadGrantResolver} is supplied — be reached by the owner's tier-2
|
|
68
|
+
* read default for the model, either because that default is `anyone` or because
|
|
69
|
+
* the viewer is a member of a circle it names AND a member of that circle's
|
|
70
|
+
* group per the serving device's own view. Shared by negotiate and merkle-sync
|
|
71
|
+
* so the two access gates cannot drift apart. Fail-closed: any arm that is not
|
|
72
|
+
* satisfiable denies.
|
|
73
|
+
*
|
|
74
|
+
* The group-membership conjunct is that half of the open-access definition
|
|
75
|
+
* (`explicitMember OR (open AND groupMember)`): for an open circle
|
|
76
|
+
* `isMemberOfAnyCircle` already implies it, so the outer check is
|
|
77
|
+
* redundant-but-consistent; for the explicit arm it is a data-plane backstop.
|
|
78
|
+
* The circle roster is a projection of admin-signed ledger entries, while the
|
|
79
|
+
* serving device's group-membership rows grow only from its own ops and verified
|
|
80
|
+
* MLS commits — so even a polluted circle projection cannot open the data plane
|
|
81
|
+
* without a matching local row. Unknown circle or unresolvable group → deny.
|
|
68
82
|
*/ export async function authorizeScope(params) {
|
|
69
83
|
if (params.viewerDID === params.ownerDID) {
|
|
70
84
|
return true;
|
|
71
85
|
}
|
|
72
|
-
|
|
86
|
+
if (await checkSyncDelegation(params.viewerDID, params.ownerDID, params.delegationTokens, params.revocationChecker)) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
if (params.circleReadGrant != null) {
|
|
90
|
+
const grant = params.circleReadGrant;
|
|
91
|
+
const rule = await grant.getUserModelReadDefault(params.ownerDID, params.modelID);
|
|
92
|
+
// Reached only through a rule the owner actually stored: a `null` rule means
|
|
93
|
+
// the owner declared nothing, and the server default must never stand in for
|
|
94
|
+
// a declaration — a device whose default flipped would otherwise open every
|
|
95
|
+
// model on it.
|
|
96
|
+
if (rule != null && rule.level === 'anyone') {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
if (rule != null && rule.level === 'restricted' && rule.allowedCircles != null && rule.allowedCircles.length > 0) {
|
|
100
|
+
// Per-circle conjunction: both checks must hold for the SAME circle, so a
|
|
101
|
+
// membership in one granted circle can never combine with membership in an
|
|
102
|
+
// unrelated circle's group. The reads are batched but the conjunction is
|
|
103
|
+
// evaluated here, per circle, so no store implementation decides it.
|
|
104
|
+
const accessible = await grant.listAccessibleCircles(params.viewerDID, rule.allowedCircles);
|
|
105
|
+
if (accessible.length > 0) {
|
|
106
|
+
const memberGroupIDs = new Set(await grant.listMemberGroupIDs(params.viewerDID, Array.from(new Set(accessible.map((circle)=>circle.groupID)))));
|
|
107
|
+
if (accessible.some((circle)=>memberGroupIDs.has(circle.groupID))) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
73
114
|
}
|
|
@@ -30,7 +30,7 @@ export type BroadcastQueue = {
|
|
|
30
30
|
* Enqueue a single mutation entry for the given target group, attributed to
|
|
31
31
|
* `senderPeerDID`. May trigger an immediate flush if a threshold is reached.
|
|
32
32
|
*/
|
|
33
|
-
enqueue(targetGroupID: string, entry: MutationApplyEntry, senderPeerDID: string): void;
|
|
33
|
+
enqueue(targetGroupID: string, entry: MutationApplyEntry, senderPeerDID: string | undefined): void;
|
|
34
34
|
/**
|
|
35
35
|
* Cancel all pending timers and synchronously flush remaining entries
|
|
36
36
|
* via `scheduleBroadcast`. Safe to call multiple times.
|
|
@@ -52,8 +52,7 @@ export type BroadcastQueue = {
|
|
|
52
52
|
* Sender-DID consistency: the queue tracks the `senderPeerDID` of the first
|
|
53
53
|
* entry in each batch. If a later enqueue arrives with a different sender
|
|
54
54
|
* (defensive — in practice senderPeerDID is constant per peer), the prior
|
|
55
|
-
* batch is flushed and a fresh batch starts
|
|
56
|
-
*
|
|
57
|
-
* `senderPeerDID`.
|
|
55
|
+
* batch is flushed and a fresh batch starts, so every flushed message carries
|
|
56
|
+
* a single coherent `senderPeerDID`.
|
|
58
57
|
*/
|
|
59
58
|
export declare function createBroadcastQueue(params: BroadcastQueueParams): BroadcastQueue;
|
|
@@ -19,9 +19,8 @@ export const DEFAULT_BROADCAST_BATCH_CONFIG = {
|
|
|
19
19
|
* Sender-DID consistency: the queue tracks the `senderPeerDID` of the first
|
|
20
20
|
* entry in each batch. If a later enqueue arrives with a different sender
|
|
21
21
|
* (defensive — in practice senderPeerDID is constant per peer), the prior
|
|
22
|
-
* batch is flushed and a fresh batch starts
|
|
23
|
-
*
|
|
24
|
-
* `senderPeerDID`.
|
|
22
|
+
* batch is flushed and a fresh batch starts, so every flushed message carries
|
|
23
|
+
* a single coherent `senderPeerDID`.
|
|
25
24
|
*/ export function createBroadcastQueue(params) {
|
|
26
25
|
const { config, scheduleBroadcast, logger } = params;
|
|
27
26
|
const queues = new Map();
|
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { StoreProvider } from '@kubun/db';
|
|
2
2
|
export type CatalogSyncScope = {
|
|
3
3
|
modelIDs: Array<string>;
|
|
4
4
|
owners: Array<string> | undefined;
|
|
5
|
-
requiredClusterIDs: Array<string>;
|
|
6
|
-
missingClusterIDs: Array<string>;
|
|
7
5
|
};
|
|
8
6
|
/**
|
|
9
7
|
* Resolve catalog criteria to sync-compatible scopes.
|
|
10
8
|
*
|
|
11
9
|
* Takes catalog IDs, resolves their criteria, and returns:
|
|
12
|
-
* - modelIDs: union of all model filters from all catalogs
|
|
10
|
+
* - modelIDs: union of all model filters from all catalogs, with an omitted
|
|
11
|
+
* filter expanded to every model this device has deployed
|
|
13
12
|
* - owners: union of all owner filters (explicit + circle members)
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
*
|
|
14
|
+
* Which of those models the requester lacks is NOT decided here: the negotiate
|
|
15
|
+
* handler answers that over the scopes it accepted, which is the only set that
|
|
16
|
+
* production actually sends.
|
|
16
17
|
*/
|
|
17
|
-
export declare function resolveCatalogSyncScopes(db:
|
|
18
|
+
export declare function resolveCatalogSyncScopes(db: StoreProvider, catalogIDs: Array<string>): Promise<CatalogSyncScope>;
|
|
@@ -4,11 +4,14 @@ import { getP2PStore } from '@kubun/store-p2p';
|
|
|
4
4
|
* Resolve catalog criteria to sync-compatible scopes.
|
|
5
5
|
*
|
|
6
6
|
* Takes catalog IDs, resolves their criteria, and returns:
|
|
7
|
-
* - modelIDs: union of all model filters from all catalogs
|
|
7
|
+
* - modelIDs: union of all model filters from all catalogs, with an omitted
|
|
8
|
+
* filter expanded to every model this device has deployed
|
|
8
9
|
* - owners: union of all owner filters (explicit + circle members)
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
|
|
10
|
+
*
|
|
11
|
+
* Which of those models the requester lacks is NOT decided here: the negotiate
|
|
12
|
+
* handler answers that over the scopes it accepted, which is the only set that
|
|
13
|
+
* production actually sends.
|
|
14
|
+
*/ export async function resolveCatalogSyncScopes(db, catalogIDs) {
|
|
12
15
|
const [graphStore, p2pStore] = await Promise.all([
|
|
13
16
|
getGraphStore(db),
|
|
14
17
|
getP2PStore(db)
|
|
@@ -16,53 +19,66 @@ import { getP2PStore } from '@kubun/store-p2p';
|
|
|
16
19
|
const allModelIDs = new Set();
|
|
17
20
|
const allOwners = new Set();
|
|
18
21
|
let hasOwnerFilter = false;
|
|
22
|
+
// Catalogs are advisory: only a locally-activated catalog scopes sync. A known
|
|
23
|
+
// (not-activated) or unknown catalog contributes nothing. Read as one batch —
|
|
24
|
+
// the loop below needs every record anyway, and `resolveCatalogScope` reads the
|
|
25
|
+
// row again for itself, so a per-id read here would be the second of three.
|
|
26
|
+
const catalogs = await graphStore.getCatalogs(catalogIDs);
|
|
27
|
+
const circleIDs = new Set();
|
|
28
|
+
// Read once, and only if some catalog omits its model filter.
|
|
29
|
+
let deployedModelIDs;
|
|
30
|
+
const getDeployedModelIDs = async ()=>{
|
|
31
|
+
deployedModelIDs ??= await graphStore.listDocumentModelIDs();
|
|
32
|
+
return deployedModelIDs;
|
|
33
|
+
};
|
|
19
34
|
for (const catalogID of catalogIDs){
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
allModelIDs.add(m);
|
|
24
|
-
}
|
|
35
|
+
const catalog = catalogs.get(catalogID);
|
|
36
|
+
if (catalog == null || catalog.active !== 1) {
|
|
37
|
+
continue;
|
|
25
38
|
}
|
|
39
|
+
const scope = await graphStore.resolveCatalogScope(catalogID);
|
|
40
|
+
const criteria = catalog.filter_criteria;
|
|
41
|
+
const catalogCircles = criteria.circles != null && criteria.circles.length > 0 ? criteria.circles : undefined;
|
|
26
42
|
if (scope.owners != null) {
|
|
27
43
|
hasOwnerFilter = true;
|
|
28
44
|
for (const o of scope.owners){
|
|
29
45
|
allOwners.add(o);
|
|
30
46
|
}
|
|
31
47
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
if (catalogCircles != null) {
|
|
49
|
+
hasOwnerFilter = true;
|
|
50
|
+
for (const circleID of catalogCircles){
|
|
51
|
+
circleIDs.add(circleID);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (scope.models != null) {
|
|
55
|
+
for (const m of scope.models){
|
|
56
|
+
allModelIDs.add(m);
|
|
57
|
+
}
|
|
58
|
+
} else if (scope.owners != null || catalogCircles != null) {
|
|
59
|
+
// "Omitted means no constraint" is only true where a dimension can be
|
|
60
|
+
// enumerated: expanding models at resolution time also covers a model
|
|
61
|
+
// deployed after the catalog was written. Owners stay anchorless on
|
|
62
|
+
// purpose — "everyone" is not a set — so the expansion is gated on the
|
|
63
|
+
// catalog naming owners or circles. Without that gate a criteria-less
|
|
64
|
+
// catalog would widen the SERVING side, where an absent owner filter
|
|
65
|
+
// means every distinct owner of every model.
|
|
66
|
+
for (const m of (await getDeployedModelIDs())){
|
|
67
|
+
allModelIDs.add(m);
|
|
44
68
|
}
|
|
45
69
|
}
|
|
46
70
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const clusterID = await graphStore.getClusterForModel(modelID);
|
|
55
|
-
if (clusterID != null) {
|
|
56
|
-
requiredClusterIDs.add(clusterID);
|
|
57
|
-
if (!knownSet.has(modelID)) {
|
|
58
|
-
missingClusterIDs.add(clusterID);
|
|
59
|
-
}
|
|
71
|
+
// Resolve circle-based owners via the p2p store, every catalog's circles at
|
|
72
|
+
// once. `listOwnersForCircles` unions `listCircleOwners` over the set: explicit
|
|
73
|
+
// members plus, for an open circle, the whole group's members — the pull-owner
|
|
74
|
+
// set mirrors the open-access predicate.
|
|
75
|
+
if (circleIDs.size > 0) {
|
|
76
|
+
for (const owner of (await p2pStore.listOwnersForCircles(Array.from(circleIDs)))){
|
|
77
|
+
allOwners.add(owner);
|
|
60
78
|
}
|
|
61
|
-
}
|
|
79
|
+
}
|
|
62
80
|
return {
|
|
63
|
-
modelIDs,
|
|
64
|
-
owners
|
|
65
|
-
requiredClusterIDs: Array.from(requiredClusterIDs),
|
|
66
|
-
missingClusterIDs: Array.from(missingClusterIDs)
|
|
81
|
+
modelIDs: Array.from(allModelIDs),
|
|
82
|
+
owners: hasOwnerFilter ? Array.from(allOwners) : undefined
|
|
67
83
|
};
|
|
68
84
|
}
|
package/lib/sync/errors.d.ts
CHANGED
|
@@ -9,4 +9,13 @@ export declare class SyncAccessDeniedError extends Error {
|
|
|
9
9
|
constructor(reason: string, message?: string);
|
|
10
10
|
get code(): 'KB08';
|
|
11
11
|
get reason(): string;
|
|
12
|
+
/**
|
|
13
|
+
* Carried alongside the code when the handler boundary translates this into a
|
|
14
|
+
* wire error, so a caller can tell the denials apart without parsing the
|
|
15
|
+
* message. The reasons are a fixed categorical set and disclose nothing the
|
|
16
|
+
* caller did not supply.
|
|
17
|
+
*/
|
|
18
|
+
get data(): {
|
|
19
|
+
reason: string;
|
|
20
|
+
};
|
|
12
21
|
}
|
package/lib/sync/errors.js
CHANGED
|
@@ -16,4 +16,14 @@
|
|
|
16
16
|
get reason() {
|
|
17
17
|
return this.#reason;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Carried alongside the code when the handler boundary translates this into a
|
|
21
|
+
* wire error, so a caller can tell the denials apart without parsing the
|
|
22
|
+
* message. The reasons are a fixed categorical set and disclose nothing the
|
|
23
|
+
* caller did not supply.
|
|
24
|
+
*/ get data() {
|
|
25
|
+
return {
|
|
26
|
+
reason: this.#reason
|
|
27
|
+
};
|
|
28
|
+
}
|
|
19
29
|
}
|
package/lib/sync/forwarder.d.ts
CHANGED
|
@@ -71,27 +71,21 @@ export type EvaluateAndForwardResult = {
|
|
|
71
71
|
skipped: number;
|
|
72
72
|
};
|
|
73
73
|
/**
|
|
74
|
-
* Evaluate forwarding candidates for
|
|
75
|
-
*
|
|
76
|
-
* `scheduleBroadcast`.
|
|
74
|
+
* Evaluate forwarding candidates for one just-applied `mutation:apply` entry and
|
|
75
|
+
* forward to each survivor via `scheduleBroadcast`.
|
|
77
76
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* document.
|
|
84
|
-
* 4. Filter (when `forwarding` is a function) — narrows further.
|
|
85
|
-
* 5. Dedup — `recordBroadcast` BEFORE schedule. False return → skip silently.
|
|
86
|
-
* 6. Schedule — single-entry `mutation:apply` broadcast with
|
|
87
|
-
* `senderPeerDID = selfDID`.
|
|
77
|
+
* Gates per candidate group, in order: the source group is excluded first;
|
|
78
|
+
* `computeBroadcastScopes` against the post-apply rule; a circle the local peer
|
|
79
|
+
* belongs to in that group must hold a catalog whose `filter_criteria` matches
|
|
80
|
+
* the document; then the caller's `forwarding` filter when it is a function.
|
|
81
|
+
* `recordBroadcast` runs BEFORE the schedule and a false return skips silently.
|
|
88
82
|
*
|
|
89
|
-
* Per-candidate errors are isolated
|
|
90
|
-
*
|
|
83
|
+
* Per-candidate errors are isolated — one bad candidate logs and the rest are
|
|
84
|
+
* still evaluated — and the function never throws.
|
|
91
85
|
*
|
|
92
|
-
*
|
|
93
|
-
* batch queue
|
|
94
|
-
* with mixing locally-authored and forwarded entries in one
|
|
95
|
-
*
|
|
86
|
+
* Each candidate gets its OWN single-entry broadcast, `senderPeerDID = selfDID`,
|
|
87
|
+
* rather than going through the batch queue, whose `senderPeerDID`-mismatch
|
|
88
|
+
* flush conflicts with mixing locally-authored and forwarded entries in one
|
|
89
|
+
* batch.
|
|
96
90
|
*/
|
|
97
91
|
export declare function evaluateAndForward(params: EvaluateAndForwardParams): Promise<EvaluateAndForwardResult>;
|
package/lib/sync/forwarder.js
CHANGED
|
@@ -10,28 +10,22 @@ export function rethrowIfProgrammerError(error) {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
|
-
* Evaluate forwarding candidates for
|
|
14
|
-
*
|
|
15
|
-
* `scheduleBroadcast`.
|
|
13
|
+
* Evaluate forwarding candidates for one just-applied `mutation:apply` entry and
|
|
14
|
+
* forward to each survivor via `scheduleBroadcast`.
|
|
16
15
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* document.
|
|
23
|
-
* 4. Filter (when `forwarding` is a function) — narrows further.
|
|
24
|
-
* 5. Dedup — `recordBroadcast` BEFORE schedule. False return → skip silently.
|
|
25
|
-
* 6. Schedule — single-entry `mutation:apply` broadcast with
|
|
26
|
-
* `senderPeerDID = selfDID`.
|
|
16
|
+
* Gates per candidate group, in order: the source group is excluded first;
|
|
17
|
+
* `computeBroadcastScopes` against the post-apply rule; a circle the local peer
|
|
18
|
+
* belongs to in that group must hold a catalog whose `filter_criteria` matches
|
|
19
|
+
* the document; then the caller's `forwarding` filter when it is a function.
|
|
20
|
+
* `recordBroadcast` runs BEFORE the schedule and a false return skips silently.
|
|
27
21
|
*
|
|
28
|
-
* Per-candidate errors are isolated
|
|
29
|
-
*
|
|
22
|
+
* Per-candidate errors are isolated — one bad candidate logs and the rest are
|
|
23
|
+
* still evaluated — and the function never throws.
|
|
30
24
|
*
|
|
31
|
-
*
|
|
32
|
-
* batch queue
|
|
33
|
-
* with mixing locally-authored and forwarded entries in one
|
|
34
|
-
*
|
|
25
|
+
* Each candidate gets its OWN single-entry broadcast, `senderPeerDID = selfDID`,
|
|
26
|
+
* rather than going through the batch queue, whose `senderPeerDID`-mismatch
|
|
27
|
+
* flush conflicts with mixing locally-authored and forwarded entries in one
|
|
28
|
+
* batch.
|
|
35
29
|
*/ export async function evaluateAndForward(params) {
|
|
36
30
|
// 1. Zero-overhead short-circuit when forwarding is disabled.
|
|
37
31
|
if (params.forwarding === false) {
|