From 723552d001e85baeb3edf6ef35caf5aa1f4f6323 Mon Sep 17 00:00:00 2001 From: Pablo Carranza Velez Date: Tue, 28 Jan 2025 18:24:34 -0300 Subject: [PATCH 01/10] chore: sign update messages before sending --- .../src/HypergraphAppContext.tsx | 22 +++++----- packages/hypergraph/src/messages/index.ts | 1 + .../src/messages/signed-update-message.ts | 43 +++++++++++++++++++ packages/hypergraph/src/messages/types.ts | 1 + 4 files changed, 57 insertions(+), 10 deletions(-) create mode 100644 packages/hypergraph/src/messages/signed-update-message.ts diff --git a/packages/hypergraph-react/src/HypergraphAppContext.tsx b/packages/hypergraph-react/src/HypergraphAppContext.tsx index 7e5fb011..6e33922f 100644 --- a/packages/hypergraph-react/src/HypergraphAppContext.tsx +++ b/packages/hypergraph-react/src/HypergraphAppContext.tsx @@ -4,6 +4,7 @@ import * as automerge from '@automerge/automerge'; import { uuid } from '@automerge/automerge'; import { RepoContext } from '@automerge/automerge-repo-react-hooks'; import { Identity, Key, Messages, SpaceEvents, type SpaceStorageEntry, Utils, store } from '@graphprotocol/hypergraph'; +import { canonicalize } from '@graphprotocol/hypergraph/utils/jsc'; import { useSelector as useSelectorStore } from '@xstate/store/react'; import { Effect, Exit } from 'effect'; import * as Schema from 'effect/Schema'; @@ -469,6 +470,11 @@ export function HypergraphAppProvider({ console.error('No encryption private key found'); return; } + const signaturePrivateKey = keys?.signaturePrivateKey; + if (!signaturePrivateKey) { + console.error('No signature private key found.'); + return; + } const onMessage = async (event: MessageEvent) => { const data = Messages.deserialize(event.data); @@ -560,17 +566,13 @@ export function HypergraphAppProvider({ const ephemeralId = uuid(); - const nonceAndCiphertext = Messages.encryptMessage({ - message: lastLocalChange, - secretKey: Utils.hexToBytes(space.keys[0].key), - }); - - const messageToSend = { - type: 'create-update', + const messageToSend = Messages.signedUpdateMessage({ ephemeralId, - update: nonceAndCiphertext, spaceId: space.id, - } as const satisfies Messages.RequestCreateUpdate; + message: lastLocalChange, + secretKey: space.keys[0].key, + signaturePrivateKey, + }); websocketConnection.send(Messages.serialize(messageToSend)); } catch (error) { console.error('Error sending message', error); @@ -664,7 +666,7 @@ export function HypergraphAppProvider({ return () => { websocketConnection.removeEventListener('message', onMessage); }; - }, [websocketConnection, spaces, keys?.encryptionPrivateKey]); + }, [websocketConnection, spaces, keys?.encryptionPrivateKey, keys?.signaturePrivateKey]); const createSpaceForContext = async () => { if (!accountId) { diff --git a/packages/hypergraph/src/messages/index.ts b/packages/hypergraph/src/messages/index.ts index 8ccc7d6e..714afee9 100644 --- a/packages/hypergraph/src/messages/index.ts +++ b/packages/hypergraph/src/messages/index.ts @@ -1,4 +1,5 @@ export * from './decrypt-message.js'; export * from './encrypt-message.js'; export * from './serialize.js'; +export * from './signed-update-message.js'; export * from './types.js'; diff --git a/packages/hypergraph/src/messages/signed-update-message.ts b/packages/hypergraph/src/messages/signed-update-message.ts new file mode 100644 index 00000000..2a429914 --- /dev/null +++ b/packages/hypergraph/src/messages/signed-update-message.ts @@ -0,0 +1,43 @@ +import { secp256k1 } from '@noble/curves/secp256k1'; +import { canonicalize, hexToBytes, stringToUint8Array } from '../utils/index.js'; +import { encryptMessage } from './encrypt-message.js'; +import type { RequestCreateUpdate } from './types.js'; + +interface Params { + ephemeralId: string; + spaceId: string; + message: Uint8Array; + secretKey: string; + signaturePrivateKey: string; +} + +export const signedUpdateMessage = ({ + ephemeralId, + spaceId, + message, + secretKey, + signaturePrivateKey, +}: Params): RequestCreateUpdate => { + const update = encryptMessage({ + message, + secretKey: hexToBytes(secretKey), + }); + + const messageToSign = stringToUint8Array( + canonicalize({ + ephemeralId, + update, + spaceId, + }), + ); + + const signature = secp256k1.sign(messageToSign, hexToBytes(signaturePrivateKey), { prehash: true }).toCompactHex(); + + return { + type: 'create-update', + ephemeralId, + update, + spaceId, + signature, + }; +}; diff --git a/packages/hypergraph/src/messages/types.ts b/packages/hypergraph/src/messages/types.ts index c505cc4b..603449bd 100644 --- a/packages/hypergraph/src/messages/types.ts +++ b/packages/hypergraph/src/messages/types.ts @@ -86,6 +86,7 @@ export const RequestCreateUpdate = Schema.Struct({ update: Schema.Uint8Array, spaceId: Schema.String, ephemeralId: Schema.String, // used to identify the confirmation message + signature: Schema.String, }); export const RequestMessage = Schema.Union( From 73d3752f0a4669b9704d824ee99e1652b3b45994 Mon Sep 17 00:00:00 2001 From: Pablo Carranza Velez Date: Tue, 28 Jan 2025 18:36:42 -0300 Subject: [PATCH 02/10] chore: include accountId in signature and message --- packages/hypergraph-react/src/HypergraphAppContext.tsx | 7 ++++++- packages/hypergraph/src/messages/signed-update-message.ts | 4 ++++ packages/hypergraph/src/messages/types.ts | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/hypergraph-react/src/HypergraphAppContext.tsx b/packages/hypergraph-react/src/HypergraphAppContext.tsx index 6e33922f..99736b1f 100644 --- a/packages/hypergraph-react/src/HypergraphAppContext.tsx +++ b/packages/hypergraph-react/src/HypergraphAppContext.tsx @@ -465,6 +465,10 @@ export function HypergraphAppProvider({ // Handle WebSocket messages in a separate effect useEffect(() => { if (!websocketConnection) return; + if (!accountId) { + console.error('No accountId found'); + return; + } const encryptionPrivateKey = keys?.encryptionPrivateKey; if (!encryptionPrivateKey) { console.error('No encryption private key found'); @@ -567,6 +571,7 @@ export function HypergraphAppProvider({ const ephemeralId = uuid(); const messageToSend = Messages.signedUpdateMessage({ + accountId, ephemeralId, spaceId: space.id, message: lastLocalChange, @@ -666,7 +671,7 @@ export function HypergraphAppProvider({ return () => { websocketConnection.removeEventListener('message', onMessage); }; - }, [websocketConnection, spaces, keys?.encryptionPrivateKey, keys?.signaturePrivateKey]); + }, [websocketConnection, spaces, accountId, keys?.encryptionPrivateKey, keys?.signaturePrivateKey]); const createSpaceForContext = async () => { if (!accountId) { diff --git a/packages/hypergraph/src/messages/signed-update-message.ts b/packages/hypergraph/src/messages/signed-update-message.ts index 2a429914..84a332df 100644 --- a/packages/hypergraph/src/messages/signed-update-message.ts +++ b/packages/hypergraph/src/messages/signed-update-message.ts @@ -4,6 +4,7 @@ import { encryptMessage } from './encrypt-message.js'; import type { RequestCreateUpdate } from './types.js'; interface Params { + accountId: string; ephemeralId: string; spaceId: string; message: Uint8Array; @@ -12,6 +13,7 @@ interface Params { } export const signedUpdateMessage = ({ + accountId, ephemeralId, spaceId, message, @@ -25,6 +27,7 @@ export const signedUpdateMessage = ({ const messageToSign = stringToUint8Array( canonicalize({ + accountId, ephemeralId, update, spaceId, @@ -38,6 +41,7 @@ export const signedUpdateMessage = ({ ephemeralId, update, spaceId, + accountId, signature, }; }; diff --git a/packages/hypergraph/src/messages/types.ts b/packages/hypergraph/src/messages/types.ts index 603449bd..dd5376b8 100644 --- a/packages/hypergraph/src/messages/types.ts +++ b/packages/hypergraph/src/messages/types.ts @@ -83,6 +83,7 @@ export type RequestListInvitations = Schema.Schema.Type Date: Tue, 28 Jan 2025 19:31:47 -0300 Subject: [PATCH 03/10] chore: verify signatures when applying updates (wip) --- .../src/HypergraphAppContext.tsx | 42 ++++++++++++++----- .../src/messages/signed-update-message.ts | 39 +++++++++++++++-- packages/hypergraph/src/messages/types.ts | 16 ++++++- 3 files changed, 81 insertions(+), 16 deletions(-) diff --git a/packages/hypergraph-react/src/HypergraphAppContext.tsx b/packages/hypergraph-react/src/HypergraphAppContext.tsx index 99736b1f..a1c12f16 100644 --- a/packages/hypergraph-react/src/HypergraphAppContext.tsx +++ b/packages/hypergraph-react/src/HypergraphAppContext.tsx @@ -4,7 +4,6 @@ import * as automerge from '@automerge/automerge'; import { uuid } from '@automerge/automerge'; import { RepoContext } from '@automerge/automerge-repo-react-hooks'; import { Identity, Key, Messages, SpaceEvents, type SpaceStorageEntry, Utils, store } from '@graphprotocol/hypergraph'; -import { canonicalize } from '@graphprotocol/hypergraph/utils/jsc'; import { useSelector as useSelectorStore } from '@xstate/store/react'; import { Effect, Exit } from 'effect'; import * as Schema from 'effect/Schema'; @@ -536,18 +535,41 @@ export function HypergraphAppProvider({ } if (response.updates) { - const updates = response.updates?.updates.map((update) => { - return Messages.decryptMessage({ - nonceAndCiphertext: update, - secretKey: Utils.hexToBytes(keys[0].key), + const updates = response.updates?.updates.map(async (update) => { + // TODO verify the update signature and that the signing key + // belongs to the reported accountId + const signer = Messages.recoverUpdateMessageSigner({ + update: update.update, + spaceId: response.id, + ephemeralId: update.ephemeralId, + signature: update.signature, + accountId: update.accountId, }); + const authorIdentity = await getUserIdentity(update.accountId); + if (authorIdentity.signaturePublicKey !== signer) { + console.error( + `Received invalid signature, recovered signer is ${signer}, + expected ${authorIdentity.signaturePublicKey}`, + ); + return { valid: false, update: new Uint8Array([]) }; + } + return { + valid: true, + update: Messages.decryptMessage({ + nonceAndCiphertext: update.update, + secretKey: Utils.hexToBytes(keys[0].key), + }), + }; }); - for (const update of updates) { - automergeDocHandle.update((existingDoc) => { - const [newDoc] = automerge.applyChanges(existingDoc, [update]); - return newDoc; - }); + for (const updatePromise of updates) { + const update = await updatePromise; + if (update.valid) { + automergeDocHandle.update((existingDoc) => { + const [newDoc] = automerge.applyChanges(existingDoc, [update.update]); + return newDoc; + }); + } } store.send({ diff --git a/packages/hypergraph/src/messages/signed-update-message.ts b/packages/hypergraph/src/messages/signed-update-message.ts index 84a332df..15b274fb 100644 --- a/packages/hypergraph/src/messages/signed-update-message.ts +++ b/packages/hypergraph/src/messages/signed-update-message.ts @@ -1,9 +1,10 @@ import { secp256k1 } from '@noble/curves/secp256k1'; -import { canonicalize, hexToBytes, stringToUint8Array } from '../utils/index.js'; +import { sha256 } from '@noble/hashes/sha256'; +import { bytesToHex, canonicalize, hexToBytes, stringToUint8Array } from '../utils/index.js'; import { encryptMessage } from './encrypt-message.js'; import type { RequestCreateUpdate } from './types.js'; -interface Params { +interface SignedMessageParams { accountId: string; ephemeralId: string; spaceId: string; @@ -12,6 +13,17 @@ interface Params { signaturePrivateKey: string; } +interface RecoverParams { + update: Uint8Array; + spaceId: string; + ephemeralId: string; + signature: { + hex: string; + recovery: number; + }; + accountId: string; +} + export const signedUpdateMessage = ({ accountId, ephemeralId, @@ -19,7 +31,7 @@ export const signedUpdateMessage = ({ message, secretKey, signaturePrivateKey, -}: Params): RequestCreateUpdate => { +}: SignedMessageParams): RequestCreateUpdate => { const update = encryptMessage({ message, secretKey: hexToBytes(secretKey), @@ -34,7 +46,12 @@ export const signedUpdateMessage = ({ }), ); - const signature = secp256k1.sign(messageToSign, hexToBytes(signaturePrivateKey), { prehash: true }).toCompactHex(); + const recoverySignature = secp256k1.sign(messageToSign, hexToBytes(signaturePrivateKey), { prehash: true }); + + const signature = { + hex: recoverySignature.toCompactHex(), + recovery: recoverySignature.recovery, + }; return { type: 'create-update', @@ -45,3 +62,17 @@ export const signedUpdateMessage = ({ signature, }; }; + +export const recoverUpdateMessageSigner = ({ update, spaceId, ephemeralId, signature, accountId }: RecoverParams) => { + const recoveredSignature = secp256k1.Signature.fromCompact(signature.hex).addRecoveryBit(signature.recovery); + const signedMessage = stringToUint8Array( + canonicalize({ + accountId, + ephemeralId, + update, + spaceId, + }), + ); + const signedMessageHash = sha256(signedMessage); + return bytesToHex(recoveredSignature.recoverPublicKey(signedMessageHash).toRawBytes(true)); +}; diff --git a/packages/hypergraph/src/messages/types.ts b/packages/hypergraph/src/messages/types.ts index dd5376b8..a08c8e8c 100644 --- a/packages/hypergraph/src/messages/types.ts +++ b/packages/hypergraph/src/messages/types.ts @@ -2,8 +2,20 @@ import * as Schema from 'effect/Schema'; import { AcceptInvitationEvent, CreateInvitationEvent, CreateSpaceEvent, SpaceEvent } from '../space-events/index.js'; +export const SignatureWithRecovery = Schema.Struct({ + hex: Schema.String, + recovery: Schema.Number, +}); + +export const SignedUpdate = Schema.Struct({ + update: Schema.Uint8Array, + accountId: Schema.String, + signature: SignatureWithRecovery, + ephemeralId: Schema.String, +}); + export const Updates = Schema.Struct({ - updates: Schema.Array(Schema.Uint8Array), + updates: Schema.Array(SignedUpdate), firstUpdateClock: Schema.Number, lastUpdateClock: Schema.Number, }); @@ -87,7 +99,7 @@ export const RequestCreateUpdate = Schema.Struct({ update: Schema.Uint8Array, spaceId: Schema.String, ephemeralId: Schema.String, // used to identify the confirmation message - signature: Schema.String, + signature: SignatureWithRecovery, }); export const RequestMessage = Schema.Union( From 3eb3539b167be60be11b292ac253f4b1b5e496ed Mon Sep 17 00:00:00 2001 From: Pablo Carranza Velez Date: Wed, 29 Jan 2025 10:47:05 -0300 Subject: [PATCH 04/10] test: add test for sign and recover --- .../messages/signed-update-message.test.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 packages/hypergraph/test/messages/signed-update-message.test.ts diff --git a/packages/hypergraph/test/messages/signed-update-message.test.ts b/packages/hypergraph/test/messages/signed-update-message.test.ts new file mode 100644 index 00000000..9b627115 --- /dev/null +++ b/packages/hypergraph/test/messages/signed-update-message.test.ts @@ -0,0 +1,40 @@ +import { secp256k1 } from '@noble/curves/secp256k1'; +import { randomBytes } from '@noble/hashes/utils'; +import { describe, expect, it } from 'vitest'; +import { recoverUpdateMessageSigner, signedUpdateMessage } from '../../src/messages/index.js'; +import { bytesToHex, hexToBytes } from '../../src/utils/index.js'; + +describe('sign updates and recover key', () => { + it('creates a signed message from which you can recover a signing key', () => { + const accountId = bytesToHex(randomBytes(20)); + const secretKey = bytesToHex(new Uint8Array(32).fill(1)); + const signaturePrivateKeyBytes = secp256k1.utils.randomPrivateKey(); + const signaturePrivateKey = bytesToHex(signaturePrivateKeyBytes); + const signaturePublicKey = bytesToHex(secp256k1.getPublicKey(signaturePrivateKeyBytes)); + const spaceId = '0x1234'; + const ephemeralId = bytesToHex(randomBytes(32)); + + const message = hexToBytes('0x01234abcdef01234'); + + const msg = signedUpdateMessage({ + accountId, + ephemeralId, + spaceId, + message, + secretKey, + signaturePrivateKey, + }); + + // The signer should be recoverable without needing anything + // outside of what's included in the message + const recoveredSigner = recoverUpdateMessageSigner({ + update: msg.update, + spaceId: msg.spaceId, + ephemeralId: msg.ephemeralId, + signature: msg.signature, + accountId: msg.accountId, + }); + + expect(recoveredSigner).to.eq(signaturePublicKey); + }); +}); From 69f901040ba15c42fcca5783b3fafebf98b95544 Mon Sep 17 00:00:00 2001 From: Pablo Carranza Velez Date: Wed, 29 Jan 2025 10:54:02 -0300 Subject: [PATCH 05/10] chore: allow passing the request msg to recover signer --- packages/hypergraph/src/messages/signed-update-message.ts | 8 +++++++- .../test/messages/signed-update-message.test.ts | 8 +------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/hypergraph/src/messages/signed-update-message.ts b/packages/hypergraph/src/messages/signed-update-message.ts index 15b274fb..f27820fe 100644 --- a/packages/hypergraph/src/messages/signed-update-message.ts +++ b/packages/hypergraph/src/messages/signed-update-message.ts @@ -63,7 +63,13 @@ export const signedUpdateMessage = ({ }; }; -export const recoverUpdateMessageSigner = ({ update, spaceId, ephemeralId, signature, accountId }: RecoverParams) => { +export const recoverUpdateMessageSigner = ({ + update, + spaceId, + ephemeralId, + signature, + accountId, +}: RecoverParams | RequestCreateUpdate) => { const recoveredSignature = secp256k1.Signature.fromCompact(signature.hex).addRecoveryBit(signature.recovery); const signedMessage = stringToUint8Array( canonicalize({ diff --git a/packages/hypergraph/test/messages/signed-update-message.test.ts b/packages/hypergraph/test/messages/signed-update-message.test.ts index 9b627115..45f53b43 100644 --- a/packages/hypergraph/test/messages/signed-update-message.test.ts +++ b/packages/hypergraph/test/messages/signed-update-message.test.ts @@ -27,13 +27,7 @@ describe('sign updates and recover key', () => { // The signer should be recoverable without needing anything // outside of what's included in the message - const recoveredSigner = recoverUpdateMessageSigner({ - update: msg.update, - spaceId: msg.spaceId, - ephemeralId: msg.ephemeralId, - signature: msg.signature, - accountId: msg.accountId, - }); + const recoveredSigner = recoverUpdateMessageSigner(msg); expect(recoveredSigner).to.eq(signaturePublicKey); }); From fd4a555430707f3fdfd50ed41f71cf170c03feae Mon Sep 17 00:00:00 2001 From: Pablo Carranza Velez Date: Wed, 29 Jan 2025 15:17:19 -0300 Subject: [PATCH 06/10] fix: use the existing SignatureWithRecovery type --- packages/hypergraph/src/messages/types.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/hypergraph/src/messages/types.ts b/packages/hypergraph/src/messages/types.ts index a08c8e8c..de6d81c8 100644 --- a/packages/hypergraph/src/messages/types.ts +++ b/packages/hypergraph/src/messages/types.ts @@ -1,11 +1,7 @@ import * as Schema from 'effect/Schema'; import { AcceptInvitationEvent, CreateInvitationEvent, CreateSpaceEvent, SpaceEvent } from '../space-events/index.js'; - -export const SignatureWithRecovery = Schema.Struct({ - hex: Schema.String, - recovery: Schema.Number, -}); +import { SignatureWithRecovery } from '../types.js'; export const SignedUpdate = Schema.Struct({ update: Schema.Uint8Array, From 25b94a359fe2eb9a9687e19357d2dc4e10b46d27 Mon Sep 17 00:00:00 2001 From: Pablo Carranza Velez Date: Wed, 29 Jan 2025 19:19:28 -0300 Subject: [PATCH 07/10] chore: verify updates on server and client --- .../migration.sql | 30 +++++ .../prisma/migrations/migration_lock.toml | 2 +- apps/server/prisma/schema.prisma | 6 + apps/server/src/handlers/createUpdate.ts | 17 ++- apps/server/src/handlers/getSpace.ts | 14 ++- apps/server/src/index.ts | 60 ++++++--- .../src/HypergraphAppContext.tsx | 117 +++++++++--------- 7 files changed, 164 insertions(+), 82 deletions(-) create mode 100644 apps/server/prisma/migrations/20250129220359_add_update_signature/migration.sql diff --git a/apps/server/prisma/migrations/20250129220359_add_update_signature/migration.sql b/apps/server/prisma/migrations/20250129220359_add_update_signature/migration.sql new file mode 100644 index 00000000..d9d9daa8 --- /dev/null +++ b/apps/server/prisma/migrations/20250129220359_add_update_signature/migration.sql @@ -0,0 +1,30 @@ +/* + Warnings: + + - Added the required column `accountId` to the `Update` table without a default value. This is not possible if the table is not empty. + - Added the required column `ephemeralId` to the `Update` table without a default value. This is not possible if the table is not empty. + - Added the required column `signatureHex` to the `Update` table without a default value. This is not possible if the table is not empty. + - Added the required column `signatureRecovery` to the `Update` table without a default value. This is not possible if the table is not empty. + +*/ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Update" ( + "spaceId" TEXT NOT NULL, + "clock" INTEGER NOT NULL, + "content" BLOB NOT NULL, + "accountId" TEXT NOT NULL, + "signatureHex" TEXT NOT NULL, + "signatureRecovery" INTEGER NOT NULL, + "ephemeralId" TEXT NOT NULL, + + PRIMARY KEY ("spaceId", "clock"), + CONSTRAINT "Update_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "Space" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Update_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_Update" ("clock", "content", "spaceId") SELECT "clock", "content", "spaceId" FROM "Update"; +DROP TABLE "Update"; +ALTER TABLE "new_Update" RENAME TO "Update"; +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/apps/server/prisma/migrations/migration_lock.toml b/apps/server/prisma/migrations/migration_lock.toml index e5e5c470..e1640d1f 100644 --- a/apps/server/prisma/migrations/migration_lock.toml +++ b/apps/server/prisma/migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) +# It should be added in your version-control system (e.g., Git) provider = "sqlite" \ No newline at end of file diff --git a/apps/server/prisma/schema.prisma b/apps/server/prisma/schema.prisma index 5b252fdc..ce478fcf 100644 --- a/apps/server/prisma/schema.prisma +++ b/apps/server/prisma/schema.prisma @@ -62,6 +62,7 @@ model Account { sessionNonce String? sessionToken String? sessionTokenExpires DateTime? + updates Update[] @@index([sessionToken]) } @@ -83,6 +84,11 @@ model Update { spaceId String clock Int content Bytes + account Account @relation(fields: [accountId], references: [id]) + accountId String + signatureHex String + signatureRecovery Int + ephemeralId String @@id([spaceId, clock]) } diff --git a/apps/server/src/handlers/createUpdate.ts b/apps/server/src/handlers/createUpdate.ts index 8ee60a4f..b5e07d4e 100644 --- a/apps/server/src/handlers/createUpdate.ts +++ b/apps/server/src/handlers/createUpdate.ts @@ -4,9 +4,19 @@ type Params = { accountId: string; update: Uint8Array; spaceId: string; + signatureHex: string; + signatureRecovery: number; + ephemeralId: string; }; -export const createUpdate = async ({ accountId, update, spaceId }: Params) => { +export const createUpdate = async ({ + accountId, + update, + spaceId, + signatureHex, + signatureRecovery, + ephemeralId, +}: Params) => { // throw error if account is not a member of the space await prisma.space.findUniqueOrThrow({ where: { id: spaceId, members: { some: { id: accountId } } }, @@ -39,11 +49,14 @@ export const createUpdate = async ({ accountId, update, spaceId }: Params) => { spaceId, clock, content: Buffer.from(update), + signatureHex, + signatureRecovery, + ephemeralId, + account: { connect: { id: accountId } }, }, }); }); success = true; - return result; } catch (error) { const dbError = error as { code?: string; message?: string }; if (dbError.code === 'P2034' || dbError.code === 'P1008' || dbError.message?.includes('database is locked')) { diff --git a/apps/server/src/handlers/getSpace.ts b/apps/server/src/handlers/getSpace.ts index 6c1d8a72..37ff7121 100644 --- a/apps/server/src/handlers/getSpace.ts +++ b/apps/server/src/handlers/getSpace.ts @@ -53,6 +53,18 @@ export const getSpace = async ({ spaceId, accountId }: Params) => { }; }); + const formatUpdate = (update) => { + return { + accountId: update.accountId, + update: new Uint8Array(update.content), + signature: { + hex: update.signatureHex, + recovery: update.signatureRecovery, + }, + ephemeralId: update.ephemeralId, + }; + }; + return { id: space.id, events: space.events.map((wrapper) => JSON.parse(wrapper.event)), @@ -60,7 +72,7 @@ export const getSpace = async ({ spaceId, accountId }: Params) => { updates: space.updates.length > 0 ? { - updates: space.updates.map((update) => new Uint8Array(update.content)), + updates: space.updates.map(formatUpdate), firstUpdateClock: space.updates[0].clock, lastUpdateClock: space.updates[space.updates.length - 1].clock, } diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index f65acca5..ae48268c 100755 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -1,5 +1,6 @@ import { parse } from 'node:url'; import { Identity, Messages, SpaceEvents, Utils } from '@graphprotocol/hypergraph'; +import { recoverUpdateMessageSigner } from '@graphprotocol/hypergraph/messages/signed-update-message'; import cors from 'cors'; import { Effect, Exit, Schema } from 'effect'; import express, { type Request, type Response } from 'express'; @@ -393,24 +394,49 @@ webSocketServer.on('connection', async (webSocket: CustomWebSocket, request: Req break; } case 'create-update': { - const update = await createUpdate({ accountId, spaceId: data.spaceId, update: data.update }); - const outgoingMessage: Messages.ResponseUpdateConfirmed = { - type: 'update-confirmed', - ephemeralId: data.ephemeralId, - clock: update.clock, - spaceId: data.spaceId, - }; - webSocket.send(Messages.serialize(outgoingMessage)); + try { + // Check that the update was signed by a valid identity + // belonging to this accountId + const signer = recoverUpdateMessageSigner(data); + const identity = await getIdentity({ signaturePublicKey: signer }); + if (identity.accountId !== accountId) { + throw new Error('Invalid signature'); + } + const update = await createUpdate({ + accountId, + spaceId: data.spaceId, + update: data.update, + signatureHex: data.signature.hex, + signatureRecovery: data.signature.recovery, + ephemeralId: data.ephemeralId, + }); + const outgoingMessage: Messages.ResponseUpdateConfirmed = { + type: 'update-confirmed', + ephemeralId: data.ephemeralId, + clock: update.clock, + spaceId: data.spaceId, + }; + webSocket.send(Messages.serialize(outgoingMessage)); - broadcastUpdates({ - spaceId: data.spaceId, - updates: { - updates: [new Uint8Array(update.content)], - firstUpdateClock: update.clock, - lastUpdateClock: update.clock, - }, - currentClient: webSocket, - }); + broadcastUpdates({ + spaceId: data.spaceId, + updates: { + updates: [ + { + accountId, + update: data.update, + signature: data.signature, + ephemeralId: data.ephemeralId, + }, + ], + firstUpdateClock: update.clock, + lastUpdateClock: update.clock, + }, + currentClient: webSocket, + }); + } catch (err) { + console.error('Error creating update:', err); + } break; } default: diff --git a/packages/hypergraph-react/src/HypergraphAppContext.tsx b/packages/hypergraph-react/src/HypergraphAppContext.tsx index a1c12f16..7353016e 100644 --- a/packages/hypergraph-react/src/HypergraphAppContext.tsx +++ b/packages/hypergraph-react/src/HypergraphAppContext.tsx @@ -2,6 +2,7 @@ import * as automerge from '@automerge/automerge'; import { uuid } from '@automerge/automerge'; +import { DocHandle } from '@automerge/automerge-repo'; import { RepoContext } from '@automerge/automerge-repo-react-hooks'; import { Identity, Key, Messages, SpaceEvents, type SpaceStorageEntry, Utils, store } from '@graphprotocol/hypergraph'; import { useSelector as useSelectorStore } from '@xstate/store/react'; @@ -479,6 +480,55 @@ export function HypergraphAppProvider({ return; } + const applyUpdates = async ( + spaceId: string, + spaceSecretKey: string, + automergeDocHandle: DocHandle, + updates: Messages.Updates, + ) => { + const verifiedUpdates = updates.updates.map(async (update) => { + const signer = Messages.recoverUpdateMessageSigner({ + update: update.update, + spaceId, + ephemeralId: update.ephemeralId, + signature: update.signature, + accountId: update.accountId, + }); + const authorIdentity = await getUserIdentity(update.accountId); + if (authorIdentity.signaturePublicKey !== signer) { + console.error( + `Received invalid signature, recovered signer is ${signer}, + expected ${authorIdentity.signaturePublicKey}`, + ); + return { valid: false, update: new Uint8Array([]) }; + } + return { + valid: true, + update: Messages.decryptMessage({ + nonceAndCiphertext: update.update, + secretKey: Utils.hexToBytes(spaceSecretKey), + }), + }; + }); + + for (const updatePromise of verifiedUpdates) { + const update = await updatePromise; + if (update.valid) { + automergeDocHandle.update((existingDoc) => { + const [newDoc] = automerge.applyChanges(existingDoc, [update.update]); + return newDoc; + }); + } + } + + store.send({ + type: 'applyUpdate', + spaceId, + firstUpdateClock: updates.firstUpdateClock, + lastUpdateClock: updates.lastUpdateClock, + }); + }; + const onMessage = async (event: MessageEvent) => { const data = Messages.deserialize(event.data); const message = decodeResponseMessage(data); @@ -535,49 +585,7 @@ export function HypergraphAppProvider({ } if (response.updates) { - const updates = response.updates?.updates.map(async (update) => { - // TODO verify the update signature and that the signing key - // belongs to the reported accountId - const signer = Messages.recoverUpdateMessageSigner({ - update: update.update, - spaceId: response.id, - ephemeralId: update.ephemeralId, - signature: update.signature, - accountId: update.accountId, - }); - const authorIdentity = await getUserIdentity(update.accountId); - if (authorIdentity.signaturePublicKey !== signer) { - console.error( - `Received invalid signature, recovered signer is ${signer}, - expected ${authorIdentity.signaturePublicKey}`, - ); - return { valid: false, update: new Uint8Array([]) }; - } - return { - valid: true, - update: Messages.decryptMessage({ - nonceAndCiphertext: update.update, - secretKey: Utils.hexToBytes(keys[0].key), - }), - }; - }); - - for (const updatePromise of updates) { - const update = await updatePromise; - if (update.valid) { - automergeDocHandle.update((existingDoc) => { - const [newDoc] = automerge.applyChanges(existingDoc, [update.update]); - return newDoc; - }); - } - } - - store.send({ - type: 'applyUpdate', - spaceId: response.id, - firstUpdateClock: response.updates?.firstUpdateClock, - lastUpdateClock: response.updates?.lastUpdateClock, - }); + await applyUpdates(response.id, keys[0].key, automergeDocHandle, response.updates); } automergeDocHandle.on('change', (result) => { @@ -660,25 +668,12 @@ export function HypergraphAppProvider({ console.error('Space not found', response.spaceId); return; } + if (!space.automergeDocHandle) { + console.error('No automergeDocHandle found', response.spaceId); + return; + } - const automergeUpdates = response.updates.updates.map((update) => { - return Messages.decryptMessage({ - nonceAndCiphertext: update, - secretKey: Utils.hexToBytes(space.keys[0].key), - }); - }); - - space?.automergeDocHandle?.update((existingDoc) => { - const [newDoc] = automerge.applyChanges(existingDoc, automergeUpdates); - return newDoc; - }); - - store.send({ - type: 'applyUpdate', - spaceId: response.spaceId, - firstUpdateClock: response.updates.firstUpdateClock, - lastUpdateClock: response.updates.lastUpdateClock, - }); + await applyUpdates(response.spaceId, space.keys[0].key, space.automergeDocHandle, response.updates); break; } default: { From 5a99a8d8e54ceaf8a678ca3fd61df69ee31e3fc9 Mon Sep 17 00:00:00 2001 From: Pablo Carranza Velez Date: Wed, 29 Jan 2025 19:20:38 -0300 Subject: [PATCH 08/10] fix: lint --- packages/hypergraph-react/src/HypergraphAppContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/hypergraph-react/src/HypergraphAppContext.tsx b/packages/hypergraph-react/src/HypergraphAppContext.tsx index 7353016e..17d487ba 100644 --- a/packages/hypergraph-react/src/HypergraphAppContext.tsx +++ b/packages/hypergraph-react/src/HypergraphAppContext.tsx @@ -2,7 +2,7 @@ import * as automerge from '@automerge/automerge'; import { uuid } from '@automerge/automerge'; -import { DocHandle } from '@automerge/automerge-repo'; +import type { DocHandle } from '@automerge/automerge-repo'; import { RepoContext } from '@automerge/automerge-repo-react-hooks'; import { Identity, Key, Messages, SpaceEvents, type SpaceStorageEntry, Utils, store } from '@graphprotocol/hypergraph'; import { useSelector as useSelectorStore } from '@xstate/store/react'; From 565be56d4a289678bb8148a2f1cede5adf43f7ca Mon Sep 17 00:00:00 2001 From: Pablo Carranza Velez Date: Thu, 30 Jan 2025 12:09:14 -0300 Subject: [PATCH 09/10] fix: apply review feedback --- .../migration.sql | 4 +- apps/server/prisma/schema.prisma | 2 +- apps/server/src/handlers/createUpdate.ts | 6 +- apps/server/src/handlers/getSpace.ts | 2 +- apps/server/src/index.ts | 9 ++- .../src/HypergraphAppContext.tsx | 67 +++++++++---------- .../src/messages/signed-update-message.ts | 14 ++-- packages/hypergraph/src/messages/types.ts | 6 +- packages/hypergraph/src/store.ts | 12 ++-- .../messages/signed-update-message.test.ts | 4 +- 10 files changed, 61 insertions(+), 65 deletions(-) diff --git a/apps/server/prisma/migrations/20250129220359_add_update_signature/migration.sql b/apps/server/prisma/migrations/20250129220359_add_update_signature/migration.sql index d9d9daa8..9342d7e1 100644 --- a/apps/server/prisma/migrations/20250129220359_add_update_signature/migration.sql +++ b/apps/server/prisma/migrations/20250129220359_add_update_signature/migration.sql @@ -2,7 +2,7 @@ Warnings: - Added the required column `accountId` to the `Update` table without a default value. This is not possible if the table is not empty. - - Added the required column `ephemeralId` to the `Update` table without a default value. This is not possible if the table is not empty. + - Added the required column `updateId` to the `Update` table without a default value. This is not possible if the table is not empty. - Added the required column `signatureHex` to the `Update` table without a default value. This is not possible if the table is not empty. - Added the required column `signatureRecovery` to the `Update` table without a default value. This is not possible if the table is not empty. @@ -17,7 +17,7 @@ CREATE TABLE "new_Update" ( "accountId" TEXT NOT NULL, "signatureHex" TEXT NOT NULL, "signatureRecovery" INTEGER NOT NULL, - "ephemeralId" TEXT NOT NULL, + "updateId" TEXT NOT NULL, PRIMARY KEY ("spaceId", "clock"), CONSTRAINT "Update_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "Space" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, diff --git a/apps/server/prisma/schema.prisma b/apps/server/prisma/schema.prisma index ce478fcf..1536ebfa 100644 --- a/apps/server/prisma/schema.prisma +++ b/apps/server/prisma/schema.prisma @@ -88,7 +88,7 @@ model Update { accountId String signatureHex String signatureRecovery Int - ephemeralId String + updateId String @@id([spaceId, clock]) } diff --git a/apps/server/src/handlers/createUpdate.ts b/apps/server/src/handlers/createUpdate.ts index b5e07d4e..f6786493 100644 --- a/apps/server/src/handlers/createUpdate.ts +++ b/apps/server/src/handlers/createUpdate.ts @@ -6,7 +6,7 @@ type Params = { spaceId: string; signatureHex: string; signatureRecovery: number; - ephemeralId: string; + updateId: string; }; export const createUpdate = async ({ @@ -15,7 +15,7 @@ export const createUpdate = async ({ spaceId, signatureHex, signatureRecovery, - ephemeralId, + updateId, }: Params) => { // throw error if account is not a member of the space await prisma.space.findUniqueOrThrow({ @@ -51,7 +51,7 @@ export const createUpdate = async ({ content: Buffer.from(update), signatureHex, signatureRecovery, - ephemeralId, + updateId, account: { connect: { id: accountId } }, }, }); diff --git a/apps/server/src/handlers/getSpace.ts b/apps/server/src/handlers/getSpace.ts index 37ff7121..d570d3ce 100644 --- a/apps/server/src/handlers/getSpace.ts +++ b/apps/server/src/handlers/getSpace.ts @@ -61,7 +61,7 @@ export const getSpace = async ({ spaceId, accountId }: Params) => { hex: update.signatureHex, recovery: update.signatureRecovery, }, - ephemeralId: update.ephemeralId, + updateId: update.updateId, }; }; diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index ae48268c..07347409 100755 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -1,6 +1,5 @@ import { parse } from 'node:url'; import { Identity, Messages, SpaceEvents, Utils } from '@graphprotocol/hypergraph'; -import { recoverUpdateMessageSigner } from '@graphprotocol/hypergraph/messages/signed-update-message'; import cors from 'cors'; import { Effect, Exit, Schema } from 'effect'; import express, { type Request, type Response } from 'express'; @@ -397,7 +396,7 @@ webSocketServer.on('connection', async (webSocket: CustomWebSocket, request: Req try { // Check that the update was signed by a valid identity // belonging to this accountId - const signer = recoverUpdateMessageSigner(data); + const signer = Messages.recoverUpdateMessageSigner(data); const identity = await getIdentity({ signaturePublicKey: signer }); if (identity.accountId !== accountId) { throw new Error('Invalid signature'); @@ -408,11 +407,11 @@ webSocketServer.on('connection', async (webSocket: CustomWebSocket, request: Req update: data.update, signatureHex: data.signature.hex, signatureRecovery: data.signature.recovery, - ephemeralId: data.ephemeralId, + updateId: data.updateId, }); const outgoingMessage: Messages.ResponseUpdateConfirmed = { type: 'update-confirmed', - ephemeralId: data.ephemeralId, + updateId: data.updateId, clock: update.clock, spaceId: data.spaceId, }; @@ -426,7 +425,7 @@ webSocketServer.on('connection', async (webSocket: CustomWebSocket, request: Req accountId, update: data.update, signature: data.signature, - ephemeralId: data.ephemeralId, + updateId: data.updateId, }, ], firstUpdateClock: update.clock, diff --git a/packages/hypergraph-react/src/HypergraphAppContext.tsx b/packages/hypergraph-react/src/HypergraphAppContext.tsx index 17d487ba..974b449d 100644 --- a/packages/hypergraph-react/src/HypergraphAppContext.tsx +++ b/packages/hypergraph-react/src/HypergraphAppContext.tsx @@ -486,41 +486,38 @@ export function HypergraphAppProvider({ automergeDocHandle: DocHandle, updates: Messages.Updates, ) => { - const verifiedUpdates = updates.updates.map(async (update) => { - const signer = Messages.recoverUpdateMessageSigner({ - update: update.update, - spaceId, - ephemeralId: update.ephemeralId, - signature: update.signature, - accountId: update.accountId, - }); - const authorIdentity = await getUserIdentity(update.accountId); - if (authorIdentity.signaturePublicKey !== signer) { - console.error( - `Received invalid signature, recovered signer is ${signer}, + const verifiedUpdates = await Promise.all( + updates.updates.map(async (update) => { + const signer = Messages.recoverUpdateMessageSigner({ + update: update.update, + spaceId, + updateId: update.updateId, + signature: update.signature, + accountId: update.accountId, + }); + const authorIdentity = await getUserIdentity(update.accountId); + if (authorIdentity.signaturePublicKey !== signer) { + console.error( + `Received invalid signature, recovered signer is ${signer}, expected ${authorIdentity.signaturePublicKey}`, - ); - return { valid: false, update: new Uint8Array([]) }; - } - return { - valid: true, - update: Messages.decryptMessage({ - nonceAndCiphertext: update.update, - secretKey: Utils.hexToBytes(spaceSecretKey), - }), - }; + ); + return { valid: false, update: new Uint8Array([]) }; + } + return { + valid: true, + update: Messages.decryptMessage({ + nonceAndCiphertext: update.update, + secretKey: Utils.hexToBytes(spaceSecretKey), + }), + }; + }), + ); + const validUpdates = verifiedUpdates.filter((update) => update.valid).map((update) => update.update); + automergeDocHandle.update((existingDoc) => { + const [newDoc] = automerge.applyChanges(existingDoc, validUpdates); + return newDoc; }); - for (const updatePromise of verifiedUpdates) { - const update = await updatePromise; - if (update.valid) { - automergeDocHandle.update((existingDoc) => { - const [newDoc] = automerge.applyChanges(existingDoc, [update.update]); - return newDoc; - }); - } - } - store.send({ type: 'applyUpdate', spaceId, @@ -598,11 +595,11 @@ export function HypergraphAppProvider({ const storeState = store.getSnapshot(); const space = storeState.context.spaces[0]; - const ephemeralId = uuid(); + const updateId = uuid(); const messageToSend = Messages.signedUpdateMessage({ accountId, - ephemeralId, + updateId, spaceId: space.id, message: lastLocalChange, secretKey: space.keys[0].key, @@ -651,7 +648,7 @@ export function HypergraphAppProvider({ case 'update-confirmed': { store.send({ type: 'removeUpdateInFlight', - ephemeralId: response.ephemeralId, + updateId: response.updateId, }); store.send({ type: 'updateConfirmed', diff --git a/packages/hypergraph/src/messages/signed-update-message.ts b/packages/hypergraph/src/messages/signed-update-message.ts index f27820fe..ffbf7b4c 100644 --- a/packages/hypergraph/src/messages/signed-update-message.ts +++ b/packages/hypergraph/src/messages/signed-update-message.ts @@ -6,7 +6,7 @@ import type { RequestCreateUpdate } from './types.js'; interface SignedMessageParams { accountId: string; - ephemeralId: string; + updateId: string; spaceId: string; message: Uint8Array; secretKey: string; @@ -16,7 +16,7 @@ interface SignedMessageParams { interface RecoverParams { update: Uint8Array; spaceId: string; - ephemeralId: string; + updateId: string; signature: { hex: string; recovery: number; @@ -26,7 +26,7 @@ interface RecoverParams { export const signedUpdateMessage = ({ accountId, - ephemeralId, + updateId, spaceId, message, secretKey, @@ -40,7 +40,7 @@ export const signedUpdateMessage = ({ const messageToSign = stringToUint8Array( canonicalize({ accountId, - ephemeralId, + updateId, update, spaceId, }), @@ -55,7 +55,7 @@ export const signedUpdateMessage = ({ return { type: 'create-update', - ephemeralId, + updateId, update, spaceId, accountId, @@ -66,7 +66,7 @@ export const signedUpdateMessage = ({ export const recoverUpdateMessageSigner = ({ update, spaceId, - ephemeralId, + updateId, signature, accountId, }: RecoverParams | RequestCreateUpdate) => { @@ -74,7 +74,7 @@ export const recoverUpdateMessageSigner = ({ const signedMessage = stringToUint8Array( canonicalize({ accountId, - ephemeralId, + updateId, update, spaceId, }), diff --git a/packages/hypergraph/src/messages/types.ts b/packages/hypergraph/src/messages/types.ts index de6d81c8..406418b5 100644 --- a/packages/hypergraph/src/messages/types.ts +++ b/packages/hypergraph/src/messages/types.ts @@ -7,7 +7,7 @@ export const SignedUpdate = Schema.Struct({ update: Schema.Uint8Array, accountId: Schema.String, signature: SignatureWithRecovery, - ephemeralId: Schema.String, + updateId: Schema.String, }); export const Updates = Schema.Struct({ @@ -94,7 +94,7 @@ export const RequestCreateUpdate = Schema.Struct({ accountId: Schema.String, update: Schema.Uint8Array, spaceId: Schema.String, - ephemeralId: Schema.String, // used to identify the confirmation message + updateId: Schema.String, // used to identify the confirmation message signature: SignatureWithRecovery, }); @@ -193,7 +193,7 @@ export type ResponseSpace = Schema.Schema.Type; export const ResponseUpdateConfirmed = Schema.Struct({ type: Schema.Literal('update-confirmed'), - ephemeralId: Schema.String, + updateId: Schema.String, clock: Schema.Number, spaceId: Schema.String, }); diff --git a/packages/hypergraph/src/store.ts b/packages/hypergraph/src/store.ts index a46ceeef..9f2b44d0 100644 --- a/packages/hypergraph/src/store.ts +++ b/packages/hypergraph/src/store.ts @@ -50,8 +50,8 @@ const initialStoreContext: StoreContext = { type StoreEvent = | { type: 'setInvitations'; invitations: Invitation[] } | { type: 'reset' } - | { type: 'addUpdateInFlight'; ephemeralId: string } - | { type: 'removeUpdateInFlight'; ephemeralId: string } + | { type: 'addUpdateInFlight'; updateId: string } + | { type: 'removeUpdateInFlight'; updateId: string } | { type: 'setSpaceFromList'; spaceId: string } | { type: 'applyEvent'; spaceId: string; event: SpaceEvent; state: SpaceState } | { type: 'updateConfirmed'; spaceId: string; clock: number } @@ -99,16 +99,16 @@ export const store: Store = create reset: () => { return initialStoreContext; }, - addUpdateInFlight: (context, event: { ephemeralId: string }) => { + addUpdateInFlight: (context, event: { updateId: string }) => { return { ...context, - updatesInFlight: [...context.updatesInFlight, event.ephemeralId], + updatesInFlight: [...context.updatesInFlight, event.updateId], }; }, - removeUpdateInFlight: (context, event: { ephemeralId: string }) => { + removeUpdateInFlight: (context, event: { updateId: string }) => { return { ...context, - updatesInFlight: context.updatesInFlight.filter((id) => id !== event.ephemeralId), + updatesInFlight: context.updatesInFlight.filter((id) => id !== event.updateId), }; }, setSpaceFromList: (context, event: { spaceId: string }) => { diff --git a/packages/hypergraph/test/messages/signed-update-message.test.ts b/packages/hypergraph/test/messages/signed-update-message.test.ts index 45f53b43..5e3df735 100644 --- a/packages/hypergraph/test/messages/signed-update-message.test.ts +++ b/packages/hypergraph/test/messages/signed-update-message.test.ts @@ -12,13 +12,13 @@ describe('sign updates and recover key', () => { const signaturePrivateKey = bytesToHex(signaturePrivateKeyBytes); const signaturePublicKey = bytesToHex(secp256k1.getPublicKey(signaturePrivateKeyBytes)); const spaceId = '0x1234'; - const ephemeralId = bytesToHex(randomBytes(32)); + const updateId = bytesToHex(randomBytes(32)); const message = hexToBytes('0x01234abcdef01234'); const msg = signedUpdateMessage({ accountId, - ephemeralId, + updateId, spaceId, message, secretKey, From 80ad4823aa0ceca62ccd902db7fbd8bd5156fd45 Mon Sep 17 00:00:00 2001 From: Pablo Carranza Velez Date: Thu, 30 Jan 2025 17:13:02 -0300 Subject: [PATCH 10/10] fix: connect spaceId correctly --- apps/server/src/handlers/createUpdate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/handlers/createUpdate.ts b/apps/server/src/handlers/createUpdate.ts index f6786493..bdf11b6b 100644 --- a/apps/server/src/handlers/createUpdate.ts +++ b/apps/server/src/handlers/createUpdate.ts @@ -46,7 +46,7 @@ export const createUpdate = async ({ return await prisma.update.create({ data: { - spaceId, + space: { connect: { id: spaceId } }, clock, content: Buffer.from(update), signatureHex,