@kubun/plugin-p2p 0.10.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.
Files changed (64) hide show
  1. package/lib/context/group.js +1 -1
  2. package/lib/context/hub.js +1 -1
  3. package/lib/context/types.d.ts +9 -0
  4. package/lib/groups/broadcast-service.d.ts +84 -10
  5. package/lib/groups/broadcast-service.js +1 -1
  6. package/lib/groups/broadcast.d.ts +70 -0
  7. package/lib/groups/broadcast.js +1 -1
  8. package/lib/groups/events.d.ts +2 -1
  9. package/lib/groups/group-handle-registry.d.ts +8 -0
  10. package/lib/groups/group-handle-registry.js +1 -1
  11. package/lib/groups/group-health-monitor.d.ts +45 -0
  12. package/lib/groups/group-health-monitor.js +1 -0
  13. package/lib/groups/manager.d.ts +9 -0
  14. package/lib/groups/manager.js +1 -1
  15. package/lib/groups/rejoin-codec.d.ts +14 -0
  16. package/lib/groups/rejoin-codec.js +1 -0
  17. package/lib/groups/wire-frame.d.ts +2 -0
  18. package/lib/groups/wire-frame.js +1 -1
  19. package/lib/hub/circle-catchup-requester.d.ts +58 -0
  20. package/lib/hub/circle-catchup-requester.js +1 -0
  21. package/lib/hub/circle-catchup-responder.d.ts +50 -0
  22. package/lib/hub/circle-catchup-responder.js +1 -0
  23. package/lib/hub/epoch-stale-detector.d.ts +18 -0
  24. package/lib/hub/epoch-stale-detector.js +1 -0
  25. package/lib/hub/errors.d.ts +7 -5
  26. package/lib/hub/errors.js +1 -1
  27. package/lib/hub/group-channel.d.ts +22 -0
  28. package/lib/hub/group-channel.js +1 -1
  29. package/lib/hub/manager.d.ts +5 -4
  30. package/lib/hub/manager.js +1 -1
  31. package/lib/hub/receive-handler.d.ts +20 -2
  32. package/lib/hub/receive-handler.js +1 -1
  33. package/lib/hub/rejoin-manager.d.ts +78 -0
  34. package/lib/hub/rejoin-manager.js +1 -0
  35. package/lib/hub/rejoin-responder.d.ts +32 -0
  36. package/lib/hub/rejoin-responder.js +1 -0
  37. package/lib/hub/relay-manager.d.ts +16 -1
  38. package/lib/hub/relay-manager.js +1 -1
  39. package/lib/hub/wiring.d.ts +19 -1
  40. package/lib/hub/wiring.js +1 -1
  41. package/lib/index.d.ts +18 -3
  42. package/lib/index.js +1 -1
  43. package/lib/protocol.d.ts +7 -0
  44. package/lib/protocol.js +1 -1
  45. package/lib/schema.js +34 -2
  46. package/lib/sync/authorize.d.ts +25 -0
  47. package/lib/sync/authorize.js +1 -0
  48. package/lib/sync/errors.d.ts +11 -0
  49. package/lib/sync/errors.js +1 -0
  50. package/lib/sync/handlers.d.ts +3 -22
  51. package/lib/sync/handlers.js +1 -1
  52. package/lib/sync/hub-tunnel-sync-listener.d.ts +2 -0
  53. package/lib/sync/hub-tunnel-sync-listener.js +1 -1
  54. package/lib/sync/merkle-channel.d.ts +19 -0
  55. package/lib/sync/merkle-channel.js +1 -1
  56. package/lib/sync/peer-registry.d.ts +7 -0
  57. package/lib/sync/peer-registry.js +1 -1
  58. package/lib/sync/sync-client.d.ts +2 -0
  59. package/lib/sync/sync-client.js +1 -1
  60. package/lib/sync/sync-manager.js +1 -1
  61. package/lib/types.d.ts +30 -0
  62. package/lib/utils.d.ts +5 -0
  63. package/lib/utils.js +1 -0
  64. package/package.json +12 -12
package/lib/index.d.ts CHANGED
@@ -21,11 +21,12 @@ export { createP2PSchemaExtension } from './schema.js';
21
21
  export { type BroadcastBatchConfig, type BroadcastQueue, type BroadcastQueueParams, createBroadcastQueue, DEFAULT_BROADCAST_BATCH_CONFIG, } from './sync/broadcast-queue.js';
