@kumiai/hub-server 0.10.0 → 0.10.2
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 +4 -0
- package/lib/handlers.js +2 -1
- package/lib/hub.d.ts +5 -1
- package/lib/hub.js +5 -0
- package/lib/index.d.ts +1 -1
- package/lib/memoryStore.js +12 -3
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -16,6 +16,10 @@ client registry for push fan-out, and an in-memory `HubStore` for development an
|
|
|
16
16
|
`DEFAULT_HUB_ACCESS_RULES`.
|
|
17
17
|
- `HubStoreErrorEvent`, `HubStoreErrorHook` — the `onStoreError` event and hook types.
|
|
18
18
|
|
|
19
|
+
`createHub` accepts `replay` with a cache and tuning (`maxAge`, `leeway`, `maxEntries`).
|
|
20
|
+
The hub always enables replay checks and rejects stale tokens. Supply a persistent `replay.cache`
|
|
21
|
+
to keep replay protection across restarts; omitted, it uses a per-process memory cache.
|
|
22
|
+
|
|
19
23
|
```ts
|
|
20
24
|
import { createHub, createMemoryStore } from '@kumiai/hub-server'
|
|
21
25
|
|
package/lib/handlers.js
CHANGED
package/lib/hub.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ServerTransportOf } from '@enkaku/protocol';
|
|
2
|
-
import type { AccessRules, ResourceLimits, Server } from '@enkaku/server';
|
|
2
|
+
import type { AccessRules, ReplayOptions, ResourceLimits, Server } from '@enkaku/server';
|
|
3
3
|
import type { Identity } from '@kokuin/token';
|
|
4
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';
|
|
@@ -20,6 +20,8 @@ export type HubPurgeOptions = {
|
|
|
20
20
|
*/
|
|
21
21
|
olderThan?: number;
|
|
22
22
|
};
|
|
23
|
+
/** Replay settings a host may tune. The hub always enables replay checks and rejects stale tokens. */
|
|
24
|
+
export type HubReplayOptions = Omit<ReplayOptions, 'enabled' | 'rejectStale'>;
|
|
23
25
|
export type CreateHubParams = {
|
|
24
26
|
transport: ServerTransportOf<HubProtocol>;
|
|
25
27
|
store: HubStore;
|
|
@@ -30,6 +32,8 @@ export type CreateHubParams = {
|
|
|
30
32
|
identity: Identity;
|
|
31
33
|
/** Access rules enforced by the server. Defaults to {@link DEFAULT_HUB_ACCESS_RULES}. */
|
|
32
34
|
accessRules?: AccessRules;
|
|
35
|
+
/** Omitted: per-process memory cache. Only a persistent `cache` survives a hub restart. */
|
|
36
|
+
replay?: HubReplayOptions;
|
|
33
37
|
/**
|
|
34
38
|
* Per-action authorization hook. Consulted for publish, subscribe, topic/fetch, keypackage/*,
|
|
35
39
|
* wake/*, and receive — the coarse `receive` gate at channel open plus a per-frame
|
package/lib/hub.js
CHANGED
|
@@ -56,6 +56,11 @@ export function createHub(params) {
|
|
|
56
56
|
transport: params.transport,
|
|
57
57
|
identity: params.identity,
|
|
58
58
|
accessRules: params.accessRules ?? DEFAULT_HUB_ACCESS_RULES,
|
|
59
|
+
replay: {
|
|
60
|
+
...params.replay,
|
|
61
|
+
enabled: true,
|
|
62
|
+
rejectStale: true
|
|
63
|
+
},
|
|
59
64
|
limits
|
|
60
65
|
});
|
|
61
66
|
if (params.purge !== false) {
|
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type { AuthorizeDecision, AuthorizeHook, AuthorizeRequest, CreateHandlersParams, HubRateLimits, HubStoreErrorEvent, HubStoreErrorHook, KeyPackageFetchLimits, } from './handlers.js';
|
|
2
2
|
export { createHandlers, DEFAULT_KEYPACKAGE_FETCH_LIMITS, DEFAULT_RATE_LIMITS, DEFAULT_RECEIVE_BUFFER_LIMIT, } from './handlers.js';
|
|
3
|
-
export type { CreateHubParams, HubInstance, HubPurgeOptions } from './hub.js';
|
|
3
|
+
export type { CreateHubParams, HubInstance, HubPurgeOptions, HubReplayOptions } from './hub.js';
|
|
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';
|
package/lib/memoryStore.js
CHANGED
|
@@ -54,6 +54,7 @@ function isLive(entry, nowSeconds) {
|
|
|
54
54
|
const entries = new Map();
|
|
55
55
|
const topicLogs = new Map();
|
|
56
56
|
const heads = new Map();
|
|
57
|
+
const removedThrough = new Map();
|
|
57
58
|
const deliveries = new Map();
|
|
58
59
|
/**
|
|
59
60
|
* publishID -> the sequenceID it was accepted as. Not a log entry and not reachable from one: no
|
|
@@ -87,6 +88,10 @@ function isLive(entry, nowSeconds) {
|
|
|
87
88
|
function removeEntry(sequenceID) {
|
|
88
89
|
const entry = entries.get(sequenceID);
|
|
89
90
|
if (entry == null) return;
|
|
91
|
+
if (entry.retain === 'log') {
|
|
92
|
+
const removed = removedThrough.get(entry.topicID);
|
|
93
|
+
if (removed == null || sequenceID > removed) removedThrough.set(entry.topicID, sequenceID);
|
|
94
|
+
}
|
|
90
95
|
for (const recipientDID of entry.pendingFor){
|
|
91
96
|
const list = deliveries.get(recipientDID);
|
|
92
97
|
if (list != null) {
|
|
@@ -295,7 +300,8 @@ function isLive(entry, nowSeconds) {
|
|
|
295
300
|
return {
|
|
296
301
|
messages,
|
|
297
302
|
head: heads.get(params.topicID) ?? null,
|
|
298
|
-
oldest: log[0] ?? null
|
|
303
|
+
oldest: log[0] ?? null,
|
|
304
|
+
gap: removedThrough.has(params.topicID) && (after == null || removedThrough.get(params.topicID) > after)
|
|
299
305
|
};
|
|
300
306
|
},
|
|
301
307
|
async trim (params) {
|
|
@@ -318,14 +324,17 @@ function isLive(entry, nowSeconds) {
|
|
|
318
324
|
}
|
|
319
325
|
},
|
|
320
326
|
async purge (params) {
|
|
321
|
-
// The age bound, for both classes: the same removal path, the same invariants — head is
|
|
322
|
-
// untouched.
|
|
323
327
|
const now = Date.now();
|
|
324
328
|
const purgedIDs = [];
|
|
329
|
+
const retainedTopics = new Set();
|
|
330
|
+
// Insertion order is log-position order, even if the clock moves backwards.
|
|
325
331
|
for (const [sequenceID, entry] of entries){
|
|
332
|
+
if (entry.retain === 'log' && retainedTopics.has(entry.topicID)) continue;
|
|
326
333
|
const retention = retentionOf(entry.topicID, params.olderThan);
|
|
327
334
|
if (entry.storedAt <= now - retention * 1000) {
|
|
328
335
|
purgedIDs.push(sequenceID);
|
|
336
|
+
} else if (entry.retain === 'log') {
|
|
337
|
+
retainedTopics.add(entry.topicID);
|
|
329
338
|
}
|
|
330
339
|
}
|
|
331
340
|
for (const sequenceID of purgedIDs){
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kumiai/hub-server",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "Hub server for Enkaku E2EE group message routing",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"hub",
|
|
@@ -25,18 +25,18 @@
|
|
|
25
25
|
"lib/*"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@enkaku/protocol": "^0.21.
|
|
29
|
-
"@enkaku/server": "^0.21.
|
|
28
|
+
"@enkaku/protocol": "^0.21.4",
|
|
29
|
+
"@enkaku/server": "^0.21.3",
|
|
30
30
|
"@kokuin/token": "^0.5.0",
|
|
31
|
-
"@kumiai/hub-protocol": "^0.10.
|
|
32
|
-
"@sozai/codec": "^0.4.
|
|
33
|
-
"@sozai/event": "^0.1.
|
|
34
|
-
"@sozai/log": "^0.
|
|
31
|
+
"@kumiai/hub-protocol": "^0.10.1",
|
|
32
|
+
"@sozai/codec": "^0.4.2",
|
|
33
|
+
"@sozai/event": "^0.1.5",
|
|
34
|
+
"@sozai/log": "^0.4.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@enkaku/client": "^0.21.
|
|
38
|
-
"@enkaku/transport": "^0.21.
|
|
39
|
-
"@kumiai/hub-conformance": "^0.10.
|
|
37
|
+
"@enkaku/client": "^0.21.3",
|
|
38
|
+
"@enkaku/transport": "^0.21.3",
|
|
39
|
+
"@kumiai/hub-conformance": "^0.10.1",
|
|
40
40
|
"@kumiai/hub-wake": "^0.10.0"
|
|
41
41
|
},
|
|
42
42
|
"publishConfig": {
|