@korajs/server 1.0.0-beta.0 → 1.0.0-beta.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/dist/index.cjs +586 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +509 -179
- package/dist/index.d.ts +509 -179
- package/dist/index.js +568 -49
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,173 @@
|
|
|
1
|
-
import { SchemaDefinition,
|
|
1
|
+
import { SchemaDefinition, Operation, BlobRef, KoraEventEmitter, VersionVector } from '@korajs/core';
|
|
2
2
|
import * as _korajs_sync from '@korajs/sync';
|
|
3
|
-
import { SyncStore, MessageSerializer, SyncMessage, AwarenessUpdateMessage, YjsDocUpdateMessage,
|
|
3
|
+
import { SyncStore, ApplyResult, MessageSerializer, SyncMessage, AwarenessUpdateMessage, YjsDocUpdateMessage, BlobChunkRequestMessage, BlobChunkResponseMessage } from '@korajs/sync';
|
|
4
4
|
import { S as ServerTransport, a as ServerMessageHandler, b as ServerCloseHandler, c as ServerErrorHandler } from './server-transport-CU1BLWgr.js';
|
|
5
5
|
import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
|
|
6
6
|
import { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Per-collection scope map from auth context.
|
|
10
|
+
*/
|
|
11
|
+
type ScopeMap = Record<string, Record<string, unknown>>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A materialized record reconstructed from the operation log
|
|
15
|
+
* or read from a materialized collection table.
|
|
16
|
+
*/
|
|
17
|
+
interface MaterializedRecord {
|
|
18
|
+
id: string;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Options for querying a materialized collection table.
|
|
23
|
+
*/
|
|
24
|
+
interface CollectionQueryOptions {
|
|
25
|
+
/** Exact-match filters on field values */
|
|
26
|
+
where?: Record<string, unknown>;
|
|
27
|
+
/** Field name to order results by */
|
|
28
|
+
orderBy?: string;
|
|
29
|
+
/** Sort direction (default: 'asc') */
|
|
30
|
+
orderDirection?: 'asc' | 'desc';
|
|
31
|
+
/** Maximum number of records to return */
|
|
32
|
+
limit?: number;
|
|
33
|
+
/** Number of records to skip (for pagination) */
|
|
34
|
+
offset?: number;
|
|
35
|
+
/** Include soft-deleted records (default: false) */
|
|
36
|
+
includeDeleted?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Server-side store interface. Extends SyncStore with lifecycle,
|
|
40
|
+
* introspection, and materialization methods needed by the sync server.
|
|
41
|
+
*/
|
|
42
|
+
interface ServerStore extends SyncStore {
|
|
43
|
+
/** Close the store and release resources */
|
|
44
|
+
close(): Promise<void>;
|
|
45
|
+
/** Get the total number of stored operations */
|
|
46
|
+
getOperationCount(): Promise<number>;
|
|
47
|
+
/**
|
|
48
|
+
* Set the schema for materialized collection tables.
|
|
49
|
+
* Creates collection tables and indexes based on the schema definition.
|
|
50
|
+
* If operations already exist in the store, backfills the materialized
|
|
51
|
+
* tables from the operation log.
|
|
52
|
+
*
|
|
53
|
+
* @param schema - The schema definition describing all collections
|
|
54
|
+
*/
|
|
55
|
+
setSchema(schema: SchemaDefinition): Promise<void>;
|
|
56
|
+
/** Schema used for materialized tables and server-side validation, if set. */
|
|
57
|
+
getSchema(): SchemaDefinition | null;
|
|
58
|
+
/**
|
|
59
|
+
* Get all records from a materialized collection.
|
|
60
|
+
* When schema is set, reads directly from the collection table (O(1) indexed).
|
|
61
|
+
* When schema is not set, falls back to replaying the operation log.
|
|
62
|
+
* Deleted records are excluded.
|
|
63
|
+
*
|
|
64
|
+
* @param collection - The collection name to query
|
|
65
|
+
* @returns Array of records with their current state
|
|
66
|
+
*/
|
|
67
|
+
materializeCollection(collection: string): Promise<MaterializedRecord[]>;
|
|
68
|
+
/**
|
|
69
|
+
* Query records from a materialized collection with filtering, ordering,
|
|
70
|
+
* and pagination. Requires schema to be set via setSchema().
|
|
71
|
+
*
|
|
72
|
+
* @param collection - The collection name to query
|
|
73
|
+
* @param options - Query options (where, orderBy, limit, offset)
|
|
74
|
+
* @returns Array of matching records
|
|
75
|
+
*/
|
|
76
|
+
queryCollection(collection: string, options?: CollectionQueryOptions): Promise<MaterializedRecord[]>;
|
|
77
|
+
/**
|
|
78
|
+
* Find a single record by ID from a materialized collection.
|
|
79
|
+
* Requires schema to be set via setSchema().
|
|
80
|
+
*
|
|
81
|
+
* @param collection - The collection name
|
|
82
|
+
* @param id - The record ID
|
|
83
|
+
* @returns The record or null if not found (or deleted)
|
|
84
|
+
*/
|
|
85
|
+
findRecord(collection: string, id: string): Promise<MaterializedRecord | null>;
|
|
86
|
+
/**
|
|
87
|
+
* Count records in a materialized collection, optionally filtered.
|
|
88
|
+
* Requires schema to be set via setSchema().
|
|
89
|
+
*
|
|
90
|
+
* @param collection - The collection name
|
|
91
|
+
* @param where - Optional exact-match filters
|
|
92
|
+
* @returns Number of matching records
|
|
93
|
+
*/
|
|
94
|
+
countCollection(collection: string, where?: Record<string, unknown>): Promise<number>;
|
|
95
|
+
/**
|
|
96
|
+
* Export all data as a portable backup binary.
|
|
97
|
+
* Ships operations, version vector, and metadata in a self-describing format
|
|
98
|
+
* with SHA-256 checksum.
|
|
99
|
+
*/
|
|
100
|
+
exportBackup(): Promise<Uint8Array>;
|
|
101
|
+
/**
|
|
102
|
+
* Restore data from a portable backup binary.
|
|
103
|
+
* Operations are applied through applyRemoteOperation for safe merge.
|
|
104
|
+
*
|
|
105
|
+
* @param data - Backup binary
|
|
106
|
+
* @param merge - If true, merge with existing operations; if false, replace all
|
|
107
|
+
*/
|
|
108
|
+
importBackup(data: Uint8Array, merge?: boolean): Promise<{
|
|
109
|
+
operationsRestored: number;
|
|
110
|
+
success: boolean;
|
|
111
|
+
}>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Shared vocabulary for why the server refused an operation.
|
|
116
|
+
*
|
|
117
|
+
* The `retriable` flag answers one question for the caller: is it worth
|
|
118
|
+
* submitting this SAME operation again later?
|
|
119
|
+
*
|
|
120
|
+
* - `true` — the rejection is transient (for example a rate limit). The
|
|
121
|
+
* identical operation may succeed once the condition clears, so a
|
|
122
|
+
* client is right to back off and retry the same bytes.
|
|
123
|
+
* - `false` — the rejection is permanent for the operation as written (a
|
|
124
|
+
* constraint violation, a referential conflict, a malformed
|
|
125
|
+
* mutation, a scope violation). Resubmitting the same bytes will
|
|
126
|
+
* always fail; the caller must change the operation or give up.
|
|
127
|
+
*
|
|
128
|
+
* This is deliberately defined once, in one place, so the trusted server
|
|
129
|
+
* data-plane (route context / `server.kora`) and the untrusted client-ingestion
|
|
130
|
+
* path speak the same language. `retriable` (this spelling) matches the wire
|
|
131
|
+
* `ErrorMessage.retriable` field the sync protocol already ships, so a single
|
|
132
|
+
* flag flows from the pipeline out to connected clients unchanged.
|
|
133
|
+
*/
|
|
134
|
+
interface OperationRejection {
|
|
135
|
+
/** Stable, machine-readable reason code (for example `CONSTRAINT_VIOLATION`). */
|
|
136
|
+
code: string;
|
|
137
|
+
/** Human-readable explanation with enough context to debug without reproduction. */
|
|
138
|
+
message: string;
|
|
139
|
+
/** Whether resubmitting the identical operation may later succeed. */
|
|
140
|
+
retriable: boolean;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Reason codes whose underlying condition is transient, so the identical
|
|
144
|
+
* operation may succeed on a later attempt.
|
|
145
|
+
*
|
|
146
|
+
* Everything NOT listed here is treated as permanent. Permanent is the safe
|
|
147
|
+
* default: a client should never be told to keep hammering a genuinely invalid
|
|
148
|
+
* operation, so a new code is non-retriable until it is deliberately added here.
|
|
149
|
+
*
|
|
150
|
+
* `RATE_LIMIT` is the code the session emits when a client exceeds its
|
|
151
|
+
* per-minute operation budget (see `client-session.ts`).
|
|
152
|
+
*/
|
|
153
|
+
declare const RETRIABLE_REJECTION_CODES: ReadonlySet<string>;
|
|
154
|
+
/**
|
|
155
|
+
* Classify a reason code as retriable (transient) or not (permanent).
|
|
156
|
+
*
|
|
157
|
+
* @param code - A rejection reason code.
|
|
158
|
+
* @returns `true` when resubmitting the identical operation may later succeed.
|
|
159
|
+
*/
|
|
160
|
+
declare function isRetriableRejection(code: string): boolean;
|
|
161
|
+
|
|
162
|
+
interface ApplyServerOperationResult {
|
|
163
|
+
/** Result of applying the primary operation */
|
|
164
|
+
result: ApplyResult;
|
|
165
|
+
/** Primary op when applied, plus any server-generated side-effect operations */
|
|
166
|
+
appliedOperations: Operation[];
|
|
167
|
+
/** Rejection reason when the operation was not applied */
|
|
168
|
+
rejection?: OperationRejection;
|
|
169
|
+
}
|
|
170
|
+
|
|
8
171
|
/**
|
|
9
172
|
* Snapshot of server-side sync metrics at a point in time.
|
|
10
173
|
*/
|
|
@@ -125,106 +288,258 @@ declare function createSilentLogger(): Logger;
|
|
|
125
288
|
declare function createDefaultLogger(): Logger;
|
|
126
289
|
|
|
127
290
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
291
|
+
* Minimal interface for a ws.WebSocketServer instance.
|
|
292
|
+
* Allows dependency injection for testing without importing ws directly.
|
|
130
293
|
*/
|
|
131
|
-
interface
|
|
132
|
-
|
|
133
|
-
|
|
294
|
+
interface WsServerLike {
|
|
295
|
+
on(event: string, listener: (...args: unknown[]) => void): void;
|
|
296
|
+
close(callback?: (err?: Error) => void): void;
|
|
297
|
+
address(): {
|
|
298
|
+
port: number;
|
|
299
|
+
} | string | null;
|
|
134
300
|
}
|
|
135
301
|
/**
|
|
136
|
-
*
|
|
302
|
+
* Constructor type for creating a WebSocket server.
|
|
137
303
|
*/
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
/** Sort direction (default: 'asc') */
|
|
144
|
-
orderDirection?: 'asc' | 'desc';
|
|
145
|
-
/** Maximum number of records to return */
|
|
146
|
-
limit?: number;
|
|
147
|
-
/** Number of records to skip (for pagination) */
|
|
148
|
-
offset?: number;
|
|
149
|
-
/** Include soft-deleted records (default: false) */
|
|
150
|
-
includeDeleted?: boolean;
|
|
151
|
-
}
|
|
304
|
+
type WsServerConstructor = new (options: {
|
|
305
|
+
port?: number;
|
|
306
|
+
host?: string;
|
|
307
|
+
path?: string;
|
|
308
|
+
}) => WsServerLike;
|
|
152
309
|
/**
|
|
153
|
-
*
|
|
154
|
-
*
|
|
310
|
+
* Self-hosted sync server. Accepts WebSocket connections from clients,
|
|
311
|
+
* handles the sync protocol, stores operations, and relays changes
|
|
312
|
+
* between connected clients.
|
|
313
|
+
*
|
|
314
|
+
* Two modes of operation:
|
|
315
|
+
* 1. **Standalone**: Call `start()` with a port — creates its own WebSocket server.
|
|
316
|
+
* 2. **Attach**: Call `handleConnection(transport)` — attach to an existing HTTP server.
|
|
155
317
|
*/
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
318
|
+
declare class KoraSyncServer {
|
|
319
|
+
private readonly store;
|
|
320
|
+
private readonly auth;
|
|
321
|
+
private readonly serializer;
|
|
322
|
+
private readonly emitter;
|
|
323
|
+
private readonly maxConnections;
|
|
324
|
+
private readonly batchSize;
|
|
325
|
+
private readonly schemaVersion;
|
|
326
|
+
private readonly supportedSchemaVersions;
|
|
327
|
+
private readonly port;
|
|
328
|
+
private readonly host;
|
|
329
|
+
private readonly path;
|
|
330
|
+
private readonly logger;
|
|
331
|
+
private readonly metrics;
|
|
332
|
+
private readonly awarenessRelay;
|
|
333
|
+
private readonly yjsDocRelay;
|
|
334
|
+
private readonly blobChunkRelay;
|
|
335
|
+
private readonly persistBlobChunk;
|
|
336
|
+
private readonly maxOperationBytes;
|
|
337
|
+
private readonly maxOpsPerMinute;
|
|
338
|
+
private readonly validateOperation;
|
|
339
|
+
private readonly koraContext;
|
|
340
|
+
private readonly sessions;
|
|
341
|
+
private readonly httpClients;
|
|
342
|
+
private readonly httpSessionToClient;
|
|
343
|
+
private readonly serverVersion;
|
|
344
|
+
private wsServer;
|
|
345
|
+
private running;
|
|
346
|
+
constructor(config: KoraSyncServerConfig);
|
|
161
347
|
/**
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
|
|
348
|
+
* The trusted, scoped data-plane context (`apply` / `query` / `findById`).
|
|
349
|
+
* Shared with the production server so HTTP routes, the operation validator,
|
|
350
|
+
* and `server.kora` are all the same object over the same pipeline.
|
|
351
|
+
*/
|
|
352
|
+
getKoraContext(): ProductionHttpRouteContext;
|
|
353
|
+
/**
|
|
354
|
+
* Subscribe to session-level events for metrics collection and logging.
|
|
355
|
+
* Called when a new session is created.
|
|
356
|
+
*/
|
|
357
|
+
private attachSessionEvents;
|
|
358
|
+
/**
|
|
359
|
+
* Get the metrics collector for external access (e.g., HTTP endpoints).
|
|
360
|
+
*/
|
|
361
|
+
getMetricsCollector(): ServerMetricsCollector;
|
|
362
|
+
/**
|
|
363
|
+
* Get the logger for external access (e.g., event streaming).
|
|
364
|
+
*/
|
|
365
|
+
getLogger(): Logger;
|
|
366
|
+
/**
|
|
367
|
+
* Start the WebSocket server in standalone mode.
|
|
166
368
|
*
|
|
167
|
-
* @param
|
|
369
|
+
* @param wsServerImpl - Optional WebSocket server constructor for testing
|
|
168
370
|
*/
|
|
169
|
-
|
|
170
|
-
/** Schema used for materialized tables and server-side validation, if set. */
|
|
171
|
-
getSchema(): SchemaDefinition | null;
|
|
371
|
+
start(wsServerImpl?: WsServerConstructor): Promise<void>;
|
|
172
372
|
/**
|
|
173
|
-
*
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
373
|
+
* Stop the server. Closes all sessions and the WebSocket server.
|
|
374
|
+
*/
|
|
375
|
+
stop(): Promise<void>;
|
|
376
|
+
/**
|
|
377
|
+
* Handle one HTTP sync request for a long-polling client.
|
|
177
378
|
*
|
|
178
|
-
*
|
|
179
|
-
* @returns Array of records with their current state
|
|
379
|
+
* A stable `clientId` identifies the logical connection across requests.
|
|
180
380
|
*/
|
|
181
|
-
|
|
381
|
+
handleHttpRequest(request: HttpSyncRequest): Promise<HttpSyncResponse>;
|
|
182
382
|
/**
|
|
183
|
-
*
|
|
184
|
-
*
|
|
383
|
+
* Handle an incoming client connection (attach mode).
|
|
384
|
+
* Creates a new ClientSession for the transport.
|
|
185
385
|
*
|
|
186
|
-
* @param
|
|
187
|
-
* @
|
|
188
|
-
* @returns Array of matching records
|
|
386
|
+
* @param transport - The server transport for the new connection
|
|
387
|
+
* @returns The session ID
|
|
189
388
|
*/
|
|
190
|
-
|
|
389
|
+
handleConnection(transport: ServerTransport): string;
|
|
191
390
|
/**
|
|
192
|
-
*
|
|
193
|
-
|
|
391
|
+
* Get the current server status.
|
|
392
|
+
*/
|
|
393
|
+
getStatus(): Promise<ServerStatus>;
|
|
394
|
+
/**
|
|
395
|
+
* Apply a server-originated operation (for example one created by a custom
|
|
396
|
+
* HTTP route) through the same validated pipeline that incoming client
|
|
397
|
+
* operations use — Tier 2 constraints, referential integrity, and cascade
|
|
398
|
+
* side effects — then relay every applied operation to connected clients.
|
|
194
399
|
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
400
|
+
* Because the operation did not come from a client session, it is relayed to
|
|
401
|
+
* ALL sessions (there is no source session to exclude). Each session still
|
|
402
|
+
* applies its own per-scope visibility filter in `relayOperations`, so a
|
|
403
|
+
* client only receives the operation if it falls within that client's scope.
|
|
404
|
+
*
|
|
405
|
+
* @param op - A fully-formed, server-originated operation to apply
|
|
406
|
+
* @returns The apply result, including any server-generated side-effect ops
|
|
198
407
|
*/
|
|
199
|
-
|
|
408
|
+
applyLocalOperation(op: Operation): Promise<ApplyServerOperationResult>;
|
|
200
409
|
/**
|
|
201
|
-
*
|
|
202
|
-
* Requires schema to be set via setSchema().
|
|
410
|
+
* Collect every blob reference still reachable from live records on the server.
|
|
203
411
|
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
412
|
+
* This is the live set for garbage-collecting the server's central blob store:
|
|
413
|
+
* pass the result to `collectBlobGarbage(blobStore, liveRefs)` from
|
|
414
|
+
* `@korajs/store` to reclaim bytes no record references any more. Only
|
|
415
|
+
* collections that declare a `blob` field are scanned.
|
|
416
|
+
*
|
|
417
|
+
* @returns Every live blob reference across all collections
|
|
207
418
|
*/
|
|
208
|
-
|
|
419
|
+
getLiveBlobRefs(): Promise<BlobRef[]>;
|
|
209
420
|
/**
|
|
210
|
-
*
|
|
211
|
-
* Ships operations, version vector, and metadata in a self-describing format
|
|
212
|
-
* with SHA-256 checksum.
|
|
421
|
+
* Get the number of currently connected clients.
|
|
213
422
|
*/
|
|
214
|
-
|
|
423
|
+
getConnectionCount(): number;
|
|
424
|
+
private handleRelay;
|
|
425
|
+
private handleSessionClose;
|
|
426
|
+
private handleAwarenessRelay;
|
|
427
|
+
private handleYjsDocRelay;
|
|
428
|
+
private getOrCreateHttpClient;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* A mutation a custom HTTP route asks the server to apply on the data plane.
|
|
433
|
+
*
|
|
434
|
+
* `recordId` is optional for inserts (a UUID v7 is generated when omitted) and
|
|
435
|
+
* required for updates and deletes. For updates, `data` holds only the changed
|
|
436
|
+
* fields; the server reads the current record to capture the previous values so
|
|
437
|
+
* the change three-way merges correctly with concurrent client edits.
|
|
438
|
+
*/
|
|
439
|
+
interface RouteMutation {
|
|
440
|
+
collection: string;
|
|
441
|
+
type: 'insert' | 'update' | 'delete';
|
|
442
|
+
recordId?: string;
|
|
443
|
+
data?: Record<string, unknown> | null;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Scope the route derived from the request's authenticated actor. When present,
|
|
447
|
+
* it is enforced exactly like a sync session's scope: `apply()` rejects a
|
|
448
|
+
* mutation whose resulting record falls outside the scope, and `query()` /
|
|
449
|
+
* `findById()` only return records inside it. Omit it for genuinely public
|
|
450
|
+
* routes (for example a signup endpoint) where no per-actor isolation applies.
|
|
451
|
+
*/
|
|
452
|
+
interface RouteScopeOptions {
|
|
453
|
+
scope?: ScopeMap;
|
|
454
|
+
}
|
|
455
|
+
/** Result of {@link ProductionHttpRouteContext.apply}. */
|
|
456
|
+
type RouteApplyResult = {
|
|
457
|
+
ok: true;
|
|
458
|
+
operation: Operation;
|
|
459
|
+
record: MaterializedRecord | null;
|
|
460
|
+
} | {
|
|
461
|
+
ok: false;
|
|
462
|
+
code: string;
|
|
463
|
+
message: string;
|
|
464
|
+
retriable: boolean;
|
|
465
|
+
};
|
|
466
|
+
/**
|
|
467
|
+
* Scoped, validated data-plane access handed to custom HTTP route handlers.
|
|
468
|
+
*
|
|
469
|
+
* Every mutation goes through the same pipeline as sync (Tier 2 constraints,
|
|
470
|
+
* referential integrity, materialization, and fan-out to connected clients), so
|
|
471
|
+
* REST endpoints cannot accidentally bypass validation, constraints, or tenant
|
|
472
|
+
* isolation the way hand-rolled store writes can.
|
|
473
|
+
*/
|
|
474
|
+
interface ProductionHttpRouteContext {
|
|
215
475
|
/**
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
* @param data - Backup binary
|
|
220
|
-
* @param merge - If true, merge with existing operations; if false, replace all
|
|
476
|
+
* Apply a mutation through the validated server pipeline and relay the
|
|
477
|
+
* resulting operation(s) to connected clients.
|
|
221
478
|
*/
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
479
|
+
apply(mutation: RouteMutation, options?: RouteScopeOptions): Promise<RouteApplyResult>;
|
|
480
|
+
/** Read materialized records from a collection, optionally scoped. */
|
|
481
|
+
query(collection: string, options?: CollectionQueryOptions & RouteScopeOptions): Promise<MaterializedRecord[]>;
|
|
482
|
+
/** Read a single materialized record by id, optionally scoped. */
|
|
483
|
+
findById(collection: string, id: string, options?: RouteScopeOptions): Promise<MaterializedRecord | null>;
|
|
226
484
|
}
|
|
227
485
|
|
|
486
|
+
/**
|
|
487
|
+
* What the server hands an operation validator so it can adjudicate an untrusted
|
|
488
|
+
* client operation before that operation becomes authoritative.
|
|
489
|
+
*
|
|
490
|
+
* The validator is POLICY (yours); the framework owns the MECHANISM around it —
|
|
491
|
+
* running it at ingestion, and routing its decision deterministically.
|
|
492
|
+
*/
|
|
493
|
+
interface OperationValidationContext {
|
|
494
|
+
/**
|
|
495
|
+
* The authenticated actor for the submitting session, or `null` for an
|
|
496
|
+
* anonymous / unauthenticated connection. This is how a public submission
|
|
497
|
+
* endpoint tells "signed-in owner" from "anonymous respondent" apart.
|
|
498
|
+
*/
|
|
499
|
+
auth: AuthContext | null;
|
|
500
|
+
/**
|
|
501
|
+
* Trusted, scoped data-plane access — the same context available as
|
|
502
|
+
* `server.kora` / `request.kora`. Use `kora.query` / `kora.findById` to read
|
|
503
|
+
* current authoritative state while deciding, and `kora.apply` to AUTHOR a
|
|
504
|
+
* derived server operation (for example promoting a validated anonymous
|
|
505
|
+
* submission into an owner-visible collection). Authoring a new operation is
|
|
506
|
+
* always preferred over mutating the incoming one, which must stay immutable
|
|
507
|
+
* so content-addressing and convergence hold.
|
|
508
|
+
*/
|
|
509
|
+
kora: ProductionHttpRouteContext;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* The verdict a validator returns for one incoming operation.
|
|
513
|
+
*
|
|
514
|
+
* - `accept` — let the operation materialize on the server as-is and relay to
|
|
515
|
+
* connected clients, exactly as an untrusted op would flow without a validator.
|
|
516
|
+
* - `reject` — refuse it. The operation never enters the authoritative log, so no
|
|
517
|
+
* other replica ever sees it; a structured rejection travels back to the
|
|
518
|
+
* submitter tied to the operation id, and the submitter keeps the op in a
|
|
519
|
+
* durable rejected store (recoverable and explainable) rather than losing it or
|
|
520
|
+
* retrying forever. `retriable` defaults from the shared taxonomy for the code.
|
|
521
|
+
* - `ignore` — the server takes responsibility for the operation out of band (for
|
|
522
|
+
* example the validator already authored a derived op via `context.kora`) and
|
|
523
|
+
* does not want the raw incoming op materialized. No rejection is sent, so the
|
|
524
|
+
* submitter treats it as handled and drops it from its pending queue.
|
|
525
|
+
*/
|
|
526
|
+
type OperationDecision = {
|
|
527
|
+
action: 'accept';
|
|
528
|
+
} | {
|
|
529
|
+
action: 'reject';
|
|
530
|
+
code: string;
|
|
531
|
+
message: string;
|
|
532
|
+
retriable?: boolean;
|
|
533
|
+
} | {
|
|
534
|
+
action: 'ignore';
|
|
535
|
+
};
|
|
536
|
+
/**
|
|
537
|
+
* An app-provided function that adjudicates an untrusted client operation at sync
|
|
538
|
+
* ingestion, after HLC ordering and the built-in guards (timestamp, rate, size),
|
|
539
|
+
* and before the operation is materialized into authoritative state.
|
|
540
|
+
*/
|
|
541
|
+
type OperationValidator = (operation: Operation, context: OperationValidationContext) => Promise<OperationDecision> | OperationDecision;
|
|
542
|
+
|
|
228
543
|
/**
|
|
229
544
|
* Authenticated client context. Returned by an AuthProvider after validation.
|
|
230
545
|
*/
|
|
@@ -286,6 +601,48 @@ interface KoraSyncServerConfig {
|
|
|
286
601
|
metricsCollector?: ServerMetricsCollector;
|
|
287
602
|
/** Enable built-in dashboard and metrics endpoints. Defaults to true. */
|
|
288
603
|
enableDashboard?: boolean;
|
|
604
|
+
/**
|
|
605
|
+
* Resolve a blob chunk by content hash from server-side storage. Optional.
|
|
606
|
+
* When provided, the server answers blob chunk requests directly from its own
|
|
607
|
+
* store; when absent, the server relays chunk requests among connected peers
|
|
608
|
+
* without storing any blob bytes itself.
|
|
609
|
+
*/
|
|
610
|
+
resolveBlobChunk?: (hash: string) => Promise<Uint8Array | null>;
|
|
611
|
+
/**
|
|
612
|
+
* Persist a client-uploaded blob chunk (or manifest) centrally, keyed by its
|
|
613
|
+
* content hash. Optional. When provided, the server advertises central blob
|
|
614
|
+
* storage at handshake, clients upload the bytes behind their `blob` fields,
|
|
615
|
+
* and those bytes remain available to other devices after the authoring device
|
|
616
|
+
* goes offline. Pair with `resolveBlobChunk` (both backed by the same store)
|
|
617
|
+
* so uploaded blobs can then be served. `toServerBlobCallbacks` in
|
|
618
|
+
* `@korajs/store` derives both from a `ContentAddressedBlobStore`.
|
|
619
|
+
*/
|
|
620
|
+
persistBlobChunk?: (hash: string, bytes: Uint8Array) => Promise<void> | void;
|
|
621
|
+
/**
|
|
622
|
+
* Maximum serialized byte size of a single client operation accepted at sync
|
|
623
|
+
* ingest. Operations larger than this are rejected before materialization.
|
|
624
|
+
* Defaults to 256 KiB. Set once here to enforce one payload cap across every
|
|
625
|
+
* connected client instead of configuring each session.
|
|
626
|
+
*/
|
|
627
|
+
maxOperationBytes?: number;
|
|
628
|
+
/**
|
|
629
|
+
* Maximum operations accepted per connected client per minute (sliding window).
|
|
630
|
+
* Operations beyond the limit are rejected until the window resets. Defaults to
|
|
631
|
+
* 600. Set once here to enforce one rate cap across every connected client.
|
|
632
|
+
*/
|
|
633
|
+
maxOpsPerMinute?: number;
|
|
634
|
+
/**
|
|
635
|
+
* Adjudicate untrusted client operations before they become authoritative.
|
|
636
|
+
*
|
|
637
|
+
* Runs at sync ingestion for every incoming client operation, after HLC
|
|
638
|
+
* ordering and the built-in guards, and before materialization. Return
|
|
639
|
+
* `accept` to let it through, `reject` to refuse it (a structured rejection
|
|
640
|
+
* travels back to the submitter and the op never enters the authoritative
|
|
641
|
+
* log), or `ignore` when the server has handled it out of band. This is what
|
|
642
|
+
* lets Kora serve public / multi-tenant apps where the client is not trusted.
|
|
643
|
+
* Omit it and every operation is accepted, as before.
|
|
644
|
+
*/
|
|
645
|
+
validateOperation?: OperationValidator;
|
|
289
646
|
}
|
|
290
647
|
/**
|
|
291
648
|
* Request envelope for the server-side HTTP sync endpoint.
|
|
@@ -439,6 +796,20 @@ type AwarenessRelayCallback = (sourceSessionId: string, message: AwarenessUpdate
|
|
|
439
796
|
* Callback invoked when a session receives a Yjs doc channel update to relay.
|
|
440
797
|
*/
|
|
441
798
|
type YjsDocRelayCallback = (sourceSessionId: string, message: YjsDocUpdateMessage) => void;
|
|
799
|
+
/**
|
|
800
|
+
* Callback invoked when a session receives a blob chunk request to route.
|
|
801
|
+
*/
|
|
802
|
+
type BlobChunkRequestCallback = (sourceSessionId: string, message: BlobChunkRequestMessage) => void;
|
|
803
|
+
/**
|
|
804
|
+
* Callback invoked when a session receives a blob chunk response to route back.
|
|
805
|
+
*/
|
|
806
|
+
type BlobChunkResponseCallback = (sourceSessionId: string, message: BlobChunkResponseMessage) => void;
|
|
807
|
+
/**
|
|
808
|
+
* Persist a blob chunk (or manifest) uploaded by a client, keyed by its content
|
|
809
|
+
* hash. Provided by the server operator; enables central blob storage so bytes
|
|
810
|
+
* survive the authoring device going offline.
|
|
811
|
+
*/
|
|
812
|
+
type PersistBlobChunk = (hash: string, bytes: Uint8Array) => Promise<void> | void;
|
|
442
813
|
/**
|
|
443
814
|
* Options for creating a ClientSession.
|
|
444
815
|
*/
|
|
@@ -470,12 +841,26 @@ interface ClientSessionOptions {
|
|
|
470
841
|
onAwarenessUpdate?: AwarenessRelayCallback;
|
|
471
842
|
/** Called when this session receives a Yjs doc channel update to broadcast */
|
|
472
843
|
onYjsDocUpdate?: YjsDocRelayCallback;
|
|
844
|
+
/** Called when this session receives a blob chunk request to route */
|
|
845
|
+
onBlobChunkRequest?: BlobChunkRequestCallback;
|
|
846
|
+
/** Called when this session receives a blob chunk response to route back */
|
|
847
|
+
onBlobChunkResponse?: BlobChunkResponseCallback;
|
|
848
|
+
/** Persist a client-uploaded blob chunk centrally (keyed by content hash) */
|
|
849
|
+
persistBlobChunk?: PersistBlobChunk;
|
|
473
850
|
/** Called when this session closes */
|
|
474
851
|
onClose?: (sessionId: string) => void;
|
|
475
852
|
/** Maximum serialized operation size in bytes. Defaults to 256 KiB. */
|
|
476
853
|
maxOperationBytes?: number;
|
|
477
854
|
/** Maximum operations accepted per minute for this session. Defaults to 600. */
|
|
478
855
|
maxOpsPerMinute?: number;
|
|
856
|
+
/**
|
|
857
|
+
* Adjudicate untrusted client operations before materialization. When present,
|
|
858
|
+
* each incoming operation is passed to this validator; a `reject` decision
|
|
859
|
+
* sends an operation-rejected message and skips materialization.
|
|
860
|
+
*/
|
|
861
|
+
validateOperation?: OperationValidator;
|
|
862
|
+
/** Trusted data-plane context handed to the validator (read state, author derived ops). */
|
|
863
|
+
koraContext?: ProductionHttpRouteContext;
|
|
479
864
|
}
|
|
480
865
|
/**
|
|
481
866
|
* Handles the sync protocol for a single connected client.
|
|
@@ -509,10 +894,15 @@ declare class ClientSession {
|
|
|
509
894
|
private readonly onRelay;
|
|
510
895
|
private readonly onAwarenessUpdate;
|
|
511
896
|
private readonly onYjsDocUpdate;
|
|
897
|
+
private readonly onBlobChunkRequest;
|
|
898
|
+
private readonly onBlobChunkResponse;
|
|
899
|
+
private readonly persistBlobChunk;
|
|
512
900
|
private readonly onClose;
|
|
513
901
|
private readonly maxOperationBytes;
|
|
514
902
|
private readonly maxOpsPerMinute;
|
|
515
903
|
private readonly rateLimiter;
|
|
904
|
+
private readonly validateOperation;
|
|
905
|
+
private readonly koraContext;
|
|
516
906
|
constructor(options: ClientSessionOptions);
|
|
517
907
|
/**
|
|
518
908
|
* Start handling messages from the client transport.
|
|
@@ -542,6 +932,12 @@ declare class ClientSession {
|
|
|
542
932
|
private sendToClient;
|
|
543
933
|
private enqueueMessage;
|
|
544
934
|
private handleMessageAsync;
|
|
935
|
+
/**
|
|
936
|
+
* Persist a client-uploaded blob chunk (or manifest) centrally. The bytes are
|
|
937
|
+
* verified to hash to the declared hash before storing, so a corrupt or
|
|
938
|
+
* mislabeled upload is rejected rather than served later as trusted content.
|
|
939
|
+
*/
|
|
940
|
+
private handleBlobChunkPush;
|
|
545
941
|
private handleMessageFailure;
|
|
546
942
|
private handleHandshake;
|
|
547
943
|
private handleOperationBatch;
|
|
@@ -550,111 +946,15 @@ declare class ClientSession {
|
|
|
550
946
|
private handleAwarenessUpdate;
|
|
551
947
|
private handleYjsDocUpdate;
|
|
552
948
|
private sendError;
|
|
553
|
-
private setSerializerWireFormat;
|
|
554
|
-
private handleTransportClose;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
/**
|
|
558
|
-
* Minimal interface for a ws.WebSocketServer instance.
|
|
559
|
-
* Allows dependency injection for testing without importing ws directly.
|
|
560
|
-
*/
|
|
561
|
-
interface WsServerLike {
|
|
562
|
-
on(event: string, listener: (...args: unknown[]) => void): void;
|
|
563
|
-
close(callback?: (err?: Error) => void): void;
|
|
564
|
-
address(): {
|
|
565
|
-
port: number;
|
|
566
|
-
} | string | null;
|
|
567
|
-
}
|
|
568
|
-
/**
|
|
569
|
-
* Constructor type for creating a WebSocket server.
|
|
570
|
-
*/
|
|
571
|
-
type WsServerConstructor = new (options: {
|
|
572
|
-
port?: number;
|
|
573
|
-
host?: string;
|
|
574
|
-
path?: string;
|
|
575
|
-
}) => WsServerLike;
|
|
576
|
-
/**
|
|
577
|
-
* Self-hosted sync server. Accepts WebSocket connections from clients,
|
|
578
|
-
* handles the sync protocol, stores operations, and relays changes
|
|
579
|
-
* between connected clients.
|
|
580
|
-
*
|
|
581
|
-
* Two modes of operation:
|
|
582
|
-
* 1. **Standalone**: Call `start()` with a port — creates its own WebSocket server.
|
|
583
|
-
* 2. **Attach**: Call `handleConnection(transport)` — attach to an existing HTTP server.
|
|
584
|
-
*/
|
|
585
|
-
declare class KoraSyncServer {
|
|
586
|
-
private readonly store;
|
|
587
|
-
private readonly auth;
|
|
588
|
-
private readonly serializer;
|
|
589
|
-
private readonly emitter;
|
|
590
|
-
private readonly maxConnections;
|
|
591
|
-
private readonly batchSize;
|
|
592
|
-
private readonly schemaVersion;
|
|
593
|
-
private readonly supportedSchemaVersions;
|
|
594
|
-
private readonly port;
|
|
595
|
-
private readonly host;
|
|
596
|
-
private readonly path;
|
|
597
|
-
private readonly logger;
|
|
598
|
-
private readonly metrics;
|
|
599
|
-
private readonly awarenessRelay;
|
|
600
|
-
private readonly yjsDocRelay;
|
|
601
|
-
private readonly sessions;
|
|
602
|
-
private readonly httpClients;
|
|
603
|
-
private readonly httpSessionToClient;
|
|
604
|
-
private readonly serverVersion;
|
|
605
|
-
private wsServer;
|
|
606
|
-
private running;
|
|
607
|
-
constructor(config: KoraSyncServerConfig);
|
|
608
949
|
/**
|
|
609
|
-
*
|
|
610
|
-
*
|
|
950
|
+
* Reject one specific client operation, tied to its id, so the submitter can
|
|
951
|
+
* divert it out of its pending queue into a durable rejected store rather than
|
|
952
|
+
* losing it or retrying forever. The op is NOT materialized, so no other
|
|
953
|
+
* replica ever sees it.
|
|
611
954
|
*/
|
|
612
|
-
private
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
*/
|
|
616
|
-
getMetricsCollector(): ServerMetricsCollector;
|
|
617
|
-
/**
|
|
618
|
-
* Get the logger for external access (e.g., event streaming).
|
|
619
|
-
*/
|
|
620
|
-
getLogger(): Logger;
|
|
621
|
-
/**
|
|
622
|
-
* Start the WebSocket server in standalone mode.
|
|
623
|
-
*
|
|
624
|
-
* @param wsServerImpl - Optional WebSocket server constructor for testing
|
|
625
|
-
*/
|
|
626
|
-
start(wsServerImpl?: WsServerConstructor): Promise<void>;
|
|
627
|
-
/**
|
|
628
|
-
* Stop the server. Closes all sessions and the WebSocket server.
|
|
629
|
-
*/
|
|
630
|
-
stop(): Promise<void>;
|
|
631
|
-
/**
|
|
632
|
-
* Handle one HTTP sync request for a long-polling client.
|
|
633
|
-
*
|
|
634
|
-
* A stable `clientId` identifies the logical connection across requests.
|
|
635
|
-
*/
|
|
636
|
-
handleHttpRequest(request: HttpSyncRequest): Promise<HttpSyncResponse>;
|
|
637
|
-
/**
|
|
638
|
-
* Handle an incoming client connection (attach mode).
|
|
639
|
-
* Creates a new ClientSession for the transport.
|
|
640
|
-
*
|
|
641
|
-
* @param transport - The server transport for the new connection
|
|
642
|
-
* @returns The session ID
|
|
643
|
-
*/
|
|
644
|
-
handleConnection(transport: ServerTransport): string;
|
|
645
|
-
/**
|
|
646
|
-
* Get the current server status.
|
|
647
|
-
*/
|
|
648
|
-
getStatus(): Promise<ServerStatus>;
|
|
649
|
-
/**
|
|
650
|
-
* Get the number of currently connected clients.
|
|
651
|
-
*/
|
|
652
|
-
getConnectionCount(): number;
|
|
653
|
-
private handleRelay;
|
|
654
|
-
private handleSessionClose;
|
|
655
|
-
private handleAwarenessRelay;
|
|
656
|
-
private handleYjsDocRelay;
|
|
657
|
-
private getOrCreateHttpClient;
|
|
955
|
+
private sendOperationRejected;
|
|
956
|
+
private setSerializerWireFormat;
|
|
957
|
+
private handleTransportClose;
|
|
658
958
|
}
|
|
659
959
|
|
|
660
960
|
/**
|
|
@@ -704,6 +1004,13 @@ interface ProductionHttpRouteRequest {
|
|
|
704
1004
|
headers?: Record<string, string | string[] | undefined>;
|
|
705
1005
|
query?: Record<string, string | string[] | undefined>;
|
|
706
1006
|
ip?: string;
|
|
1007
|
+
/**
|
|
1008
|
+
* Scoped, validated data-plane access. Use `kora.apply()` to mutate through
|
|
1009
|
+
* the same pipeline as sync (constraints, referential integrity, fan-out) and
|
|
1010
|
+
* `kora.query()` / `kora.findById()` to read materialized state, instead of
|
|
1011
|
+
* writing to the store directly and bypassing those guarantees.
|
|
1012
|
+
*/
|
|
1013
|
+
kora: ProductionHttpRouteContext;
|
|
707
1014
|
}
|
|
708
1015
|
interface ProductionHttpRouteResponse {
|
|
709
1016
|
status: number;
|
|
@@ -723,6 +1030,29 @@ interface ProductionServer {
|
|
|
723
1030
|
start(): Promise<string>;
|
|
724
1031
|
/** Stop the server gracefully. */
|
|
725
1032
|
stop(): Promise<void>;
|
|
1033
|
+
/**
|
|
1034
|
+
* Trusted, scoped data-plane access for server-side callers that have no HTTP
|
|
1035
|
+
* request — background jobs, scheduled tasks, seeding scripts.
|
|
1036
|
+
*
|
|
1037
|
+
* `kora.apply()` runs the mutation through the exact same validated pipeline as
|
|
1038
|
+
* sync (Tier 2 constraints, referential integrity, materialization, and fan-out
|
|
1039
|
+
* to connected clients), and `kora.query()` / `kora.findById()` read
|
|
1040
|
+
* materialized state. This is the same context handed to custom HTTP routes as
|
|
1041
|
+
* `request.kora`, so a job and a request share one code path and one set of
|
|
1042
|
+
* guarantees instead of the job writing to the store directly and silently
|
|
1043
|
+
* bypassing validation.
|
|
1044
|
+
*/
|
|
1045
|
+
kora: ProductionHttpRouteContext;
|
|
1046
|
+
/**
|
|
1047
|
+
* Every blob reference still reachable from a live record across all
|
|
1048
|
+
* collections that declare a `blob` field. This is the live set for
|
|
1049
|
+
* garbage-collecting the server's central blob store: pass it to
|
|
1050
|
+
* `collectBlobGarbage(blobStore, refs)` from `@korajs/store` on a schedule to
|
|
1051
|
+
* reclaim bytes no record points at any more.
|
|
1052
|
+
*
|
|
1053
|
+
* @returns Every live blob reference the server can currently see.
|
|
1054
|
+
*/
|
|
1055
|
+
getLiveBlobRefs(): Promise<BlobRef[]>;
|
|
726
1056
|
}
|
|
727
1057
|
/**
|
|
728
1058
|
* Creates a production server that serves both static files and WebSocket sync
|
|
@@ -1232,4 +1562,4 @@ declare class AwarenessRelay {
|
|
|
1232
1562
|
*/
|
|
1233
1563
|
declare function createKoraServer(config: KoraSyncServerConfig): KoraSyncServer;
|
|
1234
1564
|
|
|
1235
|
-
export { type AuthContext, type AuthProvider, AwarenessRelay, type AwarenessRelayCallback, type ClientMetrics, ClientSession, type ClientSessionOptions, type CollectionQueryOptions, type HttpPollResponse, HttpServerTransport, type HttpSyncRequest, type HttpSyncResponse, KoraAuthProvider, type KoraAuthProviderOptions, KoraSyncServer, type KoraSyncServerConfig, type LogEntry, type LogLevel, type Logger, type MaterializedRecord, MemoryServerStore, MixedAuthProvider, type MixedAuthProviderOptions, NoAuthProvider, PostgresServerStore, type ProductionHttpRoute, type ProductionHttpRouteRequest, type ProductionHttpRouteResponse, type ProductionServer, type ProductionServerConfig, type RelayCallback, ServerCloseHandler, ServerErrorHandler, ServerMessageHandler, ServerMetricsCollector, type ServerMetricsSnapshot, type ServerStatus, type ServerStore, ServerTransport, type SessionState, SqliteServerStore, TokenAuthProvider, type TokenAuthProviderOptions, type WsServerConstructor, type WsServerLike, WsServerTransport, type WsServerTransportOptions, type WsWebSocket, createDefaultLogger, createJsonLogger, createKoraServer, createPostgresServerStore, createPrettyLogger, createProductionServer, createSilentLogger, createSqliteServerStore };
|
|
1565
|
+
export { type AuthContext, type AuthProvider, AwarenessRelay, type AwarenessRelayCallback, type ClientMetrics, ClientSession, type ClientSessionOptions, type CollectionQueryOptions, type HttpPollResponse, HttpServerTransport, type HttpSyncRequest, type HttpSyncResponse, KoraAuthProvider, type KoraAuthProviderOptions, KoraSyncServer, type KoraSyncServerConfig, type LogEntry, type LogLevel, type Logger, type MaterializedRecord, MemoryServerStore, MixedAuthProvider, type MixedAuthProviderOptions, NoAuthProvider, type OperationDecision, type OperationRejection, type OperationValidationContext, type OperationValidator, PostgresServerStore, type ProductionHttpRoute, type ProductionHttpRouteContext, type ProductionHttpRouteRequest, type ProductionHttpRouteResponse, type ProductionServer, type ProductionServerConfig, RETRIABLE_REJECTION_CODES, type RelayCallback, type RouteApplyResult, type RouteMutation, type RouteScopeOptions, ServerCloseHandler, ServerErrorHandler, ServerMessageHandler, ServerMetricsCollector, type ServerMetricsSnapshot, type ServerStatus, type ServerStore, ServerTransport, type SessionState, SqliteServerStore, TokenAuthProvider, type TokenAuthProviderOptions, type WsServerConstructor, type WsServerLike, WsServerTransport, type WsServerTransportOptions, type WsWebSocket, createDefaultLogger, createJsonLogger, createKoraServer, createPostgresServerStore, createPrettyLogger, createProductionServer, createSilentLogger, createSqliteServerStore, isRetriableRejection };
|