22
22
  export { type BroadcastSenderParams, DEFAULT_PUSH_SYNC_CONFIG, type PushSyncConfig, wireBroadcastSender, } from './sync/broadcast-sender.js';
23
23
  export { type CatalogSyncScope, resolveCatalogSyncScopes } from './sync/catalog-scope.js';
24
+ export { SyncAccessDeniedError } from './sync/errors.js';
24
25
  export { type EvaluateAndForwardParams, type EvaluateAndForwardResult, evaluateAndForward, type ForwardContext, type ForwardFilter, type ForwardingConfig, } from './sync/forwarder.js';
25
- export { checkSyncDelegation, negotiateDirection } from './sync/handlers.js';
26
+ export { checkSyncDelegation } from './sync/handlers.js';
26
27
  export { HubTunnelSyncListener, type HubTunnelSyncListenerParams, } from './sync/hub-tunnel-sync-listener.js';
27
28
  export { HubTunnelSyncProvider, type HubTunnelSyncProviderParams, } from './sync/hub-tunnel-sync-provider.js';
28
- export { type PeerConfig, type PeerConfigWithID, PeerRegistry } from './sync/peer-registry.js';
29
+ export { negotiateDirection, type PeerConfig, type PeerConfigWithID, PeerRegistry, } from './sync/peer-registry.js';
29
30
  export { SyncClient, type SyncClientParams, type SyncScope, type SyncTransportProvider, } from './sync/sync-client.js';
30
31
  export { type SyncEvent, SyncManager, type SyncManagerParams, type SyncSessionInfo, type SyncStatus, } from './sync/sync-manager.js';
31
32
  export type { OnServerDIDObserved } from './hub/connection-pool.js';
