@kubun/plugin-p2p 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/context/delegation.d.ts +4 -0
- package/lib/context/delegation.js +1 -0
- package/lib/context/group.js +1 -1
- package/lib/context/join.js +1 -1
- package/lib/context/types.d.ts +16 -2
- package/lib/groups/broadcast-service.d.ts +60 -1
- package/lib/groups/broadcast-service.js +1 -1
- package/lib/groups/broadcast.d.ts +50 -0
- package/lib/groups/broadcast.js +1 -1
- package/lib/groups/events.d.ts +15 -5
- package/lib/groups/events.js +1 -1
- package/lib/groups/group-handle-registry.js +1 -1
- package/lib/groups/invite-payload.d.ts +16 -0
- package/lib/groups/join-utils.d.ts +68 -2
- package/lib/groups/join-utils.js +1 -1
- package/lib/groups/manager.d.ts +6 -4
- package/lib/groups/manager.js +1 -1
- package/lib/groups/store-received-grant.d.ts +47 -0
- package/lib/groups/store-received-grant.js +1 -0
- package/lib/groups/store-received-revocation.d.ts +46 -0
- package/lib/groups/store-received-revocation.js +1 -0
- package/lib/groups/wire-frame.d.ts +32 -0
- package/lib/groups/wire-frame.js +1 -0
- package/lib/hub/forward-remote-broadcast.d.ts +3 -3
- package/lib/hub/forward-remote-broadcast.js +1 -1
- package/lib/hub/group-channel.d.ts +8 -0
- package/lib/hub/group-channel.js +1 -1
- package/lib/hub/hub-connection.js +1 -1
- package/lib/hub/receive-handler.js +1 -1
- package/lib/hub/relay-manager.d.ts +7 -0
- package/lib/hub/relay-manager.js +1 -1
- package/lib/hub/send-handler.d.ts +16 -0
- package/lib/hub/send-handler.js +1 -1
- package/lib/hub/wiring.d.ts +14 -2
- package/lib/hub/wiring.js +1 -1
- package/lib/index.d.ts +1 -2
- package/lib/index.js +1 -1
- package/lib/schema.d.ts +2 -2
- package/lib/schema.js +47 -3
- package/lib/sync/broadcast-sender.js +1 -1
- package/lib/sync/forwarder.d.ts +1 -0
- package/lib/sync/forwarder.js +1 -1
- package/lib/sync/handlers.d.ts +5 -1
- package/lib/sync/handlers.js +1 -1
- package/lib/sync/merkle-apply.js +1 -1
- package/lib/types.d.ts +92 -1
- package/package.json +40 -40
- package/lib/sync/catalog-match.d.ts +0 -13
- package/lib/sync/catalog-match.js +0 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Logger } from '@kubun/logger';
|
|
2
|
+
import type { P2PStoreAPI } from '@kubun/store-p2p';
|
|
3
|
+
import type { P2PEventEmitter } from './events.js';
|
|
4
|
+
export type StoreReceivedGrantParams = {
|
|
5
|
+
/** P2P store the held row is written to. */
|
|
6
|
+
p2pStore: P2PStoreAPI;
|
|
7
|
+
/** Stringified capability JWT carrying the `document/write` grant. */
|
|
8
|
+
token: string;
|
|
9
|
+
/** Group the grant belongs to. */
|
|
10
|
+
groupID: string;
|
|
11
|
+
/**
|
|
12
|
+
* HLC stamped on the held row. For broadcast-delivered grants this mirrors
|
|
13
|
+
* the grantor-side issued row; for invite-delivered grants the joiner stamps
|
|
14
|
+
* its own HLC at apply time (no grantor HLC travels in the invite).
|
|
15
|
+
*/
|
|
16
|
+
hlc: string;
|
|
17
|
+
/**
|
|
18
|
+
* DID of the receiving device. The grant is only stored when the token's
|
|
19
|
+
* audience (`aud`) matches this DID — co-members relay the same grant but
|
|
20
|
+
* ignore tokens addressed to someone else.
|
|
21
|
+
*/
|
|
22
|
+
selfDID: string;
|
|
23
|
+
/**
|
|
24
|
+
* Optional emitter notified after a held row is successfully written.
|
|
25
|
+
* Consumers (e.g. the `ownDelegationTokenAdded` subscription) use this to
|
|
26
|
+
* surface freshly-arrived tokens to local subscribers.
|
|
27
|
+
*/
|
|
28
|
+
emitter?: P2PEventEmitter;
|
|
29
|
+
/** Optional logger — warns on verification failure or missing claims. */
|
|
30
|
+
logger?: Logger;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Verify a received capability grant and, when it is addressed to this device,
|
|
34
|
+
* store it as a held row so the engine's auto-attach can reuse it on later
|
|
35
|
+
* mutations.
|
|
36
|
+
*
|
|
37
|
+
* Returns `true` when the held row was inserted or materially changed (LWW
|
|
38
|
+
* upsert with a newer hlc and at least one differing content field). Returns
|
|
39
|
+
* `false` when no row change occurred — verification failure, audience
|
|
40
|
+
* mismatch, missing required claims, LWW lost against an existing row, or an
|
|
41
|
+
* idempotent re-broadcast with identical content. Never throws on a forged or
|
|
42
|
+
* malformed token — it is skipped like an unverifiable broadcast entry.
|
|
43
|
+
*
|
|
44
|
+
* Shared by the `delegation:share` broadcast path and the invite-bootstrap
|
|
45
|
+
* path so both apply identical verify + audience + null-guard + coercion logic.
|
|
46
|
+
*/
|
|
47
|
+
export declare function storeReceivedGrant(params: StoreReceivedGrantParams): Promise<boolean>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{verifyToken as e}from"@enkaku/token";export async function storeReceivedGrant(i){let t,{p2pStore:a,token:n,groupID:r,hlc:o,selfDID:d,emitter:p,logger:s}=i;try{t=(await e(n)).payload}catch(e){return s?.warn("received grant token verification failed, skipping",{groupID:r,error:e}),!1}if(t.aud!==d)return!1;if(null==t.jti||null==t.exp)return s?.warn("received grant token missing jti/exp, skipping",{groupID:r}),!1;let u="string"==typeof t.res?t.res:JSON.stringify(t.res),c="string"==typeof t.act?t.act:JSON.stringify(t.act),g=await a.addDelegationToken({jti:t.jti,grantor:t.sub,audience:t.aud,token:n,resource:u,act:c,exp:t.exp,group_id:r,hlc:o});if(g){let e=await a.getPendingRevocationByJti(t.jti);if(null!=e)if(e.revoker_did===t.sub){let i=Math.floor(Date.now()/1e3);await a.markRevocationVerified(t.jti,{cap_exp:t.exp,verified_at:i})&&null!=p&&await p.emit("delegationTokenRevoked",{jti:t.jti,grantor:t.sub,audience:t.aud,revokerDID:e.revoker_did,revokedAt:e.revoked_iat,verifiedAt:i,capExp:t.exp,groupID:r})}else s?.warn("pending revocation iss does not match arriving cap, dropping",{jti:t.jti,pendingIss:e.revoker_did,capGrantor:t.sub,groupID:r}),await a.deletePendingRevocation(t.jti)}return g&&null!=p&&await p.emit("delegationTokenAdded",{jwt:n,jti:t.jti,grantor:t.sub,audience:t.aud,resource:u,exp:t.exp}),g}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Logger } from '@kubun/logger';
|
|
2
|
+
import type { P2PStoreAPI } from '@kubun/store-p2p';
|
|
3
|
+
import type { P2PEventEmitter } from './events.js';
|
|
4
|
+
export type StoreReceivedRevocationParams = {
|
|
5
|
+
/** P2P store the revocation row is written to. */
|
|
6
|
+
p2pStore: P2PStoreAPI;
|
|
7
|
+
/** Stringified signed revocation JWT carrying `{ jti, iss, rev, iat }`. */
|
|
8
|
+
token: string;
|
|
9
|
+
/** Group the revocation belongs to. */
|
|
10
|
+
groupID: string;
|
|
11
|
+
/** HLC stamped on the revocation row for LWW arbitration. */
|
|
12
|
+
hlc: string;
|
|
13
|
+
/**
|
|
14
|
+
* DID of the receiving device. Currently unused — revocations are
|
|
15
|
+
* audience-agnostic at store time — but accepted for symmetry with
|
|
16
|
+
* `storeReceivedGrant` and to keep call sites uniform.
|
|
17
|
+
*/
|
|
18
|
+
selfDID?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Optional emitter passed through for symmetry with `storeReceivedGrant`.
|
|
21
|
+
* This helper does not emit; downstream subscription wiring lives elsewhere.
|
|
22
|
+
*/
|
|
23
|
+
emitter?: P2PEventEmitter;
|
|
24
|
+
/** Optional logger — debug on verification failure or malformed payload, warn on iss/grantor mismatch. */
|
|
25
|
+
logger?: Logger;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Verify a received revocation token and write its row to the P2P store.
|
|
29
|
+
*
|
|
30
|
+
* Verified-on-arrival: when the referenced capability is already known
|
|
31
|
+
* locally and the revocation's `iss` matches that capability's `grantor`,
|
|
32
|
+
* the row is stored with `verified_at` populated so the hot-path
|
|
33
|
+
* `isRevoked` gate fires immediately. When the capability is unknown
|
|
34
|
+
* locally, the row is stored as pending (`verified_at` null) until a later
|
|
35
|
+
* `delegation:share` triggers a cross-check that flips it via
|
|
36
|
+
* `markRevocationVerified`. A signature-verified token whose `iss` does
|
|
37
|
+
* not match a known capability's `grantor` is rejected outright — a forged
|
|
38
|
+
* revocation must not pollute the local store.
|
|
39
|
+
*
|
|
40
|
+
* Returns `true` when the row was inserted or materially changed (LWW
|
|
41
|
+
* upsert). Returns `false` on verification failure, non-revocation
|
|
42
|
+
* payload, iss/grantor mismatch, or a lost/idempotent LWW upsert. Never
|
|
43
|
+
* throws on a forged or malformed token — it is skipped like an
|
|
44
|
+
* unverifiable broadcast entry.
|
|
45
|
+
*/
|
|
46
|
+
export declare function storeReceivedRevocation(params: StoreReceivedRevocationParams): Promise<boolean>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{verifyToken as e}from"@enkaku/token";export async function storeReceivedRevocation(o){let r,{p2pStore:t,token:n,groupID:i,hlc:a,emitter:c,logger:d}=o;try{r=(await e(n)).payload}catch(e){return d?.debug("received revocation token verification failed, skipping",{groupID:i,error:e}),!1}if(!0!==r.rev)return d?.debug("received revocation token is not a revocation record, skipping",{groupID:i}),!1;let{jti:l,iss:v,iat:k}=r,u=await t.getDelegationTokenByJti(l),g=null,p=null;if(null!=u){if(u.grantor!==v)return d?.warn("received revocation iss does not match known capability grantor, skipping",{groupID:i,jti:l,iss:v,grantor:u.grantor}),!1;g=Math.floor(Date.now()/1e3),p=u.exp}let f=await t.addRevocation({jti:l,revoker_did:v,revoked_iat:k,revocation_token:n,verified_at:g,cap_exp:p,group_id:i,hlc:a});return f&&null!=g&&null!=u&&null!=c&&await c.emit("delegationTokenRevoked",{jti:l,grantor:u.grantor,audience:u.audience,revokerDID:v,revokedAt:k,verifiedAt:g,capExp:p,groupID:i}),f}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outer wire-frame discriminator for group hub payloads.
|
|
3
|
+
*
|
|
4
|
+
* The hub relay treats a group send payload as opaque bytes. Kubun prepends a
|
|
5
|
+
* single `kind` byte so the receiver can route a payload to the right decoder
|
|
6
|
+
* without trial-decryption:
|
|
7
|
+
*
|
|
8
|
+
* - `app` — an MLS-encrypted application broadcast (`GroupBroadcastMessage`),
|
|
9
|
+
* decoded via `handle.decrypt` + `deserializeBroadcast`.
|
|
10
|
+
* - `mls` — a framed MLS handshake message (a Commit), fed straight to
|
|
11
|
+
* `handle.processMessage`. The handshake is self-protected (encrypted
|
|
12
|
+
* `PrivateMessage`), so it is NOT wrapped in the application-encryption path.
|
|
13
|
+
*
|
|
14
|
+
* The discriminator lives entirely in kubun's payload bytes; the hub protocol
|
|
15
|
+
* is unchanged.
|
|
16
|
+
*/
|
|
17
|
+
export declare const WireKind: {
|
|
18
|
+
readonly app: 0;
|
|
19
|
+
readonly mls: 1;
|
|
20
|
+
};
|
|
21
|
+
export type WireKind = (typeof WireKind)[keyof typeof WireKind];
|
|
22
|
+
/** Prepend the `kind` byte to a payload. */
|
|
23
|
+
export declare function frameWire(kind: WireKind, payload: Uint8Array): Uint8Array;
|
|
24
|
+
/**
|
|
25
|
+
* Split a framed payload into its `kind` and the remaining payload bytes.
|
|
26
|
+
* Throws on an empty frame or an unrecognized `kind` byte. The returned
|
|
27
|
+
* payload is a view (`subarray`) over the input, not a copy.
|
|
28
|
+
*/
|
|
29
|
+
export declare function unframeWire(framed: Uint8Array): {
|
|
30
|
+
kind: WireKind;
|
|
31
|
+
payload: Uint8Array;
|
|
32
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const WireKind={app:0,mls:1};export function frameWire(r,e){let n=new Uint8Array(e.length+1);return n[0]=r,n.set(e,1),n}export function unframeWire(r){if(0===r.length)throw Error("empty wire frame");let e=r[0];if(e!==WireKind.app&&e!==WireKind.mls)throw Error(`unknown wire frame kind: ${e}`);return{kind:e,payload:r.subarray(1)}}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Logger } from '@kubun/logger';
|
|
2
2
|
import type { BroadcastEvent } from '../groups/broadcast-service.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { P2PEventEmitter } from '../groups/events.js';
|
|
4
4
|
/**
|
|
5
5
|
* Map a `BroadcastEvent` from `BroadcastService` into the matching
|
|
6
|
-
* `
|
|
6
|
+
* `P2PEventEmitter` event.
|
|
7
7
|
*
|
|
8
8
|
* - Events with `applied === false` are ignored (no state change).
|
|
9
9
|
* - `catalog:*` messages are currently a no-op. Catalog send-side broadcasts
|
|
@@ -12,4 +12,4 @@ import type { GroupEventEmitter } from '../groups/events.js';
|
|
|
12
12
|
* captures at the appropriate moment (after-state for create / update,
|
|
13
13
|
* before-state for delete / remove).
|
|
14
14
|
*/
|
|
15
|
-
export declare function forwardRemoteBroadcast(emitter:
|
|
15
|
+
export declare function forwardRemoteBroadcast(emitter: P2PEventEmitter, event: BroadcastEvent, logger?: Logger): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{toISO as e}from"../context/types.js";export async function forwardRemoteBroadcast(r,t,a){if(!t.applied)return;let{message:i,affected:c}=t;if(null==c)return void a?.warn("remote broadcast applied but affected row missing",{type:i.type});switch(i.type){case"circle:create":{if("circle"!==c.kind)return;let{row:t}=c;await r.emit("circleCreated",{id:t.id,groupID:t.group_id,name:t.name,description:t.description,createdAt:e(t.created_at)});return}case"circle:update":{if("circle"!==c.kind)return;let{row:t}=c,a=null!=i.update.catalogIDs?"circleCatalogsChanged":"circleDataChanged";await r.emit(a,{id:t.id,groupID:t.group_id,name:t.name,description:t.description,createdAt:e(t.created_at),circleID:t.id});return}case"circle:delete":{if("circle"!==c.kind)return;let{row:t}=c;await r.emit("circleDeleted",{id:t.id,groupID:t.group_id,name:t.name,description:t.description,createdAt:e(t.created_at)});return}case"member:add":{if("member"!==c.kind)return;let{row:t}=c;await r.emit("circleMemberAdded",{circleID:t.circle_id,memberDID:t.member_did,role:t.role,createdAt:e(t.created_at)});return}case"member:remove":{if("member"!==c.kind)return;let{row:t}=c;await r.emit("circleMemberRemoved",{circleID:t.circle_id,memberDID:t.member_did,role:t.role,createdAt:e(t.created_at)});return}case"group:update":{if("group"!==c.kind)return;let{row:t}=c;await r.emit("groupDataChanged",{id:t.id,name:t.name,description:t.description,createdBy:t.created_by,createdAt:e(t.created_at),groupID:t.id});return}case"catalog:create":case"catalog:update":case"catalog:delete":return void a?.debug("catalog broadcast ignored (not yet wired)",{type:i.type})}}
|
|
1
|
+
import{toISO as e}from"../context/types.js";export async function forwardRemoteBroadcast(r,t,a){if(!t.applied)return;let{message:i,affected:c}=t;if("delegation:share"!==i.type){if(null==c)return void a?.warn("remote broadcast applied but affected row missing",{type:i.type});switch(i.type){case"circle:create":{if("circle"!==c.kind)return;let{row:t}=c;await r.emit("circleCreated",{id:t.id,groupID:t.group_id,name:t.name,description:t.description,createdAt:e(t.created_at)});return}case"circle:update":{if("circle"!==c.kind)return;let{row:t}=c,a=null!=i.update.catalogIDs?"circleCatalogsChanged":"circleDataChanged";await r.emit(a,{id:t.id,groupID:t.group_id,name:t.name,description:t.description,createdAt:e(t.created_at),circleID:t.id});return}case"circle:delete":{if("circle"!==c.kind)return;let{row:t}=c;await r.emit("circleDeleted",{id:t.id,groupID:t.group_id,name:t.name,description:t.description,createdAt:e(t.created_at)});return}case"member:add":{if("member"!==c.kind)return;let{row:t}=c;await r.emit("circleMemberAdded",{circleID:t.circle_id,memberDID:t.member_did,role:t.role,createdAt:e(t.created_at)});return}case"member:remove":{if("member"!==c.kind)return;let{row:t}=c;await r.emit("circleMemberRemoved",{circleID:t.circle_id,memberDID:t.member_did,role:t.role,createdAt:e(t.created_at)});return}case"group:update":{if("group"!==c.kind)return;let{row:t}=c;await r.emit("groupDataChanged",{id:t.id,name:t.name,description:t.description,createdBy:t.created_by,createdAt:e(t.created_at),groupID:t.id});return}case"catalog:create":case"catalog:update":case"catalog:delete":return void a?.debug("catalog broadcast ignored (not yet wired)",{type:i.type})}}}
|
|
@@ -56,4 +56,12 @@ export declare class GroupChannel extends Disposer {
|
|
|
56
56
|
broadcast(message: GroupBroadcastMessage, opts?: {
|
|
57
57
|
timeoutMs?: number;
|
|
58
58
|
}): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Fan out a pre-produced MLS handshake Commit to the group's existing
|
|
61
|
+
* members. Mirrors {@link broadcast} but routes through `handleSendCommit`
|
|
62
|
+
* (no application encryption — the Commit is already self-protected).
|
|
63
|
+
*/
|
|
64
|
+
sendCommit(commitBytes: Uint8Array, opts?: {
|
|
65
|
+
timeoutMs?: number;
|
|
66
|
+
}): Promise<void>;
|
|
59
67
|
}
|
package/lib/hub/group-channel.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Disposer as e}from"@enkaku/async";import{handleReceivedMessage as t}from"./receive-handler.js";import{handleSendBroadcast as r}from"./send-handler.js";export class AckBatch{#e;#t;#r;#i;#s=new Set;#o=null;constructor(e){this.#e=e.flushMs,this.#t=e.flushMax,this.#r=e.send,this.#i=e.logger}add(e){let t=0===this.#s.size;if(this.#s.add(e),this.#s.size>=this.#t){null!==this.#o&&(clearTimeout(this.#o),this.#o=null),this.flush();return}t&&null===this.#o&&(this.#o=setTimeout(()=>{this.#o=null,this.flush()},this.#e))}async flush(){if(null!==this.#o&&(clearTimeout(this.#o),this.#o=null),0===this.#s.size)return;let e=Array.from(this.#s);this.#s.clear();try{await this.#r(e)}catch(t){this.#i?.error("ack batch send failed",{error:t,count:e.length})}}dispose(){null!==this.#o&&(clearTimeout(this.#o),this.#o=null),this.#s.clear()}}export function computeBackoff(e,t,r,i){let s=Math.min(t*2**e*(1+i*(2*Math.random()-1)),r);return s<0?0:s}export function abortableSleep(e,t){return new Promise((r,i)=>{if(t.aborted)return void i(t.reason??Error("aborted"));let s=setTimeout(()=>{t.removeEventListener("abort",o),r()},e),o=()=>{clearTimeout(s),t.removeEventListener("abort",o),i(t.reason??Error("aborted"))};t.addEventListener("abort",o)})}export class GroupChannel extends e{#n;#h;#a;#u;#c;#i;#
|
|
1
|
+
import{Disposer as e}from"@enkaku/async";import{handleReceivedMessage as t}from"./receive-handler.js";import{handleSendBroadcast as r,handleSendCommit as i}from"./send-handler.js";export class AckBatch{#e;#t;#r;#i;#s=new Set;#o=null;constructor(e){this.#e=e.flushMs,this.#t=e.flushMax,this.#r=e.send,this.#i=e.logger}add(e){let t=0===this.#s.size;if(this.#s.add(e),this.#s.size>=this.#t){null!==this.#o&&(clearTimeout(this.#o),this.#o=null),this.flush();return}t&&null===this.#o&&(this.#o=setTimeout(()=>{this.#o=null,this.flush()},this.#e))}async flush(){if(null!==this.#o&&(clearTimeout(this.#o),this.#o=null),0===this.#s.size)return;let e=Array.from(this.#s);this.#s.clear();try{await this.#r(e)}catch(t){this.#i?.error("ack batch send failed",{error:t,count:e.length})}}dispose(){null!==this.#o&&(clearTimeout(this.#o),this.#o=null),this.#s.clear()}}export function computeBackoff(e,t,r,i){let s=Math.min(t*2**e*(1+i*(2*Math.random()-1)),r);return s<0?0:s}export function abortableSleep(e,t){return new Promise((r,i)=>{if(t.aborted)return void i(t.reason??Error("aborted"));let s=setTimeout(()=>{t.removeEventListener("abort",o),r()},e),o=()=>{clearTimeout(s),t.removeEventListener("abort",o),i(t.reason??Error("aborted"))};t.addEventListener("abort",o)})}export class GroupChannel extends e{#n;#h;#a;#u;#c;#i;#d=!1;constructor(e){super({dispose:async()=>{await this.#l()}}),this.#n=e.groupID,this.#h=e.deviceID,this.#a=e.hubConnection,this.#u=e.broadcastService,this.#c=e.p2pStore,this.#i=e.logger}async open(){if(this.#d)throw Error("GroupChannel already opened");let e=await this.#c.getMLSState(this.#n,this.#h);if(null==e)throw Error(`MLS state missing for group ${this.#n}; cannot join hub group`);await this.#a.subscribe(this.#n,e.credential,e=>this.#p(e)),this.#d=!0}#p(e){return t({groupID:this.#n,message:e,broadcastService:this.#u,logger:this.#i})}async close(){this.#d&&await this.dispose()}async #l(){if(this.#d){try{await this.#a.unsubscribe(this.#n)}catch(e){this.#i?.warn("hub-connection unsubscribe failed during close",{groupID:this.#n,error:e})}this.#d=!1}}broadcast(e,t){return this.#d?this.#a.runInGroup(this.#n,async()=>{if(!this.#d)throw Error(`GroupChannel for ${this.#n} is not ready for broadcast`);await r({groupID:this.#n,message:e,broadcastService:this.#u,send:(e,r)=>this.#a.broadcast(e,r,{timeoutMs:t?.timeoutMs}),logger:this.#i})},{timeoutMs:t?.timeoutMs}):Promise.reject(Error(`GroupChannel for ${this.#n} is not ready for broadcast`))}sendCommit(e,t){return this.#d?this.#a.runInGroup(this.#n,async()=>{if(!this.#d)throw Error(`GroupChannel for ${this.#n} is not ready for sendCommit`);await i({groupID:this.#n,commitBytes:e,broadcastService:this.#u,send:(e,r)=>this.#a.broadcast(e,r,{timeoutMs:t?.timeoutMs}),logger:this.#i})},{timeoutMs:t?.timeoutMs}):Promise.reject(Error(`GroupChannel for ${this.#n} is not ready for sendCommit`))}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Disposer as e}from"@enkaku/async";import{fromB64 as t,toB64 as s}from"@enkaku/codec";import{EventEmitter as n}from"@enkaku/event";import{createMutex as i}from"../util/mutex.js";import{ReconnectingError as r}from"./errors.js";import{AckBatch as o,abortableSleep as h,computeBackoff as a}from"./group-channel.js";import{TunnelInbox as c}from"./tunnel-inbox.js";import{waitForGate as l}from"./wait-for-gate.js";let u=Symbol("disposed");export class HubConnection extends e{#e;#t;#s;#n;#i;#r;#o;#h;#a;#c=null;#l=null;#u=null;#b=null;#p=null;#d=new Map;#f=!1;#g=0;#m=null;#
|
|
1
|
+
import{Disposer as e}from"@enkaku/async";import{fromB64 as t,toB64 as s}from"@enkaku/codec";import{EventEmitter as n}from"@enkaku/event";import{createMutex as i}from"../util/mutex.js";import{ReconnectingError as r}from"./errors.js";import{AckBatch as o,abortableSleep as h,computeBackoff as a}from"./group-channel.js";import{TunnelInbox as c}from"./tunnel-inbox.js";import{waitForGate as l}from"./wait-for-gate.js";let u=Symbol("disposed");export class HubConnection extends e{#e;#t;#s;#n;#i;#r;#o;#h;#a;#c=null;#l=null;#u=null;#b=null;#p=null;#d=new Map;#f=!1;#g=0;#m=null;#y=new Map;#w;constructor(e){super({dispose:async()=>{await this.#L()}}),this.#e=e.hubURL,this.#t=e.deviceID,this.#s=e.pool,this.#n=e.logger,this.#i=e.ackFlushMs??500,this.#r=e.ackFlushMax??10,this.#o=e.backoffBaseMs??1e3,this.#h=e.backoffMaxMs??6e4,this.#a=e.backoffJitter??.25,this.#w=new n}get events(){return this.#w}async #I(e){try{await this.#w.emit("lifecycle",e)}catch(t){this.#n?.warn("hub-connection lifecycle subscriber threw",{hubURL:this.#e,type:e.type,error:t})}}async open(){if(this.#f)throw Error("HubConnection already opened");try{await this.#k(),this.#f=!0,await this.#I({type:"connected"})}catch(e){if(null!=this.#l){try{await this.#s.release(this.#e,this.#l)}catch(e){this.#n?.debug("hub-connection pool release after open failure failed",{hubURL:this.#e,error:e})}this.#l=null}throw this.#c=null,e}}async #k(){if(null!=this.#l){let e=this.#l;this.#l=null,this.#c=null;try{await this.#s.release(this.#e,e)}catch(e){this.#n?.debug("hub-connection pool release before reconnect failed",{hubURL:this.#e,error:e})}}let{client:e,refID:t}=await this.#s.acquire(this.#e);this.#c=e,this.#l=t;let s=e.createChannel("hub/receive",{param:{},signal:this.signal});this.#u=s,s.catch(()=>{}),this.#b=new o({flushMs:this.#i,flushMax:this.#r,send:async e=>{await s.send({ack:e})},logger:this.#n}),this.#p=this.#R(e).catch(e=>{this.#n?.debug("hub-connection receive task tail error",{hubURL:this.#e,error:e})});let n=[...this.#d.entries()];if(n.length>0){let t=await Promise.allSettled(n.map(async([t,s])=>{await e.request("hub/group/join",{param:{groupID:t,credential:s.credential}})}));for(let e=0;e<t.length;e++){let s=t[e];if("rejected"===s.status){let t=n[e][0];this.#n?.warn("hub-connection rejoin failed",{hubURL:this.#e,groupID:t,error:s.reason})}}}}async subscribe(e,t,s){if(await l(this.#m,void 0),!this.#f||null==this.#c)throw Error("HubConnection not open");if(this.#d.has(e))throw Error(`already subscribed to group ${e}`);await this.#c.request("hub/group/join",{param:{groupID:e,credential:t}}),this.#d.set(e,{credential:t,handler:s,mutex:i()})}async unsubscribe(e){await l(this.#m,void 0);let t=this.#c;if(!this.#f||null==t)throw Error("HubConnection not open");let s=this.#d.get(e);if(null==s)throw Error(`not subscribed to group ${e}`);await s.mutex.run(async()=>{}),await t.request("hub/group/leave",{param:{groupID:e}}),this.#d.delete(e)}async broadcast(e,t,s){if(await l(this.#m,s?.timeoutMs,()=>new r(this.#e,this.#g)),!this.#f||null==this.#c)throw Error("HubConnection not open");await this.#c.request("hub/group/send",{param:{groupID:e,payload:t}})}async send(e){if(await l(this.#m,e.timeoutMs,()=>new r(this.#e,this.#g)),!this.#f||null==this.#c)throw Error("HubConnection not open");return{sequenceID:(await this.#c.request("hub/send",{param:{recipients:e.recipients,payload:s(e.payload)}})).sequenceID}}async runInGroup(e,t,s){if(await l(this.#m,s?.timeoutMs,()=>new r(this.#e,this.#g)),!this.#f)throw Error("HubConnection not open");let n=this.#d.get(e);if(null==n)throw Error(`not subscribed to group ${e}`);return n.mutex.run(t)}tunnelMessageStream({peerDID:e}){if(this.#y.has(e))throw Error("tunnelMessageStream: peerDID already registered");let t=new c,s={inbox:t};this.#y.set(e,s);let n=!1;return{[Symbol.asyncIterator]:()=>t.iterator(),return:()=>{n||(n=!0,this.#y.get(e)===s&&this.#y.delete(e),t.close())}}}async #R(e){let t=this.#u;if(null==t)return;let s=t.readable.getReader(),n=null;try{for(;!this.signal.aborted;){let t=s.read(),n=e.disposed.then(()=>u),i=await Promise.race([t,n]);if("symbol"==typeof i){this.#n?.debug("hub-connection receive loop observed hub client disposal",{hubURL:this.#e});break}let{done:r,value:o}=i;if(r)break;this.#D(o),this.#g=0}}catch(e){n=e,this.#n?.warn("hub-connection receive loop error",{hubURL:this.#e,error:e})}finally{try{s.releaseLock()}catch{}}if(!this.signal.aborted){let e=n instanceof Error&&n.message?n.message:"receive loop ended";await this.#I({type:"disconnected",reason:e}),this.#U()}}#U(){this.#p=this.#M().catch(e=>{this.#n?.debug("hub-connection reconnect task tail error",{hubURL:this.#e,error:e})})}async #M(){let e=()=>{};this.#m=new Promise(t=>{e=t});try{for(;!this.signal.aborted;){let e=a(this.#g++,this.#o,this.#h,this.#a);this.#n?.debug("hub-connection reconnect scheduled",{hubURL:this.#e,delay:e,attempt:this.#g});try{await h(e,this.signal)}catch{return}await this.#I({type:"reconnecting",attempt:this.#g,delayMs:e});try{this.#b?.dispose(),this.#b=null,this.#u=null,await this.#k(),this.#g=0,await this.#I({type:"connected"});return}catch(e){this.#n?.warn("hub-connection reconnect attempt failed",{hubURL:this.#e,attempt:this.#g,error:e}),await this.#I({type:"error",error:e})}}}finally{this.#m=null,e()}}#D(e){if(null==e.groupID){let s=this.#y.get(e.senderDID);if(null!=s){let n={sequenceID:e.sequenceID,senderDID:e.senderDID,payload:t(e.payload)};s.inbox.push(n),this.#b?.add(e.sequenceID);return}this.#n?.debug("direct message received, no handler",{hubURL:this.#e,sequenceID:e.sequenceID}),this.#b?.add(e.sequenceID);return}let s=this.#d.get(e.groupID);null==s?this.#n?.debug("message for unsubscribed group",{hubURL:this.#e,groupID:e.groupID,sequenceID:e.sequenceID}):s.mutex.run(()=>s.handler(e)).then(t=>{t.ack&&this.#b?.add(e.sequenceID)}).catch(t=>{this.#n?.warn("hub-connection subscription handler error",{hubURL:this.#e,groupID:e.groupID,sequenceID:e.sequenceID,error:t})})}async close(){this.#f&&await this.dispose()}async #L(){if(!this.#f)return;let e=this.#b;if(this.#b=null,null!=e){try{await e.flush()}catch(e){this.#n?.debug("hub-connection ack flush during close failed",{hubURL:this.#e,error:e})}e.dispose()}try{this.#u?.close()}catch{}try{await this.#p}catch{}let t=[...this.#d.values()].map(e=>e.mutex.run(async()=>{}).catch(()=>{}));if(await Promise.allSettled(t),null!=this.#l){try{await this.#s.release(this.#e,this.#l)}catch(e){this.#n?.debug("hub-connection pool release during close failed",{hubURL:this.#e,error:e})}this.#l=null}for(let e of(this.#c=null,this.#u=null,this.#p=null,this.#d.clear(),this.#y.values()))e.inbox.close();this.#y.clear(),this.#f=!1}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fromB64 as e}from"@enkaku/codec";import{ApplyError as r,DecryptError as
|
|
1
|
+
import{fromB64 as e}from"@enkaku/codec";import{ApplyError as r,CommitOutOfOrderError as c,DecryptError as s}from"../groups/broadcast-service.js";export async function handleReceivedMessage(a){let{groupID:n,message:o,broadcastService:i,logger:t}=a;if(o.groupID!==n)return t?.debug("hub message groupID mismatch, skipping",{groupID:n,messageGroupID:o.groupID,sequenceID:o.sequenceID}),{ack:!1};let u=e(o.payload);try{await i.processReceived({groupID:n,encrypted:u})}catch(e){if(e instanceof s)return t?.debug("decrypt failed, acking and skipping",{groupID:n,sequenceID:o.sequenceID,error:e}),{ack:!0};if(e instanceof c)return t?.debug("mls commit out of order, will redeliver after gap fills",{groupID:n,sequenceID:o.sequenceID,messageEpoch:e.messageEpoch.toString(),handleEpoch:e.handleEpoch.toString()}),{ack:!1};if(e instanceof r)return t?.error("broadcast apply failed, will redeliver",{groupID:n,sequenceID:o.sequenceID,error:e}),{ack:!1};return t?.error("unexpected error processing hub message",{groupID:n,sequenceID:o.sequenceID,error:e}),{ack:!1}}return{ack:!0}}
|
|
@@ -115,6 +115,13 @@ export declare class HubRelayManager {
|
|
|
115
115
|
broadcast(groupID: string, message: GroupBroadcastMessage, opts?: {
|
|
116
116
|
timeoutMs?: number;
|
|
117
117
|
}): Promise<void>;
|
|
118
|
+
/**
|
|
119
|
+
* Fan out a pre-produced MLS handshake Commit to a group's existing members
|
|
120
|
+
* via its active channel. Mirrors {@link broadcast}.
|
|
121
|
+
*/
|
|
122
|
+
sendCommit(groupID: string, commitBytes: Uint8Array, opts?: {
|
|
123
|
+
timeoutMs?: number;
|
|
124
|
+
}): Promise<void>;
|
|
118
125
|
stop(): Promise<void>;
|
|
119
126
|
}
|
|
120
127
|
export {};
|
package/lib/hub/relay-manager.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{EventEmitter as e}from"@enkaku/event";import{HubConnectionPool as t}from"./connection-pool.js";import{GroupChannel as s}from"./group-channel.js";import{HubConnection as r}from"./hub-connection.js";export class HubRelayManager{#e;#t;#s;#r;#o;#a;#i;#n;#l;#c;#u;#h;#p;#g=new Map;#d=new Map;#b=new Map;#f=new Map;relayEvents;constructor(o){this.#e=o.p2pStore,this.#t=o.broadcastService,this.#s=o.deviceID,this.#o=o.logger,this.#a=o.ackFlushMs??500,this.#i=o.ackFlushMax??10,this.#n=o.backoffBaseMs??1e3,this.#l=o.backoffMaxMs??6e4,this.#c=o.backoffJitter??.25,this.#u=o.broadcastTimeoutMs;let a=o.p2pStore;this.#r=new t({createHubClient:o.createHubClient,logger:o.logger,onServerDIDObserved:o.onServerDIDObserved,resolvePinnedDID:async e=>{let t=await a.getHubByURL(e);return t?.server_did??null}}),this.#h=o.createGroupChannel??(e=>new s(e)),this.#p=o.createHubConnection??(e=>new r(e)),this.relayEvents=new e}async #M(e){let t=this.#g.get(e);if(null!=t)return t;let s=this.#p({hubURL:e,deviceID:this.#s,pool:this.#r,logger:this.#o,ackFlushMs:this.#a,ackFlushMax:this.#i,backoffBaseMs:this.#n,backoffMaxMs:this.#l,backoffJitter:this.#c});try{await s.open()}catch(e){try{await s.dispose()}catch{}throw e}let r=s.events.on("lifecycle",async t=>{let s=Object.freeze([...this.#b.entries()].filter(([,t])=>t===e).map(([e])=>e));await this.relayEvents.emit("relay",{...t,hubURL:e,groupIDs:s})});return this.#f.set(e,r),this.#g.set(e,s),s}async start(e){(await Promise.allSettled(e.map(e=>this.addGroup(e)))).forEach((t,s)=>{"rejected"===t.status&&this.#o?.error("addGroup failed during start",{groupID:e[s],error:t.reason})})}async addGroup(e){if(this.#d.has(e))return void this.#o?.debug("group already added",{groupID:e});if(null==await this.#e.getGroup(e))throw Error(`Group ${e} not found`);let t=await this.#e.listHubsByGroupID(e);0===t.length?this.#o?.warn("group has no hub bindings, skipping",{groupID:e}):(t.length>1&&this.#o?.warn("multi-hub not yet supported, using first",{groupID:e,hubURLs:t.map(e=>e.url)}),await this.#
|
|
1
|
+
import{EventEmitter as e}from"@enkaku/event";import{HubConnectionPool as t}from"./connection-pool.js";import{GroupChannel as s}from"./group-channel.js";import{HubConnection as r}from"./hub-connection.js";export class HubRelayManager{#e;#t;#s;#r;#o;#a;#i;#n;#l;#c;#u;#h;#p;#g=new Map;#d=new Map;#b=new Map;#f=new Map;relayEvents;constructor(o){this.#e=o.p2pStore,this.#t=o.broadcastService,this.#s=o.deviceID,this.#o=o.logger,this.#a=o.ackFlushMs??500,this.#i=o.ackFlushMax??10,this.#n=o.backoffBaseMs??1e3,this.#l=o.backoffMaxMs??6e4,this.#c=o.backoffJitter??.25,this.#u=o.broadcastTimeoutMs;let a=o.p2pStore;this.#r=new t({createHubClient:o.createHubClient,logger:o.logger,onServerDIDObserved:o.onServerDIDObserved,resolvePinnedDID:async e=>{let t=await a.getHubByURL(e);return t?.server_did??null}}),this.#h=o.createGroupChannel??(e=>new s(e)),this.#p=o.createHubConnection??(e=>new r(e)),this.relayEvents=new e}async #M(e){let t=this.#g.get(e);if(null!=t)return t;let s=this.#p({hubURL:e,deviceID:this.#s,pool:this.#r,logger:this.#o,ackFlushMs:this.#a,ackFlushMax:this.#i,backoffBaseMs:this.#n,backoffMaxMs:this.#l,backoffJitter:this.#c});try{await s.open()}catch(e){try{await s.dispose()}catch{}throw e}let r=s.events.on("lifecycle",async t=>{let s=Object.freeze([...this.#b.entries()].filter(([,t])=>t===e).map(([e])=>e));await this.relayEvents.emit("relay",{...t,hubURL:e,groupIDs:s})});return this.#f.set(e,r),this.#g.set(e,s),s}async start(e){(await Promise.allSettled(e.map(e=>this.addGroup(e)))).forEach((t,s)=>{"rejected"===t.status&&this.#o?.error("addGroup failed during start",{groupID:e[s],error:t.reason})})}async addGroup(e){if(this.#d.has(e))return void this.#o?.debug("group already added",{groupID:e});if(null==await this.#e.getGroup(e))throw Error(`Group ${e} not found`);let t=await this.#e.listHubsByGroupID(e);0===t.length?this.#o?.warn("group has no hub bindings, skipping",{groupID:e}):(t.length>1&&this.#o?.warn("multi-hub not yet supported, using first",{groupID:e,hubURLs:t.map(e=>e.url)}),await this.#m(e,t[0].url))}async #m(e,t){let s=await this.#M(t),r=this.#h({groupID:e,deviceID:this.#s,hubConnection:s,broadcastService:this.#t,p2pStore:this.#e,logger:this.#o});await r.open(),this.#d.set(e,r),this.#b.set(e,t)}async addBinding(e,t){let s=this.#b.get(e);null!=s?s===t?this.#o?.debug("binding already active",{groupID:e,hubURL:t}):this.#o?.warn("multi-hub not yet supported, ignoring extra binding",{groupID:e,activeHubURL:s,newHubURL:t}):await this.#m(e,t)}async removeBinding(e,t){this.#b.get(e)!==t?this.#o?.debug("binding not active, ignoring removal",{groupID:e,hubURL:t}):await this.removeGroup(e)}async removeGroup(e){let t=this.#d.get(e);null!=t&&(await t.close(),this.#d.delete(e),this.#b.delete(e))}async broadcast(e,t,s){let r=this.#d.get(e);if(null==r)throw Error(`Group ${e} is not active in HubRelayManager`);let o=s?.timeoutMs??this.#u;await r.broadcast(t,{timeoutMs:o})}async sendCommit(e,t,s){let r=this.#d.get(e);if(null==r)throw Error(`Group ${e} is not active in HubRelayManager`);let o=s?.timeoutMs??this.#u;await r.sendCommit(t,{timeoutMs:o})}async stop(){for(let e of this.#f.values())e();this.#f.clear();let e=[...this.#d.values()];this.#d.clear(),this.#b.clear(),(await Promise.allSettled(e.map(e=>e.close()))).forEach(e=>{"rejected"===e.status&&this.#o?.warn("GroupChannel close failed",{error:e.reason})});let t=[...this.#g.values()];this.#g.clear(),(await Promise.allSettled(t.map(e=>e.close()))).forEach(e=>{"rejected"===e.status&&this.#o?.warn("HubConnection close failed",{error:e.reason})}),await this.#r.disposeAll()}}
|
|
@@ -22,3 +22,19 @@ export type HandleSendBroadcastParams = {
|
|
|
22
22
|
* runs INSIDE the registry's per-group lock and a throw skips persist.
|
|
23
23
|
*/
|
|
24
24
|
export declare function handleSendBroadcast(params: HandleSendBroadcastParams): Promise<void>;
|
|
25
|
+
export type HandleSendCommitParams = {
|
|
26
|
+
groupID: string;
|
|
27
|
+
/** Framed MLSMessage(Commit) bytes from the committer's `GroupManager` call. */
|
|
28
|
+
commitBytes: Uint8Array;
|
|
29
|
+
broadcastService: BroadcastService;
|
|
30
|
+
/** Same wire-send callback as {@link HandleSendBroadcastParams.send}. */
|
|
31
|
+
send: (groupID: string, payloadB64: string) => Promise<void>;
|
|
32
|
+
logger?: Logger;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Fan out a pre-produced MLS handshake Commit to a group's existing members.
|
|
36
|
+
* Unlike {@link handleSendBroadcast} this does not encrypt — the Commit is
|
|
37
|
+
* already a self-protected `PrivateMessage`; `prepareSendCommit` just tags it
|
|
38
|
+
* `mls` on the wire.
|
|
39
|
+
*/
|
|
40
|
+
export declare function handleSendCommit(params: HandleSendCommitParams): Promise<void>;
|
package/lib/hub/send-handler.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{toB64 as r}from"@enkaku/codec";export async function handleSendBroadcast(e){let{groupID:
|
|
1
|
+
import{toB64 as r}from"@enkaku/codec";export async function handleSendBroadcast(e){let{groupID:o,message:t,broadcastService:a,send:n,logger:d}=e;try{await a.prepareSend({groupID:o,message:t,send:e=>n(o,r(e))})}catch(r){throw d?.error("hub/group/send failed",{groupID:o,error:r}),r}}export async function handleSendCommit(e){let{groupID:o,commitBytes:t,broadcastService:a,send:n,logger:d}=e;try{await a.prepareSendCommit({groupID:o,commitBytes:t,send:e=>n(o,r(e))})}catch(r){throw d?.error("hub/group/send (mls commit) failed",{groupID:o,error:r}),r}}
|
package/lib/hub/wiring.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { DefaultAccessLevel, GraphInternals } from '@kubun/engine';
|
|
|
4
4
|
import type { Logger } from '@kubun/logger';
|
|
5
5
|
import type { GroupBroadcastMessage } from '../groups/broadcast.js';
|
|
6
6
|
import { type BroadcastEvent } from '../groups/broadcast-service.js';
|
|
7
|
-
import type {
|
|
7
|
+
import type { P2PEventEmitter } from '../groups/events.js';
|
|
8
8
|
import type { GroupHandleRegistry } from '../groups/group-handle-registry.js';
|
|
9
9
|
import type { ForwardingConfig } from '../sync/forwarder.js';
|
|
10
10
|
import type { StoreUnreadableMode } from '../types.js';
|
|
@@ -15,6 +15,18 @@ import { type CreateHubClient } from './relay-manager.js';
|
|
|
15
15
|
export type HubWiring = {
|
|
16
16
|
ready: Promise<void>;
|
|
17
17
|
scheduleBroadcast: (groupID: string, message: GroupBroadcastMessage) => void;
|
|
18
|
+
/**
|
|
19
|
+
* Awaitable broadcast — resolves only after the message is encrypted + sent.
|
|
20
|
+
* Use when ordering matters relative to a subsequent local op (e.g. a
|
|
21
|
+
* leave-request that must be encrypted with the still-live MLS handle before
|
|
22
|
+
* `leaveGroup` tears it down). Rejects on send failure.
|
|
23
|
+
*/
|
|
24
|
+
broadcastNow: (groupID: string, message: GroupBroadcastMessage) => Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Fire-and-forget fan-out of a pre-produced MLS handshake Commit to a
|
|
27
|
+
* group's existing members. Wired to `HubRelayManager.sendCommit`.
|
|
28
|
+
*/
|
|
29
|
+
scheduleSendCommit: (groupID: string, commitBytes: Uint8Array) => void;
|
|
18
30
|
dispose: () => Promise<void>;
|
|
19
31
|
};
|
|
20
32
|
export type SetupHubRelayParams = {
|
|
@@ -26,7 +38,7 @@ export type SetupHubRelayParams = {
|
|
|
26
38
|
* through the engine with `origin: 'peer'`.
|
|
27
39
|
*/
|
|
28
40
|
graph: GraphInternals;
|
|
29
|
-
emitter:
|
|
41
|
+
emitter: P2PEventEmitter;
|
|
30
42
|
createHubClient: CreateHubClient;
|
|
31
43
|
onRemoteBroadcast: (event: BroadcastEvent) => void;
|
|
32
44
|
/**
|
package/lib/hub/wiring.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BroadcastService as r}from"../groups/broadcast-service.js";import{HubRelayManager as
|
|
1
|
+
import{HLC as e}from"@kubun/hlc";import{BroadcastService as r}from"../groups/broadcast-service.js";import{HubRelayManager as o}from"./relay-manager.js";export function setupHubRelay(a){let t,{identity:d,db:i,graph:n,emitter:s,createHubClient:c,onRemoteBroadcast:u,registry:p,logger:h,createGroupChannel:l,createHubConnection:m,storeUnreadable:g,defaultAccessLevel:f,forwarding:b,onServerDIDObserved:y}=a,w=[],D=(e,r)=>{t.then(o=>o.broadcast(e,r).catch(r=>{h.error("broadcast failed",{groupID:e,error:r})}),()=>{})},v=async(e,r)=>{let o=await t;await o.broadcast(e,r)};return(t=(async()=>{let[a,t]=await Promise.all([i.getStore("p2p"),i.getStore("graph")]),v=new r({p2pStore:a,graphStore:t,graph:n,selfDID:d.id,registry:p,hlc:new e({nodeID:d.id}),storeUnreadable:g,defaultAccessLevel:f,forwarding:b,scheduleBroadcast:D,emitter:s,logger:h}),B=new o({p2pStore:a,broadcastService:v,deviceID:d.id,createHubClient:c,logger:h,createGroupChannel:l,createHubConnection:m,onServerDIDObserved:y});w.push(v.on("broadcast",u)),w.push(s.on("groupJoined",e=>B.addGroup(e.id).catch(r=>{h.error("addGroup failed",{groupID:e.id,error:r})}))),w.push(s.on("groupLeft",e=>B.removeGroup(e.groupID).catch(r=>{h.error("removeGroup failed",{groupID:e.groupID,error:r})}))),w.push(s.on("hubBound",({groupID:e,hubURL:r})=>B.addBinding(e,r).catch(o=>{h.error("addBinding failed",{groupID:e,hubURL:r,error:o})}))),w.push(s.on("hubUnbound",({groupID:e,hubURL:r})=>B.removeBinding(e,r).catch(o=>{h.error("removeBinding failed",{groupID:e,hubURL:r,error:o})})));let I=await a.listGroups();return await B.start(I.map(e=>e.id)),B})()).catch(e=>{h.error("hub relay setup failed",{error:e})}),{ready:t.then(()=>void 0),scheduleBroadcast:D,broadcastNow:v,scheduleSendCommit:(e,r)=>{t.then(o=>o.sendCommit(e,r).catch(r=>{h.error("sendCommit failed",{groupID:e,error:r})}),()=>{})},dispose:async()=>{for(let e of w)try{e()}catch{}w.length=0;try{let e=await t;await e.stop()}catch{}}}}export const DISABLED_HUB={ready:Promise.resolve(),scheduleBroadcast:()=>{},broadcastNow:async()=>{},scheduleSendCommit:()=>{},dispose:async()=>{}};
|
package/lib/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export type { ContextDeps, PendingJoinRequest } from './context/types.js';
|
|
|
8
8
|
export { type AffectedRow, type GroupBroadcastMessage, type MutationApplyEntry, type ProcessBroadcastParams, type ProcessBroadcastResult, processBroadcast, } from './groups/broadcast.js';
|
|
9
9
|
export { deserializeBroadcast, serializeBroadcast } from './groups/broadcast-codec.js';
|
|
10
10
|
export { ApplyError, type BroadcastEvent, BroadcastService, type BroadcastServiceEvents, type BroadcastServiceParams, DecryptError, type ReceiveBroadcastParams, type ReceiveBroadcastResult, type SendBroadcastParams, } from './groups/broadcast-service.js';
|
|
11
|
-
export { createFilteredGenerator,
|
|
11
|
+
export { createFilteredGenerator, createP2PEventEmitter, type P2PEventEmitter, type P2PEventMap, } from './groups/events.js';
|
|
12
12
|
export { GroupHandleRegistry, type GroupHandleRegistryParams, type SeedParams, } from './groups/group-handle-registry.js';
|
|
13
13
|
export { decodeFullJoinRequest, decodeInvitePayload, decodeJoinRequest, encodeFullJoinRequest, encodeInvitePayload, encodeJoinRequest, type FullJoinRequestPayload, type InvitePayload, type JoinRequestPayload, type SuggestedHub, } from './groups/invite-payload.js';
|
|
14
14
|
export { type AddCircleMemberParams, type AddCircleMemberResult, type CreateCircleParams, type CreateCircleResult, type CreateGroupParams, type CreateGroupResult, type DeleteCircleParams, type DeleteCircleResult, GroupManager, type GroupManagerParams, type InviteToGroupParams, type InviteToGroupResult, type JoinGroupParams, type JoinGroupResult, type LeaveGroupParams, type LeaveGroupResult, type RemoveCircleMemberParams, type RemoveCircleMemberResult, type RemoveGroupMemberParams, type RemoveGroupMemberResult, type RemoveMemberParams, type RemoveMemberResult, type UpdateCircleParams, type UpdateCircleResult, type UpdateGroupParams, type UpdateGroupResult, } from './groups/manager.js';
|
|
@@ -20,7 +20,6 @@ export type { SyncClientMessage, SyncProtocol, SyncServerMessage } from './proto
|
|
|
20
20
|
export { createP2PSchemaExtension } from './schema.js';
|
|
21
21
|
export { type BroadcastBatchConfig, type BroadcastQueue, type BroadcastQueueParams, createBroadcastQueue, DEFAULT_BROADCAST_BATCH_CONFIG, } from './sync/broadcast-queue.js';
|
|
22
22
|
export { type BroadcastSenderParams, DEFAULT_PUSH_SYNC_CONFIG, type PushSyncConfig, wireBroadcastSender, } from './sync/broadcast-sender.js';
|
|
23
|
-
export { catalogMatchesDoc } from './sync/catalog-match.js';
|
|
24
23
|
export { type CatalogSyncScope, resolveCatalogSyncScopes } from './sync/catalog-scope.js';
|
|
25
24
|
export { type EvaluateAndForwardParams, type EvaluateAndForwardResult, evaluateAndForward, type ForwardContext, type ForwardFilter, type ForwardingConfig, } from './sync/forwarder.js';
|
|
26
25
|
export { checkSyncDelegation, negotiateDirection } from './sync/handlers.js';
|
package/lib/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ServerTransport as e}from"@enkaku/http-server-transport";import{Server as r}from"@enkaku/server";import{isFullIdentity as t}from"@enkaku/token";import{DirectTransports as o}from"@enkaku/transport";import{HLC as s}from"@kubun/hlc";import{p2pStoreDefinition as n}from"@kubun/store-p2p";import{
|
|
1
|
+
import{ServerTransport as e}from"@enkaku/http-server-transport";import{Server as r}from"@enkaku/server";import{isFullIdentity as t}from"@enkaku/token";import{DirectTransports as o}from"@enkaku/transport";import{HLC as s}from"@kubun/hlc";import{p2pStoreDefinition as n}from"@kubun/store-p2p";import{createDelegationContext as a}from"./context/delegation.js";import{createGroupContext as i}from"./context/group.js";import{createHubContext as c}from"./context/hub.js";import{createJoinContext as p}from"./context/join.js";import{createSyncContext as d}from"./context/sync.js";import{createP2PEventEmitter as u}from"./groups/events.js";import{GroupHandleRegistry as l}from"./groups/group-handle-registry.js";import{GroupManager as g}from"./groups/manager.js";import{forwardRemoteBroadcast as m}from"./hub/forward-remote-broadcast.js";import{createHTTPHubClient as b}from"./hub/http-client.js";import{captureServerDID as y}from"./hub/manager.js";import{DISABLED_HUB as f,setupHubRelay as h}from"./hub/wiring.js";import{createP2PSchemaExtension as j}from"./schema.js";import{createBroadcastQueue as v,DEFAULT_BROADCAST_BATCH_CONFIG as x}from"./sync/broadcast-queue.js";import{DEFAULT_PUSH_SYNC_CONFIG as S,wireBroadcastSender as w}from"./sync/broadcast-sender.js";import{createSyncHandlers as D}from"./sync/handlers.js";import{SyncManager as P}from"./sync/sync-manager.js";export{processBroadcast}from"./groups/broadcast.js";export{deserializeBroadcast,serializeBroadcast}from"./groups/broadcast-codec.js";export{ApplyError,BroadcastService,DecryptError}from"./groups/broadcast-service.js";export{createFilteredGenerator,createP2PEventEmitter}from"./groups/events.js";export{GroupHandleRegistry}from"./groups/group-handle-registry.js";export{decodeFullJoinRequest,decodeInvitePayload,decodeJoinRequest,encodeFullJoinRequest,encodeInvitePayload,encodeJoinRequest}from"./groups/invite-payload.js";export{GroupManager}from"./groups/manager.js";export{MLSEncryptor}from"./groups/mls-encryptor.js";export{restoreMLSGroupHandle}from"./groups/mls-group-handle.js";export{replacer as mlsJSONReplacer,reviver as mlsJSONReviver}from"./groups/mls-json.js";export{deserializeMLSGroupState,serializeMLSGroupState}from"./groups/mls-state.js";export{createP2PSchemaExtension}from"./schema.js";export{createBroadcastQueue,DEFAULT_BROADCAST_BATCH_CONFIG}from"./sync/broadcast-queue.js";export{DEFAULT_PUSH_SYNC_CONFIG,wireBroadcastSender}from"./sync/broadcast-sender.js";export{resolveCatalogSyncScopes}from"./sync/catalog-scope.js";export{evaluateAndForward}from"./sync/forwarder.js";export{checkSyncDelegation,negotiateDirection}from"./sync/handlers.js";export{HubTunnelSyncListener}from"./sync/hub-tunnel-sync-listener.js";export{HubTunnelSyncProvider}from"./sync/hub-tunnel-sync-provider.js";export{PeerRegistry}from"./sync/peer-registry.js";export{SyncClient}from"./sync/sync-client.js";export{SyncManager}from"./sync/sync-manager.js";export{DIDObservingTransport}from"./hub/did-observing-transport.js";export{ReconnectingError}from"./hub/errors.js";export{createHTTPHubClient}from"./hub/http-client.js";export{bindHubToGroup,captureServerDID,createHub,deleteHub,getHub,HubServerDIDConflictError,listGroupsByHub,listHubs,listHubsByGroup,unbindHubFromGroup,updateHub,upsertHub}from"./hub/manager.js";export{HubRelayManager}from"./hub/relay-manager.js";export{DISABLED_HUB,setupHubRelay}from"./hub/wiring.js";export const DEFAULT_RECEIVE_CONFIG={storeUnreadable:"persist"};export function createP2PPlugin(I){return R=>{var E;let H,L,B,C;R.db.register(n);let G=R.db.adapter;if(!(t(E=R.identity)&&"privateKey"in E))throw Error("createP2PPlugin requires an OwnIdentity (full identity with privateKey) — received a narrower Identity");let A=R.identity,F=new s({nodeID:A.id}),k={...DEFAULT_RECEIVE_CONFIG,...I?.receive},T=I?.defaultAccessLevel??{read:"only_owner",write:"only_owner"},U=new l({stores:R.db,deviceID:A.id,logger:R.getLogger("group-handle-registry")}),_=new g({identity:A,getRandomID:R.runtime.getRandomID,registry:U}),O=u(),q=new P({deployClusters:async e=>{let r=Object.values(e);await R.engine.deployGraph({clusters:r})},graph:R.graph,runtime:R.runtime,identity:R.identity,logger:R.getLogger("sync"),storeUnreadable:k.storeUnreadable,defaultAccessLevel:T}),N=D({db:R.db,graph:R.graph,logger:R.getLogger("sync-handlers"),peerRegistry:q.peerRegistry,selfDID:A.id,storeUnreadable:k.storeUnreadable,defaultAccessLevel:T}),M=new Map,J=[],z=R.getLogger("hub-relay"),K=f,V={...S,...I?.pushSync};if(I?.hub){let e="object"==typeof I.hub&&null!=I.hub.createHubClient?I.hub.createHubClient:b({identity:A,fetch:R.runtime.fetch});K=h({identity:A,db:R.db,graph:R.graph,emitter:O,createHubClient:e,registry:U,onRemoteBroadcast:e=>{m(O,e,z).catch(e=>{z.error("forwardRemoteBroadcast failed",{error:e})})},logger:z,storeUnreadable:k.storeUnreadable,defaultAccessLevel:T,forwarding:I.forwarding,onServerDIDObserved:({hubURL:e,serverDID:r})=>y({stores:R.db,hubURL:e,serverDID:r}).then(()=>void 0)}),V.enabled&&(L=v({config:{...x,...I.batch},scheduleBroadcast:K.scheduleBroadcast,logger:R.getLogger("broadcast-queue")}),H=w({db:R.db,eventBus:R.eventBus,selfDID:A.id,queue:L,logger:R.getLogger("broadcast-sender")}))}let W={hubReady:K.ready,addPeer:e=>q.addPeer({config:e,stores:R.db}),removePeer:e=>q.removePeer({peerDID:e,stores:R.db}),listPeers:()=>q.listPeers({stores:R.db}),syncWithPeer:e=>q.merkleSyncWithPeer({...e,stores:R.db}),getStatus:e=>q.getStatus({peerDID:e,stores:R.db}),onSyncEvent:e=>q.onSyncEvent(e),createSyncTransport:function(e){let t=new o({signal:e}),s=new r({getRandomID:R.runtime.getRandomID,handlers:N,logger:R.getLogger("sync-server"),transports:[t.server],signal:e});return J.push(s),t.client}};if(I?.http){let t="string"==typeof I.http?I.http.replace(/^\//,""):"sync";W.syncReady=(async()=>{let o=await R.engine.getAPI("http");await o.listening,B=new e({allowedOrigin:["*"]}),C=new r({getRandomID:R.runtime.getRandomID,handlers:N,logger:R.getLogger("sync-http-server"),transports:[B]}),J.push(C),o.registerProtocol(t,B.fetch.bind(B))})()}return{name:"p2p",schemaExtension:e=>j(O),api:W,createContextFactory:()=>(e,r)=>{let t={identity:A,groupManager:_,registry:U,syncManager:q,stores:r,adapter:G,hlc:F,emitter:O,pendingJoinRequests:M,runtime:R.runtime,autoAcceptPeers:I?.autoAcceptPeers,scheduleBroadcast:K.scheduleBroadcast,broadcastNow:K.broadcastNow,scheduleSendCommit:K.scheduleSendCommit,logger:R.getLogger("context")};return{group:i(e,t),sync:d(e,t),join:p(e,t),hub:c(e,t),delegation:a(e,t)}},dispose:async()=>{if(null!=H)try{H()}catch{}if(null!=L)try{L.dispose()}catch{}let e=(async()=>{for(let e of J)await e.dispose();null!=B&&await B.dispose()})(),r=R.getLogger("dispose"),t=await Promise.allSettled([e,q.dispose(),K.dispose()]),o=["sync-http","sync-manager","hub"];t.forEach((e,t)=>{"rejected"===e.status&&r.error("dispose branch failed",{branch:o[t],error:e.reason})})}}}}
|
package/lib/schema.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { SchemaExtension } from '@kubun/engine';
|
|
2
|
-
import { type
|
|
3
|
-
export declare function createP2PSchemaExtension(emitter:
|
|
2
|
+
import { type P2PEventEmitter } from './groups/events.js';
|
|
3
|
+
export declare function createP2PSchemaExtension(emitter: P2PEventEmitter): SchemaExtension;
|
package/lib/schema.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{createFilteredGenerator as e}from"./groups/events.js";function r(e){if(null==e.p2p)throw Error("p2p plugin is not wired into this context");return e.p2p}let
|
|
1
|
+
import{createFilteredGenerator as e}from"./groups/events.js";function r(e){if(null==e.p2p)throw Error("p2p plugin is not wired into this context");return e.p2p}async function*t(){}let o=`
|
|
2
2
|
type PeerGroup {
|
|
3
3
|
id: ID!
|
|
4
4
|
name: String!
|
|
@@ -173,6 +173,44 @@ type DeleteHubPayload {
|
|
|
173
173
|
boundGroupIDs: [ID!]!
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
type DelegationToken {
|
|
177
|
+
jwt: String!
|
|
178
|
+
jti: String!
|
|
179
|
+
grantor: ID!
|
|
180
|
+
audience: ID!
|
|
181
|
+
resource: String!
|
|
182
|
+
exp: Int!
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
input DelegationTokenCovers {
|
|
186
|
+
modelID: ID
|
|
187
|
+
docID: ID
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
"""
|
|
191
|
+
A verified revocation of a delegation token addressed to the viewer.
|
|
192
|
+
Only surfaces while the underlying held capability still exists locally;
|
|
193
|
+
revocations whose cap was deleted or never arrived are excluded by the
|
|
194
|
+
audience-derivation join.
|
|
195
|
+
"""
|
|
196
|
+
type RevokedDelegationToken {
|
|
197
|
+
jti: ID!
|
|
198
|
+
grantor: ID!
|
|
199
|
+
audience: ID!
|
|
200
|
+
revokerDID: DID!
|
|
201
|
+
"Issuer's revocation-record timestamp (iat), Unix epoch seconds."
|
|
202
|
+
revokedAt: Int!
|
|
203
|
+
"""
|
|
204
|
+
Wall-clock time the revocation became binding, Unix epoch seconds. For
|
|
205
|
+
self-issued revocations this equals revokedAt (verified at mint time);
|
|
206
|
+
for received revocations it is the receiver's clock at the moment the
|
|
207
|
+
cap-vs-revoker cross-check succeeded.
|
|
208
|
+
"""
|
|
209
|
+
verifiedAt: Int!
|
|
210
|
+
capExp: Int
|
|
211
|
+
groupID: ID!
|
|
212
|
+
}
|
|
213
|
+
|
|
176
214
|
extend type Query {
|
|
177
215
|
groups: [PeerGroup!]!
|
|
178
216
|
group(id: ID!): PeerGroup
|
|
@@ -182,6 +220,8 @@ extend type Query {
|
|
|
182
220
|
hub(id: ID!): PeerHub
|
|
183
221
|
syncPeers: [SyncPeer!]!
|
|
184
222
|
syncStatus: SyncStatus!
|
|
223
|
+
ownDelegationTokens(grantor: ID, covers: DelegationTokenCovers): [DelegationToken!]!
|
|
224
|
+
ownRevokedDelegationTokens(grantor: ID): [RevokedDelegationToken!]!
|
|
185
225
|
}
|
|
186
226
|
|
|
187
227
|
extend type Mutation {
|
|
@@ -190,7 +230,7 @@ extend type Mutation {
|
|
|
190
230
|
createCircle(groupID: ID!, name: String!, description: String): PeerCircle!
|
|
191
231
|
addCircleMember(circleID: ID!, memberDID: DID!, role: String!): PeerCircleMember!
|
|
192
232
|
createCatalog(name: String!, description: String, filterCriteria: String!): PeerCatalog!
|
|
193
|
-
inviteToGroup(groupID: ID!, joinRequest: String!): InviteToGroupPayload!
|
|
233
|
+
inviteToGroup(groupID: ID!, joinRequest: String!, grants: [String!], revocations: [String!]): InviteToGroupPayload!
|
|
194
234
|
joinGroup(invitePayload: String!, joinRequestPayload: String!): JoinGroupPayload!
|
|
195
235
|
prepareJoinRequest: PrepareJoinRequestPayload!
|
|
196
236
|
completeJoin(invitePayload: String!): CompleteJoinPayload!
|
|
@@ -202,6 +242,8 @@ extend type Mutation {
|
|
|
202
242
|
deleteCatalog(catalogID: ID!): DeleteCatalogPayload!
|
|
203
243
|
deleteCircle(circleID: ID!): DeleteCirclePayload!
|
|
204
244
|
removeCircleMember(circleID: ID!, memberDID: DID!): RemoveCircleMemberPayload!
|
|
245
|
+
grantWriteCapability(to: DID!, res: String!, groupID: ID!, expiresIn: Int): String!
|
|
246
|
+
revokeCapability(jti: ID!): Boolean!
|
|
205
247
|
addSyncPeer(peerDID: DID!, endpoint: URL!, mode: String, priority: String, trustLevel: String): Boolean!
|
|
206
248
|
removeSyncPeer(peerDID: DID!): RemoveSyncPeerPayload!
|
|
207
249
|
createHub(input: CreateHubInput!): PeerHub!
|
|
@@ -231,5 +273,7 @@ extend type Subscription {
|
|
|
231
273
|
circleMemberAdded(circleID: ID!): PeerCircleMember!
|
|
232
274
|
circleMemberRemoved(circleID: ID!): PeerCircleMember!
|
|
233
275
|
circleCatalogsChanged(circleID: ID!): PeerCircle!
|
|
276
|
+
ownDelegationTokenAdded: DelegationToken!
|
|
277
|
+
ownDelegationTokenRevoked: RevokedDelegationToken!
|
|
234
278
|
}
|
|
235
|
-
`;export function createP2PSchemaExtension(
|
|
279
|
+
`;export function createP2PSchemaExtension(i){return{sdl:o,resolvers:{queryFields:{groups:async(e,t,o)=>await r(o).group.list(),group:async(e,t,o)=>await r(o).group.get(t.id),groupMembers:async(e,t,o)=>await r(o).group.listMembers(t.groupID),circles:async(e,t,o)=>await r(o).group.listCircles(t.groupID),hubs:async(e,t,o)=>await r(o).hub.list(),hub:async(e,t,o)=>await r(o).hub.get(t.id),syncPeers:async(e,t,o)=>await r(o).sync.listPeers(),syncStatus:(e,t,o)=>{let i=r(o).sync.getStatus();return{...i,lastSyncByPeer:JSON.stringify(i.lastSyncByPeer)}},ownDelegationTokens:async(e,r,t)=>{let o=t.getViewer();if(null==o||null==t.p2p)return[];let i=r.grantor??null,a=r.covers??null;return await t.p2p.delegation.listOwnTokens({viewerDID:o,grantor:i,covers:a})},ownRevokedDelegationTokens:async(e,r,t)=>{let o=t.getViewer();if(null==o||null==t.p2p)return[];let i=r.grantor??null;return await t.p2p.delegation.listOwnRevokedTokens({viewerDID:o,grantor:i})}},mutationFields:{createGroup:async(e,t,o)=>{let i=t.input;return{group:await r(o).group.create(i)}},addGroupMember:async(e,t,o)=>await r(o).group.addMember(t.groupID,t.memberDID,t.role),createCircle:async(e,t,o)=>await r(o).group.createCircle({groupID:t.groupID,name:t.name,description:t.description}),addCircleMember:async(e,t,o)=>await r(o).group.addCircleMember(t.circleID,t.memberDID,t.role),createCatalog:async(e,t,o)=>await r(o).group.createCatalog({name:t.name,description:t.description,filterCriteria:t.filterCriteria}),inviteToGroup:async(e,t,o)=>await r(o).group.invite(t.groupID,t.joinRequest,t.grants,t.revocations),joinGroup:async(e,t,o)=>await r(o).group.join(t.invitePayload,t.joinRequestPayload),prepareJoinRequest:async(e,t,o)=>await r(o).join.prepareRequest(),completeJoin:async(e,t,o)=>await r(o).join.complete(t.invitePayload),leaveGroup:async(e,t,o)=>await r(o).group.leave(t.groupID),removeGroupMember:async(e,t,o)=>await r(o).group.removeMember(t.groupID,t.memberDID),updateCatalog:async(e,t,o)=>{let i=t.input;return await r(o).group.updateCatalog(i)},updateGroup:async(e,t,o)=>{let i=t.input;return{group:await r(o).group.update(i)}},updateCircle:async(e,t,o)=>{let i=t.input;return{circle:await r(o).group.updateCircle(i)}},deleteCatalog:async(e,t,o)=>await r(o).group.deleteCatalog(t.catalogID),deleteCircle:async(e,t,o)=>await r(o).group.deleteCircle(t.circleID),removeCircleMember:async(e,t,o)=>await r(o).group.removeCircleMember(t.circleID,t.memberDID),grantWriteCapability:async(e,t,o)=>await r(o).group.grantWriteCapability({to:t.to,res:t.res,groupID:t.groupID,expiresIn:t.expiresIn}),revokeCapability:async(e,t,o)=>await r(o).group.revokeCapability({jti:t.jti}),addSyncPeer:async(e,t,o)=>(await r(o).sync.addPeer({peerDID:t.peerDID,endpoint:t.endpoint,mode:t.mode??"on-demand",allowedUsers:{all:!0},priority:t.priority??"0",trustLevel:t.trustLevel??"trusted"}),!0),removeSyncPeer:async(e,t,o)=>(await r(o).sync.removePeer(t.peerDID),{success:!0}),createHub:async(e,t,o)=>{let i=t.input;return await r(o).hub.create(i)},updateHub:async(e,t,o)=>{let i=t.input;return await r(o).hub.update(i)},deleteHub:async(e,t,o)=>await r(o).hub.delete(t.id,t.force??!1),bindHubToGroup:async(e,t,o)=>await r(o).hub.bind({hubID:t.hubID,groupID:t.groupID}),unbindHubFromGroup:async(e,t,o)=>await r(o).hub.unbind({hubID:t.hubID,groupID:t.groupID})},typeFields:{PeerGroup:{hubs:async(e,t,o)=>await r(o).hub.listByGroup(e.id)},PeerHub:{groups:async(e,t,o)=>await r(o).hub.listGroups(e.id)}},subscriptionFields:{groupJoined:{resolve:e=>e,subscribe:()=>e(i,"groupJoined")},groupLeft:{resolve:e=>e,subscribe:()=>e(i,"groupLeft")},groupDataChanged:{resolve:e=>e,subscribe:(r,t)=>e(i,"groupDataChanged",e=>e.groupID===t.groupID)},groupMemberJoined:{resolve:e=>e,subscribe:(r,t)=>e(i,"groupMemberJoined",e=>e.groupID===t.groupID)},groupMemberLeft:{resolve:e=>e,subscribe:(r,t)=>e(i,"groupMemberLeft",e=>e.groupID===t.groupID)},circleCreated:{resolve:e=>e,subscribe:(r,t)=>e(i,"circleCreated",e=>e.groupID===t.groupID)},circleDeleted:{resolve:e=>e,subscribe:(r,t)=>e(i,"circleDeleted",e=>e.groupID===t.groupID)},circleDataChanged:{resolve:e=>e,subscribe:(r,t)=>e(i,"circleDataChanged",e=>e.circleID===t.circleID)},circleMemberAdded:{resolve:e=>e,subscribe:(r,t)=>e(i,"circleMemberAdded",e=>e.circleID===t.circleID)},circleMemberRemoved:{resolve:e=>e,subscribe:(r,t)=>e(i,"circleMemberRemoved",e=>e.circleID===t.circleID)},circleCatalogsChanged:{resolve:e=>e,subscribe:(r,t)=>e(i,"circleCatalogsChanged",e=>e.circleID===t.circleID)},ownDelegationTokenAdded:{resolve:e=>e,subscribe:(r,o,a)=>{let n=a.getViewer();return null==n?t():e(i,"delegationTokenAdded",e=>e.audience===n)}},ownDelegationTokenRevoked:{resolve:e=>e,subscribe:(r,o,a)=>{let n=a.getViewer();return null==n?t():e(i,"delegationTokenRevoked",e=>e.audience===n)}}}}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{resolveAccessRule as
|
|
1
|
+
import{catalogMatchesDoc as e,resolveAccessRule as r}from"@kubun/engine";import{DocumentID as t}from"@kubun/id";import{getGraphStore as o}from"@kubun/store-graph";import{getP2PStore as l}from"@kubun/store-p2p";import{computeBroadcastScopes as a}from"./scope-resolver.js";export const DEFAULT_PUSH_SYNC_CONFIG={enabled:!1};let n={read:"only_owner",write:"only_owner"};export function wireBroadcastSender(e){let r=e.defaultAccessLevel??n;return e.eventBus.on("engine:mutation:authored",async t=>{try{await i(t,{...e,defaultAccessLevel:r})}catch(r){e.logger?.warn("broadcast-sender handler failed",{documentID:t.documentID,error:String(r)})}})}async function i(n,i){let s=await o(i.db),d=await l(i.db),u=await s.getDocument(t.fromString(n.documentID));if(null==u)return;let f=await r(u,u.model,u.owner,"read",{getUserModelAccessDefault:(e,r,t)=>s.getUserModelAccessDefault(e,r,t),isMemberOfAnyCircle:(e,r)=>d.isMemberOfAnyCircle(e,r),isMemberOfAnyGroup:(e,r)=>d.isMemberOfAnyGroup(e,r),getModelInterfaces:e=>s.getModelInterfaces(e)},i.defaultAccessLevel);if("only_owner"===f.level)return;let c=await d.getCirclesForMember(i.selfDID),m=new Set;for(let e of c)for(let r of e.catalog_ids??[])m.add(r);let w=m.size>0?await s.getCatalogs(Array.from(m)):new Map,g=[];for(let e of c)for(let r of e.catalog_ids??[]){let t=w.get(r);null!=t&&g.push({groupID:e.group_id,criteria:t.filter_criteria})}let D=await Promise.all(g.map(async r=>({groupID:r.groupID,matched:await e(r.criteria,u,d)}))),p=new Set;for(let e of D)e.matched&&p.add(e.groupID);let I=Array.from(p);if(0===I.length)return;let b=new Set(f.allowedGroups??[]),v=new Set;if(null!=f.allowedCircles&&f.allowedCircles.length>0)for(let e of(await d.getCircles(f.allowedCircles)).values())null!=e.group_id&&v.add(e.group_id);let y=new Set;if(null!=f.allowedDIDs&&f.allowedDIDs.length>0)for(let e of(await d.getGroupsForMembers(f.allowedDIDs)).values())for(let r of e)y.add(r.id);let _=a({resolvedRule:{level:f.level,allowedGroupIDs:b,allowedCircleParentGroupIDs:v,allowedDIDGroupIDs:y},senderGroupIDs:I});if(0!==_.length)for(let e of _){if(!await d.recordBroadcast({docID:n.documentID,version:n.version,mlsGroupID:e}))continue;let r={mutationJWT:n.mutationJWT,docID:n.documentID,version:n.version,modelID:n.modelID};i.queue.enqueue(e,r,i.selfDID)}}
|
package/lib/sync/forwarder.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { DocumentNode } from '@kubun/protocol';
|
|
|
4
4
|
import type { GraphStoreAPI } from '@kubun/store-graph';
|
|
5
5
|
import type { P2PStoreAPI } from '@kubun/store-p2p';
|
|
6
6
|
import type { GroupBroadcastMessage, MutationApplyEntry } from '../groups/broadcast.js';
|
|
7
|
+
export declare function rethrowIfProgrammerError(error: unknown): void;
|
|
7
8
|
/**
|
|
8
9
|
* Context passed to a {@link ForwardFilter} for each candidate forwarding scope.
|
|
9
10
|
*
|
package/lib/sync/forwarder.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{resolveAccessRule as e}from"@kubun/engine";import{
|
|
1
|
+
import{catalogMatchesDoc as r,resolveAccessRule as e}from"@kubun/engine";import{computeBroadcastScopes as o}from"./scope-resolver.js";export function rethrowIfProgrammerError(r){if(r instanceof SyntaxError||r instanceof TypeError||r instanceof ReferenceError||r instanceof RangeError)throw r}export async function evaluateAndForward(t){let a,l;if(!1===t.forwarding)return{forwarded:0,skipped:0};let i=!0===t.forwarding?()=>!0:t.forwarding,n=(await t.p2pStore.getGroupsForMember(t.selfDID)).map(r=>r.id).filter(r=>r!==t.sourceGroupID);if(0===n.length)return{forwarded:0,skipped:0};try{a=await e(t.postState,t.postState.model,t.postState.owner,"read",{getUserModelAccessDefault:(r,e,o)=>t.graphStore.getUserModelAccessDefault(r,e,o),isMemberOfAnyCircle:(r,e)=>t.p2pStore.isMemberOfAnyCircle(r,e),isMemberOfAnyGroup:(r,e)=>t.p2pStore.isMemberOfAnyGroup(r,e),getModelInterfaces:r=>t.graphStore.getModelInterfaces(r)},t.defaultAccessLevel)}catch(r){return rethrowIfProgrammerError(r),t.logger?.warn("forwarder: resolveAccessRule failed, skipping forward",{docID:t.entry.docID,version:t.entry.version,sourceGroupID:t.sourceGroupID,error:String(r)}),{forwarded:0,skipped:0}}let s=new Set(a.allowedGroups??[]),f=new Set;if(null!=a.allowedCircles&&a.allowedCircles.length>0)try{for(let r of(await t.p2pStore.getCircles(a.allowedCircles)).values())null!=r.group_id&&f.add(r.group_id)}catch(r){rethrowIfProgrammerError(r),t.logger?.warn("forwarder: getCircles failed, skipping allowedCircles",{count:a.allowedCircles.length,error:String(r)})}let d=new Set;if(null!=a.allowedDIDs&&a.allowedDIDs.length>0)try{for(let r of(await t.p2pStore.getGroupsForMembers(a.allowedDIDs)).values())for(let e of r)d.add(e.id)}catch(r){rethrowIfProgrammerError(r),t.logger?.warn("forwarder: getGroupsForMembers failed, skipping allowedDIDs",{count:a.allowedDIDs.length,error:String(r)})}let p=o({resolvedRule:{level:a.level,allowedGroupIDs:s,allowedCircleParentGroupIDs:f,allowedDIDGroupIDs:d},senderGroupIDs:n});if(0===p.length)return{forwarded:0,skipped:0};try{l=await t.p2pStore.getCirclesForMember(t.selfDID)}catch(r){return rethrowIfProgrammerError(r),t.logger?.warn("forwarder: getCirclesForMember failed",{selfDID:t.selfDID,error:String(r)}),{forwarded:0,skipped:0}}let c=new Set(p),g=new Map;for(let r of l){if(!c.has(r.group_id))continue;let e=g.get(r.group_id);null!=e?e.push(r):g.set(r.group_id,[r])}let u=new Set;for(let r of g.values())for(let e of r)for(let r of e.catalog_ids??[])u.add(r);let w=new Map;if(u.size>0)try{w=await t.graphStore.getCatalogs(Array.from(u))}catch(r){rethrowIfProgrammerError(r),t.logger?.warn("forwarder: getCatalogs failed, treating all as missing",{count:u.size,error:String(r)})}let D=0,I=0;for(let e of p)try{let o=g.get(e)??[],a=!1;for(let e of o){for(let o of e.catalog_ids??[]){let e=w.get(o);if(null!=e&&await r(e.filter_criteria,t.postState,t.p2pStore)){a=!0;break}}if(a)break}if(!a){I++;continue}let l={docID:t.entry.docID,modelID:t.entry.modelID,ownerDID:t.postState.owner,sourceGroupID:t.sourceGroupID,candidateGroupID:e,postState:t.postState};if(!await i(l)){I++;continue}if(!await t.p2pStore.recordBroadcast({docID:t.entry.docID,version:t.entry.version,mlsGroupID:e}))continue;let n={type:"mutation:apply",entries:[t.entry],senderPeerDID:t.selfDID};await t.scheduleBroadcast(e,n),D++}catch(r){t.logger?.warn("forwarder: candidate evaluation failed",{docID:t.entry.docID,version:t.entry.version,candidateGroupID:e,sourceGroupID:t.sourceGroupID,error:String(r)})}return{forwarded:D,skipped:I}}
|