@kumiai/hub-server 0.10.2 → 0.10.4
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 +1 -0
- package/lib/handlers.js +8 -8
- package/lib/hub.d.ts +12 -0
- package/lib/hub.js +31 -7
- package/lib/wake.d.ts +2 -1
- package/lib/wake.js +8 -3
- package/package.json +10 -9
package/README.md
CHANGED
|
@@ -90,6 +90,7 @@ flood.
|
|
|
90
90
|
|
|
91
91
|
Publish rate limits are per DID (20/s, burst 50) and per topic (100/s, burst 200), merged over
|
|
92
92
|
`DEFAULT_RATE_LIMITS`; `hub/v1/keypackage/fetch` has its own request quota per requester DID.
|
|
93
|
+
A request over a limit is refused with `HUB_RATE_LIMITED` (`RateLimitedError`) before it takes effect, so the caller waits and retries it.
|
|
93
94
|
|
|
94
95
|
## A store failure that is not a request failure
|
|
95
96
|
|
package/lib/handlers.js
CHANGED
|
@@ -239,13 +239,13 @@ export function createHandlers(params) {
|
|
|
239
239
|
}
|
|
240
240
|
if (!didLimiter.tryConsume(senderDID)) {
|
|
241
241
|
throw new HandlerError({
|
|
242
|
-
code:
|
|
242
|
+
code: HUB_ERROR_CODES.rateLimited,
|
|
243
243
|
message: 'Publish rate limit exceeded for DID'
|
|
244
244
|
});
|
|
245
245
|
}
|
|
246
246
|
if (!topicLimiter.tryConsume(topicID)) {
|
|
247
247
|
throw new HandlerError({
|
|
248
|
-
code:
|
|
248
|
+
code: HUB_ERROR_CODES.rateLimited,
|
|
249
249
|
message: 'Publish rate limit exceeded for topic'
|
|
250
250
|
});
|
|
251
251
|
}
|
|
@@ -357,7 +357,7 @@ export function createHandlers(params) {
|
|
|
357
357
|
}
|
|
358
358
|
if (!didLimiter.tryConsume(clientDID)) {
|
|
359
359
|
throw new HandlerError({
|
|
360
|
-
code:
|
|
360
|
+
code: HUB_ERROR_CODES.rateLimited,
|
|
361
361
|
message: 'Subscribe rate limit exceeded for DID'
|
|
362
362
|
});
|
|
363
363
|
}
|
|
@@ -417,7 +417,7 @@ export function createHandlers(params) {
|
|
|
417
417
|
const clientDID = getClientDID(ctx);
|
|
418
418
|
if (!didLimiter.tryConsume(clientDID)) {
|
|
419
419
|
throw new HandlerError({
|
|
420
|
-
code:
|
|
420
|
+
code: HUB_ERROR_CODES.rateLimited,
|
|
421
421
|
message: 'Unsubscribe rate limit exceeded for DID'
|
|
422
422
|
});
|
|
423
423
|
}
|
|
@@ -681,7 +681,7 @@ export function createHandlers(params) {
|
|
|
681
681
|
}
|
|
682
682
|
if (!didLimiter.tryConsume(clientDID)) {
|
|
683
683
|
throw new HandlerError({
|
|
684
|
-
code:
|
|
684
|
+
code: HUB_ERROR_CODES.rateLimited,
|
|
685
685
|
message: 'Key package upload rate limit exceeded for DID'
|
|
686
686
|
});
|
|
687
687
|
}
|
|
@@ -844,7 +844,7 @@ export function createHandlers(params) {
|
|
|
844
844
|
}
|
|
845
845
|
if (!didLimiter.tryConsume(clientDID)) {
|
|
846
846
|
throw new HandlerError({
|
|
847
|
-
code:
|
|
847
|
+
code: HUB_ERROR_CODES.rateLimited,
|
|
848
848
|
message: 'Key package status rate limit exceeded for DID'
|
|
849
849
|
});
|
|
850
850
|
}
|
|
@@ -895,7 +895,7 @@ export function createHandlers(params) {
|
|
|
895
895
|
}
|
|
896
896
|
if (!didLimiter.tryConsume(clientDID)) {
|
|
897
897
|
throw new HandlerError({
|
|
898
|
-
code:
|
|
898
|
+
code: HUB_ERROR_CODES.rateLimited,
|
|
899
899
|
message: 'Wake rate limit exceeded for DID'
|
|
900
900
|
});
|
|
901
901
|
}
|
|
@@ -959,7 +959,7 @@ export function createHandlers(params) {
|
|
|
959
959
|
// the same, or the uncounted half is the one an abusive caller uses.
|
|
960
960
|
if (!didLimiter.tryConsume(clientDID)) {
|
|
961
961
|
throw new HandlerError({
|
|
962
|
-
code:
|
|
962
|
+
code: HUB_ERROR_CODES.rateLimited,
|
|
963
963
|
message: 'Wake rate limit exceeded for DID'
|
|
964
964
|
});
|
|
965
965
|
}
|
package/lib/hub.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ServerTransportOf } from '@enkaku/protocol';
|
|
2
2
|
import type { AccessRules, ReplayOptions, ResourceLimits, Server } from '@enkaku/server';
|
|
3
|
+
import type { VerifyTokenHook } from '@kokuin/capability';
|
|
3
4
|
import type { Identity } from '@kokuin/token';
|
|
4
5
|
import type { HubProtocol, HubStore, WakeRegistry, WakeSender } from '@kumiai/hub-protocol';
|
|
5
6
|
import { type AuthorizeHook, type HubRateLimits, type HubStoreErrorHook, type KeyPackageFetchLimits } from './handlers.js';
|
|
@@ -41,6 +42,12 @@ export type CreateHubParams = {
|
|
|
41
42
|
* allow-any-authed.
|
|
42
43
|
*/
|
|
43
44
|
authorize?: AuthorizeHook;
|
|
45
|
+
/**
|
|
46
|
+
* Called for each capability in a delegation chain after it verifies; throw to reject. This is
|
|
47
|
+
* where a host plugs in revocation. Forwarded to `serve()`, which only consults it for
|
|
48
|
+
* delegated capabilities.
|
|
49
|
+
*/
|
|
50
|
+
verifyToken?: VerifyTokenHook;
|
|
44
51
|
/** Publish rate limits. Merged over {@link DEFAULT_RATE_LIMITS}. */
|
|
45
52
|
rateLimits?: Partial<HubRateLimits>;
|
|
46
53
|
/**
|
|
@@ -81,5 +88,10 @@ export type CreateHubParams = {
|
|
|
81
88
|
export type HubInstance = {
|
|
82
89
|
registry: HubClientRegistry;
|
|
83
90
|
server: Server<HubProtocol>;
|
|
91
|
+
/**
|
|
92
|
+
* Stops the purge timer, waits for an in-flight purge and in-flight wake sends, then disposes the
|
|
93
|
+
* server. Idempotent: every call returns the same promise.
|
|
94
|
+
*/
|
|
95
|
+
dispose(): Promise<void>;
|
|
84
96
|
};
|
|
85
97
|
export declare function createHub(params: CreateHubParams): HubInstance;
|
package/lib/hub.js
CHANGED
|
@@ -56,6 +56,7 @@ export function createHub(params) {
|
|
|
56
56
|
transport: params.transport,
|
|
57
57
|
identity: params.identity,
|
|
58
58
|
accessRules: params.accessRules ?? DEFAULT_HUB_ACCESS_RULES,
|
|
59
|
+
verifyToken: params.verifyToken,
|
|
59
60
|
replay: {
|
|
60
61
|
...params.replay,
|
|
61
62
|
enabled: true,
|
|
@@ -63,27 +64,50 @@ export function createHub(params) {
|
|
|
63
64
|
},
|
|
64
65
|
limits
|
|
65
66
|
});
|
|
67
|
+
let purgeTimer;
|
|
68
|
+
const purgesInFlight = new Set();
|
|
66
69
|
if (params.purge !== false) {
|
|
67
70
|
const interval = params.purge?.interval ?? 3_600_000;
|
|
68
71
|
const olderThan = params.purge?.olderThan ?? 604_800;
|
|
69
|
-
|
|
70
|
-
params.store.purge({
|
|
72
|
+
purgeTimer = setInterval(()=>{
|
|
73
|
+
const run = params.store.purge({
|
|
71
74
|
olderThan
|
|
72
|
-
}).catch((error)=>{
|
|
75
|
+
}).then(()=>{}).catch((error)=>{
|
|
73
76
|
// Purge failures are non-fatal; retried on the next interval
|
|
74
77
|
storeErrorReporter({
|
|
75
78
|
method: 'purge',
|
|
76
79
|
error
|
|
77
80
|
});
|
|
78
81
|
});
|
|
82
|
+
purgesInFlight.add(run);
|
|
83
|
+
void run.finally(()=>{
|
|
84
|
+
purgesInFlight.delete(run);
|
|
85
|
+
});
|
|
79
86
|
}, interval);
|
|
80
|
-
server.disposed.then(()=>clearInterval(purgeTimer));
|
|
81
87
|
}
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
// Stop background work first so nothing new starts, then drain what already did, then the server.
|
|
89
|
+
let teardown;
|
|
90
|
+
function stopBackground() {
|
|
91
|
+
teardown ??= (async ()=>{
|
|
92
|
+
if (purgeTimer != null) clearInterval(purgeTimer);
|
|
93
|
+
await Promise.all(purgesInFlight);
|
|
94
|
+
await wakeDispatcher?.dispose();
|
|
95
|
+
})();
|
|
96
|
+
return teardown;
|
|
97
|
+
}
|
|
98
|
+
let disposing;
|
|
99
|
+
function dispose() {
|
|
100
|
+
disposing ??= (async ()=>{
|
|
101
|
+
await stopBackground();
|
|
102
|
+
await server.dispose();
|
|
103
|
+
})();
|
|
104
|
+
return disposing;
|
|
84
105
|
}
|
|
106
|
+
// Disposing the server directly must still stop purge and wake.
|
|
107
|
+
void server.disposed.then(stopBackground);
|
|
85
108
|
return {
|
|
86
109
|
registry,
|
|
87
|
-
server
|
|
110
|
+
server,
|
|
111
|
+
dispose
|
|
88
112
|
};
|
|
89
113
|
}
|
package/lib/wake.d.ts
CHANGED
|
@@ -20,7 +20,8 @@ export type WakeDispatcher = {
|
|
|
20
20
|
notify(params: WakeNotifyParams): void;
|
|
21
21
|
/** The device bound a receive channel: drop any pending trailing ping. */
|
|
22
22
|
online(did: string): void;
|
|
23
|
-
|
|
23
|
+
/** Stops accepting frames, drops pending windows, and waits for every in-flight send to settle. */
|
|
24
|
+
dispose(): Promise<void>;
|
|
24
25
|
};
|
|
25
26
|
/**
|
|
26
27
|
* Coalesces wake pings per DID on a LEADING edge: the first frame pings immediately, then a window
|
package/lib/wake.js
CHANGED
|
@@ -11,6 +11,7 @@ const DEFAULT_DEBOUNCE_MS = 10_000;
|
|
|
11
11
|
*/ export function createWakeDispatcher(params) {
|
|
12
12
|
const debounceMs = params.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
13
13
|
const pending = new Map();
|
|
14
|
+
const inFlight = new Set();
|
|
14
15
|
let disposed = false;
|
|
15
16
|
function report(did, error) {
|
|
16
17
|
try {
|
|
@@ -25,7 +26,7 @@ const DEFAULT_DEBOUNCE_MS = 10_000;
|
|
|
25
26
|
function send(did, topicID, sequenceID, count) {
|
|
26
27
|
// Deliberately not awaited: a slow or hanging provider must never delay the publish fan-out
|
|
27
28
|
// this is called from.
|
|
28
|
-
|
|
29
|
+
const task = (async ()=>{
|
|
29
30
|
try {
|
|
30
31
|
const registration = await params.registry.get(did);
|
|
31
32
|
if (registration == null) return;
|
|
@@ -45,7 +46,8 @@ const DEFAULT_DEBOUNCE_MS = 10_000;
|
|
|
45
46
|
case 'delivered':
|
|
46
47
|
break;
|
|
47
48
|
case 'gone':
|
|
48
|
-
|
|
49
|
+
// A hub being torn down must not write to a registry its host may already be closing.
|
|
50
|
+
if (!disposed) await params.registry.delete(did);
|
|
49
51
|
break;
|
|
50
52
|
case 'retry':
|
|
51
53
|
report(did, new Error('Wake send failed transiently'));
|
|
@@ -59,6 +61,8 @@ const DEFAULT_DEBOUNCE_MS = 10_000;
|
|
|
59
61
|
report(did, error);
|
|
60
62
|
}
|
|
61
63
|
})();
|
|
64
|
+
inFlight.add(task);
|
|
65
|
+
void task.finally(()=>inFlight.delete(task));
|
|
62
66
|
}
|
|
63
67
|
function openWindow(did) {
|
|
64
68
|
const entry = {
|
|
@@ -103,10 +107,11 @@ const DEFAULT_DEBOUNCE_MS = 10_000;
|
|
|
103
107
|
clearTimeout(entry.timer);
|
|
104
108
|
pending.delete(did);
|
|
105
109
|
},
|
|
106
|
-
dispose () {
|
|
110
|
+
async dispose () {
|
|
107
111
|
disposed = true;
|
|
108
112
|
for (const entry of pending.values())clearTimeout(entry.timer);
|
|
109
113
|
pending.clear();
|
|
114
|
+
await Promise.all(inFlight);
|
|
110
115
|
}
|
|
111
116
|
};
|
|
112
117
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kumiai/hub-server",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.4",
|
|
4
4
|
"description": "Hub server for Enkaku E2EE group message routing",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"hub",
|
|
@@ -25,17 +25,18 @@
|
|
|
25
25
|
"lib/*"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@enkaku/protocol": "^0.21.
|
|
29
|
-
"@enkaku/server": "^0.21.
|
|
30
|
-
"@kokuin/
|
|
31
|
-
"@
|
|
32
|
-
"@
|
|
33
|
-
"@sozai/
|
|
28
|
+
"@enkaku/protocol": "^0.21.5",
|
|
29
|
+
"@enkaku/server": "^0.21.5",
|
|
30
|
+
"@kokuin/capability": "^0.4.2",
|
|
31
|
+
"@kokuin/token": "^0.5.1",
|
|
32
|
+
"@kumiai/hub-protocol": "^0.10.2",
|
|
33
|
+
"@sozai/codec": "^0.4.3",
|
|
34
|
+
"@sozai/event": "^0.1.6",
|
|
34
35
|
"@sozai/log": "^0.4.0"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
|
-
"@enkaku/client": "^0.21.
|
|
38
|
-
"@enkaku/transport": "^0.21.
|
|
38
|
+
"@enkaku/client": "^0.21.5",
|
|
39
|
+
"@enkaku/transport": "^0.21.5",
|
|
39
40
|
"@kumiai/hub-conformance": "^0.10.1",
|
|
40
41
|
"@kumiai/hub-wake": "^0.10.0"
|
|
41
42
|
},
|