@korajs/sync 0.6.1 → 1.0.0-beta.0
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/LICENSE +21 -0
- package/dist/index.cjs +196 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -6
- package/dist/index.d.ts +70 -6
- package/dist/index.js +196 -17
- package/dist/index.js.map +1 -1
- package/dist/internal.d.cts +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/{transport-BrdBj7rH.d.cts → transport-BMesMB6s.d.cts} +5 -1
- package/dist/{transport-BrdBj7rH.d.ts → transport-BMesMB6s.d.ts} +5 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ var SYNC_STATUSES = [
|
|
|
17
17
|
"syncing",
|
|
18
18
|
"synced",
|
|
19
19
|
"offline",
|
|
20
|
+
"clock-error",
|
|
20
21
|
"error",
|
|
21
22
|
"schema-mismatch"
|
|
22
23
|
];
|
|
@@ -499,7 +500,8 @@ function toProtoEnvelope(message) {
|
|
|
499
500
|
schemaVersion: message.schemaVersion,
|
|
500
501
|
accepted: message.accepted,
|
|
501
502
|
rejectReason: message.rejectReason,
|
|
502
|
-
selectedWireFormat: message.selectedWireFormat
|
|
503
|
+
selectedWireFormat: message.selectedWireFormat,
|
|
504
|
+
serverTime: message.serverTime
|
|
503
505
|
};
|
|
504
506
|
case "operation-batch":
|
|
505
507
|
return {
|
|
@@ -559,7 +561,10 @@ function fromProtoEnvelope(envelope) {
|
|
|
559
561
|
schemaVersion: envelope.schemaVersion ?? 0,
|
|
560
562
|
accepted: envelope.accepted ?? false,
|
|
561
563
|
rejectReason: envelope.rejectReason,
|
|
562
|
-
selectedWireFormat: envelope.selectedWireFormat === "json" || envelope.selectedWireFormat === "protobuf" ? envelope.selectedWireFormat : void 0
|
|
564
|
+
selectedWireFormat: envelope.selectedWireFormat === "json" || envelope.selectedWireFormat === "protobuf" ? envelope.selectedWireFormat : void 0,
|
|
565
|
+
// Preserve the server's wall-clock time so clock-skew detection and
|
|
566
|
+
// automatic timestamp rebase work over the protobuf wire, not just JSON.
|
|
567
|
+
...envelope.serverTime !== void 0 ? { serverTime: envelope.serverTime } : {}
|
|
563
568
|
};
|
|
564
569
|
case "operation-batch":
|
|
565
570
|
return {
|
|
@@ -728,6 +733,7 @@ function encodeEnvelope(envelope) {
|
|
|
728
733
|
writer.uint32(138).string(envelope.errorMessage);
|
|
729
734
|
}
|
|
730
735
|
if (envelope.retriable !== void 0) writer.uint32(144).bool(envelope.retriable);
|
|
736
|
+
if (envelope.serverTime !== void 0) writer.uint32(152).int64(envelope.serverTime);
|
|
731
737
|
return writer.finish();
|
|
732
738
|
}
|
|
733
739
|
function decodeEnvelope(bytes) {
|
|
@@ -796,6 +802,9 @@ function decodeEnvelope(bytes) {
|
|
|
796
802
|
case 18:
|
|
797
803
|
envelope.retriable = reader.bool();
|
|
798
804
|
break;
|
|
805
|
+
case 19:
|
|
806
|
+
envelope.serverTime = longToNumber(reader.int64());
|
|
807
|
+
break;
|
|
799
808
|
default:
|
|
800
809
|
reader.skipType(tag & 7);
|
|
801
810
|
}
|
|
@@ -1297,7 +1306,7 @@ var ChaosTransport = class {
|
|
|
1297
1306
|
random;
|
|
1298
1307
|
messageHandler = null;
|
|
1299
1308
|
reorderBuffer = [];
|
|
1300
|
-
|
|
1309
|
+
pending = [];
|
|
1301
1310
|
constructor(inner, config) {
|
|
1302
1311
|
this.inner = inner;
|
|
1303
1312
|
this.dropRate = config?.dropRate ?? 0;
|
|
@@ -1312,10 +1321,14 @@ var ChaosTransport = class {
|
|
|
1312
1321
|
}
|
|
1313
1322
|
async disconnect() {
|
|
1314
1323
|
this.flushReorderBuffer();
|
|
1315
|
-
|
|
1324
|
+
const flushed = this.pending;
|
|
1325
|
+
this.pending = [];
|
|
1326
|
+
for (const { timer } of flushed) {
|
|
1316
1327
|
clearTimeout(timer);
|
|
1317
1328
|
}
|
|
1318
|
-
|
|
1329
|
+
for (const { message } of flushed) {
|
|
1330
|
+
this.deliverIncoming(message);
|
|
1331
|
+
}
|
|
1319
1332
|
return this.inner.disconnect();
|
|
1320
1333
|
}
|
|
1321
1334
|
send(message) {
|
|
@@ -1352,9 +1365,10 @@ var ChaosTransport = class {
|
|
|
1352
1365
|
if (this.maxLatency > 0) {
|
|
1353
1366
|
const delay = Math.floor(this.random() * this.maxLatency);
|
|
1354
1367
|
const timer = setTimeout(() => {
|
|
1368
|
+
this.pending = this.pending.filter((p) => p.timer !== timer);
|
|
1355
1369
|
this.deliverIncoming(message);
|
|
1356
1370
|
}, delay);
|
|
1357
|
-
this.
|
|
1371
|
+
this.pending.push({ timer, message });
|
|
1358
1372
|
return;
|
|
1359
1373
|
}
|
|
1360
1374
|
this.deliverIncoming(message);
|
|
@@ -2249,6 +2263,41 @@ var OutboundQueue = class {
|
|
|
2249
2263
|
peek(count) {
|
|
2250
2264
|
return this.queue.slice(0, count);
|
|
2251
2265
|
}
|
|
2266
|
+
/**
|
|
2267
|
+
* All queued operations (not counting in-flight), in causal order.
|
|
2268
|
+
*/
|
|
2269
|
+
getAll() {
|
|
2270
|
+
return [...this.queue];
|
|
2271
|
+
}
|
|
2272
|
+
/**
|
|
2273
|
+
* Atomically replace the entire queue with a new set of operations.
|
|
2274
|
+
*
|
|
2275
|
+
* Used after a timestamp rebase rewrote the queued operations under new
|
|
2276
|
+
* content-addressed ids: the old entries must vanish from memory AND from
|
|
2277
|
+
* persistent storage in one step, or a page refresh could resurrect
|
|
2278
|
+
* stale-stamped ops the server would reject. Resets the seen set to exactly
|
|
2279
|
+
* the new ids so the rewritten ops are not treated as duplicates.
|
|
2280
|
+
*/
|
|
2281
|
+
async replaceAll(ops) {
|
|
2282
|
+
const removeIds = this.queue.map((op) => op.id);
|
|
2283
|
+
for (const batch of this.inFlight.values()) {
|
|
2284
|
+
for (const op of batch) {
|
|
2285
|
+
removeIds.push(op.id);
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
this.inFlight.clear();
|
|
2289
|
+
this.queue = ops.length > 1 ? topologicalSort([...ops]) : [...ops];
|
|
2290
|
+
this.seen.clear();
|
|
2291
|
+
for (const op of this.queue) {
|
|
2292
|
+
this.seen.add(op.id);
|
|
2293
|
+
}
|
|
2294
|
+
if (removeIds.length > 0) {
|
|
2295
|
+
await this.storage.dequeue(removeIds);
|
|
2296
|
+
}
|
|
2297
|
+
for (const op of this.queue) {
|
|
2298
|
+
await this.storage.enqueue(op);
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2252
2301
|
/**
|
|
2253
2302
|
* Whether initialize() has been called.
|
|
2254
2303
|
*/
|
|
@@ -2310,6 +2359,8 @@ var SyncEngine = class {
|
|
|
2310
2359
|
currentBatch = null;
|
|
2311
2360
|
reconnecting = false;
|
|
2312
2361
|
schemaBlocked = false;
|
|
2362
|
+
clockBlocked = false;
|
|
2363
|
+
clockSkewMs = null;
|
|
2313
2364
|
// Track delta exchange state
|
|
2314
2365
|
deltaBatchesReceived = 0;
|
|
2315
2366
|
deltaReceiveComplete = false;
|
|
@@ -2483,10 +2534,17 @@ var SyncEngine = class {
|
|
|
2483
2534
|
lastSyncedAt: this.lastSyncedAt,
|
|
2484
2535
|
lastSuccessfulPush: this.lastSuccessfulPush,
|
|
2485
2536
|
lastSuccessfulPull: this.lastSuccessfulPull,
|
|
2486
|
-
conflicts: this.conflictCount
|
|
2537
|
+
conflicts: this.conflictCount,
|
|
2538
|
+
clockSkewMs: this.clockSkewMs
|
|
2487
2539
|
};
|
|
2488
2540
|
switch (this.state) {
|
|
2489
2541
|
case "disconnected":
|
|
2542
|
+
if (this.clockBlocked) {
|
|
2543
|
+
return { ...base, status: "clock-error" };
|
|
2544
|
+
}
|
|
2545
|
+
if (this.schemaBlocked) {
|
|
2546
|
+
return { ...base, status: "schema-mismatch" };
|
|
2547
|
+
}
|
|
2490
2548
|
return { ...base, status: "offline" };
|
|
2491
2549
|
case "connecting":
|
|
2492
2550
|
case "handshaking":
|
|
@@ -2495,6 +2553,9 @@ var SyncEngine = class {
|
|
|
2495
2553
|
case "streaming":
|
|
2496
2554
|
return { ...base, status: pendingOperations > 0 ? "syncing" : "synced" };
|
|
2497
2555
|
case "error":
|
|
2556
|
+
if (this.clockBlocked) {
|
|
2557
|
+
return { ...base, status: "clock-error" };
|
|
2558
|
+
}
|
|
2498
2559
|
return { ...base, status: this.schemaBlocked ? "schema-mismatch" : "error" };
|
|
2499
2560
|
}
|
|
2500
2561
|
}
|
|
@@ -2505,6 +2566,27 @@ var SyncEngine = class {
|
|
|
2505
2566
|
isSchemaBlocked() {
|
|
2506
2567
|
return this.schemaBlocked;
|
|
2507
2568
|
}
|
|
2569
|
+
/**
|
|
2570
|
+
* True when sync is blocked because this device's clock is too far ahead.
|
|
2571
|
+
* Local writes continue to work and queue; only sync is paused.
|
|
2572
|
+
*/
|
|
2573
|
+
isClockBlocked() {
|
|
2574
|
+
return this.clockBlocked;
|
|
2575
|
+
}
|
|
2576
|
+
/** serverTime - localTime measured at the last handshake, or null before first connect. */
|
|
2577
|
+
getClockSkewMs() {
|
|
2578
|
+
return this.clockSkewMs;
|
|
2579
|
+
}
|
|
2580
|
+
/**
|
|
2581
|
+
* Clears the clock block after the user corrects the device clock.
|
|
2582
|
+
* Moves the engine back to `disconnected` so `start()` can run again.
|
|
2583
|
+
*/
|
|
2584
|
+
clearClockBlock() {
|
|
2585
|
+
this.clockBlocked = false;
|
|
2586
|
+
if (this.state === "error") {
|
|
2587
|
+
this.transitionTo("disconnected");
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2508
2590
|
/**
|
|
2509
2591
|
* Clears schema-mismatch block after upgrading the local schema / sync config.
|
|
2510
2592
|
* Moves the engine back to `disconnected` so `start()` can run again.
|
|
@@ -2648,7 +2730,7 @@ var SyncEngine = class {
|
|
|
2648
2730
|
async handleMessageAsync(message) {
|
|
2649
2731
|
switch (message.type) {
|
|
2650
2732
|
case "handshake-response":
|
|
2651
|
-
this.handleHandshakeResponse(message);
|
|
2733
|
+
await this.handleHandshakeResponse(message);
|
|
2652
2734
|
break;
|
|
2653
2735
|
case "operation-batch":
|
|
2654
2736
|
await this.handleOperationBatch(message);
|
|
@@ -2671,8 +2753,41 @@ var SyncEngine = class {
|
|
|
2671
2753
|
const reason = error instanceof Error ? error.message : "Message handling failed";
|
|
2672
2754
|
this.handleTransportClose(reason);
|
|
2673
2755
|
}
|
|
2674
|
-
|
|
2756
|
+
/**
|
|
2757
|
+
* Compares server wall-clock time from the handshake with local time.
|
|
2758
|
+
* Negative skew = this device's clock is fast (dangerous for LWW and rejected
|
|
2759
|
+
* by server ingest beyond 60s). Positive skew = slow (accepted but surfaced).
|
|
2760
|
+
* Zero developer work required: the result flows into sync status and events.
|
|
2761
|
+
*/
|
|
2762
|
+
evaluateClockSkew(serverTime) {
|
|
2763
|
+
const skewMs = serverTime - Date.now();
|
|
2764
|
+
this.clockSkewMs = skewMs;
|
|
2765
|
+
const FAST_BLOCK_MS = 6e4;
|
|
2766
|
+
const SLOW_WARN_MS = 10 * 6e4;
|
|
2767
|
+
let severity = "info";
|
|
2768
|
+
if (skewMs < -FAST_BLOCK_MS) {
|
|
2769
|
+
severity = "fast-blocked";
|
|
2770
|
+
} else if (skewMs > SLOW_WARN_MS) {
|
|
2771
|
+
severity = "slow-warning";
|
|
2772
|
+
}
|
|
2773
|
+
this.emitter?.emit({ type: "sync:clock-skew", skewMs, severity, source: "handshake" });
|
|
2774
|
+
if (severity === "fast-blocked") {
|
|
2775
|
+
this.clockBlocked = true;
|
|
2776
|
+
} else {
|
|
2777
|
+
this.clockBlocked = false;
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
async handleHandshakeResponse(msg) {
|
|
2675
2781
|
if (this.state !== "handshaking") return;
|
|
2782
|
+
if (typeof msg.serverTime === "number") {
|
|
2783
|
+
this.evaluateClockSkew(msg.serverTime);
|
|
2784
|
+
if (this.clockBlocked) {
|
|
2785
|
+
this.metricsCollector.updateStatus("error");
|
|
2786
|
+
this.transitionTo("error");
|
|
2787
|
+
void this.transport.disconnect();
|
|
2788
|
+
return;
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2676
2791
|
if (!msg.accepted) {
|
|
2677
2792
|
const reason = msg.rejectReason ?? "Handshake rejected";
|
|
2678
2793
|
if (isSchemaMismatchReject(msg.rejectReason)) {
|
|
@@ -2719,8 +2834,51 @@ var SyncEngine = class {
|
|
|
2719
2834
|
this.deltaSentOpIds = [];
|
|
2720
2835
|
this.pendingDeltaBatchAcks.clear();
|
|
2721
2836
|
this.initialSyncTotalBatches = 0;
|
|
2837
|
+
if (typeof msg.serverTime === "number") {
|
|
2838
|
+
await this.maybeRebaseQueuedOperations(msg.serverTime);
|
|
2839
|
+
}
|
|
2722
2840
|
this.sendDelta();
|
|
2723
2841
|
}
|
|
2842
|
+
/**
|
|
2843
|
+
* Re-stamps queued (never-acknowledged) operations whose timestamps are far
|
|
2844
|
+
* enough in the future that the server would reject them, using the server's
|
|
2845
|
+
* own handshake time as the trusted "now". This is the automatic recovery
|
|
2846
|
+
* path after a user corrects a fast device clock: the queue drains
|
|
2847
|
+
* immediately instead of waiting for real time to catch up.
|
|
2848
|
+
*/
|
|
2849
|
+
async maybeRebaseQueuedOperations(serverTime) {
|
|
2850
|
+
const rebase = this.store.rebaseUnsyncedOperations?.bind(this.store);
|
|
2851
|
+
if (!rebase) return;
|
|
2852
|
+
const queued = this.outboundQueue.getAll();
|
|
2853
|
+
if (queued.length === 0) return;
|
|
2854
|
+
let maxQueuedWallTime = Number.NEGATIVE_INFINITY;
|
|
2855
|
+
for (const op of queued) {
|
|
2856
|
+
if (op.timestamp.wallTime > maxQueuedWallTime) {
|
|
2857
|
+
maxQueuedWallTime = op.timestamp.wallTime;
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
const SERVER_FUTURE_TOLERANCE_MS = 6e4;
|
|
2861
|
+
if (maxQueuedWallTime <= serverTime + SERVER_FUTURE_TOLERANCE_MS) return;
|
|
2862
|
+
try {
|
|
2863
|
+
const result = await rebase(
|
|
2864
|
+
queued.map((op) => op.id),
|
|
2865
|
+
serverTime
|
|
2866
|
+
);
|
|
2867
|
+
await this.outboundQueue.replaceAll(result.operations);
|
|
2868
|
+
this.emitter?.emit({
|
|
2869
|
+
type: "sync:clock-rebase",
|
|
2870
|
+
rebasedCount: result.rebasedCount,
|
|
2871
|
+
maxSkewMs: maxQueuedWallTime - serverTime
|
|
2872
|
+
});
|
|
2873
|
+
} catch (error) {
|
|
2874
|
+
this.emitter?.emit({
|
|
2875
|
+
type: "store:persistence-error",
|
|
2876
|
+
dbName: "kora-oplog",
|
|
2877
|
+
message: error instanceof Error ? error.message : "Timestamp rebase failed",
|
|
2878
|
+
code: "CLOCK_REBASE_FAILED"
|
|
2879
|
+
});
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2724
2882
|
async sendDelta() {
|
|
2725
2883
|
const localVector = this.store.getVersionVector();
|
|
2726
2884
|
const allMissingOps = await this.collectDelta(localVector, this.remoteVector);
|
|
@@ -2900,6 +3058,17 @@ var SyncEngine = class {
|
|
|
2900
3058
|
if (msg.code === "AUTH_FAILED") {
|
|
2901
3059
|
this.emitter?.emit({ type: "sync:auth-failed", reason: msg.message });
|
|
2902
3060
|
}
|
|
3061
|
+
if (msg.code === "INVALID_TIMESTAMP") {
|
|
3062
|
+
this.clockBlocked = true;
|
|
3063
|
+
this.emitter?.emit({
|
|
3064
|
+
type: "sync:clock-skew",
|
|
3065
|
+
skewMs: this.clockSkewMs ?? Number.NaN,
|
|
3066
|
+
severity: "fast-blocked",
|
|
3067
|
+
source: "server-reject"
|
|
3068
|
+
});
|
|
3069
|
+
this.emitter?.emit({ type: "sync:disconnected", reason: msg.message });
|
|
3070
|
+
return;
|
|
3071
|
+
}
|
|
2903
3072
|
this.emitter?.emit({ type: "sync:disconnected", reason: msg.message });
|
|
2904
3073
|
this.transitionTo("disconnected");
|
|
2905
3074
|
}
|
|
@@ -3380,7 +3549,7 @@ var KeyDerivationError = class extends SyncError5 {
|
|
|
3380
3549
|
}
|
|
3381
3550
|
};
|
|
3382
3551
|
var SALT_LENGTH = 32;
|
|
3383
|
-
var
|
|
3552
|
+
var DEFAULT_PBKDF2_ITERATIONS = 6e5;
|
|
3384
3553
|
var DERIVED_KEY_LENGTH = 256;
|
|
3385
3554
|
function assertCryptoAvailable() {
|
|
3386
3555
|
if (typeof globalThis.crypto === "undefined" || typeof globalThis.crypto.subtle === "undefined") {
|
|
@@ -3397,13 +3566,19 @@ function generateSalt() {
|
|
|
3397
3566
|
}
|
|
3398
3567
|
return globalThis.crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
|
|
3399
3568
|
}
|
|
3400
|
-
async function deriveKey(passphrase, salt) {
|
|
3569
|
+
async function deriveKey(passphrase, salt, iterations = DEFAULT_PBKDF2_ITERATIONS) {
|
|
3401
3570
|
assertCryptoAvailable();
|
|
3402
3571
|
if (passphrase.length === 0) {
|
|
3403
3572
|
throw new KeyDerivationError(
|
|
3404
3573
|
"Passphrase must not be empty. Provide a non-empty string for encryption key derivation."
|
|
3405
3574
|
);
|
|
3406
3575
|
}
|
|
3576
|
+
if (!Number.isInteger(iterations) || iterations < 1) {
|
|
3577
|
+
throw new KeyDerivationError(
|
|
3578
|
+
`PBKDF2 iteration count must be a positive integer, received: ${iterations}`,
|
|
3579
|
+
{ iterations }
|
|
3580
|
+
);
|
|
3581
|
+
}
|
|
3407
3582
|
const usedSalt = salt ?? generateSalt();
|
|
3408
3583
|
try {
|
|
3409
3584
|
const passphraseBytes = new TextEncoder().encode(passphrase);
|
|
@@ -3418,7 +3593,7 @@ async function deriveKey(passphrase, salt) {
|
|
|
3418
3593
|
{
|
|
3419
3594
|
name: "PBKDF2",
|
|
3420
3595
|
salt: usedSalt,
|
|
3421
|
-
iterations
|
|
3596
|
+
iterations,
|
|
3422
3597
|
hash: "SHA-256"
|
|
3423
3598
|
},
|
|
3424
3599
|
baseKey,
|
|
@@ -3438,13 +3613,13 @@ async function deriveKey(passphrase, salt) {
|
|
|
3438
3613
|
);
|
|
3439
3614
|
}
|
|
3440
3615
|
}
|
|
3441
|
-
async function deriveVersionedKey(passphrase, version, salt) {
|
|
3616
|
+
async function deriveVersionedKey(passphrase, version, salt, iterations = DEFAULT_PBKDF2_ITERATIONS) {
|
|
3442
3617
|
if (!Number.isInteger(version) || version < 1) {
|
|
3443
3618
|
throw new KeyDerivationError(`Key version must be a positive integer, received: ${version}`, {
|
|
3444
3619
|
version
|
|
3445
3620
|
});
|
|
3446
3621
|
}
|
|
3447
|
-
const { key, salt: usedSalt } = await deriveKey(passphrase, salt);
|
|
3622
|
+
const { key, salt: usedSalt } = await deriveKey(passphrase, salt, iterations);
|
|
3448
3623
|
return { version, key, salt: usedSalt };
|
|
3449
3624
|
}
|
|
3450
3625
|
|
|
@@ -3484,11 +3659,13 @@ var SyncEncryptor = class _SyncEncryptor {
|
|
|
3484
3659
|
* @param config - Encryption configuration with passphrase
|
|
3485
3660
|
* @param salt - Optional salt for deterministic key derivation (mainly for testing).
|
|
3486
3661
|
* If omitted, a random salt is generated.
|
|
3662
|
+
* @param iterations - Optional PBKDF2 iteration count. Defaults to the
|
|
3663
|
+
* production-strength value. Lower it only in tests.
|
|
3487
3664
|
* @returns A configured SyncEncryptor instance
|
|
3488
3665
|
* @throws {EncryptionError} If configuration is invalid
|
|
3489
3666
|
* @throws {KeyDerivationError} If key derivation fails
|
|
3490
3667
|
*/
|
|
3491
|
-
static async create(config, salt) {
|
|
3668
|
+
static async create(config, salt, iterations) {
|
|
3492
3669
|
if (!config.enabled) {
|
|
3493
3670
|
throw new EncryptionError(
|
|
3494
3671
|
"Cannot create SyncEncryptor with encryption disabled. Set enabled: true in the encryption config."
|
|
@@ -3500,7 +3677,7 @@ var SyncEncryptor = class _SyncEncryptor {
|
|
|
3500
3677
|
"Encryption key/passphrase must not be empty. Provide a non-empty string or key provider function."
|
|
3501
3678
|
);
|
|
3502
3679
|
}
|
|
3503
|
-
const versionedKey = await deriveVersionedKey(passphrase, 1, salt);
|
|
3680
|
+
const versionedKey = await deriveVersionedKey(passphrase, 1, salt, iterations);
|
|
3504
3681
|
const keys = /* @__PURE__ */ new Map();
|
|
3505
3682
|
keys.set(1, versionedKey);
|
|
3506
3683
|
return new _SyncEncryptor(keys, 1);
|
|
@@ -3801,10 +3978,12 @@ var OFFLINE_SYNC_STATUS = Object.freeze({
|
|
|
3801
3978
|
lastSyncedAt: null,
|
|
3802
3979
|
lastSuccessfulPush: null,
|
|
3803
3980
|
lastSuccessfulPull: null,
|
|
3804
|
-
conflicts: 0
|
|
3981
|
+
conflicts: 0,
|
|
3982
|
+
clockSkewMs: null
|
|
3805
3983
|
});
|
|
3806
3984
|
var SYNC_STATUS_EVENT_TYPES = [
|
|
3807
3985
|
"sync:connected",
|
|
3986
|
+
"sync:clock-skew",
|
|
3808
3987
|
"sync:disconnected",
|
|
3809
3988
|
"sync:schema-mismatch",
|
|
3810
3989
|
"sync:auth-failed",
|