@fieldnotes/sync-server 0.17.1 → 0.19.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 +79 -16
- package/dist/index.cjs +353 -63
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +126 -21
- package/dist/index.d.ts +126 -21
- package/dist/index.js +358 -65
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { ServiceKey, ElementRegistry } from '@fieldnotes/core';
|
|
2
|
+
import { WireSyncElement, WireSyncOp, LayerRecord, ExtensionKind, TypedExtensionOp, PluginSnapshot } from '@fieldnotes/sync';
|
|
3
3
|
export { PluginSnapshot } from '@fieldnotes/sync';
|
|
4
4
|
import { WebSocketServer } from 'ws';
|
|
5
5
|
import { IncomingMessage, Server } from 'http';
|
|
@@ -18,24 +18,21 @@ declare class InMemoryHubFanout implements HubFanout {
|
|
|
18
18
|
interface HubBackend {
|
|
19
19
|
/** True when every hub instance addresses the same atomic backing state (for example Redis). */
|
|
20
20
|
readonly sharedAcrossInstances?: boolean;
|
|
21
|
-
snapshot(room: string): Promise<
|
|
22
|
-
get(room: string, id: string): Promise<
|
|
23
|
-
apply(room: string, op:
|
|
21
|
+
snapshot(room: string): Promise<WireSyncElement[]>;
|
|
22
|
+
get(room: string, id: string): Promise<WireSyncElement | undefined>;
|
|
23
|
+
apply(room: string, op: WireSyncOp): Promise<void>;
|
|
24
24
|
layerRecords?(room: string): Promise<LayerRecord[]>;
|
|
25
25
|
getLayerRecord?(room: string, id: string): Promise<LayerRecord | undefined>;
|
|
26
26
|
applyLayerRecord?(room: string, record: LayerRecord): Promise<void>;
|
|
27
27
|
getService?<T>(key: ServiceKey<T>): T | undefined;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
type OwnedElement =
|
|
31
|
-
ownerId?: string;
|
|
32
|
-
audience?: string;
|
|
33
|
-
};
|
|
30
|
+
type OwnedElement = WireSyncElement;
|
|
34
31
|
interface AuthorizeContext {
|
|
35
32
|
userId?: string;
|
|
36
33
|
role?: string;
|
|
37
34
|
room: string;
|
|
38
|
-
op:
|
|
35
|
+
op: WireSyncOp;
|
|
39
36
|
currentElement?: OwnedElement;
|
|
40
37
|
}
|
|
41
38
|
type Authorize = (ctx: AuthorizeContext) => boolean | Promise<boolean>;
|
|
@@ -43,7 +40,7 @@ interface AuthorizeLayerContext {
|
|
|
43
40
|
userId?: string;
|
|
44
41
|
role?: string;
|
|
45
42
|
room: string;
|
|
46
|
-
op: Extract<
|
|
43
|
+
op: Extract<WireSyncOp, {
|
|
47
44
|
kind: 'layer-upsert' | 'layer-remove';
|
|
48
45
|
}>;
|
|
49
46
|
/** The hub's current record for the target layer, tombstones included. */
|
|
@@ -63,11 +60,39 @@ interface ReadContext {
|
|
|
63
60
|
audience: string | undefined;
|
|
64
61
|
}
|
|
65
62
|
type CanRead = (ctx: ReadContext) => boolean;
|
|
63
|
+
interface OwnerReadContext {
|
|
64
|
+
userId?: string;
|
|
65
|
+
role?: string;
|
|
66
|
+
room: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Decides whether a viewer may see the server-stamped `ownerId` on elements it
|
|
70
|
+
* receives (live ops, snapshots and corrections). Without a hook `ownerId` is
|
|
71
|
+
* stripped from every outbound frame; it stays in the backend for `authorize`.
|
|
72
|
+
*/
|
|
73
|
+
type CanReadOwnerId = (ctx: OwnerReadContext) => boolean;
|
|
74
|
+
interface ResolveAudienceContext {
|
|
75
|
+
userId?: string;
|
|
76
|
+
role?: string;
|
|
77
|
+
room: string;
|
|
78
|
+
/** The incoming element as the client sent it, client-asserted `audience` included. */
|
|
79
|
+
element: WireSyncElement;
|
|
80
|
+
/** The hub's stored element for the same id, if any. */
|
|
81
|
+
currentElement?: OwnedElement;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Decides the authoritative `audience` of an upserted element. The returned
|
|
85
|
+
* value replaces whatever the client asserted (`undefined` clears the field)
|
|
86
|
+
* before `authorize`, storage and relay, so a client can neither hide content
|
|
87
|
+
* by tagging it nor reveal content by retagging it. Without a hook the
|
|
88
|
+
* client's tag passes through and `authorize` must check it.
|
|
89
|
+
*/
|
|
90
|
+
type ResolveAudience = (ctx: ResolveAudienceContext) => string | undefined;
|
|
66
91
|
|
|
67
92
|
interface ApplyResult {
|
|
68
|
-
readonly accepted:
|
|
69
|
-
readonly corrections:
|
|
70
|
-
readonly broadcast?:
|
|
93
|
+
readonly accepted: WireSyncOp | null;
|
|
94
|
+
readonly corrections: WireSyncOp[];
|
|
95
|
+
readonly broadcast?: WireSyncOp[];
|
|
71
96
|
readonly locality?: 'shared' | 'local';
|
|
72
97
|
}
|
|
73
98
|
interface ServerOpContext {
|
|
@@ -78,7 +103,7 @@ interface ServerOpContext {
|
|
|
78
103
|
readonly backend: HubBackend;
|
|
79
104
|
backendPlugin<T>(key: ServiceKey<T>): T | undefined;
|
|
80
105
|
}
|
|
81
|
-
type ServerNext = (op:
|
|
106
|
+
type ServerNext = (op: WireSyncOp, context: ServerOpContext) => Promise<ApplyResult>;
|
|
82
107
|
interface ServerExtensionRegistry {
|
|
83
108
|
register<TPayload>(kind: ExtensionKind<TPayload>, handler: (op: TypedExtensionOp<TPayload>, context: ServerOpContext) => Promise<ApplyResult>): void;
|
|
84
109
|
}
|
|
@@ -86,8 +111,8 @@ interface ServerSyncPlugin {
|
|
|
86
111
|
readonly name: string;
|
|
87
112
|
readonly ownedLegacyKinds?: readonly string[];
|
|
88
113
|
readonly legacySnapshotKey?: string;
|
|
89
|
-
process?(op:
|
|
90
|
-
applyFanout?(op:
|
|
114
|
+
process?(op: WireSyncOp, context: ServerOpContext, next: ServerNext): Promise<ApplyResult>;
|
|
115
|
+
applyFanout?(op: WireSyncOp, context: ServerOpContext): Promise<WireSyncOp | null>;
|
|
91
116
|
registerExtensionKinds?(registry: ServerExtensionRegistry): void;
|
|
92
117
|
snapshot?(room: string, backend: HubBackend): Promise<PluginSnapshot | undefined>;
|
|
93
118
|
filterSnapshot?(snapshot: PluginSnapshot, viewer: {
|
|
@@ -111,9 +136,15 @@ interface SyncHubOptions {
|
|
|
111
136
|
authorizeLayer?: AuthorizeLayer;
|
|
112
137
|
plugins?: readonly ServerSyncPlugin[];
|
|
113
138
|
canRead?: CanRead;
|
|
139
|
+
canReadOwnerId?: CanReadOwnerId;
|
|
140
|
+
resolveAudience?: ResolveAudience;
|
|
114
141
|
maxJsonDepth?: number;
|
|
115
142
|
presenceThrottleMs?: number;
|
|
116
143
|
maxPresenceLanes?: number;
|
|
144
|
+
/** Largest `presence.data` payload relayed, in UTF-8 bytes of its JSON encoding. */
|
|
145
|
+
maxPresenceBytes?: number;
|
|
146
|
+
/** Registry used to translate extension elements for legacy peers. */
|
|
147
|
+
elementRegistry?: ElementRegistry;
|
|
117
148
|
}
|
|
118
149
|
declare class SyncHub {
|
|
119
150
|
private readonly backend;
|
|
@@ -127,11 +158,16 @@ declare class SyncHub {
|
|
|
127
158
|
private readonly authorize?;
|
|
128
159
|
private readonly authorizeLayer?;
|
|
129
160
|
private readonly pluginRegistry;
|
|
161
|
+
private readonly elementRegistry;
|
|
162
|
+
private readonly peerCapabilities;
|
|
130
163
|
private readonly canRead?;
|
|
164
|
+
private readonly canReadOwnerId?;
|
|
165
|
+
private readonly resolveAudience?;
|
|
131
166
|
private readonly memoryLayers;
|
|
132
167
|
private readonly maxJsonDepth;
|
|
133
168
|
private readonly presenceThrottleMs;
|
|
134
169
|
private readonly maxPresenceLanes;
|
|
170
|
+
private readonly maxPresenceBytes;
|
|
135
171
|
/**
|
|
136
172
|
* Presence throttle state keyed by connection, then by lane. A lane is the
|
|
137
173
|
* payload's `kind` (a non-empty string of at most 64 chars) or the reserved
|
|
@@ -170,9 +206,32 @@ declare class SyncHub {
|
|
|
170
206
|
private getLayerRecords;
|
|
171
207
|
private getLayerRecord;
|
|
172
208
|
private applyLayerRecord;
|
|
209
|
+
private mayReadOwnerId;
|
|
173
210
|
private mayRead;
|
|
174
211
|
private safePublish;
|
|
175
212
|
private relayToRoom;
|
|
213
|
+
/**
|
|
214
|
+
* Translates `op` for the peer and sends it. Returns false when the op is
|
|
215
|
+
* lossy for this peer (no legacy encoding) or the socket throws; neither
|
|
216
|
+
* may reject the room operation that produced it.
|
|
217
|
+
*/
|
|
218
|
+
private sendToConnection;
|
|
219
|
+
/**
|
|
220
|
+
* Returns the wire frame for `op` translated for `capabilities`, or null
|
|
221
|
+
* when lossy. The server-stamped `ownerId` is a server-side authorization
|
|
222
|
+
* fact: it leaves the hub only for peers `canReadOwnerId` admits.
|
|
223
|
+
*/
|
|
224
|
+
private encodeForPeer;
|
|
225
|
+
/**
|
|
226
|
+
* Normalize registered legacy wire elements before authorization, storage,
|
|
227
|
+
* and relay. A legacy type this hub has no adapter for is forwarded and
|
|
228
|
+
* stored verbatim — the hub is a relay, and the peers decide whether they
|
|
229
|
+
* understand it — so a hub deployed without domain adapters never erases
|
|
230
|
+
* the room's existing elements. Only a malformed registered element is dropped.
|
|
231
|
+
*/
|
|
232
|
+
private normalizeElement;
|
|
233
|
+
private normalizeElements;
|
|
234
|
+
private relayOpToRoom;
|
|
176
235
|
private broadcastClientPresence;
|
|
177
236
|
private clearPresenceLanes;
|
|
178
237
|
private schedulePresence;
|
|
@@ -181,6 +240,7 @@ declare class SyncHub {
|
|
|
181
240
|
private deliverToRoom;
|
|
182
241
|
private sendCorrection;
|
|
183
242
|
private onFanout;
|
|
243
|
+
private isPresenceWithinLimit;
|
|
184
244
|
private applyFanoutLayerOp;
|
|
185
245
|
close(): void;
|
|
186
246
|
}
|
|
@@ -190,9 +250,9 @@ declare class MemoryHubBackend implements HubBackend {
|
|
|
190
250
|
private roomLayers;
|
|
191
251
|
private room;
|
|
192
252
|
private layers;
|
|
193
|
-
snapshot(room: string): Promise<
|
|
194
|
-
get(room: string, id: string): Promise<
|
|
195
|
-
apply(room: string, op:
|
|
253
|
+
snapshot(room: string): Promise<WireSyncElement[]>;
|
|
254
|
+
get(room: string, id: string): Promise<WireSyncElement | undefined>;
|
|
255
|
+
apply(room: string, op: WireSyncOp): Promise<void>;
|
|
196
256
|
layerRecords(room: string): Promise<LayerRecord[]>;
|
|
197
257
|
getLayerRecord(room: string, id: string): Promise<LayerRecord | undefined>;
|
|
198
258
|
applyLayerRecord(room: string, record: LayerRecord): Promise<void>;
|
|
@@ -201,12 +261,25 @@ declare class MemoryHubBackend implements HubBackend {
|
|
|
201
261
|
interface AuthInfo {
|
|
202
262
|
req: IncomingMessage;
|
|
203
263
|
room: string;
|
|
264
|
+
/**
|
|
265
|
+
* Bearer token resolved by `readBearerToken(req)`: a `fieldnotes-bearer.<token>`
|
|
266
|
+
* `Sec-WebSocket-Protocol` entry, else an `Authorization: Bearer` header, else the
|
|
267
|
+
* `token` URL query parameter (legacy; URLs land in proxy logs). `undefined` when
|
|
268
|
+
* none is present.
|
|
269
|
+
*/
|
|
270
|
+
token?: string;
|
|
204
271
|
}
|
|
205
272
|
interface AuthResult {
|
|
206
273
|
userId: string;
|
|
207
274
|
role?: string;
|
|
208
275
|
}
|
|
209
276
|
type Authenticate = (info: AuthInfo) => AuthResult | null | Promise<AuthResult | null>;
|
|
277
|
+
/**
|
|
278
|
+
* Resolves the bearer token of an upgrade request, preferring channels that stay
|
|
279
|
+
* out of access logs: `Sec-WebSocket-Protocol` bearer entry, then
|
|
280
|
+
* `Authorization: Bearer`, then the `token` query parameter.
|
|
281
|
+
*/
|
|
282
|
+
declare function readBearerToken(req: IncomingMessage): string | undefined;
|
|
210
283
|
|
|
211
284
|
interface CreateSyncServerOptions {
|
|
212
285
|
port?: number;
|
|
@@ -219,6 +292,8 @@ interface CreateSyncServerOptions {
|
|
|
219
292
|
authorizeLayer?: AuthorizeLayer;
|
|
220
293
|
plugins?: readonly ServerSyncPlugin[];
|
|
221
294
|
canRead?: CanRead;
|
|
295
|
+
canReadOwnerId?: CanReadOwnerId;
|
|
296
|
+
resolveAudience?: ResolveAudience;
|
|
222
297
|
heartbeatIntervalMs?: number;
|
|
223
298
|
maxMessageBytes?: number;
|
|
224
299
|
maxJsonDepth?: number;
|
|
@@ -226,9 +301,30 @@ interface CreateSyncServerOptions {
|
|
|
226
301
|
maxPendingAuthBytes?: number;
|
|
227
302
|
messagesPerSecond?: number;
|
|
228
303
|
messageBurst?: number;
|
|
304
|
+
/** Sustained inbound bytes per second per connection (token bucket). */
|
|
305
|
+
bytesPerSecond?: number;
|
|
306
|
+
/** Inbound byte spike allowance per connection; a frame larger than this is never admitted. */
|
|
307
|
+
byteBurst?: number;
|
|
229
308
|
presenceThrottleMs?: number;
|
|
230
309
|
maxPresenceLanes?: number;
|
|
310
|
+
maxPresenceBytes?: number;
|
|
311
|
+
/** Concurrent sockets per client address; `Infinity` disables the cap. */
|
|
312
|
+
maxConnectionsPerIp?: number;
|
|
313
|
+
/** Concurrent sockets per room, pending-auth sockets included; `Infinity` disables the cap. */
|
|
314
|
+
maxConnectionsPerRoom?: number;
|
|
315
|
+
/**
|
|
316
|
+
* Resolves the client address the per-IP cap keys on. Defaults to the socket's
|
|
317
|
+
* remote address; behind a trusted proxy read the forwarded header here.
|
|
318
|
+
* Returning `undefined` or `''` exempts the connection from the per-IP cap.
|
|
319
|
+
*/
|
|
320
|
+
clientAddress?: (req: IncomingMessage) => string | undefined;
|
|
231
321
|
shutdownGraceMs?: number;
|
|
322
|
+
/**
|
|
323
|
+
* Registry used to translate extension envelopes for legacy peers. Without
|
|
324
|
+
* it the hub relays unknown legacy element types verbatim and cannot encode
|
|
325
|
+
* envelopes for pre-envelope clients.
|
|
326
|
+
*/
|
|
327
|
+
elementRegistry?: ElementRegistry;
|
|
232
328
|
}
|
|
233
329
|
declare function createSyncServer(options?: CreateSyncServerOptions): {
|
|
234
330
|
hub: SyncHub;
|
|
@@ -236,6 +332,15 @@ declare function createSyncServer(options?: CreateSyncServerOptions): {
|
|
|
236
332
|
close: () => Promise<void>;
|
|
237
333
|
};
|
|
238
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Room names the relay admits. Backends derive storage keys from the room
|
|
337
|
+
* name (`<prefix><room>:layers`, `<prefix><room>:fog:meta`, ...), so the
|
|
338
|
+
* alphabet excludes every separator a key builder uses and bounds the length
|
|
339
|
+
* before `authenticate` runs.
|
|
340
|
+
*/
|
|
341
|
+
declare const ROOM_NAME_PATTERN: RegExp;
|
|
342
|
+
declare function isValidRoomName(room: unknown): room is string;
|
|
343
|
+
|
|
239
344
|
interface HeartbeatSocket {
|
|
240
345
|
ping(): void;
|
|
241
346
|
terminate(): void;
|
|
@@ -250,4 +355,4 @@ interface Heartbeat {
|
|
|
250
355
|
}
|
|
251
356
|
declare function startHeartbeat(wss: HeartbeatServer, intervalMs: number): Heartbeat;
|
|
252
357
|
|
|
253
|
-
export { type ApplyResult, type AuthInfo, type AuthResult, type Authenticate, type Authorize, type AuthorizeContext, type AuthorizeLayer, type AuthorizeLayerContext, type CanRead, type Connection, type CreateSyncServerOptions, type Heartbeat, type HeartbeatServer, type HeartbeatSocket, type HubBackend, type HubFanout, InMemoryHubFanout, MemoryHubBackend, type OwnedElement, type ReadContext, type ServerExtensionRegistry, type ServerNext, type ServerOpContext, type ServerSyncPlugin, SyncHub, type SyncHubOptions, createSyncServer, startHeartbeat };
|
|
358
|
+
export { type ApplyResult, type AuthInfo, type AuthResult, type Authenticate, type Authorize, type AuthorizeContext, type AuthorizeLayer, type AuthorizeLayerContext, type CanRead, type CanReadOwnerId, type Connection, type CreateSyncServerOptions, type Heartbeat, type HeartbeatServer, type HeartbeatSocket, type HubBackend, type HubFanout, InMemoryHubFanout, MemoryHubBackend, type OwnedElement, type OwnerReadContext, ROOM_NAME_PATTERN, type ReadContext, type ResolveAudience, type ResolveAudienceContext, type ServerExtensionRegistry, type ServerNext, type ServerOpContext, type ServerSyncPlugin, SyncHub, type SyncHubOptions, createSyncServer, isValidRoomName, readBearerToken, startHeartbeat };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { ServiceKey, ElementRegistry } from '@fieldnotes/core';
|
|
2
|
+
import { WireSyncElement, WireSyncOp, LayerRecord, ExtensionKind, TypedExtensionOp, PluginSnapshot } from '@fieldnotes/sync';
|
|
3
3
|
export { PluginSnapshot } from '@fieldnotes/sync';
|
|
4
4
|
import { WebSocketServer } from 'ws';
|
|
5
5
|
import { IncomingMessage, Server } from 'http';
|
|
@@ -18,24 +18,21 @@ declare class InMemoryHubFanout implements HubFanout {
|
|
|
18
18
|
interface HubBackend {
|
|
19
19
|
/** True when every hub instance addresses the same atomic backing state (for example Redis). */
|
|
20
20
|
readonly sharedAcrossInstances?: boolean;
|
|
21
|
-
snapshot(room: string): Promise<
|
|
22
|
-
get(room: string, id: string): Promise<
|
|
23
|
-
apply(room: string, op:
|
|
21
|
+
snapshot(room: string): Promise<WireSyncElement[]>;
|
|
22
|
+
get(room: string, id: string): Promise<WireSyncElement | undefined>;
|
|
23
|
+
apply(room: string, op: WireSyncOp): Promise<void>;
|
|
24
24
|
layerRecords?(room: string): Promise<LayerRecord[]>;
|
|
25
25
|
getLayerRecord?(room: string, id: string): Promise<LayerRecord | undefined>;
|
|
26
26
|
applyLayerRecord?(room: string, record: LayerRecord): Promise<void>;
|
|
27
27
|
getService?<T>(key: ServiceKey<T>): T | undefined;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
type OwnedElement =
|
|
31
|
-
ownerId?: string;
|
|
32
|
-
audience?: string;
|
|
33
|
-
};
|
|
30
|
+
type OwnedElement = WireSyncElement;
|
|
34
31
|
interface AuthorizeContext {
|
|
35
32
|
userId?: string;
|
|
36
33
|
role?: string;
|
|
37
34
|
room: string;
|
|
38
|
-
op:
|
|
35
|
+
op: WireSyncOp;
|
|
39
36
|
currentElement?: OwnedElement;
|
|
40
37
|
}
|
|
41
38
|
type Authorize = (ctx: AuthorizeContext) => boolean | Promise<boolean>;
|
|
@@ -43,7 +40,7 @@ interface AuthorizeLayerContext {
|
|
|
43
40
|
userId?: string;
|
|
44
41
|
role?: string;
|
|
45
42
|
room: string;
|
|
46
|
-
op: Extract<
|
|
43
|
+
op: Extract<WireSyncOp, {
|
|
47
44
|
kind: 'layer-upsert' | 'layer-remove';
|
|
48
45
|
}>;
|
|
49
46
|
/** The hub's current record for the target layer, tombstones included. */
|
|
@@ -63,11 +60,39 @@ interface ReadContext {
|
|
|
63
60
|
audience: string | undefined;
|
|
64
61
|
}
|
|
65
62
|
type CanRead = (ctx: ReadContext) => boolean;
|
|
63
|
+
interface OwnerReadContext {
|
|
64
|
+
userId?: string;
|
|
65
|
+
role?: string;
|
|
66
|
+
room: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Decides whether a viewer may see the server-stamped `ownerId` on elements it
|
|
70
|
+
* receives (live ops, snapshots and corrections). Without a hook `ownerId` is
|
|
71
|
+
* stripped from every outbound frame; it stays in the backend for `authorize`.
|
|
72
|
+
*/
|
|
73
|
+
type CanReadOwnerId = (ctx: OwnerReadContext) => boolean;
|
|
74
|
+
interface ResolveAudienceContext {
|
|
75
|
+
userId?: string;
|
|
76
|
+
role?: string;
|
|
77
|
+
room: string;
|
|
78
|
+
/** The incoming element as the client sent it, client-asserted `audience` included. */
|
|
79
|
+
element: WireSyncElement;
|
|
80
|
+
/** The hub's stored element for the same id, if any. */
|
|
81
|
+
currentElement?: OwnedElement;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Decides the authoritative `audience` of an upserted element. The returned
|
|
85
|
+
* value replaces whatever the client asserted (`undefined` clears the field)
|
|
86
|
+
* before `authorize`, storage and relay, so a client can neither hide content
|
|
87
|
+
* by tagging it nor reveal content by retagging it. Without a hook the
|
|
88
|
+
* client's tag passes through and `authorize` must check it.
|
|
89
|
+
*/
|
|
90
|
+
type ResolveAudience = (ctx: ResolveAudienceContext) => string | undefined;
|
|
66
91
|
|
|
67
92
|
interface ApplyResult {
|
|
68
|
-
readonly accepted:
|
|
69
|
-
readonly corrections:
|
|
70
|
-
readonly broadcast?:
|
|
93
|
+
readonly accepted: WireSyncOp | null;
|
|
94
|
+
readonly corrections: WireSyncOp[];
|
|
95
|
+
readonly broadcast?: WireSyncOp[];
|
|
71
96
|
readonly locality?: 'shared' | 'local';
|
|
72
97
|
}
|
|
73
98
|
interface ServerOpContext {
|
|
@@ -78,7 +103,7 @@ interface ServerOpContext {
|
|
|
78
103
|
readonly backend: HubBackend;
|
|
79
104
|
backendPlugin<T>(key: ServiceKey<T>): T | undefined;
|
|
80
105
|
}
|
|
81
|
-
type ServerNext = (op:
|
|
106
|
+
type ServerNext = (op: WireSyncOp, context: ServerOpContext) => Promise<ApplyResult>;
|
|
82
107
|
interface ServerExtensionRegistry {
|
|
83
108
|
register<TPayload>(kind: ExtensionKind<TPayload>, handler: (op: TypedExtensionOp<TPayload>, context: ServerOpContext) => Promise<ApplyResult>): void;
|
|
84
109
|
}
|
|
@@ -86,8 +111,8 @@ interface ServerSyncPlugin {
|
|
|
86
111
|
readonly name: string;
|
|
87
112
|
readonly ownedLegacyKinds?: readonly string[];
|
|
88
113
|
readonly legacySnapshotKey?: string;
|
|
89
|
-
process?(op:
|
|
90
|
-
applyFanout?(op:
|
|
114
|
+
process?(op: WireSyncOp, context: ServerOpContext, next: ServerNext): Promise<ApplyResult>;
|
|
115
|
+
applyFanout?(op: WireSyncOp, context: ServerOpContext): Promise<WireSyncOp | null>;
|
|
91
116
|
registerExtensionKinds?(registry: ServerExtensionRegistry): void;
|
|
92
117
|
snapshot?(room: string, backend: HubBackend): Promise<PluginSnapshot | undefined>;
|
|
93
118
|
filterSnapshot?(snapshot: PluginSnapshot, viewer: {
|
|
@@ -111,9 +136,15 @@ interface SyncHubOptions {
|
|
|
111
136
|
authorizeLayer?: AuthorizeLayer;
|
|
112
137
|
plugins?: readonly ServerSyncPlugin[];
|
|
113
138
|
canRead?: CanRead;
|
|
139
|
+
canReadOwnerId?: CanReadOwnerId;
|
|
140
|
+
resolveAudience?: ResolveAudience;
|
|
114
141
|
maxJsonDepth?: number;
|
|
115
142
|
presenceThrottleMs?: number;
|
|
116
143
|
maxPresenceLanes?: number;
|
|
144
|
+
/** Largest `presence.data` payload relayed, in UTF-8 bytes of its JSON encoding. */
|
|
145
|
+
maxPresenceBytes?: number;
|
|
146
|
+
/** Registry used to translate extension elements for legacy peers. */
|
|
147
|
+
elementRegistry?: ElementRegistry;
|
|
117
148
|
}
|
|
118
149
|
declare class SyncHub {
|
|
119
150
|
private readonly backend;
|
|
@@ -127,11 +158,16 @@ declare class SyncHub {
|
|
|
127
158
|
private readonly authorize?;
|
|
128
159
|
private readonly authorizeLayer?;
|
|
129
160
|
private readonly pluginRegistry;
|
|
161
|
+
private readonly elementRegistry;
|
|
162
|
+
private readonly peerCapabilities;
|
|
130
163
|
private readonly canRead?;
|
|
164
|
+
private readonly canReadOwnerId?;
|
|
165
|
+
private readonly resolveAudience?;
|
|
131
166
|
private readonly memoryLayers;
|
|
132
167
|
private readonly maxJsonDepth;
|
|
133
168
|
private readonly presenceThrottleMs;
|
|
134
169
|
private readonly maxPresenceLanes;
|
|
170
|
+
private readonly maxPresenceBytes;
|
|
135
171
|
/**
|
|
136
172
|
* Presence throttle state keyed by connection, then by lane. A lane is the
|
|
137
173
|
* payload's `kind` (a non-empty string of at most 64 chars) or the reserved
|
|
@@ -170,9 +206,32 @@ declare class SyncHub {
|
|
|
170
206
|
private getLayerRecords;
|
|
171
207
|
private getLayerRecord;
|
|
172
208
|
private applyLayerRecord;
|
|
209
|
+
private mayReadOwnerId;
|
|
173
210
|
private mayRead;
|
|
174
211
|
private safePublish;
|
|
175
212
|
private relayToRoom;
|
|
213
|
+
/**
|
|
214
|
+
* Translates `op` for the peer and sends it. Returns false when the op is
|
|
215
|
+
* lossy for this peer (no legacy encoding) or the socket throws; neither
|
|
216
|
+
* may reject the room operation that produced it.
|
|
217
|
+
*/
|
|
218
|
+
private sendToConnection;
|
|
219
|
+
/**
|
|
220
|
+
* Returns the wire frame for `op` translated for `capabilities`, or null
|
|
221
|
+
* when lossy. The server-stamped `ownerId` is a server-side authorization
|
|
222
|
+
* fact: it leaves the hub only for peers `canReadOwnerId` admits.
|
|
223
|
+
*/
|
|
224
|
+
private encodeForPeer;
|
|
225
|
+
/**
|
|
226
|
+
* Normalize registered legacy wire elements before authorization, storage,
|
|
227
|
+
* and relay. A legacy type this hub has no adapter for is forwarded and
|
|
228
|
+
* stored verbatim — the hub is a relay, and the peers decide whether they
|
|
229
|
+
* understand it — so a hub deployed without domain adapters never erases
|
|
230
|
+
* the room's existing elements. Only a malformed registered element is dropped.
|
|
231
|
+
*/
|
|
232
|
+
private normalizeElement;
|
|
233
|
+
private normalizeElements;
|
|
234
|
+
private relayOpToRoom;
|
|
176
235
|
private broadcastClientPresence;
|
|
177
236
|
private clearPresenceLanes;
|
|
178
237
|
private schedulePresence;
|
|
@@ -181,6 +240,7 @@ declare class SyncHub {
|
|
|
181
240
|
private deliverToRoom;
|
|
182
241
|
private sendCorrection;
|
|
183
242
|
private onFanout;
|
|
243
|
+
private isPresenceWithinLimit;
|
|
184
244
|
private applyFanoutLayerOp;
|
|
185
245
|
close(): void;
|
|
186
246
|
}
|
|
@@ -190,9 +250,9 @@ declare class MemoryHubBackend implements HubBackend {
|
|
|
190
250
|
private roomLayers;
|
|
191
251
|
private room;
|
|
192
252
|
private layers;
|
|
193
|
-
snapshot(room: string): Promise<
|
|
194
|
-
get(room: string, id: string): Promise<
|
|
195
|
-
apply(room: string, op:
|
|
253
|
+
snapshot(room: string): Promise<WireSyncElement[]>;
|
|
254
|
+
get(room: string, id: string): Promise<WireSyncElement | undefined>;
|
|
255
|
+
apply(room: string, op: WireSyncOp): Promise<void>;
|
|
196
256
|
layerRecords(room: string): Promise<LayerRecord[]>;
|
|
197
257
|
getLayerRecord(room: string, id: string): Promise<LayerRecord | undefined>;
|
|
198
258
|
applyLayerRecord(room: string, record: LayerRecord): Promise<void>;
|
|
@@ -201,12 +261,25 @@ declare class MemoryHubBackend implements HubBackend {
|
|
|
201
261
|
interface AuthInfo {
|
|
202
262
|
req: IncomingMessage;
|
|
203
263
|
room: string;
|
|
264
|
+
/**
|
|
265
|
+
* Bearer token resolved by `readBearerToken(req)`: a `fieldnotes-bearer.<token>`
|
|
266
|
+
* `Sec-WebSocket-Protocol` entry, else an `Authorization: Bearer` header, else the
|
|
267
|
+
* `token` URL query parameter (legacy; URLs land in proxy logs). `undefined` when
|
|
268
|
+
* none is present.
|
|
269
|
+
*/
|
|
270
|
+
token?: string;
|
|
204
271
|
}
|
|
205
272
|
interface AuthResult {
|
|
206
273
|
userId: string;
|
|
207
274
|
role?: string;
|
|
208
275
|
}
|
|
209
276
|
type Authenticate = (info: AuthInfo) => AuthResult | null | Promise<AuthResult | null>;
|
|
277
|
+
/**
|
|
278
|
+
* Resolves the bearer token of an upgrade request, preferring channels that stay
|
|
279
|
+
* out of access logs: `Sec-WebSocket-Protocol` bearer entry, then
|
|
280
|
+
* `Authorization: Bearer`, then the `token` query parameter.
|
|
281
|
+
*/
|
|
282
|
+
declare function readBearerToken(req: IncomingMessage): string | undefined;
|
|
210
283
|
|
|
211
284
|
interface CreateSyncServerOptions {
|
|
212
285
|
port?: number;
|
|
@@ -219,6 +292,8 @@ interface CreateSyncServerOptions {
|
|
|
219
292
|
authorizeLayer?: AuthorizeLayer;
|
|
220
293
|
plugins?: readonly ServerSyncPlugin[];
|
|
221
294
|
canRead?: CanRead;
|
|
295
|
+
canReadOwnerId?: CanReadOwnerId;
|
|
296
|
+
resolveAudience?: ResolveAudience;
|
|
222
297
|
heartbeatIntervalMs?: number;
|
|
223
298
|
maxMessageBytes?: number;
|
|
224
299
|
maxJsonDepth?: number;
|
|
@@ -226,9 +301,30 @@ interface CreateSyncServerOptions {
|
|
|
226
301
|
maxPendingAuthBytes?: number;
|
|
227
302
|
messagesPerSecond?: number;
|
|
228
303
|
messageBurst?: number;
|
|
304
|
+
/** Sustained inbound bytes per second per connection (token bucket). */
|
|
305
|
+
bytesPerSecond?: number;
|
|
306
|
+
/** Inbound byte spike allowance per connection; a frame larger than this is never admitted. */
|
|
307
|
+
byteBurst?: number;
|
|
229
308
|
presenceThrottleMs?: number;
|
|
230
309
|
maxPresenceLanes?: number;
|
|
310
|
+
maxPresenceBytes?: number;
|
|
311
|
+
/** Concurrent sockets per client address; `Infinity` disables the cap. */
|
|
312
|
+
maxConnectionsPerIp?: number;
|
|
313
|
+
/** Concurrent sockets per room, pending-auth sockets included; `Infinity` disables the cap. */
|
|
314
|
+
maxConnectionsPerRoom?: number;
|
|
315
|
+
/**
|
|
316
|
+
* Resolves the client address the per-IP cap keys on. Defaults to the socket's
|
|
317
|
+
* remote address; behind a trusted proxy read the forwarded header here.
|
|
318
|
+
* Returning `undefined` or `''` exempts the connection from the per-IP cap.
|
|
319
|
+
*/
|
|
320
|
+
clientAddress?: (req: IncomingMessage) => string | undefined;
|
|
231
321
|
shutdownGraceMs?: number;
|
|
322
|
+
/**
|
|
323
|
+
* Registry used to translate extension envelopes for legacy peers. Without
|
|
324
|
+
* it the hub relays unknown legacy element types verbatim and cannot encode
|
|
325
|
+
* envelopes for pre-envelope clients.
|
|
326
|
+
*/
|
|
327
|
+
elementRegistry?: ElementRegistry;
|
|
232
328
|
}
|
|
233
329
|
declare function createSyncServer(options?: CreateSyncServerOptions): {
|
|
234
330
|
hub: SyncHub;
|
|
@@ -236,6 +332,15 @@ declare function createSyncServer(options?: CreateSyncServerOptions): {
|
|
|
236
332
|
close: () => Promise<void>;
|
|
237
333
|
};
|
|
238
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Room names the relay admits. Backends derive storage keys from the room
|
|
337
|
+
* name (`<prefix><room>:layers`, `<prefix><room>:fog:meta`, ...), so the
|
|
338
|
+
* alphabet excludes every separator a key builder uses and bounds the length
|
|
339
|
+
* before `authenticate` runs.
|
|
340
|
+
*/
|
|
341
|
+
declare const ROOM_NAME_PATTERN: RegExp;
|
|
342
|
+
declare function isValidRoomName(room: unknown): room is string;
|
|
343
|
+
|
|
239
344
|
interface HeartbeatSocket {
|
|
240
345
|
ping(): void;
|
|
241
346
|
terminate(): void;
|
|
@@ -250,4 +355,4 @@ interface Heartbeat {
|
|
|
250
355
|
}
|
|
251
356
|
declare function startHeartbeat(wss: HeartbeatServer, intervalMs: number): Heartbeat;
|
|
252
357
|
|
|
253
|
-
export { type ApplyResult, type AuthInfo, type AuthResult, type Authenticate, type Authorize, type AuthorizeContext, type AuthorizeLayer, type AuthorizeLayerContext, type CanRead, type Connection, type CreateSyncServerOptions, type Heartbeat, type HeartbeatServer, type HeartbeatSocket, type HubBackend, type HubFanout, InMemoryHubFanout, MemoryHubBackend, type OwnedElement, type ReadContext, type ServerExtensionRegistry, type ServerNext, type ServerOpContext, type ServerSyncPlugin, SyncHub, type SyncHubOptions, createSyncServer, startHeartbeat };
|
|
358
|
+
export { type ApplyResult, type AuthInfo, type AuthResult, type Authenticate, type Authorize, type AuthorizeContext, type AuthorizeLayer, type AuthorizeLayerContext, type CanRead, type CanReadOwnerId, type Connection, type CreateSyncServerOptions, type Heartbeat, type HeartbeatServer, type HeartbeatSocket, type HubBackend, type HubFanout, InMemoryHubFanout, MemoryHubBackend, type OwnedElement, type OwnerReadContext, ROOM_NAME_PATTERN, type ReadContext, type ResolveAudience, type ResolveAudienceContext, type ServerExtensionRegistry, type ServerNext, type ServerOpContext, type ServerSyncPlugin, SyncHub, type SyncHubOptions, createSyncServer, isValidRoomName, readBearerToken, startHeartbeat };
|