@kumiai/hub-server 0.5.0 → 0.7.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/README.md CHANGED
@@ -31,10 +31,13 @@ another sender. The `identity` param is required for that reason.
31
31
 
32
32
  Authorization is two layers, and only the second one knows about topics: `accessRules` gate the
33
33
  procedures (the default lets any authenticated DID call them), and the optional
34
- `authorize(request)` hook decides publish and subscribe per topic. It takes one discriminated
35
- `AuthorizeRequest` — narrow on `request.action` — and returns `AuthorizeDecision`, either a boolean
36
- or `{ allow, reason?, code?, retryAfterMs? }`. `AuthorizeRequest` names seven actions, but only
37
- `publish` and `subscribe` are dispatched today. The default allows any authenticated DID.
34
+ `authorize(request)` hook decides the rest. It takes one discriminated `AuthorizeRequest` — narrow
35
+ on `request.action` — and returns `AuthorizeDecision`, either a boolean or
36
+ `{ allow, reason?, code?, retryAfterMs? }`. `AuthorizeRequest` names nine actions, of which only
37
+ `unsubscribe` is not dispatched today; the two `wake/*` actions gate push registration, which is the
38
+ one durable per-device identifier the hub stores. The default allows any authenticated DID, and a
39
+ hook should allow any action it does not recognise, so that a hook written before a variant shipped
40
+ does not start refusing a procedure that used to be ungated.
38
41
 
39
42
  ## Durable subscription, ephemeral connection
40
43
 
package/lib/handlers.d.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  import { type ProcedureHandlers } from '@enkaku/server';
2
- import type { HubProtocol, HubStore } from '@kumiai/hub-protocol';
2
+ import type { HubProtocol, HubStore, WakeRegistry } from '@kumiai/hub-protocol';
3
3
  import { type RateLimitConfig } from './rateLimit.js';
4
4
  import type { HubClientRegistry } from './registry.js';
5
+ import type { WakeDispatcher } from './wake.js';
5
6
  /**
6
- * A single request to authorize. All seven variants ship even though only `publish` and
7
- * `subscribe` are currently enforced: the union is the exhaustive-switch surface a host's own
8
- * `switch (req.action)` closes over.
7
+ * A single request to authorize. All nine variants ship even though `unsubscribe` is not yet
8
+ * dispatched: the union is the exhaustive-switch surface a host's own `switch (req.action)` closes
9
+ * over, and widening it after a release is a break for every host that closed over it.
9
10
  *
10
11
  * A host's `switch` need not handle every action today — an unrecognized action should default to
11
12
  * allow, so a hook written before a new variant shipped doesn't silently start refusing a
@@ -43,6 +44,25 @@ export type AuthorizeRequest = {
43
44
  } | {
44
45
  action: 'keypackage/status';
45
46
  did: string;
47
+ }
48
+ /**
49
+ * A device registering a push endpoint. `kind` (the opaque sender tag) and `expiresAt` (the
50
+ * caller-supplied registration lifetime cap, when given) are the only details a host can usefully
51
+ * gate on — `endpoint`, `publicKey` and `authSecret` are deliberately absent, since a hook that
52
+ * saw them would be a second place the endpoint gets read, and the hub's rule is that it never
53
+ * interprets one.
54
+ *
55
+ * This is the durable cross-group per-device identifier wake introduces, so it is the one a host
56
+ * is most likely to want a say over.
57
+ */
58
+ | {
59
+ action: 'wake/register';
60
+ did: string;
61
+ kind: string;
62
+ expiresAt?: number;
63
+ } | {
64
+ action: 'wake/unregister';
65
+ did: string;
46
66
  };
47
67
  /**
48
68
  * A plain `boolean` is shorthand for `{ allow: boolean }`, with no reason, code, or retry hint.
@@ -89,6 +109,12 @@ export type HubStoreErrorEvent = {
89
109
  method: 'getSubscribers';
90
110
  topicID: string;
91
111
  error: unknown;
112
+ }
113
+ /** The DID whose wake dispatch (send, or the registry read/delete around it) failed. */
114
+ | {
115
+ method: 'wake';
116
+ did: string;
117
+ error: unknown;
92
118
  };
93
119
  export type HubStoreErrorHook = (event: HubStoreErrorEvent) => void;
