diff --git a/apps/server/src/handlers/applySpaceEvent.ts b/apps/server/src/handlers/applySpaceEvent.ts index e4ce2089..c2bf7c21 100644 --- a/apps/server/src/handlers/applySpaceEvent.ts +++ b/apps/server/src/handlers/applySpaceEvent.ts @@ -1,9 +1,10 @@ import { Effect, Exit } from 'effect'; import type { Messages } from '@graphprotocol/hypergraph'; -import { SpaceEvents } from '@graphprotocol/hypergraph'; +import { Identity, SpaceEvents } from '@graphprotocol/hypergraph'; import { prisma } from '../prisma.js'; +import { getIdentity } from './getIdentity.js'; type Params = { accountId: string; @@ -36,7 +37,28 @@ export async function applySpaceEvent({ accountId, spaceId, event, keyBoxes }: P orderBy: { counter: 'desc' }, }); - const result = await Effect.runPromiseExit(SpaceEvents.applyEvent({ event, state: JSON.parse(lastEvent.state) })); + const getVerifiedIdentity = (accountIdToFetch: string) => { + // applySpaceEvent is only allowed to be called by the account that is applying the event + if (accountIdToFetch !== accountId) { + return Effect.fail(new Identity.InvalidIdentityError()); + } + + return Effect.gen(function* () { + const identity = yield* Effect.tryPromise({ + try: () => getIdentity({ accountId: accountIdToFetch }), + catch: () => new Identity.InvalidIdentityError(), + }); + return identity; + }); + }; + + const result = await Effect.runPromiseExit( + SpaceEvents.applyEvent({ + event, + state: JSON.parse(lastEvent.state), + getVerifiedIdentity, + }), + ); if (Exit.isFailure(result)) { console.log('Failed to apply event', result); throw new Error('Invalid event'); diff --git a/apps/server/src/handlers/createSpace.ts b/apps/server/src/handlers/createSpace.ts index e0a2b5b9..064b0bfa 100644 --- a/apps/server/src/handlers/createSpace.ts +++ b/apps/server/src/handlers/createSpace.ts @@ -2,9 +2,10 @@ import { Effect, Exit } from 'effect'; import type { Messages } from '@graphprotocol/hypergraph'; -import { SpaceEvents } from '@graphprotocol/hypergraph'; +import { Identity, SpaceEvents } from '@graphprotocol/hypergraph'; import { prisma } from '../prisma.js'; +import { getIdentity } from './getIdentity.js'; type Params = { accountId: string; @@ -14,7 +15,22 @@ type Params = { }; export const createSpace = async ({ accountId, event, keyBox, keyId }: Params) => { - const result = await Effect.runPromiseExit(SpaceEvents.applyEvent({ event, state: undefined })); + const getVerifiedIdentity = (accountIdToFetch: string) => { + // applySpaceEvent is only allowed to be called by the account that is applying the event + if (accountIdToFetch !== accountId) { + return Effect.fail(new Identity.InvalidIdentityError()); + } + + return Effect.gen(function* () { + const identity = yield* Effect.tryPromise({ + try: () => getIdentity({ accountId: accountIdToFetch }), + catch: () => new Identity.InvalidIdentityError(), + }); + return identity; + }); + }; + + const result = await Effect.runPromiseExit(SpaceEvents.applyEvent({ event, state: undefined, getVerifiedIdentity })); if (Exit.isFailure(result)) { throw new Error('Invalid event'); } diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 31115e28..10e1708a 100755 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -366,8 +366,26 @@ webSocketServer.on('connection', async (webSocket: CustomWebSocket, request: Req break; } case 'create-space-event': { + const getVerifiedIdentity = (accountIdToFetch: string) => { + if (accountIdToFetch !== accountId) { + return Effect.fail(new Identity.InvalidIdentityError()); + } + + return Effect.gen(function* () { + const identity = yield* Effect.tryPromise({ + try: () => getIdentity({ accountId: accountIdToFetch }), + catch: () => new Identity.InvalidIdentityError(), + }); + return identity; + }); + }; + const applyEventResult = await Effect.runPromiseExit( - SpaceEvents.applyEvent({ event: data.event, state: undefined }), + SpaceEvents.applyEvent({ + event: data.event, + state: undefined, + getVerifiedIdentity, + }), ); if (Exit.isSuccess(applyEventResult)) { const space = await createSpace({ accountId, event: data.event, keyBox: data.keyBox, keyId: data.keyId }); diff --git a/packages/hypergraph-react/src/HypergraphAppContext.tsx b/packages/hypergraph-react/src/HypergraphAppContext.tsx index 6f1ba722..2fb3099e 100644 --- a/packages/hypergraph-react/src/HypergraphAppContext.tsx +++ b/packages/hypergraph-react/src/HypergraphAppContext.tsx @@ -35,7 +35,7 @@ export type HypergraphAppCtx = { acceptInvitation(params: Readonly<{ invitation: Messages.Invitation }>): Promise; subscribeToSpace(params: Readonly<{ spaceId: string }>): void; inviteToSpace(params: Readonly<{ space: SpaceStorageEntry; invitee: { accountId: Address } }>): Promise; - getUserIdentity(accountId: string): Promise<{ + getVerifiedIdentity(accountId: string): Promise<{ accountId: string; encryptionPublicKey: string; signaturePublicKey: string; @@ -44,28 +44,36 @@ export type HypergraphAppCtx = { }; export const HypergraphAppContext = createContext({ - async login() {}, - logout() {}, - setIdentityAndSessionToken() {}, + async login() { + throw new Error('login is missing'); + }, + logout() { + throw new Error('logout is missing'); + }, + setIdentityAndSessionToken() { + throw new Error('setIdentityAndSessionToken is missing'); + }, invitations: [], async createSpace() { - return {}; + throw new Error('createSpace is missing'); + }, + listSpaces() { + throw new Error('listSpaces is missing'); + }, + listInvitations() { + throw new Error('listInvitations is missing'); }, - listSpaces() {}, - listInvitations() {}, async acceptInvitation() { - return {}; + throw new Error('acceptInvitation is missing'); + }, + subscribeToSpace() { + throw new Error('subscribeToSpace is missing'); }, - subscribeToSpace() {}, async inviteToSpace() { - return {}; + throw new Error('inviteToSpace is missing'); }, - async getUserIdentity() { - return { - accountId: '', - encryptionPublicKey: '', - signaturePublicKey: '', - }; + async getVerifiedIdentity() { + throw new Error('getVerifiedIdentity is missing'); }, loading: true, }); @@ -294,7 +302,7 @@ export function HypergraphAppProvider({ signature: update.signature, accountId: update.accountId, }); - const authorIdentity = await getUserIdentity(update.accountId); + const authorIdentity = await Identity.getVerifiedIdentity(update.accountId, syncServerUri); if (authorIdentity.signaturePublicKey !== signer) { console.error( `Received invalid signature, recovered signer is ${signer}, @@ -325,6 +333,16 @@ export function HypergraphAppProvider({ }); }; + const getVerifiedIdentity = (accountId: string) => { + return Effect.gen(function* () { + const identity = yield* Effect.tryPromise({ + try: () => Identity.getVerifiedIdentity(accountId, syncServerUri), + catch: () => new Identity.InvalidIdentityError(), + }); + return identity; + }); + }; + const onMessage = async (event: MessageEvent) => { const data = Messages.deserialize(event.data); const message = decodeResponseMessage(data); @@ -346,7 +364,7 @@ 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 })); + await Effect.runPromiseExit(SpaceEvents.applyEvent({ state, event, getVerifiedIdentity })); if (Exit.isSuccess(applyEventResult)) { state = applyEventResult.value; } else { @@ -428,7 +446,7 @@ export function HypergraphAppProvider({ } const applyEventResult = await Effect.runPromiseExit( - SpaceEvents.applyEvent({ event: response.event, state: space.state }), + SpaceEvents.applyEvent({ event: response.event, state: space.state, getVerifiedIdentity }), ); if (Exit.isSuccess(applyEventResult)) { store.send({ @@ -488,7 +506,7 @@ export function HypergraphAppProvider({ return () => { websocketConnection.removeEventListener('message', onMessage); }; - }, [websocketConnection, spaces, accountId, keys?.encryptionPrivateKey, keys?.signaturePrivateKey]); + }, [websocketConnection, spaces, accountId, keys?.encryptionPrivateKey, keys?.signaturePrivateKey, syncServerUri]); const createSpaceForContext = async () => { if (!accountId) { @@ -593,54 +611,6 @@ export function HypergraphAppProvider({ [websocketConnection], ); - const getUserIdentity = async ( - accountId: string, - ): Promise<{ - accountId: string; - encryptionPublicKey: string; - signaturePublicKey: string; - }> => { - const storeState = store.getSnapshot(); - const identity = storeState.context.userIdentities[accountId]; - if (identity) { - return { - accountId, - encryptionPublicKey: identity.encryptionPublicKey, - signaturePublicKey: identity.signaturePublicKey, - }; - } - const res = await fetch(`${syncServerUri}/identity?accountId=${accountId}`); - if (res.status !== 200) { - throw new Error('Failed to fetch identity'); - } - const resDecoded = Schema.decodeUnknownSync(Messages.ResponseIdentity)(await res.json()); - - if ( - !(await Identity.verifyIdentityOwnership( - resDecoded.accountId, - resDecoded.signaturePublicKey, - resDecoded.accountProof, - resDecoded.keyProof, - )) - ) { - throw new Error('Invalid identity'); - } - - store.send({ - type: 'addUserIdentity', - accountId: resDecoded.accountId, - encryptionPublicKey: resDecoded.encryptionPublicKey, - signaturePublicKey: resDecoded.signaturePublicKey, - accountProof: resDecoded.accountProof, - keyProof: resDecoded.keyProof, - }); - return { - accountId: resDecoded.accountId, - encryptionPublicKey: resDecoded.encryptionPublicKey, - signaturePublicKey: resDecoded.signaturePublicKey, - }; - }; - const inviteToSpace = async ({ space, invitee, @@ -667,7 +637,7 @@ export function HypergraphAppProvider({ console.error('No state found for space'); return; } - const inviteeWithKeys = await getUserIdentity(invitee.accountId); + const inviteeWithKeys = await Identity.getVerifiedIdentity(invitee.accountId, syncServerUri); const spaceEvent = await Effect.runPromiseExit( SpaceEvents.createInvitation({ author: { @@ -709,6 +679,13 @@ export function HypergraphAppProvider({ websocketConnection?.send(Messages.serialize(message)); }; + const getVerifiedIdentity = useCallback( + (accountId: string) => { + return Identity.getVerifiedIdentity(accountId, syncServerUri); + }, + [syncServerUri], + ); + return ( => { + const storeState = store.getSnapshot(); + const identity = storeState.context.identities[accountId]; + if (identity) { + return { + accountId, + encryptionPublicKey: identity.encryptionPublicKey, + signaturePublicKey: identity.signaturePublicKey, + }; + } + const res = await fetch(`${syncServerUri}/identity?accountId=${accountId}`); + if (res.status !== 200) { + throw new Error('Failed to fetch identity'); + } + const resDecoded = Schema.decodeUnknownSync(Messages.ResponseIdentity)(await res.json()); + + if ( + !(await verifyIdentityOwnership( + resDecoded.accountId, + resDecoded.signaturePublicKey, + resDecoded.accountProof, + resDecoded.keyProof, + )) + ) { + throw new Error('Invalid identity'); + } + + store.send({ + type: 'addVerifiedIdentity', + accountId: resDecoded.accountId, + encryptionPublicKey: resDecoded.encryptionPublicKey, + signaturePublicKey: resDecoded.signaturePublicKey, + accountProof: resDecoded.accountProof, + keyProof: resDecoded.keyProof, + }); + return { + accountId: resDecoded.accountId, + encryptionPublicKey: resDecoded.encryptionPublicKey, + signaturePublicKey: resDecoded.signaturePublicKey, + }; +}; diff --git a/packages/hypergraph/src/identity/index.ts b/packages/hypergraph/src/identity/index.ts index 13d83182..45161bd5 100644 --- a/packages/hypergraph/src/identity/index.ts +++ b/packages/hypergraph/src/identity/index.ts @@ -1,5 +1,6 @@ export * from './auth-storage.js'; export * from './create-identity-keys.js'; +export * from './get-verified-identity.js'; export * from './identity-encryption.js'; export * from './login.js'; export * from './prove-ownership.js'; diff --git a/packages/hypergraph/src/identity/types.ts b/packages/hypergraph/src/identity/types.ts index 00230d85..8d5e617a 100644 --- a/packages/hypergraph/src/identity/types.ts +++ b/packages/hypergraph/src/identity/types.ts @@ -32,3 +32,13 @@ export type KeysSchema = Schema.Schema.Type; export type Identity = IdentityKeys & { accountId: string; }; + +export type PublicIdentity = { + accountId: string; + encryptionPublicKey: string; + signaturePublicKey: string; +}; + +export class InvalidIdentityError { + readonly _tag = 'InvalidIdentityError'; +} diff --git a/packages/hypergraph/src/space-events/apply-event.ts b/packages/hypergraph/src/space-events/apply-event.ts index 55e1bb46..dfb90a1d 100644 --- a/packages/hypergraph/src/space-events/apply-event.ts +++ b/packages/hypergraph/src/space-events/apply-event.ts @@ -1,6 +1,7 @@ import { secp256k1 } from '@noble/curves/secp256k1'; import { sha256 } from '@noble/hashes/sha256'; import { Effect, Schema } from 'effect'; +import type { InvalidIdentityError, PublicIdentity } from '../identity/types.js'; import { canonicalize, stringToUint8Array } from '../utils/index.js'; import { hashEvent } from './hash-event.js'; import { @@ -16,11 +17,16 @@ import { type Params = { state: SpaceState | undefined; event: SpaceEvent; + getVerifiedIdentity: (accountId: string) => Effect.Effect; }; const decodeSpaceEvent = Schema.decodeUnknownEither(SpaceEvent); -export const applyEvent = ({ state, event: rawEvent }: Params): Effect.Effect => { +export const applyEvent = ({ + state, + event: rawEvent, + getVerifiedIdentity, +}: Params): Effect.Effect => { const decodedEvent = decodeSpaceEvent(rawEvent); if (decodedEvent._tag === 'Left') { return decodedEvent.left; @@ -40,90 +46,92 @@ export const applyEvent = ({ state, event: rawEvent }: Params): Effect.Effect invitation.inviteeAccountId === event.author.accountId, - ); - if (!result) { - return Effect.fail(new InvalidEventError()); - } - const [id, invitation] = result; + let id = ''; + let members: { [accountId: string]: SpaceMember } = {}; + let removedMembers: { [accountId: string]: SpaceMember } = {}; + let invitations: { [id: string]: SpaceInvitation } = {}; - members[invitation.inviteeAccountId] = { - accountId: invitation.inviteeAccountId, - role: 'member', + if (event.transaction.type === 'create-space') { + id = event.transaction.id; + members[event.transaction.creatorAccountId] = { + accountId: event.transaction.creatorAccountId, + role: 'admin', }; - delete invitations[id]; - if (removedMembers[event.author.accountId] !== undefined) { - delete removedMembers[event.author.accountId]; - } - } else { - // check if the author is an admin - if (members[event.author.accountId]?.role !== 'admin') { - return Effect.fail(new InvalidEventError()); - } + } else if (state !== undefined) { + id = state.id; + members = { ...state.members }; + removedMembers = { ...state.removedMembers }; + invitations = { ...state.invitations }; - if (event.transaction.type === 'delete-space') { - removedMembers = { ...members }; - members = {}; - invitations = {}; - } else if (event.transaction.type === 'create-invitation') { - if (members[event.transaction.inviteeAccountId] !== undefined) { - return Effect.fail(new InvalidEventError()); + if (event.transaction.type === 'accept-invitation') { + // is already a member + if (members[event.author.accountId] !== undefined) { + yield* Effect.fail(new InvalidEventError()); } - for (const invitation of Object.values(invitations)) { - if (invitation.inviteeAccountId === event.transaction.inviteeAccountId) { - return Effect.fail(new InvalidEventError()); - } + + // find the invitation + const result = Object.entries(invitations).find( + ([, invitation]) => invitation.inviteeAccountId === event.author.accountId, + ); + if (!result) { + yield* Effect.fail(new InvalidEventError()); } - invitations[event.transaction.id] = { - inviteeAccountId: event.transaction.inviteeAccountId, + // @ts-expect-error type issue? we checked that result is not undefined before + const [id, invitation] = result; + + members[invitation.inviteeAccountId] = { + accountId: invitation.inviteeAccountId, + role: 'member', }; + delete invitations[id]; + if (removedMembers[event.author.accountId] !== undefined) { + delete removedMembers[event.author.accountId]; + } } else { - throw new Error('State is required for all events except create-space'); + // check if the author is an admin + if (members[event.author.accountId]?.role !== 'admin') { + yield* Effect.fail(new InvalidEventError()); + } + + if (event.transaction.type === 'delete-space') { + removedMembers = { ...members }; + members = {}; + invitations = {}; + } else if (event.transaction.type === 'create-invitation') { + if (members[event.transaction.inviteeAccountId] !== undefined) { + yield* Effect.fail(new InvalidEventError()); + } + for (const invitation of Object.values(invitations)) { + if (invitation.inviteeAccountId === event.transaction.inviteeAccountId) { + yield* Effect.fail(new InvalidEventError()); + } + } + + invitations[event.transaction.id] = { + inviteeAccountId: event.transaction.inviteeAccountId, + }; + } else { + // state is required for all events except create-space + yield* Effect.fail(new InvalidEventError()); + } } } - } - return Effect.succeed({ - id, - members, - removedMembers, - invitations, - lastEventHash: hashEvent(event), + return { + id, + members, + removedMembers, + invitations, + lastEventHash: hashEvent(event), + }; }); }; diff --git a/packages/hypergraph/src/space-events/types.ts b/packages/hypergraph/src/space-events/types.ts index 5ff10126..47fab696 100644 --- a/packages/hypergraph/src/space-events/types.ts +++ b/packages/hypergraph/src/space-events/types.ts @@ -1,5 +1,6 @@ import type { ParseError } from 'effect/ParseResult'; import * as Schema from 'effect/Schema'; +import { InvalidIdentityError } from '../identity/types.js'; import { SignatureWithRecovery } from '../types.js'; export const EventAuthor = Schema.Struct({ @@ -103,4 +104,4 @@ export class InvalidEventError { readonly _tag = 'InvalidEventError'; } -export type ApplyError = ParseError | VerifySignatureError | InvalidEventError; +export type ApplyError = ParseError | VerifySignatureError | InvalidEventError | InvalidIdentityError; diff --git a/packages/hypergraph/src/store.ts b/packages/hypergraph/src/store.ts index 9f2b44d0..639dc82a 100644 --- a/packages/hypergraph/src/store.ts +++ b/packages/hypergraph/src/store.ts @@ -21,7 +21,7 @@ interface StoreContext { updatesInFlight: string[]; invitations: Invitation[]; repo: Repo; - userIdentities: { + identities: { [accountId: string]: { encryptionPublicKey: string; signaturePublicKey: string; @@ -40,7 +40,7 @@ const initialStoreContext: StoreContext = { updatesInFlight: [], invitations: [], repo: new Repo({}), - userIdentities: {}, + identities: {}, authenticated: false, accountId: null, sessionToken: null, @@ -57,7 +57,7 @@ type StoreEvent = | { type: 'updateConfirmed'; spaceId: string; clock: number } | { type: 'applyUpdate'; spaceId: string; firstUpdateClock: number; lastUpdateClock: number } | { - type: 'addUserIdentity'; + type: 'addVerifiedIdentity'; accountId: string; encryptionPublicKey: string; signaturePublicKey: string; @@ -193,7 +193,7 @@ export const store: Store = create }), }; }, - addUserIdentity: ( + addVerifiedIdentity: ( context, event: { accountId: string; @@ -205,8 +205,8 @@ export const store: Store = create ) => { return { ...context, - userIdentities: { - ...context.userIdentities, + identities: { + ...context.identities, [event.accountId]: { encryptionPublicKey: event.encryptionPublicKey, signaturePublicKey: event.signaturePublicKey, diff --git a/packages/hypergraph/test/space-events/accept-invitation.test.ts b/packages/hypergraph/test/space-events/accept-invitation.test.ts index 28182404..6807f096 100644 --- a/packages/hypergraph/test/space-events/accept-invitation.test.ts +++ b/packages/hypergraph/test/space-events/accept-invitation.test.ts @@ -20,22 +20,29 @@ const invitee = { encryptionPublicKey: 'encryption', }; +const getVerifiedIdentity = (accountId: string) => { + if (accountId === author.accountId) { + return Effect.succeed(author); + } + return Effect.succeed(invitee); +}; + it('should accept an invitation', async () => { const { state3 } = await Effect.runPromise( Effect.gen(function* () { const spaceEvent = yield* createSpace({ author }); - const state = yield* applyEvent({ event: spaceEvent, state: undefined }); + const state = yield* applyEvent({ event: spaceEvent, state: undefined, getVerifiedIdentity }); const spaceEvent2 = yield* createInvitation({ author, previousEventHash: state.lastEventHash, invitee, }); - const state2 = yield* applyEvent({ event: spaceEvent2, state }); + const state2 = yield* applyEvent({ event: spaceEvent2, state, getVerifiedIdentity }); const spaceEvent3 = yield* acceptInvitation({ previousEventHash: state2.lastEventHash, author: invitee, }); - const state3 = yield* applyEvent({ event: spaceEvent3, state: state2 }); + const state3 = yield* applyEvent({ event: spaceEvent3, state: state2, getVerifiedIdentity }); return { state3, spaceEvent3, diff --git a/packages/hypergraph/test/space-events/apply-event.test.ts b/packages/hypergraph/test/space-events/apply-event.test.ts index 1ce01f65..1c48e8a5 100644 --- a/packages/hypergraph/test/space-events/apply-event.test.ts +++ b/packages/hypergraph/test/space-events/apply-event.test.ts @@ -5,6 +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 { 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'; @@ -24,6 +25,16 @@ const invitee = { encryptionPublicKey: 'encryption', }; +const getVerifiedIdentity = (accountId: string) => { + if (accountId === author.accountId) { + return Effect.succeed(author); + } + if (accountId === invitee.accountId) { + return Effect.succeed(invitee); + } + return Effect.fail(new InvalidIdentityError()); +}; + it('should fail in case of an invalid signature', async () => { const result = await Effect.runPromiseExit( Effect.gen(function* () { @@ -34,7 +45,7 @@ it('should fail in case of an invalid signature', async () => { // @ts-expect-error spaceEvent.author.signature = signature; - return yield* applyEvent({ event: spaceEvent, state: undefined }); + return yield* applyEvent({ event: spaceEvent, state: undefined, getVerifiedIdentity }); }), ); @@ -51,10 +62,10 @@ it('should fail in case state is not provided for an event other than createSpac const result = await Effect.runPromiseExit( Effect.gen(function* () { const spaceEvent = yield* createSpace({ author }); - const state = yield* applyEvent({ event: spaceEvent, state: undefined }); + const state = yield* applyEvent({ event: spaceEvent, state: undefined, getVerifiedIdentity }); const spaceEvent2 = yield* createInvitation({ author, previousEventHash: state.lastEventHash, invitee }); - return yield* applyEvent({ event: spaceEvent2, state: undefined }); + return yield* applyEvent({ event: spaceEvent2, state: undefined, getVerifiedIdentity }); }), ); @@ -71,13 +82,13 @@ it('should fail in case of an event is applied that is not based on the previous const result = await Effect.runPromiseExit( Effect.gen(function* () { const spaceEvent = yield* createSpace({ author }); - const state = yield* applyEvent({ event: spaceEvent, state: undefined }); + const state = yield* applyEvent({ event: spaceEvent, state: undefined, getVerifiedIdentity }); const spaceEvent2 = yield* createSpace({ author }); - const state2 = yield* applyEvent({ event: spaceEvent2, state }); + const state2 = yield* applyEvent({ event: spaceEvent2, state, getVerifiedIdentity }); const spaceEvent3 = yield* createInvitation({ author, previousEventHash: state.lastEventHash, invitee }); - return yield* applyEvent({ event: spaceEvent3, state: state2 }); + return yield* applyEvent({ event: spaceEvent3, state: state2, getVerifiedIdentity }); }), ); diff --git a/packages/hypergraph/test/space-events/create-invitation.test.ts b/packages/hypergraph/test/space-events/create-invitation.test.ts index d3460497..53d414cb 100644 --- a/packages/hypergraph/test/space-events/create-invitation.test.ts +++ b/packages/hypergraph/test/space-events/create-invitation.test.ts @@ -1,6 +1,7 @@ import { Cause, Effect, Exit } from 'effect'; import { expect, it } from 'vitest'; +import { InvalidIdentityError } 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'; @@ -28,17 +29,30 @@ const invitee2 = { encryptionPublicKey: 'encryption', }; +const getVerifiedIdentity = (accountId: string) => { + if (accountId === author.accountId) { + return Effect.succeed(author); + } + if (accountId === invitee.accountId) { + return Effect.succeed(invitee); + } + if (accountId === invitee2.accountId) { + return Effect.succeed(invitee2); + } + return Effect.fail(new InvalidIdentityError()); +}; + it('should create an invitation', async () => { const { spaceEvent2, state2 } = await Effect.runPromise( Effect.gen(function* () { const spaceEvent = yield* createSpace({ author }); - const state = yield* applyEvent({ event: spaceEvent, state: undefined }); + const state = yield* applyEvent({ event: spaceEvent, state: undefined, getVerifiedIdentity }); const spaceEvent2 = yield* createInvitation({ author, previousEventHash: state.lastEventHash, invitee, }); - const state2 = yield* applyEvent({ event: spaceEvent2, state }); + const state2 = yield* applyEvent({ event: spaceEvent2, state, getVerifiedIdentity }); return { state2, spaceEvent2, @@ -66,19 +80,19 @@ it('should fail to invite the account twice', async () => { const result = await Effect.runPromiseExit( Effect.gen(function* () { const spaceEvent = yield* createSpace({ author }); - const state = yield* applyEvent({ event: spaceEvent, state: undefined }); + const state = yield* applyEvent({ event: spaceEvent, state: undefined, getVerifiedIdentity }); const spaceEvent2 = yield* createInvitation({ author, previousEventHash: state.lastEventHash, invitee, }); - const state2 = yield* applyEvent({ event: spaceEvent2, state }); + const state2 = yield* applyEvent({ event: spaceEvent2, state, getVerifiedIdentity }); const spaceEvent3 = yield* createInvitation({ author, previousEventHash: state.lastEventHash, invitee, }); - return yield* applyEvent({ state: state2, event: spaceEvent3 }); + return yield* applyEvent({ state: state2, event: spaceEvent3, getVerifiedIdentity }); }), ); @@ -89,13 +103,13 @@ it('should fail to invite an account that is already a member', async () => { const result = await Effect.runPromiseExit( Effect.gen(function* () { const spaceEvent = yield* createSpace({ author }); - const state = yield* applyEvent({ event: spaceEvent, state: undefined }); + const state = yield* applyEvent({ event: spaceEvent, state: undefined, getVerifiedIdentity }); const spaceEvent2 = yield* createInvitation({ author, previousEventHash: state.lastEventHash, invitee: author, // inviting the author }); - yield* applyEvent({ event: spaceEvent2, state }); + yield* applyEvent({ event: spaceEvent2, state, getVerifiedIdentity }); }), ); @@ -106,24 +120,24 @@ it('should fail in case the author is not an admin', async () => { const result = await Effect.runPromiseExit( Effect.gen(function* () { const spaceEvent = yield* createSpace({ author }); - const state = yield* applyEvent({ event: spaceEvent, state: undefined }); + const state = yield* applyEvent({ event: spaceEvent, state: undefined, getVerifiedIdentity }); const spaceEvent2 = yield* createInvitation({ author, previousEventHash: state.lastEventHash, invitee, }); - const state2 = yield* applyEvent({ event: spaceEvent2, state }); + const state2 = yield* applyEvent({ event: spaceEvent2, state, getVerifiedIdentity }); const spaceEvent3 = yield* acceptInvitation({ previousEventHash: state2.lastEventHash, author: invitee, }); - const state3 = yield* applyEvent({ event: spaceEvent3, state: state2 }); + const state3 = yield* applyEvent({ event: spaceEvent3, state: state2, getVerifiedIdentity }); const spaceEvent4 = yield* createInvitation({ author: invitee, previousEventHash: state.lastEventHash, invitee: invitee2, }); - yield* applyEvent({ event: spaceEvent4, state: state3 }); + yield* applyEvent({ event: spaceEvent4, state: state3, getVerifiedIdentity }); }), ); diff --git a/packages/hypergraph/test/space-events/create-space.test.ts b/packages/hypergraph/test/space-events/create-space.test.ts index dc7fff30..8cfc8f98 100644 --- a/packages/hypergraph/test/space-events/create-space.test.ts +++ b/packages/hypergraph/test/space-events/create-space.test.ts @@ -1,6 +1,7 @@ import { Effect } from 'effect'; import { expect, it } from 'vitest'; +import { InvalidIdentityError } from '../../src/identity/types.js'; import { applyEvent } from '../../src/space-events/apply-event.js'; import { createSpace } from '../../src/space-events/create-space.js'; @@ -12,10 +13,17 @@ it('should create a space state', async () => { encryptionPublicKey: 'encryption', }; + const getVerifiedIdentity = (accountId: string) => { + if (accountId === author.accountId) { + return Effect.succeed(author); + } + return Effect.fail(new InvalidIdentityError()); + }; + const state = await Effect.runPromise( Effect.gen(function* () { const spaceEvent = yield* createSpace({ author }); - return yield* applyEvent({ event: spaceEvent, state: undefined }); + return yield* applyEvent({ event: spaceEvent, state: undefined, getVerifiedIdentity }); }), ); diff --git a/packages/hypergraph/test/space-events/delete-space.test.ts b/packages/hypergraph/test/space-events/delete-space.test.ts index 0ccea828..17f4fe44 100644 --- a/packages/hypergraph/test/space-events/delete-space.test.ts +++ b/packages/hypergraph/test/space-events/delete-space.test.ts @@ -1,6 +1,7 @@ import { Cause, Effect, Exit } from 'effect'; import { expect, it } from 'vitest'; +import { InvalidIdentityError } 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'; @@ -22,13 +23,23 @@ const invitee = { encryptionPublicKey: 'encryption', }; +const getVerifiedIdentity = (accountId: string) => { + if (accountId === author.accountId) { + return Effect.succeed(author); + } + if (accountId === invitee.accountId) { + return Effect.succeed(invitee); + } + return Effect.fail(new InvalidIdentityError()); +}; + it('should delete a space', async () => { const state = await Effect.runPromise( Effect.gen(function* () { const spaceEvent = yield* createSpace({ author }); - const state = yield* applyEvent({ event: spaceEvent, state: undefined }); + const state = yield* applyEvent({ event: spaceEvent, state: undefined, getVerifiedIdentity }); const spaceEvent2 = yield* deleteSpace({ author, id: state.id, previousEventHash: state.lastEventHash }); - return yield* applyEvent({ event: spaceEvent2, state }); + return yield* applyEvent({ event: spaceEvent2, state, getVerifiedIdentity }); }), ); @@ -48,24 +59,24 @@ it('should fail in case the author is not an admin', async () => { const result = await Effect.runPromiseExit( Effect.gen(function* () { const spaceEvent = yield* createSpace({ author }); - const state = yield* applyEvent({ event: spaceEvent, state: undefined }); + const state = yield* applyEvent({ event: spaceEvent, state: undefined, getVerifiedIdentity }); const spaceEvent2 = yield* createInvitation({ author, previousEventHash: state.lastEventHash, invitee, }); - const state2 = yield* applyEvent({ event: spaceEvent2, state }); + const state2 = yield* applyEvent({ event: spaceEvent2, state, getVerifiedIdentity }); const spaceEvent3 = yield* acceptInvitation({ previousEventHash: state2.lastEventHash, author: invitee, }); - const state3 = yield* applyEvent({ event: spaceEvent3, state: state2 }); + const state3 = yield* applyEvent({ event: spaceEvent3, state: state2, getVerifiedIdentity }); const spaceEvent4 = yield* deleteSpace({ author: invitee, previousEventHash: state.lastEventHash, id: state.id, }); - yield* applyEvent({ event: spaceEvent4, state: state3 }); + yield* applyEvent({ event: spaceEvent4, state: state3, getVerifiedIdentity }); }), );