Skip to content

Commit 401596c

Browse files
committed
chore: wip adding server endpoints
1 parent 64104fe commit 401596c

File tree

9 files changed

+334
-3
lines changed

9 files changed

+334
-3
lines changed

apps/server/src/handlers/getIdentity.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,17 @@ type Params =
1010
signaturePublicKey: string;
1111
};
1212

13-
export const getIdentity = async (params: Params) => {
13+
export type GetIdentityResult = {
14+
accountId: string;
15+
ciphertext: string;
16+
nonce: string;
17+
signaturePublicKey: string;
18+
encryptionPublicKey: string;
19+
accountProof: string;
20+
keyProof: string;
21+
};
22+
23+
export const getIdentity = async (params: Params): Promise<GetIdentityResult> => {
1424
if (!params.accountId && !params.signaturePublicKey) {
1525
throw new Error('Either accountId or signaturePublicKey must be provided');
1626
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
2+
export async function getSpaceInbox({ spaceId, inboxId }: { spaceId: string; inboxId: string }) {
3+
const inbox = await prisma.spaceInbox.findUnique({
4+
where: { id: inboxId, spaceId },
5+
select: {
6+
id: true,
7+
isPublic: true,
8+
authPolicy: true,
9+
encryptionPublicKey: true,
10+
},
11+
include: {
12+
spaceEvent: {
13+
select: {
14+
event: true,
15+
},
16+
},
17+
},
18+
});
19+
if (!inbox) {
20+
throw new Error('Inbox not found');
21+
}
22+
23+
return {
24+
inboxId: inbox.id,
25+
isPublic: inbox.isPublic,
26+
authPolicy: inbox.authPolicy,
27+
encryptionPublicKey: inbox.encryptionPublicKey,
28+
creationEvent: JSON.parse(inbox.spaceEvent),
29+
};
30+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { prisma } from "../prisma";
2+
3+
export async function listPublicSpaceInboxes({ spaceId }: { spaceId: string }) {
4+
const inboxes = await prisma.spaceInbox.findMany({
5+
where: { spaceId, isPublic: true },
6+
select: {
7+
id: true,
8+
isPublic: true,
9+
authPolicy: true,
10+
encryptionPublicKey: true,
11+
},
12+
include: {
13+
spaceEvent: {
14+
select: {
15+
event: true,
16+
},
17+
},
18+
},
19+
});
20+
return inboxes.map((inbox) => {
21+
return {
22+
inboxId: inbox.id,
23+
isPublic: inbox.isPublic,
24+
authPolicy: inbox.authPolicy,
25+
encryptionPublicKey: inbox.encryptionPublicKey,
26+
creationEvent: JSON.parse(inbox.spaceEvent),
27+
};
28+
});
29+
}

apps/server/src/index.ts

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,17 @@ import { applySpaceEvent } from './handlers/applySpaceEvent.js';
1010
import { createIdentity } from './handlers/createIdentity.js';
1111
import { createSpace } from './handlers/createSpace.js';
1212
import { createUpdate } from './handlers/createUpdate.js';
13-
import { getIdentity } from './handlers/getIdentity.js';
13+
import { getIdentity, GetIdentityResult } from './handlers/getIdentity.js';
1414
import { getSpace } from './handlers/getSpace.js';
1515
import { listInvitations } from './handlers/listInvitations.js';
1616
import { listSpaces } from './handlers/listSpaces.js';
1717
import { createSessionNonce, getSessionNonce } from './handlers/sessionNonce.js';
1818
import { createSessionToken, getAccountIdBySessionToken } from './handlers/sessionToken.js';
1919
import { tmpInitAccount } from './handlers/tmpInitAccount.js';
20+
import { listPublicSpaceInboxes } from './handlers/listPublicSpaceInboxes.js';
21+
import { getSpaceInbox } from './handlers/getSpaceInbox.js';
22+
import { secp256k1 } from '@noble/curves/secp256k1';
23+
import { sha256 } from '@noble/hashes/sha256';
2024

2125
interface CustomWebSocket extends WebSocket {
2226
accountId: string;
@@ -271,6 +275,98 @@ app.get('/identity', async (req, res) => {
271275
}
272276
});
273277

278+
app.get('/spaces/:spaceId/inboxes', async (req, res) => {
279+
console.log('GET spaces/:spaceId/inboxes');
280+
const spaceId = req.params.spaceId;
281+
const inboxes = await listPublicSpaceInboxes({ spaceId });
282+
const outgoingMessage: Messages.ResponseListSpaceInboxesPublic = {
283+
inboxes,
284+
};
285+
res.status(200).send(outgoingMessage);
286+
});
287+
288+
app.get('/spaces/:spaceId/inboxes/:inboxId', async (req, res) => {
289+
console.log('GET spaces/:spaceId/inboxes/:inboxId');
290+
const spaceId = req.params.spaceId;
291+
const inboxId = req.params.inboxId;
292+
const inbox = await getSpaceInbox({ spaceId, inboxId });
293+
const outgoingMessage: Messages.ResponseSpaceInboxPublic = {
294+
inbox,
295+
};
296+
res.status(200).send(outgoingMessage);
297+
});
298+
299+
app.post('/spaces/:spaceId/inboxes/:inboxId/messages', async (req, res) => {
300+
console.log('POST spaces/:spaceId/inboxes/:inboxId/messages');
301+
const spaceId = req.params.spaceId;
302+
const inboxId = req.params.inboxId;
303+
const message = req.body;
304+
let spaceInbox: Messages.SpaceInboxPublic;
305+
try {
306+
spaceInbox = await getSpaceInbox({ spaceId, inboxId });
307+
} catch (error) {
308+
res.status(404).send({ error: 'Inbox not found' });
309+
return;
310+
}
311+
312+
if (spaceInbox.authPolicy === 'auth_required') {
313+
if (!message.signature) {
314+
res.status(400).send({ error: 'Signature required for auth_required inbox' });
315+
return;
316+
}
317+
318+
// Recover the public key from the signature
319+
// TODO: move this to inboxes in the hypergraph package
320+
let signatureInstance = secp256k1.Signature.fromCompact(message.signature.hex);
321+
signatureInstance = signatureInstance.addRecoveryBit(message.signature.recovery);
322+
const authorPublicKey = `0x${signatureInstance.recoverPublicKey(sha256(Utils.stringToUint8Array(Utils.canonicalize(message)))).toHex()}`;
323+
324+
// Check if this public key corresponds to a member's identity
325+
let authorIdentity: GetIdentityResult;
326+
try {
327+
authorIdentity = await getIdentity({ signaturePublicKey: authorPublicKey });
328+
} catch (error) {
329+
res.status(403).send({ error: 'Not authorized to post to this inbox' });
330+
return;
331+
}
332+
}
333+
await createSpaceInboxMessage({ spaceId, inboxId, message });
334+
res.status(200).send({}) ;
335+
broadcastSpaceInboxMessage({ spaceId, inboxId, message });
336+
});
337+
338+
// TODO same as above but for account inboxes
339+
app.get('/accounts/:accountId/inboxes', async (req, res) => {
340+
console.log('GET accounts/:accountId/inboxes');
341+
const accountId = req.params.accountId;
342+
const inboxes = await listPublicAccountInboxes({ accountId });
343+
const outgoingMessage: Messages.ResponseListAccountInboxesPublic = {
344+
inboxes,
345+
};
346+
res.status(200).send(outgoingMessage);
347+
});
348+
349+
app.get('/accounts/:accountId/inboxes/:inboxId', async (req, res) => {
350+
console.log('GET accounts/:accountId/inboxes/:inboxId');
351+
const accountId = req.params.accountId;
352+
const inboxId = req.params.inboxId;
353+
const inbox = await getAccountInbox({ accountId, inboxId });
354+
const outgoingMessage: Messages.ResponseAccountInboxPublic = {
355+
inbox,
356+
};
357+
res.status(200).send(outgoingMessage);
358+
});
359+
360+
app.post('/accounts/:accountId/inboxes/:inboxId/messages', async (req, res) => {
361+
console.log('POST accounts/:accountId/inboxes/:inboxId/messages');
362+
const accountId = req.params.accountId;
363+
const inboxId = req.params.inboxId;
364+
const message = req.body;
365+
await createAccountInboxMessage({ accountId, inboxId, message });
366+
res.status(200).send({});
367+
broadcastAccountInboxMessage({ accountId, inboxId, message });
368+
});
369+
274370
const server = app.listen(PORT, () => {
275371
console.log(`Listening on port ${PORT}`);
276372
});

packages/hypergraph-react/src/HypergraphAppContext.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,26 @@ export function HypergraphAppProvider({
475475
state: applyEventResult.value,
476476
});
477477
}
478+
if (response.event.transaction.type === 'create-space-inbox') {
479+
const inbox = {
480+
inboxId: response.event.transaction.id,
481+
isPublic: response.event.transaction.isPublic,
482+
authPolicy: response.event.transaction.authPolicy,
483+
encryptionPublicKey: response.event.transaction.encryptionPublicKey,
484+
secretKey: Utils.bytesToHex(
485+
Messages.decryptMessage({
486+
nonceAndCiphertext: Utils.hexToBytes(response.event.transaction.secretKey),
487+
secretKey: Utils.hexToBytes(space.keys[0].key),
488+
}),
489+
),
490+
messages: [],
491+
};
492+
store.send({
493+
type: 'setSpaceInbox',
494+
spaceId: response.spaceId,
495+
inbox,
496+
});
497+
}
478498

479499
break;
480500
}
@@ -513,6 +533,23 @@ export function HypergraphAppProvider({
513533
await applyUpdates(response.spaceId, space.keys[0].key, space.automergeDocHandle, response.updates);
514534
break;
515535
}
536+
case 'account-inbox': {
537+
// Validate the signature of the inbox corresponds to the current account's identity
538+
if (!keys.signaturePrivateKey) {
539+
console.error('No signature private key found to process account inbox');
540+
return;
541+
}
542+
const inboxCreator = Inboxes.recoverAccountInboxCreatorKey(response.inbox);
543+
if (inboxCreator !== keys.signaturePublicKey) {
544+
console.error('Invalid inbox creator', response.inbox);
545+
return;
546+
}
547+
store.send({
548+
type: 'setAccountInbox',
549+
inbox: response.inbox,
550+
});
551+
break;
552+
}
516553
default: {
517554
Utils.assertExhaustive(response);
518555
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './create-inbox.js';
2+
export * from './recover-inbox-creator.js';
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { sha256 } from '@noble/hashes/sha256';
2+
import { secp256k1 } from '@noble/curves/secp256k1';
3+
import { stringToUint8Array } from '../utils/index.js';
4+
import { canonicalize } from '../utils/index.js';
5+
import { AccountInbox } from '../messages/index.js';
6+
import { CreateSpaceInboxEvent } from '../space-events/index.js';
7+
8+
export const recoverAccountInboxCreatorKey = (inbox: AccountInbox): string => {
9+
const messageToVerify = stringToUint8Array(
10+
canonicalize({
11+
accountId: inbox.accountId,
12+
inboxId: inbox.inboxId,
13+
encryptionPublicKey: inbox.encryptionPublicKey,
14+
}),
15+
);
16+
const signature = inbox.signature;
17+
let signatureInstance = secp256k1.Signature.fromCompact(signature.hex);
18+
signatureInstance = signatureInstance.addRecoveryBit(signature.recovery);
19+
const authorPublicKey = `0x${signatureInstance.recoverPublicKey(sha256(messageToVerify)).toHex()}`;
20+
return authorPublicKey;
21+
};
22+
23+
export const recoverSpaceInboxCreatorKey = (event: CreateSpaceInboxEvent): string => {
24+
const messageToVerify = stringToUint8Array(canonicalize(event.transaction));
25+
const signature = event.author.signature;
26+
let signatureInstance = secp256k1.Signature.fromCompact(signature.hex);
27+
signatureInstance = signatureInstance.addRecoveryBit(signature.recovery);
28+
const authorPublicKey = `0x${signatureInstance.recoverPublicKey(sha256(messageToVerify)).toHex()}`;
29+
return authorPublicKey;
30+
};

packages/hypergraph/src/messages/types.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,25 @@ export const SpaceInbox = Schema.Struct({
237237

238238
export type SpaceInbox = Schema.Schema.Type<typeof SpaceInbox>;
239239

240+
export const AccountInbox = Schema.Struct({
241+
accountId: Schema.String,
242+
inboxId: Schema.String,
243+
isPublic: Schema.Boolean,
244+
authPolicy: Schema.String,
245+
encryptionPublicKey: Schema.String,
246+
signature: SignatureWithRecovery,
247+
messages: Schema.Array(InboxMessage),
248+
});
249+
250+
export type AccountInbox = Schema.Schema.Type<typeof AccountInbox>;
251+
252+
export const ResponseAccountInbox = Schema.Struct({
253+
type: Schema.Literal('account-inbox'),
254+
inbox: AccountInbox,
255+
});
256+
257+
export type ResponseAccountInbox = Schema.Schema.Type<typeof ResponseAccountInbox>;
258+
240259
export const ResponseSpace = Schema.Struct({
241260
type: Schema.Literal('space'),
242261
id: Schema.String,
@@ -272,6 +291,7 @@ export const ResponseMessage = Schema.Union(
272291
ResponseSpaceEvent,
273292
ResponseUpdateConfirmed,
274293
ResponseUpdatesNotification,
294+
ResponseAccountInbox,
275295
);
276296

277297
export type ResponseMessage = Schema.Schema.Type<typeof ResponseMessage>;
@@ -310,6 +330,51 @@ export const ResponseIdentity = Schema.Struct({
310330

311331
export type ResponseIdentity = Schema.Schema.Type<typeof ResponseIdentity>;
312332

333+
export const SpaceInboxPublic = Schema.Struct({
334+
inboxId: Schema.String,
335+
isPublic: Schema.Boolean,
336+
authPolicy: Schema.String,
337+
encryptionPublicKey: Schema.String,
338+
creationEvent: CreateSpaceInboxEvent,
339+
});
340+
341+
export type SpaceInboxPublic = Schema.Schema.Type<typeof SpaceInboxPublic>;
342+
343+
export const ResponseSpaceInboxPublic = Schema.Struct({
344+
inbox: SpaceInboxPublic,
345+
});
346+
347+
export type ResponseSpaceInboxPublic = Schema.Schema.Type<typeof ResponseSpaceInboxPublic>;
348+
349+
export const ResponseListSpaceInboxesPublic = Schema.Struct({
350+
inboxes: Schema.Array(SpaceInboxPublic),
351+
});
352+
353+
export type ResponseListSpaceInboxesPublic = Schema.Schema.Type<typeof ResponseListSpaceInboxesPublic>;
354+
355+
export const AccountInboxPublic = Schema.Struct({
356+
accountId: Schema.String,
357+
inboxId: Schema.String,
358+
isPublic: Schema.Boolean,
359+
authPolicy: Schema.String,
360+
encryptionPublicKey: Schema.String,
361+
signature: SignatureWithRecovery,
362+
});
363+
364+
export type AccountInboxPublic = Schema.Schema.Type<typeof AccountInboxPublic>;
365+
366+
export const ResponseAccountInboxPublic = Schema.Struct({
367+
inbox: AccountInboxPublic,
368+
});
369+
370+
export type ResponseAccountInboxPublic = Schema.Schema.Type<typeof ResponseAccountInboxPublic>;
371+
372+
export const ResponseListAccountInboxesPublic = Schema.Struct({
373+
inboxes: Schema.Array(AccountInboxPublic),
374+
});
375+
376+
export type ResponseListAccountInboxesPublic = Schema.Schema.Type<typeof ResponseListAccountInboxesPublic>;
377+
313378
export const ResponseIdentityNotFoundError = Schema.Struct({
314379
accountId: Schema.String,
315380
});

0 commit comments

Comments
 (0)