94
120
  /**
@@ -141,5 +167,13 @@ export type CreateHandlersParams = {
141
167
  * empty handler to silence it deliberately.
142
168
  */
143
169
  onStoreError?: HubStoreErrorHook;
170
+ /**
171
+ * Wake support. Absent: both wake procedures refuse. The registry is separate from the
172
+ * dispatcher because the handlers only ever store and remove; sending is the dispatcher's.
173
+ */
174
+ wake?: {
175
+ registry: WakeRegistry;
176
+ dispatcher?: WakeDispatcher;
177
+ };
144
178
  };
145
179
  export declare function createHandlers(params: CreateHandlersParams): ProcedureHandlers<HubProtocol>;
package/lib/handlers.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { HandlerError } from '@enkaku/server';
2
- import { HUB_ERROR_CODES, hubErrorCodeOf, InvalidPayloadError, KeyPackageFetchLimitError, keyPackageDigest } from '@kumiai/hub-protocol';
2
+ import { HUB_ERROR_CODES, hubErrorCodeOf, InvalidPayloadError, KeyPackageFetchLimitError, keyPackageDigest, wakeRecipientKeyProblem } from '@kumiai/hub-protocol';
3
3
  import { fromB64, toB64 } from '@sozai/codec';
4
4
  import { getReporter } from '@sozai/log';
5
5
  import { createRateLimiter } from './rateLimit.js';
@@ -21,7 +21,8 @@ function normalizeAuthorizeDecision(decision) {
21
21
  fetchLastResortKeyPackage: 'The fetch returned what the pool could serve, without the last-resort top-up. A read that ' + 'keeps failing means the availability floor the last-resort slot exists to provide is absent, ' + 'and joins fail downstream at the inviter with no signal here.',
22
22
  ack: 'The frame stays pending and the client re-acks on the next round. An ack that keeps failing ' + 'redelivers every frame forever.',
23
23
  purge: 'Retried on the next interval. A purge that keeps failing means the store grows without bound.',
24
- getSubscribers: 'The frame is committed and queued, but the live push to connected subscribers was skipped: ' + 'each of them stops receiving new frames until it reconnects. A getSubscribers that keeps ' + 'failing means the hub has silently degraded from push to pull for every publish.'
24
+ getSubscribers: 'The frame is committed and queued, but the live push to connected subscribers was skipped: ' + 'each of them stops receiving new frames until it reconnects. A getSubscribers that keeps ' + 'failing means the hub has silently degraded from push to pull for every publish.',
25
+ wake: 'The offline device was not pinged for this frame. It still redelivers on the next reconnect ' + '(store-and-forward), so nothing is lost — but a wake that keeps failing means the device ' + 'stays asleep until something else wakes it.'
25
26
  };
26
27
  const reportStoreError = getReporter([
27
28
  'kumiai',
@@ -36,6 +37,7 @@ const reportStoreError = getReporter([
36
37
  return ` on topic ${event.topicID}`;
37
38
  case 'ack':
38
39
  case 'fetchLastResortKeyPackage':
40
+ case 'wake':
39
41
  return ` for ${event.did}`;
40
42
  }
41
43
  }
@@ -293,6 +295,29 @@ export function createHandlers(params) {
293
295
  payload: payloadBytes,
294
296
  ...logPosition
295
297
  });
298
+ } else {
299
+ // No live channel: the frame is durably queued, so all that is missing is a nudge to
300
+ // come and get it. Both retention classes wake — a commit-lane frame is log-class, and
301
+ // a membership change is exactly what a sleeping device must learn.
302
+ //
303
+ // Guarded even though `notify` is documented never to throw: `dispatcher` is
304
+ // caller-injectable (`CreateHandlersParams.wake.dispatcher`), and a misbehaving
305
+ // third-party implementation must not abort this loop mid-way — that would cost live
306
+ // delivery to every subscriber still to come, for a publish whose append already
307
+ // committed.
308
+ try {
309
+ params.wake?.dispatcher?.notify({
310
+ did: recipientDID,
311
+ topicID,
312
+ sequenceID
313
+ });
314
+ } catch (error) {
315
+ storeErrorReporter({
316
+ method: 'wake',
317
+ did: recipientDID,
318
+ error
319
+ });
320
+ }
296
321
  }
297
322
  }
298
323
  }
