@kumiai/hub-server 0.12.1 → 0.13.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/handlers.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { HandlerError } from '@enkaku/server';
2
2
  import { HUB_ERROR_CODES, hubErrorCodeOf, InvalidPayloadError, KeyPackageFetchLimitError, keyPackageDigest, wakeRecipientKeyProblem } from '@kumiai/hub-protocol';
3
3
  import { fromB64, toB64 } from '@sozai/codec';
4
- import { getReporter } from '@sozai/log';
4
+ import { getLogger, getReporter } from '@sozai/log';
5
5
  import { createRateLimiter } from './rateLimit.js';
6
6
  function normalizeAuthorizeDecision(decision) {
7
7
  return typeof decision === 'boolean' ? {
@@ -29,6 +29,11 @@ const reportStoreError = getReporter([
29
29
  'kumiai',
30
30
  'hub-server'
31
31
  ], '@kumiai/hub-server');
32
+ const receiveLogger = getLogger([
33
+ 'kumiai',
34
+ 'hub-server',
35
+ 'receive'
36
+ ]);
32
37
  /** What the failed operation was about, for the default log line: a DID for the per-recipient
33
38
  * methods, a topic for fan-out, nothing for a store-wide purge. */ function subjectOf(event) {
34
39
  switch(event.method){
@@ -550,11 +555,22 @@ export function createHandlers(params) {
550
555
  // backpressure stalls `writer.write` until `receiveBufferLimit` tears the channel down.
551
556
  function waitForRoom(bytes) {
552
557
  if (tornDown || hasRoom(bytes)) return Promise.resolve();
558
+ receiveLogger.debug('receive window full for {recipientDID}: paused with {frames} frames ({bytes} bytes) unacked', {
559
+ recipientDID: clientDID,
560
+ frames: inFlight.size,
561
+ bytes: inFlightBytes
562
+ });
553
563
  return new Promise((resolve)=>{
554
564
  let timer;
555
565
  const arm = ()=>{
556
566
  clearTimeout(timer);
557
567
  timer = setTimeout(()=>{
568
+ receiveLogger.warn('receive ack timeout for {recipientDID}: released {frames} frames ({bytes} bytes) after {timeoutMs} ms', {
569
+ recipientDID: clientDID,
570
+ frames: inFlight.size,
571
+ bytes: inFlightBytes,
572
+ timeoutMs: receiveAckTimeout
573
+ });
558
574
  releaseAll();
559
575
  notifyWindow();
560
576
  }, receiveAckTimeout);
@@ -593,13 +609,25 @@ export function createHandlers(params) {
593
609
  const done = new Promise((resolve)=>{
594
610
  resolveDone = resolve;
595
611
  });
612
+ function logUnackedFrames() {
613
+ if (inFlight.size === 0) return;
614
+ receiveLogger.debug('receive stream ended for {recipientDID} with {frames} frames ({bytes} bytes) unacked', {
615
+ recipientDID: clientDID,
616
+ frames: inFlight.size,
617
+ bytes: inFlightBytes
618
+ });
619
+ }
596
620
  // Idempotent teardown, shared by every failure and end path. Frames stay pending in the store
597
621
  // and redeliver on the next connect (store-and-forward).
598
622
  function finish() {
599
623
  if (tornDown) return;
624
+ logUnackedFrames();
600
625
  tornDown = true;
601
626
  notifyWindow();
602
- if (receiveToken != null) registry.releaseReceiveWriter(clientDID, receiveToken);
627
+ if (receiveToken != null) registry.releaseReceiveWriter({
628
+ did: clientDID,
629
+ token: receiveToken
630
+ });
603
631
  registry.unregisterIfIdle(clientDID);
604
632
  reader.cancel().catch(()=>{});
605
633
  writer.abort(new Error('receive channel torn down')).catch(()=>{});
@@ -693,7 +721,11 @@ export function createHandlers(params) {
693
721
  // Binding evicts whatever held the lane: a reconnect means the old connection broke and the
694
722
  // server learns last, so the stale writer must give way to the live one. See
695
723
  // `HubClientRegistry.bindReceiveWriter`.
696
- const { token, evicted } = registry.bindReceiveWriter(clientDID, onLive, ()=>endEvicted?.());
724
+ const { token, evicted } = registry.bindReceiveWriter({
725
+ did: clientDID,
726
+ writer: onLive,
727
+ endReceive: ()=>endEvicted?.()
728
+ });
697
729
  receiveToken = token;
698
730
  // After the bind, so the lane is never unheld between the two.
699
731
  evicted?.();
@@ -742,9 +774,13 @@ export function createHandlers(params) {
742
774
  }
743
775
  } catch (error) {
744
776
  // A synchronous drain error (e.g. store.fetch threw): clean up and reject the handler.
777
+ if (!tornDown) logUnackedFrames();
745
778
  tornDown = true;
746
779
  notifyWindow();
747
- if (receiveToken != null) registry.releaseReceiveWriter(clientDID, receiveToken);
780
+ if (receiveToken != null) registry.releaseReceiveWriter({
781
+ did: clientDID,
782
+ token: receiveToken
783
+ });
748
784
  registry.unregisterIfIdle(clientDID);
749
785
  reader.cancel().catch(()=>{});
750
786
  writer.abort(error).catch(()=>{});
package/lib/hub.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { ServerTransportOf } from '@enkaku/protocol';
2
2
  import type { AccessRules, ReplayOptions, ResourceLimits, Server } from '@enkaku/server';
3
3
  import type { VerifyTokenHook } from '@kokuin/capability';
4
- import type { Identity } from '@kokuin/token';
4
+ import type { CryptoOptions, Identity } from '@kokuin/token';
5
5
  import type { HubProtocol, HubStore, WakeRegistry, WakeSender } from '@kumiai/hub-protocol';
6
6
  import { type AuthorizeHook, type HubRateLimits, type HubStoreErrorHook, type KeyPackageFetchLimits } from './handlers.js';
7
7
  import { HubClientRegistry } from './registry.js';
@@ -28,7 +28,7 @@ export type HubPurgeOptions = {
28
28
  };
29
29
  /** Replay settings a host may tune. The hub always enables replay checks and rejects stale tokens. */
30
30
  export type HubReplayOptions = Omit<ReplayOptions, 'enabled' | 'rejectStale'>;
31
- export type CreateHubParams = {
31
+ export type CreateHubParams = CryptoOptions & {
32
32
  transport: ServerTransportOf<HubProtocol>;
33
33
  store: HubStore;
34
34
  /**
package/lib/hub.js CHANGED
@@ -55,6 +55,7 @@ export function createHub(params) {
55
55
  protocol: hubProtocol,
56
56
  transport: params.transport,
57
57
  identity: params.identity,
58
+ crypto: params.crypto,
58
59
  accessRules: params.accessRules ?? DEFAULT_HUB_ACCESS_RULES,
59
60
  verifyToken: params.verifyToken,
60
61
  replay: {
package/lib/index.d.ts CHANGED
@@ -4,6 +4,6 @@ export type { CreateHubParams, HubInstance, HubPurgeOptions, HubReplayOptions }
4
4
  export { createHub, DEFAULT_HUB_ACCESS_RULES } from './hub.js';
5
5
  export { createMemoryStore, type MemoryStoreOptions } from './memoryStore.js';
6
6
  export { createRateLimiter, type RateLimitConfig, type RateLimiter } from './rateLimit.js';
7
- export type { ClientEntry } from './registry.js';
7
+ export type { ClientEntry, HubClientRegistryBindReceiveWriterParams, HubClientRegistryReleaseReceiveWriterParams, } from './registry.js';
8
8
  export { HubClientRegistry } from './registry.js';
9
9
  export { createWakeDispatcher, type WakeDispatcher, type WakeDispatcherParams, type WakeNotifyParams, } from './wake.js';
package/lib/registry.d.ts CHANGED
@@ -7,6 +7,15 @@ export type ClientEntry = {
7
7
  /** Identifies the binding, so a channel can only ever release its OWN. */
8
8
  token: symbol | null;
9
9
  };
10
+ export type HubClientRegistryBindReceiveWriterParams = {
11
+ did: string;
12
+ writer: (message: StoredMessage) => void;
13
+ endReceive: () => void;
14
+ };
15
+ export type HubClientRegistryReleaseReceiveWriterParams = {
16
+ did: string;
17
+ token: symbol;
18
+ };
10
19
  /**
11
20
  * Tracks online clients and their live receive-channel writers. Subscription
12
21
  * state is durable in the store, not here — the registry only routes live
@@ -39,12 +48,12 @@ export declare class HubClientRegistry {
39
48
  * it. So a release names the binding it is releasing, and one that is no longer current is a
40
49
  * no-op.
41
50
  */
42
- bindReceiveWriter(did: string, writer: (message: StoredMessage) => void, endReceive: () => void): {
51
+ bindReceiveWriter({ did, writer, endReceive }: HubClientRegistryBindReceiveWriterParams): {
43
52
  token: symbol;
44
53
  evicted: (() => void) | null;
45
54
  };
46
55
  /** Clear the binding this token names. A token that is no longer current is a no-op. */
47
- releaseReceiveWriter(did: string, token: symbol): void;
56
+ releaseReceiveWriter({ did, token }: HubClientRegistryReleaseReceiveWriterParams): void;
48
57
  getClient(did: string): ClientEntry | undefined;
49
58
  isOnline(did: string): boolean;
50
59
  isWriterBound(did: string): boolean;
package/lib/registry.js CHANGED
@@ -47,7 +47,7 @@
47
47
  * null the writer the new channel has just bound, leaving the DID online with nothing behind
48
48
  * it. So a release names the binding it is releasing, and one that is no longer current is a
49
49
  * no-op.
50
- */ bindReceiveWriter(did, writer, endReceive) {
50
+ */ bindReceiveWriter({ did, writer, endReceive }) {
51
51
  const entry = this.#clients.get(did);
52
52
  if (entry == null) return {
53
53
  token: Symbol('unbound'),
@@ -63,7 +63,7 @@
63
63
  evicted
64
64
  };
65
65
  }
66
- /** Clear the binding this token names. A token that is no longer current is a no-op. */ releaseReceiveWriter(did, token) {
66
+ /** Clear the binding this token names. A token that is no longer current is a no-op. */ releaseReceiveWriter({ did, token }) {
67
67
  const entry = this.#clients.get(did);
68
68
  if (entry == null || entry.token !== token) return;
69
69
  entry.sendMessage = null;
package/lib/wake.js CHANGED
@@ -31,12 +31,15 @@ const DEFAULT_DEBOUNCE_MS = 10_000;
31
31
  const registration = await params.registry.get(did);
32
32
  if (registration == null) return;
33
33
  const body = sealWakeHint({
34
- topicID,
35
- sequenceID,
36
- count
37
- }, {
38
- publicKey: fromB64U(registration.publicKey),
39
- authSecret: fromB64U(registration.authSecret)
34
+ hint: {
35
+ topicID,
36
+ sequenceID,
37
+ count
38
+ },
39
+ recipient: {
40
+ publicKey: fromB64U(registration.publicKey),
41
+ authSecret: fromB64U(registration.authSecret)
42
+ }
40
43
  });
41
44
  const verdict = await params.sender.send({
42
45
  registration,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kumiai/hub-server",
3
- "version": "0.12.1",
3
+ "version": "0.13.0",
4
4
  "description": "Hub server for Enkaku E2EE group message routing",
5
5
  "keywords": [
6
6
  "hub",
@@ -25,20 +25,21 @@
25
25
  "lib/*"
26
26
  ],
27
27
  "dependencies": {
28
- "@enkaku/protocol": "^0.22.4",
29
- "@enkaku/server": "^0.22.4",
30
- "@kokuin/capability": "^0.5.2",
31
- "@kokuin/token": "^0.6.4",
32
- "@kumiai/hub-protocol": "^0.12.0",
28
+ "@enkaku/protocol": "^0.23.0",
29
+ "@enkaku/server": "^0.23.0",
30
+ "@kokuin/capability": "^0.7.0",
31
+ "@kokuin/token": "^0.7.0",
32
+ "@kumiai/hub-protocol": "^0.13.0",
33
33
  "@sozai/codec": "^0.4.4",
34
34
  "@sozai/event": "^0.1.6",
35
35
  "@sozai/log": "^0.4.0"
36
36
  },
37
37
  "devDependencies": {
38
- "@enkaku/client": "^0.22.4",
39
- "@enkaku/transport": "^0.22.4",
40
- "@kumiai/hub-conformance": "^0.12.0",
41
- "@kumiai/hub-wake": "^0.12.0"
38
+ "@enkaku/client": "^0.23.0",
39
+ "@enkaku/transport": "^0.23.0",
40
+ "@kokuin/crypto": "^0.7.0",
41
+ "@kumiai/hub-conformance": "^0.13.0",
42
+ "@kumiai/hub-wake": "^0.13.0"
42
43
  },
43
44
  "publishConfig": {
44
45
  "access": "public"