@@ -57,7 +58,21 @@ export type ReceiveConfig = {
57
58
  export declare const DEFAULT_RECEIVE_CONFIG: ReceiveConfig;
58
59
  export type P2PPluginOptions = {
59
60
  autoAcceptPeers?: Array<string>;
60
- http?: boolean | string;
61
+ /**
62
+ * Expose the sync protocol over the HTTP plugin transport.
63
+ * - `true` / `{}` — enable at the default protocol path `'sync'` with CORS
64
+ * open to all origins (`['*']`).
65
+ * - `'name'` / `'/name'` — enable at the given protocol path.
66
+ * - `{ path?, allowedOrigin? }` — `path` is the protocol path (default
67
+ * `'sync'`); `allowedOrigin` restricts CORS origins (default `['*']`).
68
+ * The default is open because the sync server authenticates every request,
69
+ * so CORS is not the security boundary; operators may still restrict it.
70
+ * - absent / `false` — HTTP sync transport disabled.
71
+ */
72
+ http?: boolean | string | {
73
+ path?: string;
74
+ allowedOrigin?: string | Array<string>;
75
+ };
61
76
  /**
62
77
  * Enable hub-mediated group broadcasts.
63
78
  * - `true` / `{}` — build a default HTTP hub client factory from
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{HLC as s}from"@kubun/hlc";import{p2pStoreDefinition as n}from"@kubun/store-p2p";import{createDelegationContext as a}from"./context/delegation.js";import{createGroupContext as i}from"./context/group.js";import{createHubContext as c}from"./context/hub.js";import{createJoinContext as p}from"./context/join.js";import{createSyncContext as d}from"./context/sync.js";import{createP2PEventEmitter as u}from"./groups/events.js";import{GroupHandleRegistry as l}from"./groups/group-handle-registry.js";import{GroupManager as g}from"./groups/manager.js";import{forwardRemoteBroadcast as m}from"./hub/forward-remote-broadcast.js";import{createHTTPHubClient as b}from"./hub/http-client.js";import{captureServerDID as y}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 S,wireBroadcastSender as w}from"./sync/broadcast-sender.js";import{createSyncHandlers as D}from"./sync/handlers.js";import{SyncManager as P}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{evaluateAndForward}from"./sync/forwarder.js";export{checkSyncDelegation,negotiateDirection}from"./sync/handlers.js";export{HubTunnelSyncListener}from"./sync/hub-tunnel-sync-listener.js";export{HubTunnelSyncProvider}from"./sync/hub-tunnel-sync-provider.js";export{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(I){return R=>{var E;let H,L,B,C;R.db.register(n);let G=R.db.adapter;if(!(t(E=R.identity)&&"privateKey"in E))throw Error("createP2PPlugin requires an OwnIdentity (full identity with privateKey) — received a narrower Identity");let A=R.identity,F=new s({nodeID:A.id}),k={...DEFAULT_RECEIVE_CONFIG,...I?.receive},T=I?.defaultAccessLevel??{read:"only_owner",write:"only_owner"},U=new l({stores:R.db,deviceID:A.id,logger:R.getLogger("group-handle-registry")}),_=new g({identity:A,getRandomID:R.runtime.getRandomID,registry:U}),O=u(),q=new P({deployClusters:async e=>{let r=Object.values(e);await R.engine.deployGraph({clusters:r})},graph:R.graph,runtime:R.runtime,identity:R.identity,logger:R.getLogger("sync"),storeUnreadable:k.storeUnreadable,defaultAccessLevel:T}),N=D({db:R.db,graph:R.graph,logger:R.getLogger("sync-handlers"),peerRegistry:q.peerRegistry,selfDID:A.id,storeUnreadable:k.storeUnreadable,defaultAccessLevel:T}),M=new Map,J=[],z=R.getLogger("hub-relay"),K=f,V={...S,...I?.pushSync};if(I?.hub){let e="object"==typeof I.hub&&null!=I.hub.createHubClient?I.hub.createHubClient:b({identity:A,fetch:R.runtime.fetch});K=h({identity:A,db:R.db,graph:R.graph,emitter:O,createHubClient:e,registry:U,onRemoteBroadcast:e=>{m(O,e,z).catch(e=>{z.error("forwardRemoteBroadcast failed",{error:e})})},logger:z,storeUnreadable:k.storeUnreadable,defaultAccessLevel:T,forwarding:I.forwarding,onServerDIDObserved:({hubURL:e,serverDID:r})=>y({stores:R.db,hubURL:e,serverDID:r}).then(()=>void 0)}),V.enabled&&(L=v({config:{...x,...I.batch},scheduleBroadcast:K.scheduleBroadcast,logger:R.getLogger("broadcast-queue")}),H=w({db:R.db,eventBus:R.eventBus,selfDID:A.id,queue:L,logger:R.getLogger("broadcast-sender")}))}let W={hubReady:K.ready,addPeer:e=>q.addPeer({config:e,stores:R.db}),removePeer:e=>q.removePeer({peerDID:e,stores:R.db}),listPeers:()=>q.listPeers({stores:R.db}),syncWithPeer:e=>q.merkleSyncWithPeer({...e,stores:R.db}),getStatus:e=>q.getStatus({peerDID:e,stores:R.db}),onSyncEvent:e=>q.onSyncEvent(e),createSyncTransport:function(e){let t=new o({signal:e}),s=new r({getRandomID:R.runtime.getRandomID,handlers:N,logger:R.getLogger("sync-server"),transports:[t.server],signal:e});return J.push(s),t.client}};if(I?.http){let t="string"==typeof I.http?I.http.replace(/^\//,""):"sync";W.syncReady=(async()=>{let o=await R.engine.getAPI("http");await o.listening,B=new e({allowedOrigin:["*"]}),C=new r({getRandomID:R.runtime.getRandomID,handlers:N,logger:R.getLogger("sync-http-server"),transports:[B]}),J.push(C),o.registerProtocol(t,B.fetch.bind(B))})()}return{name:"p2p",schemaExtension:e=>j(O),api:W,createContextFactory:()=>(e,r)=>{let t={identity:A,groupManager:_,registry:U,syncManager:q,stores:r,adapter:G,hlc:F,emitter:O,pendingJoinRequests:M,runtime:R.runtime,autoAcceptPeers:I?.autoAcceptPeers,scheduleBroadcast:K.scheduleBroadcast,broadcastNow:K.broadcastNow,scheduleSendCommit:K.scheduleSendCommit,logger:R.getLogger("context")};return{group:i(e,t),sync:d(e,t),join:p(e,t),hub:c(e,t),delegation:a(e,t)}},dispose:async()=>{if(null!=H)try{H()}catch{}if(null!=L)try{L.dispose()}catch{}let e=(async()=>{for(let e of J)await e.dispose();null!=B&&await B.dispose()})(),r=R.getLogger("dispose"),t=await Promise.allSettled([e,q.dispose(),K.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})})}}}}
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.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}async function*t(){}let o=`
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!
@@ -222,6 +222,36 @@ extend type Query {
222
222
  syncStatus: SyncStatus!
223
223
  ownDelegationTokens(grantor: ID, covers: DelegationTokenCovers): [DelegationToken!]!
224
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!
225
255
  }
226
256
 
227
257
  extend type Mutation {
@@ -251,6 +281,7 @@ extend type Mutation {
251
281
  deleteHub(id: ID!, force: Boolean): DeleteHubPayload!
252
282
  bindHubToGroup(hubID: ID!, groupID: ID!): Boolean!
253
283
  unbindHubFromGroup(hubID: ID!, groupID: ID!): Boolean!
284
+ rejoinGroup(groupID: ID!): RejoinGroupPayload!
254
285
  }
255
286
 
256
287
  extend type PeerGroup {
@@ -275,5 +306,6 @@ extend type Subscription {
275
306
  circleCatalogsChanged(circleID: ID!): PeerCircle!
276
307
  ownDelegationTokenAdded: DelegationToken!
277
308
  ownDelegationTokenRevoked: RevokedDelegationToken!
309
+ groupHealthChanged(groupID: ID): GroupHealthEvent!
278
310
  }
279
- `;export function createP2PSchemaExtension(i){return{sdl:o,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 i=r(o).sync.getStatus();return{...i,lastSyncByPeer:JSON.stringify(i.lastSyncByPeer)}},ownDelegationTokens:async(e,r,t)=>{let o=t.getViewer();if(null==o||null==t.p2p)return[];let i=r.grantor??null,a=r.covers??null;return await t.p2p.delegation.listOwnTokens({viewerDID:o,grantor:i,covers:a})},ownRevokedDelegationTokens:async(e,r,t)=>{let o=t.getViewer();if(null==o||null==t.p2p)return[];let i=r.grantor??null;return await t.p2p.delegation.listOwnRevokedTokens({viewerDID:o,grantor:i})}},mutationFields:{createGroup:async(e,t,o)=>{let i=t.input;return{group:await r(o).group.create(i)}},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 i=t.input;return await r(o).group.updateCatalog(i)},updateGroup:async(e,t,o)=>{let i=t.input;return{group:await r(o).group.update(i)}},updateCircle:async(e,t,o)=>{let i=t.input;return{circle:await r(o).group.updateCircle(i)}},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 i=t.input;return await r(o).hub.create(i)},updateHub:async(e,t,o)=>{let i=t.input;return await r(o).hub.update(i)},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})},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(i,"groupJoined")},groupLeft:{resolve:e=>e,subscribe:()=>e(i,"groupLeft")},groupDataChanged:{resolve:e=>e,subscribe:(r,t)=>e(i,"groupDataChanged",e=>e.groupID===t.groupID)},groupMemberJoined:{resolve:e=>e,subscribe:(r,t)=>e(i,"groupMemberJoined",e=>e.groupID===t.groupID)},groupMemberLeft:{resolve:e=>e,subscribe:(r,t)=>e(i,"groupMemberLeft",e=>e.groupID===t.groupID)},circleCreated:{resolve:e=>e,subscribe:(r,t)=>e(i,"circleCreated",e=>e.groupID===t.groupID)},circleDeleted:{resolve:e=>e,subscribe:(r,t)=>e(i,"circleDeleted",e=>e.groupID===t.groupID)},circleDataChanged:{resolve:e=>e,subscribe:(r,t)=>e(i,"circleDataChanged",e=>e.circleID===t.circleID)},circleMemberAdded:{resolve:e=>e,subscribe:(r,t)=>e(i,"circleMemberAdded",e=>e.circleID===t.circleID)},circleMemberRemoved:{resolve:e=>e,subscribe:(r,t)=>e(i,"circleMemberRemoved",e=>e.circleID===t.circleID)},circleCatalogsChanged:{resolve:e=>e,subscribe:(r,t)=>e(i,"circleCatalogsChanged",e=>e.circleID===t.circleID)},ownDelegationTokenAdded:{resolve:e=>e,subscribe:(r,o,a)=>{let n=a.getViewer();return null==n?t():e(i,"delegationTokenAdded",e=>e.audience===n)}},ownDelegationTokenRevoked:{resolve:e=>e,subscribe:(r,o,a)=>{let n=a.getViewer();return null==n?t():e(i,"delegationTokenRevoked",e=>e.audience===n)}}}}}}
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)}
@@ -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}}
@@ -1,22 +1,10 @@
1
- import { type VerifyTokenHook } from '@enkaku/capability';
2
1
  import type { KubunDB } from '@kubun/db';
3
2
  import type { DefaultAccessLevel, GraphInternals } from '@kubun/engine';
4
3
  import type { Logger } from '@kubun/logger';
5
- import type { SyncDirection } from '../protocol.js';
6
4
  import type { StoreUnreadableMode } from '../types.js';
7
- import type { PeerRegistry } from './peer-registry.js';
8
- /**
9
- * Check if delegation tokens grant the viewer read access to a user's documents.
10
- *
11
- * @param viewerDID - The DID of the requesting viewer
12
- * @param ownerDID - The DID of the document owner
13
- * @param delegationTokens - Array of delegation JWT tokens
14
- * @param revocationChecker - Optional per-leaf-capability verification hook.
15
- * When set, capabilities whose `jti` has been revoked cause the check to
16
- * fail closed.
17
- * @returns true if any token grants read access
18
- */
19
- export declare function checkSyncDelegation(viewerDID: string, ownerDID: string, delegationTokens: Array<string>, revocationChecker?: VerifyTokenHook): Promise<boolean>;
5
+ import { checkSyncDelegation } from './authorize.js';
6
+ import { type PeerRegistry } from './peer-registry.js';
7
+ export { checkSyncDelegation };
20
8
  export type CreateSyncHandlersParams = {
21
9
  db: KubunDB;
22
10
  graph: GraphInternals;
@@ -35,13 +23,6 @@ export type CreateSyncHandlersParams = {
35
23
  /** Server default access level — required when `storeUnreadable === 'drop'`. */
36
24
  defaultAccessLevel?: DefaultAccessLevel;
37
25
  };
38
- /**
39
- * Resolve the effective sync direction given what the initiator requested
40
- * and what the responder's per-peer policy allows.
41
- * If allowed is 'both' (or unset), the request passes through.
42
- * Otherwise the responder's policy wins.
43
- */
44
- export declare function negotiateDirection(requested: SyncDirection, allowed: SyncDirection): SyncDirection;
45
26
  /**
46
27
  * Create sync protocol handlers served by plugin-p2p's own Enkaku server.
47
28
  */
@@ -1 +1 @@
1
- import{checkCapability as e,createRevocationChecker as t}from"@enkaku/capability";import{getGraphStore as n}from"@kubun/store-graph";import{createP2PRevocationBackend as r,getP2PStore as o}from"@kubun/store-p2p";import{resolveCatalogSyncScopes as l}from"./catalog-scope.js";import{createMerkleSyncChannelHandler as a}from"./merkle-channel.js";export async function checkSyncDelegation(t,n,r,o){if(!r||0===r.length)return!1;let l=["*","urn:kubun:user:*",`urn:kubun:user:${n}`],a="document/read";for(let s of l)try{return await e({act:a,res:s},{iss:t,sub:n,cap:r},{verifyToken:o}),!0}catch{}for(let s of r)for(let r of l)try{return await e({act:a,res:r},{iss:t,sub:n,cap:s},{verifyToken:o}),!0}catch{}return!1}export function negotiateDirection(e,t){return"both"===t?e:"both"===e?t:e===t?e:t}export function createSyncHandlers(e){let{db:s,graph:i,logger:c,peerRegistry:u}=e,f=e.storeUnreadable??"persist",d=null;async function g(){return null==d&&(d=t(r(await o(s)))),d}return{"sync/negotiate":async e=>{let t,{scopes:r,delegationTokens:o,catalogIDs:a,knownModelIDs:i,direction:f}=e.param,d=e.message.payload,p=d.sub||d.iss;c.debug("sync/negotiate requested",{scopes:r,catalogIDs:a,viewerDID:p});let D=[],m=await n(s);if(null!=a&&a.length>0){let e=await l(s,a,i);if(e.modelIDs.length>0)if(null!=e.owners)for(let t of e.modelIDs)for(let n of e.owners){let e=p===n,r=!e&&await checkSyncDelegation(p,n,o,await g());(e||r)&&D.push({modelID:t,ownerDID:n})}else for(let t of e.modelIDs)for(let e of(await m.getDistinctOwnersForModel(t))){let n=p===e,r=!n&&await checkSyncDelegation(p,e,o,await g());(n||r)&&D.push({modelID:t,ownerDID:e})}e.missingClusterIDs.length>0&&(t=await m.getClusters(e.missingClusterIDs))}if(null!=r)for(let e of r){let t=p===e.ownerDID,n=!t&&await checkSyncDelegation(p,e.ownerDID,o,await g());t||n?D.push(e):c.debug("sync/negotiate: scope rejected",{scope:e,viewerDID:p})}let w=new Set,h=D.filter(e=>{let t=`${e.modelID}:${e.ownerDID}`;return!w.has(t)&&(w.add(t),!0)});c.info("sync/negotiate completed",{requested:(r?.length??0)+(a?.length??0),accepted:h.length,missingClusters:null!=t?Object.keys(t).length:0});let y=f??"pull",b=await u.getPeer({peerDID:p,stores:s}),k=b?.allowedDirection??"both",I=negotiateDirection(y,k);return I!==y&&c.info("sync/negotiate: direction downgraded",{requested:y,allowed:k,agreed:I}),{direction:I,acceptedScopes:h,excludedDocumentIDs:[],...null!=t&&{missingClusters:t}}},"sync/merkle-sync":a({db:s,graph:i,logger:c,selfDID:e.selfDID,storeUnreadable:f,defaultAccessLevel:e.defaultAccessLevel})}}
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;#u=!1;#l=!1;#d;#D;#y;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.syncHandlers,this.#o=t.getRandomID,this.#c=t.idleTimeoutMs,this.#a=t.reconnectTimeoutMs,this.#h=t.inboxCapacity,this.#p=t.onEvent}start(){this.#u||this.#l||(this.#u=!0,this.#y=new s({registry:this.#e,groupID:this.#s}),this.#D=this.#t.tunnelMessageStream({peerDID:this.#i}),this.#m())}async stop(){if(this.#l)return;this.#l=!0;let t=this.#d;if(this.#d=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.#y=void 0}#m(){let s;if(this.#l)return;let n=this.#y;if(null==n)throw Error("HubTunnelSyncListener: encryptor not initialized; call start() first");let i=t({hub:this.#I(),encryptor:n,groupID:this.#s,sessionID:{auto:!0},localDID:this.#n,peerDID:this.#i,idleTimeoutMs:this.#c,reconnectTimeoutMs:this.#a,inboxCapacity:this.#h,onEvent:this.#p,onSessionEnd:()=>{s?.dispose().catch(()=>{})}});s=i;let r=new e({getRandomID:this.#o,handlers:this.#r,transports:[i]}),o={transport:i,server:r};this.#d=o,i.events.on("disposed",()=>{this.#d===o&&(this.#d=void 0),r.dispose().catch(()=>{}),this.#m()})}#I(){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)})}}}}
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)})}}}}
@@ -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{applySyncMutations as t}from"./merkle-apply.js";import{buildMerkleTree as r,findDivergentBuckets as a,getTimeBuckets as l,SYNC_BATCH_SIZE as n}from"./merkle-tree.js";export function createMerkleSyncChannelHandler(i){let{db:s,graph:c,logger:o}=i,u=i.storeUnreadable??"persist";return async m=>{let{scopes:p,excludedDocumentIDs:y,tree:f,direction:w}=m.param,d=m.writable.getWriter();o.info("sync/merkle-sync channel started",{scopes:p,excludedDocumentIDs:y,direction:w});let k=0,h=0,g=0;try{let b=await e(s),v=await b.getDocumentIDsForScope(p,y),D=await b.getMutationLogForDocuments(v),W=r(D);await d.write({type:"tree",tree:W.buckets});let J={root:f.root??"",buckets:f},L=a(W,J);k=L.length;let S=async()=>{if("pull"===w||"both"===w){let e=new Set(L),t=D.filter(t=>{let{minute:r}=l(t.hlc);return e.has(r)});for(let e=0;e<t.length;e+=n){let r=t.slice(e,e+n);await d.write({type:"mutations",mutationJWTs:r.map(e=>e.mutation_jwt)}),h+=r.length}await d.write({type:"complete"})}else await d.write({type:"complete"})},T=async()=>{let e=[],t=m.readable.getReader();try{for(;;){let{done:r,value:a}=await t.read();if(r)break;if("mutations"===a.type&&null!=a.mutationJWTs)e.push(...a.mutationJWTs);else if("complete"===a.type)break}}finally{t.releaseLock()}return e},[,j]=await Promise.all([S(),T()]);if(j.length>0){let e=await t({db:s,graph:c,mutationJWTs:j,selfDID:i.selfDID,storeUnreadable:u,defaultAccessLevel:i.defaultAccessLevel});g=e.applied+e.pending,o.info("sync/merkle-sync: applied received mutations",{result:e})}}catch(e){return o.error("sync/merkle-sync channel error",{error:e}),{success:!1,divergentBuckets:0,mutationsSent:0,mutationsReceived:0}}finally{try{await d.close()}catch{}}return o.info("sync/merkle-sync channel completed",{divergentBuckets:k,mutationsSent:h,mutationsReceived:g}),{success:!0,divergentBuckets:k,mutationsSent:h,mutationsReceived:g}}}
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 i="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:i.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 i=await e(r.stores),o=await i.getSyncPeer(r.peerDID);if(o)return t(o)}async listPeers(r){let i=await e(r.stores);return(await i.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 i={...r,...t.updates},o=await e(t.stores);await o.updateSyncPeer(t.peerDID,{endpoint:i.endpoint,mode:i.mode,allowed_users:i.allowedUsers,priority:i.priority,trust_level:i.trustLevel,config:{allowedDirection:i.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})}}
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;
@@ -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 u=e.createChannel("sync/merkle-sync",{param:{scopes:s,excludedDocumentIDs:o,tree:c.buckets,direction:l}}),h=u.readable.getReader(),d=await h.read();if(d.done)throw Error("MerkleSync: channel closed before receiving remote tree");let g=d.value;if("tree"!==g.type)throw Error(`MerkleSync: expected 'tree' message, got '${g.type}'`);let p=r(c,{root:g.tree?.root??"",buckets:g.tree??{}}),m=p.length;this.#r.debug("MerkleSync: found {count} divergent buckets",{count:m});let y=0,f=async()=>{if("push"===l||"both"===l){let e=new Set(p),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 u.send({type:"mutations",mutationJWTs:r.map(e=>e.mutation_jwt)}),y+=r.length}}await u.send({type:"complete"})},k=async()=>{let e=[];for(;;){let{done:t,value:r}=await h.read();if(t)break;if("mutations"===r.type&&null!=r.mutationJWTs)e.push(...r.mutationJWTs);else if("complete"===r.type)break}return h.releaseLock(),e},[,v]=await Promise.all([f(),k()]),w=await u;if(null!=w&&!w.success)throw Error("MerkleSync: responder reported failure");return this.#r.debug("MerkleSync: completed with {received} received, {sent} sent",{received:v.length,sent:y}),{mutationJWTs:v,divergentBuckets:m,mutationsSent:y}}}
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}}}
@@ -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()}}
package/lib/types.d.ts CHANGED
@@ -230,6 +230,14 @@ export type HubRequestContext = {
230
230
  }) => Promise<boolean>;
