Skip to content

Commit a730fdc

Browse files
committed
add space key
1 parent 5599b30 commit a730fdc

File tree

25 files changed

+1000
-54
lines changed

25 files changed

+1000
-54
lines changed

apps/events/src/routes/playground.tsx

Lines changed: 107 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,60 @@ import { DebugSpaceEvents } from '@/components/debug-space-events';
33
import { DebugSpaceState } from '@/components/debug-space-state';
44
import { Button } from '@/components/ui/button';
55
import { assertExhaustive } from '@/lib/assertExhaustive';
6+
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
67
import { createFileRoute } from '@tanstack/react-router';
78
import { Effect, Exit } from 'effect';
89
import * as Schema from 'effect/Schema';
910
import type {
1011
EventMessage,
1112
Invitation,
13+
RequestCreateInvitationEvent,
14+
RequestCreateSpaceEvent,
1215
RequestListInvitations,
1316
RequestListSpaces,
1417
RequestSubscribeToSpace,
1518
SpaceEvent,
1619
SpaceState,
1720
} from 'graph-framework';
18-
import { ResponseMessage, acceptInvitation, applyEvent, createInvitation, createSpace } from 'graph-framework';
21+
import {
22+
ResponseMessage,
23+
acceptInvitation,
24+
applyEvent,
25+
createInvitation,
26+
createKey,
27+
createSpace,
28+
decryptKey,
29+
encryptKey,
30+
generateId,
31+
} from 'graph-framework';
1932
import { useEffect, useState } from 'react';
2033

2134
const availableAccounts = [
2235
{
2336
accountId: '0262701b2eb1b6b37ad03e24445dfcad1b91309199e43017b657ce2604417c12f5',
2437
signaturePrivateKey: '88bb6f20de8dc1787c722dc847f4cf3d00285b8955445f23c483d1237fe85366',
38+
encryptionPrivateKey: 'bbf164a93b0f78a85346017fa2673cf367c64d81b1c3d6af7ad45e308107a812',
39+
encryptionPublicKey: '595e1a6b0bb346d83bc382998943d2e6d9210fd341bc8b9f41a7229eede27240',
2540
},
2641
{
2742
accountId: '03bf5d2a1badf15387b08a007d1a9a13a9bfd6e1c56f681e251514d9ba10b57462',
2843
signaturePrivateKey: '1eee32d3bc202dcb5d17c3b1454fb541d2290cb941860735408f1bfe39e7bc15',
44+
encryptionPrivateKey: 'b32478dc6f40482127a09d0f1cabbf45dc83ebce638d6246f5552191009fda2c',
45+
encryptionPublicKey: '0f4e22dc85167597af85cba85988770cd77c25d317f2b14a1f49a54efcbfae3f',
2946
},
3047
{
3148
accountId: '0351460706cf386282d9b6ebee2ccdcb9ba61194fd024345e53037f3036242e6a2',
3249
signaturePrivateKey: '434518a2c9a665a7c20da086232c818b6c1592e2edfeecab29a40cf5925ca8fe',
50+
encryptionPrivateKey: 'aaf71397e44fc57b42eaad5b0869d1e0247b4a7f2fe9ec5cc00dec3815849e7a',
51+
encryptionPublicKey: 'd494144358a610604c4ab453b442d014f2843772eed19be155dd9fc55fe8a332',
3352
},
3453
];
3554

3655
type SpaceStorageEntry = {
3756
id: string;
3857
events: SpaceEvent[];
3958
state: SpaceState | undefined;
59+
keys: { id: string; key: string }[];
4060
};
4161

4262
const decodeResponseMessage = Schema.decodeUnknownEither(ResponseMessage);
@@ -45,7 +65,17 @@ export const Route = createFileRoute('/playground')({
4565
component: () => <ChooseAccount />,
4666
});
4767

