@kubun/plugin-p2p 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/context/group.d.ts +4 -0
- package/lib/context/group.js +1 -0
- package/lib/context/join.js +1 -1
- package/lib/context/sync.js +1 -1
- package/lib/context/types.d.ts +7 -8
- package/lib/groups/broadcast-codec.d.ts +3 -0
- package/lib/groups/broadcast-service.d.ts +76 -0
- package/lib/groups/broadcast-service.js +1 -0
- package/lib/{spaces → groups}/broadcast.d.ts +9 -10
- package/lib/groups/broadcast.js +1 -0
- package/lib/groups/events.d.ts +38 -0
- package/lib/{spaces → groups}/events.js +1 -1
- package/lib/{spaces → groups}/invite-payload.d.ts +2 -2
- package/lib/groups/join-utils.d.ts +19 -0
- package/lib/groups/join-utils.js +1 -0
- package/lib/{spaces → groups}/manager.d.ts +56 -46
- package/lib/groups/manager.js +1 -0
- package/lib/groups/mls-group-handle.d.ts +4 -0
- package/lib/groups/mls-group-handle.js +1 -0
- package/lib/groups/mls-state.d.ts +42 -0
- package/lib/groups/mls-state.js +1 -0
- package/lib/hub/connection-pool.d.ts +19 -0
- package/lib/hub/connection-pool.js +1 -0
- package/lib/hub/group-channel.d.ts +51 -0
- package/lib/hub/group-channel.js +1 -0
- package/lib/hub/receive-handler.d.ts +36 -0
- package/lib/hub/receive-handler.js +1 -0
- package/lib/hub/relay-manager.d.ts +37 -0
- package/lib/hub/relay-manager.js +1 -0
- package/lib/hub/send-handler.d.ts +28 -0
- package/lib/hub/send-handler.js +1 -0
- package/lib/index.d.ts +14 -13
- package/lib/index.js +1 -1
- package/lib/protocol.d.ts +44 -10
- package/lib/protocol.js +1 -1
- package/lib/schema.d.ts +2 -2
- package/lib/schema.js +42 -42
- package/lib/sync/catalog-scope.js +1 -1
- package/lib/sync/handlers.d.ts +10 -0
- package/lib/sync/handlers.js +1 -1
- package/lib/sync/merkle-apply.js +1 -1
- package/lib/sync/merkle-channel.d.ts +18 -0
- package/lib/sync/merkle-channel.js +1 -0
- package/lib/sync/merkle-tree.d.ts +3 -1
- package/lib/sync/merkle-tree.js +1 -1
- package/lib/sync/peer-registry.d.ts +27 -8
- package/lib/sync/peer-registry.js +1 -1
- package/lib/sync/sync-client.d.ts +15 -13
- package/lib/sync/sync-client.js +1 -1
- package/lib/sync/sync-manager.d.ts +36 -11
- package/lib/sync/sync-manager.js +1 -1
- package/lib/types.d.ts +42 -40
- package/package.json +20 -16
- package/lib/context/space.d.ts +0 -4
- package/lib/context/space.js +0 -1
- package/lib/migrations.d.ts +0 -8
- package/lib/migrations.js +0 -1
- package/lib/spaces/broadcast-codec.d.ts +0 -3
- package/lib/spaces/broadcast-service.d.ts +0 -59
- package/lib/spaces/broadcast-service.js +0 -1
- package/lib/spaces/broadcast.js +0 -1
- package/lib/spaces/events.d.ts +0 -38
- package/lib/spaces/group-handle.d.ts +0 -3
- package/lib/spaces/group-handle.js +0 -1
- package/lib/spaces/join-utils.d.ts +0 -22
- package/lib/spaces/join-utils.js +0 -1
- package/lib/spaces/manager.js +0 -1
- package/lib/spaces/mls-state.d.ts +0 -27
- package/lib/spaces/mls-state.js +0 -1
- /package/lib/{spaces → groups}/broadcast-codec.js +0 -0
- /package/lib/{spaces → groups}/invite-payload.js +0 -0
- /package/lib/{spaces → groups}/mls-json.d.ts +0 -0
- /package/lib/{spaces → groups}/mls-json.js +0 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import type { MutationLogEntry } from '@kubun/
|
|
1
|
+
import type { MutationLogEntry } from '@kubun/store-graph';
|
|
2
2
|
export type MerkleTree = {
|
|
3
3
|
root: string;
|
|
4
4
|
buckets: Record<string, string>;
|
|
5
5
|
};
|
|
6
|
+
/** Maximum number of mutation JWTs to send in a single channel message. */
|
|
7
|
+
export declare const SYNC_BATCH_SIZE = 1000;
|
|
6
8
|
export declare function getTimeBuckets(hlc: string): {
|
|
7
9
|
year: string;
|
|
8
10
|
month: string;
|
package/lib/sync/merkle-tree.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{blake3 as t}from"@noble/hashes/blake3.js";let e=[4,7,10,16],o=new TextEncoder;export function getTimeBuckets(t){return{year:t.slice(0,4),month:t.slice(0,7),day:t.slice(0,10),minute:t.slice(0,16)}}function r(e){return Array.from(t(o.encode(e))).map(t=>t.toString(16).padStart(2,"0")).join("")}export function buildMerkleTree(t){if(0===t.length)return{root:"",buckets:{}};let e=new Map;for(let o of t){let{minute:t}=getTimeBuckets(o.hlc),r=e.get(t);null==r&&(r=[],e.set(t,r)),r.push(o)}let o={};for(let[t,n]of e){let e=[...n].sort((t,e)=>t.mutation_hash<e.mutation_hash?-1:+(t.mutation_hash>e.mutation_hash)).map(t=>t.mutation_hash).join("\n");o[t]=r(e)}for(let t of[10,7,4]){let e=10===t?16:7===t?10:7,n=new Map;for(let r of Object.keys(o)){if(r.length!==e)continue;let o=r.slice(0,t),s=n.get(o);null==s&&(s=[],n.set(o,s)),s.push(r)}for(let[t,e]of n){let n=e.sort().map(t=>`${t}\0${o[t]}`).join("\n");o[t]=r(n)}}let n=r(Object.keys(o).filter(t=>4===t.length).sort().map(t=>`${t}\0${o[t]}`).join("\n"));return o.root=n,{root:n,buckets:o}}export function findDivergentBuckets(t,o){if(t.root===o.root)return[];if(""===t.root)return Object.keys(o.buckets).filter(t=>16===t.length).sort();if(""===o.root)return Object.keys(t.buckets).filter(t=>16===t.length).sort();let r=[],n=new Set([...Object.keys(t.buckets),...Object.keys(o.buckets)]);return!function s(l,i){let u=e[i];for(let c of[...n].filter(t=>t.length===u&&(""===l||t.startsWith(l))).sort())t.buckets[c]!==o.buckets[c]&&(i===e.length-1?r.push(c):s(c,i+1))}("",0),r.sort()}
|
|
1
|
+
import{blake3 as t}from"@noble/hashes/blake3.js";export const SYNC_BATCH_SIZE=1e3;let e=[4,7,10,16],o=new TextEncoder;export function getTimeBuckets(t){return{year:t.slice(0,4),month:t.slice(0,7),day:t.slice(0,10),minute:t.slice(0,16)}}function r(e){return Array.from(t(o.encode(e))).map(t=>t.toString(16).padStart(2,"0")).join("")}export function buildMerkleTree(t){if(0===t.length)return{root:"",buckets:{}};let e=new Map;for(let o of t){let{minute:t}=getTimeBuckets(o.hlc),r=e.get(t);null==r&&(r=[],e.set(t,r)),r.push(o)}let o={};for(let[t,n]of e){let e=[...n].sort((t,e)=>t.mutation_hash<e.mutation_hash?-1:+(t.mutation_hash>e.mutation_hash)).map(t=>t.mutation_hash).join("\n");o[t]=r(e)}for(let t of[10,7,4]){let e=10===t?16:7===t?10:7,n=new Map;for(let r of Object.keys(o)){if(r.length!==e)continue;let o=r.slice(0,t),s=n.get(o);null==s&&(s=[],n.set(o,s)),s.push(r)}for(let[t,e]of n){let n=e.sort().map(t=>`${t}\0${o[t]}`).join("\n");o[t]=r(n)}}let n=r(Object.keys(o).filter(t=>4===t.length).sort().map(t=>`${t}\0${o[t]}`).join("\n"));return o.root=n,{root:n,buckets:o}}export function findDivergentBuckets(t,o){if(t.root===o.root)return[];if(""===t.root)return Object.keys(o.buckets).filter(t=>16===t.length).sort();if(""===o.root)return Object.keys(t.buckets).filter(t=>16===t.length).sort();let r=[],n=new Set([...Object.keys(t.buckets),...Object.keys(o.buckets)]);return!function s(l,i){let u=e[i];for(let c of[...n].filter(t=>t.length===u&&(""===l||t.startsWith(l))).sort())t.buckets[c]!==o.buckets[c]&&(i===e.length-1?r.push(c):s(c,i+1))}("",0),r.sort()}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Runtime } from '@enkaku/runtime';
|
|
2
|
-
import type {
|
|
2
|
+
import type { StoreProvider } from '@kubun/db';
|
|
3
|
+
import type { SyncDirection } from '../protocol.js';
|
|
3
4
|
export type PeerConfig = {
|
|
4
5
|
peerDID: string;
|
|
5
6
|
endpoint: string;
|
|
@@ -9,6 +10,7 @@ export type PeerConfig = {
|
|
|
9
10
|
};
|
|
10
11
|
priority: string;
|
|
11
12
|
trustLevel: 'trusted' | 'restricted';
|
|
13
|
+
allowedDirection?: SyncDirection;
|
|
12
14
|
};
|
|
13
15
|
export type PeerConfigWithID = PeerConfig & {
|
|
14
16
|
id: string;
|
|
@@ -16,16 +18,33 @@ export type PeerConfigWithID = PeerConfig & {
|
|
|
16
18
|
updatedAt: number;
|
|
17
19
|
};
|
|
18
20
|
export type PeerRegistryParams = {
|
|
19
|
-
db: KubunDB;
|
|
20
21
|
runtime: Runtime;
|
|
21
22
|
};
|
|
22
23
|
export declare class PeerRegistry {
|
|
23
24
|
#private;
|
|
24
25
|
constructor(params: PeerRegistryParams);
|
|
25
|
-
addPeer(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
addPeer(params: {
|
|
27
|
+
config: PeerConfig;
|
|
28
|
+
stores: StoreProvider;
|
|
29
|
+
}): Promise<void>;
|
|
30
|
+
getPeer(params: {
|
|
31
|
+
peerDID: string;
|
|
32
|
+
stores: StoreProvider;
|
|
33
|
+
}): Promise<PeerConfigWithID | undefined>;
|
|
34
|
+
listPeers(params: {
|
|
35
|
+
stores: StoreProvider;
|
|
36
|
+
}): Promise<Array<PeerConfigWithID>>;
|
|
37
|
+
updatePeer(params: {
|
|
38
|
+
peerDID: string;
|
|
39
|
+
updates: Partial<PeerConfig>;
|
|
40
|
+
stores: StoreProvider;
|
|
41
|
+
}): Promise<void>;
|
|
42
|
+
removePeer(params: {
|
|
43
|
+
peerDID: string;
|
|
44
|
+
stores: StoreProvider;
|
|
45
|
+
}): Promise<void>;
|
|
46
|
+
isPeerAllowed(params: {
|
|
47
|
+
peerDID: string;
|
|
48
|
+
stores: StoreProvider;
|
|
49
|
+
}): Promise<boolean>;
|
|
31
50
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
function
|
|
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})}}
|
|
@@ -3,7 +3,7 @@ import type { ClientTransportOf } from '@enkaku/protocol';
|
|
|
3
3
|
import type { Runtime } from '@enkaku/runtime';
|
|
4
4
|
import type { SigningIdentity } from '@enkaku/token';
|
|
5
5
|
import type { Logger } from '@kubun/logger';
|
|
6
|
-
import type { SyncProtocol } from '../protocol.js';
|
|
6
|
+
import type { SyncDirection, SyncProtocol } from '../protocol.js';
|
|
7
7
|
/**
|
|
8
8
|
* Provider for sync-specific transport to a peer's sync server.
|
|
9
9
|
* The p2p plugin exposes this via `createSyncTransport()`.
|
|
@@ -21,14 +21,6 @@ export type SyncScope = {
|
|
|
21
21
|
modelID: string;
|
|
22
22
|
ownerDID: string;
|
|
23
23
|
};
|
|
24
|
-
export type MerkleSyncMessage = {
|
|
25
|
-
type: 'mutations';
|
|
26
|
-
mutationJWTs: Array<string>;
|
|
27
|
-
} | {
|
|
28
|
-
type: 'complete';
|
|
29
|
-
divergentBuckets?: number;
|
|
30
|
-
totalMutations?: number;
|
|
31
|
-
};
|
|
32
24
|
/**
|
|
33
25
|
* Client for connecting to peer servers for sync operations.
|
|
34
26
|
* Supports both direct (in-process) and HTTP transports.
|
|
@@ -48,21 +40,31 @@ export declare class SyncClient {
|
|
|
48
40
|
/**
|
|
49
41
|
* Negotiate sync scopes with peer.
|
|
50
42
|
*/
|
|
51
|
-
negotiate(client: Client<SyncProtocol>, scopes: Array<SyncScope>, delegationTokens?: Array<string>, knownModelIDs?: Array<string
|
|
43
|
+
negotiate(client: Client<SyncProtocol>, scopes: Array<SyncScope>, delegationTokens?: Array<string>, knownModelIDs?: Array<string>, direction?: SyncDirection): Promise<{
|
|
52
44
|
acceptedScopes: Array<SyncScope>;
|
|
53
45
|
excludedDocumentIDs: Array<string>;
|
|
54
46
|
missingClusters?: Record<string, unknown>;
|
|
47
|
+
direction?: SyncDirection;
|
|
55
48
|
}>;
|
|
56
49
|
/**
|
|
57
|
-
* Perform Merkle tree-based sync with peer.
|
|
58
|
-
* Sends local tree, receives
|
|
50
|
+
* Perform Merkle tree-based sync with peer using bidirectional channel.
|
|
51
|
+
* Sends local tree and divergent mutations, receives remote mutations.
|
|
59
52
|
*/
|
|
60
53
|
merkleSync(client: Client<SyncProtocol>, params: {
|
|
61
54
|
scopes: Array<SyncScope>;
|
|
62
55
|
excludedDocumentIDs: Array<string>;
|
|
63
|
-
|
|
56
|
+
localTree: {
|
|
57
|
+
root: string;
|
|
58
|
+
buckets: Record<string, string>;
|
|
59
|
+
};
|
|
60
|
+
direction: SyncDirection;
|
|
61
|
+
localEntries: Array<{
|
|
62
|
+
hlc: string;
|
|
63
|
+
mutation_jwt: string;
|
|
64
|
+
}>;
|
|
64
65
|
}): Promise<{
|
|
65
66
|
mutationJWTs: Array<string>;
|
|
66
67
|
divergentBuckets: number;
|
|
68
|
+
mutationsSent: number;
|
|
67
69
|
}>;
|
|
68
70
|
}
|
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{
|
|
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,8 +1,9 @@
|
|
|
1
1
|
import type { Runtime } from '@enkaku/runtime';
|
|
2
2
|
import { type Identity, type SigningIdentity } from '@enkaku/token';
|
|
3
|
-
import type {
|
|
3
|
+
import type { StoreProvider } from '@kubun/db';
|
|
4
4
|
import type { Logger } from '@kubun/logger';
|
|
5
|
-
import
|
|
5
|
+
import type { SyncDirection } from '../protocol.js';
|
|
6
|
+
import { type PeerConfig, type PeerConfigWithID, PeerRegistry } from './peer-registry.js';
|
|
6
7
|
import { type SyncScope, type SyncTransportProvider } from './sync-client.js';
|
|
7
8
|
export type SyncSessionInfo = {
|
|
8
9
|
peerID: string;
|
|
@@ -23,19 +24,24 @@ export type SyncEvent = {
|
|
|
23
24
|
reason?: string;
|
|
24
25
|
};
|
|
25
26
|
export type SyncManagerParams = {
|
|
26
|
-
db: KubunDB;
|
|
27
27
|
deployClusters?: (clusters: Record<string, unknown>) => Promise<void>;
|
|
28
28
|
runtime: Runtime;
|
|
29
29
|
identity: Identity;
|
|
30
30
|
logger: Logger;
|
|
31
31
|
serverResolver?: (serverID: string) => SyncTransportProvider | undefined;
|
|
32
32
|
};
|
|
33
|
-
export type
|
|
33
|
+
export type MerkleSyncParams = {
|
|
34
|
+
peerDID: string;
|
|
35
|
+
scopes: Array<SyncScope>;
|
|
36
|
+
stores: StoreProvider;
|
|
37
|
+
delegationTokens?: Array<string>;
|
|
34
38
|
knownModelIDs?: Array<string>;
|
|
39
|
+
direction?: SyncDirection;
|
|
35
40
|
};
|
|
36
41
|
export declare class SyncManager {
|
|
37
42
|
#private;
|
|
38
43
|
constructor(params: SyncManagerParams);
|
|
44
|
+
get peerRegistry(): PeerRegistry;
|
|
39
45
|
/**
|
|
40
46
|
* Configure the signing identity used for sync operations.
|
|
41
47
|
*/
|
|
@@ -44,18 +50,37 @@ export declare class SyncManager {
|
|
|
44
50
|
* Configure the server resolver for direct transport connections.
|
|
45
51
|
*/
|
|
46
52
|
setServerResolver(resolver: (serverID: string) => SyncTransportProvider | undefined): void;
|
|
47
|
-
addPeer(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
addPeer(params: {
|
|
54
|
+
config: PeerConfig;
|
|
55
|
+
stores: StoreProvider;
|
|
56
|
+
}): Promise<void>;
|
|
57
|
+
removePeer(params: {
|
|
58
|
+
peerDID: string;
|
|
59
|
+
stores: StoreProvider;
|
|
60
|
+
}): Promise<void>;
|
|
61
|
+
updatePeerConfig(params: {
|
|
62
|
+
peerDID: string;
|
|
63
|
+
updates: Partial<PeerConfig>;
|
|
64
|
+
stores: StoreProvider;
|
|
65
|
+
}): Promise<void>;
|
|
66
|
+
listPeers(params: {
|
|
67
|
+
stores: StoreProvider;
|
|
68
|
+
}): Promise<Array<PeerConfigWithID>>;
|
|
69
|
+
getPeer(params: {
|
|
70
|
+
peerDID: string;
|
|
71
|
+
stores: StoreProvider;
|
|
72
|
+
}): Promise<PeerConfigWithID | undefined>;
|
|
73
|
+
merkleSyncWithPeer(params: MerkleSyncParams): Promise<{
|
|
53
74
|
sessionID: string;
|
|
54
75
|
divergentBuckets: number;
|
|
55
76
|
messagesReceived: number;
|
|
77
|
+
messagesSent: number;
|
|
56
78
|
missingClusters?: Record<string, unknown>;
|
|
57
79
|
}>;
|
|
58
|
-
getStatus(
|
|
80
|
+
getStatus(params: {
|
|
81
|
+
peerDID?: string;
|
|
82
|
+
stores: StoreProvider;
|
|
83
|
+
}): SyncStatus;
|
|
59
84
|
onSyncEvent(callback: (event: SyncEvent) => void): () => void;
|
|
60
85
|
dispose(): Promise<void>;
|
|
61
86
|
}
|
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{applySyncMutations as s}from"./merkle-apply.js";import{buildMerkleTree as
|
|
1
|
+
import{EventEmitter as e}from"@enkaku/event";import{isSigningIdentity as t}from"@enkaku/token";import{getGraphStore as r}from"@kubun/store-graph";import{applySyncMutations as s}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;#r;#s;#i=new e;#n=new Map;#o=new Map;#l;#a;constructor(e){this.#e=e.deployClusters,this.#t=e.runtime,this.#l=e.identity,this.#r=e.logger,this.#s=new n({runtime:e.runtime}),this.#a=e.serverResolver}get peerRegistry(){return this.#s}setIdentity(e){this.#l=e}setServerResolver(e){this.#a=e}async addPeer(e){await this.#s.addPeer(e),this.#r.info("Peer added",{peerDID:e.config.peerDID})}async removePeer(e){await this.#s.removePeer(e),this.#r.info("Peer removed",{peerDID:e.peerDID})}async updatePeerConfig(e){await this.#s.updatePeer(e),this.#r.info("Peer updated",{peerDID:e.peerDID})}async listPeers(e){return this.#s.listPeers(e)}async getPeer(e){return this.#s.getPeer(e)}async merkleSyncWithPeer(e){let n,{peerDID:l,scopes:a,delegationTokens:c=[],knownModelIDs:g,direction:m}=e;this.#r.info("Starting Merkle sync with peer",{peerDID:l,scopes:a});let p=await this.#s.getPeer({peerDID:l,stores:e.stores});if(!p)throw Error(`Peer ${l} not found`);let y=`merkle-sync-${l}-${Date.now()}`,u={peerID:l,startTime:Date.now(),documentsAttempted:0,documentsCompleted:0};this.#n.set(y,u),this.#c({type:"started",peerID:l,timestamp:Date.now()});try{let d=this.#l;if(!t(d))throw Error("Signing identity required for Merkle sync");let h=new o({runtime:this.#t,identity:d,logger:this.#r,serverResolver:this.#a});n=await h.connect(p.endpoint,l);let v=m??"pull";this.#r.info("Negotiating sync scopes",{scopes:a,direction:v});let{acceptedScopes:f,excludedDocumentIDs:D,missingClusters:w,direction:k}=await h.negotiate(n,a,c,g,v);if(0===f.length)return this.#r.info("No scopes accepted by peer"),this.#o.set(l,Date.now()),this.#c({type:"completed",peerID:l,timestamp:Date.now()}),this.#n.delete(y),{sessionID:y,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 r(e.stores),P=await S.getDocumentIDsForScope(f,D),R=await S.getMutationLogForDocuments(P),I=i(R),C=k??v;this.#r.info("Requesting Merkle sync from peer",{localTreeBuckets:Object.keys(I.buckets).length,direction:C});let B=await h.merkleSync(n,{scopes:f,excludedDocumentIDs:D,localTree:I,direction:C,localEntries:R});return B.mutationJWTs.length>0&&(this.#r.info("Applying sync mutations",{mutations:B.mutationJWTs.length}),u.documentsCompleted=(await s({db:e.stores,mutationJWTs:B.mutationJWTs})).applied),this.#o.set(l,Date.now()),this.#c({type:"completed",peerID:l,timestamp:Date.now()}),this.#r.info("Merkle sync completed",{divergentBuckets:B.divergentBuckets,mutationsReceived:B.mutationJWTs.length}),{sessionID:y,divergentBuckets:B.divergentBuckets,messagesReceived:B.mutationJWTs.length,messagesSent:B.mutationsSent,missingClusters:w}}catch(e){throw this.#r.error("Merkle sync failed",{peerDID:l,error:e}),this.#c({type:"error",peerID:l,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.#n.delete(y)}}getStatus(e){let{peerDID:t}=e,r=Array.from(this.#n.values()).filter(e=>!t||e.peerID===t).map(e=>({peerID:e.peerID,startTime:e.startTime,documentsAttempted:e.documentsAttempted,documentsCompleted:e.documentsCompleted})),s={};for(let[e,r]of this.#o.entries())t&&e!==t||(s[e]=r);return{activeSessions:r,lastSyncByPeer:s}}onSyncEvent(e){return this.#i.on("sync",e)}#c(e){this.#i.emit("sync",e).catch(e=>{this.#r.error("Error in sync event listener",{error:e})})}async dispose(){this.#n.clear(),this.#o.clear()}}
|
package/lib/types.d.ts
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import type { ClientTransportOf } from '@enkaku/protocol';
|
|
2
|
-
import type { SyncProtocol } from './protocol.js';
|
|
2
|
+
import type { SyncDirection, SyncProtocol } from './protocol.js';
|
|
3
3
|
import type { PeerConfig, PeerConfigWithID } from './sync/peer-registry.js';
|
|
4
4
|
import type { SyncScope } from './sync/sync-client.js';
|
|
5
5
|
import type { SyncEvent, SyncStatus } from './sync/sync-manager.js';
|
|
6
|
-
export type
|
|
6
|
+
export type GroupData = {
|
|
7
7
|
id: string;
|
|
8
8
|
name: string;
|
|
9
9
|
description: string;
|
|
10
10
|
createdBy: string;
|
|
11
11
|
createdAt: string;
|
|
12
12
|
};
|
|
13
|
-
export type
|
|
14
|
-
|
|
13
|
+
export type GroupMemberData = {
|
|
14
|
+
groupID: string;
|
|
15
15
|
memberDID: string;
|
|
16
16
|
role: string;
|
|
17
17
|
createdAt: string;
|
|
18
18
|
};
|
|
19
19
|
export type CircleData = {
|
|
20
20
|
id: string;
|
|
21
|
-
|
|
21
|
+
groupID: string;
|
|
22
22
|
name: string;
|
|
23
23
|
description: string;
|
|
24
24
|
createdAt: string;
|
|
@@ -37,19 +37,19 @@ export type CatalogData = {
|
|
|
37
37
|
filterCriteria: string;
|
|
38
38
|
createdAt: string;
|
|
39
39
|
};
|
|
40
|
-
export type
|
|
40
|
+
export type InviteToGroupData = {
|
|
41
41
|
invitePayload: string;
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
groupID: string;
|
|
43
|
+
groupName: string;
|
|
44
44
|
};
|
|
45
|
-
export type
|
|
46
|
-
|
|
45
|
+
export type JoinGroupData = {
|
|
46
|
+
group: GroupData | null;
|
|
47
47
|
};
|
|
48
|
-
export type
|
|
49
|
-
|
|
48
|
+
export type LeaveGroupData = {
|
|
49
|
+
groupID: string;
|
|
50
50
|
};
|
|
51
|
-
export type
|
|
52
|
-
|
|
51
|
+
export type RemoveGroupMemberData = {
|
|
52
|
+
groupID: string;
|
|
53
53
|
};
|
|
54
54
|
export type DeleteCatalogData = {
|
|
55
55
|
deletedID: string;
|
|
@@ -61,8 +61,8 @@ export type RemoveCircleMemberData = {
|
|
|
61
61
|
circleID: string;
|
|
62
62
|
memberDID: string;
|
|
63
63
|
};
|
|
64
|
-
export type
|
|
65
|
-
|
|
64
|
+
export type UpdateGroupInput = {
|
|
65
|
+
groupID: string;
|
|
66
66
|
name?: string;
|
|
67
67
|
description?: string;
|
|
68
68
|
};
|
|
@@ -72,42 +72,42 @@ export type UpdateCircleInput = {
|
|
|
72
72
|
description?: string;
|
|
73
73
|
catalogIDs?: Array<string>;
|
|
74
74
|
};
|
|
75
|
-
export type
|
|
76
|
-
|
|
75
|
+
export type GroupRequestContext = {
|
|
76
|
+
p2pCreateGroup: (input: {
|
|
77
77
|
name: string;
|
|
78
78
|
description?: string;
|
|
79
|
-
}) => Promise<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
79
|
+
}) => Promise<GroupData>;
|
|
80
|
+
p2pGetGroups: () => Promise<Array<GroupData>>;
|
|
81
|
+
p2pGetGroup: (id: string) => Promise<GroupData | null>;
|
|
82
|
+
p2pAddGroupMember: (groupID: string, memberDID: string, role: string) => Promise<GroupMemberData>;
|
|
83
|
+
p2pGetGroupMembers: (groupID: string) => Promise<Array<GroupMemberData>>;
|
|
84
|
+
p2pCreateCircle: (params: {
|
|
85
|
+
groupID: string;
|
|
86
86
|
name: string;
|
|
87
87
|
description?: string;
|
|
88
88
|
}) => Promise<CircleData>;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
p2pGetCircles: (groupID: string) => Promise<Array<CircleData>>;
|
|
90
|
+
p2pAddCircleMember: (circleID: string, memberDID: string, role: string) => Promise<CircleMemberData>;
|
|
91
|
+
p2pCreateCatalog: (params: {
|
|
92
92
|
name: string;
|
|
93
93
|
description?: string;
|
|
94
94
|
filterCriteria: string;
|
|
95
95
|
}) => Promise<CatalogData>;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
96
|
+
p2pGetCatalogs: (ownerDID?: string) => Promise<Array<CatalogData>>;
|
|
97
|
+
p2pInviteToGroup: (groupID: string, joinRequest: string) => Promise<InviteToGroupData>;
|
|
98
|
+
p2pJoinGroup: (invitePayload: string, joinRequestPayload: string) => Promise<JoinGroupData>;
|
|
99
|
+
p2pLeaveGroup: (groupID: string) => Promise<LeaveGroupData>;
|
|
100
|
+
p2pRemoveGroupMember: (groupID: string, memberDID: string) => Promise<RemoveGroupMemberData>;
|
|
101
|
+
p2pUpdateGroup: (input: UpdateGroupInput) => Promise<GroupData>;
|
|
102
|
+
p2pUpdateCircle: (input: UpdateCircleInput) => Promise<CircleData>;
|
|
103
|
+
p2pUpdateCatalog: (params: {
|
|
104
104
|
catalogID: string;
|
|
105
105
|
name?: string;
|
|
106
106
|
filterCriteria?: string;
|
|
107
107
|
}) => Promise<CatalogData>;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
p2pDeleteCatalog: (catalogID: string) => Promise<DeleteCatalogData>;
|
|
109
|
+
p2pDeleteCircle: (circleID: string) => Promise<DeleteCircleData>;
|
|
110
|
+
p2pRemoveCircleMember: (circleID: string, memberDID: string) => Promise<RemoveCircleMemberData>;
|
|
111
111
|
};
|
|
112
112
|
export type SyncRequestContext = {
|
|
113
113
|
syncAddPeer: (config: PeerConfig) => Promise<void>;
|
|
@@ -120,13 +120,14 @@ export type P2PJoinRequestContext = {
|
|
|
120
120
|
joinRequest: string;
|
|
121
121
|
}>;
|
|
122
122
|
p2pCompleteJoin: (invitePayload: string) => Promise<{
|
|
123
|
-
|
|
123
|
+
group: GroupData | null;
|
|
124
124
|
}>;
|
|
125
125
|
};
|
|
126
126
|
export type SyncSessionResult = {
|
|
127
127
|
sessionID: string;
|
|
128
128
|
divergentBuckets: number;
|
|
129
129
|
messagesReceived: number;
|
|
130
|
+
messagesSent: number;
|
|
130
131
|
missingClusters?: Record<string, unknown>;
|
|
131
132
|
};
|
|
132
133
|
export type SyncPluginAPI = {
|
|
@@ -140,6 +141,7 @@ export type SyncPluginAPI = {
|
|
|
140
141
|
scopes: Array<SyncScope>;
|
|
141
142
|
delegationTokens?: Array<string>;
|
|
142
143
|
knownModelIDs?: Array<string>;
|
|
144
|
+
direction?: SyncDirection;
|
|
143
145
|
}): Promise<SyncSessionResult>;
|
|
144
146
|
getStatus(peerDID?: string): SyncStatus;
|
|
145
147
|
onSyncEvent(callback: (event: SyncEvent) => void): () => void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/plugin-p2p",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"license": "see LICENSE.md",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -14,42 +14,46 @@
|
|
|
14
14
|
],
|
|
15
15
|
"sideEffects": false,
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@enkaku/async": "^0.14.
|
|
17
|
+
"@enkaku/async": "^0.14.1",
|
|
18
18
|
"@enkaku/capability": "^0.14.0",
|
|
19
|
-
"@enkaku/client": "^0.14.
|
|
19
|
+
"@enkaku/client": "^0.14.4",
|
|
20
20
|
"@enkaku/codec": "^0.14.0",
|
|
21
21
|
"@enkaku/event": "^0.14.1",
|
|
22
22
|
"@enkaku/generator": "^0.14.0",
|
|
23
23
|
"@enkaku/group": "^0.14.6",
|
|
24
24
|
"@enkaku/http-client-transport": "^0.14.3",
|
|
25
25
|
"@enkaku/http-server-transport": "^0.14.2",
|
|
26
|
+
"@enkaku/hub-protocol": "^0.14.0",
|
|
26
27
|
"@enkaku/protocol": "^0.14.0",
|
|
27
28
|
"@enkaku/runtime": "^0.14.0",
|
|
28
29
|
"@enkaku/schema": "^0.14.0",
|
|
29
|
-
"@enkaku/server": "^0.14.
|
|
30
|
+
"@enkaku/server": "^0.14.4",
|
|
30
31
|
"@enkaku/stream": "^0.14.1",
|
|
31
32
|
"@enkaku/token": "0.14.1",
|
|
32
|
-
"@enkaku/transport": "0.14.
|
|
33
|
+
"@enkaku/transport": "0.14.1",
|
|
33
34
|
"@noble/hashes": "^2.2.0",
|
|
34
35
|
"graphql": "^16.13.2",
|
|
35
36
|
"kysely": "^0.28.16",
|
|
36
37
|
"@kubun/client": "^0.8.0",
|
|
37
|
-
"@kubun/
|
|
38
|
+
"@kubun/store-p2p": "^0.8.0",
|
|
39
|
+
"@kubun/graphql": "^0.8.1",
|
|
40
|
+
"@kubun/engine": "^0.8.2",
|
|
41
|
+
"@kubun/id": "^0.8.0",
|
|
38
42
|
"@kubun/hlc": "^0.8.0",
|
|
39
|
-
"@kubun/
|
|
40
|
-
"@kubun/db": "^0.8.
|
|
41
|
-
"@kubun/
|
|
43
|
+
"@kubun/mutation": "^0.8.1",
|
|
44
|
+
"@kubun/db": "^0.8.1",
|
|
45
|
+
"@kubun/store-graph": "^0.8.1",
|
|
46
|
+
"@kubun/protocol": "^0.8.1",
|
|
42
47
|
"@kubun/logger": "^0.8.0",
|
|
43
|
-
"@kubun/
|
|
44
|
-
"@kubun/mutation": "^0.8.0",
|
|
45
|
-
"@kubun/protocol": "^0.8.0"
|
|
48
|
+
"@kubun/db-adapter": "^0.8.1"
|
|
46
49
|
},
|
|
47
50
|
"devDependencies": {
|
|
51
|
+
"@enkaku/hub-server": "^0.14.1",
|
|
48
52
|
"get-port": "^7.2.0",
|
|
49
|
-
"@kubun/db-better-sqlite": "^0.8.
|
|
50
|
-
"@kubun/plugin-connector": "^0.8.
|
|
51
|
-
"@kubun/plugin-
|
|
52
|
-
"@kubun/plugin-
|
|
53
|
+
"@kubun/db-better-sqlite": "^0.8.1",
|
|
54
|
+
"@kubun/plugin-connector": "^0.8.1",
|
|
55
|
+
"@kubun/plugin-rpc": "^0.8.1",
|
|
56
|
+
"@kubun/plugin-http": "^0.8.1",
|
|
53
57
|
"@kubun/test-utils": "^0.8.0"
|
|
54
58
|
},
|
|
55
59
|
"scripts": {
|
package/lib/context/space.d.ts
DELETED
package/lib/context/space.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{HLC as e}from"@kubun/hlc";import{restoreGroupHandle as t}from"../spaces/group-handle.js";import{decodeFullJoinRequest as a,decodeInvitePayload as r,decodeJoinRequest as i,encodeInvitePayload as c}from"../spaces/invite-payload.js";import{createSpaceFromJoin as l}from"../spaces/join-utils.js";import{checkPeerAccess as s,normalizeCredential as d,toISO as n}from"./types.js";export function createSpaceContext(p,o){return{spaceCreateSpace:async e=>await p.transaction(async t=>{let a=t.db,r=await o.spaceManager.createSpace({identity:o.identity,name:e.name,description:e.description},a);await t.db.updateTable("kubun_spaces").set({created_by:p.viewerDID}).where("id","=",r.spaceID).execute();let i=await t.db.selectFrom("kubun_spaces").selectAll().where("id","=",r.spaceID).executeTakeFirst();if(null==i)throw Error("Failed to create space");t.emit("space:created",{spaceID:r.spaceID});let c={id:i.id,name:i.name,description:i.description,createdBy:i.created_by,createdAt:n(i.created_at)};return await o.emitter.emit("spaceJoined",c),c}),spaceGetSpaces:async()=>{let e=await o.getDB();return(await e.selectFrom("kubun_spaces").selectAll().execute()).map(e=>({id:e.id,name:e.name,description:e.description,createdBy:e.created_by,createdAt:n(e.created_at)}))},spaceGetSpace:async e=>{let t=await o.getDB(),a=await t.selectFrom("kubun_spaces").selectAll().where("id","=",e).executeTakeFirst();return null==a?null:{id:a.id,name:a.name,description:a.description,createdBy:a.created_by,createdAt:n(a.created_at)}},spaceAddSpaceMember:async(e,t,a)=>{let r=await o.getDB();await r.insertInto("kubun_space_members").values({space_id:e,member_did:t,role:a}).onConflict(e=>e.columns(["space_id","member_did"]).doUpdateSet(e=>({role:e.ref("excluded.role"),updated_at:o.adapter.encodeTimestamp(new Date)}))).execute();let i=await r.selectFrom("kubun_space_members").selectAll().where("space_id","=",e).where("member_did","=",t).executeTakeFirst();if(null==i)throw Error("Failed to add space member");let c={spaceID:i.space_id,memberDID:i.member_did,role:i.role,createdAt:n(i.created_at)};return await o.emitter.emit("spaceMemberJoined",{...c,spaceID:c.spaceID}),c},spaceGetSpaceMembers:async e=>{let t=await o.getDB();return(await t.selectFrom("kubun_space_members").selectAll().where("space_id","=",e).execute()).map(e=>({spaceID:e.space_id,memberDID:e.member_did,role:e.role,createdAt:n(e.created_at)}))},spaceCreateCircle:async e=>{let t=await o.spaceManager.createCircle({spaceID:e.spaceID,name:e.name,description:e.description}),a=await o.getDB(),r=await a.selectFrom("kubun_circles").selectAll().where("id","=",t.circleID).executeTakeFirst();if(null==r)throw Error("Failed to create circle");let i={id:r.id,spaceID:r.space_id,name:r.name,description:r.description,createdAt:n(r.created_at)};return await o.emitter.emit("circleCreated",{...i,spaceID:i.spaceID}),i},spaceGetCircles:async e=>{let t=await o.getDB();return(await t.selectFrom("kubun_circles").selectAll().where("space_id","=",e).execute()).map(e=>({id:e.id,spaceID:e.space_id,name:e.name,description:e.description,createdAt:n(e.created_at)}))},spaceAddCircleMember:async(t,a,r)=>{let i=await o.getDB(),c=e.serialize(o.hlc.now());await i.insertInto("kubun_circle_members").values({circle_id:t,member_did:a,role:r,hlc:c}).onConflict(e=>e.columns(["circle_id","member_did"]).doUpdateSet(e=>({role:e.ref("excluded.role"),hlc:e.ref("excluded.hlc")}))).execute();let l=await i.selectFrom("kubun_circle_members").selectAll().where("circle_id","=",t).where("member_did","=",a).executeTakeFirst();if(null==l)throw Error("Failed to add circle member");let s={circleID:l.circle_id,memberDID:l.member_did,role:l.role,createdAt:n(l.created_at)};return await o.emitter.emit("circleMemberAdded",{...s,circleID:s.circleID}),s},spaceCreateCatalog:async t=>{let a=await o.getDB(),r=o.runtime.getRandomID(),i=e.serialize(o.hlc.now());await a.insertInto("kubun_catalogs").values({id:r,owner_did:p.viewerDID,name:t.name,description:t.description??"",filter_criteria:o.adapter.encodeJSON("string"==typeof t.filterCriteria?JSON.parse(t.filterCriteria):t.filterCriteria),hlc:i}).execute();let c=await a.selectFrom("kubun_catalogs").selectAll().where("id","=",r).executeTakeFirst();if(null==c)throw Error("Failed to create catalog");return{id:c.id,ownerDID:c.owner_did,name:c.name,description:c.description,filterCriteria:"string"==typeof c.filter_criteria?c.filter_criteria:JSON.stringify(c.filter_criteria),createdAt:n(c.created_at)}},spaceGetCatalogs:async e=>{let t=(await o.getDB()).selectFrom("kubun_catalogs").selectAll();return null!=e&&(t=t.where("owner_did","=",e)),(await t.execute()).map(e=>({id:e.id,ownerDID:e.owner_did,name:e.name,description:e.description,filterCriteria:"string"==typeof e.filter_criteria?e.filter_criteria:JSON.stringify(e.filter_criteria),createdAt:n(e.created_at)}))},spaceInviteToSpace:async(e,a)=>{s(p.viewerDID,o.identity.id,o.autoAcceptPeers);let r=await o.getDB(),l=o.identity,n=await r.selectFrom("kubun_space_mls_state").selectAll().where("space_id","=",e).where("device_id","=",l.id).executeTakeFirst();if(null==n)throw Error(`No MLS state for space ${e}`);let m=await t({mlsState:n.mls_state,credential:d(n.credential),epoch:n.epoch,rootCapability:n.root_capability}),u=i(a),w=await o.spaceManager.inviteToSpace({spaceID:e,identity:l,groupHandle:m,recipientDID:u.did,recipientKeyPackage:u.publicPackage,permission:"member"},r),_=await r.selectFrom("kubun_spaces").select(["name","hub_urls"]).where("id","=",e).executeTakeFirst(),D="string"==typeof _?.hub_urls?JSON.parse(_.hub_urls):_?.hub_urls??[],b=c({spaceID:e,spaceName:_?.name??"",hubURLs:D,invite:w.invite,welcomeMessage:w.welcomeMessage,ratchetTree:w.updatedGroupHandle.state.ratchetTree});return await o.emitter.emit("spaceMemberJoined",{spaceID:e,memberDID:u.did,role:"member",createdAt:new Date().toISOString()}),{invitePayload:b,spaceID:e,spaceName:_?.name??""}},spaceJoinSpace:async(e,t)=>{let i=await o.getDB(),c=o.identity,s=r(e),d=a(t),n={publicPackage:d.publicPackage,privatePackage:d.privatePackage,ownerDID:d.did};return await o.spaceManager.joinSpace({identity:c,spaceID:s.spaceID,invite:s.invite,keyPackageBundle:n,welcomeMessage:s.welcomeMessage,ratchetTree:s.ratchetTree},i),await l({db:i,adapter:o.adapter,hlc:o.hlc,emitter:o.emitter,identity:c,invite:s})},spaceLeaveSpace:async e=>{let t=await o.getDB(),a=await t.selectFrom("kubun_spaces").selectAll().where("id","=",e).executeTakeFirst(),r=o.identity;return await o.spaceManager.leaveSpace({spaceID:e,identity:r}),null!=a&&await o.emitter.emit("spaceLeft",{id:a.id,name:a.name,description:a.description,createdBy:a.created_by,createdAt:n(a.created_at),spaceID:e}),{spaceID:e}},spaceRemoveSpaceMember:async(e,a)=>{let r=await o.getDB(),i=o.identity,c=await r.selectFrom("kubun_space_mls_state").selectAll().where("space_id","=",e).where("device_id","=",i.id).executeTakeFirst();if(null==c)throw Error(`No MLS state for space ${e}`);let l=await t({mlsState:c.mls_state,credential:d(c.credential),epoch:c.epoch,rootCapability:c.root_capability});return await o.spaceManager.removeSpaceMember({spaceID:e,groupHandle:l,memberDID:a},r),await o.emitter.emit("spaceMemberLeft",{spaceID:e,memberDID:a,role:"",createdAt:new Date().toISOString()}),{spaceID:e}},spaceUpdateSpace:async e=>{await o.spaceManager.updateSpace({spaceID:e.spaceID,update:{name:e.name,description:e.description}});let t=await o.getDB(),a=await t.selectFrom("kubun_spaces").selectAll().where("id","=",e.spaceID).executeTakeFirst();if(null==a)throw Error(`Space ${e.spaceID} not found`);let r={id:a.id,name:a.name,description:a.description,createdBy:a.created_by,createdAt:n(a.created_at)};return await o.emitter.emit("spaceDataChanged",{...r,spaceID:e.spaceID}),r},spaceUpdateCircle:async e=>{await o.spaceManager.updateCircle({circleID:e.circleID,update:{name:e.name,description:e.description,catalogIDs:e.catalogIDs}});let t=await o.getDB(),a=await t.selectFrom("kubun_circles").selectAll().where("id","=",e.circleID).executeTakeFirst();if(null==a)throw Error(`Circle ${e.circleID} not found`);let r={id:a.id,spaceID:a.space_id,name:a.name,description:a.description,createdAt:n(a.created_at)};return null!=e.catalogIDs?await o.emitter.emit("circleCatalogsChanged",{...r,circleID:e.circleID}):await o.emitter.emit("circleDataChanged",{...r,circleID:e.circleID}),r},spaceUpdateCatalog:async t=>{let a=await o.getDB(),r={hlc:e.serialize(o.hlc.now())};null!=t.name&&(r.name=t.name),null!=t.filterCriteria&&(r.filter_criteria=o.adapter.encodeJSON("string"==typeof t.filterCriteria?JSON.parse(t.filterCriteria):t.filterCriteria)),r.updated_at=o.adapter.encodeTimestamp(new Date),await a.updateTable("kubun_catalogs").set(r).where("id","=",t.catalogID).execute();let i=await a.selectFrom("kubun_catalogs").selectAll().where("id","=",t.catalogID).executeTakeFirst();if(null==i)throw Error(`Catalog ${t.catalogID} not found`);return{id:i.id,ownerDID:i.owner_did,name:i.name,description:i.description,filterCriteria:"string"==typeof i.filter_criteria?i.filter_criteria:JSON.stringify(i.filter_criteria),createdAt:n(i.created_at)}},spaceDeleteCatalog:async e=>{let t=await o.getDB();return await t.deleteFrom("kubun_catalogs").where("id","=",e).execute(),{deletedID:e}},spaceDeleteCircle:async e=>{let t=await o.getDB(),a=await t.selectFrom("kubun_circles").selectAll().where("id","=",e).executeTakeFirst();return await o.spaceManager.deleteCircle({circleID:e}),null!=a&&await o.emitter.emit("circleDeleted",{id:a.id,spaceID:a.space_id,name:a.name,description:a.description,createdAt:n(a.created_at)}),{deletedID:e}},spaceRemoveCircleMember:async(e,t)=>(await o.spaceManager.removeCircleMember({circleID:e,memberDID:t}),await o.emitter.emit("circleMemberRemoved",{circleID:e,memberDID:t,role:"",createdAt:new Date().toISOString()}),{circleID:e,memberDID:t})}}
|
package/lib/migrations.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { Adapter } from '@kubun/db-adapter';
|
|
2
|
-
import type { PluginMigrationContext } from '@kubun/engine';
|
|
3
|
-
import type { Migration } from 'kysely';
|
|
4
|
-
export type MigrationContext = {
|
|
5
|
-
types: Adapter['types'];
|
|
6
|
-
functions: Adapter['functions'];
|
|
7
|
-
};
|
|
8
|
-
export declare function createP2PMigrations(ctx: MigrationContext | PluginMigrationContext): Record<string, Migration>;
|
package/lib/migrations.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export function createP2PMigrations(e){let t=e.types,a=e.functions;return{"001_create_p2p_tables":{async up(e){await e.schema.createTable("kubun_spaces").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("name",t.text,e=>e.notNull()).addColumn("description",t.text,e=>e.notNull().defaultTo("")).addColumn("created_by",t.text,e=>e.notNull()).addColumn("hub_urls",t.json,e=>e.defaultTo("[]").notNull()).addColumn("hlc",t.text,e=>e.notNull()).addColumn("created_at",t.timestamp,e=>e.defaultTo(a.now).notNull()).addColumn("updated_at",t.timestamp).execute(),await e.schema.createTable("kubun_space_members").ifNotExists().addColumn("space_id",t.text,e=>e.notNull()).addColumn("member_did",t.text,e=>e.notNull()).addColumn("role",t.text,e=>e.notNull().defaultTo("member")).addColumn("created_at",t.timestamp,e=>e.defaultTo(a.now).notNull()).addColumn("updated_at",t.timestamp).addPrimaryKeyConstraint("pk_space_members",["space_id","member_did"]).execute(),await e.schema.createIndex("idx_space_members_did").ifNotExists().on("kubun_space_members").column("member_did").execute(),await e.schema.createTable("kubun_circles").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("space_id",t.text,e=>e.notNull()).addColumn("name",t.text,e=>e.notNull()).addColumn("description",t.text,e=>e.notNull().defaultTo("")).addColumn("catalog_ids",t.json,e=>e.notNull().defaultTo("[]")).addColumn("hlc",t.text,e=>e.notNull()).addColumn("created_at",t.timestamp,e=>e.defaultTo(a.now).notNull()).addColumn("updated_at",t.timestamp).execute(),await e.schema.createIndex("idx_circles_space").ifNotExists().on("kubun_circles").column("space_id").execute(),await e.schema.createTable("kubun_circle_members").ifNotExists().addColumn("circle_id",t.text,e=>e.notNull()).addColumn("member_did",t.text,e=>e.notNull()).addColumn("role",t.text,e=>e.notNull().defaultTo("member")).addColumn("hlc",t.text,e=>e.notNull()).addColumn("created_at",t.timestamp,e=>e.defaultTo(a.now).notNull()).addColumn("updated_at",t.timestamp).addPrimaryKeyConstraint("pk_circle_members",["circle_id","member_did"]).execute(),await e.schema.createIndex("idx_circle_members_did").ifNotExists().on("kubun_circle_members").column("member_did").execute(),await e.schema.createTable("kubun_catalogs").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("owner_did",t.text,e=>e.notNull()).addColumn("name",t.text,e=>e.notNull()).addColumn("description",t.text,e=>e.notNull().defaultTo("")).addColumn("filter_criteria",t.json,e=>e.notNull()).addColumn("hlc",t.text,e=>e.notNull()).addColumn("created_at",t.timestamp,e=>e.defaultTo(a.now).notNull()).addColumn("updated_at",t.timestamp).execute(),await e.schema.createIndex("idx_catalogs_owner").ifNotExists().on("kubun_catalogs").column("owner_did").execute(),await e.schema.createTable("kubun_space_mls_state").ifNotExists().addColumn("space_id",t.text,e=>e.notNull()).addColumn("device_id",t.text,e=>e.notNull()).addColumn("mls_state",t.binary,e=>e.notNull()).addColumn("credential",t.text,e=>e.notNull()).addColumn("epoch","integer",e=>e.notNull()).addColumn("root_capability",t.text,e=>e.notNull().defaultTo("")).addColumn("updated_at",t.timestamp,e=>e.defaultTo(a.now).notNull()).addPrimaryKeyConstraint("pk_space_mls_state",["space_id","device_id"]).execute(),await e.schema.createTable("kubun_sync_peers").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("peer_did",t.text,e=>e.notNull().unique()).addColumn("endpoint",t.text,e=>e.notNull()).addColumn("mode",t.text,e=>e.notNull()).addColumn("allowed_users",t.json,e=>e.notNull()).addColumn("priority",t.text,e=>e.notNull().defaultTo("0")).addColumn("trust_level",t.text,e=>e.notNull()).addColumn("config",t.json,e=>e.notNull()).addColumn("created_at",t.timestamp,e=>e.defaultTo(a.now).notNull()).addColumn("updated_at",t.timestamp,e=>e.defaultTo(a.now).notNull()).execute(),await e.schema.createTable("kubun_sync_checkpoints").ifNotExists().addColumn("id",t.text,e=>e.notNull().primaryKey()).addColumn("peer_did",t.text,e=>e.notNull()).addColumn("scope",t.text,e=>e.notNull()).addColumn("checkpoint_hlc",t.text,e=>e.notNull()).addColumn("updated_at",t.timestamp,e=>e.defaultTo(a.now).notNull()).execute()},async down(e){await e.schema.dropTable("kubun_sync_checkpoints").ifExists().execute(),await e.schema.dropTable("kubun_sync_peers").ifExists().execute(),await e.schema.dropTable("kubun_space_mls_state").ifExists().execute(),await e.schema.dropTable("kubun_catalogs").ifExists().execute(),await e.schema.dropTable("kubun_circle_members").ifExists().execute(),await e.schema.dropTable("kubun_circles").ifExists().execute(),await e.schema.dropTable("kubun_space_members").ifExists().execute(),await e.schema.dropTable("kubun_spaces").ifExists().execute()}}}}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from '@enkaku/event';
|
|
2
|
-
import type { Database } from '@kubun/db';
|
|
3
|
-
import type { AbstractAdapter, AdapterTypes } from '@kubun/db-adapter';
|
|
4
|
-
import type { Kysely } from 'kysely';
|
|
5
|
-
import { type SpaceBroadcastMessage } from './broadcast.js';
|
|
6
|
-
import { type SerializedGroupState } from './mls-state.js';
|
|
7
|
-
export type BroadcastEvent = {
|
|
8
|
-
spaceID: string;
|
|
9
|
-
message: SpaceBroadcastMessage;
|
|
10
|
-
applied: boolean;
|
|
11
|
-
};
|
|
12
|
-
export type BroadcastServiceEvents = {
|
|
13
|
-
broadcast: BroadcastEvent;
|
|
14
|
-
};
|
|
15
|
-
export type SendBroadcastParams = {
|
|
16
|
-
spaceID: string;
|
|
17
|
-
message: SpaceBroadcastMessage;
|
|
18
|
-
groupState: SerializedGroupState;
|
|
19
|
-
};
|
|
20
|
-
export type SendBroadcastResult = {
|
|
21
|
-
encrypted: Uint8Array;
|
|
22
|
-
updatedGroupState: SerializedGroupState;
|
|
23
|
-
};
|
|
24
|
-
export type ReceiveBroadcastParams = {
|
|
25
|
-
spaceID: string;
|
|
26
|
-
encrypted: Uint8Array;
|
|
27
|
-
groupState: SerializedGroupState;
|
|
28
|
-
};
|
|
29
|
-
export type ReceiveBroadcastResult = {
|
|
30
|
-
message: SpaceBroadcastMessage;
|
|
31
|
-
applied: boolean;
|
|
32
|
-
updatedGroupState: SerializedGroupState;
|
|
33
|
-
};
|
|
34
|
-
/**
|
|
35
|
-
* BroadcastService handles MLS encrypt/decrypt for space broadcasts
|
|
36
|
-
* and applies received messages to the local DB.
|
|
37
|
-
*
|
|
38
|
-
* It does NOT manage hub transport directly -- callers are responsible
|
|
39
|
-
* for sending encrypted bytes via hub and delivering received bytes
|
|
40
|
-
* from hub. This keeps the service testable without hub infrastructure.
|
|
41
|
-
*/
|
|
42
|
-
export type BroadcastServiceParams<T extends AdapterTypes = AdapterTypes> = {
|
|
43
|
-
kysely: Kysely<Database>;
|
|
44
|
-
adapter: AbstractAdapter<T>;
|
|
45
|
-
};
|
|
46
|
-
export declare class BroadcastService extends EventEmitter<BroadcastServiceEvents> {
|
|
47
|
-
#private;
|
|
48
|
-
constructor(params: BroadcastServiceParams);
|
|
49
|
-
/**
|
|
50
|
-
* Prepare a broadcast for sending: serialize + MLS encrypt.
|
|
51
|
-
* Returns encrypted bytes to send via hub and updated MLS state to persist.
|
|
52
|
-
*/
|
|
53
|
-
prepareSend(params: SendBroadcastParams): Promise<SendBroadcastResult>;
|
|
54
|
-
/**
|
|
55
|
-
* Process a received broadcast: MLS decrypt + deserialize + apply to DB.
|
|
56
|
-
* Returns the decoded message, whether it was applied, and updated MLS state.
|
|
57
|
-
*/
|
|
58
|
-
processReceived(params: ReceiveBroadcastParams): Promise<ReceiveBroadcastResult>;
|
|
59
|
-
}
|