diff --git a/apps/connect/src/routes/authenticate.tsx b/apps/connect/src/routes/authenticate.tsx index 7c783e16..843196b2 100644 --- a/apps/connect/src/routes/authenticate.tsx +++ b/apps/connect/src/routes/authenticate.tsx @@ -261,7 +261,7 @@ function AuthenticateComponent() { id: keyData.id, ciphertext: Utils.bytesToHex(keyBox.keyBoxCiphertext), nonce: Utils.bytesToHex(keyBox.keyBoxNonce), - authorPublicKey: appIdentity.encryptionPublicKey, + authorPublicKey: keys.encryptionPublicKey, accountAddress: accountAddress, }; }); @@ -388,19 +388,25 @@ function AuthenticateComponent() { rpcUrl: import.meta.env.VITE_HYPERGRAPH_RPC_URL, }); + const appIdentityKeys = { + encryptionPrivateKey: newAppIdentity.encryptionPrivateKey, + encryptionPublicKey: newAppIdentity.encryptionPublicKey, + signaturePrivateKey: newAppIdentity.signaturePrivateKey, + signaturePublicKey: newAppIdentity.signaturePublicKey, + }; console.log('encrypting app identity'); const { ciphertext, nonce } = await Connect.encryptAppIdentity( signer, newAppIdentity.address, newAppIdentity.addressPrivateKey, permissionId, - keys, + appIdentityKeys, ); console.log('proving ownership'); const { accountProof, keyProof } = await Identity.proveIdentityOwnership( smartAccountClient, accountAddress, - keys, + appIdentityKeys, ); const message: Messages.RequestConnectCreateAppIdentity = { @@ -432,10 +438,10 @@ function AuthenticateComponent() { address: newAppIdentity.address, addressPrivateKey: newAppIdentity.addressPrivateKey, accountAddress, - encryptionPrivateKey: keys.encryptionPrivateKey, - signaturePrivateKey: keys.signaturePrivateKey, - encryptionPublicKey: keys.encryptionPublicKey, - signaturePublicKey: keys.signaturePublicKey, + encryptionPrivateKey: newAppIdentity.encryptionPrivateKey, + signaturePrivateKey: newAppIdentity.signaturePrivateKey, + encryptionPublicKey: newAppIdentity.encryptionPublicKey, + signaturePublicKey: newAppIdentity.signaturePublicKey, sessionToken: appIdentityResponse.appIdentity.sessionToken, sessionTokenExpires: new Date(appIdentityResponse.appIdentity.sessionTokenExpires), permissionId, diff --git a/apps/events/src/Boot.tsx b/apps/events/src/Boot.tsx index 3cfd05e5..16f06508 100644 --- a/apps/events/src/Boot.tsx +++ b/apps/events/src/Boot.tsx @@ -15,7 +15,11 @@ declare module '@tanstack/react-router' { export function Boot() { return ( - + ); diff --git a/apps/events/src/routes/login.lazy.tsx b/apps/events/src/routes/login.lazy.tsx index 98091bee..589f77dc 100644 --- a/apps/events/src/routes/login.lazy.tsx +++ b/apps/events/src/routes/login.lazy.tsx @@ -16,7 +16,6 @@ function Login() { storage: localStorage, connectUrl: 'http://localhost:5180', successUrl: `${window.location.origin}/authenticate-success`, - appId: '93bb8907-085a-4a0e-83dd-62b0dc98e793', redirectFn: (url: URL) => { window.location.href = url.toString(); }, diff --git a/apps/next-example/src/components/providers.tsx b/apps/next-example/src/components/providers.tsx index 332613b7..d632d7d6 100644 --- a/apps/next-example/src/components/providers.tsx +++ b/apps/next-example/src/components/providers.tsx @@ -7,7 +7,11 @@ export default function Providers({ children }: { children: React.ReactNode }) { const storage = typeof window !== 'undefined' ? window.localStorage : (undefined as unknown as Storage); return ( - + {children} ); diff --git a/apps/server/src/handlers/applySpaceEvent.ts b/apps/server/src/handlers/applySpaceEvent.ts index 956fe317..f8800528 100644 --- a/apps/server/src/handlers/applySpaceEvent.ts +++ b/apps/server/src/handlers/applySpaceEvent.ts @@ -4,7 +4,7 @@ import type { Messages } from '@graphprotocol/hypergraph'; import { Identity, SpaceEvents } from '@graphprotocol/hypergraph'; import { prisma } from '../prisma.js'; -import { getConnectIdentity } from './getConnectIdentity.js'; +import { getAppOrConnectIdentity } from './getAppOrConnectIdentity.js'; type Params = { accountAddress: string; @@ -40,7 +40,7 @@ export async function applySpaceEvent({ accountAddress, spaceId, event, keyBoxes orderBy: { counter: 'desc' }, }); - const getVerifiedIdentity = (accountAddressToFetch: string) => { + const getVerifiedIdentity = (accountAddressToFetch: string, publicKey: string) => { console.log('getVerifiedIdentity', accountAddressToFetch, accountAddress); // applySpaceEvent is only allowed to be called by the account that is applying the event if (accountAddressToFetch !== accountAddress) { @@ -49,7 +49,8 @@ export async function applySpaceEvent({ accountAddress, spaceId, event, keyBoxes return Effect.gen(function* () { const identity = yield* Effect.tryPromise({ - try: () => getConnectIdentity({ accountAddress: accountAddressToFetch }), + try: () => + getAppOrConnectIdentity({ accountAddress: accountAddressToFetch, signaturePublicKey: publicKey, spaceId }), catch: () => new Identity.InvalidIdentityError(), }); return identity; diff --git a/apps/server/src/handlers/create-space.ts b/apps/server/src/handlers/create-space.ts index 75efbc69..57535e47 100644 --- a/apps/server/src/handlers/create-space.ts +++ b/apps/server/src/handlers/create-space.ts @@ -5,7 +5,7 @@ import type { Messages } from '@graphprotocol/hypergraph'; import { Identity, SpaceEvents } from '@graphprotocol/hypergraph'; import { prisma } from '../prisma.js'; -import { getConnectIdentity } from './getConnectIdentity.js'; +import { getAppOrConnectIdentity } from './getAppOrConnectIdentity.js'; type Params = { accountAddress: string; @@ -26,7 +26,7 @@ export const createSpace = async ({ infoSignatureRecovery, name, }: Params) => { - const getVerifiedIdentity = (accountAddressToFetch: string) => { + const getVerifiedIdentity = (accountAddressToFetch: string, publicKey: string) => { // applySpaceEvent is only allowed to be called by the account that is applying the event if (accountAddressToFetch !== accountAddress) { return Effect.fail(new Identity.InvalidIdentityError()); @@ -34,7 +34,7 @@ export const createSpace = async ({ return Effect.gen(function* () { const identity = yield* Effect.tryPromise({ - try: () => getConnectIdentity({ accountAddress: accountAddressToFetch }), + try: () => getAppOrConnectIdentity({ accountAddress: accountAddressToFetch, signaturePublicKey: publicKey }), catch: () => new Identity.InvalidIdentityError(), }); return identity; diff --git a/apps/server/src/handlers/getAccountInbox.ts b/apps/server/src/handlers/getAccountInbox.ts index 5acadcef..154170ea 100644 --- a/apps/server/src/handlers/getAccountInbox.ts +++ b/apps/server/src/handlers/getAccountInbox.ts @@ -8,7 +8,7 @@ export async function getAccountInbox({ accountAddress, inboxId }: { accountAddr id: true, account: { select: { - id: true, + address: true, }, }, isPublic: true, @@ -24,7 +24,7 @@ export async function getAccountInbox({ accountAddress, inboxId }: { accountAddr return { inboxId: inbox.id, - accountAddress: inbox.account.id, + accountAddress: inbox.account.address, isPublic: inbox.isPublic, authPolicy: inbox.authPolicy as Inboxes.InboxSenderAuthPolicy, encryptionPublicKey: inbox.encryptionPublicKey, diff --git a/apps/server/src/handlers/getAppOrConnectIdentity.ts b/apps/server/src/handlers/getAppOrConnectIdentity.ts new file mode 100644 index 00000000..18f8ee7f --- /dev/null +++ b/apps/server/src/handlers/getAppOrConnectIdentity.ts @@ -0,0 +1,82 @@ +import { prisma } from '../prisma.js'; + +type Params = + | { + accountAddress: string; + signaturePublicKey: string; + spaceId?: string; + } + | { + accountAddress: string; + appId: string; + spaceId?: string; + }; + +export type GetIdentityResult = { + accountAddress: string; + ciphertext: string; + nonce: string; + signaturePublicKey: string; + encryptionPublicKey: string; + accountProof: string; + keyProof: string; + appId: string | null; +}; + +export const getAppOrConnectIdentity = async (params: Params): Promise => { + if (!('appId' in params)) { + const where: { address: string; connectSignaturePublicKey?: string } = { address: params.accountAddress }; + if ('signaturePublicKey' in params) { + where.connectSignaturePublicKey = params.signaturePublicKey; + } + const account = await prisma.account.findFirst({ + where, + }); + if (account) { + return { + accountAddress: account.address, + ciphertext: account.connectCiphertext, + nonce: account.connectNonce, + signaturePublicKey: account.connectSignaturePublicKey, + encryptionPublicKey: account.connectEncryptionPublicKey, + accountProof: account.connectAccountProof, + keyProof: account.connectKeyProof, + appId: null, + }; + } + } + const appWhere: { + accountAddress: string; + appId?: string; + signaturePublicKey?: string; + spaces?: { some: { id: string } }; + } = { + accountAddress: params.accountAddress, + }; + if ('signaturePublicKey' in params) { + appWhere.signaturePublicKey = params.signaturePublicKey; + } + if ('appId' in params) { + appWhere.appId = params.appId; + } + if (params.spaceId) { + appWhere.spaces = { some: { id: params.spaceId } }; + } + + const appIdentity = await prisma.appIdentity.findFirst({ + where: appWhere, + }); + if (appIdentity) { + return { + accountAddress: appIdentity.accountAddress, + ciphertext: appIdentity.ciphertext, + nonce: appIdentity.nonce, + signaturePublicKey: appIdentity.signaturePublicKey, + encryptionPublicKey: appIdentity.encryptionPublicKey, + accountProof: appIdentity.accountProof, + keyProof: appIdentity.keyProof, + appId: appIdentity.appId, + }; + } + throw new Error('Identity not found'); +}; diff --git a/apps/server/src/handlers/getSpace.ts b/apps/server/src/handlers/getSpace.ts index ae1cd653..98a15dd5 100644 --- a/apps/server/src/handlers/getSpace.ts +++ b/apps/server/src/handlers/getSpace.ts @@ -4,9 +4,10 @@ import { prisma } from '../prisma.js'; type Params = { spaceId: string; accountAddress: string; + appIdentityAddress: string; }; -export const getSpace = async ({ spaceId, accountAddress }: Params) => { +export const getSpace = async ({ spaceId, accountAddress, appIdentityAddress }: Params) => { const space = await prisma.space.findUniqueOrThrow({ where: { id: spaceId, @@ -27,6 +28,7 @@ export const getSpace = async ({ spaceId, accountAddress }: Params) => { keyBoxes: { where: { accountAddress, + appIdentityAddress, }, select: { nonce: true, diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 1e100b91..842c6850 100755 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -17,6 +17,7 @@ import { createUpdate } from './handlers/createUpdate.js'; import { findAppIdentity } from './handlers/find-app-identity.js'; import { getAppIdentityBySessionToken } from './handlers/get-app-identity-by-session-token.js'; import { getAccountInbox } from './handlers/getAccountInbox.js'; +import { getAppOrConnectIdentity } from './handlers/getAppOrConnectIdentity.js'; import { type GetIdentityResult, getConnectIdentity } from './handlers/getConnectIdentity.js'; import { getLatestAccountInboxMessages } from './handlers/getLatestAccountInboxMessages.js'; import { getLatestSpaceInboxMessages } from './handlers/getLatestSpaceInboxMessages.js'; @@ -169,7 +170,7 @@ app.post('/connect/add-app-identity-to-spaces', async (req, res) => { }); res.status(200).json({ space }); } catch (error) { - console.error('Error creating space:', error); + console.error('Error adding identity to spaces:', error); if (error instanceof Error && error.message === 'No Privy ID token provided') { res.status(401).json({ message: 'Unauthorized' }); } else if (error instanceof Error && error.message === 'Missing Privy configuration') { @@ -318,6 +319,20 @@ app.post('/connect/app-identity', async (req, res) => { res.status(401).send('Unauthorized'); return; } + if ( + !Identity.verifyIdentityOwnership( + accountAddress, + message.signaturePublicKey, + message.accountProof, + message.keyProof, + CHAIN, + RPC_URL, + ) + ) { + console.log('Ownership proof is invalid'); + res.status(401).send('Unauthorized'); + return; + } const sessionToken = bytesToHex(randomBytes(32)); const sessionTokenExpires = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30); // 30 days const appIdentity = await createAppIdentity({ @@ -366,21 +381,54 @@ app.get('/whoami', async (req, res) => { } }); +app.get('/connect/identity', async (req, res) => { + console.log('GET connect/identity'); + const accountAddress = req.query.accountAddress as string; + if (!accountAddress) { + res.status(400).send('No accountAddress'); + return; + } + try { + const identity = await getConnectIdentity({ accountAddress }); + const outgoingMessage: Messages.ResponseIdentity = { + accountAddress, + signaturePublicKey: identity.signaturePublicKey, + encryptionPublicKey: identity.encryptionPublicKey, + accountProof: identity.accountProof, + keyProof: identity.keyProof, + }; + res.status(200).send(outgoingMessage); + } catch (error) { + const outgoingMessage: Messages.ResponseIdentityNotFoundError = { + accountAddress, + }; + res.status(404).send(outgoingMessage); + } +}); + app.get('/identity', async (req, res) => { console.log('GET identity'); const accountAddress = req.query.accountAddress as string; + const signaturePublicKey = req.query.signaturePublicKey as string; + const appId = req.query.appId as string; if (!accountAddress) { res.status(400).send('No accountAddress'); return; } + if (!signaturePublicKey && !appId) { + res.status(400).send('No signaturePublicKey or appId'); + return; + } try { - const identity = await getConnectIdentity({ accountAddress }); + const params = signaturePublicKey ? { accountAddress, signaturePublicKey } : { accountAddress, appId }; + const identity = await getAppOrConnectIdentity(params); const outgoingMessage: Messages.ResponseIdentity = { accountAddress, signaturePublicKey: identity.signaturePublicKey, encryptionPublicKey: identity.encryptionPublicKey, accountProof: identity.accountProof, keyProof: identity.keyProof, + appId: identity.appId ?? undefined, }; res.status(200).send(outgoingMessage); } catch (error) { @@ -471,7 +519,10 @@ app.post('/spaces/:spaceId/inboxes/:inboxId/messages', async (req, res) => { // Check if this public key corresponds to a user's identity let authorIdentity: GetIdentityResult; try { - authorIdentity = await getConnectIdentity({ connectSignaturePublicKey: authorPublicKey }); + authorIdentity = await getAppOrConnectIdentity({ + accountAddress: message.authorAccountAddress, + signaturePublicKey: authorPublicKey, + }); } catch (error) { res.status(403).send({ error: 'Not authorized to post to this inbox' }); return; @@ -569,7 +620,10 @@ app.post('/accounts/:accountAddress/inboxes/:inboxId/messages', async (req, res) // Check if this public key corresponds to a user's identity let authorIdentity: GetIdentityResult; try { - authorIdentity = await getConnectIdentity({ connectSignaturePublicKey: authorPublicKey }); + authorIdentity = await getAppOrConnectIdentity({ + accountAddress: message.authorAccountAddress, + signaturePublicKey: authorPublicKey, + }); } catch (error) { res.status(403).send({ error: 'Not authorized to post to this inbox' }); return; @@ -735,7 +789,7 @@ webSocketServer.on('connection', async (webSocket: CustomWebSocket, request: Req const data = result.right; switch (data.type) { case 'subscribe-space': { - const space = await getSpace({ accountAddress, spaceId: data.id }); + const space = await getSpace({ accountAddress, spaceId: data.id, appIdentityAddress }); const outgoingMessage: Messages.ResponseSpace = { ...space, type: 'space', @@ -760,19 +814,15 @@ webSocketServer.on('connection', async (webSocket: CustomWebSocket, request: Req break; } case 'create-space-event': { - const getVerifiedIdentity = (accountAddressToFetch: string) => { - console.log( - 'TODO getVerifiedIdentity should work for app identities', - accountAddressToFetch, - accountAddress, - ); + const getVerifiedIdentity = (accountAddressToFetch: string, publicKey: string) => { if (accountAddressToFetch !== accountAddress) { return Effect.fail(new Identity.InvalidIdentityError()); } return Effect.gen(function* () { const identity = yield* Effect.tryPromise({ - try: () => getConnectIdentity({ accountAddress: accountAddressToFetch }), + try: () => + getAppOrConnectIdentity({ accountAddress: accountAddressToFetch, signaturePublicKey: publicKey }), catch: () => new Identity.InvalidIdentityError(), }); return identity; @@ -796,7 +846,7 @@ webSocketServer.on('connection', async (webSocket: CustomWebSocket, request: Req infoSignatureRecovery: 0, name: data.name, }); - const spaceWithEvents = await getSpace({ accountAddress, spaceId: space.id }); + const spaceWithEvents = await getSpace({ accountAddress, spaceId: space.id, appIdentityAddress }); const outgoingMessage: Messages.ResponseSpace = { ...spaceWithEvents, type: 'space', @@ -816,8 +866,7 @@ webSocketServer.on('connection', async (webSocket: CustomWebSocket, request: Req event: data.event, keyBoxes: data.keyBoxes.map((keyBox) => keyBox), }); - const spaceWithEvents = await getSpace({ accountAddress, spaceId: data.spaceId }); - // TODO send back confirmation instead of the entire space + const spaceWithEvents = await getSpace({ accountAddress, spaceId: data.spaceId, appIdentityAddress }); const outgoingMessage: Messages.ResponseSpace = { ...spaceWithEvents, type: 'space', @@ -843,7 +892,7 @@ webSocketServer.on('connection', async (webSocket: CustomWebSocket, request: Req } case 'accept-invitation-event': { await applySpaceEvent({ accountAddress, spaceId: data.spaceId, event: data.event, keyBoxes: [] }); - const spaceWithEvents = await getSpace({ accountAddress, spaceId: data.spaceId }); + const spaceWithEvents = await getSpace({ accountAddress, spaceId: data.spaceId, appIdentityAddress }); const outgoingMessage: Messages.ResponseSpace = { ...spaceWithEvents, type: 'space', @@ -854,7 +903,7 @@ webSocketServer.on('connection', async (webSocket: CustomWebSocket, request: Req } case 'create-space-inbox-event': { await applySpaceEvent({ accountAddress, spaceId: data.spaceId, event: data.event, keyBoxes: [] }); - const spaceWithEvents = await getSpace({ accountAddress, spaceId: data.spaceId }); + const spaceWithEvents = await getSpace({ accountAddress, spaceId: data.spaceId, appIdentityAddress }); // TODO send back confirmation instead of the entire space const outgoingMessage: Messages.ResponseSpace = { ...spaceWithEvents, @@ -871,7 +920,10 @@ webSocketServer.on('connection', async (webSocket: CustomWebSocket, request: Req throw new Error('Invalid accountAddress'); } const signer = Inboxes.recoverAccountInboxCreatorKey(data); - const signerAccount = await getConnectIdentity({ connectSignaturePublicKey: signer }); + const signerAccount = await getAppOrConnectIdentity({ + accountAddress: data.accountAddress, + signaturePublicKey: signer, + }); if (signerAccount.accountAddress !== accountAddress) { throw new Error('Invalid signature'); } @@ -888,7 +940,7 @@ webSocketServer.on('connection', async (webSocket: CustomWebSocket, request: Req case 'get-latest-space-inbox-messages': { try { // Check that the user has access to this space - await getSpace({ accountAddress, spaceId: data.spaceId }); + await getSpace({ accountAddress, spaceId: data.spaceId, appIdentityAddress }); const messages = await getLatestSpaceInboxMessages({ inboxId: data.inboxId, since: data.since, @@ -941,7 +993,10 @@ webSocketServer.on('connection', async (webSocket: CustomWebSocket, request: Req // Check that the update was signed by a valid identity // belonging to this accountAddress const signer = Messages.recoverUpdateMessageSigner(data); - const identity = await getConnectIdentity({ connectSignaturePublicKey: signer }); + const identity = await getAppOrConnectIdentity({ + accountAddress: data.accountAddress, + signaturePublicKey: signer, + }); if (identity.accountAddress !== accountAddress) { throw new Error('Invalid signature'); } diff --git a/packages/hypergraph-react/src/HypergraphAppContext.tsx b/packages/hypergraph-react/src/HypergraphAppContext.tsx index 3a23518f..cda378bc 100644 --- a/packages/hypergraph-react/src/HypergraphAppContext.tsx +++ b/packages/hypergraph-react/src/HypergraphAppContext.tsx @@ -10,6 +10,7 @@ import { uuid } from '@automerge/automerge/slim'; import { Graph } from '@graphprotocol/grc-20'; import { Connect, + type ConnectCallbackResult, Identity, type InboxMessageStorageEntry, Inboxes, @@ -92,7 +93,11 @@ export type HypergraphAppCtx = { acceptInvitation(params: Readonly<{ invitation: Messages.Invitation }>): Promise; subscribeToSpace(params: Readonly<{ spaceId: string }>): void; inviteToSpace(params: Readonly<{ space: SpaceStorageEntry; invitee: { accountAddress: Address } }>): Promise; - getVerifiedIdentity(accountAddress: string): Promise<{ + getVerifiedIdentity( + accountAddress: string, + publicKey: string | null, + appId: string | null, + ): Promise<{ accountAddress: string; encryptionPublicKey: string; signaturePublicKey: string; @@ -108,7 +113,6 @@ export type HypergraphAppCtx = { redirectToConnect(params: { storage: Identity.Storage; successUrl: string; - appId: string; connectUrl: string; redirectFn: (url: URL) => void; }): void; @@ -220,6 +224,7 @@ export type HypergraphAppProviderProps = Readonly<{ chainId?: number; children: ReactNode; mapping: Mapping; + appId: string; }>; const mockStorage = { @@ -239,6 +244,7 @@ export function HypergraphAppProvider({ storage = typeof window !== 'undefined' ? localStorage : mockStorage, syncServerUri = 'https://hypergraph.fly.dev', chainId = Connect.GEO_TESTNET.id, + appId, children, mapping, }: HypergraphAppProviderProps) { @@ -376,6 +382,8 @@ export function HypergraphAppProvider({ }); const authorIdentity = await Identity.getVerifiedIdentity( update.accountAddress, + signer, + null, syncServerUri, CHAIN, RPC_URL, @@ -411,10 +419,10 @@ export function HypergraphAppProvider({ }); }; - const getVerifiedIdentity = (accountAddress: string) => { + const getVerifiedIdentityForEvent = (accountAddress: string, publicKey: string) => { return Effect.gen(function* () { const identity = yield* Effect.tryPromise({ - try: () => Identity.getVerifiedIdentity(accountAddress, syncServerUri, CHAIN, RPC_URL), + try: () => Identity.getVerifiedIdentity(accountAddress, publicKey, null, syncServerUri, CHAIN, RPC_URL), catch: () => new Identity.InvalidIdentityError(), }); return identity; @@ -443,7 +451,9 @@ export function HypergraphAppProvider({ for (const event of response.events) { // Not sure why but type inference doesn't work here const applyEventResult: Exit.Exit = - await Effect.runPromiseExit(SpaceEvents.applyEvent({ state, event, getVerifiedIdentity })); + await Effect.runPromiseExit( + SpaceEvents.applyEvent({ state, event, getVerifiedIdentity: getVerifiedIdentityForEvent }), + ); if (Exit.isSuccess(applyEventResult)) { state = applyEventResult.value; } else { @@ -518,7 +528,7 @@ export function HypergraphAppProvider({ const updateId = uuid(); const messageToSend = Messages.signedUpdateMessage({ - accountAddress: identity.address, + accountAddress: identity.accountAddress, updateId, spaceId: space.id, message: lastLocalChange, @@ -549,7 +559,11 @@ export function HypergraphAppProvider({ } const applyEventResult = await Effect.runPromiseExit( - SpaceEvents.applyEvent({ event: response.event, state: space.state, getVerifiedIdentity }), + SpaceEvents.applyEvent({ + event: response.event, + state: space.state, + getVerifiedIdentity: getVerifiedIdentityForEvent, + }), ); if (Exit.isSuccess(applyEventResult)) { store.send({ @@ -704,7 +718,7 @@ export function HypergraphAppProvider({ const isValid = await Inboxes.validateAccountInboxMessage( response.message, inbox, - identity.address, + identity.accountAddress, syncServerUri, CHAIN, RPC_URL, @@ -774,7 +788,7 @@ export function HypergraphAppProvider({ return Inboxes.validateAccountInboxMessage( message, inbox, - identity.address, + identity.accountAddress, syncServerUri, CHAIN, RPC_URL, @@ -930,7 +944,7 @@ export function HypergraphAppProvider({ const spaceEvent = await Effect.runPromise( SpaceEvents.createSpace({ author: { - accountAddress: identity.address, + accountAddress: identity.accountAddress, encryptionPublicKey, signaturePrivateKey, signaturePublicKey, @@ -948,7 +962,7 @@ export function HypergraphAppProvider({ event: spaceEvent, spaceId: spaceEvent.transaction.id, keyBox: { - accountAddress: identity.address, + accountAddress: identity.accountAddress, ciphertext: Utils.bytesToHex(result.keyBoxCiphertext), nonce: Utils.bytesToHex(result.keyBoxNonce), authorPublicKey: encryptionPublicKey, @@ -1223,7 +1237,7 @@ export function HypergraphAppProvider({ const spaceEvent = await Effect.runPromiseExit( SpaceEvents.acceptInvitation({ author: { - accountAddress: identity.address, + accountAddress: identity.accountAddress, signaturePublicKey, encryptionPublicKey, signaturePrivateKey, @@ -1290,11 +1304,18 @@ export function HypergraphAppProvider({ console.error('No state found for space'); return; } - const inviteeWithKeys = await Identity.getVerifiedIdentity(invitee.accountAddress, syncServerUri, CHAIN, RPC_URL); + const inviteeWithKeys = await Identity.getVerifiedIdentity( + invitee.accountAddress, + null, + appId, + syncServerUri, + CHAIN, + RPC_URL, + ); const spaceEvent = await Effect.runPromiseExit( SpaceEvents.createInvitation({ author: { - accountAddress: identity.address, + accountAddress: identity.accountAddress, signaturePublicKey, encryptionPublicKey, signaturePrivateKey, @@ -1331,14 +1352,15 @@ export function HypergraphAppProvider({ }; websocketConnection?.send(Messages.serialize(message)); }, - [identity, websocketConnection, syncServerUri], + [identity, websocketConnection, syncServerUri, appId], ); - const getVerifiedIdentity = useCallback( - (accountAddress: string) => { - return Identity.getVerifiedIdentity(accountAddress, syncServerUri, CHAIN, RPC_URL); + const getVerifiedIdentityForContext = useCallback( + (accountAddress: string, publicKey: string | null, inputAppId: string | null) => { + const appIdToUse = inputAppId ?? (publicKey ? null : appId); + return Identity.getVerifiedIdentity(accountAddress, publicKey, appIdToUse, syncServerUri, CHAIN, RPC_URL); }, - [syncServerUri], + [syncServerUri, appId], ); const ensureSpaceInboxForContext = useCallback( @@ -1395,11 +1417,10 @@ export function HypergraphAppProvider({ (params: { storage: Identity.Storage; successUrl: string; - appId: string; connectUrl: string; redirectFn: (url: URL) => void; }) => { - const { storage, successUrl, redirectFn, appId, connectUrl } = params; + const { storage, successUrl, redirectFn, connectUrl } = params; const { url, nonce, expiry, secretKey, publicKey } = Connect.createAuthUrl({ connectUrl: `${connectUrl}/authenticate`, redirectUrl: successUrl, @@ -1411,7 +1432,7 @@ export function HypergraphAppProvider({ storage.setItem('geo-connect-auth-public-key', publicKey); redirectFn(url); }, - [], + [appId], ); const processConnectAuthSuccessForContext = useCallback( @@ -1427,7 +1448,7 @@ export function HypergraphAppProvider({ } try { - const parsedAuthParams = Effect.runSync( + const parsedAuthParams: ConnectCallbackResult = Effect.runSync( Connect.parseCallbackParams({ ciphertext, nonce, @@ -1499,7 +1520,7 @@ export function HypergraphAppProvider({ listInvitations, acceptInvitation: acceptInvitationForContext, subscribeToSpace, - getVerifiedIdentity, + getVerifiedIdentity: getVerifiedIdentityForContext, inviteToSpace, isConnecting, isLoadingSpaces, diff --git a/packages/hypergraph-react/src/hooks/useExternalAccountInbox.ts b/packages/hypergraph-react/src/hooks/useExternalAccountInbox.ts index f9c01ded..55d96226 100644 --- a/packages/hypergraph-react/src/hooks/useExternalAccountInbox.ts +++ b/packages/hypergraph-react/src/hooks/useExternalAccountInbox.ts @@ -46,7 +46,7 @@ export function useExternalAccountInbox(accountAddress: string, inboxId: string) let authorAccountAddress: string | null = null; let signaturePrivateKey: string | null = null; if (identity?.address && inbox.authPolicy !== 'anonymous') { - authorAccountAddress = identity.address; + authorAccountAddress = identity.accountAddress; signaturePrivateKey = identity.signaturePrivateKey; } diff --git a/packages/hypergraph/src/connect/login.ts b/packages/hypergraph/src/connect/login.ts index 4dc01328..c8213c27 100644 --- a/packages/hypergraph/src/connect/login.ts +++ b/packages/hypergraph/src/connect/login.ts @@ -19,7 +19,7 @@ import { import type { IdentityKeys, Signer, Storage } from './types.js'; export async function identityExists(accountAddress: string, syncServerUri: string) { - const res = await fetch(new URL(`/identity?accountAddress=${accountAddress}`, syncServerUri), { + const res = await fetch(new URL(`/connect/identity?accountAddress=${accountAddress}`, syncServerUri), { method: 'GET', }); return res.status === 200; diff --git a/packages/hypergraph/src/identity/get-verified-identity.ts b/packages/hypergraph/src/identity/get-verified-identity.ts index 60bbb9ba..56456e37 100644 --- a/packages/hypergraph/src/identity/get-verified-identity.ts +++ b/packages/hypergraph/src/identity/get-verified-identity.ts @@ -6,6 +6,8 @@ import { verifyIdentityOwnership } from './prove-ownership.js'; export const getVerifiedIdentity = async ( accountAddress: string, + signaturePublicKey: string | null, + appId: string | null, syncServerUri: string, chain: Chain, rpcUrl: string, @@ -14,8 +16,19 @@ export const getVerifiedIdentity = async ( encryptionPublicKey: string; signaturePublicKey: string; }> => { + if (signaturePublicKey && appId) { + throw new Error('Cannot specify both signaturePublicKey and appId'); + } + if (!signaturePublicKey && !appId) { + throw new Error('Must specify either signaturePublicKey or appId'); + } const storeState = store.getSnapshot(); - const identity = storeState.context.identities[accountAddress]; + const identity = storeState.context.identities[accountAddress]?.find((identity) => { + if (signaturePublicKey) { + return identity.signaturePublicKey === signaturePublicKey; + } + return identity.appId === appId; + }); if (identity) { return { accountAddress, @@ -23,7 +36,8 @@ export const getVerifiedIdentity = async ( signaturePublicKey: identity.signaturePublicKey, }; } - const res = await fetch(`${syncServerUri}/identity?accountAddress=${accountAddress}`); + const query = signaturePublicKey ? `&signaturePublicKey=${signaturePublicKey}` : `&appId=${appId}`; + const res = await fetch(`${syncServerUri}/identity?accountAddress=${accountAddress}${query}`); if (res.status !== 200) { throw new Error('Failed to fetch identity'); } @@ -49,6 +63,7 @@ export const getVerifiedIdentity = async ( signaturePublicKey: resDecoded.signaturePublicKey, accountProof: resDecoded.accountProof, keyProof: resDecoded.keyProof, + appId: resDecoded.appId ?? null, }); return { accountAddress: resDecoded.accountAddress, diff --git a/packages/hypergraph/src/inboxes/message-validation.ts b/packages/hypergraph/src/inboxes/message-validation.ts index 7c0dd643..8090d481 100644 --- a/packages/hypergraph/src/inboxes/message-validation.ts +++ b/packages/hypergraph/src/inboxes/message-validation.ts @@ -24,6 +24,8 @@ export const validateSpaceInboxMessage = async ( const signer = recoverSpaceInboxMessageSigner(message, spaceId, inbox.inboxId); const verifiedIdentity = await Identity.getVerifiedIdentity( message.authorAccountAddress, + signer, + null, syncServerUri, chain, rpcUrl, @@ -62,6 +64,8 @@ export const validateAccountInboxMessage = async ( const signer = recoverAccountInboxMessageSigner(message, accountAddress, inbox.inboxId); const verifiedIdentity = await Identity.getVerifiedIdentity( message.authorAccountAddress, + signer, + null, syncServerUri, chain, rpcUrl, diff --git a/packages/hypergraph/src/messages/types.ts b/packages/hypergraph/src/messages/types.ts index 05f6b0bd..5adbf3b3 100644 --- a/packages/hypergraph/src/messages/types.ts +++ b/packages/hypergraph/src/messages/types.ts @@ -450,6 +450,7 @@ export const ResponseIdentity = Schema.Struct({ encryptionPublicKey: Schema.String, accountProof: Schema.String, keyProof: Schema.String, + appId: Schema.optional(Schema.String), }); export type ResponseIdentity = Schema.Schema.Type; diff --git a/packages/hypergraph/src/space-events/apply-event.ts b/packages/hypergraph/src/space-events/apply-event.ts index 0af19056..cbf1cefd 100644 --- a/packages/hypergraph/src/space-events/apply-event.ts +++ b/packages/hypergraph/src/space-events/apply-event.ts @@ -18,7 +18,10 @@ import { type Params = { state: SpaceState | undefined; event: SpaceEvent; - getVerifiedIdentity: (accountAddress: string) => Effect.Effect; + getVerifiedIdentity: ( + accountAddress: string, + publicKey: string, + ) => Effect.Effect; }; const decodeSpaceEvent = Schema.decodeUnknownEither(SpaceEvent); @@ -50,7 +53,7 @@ export const applyEvent = ({ const authorPublicKey = `0x${signatureInstance.recoverPublicKey(sha256(encodedTransaction)).toHex()}`; return Effect.gen(function* () { - const identity = yield* getVerifiedIdentity(event.author.accountAddress); + const identity = yield* getVerifiedIdentity(event.author.accountAddress, authorPublicKey); if (authorPublicKey !== identity.signaturePublicKey) { yield* Effect.fail(new VerifySignatureError()); } diff --git a/packages/hypergraph/src/store.ts b/packages/hypergraph/src/store.ts index c873995c..11fc1026 100644 --- a/packages/hypergraph/src/store.ts +++ b/packages/hypergraph/src/store.ts @@ -64,7 +64,8 @@ interface StoreContext { signaturePublicKey: string; accountProof: string; keyProof: string; - }; + appId: string | null; + }[]; }; authenticated: boolean; identity: PrivateAppIdentity | null; @@ -104,6 +105,7 @@ type StoreEvent = signaturePublicKey: string; accountProof: string; keyProof: string; + appId: string | null; } | { type: 'setSpaceInbox'; @@ -291,18 +293,46 @@ export const store: Store = create signaturePublicKey: string; accountProof: string; keyProof: string; + appId: string | null; }, ) => { + const existingIdentity = context.identities[event.accountAddress]?.find( + (identity) => identity.signaturePublicKey === event.signaturePublicKey, + ); + if (existingIdentity) { + return context; + } + if (context.identities[event.accountAddress]) { + return { + ...context, + identities: { + ...context.identities, + [event.accountAddress]: [ + ...context.identities[event.accountAddress], + { + encryptionPublicKey: event.encryptionPublicKey, + signaturePublicKey: event.signaturePublicKey, + accountProof: event.accountProof, + keyProof: event.keyProof, + appId: event.appId, + }, + ], + }, + }; + } return { ...context, identities: { ...context.identities, - [event.accountAddress]: { - encryptionPublicKey: event.encryptionPublicKey, - signaturePublicKey: event.signaturePublicKey, - accountProof: event.accountProof, - keyProof: event.keyProof, - }, + [event.accountAddress]: [ + { + encryptionPublicKey: event.encryptionPublicKey, + signaturePublicKey: event.signaturePublicKey, + accountProof: event.accountProof, + keyProof: event.keyProof, + appId: event.appId, + }, + ], }, }; }, diff --git a/packages/hypergraph/test/inboxes/inboxes.test.ts b/packages/hypergraph/test/inboxes/inboxes.test.ts index 4b55f6e3..3e4f0c53 100644 --- a/packages/hypergraph/test/inboxes/inboxes.test.ts +++ b/packages/hypergraph/test/inboxes/inboxes.test.ts @@ -351,11 +351,13 @@ describe('inboxes', () => { vi.clearAllMocks(); }); - vi.spyOn(Identity, 'getVerifiedIdentity').mockImplementation(async (accountAddress: string) => ({ - accountAddress, - signaturePublicKey: bytesToHex(signaturePublicKey), - encryptionPublicKey: bytesToHex(encryptionPublicKey), - })); + vi.spyOn(Identity, 'getVerifiedIdentity').mockImplementation( + async (accountAddress: string, publicKey: string | null) => ({ + accountAddress, + signaturePublicKey: publicKey ?? '', + encryptionPublicKey: bytesToHex(encryptionPublicKey), + }), + ); it.skip('should validate a properly signed space inbox message', async () => { const spaceId = generateId(); @@ -382,7 +384,11 @@ describe('inboxes', () => { }; const isValid = await validateSpaceInboxMessage( - message, + { + ...message, + id: generateId(), + createdAt: new Date(), + }, inbox, spaceId, 'https://sync.example.com', @@ -391,7 +397,11 @@ describe('inboxes', () => { ); expect(isValid).toBe(true); - expect(Identity.getVerifiedIdentity).toHaveBeenCalledWith(testParams.accountAddress, 'https://sync.example.com'); + expect(Identity.getVerifiedIdentity).toHaveBeenCalledWith( + testParams.accountAddress, + bytesToHex(signaturePublicKey), + 'https://sync.example.com', + ); }); it('should reject unsigned messages for RequiresAuth inboxes', async () => { @@ -411,7 +421,11 @@ describe('inboxes', () => { }; const isValid = await validateSpaceInboxMessage( - message, + { + ...message, + id: generateId(), + createdAt: new Date(), + }, inbox, generateId(), 'https://sync.example.com', @@ -477,7 +491,11 @@ describe('inboxes', () => { }; const isValid = await validateSpaceInboxMessage( - message, + { + ...message, + id: generateId(), + createdAt: new Date(), + }, inbox, spaceId, 'https://sync.example.com', @@ -517,7 +535,18 @@ describe('inboxes', () => { }; await expect( - validateSpaceInboxMessage(message, inbox, spaceId, 'https://sync.example.com', CHAIN, RPC_URL), + validateSpaceInboxMessage( + { + ...message, + id: generateId(), + createdAt: new Date(), + }, + inbox, + spaceId, + 'https://sync.example.com', + CHAIN, + RPC_URL, + ), ).rejects.toThrow('Failed to verify identity'); }); @@ -546,7 +575,11 @@ describe('inboxes', () => { }; const isValid = await validateSpaceInboxMessage( - message, + { + ...message, + id: generateId(), + createdAt: new Date(), + }, inbox, spaceId, 'https://sync.example.com', @@ -555,7 +588,11 @@ describe('inboxes', () => { ); expect(isValid).toBe(true); - expect(Identity.getVerifiedIdentity).toHaveBeenCalledWith(testParams.accountAddress, 'https://sync.example.com'); + expect(Identity.getVerifiedIdentity).toHaveBeenCalledWith( + testParams.accountAddress, + bytesToHex(signaturePublicKey), + 'https://sync.example.com', + ); }); it('should accept unsigned messages on inboxes with optional auth', async () => { @@ -575,7 +612,11 @@ describe('inboxes', () => { }; const isValid = await validateSpaceInboxMessage( - message, + { + ...message, + id: generateId(), + createdAt: new Date(), + }, inbox, generateId(), 'https://sync.example.com', @@ -634,7 +675,11 @@ describe('inboxes', () => { }; const isValid = await validateSpaceInboxMessage( - message, + { + ...message, + id: generateId(), + createdAt: new Date(), + }, inbox, spaceId, 'https://sync.example.com', @@ -643,7 +688,11 @@ describe('inboxes', () => { ); expect(isValid).toBe(false); - expect(Identity.getVerifiedIdentity).toHaveBeenCalledWith(testParams.accountAddress, 'https://sync.example.com'); + expect(Identity.getVerifiedIdentity).toHaveBeenCalledWith( + testParams.accountAddress, + bytesToHex(signaturePublicKey), + 'https://sync.example.com', + ); }); }); @@ -651,11 +700,13 @@ describe('inboxes', () => { beforeEach(() => { vi.clearAllMocks(); - vi.spyOn(Identity, 'getVerifiedIdentity').mockImplementation(async (accountAddress: string) => ({ - accountAddress, - signaturePublicKey: bytesToHex(signaturePublicKey), - encryptionPublicKey: bytesToHex(encryptionPublicKey), - })); + vi.spyOn(Identity, 'getVerifiedIdentity').mockImplementation( + async (accountAddress: string, publicKey: string | null) => ({ + accountAddress, + signaturePublicKey: publicKey ?? '', + encryptionPublicKey: bytesToHex(encryptionPublicKey), + }), + ); }); it.skip('should validate a properly signed account inbox message', async () => { @@ -682,7 +733,11 @@ describe('inboxes', () => { }; const isValid = await validateAccountInboxMessage( - message, + { + ...message, + id: generateId(), + createdAt: new Date(), + }, inbox, accountAddress, 'https://sync.example.com', @@ -691,7 +746,11 @@ describe('inboxes', () => { ); expect(isValid).toBe(true); - expect(Identity.getVerifiedIdentity).toHaveBeenCalledWith(testParams.accountAddress, 'https://sync.example.com'); + expect(Identity.getVerifiedIdentity).toHaveBeenCalledWith( + testParams.accountAddress, + bytesToHex(signaturePublicKey), + 'https://sync.example.com', + ); }); it('should reject unsigned messages for RequiresAuth inboxes', async () => { @@ -713,7 +772,11 @@ describe('inboxes', () => { }; const isValid = await validateAccountInboxMessage( - message, + { + ...message, + id: generateId(), + createdAt: new Date(), + }, inbox, accountAddress, 'https://sync.example.com', @@ -771,7 +834,11 @@ describe('inboxes', () => { }; const isValid = await validateAccountInboxMessage( - message, + { + ...message, + id: generateId(), + createdAt: new Date(), + }, inbox, accountAddress, 'https://sync.example.com', @@ -780,7 +847,11 @@ describe('inboxes', () => { ); expect(isValid).toBe(false); - expect(Identity.getVerifiedIdentity).toHaveBeenCalledWith(testParams.accountAddress, 'https://sync.example.com'); + expect(Identity.getVerifiedIdentity).toHaveBeenCalledWith( + testParams.accountAddress, + bytesToHex(signaturePublicKey), + 'https://sync.example.com', + ); }); it('should accept unsigned messages for Anonymous inboxes', async () => { @@ -802,7 +873,11 @@ describe('inboxes', () => { }; const isValid = await validateAccountInboxMessage( - message, + { + ...message, + id: generateId(), + createdAt: new Date(), + }, inbox, accountAddress, 'https://sync.example.com', @@ -838,7 +913,11 @@ describe('inboxes', () => { }; const isValid = await validateAccountInboxMessage( - message, + { + ...message, + id: generateId(), + createdAt: new Date(), + }, inbox, accountAddress, 'https://sync.example.com', @@ -874,7 +953,11 @@ describe('inboxes', () => { }; const isValid = await validateAccountInboxMessage( - message, + { + ...message, + id: generateId(), + createdAt: new Date(), + }, inbox, accountAddress, 'https://sync.example.com', @@ -883,7 +966,11 @@ describe('inboxes', () => { ); expect(isValid).toBe(true); - expect(Identity.getVerifiedIdentity).toHaveBeenCalledWith(testParams.accountAddress, 'https://sync.example.com'); + expect(Identity.getVerifiedIdentity).toHaveBeenCalledWith( + testParams.accountAddress, + bytesToHex(signaturePublicKey), + 'https://sync.example.com', + ); }); it('should accept unsigned messages on inboxes with optional auth', async () => { @@ -904,7 +991,11 @@ describe('inboxes', () => { }; const isValid = await validateAccountInboxMessage( - message, + { + ...message, + id: generateId(), + createdAt: new Date(), + }, inbox, accountAddress, 'https://sync.example.com', diff --git a/packages/hypergraph/test/space-events/accept-invitation.test.ts b/packages/hypergraph/test/space-events/accept-invitation.test.ts index b08fe2fd..a46c1073 100644 --- a/packages/hypergraph/test/space-events/accept-invitation.test.ts +++ b/packages/hypergraph/test/space-events/accept-invitation.test.ts @@ -1,6 +1,7 @@ import { Effect } from 'effect'; import { expect, it } from 'vitest'; +import { InvalidIdentityError, type PublicIdentity } from '../../src/identity/types.js'; import { acceptInvitation } from '../../src/space-events/accept-invitation.js'; import { applyEvent } from '../../src/space-events/apply-event.js'; import { createInvitation } from '../../src/space-events/create-invitation.js'; @@ -20,11 +21,14 @@ const invitee = { encryptionPublicKey: 'encryption', }; -const getVerifiedIdentity = (accountAddress: string) => { - if (accountAddress === author.accountAddress) { - return Effect.succeed(author); +const getVerifiedIdentity = (accountAddress: string, publicKey: string) => { + if (accountAddress === author.accountAddress && publicKey === author.signaturePublicKey) { + return Effect.succeed(author as PublicIdentity); } - return Effect.succeed(invitee); + if (accountAddress === invitee.accountAddress && publicKey === invitee.signaturePublicKey) { + return Effect.succeed(invitee as PublicIdentity); + } + return Effect.fail(new InvalidIdentityError()); }; it('should accept an invitation', async () => { diff --git a/packages/hypergraph/test/space-events/apply-event.test.ts b/packages/hypergraph/test/space-events/apply-event.test.ts index a6a8ad86..39c788ce 100644 --- a/packages/hypergraph/test/space-events/apply-event.test.ts +++ b/packages/hypergraph/test/space-events/apply-event.test.ts @@ -5,7 +5,7 @@ import { expect, it } from 'vitest'; import { canonicalize } from '../../src/utils/jsc.js'; import { stringToUint8Array } from '../../src/utils/stringToUint8Array.js'; -import { InvalidIdentityError } from '../../src/identity/types.js'; +import { InvalidIdentityError, type PublicIdentity } from '../../src/identity/types.js'; import { applyEvent } from '../../src/space-events/apply-event.js'; import { createInvitation } from '../../src/space-events/create-invitation.js'; import { createSpace } from '../../src/space-events/create-space.js'; @@ -25,12 +25,12 @@ const invitee = { encryptionPublicKey: 'encryption', }; -const getVerifiedIdentity = (accountAddress: string) => { - if (accountAddress === author.accountAddress) { - return Effect.succeed(author); +const getVerifiedIdentity = (accountAddress: string, publicKey: string) => { + if (accountAddress === author.accountAddress && publicKey === author.signaturePublicKey) { + return Effect.succeed(author as PublicIdentity); } - if (accountAddress === invitee.accountAddress) { - return Effect.succeed(invitee); + if (accountAddress === invitee.accountAddress && publicKey === invitee.signaturePublicKey) { + return Effect.succeed(invitee as PublicIdentity); } return Effect.fail(new InvalidIdentityError()); }; diff --git a/packages/hypergraph/test/space-events/create-space.test.ts b/packages/hypergraph/test/space-events/create-space.test.ts index fd135abb..4264c18a 100644 --- a/packages/hypergraph/test/space-events/create-space.test.ts +++ b/packages/hypergraph/test/space-events/create-space.test.ts @@ -1,7 +1,7 @@ import { Effect } from 'effect'; import { expect, it } from 'vitest'; -import { InvalidIdentityError } from '../../src/identity/types.js'; +import { InvalidIdentityError, type PublicIdentity } from '../../src/identity/types.js'; import { applyEvent } from '../../src/space-events/apply-event.js'; import { createSpace } from '../../src/space-events/create-space.js'; @@ -13,9 +13,9 @@ it('should create a space state', async () => { encryptionPublicKey: 'encryption', }; - const getVerifiedIdentity = (accountAddress: string) => { - if (accountAddress === author.accountAddress) { - return Effect.succeed(author); + const getVerifiedIdentity = (accountAddress: string, publicKey: string) => { + if (accountAddress === author.accountAddress && publicKey === author.signaturePublicKey) { + return Effect.succeed(author as PublicIdentity); } return Effect.fail(new InvalidIdentityError()); }; diff --git a/packages/hypergraph/test/space-events/delete-space.test.ts b/packages/hypergraph/test/space-events/delete-space.test.ts index 62bdf3a8..e321b9c3 100644 --- a/packages/hypergraph/test/space-events/delete-space.test.ts +++ b/packages/hypergraph/test/space-events/delete-space.test.ts @@ -1,7 +1,7 @@ import { Cause, Effect, Exit } from 'effect'; import { expect, it } from 'vitest'; -import { InvalidIdentityError } from '../../src/identity/types.js'; +import { InvalidIdentityError, type PublicIdentity } from '../../src/identity/types.js'; import { acceptInvitation } from '../../src/space-events/accept-invitation.js'; import { applyEvent } from '../../src/space-events/apply-event.js'; import { createInvitation } from '../../src/space-events/create-invitation.js'; @@ -23,12 +23,12 @@ const invitee = { encryptionPublicKey: 'encryption', }; -const getVerifiedIdentity = (accountAddress: string) => { - if (accountAddress === author.accountAddress) { - return Effect.succeed(author); +const getVerifiedIdentity = (accountAddress: string, publicKey: string) => { + if (accountAddress === author.accountAddress && publicKey === author.signaturePublicKey) { + return Effect.succeed(author as PublicIdentity); } - if (accountAddress === invitee.accountAddress) { - return Effect.succeed(invitee); + if (accountAddress === invitee.accountAddress && publicKey === invitee.signaturePublicKey) { + return Effect.succeed(invitee as PublicIdentity); } return Effect.fail(new InvalidIdentityError()); }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fbf7cfd7..bc96bfb1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -495,78 +495,6 @@ importers: version: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.1)(tsx@4.20.3)(yaml@2.7.0) publishDirectory: dist - apps/typesync/dist: - dependencies: - '@graphprotocol/grc-20': - specifier: ^0.21.6 - version: 0.21.6(bufferutil@4.0.9)(graphql@16.11.0)(ox@0.6.7(typescript@5.8.3)(zod@3.25.51))(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.51) - '@graphql-typed-document-node/core': - specifier: ^3.2.0 - version: 3.2.0(graphql@16.11.0) - '@headlessui/react': - specifier: ^2.2.4 - version: 2.2.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@heroicons/react': - specifier: ^2.2.0 - version: 2.2.0(react@19.1.0) - '@phosphor-icons/react': - specifier: ^2.1.10 - version: 2.1.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@radix-ui/react-tabs': - specifier: ^1.1.12 - version: 1.1.12(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@tailwindcss/vite': - specifier: ^4.1.11 - version: 4.1.11(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.1)(tsx@4.20.3)(yaml@2.7.0)) - '@tanstack/react-form': - specifier: ^1.14.1 - version: 1.14.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@tanstack/react-query': - specifier: ^5.83.0 - version: 5.83.0(react@19.1.0) - '@tanstack/react-query-devtools': - specifier: ^5.83.0 - version: 5.83.0(@tanstack/react-query@5.83.0(react@19.1.0))(react@19.1.0) - '@tanstack/react-router': - specifier: ^1.127.1 - version: 1.127.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@tanstack/react-router-devtools': - specifier: ^1.127.1 - version: 1.127.1(@tanstack/react-router@1.127.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@tanstack/router-core@1.127.3)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.5)(tiny-invariant@1.3.3) - better-sqlite3: - specifier: ^12.2.0 - version: 12.2.0 - date-fns: - specifier: ^4.1.0 - version: 4.1.0 - effect: - specifier: ^3.16.12 - version: 3.16.12 - graphql: - specifier: ^16.11.0 - version: 16.11.0 - graphql-request: - specifier: ^7.2.0 - version: 7.2.0(graphql@16.11.0) - jotai: - specifier: ^2.12.5 - version: 2.12.5(@types/react@19.1.8)(react@19.1.0) - open: - specifier: ^10.1.2 - version: 10.1.2 - react: - specifier: ^19.1.0 - version: 19.1.0 - react-dom: - specifier: ^19.1.0 - version: 19.1.0(react@19.1.0) - shiki: - specifier: ^3.7.0 - version: 3.7.0 - tailwindcss: - specifier: ^4.1.11 - version: 4.1.11 - docs: dependencies: '@docusaurus/core': @@ -4316,39 +4244,21 @@ packages: '@serenity-kit/noble-sodium@0.2.1': resolution: {integrity: sha512-023EjSl/ZMl8yNmnzeeWJh/V44QyBC82I8xuHltITeWdcyrQHbGnmMZRZOm/uTRinhgqoMzRBNQqbrfyuI5idg==} - '@shikijs/core@3.7.0': - resolution: {integrity: sha512-yilc0S9HvTPyahHpcum8eonYrQtmGTU0lbtwxhA6jHv4Bm1cAdlPFRCJX4AHebkCm75aKTjjRAW+DezqD1b/cg==} - '@shikijs/core@3.8.0': resolution: {integrity: sha512-gWt8NNZFurL6FMESO4lEsmspDh0H1fyUibhx1NnEH/S3kOXgYiWa6ZFqy+dcjBLhZqCXsepuUaL1QFXk6PrpsQ==} - '@shikijs/engine-javascript@3.7.0': - resolution: {integrity: sha512-0t17s03Cbv+ZcUvv+y33GtX75WBLQELgNdVghnsdhTgU3hVcWcMsoP6Lb0nDTl95ZJfbP1mVMO0p3byVh3uuzA==} - '@shikijs/engine-javascript@3.8.0': resolution: {integrity: sha512-IBULFFpQ1N5Cg/C7jPCGnjIKz72CcRtD0BIbNhSuXPUOxLG0bF1URsP/uLfxQFQ9ORfunCQwL7UuSX1RSRBwUQ==} - '@shikijs/engine-oniguruma@3.7.0': - resolution: {integrity: sha512-5BxcD6LjVWsGu4xyaBC5bu8LdNgPCVBnAkWTtOCs/CZxcB22L8rcoWfv7Hh/3WooVjBZmFtyxhgvkQFedPGnFw==} - '@shikijs/engine-oniguruma@3.8.0': resolution: {integrity: sha512-Tx7kR0oFzqa+rY7t80LjN8ZVtHO3a4+33EUnBVx2qYP3fGxoI9H0bvnln5ySelz9SIUTsS0/Qn+9dg5zcUMsUw==} - '@shikijs/langs@3.7.0': - resolution: {integrity: sha512-1zYtdfXLr9xDKLTGy5kb7O0zDQsxXiIsw1iIBcNOO8Yi5/Y1qDbJ+0VsFoqTlzdmneO8Ij35g7QKF8kcLyznCQ==} - '@shikijs/langs@3.8.0': resolution: {integrity: sha512-mfGYuUgjQ5GgXinB5spjGlBVhG2crKRpKkfADlp8r9k/XvZhtNXxyOToSnCEnF0QNiZnJjlt5MmU9PmhRdwAbg==} - '@shikijs/themes@3.7.0': - resolution: {integrity: sha512-VJx8497iZPy5zLiiCTSIaOChIcKQwR0FebwE9S3rcN0+J/GTWwQ1v/bqhTbpbY3zybPKeO8wdammqkpXc4NVjQ==} - '@shikijs/themes@3.8.0': resolution: {integrity: sha512-yaZiLuyO23sXe16JFU76KyUMTZCJi4EMQKIrdQt7okoTzI4yAaJhVXT2Uy4k8yBIEFRiia5dtD7gC1t8m6y3oQ==} - '@shikijs/types@3.7.0': - resolution: {integrity: sha512-MGaLeaRlSWpnP0XSAum3kP3a8vtcTsITqoEPYdt3lQG3YCdQH4DnEhodkYcNMcU0uW0RffhoD1O3e0vG5eSBBg==} - '@shikijs/types@3.8.0': resolution: {integrity: sha512-I/b/aNg0rP+kznVDo7s3UK8jMcqEGTtoPDdQ+JlQ2bcJIyu/e2iRvl42GLIDMK03/W1YOHOuhlhQ7aM+XbKUeg==} @@ -4958,14 +4868,6 @@ packages: react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' - '@tanstack/react-router-devtools@1.127.1': - resolution: {integrity: sha512-vKIO9ccqPNXqIU1mfZ4FRGNSeJAjvCqi9ZDxQkOb88euMkejzmN+4VTGFRQ51LwCOZaIgdy1l4JPaxH2tgq+vQ==} - engines: {node: '>=12'} - peerDependencies: - '@tanstack/react-router': ^1.127.1 - react: '>=18.0.0 || >=19.0.0' - react-dom: '>=18.0.0 || >=19.0.0' - '@tanstack/react-router-devtools@1.127.3': resolution: {integrity: sha512-MS8+ArGAoRpFaVWpXnQxNpq2bU5e2WGwV/3Gskh9YB09gqX3t6knp9im4kJ0kam16+A8Vohq1yOpCliyHzQawA==} engines: {node: '>=12'} @@ -4981,13 +4883,6 @@ packages: react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' - '@tanstack/react-router@1.127.1': - resolution: {integrity: sha512-6Ofe9VxvmuGmaJ1qUBSOsCimsPHlq4nx75EC8JhIwnkc95AQaFCZce7mTp++qECVf6HlkE5El12YwpeTz8wnpQ==} - engines: {node: '>=12'} - peerDependencies: - react: '>=18.0.0 || >=19.0.0' - react-dom: '>=18.0.0 || >=19.0.0' - '@tanstack/react-router@1.127.3': resolution: {integrity: sha512-QprmWHJrGbEKXJiP7WZ+dilTJRc7nMbsFCUnfAUw8PsOYanhgvBkBwAU05YEo8WTIZ9atCc1R90hyzqbiBFkdA==} engines: {node: '>=12'} @@ -5001,12 +4896,6 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/react-store@0.7.1': - resolution: {integrity: sha512-qUTEKdId6QPWGiWyKAPf/gkN29scEsz6EUSJ0C3HgLMgaqTAyBsQ2sMCfGVcqb+kkhEXAdjleCgH6LAPD6f2sA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/react-store@0.7.3': resolution: {integrity: sha512-3Dnqtbw9P2P0gw8uUM8WP2fFfg8XMDSZCTsywRPZe/XqqYW8PGkXKZTvP0AHkE4mpqP9Y43GpOg9vwO44azu6Q==} peerDependencies: @@ -5029,10 +4918,6 @@ packages: resolution: {integrity: sha512-3dZYP5cCq3jJYgnRDzKR3w4sYzrXP5sw1st303ye87VV26r31I8UaIuUEs7kiJaxgWBvqHglWCiygBWQODZXVw==} engines: {node: '>=12'} - '@tanstack/router-core@1.127.0': - resolution: {integrity: sha512-hHgbtLOAnN61LFqBrE2bq3mctRLfXvJefBlTFakZJavSoMEniX6bMQ5ZMDwMtpo57Hbyzx2rTD4yZfYu74Eydg==} - engines: {node: '>=12'} - '@tanstack/router-core@1.127.3': resolution: {integrity: sha512-08JlfwsMIDkMyCQsRviMVBn0cVUzlNzkll4pZgf6QRSO1RASBsci5hMojcsdH0d/yXLH0FBJ6fINbj0ctBm63Q==} engines: {node: '>=12'} @@ -5049,18 +4934,6 @@ packages: csstype: optional: true - '@tanstack/router-devtools-core@1.127.0': - resolution: {integrity: sha512-K/UFaru0sVonaRoqQFUoNiqDt4AvXLxcRd2+9HjbGSC1xckAUNEEAcVl7jwQfERg89e9IVfHiPOIuDdQFjFBtA==} - engines: {node: '>=12'} - peerDependencies: - '@tanstack/router-core': ^1.127.0 - csstype: ^3.0.10 - solid-js: '>=1.9.5' - tiny-invariant: ^1.3.3 - peerDependenciesMeta: - csstype: - optional: true - '@tanstack/router-devtools-core@1.127.3': resolution: {integrity: sha512-TaLa0h7efSTmIMckTJ1s4PuvJSRGGv4PBSDQE9QnrtCn3SJAlzjK6VIcGq3C72QKJiVDyDtCcDas4q0YeT8I+A==} engines: {node: '>=12'} @@ -5139,9 +5012,6 @@ packages: '@tanstack/store@0.7.0': resolution: {integrity: sha512-CNIhdoUsmD2NolYuaIs8VfWM467RK6oIBAW4nPEKZhg1smZ+/CwtCdpURgp7nxSqOaV9oKkzdWD80+bC66F/Jg==} - '@tanstack/store@0.7.1': - resolution: {integrity: sha512-PjUQKXEXhLYj2X5/6c1Xn/0/qKY0IVFxTJweopRfF26xfjVyb14yALydJrHupDh3/d+1WKmfEgZPBVCmDkzzwg==} - '@tanstack/store@0.7.2': resolution: {integrity: sha512-RP80Z30BYiPX2Pyo0Nyw4s1SJFH2jyM6f9i3HfX4pA+gm5jsnYryscdq2aIQLnL4TaGuQMO+zXmN9nh1Qck+Pg==} @@ -9363,10 +9233,6 @@ packages: oniguruma-to-es@4.3.3: resolution: {integrity: sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==} - open@10.1.2: - resolution: {integrity: sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==} - engines: {node: '>=18'} - open@10.2.0: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} engines: {node: '>=18'} @@ -10908,9 +10774,6 @@ packages: engines: {node: '>=4'} hasBin: true - shiki@3.7.0: - resolution: {integrity: sha512-ZcI4UT9n6N2pDuM2n3Jbk0sR4Swzq43nLPgS/4h0E3B/NrFn2HKElrDtceSf8Zx/OWYOo7G1SAtBLypCp+YXqg==} - shiki@3.8.0: resolution: {integrity: sha512-yPqK0y68t20aakv+3aMTpUMJZd6UHaBY2/SBUDowh9M70gVUwqT0bf7Kz5CWG0AXfHtFvXCHhBBHVAzdp0ILoQ==} @@ -17594,13 +17457,6 @@ snapshots: '@noble/curves': 1.9.0 '@noble/hashes': 1.8.0 - '@shikijs/core@3.7.0': - dependencies: - '@shikijs/types': 3.7.0 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - hast-util-to-html: 9.0.5 - '@shikijs/core@3.8.0': dependencies: '@shikijs/types': 3.8.0 @@ -17608,49 +17464,25 @@ snapshots: '@types/hast': 3.0.4 hast-util-to-html: 9.0.5 - '@shikijs/engine-javascript@3.7.0': - dependencies: - '@shikijs/types': 3.7.0 - '@shikijs/vscode-textmate': 10.0.2 - oniguruma-to-es: 4.3.3 - '@shikijs/engine-javascript@3.8.0': dependencies: '@shikijs/types': 3.8.0 '@shikijs/vscode-textmate': 10.0.2 oniguruma-to-es: 4.3.3 - '@shikijs/engine-oniguruma@3.7.0': - dependencies: - '@shikijs/types': 3.7.0 - '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/engine-oniguruma@3.8.0': dependencies: '@shikijs/types': 3.8.0 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@3.7.0': - dependencies: - '@shikijs/types': 3.7.0 - '@shikijs/langs@3.8.0': dependencies: '@shikijs/types': 3.8.0 - '@shikijs/themes@3.7.0': - dependencies: - '@shikijs/types': 3.7.0 - '@shikijs/themes@3.8.0': dependencies: '@shikijs/types': 3.8.0 - '@shikijs/types@3.7.0': - dependencies: - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - '@shikijs/types@3.8.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 @@ -18226,18 +18058,6 @@ snapshots: - solid-js - tiny-invariant - '@tanstack/react-router-devtools@1.127.1(@tanstack/react-router@1.127.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@tanstack/router-core@1.127.3)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.5)(tiny-invariant@1.3.3)': - dependencies: - '@tanstack/react-router': 1.127.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@tanstack/router-devtools-core': 1.127.0(@tanstack/router-core@1.127.3)(csstype@3.1.3)(solid-js@1.9.5)(tiny-invariant@1.3.3) - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - transitivePeerDependencies: - - '@tanstack/router-core' - - csstype - - solid-js - - tiny-invariant - '@tanstack/react-router-devtools@1.127.3(@tanstack/react-router@1.127.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@tanstack/router-core@1.127.3)(csstype@3.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(solid-js@1.9.5)(tiny-invariant@1.3.3)': dependencies: '@tanstack/react-router': 1.127.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -18261,18 +18081,6 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/react-router@1.127.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@tanstack/history': 1.121.34 - '@tanstack/react-store': 0.7.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@tanstack/router-core': 1.127.0 - isbot: 5.1.28 - jsesc: 3.1.0 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - tiny-invariant: 1.3.3 - tiny-warning: 1.0.3 - '@tanstack/react-router@1.127.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@tanstack/history': 1.121.34 @@ -18291,13 +18099,6 @@ snapshots: react-dom: 19.1.0(react@19.1.0) use-sync-external-store: 1.4.0(react@19.1.0) - '@tanstack/react-store@0.7.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - '@tanstack/store': 0.7.1 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - use-sync-external-store: 1.5.0(react@19.1.0) - '@tanstack/react-store@0.7.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@tanstack/store': 0.7.2 @@ -18323,17 +18124,6 @@ snapshots: '@tanstack/store': 0.7.0 tiny-invariant: 1.3.3 - '@tanstack/router-core@1.127.0': - dependencies: - '@tanstack/history': 1.121.34 - '@tanstack/store': 0.7.1 - cookie-es: 1.2.2 - jsesc: 3.1.0 - seroval: 1.3.2 - seroval-plugins: 1.3.2(seroval@1.3.2) - tiny-invariant: 1.3.3 - tiny-warning: 1.0.3 - '@tanstack/router-core@1.127.3': dependencies: '@tanstack/history': 1.121.34 @@ -18354,16 +18144,6 @@ snapshots: optionalDependencies: csstype: 3.1.3 - '@tanstack/router-devtools-core@1.127.0(@tanstack/router-core@1.127.3)(csstype@3.1.3)(solid-js@1.9.5)(tiny-invariant@1.3.3)': - dependencies: - '@tanstack/router-core': 1.127.3 - clsx: 2.1.1 - goober: 2.1.16(csstype@3.1.3) - solid-js: 1.9.5 - tiny-invariant: 1.3.3 - optionalDependencies: - csstype: 3.1.3 - '@tanstack/router-devtools-core@1.127.3(@tanstack/router-core@1.127.3)(csstype@3.1.3)(solid-js@1.9.5)(tiny-invariant@1.3.3)': dependencies: '@tanstack/router-core': 1.127.3 @@ -18465,8 +18245,6 @@ snapshots: '@tanstack/store@0.7.0': {} - '@tanstack/store@0.7.1': {} - '@tanstack/store@0.7.2': {} '@tanstack/virtual-core@3.13.6': {} @@ -23885,13 +23663,6 @@ snapshots: regex: 6.0.1 regex-recursion: 6.0.2 - open@10.1.2: - dependencies: - default-browser: 5.2.1 - define-lazy-prop: 3.0.0 - is-inside-container: 1.0.0 - is-wsl: 3.1.0 - open@10.2.0: dependencies: default-browser: 5.2.1 @@ -25761,17 +25532,6 @@ snapshots: interpret: 1.4.0 rechoir: 0.6.2 - shiki@3.7.0: - dependencies: - '@shikijs/core': 3.7.0 - '@shikijs/engine-javascript': 3.7.0 - '@shikijs/engine-oniguruma': 3.7.0 - '@shikijs/langs': 3.7.0 - '@shikijs/themes': 3.7.0 - '@shikijs/types': 3.7.0 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - shiki@3.8.0: dependencies: '@shikijs/core': 3.8.0