@jrpool/kilotest 35.0.1 → 36.1.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.
package/SERVICE.md CHANGED
@@ -20,6 +20,8 @@ Any other application `xyz` can be installed at `/opt/jpdev/xyz` on the server.
20
20
 
21
21
  ## Process management
22
22
 
23
+ ### Process manager
24
+
23
25
  Kilotest is managed with [PM2](https://pm2.keymetrics.io) on the server (not on the local development host). The PM2 configuration is specified in the repository as `pm2.config.js`:
24
26
 
25
27
  ```javascript
@@ -49,6 +51,10 @@ pm2 restart kilotest --time --update-env
49
51
  pm2 save
50
52
  ```
51
53
 
54
+ The management of the PM2 logs is [documented by PM2](https://pm2.keymetrics.io/docs/usage/log-management/).
55
+
56
+ ### Process tuning
57
+
52
58
  The server configuration has been tuned for improved performance. The file edited for this purpose is `/etc/default/zramswap`:
53
59
 
54
60
  ```properties
@@ -190,22 +196,52 @@ The Caddy configuration is maintained and tracked in `/etc/caddy/Caddyfile`. Lea
190
196
  kilotest.com {
191
197
  # Enable Zstandard and Gzip compression of responses.
192
198
  encode zstd gzip
193
-
194
- # Truncate the initial /qai from the forwarded path.
199
+ # Specify the only paths of forwardable requests.
200
+ @allowedGET {
201
+ method GET
202
+ path /mcp / /index.html /robots.txt /openapi.yaml /openapi.json /swagger.yaml /swagger.json /api-docs /llms.txt /llms-full.txt *.html* /fullReport.json/* /api/* /tutorial/images/* /favicon.* /style.css /sitemap.xml
203
+ }
204
+ @allowedPOST {
205
+ method POST
206
+ path /mcp /retestRec.html/* /testRec.html/* /recAction.html /reannotate.html /wcagRenew.html /api/* /tutorialComment.html
207
+ }
208
+ @allowedOPTIONS method OPTIONS
209
+ handle @allowedOPTIONS {
210
+ header {
211
+ Access-Control-Allow-Origin *
212
+ Access-Control-Allow-Methods GET,POST,OPTIONS
213
+ Access-Control-Allow-Headers Content-Type,Authorization
214
+ }
215
+ respond 204
216
+ }
217
+ # Truncate any initial /qai from the forwarded path.
195
218
  handle_path /qai* {
196
- # Forward any matching request to port 3001.
219
+ # Forward any QAI request to port 3001.
197
220
  reverse_proxy localhost:3001
198
221
  }
199
-
200
- # Forward any other request to port 3000.
201
- reverse_proxy localhost:3000 {
202
- # Improve SSE latency.
203
- flush_interval -1
222
+ # Forward any other GET, POST, or OPTIONS request, if allowed, to port 3000.
223
+ handle @allowedGET {
224
+ reverse_proxy localhost:3000 {
225
+ # Improve SSE latency.
226
+ flush_interval -1
227
+ }
228
+ }
229
+ handle @allowedPOST {
230
+ reverse_proxy localhost:3000 {
231
+ # Improve SSE latency.
232
+ flush_interval -1
233
+ }
234
+ }
235
+ # Return not-found for any other request.
236
+ handle {
237
+ respond 404
204
238
  }
205
239
  }
206
240
  ```
207
241
 
208
- This configuration prevents granular reporting by Testaro agents from being buffered, so the updates reach the browser without delay.
242
+ This configuration can be futher tightened if experience warrants.
243
+
244
+ The `flush_interval` setting prevents granular reporting by Testaro agents from being buffered, so the updates reach the browser without delay.
209
245
 
210
246
  ## Version management
211
247
 
@@ -47,7 +47,7 @@ exports.answer = async (_, search) => {
47
47
  // Report the error.
48
48
  return {
49
49
  status: 'error',
50
- error: 'Invalid authorization code'
50
+ message: 'Invalid authorization code'
51
51
  }
52
52
  }
53
53
  }
@@ -60,7 +60,7 @@ exports.answer = async (_, search) => {
60
60
  oldBalance = `$${balanceData.balance} is the`;
61
61
  }
62
62
  // If the balance file does not exist or is invalid:
63
- catch (error) {
63
+ catch {
64
64
  oldBalance = 'There is no';
65
65
  }
66
66
  }
@@ -135,6 +135,6 @@ exports.answer = async (pageArgs, search) => {
135
135
  // Otherwise, report this.
136
136
  return {
137
137
  status: 'error',
138
- error: 'Invalid report specification'
138
+ message: 'Invalid report specification'
139
139
  };
140
140
  };
package/index.js CHANGED
@@ -28,7 +28,7 @@ const {
28
28
  reportsPath,
29
29
  ruleIDs
30
30
  } = require('./util');
31
- const {handleMCP} = require('./mcp');
31
+ const {handleMCP, mcpPath} = require('./mcp');
32
32
  const fs = require('fs/promises');
33
33
  const http = require('http');
34
34
  const https = require('https');
@@ -80,7 +80,8 @@ const AI_MODEL0_OUTPUT_PRICE = Number(process.env.AI_MODEL0_OUTPUT_PRICE);
80
80
 
81
81
  // Serves or sends an error message.
82
82
  const serveError = async (error, response, isHumanUser = true) => {
83
- console.log(error.message || 'ERROR');
83
+ const errorLines = Object.entries(error).map(pair => `${pair[0]}: ${pair[1]}`);
84
+ console.log(errorLines.join('\n') || 'ERROR');
84
85
  if (! response.writableEnded) {
85
86
  response.statusCode = 400;
86
87
  // If the request is from a human user:
@@ -128,7 +129,7 @@ const checkBalancesForAlerts = async report => {
128
129
  // Get the recorded AI service 0 balance.
129
130
  balanceJSON = await fs.readFile(balancePath, 'utf8');
130
131
  }
131
- catch (error) {
132
+ catch {
132
133
  console.error('ERROR: AI service 0 balance file missing');
133
134
  }
134
135
  // If the variables required for an AI service 0 balance alert are defined:
@@ -169,6 +170,21 @@ const checkBalancesForAlerts = async report => {
169
170
  }
170
171
  }
171
172
  };
173
+ // Creates an error object about a suspicious request.
174
+ const getAbuseError = (request, reason) => {
175
+ const {method, url, headers} = request;
176
+ const ip = headers['x-forwarded-for'] || request.socket.remoteAddress || 'unknown';
177
+ return {
178
+ message: 'Invalid request',
179
+ reason,
180
+ 'IP address': ip,
181
+ method,
182
+ URL: url,
183
+ 'user agent': headers['user-agent'] || 'none',
184
+ referer: headers.referer || 'none',
185
+ time: new Date().toISOString()
186
+ };
187
+ };
172
188
  // Handles a request.
173
189
  const requestHandler = async (request, response) => {
174
190
  // Sets response headers.
@@ -194,19 +210,11 @@ const requestHandler = async (request, response) => {
194
210
  const {pathname, search} = requestURL;
195
211
  const pageName = pathname.split('/')[1];
196
212
  const pathTail = pathname.split('/').slice(2).join('/');
197
- // If the request is an OPTIONS request:
198
- if (method === 'OPTIONS') {
199
- // Serve response headers, including one allowing requests from other applications.
200
- response.setHeader('Access-Control-Allow-Origin', '*');
201
- response.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
202
- response.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
203
- response.statusCode = 204;
204
- response.end();
205
- }
206
- // Otherwise, if the request is a GET request:
207
- else if (method === 'GET') {
213
+ // If the request is a GET request:
214
+ if (method === 'GET') {
208
215
  // If it is for the model context protocol server:
209
- if (pathname === '/mcp') {
216
+ if (pathname === mcpPath) {
217
+ // Handle the MCP request.
210
218
  await handleMCP(request, response);
211
219
  }
212
220
  // Otherwise, if it is for the home page:
@@ -251,6 +259,13 @@ const requestHandler = async (request, response) => {
251
259
  setHeaders('text/plain', '/llms-full.txt', 'medium');
252
260
  response.end(llmsfull);
253
261
  }
262
+ // Otherwise, if it is for the XML sitemap:
263
+ else if (pageName === 'sitemap.xml') {
264
+ const sitemap = await fs.readFile('sitemap.xml', 'utf8');
265
+ // Serve it.
266
+ setHeaders('application/xml', '/sitemap.xml', 'medium');
267
+ response.end(sitemap);
268
+ }
254
269
  // Otherwise, if it is for a full report download:
255
270
  else if (pageName === 'fullReport.json') {
256
271
  const [timeStamp, jobID] = pathTail.split('/');
@@ -260,17 +275,17 @@ const requestHandler = async (request, response) => {
260
275
  // If the report exists and is hidden:
261
276
  if (reportHidden === true) {
262
277
  console.log(`ERROR: Hidden report ${timeStamp}-${jobID} requested`);
263
- // Report this.
278
+ // Report this as suspected abuse.
264
279
  await serveError(
265
- {message: `ERROR: requested report ${timeStamp}-${jobID} not available`},
280
+ getAbuseError(request, `Requested report ${timeStamp}-${jobID} not available`),
266
281
  response,
267
282
  true
268
283
  );
269
284
  }
270
- // Otherwise, if any other error occurred:
285
+ // Otherwise, if the report does not exist or another error occurred:
271
286
  else if (typeof reportHidden === 'string') {
272
- // Report it.
273
- await serveError({message: reportHidden}, response, true);
287
+ // Report this as suspected abuse.
288
+ await serveError(getAbuseError(request, reportHidden), response, true);
274
289
  }
275
290
  // Otherwise, i.e. if the report log is valid and not hidden:
276
291
  else {
@@ -314,14 +329,14 @@ const requestHandler = async (request, response) => {
314
329
  }
315
330
  // Otherwise, i.e. if they are invalid:
316
331
  else {
317
- // Report the error.
318
- await serveError({message: answerData.error}, response, true);
332
+ // Report the error as suspected abuse.
333
+ await serveError(getAbuseError(request, answerData.message), response, true);
319
334
  }
320
335
  }
321
336
  // Otherwise, i.e. if the answer cannot be generated:
322
337
  else {
323
- // Report the error.
324
- await serveError({message: 'ERROR: Invalid request'}, response, true);
338
+ // Report the error as suspected abuse.
339
+ await serveError(getAbuseError(request, 'Request for nonexistent page'), response, true);
325
340
  }
326
341
  }
327
342
  // Otherwise, if it is for an API service:
@@ -377,7 +392,7 @@ const requestHandler = async (request, response) => {
377
392
  setHeaders(mimeTypes[ext] || 'application/octet-stream', null, 'low');
378
393
  response.end(img);
379
394
  }
380
- catch (_) {
395
+ catch {
381
396
  await serveError({message: 'ERROR: Image not found'}, response, true);
382
397
  }
383
398
  }
@@ -439,7 +454,7 @@ const requestHandler = async (request, response) => {
439
454
  // Otherwise, i.e. if they are invalid:
440
455
  else {
441
456
  // Report the error.
442
- await serveError({message: answerData.error}, response, true);
457
+ await serveError({message: answerData.message}, response, true);
443
458
  }
444
459
  }
445
460
  // Otherwise, i.e. if the request is invalid:
@@ -473,7 +488,7 @@ const requestHandler = async (request, response) => {
473
488
  // Otherwise, i.e. if they are invalid:
474
489
  else {
475
490
  // Report the error.
476
- await serveError({message: answerData.error}, response, true);
491
+ await serveError({message: answerData.message}, response, true);
477
492
  }
478
493
  }
479
494
  }
@@ -495,18 +510,18 @@ const requestHandler = async (request, response) => {
495
510
  if (what) {
496
511
  // Set a location header for a response.
497
512
  response.setHeader('content-location', `${pathname}${search}`);
498
- // Get the answer data.
513
+ // Get the answer data about the remaining recommendations.
499
514
  const answerData = await require(path.join(__dirname, 'testOrder', 'index'))
500
515
  .answer(url, what, authCode);
501
516
  // If the answer data are valid:
502
517
  if (answerData.status === 'ok') {
503
- // Serve the answer page.
518
+ // Serve the test-order page with the remaining recommendations.
504
519
  response.end(answerData.answerPage);
505
520
  }
506
521
  // Otherwise, i.e. if they are invalid:
507
522
  else {
508
523
  // Report the error.
509
- await serveError({message: answerData.error}, response, true);
524
+ await serveError({message: answerData.message}, response, true);
510
525
  }
511
526
  }
512
527
  // Otherwise, i.e. if it is a rejection:
@@ -521,7 +536,7 @@ const requestHandler = async (request, response) => {
521
536
  response.setHeader('content-location', '/recActionForm.html');
522
537
  // Get the answer data.
523
538
  const answerData = await require(path.join(__dirname, 'recActionForm', 'index')).answer();
524
- // Serve the test-order form.
539
+ // Serve the test-order form with the remaining recommendations.
525
540
  response.end(answerData.answerPage);
526
541
  }
527
542
  }
@@ -547,7 +562,7 @@ const requestHandler = async (request, response) => {
547
562
  // Otherwise, i.e. if they are invalid:
548
563
  else {
549
564
  // Report the error.
550
- await serveError({message: answerData.error}, response, true);
565
+ await serveError({message: answerData.message}, response, true);
551
566
  }
552
567
  }
553
568
  // Otherwise, if it is a WCAG map renewal:
@@ -566,7 +581,7 @@ const requestHandler = async (request, response) => {
566
581
  // Otherwise, i.e. if they are invalid:
567
582
  else {
568
583
  // Report the error.
569
- await serveError({message: answerData.error}, response, true);
584
+ await serveError({message: answerData.message}, response, true);
570
585
  }
571
586
  }
572
587
  // Otherwise, if it is a request from an agent:
@@ -753,7 +768,7 @@ const requestHandler = async (request, response) => {
753
768
  }
754
769
  else {
755
770
  response.statusCode = 400;
756
- response.end(JSON.stringify({status: 'error', message: answerData.error}));
771
+ response.end(JSON.stringify({status: 'error', message: answerData.message}));
757
772
  }
758
773
  }
759
774
  // Otherwise, i.e. if it is any other POST request:
@@ -765,7 +780,7 @@ const requestHandler = async (request, response) => {
765
780
  }
766
781
  // Otherwise, i.e. if it is neither a GET nor a POST request:
767
782
  else {
768
- // Report its invalidity.
783
+ // Report its invalidity. (Caddy handles OPTIONS requests.)
769
784
  await serveError({message: 'ERROR: Invalid request method'}, response, true);
770
785
  }
771
786
  };
package/mcp.js CHANGED
@@ -15,6 +15,10 @@ const reportIssuesAPI = require('./reportIssues/api');
15
15
  const reportIssueAPI = require('./reportIssue/api');
16
16
  const testRecFormAPI = require('./testRecForm/api');
17
17
 
18
+ // CONSTANTS
19
+
20
+ exports.mcpPath = '/mcp';
21
+
18
22
  // FUNCTIONS
19
23
 
20
24
  // Creates and returns an McpServer with Kilotest tools registered.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jrpool/kilotest",
3
- "version": "35.0.1",
3
+ "version": "36.1.0",
4
4
  "description": "An ensemble testing service with a focus on accessibility",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -34,6 +34,6 @@ exports.answer = async authCode => {
34
34
  // Otherwise, i.e. if the authorization code is invalid, return an error page.
35
35
  return {
36
36
  status: 'error',
37
- error: 'Invalid authorization code'
37
+ message: 'Invalid authorization code'
38
38
  };
39
39
  };
@@ -32,7 +32,7 @@ exports.answer = async (_, search) => {
32
32
  // Report the error.
33
33
  return {
34
34
  status: 'error',
35
- error: 'Invalid authorization code'
35
+ message: 'Invalid authorization code'
36
36
  }
37
37
  }
38
38
  }
@@ -176,6 +176,6 @@ exports.answer = async pageArgs => {
176
176
  // Otherwise, report this.
177
177
  return {
178
178
  status: 'error',
179
- error: 'Invalid report specification'
179
+ message: 'Invalid report specification'
180
180
  };
181
181
  };
@@ -171,6 +171,6 @@ exports.answer = async pageArgs => {
171
171
  // Otherwise, i.e. if they were not obtained, report this.
172
172
  return {
173
173
  status: 'error',
174
- error: 'Report processing failed'
174
+ message: 'Report processing failed'
175
175
  };
176
176
  };
@@ -32,7 +32,7 @@ exports.answer = async (_, search) => {
32
32
  // Report the error.
33
33
  return {
34
34
  status: 'error',
35
- error: 'Invalid authorization code'
35
+ message: 'Invalid authorization code'
36
36
  }
37
37
  }
38
38
  }
@@ -33,7 +33,7 @@ exports.answer = async (_, search) => {
33
33
  // Report the error.
34
34
  return {
35
35
  status: 'error',
36
- error: 'Invalid authorization code'
36
+ message: 'Invalid authorization code'
37
37
  }
38
38
  }
39
39
  }
@@ -33,7 +33,7 @@ exports.answer = async (_, search) => {
33
33
  // Report the error.
34
34
  return {
35
35
  status: 'error',
36
- error: 'Invalid authorization code'
36
+ message: 'Invalid authorization code'
37
37
  }
38
38
  }
39
39
  }
@@ -33,7 +33,7 @@ exports.answer = async (_, search) => {
33
33
  // Report the error.
34
34
  return {
35
35
  status: 'error',
36
- error: 'Invalid authorization code'
36
+ message: 'Invalid authorization code'
37
37
  }
38
38
  }
39
39
  }
