171171 * `applyProtocolMessage` function with conditional arguments to reduce code
172172 * duplication.
173173 * - ProtocolQuotaError should return storedBytes and actual quota.
174+ * - Replace try-catch with Result + new Error (to preserve stacktraces). Measure
175+ * Result overhead, it should be super small.
174176 */
175177
176178import { Packr } from "msgpackr" ;
@@ -186,8 +188,8 @@ import {
186188 utf8ToBytes ,
187189} from "../Buffer.js" ;
188190import {
191+ createPadmePadding ,
189192 EncryptionKey ,
190- padmePaddingLength ,
191193 RandomBytesDep ,
192194 SymmetricCryptoDecryptError ,
193195 SymmetricCryptoDep ,
@@ -436,7 +438,7 @@ export interface ProtocolSyncError extends BaseOwnerError {
436438export interface ProtocolTimestampMismatchError {
437439 readonly type : "ProtocolTimestampMismatchError" ;
438440 readonly expected : Timestamp ;
439- readonly embedded : Timestamp ;
441+ readonly timestamp : Timestamp ;
440442}
441443
442444/**
@@ -909,8 +911,6 @@ export const applyProtocolMessageAsClient =
909911 | ProtocolQuotaError
910912 >
911913 > => {
912- // try-catch instead of Result for performance and stacktraces
913- // DEV: Measure it again, I think we should use Result with new Error.
914914 try {
915915 const input = createBuffer ( inputMessage ) ;
916916 const [ requestedVersion , ownerId ] = decodeVersionAndOwner ( input ) ;
@@ -1057,8 +1057,6 @@ export const applyProtocolMessageAsRelay =
10571057 ) : Promise <
10581058 Result < ApplyProtocolMessageAsRelayResult , ProtocolInvalidDataError >
10591059 > => {
1060- // try-catch instead of Result for performance and stacktraces
1061- // DEV: Measure it again, I think we should use Result with new Error.
10621060 try {
10631061 const input = createBuffer ( inputMessage ) ;
10641062 const [ requestedVersion , ownerId ] = decodeVersionAndOwner ( input ) ;
@@ -1664,6 +1662,52 @@ export const decodeNumber = (buffer: Buffer): number => {
16641662 return numberResult . value ;
16651663} ;
16661664
1665+ /**
1666+ * Encodes an array of boolean flags into a single byte.
1667+ *
1668+ * Each element in the array corresponds to a bit (0-7). Array can have 0-8
1669+ * elements.
1670+ *
1671+ * ### Example
1672+ *
1673+ * ```ts
1674+ * encodeFlags(buffer, [true, false, true]); // Encodes bits 0, 1, 2
1675+ * ```
1676+ */
1677+ export const encodeFlags = (
1678+ buffer : Buffer ,
1679+ flags : ReadonlyArray < boolean > ,
1680+ ) : void => {
1681+ let byte = 0 ;
1682+ for ( let i = 0 ; i < flags . length && i < 8 ; i ++ ) {
1683+ if ( flags [ i ] ) {
1684+ byte |= 1 << i ;
1685+ }
1686+ }
1687+ buffer . extend ( [ byte ] ) ;
1688+ } ;
1689+
1690+ /**
1691+ * Decodes a byte into an array of boolean flags.
1692+ *
1693+ * ### Example
1694+ *
1695+ * ```ts
1696+ * const flags = decodeFlags(buffer, 3); // Decode 3 flags
1697+ * ```
1698+ */
1699+ export const decodeFlags = (
1700+ buffer : Buffer ,
1701+ count : PositiveInt ,
1702+ ) : ReadonlyArray < boolean > => {
1703+ const byte = buffer . shift ( ) ;
1704+ const flags : Array < boolean > = [ ] ;
1705+ for ( let i = 0 ; i < count && i < 8 ; i ++ ) {
1706+ flags . push ( ( byte & ( 1 << i ) ) !== 0 ) ;
1707+ }
1708+ return flags ;
1709+ } ;
1710+
16671711/**
16681712 * Encodes and encrypts a {@link DbChange} using the provided owner's encryption
16691713 * key. Returns an encrypted binary representation as {@link EncryptedDbChange}.
@@ -1675,36 +1719,29 @@ export const decodeNumber = (buffer: Buffer): number => {
16751719export const encodeAndEncryptDbChange =
16761720 ( deps : SymmetricCryptoDep ) =>
16771721 ( message : CrdtMessage , key : EncryptionKey ) : EncryptedDbChange => {
1678- const change = message . change ;
16791722 const buffer = createBuffer ( ) ;
16801723
1681- // Encode protocol version first for backward compatibility
16821724 encodeNonNegativeInt ( buffer , protocolVersion ) ;
16831725
1684- // Encode the timestamp (after version) for tamper verification
1685- const timestampBytes = timestampToTimestampBytes ( message . timestamp ) ;
1686- buffer . extend ( timestampBytes ) ;
1726+ // Encode the timestamp to prevent tampering (e.g., a malicious relay
1727+ // assigning this EncryptedDbChange to a different EncryptedCrdtMessage)
1728+ buffer . extend ( timestampToTimestampBytes ( message . timestamp ) ) ;
16871729
1688- encodeString ( buffer , change . table ) ;
1730+ encodeFlags ( buffer , [ message . change . isInsert ] ) ;
16891731
1690- buffer . extend ( idToIdBytes ( change . id ) ) ;
1732+ encodeString ( buffer , message . change . table ) ;
1733+ buffer . extend ( idToIdBytes ( message . change . id ) ) ;
16911734
1692- const entries = objectToEntries ( change . values ) . map (
1693- ( [ column , value ] ) : [ string , SqliteValue ] => {
1694- return [ column , value ] ;
1695- } ,
1696- ) ;
1735+ const entries = objectToEntries ( message . change . values ) ;
16971736
16981737 encodeLength ( buffer , entries ) ;
1699-
17001738 for ( const [ column , value ] of entries ) {
17011739 encodeString ( buffer , column ) ;
17021740 encodeSqliteValue ( buffer , value ) ;
17031741 }
17041742
1705- const paddingLength = padmePaddingLength ( buffer . getLength ( ) ) ;
1706- // Add zero bytes as PADMÉ padding - these will be ignored during decoding.
1707- buffer . extend ( new Uint8Array ( paddingLength ) ) ;
1743+ // Add PADMÉ padding (ignored during decoding)
1744+ buffer . extend ( createPadmePadding ( buffer . getLength ( ) ) ) ;
17081745
17091746 const { nonce, ciphertext } = deps . symmetricCrypto . encrypt (
17101747 buffer . unwrap ( ) ,
@@ -1735,13 +1772,11 @@ export const decryptAndDecodeDbChange =
17351772 | ProtocolInvalidDataError
17361773 | ProtocolTimestampMismatchError
17371774 > => {
1738- // try-catch instead of Result for performance and stacktraces
17391775 try {
17401776 const buffer = createBuffer ( message . change ) ;
1741- const nonce = buffer . shiftN ( deps . symmetricCrypto . nonceLength ) ;
17421777
1743- const ciphertextLength = decodeLength ( buffer ) ;
1744- const ciphertext = buffer . shiftN ( ciphertextLength ) ;
1778+ const nonce = buffer . shiftN ( deps . symmetricCrypto . nonceLength ) ;
1779+ const ciphertext = buffer . shiftN ( decodeLength ( buffer ) ) ;
17451780
17461781 const plaintextBytes = deps . symmetricCrypto . decrypt (
17471782 ciphertext ,
@@ -1753,24 +1788,24 @@ export const decryptAndDecodeDbChange =
17531788 buffer . reset ( ) ;
17541789 buffer . extend ( plaintextBytes . value ) ;
17551790
1756- // Decode version (for future compatibility, no validation needed for now )
1791+ // Decode version (for future compatibility, not need yet )
17571792 decodeNonNegativeInt ( buffer ) ;
17581793
1759- // Decode and verify the embedded timestamp
1760- const embeddedTimestampBytes = buffer . shiftN ( timestampBytesLength ) ;
1761- const embeddedTimestamp = timestampBytesToTimestamp (
1762- embeddedTimestampBytes as TimestampBytes ,
1794+ const timestamp = timestampBytesToTimestamp (
1795+ buffer . shiftN ( timestampBytesLength ) as TimestampBytes ,
17631796 ) ;
17641797
1765- // Verify timestamp integrity
1766- if ( ! eqTimestamp ( embeddedTimestamp , message . timestamp ) ) {
1798+ if ( ! eqTimestamp ( timestamp , message . timestamp ) ) {
17671799 return err < ProtocolTimestampMismatchError > ( {
17681800 type : "ProtocolTimestampMismatchError" ,
17691801 expected : message . timestamp ,
1770- embedded : embeddedTimestamp ,
1802+ timestamp ,
17711803 } ) ;
17721804 }
17731805
1806+ const flags = decodeFlags ( buffer , PositiveInt . orThrow ( 1 ) ) ;
1807+ const isInsert = flags [ 0 ] ;
1808+
17741809 const table = decodeString ( buffer ) ;
17751810 const id = decodeId ( buffer ) ;
17761811
@@ -1783,7 +1818,7 @@ export const decryptAndDecodeDbChange =
17831818 values [ column ] = value ;
17841819 }
17851820
1786- const dbChange = { table, id, values } ;
1821+ const dbChange = DbChange . orThrow ( { table, id, values, isInsert } ) ;
17871822
17881823 return ok ( dbChange ) ;
17891824 } catch ( error ) {
0 commit comments