@@ -480,6 +505,8 @@ export function createHandlers(params) {
480
505
  receiveToken = token;
481
506
  // After the bind, so the lane is never unheld between the two.
482
507
  evicted?.();
508
+ // The device is draining; a trailing summary would be noise it has already outrun.
509
+ params.wake?.dispatcher?.online(clientDID);
483
510
  // Drain the backlog. Await each page so lastServed is exact before the flush.
484
511
  let cursor = after;
485
512
  while(!tornDown){
@@ -767,6 +794,111 @@ export function createHandlers(params) {
767
794
  count,
768
795
  lastResort: stored == null ? null : keyPackageDigest(stored)
769
796
  };
797
+ },
798
+ 'hub/v1/wake/register': async (ctx)=>{
799
+ const clientDID = getClientDID(ctx);
800
+ const wake = params.wake;
801
+ if (wake == null) {
802
+ throw new HandlerError({
803
+ code: HUB_ERROR_CODES.wakeNotSupported,
804
+ message: 'This hub does not support wake notifications'
805
+ });
806
+ }
807
+ // After the not-supported refusal, before the rate limit — the order every enforced variant
808
+ // uses. Configuration is answered first because a hub with no wake support has nothing for a
809
+ // host to have an opinion about.
810
+ const decision = normalizeAuthorizeDecision(await authorize({
811
+ action: 'wake/register',
812
+ did: clientDID,
813
+ kind: ctx.param.kind,
814
+ expiresAt: ctx.param.expiresAt
815
+ }));
816
+ if (!decision.allow) {
817
+ throw new HandlerError({
818
+ code: HUB_ERROR_CODES.authorizationDenied,
819
+ message: decision.reason ?? 'Not authorized to register a wake endpoint'
820
+ });
821
+ }
822
+ if (!didLimiter.tryConsume(clientDID)) {
823
+ throw new HandlerError({
824
+ code: 'EK01',
825
+ message: 'Wake rate limit exceeded for DID'
826
+ });
827
+ }
828
+ // The param schema bounds the length (`minLength: 1`, `maxLength: 128`/`64`) but not the
829
+ // shape, which accepts key material the hub can never seal to. Storing it answers
830
+ // `registered: true` and then fails inside every single seal — one error per frame, forever,
831
+ // since a seal failure is not a `gone` verdict and nothing removes the entry. The device
832
+ // believes it is reachable and is never woken.
833
+ // The rule itself lives in hub-protocol next to RFC 8291, so the hub needs no curve of its
834
+ // own to know that a 65-byte value off the curve fails exactly as a 33-byte one does.
835
+ const keyProblem = wakeRecipientKeyProblem({
836
+ publicKey: ctx.param.publicKey,
837
+ authSecret: ctx.param.authSecret
838
+ });
839
+ if (keyProblem != null) {
840
+ throw new HandlerError({
841
+ code: HUB_ERROR_CODES.invalidPayload,
842
+ message: `Wake ${keyProblem}`
843
+ });
844
+ }
845
+ try {
846
+ await wake.registry.put({
847
+ // The DID is the verified issuer, never a wire field — otherwise any member could
848
+ // redirect another member's wakes to an endpoint they control.
849
+ did: clientDID,
850
+ kind: ctx.param.kind,
851
+ endpoint: ctx.param.endpoint,
852
+ publicKey: ctx.param.publicKey,
853
+ authSecret: ctx.param.authSecret,
854
+ ...ctx.param.expiresAt != null ? {
855
+ expiresAt: ctx.param.expiresAt
856
+ } : {}
857
+ });
858
+ } catch (error) {
859
+ rethrowAsHandlerError(error);
860
+ }
861
+ return {
862
+ registered: true
863
+ };
864
+ },
865
+ 'hub/v1/wake/unregister': async (ctx)=>{
866
+ const clientDID = getClientDID(ctx);
867
+ const wake = params.wake;
868
+ if (wake == null) {
869
+ throw new HandlerError({
870
+ code: HUB_ERROR_CODES.wakeNotSupported,
871
+ message: 'This hub does not support wake notifications'
872
+ });
873
+ }
874
+ const decision = normalizeAuthorizeDecision(await authorize({
875
+ action: 'wake/unregister',
876
+ did: clientDID
877
+ }));
878
+ if (!decision.allow) {
879
+ throw new HandlerError({
880
+ code: HUB_ERROR_CODES.authorizationDenied,
881
+ message: decision.reason ?? 'Not authorized to unregister a wake endpoint'
882
+ });
883
+ }
884
+ // Matches `register` above and the subscribe/unsubscribe pair: both halves of a pair cost
885
+ // the same, or the uncounted half is the one an abusive caller uses.
886
+ if (!didLimiter.tryConsume(clientDID)) {
887
+ throw new HandlerError({
888
+ code: 'EK01',
889
+ message: 'Wake rate limit exceeded for DID'
890
+ });
891
+ }
892
+ let existed;
893
+ try {
894
+ existed = await wake.registry.get(clientDID) != null;
895
+ await wake.registry.delete(clientDID);
896
+ } catch (error) {
897
+ rethrowAsHandlerError(error);
898
+ }
899
+ return {
900
+ unregistered: existed
901
+ };
770
902
  }
771
903
  };
