@kubun/hub 0.8.0 → 0.9.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.
@@ -0,0 +1,44 @@
1
+ import type { HubStore } from '@enkaku/hub-protocol';
2
+ import type { Identity } from '@enkaku/token';
3
+ import type { Adapter } from '@kubun/db-adapter';
4
+ import { type AdapterInput } from '@kubun/db-provider';
5
+ import { Kysely } from 'kysely';
6
+ import { type HubDatabase } from './hub-store.js';
7
+ import { type RelayObservers } from './observers.js';
8
+ export type HubAuthConfig = {
9
+ mode: 'open';
10
+ } | {
11
+ mode: 'allowlist';
12
+ allowedDIDs?: Array<string>;
13
+ isAllowed?: (did: string) => boolean | Promise<boolean>;
14
+ };
15
+ export type RelayRetentionConfig = {
16
+ olderThanSeconds: number;
17
+ intervalSeconds?: number;
18
+ };
19
+ export type CreateRelayOptions = {
20
+ db: AdapterInput;
21
+ identity?: Identity;
22
+ auth?: HubAuthConfig;
23
+ allowedOrigin?: string | Array<string>;
24
+ retention?: RelayRetentionConfig;
25
+ observers?: RelayObservers;
26
+ };
27
+ export type Relay = {
28
+ fetch: (request: Request) => Promise<Response>;
29
+ hubStore: HubStore;
30
+ pendingDeliveries: () => Promise<number>;
31
+ dispose: () => Promise<void>;
32
+ };
33
+ export type RelayInternalOptions = {
34
+ kysely: Kysely<HubDatabase>;
35
+ adapter: Adapter;
36
+ identity?: Identity;
37
+ auth?: HubAuthConfig;
38
+ allowedOrigin?: string | Array<string>;
39
+ retention?: RelayRetentionConfig;
40
+ observers?: RelayObservers;
41
+ ownsKysely: boolean;
42
+ };
43
+ export declare function createRelayInternal(options: RelayInternalOptions): Promise<Relay>;
44
+ export declare function createRelay(options: CreateRelayOptions): Promise<Relay>;
@@ -0,0 +1 @@
1
+ import{ServerTransport as e}from"@enkaku/http-server-transport";import{createHandlers as t,HubClientRegistry as r}from"@enkaku/hub-server";import{Server as n}from"@enkaku/server";import{resolveAdapter as o}from"@kubun/db-provider";import{Kysely as i,Migrator as a}from"kysely";import{createHubStore as l}from"./hub-store.js";import{createHubMigrations as s}from"./migrations.js";import{decorateHubStore as u}from"./observers.js";export async function createRelayInternal(o){var i;let c,d,{kysely:h,adapter:m,observers:y,ownsKysely:p}=o;if(null!=o.auth&&null==o.identity)throw Error("createRelay: 'identity' is required when 'auth' is set");let f=s({types:m.types,functions:m.functions}),w=new a({db:h,provider:{getMigrations:async()=>f},migrationTableName:"kubun_hub_migration",migrationLockTableName:"kubun_hub_migration_lock"}),{error:g}=await w.migrateToLatest();if(null!=g)throw g;let b=new r,v=l({kysely:h,adapter:m}),k=u(v,y);if(y?.onPurge!=null){let e=y.onPurge;c=v.events.on("purge",t=>{try{e({sequenceCount:t.sequenceIDs.length})}catch{}})}let _=t({registry:b,store:k}),R=new e({allowedOrigin:null==(i=o.allowedOrigin)?["*"]:"string"==typeof i?i.split(",").map(e=>e.trim()):i}),T=function(e){if(null==e)return{};switch(e.mode){case"open":return{"*":{allow:!0}};case"allowlist":{let{allowedDIDs:t,isAllowed:r}=e;if(null!=r)return{"*":{allow:async e=>{let n=e.payload.iss;return!!t?.includes(n)||await r(n)}}};return{"*":{allow:t??[]}}}}}(o.auth),j=new n(o.identity?{identity:o.identity,accessRules:T,handlers:_,transports:[R]}:{handlers:_,transports:[R]});if(y?.onAuthRejected!=null){let e=y.onAuthRejected;j.events.on("handlerError",t=>{if("auth"===t.category)try{e({reason:t.error.message})}catch{}})}if(null!=o.retention){let{olderThanSeconds:e}=o.retention;d=setInterval(()=>{k.purge({olderThan:e}).catch(e=>{if(y?.onError!=null)try{y.onError({stage:"purge",error:e})}catch{}})},Math.max(1,Math.floor(1e3*(o.retention.intervalSeconds??3600))))}return{fetch:e=>R.fetch(e),hubStore:k,pendingDeliveries:async()=>Number((await h.selectFrom("kubun_hub_deliveries").select(({fn:e})=>e.countAll().as("count")).executeTakeFirstOrThrow()).count),dispose:async()=>{null!=d&&(clearInterval(d),d=void 0),null!=c&&(c(),c=void 0),await j.dispose(),p&&await h.destroy()}}}export async function createRelay(e){let t=o(e.db),r=new i({dialect:t.dialect});return await createRelayInternal({kysely:r,adapter:t,identity:e.identity,auth:e.auth,allowedOrigin:e.allowedOrigin,retention:e.retention,observers:e.observers,ownsKysely:!0})}
package/lib/hub-store.js CHANGED
@@ -1 +1 @@
1
- import{EventEmitter as e}from"@enkaku/event";export function createHubStore(u){let t,n,{adapter:a}=u;async function i(){return null==t&&(t=await u.kysely),t}async function s(){let e=await i();if(null==n){let u=await e.selectFrom("kubun_hub_messages").select(e.fn.max("sequence_id").as("max_id")).executeTakeFirst();n=u?.max_id!=null?Number.parseInt(u.max_id,10):0}return String(++n).padStart(12,"0")}let r=new e;async function c(e){if(0===e.sequenceIDs.length)return;let u=await i();await u.deleteFrom("kubun_hub_deliveries").where("recipient_did","=",e.recipientDID).where("sequence_id","in",e.sequenceIDs).execute();let t=new Set((await u.selectFrom("kubun_hub_deliveries").select("sequence_id").where("sequence_id","in",e.sequenceIDs).execute()).map(e=>e.sequence_id)),n=e.sequenceIDs.filter(e=>!t.has(e));n.length>0&&await u.deleteFrom("kubun_hub_messages").where("sequence_id","in",n).execute()}async function _(e){let u=await i(),t=Date.now()-1e3*e.olderThan,n=(await u.selectFrom("kubun_hub_messages").select("sequence_id").where("stored_at","<=",t).execute()).map(e=>e.sequence_id);return 0===n.length?[]:(await u.deleteFrom("kubun_hub_deliveries").where("sequence_id","in",n).execute(),await u.deleteFrom("kubun_hub_messages").where("sequence_id","in",n).execute(),await r.emit("purge",{sequenceIDs:n}),n)}return{events:r,store:async function(e){let u=await s(),t=Date.now(),n=await i();return await n.insertInto("kubun_hub_messages").values({sequence_id:u,sender_did:e.senderDID,group_id:e.groupID??null,payload:a.encodeBinary(e.payload),stored_at:t}).execute(),e.recipients.length>0&&await n.insertInto("kubun_hub_deliveries").values(e.recipients.map(e=>({sequence_id:u,recipient_did:e}))).execute(),u},fetch:async function(e){null!=e.ack&&e.ack.length>0&&await c({recipientDID:e.recipientDID,sequenceIDs:e.ack});let u=(await i()).selectFrom("kubun_hub_deliveries").innerJoin("kubun_hub_messages","kubun_hub_messages.sequence_id","kubun_hub_deliveries.sequence_id").select(["kubun_hub_messages.sequence_id","kubun_hub_messages.sender_did","kubun_hub_messages.group_id","kubun_hub_messages.payload"]).where("kubun_hub_deliveries.recipient_did","=",e.recipientDID).orderBy("kubun_hub_messages.sequence_id","asc");null!=e.after&&(u=u.where("kubun_hub_messages.sequence_id",">",e.after));let t=e.limit;null!=t&&(u=u.limit(t+1));let n=await u.execute(),a=!1,s=n;null!=t&&n.length>t&&(a=!0,s=n.slice(0,t));let r=s.map(e=>({sequenceID:e.sequence_id,senderDID:e.sender_did,groupID:e.group_id??void 0,payload:e.payload})),_=r.length>0?r[r.length-1].sequenceID:null;return{messages:r,cursor:_,hasMore:a}},ack:c,purge:_,storeKeyPackage:async function(e,u){let t=await i();await t.insertInto("kubun_hub_key_packages").values({owner_did:e,key_package:u}).execute()},fetchKeyPackages:async function(e,u){let t=await i(),n=await t.selectFrom("kubun_hub_key_packages").select(["id","key_package"]).where("owner_did","=",e).orderBy("id","asc").limit(u??1).execute();if(0===n.length)return[];let a=n.map(e=>e.id);return await t.deleteFrom("kubun_hub_key_packages").where("id","in",a).execute(),n.map(e=>e.key_package)},setGroupMembers:async function(e,u){let t=await i();await t.deleteFrom("kubun_hub_group_members").where("group_id","=",e).execute(),u.length>0&&await t.insertInto("kubun_hub_group_members").values(u.map(u=>({group_id:e,member_did:u}))).execute()},getGroupMembers:async function(e){let u=await i();return(await u.selectFrom("kubun_hub_group_members").select("member_did").where("group_id","=",e).execute()).map(e=>e.member_did)}}}
1
+ import{EventEmitter as e}from"@enkaku/event";export function createHubStore(u){let n,t,{adapter:a}=u;async function i(){return null==n&&(n=await u.kysely),n}async function s(){let e=await i();if(null==t){let u=await e.selectFrom("kubun_hub_messages").select(e.fn.max("sequence_id").as("max_id")).executeTakeFirst();t=u?.max_id!=null?Number.parseInt(u.max_id,10):0}return String(++t).padStart(12,"0")}let r=new e;async function _(e){if(0===e.sequenceIDs.length)return;let u=await i();await u.deleteFrom("kubun_hub_deliveries").where("recipient_did","=",e.recipientDID).where("sequence_id","in",e.sequenceIDs).execute();let n=new Set((await u.selectFrom("kubun_hub_deliveries").select("sequence_id").where("sequence_id","in",e.sequenceIDs).execute()).map(e=>e.sequence_id)),t=e.sequenceIDs.filter(e=>!n.has(e));t.length>0&&await u.deleteFrom("kubun_hub_messages").where("sequence_id","in",t).execute()}async function c(e){let u=await i(),n=Date.now()-1e3*e.olderThan,t=(await u.selectFrom("kubun_hub_messages").select("sequence_id").where("stored_at","<=",n).execute()).map(e=>e.sequence_id);return 0===t.length?[]:(await u.deleteFrom("kubun_hub_deliveries").where("sequence_id","in",t).execute(),await u.deleteFrom("kubun_hub_messages").where("sequence_id","in",t).execute(),await r.emit("purge",{sequenceIDs:t}),t)}return{events:r,store:async function(e){let u=await s(),n=Date.now(),t=await i();return await t.insertInto("kubun_hub_messages").values({sequence_id:u,sender_did:e.senderDID,group_id:e.groupID??null,payload:a.encodeBinary(e.payload),stored_at:n}).execute(),e.recipients.length>0&&await t.insertInto("kubun_hub_deliveries").values(e.recipients.map(e=>({sequence_id:u,recipient_did:e}))).execute(),u},fetch:async function(e){null!=e.ack&&e.ack.length>0&&await _({recipientDID:e.recipientDID,sequenceIDs:e.ack});let u=(await i()).selectFrom("kubun_hub_deliveries").innerJoin("kubun_hub_messages","kubun_hub_messages.sequence_id","kubun_hub_deliveries.sequence_id").select(["kubun_hub_messages.sequence_id","kubun_hub_messages.sender_did","kubun_hub_messages.group_id","kubun_hub_messages.payload"]).where("kubun_hub_deliveries.recipient_did","=",e.recipientDID).orderBy("kubun_hub_messages.sequence_id","asc");null!=e.after&&(u=u.where("kubun_hub_messages.sequence_id",">",e.after));let n=e.limit;null!=n&&(u=u.limit(n+1));let t=await u.execute(),a=!1,s=t;null!=n&&t.length>n&&(a=!0,s=t.slice(0,n));let r=s.map(e=>({sequenceID:e.sequence_id,senderDID:e.sender_did,groupID:e.group_id??void 0,payload:e.payload})),c=r.length>0?r[r.length-1].sequenceID:null;return{messages:r,cursor:c,hasMore:a}},ack:_,purge:c,storeKeyPackage:async function(e,u){let n=await i();await n.insertInto("kubun_hub_key_packages").values({owner_did:e,key_package:u}).execute()},fetchKeyPackages:async function(e,u){let n=await i();return(await n.deleteFrom("kubun_hub_key_packages").where("id","in",n.selectFrom("kubun_hub_key_packages").select("id").where("owner_did","=",e).orderBy("id","asc").limit(u??1)).returning("key_package").execute()).map(e=>e.key_package)},setGroupMembers:async function(e,u){let n=await i();await n.deleteFrom("kubun_hub_group_members").where("group_id","=",e).execute(),u.length>0&&await n.insertInto("kubun_hub_group_members").values(u.map(u=>({group_id:e,member_did:u}))).execute()},getGroupMembers:async function(e){let u=await i();return(await u.selectFrom("kubun_hub_group_members").select("member_did").where("group_id","=",e).execute()).map(e=>e.member_did)}}}
package/lib/index.d.ts CHANGED
@@ -1,8 +1,13 @@
1
+ import type { Identity } from '@enkaku/token';
1
2
  import type { Adapter } from '@kubun/db-adapter';
