@korajs/server 1.0.0-beta.12 → 1.0.0-beta.14
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-UKWG5T6J.js +188 -0
- package/dist/chunk-UKWG5T6J.js.map +1 -0
- package/dist/index.cjs +10962 -1337
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3100 -151
- package/dist/index.d.ts +3100 -151
- package/dist/index.js +10556 -1215
- 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-KITNPMZJ.js +249 -0
- package/dist/server-backup-KITNPMZJ.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 +5 -4
- package/dist/server-backup-MOICK6MI.js +0 -116
- package/dist/server-backup-MOICK6MI.js.map +0 -1
package/dist/index.d.ts
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.js';
|
|
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,277 @@ 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
|
+
* Atomically claim a node id that has no real owner for `userId` (F1): a node
|
|
407
|
+
* with operation history but no claim (written before node claims existed, by a
|
|
408
|
+
* beta.12 or older server) or one an admin released (owner `''`). A node another
|
|
409
|
+
* principal owns is never taken. One statement, so there is no gap between a
|
|
410
|
+
* release and a claim another claimant could slip into. Returns true when
|
|
411
|
+
* `userId` owns the node afterwards. The server calls it only for a node whose id
|
|
412
|
+
* equals the device id the auth provider verified for that user. Optional for
|
|
413
|
+
* custom stores; without it such nodes stay refused until an admin binds them.
|
|
414
|
+
*/
|
|
415
|
+
claimUnownedNode?(nodeId: string, userId: string): Promise<boolean>;
|
|
416
|
+
/**
|
|
417
|
+
* Admin release of a node id (RT-5): the next principal to claim it takes it
|
|
418
|
+
* over, even when the node has operation history. Returns true when the node had
|
|
419
|
+
* a claim or history to release, false when it was unknown.
|
|
420
|
+
*/
|
|
421
|
+
releaseNodeClaim?(nodeId: string): Promise<boolean>;
|
|
422
|
+
/**
|
|
423
|
+
* The owner a node id is currently claimed by, or null when unclaimed (RT-21).
|
|
424
|
+
* Optional; anonymous provisional claims need it together with
|
|
425
|
+
* {@link replaceNodeClaim}.
|
|
426
|
+
*/
|
|
427
|
+
getNodeClaimOwner?(nodeId: string): Promise<string | null>;
|
|
428
|
+
/**
|
|
429
|
+
* Atomically replace the owner of a node claim, only if it is still
|
|
430
|
+
* `expectedOwner` (compare-and-set, RT-21). Returns true when replaced. Used to
|
|
431
|
+
* confirm or re-issue an anonymous device's provisional claim; never creates one.
|
|
432
|
+
*/
|
|
433
|
+
replaceNodeClaim?(nodeId: string, expectedOwner: string, newOwner: string): Promise<boolean>;
|
|
434
|
+
/**
|
|
435
|
+
* The wrapped encryption key record (JSON) of one owner's keyring, or null (ENC-1).
|
|
436
|
+
* The record holds only salt, KDF parameters and wrapped keys: never a usable key.
|
|
437
|
+
* Optional for custom stores; without it the server answers key requests with
|
|
438
|
+
* `unsupported` (it never keeps key records in memory only, which would fork a
|
|
439
|
+
* user's keys after a restart).
|
|
440
|
+
*/
|
|
441
|
+
getEncryptionKeyRecord?(owner: string, keyring: string): Promise<string | null>;
|
|
442
|
+
/**
|
|
443
|
+
* Write a key record with compare-and-set: store `record` at `revision` only when the
|
|
444
|
+
* stored revision is `expectedRevision` (0: no record yet). Returns false when
|
|
445
|
+
* another write won. Must be atomic per (owner, keyring) across server instances.
|
|
446
|
+
*/
|
|
447
|
+
putEncryptionKeyRecord?(owner: string, keyring: string, record: string, revision: number, expectedRevision: number): Promise<boolean>;
|
|
448
|
+
/**
|
|
449
|
+
* Stored key records: every one (for `exportBackup`; RT-104: a server restored from
|
|
450
|
+
* its backup must still hold the records its encrypted history needs), or one owner's
|
|
451
|
+
* (the key service tells another keyring's history from a lost record with it).
|
|
452
|
+
* Optional.
|
|
453
|
+
*/
|
|
454
|
+
listEncryptionKeyRecords?(owner?: string): Promise<EncryptionKeyRecordRow[]>;
|
|
455
|
+
/**
|
|
456
|
+
* Key ids named by stored encrypted operations (their envelope's `keyId`), a sample
|
|
457
|
+
* of at most `limit` distinct ids (RT-104). `nodeOwner` restricts it to operations of
|
|
458
|
+
* nodes claimed by that principal (see {@link claimNode}); null means every node (a
|
|
459
|
+
* server without authentication, whose clients share one keyring). The key service
|
|
460
|
+
* reports them when an owner has no key record, so a new device can tell a lost
|
|
461
|
+
* record (encrypted history exists) from a first one. Optional; without it a new
|
|
462
|
+
* device cannot tell, and a fork it creates is merged later by a device holding the
|
|
463
|
+
* old ring.
|
|
464
|
+
*/
|
|
465
|
+
getEncryptedKeyIds?(nodeOwner: string | null, limit: number): Promise<string[]>;
|
|
466
|
+
/**
|
|
467
|
+
* Record that `owner` holds the bytes behind a blob content hash (it pushed them,
|
|
468
|
+
* proving possession) (RT-11). Idempotent. Optional; without it the sync server
|
|
469
|
+
* keeps ownership in memory (lost on restart, not shared between instances).
|
|
470
|
+
*/
|
|
471
|
+
recordBlobOwner?(hash: string, owner: string): Promise<void>;
|
|
472
|
+
/** The owners recorded for each hash (an empty list for unowned hashes). */
|
|
473
|
+
getBlobOwners?(hashes: string[]): Promise<Map<string, string[]>>;
|
|
474
|
+
/**
|
|
475
|
+
* Atomically make `owner` the first owner of a hash nobody owns yet. Returns true
|
|
476
|
+
* when `owner` owns the hash afterwards (newly, or already), false when someone
|
|
477
|
+
* else does. Must be atomic per hash across server instances.
|
|
478
|
+
*/
|
|
479
|
+
claimBlobIfUnowned?(hash: string, owner: string): Promise<boolean>;
|
|
480
|
+
/**
|
|
481
|
+
* The scope snapshots captured when the given operations were applied (RT-14).
|
|
482
|
+
* Operations without one are absent from the result.
|
|
483
|
+
*/
|
|
484
|
+
getOperationScopeSnapshots?(operationIds: string[]): Promise<Map<string, OperationScopeSnapshot>>;
|
|
485
|
+
/**
|
|
486
|
+
* The greatest HLC timestamp among every stored operation of one record (its
|
|
487
|
+
* newest field write), or null when the record has no operations (RT-19). A
|
|
488
|
+
* scope-entry operation is stamped with it so it never overrides newer client data.
|
|
489
|
+
*/
|
|
490
|
+
getRecordLatestTimestamp?(collection: string, recordId: string): Promise<HLCTimestamp | null>;
|
|
491
|
+
/**
|
|
492
|
+
* Per-field versions of one live record, folded from its stored operations exactly
|
|
493
|
+
* as the materialization folds its values (RT-27): each field's version is the HLC
|
|
494
|
+
* of the write that produced its current value, plus the record's first and newest
|
|
495
|
+
* operation. Null when the record is deleted or has no operations. A scope-entry
|
|
496
|
+
* operation carries them so a receiver resolves every field on its own.
|
|
497
|
+
*/
|
|
498
|
+
getRecordFieldVersions?(collection: string, recordId: string): Promise<RecordFieldVersions | null>;
|
|
499
|
+
/**
|
|
500
|
+
* The record's fold state (W7): the per-field CRDT state its row is projected from.
|
|
501
|
+
* Null when the record has no operations or its collection is not materialized. A
|
|
502
|
+
* scope-entry operation carries it (filtered to the fields the receiver may see) so
|
|
503
|
+
* the receiver merges richtext, counter and resolver fields exactly (RT-29).
|
|
504
|
+
*/
|
|
505
|
+
getRecordFoldState?(collection: string, recordId: string): Promise<FoldState | null>;
|
|
506
|
+
/** Every stored operation of one record, in delivery (commit) order. */
|
|
507
|
+
getRecordOperations?(collection: string, recordId: string): Promise<Operation[]>;
|
|
508
|
+
/**
|
|
509
|
+
* The record's row as it would be after merging `op` into its fold state, with
|
|
510
|
+
* nothing written: the candidate a Tier-2 constraint check judges at ingest. Null
|
|
511
|
+
* when the record would not be live (deleted, or never inserted).
|
|
512
|
+
*/
|
|
513
|
+
previewOperation?(op: Operation): Promise<MaterializedRecord | null>;
|
|
514
|
+
/**
|
|
515
|
+
* Node ids whose operations win `merge('server-authoritative')` fields in the fold:
|
|
516
|
+
* this store's own node id (every server-originated operation is authored by it)
|
|
517
|
+
* plus any configured extras. The sync server advertises them in the handshake so
|
|
518
|
+
* clients fold with the same authority.
|
|
519
|
+
*/
|
|
520
|
+
getAuthoritativeNodeIds?(): string[];
|
|
521
|
+
/** Explicit authoritative ids the deployment revoked (RT-81); advertised at handshake. */
|
|
522
|
+
getRevokedAuthoritativeNodeIds?(): string[];
|
|
523
|
+
/**
|
|
524
|
+
* Every explicit id the deployment ever held authoritative, revoked ones included
|
|
525
|
+
* (RT-81). The session never accepts one as a device node id.
|
|
526
|
+
*/
|
|
527
|
+
getEverAuthoritativeNodeIds?(): string[];
|
|
528
|
+
/**
|
|
529
|
+
* Keyed id of a server-derived operation (cascade, set-null, constraint correction):
|
|
530
|
+
* deterministic across every instance of the deployment, unpredictable to clients
|
|
531
|
+
* (RT-64). Built-in stores key it with a persisted deployment secret. A store
|
|
532
|
+
* without it falls back to the unkeyed `deriveSideEffectOpId`.
|
|
533
|
+
*/
|
|
534
|
+
deriveServerOperationId?(parentOpId: string, ruleId: string, targetRecordId: string): Promise<string>;
|
|
145
535
|
/** Close the store and release resources */
|
|
146
536
|
close(): Promise<void>;
|
|
147
537
|
/** Get the total number of stored operations */
|
|
@@ -188,10 +578,23 @@ interface ServerStore extends SyncStore {
|
|
|
188
578
|
* tables from the operation log.
|
|
189
579
|
*
|
|
190
580
|
* @param schema - The schema definition describing all collections
|
|
581
|
+
* @param options - `operationTransforms`: the schema transforms the store folds with
|
|
582
|
+
* (transforms at fold time, RT-84). Pass the same list as the sync server's
|
|
583
|
+
* `operationTransforms`, so the startup re-materialization folds with them once.
|
|
191
584
|
*/
|
|
192
|
-
setSchema(schema: SchemaDefinition): Promise<void>;
|
|
585
|
+
setSchema(schema: SchemaDefinition, options?: ServerSchemaOptions): Promise<void>;
|
|
193
586
|
/** Schema used for materialized tables and server-side validation, if set. */
|
|
194
587
|
getSchema(): SchemaDefinition | null;
|
|
588
|
+
/**
|
|
589
|
+
* Set the schema transforms the store folds with (RT-84). Operations are stored
|
|
590
|
+
* exactly as uploaded; the fold merges each operation as
|
|
591
|
+
* `operationSchemaView(op, schema.version, transforms)` reads it. Transforms are part
|
|
592
|
+
* of the fold plan fingerprint: a change re-folds every record (once). The sync
|
|
593
|
+
* server calls this with its `operationTransforms` at construction.
|
|
594
|
+
*/
|
|
595
|
+
setOperationTransforms?(transforms: readonly OperationTransform[]): Promise<void>;
|
|
596
|
+
/** The schema transforms the store folds with (empty when none). */
|
|
597
|
+
getOperationTransforms?(): readonly OperationTransform[];
|
|
195
598
|
/**
|
|
196
599
|
* Get all records from a materialized collection.
|
|
197
600
|
* When schema is set, reads directly from the collection table (O(1) indexed).
|
|
@@ -303,6 +706,68 @@ interface ApplyServerOperationResult {
|
|
|
303
706
|
appliedOperations: Operation[];
|
|
304
707
|
/** Rejection reason when the operation was not applied */
|
|
305
708
|
rejection?: OperationRejection;
|
|
709
|
+
/**
|
|
710
|
+
* Side effects of an applied delete that were NOT derived because
|
|
711
|
+
* {@link ApplyServerOperationOptions.isAuthoredSideEffect} claimed them (RT-69). The
|
|
712
|
+
* caller must derive each one with {@link deriveServerSideEffects} unless the
|
|
713
|
+
* author's own copy is stored.
|
|
714
|
+
*/
|
|
715
|
+
deferredSideEffects?: SideEffectOp[];
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Decides whether an untrusted writer may cause one referential side effect of its
|
|
719
|
+
* delete. `effect` is the side effect shaped as the operation the server would
|
|
720
|
+
* write; `stored` is the affected record as stored now (including soft-deleted).
|
|
721
|
+
*/
|
|
722
|
+
type SideEffectAuthorizer = (effect: Operation, stored: MaterializedRecord | null) => UplinkAuthorizationResult;
|
|
723
|
+
/** Options for {@link applyServerOperation}. */
|
|
724
|
+
interface ApplyServerOperationOptions {
|
|
725
|
+
/**
|
|
726
|
+
* Uplink authorization for an untrusted writer, re-evaluated by the store inside
|
|
727
|
+
* its apply critical section against the row as stored at commit time (see
|
|
728
|
+
* `ApplyRemoteOptions.authorize`). A refusal becomes a non-retriable rejection and
|
|
729
|
+
* nothing is written.
|
|
730
|
+
*/
|
|
731
|
+
authorize?: ApplyRemoteOptions['authorize'];
|
|
732
|
+
/**
|
|
733
|
+
* Authorization of the referential side effects (cascade delete, set-null) an
|
|
734
|
+
* untrusted writer's delete would cause (RT-10). Every side effect is checked
|
|
735
|
+
* against the writer's own scope BEFORE the delete is applied; if any falls
|
|
736
|
+
* outside it, the whole delete is refused and nothing is written.
|
|
737
|
+
*
|
|
738
|
+
* Design: integrity is judged on ALL children, in every tenant (a restrict or a
|
|
739
|
+
* cross-tenant cascade is never silently skipped, which would leave dangling
|
|
740
|
+
* references), but the writer learns nothing about children it cannot see: a
|
|
741
|
+
* restrict violation and an unauthorized side effect are refused with the same
|
|
742
|
+
* generic `RESTRICTED` rejection, with no ids and no counts. Setting `authorize`
|
|
743
|
+
* alone also switches restrict refusals to the generic form.
|
|
744
|
+
*/
|
|
745
|
+
authorizeSideEffect?: SideEffectAuthorizer;
|
|
746
|
+
/**
|
|
747
|
+
* The writer is a legacy client that does not reserve sequence numbers (RT-37): a
|
|
748
|
+
* different operation under a held `(nodeId, sequenceNumber)` is stored as a legacy
|
|
749
|
+
* pair instead of being refused with `SEQUENCE_CONFLICT`. Applies to the primary
|
|
750
|
+
* operation only; server-originated side effects are always enforced.
|
|
751
|
+
*/
|
|
752
|
+
legacySequenceWriter?: boolean;
|
|
753
|
+
/** See `ApplyRemoteOptions.onLegacySequencePair`; for the primary operation. */
|
|
754
|
+
onLegacySequencePair?: ApplyRemoteOptions['onLegacySequencePair'];
|
|
755
|
+
/**
|
|
756
|
+
* True for a referential side effect of this delete that the writer authored itself
|
|
757
|
+
* (its own cascade or set-null, later in the same upload, RT-69). Such an effect is
|
|
758
|
+
* not derived now but returned in `deferredSideEffects`, so the log stores one copy
|
|
759
|
+
* per child instead of the author's and the server's. Effects it does not claim (a
|
|
760
|
+
* child the author did not know, a legacy client that authors no cascades) are
|
|
761
|
+
* derived as before.
|
|
762
|
+
*/
|
|
763
|
+
isAuthoredSideEffect?: (effect: SideEffectOp) => boolean;
|
|
764
|
+
/**
|
|
765
|
+
* The operation as the server schema reads it (transforms at fold time, RT-84), when
|
|
766
|
+
* the caller already computed it. Shape, constraint and referential checks judge the
|
|
767
|
+
* view; the store keeps `op` exactly as given and folds the same view. Default: the
|
|
768
|
+
* store's own view (`serverOperationView`).
|
|
769
|
+
*/
|
|
770
|
+
view?: Operation;
|
|
306
771
|
}
|
|
307
772
|
|
|
308
773
|
/**
|
|
@@ -318,6 +783,13 @@ interface ServerMetricsSnapshot {
|
|
|
318
783
|
uniqueOperationsReceived: number;
|
|
319
784
|
duplicateOperationsReceived: number;
|
|
320
785
|
rejectedOperations: number;
|
|
786
|
+
/**
|
|
787
|
+
* Operations from protocol-1 (beta.12) clients stored with an id that could not be
|
|
788
|
+
* verified (RT-71). Non-zero only while beta.12 clients write `undefined` members.
|
|
789
|
+
*/
|
|
790
|
+
unverifiedLegacyOperations: number;
|
|
791
|
+
/** Uploads refused for reusing a stored operation id with other content (RT-77). */
|
|
792
|
+
forgedDuplicates: number;
|
|
321
793
|
operationsSent: number;
|
|
322
794
|
bytesReceived: number;
|
|
323
795
|
bytesSent: number;
|
|
@@ -352,6 +824,8 @@ declare class ServerMetricsCollector {
|
|
|
352
824
|
private uniqueOperationsReceived;
|
|
353
825
|
private duplicateOperationsReceived;
|
|
354
826
|
private rejectedOperations;
|
|
827
|
+
private unverifiedLegacyOperations;
|
|
828
|
+
private forgedDuplicates;
|
|
355
829
|
private operationsSent;
|
|
356
830
|
private bytesReceived;
|
|
357
831
|
private bytesSent;
|
|
@@ -376,6 +850,10 @@ declare class ServerMetricsCollector {
|
|
|
376
850
|
}): void;
|
|
377
851
|
/** Record operations sent to a client. */
|
|
378
852
|
recordSent(sessionId: string, count: number, byteSize: number): void;
|
|
853
|
+
/** Record an operation stored with an unverified legacy id (RT-71). */
|
|
854
|
+
recordUnverifiedLegacyOperation(): void;
|
|
855
|
+
/** Record an upload refused for reusing a stored id with other content (RT-77). */
|
|
856
|
+
recordForgedDuplicate(): void;
|
|
379
857
|
/** Record an error. */
|
|
380
858
|
recordError(): void;
|
|
381
859
|
/** Set the schema version. */
|
|
@@ -440,6 +918,210 @@ declare function createSilentLogger(): Logger;
|
|
|
440
918
|
*/
|
|
441
919
|
declare function createDefaultLogger(): Logger;
|
|
442
920
|
|
|
921
|
+
/**
|
|
922
|
+
* Resolves a blob chunk by content hash from server-side storage. Optional: when
|
|
923
|
+
* provided, the server can answer chunk requests directly from its own store
|
|
924
|
+
* (central-store deployment). When absent, the server acts as a pure relay,
|
|
925
|
+
* forwarding chunk requests to peer sessions and their responses back.
|
|
926
|
+
*/
|
|
927
|
+
type ResolveBlobChunk = (hash: string) => Promise<Uint8Array | null>;
|
|
928
|
+
|
|
929
|
+
/** Input to {@link BlobAccessIndex.authorizeReference}. */
|
|
930
|
+
interface BlobReferenceRequest {
|
|
931
|
+
/** The content hash (blob, manifest or chunk) the writer wants to reference. */
|
|
932
|
+
hash: string;
|
|
933
|
+
/** The writer's download scope, or undefined for an unscoped writer. */
|
|
934
|
+
scopes: ScopeMap | undefined;
|
|
935
|
+
/**
|
|
936
|
+
* Stable ownership key of the writer: the user id for a signed-in principal, the
|
|
937
|
+
* node-claim owner for an anonymous device, a `kora:route:` key for a scoped route.
|
|
938
|
+
*/
|
|
939
|
+
owner: string;
|
|
940
|
+
/** Hashes the written record already references as stored (unchanged references). */
|
|
941
|
+
alreadyReferenced?: ReadonlySet<string>;
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Answers "does a live record inside this download scope reference this blob
|
|
945
|
+
* hash?" for the blob relay (RT-1), and "may this writer reference this hash?" at
|
|
946
|
+
* ingest (RT-11).
|
|
947
|
+
*
|
|
948
|
+
* **A content hash is not a secret.** Hashes of well-known content can be computed
|
|
949
|
+
* by anyone, and they travel in records, manifests and logs. So knowing a hash never
|
|
950
|
+
* grants access: a write may reference a hash only when the writer can already read
|
|
951
|
+
* a record that references it, or has proven possession of the bytes (pushed them).
|
|
952
|
+
* Only in peer-relay mode (no central store, so nothing can be pre-claimed and then
|
|
953
|
+
* read back from the server) may the first writer of a hash nobody references or
|
|
954
|
+
* owns claim it by reference alone.
|
|
955
|
+
*
|
|
956
|
+
* A hash is referenced in a scope when a live (not deleted) record in scope has a
|
|
957
|
+
* blob field whose `BlobRef.manifestHash` (or, for a bare reference without a
|
|
958
|
+
* 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
|
|
959
|
+
* the manifest (so a crafted manifest listing someone else's chunk hashes exposes
|
|
960
|
+
* nothing). Manifests are read through `resolveBlobChunk` (central store) or learned
|
|
961
|
+
* from manifest bytes verified in transit.
|
|
962
|
+
*
|
|
963
|
+
* Cost: the referenced set of a scope is built by a scoped query over every
|
|
964
|
+
* collection that declares a blob field (O(records in scope with blobs)). It is
|
|
965
|
+
* cached per scope, rebuilt after a write to one of the blob collections it scanned
|
|
966
|
+
* ({@link invalidate}) or after `ttlMs` (writes through another server instance),
|
|
967
|
+
* and concurrent lookups for one scope share a single rebuild (RT-17). A full hash
|
|
968
|
+
* index in the store would make this O(1); this is the interim, correctness-first
|
|
969
|
+
* form.
|
|
970
|
+
*/
|
|
971
|
+
declare class BlobAccessIndex {
|
|
972
|
+
private readonly store;
|
|
973
|
+
private readonly resolveBlobChunk;
|
|
974
|
+
private readonly ttlMs;
|
|
975
|
+
private readonly byScope;
|
|
976
|
+
/** Rebuilds in progress per scope key, shared by concurrent lookups (RT-17). */
|
|
977
|
+
private readonly inflight;
|
|
978
|
+
/** manifestHash -> chunk hashes it lists (only from bytes verified against the hash). */
|
|
979
|
+
private readonly manifestChunks;
|
|
980
|
+
/** chunkHash -> manifests listing it. */
|
|
981
|
+
private readonly chunkManifests;
|
|
982
|
+
/** Ownership fallback for stores without blob-owner persistence. */
|
|
983
|
+
private readonly memoryOwners;
|
|
984
|
+
private globalGeneration;
|
|
985
|
+
private readonly collectionGeneration;
|
|
986
|
+
constructor(store: ServerStore, resolveBlobChunk: ResolveBlobChunk | null, ttlMs?: number);
|
|
987
|
+
/**
|
|
988
|
+
* Forget cached scope sets because records changed. With `collections`, only sets
|
|
989
|
+
* built from those collections are dropped (a write to a collection without blob
|
|
990
|
+
* fields drops nothing); without, every set is dropped (for example writes made
|
|
991
|
+
* through another server instance).
|
|
992
|
+
*/
|
|
993
|
+
invalidate(collections?: Iterable<string>): void;
|
|
994
|
+
/**
|
|
995
|
+
* True when a live record inside `scopes` references `hash`.
|
|
996
|
+
*
|
|
997
|
+
* @param scopes - The session's download scope, or undefined for unscoped
|
|
998
|
+
* @param hash - The requested content hash
|
|
999
|
+
*/
|
|
1000
|
+
isReferenced(scopes: ScopeMap | undefined, hash: string): Promise<boolean>;
|
|
1001
|
+
/**
|
|
1002
|
+
* Decide whether a writer may reference `hash` in a blob field (RT-11). Allowed
|
|
1003
|
+
* when the record already referenced it, when a live record inside the writer's
|
|
1004
|
+
* download scope references it, when the writer owns it (pushed the bytes, or
|
|
1005
|
+
* claimed it first), or when nobody references, owns or stores it yet: then the
|
|
1006
|
+
* writer claims it, atomically, so a later forger cannot.
|
|
1007
|
+
*/
|
|
1008
|
+
authorizeReference(request: BlobReferenceRequest): Promise<boolean>;
|
|
1009
|
+
/** True when `owner` pushed (or first claimed) the bytes behind `hash`. */
|
|
1010
|
+
isOwnedBy(hash: string, owner: string): Promise<boolean>;
|
|
1011
|
+
/**
|
|
1012
|
+
* Record that `owner` pushed bytes verified to hash to `hash` (proof of
|
|
1013
|
+
* possession), and learn the chunk list when the bytes are a manifest.
|
|
1014
|
+
*/
|
|
1015
|
+
recordPush(hash: string, bytes: Uint8Array, owner: string): Promise<void>;
|
|
1016
|
+
/**
|
|
1017
|
+
* When `bytes` are a manifest, true only if the pusher may reference every chunk
|
|
1018
|
+
* it lists (the same rule as {@link authorizeReference}); true for non-manifest
|
|
1019
|
+
* bytes. A manifest listing someone else's chunks is refused before it is stored.
|
|
1020
|
+
*/
|
|
1021
|
+
authorizeManifestPush(bytes: Uint8Array, scopes: ScopeMap | undefined, owner: string): Promise<boolean>;
|
|
1022
|
+
/**
|
|
1023
|
+
* Learn a manifest's chunk list from bytes already verified to hash to `hash`.
|
|
1024
|
+
* Bytes that are not a manifest are ignored. Unowned chunks of an owned manifest
|
|
1025
|
+
* are claimed for the manifest's owner, so a manifest crafted later cannot adopt
|
|
1026
|
+
* them.
|
|
1027
|
+
*/
|
|
1028
|
+
observeVerifiedBytes(hash: string, bytes: Uint8Array): void;
|
|
1029
|
+
private claimChunksForManifestOwner;
|
|
1030
|
+
private currentStamp;
|
|
1031
|
+
private isCurrent;
|
|
1032
|
+
private blobCollections;
|
|
1033
|
+
private directHashes;
|
|
1034
|
+
private build;
|
|
1035
|
+
/** Read unknown manifests from the central store, verifying them against their hash. */
|
|
1036
|
+
private loadManifests;
|
|
1037
|
+
private ownersOf;
|
|
1038
|
+
private addOwner;
|
|
1039
|
+
private claim;
|
|
1040
|
+
private rememberMemoryOwner;
|
|
1041
|
+
private rememberManifest;
|
|
1042
|
+
private forgetManifest;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Minimal interface for a ws.WebSocket instance.
|
|
1047
|
+
* Allows dependency injection for testing without importing ws directly.
|
|
1048
|
+
*/
|
|
1049
|
+
interface WsWebSocket {
|
|
1050
|
+
readyState: number;
|
|
1051
|
+
send(data: string | Uint8Array, callback?: (err?: Error) => void): void;
|
|
1052
|
+
close(code?: number, reason?: string): void;
|
|
1053
|
+
on(event: string, listener: (...args: unknown[]) => void): void;
|
|
1054
|
+
removeAllListeners(): void;
|
|
1055
|
+
/** Send a ping frame (ws). Without it, no liveness probing happens. */
|
|
1056
|
+
ping?(): void;
|
|
1057
|
+
/** Destroy the socket at once, emitting `close` (ws). Falls back to `close()`. */
|
|
1058
|
+
terminate?(): void;
|
|
1059
|
+
/** Bytes queued and not yet written to the network (ws). */
|
|
1060
|
+
readonly bufferedAmount?: number;
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Options for WsServerTransport.
|
|
1064
|
+
*/
|
|
1065
|
+
interface WsServerTransportOptions {
|
|
1066
|
+
/** Message serializer. Defaults to JsonMessageSerializer. */
|
|
1067
|
+
serializer?: MessageSerializer;
|
|
1068
|
+
/**
|
|
1069
|
+
* Interval between WebSocket pings, in ms (SRV-6). A connection that leaves
|
|
1070
|
+
* `maxMissedPongs` pings in a row unanswered (and sends nothing else meanwhile) is
|
|
1071
|
+
* terminated, which runs Kora's normal close path. Defaults to 25 seconds; 0
|
|
1072
|
+
* disables probing. Keep it at 15 seconds or more on mobile networks.
|
|
1073
|
+
*/
|
|
1074
|
+
heartbeatIntervalMs?: number;
|
|
1075
|
+
/** Unanswered pings before the connection is terminated. Defaults to 2. */
|
|
1076
|
+
maxMissedPongs?: number;
|
|
1077
|
+
/**
|
|
1078
|
+
* Bytes that may be queued for this client before it is disconnected as a slow
|
|
1079
|
+
* consumer. Defaults to 32 MiB; 0 disables the ceiling.
|
|
1080
|
+
*/
|
|
1081
|
+
maxBufferedBytes?: number;
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Server-side transport wrapping a ws.WebSocket connection.
|
|
1085
|
+
* Created for each incoming client connection.
|
|
1086
|
+
*
|
|
1087
|
+
* It probes liveness with WebSocket pings (both the standalone server and
|
|
1088
|
+
* `createProductionServer` create their connections through it), so a half-open
|
|
1089
|
+
* connection, a peer that vanished without a FIN, is detected and closed instead of
|
|
1090
|
+
* holding a session forever.
|
|
1091
|
+
*/
|
|
1092
|
+
declare class WsServerTransport implements ServerTransport {
|
|
1093
|
+
private readonly ws;
|
|
1094
|
+
private readonly serializer;
|
|
1095
|
+
private messageHandler;
|
|
1096
|
+
private closeHandler;
|
|
1097
|
+
private errorHandler;
|
|
1098
|
+
private readonly maxMissedPongs;
|
|
1099
|
+
private readonly maxBufferedBytes;
|
|
1100
|
+
private heartbeatTimer;
|
|
1101
|
+
private missedPongs;
|
|
1102
|
+
private terminated;
|
|
1103
|
+
constructor(ws: WsWebSocket, options?: WsServerTransportOptions);
|
|
1104
|
+
send(message: SyncMessage): void;
|
|
1105
|
+
onMessage(handler: ServerMessageHandler): void;
|
|
1106
|
+
onClose(handler: ServerCloseHandler): void;
|
|
1107
|
+
onError(handler: ServerErrorHandler): void;
|
|
1108
|
+
isConnected(): boolean;
|
|
1109
|
+
close(code?: number, reason?: string): void;
|
|
1110
|
+
bufferedAmount(): number;
|
|
1111
|
+
/**
|
|
1112
|
+
* Ping every `intervalMs`; terminate once `maxMissedPongs` pings in a row went
|
|
1113
|
+
* unanswered. A pong or any inbound message proves the peer alive.
|
|
1114
|
+
*/
|
|
1115
|
+
private startHeartbeat;
|
|
1116
|
+
private stopHeartbeat;
|
|
1117
|
+
/**
|
|
1118
|
+
* Drop the connection at once. ws emits `close` from `terminate()`, which runs
|
|
1119
|
+
* Kora's normal close path (session, relays and side channels are released).
|
|
1120
|
+
*/
|
|
1121
|
+
private terminate;
|
|
1122
|
+
private setupListeners;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
443
1125
|
/**
|
|
444
1126
|
* Minimal interface for a ws.WebSocketServer instance.
|
|
445
1127
|
* Allows dependency injection for testing without importing ws directly.
|
|
@@ -458,6 +1140,8 @@ type WsServerConstructor = new (options: {
|
|
|
458
1140
|
port?: number;
|
|
459
1141
|
host?: string;
|
|
460
1142
|
path?: string;
|
|
1143
|
+
maxPayload?: number;
|
|
1144
|
+
perMessageDeflate?: false | Record<string, unknown>;
|
|
461
1145
|
}) => WsServerLike;
|
|
462
1146
|
/**
|
|
463
1147
|
* Self-hosted sync server. Accepts WebSocket connections from clients,
|
|
@@ -480,22 +1164,74 @@ declare class KoraSyncServer {
|
|
|
480
1164
|
private readonly schemaVersion;
|
|
481
1165
|
private readonly supportedSchemaVersions;
|
|
482
1166
|
private readonly operationTransforms;
|
|
1167
|
+
/** The store's re-fold for the configured transforms (RT-84), awaited by start(). */
|
|
1168
|
+
private storeTransformsReady;
|
|
483
1169
|
private readonly port;
|
|
484
1170
|
private readonly host;
|
|
485
1171
|
private readonly path;
|
|
486
1172
|
private readonly logger;
|
|
487
1173
|
private readonly metrics;
|
|
488
1174
|
private readonly awarenessRelay;
|
|
1175
|
+
/** Reads the records presence cursors name, for every session (F16). */
|
|
1176
|
+
private readonly presenceRecords;
|
|
1177
|
+
/** The record each session's current presence cursor names (F16). */
|
|
1178
|
+
private readonly presenceTargets;
|
|
1179
|
+
/** The sessions whose presence cursor names each record, by record key. */
|
|
1180
|
+
private readonly presenceTargetsByKey;
|
|
1181
|
+
/**
|
|
1182
|
+
* Audience re-decisions running, one per record: a write while one runs marks it
|
|
1183
|
+
* dirty, so it reads again and the last decision applied is always from a read that
|
|
1184
|
+
* began after the last write (never an older read finishing late).
|
|
1185
|
+
*/
|
|
1186
|
+
private readonly presenceRefreshes;
|
|
489
1187
|
private readonly yjsDocRelay;
|
|
490
1188
|
private readonly blobChunkRelay;
|
|
1189
|
+
/** Wrapped encryption key records (ENC-1, D4b), persisted in the server store. */
|
|
1190
|
+
private readonly encryptionKeys;
|
|
1191
|
+
/** Which blob hashes each download scope may obtain (RT-1). */
|
|
1192
|
+
private readonly blobAccess;
|
|
491
1193
|
private readonly persistBlobChunk;
|
|
492
1194
|
private readonly maxOperationBytes;
|
|
493
1195
|
private readonly maxOpsPerMinute;
|
|
1196
|
+
/** Per-principal ingest budget per minute; 0 when disabled. */
|
|
1197
|
+
private readonly maxOpsPerMinutePerUser;
|
|
1198
|
+
private readonly allowLegacyAnonymousClaims;
|
|
1199
|
+
private readonly deviceNodeHandover;
|
|
1200
|
+
private readonly unscopedSharing;
|
|
1201
|
+
private readonly anonymousClaimTtlMs;
|
|
1202
|
+
private readonly maxOpsPerBatch;
|
|
1203
|
+
private readonly maxScopePredicateValues;
|
|
1204
|
+
private readonly maxMessageBytes;
|
|
1205
|
+
private readonly heartbeatIntervalMs;
|
|
1206
|
+
private readonly appHeartbeatIntervalMs;
|
|
1207
|
+
private readonly encryptionPolicy;
|
|
1208
|
+
private readonly handshakeTimeoutMs;
|
|
1209
|
+
private readonly maxBufferedBytes;
|
|
1210
|
+
private readonly deliveryHighWaterBytes;
|
|
1211
|
+
private readonly perMessageDeflate;
|
|
1212
|
+
/**
|
|
1213
|
+
* Ingest rate limiters by device node id, shared by that node's sessions so a
|
|
1214
|
+
* reconnect does not reset the budget (SRV-6). Entries idle past the window are
|
|
1215
|
+
* dropped by the background tick; insertion order doubles as LRU order.
|
|
1216
|
+
*/
|
|
1217
|
+
private readonly rateLimiters;
|
|
1218
|
+
/**
|
|
1219
|
+
* Ingest budgets by authenticated principal, shared by every node of that user, so
|
|
1220
|
+
* minting node ids does not multiply the per-node budget. Expired like the node map.
|
|
1221
|
+
*/
|
|
1222
|
+
private readonly userRateLimiters;
|
|
1223
|
+
private readonly blobLimits;
|
|
494
1224
|
private readonly validateOperation;
|
|
495
1225
|
private readonly koraContext;
|
|
496
1226
|
private readonly sessions;
|
|
497
|
-
|
|
498
|
-
|
|
1227
|
+
/**
|
|
1228
|
+
* HTTP long-poll sessions by their server-issued, high-entropy id (RT-2). Each is
|
|
1229
|
+
* bound to the principal that opened it; every request must re-authenticate as it.
|
|
1230
|
+
*/
|
|
1231
|
+
private readonly httpSessions;
|
|
1232
|
+
/** Internal ClientSession id -> HTTP session id, for cleanup on close. */
|
|
1233
|
+
private readonly httpSessionIdBySession;
|
|
1234
|
+
private readonly httpSessionIdleTimeoutMs;
|
|
499
1235
|
private readonly serverVersion;
|
|
500
1236
|
private wsServer;
|
|
501
1237
|
private running;
|
|
@@ -508,6 +1244,17 @@ declare class KoraSyncServer {
|
|
|
508
1244
|
private deliveryPollTimer;
|
|
509
1245
|
private lastObservedDeliverySequence;
|
|
510
1246
|
private deliveryPollInFlight;
|
|
1247
|
+
/**
|
|
1248
|
+
* Re-validates every live session's credential against the auth provider, so a
|
|
1249
|
+
* revocation persisted by ANOTHER server instance (whose in-process revocation
|
|
1250
|
+
* listeners never reach this one) still ends the session here (RT-18).
|
|
1251
|
+
*/
|
|
1252
|
+
private readonly sessionRevalidationIntervalMs;
|
|
1253
|
+
private sessionRevalidationTimer;
|
|
1254
|
+
private lastSessionRevalidationAtMs;
|
|
1255
|
+
private sessionRevalidationInFlight;
|
|
1256
|
+
/** Unsubscribes from the auth provider's revocation feed (AUTH-11). */
|
|
1257
|
+
private revocationUnsubscribe;
|
|
511
1258
|
/**
|
|
512
1259
|
* Relay operations a client never acknowledged before it disconnected, buffered by
|
|
513
1260
|
* client node id so they can be redelivered on its next connection. Bounded per
|
|
@@ -517,19 +1264,142 @@ declare class KoraSyncServer {
|
|
|
517
1264
|
private static readonly MAX_ORPHANED_RELAY_OPS_PER_NODE;
|
|
518
1265
|
private static readonly ORPHANED_RELAY_TTL_MS;
|
|
519
1266
|
constructor(config: KoraSyncServerConfig);
|
|
1267
|
+
/**
|
|
1268
|
+
* Follow the auth provider's revocation feed, when it has one, so revoking a
|
|
1269
|
+
* device or user ends its live sessions without any wiring by the app.
|
|
1270
|
+
*/
|
|
1271
|
+
private ensureRevocationSubscribed;
|
|
1272
|
+
/**
|
|
1273
|
+
* End live sync sessions whose credential was revoked (AUTH-11).
|
|
1274
|
+
*
|
|
1275
|
+
* Each matching session receives a retriable `AUTH_REVOKED` (or `AUTH_EXPIRED`)
|
|
1276
|
+
* error and is closed. The client refreshes its credentials and re-handshakes,
|
|
1277
|
+
* so the server authenticates it again: a still-valid device reconnects, a
|
|
1278
|
+
* revoked one is refused. Sessions still in their handshake are refused as soon
|
|
1279
|
+
* as their principal is known.
|
|
1280
|
+
*
|
|
1281
|
+
* `createKoraAuthServer().bindSyncServer(server)` calls this on device revoke,
|
|
1282
|
+
* sign-out, password reset or change, and admin revoke. A provider exposing
|
|
1283
|
+
* `onRevoke` (the built-in one does) is followed automatically.
|
|
1284
|
+
*
|
|
1285
|
+
* @param filter - `userId` ends every session of that user; with `deviceId`,
|
|
1286
|
+
* only that device's sessions. At least one of the two is required.
|
|
1287
|
+
* @returns The number of established sessions that were closed
|
|
1288
|
+
*
|
|
1289
|
+
* @example
|
|
1290
|
+
* ```typescript
|
|
1291
|
+
* server.terminateSessions({ userId: 'u1', deviceId: 'laptop' })
|
|
1292
|
+
* ```
|
|
1293
|
+
*/
|
|
1294
|
+
terminateSessions(filter: TerminateSessionsFilter): number;
|
|
1295
|
+
/**
|
|
1296
|
+
* Admin release of a device node id (RT-5). The next principal to handshake with
|
|
1297
|
+
* this node id claims it, even when the node already has operation history.
|
|
1298
|
+
*
|
|
1299
|
+
* Use it to hand over operation history written before node claims existed (an
|
|
1300
|
+
* upgrade from beta.12, where nobody may adopt such a node until it is released),
|
|
1301
|
+
* or to reassign a lost device's node id. Live sessions on that node id are ended
|
|
1302
|
+
* with a retriable `NODE_RELEASED` error so the handover starts clean.
|
|
1303
|
+
*
|
|
1304
|
+
* @param nodeId - The device node id to release
|
|
1305
|
+
* @returns True when the node had a claim or history to release; false when it
|
|
1306
|
+
* was unknown or the store does not support node claims
|
|
1307
|
+
*
|
|
1308
|
+
* @example
|
|
1309
|
+
* ```typescript
|
|
1310
|
+
* await server.releaseNodeClaim('0190a1b2-...')
|
|
1311
|
+
* ```
|
|
1312
|
+
*/
|
|
1313
|
+
releaseNodeClaim(nodeId: string): Promise<boolean>;
|
|
520
1314
|
private ensureBackgroundTimersStarted;
|
|
1315
|
+
/**
|
|
1316
|
+
* Re-validate the credential of every live session with the auth provider and end
|
|
1317
|
+
* the sessions whose credential is no longer accepted (RT-18).
|
|
1318
|
+
*
|
|
1319
|
+
* Revocations are persisted by the auth stores (token, device and per-user
|
|
1320
|
+
* cut-offs), but `onRevoke` listeners only run in the process that handled the
|
|
1321
|
+
* revocation. Every instance therefore re-checks its own live sessions
|
|
1322
|
+
* periodically (`sessionRevalidationIntervalMs`, default 30 seconds, also driven by
|
|
1323
|
+
* the delivery poll tick) by authenticating the session's credential again. A
|
|
1324
|
+
* session whose credential is refused, or now resolves to another principal, gets
|
|
1325
|
+
* a retriable `AUTH_REVOKED` (or `AUTH_EXPIRED`) and is closed. A provider error
|
|
1326
|
+
* (for example the auth database is unreachable) ends nothing: it is logged and the
|
|
1327
|
+
* next pass retries, so an outage does not disconnect every client.
|
|
1328
|
+
*
|
|
1329
|
+
* @returns The number of sessions ended by this pass
|
|
1330
|
+
*
|
|
1331
|
+
* @example
|
|
1332
|
+
* ```typescript
|
|
1333
|
+
* // After revoking through another instance's admin API:
|
|
1334
|
+
* await server.revalidateSessions()
|
|
1335
|
+
* ```
|
|
1336
|
+
*/
|
|
1337
|
+
revalidateSessions(): Promise<number>;
|
|
1338
|
+
/**
|
|
1339
|
+
* Re-resolve one user's grant on every live session of that user now, instead of
|
|
1340
|
+
* waiting for the next revalidation pass (`sessionRevalidationIntervalMs`). Call it
|
|
1341
|
+
* after a membership change the auth provider's grant depends on: an invitation
|
|
1342
|
+
* accepted, a collaborator removed, a role changed.
|
|
1343
|
+
*
|
|
1344
|
+
* Each of the user's sessions re-authenticates its credential with the provider. A
|
|
1345
|
+
* session whose resolved download or upload scope changed ends with a retriable
|
|
1346
|
+
* `SCOPE_CHANGED`; its client reconnects at once and receives the new grant. A
|
|
1347
|
+
* narrowed grant applies the client's `scopeExit` policy at that handshake
|
|
1348
|
+
* (`'retract'` hides the rows that left the scope), and the device's unsynced writes
|
|
1349
|
+
* outside the new upload scope are refused. A session still in its handshake
|
|
1350
|
+
* re-checks as soon as it is established, so a grant read before the change never
|
|
1351
|
+
* outlives this call. Sessions whose grant did not change are kept.
|
|
1352
|
+
*
|
|
1353
|
+
* Only this process's sessions are refreshed. Other instances apply the change at
|
|
1354
|
+
* their next revalidation pass, or call this too (for example from a pub/sub
|
|
1355
|
+
* message).
|
|
1356
|
+
*
|
|
1357
|
+
* @param userId - The user whose grant changed (the auth provider's `userId`)
|
|
1358
|
+
* @returns The number of sessions this call ended
|
|
1359
|
+
*
|
|
1360
|
+
* @example
|
|
1361
|
+
* ```typescript
|
|
1362
|
+
* await removeCollaborator(documentId, bobId)
|
|
1363
|
+
* await server.refreshScopes(bobId)
|
|
1364
|
+
* ```
|
|
1365
|
+
*/
|
|
1366
|
+
refreshScopes(userId: string): Promise<number>;
|
|
1367
|
+
/** Run {@link revalidateSessions} from the delivery poll tick once the interval elapsed. */
|
|
1368
|
+
private maybeRevalidateSessions;
|
|
521
1369
|
/**
|
|
522
1370
|
* Retransmit relay batches that connected clients have not acknowledged within
|
|
523
1371
|
* `staleMs`. Called on a periodic tick and directly by tests. Redelivering an
|
|
524
1372
|
* already-applied operation is harmless (clients dedup by content-addressed id).
|
|
525
1373
|
*/
|
|
526
1374
|
retransmitPendingRelays(staleMs?: number): void;
|
|
1375
|
+
/**
|
|
1376
|
+
* The ingest rate limiter of a device node, created on first use and shared by
|
|
1377
|
+
* every session of that node, so reconnecting does not buy a fresh budget (SRV-6).
|
|
1378
|
+
*/
|
|
1379
|
+
private rateLimiterFor;
|
|
1380
|
+
/**
|
|
1381
|
+
* Forget rate limiters of nodes with no live session whose window has long
|
|
1382
|
+
* passed: their budget would be full again anyway.
|
|
1383
|
+
*/
|
|
1384
|
+
private expireIdleRateLimiters;
|
|
1385
|
+
/** Close HTTP long-poll sessions that sent no request within the idle timeout. */
|
|
1386
|
+
private expireIdleHttpSessions;
|
|
527
1387
|
/**
|
|
528
1388
|
* Check whether the authoritative delivery log advanced without passing through
|
|
529
1389
|
* this server instance, then wake delivery-watermark sessions to scan from their
|
|
530
1390
|
* own acknowledged cursors. Session-level filtering remains the only fan-out path.
|
|
531
1391
|
*/
|
|
532
1392
|
pollDeliveryLog(): Promise<void>;
|
|
1393
|
+
/**
|
|
1394
|
+
* Invalidate cached blob access sets for the collections written between two
|
|
1395
|
+
* delivery frontiers. Falls back to dropping every set when the gap is large.
|
|
1396
|
+
*/
|
|
1397
|
+
private invalidateBlobAccessSince;
|
|
1398
|
+
/**
|
|
1399
|
+
* The blob reference authority shared by every session and route (RT-11).
|
|
1400
|
+
* @internal Used by the route context; not part of the public API.
|
|
1401
|
+
*/
|
|
1402
|
+
getBlobAccessIndex(): BlobAccessIndex;
|
|
533
1403
|
/**
|
|
534
1404
|
* Buffer relay operations a disconnected client never acknowledged, keyed by its
|
|
535
1405
|
* node id, deduped by operation id and bounded by count. Redelivered when the
|
|
@@ -572,19 +1442,50 @@ declare class KoraSyncServer {
|
|
|
572
1442
|
/**
|
|
573
1443
|
* Handle one HTTP sync request for a long-polling client.
|
|
574
1444
|
*
|
|
575
|
-
*
|
|
1445
|
+
* The POST that opens a session (the handshake, sent without `sessionId`) creates
|
|
1446
|
+
* it and answers with a server-issued, high-entropy session id in the
|
|
1447
|
+
* `x-kora-session` response header. Every later request must name that id.
|
|
1448
|
+
*
|
|
1449
|
+
* With an auth provider, EVERY request is authenticated from its `authorization`
|
|
1450
|
+
* header and must resolve to the same principal (user, device and anonymity) as
|
|
1451
|
+
* the request that opened the session and as the session's handshake, so knowing
|
|
1452
|
+
* a session id alone grants nothing (RT-2). Sessions with no request for
|
|
1453
|
+
* `httpSessionIdleTimeoutMs` are closed.
|
|
1454
|
+
*
|
|
1455
|
+
* Responses: 202 (POST accepted), 200/204/304 (GET), 400 (malformed), 401
|
|
1456
|
+
* (credential missing or invalid), 403 (credential of another principal), 404
|
|
1457
|
+
* (unknown or expired session), 405 (method), 410 (session closed).
|
|
576
1458
|
*/
|
|
577
1459
|
handleHttpRequest(request: HttpSyncRequest): Promise<HttpSyncResponse>;
|
|
578
1460
|
/**
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
582
|
-
* @param transport - The server transport for the new connection
|
|
583
|
-
* @returns The session ID
|
|
1461
|
+
* Authenticate one HTTP request. Returns the principal identity, null when the
|
|
1462
|
+
* server has no real auth provider, or 'unauthorized'.
|
|
584
1463
|
*/
|
|
585
|
-
|
|
1464
|
+
private authenticateHttpRequest;
|
|
586
1465
|
/**
|
|
587
|
-
*
|
|
1466
|
+
* True when a request's principal is the one that opened the session and, once
|
|
1467
|
+
* the handshake authenticated, the session's own principal.
|
|
1468
|
+
*/
|
|
1469
|
+
private httpPrincipalMatches;
|
|
1470
|
+
/**
|
|
1471
|
+
* Handle an accepted WebSocket (attach mode with `ws`): wraps it in a
|
|
1472
|
+
* {@link WsServerTransport} configured like the standalone server's (serializer,
|
|
1473
|
+
* ping/pong liveness probing, send-buffer ceiling) and starts a session.
|
|
1474
|
+
*
|
|
1475
|
+
* @param ws - A `ws` WebSocket from `WebSocketServer.handleUpgrade` or `connection`
|
|
1476
|
+
* @returns The session ID
|
|
1477
|
+
*/
|
|
1478
|
+
handleWebSocket(ws: WsWebSocket): string;
|
|
1479
|
+
/**
|
|
1480
|
+
* Handle an incoming client connection (attach mode).
|
|
1481
|
+
* Creates a new ClientSession for the transport.
|
|
1482
|
+
*
|
|
1483
|
+
* @param transport - The server transport for the new connection
|
|
1484
|
+
* @returns The session ID
|
|
1485
|
+
*/
|
|
1486
|
+
handleConnection(transport: ServerTransport): string;
|
|
1487
|
+
/**
|
|
1488
|
+
* Get the current server status.
|
|
588
1489
|
*/
|
|
589
1490
|
getStatus(): Promise<ServerStatus>;
|
|
590
1491
|
/**
|
|
@@ -599,9 +1500,10 @@ declare class KoraSyncServer {
|
|
|
599
1500
|
* client only receives the operation if it falls within that client's scope.
|
|
600
1501
|
*
|
|
601
1502
|
* @param op - A fully-formed, server-originated operation to apply
|
|
1503
|
+
* @param options - Optional in-store authorization (used by scoped routes)
|
|
602
1504
|
* @returns The apply result, including any server-generated side-effect ops
|
|
603
1505
|
*/
|
|
604
|
-
applyLocalOperation(op: Operation): Promise<ApplyServerOperationResult>;
|
|
1506
|
+
applyLocalOperation(op: Operation, options?: ApplyServerOperationOptions): Promise<ApplyServerOperationResult>;
|
|
605
1507
|
/**
|
|
606
1508
|
* Relay already-applied, server-originated operations to every connected client.
|
|
607
1509
|
* Used by the conditional route apply, which commits its operations atomically
|
|
@@ -626,11 +1528,65 @@ declare class KoraSyncServer {
|
|
|
626
1528
|
* Get the number of currently connected clients.
|
|
627
1529
|
*/
|
|
628
1530
|
getConnectionCount(): number;
|
|
1531
|
+
/**
|
|
1532
|
+
* Node ids whose operations win `merge('server-authoritative')` fields in the fold
|
|
1533
|
+
* (W7): the store's own node id, which authors every server-originated operation
|
|
1534
|
+
* (side effects, constraint corrections, route writes), plus any extras the store
|
|
1535
|
+
* was configured with (`authoritativeNodeIds` store option). This is the one source
|
|
1536
|
+
* of truth: every session advertises exactly this list in the handshake, so clients
|
|
1537
|
+
* fold with the same authority as the server's stores.
|
|
1538
|
+
*/
|
|
1539
|
+
get authoritativeNodeIds(): string[];
|
|
1540
|
+
/**
|
|
1541
|
+
* Blob access policy (RT-1): true when the session is streaming and a live record
|
|
1542
|
+
* inside its download scope references the hash. Gates the central store.
|
|
1543
|
+
*/
|
|
1544
|
+
private sessionReferencesBlob;
|
|
1545
|
+
/** True when a session other than `exceptSessionId` is connected as `nodeId`. */
|
|
1546
|
+
private isNodeLive;
|
|
1547
|
+
/**
|
|
1548
|
+
* Central-store read policy (RT-1, RT-25): the session owns the hash (it pushed
|
|
1549
|
+
* the bytes) or a live record inside its download scope references it.
|
|
1550
|
+
*/
|
|
1551
|
+
private sessionMayReadStoredBlob;
|
|
1552
|
+
/**
|
|
1553
|
+
* Blob access policy (RT-1): a request may be forwarded to a peer that shares the
|
|
1554
|
+
* requester's exact download scope (the same tenant view, as for presence), which
|
|
1555
|
+
* keeps manifests handed over out of band working; across scopes, only when both
|
|
1556
|
+
* scopes reference the hash, so neither the hash nor the bytes cross a tenant.
|
|
1557
|
+
*/
|
|
1558
|
+
private mayForwardBlobRequest;
|
|
629
1559
|
private handleRelay;
|
|
630
1560
|
private handleSessionClose;
|
|
631
1561
|
private handleAwarenessRelay;
|
|
1562
|
+
/** Point a session's presence at a record (or at none), keeping the index in step. */
|
|
1563
|
+
private setPresenceTarget;
|
|
1564
|
+
/**
|
|
1565
|
+
* Records changed (`operations`, or anything when null): re-decide the audience of
|
|
1566
|
+
* every presence cursor on one of them (F16). A record that moved out of a session's
|
|
1567
|
+
* grant takes the presence on it out of that session, and one that moved in brings
|
|
1568
|
+
* it, without waiting for the sender's next update; catch-ups use the new audience.
|
|
1569
|
+
* Every write is noted first, so a presence read that began before it is never
|
|
1570
|
+
* taken as current.
|
|
1571
|
+
*/
|
|
1572
|
+
private refreshPresence;
|
|
1573
|
+
/** Re-decide the presence on one record, coalesced with a re-decision already running. */
|
|
1574
|
+
private schedulePresenceRefresh;
|
|
1575
|
+
private runPresenceRefresh;
|
|
1576
|
+
/**
|
|
1577
|
+
* Who may see a presence state (F16). A state whose cursor names a record reaches
|
|
1578
|
+
* exactly the sessions whose download scope contains that record, the rule of the
|
|
1579
|
+
* Yjs doc channel, and only when the sender may read the record too: collaborators
|
|
1580
|
+
* with different grants see each other on the documents they share, and nobody
|
|
1581
|
+
* sees presence on a record outside their grant. A malformed cursor reaches
|
|
1582
|
+
* nobody. A state without a cursor names no record: it reaches only sessions of
|
|
1583
|
+
* the same presence partition (identical download scope; anonymous sessions are
|
|
1584
|
+
* each their own partition).
|
|
1585
|
+
*/
|
|
1586
|
+
private awarenessAudience;
|
|
632
1587
|
private handleYjsDocRelay;
|
|
633
|
-
|
|
1588
|
+
/** Open a new HTTP long-poll session bound to `principal`, under a fresh random id. */
|
|
1589
|
+
private openHttpSession;
|
|
634
1590
|
}
|
|
635
1591
|
|
|
636
1592
|
/**
|
|
@@ -820,6 +1776,19 @@ interface AuthContext {
|
|
|
820
1776
|
uplinkScopes?: Record<string, Record<string, unknown>>;
|
|
821
1777
|
/** Arbitrary metadata about the authenticated user */
|
|
822
1778
|
metadata?: Record<string, unknown>;
|
|
1779
|
+
/**
|
|
1780
|
+
* True for an unauthenticated (anonymous) principal, such as the fallback of
|
|
1781
|
+
* `MixedAuthProvider`. Its `userId` is not stable across connections, so node-id
|
|
1782
|
+
* claims are keyed by the shared anonymous owner instead: an anonymous device can
|
|
1783
|
+
* reconnect with its node id, but can never take a signed-in user's node id.
|
|
1784
|
+
*/
|
|
1785
|
+
anonymous?: boolean;
|
|
1786
|
+
/**
|
|
1787
|
+
* When the credential that authenticated this session expires (ms since
|
|
1788
|
+
* epoch). A session must not outlive its credential: the sync server can
|
|
1789
|
+
* close it with a retriable AUTH_EXPIRED so the client refreshes (AUTH-11).
|
|
1790
|
+
*/
|
|
1791
|
+
expiresAt?: number;
|
|
823
1792
|
}
|
|
824
1793
|
/**
|
|
825
1794
|
* Interface for authenticating incoming client connections.
|
|
@@ -832,6 +1801,33 @@ interface AuthProvider {
|
|
|
832
1801
|
* @returns AuthContext if valid, null if rejected
|
|
833
1802
|
*/
|
|
834
1803
|
authenticate(token: string): Promise<AuthContext | null>;
|
|
1804
|
+
/**
|
|
1805
|
+
* Optional revocation feed. When present, `KoraSyncServer` subscribes on
|
|
1806
|
+
* construction and calls {@link KoraSyncServer.terminateSessions} for every
|
|
1807
|
+
* event, so a revoked device or user loses its live sync sessions immediately
|
|
1808
|
+
* (AUTH-11). The built-in `createKoraAuthServer().auth` provides it.
|
|
1809
|
+
*
|
|
1810
|
+
* @returns An unsubscribe function
|
|
1811
|
+
*/
|
|
1812
|
+
onRevoke?(listener: (event: SessionRevocation) => void | Promise<void>): () => void;
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Which live sync sessions a credential revocation ends. `userId` alone ends
|
|
1816
|
+
* every session of that user; with `deviceId` only that device's sessions
|
|
1817
|
+
* (matched against `AuthContext.metadata.deviceId`).
|
|
1818
|
+
*/
|
|
1819
|
+
interface SessionRevocation {
|
|
1820
|
+
userId?: string;
|
|
1821
|
+
deviceId?: string;
|
|
1822
|
+
}
|
|
1823
|
+
/** Options for {@link KoraSyncServer.terminateSessions}. */
|
|
1824
|
+
interface TerminateSessionsFilter extends SessionRevocation {
|
|
1825
|
+
/**
|
|
1826
|
+
* Error code sent to the client before the session closes. Both are retriable:
|
|
1827
|
+
* the client refreshes its credentials and re-handshakes, and the server then
|
|
1828
|
+
* decides again. Defaults to `'AUTH_REVOKED'`.
|
|
1829
|
+
*/
|
|
1830
|
+
code?: 'AUTH_REVOKED' | 'AUTH_EXPIRED';
|
|
835
1831
|
}
|
|
836
1832
|
/**
|
|
837
1833
|
* Configuration for creating a KoraSyncServer.
|
|
@@ -849,8 +1845,60 @@ interface KoraSyncServerConfig {
|
|
|
849
1845
|
serializer?: MessageSerializer;
|
|
850
1846
|
/** Event emitter for DevTools integration */
|
|
851
1847
|
emitter?: KoraEventEmitter;
|
|
852
|
-
/**
|
|
1848
|
+
/**
|
|
1849
|
+
* Maximum concurrent client connections (sessions, including ones still
|
|
1850
|
+
* handshaking). A connection over the limit gets a retriable `MAX_CONNECTIONS` error
|
|
1851
|
+
* and is closed. 0 = unlimited. Defaults to 10,000.
|
|
1852
|
+
*/
|
|
853
1853
|
maxConnections?: number;
|
|
1854
|
+
/**
|
|
1855
|
+
* Interval between WebSocket pings, in ms (SRV-6, LMS #12). A connection that
|
|
1856
|
+
* leaves two pings in a row unanswered is terminated, which ends its session.
|
|
1857
|
+
* Applies to the standalone server and `createProductionServer`. Defaults to 25
|
|
1858
|
+
* seconds; 0 disables probing.
|
|
1859
|
+
*/
|
|
1860
|
+
heartbeatIntervalMs?: number;
|
|
1861
|
+
/**
|
|
1862
|
+
* Interval of the application-level `heartbeat` message sent to clients that
|
|
1863
|
+
* advertise support for it (browsers cannot see WebSocket pings), in ms. A client
|
|
1864
|
+
* that hears nothing for about 2.5 intervals reconnects. Defaults to 25 seconds; 0
|
|
1865
|
+
* disables it.
|
|
1866
|
+
*/
|
|
1867
|
+
appHeartbeatIntervalMs?: number;
|
|
1868
|
+
/**
|
|
1869
|
+
* End-to-end encryption policy (protocol v2, ENC-3). With `required: true` an
|
|
1870
|
+
* uploaded plaintext operation that carries data is refused non-retriably
|
|
1871
|
+
* (`PLAINTEXT_REJECTED`), unless `allowPlaintextMigration` opens a migration window.
|
|
1872
|
+
* Encrypted operations (envelope v2) are always stored opaquely, never transformed.
|
|
1873
|
+
*/
|
|
1874
|
+
encryption?: {
|
|
1875
|
+
required: boolean;
|
|
1876
|
+
allowPlaintextMigration?: boolean;
|
|
1877
|
+
};
|
|
1878
|
+
/**
|
|
1879
|
+
* Time a new connection has to send its handshake, in ms. Defaults to 10 seconds;
|
|
1880
|
+
* 0 disables the deadline.
|
|
1881
|
+
*/
|
|
1882
|
+
handshakeTimeoutMs?: number;
|
|
1883
|
+
/**
|
|
1884
|
+
* Bytes that may wait unsent for one client (WebSocket send buffer, or the queue of
|
|
1885
|
+
* an HTTP long-poll client that stopped polling) before it is disconnected as a slow
|
|
1886
|
+
* consumer. Defaults to 32 MiB; 0 disables the ceiling. The delivery stream pauses
|
|
1887
|
+
* well before this (`deliveryHighWaterBytes`).
|
|
1888
|
+
*/
|
|
1889
|
+
maxBufferedBytes?: number;
|
|
1890
|
+
/**
|
|
1891
|
+
* Queued outbound bytes above which a client's delivery stream pauses until the
|
|
1892
|
+
* client drains them (backpressure). Defaults to 1 MiB.
|
|
1893
|
+
*/
|
|
1894
|
+
deliveryHighWaterBytes?: number;
|
|
1895
|
+
/**
|
|
1896
|
+
* WebSocket permessage-deflate compression. `true` (the default) compresses messages
|
|
1897
|
+
* of 1 KiB or more without keeping a compression context between messages, which
|
|
1898
|
+
* cuts sync payloads several-fold on 2G/3G links at a bounded memory cost per
|
|
1899
|
+
* connection. `false` disables it; an object is passed to `ws` as is.
|
|
1900
|
+
*/
|
|
1901
|
+
perMessageDeflate?: boolean | Record<string, unknown>;
|
|
854
1902
|
/** Maximum operations per sync batch. Defaults to 100. */
|
|
855
1903
|
batchSize?: number;
|
|
856
1904
|
/**
|
|
@@ -878,10 +1926,13 @@ interface KoraSyncServerConfig {
|
|
|
878
1926
|
max: number;
|
|
879
1927
|
};
|
|
880
1928
|
/**
|
|
881
|
-
*
|
|
882
|
-
*
|
|
883
|
-
*
|
|
884
|
-
*
|
|
1929
|
+
* Schema transforms from older client schema versions to the server schema version.
|
|
1930
|
+
* Required when `supportedSchemaVersions.min` is lower than `schemaVersion` and
|
|
1931
|
+
* operation shapes changed across versions. They run at fold time (RT-84): every
|
|
1932
|
+
* operation is stored and delivered exactly as uploaded; authorization, validators
|
|
1933
|
+
* and the store's fold use its transformed view. The server hands this list to its
|
|
1934
|
+
* store (`setOperationTransforms`); pass it to `store.setSchema` too to fold with it
|
|
1935
|
+
* from the first start. Transforms must be pure and deterministic.
|
|
885
1936
|
*/
|
|
886
1937
|
operationTransforms?: OperationTransform[];
|
|
887
1938
|
/** WebSocket path (standalone mode). Defaults to '/'. */
|
|
@@ -909,6 +1960,32 @@ interface KoraSyncServerConfig {
|
|
|
909
1960
|
* `@korajs/store` derives both from a `ContentAddressedBlobStore`.
|
|
910
1961
|
*/
|
|
911
1962
|
persistBlobChunk?: (hash: string, bytes: Uint8Array) => Promise<void> | void;
|
|
1963
|
+
/**
|
|
1964
|
+
* Limits on the blob side channel. `maxChunkBytes` (default 1 MiB) caps one
|
|
1965
|
+
* pushed chunk or manifest, `maxBytesPerSession` (default 256 MiB) caps the total
|
|
1966
|
+
* a session may push for central persistence, `maxPendingRequestsPerSession`
|
|
1967
|
+
* (default 256) and `pendingRequestTtlMs` (default 60s) bound the chunk requests
|
|
1968
|
+
* the relay remembers per session.
|
|
1969
|
+
*/
|
|
1970
|
+
blobLimits?: {
|
|
1971
|
+
maxChunkBytes?: number;
|
|
1972
|
+
maxBytesPerSession?: number;
|
|
1973
|
+
maxPendingRequestsPerSession?: number;
|
|
1974
|
+
pendingRequestTtlMs?: number;
|
|
1975
|
+
/**
|
|
1976
|
+
* Blob chunk requests one session may make per minute, a budget separate from
|
|
1977
|
+
* `maxOpsPerMinute` (one request per chunk, so a large blob makes many). Over
|
|
1978
|
+
* it, a request is answered with a retriable `throttled` response the client
|
|
1979
|
+
* backs off on (RT-24). Defaults to 6000.
|
|
1980
|
+
*/
|
|
1981
|
+
maxRequestsPerMinute?: number;
|
|
1982
|
+
};
|
|
1983
|
+
/**
|
|
1984
|
+
* Largest WebSocket message the standalone server (and `createProductionServer`)
|
|
1985
|
+
* accepts, in bytes. Larger frames are refused by the socket layer before they are
|
|
1986
|
+
* buffered. Defaults to 32 MiB (the `ws` library default is 100 MiB).
|
|
1987
|
+
*/
|
|
1988
|
+
maxMessageBytes?: number;
|
|
912
1989
|
/**
|
|
913
1990
|
* Maximum serialized byte size of a single client operation accepted at sync
|
|
914
1991
|
* ingest. Operations larger than this are rejected before materialization.
|
|
@@ -917,11 +1994,95 @@ interface KoraSyncServerConfig {
|
|
|
917
1994
|
*/
|
|
918
1995
|
maxOperationBytes?: number;
|
|
919
1996
|
/**
|
|
920
|
-
* Maximum operations accepted per
|
|
921
|
-
*
|
|
922
|
-
*
|
|
1997
|
+
* Maximum operations accepted per device node per minute (fixed window). The budget
|
|
1998
|
+
* belongs to the node id (bound to its principal when auth is configured), not to
|
|
1999
|
+
* the connection, so reconnecting does not reset it (SRV-6). Operations beyond the
|
|
2000
|
+
* limit get a retriable `RATE_LIMIT` until the window resets. Defaults to 600.
|
|
923
2001
|
*/
|
|
924
2002
|
maxOpsPerMinute?: number;
|
|
2003
|
+
/**
|
|
2004
|
+
* Maximum operations accepted per authenticated principal per minute, across all of
|
|
2005
|
+
* its device nodes, on top of the per-node `maxOpsPerMinute`. Without it a user could
|
|
2006
|
+
* multiply the per-node budget by minting node ids. Keyed by the provider's user id;
|
|
2007
|
+
* anonymous and unauthenticated sessions (no stable principal) have only the per-node
|
|
2008
|
+
* budget. Defaults to 4 x `maxOpsPerMinute` (2400), so one busy device never meets it
|
|
2009
|
+
* and a user's devices share it. `0` disables the per-user budget.
|
|
2010
|
+
*/
|
|
2011
|
+
maxOpsPerMinutePerUser?: number;
|
|
2012
|
+
/**
|
|
2013
|
+
* Largest operation batch accepted from a client in one message. A larger batch is
|
|
2014
|
+
* refused whole with `BATCH_TOO_LARGE` before the server decodes it or reads the
|
|
2015
|
+
* store, so one message cannot buy unbounded work. Defaults to 1000 (the client
|
|
2016
|
+
* sends batches of 100 by default).
|
|
2017
|
+
*/
|
|
2018
|
+
maxOpsPerBatch?: number;
|
|
2019
|
+
/**
|
|
2020
|
+
* Most values one `$in` scope predicate may hold, in either direction. A grant
|
|
2021
|
+
* with more is refused at handshake with `SCOPE_PREDICATE_LIMIT`, so an auth
|
|
2022
|
+
* provider bug cannot hand one session an unbounded predicate that every delivery
|
|
2023
|
+
* and revalidation then evaluates. Defaults to 100. See the production server
|
|
2024
|
+
* guide for the measured cost of larger values before raising it.
|
|
2025
|
+
*/
|
|
2026
|
+
maxScopePredicateValues?: number;
|
|
2027
|
+
/**
|
|
2028
|
+
* Accept anonymous devices whose node claim predates confirmed claims (RT-21):
|
|
2029
|
+
* nodes whose operation history predates node claims (a database written by a
|
|
2030
|
+
* beta.12 or older server, which recorded no claims; RT-91), nodes held by the
|
|
2031
|
+
* pre-release shared anonymous owner, and provisional claims that expired without
|
|
2032
|
+
* the device ever confirming its node token (beta.12 and older clients have no
|
|
2033
|
+
* token support). Accepted with a deprecation warning and re-issued a token.
|
|
2034
|
+
* Defaults to `true` for 1.0.0-beta.13; the default flips to `false` in the next
|
|
2035
|
+
* release, after which such devices rotate to a fresh node id instead (beta.12
|
|
2036
|
+
* clients cannot rotate and stay refused).
|
|
2037
|
+
*
|
|
2038
|
+
* With `MixedAuthProvider`, a node with pre-claims history may also be a signed-in
|
|
2039
|
+
* user's beta.12 device: an anonymous device can take such a node, as it could on
|
|
2040
|
+
* beta.12. Set this to `false` if that matters more than keeping anonymous beta.12
|
|
2041
|
+
* devices syncing.
|
|
2042
|
+
*/
|
|
2043
|
+
allowLegacyAnonymousClaims?: boolean;
|
|
2044
|
+
/**
|
|
2045
|
+
* Automatic device handover after an upgrade from beta.12 (F1). When a signed-in
|
|
2046
|
+
* device presents a node id that has operation history but no owner (beta.12
|
|
2047
|
+
* recorded no node claims) or that an administrator released, and the node id
|
|
2048
|
+
* equals the device id the auth provider verified for that user
|
|
2049
|
+
* (`AuthContext.metadata.deviceId`, set by the built-in `KoraAuthProvider` from the
|
|
2050
|
+
* token), the node is claimed for that user in one atomic step instead of being
|
|
2051
|
+
* refused `NODE_ID_CLAIMED`. A node another principal owns is never taken.
|
|
2052
|
+
* Defaults to `true`. A custom auth provider must set `metadata.deviceId` only to
|
|
2053
|
+
* a device id it verified as registered to the user; set this to `false` if yours
|
|
2054
|
+
* cannot guarantee that.
|
|
2055
|
+
*/
|
|
2056
|
+
deviceNodeHandover?: boolean;
|
|
2057
|
+
/**
|
|
2058
|
+
* What to do when a signed-in session's grant restricts no collection, so every user
|
|
2059
|
+
* syncs every other user's data (F4). That happens when the auth provider returns
|
|
2060
|
+
* a claims grant (the built-in provider's default) and the schema declares no sync
|
|
2061
|
+
* rule binding it, or when a provider returns no scopes. `'warn'` (default) logs it
|
|
2062
|
+
* once per provider; `'allow'` silences it for apps whose users all share one data
|
|
2063
|
+
* set; `'refuse'` refuses such handshakes with `UNSCOPED_SHARING_REFUSED`.
|
|
2064
|
+
* Anonymous sessions are not judged: their grant is the app's own choice.
|
|
2065
|
+
*/
|
|
2066
|
+
unscopedSharing?: 'warn' | 'allow' | 'refuse';
|
|
2067
|
+
/**
|
|
2068
|
+
* How long an anonymous device's provisional node claim may stay unconfirmed and
|
|
2069
|
+
* still be re-issued to a device that presents no token (a handshake response lost
|
|
2070
|
+
* in transit), in milliseconds (RT-21). Defaults to 24 hours.
|
|
2071
|
+
*/
|
|
2072
|
+
anonymousClaimTtlMs?: number;
|
|
2073
|
+
/**
|
|
2074
|
+
* How often every live session's credential is re-validated with the auth
|
|
2075
|
+
* provider, in milliseconds (RT-18). A revocation persisted by another server
|
|
2076
|
+
* instance ends the session here within this interval. Defaults to 30 seconds; 0
|
|
2077
|
+
* disables it (sessions then end only through this process's `onRevoke` feed,
|
|
2078
|
+
* `terminateSessions`, or credential expiry).
|
|
2079
|
+
*/
|
|
2080
|
+
sessionRevalidationIntervalMs?: number;
|
|
2081
|
+
/**
|
|
2082
|
+
* How long an HTTP long-poll session may go without any request before the server
|
|
2083
|
+
* closes it, in milliseconds. Defaults to 2 minutes; 0 disables expiry.
|
|
2084
|
+
*/
|
|
2085
|
+
httpSessionIdleTimeoutMs?: number;
|
|
925
2086
|
/**
|
|
926
2087
|
* Adjudicate untrusted client operations before they become authoritative.
|
|
927
2088
|
*
|
|
@@ -937,12 +2098,25 @@ interface KoraSyncServerConfig {
|
|
|
937
2098
|
}
|
|
938
2099
|
/**
|
|
939
2100
|
* Request envelope for the server-side HTTP sync endpoint.
|
|
2101
|
+
*
|
|
2102
|
+
* Map it from your HTTP framework: `sessionId` from the `x-kora-session` header and
|
|
2103
|
+
* `authorization` from the `Authorization` header, on every request.
|
|
940
2104
|
*/
|
|
941
2105
|
interface HttpSyncRequest {
|
|
942
|
-
/** Stable client identifier for binding HTTP requests to a server session */
|
|
943
|
-
clientId: string;
|
|
944
2106
|
/** HTTP method */
|
|
945
2107
|
method: 'GET' | 'POST';
|
|
2108
|
+
/**
|
|
2109
|
+
* The server-issued session id (`x-kora-session` request header). Absent only on
|
|
2110
|
+
* the POST that opens a session (the handshake); the response to that POST
|
|
2111
|
+
* carries the new id in its `x-kora-session` header. Never chosen by the client.
|
|
2112
|
+
*/
|
|
2113
|
+
sessionId?: string;
|
|
2114
|
+
/**
|
|
2115
|
+
* The raw `Authorization` header (`Bearer <token>`). With an auth provider
|
|
2116
|
+
* configured, EVERY request is authenticated and must resolve to the same
|
|
2117
|
+
* principal and device as the session it names (RT-2).
|
|
2118
|
+
*/
|
|
2119
|
+
authorization?: string;
|
|
946
2120
|
/** Optional raw request payload for POST */
|
|
947
2121
|
body?: string | Uint8Array;
|
|
948
2122
|
/** Value of the Content-Type header for POST payloads */
|
|
@@ -955,7 +2129,7 @@ interface HttpSyncRequest {
|
|
|
955
2129
|
*/
|
|
956
2130
|
interface HttpSyncResponse {
|
|
957
2131
|
/** HTTP status code */
|
|
958
|
-
status: 200 | 202 | 204 | 304 | 400 | 405 | 410;
|
|
2132
|
+
status: 200 | 202 | 204 | 304 | 400 | 401 | 403 | 404 | 405 | 410;
|
|
959
2133
|
/** Optional raw response payload */
|
|
960
2134
|
body?: string | Uint8Array;
|
|
961
2135
|
/** Optional response headers */
|
|
@@ -1002,6 +2176,20 @@ interface ServerStatus {
|
|
|
1002
2176
|
errorCount: number;
|
|
1003
2177
|
}
|
|
1004
2178
|
|
|
2179
|
+
/**
|
|
2180
|
+
* Which reverse proxies in front of the server are trusted to report the client
|
|
2181
|
+
* address in `X-Forwarded-For`.
|
|
2182
|
+
*
|
|
2183
|
+
* - `number`: trust that many hops, counting the socket peer as the first. `1` means
|
|
2184
|
+
* "one proxy (for example a load balancer) sits directly in front of the server".
|
|
2185
|
+
* - `string[]`: trust peers whose address is in one of these IPs or CIDR ranges
|
|
2186
|
+
* (IPv4 and IPv6, for example `['10.0.0.0/8', '::1']`).
|
|
2187
|
+
*
|
|
2188
|
+
* When unset, `X-Forwarded-For` is ignored and the socket address is the client
|
|
2189
|
+
* address, so a directly connected client cannot choose its own address.
|
|
2190
|
+
*/
|
|
2191
|
+
type TrustProxySetting = number | string[];
|
|
2192
|
+
|
|
1005
2193
|
interface HttpPollResponse {
|
|
1006
2194
|
status: 200 | 204 | 304 | 410;
|
|
1007
2195
|
body?: string | Uint8Array;
|
|
@@ -1021,56 +2209,176 @@ declare class HttpServerTransport implements ServerTransport {
|
|
|
1021
2209
|
private connected;
|
|
1022
2210
|
private nextSequence;
|
|
1023
2211
|
private readonly queue;
|
|
1024
|
-
|
|
2212
|
+
private queuedBytes;
|
|
2213
|
+
private readonly maxQueuedBytes;
|
|
2214
|
+
/**
|
|
2215
|
+
* @param serializer - Message serializer
|
|
2216
|
+
* @param options - `maxQueuedBytes`: close the session once this many bytes wait
|
|
2217
|
+
* unpolled (default 32 MiB; 0 disables the ceiling)
|
|
2218
|
+
*/
|
|
2219
|
+
constructor(serializer: MessageSerializer, options?: {
|
|
2220
|
+
maxQueuedBytes?: number;
|
|
2221
|
+
});
|
|
1025
2222
|
send(message: _korajs_sync.SyncMessage): void;
|
|
2223
|
+
/** Bytes queued for the client and not yet polled (delivery-stream backpressure). */
|
|
2224
|
+
bufferedAmount(): number;
|
|
1026
2225
|
onMessage(handler: ServerMessageHandler): void;
|
|
1027
2226
|
onClose(handler: ServerCloseHandler): void;
|
|
1028
2227
|
onError(handler: ServerErrorHandler): void;
|
|
1029
2228
|
isConnected(): boolean;
|
|
2229
|
+
/**
|
|
2230
|
+
* Close the session. Messages already queued (typically the final error that
|
|
2231
|
+
* explains the close, for example a refused handshake) stay pollable, so a
|
|
2232
|
+
* long-polling client learns why, exactly as a WebSocket client receives the error
|
|
2233
|
+
* frame before the close; once drained, polls answer 410.
|
|
2234
|
+
*/
|
|
1030
2235
|
close(code?: number, reason?: string): void;
|
|
2236
|
+
/** True while messages queued before the close are still waiting to be polled. */
|
|
2237
|
+
hasPending(): boolean;
|
|
1031
2238
|
receive(payload: string | Uint8Array): void;
|
|
1032
2239
|
poll(ifNoneMatch?: string): HttpPollResponse;
|
|
1033
2240
|
private makeEtag;
|
|
1034
2241
|
}
|
|
1035
2242
|
|
|
1036
|
-
/**
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
removeAllListeners(): void;
|
|
2243
|
+
/** One read of a record for a presence decision. */
|
|
2244
|
+
interface PresenceRecordRead {
|
|
2245
|
+
/** The stored row (soft-deleted rows included), or null when the server holds none */
|
|
2246
|
+
stored: MaterializedRecord | null;
|
|
2247
|
+
/**
|
|
2248
|
+
* The reader's clock when the read began. A write that touched the record after
|
|
2249
|
+
* it ({@link PresenceRecords.touchedSince}) means the row may predate that write.
|
|
2250
|
+
*/
|
|
2251
|
+
asOf: number;
|
|
1046
2252
|
}
|
|
1047
2253
|
/**
|
|
1048
|
-
*
|
|
2254
|
+
* Reads the records presence cursors name, for every session of one sync server (F16).
|
|
2255
|
+
*
|
|
2256
|
+
* Presence decides who may see a cursor from the record it names, so the row must be
|
|
2257
|
+
* the store's latest, never a copy another code path holds (a session's delivery
|
|
2258
|
+
* pass caches rows for the whole chunk it filters). Every write the server commits
|
|
2259
|
+
* {@link touch}es the records it wrote; a read is current when no touch of its record
|
|
2260
|
+
* came after it began. Reads of one record are shared while current (many cursors on
|
|
2261
|
+
* one busy document cost one read per write burst, not one per cursor per write), a
|
|
2262
|
+
* read is never shared across a touch, and reads are bounded in number at once.
|
|
2263
|
+
*
|
|
2264
|
+
* Presence is ephemeral and nothing here is persisted.
|
|
1049
2265
|
*/
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
2266
|
+
declare class PresenceRecords {
|
|
2267
|
+
private readonly store;
|
|
2268
|
+
private readonly now;
|
|
2269
|
+
private clock;
|
|
2270
|
+
/** The clock value of each tracked record's last touch, oldest first. */
|
|
2271
|
+
private readonly lastTouch;
|
|
2272
|
+
/** Touches forgotten to bound memory count as having happened at this clock. */
|
|
2273
|
+
private forgottenTouchesAt;
|
|
2274
|
+
private readonly inFlight;
|
|
2275
|
+
private readonly cache;
|
|
2276
|
+
private running;
|
|
2277
|
+
private readonly waiting;
|
|
2278
|
+
constructor(store: ServerStore, now?: () => number);
|
|
2279
|
+
/**
|
|
2280
|
+
* Record that writes to these records committed: reads that began before are no
|
|
2281
|
+
* longer current. Null means any record may have changed.
|
|
2282
|
+
*
|
|
2283
|
+
* @param keys - Record keys from {@link presenceRecordKey}, or null for every record
|
|
2284
|
+
*/
|
|
2285
|
+
touch(keys: Iterable<string> | null): void;
|
|
2286
|
+
/**
|
|
2287
|
+
* True when a write touched the record after a read that began at `asOf`.
|
|
2288
|
+
*
|
|
2289
|
+
* @param key - Record key from {@link presenceRecordKey}
|
|
2290
|
+
* @param asOf - The read's {@link PresenceRecordRead.asOf}
|
|
2291
|
+
*/
|
|
2292
|
+
touchedSince(key: string, asOf: number): boolean;
|
|
2293
|
+
/**
|
|
2294
|
+
* A current read of the record that is still fresh enough to reuse, without a store
|
|
2295
|
+
* read; undefined when the record must be read.
|
|
2296
|
+
*
|
|
2297
|
+
* @param collection - The record's collection
|
|
2298
|
+
* @param recordId - The record's id
|
|
2299
|
+
*/
|
|
2300
|
+
peek(collection: string, recordId: string): PresenceRecordRead | undefined;
|
|
2301
|
+
/**
|
|
2302
|
+
* Read the record from the store, joining a read of it already running when no
|
|
2303
|
+
* write touched the record since that read began. Rejects when the store read fails.
|
|
2304
|
+
*
|
|
2305
|
+
* @param collection - The record's collection
|
|
2306
|
+
* @param recordId - The record's id
|
|
2307
|
+
*/
|
|
2308
|
+
read(collection: string, recordId: string): Promise<PresenceRecordRead>;
|
|
2309
|
+
/** Forget every read and touch (the server stopped). */
|
|
2310
|
+
clear(): void;
|
|
2311
|
+
private readStore;
|
|
2312
|
+
private forget;
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
/** Outcome of one key-service message. */
|
|
2316
|
+
interface KeyServiceOutcome {
|
|
2317
|
+
response: EncryptionKeyResponseMessage;
|
|
2318
|
+
/** The record written, when the message was a successful write (to push to siblings). */
|
|
2319
|
+
written: WrappedKeyRecord | null;
|
|
1053
2320
|
}
|
|
1054
2321
|
/**
|
|
1055
|
-
*
|
|
1056
|
-
*
|
|
2322
|
+
* The sync server's key service (ENC-1, decision D4b). Stores each owner's wrapped key
|
|
2323
|
+
* record and serves it back. It never sees a passphrase or a data key: a record is
|
|
2324
|
+
* salt, KDF parameters, AES-GCM-wrapped data keys, key ids and versions.
|
|
2325
|
+
*
|
|
2326
|
+
* Rules it enforces:
|
|
2327
|
+
* - **Owner from the session only.** The owner is the session's authenticated
|
|
2328
|
+
* principal (or the shared anonymous owner on a server without auth); a message
|
|
2329
|
+
* cannot name a user, so no session reads or writes another user's record.
|
|
2330
|
+
* - **Compare-and-set.** A write names the revision it replaces; a concurrent write
|
|
2331
|
+
* loses with `conflict` and the current record, so two devices that both create a
|
|
2332
|
+
* first key converge on one.
|
|
2333
|
+
* - **History is append-only.** A write must raise the revision, keep the ring id and
|
|
2334
|
+
* every key version (same key id), so no device can make old operations unreadable
|
|
2335
|
+
* for the others. A first write (or a re-upload after the record was lost) keeps the
|
|
2336
|
+
* revision it carries, since devices refuse revisions below the one they accepted.
|
|
2337
|
+
* - **Lost records are visible.** A fetch with no record reports key ids of the owner's
|
|
2338
|
+
* stored encrypted operations, so a new device waits for one holding the ring.
|
|
2339
|
+
*
|
|
2340
|
+
* It cannot check the record MAC or that wraps open (only a holder of the ring's master
|
|
2341
|
+
* key can): devices authenticate every record themselves and act on nothing else. A
|
|
2342
|
+
* principal can still overwrite its own record with garbage; devices refuse it and
|
|
2343
|
+
* keep their keys.
|
|
1057
2344
|
*/
|
|
1058
|
-
declare class
|
|
1059
|
-
private readonly
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
2345
|
+
declare class EncryptionKeyService {
|
|
2346
|
+
private readonly store;
|
|
2347
|
+
constructor(store: ServerStore);
|
|
2348
|
+
/** Whether the store can persist key records (all built-in stores can). */
|
|
2349
|
+
isSupported(): boolean;
|
|
2350
|
+
/**
|
|
2351
|
+
* Answer one key-service message for `owner`.
|
|
2352
|
+
*
|
|
2353
|
+
* @param owner - The session's owner key, or null when the session may not hold keys
|
|
2354
|
+
* @param message - A request or a write
|
|
2355
|
+
*/
|
|
2356
|
+
handle(owner: string | null, message: EncryptionKeyRequestMessage | EncryptionKeyPutMessage): Promise<KeyServiceOutcome>;
|
|
2357
|
+
/**
|
|
2358
|
+
* Key ids of the owner's stored encrypted operations (a sample). Reported only while
|
|
2359
|
+
* no record exists: then non-empty means the record was lost, and a new device must
|
|
2360
|
+
* wait for one that holds the ring instead of starting another (RT-104).
|
|
2361
|
+
*/
|
|
2362
|
+
private knownKeyIds;
|
|
2363
|
+
private read;
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
/** What a session charges uploaded operations to. */
|
|
2367
|
+
interface IngestRateLimiter {
|
|
2368
|
+
/** Units allowed per window (the tightest applicable budget). */
|
|
2369
|
+
readonly limit: number;
|
|
2370
|
+
/** Charge `count` units; false when a budget is exceeded (the units are refused). */
|
|
2371
|
+
allow(count?: number): boolean;
|
|
2372
|
+
/** Milliseconds until a refused unit may be retried. */
|
|
2373
|
+
retryAfterMs(): number;
|
|
1072
2374
|
}
|
|
1073
2375
|
|
|
2376
|
+
/**
|
|
2377
|
+
* Session-ending error codes, all retriable. `AUTH_REVOKED` / `AUTH_EXPIRED`: the
|
|
2378
|
+
* client refreshes its credential and re-handshakes. `SCOPE_CHANGED` (RT-26): the
|
|
2379
|
+
* principal's grant changed; the client simply reconnects and gets the new scope.
|
|
2380
|
+
*/
|
|
2381
|
+
type SessionTerminationCode = 'AUTH_REVOKED' | 'AUTH_EXPIRED' | 'SCOPE_CHANGED';
|
|
1074
2382
|
/**
|
|
1075
2383
|
* Possible states for a client session.
|
|
1076
2384
|
*/
|
|
@@ -1082,11 +2390,27 @@ type RelayCallback = (sourceSessionId: string, operations: Operation[]) => void;
|
|
|
1082
2390
|
/**
|
|
1083
2391
|
* Callback invoked when a session receives an awareness update to relay to other sessions.
|
|
1084
2392
|
*/
|
|
1085
|
-
type AwarenessRelayCallback = (sourceSessionId: string, message: AwarenessUpdateMessage) => void;
|
|
2393
|
+
type AwarenessRelayCallback = (sourceSessionId: string, message: AwarenessUpdateMessage, cursorTarget?: AwarenessCursorTarget) => void;
|
|
2394
|
+
/**
|
|
2395
|
+
* The record an awareness state's cursor names, as the sending session resolved it
|
|
2396
|
+
* from the store. `stored` is null for a record the server does not hold. A cursor
|
|
2397
|
+
* that is not a `{ collection, recordId }` pair of strings is `{ invalid: true }`;
|
|
2398
|
+
* a state without a cursor has no target.
|
|
2399
|
+
*/
|
|
2400
|
+
type AwarenessCursorTarget = {
|
|
2401
|
+
invalid?: false;
|
|
2402
|
+
collection: string;
|
|
2403
|
+
recordId: string;
|
|
2404
|
+
stored: MaterializedRecord | null;
|
|
2405
|
+
/** When the read of `stored` began, on the server's presence reader clock. */
|
|
2406
|
+
asOf?: number;
|
|
2407
|
+
} | {
|
|
2408
|
+
invalid: true;
|
|
2409
|
+
};
|
|
1086
2410
|
/**
|
|
1087
2411
|
* Callback invoked when a session receives a Yjs doc channel update to relay.
|
|
1088
2412
|
*/
|
|
1089
|
-
type YjsDocRelayCallback = (sourceSessionId: string, message: YjsDocUpdateMessage) => void;
|
|
2413
|
+
type YjsDocRelayCallback = (sourceSessionId: string, message: YjsDocUpdateMessage, storedRecord: MaterializedRecord | null) => void;
|
|
1090
2414
|
/**
|
|
1091
2415
|
* Callback invoked when a session receives a blob chunk request to route.
|
|
1092
2416
|
*/
|
|
@@ -1128,20 +2452,41 @@ interface ClientSessionOptions {
|
|
|
1128
2452
|
min: number;
|
|
1129
2453
|
max: number;
|
|
1130
2454
|
};
|
|
1131
|
-
/**
|
|
2455
|
+
/** Schema transforms: judged views of older operations (transforms at fold time, RT-84). */
|
|
1132
2456
|
operationTransforms?: OperationTransform[];
|
|
1133
2457
|
/** Called when this session has operations to relay to other sessions */
|
|
1134
2458
|
onRelay?: RelayCallback;
|
|
1135
2459
|
/** Called when this session receives an awareness update to broadcast */
|
|
1136
2460
|
onAwarenessUpdate?: AwarenessRelayCallback;
|
|
2461
|
+
/**
|
|
2462
|
+
* The reader of the records presence cursors name, shared by the server's sessions
|
|
2463
|
+
* (F16). Defaults to one of this session's own.
|
|
2464
|
+
*/
|
|
2465
|
+
presenceRecords?: PresenceRecords;
|
|
1137
2466
|
/** Called when this session receives a Yjs doc channel update to broadcast */
|
|
1138
2467
|
onYjsDocUpdate?: YjsDocRelayCallback;
|
|
1139
2468
|
/** Called when this session receives a blob chunk request to route */
|
|
1140
2469
|
onBlobChunkRequest?: BlobChunkRequestCallback;
|
|
1141
2470
|
/** Called when this session receives a blob chunk response to route back */
|
|
1142
2471
|
onBlobChunkResponse?: BlobChunkResponseCallback;
|
|
2472
|
+
/** The key service storing wrapped encryption key records (ENC-1). */
|
|
2473
|
+
encryptionKeys?: EncryptionKeyService;
|
|
2474
|
+
/** Called after this session wrote a key record, to push it to the owner's other sessions. */
|
|
2475
|
+
onEncryptionKeyWritten?: (sessionId: string, owner: string, keyring: string, record: WrappedKeyRecord) => void;
|
|
2476
|
+
/** Key-service messages allowed per session per minute. Defaults to 60. */
|
|
2477
|
+
maxKeyRequestsPerMinute?: number;
|
|
1143
2478
|
/** Persist a client-uploaded blob chunk centrally (keyed by content hash) */
|
|
1144
2479
|
persistBlobChunk?: PersistBlobChunk;
|
|
2480
|
+
/**
|
|
2481
|
+
* Called once the session completes an accepted handshake and reaches streaming.
|
|
2482
|
+
* Side channels (Yjs doc, blob and awareness relays) register the session here,
|
|
2483
|
+
* never at connect time, so an unauthenticated connection is never on a relay.
|
|
2484
|
+
*/
|
|
2485
|
+
onReady?: (sessionId: string) => void;
|
|
2486
|
+
/** Largest single blob chunk this session may push. Defaults to 1 MiB. */
|
|
2487
|
+
maxBlobChunkBytes?: number;
|
|
2488
|
+
/** Total blob bytes this session may push. Defaults to 256 MiB. */
|
|
2489
|
+
maxBlobBytesPerSession?: number;
|
|
1145
2490
|
/** Called when this session closes */
|
|
1146
2491
|
onClose?: (sessionId: string) => void;
|
|
1147
2492
|
/**
|
|
@@ -1161,6 +2506,24 @@ interface ClientSessionOptions {
|
|
|
1161
2506
|
maxOperationBytes?: number;
|
|
1162
2507
|
/** Maximum operations accepted per minute for this session. Defaults to 600. */
|
|
1163
2508
|
maxOpsPerMinute?: number;
|
|
2509
|
+
/**
|
|
2510
|
+
* Blob chunk requests accepted per minute, separate from the operation budget
|
|
2511
|
+
* (RT-24). Defaults to 6000.
|
|
2512
|
+
*/
|
|
2513
|
+
maxBlobRequestsPerMinute?: number;
|
|
2514
|
+
/** Length of the blob request window in ms. One minute; shorter only in tests. */
|
|
2515
|
+
blobRequestWindowMs?: number;
|
|
2516
|
+
/**
|
|
2517
|
+
* Largest operation batch accepted in one message. A larger batch is refused whole
|
|
2518
|
+
* (`BATCH_TOO_LARGE`) before it is decoded or the store is read. Defaults to 1000.
|
|
2519
|
+
*/
|
|
2520
|
+
maxOpsPerBatch?: number;
|
|
2521
|
+
/**
|
|
2522
|
+
* Most values one `$in` scope predicate may hold. A grant over the limit is
|
|
2523
|
+
* refused at handshake (`SCOPE_PREDICATE_LIMIT`). Defaults to
|
|
2524
|
+
* {@link DEFAULT_MAX_SCOPE_PREDICATE_VALUES} (100).
|
|
2525
|
+
*/
|
|
2526
|
+
maxScopePredicateValues?: number;
|
|
1164
2527
|
/**
|
|
1165
2528
|
* Adjudicate untrusted client operations before materialization. When present,
|
|
1166
2529
|
* each incoming operation is passed to this validator; a `reject` decision
|
|
@@ -1169,6 +2532,71 @@ interface ClientSessionOptions {
|
|
|
1169
2532
|
validateOperation?: OperationValidator;
|
|
1170
2533
|
/** Trusted data-plane context handed to the validator (read state, author derived ops). */
|
|
1171
2534
|
koraContext?: ProductionHttpRouteContext;
|
|
2535
|
+
/**
|
|
2536
|
+
* Blob reference authority (RT-11): decides which content hashes this session may
|
|
2537
|
+
* reference in blob fields and records the bytes it pushes. Without it, blob
|
|
2538
|
+
* references are not checked at ingest.
|
|
2539
|
+
*/
|
|
2540
|
+
blobAccess?: BlobAccessIndex;
|
|
2541
|
+
/** See `KoraSyncServerConfig.allowLegacyAnonymousClaims` (RT-21). Defaults to true. */
|
|
2542
|
+
allowLegacyAnonymousClaims?: boolean;
|
|
2543
|
+
/** See `KoraSyncServerConfig.deviceNodeHandover` (F1). Defaults to true. */
|
|
2544
|
+
deviceNodeHandover?: boolean;
|
|
2545
|
+
/** See `KoraSyncServerConfig.unscopedSharing` (F4). Defaults to `'warn'`. */
|
|
2546
|
+
unscopedSharing?: 'warn' | 'allow' | 'refuse';
|
|
2547
|
+
/** See `KoraSyncServerConfig.anonymousClaimTtlMs` (RT-21). Defaults to 24 hours. */
|
|
2548
|
+
anonymousClaimTtlMs?: number;
|
|
2549
|
+
/**
|
|
2550
|
+
* True when another live session (not `exceptSessionId`) is connected as `nodeId`.
|
|
2551
|
+
* A provisional anonymous claim is never re-issued away from a connected device.
|
|
2552
|
+
*/
|
|
2553
|
+
isNodeLive?: (nodeId: string, exceptSessionId: string) => boolean;
|
|
2554
|
+
/**
|
|
2555
|
+
* Time a new connection has to deliver its handshake, in ms (SRV-6). A connection
|
|
2556
|
+
* that sends none is closed with `HANDSHAKE_TIMEOUT`. Defaults to 10 seconds; 0
|
|
2557
|
+
* disables the deadline.
|
|
2558
|
+
*/
|
|
2559
|
+
handshakeTimeoutMs?: number;
|
|
2560
|
+
/**
|
|
2561
|
+
* Queued outbound bytes above which the delivery stream pauses until the client
|
|
2562
|
+
* drains its socket (transports that report `bufferedAmount`). Defaults to 1 MiB.
|
|
2563
|
+
*/
|
|
2564
|
+
deliveryHighWaterBytes?: number;
|
|
2565
|
+
/**
|
|
2566
|
+
* The ingest rate limiter for a node, shared across that node's sessions so a
|
|
2567
|
+
* reconnect does not reset it (SRV-6). Called once the handshake binds the node,
|
|
2568
|
+
* with the authenticated principal's user id (null for anonymous and
|
|
2569
|
+
* unauthenticated sessions) so a per-user budget can be layered on top: a user
|
|
2570
|
+
* minting node ids must not multiply the per-node budget.
|
|
2571
|
+
* Without it the session keeps a private limiter.
|
|
2572
|
+
*/
|
|
2573
|
+
rateLimiterFor?: (nodeId: string, principal: string | null) => IngestRateLimiter;
|
|
2574
|
+
/**
|
|
2575
|
+
* Interval of the application-level heartbeat sent to clients that advertised
|
|
2576
|
+
* support for it, in ms (LMS #12). The handshake response tells the client the
|
|
2577
|
+
* interval so it can declare the connection dead after missing two. 0 disables it.
|
|
2578
|
+
*/
|
|
2579
|
+
appHeartbeatIntervalMs?: number;
|
|
2580
|
+
/**
|
|
2581
|
+
* Node ids whose operations this server authors (protocol v2). Sent to clients in
|
|
2582
|
+
* the handshake response (`authoritativeNodeIds`); only operations from these nodes
|
|
2583
|
+
* may carry server-authored metadata (`fieldVersions`, `foldState`). Defaults to the
|
|
2584
|
+
* ids the store folds with (`ServerStore.getAuthoritativeNodeIds`), which is what
|
|
2585
|
+
* `KoraSyncServer` always passes. Scope-entry operations (`kora:scope-entry`) are not
|
|
2586
|
+
* listed: they carry the server's fold state and are joined, never folded as writes.
|
|
2587
|
+
*/
|
|
2588
|
+
authoritativeNodeIds?: readonly string[];
|
|
2589
|
+
/**
|
|
2590
|
+
* End-to-end encryption policy (protocol v2, ENC-3). With `required`, every uploaded
|
|
2591
|
+
* data-bearing operation must carry the encryption envelope; a plaintext one is
|
|
2592
|
+
* refused non-retriably (`PLAINTEXT_REJECTED`), unless `allowPlaintextMigration` is
|
|
2593
|
+
* set for a migration window. Without it, the server stores whatever it receives
|
|
2594
|
+
* (envelope operations always opaquely).
|
|
2595
|
+
*/
|
|
2596
|
+
encryption?: {
|
|
2597
|
+
required: boolean;
|
|
2598
|
+
allowPlaintextMigration?: boolean;
|
|
2599
|
+
};
|
|
1172
2600
|
}
|
|
1173
2601
|
/**
|
|
1174
2602
|
* Handles the sync protocol for a single connected client.
|
|
@@ -1188,6 +2616,31 @@ declare class ClientSession {
|
|
|
1188
2616
|
private state;
|
|
1189
2617
|
private clientNodeId;
|
|
1190
2618
|
private authContext;
|
|
2619
|
+
/**
|
|
2620
|
+
* The context the auth provider returned, kept apart from {@link authContext}
|
|
2621
|
+
* (which an unauthenticated session also gets, keyed by node id) so revocation
|
|
2622
|
+
* only ever matches a verified principal.
|
|
2623
|
+
*/
|
|
2624
|
+
private principal;
|
|
2625
|
+
/** Closes the session when its credential expires (AUTH-11). */
|
|
2626
|
+
private expiryTimer;
|
|
2627
|
+
/** Revocations that arrived while the handshake was still authenticating. */
|
|
2628
|
+
private readonly pendingRevocations;
|
|
2629
|
+
/** Node token issued by this handshake's first anonymous claim, sent once in the response (RT-12). */
|
|
2630
|
+
private issuedNodeToken;
|
|
2631
|
+
/** The node-claim owner this session holds its node id under (userId, or an anonymous device key). */
|
|
2632
|
+
private nodeOwnerKey;
|
|
2633
|
+
/** A provisional anonymous claim awaiting the device's confirmation (RT-21). */
|
|
2634
|
+
private pendingNodeClaim;
|
|
2635
|
+
private readonly allowLegacyAnonymousClaims;
|
|
2636
|
+
private readonly deviceNodeHandover;
|
|
2637
|
+
private readonly unscopedSharing;
|
|
2638
|
+
private readonly anonymousClaimTtlMs;
|
|
2639
|
+
private readonly isNodeLive;
|
|
2640
|
+
/** The credential presented at handshake, kept to re-validate the session (RT-18). */
|
|
2641
|
+
private credential;
|
|
2642
|
+
/** The scope the client asked for at handshake, kept to re-resolve scopes (RT-26). */
|
|
2643
|
+
private handshakeScope;
|
|
1191
2644
|
private syncQuerySubsets;
|
|
1192
2645
|
private scopeExitPolicy;
|
|
1193
2646
|
private resumeDeltaCursor;
|
|
@@ -1200,23 +2653,69 @@ declare class ClientSession {
|
|
|
1200
2653
|
private clientDeliveryWatermark;
|
|
1201
2654
|
/**
|
|
1202
2655
|
* The highest delivery sequence this client has ACKNOWLEDGED (its confirmed
|
|
1203
|
-
* watermark, as reported in acks).
|
|
1204
|
-
*
|
|
1205
|
-
*
|
|
1206
|
-
*
|
|
1207
|
-
* is no sent-but-unacked window that a buffer eviction could strand. Seeded from the
|
|
1208
|
-
* client's reported watermark at handshake.
|
|
2656
|
+
* watermark, as reported in acks). A retransmission rewinds the send cursor to here,
|
|
2657
|
+
* so a dropped or unapplied batch is always re-included by a re-scan from the
|
|
2658
|
+
* acknowledged position: recovery never depends on a bounded retransmit buffer.
|
|
2659
|
+
* Seeded from the client's reported watermark at handshake.
|
|
1209
2660
|
*/
|
|
1210
2661
|
private lastAckedDeliverySeq;
|
|
1211
2662
|
/**
|
|
1212
|
-
*
|
|
1213
|
-
*
|
|
1214
|
-
*
|
|
2663
|
+
* The delivery send cursor (SRV-3): the max of the last batch sent. Live pushes
|
|
2664
|
+
* chain from it (base = lastSent), so each operation goes out once per retransmit
|
|
2665
|
+
* epoch instead of the whole unacknowledged backlog on every write. Invariant:
|
|
2666
|
+
* lastAcked <= lastSent. It rewinds to lastAcked only when the outstanding delivery
|
|
2667
|
+
* went unacknowledged for the (backed-off) stale window.
|
|
2668
|
+
*/
|
|
2669
|
+
private lastSentDeliverySeq;
|
|
2670
|
+
/** The highest batch max ever sent to this client (acks are capped by it). */
|
|
2671
|
+
private highestSentDeliverySeq;
|
|
2672
|
+
/**
|
|
2673
|
+
* Own-node operations at or below this delivery sequence are streamed to this
|
|
2674
|
+
* client; above it they are skipped (it uploaded them itself). Set at handshake: a
|
|
2675
|
+
* full resync (watermark 0) recovers the client's own history up to the frontier
|
|
2676
|
+
* at that moment; a resume excludes all of them.
|
|
1215
2677
|
*/
|
|
1216
|
-
private
|
|
1217
|
-
|
|
1218
|
-
|
|
2678
|
+
private ownOperationsIncludedThrough;
|
|
2679
|
+
/** When the delivery last made progress (an ack advanced, or a fresh send/rewind). */
|
|
2680
|
+
private lastDeliveryProgressAtMs;
|
|
2681
|
+
/** Consecutive rewinds without acknowledgment progress (drives the backoff, LMS #12). */
|
|
2682
|
+
private deliveryRewinds;
|
|
2683
|
+
/**
|
|
2684
|
+
* Sent, not yet acknowledged delivery batches in send order (their max and send
|
|
2685
|
+
* time), for round-trip samples. `resent` marks a re-sent range, which yields no
|
|
2686
|
+
* sample (Karn's rule: its ack cannot be matched to one transmission).
|
|
2687
|
+
*/
|
|
2688
|
+
private readonly unackedBatches;
|
|
2689
|
+
private smoothedRttMs;
|
|
2690
|
+
private rttVarianceMs;
|
|
2691
|
+
/**
|
|
2692
|
+
* Retransmit timeout of the delivery stream (RFC 6298 style: smoothed round trip
|
|
2693
|
+
* plus four deviations, floored). An outstanding delivery with no acknowledgment
|
|
2694
|
+
* progress for this long (doubled per consecutive re-send) is re-sent from the
|
|
2695
|
+
* acknowledged position. Round trips include the client's apply time, so a slow
|
|
2696
|
+
* device earns a longer timeout instead of spurious re-sends.
|
|
2697
|
+
*/
|
|
2698
|
+
private retransmitTimeoutMs;
|
|
2699
|
+
private retransmitTimer;
|
|
2700
|
+
/** A delivery push is queued but not started: further wake-ups coalesce into it. */
|
|
2701
|
+
private deliveryPushQueued;
|
|
2702
|
+
/** Serializes delivery pushes so their batches never interleave. */
|
|
1219
2703
|
private deliveryPushChain;
|
|
2704
|
+
/**
|
|
2705
|
+
* Records prefetched for the delivery chunk being filtered (LMS #11: one batched
|
|
2706
|
+
* lookup per collection per chunk instead of one query per operation). Keyed
|
|
2707
|
+
* `collection\u0000id`; null marks a record known to be absent. Only set while a
|
|
2708
|
+
* chunk is filtered.
|
|
2709
|
+
*/
|
|
2710
|
+
private readonly deliveryHighWaterBytes;
|
|
2711
|
+
private readonly handshakeTimeoutMs;
|
|
2712
|
+
private handshakeTimer;
|
|
2713
|
+
private handshakeReceived;
|
|
2714
|
+
private readonly rateLimiterFor;
|
|
2715
|
+
private readonly appHeartbeatIntervalMs;
|
|
2716
|
+
private appHeartbeatTimer;
|
|
2717
|
+
/** Last time anything was sent to the client (a heartbeat is skipped when recent). */
|
|
2718
|
+
private lastClientSendAtMs;
|
|
1220
2719
|
/**
|
|
1221
2720
|
* Relay batches sent to this client but not yet acknowledged, keyed by messageId.
|
|
1222
2721
|
* The client acks every operation-batch it applies; on ack the entry is cleared.
|
|
@@ -1243,15 +2742,64 @@ declare class ClientSession {
|
|
|
1243
2742
|
private readonly onYjsDocUpdate;
|
|
1244
2743
|
private readonly onBlobChunkRequest;
|
|
1245
2744
|
private readonly onBlobChunkResponse;
|
|
2745
|
+
private readonly encryptionKeys;
|
|
2746
|
+
private readonly onEncryptionKeyWritten;
|
|
2747
|
+
/** Budget of key-service messages (each may cost a store read and write). */
|
|
2748
|
+
private readonly keyRateLimiter;
|
|
1246
2749
|
private readonly persistBlobChunk;
|
|
1247
2750
|
private readonly onClose;
|
|
2751
|
+
private readonly onReady;
|
|
2752
|
+
private readonly maxBlobChunkBytes;
|
|
2753
|
+
private readonly maxBlobBytesPerSession;
|
|
2754
|
+
private blobBytesPushed;
|
|
1248
2755
|
private readonly onOrphanedRelays;
|
|
1249
2756
|
private readonly takeOrphanedRelays;
|
|
1250
2757
|
private readonly maxOperationBytes;
|
|
1251
2758
|
private readonly maxOpsPerMinute;
|
|
1252
|
-
private readonly
|
|
2759
|
+
private readonly maxOpsPerBatch;
|
|
2760
|
+
private readonly maxScopePredicateValues;
|
|
2761
|
+
/** {@link getScopePartitionKey}, cached for the scope map it was computed from. */
|
|
2762
|
+
private scopePartitionKeyCache;
|
|
2763
|
+
/** Store reads for presence cursors (F16), per minute. */
|
|
2764
|
+
private readonly presenceLookupLimiter;
|
|
2765
|
+
/** Reads the records presence cursors name (shared by the server's sessions). */
|
|
2766
|
+
private readonly presenceRecords;
|
|
2767
|
+
/** A {@link refreshScopes} request arrived during the handshake: re-check once streaming. */
|
|
2768
|
+
private scopeRefreshPending;
|
|
2769
|
+
private rateLimiter;
|
|
2770
|
+
/** Separate budget for blob chunk requests (RT-24). */
|
|
2771
|
+
private readonly blobRateLimiter;
|
|
2772
|
+
/** Operations refused by the rate limiter (RT-6), for diagnostics. */
|
|
2773
|
+
private rateLimitedOperations;
|
|
2774
|
+
/** Batches refused whole for exceeding {@link maxOpsPerBatch} (RT-6). */
|
|
2775
|
+
private rejectedBatches;
|
|
2776
|
+
/** Blob chunk requests answered "not held" because the session was over budget (RT-17). */
|
|
2777
|
+
private rateLimitedBlobRequests;
|
|
2778
|
+
/**
|
|
2779
|
+
* The client advertised the `sequenceReservation` handshake capability (RT-37): it
|
|
2780
|
+
* reserves sequence numbers in-transaction, so a second operation under a held
|
|
2781
|
+
* (node, sequence) is refused with SEQUENCE_CONFLICT. False for a legacy client
|
|
2782
|
+
* (Kora <= beta.12), whose duplicate pairs are stored instead.
|
|
2783
|
+
*/
|
|
2784
|
+
private sequenceReservation;
|
|
2785
|
+
/** Legacy duplicate pairs this session stored (RT-37), for diagnostics. */
|
|
2786
|
+
private legacySequencePairs;
|
|
2787
|
+
/** Protocol version the client declared in its handshake (1 when absent). */
|
|
2788
|
+
private clientProtocolVersion;
|
|
2789
|
+
private readonly authoritativeNodeIds;
|
|
2790
|
+
/** Hash version an undeclared uploaded id was verified as (RT-64), by operation object. */
|
|
2791
|
+
private readonly matchedHashVersions;
|
|
2792
|
+
/** Uploads whose data is stored as the beta.12 writer held it (RT-71). */
|
|
2793
|
+
private readonly restoredLegacyData;
|
|
2794
|
+
/** Operations accepted with an unverified legacy (beta.12) id (RT-71). */
|
|
2795
|
+
private unverifiedLegacyOperations;
|
|
2796
|
+
private forgedDuplicates;
|
|
2797
|
+
private readonly encryptionPolicy;
|
|
2798
|
+
/** Uploaded operations refused because their id is not their content hash (CORE-1). */
|
|
2799
|
+
private invalidOperationIds;
|
|
1253
2800
|
private readonly validateOperation;
|
|
1254
2801
|
private readonly koraContext;
|
|
2802
|
+
private readonly blobAccess;
|
|
1255
2803
|
constructor(options: ClientSessionOptions);
|
|
1256
2804
|
/**
|
|
1257
2805
|
* Start handling messages from the client transport.
|
|
@@ -1263,9 +2811,13 @@ declare class ClientSession {
|
|
|
1263
2811
|
*/
|
|
1264
2812
|
relayOperations(operations: Operation[]): void;
|
|
1265
2813
|
/**
|
|
1266
|
-
* Push newly-available in-scope operations to a delivery-watermark client,
|
|
1267
|
-
* from the
|
|
1268
|
-
*
|
|
2814
|
+
* Push newly-available in-scope operations to a delivery-watermark client, chained
|
|
2815
|
+
* from the send cursor (SRV-3): only what was not sent yet goes out. `rewind`
|
|
2816
|
+
* first moves the cursor back to the acknowledged position (a retransmission), and
|
|
2817
|
+
* `initial` is the handshake stream (it always ends in a final batch, even an empty
|
|
2818
|
+
* one, so the client completes initial sync). Pushes are serialized so their batches
|
|
2819
|
+
* never interleave (which would break the base/max chain); wake-ups that arrive
|
|
2820
|
+
* while one is queued coalesce into it.
|
|
1269
2821
|
*/
|
|
1270
2822
|
private pushDeliveryStream;
|
|
1271
2823
|
private relayVisibleOperations;
|
|
@@ -1275,18 +2827,63 @@ declare class ClientSession {
|
|
|
1275
2827
|
* Retransmit relay batches this client has not acknowledged within `staleMs`.
|
|
1276
2828
|
* Called on a periodic tick by the server (and directly by tests). Redelivering an
|
|
1277
2829
|
* already-applied op is harmless: the client dedups by content-addressed id.
|
|
2830
|
+
*
|
|
2831
|
+
* For a delivery-watermark client it re-sends an outstanding (sent, unacknowledged)
|
|
2832
|
+
* delivery from the acknowledged position, under the same backed-off window as the
|
|
2833
|
+
* retransmit timer; `staleMs` 0 re-sends at once (a deterministic trigger for tests).
|
|
1278
2834
|
*/
|
|
1279
2835
|
retransmitPendingRelays(staleMs?: number): void;
|
|
1280
2836
|
/**
|
|
1281
2837
|
* Wake the durable delivery stream for clients that negotiated delivery
|
|
1282
|
-
* watermarks
|
|
1283
|
-
*
|
|
1284
|
-
*
|
|
2838
|
+
* watermarks: for operations appended by another server/store instance (sent from
|
|
2839
|
+
* the send cursor), and for a delivery that went unacknowledged (re-sent from the
|
|
2840
|
+
* acknowledged position once it made no progress for `staleMs`, backed off while it
|
|
2841
|
+
* stays stuck). `staleMs` 0 re-sends at once.
|
|
2842
|
+
*
|
|
2843
|
+
* @param staleMs - Base stale window before an unacknowledged delivery is re-sent
|
|
2844
|
+
* @param options - `serverFrontier`: the store's max delivery sequence, when known;
|
|
2845
|
+
* `trackStall`: emit `sync:delivery-stalled` after repeated re-sends
|
|
1285
2846
|
*/
|
|
1286
2847
|
pushDeliveryStreamIfSupported(staleMs?: number, options?: {
|
|
1287
2848
|
trackStall?: boolean;
|
|
1288
2849
|
serverFrontier?: number;
|
|
1289
2850
|
}): void;
|
|
2851
|
+
/** Book-keeping for one sent delivery batch ending at `max`. */
|
|
2852
|
+
private recordDeliverySent;
|
|
2853
|
+
/**
|
|
2854
|
+
* A delivery acknowledgment: advance the confirmed watermark (a retransmission
|
|
2855
|
+
* rewinds to it), take a round-trip sample, and reset the backoff. The watermark
|
|
2856
|
+
* never passes what was sent: a client cannot acknowledge what it never received.
|
|
2857
|
+
*/
|
|
2858
|
+
private noteDeliveryAcked;
|
|
2859
|
+
/** Fold one round-trip sample into the retransmit timeout (RFC 6298). */
|
|
2860
|
+
private recordRoundTrip;
|
|
2861
|
+
/**
|
|
2862
|
+
* The current retransmit window: the timeout, which each re-send doubled (up to
|
|
2863
|
+
* five minutes) and which only a fresh round-trip sample brings back down.
|
|
2864
|
+
*/
|
|
2865
|
+
private retransmitWindowMs;
|
|
2866
|
+
/**
|
|
2867
|
+
* (Re)arm the retransmit timer for the outstanding delivery: it fires once no
|
|
2868
|
+
* acknowledgment progress was made for the retransmit window.
|
|
2869
|
+
*/
|
|
2870
|
+
private armRetransmitTimer;
|
|
2871
|
+
/**
|
|
2872
|
+
* No acknowledgment progress for the retransmit window: re-send from the
|
|
2873
|
+
* acknowledged position (the batch was dropped, or the client could not apply it),
|
|
2874
|
+
* unless the socket is still draining what was sent.
|
|
2875
|
+
*/
|
|
2876
|
+
private onRetransmitTimeout;
|
|
2877
|
+
/** Re-send the outstanding delivery from the acknowledged position. */
|
|
2878
|
+
private rewindDelivery;
|
|
2879
|
+
/** Bytes queued on the transport and not yet written to the network (0 if unknown). */
|
|
2880
|
+
private transportBufferedAmount;
|
|
2881
|
+
/**
|
|
2882
|
+
* Wait until the transport's queued bytes drop to the high-water mark (SRV-5
|
|
2883
|
+
* backpressure), so a slow client never makes the server buffer the whole stream
|
|
2884
|
+
* in socket memory. Resolves early when the session closes.
|
|
2885
|
+
*/
|
|
2886
|
+
private waitForSendWindow;
|
|
1290
2887
|
/**
|
|
1291
2888
|
* Close this session.
|
|
1292
2889
|
*/
|
|
@@ -1297,11 +2894,118 @@ declare class ClientSession {
|
|
|
1297
2894
|
*/
|
|
1298
2895
|
private flushOrphanedRelays;
|
|
1299
2896
|
close(reason?: string): void;
|
|
2897
|
+
/**
|
|
2898
|
+
* End this session if its verified principal matches a credential revocation
|
|
2899
|
+
* (AUTH-11): send a retriable `code` error so the client refreshes and
|
|
2900
|
+
* re-handshakes, then close. A session still authenticating remembers the
|
|
2901
|
+
* revocation and applies it as soon as its principal is known.
|
|
2902
|
+
*
|
|
2903
|
+
* @returns True when the session was closed by this call
|
|
2904
|
+
*/
|
|
2905
|
+
terminateIfMatches(filter: SessionRevocation, code: SessionTerminationCode): boolean;
|
|
2906
|
+
/**
|
|
2907
|
+
* Re-authenticate the credential that opened this session (RT-18). The session is
|
|
2908
|
+
* ended with a retriable `AUTH_REVOKED` (`AUTH_EXPIRED` once past its expiry) when
|
|
2909
|
+
* the provider now refuses the credential or resolves it to another principal, for
|
|
2910
|
+
* example after a revocation persisted by another server instance.
|
|
2911
|
+
*
|
|
2912
|
+
* @returns `'terminated'` when this call ended the session, `'error'` when the
|
|
2913
|
+
* provider threw (nothing is ended; the caller retries later), else `'valid'`
|
|
2914
|
+
*/
|
|
2915
|
+
revalidateCredential(): Promise<'valid' | 'terminated' | 'error'>;
|
|
2916
|
+
/**
|
|
2917
|
+
* Re-resolve this session's grant on request (`KoraSyncServer.refreshScopes`), for
|
|
2918
|
+
* the user named. A streaming session whose principal is that user re-authenticates
|
|
2919
|
+
* now and ends with a retriable `SCOPE_CHANGED` when its grant changed (or
|
|
2920
|
+
* `AUTH_REVOKED` when the credential is no longer accepted). A session still in its
|
|
2921
|
+
* handshake may have resolved its grant from membership read before the change, so
|
|
2922
|
+
* it re-checks as soon as it reaches streaming; a session whose principal is not
|
|
2923
|
+
* known yet does the same (the re-check is a no-op for a grant that did not change).
|
|
2924
|
+
*
|
|
2925
|
+
* @param userId - The user whose grant changed
|
|
2926
|
+
* @returns The revalidation outcome, `'deferred'` when the check waits for the
|
|
2927
|
+
* handshake, or `'skipped'` for another user's (or a closed) session
|
|
2928
|
+
*/
|
|
2929
|
+
refreshScopes(userId: string): Promise<'valid' | 'terminated' | 'error' | 'deferred' | 'skipped'>;
|
|
2930
|
+
/** Run a re-check deferred by {@link refreshScopes} once the session streams. */
|
|
2931
|
+
private runDeferredScopeRefresh;
|
|
2932
|
+
/**
|
|
2933
|
+
* True when `context` (a fresh authentication of this session's principal)
|
|
2934
|
+
* resolves to download or upload scopes other than the ones this session holds.
|
|
2935
|
+
* A grant that no longer resolves at all counts as changed.
|
|
2936
|
+
*/
|
|
2937
|
+
private scopesChangedFor;
|
|
2938
|
+
/**
|
|
2939
|
+
* End this session because an admin released its node id (RT-5). Retriable: a
|
|
2940
|
+
* client that still owns the node simply reconnects and claims it again.
|
|
2941
|
+
*/
|
|
2942
|
+
endForNodeRelease(): void;
|
|
2943
|
+
private terminate;
|
|
2944
|
+
/** Clear every timer this session owns (expiry, handshake deadline, heartbeat). */
|
|
2945
|
+
private clearSessionTimers;
|
|
2946
|
+
/**
|
|
2947
|
+
* Start the application-level heartbeat for a client that advertised support for it
|
|
2948
|
+
* (LMS #12): a `heartbeat` message whenever nothing else was sent for an interval,
|
|
2949
|
+
* so the client can detect a dead connection it cannot see (browsers never surface
|
|
2950
|
+
* WebSocket pings).
|
|
2951
|
+
*/
|
|
2952
|
+
private startAppHeartbeat;
|
|
2953
|
+
/**
|
|
2954
|
+
* Arm the expiry timer from `AuthContext.expiresAt`. Returns false when the
|
|
2955
|
+
* credential has already expired (the caller refuses the handshake).
|
|
2956
|
+
*/
|
|
2957
|
+
private armExpiryTimer;
|
|
2958
|
+
private clearExpiryTimer;
|
|
1300
2959
|
getState(): SessionState;
|
|
1301
2960
|
getSessionId(): string;
|
|
1302
2961
|
getClientNodeId(): string | null;
|
|
1303
2962
|
getAuthContext(): AuthContext | null;
|
|
2963
|
+
/**
|
|
2964
|
+
* The principal the auth provider verified at handshake, or null before (or
|
|
2965
|
+
* without) authentication. Unlike {@link getAuthContext}, never a context
|
|
2966
|
+
* synthesized for an unauthenticated session.
|
|
2967
|
+
*/
|
|
2968
|
+
getPrincipal(): AuthContext | null;
|
|
1304
2969
|
isStreaming(): boolean;
|
|
2970
|
+
/**
|
|
2971
|
+
* Ingest refusals that happen before any store work (RT-6): operations refused by
|
|
2972
|
+
* the per-minute rate limiter and batches refused for exceeding the per-batch cap.
|
|
2973
|
+
*/
|
|
2974
|
+
getIngestLimitCounts(): {
|
|
2975
|
+
rateLimitedOperations: number;
|
|
2976
|
+
rejectedBatches: number;
|
|
2977
|
+
rateLimitedBlobRequests: number;
|
|
2978
|
+
};
|
|
2979
|
+
/**
|
|
2980
|
+
* True when the client did not advertise the `sequenceReservation` capability
|
|
2981
|
+
* (RT-37): a legacy client (Kora <= beta.12) that may give two operations one
|
|
2982
|
+
* sequence number. Such a pair is stored and delivered, never refused with
|
|
2983
|
+
* SEQUENCE_CONFLICT. Meaningful once the handshake was accepted.
|
|
2984
|
+
*/
|
|
2985
|
+
isLegacySequenceClient(): boolean;
|
|
2986
|
+
/** Legacy duplicate (node, sequence) pairs this session stored (RT-37). */
|
|
2987
|
+
getLegacySequencePairCount(): number;
|
|
2988
|
+
/**
|
|
2989
|
+
* True when a stored record is inside this session's download scope, so a side
|
|
2990
|
+
* channel (for example a Yjs doc update) about it may be delivered here. A record
|
|
2991
|
+
* not stored yet is judged by its id alone.
|
|
2992
|
+
*/
|
|
2993
|
+
canReceiveRecord(collection: string, recordId: string, storedRecord: Record<string, unknown> | null): boolean;
|
|
2994
|
+
/** This session's download scope, or undefined when it is unscoped. */
|
|
2995
|
+
getDownlinkScopes(): ScopeMap | undefined;
|
|
2996
|
+
/**
|
|
2997
|
+
* A stable key for this session's download scope: sessions with the same key see
|
|
2998
|
+
* exactly the same data. Unscoped sessions share the key `"*"`. Cached per scope
|
|
2999
|
+
* map, since a large `$in` grant makes the key costly and the relays read it often.
|
|
3000
|
+
*/
|
|
3001
|
+
getScopePartitionKey(): string;
|
|
3002
|
+
/**
|
|
3003
|
+
* Partition for presence without a cursor (a "who is online" state that names no
|
|
3004
|
+
* record): only sessions with the same key see each other's. Signed-in sessions
|
|
3005
|
+
* use their download scope key; every anonymous session is its own partition,
|
|
3006
|
+
* because sessions holding the same anonymous grant are unrelated people.
|
|
3007
|
+
*/
|
|
3008
|
+
getPresencePartitionKey(): string;
|
|
1305
3009
|
/**
|
|
1306
3010
|
* Get the transport for this session.
|
|
1307
3011
|
* Used by the awareness relay to send messages to this client.
|
|
@@ -1312,50 +3016,371 @@ declare class ClientSession {
|
|
|
1312
3016
|
private sendToClient;
|
|
1313
3017
|
private enqueueMessage;
|
|
1314
3018
|
private handleMessageAsync;
|
|
3019
|
+
/**
|
|
3020
|
+
* The owner of this session's encryption key records (ENC-1), or null when it may
|
|
3021
|
+
* hold none: the authenticated user, or on a server without auth the one shared
|
|
3022
|
+
* anonymous owner. An anonymous principal of a mixed provider has no stable
|
|
3023
|
+
* identity, so it holds no keys.
|
|
3024
|
+
*/
|
|
3025
|
+
getEncryptionKeyOwner(): string | null;
|
|
3026
|
+
/** Push a key record another session of the same owner wrote (rotation, new passphrase). */
|
|
3027
|
+
pushEncryptionKeyRecord(keyring: string, record: WrappedKeyRecord): void;
|
|
3028
|
+
private handleEncryptionKeyMessage;
|
|
1315
3029
|
/**
|
|
1316
3030
|
* Persist a client-uploaded blob chunk (or manifest) centrally. The bytes are
|
|
1317
3031
|
* verified to hash to the declared hash before storing, so a corrupt or
|
|
1318
3032
|
* mislabeled upload is rejected rather than served later as trusted content.
|
|
1319
3033
|
*/
|
|
1320
|
-
private handleBlobChunkPush;
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
3034
|
+
private handleBlobChunkPush;
|
|
3035
|
+
/**
|
|
3036
|
+
* Claim (or re-claim) a node id for an anonymous device (RT-12, RT-21).
|
|
3037
|
+
*
|
|
3038
|
+
* A device presenting a token holds the node when the claim is (or can become)
|
|
3039
|
+
* `kora:anon-node:<hash(token)>`: its confirmed claim, a fresh claim, or its own
|
|
3040
|
+
* provisional claim, which presenting the token confirms.
|
|
3041
|
+
*
|
|
3042
|
+
* A device presenting no token gets a fresh token and a PROVISIONAL claim, which it
|
|
3043
|
+
* confirms by acknowledging the response with the token once saved (or by
|
|
3044
|
+
* presenting it next time). Until then the claim may be re-issued to a device that
|
|
3045
|
+
* again presents no token (the response was lost in transit), but only while no
|
|
3046
|
+
* session is connected as that node and before the claim expires. Legacy claims
|
|
3047
|
+
* (expired provisional ones, and nodes of the pre-release shared anonymous owner)
|
|
3048
|
+
* are re-issued only with `allowLegacyAnonymousClaims`, with a deprecation warning.
|
|
3049
|
+
*/
|
|
3050
|
+
private claimAnonymousNode;
|
|
3051
|
+
/**
|
|
3052
|
+
* Automatic device handover after an upgrade from beta.12 (F1). A node with
|
|
3053
|
+
* history but no owner (written before node claims existed) or released by an
|
|
3054
|
+
* admin is claimed for the signed-in user when the node id IS the device id the
|
|
3055
|
+
* auth provider verified for this credential (`metadata.deviceId`; the built-in
|
|
3056
|
+
* provider takes it from the token's `dev` claim, which names a device registered
|
|
3057
|
+
* to that user). `@korajs/auth` clients use that device id as their node id and
|
|
3058
|
+
* cannot change it, so without this they stay refused until an operator binds the
|
|
3059
|
+
* node. The claim is one atomic store step; a node another principal owns is never
|
|
3060
|
+
* taken, and another user presenting the node id is still refused.
|
|
3061
|
+
*
|
|
3062
|
+
* The claim decides ownership from now on only. History written under the node on
|
|
3063
|
+
* beta.12 may include operations another user forged (beta.12 did not verify node
|
|
3064
|
+
* ids); the operation log integrity scan is the tool for auditing it.
|
|
3065
|
+
*/
|
|
3066
|
+
private handOverDeviceNode;
|
|
3067
|
+
/**
|
|
3068
|
+
* The download grant the server itself gives this session, before the client's
|
|
3069
|
+
* handshake scope narrows it (F4). A client may narrow its own view, but that does
|
|
3070
|
+
* not isolate tenants: another client of the same user set can ask for everything.
|
|
3071
|
+
* `null` when the grant cannot be resolved here (the handshake already refused or
|
|
3072
|
+
* accepted it on the full path); then nothing is judged.
|
|
3073
|
+
*/
|
|
3074
|
+
private grantWithoutHandshakeNarrowing;
|
|
3075
|
+
/** Who holds a refused node id, as `NODE_ID_CLAIMED` reports it (F9). */
|
|
3076
|
+
private nodeOwnershipOf;
|
|
3077
|
+
/** The deprecation warning for an anonymous device adopted under a legacy claim (RT-21, RT-91). */
|
|
3078
|
+
private warnLegacyAnonymousClaim;
|
|
3079
|
+
/**
|
|
3080
|
+
* The device proved it saved the node token issued by this handshake: make the
|
|
3081
|
+
* provisional claim permanent (RT-21). A mismatched token is ignored.
|
|
3082
|
+
*/
|
|
3083
|
+
private confirmNodeClaim;
|
|
3084
|
+
/**
|
|
3085
|
+
* The download scope references are authorized against (RT-11, RT-13), or
|
|
3086
|
+
* undefined when they are not checked: without a real auth provider there is no
|
|
3087
|
+
* tenant boundary to protect.
|
|
3088
|
+
*/
|
|
3089
|
+
private referenceScopes;
|
|
3090
|
+
/**
|
|
3091
|
+
* Stable blob-ownership key of this session's principal: the user id, or for an
|
|
3092
|
+
* anonymous device its node-claim owner (never shared by two anonymous devices).
|
|
3093
|
+
*/
|
|
3094
|
+
getBlobOwnerKey(): string;
|
|
3095
|
+
/**
|
|
3096
|
+
* Partition key for peer-to-peer blob forwarding. Sessions with the same download
|
|
3097
|
+
* scope share a tenant view, except anonymous devices: every anonymous session has
|
|
3098
|
+
* the same grant but they are unrelated people, so each is its own partition.
|
|
3099
|
+
*/
|
|
3100
|
+
getBlobPartitionKey(): string;
|
|
3101
|
+
/** Authorize foreign-key targets and blob references of an untrusted write. */
|
|
3102
|
+
private authorizeReferences;
|
|
3103
|
+
private handleMessageFailure;
|
|
3104
|
+
private handleHandshake;
|
|
3105
|
+
/**
|
|
3106
|
+
* The nodes whose operations after `fromDeliverySeq` this scoped session will
|
|
3107
|
+
* receive, for the handshake vector (RT-7). Scans the delivery log without holding
|
|
3108
|
+
* it (memory is O(nodes)), and judges each node only until one of its operations is
|
|
3109
|
+
* visible, so most operations cost one scope comparison and no record read.
|
|
3110
|
+
*/
|
|
3111
|
+
private collectVisibleNodeIds;
|
|
3112
|
+
/**
|
|
3113
|
+
* The version vector as committed in the store (SRV-4). A store shared by several
|
|
3114
|
+
* instances reads it fresh; others serve their own (complete) vector.
|
|
3115
|
+
*/
|
|
3116
|
+
private readServerVector;
|
|
3117
|
+
/**
|
|
3118
|
+
* Resolve this session's download and upload scopes from an auth context and the
|
|
3119
|
+
* scope the client asked for at handshake. Side-effect free, so revalidation can
|
|
3120
|
+
* recompute it and compare (RT-26). The legacy `scopes` contract remains shorthand
|
|
3121
|
+
* for both directions.
|
|
3122
|
+
*/
|
|
3123
|
+
private computeSessionScopes;
|
|
3124
|
+
private handleOperationBatch;
|
|
3125
|
+
/**
|
|
3126
|
+
* For each delete of this session's node in an upload batch, the batch's later
|
|
3127
|
+
* operations of the same node that name it as a causal parent: candidates for the
|
|
3128
|
+
* author's own copies of its cascades and set-nulls (RT-69). Indexed by the delete's
|
|
3129
|
+
* id, in the server's schema (field names as the server derives effects).
|
|
3130
|
+
*/
|
|
3131
|
+
private indexAuthoredSideEffectCopies;
|
|
3132
|
+
/**
|
|
3133
|
+
* The referential effects of a delete that arrived again as a stored duplicate and
|
|
3134
|
+
* are still undone (RT-73): effects the author covers later in this batch are
|
|
3135
|
+
* deferred like a fresh delete's (derived after the batch unless a copy is stored);
|
|
3136
|
+
* the rest are derived now. Derived ids are deterministic, so a concurrent retry or
|
|
3137
|
+
* another instance stores the same operations.
|
|
3138
|
+
*
|
|
3139
|
+
* @returns The derived operations (to relay)
|
|
3140
|
+
*/
|
|
3141
|
+
private resumeStoredDeleteEffects;
|
|
3142
|
+
/**
|
|
3143
|
+
* Derive the server's copy of each deferred side effect unless one of the author's
|
|
3144
|
+
* copies is stored (applied or already held). A store with the batch lookup is asked
|
|
3145
|
+
* again, so a copy stored by a concurrent session counts too.
|
|
3146
|
+
*/
|
|
3147
|
+
private deriveUncoveredSideEffects;
|
|
3148
|
+
/** {@link ServerStore.findStoredOperations} by id; empty without it or on a failed read. */
|
|
3149
|
+
private findStoredOperationIds;
|
|
3150
|
+
/**
|
|
3151
|
+
* The operation as the server schema reads it (transforms at fold time, RT-84): the
|
|
3152
|
+
* view authorization, validators, constraint checks and scope filters judge, and the
|
|
3153
|
+
* one the server stores fold (core `operationSchemaView`, same transforms). Never
|
|
3154
|
+
* stored: the store keeps the operation as uploaded. An envelope operation is opaque
|
|
3155
|
+
* to the server (NEW-ENC-1) and judged as is; devices transform after decryption.
|
|
3156
|
+
*/
|
|
3157
|
+
private schemaView;
|
|
3158
|
+
/**
|
|
3159
|
+
* Integrity checks on an uploaded operation before anything else judges it:
|
|
3160
|
+
* - a version-2 plaintext id must be the content hash (`INVALID_OPERATION_ID`);
|
|
3161
|
+
* - an unknown declared hash version is refused the same way;
|
|
3162
|
+
* - with required encryption, a data-bearing plaintext operation is refused
|
|
3163
|
+
* (`PLAINTEXT_REJECTED`) unless the plaintext migration window is open.
|
|
3164
|
+
*
|
|
3165
|
+
* @returns null when the operation passes, or the refusal
|
|
3166
|
+
*/
|
|
3167
|
+
private checkUploadIntegrity;
|
|
3168
|
+
/**
|
|
3169
|
+
* Whether an undeclared version-1 id that does not verify is accepted unverified
|
|
3170
|
+
* (RT-71): only from a protocol-1 session (Kora <= beta.12), and only for the
|
|
3171
|
+
* session's own node.
|
|
3172
|
+
*/
|
|
3173
|
+
private acceptsUnverifiedLegacyId;
|
|
3174
|
+
/** Log, count and emit an operation accepted with an unverified legacy id (RT-71). */
|
|
3175
|
+
private reportUnverifiedLegacyOperation;
|
|
3176
|
+
/** Operations this session accepted with an unverified legacy id (RT-71). */
|
|
3177
|
+
getUnverifiedLegacyOperationCount(): number;
|
|
3178
|
+
/**
|
|
3179
|
+
* The operation as the server stores it: an undeclared plaintext id this server
|
|
3180
|
+
* verified declares the version it matched (1, or 2 when the declaration was lost),
|
|
3181
|
+
* so receivers verify it too (RT-64). An absent version is left only on ids nobody
|
|
3182
|
+
* could verify (protocol-1 encrypted payloads, server-side transforms).
|
|
3183
|
+
*/
|
|
3184
|
+
private declareVerifiedHashVersion;
|
|
3185
|
+
/**
|
|
3186
|
+
* True for a node id that is (or was) the server's own: the `kora:` namespace, the
|
|
3187
|
+
* store's node id, and every id the server advertises or folds as authoritative
|
|
3188
|
+
* (other instances, legacy server node ids, configured extras). No device may use one
|
|
3189
|
+
* (RT-61), whether or not it has history yet.
|
|
3190
|
+
*/
|
|
3191
|
+
private isServerAuthorNodeId;
|
|
3192
|
+
/**
|
|
3193
|
+
* The authoritative ids a handshake advertises (RT-75): only explicit ones (legacy
|
|
3194
|
+
* server node ids, configured extras). Every `kora:server:` id is authoritative by
|
|
3195
|
+
* prefix on every replica, and listing instance ids would make the list differ per
|
|
3196
|
+
* instance and per start, which devices would have to treat as news.
|
|
3197
|
+
*/
|
|
3198
|
+
/** The handshake's explicit revocations (RT-81), when the deployment has any. */
|
|
3199
|
+
private advertisedRevocations;
|
|
3200
|
+
private advertisedAuthoritativeNodeIds;
|
|
3201
|
+
/** Node ids this server authors operations under (protocol v2 handshake response). */
|
|
3202
|
+
private serverAuthoritativeNodeIds;
|
|
3203
|
+
/**
|
|
3204
|
+
* A protocol-1 client (Kora <= beta.12) is served for one release (beta.13) with a
|
|
3205
|
+
* deprecation warning: its operation ids are version-1 hashes and its encrypted
|
|
3206
|
+
* payloads have no envelope binding.
|
|
3207
|
+
*/
|
|
3208
|
+
private warnLegacyProtocol;
|
|
3209
|
+
/** The in-scope operations a version-vector client is missing (not yet sent). */
|
|
3210
|
+
private collectDeltaOperations;
|
|
3211
|
+
/** Send a collected version-vector delta in paginated, cursor-carrying batches. */
|
|
3212
|
+
private sendCollectedDelta;
|
|
1326
3213
|
/**
|
|
1327
|
-
*
|
|
3214
|
+
* Stream the gap-free server->client delivery stream, resuming just after
|
|
1328
3215
|
* `fromDeliverySeq`. Operations are scanned in server delivery-sequence order
|
|
1329
3216
|
* (commit order), scope-filtered for this session, and sent in batches that chain:
|
|
1330
3217
|
* each batch's `baseDeliverySequence` equals the previous batch's
|
|
1331
3218
|
* `maxDeliverySequence` (the first batch bases on `fromDeliverySeq`). The client
|
|
1332
3219
|
* applies a batch only when its watermark equals the base and advances the
|
|
1333
3220
|
* watermark to the max, so a dropped batch stalls the watermark and is recovered by
|
|
1334
|
-
* the
|
|
1335
|
-
* (a dependency is always committed, and thus sequenced, before its
|
|
1336
|
-
* topological sort is needed
|
|
1337
|
-
*
|
|
3221
|
+
* a re-send from the acknowledged position. Because delivery-sequence order respects
|
|
3222
|
+
* causal order (a dependency is always committed, and thus sequenced, before its
|
|
3223
|
+
* dependent), no topological sort is needed.
|
|
3224
|
+
*
|
|
3225
|
+
* Batches go out as each scan chunk is filtered (SRV-5): the server holds at most
|
|
3226
|
+
* one scan chunk plus two batches per stream, never the whole backlog, and the
|
|
3227
|
+
* first batch reaches the client after one chunk. One full batch is held back so
|
|
3228
|
+
* the stream's last batch can be marked `isFinal` (one-batch lookahead). The final
|
|
3229
|
+
* batch carries the highest sequence scanned, not merely the last in-scope one, so
|
|
3230
|
+
* an out-of-scope tail is not re-scanned. Every sent batch advances the send cursor
|
|
3231
|
+
* ({@link lastSentDeliverySeq}). Between batches the stream waits for the client to
|
|
3232
|
+
* drain its socket (backpressure).
|
|
1338
3233
|
*
|
|
1339
|
-
* The
|
|
1340
|
-
*
|
|
1341
|
-
*
|
|
3234
|
+
* @param fromDeliverySeq - The chain base: the client's watermark or the send cursor
|
|
3235
|
+
* @param finalizeWhenEmpty - Send an empty final batch even when nothing new was
|
|
3236
|
+
* scanned (the handshake stream, so the client completes initial sync)
|
|
3237
|
+
*/
|
|
3238
|
+
private streamDelivery;
|
|
3239
|
+
/**
|
|
3240
|
+
* Filter one scan chunk of the delivery log down to what this session receives:
|
|
3241
|
+
* visible operations (each preceded by its scope-entry insert, RT-19) and
|
|
3242
|
+
* retractions. The records the decisions need are fetched for the whole chunk at
|
|
3243
|
+
* once (LMS #11), one batched read per collection instead of one query per op.
|
|
1342
3244
|
*/
|
|
1343
|
-
private
|
|
3245
|
+
private filterDeliveryChunk;
|
|
3246
|
+
/**
|
|
3247
|
+
* Read, in one batched query per collection, every record the visibility decisions
|
|
3248
|
+
* for these operations may look up, when the store supports batched reads. Returns
|
|
3249
|
+
* null (per-operation lookups) otherwise. Over-fetching is harmless: the chunk
|
|
3250
|
+
* bounds it.
|
|
3251
|
+
*/
|
|
3252
|
+
private prefetchRecordsFor;
|
|
1344
3253
|
/** Build, track, and send one chained delivery-stream batch. */
|
|
1345
3254
|
private sendDeliveryBatch;
|
|
3255
|
+
/** The scope snapshots of these operations, when the store keeps them (RT-14). */
|
|
3256
|
+
private scopeSnapshotsFor;
|
|
3257
|
+
/**
|
|
3258
|
+
* Download visibility of one operation. With a scope snapshot (RT-14) the
|
|
3259
|
+
* operation is judged on the record's scope values right after it was applied, so
|
|
3260
|
+
* history written while a record belonged to someone else stays hidden after an
|
|
3261
|
+
* ownership transfer, and a previous owner still receives its own history. Query
|
|
3262
|
+
* subsets (a client-chosen narrowing, not an authorization) keep using the current
|
|
3263
|
+
* row. Operations without a snapshot (legacy rows, custom stores) fall back to the
|
|
3264
|
+
* current row plus `op.data`.
|
|
3265
|
+
*/
|
|
1346
3266
|
private operationVisibleToClient;
|
|
1347
|
-
/**
|
|
1348
|
-
|
|
3267
|
+
/**
|
|
3268
|
+
* Where the store already holds each of `operations` (by id), in one store read.
|
|
3269
|
+
* Null when the store cannot answer by id (a custom store without
|
|
3270
|
+
* `findStoredOperations`, or the read failed): callers then fall back to a
|
|
3271
|
+
* per-operation (node, sequence) lookup and the store's own dedup at apply.
|
|
3272
|
+
*/
|
|
3273
|
+
private findStoredOperations;
|
|
3274
|
+
/**
|
|
3275
|
+
* True when the batch lookup alone shows `op` is already stored and may be
|
|
3276
|
+
* acknowledged as a duplicate with no further store work (RT-31, RT-39): this
|
|
3277
|
+
* device's own operation stored under its node (its sequence repair may have
|
|
3278
|
+
* renumbered it, keeping the id), or any other operation stored under exactly its
|
|
3279
|
+
* (node, sequence, id), such as one the server itself delivered and the client
|
|
3280
|
+
* echoed back. Never true for a store without the batch lookup.
|
|
3281
|
+
*/
|
|
3282
|
+
private isStoredDuplicate;
|
|
3283
|
+
/**
|
|
3284
|
+
* The operation the store holds under `op`'s id (at the node and sequence the batch
|
|
3285
|
+
* lookup found), or null when it cannot be read (the upload is then judged like any
|
|
3286
|
+
* other: verified, validated, charged, and deduplicated by the store at apply).
|
|
3287
|
+
*/
|
|
3288
|
+
private loadStoredOperation;
|
|
3289
|
+
/**
|
|
3290
|
+
* Refuse an upload that reuses a stored operation's id with different content (RT-77):
|
|
3291
|
+
* terminal, nothing recorded or derived, logged and emitted as tampering. An honest
|
|
3292
|
+
* client never does this (ids are content hashes), so the rejection only reaches a
|
|
3293
|
+
* forger. No resolution is recorded: the id belongs to the stored operation.
|
|
3294
|
+
*/
|
|
3295
|
+
private refuseForgedDuplicate;
|
|
3296
|
+
/** Uploads refused for reusing a stored id with other content (RT-77). */
|
|
3297
|
+
getForgedDuplicateCount(): number;
|
|
3298
|
+
/**
|
|
3299
|
+
* The recorded resolutions of the batch's own-node operations that the lookup did
|
|
3300
|
+
* not find stored (RT-43, RT-47). Asked only for this session's own node, so the
|
|
3301
|
+
* answer never reveals another device's (or tenant's) refusals.
|
|
3302
|
+
*/
|
|
3303
|
+
private findResolutions;
|
|
3304
|
+
/** The highest resolved sequence of `nodeId` (0 without the store method or on error). */
|
|
3305
|
+
private resolvedThrough;
|
|
3306
|
+
/**
|
|
3307
|
+
* Durably record how this session's own operation was resolved without being stored
|
|
3308
|
+
* under its sequence. Awaited before the answer is sent: a failure propagates (the
|
|
3309
|
+
* batch fails and is not acknowledged), so the client never acts on a resolution the
|
|
3310
|
+
* server could forget.
|
|
3311
|
+
*/
|
|
3312
|
+
private recordResolution;
|
|
3313
|
+
/**
|
|
3314
|
+
* Refuse an operation for good (non-retriable): remember the refusal (RT-47), then
|
|
3315
|
+
* tell the client. A resubmission of the same id is answered with this rejection
|
|
3316
|
+
* without being judged again.
|
|
3317
|
+
*/
|
|
3318
|
+
private refuseTerminally;
|
|
3319
|
+
/**
|
|
3320
|
+
* An own operation acknowledged as a duplicate of the copy stored under ANOTHER
|
|
3321
|
+
* sequence (the client's sequence repair renumbered it, keeping its id): record the
|
|
3322
|
+
* submitted sequence as resolved, so the handshake does not read it as lost (RT-43).
|
|
3323
|
+
*/
|
|
3324
|
+
private noteStoredElsewhere;
|
|
3325
|
+
/** Refuse the rest of a batch for the per-minute ingest budget (retriable). */
|
|
3326
|
+
private sendRateLimited;
|
|
3327
|
+
/**
|
|
3328
|
+
* True when the store already holds exactly this operation (same node, sequence and
|
|
3329
|
+
* id). Answered from the batch lookup when the store supports it.
|
|
3330
|
+
*/
|
|
3331
|
+
private isStoredOperation;
|
|
3332
|
+
/**
|
|
3333
|
+
* The scopes this session may write under. Fails closed: with auth configured but
|
|
3334
|
+
* no accepted authentication, the session may write nothing (an empty scope map),
|
|
3335
|
+
* never everything.
|
|
3336
|
+
*/
|
|
3337
|
+
private uplinkScopes;
|
|
3338
|
+
/**
|
|
3339
|
+
* Upload authorization, independent from the client's downloaded/query view. The
|
|
3340
|
+
* stored row (including a soft-deleted one) is always loaded when scopes apply and
|
|
3341
|
+
* is authoritative; client-supplied previousData is never consulted.
|
|
3342
|
+
*/
|
|
3343
|
+
private authorizeClientOperation;
|
|
3344
|
+
/**
|
|
3345
|
+
* True when this update moved the record out of the session's scope, judged on the
|
|
3346
|
+
* store's own before/after values (RT-15), never on the writer's previousData. An
|
|
3347
|
+
* operation without a snapshot never produces a retraction (fail quiet: the
|
|
3348
|
+
* client keeps what it already had, and no other tenant's record id leaks).
|
|
3349
|
+
*/
|
|
1349
3350
|
private scopeRetractionFor;
|
|
3351
|
+
/**
|
|
3352
|
+
* The scope-entry operation to send before `op` when `op` moved an existing record
|
|
3353
|
+
* into this session's download scope (RT-19), or null. Judged on the store's own
|
|
3354
|
+
* pre/post snapshot; built from the record's CURRENT row, and only while that row
|
|
3355
|
+
* is live and still inside the scope (a record that has since left again, or was
|
|
3356
|
+
* deleted, gets no entry: its later operations retract or delete it anyway).
|
|
3357
|
+
* Operations without a snapshot (legacy rows, custom stores) never produce one.
|
|
3358
|
+
*/
|
|
3359
|
+
private scopeEntryFor;
|
|
1350
3360
|
/**
|
|
1351
3361
|
* Read a record's current field values from the materialized store for scope /
|
|
1352
3362
|
* query-subset backfill. Includes soft-deleted rows so a relayed delete (whose op
|
|
1353
3363
|
* carries no fields) is still judged against the record's actual scope. Returns
|
|
1354
3364
|
* undefined when the record cannot be read (never materialized, or no schema).
|
|
3365
|
+
* `rows` are the rows one delivery pass prefetched for its own decisions.
|
|
1355
3366
|
*/
|
|
1356
3367
|
private lookupRecordFields;
|
|
3368
|
+
/**
|
|
3369
|
+
* Hand an awareness update to the server's relay, with the record its cursor
|
|
3370
|
+
* names, read from the store, so the relay can deliver it only to sessions whose
|
|
3371
|
+
* download scope contains that record (the rule of the Yjs doc channel).
|
|
3372
|
+
* Awareness is purely ephemeral: nothing is persisted.
|
|
3373
|
+
*/
|
|
1357
3374
|
private handleAwarenessUpdate;
|
|
3375
|
+
/**
|
|
3376
|
+
* A Yjs doc-channel update is a write to a richtext field: the sender must be
|
|
3377
|
+
* allowed to write the stored record (same rule as operation uploads). The stored
|
|
3378
|
+
* row is handed to the relay so delivery can be limited to sessions whose download
|
|
3379
|
+
* scope contains the record.
|
|
3380
|
+
*/
|
|
1358
3381
|
private handleYjsDocUpdate;
|
|
3382
|
+
/** A legacy client's duplicate pair was stored (RT-37): count it and log it. */
|
|
3383
|
+
private recordLegacySequencePair;
|
|
1359
3384
|
private sendError;
|
|
1360
3385
|
/**
|
|
1361
3386
|
* Reject one specific client operation, tied to its id, so the submitter can
|
|
@@ -1364,10 +3389,57 @@ declare class ClientSession {
|
|
|
1364
3389
|
* replica ever sees it.
|
|
1365
3390
|
*/
|
|
1366
3391
|
private sendOperationRejected;
|
|
1367
|
-
private setSerializerWireFormat;
|
|
1368
3392
|
private handleTransportClose;
|
|
1369
3393
|
}
|
|
1370
3394
|
|
|
3395
|
+
/** {@link findJsonStringValues} was given a page or sample size it cannot use. */
|
|
3396
|
+
declare class InvalidDiagnosticOptionsError extends KoraError {
|
|
3397
|
+
constructor(option: string, value: unknown);
|
|
3398
|
+
}
|
|
3399
|
+
/** Rows of one json/object field whose stored value is a string that itself holds JSON. */
|
|
3400
|
+
interface JsonStringValueReport {
|
|
3401
|
+
collection: string;
|
|
3402
|
+
field: string;
|
|
3403
|
+
/** How many live rows hold such a string. */
|
|
3404
|
+
count: number;
|
|
3405
|
+
/** Ids of the first such rows, to inspect. */
|
|
3406
|
+
sampleIds: string[];
|
|
3407
|
+
}
|
|
3408
|
+
/** Options for {@link findJsonStringValues}. */
|
|
3409
|
+
interface FindJsonStringValuesOptions {
|
|
3410
|
+
/** Ids reported per field. Defaults to 10. */
|
|
3411
|
+
sampleSize?: number;
|
|
3412
|
+
/** Rows read per page. Defaults to 1,000. */
|
|
3413
|
+
pageSize?: number;
|
|
3414
|
+
}
|
|
3415
|
+
/**
|
|
3416
|
+
* Find `t.json()` / `t.object()` fields whose stored value is a JSON-encoded string
|
|
3417
|
+
* (`'{"a":1}'` instead of `{ a: 1 }`), after an upgrade from a server older than
|
|
3418
|
+
* 1.0.0-beta.13 (F14).
|
|
3419
|
+
*
|
|
3420
|
+
* Servers before beta.13 decoded one JSON-string layer of such values when they built
|
|
3421
|
+
* rows; since beta.13 the rows hold exactly what the operations wrote. A client that
|
|
3422
|
+
* wrote a json field as a string (or double-encoded it) therefore reads back a string
|
|
3423
|
+
* where the old server returned an object. Kora does not rewrite these values: a string
|
|
3424
|
+
* is also a legitimate json value, so no rule can tell a real string from an encoded
|
|
3425
|
+
* object. Fix them with server-authored updates (`server.kora.apply`) once you know
|
|
3426
|
+
* which shape your app expects.
|
|
3427
|
+
*
|
|
3428
|
+
* @param store - The server store, with its schema set
|
|
3429
|
+
* @param options - Sample and page sizes
|
|
3430
|
+
* @returns One report per field that holds at least one such value
|
|
3431
|
+
* @throws {InvalidDiagnosticOptionsError} When `pageSize` is not a positive integer or
|
|
3432
|
+
* `sampleSize` not a non-negative one
|
|
3433
|
+
*
|
|
3434
|
+
* @example
|
|
3435
|
+
* ```typescript
|
|
3436
|
+
* for (const report of await findJsonStringValues(store)) {
|
|
3437
|
+
* console.warn(`${report.collection}.${report.field}: ${report.count} rows`, report.sampleIds)
|
|
3438
|
+
* }
|
|
3439
|
+
* ```
|
|
3440
|
+
*/
|
|
3441
|
+
declare function findJsonStringValues(store: ServerStore, options?: FindJsonStringValuesOptions): Promise<JsonStringValueReport[]>;
|
|
3442
|
+
|
|
1371
3443
|
/**
|
|
1372
3444
|
* Configuration for the production server that serves both
|
|
1373
3445
|
* static files and WebSocket sync on a single port.
|
|
@@ -1406,6 +3478,32 @@ interface ProductionServerConfig {
|
|
|
1406
3478
|
* backupToken.
|
|
1407
3479
|
*/
|
|
1408
3480
|
operationalAuth?: ProductionOperationalAuth;
|
|
3481
|
+
/**
|
|
3482
|
+
* Reverse proxies trusted to report the client address in `X-Forwarded-For`,
|
|
3483
|
+
* which becomes `request.ip` (the key `@korajs/auth` rate-limits sign-in by).
|
|
3484
|
+
* A hop count (`1` for a single load balancer in front) or a list of trusted
|
|
3485
|
+
* proxy IPs / CIDR ranges. When unset, the header is ignored and `request.ip`
|
|
3486
|
+
* is the socket address, so a client cannot pick its own rate-limit bucket.
|
|
3487
|
+
*/
|
|
3488
|
+
trustProxy?: TrustProxySetting;
|
|
3489
|
+
/**
|
|
3490
|
+
* Largest request body a custom HTTP route (`httpRoutes`) accepts, in bytes. A
|
|
3491
|
+
* larger body is refused with 413 before it is buffered (SRV-6), so an
|
|
3492
|
+
* unauthenticated client cannot make the server hold arbitrary amounts of memory.
|
|
3493
|
+
* Defaults to 1 MiB.
|
|
3494
|
+
*/
|
|
3495
|
+
maxRequestBodyBytes?: number;
|
|
3496
|
+
/**
|
|
3497
|
+
* Largest backup accepted by `/__kora/backup/import`, in bytes. Defaults to 256 MiB.
|
|
3498
|
+
*/
|
|
3499
|
+
maxBackupBytes?: number;
|
|
3500
|
+
/**
|
|
3501
|
+
* Which requests for a missing path get the app shell (`index.html`). `'navigation'`
|
|
3502
|
+
* (default): browser navigations only, so a missing API path is a real 404.
|
|
3503
|
+
* `'extensionless'`: also any path without a file extension, for a service worker
|
|
3504
|
+
* that warms app routes with a plain `fetch(url)`. Never under `/assets/`.
|
|
3505
|
+
*/
|
|
3506
|
+
spaFallback?: 'navigation' | 'extensionless';
|
|
1409
3507
|
}
|
|
1410
3508
|
interface ProductionOperationalAuth {
|
|
1411
3509
|
/** Protects /__kora, /__kora/status, and /__kora/events. */
|
|
@@ -1414,6 +3512,14 @@ interface ProductionOperationalAuth {
|
|
|
1414
3512
|
metricsToken?: string;
|
|
1415
3513
|
/** Protects /__kora/backup/*. Falls back to adminToken when omitted. */
|
|
1416
3514
|
backupToken?: string;
|
|
3515
|
+
/**
|
|
3516
|
+
* With `NODE_ENV=production`, an endpoint group whose token is unset is disabled
|
|
3517
|
+
* (`403 OPERATIONAL_ENDPOINT_DISABLED`): backups export or replace every user's data,
|
|
3518
|
+
* and the dashboard lists connected devices. Set this to `true` to serve such groups
|
|
3519
|
+
* without a token anyway (for example behind a network you control). Outside
|
|
3520
|
+
* production, groups without a token stay public, with a startup warning.
|
|
3521
|
+
*/
|
|
3522
|
+
allowPublic?: boolean;
|
|
1417
3523
|
}
|
|
1418
3524
|
interface ProductionHttpRouteRequest {
|
|
1419
3525
|
method: string;
|
|
@@ -1444,7 +3550,10 @@ interface ProductionHttpRoute {
|
|
|
1444
3550
|
* A production server handle returned by createProductionServer.
|
|
1445
3551
|
*/
|
|
1446
3552
|
interface ProductionServer {
|
|
1447
|
-
/**
|
|
3553
|
+
/**
|
|
3554
|
+
* Start listening. Returns the URL the server is available at, with the port it
|
|
3555
|
+
* actually bound (pass `port: 0` to let the OS pick a free one).
|
|
3556
|
+
*/
|
|
1448
3557
|
start(): Promise<string>;
|
|
1449
3558
|
/** Stop the server gracefully. */
|
|
1450
3559
|
stop(): Promise<void>;
|
|
@@ -1471,6 +3580,25 @@ interface ProductionServer {
|
|
|
1471
3580
|
* @returns Every live blob reference the server can currently see.
|
|
1472
3581
|
*/
|
|
1473
3582
|
getLiveBlobRefs(): Promise<BlobRef[]>;
|
|
3583
|
+
/**
|
|
3584
|
+
* Re-resolve one user's grant on their live sync sessions now. Call it after a
|
|
3585
|
+
* membership change (an invitation accepted, a collaborator removed): a session
|
|
3586
|
+
* whose scope changed ends with a retriable `SCOPE_CHANGED` and its client
|
|
3587
|
+
* reconnects with the new grant, applying its `scopeExit` policy. See
|
|
3588
|
+
* `KoraSyncServer.refreshScopes`.
|
|
3589
|
+
*
|
|
3590
|
+
* @param userId - The user whose grant changed
|
|
3591
|
+
* @returns The number of sessions ended
|
|
3592
|
+
*/
|
|
3593
|
+
refreshScopes(userId: string): Promise<number>;
|
|
3594
|
+
/**
|
|
3595
|
+
* Re-validate every live sync session's credential and grant now (the pass that
|
|
3596
|
+
* otherwise runs every `sessionRevalidationIntervalMs`). See
|
|
3597
|
+
* `KoraSyncServer.revalidateSessions`.
|
|
3598
|
+
*
|
|
3599
|
+
* @returns The number of sessions ended
|
|
3600
|
+
*/
|
|
3601
|
+
revalidateSessions(): Promise<number>;
|
|
1474
3602
|
}
|
|
1475
3603
|
/**
|
|
1476
3604
|
* Creates a production server that serves both static files and WebSocket sync
|
|
@@ -1496,6 +3624,10 @@ interface TokenAuthProviderOptions {
|
|
|
1496
3624
|
/**
|
|
1497
3625
|
* Validate a token and return an AuthContext if valid, or null if rejected.
|
|
1498
3626
|
* This is where you implement your auth logic (JWT verification, database lookup, etc.).
|
|
3627
|
+
*
|
|
3628
|
+
* Return `scopes` to grant sync access explicitly. When the returned context
|
|
3629
|
+
* has no scopes, schema-scoped collections are bound to `{ userId }` and any
|
|
3630
|
+
* collection scoped by another key is denied.
|
|
1499
3631
|
*/
|
|
1500
3632
|
validate: (token: string) => Promise<AuthContext | null>;
|
|
1501
3633
|
}
|
|
@@ -1528,11 +3660,13 @@ interface TokenValidator {
|
|
|
1528
3660
|
sub: string;
|
|
1529
3661
|
dev: string;
|
|
1530
3662
|
type: string;
|
|
3663
|
+
exp?: number;
|
|
1531
3664
|
} | null;
|
|
1532
3665
|
validateTokenWithRevocation?(token: string): Promise<{
|
|
1533
3666
|
sub: string;
|
|
1534
3667
|
dev: string;
|
|
1535
3668
|
type: string;
|
|
3669
|
+
exp?: number;
|
|
1536
3670
|
} | null>;
|
|
1537
3671
|
}
|
|
1538
3672
|
/**
|
|
@@ -1575,8 +3709,15 @@ interface KoraAuthProviderOptions {
|
|
|
1575
3709
|
*/
|
|
1576
3710
|
deviceTracker?: DeviceToucher;
|
|
1577
3711
|
/**
|
|
1578
|
-
* Optional scope resolver. Called with the user ID to determine
|
|
1579
|
-
* which collections/records the user can sync.
|
|
3712
|
+
* Optional scope resolver. Called with the verified user ID to determine
|
|
3713
|
+
* which collections/records the user can sync. Its result is the complete
|
|
3714
|
+
* server grant: a collection it omits is not visible, and the client
|
|
3715
|
+
* handshake can only narrow it. Use `claimScopes({ userId, orgId })` from
|
|
3716
|
+
* `@korajs/core` to bind every schema-scoped collection from verified values.
|
|
3717
|
+
*
|
|
3718
|
+
* When omitted, the grant binds every schema-scoped collection from
|
|
3719
|
+
* `{ userId: <verified sub> }`; collections scoped by any other key are denied
|
|
3720
|
+
* until you supply it.
|
|
1580
3721
|
*/
|
|
1581
3722
|
resolveScopes?: (userId: string) => Promise<Record<string, Record<string, unknown>>>;
|
|
1582
3723
|
}
|
|
@@ -1630,7 +3771,9 @@ interface MixedAuthProviderOptions {
|
|
|
1630
3771
|
*/
|
|
1631
3772
|
primary: AuthProvider;
|
|
1632
3773
|
/**
|
|
1633
|
-
* Scopes to apply to anonymous connections.
|
|
3774
|
+
* Scopes to apply to anonymous connections. This is the complete anonymous
|
|
3775
|
+
* grant: anonymous sessions can sync exactly these collections, and the
|
|
3776
|
+
* client handshake can only narrow them, never add one.
|
|
1634
3777
|
* Each key is a collection name; the value is a filter object
|
|
1635
3778
|
* (use `{}` for unrestricted access to that collection).
|
|
1636
3779
|
*
|
|
@@ -1654,10 +3797,10 @@ interface MixedAuthProviderOptions {
|
|
|
1654
3797
|
/**
|
|
1655
3798
|
* Auth provider that supports both authenticated and anonymous connections.
|
|
1656
3799
|
*
|
|
1657
|
-
* When a client connects with a
|
|
1658
|
-
*
|
|
1659
|
-
* (
|
|
1660
|
-
*
|
|
3800
|
+
* When a client connects with a token, the primary auth provider decides:
|
|
3801
|
+
* a valid token authenticates normally and an invalid, expired or revoked one
|
|
3802
|
+
* is rejected (the client refreshes and reconnects). Only a client that sends
|
|
3803
|
+
* no token at all is accepted as anonymous, with exactly `anonymousScopes`.
|
|
1661
3804
|
*
|
|
1662
3805
|
* This is the recommended pattern for apps that need public data access
|
|
1663
3806
|
* alongside authenticated users — for example, a form builder where
|
|
@@ -1698,9 +3841,24 @@ declare class MixedAuthProvider implements AuthProvider {
|
|
|
1698
3841
|
private readonly anonymousPrefix;
|
|
1699
3842
|
private anonymousCounter;
|
|
1700
3843
|
constructor(options: MixedAuthProviderOptions);
|
|
1701
|
-
|
|
3844
|
+
/** The primary provider's revocation feed, when it has one. */
|
|
3845
|
+
onRevoke?: AuthProvider['onRevoke'];
|
|
3846
|
+
authenticate(token: string): Promise<AuthContext | null>;
|
|
1702
3847
|
}
|
|
1703
3848
|
|
|
3849
|
+
/** Options for {@link MemoryServerStore}. */
|
|
3850
|
+
interface MemoryServerStoreOptions {
|
|
3851
|
+
/**
|
|
3852
|
+
* Node ids, besides this store's own, whose operations win
|
|
3853
|
+
* `merge('server-authoritative')` fields (legacy server node ids). Every
|
|
3854
|
+
* `kora:server:` node id is authoritative without being listed.
|
|
3855
|
+
*/
|
|
3856
|
+
authoritativeNodeIds?: string[];
|
|
3857
|
+
/** Revoked explicit authorities (see `ServerIdentityOptions.revokedAuthoritativeNodeIds`). */
|
|
3858
|
+
revokedAuthoritativeNodeIds?: string[];
|
|
3859
|
+
/** Instance id within the deployment (see `ServerIdentityOptions.instanceId`). */
|
|
3860
|
+
instanceId?: string;
|
|
3861
|
+
}
|
|
1704
3862
|
/**
|
|
1705
3863
|
* In-memory server store for testing and quick prototyping.
|
|
1706
3864
|
* Not suitable for production — data does not survive process restart.
|
|
@@ -1712,6 +3870,13 @@ declare class MemoryServerStore implements ServerStore {
|
|
|
1712
3870
|
private readonly nodeId;
|
|
1713
3871
|
private readonly operations;
|
|
1714
3872
|
private readonly operationIndex;
|
|
3873
|
+
/** (nodeId, sequenceNumber) -> the id of the operation holding it (W3 step 4). */
|
|
3874
|
+
/** (node, sequence) -> ids holding it, in store order (several only for a legacy pair). */
|
|
3875
|
+
private readonly operationIdsBySequence;
|
|
3876
|
+
/** Node -> sequences held by more than one operation (legacy pairs, RT-48). */
|
|
3877
|
+
private readonly pairSequencesByNode;
|
|
3878
|
+
/** Operation id -> how it was resolved without being stored (RT-43, RT-47). */
|
|
3879
|
+
private readonly resolutions;
|
|
1715
3880
|
private readonly versionVector;
|
|
1716
3881
|
/**
|
|
1717
3882
|
* Server-assigned delivery sequence per operation id. Single-process, so a plain
|
|
@@ -1722,24 +3887,118 @@ declare class MemoryServerStore implements ServerStore {
|
|
|
1722
3887
|
private readonly deliverySeqByOpId;
|
|
1723
3888
|
private deliverySeqCounter;
|
|
1724
3889
|
private schema;
|
|
3890
|
+
/** Node id -> the authenticated principal that claimed it (see claimNode). */
|
|
3891
|
+
private readonly nodeOwners;
|
|
3892
|
+
/** Operation id -> scope snapshot captured at apply time (RT-14). */
|
|
3893
|
+
private readonly scopeSnapshots;
|
|
3894
|
+
/** Fields the current snapshots were captured with (RT-20). */
|
|
3895
|
+
private snapshotFingerprint;
|
|
3896
|
+
/** Blob content hash -> owners that pushed or first claimed it (RT-11). */
|
|
3897
|
+
private readonly blobOwners;
|
|
3898
|
+
/** Wrapped encryption key records (ENC-1): owner + keyring -> record JSON and revision. */
|
|
3899
|
+
private readonly encryptionKeyRecords;
|
|
1725
3900
|
/** Materialized records: collection -> recordId -> record data */
|
|
1726
3901
|
private readonly materializedRecords;
|
|
3902
|
+
/** Fold state per materialized record (W7): rows are projected from it. */
|
|
3903
|
+
private readonly foldStates;
|
|
3904
|
+
/** Every operation of a record, in store (delivery) order. */
|
|
3905
|
+
private readonly recordOps;
|
|
3906
|
+
private readonly authoritativeNodeIds;
|
|
3907
|
+
/** Legacy and configured authorities (the `kora:server:` prefix needs no listing). */
|
|
3908
|
+
private readonly explicitAuthorities;
|
|
3909
|
+
/** Explicit authority over time: revoked and ever-held ids (RT-81). */
|
|
3910
|
+
private readonly authorityHistory;
|
|
3911
|
+
private foldOptions;
|
|
3912
|
+
/** Schema transforms the fold applies (transforms at fold time, RT-84). */
|
|
3913
|
+
private operationTransforms;
|
|
1727
3914
|
private closed;
|
|
1728
|
-
|
|
3915
|
+
/**
|
|
3916
|
+
* Sequence-enforcement epoch (see SEQUENCE_ENFORCEMENT_EPOCH_KEY). A memory store
|
|
3917
|
+
* starts empty under this release, so 0: every holder was stored under enforcement.
|
|
3918
|
+
* A replace-mode backup import moves it to the restored log's end.
|
|
3919
|
+
*/
|
|
3920
|
+
private sequenceEpoch;
|
|
3921
|
+
/** Derivation secret of server-derived ids (RT-64). Per process: nothing persists. */
|
|
3922
|
+
private readonly derivationSecret;
|
|
3923
|
+
/**
|
|
3924
|
+
* @param nodeId - Deprecated: a plain id is recorded as a legacy authoritative id; a
|
|
3925
|
+
* `kora:server:` id is used verbatim. Leave unset: the store authors under
|
|
3926
|
+
* `kora:server:<deployment>:<instance>` (a fresh deployment per process, since a
|
|
3927
|
+
* memory store persists nothing).
|
|
3928
|
+
* @param options - Extra authoritative ids and the instance id
|
|
3929
|
+
*/
|
|
3930
|
+
constructor(nodeId?: string, options?: MemoryServerStoreOptions);
|
|
3931
|
+
deriveServerOperationId(parentOpId: string, ruleId: string, targetRecordId: string): Promise<string>;
|
|
3932
|
+
getAuthoritativeNodeIds(): string[];
|
|
3933
|
+
/** Explicit authoritative ids the deployment revoked (RT-81), advertised at handshake. */
|
|
3934
|
+
getRevokedAuthoritativeNodeIds(): string[];
|
|
3935
|
+
/**
|
|
3936
|
+
* Every explicit id the deployment ever held authoritative, revoked ones included
|
|
3937
|
+
* (RT-81): never accepted as a device node id.
|
|
3938
|
+
*/
|
|
3939
|
+
getEverAuthoritativeNodeIds(): string[];
|
|
1729
3940
|
getVersionVector(): VersionVector;
|
|
1730
3941
|
getNodeId(): string;
|
|
1731
3942
|
getSchema(): SchemaDefinition | null;
|
|
1732
|
-
setSchema(schema: SchemaDefinition): Promise<void>;
|
|
1733
|
-
|
|
3943
|
+
setSchema(schema: SchemaDefinition, options?: ServerSchemaOptions): Promise<void>;
|
|
3944
|
+
getOperationTransforms(): readonly OperationTransform[];
|
|
3945
|
+
setOperationTransforms(transforms: readonly OperationTransform[]): Promise<void>;
|
|
3946
|
+
applyRemoteOperation(op: Operation, options?: ApplyRemoteOptions): Promise<ApplyResult>;
|
|
3947
|
+
private addSequenceHolder;
|
|
3948
|
+
getSequencePairOperations(nodeId: string, throughSequence: number): Promise<Operation[]>;
|
|
3949
|
+
recordOperationResolution(resolution: OperationResolution): Promise<void>;
|
|
3950
|
+
findOperationResolutions(nodeId: string, ids: string[]): Promise<Map<string, OperationResolution>>;
|
|
3951
|
+
deleteOperationResolution(nodeId: string, operationId: string): Promise<void>;
|
|
3952
|
+
getResolvedThrough(nodeId: string): Promise<number>;
|
|
3953
|
+
findStoredOperations(ids: string[]): Promise<Map<string, StoredOperationKey>>;
|
|
3954
|
+
getOperationScopeSnapshots(operationIds: string[]): Promise<Map<string, OperationScopeSnapshot>>;
|
|
3955
|
+
getRecordLatestTimestamp(collection: string, recordId: string): Promise<HLCTimestamp | null>;
|
|
3956
|
+
getRecordFieldVersions(collection: string, recordId: string): Promise<RecordFieldVersions | null>;
|
|
3957
|
+
getRecordFoldState(collection: string, recordId: string): Promise<FoldState | null>;
|
|
3958
|
+
getRecordOperations(collection: string, recordId: string): Promise<Operation[]>;
|
|
3959
|
+
previewOperation(op: Operation): Promise<MaterializedRecord | null>;
|
|
3960
|
+
/**
|
|
3961
|
+
* The record's fold state: the stored one for a materialized collection, else
|
|
3962
|
+
* folded from the record's operations (a collection outside the schema).
|
|
3963
|
+
*/
|
|
3964
|
+
private readFoldState;
|
|
3965
|
+
private addRecordOp;
|
|
3966
|
+
recordBlobOwner(hash: string, owner: string): Promise<void>;
|
|
3967
|
+
getBlobOwners(hashes: string[]): Promise<Map<string, string[]>>;
|
|
3968
|
+
claimBlobIfUnowned(hash: string, owner: string): Promise<boolean>;
|
|
3969
|
+
/** Scope values of a live record before a write; null when absent or deleted. */
|
|
3970
|
+
private scopeValuesBefore;
|
|
3971
|
+
/**
|
|
3972
|
+
* Rebuild missing scope snapshots from the log (migration from a store without
|
|
3973
|
+
* them, or a replace-mode backup import), replaying each record in commit order.
|
|
3974
|
+
*/
|
|
3975
|
+
private backfillScopeSnapshots;
|
|
1734
3976
|
getOperationRange(nodeId: string, fromSeq: number, toSeq: number): Promise<Operation[]>;
|
|
1735
3977
|
getOperationCount(): Promise<number>;
|
|
1736
3978
|
getMaxDeliverySequence(): Promise<number>;
|
|
1737
3979
|
getOperationsAfterDelivery(afterDeliverySequence: number, limit: number): Promise<DeliveredOperation[]>;
|
|
1738
3980
|
materializeCollection(collection: string): Promise<MaterializedRecord[]>;
|
|
3981
|
+
getNodeIdsAfterDelivery(afterDeliverySequence: number): Promise<string[]>;
|
|
3982
|
+
findRecordsByIds(collection: string, ids: string[]): Promise<Map<string, MaterializedRecord>>;
|
|
1739
3983
|
queryCollection(collection: string, options?: CollectionQueryOptions): Promise<MaterializedRecord[]>;
|
|
1740
3984
|
findRecord(collection: string, id: string): Promise<MaterializedRecord | null>;
|
|
1741
3985
|
countCollection(collection: string, where?: Record<string, unknown>): Promise<number>;
|
|
1742
3986
|
close(): Promise<void>;
|
|
3987
|
+
getEncryptionKeyRecord(owner: string, keyring: string): Promise<string | null>;
|
|
3988
|
+
putEncryptionKeyRecord(owner: string, keyring: string, record: string, revision: number, expectedRevision: number): Promise<boolean>;
|
|
3989
|
+
listEncryptionKeyRecords(owner?: string): Promise<EncryptionKeyRecordRow[]>;
|
|
3990
|
+
getEncryptedKeyIds(nodeOwner: string | null, limit: number): Promise<string[]>;
|
|
3991
|
+
claimNode(nodeId: string, userId: string): Promise<boolean>;
|
|
3992
|
+
claimUnownedNode(nodeId: string, userId: string): Promise<boolean>;
|
|
3993
|
+
getNodeClaimOwner(nodeId: string): Promise<string | null>;
|
|
3994
|
+
replaceNodeClaim(nodeId: string, expectedOwner: string, newOwner: string): Promise<boolean>;
|
|
3995
|
+
releaseNodeClaim(nodeId: string): Promise<boolean>;
|
|
3996
|
+
/**
|
|
3997
|
+
* The record as currently stored, including a soft-deleted one (whose last field
|
|
3998
|
+
* values are kept), or null when it was never written. Without a materialized
|
|
3999
|
+
* table, the last known values are replayed from the operation log.
|
|
4000
|
+
*/
|
|
4001
|
+
private readStoredRow;
|
|
1743
4002
|
/**
|
|
1744
4003
|
* Wipes all in-memory state. For tests and E2E isolation only.
|
|
1745
4004
|
*/
|
|
@@ -1753,7 +4012,16 @@ declare class MemoryServerStore implements ServerStore {
|
|
|
1753
4012
|
* Get all stored operations (for test assertions).
|
|
1754
4013
|
*/
|
|
1755
4014
|
getAllOperations(): Operation[];
|
|
1756
|
-
|
|
4015
|
+
/**
|
|
4016
|
+
* Merge one just-appended operation into its record's fold state and project the
|
|
4017
|
+
* row: O(fields the operation touches), independent of the record's history
|
|
4018
|
+
* (SRV-7). Falls back to a full re-fold only when the stored state cannot take the
|
|
4019
|
+
* operation (a field's fold kind changed with the schema).
|
|
4020
|
+
*/
|
|
4021
|
+
private mergeIntoRecord;
|
|
4022
|
+
/** Persist a record's fold state and project it onto its materialized row. */
|
|
4023
|
+
private writeRecord;
|
|
4024
|
+
/** Re-fold every record of every materialized collection (schema set, backup restore). */
|
|
1757
4025
|
private backfillAllCollections;
|
|
1758
4026
|
private materializeFromOps;
|
|
1759
4027
|
private assertOpen;
|
|
@@ -1761,6 +4029,123 @@ declare class MemoryServerStore implements ServerStore {
|
|
|
1761
4029
|
private assertCollection;
|
|
1762
4030
|
}
|
|
1763
4031
|
|
|
4032
|
+
/**
|
|
4033
|
+
* Server materialization through the one deterministic fold (W7 Stage B2).
|
|
4034
|
+
*
|
|
4035
|
+
* Every server store (memory, SQLite, Postgres) keeps, per materialized record, the
|
|
4036
|
+
* record's serialized {@link FoldState} next to its collection row. Applying an
|
|
4037
|
+
* operation is "append, then `mergeOp`": O(fields the operation touches), never a
|
|
4038
|
+
* replay of the record's history (SRV-7). The collection row is a pure projection of
|
|
4039
|
+
* the fold state ({@link projectFoldState}), so the server's rows are exactly what a
|
|
4040
|
+
* client that holds the same operations materializes (SRV-1).
|
|
4041
|
+
*
|
|
4042
|
+
* The fold is pure JavaScript: ordering uses HLC stamps compared in JS, never a
|
|
4043
|
+
* database collation (the Phase 1 COLLATE "C" concern disappears for materialization).
|
|
4044
|
+
*
|
|
4045
|
+
* Old helpers (`replayOperationsForRecord`, `mergeArraySet`) are no longer used to
|
|
4046
|
+
* materialize; they stay exported from core for legacy comparison only.
|
|
4047
|
+
*/
|
|
4048
|
+
|
|
4049
|
+
/** What a store's startup re-materialization did (W7 step 7). */
|
|
4050
|
+
interface FoldMigrationReport {
|
|
4051
|
+
/** False when no schema was set (nothing is materialized). */
|
|
4052
|
+
ran: boolean;
|
|
4053
|
+
/** Records re-folded and rewritten. */
|
|
4054
|
+
records: number;
|
|
4055
|
+
/**
|
|
4056
|
+
* Records that own quarantined operations (see `ServerLogIntegrityReport`): their log
|
|
4057
|
+
* is incomplete, so their pre-fold rows were kept as a snapshot base with the
|
|
4058
|
+
* remaining operations folded onto it (RT-70), never re-folded from the log alone.
|
|
4059
|
+
*/
|
|
4060
|
+
skippedUnclean: number;
|
|
4061
|
+
/** True when the fold plan (field kinds, strategies, resolvers) changed: every record was re-folded. */
|
|
4062
|
+
fullRefold: boolean;
|
|
4063
|
+
}
|
|
4064
|
+
|
|
4065
|
+
/** A row the scan quarantined. */
|
|
4066
|
+
interface ServerLogIntegrityRow {
|
|
4067
|
+
operationId: string;
|
|
4068
|
+
problem: string;
|
|
4069
|
+
detail: string;
|
|
4070
|
+
}
|
|
4071
|
+
/** Result of the server scan. */
|
|
4072
|
+
interface ServerLogIntegrityReport {
|
|
4073
|
+
/** Rows checked by this scan (0 when it already ran on this database). */
|
|
4074
|
+
checkedRows: number;
|
|
4075
|
+
/** Rows this scan moved to the quarantine table. */
|
|
4076
|
+
quarantined: ServerLogIntegrityRow[];
|
|
4077
|
+
/** False when the one-time scan had already run (only the quarantine is reported). */
|
|
4078
|
+
ran: boolean;
|
|
4079
|
+
/** Every row in the quarantine table. */
|
|
4080
|
+
totalQuarantined: number;
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
/**
|
|
4084
|
+
* Stable server identity (RT-61, RT-62, RT-64).
|
|
4085
|
+
*
|
|
4086
|
+
* Every operation the server authors (route writes, cascades, set-nulls, constraint
|
|
4087
|
+
* corrections) is authored by a node id in the reserved namespace
|
|
4088
|
+
* `kora:server:<deploymentId>:<instanceId>`:
|
|
4089
|
+
*
|
|
4090
|
+
* - `deploymentId` is generated once per database and persisted in `kora_server_meta`,
|
|
4091
|
+
* so every instance that shares the database (and every restart) belongs to one
|
|
4092
|
+
* deployment.
|
|
4093
|
+
* - `instanceId` is distinct per running instance, so two instances never author the
|
|
4094
|
+
* same `(nodeId, sequenceNumber)`: SQLite persists one (a SQLite database has one
|
|
4095
|
+
* writer process), Postgres allocates a fresh one from a counter in the database on
|
|
4096
|
+
* every start unless the operator configures a stable `instanceId` per instance.
|
|
4097
|
+
*
|
|
4098
|
+
* Authority (`merge('server-authoritative')`) is the prefix rule: any operation whose
|
|
4099
|
+
* node id starts with {@link SERVER_NODE_PREFIX} is authoritative, on the server and on
|
|
4100
|
+
* every device. Devices can never use a `kora:` node id (the session refuses it at
|
|
4101
|
+
* handshake). Server node ids written before this release (random per-process ids, or
|
|
4102
|
+
* an id the operator configured) are recorded once, at the first start of this release,
|
|
4103
|
+
* in a persisted list of legacy authoritative ids that the handshake keeps advertising,
|
|
4104
|
+
* so earlier server decisions keep winning everywhere.
|
|
4105
|
+
*
|
|
4106
|
+
* Server-derived operation ids (cascades, corrections) are keyed with a persisted,
|
|
4107
|
+
* deployment-wide secret (HMAC-SHA-256), so a writer cannot predict them and occupy one
|
|
4108
|
+
* first; every instance of the deployment derives the same id for the same effect.
|
|
4109
|
+
*/
|
|
4110
|
+
|
|
4111
|
+
/** Prefix of every node id the server authors under. */
|
|
4112
|
+
declare const SERVER_NODE_PREFIX = "kora:server:";
|
|
4113
|
+
/** Thrown for an invalid server identity configuration. */
|
|
4114
|
+
declare class ServerIdentityError extends KoraError {
|
|
4115
|
+
constructor(message: string, context?: Record<string, unknown>);
|
|
4116
|
+
}
|
|
4117
|
+
/** Identity options every server store accepts. */
|
|
4118
|
+
interface ServerIdentityOptions {
|
|
4119
|
+
/**
|
|
4120
|
+
* Deprecated. A plain id (for example one set before beta.13) is no longer the id the
|
|
4121
|
+
* server authors under: it is recorded as a legacy authoritative id, so operations
|
|
4122
|
+
* already stored under it keep their authority, and no device may use it. An id in
|
|
4123
|
+
* the `kora:server:` namespace is used verbatim (it must then be unique per instance).
|
|
4124
|
+
*/
|
|
4125
|
+
nodeId?: string;
|
|
4126
|
+
/**
|
|
4127
|
+
* Stable id of this instance within the deployment (`[A-Za-z0-9._-]`, at most 64
|
|
4128
|
+
* characters). Must be distinct per running instance. SQLite persists one on its own;
|
|
4129
|
+
* Postgres allocates a new one per start when it is not configured.
|
|
4130
|
+
*/
|
|
4131
|
+
instanceId?: string;
|
|
4132
|
+
/**
|
|
4133
|
+
* Extra node ids whose operations win `merge('server-authoritative')` fields. Every id
|
|
4134
|
+
* listed here once stays authoritative (devices keep it too) until it is revoked with
|
|
4135
|
+
* {@link revokedAuthoritativeNodeIds}: removing it from this list does not revoke it.
|
|
4136
|
+
*/
|
|
4137
|
+
authoritativeNodeIds?: string[];
|
|
4138
|
+
/**
|
|
4139
|
+
* Explicit authoritative node ids to revoke (RT-81): they stop being authoritative on
|
|
4140
|
+
* the server, the handshake tells every device to drop them (devices re-fold), and they
|
|
4141
|
+
* are still never accepted as device node ids. Revocation is permanent (persisted); an
|
|
4142
|
+
* id may not be both listed in `authoritativeNodeIds` and revoked.
|
|
4143
|
+
*/
|
|
4144
|
+
revokedAuthoritativeNodeIds?: string[];
|
|
4145
|
+
}
|
|
4146
|
+
/** True for a node id the server authors under (current or any instance of any deployment). */
|
|
4147
|
+
declare function isServerNodeId(nodeId: string): boolean;
|
|
4148
|
+
|
|
1764
4149
|
/**
|
|
1765
4150
|
* PostgreSQL-backed server store using Drizzle ORM.
|
|
1766
4151
|
* All reads and writes go through Drizzle's typed query builder.
|
|
@@ -1769,7 +4154,18 @@ declare class MemoryServerStore implements ServerStore {
|
|
|
1769
4154
|
* collection tables for efficient indexed queries (dual-write).
|
|
1770
4155
|
*/
|
|
1771
4156
|
declare class PostgresServerStore implements ServerStore {
|
|
1772
|
-
|
|
4157
|
+
/** This instance's server node id; loaded from the database during startup (RT-62). */
|
|
4158
|
+
private loadedNodeId;
|
|
4159
|
+
private readonly configuredIdentity;
|
|
4160
|
+
/** Derivation secret of server-derived ids, shared by the deployment (RT-64). */
|
|
4161
|
+
private derivationSecret;
|
|
4162
|
+
/** Legacy and configured authorities (the `kora:server:` prefix needs no listing). */
|
|
4163
|
+
private explicitAuthorities;
|
|
4164
|
+
/** Explicit authority over time: revoked and ever-held ids (RT-81). */
|
|
4165
|
+
private authorityHistory;
|
|
4166
|
+
/** Other `kora:server:` nodes with stored operations (advertised for older clients). */
|
|
4167
|
+
/** Records owning quarantined operations: folded onto their kept rows (RT-70). */
|
|
4168
|
+
private quarantine;
|
|
1773
4169
|
private readonly db;
|
|
1774
4170
|
private readonly versionVector;
|
|
1775
4171
|
private readonly ready;
|
|
@@ -1783,6 +4179,9 @@ declare class PostgresServerStore implements ServerStore {
|
|
|
1783
4179
|
* null after a backup import so it re-seeds from the restored version vector.
|
|
1784
4180
|
*/
|
|
1785
4181
|
private sequenceCounter;
|
|
4182
|
+
/** See {@link SEQUENCE_ENFORCEMENT_EPOCH_KEY}; fixed per database at first start. */
|
|
4183
|
+
private sequenceEpoch;
|
|
4184
|
+
private logIntegrity;
|
|
1786
4185
|
/**
|
|
1787
4186
|
* Time source for HLC timestamps on server-originated operations. Injectable so a
|
|
1788
4187
|
* test can freeze wall-clock time and prove the conditional-apply ordering holds
|
|
@@ -1790,7 +4189,54 @@ declare class PostgresServerStore implements ServerStore {
|
|
|
1790
4189
|
* exist to make correct). Defaults to the system clock in production.
|
|
1791
4190
|
*/
|
|
1792
4191
|
private readonly timeSource;
|
|
1793
|
-
|
|
4192
|
+
private foldOptions;
|
|
4193
|
+
/** Schema transforms the fold applies (transforms at fold time, RT-84). */
|
|
4194
|
+
private operationTransforms;
|
|
4195
|
+
private foldMigration;
|
|
4196
|
+
/**
|
|
4197
|
+
* @param db - The Drizzle database
|
|
4198
|
+
* @param nodeId - Deprecated: a plain id is recorded as a legacy authoritative id; a
|
|
4199
|
+
* `kora:server:` id is used verbatim (it must then be unique per instance). Leave
|
|
4200
|
+
* unset: the store authors under `kora:server:<deployment>:<instance>`, with the
|
|
4201
|
+
* deployment persisted in the database and a distinct instance per process (RT-62).
|
|
4202
|
+
* @param timeSource - Clock for server-originated operations (tests)
|
|
4203
|
+
* @param options - Extra authoritative ids and a stable instance id
|
|
4204
|
+
*/
|
|
4205
|
+
constructor(db: PostgresJsDatabase, nodeId?: string, timeSource?: TimeSource, options?: Omit<ServerIdentityOptions, 'nodeId'>);
|
|
4206
|
+
/** Resolves once the store's tables and identity are loaded. */
|
|
4207
|
+
whenReady(): Promise<void>;
|
|
4208
|
+
/**
|
|
4209
|
+
* This store's node id and the legacy and configured ids (explicit authorities).
|
|
4210
|
+
* Other `kora:server:` instances are not listed: the prefix rule makes every one of
|
|
4211
|
+
* them authoritative, and a list that grew by one instance id per start would make
|
|
4212
|
+
* every device re-fold after each deploy (RT-75). Handshakes advertise only the
|
|
4213
|
+
* explicit (non-prefixed) ids.
|
|
4214
|
+
*/
|
|
4215
|
+
getAuthoritativeNodeIds(): string[];
|
|
4216
|
+
/** Legacy and configured authorities (beside the `kora:server:` namespace). */
|
|
4217
|
+
getLegacyAuthoritativeNodeIds(): string[];
|
|
4218
|
+
/** Explicit authoritative ids the deployment revoked (RT-81), advertised at handshake. */
|
|
4219
|
+
getRevokedAuthoritativeNodeIds(): string[];
|
|
4220
|
+
/**
|
|
4221
|
+
* Every explicit id the deployment ever held authoritative, revoked ones included
|
|
4222
|
+
* (RT-81): never accepted as a device node id.
|
|
4223
|
+
*/
|
|
4224
|
+
getEverAuthoritativeNodeIds(): string[];
|
|
4225
|
+
deriveServerOperationId(parentOpId: string, ruleId: string, targetRecordId: string): Promise<string>;
|
|
4226
|
+
private get nodeId();
|
|
4227
|
+
/** Remember another `kora:server:` node that authored stored operations. */
|
|
4228
|
+
/**
|
|
4229
|
+
* Load (creating on first start) the persisted server identity under an advisory
|
|
4230
|
+
* lock: the deployment id and derivation secret (one per database, shared by every
|
|
4231
|
+
* instance), a fresh instance id from the database counter (unless configured), and
|
|
4232
|
+
* the legacy authoritative ids. At the first start of this release, every node id
|
|
4233
|
+
* whose operations hold an authority class in a stored fold state (a server decision
|
|
4234
|
+
* folded under an earlier, per-process server node id) is recorded as a legacy
|
|
4235
|
+
* authority, before any re-fold could drop that class.
|
|
4236
|
+
*/
|
|
4237
|
+
private loadIdentity;
|
|
4238
|
+
/** What the last startup re-materialization did (W7 step 7). */
|
|
4239
|
+
getFoldMigrationReport(): FoldMigrationReport;
|
|
1794
4240
|
getVersionVector(): VersionVector;
|
|
1795
4241
|
/**
|
|
1796
4242
|
* Atomically reserve this node's next server-operation sequence number. The
|
|
@@ -1800,8 +4246,51 @@ declare class PostgresServerStore implements ServerStore {
|
|
|
1800
4246
|
reserveSequenceNumber(): number;
|
|
1801
4247
|
getNodeId(): string;
|
|
1802
4248
|
getSchema(): SchemaDefinition | null;
|
|
1803
|
-
|
|
1804
|
-
|
|
4249
|
+
/**
|
|
4250
|
+
* One-time migration (RT-101): drop the enum `CHECK` constraints (and any `NOT NULL`
|
|
4251
|
+
* on a schema field) that beta.12 and earlier DDL put on collection tables, in one
|
|
4252
|
+
* transaction. A check is dropped when it parses as an enum check (one column against
|
|
4253
|
+
* string literals, in every form Postgres normalizes `IN (...)` to, the single-value
|
|
4254
|
+
* `col = 'x'` included, RT-108) on an enum field of the schema, or as the multi-value
|
|
4255
|
+
* `= ANY (ARRAY[...])` shape on another non-internal column; checks added by hand are
|
|
4256
|
+
* kept. Idempotent and resumable: a later start finds nothing to drop.
|
|
4257
|
+
*/
|
|
4258
|
+
private relaxValueDomainConstraints;
|
|
4259
|
+
/**
|
|
4260
|
+
* Throws {@link OperationTransformCoverageError} when a schema version in the stored
|
|
4261
|
+
* log has no transform path to `version` (RT-103): those operations would fold as
|
|
4262
|
+
* absent, silently erasing them from their records.
|
|
4263
|
+
*/
|
|
4264
|
+
private assertTransformCoverage;
|
|
4265
|
+
getOperationTransforms(): readonly OperationTransform[];
|
|
4266
|
+
setOperationTransforms(transforms: readonly OperationTransform[]): Promise<void>;
|
|
4267
|
+
setSchema(schema: SchemaDefinition, options?: ServerSchemaOptions): Promise<void>;
|
|
4268
|
+
/**
|
|
4269
|
+
* One-time migration of each collection table to the lossless text encoding (RT-65):
|
|
4270
|
+
* rows written before it that contain U+FFFF (the escape introducer) are re-encoded,
|
|
4271
|
+
* so the decoder returns them unchanged. Per table, under an advisory lock, recorded
|
|
4272
|
+
* in `kora_server_meta`.
|
|
4273
|
+
*/
|
|
4274
|
+
private migrateTextCodec;
|
|
4275
|
+
applyRemoteOperation(op: Operation, options?: ApplyRemoteOptions): Promise<ApplyResult>;
|
|
4276
|
+
/**
|
|
4277
|
+
* The version vector as committed in `sync_state`, shared by every instance
|
|
4278
|
+
* (SRV-4). Also refreshes this instance's cache, which `getVersionVector()` serves.
|
|
4279
|
+
*/
|
|
4280
|
+
readVersionVector(): Promise<VersionVector>;
|
|
4281
|
+
/**
|
|
4282
|
+
* Insert one operation row inside an append transaction that already holds the
|
|
4283
|
+
* delivery-counter lock. Returns `'duplicate'` (writing nothing) when an operation
|
|
4284
|
+
* with the same id is stored; throws {@link SequenceConflictError} when a DIFFERENT
|
|
4285
|
+
* operation stored after the sequence-enforcement epoch holds its (node, sequence)
|
|
4286
|
+
* and the writer reserves its sequences (W3 step 4); a legacy holder or a legacy
|
|
4287
|
+
* writer (RT-37) is accepted and the row is stored unflagged. Otherwise returns the
|
|
4288
|
+
* verdict the row was stored under. The partial unique index over sole-holder rows
|
|
4289
|
+
* backs the check against a writer that bypasses the counter lock.
|
|
4290
|
+
*/
|
|
4291
|
+
private insertOperationRow;
|
|
4292
|
+
/** Advance `sync_state` (the persisted version vector) for an appended operation. */
|
|
4293
|
+
private advanceSyncState;
|
|
1805
4294
|
/**
|
|
1806
4295
|
* Conditionally apply operations atomically, serialized across server instances
|
|
1807
4296
|
* on the target record via a transaction-scoped advisory lock. The lock makes
|
|
@@ -1813,7 +4302,37 @@ declare class PostgresServerStore implements ServerStore {
|
|
|
1813
4302
|
getOperationCount(): Promise<number>;
|
|
1814
4303
|
getMaxDeliverySequence(): Promise<number>;
|
|
1815
4304
|
getOperationsAfterDelivery(afterDeliverySequence: number, limit: number): Promise<DeliveredOperation[]>;
|
|
4305
|
+
getOperationScopeSnapshots(operationIds: string[]): Promise<Map<string, OperationScopeSnapshot>>;
|
|
4306
|
+
getRecordLatestTimestamp(collection: string, recordId: string): Promise<HLCTimestamp | null>;
|
|
4307
|
+
getRecordFieldVersions(collection: string, recordId: string): Promise<RecordFieldVersions | null>;
|
|
4308
|
+
getRecordFoldState(collection: string, recordId: string): Promise<FoldState | null>;
|
|
4309
|
+
getRecordOperations(collection: string, recordId: string): Promise<Operation[]>;
|
|
4310
|
+
previewOperation(op: Operation): Promise<MaterializedRecord | null>;
|
|
4311
|
+
recordBlobOwner(hash: string, owner: string): Promise<void>;
|
|
4312
|
+
getBlobOwners(hashes: string[]): Promise<Map<string, string[]>>;
|
|
4313
|
+
claimBlobIfUnowned(hash: string, owner: string): Promise<boolean>;
|
|
4314
|
+
/**
|
|
4315
|
+
* Scope values of a record as stored, for an operation's scope snapshot. With
|
|
4316
|
+
* `includeDeleted` false a soft-deleted row counts as absent; with true it keeps
|
|
4317
|
+
* its last values (a delete's post-image).
|
|
4318
|
+
*/
|
|
4319
|
+
private readScopeValues;
|
|
4320
|
+
/** Persist the scope snapshot of a just-applied operation (RT-14). */
|
|
4321
|
+
private writeScopeSnapshot;
|
|
4322
|
+
/**
|
|
4323
|
+
* Rebuild missing scope snapshots from the log (migration from a database written
|
|
4324
|
+
* before snapshots existed), replaying each affected record in commit order.
|
|
4325
|
+
*/
|
|
4326
|
+
private backfillScopeSnapshots;
|
|
1816
4327
|
materializeCollection(collection: string): Promise<MaterializedRecord[]>;
|
|
4328
|
+
getNodeIdsAfterDelivery(afterDeliverySequence: number): Promise<string[]>;
|
|
4329
|
+
getSequencePairOperations(nodeId: string, throughSequence: number): Promise<Operation[]>;
|
|
4330
|
+
recordOperationResolution(resolution: OperationResolution): Promise<void>;
|
|
4331
|
+
findOperationResolutions(nodeId: string, ids: string[]): Promise<Map<string, OperationResolution>>;
|
|
4332
|
+
deleteOperationResolution(nodeId: string, operationId: string): Promise<void>;
|
|
4333
|
+
getResolvedThrough(nodeId: string): Promise<number>;
|
|
4334
|
+
findStoredOperations(ids: string[]): Promise<Map<string, StoredOperationKey>>;
|
|
4335
|
+
findRecordsByIds(collection: string, ids: string[]): Promise<Map<string, MaterializedRecord>>;
|
|
1817
4336
|
queryCollection(collection: string, options?: CollectionQueryOptions): Promise<MaterializedRecord[]>;
|
|
1818
4337
|
findRecord(collection: string, id: string): Promise<MaterializedRecord | null>;
|
|
1819
4338
|
countCollection(collection: string, where?: Record<string, unknown>): Promise<number>;
|
|
@@ -1830,28 +4349,106 @@ declare class PostgresServerStore implements ServerStore {
|
|
|
1830
4349
|
* write so newly built operations sort strictly after it.
|
|
1831
4350
|
*/
|
|
1832
4351
|
private latestOperationTimestamp;
|
|
4352
|
+
getEncryptionKeyRecord(owner: string, keyring: string): Promise<string | null>;
|
|
4353
|
+
putEncryptionKeyRecord(owner: string, keyring: string, record: string, revision: number, expectedRevision: number): Promise<boolean>;
|
|
4354
|
+
listEncryptionKeyRecords(owner?: string): Promise<EncryptionKeyRecordRow[]>;
|
|
4355
|
+
getEncryptedKeyIds(nodeOwner: string | null, limit: number): Promise<string[]>;
|
|
4356
|
+
claimNode(nodeId: string, userId: string): Promise<boolean>;
|
|
4357
|
+
claimUnownedNode(nodeId: string, userId: string): Promise<boolean>;
|
|
4358
|
+
getNodeClaimOwner(nodeId: string): Promise<string | null>;
|
|
4359
|
+
replaceNodeClaim(nodeId: string, expectedOwner: string, newOwner: string): Promise<boolean>;
|
|
4360
|
+
releaseNodeClaim(nodeId: string): Promise<boolean>;
|
|
4361
|
+
/**
|
|
4362
|
+
* The record as currently stored, including a soft-deleted one (whose last field
|
|
4363
|
+
* values are kept), or null when it was never written. Without a materialized
|
|
4364
|
+
* table, the last known values are replayed from the operation log.
|
|
4365
|
+
*/
|
|
4366
|
+
private readStoredRow;
|
|
4367
|
+
/**
|
|
4368
|
+
* Merge a just-appended operation (stored under `deliverySeq`) into its record's
|
|
4369
|
+
* fold state and project the row, inside the append transaction. O(fields the
|
|
4370
|
+
* operation touches): the state is read by primary key and the freshness check is
|
|
4371
|
+
* one index probe (SRV-7).
|
|
4372
|
+
*
|
|
4373
|
+
* Concurrency: every append on every instance holds the delivery-counter row lock
|
|
4374
|
+
* until commit (see nextDeliverySeq), and applyConditional takes its per-record
|
|
4375
|
+
* advisory lock before it, so this read-merge-write of one record's state is
|
|
4376
|
+
* serialized across instances; the `FOR UPDATE` on the state row is defense in
|
|
4377
|
+
* depth. A record whose log grew without this state (an older release during a
|
|
4378
|
+
* rolling upgrade) has its missing tail merged first; an unreadable, newer-than-log
|
|
4379
|
+
* or plan-incompatible state is re-folded from the log.
|
|
4380
|
+
*/
|
|
4381
|
+
private mergeIntoRecord;
|
|
4382
|
+
/**
|
|
4383
|
+
* The fold state of a record that owns quarantined operations (RT-70): its stored
|
|
4384
|
+
* state, or its kept row as a snapshot base, with every remaining operation of the
|
|
4385
|
+
* record merged on top (`rebuildFromSnapshot`). Nothing is written.
|
|
4386
|
+
*/
|
|
4387
|
+
private rebuildAffected;
|
|
4388
|
+
/** True when the record's operation at delivery sequence `seq` has id `opId`. */
|
|
4389
|
+
private coversStoredOperation;
|
|
4390
|
+
/**
|
|
4391
|
+
* The record's current fold state for a read: the stored one when it covers the
|
|
4392
|
+
* record's whole log, else folded from the log (nothing is written).
|
|
4393
|
+
*/
|
|
4394
|
+
private readFoldState;
|
|
4395
|
+
/** A record's operations stored after `afterDeliverySeq`, in delivery order. */
|
|
4396
|
+
private readRecordOperations;
|
|
1833
4397
|
/**
|
|
1834
|
-
*
|
|
1835
|
-
*
|
|
4398
|
+
* Persist a record's fold state (covering the log up to `coveredSeq`) and project
|
|
4399
|
+
* it onto the materialized row.
|
|
1836
4400
|
*/
|
|
1837
|
-
private
|
|
4401
|
+
private writeFoldedRecord;
|
|
1838
4402
|
/**
|
|
1839
4403
|
* UPSERT a record into the materialized collection table.
|
|
1840
4404
|
*/
|
|
1841
4405
|
private upsertMaterializedRecord;
|
|
1842
4406
|
/**
|
|
1843
|
-
*
|
|
4407
|
+
* {@link writeFoldedRecord} for a batch of records of one collection, in a handful
|
|
4408
|
+
* of multi-row statements (the migration's write path). Rows are chunked so a
|
|
4409
|
+
* statement never nears the 65,535 bind-parameter limit, however wide the collection.
|
|
4410
|
+
*/
|
|
4411
|
+
private writeFoldedRecords;
|
|
4412
|
+
/**
|
|
4413
|
+
* Re-materialization migration (W7 step 7), run by setSchema and after a
|
|
4414
|
+
* replace-mode restore: every record whose fold state is missing, stale or built
|
|
4415
|
+
* under a different fold plan is re-folded from its log; nothing else is touched.
|
|
4416
|
+
* See the SQLite store's `rematerialize` for the rules (a record owning quarantined
|
|
4417
|
+
* operations folds onto its kept row, RT-70; idempotent; resumable).
|
|
4418
|
+
*
|
|
4419
|
+
* Postgres specifics: records are paged in batches of
|
|
4420
|
+
* {@link FOLD_MIGRATION_BATCH} through the (collection, record_id, delivery_seq)
|
|
4421
|
+
* index, and every batch is one transaction that first takes the delivery-counter
|
|
4422
|
+
* row lock. Appends on every instance take that lock too, so a batch reads each
|
|
4423
|
+
* record's complete log and writes its state while no append to it can commit: a
|
|
4424
|
+
* live write on another instance during a rolling deploy is never overwritten by a
|
|
4425
|
+
* stale re-fold (NEW-SRV-5), it either committed before the batch (and is folded)
|
|
4426
|
+
* or commits after it (and is merged into the batch's state).
|
|
1844
4427
|
*/
|
|
1845
|
-
private backfillAllCollections;
|
|
1846
4428
|
/**
|
|
1847
|
-
*
|
|
4429
|
+
* Write the beta.12 clears the ids prove into the stored bodies, once per database
|
|
4430
|
+
* (RT-85, see `provenLegacyClears`), and mark their records' fold states stale.
|
|
4431
|
+
* Idempotent: concurrent first starts write the same bodies.
|
|
1848
4432
|
*/
|
|
1849
|
-
private
|
|
4433
|
+
private canonicalizeLegacyBodies;
|
|
4434
|
+
private rematerialize;
|
|
1850
4435
|
private buildSelectQuery;
|
|
1851
4436
|
private buildWhereClause;
|
|
1852
4437
|
private deserializeRow;
|
|
1853
4438
|
private materializeFromOpsLog;
|
|
1854
4439
|
private initialize;
|
|
4440
|
+
/**
|
|
4441
|
+
* The startup log-integrity scan's result (W8 step 0): rows that could not be read
|
|
4442
|
+
* back into an operation are moved to `operations_quarantine` once per database,
|
|
4443
|
+
* before any fold reads them.
|
|
4444
|
+
*/
|
|
4445
|
+
getLogIntegrityReport(): Promise<ServerLogIntegrityReport>;
|
|
4446
|
+
/**
|
|
4447
|
+
* One-time scan under the setup advisory lock (concurrent starts run it once). Keyset
|
|
4448
|
+
* pages by id, so a large log is read in bounded chunks; like the delivery-sequence
|
|
4449
|
+
* backfill it runs on the first start of this release only.
|
|
4450
|
+
*/
|
|
4451
|
+
private scanLogIntegrityOnce;
|
|
1855
4452
|
private ensureTables;
|
|
1856
4453
|
/**
|
|
1857
4454
|
* Reserve the next delivery sequence inside an open append transaction. The
|
|
@@ -1859,6 +4456,11 @@ declare class PostgresServerStore implements ServerStore {
|
|
|
1859
4456
|
* serializing assignment into commit order (see the counter table comment).
|
|
1860
4457
|
*/
|
|
1861
4458
|
private nextDeliverySeq;
|
|
4459
|
+
/**
|
|
4460
|
+
* @param soleHolder - Nothing else held the (node, sequence) at insert: the row is
|
|
4461
|
+
* flagged `seq_unique` and covered by {@link NODE_SEQ_UNIQUE_INDEX}. False for a
|
|
4462
|
+
* legacy pair's second operation and for restored backup rows.
|
|
4463
|
+
*/
|
|
1862
4464
|
private serializeOperation;
|
|
1863
4465
|
private deserializeOperation;
|
|
1864
4466
|
private assertOpen;
|
|
@@ -1870,8 +4472,14 @@ declare class PostgresServerStore implements ServerStore {
|
|
|
1870
4472
|
*/
|
|
1871
4473
|
declare function createPostgresServerStore(options: {
|
|
1872
4474
|
connectionString: string;
|
|
1873
|
-
|
|
1874
|
-
|
|
4475
|
+
} & ServerIdentityOptions): Promise<PostgresServerStore>;
|
|
4476
|
+
/**
|
|
4477
|
+
* The Postgres server store could not start: the database is unreachable, refused the
|
|
4478
|
+
* credentials, or its tables could not be created. `cause` is the driver's error.
|
|
4479
|
+
*/
|
|
4480
|
+
declare class ServerStoreUnavailableError extends KoraError {
|
|
4481
|
+
constructor(connectionString: string, cause: unknown);
|
|
4482
|
+
}
|
|
1875
4483
|
|
|
1876
4484
|
/**
|
|
1877
4485
|
* SQLite-backed server store using Drizzle ORM.
|
|
@@ -1886,46 +4494,207 @@ declare class SqliteServerStore implements ServerStore {
|
|
|
1886
4494
|
private readonly db;
|
|
1887
4495
|
private schema;
|
|
1888
4496
|
private closed;
|
|
1889
|
-
|
|
4497
|
+
/** See {@link SEQUENCE_ENFORCEMENT_EPOCH_KEY}; fixed per database at first start. */
|
|
4498
|
+
private sequenceEpoch;
|
|
4499
|
+
private logIntegrity;
|
|
4500
|
+
/** Derivation secret of server-derived ids, shared by the deployment (RT-64). */
|
|
4501
|
+
private readonly derivationSecret;
|
|
4502
|
+
/** Legacy and configured authorities (the `kora:server:` prefix needs no listing). */
|
|
4503
|
+
private readonly explicitAuthorities;
|
|
4504
|
+
/** Explicit authority over time: revoked and ever-held ids (RT-81). */
|
|
4505
|
+
private readonly authorityHistory;
|
|
4506
|
+
/** Other `kora:server:` nodes with stored operations (advertised for older clients). */
|
|
4507
|
+
/** Records owning quarantined operations: folded onto their kept rows (RT-70). */
|
|
4508
|
+
private quarantine;
|
|
4509
|
+
private foldOptions;
|
|
4510
|
+
/** Schema transforms the fold applies (transforms at fold time, RT-84). */
|
|
4511
|
+
private operationTransforms;
|
|
4512
|
+
private foldMigration;
|
|
4513
|
+
/**
|
|
4514
|
+
* @param db - The Drizzle database
|
|
4515
|
+
* @param nodeId - Deprecated: a plain id is recorded as a legacy authoritative id; a
|
|
4516
|
+
* `kora:server:` id is used verbatim. Leave unset: the store authors under
|
|
4517
|
+
* `kora:server:<deployment>:<instance>`, both persisted in the database (RT-62).
|
|
4518
|
+
* @param options - Extra authoritative ids and the instance id
|
|
4519
|
+
*/
|
|
4520
|
+
constructor(db: BetterSQLite3Database, nodeId?: string, options?: Omit<ServerIdentityOptions, 'nodeId'>);
|
|
4521
|
+
/**
|
|
4522
|
+
* This store's node id and the legacy and configured ids (explicit authorities).
|
|
4523
|
+
* Other `kora:server:` instances are not listed: the prefix rule makes every one of
|
|
4524
|
+
* them authoritative, and a list that grew by one instance id per start would make
|
|
4525
|
+
* every device re-fold after each deploy (RT-75). Handshakes advertise only the
|
|
4526
|
+
* explicit (non-prefixed) ids.
|
|
4527
|
+
*/
|
|
4528
|
+
getAuthoritativeNodeIds(): string[];
|
|
4529
|
+
/** Legacy and configured authorities (beside the `kora:server:` namespace). */
|
|
4530
|
+
getLegacyAuthoritativeNodeIds(): string[];
|
|
4531
|
+
/** Explicit authoritative ids the deployment revoked (RT-81), advertised at handshake. */
|
|
4532
|
+
getRevokedAuthoritativeNodeIds(): string[];
|
|
4533
|
+
/**
|
|
4534
|
+
* Every explicit id the deployment ever held authoritative, revoked ones included
|
|
4535
|
+
* (RT-81): never accepted as a device node id.
|
|
4536
|
+
*/
|
|
4537
|
+
getEverAuthoritativeNodeIds(): string[];
|
|
4538
|
+
deriveServerOperationId(parentOpId: string, ruleId: string, targetRecordId: string): Promise<string>;
|
|
4539
|
+
/** Remember another `kora:server:` node that authored stored operations. */
|
|
4540
|
+
/**
|
|
4541
|
+
* Load (creating on first start) the persisted server identity: the deployment id,
|
|
4542
|
+
* the derivation secret, this database's instance id and the legacy authoritative
|
|
4543
|
+
* ids. At the first start of this release, every node id whose operations hold an
|
|
4544
|
+
* authority class in a stored fold state (a server decision folded under an earlier,
|
|
4545
|
+
* per-process server node id) is recorded as a legacy authority, before any re-fold
|
|
4546
|
+
* could drop that class.
|
|
4547
|
+
*/
|
|
4548
|
+
private loadIdentity;
|
|
4549
|
+
/** What the last startup re-materialization did (W7 step 7). */
|
|
4550
|
+
getFoldMigrationReport(): FoldMigrationReport;
|
|
1890
4551
|
getVersionVector(): VersionVector;
|
|
1891
4552
|
getNodeId(): string;
|
|
1892
4553
|
getSchema(): SchemaDefinition | null;
|
|
1893
|
-
setSchema(schema: SchemaDefinition): Promise<void>;
|
|
1894
|
-
|
|
4554
|
+
setSchema(schema: SchemaDefinition, options?: ServerSchemaOptions): Promise<void>;
|
|
4555
|
+
/**
|
|
4556
|
+
* One-time migration (RT-101): rebuild collection tables that still carry an enum
|
|
4557
|
+
* `CHECK` (or `NOT NULL` on a schema field), in one transaction, keeping rows, indexes
|
|
4558
|
+
* and triggers. Idempotent (a relaxed table is left alone) and resumable (an
|
|
4559
|
+
* interrupted rebuild rolls back and runs again at the next start).
|
|
4560
|
+
*/
|
|
4561
|
+
private relaxValueDomainConstraints;
|
|
4562
|
+
/**
|
|
4563
|
+
* Throws {@link OperationTransformCoverageError} when a schema version in the stored
|
|
4564
|
+
* log has no transform path to `version` (RT-103): those operations would fold as
|
|
4565
|
+
* absent, silently erasing them from their records.
|
|
4566
|
+
*/
|
|
4567
|
+
private assertTransformCoverage;
|
|
4568
|
+
getOperationTransforms(): readonly OperationTransform[];
|
|
4569
|
+
setOperationTransforms(transforms: readonly OperationTransform[]): Promise<void>;
|
|
4570
|
+
applyRemoteOperation(op: Operation, options?: ApplyRemoteOptions): Promise<ApplyResult>;
|
|
4571
|
+
private applyInTransaction;
|
|
1895
4572
|
getOperationRange(nodeId: string, fromSeq: number, toSeq: number): Promise<Operation[]>;
|
|
1896
4573
|
getOperationCount(): Promise<number>;
|
|
1897
4574
|
getMaxDeliverySequence(): Promise<number>;
|
|
1898
4575
|
getOperationsAfterDelivery(afterDeliverySequence: number, limit: number): Promise<DeliveredOperation[]>;
|
|
4576
|
+
getOperationScopeSnapshots(operationIds: string[]): Promise<Map<string, OperationScopeSnapshot>>;
|
|
4577
|
+
getRecordLatestTimestamp(collection: string, recordId: string): Promise<HLCTimestamp | null>;
|
|
4578
|
+
getRecordFieldVersions(collection: string, recordId: string): Promise<RecordFieldVersions | null>;
|
|
4579
|
+
getRecordFoldState(collection: string, recordId: string): Promise<FoldState | null>;
|
|
4580
|
+
getRecordOperations(collection: string, recordId: string): Promise<Operation[]>;
|
|
4581
|
+
previewOperation(op: Operation): Promise<MaterializedRecord | null>;
|
|
4582
|
+
recordBlobOwner(hash: string, owner: string): Promise<void>;
|
|
4583
|
+
getBlobOwners(hashes: string[]): Promise<Map<string, string[]>>;
|
|
4584
|
+
claimBlobIfUnowned(hash: string, owner: string): Promise<boolean>;
|
|
4585
|
+
/**
|
|
4586
|
+
* Scope values of a record as stored, for an operation's scope snapshot. With
|
|
4587
|
+
* `includeDeleted` false a soft-deleted row counts as absent (the pre-image of a
|
|
4588
|
+
* write to a deleted record); with true it keeps its last values (a delete's
|
|
4589
|
+
* post-image).
|
|
4590
|
+
*/
|
|
4591
|
+
private readScopeValues;
|
|
4592
|
+
/**
|
|
4593
|
+
* Rebuild missing scope snapshots from the log (migration from a database written
|
|
4594
|
+
* before snapshots existed), replaying each affected record in commit order.
|
|
4595
|
+
*/
|
|
4596
|
+
private backfillScopeSnapshots;
|
|
1899
4597
|
materializeCollection(collection: string): Promise<MaterializedRecord[]>;
|
|
4598
|
+
getNodeIdsAfterDelivery(afterDeliverySequence: number): Promise<string[]>;
|
|
4599
|
+
getSequencePairOperations(nodeId: string, throughSequence: number): Promise<Operation[]>;
|
|
4600
|
+
recordOperationResolution(resolution: OperationResolution): Promise<void>;
|
|
4601
|
+
findOperationResolutions(nodeId: string, ids: string[]): Promise<Map<string, OperationResolution>>;
|
|
4602
|
+
deleteOperationResolution(nodeId: string, operationId: string): Promise<void>;
|
|
4603
|
+
getResolvedThrough(nodeId: string): Promise<number>;
|
|
4604
|
+
findStoredOperations(ids: string[]): Promise<Map<string, StoredOperationKey>>;
|
|
4605
|
+
findRecordsByIds(collection: string, ids: string[]): Promise<Map<string, MaterializedRecord>>;
|
|
1900
4606
|
queryCollection(collection: string, options?: CollectionQueryOptions): Promise<MaterializedRecord[]>;
|
|
1901
4607
|
findRecord(collection: string, id: string): Promise<MaterializedRecord | null>;
|
|
1902
4608
|
countCollection(collection: string, where?: Record<string, unknown>): Promise<number>;
|
|
1903
4609
|
close(): Promise<void>;
|
|
4610
|
+
getEncryptionKeyRecord(owner: string, keyring: string): Promise<string | null>;
|
|
4611
|
+
putEncryptionKeyRecord(owner: string, keyring: string, record: string, revision: number, expectedRevision: number): Promise<boolean>;
|
|
4612
|
+
listEncryptionKeyRecords(owner?: string): Promise<EncryptionKeyRecordRow[]>;
|
|
4613
|
+
getEncryptedKeyIds(nodeOwner: string | null, limit: number): Promise<string[]>;
|
|
4614
|
+
claimNode(nodeId: string, userId: string): Promise<boolean>;
|
|
4615
|
+
claimUnownedNode(nodeId: string, userId: string): Promise<boolean>;
|
|
4616
|
+
getNodeClaimOwner(nodeId: string): Promise<string | null>;
|
|
4617
|
+
replaceNodeClaim(nodeId: string, expectedOwner: string, newOwner: string): Promise<boolean>;
|
|
4618
|
+
releaseNodeClaim(nodeId: string): Promise<boolean>;
|
|
4619
|
+
/**
|
|
4620
|
+
* The record as currently stored, including a soft-deleted one (whose last field
|
|
4621
|
+
* values are kept), or null when it was never written. Without a materialized
|
|
4622
|
+
* table, the last known values are replayed from the operation log.
|
|
4623
|
+
*/
|
|
4624
|
+
private readStoredRow;
|
|
4625
|
+
/**
|
|
4626
|
+
* The fold state of a record that owns quarantined operations (RT-70): its stored
|
|
4627
|
+
* state, or its kept row as a snapshot base, with every remaining operation of the
|
|
4628
|
+
* record merged on top (`rebuildFromSnapshot`). Nothing is written.
|
|
4629
|
+
*/
|
|
4630
|
+
private rebuildAffected;
|
|
1904
4631
|
exportBackup(): Promise<Uint8Array>;
|
|
1905
4632
|
importBackup(data: Uint8Array, merge?: boolean): Promise<{
|
|
1906
4633
|
operationsRestored: number;
|
|
1907
4634
|
success: boolean;
|
|
1908
4635
|
}>;
|
|
1909
4636
|
/**
|
|
1910
|
-
*
|
|
1911
|
-
*
|
|
1912
|
-
*
|
|
4637
|
+
* Merge a just-appended operation (stored under `deliverySeq`) into its record's
|
|
4638
|
+
* fold state and project the row, inside the append transaction. O(fields the
|
|
4639
|
+
* operation touches): the stored state is read by primary key and the freshness
|
|
4640
|
+
* check is one index probe, never a scan of the record's history (SRV-7).
|
|
4641
|
+
*
|
|
4642
|
+
* The stored state records the highest delivery sequence it covers. If another
|
|
4643
|
+
* writer appended operations to this record without folding them (an older release
|
|
4644
|
+
* during a rolling upgrade), the missing tail is merged first; if the stored state
|
|
4645
|
+
* is unreadable, newer than the log, or built under an incompatible field kind,
|
|
4646
|
+
* the record is re-folded from its log.
|
|
4647
|
+
*/
|
|
4648
|
+
private mergeIntoRecord;
|
|
4649
|
+
/** True when the record's operation at delivery sequence `seq` has id `opId`. */
|
|
4650
|
+
private coversStoredOperation;
|
|
4651
|
+
/**
|
|
4652
|
+
* The record's current fold state for a read: the stored one when it covers the
|
|
4653
|
+
* record's whole log, else folded from the log (nothing is written).
|
|
4654
|
+
*/
|
|
4655
|
+
private readFoldState;
|
|
4656
|
+
/** A record's operations stored after `afterDeliverySeq`, in delivery order. */
|
|
4657
|
+
private readRecordOperations;
|
|
4658
|
+
/**
|
|
4659
|
+
* Persist a record's fold state (covering the log up to `coveredSeq`) and project
|
|
4660
|
+
* it onto the materialized row.
|
|
1913
4661
|
*/
|
|
1914
|
-
private
|
|
4662
|
+
private writeFoldedRecord;
|
|
1915
4663
|
/**
|
|
1916
4664
|
* UPSERT a record into the materialized collection table.
|
|
1917
4665
|
* Uses INSERT ... ON CONFLICT (id) DO UPDATE SET for atomic upsert.
|
|
1918
4666
|
*/
|
|
1919
4667
|
private upsertMaterializedRecord;
|
|
1920
4668
|
/**
|
|
1921
|
-
*
|
|
1922
|
-
*
|
|
4669
|
+
* Re-materialization migration (W7 step 7), run by setSchema and after a
|
|
4670
|
+
* replace-mode restore. Every record whose fold state is missing, stale (it covers
|
|
4671
|
+
* less, or more, of the log than is stored) or built under a different fold plan
|
|
4672
|
+
* (field kind, merge strategy, resolver) is re-folded from its log and its row
|
|
4673
|
+
* rewritten; nothing else is touched, so a restart on an up-to-date database does
|
|
4674
|
+
* one aggregate read.
|
|
4675
|
+
*
|
|
4676
|
+
* - Runs only on a log the W8b integrity scan reports clean. With quarantined rows,
|
|
4677
|
+
* rows materialized before the fold existed are kept as they are (re-folding a
|
|
4678
|
+
* log that lost operations would bake the loss in) and the skip is logged loudly;
|
|
4679
|
+
* such a record is folded from its remaining log on its next write. Records with
|
|
4680
|
+
* no row yet, and fold states this release already wrote, are still processed.
|
|
4681
|
+
* - Idempotent and resumable: each batch commits on its own, and staleness is
|
|
4682
|
+
* re-derived from the tables at every start, so a crash resumes where it stopped.
|
|
4683
|
+
* A plan change first marks every stored state stale (one statement), then
|
|
4684
|
+
* records the new fingerprint, so it is resumable too.
|
|
1923
4685
|
*/
|
|
1924
|
-
private backfillAllCollections;
|
|
1925
4686
|
/**
|
|
1926
|
-
*
|
|
4687
|
+
* One-time migration of each collection table to the lossless text encoding (RT-65):
|
|
4688
|
+
* rows written before it whose raw-string columns contain U+FFFF (the escape
|
|
4689
|
+
* introducer) are re-encoded, so the decoder returns them unchanged.
|
|
4690
|
+
*/
|
|
4691
|
+
private migrateTextCodec;
|
|
4692
|
+
/**
|
|
4693
|
+
* Write the beta.12 clears the ids prove into the stored bodies, once per database
|
|
4694
|
+
* (RT-85, see `provenLegacyClears`), and mark their records' fold states stale.
|
|
1927
4695
|
*/
|
|
1928
|
-
private
|
|
4696
|
+
private canonicalizeLegacyBodies;
|
|
4697
|
+
private rematerialize;
|
|
1929
4698
|
private buildSelectQuery;
|
|
1930
4699
|
private buildWhereClause;
|
|
1931
4700
|
private deserializeRow;
|
|
@@ -1934,6 +4703,29 @@ declare class SqliteServerStore implements ServerStore {
|
|
|
1934
4703
|
* Create the operations and sync_state tables if they don't exist.
|
|
1935
4704
|
*/
|
|
1936
4705
|
private ensureTables;
|
|
4706
|
+
/**
|
|
4707
|
+
* The startup log-integrity scan's result (W8 step 0): rows that could not be read
|
|
4708
|
+
* back into an operation are moved to `operations_quarantine` once per database,
|
|
4709
|
+
* before any fold reads them.
|
|
4710
|
+
*/
|
|
4711
|
+
getLogIntegrityReport(): ServerLogIntegrityReport;
|
|
4712
|
+
private scanLogIntegrityOnce;
|
|
4713
|
+
/**
|
|
4714
|
+
* Index the legacy pairs already in the log once (history written before the
|
|
4715
|
+
* `sequence_pairs` table existed); appends maintain it from then on.
|
|
4716
|
+
*/
|
|
4717
|
+
private backfillSequencePairs;
|
|
4718
|
+
/**
|
|
4719
|
+
* Record the sequence-enforcement epoch on the first start of this release (the log's
|
|
4720
|
+
* highest delivery sequence then; see {@link SEQUENCE_ENFORCEMENT_EPOCH_KEY}) and
|
|
4721
|
+
* back the append check with the partial unique index over sole-holder rows
|
|
4722
|
+
* ({@link NODE_SEQ_UNIQUE_INDEX}), which therefore always exists: a legacy duplicate
|
|
4723
|
+
* pair is stored unflagged. Replaces the earlier indexes (a full one, created only
|
|
4724
|
+
* when the log had no duplicates, and one over the rows past the epoch, which a
|
|
4725
|
+
* legacy client's pair would violate). Runs in one write transaction, so stores
|
|
4726
|
+
* sharing a file agree on the epoch.
|
|
4727
|
+
*/
|
|
4728
|
+
private ensureSequenceEnforcement;
|
|
1937
4729
|
/**
|
|
1938
4730
|
* Assign delivery sequences to any operations written before the column existed,
|
|
1939
4731
|
* then seed the durable counter from the current max. Ordered by
|
|
@@ -1947,6 +4739,11 @@ declare class SqliteServerStore implements ServerStore {
|
|
|
1947
4739
|
* allocation shared by every SqliteServerStore instance using the same file.
|
|
1948
4740
|
*/
|
|
1949
4741
|
private nextDeliverySeq;
|
|
4742
|
+
/**
|
|
4743
|
+
* @param soleHolder - Nothing else held the (node, sequence) at insert: the row is
|
|
4744
|
+
* flagged `seq_unique` and covered by {@link NODE_SEQ_UNIQUE_INDEX}. False for a
|
|
4745
|
+
* legacy pair's second operation and for restored backup rows.
|
|
4746
|
+
*/
|
|
1950
4747
|
private serializeOperation;
|
|
1951
4748
|
private deserializeOperation;
|
|
1952
4749
|
private assertOpen;
|
|
@@ -1959,7 +4756,9 @@ declare class SqliteServerStore implements ServerStore {
|
|
|
1959
4756
|
*
|
|
1960
4757
|
* @param options - Configuration options
|
|
1961
4758
|
* @param options.filename - Path to SQLite database file. Defaults to ':memory:' for testing.
|
|
1962
|
-
* @param options.nodeId -
|
|
4759
|
+
* @param options.nodeId - Deprecated (a plain id becomes a legacy authoritative id). Leave
|
|
4760
|
+
* unset: the server authors under the persisted `kora:server:<deployment>:<instance>`.
|
|
4761
|
+
* @param options.instanceId - Optional stable instance id
|
|
1963
4762
|
* @returns A ready-to-use SqliteServerStore
|
|
1964
4763
|
*
|
|
1965
4764
|
* @example
|
|
@@ -1976,8 +4775,7 @@ declare class SqliteServerStore implements ServerStore {
|
|
|
1976
4775
|
*/
|
|
1977
4776
|
declare function createSqliteServerStore(options: {
|
|
1978
4777
|
filename?: string;
|
|
1979
|
-
|
|
1980
|
-
}): SqliteServerStore;
|
|
4778
|
+
} & ServerIdentityOptions): SqliteServerStore;
|
|
1981
4779
|
|
|
1982
4780
|
/**
|
|
1983
4781
|
* Auth provider that accepts all connections.
|
|
@@ -1988,38 +4786,187 @@ declare class NoAuthProvider implements AuthProvider {
|
|
|
1988
4786
|
authenticate(_token: string): Promise<AuthContext>;
|
|
1989
4787
|
}
|
|
1990
4788
|
|
|
4789
|
+
/**
|
|
4790
|
+
* Thrown when a sync scope predicate value is `undefined` or `null` (also inside a
|
|
4791
|
+
* `$in` list). Such a value matches every record that lacks the field, so a grant
|
|
4792
|
+
* built from a failed lookup (for example `{ orgId: user.orgId }` with no org)
|
|
4793
|
+
* would expose every unowned record. Kora refuses it instead (fail closed).
|
|
4794
|
+
*/
|
|
4795
|
+
declare class InvalidScopePredicateError extends KoraError {
|
|
4796
|
+
readonly collection: string;
|
|
4797
|
+
readonly field: string;
|
|
4798
|
+
constructor(collection: string, field: string);
|
|
4799
|
+
}
|
|
4800
|
+
/** Thrown when a `$in` scope predicate is malformed or larger than the configured limit. */
|
|
4801
|
+
declare class ScopePredicateLimitError extends KoraError {
|
|
4802
|
+
constructor(message: string, context: Record<string, unknown>);
|
|
4803
|
+
}
|
|
4804
|
+
|
|
4805
|
+
interface ResolveSessionScopesOptions {
|
|
4806
|
+
/** Scope map sent by the client during handshake. It can only NARROW the grant. */
|
|
4807
|
+
handshakeScope?: ScopeMap$1;
|
|
4808
|
+
/**
|
|
4809
|
+
* The server grant from the auth provider. Its collection set is authoritative:
|
|
4810
|
+
* a collection it does not list is not visible, whatever the handshake asks for.
|
|
4811
|
+
* It may carry verified claim values under `$claims` (see `claimScopes` in
|
|
4812
|
+
* `@korajs/core`), which are bound to every schema-scoped collection.
|
|
4813
|
+
*/
|
|
4814
|
+
authScopes?: ScopeMap$1;
|
|
4815
|
+
/**
|
|
4816
|
+
* Verified, server-derived scope values (for example `{ userId: sub }`). Treated
|
|
4817
|
+
* exactly like `$claims` in `authScopes`. Never pass client-supplied values here.
|
|
4818
|
+
*/
|
|
4819
|
+
scopeValues?: Record<string, unknown>;
|
|
4820
|
+
/**
|
|
4821
|
+
* True when an auth provider authenticated this session. An authenticated
|
|
4822
|
+
* session whose provider returned no grant at all is denied every
|
|
4823
|
+
* schema-scoped collection instead of trusting the client handshake.
|
|
4824
|
+
*
|
|
4825
|
+
* Leave it false only for servers without an auth provider (or with
|
|
4826
|
+
* `NoAuthProvider`), where the handshake is a client-side filter, not a grant.
|
|
4827
|
+
* @default false
|
|
4828
|
+
*/
|
|
4829
|
+
authenticated?: boolean;
|
|
4830
|
+
/**
|
|
4831
|
+
* What to do when a schema-scoped collection's bindings cannot all be resolved
|
|
4832
|
+
* from the verified claims. `'deny'` hides the collection (fail closed);
|
|
4833
|
+
* `'throw'` raises {@link ScopeRequiredError} so the caller can reject the
|
|
4834
|
+
* handshake with `SCOPE_REQUIRED`.
|
|
4835
|
+
* @default 'deny'
|
|
4836
|
+
*/
|
|
4837
|
+
onUnresolved?: 'deny' | 'throw';
|
|
4838
|
+
}
|
|
4839
|
+
/** A schema-scoped collection the session was denied because a binding was unresolved. */
|
|
4840
|
+
interface DeniedScopeCollection {
|
|
4841
|
+
collection: string;
|
|
4842
|
+
/** Scope binding keys (for example `orgId`) that had no verified value. */
|
|
4843
|
+
missingKeys: string[];
|
|
4844
|
+
}
|
|
4845
|
+
/** Detailed result of {@link resolveSessionScopeGrant}. */
|
|
4846
|
+
interface ResolvedSessionScopeGrant {
|
|
4847
|
+
/**
|
|
4848
|
+
* Effective scope map. `undefined` means "no scoping" and is only ever returned
|
|
4849
|
+
* for unauthenticated servers without a grant. An empty object means "nothing
|
|
4850
|
+
* visible".
|
|
4851
|
+
*/
|
|
4852
|
+
scopes: ScopeMap$1 | undefined;
|
|
4853
|
+
/** Collections denied because their scope bindings could not be resolved. */
|
|
4854
|
+
denied: DeniedScopeCollection[];
|
|
4855
|
+
}
|
|
4856
|
+
/**
|
|
4857
|
+
* Thrown (with `onUnresolved: 'throw'`) when an authenticated session cannot be
|
|
4858
|
+
* bound to a schema-scoped collection because the server grant lacks a value.
|
|
4859
|
+
*/
|
|
4860
|
+
declare class ScopeRequiredError extends KoraError {
|
|
4861
|
+
readonly denied: DeniedScopeCollection[];
|
|
4862
|
+
constructor(denied: DeniedScopeCollection[]);
|
|
4863
|
+
}
|
|
4864
|
+
/**
|
|
4865
|
+
* Resolve the effective per-session sync scope map.
|
|
4866
|
+
*
|
|
4867
|
+
* The result is the INTERSECTION of the server grant and the client handshake:
|
|
4868
|
+
* - The collection set is the grant's collection set. A handshake can never add
|
|
4869
|
+
* a collection.
|
|
4870
|
+
* - Per collection, handshake predicates only narrow: grant fields win, a
|
|
4871
|
+
* handshake field on an unconstrained field is added, and for an `$in` grant a
|
|
4872
|
+
* handshake value is used only when it is a subset of the granted values.
|
|
4873
|
+
* - Schema-scoped collections bound from verified claims fail closed: an
|
|
4874
|
+
* unresolved binding denies the collection instead of producing `{}`.
|
|
4875
|
+
*
|
|
4876
|
+
* Invariant: the effective scope is a subset of the server grant, and no
|
|
4877
|
+
* schema-scoped collection resolves to `{}` from claims for an authenticated
|
|
4878
|
+
* principal.
|
|
4879
|
+
*
|
|
4880
|
+
* @returns The effective scope map; `undefined` only for unauthenticated servers
|
|
4881
|
+
* with neither a grant nor a handshake scope
|
|
4882
|
+
*/
|
|
4883
|
+
declare function resolveSessionScopes(schema: SchemaDefinition | null, options: ResolveSessionScopesOptions): ScopeMap$1 | undefined;
|
|
4884
|
+
/**
|
|
4885
|
+
* Same as {@link resolveSessionScopes}, but also reports which collections were
|
|
4886
|
+
* denied for unresolved bindings, so a caller can surface `SCOPE_REQUIRED`.
|
|
4887
|
+
*/
|
|
4888
|
+
declare function resolveSessionScopeGrant(schema: SchemaDefinition | null, options: ResolveSessionScopesOptions): ResolvedSessionScopeGrant;
|
|
4889
|
+
|
|
4890
|
+
/**
|
|
4891
|
+
* Decides whether one client's current awareness state may reach another session.
|
|
4892
|
+
* Called with the receiving session's id; returning false keeps the state from it.
|
|
4893
|
+
* The sync server builds one per update from the record named in the state's cursor.
|
|
4894
|
+
*/
|
|
4895
|
+
type AwarenessAudience = (targetSessionId: string) => boolean;
|
|
1991
4896
|
/**
|
|
1992
4897
|
* Server-side awareness relay. Broadcasts ephemeral awareness states
|
|
1993
4898
|
* (cursor positions, user presence) between connected clients.
|
|
1994
4899
|
*
|
|
1995
|
-
* Awareness data is never persisted.
|
|
1996
|
-
*
|
|
1997
|
-
*
|
|
4900
|
+
* Awareness data is never persisted. Each session's awareness `clientId` is bound on
|
|
4901
|
+
* registration: a session may only publish (or clear) its own state, never another
|
|
4902
|
+
* client's. A state reaches exactly the sessions its audience admits (the sync server
|
|
4903
|
+
* admits sessions whose download scope contains the record named in the cursor), or,
|
|
4904
|
+
* for an update without an audience, the clients of the same partition.
|
|
1998
4905
|
*/
|
|
1999
4906
|
declare class AwarenessRelay {
|
|
2000
4907
|
private readonly clients;
|
|
2001
4908
|
/**
|
|
2002
|
-
* Register a client for awareness broadcasting.
|
|
4909
|
+
* Register a client for awareness broadcasting. A session registers once; later
|
|
4910
|
+
* calls for an already-registered session are ignored, so a client cannot rebind
|
|
4911
|
+
* itself to another client's id.
|
|
4912
|
+
*
|
|
4913
|
+
* The new client is sent every existing state it may see (catch-up): a state with
|
|
4914
|
+
* an audience when the audience admits the new session, otherwise a state of the
|
|
4915
|
+
* same partition.
|
|
2003
4916
|
*
|
|
2004
4917
|
* @param sessionId - Unique session identifier
|
|
2005
4918
|
* @param clientId - Client-assigned awareness ID
|
|
2006
4919
|
* @param transport - Transport for sending messages to this client
|
|
4920
|
+
* @param partition - Presence partition for states relayed without an audience
|
|
4921
|
+
*/
|
|
4922
|
+
addClient(sessionId: string, clientId: number, transport: ServerTransport, partition?: string): void;
|
|
4923
|
+
/**
|
|
4924
|
+
* True when the session is registered with the relay.
|
|
4925
|
+
*
|
|
4926
|
+
* @param sessionId - Session to check
|
|
2007
4927
|
*/
|
|
2008
|
-
|
|
4928
|
+
hasClient(sessionId: string): boolean;
|
|
2009
4929
|
/**
|
|
2010
|
-
* Remove a client and
|
|
4930
|
+
* Remove a client and send its removal to every client that was shown its state.
|
|
2011
4931
|
*
|
|
2012
4932
|
* @param sessionId - Session ID of the disconnecting client
|
|
2013
4933
|
*/
|
|
2014
4934
|
removeClient(sessionId: string): void;
|
|
2015
4935
|
/**
|
|
2016
|
-
*
|
|
2017
|
-
*
|
|
4936
|
+
* True when {@link handleUpdate} would take this update: the session is registered,
|
|
4937
|
+
* the update is stamped with its bound `clientId` and carries that client's entry.
|
|
4938
|
+
*
|
|
4939
|
+
* @param sessionId - Session ID of the sending client
|
|
4940
|
+
* @param message - The awareness update message
|
|
4941
|
+
*/
|
|
4942
|
+
accepts(sessionId: string, message: AwarenessUpdateMessage): boolean;
|
|
4943
|
+
/**
|
|
4944
|
+
* Handle an incoming awareness update from a registered client.
|
|
4945
|
+
*
|
|
4946
|
+
* Only the sender's own entry is accepted: the message must be stamped with the
|
|
4947
|
+
* sender's bound `clientId`, and only `states[clientId]` is stored and relayed.
|
|
4948
|
+
* Entries for other clients are dropped, so a sender cannot overwrite or remove
|
|
4949
|
+
* anyone else's presence.
|
|
4950
|
+
*
|
|
4951
|
+
* The state is sent to every other client the audience admits (without an
|
|
4952
|
+
* audience: every client of the sender's partition). A client that was shown an
|
|
4953
|
+
* earlier state of the sender but is not admitted now receives a removal.
|
|
2018
4954
|
*
|
|
2019
4955
|
* @param sessionId - Session ID of the sending client
|
|
2020
4956
|
* @param message - The awareness update message
|
|
4957
|
+
* @param audience - Who may see this state; omitted means the sender's partition
|
|
4958
|
+
*/
|
|
4959
|
+
handleUpdate(sessionId: string, message: AwarenessUpdateMessage, audience?: AwarenessAudience): void;
|
|
4960
|
+
/**
|
|
4961
|
+
* Re-decide who may see a client's current state after the record its cursor names
|
|
4962
|
+
* changed (F16): it moved into or out of a session's grant. Sessions that may no
|
|
4963
|
+
* longer see the state get a removal; sessions that may see it now and were not
|
|
4964
|
+
* shown it get the state. Later catch-ups use the new audience.
|
|
4965
|
+
*
|
|
4966
|
+
* @param sessionId - The client whose audience changed
|
|
4967
|
+
* @param audience - The new audience
|
|
2021
4968
|
*/
|
|
2022
|
-
|
|
4969
|
+
updateAudience(sessionId: string, audience: AwarenessAudience): void;
|
|
2023
4970
|
/**
|
|
2024
4971
|
* Get the number of registered awareness clients.
|
|
2025
4972
|
*/
|
|
@@ -2028,7 +4975,9 @@ declare class AwarenessRelay {
|
|
|
2028
4975
|
* Remove all clients and clear all state.
|
|
2029
4976
|
*/
|
|
2030
4977
|
clear(): void;
|
|
2031
|
-
|
|
4978
|
+
/** True when `target` may see `source`'s current state. Fails closed. */
|
|
4979
|
+
private mayReceive;
|
|
4980
|
+
private removalMessage;
|
|
2032
4981
|
}
|
|
2033
4982
|
|
|
2034
4983
|
/**
|
|
@@ -2048,4 +4997,4 @@ declare class AwarenessRelay {
|
|
|
2048
4997
|
*/
|
|
2049
4998
|
declare function createKoraServer(config: KoraSyncServerConfig): KoraSyncServer;
|
|
2050
4999
|
|
|
2051
|
-
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 };
|
|
5000
|
+
export { type ApplyRemoteOptions, type AuthContext, type AuthProvider, type AwarenessAudience, type AwarenessCursorTarget, AwarenessRelay, type AwarenessRelayCallback, type ClientMetrics, ClientSession, type ClientSessionOptions, type CollectionQueryOptions, DEFAULT_MAX_SCOPE_PREDICATE_VALUES, type DeniedScopeCollection, type FindJsonStringValuesOptions, type FoldMigrationReport, type HttpPollResponse, HttpServerTransport, type HttpSyncRequest, type HttpSyncResponse, InvalidDiagnosticOptionsError, InvalidScopePredicateError, type JsonStringValueReport, 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, findJsonStringValues, isRetriableRejection, isServerNodeId, normalizeScopeMap, operationMatchesScopes, resolveSessionScopeGrant, resolveSessionScopes };
|