772
904
  }
package/lib/hub.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { ServerTransportOf } from '@enkaku/protocol';
2
2
  import type { AccessRules, ResourceLimits, Server } from '@enkaku/server';
3
3
  import type { Identity } from '@kokuin/token';
4
- import { type HubProtocol, type HubStore } from '@kumiai/hub-protocol';
4
+ import type { HubProtocol, HubStore, WakeRegistry, WakeSender } from '@kumiai/hub-protocol';
5
5
  import { type AuthorizeHook, type HubRateLimits, type HubStoreErrorHook, type KeyPackageFetchLimits } from './handlers.js';
6
6
  import { HubClientRegistry } from './registry.js';
7
7
  /**
@@ -52,6 +52,17 @@ export type CreateHubParams = {
52
52
  * `controllerTimeoutMs` and from the `maxConcurrentHandlers` cap.
53
53
  */
54
54
  limits?: Partial<ResourceLimits>;
55
+ /**
56
+ * Wake notifications. Absent: `hub/v1/wake/*` refuse with `WakeNotSupportedError` — refusing is
57
+ * the only honest answer, since accepting a registration the hub will never act on leaves the
58
+ * device believing it is reachable.
59
+ */
60
+ wake?: {
61
+ registry: WakeRegistry;
62
+ sender: WakeSender;
63
+ /** Coalescing window in milliseconds. Default: 10 000. */
64
+ debounceMs?: number;
65
+ };
55
66
  };
