@kumiai/hub-server 0.7.0 → 0.8.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.d.ts +19 -0
- package/lib/handlers.js +50 -0
- package/lib/hub.d.ts +11 -1
- package/lib/hub.js +1 -0
- package/package.json +7 -7
package/lib/handlers.d.ts
CHANGED
|
@@ -63,6 +63,13 @@ export type AuthorizeRequest = {
|
|
|
63
63
|
} | {
|
|
64
64
|
action: 'wake/unregister';
|
|
65
65
|
did: string;
|
|
66
|
+
} | {
|
|
67
|
+
action: 'receive';
|
|
68
|
+
did: string;
|
|
69
|
+
} | {
|
|
70
|
+
action: 'receive/deliver';
|
|
71
|
+
did: string;
|
|
72
|
+
topicID: string;
|
|
66
73
|
};
|
|
67
74
|
/**
|
|
68
75
|
* A plain `boolean` is shorthand for `{ allow: boolean }`, with no reason, code, or retry hint.
|
|
@@ -150,6 +157,12 @@ export declare const DEFAULT_KEYPACKAGE_FETCH_LIMITS: KeyPackageFetchLimits;
|
|
|
150
157
|
* writer health. The default of 256 is safely above it.
|
|
151
158
|
*/
|
|
152
159
|
export declare const DEFAULT_RECEIVE_BUFFER_LIMIT = 256;
|
|
160
|
+
/**
|
|
161
|
+
* Default TTL (ms) for a receive channel's per-(did, topicID) delivery-authorization decision.
|
|
162
|
+
* Bounds how long an already-resolved allow is reused before the hook is re-consulted — i.e. the
|
|
163
|
+
* maximum window a removed member can keep draining a topic. See the receive handler's `gate`.
|
|
164
|
+
*/
|
|
165
|
+
export declare const DEFAULT_RECEIVE_AUTH_CACHE_TTL = 5000;
|
|
153
166
|
export type CreateHandlersParams = {
|
|
154
167
|
registry: HubClientRegistry;
|
|
155
168
|
store: HubStore;
|
|
@@ -159,6 +172,12 @@ export type CreateHandlersParams = {
|
|
|
159
172
|
/** Max frames queued-but-unflushed on a receive channel. See {@link DEFAULT_RECEIVE_BUFFER_LIMIT}
|
|
160
173
|
* for the >= 50-frame-page floor this should respect. Default: 256 */
|
|
161
174
|
receiveBufferLimit?: number;
|
|
175
|
+
/**
|
|
176
|
+
* TTL (ms) for the per-(did, topicID) receive delivery-authorization cache. `0` disables reuse
|
|
177
|
+
* (the hook is consulted for every frame). Non-finite or negative values fall back to
|
|
178
|
+
* {@link DEFAULT_RECEIVE_AUTH_CACHE_TTL}. Default: 5000.
|
|
179
|
+
*/
|
|
180
|
+
receiveAuthCacheTTL?: number;
|
|
162
181
|
/**
|
|
163
182
|
* Called when a `HubStore` operation fails at a point where the hub deliberately does not fail
|
|
164
183
|
* the request. Fire-and-forget; a throw here is swallowed.
|
package/lib/handlers.js
CHANGED
|
@@ -84,6 +84,11 @@ export const DEFAULT_KEYPACKAGE_FETCH_LIMITS = {
|
|
|
84
84
|
* before awaiting, so a limit below 50 would trip the cap on the very first page regardless of
|
|
85
85
|
* writer health. The default of 256 is safely above it.
|
|
86
86
|
*/ export const DEFAULT_RECEIVE_BUFFER_LIMIT = 256;
|
|
87
|
+
/**
|
|
88
|
+
* Default TTL (ms) for a receive channel's per-(did, topicID) delivery-authorization decision.
|
|
89
|
+
* Bounds how long an already-resolved allow is reused before the hook is re-consulted — i.e. the
|
|
90
|
+
* maximum window a removed member can keep draining a topic. See the receive handler's `gate`.
|
|
91
|
+
*/ export const DEFAULT_RECEIVE_AUTH_CACHE_TTL = 5000;
|
|
87
92
|
function getClientDID(ctx) {
|
|
88
93
|
const iss = ctx.message.payload.iss;
|
|
89
94
|
if (typeof iss !== 'string' || iss.length === 0) {
|
|
@@ -105,6 +110,13 @@ function getClientDID(ctx) {
|
|
|
105
110
|
}
|
|
106
111
|
throw error;
|
|
107
112
|
}
|
|
113
|
+
function resolveReceiveAuthCacheTTL(value) {
|
|
114
|
+
if (value === undefined || !Number.isFinite(value) || value < 0) {
|
|
115
|
+
return DEFAULT_RECEIVE_AUTH_CACHE_TTL;
|
|
116
|
+
}
|
|
117
|
+
return value // 0 allowed: no reuse
|
|
118
|
+
;
|
|
119
|
+
}
|
|
108
120
|
function toReceiveFrame(message) {
|
|
109
121
|
return {
|
|
110
122
|
sequenceID: message.sequenceID,
|
|
@@ -134,6 +146,7 @@ export function createHandlers(params) {
|
|
|
134
146
|
const didLimiter = createRateLimiter(rateLimits.perDID);
|
|
135
147
|
const topicLimiter = createRateLimiter(rateLimits.perTopic);
|
|
136
148
|
const receiveBufferLimit = params.receiveBufferLimit ?? DEFAULT_RECEIVE_BUFFER_LIMIT;
|
|
149
|
+
const receiveAuthCacheTTL = resolveReceiveAuthCacheTTL(params.receiveAuthCacheTTL);
|
|
137
150
|
const storeErrorReporter = createStoreErrorReporter(params.onStoreError);
|
|
138
151
|
const fetchLimits = {
|
|
139
152
|
...DEFAULT_KEYPACKAGE_FETCH_LIMITS,
|
|
@@ -417,7 +430,43 @@ export function createHandlers(params) {
|
|
|
417
430
|
'hub/v1/receive': async (ctx)=>{
|
|
418
431
|
const clientDID = getClientDID(ctx);
|
|
419
432
|
const { after } = ctx.param ?? {};
|
|
433
|
+
const connectDecision = normalizeAuthorizeDecision(await authorize({
|
|
434
|
+
action: 'receive',
|
|
435
|
+
did: clientDID
|
|
436
|
+
}));
|
|
437
|
+
if (!connectDecision.allow) {
|
|
438
|
+
throw new HandlerError({
|
|
439
|
+
code: HUB_ERROR_CODES.authorizationDenied,
|
|
440
|
+
message: connectDecision.reason ?? 'Not authorized to receive'
|
|
441
|
+
});
|
|
442
|
+
}
|
|
420
443
|
registry.register(clientDID);
|
|
444
|
+
// Per-(did, topicID) delivery-authorization cache, local to this channel (torn down with it).
|
|
445
|
+
// Both allow AND deny are cached for the TTL, so re-grant is not immediate: a topic that was
|
|
446
|
+
// just re-authorized keeps being denied for up to `receiveAuthCacheTTL` ms, and a frame denied
|
|
447
|
+
// in-session redelivers on the recipient's next connect.
|
|
448
|
+
const authCache = new Map();
|
|
449
|
+
const gate = async (topicID)=>{
|
|
450
|
+
const key = `${clientDID}${topicID}`;
|
|
451
|
+
const now = Date.now();
|
|
452
|
+
const cached = authCache.get(key);
|
|
453
|
+
if (cached != null && cached.expiresAt > now) return cached.allow;
|
|
454
|
+
const decision = normalizeAuthorizeDecision(await authorize({
|
|
455
|
+
action: 'receive/deliver',
|
|
456
|
+
did: clientDID,
|
|
457
|
+
topicID
|
|
458
|
+
}));
|
|
459
|
+
if (receiveAuthCacheTTL > 0) {
|
|
460
|
+
// Expiry is measured from when the decision is stored, AFTER the hook resolves — so the
|
|
461
|
+
// reuse window excludes the hook's own execution time. Re-read the clock here rather than
|
|
462
|
+
// reusing `now` (captured before the await).
|
|
463
|
+
authCache.set(key, {
|
|
464
|
+
allow: decision.allow,
|
|
465
|
+
expiresAt: Date.now() + receiveAuthCacheTTL
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
return decision.allow;
|
|
469
|
+
};
|
|
421
470
|
const writer = ctx.writable.getWriter();
|
|
422
471
|
const reader = ctx.readable.getReader();
|
|
423
472
|
let endEvicted;
|
|
@@ -464,6 +513,7 @@ export function createHandlers(params) {
|
|
|
464
513
|
writeChain = writeChain.then(async ()=>{
|
|
465
514
|
if (tornDown) return;
|
|
466
515
|
try {
|
|
516
|
+
if (!await gate(frame.topicID)) return; // denied: skip, leave pending, no ack
|
|
467
517
|
await writer.ready;
|
|
468
518
|
await writer.write(frame);
|
|
469
519
|
if (frame.sequenceID > lastServed) lastServed = frame.sequenceID;
|
package/lib/hub.d.ts
CHANGED
|
@@ -30,7 +30,12 @@ export type CreateHubParams = {
|
|
|
30
30
|
identity: Identity;
|
|
31
31
|
/** Access rules enforced by the server. Defaults to {@link DEFAULT_HUB_ACCESS_RULES}. */
|
|
32
32
|
accessRules?: AccessRules;
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Per-action authorization hook. Consulted for publish, subscribe, topic/fetch, keypackage/*,
|
|
35
|
+
* wake/*, and receive — the coarse `receive` gate at channel open plus a per-frame
|
|
36
|
+
* `receive/deliver` gate by topic (see {@link CreateHubParams.receiveAuthCacheTTL}). Defaults to
|
|
37
|
+
* allow-any-authed.
|
|
38
|
+
*/
|
|
34
39
|
authorize?: AuthorizeHook;
|
|
35
40
|
/** Publish rate limits. Merged over {@link DEFAULT_RATE_LIMITS}. */
|
|
36
41
|
rateLimits?: Partial<HubRateLimits>;
|
|
@@ -39,6 +44,11 @@ export type CreateHubParams = {
|
|
|
39
44
|
* {@link DEFAULT_KEYPACKAGE_FETCH_LIMITS}.
|
|
40
45
|
*/
|
|
41
46
|
keyPackageFetchLimits?: Partial<KeyPackageFetchLimits>;
|
|
47
|
+
/**
|
|
48
|
+
* TTL (ms) for the per-(did, topicID) receive delivery-authorization cache. `0` consults the
|
|
49
|
+
* hook for every frame. Forwarded to {@link createHandlers}. Default: 5000.
|
|
50
|
+
*/
|
|
51
|
+
receiveAuthCacheTTL?: number;
|
|
42
52
|
/**
|
|
43
53
|
* Called when a `HubStore` operation fails where the hub deliberately does not fail the
|
|
44
54
|
* request. Forwarded to {@link createHandlers} and used by the purge timer. Fire-and-forget.
|
package/lib/hub.js
CHANGED
|
@@ -34,6 +34,7 @@ export function createHub(params) {
|
|
|
34
34
|
authorize: params.authorize,
|
|
35
35
|
rateLimits: params.rateLimits,
|
|
36
36
|
keyPackageFetchLimits: params.keyPackageFetchLimits,
|
|
37
|
+
receiveAuthCacheTTL: params.receiveAuthCacheTTL,
|
|
37
38
|
onStoreError: storeErrorReporter,
|
|
38
39
|
wake: params.wake == null ? undefined : {
|
|
39
40
|
registry: params.wake.registry,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kumiai/hub-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Hub server for Enkaku E2EE group message routing",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"hub",
|
|
@@ -26,18 +26,18 @@
|
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@enkaku/protocol": "^0.21.0",
|
|
29
|
-
"@enkaku/server": "^0.21.
|
|
29
|
+
"@enkaku/server": "^0.21.2",
|
|
30
30
|
"@kokuin/token": "^0.5.0",
|
|
31
|
+
"@kumiai/hub-protocol": "^0.8.0",
|
|
31
32
|
"@sozai/codec": "^0.4.0",
|
|
32
33
|
"@sozai/event": "^0.1.3",
|
|
33
|
-
"@sozai/log": "^0.3.1"
|
|
34
|
-
"@kumiai/hub-protocol": "^0.7.0"
|
|
34
|
+
"@sozai/log": "^0.3.1"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@enkaku/client": "^0.21.
|
|
37
|
+
"@enkaku/client": "^0.21.1",
|
|
38
38
|
"@enkaku/transport": "^0.21.0",
|
|
39
|
-
"@kumiai/hub-conformance": "^0.
|
|
40
|
-
"@kumiai/hub-wake": "^0.
|
|
39
|
+
"@kumiai/hub-conformance": "^0.8.0",
|
|
40
|
+
"@kumiai/hub-wake": "^0.8.0"
|
|
41
41
|
},
|
|
42
42
|
"publishConfig": {
|
|
43
43
|
"access": "public"
|