@korajs/server 1.0.0-beta.11 → 1.0.0-beta.13
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 +53 -78
- package/dist/chunk-IRRXJ7YN.js +188 -0
- package/dist/chunk-IRRXJ7YN.js.map +1 -0
- package/dist/index.cjs +10142 -1178
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2713 -141
- package/dist/index.d.ts +2713 -141
- package/dist/index.js +9837 -1153
- package/dist/index.js.map +1 -1
- package/dist/internal.d.cts +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/server-backup-WBIYHWYQ.js +249 -0
- package/dist/server-backup-WBIYHWYQ.js.map +1 -0
- package/dist/{server-transport-CU1BLWgr.d.cts → server-transport-BGQkRCI4.d.cts} +7 -0
- package/dist/{server-transport-CU1BLWgr.d.ts → server-transport-BGQkRCI4.d.ts} +7 -0
- package/package.json +4 -4
- package/dist/server-backup-MOICK6MI.js +0 -116
- package/dist/server-backup-MOICK6MI.js.map +0 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { Operation, HybridLogicalClock, SchemaDefinition, BlobRef, KoraEventEmitter,
|
|
1
|
+
import { KoraError, Operation, VersionVector, HLCTimestamp, RecordFieldVersions, FoldState, HybridLogicalClock, SchemaDefinition, OperationTransform, BlobRef, KoraEventEmitter, TimeSource, ScopeMap as ScopeMap$1 } from '@korajs/core';
|
|
2
|
+
export { SCOPE_CLAIMS_KEY, claimScopes } from '@korajs/core';
|
|
2
3
|
import * as _korajs_sync from '@korajs/sync';
|
|
3
|
-
import { SyncStore, ApplyResult, MessageSerializer, SyncMessage, AwarenessUpdateMessage, YjsDocUpdateMessage, BlobChunkRequestMessage, BlobChunkResponseMessage } from '@korajs/sync';
|
|
4
|
-
import { S as ServerTransport, a as ServerMessageHandler, b as ServerCloseHandler, c as ServerErrorHandler } from './server-transport-
|
|
4
|
+
import { SyncStore, ApplyResult, MessageSerializer, SyncMessage, EncryptionKeyRequestMessage, EncryptionKeyPutMessage, EncryptionKeyResponseMessage, WrappedKeyRecord, AwarenessUpdateMessage, YjsDocUpdateMessage, BlobChunkRequestMessage, BlobChunkResponseMessage } from '@korajs/sync';
|
|
5
|
+
import { S as ServerTransport, a as ServerMessageHandler, b as ServerCloseHandler, c as ServerErrorHandler } from './server-transport-BGQkRCI4.cjs';
|
|
6
|
+
import { SideEffectOp } from '@korajs/merge';
|
|
5
7
|
import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
|
|
6
8
|
import { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3';
|
|
7
9
|
|
|
@@ -36,7 +38,15 @@ type RoutePredicate = Record<string, RoutePredicateOperators>;
|
|
|
36
38
|
*/
|
|
37
39
|
type ScopeMap = Record<string, Record<string, unknown>>;
|
|
38
40
|
declare const DEFAULT_MAX_SCOPE_PREDICATE_VALUES = 100;
|
|
39
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Canonicalize bounded `$in` predicates so equivalent authorization has one signature.
|
|
43
|
+
*
|
|
44
|
+
* Fails closed: an `undefined`/`null` predicate value (also inside `$in`) would match
|
|
45
|
+
* every record lacking the field, so it is refused (RT-8).
|
|
46
|
+
*
|
|
47
|
+
* @throws {InvalidScopePredicateError} On an undefined/null predicate value
|
|
48
|
+
* @throws {ScopePredicateLimitError} On a malformed or oversized `$in`
|
|
49
|
+
*/
|
|
40
50
|
declare function normalizeScopeMap(scopes: ScopeMap, maxValues?: number): ScopeMap;
|
|
41
51
|
/**
|
|
42
52
|
* Returns true if an operation is visible to a session based on its scopes.
|
|
@@ -47,7 +57,91 @@ declare function normalizeScopeMap(scopes: ScopeMap, maxValues?: number): ScopeM
|
|
|
47
57
|
* - All scoped field/value pairs must match the operation snapshot
|
|
48
58
|
*/
|
|
49
59
|
declare function operationMatchesScopes(op: Operation, scopes: ScopeMap | undefined, fullRecord?: Record<string, unknown> | null): boolean;
|
|
60
|
+
/** Why {@link authorizeUplinkWrite} refused a write. */
|
|
61
|
+
type UplinkAuthorizationCode = 'SCOPE_VIOLATION' | 'INVALID_OPERATION';
|
|
62
|
+
/** Result of {@link authorizeUplinkWrite}. */
|
|
63
|
+
type UplinkAuthorizationResult = {
|
|
64
|
+
allowed: true;
|
|
65
|
+
} | {
|
|
66
|
+
allowed: false;
|
|
67
|
+
code: UplinkAuthorizationCode;
|
|
68
|
+
message: string;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Thrown by a server store when the uplink authorization re-check, evaluated inside
|
|
72
|
+
* the store's apply critical section against the row as it is at commit time,
|
|
73
|
+
* refuses an operation. Nothing was written.
|
|
74
|
+
*/
|
|
75
|
+
declare class UplinkAuthorizationError extends KoraError {
|
|
76
|
+
readonly rejectionCode: UplinkAuthorizationCode;
|
|
77
|
+
constructor(rejectionCode: UplinkAuthorizationCode, message: string, context?: Record<string, unknown>);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Decides whether an untrusted writer (a sync session or a scoped route) may apply
|
|
81
|
+
* `op`, given the record as it is currently stored and the writer's scope.
|
|
82
|
+
*
|
|
83
|
+
* This is the single authorization rule for every untrusted write path (sync
|
|
84
|
+
* uploads, `request.kora` routes, the conditional store path and the Yjs doc
|
|
85
|
+
* channel). It never reads `op.previousData` for scope decisions, because it is
|
|
86
|
+
* attacker-controlled, and it never lets op fields name a different record:
|
|
87
|
+
*
|
|
88
|
+
* 1. `op.data` / `op.previousData` must not carry an `id` other than `op.recordId`
|
|
89
|
+
* (`INVALID_OPERATION`), whether or not scopes are configured.
|
|
90
|
+
* 2. Without scopes every (well-formed) write is allowed.
|
|
91
|
+
* 3. If a stored row exists (including a soft-deleted one) it must be in scope,
|
|
92
|
+
* judged on the stored values only, for inserts, updates and deletes alike. This
|
|
93
|
+
* stops editing, deleting or overwriting (same-id insert) another tenant's record.
|
|
94
|
+
* 4. The post-image must be in scope: `{...stored, ...op.data}` for updates,
|
|
95
|
+
* `op.data` for inserts and the stored row for deletes. This stops moving a
|
|
96
|
+
* record out of scope (ownership transfer is a trusted server-route operation).
|
|
97
|
+
*
|
|
98
|
+
* In both images `id` is `op.recordId`, assigned last.
|
|
99
|
+
*
|
|
100
|
+
* @param op - The operation the writer wants to apply
|
|
101
|
+
* @param storedRow - The record as stored now (including soft-deleted), or null
|
|
102
|
+
* @param scopes - The writer's uplink scope map, or undefined for no restriction
|
|
103
|
+
* @returns Whether the write is allowed, and why not when it is refused
|
|
104
|
+
*/
|
|
105
|
+
declare function authorizeUplinkWrite(op: Operation, storedRow: Record<string, unknown> | null | undefined, scopes: ScopeMap | undefined): UplinkAuthorizationResult;
|
|
50
106
|
|
|
107
|
+
/** A stored legacy pair, reported to {@link ApplyRemoteOptions.onLegacySequencePair}. */
|
|
108
|
+
interface LegacySequencePair {
|
|
109
|
+
operationId: string;
|
|
110
|
+
nodeId: string;
|
|
111
|
+
sequenceNumber: number;
|
|
112
|
+
/** The operations that already held the sequence. */
|
|
113
|
+
holderIds: string[];
|
|
114
|
+
/** True when accepted because the writer is a legacy client (not a pre-epoch holder). */
|
|
115
|
+
legacyWriter: boolean;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* How the server resolved an uploaded operation it did NOT store under the submitted
|
|
119
|
+
* (node, sequence) (RT-43, RT-47):
|
|
120
|
+
* - `'ignored'`: the validator answered `ignore` (handled out of band, nothing stored);
|
|
121
|
+
* - `'refused'`: a terminal (non-retriable) rejection; the same id is answered with the
|
|
122
|
+
* original rejection forever after;
|
|
123
|
+
* - `'stored-elsewhere'`: the id is stored under another sequence of the same node (a
|
|
124
|
+
* client sequence repair renumbered it), acknowledged as a duplicate.
|
|
125
|
+
* A retriable rejection, and a `SEQUENCE_CONFLICT` (the client renumbers and resubmits
|
|
126
|
+
* under the same id), are never resolutions.
|
|
127
|
+
*/
|
|
128
|
+
type OperationResolutionOutcome = 'ignored' | 'refused' | 'stored-elsewhere';
|
|
129
|
+
/** A durable record of how an uploaded operation was resolved without being stored. */
|
|
130
|
+
interface OperationResolution {
|
|
131
|
+
operationId: string;
|
|
132
|
+
nodeId: string;
|
|
133
|
+
sequenceNumber: number;
|
|
134
|
+
outcome: OperationResolutionOutcome;
|
|
135
|
+
/** Rejection code, for `'refused'`. */
|
|
136
|
+
code: string | null;
|
|
137
|
+
/** Rejection message, for `'refused'`. */
|
|
138
|
+
message: string | null;
|
|
139
|
+
}
|
|
140
|
+
/** Where a stored operation sits in its node's sequence space. */
|
|
141
|
+
interface StoredOperationKey {
|
|
142
|
+
nodeId: string;
|
|
143
|
+
sequenceNumber: number;
|
|
144
|
+
}
|
|
51
145
|
/**
|
|
52
146
|
* A materialized record reconstructed from the operation log
|
|
53
147
|
* or read from a materialized collection table.
|
|
@@ -100,6 +194,12 @@ interface ConditionalApplyContext {
|
|
|
100
194
|
* it into operation creation) so they sort strictly after every prior write.
|
|
101
195
|
*/
|
|
102
196
|
clock: HybridLogicalClock;
|
|
197
|
+
/**
|
|
198
|
+
* Read a record as stored (including a soft-deleted one) inside the store's
|
|
199
|
+
* conditional-apply transaction, for authorizing the built operations against the
|
|
200
|
+
* same state they commit over. Optional; callers fall back to a plain read.
|
|
201
|
+
*/
|
|
202
|
+
readStoredRow?: (collection: string, id: string) => Promise<MaterializedRecord | null>;
|
|
103
203
|
}
|
|
104
204
|
/** Result of {@link ServerStore.applyConditional}. */
|
|
105
205
|
interface ConditionalApplyResult {
|
|
@@ -119,6 +219,30 @@ interface ConditionalApplyResult {
|
|
|
119
219
|
interface DeliveredOperation {
|
|
120
220
|
operation: Operation;
|
|
121
221
|
deliverySequence: number;
|
|
222
|
+
/**
|
|
223
|
+
* The record's scope-relevant values around this operation, captured from the
|
|
224
|
+
* server's own rows when it was applied (RT-14, RT-15). Absent (or null) for
|
|
225
|
+
* operations stored before snapshots existed that could not be backfilled.
|
|
226
|
+
*/
|
|
227
|
+
scopeSnapshot?: OperationScopeSnapshot | null;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Scope-relevant field values of a record, captured by the store from its own rows
|
|
231
|
+
* when an operation is applied (never from the writer's `previousData`). Download
|
|
232
|
+
* visibility of a historical operation is judged on `post`, so a later ownership
|
|
233
|
+
* transfer does not disclose the record's earlier history to the new owner (RT-14),
|
|
234
|
+
* and a scope exit is judged from `pre` to `post` (RT-15).
|
|
235
|
+
*
|
|
236
|
+
* Only scalar fields (string, number, boolean, enum, timestamp) and `id` are kept:
|
|
237
|
+
* scope predicates compare with `Object.is`, so other kinds can never match. A
|
|
238
|
+
* string longer than {@link MAX_SCOPE_SNAPSHOT_STRING_LENGTH} is left out (fails
|
|
239
|
+
* closed for a scope on such a field) so a snapshot never copies large text.
|
|
240
|
+
*/
|
|
241
|
+
interface OperationScopeSnapshot {
|
|
242
|
+
/** Values before the operation; null when the record did not exist or was deleted. */
|
|
243
|
+
pre: Record<string, unknown> | null;
|
|
244
|
+
/** Values after it; a delete keeps the record's last values; null when unknown. */
|
|
245
|
+
post: Record<string, unknown> | null;
|
|
122
246
|
}
|
|
123
247
|
/**
|
|
124
248
|
* Options for querying a materialized collection table.
|
|
@@ -137,11 +261,266 @@ interface CollectionQueryOptions {
|
|
|
137
261
|
/** Include soft-deleted records (default: false) */
|
|
138
262
|
includeDeleted?: boolean;
|
|
139
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Options for {@link ServerStore.applyRemoteOperation}.
|
|
266
|
+
*/
|
|
267
|
+
interface ApplyRemoteOptions {
|
|
268
|
+
/**
|
|
269
|
+
* Authorization re-check for an operation submitted by an untrusted writer.
|
|
270
|
+
*
|
|
271
|
+
* The store calls it inside its apply critical section (the SQLite write
|
|
272
|
+
* transaction, the in-memory store's synchronous apply, or a Postgres
|
|
273
|
+
* transaction holding the per-record advisory lock shared with conditional
|
|
274
|
+
* applies) with the record as stored at that moment, including a soft-deleted
|
|
275
|
+
* one, or null when none exists. Returning a refusal aborts the apply and the
|
|
276
|
+
* store throws {@link UplinkAuthorizationError}; nothing is written.
|
|
277
|
+
*
|
|
278
|
+
* This closes the window between a caller's own pre-check and the write, where an
|
|
279
|
+
* ownership change or a same-id insert committed by another writer (or another
|
|
280
|
+
* server instance) could otherwise slip in.
|
|
281
|
+
*/
|
|
282
|
+
authorize?: (storedRow: MaterializedRecord | null) => UplinkAuthorizationResult;
|
|
283
|
+
/**
|
|
284
|
+
* The writer does not reserve its sequence numbers inside the write transaction
|
|
285
|
+
* (a client that did not advertise the `sequenceReservation` handshake capability,
|
|
286
|
+
* Kora <= beta.12; RT-37). A different operation already holding the
|
|
287
|
+
* `(nodeId, sequenceNumber)` then does not refuse this one: both are stored as a
|
|
288
|
+
* legacy pair (each keeps its own delivery sequence, so both are delivered) instead
|
|
289
|
+
* of throwing {@link SequenceConflictError}. Default false: enforce.
|
|
290
|
+
*/
|
|
291
|
+
legacySequenceWriter?: boolean;
|
|
292
|
+
/**
|
|
293
|
+
* Called once the operation was committed as the second holder of its sequence (a
|
|
294
|
+
* legacy pair), for logging and diagnostics. Never called for a refused write.
|
|
295
|
+
*/
|
|
296
|
+
onLegacySequencePair?: (pair: LegacySequencePair) => void;
|
|
297
|
+
}
|
|
298
|
+
/** Options of {@link ServerStore.setSchema}. */
|
|
299
|
+
interface ServerSchemaOptions {
|
|
300
|
+
/** Schema transforms the store folds with (transforms at fold time, RT-84). */
|
|
301
|
+
operationTransforms?: readonly OperationTransform[];
|
|
302
|
+
}
|
|
303
|
+
/** One stored end-to-end key record (opaque JSON) and its owner, as backups carry it. */
|
|
304
|
+
interface EncryptionKeyRecordRow {
|
|
305
|
+
owner: string;
|
|
306
|
+
keyring: string;
|
|
307
|
+
revision: number;
|
|
308
|
+
/** The record's JSON, exactly as stored. */
|
|
309
|
+
record: string;
|
|
310
|
+
}
|
|
140
311
|
/**
|
|
141
312
|
* Server-side store interface. Extends SyncStore with lifecycle,
|
|
142
313
|
* introspection, and materialization methods needed by the sync server.
|
|
143
314
|
*/
|
|
144
315
|
interface ServerStore extends SyncStore {
|
|
316
|
+
/**
|
|
317
|
+
* Apply an operation to the log and materialized state. With
|
|
318
|
+
* `options.authorize`, the authorization is re-checked atomically with the write
|
|
319
|
+
* (see {@link ApplyRemoteOptions}). Stores written before this option existed
|
|
320
|
+
* may ignore it; callers always pre-check as well.
|
|
321
|
+
*
|
|
322
|
+
* Built-in stores throw {@link SequenceConflictError} when a different operation
|
|
323
|
+
* already holds the operation's `(nodeId, sequenceNumber)` under enforcement
|
|
324
|
+
* (unless `options.legacySequenceWriter`, see {@link judgeSequenceHolders}), and return
|
|
325
|
+
* `'duplicate'` (writing nothing) for an operation id already stored, decided
|
|
326
|
+
* atomically with the write.
|
|
327
|
+
*/
|
|
328
|
+
applyRemoteOperation(op: Operation, options?: ApplyRemoteOptions): Promise<ApplyResult>;
|
|
329
|
+
/**
|
|
330
|
+
* The version vector as committed in the shared database, read fresh (SRV-4). A
|
|
331
|
+
* store shared by several server instances must implement it: its synchronous
|
|
332
|
+
* {@link SyncStore.getVersionVector} can only reflect this instance's own writes.
|
|
333
|
+
* Optional; callers fall back to `getVersionVector()`.
|
|
334
|
+
*/
|
|
335
|
+
readVersionVector?(): Promise<VersionVector>;
|
|
336
|
+
/**
|
|
337
|
+
* Several records of one collection by id, as stored (soft-deleted ones included),
|
|
338
|
+
* in one read: `id = ANY(...)` on Postgres, chunked `IN (...)` on SQLite (LMS #11).
|
|
339
|
+
* Ids with no row are absent from the result. Optional; the delivery stream falls
|
|
340
|
+
* back to one lookup per operation without it.
|
|
341
|
+
*/
|
|
342
|
+
findRecordsByIds?(collection: string, ids: string[]): Promise<Map<string, MaterializedRecord>>;
|
|
343
|
+
/**
|
|
344
|
+
* Which of `ids` the operation log already holds, with the node and sequence each is
|
|
345
|
+
* stored under, in one read per batch (chunked `IN (...)`). The sync session asks before any other per-operation check, so a
|
|
346
|
+
* device re-uploading history the server already stores (the one-time upgrade
|
|
347
|
+
* re-upload, or an op the client's sequence repair renumbered under its original
|
|
348
|
+
* id) is acknowledged as a duplicate instead of being re-judged by today's
|
|
349
|
+
* authorization and validators. Optional; sessions fall back to a per-operation
|
|
350
|
+
* (node, sequence) lookup without it.
|
|
351
|
+
*/
|
|
352
|
+
findStoredOperations?(ids: string[]): Promise<Map<string, StoredOperationKey>>;
|
|
353
|
+
/**
|
|
354
|
+
* Durably record how an uploaded operation was resolved without being stored under
|
|
355
|
+
* its sequence (RT-43, RT-47; see {@link OperationResolution}). Idempotent per
|
|
356
|
+
* operation id: the first resolution wins. The session awaits it BEFORE answering the
|
|
357
|
+
* client, so a client never acts on a resolution the server could forget. Optional
|
|
358
|
+
* for custom stores: without it an ignored tail op may be re-validated at reconnect
|
|
359
|
+
* and a refused id re-judged on resubmission.
|
|
360
|
+
*/
|
|
361
|
+
recordOperationResolution?(resolution: OperationResolution): Promise<void>;
|
|
362
|
+
/**
|
|
363
|
+
* The resolutions recorded for `ids` under `nodeId` (ids resolved under another node
|
|
364
|
+
* are absent: the answer never crosses devices or tenants).
|
|
365
|
+
*/
|
|
366
|
+
findOperationResolutions?(nodeId: string, ids: string[]): Promise<Map<string, OperationResolution>>;
|
|
367
|
+
/** The highest sequence number of `nodeId` with a recorded resolution (0 when none). */
|
|
368
|
+
getResolvedThrough?(nodeId: string): Promise<number>;
|
|
369
|
+
/**
|
|
370
|
+
* Forget the resolution recorded for `operationId` under `nodeId` (no-op when none).
|
|
371
|
+
* The session calls it for a stale `stored-elsewhere` record: one whose id the batch
|
|
372
|
+
* lookup did not find stored (RT-51). The operation is then judged normally, and its
|
|
373
|
+
* real outcome is recorded in place of the stale one. Optional for custom stores:
|
|
374
|
+
* without it the stale record is ignored but kept.
|
|
375
|
+
*/
|
|
376
|
+
deleteOperationResolution?(nodeId: string, operationId: string): Promise<void>;
|
|
377
|
+
/**
|
|
378
|
+
* Every operation of `nodeId` at or below `throughSequence` that shares its sequence
|
|
379
|
+
* with another stored operation (a legacy pair, RT-37), ordered by sequence then
|
|
380
|
+
* delivery. A version-vector client reporting `throughSequence` for the node may hold
|
|
381
|
+
* only one of a pair, and a range read above its entry can never return the other
|
|
382
|
+
* (RT-48). Optional; without it such clients miss the second op of a pair.
|
|
383
|
+
*/
|
|
384
|
+
getSequencePairOperations?(nodeId: string, throughSequence: number): Promise<Operation[]>;
|
|
385
|
+
/**
|
|
386
|
+
* The distinct node ids of the operations with `deliverySequence >
|
|
387
|
+
* afterDeliverySequence`. Optional; lets a handshake stop judging which nodes a
|
|
388
|
+
* scoped client will hear from as soon as every candidate was found visible.
|
|
389
|
+
*/
|
|
390
|
+
getNodeIdsAfterDelivery?(afterDeliverySequence: number): Promise<string[]>;
|
|
391
|
+
/**
|
|
392
|
+
* Bind a client node id to the authenticated principal that first used it.
|
|
393
|
+
* Returns true when the node id is already owned by `userId`, was released by an
|
|
394
|
+
* admin (see {@link releaseNodeClaim}; the caller takes it over), or is unclaimed
|
|
395
|
+
* AND has no operation history. Returns false when another principal owns it, or
|
|
396
|
+
* when it is unclaimed but already has operations in the log: history written
|
|
397
|
+
* before node claims existed has no recorded writer, so nobody may adopt it
|
|
398
|
+
* until an admin releases it (RT-5). Called at handshake when auth is configured,
|
|
399
|
+
* so one user cannot upload operations under another user's device id. Every
|
|
400
|
+
* write after this check is preceded by a claim, so going forward the claim row
|
|
401
|
+
* records the writer of a node's history. Must be atomic per node id. Optional
|
|
402
|
+
* for custom stores; the built-in stores persist the claim.
|
|
403
|
+
*/
|
|
404
|
+
claimNode?(nodeId: string, userId: string): Promise<boolean>;
|
|
405
|
+
/**
|
|
406
|
+
* Admin release of a node id (RT-5): the next principal to claim it takes it
|
|
407
|
+
* over, even when the node has operation history. Returns true when the node had
|
|
408
|
+
* a claim or history to release, false when it was unknown.
|
|
409
|
+
*/
|
|
410
|
+
releaseNodeClaim?(nodeId: string): Promise<boolean>;
|
|
411
|
+
/**
|
|
412
|
+
* The owner a node id is currently claimed by, or null when unclaimed (RT-21).
|
|
413
|
+
* Optional; anonymous provisional claims need it together with
|
|
414
|
+
* {@link replaceNodeClaim}.
|
|
415
|
+
*/
|
|
416
|
+
getNodeClaimOwner?(nodeId: string): Promise<string | null>;
|
|
417
|
+
/**
|
|
418
|
+
* Atomically replace the owner of a node claim, only if it is still
|
|
419
|
+
* `expectedOwner` (compare-and-set, RT-21). Returns true when replaced. Used to
|
|
420
|
+
* confirm or re-issue an anonymous device's provisional claim; never creates one.
|
|
421
|
+
*/
|
|
422
|
+
replaceNodeClaim?(nodeId: string, expectedOwner: string, newOwner: string): Promise<boolean>;
|
|
423
|
+
/**
|
|
424
|
+
* The wrapped encryption key record (JSON) of one owner's keyring, or null (ENC-1).
|
|
425
|
+
* The record holds only salt, KDF parameters and wrapped keys: never a usable key.
|
|
426
|
+
* Optional for custom stores; without it the server answers key requests with
|
|
427
|
+
* `unsupported` (it never keeps key records in memory only, which would fork a
|
|
428
|
+
* user's keys after a restart).
|
|
429
|
+
*/
|
|
430
|
+
getEncryptionKeyRecord?(owner: string, keyring: string): Promise<string | null>;
|
|
431
|
+
/**
|
|
432
|
+
* Write a key record with compare-and-set: store `record` at `revision` only when the
|
|
433
|
+
* stored revision is `expectedRevision` (0: no record yet). Returns false when
|
|
434
|
+
* another write won. Must be atomic per (owner, keyring) across server instances.
|
|
435
|
+
*/
|
|
436
|
+
putEncryptionKeyRecord?(owner: string, keyring: string, record: string, revision: number, expectedRevision: number): Promise<boolean>;
|
|
437
|
+
/**
|
|
438
|
+
* Stored key records: every one (for `exportBackup`; RT-104: a server restored from
|
|
439
|
+
* its backup must still hold the records its encrypted history needs), or one owner's
|
|
440
|
+
* (the key service tells another keyring's history from a lost record with it).
|
|
441
|
+
* Optional.
|
|
442
|
+
*/
|
|
443
|
+
listEncryptionKeyRecords?(owner?: string): Promise<EncryptionKeyRecordRow[]>;
|
|
444
|
+
/**
|
|
445
|
+
* Key ids named by stored encrypted operations (their envelope's `keyId`), a sample
|
|
446
|
+
* of at most `limit` distinct ids (RT-104). `nodeOwner` restricts it to operations of
|
|
447
|
+
* nodes claimed by that principal (see {@link claimNode}); null means every node (a
|
|
448
|
+
* server without authentication, whose clients share one keyring). The key service
|
|
449
|
+
* reports them when an owner has no key record, so a new device can tell a lost
|
|
450
|
+
* record (encrypted history exists) from a first one. Optional; without it a new
|
|
451
|
+
* device cannot tell, and a fork it creates is merged later by a device holding the
|
|
452
|
+
* old ring.
|
|
453
|
+
*/
|
|
454
|
+
getEncryptedKeyIds?(nodeOwner: string | null, limit: number): Promise<string[]>;
|
|
455
|
+
/**
|
|
456
|
+
* Record that `owner` holds the bytes behind a blob content hash (it pushed them,
|
|
457
|
+
* proving possession) (RT-11). Idempotent. Optional; without it the sync server
|
|
458
|
+
* keeps ownership in memory (lost on restart, not shared between instances).
|
|
459
|
+
*/
|
|
460
|
+
recordBlobOwner?(hash: string, owner: string): Promise<void>;
|
|
461
|
+
/** The owners recorded for each hash (an empty list for unowned hashes). */
|
|
462
|
+
getBlobOwners?(hashes: string[]): Promise<Map<string, string[]>>;
|
|
463
|
+
/**
|
|
464
|
+
* Atomically make `owner` the first owner of a hash nobody owns yet. Returns true
|
|
465
|
+
* when `owner` owns the hash afterwards (newly, or already), false when someone
|
|
466
|
+
* else does. Must be atomic per hash across server instances.
|
|
467
|
+
*/
|
|
468
|
+
claimBlobIfUnowned?(hash: string, owner: string): Promise<boolean>;
|
|
469
|
+
/**
|
|
470
|
+
* The scope snapshots captured when the given operations were applied (RT-14).
|
|
471
|
+
* Operations without one are absent from the result.
|
|
472
|
+
*/
|
|
473
|
+
getOperationScopeSnapshots?(operationIds: string[]): Promise<Map<string, OperationScopeSnapshot>>;
|
|
474
|
+
/**
|
|
475
|
+
* The greatest HLC timestamp among every stored operation of one record (its
|
|
476
|
+
* newest field write), or null when the record has no operations (RT-19). A
|
|
477
|
+
* scope-entry operation is stamped with it so it never overrides newer client data.
|
|
478
|
+
*/
|
|
479
|
+
getRecordLatestTimestamp?(collection: string, recordId: string): Promise<HLCTimestamp | null>;
|
|
480
|
+
/**
|
|
481
|
+
* Per-field versions of one live record, folded from its stored operations exactly
|
|
482
|
+
* as the materialization folds its values (RT-27): each field's version is the HLC
|
|
483
|
+
* of the write that produced its current value, plus the record's first and newest
|
|
484
|
+
* operation. Null when the record is deleted or has no operations. A scope-entry
|
|
485
|
+
* operation carries them so a receiver resolves every field on its own.
|
|
486
|
+
*/
|
|
487
|
+
getRecordFieldVersions?(collection: string, recordId: string): Promise<RecordFieldVersions | null>;
|
|
488
|
+
/**
|
|
489
|
+
* The record's fold state (W7): the per-field CRDT state its row is projected from.
|
|
490
|
+
* Null when the record has no operations or its collection is not materialized. A
|
|
491
|
+
* scope-entry operation carries it (filtered to the fields the receiver may see) so
|
|
492
|
+
* the receiver merges richtext, counter and resolver fields exactly (RT-29).
|
|
493
|
+
*/
|
|
494
|
+
getRecordFoldState?(collection: string, recordId: string): Promise<FoldState | null>;
|
|
495
|
+
/** Every stored operation of one record, in delivery (commit) order. */
|
|
496
|
+
getRecordOperations?(collection: string, recordId: string): Promise<Operation[]>;
|
|
497
|
+
/**
|
|
498
|
+
* The record's row as it would be after merging `op` into its fold state, with
|
|
499
|
+
* nothing written: the candidate a Tier-2 constraint check judges at ingest. Null
|
|
500
|
+
* when the record would not be live (deleted, or never inserted).
|
|
501
|
+
*/
|
|
502
|
+
previewOperation?(op: Operation): Promise<MaterializedRecord | null>;
|
|
503
|
+
/**
|
|
504
|
+
* Node ids whose operations win `merge('server-authoritative')` fields in the fold:
|
|
505
|
+
* this store's own node id (every server-originated operation is authored by it)
|
|
506
|
+
* plus any configured extras. The sync server advertises them in the handshake so
|
|
507
|
+
* clients fold with the same authority.
|
|
508
|
+
*/
|
|
509
|
+
getAuthoritativeNodeIds?(): string[];
|
|
510
|
+
/** Explicit authoritative ids the deployment revoked (RT-81); advertised at handshake. */
|
|
511
|
+
getRevokedAuthoritativeNodeIds?(): string[];
|
|
512
|
+
/**
|
|
513
|
+
* Every explicit id the deployment ever held authoritative, revoked ones included
|
|
514
|
+
* (RT-81). The session never accepts one as a device node id.
|
|
515
|
+
*/
|
|
516
|
+
getEverAuthoritativeNodeIds?(): string[];
|
|
517
|
+
/**
|
|
518
|
+
* Keyed id of a server-derived operation (cascade, set-null, constraint correction):
|
|
519
|
+
* deterministic across every instance of the deployment, unpredictable to clients
|
|
520
|
+
* (RT-64). Built-in stores key it with a persisted deployment secret. A store
|
|
521
|
+
* without it falls back to the unkeyed `deriveSideEffectOpId`.
|
|
522
|
+
*/
|
|
523
|
+
deriveServerOperationId?(parentOpId: string, ruleId: string, targetRecordId: string): Promise<string>;
|
|
145
524
|
/** Close the store and release resources */
|
|
146
525
|
close(): Promise<void>;
|
|
147
526
|
/** Get the total number of stored operations */
|
|
@@ -188,10 +567,23 @@ interface ServerStore extends SyncStore {
|
|
|
188
567
|
* tables from the operation log.
|
|
189
568
|
*
|
|
190
569
|
* @param schema - The schema definition describing all collections
|
|
570
|
+
* @param options - `operationTransforms`: the schema transforms the store folds with
|
|
571
|
+
* (transforms at fold time, RT-84). Pass the same list as the sync server's
|
|
572
|
+
* `operationTransforms`, so the startup re-materialization folds with them once.
|
|
191
573
|
*/
|
|
192
|
-
setSchema(schema: SchemaDefinition): Promise<void>;
|
|
574
|
+
setSchema(schema: SchemaDefinition, options?: ServerSchemaOptions): Promise<void>;
|
|
193
575
|
/** Schema used for materialized tables and server-side validation, if set. */
|
|
194
576
|
getSchema(): SchemaDefinition | null;
|
|
577
|
+
/**
|
|
578
|
+
* Set the schema transforms the store folds with (RT-84). Operations are stored
|
|
579
|
+
* exactly as uploaded; the fold merges each operation as
|
|
580
|
+
* `operationSchemaView(op, schema.version, transforms)` reads it. Transforms are part
|
|
581
|
+
* of the fold plan fingerprint: a change re-folds every record (once). The sync
|
|
582
|
+
* server calls this with its `operationTransforms` at construction.
|
|
583
|
+
*/
|
|
584
|
+
setOperationTransforms?(transforms: readonly OperationTransform[]): Promise<void>;
|
|
585
|
+
/** The schema transforms the store folds with (empty when none). */
|
|
586
|
+
getOperationTransforms?(): readonly OperationTransform[];
|
|
195
587
|
/**
|
|
196
588
|
* Get all records from a materialized collection.
|
|
197
589
|
* When schema is set, reads directly from the collection table (O(1) indexed).
|
|
@@ -303,6 +695,68 @@ interface ApplyServerOperationResult {
|
|
|
303
695
|
appliedOperations: Operation[];
|
|
304
696
|
/** Rejection reason when the operation was not applied */
|
|
305
697
|
rejection?: OperationRejection;
|
|
698
|
+
/**
|
|
699
|
+
* Side effects of an applied delete that were NOT derived because
|
|
700
|
+
* {@link ApplyServerOperationOptions.isAuthoredSideEffect} claimed them (RT-69). The
|
|
701
|
+
* caller must derive each one with {@link deriveServerSideEffects} unless the
|
|
702
|
+
* author's own copy is stored.
|
|
703
|
+
*/
|
|
704
|
+
deferredSideEffects?: SideEffectOp[];
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Decides whether an untrusted writer may cause one referential side effect of its
|
|
708
|
+
* delete. `effect` is the side effect shaped as the operation the server would
|
|
709
|
+
* write; `stored` is the affected record as stored now (including soft-deleted).
|
|
710
|
+
*/
|
|
711
|
+
type SideEffectAuthorizer = (effect: Operation, stored: MaterializedRecord | null) => UplinkAuthorizationResult;
|
|
712
|
+
/** Options for {@link applyServerOperation}. */
|
|
713
|
+
interface ApplyServerOperationOptions {
|
|
714
|
+
/**
|
|
715
|
+
* Uplink authorization for an untrusted writer, re-evaluated by the store inside
|
|
716
|
+
* its apply critical section against the row as stored at commit time (see
|
|
717
|
+
* `ApplyRemoteOptions.authorize`). A refusal becomes a non-retriable rejection and
|
|
718
|
+
* nothing is written.
|
|
719
|
+
*/
|
|
720
|
+
authorize?: ApplyRemoteOptions['authorize'];
|
|
721
|
+
/**
|
|
722
|
+
* Authorization of the referential side effects (cascade delete, set-null) an
|
|
723
|
+
* untrusted writer's delete would cause (RT-10). Every side effect is checked
|
|
724
|
+
* against the writer's own scope BEFORE the delete is applied; if any falls
|
|
725
|
+
* outside it, the whole delete is refused and nothing is written.
|
|
726
|
+
*
|
|
727
|
+
* Design: integrity is judged on ALL children, in every tenant (a restrict or a
|
|
728
|
+
* cross-tenant cascade is never silently skipped, which would leave dangling
|
|
729
|
+
* references), but the writer learns nothing about children it cannot see: a
|
|
730
|
+
* restrict violation and an unauthorized side effect are refused with the same
|
|
731
|
+
* generic `RESTRICTED` rejection, with no ids and no counts. Setting `authorize`
|
|
732
|
+
* alone also switches restrict refusals to the generic form.
|
|
733
|
+
*/
|
|
734
|
+
authorizeSideEffect?: SideEffectAuthorizer;
|
|
735
|
+
/**
|
|
736
|
+
* The writer is a legacy client that does not reserve sequence numbers (RT-37): a
|
|
737
|
+
* different operation under a held `(nodeId, sequenceNumber)` is stored as a legacy
|
|
738
|
+
* pair instead of being refused with `SEQUENCE_CONFLICT`. Applies to the primary
|
|
739
|
+
* operation only; server-originated side effects are always enforced.
|
|
740
|
+
*/
|
|
741
|
+
legacySequenceWriter?: boolean;
|
|
742
|
+
/** See `ApplyRemoteOptions.onLegacySequencePair`; for the primary operation. */
|
|
743
|
+
onLegacySequencePair?: ApplyRemoteOptions['onLegacySequencePair'];
|
|
744
|
+
/**
|
|
745
|
+
* True for a referential side effect of this delete that the writer authored itself
|
|
746
|
+
* (its own cascade or set-null, later in the same upload, RT-69). Such an effect is
|
|
747
|
+
* not derived now but returned in `deferredSideEffects`, so the log stores one copy
|
|
748
|
+
* per child instead of the author's and the server's. Effects it does not claim (a
|
|
749
|
+
* child the author did not know, a legacy client that authors no cascades) are
|
|
750
|
+
* derived as before.
|
|
751
|
+
*/
|
|
752
|
+
isAuthoredSideEffect?: (effect: SideEffectOp) => boolean;
|
|
753
|
+
/**
|
|
754
|
+
* The operation as the server schema reads it (transforms at fold time, RT-84), when
|
|
755
|
+
* the caller already computed it. Shape, constraint and referential checks judge the
|
|
756
|
+
* view; the store keeps `op` exactly as given and folds the same view. Default: the
|
|
757
|
+
* store's own view (`serverOperationView`).
|
|
758
|
+
*/
|
|
759
|
+
view?: Operation;
|
|
306
760
|
}
|
|
307
761
|
|
|
308
762
|
/**
|
|
@@ -314,6 +768,17 @@ interface ServerMetricsSnapshot {
|
|
|
314
768
|
peakConnections: number;
|
|
315
769
|
connectionsTotal: number;
|
|
316
770
|
operationsReceived: number;
|
|
771
|
+
batchesReceived: number;
|
|
772
|
+
uniqueOperationsReceived: number;
|
|
773
|
+
duplicateOperationsReceived: number;
|
|
774
|
+
rejectedOperations: number;
|
|
775
|
+
/**
|
|
776
|
+
* Operations from protocol-1 (beta.12) clients stored with an id that could not be
|
|
777
|
+
* verified (RT-71). Non-zero only while beta.12 clients write `undefined` members.
|
|
778
|
+
*/
|
|
779
|
+
unverifiedLegacyOperations: number;
|
|
780
|
+
/** Uploads refused for reusing a stored operation id with other content (RT-77). */
|
|
781
|
+
forgedDuplicates: number;
|
|
317
782
|
operationsSent: number;
|
|
318
783
|
bytesReceived: number;
|
|
319
784
|
bytesSent: number;
|
|
@@ -332,6 +797,10 @@ interface ClientMetrics {
|
|
|
332
797
|
state: SessionState$1;
|
|
333
798
|
connectedAt: number;
|
|
334
799
|
operationsReceived: number;
|
|
800
|
+
batchesReceived: number;
|
|
801
|
+
uniqueOperationsReceived: number;
|
|
802
|
+
duplicateOperationsReceived: number;
|
|
803
|
+
rejectedOperations: number;
|
|
335
804
|
operationsSent: number;
|
|
336
805
|
authContext: AuthContext | null;
|
|
337
806
|
}
|
|
@@ -340,6 +809,12 @@ declare class ServerMetricsCollector {
|
|
|
340
809
|
private peakConnections;
|
|
341
810
|
private connectionsTotal;
|
|
342
811
|
private operationsReceived;
|
|
812
|
+
private batchesReceived;
|
|
813
|
+
private uniqueOperationsReceived;
|
|
814
|
+
private duplicateOperationsReceived;
|
|
815
|
+
private rejectedOperations;
|
|
816
|
+
private unverifiedLegacyOperations;
|
|
817
|
+
private forgedDuplicates;
|
|
343
818
|
private operationsSent;
|
|
344
819
|
private bytesReceived;
|
|
345
820
|
private bytesSent;
|
|
@@ -357,9 +832,17 @@ declare class ServerMetricsCollector {
|
|
|
357
832
|
/** Record authentication context for a session. */
|
|
358
833
|
recordAuth(sessionId: string, authContext: AuthContext | null): void;
|
|
359
834
|
/** Record operations received from a client. */
|
|
360
|
-
recordReceived(sessionId: string, count: number, byteSize: number
|
|
835
|
+
recordReceived(sessionId: string, count: number, byteSize: number, outcomes?: {
|
|
836
|
+
unique: number;
|
|
837
|
+
duplicates: number;
|
|
838
|
+
rejected: number;
|
|
839
|
+
}): void;
|
|
361
840
|
/** Record operations sent to a client. */
|
|
362
841
|
recordSent(sessionId: string, count: number, byteSize: number): void;
|
|
842
|
+
/** Record an operation stored with an unverified legacy id (RT-71). */
|
|
843
|
+
recordUnverifiedLegacyOperation(): void;
|
|
844
|
+
/** Record an upload refused for reusing a stored id with other content (RT-77). */
|
|
845
|
+
recordForgedDuplicate(): void;
|
|
363
846
|
/** Record an error. */
|
|
364
847
|
recordError(): void;
|
|
365
848
|
/** Set the schema version. */
|
|
@@ -424,6 +907,210 @@ declare function createSilentLogger(): Logger;
|
|
|
424
907
|
*/
|
|
425
908
|
declare function createDefaultLogger(): Logger;
|
|
426
909
|
|
|
910
|
+
/**
|
|
911
|
+
* Resolves a blob chunk by content hash from server-side storage. Optional: when
|
|
912
|
+
* provided, the server can answer chunk requests directly from its own store
|
|
913
|
+
* (central-store deployment). When absent, the server acts as a pure relay,
|
|
914
|
+
* forwarding chunk requests to peer sessions and their responses back.
|
|
915
|
+
*/
|
|
916
|
+
type ResolveBlobChunk = (hash: string) => Promise<Uint8Array | null>;
|
|
917
|
+
|
|
918
|
+
/** Input to {@link BlobAccessIndex.authorizeReference}. */
|
|
919
|
+
interface BlobReferenceRequest {
|
|
920
|
+
/** The content hash (blob, manifest or chunk) the writer wants to reference. */
|
|
921
|
+
hash: string;
|
|
922
|
+
/** The writer's download scope, or undefined for an unscoped writer. */
|
|
923
|
+
scopes: ScopeMap | undefined;
|
|
924
|
+
/**
|
|
925
|
+
* Stable ownership key of the writer: the user id for a signed-in principal, the
|
|
926
|
+
* node-claim owner for an anonymous device, a `kora:route:` key for a scoped route.
|
|
927
|
+
*/
|
|
928
|
+
owner: string;
|
|
929
|
+
/** Hashes the written record already references as stored (unchanged references). */
|
|
930
|
+
alreadyReferenced?: ReadonlySet<string>;
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Answers "does a live record inside this download scope reference this blob
|
|
934
|
+
* hash?" for the blob relay (RT-1), and "may this writer reference this hash?" at
|
|
935
|
+
* ingest (RT-11).
|
|
936
|
+
*
|
|
937
|
+
* **A content hash is not a secret.** Hashes of well-known content can be computed
|
|
938
|
+
* by anyone, and they travel in records, manifests and logs. So knowing a hash never
|
|
939
|
+
* grants access: a write may reference a hash only when the writer can already read
|
|
940
|
+
* a record that references it, or has proven possession of the bytes (pushed them).
|
|
941
|
+
* Only in peer-relay mode (no central store, so nothing can be pre-claimed and then
|
|
942
|
+
* read back from the server) may the first writer of a hash nobody references or
|
|
943
|
+
* owns claim it by reference alone.
|
|
944
|
+
*
|
|
945
|
+
* A hash is referenced in a scope when a live (not deleted) record in scope has a
|
|
946
|
+
* blob field whose `BlobRef.manifestHash` (or, for a bare reference without a
|
|
947
|
+
* manifest, `BlobRef.hash`) equals it, or when it is a chunk listed in such a manifest and the chunk is unowned or shares an owner with
|
|
948
|
+
* the manifest (so a crafted manifest listing someone else's chunk hashes exposes
|
|
949
|
+
* nothing). Manifests are read through `resolveBlobChunk` (central store) or learned
|
|
950
|
+
* from manifest bytes verified in transit.
|
|
951
|
+
*
|
|
952
|
+
* Cost: the referenced set of a scope is built by a scoped query over every
|
|
953
|
+
* collection that declares a blob field (O(records in scope with blobs)). It is
|
|
954
|
+
* cached per scope, rebuilt after a write to one of the blob collections it scanned
|
|
955
|
+
* ({@link invalidate}) or after `ttlMs` (writes through another server instance),
|
|
956
|
+
* and concurrent lookups for one scope share a single rebuild (RT-17). A full hash
|
|
957
|
+
* index in the store would make this O(1); this is the interim, correctness-first
|
|
958
|
+
* form.
|
|
959
|
+
*/
|
|
960
|
+
declare class BlobAccessIndex {
|
|
961
|
+
private readonly store;
|
|
962
|
+
private readonly resolveBlobChunk;
|
|
963
|
+
private readonly ttlMs;
|
|
964
|
+
private readonly byScope;
|
|
965
|
+
/** Rebuilds in progress per scope key, shared by concurrent lookups (RT-17). */
|
|
966
|
+
private readonly inflight;
|
|
967
|
+
/** manifestHash -> chunk hashes it lists (only from bytes verified against the hash). */
|
|
968
|
+
private readonly manifestChunks;
|
|
969
|
+
/** chunkHash -> manifests listing it. */
|
|
970
|
+
private readonly chunkManifests;
|
|
971
|
+
/** Ownership fallback for stores without blob-owner persistence. */
|
|
972
|
+
private readonly memoryOwners;
|
|
973
|
+
private globalGeneration;
|
|
974
|
+
private readonly collectionGeneration;
|
|
975
|
+
constructor(store: ServerStore, resolveBlobChunk: ResolveBlobChunk | null, ttlMs?: number);
|
|
976
|
+
/**
|
|
977
|
+
* Forget cached scope sets because records changed. With `collections`, only sets
|
|
978
|
+
* built from those collections are dropped (a write to a collection without blob
|
|
979
|
+
* fields drops nothing); without, every set is dropped (for example writes made
|
|
980
|
+
* through another server instance).
|
|
981
|
+
*/
|
|
982
|
+
invalidate(collections?: Iterable<string>): void;
|
|
983
|
+
/**
|
|
984
|
+
* True when a live record inside `scopes` references `hash`.
|
|
985
|
+
*
|
|
986
|
+
* @param scopes - The session's download scope, or undefined for unscoped
|
|
987
|
+
* @param hash - The requested content hash
|
|
988
|
+
*/
|
|
989
|
+
isReferenced(scopes: ScopeMap | undefined, hash: string): Promise<boolean>;
|
|
990
|
+
/**
|
|
991
|
+
* Decide whether a writer may reference `hash` in a blob field (RT-11). Allowed
|
|
992
|
+
* when the record already referenced it, when a live record inside the writer's
|
|
993
|
+
* download scope references it, when the writer owns it (pushed the bytes, or
|
|
994
|
+
* claimed it first), or when nobody references, owns or stores it yet: then the
|
|
995
|
+
* writer claims it, atomically, so a later forger cannot.
|
|
996
|
+
*/
|
|
997
|
+
authorizeReference(request: BlobReferenceRequest): Promise<boolean>;
|
|
998
|
+
/** True when `owner` pushed (or first claimed) the bytes behind `hash`. */
|
|
999
|
+
isOwnedBy(hash: string, owner: string): Promise<boolean>;
|
|
1000
|
+
/**
|
|
1001
|
+
* Record that `owner` pushed bytes verified to hash to `hash` (proof of
|
|
1002
|
+
* possession), and learn the chunk list when the bytes are a manifest.
|
|
1003
|
+
*/
|
|
1004
|
+
recordPush(hash: string, bytes: Uint8Array, owner: string): Promise<void>;
|
|
1005
|
+
/**
|
|
1006
|
+
* When `bytes` are a manifest, true only if the pusher may reference every chunk
|
|
1007
|
+
* it lists (the same rule as {@link authorizeReference}); true for non-manifest
|
|
1008
|
+
* bytes. A manifest listing someone else's chunks is refused before it is stored.
|
|
1009
|
+
*/
|
|
1010
|
+
authorizeManifestPush(bytes: Uint8Array, scopes: ScopeMap | undefined, owner: string): Promise<boolean>;
|
|
1011
|
+
/**
|
|
1012
|
+
* Learn a manifest's chunk list from bytes already verified to hash to `hash`.
|
|
1013
|
+
* Bytes that are not a manifest are ignored. Unowned chunks of an owned manifest
|
|
1014
|
+
* are claimed for the manifest's owner, so a manifest crafted later cannot adopt
|
|
1015
|
+
* them.
|
|
1016
|
+
*/
|
|
1017
|
+
observeVerifiedBytes(hash: string, bytes: Uint8Array): void;
|
|
1018
|
+
private claimChunksForManifestOwner;
|
|
1019
|
+
private currentStamp;
|
|
1020
|
+
private isCurrent;
|
|
1021
|
+
private blobCollections;
|
|
1022
|
+
private directHashes;
|
|
1023
|
+
private build;
|
|
1024
|
+
/** Read unknown manifests from the central store, verifying them against their hash. */
|
|
1025
|
+
private loadManifests;
|
|
1026
|
+
private ownersOf;
|
|
1027
|
+
private addOwner;
|
|
1028
|
+
private claim;
|
|
1029
|
+
private rememberMemoryOwner;
|
|
1030
|
+
private rememberManifest;
|
|
1031
|
+
private forgetManifest;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* Minimal interface for a ws.WebSocket instance.
|
|
1036
|
+
* Allows dependency injection for testing without importing ws directly.
|
|
1037
|
+
*/
|
|
1038
|
+
interface WsWebSocket {
|
|
1039
|
+
readyState: number;
|
|
1040
|
+
send(data: string | Uint8Array, callback?: (err?: Error) => void): void;
|
|
1041
|
+
close(code?: number, reason?: string): void;
|
|
1042
|
+
on(event: string, listener: (...args: unknown[]) => void): void;
|
|
1043
|
+
removeAllListeners(): void;
|
|
1044
|
+
/** Send a ping frame (ws). Without it, no liveness probing happens. */
|
|
1045
|
+
ping?(): void;
|
|
1046
|
+
/** Destroy the socket at once, emitting `close` (ws). Falls back to `close()`. */
|
|
1047
|
+
terminate?(): void;
|
|
1048
|
+
/** Bytes queued and not yet written to the network (ws). */
|
|
1049
|
+
readonly bufferedAmount?: number;
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Options for WsServerTransport.
|
|
1053
|
+
*/
|
|
1054
|
+
interface WsServerTransportOptions {
|
|
1055
|
+
/** Message serializer. Defaults to JsonMessageSerializer. */
|
|
1056
|
+
serializer?: MessageSerializer;
|
|
1057
|
+
/**
|
|
1058
|
+
* Interval between WebSocket pings, in ms (SRV-6). A connection that leaves
|
|
1059
|
+
* `maxMissedPongs` pings in a row unanswered (and sends nothing else meanwhile) is
|
|
1060
|
+
* terminated, which runs Kora's normal close path. Defaults to 25 seconds; 0
|
|
1061
|
+
* disables probing. Keep it at 15 seconds or more on mobile networks.
|
|
1062
|
+
*/
|
|
1063
|
+
heartbeatIntervalMs?: number;
|
|
1064
|
+
/** Unanswered pings before the connection is terminated. Defaults to 2. */
|
|
1065
|
+
maxMissedPongs?: number;
|
|
1066
|
+
/**
|
|
1067
|
+
* Bytes that may be queued for this client before it is disconnected as a slow
|
|
1068
|
+
* consumer. Defaults to 32 MiB; 0 disables the ceiling.
|
|
1069
|
+
*/
|
|
1070
|
+
maxBufferedBytes?: number;
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Server-side transport wrapping a ws.WebSocket connection.
|
|
1074
|
+
* Created for each incoming client connection.
|
|
1075
|
+
*
|
|
1076
|
+
* It probes liveness with WebSocket pings (both the standalone server and
|
|
1077
|
+
* `createProductionServer` create their connections through it), so a half-open
|
|
1078
|
+
* connection, a peer that vanished without a FIN, is detected and closed instead of
|
|
1079
|
+
* holding a session forever.
|
|
1080
|
+
*/
|
|
1081
|
+
declare class WsServerTransport implements ServerTransport {
|
|
1082
|
+
private readonly ws;
|
|
1083
|
+
private readonly serializer;
|
|
1084
|
+
private messageHandler;
|
|
1085
|
+
private closeHandler;
|
|
1086
|
+
private errorHandler;
|
|
1087
|
+
private readonly maxMissedPongs;
|
|
1088
|
+
private readonly maxBufferedBytes;
|
|
1089
|
+
private heartbeatTimer;
|
|
1090
|
+
private missedPongs;
|
|
1091
|
+
private terminated;
|
|
1092
|
+
constructor(ws: WsWebSocket, options?: WsServerTransportOptions);
|
|
1093
|
+
send(message: SyncMessage): void;
|
|
1094
|
+
onMessage(handler: ServerMessageHandler): void;
|
|
1095
|
+
onClose(handler: ServerCloseHandler): void;
|
|
1096
|
+
onError(handler: ServerErrorHandler): void;
|
|
1097
|
+
isConnected(): boolean;
|
|
1098
|
+
close(code?: number, reason?: string): void;
|
|
1099
|
+
bufferedAmount(): number;
|
|
1100
|
+
/**
|
|
1101
|
+
* Ping every `intervalMs`; terminate once `maxMissedPongs` pings in a row went
|
|
1102
|
+
* unanswered. A pong or any inbound message proves the peer alive.
|
|
1103
|
+
*/
|
|
1104
|
+
private startHeartbeat;
|
|
1105
|
+
private stopHeartbeat;
|
|
1106
|
+
/**
|
|
1107
|
+
* Drop the connection at once. ws emits `close` from `terminate()`, which runs
|
|
1108
|
+
* Kora's normal close path (session, relays and side channels are released).
|
|
1109
|
+
*/
|
|
1110
|
+
private terminate;
|
|
1111
|
+
private setupListeners;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
427
1114
|
/**
|
|
428
1115
|
* Minimal interface for a ws.WebSocketServer instance.
|
|
429
1116
|
* Allows dependency injection for testing without importing ws directly.
|
|
@@ -442,6 +1129,8 @@ type WsServerConstructor = new (options: {
|
|
|
442
1129
|
port?: number;
|
|
443
1130
|
host?: string;
|
|
444
1131
|
path?: string;
|
|
1132
|
+
maxPayload?: number;
|
|
1133
|
+
perMessageDeflate?: false | Record<string, unknown>;
|
|
445
1134
|
}) => WsServerLike;
|
|
446
1135
|
/**
|
|
447
1136
|
* Self-hosted sync server. Accepts WebSocket connections from clients,
|
|
@@ -464,6 +1153,8 @@ declare class KoraSyncServer {
|
|
|
464
1153
|
private readonly schemaVersion;
|
|
465
1154
|
private readonly supportedSchemaVersions;
|
|
466
1155
|
private readonly operationTransforms;
|
|
1156
|
+
/** The store's re-fold for the configured transforms (RT-84), awaited by start(). */
|
|
1157
|
+
private storeTransformsReady;
|
|
467
1158
|
private readonly port;
|
|
468
1159
|
private readonly host;
|
|
469
1160
|
private readonly path;
|
|
@@ -472,14 +1163,49 @@ declare class KoraSyncServer {
|
|
|
472
1163
|
private readonly awarenessRelay;
|
|
473
1164
|
private readonly yjsDocRelay;
|
|
474
1165
|
private readonly blobChunkRelay;
|
|
1166
|
+
/** Wrapped encryption key records (ENC-1, D4b), persisted in the server store. */
|
|
1167
|
+
private readonly encryptionKeys;
|
|
1168
|
+
/** Which blob hashes each download scope may obtain (RT-1). */
|
|
1169
|
+
private readonly blobAccess;
|
|
475
1170
|
private readonly persistBlobChunk;
|
|
476
1171
|
private readonly maxOperationBytes;
|
|
477
1172
|
private readonly maxOpsPerMinute;
|
|
1173
|
+
/** Per-principal ingest budget per minute; 0 when disabled. */
|
|
1174
|
+
private readonly maxOpsPerMinutePerUser;
|
|
1175
|
+
private readonly allowLegacyAnonymousClaims;
|
|
1176
|
+
private readonly anonymousClaimTtlMs;
|
|
1177
|
+
private readonly maxOpsPerBatch;
|
|
1178
|
+
private readonly maxMessageBytes;
|
|
1179
|
+
private readonly heartbeatIntervalMs;
|
|
1180
|
+
private readonly appHeartbeatIntervalMs;
|
|
1181
|
+
private readonly encryptionPolicy;
|
|
1182
|
+
private readonly handshakeTimeoutMs;
|
|
1183
|
+
private readonly maxBufferedBytes;
|
|
1184
|
+
private readonly deliveryHighWaterBytes;
|
|
1185
|
+
private readonly perMessageDeflate;
|
|
1186
|
+
/**
|
|
1187
|
+
* Ingest rate limiters by device node id, shared by that node's sessions so a
|
|
1188
|
+
* reconnect does not reset the budget (SRV-6). Entries idle past the window are
|
|
1189
|
+
* dropped by the background tick; insertion order doubles as LRU order.
|
|
1190
|
+
*/
|
|
1191
|
+
private readonly rateLimiters;
|
|
1192
|
+
/**
|
|
1193
|
+
* Ingest budgets by authenticated principal, shared by every node of that user, so
|
|
1194
|
+
* minting node ids does not multiply the per-node budget. Expired like the node map.
|
|
1195
|
+
*/
|
|
1196
|
+
private readonly userRateLimiters;
|
|
1197
|
+
private readonly blobLimits;
|
|
478
1198
|
private readonly validateOperation;
|
|
479
1199
|
private readonly koraContext;
|
|
480
1200
|
private readonly sessions;
|
|
481
|
-
|
|
482
|
-
|
|
1201
|
+
/**
|
|
1202
|
+
* HTTP long-poll sessions by their server-issued, high-entropy id (RT-2). Each is
|
|
1203
|
+
* bound to the principal that opened it; every request must re-authenticate as it.
|
|
1204
|
+
*/
|
|
1205
|
+
private readonly httpSessions;
|
|
1206
|
+
/** Internal ClientSession id -> HTTP session id, for cleanup on close. */
|
|
1207
|
+
private readonly httpSessionIdBySession;
|
|
1208
|
+
private readonly httpSessionIdleTimeoutMs;
|
|
483
1209
|
private readonly serverVersion;
|
|
484
1210
|
private wsServer;
|
|
485
1211
|
private running;
|
|
@@ -492,6 +1218,17 @@ declare class KoraSyncServer {
|
|
|
492
1218
|
private deliveryPollTimer;
|
|
493
1219
|
private lastObservedDeliverySequence;
|
|
494
1220
|
private deliveryPollInFlight;
|
|
1221
|
+
/**
|
|
1222
|
+
* Re-validates every live session's credential against the auth provider, so a
|
|
1223
|
+
* revocation persisted by ANOTHER server instance (whose in-process revocation
|
|
1224
|
+
* listeners never reach this one) still ends the session here (RT-18).
|
|
1225
|
+
*/
|
|
1226
|
+
private readonly sessionRevalidationIntervalMs;
|
|
1227
|
+
private sessionRevalidationTimer;
|
|
1228
|
+
private lastSessionRevalidationAtMs;
|
|
1229
|
+
private sessionRevalidationInFlight;
|
|
1230
|
+
/** Unsubscribes from the auth provider's revocation feed (AUTH-11). */
|
|
1231
|
+
private revocationUnsubscribe;
|
|
495
1232
|
/**
|
|
496
1233
|
* Relay operations a client never acknowledged before it disconnected, buffered by
|
|
497
1234
|
* client node id so they can be redelivered on its next connection. Bounded per
|
|
@@ -501,19 +1238,113 @@ declare class KoraSyncServer {
|
|
|
501
1238
|
private static readonly MAX_ORPHANED_RELAY_OPS_PER_NODE;
|
|
502
1239
|
private static readonly ORPHANED_RELAY_TTL_MS;
|
|
503
1240
|
constructor(config: KoraSyncServerConfig);
|
|
1241
|
+
/**
|
|
1242
|
+
* Follow the auth provider's revocation feed, when it has one, so revoking a
|
|
1243
|
+
* device or user ends its live sessions without any wiring by the app.
|
|
1244
|
+
*/
|
|
1245
|
+
private ensureRevocationSubscribed;
|
|
1246
|
+
/**
|
|
1247
|
+
* End live sync sessions whose credential was revoked (AUTH-11).
|
|
1248
|
+
*
|
|
1249
|
+
* Each matching session receives a retriable `AUTH_REVOKED` (or `AUTH_EXPIRED`)
|
|
1250
|
+
* error and is closed. The client refreshes its credentials and re-handshakes,
|
|
1251
|
+
* so the server authenticates it again: a still-valid device reconnects, a
|
|
1252
|
+
* revoked one is refused. Sessions still in their handshake are refused as soon
|
|
1253
|
+
* as their principal is known.
|
|
1254
|
+
*
|
|
1255
|
+
* `createKoraAuthServer().bindSyncServer(server)` calls this on device revoke,
|
|
1256
|
+
* sign-out, password reset or change, and admin revoke. A provider exposing
|
|
1257
|
+
* `onRevoke` (the built-in one does) is followed automatically.
|
|
1258
|
+
*
|
|
1259
|
+
* @param filter - `userId` ends every session of that user; with `deviceId`,
|
|
1260
|
+
* only that device's sessions. At least one of the two is required.
|
|
1261
|
+
* @returns The number of established sessions that were closed
|
|
1262
|
+
*
|
|
1263
|
+
* @example
|
|
1264
|
+
* ```typescript
|
|
1265
|
+
* server.terminateSessions({ userId: 'u1', deviceId: 'laptop' })
|
|
1266
|
+
* ```
|
|
1267
|
+
*/
|
|
1268
|
+
terminateSessions(filter: TerminateSessionsFilter): number;
|
|
1269
|
+
/**
|
|
1270
|
+
* Admin release of a device node id (RT-5). The next principal to handshake with
|
|
1271
|
+
* this node id claims it, even when the node already has operation history.
|
|
1272
|
+
*
|
|
1273
|
+
* Use it to hand over operation history written before node claims existed (an
|
|
1274
|
+
* upgrade from beta.12, where nobody may adopt such a node until it is released),
|
|
1275
|
+
* or to reassign a lost device's node id. Live sessions on that node id are ended
|
|
1276
|
+
* with a retriable `NODE_RELEASED` error so the handover starts clean.
|
|
1277
|
+
*
|
|
1278
|
+
* @param nodeId - The device node id to release
|
|
1279
|
+
* @returns True when the node had a claim or history to release; false when it
|
|
1280
|
+
* was unknown or the store does not support node claims
|
|
1281
|
+
*
|
|
1282
|
+
* @example
|
|
1283
|
+
* ```typescript
|
|
1284
|
+
* await server.releaseNodeClaim('0190a1b2-...')
|
|
1285
|
+
* ```
|
|
1286
|
+
*/
|
|
1287
|
+
releaseNodeClaim(nodeId: string): Promise<boolean>;
|
|
504
1288
|
private ensureBackgroundTimersStarted;
|
|
1289
|
+
/**
|
|
1290
|
+
* Re-validate the credential of every live session with the auth provider and end
|
|
1291
|
+
* the sessions whose credential is no longer accepted (RT-18).
|
|
1292
|
+
*
|
|
1293
|
+
* Revocations are persisted by the auth stores (token, device and per-user
|
|
1294
|
+
* cut-offs), but `onRevoke` listeners only run in the process that handled the
|
|
1295
|
+
* revocation. Every instance therefore re-checks its own live sessions
|
|
1296
|
+
* periodically (`sessionRevalidationIntervalMs`, default 30 seconds, also driven by
|
|
1297
|
+
* the delivery poll tick) by authenticating the session's credential again. A
|
|
1298
|
+
* session whose credential is refused, or now resolves to another principal, gets
|
|
1299
|
+
* a retriable `AUTH_REVOKED` (or `AUTH_EXPIRED`) and is closed. A provider error
|
|
1300
|
+
* (for example the auth database is unreachable) ends nothing: it is logged and the
|
|
1301
|
+
* next pass retries, so an outage does not disconnect every client.
|
|
1302
|
+
*
|
|
1303
|
+
* @returns The number of sessions ended by this pass
|
|
1304
|
+
*
|
|
1305
|
+
* @example
|
|
1306
|
+
* ```typescript
|
|
1307
|
+
* // After revoking through another instance's admin API:
|
|
1308
|
+
* await server.revalidateSessions()
|
|
1309
|
+
* ```
|
|
1310
|
+
*/
|
|
1311
|
+
revalidateSessions(): Promise<number>;
|
|
1312
|
+
/** Run {@link revalidateSessions} from the delivery poll tick once the interval elapsed. */
|
|
1313
|
+
private maybeRevalidateSessions;
|
|
505
1314
|
/**
|
|
506
1315
|
* Retransmit relay batches that connected clients have not acknowledged within
|
|
507
1316
|
* `staleMs`. Called on a periodic tick and directly by tests. Redelivering an
|
|
508
1317
|
* already-applied operation is harmless (clients dedup by content-addressed id).
|
|
509
1318
|
*/
|
|
510
1319
|
retransmitPendingRelays(staleMs?: number): void;
|
|
1320
|
+
/**
|
|
1321
|
+
* The ingest rate limiter of a device node, created on first use and shared by
|
|
1322
|
+
* every session of that node, so reconnecting does not buy a fresh budget (SRV-6).
|
|
1323
|
+
*/
|
|
1324
|
+
private rateLimiterFor;
|
|
1325
|
+
/**
|
|
1326
|
+
* Forget rate limiters of nodes with no live session whose window has long
|
|
1327
|
+
* passed: their budget would be full again anyway.
|
|
1328
|
+
*/
|
|
1329
|
+
private expireIdleRateLimiters;
|
|
1330
|
+
/** Close HTTP long-poll sessions that sent no request within the idle timeout. */
|
|
1331
|
+
private expireIdleHttpSessions;
|
|
511
1332
|
/**
|
|
512
1333
|
* Check whether the authoritative delivery log advanced without passing through
|
|
513
1334
|
* this server instance, then wake delivery-watermark sessions to scan from their
|
|
514
1335
|
* own acknowledged cursors. Session-level filtering remains the only fan-out path.
|
|
515
1336
|
*/
|
|
516
1337
|
pollDeliveryLog(): Promise<void>;
|
|
1338
|
+
/**
|
|
1339
|
+
* Invalidate cached blob access sets for the collections written between two
|
|
1340
|
+
* delivery frontiers. Falls back to dropping every set when the gap is large.
|
|
1341
|
+
*/
|
|
1342
|
+
private invalidateBlobAccessSince;
|
|
1343
|
+
/**
|
|
1344
|
+
* The blob reference authority shared by every session and route (RT-11).
|
|
1345
|
+
* @internal Used by the route context; not part of the public API.
|
|
1346
|
+
*/
|
|
1347
|
+
getBlobAccessIndex(): BlobAccessIndex;
|
|
517
1348
|
/**
|
|
518
1349
|
* Buffer relay operations a disconnected client never acknowledged, keyed by its
|
|
519
1350
|
* node id, deduped by operation id and bounded by count. Redelivered when the
|
|
@@ -556,9 +1387,40 @@ declare class KoraSyncServer {
|
|
|
556
1387
|
/**
|
|
557
1388
|
* Handle one HTTP sync request for a long-polling client.
|
|
558
1389
|
*
|
|
559
|
-
*
|
|
1390
|
+
* The POST that opens a session (the handshake, sent without `sessionId`) creates
|
|
1391
|
+
* it and answers with a server-issued, high-entropy session id in the
|
|
1392
|
+
* `x-kora-session` response header. Every later request must name that id.
|
|
1393
|
+
*
|
|
1394
|
+
* With an auth provider, EVERY request is authenticated from its `authorization`
|
|
1395
|
+
* header and must resolve to the same principal (user, device and anonymity) as
|
|
1396
|
+
* the request that opened the session and as the session's handshake, so knowing
|
|
1397
|
+
* a session id alone grants nothing (RT-2). Sessions with no request for
|
|
1398
|
+
* `httpSessionIdleTimeoutMs` are closed.
|
|
1399
|
+
*
|
|
1400
|
+
* Responses: 202 (POST accepted), 200/204/304 (GET), 400 (malformed), 401
|
|
1401
|
+
* (credential missing or invalid), 403 (credential of another principal), 404
|
|
1402
|
+
* (unknown or expired session), 405 (method), 410 (session closed).
|
|
560
1403
|
*/
|
|
561
1404
|
handleHttpRequest(request: HttpSyncRequest): Promise<HttpSyncResponse>;
|
|
1405
|
+
/**
|
|
1406
|
+
* Authenticate one HTTP request. Returns the principal identity, null when the
|
|
1407
|
+
* server has no real auth provider, or 'unauthorized'.
|
|
1408
|
+
*/
|
|
1409
|
+
private authenticateHttpRequest;
|
|
1410
|
+
/**
|
|
1411
|
+
* True when a request's principal is the one that opened the session and, once
|
|
1412
|
+
* the handshake authenticated, the session's own principal.
|
|
1413
|
+
*/
|
|
1414
|
+
private httpPrincipalMatches;
|
|
1415
|
+
/**
|
|
1416
|
+
* Handle an accepted WebSocket (attach mode with `ws`): wraps it in a
|
|
1417
|
+
* {@link WsServerTransport} configured like the standalone server's (serializer,
|
|
1418
|
+
* ping/pong liveness probing, send-buffer ceiling) and starts a session.
|
|
1419
|
+
*
|
|
1420
|
+
* @param ws - A `ws` WebSocket from `WebSocketServer.handleUpgrade` or `connection`
|
|
1421
|
+
* @returns The session ID
|
|
1422
|
+
*/
|
|
1423
|
+
handleWebSocket(ws: WsWebSocket): string;
|
|
562
1424
|
/**
|
|
563
1425
|
* Handle an incoming client connection (attach mode).
|
|
564
1426
|
* Creates a new ClientSession for the transport.
|
|
@@ -583,9 +1445,10 @@ declare class KoraSyncServer {
|
|
|
583
1445
|
* client only receives the operation if it falls within that client's scope.
|
|
584
1446
|
*
|
|
585
1447
|
* @param op - A fully-formed, server-originated operation to apply
|
|
1448
|
+
* @param options - Optional in-store authorization (used by scoped routes)
|
|
586
1449
|
* @returns The apply result, including any server-generated side-effect ops
|
|
587
1450
|
*/
|
|
588
|
-
applyLocalOperation(op: Operation): Promise<ApplyServerOperationResult>;
|
|
1451
|
+
applyLocalOperation(op: Operation, options?: ApplyServerOperationOptions): Promise<ApplyServerOperationResult>;
|
|
589
1452
|
/**
|
|
590
1453
|
* Relay already-applied, server-originated operations to every connected client.
|
|
591
1454
|
* Used by the conditional route apply, which commits its operations atomically
|
|
@@ -610,11 +1473,40 @@ declare class KoraSyncServer {
|
|
|
610
1473
|
* Get the number of currently connected clients.
|
|
611
1474
|
*/
|
|
612
1475
|
getConnectionCount(): number;
|
|
1476
|
+
/**
|
|
1477
|
+
* Node ids whose operations win `merge('server-authoritative')` fields in the fold
|
|
1478
|
+
* (W7): the store's own node id, which authors every server-originated operation
|
|
1479
|
+
* (side effects, constraint corrections, route writes), plus any extras the store
|
|
1480
|
+
* was configured with (`authoritativeNodeIds` store option). This is the one source
|
|
1481
|
+
* of truth: every session advertises exactly this list in the handshake, so clients
|
|
1482
|
+
* fold with the same authority as the server's stores.
|
|
1483
|
+
*/
|
|
1484
|
+
get authoritativeNodeIds(): string[];
|
|
1485
|
+
/**
|
|
1486
|
+
* Blob access policy (RT-1): true when the session is streaming and a live record
|
|
1487
|
+
* inside its download scope references the hash. Gates the central store.
|
|
1488
|
+
*/
|
|
1489
|
+
private sessionReferencesBlob;
|
|
1490
|
+
/** True when a session other than `exceptSessionId` is connected as `nodeId`. */
|
|
1491
|
+
private isNodeLive;
|
|
1492
|
+
/**
|
|
1493
|
+
* Central-store read policy (RT-1, RT-25): the session owns the hash (it pushed
|
|
1494
|
+
* the bytes) or a live record inside its download scope references it.
|
|
1495
|
+
*/
|
|
1496
|
+
private sessionMayReadStoredBlob;
|
|
1497
|
+
/**
|
|
1498
|
+
* Blob access policy (RT-1): a request may be forwarded to a peer that shares the
|
|
1499
|
+
* requester's exact download scope (the same tenant view, as for presence), which
|
|
1500
|
+
* keeps manifests handed over out of band working; across scopes, only when both
|
|
1501
|
+
* scopes reference the hash, so neither the hash nor the bytes cross a tenant.
|
|
1502
|
+
*/
|
|
1503
|
+
private mayForwardBlobRequest;
|
|
613
1504
|
private handleRelay;
|
|
614
1505
|
private handleSessionClose;
|
|
615
1506
|
private handleAwarenessRelay;
|
|
616
1507
|
private handleYjsDocRelay;
|
|
617
|
-
|
|
1508
|
+
/** Open a new HTTP long-poll session bound to `principal`, under a fresh random id. */
|
|
1509
|
+
private openHttpSession;
|
|
618
1510
|
}
|
|
619
1511
|
|
|
620
1512
|
/**
|
|
@@ -798,11 +1690,28 @@ interface AuthContext {
|
|
|
798
1690
|
userId: string;
|
|
799
1691
|
/** Per-collection sync scopes (optional) */
|
|
800
1692
|
scopes?: Record<string, Record<string, unknown>>;
|
|
1693
|
+
/** Server-authoritative records this session may receive. Overrides `scopes`. */
|
|
1694
|
+
downlinkScopes?: Record<string, Record<string, unknown>>;
|
|
1695
|
+
/** Server-authoritative records this session may upload. Overrides `scopes`. */
|
|
1696
|
+
uplinkScopes?: Record<string, Record<string, unknown>>;
|
|
801
1697
|
/** Arbitrary metadata about the authenticated user */
|
|
802
1698
|
metadata?: Record<string, unknown>;
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
1699
|
+
/**
|
|
1700
|
+
* True for an unauthenticated (anonymous) principal, such as the fallback of
|
|
1701
|
+
* `MixedAuthProvider`. Its `userId` is not stable across connections, so node-id
|
|
1702
|
+
* claims are keyed by the shared anonymous owner instead: an anonymous device can
|
|
1703
|
+
* reconnect with its node id, but can never take a signed-in user's node id.
|
|
1704
|
+
*/
|
|
1705
|
+
anonymous?: boolean;
|
|
1706
|
+
/**
|
|
1707
|
+
* When the credential that authenticated this session expires (ms since
|
|
1708
|
+
* epoch). A session must not outlive its credential: the sync server can
|
|
1709
|
+
* close it with a retriable AUTH_EXPIRED so the client refreshes (AUTH-11).
|
|
1710
|
+
*/
|
|
1711
|
+
expiresAt?: number;
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Interface for authenticating incoming client connections.
|
|
806
1715
|
* Implementations validate tokens and return an AuthContext on success.
|
|
807
1716
|
*/
|
|
808
1717
|
interface AuthProvider {
|
|
@@ -812,6 +1721,33 @@ interface AuthProvider {
|
|
|
812
1721
|
* @returns AuthContext if valid, null if rejected
|
|
813
1722
|
*/
|
|
814
1723
|
authenticate(token: string): Promise<AuthContext | null>;
|
|
1724
|
+
/**
|
|
1725
|
+
* Optional revocation feed. When present, `KoraSyncServer` subscribes on
|
|
1726
|
+
* construction and calls {@link KoraSyncServer.terminateSessions} for every
|
|
1727
|
+
* event, so a revoked device or user loses its live sync sessions immediately
|
|
1728
|
+
* (AUTH-11). The built-in `createKoraAuthServer().auth` provides it.
|
|
1729
|
+
*
|
|
1730
|
+
* @returns An unsubscribe function
|
|
1731
|
+
*/
|
|
1732
|
+
onRevoke?(listener: (event: SessionRevocation) => void | Promise<void>): () => void;
|
|
1733
|
+
}
|
|
1734
|
+
/**
|
|
1735
|
+
* Which live sync sessions a credential revocation ends. `userId` alone ends
|
|
1736
|
+
* every session of that user; with `deviceId` only that device's sessions
|
|
1737
|
+
* (matched against `AuthContext.metadata.deviceId`).
|
|
1738
|
+
*/
|
|
1739
|
+
interface SessionRevocation {
|
|
1740
|
+
userId?: string;
|
|
1741
|
+
deviceId?: string;
|
|
1742
|
+
}
|
|
1743
|
+
/** Options for {@link KoraSyncServer.terminateSessions}. */
|
|
1744
|
+
interface TerminateSessionsFilter extends SessionRevocation {
|
|
1745
|
+
/**
|
|
1746
|
+
* Error code sent to the client before the session closes. Both are retriable:
|
|
1747
|
+
* the client refreshes its credentials and re-handshakes, and the server then
|
|
1748
|
+
* decides again. Defaults to `'AUTH_REVOKED'`.
|
|
1749
|
+
*/
|
|
1750
|
+
code?: 'AUTH_REVOKED' | 'AUTH_EXPIRED';
|
|
815
1751
|
}
|
|
816
1752
|
/**
|
|
817
1753
|
* Configuration for creating a KoraSyncServer.
|
|
@@ -829,8 +1765,60 @@ interface KoraSyncServerConfig {
|
|
|
829
1765
|
serializer?: MessageSerializer;
|
|
830
1766
|
/** Event emitter for DevTools integration */
|
|
831
1767
|
emitter?: KoraEventEmitter;
|
|
832
|
-
/**
|
|
1768
|
+
/**
|
|
1769
|
+
* Maximum concurrent client connections (sessions, including ones still
|
|
1770
|
+
* handshaking). A connection over the limit gets a retriable `MAX_CONNECTIONS` error
|
|
1771
|
+
* and is closed. 0 = unlimited. Defaults to 10,000.
|
|
1772
|
+
*/
|
|
833
1773
|
maxConnections?: number;
|
|
1774
|
+
/**
|
|
1775
|
+
* Interval between WebSocket pings, in ms (SRV-6, LMS #12). A connection that
|
|
1776
|
+
* leaves two pings in a row unanswered is terminated, which ends its session.
|
|
1777
|
+
* Applies to the standalone server and `createProductionServer`. Defaults to 25
|
|
1778
|
+
* seconds; 0 disables probing.
|
|
1779
|
+
*/
|
|
1780
|
+
heartbeatIntervalMs?: number;
|
|
1781
|
+
/**
|
|
1782
|
+
* Interval of the application-level `heartbeat` message sent to clients that
|
|
1783
|
+
* advertise support for it (browsers cannot see WebSocket pings), in ms. A client
|
|
1784
|
+
* that hears nothing for about 2.5 intervals reconnects. Defaults to 25 seconds; 0
|
|
1785
|
+
* disables it.
|
|
1786
|
+
*/
|
|
1787
|
+
appHeartbeatIntervalMs?: number;
|
|
1788
|
+
/**
|
|
1789
|
+
* End-to-end encryption policy (protocol v2, ENC-3). With `required: true` an
|
|
1790
|
+
* uploaded plaintext operation that carries data is refused non-retriably
|
|
1791
|
+
* (`PLAINTEXT_REJECTED`), unless `allowPlaintextMigration` opens a migration window.
|
|
1792
|
+
* Encrypted operations (envelope v2) are always stored opaquely, never transformed.
|
|
1793
|
+
*/
|
|
1794
|
+
encryption?: {
|
|
1795
|
+
required: boolean;
|
|
1796
|
+
allowPlaintextMigration?: boolean;
|
|
1797
|
+
};
|
|
1798
|
+
/**
|
|
1799
|
+
* Time a new connection has to send its handshake, in ms. Defaults to 10 seconds;
|
|
1800
|
+
* 0 disables the deadline.
|
|
1801
|
+
*/
|
|
1802
|
+
handshakeTimeoutMs?: number;
|
|
1803
|
+
/**
|
|
1804
|
+
* Bytes that may wait unsent for one client (WebSocket send buffer, or the queue of
|
|
1805
|
+
* an HTTP long-poll client that stopped polling) before it is disconnected as a slow
|
|
1806
|
+
* consumer. Defaults to 32 MiB; 0 disables the ceiling. The delivery stream pauses
|
|
1807
|
+
* well before this (`deliveryHighWaterBytes`).
|
|
1808
|
+
*/
|
|
1809
|
+
maxBufferedBytes?: number;
|
|
1810
|
+
/**
|
|
1811
|
+
* Queued outbound bytes above which a client's delivery stream pauses until the
|
|
1812
|
+
* client drains them (backpressure). Defaults to 1 MiB.
|
|
1813
|
+
*/
|
|
1814
|
+
deliveryHighWaterBytes?: number;
|
|
1815
|
+
/**
|
|
1816
|
+
* WebSocket permessage-deflate compression. `true` (the default) compresses messages
|
|
1817
|
+
* of 1 KiB or more without keeping a compression context between messages, which
|
|
1818
|
+
* cuts sync payloads several-fold on 2G/3G links at a bounded memory cost per
|
|
1819
|
+
* connection. `false` disables it; an object is passed to `ws` as is.
|
|
1820
|
+
*/
|
|
1821
|
+
perMessageDeflate?: boolean | Record<string, unknown>;
|
|
834
1822
|
/** Maximum operations per sync batch. Defaults to 100. */
|
|
835
1823
|
batchSize?: number;
|
|
836
1824
|
/**
|
|
@@ -858,10 +1846,13 @@ interface KoraSyncServerConfig {
|
|
|
858
1846
|
max: number;
|
|
859
1847
|
};
|
|
860
1848
|
/**
|
|
861
|
-
*
|
|
862
|
-
*
|
|
863
|
-
*
|
|
864
|
-
*
|
|
1849
|
+
* Schema transforms from older client schema versions to the server schema version.
|
|
1850
|
+
* Required when `supportedSchemaVersions.min` is lower than `schemaVersion` and
|
|
1851
|
+
* operation shapes changed across versions. They run at fold time (RT-84): every
|
|
1852
|
+
* operation is stored and delivered exactly as uploaded; authorization, validators
|
|
1853
|
+
* and the store's fold use its transformed view. The server hands this list to its
|
|
1854
|
+
* store (`setOperationTransforms`); pass it to `store.setSchema` too to fold with it
|
|
1855
|
+
* from the first start. Transforms must be pure and deterministic.
|
|
865
1856
|
*/
|
|
866
1857
|
operationTransforms?: OperationTransform[];
|
|
867
1858
|
/** WebSocket path (standalone mode). Defaults to '/'. */
|
|
@@ -889,6 +1880,32 @@ interface KoraSyncServerConfig {
|
|
|
889
1880
|
* `@korajs/store` derives both from a `ContentAddressedBlobStore`.
|
|
890
1881
|
*/
|
|
891
1882
|
persistBlobChunk?: (hash: string, bytes: Uint8Array) => Promise<void> | void;
|
|
1883
|
+
/**
|
|
1884
|
+
* Limits on the blob side channel. `maxChunkBytes` (default 1 MiB) caps one
|
|
1885
|
+
* pushed chunk or manifest, `maxBytesPerSession` (default 256 MiB) caps the total
|
|
1886
|
+
* a session may push for central persistence, `maxPendingRequestsPerSession`
|
|
1887
|
+
* (default 256) and `pendingRequestTtlMs` (default 60s) bound the chunk requests
|
|
1888
|
+
* the relay remembers per session.
|
|
1889
|
+
*/
|
|
1890
|
+
blobLimits?: {
|
|
1891
|
+
maxChunkBytes?: number;
|
|
1892
|
+
maxBytesPerSession?: number;
|
|
1893
|
+
maxPendingRequestsPerSession?: number;
|
|
1894
|
+
pendingRequestTtlMs?: number;
|
|
1895
|
+
/**
|
|
1896
|
+
* Blob chunk requests one session may make per minute, a budget separate from
|
|
1897
|
+
* `maxOpsPerMinute` (one request per chunk, so a large blob makes many). Over
|
|
1898
|
+
* it, a request is answered with a retriable `throttled` response the client
|
|
1899
|
+
* backs off on (RT-24). Defaults to 6000.
|
|
1900
|
+
*/
|
|
1901
|
+
maxRequestsPerMinute?: number;
|
|
1902
|
+
};
|
|
1903
|
+
/**
|
|
1904
|
+
* Largest WebSocket message the standalone server (and `createProductionServer`)
|
|
1905
|
+
* accepts, in bytes. Larger frames are refused by the socket layer before they are
|
|
1906
|
+
* buffered. Defaults to 32 MiB (the `ws` library default is 100 MiB).
|
|
1907
|
+
*/
|
|
1908
|
+
maxMessageBytes?: number;
|
|
892
1909
|
/**
|
|
893
1910
|
* Maximum serialized byte size of a single client operation accepted at sync
|
|
894
1911
|
* ingest. Operations larger than this are rejected before materialization.
|
|
@@ -897,11 +1914,64 @@ interface KoraSyncServerConfig {
|
|
|
897
1914
|
*/
|
|
898
1915
|
maxOperationBytes?: number;
|
|
899
1916
|
/**
|
|
900
|
-
* Maximum operations accepted per
|
|
901
|
-
*
|
|
902
|
-
*
|
|
1917
|
+
* Maximum operations accepted per device node per minute (fixed window). The budget
|
|
1918
|
+
* belongs to the node id (bound to its principal when auth is configured), not to
|
|
1919
|
+
* the connection, so reconnecting does not reset it (SRV-6). Operations beyond the
|
|
1920
|
+
* limit get a retriable `RATE_LIMIT` until the window resets. Defaults to 600.
|
|
903
1921
|
*/
|
|
904
1922
|
maxOpsPerMinute?: number;
|
|
1923
|
+
/**
|
|
1924
|
+
* Maximum operations accepted per authenticated principal per minute, across all of
|
|
1925
|
+
* its device nodes, on top of the per-node `maxOpsPerMinute`. Without it a user could
|
|
1926
|
+
* multiply the per-node budget by minting node ids. Keyed by the provider's user id;
|
|
1927
|
+
* anonymous and unauthenticated sessions (no stable principal) have only the per-node
|
|
1928
|
+
* budget. Defaults to 4 x `maxOpsPerMinute` (2400), so one busy device never meets it
|
|
1929
|
+
* and a user's devices share it. `0` disables the per-user budget.
|
|
1930
|
+
*/
|
|
1931
|
+
maxOpsPerMinutePerUser?: number;
|
|
1932
|
+
/**
|
|
1933
|
+
* Largest operation batch accepted from a client in one message. A larger batch is
|
|
1934
|
+
* refused whole with `BATCH_TOO_LARGE` before the server decodes it or reads the
|
|
1935
|
+
* store, so one message cannot buy unbounded work. Defaults to 1000 (the client
|
|
1936
|
+
* sends batches of 100 by default).
|
|
1937
|
+
*/
|
|
1938
|
+
maxOpsPerBatch?: number;
|
|
1939
|
+
/**
|
|
1940
|
+
* Accept anonymous devices whose node claim predates confirmed claims (RT-21):
|
|
1941
|
+
* nodes whose operation history predates node claims (a database written by a
|
|
1942
|
+
* beta.12 or older server, which recorded no claims; RT-91), nodes held by the
|
|
1943
|
+
* pre-release shared anonymous owner, and provisional claims that expired without
|
|
1944
|
+
* the device ever confirming its node token (beta.12 and older clients have no
|
|
1945
|
+
* token support). Accepted with a deprecation warning and re-issued a token.
|
|
1946
|
+
* Defaults to `true` for 1.0.0-beta.13; the default flips to `false` in the next
|
|
1947
|
+
* release, after which such devices rotate to a fresh node id instead (beta.12
|
|
1948
|
+
* clients cannot rotate and stay refused).
|
|
1949
|
+
*
|
|
1950
|
+
* With `MixedAuthProvider`, a node with pre-claims history may also be a signed-in
|
|
1951
|
+
* user's beta.12 device: an anonymous device can take such a node, as it could on
|
|
1952
|
+
* beta.12. Set this to `false` if that matters more than keeping anonymous beta.12
|
|
1953
|
+
* devices syncing.
|
|
1954
|
+
*/
|
|
1955
|
+
allowLegacyAnonymousClaims?: boolean;
|
|
1956
|
+
/**
|
|
1957
|
+
* How long an anonymous device's provisional node claim may stay unconfirmed and
|
|
1958
|
+
* still be re-issued to a device that presents no token (a handshake response lost
|
|
1959
|
+
* in transit), in milliseconds (RT-21). Defaults to 24 hours.
|
|
1960
|
+
*/
|
|
1961
|
+
anonymousClaimTtlMs?: number;
|
|
1962
|
+
/**
|
|
1963
|
+
* How often every live session's credential is re-validated with the auth
|
|
1964
|
+
* provider, in milliseconds (RT-18). A revocation persisted by another server
|
|
1965
|
+
* instance ends the session here within this interval. Defaults to 30 seconds; 0
|
|
1966
|
+
* disables it (sessions then end only through this process's `onRevoke` feed,
|
|
1967
|
+
* `terminateSessions`, or credential expiry).
|
|
1968
|
+
*/
|
|
1969
|
+
sessionRevalidationIntervalMs?: number;
|
|
1970
|
+
/**
|
|
1971
|
+
* How long an HTTP long-poll session may go without any request before the server
|
|
1972
|
+
* closes it, in milliseconds. Defaults to 2 minutes; 0 disables expiry.
|
|
1973
|
+
*/
|
|
1974
|
+
httpSessionIdleTimeoutMs?: number;
|
|
905
1975
|
/**
|
|
906
1976
|
* Adjudicate untrusted client operations before they become authoritative.
|
|
907
1977
|
*
|
|
@@ -917,12 +1987,25 @@ interface KoraSyncServerConfig {
|
|
|
917
1987
|
}
|
|
918
1988
|
/**
|
|
919
1989
|
* Request envelope for the server-side HTTP sync endpoint.
|
|
1990
|
+
*
|
|
1991
|
+
* Map it from your HTTP framework: `sessionId` from the `x-kora-session` header and
|
|
1992
|
+
* `authorization` from the `Authorization` header, on every request.
|
|
920
1993
|
*/
|
|
921
1994
|
interface HttpSyncRequest {
|
|
922
|
-
/** Stable client identifier for binding HTTP requests to a server session */
|
|
923
|
-
clientId: string;
|
|
924
1995
|
/** HTTP method */
|
|
925
1996
|
method: 'GET' | 'POST';
|
|
1997
|
+
/**
|
|
1998
|
+
* The server-issued session id (`x-kora-session` request header). Absent only on
|
|
1999
|
+
* the POST that opens a session (the handshake); the response to that POST
|
|
2000
|
+
* carries the new id in its `x-kora-session` header. Never chosen by the client.
|
|
2001
|
+
*/
|
|
2002
|
+
sessionId?: string;
|
|
2003
|
+
/**
|
|
2004
|
+
* The raw `Authorization` header (`Bearer <token>`). With an auth provider
|
|
2005
|
+
* configured, EVERY request is authenticated and must resolve to the same
|
|
2006
|
+
* principal and device as the session it names (RT-2).
|
|
2007
|
+
*/
|
|
2008
|
+
authorization?: string;
|
|
926
2009
|
/** Optional raw request payload for POST */
|
|
927
2010
|
body?: string | Uint8Array;
|
|
928
2011
|
/** Value of the Content-Type header for POST payloads */
|
|
@@ -935,7 +2018,7 @@ interface HttpSyncRequest {
|
|
|
935
2018
|
*/
|
|
936
2019
|
interface HttpSyncResponse {
|
|
937
2020
|
/** HTTP status code */
|
|
938
|
-
status: 200 | 202 | 204 | 304 | 400 | 405 | 410;
|
|
2021
|
+
status: 200 | 202 | 204 | 304 | 400 | 401 | 403 | 404 | 405 | 410;
|
|
939
2022
|
/** Optional raw response payload */
|
|
940
2023
|
body?: string | Uint8Array;
|
|
941
2024
|
/** Optional response headers */
|
|
@@ -982,6 +2065,20 @@ interface ServerStatus {
|
|
|
982
2065
|
errorCount: number;
|
|
983
2066
|
}
|
|
984
2067
|
|
|
2068
|
+
/**
|
|
2069
|
+
* Which reverse proxies in front of the server are trusted to report the client
|
|
2070
|
+
* address in `X-Forwarded-For`.
|
|
2071
|
+
*
|
|
2072
|
+
* - `number`: trust that many hops, counting the socket peer as the first. `1` means
|
|
2073
|
+
* "one proxy (for example a load balancer) sits directly in front of the server".
|
|
2074
|
+
* - `string[]`: trust peers whose address is in one of these IPs or CIDR ranges
|
|
2075
|
+
* (IPv4 and IPv6, for example `['10.0.0.0/8', '::1']`).
|
|
2076
|
+
*
|
|
2077
|
+
* When unset, `X-Forwarded-For` is ignored and the socket address is the client
|
|
2078
|
+
* address, so a directly connected client cannot choose its own address.
|
|
2079
|
+
*/
|
|
2080
|
+
type TrustProxySetting = number | string[];
|
|
2081
|
+
|
|
985
2082
|
interface HttpPollResponse {
|
|
986
2083
|
status: 200 | 204 | 304 | 410;
|
|
987
2084
|
body?: string | Uint8Array;
|
|
@@ -1001,56 +2098,104 @@ declare class HttpServerTransport implements ServerTransport {
|
|
|
1001
2098
|
private connected;
|
|
1002
2099
|
private nextSequence;
|
|
1003
2100
|
private readonly queue;
|
|
1004
|
-
|
|
2101
|
+
private queuedBytes;
|
|
2102
|
+
private readonly maxQueuedBytes;
|
|
2103
|
+
/**
|
|
2104
|
+
* @param serializer - Message serializer
|
|
2105
|
+
* @param options - `maxQueuedBytes`: close the session once this many bytes wait
|
|
2106
|
+
* unpolled (default 32 MiB; 0 disables the ceiling)
|
|
2107
|
+
*/
|
|
2108
|
+
constructor(serializer: MessageSerializer, options?: {
|
|
2109
|
+
maxQueuedBytes?: number;
|
|
2110
|
+
});
|
|
1005
2111
|
send(message: _korajs_sync.SyncMessage): void;
|
|
2112
|
+
/** Bytes queued for the client and not yet polled (delivery-stream backpressure). */
|
|
2113
|
+
bufferedAmount(): number;
|
|
1006
2114
|
onMessage(handler: ServerMessageHandler): void;
|
|
1007
2115
|
onClose(handler: ServerCloseHandler): void;
|
|
1008
2116
|
onError(handler: ServerErrorHandler): void;
|
|
1009
2117
|
isConnected(): boolean;
|
|
2118
|
+
/**
|
|
2119
|
+
* Close the session. Messages already queued (typically the final error that
|
|
2120
|
+
* explains the close, for example a refused handshake) stay pollable, so a
|
|
2121
|
+
* long-polling client learns why, exactly as a WebSocket client receives the error
|
|
2122
|
+
* frame before the close; once drained, polls answer 410.
|
|
2123
|
+
*/
|
|
1010
2124
|
close(code?: number, reason?: string): void;
|
|
2125
|
+
/** True while messages queued before the close are still waiting to be polled. */
|
|
2126
|
+
hasPending(): boolean;
|
|
1011
2127
|
receive(payload: string | Uint8Array): void;
|
|
1012
2128
|
poll(ifNoneMatch?: string): HttpPollResponse;
|
|
1013
2129
|
private makeEtag;
|
|
1014
2130
|
}
|
|
1015
2131
|
|
|
1016
|
-
/**
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
*/
|
|
1020
|
-
|
|
1021
|
-
readyState: number;
|
|
1022
|
-
send(data: string | Uint8Array, callback?: (err?: Error) => void): void;
|
|
1023
|
-
close(code?: number, reason?: string): void;
|
|
1024
|
-
on(event: string, listener: (...args: unknown[]) => void): void;
|
|
1025
|
-
removeAllListeners(): void;
|
|
2132
|
+
/** Outcome of one key-service message. */
|
|
2133
|
+
interface KeyServiceOutcome {
|
|
2134
|
+
response: EncryptionKeyResponseMessage;
|
|
2135
|
+
/** The record written, when the message was a successful write (to push to siblings). */
|
|
2136
|
+
written: WrappedKeyRecord | null;
|
|
1026
2137
|
}
|
|
1027
2138
|
/**
|
|
1028
|
-
*
|
|
2139
|
+
* The sync server's key service (ENC-1, decision D4b). Stores each owner's wrapped key
|
|
2140
|
+
* record and serves it back. It never sees a passphrase or a data key: a record is
|
|
2141
|
+
* salt, KDF parameters, AES-GCM-wrapped data keys, key ids and versions.
|
|
2142
|
+
*
|
|
2143
|
+
* Rules it enforces:
|
|
2144
|
+
* - **Owner from the session only.** The owner is the session's authenticated
|
|
2145
|
+
* principal (or the shared anonymous owner on a server without auth); a message
|
|
2146
|
+
* cannot name a user, so no session reads or writes another user's record.
|
|
2147
|
+
* - **Compare-and-set.** A write names the revision it replaces; a concurrent write
|
|
2148
|
+
* loses with `conflict` and the current record, so two devices that both create a
|
|
2149
|
+
* first key converge on one.
|
|
2150
|
+
* - **History is append-only.** A write must raise the revision, keep the ring id and
|
|
2151
|
+
* every key version (same key id), so no device can make old operations unreadable
|
|
2152
|
+
* for the others. A first write (or a re-upload after the record was lost) keeps the
|
|
2153
|
+
* revision it carries, since devices refuse revisions below the one they accepted.
|
|
2154
|
+
* - **Lost records are visible.** A fetch with no record reports key ids of the owner's
|
|
2155
|
+
* stored encrypted operations, so a new device waits for one holding the ring.
|
|
2156
|
+
*
|
|
2157
|
+
* It cannot check the record MAC or that wraps open (only a holder of the ring's master
|
|
2158
|
+
* key can): devices authenticate every record themselves and act on nothing else. A
|
|
2159
|
+
* principal can still overwrite its own record with garbage; devices refuse it and
|
|
2160
|
+
* keep their keys.
|
|
1029
2161
|
*/
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
2162
|
+
declare class EncryptionKeyService {
|
|
2163
|
+
private readonly store;
|
|
2164
|
+
constructor(store: ServerStore);
|
|
2165
|
+
/** Whether the store can persist key records (all built-in stores can). */
|
|
2166
|
+
isSupported(): boolean;
|
|
2167
|
+
/**
|
|
2168
|
+
* Answer one key-service message for `owner`.
|
|
2169
|
+
*
|
|
2170
|
+
* @param owner - The session's owner key, or null when the session may not hold keys
|
|
2171
|
+
* @param message - A request or a write
|
|
2172
|
+
*/
|
|
2173
|
+
handle(owner: string | null, message: EncryptionKeyRequestMessage | EncryptionKeyPutMessage): Promise<KeyServiceOutcome>;
|
|
2174
|
+
/**
|
|
2175
|
+
* Key ids of the owner's stored encrypted operations (a sample). Reported only while
|
|
2176
|
+
* no record exists: then non-empty means the record was lost, and a new device must
|
|
2177
|
+
* wait for one that holds the ring instead of starting another (RT-104).
|
|
2178
|
+
*/
|
|
2179
|
+
private knownKeyIds;
|
|
2180
|
+
private read;
|
|
1033
2181
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
*/
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
private errorHandler;
|
|
1044
|
-
constructor(ws: WsWebSocket, options?: WsServerTransportOptions);
|
|
1045
|
-
send(message: SyncMessage): void;
|
|
1046
|
-
onMessage(handler: ServerMessageHandler): void;
|
|
1047
|
-
onClose(handler: ServerCloseHandler): void;
|
|
1048
|
-
onError(handler: ServerErrorHandler): void;
|
|
1049
|
-
isConnected(): boolean;
|
|
1050
|
-
close(code?: number, reason?: string): void;
|
|
1051
|
-
private setupListeners;
|
|
2182
|
+
|
|
2183
|
+
/** What a session charges uploaded operations to. */
|
|
2184
|
+
interface IngestRateLimiter {
|
|
2185
|
+
/** Units allowed per window (the tightest applicable budget). */
|
|
2186
|
+
readonly limit: number;
|
|
2187
|
+
/** Charge `count` units; false when a budget is exceeded (the units are refused). */
|
|
2188
|
+
allow(count?: number): boolean;
|
|
2189
|
+
/** Milliseconds until a refused unit may be retried. */
|
|
2190
|
+
retryAfterMs(): number;
|
|
1052
2191
|
}
|
|
1053
2192
|
|
|
2193
|
+
/**
|
|
2194
|
+
* Session-ending error codes, all retriable. `AUTH_REVOKED` / `AUTH_EXPIRED`: the
|
|
2195
|
+
* client refreshes its credential and re-handshakes. `SCOPE_CHANGED` (RT-26): the
|
|
2196
|
+
* principal's grant changed; the client simply reconnects and gets the new scope.
|
|
2197
|
+
*/
|
|
2198
|
+
type SessionTerminationCode = 'AUTH_REVOKED' | 'AUTH_EXPIRED' | 'SCOPE_CHANGED';
|
|
1054
2199
|
/**
|
|
1055
2200
|
* Possible states for a client session.
|
|
1056
2201
|
*/
|
|
@@ -1066,7 +2211,7 @@ type AwarenessRelayCallback = (sourceSessionId: string, message: AwarenessUpdate
|
|
|
1066
2211
|
/**
|
|
1067
2212
|
* Callback invoked when a session receives a Yjs doc channel update to relay.
|
|
1068
2213
|
*/
|
|
1069
|
-
type YjsDocRelayCallback = (sourceSessionId: string, message: YjsDocUpdateMessage) => void;
|
|
2214
|
+
type YjsDocRelayCallback = (sourceSessionId: string, message: YjsDocUpdateMessage, storedRecord: MaterializedRecord | null) => void;
|
|
1070
2215
|
/**
|
|
1071
2216
|
* Callback invoked when a session receives a blob chunk request to route.
|
|
1072
2217
|
*/
|
|
@@ -1108,7 +2253,7 @@ interface ClientSessionOptions {
|
|
|
1108
2253
|
min: number;
|
|
1109
2254
|
max: number;
|
|
1110
2255
|
};
|
|
1111
|
-
/**
|
|
2256
|
+
/** Schema transforms: judged views of older operations (transforms at fold time, RT-84). */
|
|
1112
2257
|
operationTransforms?: OperationTransform[];
|
|
1113
2258
|
/** Called when this session has operations to relay to other sessions */
|
|
1114
2259
|
onRelay?: RelayCallback;
|
|
@@ -1120,8 +2265,24 @@ interface ClientSessionOptions {
|
|
|
1120
2265
|
onBlobChunkRequest?: BlobChunkRequestCallback;
|
|
1121
2266
|
/** Called when this session receives a blob chunk response to route back */
|
|
1122
2267
|
onBlobChunkResponse?: BlobChunkResponseCallback;
|
|
2268
|
+
/** The key service storing wrapped encryption key records (ENC-1). */
|
|
2269
|
+
encryptionKeys?: EncryptionKeyService;
|
|
2270
|
+
/** Called after this session wrote a key record, to push it to the owner's other sessions. */
|
|
2271
|
+
onEncryptionKeyWritten?: (sessionId: string, owner: string, keyring: string, record: WrappedKeyRecord) => void;
|
|
2272
|
+
/** Key-service messages allowed per session per minute. Defaults to 60. */
|
|
2273
|
+
maxKeyRequestsPerMinute?: number;
|
|
1123
2274
|
/** Persist a client-uploaded blob chunk centrally (keyed by content hash) */
|
|
1124
2275
|
persistBlobChunk?: PersistBlobChunk;
|
|
2276
|
+
/**
|
|
2277
|
+
* Called once the session completes an accepted handshake and reaches streaming.
|
|
2278
|
+
* Side channels (Yjs doc, blob and awareness relays) register the session here,
|
|
2279
|
+
* never at connect time, so an unauthenticated connection is never on a relay.
|
|
2280
|
+
*/
|
|
2281
|
+
onReady?: (sessionId: string) => void;
|
|
2282
|
+
/** Largest single blob chunk this session may push. Defaults to 1 MiB. */
|
|
2283
|
+
maxBlobChunkBytes?: number;
|
|
2284
|
+
/** Total blob bytes this session may push. Defaults to 256 MiB. */
|
|
2285
|
+
maxBlobBytesPerSession?: number;
|
|
1125
2286
|
/** Called when this session closes */
|
|
1126
2287
|
onClose?: (sessionId: string) => void;
|
|
1127
2288
|
/**
|
|
@@ -1141,6 +2302,18 @@ interface ClientSessionOptions {
|
|
|
1141
2302
|
maxOperationBytes?: number;
|
|
1142
2303
|
/** Maximum operations accepted per minute for this session. Defaults to 600. */
|
|
1143
2304
|
maxOpsPerMinute?: number;
|
|
2305
|
+
/**
|
|
2306
|
+
* Blob chunk requests accepted per minute, separate from the operation budget
|
|
2307
|
+
* (RT-24). Defaults to 6000.
|
|
2308
|
+
*/
|
|
2309
|
+
maxBlobRequestsPerMinute?: number;
|
|
2310
|
+
/** Length of the blob request window in ms. One minute; shorter only in tests. */
|
|
2311
|
+
blobRequestWindowMs?: number;
|
|
2312
|
+
/**
|
|
2313
|
+
* Largest operation batch accepted in one message. A larger batch is refused whole
|
|
2314
|
+
* (`BATCH_TOO_LARGE`) before it is decoded or the store is read. Defaults to 1000.
|
|
2315
|
+
*/
|
|
2316
|
+
maxOpsPerBatch?: number;
|
|
1144
2317
|
/**
|
|
1145
2318
|
* Adjudicate untrusted client operations before materialization. When present,
|
|
1146
2319
|
* each incoming operation is passed to this validator; a `reject` decision
|
|
@@ -1149,6 +2322,67 @@ interface ClientSessionOptions {
|
|
|
1149
2322
|
validateOperation?: OperationValidator;
|
|
1150
2323
|
/** Trusted data-plane context handed to the validator (read state, author derived ops). */
|
|
1151
2324
|
koraContext?: ProductionHttpRouteContext;
|
|
2325
|
+
/**
|
|
2326
|
+
* Blob reference authority (RT-11): decides which content hashes this session may
|
|
2327
|
+
* reference in blob fields and records the bytes it pushes. Without it, blob
|
|
2328
|
+
* references are not checked at ingest.
|
|
2329
|
+
*/
|
|
2330
|
+
blobAccess?: BlobAccessIndex;
|
|
2331
|
+
/** See `KoraSyncServerConfig.allowLegacyAnonymousClaims` (RT-21). Defaults to true. */
|
|
2332
|
+
allowLegacyAnonymousClaims?: boolean;
|
|
2333
|
+
/** See `KoraSyncServerConfig.anonymousClaimTtlMs` (RT-21). Defaults to 24 hours. */
|
|
2334
|
+
anonymousClaimTtlMs?: number;
|
|
2335
|
+
/**
|
|
2336
|
+
* True when another live session (not `exceptSessionId`) is connected as `nodeId`.
|
|
2337
|
+
* A provisional anonymous claim is never re-issued away from a connected device.
|
|
2338
|
+
*/
|
|
2339
|
+
isNodeLive?: (nodeId: string, exceptSessionId: string) => boolean;
|
|
2340
|
+
/**
|
|
2341
|
+
* Time a new connection has to deliver its handshake, in ms (SRV-6). A connection
|
|
2342
|
+
* that sends none is closed with `HANDSHAKE_TIMEOUT`. Defaults to 10 seconds; 0
|
|
2343
|
+
* disables the deadline.
|
|
2344
|
+
*/
|
|
2345
|
+
handshakeTimeoutMs?: number;
|
|
2346
|
+
/**
|
|
2347
|
+
* Queued outbound bytes above which the delivery stream pauses until the client
|
|
2348
|
+
* drains its socket (transports that report `bufferedAmount`). Defaults to 1 MiB.
|
|
2349
|
+
*/
|
|
2350
|
+
deliveryHighWaterBytes?: number;
|
|
2351
|
+
/**
|
|
2352
|
+
* The ingest rate limiter for a node, shared across that node's sessions so a
|
|
2353
|
+
* reconnect does not reset it (SRV-6). Called once the handshake binds the node,
|
|
2354
|
+
* with the authenticated principal's user id (null for anonymous and
|
|
2355
|
+
* unauthenticated sessions) so a per-user budget can be layered on top: a user
|
|
2356
|
+
* minting node ids must not multiply the per-node budget.
|
|
2357
|
+
* Without it the session keeps a private limiter.
|
|
2358
|
+
*/
|
|
2359
|
+
rateLimiterFor?: (nodeId: string, principal: string | null) => IngestRateLimiter;
|
|
2360
|
+
/**
|
|
2361
|
+
* Interval of the application-level heartbeat sent to clients that advertised
|
|
2362
|
+
* support for it, in ms (LMS #12). The handshake response tells the client the
|
|
2363
|
+
* interval so it can declare the connection dead after missing two. 0 disables it.
|
|
2364
|
+
*/
|
|
2365
|
+
appHeartbeatIntervalMs?: number;
|
|
2366
|
+
/**
|
|
2367
|
+
* Node ids whose operations this server authors (protocol v2). Sent to clients in
|
|
2368
|
+
* the handshake response (`authoritativeNodeIds`); only operations from these nodes
|
|
2369
|
+
* may carry server-authored metadata (`fieldVersions`, `foldState`). Defaults to the
|
|
2370
|
+
* ids the store folds with (`ServerStore.getAuthoritativeNodeIds`), which is what
|
|
2371
|
+
* `KoraSyncServer` always passes. Scope-entry operations (`kora:scope-entry`) are not
|
|
2372
|
+
* listed: they carry the server's fold state and are joined, never folded as writes.
|
|
2373
|
+
*/
|
|
2374
|
+
authoritativeNodeIds?: readonly string[];
|
|
2375
|
+
/**
|
|
2376
|
+
* End-to-end encryption policy (protocol v2, ENC-3). With `required`, every uploaded
|
|
2377
|
+
* data-bearing operation must carry the encryption envelope; a plaintext one is
|
|
2378
|
+
* refused non-retriably (`PLAINTEXT_REJECTED`), unless `allowPlaintextMigration` is
|
|
2379
|
+
* set for a migration window. Without it, the server stores whatever it receives
|
|
2380
|
+
* (envelope operations always opaquely).
|
|
2381
|
+
*/
|
|
2382
|
+
encryption?: {
|
|
2383
|
+
required: boolean;
|
|
2384
|
+
allowPlaintextMigration?: boolean;
|
|
2385
|
+
};
|
|
1152
2386
|
}
|
|
1153
2387
|
/**
|
|
1154
2388
|
* Handles the sync protocol for a single connected client.
|
|
@@ -1168,7 +2402,31 @@ declare class ClientSession {
|
|
|
1168
2402
|
private state;
|
|
1169
2403
|
private clientNodeId;
|
|
1170
2404
|
private authContext;
|
|
2405
|
+
/**
|
|
2406
|
+
* The context the auth provider returned, kept apart from {@link authContext}
|
|
2407
|
+
* (which an unauthenticated session also gets, keyed by node id) so revocation
|
|
2408
|
+
* only ever matches a verified principal.
|
|
2409
|
+
*/
|
|
2410
|
+
private principal;
|
|
2411
|
+
/** Closes the session when its credential expires (AUTH-11). */
|
|
2412
|
+
private expiryTimer;
|
|
2413
|
+
/** Revocations that arrived while the handshake was still authenticating. */
|
|
2414
|
+
private readonly pendingRevocations;
|
|
2415
|
+
/** Node token issued by this handshake's first anonymous claim, sent once in the response (RT-12). */
|
|
2416
|
+
private issuedNodeToken;
|
|
2417
|
+
/** The node-claim owner this session holds its node id under (userId, or an anonymous device key). */
|
|
2418
|
+
private nodeOwnerKey;
|
|
2419
|
+
/** A provisional anonymous claim awaiting the device's confirmation (RT-21). */
|
|
2420
|
+
private pendingNodeClaim;
|
|
2421
|
+
private readonly allowLegacyAnonymousClaims;
|
|
2422
|
+
private readonly anonymousClaimTtlMs;
|
|
2423
|
+
private readonly isNodeLive;
|
|
2424
|
+
/** The credential presented at handshake, kept to re-validate the session (RT-18). */
|
|
2425
|
+
private credential;
|
|
2426
|
+
/** The scope the client asked for at handshake, kept to re-resolve scopes (RT-26). */
|
|
2427
|
+
private handshakeScope;
|
|
1171
2428
|
private syncQuerySubsets;
|
|
2429
|
+
private scopeExitPolicy;
|
|
1172
2430
|
private resumeDeltaCursor;
|
|
1173
2431
|
/**
|
|
1174
2432
|
* The delivery watermark the client reported at handshake, or null when the client
|
|
@@ -1179,23 +2437,70 @@ declare class ClientSession {
|
|
|
1179
2437
|
private clientDeliveryWatermark;
|
|
1180
2438
|
/**
|
|
1181
2439
|
* The highest delivery sequence this client has ACKNOWLEDGED (its confirmed
|
|
1182
|
-
* watermark, as reported in acks).
|
|
1183
|
-
*
|
|
1184
|
-
*
|
|
1185
|
-
*
|
|
1186
|
-
* is no sent-but-unacked window that a buffer eviction could strand. Seeded from the
|
|
1187
|
-
* client's reported watermark at handshake.
|
|
2440
|
+
* watermark, as reported in acks). A retransmission rewinds the send cursor to here,
|
|
2441
|
+
* so a dropped or unapplied batch is always re-included by a re-scan from the
|
|
2442
|
+
* acknowledged position: recovery never depends on a bounded retransmit buffer.
|
|
2443
|
+
* Seeded from the client's reported watermark at handshake.
|
|
1188
2444
|
*/
|
|
1189
2445
|
private lastAckedDeliverySeq;
|
|
1190
2446
|
/**
|
|
1191
|
-
*
|
|
1192
|
-
*
|
|
1193
|
-
*
|
|
2447
|
+
* The delivery send cursor (SRV-3): the max of the last batch sent. Live pushes
|
|
2448
|
+
* chain from it (base = lastSent), so each operation goes out once per retransmit
|
|
2449
|
+
* epoch instead of the whole unacknowledged backlog on every write. Invariant:
|
|
2450
|
+
* lastAcked <= lastSent. It rewinds to lastAcked only when the outstanding delivery
|
|
2451
|
+
* went unacknowledged for the (backed-off) stale window.
|
|
2452
|
+
*/
|
|
2453
|
+
private lastSentDeliverySeq;
|
|
2454
|
+
/** The highest batch max ever sent to this client (acks are capped by it). */
|
|
2455
|
+
private highestSentDeliverySeq;
|
|
2456
|
+
/**
|
|
2457
|
+
* Own-node operations at or below this delivery sequence are streamed to this
|
|
2458
|
+
* client; above it they are skipped (it uploaded them itself). Set at handshake: a
|
|
2459
|
+
* full resync (watermark 0) recovers the client's own history up to the frontier
|
|
2460
|
+
* at that moment; a resume excludes all of them.
|
|
2461
|
+
*/
|
|
2462
|
+
private ownOperationsIncludedThrough;
|
|
2463
|
+
/** When the delivery last made progress (an ack advanced, or a fresh send/rewind). */
|
|
2464
|
+
private lastDeliveryProgressAtMs;
|
|
2465
|
+
/** Consecutive rewinds without acknowledgment progress (drives the backoff, LMS #12). */
|
|
2466
|
+
private deliveryRewinds;
|
|
2467
|
+
/**
|
|
2468
|
+
* Sent, not yet acknowledged delivery batches in send order (their max and send
|
|
2469
|
+
* time), for round-trip samples. `resent` marks a re-sent range, which yields no
|
|
2470
|
+
* sample (Karn's rule: its ack cannot be matched to one transmission).
|
|
1194
2471
|
*/
|
|
1195
|
-
private
|
|
1196
|
-
private
|
|
1197
|
-
|
|
2472
|
+
private readonly unackedBatches;
|
|
2473
|
+
private smoothedRttMs;
|
|
2474
|
+
private rttVarianceMs;
|
|
2475
|
+
/**
|
|
2476
|
+
* Retransmit timeout of the delivery stream (RFC 6298 style: smoothed round trip
|
|
2477
|
+
* plus four deviations, floored). An outstanding delivery with no acknowledgment
|
|
2478
|
+
* progress for this long (doubled per consecutive re-send) is re-sent from the
|
|
2479
|
+
* acknowledged position. Round trips include the client's apply time, so a slow
|
|
2480
|
+
* device earns a longer timeout instead of spurious re-sends.
|
|
2481
|
+
*/
|
|
2482
|
+
private retransmitTimeoutMs;
|
|
2483
|
+
private retransmitTimer;
|
|
2484
|
+
/** A delivery push is queued but not started: further wake-ups coalesce into it. */
|
|
2485
|
+
private deliveryPushQueued;
|
|
2486
|
+
/** Serializes delivery pushes so their batches never interleave. */
|
|
1198
2487
|
private deliveryPushChain;
|
|
2488
|
+
/**
|
|
2489
|
+
* Records prefetched for the delivery chunk being filtered (LMS #11: one batched
|
|
2490
|
+
* lookup per collection per chunk instead of one query per operation). Keyed
|
|
2491
|
+
* `collection\u0000id`; null marks a record known to be absent. Only set while a
|
|
2492
|
+
* chunk is filtered.
|
|
2493
|
+
*/
|
|
2494
|
+
private recordLookupCache;
|
|
2495
|
+
private readonly deliveryHighWaterBytes;
|
|
2496
|
+
private readonly handshakeTimeoutMs;
|
|
2497
|
+
private handshakeTimer;
|
|
2498
|
+
private handshakeReceived;
|
|
2499
|
+
private readonly rateLimiterFor;
|
|
2500
|
+
private readonly appHeartbeatIntervalMs;
|
|
2501
|
+
private appHeartbeatTimer;
|
|
2502
|
+
/** Last time anything was sent to the client (a heartbeat is skipped when recent). */
|
|
2503
|
+
private lastClientSendAtMs;
|
|
1199
2504
|
/**
|
|
1200
2505
|
* Relay batches sent to this client but not yet acknowledged, keyed by messageId.
|
|
1201
2506
|
* The client acks every operation-batch it applies; on ack the entry is cleared.
|
|
@@ -1222,15 +2527,55 @@ declare class ClientSession {
|
|
|
1222
2527
|
private readonly onYjsDocUpdate;
|
|
1223
2528
|
private readonly onBlobChunkRequest;
|
|
1224
2529
|
private readonly onBlobChunkResponse;
|
|
2530
|
+
private readonly encryptionKeys;
|
|
2531
|
+
private readonly onEncryptionKeyWritten;
|
|
2532
|
+
/** Budget of key-service messages (each may cost a store read and write). */
|
|
2533
|
+
private readonly keyRateLimiter;
|
|
1225
2534
|
private readonly persistBlobChunk;
|
|
1226
2535
|
private readonly onClose;
|
|
2536
|
+
private readonly onReady;
|
|
2537
|
+
private readonly maxBlobChunkBytes;
|
|
2538
|
+
private readonly maxBlobBytesPerSession;
|
|
2539
|
+
private blobBytesPushed;
|
|
1227
2540
|
private readonly onOrphanedRelays;
|
|
1228
2541
|
private readonly takeOrphanedRelays;
|
|
1229
2542
|
private readonly maxOperationBytes;
|
|
1230
2543
|
private readonly maxOpsPerMinute;
|
|
1231
|
-
private readonly
|
|
2544
|
+
private readonly maxOpsPerBatch;
|
|
2545
|
+
private rateLimiter;
|
|
2546
|
+
/** Separate budget for blob chunk requests (RT-24). */
|
|
2547
|
+
private readonly blobRateLimiter;
|
|
2548
|
+
/** Operations refused by the rate limiter (RT-6), for diagnostics. */
|
|
2549
|
+
private rateLimitedOperations;
|
|
2550
|
+
/** Batches refused whole for exceeding {@link maxOpsPerBatch} (RT-6). */
|
|
2551
|
+
private rejectedBatches;
|
|
2552
|
+
/** Blob chunk requests answered "not held" because the session was over budget (RT-17). */
|
|
2553
|
+
private rateLimitedBlobRequests;
|
|
2554
|
+
/**
|
|
2555
|
+
* The client advertised the `sequenceReservation` handshake capability (RT-37): it
|
|
2556
|
+
* reserves sequence numbers in-transaction, so a second operation under a held
|
|
2557
|
+
* (node, sequence) is refused with SEQUENCE_CONFLICT. False for a legacy client
|
|
2558
|
+
* (Kora <= beta.12), whose duplicate pairs are stored instead.
|
|
2559
|
+
*/
|
|
2560
|
+
private sequenceReservation;
|
|
2561
|
+
/** Legacy duplicate pairs this session stored (RT-37), for diagnostics. */
|
|
2562
|
+
private legacySequencePairs;
|
|
2563
|
+
/** Protocol version the client declared in its handshake (1 when absent). */
|
|
2564
|
+
private clientProtocolVersion;
|
|
2565
|
+
private readonly authoritativeNodeIds;
|
|
2566
|
+
/** Hash version an undeclared uploaded id was verified as (RT-64), by operation object. */
|
|
2567
|
+
private readonly matchedHashVersions;
|
|
2568
|
+
/** Uploads whose data is stored as the beta.12 writer held it (RT-71). */
|
|
2569
|
+
private readonly restoredLegacyData;
|
|
2570
|
+
/** Operations accepted with an unverified legacy (beta.12) id (RT-71). */
|
|
2571
|
+
private unverifiedLegacyOperations;
|
|
2572
|
+
private forgedDuplicates;
|
|
2573
|
+
private readonly encryptionPolicy;
|
|
2574
|
+
/** Uploaded operations refused because their id is not their content hash (CORE-1). */
|
|
2575
|
+
private invalidOperationIds;
|
|
1232
2576
|
private readonly validateOperation;
|
|
1233
2577
|
private readonly koraContext;
|
|
2578
|
+
private readonly blobAccess;
|
|
1234
2579
|
constructor(options: ClientSessionOptions);
|
|
1235
2580
|
/**
|
|
1236
2581
|
* Start handling messages from the client transport.
|
|
@@ -1242,9 +2587,13 @@ declare class ClientSession {
|
|
|
1242
2587
|
*/
|
|
1243
2588
|
relayOperations(operations: Operation[]): void;
|
|
1244
2589
|
/**
|
|
1245
|
-
* Push newly-available in-scope operations to a delivery-watermark client,
|
|
1246
|
-
* from the
|
|
1247
|
-
*
|
|
2590
|
+
* Push newly-available in-scope operations to a delivery-watermark client, chained
|
|
2591
|
+
* from the send cursor (SRV-3): only what was not sent yet goes out. `rewind`
|
|
2592
|
+
* first moves the cursor back to the acknowledged position (a retransmission), and
|
|
2593
|
+
* `initial` is the handshake stream (it always ends in a final batch, even an empty
|
|
2594
|
+
* one, so the client completes initial sync). Pushes are serialized so their batches
|
|
2595
|
+
* never interleave (which would break the base/max chain); wake-ups that arrive
|
|
2596
|
+
* while one is queued coalesce into it.
|
|
1248
2597
|
*/
|
|
1249
2598
|
private pushDeliveryStream;
|
|
1250
2599
|
private relayVisibleOperations;
|
|
@@ -1254,17 +2603,63 @@ declare class ClientSession {
|
|
|
1254
2603
|
* Retransmit relay batches this client has not acknowledged within `staleMs`.
|
|
1255
2604
|
* Called on a periodic tick by the server (and directly by tests). Redelivering an
|
|
1256
2605
|
* already-applied op is harmless: the client dedups by content-addressed id.
|
|
2606
|
+
*
|
|
2607
|
+
* For a delivery-watermark client it re-sends an outstanding (sent, unacknowledged)
|
|
2608
|
+
* delivery from the acknowledged position, under the same backed-off window as the
|
|
2609
|
+
* retransmit timer; `staleMs` 0 re-sends at once (a deterministic trigger for tests).
|
|
1257
2610
|
*/
|
|
1258
2611
|
retransmitPendingRelays(staleMs?: number): void;
|
|
1259
2612
|
/**
|
|
1260
2613
|
* Wake the durable delivery stream for clients that negotiated delivery
|
|
1261
|
-
* watermarks
|
|
1262
|
-
*
|
|
1263
|
-
*
|
|
2614
|
+
* watermarks: for operations appended by another server/store instance (sent from
|
|
2615
|
+
* the send cursor), and for a delivery that went unacknowledged (re-sent from the
|
|
2616
|
+
* acknowledged position once it made no progress for `staleMs`, backed off while it
|
|
2617
|
+
* stays stuck). `staleMs` 0 re-sends at once.
|
|
2618
|
+
*
|
|
2619
|
+
* @param staleMs - Base stale window before an unacknowledged delivery is re-sent
|
|
2620
|
+
* @param options - `serverFrontier`: the store's max delivery sequence, when known;
|
|
2621
|
+
* `trackStall`: emit `sync:delivery-stalled` after repeated re-sends
|
|
1264
2622
|
*/
|
|
1265
2623
|
pushDeliveryStreamIfSupported(staleMs?: number, options?: {
|
|
1266
2624
|
trackStall?: boolean;
|
|
2625
|
+
serverFrontier?: number;
|
|
1267
2626
|
}): void;
|
|
2627
|
+
/** Book-keeping for one sent delivery batch ending at `max`. */
|
|
2628
|
+
private recordDeliverySent;
|
|
2629
|
+
/**
|
|
2630
|
+
* A delivery acknowledgment: advance the confirmed watermark (a retransmission
|
|
2631
|
+
* rewinds to it), take a round-trip sample, and reset the backoff. The watermark
|
|
2632
|
+
* never passes what was sent: a client cannot acknowledge what it never received.
|
|
2633
|
+
*/
|
|
2634
|
+
private noteDeliveryAcked;
|
|
2635
|
+
/** Fold one round-trip sample into the retransmit timeout (RFC 6298). */
|
|
2636
|
+
private recordRoundTrip;
|
|
2637
|
+
/**
|
|
2638
|
+
* The current retransmit window: the timeout, which each re-send doubled (up to
|
|
2639
|
+
* five minutes) and which only a fresh round-trip sample brings back down.
|
|
2640
|
+
*/
|
|
2641
|
+
private retransmitWindowMs;
|
|
2642
|
+
/**
|
|
2643
|
+
* (Re)arm the retransmit timer for the outstanding delivery: it fires once no
|
|
2644
|
+
* acknowledgment progress was made for the retransmit window.
|
|
2645
|
+
*/
|
|
2646
|
+
private armRetransmitTimer;
|
|
2647
|
+
/**
|
|
2648
|
+
* No acknowledgment progress for the retransmit window: re-send from the
|
|
2649
|
+
* acknowledged position (the batch was dropped, or the client could not apply it),
|
|
2650
|
+
* unless the socket is still draining what was sent.
|
|
2651
|
+
*/
|
|
2652
|
+
private onRetransmitTimeout;
|
|
2653
|
+
/** Re-send the outstanding delivery from the acknowledged position. */
|
|
2654
|
+
private rewindDelivery;
|
|
2655
|
+
/** Bytes queued on the transport and not yet written to the network (0 if unknown). */
|
|
2656
|
+
private transportBufferedAmount;
|
|
2657
|
+
/**
|
|
2658
|
+
* Wait until the transport's queued bytes drop to the high-water mark (SRV-5
|
|
2659
|
+
* backpressure), so a slow client never makes the server buffer the whole stream
|
|
2660
|
+
* in socket memory. Resolves early when the session closes.
|
|
2661
|
+
*/
|
|
2662
|
+
private waitForSendWindow;
|
|
1268
2663
|
/**
|
|
1269
2664
|
* Close this session.
|
|
1270
2665
|
*/
|
|
@@ -1275,11 +2670,95 @@ declare class ClientSession {
|
|
|
1275
2670
|
*/
|
|
1276
2671
|
private flushOrphanedRelays;
|
|
1277
2672
|
close(reason?: string): void;
|
|
2673
|
+
/**
|
|
2674
|
+
* End this session if its verified principal matches a credential revocation
|
|
2675
|
+
* (AUTH-11): send a retriable `code` error so the client refreshes and
|
|
2676
|
+
* re-handshakes, then close. A session still authenticating remembers the
|
|
2677
|
+
* revocation and applies it as soon as its principal is known.
|
|
2678
|
+
*
|
|
2679
|
+
* @returns True when the session was closed by this call
|
|
2680
|
+
*/
|
|
2681
|
+
terminateIfMatches(filter: SessionRevocation, code: SessionTerminationCode): boolean;
|
|
2682
|
+
/**
|
|
2683
|
+
* Re-authenticate the credential that opened this session (RT-18). The session is
|
|
2684
|
+
* ended with a retriable `AUTH_REVOKED` (`AUTH_EXPIRED` once past its expiry) when
|
|
2685
|
+
* the provider now refuses the credential or resolves it to another principal, for
|
|
2686
|
+
* example after a revocation persisted by another server instance.
|
|
2687
|
+
*
|
|
2688
|
+
* @returns `'terminated'` when this call ended the session, `'error'` when the
|
|
2689
|
+
* provider threw (nothing is ended; the caller retries later), else `'valid'`
|
|
2690
|
+
*/
|
|
2691
|
+
revalidateCredential(): Promise<'valid' | 'terminated' | 'error'>;
|
|
2692
|
+
/**
|
|
2693
|
+
* True when `context` (a fresh authentication of this session's principal)
|
|
2694
|
+
* resolves to download or upload scopes other than the ones this session holds.
|
|
2695
|
+
* A grant that no longer resolves at all counts as changed.
|
|
2696
|
+
*/
|
|
2697
|
+
private scopesChangedFor;
|
|
2698
|
+
/**
|
|
2699
|
+
* End this session because an admin released its node id (RT-5). Retriable: a
|
|
2700
|
+
* client that still owns the node simply reconnects and claims it again.
|
|
2701
|
+
*/
|
|
2702
|
+
endForNodeRelease(): void;
|
|
2703
|
+
private terminate;
|
|
2704
|
+
/** Clear every timer this session owns (expiry, handshake deadline, heartbeat). */
|
|
2705
|
+
private clearSessionTimers;
|
|
2706
|
+
/**
|
|
2707
|
+
* Start the application-level heartbeat for a client that advertised support for it
|
|
2708
|
+
* (LMS #12): a `heartbeat` message whenever nothing else was sent for an interval,
|
|
2709
|
+
* so the client can detect a dead connection it cannot see (browsers never surface
|
|
2710
|
+
* WebSocket pings).
|
|
2711
|
+
*/
|
|
2712
|
+
private startAppHeartbeat;
|
|
2713
|
+
/**
|
|
2714
|
+
* Arm the expiry timer from `AuthContext.expiresAt`. Returns false when the
|
|
2715
|
+
* credential has already expired (the caller refuses the handshake).
|
|
2716
|
+
*/
|
|
2717
|
+
private armExpiryTimer;
|
|
2718
|
+
private clearExpiryTimer;
|
|
1278
2719
|
getState(): SessionState;
|
|
1279
2720
|
getSessionId(): string;
|
|
1280
2721
|
getClientNodeId(): string | null;
|
|
1281
2722
|
getAuthContext(): AuthContext | null;
|
|
2723
|
+
/**
|
|
2724
|
+
* The principal the auth provider verified at handshake, or null before (or
|
|
2725
|
+
* without) authentication. Unlike {@link getAuthContext}, never a context
|
|
2726
|
+
* synthesized for an unauthenticated session.
|
|
2727
|
+
*/
|
|
2728
|
+
getPrincipal(): AuthContext | null;
|
|
1282
2729
|
isStreaming(): boolean;
|
|
2730
|
+
/**
|
|
2731
|
+
* Ingest refusals that happen before any store work (RT-6): operations refused by
|
|
2732
|
+
* the per-minute rate limiter and batches refused for exceeding the per-batch cap.
|
|
2733
|
+
*/
|
|
2734
|
+
getIngestLimitCounts(): {
|
|
2735
|
+
rateLimitedOperations: number;
|
|
2736
|
+
rejectedBatches: number;
|
|
2737
|
+
rateLimitedBlobRequests: number;
|
|
2738
|
+
};
|
|
2739
|
+
/**
|
|
2740
|
+
* True when the client did not advertise the `sequenceReservation` capability
|
|
2741
|
+
* (RT-37): a legacy client (Kora <= beta.12) that may give two operations one
|
|
2742
|
+
* sequence number. Such a pair is stored and delivered, never refused with
|
|
2743
|
+
* SEQUENCE_CONFLICT. Meaningful once the handshake was accepted.
|
|
2744
|
+
*/
|
|
2745
|
+
isLegacySequenceClient(): boolean;
|
|
2746
|
+
/** Legacy duplicate (node, sequence) pairs this session stored (RT-37). */
|
|
2747
|
+
getLegacySequencePairCount(): number;
|
|
2748
|
+
/**
|
|
2749
|
+
* True when a stored record is inside this session's download scope, so a side
|
|
2750
|
+
* channel (for example a Yjs doc update) about it may be delivered here. A record
|
|
2751
|
+
* not stored yet is judged by its id alone.
|
|
2752
|
+
*/
|
|
2753
|
+
canReceiveRecord(collection: string, recordId: string, storedRecord: Record<string, unknown> | null): boolean;
|
|
2754
|
+
/**
|
|
2755
|
+
* A stable key for this session's download scope. Presence (awareness) is only
|
|
2756
|
+
* relayed between sessions that share exactly the same key, so it never crosses a
|
|
2757
|
+
* tenant boundary. Unscoped sessions share the key `"*"`.
|
|
2758
|
+
*/
|
|
2759
|
+
/** This session's download scope, or undefined when it is unscoped. */
|
|
2760
|
+
getDownlinkScopes(): ScopeMap | undefined;
|
|
2761
|
+
getScopePartitionKey(): string;
|
|
1283
2762
|
/**
|
|
1284
2763
|
* Get the transport for this session.
|
|
1285
2764
|
* Used by the awareness relay to send messages to this client.
|
|
@@ -1290,38 +2769,321 @@ declare class ClientSession {
|
|
|
1290
2769
|
private sendToClient;
|
|
1291
2770
|
private enqueueMessage;
|
|
1292
2771
|
private handleMessageAsync;
|
|
2772
|
+
/**
|
|
2773
|
+
* The owner of this session's encryption key records (ENC-1), or null when it may
|
|
2774
|
+
* hold none: the authenticated user, or on a server without auth the one shared
|
|
2775
|
+
* anonymous owner. An anonymous principal of a mixed provider has no stable
|
|
2776
|
+
* identity, so it holds no keys.
|
|
2777
|
+
*/
|
|
2778
|
+
getEncryptionKeyOwner(): string | null;
|
|
2779
|
+
/** Push a key record another session of the same owner wrote (rotation, new passphrase). */
|
|
2780
|
+
pushEncryptionKeyRecord(keyring: string, record: WrappedKeyRecord): void;
|
|
2781
|
+
private handleEncryptionKeyMessage;
|
|
1293
2782
|
/**
|
|
1294
2783
|
* Persist a client-uploaded blob chunk (or manifest) centrally. The bytes are
|
|
1295
2784
|
* verified to hash to the declared hash before storing, so a corrupt or
|
|
1296
2785
|
* mislabeled upload is rejected rather than served later as trusted content.
|
|
1297
2786
|
*/
|
|
1298
2787
|
private handleBlobChunkPush;
|
|
2788
|
+
/**
|
|
2789
|
+
* Claim (or re-claim) a node id for an anonymous device (RT-12, RT-21).
|
|
2790
|
+
*
|
|
2791
|
+
* A device presenting a token holds the node when the claim is (or can become)
|
|
2792
|
+
* `kora:anon-node:<hash(token)>`: its confirmed claim, a fresh claim, or its own
|
|
2793
|
+
* provisional claim, which presenting the token confirms.
|
|
2794
|
+
*
|
|
2795
|
+
* A device presenting no token gets a fresh token and a PROVISIONAL claim, which it
|
|
2796
|
+
* confirms by acknowledging the response with the token once saved (or by
|
|
2797
|
+
* presenting it next time). Until then the claim may be re-issued to a device that
|
|
2798
|
+
* again presents no token (the response was lost in transit), but only while no
|
|
2799
|
+
* session is connected as that node and before the claim expires. Legacy claims
|
|
2800
|
+
* (expired provisional ones, and nodes of the pre-release shared anonymous owner)
|
|
2801
|
+
* are re-issued only with `allowLegacyAnonymousClaims`, with a deprecation warning.
|
|
2802
|
+
*/
|
|
2803
|
+
private claimAnonymousNode;
|
|
2804
|
+
/** The deprecation warning for an anonymous device adopted under a legacy claim (RT-21, RT-91). */
|
|
2805
|
+
private warnLegacyAnonymousClaim;
|
|
2806
|
+
/**
|
|
2807
|
+
* The device proved it saved the node token issued by this handshake: make the
|
|
2808
|
+
* provisional claim permanent (RT-21). A mismatched token is ignored.
|
|
2809
|
+
*/
|
|
2810
|
+
private confirmNodeClaim;
|
|
2811
|
+
/**
|
|
2812
|
+
* The download scope references are authorized against (RT-11, RT-13), or
|
|
2813
|
+
* undefined when they are not checked: without a real auth provider there is no
|
|
2814
|
+
* tenant boundary to protect.
|
|
2815
|
+
*/
|
|
2816
|
+
private referenceScopes;
|
|
2817
|
+
/**
|
|
2818
|
+
* Stable blob-ownership key of this session's principal: the user id, or for an
|
|
2819
|
+
* anonymous device its node-claim owner (never shared by two anonymous devices).
|
|
2820
|
+
*/
|
|
2821
|
+
getBlobOwnerKey(): string;
|
|
2822
|
+
/**
|
|
2823
|
+
* Partition key for peer-to-peer blob forwarding. Sessions with the same download
|
|
2824
|
+
* scope share a tenant view, except anonymous devices: every anonymous session has
|
|
2825
|
+
* the same grant but they are unrelated people, so each is its own partition.
|
|
2826
|
+
*/
|
|
2827
|
+
getBlobPartitionKey(): string;
|
|
2828
|
+
/** Authorize foreign-key targets and blob references of an untrusted write. */
|
|
2829
|
+
private authorizeReferences;
|
|
1299
2830
|
private handleMessageFailure;
|
|
1300
2831
|
private handleHandshake;
|
|
2832
|
+
/**
|
|
2833
|
+
* The nodes whose operations after `fromDeliverySeq` this scoped session will
|
|
2834
|
+
* receive, for the handshake vector (RT-7). Scans the delivery log without holding
|
|
2835
|
+
* it (memory is O(nodes)), and judges each node only until one of its operations is
|
|
2836
|
+
* visible, so most operations cost one scope comparison and no record read.
|
|
2837
|
+
*/
|
|
2838
|
+
private collectVisibleNodeIds;
|
|
2839
|
+
/**
|
|
2840
|
+
* The version vector as committed in the store (SRV-4). A store shared by several
|
|
2841
|
+
* instances reads it fresh; others serve their own (complete) vector.
|
|
2842
|
+
*/
|
|
2843
|
+
private readServerVector;
|
|
2844
|
+
/**
|
|
2845
|
+
* Resolve this session's download and upload scopes from an auth context and the
|
|
2846
|
+
* scope the client asked for at handshake. Side-effect free, so revalidation can
|
|
2847
|
+
* recompute it and compare (RT-26). The legacy `scopes` contract remains shorthand
|
|
2848
|
+
* for both directions.
|
|
2849
|
+
*/
|
|
2850
|
+
private computeSessionScopes;
|
|
1301
2851
|
private handleOperationBatch;
|
|
1302
|
-
private transformForServerSchema;
|
|
1303
|
-
private sendDelta;
|
|
1304
2852
|
/**
|
|
1305
|
-
*
|
|
2853
|
+
* For each delete of this session's node in an upload batch, the batch's later
|
|
2854
|
+
* operations of the same node that name it as a causal parent: candidates for the
|
|
2855
|
+
* author's own copies of its cascades and set-nulls (RT-69). Indexed by the delete's
|
|
2856
|
+
* id, in the server's schema (field names as the server derives effects).
|
|
2857
|
+
*/
|
|
2858
|
+
private indexAuthoredSideEffectCopies;
|
|
2859
|
+
/**
|
|
2860
|
+
* The referential effects of a delete that arrived again as a stored duplicate and
|
|
2861
|
+
* are still undone (RT-73): effects the author covers later in this batch are
|
|
2862
|
+
* deferred like a fresh delete's (derived after the batch unless a copy is stored);
|
|
2863
|
+
* the rest are derived now. Derived ids are deterministic, so a concurrent retry or
|
|
2864
|
+
* another instance stores the same operations.
|
|
2865
|
+
*
|
|
2866
|
+
* @returns The derived operations (to relay)
|
|
2867
|
+
*/
|
|
2868
|
+
private resumeStoredDeleteEffects;
|
|
2869
|
+
/**
|
|
2870
|
+
* Derive the server's copy of each deferred side effect unless one of the author's
|
|
2871
|
+
* copies is stored (applied or already held). A store with the batch lookup is asked
|
|
2872
|
+
* again, so a copy stored by a concurrent session counts too.
|
|
2873
|
+
*/
|
|
2874
|
+
private deriveUncoveredSideEffects;
|
|
2875
|
+
/** {@link ServerStore.findStoredOperations} by id; empty without it or on a failed read. */
|
|
2876
|
+
private findStoredOperationIds;
|
|
2877
|
+
/**
|
|
2878
|
+
* The operation as the server schema reads it (transforms at fold time, RT-84): the
|
|
2879
|
+
* view authorization, validators, constraint checks and scope filters judge, and the
|
|
2880
|
+
* one the server stores fold (core `operationSchemaView`, same transforms). Never
|
|
2881
|
+
* stored: the store keeps the operation as uploaded. An envelope operation is opaque
|
|
2882
|
+
* to the server (NEW-ENC-1) and judged as is; devices transform after decryption.
|
|
2883
|
+
*/
|
|
2884
|
+
private schemaView;
|
|
2885
|
+
/**
|
|
2886
|
+
* Integrity checks on an uploaded operation before anything else judges it:
|
|
2887
|
+
* - a version-2 plaintext id must be the content hash (`INVALID_OPERATION_ID`);
|
|
2888
|
+
* - an unknown declared hash version is refused the same way;
|
|
2889
|
+
* - with required encryption, a data-bearing plaintext operation is refused
|
|
2890
|
+
* (`PLAINTEXT_REJECTED`) unless the plaintext migration window is open.
|
|
2891
|
+
*
|
|
2892
|
+
* @returns null when the operation passes, or the refusal
|
|
2893
|
+
*/
|
|
2894
|
+
private checkUploadIntegrity;
|
|
2895
|
+
/**
|
|
2896
|
+
* Whether an undeclared version-1 id that does not verify is accepted unverified
|
|
2897
|
+
* (RT-71): only from a protocol-1 session (Kora <= beta.12), and only for the
|
|
2898
|
+
* session's own node.
|
|
2899
|
+
*/
|
|
2900
|
+
private acceptsUnverifiedLegacyId;
|
|
2901
|
+
/** Log, count and emit an operation accepted with an unverified legacy id (RT-71). */
|
|
2902
|
+
private reportUnverifiedLegacyOperation;
|
|
2903
|
+
/** Operations this session accepted with an unverified legacy id (RT-71). */
|
|
2904
|
+
getUnverifiedLegacyOperationCount(): number;
|
|
2905
|
+
/**
|
|
2906
|
+
* The operation as the server stores it: an undeclared plaintext id this server
|
|
2907
|
+
* verified declares the version it matched (1, or 2 when the declaration was lost),
|
|
2908
|
+
* so receivers verify it too (RT-64). An absent version is left only on ids nobody
|
|
2909
|
+
* could verify (protocol-1 encrypted payloads, server-side transforms).
|
|
2910
|
+
*/
|
|
2911
|
+
private declareVerifiedHashVersion;
|
|
2912
|
+
/**
|
|
2913
|
+
* True for a node id that is (or was) the server's own: the `kora:` namespace, the
|
|
2914
|
+
* store's node id, and every id the server advertises or folds as authoritative
|
|
2915
|
+
* (other instances, legacy server node ids, configured extras). No device may use one
|
|
2916
|
+
* (RT-61), whether or not it has history yet.
|
|
2917
|
+
*/
|
|
2918
|
+
private isServerAuthorNodeId;
|
|
2919
|
+
/**
|
|
2920
|
+
* The authoritative ids a handshake advertises (RT-75): only explicit ones (legacy
|
|
2921
|
+
* server node ids, configured extras). Every `kora:server:` id is authoritative by
|
|
2922
|
+
* prefix on every replica, and listing instance ids would make the list differ per
|
|
2923
|
+
* instance and per start, which devices would have to treat as news.
|
|
2924
|
+
*/
|
|
2925
|
+
/** The handshake's explicit revocations (RT-81), when the deployment has any. */
|
|
2926
|
+
private advertisedRevocations;
|
|
2927
|
+
private advertisedAuthoritativeNodeIds;
|
|
2928
|
+
/** Node ids this server authors operations under (protocol v2 handshake response). */
|
|
2929
|
+
private serverAuthoritativeNodeIds;
|
|
2930
|
+
/**
|
|
2931
|
+
* A protocol-1 client (Kora <= beta.12) is served for one release (beta.13) with a
|
|
2932
|
+
* deprecation warning: its operation ids are version-1 hashes and its encrypted
|
|
2933
|
+
* payloads have no envelope binding.
|
|
2934
|
+
*/
|
|
2935
|
+
private warnLegacyProtocol;
|
|
2936
|
+
/** The in-scope operations a version-vector client is missing (not yet sent). */
|
|
2937
|
+
private collectDeltaOperations;
|
|
2938
|
+
/** Send a collected version-vector delta in paginated, cursor-carrying batches. */
|
|
2939
|
+
private sendCollectedDelta;
|
|
2940
|
+
/**
|
|
2941
|
+
* Stream the gap-free server->client delivery stream, resuming just after
|
|
1306
2942
|
* `fromDeliverySeq`. Operations are scanned in server delivery-sequence order
|
|
1307
2943
|
* (commit order), scope-filtered for this session, and sent in batches that chain:
|
|
1308
2944
|
* each batch's `baseDeliverySequence` equals the previous batch's
|
|
1309
2945
|
* `maxDeliverySequence` (the first batch bases on `fromDeliverySeq`). The client
|
|
1310
2946
|
* applies a batch only when its watermark equals the base and advances the
|
|
1311
2947
|
* watermark to the max, so a dropped batch stalls the watermark and is recovered by
|
|
1312
|
-
* the
|
|
1313
|
-
* (a dependency is always committed, and thus sequenced, before its
|
|
1314
|
-
* topological sort is needed
|
|
1315
|
-
* that is itself in this stream.
|
|
2948
|
+
* a re-send from the acknowledged position. Because delivery-sequence order respects
|
|
2949
|
+
* causal order (a dependency is always committed, and thus sequenced, before its
|
|
2950
|
+
* dependent), no topological sort is needed.
|
|
1316
2951
|
*
|
|
1317
|
-
*
|
|
1318
|
-
*
|
|
1319
|
-
*
|
|
2952
|
+
* Batches go out as each scan chunk is filtered (SRV-5): the server holds at most
|
|
2953
|
+
* one scan chunk plus two batches per stream, never the whole backlog, and the
|
|
2954
|
+
* first batch reaches the client after one chunk. One full batch is held back so
|
|
2955
|
+
* the stream's last batch can be marked `isFinal` (one-batch lookahead). The final
|
|
2956
|
+
* batch carries the highest sequence scanned, not merely the last in-scope one, so
|
|
2957
|
+
* an out-of-scope tail is not re-scanned. Every sent batch advances the send cursor
|
|
2958
|
+
* ({@link lastSentDeliverySeq}). Between batches the stream waits for the client to
|
|
2959
|
+
* drain its socket (backpressure).
|
|
2960
|
+
*
|
|
2961
|
+
* @param fromDeliverySeq - The chain base: the client's watermark or the send cursor
|
|
2962
|
+
* @param finalizeWhenEmpty - Send an empty final batch even when nothing new was
|
|
2963
|
+
* scanned (the handshake stream, so the client completes initial sync)
|
|
2964
|
+
*/
|
|
2965
|
+
private streamDelivery;
|
|
2966
|
+
/**
|
|
2967
|
+
* Filter one scan chunk of the delivery log down to what this session receives:
|
|
2968
|
+
* visible operations (each preceded by its scope-entry insert, RT-19) and
|
|
2969
|
+
* retractions. The records the decisions need are fetched for the whole chunk at
|
|
2970
|
+
* once (LMS #11), one batched read per collection instead of one query per op.
|
|
1320
2971
|
*/
|
|
1321
|
-
private
|
|
2972
|
+
private filterDeliveryChunk;
|
|
2973
|
+
/**
|
|
2974
|
+
* Read, in one batched query per collection, every record the visibility decisions
|
|
2975
|
+
* for these operations may look up, when the store supports batched reads. Returns
|
|
2976
|
+
* null (per-operation lookups) otherwise. Over-fetching is harmless: the chunk
|
|
2977
|
+
* bounds it.
|
|
2978
|
+
*/
|
|
2979
|
+
private prefetchRecordsFor;
|
|
1322
2980
|
/** Build, track, and send one chained delivery-stream batch. */
|
|
1323
2981
|
private sendDeliveryBatch;
|
|
2982
|
+
/** The scope snapshots of these operations, when the store keeps them (RT-14). */
|
|
2983
|
+
private scopeSnapshotsFor;
|
|
2984
|
+
/**
|
|
2985
|
+
* Download visibility of one operation. With a scope snapshot (RT-14) the
|
|
2986
|
+
* operation is judged on the record's scope values right after it was applied, so
|
|
2987
|
+
* history written while a record belonged to someone else stays hidden after an
|
|
2988
|
+
* ownership transfer, and a previous owner still receives its own history. Query
|
|
2989
|
+
* subsets (a client-chosen narrowing, not an authorization) keep using the current
|
|
2990
|
+
* row. Operations without a snapshot (legacy rows, custom stores) fall back to the
|
|
2991
|
+
* current row plus `op.data`.
|
|
2992
|
+
*/
|
|
1324
2993
|
private operationVisibleToClient;
|
|
2994
|
+
/**
|
|
2995
|
+
* Where the store already holds each of `operations` (by id), in one store read.
|
|
2996
|
+
* Null when the store cannot answer by id (a custom store without
|
|
2997
|
+
* `findStoredOperations`, or the read failed): callers then fall back to a
|
|
2998
|
+
* per-operation (node, sequence) lookup and the store's own dedup at apply.
|
|
2999
|
+
*/
|
|
3000
|
+
private findStoredOperations;
|
|
3001
|
+
/**
|
|
3002
|
+
* True when the batch lookup alone shows `op` is already stored and may be
|
|
3003
|
+
* acknowledged as a duplicate with no further store work (RT-31, RT-39): this
|
|
3004
|
+
* device's own operation stored under its node (its sequence repair may have
|
|
3005
|
+
* renumbered it, keeping the id), or any other operation stored under exactly its
|
|
3006
|
+
* (node, sequence, id), such as one the server itself delivered and the client
|
|
3007
|
+
* echoed back. Never true for a store without the batch lookup.
|
|
3008
|
+
*/
|
|
3009
|
+
private isStoredDuplicate;
|
|
3010
|
+
/**
|
|
3011
|
+
* The operation the store holds under `op`'s id (at the node and sequence the batch
|
|
3012
|
+
* lookup found), or null when it cannot be read (the upload is then judged like any
|
|
3013
|
+
* other: verified, validated, charged, and deduplicated by the store at apply).
|
|
3014
|
+
*/
|
|
3015
|
+
private loadStoredOperation;
|
|
3016
|
+
/**
|
|
3017
|
+
* Refuse an upload that reuses a stored operation's id with different content (RT-77):
|
|
3018
|
+
* terminal, nothing recorded or derived, logged and emitted as tampering. An honest
|
|
3019
|
+
* client never does this (ids are content hashes), so the rejection only reaches a
|
|
3020
|
+
* forger. No resolution is recorded: the id belongs to the stored operation.
|
|
3021
|
+
*/
|
|
3022
|
+
private refuseForgedDuplicate;
|
|
3023
|
+
/** Uploads refused for reusing a stored id with other content (RT-77). */
|
|
3024
|
+
getForgedDuplicateCount(): number;
|
|
3025
|
+
/**
|
|
3026
|
+
* The recorded resolutions of the batch's own-node operations that the lookup did
|
|
3027
|
+
* not find stored (RT-43, RT-47). Asked only for this session's own node, so the
|
|
3028
|
+
* answer never reveals another device's (or tenant's) refusals.
|
|
3029
|
+
*/
|
|
3030
|
+
private findResolutions;
|
|
3031
|
+
/** The highest resolved sequence of `nodeId` (0 without the store method or on error). */
|
|
3032
|
+
private resolvedThrough;
|
|
3033
|
+
/**
|
|
3034
|
+
* Durably record how this session's own operation was resolved without being stored
|
|
3035
|
+
* under its sequence. Awaited before the answer is sent: a failure propagates (the
|
|
3036
|
+
* batch fails and is not acknowledged), so the client never acts on a resolution the
|
|
3037
|
+
* server could forget.
|
|
3038
|
+
*/
|
|
3039
|
+
private recordResolution;
|
|
3040
|
+
/**
|
|
3041
|
+
* Refuse an operation for good (non-retriable): remember the refusal (RT-47), then
|
|
3042
|
+
* tell the client. A resubmission of the same id is answered with this rejection
|
|
3043
|
+
* without being judged again.
|
|
3044
|
+
*/
|
|
3045
|
+
private refuseTerminally;
|
|
3046
|
+
/**
|
|
3047
|
+
* An own operation acknowledged as a duplicate of the copy stored under ANOTHER
|
|
3048
|
+
* sequence (the client's sequence repair renumbered it, keeping its id): record the
|
|
3049
|
+
* submitted sequence as resolved, so the handshake does not read it as lost (RT-43).
|
|
3050
|
+
*/
|
|
3051
|
+
private noteStoredElsewhere;
|
|
3052
|
+
/** Refuse the rest of a batch for the per-minute ingest budget (retriable). */
|
|
3053
|
+
private sendRateLimited;
|
|
3054
|
+
/**
|
|
3055
|
+
* True when the store already holds exactly this operation (same node, sequence and
|
|
3056
|
+
* id). Answered from the batch lookup when the store supports it.
|
|
3057
|
+
*/
|
|
3058
|
+
private isStoredOperation;
|
|
3059
|
+
/**
|
|
3060
|
+
* The scopes this session may write under. Fails closed: with auth configured but
|
|
3061
|
+
* no accepted authentication, the session may write nothing (an empty scope map),
|
|
3062
|
+
* never everything.
|
|
3063
|
+
*/
|
|
3064
|
+
private uplinkScopes;
|
|
3065
|
+
/**
|
|
3066
|
+
* Upload authorization, independent from the client's downloaded/query view. The
|
|
3067
|
+
* stored row (including a soft-deleted one) is always loaded when scopes apply and
|
|
3068
|
+
* is authoritative; client-supplied previousData is never consulted.
|
|
3069
|
+
*/
|
|
3070
|
+
private authorizeClientOperation;
|
|
3071
|
+
/**
|
|
3072
|
+
* True when this update moved the record out of the session's scope, judged on the
|
|
3073
|
+
* store's own before/after values (RT-15), never on the writer's previousData. An
|
|
3074
|
+
* operation without a snapshot never produces a retraction (fail quiet: the
|
|
3075
|
+
* client keeps what it already had, and no other tenant's record id leaks).
|
|
3076
|
+
*/
|
|
3077
|
+
private scopeRetractionFor;
|
|
3078
|
+
/**
|
|
3079
|
+
* The scope-entry operation to send before `op` when `op` moved an existing record
|
|
3080
|
+
* into this session's download scope (RT-19), or null. Judged on the store's own
|
|
3081
|
+
* pre/post snapshot; built from the record's CURRENT row, and only while that row
|
|
3082
|
+
* is live and still inside the scope (a record that has since left again, or was
|
|
3083
|
+
* deleted, gets no entry: its later operations retract or delete it anyway).
|
|
3084
|
+
* Operations without a snapshot (legacy rows, custom stores) never produce one.
|
|
3085
|
+
*/
|
|
3086
|
+
private scopeEntryFor;
|
|
1325
3087
|
/**
|
|
1326
3088
|
* Read a record's current field values from the materialized store for scope /
|
|
1327
3089
|
* query-subset backfill. Includes soft-deleted rows so a relayed delete (whose op
|
|
@@ -1330,7 +3092,15 @@ declare class ClientSession {
|
|
|
1330
3092
|
*/
|
|
1331
3093
|
private lookupRecordFields;
|
|
1332
3094
|
private handleAwarenessUpdate;
|
|
3095
|
+
/**
|
|
3096
|
+
* A Yjs doc-channel update is a write to a richtext field: the sender must be
|
|
3097
|
+
* allowed to write the stored record (same rule as operation uploads). The stored
|
|
3098
|
+
* row is handed to the relay so delivery can be limited to sessions whose download
|
|
3099
|
+
* scope contains the record.
|
|
3100
|
+
*/
|
|
1333
3101
|
private handleYjsDocUpdate;
|
|
3102
|
+
/** A legacy client's duplicate pair was stored (RT-37): count it and log it. */
|
|
3103
|
+
private recordLegacySequencePair;
|
|
1334
3104
|
private sendError;
|
|
1335
3105
|
/**
|
|
1336
3106
|
* Reject one specific client operation, tied to its id, so the submitter can
|
|
@@ -1339,7 +3109,6 @@ declare class ClientSession {
|
|
|
1339
3109
|
* replica ever sees it.
|
|
1340
3110
|
*/
|
|
1341
3111
|
private sendOperationRejected;
|
|
1342
|
-
private setSerializerWireFormat;
|
|
1343
3112
|
private handleTransportClose;
|
|
1344
3113
|
}
|
|
1345
3114
|
|
|
@@ -1381,6 +3150,25 @@ interface ProductionServerConfig {
|
|
|
1381
3150
|
* backupToken.
|
|
1382
3151
|
*/
|
|
1383
3152
|
operationalAuth?: ProductionOperationalAuth;
|
|
3153
|
+
/**
|
|
3154
|
+
* Reverse proxies trusted to report the client address in `X-Forwarded-For`,
|
|
3155
|
+
* which becomes `request.ip` (the key `@korajs/auth` rate-limits sign-in by).
|
|
3156
|
+
* A hop count (`1` for a single load balancer in front) or a list of trusted
|
|
3157
|
+
* proxy IPs / CIDR ranges. When unset, the header is ignored and `request.ip`
|
|
3158
|
+
* is the socket address, so a client cannot pick its own rate-limit bucket.
|
|
3159
|
+
*/
|
|
3160
|
+
trustProxy?: TrustProxySetting;
|
|
3161
|
+
/**
|
|
3162
|
+
* Largest request body a custom HTTP route (`httpRoutes`) accepts, in bytes. A
|
|
3163
|
+
* larger body is refused with 413 before it is buffered (SRV-6), so an
|
|
3164
|
+
* unauthenticated client cannot make the server hold arbitrary amounts of memory.
|
|
3165
|
+
* Defaults to 1 MiB.
|
|
3166
|
+
*/
|
|
3167
|
+
maxRequestBodyBytes?: number;
|
|
3168
|
+
/**
|
|
3169
|
+
* Largest backup accepted by `/__kora/backup/import`, in bytes. Defaults to 256 MiB.
|
|
3170
|
+
*/
|
|
3171
|
+
maxBackupBytes?: number;
|
|
1384
3172
|
}
|
|
1385
3173
|
interface ProductionOperationalAuth {
|
|
1386
3174
|
/** Protects /__kora, /__kora/status, and /__kora/events. */
|
|
@@ -1419,7 +3207,10 @@ interface ProductionHttpRoute {
|
|
|
1419
3207
|
* A production server handle returned by createProductionServer.
|
|
1420
3208
|
*/
|
|
1421
3209
|
interface ProductionServer {
|
|
1422
|
-
/**
|
|
3210
|
+
/**
|
|
3211
|
+
* Start listening. Returns the URL the server is available at, with the port it
|
|
3212
|
+
* actually bound (pass `port: 0` to let the OS pick a free one).
|
|
3213
|
+
*/
|
|
1423
3214
|
start(): Promise<string>;
|
|
1424
3215
|
/** Stop the server gracefully. */
|
|
1425
3216
|
stop(): Promise<void>;
|
|
@@ -1471,6 +3262,10 @@ interface TokenAuthProviderOptions {
|
|
|
1471
3262
|
/**
|
|
1472
3263
|
* Validate a token and return an AuthContext if valid, or null if rejected.
|
|
1473
3264
|
* This is where you implement your auth logic (JWT verification, database lookup, etc.).
|
|
3265
|
+
*
|
|
3266
|
+
* Return `scopes` to grant sync access explicitly. When the returned context
|
|
3267
|
+
* has no scopes, schema-scoped collections are bound to `{ userId }` and any
|
|
3268
|
+
* collection scoped by another key is denied.
|
|
1474
3269
|
*/
|
|
1475
3270
|
validate: (token: string) => Promise<AuthContext | null>;
|
|
1476
3271
|
}
|
|
@@ -1503,11 +3298,13 @@ interface TokenValidator {
|
|
|
1503
3298
|
sub: string;
|
|
1504
3299
|
dev: string;
|
|
1505
3300
|
type: string;
|
|
3301
|
+
exp?: number;
|
|
1506
3302
|
} | null;
|
|
1507
3303
|
validateTokenWithRevocation?(token: string): Promise<{
|
|
1508
3304
|
sub: string;
|
|
1509
3305
|
dev: string;
|
|
1510
3306
|
type: string;
|
|
3307
|
+
exp?: number;
|
|
1511
3308
|
} | null>;
|
|
1512
3309
|
}
|
|
1513
3310
|
/**
|
|
@@ -1550,8 +3347,15 @@ interface KoraAuthProviderOptions {
|
|
|
1550
3347
|
*/
|
|
1551
3348
|
deviceTracker?: DeviceToucher;
|
|
1552
3349
|
/**
|
|
1553
|
-
* Optional scope resolver. Called with the user ID to determine
|
|
1554
|
-
* which collections/records the user can sync.
|
|
3350
|
+
* Optional scope resolver. Called with the verified user ID to determine
|
|
3351
|
+
* which collections/records the user can sync. Its result is the complete
|
|
3352
|
+
* server grant: a collection it omits is not visible, and the client
|
|
3353
|
+
* handshake can only narrow it. Use `claimScopes({ userId, orgId })` from
|
|
3354
|
+
* `@korajs/core` to bind every schema-scoped collection from verified values.
|
|
3355
|
+
*
|
|
3356
|
+
* When omitted, the grant binds every schema-scoped collection from
|
|
3357
|
+
* `{ userId: <verified sub> }`; collections scoped by any other key are denied
|
|
3358
|
+
* until you supply it.
|
|
1555
3359
|
*/
|
|
1556
3360
|
resolveScopes?: (userId: string) => Promise<Record<string, Record<string, unknown>>>;
|
|
1557
3361
|
}
|
|
@@ -1605,7 +3409,9 @@ interface MixedAuthProviderOptions {
|
|
|
1605
3409
|
*/
|
|
1606
3410
|
primary: AuthProvider;
|
|
1607
3411
|
/**
|
|
1608
|
-
* Scopes to apply to anonymous connections.
|
|
3412
|
+
* Scopes to apply to anonymous connections. This is the complete anonymous
|
|
3413
|
+
* grant: anonymous sessions can sync exactly these collections, and the
|
|
3414
|
+
* client handshake can only narrow them, never add one.
|
|
1609
3415
|
* Each key is a collection name; the value is a filter object
|
|
1610
3416
|
* (use `{}` for unrestricted access to that collection).
|
|
1611
3417
|
*
|
|
@@ -1629,10 +3435,10 @@ interface MixedAuthProviderOptions {
|
|
|
1629
3435
|
/**
|
|
1630
3436
|
* Auth provider that supports both authenticated and anonymous connections.
|
|
1631
3437
|
*
|
|
1632
|
-
* When a client connects with a
|
|
1633
|
-
*
|
|
1634
|
-
* (
|
|
1635
|
-
*
|
|
3438
|
+
* When a client connects with a token, the primary auth provider decides:
|
|
3439
|
+
* a valid token authenticates normally and an invalid, expired or revoked one
|
|
3440
|
+
* is rejected (the client refreshes and reconnects). Only a client that sends
|
|
3441
|
+
* no token at all is accepted as anonymous, with exactly `anonymousScopes`.
|
|
1636
3442
|
*
|
|
1637
3443
|
* This is the recommended pattern for apps that need public data access
|
|
1638
3444
|
* alongside authenticated users — for example, a form builder where
|
|
@@ -1673,9 +3479,24 @@ declare class MixedAuthProvider implements AuthProvider {
|
|
|
1673
3479
|
private readonly anonymousPrefix;
|
|
1674
3480
|
private anonymousCounter;
|
|
1675
3481
|
constructor(options: MixedAuthProviderOptions);
|
|
1676
|
-
|
|
3482
|
+
/** The primary provider's revocation feed, when it has one. */
|
|
3483
|
+
onRevoke?: AuthProvider['onRevoke'];
|
|
3484
|
+
authenticate(token: string): Promise<AuthContext | null>;
|
|
1677
3485
|
}
|
|
1678
3486
|
|
|
3487
|
+
/** Options for {@link MemoryServerStore}. */
|
|
3488
|
+
interface MemoryServerStoreOptions {
|
|
3489
|
+
/**
|
|
3490
|
+
* Node ids, besides this store's own, whose operations win
|
|
3491
|
+
* `merge('server-authoritative')` fields (legacy server node ids). Every
|
|
3492
|
+
* `kora:server:` node id is authoritative without being listed.
|
|
3493
|
+
*/
|
|
3494
|
+
authoritativeNodeIds?: string[];
|
|
3495
|
+
/** Revoked explicit authorities (see `ServerIdentityOptions.revokedAuthoritativeNodeIds`). */
|
|
3496
|
+
revokedAuthoritativeNodeIds?: string[];
|
|
3497
|
+
/** Instance id within the deployment (see `ServerIdentityOptions.instanceId`). */
|
|
3498
|
+
instanceId?: string;
|
|
3499
|
+
}
|
|
1679
3500
|
/**
|
|
1680
3501
|
* In-memory server store for testing and quick prototyping.
|
|
1681
3502
|
* Not suitable for production — data does not survive process restart.
|
|
@@ -1687,6 +3508,13 @@ declare class MemoryServerStore implements ServerStore {
|
|
|
1687
3508
|
private readonly nodeId;
|
|
1688
3509
|
private readonly operations;
|
|
1689
3510
|
private readonly operationIndex;
|
|
3511
|
+
/** (nodeId, sequenceNumber) -> the id of the operation holding it (W3 step 4). */
|
|
3512
|
+
/** (node, sequence) -> ids holding it, in store order (several only for a legacy pair). */
|
|
3513
|
+
private readonly operationIdsBySequence;
|
|
3514
|
+
/** Node -> sequences held by more than one operation (legacy pairs, RT-48). */
|
|
3515
|
+
private readonly pairSequencesByNode;
|
|
3516
|
+
/** Operation id -> how it was resolved without being stored (RT-43, RT-47). */
|
|
3517
|
+
private readonly resolutions;
|
|
1690
3518
|
private readonly versionVector;
|
|
1691
3519
|
/**
|
|
1692
3520
|
* Server-assigned delivery sequence per operation id. Single-process, so a plain
|
|
@@ -1697,24 +3525,117 @@ declare class MemoryServerStore implements ServerStore {
|
|
|
1697
3525
|
private readonly deliverySeqByOpId;
|
|
1698
3526
|
private deliverySeqCounter;
|
|
1699
3527
|
private schema;
|
|
3528
|
+
/** Node id -> the authenticated principal that claimed it (see claimNode). */
|
|
3529
|
+
private readonly nodeOwners;
|
|
3530
|
+
/** Operation id -> scope snapshot captured at apply time (RT-14). */
|
|
3531
|
+
private readonly scopeSnapshots;
|
|
3532
|
+
/** Fields the current snapshots were captured with (RT-20). */
|
|
3533
|
+
private snapshotFingerprint;
|
|
3534
|
+
/** Blob content hash -> owners that pushed or first claimed it (RT-11). */
|
|
3535
|
+
private readonly blobOwners;
|
|
3536
|
+
/** Wrapped encryption key records (ENC-1): owner + keyring -> record JSON and revision. */
|
|
3537
|
+
private readonly encryptionKeyRecords;
|
|
1700
3538
|
/** Materialized records: collection -> recordId -> record data */
|
|
1701
3539
|
private readonly materializedRecords;
|
|
3540
|
+
/** Fold state per materialized record (W7): rows are projected from it. */
|
|
3541
|
+
private readonly foldStates;
|
|
3542
|
+
/** Every operation of a record, in store (delivery) order. */
|
|
3543
|
+
private readonly recordOps;
|
|
3544
|
+
private readonly authoritativeNodeIds;
|
|
3545
|
+
/** Legacy and configured authorities (the `kora:server:` prefix needs no listing). */
|
|
3546
|
+
private readonly explicitAuthorities;
|
|
3547
|
+
/** Explicit authority over time: revoked and ever-held ids (RT-81). */
|
|
3548
|
+
private readonly authorityHistory;
|
|
3549
|
+
private foldOptions;
|
|
3550
|
+
/** Schema transforms the fold applies (transforms at fold time, RT-84). */
|
|
3551
|
+
private operationTransforms;
|
|
1702
3552
|
private closed;
|
|
1703
|
-
|
|
3553
|
+
/**
|
|
3554
|
+
* Sequence-enforcement epoch (see SEQUENCE_ENFORCEMENT_EPOCH_KEY). A memory store
|
|
3555
|
+
* starts empty under this release, so 0: every holder was stored under enforcement.
|
|
3556
|
+
* A replace-mode backup import moves it to the restored log's end.
|
|
3557
|
+
*/
|
|
3558
|
+
private sequenceEpoch;
|
|
3559
|
+
/** Derivation secret of server-derived ids (RT-64). Per process: nothing persists. */
|
|
3560
|
+
private readonly derivationSecret;
|
|
3561
|
+
/**
|
|
3562
|
+
* @param nodeId - Deprecated: a plain id is recorded as a legacy authoritative id; a
|
|
3563
|
+
* `kora:server:` id is used verbatim. Leave unset: the store authors under
|
|
3564
|
+
* `kora:server:<deployment>:<instance>` (a fresh deployment per process, since a
|
|
3565
|
+
* memory store persists nothing).
|
|
3566
|
+
* @param options - Extra authoritative ids and the instance id
|
|
3567
|
+
*/
|
|
3568
|
+
constructor(nodeId?: string, options?: MemoryServerStoreOptions);
|
|
3569
|
+
deriveServerOperationId(parentOpId: string, ruleId: string, targetRecordId: string): Promise<string>;
|
|
3570
|
+
getAuthoritativeNodeIds(): string[];
|
|
3571
|
+
/** Explicit authoritative ids the deployment revoked (RT-81), advertised at handshake. */
|
|
3572
|
+
getRevokedAuthoritativeNodeIds(): string[];
|
|
3573
|
+
/**
|
|
3574
|
+
* Every explicit id the deployment ever held authoritative, revoked ones included
|
|
3575
|
+
* (RT-81): never accepted as a device node id.
|
|
3576
|
+
*/
|
|
3577
|
+
getEverAuthoritativeNodeIds(): string[];
|
|
1704
3578
|
getVersionVector(): VersionVector;
|
|
1705
3579
|
getNodeId(): string;
|
|
1706
3580
|
getSchema(): SchemaDefinition | null;
|
|
1707
|
-
setSchema(schema: SchemaDefinition): Promise<void>;
|
|
1708
|
-
|
|
3581
|
+
setSchema(schema: SchemaDefinition, options?: ServerSchemaOptions): Promise<void>;
|
|
3582
|
+
getOperationTransforms(): readonly OperationTransform[];
|
|
3583
|
+
setOperationTransforms(transforms: readonly OperationTransform[]): Promise<void>;
|
|
3584
|
+
applyRemoteOperation(op: Operation, options?: ApplyRemoteOptions): Promise<ApplyResult>;
|
|
3585
|
+
private addSequenceHolder;
|
|
3586
|
+
getSequencePairOperations(nodeId: string, throughSequence: number): Promise<Operation[]>;
|
|
3587
|
+
recordOperationResolution(resolution: OperationResolution): Promise<void>;
|
|
3588
|
+
findOperationResolutions(nodeId: string, ids: string[]): Promise<Map<string, OperationResolution>>;
|
|
3589
|
+
deleteOperationResolution(nodeId: string, operationId: string): Promise<void>;
|
|
3590
|
+
getResolvedThrough(nodeId: string): Promise<number>;
|
|
3591
|
+
findStoredOperations(ids: string[]): Promise<Map<string, StoredOperationKey>>;
|
|
3592
|
+
getOperationScopeSnapshots(operationIds: string[]): Promise<Map<string, OperationScopeSnapshot>>;
|
|
3593
|
+
getRecordLatestTimestamp(collection: string, recordId: string): Promise<HLCTimestamp | null>;
|
|
3594
|
+
getRecordFieldVersions(collection: string, recordId: string): Promise<RecordFieldVersions | null>;
|
|
3595
|
+
getRecordFoldState(collection: string, recordId: string): Promise<FoldState | null>;
|
|
3596
|
+
getRecordOperations(collection: string, recordId: string): Promise<Operation[]>;
|
|
3597
|
+
previewOperation(op: Operation): Promise<MaterializedRecord | null>;
|
|
3598
|
+
/**
|
|
3599
|
+
* The record's fold state: the stored one for a materialized collection, else
|
|
3600
|
+
* folded from the record's operations (a collection outside the schema).
|
|
3601
|
+
*/
|
|
3602
|
+
private readFoldState;
|
|
3603
|
+
private addRecordOp;
|
|
3604
|
+
recordBlobOwner(hash: string, owner: string): Promise<void>;
|
|
3605
|
+
getBlobOwners(hashes: string[]): Promise<Map<string, string[]>>;
|
|
3606
|
+
claimBlobIfUnowned(hash: string, owner: string): Promise<boolean>;
|
|
3607
|
+
/** Scope values of a live record before a write; null when absent or deleted. */
|
|
3608
|
+
private scopeValuesBefore;
|
|
3609
|
+
/**
|
|
3610
|
+
* Rebuild missing scope snapshots from the log (migration from a store without
|
|
3611
|
+
* them, or a replace-mode backup import), replaying each record in commit order.
|
|
3612
|
+
*/
|
|
3613
|
+
private backfillScopeSnapshots;
|
|
1709
3614
|
getOperationRange(nodeId: string, fromSeq: number, toSeq: number): Promise<Operation[]>;
|
|
1710
3615
|
getOperationCount(): Promise<number>;
|
|
1711
3616
|
getMaxDeliverySequence(): Promise<number>;
|
|
1712
3617
|
getOperationsAfterDelivery(afterDeliverySequence: number, limit: number): Promise<DeliveredOperation[]>;
|
|
1713
3618
|
materializeCollection(collection: string): Promise<MaterializedRecord[]>;
|
|
3619
|
+
getNodeIdsAfterDelivery(afterDeliverySequence: number): Promise<string[]>;
|
|
3620
|
+
findRecordsByIds(collection: string, ids: string[]): Promise<Map<string, MaterializedRecord>>;
|
|
1714
3621
|
queryCollection(collection: string, options?: CollectionQueryOptions): Promise<MaterializedRecord[]>;
|
|
1715
3622
|
findRecord(collection: string, id: string): Promise<MaterializedRecord | null>;
|
|
1716
3623
|
countCollection(collection: string, where?: Record<string, unknown>): Promise<number>;
|
|
1717
3624
|
close(): Promise<void>;
|
|
3625
|
+
getEncryptionKeyRecord(owner: string, keyring: string): Promise<string | null>;
|
|
3626
|
+
putEncryptionKeyRecord(owner: string, keyring: string, record: string, revision: number, expectedRevision: number): Promise<boolean>;
|
|
3627
|
+
listEncryptionKeyRecords(owner?: string): Promise<EncryptionKeyRecordRow[]>;
|
|
3628
|
+
getEncryptedKeyIds(nodeOwner: string | null, limit: number): Promise<string[]>;
|
|
3629
|
+
claimNode(nodeId: string, userId: string): Promise<boolean>;
|
|
3630
|
+
getNodeClaimOwner(nodeId: string): Promise<string | null>;
|
|
3631
|
+
replaceNodeClaim(nodeId: string, expectedOwner: string, newOwner: string): Promise<boolean>;
|
|
3632
|
+
releaseNodeClaim(nodeId: string): Promise<boolean>;
|
|
3633
|
+
/**
|
|
3634
|
+
* The record as currently stored, including a soft-deleted one (whose last field
|
|
3635
|
+
* values are kept), or null when it was never written. Without a materialized
|
|
3636
|
+
* table, the last known values are replayed from the operation log.
|
|
3637
|
+
*/
|
|
3638
|
+
private readStoredRow;
|
|
1718
3639
|
/**
|
|
1719
3640
|
* Wipes all in-memory state. For tests and E2E isolation only.
|
|
1720
3641
|
*/
|
|
@@ -1728,7 +3649,16 @@ declare class MemoryServerStore implements ServerStore {
|
|
|
1728
3649
|
* Get all stored operations (for test assertions).
|
|
1729
3650
|
*/
|
|
1730
3651
|
getAllOperations(): Operation[];
|
|
1731
|
-
|
|
3652
|
+
/**
|
|
3653
|
+
* Merge one just-appended operation into its record's fold state and project the
|
|
3654
|
+
* row: O(fields the operation touches), independent of the record's history
|
|
3655
|
+
* (SRV-7). Falls back to a full re-fold only when the stored state cannot take the
|
|
3656
|
+
* operation (a field's fold kind changed with the schema).
|
|
3657
|
+
*/
|
|
3658
|
+
private mergeIntoRecord;
|
|
3659
|
+
/** Persist a record's fold state and project it onto its materialized row. */
|
|
3660
|
+
private writeRecord;
|
|
3661
|
+
/** Re-fold every record of every materialized collection (schema set, backup restore). */
|
|
1732
3662
|
private backfillAllCollections;
|
|
1733
3663
|
private materializeFromOps;
|
|
1734
3664
|
private assertOpen;
|
|
@@ -1736,6 +3666,123 @@ declare class MemoryServerStore implements ServerStore {
|
|
|
1736
3666
|
private assertCollection;
|
|
1737
3667
|
}
|
|
1738
3668
|
|
|
3669
|
+
/**
|
|
3670
|
+
* Server materialization through the one deterministic fold (W7 Stage B2).
|
|
3671
|
+
*
|
|
3672
|
+
* Every server store (memory, SQLite, Postgres) keeps, per materialized record, the
|
|
3673
|
+
* record's serialized {@link FoldState} next to its collection row. Applying an
|
|
3674
|
+
* operation is "append, then `mergeOp`": O(fields the operation touches), never a
|
|
3675
|
+
* replay of the record's history (SRV-7). The collection row is a pure projection of
|
|
3676
|
+
* the fold state ({@link projectFoldState}), so the server's rows are exactly what a
|
|
3677
|
+
* client that holds the same operations materializes (SRV-1).
|
|
3678
|
+
*
|
|
3679
|
+
* The fold is pure JavaScript: ordering uses HLC stamps compared in JS, never a
|
|
3680
|
+
* database collation (the Phase 1 COLLATE "C" concern disappears for materialization).
|
|
3681
|
+
*
|
|
3682
|
+
* Old helpers (`replayOperationsForRecord`, `mergeArraySet`) are no longer used to
|
|
3683
|
+
* materialize; they stay exported from core for legacy comparison only.
|
|
3684
|
+
*/
|
|
3685
|
+
|
|
3686
|
+
/** What a store's startup re-materialization did (W7 step 7). */
|
|
3687
|
+
interface FoldMigrationReport {
|
|
3688
|
+
/** False when no schema was set (nothing is materialized). */
|
|
3689
|
+
ran: boolean;
|
|
3690
|
+
/** Records re-folded and rewritten. */
|
|
3691
|
+
records: number;
|
|
3692
|
+
/**
|
|
3693
|
+
* Records that own quarantined operations (see `ServerLogIntegrityReport`): their log
|
|
3694
|
+
* is incomplete, so their pre-fold rows were kept as a snapshot base with the
|
|
3695
|
+
* remaining operations folded onto it (RT-70), never re-folded from the log alone.
|
|
3696
|
+
*/
|
|
3697
|
+
skippedUnclean: number;
|
|
3698
|
+
/** True when the fold plan (field kinds, strategies, resolvers) changed: every record was re-folded. */
|
|
3699
|
+
fullRefold: boolean;
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3702
|
+
/** A row the scan quarantined. */
|
|
3703
|
+
interface ServerLogIntegrityRow {
|
|
3704
|
+
operationId: string;
|
|
3705
|
+
problem: string;
|
|
3706
|
+
detail: string;
|
|
3707
|
+
}
|
|
3708
|
+
/** Result of the server scan. */
|
|
3709
|
+
interface ServerLogIntegrityReport {
|
|
3710
|
+
/** Rows checked by this scan (0 when it already ran on this database). */
|
|
3711
|
+
checkedRows: number;
|
|
3712
|
+
/** Rows this scan moved to the quarantine table. */
|
|
3713
|
+
quarantined: ServerLogIntegrityRow[];
|
|
3714
|
+
/** False when the one-time scan had already run (only the quarantine is reported). */
|
|
3715
|
+
ran: boolean;
|
|
3716
|
+
/** Every row in the quarantine table. */
|
|
3717
|
+
totalQuarantined: number;
|
|
3718
|
+
}
|
|
3719
|
+
|
|
3720
|
+
/**
|
|
3721
|
+
* Stable server identity (RT-61, RT-62, RT-64).
|
|
3722
|
+
*
|
|
3723
|
+
* Every operation the server authors (route writes, cascades, set-nulls, constraint
|
|
3724
|
+
* corrections) is authored by a node id in the reserved namespace
|
|
3725
|
+
* `kora:server:<deploymentId>:<instanceId>`:
|
|
3726
|
+
*
|
|
3727
|
+
* - `deploymentId` is generated once per database and persisted in `kora_server_meta`,
|
|
3728
|
+
* so every instance that shares the database (and every restart) belongs to one
|
|
3729
|
+
* deployment.
|
|
3730
|
+
* - `instanceId` is distinct per running instance, so two instances never author the
|
|
3731
|
+
* same `(nodeId, sequenceNumber)`: SQLite persists one (a SQLite database has one
|
|
3732
|
+
* writer process), Postgres allocates a fresh one from a counter in the database on
|
|
3733
|
+
* every start unless the operator configures a stable `instanceId` per instance.
|
|
3734
|
+
*
|
|
3735
|
+
* Authority (`merge('server-authoritative')`) is the prefix rule: any operation whose
|
|
3736
|
+
* node id starts with {@link SERVER_NODE_PREFIX} is authoritative, on the server and on
|
|
3737
|
+
* every device. Devices can never use a `kora:` node id (the session refuses it at
|
|
3738
|
+
* handshake). Server node ids written before this release (random per-process ids, or
|
|
3739
|
+
* an id the operator configured) are recorded once, at the first start of this release,
|
|
3740
|
+
* in a persisted list of legacy authoritative ids that the handshake keeps advertising,
|
|
3741
|
+
* so earlier server decisions keep winning everywhere.
|
|
3742
|
+
*
|
|
3743
|
+
* Server-derived operation ids (cascades, corrections) are keyed with a persisted,
|
|
3744
|
+
* deployment-wide secret (HMAC-SHA-256), so a writer cannot predict them and occupy one
|
|
3745
|
+
* first; every instance of the deployment derives the same id for the same effect.
|
|
3746
|
+
*/
|
|
3747
|
+
|
|
3748
|
+
/** Prefix of every node id the server authors under. */
|
|
3749
|
+
declare const SERVER_NODE_PREFIX = "kora:server:";
|
|
3750
|
+
/** Thrown for an invalid server identity configuration. */
|
|
3751
|
+
declare class ServerIdentityError extends KoraError {
|
|
3752
|
+
constructor(message: string, context?: Record<string, unknown>);
|
|
3753
|
+
}
|
|
3754
|
+
/** Identity options every server store accepts. */
|
|
3755
|
+
interface ServerIdentityOptions {
|
|
3756
|
+
/**
|
|
3757
|
+
* Deprecated. A plain id (for example one set before beta.13) is no longer the id the
|
|
3758
|
+
* server authors under: it is recorded as a legacy authoritative id, so operations
|
|
3759
|
+
* already stored under it keep their authority, and no device may use it. An id in
|
|
3760
|
+
* the `kora:server:` namespace is used verbatim (it must then be unique per instance).
|
|
3761
|
+
*/
|
|
3762
|
+
nodeId?: string;
|
|
3763
|
+
/**
|
|
3764
|
+
* Stable id of this instance within the deployment (`[A-Za-z0-9._-]`, at most 64
|
|
3765
|
+
* characters). Must be distinct per running instance. SQLite persists one on its own;
|
|
3766
|
+
* Postgres allocates a new one per start when it is not configured.
|
|
3767
|
+
*/
|
|
3768
|
+
instanceId?: string;
|
|
3769
|
+
/**
|
|
3770
|
+
* Extra node ids whose operations win `merge('server-authoritative')` fields. Every id
|
|
3771
|
+
* listed here once stays authoritative (devices keep it too) until it is revoked with
|
|
3772
|
+
* {@link revokedAuthoritativeNodeIds}: removing it from this list does not revoke it.
|
|
3773
|
+
*/
|
|
3774
|
+
authoritativeNodeIds?: string[];
|
|
3775
|
+
/**
|
|
3776
|
+
* Explicit authoritative node ids to revoke (RT-81): they stop being authoritative on
|
|
3777
|
+
* the server, the handshake tells every device to drop them (devices re-fold), and they
|
|
3778
|
+
* are still never accepted as device node ids. Revocation is permanent (persisted); an
|
|
3779
|
+
* id may not be both listed in `authoritativeNodeIds` and revoked.
|
|
3780
|
+
*/
|
|
3781
|
+
revokedAuthoritativeNodeIds?: string[];
|
|
3782
|
+
}
|
|
3783
|
+
/** True for a node id the server authors under (current or any instance of any deployment). */
|
|
3784
|
+
declare function isServerNodeId(nodeId: string): boolean;
|
|
3785
|
+
|
|
1739
3786
|
/**
|
|
1740
3787
|
* PostgreSQL-backed server store using Drizzle ORM.
|
|
1741
3788
|
* All reads and writes go through Drizzle's typed query builder.
|
|
@@ -1744,7 +3791,18 @@ declare class MemoryServerStore implements ServerStore {
|
|
|
1744
3791
|
* collection tables for efficient indexed queries (dual-write).
|
|
1745
3792
|
*/
|
|
1746
3793
|
declare class PostgresServerStore implements ServerStore {
|
|
1747
|
-
|
|
3794
|
+
/** This instance's server node id; loaded from the database during startup (RT-62). */
|
|
3795
|
+
private loadedNodeId;
|
|
3796
|
+
private readonly configuredIdentity;
|
|
3797
|
+
/** Derivation secret of server-derived ids, shared by the deployment (RT-64). */
|
|
3798
|
+
private derivationSecret;
|
|
3799
|
+
/** Legacy and configured authorities (the `kora:server:` prefix needs no listing). */
|
|
3800
|
+
private explicitAuthorities;
|
|
3801
|
+
/** Explicit authority over time: revoked and ever-held ids (RT-81). */
|
|
3802
|
+
private authorityHistory;
|
|
3803
|
+
/** Other `kora:server:` nodes with stored operations (advertised for older clients). */
|
|
3804
|
+
/** Records owning quarantined operations: folded onto their kept rows (RT-70). */
|
|
3805
|
+
private quarantine;
|
|
1748
3806
|
private readonly db;
|
|
1749
3807
|
private readonly versionVector;
|
|
1750
3808
|
private readonly ready;
|
|
@@ -1758,6 +3816,9 @@ declare class PostgresServerStore implements ServerStore {
|
|
|
1758
3816
|
* null after a backup import so it re-seeds from the restored version vector.
|
|
1759
3817
|
*/
|
|
1760
3818
|
private sequenceCounter;
|
|
3819
|
+
/** See {@link SEQUENCE_ENFORCEMENT_EPOCH_KEY}; fixed per database at first start. */
|
|
3820
|
+
private sequenceEpoch;
|
|
3821
|
+
private logIntegrity;
|
|
1761
3822
|
/**
|
|
1762
3823
|
* Time source for HLC timestamps on server-originated operations. Injectable so a
|
|
1763
3824
|
* test can freeze wall-clock time and prove the conditional-apply ordering holds
|
|
@@ -1765,7 +3826,54 @@ declare class PostgresServerStore implements ServerStore {
|
|
|
1765
3826
|
* exist to make correct). Defaults to the system clock in production.
|
|
1766
3827
|
*/
|
|
1767
3828
|
private readonly timeSource;
|
|
1768
|
-
|
|
3829
|
+
private foldOptions;
|
|
3830
|
+
/** Schema transforms the fold applies (transforms at fold time, RT-84). */
|
|
3831
|
+
private operationTransforms;
|
|
3832
|
+
private foldMigration;
|
|
3833
|
+
/**
|
|
3834
|
+
* @param db - The Drizzle database
|
|
3835
|
+
* @param nodeId - Deprecated: a plain id is recorded as a legacy authoritative id; a
|
|
3836
|
+
* `kora:server:` id is used verbatim (it must then be unique per instance). Leave
|
|
3837
|
+
* unset: the store authors under `kora:server:<deployment>:<instance>`, with the
|
|
3838
|
+
* deployment persisted in the database and a distinct instance per process (RT-62).
|
|
3839
|
+
* @param timeSource - Clock for server-originated operations (tests)
|
|
3840
|
+
* @param options - Extra authoritative ids and a stable instance id
|
|
3841
|
+
*/
|
|
3842
|
+
constructor(db: PostgresJsDatabase, nodeId?: string, timeSource?: TimeSource, options?: Omit<ServerIdentityOptions, 'nodeId'>);
|
|
3843
|
+
/** Resolves once the store's tables and identity are loaded. */
|
|
3844
|
+
whenReady(): Promise<void>;
|
|
3845
|
+
/**
|
|
3846
|
+
* This store's node id and the legacy and configured ids (explicit authorities).
|
|
3847
|
+
* Other `kora:server:` instances are not listed: the prefix rule makes every one of
|
|
3848
|
+
* them authoritative, and a list that grew by one instance id per start would make
|
|
3849
|
+
* every device re-fold after each deploy (RT-75). Handshakes advertise only the
|
|
3850
|
+
* explicit (non-prefixed) ids.
|
|
3851
|
+
*/
|
|
3852
|
+
getAuthoritativeNodeIds(): string[];
|
|
3853
|
+
/** Legacy and configured authorities (beside the `kora:server:` namespace). */
|
|
3854
|
+
getLegacyAuthoritativeNodeIds(): string[];
|
|
3855
|
+
/** Explicit authoritative ids the deployment revoked (RT-81), advertised at handshake. */
|
|
3856
|
+
getRevokedAuthoritativeNodeIds(): string[];
|
|
3857
|
+
/**
|
|
3858
|
+
* Every explicit id the deployment ever held authoritative, revoked ones included
|
|
3859
|
+
* (RT-81): never accepted as a device node id.
|
|
3860
|
+
*/
|
|
3861
|
+
getEverAuthoritativeNodeIds(): string[];
|
|
3862
|
+
deriveServerOperationId(parentOpId: string, ruleId: string, targetRecordId: string): Promise<string>;
|
|
3863
|
+
private get nodeId();
|
|
3864
|
+
/** Remember another `kora:server:` node that authored stored operations. */
|
|
3865
|
+
/**
|
|
3866
|
+
* Load (creating on first start) the persisted server identity under an advisory
|
|
3867
|
+
* lock: the deployment id and derivation secret (one per database, shared by every
|
|
3868
|
+
* instance), a fresh instance id from the database counter (unless configured), and
|
|
3869
|
+
* the legacy authoritative ids. At the first start of this release, every node id
|
|
3870
|
+
* whose operations hold an authority class in a stored fold state (a server decision
|
|
3871
|
+
* folded under an earlier, per-process server node id) is recorded as a legacy
|
|
3872
|
+
* authority, before any re-fold could drop that class.
|
|
3873
|
+
*/
|
|
3874
|
+
private loadIdentity;
|
|
3875
|
+
/** What the last startup re-materialization did (W7 step 7). */
|
|
3876
|
+
getFoldMigrationReport(): FoldMigrationReport;
|
|
1769
3877
|
getVersionVector(): VersionVector;
|
|
1770
3878
|
/**
|
|
1771
3879
|
* Atomically reserve this node's next server-operation sequence number. The
|
|
@@ -1775,8 +3883,51 @@ declare class PostgresServerStore implements ServerStore {
|
|
|
1775
3883
|
reserveSequenceNumber(): number;
|
|
1776
3884
|
getNodeId(): string;
|
|
1777
3885
|
getSchema(): SchemaDefinition | null;
|
|
1778
|
-
|
|
1779
|
-
|
|
3886
|
+
/**
|
|
3887
|
+
* One-time migration (RT-101): drop the enum `CHECK` constraints (and any `NOT NULL`
|
|
3888
|
+
* on a schema field) that beta.12 and earlier DDL put on collection tables, in one
|
|
3889
|
+
* transaction. A check is dropped when it parses as an enum check (one column against
|
|
3890
|
+
* string literals, in every form Postgres normalizes `IN (...)` to, the single-value
|
|
3891
|
+
* `col = 'x'` included, RT-108) on an enum field of the schema, or as the multi-value
|
|
3892
|
+
* `= ANY (ARRAY[...])` shape on another non-internal column; checks added by hand are
|
|
3893
|
+
* kept. Idempotent and resumable: a later start finds nothing to drop.
|
|
3894
|
+
*/
|
|
3895
|
+
private relaxValueDomainConstraints;
|
|
3896
|
+
/**
|
|
3897
|
+
* Throws {@link OperationTransformCoverageError} when a schema version in the stored
|
|
3898
|
+
* log has no transform path to `version` (RT-103): those operations would fold as
|
|
3899
|
+
* absent, silently erasing them from their records.
|
|
3900
|
+
*/
|
|
3901
|
+
private assertTransformCoverage;
|
|
3902
|
+
getOperationTransforms(): readonly OperationTransform[];
|
|
3903
|
+
setOperationTransforms(transforms: readonly OperationTransform[]): Promise<void>;
|
|
3904
|
+
setSchema(schema: SchemaDefinition, options?: ServerSchemaOptions): Promise<void>;
|
|
3905
|
+
/**
|
|
3906
|
+
* One-time migration of each collection table to the lossless text encoding (RT-65):
|
|
3907
|
+
* rows written before it that contain U+FFFF (the escape introducer) are re-encoded,
|
|
3908
|
+
* so the decoder returns them unchanged. Per table, under an advisory lock, recorded
|
|
3909
|
+
* in `kora_server_meta`.
|
|
3910
|
+
*/
|
|
3911
|
+
private migrateTextCodec;
|
|
3912
|
+
applyRemoteOperation(op: Operation, options?: ApplyRemoteOptions): Promise<ApplyResult>;
|
|
3913
|
+
/**
|
|
3914
|
+
* The version vector as committed in `sync_state`, shared by every instance
|
|
3915
|
+
* (SRV-4). Also refreshes this instance's cache, which `getVersionVector()` serves.
|
|
3916
|
+
*/
|
|
3917
|
+
readVersionVector(): Promise<VersionVector>;
|
|
3918
|
+
/**
|
|
3919
|
+
* Insert one operation row inside an append transaction that already holds the
|
|
3920
|
+
* delivery-counter lock. Returns `'duplicate'` (writing nothing) when an operation
|
|
3921
|
+
* with the same id is stored; throws {@link SequenceConflictError} when a DIFFERENT
|
|
3922
|
+
* operation stored after the sequence-enforcement epoch holds its (node, sequence)
|
|
3923
|
+
* and the writer reserves its sequences (W3 step 4); a legacy holder or a legacy
|
|
3924
|
+
* writer (RT-37) is accepted and the row is stored unflagged. Otherwise returns the
|
|
3925
|
+
* verdict the row was stored under. The partial unique index over sole-holder rows
|
|
3926
|
+
* backs the check against a writer that bypasses the counter lock.
|
|
3927
|
+
*/
|
|
3928
|
+
private insertOperationRow;
|
|
3929
|
+
/** Advance `sync_state` (the persisted version vector) for an appended operation. */
|
|
3930
|
+
private advanceSyncState;
|
|
1780
3931
|
/**
|
|
1781
3932
|
* Conditionally apply operations atomically, serialized across server instances
|
|
1782
3933
|
* on the target record via a transaction-scoped advisory lock. The lock makes
|
|
@@ -1788,7 +3939,37 @@ declare class PostgresServerStore implements ServerStore {
|
|
|
1788
3939
|
getOperationCount(): Promise<number>;
|
|
1789
3940
|
getMaxDeliverySequence(): Promise<number>;
|
|
1790
3941
|
getOperationsAfterDelivery(afterDeliverySequence: number, limit: number): Promise<DeliveredOperation[]>;
|
|
3942
|
+
getOperationScopeSnapshots(operationIds: string[]): Promise<Map<string, OperationScopeSnapshot>>;
|
|
3943
|
+
getRecordLatestTimestamp(collection: string, recordId: string): Promise<HLCTimestamp | null>;
|
|
3944
|
+
getRecordFieldVersions(collection: string, recordId: string): Promise<RecordFieldVersions | null>;
|
|
3945
|
+
getRecordFoldState(collection: string, recordId: string): Promise<FoldState | null>;
|
|
3946
|
+
getRecordOperations(collection: string, recordId: string): Promise<Operation[]>;
|
|
3947
|
+
previewOperation(op: Operation): Promise<MaterializedRecord | null>;
|
|
3948
|
+
recordBlobOwner(hash: string, owner: string): Promise<void>;
|
|
3949
|
+
getBlobOwners(hashes: string[]): Promise<Map<string, string[]>>;
|
|
3950
|
+
claimBlobIfUnowned(hash: string, owner: string): Promise<boolean>;
|
|
3951
|
+
/**
|
|
3952
|
+
* Scope values of a record as stored, for an operation's scope snapshot. With
|
|
3953
|
+
* `includeDeleted` false a soft-deleted row counts as absent; with true it keeps
|
|
3954
|
+
* its last values (a delete's post-image).
|
|
3955
|
+
*/
|
|
3956
|
+
private readScopeValues;
|
|
3957
|
+
/** Persist the scope snapshot of a just-applied operation (RT-14). */
|
|
3958
|
+
private writeScopeSnapshot;
|
|
3959
|
+
/**
|
|
3960
|
+
* Rebuild missing scope snapshots from the log (migration from a database written
|
|
3961
|
+
* before snapshots existed), replaying each affected record in commit order.
|
|
3962
|
+
*/
|
|
3963
|
+
private backfillScopeSnapshots;
|
|
1791
3964
|
materializeCollection(collection: string): Promise<MaterializedRecord[]>;
|
|
3965
|
+
getNodeIdsAfterDelivery(afterDeliverySequence: number): Promise<string[]>;
|
|
3966
|
+
getSequencePairOperations(nodeId: string, throughSequence: number): Promise<Operation[]>;
|
|
3967
|
+
recordOperationResolution(resolution: OperationResolution): Promise<void>;
|
|
3968
|
+
findOperationResolutions(nodeId: string, ids: string[]): Promise<Map<string, OperationResolution>>;
|
|
3969
|
+
deleteOperationResolution(nodeId: string, operationId: string): Promise<void>;
|
|
3970
|
+
getResolvedThrough(nodeId: string): Promise<number>;
|
|
3971
|
+
findStoredOperations(ids: string[]): Promise<Map<string, StoredOperationKey>>;
|
|
3972
|
+
findRecordsByIds(collection: string, ids: string[]): Promise<Map<string, MaterializedRecord>>;
|
|
1792
3973
|
queryCollection(collection: string, options?: CollectionQueryOptions): Promise<MaterializedRecord[]>;
|
|
1793
3974
|
findRecord(collection: string, id: string): Promise<MaterializedRecord | null>;
|
|
1794
3975
|
countCollection(collection: string, where?: Record<string, unknown>): Promise<number>;
|
|
@@ -1805,28 +3986,105 @@ declare class PostgresServerStore implements ServerStore {
|
|
|
1805
3986
|
* write so newly built operations sort strictly after it.
|
|
1806
3987
|
*/
|
|
1807
3988
|
private latestOperationTimestamp;
|
|
3989
|
+
getEncryptionKeyRecord(owner: string, keyring: string): Promise<string | null>;
|
|
3990
|
+
putEncryptionKeyRecord(owner: string, keyring: string, record: string, revision: number, expectedRevision: number): Promise<boolean>;
|
|
3991
|
+
listEncryptionKeyRecords(owner?: string): Promise<EncryptionKeyRecordRow[]>;
|
|
3992
|
+
getEncryptedKeyIds(nodeOwner: string | null, limit: number): Promise<string[]>;
|
|
3993
|
+
claimNode(nodeId: string, userId: string): Promise<boolean>;
|
|
3994
|
+
getNodeClaimOwner(nodeId: string): Promise<string | null>;
|
|
3995
|
+
replaceNodeClaim(nodeId: string, expectedOwner: string, newOwner: string): Promise<boolean>;
|
|
3996
|
+
releaseNodeClaim(nodeId: string): Promise<boolean>;
|
|
3997
|
+
/**
|
|
3998
|
+
* The record as currently stored, including a soft-deleted one (whose last field
|
|
3999
|
+
* values are kept), or null when it was never written. Without a materialized
|
|
4000
|
+
* table, the last known values are replayed from the operation log.
|
|
4001
|
+
*/
|
|
4002
|
+
private readStoredRow;
|
|
4003
|
+
/**
|
|
4004
|
+
* Merge a just-appended operation (stored under `deliverySeq`) into its record's
|
|
4005
|
+
* fold state and project the row, inside the append transaction. O(fields the
|
|
4006
|
+
* operation touches): the state is read by primary key and the freshness check is
|
|
4007
|
+
* one index probe (SRV-7).
|
|
4008
|
+
*
|
|
4009
|
+
* Concurrency: every append on every instance holds the delivery-counter row lock
|
|
4010
|
+
* until commit (see nextDeliverySeq), and applyConditional takes its per-record
|
|
4011
|
+
* advisory lock before it, so this read-merge-write of one record's state is
|
|
4012
|
+
* serialized across instances; the `FOR UPDATE` on the state row is defense in
|
|
4013
|
+
* depth. A record whose log grew without this state (an older release during a
|
|
4014
|
+
* rolling upgrade) has its missing tail merged first; an unreadable, newer-than-log
|
|
4015
|
+
* or plan-incompatible state is re-folded from the log.
|
|
4016
|
+
*/
|
|
4017
|
+
private mergeIntoRecord;
|
|
4018
|
+
/**
|
|
4019
|
+
* The fold state of a record that owns quarantined operations (RT-70): its stored
|
|
4020
|
+
* state, or its kept row as a snapshot base, with every remaining operation of the
|
|
4021
|
+
* record merged on top (`rebuildFromSnapshot`). Nothing is written.
|
|
4022
|
+
*/
|
|
4023
|
+
private rebuildAffected;
|
|
4024
|
+
/** True when the record's operation at delivery sequence `seq` has id `opId`. */
|
|
4025
|
+
private coversStoredOperation;
|
|
4026
|
+
/**
|
|
4027
|
+
* The record's current fold state for a read: the stored one when it covers the
|
|
4028
|
+
* record's whole log, else folded from the log (nothing is written).
|
|
4029
|
+
*/
|
|
4030
|
+
private readFoldState;
|
|
4031
|
+
/** A record's operations stored after `afterDeliverySeq`, in delivery order. */
|
|
4032
|
+
private readRecordOperations;
|
|
1808
4033
|
/**
|
|
1809
|
-
*
|
|
1810
|
-
*
|
|
4034
|
+
* Persist a record's fold state (covering the log up to `coveredSeq`) and project
|
|
4035
|
+
* it onto the materialized row.
|
|
1811
4036
|
*/
|
|
1812
|
-
private
|
|
4037
|
+
private writeFoldedRecord;
|
|
1813
4038
|
/**
|
|
1814
4039
|
* UPSERT a record into the materialized collection table.
|
|
1815
4040
|
*/
|
|
1816
4041
|
private upsertMaterializedRecord;
|
|
1817
4042
|
/**
|
|
1818
|
-
*
|
|
4043
|
+
* {@link writeFoldedRecord} for a batch of records of one collection, in a handful
|
|
4044
|
+
* of multi-row statements (the migration's write path). Rows are chunked so a
|
|
4045
|
+
* statement never nears the 65,535 bind-parameter limit, however wide the collection.
|
|
1819
4046
|
*/
|
|
1820
|
-
private
|
|
4047
|
+
private writeFoldedRecords;
|
|
1821
4048
|
/**
|
|
1822
|
-
*
|
|
4049
|
+
* Re-materialization migration (W7 step 7), run by setSchema and after a
|
|
4050
|
+
* replace-mode restore: every record whose fold state is missing, stale or built
|
|
4051
|
+
* under a different fold plan is re-folded from its log; nothing else is touched.
|
|
4052
|
+
* See the SQLite store's `rematerialize` for the rules (a record owning quarantined
|
|
4053
|
+
* operations folds onto its kept row, RT-70; idempotent; resumable).
|
|
4054
|
+
*
|
|
4055
|
+
* Postgres specifics: records are paged in batches of
|
|
4056
|
+
* {@link FOLD_MIGRATION_BATCH} through the (collection, record_id, delivery_seq)
|
|
4057
|
+
* index, and every batch is one transaction that first takes the delivery-counter
|
|
4058
|
+
* row lock. Appends on every instance take that lock too, so a batch reads each
|
|
4059
|
+
* record's complete log and writes its state while no append to it can commit: a
|
|
4060
|
+
* live write on another instance during a rolling deploy is never overwritten by a
|
|
4061
|
+
* stale re-fold (NEW-SRV-5), it either committed before the batch (and is folded)
|
|
4062
|
+
* or commits after it (and is merged into the batch's state).
|
|
1823
4063
|
*/
|
|
1824
|
-
|
|
4064
|
+
/**
|
|
4065
|
+
* Write the beta.12 clears the ids prove into the stored bodies, once per database
|
|
4066
|
+
* (RT-85, see `provenLegacyClears`), and mark their records' fold states stale.
|
|
4067
|
+
* Idempotent: concurrent first starts write the same bodies.
|
|
4068
|
+
*/
|
|
4069
|
+
private canonicalizeLegacyBodies;
|
|
4070
|
+
private rematerialize;
|
|
1825
4071
|
private buildSelectQuery;
|
|
1826
4072
|
private buildWhereClause;
|
|
1827
4073
|
private deserializeRow;
|
|
1828
4074
|
private materializeFromOpsLog;
|
|
1829
4075
|
private initialize;
|
|
4076
|
+
/**
|
|
4077
|
+
* The startup log-integrity scan's result (W8 step 0): rows that could not be read
|
|
4078
|
+
* back into an operation are moved to `operations_quarantine` once per database,
|
|
4079
|
+
* before any fold reads them.
|
|
4080
|
+
*/
|
|
4081
|
+
getLogIntegrityReport(): Promise<ServerLogIntegrityReport>;
|
|
4082
|
+
/**
|
|
4083
|
+
* One-time scan under the setup advisory lock (concurrent starts run it once). Keyset
|
|
4084
|
+
* pages by id, so a large log is read in bounded chunks; like the delivery-sequence
|
|
4085
|
+
* backfill it runs on the first start of this release only.
|
|
4086
|
+
*/
|
|
4087
|
+
private scanLogIntegrityOnce;
|
|
1830
4088
|
private ensureTables;
|
|
1831
4089
|
/**
|
|
1832
4090
|
* Reserve the next delivery sequence inside an open append transaction. The
|
|
@@ -1834,6 +4092,11 @@ declare class PostgresServerStore implements ServerStore {
|
|
|
1834
4092
|
* serializing assignment into commit order (see the counter table comment).
|
|
1835
4093
|
*/
|
|
1836
4094
|
private nextDeliverySeq;
|
|
4095
|
+
/**
|
|
4096
|
+
* @param soleHolder - Nothing else held the (node, sequence) at insert: the row is
|
|
4097
|
+
* flagged `seq_unique` and covered by {@link NODE_SEQ_UNIQUE_INDEX}. False for a
|
|
4098
|
+
* legacy pair's second operation and for restored backup rows.
|
|
4099
|
+
*/
|
|
1837
4100
|
private serializeOperation;
|
|
1838
4101
|
private deserializeOperation;
|
|
1839
4102
|
private assertOpen;
|
|
@@ -1845,8 +4108,14 @@ declare class PostgresServerStore implements ServerStore {
|
|
|
1845
4108
|
*/
|
|
1846
4109
|
declare function createPostgresServerStore(options: {
|
|
1847
4110
|
connectionString: string;
|
|
1848
|
-
|
|
1849
|
-
|
|
4111
|
+
} & ServerIdentityOptions): Promise<PostgresServerStore>;
|
|
4112
|
+
/**
|
|
4113
|
+
* The Postgres server store could not start: the database is unreachable, refused the
|
|
4114
|
+
* credentials, or its tables could not be created. `cause` is the driver's error.
|
|
4115
|
+
*/
|
|
4116
|
+
declare class ServerStoreUnavailableError extends KoraError {
|
|
4117
|
+
constructor(connectionString: string, cause: unknown);
|
|
4118
|
+
}
|
|
1850
4119
|
|
|
1851
4120
|
/**
|
|
1852
4121
|
* SQLite-backed server store using Drizzle ORM.
|
|
@@ -1861,46 +4130,206 @@ declare class SqliteServerStore implements ServerStore {
|
|
|
1861
4130
|
private readonly db;
|
|
1862
4131
|
private schema;
|
|
1863
4132
|
private closed;
|
|
1864
|
-
|
|
4133
|
+
/** See {@link SEQUENCE_ENFORCEMENT_EPOCH_KEY}; fixed per database at first start. */
|
|
4134
|
+
private sequenceEpoch;
|
|
4135
|
+
private logIntegrity;
|
|
4136
|
+
/** Derivation secret of server-derived ids, shared by the deployment (RT-64). */
|
|
4137
|
+
private readonly derivationSecret;
|
|
4138
|
+
/** Legacy and configured authorities (the `kora:server:` prefix needs no listing). */
|
|
4139
|
+
private readonly explicitAuthorities;
|
|
4140
|
+
/** Explicit authority over time: revoked and ever-held ids (RT-81). */
|
|
4141
|
+
private readonly authorityHistory;
|
|
4142
|
+
/** Other `kora:server:` nodes with stored operations (advertised for older clients). */
|
|
4143
|
+
/** Records owning quarantined operations: folded onto their kept rows (RT-70). */
|
|
4144
|
+
private quarantine;
|
|
4145
|
+
private foldOptions;
|
|
4146
|
+
/** Schema transforms the fold applies (transforms at fold time, RT-84). */
|
|
4147
|
+
private operationTransforms;
|
|
4148
|
+
private foldMigration;
|
|
4149
|
+
/**
|
|
4150
|
+
* @param db - The Drizzle database
|
|
4151
|
+
* @param nodeId - Deprecated: a plain id is recorded as a legacy authoritative id; a
|
|
4152
|
+
* `kora:server:` id is used verbatim. Leave unset: the store authors under
|
|
4153
|
+
* `kora:server:<deployment>:<instance>`, both persisted in the database (RT-62).
|
|
4154
|
+
* @param options - Extra authoritative ids and the instance id
|
|
4155
|
+
*/
|
|
4156
|
+
constructor(db: BetterSQLite3Database, nodeId?: string, options?: Omit<ServerIdentityOptions, 'nodeId'>);
|
|
4157
|
+
/**
|
|
4158
|
+
* This store's node id and the legacy and configured ids (explicit authorities).
|
|
4159
|
+
* Other `kora:server:` instances are not listed: the prefix rule makes every one of
|
|
4160
|
+
* them authoritative, and a list that grew by one instance id per start would make
|
|
4161
|
+
* every device re-fold after each deploy (RT-75). Handshakes advertise only the
|
|
4162
|
+
* explicit (non-prefixed) ids.
|
|
4163
|
+
*/
|
|
4164
|
+
getAuthoritativeNodeIds(): string[];
|
|
4165
|
+
/** Legacy and configured authorities (beside the `kora:server:` namespace). */
|
|
4166
|
+
getLegacyAuthoritativeNodeIds(): string[];
|
|
4167
|
+
/** Explicit authoritative ids the deployment revoked (RT-81), advertised at handshake. */
|
|
4168
|
+
getRevokedAuthoritativeNodeIds(): string[];
|
|
4169
|
+
/**
|
|
4170
|
+
* Every explicit id the deployment ever held authoritative, revoked ones included
|
|
4171
|
+
* (RT-81): never accepted as a device node id.
|
|
4172
|
+
*/
|
|
4173
|
+
getEverAuthoritativeNodeIds(): string[];
|
|
4174
|
+
deriveServerOperationId(parentOpId: string, ruleId: string, targetRecordId: string): Promise<string>;
|
|
4175
|
+
/** Remember another `kora:server:` node that authored stored operations. */
|
|
4176
|
+
/**
|
|
4177
|
+
* Load (creating on first start) the persisted server identity: the deployment id,
|
|
4178
|
+
* the derivation secret, this database's instance id and the legacy authoritative
|
|
4179
|
+
* ids. At the first start of this release, every node id whose operations hold an
|
|
4180
|
+
* authority class in a stored fold state (a server decision folded under an earlier,
|
|
4181
|
+
* per-process server node id) is recorded as a legacy authority, before any re-fold
|
|
4182
|
+
* could drop that class.
|
|
4183
|
+
*/
|
|
4184
|
+
private loadIdentity;
|
|
4185
|
+
/** What the last startup re-materialization did (W7 step 7). */
|
|
4186
|
+
getFoldMigrationReport(): FoldMigrationReport;
|
|
1865
4187
|
getVersionVector(): VersionVector;
|
|
1866
4188
|
getNodeId(): string;
|
|
1867
4189
|
getSchema(): SchemaDefinition | null;
|
|
1868
|
-
setSchema(schema: SchemaDefinition): Promise<void>;
|
|
1869
|
-
|
|
4190
|
+
setSchema(schema: SchemaDefinition, options?: ServerSchemaOptions): Promise<void>;
|
|
4191
|
+
/**
|
|
4192
|
+
* One-time migration (RT-101): rebuild collection tables that still carry an enum
|
|
4193
|
+
* `CHECK` (or `NOT NULL` on a schema field), in one transaction, keeping rows, indexes
|
|
4194
|
+
* and triggers. Idempotent (a relaxed table is left alone) and resumable (an
|
|
4195
|
+
* interrupted rebuild rolls back and runs again at the next start).
|
|
4196
|
+
*/
|
|
4197
|
+
private relaxValueDomainConstraints;
|
|
4198
|
+
/**
|
|
4199
|
+
* Throws {@link OperationTransformCoverageError} when a schema version in the stored
|
|
4200
|
+
* log has no transform path to `version` (RT-103): those operations would fold as
|
|
4201
|
+
* absent, silently erasing them from their records.
|
|
4202
|
+
*/
|
|
4203
|
+
private assertTransformCoverage;
|
|
4204
|
+
getOperationTransforms(): readonly OperationTransform[];
|
|
4205
|
+
setOperationTransforms(transforms: readonly OperationTransform[]): Promise<void>;
|
|
4206
|
+
applyRemoteOperation(op: Operation, options?: ApplyRemoteOptions): Promise<ApplyResult>;
|
|
4207
|
+
private applyInTransaction;
|
|
1870
4208
|
getOperationRange(nodeId: string, fromSeq: number, toSeq: number): Promise<Operation[]>;
|
|
1871
4209
|
getOperationCount(): Promise<number>;
|
|
1872
4210
|
getMaxDeliverySequence(): Promise<number>;
|
|
1873
4211
|
getOperationsAfterDelivery(afterDeliverySequence: number, limit: number): Promise<DeliveredOperation[]>;
|
|
4212
|
+
getOperationScopeSnapshots(operationIds: string[]): Promise<Map<string, OperationScopeSnapshot>>;
|
|
4213
|
+
getRecordLatestTimestamp(collection: string, recordId: string): Promise<HLCTimestamp | null>;
|
|
4214
|
+
getRecordFieldVersions(collection: string, recordId: string): Promise<RecordFieldVersions | null>;
|
|
4215
|
+
getRecordFoldState(collection: string, recordId: string): Promise<FoldState | null>;
|
|
4216
|
+
getRecordOperations(collection: string, recordId: string): Promise<Operation[]>;
|
|
4217
|
+
previewOperation(op: Operation): Promise<MaterializedRecord | null>;
|
|
4218
|
+
recordBlobOwner(hash: string, owner: string): Promise<void>;
|
|
4219
|
+
getBlobOwners(hashes: string[]): Promise<Map<string, string[]>>;
|
|
4220
|
+
claimBlobIfUnowned(hash: string, owner: string): Promise<boolean>;
|
|
4221
|
+
/**
|
|
4222
|
+
* Scope values of a record as stored, for an operation's scope snapshot. With
|
|
4223
|
+
* `includeDeleted` false a soft-deleted row counts as absent (the pre-image of a
|
|
4224
|
+
* write to a deleted record); with true it keeps its last values (a delete's
|
|
4225
|
+
* post-image).
|
|
4226
|
+
*/
|
|
4227
|
+
private readScopeValues;
|
|
4228
|
+
/**
|
|
4229
|
+
* Rebuild missing scope snapshots from the log (migration from a database written
|
|
4230
|
+
* before snapshots existed), replaying each affected record in commit order.
|
|
4231
|
+
*/
|
|
4232
|
+
private backfillScopeSnapshots;
|
|
1874
4233
|
materializeCollection(collection: string): Promise<MaterializedRecord[]>;
|
|
4234
|
+
getNodeIdsAfterDelivery(afterDeliverySequence: number): Promise<string[]>;
|
|
4235
|
+
getSequencePairOperations(nodeId: string, throughSequence: number): Promise<Operation[]>;
|
|
4236
|
+
recordOperationResolution(resolution: OperationResolution): Promise<void>;
|
|
4237
|
+
findOperationResolutions(nodeId: string, ids: string[]): Promise<Map<string, OperationResolution>>;
|
|
4238
|
+
deleteOperationResolution(nodeId: string, operationId: string): Promise<void>;
|
|
4239
|
+
getResolvedThrough(nodeId: string): Promise<number>;
|
|
4240
|
+
findStoredOperations(ids: string[]): Promise<Map<string, StoredOperationKey>>;
|
|
4241
|
+
findRecordsByIds(collection: string, ids: string[]): Promise<Map<string, MaterializedRecord>>;
|
|
1875
4242
|
queryCollection(collection: string, options?: CollectionQueryOptions): Promise<MaterializedRecord[]>;
|
|
1876
4243
|
findRecord(collection: string, id: string): Promise<MaterializedRecord | null>;
|
|
1877
4244
|
countCollection(collection: string, where?: Record<string, unknown>): Promise<number>;
|
|
1878
4245
|
close(): Promise<void>;
|
|
4246
|
+
getEncryptionKeyRecord(owner: string, keyring: string): Promise<string | null>;
|
|
4247
|
+
putEncryptionKeyRecord(owner: string, keyring: string, record: string, revision: number, expectedRevision: number): Promise<boolean>;
|
|
4248
|
+
listEncryptionKeyRecords(owner?: string): Promise<EncryptionKeyRecordRow[]>;
|
|
4249
|
+
getEncryptedKeyIds(nodeOwner: string | null, limit: number): Promise<string[]>;
|
|
4250
|
+
claimNode(nodeId: string, userId: string): Promise<boolean>;
|
|
4251
|
+
getNodeClaimOwner(nodeId: string): Promise<string | null>;
|
|
4252
|
+
replaceNodeClaim(nodeId: string, expectedOwner: string, newOwner: string): Promise<boolean>;
|
|
4253
|
+
releaseNodeClaim(nodeId: string): Promise<boolean>;
|
|
4254
|
+
/**
|
|
4255
|
+
* The record as currently stored, including a soft-deleted one (whose last field
|
|
4256
|
+
* values are kept), or null when it was never written. Without a materialized
|
|
4257
|
+
* table, the last known values are replayed from the operation log.
|
|
4258
|
+
*/
|
|
4259
|
+
private readStoredRow;
|
|
4260
|
+
/**
|
|
4261
|
+
* The fold state of a record that owns quarantined operations (RT-70): its stored
|
|
4262
|
+
* state, or its kept row as a snapshot base, with every remaining operation of the
|
|
4263
|
+
* record merged on top (`rebuildFromSnapshot`). Nothing is written.
|
|
4264
|
+
*/
|
|
4265
|
+
private rebuildAffected;
|
|
1879
4266
|
exportBackup(): Promise<Uint8Array>;
|
|
1880
4267
|
importBackup(data: Uint8Array, merge?: boolean): Promise<{
|
|
1881
4268
|
operationsRestored: number;
|
|
1882
4269
|
success: boolean;
|
|
1883
4270
|
}>;
|
|
1884
4271
|
/**
|
|
1885
|
-
*
|
|
1886
|
-
*
|
|
1887
|
-
*
|
|
4272
|
+
* Merge a just-appended operation (stored under `deliverySeq`) into its record's
|
|
4273
|
+
* fold state and project the row, inside the append transaction. O(fields the
|
|
4274
|
+
* operation touches): the stored state is read by primary key and the freshness
|
|
4275
|
+
* check is one index probe, never a scan of the record's history (SRV-7).
|
|
4276
|
+
*
|
|
4277
|
+
* The stored state records the highest delivery sequence it covers. If another
|
|
4278
|
+
* writer appended operations to this record without folding them (an older release
|
|
4279
|
+
* during a rolling upgrade), the missing tail is merged first; if the stored state
|
|
4280
|
+
* is unreadable, newer than the log, or built under an incompatible field kind,
|
|
4281
|
+
* the record is re-folded from its log.
|
|
4282
|
+
*/
|
|
4283
|
+
private mergeIntoRecord;
|
|
4284
|
+
/** True when the record's operation at delivery sequence `seq` has id `opId`. */
|
|
4285
|
+
private coversStoredOperation;
|
|
4286
|
+
/**
|
|
4287
|
+
* The record's current fold state for a read: the stored one when it covers the
|
|
4288
|
+
* record's whole log, else folded from the log (nothing is written).
|
|
1888
4289
|
*/
|
|
1889
|
-
private
|
|
4290
|
+
private readFoldState;
|
|
4291
|
+
/** A record's operations stored after `afterDeliverySeq`, in delivery order. */
|
|
4292
|
+
private readRecordOperations;
|
|
4293
|
+
/**
|
|
4294
|
+
* Persist a record's fold state (covering the log up to `coveredSeq`) and project
|
|
4295
|
+
* it onto the materialized row.
|
|
4296
|
+
*/
|
|
4297
|
+
private writeFoldedRecord;
|
|
1890
4298
|
/**
|
|
1891
4299
|
* UPSERT a record into the materialized collection table.
|
|
1892
4300
|
* Uses INSERT ... ON CONFLICT (id) DO UPDATE SET for atomic upsert.
|
|
1893
4301
|
*/
|
|
1894
4302
|
private upsertMaterializedRecord;
|
|
1895
4303
|
/**
|
|
1896
|
-
*
|
|
1897
|
-
*
|
|
4304
|
+
* Re-materialization migration (W7 step 7), run by setSchema and after a
|
|
4305
|
+
* replace-mode restore. Every record whose fold state is missing, stale (it covers
|
|
4306
|
+
* less, or more, of the log than is stored) or built under a different fold plan
|
|
4307
|
+
* (field kind, merge strategy, resolver) is re-folded from its log and its row
|
|
4308
|
+
* rewritten; nothing else is touched, so a restart on an up-to-date database does
|
|
4309
|
+
* one aggregate read.
|
|
4310
|
+
*
|
|
4311
|
+
* - Runs only on a log the W8b integrity scan reports clean. With quarantined rows,
|
|
4312
|
+
* rows materialized before the fold existed are kept as they are (re-folding a
|
|
4313
|
+
* log that lost operations would bake the loss in) and the skip is logged loudly;
|
|
4314
|
+
* such a record is folded from its remaining log on its next write. Records with
|
|
4315
|
+
* no row yet, and fold states this release already wrote, are still processed.
|
|
4316
|
+
* - Idempotent and resumable: each batch commits on its own, and staleness is
|
|
4317
|
+
* re-derived from the tables at every start, so a crash resumes where it stopped.
|
|
4318
|
+
* A plan change first marks every stored state stale (one statement), then
|
|
4319
|
+
* records the new fingerprint, so it is resumable too.
|
|
4320
|
+
*/
|
|
4321
|
+
/**
|
|
4322
|
+
* One-time migration of each collection table to the lossless text encoding (RT-65):
|
|
4323
|
+
* rows written before it whose raw-string columns contain U+FFFF (the escape
|
|
4324
|
+
* introducer) are re-encoded, so the decoder returns them unchanged.
|
|
1898
4325
|
*/
|
|
1899
|
-
private
|
|
4326
|
+
private migrateTextCodec;
|
|
1900
4327
|
/**
|
|
1901
|
-
*
|
|
4328
|
+
* Write the beta.12 clears the ids prove into the stored bodies, once per database
|
|
4329
|
+
* (RT-85, see `provenLegacyClears`), and mark their records' fold states stale.
|
|
1902
4330
|
*/
|
|
1903
|
-
private
|
|
4331
|
+
private canonicalizeLegacyBodies;
|
|
4332
|
+
private rematerialize;
|
|
1904
4333
|
private buildSelectQuery;
|
|
1905
4334
|
private buildWhereClause;
|
|
1906
4335
|
private deserializeRow;
|
|
@@ -1909,6 +4338,29 @@ declare class SqliteServerStore implements ServerStore {
|
|
|
1909
4338
|
* Create the operations and sync_state tables if they don't exist.
|
|
1910
4339
|
*/
|
|
1911
4340
|
private ensureTables;
|
|
4341
|
+
/**
|
|
4342
|
+
* The startup log-integrity scan's result (W8 step 0): rows that could not be read
|
|
4343
|
+
* back into an operation are moved to `operations_quarantine` once per database,
|
|
4344
|
+
* before any fold reads them.
|
|
4345
|
+
*/
|
|
4346
|
+
getLogIntegrityReport(): ServerLogIntegrityReport;
|
|
4347
|
+
private scanLogIntegrityOnce;
|
|
4348
|
+
/**
|
|
4349
|
+
* Index the legacy pairs already in the log once (history written before the
|
|
4350
|
+
* `sequence_pairs` table existed); appends maintain it from then on.
|
|
4351
|
+
*/
|
|
4352
|
+
private backfillSequencePairs;
|
|
4353
|
+
/**
|
|
4354
|
+
* Record the sequence-enforcement epoch on the first start of this release (the log's
|
|
4355
|
+
* highest delivery sequence then; see {@link SEQUENCE_ENFORCEMENT_EPOCH_KEY}) and
|
|
4356
|
+
* back the append check with the partial unique index over sole-holder rows
|
|
4357
|
+
* ({@link NODE_SEQ_UNIQUE_INDEX}), which therefore always exists: a legacy duplicate
|
|
4358
|
+
* pair is stored unflagged. Replaces the earlier indexes (a full one, created only
|
|
4359
|
+
* when the log had no duplicates, and one over the rows past the epoch, which a
|
|
4360
|
+
* legacy client's pair would violate). Runs in one write transaction, so stores
|
|
4361
|
+
* sharing a file agree on the epoch.
|
|
4362
|
+
*/
|
|
4363
|
+
private ensureSequenceEnforcement;
|
|
1912
4364
|
/**
|
|
1913
4365
|
* Assign delivery sequences to any operations written before the column existed,
|
|
1914
4366
|
* then seed the durable counter from the current max. Ordered by
|
|
@@ -1922,6 +4374,11 @@ declare class SqliteServerStore implements ServerStore {
|
|
|
1922
4374
|
* allocation shared by every SqliteServerStore instance using the same file.
|
|
1923
4375
|
*/
|
|
1924
4376
|
private nextDeliverySeq;
|
|
4377
|
+
/**
|
|
4378
|
+
* @param soleHolder - Nothing else held the (node, sequence) at insert: the row is
|
|
4379
|
+
* flagged `seq_unique` and covered by {@link NODE_SEQ_UNIQUE_INDEX}. False for a
|
|
4380
|
+
* legacy pair's second operation and for restored backup rows.
|
|
4381
|
+
*/
|
|
1925
4382
|
private serializeOperation;
|
|
1926
4383
|
private deserializeOperation;
|
|
1927
4384
|
private assertOpen;
|
|
@@ -1934,7 +4391,9 @@ declare class SqliteServerStore implements ServerStore {
|
|
|
1934
4391
|
*
|
|
1935
4392
|
* @param options - Configuration options
|
|
1936
4393
|
* @param options.filename - Path to SQLite database file. Defaults to ':memory:' for testing.
|
|
1937
|
-
* @param options.nodeId -
|
|
4394
|
+
* @param options.nodeId - Deprecated (a plain id becomes a legacy authoritative id). Leave
|
|
4395
|
+
* unset: the server authors under the persisted `kora:server:<deployment>:<instance>`.
|
|
4396
|
+
* @param options.instanceId - Optional stable instance id
|
|
1938
4397
|
* @returns A ready-to-use SqliteServerStore
|
|
1939
4398
|
*
|
|
1940
4399
|
* @example
|
|
@@ -1951,8 +4410,7 @@ declare class SqliteServerStore implements ServerStore {
|
|
|
1951
4410
|
*/
|
|
1952
4411
|
declare function createSqliteServerStore(options: {
|
|
1953
4412
|
filename?: string;
|
|
1954
|
-
|
|
1955
|
-
}): SqliteServerStore;
|
|
4413
|
+
} & ServerIdentityOptions): SqliteServerStore;
|
|
1956
4414
|
|
|
1957
4415
|
/**
|
|
1958
4416
|
* Auth provider that accepts all connections.
|
|
@@ -1963,33 +4421,147 @@ declare class NoAuthProvider implements AuthProvider {
|
|
|
1963
4421
|
authenticate(_token: string): Promise<AuthContext>;
|
|
1964
4422
|
}
|
|
1965
4423
|
|
|
4424
|
+
/**
|
|
4425
|
+
* Thrown when a sync scope predicate value is `undefined` or `null` (also inside a
|
|
4426
|
+
* `$in` list). Such a value matches every record that lacks the field, so a grant
|
|
4427
|
+
* built from a failed lookup (for example `{ orgId: user.orgId }` with no org)
|
|
4428
|
+
* would expose every unowned record. Kora refuses it instead (fail closed).
|
|
4429
|
+
*/
|
|
4430
|
+
declare class InvalidScopePredicateError extends KoraError {
|
|
4431
|
+
readonly collection: string;
|
|
4432
|
+
readonly field: string;
|
|
4433
|
+
constructor(collection: string, field: string);
|
|
4434
|
+
}
|
|
4435
|
+
/** Thrown when a `$in` scope predicate is malformed or larger than the configured limit. */
|
|
4436
|
+
declare class ScopePredicateLimitError extends KoraError {
|
|
4437
|
+
constructor(message: string, context: Record<string, unknown>);
|
|
4438
|
+
}
|
|
4439
|
+
|
|
4440
|
+
interface ResolveSessionScopesOptions {
|
|
4441
|
+
/** Scope map sent by the client during handshake. It can only NARROW the grant. */
|
|
4442
|
+
handshakeScope?: ScopeMap$1;
|
|
4443
|
+
/**
|
|
4444
|
+
* The server grant from the auth provider. Its collection set is authoritative:
|
|
4445
|
+
* a collection it does not list is not visible, whatever the handshake asks for.
|
|
4446
|
+
* It may carry verified claim values under `$claims` (see `claimScopes` in
|
|
4447
|
+
* `@korajs/core`), which are bound to every schema-scoped collection.
|
|
4448
|
+
*/
|
|
4449
|
+
authScopes?: ScopeMap$1;
|
|
4450
|
+
/**
|
|
4451
|
+
* Verified, server-derived scope values (for example `{ userId: sub }`). Treated
|
|
4452
|
+
* exactly like `$claims` in `authScopes`. Never pass client-supplied values here.
|
|
4453
|
+
*/
|
|
4454
|
+
scopeValues?: Record<string, unknown>;
|
|
4455
|
+
/**
|
|
4456
|
+
* True when an auth provider authenticated this session. An authenticated
|
|
4457
|
+
* session whose provider returned no grant at all is denied every
|
|
4458
|
+
* schema-scoped collection instead of trusting the client handshake.
|
|
4459
|
+
*
|
|
4460
|
+
* Leave it false only for servers without an auth provider (or with
|
|
4461
|
+
* `NoAuthProvider`), where the handshake is a client-side filter, not a grant.
|
|
4462
|
+
* @default false
|
|
4463
|
+
*/
|
|
4464
|
+
authenticated?: boolean;
|
|
4465
|
+
/**
|
|
4466
|
+
* What to do when a schema-scoped collection's bindings cannot all be resolved
|
|
4467
|
+
* from the verified claims. `'deny'` hides the collection (fail closed);
|
|
4468
|
+
* `'throw'` raises {@link ScopeRequiredError} so the caller can reject the
|
|
4469
|
+
* handshake with `SCOPE_REQUIRED`.
|
|
4470
|
+
* @default 'deny'
|
|
4471
|
+
*/
|
|
4472
|
+
onUnresolved?: 'deny' | 'throw';
|
|
4473
|
+
}
|
|
4474
|
+
/** A schema-scoped collection the session was denied because a binding was unresolved. */
|
|
4475
|
+
interface DeniedScopeCollection {
|
|
4476
|
+
collection: string;
|
|
4477
|
+
/** Scope binding keys (for example `orgId`) that had no verified value. */
|
|
4478
|
+
missingKeys: string[];
|
|
4479
|
+
}
|
|
4480
|
+
/** Detailed result of {@link resolveSessionScopeGrant}. */
|
|
4481
|
+
interface ResolvedSessionScopeGrant {
|
|
4482
|
+
/**
|
|
4483
|
+
* Effective scope map. `undefined` means "no scoping" and is only ever returned
|
|
4484
|
+
* for unauthenticated servers without a grant. An empty object means "nothing
|
|
4485
|
+
* visible".
|
|
4486
|
+
*/
|
|
4487
|
+
scopes: ScopeMap$1 | undefined;
|
|
4488
|
+
/** Collections denied because their scope bindings could not be resolved. */
|
|
4489
|
+
denied: DeniedScopeCollection[];
|
|
4490
|
+
}
|
|
4491
|
+
/**
|
|
4492
|
+
* Thrown (with `onUnresolved: 'throw'`) when an authenticated session cannot be
|
|
4493
|
+
* bound to a schema-scoped collection because the server grant lacks a value.
|
|
4494
|
+
*/
|
|
4495
|
+
declare class ScopeRequiredError extends KoraError {
|
|
4496
|
+
readonly denied: DeniedScopeCollection[];
|
|
4497
|
+
constructor(denied: DeniedScopeCollection[]);
|
|
4498
|
+
}
|
|
4499
|
+
/**
|
|
4500
|
+
* Resolve the effective per-session sync scope map.
|
|
4501
|
+
*
|
|
4502
|
+
* The result is the INTERSECTION of the server grant and the client handshake:
|
|
4503
|
+
* - The collection set is the grant's collection set. A handshake can never add
|
|
4504
|
+
* a collection.
|
|
4505
|
+
* - Per collection, handshake predicates only narrow: grant fields win, a
|
|
4506
|
+
* handshake field on an unconstrained field is added, and for an `$in` grant a
|
|
4507
|
+
* handshake value is used only when it is a subset of the granted values.
|
|
4508
|
+
* - Schema-scoped collections bound from verified claims fail closed: an
|
|
4509
|
+
* unresolved binding denies the collection instead of producing `{}`.
|
|
4510
|
+
*
|
|
4511
|
+
* Invariant: the effective scope is a subset of the server grant, and no
|
|
4512
|
+
* schema-scoped collection resolves to `{}` from claims for an authenticated
|
|
4513
|
+
* principal.
|
|
4514
|
+
*
|
|
4515
|
+
* @returns The effective scope map; `undefined` only for unauthenticated servers
|
|
4516
|
+
* with neither a grant nor a handshake scope
|
|
4517
|
+
*/
|
|
4518
|
+
declare function resolveSessionScopes(schema: SchemaDefinition | null, options: ResolveSessionScopesOptions): ScopeMap$1 | undefined;
|
|
4519
|
+
/**
|
|
4520
|
+
* Same as {@link resolveSessionScopes}, but also reports which collections were
|
|
4521
|
+
* denied for unresolved bindings, so a caller can surface `SCOPE_REQUIRED`.
|
|
4522
|
+
*/
|
|
4523
|
+
declare function resolveSessionScopeGrant(schema: SchemaDefinition | null, options: ResolveSessionScopesOptions): ResolvedSessionScopeGrant;
|
|
4524
|
+
|
|
1966
4525
|
/**
|
|
1967
4526
|
* Server-side awareness relay. Broadcasts ephemeral awareness states
|
|
1968
4527
|
* (cursor positions, user presence) between connected clients.
|
|
1969
4528
|
*
|
|
1970
|
-
* Awareness data is never persisted.
|
|
1971
|
-
*
|
|
1972
|
-
*
|
|
4529
|
+
* Awareness data is never persisted. Each session's awareness `clientId` is bound on
|
|
4530
|
+
* registration: a session may only publish (or clear) its own state, never another
|
|
4531
|
+
* client's, and its updates reach only clients in the same partition.
|
|
1973
4532
|
*/
|
|
1974
4533
|
declare class AwarenessRelay {
|
|
1975
4534
|
private readonly clients;
|
|
1976
4535
|
/**
|
|
1977
|
-
* Register a client for awareness broadcasting.
|
|
4536
|
+
* Register a client for awareness broadcasting. A session registers once; later
|
|
4537
|
+
* calls for an already-registered session are ignored, so a client cannot rebind
|
|
4538
|
+
* itself to another client's id.
|
|
1978
4539
|
*
|
|
1979
4540
|
* @param sessionId - Unique session identifier
|
|
1980
4541
|
* @param clientId - Client-assigned awareness ID
|
|
1981
4542
|
* @param transport - Transport for sending messages to this client
|
|
4543
|
+
* @param partition - Presence partition (clients only see their own partition)
|
|
4544
|
+
*/
|
|
4545
|
+
addClient(sessionId: string, clientId: number, transport: ServerTransport, partition?: string): void;
|
|
4546
|
+
/**
|
|
4547
|
+
* True when the session is registered with the relay.
|
|
4548
|
+
*
|
|
4549
|
+
* @param sessionId - Session to check
|
|
1982
4550
|
*/
|
|
1983
|
-
|
|
4551
|
+
hasClient(sessionId: string): boolean;
|
|
1984
4552
|
/**
|
|
1985
|
-
* Remove a client and broadcast its removal to
|
|
4553
|
+
* Remove a client and broadcast its removal to the remaining clients in its partition.
|
|
1986
4554
|
*
|
|
1987
4555
|
* @param sessionId - Session ID of the disconnecting client
|
|
1988
4556
|
*/
|
|
1989
4557
|
removeClient(sessionId: string): void;
|
|
1990
4558
|
/**
|
|
1991
|
-
* Handle an incoming awareness update from a client.
|
|
1992
|
-
*
|
|
4559
|
+
* Handle an incoming awareness update from a registered client.
|
|
4560
|
+
*
|
|
4561
|
+
* Only the sender's own entry is accepted: the message must be stamped with the
|
|
4562
|
+
* sender's bound `clientId`, and only `states[clientId]` is stored and relayed.
|
|
4563
|
+
* Entries for other clients are dropped, so a sender cannot overwrite or remove
|
|
4564
|
+
* anyone else's presence. The sanitized update reaches only the sender's partition.
|
|
1993
4565
|
*
|
|
1994
4566
|
* @param sessionId - Session ID of the sending client
|
|
1995
4567
|
* @param message - The awareness update message
|
|
@@ -2023,4 +4595,4 @@ declare class AwarenessRelay {
|
|
|
2023
4595
|
*/
|
|
2024
4596
|
declare function createKoraServer(config: KoraSyncServerConfig): KoraSyncServer;
|
|
2025
4597
|
|
|
2026
|
-
export { type AuthContext, type AuthProvider, AwarenessRelay, type AwarenessRelayCallback, type ClientMetrics, ClientSession, type ClientSessionOptions, type CollectionQueryOptions, DEFAULT_MAX_SCOPE_PREDICATE_VALUES, 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 RouteConditionalApply, type RouteConditionalResult, type RouteMutation, type RoutePredicate, type RoutePredicateOperators, 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, normalizeScopeMap, operationMatchesScopes };
|
|
4598
|
+
export { type ApplyRemoteOptions, type AuthContext, type AuthProvider, AwarenessRelay, type AwarenessRelayCallback, type ClientMetrics, ClientSession, type ClientSessionOptions, type CollectionQueryOptions, DEFAULT_MAX_SCOPE_PREDICATE_VALUES, type DeniedScopeCollection, type FoldMigrationReport, type HttpPollResponse, HttpServerTransport, type HttpSyncRequest, type HttpSyncResponse, InvalidScopePredicateError, KoraAuthProvider, type KoraAuthProviderOptions, KoraSyncServer, type KoraSyncServerConfig, type LogEntry, type LogLevel, type Logger, type MaterializedRecord, MemoryServerStore, type MemoryServerStoreOptions, 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 ResolveSessionScopesOptions, type ResolvedSessionScopeGrant, type RouteApplyResult, type RouteConditionalApply, type RouteConditionalResult, type RouteMutation, type RoutePredicate, type RoutePredicateOperators, type RouteScopeOptions, SERVER_NODE_PREFIX, ScopePredicateLimitError, ScopeRequiredError, ServerCloseHandler, ServerErrorHandler, ServerIdentityError, type ServerIdentityOptions, ServerMessageHandler, ServerMetricsCollector, type ServerMetricsSnapshot, type ServerSchemaOptions, type ServerStatus, type ServerStore, ServerStoreUnavailableError, ServerTransport, type SessionRevocation, type SessionState, type SessionTerminationCode, SqliteServerStore, type StoredOperationKey, type TerminateSessionsFilter, TokenAuthProvider, type TokenAuthProviderOptions, type TrustProxySetting, type UplinkAuthorizationCode, UplinkAuthorizationError, type UplinkAuthorizationResult, type WsServerConstructor, type WsServerLike, WsServerTransport, type WsServerTransportOptions, type WsWebSocket, authorizeUplinkWrite, createDefaultLogger, createJsonLogger, createKoraServer, createPostgresServerStore, createPrettyLogger, createProductionServer, createSilentLogger, createSqliteServerStore, isRetriableRejection, isServerNodeId, normalizeScopeMap, operationMatchesScopes, resolveSessionScopeGrant, resolveSessionScopes };
|