56
67
  export type HubInstance = {
57
68
  registry: HubClientRegistry;
package/lib/hub.js CHANGED
@@ -2,6 +2,7 @@ import { serve } from '@enkaku/server';
2
2
  import { hubProtocol } from '@kumiai/hub-protocol';
3
3
  import { createHandlers, createStoreErrorReporter } from './handlers.js';
4
4
  import { HubClientRegistry } from './registry.js';
5
+ import { createWakeDispatcher } from './wake.js';
5
6
  /**
6
7
  * Default access rules: any authenticated DID may call hub procedures.
7
8
  * The hub is a blind relay — per-procedure authorization (the `authorize`
@@ -17,13 +18,27 @@ export function createHub(params) {
17
18
  // wrapper delegates to this instance rather than building a second one from the same hook —
18
19
  // which matters the moment a reporter holds state, as the throttling the README names would.
19
20
  const storeErrorReporter = createStoreErrorReporter(params.onStoreError);
21
+ const wakeDispatcher = params.wake == null ? undefined : createWakeDispatcher({
22
+ registry: params.wake.registry,
23
+ sender: params.wake.sender,
24
+ debounceMs: params.wake.debounceMs,
25
+ onError: ({ did, error })=>storeErrorReporter({
26
+ method: 'wake',
27
+ did,
28
+ error
29
+ })
30
+ });
20
31
  const handlers = createHandlers({
21
32
  registry,
22
33
  store: params.store,
23
34
  authorize: params.authorize,
24
35
  rateLimits: params.rateLimits,
25
36
  keyPackageFetchLimits: params.keyPackageFetchLimits,
26
- onStoreError: storeErrorReporter
37
+ onStoreError: storeErrorReporter,
38
+ wake: params.wake == null ? undefined : {
39
+ registry: params.wake.registry,
40
+ dispatcher: wakeDispatcher
41
+ }
27
42
  });
28
43
  const limits = {
29
44
  ...params.limits,
@@ -58,6 +73,9 @@ export function createHub(params) {
58
73
  }, interval);
59
74
  server.disposed.then(()=>clearInterval(purgeTimer));
60
75
  }
76
+ if (wakeDispatcher != null) {
77
+ server.disposed.then(()=>wakeDispatcher.dispose());
78
+ }
61
79
  return {
62
80
  registry,
63
81
  server
package/lib/index.d.ts CHANGED
@@ -6,3 +6,4 @@ export { createMemoryStore, type MemoryStoreOptions } from './memoryStore.js';
6
6
  export { createRateLimiter, type RateLimitConfig, type RateLimiter } from './rateLimit.js';
7
7
  export type { ClientEntry } from './registry.js';
8
8
  export { HubClientRegistry } from './registry.js';
9
+ export { createWakeDispatcher, type WakeDispatcher, type WakeDispatcherParams, type WakeNotifyParams, } from './wake.js';
package/lib/index.js CHANGED
@@ -3,3 +3,4 @@ export { createHub, DEFAULT_HUB_ACCESS_RULES } from './hub.js';
3
3
  export { createMemoryStore } from './memoryStore.js';
4
4
  export { createRateLimiter } from './rateLimit.js';
5
5
  export { HubClientRegistry } from './registry.js';
6
+ export { createWakeDispatcher } from './wake.js';
package/lib/wake.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { type WakeRegistry, type WakeSender } from '@kumiai/hub-protocol';
2
+ export type WakeDispatcherParams = {
3
+ registry: WakeRegistry;
4
+ sender: WakeSender;
5
+ /** Coalescing window in milliseconds. Default: 10 000. */
6
+ debounceMs?: number;
7
+ /** Reports a transient send failure or a throwing sender. Fire-and-forget. */
8
+ onError?: (params: {
9
+ did: string;
10
+ error: unknown;
11
+ }) => void;
12
+ };
13
+ export type WakeNotifyParams = {
14
+ did: string;
15
+ topicID: string;
16
+ sequenceID: string;
17
+ };
18
+ export type WakeDispatcher = {
19
+ /** A frame was queued for an OFFLINE subscriber. Never throws, never awaited by the caller. */
20
+ notify(params: WakeNotifyParams): void;
21
+ /** The device bound a receive channel: drop any pending trailing ping. */
22
+ online(did: string): void;
23
+ dispose(): void;
24
+ };
25
+ /**
26
+ * Coalesces wake pings per DID on a LEADING edge: the first frame pings immediately, then a window
27
+ * opens and everything inside it collapses into one trailing summary.
28
+ *
29
+ * Leading rather than trailing because the timer map is in-process. A hub restart drops pending
30
+ * windows; on a leading edge that loses at most a summary, whereas trailing-only would lose the
31
+ * notification itself every time a restart landed inside a window.
32
+ */
33
+ export declare function createWakeDispatcher(params: WakeDispatcherParams): WakeDispatcher;
package/lib/wake.js ADDED
@@ -0,0 +1,112 @@
1
+ import { sealWakeHint } from '@kumiai/hub-protocol';
2
+ import { fromB64U } from '@sozai/codec';
3
+ const DEFAULT_DEBOUNCE_MS = 10_000;
4
+ /**
5
+ * Coalesces wake pings per DID on a LEADING edge: the first frame pings immediately, then a window
6
+ * opens and everything inside it collapses into one trailing summary.
7
+ *
8
+ * Leading rather than trailing because the timer map is in-process. A hub restart drops pending
9
+ * windows; on a leading edge that loses at most a summary, whereas trailing-only would lose the
10
+ * notification itself every time a restart landed inside a window.
11
+ */ export function createWakeDispatcher(params) {
12
+ const debounceMs = params.debounceMs ?? DEFAULT_DEBOUNCE_MS;
13
+ const pending = new Map();
14
+ let disposed = false;
15
+ function report(did, error) {
16
+ try {
17
+ params.onError?.({
18
+ did,
19
+ error
20
+ });
21
+ } catch {
22
+ // A reporter that throws must not take the dispatcher with it.
23
+ }
24
+ }
25
+ function send(did, topicID, sequenceID, count) {
26
+ // Deliberately not awaited: a slow or hanging provider must never delay the publish fan-out
27
+ // this is called from.
28
+ void (async ()=>{
29
+ try {
30
+ const registration = await params.registry.get(did);
31
+ if (registration == null) return;
32
+ const body = sealWakeHint({
33
+ topicID,
34
+ sequenceID,
35
+ count
36
+ }, {
37
+ publicKey: fromB64U(registration.publicKey),
38
+ authSecret: fromB64U(registration.authSecret)
39
+ });
40
+ const verdict = await params.sender.send({
41
+ registration,
42
+ body
43
+ });
44
+ switch(verdict){
45
+ case 'delivered':
46
+ break;
47
+ case 'gone':
48
+ await params.registry.delete(did);
49
+ break;
50
+ case 'retry':
51
+ report(did, new Error('Wake send failed transiently'));
52
+ break;
53
+ default:
54
+ // Exhaustive over WakeVerdict: a future verdict must be handled explicitly here rather
55
+ // than silently falling through as 'delivered'.
56
+ verdict;
57
+ }
58
+ } catch (error) {
59
+ report(did, error);
60
+ }
61
+ })();
62
+ }
63
+ function openWindow(did) {
64
+ const entry = {
65
+ timer: setTimeout(()=>{
66
+ const current = pending.get(did);
67
+ pending.delete(did);
68
+ if (current?.latest == null) return;
69
+ send(did, current.latest.topicID, current.latest.sequenceID, current.count);
70
+ // Traffic is still flowing, so a fresh window opens behind the summary rather than letting
71
+ // the next frame ping immediately and undo the coalescing.
72
+ pending.set(did, openWindow(did));
73
+ }, debounceMs),
74
+ latest: null,
75
+ count: 0
76
+ };
77
+ // A pending wake must never hold a process open by itself. `.unref` is a Node timer method
78
+ // absent from the DOM `Timeout` type; it only typechecks here because this package's
79
+ // tsconfig.json (unlike its siblings') sets `"types": ["node"]`. If hub-server is ever
80
+ // aligned to drop that, this line needs `(entry.timer as NodeJS.Timeout).unref?.()` or an
81
+ // equivalent cast.
82
+ entry.timer.unref?.();
83
+ return entry;
84
+ }
85
+ return {
86
+ notify ({ did, topicID, sequenceID }) {
87
+ if (disposed) return;
88
+ const entry = pending.get(did);
89
+ if (entry == null) {
90
+ pending.set(did, openWindow(did));
91
+ send(did, topicID, sequenceID, 1);
92
+ return;
93
+ }
94
+ entry.latest = {
95
+ topicID,
96
+ sequenceID
97
+ };
98
+ entry.count += 1;
99
+ },
100
+ online (did) {
101
+ const entry = pending.get(did);
102
+ if (entry == null) return;
103
+ clearTimeout(entry.timer);
104
+ pending.delete(did);
105
+ },
106
+ dispose () {
107
+ disposed = true;
108
+ for (const entry of pending.values())clearTimeout(entry.timer);
109
+ pending.clear();
110
+ }
111
+ };
112
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kumiai/hub-server",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Hub server for Enkaku E2EE group message routing",
5
5
  "keywords": [
6
6
  "hub",
@@ -25,18 +25,19 @@
25
25
  "lib/*"
26
26
  ],
27
27
  "dependencies": {
28
- "@enkaku/protocol": "^0.20.0",
29
- "@enkaku/server": "^0.20.0",
30
- "@kokuin/token": "^0.3.0",
31
- "@sozai/codec": "^0.3.0",
28
+ "@enkaku/protocol": "^0.21.0",
29
+ "@enkaku/server": "^0.21.1",
30
+ "@kokuin/token": "^0.5.0",
31
+ "@sozai/codec": "^0.4.0",
32
32
  "@sozai/event": "^0.1.3",
33
- "@sozai/log": "^0.3.0",
34
- "@kumiai/hub-protocol": "^0.5.0"
33
+ "@sozai/log": "^0.3.1",
34
+ "@kumiai/hub-protocol": "^0.7.0"
35
35
  },
36
36
  "devDependencies": {
37
- "@enkaku/client": "^0.20.0",
38
- "@enkaku/transport": "^0.20.0",
39
- "@kumiai/hub-conformance": "^0.5.0"
37
+ "@enkaku/client": "^0.21.0",
38
+ "@enkaku/transport": "^0.21.0",
39
+ "@kumiai/hub-conformance": "^0.7.0",
40
+ "@kumiai/hub-wake": "^0.7.0"
40
41
  },
41
42
  "publishConfig": {
42
43
  "access": "public"