2
- import { type Kysely } from 'kysely';
3
- import { type HubDatabase } from './hub-store.js';
3
+ import type { Kysely } from 'kysely';
4
+ import { type HubAuthConfig, type RelayRetentionConfig } from './create-relay.js';
5
+ import type { HubDatabase } from './hub-store.js';
6
+ import type { RelayObservers } from './observers.js';
7
+ export { type CreateRelayOptions, createRelay, createRelayInternal, type HubAuthConfig, type Relay, type RelayRetentionConfig, } from './create-relay.js';
4
8
  export { type CreateHubStoreParams, createHubStore, type HubDatabase } from './hub-store.js';
5
9
  export { createHubMigrations, type MigrationContext } from './migrations.js';
10
+ export { decorateHubStore, type RelayObserverAckInfo, type RelayObserverAuthRejectedInfo, type RelayObserverErrorInfo, type RelayObserverErrorStage, type RelayObserverFetchInfo, type RelayObserverPurgeInfo, type RelayObserverStoreInfo, type RelayObservers, } from './observers.js';
6
11
  export type HubOptions = {
7
12
  /** Kysely instance for the hub database. */
8
13
  kysely: Kysely<HubDatabase>;
@@ -12,6 +17,21 @@ export type HubOptions = {
12
17
  port?: number;
13
18
  /** Allowed CORS origin(s). Default: ['*']. */
14
19
  allowedOrigin?: string | Array<string>;
20
+ /**
21
+ * Hub server identity. Required when `auth` is provided — the server
22
+ * signs response tokens and clients pin its DID for TOFU verification.
23
+ */
24
+ identity?: Identity;
25
+ /**
26
+ * Authentication configuration. When omitted, the hub runs without
27
+ * access control (back-compat). When provided, `identity` must also be
28
+ * provided.
29
+ */
30
+ auth?: HubAuthConfig;
31
+ /** Optional retention scheduler. */
32
+ retention?: RelayRetentionConfig;
33
+ /** Optional observers for counters / diagnostics. */
34
+ observers?: RelayObservers;
15
35
  };
16
36
  export type Hub = {
17
37
  port: number;
package/lib/index.js CHANGED
@@ -1 +1 @@
1
- import{ServerTransport as t}from"@enkaku/http-server-transport";import{createHandlers as r,HubClientRegistry as e}from"@enkaku/hub-server";import{Server as o}from"@enkaku/server";import{serve as a}from"@hono/node-server";import s from"get-port";import{Hono as i}from"hono";import{Migrator as n}from"kysely";import{createHubStore as m}from"./hub-store.js";import{createHubMigrations as p}from"./migrations.js";export{createHubStore}from"./hub-store.js";export{createHubMigrations}from"./migrations.js";export async function createHub(l){var c;let{kysely:u,adapter:f}=l,h=p({types:f.types,functions:f.functions}),g=new n({db:u,provider:{getMigrations:async()=>h},migrationTableName:"kubun_hub_migration",migrationLockTableName:"kubun_hub_migration_lock"}),{error:w}=await g.migrateToLatest();if(null!=w)throw w;let b=r({registry:new e,store:m({kysely:u,adapter:f})}),k=new i;k.get("/health",t=>t.json({status:"ok"}));let y=new t({allowedOrigin:null==(c=l.allowedOrigin)?["*"]:"string"==typeof c?c.split(",").map(t=>t.trim()):c}),d=new o({accessControl:!1,handlers:b,transports:[y]});k.post("*",async t=>y.fetch(t.req.raw)),k.options("*",async t=>y.fetch(t.req.raw));let v=l.port??await s(),j=`http://localhost:${v}`,_=await new Promise(t=>{let r=a({fetch:k.fetch,port:v},()=>t(r))});return{port:v,url:j,dispose:async()=>{await d.dispose(),_.close()}}}
1
+ import{serve as e}from"@hono/node-server";import t from"get-port";import{Hono as r}from"hono";import{createRelayInternal as o}from"./create-relay.js";export{createRelay,createRelayInternal}from"./create-relay.js";export{createHubStore}from"./hub-store.js";export{createHubMigrations}from"./migrations.js";export{decorateHubStore}from"./observers.js";export async function createHub(a){let s=await o({kysely:a.kysely,adapter:a.adapter,identity:a.identity,auth:a.auth,allowedOrigin:a.allowedOrigin,retention:a.retention,observers:a.observers,ownsKysely:!1}),i=new r;i.get("/health",e=>e.json({status:"ok"})),i.post("*",async e=>s.fetch(e.req.raw)),i.options("*",async e=>s.fetch(e.req.raw));let n=a.port??await t(),c=`http://localhost:${n}`,l=await new Promise(t=>{let r=e({fetch:i.fetch,port:n},()=>t(r))});return{port:n,url:c,dispose:async()=>{await s.dispose(),l.close()}}}
@@ -0,0 +1,35 @@
1
+ import type { HubStore } from '@enkaku/hub-protocol';
2
+ export type RelayObserverStoreInfo = {
3
+ senderDID: string;
4
+ groupID?: string;
5
+ recipientCount: number;
6
+ sequenceID: string;
7
+ };
8
+ export type RelayObserverFetchInfo = {
9
+ recipientDID: string;
10
+ messageCount: number;
11
+ };
12
+ export type RelayObserverAckInfo = {
13
+ recipientDID: string;
14
+ sequenceCount: number;
15
+ };
16
+ export type RelayObserverPurgeInfo = {
17
+ sequenceCount: number;
18
+ };
19
+ export type RelayObserverAuthRejectedInfo = {
20
+ reason: string;
21
+ };
22
+ export type RelayObserverErrorStage = 'store' | 'fetch' | 'ack' | 'purge';
23
+ export type RelayObserverErrorInfo = {
24
+ stage: RelayObserverErrorStage;
25
+ error: unknown;
26
+ };
27
+ export type RelayObservers = {
28
+ onStore?: (info: RelayObserverStoreInfo) => void;
29
+ onFetch?: (info: RelayObserverFetchInfo) => void;
30
+ onAck?: (info: RelayObserverAckInfo) => void;
31
+ onPurge?: (info: RelayObserverPurgeInfo) => void;
32
+ onAuthRejected?: (info: RelayObserverAuthRejectedInfo) => void;
33
+ onError?: (info: RelayObserverErrorInfo) => void;
34
+ };
35
+ export declare function decorateHubStore(store: HubStore, observers?: RelayObservers): HubStore;
@@ -0,0 +1 @@
1
+ function e(e,t){if(null!=e)try{e(t)}catch{}}export function decorateHubStore(t,r){if(null==r)return t;let{onStore:n,onFetch:c,onAck:s,onError:a}=r;return{events:t.events,storeKeyPackage:t.storeKeyPackage.bind(t),fetchKeyPackages:t.fetchKeyPackages.bind(t),setGroupMembers:t.setGroupMembers.bind(t),getGroupMembers:t.getGroupMembers.bind(t),purge:t.purge.bind(t),async store(r){try{let c=await t.store(r);return e(n,{senderDID:r.senderDID,groupID:r.groupID,recipientCount:r.recipients.length,sequenceID:c}),c}catch(t){throw e(a,{stage:"store",error:t}),t}},async fetch(r){try{let n=await t.fetch(r);return e(c,{recipientDID:r.recipientDID,messageCount:n.messages.length}),n}catch(t){throw e(a,{stage:"fetch",error:t}),t}},async ack(r){try{await t.ack(r),e(s,{recipientDID:r.recipientDID,sequenceCount:r.sequenceIDs.length})}catch(t){throw e(a,{stage:"ack",error:t}),t}}}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubun/hub",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "license": "see LICENSE.md",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -14,25 +14,27 @@
14
14
  ],
15
15
  "sideEffects": false,
16
16
  "dependencies": {
17
- "@enkaku/event": "^0.14.1",
18
- "@enkaku/http-server-transport": "^0.14.2",
19
- "@enkaku/hub-protocol": "^0.14.0",
20
- "@enkaku/hub-server": "^0.14.1",
21
- "@enkaku/server": "^0.14.3",
22
- "@hono/node-server": "^1.19.12",
17
+ "@enkaku/event": "^0.15.0",
18
+ "@enkaku/http-server-transport": "^0.15.0",
19
+ "@enkaku/hub-protocol": "^0.15.0",
20
+ "@enkaku/hub-server": "^0.15.0",
21
+ "@enkaku/server": "^0.15.0",
22
+ "@enkaku/token": "^0.15.0",
23
+ "@hono/node-server": "^2.0.2",
23
24
  "get-port": "^7.2.0",
24
- "hono": "^4.12.9",
25
- "kysely": "^0.28.16",
26
- "@kubun/db-adapter": "^0.8.0"
25
+ "hono": "^4.12.17",
26
+ "kysely": "^0.28.17",
27
+ "@kubun/db-adapter": "^0.9.0",
28
+ "@kubun/db-provider": "^0.9.0"
27
29
  },
28
30
  "devDependencies": {
29
- "@enkaku/async": "^0.14.0",
30
- "@enkaku/client": "^0.14.3",
31
- "@enkaku/http-client-transport": "^0.14.3",
32
- "@enkaku/protocol": "^0.14.0",
33
- "@enkaku/transport": "0.14.0",
34
- "@kubun/db-node-sqlite": "^0.8.0",
35
- "@kubun/db-better-sqlite": "^0.8.0"
31
+ "@enkaku/async": "^0.15.0",
32
+ "@enkaku/client": "^0.15.1",
33
+ "@enkaku/http-client-transport": "^0.15.0",
34
+ "@enkaku/protocol": "^0.15.0",
35
+ "@enkaku/transport": "^0.15.0",
36
+ "@kubun/db-node-sqlite": "^0.9.0",
37
+ "@kubun/db-better-sqlite": "^0.9.0"
36
38
  },
37
39
  "scripts": {
38
40
  "build:clean": "del lib",