@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.
Files changed (40) hide show
  1. package/dist/src/Assert.d.ts +6 -3
  2. package/dist/src/Assert.d.ts.map +1 -1
  3. package/dist/src/Assert.js +6 -3
  4. package/dist/src/Crypto.d.ts +11 -0
  5. package/dist/src/Crypto.d.ts.map +1 -1
  6. package/dist/src/Evolu/Config.d.ts +17 -4
  7. package/dist/src/Evolu/Config.d.ts.map +1 -1
  8. package/dist/src/Evolu/Config.js +1 -1
  9. package/dist/src/Evolu/Db.d.ts +21 -13
  10. package/dist/src/Evolu/Db.d.ts.map +1 -1
  11. package/dist/src/Evolu/Db.js +81 -64
  12. package/dist/src/Evolu/Evolu.d.ts +6 -6
  13. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  14. package/dist/src/Evolu/Evolu.js +19 -24
  15. package/dist/src/Evolu/Protocol.d.ts +79 -37
  16. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  17. package/dist/src/Evolu/Protocol.js +170 -58
  18. package/dist/src/Evolu/Relay.d.ts +3 -1
  19. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  20. package/dist/src/Evolu/Relay.js +39 -3
  21. package/dist/src/Evolu/Schema.d.ts +24 -40
  22. package/dist/src/Evolu/Schema.d.ts.map +1 -1
  23. package/dist/src/Evolu/Schema.js +13 -72
  24. package/dist/src/Evolu/Storage.d.ts +1 -0
  25. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  26. package/dist/src/Evolu/Storage.js +10 -0
  27. package/dist/src/Type.d.ts +42 -2
  28. package/dist/src/Type.d.ts.map +1 -1
  29. package/dist/src/Type.js +58 -1
  30. package/package.json +3 -3
  31. package/src/Assert.ts +6 -3
  32. package/src/Crypto.ts +13 -0
  33. package/src/Evolu/Config.ts +19 -5
  34. package/src/Evolu/Db.ts +92 -76
  35. package/src/Evolu/Evolu.ts +38 -35
  36. package/src/Evolu/Protocol.ts +209 -89
  37. package/src/Evolu/Relay.ts +45 -4
  38. package/src/Evolu/Schema.ts +102 -130
  39. package/src/Evolu/Storage.ts +12 -0
  40. package/src/Type.ts +71 -3
@@ -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
- * | - {@link ProtocolErrorCode} | In non-initiator response. |
24
- * | **Messages** | |
25
- * | - {@link NonNegativeInt} | A number of messages. |
26
- * | - {@link EncryptedCrdtMessage} | |
27
- * | - {@link WriteKey} | In initiator request. |
28
- * | **Ranges** | |
29
- * | - {@link NonNegativeInt} | Number of ranges. |
30
- * | - {@link Range} | |
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
- * Every protocol message belongs to an {@link Owner}.
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, ReadonlyRecord } from "../Object.js";
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 interface DbChange {
301
- readonly table: Base64Url256;
302
- readonly id: Id;
303
- readonly values: ReadonlyRecord<Base64Url256, SqliteValue>;
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
- if (errorCode != null) buffers.header.extend([errorCode]);
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 = decodeNonNegativeInt(input);
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 messages = decodeMessages(input);
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 (isNonEmptyReadonlyArray(messages)) {
931
- const messagesEnd = inputMessage.length - input.getLength();
932
- const writeKey = input.shiftN(writeKeyLength) as WriteKey;
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
- const writeKeyIsValid = deps.storage.validateWriteKey(
935
- binaryOwnerId,
936
- writeKey,
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
- if (!writeKeyIsValid)
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
- if (broadcast) {
947
- // Instead of encoding a new protocol message, we reuse the inputMessage.
948
- const broadcastMessage = concatBytes(
949
- inputMessage.slice(0, 17),
950
- new Uint8Array([ProtocolErrorCode.NoError]),
951
- inputMessage.slice(17, messagesEnd),
952
- ) as ProtocolMessage;
953
-
954
- broadcast(ownerId, broadcastMessage);
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
- if (error instanceof ProtocolDecodeError || error instanceof BufferError)
987
- return err<ProtocolInvalidDataError>({
988
- type: "ProtocolInvalidDataError",
989
- data,
990
- error,
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 = urlAlphabet.indexOf(char);
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 >= urlAlphabet.length) {
1604
+ if (charValue < 0 || charValue >= base64UrlAlphabet.length) {
1509
1605
  throw new ProtocolDecodeError("invalid charValue");
1510
1606
  }
1511
- string += urlAlphabet[charValue];
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
+ };
@@ -1,7 +1,7 @@
1
1
  import { isNonEmptyReadonlyArray } from "../Array.js";
2
2
  import { ConsoleConfig } from "../Console.js";
3
- import { eqArrayNumber } from "../Eq.js";
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: SqliteStorageDeps) =>
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 eqArrayNumber(rows[0].writeKey, writeKey);
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
  };