Skip to content

Commit 77c207b

Browse files
authored
(new server) add websocket handling (#524)
1 parent de0d153 commit 77c207b

File tree

15 files changed

+1333
-29
lines changed

15 files changed

+1333
-29
lines changed

.changeset/lazy-birds-wait.md

Lines changed: 0 additions & 6 deletions
This file was deleted.

.claude/settings.local.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"permissions": {
3+
"allow": ["Bash(pnpm lint:fix:*)", "Bash(pnpm typecheck:*)", "Bash(pnpm check:*)"],
4+
"deny": [],
5+
"ask": []
6+
}
7+
}

apps/server-new/src/http/api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export class IdentityQuery extends Schema.Class<IdentityQuery>('IdentityQuery')(
6565
/**
6666
* Health endpoints
6767
*/
68-
export const statusEndpoint = HttpApiEndpoint.get('status')`/`.addSuccess(Schema.String);
68+
export const statusEndpoint = HttpApiEndpoint.get('status')`/status`.addSuccess(Schema.String);
6969

7070
export const healthGroup = HttpApiGroup.make('Health').add(statusEndpoint);
7171

apps/server-new/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const Observability = Layer.unwrapEffect(
2626

2727
const layer = server.pipe(
2828
Layer.provide(Logger.structured),
29+
// Layer.provide(Logger.pretty),
2930
Layer.provide(Observability),
3031
Layer.provide(PlatformConfigProvider.layerDotEnvAdd('.env')),
3132
Layer.provide(NodeContext.layer),

apps/server-new/src/server.ts

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,14 @@ import { createServer } from 'node:http';
22
import * as HttpApiScalar from '@effect/platform/HttpApiScalar';
33
import * as HttpLayerRouter from '@effect/platform/HttpLayerRouter';
44
import * as HttpMiddleware from '@effect/platform/HttpMiddleware';
5-
import * as HttpServerRequest from '@effect/platform/HttpServerRequest';
6-
import * as HttpServerResponse from '@effect/platform/HttpServerResponse';
75
import * as NodeHttpServer from '@effect/platform-node/NodeHttpServer';
86
import * as Effect from 'effect/Effect';
97
import * as Layer from 'effect/Layer';
10-
import * as Schedule from 'effect/Schedule';
11-
import * as Stream from 'effect/Stream';
128
import { serverPortConfig } from './config/server.ts';
139
import { hypergraphApi } from './http/api.ts';
1410
import { HandlersLive } from './http/handlers.ts';
11+
import * as ConnectionsService from './services/connections.ts';
12+
import { WebSocketLayer } from './websocket.ts';
1513

1614
// Create scalar openapi browser layer at /docs.
1715
const DocsLayer = HttpApiScalar.layerHttpLayerRouter({
@@ -24,20 +22,6 @@ const ApiLayer = HttpLayerRouter.addHttpApi(hypergraphApi, {
2422
openapiPath: '/docs/openapi.json',
2523
}).pipe(Layer.provide(HandlersLive));
2624

27-
// Create websocket layer at /ws.
28-
const WebSocketLayer = HttpLayerRouter.add(
29-
'GET',
30-
'/ws',
31-
// TODO: Implement actual websocket logic here.
32-
Stream.fromSchedule(Schedule.spaced(1000)).pipe(
33-
Stream.map(JSON.stringify),
34-
Stream.pipeThroughChannel(HttpServerRequest.upgradeChannel()),
35-
Stream.decodeText(),
36-
Stream.runForEach((_) => Effect.log(_)),
37-
Effect.as(HttpServerResponse.empty()),
38-
),
39-
);
40-
4125
// Merge router layers together and add the cors middleware layer.
4226
const CorsMiddleware = HttpLayerRouter.middleware(HttpMiddleware.cors());
4327
const AppLayer = Layer.mergeAll(ApiLayer, DocsLayer, WebSocketLayer).pipe(Layer.provide(CorsMiddleware.layer));
@@ -47,4 +31,7 @@ const HttpServerLayer = serverPortConfig.pipe(
4731
Layer.unwrapEffect,
4832
);
4933

50-
export const server = HttpLayerRouter.serve(AppLayer).pipe(Layer.provide(HttpServerLayer));
34+
export const server = HttpLayerRouter.serve(AppLayer).pipe(
35+
Layer.provide(HttpServerLayer),
36+
Layer.provide(ConnectionsService.layer),
37+
);

apps/server-new/src/services/account-inbox.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ export class AccountInboxService extends Context.Tag('AccountInboxService')<
3737
Messages.InboxMessage,
3838
ResourceNotFoundError | ValidationError | AuthorizationError | DatabaseService.DatabaseError
3939
>;
40+
readonly createAccountInbox: (
41+
data: Messages.RequestCreateAccountInbox,
42+
) => Effect.Effect<AccountInboxResult, ValidationError | AuthorizationError | DatabaseService.DatabaseError>;
43+
readonly getLatestAccountInboxMessages: (params: {
44+
inboxId: string;
45+
since: Date;
46+
}) => Effect.Effect<Messages.InboxMessage[], DatabaseService.DatabaseError>;
47+
readonly listAccountInboxes: (params: {
48+
accountAddress: string;
49+
}) => Effect.Effect<Messages.AccountInbox[], DatabaseService.DatabaseError>;
4050
}
4151
>() {}
4252

@@ -258,9 +268,131 @@ export const layer = Effect.gen(function* () {
258268
return createdMessage;
259269
});
260270

271+
const createAccountInbox = Effect.fn('createAccountInbox')(function* (data: Messages.RequestCreateAccountInbox) {
272+
const { accountAddress, inboxId, isPublic, authPolicy, encryptionPublicKey, signature } = data;
273+
274+
// Verify the signature is valid for the corresponding accountAddress
275+
const signer = Inboxes.recoverAccountInboxCreatorKey(data);
276+
const signerAccount = yield* getAppOrConnectIdentity({
277+
accountAddress: data.accountAddress,
278+
signaturePublicKey: signer,
279+
}).pipe(Effect.mapError(() => new AuthorizationError({ message: 'Invalid signature' })));
280+
281+
if (signerAccount.accountAddress !== accountAddress) {
282+
return yield* Effect.fail(new AuthorizationError({ message: 'Invalid signature' }));
283+
}
284+
285+
// Create the inbox (will throw an error if it already exists)
286+
const inbox = yield* use((client) =>
287+
client.accountInbox.create({
288+
data: {
289+
id: inboxId,
290+
isPublic,
291+
authPolicy,
292+
encryptionPublicKey,
293+
signatureHex: signature.hex,
294+
signatureRecovery: signature.recovery,
295+
account: { connect: { address: accountAddress } },
296+
},
297+
}),
298+
);
299+
300+
return {
301+
inboxId: inbox.id,
302+
accountAddress,
303+
isPublic: inbox.isPublic,
304+
authPolicy: inbox.authPolicy as Inboxes.InboxSenderAuthPolicy,
305+
encryptionPublicKey: inbox.encryptionPublicKey,
306+
signature: {
307+
hex: inbox.signatureHex,
308+
recovery: inbox.signatureRecovery,
309+
},
310+
};
311+
});
312+
313+
const getLatestAccountInboxMessages = Effect.fn('getLatestAccountInboxMessages')(function* ({
314+
inboxId,
315+
since,
316+
}: {
317+
inboxId: string;
318+
since: Date;
319+
}) {
320+
const messages = yield* use((client) =>
321+
client.accountInboxMessage.findMany({
322+
where: {
323+
accountInboxId: inboxId,
324+
createdAt: {
325+
gte: since,
326+
},
327+
},
328+
orderBy: {
329+
createdAt: 'asc',
330+
},
331+
}),
332+
);
333+
334+
return messages.map(
335+
(msg): Messages.InboxMessage => ({
336+
id: msg.id,
337+
ciphertext: msg.ciphertext,
338+
signature:
339+
msg.signatureHex != null && msg.signatureRecovery != null
340+
? {
341+
hex: msg.signatureHex,
342+
recovery: msg.signatureRecovery,
343+
}
344+
: undefined,
345+
authorAccountAddress: msg.authorAccountAddress ?? undefined,
346+
createdAt: msg.createdAt,
347+
}),
348+
);
349+
});
350+
351+
const listAccountInboxes = Effect.fn('listAccountInboxes')(function* ({
352+
accountAddress,
353+
}: {
354+
accountAddress: string;
355+
}) {
356+
const inboxes = yield* use((client) =>
357+
client.accountInbox.findMany({
358+
where: { accountAddress },
359+
select: {
360+
id: true,
361+
isPublic: true,
362+
authPolicy: true,
363+
encryptionPublicKey: true,
364+
account: {
365+
select: {
366+
address: true,
367+
},
368+
},
369+
signatureHex: true,
370+
signatureRecovery: true,
371+
},
372+
}),
373+
);
374+
375+
return inboxes.map(
376+
(inbox): Messages.AccountInbox => ({
377+
inboxId: inbox.id,
378+
accountAddress: inbox.account.address,
379+
isPublic: inbox.isPublic,
380+
authPolicy: inbox.authPolicy as Inboxes.InboxSenderAuthPolicy,
381+
encryptionPublicKey: inbox.encryptionPublicKey,
382+
signature: {
383+
hex: inbox.signatureHex,
384+
recovery: inbox.signatureRecovery,
385+
},
386+
}),
387+
);
388+
});
389+
261390
return {
262391
listPublicAccountInboxes,
263392
getAccountInbox,
264393
postAccountInboxMessage,
394+
createAccountInbox,
395+
getLatestAccountInboxMessages,
396+
listAccountInboxes,
265397
} as const;
266398
}).pipe(Layer.effect(AccountInboxService), Layer.provide(DatabaseService.layer), Layer.provide(IdentityService.layer));

0 commit comments

Comments
 (0)