arisa 4.0.14 → 4.0.18

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.
@@ -1,267 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { createServer } from "node:http";
3
- import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
4
- import os from "node:os";
5
- import path from "node:path";
6
- import test from "node:test";
7
- import { createWebRouter } from "../src/runtime/web/web-router.js";
8
-
9
- async function createHandlerFiles() {
10
- const root = await mkdtemp(path.join(os.tmpdir(), "arisa-web-router-"));
11
- await mkdir(root, { recursive: true });
12
- const okHandler = path.join(root, "ok.js");
13
- const errorHandler = path.join(root, "error.js");
14
-
15
- await writeFile(okHandler, `export async function handleWebRequest(_req, res, context) {
16
- res.writeHead(200, { "Content-Type": "application/json" });
17
- res.end(JSON.stringify({ toolName: context.toolName, chatId: context.chatId }));
18
- }
19
- `, "utf8");
20
-
21
- await writeFile(errorHandler, `export async function handleWebRequest() {
22
- throw new Error("secret stack marker");
23
- }
24
- `, "utf8");
25
-
26
- return { okHandler, errorHandler };
27
- }
28
-
29
- function buildRouter({ routes, token = "secret-token", limits, logger } = {}) {
30
- return createWebRouter({
31
- getRoutes: () => routes,
32
- getToken: () => token,
33
- logger,
34
- limits,
35
- buildContext: ({ toolName, chatId }) => ({ toolName, chatId })
36
- });
37
- }
38
-
39
- async function startRouterServer(router) {
40
- const server = createServer((req, res) => {
41
- router.dispatch(req, res);
42
- });
43
- await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
44
- const address = server.address();
45
- const baseUrl = `http://127.0.0.1:${address.port}`;
46
- return {
47
- baseUrl,
48
- close: () => new Promise((resolve, reject) => {
49
- server.close((error) => error ? reject(error) : resolve());
50
- })
51
- };
52
- }
53
-
54
- test("protected routes require the shared bearer token", async () => {
55
- const { okHandler } = await createHandlerFiles();
56
- const routes = [
57
- { toolName: "private-tool", path: "/private", methods: ["GET"], public: false, handlerPath: okHandler }
58
- ];
59
- const router = createWebRouter({
60
- getRoutes: () => routes,
61
- getToken: () => "secret-token",
62
- buildContext: ({ toolName, chatId }) => ({ toolName, chatId })
63
- });
64
- const server = await startRouterServer(router);
65
-
66
- try {
67
- const rejected = await fetch(`${server.baseUrl}/private`);
68
- assert.equal(rejected.status, 401);
69
-
70
- const accepted = await fetch(`${server.baseUrl}/private?chatId=123`, {
71
- headers: { Authorization: "Bearer secret-token" }
72
- });
73
- assert.equal(accepted.status, 200);
74
- assert.deepEqual(await accepted.json(), { toolName: "private-tool", chatId: 123 });
75
- } finally {
76
- await server.close();
77
- }
78
- });
79
-
80
- test("public routes do not require a token", async () => {
81
- const { okHandler } = await createHandlerFiles();
82
- const routes = [
83
- { toolName: "public-tool", path: "/public", methods: ["GET"], public: true, handlerPath: okHandler }
84
- ];
85
- const router = createWebRouter({
86
- getRoutes: () => routes,
87
- getToken: () => "secret-token",
88
- buildContext: ({ toolName, chatId }) => ({ toolName, chatId })
89
- });
90
- const server = await startRouterServer(router);
91
-
92
- try {
93
- const response = await fetch(`${server.baseUrl}/public`);
94
- assert.equal(response.status, 200);
95
- assert.deepEqual(await response.json(), { toolName: "public-tool", chatId: null });
96
- } finally {
97
- await server.close();
98
- }
99
- });
100
-
101
- test("disallows undeclared HTTP methods", async () => {
102
- const { okHandler } = await createHandlerFiles();
103
- const routes = [
104
- { toolName: "public-tool", path: "/public", methods: ["GET"], public: true, handlerPath: okHandler }
105
- ];
106
- const router = createWebRouter({
107
- getRoutes: () => routes,
108
- getToken: () => "secret-token",
109
- buildContext: ({ toolName, chatId }) => ({ toolName, chatId })
110
- });
111
- const server = await startRouterServer(router);
112
-
113
- try {
114
- const response = await fetch(`${server.baseUrl}/public`, { method: "POST" });
115
- assert.equal(response.status, 405);
116
- assert.equal(response.headers.get("allow"), "GET");
117
- } finally {
118
- await server.close();
119
- }
120
- });
121
-
122
- test("handler errors do not expose stack traces to clients", async () => {
123
- const { errorHandler } = await createHandlerFiles();
124
- const logs = [];
125
- const routes = [
126
- { toolName: "error-tool", path: "/error", methods: ["GET"], public: true, handlerPath: errorHandler }
127
- ];
128
- const router = createWebRouter({
129
- getRoutes: () => routes,
130
- getToken: () => "secret-token",
131
- logger: { error: (scope, message) => logs.push({ scope, message }) },
132
- buildContext: ({ toolName, chatId }) => ({ toolName, chatId })
133
- });
134
- const server = await startRouterServer(router);
135
-
136
- try {
137
- const response = await fetch(`${server.baseUrl}/error`);
138
- const body = await response.text();
139
- assert.equal(response.status, 500);
140
- assert.equal(body.includes("secret stack marker"), false);
141
- assert.equal(body.includes("Error:"), false);
142
- assert.equal(logs[0].scope, "web");
143
- assert.match(logs[0].message, /secret stack marker/);
144
- } finally {
145
- await server.close();
146
- }
147
- });
148
-
149
- test("core routes take precedence over tool routes", async () => {
150
- const { okHandler } = await createHandlerFiles();
151
- const router = buildRouter({
152
- routes: [
153
- { toolName: "tool", path: "/health", methods: ["GET"], public: true, handlerPath: okHandler }
154
- ]
155
- });
156
- router.registerCoreRoute({
157
- method: "GET",
158
- path: "/health",
159
- handler: (_req, res) => {
160
- res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
161
- res.end("core-health");
162
- }
163
- });
164
- const server = await startRouterServer(router);
165
-
166
- try {
167
- const response = await fetch(`${server.baseUrl}/health`);
168
- assert.equal(response.status, 200);
169
- assert.equal(await response.text(), "core-health");
170
- } finally {
171
- await server.close();
172
- }
173
- });
174
-
175
- test("unknown paths return a plain ok response", async () => {
176
- const router = buildRouter({ routes: [] });
177
- const server = await startRouterServer(router);
178
-
179
- try {
180
- const response = await fetch(`${server.baseUrl}/does-not-exist`);
181
- assert.equal(response.status, 200);
182
- assert.equal(await response.text(), "ok");
183
- } finally {
184
- await server.close();
185
- }
186
- });
187
-
188
- test("rejects request bodies over the configured limit", async () => {
189
- const { okHandler } = await createHandlerFiles();
190
- const router = buildRouter({
191
- routes: [
192
- { toolName: "upload-tool", path: "/upload", methods: ["POST"], public: true, handlerPath: okHandler }
193
- ],
194
- limits: { bodyLimitBytes: 8 }
195
- });
196
- const server = await startRouterServer(router);
197
-
198
- try {
199
- const response = await fetch(`${server.baseUrl}/upload`, {
200
- method: "POST",
201
- body: "x".repeat(64)
202
- });
203
- assert.equal(response.status, 413);
204
- } finally {
205
- await server.close();
206
- }
207
- });
208
-
209
- test("accepts the shared token from the query string", async () => {
210
- const { okHandler } = await createHandlerFiles();
211
- const router = buildRouter({
212
- routes: [
213
- { toolName: "private-tool", path: "/private", methods: ["GET"], public: false, handlerPath: okHandler }
214
- ]
215
- });
216
- const server = await startRouterServer(router);
217
-
218
- try {
219
- const response = await fetch(`${server.baseUrl}/private?token=secret-token`);
220
- assert.equal(response.status, 200);
221
- } finally {
222
- await server.close();
223
- }
224
- });
225
-
226
- test("rejects wrong tokens and locks down protected routes without a configured token", async () => {
227
- const { okHandler } = await createHandlerFiles();
228
- const protectedRoute = { toolName: "private-tool", path: "/private", methods: ["GET"], public: false, handlerPath: okHandler };
229
-
230
- const wrongTokenServer = await startRouterServer(buildRouter({ routes: [protectedRoute] }));
231
- try {
232
- const response = await fetch(`${wrongTokenServer.baseUrl}/private`, {
233
- headers: { Authorization: "Bearer wrong-token" }
234
- });
235
- assert.equal(response.status, 401);
236
- } finally {
237
- await wrongTokenServer.close();
238
- }
239
-
240
- const noTokenServer = await startRouterServer(buildRouter({ routes: [protectedRoute], token: "" }));
241
- try {
242
- const response = await fetch(`${noTokenServer.baseUrl}/private`, {
243
- headers: { Authorization: "Bearer anything" }
244
- });
245
- assert.equal(response.status, 401);
246
- } finally {
247
- await noTokenServer.close();
248
- }
249
- });
250
-
251
- test("passes a non-numeric chatId through to the handler context", async () => {
252
- const { okHandler } = await createHandlerFiles();
253
- const router = buildRouter({
254
- routes: [
255
- { toolName: "public-tool", path: "/public", methods: ["GET"], public: true, handlerPath: okHandler }
256
- ]
257
- });
258
- const server = await startRouterServer(router);
259
-
260
- try {
261
- const response = await fetch(`${server.baseUrl}/public?chatId=team-7`);
262
- assert.equal(response.status, 200);
263
- assert.deepEqual(await response.json(), { toolName: "public-tool", chatId: "team-7" });
264
- } finally {
265
- await server.close();
266
- }
267
- });