@covenant-rpc/server 0.1.3
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.
- package/CHANGELOG.md +17 -0
- package/lib/adapters/vanilla.ts +9 -0
- package/lib/index.ts +11 -0
- package/lib/interfaces/direct.ts +116 -0
- package/lib/interfaces/empty.ts +9 -0
- package/lib/interfaces/http.ts +111 -0
- package/lib/interfaces/mock.ts +32 -0
- package/lib/logger.ts +79 -0
- package/lib/server.ts +453 -0
- package/lib/sidekick/handlers.ts +173 -0
- package/lib/sidekick/index.ts +109 -0
- package/lib/sidekick/socket.ts +5 -0
- package/package.json +21 -0
- package/tests/channel-http.test.ts +481 -0
- package/tests/channel.test.ts +689 -0
- package/tests/procedure.test.ts +238 -0
- package/tests/sidekick.test.ts +23 -0
- package/tests/validation-types.test.ts +122 -0
- package/tests/validation.test.ts +144 -0
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@covenant-rpc/server",
|
|
3
|
+
"module": "lib/index.ts",
|
|
4
|
+
"version": "0.1.3",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./lib/index.ts",
|
|
11
|
+
"./*": "./lib/*.ts"
|
|
12
|
+
},
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"typescript": "^5"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@covenant-rpc/core": "workspace:*",
|
|
18
|
+
"@covenant-rpc/ion": "workspace:*",
|
|
19
|
+
"@covenant-rpc/request-serializer": "workspace:*"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { test, expect } from "bun:test";
|
|
3
|
+
import { declareCovenant, channel } from "@covenant-rpc/core";
|
|
4
|
+
import { CovenantServer } from "../lib/server";
|
|
5
|
+
import { CovenantClient } from "@covenant-rpc/client";
|
|
6
|
+
import { httpClientToServer } from "@covenant-rpc/client/interfaces/http";
|
|
7
|
+
import { httpServerToSidekick } from "../lib/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
|
+
expect(errorBody.fault).toBe("client");
|
|
374
|
+
expect(errorBody.message).toContain("spam");
|
|
375
|
+
|
|
376
|
+
mock.cleanup();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test("HTTP connection with invalid channel", async () => {
|
|
380
|
+
const sidekick = new InternalSidekick();
|
|
381
|
+
|
|
382
|
+
const covenant = declareCovenant({
|
|
383
|
+
procedures: {},
|
|
384
|
+
channels: {
|
|
385
|
+
chat: channel({
|
|
386
|
+
clientMessage: z.null(),
|
|
387
|
+
serverMessage: z.null(),
|
|
388
|
+
connectionRequest: z.null(),
|
|
389
|
+
connectionContext: z.null(),
|
|
390
|
+
params: [],
|
|
391
|
+
}),
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const server = new CovenantServer(covenant, {
|
|
396
|
+
contextGenerator: () => undefined,
|
|
397
|
+
derivation: () => {},
|
|
398
|
+
sidekickConnection: sidekick.getConnectionFromServer(),
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
server.defineChannel("chat", {
|
|
402
|
+
onConnect: () => null,
|
|
403
|
+
onMessage: () => {},
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
server.assertAllDefined();
|
|
407
|
+
|
|
408
|
+
const mock = await createMockHttpServer(server);
|
|
409
|
+
|
|
410
|
+
const client = new CovenantClient(covenant, {
|
|
411
|
+
sidekickConnection: sidekick.getConnectionFromClient(),
|
|
412
|
+
serverConnection: httpClientToServer("http://localhost:3000", {}),
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// Try to connect to a channel that doesn't exist
|
|
416
|
+
// @ts-expect-error - intentionally using wrong channel name
|
|
417
|
+
const result = await client.connect("nonexistent", {}, null);
|
|
418
|
+
|
|
419
|
+
expect(result.success).toBe(false);
|
|
420
|
+
if (!result.success) {
|
|
421
|
+
expect(result.error).toBeDefined();
|
|
422
|
+
expect(result.error.fault).toBe("server");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
mock.cleanup();
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("HTTP connection with invalid request data", async () => {
|
|
429
|
+
const sidekick = new InternalSidekick();
|
|
430
|
+
|
|
431
|
+
const covenant = declareCovenant({
|
|
432
|
+
procedures: {},
|
|
433
|
+
channels: {
|
|
434
|
+
chat: channel({
|
|
435
|
+
clientMessage: z.null(),
|
|
436
|
+
serverMessage: z.null(),
|
|
437
|
+
connectionRequest: z.object({
|
|
438
|
+
username: z.string(),
|
|
439
|
+
email: z.string().email(),
|
|
440
|
+
}),
|
|
441
|
+
connectionContext: z.null(),
|
|
442
|
+
params: [],
|
|
443
|
+
}),
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const server = new CovenantServer(covenant, {
|
|
448
|
+
contextGenerator: () => undefined,
|
|
449
|
+
derivation: () => {},
|
|
450
|
+
sidekickConnection: sidekick.getConnectionFromServer(),
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
server.defineChannel("chat", {
|
|
454
|
+
onConnect: () => null,
|
|
455
|
+
onMessage: () => {},
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
server.assertAllDefined();
|
|
459
|
+
|
|
460
|
+
const mock = await createMockHttpServer(server);
|
|
461
|
+
|
|
462
|
+
const client = new CovenantClient(covenant, {
|
|
463
|
+
sidekickConnection: sidekick.getConnectionFromClient(),
|
|
464
|
+
serverConnection: httpClientToServer("http://localhost:3000", {}),
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// Try to connect with invalid email
|
|
468
|
+
const result = await client.connect("chat", {}, {
|
|
469
|
+
username: "Alice",
|
|
470
|
+
email: "not-an-email",
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
expect(result.success).toBe(false);
|
|
474
|
+
if (!result.success) {
|
|
475
|
+
expect(result.error).toBeDefined();
|
|
476
|
+
expect(result.error.fault).toBe("client");
|
|
477
|
+
expect(result.error.message).toContain("Invalid");
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
mock.cleanup();
|
|
481
|
+
});
|