@kumiai/hub-server 0.12.2 → 0.13.1
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 +18 -2
- package/lib/handlers.d.ts +5 -1
- package/lib/handlers.js +22 -3
- package/lib/hub.d.ts +7 -4
- package/lib/hub.js +3 -0
- package/lib/index.d.ts +1 -1
- package/lib/memoryStore.js +9 -3
- package/lib/registry.d.ts +11 -2
- package/lib/registry.js +2 -2
- package/lib/wake.js +9 -6
- package/package.json +11 -10
package/README.md
CHANGED
|
@@ -5,8 +5,8 @@ client registry for push fan-out, and an in-memory `HubStore` for development an
|
|
|
5
5
|
|
|
6
6
|
## Exports
|
|
7
7
|
|
|
8
|
-
- `createHub({ transport, store, identity, ... })`
|
|
9
|
-
|
|
8
|
+
- `createHub({ transport, store, identity, ... })` -- handlers, registry, access rules, rate limits and an optional scheduled purge.
|
|
9
|
+
Returns `{ registry, server, dispose }`.
|
|
10
10
|
- `createHandlers({ registry, store, ... })` — the handlers alone, for a host assembling its own
|
|
11
11
|
server.
|
|
12
12
|
- `createMemoryStore({ maxDepth?, retention? })` — an in-memory `HubStore`. For testing and
|
|
@@ -160,3 +160,19 @@ rather than passing silently. Pass an empty handler to silence it deliberately.
|
|
|
160
160
|
There is no throttling: a permanently broken store reports per request. logtape ships
|
|
161
161
|
`getThrottlingFilter`, so rate control belongs in the app's sink configuration where an operator
|
|
162
162
|
can tune it.
|
|
163
|
+
|
|
164
|
+
## Invocation hooks and attribution
|
|
165
|
+
|
|
166
|
+
`createHub` accepts optional `onInvocation`, forwarded to the Enkaku server for authenticated invocation context and delegation verification.
|
|
167
|
+
`createHub` and `createHandlers` accept optional `resolveAttribution({ message, senderDID })`.
|
|
168
|
+
The resolver can return `HubAttribution`, `undefined`, or a promise of either.
|
|
169
|
+
|
|
170
|
+
Without a resolver result, publishes use `{ account: senderDID, actor: senderDID, via: 'direct' }`.
|
|
171
|
+
The sender DID still comes from the verified signature.
|
|
172
|
+
A resolver supplies host accounting metadata rather than changing the authenticated sender or authorisation rules.
|
|
173
|
+
Only a verified delegation yields `via: 'delegated'`. A claimed subject alone does not prove delegation.
|
|
174
|
+
Hosts can use `onInvocation` to verify delegation before resolving attribution.
|
|
175
|
+
|
|
176
|
+
The store receives the resolved attribution with the publish.
|
|
177
|
+
The SQL stores persist it. The development memory store reports publish outcomes but does not implement occupancy sampling.
|
|
178
|
+
Unconfigured hooks preserve existing routing, retention and delivery behaviour.
|
package/lib/handlers.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ProcedureHandlers } from '@enkaku/server';
|
|
2
|
-
import type { HubProtocol, HubStore, WakeRegistry } from '@kumiai/hub-protocol';
|
|
2
|
+
import type { HubAttribution, HubProtocol, HubStore, WakeRegistry } from '@kumiai/hub-protocol';
|
|
3
3
|
import { type RateLimitConfig } from './rateLimit.js';
|
|
4
4
|
import type { HubClientRegistry } from './registry.js';
|
|
5
5
|
import type { WakeDispatcher } from './wake.js';
|
|
@@ -198,6 +198,10 @@ export type CreateHandlersParams = {
|
|
|
198
198
|
registry: HubClientRegistry;
|
|
199
199
|
store: HubStore;
|
|
200
200
|
authorize?: AuthorizeHook;
|
|
201
|
+
resolveAttribution?: (params: {
|
|
202
|
+
message: unknown;
|
|
203
|
+
senderDID: string;
|
|
204
|
+
}) => HubAttribution | undefined | Promise<HubAttribution | undefined>;
|
|
201
205
|
rateLimits?: Partial<HubRateLimits>;
|
|
202
206
|
keyPackageFetchLimits?: Partial<KeyPackageFetchLimits>;
|
|
203
207
|
/** Max frames queued-but-unflushed on a receive channel. See {@link DEFAULT_RECEIVE_BUFFER_LIMIT}
|
package/lib/handlers.js
CHANGED
|
@@ -287,8 +287,17 @@ export function createHandlers(params) {
|
|
|
287
287
|
let sequenceID;
|
|
288
288
|
let deduped;
|
|
289
289
|
try {
|
|
290
|
+
const attribution = await params.resolveAttribution?.({
|
|
291
|
+
message: ctx.message,
|
|
292
|
+
senderDID
|
|
293
|
+
}) ?? {
|
|
294
|
+
account: senderDID,
|
|
295
|
+
actor: senderDID,
|
|
296
|
+
via: 'direct'
|
|
297
|
+
};
|
|
290
298
|
const result = await store.publish({
|
|
291
299
|
senderDID,
|
|
300
|
+
attribution,
|
|
292
301
|
topicID,
|
|
293
302
|
payload: payloadBytes,
|
|
294
303
|
retain: ctx.param.retain,
|
|
@@ -624,7 +633,10 @@ export function createHandlers(params) {
|
|
|
624
633
|
logUnackedFrames();
|
|
625
634
|
tornDown = true;
|
|
626
635
|
notifyWindow();
|
|
627
|
-
if (receiveToken != null) registry.releaseReceiveWriter(
|
|
636
|
+
if (receiveToken != null) registry.releaseReceiveWriter({
|
|
637
|
+
did: clientDID,
|
|
638
|
+
token: receiveToken
|
|
639
|
+
});
|
|
628
640
|
registry.unregisterIfIdle(clientDID);
|
|
629
641
|
reader.cancel().catch(()=>{});
|
|
630
642
|
writer.abort(new Error('receive channel torn down')).catch(()=>{});
|
|
@@ -718,7 +730,11 @@ export function createHandlers(params) {
|
|
|
718
730
|
// Binding evicts whatever held the lane: a reconnect means the old connection broke and the
|
|
719
731
|
// server learns last, so the stale writer must give way to the live one. See
|
|
720
732
|
// `HubClientRegistry.bindReceiveWriter`.
|
|
721
|
-
const { token, evicted } = registry.bindReceiveWriter(
|
|
733
|
+
const { token, evicted } = registry.bindReceiveWriter({
|
|
734
|
+
did: clientDID,
|
|
735
|
+
writer: onLive,
|
|
736
|
+
endReceive: ()=>endEvicted?.()
|
|
737
|
+
});
|
|
722
738
|
receiveToken = token;
|
|
723
739
|
// After the bind, so the lane is never unheld between the two.
|
|
724
740
|
evicted?.();
|
|
@@ -770,7 +786,10 @@ export function createHandlers(params) {
|
|
|
770
786
|
if (!tornDown) logUnackedFrames();
|
|
771
787
|
tornDown = true;
|
|
772
788
|
notifyWindow();
|
|
773
|
-
if (receiveToken != null) registry.releaseReceiveWriter(
|
|
789
|
+
if (receiveToken != null) registry.releaseReceiveWriter({
|
|
790
|
+
did: clientDID,
|
|
791
|
+
token: receiveToken
|
|
792
|
+
});
|
|
774
793
|
registry.unregisterIfIdle(clientDID);
|
|
775
794
|
reader.cancel().catch(()=>{});
|
|
776
795
|
writer.abort(error).catch(()=>{});
|
package/lib/hub.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { ServerTransportOf } from '@enkaku/protocol';
|
|
2
|
-
import type { AccessRules, ReplayOptions, ResourceLimits, Server } from '@enkaku/server';
|
|
2
|
+
import type { AccessRules, InvocationHook, 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
|
-
import {
|
|
6
|
+
import type { AuthorizeHook, CreateHandlersParams, HubRateLimits, HubStoreErrorHook, KeyPackageFetchLimits } from './handlers.js';
|
|
7
7
|
import { HubClientRegistry } from './registry.js';
|
|
8
8
|
/**
|
|
9
9
|
* Default access rules: any authenticated DID may call hub procedures.
|
|
@@ -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
|
/**
|
|
@@ -53,6 +53,9 @@ export type CreateHubParams = {
|
|
|
53
53
|
* delegated capabilities.
|
|
54
54
|
*/
|
|
55
55
|
verifyToken?: VerifyTokenHook;
|
|
56
|
+
onInvocation?: InvocationHook;
|
|
57
|
+
/** An absent result attributes directly to the sender. */
|
|
58
|
+
resolveAttribution?: CreateHandlersParams['resolveAttribution'];
|
|
56
59
|
/** Publish rate limits. Merged over {@link DEFAULT_RATE_LIMITS}. */
|
|
57
60
|
rateLimits?: Partial<HubRateLimits>;
|
|
58
61
|
/**
|
package/lib/hub.js
CHANGED
|
@@ -32,6 +32,7 @@ export function createHub(params) {
|
|
|
32
32
|
registry,
|
|
33
33
|
store: params.store,
|
|
34
34
|
authorize: params.authorize,
|
|
35
|
+
resolveAttribution: params.resolveAttribution,
|
|
35
36
|
rateLimits: params.rateLimits,
|
|
36
37
|
keyPackageFetchLimits: params.keyPackageFetchLimits,
|
|
37
38
|
receiveAuthCacheTTL: params.receiveAuthCacheTTL,
|
|
@@ -55,8 +56,10 @@ export function createHub(params) {
|
|
|
55
56
|
protocol: hubProtocol,
|
|
56
57
|
transport: params.transport,
|
|
57
58
|
identity: params.identity,
|
|
59
|
+
crypto: params.crypto,
|
|
58
60
|
accessRules: params.accessRules ?? DEFAULT_HUB_ACCESS_RULES,
|
|
59
61
|
verifyToken: params.verifyToken,
|
|
62
|
+
onInvocation: params.onInvocation,
|
|
60
63
|
replay: {
|
|
61
64
|
...params.replay,
|
|
62
65
|
enabled: true,
|
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/memoryStore.js
CHANGED
|
@@ -137,7 +137,9 @@ function isLive(entry, nowSeconds) {
|
|
|
137
137
|
if (accepted !== undefined) {
|
|
138
138
|
return {
|
|
139
139
|
sequenceID: accepted,
|
|
140
|
-
deduped: true
|
|
140
|
+
deduped: true,
|
|
141
|
+
outcome: 'deduplicated',
|
|
142
|
+
storedBytes: 0
|
|
141
143
|
};
|
|
142
144
|
}
|
|
143
145
|
}
|
|
@@ -174,7 +176,9 @@ function isLive(entry, nowSeconds) {
|
|
|
174
176
|
if (retain === 'mailbox' && recipients.size === 0) {
|
|
175
177
|
return {
|
|
176
178
|
sequenceID,
|
|
177
|
-
deduped: false
|
|
179
|
+
deduped: false,
|
|
180
|
+
outcome: 'no-recipients',
|
|
181
|
+
storedBytes: 0
|
|
178
182
|
};
|
|
179
183
|
}
|
|
180
184
|
const entry = {
|
|
@@ -219,7 +223,9 @@ function isLive(entry, nowSeconds) {
|
|
|
219
223
|
}
|
|
220
224
|
return {
|
|
221
225
|
sequenceID,
|
|
222
|
-
deduped: false
|
|
226
|
+
deduped: false,
|
|
227
|
+
outcome: 'inserted',
|
|
228
|
+
storedBytes: params.payload.byteLength
|
|
223
229
|
};
|
|
224
230
|
},
|
|
225
231
|
async fetch (params) {
|
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
|
|
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
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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.
|
|
3
|
+
"version": "0.13.1",
|
|
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.
|
|
29
|
-
"@enkaku/server": "^0.
|
|
30
|
-
"@kokuin/capability": "^0.
|
|
31
|
-
"@kokuin/token": "^0.
|
|
32
|
-
"@kumiai/hub-protocol": "^0.
|
|
28
|
+
"@enkaku/protocol": "^0.23.1",
|
|
29
|
+
"@enkaku/server": "^0.23.1",
|
|
30
|
+
"@kokuin/capability": "^0.7.0",
|
|
31
|
+
"@kokuin/token": "^0.7.0",
|
|
32
|
+
"@kumiai/hub-protocol": "^0.13.1",
|
|
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.
|
|
39
|
-
"@enkaku/transport": "^0.
|
|
40
|
-
"@
|
|
41
|
-
"@kumiai/hub-
|
|
38
|
+
"@enkaku/client": "^0.23.1",
|
|
39
|
+
"@enkaku/transport": "^0.23.1",
|
|
40
|
+
"@kokuin/crypto": "^0.7.1",
|
|
41
|
+
"@kumiai/hub-conformance": "^0.13.0",
|
|
42
|
+
"@kumiai/hub-wake": "^0.13.0"
|
|
42
43
|
},
|
|
43
44
|
"publishConfig": {
|
|
44
45
|
"access": "public"
|