@korajs/sync 1.0.0-beta.12 → 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 +6540 -1103
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1602 -98
- package/dist/index.d.ts +1602 -98
- package/dist/index.js +6250 -1120
- 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-CJvRO5ix.d.cts +0 -745
- package/dist/transport-CJvRO5ix.d.ts +0 -745
|
@@ -0,0 +1,1448 @@
|
|
|
1
|
+
import * as _korajs_core from '@korajs/core';
|
|
2
|
+
import { KoraError, Operation, VersionVector, OperationType, HLCTimestamp, AtomicOp, EncryptedOperationEnvelope } from '@korajs/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Encryption types for the Kora.js sync layer.
|
|
6
|
+
*
|
|
7
|
+
* These types define the configuration and wire format for end-to-end
|
|
8
|
+
* encryption of operation data during sync. Only `data` and `previousData`
|
|
9
|
+
* fields are encrypted — metadata stays in cleartext for routing and ordering.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Supported encryption algorithms. Currently only AES-256-GCM is supported,
|
|
13
|
+
* but the type is extensible for future algorithms.
|
|
14
|
+
*/
|
|
15
|
+
type SyncEncryptionAlgorithm = 'aes-256-gcm';
|
|
16
|
+
/**
|
|
17
|
+
* Configuration for sync-layer end-to-end encryption.
|
|
18
|
+
*
|
|
19
|
+
* Every user has a keyring of random 256-bit data keys. The sync server stores them
|
|
20
|
+
* only wrapped by a key derived from the user's passphrase (PBKDF2-SHA256, 600,000
|
|
21
|
+
* iterations, per-user random salt); it never sees the passphrase or a data key. A
|
|
22
|
+
* device fetches and opens the record during the sync handshake, caches the opened
|
|
23
|
+
* keys (non-extractable), and from then on works offline. Every device of the user
|
|
24
|
+
* that opens the record holds the same keys, so it decrypts everything.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const app = createApp({
|
|
29
|
+
* schema,
|
|
30
|
+
* sync: {
|
|
31
|
+
* url: 'wss://my-server.com/kora',
|
|
32
|
+
* encryption: { enabled: true }, // locked until app.encryption.unlock(passphrase)
|
|
33
|
+
* }
|
|
34
|
+
* })
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
interface SyncEncryptionConfig {
|
|
38
|
+
/** Whether encryption is enabled. When false, all other fields are ignored. */
|
|
39
|
+
enabled: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* The user's encryption passphrase, or an async provider of it. Optional: without
|
|
42
|
+
* it the keyring starts locked (sync paused, local data usable) until
|
|
43
|
+
* `app.encryption.unlock(passphrase)`. With it, the keyring is opened (or, on the
|
|
44
|
+
* user's first device, created) automatically at the first sync handshake.
|
|
45
|
+
*/
|
|
46
|
+
key?: string | (() => Promise<string>);
|
|
47
|
+
/**
|
|
48
|
+
* Keyring name: one encryption scope of the signed-in user. Defaults to 'default'.
|
|
49
|
+
* Distinct keyrings have distinct keys and passphrases.
|
|
50
|
+
*/
|
|
51
|
+
keyring?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Where unlocked keys are kept on the device: 'auto' (IndexedDB in browsers, memory
|
|
54
|
+
* elsewhere), 'indexeddb', 'memory' (until the app closes) or 'none' (ask for the
|
|
55
|
+
* passphrase at every start). Keys are stored non-extractable.
|
|
56
|
+
*/
|
|
57
|
+
keyCache?: 'auto' | 'indexeddb' | 'memory' | 'none';
|
|
58
|
+
/**
|
|
59
|
+
* PBKDF2 iterations for new key records, and the minimum accepted from the server.
|
|
60
|
+
* Defaults to 600,000 (OWASP). Lower it only in tests; every device of a user must
|
|
61
|
+
* use the same value or a lower one.
|
|
62
|
+
*/
|
|
63
|
+
kdfIterations?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Encryption algorithm. Defaults to 'aes-256-gcm'.
|
|
66
|
+
* Currently only AES-256-GCM is supported.
|
|
67
|
+
*/
|
|
68
|
+
algorithm?: SyncEncryptionAlgorithm;
|
|
69
|
+
/**
|
|
70
|
+
* Per collection, the data fields that travel in cleartext beside the encryption
|
|
71
|
+
* envelope, so the server can evaluate sync scopes on them (for example
|
|
72
|
+
* `{ todos: ['ownerId'] }`). Every other field, `previousData` and atomic ops are
|
|
73
|
+
* ciphertext. Values listed here are visible to the server: list only scope keys.
|
|
74
|
+
*/
|
|
75
|
+
cleartextFields?: Record<string, string[]>;
|
|
76
|
+
/**
|
|
77
|
+
* Accept inbound operations without an encryption envelope during a migration from
|
|
78
|
+
* plaintext sync. Off by default: with encryption enabled a plaintext operation is
|
|
79
|
+
* refused (quarantined), because anyone who can reach the sync server could have
|
|
80
|
+
* written it (ENC-3). Turn it on only for the migration window.
|
|
81
|
+
*/
|
|
82
|
+
allowPlaintextMigration?: boolean;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Encrypted payload structure embedded in operation `data` and `previousData`
|
|
86
|
+
* fields when encryption is enabled.
|
|
87
|
+
*
|
|
88
|
+
* This structure replaces the original field values on the wire. The server
|
|
89
|
+
* stores and relays these opaque payloads without being able to read the
|
|
90
|
+
* plaintext contents.
|
|
91
|
+
*/
|
|
92
|
+
interface EncryptedPayload {
|
|
93
|
+
/** Encryption key version. Supports key rotation: older operations may use older key versions. */
|
|
94
|
+
v: number;
|
|
95
|
+
/** Base64-encoded initialization vector (12 bytes for AES-GCM). Unique per operation field. */
|
|
96
|
+
iv: string;
|
|
97
|
+
/** Base64-encoded ciphertext (AES-256-GCM output including authentication tag). */
|
|
98
|
+
ct: string;
|
|
99
|
+
/** Encryption algorithm identifier. */
|
|
100
|
+
alg: SyncEncryptionAlgorithm;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* A versioned encryption key.
|
|
104
|
+
*
|
|
105
|
+
* Key versions enable rotation: operations encrypted with older key versions carry
|
|
106
|
+
* their version number in the envelope, so the decryptor selects the correct key.
|
|
107
|
+
*/
|
|
108
|
+
interface VersionedKey {
|
|
109
|
+
/** Key version number (monotonically increasing, starting at 1). */
|
|
110
|
+
version: number;
|
|
111
|
+
/** The CryptoKey for AES-256-GCM operations. */
|
|
112
|
+
key: CryptoKey;
|
|
113
|
+
/**
|
|
114
|
+
* The id written into envelopes (`keyId`). Keyring keys carry a random id from the
|
|
115
|
+
* key record. When absent, the id is a fingerprint of `salt`.
|
|
116
|
+
*/
|
|
117
|
+
keyId?: string;
|
|
118
|
+
/** The salt a passphrase-derived key was derived with (names it when `keyId` is absent). */
|
|
119
|
+
salt?: Uint8Array;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Internal sync engine states. Used for state machine transitions.
|
|
124
|
+
*/
|
|
125
|
+
declare const SYNC_STATES: readonly ["disconnected", "connecting", "handshaking", "syncing", "streaming", "error"];
|
|
126
|
+
type SyncState = (typeof SYNC_STATES)[number];
|
|
127
|
+
/**
|
|
128
|
+
* Developer-facing sync status. Simplified view of the internal state.
|
|
129
|
+
*/
|
|
130
|
+
declare const SYNC_STATUSES: readonly ["connected", "reconnecting", "syncing", "synced", "offline", "clock-error", "error", "schema-mismatch", "auth-required", "encryption-locked"];
|
|
131
|
+
type SyncStatus = (typeof SYNC_STATUSES)[number];
|
|
132
|
+
type SyncPhase = 'suspended' | 'offline' | 'connecting' | 'authenticating' | 'handshaking' | 'uploading' | 'receiving' | 'applying' | 'streaming' | 'blocked';
|
|
133
|
+
interface ActiveApplyFailure {
|
|
134
|
+
operationId: string;
|
|
135
|
+
collection: string;
|
|
136
|
+
recordId: string;
|
|
137
|
+
code: string;
|
|
138
|
+
message: string;
|
|
139
|
+
retriable: boolean;
|
|
140
|
+
firstSeenAt: number;
|
|
141
|
+
retryCount: number;
|
|
142
|
+
}
|
|
143
|
+
/** Why a local node's unsynced writes are held (RT-38, RT-50). */
|
|
144
|
+
type HeldReason = 'other-user' | 'unassigned';
|
|
145
|
+
/** A local node whose unsynced writes are held (see {@link SyncStatusInfo.heldNodes}). */
|
|
146
|
+
interface HeldNodeInfo {
|
|
147
|
+
nodeId: string;
|
|
148
|
+
/** Unsynced writes the node holds. */
|
|
149
|
+
operationCount: number;
|
|
150
|
+
reason: HeldReason;
|
|
151
|
+
/** The user the node is bound to, or null when nobody knows (unassigned). */
|
|
152
|
+
principal: string | null;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Sync status information exposed to developers.
|
|
156
|
+
*/
|
|
157
|
+
interface SyncStatusInfo {
|
|
158
|
+
/** Current developer-facing status */
|
|
159
|
+
status: SyncStatus;
|
|
160
|
+
phase?: SyncPhase;
|
|
161
|
+
reason?: string;
|
|
162
|
+
/** True when the engine is actively trying to re-establish a transport session. */
|
|
163
|
+
reconnecting: boolean;
|
|
164
|
+
/** Number of operations waiting to be sent */
|
|
165
|
+
pendingOperations: number;
|
|
166
|
+
/** Timestamp of last successful sync (null if never synced) */
|
|
167
|
+
lastSyncedAt: number | null;
|
|
168
|
+
/** Timestamp of last successful push to the server (null if never pushed) */
|
|
169
|
+
lastSuccessfulPush: number | null;
|
|
170
|
+
/** Timestamp of last successful pull from the server (null if never pulled) */
|
|
171
|
+
lastSuccessfulPull: number | null;
|
|
172
|
+
/** Number of merge conflicts encountered during this session */
|
|
173
|
+
conflicts: number;
|
|
174
|
+
/**
|
|
175
|
+
* Unsynced writes of another user who shared this local database (RT-38): the server
|
|
176
|
+
* refused their node for the signed-in principal, so they wait, not counted in
|
|
177
|
+
* `pendingOperations`, until that user signs in again on this device. Use
|
|
178
|
+
* `store.namespaceByAuthUser` to give each user their own database instead.
|
|
179
|
+
*/
|
|
180
|
+
heldOperations?: number;
|
|
181
|
+
/**
|
|
182
|
+
* The local nodes whose unsynced writes are held (RT-38, RT-50), with how many each
|
|
183
|
+
* holds and why: `other-user` (they belong to another user, or the server refused
|
|
184
|
+
* the node for the signed-in one: they upload when their user signs in) or
|
|
185
|
+
* `unassigned` (written before the app knew who was signed in, on a node that never
|
|
186
|
+
* synced: nobody can tell whose they are, so the app decides with
|
|
187
|
+
* `app.sync.assignHeld` or `app.sync.discardHeld`). As of the last session start.
|
|
188
|
+
*/
|
|
189
|
+
heldNodes?: HeldNodeInfo[];
|
|
190
|
+
/**
|
|
191
|
+
* `degraded` when this device's local database could not be made durable several times
|
|
192
|
+
* in a row (storage quota exceeded, IndexedDB broken; RT-49). Uploads continue so the
|
|
193
|
+
* server keeps a durable copy, and a later reload recovers its writes from the server;
|
|
194
|
+
* writes made while offline in this state may be lost if the page closes. Show the user
|
|
195
|
+
* a warning (free up storage, stay online). `durable` otherwise.
|
|
196
|
+
*/
|
|
197
|
+
localDurability?: 'durable' | 'degraded';
|
|
198
|
+
/**
|
|
199
|
+
* Sync protocol version the server answered with in the current (or last) session,
|
|
200
|
+
* or null before any server answered.
|
|
201
|
+
*/
|
|
202
|
+
serverProtocolVersion?: number | null;
|
|
203
|
+
/**
|
|
204
|
+
* True when the server speaks an older sync protocol than this client (a pre-beta.13
|
|
205
|
+
* server). It still syncs, but that protocol is deprecated and a later release
|
|
206
|
+
* refuses it: upgrade the sync server.
|
|
207
|
+
*/
|
|
208
|
+
protocolDeprecated?: boolean;
|
|
209
|
+
/** serverTime - localTime in ms measured at the last handshake, or null before first connect. Negative = this device's clock is fast. */
|
|
210
|
+
clockSkewMs: number | null;
|
|
211
|
+
inFlightUploadOperations?: number;
|
|
212
|
+
hasInFlightDeliveryBatch?: boolean;
|
|
213
|
+
activeViewId?: string;
|
|
214
|
+
activeViewComplete?: boolean;
|
|
215
|
+
initialSync?: {
|
|
216
|
+
complete: boolean;
|
|
217
|
+
receivedBatches: number;
|
|
218
|
+
totalBatches: number | null;
|
|
219
|
+
progress: number | null;
|
|
220
|
+
};
|
|
221
|
+
deliveryWatermark?: number;
|
|
222
|
+
serverFrontier?: number | null;
|
|
223
|
+
blockedFailure?: ActiveApplyFailure | null;
|
|
224
|
+
}
|
|
225
|
+
interface SyncSettlementOptions {
|
|
226
|
+
upload?: boolean;
|
|
227
|
+
download?: 'active-view' | false;
|
|
228
|
+
timeoutMs?: number;
|
|
229
|
+
signal?: AbortSignal;
|
|
230
|
+
}
|
|
231
|
+
type SyncSettlementResult = {
|
|
232
|
+
outcome: 'settled';
|
|
233
|
+
status: SyncStatusInfo;
|
|
234
|
+
} | {
|
|
235
|
+
outcome: 'offline';
|
|
236
|
+
status: SyncStatusInfo;
|
|
237
|
+
} | {
|
|
238
|
+
outcome: 'suspended';
|
|
239
|
+
reason: string;
|
|
240
|
+
status: SyncStatusInfo;
|
|
241
|
+
} | {
|
|
242
|
+
outcome: 'blocked';
|
|
243
|
+
failure: ActiveApplyFailure;
|
|
244
|
+
status: SyncStatusInfo;
|
|
245
|
+
} | {
|
|
246
|
+
outcome: 'timeout';
|
|
247
|
+
status: SyncStatusInfo;
|
|
248
|
+
} | {
|
|
249
|
+
outcome: 'aborted';
|
|
250
|
+
status: SyncStatusInfo;
|
|
251
|
+
};
|
|
252
|
+
/**
|
|
253
|
+
* Per-collection sync scope map. Maps collection names to field-value filters.
|
|
254
|
+
* Empty filter `{}` means no restriction (all records visible).
|
|
255
|
+
* Missing collection means hidden (no records visible for that collection).
|
|
256
|
+
*/
|
|
257
|
+
type SyncScopeMap = Record<string, Record<string, unknown>>;
|
|
258
|
+
/** Options passed to {@link SyncConfig.auth}. */
|
|
259
|
+
interface SyncAuthRequest {
|
|
260
|
+
/**
|
|
261
|
+
* True when the server ended the previous session because its credential
|
|
262
|
+
* expired or was revoked. A cached token must not be reused.
|
|
263
|
+
*/
|
|
264
|
+
forceRefresh?: boolean;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Sync configuration provided by the developer.
|
|
268
|
+
*/
|
|
269
|
+
interface SyncConfig {
|
|
270
|
+
/** WebSocket or HTTP URL for the sync server */
|
|
271
|
+
url: string;
|
|
272
|
+
/** Transport type to use. Defaults to 'websocket'. */
|
|
273
|
+
transport?: 'websocket' | 'http';
|
|
274
|
+
/**
|
|
275
|
+
* Auth provider function. Called before each connection attempt. After the
|
|
276
|
+
* server ends a session with `AUTH_EXPIRED` or `AUTH_REVOKED` it is called
|
|
277
|
+
* with `{ forceRefresh: true }`: return a freshly refreshed token, never the
|
|
278
|
+
* cached one the server just refused.
|
|
279
|
+
*/
|
|
280
|
+
auth?: (options?: SyncAuthRequest) => Promise<{
|
|
281
|
+
token: string;
|
|
282
|
+
}>;
|
|
283
|
+
/**
|
|
284
|
+
* The signed-in user whose writes this device makes (RT-42): a user id, or null /
|
|
285
|
+
* undefined when nobody is known to be signed in. Called before each session. With
|
|
286
|
+
* it, the engine binds the store's node to that user (the store moves to the user's
|
|
287
|
+
* own node when another user's is in use) and never uploads, or adopts, a node that
|
|
288
|
+
* belongs to another user: those writes wait for their user (`heldOperations`).
|
|
289
|
+
* `createApp` derives it from `sync.authClient`.
|
|
290
|
+
*/
|
|
291
|
+
principal?: () => Promise<string | null | undefined>;
|
|
292
|
+
/** Auth readiness gate. A suspended result prevents transport creation and retries. */
|
|
293
|
+
authState?: () => Promise<{
|
|
294
|
+
state: 'loading' | 'signed-out' | 'anonymous' | 'authenticated';
|
|
295
|
+
mayConnectAnonymously?: boolean;
|
|
296
|
+
}>;
|
|
297
|
+
querySubsets?: {
|
|
298
|
+
mode?: 'reactive' | 'static' | 'disabled';
|
|
299
|
+
};
|
|
300
|
+
/** Client-local behavior for records leaving the accepted downlink scope. Defaults to `retain`. */
|
|
301
|
+
scopeExit?: 'retain' | 'retract';
|
|
302
|
+
/** Sync scopes per collection. Limits which records sync to this client. */
|
|
303
|
+
scopes?: Record<string, (ctx: SyncScopeContext) => Record<string, unknown>>;
|
|
304
|
+
/**
|
|
305
|
+
* Pre-computed per-collection sync scope map. Sent to the server in the handshake.
|
|
306
|
+
* Built automatically by createApp from schema scope declarations + flat scope values.
|
|
307
|
+
*/
|
|
308
|
+
scopeMap?: SyncScopeMap;
|
|
309
|
+
/**
|
|
310
|
+
* Collections the app's schema syncs (all of them, or the sync-scoped ones when the
|
|
311
|
+
* schema declares partial sync rules). Set by createApp. When no `scopeMap` is
|
|
312
|
+
* configured, a write on one of these that the server's accepted upload scope omits
|
|
313
|
+
* is surfaced as `sync:operation-rejected` (`OUT_OF_UPLINK_SCOPE`) instead of being
|
|
314
|
+
* treated as local-only (RT-89). Omitted: such writes stay local without a rejection.
|
|
315
|
+
*/
|
|
316
|
+
syncedCollections?: readonly string[];
|
|
317
|
+
/** Number of operations per batch. Defaults to 100. */
|
|
318
|
+
batchSize?: number;
|
|
319
|
+
/**
|
|
320
|
+
* Maximum time to wait for the server to acknowledge an outbound batch before
|
|
321
|
+
* treating the connection as stalled. The batch is returned to the durable
|
|
322
|
+
* outbound queue before disconnecting so it can be retried on reconnect.
|
|
323
|
+
* Defaults to 30000ms. Set to 0 to disable the watchdog.
|
|
324
|
+
*/
|
|
325
|
+
outboundAckTimeoutMs?: number;
|
|
326
|
+
/** Initial delay before resending a transiently rejected outbound operation. Defaults to 250ms. */
|
|
327
|
+
outboundRetryBaseDelayMs?: number;
|
|
328
|
+
/** Maximum transient-operation retry delay. Defaults to 30000ms. */
|
|
329
|
+
outboundRetryMaxDelayMs?: number;
|
|
330
|
+
/** Initial reconnection delay in ms. Defaults to 1000. */
|
|
331
|
+
reconnectInterval?: number;
|
|
332
|
+
/** Maximum reconnection delay in ms. Defaults to 30000. */
|
|
333
|
+
maxReconnectInterval?: number;
|
|
334
|
+
/** Schema version of this client. */
|
|
335
|
+
schemaVersion?: number;
|
|
336
|
+
/** Start sync automatically when the engine is created. Defaults to false. */
|
|
337
|
+
autoConnect?: boolean;
|
|
338
|
+
/**
|
|
339
|
+
* When true, wait for server ACKs on all outbound handshake delta batches before
|
|
340
|
+
* entering streaming. Improves backpressure for large initial syncs.
|
|
341
|
+
*/
|
|
342
|
+
strictHandshake?: boolean;
|
|
343
|
+
/**
|
|
344
|
+
* Optional schema transforms for cross-schema-version sync. They run at fold time
|
|
345
|
+
* (RT-84): delivered operations are stored exactly as received and the local store
|
|
346
|
+
* folds their transformed view, so the store must be given the same list (`createApp`
|
|
347
|
+
* does). An operation with no transform path to this schema version is kept aside
|
|
348
|
+
* and replayed after an upgrade.
|
|
349
|
+
*/
|
|
350
|
+
operationTransforms?: _korajs_core.OperationTransform[];
|
|
351
|
+
/**
|
|
352
|
+
* Richtext snapshot size (bytes) at which the optional Yjs doc channel is used.
|
|
353
|
+
* Defaults to 4096.
|
|
354
|
+
*/
|
|
355
|
+
richtextDocChannelThreshold?: number;
|
|
356
|
+
/**
|
|
357
|
+
* End-to-end encryption configuration.
|
|
358
|
+
* When enabled, `data` and `previousData` fields are encrypted before sending
|
|
359
|
+
* over the wire. The server never sees plaintext user data.
|
|
360
|
+
*/
|
|
361
|
+
encryption?: SyncEncryptionConfig;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Context passed to sync scope functions.
|
|
365
|
+
*/
|
|
366
|
+
interface SyncScopeContext {
|
|
367
|
+
userId?: string;
|
|
368
|
+
[key: string]: unknown;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Persists last-acked server version vector and computes unsynced operations from the op log.
|
|
372
|
+
*/
|
|
373
|
+
interface DeltaCursor {
|
|
374
|
+
/** ID of the last fully applied operation in the previous delta stream */
|
|
375
|
+
lastOperationId: string;
|
|
376
|
+
/** Zero-based batch index where the cursor was recorded */
|
|
377
|
+
batchIndex: number;
|
|
378
|
+
}
|
|
379
|
+
interface SyncStatePersistence {
|
|
380
|
+
loadLastAckedServerVector(): Promise<VersionVector>;
|
|
381
|
+
saveLastAckedServerVector(vector: VersionVector): Promise<void>;
|
|
382
|
+
mergeServerVectors(a: VersionVector, b: VersionVector): VersionVector;
|
|
383
|
+
countUnsyncedOperations(serverVector: VersionVector): Promise<number>;
|
|
384
|
+
getUnsyncedOperations(serverVector: VersionVector): Promise<Operation[]>;
|
|
385
|
+
/** Resume position for paginated initial sync (optional). */
|
|
386
|
+
loadDeltaCursor?(): Promise<DeltaCursor | null>;
|
|
387
|
+
saveDeltaCursor?(cursor: DeltaCursor | null): Promise<void>;
|
|
388
|
+
/**
|
|
389
|
+
* The delivery watermark for a sync view, keyed by an opaque view signature (the auth
|
|
390
|
+
* scope plus active query subscriptions). Persisted per view so a restarted client
|
|
391
|
+
* resumes each view's gap-free stream from the right point, and switching views does not
|
|
392
|
+
* re-sync a view already synced. The empty-string signature is the default, unfiltered
|
|
393
|
+
* view. Optional: a persistence layer that omits these falls back to re-sync on restart.
|
|
394
|
+
*/
|
|
395
|
+
loadDeliveryWatermark?(signature: string): Promise<number>;
|
|
396
|
+
saveDeliveryWatermark?(signature: string, watermark: number): Promise<void>;
|
|
397
|
+
/** Load every persisted view watermark, keyed by signature. */
|
|
398
|
+
loadAllDeliveryWatermarks?(): Promise<Record<string, number>>;
|
|
399
|
+
/**
|
|
400
|
+
* Delete a persisted view watermark. Used to bound the number of stored view watermarks
|
|
401
|
+
* (a cold view is evicted from the client's cache and its persisted row removed). Safe:
|
|
402
|
+
* an evicted view back-fills from 0 (deduplicated) when next visited.
|
|
403
|
+
*/
|
|
404
|
+
deleteDeliveryWatermark?(signature: string): Promise<void>;
|
|
405
|
+
/**
|
|
406
|
+
* The per-device node token the server issued at this node id's first anonymous
|
|
407
|
+
* claim (RT-12), stored next to the node id so the device can reconnect with it.
|
|
408
|
+
* Optional: without it the token lives only as long as the sync engine.
|
|
409
|
+
*/
|
|
410
|
+
loadNodeToken?(nodeId?: string): Promise<string | null>;
|
|
411
|
+
/**
|
|
412
|
+
* `nodeId` names the node the token was issued for: a database that authors under
|
|
413
|
+
* several node ids (a rotated identity, per-tab isolation) keeps one token per node.
|
|
414
|
+
*/
|
|
415
|
+
saveNodeToken?(token: string, nodeId?: string): Promise<void>;
|
|
416
|
+
/**
|
|
417
|
+
* The node ids the server named authoritative in its last accepted handshake response
|
|
418
|
+
* (`HandshakeResponseMessage.authoritativeNodeIds`, protocol v2): operations from
|
|
419
|
+
* these nodes are server-authored and may carry `fieldVersions` / `foldState`.
|
|
420
|
+
* Returns null when no protocol-2 server ever answered. Optional.
|
|
421
|
+
*/
|
|
422
|
+
loadAuthoritativeNodeIds?(): Promise<string[] | null>;
|
|
423
|
+
saveAuthoritativeNodeIds?(nodeIds: string[]): Promise<void>;
|
|
424
|
+
/**
|
|
425
|
+
* The contiguous acknowledged prefix of this device's own operations (W3): the highest
|
|
426
|
+
* sequence s such that every own operation with sequence <= s is stored on the server,
|
|
427
|
+
* terminally rejected and recorded, or was never upload-eligible. Keyed by node id, so
|
|
428
|
+
* a rotated node starts from 0. Returns null when nothing was ever recorded under this
|
|
429
|
+
* contract (a device upgrading from a release that persisted a max, not a prefix): the
|
|
430
|
+
* engine then re-uploads the device's own history once, from 0; the server dedups by id.
|
|
431
|
+
* Optional: without it the engine trusts the own entry of the last acked server vector.
|
|
432
|
+
*/
|
|
433
|
+
loadOwnAckedThrough?(nodeId: string): Promise<number | null>;
|
|
434
|
+
saveOwnAckedThrough?(nodeId: string, sequence: number): Promise<void>;
|
|
435
|
+
/**
|
|
436
|
+
* Durable inbound quarantine (W4): delivered operations the client deliberately did
|
|
437
|
+
* not apply (unknown collection, transform unavailable, deferred or rejected apply,
|
|
438
|
+
* far-future timestamp, undecryptable payload). When `watermark` is given, the rows and
|
|
439
|
+
* the delivery watermark advance MUST be written in one transaction, so the watermark
|
|
440
|
+
* never passes an operation that is neither applied nor recorded here. Optional: a
|
|
441
|
+
* persistence layer without it keeps the old behaviour (the watermark stalls instead).
|
|
442
|
+
*/
|
|
443
|
+
saveQuarantine?(entries: QuarantinedOperation[], watermark?: {
|
|
444
|
+
signature: string;
|
|
445
|
+
watermark: number;
|
|
446
|
+
}): Promise<void>;
|
|
447
|
+
/** Every quarantined operation, oldest delivery first. */
|
|
448
|
+
loadQuarantine?(): Promise<QuarantinedOperation[]>;
|
|
449
|
+
/** Remove quarantined operations once applied (replay) or reconciled. */
|
|
450
|
+
removeQuarantine?(operationIds: string[]): Promise<void>;
|
|
451
|
+
/**
|
|
452
|
+
* The downlink scope the server last accepted for this device (SYNC-11). The next
|
|
453
|
+
* handshake reports its canonical key and that view's delivery watermark next to the
|
|
454
|
+
* requested view's, so a server that resolves the same scope again resumes the
|
|
455
|
+
* stream instead of restarting it from 0. Never sent as the requested scope.
|
|
456
|
+
* Optional; without it every handshake with a server-chosen scope rescans from 0.
|
|
457
|
+
*/
|
|
458
|
+
loadAcceptedDownlinkScope?(): Promise<SyncScopeMap | null>;
|
|
459
|
+
saveAcceptedDownlinkScope?(scope: SyncScopeMap | null): Promise<void>;
|
|
460
|
+
/**
|
|
461
|
+
* Durable terminal-rejection markers (RT-36): operations the server refused with a
|
|
462
|
+
* non-retriable rejection. Never cleared (unlike the app's rejected list), so a
|
|
463
|
+
* rescan of the device's own history never submits a refused operation again.
|
|
464
|
+
* Optional: without them the engine remembers refusals for its own lifetime only.
|
|
465
|
+
*/
|
|
466
|
+
recordTerminalRejections?(entries: TerminalRejectionRecord[]): Promise<void>;
|
|
467
|
+
/** Which of these operation ids carry a terminal-rejection marker. */
|
|
468
|
+
findTerminalRejections?(operationIds: string[]): Promise<Set<string>>;
|
|
469
|
+
/**
|
|
470
|
+
* The node ids this database authored operations under (RT-38, RT-40), with what the
|
|
471
|
+
* sync server said about each. Optional: without it the engine tracks only the
|
|
472
|
+
* current node and keeps the pre-Phase-2 behaviour for refused nodes.
|
|
473
|
+
*/
|
|
474
|
+
listLocalNodes?(): Promise<LocalNodeInfo[]>;
|
|
475
|
+
/** Record an accepted handshake as `nodeId`; starts a new refusal cycle. */
|
|
476
|
+
markLocalNodeAccepted?(nodeId: string): Promise<void>;
|
|
477
|
+
/** Record that the server refused `nodeId`; `held` holds its unsynced writes. */
|
|
478
|
+
markLocalNodeRefused?(nodeId: string, held: boolean): Promise<void>;
|
|
479
|
+
/**
|
|
480
|
+
* A handshake as `nodeId` was accepted for `principal` (RT-50): bind an unbound node
|
|
481
|
+
* (or replace a guessed binding) from the server's answer.
|
|
482
|
+
*/
|
|
483
|
+
confirmLocalNodePrincipal?(nodeId: string, principal: string): Promise<void>;
|
|
484
|
+
/**
|
|
485
|
+
* The server refused `nodeId` for `principal` (RT-50): clear a guessed binding, and
|
|
486
|
+
* remember that an unbound node is not theirs.
|
|
487
|
+
*/
|
|
488
|
+
recordLocalNodeRefusedFor?(nodeId: string, principal: string): Promise<void>;
|
|
489
|
+
/**
|
|
490
|
+
* Assign a held, unbound node's writes to `principal` (RT-50, the app's decision).
|
|
491
|
+
* @returns false when the node is not unbound, or was refused for `principal`
|
|
492
|
+
*/
|
|
493
|
+
assignLocalNodePrincipal?(nodeId: string, principal: string): Promise<boolean>;
|
|
494
|
+
/** Forget a non-current local node whatever it holds (its held writes were discarded). */
|
|
495
|
+
dropLocalNode?(nodeId: string): Promise<void>;
|
|
496
|
+
/** The current refusal cycle (count of accepted handshakes). */
|
|
497
|
+
loadAcceptedCycle?(): Promise<number>;
|
|
498
|
+
/** Forget a non-current local node with nothing left to upload (bounds the registry). */
|
|
499
|
+
forgetLocalNode?(nodeId: string): Promise<void>;
|
|
500
|
+
/**
|
|
501
|
+
* Which adoptions of other local nodes are parked (RT-46), and an upload-progress
|
|
502
|
+
* counter. Optional: without it the schedule lives for the engine's lifetime.
|
|
503
|
+
*/
|
|
504
|
+
loadAdoptionSchedule?(): Promise<AdoptionScheduleInfo>;
|
|
505
|
+
saveAdoptionSchedule?(schedule: AdoptionScheduleInfo): Promise<void>;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Upload scheduling across a database's local nodes (RT-46). A node whose adoption made
|
|
509
|
+
* no progress is parked: it is retried once anything else uploaded (`progress` moved past
|
|
510
|
+
* `progressMark`) or after its backoff (`untilMs`).
|
|
511
|
+
*/
|
|
512
|
+
interface AdoptionScheduleInfo {
|
|
513
|
+
progress: number;
|
|
514
|
+
parked: Record<string, {
|
|
515
|
+
progressMark: number;
|
|
516
|
+
untilMs: number;
|
|
517
|
+
count: number;
|
|
518
|
+
parkedAtMs: number;
|
|
519
|
+
}>;
|
|
520
|
+
}
|
|
521
|
+
/** A terminally rejected operation, as recorded in the durable markers (RT-36). */
|
|
522
|
+
interface TerminalRejectionRecord {
|
|
523
|
+
operationId: string;
|
|
524
|
+
nodeId: string | null;
|
|
525
|
+
sequenceNumber: number | null;
|
|
526
|
+
code: string;
|
|
527
|
+
rejectedAt: number;
|
|
528
|
+
}
|
|
529
|
+
/** A node id this database authored operations under (RT-38, RT-40). */
|
|
530
|
+
interface LocalNodeInfo {
|
|
531
|
+
nodeId: string;
|
|
532
|
+
/** A handshake as this node was accepted at least once. */
|
|
533
|
+
accepted: boolean;
|
|
534
|
+
/** Refused after acceptance: its unsynced writes belong to a principal not signed in. */
|
|
535
|
+
held: boolean;
|
|
536
|
+
/** Refusal cycle in which the server last refused it, or null. */
|
|
537
|
+
refusedCycle: number | null;
|
|
538
|
+
/**
|
|
539
|
+
* The user the node's writes belong to (RT-42), or null when unknown. A node bound to
|
|
540
|
+
* another user than the signed-in one is never uploaded or adopted.
|
|
541
|
+
*/
|
|
542
|
+
principal?: string | null;
|
|
543
|
+
/**
|
|
544
|
+
* How `principal` was learned (RT-50): `fresh` (bound before its first write),
|
|
545
|
+
* `server` (an accepted handshake), `app` (assigned by the app: a guess the server can
|
|
546
|
+
* overrule), or null (unbound, or a guess by an earlier release).
|
|
547
|
+
*/
|
|
548
|
+
binding?: 'fresh' | 'server' | 'app' | null;
|
|
549
|
+
/** Users the server refused this unbound node for (RT-50): never tried for them again. */
|
|
550
|
+
refusedPrincipals?: string[];
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* A delivered operation the client deliberately did not apply, kept durably so it is
|
|
554
|
+
* never silently lost and can be replayed (on start, after a schema upgrade, or on demand).
|
|
555
|
+
*/
|
|
556
|
+
interface QuarantinedOperation {
|
|
557
|
+
/** The operation as delivered (still encrypted when decryption failed). */
|
|
558
|
+
operation: Operation;
|
|
559
|
+
/** Delivery sequence of the batch that carried it, or null for a legacy batch. */
|
|
560
|
+
deliverySequence: number | null;
|
|
561
|
+
/** Machine-readable reason (for example `APPLY_SKIPPED`, `REMOTE_CLOCK_DRIFT`). */
|
|
562
|
+
code: string;
|
|
563
|
+
/** Human-readable explanation. */
|
|
564
|
+
message: string;
|
|
565
|
+
/** Wall-clock time (ms) it was quarantined. Display only. */
|
|
566
|
+
quarantinedAt: number;
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Interface for persisting the outbound operation queue.
|
|
570
|
+
* Operations must survive page refreshes and be sent when connection is re-established.
|
|
571
|
+
*/
|
|
572
|
+
interface QueueStorage {
|
|
573
|
+
/** Load all queued operations from persistent storage */
|
|
574
|
+
load(): Promise<Operation[]>;
|
|
575
|
+
/** Persist an operation to the queue */
|
|
576
|
+
enqueue(op: Operation): Promise<void>;
|
|
577
|
+
/** Remove acknowledged operations by their IDs */
|
|
578
|
+
dequeue(ids: string[]): Promise<void>;
|
|
579
|
+
/** Return number of operations in storage */
|
|
580
|
+
count(): Promise<number>;
|
|
581
|
+
/**
|
|
582
|
+
* Record that these queued operations were put on the wire at least once. A sent
|
|
583
|
+
* operation may already be stored on the server (its ack can be lost), so it must
|
|
584
|
+
* never be re-stamped by a clock rebase (W3 step 4). Optional: without it the flag
|
|
585
|
+
* lives for the engine's lifetime only.
|
|
586
|
+
*/
|
|
587
|
+
markSent?(ops: Operation[]): Promise<void>;
|
|
588
|
+
/** Ids of queued operations recorded by {@link markSent}. */
|
|
589
|
+
loadSentIds?(): Promise<string[]>;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* A record of one outbound operation the server permanently refused. Preserved so
|
|
593
|
+
* a rejected op is explainable and reconcilable rather than silently lost.
|
|
594
|
+
*/
|
|
595
|
+
interface RejectedOperation {
|
|
596
|
+
/** Content-addressed id of the rejected operation. */
|
|
597
|
+
operationId: string;
|
|
598
|
+
/** Collection the operation targeted. */
|
|
599
|
+
collection: string;
|
|
600
|
+
/** Record the operation targeted. */
|
|
601
|
+
recordId: string;
|
|
602
|
+
/** Stable, machine-readable reason code from the server. */
|
|
603
|
+
code: string;
|
|
604
|
+
/** Human-readable explanation. */
|
|
605
|
+
message: string;
|
|
606
|
+
/** Whether resubmitting the identical operation may later succeed. */
|
|
607
|
+
retriable: boolean;
|
|
608
|
+
/** Wall-clock time (ms since epoch) the rejection was recorded. Display only. */
|
|
609
|
+
rejectedAt: number;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Interface for durably persisting operations the server rejected, so the record
|
|
613
|
+
* survives a page refresh. Keyed by operation id (idempotent on re-rejection).
|
|
614
|
+
*/
|
|
615
|
+
interface RejectedOperationStorage {
|
|
616
|
+
/** Record (or overwrite) a rejected operation. */
|
|
617
|
+
record(rejected: RejectedOperation): Promise<void>;
|
|
618
|
+
/** List all recorded rejected operations. */
|
|
619
|
+
list(): Promise<RejectedOperation[]>;
|
|
620
|
+
/** Remove rejected operations by their operation ids (after the app reconciles them). */
|
|
621
|
+
remove(operationIds: string[]): Promise<void>;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* In-memory {@link RejectedOperationStorage}. The default when no durable store is
|
|
625
|
+
* provided; suitable for tests and ephemeral sessions.
|
|
626
|
+
*/
|
|
627
|
+
declare class MemoryRejectedOperationStorage implements RejectedOperationStorage {
|
|
628
|
+
private readonly rejected;
|
|
629
|
+
record(rejected: RejectedOperation): Promise<void>;
|
|
630
|
+
list(): Promise<RejectedOperation[]>;
|
|
631
|
+
remove(operationIds: string[]): Promise<void>;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Thrown when an operation violates sync scope constraints.
|
|
635
|
+
* This can happen when:
|
|
636
|
+
* - A client tries to push an operation outside its configured scope
|
|
637
|
+
* - The server rejects an operation because it falls outside the client's scope
|
|
638
|
+
*/
|
|
639
|
+
declare class ScopeViolationError extends KoraError {
|
|
640
|
+
readonly operationId: string;
|
|
641
|
+
readonly collection: string;
|
|
642
|
+
readonly scope: Record<string, unknown>;
|
|
643
|
+
constructor(operationId: string, collection: string, scope: Record<string, unknown>, message?: string);
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Thrown when a sync scope configuration is invalid.
|
|
647
|
+
*/
|
|
648
|
+
declare class InvalidScopeError extends KoraError {
|
|
649
|
+
constructor(message: string, context?: Record<string, unknown>);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/** Options for {@link buildScopeSnapshot}. */
|
|
653
|
+
interface ScopeSnapshotOptions {
|
|
654
|
+
/**
|
|
655
|
+
* Layer the writer's `previousData` between the stored record and `op.data`.
|
|
656
|
+
* Off by default: `previousData` is supplied by the writer and is never checked
|
|
657
|
+
* against the stored row, so a server visibility decision built on it lets one
|
|
658
|
+
* tenant steer its operations into another tenant's log (RT-3). Opt in only
|
|
659
|
+
* where the writer is trusted for the decision, such as a client filtering its
|
|
660
|
+
* own local view.
|
|
661
|
+
* @default false
|
|
662
|
+
*/
|
|
663
|
+
includePreviousData?: boolean;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Build the record snapshot a scope or query-subset predicate is evaluated against
|
|
667
|
+
* for an operation.
|
|
668
|
+
*
|
|
669
|
+
* Layering is `fullRecord < data` (or `fullRecord < previousData < data` with
|
|
670
|
+
* `includePreviousData`), so the snapshot approximates the record after the
|
|
671
|
+
* operation. The record identity is NOT taken from any of those
|
|
672
|
+
* layers: `id` is always `op.recordId`, assigned last. An operation (or a stale
|
|
673
|
+
* stored row) can therefore never claim to be a different record than the one it
|
|
674
|
+
* actually targets, which is what made `previousData: { id: '<allowed>' }` a write
|
|
675
|
+
* bypass under id-scoped predicates.
|
|
676
|
+
*
|
|
677
|
+
* This is the single snapshot builder shared by the client scope filter, the client
|
|
678
|
+
* query-subset filter and the server scope filter. It is used for visibility
|
|
679
|
+
* decisions only; upload authorization never reads `previousData` (see the server's
|
|
680
|
+
* `authorizeUplinkWrite`).
|
|
681
|
+
*
|
|
682
|
+
* @param op - The operation being judged
|
|
683
|
+
* @param fullRecord - Optional stored record state used to fill fields the op omits
|
|
684
|
+
* @param options - Whether to trust the writer's `previousData` (default: no)
|
|
685
|
+
* @returns The snapshot, always carrying `id === op.recordId`
|
|
686
|
+
*/
|
|
687
|
+
declare function buildScopeSnapshot(op: Operation, fullRecord?: Record<string, unknown> | null, options?: ScopeSnapshotOptions): Record<string, unknown>;
|
|
688
|
+
/**
|
|
689
|
+
* True when `actual` satisfies a scope predicate value: either an exact value
|
|
690
|
+
* (compared with `Object.is`) or a bounded `{ $in: [...] }` set. An `undefined` or
|
|
691
|
+
* `null` predicate value (or `$in` member) never matches.
|
|
692
|
+
*
|
|
693
|
+
* @param actual - The record's field value
|
|
694
|
+
* @param expected - The scope predicate for that field
|
|
695
|
+
* @returns Whether the value is inside the predicate
|
|
696
|
+
*/
|
|
697
|
+
declare function matchesScopePredicate(actual: unknown, expected: unknown): boolean;
|
|
698
|
+
/**
|
|
699
|
+
* True when every field predicate in a collection scope matches the record.
|
|
700
|
+
*
|
|
701
|
+
* @param record - The record (or snapshot) to test
|
|
702
|
+
* @param collectionScope - Field predicates for the record's collection
|
|
703
|
+
* @returns Whether the record is inside the scope
|
|
704
|
+
*/
|
|
705
|
+
declare function recordMatchesScopePredicates(record: Record<string, unknown>, collectionScope: Record<string, unknown>): boolean;
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* A live query filter that narrows which operations sync for a collection.
|
|
709
|
+
*/
|
|
710
|
+
interface SyncQuerySubset {
|
|
711
|
+
collection: string;
|
|
712
|
+
where: Record<string, unknown>;
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Returns true when an operation matches at least one active query subset
|
|
716
|
+
* for its collection. Collections without query subsets pass through.
|
|
717
|
+
*
|
|
718
|
+
* The writer's `previousData` is ignored unless `options.includePreviousData` is
|
|
719
|
+
* set (see {@link ScopeSnapshotOptions}); the server never sets it.
|
|
720
|
+
*/
|
|
721
|
+
declare function operationMatchesQuerySubsets(op: Operation, subsets: SyncQuerySubset[] | undefined, fullRecord?: Record<string, unknown> | null, options?: ScopeSnapshotOptions): boolean;
|
|
722
|
+
/** Canonicalize equality subsets and remove subsets already covered by a broader one. */
|
|
723
|
+
declare function dedupeQuerySubsets(subsets: SyncQuerySubset[]): SyncQuerySubset[];
|
|
724
|
+
/** True when every record selected by `narrow` is also selected by `broad`. */
|
|
725
|
+
declare function querySubsetContains(broad: SyncQuerySubset[], narrow: SyncQuerySubset[]): boolean;
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* The server-stored, passphrase-wrapped key record (ENC-1, decision D4b; format 2 since
|
|
729
|
+
* the final RC red team, RT-95/RT-96/RT-97/RT-104).
|
|
730
|
+
*
|
|
731
|
+
* Key hierarchy of one keyring ("ring"):
|
|
732
|
+
*
|
|
733
|
+
* passphrase --PBKDF2-SHA256(salt, iterations)--> KEK
|
|
734
|
+
* KEK --AES-GCM--> master key (random 256 bits, the ring's secret)
|
|
735
|
+
* recovery public key --ECDH P-256 + HKDF + AES-GCM--> master key (optional)
|
|
736
|
+
* master key --HKDF "data-key-wrap"--> wrapping key --AES-GCM--> data key v1, v2, ...
|
|
737
|
+
* master key --HKDF "record-mac"--> MAC key --HMAC-SHA256--> `mac` over the whole record
|
|
738
|
+
*
|
|
739
|
+
* The MAC covers every field except itself (format, keyring, ring id, revision, current
|
|
740
|
+
* version, KDF parameters, the master wrap, every data-key wrap, the recovery block), so
|
|
741
|
+
* a server cannot add or swap a recovery key, relabel or drop a version, or serve
|
|
742
|
+
* another record under the same revision without the device noticing. Only a holder of
|
|
743
|
+
* the master key (the passphrase, a device that opened the ring, or the recovery key)
|
|
744
|
+
* can write a record devices accept. The server stores the record opaquely: it never
|
|
745
|
+
* sees the passphrase, the KEK, the master key or a data key.
|
|
746
|
+
*
|
|
747
|
+
* Shared by the client keyring (which creates, opens and authenticates records) and the
|
|
748
|
+
* sync server (which validates their structure and stores them with compare-and-set).
|
|
749
|
+
*/
|
|
750
|
+
/** Current record format. Format 1 (beta.13 release candidates only) is not read. */
|
|
751
|
+
declare const KEY_RECORD_FORMAT: 2;
|
|
752
|
+
/** Name of the default keyring (one per user). */
|
|
753
|
+
declare const DEFAULT_KEYRING = "default";
|
|
754
|
+
/** Maximum serialized size of a key record the server accepts. */
|
|
755
|
+
declare const MAX_KEY_RECORD_BYTES: number;
|
|
756
|
+
/**
|
|
757
|
+
* Maximum PBKDF2 iterations a device accepts. A record above it would make an unlock
|
|
758
|
+
* never finish (the floor is the app's own `kdfIterations`).
|
|
759
|
+
*/
|
|
760
|
+
declare const MAX_KDF_ITERATIONS = 10000000;
|
|
761
|
+
/** Recovery wrap algorithm of format 2. */
|
|
762
|
+
declare const RECOVERY_ALGORITHM: "ECDH-P256+HKDF-SHA256+AES-GCM";
|
|
763
|
+
/** Key-derivation parameters of a record's passphrase KEK. */
|
|
764
|
+
interface KeyRecordKdf {
|
|
765
|
+
name: 'PBKDF2';
|
|
766
|
+
hash: 'SHA-256';
|
|
767
|
+
/** PBKDF2 iteration count (600,000 by default). */
|
|
768
|
+
iterations: number;
|
|
769
|
+
/** Base64 random 32-byte salt, generated per passphrase. */
|
|
770
|
+
salt: string;
|
|
771
|
+
}
|
|
772
|
+
/** The ring's master key, wrapped by the passphrase KEK (AES-256-GCM). */
|
|
773
|
+
interface WrappedMasterKey {
|
|
774
|
+
/** Base64 12-byte AES-GCM IV. */
|
|
775
|
+
iv: string;
|
|
776
|
+
/** Base64 AES-GCM ciphertext of the 32-byte master key (with tag: 48 bytes). */
|
|
777
|
+
wrappedKey: string;
|
|
778
|
+
}
|
|
779
|
+
/** One data key version, wrapped by the master key's wrapping key. */
|
|
780
|
+
interface WrappedDataKey {
|
|
781
|
+
/** Key version (1, 2, ...) in this ring. */
|
|
782
|
+
keyVersion: number;
|
|
783
|
+
/** Random key id: the envelope's `keyId`. Never derived from key material. */
|
|
784
|
+
keyId: string;
|
|
785
|
+
/** Base64 12-byte AES-GCM IV of the wrap. */
|
|
786
|
+
iv: string;
|
|
787
|
+
/** Base64 AES-GCM wrap of the raw 32-byte data key (ciphertext and tag). */
|
|
788
|
+
wrappedKey: string;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Optional recovery: the master key, wrapped to a recovery public key (ephemeral-static
|
|
792
|
+
* ECDH P-256, HKDF-SHA256 over the shared secret and both public keys, AES-256-GCM). The
|
|
793
|
+
* private half is the user's recovery key; it opens the master key, hence every data key.
|
|
794
|
+
*/
|
|
795
|
+
interface KeyRecordRecovery {
|
|
796
|
+
alg: typeof RECOVERY_ALGORITHM;
|
|
797
|
+
/** Recovery public key (JWK x/y, base64url). */
|
|
798
|
+
publicKey: {
|
|
799
|
+
x: string;
|
|
800
|
+
y: string;
|
|
801
|
+
};
|
|
802
|
+
/** Ephemeral public key of the wrap (JWK x/y, base64url). */
|
|
803
|
+
ephemeralPublicKey: {
|
|
804
|
+
x: string;
|
|
805
|
+
y: string;
|
|
806
|
+
};
|
|
807
|
+
iv: string;
|
|
808
|
+
wrappedKey: string;
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* The record the sync server stores per (user, keyring). Opaque to the server apart
|
|
812
|
+
* from its structure: it cannot unwrap any key or forge the MAC.
|
|
813
|
+
*/
|
|
814
|
+
interface WrappedKeyRecord {
|
|
815
|
+
format: typeof KEY_RECORD_FORMAT;
|
|
816
|
+
keyring: string;
|
|
817
|
+
/** Random id of this ring (`r-` and 32 hex digits). Two rings of one keyring are a fork. */
|
|
818
|
+
ringId: string;
|
|
819
|
+
/** Compare-and-set counter: grows on every write; devices never accept a lower one. */
|
|
820
|
+
revision: number;
|
|
821
|
+
/** The key version new operations are encrypted with (the highest one). */
|
|
822
|
+
currentVersion: number;
|
|
823
|
+
kdf: KeyRecordKdf;
|
|
824
|
+
master: WrappedMasterKey;
|
|
825
|
+
/** Every key version ever created. Never shrinks: old history must stay readable. */
|
|
826
|
+
keys: WrappedDataKey[];
|
|
827
|
+
recovery?: KeyRecordRecovery;
|
|
828
|
+
/**
|
|
829
|
+
* Ids (`m-` and 32 hex digits) of master keys a passphrase change retired (RT-107).
|
|
830
|
+
* Append-only. A device that authenticated a record listing them refuses every later
|
|
831
|
+
* record authenticated only by one of them, however it was opened (held master key,
|
|
832
|
+
* a stale configured passphrase, the old passphrase typed again). Absent until the
|
|
833
|
+
* first passphrase change.
|
|
834
|
+
*/
|
|
835
|
+
retiredMasters?: string[];
|
|
836
|
+
/** Base64 HMAC-SHA256 of every other field (see {@link keyRecordMacInput}). */
|
|
837
|
+
mac: string;
|
|
838
|
+
}
|
|
839
|
+
/** Result of a structural validation. */
|
|
840
|
+
type KeyRecordValidation = {
|
|
841
|
+
ok: true;
|
|
842
|
+
} | {
|
|
843
|
+
ok: false;
|
|
844
|
+
reason: string;
|
|
845
|
+
};
|
|
846
|
+
/**
|
|
847
|
+
* Structural validation of a key record (server and client). It does not and cannot
|
|
848
|
+
* check the MAC or that the wraps open: only a holder of the master key can.
|
|
849
|
+
*
|
|
850
|
+
* @param value - A decoded record
|
|
851
|
+
* @param keyring - The keyring the record must belong to
|
|
852
|
+
*/
|
|
853
|
+
declare function validateKeyRecord(value: unknown, keyring: string): KeyRecordValidation;
|
|
854
|
+
/**
|
|
855
|
+
* Whether `next` may replace `previous` (the server's write rule): the revision grows,
|
|
856
|
+
* the ring id stays, and every key version of `previous` is still present under the
|
|
857
|
+
* same key id, so no history becomes unreadable. Wraps may change (a passphrase change
|
|
858
|
+
* re-wraps every version). A first record (or one re-uploaded after the server lost
|
|
859
|
+
* it) may carry any revision: devices pin the highest revision they accepted, so a
|
|
860
|
+
* restore must keep the revision it had.
|
|
861
|
+
*
|
|
862
|
+
* @param previous - The stored record, or null when none exists
|
|
863
|
+
* @param next - The proposed record
|
|
864
|
+
*/
|
|
865
|
+
declare function isKeyRecordSuccessor(previous: WrappedKeyRecord | null, next: WrappedKeyRecord): KeyRecordValidation;
|
|
866
|
+
/** Whether a keyring name is acceptable. */
|
|
867
|
+
declare function isValidKeyringName(value: unknown): value is string;
|
|
868
|
+
/** Whether a value is a key id (`k2-` and 32 hex digits). */
|
|
869
|
+
declare function isKeyId(value: unknown): value is string;
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Key-distribution side channel of the sync protocol (ENC-1, D4b). Accepted only after
|
|
873
|
+
* a successful handshake. The server answers for the session's own principal: a
|
|
874
|
+
* message never names a user, so no session can read or write another user's record.
|
|
875
|
+
* JSON on every wire format (the records are small and rare).
|
|
876
|
+
*/
|
|
877
|
+
/** Client -> server: fetch the session principal's record of one keyring. */
|
|
878
|
+
interface EncryptionKeyRequestMessage {
|
|
879
|
+
type: 'encryption-key-request';
|
|
880
|
+
messageId: string;
|
|
881
|
+
/** Correlates the response. */
|
|
882
|
+
requestId: string;
|
|
883
|
+
keyring: string;
|
|
884
|
+
}
|
|
885
|
+
/** Client -> server: write the record with compare-and-set on its revision. */
|
|
886
|
+
interface EncryptionKeyPutMessage {
|
|
887
|
+
type: 'encryption-key-put';
|
|
888
|
+
messageId: string;
|
|
889
|
+
requestId: string;
|
|
890
|
+
keyring: string;
|
|
891
|
+
/** The full new record; its revision must be `expectedRevision + 1`. */
|
|
892
|
+
record: WrappedKeyRecord;
|
|
893
|
+
/** The revision being replaced (0: create). */
|
|
894
|
+
expectedRevision: number;
|
|
895
|
+
}
|
|
896
|
+
/** Status of a key-service response. */
|
|
897
|
+
type EncryptionKeyResponseStatus = 'ok' | 'conflict' | 'forbidden' | 'invalid' | 'unsupported' | 'throttled';
|
|
898
|
+
/**
|
|
899
|
+
* Server -> client: the answer to a request or put (with `requestId`), or an
|
|
900
|
+
* unsolicited push of a record another device of the same principal wrote (without).
|
|
901
|
+
*/
|
|
902
|
+
interface EncryptionKeyResponseMessage {
|
|
903
|
+
type: 'encryption-key-response';
|
|
904
|
+
messageId: string;
|
|
905
|
+
requestId?: string;
|
|
906
|
+
keyring: string;
|
|
907
|
+
status: EncryptionKeyResponseStatus;
|
|
908
|
+
/** The current record (null when none exists, or on refusal). */
|
|
909
|
+
record: WrappedKeyRecord | null;
|
|
910
|
+
/** Human-readable reason for a refusal. */
|
|
911
|
+
message?: string;
|
|
912
|
+
/**
|
|
913
|
+
* With `record: null`: key ids the server's stored operations of this owner are
|
|
914
|
+
* encrypted with (a sample, RT-104). Non-empty means encrypted history exists, so
|
|
915
|
+
* the record was lost: a new device waits for one that holds the ring.
|
|
916
|
+
*/
|
|
917
|
+
knownKeyIds?: string[];
|
|
918
|
+
}
|
|
919
|
+
/** Every key-distribution message. */
|
|
920
|
+
type EncryptionKeyMessage = EncryptionKeyRequestMessage | EncryptionKeyPutMessage | EncryptionKeyResponseMessage;
|
|
921
|
+
/**
|
|
922
|
+
* Structural guard of the key-distribution messages (records are validated in full
|
|
923
|
+
* by the receiver).
|
|
924
|
+
*/
|
|
925
|
+
declare function isEncryptionKeyMessage(value: unknown): value is EncryptionKeyMessage;
|
|
926
|
+
|
|
927
|
+
type WireFormat = 'json' | 'protobuf';
|
|
928
|
+
/**
|
|
929
|
+
* Wire-format operation. Plain object (no Map) for JSON serialization.
|
|
930
|
+
* Maps 1:1 with Operation, but uses Record instead of Map for version vectors.
|
|
931
|
+
*/
|
|
932
|
+
interface SerializedOperation {
|
|
933
|
+
id: string;
|
|
934
|
+
nodeId: string;
|
|
935
|
+
type: OperationType;
|
|
936
|
+
collection: string;
|
|
937
|
+
recordId: string;
|
|
938
|
+
data: Record<string, unknown> | null;
|
|
939
|
+
previousData: Record<string, unknown> | null;
|
|
940
|
+
timestamp: HLCTimestamp;
|
|
941
|
+
sequenceNumber: number;
|
|
942
|
+
causalDeps: string[];
|
|
943
|
+
schemaVersion: number;
|
|
944
|
+
/** Atomic operation intents, present only when atomic ops were used. */
|
|
945
|
+
atomicOps?: Record<string, AtomicOp>;
|
|
946
|
+
/** Groups this operation with others in an atomic transaction. */
|
|
947
|
+
transactionId?: string;
|
|
948
|
+
/** Human-readable name for the mutation group. For DevTools display. */
|
|
949
|
+
mutationName?: string;
|
|
950
|
+
/** Per-field versions of a server scope-entry insert (RT-27). Server-authored only. */
|
|
951
|
+
fieldVersions?: Record<string, HLCTimestamp>;
|
|
952
|
+
/**
|
|
953
|
+
* Content-hash version of `id` (CORE-1, protocol v2). Absent means 1. Protobuf
|
|
954
|
+
* operation field 14.
|
|
955
|
+
*/
|
|
956
|
+
hashVersion?: 1 | 2;
|
|
957
|
+
/**
|
|
958
|
+
* Serialized fold state of a server scope-entry operation (W7). Server-authored
|
|
959
|
+
* only: the server strips it from device uploads. Protobuf operation field 15.
|
|
960
|
+
*/
|
|
961
|
+
foldState?: string;
|
|
962
|
+
/**
|
|
963
|
+
* End-to-end encryption envelope v2 (W9). `data` is then null or holds only the
|
|
964
|
+
* documented cleartext scope fields. Protobuf operation field 16 (JSON).
|
|
965
|
+
*/
|
|
966
|
+
encrypted?: EncryptedOperationEnvelope;
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Handshake message sent by client to initiate sync.
|
|
970
|
+
*/
|
|
971
|
+
interface HandshakeMessage {
|
|
972
|
+
type: 'handshake';
|
|
973
|
+
messageId: string;
|
|
974
|
+
nodeId: string;
|
|
975
|
+
/** Version vector as plain object (nodeId -> sequence number) */
|
|
976
|
+
versionVector: Record<string, number>;
|
|
977
|
+
schemaVersion: number;
|
|
978
|
+
authToken?: string;
|
|
979
|
+
supportedWireFormats?: WireFormat[];
|
|
980
|
+
/** Per-collection sync scope filters. Limits which records are synced to this client. */
|
|
981
|
+
syncScope?: Record<string, Record<string, unknown>>;
|
|
982
|
+
/** Base64url-encoded delta cursor for resuming paginated initial sync */
|
|
983
|
+
deltaCursor?: string;
|
|
984
|
+
/** Live query filters that further narrow synced data within auth/schema scope */
|
|
985
|
+
syncQueries?: SyncQuerySubset[];
|
|
986
|
+
/**
|
|
987
|
+
* The client's persisted delivery watermark: the highest server delivery sequence
|
|
988
|
+
* up to which it has contiguously applied every in-scope operation. Omitted (or 0)
|
|
989
|
+
* on first sync. When present, the server resumes the gap-free server->client
|
|
990
|
+
* stream from just after it, superseding the version-vector delta and the resume
|
|
991
|
+
* cursor for the server->client direction. Optional, so old servers ignore it.
|
|
992
|
+
*/
|
|
993
|
+
lastDeliverySequence?: number;
|
|
994
|
+
/**
|
|
995
|
+
* SYNC-11: canonical key (`scopeViewKey`) of the downlink scope the server accepted at
|
|
996
|
+
* this client's last handshake, when it differs from `syncScope`. A server that
|
|
997
|
+
* resolves a scope with the same key resumes the delivery stream from
|
|
998
|
+
* `acceptedScopeWatermark` instead of restarting it from 0. Optional; old servers
|
|
999
|
+
* ignore it.
|
|
1000
|
+
*/
|
|
1001
|
+
acceptedScopeKey?: string;
|
|
1002
|
+
/** The client's delivery watermark for the view named by `acceptedScopeKey`. */
|
|
1003
|
+
acceptedScopeWatermark?: number;
|
|
1004
|
+
/** Opt in to client-local removal when records leave the accepted downlink view. */
|
|
1005
|
+
scopeExitPolicy?: 'retain' | 'retract';
|
|
1006
|
+
/**
|
|
1007
|
+
* The per-device secret the server issued at this node id's first claim
|
|
1008
|
+
* (`HandshakeResponseMessage.nodeToken`). An anonymous principal must present it
|
|
1009
|
+
* to reconnect with its node id, so another anonymous client cannot take the node
|
|
1010
|
+
* over (RT-12). Stored by the client next to its node id; never sent elsewhere.
|
|
1011
|
+
*/
|
|
1012
|
+
nodeToken?: string;
|
|
1013
|
+
/**
|
|
1014
|
+
* Set by a client transport that understands `heartbeat` messages (LMS #12). Only
|
|
1015
|
+
* then does the server send them, so an older client never receives a message type
|
|
1016
|
+
* it cannot decode. Old servers ignore it.
|
|
1017
|
+
*/
|
|
1018
|
+
supportsHeartbeat?: boolean;
|
|
1019
|
+
/**
|
|
1020
|
+
* Capability (RT-37): this client reserves each operation's sequence number inside
|
|
1021
|
+
* the same local transaction that writes the operation, so it never puts two
|
|
1022
|
+
* different operations under one `(nodeId, sequenceNumber)`. Only for such a client
|
|
1023
|
+
* does the server refuse a second, different operation under a held sequence
|
|
1024
|
+
* (`SEQUENCE_CONFLICT`). A client that omits it (Kora <= beta.12, which could give
|
|
1025
|
+
* two concurrent transactions one number) is served as legacy: such a pair is
|
|
1026
|
+
* stored, both operations are delivered, and a warning is logged. Old servers
|
|
1027
|
+
* ignore it. Protobuf field 45.
|
|
1028
|
+
*/
|
|
1029
|
+
sequenceReservation?: boolean;
|
|
1030
|
+
/**
|
|
1031
|
+
* Sync protocol version the client speaks (`SYNC_PROTOCOL_VERSION`). Absent means 1
|
|
1032
|
+
* (Kora <= beta.12). A protocol-2 client sends hash-version-2 operation ids, the
|
|
1033
|
+
* encryption envelope v2 and always `sequenceReservation`. Protobuf field 47.
|
|
1034
|
+
*/
|
|
1035
|
+
protocolVersion?: number;
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Application-level liveness message (LMS #12). A server sends it every
|
|
1039
|
+
* `HandshakeResponseMessage.heartbeatIntervalMs` to a client that set
|
|
1040
|
+
* `HandshakeMessage.supportsHeartbeat`. Browsers cannot see WebSocket ping frames, so
|
|
1041
|
+
* this is how a client notices a half-open connection: no inbound traffic for about
|
|
1042
|
+
* two intervals means the connection is dead. Carries no data; never acknowledged.
|
|
1043
|
+
*/
|
|
1044
|
+
interface HeartbeatMessage {
|
|
1045
|
+
type: 'heartbeat';
|
|
1046
|
+
messageId: string;
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Server response to a handshake.
|
|
1050
|
+
*/
|
|
1051
|
+
interface HandshakeResponseMessage {
|
|
1052
|
+
type: 'handshake-response';
|
|
1053
|
+
messageId: string;
|
|
1054
|
+
nodeId: string;
|
|
1055
|
+
versionVector: Record<string, number>;
|
|
1056
|
+
schemaVersion: number;
|
|
1057
|
+
accepted: boolean;
|
|
1058
|
+
rejectReason?: string;
|
|
1059
|
+
/** Inclusive minimum schema version the server accepts (present when rejected for schema mismatch). */
|
|
1060
|
+
supportedSchemaMin?: number;
|
|
1061
|
+
/** Inclusive maximum schema version the server accepts (present when rejected for schema mismatch). */
|
|
1062
|
+
supportedSchemaMax?: number;
|
|
1063
|
+
selectedWireFormat?: WireFormat;
|
|
1064
|
+
/** The server-accepted per-collection sync scope. Confirms what data will be synced. */
|
|
1065
|
+
acceptedScope?: Record<string, Record<string, unknown>>;
|
|
1066
|
+
/** Server-authoritative scope for records delivered to this client. */
|
|
1067
|
+
acceptedDownlinkScopes?: Record<string, Record<string, unknown>>;
|
|
1068
|
+
/** Server-authoritative scope for operations uploaded by this client. */
|
|
1069
|
+
acceptedUplinkScopes?: Record<string, Record<string, unknown>>;
|
|
1070
|
+
/** Server wall-clock time (ms since epoch) at response creation. Lets clients measure their own clock skew. */
|
|
1071
|
+
serverTime?: number;
|
|
1072
|
+
/**
|
|
1073
|
+
* Whether the server persists blob bytes centrally. When true, a client
|
|
1074
|
+
* automatically uploads the bytes behind its `blob` fields so they remain
|
|
1075
|
+
* available to other devices even after the authoring device goes offline.
|
|
1076
|
+
*/
|
|
1077
|
+
blobStorageEnabled?: boolean;
|
|
1078
|
+
/**
|
|
1079
|
+
* The server's highest delivery sequence at handshake time. A client whose
|
|
1080
|
+
* persisted delivery watermark exceeds this (the server's operation log was rolled
|
|
1081
|
+
* back, for example by a backup restore) resets its watermark to 0 so it resyncs
|
|
1082
|
+
* from the beginning instead of sitting above a frontier that no longer exists.
|
|
1083
|
+
*/
|
|
1084
|
+
serverMaxDeliverySequence?: number;
|
|
1085
|
+
/**
|
|
1086
|
+
* A per-device secret issued when this handshake made the first claim of the node
|
|
1087
|
+
* id for an anonymous principal (RT-12). The client stores it next to its node id
|
|
1088
|
+
* and presents it in every later handshake (`HandshakeMessage.nodeToken`).
|
|
1089
|
+
*
|
|
1090
|
+
* The claim stays provisional until the client proves it saved the token, by
|
|
1091
|
+
* presenting it in a later handshake or by echoing it in an acknowledgment of this
|
|
1092
|
+
* response (`AcknowledgmentMessage.nodeToken`) right after persisting it (RT-21).
|
|
1093
|
+
*/
|
|
1094
|
+
nodeToken?: string;
|
|
1095
|
+
/**
|
|
1096
|
+
* Peer-relay servers (no central blob storage) set this to ask the client to push
|
|
1097
|
+
* the bytes behind its blob references anyway: the server verifies them against
|
|
1098
|
+
* their hash, records possession, and drops them (RT-23).
|
|
1099
|
+
*/
|
|
1100
|
+
blobPossessionProof?: boolean;
|
|
1101
|
+
/**
|
|
1102
|
+
* Interval of the server's `heartbeat` messages, in ms, present when the client set
|
|
1103
|
+
* `supportsHeartbeat` and the server sends them (LMS #12).
|
|
1104
|
+
*/
|
|
1105
|
+
heartbeatIntervalMs?: number;
|
|
1106
|
+
/**
|
|
1107
|
+
* Sync protocol version the server speaks. Absent means 1 (a beta.12-era server).
|
|
1108
|
+
* Protobuf field 47.
|
|
1109
|
+
*/
|
|
1110
|
+
protocolVersion?: number;
|
|
1111
|
+
/**
|
|
1112
|
+
* Explicit authoritative node ids (RT-75): legacy server node ids from before protocol
|
|
1113
|
+
* 2 and extras configured on the server store. Every `kora:server:` node id is
|
|
1114
|
+
* authoritative by its prefix and is not listed. Only operations of authoritative
|
|
1115
|
+
* nodes may carry server-authored metadata (`fieldVersions`, `foldState`); the client
|
|
1116
|
+
* keeps the union of every id it learns (`SyncStatePersistence.saveAuthoritativeNodeIds`).
|
|
1117
|
+
* Absent from older servers. Protobuf field 46 (repeated).
|
|
1118
|
+
*/
|
|
1119
|
+
authoritativeNodeIds?: string[];
|
|
1120
|
+
/**
|
|
1121
|
+
* Explicit authoritative node ids the deployment revoked (RT-81, server config
|
|
1122
|
+
* `revokedAuthoritativeNodeIds`). A client removes them from the authority union it
|
|
1123
|
+
* keeps, never learns them again, and re-folds the affected records; the server
|
|
1124
|
+
* never accepts them as device node ids. Absent when nothing was revoked, and from
|
|
1125
|
+
* older servers. Protobuf field 48 (repeated).
|
|
1126
|
+
*/
|
|
1127
|
+
revokedAuthoritativeNodeIds?: string[];
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* Batch of operations sent during delta exchange or streaming.
|
|
1131
|
+
*/
|
|
1132
|
+
interface OperationBatchMessage {
|
|
1133
|
+
type: 'operation-batch';
|
|
1134
|
+
messageId: string;
|
|
1135
|
+
operations: SerializedOperation[];
|
|
1136
|
+
/** Client-local authorization removals; these are never domain delete operations. */
|
|
1137
|
+
retractions?: ScopeRetraction[];
|
|
1138
|
+
/** True if this is the last batch in the delta exchange phase */
|
|
1139
|
+
isFinal: boolean;
|
|
1140
|
+
/** Index of this batch (0-based) for ordering */
|
|
1141
|
+
batchIndex: number;
|
|
1142
|
+
/** Base64url-encoded cursor marking the last operation in this batch */
|
|
1143
|
+
cursor?: string;
|
|
1144
|
+
/** Total batches in this delta exchange (for progress reporting) */
|
|
1145
|
+
totalBatches?: number;
|
|
1146
|
+
/**
|
|
1147
|
+
* Delivery-watermark chaining (server->client). `baseDeliverySequence` is the
|
|
1148
|
+
* delivery sequence the client must already hold for this batch to apply in order
|
|
1149
|
+
* (it equals the previous batch's `maxDeliverySequence`, or the client's reported
|
|
1150
|
+
* watermark for the first batch after a handshake). `maxDeliverySequence` is the
|
|
1151
|
+
* highest delivery sequence carried by this batch. The client applies the batch
|
|
1152
|
+
* only when its watermark equals `baseDeliverySequence`, then advances the
|
|
1153
|
+
* watermark to `maxDeliverySequence`. A dropped batch breaks the chain and stalls
|
|
1154
|
+
* the watermark, so the reconnect resend recovers it. Present only on batches the
|
|
1155
|
+
* server sends as part of the delivery stream; absent on legacy version-vector
|
|
1156
|
+
* batches.
|
|
1157
|
+
*/
|
|
1158
|
+
baseDeliverySequence?: number;
|
|
1159
|
+
maxDeliverySequence?: number;
|
|
1160
|
+
}
|
|
1161
|
+
interface ScopeRetraction {
|
|
1162
|
+
collection: string;
|
|
1163
|
+
recordId: string;
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* Acknowledgment of a received message.
|
|
1167
|
+
*/
|
|
1168
|
+
interface AcknowledgmentMessage {
|
|
1169
|
+
type: 'acknowledgment';
|
|
1170
|
+
messageId: string;
|
|
1171
|
+
acknowledgedMessageId: string;
|
|
1172
|
+
lastSequenceNumber: number;
|
|
1173
|
+
/**
|
|
1174
|
+
* The acknowledged batch's `maxDeliverySequence`, echoed so the server can advance
|
|
1175
|
+
* this client's durable delivery watermark. Present only when acking a delivery-
|
|
1176
|
+
* stream batch. `lastSequenceNumber` stays for existing per-node behavior.
|
|
1177
|
+
*/
|
|
1178
|
+
deliverySequence?: number;
|
|
1179
|
+
/**
|
|
1180
|
+
* Claim confirmation (RT-21): the node token from a handshake response, echoed
|
|
1181
|
+
* once the client has persisted it, so the server makes the provisional claim of
|
|
1182
|
+
* the node id permanent. Sent with `acknowledgedMessageId` set to the response id.
|
|
1183
|
+
*/
|
|
1184
|
+
nodeToken?: string;
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Error message from the server or client.
|
|
1188
|
+
*/
|
|
1189
|
+
interface ErrorMessage {
|
|
1190
|
+
type: 'error';
|
|
1191
|
+
messageId: string;
|
|
1192
|
+
code: string;
|
|
1193
|
+
message: string;
|
|
1194
|
+
retriable: boolean;
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Server-to-client rejection of ONE specific operation the client submitted.
|
|
1198
|
+
*
|
|
1199
|
+
* Unlike {@link ErrorMessage}, which is connection-level, this is tied to an
|
|
1200
|
+
* `operationId` so the client can divert exactly that operation out of its
|
|
1201
|
+
* pending outbound queue and into a durable rejected-operations store — kept and
|
|
1202
|
+
* explainable rather than silently retried forever or lost on the batch ack. The
|
|
1203
|
+
* operation never entered the server's authoritative log, so no other replica
|
|
1204
|
+
* ever sees it; the submitter is told so its optimistic local copy can be
|
|
1205
|
+
* reconciled (rolled back or resubmitted) by the app.
|
|
1206
|
+
*/
|
|
1207
|
+
interface OperationRejectedMessage {
|
|
1208
|
+
type: 'operation-rejected';
|
|
1209
|
+
messageId: string;
|
|
1210
|
+
/** Content-addressed id of the rejected operation. */
|
|
1211
|
+
operationId: string;
|
|
1212
|
+
/** Collection the rejected operation targeted (for app routing / display). */
|
|
1213
|
+
collection: string;
|
|
1214
|
+
/** Record the rejected operation targeted. */
|
|
1215
|
+
recordId: string;
|
|
1216
|
+
/** Stable, machine-readable reason code. */
|
|
1217
|
+
code: string;
|
|
1218
|
+
/** Human-readable explanation. */
|
|
1219
|
+
message: string;
|
|
1220
|
+
/** Whether resubmitting the identical operation may later succeed. */
|
|
1221
|
+
retriable: boolean;
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Awareness state for a single client (cursor position, user info).
|
|
1225
|
+
* Wire-format representation for JSON transport.
|
|
1226
|
+
* Ephemeral -- not persisted, only shared with connected peers.
|
|
1227
|
+
*/
|
|
1228
|
+
interface AwarenessStateWire {
|
|
1229
|
+
user: {
|
|
1230
|
+
name: string;
|
|
1231
|
+
color: string;
|
|
1232
|
+
avatar?: string;
|
|
1233
|
+
};
|
|
1234
|
+
cursor?: {
|
|
1235
|
+
collection: string;
|
|
1236
|
+
recordId: string;
|
|
1237
|
+
field: string;
|
|
1238
|
+
anchor: number;
|
|
1239
|
+
head: number;
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Awareness update message. Carries ephemeral presence data (cursors, user info).
|
|
1244
|
+
* Processed separately from operation sync -- never persisted.
|
|
1245
|
+
*/
|
|
1246
|
+
interface AwarenessUpdateMessage {
|
|
1247
|
+
type: 'awareness-update';
|
|
1248
|
+
messageId: string;
|
|
1249
|
+
/** Client ID of the sender */
|
|
1250
|
+
clientId: number;
|
|
1251
|
+
/** Map of clientId -> state (null means removal) */
|
|
1252
|
+
states: Record<string, AwarenessStateWire | null>;
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Incremental Yjs update for a richtext field. Ephemeral side channel for large documents.
|
|
1256
|
+
* JSON transport only; not persisted on the server operation log.
|
|
1257
|
+
*/
|
|
1258
|
+
interface YjsDocUpdateMessage {
|
|
1259
|
+
type: 'yjs-doc-update';
|
|
1260
|
+
messageId: string;
|
|
1261
|
+
collection: string;
|
|
1262
|
+
recordId: string;
|
|
1263
|
+
field: string;
|
|
1264
|
+
/** Base64-encoded Yjs update binary */
|
|
1265
|
+
update: string;
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Request for a single blob chunk by content hash. Ephemeral side channel for
|
|
1269
|
+
* out-of-band blob transfer over the sync connection; never persisted in the
|
|
1270
|
+
* operation log (durable state is the BlobRef inside a record's fields).
|
|
1271
|
+
*
|
|
1272
|
+
* Knowing a hash is not a capability: the server serves (from its central store)
|
|
1273
|
+
* or forwards a request only when a live record inside the requester's download
|
|
1274
|
+
* scope references the hash (as a blob, a manifest, or a chunk of one), forwards it
|
|
1275
|
+
* only to peers whose own scope references it, accepts the answer only from a peer
|
|
1276
|
+
* it asked and only when the bytes hash to the requested hash. Any other request
|
|
1277
|
+
* is answered `bytes: null`, exactly like an unknown hash.
|
|
1278
|
+
*/
|
|
1279
|
+
interface BlobChunkRequestMessage {
|
|
1280
|
+
type: 'blob-chunk-request';
|
|
1281
|
+
messageId: string;
|
|
1282
|
+
/** Correlates the response to this request. */
|
|
1283
|
+
requestId: string;
|
|
1284
|
+
/** SHA-256 hash of the requested chunk. */
|
|
1285
|
+
hash: string;
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* Response to a {@link BlobChunkRequestMessage}. Carries the chunk bytes, or
|
|
1289
|
+
* null when the responder does not hold that hash.
|
|
1290
|
+
*/
|
|
1291
|
+
interface BlobChunkResponseMessage {
|
|
1292
|
+
type: 'blob-chunk-response';
|
|
1293
|
+
messageId: string;
|
|
1294
|
+
/** Echoes the request's `requestId` for correlation. */
|
|
1295
|
+
requestId: string;
|
|
1296
|
+
/** Base64-encoded chunk bytes, or null when the responder does not hold the hash. */
|
|
1297
|
+
bytes: string | null;
|
|
1298
|
+
/**
|
|
1299
|
+
* True when the server refused the request for rate (RT-24), not because the
|
|
1300
|
+
* chunk is missing: retry after `retryAfterMs`. Absent (or false) otherwise.
|
|
1301
|
+
*/
|
|
1302
|
+
throttled?: boolean;
|
|
1303
|
+
/** With `throttled`: milliseconds until the request may be retried. */
|
|
1304
|
+
retryAfterMs?: number;
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Unsolicited upload of a blob chunk (or a blob manifest) from a client to the
|
|
1308
|
+
* server, so the server can persist it centrally and serve it to other devices
|
|
1309
|
+
* even after the authoring device disconnects. Content-addressed: the receiver
|
|
1310
|
+
* verifies the bytes hash to `hash`.
|
|
1311
|
+
*/
|
|
1312
|
+
interface BlobChunkPushMessage {
|
|
1313
|
+
type: 'blob-chunk-push';
|
|
1314
|
+
messageId: string;
|
|
1315
|
+
/** SHA-256 hash of the pushed bytes (a chunk hash or a manifest hash). */
|
|
1316
|
+
hash: string;
|
|
1317
|
+
/** Base64-encoded bytes. */
|
|
1318
|
+
bytes: string;
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Union of all sync protocol messages.
|
|
1322
|
+
*/
|
|
1323
|
+
type SyncMessage = HandshakeMessage | HandshakeResponseMessage | OperationBatchMessage | AcknowledgmentMessage | ErrorMessage | OperationRejectedMessage | AwarenessUpdateMessage | YjsDocUpdateMessage | BlobChunkRequestMessage | BlobChunkResponseMessage | BlobChunkPushMessage | HeartbeatMessage | EncryptionKeyMessage;
|
|
1324
|
+
/**
|
|
1325
|
+
* Check if an unknown value is a valid SyncMessage.
|
|
1326
|
+
*/
|
|
1327
|
+
declare function isSyncMessage(value: unknown): value is SyncMessage;
|
|
1328
|
+
/**
|
|
1329
|
+
* Check if a value is a HandshakeMessage.
|
|
1330
|
+
*/
|
|
1331
|
+
declare function isHandshakeMessage(value: unknown): value is HandshakeMessage;
|
|
1332
|
+
/**
|
|
1333
|
+
* Check if a value is a HandshakeResponseMessage.
|
|
1334
|
+
*/
|
|
1335
|
+
declare function isHandshakeResponseMessage(value: unknown): value is HandshakeResponseMessage;
|
|
1336
|
+
/**
|
|
1337
|
+
* Check if a value is an OperationBatchMessage.
|
|
1338
|
+
*/
|
|
1339
|
+
declare function isOperationBatchMessage(value: unknown): value is OperationBatchMessage;
|
|
1340
|
+
/**
|
|
1341
|
+
* Check if a value is an AcknowledgmentMessage.
|
|
1342
|
+
*/
|
|
1343
|
+
declare function isAcknowledgmentMessage(value: unknown): value is AcknowledgmentMessage;
|
|
1344
|
+
/**
|
|
1345
|
+
* Check if a value is an ErrorMessage.
|
|
1346
|
+
*/
|
|
1347
|
+
declare function isErrorMessage(value: unknown): value is ErrorMessage;
|
|
1348
|
+
/**
|
|
1349
|
+
* Check if a value is an OperationRejectedMessage.
|
|
1350
|
+
*/
|
|
1351
|
+
declare function isOperationRejectedMessage(value: unknown): value is OperationRejectedMessage;
|
|
1352
|
+
/**
|
|
1353
|
+
* Check if a value is an AwarenessUpdateMessage.
|
|
1354
|
+
*/
|
|
1355
|
+
declare function isAwarenessUpdateMessage(value: unknown): value is AwarenessUpdateMessage;
|
|
1356
|
+
/**
|
|
1357
|
+
* Check if a value is a YjsDocUpdateMessage.
|
|
1358
|
+
*/
|
|
1359
|
+
declare function isYjsDocUpdateMessage(value: unknown): value is YjsDocUpdateMessage;
|
|
1360
|
+
/**
|
|
1361
|
+
* Check if a value is a BlobChunkRequestMessage.
|
|
1362
|
+
*/
|
|
1363
|
+
declare function isBlobChunkRequestMessage(value: unknown): value is BlobChunkRequestMessage;
|
|
1364
|
+
/**
|
|
1365
|
+
* Check if a value is a BlobChunkResponseMessage.
|
|
1366
|
+
*/
|
|
1367
|
+
declare function isBlobChunkResponseMessage(value: unknown): value is BlobChunkResponseMessage;
|
|
1368
|
+
/**
|
|
1369
|
+
* Check if a value is a BlobChunkPushMessage.
|
|
1370
|
+
*/
|
|
1371
|
+
declare function isBlobChunkPushMessage(value: unknown): value is BlobChunkPushMessage;
|
|
1372
|
+
|
|
1373
|
+
/**
|
|
1374
|
+
* Sync protocol version this build speaks (decision D2: one bump carrying all wire
|
|
1375
|
+
* breaks). Version 2 adds:
|
|
1376
|
+
* - operation ids hashed with content-hash version 2 (CORE-1), with `hashVersion`
|
|
1377
|
+
* carried on the wire and verified on receive;
|
|
1378
|
+
* - the end-to-end encryption envelope v2 (`op.encrypted`, AES-GCM AAD, ENC-3 and
|
|
1379
|
+
* NEW-ENC-1);
|
|
1380
|
+
* - the sequence-reservation capability on every handshake (RT-37);
|
|
1381
|
+
* - `authoritativeNodeIds` and server-authored `foldState` (W7).
|
|
1382
|
+
*
|
|
1383
|
+
* A handshake without `protocolVersion` is version 1 (Kora <= beta.12). Servers accept
|
|
1384
|
+
* version-1 clients for one release (beta.13) with a deprecation warning.
|
|
1385
|
+
*/
|
|
1386
|
+
declare const SYNC_PROTOCOL_VERSION = 2;
|
|
1387
|
+
/** Protocol version assumed when a handshake or response does not declare one. */
|
|
1388
|
+
declare const LEGACY_SYNC_PROTOCOL_VERSION = 1;
|
|
1389
|
+
/** Code of the server's deprecation warning for a version-1 client. */
|
|
1390
|
+
declare const PROTOCOL_V1_DEPRECATED = "PROTOCOL_V1_DEPRECATED";
|
|
1391
|
+
/** Non-retriable rejection code for an operation whose id is not its content hash. */
|
|
1392
|
+
declare const INVALID_OPERATION_ID = "INVALID_OPERATION_ID";
|
|
1393
|
+
/**
|
|
1394
|
+
* Non-retriable rejection code for a plaintext operation uploaded to a server that
|
|
1395
|
+
* requires the encryption envelope.
|
|
1396
|
+
*/
|
|
1397
|
+
declare const PLAINTEXT_REJECTED = "PLAINTEXT_REJECTED";
|
|
1398
|
+
/**
|
|
1399
|
+
* The protocol version a handshake (or handshake response) declares.
|
|
1400
|
+
*
|
|
1401
|
+
* @param declared - The message's `protocolVersion`
|
|
1402
|
+
* @returns The declared version, or 1 when absent or malformed
|
|
1403
|
+
*/
|
|
1404
|
+
declare function declaredProtocolVersion(declared: unknown): number;
|
|
1405
|
+
|
|
1406
|
+
/**
|
|
1407
|
+
* Options for transport connection.
|
|
1408
|
+
*/
|
|
1409
|
+
interface TransportOptions {
|
|
1410
|
+
/** Authentication token sent during connection */
|
|
1411
|
+
authToken?: string;
|
|
1412
|
+
/** Additional headers (for HTTP-based transports) */
|
|
1413
|
+
headers?: Record<string, string>;
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Transport-level error handler.
|
|
1417
|
+
*/
|
|
1418
|
+
type TransportErrorHandler = (error: Error) => void;
|
|
1419
|
+
/**
|
|
1420
|
+
* Transport-level close handler.
|
|
1421
|
+
*/
|
|
1422
|
+
type TransportCloseHandler = (reason: string) => void;
|
|
1423
|
+
/**
|
|
1424
|
+
* Transport-level message handler.
|
|
1425
|
+
*/
|
|
1426
|
+
type TransportMessageHandler = (message: SyncMessage) => void;
|
|
1427
|
+
/**
|
|
1428
|
+
* Abstract transport interface for sync communication.
|
|
1429
|
+
* Implementations can use WebSocket, HTTP, Bluetooth, or any other transport.
|
|
1430
|
+
*/
|
|
1431
|
+
interface SyncTransport {
|
|
1432
|
+
/** Connect to the sync server at the given URL */
|
|
1433
|
+
connect(url: string, options?: TransportOptions): Promise<void>;
|
|
1434
|
+
/** Disconnect from the server */
|
|
1435
|
+
disconnect(): Promise<void>;
|
|
1436
|
+
/** Send a sync message */
|
|
1437
|
+
send(message: SyncMessage): void;
|
|
1438
|
+
/** Register a handler for incoming messages */
|
|
1439
|
+
onMessage(handler: TransportMessageHandler): void;
|
|
1440
|
+
/** Register a handler for connection close events */
|
|
1441
|
+
onClose(handler: TransportCloseHandler): void;
|
|
1442
|
+
/** Register a handler for transport errors */
|
|
1443
|
+
onError(handler: TransportErrorHandler): void;
|
|
1444
|
+
/** Returns true if the transport is currently connected */
|
|
1445
|
+
isConnected(): boolean;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
export { MAX_KDF_ITERATIONS as $, type AcknowledgmentMessage as A, type BlobChunkRequestMessage as B, type EncryptionKeyResponseMessage as C, type DeltaCursor as D, type EncryptedPayload as E, type EncryptionKeyResponseStatus as F, type ErrorMessage as G, type HeldNodeInfo as H, type HandshakeMessage as I, type HandshakeResponseMessage as J, type HeartbeatMessage as K, type HeldReason as L, INVALID_OPERATION_ID as M, InvalidScopeError as N, KEY_RECORD_FORMAT as O, type KeyRecordKdf as P, type QueueStorage as Q, type RejectedOperationStorage as R, type SyncScopeMap as S, type TransportOptions as T, type KeyRecordRecovery as U, type VersionedKey as V, type WireFormat as W, type KeyRecordValidation as X, type YjsDocUpdateMessage as Y, LEGACY_SYNC_PROTOCOL_VERSION as Z, type LocalNodeInfo as _, type ScopeSnapshotOptions as a, MAX_KEY_RECORD_BYTES as a0, MemoryRejectedOperationStorage as a1, type OperationBatchMessage as a2, type OperationRejectedMessage as a3, PLAINTEXT_REJECTED as a4, PROTOCOL_V1_DEPRECATED as a5, SYNC_PROTOCOL_VERSION as a6, SYNC_STATES as a7, SYNC_STATUSES as a8, ScopeViolationError as a9, isValidKeyringName as aA, isYjsDocUpdateMessage as aB, operationMatchesQuerySubsets as aC, querySubsetContains as aD, validateKeyRecord as aE, buildScopeSnapshot as aF, matchesScopePredicate as aG, recordMatchesScopePredicates as aH, type SyncAuthRequest as aa, type SyncEncryptionAlgorithm as ab, type SyncPhase as ac, type SyncScopeContext as ad, type SyncSettlementOptions as ae, type SyncSettlementResult as af, type SyncStatus as ag, type TerminalRejectionRecord as ah, type WrappedDataKey as ai, type WrappedMasterKey as aj, declaredProtocolVersion as ak, dedupeQuerySubsets as al, isAcknowledgmentMessage as am, isAwarenessUpdateMessage as an, isBlobChunkPushMessage as ao, isBlobChunkRequestMessage as ap, isBlobChunkResponseMessage as aq, isEncryptionKeyMessage as ar, isErrorMessage as as, isHandshakeMessage as at, isHandshakeResponseMessage as au, isKeyId as av, isKeyRecordSuccessor as aw, isOperationBatchMessage as ax, isOperationRejectedMessage as ay, isSyncMessage as az, type SyncMessage as b, type SerializedOperation as c, type SyncTransport as d, type TransportMessageHandler as e, type TransportCloseHandler as f, type TransportErrorHandler as g, type BlobChunkResponseMessage as h, type BlobChunkPushMessage as i, type WrappedKeyRecord as j, type SyncEncryptionConfig as k, type SyncState as l, type SyncStatusInfo as m, type SyncConfig as n, type SyncStatePersistence as o, type SyncQuerySubset as p, type QuarantinedOperation as q, type RejectedOperation as r, type ActiveApplyFailure as s, type AdoptionScheduleInfo as t, type AwarenessStateWire as u, type AwarenessUpdateMessage as v, DEFAULT_KEYRING as w, type EncryptionKeyMessage as x, type EncryptionKeyPutMessage as y, type EncryptionKeyRequestMessage as z };
|