@kubun/plugin-p2p 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +57 -0
- package/lib/context/join.d.ts +4 -0
- package/lib/context/join.js +1 -0
- package/lib/context/space.d.ts +4 -0
- package/lib/context/space.js +1 -0
- package/lib/context/sync.d.ts +3 -0
- package/lib/context/sync.js +1 -0
- package/lib/context/types.d.ts +35 -0
- package/lib/context/types.js +1 -0
- package/lib/index.d.ts +25 -0
- package/lib/index.js +1 -0
- package/lib/migrations.d.ts +8 -0
- package/lib/migrations.js +1 -0
- package/lib/protocol.d.ts +175 -0
- package/lib/protocol.js +1 -0
- package/lib/schema.d.ts +3 -0
- package/lib/schema.js +187 -0
- package/lib/spaces/broadcast-codec.d.ts +3 -0
- package/lib/spaces/broadcast-codec.js +1 -0
- package/lib/spaces/broadcast-service.d.ts +59 -0
- package/lib/spaces/broadcast-service.js +1 -0
- package/lib/spaces/broadcast.d.ts +65 -0
- package/lib/spaces/broadcast.js +1 -0
- package/lib/spaces/events.d.ts +38 -0
- package/lib/spaces/events.js +1 -0
- package/lib/spaces/group-handle.d.ts +3 -0
- package/lib/spaces/group-handle.js +1 -0
- package/lib/spaces/invite-payload.d.ts +24 -0
- package/lib/spaces/invite-payload.js +1 -0
- package/lib/spaces/join-utils.d.ts +22 -0
- package/lib/spaces/join-utils.js +1 -0
- package/lib/spaces/manager.d.ts +147 -0
- package/lib/spaces/manager.js +1 -0
- package/lib/spaces/mls-json.d.ts +2 -0
- package/lib/spaces/mls-json.js +1 -0
- package/lib/spaces/mls-state.d.ts +27 -0
- package/lib/spaces/mls-state.js +1 -0
- package/lib/sync/catalog-scope.d.ts +17 -0
- package/lib/sync/catalog-scope.js +1 -0
- package/lib/sync/handlers.d.ts +19 -0
- package/lib/sync/handlers.js +1 -0
- package/lib/sync/merkle-apply.d.ts +12 -0
- package/lib/sync/merkle-apply.js +1 -0
- package/lib/sync/merkle-tree.d.ts +13 -0
- package/lib/sync/merkle-tree.js +1 -0
- package/lib/sync/peer-registry.d.ts +31 -0
- package/lib/sync/peer-registry.js +1 -0
- package/lib/sync/sync-client.d.ts +68 -0
- package/lib/sync/sync-client.js +1 -0
- package/lib/sync/sync-manager.d.ts +61 -0
- package/lib/sync/sync-manager.js +1 -0
- package/lib/types.d.ts +148 -0
- package/lib/types.js +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{EventEmitter as e}from"@enkaku/event";import{processBroadcast as a}from"./broadcast.js";import{deserializeBroadcast as r,serializeBroadcast as t}from"./broadcast-codec.js";import{restoreGroupHandle as s}from"./group-handle.js";import{replacer as o,reviver as c}from"./mls-json.js";import{serializeGroupState as d}from"./mls-state.js";export class BroadcastService extends e{#e;constructor(e){super(),this.#e={kysely:e.kysely,adapter:e.adapter}}async prepareSend(e){let{message:a,groupState:r}=e,c=t(a),i=await s(r),{message:m}=await i.encrypt(c),n=d(i);return{encrypted:new TextEncoder().encode(JSON.stringify(m,o)),updatedGroupState:n}}async processReceived(e){let{spaceID:t,encrypted:o,groupState:i}=e,m=await s(i),n=JSON.parse(new TextDecoder().decode(o),c),p=r(await m.decrypt(n)),y=await a(this.#e,p),l=d(m);return await this.emit("broadcast",{spaceID:t,message:p,applied:y}),{message:p,applied:y,updatedGroupState:l}}}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Database } from '@kubun/db';
|
|
2
|
+
import type { AbstractAdapter, AdapterTypes } from '@kubun/db-adapter';
|
|
3
|
+
import type { CatalogRecord, CircleMemberRecord, CircleRecord } from '@kubun/protocol';
|
|
4
|
+
import type { Kysely } from 'kysely';
|
|
5
|
+
export type SpaceBroadcastMessage = {
|
|
6
|
+
type: 'circle:create';
|
|
7
|
+
circle: CircleRecord;
|
|
8
|
+
} | {
|
|
9
|
+
type: 'circle:update';
|
|
10
|
+
circleID: string;
|
|
11
|
+
update: {
|
|
12
|
+
name?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
catalogIDs?: Array<string>;
|
|
15
|
+
hlc: string;
|
|
16
|
+
};
|
|
17
|
+
} | {
|
|
18
|
+
type: 'circle:delete';
|
|
19
|
+
circleID: string;
|
|
20
|
+
hlc: string;
|
|
21
|
+
} | {
|
|
22
|
+
type: 'member:add';
|
|
23
|
+
member: CircleMemberRecord;
|
|
24
|
+
} | {
|
|
25
|
+
type: 'member:remove';
|
|
26
|
+
circleID: string;
|
|
27
|
+
memberDID: string;
|
|
28
|
+
hlc: string;
|
|
29
|
+
} | {
|
|
30
|
+
type: 'catalog:create';
|
|
31
|
+
catalog: CatalogRecord;
|
|
32
|
+
} | {
|
|
33
|
+
type: 'catalog:update';
|
|
34
|
+
catalogID: string;
|
|
35
|
+
update: {
|
|
36
|
+
name?: string;
|
|
37
|
+
description?: string;
|
|
38
|
+
filterCriteria?: CatalogRecord['filterCriteria'];
|
|
39
|
+
hlc: string;
|
|
40
|
+
};
|
|
41
|
+
} | {
|
|
42
|
+
type: 'catalog:delete';
|
|
43
|
+
catalogID: string;
|
|
44
|
+
hlc: string;
|
|
45
|
+
} | {
|
|
46
|
+
type: 'space:update';
|
|
47
|
+
spaceID: string;
|
|
48
|
+
update: {
|
|
49
|
+
name?: string;
|
|
50
|
+
description?: string;
|
|
51
|
+
hubURLs?: Array<string>;
|
|
52
|
+
hlc: string;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
export type ProcessBroadcastParams<T extends AdapterTypes = AdapterTypes> = {
|
|
56
|
+
kysely: Kysely<Database>;
|
|
57
|
+
adapter: AbstractAdapter<T>;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Process a received broadcast message, applying it to the local database
|
|
61
|
+
* with HLC-based last-write-wins conflict resolution.
|
|
62
|
+
*
|
|
63
|
+
* Returns true if the message was applied, false if it was skipped (stale HLC or missing target).
|
|
64
|
+
*/
|
|
65
|
+
export declare function processBroadcast<T extends AdapterTypes = AdapterTypes>(params: ProcessBroadcastParams<T>, message: SpaceBroadcastMessage): Promise<boolean>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{HLC as e}from"@kubun/hlc";function t(t,c){return e.compare(e.parse(t),e.parse(c))>0}export async function processBroadcast(e,c){let{kysely:a,adapter:l}=e;switch(c.type){case"circle:create":{let e=await a.selectFrom("kubun_circles").selectAll().where("id","=",c.circle.id).executeTakeFirst();if(null!=e){if(!t(c.circle.hlc,e.hlc))return!1;let r={name:c.circle.name,description:c.circle.description,catalog_ids:l.encodeJSON(c.circle.catalogIDs),hlc:c.circle.hlc,updated_at:l.encodeTimestamp(new Date)};return await a.updateTable("kubun_circles").set(r).where("id","=",c.circle.id).execute(),!0}return await a.insertInto("kubun_circles").values({id:c.circle.id,space_id:c.circle.spaceID,name:c.circle.name,description:c.circle.description,catalog_ids:l.encodeJSON(c.circle.catalogIDs),hlc:c.circle.hlc}).execute(),!0}case"circle:update":{let e=await a.selectFrom("kubun_circles").selectAll().where("id","=",c.circleID).executeTakeFirst();if(null==e||!t(c.update.hlc,e.hlc))return!1;let r={};return null!=c.update.name&&(r.name=c.update.name),null!=c.update.description&&(r.description=c.update.description),null!=c.update.catalogIDs&&(r.catalog_ids=l.encodeJSON(c.update.catalogIDs)),r.hlc=c.update.hlc,r.updated_at=l.encodeTimestamp(new Date),await a.updateTable("kubun_circles").set(r).where("id","=",c.circleID).execute(),!0}case"circle:delete":{let e=await a.selectFrom("kubun_circles").selectAll().where("id","=",c.circleID).executeTakeFirst();if(null==e||!t(c.hlc,e.hlc))return!1;return await a.deleteFrom("kubun_circles").where("id","=",c.circleID).execute(),!0}case"member:add":return await a.insertInto("kubun_circle_members").values({circle_id:c.member.circleID,member_did:c.member.memberDID,role:c.member.role,hlc:c.member.hlc}).onConflict(e=>e.columns(["circle_id","member_did"]).doUpdateSet(e=>({role:e.ref("excluded.role"),hlc:e.ref("excluded.hlc")}))).execute(),!0;case"member:remove":return await a.deleteFrom("kubun_circle_members").where("circle_id","=",c.circleID).where("member_did","=",c.memberDID).execute(),!0;case"catalog:create":{let e=await a.selectFrom("kubun_catalogs").selectAll().where("id","=",c.catalog.id).executeTakeFirst();if(null!=e){if(!t(c.catalog.hlc,e.hlc))return!1;let r={name:c.catalog.name,description:c.catalog.description,filter_criteria:l.encodeJSON(c.catalog.filterCriteria),hlc:c.catalog.hlc,updated_at:l.encodeTimestamp(new Date)};return await a.updateTable("kubun_catalogs").set(r).where("id","=",c.catalog.id).execute(),!0}return await a.insertInto("kubun_catalogs").values({id:c.catalog.id,owner_did:c.catalog.ownerDID,name:c.catalog.name,description:c.catalog.description,filter_criteria:l.encodeJSON(c.catalog.filterCriteria),hlc:c.catalog.hlc}).execute(),!0}case"catalog:update":{let e=await a.selectFrom("kubun_catalogs").selectAll().where("id","=",c.catalogID).executeTakeFirst();if(null==e||!t(c.update.hlc,e.hlc))return!1;let r={};return null!=c.update.name&&(r.name=c.update.name),null!=c.update.description&&(r.description=c.update.description),null!=c.update.filterCriteria&&(r.filter_criteria=l.encodeJSON(c.update.filterCriteria)),r.hlc=c.update.hlc,r.updated_at=l.encodeTimestamp(new Date),await a.updateTable("kubun_catalogs").set(r).where("id","=",c.catalogID).execute(),!0}case"catalog:delete":{let e=await a.selectFrom("kubun_catalogs").selectAll().where("id","=",c.catalogID).executeTakeFirst();if(null==e||!t(c.hlc,e.hlc))return!1;return await a.deleteFrom("kubun_catalogs").where("id","=",c.catalogID).execute(),!0}case"space:update":{let e=await a.selectFrom("kubun_spaces").selectAll().where("id","=",c.spaceID).executeTakeFirst();if(null==e||!t(c.update.hlc,e.hlc))return!1;let r={};return null!=c.update.name&&(r.name=c.update.name),null!=c.update.description&&(r.description=c.update.description),null!=c.update.hubURLs&&(r.hub_urls=l.encodeJSON(c.update.hubURLs)),r.hlc=c.update.hlc,r.updated_at=l.encodeTimestamp(new Date),await a.updateTable("kubun_spaces").set(r).where("id","=",c.spaceID).execute(),!0}}}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { EventEmitter } from '@enkaku/event';
|
|
2
|
+
import type { CircleData, CircleMemberData, SpaceData, SpaceMemberData } from '../index.js';
|
|
3
|
+
export type SpaceEventMap = {
|
|
4
|
+
spaceJoined: SpaceData;
|
|
5
|
+
spaceLeft: SpaceData & {
|
|
6
|
+
spaceID: string;
|
|
7
|
+
};
|
|
8
|
+
spaceDataChanged: SpaceData & {
|
|
9
|
+
spaceID: string;
|
|
10
|
+
};
|
|
11
|
+
spaceMemberJoined: SpaceMemberData & {
|
|
12
|
+
spaceID: string;
|
|
13
|
+
};
|
|
14
|
+
spaceMemberLeft: SpaceMemberData & {
|
|
15
|
+
spaceID: string;
|
|
16
|
+
};
|
|
17
|
+
circleCreated: CircleData & {
|
|
18
|
+
spaceID: string;
|
|
19
|
+
};
|
|
20
|
+
circleDeleted: CircleData & {
|
|
21
|
+
spaceID: string;
|
|
22
|
+
};
|
|
23
|
+
circleDataChanged: CircleData & {
|
|
24
|
+
circleID: string;
|
|
25
|
+
};
|
|
26
|
+
circleMemberAdded: CircleMemberData & {
|
|
27
|
+
circleID: string;
|
|
28
|
+
};
|
|
29
|
+
circleMemberRemoved: CircleMemberData & {
|
|
30
|
+
circleID: string;
|
|
31
|
+
};
|
|
32
|
+
circleCatalogsChanged: CircleData & {
|
|
33
|
+
circleID: string;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
export type SpaceEventEmitter = EventEmitter<SpaceEventMap>;
|
|
37
|
+
export declare function createSpaceEventEmitter(): SpaceEventEmitter;
|
|
38
|
+
export declare function createFilteredGenerator<K extends keyof SpaceEventMap>(emitter: SpaceEventEmitter, eventName: K, filter?: (data: SpaceEventMap[K]) => boolean): AsyncGenerator<SpaceEventMap[K], void, void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{EventEmitter as e}from"@enkaku/event";import{fromEmitter as r}from"@enkaku/generator";export function createSpaceEventEmitter(){return new e}export function createFilteredGenerator(e,t,n){return r(e,t,{filter:n})}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{restoreGroup as r}from"@enkaku/group";import{deserializeGroupState as o}from"./mls-state.js";export async function restoreGroupHandle(t){let{state:e,credential:n,rootCapability:m}=o(t);return r({state:e,credential:n,rootCapability:m})}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Invite, KeyPackageBundle } from '@enkaku/group';
|
|
2
|
+
export type JoinRequestPayload = {
|
|
3
|
+
did: string;
|
|
4
|
+
publicPackage: KeyPackageBundle['publicPackage'];
|
|
5
|
+
};
|
|
6
|
+
export type FullJoinRequestPayload = {
|
|
7
|
+
did: string;
|
|
8
|
+
publicPackage: KeyPackageBundle['publicPackage'];
|
|
9
|
+
privatePackage: KeyPackageBundle['privatePackage'];
|
|
10
|
+
};
|
|
11
|
+
export type InvitePayload = {
|
|
12
|
+
spaceID: string;
|
|
13
|
+
spaceName: string;
|
|
14
|
+
hubURLs: Array<string>;
|
|
15
|
+
invite: Invite;
|
|
16
|
+
welcomeMessage: unknown;
|
|
17
|
+
ratchetTree: unknown;
|
|
18
|
+
};
|
|
19
|
+
export declare function encodeJoinRequest(payload: JoinRequestPayload): string;
|
|
20
|
+
export declare function decodeJoinRequest(encoded: string): JoinRequestPayload;
|
|
21
|
+
export declare function encodeInvitePayload(payload: InvitePayload): string;
|
|
22
|
+
export declare function decodeInvitePayload(encoded: string): InvitePayload;
|
|
23
|
+
export declare function encodeFullJoinRequest(payload: FullJoinRequestPayload): string;
|
|
24
|
+
export declare function decodeFullJoinRequest(encoded: string): FullJoinRequestPayload;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{fromB64 as e,toB64 as n}from"@enkaku/codec";import{replacer as o,reviver as t}from"./mls-json.js";export function encodeJoinRequest(e){let t=JSON.stringify(e,o);return n(new TextEncoder().encode(t))}export function decodeJoinRequest(n){return JSON.parse(new TextDecoder().decode(e(n)),t)}export function encodeInvitePayload(e){let t=JSON.stringify(e,o);return n(new TextEncoder().encode(t))}export function decodeInvitePayload(n){return JSON.parse(new TextDecoder().decode(e(n)),t)}export function encodeFullJoinRequest(e){let t=JSON.stringify(e,o);return n(new TextEncoder().encode(t))}export function decodeFullJoinRequest(n){return JSON.parse(new TextDecoder().decode(e(n)),t)}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { OwnIdentity } from '@enkaku/token';
|
|
2
|
+
import type { Database } from '@kubun/db';
|
|
3
|
+
import type { Adapter } from '@kubun/db-adapter';
|
|
4
|
+
import { HLC } from '@kubun/hlc';
|
|
5
|
+
import type { Kysely } from 'kysely';
|
|
6
|
+
import type { SpaceData } from '../types.js';
|
|
7
|
+
import type { SpaceEventEmitter } from './events.js';
|
|
8
|
+
export type CreateSpaceFromJoinParams = {
|
|
9
|
+
db: Kysely<Database>;
|
|
10
|
+
adapter: Adapter;
|
|
11
|
+
hlc: HLC;
|
|
12
|
+
emitter: SpaceEventEmitter;
|
|
13
|
+
identity: OwnIdentity;
|
|
14
|
+
invite: {
|
|
15
|
+
spaceID: string;
|
|
16
|
+
spaceName: string;
|
|
17
|
+
hubURLs: Array<string>;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export declare function createSpaceFromJoin(params: CreateSpaceFromJoinParams): Promise<{
|
|
21
|
+
space: SpaceData | null;
|
|
22
|
+
}>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{HLC as e}from"@kubun/hlc";import{toISO as t}from"../context/types.js";export async function createSpaceFromJoin(a){let{db:c,adapter:s,hlc:i,emitter:r,identity:n,invite:l}=a;null==await c.selectFrom("kubun_spaces").selectAll().where("id","=",l.spaceID).executeTakeFirst()&&await c.insertInto("kubun_spaces").values({id:l.spaceID,name:l.spaceName,description:"",created_by:n.id,hub_urls:s.encodeJSON(l.hubURLs),hlc:e.serialize(i.now())}).execute();let u=await c.selectFrom("kubun_spaces").selectAll().where("id","=",l.spaceID).executeTakeFirst(),o=null!=u?{id:u.id,name:u.name,description:u.description,createdBy:u.created_by,createdAt:t(u.created_at)}:null;return null!=o&&await r.emit("spaceJoined",o),{space:o}}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { type GroupHandle, type GroupPermission, type Invite, type KeyPackageBundle } from '@enkaku/group';
|
|
2
|
+
import type { Identity, OwnIdentity } from '@enkaku/token';
|
|
3
|
+
import type { Database } from '@kubun/db';
|
|
4
|
+
import type { AbstractAdapter, AdapterTypes } from '@kubun/db-adapter';
|
|
5
|
+
import type { Kysely, Transaction } from 'kysely';
|
|
6
|
+
import type { SpaceBroadcastMessage } from './broadcast.js';
|
|
7
|
+
export type QueryRunner = Kysely<Database> | Transaction<Database>;
|
|
8
|
+
export type SpaceManagerParams<T extends AdapterTypes = AdapterTypes> = {
|
|
9
|
+
kysely: Kysely<Database> | Promise<Kysely<Database>>;
|
|
10
|
+
adapter: AbstractAdapter<T>;
|
|
11
|
+
identity: Identity;
|
|
12
|
+
getRandomID: () => string;
|
|
13
|
+
};
|
|
14
|
+
export type CreateSpaceParams = {
|
|
15
|
+
identity: OwnIdentity;
|
|
16
|
+
name: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
hubURLs?: Array<string>;
|
|
19
|
+
};
|
|
20
|
+
export type CreateSpaceResult = {
|
|
21
|
+
spaceID: string;
|
|
22
|
+
groupHandle: GroupHandle;
|
|
23
|
+
};
|
|
24
|
+
export type UpdateSpaceParams = {
|
|
25
|
+
spaceID: string;
|
|
26
|
+
update: {
|
|
27
|
+
name?: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
hubURLs?: Array<string>;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
export type UpdateSpaceResult = {
|
|
33
|
+
broadcast: SpaceBroadcastMessage;
|
|
34
|
+
};
|
|
35
|
+
export type InviteToSpaceParams = {
|
|
36
|
+
spaceID: string;
|
|
37
|
+
identity: OwnIdentity;
|
|
38
|
+
groupHandle: GroupHandle;
|
|
39
|
+
recipientDID: string;
|
|
40
|
+
recipientKeyPackage: KeyPackageBundle['publicPackage'];
|
|
41
|
+
permission: GroupPermission;
|
|
42
|
+
};
|
|
43
|
+
export type InviteToSpaceResult = {
|
|
44
|
+
invite: Invite;
|
|
45
|
+
welcomeMessage: unknown;
|
|
46
|
+
commitMessage: unknown;
|
|
47
|
+
updatedGroupHandle: GroupHandle;
|
|
48
|
+
};
|
|
49
|
+
export type JoinSpaceParams = {
|
|
50
|
+
identity: OwnIdentity;
|
|
51
|
+
spaceID: string;
|
|
52
|
+
invite: Invite;
|
|
53
|
+
keyPackageBundle: KeyPackageBundle;
|
|
54
|
+
welcomeMessage: unknown;
|
|
55
|
+
ratchetTree: unknown;
|
|
56
|
+
};
|
|
57
|
+
export type JoinSpaceResult = {
|
|
58
|
+
groupHandle: GroupHandle;
|
|
59
|
+
};
|
|
60
|
+
export type RemoveMemberParams = {
|
|
61
|
+
spaceID: string;
|
|
62
|
+
groupHandle: GroupHandle;
|
|
63
|
+
leafIndex: number;
|
|
64
|
+
memberDID: string;
|
|
65
|
+
};
|
|
66
|
+
export type RemoveMemberResult = {
|
|
67
|
+
commitMessage: unknown;
|
|
68
|
+
updatedGroupHandle: GroupHandle;
|
|
69
|
+
};
|
|
70
|
+
export type RemoveSpaceMemberParams = {
|
|
71
|
+
spaceID: string;
|
|
72
|
+
groupHandle: GroupHandle;
|
|
73
|
+
memberDID: string;
|
|
74
|
+
};
|
|
75
|
+
export type RemoveSpaceMemberResult = {
|
|
76
|
+
commitMessage: unknown;
|
|
77
|
+
updatedGroupHandle: GroupHandle;
|
|
78
|
+
};
|
|
79
|
+
export type LeaveSpaceParams = {
|
|
80
|
+
spaceID: string;
|
|
81
|
+
identity: OwnIdentity;
|
|
82
|
+
};
|
|
83
|
+
export type LeaveSpaceResult = undefined;
|
|
84
|
+
export type CreateCircleParams = {
|
|
85
|
+
spaceID: string;
|
|
86
|
+
name: string;
|
|
87
|
+
description?: string;
|
|
88
|
+
catalogIDs?: Array<string>;
|
|
89
|
+
};
|
|
90
|
+
export type CreateCircleResult = {
|
|
91
|
+
circleID: string;
|
|
92
|
+
broadcast: SpaceBroadcastMessage;
|
|
93
|
+
};
|
|
94
|
+
export type UpdateCircleParams = {
|
|
95
|
+
circleID: string;
|
|
96
|
+
update: {
|
|
97
|
+
name?: string;
|
|
98
|
+
description?: string;
|
|
99
|
+
catalogIDs?: Array<string>;
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
export type UpdateCircleResult = {
|
|
103
|
+
broadcast: SpaceBroadcastMessage;
|
|
104
|
+
broadcasts: Array<SpaceBroadcastMessage>;
|
|
105
|
+
};
|
|
106
|
+
export type DeleteCircleParams = {
|
|
107
|
+
circleID: string;
|
|
108
|
+
};
|
|
109
|
+
export type DeleteCircleResult = {
|
|
110
|
+
broadcast: SpaceBroadcastMessage;
|
|
111
|
+
};
|
|
112
|
+
export type AddCircleMemberParams = {
|
|
113
|
+
spaceID: string;
|
|
114
|
+
circleID: string;
|
|
115
|
+
memberDID: string;
|
|
116
|
+
role: 'admin' | 'member';
|
|
117
|
+
};
|
|
118
|
+
export type AddCircleMemberResult = {
|
|
119
|
+
broadcast: SpaceBroadcastMessage;
|
|
120
|
+
};
|
|
121
|
+
export type RemoveCircleMemberParams = {
|
|
122
|
+
circleID: string;
|
|
123
|
+
memberDID: string;
|
|
124
|
+
};
|
|
125
|
+
export type RemoveCircleMemberResult = {
|
|
126
|
+
broadcast: SpaceBroadcastMessage;
|
|
127
|
+
};
|
|
128
|
+
export declare class SpaceManager {
|
|
129
|
+
#private;
|
|
130
|
+
constructor(params: SpaceManagerParams);
|
|
131
|
+
createSpace(params: CreateSpaceParams, db?: QueryRunner): Promise<CreateSpaceResult>;
|
|
132
|
+
updateSpace(params: UpdateSpaceParams): Promise<UpdateSpaceResult>;
|
|
133
|
+
inviteToSpace(params: InviteToSpaceParams, db?: QueryRunner): Promise<InviteToSpaceResult>;
|
|
134
|
+
joinSpace(params: JoinSpaceParams, db?: QueryRunner): Promise<JoinSpaceResult>;
|
|
135
|
+
removeMember(params: RemoveMemberParams, db?: QueryRunner): Promise<RemoveMemberResult>;
|
|
136
|
+
removeSpaceMember(params: RemoveSpaceMemberParams, db?: QueryRunner): Promise<RemoveSpaceMemberResult>;
|
|
137
|
+
leaveSpace(params: LeaveSpaceParams, db?: QueryRunner): Promise<LeaveSpaceResult>;
|
|
138
|
+
loadSpace(spaceID: string): Promise<{
|
|
139
|
+
epoch: number;
|
|
140
|
+
credential: string;
|
|
141
|
+
} | undefined>;
|
|
142
|
+
createCircle(params: CreateCircleParams): Promise<CreateCircleResult>;
|
|
143
|
+
updateCircle(params: UpdateCircleParams): Promise<UpdateCircleResult>;
|
|
144
|
+
deleteCircle(params: DeleteCircleParams): Promise<DeleteCircleResult>;
|
|
145
|
+
addCircleMember(params: AddCircleMemberParams): Promise<AddCircleMemberResult>;
|
|
146
|
+
removeCircleMember(params: RemoveCircleMemberParams): Promise<RemoveCircleMemberResult>;
|
|
147
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{commitInvite as e,createGroup as t,createInvite as a,processWelcome as i,removeMember as r}from"@enkaku/group";import{HLC as c}from"@kubun/hlc";import{serializeGroupState as d}from"./mls-state.js";export class SpaceManager{#e;#t;#a;#i;#r;constructor(e){this.#e=e.kysely,this.#t=e.adapter,this.#a=e.identity.id,this.#i=e.getRandomID,this.#r=new c({nodeID:e.identity.id})}async #c(){return await this.#e}async #d(e,t,a){let i=d(t),r=a??await this.#c();await r.insertInto("kubun_space_mls_state").values({space_id:e,device_id:this.#a,mls_state:this.#t.encodeBinary(i.mlsState),credential:i.credential,epoch:i.epoch,root_capability:i.rootCapability}).onConflict(e=>e.columns(["space_id","device_id"]).doUpdateSet(e=>({mls_state:e.ref("excluded.mls_state"),credential:e.ref("excluded.credential"),epoch:e.ref("excluded.epoch"),root_capability:e.ref("excluded.root_capability"),updated_at:this.#t.encodeTimestamp(new Date)}))).execute()}async createSpace(e,a){let i=this.#i(),{group:r}=await t(e.identity,i),d=a??await this.#c();return await d.insertInto("kubun_spaces").values({id:i,name:e.name,description:e.description??"",created_by:e.identity.id,hub_urls:this.#t.encodeJSON(e.hubURLs??[]),hlc:c.serialize(this.#r.now())}).execute(),await this.#d(i,r,d),await d.insertInto("kubun_space_members").values({space_id:i,member_did:e.identity.id,role:"admin"}).onConflict(e=>e.columns(["space_id","member_did"]).doUpdateSet(e=>({role:e.ref("excluded.role"),updated_at:this.#t.encodeTimestamp(new Date)}))).execute(),{spaceID:i,groupHandle:r}}async updateSpace(e){let t=c.serialize(this.#r.now()),a={};null!=e.update.name&&(a.name=e.update.name),null!=e.update.description&&(a.description=e.update.description),null!=e.update.hubURLs&&(a.hub_urls=this.#t.encodeJSON(e.update.hubURLs)),a.hlc=t,a.updated_at=this.#t.encodeTimestamp(new Date);let i=await this.#c();return await i.updateTable("kubun_spaces").set(a).where("id","=",e.spaceID).execute(),{broadcast:{type:"space:update",spaceID:e.spaceID,update:{...e.update,hlc:t}}}}async inviteToSpace(t,i){let{invite:r}=await a({group:t.groupHandle,identity:t.identity,recipientDID:t.recipientDID,permission:t.permission}),{commitMessage:c,welcomeMessage:d,newGroup:s}=await e(t.groupHandle,t.recipientKeyPackage),l=i??await this.#c();return await this.#d(t.spaceID,s,l),await l.insertInto("kubun_space_members").values({space_id:t.spaceID,member_did:t.recipientDID,role:"admin"===t.permission?"admin":"member"}).onConflict(e=>e.columns(["space_id","member_did"]).doUpdateSet(e=>({role:e.ref("excluded.role"),updated_at:this.#t.encodeTimestamp(new Date)}))).execute(),{invite:r,welcomeMessage:d,commitMessage:c,updatedGroupHandle:s}}async joinSpace(e,t){let{group:a}=await i({identity:e.identity,invite:e.invite,welcome:e.welcomeMessage,keyPackageBundle:e.keyPackageBundle,ratchetTree:e.ratchetTree}),r=t??await this.#c();return await this.#d(e.spaceID,a,r),await r.insertInto("kubun_space_members").values({space_id:e.spaceID,member_did:e.identity.id,role:"member"}).onConflict(e=>e.columns(["space_id","member_did"]).doUpdateSet(e=>({role:e.ref("excluded.role"),updated_at:this.#t.encodeTimestamp(new Date)}))).execute(),{groupHandle:a}}async removeMember(e,t){let{commitMessage:a,newGroup:i}=await r(e.groupHandle,e.leafIndex),c=t??await this.#c();return await this.#d(e.spaceID,i,c),await c.deleteFrom("kubun_space_members").where("space_id","=",e.spaceID).where("member_did","=",e.memberDID).execute(),{commitMessage:a,updatedGroupHandle:i}}async removeSpaceMember(e,t){let a=e.groupHandle.findMemberLeafIndex(e.memberDID);if(null==a)throw Error(`Member ${e.memberDID} not found in MLS group`);let{commitMessage:i,newGroup:c}=await r(e.groupHandle,a),d=t??await this.#c();return await this.#d(e.spaceID,c,d),await d.deleteFrom("kubun_space_members").where("space_id","=",e.spaceID).where("member_did","=",e.memberDID).execute(),{commitMessage:i,updatedGroupHandle:c}}async leaveSpace(e,t){let a=async t=>{await t.deleteFrom("kubun_space_mls_state").where("space_id","=",e.spaceID).where("device_id","=",this.#a).execute(),await t.deleteFrom("kubun_space_members").where("space_id","=",e.spaceID).where("member_did","=",e.identity.id).execute()};if(null!=t)await a(t);else{let e=await this.#c();await e.transaction().execute(a)}}async loadSpace(e){let t=await this.#c(),a=await t.selectFrom("kubun_space_mls_state").selectAll().where("space_id","=",e).where("device_id","=",this.#a).executeTakeFirst();if(null!=a)return{epoch:a.epoch,credential:a.credential}}async createCircle(e){let t=this.#i(),a=c.serialize(this.#r.now()),i=e.catalogIDs??[],r=await this.#c();return await r.insertInto("kubun_circles").values({id:t,space_id:e.spaceID,name:e.name,description:e.description??"",catalog_ids:this.#t.encodeJSON(i),hlc:a}).execute(),{circleID:t,broadcast:{type:"circle:create",circle:{id:t,spaceID:e.spaceID,name:e.name,description:e.description??"",catalogIDs:i,hlc:a}}}}async updateCircle(e){let t=c.serialize(this.#r.now()),a=[],i=await this.#c();if(null!=e.update.catalogIDs){let t=await i.selectFrom("kubun_circles").selectAll().where("id","=",e.circleID).executeTakeFirst(),r=new Set(null!=t?t.catalog_ids:[]),c=e.update.catalogIDs.filter(e=>!r.has(e));for(let e of(await Promise.all(c.map(e=>i.selectFrom("kubun_catalogs").selectAll().where("id","=",e).executeTakeFirst()))))null!=e&&a.push({type:"catalog:create",catalog:{id:e.id,ownerDID:e.owner_did,name:e.name,description:e.description,filterCriteria:e.filter_criteria,hlc:e.hlc}})}let r={};null!=e.update.name&&(r.name=e.update.name),null!=e.update.description&&(r.description=e.update.description),null!=e.update.catalogIDs&&(r.catalog_ids=this.#t.encodeJSON(e.update.catalogIDs)),r.hlc=t,r.updated_at=this.#t.encodeTimestamp(new Date),await i.updateTable("kubun_circles").set(r).where("id","=",e.circleID).execute();let d={type:"circle:update",circleID:e.circleID,update:{...e.update,hlc:t}};return{broadcast:d,broadcasts:[d,...a]}}async deleteCircle(e){let t=c.serialize(this.#r.now()),a=await this.#c();return await a.deleteFrom("kubun_circles").where("id","=",e.circleID).execute(),{broadcast:{type:"circle:delete",circleID:e.circleID,hlc:t}}}async addCircleMember(e){let t=await this.#c();if(null==await t.selectFrom("kubun_space_members").select("member_did").where("space_id","=",e.spaceID).where("member_did","=",e.memberDID).executeTakeFirst())throw Error(`${e.memberDID} is not a member of space ${e.spaceID}`);let a=c.serialize(this.#r.now());return await t.insertInto("kubun_circle_members").values({circle_id:e.circleID,member_did:e.memberDID,role:e.role,hlc:a}).onConflict(e=>e.columns(["circle_id","member_did"]).doUpdateSet(e=>({role:e.ref("excluded.role"),hlc:e.ref("excluded.hlc")}))).execute(),{broadcast:{type:"member:add",member:{circleID:e.circleID,memberDID:e.memberDID,role:e.role,hlc:a}}}}async removeCircleMember(e){let t=c.serialize(this.#r.now()),a=await this.#c();return await a.deleteFrom("kubun_circle_members").where("circle_id","=",e.circleID).where("member_did","=",e.memberDID).execute(),{broadcast:{type:"member:remove",circleID:e.circleID,memberDID:e.memberDID,hlc:t}}}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{fromB64 as t,toB64 as e}from"@enkaku/codec";let r="\0bi:",i="\0u8:";export function replacer(t,n){if("bigint"==typeof n)return`${r}${n}`;let o=this[t];return o instanceof Uint8Array?`${i}${e(o)}`:n}export function reviver(e,n){return"string"!=typeof n?n:n.startsWith(r)?BigInt(n.slice(r.length)):n.startsWith(i)?t(n.slice(i.length)):n}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type ClientState, type GroupHandle, type MemberCredential } from '@enkaku/group';
|
|
2
|
+
/**
|
|
3
|
+
* Serialized form of a GroupHandle for DB persistence.
|
|
4
|
+
* The MLS ClientState is opaque binary; the credential is JSON.
|
|
5
|
+
*/
|
|
6
|
+
export type SerializedGroupState = {
|
|
7
|
+
mlsState: Uint8Array;
|
|
8
|
+
credential: string;
|
|
9
|
+
epoch: number;
|
|
10
|
+
rootCapability: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Serialize a GroupHandle's state for persistence.
|
|
14
|
+
* The implementer must verify the serialization approach works
|
|
15
|
+
* with the installed version of ts-mls.
|
|
16
|
+
*/
|
|
17
|
+
export declare function serializeGroupState(group: GroupHandle): SerializedGroupState;
|
|
18
|
+
export type GroupState = {
|
|
19
|
+
state: ClientState;
|
|
20
|
+
credential: MemberCredential;
|
|
21
|
+
rootCapability: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Deserialize persisted state back into GroupHandle constructor params.
|
|
25
|
+
* Returns the params needed to reconstruct a GroupHandle.
|
|
26
|
+
*/
|
|
27
|
+
export declare function deserializeGroupState(serialized: SerializedGroupState): GroupState;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{decodeClientState as t,encodeClientState as e}from"@enkaku/group";export function serializeGroupState(t){return{mlsState:e(t.state),credential:JSON.stringify(t.credential),epoch:Number(t.epoch),rootCapability:t.rootCapability}}export function deserializeGroupState(e){let r=t(e.mlsState);if(null==r)throw Error("Could not decode ClientState");return{state:r,credential:JSON.parse(e.credential),rootCapability:e.rootCapability}}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { KubunDB } from '@kubun/db';
|
|
2
|
+
export type CatalogSyncScope = {
|
|
3
|
+
modelIDs: Array<string>;
|
|
4
|
+
owners: Array<string> | undefined;
|
|
5
|
+
requiredClusterIDs: Array<string>;
|
|
6
|
+
missingClusterIDs: Array<string>;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Resolve catalog criteria to sync-compatible scopes.
|
|
10
|
+
*
|
|
11
|
+
* Takes catalog IDs, resolves their criteria, and returns:
|
|
12
|
+
* - modelIDs: union of all model filters from all catalogs
|
|
13
|
+
* - owners: union of all owner filters (explicit + circle members)
|
|
14
|
+
* - requiredClusterIDs: cluster IDs for all required models
|
|
15
|
+
* - missingClusterIDs: cluster IDs for models not in knownModelIDs
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveCatalogSyncScopes(db: KubunDB, catalogIDs: Array<string>, knownModelIDs?: Array<string>): Promise<CatalogSyncScope>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export async function resolveCatalogSyncScopes(e,r,o){let l=new Set,t=new Set,a=!1;for(let o of r){let r=await e.resolveCatalogScope(o);if(null!=r.models)for(let e of r.models)l.add(e);if(null!=r.owners)for(let e of(a=!0,r.owners))t.add(e)}let s=Array.from(l),n=a?Array.from(t):void 0,d=new Set(o??[]),f=new Set,i=new Set;return await Promise.all(s.map(async r=>{let o=await e.getClusterForModel(r);null!=o&&(f.add(o),d.has(r)||i.add(o))})),{modelIDs:s,owners:n,requiredClusterIDs:Array.from(f),missingClusterIDs:Array.from(i)}}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { KubunDB } from '@kubun/db';
|
|
2
|
+
import type { Logger } from '@kubun/logger';
|
|
3
|
+
/**
|
|
4
|
+
* Check if delegation tokens grant the viewer read access to a user's documents.
|
|
5
|
+
*
|
|
6
|
+
* @param viewerDID - The DID of the requesting viewer
|
|
7
|
+
* @param ownerDID - The DID of the document owner
|
|
8
|
+
* @param delegationTokens - Array of delegation JWT tokens
|
|
9
|
+
* @returns true if any token grants read access
|
|
10
|
+
*/
|
|
11
|
+
export declare function checkSyncDelegation(viewerDID: string, ownerDID: string, delegationTokens: Array<string>): Promise<boolean>;
|
|
12
|
+
export type CreateSyncHandlersParams = {
|
|
13
|
+
db: KubunDB;
|
|
14
|
+
logger: Logger;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Create sync protocol handlers served by plugin-p2p's own Enkaku server.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createSyncHandlers(params: CreateSyncHandlersParams): Record<string, unknown>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{checkCapability as e}from"@enkaku/capability";import{resolveCatalogSyncScopes as t}from"./catalog-scope.js";import{buildMerkleTree as n,findDivergentBuckets as r,getTimeBuckets as s}from"./merkle-tree.js";export async function checkSyncDelegation(t,n,r){if(!r||0===r.length)return!1;let s=["*","urn:kubun:user:*",`urn:kubun:user:${n}`],l="document/read";for(let o of s)try{return await e({act:l,res:o},{iss:t,sub:n,cap:r}),!0}catch{}for(let o of r)for(let r of s)try{return await e({act:l,res:r},{iss:t,sub:n,cap:o}),!0}catch{}return!1}export function createSyncHandlers(e){let{db:l,logger:o}=e;return{"sync/negotiate":async e=>{let n,{scopes:r,delegationTokens:s,catalogIDs:a,knownModelIDs:c}=e.param,i=e.message.payload,u=i.sub||i.iss;o.debug("sync/negotiate requested",{scopes:r,catalogIDs:a,viewerDID:u});let g=[];if(null!=a&&a.length>0){let e=await t(l,a,c);if(e.modelIDs.length>0)if(null!=e.owners)for(let t of e.modelIDs)for(let n of e.owners){let e=u===n,r=!e&&await checkSyncDelegation(u,n,s);(e||r)&&g.push({modelID:t,ownerDID:n})}else for(let t of e.modelIDs)for(let e of(await l.getDistinctOwnersForModel(t))){let n=u===e,r=!n&&await checkSyncDelegation(u,e,s);(n||r)&&g.push({modelID:t,ownerDID:e})}e.missingClusterIDs.length>0&&(n=await l.getClusters(e.missingClusterIDs))}if(null!=r)for(let e of r){let t=u===e.ownerDID,n=!t&&await checkSyncDelegation(u,e.ownerDID,s);t||n?g.push(e):o.debug("sync/negotiate: scope rejected",{scope:e,viewerDID:u})}let m=new Set,f=g.filter(e=>{let t=`${e.modelID}:${e.ownerDID}`;return!m.has(t)&&(m.add(t),!0)});return o.info("sync/negotiate completed",{requested:(r?.length??0)+(a?.length??0),accepted:f.length,missingClusters:null!=n?Object.keys(n).length:0}),{acceptedScopes:f,excludedDocumentIDs:[],...null!=n&&{missingClusters:n}}},"sync/merkle-sync":async e=>{let{scopes:t,excludedDocumentIDs:a,tree:c}=e.param,i=e.writable.getWriter();o.info("sync/merkle-sync started",{scopes:t,excludedDocumentIDs:a});let u=0,g=0;try{let e=await l.getDocumentIDsForScope(t,a);if(0===e.length)return await i.write({type:"complete",divergentBuckets:0,totalMutations:0}),{success:!0,divergentBuckets:0,mutationsSent:0};let m=await l.getMutationLogForDocuments(e),f=n(m),y={root:c.root??"",buckets:c},d=r(f,y);if(u=d.length,0===d.length)return o.info("sync/merkle-sync: no divergent buckets, already in sync"),await i.write({type:"complete",divergentBuckets:0,totalMutations:0}),{success:!0,divergentBuckets:0,mutationsSent:0};let p=new Set(d),w=m.filter(e=>{let{minute:t}=s(e.hlc);return p.has(t)});for(let e=0;e<w.length;e+=1e3){let t=w.slice(e,e+1e3);await i.write({type:"mutations",mutationJWTs:t.map(e=>e.mutation_jwt)}),g+=t.length}await i.write({type:"complete",divergentBuckets:u,totalMutations:g})}catch(e){throw o.error("sync/merkle-sync error",{error:e}),e}finally{try{await i.close()}catch{}}return o.info("sync/merkle-sync completed",{divergentBuckets:u,mutationsSent:g}),{success:!0,divergentBuckets:u,mutationsSent:g}}}}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { KubunDB } from '@kubun/db';
|
|
2
|
+
export type ApplySyncMutationsParams = {
|
|
3
|
+
db: KubunDB;
|
|
4
|
+
mutationJWTs: Array<string>;
|
|
5
|
+
};
|
|
6
|
+
export type ApplySyncMutationsResult = {
|
|
7
|
+
applied: number;
|
|
8
|
+
rejected: number;
|
|
9
|
+
pending: number;
|
|
10
|
+
skipped: number;
|
|
11
|
+
};
|
|
12
|
+
export declare function applySyncMutations(params: ApplySyncMutationsParams): Promise<ApplySyncMutationsResult>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{asType as t,createValidator as a}from"@enkaku/schema";import{verifyToken as i}from"@enkaku/token";import{computeMutationHash as o}from"@kubun/engine";import{DocumentID as n}from"@kubun/id";import{applyMutation as u}from"@kubun/mutation";import{documentMutation as e}from"@kubun/protocol";let r=a(e);export async function applySyncMutations(a){let{db:e,mutationJWTs:m}=a,s=0,d=0,l=0,c=0,h={db:e,validators:{}};for(let a of m){let m,p=o(a);if(await e.hasMutationHash(p)){c++;continue}try{let o=await i(a);m=t(r,o.payload)}catch{d++;continue}let _=m.sub,f=n.fromString(_).model.toString(),w=n.fromString(_);if(null==await e.getDocument(w)&&"change"===m.typ){await e.insertMutationLogEntry({mutation_hash:p,model_id:f,document_id:_,author_did:m.iss,hlc:m.hlc,mutation_jwt:a,status:"pending"}),l++;continue}try{if(await u(h,m),await e.insertMutationLogEntry({mutation_hash:p,model_id:f,document_id:_,author_did:m.iss,hlc:m.hlc,mutation_jwt:a,status:"applied"}),s++,"set"===m.typ)for(let a of(await e.getPendingMutations(_)))try{let o=await i(a.mutation_jwt),n=t(r,o.payload);await u(h,n),await e.updateMutationStatus(a.mutation_hash,"applied"),s++}catch{await e.updateMutationStatus(a.mutation_hash,"rejected"),d++}}catch{await e.insertMutationLogEntry({mutation_hash:p,model_id:f,document_id:_,author_did:m.iss,hlc:m.hlc,mutation_jwt:a,status:"rejected"}),d++}}return{applied:s,rejected:d,pending:l,skipped:c}}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { MutationLogEntry } from '@kubun/db';
|
|
2
|
+
export type MerkleTree = {
|
|
3
|
+
root: string;
|
|
4
|
+
buckets: Record<string, string>;
|
|
5
|
+
};
|
|
6
|
+
export declare function getTimeBuckets(hlc: string): {
|
|
7
|
+
year: string;
|
|
8
|
+
month: string;
|
|
9
|
+
day: string;
|
|
10
|
+
minute: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function buildMerkleTree(entries: Array<MutationLogEntry>): MerkleTree;
|
|
13
|
+
export declare function findDivergentBuckets(local: MerkleTree, remote: MerkleTree): Array<string>;
|
|
@@ -0,0 +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()}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Runtime } from '@enkaku/runtime';
|
|
2
|
+
import type { KubunDB } from '@kubun/db';
|
|
3
|
+
export type PeerConfig = {
|
|
4
|
+
peerDID: string;
|
|
5
|
+
endpoint: string;
|
|
6
|
+
mode: 'persistent' | 'on-demand';
|
|
7
|
+
allowedUsers: Array<string> | {
|
|
8
|
+
all: boolean;
|
|
9
|
+
};
|
|
10
|
+
priority: string;
|
|
11
|
+
trustLevel: 'trusted' | 'restricted';
|
|
12
|
+
};
|
|
13
|
+
export type PeerConfigWithID = PeerConfig & {
|
|
14
|
+
id: string;
|
|
15
|
+
createdAt: number;
|
|
16
|
+
updatedAt: number;
|
|
17
|
+
};
|
|
18
|
+
export type PeerRegistryParams = {
|
|
19
|
+
db: KubunDB;
|
|
20
|
+
runtime: Runtime;
|
|
21
|
+
};
|
|
22
|
+
export declare class PeerRegistry {
|
|
23
|
+
#private;
|
|
24
|
+
constructor(params: PeerRegistryParams);
|
|
25
|
+
addPeer(config: PeerConfig): Promise<void>;
|
|
26
|
+
getPeer(peerDID: string): Promise<PeerConfigWithID | undefined>;
|
|
27
|
+
listPeers(): Promise<Array<PeerConfigWithID>>;
|
|
28
|
+
updatePeer(peerDID: string, updates: Partial<PeerConfig>): Promise<void>;
|
|
29
|
+
removePeer(peerDID: string): Promise<void>;
|
|
30
|
+
isPeerAllowed(peerDID: string): Promise<boolean>;
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function e(e){var t;return{id:e.id,peerDID:e.peer_did,endpoint:e.endpoint,mode:e.mode,allowedUsers:"string"==typeof(t=e.allowed_users)?JSON.parse(t):t,priority:e.priority,trustLevel:e.trust_level,createdAt:e.created_at,updatedAt:e.updated_at}}export class PeerRegistry{#e;#t;constructor(e){this.#e=e.db,this.#t=e.runtime}async addPeer(e){let t=await this.#e.getDB();await t.insertInto("kubun_sync_peers").values({id:this.#t.getRandomID(),peer_did:e.peerDID,endpoint:e.endpoint,mode:e.mode,allowed_users:JSON.stringify(e.allowedUsers),priority:e.priority,trust_level:e.trustLevel,config:JSON.stringify({}),created_at:Date.now(),updated_at:Date.now()}).onConflict(t=>t.column("peer_did").doUpdateSet({endpoint:e.endpoint,mode:e.mode,allowed_users:JSON.stringify(e.allowedUsers),priority:e.priority,trust_level:e.trustLevel,updated_at:Date.now()})).execute()}async getPeer(t){let r=await this.#e.getDB(),i=await r.selectFrom("kubun_sync_peers").selectAll().where("peer_did","=",t).executeTakeFirst();if(i)return e(i)}async listPeers(){let t=await this.#e.getDB();return(await t.selectFrom("kubun_sync_peers").selectAll().execute()).map(t=>e(t))}async updatePeer(e,t){let r=await this.getPeer(e);if(!r)throw Error(`Peer ${e} not found`);let i={...r,...t},s=await this.#e.getDB();await s.updateTable("kubun_sync_peers").set({endpoint:i.endpoint,mode:i.mode,allowed_users:JSON.stringify(i.allowedUsers),priority:i.priority,trust_level:i.trustLevel,config:JSON.stringify({}),updated_at:Date.now()}).where("peer_did","=",e).execute()}async removePeer(e){let t=await this.#e.getDB();await t.deleteFrom("kubun_sync_peers").where("peer_did","=",e).execute()}async isPeerAllowed(e){return void 0!==await this.getPeer(e)}}
|