231
231
  listByGroup: (groupID: string) => Promise<Array<HubData>>;
232
232
  listGroups: (hubID: string) => Promise<Array<GroupData>>;
233
+ /** Current health snapshot for a group (state + last transition time). */
234
+ getGroupHealth: (groupID: string) => GroupHealthSnapshot;
235
+ /**
236
+ * Kick off stale-device recovery for a group. Fire-and-forget at the resolver
237
+ * boundary; the terminal outcome arrives via the `groupHealthChanged`
238
+ * subscription. Resolves with the flow's final result for callers that await.
239
+ */
240
+ rejoinGroup: (groupID: string) => Promise<RejoinResult>;
233
241
  };
234
242
  export type DelegationData = {
235
243
  jwt: string;
@@ -285,6 +293,28 @@ export type SyncSessionResult = {
285
293
  messagesSent: number;
286
294
  missingClusters?: Record<string, unknown>;
287
295
  };
296
+ /** A group-level health condition the monitor tracks. */
297
+ export type GroupHealthCondition = 'epoch-stale';
298
+ /**
299
+ * Persisted health state for a group. `recovered` is a transient transition
300
+ * surfaced only on the emitted event; `getState` collapses it to `healthy`.
301
+ */
302
+ export type GroupHealthState = 'healthy' | 'degraded' | 'recovering' | 'recovered' | 'recovery-failed';
303
+ export type GroupHealthData = {
304
+ groupID: string;
305
+ condition: GroupHealthCondition;
306
+ state: GroupHealthState;
307
+ };
308
+ /** A point-in-time health snapshot for a group (drives the GraphQL query). */
309
+ export type GroupHealthSnapshot = {
310
+ state: GroupHealthState;
311
+ /** Epoch ms of the last state transition, or null if never transitioned. */
312
+ lastTransitionAt: number | null;
313
+ };
314
+ /** Terminal outcome of a rejoin flow. */
315
+ export type RejoinResult = {
316
+ status: 'recovered' | 'recovery-failed';
317
+ };
288
318
  export type SyncPluginAPI = {
289
319
  /** Resolves when the HTTP sync transport is registered (only present when http option is enabled). */
290
320
  syncReady?: Promise<void>;
package/lib/utils.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ /** Node returns a Timeout object with `unref()`; browsers return a number. */
2
+ export declare function unref(timer: ReturnType<typeof setTimeout>): void;
3
+ /** Resolve after `ms`, without keeping the process alive. */
4
+ export declare function sleep(ms: number): Promise<void>;
5
+ export declare function bytesEqual(a: Uint8Array, b: Uint8Array): boolean;
package/lib/utils.js ADDED
@@ -0,0 +1 @@
1
+ export function unref(e){e.unref?.()}export function sleep(e){return new Promise(n=>{unref(setTimeout(n,e))})}export function bytesEqual(e,n){if(e.length!==n.length)return!1;for(let t=0;t<e.length;t++)if(e[t]!==n[t])return!1;return!0}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubun/plugin-p2p",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "license": "see LICENSE.md",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -20,7 +20,7 @@
20
20
  "@enkaku/codec": "^0.16.0",
21
21
  "@enkaku/event": "^0.16.0",
22
22
  "@enkaku/generator": "^0.16.0",
23
- "@enkaku/group": "^0.16.1",
23
+ "@enkaku/group": "^0.16.2",
24
24
  "@enkaku/http-client-transport": "^0.16.0",
25
25
  "@enkaku/http-server-transport": "^0.16.0",
26
26
  "@enkaku/hub-protocol": "^0.16.0",
@@ -33,18 +33,18 @@
33
33
  "@enkaku/token": "^0.16.0",
34
34
  "@enkaku/transport": "^0.16.0",
35
35
  "@noble/hashes": "^2.2.0",
36
- "graphql": "^16.13.2",
36
+ "graphql": "^16.14.2",
37
37
  "kysely": "^0.29.2",
38
- "@kubun/client": "^0.10.0",
39
- "@kubun/db": "^0.10.0",
40
- "@kubun/store-graph": "^0.10.0",
41
- "@kubun/graphql": "^0.10.0",
38
+ "@kubun/client": "^0.10.1",
42
39
  "@kubun/db-adapter": "^0.10.0",
43
- "@kubun/hlc": "^0.10.0",
40
+ "@kubun/db": "^0.10.0",
41
+ "@kubun/engine": "^0.10.3",
42
+ "@kubun/store-graph": "^0.10.2",
43
+ "@kubun/store-p2p": "^0.10.1",
44
+ "@kubun/hlc": "^0.10.1",
45
+ "@kubun/graphql": "^0.10.1",
44
46
  "@kubun/id": "^0.10.0",
45
- "@kubun/engine": "^0.10.0",
46
- "@kubun/store-p2p": "^0.10.0",
47
- "@kubun/mutation": "^0.10.0",
47
+ "@kubun/mutation": "^0.10.1",
48
48
  "@kubun/logger": "^0.10.0",
49
49
  "@kubun/protocol": "^0.10.0"
50
50
  },
@@ -52,8 +52,8 @@
52
52
  "@enkaku/hub-server": "^0.16.0",
53
53
  "get-port": "^7.2.0",
54
54
  "@kubun/db-better-sqlite": "^0.10.0",
55
- "@kubun/plugin-connector": "^0.10.0",
56
55
  "@kubun/hub": "^0.10.0",
56
+ "@kubun/plugin-connector": "^0.10.0",
57
57
  "@kubun/db-node-sqlite": "^0.10.0",
58
58
  "@kubun/plugin-http": "^0.10.0",
59
59
  "@kubun/plugin-rpc": "^0.10.0",