@covenant-rpc/server 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/adapters/vanilla.d.ts +3 -0
  2. package/dist/adapters/vanilla.d.ts.map +1 -0
  3. package/dist/adapters/vanilla.js +7 -0
  4. package/dist/adapters/vanilla.js.map +1 -0
  5. package/{index.ts → dist/index.d.ts} +1 -2
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +8 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/interfaces/direct.d.ts +4 -0
  10. package/dist/interfaces/direct.d.ts.map +1 -0
  11. package/dist/interfaces/direct.js +98 -0
  12. package/dist/interfaces/direct.js.map +1 -0
  13. package/dist/interfaces/empty.d.ts +3 -0
  14. package/dist/interfaces/empty.d.ts.map +1 -0
  15. package/dist/interfaces/empty.js +8 -0
  16. package/dist/interfaces/empty.js.map +1 -0
  17. package/dist/interfaces/http.d.ts +4 -0
  18. package/dist/interfaces/http.d.ts.map +1 -0
  19. package/dist/interfaces/http.js +92 -0
  20. package/dist/interfaces/http.js.map +1 -0
  21. package/dist/interfaces/mock.d.ts +4 -0
  22. package/dist/interfaces/mock.d.ts.map +1 -0
  23. package/dist/interfaces/mock.js +28 -0
  24. package/dist/interfaces/mock.js.map +1 -0
  25. package/dist/logger.d.ts +16 -0
  26. package/dist/logger.d.ts.map +1 -0
  27. package/dist/logger.js +60 -0
  28. package/dist/logger.js.map +1 -0
  29. package/dist/server.d.ts +46 -0
  30. package/dist/server.d.ts.map +1 -0
  31. package/dist/server.js +342 -0
  32. package/dist/server.js.map +1 -0
  33. package/dist/sidekick/handlers.d.ts +15 -0
  34. package/dist/sidekick/handlers.d.ts.map +1 -0
  35. package/dist/sidekick/handlers.js +143 -0
  36. package/dist/sidekick/handlers.js.map +1 -0
  37. package/dist/sidekick/index.d.ts +33 -0
  38. package/dist/sidekick/index.d.ts.map +1 -0
  39. package/dist/sidekick/index.js +72 -0
  40. package/dist/sidekick/index.js.map +1 -0
  41. package/dist/sidekick/socket.d.ts +4 -0
  42. package/dist/sidekick/socket.d.ts.map +1 -0
  43. package/dist/sidekick/socket.js +5 -0
  44. package/dist/sidekick/socket.js.map +1 -0
  45. package/package.json +13 -6
  46. package/adapters/vanilla.ts +0 -9
  47. package/interfaces/direct.ts +0 -116
  48. package/interfaces/empty.ts +0 -9
  49. package/interfaces/http.ts +0 -111
  50. package/interfaces/mock.ts +0 -33
  51. package/logger.ts +0 -79
  52. package/server.ts +0 -454
  53. package/sidekick/handlers.ts +0 -173
  54. package/sidekick/index.ts +0 -109
  55. package/sidekick/socket.ts +0 -5
  56. package/tests/channel-http.test.ts +0 -483
  57. package/tests/channel.test.ts +0 -689
  58. package/tests/procedure.test.ts +0 -238
  59. package/tests/sidekick.test.ts +0 -23
  60. package/tests/validation-types.test.ts +0 -122
  61. package/tests/validation.test.ts +0 -144
