@kubun/plugin-p2p 0.10.1 → 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/delegation.js +130 -1
- package/lib/context/group.js +1646 -1
- package/lib/context/hub.js +151 -1
- package/lib/context/join.js +115 -1
- 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 +252 -1
- package/lib/context/types.d.ts +58 -22
- package/lib/context/types.js +48 -1
- 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 +55 -0
- package/lib/groups/admin-roster.js +69 -0
- 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-codec.js +6 -1
- package/lib/groups/broadcast.d.ts +238 -106
- package/lib/groups/broadcast.js +702 -1
- package/lib/groups/catalog-token.d.ts +50 -0
- package/lib/groups/catalog-token.js +96 -0
- package/lib/groups/circle-projection.d.ts +90 -0
- package/lib/groups/circle-projection.js +202 -0
- package/lib/groups/circle-reducers.d.ts +139 -0
- package/lib/groups/circle-reducers.js +128 -0
- 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 +19 -2
- package/lib/groups/events.js +10 -1
- package/lib/groups/group-anchor.d.ts +24 -0
- package/lib/groups/group-anchor.js +22 -0
- package/lib/groups/group-crypto.d.ts +42 -0
- package/lib/groups/group-crypto.js +159 -0
- package/lib/groups/group-handle-registry.d.ts +138 -9
- package/lib/groups/group-handle-registry.js +533 -1
- package/lib/groups/group-handlers.d.ts +30 -0
- package/lib/groups/group-handlers.js +218 -0
- package/lib/groups/group-health-monitor.d.ts +11 -0
- package/lib/groups/group-health-monitor.js +139 -1
- package/lib/groups/group-mls.d.ts +66 -0
- package/lib/groups/group-mls.js +696 -0
- package/lib/groups/group-peer-manager.d.ts +301 -0
- package/lib/groups/group-peer-manager.js +1336 -0
- package/lib/groups/group-protocols.d.ts +754 -0
- package/lib/groups/group-protocols.js +448 -0
- package/lib/groups/invite-payload.d.ts +49 -1
- package/lib/groups/invite-payload.js +26 -1
- 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 +61 -0
- package/lib/groups/join-utils.js +366 -1
- package/lib/groups/ledger-affected-events.d.ts +20 -0
- package/lib/groups/ledger-affected-events.js +85 -0
- 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 +217 -85
- package/lib/groups/manager.js +798 -1
- package/lib/groups/mls-codec.d.ts +28 -7
- package/lib/groups/mls-codec.js +33 -1
- package/lib/groups/mls-encryptor.d.ts +2 -2
- package/lib/groups/mls-encryptor.js +33 -1
- package/lib/groups/mls-group-handle.d.ts +37 -2
- package/lib/groups/mls-group-handle.js +18 -1
- package/lib/groups/mls-json.js +17 -1
- package/lib/groups/mls-receive-errors.d.ts +27 -0
- package/lib/groups/mls-receive-errors.js +39 -0
- package/lib/groups/mls-state.d.ts +6 -4
- package/lib/groups/mls-state.js +79 -1
- 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 +7 -3
- package/lib/groups/store-received-grant.js +161 -1
- package/lib/groups/store-received-revocation.d.ts +19 -16
- package/lib/groups/store-received-revocation.js +83 -1
- package/lib/hub/ack-backoff.d.ts +22 -0
- package/lib/hub/ack-backoff.js +88 -0
- package/lib/hub/http-client.d.ts +18 -10
- package/lib/hub/http-client.js +15 -1
- package/lib/hub/hub-like.d.ts +75 -0
- package/lib/hub/hub-like.js +1022 -0
- 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 +155 -1
- 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 +77 -51
- package/lib/hub/wiring.js +189 -1
- package/lib/index.d.ts +58 -22
- package/lib/index.js +563 -1
- 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 +214 -1
- package/lib/protocol.js +519 -1
- package/lib/schema.d.ts +3 -1
- package/lib/schema.js +1150 -39
- 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 +58 -4
- package/lib/sync/authorize.js +114 -1
- package/lib/sync/broadcast-queue.d.ts +3 -4
- package/lib/sync/broadcast-queue.js +182 -1
- package/lib/sync/broadcast-sender.js +179 -1
- package/lib/sync/catalog-scope.d.ts +8 -7
- package/lib/sync/catalog-scope.js +84 -1
- package/lib/sync/errors.d.ts +12 -2
- package/lib/sync/errors.js +29 -1
- package/lib/sync/forwarder.d.ts +13 -19
- package/lib/sync/forwarder.js +271 -1
- package/lib/sync/handlers.js +200 -1
- package/lib/sync/hub-tunnel-sync-listener.d.ts +19 -5
- package/lib/sync/hub-tunnel-sync-listener.js +261 -1
- package/lib/sync/hub-tunnel-sync-provider.d.ts +34 -14
- package/lib/sync/hub-tunnel-sync-provider.js +113 -1
- package/lib/sync/merkle-apply.d.ts +5 -0
- package/lib/sync/merkle-apply.js +194 -1
- package/lib/sync/merkle-channel.d.ts +11 -3
- package/lib/sync/merkle-channel.js +0 -0
- package/lib/sync/merkle-tree.js +120 -1
- package/lib/sync/peer-registry.d.ts +15 -11
- package/lib/sync/peer-registry.js +82 -1
- package/lib/sync/receive-access-gate.js +24 -1
- package/lib/sync/scope-resolver.js +41 -1
- package/lib/sync/sync-client.d.ts +14 -4
- package/lib/sync/sync-client.js +254 -1
- package/lib/sync/sync-manager.d.ts +24 -6
- package/lib/sync/sync-manager.js +301 -1
- 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 +704 -22
- package/lib/types.js +1 -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 +44 -1
- package/lib/utils.js +21 -1
- package/package.json +58 -47
- package/lib/groups/broadcast-service.d.ts +0 -263
- package/lib/groups/broadcast-service.js +0 -1
- package/lib/groups/rejoin-codec.d.ts +0 -14
- package/lib/groups/rejoin-codec.js +0 -1
- package/lib/groups/wire-frame.d.ts +0 -34
- package/lib/groups/wire-frame.js +0 -1
- package/lib/hub/circle-catchup-requester.d.ts +0 -58
- package/lib/hub/circle-catchup-requester.js +0 -1
- package/lib/hub/circle-catchup-responder.d.ts +0 -50
- package/lib/hub/circle-catchup-responder.js +0 -1
- package/lib/hub/connection-pool.d.ts +0 -43
- package/lib/hub/connection-pool.js +0 -1
- package/lib/hub/did-observing-transport.d.ts +0 -64
- package/lib/hub/did-observing-transport.js +0 -1
- package/lib/hub/epoch-stale-detector.d.ts +0 -18
- package/lib/hub/epoch-stale-detector.js +0 -1
- package/lib/hub/errors.d.ts +0 -30
- package/lib/hub/errors.js +0 -1
- package/lib/hub/forward-remote-broadcast.d.ts +0 -15
- package/lib/hub/forward-remote-broadcast.js +0 -1
- package/lib/hub/group-channel.d.ts +0 -89
- package/lib/hub/group-channel.js +0 -1
- package/lib/hub/hub-connection.d.ts +0 -96
- package/lib/hub/hub-connection.js +0 -1
- package/lib/hub/receive-handler.d.ts +0 -51
- package/lib/hub/receive-handler.js +0 -1
- package/lib/hub/rejoin-manager.d.ts +0 -78
- package/lib/hub/rejoin-manager.js +0 -1
- package/lib/hub/rejoin-responder.d.ts +0 -32
- package/lib/hub/rejoin-responder.js +0 -1
- package/lib/hub/relay-manager.d.ts +0 -142
- package/lib/hub/relay-manager.js +0 -1
- package/lib/hub/send-handler.d.ts +0 -40
- package/lib/hub/send-handler.js +0 -1
- package/lib/hub/tunnel-inbox.d.ts +0 -20
- package/lib/hub/tunnel-inbox.js +0 -1
- package/lib/hub/wait-for-gate.d.ts +0 -14
- package/lib/hub/wait-for-gate.js +0 -1
|
@@ -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
|
-
import { type VerifyTokenHook } from '@
|
|
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 '@enkaku/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
|
@@ -1 +1,114 @@
|
|
|
1
|
-
import{checkCapability
|
|
1
|
+
import { checkCapability } from '@kokuin/capability';
|
|
2
|
+
/**
|
|
3
|
+
* Check if delegation tokens grant the viewer read access to a user's documents.
|
|
4
|
+
*
|
|
5
|
+
* @param viewerDID - The DID of the requesting viewer
|
|
6
|
+
* @param ownerDID - The DID of the document owner
|
|
7
|
+
* @param delegationTokens - Array of delegation JWT tokens
|
|
8
|
+
* @param revocationChecker - Optional per-leaf-capability verification hook.
|
|
9
|
+
* When set, capabilities whose `jti` has been revoked cause the check to
|
|
10
|
+
* fail closed.
|
|
11
|
+
* @returns true if any token grants read access
|
|
12
|
+
*/ export async function checkSyncDelegation(viewerDID, ownerDID, delegationTokens, revocationChecker) {
|
|
13
|
+
if (!delegationTokens || delegationTokens.length === 0) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
// Resource patterns to check (from most general to specific)
|
|
17
|
+
// We need at least read access to the user's documents
|
|
18
|
+
const resources = [
|
|
19
|
+
'*',
|
|
20
|
+
'urn:kubun:user:*',
|
|
21
|
+
`urn:kubun:user:${ownerDID}`
|
|
22
|
+
];
|
|
23
|
+
const action = 'document/read';
|
|
24
|
+
// Try delegation chain first (tokens are a chain: A->B->C)
|
|
25
|
+
for (const res of resources){
|
|
26
|
+
try {
|
|
27
|
+
await checkCapability({
|
|
28
|
+
act: action,
|
|
29
|
+
res
|
|
30
|
+
}, {
|
|
31
|
+
iss: viewerDID,
|
|
32
|
+
sub: ownerDID,
|
|
33
|
+
cap: delegationTokens
|
|
34
|
+
}, {
|
|
35
|
+
verifyToken: revocationChecker
|
|
36
|
+
});
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
// Continue checking other resources
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Try each token independently (multiple independent grants)
|
|
43
|
+
for (const token of delegationTokens){
|
|
44
|
+
for (const res of resources){
|
|
45
|
+
try {
|
|
46
|
+
await checkCapability({
|
|
47
|
+
act: action,
|
|
48
|
+
res
|
|
49
|
+
}, {
|
|
50
|
+
iss: viewerDID,
|
|
51
|
+
sub: ownerDID,
|
|
52
|
+
cap: token
|
|
53
|
+
}, {
|
|
54
|
+
verifyToken: revocationChecker
|
|
55
|
+
});
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
// Continue checking
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
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.
|
|
82
|
+
*/ export async function authorizeScope(params) {
|
|
83
|
+
if (params.viewerDID === params.ownerDID) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
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;
|
|
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;
|
|
@@ -1 +1,182 @@
|
|
|
1
|
-
export const DEFAULT_BROADCAST_BATCH_CONFIG
|
|
1
|
+
export const DEFAULT_BROADCAST_BATCH_CONFIG = {
|
|
2
|
+
enabled: true,
|
|
3
|
+
windowMs: 500,
|
|
4
|
+
maxCount: 20,
|
|
5
|
+
maxBytes: 64 * 1024
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Build a per-target-group batch queue around a `scheduleBroadcast` hook.
|
|
9
|
+
*
|
|
10
|
+
* The `mutation:apply` broadcast variant carries `entries: Array<...>` —
|
|
11
|
+
* a queue flush emits ONE broadcast with the accumulated entries, not one
|
|
12
|
+
* broadcast per entry.
|
|
13
|
+
*
|
|
14
|
+
* Each enqueued entry's wire size is approximated via `JSON.stringify(entry).length`.
|
|
15
|
+
* The accumulated byte budget tracks per-batch payload size only — message
|
|
16
|
+
* envelope (`type`, `senderPeerDID`) overhead is intentionally ignored to keep
|
|
17
|
+
* accounting cheap and predictable.
|
|
18
|
+
*
|
|
19
|
+
* Sender-DID consistency: the queue tracks the `senderPeerDID` of the first
|
|
20
|
+
* entry in each batch. If a later enqueue arrives with a different sender
|
|
21
|
+
* (defensive — in practice senderPeerDID is constant per peer), the prior
|
|
22
|
+
* batch is flushed and a fresh batch starts, so every flushed message carries
|
|
23
|
+
* a single coherent `senderPeerDID`.
|
|
24
|
+
*/ export function createBroadcastQueue(params) {
|
|
25
|
+
const { config, scheduleBroadcast, logger } = params;
|
|
26
|
+
const queues = new Map();
|
|
27
|
+
let disposed = false;
|
|
28
|
+
function clearTimer(state) {
|
|
29
|
+
if (state.timer != null) {
|
|
30
|
+
clearTimeout(state.timer);
|
|
31
|
+
state.timer = null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function flush(targetGroupID) {
|
|
35
|
+
const state = queues.get(targetGroupID);
|
|
36
|
+
if (state == null || state.entries.length === 0) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
clearTimer(state);
|
|
40
|
+
const entries = state.entries;
|
|
41
|
+
const senderPeerDID = state.senderPeerDID;
|
|
42
|
+
queues.delete(targetGroupID);
|
|
43
|
+
try {
|
|
44
|
+
scheduleBroadcast(targetGroupID, {
|
|
45
|
+
type: 'mutation:apply',
|
|
46
|
+
entries,
|
|
47
|
+
senderPeerDID
|
|
48
|
+
});
|
|
49
|
+
} catch (error) {
|
|
50
|
+
logger?.warn('broadcast-queue flush failed', {
|
|
51
|
+
targetGroupID,
|
|
52
|
+
entryCount: entries.length,
|
|
53
|
+
error: String(error)
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function startTimer(targetGroupID) {
|
|
58
|
+
const state = queues.get(targetGroupID);
|
|
59
|
+
if (state == null || state.timer != null) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const timer = setTimeout(()=>{
|
|
63
|
+
flush(targetGroupID);
|
|
64
|
+
}, config.windowMs);
|
|
65
|
+
// Don't hold the process open just for a pending broadcast batch.
|
|
66
|
+
// Node returns a Timeout object with `unref()`; browsers return a number.
|
|
67
|
+
const maybeUnref = timer.unref;
|
|
68
|
+
if (typeof maybeUnref === 'function') {
|
|
69
|
+
maybeUnref.call(timer);
|
|
70
|
+
}
|
|
71
|
+
state.timer = timer;
|
|
72
|
+
}
|
|
73
|
+
function appendToFreshBatch(targetGroupID, entry, entryBytes, senderPeerDID) {
|
|
74
|
+
const state = {
|
|
75
|
+
entries: [
|
|
76
|
+
entry
|
|
77
|
+
],
|
|
78
|
+
bytes: entryBytes,
|
|
79
|
+
senderPeerDID,
|
|
80
|
+
timer: null
|
|
81
|
+
};
|
|
82
|
+
queues.set(targetGroupID, state);
|
|
83
|
+
}
|
|
84
|
+
function enqueue(targetGroupID, entry, senderPeerDID) {
|
|
85
|
+
if (disposed) {
|
|
86
|
+
logger?.warn('broadcast-queue enqueue after dispose, dropping entry', {
|
|
87
|
+
targetGroupID,
|
|
88
|
+
docID: entry.docID
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// Pass-through mode: emit each entry as its own single-entry broadcast.
|
|
93
|
+
if (!config.enabled) {
|
|
94
|
+
try {
|
|
95
|
+
scheduleBroadcast(targetGroupID, {
|
|
96
|
+
type: 'mutation:apply',
|
|
97
|
+
entries: [
|
|
98
|
+
entry
|
|
99
|
+
],
|
|
100
|
+
senderPeerDID
|
|
101
|
+
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
logger?.warn('broadcast-queue pass-through scheduleBroadcast failed', {
|
|
104
|
+
targetGroupID,
|
|
105
|
+
docID: entry.docID,
|
|
106
|
+
error: String(error)
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const entryBytes = JSON.stringify(entry).length;
|
|
112
|
+
// Oversize lone entry: skip the queue and ship as its own batch immediately.
|
|
113
|
+
// If a batch is already queued for this group, flush it first to preserve
|
|
114
|
+
// ordering (older entries ship before this lone giant).
|
|
115
|
+
if (entryBytes >= config.maxBytes) {
|
|
116
|
+
const existing = queues.get(targetGroupID);
|
|
117
|
+
if (existing != null && existing.entries.length > 0) {
|
|
118
|
+
flush(targetGroupID);
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
scheduleBroadcast(targetGroupID, {
|
|
122
|
+
type: 'mutation:apply',
|
|
123
|
+
entries: [
|
|
124
|
+
entry
|
|
125
|
+
],
|
|
126
|
+
senderPeerDID
|
|
127
|
+
});
|
|
128
|
+
} catch (error) {
|
|
129
|
+
logger?.warn('broadcast-queue oversize-entry scheduleBroadcast failed', {
|
|
130
|
+
targetGroupID,
|
|
131
|
+
docID: entry.docID,
|
|
132
|
+
error: String(error)
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
let state = queues.get(targetGroupID);
|
|
138
|
+
// Sender mismatch: flush prior batch, start fresh.
|
|
139
|
+
if (state != null && state.senderPeerDID !== senderPeerDID) {
|
|
140
|
+
flush(targetGroupID);
|
|
141
|
+
state = undefined;
|
|
142
|
+
}
|
|
143
|
+
// Byte threshold: would adding this entry overflow? Flush first, start new batch.
|
|
144
|
+
if (state != null && state.bytes + entryBytes > config.maxBytes) {
|
|
145
|
+
flush(targetGroupID);
|
|
146
|
+
state = undefined;
|
|
147
|
+
}
|
|
148
|
+
if (state == null) {
|
|
149
|
+
appendToFreshBatch(targetGroupID, entry, entryBytes, senderPeerDID);
|
|
150
|
+
// Start the window timer for this new batch — only if we won't flush
|
|
151
|
+
// synchronously below for count reasons.
|
|
152
|
+
const fresh = queues.get(targetGroupID);
|
|
153
|
+
if (fresh != null && fresh.entries.length >= config.maxCount) {
|
|
154
|
+
flush(targetGroupID);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
startTimer(targetGroupID);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
state.entries.push(entry);
|
|
161
|
+
state.bytes += entryBytes;
|
|
162
|
+
// Count threshold: flush synchronously without waiting for the timer.
|
|
163
|
+
if (state.entries.length >= config.maxCount) {
|
|
164
|
+
flush(targetGroupID);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function dispose() {
|
|
168
|
+
if (disposed) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
disposed = true;
|
|
172
|
+
// Snapshot keys — flush mutates the map.
|
|
173
|
+
const targets = Array.from(queues.keys());
|
|
174
|
+
for (const targetGroupID of targets){
|
|
175
|
+
flush(targetGroupID);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
enqueue,
|
|
180
|
+
dispose
|
|
181
|
+
};
|
|
182
|
+
}
|