package/researchAgent.js CHANGED
@@ -63,7 +63,7 @@ const getContent = async response => {
63
63
  // If it is not JSON:
64
64
  catch {
65
65
  // Return this.
66
- resolve({error: `Response content not JSON (${contentString})`});
66
+ resolve({message: `Response content not JSON (${contentString})`});
67
67
  }
68
68
  });
69
69
  });
package/robots.txt CHANGED
@@ -1,4 +1,16 @@
1
1
  # robots.txt
2
2
  User-agent: *
3
3
  Allow: /
4
+ Disallow: /ai0BalanceForm.html
5
+ Disallow: /recActionForm.html
6
+ Disallow: /recAction.html
7
+ Disallow: /reannotateForm.html
8
+ Disallow: /reannotate.html
9
+ Disallow: /reportHideForm.html
10
+ Disallow: /reportUnhideForm.html
11
+ Disallow: /reportsPruneForm.html
12
+ Disallow: /reportsRewindForm.html
13
+ Disallow: /reportsExpungeForm.html
14
+ Disallow: /wcagRenewForm.html
15
+ Disallow: /wcagRenew.html
4
16
  Sitemap: https://kilotest.com/sitemap.xml
package/sitemap.xml CHANGED
@@ -8,4 +8,5 @@
8
8
  <url><loc>https://kilotest.com/tutorial.html</loc></url>
