Skip to content

Commit 1e03c12

Browse files
committed
chore: validation, more server actions
1 parent d814cd2 commit 1e03c12

File tree

7 files changed

+238
-32
lines changed

7 files changed

+238
-32
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { Messages } from '@graphprotocol/hypergraph';
2+
import { prisma } from '../prisma';
3+
export const createAccountInbox = async (data: Messages.RequestCreateAccountInbox) => {
4+
const { accountId, inboxId, isPublic, authPolicy, encryptionPublicKey, signature } = data;
5+
// This will throw an error if the inbox already exists
6+
const inbox = await prisma.accountInbox.create({
7+
data: {
8+
id: inboxId,
9+
isPublic,
10+
authPolicy,
11+
encryptionPublicKey,
12+
signatureHex: signature.hex,
13+
signatureRecovery: signature.recovery,
14+
account: { connect: { id: accountId } },
15+
},
16+
});
17+
return inbox;
18+
};

apps/server/src/handlers/createAccountInboxMessage.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ export const createAccountInboxMessage = async (params: Params) => {
2424
ciphertext: message.ciphertext,
2525
nonce: message.nonce,
2626
ephemeralPublicKey: message.ephemeralPublicKey,
27-
signatureHex: message.signature?.hex,
28-
signatureRecovery: message.signature?.recovery,
29-
authorAccountId: message.authorAccountId,
27+
signatureHex: message.signature?.hex ?? null,
28+
signatureRecovery: message.signature?.recovery ?? null,
29+
authorAccountId: message.authorAccountId ?? null,
3030
accountInbox: {
3131
connect: {
3232
id: accountInbox.id,
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { Messages } from '@graphprotocol/hypergraph';
2+
import { prisma } from '../prisma.js';
3+
4+
interface GetLatestAccountInboxMessagesParams {
5+
inboxId: string;
6+
since: number;
7+
}
8+
9+
export async function getLatestAccountInboxMessages({
10+
inboxId,
11+
since,
12+
}: GetLatestAccountInboxMessagesParams): Promise<Messages.InboxMessage[]> {
13+
const date = new Date(since * 1000); // Convert Unix timestamp to Date
14+
15+
const messages = await prisma.accountInboxMessage.findMany({
16+
where: {
17+
accountInboxId: inboxId,
18+
createdAt: {
19+
gte: date,
20+
},
21+
},
22+
orderBy: {
23+
id: 'asc',
24+
},
25+
});
26+
27+
return messages.map((msg) => ({
28+
id: msg.id,
29+
ciphertext: msg.ciphertext,
30+
nonce: msg.nonce,
31+
ephemeralPublicKey: msg.ephemeralPublicKey,
32+
signature:
33+
msg.signatureHex && msg.signatureRecovery
34+
? {
35+
hex: msg.signatureHex,
36+
recovery: msg.signatureRecovery,
37+
}
38+
: undefined,
39+
authorAccountId: msg.authorAccountId || undefined,
40+
createdAt: Math.floor(msg.createdAt.getTime() / 1000), // Convert Date to Unix timestamp
41+
}));
42+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { Messages } from '@graphprotocol/hypergraph';
2+
import { prisma } from '../prisma.js';
3+
4+
interface GetLatestSpaceInboxMessagesParams {
5+
inboxId: string;
6+
since: number;
7+
}
8+
9+
export async function getLatestSpaceInboxMessages({
10+
inboxId,
11+
since,
12+
}: GetLatestSpaceInboxMessagesParams): Promise<Messages.InboxMessage[]> {
13+
const date = new Date(since * 1000); // Convert Unix timestamp to Date
14+
15+
const messages = await prisma.spaceInboxMessage.findMany({
16+
where: {
17+
spaceInboxId: inboxId,
18+
createdAt: {
19+
gte: date,
20+
},
21+
},
22+
orderBy: {
23+
id: 'asc',
24+
},
25+
});
26+
27+
return messages.map((msg) => ({
28+
id: msg.id,
29+
ciphertext: msg.ciphertext,
30+
nonce: msg.nonce,
31+
ephemeralPublicKey: msg.ephemeralPublicKey,
32+
signature:
33+
msg.signatureHex && msg.signatureRecovery
34+
? {
35+
hex: msg.signatureHex,
36+
recovery: msg.signatureRecovery,
37+
}
38+
: undefined,
39+
authorAccountId: msg.authorAccountId || undefined,
40+
createdAt: Math.floor(msg.createdAt.getTime() / 1000), // Convert Date to Unix timestamp
41+
}));
42+
}

apps/server/src/handlers/getSpace.ts

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -62,37 +62,29 @@ export const getSpace = async ({ spaceId, accountId }: Params) => {
6262
};
6363
});
6464

65-
const formatUpdate = (update) => {
66-
return {
67-
accountId: update.accountId,
68-
update: new Uint8Array(update.content),
69-
signature: {
70-
hex: update.signatureHex,
71-
recovery: update.signatureRecovery,
72-
},
73-
updateId: update.updateId,
74-
};
75-
};
76-
77-
const formatInbox = (inbox) => {
78-
return {
65+
return {
66+
id: space.id,
67+
events: space.events.map((wrapper) => JSON.parse(wrapper.event)),
68+
keyBoxes,
69+
inboxes: space.inboxes.map((inbox) => ({
7970
inboxId: inbox.id,
8071
isPublic: inbox.isPublic,
8172
authPolicy: inbox.authPolicy,
8273
encryptionPublicKey: inbox.encryptionPublicKey,
8374
secretKey: inbox.encryptedSecretKey,
84-
};
85-
};
86-
87-
return {
88-
id: space.id,
89-
events: space.events.map((wrapper) => JSON.parse(wrapper.event)),
90-
keyBoxes,
91-
inboxes: space.inboxes.map(formatInbox),
75+
})),
9276
updates:
9377
space.updates.length > 0
9478
? {
95-
updates: space.updates.map(formatUpdate),
79+
updates: space.updates.map((update) => ({
80+
accountId: update.accountId,
81+
update: new Uint8Array(update.content),
82+
signature: {
83+
hex: update.signatureHex,
84+
recovery: update.signatureRecovery,
85+
},
86+
updateId: update.updateId,
87+
})),
9688
firstUpdateClock: space.updates[0].clock,
9789
lastUpdateClock: space.updates[space.updates.length - 1].clock,
9890
}

apps/server/src/index.ts

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,19 @@ import { SiweMessage } from 'siwe';
77
import type { Hex } from 'viem';
88
import WebSocket, { WebSocketServer } from 'ws';
99
import { applySpaceEvent } from './handlers/applySpaceEvent.js';
10+
import { createAccountInbox } from './handlers/createAccountInbox.js';
1011
import { createAccountInboxMessage } from './handlers/createAccountInboxMessage.js';
1112
import { createIdentity } from './handlers/createIdentity.js';
1213
import { createSpace } from './handlers/createSpace.js';
1314
import { createSpaceInboxMessage } from './handlers/createSpaceInboxMessage.js';
1415
import { createUpdate } from './handlers/createUpdate.js';
1516
import { getAccountInbox } from './handlers/getAccountInbox.js';
1617
import { type GetIdentityResult, getIdentity } from './handlers/getIdentity.js';
18+
import { getLatestAccountInboxMessages } from './handlers/getLatestAccountInboxMessages.js';
19+
import { getLatestSpaceInboxMessages } from './handlers/getLatestSpaceInboxMessages.js';
1720
import { getSpace } from './handlers/getSpace.js';
1821
import { getSpaceInbox } from './handlers/getSpaceInbox.js';
22+
import { listAccountInboxes } from './handlers/listAccountInboxes.js';
1923
import { listInvitations } from './handlers/listInvitations.js';
2024
import { listPublicAccountInboxes } from './handlers/listPublicAccountInboxes.js';
2125
import { listPublicSpaceInboxes } from './handlers/listPublicSpaceInboxes.js';
@@ -358,7 +362,6 @@ app.post('/spaces/:spaceId/inboxes/:inboxId/messages', async (req, res) => {
358362
broadcastSpaceInboxMessage({ spaceId, inboxId, message });
359363
});
360364

361-
// TODO same as above but for account inboxes
362365
app.get('/accounts/:accountId/inboxes', async (req, res) => {
363366
console.log('GET accounts/:accountId/inboxes');
364367
const accountId = req.params.accountId;
@@ -499,6 +502,18 @@ function broadcastSpaceInboxMessage({
499502
}
500503
}
501504

505+
function broadcastAccountInbox({ inbox }: { inbox: Messages.AccountInboxPublic }) {
506+
const outgoingMessage: Messages.ResponseAccountInbox = {
507+
type: 'account-inbox',
508+
inbox,
509+
};
510+
for (const client of webSocketServer.clients as Set<CustomWebSocket>) {
511+
if (client.readyState === WebSocket.OPEN && client.accountId === inbox.accountId) {
512+
client.send(Messages.serialize(outgoingMessage));
513+
}
514+
}
515+
}
516+
502517
function broadcastAccountInboxMessage({
503518
accountId,
504519
inboxId,
@@ -660,17 +675,65 @@ webSocketServer.on('connection', async (webSocket: CustomWebSocket, request: Req
660675
}
661676
case 'create-account-inbox': {
662677
// TODO
663-
// Check that the signature is valid for the corresponding accountId
664-
// Create the inbox (if it doesn't exist)
665-
// Broadcast the inbox to other clients from the same account
678+
try {
679+
// Check that the signature is valid for the corresponding accountId
680+
if (data.accountId !== accountId) {
681+
throw new Error('Invalid accountId');
682+
}
683+
const signer = Inboxes.recoverAccountInboxCreatorKey(data);
684+
if (signer !== accountId) {
685+
throw new Error('Invalid signature');
686+
}
687+
// Create the inbox (if it doesn't exist)
688+
await createAccountInbox(data);
689+
// Broadcast the inbox to other clients from the same account
690+
broadcastAccountInbox({ inbox: data });
691+
} catch (error) {
692+
console.error('Error creating account inbox:', error);
693+
return;
694+
}
666695
break;
667696
}
668697
case 'get-latest-space-inbox-messages': {
669-
// TODO
698+
try {
699+
// Check that the user has access to this space
700+
await getSpace({ accountId, spaceId: data.spaceId });
701+
const messages = await getLatestSpaceInboxMessages({
702+
inboxId: data.inboxId,
703+
since: data.since,
704+
});
705+
const outgoingMessage: Messages.ResponseSpaceInboxMessages = {
706+
type: 'space-inbox-messages',
707+
spaceId: data.spaceId,
708+
inboxId: data.inboxId,
709+
messages,
710+
};
711+
webSocket.send(Messages.serialize(outgoingMessage));
712+
} catch (error) {
713+
console.error('Error getting latest space inbox messages:', error);
714+
return;
715+
}
670716
break;
671717
}
672718
case 'get-latest-account-inbox-messages': {
673-
// TODO
719+
try {
720+
// Check that the user has access to this inbox
721+
await getAccountInbox({ accountId, inboxId: data.inboxId });
722+
const messages = await getLatestAccountInboxMessages({
723+
inboxId: data.inboxId,
724+
since: data.since,
725+
});
726+
const outgoingMessage: Messages.ResponseAccountInboxMessages = {
727+
type: 'account-inbox-messages',
728+
accountId,
729+
inboxId: data.inboxId,
730+
messages,
731+
};
732+
webSocket.send(Messages.serialize(outgoingMessage));
733+
} catch (error) {
734+
console.error('Error getting latest account inbox messages:', error);
735+
return;
736+
}
674737
break;
675738
}
676739
case 'get-account-inboxes': {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Identity, Messages } from '@graphprotocol/hypergraph';
2+
import type { AccountInboxStorageEntry, SpaceInboxStorageEntry } from '../store.js';
3+
import { recoverAccountInboxMessageSigner, recoverSpaceInboxMessageSigner } from './recover-inbox-message-signer.js';
4+
5+
export const validateSpaceInboxMessage = async (
6+
message: Messages.InboxMessage,
7+
inbox: SpaceInboxStorageEntry,
8+
spaceId: string,
9+
syncServerUri: string,
10+
) => {
11+
if (message.signature) {
12+
if (inbox.authPolicy === Messages.InboxSenderAuthPolicy.Anonymous) {
13+
// Signed messages are invalid in anonymous inboxes
14+
return false;
15+
}
16+
if (!message.authorAccountId) {
17+
// Signed message without authorAccountId is invalid
18+
return false;
19+
}
20+
const signer = recoverSpaceInboxMessageSigner(message, spaceId, inbox.inboxId);
21+
const verifiedIdentity = await Identity.getVerifiedIdentity(message.authorAccountId, syncServerUri);
22+
return signer === verifiedIdentity.signaturePublicKey;
23+
}
24+
// Unsigned message is valid if the inbox is anonymous or optional auth
25+
return inbox.authPolicy !== Messages.InboxSenderAuthPolicy.RequiresAuth;
26+
};
27+
28+
export const validateAccountInboxMessage = async (
29+
message: Messages.InboxMessage,
30+
inbox: AccountInboxStorageEntry,
31+
accountId: string,
32+
syncServerUri: string,
33+
) => {
34+
if (message.signature) {
35+
if (inbox.authPolicy === Messages.InboxSenderAuthPolicy.Anonymous) {
36+
// Signed messages are invalid in anonymous inboxes
37+
return false;
38+
}
39+
if (!message.authorAccountId) {
40+
// Signed message without authorAccountId is invalid
41+
return false;
42+
}
43+
const signer = recoverAccountInboxMessageSigner(message, accountId, inbox.inboxId);
44+
const verifiedIdentity = await Identity.getVerifiedIdentity(message.authorAccountId, syncServerUri);
45+
return signer === verifiedIdentity.signaturePublicKey;
46+
}
47+
// Unsigned message is valid if the inbox is anonymous or optional auth
48+
return inbox.authPolicy !== Messages.InboxSenderAuthPolicy.RequiresAuth;
49+
};

0 commit comments

Comments
 (0)