package/sidekick/index.ts DELETED
@@ -1,109 +0,0 @@
1
- import type { ChannelConnectionPayload, ServerMessage } from "@covenant-rpc/core/channel";
2
- import type { SidekickToServerConnection } from "@covenant-rpc/core/interfaces";
3
- import { httpSidekickToServer } from "../interfaces/http";
4
- import type { LoggerLevel } from "@covenant-rpc/core/logger";
5
- import { Logger } from "../logger";
6
- import { handleListenMessage, handleSendMessage, handleSubscribeMessage, handleUnlistenMessage, handleUnsubscribeMessage, type SidekickHandlerContext } from "./handlers";
7
- import { getChannelTopicName, getResourceTopicName, type SidekickIncomingMessage, type SidekickOutgoingMessage } from "@covenant-rpc/core/sidekick/protocol";
8
-
9
- export interface SidekickClient {
10
- subscribe(topic: string): void;
11
- unsubscribe(topic: string): void;
12
- getId(): string;
13
- directMessage(message: SidekickOutgoingMessage): void;
14
- }
15
-
16
- export interface SidekickState {
17
- contextMap: Map<string, unknown>;
18
- tokenMap: Map<string, ChannelConnectionPayload>;
19
- usedTokenMap: Map<string, {
20
- channel: string;
21
- params: Record<string, string>
22
- id: string;
23
- }>
24
- serverConnection: SidekickToServerConnection;
25
- }
26
-
27
- export type PublishFunction = (topic: string, message: SidekickOutgoingMessage) => Promise<void>;
28
-
29
-
30
- export class Sidekick {
31
- private publish: PublishFunction;
32
- private state: SidekickState = {
33
- contextMap: new Map(),
34
- tokenMap: new Map(),
35
- usedTokenMap: new Map(),
36
- serverConnection: httpSidekickToServer("", ""),
37
- };
38
- private logger: Logger;
39
-
40
- constructor(publishFunction: PublishFunction, logLevel?: LoggerLevel) {
41
- this.publish = publishFunction
42
- this.logger = new Logger(logLevel ?? "info", [
43
- () => new Date().toUTCString(),
44
- ]);
45
- }
46
-
47
- async updateResources(resources: string[]) {
48
- const promises: (() => Promise<void>)[] = [];
49
-
50
- for (const r of resources) {
51
- promises.push(async () => {
52
- const topic = getResourceTopicName(r);
53
- await this.publish(topic, {
54
- type: "updated",
55
- resource: r,
56
- });
57
-
58
- });
59
- }
60
-
61
- await Promise.all(promises.map(p => p()));
62
- }
63
-
64
- async postServerMessage(message: ServerMessage) {
65
- const topic = getChannelTopicName(message.channel, message.params);
66
- await this.publish(topic, {
67
- type: "message",
68
- ...message
69
- });
70
- }
71
-
72
- addConnection(payload: ChannelConnectionPayload) {
73
- this.state.tokenMap.set(payload.token, payload);
74
- }
75
-
76
- async handleClientMessage(client: SidekickClient, message: SidekickIncomingMessage) {
77
- const logger = this.logger.clone()
78
- .pushPrefix(`Client: ${client.getId()}`)
79
- .pushPrefix(`Type: ${message.type}`)
80
-
81
- const ctx: SidekickHandlerContext = {
82
- client,
83
- logger,
84
- state: this.state,
85
- publish: this.publish,
86
- }
87
-
88
- switch (message.type) {
89
- case "unsubscribe":
90
- await handleUnsubscribeMessage(message, ctx);
91
- break;
92
- case "subscribe":
93
- await handleSubscribeMessage(message, ctx);
94
- break;
95
- case "listen":
96
- await handleListenMessage(message, ctx);
97
- break;
98
- case "unlisten":
99
- await handleUnlistenMessage(message, ctx);
100
- break;
101
- case "send":
102
- await handleSendMessage(message, ctx);
103
- break;
104
- }
105
- }
106
- }
107
-
108
- export type UpdateListener = (resources: string[]) => Promise<void> | void;
109
-
@@ -1,5 +0,0 @@
1
-
2
- export class SocketSidekick {
3
- constructor(ws: WebSocket) {
4
- }
5
- }
@@ -1,483 +0,0 @@
1
- import { z } from "zod";
2
- import { test, expect } from "bun:test";
3
- import { declareCovenant, channel } from "@covenant-rpc/core";
4
- import { CovenantServer } from "../server";
5
- import { CovenantClient } from "@covenant-rpc/client";
6
- import { httpClientToServer } from "@covenant-rpc/client/interfaces/http";
7
- import { httpServerToSidekick } from "../interfaces/http";
8
- import { InternalSidekick } from "@covenant-rpc/sidekick/internal";
9
-
10
- // Helper to create a mock HTTP server
11
- async function createMockHttpServer(server: CovenantServer<any, any, any, any>) {
12
- const handlers = new Map<string, (req: Request) => Promise<Response>>();
13
-
14
- // Add server handler
15
- handlers.set("server", (req) => server.handle(req));
16
-
17
- const mockFetch = async (url: string | URL | Request, init?: RequestInit): Promise<Response> => {
18
- const requestUrl = typeof url === 'string' ? url : url instanceof URL ? url.toString() : url.url;
19
- const urlObj = new URL(requestUrl);
20
- const type = urlObj.searchParams.get("type");
21
-
22
- const request = new Request(requestUrl, init);
23
-
24
- if (type === "procedure" || type === "connect" || type === "channel") {
25
- const handler = handlers.get("server");
26
- if (handler) {
27
- return await handler(request);
28
- }
29
- }
30
-
31
- return new Response("Not Found", { status: 404 });
32
- };
33
-
34
- // @ts-expect-error - mocking global fetch
35
- global.fetch = mockFetch;
36
-
37
- return {
38
- cleanup: () => {
39
- // @ts-expect-error - restore
40
- global.fetch = undefined;
41
- }
42
- };
43
- }
44
-
45
- test("HTTP channel connection request", async () => {
46
- const sidekick = new InternalSidekick();
47
-
48
- const covenant = declareCovenant({
49
- procedures: {},
50
- channels: {
51
- chat: channel({
52
- clientMessage: z.object({ text: z.string() }),
53
- serverMessage: z.object({ text: z.string() }),
54
- connectionRequest: z.object({ username: z.string() }),
55
- connectionContext: z.object({ userId: z.string() }),
56
- params: ["roomId"],
57
- }),
58
- },
59
- });
60
-
61
- const server = new CovenantServer(covenant, {
62
- contextGenerator: () => undefined,
63
- derivation: () => {},
64
- sidekickConnection: sidekick.getConnectionFromServer(),
65
- });
66
-
67
- sidekick.setServerCallback((channelName, params, data, context) =>
68
- server.processChannelMessage(channelName, params, data, context)
69
- );
70
-
71
- server.defineChannel("chat", {
72
- onConnect: ({ inputs, params }) => {
73
- expect(inputs.username).toBe("Alice");
74
- expect(params.roomId).toBe("room1");
75
- return { userId: `user-${inputs.username}` };
76
- },
77
- onMessage: () => {},
78
- });
79
-
80
- server.assertAllDefined();
81
-
82
- const mock = await createMockHttpServer(server);
83
-
84
- const client = new CovenantClient(covenant, {
85
- sidekickConnection: sidekick.getConnectionFromClient(),
86
- serverConnection: httpClientToServer("http://localhost:3000", {}),
87
- });
88
-
89
- const result = await client.connect("chat", { roomId: "room1" }, {
90
- username: "Alice",
91
- });
92
-
93
- expect(result.success).toBe(true);
94
- expect(result.error).toBe(null);
95
- if (result.success) {
96
- expect(result.token).toBeDefined();
97
- expect(typeof result.token).toBe("string");
98
- }
99
-
100
- mock.cleanup();
101
- });
102
-
103
- test("HTTP channel connection error handling", async () => {
104
- const sidekick = new InternalSidekick();
105
-
106
- const covenant = declareCovenant({
107
- procedures: {},
108
- channels: {
109
- restricted: channel({
110
- clientMessage: z.null(),
111
- serverMessage: z.null(),
112
- connectionRequest: z.object({ password: z.string() }),
113
- connectionContext: z.object({ authenticated: z.boolean() }),
114
- params: [],
115
- }),
116
- },
117
- });
118
-
119
- const server = new CovenantServer(covenant, {
120
- contextGenerator: () => undefined,
121
- derivation: () => {},
122
- sidekickConnection: sidekick.getConnectionFromServer(),
123
- });
124
-
125
- sidekick.setServerCallback((channelName, params, data, context) =>
126
- server.processChannelMessage(channelName, params, data, context)
127
- );
128
-
129
- server.defineChannel("restricted", {
130
- onConnect: ({ inputs, reject }) => {
131
- if (inputs.password !== "secret123") {
132
- reject("Invalid password", "client");
133
- }
134
- return { authenticated: true };
135
- },
136
- onMessage: () => {},
137
- });
138
-
139
- server.assertAllDefined();
140
-
141
- const mock = await createMockHttpServer(server);
142
-
143
- const client = new CovenantClient(covenant, {
144
- sidekickConnection: sidekick.getConnectionFromClient(),
145
- serverConnection: httpClientToServer("http://localhost:3000", {}),
146
- });
147
-
148
- // Try with wrong password
149
- const result = await client.connect("restricted", {}, {
150
- password: "wrong",
151
- });
152
-
153
- expect(result.success).toBe(false);
154
- if (!result.success) {
155
- expect(result.error).toBeDefined();
156
- expect(result.error.message).toBe("Invalid password");
157
- expect(result.error.fault).toBe("client");
158
- }
159
-
160
- mock.cleanup();
161
- });
162
-
163
- test("HTTP server-to-sidekick message posting", async () => {
164
- const receivedMessages: any[] = [];
165
-
166
- // Create a mock sidekick HTTP endpoint
167
- const mockSidekickFetch = async (url: string | URL | Request, init?: RequestInit): Promise<Response> => {
168
- const requestUrl = typeof url === 'string' ? url : url instanceof URL ? url.toString() : url.url;
169
- const urlObj = new URL(requestUrl);
170
-
171
- if (urlObj.pathname === "/connection") {
172
- const body = await (init?.body ? JSON.parse(init.body as string) : {});
173
- receivedMessages.push({ type: "connection", ...body });
174
- return new Response(null, { status: 200 });
175
- }
176
-
177
- if (urlObj.pathname === "/message") {
178
- const body = await (init?.body ? JSON.parse(init.body as string) : {});
179
- receivedMessages.push({ type: "message", ...body });
180
- return new Response(null, { status: 200 });
181
- }
182
-
183
- if (urlObj.pathname === "/resources") {
184
- const body = await (init?.body ? JSON.parse(init.body as string) : {});
185
- receivedMessages.push({ type: "resources", ...body });
186
- return new Response(null, { status: 200 });
187
- }
188
-
189
- return new Response("Not Found", { status: 404 });
190
- };
191
-
192
- // @ts-expect-error - mocking global fetch
193
- global.fetch = mockSidekickFetch;
194
-
195
- const sidekickConnection = httpServerToSidekick("http://localhost:4000", "test-key");
196
-
197
- const covenant = declareCovenant({
198
- procedures: {},
199
- channels: {
200
- notifications: channel({
201
- clientMessage: z.null(),
202
- serverMessage: z.object({ text: z.string() }),
203
- connectionRequest: z.null(),
204
- connectionContext: z.null(),
205
- params: [],
206
- }),
207
- },
208
- });
209
-
210
- const server = new CovenantServer(covenant, {
211
- contextGenerator: () => undefined,
212
- derivation: () => {},
213
- sidekickConnection,
214
- });
215
-
216
- server.defineChannel("notifications", {
217
- onConnect: () => null,
218
- onMessage: () => {},
219
- });
220
-
221
- server.assertAllDefined();
222
-
223
- // Test adding connection
224
- const connectionError = await sidekickConnection.addConnection({
225
- token: "test-token-123",
226
- channel: "notifications",
227
- params: {},
228
- context: null,
229
- });
230
-
231
- expect(connectionError).toBe(null);
232
- expect(receivedMessages.length).toBe(1);
233
- expect(receivedMessages[0].type).toBe("connection");
234
- expect(receivedMessages[0].token).toBe("test-token-123");
235
-
236
- // Test posting message
237
- const messageError = await sidekickConnection.postMessage({
238
- channel: "notifications",
239
- params: {},
240
- data: { text: "Hello!" },
241
- });
242
-
243
- expect(messageError).toBe(null);
244
- expect(receivedMessages.length).toBe(2);
245
- expect(receivedMessages[1].type).toBe("message");
246
- expect(receivedMessages[1].data).toEqual({ text: "Hello!" });
247
-
248
- // Test updating resources
249
- const updateError = await sidekickConnection.update(["resource1", "resource2"]);
250
-
251
- expect(updateError).toBe(null);
252
- expect(receivedMessages.length).toBe(3);
253
- expect(receivedMessages[2].type).toBe("resources");
254
- expect(receivedMessages[2].resources).toEqual(["resource1", "resource2"]);
255
-
256
- // @ts-expect-error - restore
257
- global.fetch = undefined;
258
- });
259
-
260
- test("HTTP channel message from sidekick to server", async () => {
261
- const receivedMessages: Array<{ text: string; userId: string }> = [];
262
-
263
- const covenant = declareCovenant({
264
- procedures: {},
265
- channels: {
266
- chat: channel({
267
- clientMessage: z.object({ text: z.string() }),
268
- serverMessage: z.null(),
269
- connectionRequest: z.object({ username: z.string() }),
270
- connectionContext: z.object({ userId: z.string() }),
271
- params: ["roomId"],
272
- }),
273
- },
274
- });
275
-
276
- const server = new CovenantServer(covenant, {
277
- contextGenerator: () => undefined,
278
- derivation: () => {},
279
- sidekickConnection: httpServerToSidekick("http://localhost:4000", "test-key"),
280
- });
281
-
282
- server.defineChannel("chat", {
283
- onConnect: ({ inputs }) => {
284
- return { userId: `user-${inputs.username}` };
285
- },
286
- onMessage: ({ inputs, context }) => {
287
- receivedMessages.push({
288
- text: inputs.text,
289
- userId: context.userId,
290
- });
291
- },
292
- });
293
-
294
- server.assertAllDefined();
295
-
296
- const mock = await createMockHttpServer(server);
297
-
298
- // Simulate sidekick sending a message to the server
299
- const response = await fetch("http://localhost:3000?type=channel", {
300
- method: "POST",
301
- headers: {
302
- "Content-Type": "application/json",
303
- "Authorization": "Bearer test-key",
304
- },
305
- body: JSON.stringify({
306
- channel: "chat",
307
- params: { roomId: "general" },
308
- data: { text: "Hello from sidekick!" },
309
- context: { userId: "user-Bob" },
310
- }),
311
- });
312
-
313
- expect(response.status).toBe(204);
314
- expect(receivedMessages.length).toBe(1);
315
- expect(receivedMessages[0]).toEqual({
316
- text: "Hello from sidekick!",
317
- userId: "user-Bob",
318
- });
319
-
320
- mock.cleanup();
321
- });
322
-
323
- test("HTTP channel message error handling", async () => {
324
- const covenant = declareCovenant({
325
- procedures: {},
326
- channels: {
327
- moderated: channel({
328
- clientMessage: z.object({ text: z.string() }),
329
- serverMessage: z.null(),
330
- connectionRequest: z.null(),
331
- connectionContext: z.null(),
332
- params: [],
333
- }),
334
- },
335
- });
336
-
337
- const server = new CovenantServer(covenant, {
338
- contextGenerator: () => undefined,
339
- derivation: () => {},
340
- sidekickConnection: httpServerToSidekick("http://localhost:4000", "test-key"),
341
- });
342
-
343
- server.defineChannel("moderated", {
344
- onConnect: () => null,
345
- onMessage: ({ inputs, error }) => {
346
- if (inputs.text.includes("spam")) {
347
- error("Message contains spam", "client");
348
- }
349
- },
350
- });
351
-
352
- server.assertAllDefined();
353
-
354
- const mock = await createMockHttpServer(server);
355
-
356
- // Simulate sidekick sending a spam message
357
- const response = await fetch("http://localhost:3000?type=channel", {
358
- method: "POST",
359
- headers: {
360
- "Content-Type": "application/json",
361
- "Authorization": "Bearer test-key",
362
- },
363
- body: JSON.stringify({
364
- channel: "moderated",
365
- params: {},
366
- data: { text: "This is spam content" },
367
- context: null,
368
- }),
369
- });
370
-
371
- expect(response.status).toBe(400);
372
- const errorBody = await response.json();
373
- //@ts-expect-error we are doing the validation
374
- expect(errorBody.fault).toBe("client");
375
- //@ts-expect-error we are doing the validation
376
- expect(errorBody.message).toContain("spam");
377
-
378
- mock.cleanup();
379
- });
380
-
381
- test("HTTP connection with invalid channel", async () => {
382
- const sidekick = new InternalSidekick();
383
-
384
- const covenant = declareCovenant({
385
- procedures: {},
386
- channels: {
387
- chat: channel({
388
- clientMessage: z.null(),
389
- serverMessage: z.null(),
390
- connectionRequest: z.null(),
391
- connectionContext: z.null(),
392
- params: [],
393
- }),
394
- },
395
- });
396
-
397
- const server = new CovenantServer(covenant, {
398
- contextGenerator: () => undefined,
399
- derivation: () => {},
400
- sidekickConnection: sidekick.getConnectionFromServer(),
401
- });
402
-
403
- server.defineChannel("chat", {
404
- onConnect: () => null,
405
- onMessage: () => {},
406
- });
407
-
408
- server.assertAllDefined();
409
-
410
- const mock = await createMockHttpServer(server);
411
-
412
- const client = new CovenantClient(covenant, {
413
- sidekickConnection: sidekick.getConnectionFromClient(),
414
- serverConnection: httpClientToServer("http://localhost:3000", {}),
415
- });
416
-
417
- // Try to connect to a channel that doesn't exist
418
- // @ts-expect-error - intentionally using wrong channel name
419
- const result = await client.connect("nonexistent", {}, null);
420
-
421
- expect(result.success).toBe(false);
422
- if (!result.success) {
423
- expect(result.error).toBeDefined();
424
- expect(result.error.fault).toBe("server");
425
- }
426
-
427
- mock.cleanup();
428
- });
429
-
430
- test("HTTP connection with invalid request data", async () => {
431
- const sidekick = new InternalSidekick();
432
-
433
- const covenant = declareCovenant({
434
- procedures: {},
435
- channels: {
436
- chat: channel({
437
- clientMessage: z.null(),
438
- serverMessage: z.null(),
439
- connectionRequest: z.object({
440
- username: z.string(),
441
- email: z.string().email(),
442
- }),
443
- connectionContext: z.null(),
444
- params: [],
445
- }),
446
- },
447
- });
448
-
449
- const server = new CovenantServer(covenant, {
450
- contextGenerator: () => undefined,
451
- derivation: () => {},
452
- sidekickConnection: sidekick.getConnectionFromServer(),
453
- });
454
-
455
- server.defineChannel("chat", {
456
- onConnect: () => null,
457
- onMessage: () => {},
458
- });
459
-
460
- server.assertAllDefined();
461
-
462
- const mock = await createMockHttpServer(server);
463
-
464
- const client = new CovenantClient(covenant, {
465
- sidekickConnection: sidekick.getConnectionFromClient(),
466
- serverConnection: httpClientToServer("http://localhost:3000", {}),
467
- });
468
-
469
- // Try to connect with invalid email
470
- const result = await client.connect("chat", {}, {
471
- username: "Alice",
472
- email: "not-an-email",
473
- });
474
-
475
- expect(result.success).toBe(false);
476
- if (!result.success) {
477
- expect(result.error).toBeDefined();
478
- expect(result.error.fault).toBe("client");
479
- expect(result.error.message).toContain("Invalid");
480
- }
481
-
482
- mock.cleanup();
483
- });