9
9
  <url><loc>https://kilotest.com/openapi.yaml</loc></url>
10
10
  <url><loc>https://kilotest.com/llms.txt</loc></url>
11
+ <url><loc>https://kilotest.com/llms-full.txt</loc></url>
11
12
  </urlset>
package/style.css CHANGED
@@ -57,7 +57,7 @@ code.thin {
57
57
  margin-top: 0;
58
58
  }
59
59
  details {
60
- padding: rem;
60
+ padding: 0.5rem;
61
61
  border: solid #000 1px;
62
62
  }
63
63
  .error {
package/targets/index.js CHANGED
@@ -12,7 +12,6 @@ const {
12
12
  getObject,
13
13
  getPageDataStrings,
14
14
  getRecs,
15
- getToolNamesString,
16
15
  getLogs,
17
16
  getReportData,
18
17
  isRecommendable,
@@ -11,7 +11,7 @@ const path = require('path');
11
11
 
12
12
  // FUNCTIONS
13
13
 
14
- // Implements a test order and returns an acknowledgement page.
14
+ // Implements a test order and returns a revised recommendations page.
15
15
  exports.answer = async (url, what, authCode) => {
16
16
  // If the arguments are valid:
17
17
  if (isURL(url) && what && authCode === process.env.AUTH_CODE) {
@@ -57,6 +57,6 @@ exports.answer = async (url, what, authCode) => {
57
57
  // Otherwise, i.e. if the authorization code is invalid, return an error page.
58
58
  return {
59
59
  status: 'error',
60
- error: 'Invalid authorization code'
60
+ message: 'Invalid authorization code'
61
61
  };
62
62
  };
package/testRec/index.js CHANGED
@@ -17,7 +17,7 @@ exports.answer = async (what, url, why) => {
17
17
  // Return an answer reporting this.
18
18
  return {
19
19
  status: 'error',
20
- error: `Page is already ${status}`
20
+ message: `Page is already ${status}`
21
21
  };
22
22
  }
23
23
  // Otherwise, i.e. if it is recommendable, process the recommendation.
package/tutorial/index.js CHANGED
@@ -33,11 +33,11 @@ exports.answer = async () => {
33
33
  // Sanitizes and saves a tutorial comment to comments.json.
34
34
  exports.saveComment = async content => {
35
35
  if (!content || typeof content !== 'string') {
36
- return {status: 'error', error: 'No content provided'};
36
+ return {status: 'error', message: 'No content provided'};
37
37
  }
38
38
  const sanitized = sanitize(content);
39
39
  if (!sanitized) {
40
- return {status: 'error', error: 'Comment is empty after sanitization'};
40
+ return {status: 'error', message: 'Comment is empty after sanitization'};
41
41
  }
42
42
  let comments = [];
43
43
  try {
package/util.js CHANGED
@@ -679,7 +679,7 @@ exports.processRec = async (testType, dirName, what, url, why) => {
679
679
  }
680
680
  return {
681
681
  status: 'error',
682
- error: 'Invalid recommendation'
682
+ message: 'Invalid recommendation'
683
683
  };
684
684
  };
685
685
  // Gets the WCAG Understanding link for a numeric WCAG standard identifier.
@@ -50,21 +50,21 @@ exports.answer = async authCode => {
50
50
  // Report this.
51
51
  return {
52
52
  status: 'error',
53
- error: 'No entries found in WCAG map source'
53
+ message: 'No entries found in WCAG map source'
54
54
  };
55
55
  }
56
56
  }
57
57
  else {
58
58
  return {
59
59
  status: 'error',
60
- error: 'WCAG map source not retrieved'
60
+ message: 'WCAG map source not retrieved'
61
61
  };
62
62
  }
63
63
  }
64
64
  else {
65
65
  return {
66
66
  status: 'error',
67
- error: 'Authorization code invalid'
67
+ message: 'Authorization code invalid'
68
68
  };
69
69
  }
70
70
  };