@korajs/sync 1.0.0-beta.11 → 1.0.0-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -40
- package/dist/chunk-5S6U7EVU.js +344 -0
- package/dist/chunk-5S6U7EVU.js.map +1 -0
- package/dist/index.cjs +6671 -1082
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1625 -96
- package/dist/index.d.ts +1625 -96
- package/dist/index.js +6382 -1100
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +313 -2
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +117 -3
- package/dist/internal.d.ts +117 -3
- package/dist/internal.js +17 -3
- package/dist/internal.js.map +1 -1
- package/dist/transport-Bemtqwep.d.cts +1448 -0
- package/dist/transport-Bemtqwep.d.ts +1448 -0
- package/package.json +3 -3
- package/dist/chunk-TU345YUC.js +0 -23
- package/dist/chunk-TU345YUC.js.map +0 -1
- package/dist/transport-iXX0KA_W.d.cts +0 -727
- package/dist/transport-iXX0KA_W.d.ts +0 -727
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { S as SyncScopeMap, D as DeltaCursor,
|
|
2
|
-
export { A as AcknowledgmentMessage,
|
|
1
|
+
import { S as SyncScopeMap, a as ScopeSnapshotOptions, D as DeltaCursor, b as SyncMessage, c as SerializedOperation, W as WireFormat, d as SyncTransport, T as TransportOptions, e as TransportMessageHandler, f as TransportCloseHandler, g as TransportErrorHandler, B as BlobChunkRequestMessage, h as BlobChunkResponseMessage, i as BlobChunkPushMessage, j as WrappedKeyRecord, k as SyncEncryptionConfig, V as VersionedKey, Y as YjsDocUpdateMessage, Q as QueueStorage, l as SyncState, m as SyncStatusInfo, n as SyncConfig, R as RejectedOperationStorage, o as SyncStatePersistence, p as SyncQuerySubset, q as QuarantinedOperation, H as HeldNodeInfo, r as RejectedOperation } from './transport-Bemtqwep.js';
|
|
2
|
+
export { A as AcknowledgmentMessage, s as ActiveApplyFailure, t as AdoptionScheduleInfo, u as AwarenessStateWire, v as AwarenessUpdateMessage, w as DEFAULT_KEYRING, E as EncryptedPayload, x as EncryptionKeyMessage, y as EncryptionKeyPutMessage, z as EncryptionKeyRequestMessage, C as EncryptionKeyResponseMessage, F as EncryptionKeyResponseStatus, G as ErrorMessage, I as HandshakeMessage, J as HandshakeResponseMessage, K as HeartbeatMessage, L as HeldReason, M as INVALID_OPERATION_ID, N as InvalidScopeError, O as KEY_RECORD_FORMAT, P as KeyRecordKdf, U as KeyRecordRecovery, X as KeyRecordValidation, Z as LEGACY_SYNC_PROTOCOL_VERSION, _ as LocalNodeInfo, $ as MAX_KDF_ITERATIONS, a0 as MAX_KEY_RECORD_BYTES, a1 as MemoryRejectedOperationStorage, a2 as OperationBatchMessage, a3 as OperationRejectedMessage, a4 as PLAINTEXT_REJECTED, a5 as PROTOCOL_V1_DEPRECATED, a6 as SYNC_PROTOCOL_VERSION, a7 as SYNC_STATES, a8 as SYNC_STATUSES, a9 as ScopeViolationError, aa as SyncAuthRequest, ab as SyncEncryptionAlgorithm, ac as SyncPhase, ad as SyncScopeContext, ae as SyncSettlementOptions, af as SyncSettlementResult, ag as SyncStatus, ah as TerminalRejectionRecord, ai as WrappedDataKey, aj as WrappedMasterKey, ak as declaredProtocolVersion, al as dedupeQuerySubsets, am as isAcknowledgmentMessage, an as isAwarenessUpdateMessage, ao as isBlobChunkPushMessage, ap as isBlobChunkRequestMessage, aq as isBlobChunkResponseMessage, ar as isEncryptionKeyMessage, as as isErrorMessage, at as isHandshakeMessage, au as isHandshakeResponseMessage, av as isKeyId, aw as isKeyRecordSuccessor, ax as isOperationBatchMessage, ay as isOperationRejectedMessage, az as isSyncMessage, aA as isValidKeyringName, aB as isYjsDocUpdateMessage, aC as operationMatchesQuerySubsets, aD as querySubsetContains, aE as validateKeyRecord } from './transport-Bemtqwep.js';
|
|
3
3
|
import * as _korajs_core from '@korajs/core';
|
|
4
|
-
import { Operation, VersionVector, ApplyResult, KoraEventEmitter, TimeSource, SyncError, ConnectionQuality } from '@korajs/core';
|
|
4
|
+
import { Operation, VersionVector, ApplyResult, OperationTransform, KoraEventEmitter, TimeSource, SyncError, ConnectionQuality, KoraError, SchemaDefinition } from '@korajs/core';
|
|
5
5
|
export { ApplyFailureReason, ApplyResult } from '@korajs/core';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -13,7 +13,8 @@ export { ApplyFailureReason, ApplyResult } from '@korajs/core';
|
|
|
13
13
|
* - Empty scope for a collection `{}`: no field restrictions, operation is in scope.
|
|
14
14
|
* - Scope has field/value pairs: all must match in the operation's data snapshot.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
16
|
+
* The snapshot is the operation's `data` (layered over `previousData` only with
|
|
17
|
+
* `options.includePreviousData`), with `id` always taken from `op.recordId`,
|
|
17
18
|
* which represents the record's state after the operation is applied. When an optional
|
|
18
19
|
* `fullRecord` is provided (e.g., from the local store), its values fill in scope fields
|
|
19
20
|
* that weren't included in the operation's data (critical for update operations where
|
|
@@ -23,9 +24,11 @@ export { ApplyFailureReason, ApplyResult } from '@korajs/core';
|
|
|
23
24
|
* @param scopeMap - Per-collection scope filters, or undefined for no filtering
|
|
24
25
|
* @param fullRecord - Optional full record state from the store, used to fill in scope
|
|
25
26
|
* fields not present in the operation's partial data
|
|
27
|
+
* @param options - Snapshot options; `includePreviousData` trusts the writer's
|
|
28
|
+
* `previousData`, which is only safe for a client judging its own local view
|
|
26
29
|
* @returns true if the operation is within scope
|
|
27
30
|
*/
|
|
28
|
-
declare function operationMatchesScope(op: Operation, scopeMap: SyncScopeMap | undefined, fullRecord?: Record<string, unknown> | null): boolean;
|
|
31
|
+
declare function operationMatchesScope(op: Operation, scopeMap: SyncScopeMap | undefined, fullRecord?: Record<string, unknown> | null, options?: ScopeSnapshotOptions): boolean;
|
|
29
32
|
/**
|
|
30
33
|
* Filter operations to only those matching the given scope map.
|
|
31
34
|
*
|
|
@@ -73,6 +76,12 @@ interface SyncStore {
|
|
|
73
76
|
* @returns 'applied' if the operation was new, 'duplicate' if already seen, 'skipped' if filtered
|
|
74
77
|
*/
|
|
75
78
|
applyRemoteOperation(op: Operation): Promise<ApplyResult>;
|
|
79
|
+
/**
|
|
80
|
+
* Optional: the schema transforms the store folds with (transforms at fold time,
|
|
81
|
+
* RT-84). The engine hands the store every operation exactly as delivered; the store
|
|
82
|
+
* folds its view. When present, it must match the engine's `operationTransforms`.
|
|
83
|
+
*/
|
|
84
|
+
getOperationTransforms?(): readonly OperationTransform[];
|
|
76
85
|
/**
|
|
77
86
|
* Get operations from a specific node within a sequence range.
|
|
78
87
|
* Used for computing deltas during sync.
|
|
@@ -90,11 +99,75 @@ interface SyncStore {
|
|
|
90
99
|
* @param ids - Operation ids that are candidates for re-stamping
|
|
91
100
|
* @param correctedNowMs - Trusted "now" (server time at handshake) in ms
|
|
92
101
|
*/
|
|
102
|
+
/**
|
|
103
|
+
* Optional: move the device to a fresh node id, re-authoring the given unsynced
|
|
104
|
+
* operations under it (RT-21). Called after the server refused the node id
|
|
105
|
+
* (`NODE_ID_CLAIMED`). Returns the new node id and the rewritten operations.
|
|
106
|
+
*/
|
|
107
|
+
rotateNodeId?(ids: string[]): Promise<{
|
|
108
|
+
nodeId: string;
|
|
109
|
+
operations: Operation[];
|
|
110
|
+
}>;
|
|
111
|
+
/**
|
|
112
|
+
* Optional: move the device back to a node id this database authored under before
|
|
113
|
+
* (RT-38), without rewriting anything. Used after the server refused the current node
|
|
114
|
+
* (another principal owns it) to try a node the signed-in principal owns.
|
|
115
|
+
*/
|
|
116
|
+
switchNodeId?(nodeId: string): Promise<void>;
|
|
117
|
+
/**
|
|
118
|
+
* Optional: bind local writes to the signed-in user (RT-42). Moves the store to the
|
|
119
|
+
* user's own node (or a fresh one) when the current node belongs to another user.
|
|
120
|
+
* `conflict`: the node id is pinned and belongs to another user.
|
|
121
|
+
*/
|
|
122
|
+
bindPrincipal?(principal: string): Promise<{
|
|
123
|
+
nodeId: string;
|
|
124
|
+
previousNodeId: string;
|
|
125
|
+
switched: boolean;
|
|
126
|
+
conflict: boolean;
|
|
127
|
+
}>;
|
|
128
|
+
/**
|
|
129
|
+
* Optional durability barrier (RT-35): resolve once every write committed so far is
|
|
130
|
+
* durable on this device; reject when it cannot be made durable. The engine awaits it
|
|
131
|
+
* before any operation leaves the device, so the server never holds an operation the
|
|
132
|
+
* device could lose on reload.
|
|
133
|
+
*/
|
|
134
|
+
ensureDurable?(): Promise<void>;
|
|
135
|
+
/**
|
|
136
|
+
* Optional: raise a local node's sequence counter to at least `floor`, in a
|
|
137
|
+
* transaction (RT-35: the server holds operations of this node the device lost).
|
|
138
|
+
* @returns Whether the counter moved
|
|
139
|
+
*/
|
|
140
|
+
raiseSequenceFloor?(nodeId: string, floor: number): Promise<boolean>;
|
|
141
|
+
/**
|
|
142
|
+
* Optional: give a local operation a fresh sequence number above `floor` (RT-35: the
|
|
143
|
+
* server refused it with `SEQUENCE_CONFLICT` because it holds another operation of
|
|
144
|
+
* this node, lost locally, under that number). A version-2 operation is re-hashed
|
|
145
|
+
* under a new id; its never-sent dependents (`rewritableDependents`) are rewritten
|
|
146
|
+
* to name the new id, transitively.
|
|
147
|
+
* @returns The renumbered operation, the rewritten dependents and the id mapping,
|
|
148
|
+
* or null when it is not in the log
|
|
149
|
+
*/
|
|
150
|
+
resequenceOperation?(operationId: string, nodeId: string, floor: number, rewritableDependents?: readonly string[]): Promise<{
|
|
151
|
+
operation: Operation;
|
|
152
|
+
dependents: Operation[];
|
|
153
|
+
idMapping: Record<string, string>;
|
|
154
|
+
} | null>;
|
|
155
|
+
/**
|
|
156
|
+
* Optional: take over another local node id's unsynced writes (RT-40). Resolves to a
|
|
157
|
+
* release function when no live tab uses it, or null when one does.
|
|
158
|
+
*/
|
|
159
|
+
claimLocalNode?(nodeId: string): Promise<(() => void) | null>;
|
|
93
160
|
rebaseUnsyncedOperations?(ids: string[], correctedNowMs: number): Promise<{
|
|
94
161
|
operations: Operation[];
|
|
95
162
|
idMapping: Record<string, string>;
|
|
96
163
|
rebasedCount: number;
|
|
97
164
|
}>;
|
|
165
|
+
/**
|
|
166
|
+
* Optional (W7): the server's node ids whose writes win `merge('server-
|
|
167
|
+
* authoritative')` fields, from the handshake response. The store persists them
|
|
168
|
+
* and folds with them (re-folding affected records when the set changes).
|
|
169
|
+
*/
|
|
170
|
+
setAuthoritativeNodeIds?(nodeIds: readonly string[], revokedNodeIds?: readonly string[]): Promise<void>;
|
|
98
171
|
/**
|
|
99
172
|
* Optional: read a record's current materialized field values, used to backfill
|
|
100
173
|
* scope / query-subset fields that a partial update (or a delete) does not carry
|
|
@@ -106,6 +179,35 @@ interface SyncStore {
|
|
|
106
179
|
* @returns the record's fields, or null when it cannot be read.
|
|
107
180
|
*/
|
|
108
181
|
readRecordFields?(collection: string, recordId: string): Promise<Record<string, unknown> | null>;
|
|
182
|
+
/**
|
|
183
|
+
* Optional: whether the local schema defines this collection. Lets the engine tell a
|
|
184
|
+
* merge-decided `'skipped'` (the record is settled, nothing to keep) from a skip
|
|
185
|
+
* because the collection is unknown (the operation must be quarantined and replayed
|
|
186
|
+
* after a schema upgrade). Without it every `'skipped'` result is quarantined.
|
|
187
|
+
*/
|
|
188
|
+
hasCollection?(collection: string): boolean;
|
|
189
|
+
/**
|
|
190
|
+
* Remove a record from this client's materialized authorization view without
|
|
191
|
+
* writing a domain delete to the replicated operation log.
|
|
192
|
+
*/
|
|
193
|
+
applyScopeRetraction?(collection: string, recordId: string): Promise<void>;
|
|
194
|
+
/** Retract every currently materialized row outside a newly accepted scope. */
|
|
195
|
+
applyScopeNarrowing?(scopes: Record<string, Record<string, unknown>>): Promise<Array<{
|
|
196
|
+
collection: string;
|
|
197
|
+
recordId: string;
|
|
198
|
+
}>>;
|
|
199
|
+
/**
|
|
200
|
+
* Optional (W7): the delivery stream caught up (its final batch fully applied).
|
|
201
|
+
* The store retires local-only provisional cascades of remote deletes (RT-69) and
|
|
202
|
+
* drops row snapshots whose history a full resync brought back (RT-68). With
|
|
203
|
+
* `provisionalOnly` it only retires the provisional cascades: a stream batch that is
|
|
204
|
+
* not the end of a resync must not settle row snapshots (RT-93).
|
|
205
|
+
*
|
|
206
|
+
* @returns How many row snapshots were dropped
|
|
207
|
+
*/
|
|
208
|
+
settleAfterCatchUp?(options?: {
|
|
209
|
+
provisionalOnly?: boolean;
|
|
210
|
+
}): Promise<number>;
|
|
109
211
|
}
|
|
110
212
|
|
|
111
213
|
/** Prefix for handshake rejection when client schema version is unsupported. */
|
|
@@ -154,6 +256,19 @@ declare class JsonMessageSerializer implements MessageSerializer {
|
|
|
154
256
|
/**
|
|
155
257
|
* Protobuf-based serializer for sync messages.
|
|
156
258
|
*/
|
|
259
|
+
/**
|
|
260
|
+
* Protobuf message serializer.
|
|
261
|
+
*
|
|
262
|
+
* Lossless by construction (SYNC-9): fields with a native protobuf slot are written there;
|
|
263
|
+
* every other top-level member of the message (fields added after the native mapping,
|
|
264
|
+
* and the bodies of the JSON-shaped awareness and Yjs messages) travels in the `extJson`
|
|
265
|
+
* envelope field (49). Decoding therefore yields exactly what the JSON wire would, for
|
|
266
|
+
* every message type, which a property test over every message type pins.
|
|
267
|
+
*
|
|
268
|
+
* It is not negotiated: the sync engine advertises `['json']` only and the server
|
|
269
|
+
* reports the format its transport actually frames with. Use it explicitly, on both
|
|
270
|
+
* ends of a transport, or not at all.
|
|
271
|
+
*/
|
|
157
272
|
declare class ProtobufMessageSerializer implements MessageSerializer {
|
|
158
273
|
encode(message: SyncMessage): Uint8Array;
|
|
159
274
|
decode(data: string | Uint8Array | ArrayBuffer): SyncMessage;
|
|
@@ -208,18 +323,49 @@ interface WebSocketTransportOptions {
|
|
|
208
323
|
WebSocketImpl?: WebSocketConstructor;
|
|
209
324
|
/** Connection timeout in ms. Defaults to 10000 (10s). */
|
|
210
325
|
connectTimeout?: number;
|
|
326
|
+
/**
|
|
327
|
+
* Also append the auth token to the connection URL as `?token=`. Off by
|
|
328
|
+
* default: the token already travels in the handshake message, and URLs are
|
|
329
|
+
* recorded by reverse proxies, load balancers and access logs. Enable only for an
|
|
330
|
+
* intermediary that authenticates the WebSocket upgrade by query parameter.
|
|
331
|
+
*/
|
|
332
|
+
tokenInUrl?: boolean;
|
|
333
|
+
/**
|
|
334
|
+
* Detect dead connections with the server's application-level heartbeat (LMS #12).
|
|
335
|
+
* The transport advertises support in the handshake; a server that sends
|
|
336
|
+
* heartbeats states their interval, and a connection that then receives nothing
|
|
337
|
+
* for `heartbeatTimeoutFactor` intervals is closed as dead, which starts a
|
|
338
|
+
* reconnect. Browsers cannot observe WebSocket pings, so without this a half-open
|
|
339
|
+
* connection (a phone that changed networks) looks alive until TCP gives up, often
|
|
340
|
+
* minutes later. Defaults to true.
|
|
341
|
+
*/
|
|
342
|
+
heartbeat?: boolean;
|
|
343
|
+
/** Silent intervals tolerated before the connection is declared dead. Defaults to 2.5. */
|
|
344
|
+
heartbeatTimeoutFactor?: number;
|
|
211
345
|
}
|
|
212
346
|
/**
|
|
213
347
|
* WebSocket-based sync transport implementation.
|
|
348
|
+
*
|
|
349
|
+
* Every socket is bound to a generation (SYNC-5): `connect()` closes and detaches the
|
|
350
|
+
* previous socket first, and callbacks of a superseded socket are ignored, so a stale
|
|
351
|
+
* socket closing late can never sever (or feed messages into) the live session.
|
|
214
352
|
*/
|
|
215
353
|
declare class WebSocketTransport implements SyncTransport {
|
|
216
354
|
private ws;
|
|
355
|
+
/** Incremented for every socket; callbacks check theirs is still current. */
|
|
356
|
+
private generation;
|
|
217
357
|
private messageHandler;
|
|
218
358
|
private closeHandler;
|
|
219
359
|
private errorHandler;
|
|
220
360
|
private readonly serializer;
|
|
221
361
|
private readonly WebSocketImpl;
|
|
222
362
|
private readonly connectTimeout;
|
|
363
|
+
private readonly tokenInUrl;
|
|
364
|
+
private readonly heartbeat;
|
|
365
|
+
private readonly heartbeatTimeoutFactor;
|
|
366
|
+
/** Silence (ms) after which the current connection is declared dead; 0 = unarmed. */
|
|
367
|
+
private livenessTimeoutMs;
|
|
368
|
+
private livenessTimer;
|
|
223
369
|
constructor(options?: WebSocketTransportOptions);
|
|
224
370
|
connect(url: string, options?: TransportOptions): Promise<void>;
|
|
225
371
|
disconnect(): Promise<void>;
|
|
@@ -228,8 +374,20 @@ declare class WebSocketTransport implements SyncTransport {
|
|
|
228
374
|
onClose(handler: TransportCloseHandler): void;
|
|
229
375
|
onError(handler: TransportErrorHandler): void;
|
|
230
376
|
isConnected(): boolean;
|
|
377
|
+
/** Close and detach the current socket, if any, without firing the close handler. */
|
|
378
|
+
private releaseSocket;
|
|
379
|
+
/** Start watching for silence once the server stated its heartbeat interval. */
|
|
380
|
+
private armLiveness;
|
|
381
|
+
private touchLiveness;
|
|
382
|
+
private clearLiveness;
|
|
231
383
|
}
|
|
232
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Header carrying the server-issued HTTP sync session id. The server returns it on
|
|
387
|
+
* the response to the first POST (the handshake); the client sends it on every
|
|
388
|
+
* later request, together with its bearer credential.
|
|
389
|
+
*/
|
|
390
|
+
declare const HTTP_SYNC_SESSION_HEADER = "x-kora-session";
|
|
233
391
|
interface HttpLongPollingTransportOptions {
|
|
234
392
|
serializer?: MessageSerializer;
|
|
235
393
|
fetchImpl?: typeof fetch;
|
|
@@ -239,6 +397,13 @@ interface HttpLongPollingTransportOptions {
|
|
|
239
397
|
}
|
|
240
398
|
/**
|
|
241
399
|
* HTTP long-polling transport with optional WebSocket upgrade.
|
|
400
|
+
*
|
|
401
|
+
* The first POST (the handshake) opens a server-side session; the server answers
|
|
402
|
+
* with a high-entropy session id in the `x-kora-session` header. Every later
|
|
403
|
+
* request carries that id and the bearer credential, and the long-poll loop starts
|
|
404
|
+
* only once the id is known. POSTs are sent one at a time, in order. A 401, 403,
|
|
405
|
+
* 404 or 410 means the server-side session is gone: the transport closes so the
|
|
406
|
+
* sync engine reconnects (and re-authenticates) from scratch.
|
|
242
407
|
*/
|
|
243
408
|
declare class HttpLongPollingTransport implements SyncTransport {
|
|
244
409
|
private readonly serializer;
|
|
@@ -255,6 +420,10 @@ declare class HttpLongPollingTransport implements SyncTransport {
|
|
|
255
420
|
private authToken;
|
|
256
421
|
private pollAbort;
|
|
257
422
|
private upgradedTransport;
|
|
423
|
+
/** Server-issued session id; null until the handshake POST is answered. */
|
|
424
|
+
private sessionId;
|
|
425
|
+
/** Serializes POSTs so the server receives messages in send order. */
|
|
426
|
+
private postChain;
|
|
258
427
|
constructor(options?: HttpLongPollingTransportOptions);
|
|
259
428
|
connect(url: string, options?: TransportOptions): Promise<void>;
|
|
260
429
|
disconnect(): Promise<void>;
|
|
@@ -265,8 +434,12 @@ declare class HttpLongPollingTransport implements SyncTransport {
|
|
|
265
434
|
isConnected(): boolean;
|
|
266
435
|
private tryUpgradeToWebSocket;
|
|
267
436
|
private postMessage;
|
|
437
|
+
private startPolling;
|
|
438
|
+
/** The server-side session is gone: stop and report a close so the engine reconnects. */
|
|
439
|
+
private endSession;
|
|
268
440
|
private runPollLoop;
|
|
269
|
-
|
|
441
|
+
/** Accept, bearer credential and (once issued) the session id: sent on every request. */
|
|
442
|
+
private makeHeaders;
|
|
270
443
|
}
|
|
271
444
|
|
|
272
445
|
/**
|
|
@@ -506,6 +679,9 @@ type BlobChunkChannelMessage = {
|
|
|
506
679
|
type: 'blob-chunk-response';
|
|
507
680
|
requestId: string;
|
|
508
681
|
bytes: Uint8Array | null;
|
|
682
|
+
/** The server refused the request for rate; retry after `retryAfterMs` (RT-24). */
|
|
683
|
+
throttled?: boolean;
|
|
684
|
+
retryAfterMs?: number;
|
|
509
685
|
} | {
|
|
510
686
|
type: 'blob-chunk-push';
|
|
511
687
|
hash: string;
|
|
@@ -569,6 +745,122 @@ interface MetricsCollectorConfig {
|
|
|
569
745
|
timeSource?: TimeSource;
|
|
570
746
|
}
|
|
571
747
|
|
|
748
|
+
/**
|
|
749
|
+
* WebCrypto primitives of the key hierarchy (ENC-1, D4b, record format 2):
|
|
750
|
+
*
|
|
751
|
+
* passphrase --PBKDF2-SHA256(salt, iterations)--> KEK (AES-256-GCM, non-extractable)
|
|
752
|
+
* KEK --AES-GCM (AAD: keyring, ring)--> master key (32 random bytes)
|
|
753
|
+
* recovery public key --ECDH P-256, HKDF-SHA256, AES-GCM--> master key (optional)
|
|
754
|
+
* master key --HKDF-SHA256 "data-key-wrap"--> wrapping key (AES-256-GCM)
|
|
755
|
+
* master key --HKDF-SHA256 "record-mac"--> MAC key (HMAC-SHA256)
|
|
756
|
+
* wrapping key --AES-GCM wrap (AAD: keyring, version, keyId)--> data key v1, v2, ...
|
|
757
|
+
* master key --HKDF-SHA256 "master-id"--> master id (names a master a passphrase change retires)
|
|
758
|
+
* recovery anchor key (a data key created WITH the recovery key) --HMAC-SHA256 over
|
|
759
|
+
* (keyring, ring id, key id, recovery public key)--> anchor (16 bytes, in the recovery key)
|
|
760
|
+
*
|
|
761
|
+
* Raw master-key bytes exist only inside the calls that create, re-wrap or open the
|
|
762
|
+
* master key, and are zeroed afterwards. What a device keeps is the two derived keys
|
|
763
|
+
* ({@link MasterKeys}), both non-extractable. Data keys are extractable only for the
|
|
764
|
+
* moment they are wrapped; the copy a device encrypts with is non-extractable.
|
|
765
|
+
*/
|
|
766
|
+
/** Thrown when a key record cannot be opened with the given passphrase or recovery key. */
|
|
767
|
+
declare class KeyUnwrapError extends SyncError {
|
|
768
|
+
constructor(message: string, context?: Record<string, unknown>);
|
|
769
|
+
}
|
|
770
|
+
/** The keys a device derives from (and keeps instead of) a ring's master key. */
|
|
771
|
+
interface MasterKeys {
|
|
772
|
+
/** Wraps and unwraps the ring's data keys. */
|
|
773
|
+
wrapKey: CryptoKey;
|
|
774
|
+
/** Authenticates the whole key record. */
|
|
775
|
+
macKey: CryptoKey;
|
|
776
|
+
/**
|
|
777
|
+
* Public name of the master key (`m-` and 32 hex digits, an HKDF output of the master
|
|
778
|
+
* key): what a record's `retiredMasters` lists after a passphrase change. Reveals
|
|
779
|
+
* nothing about the key; only a holder of the master key can compute it.
|
|
780
|
+
*/
|
|
781
|
+
id: string;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* What a device keeps of an unlocked keyring: the last key record it authenticated and
|
|
786
|
+
* accepted (also the pin: its ring id and revision are the floor for every later
|
|
787
|
+
* record), the passphrase KEK when known, the keys derived from the ring's master key
|
|
788
|
+
* (to authenticate later records and wrap new data keys), and every data key it
|
|
789
|
+
* opened. Keys are NON-EXTRACTABLE CryptoKeys: script on the page can use them while it
|
|
790
|
+
* runs, but cannot read their bytes. A locked keyring keeps the record (the pin) and
|
|
791
|
+
* nothing else.
|
|
792
|
+
*/
|
|
793
|
+
interface CachedKeyring {
|
|
794
|
+
record: WrappedKeyRecord;
|
|
795
|
+
kek: CryptoKey | null;
|
|
796
|
+
/** Derived from the ring's master key; null while locked. */
|
|
797
|
+
master?: MasterKeys | null;
|
|
798
|
+
keys: Array<{
|
|
799
|
+
keyVersion: number;
|
|
800
|
+
keyId: string;
|
|
801
|
+
key: CryptoKey;
|
|
802
|
+
}>;
|
|
803
|
+
/**
|
|
804
|
+
* Ids of master keys a passphrase change retired, from every record this device
|
|
805
|
+
* authenticated (RT-107). Kept when locked: records authenticated only by one of them
|
|
806
|
+
* stay refused across restarts.
|
|
807
|
+
*/
|
|
808
|
+
retiredMasters?: string[];
|
|
809
|
+
/**
|
|
810
|
+
* Ids of master keys this device held when the server served a NEWER revision of the
|
|
811
|
+
* same ring that they do not authenticate (a passphrase change this device could not
|
|
812
|
+
* read yet). Records authenticated only by one of them are not adopted automatically;
|
|
813
|
+
* an explicit `unlock(passphrase)` decides (RT-107).
|
|
814
|
+
*/
|
|
815
|
+
supersededMasters?: string[];
|
|
816
|
+
}
|
|
817
|
+
/** Where a device keeps unlocked keyrings between app starts. */
|
|
818
|
+
interface KeyCache {
|
|
819
|
+
/** Which kind of cache this is (for status and docs). */
|
|
820
|
+
readonly kind: 'indexeddb' | 'memory' | 'none';
|
|
821
|
+
load(id: string): Promise<CachedKeyring | null>;
|
|
822
|
+
save(id: string, entry: CachedKeyring): Promise<void>;
|
|
823
|
+
clear(id: string): Promise<void>;
|
|
824
|
+
}
|
|
825
|
+
/** Keeps unlocked keyrings for the lifetime of the process only. */
|
|
826
|
+
declare class MemoryKeyCache implements KeyCache {
|
|
827
|
+
readonly kind: "memory";
|
|
828
|
+
private readonly entries;
|
|
829
|
+
load(id: string): Promise<CachedKeyring | null>;
|
|
830
|
+
save(id: string, entry: CachedKeyring): Promise<void>;
|
|
831
|
+
clear(id: string): Promise<void>;
|
|
832
|
+
}
|
|
833
|
+
/** Keeps nothing: the device asks for the passphrase on every start. */
|
|
834
|
+
declare class NoKeyCache implements KeyCache {
|
|
835
|
+
readonly kind: "none";
|
|
836
|
+
load(): Promise<CachedKeyring | null>;
|
|
837
|
+
save(): Promise<void>;
|
|
838
|
+
clear(): Promise<void>;
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Keeps unlocked keyrings in IndexedDB (structured clone keeps a CryptoKey
|
|
842
|
+
* non-extractable). Survives reloads, so a device unlocks once and then works offline.
|
|
843
|
+
*/
|
|
844
|
+
declare class IndexedDbKeyCache implements KeyCache {
|
|
845
|
+
private readonly databaseName;
|
|
846
|
+
readonly kind: "indexeddb";
|
|
847
|
+
private db;
|
|
848
|
+
/** @param databaseName - IndexedDB database name (one per app database) */
|
|
849
|
+
constructor(databaseName: string);
|
|
850
|
+
load(id: string): Promise<CachedKeyring | null>;
|
|
851
|
+
save(id: string, entry: CachedKeyring): Promise<void>;
|
|
852
|
+
clear(id: string): Promise<void>;
|
|
853
|
+
private open;
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* The default cache: IndexedDB where it exists (browsers), memory otherwise (Node,
|
|
857
|
+
* tests). See the encryption guide for what each means for offline unlock.
|
|
858
|
+
*
|
|
859
|
+
* @param mode - 'auto' (default), 'indexeddb', 'memory' or 'none'
|
|
860
|
+
* @param databaseName - IndexedDB database name
|
|
861
|
+
*/
|
|
862
|
+
declare function createKeyCache(mode?: 'auto' | 'indexeddb' | 'memory' | 'none', databaseName?: string): KeyCache;
|
|
863
|
+
|
|
572
864
|
/**
|
|
573
865
|
* Thrown when encryption of operation data fails.
|
|
574
866
|
*/
|
|
@@ -583,31 +875,38 @@ declare class DecryptionError extends SyncError {
|
|
|
583
875
|
constructor(message: string, context?: Record<string, unknown>);
|
|
584
876
|
}
|
|
585
877
|
/**
|
|
586
|
-
*
|
|
587
|
-
* end-to-end encryption in the sync layer.
|
|
878
|
+
* End-to-end encryption of operations, envelope v2 (protocol v2; ENC-3, NEW-ENC-1).
|
|
588
879
|
*
|
|
589
|
-
* **
|
|
590
|
-
*
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
* in the {@link EncryptedPayload} so the decryptor can select the correct key.
|
|
597
|
-
* - Unencrypted fields pass through during decryption (backward compatibility).
|
|
880
|
+
* **Envelope.** The plaintext `data`, `previousData` and `atomicOps` move into
|
|
881
|
+
* `op.encrypted` ({@link EncryptedOperationEnvelope}: alg, keyId, keyVersion and one
|
|
882
|
+
* AES-256-GCM ciphertext per member). On the wire `data` is null, or holds only the
|
|
883
|
+
* collection's documented cleartext scope fields (`cleartextFields`), so a schema-aware
|
|
884
|
+
* server stores the operation opaquely and can still evaluate scopes. Metadata (id,
|
|
885
|
+
* nodeId, collection, recordId, timestamp, sequence, causalDeps, schemaVersion) stays in
|
|
886
|
+
* cleartext so the server can route, deduplicate and order.
|
|
598
887
|
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
888
|
+
* **Binding (ENC-3).** Every ciphertext is authenticated with AES-GCM additional data
|
|
889
|
+
* canonical(nodeId, collection, recordId, type, timestamp, sequenceNumber, field,
|
|
890
|
+
* keyVersion, hashVersion): a ciphertext moved to another operation, record or member,
|
|
891
|
+
* or an envelope whose metadata was rewritten, fails authentication. `data` and
|
|
892
|
+
* `previousData` are always encrypted, even when null, so a delete is authenticated too.
|
|
893
|
+
* The operation id (the version-2 content hash of the PLAINTEXT) is verified after
|
|
894
|
+
* decryption by the sync engine (`verify-inbound.ts`).
|
|
895
|
+
*
|
|
896
|
+
* **Plaintext.** With encryption enabled an operation without an envelope is refused
|
|
897
|
+
* (a server must not be able to inject unauthenticated writes), unless the config opens
|
|
898
|
+
* a migration window (`allowPlaintextMigration`). Protocol-1 payloads (ciphertext inside
|
|
899
|
+
* `data`, no binding) are refused the same way.
|
|
605
900
|
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
901
|
+
* **Key id.** The envelope names its key material (`keyId`: the keyring's random key
|
|
902
|
+
* id, ENC-1), so a device holding different material reports a diagnosable
|
|
903
|
+
* `KEY_ID_MISMATCH` rather than a bare authentication failure.
|
|
608
904
|
*
|
|
609
|
-
*
|
|
610
|
-
*
|
|
905
|
+
* @example
|
|
906
|
+
* ```typescript
|
|
907
|
+
* const encryptor = SyncEncryptor.fromKeys([{ version: 1, key: dataKey, keyId }])
|
|
908
|
+
* const sealed = await encryptor.encryptOperation(operation)
|
|
909
|
+
* const opened = await encryptor.decryptOperation(sealed)
|
|
611
910
|
* ```
|
|
612
911
|
*/
|
|
613
912
|
declare class SyncEncryptor {
|
|
@@ -618,23 +917,28 @@ declare class SyncEncryptor {
|
|
|
618
917
|
private readonly keys;
|
|
619
918
|
/** The current key version used for encryption. */
|
|
620
919
|
private currentVersion;
|
|
920
|
+
/** Fingerprint of each key version's material (cached). */
|
|
921
|
+
private readonly keyIds;
|
|
922
|
+
private readonly cleartextFields;
|
|
923
|
+
private readonly allowPlaintextMigration;
|
|
621
924
|
private constructor();
|
|
622
925
|
/**
|
|
623
|
-
* Creates a SyncEncryptor from a
|
|
926
|
+
* Creates a SyncEncryptor from a passphrase and an explicit, shared salt.
|
|
624
927
|
*
|
|
625
|
-
*
|
|
626
|
-
*
|
|
928
|
+
* Low-level: apps use the keyring (`createApp` with `sync.encryption`), which
|
|
929
|
+
* distributes random data keys wrapped by the passphrase. This derives the data key
|
|
930
|
+
* directly from the passphrase, so every device must pass the same salt; there is
|
|
931
|
+
* no random-salt default any more (ENC-1: a per-process random salt made every
|
|
932
|
+
* device derive a different key).
|
|
627
933
|
*
|
|
628
|
-
* @param config - Encryption configuration with passphrase
|
|
629
|
-
* @param salt -
|
|
630
|
-
*
|
|
631
|
-
* @param iterations - Optional PBKDF2 iteration count. Defaults to the
|
|
632
|
-
* production-strength value. Lower it only in tests.
|
|
934
|
+
* @param config - Encryption configuration with the passphrase in `key`
|
|
935
|
+
* @param salt - The key-derivation salt, shared by every device (16 bytes or more)
|
|
936
|
+
* @param iterations - Optional PBKDF2 iteration count. Lower it only in tests.
|
|
633
937
|
* @returns A configured SyncEncryptor instance
|
|
634
|
-
* @throws {EncryptionError} If configuration is invalid
|
|
938
|
+
* @throws {EncryptionError} If configuration is invalid or the salt is missing
|
|
635
939
|
* @throws {KeyDerivationError} If key derivation fails
|
|
636
940
|
*/
|
|
637
|
-
static create(config: SyncEncryptionConfig, salt
|
|
941
|
+
static create(config: SyncEncryptionConfig, salt: Uint8Array, iterations?: number): Promise<SyncEncryptor>;
|
|
638
942
|
/**
|
|
639
943
|
* Creates a SyncEncryptor from pre-derived versioned keys.
|
|
640
944
|
*
|
|
@@ -642,10 +946,11 @@ declare class SyncEncryptor {
|
|
|
642
946
|
* or when you have already derived the keys externally.
|
|
643
947
|
*
|
|
644
948
|
* @param versionedKeys - Array of versioned keys. The highest version is used for encryption.
|
|
949
|
+
* @param options - Cleartext scope fields and the plaintext migration window
|
|
645
950
|
* @returns A configured SyncEncryptor instance
|
|
646
951
|
* @throws {EncryptionError} If no keys are provided
|
|
647
952
|
*/
|
|
648
|
-
static fromKeys(versionedKeys: VersionedKey[]): SyncEncryptor;
|
|
953
|
+
static fromKeys(versionedKeys: VersionedKey[], options?: EncryptorOptions): SyncEncryptor;
|
|
649
954
|
/**
|
|
650
955
|
* Add a new key version for key rotation.
|
|
651
956
|
*
|
|
@@ -657,57 +962,55 @@ declare class SyncEncryptor {
|
|
|
657
962
|
* @throws {EncryptionError} If the key version already exists
|
|
658
963
|
*/
|
|
659
964
|
addKey(key: VersionedKey): void;
|
|
965
|
+
/** The key registered under an explicit key id (keyring keys), if any. */
|
|
966
|
+
private keyById;
|
|
660
967
|
/**
|
|
661
968
|
* Get the current encryption key version number.
|
|
662
969
|
*/
|
|
663
970
|
getCurrentKeyVersion(): number;
|
|
664
971
|
/**
|
|
665
|
-
*
|
|
972
|
+
* The key id written into envelopes for a key version: the keyring's random key id,
|
|
973
|
+
* or for a passphrase-derived key a fingerprint of its salt (never the key itself).
|
|
666
974
|
*
|
|
667
|
-
*
|
|
668
|
-
|
|
975
|
+
* @param version - Key version (defaults to the current one)
|
|
976
|
+
*/
|
|
977
|
+
getKeyId(version?: number): Promise<string>;
|
|
978
|
+
/**
|
|
979
|
+
* Seal an operation into an envelope v2.
|
|
669
980
|
*
|
|
670
|
-
*
|
|
981
|
+
* Returns a new operation (operations are immutable): `encrypted` holds the
|
|
982
|
+
* ciphertexts, `data` is null or the collection's cleartext scope fields, and
|
|
983
|
+
* `previousData`/`atomicOps` are removed. An operation that already carries an
|
|
984
|
+
* envelope is returned unchanged.
|
|
671
985
|
*
|
|
672
|
-
* @param operation - The operation
|
|
673
|
-
* @returns
|
|
986
|
+
* @param operation - The plaintext operation
|
|
987
|
+
* @returns The sealed operation
|
|
674
988
|
* @throws {EncryptionError} If encryption fails
|
|
675
989
|
*/
|
|
676
990
|
encryptOperation(operation: Operation): Promise<Operation>;
|
|
677
991
|
/**
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
* Returns a new Operation with the original field values restored.
|
|
681
|
-
* The original operation is not mutated.
|
|
992
|
+
* Open an envelope v2 and restore the plaintext `data`, `previousData` and
|
|
993
|
+
* `atomicOps`. The returned operation has no `encrypted` member.
|
|
682
994
|
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
* @throws {DecryptionError} If decryption fails (wrong key, tampered data, unsupported version)
|
|
995
|
+
* @param operation - The sealed operation
|
|
996
|
+
* @returns The plaintext operation (its id is NOT verified here; the caller does)
|
|
997
|
+
* @throws {DecryptionError} On a plaintext operation (unless the migration window
|
|
998
|
+
* is open), a protocol-1 payload, an unknown key, a key-id mismatch, or a
|
|
999
|
+
* ciphertext that fails authentication (tampered, transplanted, wrong key)
|
|
689
1000
|
*/
|
|
690
1001
|
decryptOperation(operation: Operation): Promise<Operation>;
|
|
691
1002
|
/**
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
* Same as {@link encryptOperation} but works with the wire-format
|
|
695
|
-
* {@link SerializedOperation} type used by the serializer.
|
|
1003
|
+
* Same as {@link encryptOperation} for the wire form.
|
|
696
1004
|
*
|
|
697
1005
|
* @param serialized - The serialized operation to encrypt
|
|
698
|
-
* @returns
|
|
699
|
-
* @throws {EncryptionError} If encryption fails
|
|
1006
|
+
* @returns The sealed serialized operation
|
|
700
1007
|
*/
|
|
701
1008
|
encryptSerializedOperation(serialized: SerializedOperation): Promise<SerializedOperation>;
|
|
702
1009
|
/**
|
|
703
|
-
*
|
|
704
|
-
*
|
|
705
|
-
* Same as {@link decryptOperation} but works with the wire-format
|
|
706
|
-
* {@link SerializedOperation} type used by the serializer.
|
|
1010
|
+
* Same as {@link decryptOperation} for the wire form.
|
|
707
1011
|
*
|
|
708
1012
|
* @param serialized - The serialized operation to decrypt
|
|
709
|
-
* @returns
|
|
710
|
-
* @throws {DecryptionError} If decryption fails
|
|
1013
|
+
* @returns The plaintext serialized operation
|
|
711
1014
|
*/
|
|
712
1015
|
decryptSerializedOperation(serialized: SerializedOperation): Promise<SerializedOperation>;
|
|
713
1016
|
/**
|
|
@@ -725,23 +1028,425 @@ declare class SyncEncryptor {
|
|
|
725
1028
|
*/
|
|
726
1029
|
decryptBatch(operations: Operation[]): Promise<Operation[]>;
|
|
727
1030
|
/**
|
|
728
|
-
* Check if
|
|
1031
|
+
* Check if an operation `data` field holds a protocol-1 encrypted payload (the
|
|
1032
|
+
* pre-envelope format, refused since protocol v2).
|
|
729
1033
|
*
|
|
730
1034
|
* @param field - An operation's `data` or `previousData` field
|
|
731
|
-
* @returns true if the field contains an encrypted payload
|
|
732
1035
|
*/
|
|
733
1036
|
static isEncryptedPayload(field: Record<string, unknown> | null): boolean;
|
|
734
|
-
|
|
735
|
-
|
|
1037
|
+
/**
|
|
1038
|
+
* Whether a plaintext operation touches only data the server can already read: its
|
|
1039
|
+
* `data` (and, except for a delete, its `previousData`) names only the collection's
|
|
1040
|
+
* cleartext scope fields, and it has no atomic ops. The sync engine accepts such an
|
|
1041
|
+
* operation from a server-authored node (a cascade, a set-null of a cleartext
|
|
1042
|
+
* reference, a constraint correction of a cleartext field) under end-to-end
|
|
1043
|
+
* encryption: the server cannot seal it (it has no key), and it reveals or forges
|
|
1044
|
+
* nothing the server does not already hold in cleartext. Any sealed field makes it
|
|
1045
|
+
* false, so the operation is refused as plaintext.
|
|
1046
|
+
*
|
|
1047
|
+
* @param operation - A delivered operation without an envelope
|
|
1048
|
+
*/
|
|
1049
|
+
isCleartextOnly(operation: Operation): boolean;
|
|
1050
|
+
/** Whether an operation carries an envelope v2. */
|
|
1051
|
+
static isEncryptedOperation(operation: {
|
|
1052
|
+
encrypted?: unknown;
|
|
1053
|
+
}): boolean;
|
|
1054
|
+
private seal;
|
|
1055
|
+
private open;
|
|
1056
|
+
/** The collection's documented cleartext scope fields, or null when none. */
|
|
1057
|
+
private cleartextScope;
|
|
1058
|
+
private encryptMember;
|
|
1059
|
+
private decryptMember;
|
|
1060
|
+
}
|
|
1061
|
+
/** Options of {@link SyncEncryptor.fromKeys}. */
|
|
1062
|
+
interface EncryptorOptions {
|
|
1063
|
+
/** Per collection, the data fields kept in cleartext beside the envelope (for scopes). */
|
|
1064
|
+
cleartextFields?: Readonly<Record<string, readonly string[]>>;
|
|
1065
|
+
/** Accept plaintext (envelope-less) inbound operations during a migration window. */
|
|
1066
|
+
allowPlaintextMigration?: boolean;
|
|
736
1067
|
}
|
|
737
1068
|
/**
|
|
738
|
-
* Check if a field value contains
|
|
1069
|
+
* Check if a field value contains a protocol-1 encrypted payload (ciphertext inside
|
|
1070
|
+
* `data`). Protocol v2 refuses such payloads; the envelope lives in `op.encrypted`.
|
|
739
1071
|
*
|
|
740
1072
|
* @param field - An operation's `data` or `previousData` field
|
|
741
|
-
* @returns true if the field contains
|
|
1073
|
+
* @returns true if the field contains a protocol-1 encrypted payload
|
|
742
1074
|
*/
|
|
743
1075
|
declare function isEncryptedPayload(field: Record<string, unknown> | null): boolean;
|
|
744
1076
|
|
|
1077
|
+
/** Lock state of an encryption keyring. */
|
|
1078
|
+
type EncryptionLockState = 'locked' | 'unlocking' | 'unlocked' | 'error';
|
|
1079
|
+
/** Why a keyring is locked or in error (or, while unlocked, what needs attention). */
|
|
1080
|
+
type EncryptionStatusCode = 'NO_PASSPHRASE' | 'AWAITING_SERVER' | 'LOCKED_BY_APP' | 'WRONG_PASSPHRASE' | 'PASSPHRASE_REQUIRED' | 'KEY_RECORD_INVALID' | 'KEY_RECORD_ROLLBACK' | 'KEY_RECORD_MISSING' | 'KEY_RING_FORK' | 'KEY_SERVICE_FORBIDDEN' | 'KEY_SERVICE_UNSUPPORTED' | 'RECOVERY_FAILED';
|
|
1081
|
+
/** Status of an encryption keyring, as `app.encryption.getStatus()` reports it. */
|
|
1082
|
+
interface EncryptionStatus {
|
|
1083
|
+
state: EncryptionLockState;
|
|
1084
|
+
keyring: string;
|
|
1085
|
+
/** Key version new operations are encrypted with, or null while locked. */
|
|
1086
|
+
keyVersion: number | null;
|
|
1087
|
+
/** Key id of that version, or null while locked. */
|
|
1088
|
+
keyId: string | null;
|
|
1089
|
+
/** Every key version this device can decrypt. */
|
|
1090
|
+
availableVersions: number[];
|
|
1091
|
+
/** Where unlocked keys are kept between starts. */
|
|
1092
|
+
cache: KeyCache['kind'];
|
|
1093
|
+
/**
|
|
1094
|
+
* Set while locked or in error. While `unlocked`, `PASSPHRASE_REQUIRED` means the
|
|
1095
|
+
* passphrase changed on another device: this device keeps working with the keys it
|
|
1096
|
+
* holds, and needs the current passphrase for key management.
|
|
1097
|
+
*/
|
|
1098
|
+
code?: EncryptionStatusCode;
|
|
1099
|
+
message?: string;
|
|
1100
|
+
}
|
|
1101
|
+
/** The server's answer to a key-record fetch or write. */
|
|
1102
|
+
interface KeyServiceReply {
|
|
1103
|
+
/** 'conflict': the write lost a compare-and-set; `record` is the current one. */
|
|
1104
|
+
status: 'ok' | 'conflict';
|
|
1105
|
+
record: WrappedKeyRecord | null;
|
|
1106
|
+
/**
|
|
1107
|
+
* With no record: key ids the server's stored operations of this owner are encrypted
|
|
1108
|
+
* with (a sample). Non-empty means encrypted history exists, so the record was lost
|
|
1109
|
+
* and a device that holds the ring must re-upload it; nobody may start a new ring.
|
|
1110
|
+
*/
|
|
1111
|
+
knownKeyIds?: string[];
|
|
1112
|
+
}
|
|
1113
|
+
/** How a keyring reaches the server's key service (the sync connection). */
|
|
1114
|
+
interface KeyServiceChannel {
|
|
1115
|
+
fetch(keyring: string): Promise<KeyServiceReply>;
|
|
1116
|
+
put(keyring: string, record: WrappedKeyRecord, expectedRevision: number): Promise<KeyServiceReply>;
|
|
1117
|
+
}
|
|
1118
|
+
/** Options of {@link EncryptionKeyring}. */
|
|
1119
|
+
interface EncryptionKeyringOptions {
|
|
1120
|
+
/** Keyring name (one per user by default). */
|
|
1121
|
+
keyring?: string;
|
|
1122
|
+
/** Passphrase source; without one the keyring starts locked until `unlock()`. */
|
|
1123
|
+
passphrase?: string | (() => Promise<string>);
|
|
1124
|
+
/** Where unlocked keys are kept. Defaults to memory (the app picks IndexedDB in browsers). */
|
|
1125
|
+
cache?: KeyCache;
|
|
1126
|
+
/**
|
|
1127
|
+
* PBKDF2 iterations for new records, and the minimum accepted from the server
|
|
1128
|
+
* (a server cannot downgrade the KDF). Defaults to 600,000. Lower only in tests.
|
|
1129
|
+
*/
|
|
1130
|
+
kdfIterations?: number;
|
|
1131
|
+
/** Encryptor options (cleartext scope fields, plaintext migration window). */
|
|
1132
|
+
encryptor?: EncryptorOptions;
|
|
1133
|
+
}
|
|
1134
|
+
/** Thrown by keyring management calls. `context.code` names the reason. */
|
|
1135
|
+
declare class EncryptionKeyError extends SyncError {
|
|
1136
|
+
constructor(message: string, context: Record<string, unknown> & {
|
|
1137
|
+
code: string;
|
|
1138
|
+
});
|
|
1139
|
+
/** The reason code (`context.code`), e.g. WRONG_PASSPHRASE. */
|
|
1140
|
+
get keyCode(): string;
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* A device's view of one encryption keyring (ENC-1, decision D4b; record format 2): the
|
|
1144
|
+
* server-stored, passphrase-wrapped ring, opened and authenticated locally.
|
|
1145
|
+
*
|
|
1146
|
+
* - **Shared key material.** Every device of a user opens the same record, so every
|
|
1147
|
+
* device holds the same data keys and decrypts everything.
|
|
1148
|
+
* - **Authenticated record.** The record carries an HMAC under a key derived from the
|
|
1149
|
+
* ring's master key; a device acts only on records it verified (RT-95). The server
|
|
1150
|
+
* can store, withhold or replay records, nothing more.
|
|
1151
|
+
* - **Forward only.** A device pins the ring id and highest revision it accepted and
|
|
1152
|
+
* refuses a lower revision (`KEY_RECORD_ROLLBACK`, RT-96), keeping its keys and
|
|
1153
|
+
* re-uploading its newer record when connected.
|
|
1154
|
+
* - **Writes re-read.** Every compare-and-set retry adopts the server's current record
|
|
1155
|
+
* first and wraps under that record's master key only (RT-97).
|
|
1156
|
+
* - **Lost records.** A device holding the ring re-uploads it unchanged; a new device
|
|
1157
|
+
* that finds no record but encrypted history waits (`KEY_RECORD_MISSING`); two rings
|
|
1158
|
+
* that exist anyway are merged by the first device holding both (RT-104).
|
|
1159
|
+
* - **Offline.** Once unlocked, keys are cached (non-extractable) and the keyring needs
|
|
1160
|
+
* no server; a cached record also lets `unlock()` work offline.
|
|
1161
|
+
*/
|
|
1162
|
+
declare class EncryptionKeyring {
|
|
1163
|
+
readonly name: string;
|
|
1164
|
+
private passphraseSource;
|
|
1165
|
+
private readonly cache;
|
|
1166
|
+
private readonly kdfIterations;
|
|
1167
|
+
private readonly encryptorOptions;
|
|
1168
|
+
private loadedPrincipal;
|
|
1169
|
+
/** The last record this device authenticated and accepted: the pin. */
|
|
1170
|
+
private record;
|
|
1171
|
+
/** Keys derived from the pinned record's master key. */
|
|
1172
|
+
private master;
|
|
1173
|
+
private kek;
|
|
1174
|
+
/** Salt (base64) the KEK was derived with. */
|
|
1175
|
+
private kekSalt;
|
|
1176
|
+
/** Held data keys by key id. Invariant: every held key id is in `record`. */
|
|
1177
|
+
private readonly dataKeys;
|
|
1178
|
+
/**
|
|
1179
|
+
* Ids of master keys a passphrase change retired, from every record this device
|
|
1180
|
+
* authenticated (persisted with the cache): a record authenticated only by one of
|
|
1181
|
+
* them is refused (RT-107).
|
|
1182
|
+
*/
|
|
1183
|
+
private readonly retiredMasters;
|
|
1184
|
+
/**
|
|
1185
|
+
* Ids of master keys this device held when the server served a newer revision of the
|
|
1186
|
+
* same ring that they do not authenticate (most likely a passphrase change made
|
|
1187
|
+
* elsewhere). A record authenticated only by one of them is not adopted without an
|
|
1188
|
+
* explicit `unlock(passphrase)`: after a genuine change, no later genuine record is
|
|
1189
|
+
* authenticated by the old master key (RT-107).
|
|
1190
|
+
*/
|
|
1191
|
+
private readonly supersededMasters;
|
|
1192
|
+
private encryptor;
|
|
1193
|
+
private status;
|
|
1194
|
+
private readonly listeners;
|
|
1195
|
+
private lockedByApp;
|
|
1196
|
+
private pendingPassphrase;
|
|
1197
|
+
/** A recovery requested while offline, run at the next handshake. */
|
|
1198
|
+
private pendingRecovery;
|
|
1199
|
+
/** The configured passphrase that failed for a salt (not retried for that salt). */
|
|
1200
|
+
private failedSource;
|
|
1201
|
+
/** The current lock is a server-side condition a later session can clear (no user action). */
|
|
1202
|
+
private retryable;
|
|
1203
|
+
/** startNewKeyring() was called: a missing record with encrypted history may be replaced. */
|
|
1204
|
+
private allowNewRing;
|
|
1205
|
+
private failedUnlocks;
|
|
1206
|
+
private nextUnlockAt;
|
|
1207
|
+
private mutex;
|
|
1208
|
+
constructor(options?: EncryptionKeyringOptions);
|
|
1209
|
+
/** The encryptor of the unlocked keyring, or null while locked. */
|
|
1210
|
+
getEncryptor(): SyncEncryptor | null;
|
|
1211
|
+
/** Current status. */
|
|
1212
|
+
getStatus(): EncryptionStatus;
|
|
1213
|
+
/** Whether this device holds the data key with this key id. */
|
|
1214
|
+
holdsKeyId(keyId: string): boolean;
|
|
1215
|
+
/**
|
|
1216
|
+
* Whether the current lock is a server-side condition (a rolled-back, missing,
|
|
1217
|
+
* malformed or forked record) that a later sync session can clear without the user:
|
|
1218
|
+
* sync should reconnect with backoff rather than wait for `unlock()`.
|
|
1219
|
+
*/
|
|
1220
|
+
isRetryableLock(): boolean;
|
|
1221
|
+
/** Subscribe to status changes. Returns an unsubscribe function. */
|
|
1222
|
+
onStatusChange(listener: (status: EncryptionStatus) => void): () => void;
|
|
1223
|
+
/**
|
|
1224
|
+
* Whether sync may connect: the keyring is unlocked, or a passphrase is available to
|
|
1225
|
+
* open (or create) the server record during the handshake.
|
|
1226
|
+
*/
|
|
1227
|
+
canConnect(): boolean;
|
|
1228
|
+
/**
|
|
1229
|
+
* Load the cached keyring of a principal (the signed-in user, or null). A principal
|
|
1230
|
+
* change drops everything held for the previous one first, so one user's keys never
|
|
1231
|
+
* encrypt another user's operations.
|
|
1232
|
+
*/
|
|
1233
|
+
load(principal: string | null): Promise<EncryptionStatus>;
|
|
1234
|
+
/**
|
|
1235
|
+
* Open the keyring against the server's record during a sync handshake: fetch it,
|
|
1236
|
+
* create it on a user's first device, adopt a newer revision, re-upload a lost or
|
|
1237
|
+
* rolled-back one, or merge a forked ring.
|
|
1238
|
+
*
|
|
1239
|
+
* @returns 'ready' when unlocked, 'locked' when a passphrase is needed (or wrong), or
|
|
1240
|
+
* when the server's record must change first ({@link isRetryableLock})
|
|
1241
|
+
* @throws When the key service fails (the session is retried later)
|
|
1242
|
+
*/
|
|
1243
|
+
synchronize(channel: KeyServiceChannel, principal: string | null): Promise<'ready' | 'locked'>;
|
|
1244
|
+
/**
|
|
1245
|
+
* Adopt a record the server pushed (another device rotated keys, changed the
|
|
1246
|
+
* passphrase or set up recovery). Without a connection to write to, a rollback or a
|
|
1247
|
+
* fork is reported and resolved at the next handshake.
|
|
1248
|
+
*
|
|
1249
|
+
* @returns 'ready' when still unlocked, 'locked' when the passphrase is needed or the
|
|
1250
|
+
* record was refused
|
|
1251
|
+
*/
|
|
1252
|
+
adoptPushed(record: WrappedKeyRecord): Promise<'ready' | 'locked'>;
|
|
1253
|
+
/**
|
|
1254
|
+
* Unlock with a passphrase. With a cached record this works offline; otherwise the
|
|
1255
|
+
* passphrase is kept until the next handshake opens (or creates) the server record,
|
|
1256
|
+
* and the returned promise settles then (pass `channel` when connected).
|
|
1257
|
+
*
|
|
1258
|
+
* Repeated wrong passphrases are slowed down on the device (backoff after three).
|
|
1259
|
+
* This is a usability guard, not a security boundary: whoever holds the record can
|
|
1260
|
+
* try passphrases offline, so passphrase strength and PBKDF2 are the protection.
|
|
1261
|
+
*
|
|
1262
|
+
* @throws {EncryptionKeyError} WRONG_PASSPHRASE, UNLOCK_THROTTLED, KEY_RECORD_MISSING
|
|
1263
|
+
*/
|
|
1264
|
+
unlock(passphrase: string, channel: KeyServiceChannel | null): Promise<EncryptionStatus>;
|
|
1265
|
+
/**
|
|
1266
|
+
* Lock: forget the data keys, the master keys and the KEK on this device (cache
|
|
1267
|
+
* included). Sync stops until `unlock()`; local data stays readable (it is not
|
|
1268
|
+
* encrypted at rest by this layer). The pinned record is kept (it is the rollback
|
|
1269
|
+
* floor). The configured passphrase is not used again until `unlock()`.
|
|
1270
|
+
*/
|
|
1271
|
+
lock(): Promise<EncryptionStatus>;
|
|
1272
|
+
/**
|
|
1273
|
+
* Rotate: create a new data key version. New operations use it; old versions stay
|
|
1274
|
+
* in the record so history decrypts. Needs the server (compare-and-set). Each retry
|
|
1275
|
+
* adopts the server's current record first and wraps under ITS master key only.
|
|
1276
|
+
*/
|
|
1277
|
+
rotate(channel: KeyServiceChannel): Promise<EncryptionStatus>;
|
|
1278
|
+
/**
|
|
1279
|
+
* Change the passphrase: a NEW master key, sealed under a KEK derived from the new
|
|
1280
|
+
* passphrase and a new salt, re-wraps every data key version (and the recovery wrap,
|
|
1281
|
+
* if any). No operation is re-encrypted. The record lists the old master key as
|
|
1282
|
+
* retired (`retiredMasters`): a device that authenticated this record (or a later
|
|
1283
|
+
* one) refuses every record authenticated only by the old master key, however it is
|
|
1284
|
+
* opened. Other devices keep working with their cached data keys (status
|
|
1285
|
+
* `PASSPHRASE_REQUIRED` while unlocked); they need the new passphrase for a key they
|
|
1286
|
+
* do not hold and for key management. A device that was served the new record but
|
|
1287
|
+
* could not read it no longer adopts, without an explicit `unlock()`, a record that
|
|
1288
|
+
* only its old master key authenticates.
|
|
1289
|
+
*
|
|
1290
|
+
* **A passphrase change alone does not contain a leaked passphrase.** The ring is a
|
|
1291
|
+
* shared secret: a device that still holds only the OLD master key cannot tell a
|
|
1292
|
+
* record the server forged under it (with a data key of the attacker's) from a
|
|
1293
|
+
* genuine one, and the server decides which records it forwards. After a suspected
|
|
1294
|
+
* leak: change the passphrase, `rotate()`, call `enableRecovery()` again (a recovery
|
|
1295
|
+
* key made before the change can be steered to a ring the old passphrase built) and
|
|
1296
|
+
* re-unlock EVERY other device with the new passphrase (`lock()`, then
|
|
1297
|
+
* `unlock(newPassphrase)`, and update any configured `key`; never type the old
|
|
1298
|
+
* passphrase again: an explicit unlock with it is the user's decision). A device that
|
|
1299
|
+
* is not re-unlocked, or an old recovery key that is still used, remains exposed.
|
|
1300
|
+
*
|
|
1301
|
+
* @param newPassphrase - The new passphrase
|
|
1302
|
+
* @param channel - The key service
|
|
1303
|
+
* @param currentPassphrase - Optional: verified against the record first
|
|
1304
|
+
*/
|
|
1305
|
+
changePassphrase(newPassphrase: string, channel: KeyServiceChannel, currentPassphrase?: string): Promise<EncryptionStatus>;
|
|
1306
|
+
/**
|
|
1307
|
+
* Set up (or replace) the recovery key: the ring's master key is also wrapped to a
|
|
1308
|
+
* new recovery public key, and the private half is returned ONCE. Store it offline;
|
|
1309
|
+
* it recovers the data after a lost passphrase. Without it, a lost passphrase means
|
|
1310
|
+
* the encrypted data is unrecoverable. Needs the passphrase key of the CURRENT
|
|
1311
|
+
* record (the recovery block is authenticated with the record, so only a holder of
|
|
1312
|
+
* the master key can add or replace it).
|
|
1313
|
+
*
|
|
1314
|
+
* The same write adds a new data key version (a rotation) that anchors the recovery
|
|
1315
|
+
* key to this ring: `recover()` accepts only a ring holding that key, which no older
|
|
1316
|
+
* passphrase ever opened (RT-107). After a suspected passphrase leak, call this again
|
|
1317
|
+
* once the passphrase was changed, and destroy the previous recovery key.
|
|
1318
|
+
*
|
|
1319
|
+
* @returns The recovery key (`kora-rk3-<key>.<anchor>`)
|
|
1320
|
+
*/
|
|
1321
|
+
enableRecovery(channel: KeyServiceChannel): Promise<string>;
|
|
1322
|
+
/**
|
|
1323
|
+
* Recover after a lost passphrase: open the ring's master key with the recovery key,
|
|
1324
|
+
* verify the whole record with it, and seal the master key under a new passphrase.
|
|
1325
|
+
* Unlocks this device. Without a channel (sync not connected) the recovery runs at
|
|
1326
|
+
* the next handshake. Devices that hold the ring keep working without a prompt (the
|
|
1327
|
+
* master key is unchanged).
|
|
1328
|
+
*/
|
|
1329
|
+
recover(recoveryKey: string, newPassphrase: string, channel: KeyServiceChannel | null): Promise<EncryptionStatus>;
|
|
1330
|
+
/**
|
|
1331
|
+
* Last resort after the server lost the key record and no device that holds the ring
|
|
1332
|
+
* will come back (`KEY_RECORD_MISSING`): allow this device to create a new ring.
|
|
1333
|
+
* Operations encrypted under the lost ring stay unreadable on this device. If a device
|
|
1334
|
+
* holding the old ring reconnects later, it merges both rings and the history becomes
|
|
1335
|
+
* readable again. Without a channel (sync is not connected: a missing record ends the
|
|
1336
|
+
* session) the ring is created at the next handshake.
|
|
1337
|
+
*
|
|
1338
|
+
* @throws {EncryptionKeyError} KEY_RECORD_EXISTS when the server has a record (unlock it)
|
|
1339
|
+
*/
|
|
1340
|
+
startNewKeyring(channel: KeyServiceChannel | null): Promise<EncryptionStatus>;
|
|
1341
|
+
private recoverLocked;
|
|
1342
|
+
private loadLocked;
|
|
1343
|
+
/**
|
|
1344
|
+
* No record on the server. A device that holds a ring re-uploads it unchanged (same
|
|
1345
|
+
* revision and MAC, so every device's pin still accepts it). A device without one
|
|
1346
|
+
* creates the first ring, unless the server reports encrypted history: then the
|
|
1347
|
+
* record was lost, and it waits for a device that holds the ring.
|
|
1348
|
+
*/
|
|
1349
|
+
private createOrRestoreLocked;
|
|
1350
|
+
/** Create a brand-new ring (the user's first device). */
|
|
1351
|
+
private createRingLocked;
|
|
1352
|
+
/**
|
|
1353
|
+
* Accept a server record, after authenticating it.
|
|
1354
|
+
*
|
|
1355
|
+
* - Lower revision of the pinned ring: refused (rollback); re-upload the pin.
|
|
1356
|
+
* - Authenticated by the held master key, or by the passphrase (explicit, else the
|
|
1357
|
+
* configured one): forward when it keeps every key this device holds, else merge.
|
|
1358
|
+
* - Not authenticated: nothing is adopted. A device that holds every key of a newer
|
|
1359
|
+
* revision of its ring keeps working (the passphrase changed elsewhere); otherwise
|
|
1360
|
+
* it needs the passphrase.
|
|
1361
|
+
*/
|
|
1362
|
+
private adoptLocked;
|
|
1363
|
+
/** An authenticated record: move forward to it, or merge it with the held ring. */
|
|
1364
|
+
private acceptLocked;
|
|
1365
|
+
/** A lower revision of the pinned ring: refuse it, keep the keys, re-upload the pin. */
|
|
1366
|
+
private rollbackLocked;
|
|
1367
|
+
/**
|
|
1368
|
+
* A record authenticated only by a master key a passphrase change retired: whoever
|
|
1369
|
+
* wrote it knew an OLD passphrase (or master key), not the current one (RT-107).
|
|
1370
|
+
*/
|
|
1371
|
+
private refuseRetired;
|
|
1372
|
+
/**
|
|
1373
|
+
* A record authenticated only by a master key this device held when the server served
|
|
1374
|
+
* a newer revision of the ring that the key does not authenticate (RT-107).
|
|
1375
|
+
*/
|
|
1376
|
+
private refuseSuperseded;
|
|
1377
|
+
/**
|
|
1378
|
+
* Whether `master` was superseded on this device. An explicit `unlock(passphrase)` is
|
|
1379
|
+
* the user's decision and clears the mark (a server that served a junk revision can
|
|
1380
|
+
* delay, never lock out, a device).
|
|
1381
|
+
*/
|
|
1382
|
+
private isSupersededMaster;
|
|
1383
|
+
/** A record this device cannot authenticate: adopt nothing. */
|
|
1384
|
+
private unverifiableLocked;
|
|
1385
|
+
/**
|
|
1386
|
+
* Merge the held ring into `base` (the server's current record, which survives: the
|
|
1387
|
+
* server only accepts append-only successors of what it stores). Keys this device
|
|
1388
|
+
* holds that `base` lacks are appended as new versions; operations keep naming them
|
|
1389
|
+
* by key id. The result has a higher revision than both and is written with
|
|
1390
|
+
* compare-and-set; a lost race adopts (or merges) the winner.
|
|
1391
|
+
*/
|
|
1392
|
+
private mergeLocked;
|
|
1393
|
+
/**
|
|
1394
|
+
* Re-wrap the held data keys `base` lacks under `target` (appended after base's
|
|
1395
|
+
* highest version). They are extracted from the pinned record with the held master.
|
|
1396
|
+
*/
|
|
1397
|
+
private absorbHeldKeys;
|
|
1398
|
+
/** Open every data key of `record` this device does not hold yet. */
|
|
1399
|
+
private openDataKeys;
|
|
1400
|
+
/**
|
|
1401
|
+
* Whether `record` holds the anchor key of a recovery key: data keys are never removed
|
|
1402
|
+
* or re-keyed (a passphrase change, recovery or merge re-wraps them under the same key
|
|
1403
|
+
* id), so the anchor stays valid for the ring.
|
|
1404
|
+
*/
|
|
1405
|
+
private holdsAnchor;
|
|
1406
|
+
/**
|
|
1407
|
+
* Whether `master` (which authenticated `record`) is a master key a passphrase change
|
|
1408
|
+
* retired: listed in `record` itself or in any record this device authenticated.
|
|
1409
|
+
*/
|
|
1410
|
+
private isRetiredMaster;
|
|
1411
|
+
/** The next revision after `base`, above the pinned one when it is the same ring. */
|
|
1412
|
+
private nextRevision;
|
|
1413
|
+
/** Authenticate a record with the held master key (no passphrase needed). */
|
|
1414
|
+
private authenticateHeld;
|
|
1415
|
+
/**
|
|
1416
|
+
* Open and authenticate a record with a passphrase.
|
|
1417
|
+
*
|
|
1418
|
+
* @throws {EncryptionKeyError} WRONG_PASSPHRASE, or KEY_RECORD_INVALID when the master
|
|
1419
|
+
* key opens but the record does not authenticate under it (tampered)
|
|
1420
|
+
*/
|
|
1421
|
+
private openWithPassphrase;
|
|
1422
|
+
/** The configured or pending passphrase, unless it already failed for this salt. */
|
|
1423
|
+
private sourcePassphraseFor;
|
|
1424
|
+
private followConflictLocked;
|
|
1425
|
+
private commitLocked;
|
|
1426
|
+
/** Persist the pinned record, the held keys and the master marks (RT-107). */
|
|
1427
|
+
private saveCacheLocked;
|
|
1428
|
+
private rebuildEncryptor;
|
|
1429
|
+
private availablePassphrase;
|
|
1430
|
+
private requireMaster;
|
|
1431
|
+
/** The KEK of `record`'s own salt (never one derived for another record's salt). */
|
|
1432
|
+
private requireKek;
|
|
1433
|
+
private requireRecord;
|
|
1434
|
+
private noteFailedUnlock;
|
|
1435
|
+
private dropKeys;
|
|
1436
|
+
/** A record refused for what the server sent: the held keys stay, nothing is adopted. */
|
|
1437
|
+
private refuseRecord;
|
|
1438
|
+
/** A server-side condition: keys kept, a later session (or a push) can clear it. */
|
|
1439
|
+
private holdFor;
|
|
1440
|
+
private lockWith;
|
|
1441
|
+
/** Locks that need the user stop encrypting; records refused keep held keys. */
|
|
1442
|
+
private dropKeysIfUnusable;
|
|
1443
|
+
private statusError;
|
|
1444
|
+
private cacheId;
|
|
1445
|
+
private makeStatus;
|
|
1446
|
+
private setStatus;
|
|
1447
|
+
private exclusive;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
745
1450
|
/** Default snapshot size above which the doc channel is preferred. */
|
|
746
1451
|
declare const DEFAULT_RICHTEXT_DOC_CHANNEL_THRESHOLD = 4096;
|
|
747
1452
|
type RichtextDocListener = (update: Uint8Array) => void;
|
|
@@ -801,6 +1506,12 @@ declare class OutboundQueue {
|
|
|
801
1506
|
private queue;
|
|
802
1507
|
private readonly seen;
|
|
803
1508
|
private readonly inFlight;
|
|
1509
|
+
/**
|
|
1510
|
+
* Ids of queued operations that were put on the wire at least once. Such an op may
|
|
1511
|
+
* already be stored on the server (its ack can be lost), so a clock rebase must never
|
|
1512
|
+
* re-stamp it (W3 step 4). Persisted through the storage when it supports it.
|
|
1513
|
+
*/
|
|
1514
|
+
private readonly sent;
|
|
804
1515
|
private nextBatchId;
|
|
805
1516
|
private initialized;
|
|
806
1517
|
constructor(storage: QueueStorage);
|
|
@@ -814,13 +1525,25 @@ declare class OutboundQueue {
|
|
|
814
1525
|
* Deduplicates by operation ID. Persists to storage.
|
|
815
1526
|
*/
|
|
816
1527
|
enqueue(op: Operation): Promise<void>;
|
|
1528
|
+
/**
|
|
1529
|
+
* Add several operations at once: deduplicated by id, persisted, and causally
|
|
1530
|
+
* re-sorted once (rather than once per operation).
|
|
1531
|
+
*/
|
|
1532
|
+
enqueueMany(ops: Operation[]): Promise<void>;
|
|
817
1533
|
/**
|
|
818
1534
|
* Take a batch of operations from the front of the queue.
|
|
819
1535
|
* Moves them to in-flight status. Returns null if queue is empty.
|
|
820
1536
|
*
|
|
821
1537
|
* @param batchSize - Maximum number of operations in the batch
|
|
1538
|
+
* @param accept - Only operations it accepts are taken (in queue order); the others
|
|
1539
|
+
* stay queued. The engine passes "authored under this session's node id": a session
|
|
1540
|
+
* may only upload its own node's operations (RT-38, RT-40).
|
|
822
1541
|
*/
|
|
823
|
-
takeBatch(batchSize: number): OutboundBatch | null;
|
|
1542
|
+
takeBatch(batchSize: number, accept?: (op: Operation) => boolean): OutboundBatch | null;
|
|
1543
|
+
/** Whether any queued (not in-flight) operation satisfies `accept`. */
|
|
1544
|
+
hasOperationsMatching(accept: (op: Operation) => boolean): boolean;
|
|
1545
|
+
/** Number of queued (not in-flight) operations that satisfy `accept`. */
|
|
1546
|
+
countMatching(accept: (op: Operation) => boolean): number;
|
|
824
1547
|
/**
|
|
825
1548
|
* Acknowledge a batch, removing its operations permanently.
|
|
826
1549
|
*/
|
|
@@ -831,7 +1554,33 @@ declare class OutboundQueue {
|
|
|
831
1554
|
* queue for retry. This is required when the server accepts a prefix but rejects a
|
|
832
1555
|
* later operation retriably (for example a stale-scope push after auth changed).
|
|
833
1556
|
*/
|
|
834
|
-
acknowledgeThrough(batchId: string, lastSequenceNumber: number): Promise<
|
|
1557
|
+
acknowledgeThrough(batchId: string, lastSequenceNumber: number): Promise<{
|
|
1558
|
+
acknowledged: Operation[];
|
|
1559
|
+
returned: Operation[];
|
|
1560
|
+
}>;
|
|
1561
|
+
/**
|
|
1562
|
+
* Record that a taken batch was put on the wire. Its operations may now be stored on
|
|
1563
|
+
* the server even if no ack ever arrives, so they are excluded from clock rebases.
|
|
1564
|
+
*/
|
|
1565
|
+
markSent(batchId: string): Promise<void>;
|
|
1566
|
+
/** Whether this queued operation was ever put on the wire. */
|
|
1567
|
+
wasSent(opId: string): boolean;
|
|
1568
|
+
/**
|
|
1569
|
+
* Forget the sent flag of operations the server explicitly refused without storing
|
|
1570
|
+
* (for example a batch refused with INVALID_TIMESTAMP), so a clock rebase may re-stamp
|
|
1571
|
+
* them. In memory only: after a restart they are conservatively treated as sent.
|
|
1572
|
+
*/
|
|
1573
|
+
clearSent(opIds: string[]): void;
|
|
1574
|
+
/** Whether the queue owns this operation (queued or in flight). */
|
|
1575
|
+
has(opId: string): boolean;
|
|
1576
|
+
/** Every operation currently in flight, across batches. */
|
|
1577
|
+
getInFlight(): Operation[];
|
|
1578
|
+
/**
|
|
1579
|
+
* Replace some queued (not in-flight) operations by rewritten ones, for example after
|
|
1580
|
+
* a clock rebase re-stamped never-sent operations under new content-addressed ids.
|
|
1581
|
+
* Operations not named in `oldIds` are untouched.
|
|
1582
|
+
*/
|
|
1583
|
+
replace(oldIds: string[], newOps: Operation[]): Promise<void>;
|
|
835
1584
|
/**
|
|
836
1585
|
* Return a failed batch to the front of the queue for retry.
|
|
837
1586
|
* Prepends the operations to maintain priority.
|
|
@@ -879,6 +1628,8 @@ declare class OutboundQueue {
|
|
|
879
1628
|
* or null if it was not present (already acknowledged and removed).
|
|
880
1629
|
*/
|
|
881
1630
|
reject(opId: string): Promise<Operation | null>;
|
|
1631
|
+
/** Remove every pending/in-flight operation for a record so authorization loss cannot upload it. */
|
|
1632
|
+
rejectRecord(collection: string, recordId: string): Promise<Operation[]>;
|
|
882
1633
|
/**
|
|
883
1634
|
* Remove operations by id from queue and persistent storage.
|
|
884
1635
|
* Used when ops were already sent during handshake delta exchange.
|
|
@@ -913,6 +1664,14 @@ interface SyncEngineOptions {
|
|
|
913
1664
|
* before sending and decrypted after receiving. The server never sees plaintext data.
|
|
914
1665
|
*/
|
|
915
1666
|
encryptor?: SyncEncryptor;
|
|
1667
|
+
/**
|
|
1668
|
+
* End-to-end encryption keyring (ENC-1): the server-stored, passphrase-wrapped data
|
|
1669
|
+
* keys, opened during each handshake before any operation is exchanged. Takes the
|
|
1670
|
+
* place of `encryptor`. While it is locked, sync does not connect.
|
|
1671
|
+
*/
|
|
1672
|
+
keyring?: EncryptionKeyring;
|
|
1673
|
+
/** How long a key-service request may take before the session is retried (ms). */
|
|
1674
|
+
keyServiceTimeoutMs?: number;
|
|
916
1675
|
/** Optional configuration for the metrics collector. */
|
|
917
1676
|
metricsConfig?: MetricsCollectorConfig;
|
|
918
1677
|
/** Op-log backed sync state (last acked server vector, unsynced counts). */
|
|
@@ -947,6 +1706,8 @@ interface SyncDiagnostics {
|
|
|
947
1706
|
*/
|
|
948
1707
|
declare class SyncEngine {
|
|
949
1708
|
private state;
|
|
1709
|
+
/** Listeners registered through {@link onStateChange}. */
|
|
1710
|
+
private readonly stateListeners;
|
|
950
1711
|
private readonly transport;
|
|
951
1712
|
private readonly store;
|
|
952
1713
|
private readonly config;
|
|
@@ -956,28 +1717,98 @@ declare class SyncEngine {
|
|
|
956
1717
|
private readonly rejectedStorage;
|
|
957
1718
|
private readonly batchSize;
|
|
958
1719
|
private readonly outboundAckTimeoutMs;
|
|
1720
|
+
private readonly outboundRetryBaseDelayMs;
|
|
1721
|
+
private readonly outboundRetryMaxDelayMs;
|
|
959
1722
|
private readonly encryptor;
|
|
1723
|
+
private readonly keyring;
|
|
1724
|
+
private readonly keyServiceTimeoutMs;
|
|
1725
|
+
/** Key-service requests awaiting their response, by request id. */
|
|
1726
|
+
private readonly keyRequests;
|
|
1727
|
+
/** Sync is paused because the encryption keyring is locked (ENC-1). */
|
|
1728
|
+
private encryptionLocked;
|
|
1729
|
+
/** A key-record refresh for an unknown key version is running. */
|
|
1730
|
+
private keyRefreshInFlight;
|
|
960
1731
|
private readonly awarenessManager;
|
|
961
1732
|
private readonly richtextDocChannel;
|
|
962
1733
|
private readonly blobChunkChannel;
|
|
963
1734
|
private readonly metricsCollector;
|
|
964
1735
|
private readonly syncState;
|
|
965
1736
|
private remoteVector;
|
|
1737
|
+
/** Runs before each outbound operation batch is sent (see setOutboundPreparer). */
|
|
1738
|
+
private outboundPreparer;
|
|
1739
|
+
/**
|
|
1740
|
+
* Per-device node token the server issued at this node id's first anonymous claim
|
|
1741
|
+
* (RT-12). Presented in every handshake; persisted next to the node id when the
|
|
1742
|
+
* sync-state persistence supports it, otherwise kept for this engine's lifetime.
|
|
1743
|
+
*/
|
|
1744
|
+
private nodeToken;
|
|
1745
|
+
/**
|
|
1746
|
+
* Node ids the server named authoritative in its last accepted handshake response
|
|
1747
|
+
* (protocol v2). Null until a protocol-2 server answered in this engine's lifetime;
|
|
1748
|
+
* loaded from persistence on start. See {@link SyncEngine.getAuthoritativeNodeIds}.
|
|
1749
|
+
*/
|
|
1750
|
+
private authoritativeNodeIds;
|
|
1751
|
+
/** Protocol version of the server in the current session (1 for a beta.12-era server). */
|
|
1752
|
+
private serverProtocolVersion;
|
|
1753
|
+
/** Whether any server answered a handshake (serverProtocolVersion is then real). */
|
|
1754
|
+
private serverProtocolKnown;
|
|
966
1755
|
private lastAckedServerVector;
|
|
967
1756
|
private cachedUnsyncedCount;
|
|
968
1757
|
private lastSyncedAt;
|
|
969
1758
|
private lastSuccessfulPush;
|
|
970
1759
|
private lastSuccessfulPull;
|
|
971
1760
|
private conflictCount;
|
|
972
|
-
|
|
973
|
-
|
|
1761
|
+
/**
|
|
1762
|
+
* Every outbound batch taken from the queue and not yet resolved, keyed by queue batch
|
|
1763
|
+
* id: handshake-delta and streaming batches alike. An ack resolves only the batch it
|
|
1764
|
+
* names (SYNC-4); a close returns all of them to the queue.
|
|
1765
|
+
*/
|
|
1766
|
+
private readonly inFlightUploads;
|
|
1767
|
+
/** Wire message id -> queue batch id, for ack and rejection correlation. */
|
|
1768
|
+
private readonly uploadByMessageId;
|
|
1769
|
+
/** Incremented on every connect and every disconnect; stale async sends check it. */
|
|
1770
|
+
private sessionEpoch;
|
|
1771
|
+
/** Set by destroy(): the engine never connects or arms a timer again. */
|
|
1772
|
+
private destroyed;
|
|
1773
|
+
private readonly statusListeners;
|
|
1774
|
+
/**
|
|
1775
|
+
* Contiguous acknowledged prefix of this device's own operations (W3 step 2): every
|
|
1776
|
+
* own op with sequence <= this is stored on the server, terminally rejected and
|
|
1777
|
+
* recorded, or not upload-eligible. Never advanced by a server-advertised vector.
|
|
1778
|
+
*/
|
|
1779
|
+
private ownAckedThrough;
|
|
1780
|
+
/** Node id the prefix belongs to (a rotation starts a new prefix at 0). */
|
|
1781
|
+
private ownTrackingNodeId;
|
|
1782
|
+
/** Highest own sequence read from the op log this session (above the prefix). */
|
|
1783
|
+
private ownScannedThrough;
|
|
1784
|
+
/**
|
|
1785
|
+
* Own sequences above the prefix that this session knows about, mapped to the ids of
|
|
1786
|
+
* their operations that are still unresolved. An empty set marks a sequence resolved.
|
|
1787
|
+
*/
|
|
1788
|
+
private readonly ownUnresolved;
|
|
1789
|
+
/** Ids resolved at sequences the log scan has not reached yet (skip on scan). */
|
|
1790
|
+
private readonly ownResolvedAhead;
|
|
1791
|
+
private ownTrackingChain;
|
|
1792
|
+
/** Inbound quarantine kept in memory when the engine has no sync-state persistence. */
|
|
1793
|
+
private readonly memoryQuarantine;
|
|
1794
|
+
private outboundRetryAttempt;
|
|
1795
|
+
private outboundRetryTimer;
|
|
1796
|
+
/** {@link pushOperation} calls not yet enqueued; the streaming flush waits for 0. */
|
|
1797
|
+
private pendingPushes;
|
|
974
1798
|
private reconnecting;
|
|
975
1799
|
private schemaBlocked;
|
|
976
1800
|
private clockBlocked;
|
|
977
1801
|
private clockSkewMs;
|
|
978
1802
|
private blobStorageEnabled;
|
|
1803
|
+
private blobPossessionProof;
|
|
979
1804
|
private suspensionReason;
|
|
980
1805
|
private authRejected;
|
|
1806
|
+
/**
|
|
1807
|
+
* Set when the server ended the session with AUTH_EXPIRED or AUTH_REVOKED. The
|
|
1808
|
+
* next connect asks the auth callback for a refreshed token (never the cached
|
|
1809
|
+
* one the server just refused) and is cleared by an accepted handshake.
|
|
1810
|
+
*/
|
|
1811
|
+
private credentialRefreshRequired;
|
|
981
1812
|
private serverFrontier;
|
|
982
1813
|
private hasInFlightDeliveryBatch;
|
|
983
1814
|
private blockedFailure;
|
|
@@ -985,11 +1816,84 @@ declare class SyncEngine {
|
|
|
985
1816
|
private startPromise;
|
|
986
1817
|
private stopPromise;
|
|
987
1818
|
private reconnectPromise;
|
|
1819
|
+
/** A node-id rotation started by NODE_ID_CLAIMED; the next connect waits for it (RT-21). */
|
|
1820
|
+
private nodeRotation;
|
|
1821
|
+
/**
|
|
1822
|
+
* Node id of the current (or last) session. Usually the store's node id; while the
|
|
1823
|
+
* engine adopts the unsynced writes of a node no live tab uses (RT-40) it is that
|
|
1824
|
+
* node. A session uploads only operations authored under its node id.
|
|
1825
|
+
*/
|
|
1826
|
+
private sessionNodeId;
|
|
1827
|
+
/** Node id the in-memory node token belongs to. */
|
|
1828
|
+
private nodeTokenNodeId;
|
|
1829
|
+
/** Releases the lock held on an adopted node (RT-40); null when not adopting. */
|
|
1830
|
+
private releaseAdoption;
|
|
1831
|
+
/**
|
|
1832
|
+
* Ids of operations the server refused for good (RT-36). Cache of the durable markers,
|
|
1833
|
+
* and the only record when the persistence layer has none (engine lifetime).
|
|
1834
|
+
*/
|
|
1835
|
+
private readonly terminalRejected;
|
|
1836
|
+
/** Nodes accepted at a handshake, when the persistence layer keeps no registry. */
|
|
1837
|
+
private readonly memoryAcceptedNodes;
|
|
1838
|
+
/** Every node id this database authored operations under (loaded at start). */
|
|
1839
|
+
private localNodeIds;
|
|
1840
|
+
/** Local nodes whose unsynced writes are held for another principal (RT-38). */
|
|
1841
|
+
private heldNodeIds;
|
|
1842
|
+
/** Unsynced, unqueued operations of other uploadable local nodes (RT-40), counted pending. */
|
|
1843
|
+
private otherNodesPending;
|
|
1844
|
+
/** Unsynced, unqueued operations of held nodes (RT-38), reported separately. */
|
|
1845
|
+
private heldPending;
|
|
1846
|
+
/** Held local nodes as of the last session start (RT-50), for status.heldNodes. */
|
|
1847
|
+
private heldNodeInfos;
|
|
1848
|
+
/**
|
|
1849
|
+
* The server holds operations of this node that the device lost (RT-35): the delivery
|
|
1850
|
+
* watermark was reset so the next session resyncs from 0, which fetches them back.
|
|
1851
|
+
* The session ends once its in-flight uploads are resolved.
|
|
1852
|
+
*/
|
|
1853
|
+
private ownRecoveryPending;
|
|
1854
|
+
/**
|
|
1855
|
+
* The signed-in user as resolved for the current session (RT-42): a user id, null
|
|
1856
|
+
* (nobody signed in) or undefined (the app does not tell the engine).
|
|
1857
|
+
*/
|
|
1858
|
+
private principal;
|
|
1859
|
+
/**
|
|
1860
|
+
* The user whose credential the current session's handshake carried (RT-50, RT-52):
|
|
1861
|
+
* re-read after the credential fetch, so an accepted handshake binds a node to the
|
|
1862
|
+
* user the server accepted it for, never to a user who signed out meanwhile.
|
|
1863
|
+
*/
|
|
1864
|
+
private sessionPrincipal;
|
|
1865
|
+
/** Serializes store bindings to the signed-in user (RT-52). */
|
|
1866
|
+
private principalChain;
|
|
1867
|
+
/** Adoption schedule kept in memory when the persistence layer has none (RT-46). */
|
|
1868
|
+
private memorySchedule;
|
|
1869
|
+
/** The schedule had parked adoptions when last read or written (RT-46). */
|
|
1870
|
+
private scheduleHasParked;
|
|
1871
|
+
/**
|
|
1872
|
+
* Local nodes with unsynced writes this session did not adopt (RT-46): parked (no
|
|
1873
|
+
* progress last time) or waiting for another local node's unsynced parent.
|
|
1874
|
+
*/
|
|
1875
|
+
private deferredAdoptions;
|
|
1876
|
+
/** Operations acknowledged in this session (RT-46: a yield needs progress). */
|
|
1877
|
+
private sessionAcked;
|
|
1878
|
+
/** An upload of the adopted node came back unprocessed in this session (RT-46). */
|
|
1879
|
+
private adoptionStalled;
|
|
1880
|
+
/**
|
|
1881
|
+
* The session node's entry in this session's handshake vector (RT-44). The server held
|
|
1882
|
+
* nothing of the node above it then; a SEQUENCE_CONFLICT above it was caused by an
|
|
1883
|
+
* operation stored during this session by another live copy of this database.
|
|
1884
|
+
*/
|
|
1885
|
+
private handshakeOwnEntry;
|
|
1886
|
+
/** When this engine last reset delivery for a SEQUENCE_CONFLICT recovery (RT-44). */
|
|
1887
|
+
private lastFullResyncAt;
|
|
1888
|
+
/** A recovery full resync was rate-limited; the next session start after the interval runs it. */
|
|
1889
|
+
private fullResyncDeferred;
|
|
1890
|
+
/** Consecutive failed durability barriers before an upload (RT-49). */
|
|
1891
|
+
private durabilityFailures;
|
|
1892
|
+
/** Uploads proceed without local durability: the barrier failed persistently (RT-49). */
|
|
1893
|
+
private durabilityDegraded;
|
|
988
1894
|
private deltaBatchesReceived;
|
|
989
1895
|
private deltaReceiveComplete;
|
|
990
1896
|
private deltaSendComplete;
|
|
991
|
-
/** Op ids sent during handshake delta — removed from outbound queue to avoid duplicate send */
|
|
992
|
-
private deltaSentOpIds;
|
|
993
1897
|
/** Outbound delta batch message IDs awaiting ACK when strictHandshake is enabled */
|
|
994
1898
|
private pendingDeltaBatchAcks;
|
|
995
1899
|
/**
|
|
@@ -998,10 +1902,25 @@ declare class SyncEngine {
|
|
|
998
1902
|
* with the server-accepted scope (server is authoritative).
|
|
999
1903
|
*/
|
|
1000
1904
|
private activeScope;
|
|
1905
|
+
/** Server-authoritative upload authorization, separate from the downloaded view. */
|
|
1906
|
+
private activeUplinkScope;
|
|
1907
|
+
/**
|
|
1908
|
+
* The downlink scope the server accepted at the last handshake that named one
|
|
1909
|
+
* (SYNC-11), persisted. The next handshake reports its key and that view's watermark
|
|
1910
|
+
* so the server can resume the stream of an unchanged grant. Never sent as the
|
|
1911
|
+
* requested scope: a later-widened grant must not be narrowed to it.
|
|
1912
|
+
*/
|
|
1913
|
+
private lastAcceptedScope;
|
|
1001
1914
|
/** Live query subsets registered from reactive subscriptions */
|
|
1002
1915
|
private querySubsets;
|
|
1003
1916
|
private staticQuerySubsets;
|
|
1004
1917
|
private querySubsetReconnectTimer;
|
|
1918
|
+
/**
|
|
1919
|
+
* Fires when the earliest parked adoption's backoff runs out (RT-53): an idle session of
|
|
1920
|
+
* this tab's own node then hands over to it, so a closed tab's deferred writes are
|
|
1921
|
+
* retried even if this tab never writes or reconnects.
|
|
1922
|
+
*/
|
|
1923
|
+
private adoptionRetryTimer;
|
|
1005
1924
|
/** Resume cursor for paginated initial sync (persisted across reconnects) */
|
|
1006
1925
|
private resumeDeltaCursor;
|
|
1007
1926
|
private initialSyncTotalBatches;
|
|
@@ -1024,6 +1943,8 @@ declare class SyncEngine {
|
|
|
1024
1943
|
* so a view switch resolves synchronously (no race with the reconnect it triggers).
|
|
1025
1944
|
*/
|
|
1026
1945
|
private readonly deliverySignatureWatermarks;
|
|
1946
|
+
/** Schema transforms: the engine checks views, the store folds them (RT-84). */
|
|
1947
|
+
private readonly operationTransforms;
|
|
1027
1948
|
private deliveryGapRepeatCount;
|
|
1028
1949
|
private lastDeliveryGapKey;
|
|
1029
1950
|
constructor(options: SyncEngineOptions);
|
|
@@ -1037,13 +1958,44 @@ declare class SyncEngine {
|
|
|
1037
1958
|
*/
|
|
1038
1959
|
stop(): Promise<void>;
|
|
1039
1960
|
private stopInternal;
|
|
1961
|
+
/**
|
|
1962
|
+
* Stop for good (`app.close()`): disconnect, clear every timer and listener, and refuse
|
|
1963
|
+
* any later start. Unlike stop(), nothing the engine scheduled can run afterwards.
|
|
1964
|
+
*/
|
|
1965
|
+
destroy(): Promise<void>;
|
|
1966
|
+
/** Clear every timer the engine owns. */
|
|
1967
|
+
private clearAllTimers;
|
|
1968
|
+
private clearAdoptionRetryTimer;
|
|
1969
|
+
/** Close the transport, logging nothing: it may already be closed. */
|
|
1970
|
+
private closeTransportQuietly;
|
|
1971
|
+
/**
|
|
1972
|
+
* The engine gave up on this session (a message it could not process, a transport
|
|
1973
|
+
* error, a server error): move to disconnected and close the transport (SYNC-5). The
|
|
1974
|
+
* close starts synchronously in this same frame, so the socket is released before
|
|
1975
|
+
* auto-reconnect (which is asynchronous) can open another one.
|
|
1976
|
+
*/
|
|
1977
|
+
private abandonSession;
|
|
1978
|
+
/**
|
|
1979
|
+
* Subscribe to status changes that no event announces (an upload ack lowering the
|
|
1980
|
+
* pending count, reaching streaming). Status bridges refresh on it, so a waiter such
|
|
1981
|
+
* as `waitForSettled` sees the change immediately (RT-28).
|
|
1982
|
+
*/
|
|
1983
|
+
onStatusChange(listener: () => void): () => void;
|
|
1984
|
+
private notifyStatusChange;
|
|
1040
1985
|
private ensureDisconnected;
|
|
1041
1986
|
/**
|
|
1042
1987
|
* Push a local operation to the outbound queue.
|
|
1043
1988
|
* If streaming, flushes immediately.
|
|
1044
1989
|
*
|
|
1045
|
-
*
|
|
1046
|
-
*
|
|
1990
|
+
* Uploads are judged against the UPLINK scope only. Query subsets narrow what
|
|
1991
|
+
* this client downloads, never what it uploads: an edit that moves a record out
|
|
1992
|
+
* of a reactive query's view must still reach the server (SYNC-1).
|
|
1993
|
+
*
|
|
1994
|
+
* Operations on collections that do not sync at all stay local-only by design.
|
|
1995
|
+
* An operation on a synced collection that falls outside the uplink scope would
|
|
1996
|
+
* be refused by the server, so it is recorded in the rejected store and
|
|
1997
|
+
* surfaced as `sync:operation-rejected` (code `OUT_OF_UPLINK_SCOPE`) rather than
|
|
1998
|
+
* kept as a silent local fork.
|
|
1047
1999
|
*/
|
|
1048
2000
|
pushOperation(op: Operation): Promise<void>;
|
|
1049
2001
|
/**
|
|
@@ -1056,7 +2008,23 @@ declare class SyncEngine {
|
|
|
1056
2008
|
/**
|
|
1057
2009
|
* Get the current developer-facing sync status.
|
|
1058
2010
|
*/
|
|
2011
|
+
/**
|
|
2012
|
+
* Node ids the sync server named authoritative (protocol v2): operations from these
|
|
2013
|
+
* nodes are server-authored and are the only ones that may carry `fieldVersions` or
|
|
2014
|
+
* `foldState`. Null until a protocol-2 server answered (or one answered in an earlier
|
|
2015
|
+
* session and the list was persisted).
|
|
2016
|
+
*/
|
|
2017
|
+
getAuthoritativeNodeIds(): readonly string[] | null;
|
|
2018
|
+
/** Protocol version of the server in the current (or last) session; 1 before any. */
|
|
2019
|
+
getServerProtocolVersion(): number;
|
|
1059
2020
|
getStatus(): SyncStatusInfo;
|
|
2021
|
+
/**
|
|
2022
|
+
* Sync is paused on a locked keyring (ENC-1): no session runs until the app unlocks
|
|
2023
|
+
* it. Reported as a suspension (`phase: 'suspended'`, `reason: 'encryption-locked'`,
|
|
2024
|
+
* `status: 'encryption-locked'`) rather than as plain offline, so the UI can ask for
|
|
2025
|
+
* the passphrase and `waitForSettled` reports why it cannot settle.
|
|
2026
|
+
*/
|
|
2027
|
+
private isLockedSuspension;
|
|
1060
2028
|
private resolvePhase;
|
|
1061
2029
|
/**
|
|
1062
2030
|
* True when the server rejected the client's schema version at handshake.
|
|
@@ -1068,6 +2036,18 @@ declare class SyncEngine {
|
|
|
1068
2036
|
* Local writes continue to work and queue; only sync is paused.
|
|
1069
2037
|
*/
|
|
1070
2038
|
isClockBlocked(): boolean;
|
|
2039
|
+
/**
|
|
2040
|
+
* True when sync is paused because the end-to-end encryption keyring is locked (no
|
|
2041
|
+
* passphrase, a wrong one, or a key record that must not be used). Local writes go
|
|
2042
|
+
* on and queue; `retryNow()` after an unlock resumes sync.
|
|
2043
|
+
*/
|
|
2044
|
+
isEncryptionLocked(): boolean;
|
|
2045
|
+
/**
|
|
2046
|
+
* The key service over the live sync session, for keyring management (rotation,
|
|
2047
|
+
* passphrase change, recovery). Null while not connected: those calls need the
|
|
2048
|
+
* server.
|
|
2049
|
+
*/
|
|
2050
|
+
getKeyServiceChannel(): KeyServiceChannel | null;
|
|
1071
2051
|
/** serverTime - localTime measured at the last handshake, or null before first connect. */
|
|
1072
2052
|
getClockSkewMs(): number | null;
|
|
1073
2053
|
/**
|
|
@@ -1086,9 +2066,16 @@ declare class SyncEngine {
|
|
|
1086
2066
|
*/
|
|
1087
2067
|
recordConflict(): void;
|
|
1088
2068
|
/**
|
|
1089
|
-
* Count of
|
|
2069
|
+
* Count of this device's own operations not yet stored on the server (or terminally
|
|
2070
|
+
* rejected and recorded): queued, in flight, or above the acknowledged prefix.
|
|
1090
2071
|
*/
|
|
1091
2072
|
getUnsyncedOperationCount(): Promise<number>;
|
|
2073
|
+
/**
|
|
2074
|
+
* Surface an inbound apply failure. `blocking` marks the stream as stalled on this op
|
|
2075
|
+
* (the watermark does not pass it); `quarantined` means the op is durably recorded and
|
|
2076
|
+
* the stream moves on, so it never marks the stream blocked, whatever `retriable` says
|
|
2077
|
+
* (retriable there means the quarantine replays it later).
|
|
2078
|
+
*/
|
|
1092
2079
|
private emitApplyFailure;
|
|
1093
2080
|
/**
|
|
1094
2081
|
* Force an immediate reconnection attempt. If the engine is disconnected
|
|
@@ -1097,6 +2084,35 @@ declare class SyncEngine {
|
|
|
1097
2084
|
retryNow(): Promise<void>;
|
|
1098
2085
|
/** Wake a permanent auth suspension after the binding reports a new auth state. */
|
|
1099
2086
|
notifyAuthChanged(): void;
|
|
2087
|
+
/**
|
|
2088
|
+
* The signed-in user may have changed (RT-42): end the live session (it belongs to the
|
|
2089
|
+
* previous user), then bind the store to the user `config.principal` reports, so the
|
|
2090
|
+
* next local write is authored under that user's own node. Call it before the next
|
|
2091
|
+
* write after an auth change; `createApp` does so for `sync.authClient`. The next
|
|
2092
|
+
* `start()` connects as that user's node.
|
|
2093
|
+
*/
|
|
2094
|
+
refreshPrincipal(): Promise<void>;
|
|
2095
|
+
/**
|
|
2096
|
+
* Bind the store to the user the app reports NOW (RT-52), without ending or waiting
|
|
2097
|
+
* for any session: call it on every auth event, before queueing the reconnect that
|
|
2098
|
+
* follows. Writes made from then on are authored under that user's node, even while
|
|
2099
|
+
* an earlier reconnect is still fetching a credential or connecting; that attempt
|
|
2100
|
+
* notices the change before its handshake and starts over. A live session keeps
|
|
2101
|
+
* running as the previous user's node and uploads only that node's writes, until the
|
|
2102
|
+
* queued reconnect replaces it.
|
|
2103
|
+
*/
|
|
2104
|
+
bindSignedInUser(): Promise<void>;
|
|
2105
|
+
/** The signed-in user as `config.principal` reports it (undefined when it throws). */
|
|
2106
|
+
private resolvePrincipal;
|
|
2107
|
+
/** Run `fn` after every earlier principal binding (one store binding at a time). */
|
|
2108
|
+
private withPrincipalLock;
|
|
2109
|
+
/**
|
|
2110
|
+
* Resolve the signed-in user and bind the store's node to them (RT-42).
|
|
2111
|
+
*
|
|
2112
|
+
* @returns false when sync must stay suspended: the store's node id is pinned and
|
|
2113
|
+
* belongs to another user, so nothing on this database may upload as this user.
|
|
2114
|
+
*/
|
|
2115
|
+
private applyPrincipal;
|
|
1100
2116
|
/**
|
|
1101
2117
|
* Atomically refresh the transport session. Overlapping callers share one
|
|
1102
2118
|
* reconnect, in-flight outbound batches are returned to the queue by stop(), and the
|
|
@@ -1113,6 +2129,15 @@ declare class SyncEngine {
|
|
|
1113
2129
|
* Get the current internal state (for testing).
|
|
1114
2130
|
*/
|
|
1115
2131
|
getState(): SyncState;
|
|
2132
|
+
/**
|
|
2133
|
+
* Subscribe to internal state changes (SYNC-8). The reconnection loop uses it to
|
|
2134
|
+
* count an attempt as successful only once the session reaches `streaming`, not
|
|
2135
|
+
* when the handshake is merely sent.
|
|
2136
|
+
*
|
|
2137
|
+
* @param listener - Called with the new state after every transition
|
|
2138
|
+
* @returns Unsubscribe function
|
|
2139
|
+
*/
|
|
2140
|
+
onStateChange(listener: (state: SyncState) => void): () => void;
|
|
1116
2141
|
/**
|
|
1117
2142
|
* Get the outbound queue (for testing).
|
|
1118
2143
|
*/
|
|
@@ -1137,6 +2162,16 @@ declare class SyncEngine {
|
|
|
1137
2162
|
* its own last position.
|
|
1138
2163
|
*/
|
|
1139
2164
|
private deliverySignature;
|
|
2165
|
+
/** The view signature {@link deliverySignature} gives under `scope` and today's subsets. */
|
|
2166
|
+
private deliverySignatureFor;
|
|
2167
|
+
/**
|
|
2168
|
+
* Handshake fields naming the accepted view this client last streamed under (SYNC-11):
|
|
2169
|
+
* its canonical scope key and its delivery watermark. Omitted when no scope was ever
|
|
2170
|
+
* accepted, or when it equals the requested scope (the requested watermark covers it).
|
|
2171
|
+
*/
|
|
2172
|
+
private acceptedViewHandshakeFields;
|
|
2173
|
+
/** Persist the downlink scope the server accepted at this handshake (null: none). */
|
|
2174
|
+
private rememberAcceptedScope;
|
|
1140
2175
|
/**
|
|
1141
2176
|
* Switch the active delivery watermark to the current view after a scope or subscription
|
|
1142
2177
|
* change. The prior view's watermark is saved so returning to it resumes; the new view's
|
|
@@ -1169,6 +2204,8 @@ declare class SyncEngine {
|
|
|
1169
2204
|
* Get the currently active scope map. Returns undefined if no scope is configured.
|
|
1170
2205
|
*/
|
|
1171
2206
|
getActiveScope(): SyncScopeMap | undefined;
|
|
2207
|
+
/** Get the server-accepted upload authorization scope. */
|
|
2208
|
+
getActiveUplinkScope(): SyncScopeMap | undefined;
|
|
1172
2209
|
/**
|
|
1173
2210
|
* Register a live query subset that narrows synced data for a collection.
|
|
1174
2211
|
* Takes effect on the next connection; reconnects when already connected.
|
|
@@ -1203,13 +2240,50 @@ declare class SyncEngine {
|
|
|
1203
2240
|
*/
|
|
1204
2241
|
isBlobStorageEnabled(): boolean;
|
|
1205
2242
|
/**
|
|
1206
|
-
*
|
|
1207
|
-
*
|
|
2243
|
+
* Whether the connected server (peer-relay mode, no central storage) asks for the
|
|
2244
|
+
* bytes behind blob references as proof of possession (RT-23). The server verifies
|
|
2245
|
+
* and drops them; the app uploads exactly as for central storage.
|
|
2246
|
+
*/
|
|
2247
|
+
isBlobPossessionProofRequested(): boolean;
|
|
2248
|
+
/**
|
|
2249
|
+
* Upload a blob chunk (or manifest) to the server, for central persistence or as
|
|
2250
|
+
* proof of possession. A no-op unless the handshake was accepted (syncing or
|
|
2251
|
+
* streaming) and the server advertised blob storage or asked for proofs.
|
|
1208
2252
|
*/
|
|
1209
2253
|
uploadBlobChunk(hash: string, bytes: Uint8Array): void;
|
|
2254
|
+
/**
|
|
2255
|
+
* Register work that must reach the server before each outbound operation batch,
|
|
2256
|
+
* on the same connection (messages are processed in order). Used to upload the
|
|
2257
|
+
* bytes behind blob references first: the server only accepts a reference to
|
|
2258
|
+
* content the writer can read or has uploaded (RT-11). Errors are ignored; the
|
|
2259
|
+
* batch is sent regardless.
|
|
2260
|
+
*
|
|
2261
|
+
* @param preparer - Called with the operations about to be sent; null to remove
|
|
2262
|
+
* @returns A function that removes this preparer
|
|
2263
|
+
*/
|
|
2264
|
+
setOutboundPreparer(preparer: ((operations: Operation[]) => Promise<void>) | null): () => void;
|
|
1210
2265
|
private messageChain;
|
|
1211
2266
|
private enqueueMessage;
|
|
1212
2267
|
private handleMessageAsync;
|
|
2268
|
+
/** Adopt a key record the server pushed; stop the session if it locks the keyring. */
|
|
2269
|
+
private handlePushedKeyRecord;
|
|
2270
|
+
/**
|
|
2271
|
+
* An operation sealed under a key version newer than any this device holds: another
|
|
2272
|
+
* device rotated keys and the server's push did not reach this session (another
|
|
2273
|
+
* server instance). Fetch the record once, then replay the quarantine in the chain.
|
|
2274
|
+
*/
|
|
2275
|
+
private maybeRefreshKeyring;
|
|
2276
|
+
/**
|
|
2277
|
+
* End the session because the keyring is locked; no reconnect until unlocked. A lock
|
|
2278
|
+
* caused by the server's record (rolled back, lost, malformed, forked: RT-96, RT-104)
|
|
2279
|
+
* needs no user action, so the session ends without pausing and the reconnection
|
|
2280
|
+
* loop retries with backoff; the next handshake re-uploads, merges or opens it.
|
|
2281
|
+
*/
|
|
2282
|
+
private pauseForLockedKeyring;
|
|
2283
|
+
/** One key-service round trip over the live session. */
|
|
2284
|
+
private keyServiceRequest;
|
|
2285
|
+
/** Fail every pending key-service request (the session ended). */
|
|
2286
|
+
private rejectKeyRequests;
|
|
1213
2287
|
private handleMessageFailure;
|
|
1214
2288
|
/**
|
|
1215
2289
|
* Compares server wall-clock time from the handshake with local time.
|
|
@@ -1219,6 +2293,41 @@ declare class SyncEngine {
|
|
|
1219
2293
|
*/
|
|
1220
2294
|
private evaluateClockSkew;
|
|
1221
2295
|
private handleHandshakeResponse;
|
|
2296
|
+
/**
|
|
2297
|
+
* The server refused this session's node id (`NODE_ID_CLAIMED`). What happens to the
|
|
2298
|
+
* node's unsynced writes depends on whether this database was ever accepted as it:
|
|
2299
|
+
*
|
|
2300
|
+
* - Never accepted (RT-21: the claim's token was lost before the device saw it, or
|
|
2301
|
+
* another device took the id first): no principal ever received these writes under
|
|
2302
|
+
* this node, so the never-sent ones are re-authored under a fresh node id and
|
|
2303
|
+
* uploaded. An operation flagged sent is never re-authored (RT-38): it may already
|
|
2304
|
+
* be stored, and a copy under a new id would store the change twice.
|
|
2305
|
+
* - Accepted before (RT-38: another user signed in on a database shared between
|
|
2306
|
+
* users): the node belongs to the principal that used it, so its unsynced writes
|
|
2307
|
+
* are held for that principal, never re-authored or uploaded under another one. The
|
|
2308
|
+
* device moves to a node the signed-in principal may own (a held node not yet tried
|
|
2309
|
+
* in this refusal cycle, typically that user's own earlier node) or to a fresh one.
|
|
2310
|
+
* - An adopted node (RT-40) is held; the store's own node is left as it is.
|
|
2311
|
+
*/
|
|
2312
|
+
private rotateNodeIdentity;
|
|
2313
|
+
/** RT-38: hold a refused, previously accepted node's writes and move to another node. */
|
|
2314
|
+
private holdNodeAndSwitch;
|
|
2315
|
+
/** RT-21: re-author a never-accepted node's never-sent writes under a fresh node. */
|
|
2316
|
+
private reauthorUnsentAndRotate;
|
|
2317
|
+
/**
|
|
2318
|
+
* Move `nodeId`'s never-sent unsynced operations to a fresh node id (re-authored, so
|
|
2319
|
+
* they upload under it) and start using it. An operation flagged sent stays: it may
|
|
2320
|
+
* already be stored under `nodeId` (RT-38).
|
|
2321
|
+
*
|
|
2322
|
+
* @returns How many unsynced operations stayed under `nodeId` (sent, unacknowledged)
|
|
2323
|
+
*/
|
|
2324
|
+
private rotateUnsent;
|
|
2325
|
+
/** A fresh node id belongs to the signed-in user, when the app says who that is (RT-42). */
|
|
2326
|
+
private bindFreshNode;
|
|
2327
|
+
/** The store moved to another node id: the next session starts from it. */
|
|
2328
|
+
private afterIdentityChange;
|
|
2329
|
+
/** Whether a handshake as `nodeId` was ever accepted on this database. */
|
|
2330
|
+
private wasNodeAccepted;
|
|
1222
2331
|
/**
|
|
1223
2332
|
* Re-stamps queued (never-acknowledged) operations whose timestamps are far
|
|
1224
2333
|
* enough in the future that the server would reject them, using the server's
|
|
@@ -1227,12 +2336,123 @@ declare class SyncEngine {
|
|
|
1227
2336
|
* immediately instead of waiting for real time to catch up.
|
|
1228
2337
|
*/
|
|
1229
2338
|
private maybeRebaseQueuedOperations;
|
|
2339
|
+
/**
|
|
2340
|
+
* Upload this device's unsynced operations as the handshake delta. The delta is the
|
|
2341
|
+
* outbound queue itself (rebuilt from the op log above the acknowledged prefix), sent
|
|
2342
|
+
* as tracked batches: nothing leaves the queue until the server acknowledges the batch
|
|
2343
|
+
* that carried it (SYNC-4).
|
|
2344
|
+
*/
|
|
1230
2345
|
private sendDelta;
|
|
1231
2346
|
private markDeltaSendCompleteIfReady;
|
|
1232
|
-
|
|
2347
|
+
/** Take the next batch from the queue and track it until it is resolved. */
|
|
2348
|
+
private takeUpload;
|
|
2349
|
+
/** Whether a taken batch still belongs to the live session (SYNC-10). */
|
|
2350
|
+
private isUploadCurrent;
|
|
2351
|
+
/** Return one taken batch to the queue and forget its tracking. */
|
|
2352
|
+
private returnUpload;
|
|
2353
|
+
/** Return every taken batch to the queue (the session ended; nothing was resolved). */
|
|
2354
|
+
private returnAllInFlightUploads;
|
|
2355
|
+
/**
|
|
2356
|
+
* The oldest batch taken from the queue and not yet resolved (diagnostics and
|
|
2357
|
+
* tests); null when nothing is in flight.
|
|
2358
|
+
*/
|
|
2359
|
+
private get currentBatch();
|
|
2360
|
+
private inFlightUploadCount;
|
|
2361
|
+
/**
|
|
2362
|
+
* Prepare, encrypt and send one taken batch, then register its message id so the ack
|
|
2363
|
+
* that names it resolves exactly this batch. Every await is followed by a check that
|
|
2364
|
+
* the batch still belongs to the live session; a send that throws returns the batch
|
|
2365
|
+
* to the queue instead of rejecting unhandled (SYNC-10).
|
|
2366
|
+
*
|
|
2367
|
+
* @returns The wire message id, or null when the batch was not sent.
|
|
2368
|
+
*/
|
|
2369
|
+
private sendUpload;
|
|
2370
|
+
/**
|
|
2371
|
+
* The durability barrier before an upload (RT-35), with a bounded number of failures
|
|
2372
|
+
* (RT-49). Throws while the failures may be transient; after
|
|
2373
|
+
* {@link DURABILITY_FAILURES_BEFORE_DEGRADED} consecutive failures it returns anyway
|
|
2374
|
+
* (degraded mode, `localDurability: 'degraded'`, `sync:durability-degraded`) so the
|
|
2375
|
+
* server keeps a durable copy. The barrier is still attempted every time: the first
|
|
2376
|
+
* success leaves degraded mode (`sync:durability-restored`).
|
|
2377
|
+
*/
|
|
2378
|
+
private awaitUploadDurability;
|
|
1233
2379
|
private handleOperationBatch;
|
|
2380
|
+
/**
|
|
2381
|
+
* Initial-sync bookkeeping for a received batch: progress, the legacy resume cursor,
|
|
2382
|
+
* and completion on the final batch. Runs for duplicate batches too (NEW-SYNC-1).
|
|
2383
|
+
*/
|
|
2384
|
+
private recordReceivedBatch;
|
|
2385
|
+
/**
|
|
2386
|
+
* Apply one delivered operation. Outcomes:
|
|
2387
|
+
* - `applied`: applied, a duplicate, or a merge-decided skip (the record is settled);
|
|
2388
|
+
* - `quarantine`: deliberately not applied and to be recorded durably (unknown
|
|
2389
|
+
* collection, transform unavailable, rejected or deferred apply, far-future
|
|
2390
|
+
* timestamp, undecryptable payload, referential failure), with `sync:apply-failed`;
|
|
2391
|
+
* - `stall`: a retriable failure; the watermark must not pass it (or the engine
|
|
2392
|
+
* cannot record a quarantine durably), so the server re-sends it.
|
|
2393
|
+
*/
|
|
2394
|
+
private applyInbound;
|
|
2395
|
+
/**
|
|
2396
|
+
* The delivery stream caught up: let the store retire provisional cascades of
|
|
2397
|
+
* remote deletes (RT-69) and settle row-snapshot records after a full resync
|
|
2398
|
+
* (RT-68). A failure is reported, never fatal: both are retried on the next
|
|
2399
|
+
* catch-up.
|
|
2400
|
+
*/
|
|
2401
|
+
private settleAfterCatchUp;
|
|
2402
|
+
/**
|
|
2403
|
+
* Whether a not-applied operation can be recorded so the watermark may pass it: with
|
|
2404
|
+
* a durable quarantine, or when nothing is durable (no persisted watermark either, so
|
|
2405
|
+
* a restart re-delivers everything anyway).
|
|
2406
|
+
*/
|
|
2407
|
+
private canQuarantine;
|
|
2408
|
+
/**
|
|
2409
|
+
* Record quarantined operations and (for a delivery batch) the watermark advance in
|
|
2410
|
+
* one durable step.
|
|
2411
|
+
*/
|
|
2412
|
+
private commitDeliveryProgress;
|
|
2413
|
+
/** Every inbound operation held in quarantine (durable when persistence supports it). */
|
|
2414
|
+
getQuarantinedOperations(): Promise<QuarantinedOperation[]>;
|
|
2415
|
+
/**
|
|
2416
|
+
* Try to apply every quarantined operation again, oldest delivery first and in HLC
|
|
2417
|
+
* order within a delivery. Runs on start (after a schema upgrade the unknown
|
|
2418
|
+
* collection now exists; after time catches up a far-future op is acceptable) and on
|
|
2419
|
+
* demand. Applied operations leave the quarantine; the rest stay.
|
|
2420
|
+
*
|
|
2421
|
+
* @returns How many operations left the quarantine.
|
|
2422
|
+
*/
|
|
2423
|
+
retryQuarantinedOperations(): Promise<number>;
|
|
2424
|
+
private replayQuarantine;
|
|
2425
|
+
/**
|
|
2426
|
+
* Under end-to-end encryption, a plaintext operation authored by the server (a node
|
|
2427
|
+
* the handshake named authoritative: cascades, set-nulls, constraint corrections,
|
|
2428
|
+
* route writes) that touches only the collection's cleartext fields is accepted
|
|
2429
|
+
* without an envelope: the server holds no key, so it cannot seal its own writes, and
|
|
2430
|
+
* such an operation carries nothing the server cannot already read. Any sealed field
|
|
2431
|
+
* (or an operation from any other node) still has to arrive sealed.
|
|
2432
|
+
*/
|
|
2433
|
+
private isServerAuthoredCleartext;
|
|
2434
|
+
/** Whether end-to-end encryption is configured (a keyring or a fixed encryptor). */
|
|
2435
|
+
private encryptionConfigured;
|
|
2436
|
+
/** The encryptor in force: the unlocked keyring's, or the fixed one; null when locked. */
|
|
2437
|
+
private activeEncryptor;
|
|
2438
|
+
/**
|
|
2439
|
+
* The encryptor in force. With encryption configured and the keyring locked this
|
|
2440
|
+
* throws: an operation is never sent or applied unencrypted because a key is missing.
|
|
2441
|
+
*/
|
|
2442
|
+
private requireEncryptor;
|
|
2443
|
+
/** The key service over this session (the handshake has been accepted). */
|
|
2444
|
+
private liveKeyServiceChannel;
|
|
2445
|
+
/** Re-apply a quarantined operation; true when it no longer needs to be kept. */
|
|
2446
|
+
private applyInboundQuietly;
|
|
1234
2447
|
/** Acknowledge a delivery batch (used for duplicates) without re-applying it. */
|
|
1235
2448
|
private sendDeliveryAck;
|
|
2449
|
+
/**
|
|
2450
|
+
* An acknowledgment resolves exactly the upload batch it names (SYNC-4): operations
|
|
2451
|
+
* the server processed (sequence <= `lastSequenceNumber`) leave the queue and count
|
|
2452
|
+
* toward the acknowledged prefix; the rest return to the queue for retry. An ack that
|
|
2453
|
+
* names no tracked batch (a node-token confirmation echo, a batch from an older
|
|
2454
|
+
* session) resolves nothing.
|
|
2455
|
+
*/
|
|
1236
2456
|
private handleAcknowledgment;
|
|
1237
2457
|
private handleError;
|
|
1238
2458
|
/**
|
|
@@ -1242,6 +2462,34 @@ declare class SyncEngine {
|
|
|
1242
2462
|
* the app can reconcile. Unlike {@link handleError}, this is a normal per-op
|
|
1243
2463
|
* signal, so the connection stays up.
|
|
1244
2464
|
*/
|
|
2465
|
+
private applyScopeRetraction;
|
|
2466
|
+
/**
|
|
2467
|
+
* The local nodes whose unsynced writes are held now (RT-38, RT-50), for the user the
|
|
2468
|
+
* app reports as signed in. See {@link SyncStatusInfo.heldNodes}.
|
|
2469
|
+
*/
|
|
2470
|
+
getHeldNodes(): Promise<HeldNodeInfo[]>;
|
|
2471
|
+
/**
|
|
2472
|
+
* Assign the `unassigned` held writes of `nodeId` to the signed-in user (RT-50): writes
|
|
2473
|
+
* made on this device before the app knew who was signed in, on a node that never
|
|
2474
|
+
* synced. Only the app can know whose they are (for example, a single-user device).
|
|
2475
|
+
* They upload on the user's next session (the server then binds the node to them).
|
|
2476
|
+
*
|
|
2477
|
+
* @throws {SyncError} `HELD_ASSIGN_NO_USER` when nobody is signed in;
|
|
2478
|
+
* `HELD_NODE_NOT_ASSIGNABLE` when `nodeId` is not an unassigned held node (it belongs
|
|
2479
|
+
* to a user, or the server refused it for this one)
|
|
2480
|
+
*/
|
|
2481
|
+
assignHeld(nodeId: string): Promise<void>;
|
|
2482
|
+
/**
|
|
2483
|
+
* Discard the `unassigned` held writes of `nodeId` from sync (RT-50): they are never
|
|
2484
|
+
* uploaded. They are NOT rolled back: they stay in this device's local database, and
|
|
2485
|
+
* count as synced from now on (`deleteDatabase` no longer protects them). Writes held
|
|
2486
|
+
* for another user (`other-user`) cannot be discarded by this user.
|
|
2487
|
+
*
|
|
2488
|
+
* @returns How many writes were discarded
|
|
2489
|
+
* @throws {SyncError} `HELD_NODE_NOT_DISCARDABLE` when `nodeId` is not an unassigned
|
|
2490
|
+
* held node
|
|
2491
|
+
*/
|
|
2492
|
+
discardHeld(nodeId: string): Promise<number>;
|
|
1245
2493
|
private handleOperationRejected;
|
|
1246
2494
|
/**
|
|
1247
2495
|
* Every operation the server rejected that has not yet been reconciled. The app
|
|
@@ -1256,36 +2504,243 @@ declare class SyncEngine {
|
|
|
1256
2504
|
clearRejectedOperations(operationIds: string[]): Promise<void>;
|
|
1257
2505
|
private checkDeltaComplete;
|
|
1258
2506
|
/**
|
|
1259
|
-
*
|
|
2507
|
+
* In a session of this tab's own node, arm a timer for the earliest parked adoption's
|
|
2508
|
+
* backoff expiry (RT-53). Without it a parked node is retried only at a session start
|
|
2509
|
+
* or after this tab's own uploads made progress, so an idle tab (a dashboard, a kiosk)
|
|
2510
|
+
* would never retry a closed tab's deferred writes while its connection holds.
|
|
2511
|
+
*/
|
|
2512
|
+
private armAdoptionRetryTimer;
|
|
2513
|
+
/**
|
|
2514
|
+
* A parked adoption's backoff ran out during this session (RT-53). An idle own-node
|
|
2515
|
+
* session hands over now: it reconnects, and the next session adopts the node (one
|
|
2516
|
+
* retry per backoff; a node deferred again is parked again for longer). A busy session
|
|
2517
|
+
* hands over when its uploads drain (maybeYieldToDeferredNode sees the expiry).
|
|
2518
|
+
*/
|
|
2519
|
+
private onAdoptionRetryTimer;
|
|
2520
|
+
/** Serialize every read-modify-write of the own-operation tracking. */
|
|
2521
|
+
private withOwnTracking;
|
|
2522
|
+
private loadOwnTracking;
|
|
2523
|
+
/**
|
|
2524
|
+
* Load the acknowledged prefix for the current node id, once per node id (an engine
|
|
2525
|
+
* restart keeps its in-memory progress). A persistence layer that has never stored a
|
|
2526
|
+
* prefix under this contract means an upgrade from a release that persisted a MAX:
|
|
2527
|
+
* that value can hide a lost op below it, so the prefix restarts at 0 and the device
|
|
2528
|
+
* re-uploads its own history once, chunk by chunk (the server dedups by id). The
|
|
2529
|
+
* prefix is persisted as it advances, so the re-upload resumes where it stopped.
|
|
2530
|
+
*/
|
|
2531
|
+
private loadOwnTrackingLocked;
|
|
2532
|
+
/**
|
|
2533
|
+
* Register an own operation. `unresolved` ops are queued for upload; the others are
|
|
2534
|
+
* already resolved (not upload-eligible). An op at or below the prefix (a sequence
|
|
2535
|
+
* that was a gap when the prefix passed it) lowers the prefix: re-uploading is safe,
|
|
2536
|
+
* skipping is not.
|
|
2537
|
+
*/
|
|
2538
|
+
private trackOwnOperation;
|
|
2539
|
+
/** Mark an own operation resolved (stored, terminally rejected, or not eligible). */
|
|
2540
|
+
private markOwnResolved;
|
|
2541
|
+
private isOwnOperationResolved;
|
|
2542
|
+
/**
|
|
2543
|
+
* Lower the prefix (the server holds fewer of this device's operations than it
|
|
2544
|
+
* believed) and forget what this session knew above it: the next advance rescans the
|
|
2545
|
+
* log from there and re-enqueues what is missing.
|
|
2546
|
+
*/
|
|
2547
|
+
private lowerOwnPrefixLocked;
|
|
2548
|
+
/**
|
|
2549
|
+
* Advance the prefix over every resolved own sequence, reading the next chunk of the
|
|
2550
|
+
* op log whenever it reaches a sequence this session has not examined yet. Reading a
|
|
2551
|
+
* chunk enqueues its upload-eligible operations (and flushes when streaming), so the
|
|
2552
|
+
* one-time upgrade re-upload proceeds chunk by chunk as acks arrive.
|
|
2553
|
+
*/
|
|
2554
|
+
private advanceOwnPrefixLocked;
|
|
2555
|
+
/**
|
|
2556
|
+
* Read own operations [from, to] from the op log and register each sequence: an
|
|
2557
|
+
* upload-eligible op not yet queued is enqueued; an ineligible one is recorded and
|
|
2558
|
+
* resolved; a sequence with no op is resolved. Every sequence in the range ends up
|
|
2559
|
+
* registered, which guarantees the advance loop makes progress.
|
|
1260
2560
|
*/
|
|
1261
|
-
private
|
|
2561
|
+
private scanOwnLog;
|
|
2562
|
+
private persistOwnPrefix;
|
|
1262
2563
|
private persistLastAckedServerVector;
|
|
1263
|
-
private advanceLastAckedForLocalNode;
|
|
1264
2564
|
/**
|
|
1265
|
-
* Refresh
|
|
2565
|
+
* Refresh the pending count: own operations not yet stored on the server (queued or
|
|
2566
|
+
* in flight), plus the own operations above the prefix not read from the log yet.
|
|
2567
|
+
* Computed from the client's own acknowledgments only, so it drops the moment the
|
|
2568
|
+
* server acknowledges, without waiting for a later handshake (RT-28).
|
|
1266
2569
|
*/
|
|
1267
2570
|
refreshPendingCount(): Promise<void>;
|
|
2571
|
+
/** Synchronous pending count (see {@link refreshPendingCount}). */
|
|
2572
|
+
private computePendingCount;
|
|
2573
|
+
/** Unsynced operations held for a principal that is not signed in (RT-38). */
|
|
2574
|
+
private computeHeldCount;
|
|
1268
2575
|
/**
|
|
1269
|
-
*
|
|
2576
|
+
* This device's operations not yet stored on the server (queued or in flight).
|
|
1270
2577
|
*/
|
|
1271
2578
|
getPendingSyncOperations(): Promise<Operation[]>;
|
|
1272
2579
|
/**
|
|
1273
|
-
*
|
|
2580
|
+
* Rebuild the upload set from the op log: every own operation above the acknowledged
|
|
2581
|
+
* prefix that is upload-eligible is queued (first chunk now, the rest as acks move
|
|
2582
|
+
* the prefix). Persisted queue entries that are already resolved are dropped.
|
|
1274
2583
|
*/
|
|
1275
2584
|
private reconcileOutboundFromOpLog;
|
|
2585
|
+
/** Node id of the current session: the store's own, or an adopted node's (RT-40). */
|
|
2586
|
+
private currentNodeId;
|
|
2587
|
+
/** A session uploads only operations authored under its own node id (RT-38, RT-40). */
|
|
2588
|
+
private readonly isSessionOperation;
|
|
2589
|
+
/** Whether the queue holds an operation this session may upload. */
|
|
2590
|
+
private hasUploadable;
|
|
2591
|
+
/** A queued or in-flight operation by id. */
|
|
2592
|
+
private findQueuedOperation;
|
|
2593
|
+
/**
|
|
2594
|
+
* Pick the node this session runs as (RT-40, RT-46). A session uploads under one node
|
|
2595
|
+
* id (the server confines uploads to the session's node), so the database's local
|
|
2596
|
+
* nodes take turns, each for one session:
|
|
2597
|
+
*
|
|
2598
|
+
* - A local node other than the store's own that still has unsynced writes, is not
|
|
2599
|
+
* held for another principal (RT-38) or bound to another user (RT-42), was not
|
|
2600
|
+
* refused in this refusal cycle, and is used by no live tab (its lock is free) may
|
|
2601
|
+
* be adopted: the session runs as that node and uploads its writes with their
|
|
2602
|
+
* original identity, then ends; the next session picks again.
|
|
2603
|
+
* - Never one whose writes depend causally on another local node's unsynced writes:
|
|
2604
|
+
* the parent's node goes first (a child is never uploaded before its parent).
|
|
2605
|
+
* - Never a parked one (its last adoption made no progress, for example the server
|
|
2606
|
+
* kept deferring a write) until something else uploaded or its backoff ran out, so
|
|
2607
|
+
* one stuck node never blocks the others or this tab's own writes.
|
|
2608
|
+
* - Otherwise the session runs as the store's own node; it yields to a deferred node
|
|
2609
|
+
* once its own uploads made that node worth retrying (see maybeEndSessionForNodeWork).
|
|
2610
|
+
*
|
|
2611
|
+
* Also counts the other local nodes' unsynced writes, so they are reported (pending,
|
|
2612
|
+
* or held).
|
|
2613
|
+
*/
|
|
2614
|
+
private chooseSessionNode;
|
|
2615
|
+
/**
|
|
2616
|
+
* Why a local node other than the store's own must not upload on this session, or
|
|
2617
|
+
* null when it may (RT-38, RT-42, RT-50):
|
|
2618
|
+
*
|
|
2619
|
+
* - Bound to another user than the signed-in one (or refused after acceptance,
|
|
2620
|
+
* RT-38): `other-user`.
|
|
2621
|
+
* - Unbound (its owner was never recorded, RT-50) while a user is signed in: its
|
|
2622
|
+
* owner is learned from the server, never guessed. If it was accepted before, the
|
|
2623
|
+
* server holds a claim on it: it is tried (adopted) on this user's session unless
|
|
2624
|
+
* the server already refused it for them, and an accepted handshake binds it to
|
|
2625
|
+
* them. If it never synced nobody can tell whose its writes are: `unassigned`,
|
|
2626
|
+
* held until the app assigns or discards them.
|
|
2627
|
+
*/
|
|
2628
|
+
private holdReason;
|
|
2629
|
+
/** A node bound to another user than the signed-in one (RT-42). */
|
|
2630
|
+
private belongsToAnotherPrincipal;
|
|
2631
|
+
/**
|
|
2632
|
+
* A local node's own operations above its acknowledged prefix that are not
|
|
2633
|
+
* terminally rejected: `count` all of them, `notQueued` those not in the queue, their
|
|
2634
|
+
* `ids`, and the causal parents they name outside this set (`deps`).
|
|
2635
|
+
*/
|
|
2636
|
+
private countUnsyncedOfNode;
|
|
2637
|
+
/** The adoption schedule (RT-46), from persistence when it keeps one. */
|
|
2638
|
+
private loadSchedule;
|
|
2639
|
+
private saveSchedule;
|
|
2640
|
+
/**
|
|
2641
|
+
* Operations of `nodeId` were stored (RT-46): a parked adoption may have waited for
|
|
2642
|
+
* them, so the progress counter moves; the node itself is no longer parked.
|
|
2643
|
+
*/
|
|
2644
|
+
private recordUploadProgress;
|
|
2645
|
+
/** Park the adopted node: it made no progress in this session (RT-46). */
|
|
2646
|
+
private parkAdoption;
|
|
2647
|
+
/** Forget a drained, non-held local node no live tab uses (bounds the registry). */
|
|
2648
|
+
private forgetDrainedNode;
|
|
2649
|
+
/** Release the lock on an adopted node (RT-40). */
|
|
2650
|
+
private endAdoption;
|
|
2651
|
+
/**
|
|
2652
|
+
* Record an accepted handshake as `nodeId` (RT-38), durably when supported. The server
|
|
2653
|
+
* accepted it for the session's user, so an unbound node is now theirs (RT-50).
|
|
2654
|
+
*/
|
|
2655
|
+
private recordNodeAccepted;
|
|
2656
|
+
/** Which of these operation ids the server refused for good (RT-36). */
|
|
2657
|
+
private findTerminalRejections;
|
|
2658
|
+
/** Record durable terminal-rejection markers (RT-36). */
|
|
2659
|
+
private recordTerminalRejections;
|
|
2660
|
+
/**
|
|
2661
|
+
* Restart server->client delivery from 0 at the next session, for the requested and the
|
|
2662
|
+
* accepted views. A full resync includes this client's own operations (a resumed one
|
|
2663
|
+
* does not), which is how operations the device lost come back (RT-35).
|
|
2664
|
+
*/
|
|
2665
|
+
private resetDeliveryForFullResync;
|
|
2666
|
+
/**
|
|
2667
|
+
* The server refused an upload with `SEQUENCE_CONFLICT`: it holds another operation of
|
|
2668
|
+
* this node under that number, one this device lost (RT-35: a reload inside the
|
|
2669
|
+
* IndexedDB snapshot window, a restored older copy). Not a refusal of the write: it
|
|
2670
|
+
* gets a fresh number above everything the server holds (same id and content) and is
|
|
2671
|
+
* uploaded again, and the session ends once its uploads are resolved so the next one
|
|
2672
|
+
* resyncs from 0 and fetches the lost operation back.
|
|
2673
|
+
*
|
|
2674
|
+
* @returns Whether the conflict was recovered (false: treat as a plain rejection)
|
|
2675
|
+
*/
|
|
2676
|
+
private recoverSequenceConflict;
|
|
2677
|
+
/**
|
|
2678
|
+
* A SEQUENCE_CONFLICT above this session's handshake entry (RT-44): the server held
|
|
2679
|
+
* nothing of this node above that entry when the session began, so the operation now
|
|
2680
|
+
* holding the number was stored during this session, and not by this session. Another
|
|
2681
|
+
* live copy of this database uses the same node id (app data copied to a new machine,
|
|
2682
|
+
* a restored image used alongside the original). Renumbering would only collide again,
|
|
2683
|
+
* and each copy's writes count as the other's own operations (never delivered on a
|
|
2684
|
+
* resumed stream), so this copy moves to a fresh node id: the refused write (refused,
|
|
2685
|
+
* so certainly not stored) and every never-sent one are re-authored under it, the
|
|
2686
|
+
* session ends, and the next one resyncs from 0 to fetch what the other copy wrote.
|
|
2687
|
+
*/
|
|
2688
|
+
private recoverFromClone;
|
|
2689
|
+
/**
|
|
2690
|
+
* End the session when it has done its node work and nothing is in flight:
|
|
2691
|
+
* - an own-history recovery is pending (RT-35: reconnect to resync from 0);
|
|
2692
|
+
* - an adopted node's writes are all stored (RT-40: reconnect as the next node, or
|
|
2693
|
+
* this tab's own);
|
|
2694
|
+
* - an adopted node stalled (RT-46: the server deferred part of it): it is parked and
|
|
2695
|
+
* the next session serves another node;
|
|
2696
|
+
* - this tab's own uploads are drained, made progress, and a deferred node is now
|
|
2697
|
+
* worth retrying (RT-46: its parent was among them, or its park has expired).
|
|
2698
|
+
*
|
|
2699
|
+
* @returns Whether the session was ended
|
|
2700
|
+
*/
|
|
2701
|
+
private maybeEndSessionForNodeWork;
|
|
2702
|
+
/**
|
|
2703
|
+
* This tab's own session: once its uploads are drained and it stored something, end
|
|
2704
|
+
* it when a node deferred at session start is worth retrying now (RT-46), so other
|
|
2705
|
+
* nodes' writes do not wait for an unrelated reconnect. Bounded: each yield needs new
|
|
2706
|
+
* own progress, and a parked node at most once per {@link ADOPTION_YIELD_MIN_MS}.
|
|
2707
|
+
*/
|
|
2708
|
+
private maybeYieldToDeferredNode;
|
|
1276
2709
|
private flushQueue;
|
|
1277
2710
|
private startOutboundAckTimer;
|
|
1278
2711
|
private clearOutboundAckTimer;
|
|
2712
|
+
private scheduleOutboundRetry;
|
|
2713
|
+
private clearOutboundRetryTimer;
|
|
2714
|
+
/** No ack arrived in time: return every batch in flight and reconnect. */
|
|
1279
2715
|
private handleOutboundAckTimeout;
|
|
1280
2716
|
private handleAwarenessUpdate;
|
|
1281
2717
|
private handleTransportClose;
|
|
1282
2718
|
private handleTransportError;
|
|
1283
2719
|
private transitionTo;
|
|
1284
2720
|
private setSerializerWireFormat;
|
|
1285
|
-
|
|
1286
|
-
|
|
2721
|
+
/**
|
|
2722
|
+
* Upload predicate: the UPLINK scope only, never query subsets (SYNC-1). Query
|
|
2723
|
+
* subsets describe what this client wants to download; the server authorizes
|
|
2724
|
+
* uploads independently of the client's downloaded view.
|
|
2725
|
+
*/
|
|
2726
|
+
private operationAllowedForUpload;
|
|
2727
|
+
/**
|
|
2728
|
+
* Record a local operation that the uplink scope refuses, so it is visible to
|
|
2729
|
+
* the app instead of silently diverging from the server. Operations on
|
|
2730
|
+
* collections that are not synced in either direction are local-only by design
|
|
2731
|
+
* and are not recorded.
|
|
2732
|
+
*
|
|
2733
|
+
* Such an operation counts as resolved for the contiguous acknowledged prefix: it is
|
|
2734
|
+
* recorded here and never uploaded (W3 step 2).
|
|
2735
|
+
*/
|
|
2736
|
+
private recordOutOfUplinkScope;
|
|
2737
|
+
/**
|
|
2738
|
+
* Whether an operation falls inside the uplink scope. Query subsets never apply to
|
|
2739
|
+
* uploads (SYNC-1), and nothing applies to inbound operations: the client applies
|
|
2740
|
+
* what the server delivered (SYNC-2).
|
|
2741
|
+
*/
|
|
2742
|
+
private matchesUplinkScope;
|
|
1287
2743
|
private readRecordForBackfill;
|
|
1288
|
-
private filterAllowedForSync;
|
|
1289
2744
|
private persistDeltaCursor;
|
|
1290
2745
|
private persistDeliveryWatermark;
|
|
1291
2746
|
private scheduleQuerySubsetReconnect;
|
|
@@ -1366,6 +2821,13 @@ interface ReconnectionConfig {
|
|
|
1366
2821
|
maxAttempts?: number;
|
|
1367
2822
|
/** Jitter factor (0-1). Random variation applied to delay. Defaults to 0.25. */
|
|
1368
2823
|
jitter?: number;
|
|
2824
|
+
/**
|
|
2825
|
+
* How long a connection must stay up (see {@link ReconnectionManager.reportConnected})
|
|
2826
|
+
* before the backoff resets to the initial delay, in ms (SYNC-8). A server that
|
|
2827
|
+
* accepts sessions and drops them right away therefore keeps backing off instead of
|
|
2828
|
+
* being retried at the initial interval forever. Defaults to 10000.
|
|
2829
|
+
*/
|
|
2830
|
+
stableAfterMs?: number;
|
|
1369
2831
|
/** Injectable time source for deterministic testing. */
|
|
1370
2832
|
timeSource?: TimeSource;
|
|
1371
2833
|
/** Injectable random source for deterministic jitter. Returns value in [0, 1). */
|
|
@@ -1375,6 +2837,10 @@ interface ReconnectionConfig {
|
|
|
1375
2837
|
* Manages reconnection attempts with exponential backoff and jitter.
|
|
1376
2838
|
*
|
|
1377
2839
|
* Formula: min(initialDelay * multiplier^attempt, maxDelay) * (1 + jitter * (random - 0.5) * 2)
|
|
2840
|
+
*
|
|
2841
|
+
* The attempt counter belongs to the manager, not to one `start()` run (SYNC-8): it
|
|
2842
|
+
* keeps growing across runs and resets only once a connection stayed up for
|
|
2843
|
+
* `stableAfterMs` ({@link reportConnected}), or on an explicit {@link reset}.
|
|
1378
2844
|
*/
|
|
1379
2845
|
declare class ReconnectionManager {
|
|
1380
2846
|
private readonly initialDelay;
|
|
@@ -1382,20 +2848,44 @@ declare class ReconnectionManager {
|
|
|
1382
2848
|
private readonly multiplier;
|
|
1383
2849
|
private readonly maxAttempts;
|
|
1384
2850
|
private readonly jitter;
|
|
2851
|
+
private readonly stableAfterMs;
|
|
1385
2852
|
private readonly random;
|
|
1386
2853
|
private attempt;
|
|
1387
2854
|
private timer;
|
|
2855
|
+
private stableTimer;
|
|
1388
2856
|
private stopped;
|
|
1389
2857
|
private running;
|
|
2858
|
+
/** A disconnect was reported while an attempt was in flight (NEW-SYNC-2). */
|
|
2859
|
+
private pendingRetry;
|
|
1390
2860
|
private waitResolve;
|
|
1391
2861
|
constructor(config?: ReconnectionConfig);
|
|
1392
2862
|
/**
|
|
1393
2863
|
* Start reconnection attempts. Calls `onReconnect` with exponential backoff.
|
|
1394
2864
|
*
|
|
1395
|
-
*
|
|
2865
|
+
* An attempt counts as successful only when `onReconnect` returns true AND no
|
|
2866
|
+
* disconnect was reported ({@link requestRetry}) while it ran: a session that
|
|
2867
|
+
* drops before the attempt returns is retried, never silently lost (NEW-SYNC-2).
|
|
2868
|
+
*
|
|
2869
|
+
* @param onReconnect - Called on each attempt. Return `true` if reconnection succeeded
|
|
2870
|
+
* (the caller decides what success means; Kora waits for the `streaming` state).
|
|
1396
2871
|
* @returns Promise that resolves when reconnection succeeds or maxAttempts reached.
|
|
1397
2872
|
*/
|
|
1398
2873
|
start(onReconnect: () => Promise<boolean>): Promise<boolean>;
|
|
2874
|
+
/**
|
|
2875
|
+
* Report a disconnect. While a run is in flight it makes the current attempt count
|
|
2876
|
+
* as failed, so the loop retries (with backoff) instead of exiting on a session
|
|
2877
|
+
* that already dropped. Returns true when a running loop took the request; false
|
|
2878
|
+
* means the caller should start a new run.
|
|
2879
|
+
*/
|
|
2880
|
+
requestRetry(): boolean;
|
|
2881
|
+
/**
|
|
2882
|
+
* Report that a connection is up (it reached streaming). If it is still up after
|
|
2883
|
+
* `stableAfterMs`, the backoff resets to the initial delay. A disconnect reported
|
|
2884
|
+
* before then ({@link requestRetry} or {@link reportDisconnected}) cancels the reset.
|
|
2885
|
+
*/
|
|
2886
|
+
reportConnected(): void;
|
|
2887
|
+
/** Report that the connection dropped: a pending backoff reset is cancelled. */
|
|
2888
|
+
reportDisconnected(): void;
|
|
1399
2889
|
/**
|
|
1400
2890
|
* Cancel the current backoff wait and proceed to the next reconnect attempt immediately.
|
|
1401
2891
|
* No-op if not waiting.
|
|
@@ -1406,7 +2896,9 @@ declare class ReconnectionManager {
|
|
|
1406
2896
|
*/
|
|
1407
2897
|
stop(): void;
|
|
1408
2898
|
/**
|
|
1409
|
-
* Reset the attempt counter. Call after a
|
|
2899
|
+
* Reset the attempt counter to the initial delay. Call after a deliberate
|
|
2900
|
+
* (user-initiated) reconnection. It never clears a {@link stop}: a stopped loop
|
|
2901
|
+
* stays stopped, so a stop racing a reset cannot resurrect it (SYNC-8).
|
|
1410
2902
|
*/
|
|
1411
2903
|
reset(): void;
|
|
1412
2904
|
/**
|
|
@@ -1422,6 +2914,7 @@ declare class ReconnectionManager {
|
|
|
1422
2914
|
* Current attempt number (for testing).
|
|
1423
2915
|
*/
|
|
1424
2916
|
getAttemptCount(): number;
|
|
2917
|
+
private clearStableTimer;
|
|
1425
2918
|
private wait;
|
|
1426
2919
|
}
|
|
1427
2920
|
|
|
@@ -1438,6 +2931,42 @@ declare function decodeYjsUpdate(encoded: string): Uint8Array;
|
|
|
1438
2931
|
*/
|
|
1439
2932
|
declare function richtextDocKey(collection: string, recordId: string, field: string): string;
|
|
1440
2933
|
|
|
2934
|
+
/** The part of the sync encryption config that decides what the server can read. */
|
|
2935
|
+
interface EncryptedRelationConfig {
|
|
2936
|
+
enabled?: boolean;
|
|
2937
|
+
cleartextFields?: Readonly<Record<string, readonly string[]>>;
|
|
2938
|
+
}
|
|
2939
|
+
/**
|
|
2940
|
+
* Thrown at app initialization when end-to-end encryption would seal the foreign-key
|
|
2941
|
+
* field of a relation with a referential `onDelete` policy.
|
|
2942
|
+
*/
|
|
2943
|
+
declare class SealedRelationFieldError extends KoraError {
|
|
2944
|
+
readonly relation: string;
|
|
2945
|
+
readonly collection: string;
|
|
2946
|
+
readonly field: string;
|
|
2947
|
+
constructor(message: string, relation: string, collection: string, field: string);
|
|
2948
|
+
}
|
|
2949
|
+
/**
|
|
2950
|
+
* Refuse a configuration where end-to-end encryption seals the foreign key of a
|
|
2951
|
+
* relation whose `onDelete` is `cascade`, `set-null` or `restrict`.
|
|
2952
|
+
*
|
|
2953
|
+
* Referential policies are enforced by the sync server, the one replica that sees every
|
|
2954
|
+
* child: it cascades (or nulls, or refuses) for children no single device knows about,
|
|
2955
|
+
* and relays the result. It can only do so when it can read the foreign key. With the
|
|
2956
|
+
* key sealed, no replica can enforce the policy for everyone, and every device-side
|
|
2957
|
+
* substitute either loses a cascade or deletes a child another device moved away
|
|
2958
|
+
* (RT-74, RT-78, RT-82). So the foreign key of such a relation must travel in cleartext:
|
|
2959
|
+
* it reveals which parent a child belongs to, never the child's other fields.
|
|
2960
|
+
*
|
|
2961
|
+
* Relations with `onDelete: 'no-action'` (or none) are not affected: nothing is
|
|
2962
|
+
* enforced on delete, so their foreign key may stay sealed.
|
|
2963
|
+
*
|
|
2964
|
+
* @param schema - The app schema
|
|
2965
|
+
* @param encryption - The sync encryption config (none, or disabled: nothing to check)
|
|
2966
|
+
* @throws {SealedRelationFieldError} Naming the relation, the field and the fix
|
|
2967
|
+
*/
|
|
2968
|
+
declare function validateEncryptedRelations(schema: SchemaDefinition, encryption: EncryptedRelationConfig | null | undefined): void;
|
|
2969
|
+
|
|
1441
2970
|
/**
|
|
1442
2971
|
* Thrown when key derivation fails.
|
|
1443
2972
|
*/
|
|
@@ -1529,4 +3058,4 @@ declare function getRemoteAwarenessStates(awareness: AwarenessManager): Awarenes
|
|
|
1529
3058
|
*/
|
|
1530
3059
|
declare function subscribeRemoteAwarenessStates(awareness: AwarenessManager, listener: (states: AwarenessState[]) => void): () => void;
|
|
1531
3060
|
|
|
1532
|
-
export { type AwarenessChange, type AwarenessCursor, AwarenessManager, type AwarenessMessage, type AwarenessState, type AwarenessUser, BlobChunkChannel, type BlobChunkChannelMessage, type BlobChunkChannelOptions, BlobChunkPushMessage, BlobChunkRequestMessage, BlobChunkResponseMessage, type ChaosConfig, ChaosTransport, ConnectionMonitor, type ConnectionMonitorConfig, type CursorInfo, DEFAULT_RICHTEXT_DOC_CHANNEL_THRESHOLD, DecryptionError, DeltaCursor, EncryptionError, HttpLongPollingTransport, type HttpLongPollingTransportOptions, JsonMessageSerializer, KeyDerivationError, type MessageSerializer, NegotiatedMessageSerializer, OFFLINE_SYNC_STATUS, type OutboundBatch, OutboundQueue, ProtobufMessageSerializer, QueueStorage, type ReconnectionConfig, ReconnectionManager, RejectedOperation, RejectedOperationStorage, RichtextDocChannel, type RichtextDocChannelOptions, type RichtextDocListener, SCHEMA_MISMATCH_PREFIX, SerializedOperation, SyncConfig, type SyncDiagnostics, SyncEncryptionConfig, SyncEncryptor, SyncEngine, type SyncEngineOptions, SyncMessage, SyncQuerySubset, SyncScopeMap, SyncState, SyncStatePersistence, type SyncStatusController, type SyncStatusControllerOptions, SyncStatusInfo, type SyncStore, SyncTransport, TransportCloseHandler, TransportErrorHandler, TransportMessageHandler, TransportOptions, VersionedKey, type WebSocketConstructor, type WebSocketLike, WebSocketTransport, type WebSocketTransportOptions, WireFormat, YjsDocUpdateMessage, createDeltaCursorFromBatch, createSyncStatusController, decodeBlobChunkBytes, decodeDeltaCursor, decodeYjsUpdate, deriveKey, deriveVersionedKey, encodeBlobChunkBytes, encodeDeltaCursor, encodeYjsUpdate, filterOperationsByScope, generateSalt, getRemoteAwarenessStates, isClientSchemaVersionSupported, isEncryptedPayload, isSchemaMismatchReject, operationMatchesScope, richtextDocKey, sliceOperationsAfterCursor, subscribeRemoteAwarenessStates, versionVectorToWire, wireToVersionVector };
|
|
3061
|
+
export { type AwarenessChange, type AwarenessCursor, AwarenessManager, type AwarenessMessage, type AwarenessState, type AwarenessUser, BlobChunkChannel, type BlobChunkChannelMessage, type BlobChunkChannelOptions, BlobChunkPushMessage, BlobChunkRequestMessage, BlobChunkResponseMessage, type CachedKeyring, type ChaosConfig, ChaosTransport, ConnectionMonitor, type ConnectionMonitorConfig, type CursorInfo, DEFAULT_RICHTEXT_DOC_CHANNEL_THRESHOLD, DecryptionError, DeltaCursor, type EncryptedRelationConfig, EncryptionError, EncryptionKeyError, EncryptionKeyring, type EncryptionKeyringOptions, type EncryptionLockState, type EncryptionStatus, type EncryptionStatusCode, HTTP_SYNC_SESSION_HEADER, HeldNodeInfo, HttpLongPollingTransport, type HttpLongPollingTransportOptions, IndexedDbKeyCache, JsonMessageSerializer, type KeyCache, KeyDerivationError, type KeyServiceChannel, type KeyServiceReply, KeyUnwrapError, MemoryKeyCache, type MessageSerializer, NegotiatedMessageSerializer, NoKeyCache, OFFLINE_SYNC_STATUS, type OutboundBatch, OutboundQueue, ProtobufMessageSerializer, QuarantinedOperation, QueueStorage, type ReconnectionConfig, ReconnectionManager, RejectedOperation, RejectedOperationStorage, RichtextDocChannel, type RichtextDocChannelOptions, type RichtextDocListener, SCHEMA_MISMATCH_PREFIX, ScopeSnapshotOptions, SealedRelationFieldError, SerializedOperation, SyncConfig, type SyncDiagnostics, SyncEncryptionConfig, SyncEncryptor, SyncEngine, type SyncEngineOptions, SyncMessage, SyncQuerySubset, SyncScopeMap, SyncState, SyncStatePersistence, type SyncStatusController, type SyncStatusControllerOptions, SyncStatusInfo, type SyncStore, SyncTransport, TransportCloseHandler, TransportErrorHandler, TransportMessageHandler, TransportOptions, VersionedKey, type WebSocketConstructor, type WebSocketLike, WebSocketTransport, type WebSocketTransportOptions, WireFormat, WrappedKeyRecord, YjsDocUpdateMessage, createDeltaCursorFromBatch, createKeyCache, createSyncStatusController, decodeBlobChunkBytes, decodeDeltaCursor, decodeYjsUpdate, deriveKey, deriveVersionedKey, encodeBlobChunkBytes, encodeDeltaCursor, encodeYjsUpdate, filterOperationsByScope, generateSalt, getRemoteAwarenessStates, isClientSchemaVersionSupported, isEncryptedPayload, isSchemaMismatchReject, operationMatchesScope, richtextDocKey, sliceOperationsAfterCursor, subscribeRemoteAwarenessStates, validateEncryptedRelations, versionVectorToWire, wireToVersionVector };
|