48-
const App = ({ accountId, signaturePrivateKey }: { accountId: string; signaturePrivateKey: string }) => {
68+
const App = ({
69+
accountId,
70+
signaturePrivateKey,
71+
encryptionPublicKey,
72+
encryptionPrivateKey,
73+
}: {
74+
accountId: string;
75+
signaturePrivateKey: string;
76+
encryptionPrivateKey: string;
77+
encryptionPublicKey: string;
78+
}) => {
4979
const [websocketConnection, setWebsocketConnection] = useState<WebSocket>();
5080
const [spaces, setSpaces] = useState<SpaceStorageEntry[]>([]);
5181
const [invitations, setInvitations] = useState<Invitation[]>([]);
@@ -66,7 +96,12 @@ const App = ({ accountId, signaturePrivateKey }: { accountId: string; signatureP
6696
setSpaces((existingSpaces) => {
6797
return response.spaces.map((space) => {
6898
const existingSpace = existingSpaces.find((s) => s.id === space.id);
69-
return { id: space.id, events: existingSpace?.events ?? [], state: existingSpace?.state };
99+
return {
100+
id: space.id,
101+
events: existingSpace?.events ?? [],
102+
state: existingSpace?.state,
103+
keys: existingSpace?.keys ?? [],
104+
};
70105
});
71106
});
72107
// fetch all spaces (for debugging purposes)
@@ -96,11 +131,26 @@ const App = ({ accountId, signaturePrivateKey }: { accountId: string; signatureP
96131

97132
const newState = state as SpaceState;
98133

134+
const keys = response.keyBoxes.map((keyBox) => {
135+
const key = decryptKey({
136+
keyBoxCiphertext: hexToBytes(keyBox.ciphertext),
137+
keyBoxNonce: hexToBytes(keyBox.nonce),
138+
publicKey: hexToBytes(keyBox.authorPublicKey),
139+
privateKey: hexToBytes(encryptionPrivateKey),
140+
});
141+
return { id: keyBox.id, key: bytesToHex(key) };
142+
});
143+
99144
setSpaces((spaces) =>
100145
spaces.map((space) => {
101146
if (space.id === response.id) {
102147
// TODO fix readonly type issue
103-
return { ...space, events: response.events as SpaceEvent[], state: newState };
148+
return {
149+
...space,
150+
events: response.events as SpaceEvent[],
151+
state: newState,
152+
keys,
153+
};
104154
}
105155
return space;
106156
}),
@@ -144,7 +194,7 @@ const App = ({ accountId, signaturePrivateKey }: { accountId: string; signatureP
144194
websocketConnection.removeEventListener('close', onClose);
145195
websocketConnection.close();
146196
};
147-
}, [accountId]);
197+
}, [accountId, encryptionPrivateKey]);
148198

149199
return (
150200
<>
@@ -154,13 +204,28 @@ const App = ({ accountId, signaturePrivateKey }: { accountId: string; signatureP
154204
const spaceEvent = await Effect.runPromise(
155205
createSpace({
156206
author: {
157-
encryptionPublicKey: 'TODO',
207+
encryptionPublicKey,
158208
signaturePrivateKey,
159209
signaturePublicKey: accountId,
160210
},
161211
}),
162212
);
163-
const message: EventMessage = { type: 'event', event: spaceEvent, spaceId: spaceEvent.transaction.id };
213+
const result = createKey({
214+
privateKey: hexToBytes(encryptionPrivateKey),
215+
publicKey: hexToBytes(encryptionPublicKey),
216+
});
217+
const message: RequestCreateSpaceEvent = {
218+
type: 'create-space-event',
219+
event: spaceEvent,
220+
spaceId: spaceEvent.transaction.id,
221+
keyId: generateId(),
222+
keyBox: {
223+
accountId,
224+
ciphertext: bytesToHex(result.keyBoxCiphertext),
225+
nonce: bytesToHex(result.keyBoxNonce),
226+
authorPublicKey: encryptionPublicKey,
227+
},
228+
};
164229
websocketConnection?.send(JSON.stringify(message));
165230
}}
166231
>
@@ -193,7 +258,7 @@ const App = ({ accountId, signaturePrivateKey }: { accountId: string; signatureP
193258
acceptInvitation({
194259
author: {
195260
signaturePublicKey: accountId,
196-
encryptionPublicKey: 'TODO',
261+
encryptionPublicKey,
197262
signaturePrivateKey,
198263
},
199264
previousEventHash: invitation.previousEventHash,
@@ -213,6 +278,8 @@ const App = ({ accountId, signaturePrivateKey }: { accountId: string; signatureP
213278
return (
214279
<li key={space.id}>
215280
<h3>Space id: {space.id}</h3>
281+
<p>Keys:</p>
282+
<pre className="text-xs">{JSON.stringify(space.keys)}</pre>
216283
<Button
217284
onClick={() => {
218285
const message: RequestSubscribeToSpace = { type: 'subscribe-space', id: space.id };
@@ -235,21 +302,42 @@ const App = ({ accountId, signaturePrivateKey }: { accountId: string; signatureP
235302
createInvitation({
236303
author: {
237304
signaturePublicKey: accountId,
238-
encryptionPublicKey: 'TODO',
305+
encryptionPublicKey,
239306
signaturePrivateKey,
240307
},
241308
previousEventHash: space.state.lastEventHash,
242309
invitee: {
243310
signaturePublicKey: invitee.accountId,
244-
encryptionPublicKey: 'TODO',
311+
encryptionPublicKey,
245312
},
246313
}),
247314
);
248315
if (Exit.isFailure(spaceEvent)) {
249316
console.error('Failed to create invitation', spaceEvent);
250317
return;
251318
}
252-
const message: EventMessage = { type: 'event', event: spaceEvent.value, spaceId: space.id };
319+
320+
const keyBoxes = space.keys.map((key) => {
321+
const keyBox = encryptKey({
322+
key: hexToBytes(key.key),
323+
publicKey: hexToBytes(invitee.encryptionPublicKey),
324+
privateKey: hexToBytes(encryptionPrivateKey),
325+
});
326+
return {
327+
id: key.id,
328+
ciphertext: bytesToHex(keyBox.keyBoxCiphertext),
329+
nonce: bytesToHex(keyBox.keyBoxNonce),
330+
authorPublicKey: encryptionPublicKey,
331+
accountId: invitee.accountId,
332+
};
333+
});
334+
335+
const message: RequestCreateInvitationEvent = {
336+
type: 'create-invitation-event',
337+
event: spaceEvent.value,
338+
spaceId: space.id,
339+
keyBoxes,
340+
};
253341
websocketConnection?.send(JSON.stringify(message));
254342
}}
255343
>
@@ -271,7 +359,12 @@ const App = ({ accountId, signaturePrivateKey }: { accountId: string; signatureP
271359
};
272360

273361
export const ChooseAccount = () => {
274-
const [account, setAccount] = useState<{ accountId: string; signaturePrivateKey: string } | null>();
362+
const [account, setAccount] = useState<{
363+
accountId: string;
364+
signaturePrivateKey: string;
365+
encryptionPrivateKey: string;
366+
encryptionPublicKey: string;
367+
} | null>();
275368

276369
return (
277370
<div>
@@ -305,6 +398,8 @@ export const ChooseAccount = () => {
305398
key={account.accountId}
306399
accountId={account.accountId}
307400
signaturePrivateKey={account.signaturePrivateKey}
401+
encryptionPrivateKey={account.encryptionPrivateKey}
402+
encryptionPublicKey={account.encryptionPublicKey}
308403
/>
309404
)}
310405
</div>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-- CreateTable
2+
CREATE TABLE "SpaceKey" (
3+
"id" TEXT NOT NULL PRIMARY KEY,
4+
"spaceId" TEXT NOT NULL,
5+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
6+
CONSTRAINT "SpaceKey_spaceId_fkey" FOREIGN KEY ("spaceId") REFERENCES "Space" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
7+
);
8+
9+
-- CreateTable
10+
CREATE TABLE "SpaceKeyBox" (
11+
"id" TEXT NOT NULL PRIMARY KEY,
12+
"spaceKeyId" TEXT NOT NULL,
13+
"accountId" TEXT NOT NULL,
14+
"ciphertext" TEXT NOT NULL,
15+
"nonce" TEXT NOT NULL,
16+
"authorPublicKey" TEXT NOT NULL,
17+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
18+
CONSTRAINT "SpaceKeyBox_spaceKeyId_fkey" FOREIGN KEY ("spaceKeyId") REFERENCES "SpaceKey" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
19+
CONSTRAINT "SpaceKeyBox_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
20+
);

apps/server/prisma/schema.prisma

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,34 @@ model Space {
2727
events SpaceEvent[]
2828
members Account[]
2929
invitations Invitation[]
30+
keys SpaceKey[]
31+
}
32+
33+
model SpaceKey {
34+
id String @id
35+
space Space @relation(fields: [spaceId], references: [id])
36+
spaceId String
37+
createdAt DateTime @default(now())
38+
keyBoxes SpaceKeyBox[]
39+
}
40+
41+
model SpaceKeyBox {
42+
id String @id
43+
spaceKey SpaceKey @relation(fields: [spaceKeyId], references: [id])
44+
spaceKeyId String
45+
account Account @relation(fields: [accountId], references: [id])
46+
accountId String
47+
ciphertext String
48+
nonce String
49+
authorPublicKey String
50+
createdAt DateTime @default(now())
3051
}
3152

3253
model Account {
33-
id String @id
54+
id String @id
3455
spaces Space[]
3556
invitations Invitation[]
57+
keyBoxes SpaceKeyBox[]
3658
}
3759

3860
model Invitation {

apps/server/src/handlers/applySpaceEvent.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Effect, Exit } from 'effect';
2+
import type { KeyBoxWithKeyId } from 'graph-framework-messages';
23
import type { SpaceEvent } from 'graph-framework-space-events';
34
import { applyEvent } from 'graph-framework-space-events';
45
import { prisma } from '../prisma.js';
@@ -7,9 +8,10 @@ type Params = {
78
accountId: string;
89
spaceId: string;
910
event: SpaceEvent;
11+
keyBoxes: KeyBoxWithKeyId[];
1012
};
1113

12-
export async function applySpaceEvent({ accountId, spaceId, event }: Params) {
14+
export async function applySpaceEvent({ accountId, spaceId, event, keyBoxes }: Params) {
1315
if (event.transaction.type === 'create-space') {
1416
throw new Error('applySpaceEvent does not support create-space events.');
1517
}
@@ -40,14 +42,25 @@ export async function applySpaceEvent({ accountId, spaceId, event }: Params) {
4042
}
4143

4244
if (event.transaction.type === 'create-invitation') {
45+
const inviteeAccountId = event.transaction.signaturePublicKey;
4346
await transaction.invitation.create({
4447
data: {
4548
id: event.transaction.id,
4649
spaceId,
4750
accountId: event.transaction.signaturePublicKey,
48-
inviteeAccountId: event.transaction.signaturePublicKey,
51+
inviteeAccountId,
4952
},
5053
});
54+
await transaction.spaceKeyBox.createMany({
55+
data: keyBoxes.map((keyBox) => ({
56+
id: `${keyBox.id}-${inviteeAccountId}`,
57+
nonce: keyBox.nonce,
58+
ciphertext: keyBox.ciphertext,
59+
accountId: inviteeAccountId,
60+
authorPublicKey: keyBox.authorPublicKey,
61+
spaceKeyId: keyBox.id,
62+
})),
63+
});
5164
}
5265
if (event.transaction.type === 'accept-invitation') {
5366
await transaction.invitation.delete({

apps/server/src/handlers/createSpace.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import { Effect, Exit } from 'effect';
2+
import type { KeyBox } from 'graph-framework-messages';
23
import { type CreateSpaceEvent, applyEvent } from 'graph-framework-space-events';
34
import { prisma } from '../prisma.js';
45

56
type Params = {
67
accountId: string;
78
event: CreateSpaceEvent;
9+
keyBox: KeyBox;
10+
keyId: string;
811
};
912

10-
export const createSpace = async ({ accountId, event }: Params) => {
13+
export const createSpace = async ({ accountId, event, keyBox, keyId }: Params) => {
1114
const result = await Effect.runPromiseExit(applyEvent({ event }));
1215
if (Exit.isFailure(result)) {
1316
throw new Error('Invalid event');
@@ -27,6 +30,20 @@ export const createSpace = async ({ accountId, event }: Params) => {
2730
id: accountId,
2831
},
2932
},
33+
keys: {
34+
create: {
35+
id: keyId,
36+
keyBoxes: {
37+
create: {
38+
id: `${keyId}-${accountId}`,
39+
nonce: keyBox.nonce,
40+
ciphertext: keyBox.ciphertext,
41+
accountId: accountId,
42+
authorPublicKey: keyBox.authorPublicKey,
43+
},
44+
},
45+
},
46+
},
3047
},
3148
},
3249
},

0 commit comments

Comments
 (0)