@evolu/common 6.0.1-preview.1 → 6.0.1-preview.11
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/dist/src/Assert.d.ts +6 -3
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +6 -3
- package/dist/src/Crypto.d.ts +11 -0
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Evolu/Config.d.ts +17 -4
- package/dist/src/Evolu/Config.d.ts.map +1 -1
- package/dist/src/Evolu/Config.js +1 -1
- package/dist/src/Evolu/Db.d.ts +21 -13
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +81 -64
- package/dist/src/Evolu/Evolu.d.ts +6 -6
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +19 -24
- package/dist/src/Evolu/Protocol.d.ts +79 -37
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +170 -58
- package/dist/src/Evolu/Relay.d.ts +3 -1
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +39 -3
- package/dist/src/Evolu/Schema.d.ts +24 -40
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +13 -72
- package/dist/src/Evolu/Storage.d.ts +1 -0
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +10 -0
- package/dist/src/Type.d.ts +42 -2
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +58 -1
- package/package.json +3 -3
- package/src/Assert.ts +6 -3
- package/src/Crypto.ts +13 -0
- package/src/Evolu/Config.ts +19 -5
- package/src/Evolu/Db.ts +92 -76
- package/src/Evolu/Evolu.ts +38 -35
- package/src/Evolu/Protocol.ts +209 -89
- package/src/Evolu/Relay.ts +45 -4
- package/src/Evolu/Schema.ts +102 -130
- package/src/Evolu/Storage.ts +12 -0
- package/src/Type.ts +71 -3
package/src/Evolu/Protocol.ts
CHANGED
|
@@ -15,21 +15,39 @@
|
|
|
15
15
|
*
|
|
16
16
|
* ### Message Structure
|
|
17
17
|
*
|
|
18
|
-
* | Field | Notes
|
|
19
|
-
* | :----------------------------- |
|
|
20
|
-
* | **Header** |
|
|
21
|
-
* | - {@link protocolVersion} |
|
|
22
|
-
* | - {@link OwnerId} |
|
|
23
|
-
* |
|
|
24
|
-
* |
|
|
25
|
-
* | - {@link
|
|
26
|
-
* | - {@link
|
|
27
|
-
* | -
|
|
28
|
-
* |
|
|
29
|
-
* |
|
|
30
|
-
* | - {@link
|
|
18
|
+
* | Field | Notes |
|
|
19
|
+
* | :----------------------------- | :------------------------ |
|
|
20
|
+
* | **Header** | |
|
|
21
|
+
* | - {@link protocolVersion} | |
|
|
22
|
+
* | - {@link OwnerId} | {@link Owner} |
|
|
23
|
+
* | **Initiator** | |
|
|
24
|
+
* | - {@link WriteKeyMode} | |
|
|
25
|
+
* | - {@link WriteKey} | If WriteKeyMode >= 1 |
|
|
26
|
+
* | - {@link WriteKey} | If WriteKeyMode = 2 (new) |
|
|
27
|
+
* | **Non-initiator** | |
|
|
28
|
+
* | - {@link ProtocolErrorCode} | |
|
|
29
|
+
* | **Messages** | |
|
|
30
|
+
* | - {@link NonNegativeInt} | A number of messages. |
|
|
31
|
+
* | - {@link EncryptedCrdtMessage} | |
|
|
32
|
+
* | **Ranges** | |
|
|
33
|
+
* | - {@link NonNegativeInt} | Number of ranges. |
|
|
34
|
+
* | - {@link Range} | |
|
|
31
35
|
*
|
|
32
|
-
*
|
|
36
|
+
* ### WriteKey Validation
|
|
37
|
+
*
|
|
38
|
+
* The initiator sends WriteKeyMode and optionally one or two WriteKeys. One key
|
|
39
|
+
* for write operations and two for key rotation (current and new). Note that
|
|
40
|
+
* it's ok to not send any key if initiator is going to be synced with readonly
|
|
41
|
+
* owner. The non-initiator validates them immediately after parsing the
|
|
42
|
+
* initiator header, before processing any messages or ranges.
|
|
43
|
+
*
|
|
44
|
+
* ### WriteKey Rotation
|
|
45
|
+
*
|
|
46
|
+
* When initiator's {@link WriteKeyMode} is `Rotation`, two WriteKeys are
|
|
47
|
+
* present:
|
|
48
|
+
*
|
|
49
|
+
* 1. Current WriteKey (for validation)
|
|
50
|
+
* 2. New WriteKey (to be stored)
|
|
33
51
|
*
|
|
34
52
|
* ### Synchronization
|
|
35
53
|
*
|
|
@@ -127,10 +145,8 @@ import { isNonEmptyReadonlyArray, NonEmptyReadonlyArray } from "../Array.js";
|
|
|
127
145
|
import { assert } from "../Assert.js";
|
|
128
146
|
import {
|
|
129
147
|
Buffer,
|
|
130
|
-
BufferError,
|
|
131
148
|
bytesToHex,
|
|
132
149
|
bytesToUtf8,
|
|
133
|
-
concatBytes,
|
|
134
150
|
createBuffer,
|
|
135
151
|
hexToBytes,
|
|
136
152
|
utf8ToBytes,
|
|
@@ -144,11 +160,12 @@ import {
|
|
|
144
160
|
} from "../Crypto.js";
|
|
145
161
|
import { eqArrayNumber } from "../Eq.js";
|
|
146
162
|
import { computeBalancedBuckets } from "../Number.js";
|
|
147
|
-
import { objectToEntries
|
|
163
|
+
import { objectToEntries } from "../Object.js";
|
|
148
164
|
import { err, ok, Result } from "../Result.js";
|
|
149
165
|
import { SqliteValue } from "../Sqlite.js";
|
|
150
166
|
import {
|
|
151
167
|
Base64Url,
|
|
168
|
+
base64UrlAlphabet,
|
|
152
169
|
DateIsoString,
|
|
153
170
|
Id,
|
|
154
171
|
idTypeValueLength,
|
|
@@ -157,11 +174,12 @@ import {
|
|
|
157
174
|
NanoId,
|
|
158
175
|
NonNegativeInt,
|
|
159
176
|
Number,
|
|
177
|
+
object,
|
|
160
178
|
PositiveInt,
|
|
179
|
+
record,
|
|
161
180
|
} from "../Type.js";
|
|
162
181
|
import { Brand, Predicate } from "../Types.js";
|
|
163
182
|
import {
|
|
164
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
165
183
|
Owner,
|
|
166
184
|
OwnerId,
|
|
167
185
|
OwnerWithWriteAccess,
|
|
@@ -203,6 +221,14 @@ export const ProtocolErrorCode = {
|
|
|
203
221
|
type ProtocolErrorCode =
|
|
204
222
|
(typeof ProtocolErrorCode)[keyof typeof ProtocolErrorCode];
|
|
205
223
|
|
|
224
|
+
export const WriteKeyMode = {
|
|
225
|
+
None: 0,
|
|
226
|
+
Single: 1,
|
|
227
|
+
Rotation: 2,
|
|
228
|
+
} as const;
|
|
229
|
+
|
|
230
|
+
type WriteKeyMode = (typeof WriteKeyMode)[keyof typeof WriteKeyMode];
|
|
231
|
+
|
|
206
232
|
/**
|
|
207
233
|
* Evolu Protocol Storage
|
|
208
234
|
*
|
|
@@ -247,17 +273,15 @@ export interface Storage {
|
|
|
247
273
|
callback: (timestamp: BinaryTimestamp, index: NonNegativeInt) => boolean,
|
|
248
274
|
) => void;
|
|
249
275
|
|
|
250
|
-
/**
|
|
251
|
-
* Authorizes the initiator's {@link WriteKey} for the given
|
|
252
|
-
* {@link BinaryOwnerId}.
|
|
253
|
-
*
|
|
254
|
-
* For a client that does not expect foreign writes, return `false`.
|
|
255
|
-
*/
|
|
276
|
+
/** Validates the {@link WriteKey} for the given {@link Owner}. */
|
|
256
277
|
readonly validateWriteKey: (
|
|
257
278
|
ownerId: BinaryOwnerId,
|
|
258
279
|
writeKey: WriteKey,
|
|
259
280
|
) => boolean;
|
|
260
281
|
|
|
282
|
+
/** Sets the {@link WriteKey} for the given {@link Owner}. */
|
|
283
|
+
readonly setWriteKey: (ownerId: BinaryOwnerId, writeKey: WriteKey) => boolean;
|
|
284
|
+
|
|
261
285
|
/** Write encrypted {@link CrdtMessage}s to storage. */
|
|
262
286
|
readonly writeMessages: (
|
|
263
287
|
ownerId: BinaryOwnerId,
|
|
@@ -269,6 +293,9 @@ export interface Storage {
|
|
|
269
293
|
ownerId: BinaryOwnerId,
|
|
270
294
|
timestamp: BinaryTimestamp,
|
|
271
295
|
) => EncryptedDbChange | null;
|
|
296
|
+
|
|
297
|
+
/** Delete all data for the given {@link Owner}. */
|
|
298
|
+
readonly deleteOwner: (ownerId: BinaryOwnerId) => boolean;
|
|
272
299
|
}
|
|
273
300
|
|
|
274
301
|
export interface StorageDep {
|
|
@@ -293,15 +320,23 @@ export interface CrdtMessage {
|
|
|
293
320
|
readonly change: DbChange;
|
|
294
321
|
}
|
|
295
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Base64Url string with maximum length of 256 characters. Encoding strings as
|
|
325
|
+
* Base64UrlString saves up to 25% in size compared to regular strings.
|
|
326
|
+
*/
|
|
327
|
+
export const Base64Url256 = maxLength(256)(Base64Url);
|
|
328
|
+
export type Base64Url256 = typeof Base64Url256.Type;
|
|
329
|
+
|
|
296
330
|
/**
|
|
297
331
|
* A DbChange is a change to a table row. Together with a unique
|
|
298
332
|
* {@link Timestamp}, it forms a {@link CrdtMessage}.
|
|
299
333
|
*/
|
|
300
|
-
export
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
334
|
+
export const DbChange = object({
|
|
335
|
+
table: Base64Url256,
|
|
336
|
+
id: Id,
|
|
337
|
+
values: record(Base64Url256, SqliteValue),
|
|
338
|
+
});
|
|
339
|
+
export type DbChange = typeof DbChange.Type;
|
|
305
340
|
|
|
306
341
|
export const RangeType = {
|
|
307
342
|
Fingerprint: 1,
|
|
@@ -425,6 +460,7 @@ export const createProtocolMessageFromCrdtMessages =
|
|
|
425
460
|
maxSize?: PositiveInt,
|
|
426
461
|
): ProtocolMessage => {
|
|
427
462
|
const buffer = createProtocolMessageBuffer(owner.id, {
|
|
463
|
+
type: "initiator",
|
|
428
464
|
totalMaxSize: maxSize ?? maxProtocolMessageSize,
|
|
429
465
|
writeKey: owner.writeKey,
|
|
430
466
|
});
|
|
@@ -480,7 +516,7 @@ export const createProtocolMessageFromCrdtMessages =
|
|
|
480
516
|
export const createProtocolMessageForSync =
|
|
481
517
|
(deps: StorageDep) =>
|
|
482
518
|
(ownerId: OwnerId): ProtocolMessage | null => {
|
|
483
|
-
const buffer = createProtocolMessageBuffer(ownerId);
|
|
519
|
+
const buffer = createProtocolMessageBuffer(ownerId, { type: "initiator" });
|
|
484
520
|
const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
|
|
485
521
|
|
|
486
522
|
const size = deps.storage.getSize(binaryOwnerId);
|
|
@@ -498,6 +534,19 @@ export const createProtocolMessageForSync =
|
|
|
498
534
|
return buffer.unwrap();
|
|
499
535
|
};
|
|
500
536
|
|
|
537
|
+
/** Creates a ProtocolMessage for {@link WriteKey} rotation. */
|
|
538
|
+
export const createProtocolMessageForWriteKeyRotation = (
|
|
539
|
+
ownerId: OwnerId,
|
|
540
|
+
currentWriteKey: WriteKey,
|
|
541
|
+
newWriteKey: WriteKey,
|
|
542
|
+
): ProtocolMessage => {
|
|
543
|
+
const buffer = createProtocolMessageBuffer(ownerId, {
|
|
544
|
+
type: "initiator",
|
|
545
|
+
writeKey: [currentWriteKey, newWriteKey],
|
|
546
|
+
});
|
|
547
|
+
return buffer.unwrap();
|
|
548
|
+
};
|
|
549
|
+
|
|
501
550
|
/**
|
|
502
551
|
* Mutable builder for constructing {@link ProtocolMessage} respecting size
|
|
503
552
|
* limits.
|
|
@@ -525,16 +574,22 @@ export interface ProtocolMessageBuffer {
|
|
|
525
574
|
export const createProtocolMessageBuffer = (
|
|
526
575
|
ownerId: OwnerId,
|
|
527
576
|
options: {
|
|
528
|
-
readonly errorCode?: ProtocolErrorCode;
|
|
529
|
-
readonly writeKey?: WriteKey;
|
|
530
577
|
readonly totalMaxSize?: PositiveInt | undefined;
|
|
531
578
|
readonly rangesMaxSize?: PositiveInt | undefined;
|
|
532
579
|
readonly version?: NonNegativeInt;
|
|
533
|
-
}
|
|
580
|
+
} & (
|
|
581
|
+
| {
|
|
582
|
+
readonly type: "initiator";
|
|
583
|
+
/** Single key or [current, new] for rotation. */
|
|
584
|
+
readonly writeKey?: WriteKey | readonly [WriteKey, WriteKey];
|
|
585
|
+
}
|
|
586
|
+
| {
|
|
587
|
+
readonly type: "non-initiator";
|
|
588
|
+
readonly errorCode: ProtocolErrorCode;
|
|
589
|
+
}
|
|
590
|
+
),
|
|
534
591
|
): ProtocolMessageBuffer => {
|
|
535
592
|
const {
|
|
536
|
-
errorCode,
|
|
537
|
-
writeKey,
|
|
538
593
|
totalMaxSize = maxProtocolMessageSize,
|
|
539
594
|
rangesMaxSize = maxProtocolMessageRangesSize,
|
|
540
595
|
version = protocolVersion,
|
|
@@ -555,7 +610,21 @@ export const createProtocolMessageBuffer = (
|
|
|
555
610
|
|
|
556
611
|
encodeNonNegativeInt(buffers.header, version);
|
|
557
612
|
buffers.header.extend(ownerIdToBinaryOwnerId(ownerId));
|
|
558
|
-
|
|
613
|
+
|
|
614
|
+
if (options.type === "initiator") {
|
|
615
|
+
if (!options.writeKey) {
|
|
616
|
+
buffers.header.extend([WriteKeyMode.None]);
|
|
617
|
+
} else if (!Array.isArray(options.writeKey)) {
|
|
618
|
+
buffers.header.extend([WriteKeyMode.Single]);
|
|
619
|
+
buffers.header.extend(options.writeKey as WriteKey);
|
|
620
|
+
} else {
|
|
621
|
+
buffers.header.extend([WriteKeyMode.Rotation]);
|
|
622
|
+
buffers.header.extend(options.writeKey[0] as WriteKey); // current
|
|
623
|
+
buffers.header.extend(options.writeKey[1] as WriteKey); // new
|
|
624
|
+
}
|
|
625
|
+
} else {
|
|
626
|
+
buffers.header.extend([options.errorCode]);
|
|
627
|
+
}
|
|
559
628
|
|
|
560
629
|
let isLastRangeInfinite = false;
|
|
561
630
|
|
|
@@ -567,10 +636,7 @@ export const createProtocolMessageBuffer = (
|
|
|
567
636
|
const getHeaderAndMessagesSize = () =>
|
|
568
637
|
buffers.header.getLength() +
|
|
569
638
|
buffers.messages.timestamps.getLength() +
|
|
570
|
-
buffers.messages.dbChanges.getLength()
|
|
571
|
-
(buffers.messages.timestamps.getCount() > 0 && writeKey
|
|
572
|
-
? writeKeyLength
|
|
573
|
-
: 0);
|
|
639
|
+
buffers.messages.dbChanges.getLength();
|
|
574
640
|
|
|
575
641
|
const getRangesSize = () =>
|
|
576
642
|
buffers.ranges.timestamps.getCount() > 0
|
|
@@ -685,8 +751,6 @@ export const createProtocolMessageBuffer = (
|
|
|
685
751
|
|
|
686
752
|
buffers.messages.timestamps.append(buffers.header);
|
|
687
753
|
buffers.header.extend(buffers.messages.dbChanges.unwrap());
|
|
688
|
-
if (buffers.messages.timestamps.getCount() > 0 && writeKey)
|
|
689
|
-
buffers.header.extend(writeKey);
|
|
690
754
|
|
|
691
755
|
if (buffers.ranges.timestamps.getCount() > 0) {
|
|
692
756
|
buffers.ranges.timestamps.append(buffers.header);
|
|
@@ -836,8 +900,6 @@ export const applyProtocolMessageAsClient =
|
|
|
836
900
|
});
|
|
837
901
|
}
|
|
838
902
|
|
|
839
|
-
const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
|
|
840
|
-
|
|
841
903
|
const errorCode = input.shift() as ProtocolErrorCode;
|
|
842
904
|
if (errorCode !== ProtocolErrorCode.NoError) {
|
|
843
905
|
switch (errorCode) {
|
|
@@ -864,6 +926,7 @@ export const applyProtocolMessageAsClient =
|
|
|
864
926
|
}
|
|
865
927
|
|
|
866
928
|
const messages = decodeMessages(input);
|
|
929
|
+
const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
|
|
867
930
|
|
|
868
931
|
if (
|
|
869
932
|
isNonEmptyReadonlyArray(messages) &&
|
|
@@ -877,6 +940,7 @@ export const applyProtocolMessageAsClient =
|
|
|
877
940
|
if (writeKey == null) return ok(null);
|
|
878
941
|
|
|
879
942
|
const output = createProtocolMessageBuffer(ownerId, {
|
|
943
|
+
type: "initiator",
|
|
880
944
|
writeKey,
|
|
881
945
|
totalMaxSize,
|
|
882
946
|
rangesMaxSize,
|
|
@@ -911,8 +975,7 @@ export const applyProtocolMessageAsRelay =
|
|
|
911
975
|
version = protocolVersion,
|
|
912
976
|
): Result<ProtocolMessage | null, ProtocolInvalidDataError> =>
|
|
913
977
|
tryDecodeProtocolData(inputMessage, (input) => {
|
|
914
|
-
const requestedVersion =
|
|
915
|
-
const ownerId = decodeOwnerId(input);
|
|
978
|
+
const [requestedVersion, ownerId] = decodeVersionAndOwner(input);
|
|
916
979
|
const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
|
|
917
980
|
|
|
918
981
|
if (requestedVersion !== version) {
|
|
@@ -925,44 +988,89 @@ export const applyProtocolMessageAsRelay =
|
|
|
925
988
|
|
|
926
989
|
subscribe?.(ownerId);
|
|
927
990
|
|
|
928
|
-
const
|
|
991
|
+
const writeKeyMode = input.shift() as WriteKeyMode;
|
|
992
|
+
let writeKey: WriteKey | undefined;
|
|
993
|
+
let newWriteKey: WriteKey | undefined;
|
|
994
|
+
|
|
995
|
+
if (writeKeyMode !== WriteKeyMode.None) {
|
|
996
|
+
writeKey = input.shiftN(writeKeyLength) as WriteKey;
|
|
997
|
+
switch (writeKeyMode) {
|
|
998
|
+
case WriteKeyMode.Single:
|
|
999
|
+
break;
|
|
1000
|
+
case WriteKeyMode.Rotation:
|
|
1001
|
+
newWriteKey = input.shiftN(writeKeyLength) as WriteKey;
|
|
1002
|
+
break;
|
|
1003
|
+
default:
|
|
1004
|
+
throw new ProtocolDecodeError(
|
|
1005
|
+
`Invalid WriteKeyMode: ${writeKeyMode}`,
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
929
1009
|
|
|
930
|
-
if (
|
|
931
|
-
const
|
|
932
|
-
|
|
1010
|
+
if (writeKey) {
|
|
1011
|
+
const isValid = deps.storage.validateWriteKey(binaryOwnerId, writeKey);
|
|
1012
|
+
if (!isValid) {
|
|
1013
|
+
return ok(
|
|
1014
|
+
createProtocolMessageBuffer(ownerId, {
|
|
1015
|
+
type: "non-initiator",
|
|
1016
|
+
errorCode: ProtocolErrorCode.WriteKeyError,
|
|
1017
|
+
}).unwrap(),
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
933
1020
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1021
|
+
if (newWriteKey) {
|
|
1022
|
+
const rotationSuccess = deps.storage.setWriteKey(
|
|
1023
|
+
binaryOwnerId,
|
|
1024
|
+
newWriteKey,
|
|
1025
|
+
);
|
|
1026
|
+
if (!rotationSuccess) {
|
|
1027
|
+
return ok(
|
|
1028
|
+
createProtocolMessageBuffer(ownerId, {
|
|
1029
|
+
type: "non-initiator",
|
|
1030
|
+
errorCode: ProtocolErrorCode.WriteError,
|
|
1031
|
+
}).unwrap(),
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
const messages = decodeMessages(input);
|
|
938
1038
|
|
|
939
|
-
|
|
1039
|
+
if (isNonEmptyReadonlyArray(messages)) {
|
|
1040
|
+
if (!writeKey)
|
|
940
1041
|
return ok(
|
|
941
1042
|
createProtocolMessageBuffer(ownerId, {
|
|
1043
|
+
type: "non-initiator",
|
|
942
1044
|
errorCode: ProtocolErrorCode.WriteKeyError,
|
|
943
1045
|
}).unwrap(),
|
|
944
1046
|
);
|
|
945
1047
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
const
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1048
|
+
// Only broadcast if there's no ranges.
|
|
1049
|
+
if (broadcast && input.getLength() === 0) {
|
|
1050
|
+
const broadcastBuffer = createProtocolMessageBuffer(ownerId, {
|
|
1051
|
+
type: "non-initiator",
|
|
1052
|
+
errorCode: ProtocolErrorCode.NoError,
|
|
1053
|
+
totalMaxSize,
|
|
1054
|
+
rangesMaxSize,
|
|
1055
|
+
version,
|
|
1056
|
+
});
|
|
1057
|
+
for (const message of messages) {
|
|
1058
|
+
broadcastBuffer.addMessage(message);
|
|
1059
|
+
}
|
|
1060
|
+
broadcast(ownerId, broadcastBuffer.unwrap());
|
|
955
1061
|
}
|
|
956
1062
|
|
|
957
1063
|
if (!deps.storage.writeMessages(binaryOwnerId, messages))
|
|
958
1064
|
return ok(
|
|
959
1065
|
createProtocolMessageBuffer(ownerId, {
|
|
1066
|
+
type: "non-initiator",
|
|
960
1067
|
errorCode: ProtocolErrorCode.WriteError,
|
|
961
1068
|
}).unwrap(),
|
|
962
1069
|
);
|
|
963
1070
|
}
|
|
964
1071
|
|
|
965
1072
|
const output = createProtocolMessageBuffer(ownerId, {
|
|
1073
|
+
type: "non-initiator",
|
|
966
1074
|
errorCode: ProtocolErrorCode.NoError,
|
|
967
1075
|
totalMaxSize,
|
|
968
1076
|
rangesMaxSize,
|
|
@@ -983,18 +1091,19 @@ const tryDecodeProtocolData = <T, E>(
|
|
|
983
1091
|
try {
|
|
984
1092
|
return callback(createBuffer(data));
|
|
985
1093
|
} catch (error: unknown) {
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
});
|
|
992
|
-
|
|
993
|
-
throw error;
|
|
1094
|
+
return err<ProtocolInvalidDataError>({
|
|
1095
|
+
type: "ProtocolInvalidDataError",
|
|
1096
|
+
data,
|
|
1097
|
+
error,
|
|
1098
|
+
});
|
|
994
1099
|
}
|
|
995
1100
|
};
|
|
996
1101
|
|
|
997
1102
|
const decodeVersionAndOwner = (input: Buffer): [NonNegativeInt, OwnerId] => {
|
|
1103
|
+
// This structure must never change across protocol versions. The version
|
|
1104
|
+
// and owner ID must always be the first two fields in every protocol message
|
|
1105
|
+
// to enable version negotiation and owner identification before any other
|
|
1106
|
+
// processing occurs.
|
|
998
1107
|
const version = decodeNonNegativeInt(input);
|
|
999
1108
|
const ownerId = decodeOwnerId(input);
|
|
1000
1109
|
return [version, ownerId];
|
|
@@ -1052,6 +1161,7 @@ const sync =
|
|
|
1052
1161
|
return ok(null);
|
|
1053
1162
|
}
|
|
1054
1163
|
const message = createProtocolMessageBuffer(binaryOwnerId, {
|
|
1164
|
+
type: "non-initiator",
|
|
1055
1165
|
errorCode: ProtocolErrorCode.SyncError,
|
|
1056
1166
|
});
|
|
1057
1167
|
return ok(message.unwrap());
|
|
@@ -1434,26 +1544,12 @@ export const ownerIdToBinaryOwnerId = (ownerId: OwnerId): BinaryOwnerId =>
|
|
|
1434
1544
|
export const binaryOwnerIdToOwnerId = (binaryOwnerId: BinaryOwnerId): OwnerId =>
|
|
1435
1545
|
decodeOwnerId(createBuffer(binaryOwnerId));
|
|
1436
1546
|
|
|
1437
|
-
/**
|
|
1438
|
-
* Base64Url string with maximum length of 256 characters. Encoding strings as
|
|
1439
|
-
* Base64UrlString saves up to 25% in size compared to regular strings.
|
|
1440
|
-
*/
|
|
1441
|
-
export const Base64Url256 = maxLength(256)(Base64Url);
|
|
1442
|
-
export type Base64Url256 = typeof Base64Url256.Type;
|
|
1443
|
-
|
|
1444
1547
|
/**
|
|
1445
1548
|
* Union type for all variants of Base64Url strings with limited length. All
|
|
1446
1549
|
* these types use Base64Url alphabet and are < 256 characters.
|
|
1447
1550
|
*/
|
|
1448
1551
|
export type Base64Url256Variant = Base64Url256 | Id | NanoId | OwnerId;
|
|
1449
1552
|
|
|
1450
|
-
/**
|
|
1451
|
-
* Alphabet used for Base64Url encoding. This is copied from the `nanoid`
|
|
1452
|
-
* library to avoid dependency on a specific version of `nanoid`.
|
|
1453
|
-
*/
|
|
1454
|
-
const urlAlphabet =
|
|
1455
|
-
"useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
|
1456
|
-
|
|
1457
1553
|
/**
|
|
1458
1554
|
* Converts a Base64Url string to a Uint8Array for binary storage. This encoding
|
|
1459
1555
|
* is more space-efficient than UTF-8 for Base64Url strings.
|
|
@@ -1470,7 +1566,7 @@ export const base64Url256ToBytes = (
|
|
|
1470
1566
|
let byteIndex = 0;
|
|
1471
1567
|
|
|
1472
1568
|
for (const char of string) {
|
|
1473
|
-
const charValue =
|
|
1569
|
+
const charValue = base64UrlAlphabet.indexOf(char);
|
|
1474
1570
|
bitBuffer = (bitBuffer << 6) | charValue;
|
|
1475
1571
|
bitsInBuffer += 6;
|
|
1476
1572
|
while (bitsInBuffer >= 8) {
|
|
@@ -1505,10 +1601,10 @@ export const decodeBase64Url256 = (
|
|
|
1505
1601
|
bitsInBuffer -= 6;
|
|
1506
1602
|
if (string.length < stringLength) {
|
|
1507
1603
|
const charValue = (bitBuffer >> bitsInBuffer) & 0x3f;
|
|
1508
|
-
if (charValue < 0 || charValue >=
|
|
1604
|
+
if (charValue < 0 || charValue >= base64UrlAlphabet.length) {
|
|
1509
1605
|
throw new ProtocolDecodeError("invalid charValue");
|
|
1510
1606
|
}
|
|
1511
|
-
string +=
|
|
1607
|
+
string += base64UrlAlphabet[charValue];
|
|
1512
1608
|
}
|
|
1513
1609
|
}
|
|
1514
1610
|
}
|
|
@@ -1895,3 +1991,27 @@ export const decodeSqliteValue = (buffer: Buffer): SqliteValue => {
|
|
|
1895
1991
|
throw new ProtocolDecodeError("invalid ProtocolValueType");
|
|
1896
1992
|
}
|
|
1897
1993
|
};
|
|
1994
|
+
|
|
1995
|
+
/**
|
|
1996
|
+
* Decodes a ProtocolMessage into a readable JSON object for debugging.
|
|
1997
|
+
*
|
|
1998
|
+
* Note: This is a stub for future implementation. It should use:
|
|
1999
|
+
*
|
|
2000
|
+
* - DecodeVersionAndOwner
|
|
2001
|
+
* - DecodeError or decodeWriteKeys (depending on context)
|
|
2002
|
+
* - DecodeMessages
|
|
2003
|
+
* - DecodeRanges
|
|
2004
|
+
*
|
|
2005
|
+
* If you want to help, please contribute to this function.
|
|
2006
|
+
*/
|
|
2007
|
+
export const decodeProtocolMessageToJson = (
|
|
2008
|
+
_protocolMessage: ProtocolMessage,
|
|
2009
|
+
_isInitiator: boolean,
|
|
2010
|
+
): unknown => {
|
|
2011
|
+
// TODO: Implement using
|
|
2012
|
+
// - decodeVersionAndOwner
|
|
2013
|
+
// -- decodeError or decodeWriteKeys (should be refactored out),
|
|
2014
|
+
// -- decodeMessages, and decodeRanges.
|
|
2015
|
+
// This is a stub for PRs and community contributions.
|
|
2016
|
+
throw new Error("decodeProtocolMessageToJson is not implemented yet.");
|
|
2017
|
+
};
|
package/src/Evolu/Relay.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isNonEmptyReadonlyArray } from "../Array.js";
|
|
2
2
|
import { ConsoleConfig } from "../Console.js";
|
|
3
|
-
import {
|
|
4
|
-
import { ok, Result } from "../Result.js";
|
|
3
|
+
import { TimingSafeEqualDep } from "../Crypto.js";
|
|
4
|
+
import { err, ok, Result } from "../Result.js";
|
|
5
5
|
import { sql, SqliteError } from "../Sqlite.js";
|
|
6
6
|
import { SimpleName } from "../Type.js";
|
|
7
7
|
import { OwnerId, WriteKey } from "./Owner.js";
|
|
@@ -19,8 +19,10 @@ export interface RelayConfig extends ConsoleConfig {
|
|
|
19
19
|
readonly name?: SimpleName;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export type RelaySqliteStorageDeps = SqliteStorageDeps & TimingSafeEqualDep;
|
|
23
|
+
|
|
22
24
|
export const createRelayStorage =
|
|
23
|
-
(deps:
|
|
25
|
+
(deps: RelaySqliteStorageDeps) =>
|
|
24
26
|
(options: CreateSqliteStorageBaseOptions): Result<Storage, SqliteError> => {
|
|
25
27
|
const sqliteStorageBase = createSqliteStorageBase(deps)(options);
|
|
26
28
|
if (!sqliteStorageBase.ok) return sqliteStorageBase;
|
|
@@ -87,7 +89,22 @@ export const createRelayStorage =
|
|
|
87
89
|
return true;
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
return
|
|
92
|
+
return deps.timingSafeEqual(rows[0].writeKey, writeKey);
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
setWriteKey: (ownerId, writeKey) => {
|
|
96
|
+
const upsertWriteKey = deps.sqlite.exec(sql`
|
|
97
|
+
insert into evolu_writeKey (ownerId, writeKey)
|
|
98
|
+
values (${ownerId}, ${writeKey})
|
|
99
|
+
on conflict (ownerId) do update
|
|
100
|
+
set writeKey = excluded.writeKey;
|
|
101
|
+
`);
|
|
102
|
+
if (!upsertWriteKey.ok) {
|
|
103
|
+
options.onStorageError(upsertWriteKey.error);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return true;
|
|
91
108
|
},
|
|
92
109
|
|
|
93
110
|
writeMessages: (ownerId, messages) => {
|
|
@@ -138,5 +155,29 @@ export const createRelayStorage =
|
|
|
138
155
|
|
|
139
156
|
return result.value.rows[0]?.change;
|
|
140
157
|
},
|
|
158
|
+
|
|
159
|
+
deleteOwner: (ownerId) => {
|
|
160
|
+
const result = deps.sqlite.transaction(() => {
|
|
161
|
+
const del1 = deps.sqlite.exec(sql`
|
|
162
|
+
delete from evolu_writeKey where ownerId = ${ownerId};
|
|
163
|
+
`);
|
|
164
|
+
if (!del1.ok) return del1;
|
|
165
|
+
|
|
166
|
+
const del2 = deps.sqlite.exec(sql`
|
|
167
|
+
delete from evolu_message where ownerId = ${ownerId};
|
|
168
|
+
`);
|
|
169
|
+
if (!del2.ok) return del2;
|
|
170
|
+
|
|
171
|
+
const del3 = sqliteStorageBase.value.deleteOwner(ownerId);
|
|
172
|
+
if (!del3) return err(null);
|
|
173
|
+
|
|
174
|
+
return ok();
|
|
175
|
+
});
|
|
176
|
+
if (!result.ok) {
|
|
177
|
+
if (result.error) options.onStorageError(result.error);
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
return true;
|
|
181
|
+
},
|
|
141
182
|
});
|
|
142
183
|
};
|