@kubun/plugin-p2p 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/context/delegation.d.ts +4 -0
- package/lib/context/delegation.js +1 -0
- package/lib/context/group.js +1 -1
- package/lib/context/hub.js +1 -1
- package/lib/context/join.js +1 -1
- package/lib/context/types.d.ts +25 -2
- package/lib/groups/broadcast-service.d.ts +137 -4
- package/lib/groups/broadcast-service.js +1 -1
- package/lib/groups/broadcast.d.ts +120 -0
- package/lib/groups/broadcast.js +1 -1
- package/lib/groups/events.d.ts +16 -5
- package/lib/groups/events.js +1 -1
- package/lib/groups/group-handle-registry.d.ts +8 -0
- package/lib/groups/group-handle-registry.js +1 -1
- package/lib/groups/group-health-monitor.d.ts +45 -0
- package/lib/groups/group-health-monitor.js +1 -0
- package/lib/groups/invite-payload.d.ts +16 -0
- package/lib/groups/join-utils.d.ts +68 -2
- package/lib/groups/join-utils.js +1 -1
- package/lib/groups/manager.d.ts +15 -4
- package/lib/groups/manager.js +1 -1
- package/lib/groups/rejoin-codec.d.ts +14 -0
- package/lib/groups/rejoin-codec.js +1 -0
- package/lib/groups/store-received-grant.d.ts +47 -0
- package/lib/groups/store-received-grant.js +1 -0
- package/lib/groups/store-received-revocation.d.ts +46 -0
- package/lib/groups/store-received-revocation.js +1 -0
- package/lib/groups/wire-frame.d.ts +34 -0
- package/lib/groups/wire-frame.js +1 -0
- package/lib/hub/circle-catchup-requester.d.ts +58 -0
- package/lib/hub/circle-catchup-requester.js +1 -0
- package/lib/hub/circle-catchup-responder.d.ts +50 -0
- package/lib/hub/circle-catchup-responder.js +1 -0
- package/lib/hub/epoch-stale-detector.d.ts +18 -0
- package/lib/hub/epoch-stale-detector.js +1 -0
- package/lib/hub/errors.d.ts +7 -5
- package/lib/hub/errors.js +1 -1
- package/lib/hub/forward-remote-broadcast.d.ts +3 -3
- package/lib/hub/forward-remote-broadcast.js +1 -1
- package/lib/hub/group-channel.d.ts +30 -0
- package/lib/hub/group-channel.js +1 -1
- package/lib/hub/hub-connection.js +1 -1
- package/lib/hub/manager.d.ts +5 -4
- package/lib/hub/manager.js +1 -1
- package/lib/hub/receive-handler.d.ts +20 -2
- package/lib/hub/receive-handler.js +1 -1
- package/lib/hub/rejoin-manager.d.ts +78 -0
- package/lib/hub/rejoin-manager.js +1 -0
- package/lib/hub/rejoin-responder.d.ts +32 -0
- package/lib/hub/rejoin-responder.js +1 -0
- package/lib/hub/relay-manager.d.ts +23 -1
- package/lib/hub/relay-manager.js +1 -1
- package/lib/hub/send-handler.d.ts +16 -0
- package/lib/hub/send-handler.js +1 -1
- package/lib/hub/wiring.d.ts +33 -3
- package/lib/hub/wiring.js +1 -1
- package/lib/index.d.ts +19 -5
- package/lib/index.js +1 -1
- package/lib/protocol.d.ts +7 -0
- package/lib/protocol.js +1 -1
- package/lib/schema.d.ts +2 -2
- package/lib/schema.js +79 -3
- package/lib/sync/authorize.d.ts +25 -0
- package/lib/sync/authorize.js +1 -0
- package/lib/sync/broadcast-sender.js +1 -1
- package/lib/sync/errors.d.ts +11 -0
- package/lib/sync/errors.js +1 -0
- package/lib/sync/forwarder.d.ts +1 -0
- package/lib/sync/forwarder.js +1 -1
- package/lib/sync/handlers.d.ts +3 -18
- package/lib/sync/handlers.js +1 -1
- package/lib/sync/hub-tunnel-sync-listener.d.ts +2 -0
- package/lib/sync/hub-tunnel-sync-listener.js +1 -1
- package/lib/sync/merkle-apply.js +1 -1
- package/lib/sync/merkle-channel.d.ts +19 -0
- package/lib/sync/merkle-channel.js +1 -1
- package/lib/sync/peer-registry.d.ts +7 -0
- package/lib/sync/peer-registry.js +1 -1
- package/lib/sync/sync-client.d.ts +2 -0
- package/lib/sync/sync-client.js +1 -1
- package/lib/sync/sync-manager.js +1 -1
- package/lib/types.d.ts +122 -1
- package/lib/utils.d.ts +5 -0
- package/lib/utils.js +1 -0
- package/package.json +41 -41
- package/lib/sync/catalog-match.d.ts +0 -13
- package/lib/sync/catalog-match.js +0 -1
package/lib/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ServerTransport as e}from"@enkaku/http-server-transport";import{Server as r}from"@enkaku/server";import{isFullIdentity as t}from"@enkaku/token";import{DirectTransports as o}from"@enkaku/transport";import{
|
|
1
|
+
import{ServerTransport as e}from"@enkaku/http-server-transport";import{Server as r}from"@enkaku/server";import{isFullIdentity as t}from"@enkaku/token";import{DirectTransports as o}from"@enkaku/transport";import{p2pStoreDefinition as s}from"@kubun/store-p2p";import{createDelegationContext as n}from"./context/delegation.js";import{createGroupContext as a}from"./context/group.js";import{createHubContext as i}from"./context/hub.js";import{createJoinContext as c}from"./context/join.js";import{createSyncContext as p}from"./context/sync.js";import{createP2PEventEmitter as d}from"./groups/events.js";import{GroupHandleRegistry as u}from"./groups/group-handle-registry.js";import{GroupHealthMonitor as l}from"./groups/group-health-monitor.js";import{GroupManager as g}from"./groups/manager.js";import{forwardRemoteBroadcast as m}from"./hub/forward-remote-broadcast.js";import{createHTTPHubClient as y}from"./hub/http-client.js";import{captureServerDID as b}from"./hub/manager.js";import{DISABLED_HUB as f,setupHubRelay as h}from"./hub/wiring.js";import{createP2PSchemaExtension as j}from"./schema.js";import{createBroadcastQueue as v,DEFAULT_BROADCAST_BATCH_CONFIG as x}from"./sync/broadcast-queue.js";import{DEFAULT_PUSH_SYNC_CONFIG as w,wireBroadcastSender as S}from"./sync/broadcast-sender.js";import{createSyncHandlers as D}from"./sync/handlers.js";import{SyncManager as R}from"./sync/sync-manager.js";export{processBroadcast}from"./groups/broadcast.js";export{deserializeBroadcast,serializeBroadcast}from"./groups/broadcast-codec.js";export{ApplyError,BroadcastService,DecryptError}from"./groups/broadcast-service.js";export{createFilteredGenerator,createP2PEventEmitter}from"./groups/events.js";export{GroupHandleRegistry}from"./groups/group-handle-registry.js";export{decodeFullJoinRequest,decodeInvitePayload,decodeJoinRequest,encodeFullJoinRequest,encodeInvitePayload,encodeJoinRequest}from"./groups/invite-payload.js";export{GroupManager}from"./groups/manager.js";export{MLSEncryptor}from"./groups/mls-encryptor.js";export{restoreMLSGroupHandle}from"./groups/mls-group-handle.js";export{replacer as mlsJSONReplacer,reviver as mlsJSONReviver}from"./groups/mls-json.js";export{deserializeMLSGroupState,serializeMLSGroupState}from"./groups/mls-state.js";export{createP2PSchemaExtension}from"./schema.js";export{createBroadcastQueue,DEFAULT_BROADCAST_BATCH_CONFIG}from"./sync/broadcast-queue.js";export{DEFAULT_PUSH_SYNC_CONFIG,wireBroadcastSender}from"./sync/broadcast-sender.js";export{resolveCatalogSyncScopes}from"./sync/catalog-scope.js";export{SyncAccessDeniedError}from"./sync/errors.js";export{evaluateAndForward}from"./sync/forwarder.js";export{checkSyncDelegation}from"./sync/handlers.js";export{HubTunnelSyncListener}from"./sync/hub-tunnel-sync-listener.js";export{HubTunnelSyncProvider}from"./sync/hub-tunnel-sync-provider.js";export{negotiateDirection,PeerRegistry}from"./sync/peer-registry.js";export{SyncClient}from"./sync/sync-client.js";export{SyncManager}from"./sync/sync-manager.js";export{DIDObservingTransport}from"./hub/did-observing-transport.js";export{ReconnectingError}from"./hub/errors.js";export{createHTTPHubClient}from"./hub/http-client.js";export{bindHubToGroup,captureServerDID,createHub,deleteHub,getHub,HubServerDIDConflictError,listGroupsByHub,listHubs,listHubsByGroup,unbindHubFromGroup,updateHub,upsertHub}from"./hub/manager.js";export{HubRelayManager}from"./hub/relay-manager.js";export{DISABLED_HUB,setupHubRelay}from"./hub/wiring.js";export const DEFAULT_RECEIVE_CONFIG={storeUnreadable:"persist"};export function createP2PPlugin(P){return I=>{var E;let H,L,B,C;I.db.register(s);let G=I.db.adapter;if(!(t(E=I.identity)&&"privateKey"in E))throw Error("createP2PPlugin requires an OwnIdentity (full identity with privateKey) — received a narrower Identity");let A=I.identity,F=I.hlc,T={...DEFAULT_RECEIVE_CONFIG,...P?.receive},U=P?.defaultAccessLevel??{read:"only_owner",write:"only_owner"},_=new u({stores:I.db,deviceID:A.id,logger:I.getLogger("group-handle-registry")}),k=new g({identity:A,getRandomID:I.runtime.getRandomID,registry:_,hlc:F}),O=d(),q=new l({emitter:O}),N=new R({deployClusters:async e=>{let r=Object.values(e);await I.engine.deployGraph({clusters:r})},graph:I.graph,runtime:I.runtime,identity:I.identity,logger:I.getLogger("sync"),storeUnreadable:T.storeUnreadable,defaultAccessLevel:U}),M=D({db:I.db,graph:I.graph,logger:I.getLogger("sync-handlers"),peerRegistry:N.peerRegistry,selfDID:A.id,storeUnreadable:T.storeUnreadable,defaultAccessLevel:U}),J=new Map,z=[],K=I.getLogger("hub-relay"),V=f,W={...w,...P?.pushSync};if(P?.hub){let e="object"==typeof P.hub&&null!=P.hub.createHubClient?P.hub.createHubClient:y({identity:A,fetch:I.runtime.fetch});V=h({identity:A,getRandomID:I.runtime.getRandomID,db:I.db,hlc:F,graph:I.graph,emitter:O,createHubClient:e,registry:_,monitor:q,onRemoteBroadcast:e=>{m(O,e,K).catch(e=>{K.error("forwardRemoteBroadcast failed",{error:e})})},logger:K,storeUnreadable:T.storeUnreadable,defaultAccessLevel:U,forwarding:P.forwarding,onServerDIDObserved:({hubURL:e,serverDID:r})=>b({stores:I.db,hubURL:e,serverDID:r}).then(()=>void 0)}),W.enabled&&(L=v({config:{...x,...P.batch},scheduleBroadcast:V.scheduleBroadcast,logger:I.getLogger("broadcast-queue")}),H=S({db:I.db,eventBus:I.eventBus,selfDID:A.id,queue:L,logger:I.getLogger("broadcast-sender")}))}let Q={hubReady:V.ready,addPeer:e=>N.addPeer({config:e,stores:I.db}),removePeer:e=>N.removePeer({peerDID:e,stores:I.db}),listPeers:()=>N.listPeers({stores:I.db}),syncWithPeer:e=>N.merkleSyncWithPeer({...e,stores:I.db}),getStatus:e=>N.getStatus({peerDID:e,stores:I.db}),onSyncEvent:e=>N.onSyncEvent(e),createSyncTransport:function(e){let t=new o({signal:e}),s=new r({getRandomID:I.runtime.getRandomID,handlers:M,identity:A,accessRules:{"sync/*":{allow:!0}},logger:I.getLogger("sync-server"),transports:[t.server],signal:e});return z.push(s),t.client}};if(P?.http){let t=P.http,o=("string"==typeof t?t:"object"==typeof t?t.path??"sync":"sync").replace(/^\//,""),s="object"==typeof t?t.allowedOrigin??["*"]:["*"];Q.syncReady=(async()=>{let t=await I.engine.getAPI("http");await t.listening,B=new e({allowedOrigin:s}),C=new r({getRandomID:I.runtime.getRandomID,handlers:M,identity:A,accessRules:{"sync/*":{allow:!0}},logger:I.getLogger("sync-http-server"),transports:[B]}),z.push(C),t.registerProtocol(o,B.fetch.bind(B))})()}return{name:"p2p",schemaExtension:e=>j(O),api:Q,createContextFactory:()=>(e,r)=>{let t={identity:A,groupManager:k,registry:_,syncManager:N,stores:r,adapter:G,hlc:F,emitter:O,pendingJoinRequests:J,runtime:I.runtime,autoAcceptPeers:P?.autoAcceptPeers,scheduleBroadcast:V.scheduleBroadcast,broadcastNow:V.broadcastNow,scheduleSendCommit:V.scheduleSendCommit,monitor:q,rejoinGroup:V.rejoinGroup,logger:I.getLogger("context")};return{group:a(e,t),sync:p(e,t),join:c(e,t),hub:i(e,t),delegation:n(e,t)}},dispose:async()=>{if(null!=H)try{H()}catch{}if(null!=L)try{L.dispose()}catch{}let e=(async()=>{for(let e of z)await e.dispose();null!=B&&await B.dispose()})(),r=I.getLogger("dispose"),t=await Promise.allSettled([e,N.dispose(),V.dispose()]),o=["sync-http","sync-manager","hub"];t.forEach((e,t)=>{"rejected"===e.status&&r.error("dispose branch failed",{branch:o[t],error:e.reason})})}}}}
|
package/lib/protocol.d.ts
CHANGED
|
@@ -129,6 +129,13 @@ export declare const syncProtocol: {
|
|
|
129
129
|
readonly type: "string";
|
|
130
130
|
readonly enum: readonly ["push", "pull", "both"];
|
|
131
131
|
};
|
|
132
|
+
readonly delegationTokens: {
|
|
133
|
+
readonly type: "array";
|
|
134
|
+
readonly items: {
|
|
135
|
+
readonly type: "string";
|
|
136
|
+
};
|
|
137
|
+
readonly description: "Delegation JWTs authorizing the caller for non-owned scopes";
|
|
138
|
+
};
|
|
132
139
|
};
|
|
133
140
|
readonly required: readonly ["scopes", "excludedDocumentIDs", "tree", "direction"];
|
|
134
141
|
readonly additionalProperties: false;
|
package/lib/protocol.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const syncProtocol={"sync/negotiate":{type:"request",param:{type:"object",properties:{scopes:{type:"array",items:{type:"object",properties:{modelID:{type:"string"},ownerDID:{type:"string"}},required:["modelID","ownerDID"],additionalProperties:!1},description:"Requested sync scopes as (model, owner) pairs"},delegationTokens:{type:"array",items:{type:"string"},description:"JWT delegation tokens for authorization"},catalogIDs:{type:"array",items:{type:"string"},description:"Catalog IDs to resolve sync scope from criteria"},knownModelIDs:{type:"array",items:{type:"string"},description:"Model IDs the requester already has — used to identify missing clusters"},direction:{type:"string",enum:["push","pull","both"]}},required:["delegationTokens"],additionalProperties:!1},result:{type:"object",properties:{direction:{type:"string",enum:["push","pull","both"]},acceptedScopes:{type:"array",items:{type:"object",properties:{modelID:{type:"string"},ownerDID:{type:"string"}},required:["modelID","ownerDID"],additionalProperties:!1}},excludedDocumentIDs:{type:"array",items:{type:"string"}},missingClusters:{type:"object",description:"Cluster definitions for models the requester does not have yet",additionalProperties:!0}},required:["acceptedScopes","excludedDocumentIDs"],additionalProperties:!1}},"sync/merkle-sync":{type:"channel",param:{type:"object",properties:{scopes:{type:"array",items:{type:"object",properties:{modelID:{type:"string"},ownerDID:{type:"string"}},required:["modelID","ownerDID"],additionalProperties:!1}},excludedDocumentIDs:{type:"array",items:{type:"string"}},tree:{type:"object",description:"Merkle tree bucket hashes from requester",additionalProperties:{type:"string"}},direction:{type:"string",enum:["push","pull","both"]}},required:["scopes","excludedDocumentIDs","tree","direction"],additionalProperties:!1},send:{type:"object",properties:{type:{type:"string",enum:["mutations","complete"]},mutationJWTs:{type:"array",items:{type:"string"},description:"Signed mutation JWTs for divergent time buckets"}},required:["type"],additionalProperties:!1},receive:{type:"object",properties:{type:{type:"string",enum:["tree","mutations","complete"]},tree:{type:"object",description:"Responder merkle tree bucket hashes",additionalProperties:{type:"string"}},mutationJWTs:{type:"array",items:{type:"string"},description:"Signed mutation JWTs for divergent time buckets"}},required:["type"],additionalProperties:!1},result:{type:"object",properties:{success:{type:"boolean"},divergentBuckets:{type:"number"},mutationsSent:{type:"number"},mutationsReceived:{type:"number"}},required:["success","divergentBuckets","mutationsSent","mutationsReceived"],additionalProperties:!1}}};
|
|
1
|
+
export const syncProtocol={"sync/negotiate":{type:"request",param:{type:"object",properties:{scopes:{type:"array",items:{type:"object",properties:{modelID:{type:"string"},ownerDID:{type:"string"}},required:["modelID","ownerDID"],additionalProperties:!1},description:"Requested sync scopes as (model, owner) pairs"},delegationTokens:{type:"array",items:{type:"string"},description:"JWT delegation tokens for authorization"},catalogIDs:{type:"array",items:{type:"string"},description:"Catalog IDs to resolve sync scope from criteria"},knownModelIDs:{type:"array",items:{type:"string"},description:"Model IDs the requester already has — used to identify missing clusters"},direction:{type:"string",enum:["push","pull","both"]}},required:["delegationTokens"],additionalProperties:!1},result:{type:"object",properties:{direction:{type:"string",enum:["push","pull","both"]},acceptedScopes:{type:"array",items:{type:"object",properties:{modelID:{type:"string"},ownerDID:{type:"string"}},required:["modelID","ownerDID"],additionalProperties:!1}},excludedDocumentIDs:{type:"array",items:{type:"string"}},missingClusters:{type:"object",description:"Cluster definitions for models the requester does not have yet",additionalProperties:!0}},required:["acceptedScopes","excludedDocumentIDs"],additionalProperties:!1}},"sync/merkle-sync":{type:"channel",param:{type:"object",properties:{scopes:{type:"array",items:{type:"object",properties:{modelID:{type:"string"},ownerDID:{type:"string"}},required:["modelID","ownerDID"],additionalProperties:!1}},excludedDocumentIDs:{type:"array",items:{type:"string"}},tree:{type:"object",description:"Merkle tree bucket hashes from requester",additionalProperties:{type:"string"}},direction:{type:"string",enum:["push","pull","both"]},delegationTokens:{type:"array",items:{type:"string"},description:"Delegation JWTs authorizing the caller for non-owned scopes"}},required:["scopes","excludedDocumentIDs","tree","direction"],additionalProperties:!1},send:{type:"object",properties:{type:{type:"string",enum:["mutations","complete"]},mutationJWTs:{type:"array",items:{type:"string"},description:"Signed mutation JWTs for divergent time buckets"}},required:["type"],additionalProperties:!1},receive:{type:"object",properties:{type:{type:"string",enum:["tree","mutations","complete"]},tree:{type:"object",description:"Responder merkle tree bucket hashes",additionalProperties:{type:"string"}},mutationJWTs:{type:"array",items:{type:"string"},description:"Signed mutation JWTs for divergent time buckets"}},required:["type"],additionalProperties:!1},result:{type:"object",properties:{success:{type:"boolean"},divergentBuckets:{type:"number"},mutationsSent:{type:"number"},mutationsReceived:{type:"number"}},required:["success","divergentBuckets","mutationsSent","mutationsReceived"],additionalProperties:!1}}};
|
package/lib/schema.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { SchemaExtension } from '@kubun/engine';
|
|
2
|
-
import { type
|
|
3
|
-
export declare function createP2PSchemaExtension(emitter:
|
|
2
|
+
import { type P2PEventEmitter } from './groups/events.js';
|
|
3
|
+
export declare function createP2PSchemaExtension(emitter: P2PEventEmitter): SchemaExtension;
|
package/lib/schema.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{createFilteredGenerator as e}from"./groups/events.js";function r(e){if(null==e.p2p)throw Error("p2p plugin is not wired into this context");return e.p2p}let t=`
|
|
1
|
+
import{createFilteredGenerator as e}from"./groups/events.js";function r(e){if(null==e.p2p)throw Error("p2p plugin is not wired into this context");return e.p2p}let t={healthy:"HEALTHY",degraded:"DEGRADED",recovering:"RECOVERING",recovered:"RECOVERED","recovery-failed":"RECOVERY_FAILED"},o={"epoch-stale":"EPOCH_STALE"};async function*a(){}let i=`
|
|
2
2
|
type PeerGroup {
|
|
3
3
|
id: ID!
|
|
4
4
|
name: String!
|
|
@@ -173,6 +173,44 @@ type DeleteHubPayload {
|
|
|
173
173
|
boundGroupIDs: [ID!]!
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
type DelegationToken {
|
|
177
|
+
jwt: String!
|
|
178
|
+
jti: String!
|
|
179
|
+
grantor: ID!
|
|
180
|
+
audience: ID!
|
|
181
|
+
resource: String!
|
|
182
|
+
exp: Int!
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
input DelegationTokenCovers {
|
|
186
|
+
modelID: ID
|
|
187
|
+
docID: ID
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
"""
|
|
191
|
+
A verified revocation of a delegation token addressed to the viewer.
|
|
192
|
+
Only surfaces while the underlying held capability still exists locally;
|
|
193
|
+
revocations whose cap was deleted or never arrived are excluded by the
|
|
194
|
+
audience-derivation join.
|
|
195
|
+
"""
|
|
196
|
+
type RevokedDelegationToken {
|
|
197
|
+
jti: ID!
|
|
198
|
+
grantor: ID!
|
|
199
|
+
audience: ID!
|
|
200
|
+
revokerDID: DID!
|
|
201
|
+
"Issuer's revocation-record timestamp (iat), Unix epoch seconds."
|
|
202
|
+
revokedAt: Int!
|
|
203
|
+
"""
|
|
204
|
+
Wall-clock time the revocation became binding, Unix epoch seconds. For
|
|
205
|
+
self-issued revocations this equals revokedAt (verified at mint time);
|
|
206
|
+
for received revocations it is the receiver's clock at the moment the
|
|
207
|
+
cap-vs-revoker cross-check succeeded.
|
|
208
|
+
"""
|
|
209
|
+
verifiedAt: Int!
|
|
210
|
+
capExp: Int
|
|
211
|
+
groupID: ID!
|
|
212
|
+
}
|
|
213
|
+
|
|
176
214
|
extend type Query {
|
|
177
215
|
groups: [PeerGroup!]!
|
|
178
216
|
group(id: ID!): PeerGroup
|
|
@@ -182,6 +220,38 @@ extend type Query {
|
|
|
182
220
|
hub(id: ID!): PeerHub
|
|
183
221
|
syncPeers: [SyncPeer!]!
|
|
184
222
|
syncStatus: SyncStatus!
|
|
223
|
+
ownDelegationTokens(grantor: ID, covers: DelegationTokenCovers): [DelegationToken!]!
|
|
224
|
+
ownRevokedDelegationTokens(grantor: ID): [RevokedDelegationToken!]!
|
|
225
|
+
groupHealth(groupID: ID!): GroupHealthStatus!
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
enum GroupHealthCondition {
|
|
229
|
+
EPOCH_STALE
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
enum GroupHealthState {
|
|
233
|
+
HEALTHY
|
|
234
|
+
DEGRADED
|
|
235
|
+
RECOVERING
|
|
236
|
+
RECOVERED
|
|
237
|
+
RECOVERY_FAILED
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
type GroupHealthEvent {
|
|
241
|
+
groupID: ID!
|
|
242
|
+
condition: GroupHealthCondition!
|
|
243
|
+
state: GroupHealthState!
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
type GroupHealthStatus {
|
|
247
|
+
groupID: ID!
|
|
248
|
+
state: GroupHealthState!
|
|
249
|
+
lastTransitionAt: DateTimeISO
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
type RejoinGroupPayload {
|
|
253
|
+
groupID: ID!
|
|
254
|
+
state: GroupHealthState!
|
|
185
255
|
}
|
|
186
256
|
|
|
187
257
|
extend type Mutation {
|
|
@@ -190,7 +260,7 @@ extend type Mutation {
|
|
|
190
260
|
createCircle(groupID: ID!, name: String!, description: String): PeerCircle!
|
|
191
261
|
addCircleMember(circleID: ID!, memberDID: DID!, role: String!): PeerCircleMember!
|
|
192
262
|
createCatalog(name: String!, description: String, filterCriteria: String!): PeerCatalog!
|
|
193
|
-
inviteToGroup(groupID: ID!, joinRequest: String!): InviteToGroupPayload!
|
|
263
|
+
inviteToGroup(groupID: ID!, joinRequest: String!, grants: [String!], revocations: [String!]): InviteToGroupPayload!
|
|
194
264
|
joinGroup(invitePayload: String!, joinRequestPayload: String!): JoinGroupPayload!
|
|
195
265
|
prepareJoinRequest: PrepareJoinRequestPayload!
|
|
196
266
|
completeJoin(invitePayload: String!): CompleteJoinPayload!
|
|
@@ -202,6 +272,8 @@ extend type Mutation {
|
|
|
202
272
|
deleteCatalog(catalogID: ID!): DeleteCatalogPayload!
|
|
203
273
|
deleteCircle(circleID: ID!): DeleteCirclePayload!
|
|
204
274
|
removeCircleMember(circleID: ID!, memberDID: DID!): RemoveCircleMemberPayload!
|
|
275
|
+
grantWriteCapability(to: DID!, res: String!, groupID: ID!, expiresIn: Int): String!
|
|
276
|
+
revokeCapability(jti: ID!): Boolean!
|
|
205
277
|
addSyncPeer(peerDID: DID!, endpoint: URL!, mode: String, priority: String, trustLevel: String): Boolean!
|
|
206
278
|
removeSyncPeer(peerDID: DID!): RemoveSyncPeerPayload!
|
|
207
279
|
createHub(input: CreateHubInput!): PeerHub!
|
|
@@ -209,6 +281,7 @@ extend type Mutation {
|
|
|
209
281
|
deleteHub(id: ID!, force: Boolean): DeleteHubPayload!
|
|
210
282
|
bindHubToGroup(hubID: ID!, groupID: ID!): Boolean!
|
|
211
283
|
unbindHubFromGroup(hubID: ID!, groupID: ID!): Boolean!
|
|
284
|
+
rejoinGroup(groupID: ID!): RejoinGroupPayload!
|
|
212
285
|
}
|
|
213
286
|
|
|
214
287
|
extend type PeerGroup {
|
|
@@ -231,5 +304,8 @@ extend type Subscription {
|
|
|
231
304
|
circleMemberAdded(circleID: ID!): PeerCircleMember!
|
|
232
305
|
circleMemberRemoved(circleID: ID!): PeerCircleMember!
|
|
233
306
|
circleCatalogsChanged(circleID: ID!): PeerCircle!
|
|
307
|
+
ownDelegationTokenAdded: DelegationToken!
|
|
308
|
+
ownDelegationTokenRevoked: RevokedDelegationToken!
|
|
309
|
+
groupHealthChanged(groupID: ID): GroupHealthEvent!
|
|
234
310
|
}
|
|
235
|
-
`;export function createP2PSchemaExtension(
|
|
311
|
+
`;export function createP2PSchemaExtension(n){return{sdl:i,resolvers:{queryFields:{groups:async(e,t,o)=>await r(o).group.list(),group:async(e,t,o)=>await r(o).group.get(t.id),groupMembers:async(e,t,o)=>await r(o).group.listMembers(t.groupID),circles:async(e,t,o)=>await r(o).group.listCircles(t.groupID),hubs:async(e,t,o)=>await r(o).hub.list(),hub:async(e,t,o)=>await r(o).hub.get(t.id),syncPeers:async(e,t,o)=>await r(o).sync.listPeers(),syncStatus:(e,t,o)=>{let a=r(o).sync.getStatus();return{...a,lastSyncByPeer:JSON.stringify(a.lastSyncByPeer)}},groupHealth:(e,o,a)=>{let i=o.groupID,{state:n,lastTransitionAt:u}=r(a).hub.getGroupHealth(i);return{groupID:i,state:t[n],lastTransitionAt:null==u?null:new Date(u).toISOString()}},ownDelegationTokens:async(e,r,t)=>{let o=t.getViewer();if(null==o||null==t.p2p)return[];let a=r.grantor??null,i=r.covers??null;return await t.p2p.delegation.listOwnTokens({viewerDID:o,grantor:a,covers:i})},ownRevokedDelegationTokens:async(e,r,t)=>{let o=t.getViewer();if(null==o||null==t.p2p)return[];let a=r.grantor??null;return await t.p2p.delegation.listOwnRevokedTokens({viewerDID:o,grantor:a})}},mutationFields:{createGroup:async(e,t,o)=>{let a=t.input;return{group:await r(o).group.create(a)}},addGroupMember:async(e,t,o)=>await r(o).group.addMember(t.groupID,t.memberDID,t.role),createCircle:async(e,t,o)=>await r(o).group.createCircle({groupID:t.groupID,name:t.name,description:t.description}),addCircleMember:async(e,t,o)=>await r(o).group.addCircleMember(t.circleID,t.memberDID,t.role),createCatalog:async(e,t,o)=>await r(o).group.createCatalog({name:t.name,description:t.description,filterCriteria:t.filterCriteria}),inviteToGroup:async(e,t,o)=>await r(o).group.invite(t.groupID,t.joinRequest,t.grants,t.revocations),joinGroup:async(e,t,o)=>await r(o).group.join(t.invitePayload,t.joinRequestPayload),prepareJoinRequest:async(e,t,o)=>await r(o).join.prepareRequest(),completeJoin:async(e,t,o)=>await r(o).join.complete(t.invitePayload),leaveGroup:async(e,t,o)=>await r(o).group.leave(t.groupID),removeGroupMember:async(e,t,o)=>await r(o).group.removeMember(t.groupID,t.memberDID),updateCatalog:async(e,t,o)=>{let a=t.input;return await r(o).group.updateCatalog(a)},updateGroup:async(e,t,o)=>{let a=t.input;return{group:await r(o).group.update(a)}},updateCircle:async(e,t,o)=>{let a=t.input;return{circle:await r(o).group.updateCircle(a)}},deleteCatalog:async(e,t,o)=>await r(o).group.deleteCatalog(t.catalogID),deleteCircle:async(e,t,o)=>await r(o).group.deleteCircle(t.circleID),removeCircleMember:async(e,t,o)=>await r(o).group.removeCircleMember(t.circleID,t.memberDID),grantWriteCapability:async(e,t,o)=>await r(o).group.grantWriteCapability({to:t.to,res:t.res,groupID:t.groupID,expiresIn:t.expiresIn}),revokeCapability:async(e,t,o)=>await r(o).group.revokeCapability({jti:t.jti}),addSyncPeer:async(e,t,o)=>(await r(o).sync.addPeer({peerDID:t.peerDID,endpoint:t.endpoint,mode:t.mode??"on-demand",allowedUsers:{all:!0},priority:t.priority??"0",trustLevel:t.trustLevel??"trusted"}),!0),removeSyncPeer:async(e,t,o)=>(await r(o).sync.removePeer(t.peerDID),{success:!0}),createHub:async(e,t,o)=>{let a=t.input;return await r(o).hub.create(a)},updateHub:async(e,t,o)=>{let a=t.input;return await r(o).hub.update(a)},deleteHub:async(e,t,o)=>await r(o).hub.delete(t.id,t.force??!1),bindHubToGroup:async(e,t,o)=>await r(o).hub.bind({hubID:t.hubID,groupID:t.groupID}),unbindHubFromGroup:async(e,t,o)=>await r(o).hub.unbind({hubID:t.hubID,groupID:t.groupID}),rejoinGroup:(e,t,o)=>{let a=t.groupID,i=r(o).hub,{state:n}=i.getGroupHealth(a);return"healthy"!==n&&"recovering"!==n&&i.rejoinGroup(a),{groupID:a,state:"healthy"===n?"HEALTHY":"RECOVERING"}}},typeFields:{PeerGroup:{hubs:async(e,t,o)=>await r(o).hub.listByGroup(e.id)},PeerHub:{groups:async(e,t,o)=>await r(o).hub.listGroups(e.id)}},subscriptionFields:{groupJoined:{resolve:e=>e,subscribe:()=>e(n,"groupJoined")},groupLeft:{resolve:e=>e,subscribe:()=>e(n,"groupLeft")},groupDataChanged:{resolve:e=>e,subscribe:(r,t)=>e(n,"groupDataChanged",e=>e.groupID===t.groupID)},groupMemberJoined:{resolve:e=>e,subscribe:(r,t)=>e(n,"groupMemberJoined",e=>e.groupID===t.groupID)},groupMemberLeft:{resolve:e=>e,subscribe:(r,t)=>e(n,"groupMemberLeft",e=>e.groupID===t.groupID)},circleCreated:{resolve:e=>e,subscribe:(r,t)=>e(n,"circleCreated",e=>e.groupID===t.groupID)},circleDeleted:{resolve:e=>e,subscribe:(r,t)=>e(n,"circleDeleted",e=>e.groupID===t.groupID)},circleDataChanged:{resolve:e=>e,subscribe:(r,t)=>e(n,"circleDataChanged",e=>e.circleID===t.circleID)},circleMemberAdded:{resolve:e=>e,subscribe:(r,t)=>e(n,"circleMemberAdded",e=>e.circleID===t.circleID)},circleMemberRemoved:{resolve:e=>e,subscribe:(r,t)=>e(n,"circleMemberRemoved",e=>e.circleID===t.circleID)},circleCatalogsChanged:{resolve:e=>e,subscribe:(r,t)=>e(n,"circleCatalogsChanged",e=>e.circleID===t.circleID)},ownDelegationTokenAdded:{resolve:e=>e,subscribe:(r,t,o)=>{let i=o.getViewer();return null==i?a():e(n,"delegationTokenAdded",e=>e.audience===i)}},ownDelegationTokenRevoked:{resolve:e=>e,subscribe:(r,t,o)=>{let i=o.getViewer();return null==i?a():e(n,"delegationTokenRevoked",e=>e.audience===i)}},groupHealthChanged:{resolve:e=>({groupID:e.groupID,condition:o[e.condition],state:t[e.state]}),subscribe:(r,t)=>{let o=t.groupID;return e(n,"groupHealthChanged",null==o?void 0:e=>e.groupID===o)}}}}}}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type VerifyTokenHook } from '@enkaku/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
|
+
*/
|
|
13
|
+
export declare function checkSyncDelegation(viewerDID: string, ownerDID: string, delegationTokens: Array<string>, revocationChecker?: VerifyTokenHook): Promise<boolean>;
|
|
14
|
+
export type AuthorizeScopeParams = {
|
|
15
|
+
viewerDID: string;
|
|
16
|
+
ownerDID: string;
|
|
17
|
+
delegationTokens: Array<string>;
|
|
18
|
+
revocationChecker?: VerifyTokenHook;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Authorize a single sync scope: the viewer must be the owner, or hold a
|
|
22
|
+
* delegation token granting read access to the owner's documents. Shared by
|
|
23
|
+
* negotiate and merkle-sync so the two access gates cannot drift apart.
|
|
24
|
+
*/
|
|
25
|
+
export declare function authorizeScope(params: AuthorizeScopeParams): Promise<boolean>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{checkCapability as e}from"@enkaku/capability";export async function checkSyncDelegation(r,t,n,o){if(!n||0===n.length)return!1;let c=["*","urn:kubun:user:*",`urn:kubun:user:${t}`],a="document/read";for(let u of c)try{return await e({act:a,res:u},{iss:r,sub:t,cap:n},{verifyToken:o}),!0}catch{}for(let u of n)for(let n of c)try{return await e({act:a,res:n},{iss:r,sub:t,cap:u},{verifyToken:o}),!0}catch{}return!1}export async function authorizeScope(e){return e.viewerDID===e.ownerDID||checkSyncDelegation(e.viewerDID,e.ownerDID,e.delegationTokens,e.revocationChecker)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{resolveAccessRule as
|
|
1
|
+
import{catalogMatchesDoc as e,resolveAccessRule as r}from"@kubun/engine";import{DocumentID as t}from"@kubun/id";import{getGraphStore as o}from"@kubun/store-graph";import{getP2PStore as l}from"@kubun/store-p2p";import{computeBroadcastScopes as a}from"./scope-resolver.js";export const DEFAULT_PUSH_SYNC_CONFIG={enabled:!1};let n={read:"only_owner",write:"only_owner"};export function wireBroadcastSender(e){let r=e.defaultAccessLevel??n;return e.eventBus.on("engine:mutation:authored",async t=>{try{await i(t,{...e,defaultAccessLevel:r})}catch(r){e.logger?.warn("broadcast-sender handler failed",{documentID:t.documentID,error:String(r)})}})}async function i(n,i){let s=await o(i.db),d=await l(i.db),u=await s.getDocument(t.fromString(n.documentID));if(null==u)return;let f=await r(u,u.model,u.owner,"read",{getUserModelAccessDefault:(e,r,t)=>s.getUserModelAccessDefault(e,r,t),isMemberOfAnyCircle:(e,r)=>d.isMemberOfAnyCircle(e,r),isMemberOfAnyGroup:(e,r)=>d.isMemberOfAnyGroup(e,r),getModelInterfaces:e=>s.getModelInterfaces(e)},i.defaultAccessLevel);if("only_owner"===f.level)return;let c=await d.getCirclesForMember(i.selfDID),m=new Set;for(let e of c)for(let r of e.catalog_ids??[])m.add(r);let w=m.size>0?await s.getCatalogs(Array.from(m)):new Map,g=[];for(let e of c)for(let r of e.catalog_ids??[]){let t=w.get(r);null!=t&&g.push({groupID:e.group_id,criteria:t.filter_criteria})}let D=await Promise.all(g.map(async r=>({groupID:r.groupID,matched:await e(r.criteria,u,d)}))),p=new Set;for(let e of D)e.matched&&p.add(e.groupID);let I=Array.from(p);if(0===I.length)return;let b=new Set(f.allowedGroups??[]),v=new Set;if(null!=f.allowedCircles&&f.allowedCircles.length>0)for(let e of(await d.getCircles(f.allowedCircles)).values())null!=e.group_id&&v.add(e.group_id);let y=new Set;if(null!=f.allowedDIDs&&f.allowedDIDs.length>0)for(let e of(await d.getGroupsForMembers(f.allowedDIDs)).values())for(let r of e)y.add(r.id);let _=a({resolvedRule:{level:f.level,allowedGroupIDs:b,allowedCircleParentGroupIDs:v,allowedDIDGroupIDs:y},senderGroupIDs:I});if(0!==_.length)for(let e of _){if(!await d.recordBroadcast({docID:n.documentID,version:n.version,mlsGroupID:e}))continue;let r={mutationJWT:n.mutationJWT,docID:n.documentID,version:n.version,modelID:n.modelID};i.queue.enqueue(e,r,i.selfDID)}}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown by the sync handlers when a request is denied before any store
|
|
3
|
+
* read — for example a forged subject, an unauthorized scope, or a
|
|
4
|
+
* disallowed direction. Tagged with the protocol-level error code `KB08`
|
|
5
|
+
* so server-side logging and tests can assert the reason.
|
|
6
|
+
*/
|
|
7
|
+
export declare class SyncAccessDeniedError extends Error {
|
|
8
|
+
readonly code: "KB08";
|
|
9
|
+
readonly reason: string;
|
|
10
|
+
constructor(reason: string, message?: string);
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class SyncAccessDeniedError extends Error{code="KB08";reason;constructor(e,r){super(r??`Sync access denied: ${e}`),this.name="SyncAccessDeniedError",this.reason=e}}
|
package/lib/sync/forwarder.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { DocumentNode } from '@kubun/protocol';
|
|
|
4
4
|
import type { GraphStoreAPI } from '@kubun/store-graph';
|
|
5
5
|
import type { P2PStoreAPI } from '@kubun/store-p2p';
|
|
6
6
|
import type { GroupBroadcastMessage, MutationApplyEntry } from '../groups/broadcast.js';
|
|
7
|
+
export declare function rethrowIfProgrammerError(error: unknown): void;
|
|
7
8
|
/**
|
|
8
9
|
* Context passed to a {@link ForwardFilter} for each candidate forwarding scope.
|
|
9
10
|
*
|
package/lib/sync/forwarder.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{resolveAccessRule as e}from"@kubun/engine";import{
|
|
1
|
+
import{catalogMatchesDoc as r,resolveAccessRule as e}from"@kubun/engine";import{computeBroadcastScopes as o}from"./scope-resolver.js";export function rethrowIfProgrammerError(r){if(r instanceof SyntaxError||r instanceof TypeError||r instanceof ReferenceError||r instanceof RangeError)throw r}export async function evaluateAndForward(t){let a,l;if(!1===t.forwarding)return{forwarded:0,skipped:0};let i=!0===t.forwarding?()=>!0:t.forwarding,n=(await t.p2pStore.getGroupsForMember(t.selfDID)).map(r=>r.id).filter(r=>r!==t.sourceGroupID);if(0===n.length)return{forwarded:0,skipped:0};try{a=await e(t.postState,t.postState.model,t.postState.owner,"read",{getUserModelAccessDefault:(r,e,o)=>t.graphStore.getUserModelAccessDefault(r,e,o),isMemberOfAnyCircle:(r,e)=>t.p2pStore.isMemberOfAnyCircle(r,e),isMemberOfAnyGroup:(r,e)=>t.p2pStore.isMemberOfAnyGroup(r,e),getModelInterfaces:r=>t.graphStore.getModelInterfaces(r)},t.defaultAccessLevel)}catch(r){return rethrowIfProgrammerError(r),t.logger?.warn("forwarder: resolveAccessRule failed, skipping forward",{docID:t.entry.docID,version:t.entry.version,sourceGroupID:t.sourceGroupID,error:String(r)}),{forwarded:0,skipped:0}}let s=new Set(a.allowedGroups??[]),f=new Set;if(null!=a.allowedCircles&&a.allowedCircles.length>0)try{for(let r of(await t.p2pStore.getCircles(a.allowedCircles)).values())null!=r.group_id&&f.add(r.group_id)}catch(r){rethrowIfProgrammerError(r),t.logger?.warn("forwarder: getCircles failed, skipping allowedCircles",{count:a.allowedCircles.length,error:String(r)})}let d=new Set;if(null!=a.allowedDIDs&&a.allowedDIDs.length>0)try{for(let r of(await t.p2pStore.getGroupsForMembers(a.allowedDIDs)).values())for(let e of r)d.add(e.id)}catch(r){rethrowIfProgrammerError(r),t.logger?.warn("forwarder: getGroupsForMembers failed, skipping allowedDIDs",{count:a.allowedDIDs.length,error:String(r)})}let p=o({resolvedRule:{level:a.level,allowedGroupIDs:s,allowedCircleParentGroupIDs:f,allowedDIDGroupIDs:d},senderGroupIDs:n});if(0===p.length)return{forwarded:0,skipped:0};try{l=await t.p2pStore.getCirclesForMember(t.selfDID)}catch(r){return rethrowIfProgrammerError(r),t.logger?.warn("forwarder: getCirclesForMember failed",{selfDID:t.selfDID,error:String(r)}),{forwarded:0,skipped:0}}let c=new Set(p),g=new Map;for(let r of l){if(!c.has(r.group_id))continue;let e=g.get(r.group_id);null!=e?e.push(r):g.set(r.group_id,[r])}let u=new Set;for(let r of g.values())for(let e of r)for(let r of e.catalog_ids??[])u.add(r);let w=new Map;if(u.size>0)try{w=await t.graphStore.getCatalogs(Array.from(u))}catch(r){rethrowIfProgrammerError(r),t.logger?.warn("forwarder: getCatalogs failed, treating all as missing",{count:u.size,error:String(r)})}let D=0,I=0;for(let e of p)try{let o=g.get(e)??[],a=!1;for(let e of o){for(let o of e.catalog_ids??[]){let e=w.get(o);if(null!=e&&await r(e.filter_criteria,t.postState,t.p2pStore)){a=!0;break}}if(a)break}if(!a){I++;continue}let l={docID:t.entry.docID,modelID:t.entry.modelID,ownerDID:t.postState.owner,sourceGroupID:t.sourceGroupID,candidateGroupID:e,postState:t.postState};if(!await i(l)){I++;continue}if(!await t.p2pStore.recordBroadcast({docID:t.entry.docID,version:t.entry.version,mlsGroupID:e}))continue;let n={type:"mutation:apply",entries:[t.entry],senderPeerDID:t.selfDID};await t.scheduleBroadcast(e,n),D++}catch(r){t.logger?.warn("forwarder: candidate evaluation failed",{docID:t.entry.docID,version:t.entry.version,candidateGroupID:e,sourceGroupID:t.sourceGroupID,error:String(r)})}return{forwarded:D,skipped:I}}
|
package/lib/sync/handlers.d.ts
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
import type { KubunDB } from '@kubun/db';
|
|
2
2
|
import type { DefaultAccessLevel, GraphInternals } from '@kubun/engine';
|
|
3
3
|
import type { Logger } from '@kubun/logger';
|
|
4
|
-
import type { SyncDirection } from '../protocol.js';
|
|
5
4
|
import type { StoreUnreadableMode } from '../types.js';
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*
|
|
10
|
-
* @param viewerDID - The DID of the requesting viewer
|
|
11
|
-
* @param ownerDID - The DID of the document owner
|
|
12
|
-
* @param delegationTokens - Array of delegation JWT tokens
|
|
13
|
-
* @returns true if any token grants read access
|
|
14
|
-
*/
|
|
15
|
-
export declare function checkSyncDelegation(viewerDID: string, ownerDID: string, delegationTokens: Array<string>): Promise<boolean>;
|
|
5
|
+
import { checkSyncDelegation } from './authorize.js';
|
|
6
|
+
import { type PeerRegistry } from './peer-registry.js';
|
|
7
|
+
export { checkSyncDelegation };
|
|
16
8
|
export type CreateSyncHandlersParams = {
|
|
17
9
|
db: KubunDB;
|
|
18
10
|
graph: GraphInternals;
|
|
@@ -31,13 +23,6 @@ export type CreateSyncHandlersParams = {
|
|
|
31
23
|
/** Server default access level — required when `storeUnreadable === 'drop'`. */
|
|
32
24
|
defaultAccessLevel?: DefaultAccessLevel;
|
|
33
25
|
};
|
|
34
|
-
/**
|
|
35
|
-
* Resolve the effective sync direction given what the initiator requested
|
|
36
|
-
* and what the responder's per-peer policy allows.
|
|
37
|
-
* If allowed is 'both' (or unset), the request passes through.
|
|
38
|
-
* Otherwise the responder's policy wins.
|
|
39
|
-
*/
|
|
40
|
-
export declare function negotiateDirection(requested: SyncDirection, allowed: SyncDirection): SyncDirection;
|
|
41
26
|
/**
|
|
42
27
|
* Create sync protocol handlers served by plugin-p2p's own Enkaku server.
|
|
43
28
|
*/
|
package/lib/sync/handlers.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{createRevocationChecker as e}from"@enkaku/capability";import{getGraphStore as t}from"@kubun/store-graph";import{createP2PRevocationBackend as r,getP2PStore as o}from"@kubun/store-p2p";import{authorizeScope as s,checkSyncDelegation as n}from"./authorize.js";import{resolveCatalogSyncScopes as l}from"./catalog-scope.js";import{SyncAccessDeniedError as a}from"./errors.js";import{createMerkleSyncChannelHandler as i}from"./merkle-channel.js";import{negotiateDirection as c}from"./peer-registry.js";export function createSyncHandlers(n){let{db:u,graph:d,logger:f,peerRegistry:m}=n,p=n.storeUnreadable??"persist",g=null;async function w(){return null==g&&(g=e(r(await o(u)))),g}return{"sync/negotiate":async e=>{let r,{scopes:o,delegationTokens:n,catalogIDs:i,knownModelIDs:d,direction:p}=e.param,g=e.message.payload;if(null!=g.sub&&g.sub!==g.iss)throw new a("forged-subject","Sync access denied: signed payload subject does not match issuer");let D=g.iss;f.debug("sync/negotiate requested",{scopes:o,catalogIDs:i,viewerDID:D});let h=[],y=await t(u);if(null!=i&&i.length>0){let e=await l(u,i,d);if(e.modelIDs.length>0)if(null!=e.owners)for(let t of e.modelIDs)for(let r of e.owners)await s({viewerDID:D,ownerDID:r,delegationTokens:n,revocationChecker:await w()})&&h.push({modelID:t,ownerDID:r});else for(let t of e.modelIDs)for(let e of(await y.getDistinctOwnersForModel(t)))await s({viewerDID:D,ownerDID:e,delegationTokens:n,revocationChecker:await w()})&&h.push({modelID:t,ownerDID:e});e.missingClusterIDs.length>0&&(r=await y.getClusters(e.missingClusterIDs))}if(null!=o)for(let e of o)await s({viewerDID:D,ownerDID:e.ownerDID,delegationTokens:n,revocationChecker:await w()})?h.push(e):f.debug("sync/negotiate: scope rejected",{scope:e,viewerDID:D});let I=new Set,b=h.filter(e=>{let t=`${e.modelID}:${e.ownerDID}`;return!I.has(t)&&(I.add(t),!0)});f.info("sync/negotiate completed",{requested:(o?.length??0)+(i?.length??0),accepted:b.length,missingClusters:null!=r?Object.keys(r).length:0});let k=p??"pull",j=await m.getPeer({peerDID:D,stores:u}),C=j?.allowedDirection??"both",v=c(k,C);return v!==k&&f.info("sync/negotiate: direction downgraded",{requested:k,allowed:C,agreed:v}),{direction:v,acceptedScopes:b,excludedDocumentIDs:[],...null!=r&&{missingClusters:r}}},"sync/merkle-sync":i({db:u,graph:d,logger:f,peerRegistry:m,selfDID:n.selfDID,storeUnreadable:p,defaultAccessLevel:n.defaultAccessLevel,getRevocationChecker:w})}}export{n as checkSyncDelegation};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ObservabilityEventListener } from '@enkaku/hub-tunnel';
|
|
2
2
|
import { type ProcedureHandlers } from '@enkaku/server';
|
|
3
|
+
import type { OwnIdentity } from '@enkaku/token';
|
|
3
4
|
import type { GroupHandleRegistry } from '../groups/group-handle-registry.js';
|
|
4
5
|
import type { HubConnection } from '../hub/hub-connection.js';
|
|
5
6
|
import type { SyncProtocol } from '../protocol.js';
|
|
@@ -9,6 +10,7 @@ export type HubTunnelSyncListenerParams = {
|
|
|
9
10
|
groupID: string;
|
|
10
11
|
localDID: string;
|
|
11
12
|
peerDID: string;
|
|
13
|
+
identity: OwnIdentity;
|
|
12
14
|
syncHandlers: ProcedureHandlers<SyncProtocol>;
|
|
13
15
|
getRandomID?: () => string;
|
|
14
16
|
idleTimeoutMs?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createEncryptedHubTunnelTransport as t}from"@enkaku/hub-tunnel";import{Server as e}from"@enkaku/server";import{MLSEncryptor as s}from"../groups/mls-encryptor.js";export class HubTunnelSyncListener{#t;#e;#s;#n;#i;#r;#o;#c;#a;#h;#p;#
|
|
1
|
+
import{createEncryptedHubTunnelTransport as t}from"@enkaku/hub-tunnel";import{Server as e}from"@enkaku/server";import{MLSEncryptor as s}from"../groups/mls-encryptor.js";export class HubTunnelSyncListener{#t;#e;#s;#n;#i;#r;#o;#c;#a;#h;#u;#p;#l=!1;#d=!1;#y;#D;#m;constructor(t){this.#t=t.hubConnection,this.#e=t.registry,this.#s=t.groupID,this.#n=t.localDID,this.#i=t.peerDID,this.#r=t.identity,this.#o=t.syncHandlers,this.#c=t.getRandomID,this.#a=t.idleTimeoutMs,this.#h=t.reconnectTimeoutMs,this.#u=t.inboxCapacity,this.#p=t.onEvent}start(){this.#l||this.#d||(this.#l=!0,this.#m=new s({registry:this.#e,groupID:this.#s}),this.#D=this.#t.tunnelMessageStream({peerDID:this.#i}),this.#I())}async stop(){if(this.#d)return;this.#d=!0;let t=this.#y;if(this.#y=void 0,null!=t){try{await t.transport.dispose()}catch{}try{await t.server.dispose()}catch{}}let e=this.#D;if(this.#D=void 0,null!=e)try{e.return()}catch{}this.#m=void 0}#I(){let s;if(this.#d)return;let n=this.#m;if(null==n)throw Error("HubTunnelSyncListener: encryptor not initialized; call start() first");let i=t({hub:this.#b(),encryptor:n,groupID:this.#s,sessionID:{auto:!0},localDID:this.#n,peerDID:this.#i,idleTimeoutMs:this.#a,reconnectTimeoutMs:this.#h,inboxCapacity:this.#u,onEvent:this.#p,onSessionEnd:()=>{s?.dispose().catch(()=>{})}});s=i;let r=new e({getRandomID:this.#c,handlers:this.#o,identity:this.#r,accessRules:{"sync/*":{allow:!0}},transports:[i]}),o={transport:i,server:r};this.#y=o,i.events.on("disposed",()=>{this.#y===o&&(this.#y=void 0),r.dispose().catch(()=>{}),this.#I()})}#b(){let t=this.#t,e=this.#D;if(null==e)throw Error("HubTunnelSyncListener: stream not initialized; call start() first");return{send:async e=>t.send({recipients:e.recipients,payload:e.payload}),receive:t=>e,events:{subscribe:e=>t.events.on("lifecycle",t=>{let s=function(t){switch(t.type){case"connected":return{type:"connected"};case"disconnected":return{type:"disconnected"};case"reconnecting":return{type:"reconnecting"};case"error":return}}(t);void 0!==s&&e(s)})}}}}
|
package/lib/sync/merkle-apply.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{asType as t,createValidator as e}from"@enkaku/schema";import{verifyToken as a}from"@enkaku/token";import{computeMutationHash as i}from"@kubun/engine";import{DocumentID as o}from"@kubun/id";import{documentMutation as n}from"@kubun/protocol";import{getGraphStore as r}from"@kubun/store-graph";import{getP2PStore as u}from"@kubun/store-p2p";import{createReceiveAccessGate as s}from"./receive-access-gate.js";let l=e(n);export async function applySyncMutations(e){let n,{db:c,graph:
|
|
1
|
+
import{asType as t,createValidator as e}from"@enkaku/schema";import{verifyToken as a}from"@enkaku/token";import{computeMutationHash as i}from"@kubun/engine";import{DocumentID as o}from"@kubun/id";import{documentMutation as n}from"@kubun/protocol";import{getGraphStore as r}from"@kubun/store-graph";import{getP2PStore as u}from"@kubun/store-p2p";import{createReceiveAccessGate as s}from"./receive-access-gate.js";let l=e(n);export async function applySyncMutations(e){let n,{db:c,graph:d,mutationJWTs:p}=e,f=await r(c);if("drop"===(e.storeUnreadable??"persist")){if(null==e.selfDID)throw Error("applySyncMutations: 'storeUnreadable: drop' requires selfDID");let t=await u(c);n=s({selfDID:e.selfDID,db:{getUserModelAccessDefault:(t,e,a)=>f.getUserModelAccessDefault(t,e,a),isMemberOfAnyCircle:(e,a)=>t.isMemberOfAnyCircle(e,a),isMemberOfAnyGroup:(e,a)=>t.isMemberOfAnyGroup(e,a),getModelInterfaces:t=>f.getModelInterfaces(t)},defaultAccessLevel:e.defaultAccessLevel??{read:"only_owner",write:"only_owner"}})}let m=0,h=0,y=0,w=0,g=0;for(let e of p){let r,u=i(e);if(await f.hasMutationHash(u)){w++;continue}try{let i=await a(e);r=t(l,i.payload)}catch{h++;continue}let s=r.sub,c=o.fromString(s).model.toString(),p=o.fromString(s);if(null==await f.getDocument(p)&&"change"===r.typ){await f.insertMutationLogEntry({mutation_hash:u,model_id:c,document_id:s,author_did:r.iss,hlc:r.hlc,mutation_jwt:e,status:"pending"}),y++;continue}try{if((await d.applyVerifiedMutation({token:e,origin:"peer",...null!=n?{accessGate:n}:{}})).dropped){g++;continue}if(m++,"set"===r.typ)for(let t of(await f.getPendingMutations(s)))try{if((await d.applyVerifiedMutation({token:t.mutation_jwt,origin:"peer",...null!=n?{accessGate:n}:{}})).dropped){await f.updateMutationStatus(t.mutation_hash,"rejected"),g++;continue}await f.updateMutationStatus(t.mutation_hash,"applied"),m++}catch{await f.updateMutationStatus(t.mutation_hash,"rejected"),h++}}catch{await f.insertMutationLogEntry({mutation_hash:u,model_id:c,document_id:s,author_did:r.iss,hlc:r.hlc,mutation_jwt:e,status:"rejected"}),h++}}return{applied:m,rejected:h,pending:y,skipped:w,dropped:g}}
|
|
@@ -1,14 +1,20 @@
|
|
|
1
|
+
import type { VerifyTokenHook } from '@enkaku/capability';
|
|
2
|
+
import type { SignedPayload } from '@enkaku/token';
|
|
1
3
|
import type { KubunDB } from '@kubun/db';
|
|
2
4
|
import type { DefaultAccessLevel, GraphInternals } from '@kubun/engine';
|
|
3
5
|
import type { Logger } from '@kubun/logger';
|
|
4
6
|
import type { SyncMerkleSyncParams, SyncMerkleSyncReceive, SyncMerkleSyncResult, SyncMerkleSyncSend } from '../protocol.js';
|
|
5
7
|
import type { StoreUnreadableMode } from '../types.js';
|
|
8
|
+
import { type PeerRegistry } from './peer-registry.js';
|
|
6
9
|
/**
|
|
7
10
|
* Minimal handler context shape for sync/merkle-sync channel.
|
|
8
11
|
* Matches the Enkaku ChannelHandlerContext at runtime.
|
|
9
12
|
*/
|
|
10
13
|
type MerkleSyncChannelContext = {
|
|
11
14
|
param: SyncMerkleSyncParams;
|
|
15
|
+
message: {
|
|
16
|
+
payload: SignedPayload;
|
|
17
|
+
};
|
|
12
18
|
readable: ReadableStream<SyncMerkleSyncSend>;
|
|
13
19
|
writable: WritableStream<SyncMerkleSyncReceive>;
|
|
14
20
|
};
|
|
@@ -16,6 +22,13 @@ export type CreateMerkleSyncChannelHandlerParams = {
|
|
|
16
22
|
db: KubunDB;
|
|
17
23
|
graph: GraphInternals;
|
|
18
24
|
logger: Logger;
|
|
25
|
+
/**
|
|
26
|
+
* Per-peer policy registry. The requested direction is re-resolved against
|
|
27
|
+
* the peer's `allowedDirection` here: merkle-sync is the fail-closed backstop
|
|
28
|
+
* for a client that skips negotiate, so a disallowed direction is denied
|
|
29
|
+
* rather than downgraded.
|
|
30
|
+
*/
|
|
31
|
+
peerRegistry: PeerRegistry;
|
|
19
32
|
/**
|
|
20
33
|
* Local peer's DID — required when `storeUnreadable === 'drop'` so the
|
|
21
34
|
* receive-time access gate can use it as viewer.
|
|
@@ -28,6 +41,12 @@ export type CreateMerkleSyncChannelHandlerParams = {
|
|
|
28
41
|
storeUnreadable?: StoreUnreadableMode;
|
|
29
42
|
/** Server default access level — required when `storeUnreadable === 'drop'`. */
|
|
30
43
|
defaultAccessLevel?: DefaultAccessLevel;
|
|
44
|
+
/**
|
|
45
|
+
* Resolves the revocation checker used to authorize delegated scopes. Shared
|
|
46
|
+
* with the negotiate handler so the two access gates use the same revocation
|
|
47
|
+
* state and cannot drift.
|
|
48
|
+
*/
|
|
49
|
+
getRevocationChecker?: () => Promise<VerifyTokenHook>;
|
|
31
50
|
};
|
|
32
51
|
export declare function createMerkleSyncChannelHandler(params: CreateMerkleSyncChannelHandlerParams): (ctx: MerkleSyncChannelContext) => Promise<SyncMerkleSyncResult>;
|
|
33
52
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{getGraphStore as e}from"@kubun/store-graph";import{
|
|
1
|
+
import{getGraphStore as e}from"@kubun/store-graph";import{authorizeScope as t}from"./authorize.js";import{SyncAccessDeniedError as r}from"./errors.js";import{applySyncMutations as i}from"./merkle-apply.js";import{buildMerkleTree as n,findDivergentBuckets as a,getTimeBuckets as s,SYNC_BATCH_SIZE as o}from"./merkle-tree.js";import{negotiateDirection as l}from"./peer-registry.js";export function createMerkleSyncChannelHandler(c){let{db:d,graph:u,logger:m}=c,p=c.storeUnreadable??"persist";return async y=>{let{scopes:f,excludedDocumentIDs:w,tree:g,direction:h}=y.param,k=y.message?.payload;if(k?.iss==null)throw new r("unsigned","Sync access denied: missing verified signer");if(null!=k.sub&&k.sub!==k.iss)throw new r("forged-subject","Sync access denied: signed payload subject does not match issuer");let D=k.iss,b=null!=c.getRevocationChecker?await c.getRevocationChecker():void 0;for(let e of f)if(!await t({viewerDID:D,ownerDID:e.ownerDID,delegationTokens:y.param.delegationTokens??[],revocationChecker:b}))throw new r("scope-denied",`Sync access denied: caller not authorized for scope ${e.modelID}/${e.ownerDID}`);let v=h??"pull",j=await c.peerRegistry.getPeer({peerDID:D,stores:d});if(l(v,j?.allowedDirection??"both")!==v)throw new r("direction-denied",`Sync access denied: direction ${v} not permitted by peer policy`);let I=y.writable.getWriter();m.info("sync/merkle-sync channel started",{scopes:f,excludedDocumentIDs:w,direction:h});let S=0,T=0,R=0;try{let t=await e(d),r=await t.getDocumentIDsForScope(f,w),l=await t.getMutationLogForDocuments(r),k=n(l);await I.write({type:"tree",tree:k.buckets});let D={root:g.root??"",buckets:g},b=a(k,D);S=b.length;let v=async()=>{if("pull"===h||"both"===h){let e=new Set(b),t=l.filter(t=>{let{minute:r}=s(t.hlc);return e.has(r)});for(let e=0;e<t.length;e+=o){let r=t.slice(e,e+o);await I.write({type:"mutations",mutationJWTs:r.map(e=>e.mutation_jwt)}),T+=r.length}await I.write({type:"complete"})}else await I.write({type:"complete"})},j=async()=>{let e=[],t=y.readable.getReader();try{for(;;){let{done:r,value:i}=await t.read();if(r)break;if("mutations"===i.type&&null!=i.mutationJWTs)e.push(...i.mutationJWTs);else if("complete"===i.type)break}}finally{t.releaseLock()}return e},[,W]=await Promise.all([v(),j()]);if(W.length>0){let e=await i({db:d,graph:u,mutationJWTs:W,selfDID:c.selfDID,storeUnreadable:p,defaultAccessLevel:c.defaultAccessLevel});R=e.applied+e.pending,m.info("sync/merkle-sync: applied received mutations",{result:e})}}catch(e){return m.error("sync/merkle-sync channel error",{error:e}),{success:!1,divergentBuckets:0,mutationsSent:0,mutationsReceived:0}}finally{try{await I.close()}catch{}}return m.info("sync/merkle-sync channel completed",{divergentBuckets:S,mutationsSent:T,mutationsReceived:R}),{success:!0,divergentBuckets:S,mutationsSent:T,mutationsReceived:R}}}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type { Runtime } from '@enkaku/runtime';
|
|
2
2
|
import type { StoreProvider } from '@kubun/db';
|
|
3
3
|
import type { SyncDirection } from '../protocol.js';
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the effective sync direction given what the initiator requested
|
|
6
|
+
* and what the responder's per-peer policy allows.
|
|
7
|
+
* If allowed is 'both' (or unset), the request passes through.
|
|
8
|
+
* Otherwise the responder's policy wins.
|
|
9
|
+
*/
|
|
10
|
+
export declare function negotiateDirection(requested: SyncDirection, allowed: SyncDirection): SyncDirection;
|
|
4
11
|
export type PeerConfig = {
|
|
5
12
|
peerDID: string;
|
|
6
13
|
endpoint: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{getP2PStore as e}from"@kubun/store-p2p";function t(e){var t,r;let
|
|
1
|
+
import{getP2PStore as e}from"@kubun/store-p2p";export function negotiateDirection(e,t){return"both"===t?e:"both"===e?t:e===t?e:t}function t(e){var t,r;let o="string"==typeof(t=e.config)?JSON.parse(t):t??{};return{id:e.id,peerDID:e.peer_did,endpoint:e.endpoint,mode:e.mode,allowedUsers:"string"==typeof(r=e.allowed_users)?JSON.parse(r):r,priority:e.priority,trustLevel:e.trust_level,allowedDirection:o.allowedDirection,createdAt:e.created_at,updatedAt:e.updated_at}}export class PeerRegistry{#e;constructor(e){this.#e=e.runtime}async addPeer(t){let r=await e(t.stores);await r.addSyncPeer({id:this.#e.getRandomID(),peer_did:t.config.peerDID,endpoint:t.config.endpoint,mode:t.config.mode,allowed_users:t.config.allowedUsers,priority:t.config.priority,trust_level:t.config.trustLevel,config:{allowedDirection:t.config.allowedDirection},created_at:Date.now(),updated_at:Date.now()})}async getPeer(r){let o=await e(r.stores),i=await o.getSyncPeer(r.peerDID);if(i)return t(i)}async listPeers(r){let o=await e(r.stores);return(await o.listSyncPeers()).map(e=>t(e))}async updatePeer(t){let r=await this.getPeer({peerDID:t.peerDID,stores:t.stores});if(!r)throw Error(`Peer ${t.peerDID} not found`);let o={...r,...t.updates},i=await e(t.stores);await i.updateSyncPeer(t.peerDID,{endpoint:o.endpoint,mode:o.mode,allowed_users:o.allowedUsers,priority:o.priority,trust_level:o.trustLevel,config:{allowedDirection:o.allowedDirection}})}async removePeer(t){let r=await e(t.stores);await r.removeSyncPeer(t.peerDID)}async isPeerAllowed(e){return void 0!==await this.getPeer({peerDID:e.peerDID,stores:e.stores})}}
|
|
@@ -62,6 +62,8 @@ export declare class SyncClient {
|
|
|
62
62
|
hlc: string;
|
|
63
63
|
mutation_jwt: string;
|
|
64
64
|
}>;
|
|
65
|
+
/** Delegation JWTs authorizing the caller for non-owned scopes. */
|
|
66
|
+
delegationTokens?: Array<string>;
|
|
65
67
|
}): Promise<{
|
|
66
68
|
mutationJWTs: Array<string>;
|
|
67
69
|
divergentBuckets: number;
|
package/lib/sync/sync-client.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Client as e}from"@enkaku/client";import{ClientTransport as t}from"@enkaku/http-client-transport";import{findDivergentBuckets as r,getTimeBuckets as n,SYNC_BATCH_SIZE as i}from"./merkle-tree.js";export class SyncClient{#e;#t;#r;#n;constructor(e){this.#e=e.runtime,this.#t=e.identity,this.#r=e.logger,this.#n=e.serverResolver}async connect(e,t){if(e.startsWith("direct://"))return this.#i(e);if(e.startsWith("http://")||e.startsWith("https://"))return this.#s(e,t);throw Error(`Unsupported endpoint scheme: ${e}`)}#s(r,n){let i=new t({url:r,fetch:this.#e.fetch});return new e({runtime:this.#e,identity:this.#t,logger:this.#r.getChild("sync-client"),serverID:n,transport:i})}#i(t){let r=t.replace("direct://","");if(!this.#n)throw Error("Server resolver not configured for direct transport");let n=this.#n(r);if(!n)throw Error(`Server not found: ${r}`);let i=n.createSyncTransport();return new e({runtime:this.#e,identity:this.#t,logger:this.#r.getChild("sync-client"),serverID:r,transport:i})}async negotiate(e,t,r=[],n,i){this.#r.debug("Negotiate: sending {scopeCount} scopes",{scopeCount:t.length});let s=await e.request("sync/negotiate",{param:{scopes:t,delegationTokens:r,knownModelIDs:n,direction:i}});return this.#r.debug("Negotiate: accepted {acceptedCount} scopes, excluded {excludedCount} docs",{acceptedCount:s.acceptedScopes.length,excludedCount:s.excludedDocumentIDs.length}),s}async merkleSync(e,t){let{scopes:s,excludedDocumentIDs:o,localTree:c,direction:l,localEntries:a}=t;this.#r.debug("MerkleSync: creating channel with {buckets} local buckets",{buckets:Object.keys(c.buckets).length});let
|
|
1
|
+
import{Client as e}from"@enkaku/client";import{ClientTransport as t}from"@enkaku/http-client-transport";import{findDivergentBuckets as r,getTimeBuckets as n,SYNC_BATCH_SIZE as i}from"./merkle-tree.js";export class SyncClient{#e;#t;#r;#n;constructor(e){this.#e=e.runtime,this.#t=e.identity,this.#r=e.logger,this.#n=e.serverResolver}async connect(e,t){if(e.startsWith("direct://"))return this.#i(e);if(e.startsWith("http://")||e.startsWith("https://"))return this.#s(e,t);throw Error(`Unsupported endpoint scheme: ${e}`)}#s(r,n){if(null==n||""===n)throw Error("HTTP sync transport requires a serverID (peer DID) to authenticate; without it the server rejects all requests");let i=new t({url:r,fetch:this.#e.fetch});return new e({runtime:this.#e,identity:this.#t,logger:this.#r.getChild("sync-client"),serverID:n,transport:i})}#i(t){let r=t.replace("direct://","");if(!this.#n)throw Error("Server resolver not configured for direct transport");let n=this.#n(r);if(!n)throw Error(`Server not found: ${r}`);let i=n.createSyncTransport();return new e({runtime:this.#e,identity:this.#t,logger:this.#r.getChild("sync-client"),serverID:r,transport:i})}async negotiate(e,t,r=[],n,i){this.#r.debug("Negotiate: sending {scopeCount} scopes",{scopeCount:t.length});let s=await e.request("sync/negotiate",{param:{scopes:t,delegationTokens:r,knownModelIDs:n,direction:i}});return this.#r.debug("Negotiate: accepted {acceptedCount} scopes, excluded {excludedCount} docs",{acceptedCount:s.acceptedScopes.length,excludedCount:s.excludedDocumentIDs.length}),s}async merkleSync(e,t){let{scopes:s,excludedDocumentIDs:o,localTree:c,direction:l,localEntries:a,delegationTokens:u}=t;this.#r.debug("MerkleSync: creating channel with {buckets} local buckets",{buckets:Object.keys(c.buckets).length});let h=e.createChannel("sync/merkle-sync",{param:{scopes:s,excludedDocumentIDs:o,tree:c.buckets,direction:l,...null!=u&&{delegationTokens:u}}}),d=h.readable.getReader(),g=await d.read();if(g.done)throw Error("MerkleSync: channel closed before receiving remote tree");let p=g.value;if("tree"!==p.type)throw Error(`MerkleSync: expected 'tree' message, got '${p.type}'`);let m=r(c,{root:p.tree?.root??"",buckets:p.tree??{}}),y=m.length;this.#r.debug("MerkleSync: found {count} divergent buckets",{count:y});let f=0,k=async()=>{if("push"===l||"both"===l){let e=new Set(m),t=a.filter(t=>{let{minute:r}=n(t.hlc);return e.has(r)});for(let e=0;e<t.length;e+=i){let r=t.slice(e,e+i);await h.send({type:"mutations",mutationJWTs:r.map(e=>e.mutation_jwt)}),f+=r.length}}await h.send({type:"complete"})},v=async()=>{let e=[];for(;;){let{done:t,value:r}=await d.read();if(t)break;if("mutations"===r.type&&null!=r.mutationJWTs)e.push(...r.mutationJWTs);else if("complete"===r.type)break}return d.releaseLock(),e},[,w]=await Promise.all([k(),v()]),b=await h;if(null!=b&&!b.success)throw Error("MerkleSync: responder reported failure");return this.#r.debug("MerkleSync: completed with {received} received, {sent} sent",{received:w.length,sent:f}),{mutationJWTs:w,divergentBuckets:y,mutationsSent:f}}}
|
package/lib/sync/sync-manager.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{EventEmitter as e}from"@enkaku/event";import{isSigningIdentity as t}from"@enkaku/token";import{getGraphStore as s}from"@kubun/store-graph";import{applySyncMutations as r}from"./merkle-apply.js";import{buildMerkleTree as i}from"./merkle-tree.js";import{PeerRegistry as n}from"./peer-registry.js";import{SyncClient as o}from"./sync-client.js";export class SyncManager{#e;#t;#s;#r;#i;#n=new e;#o=new Map;#a=new Map;#l;#c;#g;#p;constructor(e){this.#e=e.deployClusters,this.#t=e.graph,this.#s=e.runtime,this.#l=e.identity,this.#r=e.logger,this.#i=new n({runtime:e.runtime}),this.#c=e.serverResolver,this.#g=e.storeUnreadable??"persist",this.#p=e.defaultAccessLevel}get peerRegistry(){return this.#i}setIdentity(e){this.#l=e}setServerResolver(e){this.#c=e}async addPeer(e){await this.#i.addPeer(e),this.#r.info("Peer added",{peerDID:e.config.peerDID})}async removePeer(e){await this.#i.removePeer(e),this.#r.info("Peer removed",{peerDID:e.peerDID})}async updatePeerConfig(e){await this.#i.updatePeer(e),this.#r.info("Peer updated",{peerDID:e.peerDID})}async listPeers(e){return this.#i.listPeers(e)}async getPeer(e){return this.#i.getPeer(e)}async merkleSyncWithPeer(e){let n,{peerDID:a,scopes:l,delegationTokens:c=[],knownModelIDs:g,direction:p}=e;this.#r.info("Starting Merkle sync with peer",{peerDID:a,scopes:l});let m=await this.#i.getPeer({peerDID:a,stores:e.stores});if(!m)throw Error(`Peer ${a} not found`);let d=`merkle-sync-${a}-${Date.now()}`,h={peerID:a,startTime:Date.now(),documentsAttempted:0,documentsCompleted:0};this.#o.set(d,h),this.#m({type:"started",peerID:a,timestamp:Date.now()});try{let u=this.#l;if(!t(u))throw Error("Signing identity required for Merkle sync");let y=new o({runtime:this.#s,identity:u,logger:this.#r,serverResolver:this.#c});n=await y.connect(m.endpoint,a);let v=p??"pull";this.#r.info("Negotiating sync scopes",{scopes:l,direction:v});let{acceptedScopes:f,excludedDocumentIDs:D,missingClusters:w,direction:k}=await y.negotiate(n,l,c,g,v);if(0===f.length)return this.#r.info("No scopes accepted by peer"),this.#a.set(a,Date.now()),this.#m({type:"completed",peerID:a,timestamp:Date.now()}),this.#o.delete(d),{sessionID:d,divergentBuckets:0,messagesReceived:0,messagesSent:0,missingClusters:w};null!=w&&Object.keys(w).length>0&&(null!=this.#e?(this.#r.info("Deploying missing clusters from peer",{clusterCount:Object.keys(w).length}),await this.#e(w)):this.#r.warn("Missing clusters received but no deployClusters callback configured",{clusterCount:Object.keys(w).length})),this.#r.info("Building local Merkle tree",{acceptedScopes:f});let S=await s(e.stores),P=await S.getDocumentIDsForScope(f,D),R=await S.getMutationLogForDocuments(P),I=i(R),b=k??v;this.#r.info("Requesting Merkle sync from peer",{localTreeBuckets:Object.keys(I.buckets).length,direction:b});let C=await y.merkleSync(n,{scopes:f,excludedDocumentIDs:D,localTree:I,direction:b,localEntries:R});return C.mutationJWTs.length>0&&(this.#r.info("Applying sync mutations",{mutations:C.mutationJWTs.length}),h.documentsCompleted=(await r({db:e.stores,graph:this.#t,mutationJWTs:C.mutationJWTs,selfDID:this.#l.id,storeUnreadable:this.#g,defaultAccessLevel:this.#p})).applied),this.#a.set(a,Date.now()),this.#m({type:"completed",peerID:a,timestamp:Date.now()}),this.#r.info("Merkle sync completed",{divergentBuckets:C.divergentBuckets,mutationsReceived:C.mutationJWTs.length}),{sessionID:d,divergentBuckets:C.divergentBuckets,messagesReceived:C.mutationJWTs.length,messagesSent:C.mutationsSent,missingClusters:w}}catch(e){throw this.#r.error("Merkle sync failed",{peerDID:a,error:e}),this.#m({type:"error",peerID:a,timestamp:Date.now(),error:e instanceof Error?e.message:String(e)}),e}finally{if(null!=n)try{n.abort("SyncComplete"),await n.dispose()}catch{}this.#o.delete(d)}}getStatus(e){let{peerDID:t}=e,s=Array.from(this.#o.values()).filter(e=>!t||e.peerID===t).map(e=>({peerID:e.peerID,startTime:e.startTime,documentsAttempted:e.documentsAttempted,documentsCompleted:e.documentsCompleted})),r={};for(let[e,s]of this.#a.entries())t&&e!==t||(r[e]=s);return{activeSessions:s,lastSyncByPeer:r}}onSyncEvent(e){return this.#n.on("sync",e)}#m(e){this.#n.emit("sync",e).catch(e=>{this.#r.error("Error in sync event listener",{error:e})})}async dispose(){this.#o.clear(),this.#a.clear()}}
|
|
1
|
+
import{EventEmitter as e}from"@enkaku/event";import{isSigningIdentity as t}from"@enkaku/token";import{getGraphStore as s}from"@kubun/store-graph";import{applySyncMutations as r}from"./merkle-apply.js";import{buildMerkleTree as i}from"./merkle-tree.js";import{PeerRegistry as n}from"./peer-registry.js";import{SyncClient as o}from"./sync-client.js";export class SyncManager{#e;#t;#s;#r;#i;#n=new e;#o=new Map;#a=new Map;#l;#c;#g;#p;constructor(e){this.#e=e.deployClusters,this.#t=e.graph,this.#s=e.runtime,this.#l=e.identity,this.#r=e.logger,this.#i=new n({runtime:e.runtime}),this.#c=e.serverResolver,this.#g=e.storeUnreadable??"persist",this.#p=e.defaultAccessLevel}get peerRegistry(){return this.#i}setIdentity(e){this.#l=e}setServerResolver(e){this.#c=e}async addPeer(e){await this.#i.addPeer(e),this.#r.info("Peer added",{peerDID:e.config.peerDID})}async removePeer(e){await this.#i.removePeer(e),this.#r.info("Peer removed",{peerDID:e.peerDID})}async updatePeerConfig(e){await this.#i.updatePeer(e),this.#r.info("Peer updated",{peerDID:e.peerDID})}async listPeers(e){return this.#i.listPeers(e)}async getPeer(e){return this.#i.getPeer(e)}async merkleSyncWithPeer(e){let n,{peerDID:a,scopes:l,delegationTokens:c=[],knownModelIDs:g,direction:p}=e;this.#r.info("Starting Merkle sync with peer",{peerDID:a,scopes:l});let m=await this.#i.getPeer({peerDID:a,stores:e.stores});if(!m)throw Error(`Peer ${a} not found`);let d=`merkle-sync-${a}-${Date.now()}`,h={peerID:a,startTime:Date.now(),documentsAttempted:0,documentsCompleted:0};this.#o.set(d,h),this.#m({type:"started",peerID:a,timestamp:Date.now()});try{let u=this.#l;if(!t(u))throw Error("Signing identity required for Merkle sync");let y=new o({runtime:this.#s,identity:u,logger:this.#r,serverResolver:this.#c});n=await y.connect(m.endpoint,a);let v=p??"pull";this.#r.info("Negotiating sync scopes",{scopes:l,direction:v});let{acceptedScopes:f,excludedDocumentIDs:D,missingClusters:w,direction:k}=await y.negotiate(n,l,c,g,v);if(0===f.length)return this.#r.info("No scopes accepted by peer"),this.#a.set(a,Date.now()),this.#m({type:"completed",peerID:a,timestamp:Date.now()}),this.#o.delete(d),{sessionID:d,divergentBuckets:0,messagesReceived:0,messagesSent:0,missingClusters:w};null!=w&&Object.keys(w).length>0&&(null!=this.#e?(this.#r.info("Deploying missing clusters from peer",{clusterCount:Object.keys(w).length}),await this.#e(w)):this.#r.warn("Missing clusters received but no deployClusters callback configured",{clusterCount:Object.keys(w).length})),this.#r.info("Building local Merkle tree",{acceptedScopes:f});let S=await s(e.stores),P=await S.getDocumentIDsForScope(f,D),R=await S.getMutationLogForDocuments(P),I=i(R),b=k??v;this.#r.info("Requesting Merkle sync from peer",{localTreeBuckets:Object.keys(I.buckets).length,direction:b});let C=await y.merkleSync(n,{scopes:f,excludedDocumentIDs:D,localTree:I,direction:b,localEntries:R,delegationTokens:c});return C.mutationJWTs.length>0&&(this.#r.info("Applying sync mutations",{mutations:C.mutationJWTs.length}),h.documentsCompleted=(await r({db:e.stores,graph:this.#t,mutationJWTs:C.mutationJWTs,selfDID:this.#l.id,storeUnreadable:this.#g,defaultAccessLevel:this.#p})).applied),this.#a.set(a,Date.now()),this.#m({type:"completed",peerID:a,timestamp:Date.now()}),this.#r.info("Merkle sync completed",{divergentBuckets:C.divergentBuckets,mutationsReceived:C.mutationJWTs.length}),{sessionID:d,divergentBuckets:C.divergentBuckets,messagesReceived:C.mutationJWTs.length,messagesSent:C.mutationsSent,missingClusters:w}}catch(e){throw this.#r.error("Merkle sync failed",{peerDID:a,error:e}),this.#m({type:"error",peerID:a,timestamp:Date.now(),error:e instanceof Error?e.message:String(e)}),e}finally{if(null!=n)try{n.abort("SyncComplete"),await n.dispose()}catch{}this.#o.delete(d)}}getStatus(e){let{peerDID:t}=e,s=Array.from(this.#o.values()).filter(e=>!t||e.peerID===t).map(e=>({peerID:e.peerID,startTime:e.startTime,documentsAttempted:e.documentsAttempted,documentsCompleted:e.documentsCompleted})),r={};for(let[e,s]of this.#a.entries())t&&e!==t||(r[e]=s);return{activeSessions:s,lastSyncByPeer:r}}onSyncEvent(e){return this.#n.on("sync",e)}#m(e){this.#n.emit("sync",e).catch(e=>{this.#r.error("Error in sync event listener",{error:e})})}async dispose(){this.#o.clear(),this.#a.clear()}}
|