testaro 78.0.6 → 78.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 (361) hide show
  1. package/.gitattributes +94 -0
  2. package/.github/workflows/ci.yml +58 -0
  3. package/.github/workflows/publish.yml +49 -0
  4. package/.github/workflows/typescript.yml +35 -0
  5. package/AGENTS.md +2 -2
  6. package/CLAUDE.md +25 -19
  7. package/CONTAINERS.md +10 -6
  8. package/CONTRIBUTING.md +29 -3
  9. package/Dockerfile +1 -1
  10. package/README.md +75 -20
  11. package/UPGRADES.md +4 -0
  12. package/actSpecs-doc.md +13 -1
  13. package/actSpecs.js +29 -5
  14. package/call.js +6 -5
  15. package/docker-compose.yml +8 -2
  16. package/docs/checkpoint-scanning.md +199 -0
  17. package/docs/standard-result-outcome.md +165 -0
  18. package/env.example +44 -10
  19. package/eslint.config.mjs +101 -0
  20. package/netWatch.js +373 -222
  21. package/package.json +46 -31
  22. package/pour/README.md +47 -0
  23. package/pour/pour.min.js +8 -0
  24. package/procs/actDo.js +808 -0
  25. package/procs/catalog.d.ts +17 -0
  26. package/procs/catalog.js +313 -221
  27. package/procs/catalog.ts +398 -0
  28. package/procs/checkpoint.js +110 -0
  29. package/procs/config.d.ts +35 -0
  30. package/procs/config.js +63 -0
  31. package/procs/dateTime.js +2 -1
  32. package/procs/doActs.js +442 -849
  33. package/procs/doTestAct.js +27 -121
  34. package/procs/flow.js +221 -0
  35. package/procs/generateRuleRegistry.js +94 -0
  36. package/procs/getSource.d.ts +26 -0
  37. package/procs/getSource.js +1 -0
  38. package/procs/job.js +116 -1
  39. package/procs/launch.d.ts +40 -0
  40. package/procs/launch.js +239 -108
  41. package/procs/nu.d.ts +48 -0
  42. package/procs/nu.js +1 -0
  43. package/procs/scope.js +196 -0
  44. package/procs/shoot.d.ts +33 -0
  45. package/procs/shoot.js +3 -1
  46. package/procs/standard.d.ts +18 -0
  47. package/procs/standard.js +83 -0
  48. package/procs/standard.ts +126 -0
  49. package/procs/testAct.js +145 -0
  50. package/procs/testaro.d.ts +20 -0
  51. package/procs/testaro.js +237 -189
  52. package/procs/testaro.ts +321 -0
  53. package/procs/userPath.js +126 -0
  54. package/procs/xPath.d.ts +4 -0
  55. package/procs/xPath.js +82 -64
  56. package/procs/xPath.ts +106 -0
  57. package/procs/xPathScript.d.ts +6 -0
  58. package/procs/xPathScript.js +70 -0
  59. package/run.js +3 -2
  60. package/surea11y/README.md +44 -0
  61. package/surea11y/surea11y.browser.js +14 -0
  62. package/testaro/adbID.d.ts +3 -0
  63. package/testaro/adbID.js +39 -41
  64. package/testaro/adbID.ts +66 -0
  65. package/testaro/allCapStyle.d.ts +3 -0
  66. package/testaro/allCapStyle.js +32 -34
  67. package/testaro/allCapStyle.ts +55 -0
  68. package/testaro/allCaps.d.ts +16 -0
  69. package/testaro/allCaps.js +179 -151
  70. package/testaro/allCaps.ts +228 -0
  71. package/testaro/allHidden.d.ts +13 -0
  72. package/testaro/allHidden.js +30 -30
  73. package/testaro/allHidden.ts +50 -0
  74. package/testaro/allSlanted.d.ts +3 -0
  75. package/testaro/allSlanted.js +31 -33
  76. package/testaro/allSlanted.ts +54 -0
  77. package/testaro/altScheme.d.ts +3 -0
  78. package/testaro/altScheme.js +28 -30
  79. package/testaro/altScheme.ts +50 -0
  80. package/testaro/attVal.d.ts +3 -0
  81. package/testaro/attVal.js +21 -21
  82. package/testaro/attVal.ts +52 -0
  83. package/testaro/autocomplete.d.ts +3 -0
  84. package/testaro/autocomplete.js +60 -75
  85. package/testaro/autocomplete.ts +101 -0
  86. package/testaro/bulk.d.ts +13 -0
  87. package/testaro/bulk.js +33 -33
  88. package/testaro/bulk.ts +55 -0
  89. package/testaro/buttonMenu.d.ts +9 -0
  90. package/testaro/buttonMenu.js +318 -319
  91. package/testaro/buttonMenu.ts +391 -0
  92. package/testaro/captionLoc.d.ts +3 -0
  93. package/testaro/captionLoc.js +18 -20
  94. package/testaro/captionLoc.ts +40 -0
  95. package/testaro/datalistRef.d.ts +3 -0
  96. package/testaro/datalistRef.js +34 -36
  97. package/testaro/datalistRef.ts +55 -0
  98. package/testaro/distortion.d.ts +3 -0
  99. package/testaro/distortion.js +58 -26
  100. package/testaro/distortion.ts +81 -0
  101. package/testaro/docType.d.ts +15 -0
  102. package/testaro/docType.js +26 -25
  103. package/testaro/docType.ts +45 -0
  104. package/testaro/dupAtt.d.ts +16 -0
  105. package/testaro/dupAtt.js +114 -104
  106. package/testaro/dupAtt.ts +144 -0
  107. package/testaro/elements.d.ts +6 -0
  108. package/testaro/elements.js +153 -153
  109. package/testaro/elements.ts +215 -0
  110. package/testaro/embAc.d.ts +3 -0
  111. package/testaro/embAc.js +20 -20
  112. package/testaro/embAc.ts +40 -0
  113. package/testaro/focAll.d.ts +13 -0
  114. package/testaro/focAll.js +186 -191
  115. package/testaro/focAll.ts +217 -0
  116. package/testaro/focAndOp.d.ts +3 -0
  117. package/testaro/focAndOp.js +101 -104
  118. package/testaro/focAndOp.ts +128 -0
  119. package/testaro/focInd.d.ts +3 -0
  120. package/testaro/focInd.js +65 -66
  121. package/testaro/focInd.ts +96 -0
  122. package/testaro/focVis.d.ts +3 -0
  123. package/testaro/focVis.js +29 -30
  124. package/testaro/focVis.ts +52 -0
  125. package/testaro/headEl.d.ts +10 -0
  126. package/testaro/headEl.js +62 -62
  127. package/testaro/headEl.ts +82 -0
  128. package/testaro/headingAmb.d.ts +3 -0
  129. package/testaro/headingAmb.js +55 -63
  130. package/testaro/headingAmb.ts +72 -0
  131. package/testaro/hovInd.d.ts +12 -0
  132. package/testaro/hovInd.js +164 -130
  133. package/testaro/hovInd.ts +199 -0
  134. package/testaro/hover.d.ts +3 -0
  135. package/testaro/hover.js +156 -126
  136. package/testaro/hover.ts +154 -0
  137. package/testaro/hr.d.ts +3 -0
  138. package/testaro/hr.js +15 -17
  139. package/testaro/hr.ts +36 -0
  140. package/testaro/imageLink.d.ts +3 -0
  141. package/testaro/imageLink.js +18 -20
  142. package/testaro/imageLink.ts +42 -0
  143. package/testaro/labClash.d.ts +3 -0
  144. package/testaro/labClash.js +32 -33
  145. package/testaro/labClash.ts +54 -0
  146. package/testaro/legendLoc.d.ts +3 -0
  147. package/testaro/legendLoc.js +18 -20
  148. package/testaro/legendLoc.ts +42 -0
  149. package/testaro/lineHeight.d.ts +3 -0
  150. package/testaro/lineHeight.js +44 -48
  151. package/testaro/lineHeight.ts +70 -0
  152. package/testaro/linkAmb.d.ts +7 -0
  153. package/testaro/linkAmb.js +80 -80
  154. package/testaro/linkAmb.ts +105 -0
  155. package/testaro/linkExt.d.ts +3 -0
  156. package/testaro/linkExt.js +14 -16
  157. package/testaro/linkExt.ts +35 -0
  158. package/testaro/linkOldAtt.d.ts +3 -0
  159. package/testaro/linkOldAtt.js +26 -28
  160. package/testaro/linkOldAtt.ts +48 -0
  161. package/testaro/linkTo.d.ts +3 -0
  162. package/testaro/linkTo.js +23 -22
  163. package/testaro/linkTo.ts +43 -0
  164. package/testaro/linkUl.d.ts +3 -0
  165. package/testaro/linkUl.js +31 -34
  166. package/testaro/linkUl.ts +54 -0
  167. package/testaro/miniText.d.ts +3 -0
  168. package/testaro/miniText.js +42 -44
  169. package/testaro/miniText.ts +68 -0
  170. package/testaro/motion.d.ts +10 -0
  171. package/testaro/motion.js +93 -96
  172. package/testaro/motion.ts +125 -0
  173. package/testaro/nonTable.d.ts +3 -0
  174. package/testaro/nonTable.js +40 -45
  175. package/testaro/nonTable.ts +66 -0
  176. package/testaro/optRoleSel.d.ts +3 -0
  177. package/testaro/optRoleSel.js +17 -19
  178. package/testaro/optRoleSel.ts +41 -0
  179. package/testaro/phOnly.d.ts +3 -0
  180. package/testaro/phOnly.js +19 -21
  181. package/testaro/phOnly.ts +43 -0
  182. package/testaro/pseudoP.d.ts +3 -0
  183. package/testaro/pseudoP.js +37 -38
  184. package/testaro/pseudoP.ts +59 -0
  185. package/testaro/radioSet.d.ts +3 -0
  186. package/testaro/radioSet.js +58 -59
  187. package/testaro/radioSet.ts +79 -0
  188. package/testaro/registry.d.ts +63 -0
  189. package/testaro/registry.js +67 -0
  190. package/testaro/registry.ts +141 -0
  191. package/testaro/role.d.ts +3 -0
  192. package/testaro/role.js +28 -29
  193. package/testaro/role.ts +53 -0
  194. package/testaro/secHeading.d.ts +3 -0
  195. package/testaro/secHeading.js +31 -33
  196. package/testaro/secHeading.ts +53 -0
  197. package/testaro/styleDiff.d.ts +25 -0
  198. package/testaro/styleDiff.js +249 -252
  199. package/testaro/styleDiff.ts +303 -0
  200. package/testaro/tabNav.d.ts +33 -0
  201. package/testaro/tabNav.js +273 -342
  202. package/testaro/tabNav.ts +454 -0
  203. package/testaro/targetsNear.d.ts +9 -0
  204. package/testaro/targetsNear.js +131 -132
  205. package/testaro/targetsNear.ts +160 -0
  206. package/testaro/textNodes.d.ts +6 -0
  207. package/testaro/textNodes.js +139 -135
  208. package/testaro/textNodes.ts +185 -0
  209. package/testaro/textSem.d.ts +3 -0
  210. package/testaro/textSem.js +26 -28
  211. package/testaro/textSem.ts +47 -0
  212. package/testaro/title.d.ts +9 -0
  213. package/testaro/title.js +16 -13
  214. package/testaro/title.ts +31 -0
  215. package/testaro/titledEl.d.ts +3 -0
  216. package/testaro/titledEl.js +16 -18
  217. package/testaro/titledEl.ts +38 -0
  218. package/testaro/zIndex.d.ts +3 -0
  219. package/testaro/zIndex.js +20 -22
  220. package/testaro/zIndex.ts +42 -0
  221. package/tests/alfa.d.ts +45 -0
  222. package/tests/alfa.js +139 -141
  223. package/tests/alfa.ts +214 -0
  224. package/tests/aslint.d.ts +33 -0
  225. package/tests/aslint.js +273 -249
  226. package/tests/aslint.ts +301 -0
  227. package/tests/axe.d.ts +27 -0
  228. package/tests/axe.js +200 -200
  229. package/tests/axe.ts +277 -0
  230. package/tests/ed11y.d.ts +28 -0
  231. package/tests/ed11y.js +142 -99
  232. package/tests/ed11y.ts +178 -0
  233. package/tests/htmlcs.d.ts +21 -0
  234. package/tests/htmlcs.js +175 -140
  235. package/tests/htmlcs.ts +181 -0
  236. package/tests/ibm.d.ts +52 -0
  237. package/tests/ibm.js +165 -166
  238. package/tests/ibm.ts +251 -0
  239. package/tests/nuVal.d.ts +13 -0
  240. package/tests/nuVal.js +107 -112
  241. package/tests/nuVal.ts +145 -0
  242. package/tests/nuVnu.d.ts +15 -0
  243. package/tests/nuVnu.js +142 -111
  244. package/tests/nuVnu.ts +144 -0
  245. package/tests/pour.d.ts +31 -0
  246. package/tests/pour.js +242 -0
  247. package/tests/pour.ts +273 -0
  248. package/tests/qualWeb.d.ts +39 -0
  249. package/tests/qualWeb.js +303 -272
  250. package/tests/qualWeb.ts +415 -0
  251. package/tests/surea11y.d.ts +33 -0
  252. package/tests/surea11y.js +288 -0
  253. package/tests/surea11y.ts +334 -0
  254. package/tests/testaro.d.ts +25 -0
  255. package/tests/testaro.js +746 -652
  256. package/tests/testaro.ts +862 -0
  257. package/tests/wave.d.ts +46 -0
  258. package/tests/wave.js +167 -177
  259. package/tests/wave.ts +252 -0
  260. package/tsconfig.json +17 -0
  261. package/types.d.ts +243 -0
  262. package/types.js +10 -0
  263. package/types.ts +376 -0
  264. package/validation/act/README.md +46 -0
  265. package/validation/act/capture.js +424 -0
  266. package/validation/act/chromium-issue-draft.md +66 -0
  267. package/validation/act/fp-triage-2026-08-22.md +86 -0
  268. package/validation/act/isolation-notes.md +159 -0
  269. package/validation/act/playwright-issue-draft.md +73 -0
  270. package/validation/act/propose-mappings.js +0 -0
  271. package/validation/act/repro-cdp-raw.js +105 -0
  272. package/validation/act/repro-metarefresh.js +65 -0
  273. package/validation/act/score.js +184 -0
  274. package/validation/act/stage3a-stress-report.md +69 -0
  275. package/validation/act/stage3b-mapping-proposals.md +94 -0
  276. package/validation/act/stage3b-triage-draft.md +138 -0
  277. package/validation/act/surea11y-track-a-2026-09-01.md +50 -0
  278. package/validation/executors/netWatch.js +180 -90
  279. package/validation/executors/test.js +17 -2
  280. package/validation/executors/tests.js +118 -10
  281. package/validation/jobs/reports/raw/260901T1000-surea11y-validation.json +964 -0
  282. package/validation/jobs/todo/240101T1200-simple-example.json +14 -6
  283. package/validation/jobs/todo/240101T1300-shoot-example.json +2 -1
  284. package/validation/jobs/todo/260821T1900-pour-validation.json +45 -0
  285. package/validation/jobs/todo/260901T1000-surea11y-validation.json +45 -0
  286. package/validation/knownFailures.json +1 -0
  287. package/validation/tests/jobProperties/adbID.json +27 -2
  288. package/validation/tests/jobProperties/{focOp.json → allCapStyle.json} +56 -53
  289. package/validation/tests/jobProperties/allCaps.json +25 -0
  290. package/validation/tests/jobProperties/allHidden.json +133 -13
  291. package/validation/tests/jobProperties/allSlanted.json +3 -3
  292. package/validation/tests/jobProperties/altScheme.json +23 -8
  293. package/validation/tests/jobProperties/attVal.json +57 -57
  294. package/validation/tests/jobProperties/autocomplete.json +11 -1
  295. package/validation/tests/jobProperties/bulk.json +6 -1
  296. package/validation/tests/jobProperties/buttonMenu.json +63 -42
  297. package/validation/tests/jobProperties/captionLoc.json +1 -6
  298. package/validation/tests/jobProperties/checkpoint-browser.json +401 -0
  299. package/validation/tests/jobProperties/checkpoint-page.json +397 -0
  300. package/validation/tests/jobProperties/checkpoint.json +395 -0
  301. package/validation/tests/jobProperties/datalistRef.json +20 -5
  302. package/validation/tests/jobProperties/distortion.json +28 -3
  303. package/validation/tests/jobProperties/docType.json +2 -2
  304. package/validation/tests/jobProperties/dupAtt.json +48 -33
  305. package/validation/tests/jobProperties/elements.json +28 -28
  306. package/validation/tests/jobProperties/embAc.json +36 -31
  307. package/validation/tests/jobProperties/focAndOp.json +284 -0
  308. package/validation/tests/jobProperties/focInd.json +34 -34
  309. package/validation/tests/jobProperties/focVis.json +1 -1
  310. package/validation/tests/jobProperties/hover.json +39 -37
  311. package/validation/tests/jobProperties/hr.json +13 -3
  312. package/validation/tests/jobProperties/imageLink.json +0 -5
  313. package/validation/tests/jobProperties/labClash.json +68 -33
  314. package/validation/tests/jobProperties/legendLoc.json +1 -6
  315. package/validation/tests/jobProperties/lineHeight.json +2 -2
  316. package/validation/tests/jobProperties/linkAmb.json +13 -18
  317. package/validation/tests/jobProperties/linkExt.json +1 -1
  318. package/validation/tests/jobProperties/linkOldAtt.json +12 -2
  319. package/validation/tests/jobProperties/linkTo.json +1 -1
  320. package/validation/tests/jobProperties/linkUl.json +65 -65
  321. package/validation/tests/jobProperties/miniText.json +7 -2
  322. package/validation/tests/jobProperties/motion.json +4 -50
  323. package/validation/tests/jobProperties/nonTable.json +48 -3
  324. package/validation/tests/jobProperties/optRoleSel.json +12 -2
  325. package/validation/tests/jobProperties/phOnly.json +6 -16
  326. package/validation/tests/jobProperties/pseudoP.json +17 -2
  327. package/validation/tests/jobProperties/radioSet.json +30 -30
  328. package/validation/tests/jobProperties/role.json +25 -5
  329. package/validation/tests/jobProperties/secHeading.json +26 -21
  330. package/validation/tests/jobProperties/styleDiff.json +35 -5
  331. package/validation/tests/jobProperties/tabNav.json +3 -1
  332. package/validation/tests/jobProperties/targetsNear.json +237 -0
  333. package/validation/tests/jobProperties/textNodes.json +29 -29
  334. package/validation/tests/jobProperties/textSem.json +26 -1
  335. package/validation/tests/jobProperties/title.json +12 -2
  336. package/validation/tests/jobProperties/titledEl.json +39 -9
  337. package/validation/tests/jobProperties/userPath.json +317 -0
  338. package/validation/tests/jobProperties/zIndex.json +40 -35
  339. package/validation/tests/targets/allCapStyle/index.html +26 -0
  340. package/validation/tests/targets/checkpoint/index.html +38 -0
  341. package/validation/tests/targets/datalistRef/index.html +1 -1
  342. package/validation/tests/targets/focAndOp/bad.html +29 -0
  343. package/validation/tests/targets/{focOp → focAndOp}/good.html +3 -1
  344. package/validation/tests/targets/focInd/bad.html +2 -1
  345. package/validation/tests/targets/headEl/index.html +10 -1
  346. package/validation/tests/targets/{targetSmall → targetsNear}/index.html +15 -1
  347. package/validation/tests/targets/userPath/index.html +42 -0
  348. package/validation/validateTest.js +62 -10
  349. package/.claude/settings.local.json +0 -11
  350. package/.eslintrc.json +0 -41
  351. package/htmlcs/.eslintrc.json +0 -67
  352. package/memory/MEMORY.md +0 -3
  353. package/memory/project_validation_pause.md +0 -10
  354. package/validation/tests/jobProperties/linkTitle.json +0 -127
  355. package/validation/tests/jobProperties/opFoc.json +0 -164
  356. package/validation/tests/jobProperties/targetSmall.json +0 -152
  357. package/validation/tests/jobProperties/targetTiny.json +0 -142
  358. package/validation/tests/targets/focOp/bad.html +0 -25
  359. package/validation/tests/targets/linkTitle/index.html +0 -24
  360. package/validation/tests/targets/opFoc/bad.html +0 -26
  361. package/validation/tests/targets/opFoc/good.html +0 -23
package/netWatch.js CHANGED
@@ -1,5 +1,6 @@
1
1
  /*
2
2
  © 2022–2025 CVS Health and/or one of its affiliates. All rights reserved.
3
+ © 2026 Jeff Witt.
3
4
  © 2025–2026 Jonathan Robert Pool.
4
5
 
5
6
  Licensed under the MIT License. See LICENSE file at the project root or
@@ -17,6 +18,8 @@
17
18
 
18
19
  // Module to keep secrets.
19
20
  require('dotenv').config();
21
+ // Module to access files.
22
+ const fs = require('fs/promises');
20
23
  // Module to validate jobs.
21
24
  const {isValidJob} = require('./procs/job');
22
25
  // Modules to make requests to servers.
@@ -29,9 +32,16 @@ const {nowString} = require('./procs/dateTime');
29
32
 
30
33
  // CONSTANTS
31
34
 
32
- // The netWatch environment variables are required only for network watching,
33
- // but call.js loads this module for every command, so their absence or
34
- // invalidity must not throw here; netWatch reports it if netWatch is used.
35
+ // Auth types and the environment variables each one requires.
36
+ const authRequirements = {
37
+ none: [],
38
+ pathBody: ['NETWATCH_WORKER_SECRET'],
39
+ header: ['NETWATCH_WORKER_ID', 'NETWATCH_WORKER_SECRET']
40
+ };
41
+
42
+ // FUNCTIONS
43
+
44
+ // Returns the URL represented by a string, or null if invalid.
35
45
  const toURL = urlString => {
36
46
  try {
37
47
  return new URL(urlString);
@@ -40,14 +50,6 @@ const toURL = urlString => {
40
50
  return null;
41
51
  }
42
52
  };
43
- const jobURL = toURL(process.env.NETWATCH_URL_JOB);
44
- const jobHost = jobURL && jobURL.host;
45
- const reportURL = toURL(process.env.NETWATCH_URL_REPORT);
46
- const reportHost = reportURL && reportURL.host;
47
- const agentPW = process.env.NETWATCH_URL_AUTH;
48
-
49
- // FUNCTIONS
50
-
51
53
  // Waits.
52
54
  const wait = ms => {
53
55
  return new Promise(resolve => {
@@ -56,142 +58,301 @@ const wait = ms => {
56
58
  }, ms);
57
59
  });
58
60
  };
59
- // Ends a response with an object in JSON format.
60
- const respondWithObject = (object, response) => {
61
- response.setHeader('content-type', 'application/json; charset=utf-8');
62
- response.end(JSON.stringify(object));
61
+ // Returns the value of a basic authorization header for a worker ID and secret.
62
+ const basicAuthHeader = (workerID, workerSecret) =>
63
+ `Basic ${Buffer.from(`${workerID}:${workerSecret}`).toString('base64')}`;
64
+ // Returns the netWatch configuration from the environment, with any problems named.
65
+ const getConfig = () => {
66
+ const problems = [];
67
+ const warnings = [];
68
+ let authType = process.env.NETWATCH_AUTH_TYPE;
69
+ let workerID = process.env.NETWATCH_WORKER_ID;
70
+ let workerSecret = process.env.NETWATCH_WORKER_SECRET;
71
+ // If the worker ID is specified only by the deprecated AGENT variable:
72
+ if (! workerID && process.env.AGENT) {
73
+ // Adopt it and warn about the deprecation.
74
+ workerID = process.env.AGENT;
75
+ warnings.push('AGENT is deprecated; rename it to NETWATCH_WORKER_ID');
76
+ }
77
+ // If the auth type is unspecified but the deprecated NETWATCH_URL_AUTH variable exists:
78
+ if (! authType && process.env.NETWATCH_URL_AUTH) {
79
+ // Treat the configuration as a pathBody configuration and warn about the deprecation.
80
+ authType = 'pathBody';
81
+ workerSecret ||= process.env.NETWATCH_URL_AUTH;
82
+ warnings.push(
83
+ 'NETWATCH_URL_AUTH is deprecated; set NETWATCH_AUTH_TYPE=pathBody and NETWATCH_WORKER_SECRET'
84
+ );
85
+ }
86
+ // If no auth configuration exists at all:
87
+ if (! authType && ! workerSecret) {
88
+ // Treat the configuration as an unauthenticated configuration and warn about this.
89
+ authType = 'none';
90
+ warnings.push('NETWATCH_AUTH_TYPE not set; defaulting to none (no credentials sent)');
91
+ }
92
+ const jobURL = toURL(process.env.NETWATCH_URL_JOB);
93
+ const reportURL = toURL(process.env.NETWATCH_URL_REPORT);
94
+ // Identify any problems with the configuration, naming the offending variables.
95
+ if (! jobURL) {
96
+ problems.push(`NETWATCH_URL_JOB missing or not a valid URL (${process.env.NETWATCH_URL_JOB})`);
97
+ }
98
+ if (! reportURL) {
99
+ problems.push(
100
+ `NETWATCH_URL_REPORT missing or not a valid URL (${process.env.NETWATCH_URL_REPORT})`
101
+ );
102
+ }
103
+ if (! Object.keys(authRequirements).includes(authType)) {
104
+ problems.push(
105
+ `NETWATCH_AUTH_TYPE (${authType}) not one of ${Object.keys(authRequirements).join(', ')}`
106
+ );
107
+ }
108
+ else {
109
+ const varValues = {
110
+ NETWATCH_WORKER_ID: workerID,
111
+ NETWATCH_WORKER_SECRET: workerSecret
112
+ };
113
+ authRequirements[authType].forEach(varName => {
114
+ if (! varValues[varName]) {
115
+ problems.push(`${varName} required when NETWATCH_AUTH_TYPE is ${authType}`);
116
+ }
117
+ });
118
+ // If the worker ID contains a colon, prohibited in basic authentication by RFC 7617:
119
+ if (authType === 'header' && workerID && workerID.includes(':')) {
120
+ problems.push('NETWATCH_WORKER_ID must not contain a colon');
121
+ }
122
+ // If any credential is not ASCII-only, so servers may decode it differently:
123
+ if ([workerID, workerSecret].some(value => value && /[^\x20-\x7e]/.test(value))) {
124
+ warnings.push(
125
+ 'NETWATCH_WORKER_ID or NETWATCH_WORKER_SECRET contains non-ASCII characters, which servers may decode differently'
126
+ );
127
+ }
128
+ }
129
+ return {problems, warnings, jobURL, reportURL, authType, workerID, workerSecret};
130
+ };
131
+ // Saves a report that could not be submitted, so it is not lost.
132
+ const saveFailedReport = async report => {
133
+ try {
134
+ const saveDir = `${process.env.REPORTDIR || '.'}/netWatchFailed`;
135
+ await fs.mkdir(saveDir, {recursive: true});
136
+ const savePath = `${saveDir}/${report && report.id || 'report'}.json`;
137
+ await fs.writeFile(savePath, JSON.stringify(report, null, 2));
138
+ console.log(`Unsubmitted report saved at ${savePath}`);
139
+ }
140
+ catch(error) {
141
+ console.log(`ERROR saving unsubmitted report (${error.message})`);
142
+ }
63
143
  };
64
144
  /*
65
145
  Requests a network job and, when found, performs and reports it.
66
146
  Arguments:
67
147
  0. whether to continue watching after a job is run.
68
148
  1: interval in seconds from a no-job check to the next check.
69
- 2. whether to ignore unknown-certificate errors from watched servers.
149
+ 2. whether to ignore unknown-certificate errors from a watched server (default false).
150
+ Returns whether watching ended without an abort, an invalid configuration, or a lost report.
70
151
  */
71
- exports.netWatch = async (isForever, intervalInSeconds, isCertTolerant = true) => {
72
- // If the job and report URLs exist and are valid:
73
- if (jobURL && reportURL) {
74
- // Configure the watch.
75
- let noJobYet = true;
76
- let abort = false;
77
- const certInfo = `Certificate-${isCertTolerant ? '' : 'in'}tolerant`;
78
- const foreverInfo = isForever ? 'repeating' : 'one-job';
79
- const intervalInfo = `with ${intervalInSeconds}-second intervals`;
152
+ exports.netWatch = async (isForever, intervalInSeconds, isCertTolerant = false) => {
153
+ const {problems, warnings, jobURL, reportURL, authType, workerID, workerSecret} = getConfig();
154
+ // Report any warnings about the configuration.
155
+ warnings.forEach(warning => {
156
+ console.log(`WARNING: ${warning}`);
157
+ });
158
+ // If the netWatch configuration is invalid:
159
+ if (problems.length) {
160
+ // Report each problem and quit.
161
+ problems.forEach(problem => {
162
+ console.log(`ERROR: ${problem}`);
163
+ });
164
+ console.log('ERROR: Configuration of netWatch is invalid');
165
+ return false;
166
+ }
167
+ // Configure the watch.
168
+ const headers = {
169
+ 'content-type': 'application/json; charset=utf-8'
170
+ };
171
+ if (authType === 'header') {
172
+ headers.authorization = basicAuthHeader(workerID, workerSecret);
173
+ }
174
+ // Body properties transmitting the credentials, if the auth type requires them there.
175
+ const authBody = authType === 'pathBody' ? {agentPW: workerSecret} : {};
176
+ const jobRequestJSON = JSON.stringify(authBody);
177
+ // Returns the client and request options for a request to a URL.
178
+ const requestConfigFor = url => {
179
+ const options = {
180
+ method: 'POST',
181
+ headers: {... headers}
182
+ };
183
+ let client = httpClient;
184
+ if (url.protocol === 'https:') {
185
+ client = httpsClient;
186
+ options.rejectUnauthorized = ! isCertTolerant;
187
+ }
188
+ return {client, options};
189
+ };
190
+ // If certificate tolerance would expose credentials on an encrypted connection:
191
+ if (isCertTolerant && [jobURL, reportURL].some(url => url.protocol === 'https:')) {
192
+ // Warn about this.
80
193
  console.log(
81
- `${certInfo} ${foreverInfo} network watching started ${intervalInfo} (${nowString()})\n`
194
+ 'WARNING: Certificate tolerance disables certificate verification, so credentials and reports are exposed to interception'
82
195
  );
83
- // As long as watching is to continue:
84
- while ((isForever || noJobYet) && ! abort) {
85
- // Log the start of a check.
86
- console.log('--');
87
- // Configure the next check.
88
- const logStart = `Requested job from ${jobHost} and got `;
89
- // Perform it.
90
- await new Promise(async resolve => {
91
- try {
92
- const client = jobURL.protocol === 'https:' ? httpsClient : httpClient;
93
- // Request a job.
94
- const requestOptions = {
95
- method: 'POST',
96
- headers: {
97
- host: jobHost,
98
- 'content-type': 'application/json; charset=utf-8'
196
+ }
197
+ let noJobYet = true;
198
+ let abort = false;
199
+ let reportLost = false;
200
+ const jobHost = jobURL.host;
201
+ const certInfo = `Certificate-${isCertTolerant ? '' : 'in'}tolerant`;
202
+ const foreverInfo = isForever ? 'repeating' : 'one-job';
203
+ const intervalInfo = `with ${intervalInSeconds}-second intervals`;
204
+ console.log(
205
+ `${certInfo} ${foreverInfo} network watching started ${intervalInfo} (${nowString()})\n`
206
+ );
207
+ // As long as watching is to continue:
208
+ while ((isForever || noJobYet) && ! abort) {
209
+ // Log the start of a check.
210
+ console.log('--');
211
+ // Configure the next check.
212
+ const logStart = `Requested job from ${jobHost} and got `;
213
+ // Perform it.
214
+ await new Promise(resolve => {
215
+ // Ensure the check is concluded at most once, so error events cannot start overlapping checks.
216
+ let settled = false;
217
+ let jobDispatched = false;
218
+ const finish = () => {
219
+ if (! settled) {
220
+ settled = true;
221
+ resolve(true);
222
+ }
223
+ };
224
+ const finishAfterWait = async () => {
225
+ if (! settled) {
226
+ // Wait for the specified interval.
227
+ await wait(1000 * intervalInSeconds);
228
+ finish();
229
+ }
230
+ };
231
+ try {
232
+ // Get the client and request options for a job request.
233
+ const {client, options} = requestConfigFor(jobURL);
234
+ // Request a job.
235
+ client.request(jobURL, options, response => {
236
+ // Initialize a collection of data from the response.
237
+ const chunks = [];
238
+ response
239
+ // If the response throws an error:
240
+ .on('error', error => {
241
+ // Report it.
242
+ console.log(`${logStart}error message ${error.message}`);
243
+ // Unless a job is already being performed, wait and conclude the check.
244
+ if (! jobDispatched) {
245
+ finishAfterWait();
99
246
  }
100
- };
101
- client.request(jobURL, requestOptions, response => {
102
- // Initialize a collection of data from the response.
103
- const chunks = [];
104
- response
105
- // If the response throws an error:
106
- .on('error', async error => {
247
+ })
248
+ // If the response delivers data:
249
+ .on('data', chunk => {
250
+ // Add them to the collection.
251
+ chunks.push(chunk);
252
+ })
253
+ // When the response is completed:
254
+ .on('end', async () => {
255
+ const content = chunks.join('');
256
+ const {statusCode} = response;
257
+ // If the server reported a failure:
258
+ if (statusCode < 200 || statusCode > 299) {
107
259
  // Report it.
108
- console.log(`${logStart}error message ${error.message}`);
109
- })
110
- // If the response delivers data:
111
- .on('data', chunk => {
112
- // Add them to the collection.
113
- chunks.push(chunk);
114
- })
115
- // When the response is completed:
116
- .on('end', async () => {
117
- const content = chunks.join('');
118
- try {
119
- // Parse it as a JSON job.
120
- let contentObj = JSON.parse(content);
121
- const {id, sources} = contentObj;
122
- // If it is a no-job message:
123
- if (! Object.keys(contentObj).length) {
260
+ console.log(
261
+ `ERROR: ${logStart}status ${statusCode} and response ${content.slice(0, 1000)}`
262
+ );
263
+ // If it was an authentication or authorization rejection:
264
+ if ([401, 403].includes(statusCode)) {
265
+ // Abort the watch, because rechecking cannot succeed until it is reconfigured.
266
+ abort = true;
267
+ finish();
268
+ }
269
+ // Otherwise, i.e. if the failure may be transient:
270
+ else {
271
+ // Wait and conclude the check.
272
+ finishAfterWait();
273
+ }
274
+ return;
275
+ }
276
+ try {
277
+ // Parse it as a JSON job.
278
+ let contentObj = JSON.parse(content);
279
+ const {id} = contentObj;
280
+ // If it is a no-job message:
281
+ if (! Object.keys(contentObj).length) {
282
+ // Report this.
283
+ console.log(`${logStart}no job to do; waiting ${intervalInSeconds} sec before next check`);
284
+ // Wait and conclude the check.
285
+ finishAfterWait();
286
+ }
287
+ // Otherwise, if it is a job:
288
+ else if (id) {
289
+ // Check it for validity.
290
+ const jobValidity = isValidJob(contentObj);
291
+ // If it is invalid:
292
+ if (! jobValidity.isValid) {
124
293
  // Report this.
125
- console.log(`${logStart}no job to do; waiting ${intervalInSeconds} sec before next check`);
126
- // Wait for the specified interval.
127
- await wait(1000 * intervalInSeconds);
128
- resolve(true);
294
+ console.log(`${logStart}invalid job (${jobValidity.error})`);
295
+ // Wait and conclude the check.
296
+ finishAfterWait();
129
297
  }
130
- // Otherwise, if it is a job:
131
- else if (id) {
132
- // Check it for validity.
133
- const jobValidity = isValidJob(contentObj);
134
- // If it is invalid:
135
- if (! jobValidity.isValid) {
136
- // Report this to the server.
137
- respondWithObject({
138
- message: `invalidJob`,
139
- error: jobValidity.error
140
- }, response);
141
- console.log(`${logStart}invalid job (${jobValidity.error})`);
142
- // Wait for the specified interval.
143
- await wait(1000 * intervalInSeconds);
144
- resolve(true);
298
+ // Otherwise, i.e. if it is valid:
299
+ else {
300
+ // Prevent further watching, if unwanted.
301
+ noJobYet = false;
302
+ jobDispatched = true;
303
+ // Identify this worker in the report, if a worker ID exists.
304
+ if (workerID && contentObj.sources) {
305
+ contentObj.sources.agent = workerID;
145
306
  }
146
- // Otherwise, i.e. if it is valid:
147
- else {
148
- // Prevent further watching, if unwanted.
149
- noJobYet = false;
150
- // Add the agent and the server ID to the job.
151
- sources.agent = process.env.AGENT || '';
307
+ console.log(`${logStart}job ${id} (${nowString()})`);
308
+ try {
152
309
  // Perform the job and create a report.
153
- console.log(`${logStart}job ${id} (${nowString()})`);
154
- try {
155
- const report = await doJob(contentObj);
156
- const responseObj = {
157
- agentPW,
158
- report
159
- };
160
- let responseJSON = JSON.stringify(responseObj, null, 2);
161
- console.log(`Job ${id} finished (${nowString()})`);
162
- const reportLogStart = `Submitted report ${id} to ${reportURL} and got `;
163
- const requestOptions = {
164
- method: 'POST',
165
- headers: {
166
- host: reportHost,
167
- 'content-type': 'application/json; charset=utf-8'
310
+ const report = await doJob(contentObj);
311
+ // Make it the report property of the response body, with any body credentials.
312
+ const responseObj = {
313
+ ... authBody,
314
+ report
315
+ };
316
+ let responseJSON = JSON.stringify(responseObj, null, 2);
317
+ console.log(`Job ${id} finished (${nowString()})`);
318
+ const reportLogStart = `Submitted report ${id} to ${reportURL} and got `;
319
+ // Get the client and request options for a report-submission request.
320
+ const {client: repClient, options: repOptions} = requestConfigFor(reportURL);
321
+ // Submit the report.
322
+ repClient.request(reportURL, repOptions, repResponse => {
323
+ // Initialize a collection of data from the response.
324
+ const repChunks = [];
325
+ repResponse
326
+ // If the response to the report threw an error:
327
+ .on('error', error => {
328
+ // Report this.
329
+ console.log(`${reportLogStart}error message ${error.message}\n`);
330
+ // Wait and conclude the check.
331
+ finishAfterWait();
332
+ })
333
+ // If the response delivers data:
334
+ .on('data', chunk => {
335
+ // Add them to the collection.
336
+ repChunks.push(chunk);
337
+ })
338
+ // When the response to the report is completed:
339
+ .on('end', async () => {
340
+ const repContent = repChunks.join('');
341
+ const repStatusCode = repResponse.statusCode;
342
+ // If the server reported a failure:
343
+ if (repStatusCode < 200 || repStatusCode > 299) {
344
+ // Report this and save the report, so it is not lost.
345
+ console.log(
346
+ `ERROR: ${reportLogStart}status ${repStatusCode} and response ${repContent.slice(0, 1000)}\n`
347
+ );
348
+ reportLost = true;
349
+ await saveFailedReport(report);
168
350
  }
169
- };
170
- // Submit the report.
171
- const client = reportURL.protocol === 'https:' ? httpsClient : httpClient;
172
- client.request(reportURL, requestOptions, repResponse => {
173
- // Initialize a collection of data from the response.
174
- const chunks = [];
175
- repResponse
176
- // If the response to the report threw an error:
177
- .on('error', async error => {
178
- // Report this.
179
- console.log(`${reportLogStart}error message ${error.message}\n`);
180
- // Wait for the specified interval.
181
- await wait(1000 * intervalInSeconds);
182
- resolve(true);
183
- })
184
- // If the response delivers data:
185
- .on('data', chunk => {
186
- // Add them to the collection.
187
- chunks.push(chunk);
188
- })
189
- // When the response to the report is completed:
190
- .on('end', async () => {
191
- const content = chunks.join('');
351
+ // Otherwise, i.e. if the server accepted the report:
352
+ else {
192
353
  try {
193
- // Parse it as a JSON message.
194
- const ackObj = JSON.parse(content);
354
+ // Parse the acknowledgement as JSON.
355
+ const ackObj = JSON.parse(repContent);
195
356
  // Report it.
196
357
  console.log(
197
358
  `${reportLogStart}response message: ${JSON.stringify(ackObj, null, 2)}\n`
@@ -201,105 +362,95 @@ exports.netWatch = async (isForever, intervalInSeconds, isCertTolerant = true) =
201
362
  catch(error) {
202
363
  // Report this.
203
364
  console.log(
204
- `ERROR: ${reportLogStart}status ${repResponse.statusCode}, error message ${error.message}, and response ${content.slice(0, 1000)}\n`
365
+ `ERROR: ${reportLogStart}status ${repStatusCode}, error message ${error.message}, and response ${repContent.slice(0, 1000)}\n`
205
366
  );
206
- // Wait for the specified interval.
207
- await wait(1000 * intervalInSeconds);
208
367
  }
209
- // Free the memory used by the job and the report.
210
- contentObj = {};
211
- responseJSON = '';
212
- resolve(true);
213
- });
214
- })
215
- // If the report submission throws an error:
216
- .on('error', async error => {
217
- // Abort the watch.
218
- abort = true;
219
- // Report this.
220
- console.log(
221
- `ERROR ${error.code} in report submission: ${reportLogStart}error message ${error.message}\n`
222
- );
223
- // Wait for the specified interval.
224
- await wait(1000 * intervalInSeconds);
225
- resolve(true);
226
- })
227
- // Finish submitting the report.
228
- .end(responseJSON);
229
- }
230
- catch(error) {
231
- console.log(`ERROR performing job ${id} (${error.message})`);
232
- // Wait for the specified interval.
233
- await wait(1000 * intervalInSeconds);
234
- resolve(true);
235
- }
368
+ }
369
+ // Free the memory used by the job and the report.
370
+ contentObj = {};
371
+ responseJSON = '';
372
+ finish();
373
+ });
374
+ })
375
+ // If the report submission throws an error:
376
+ .on('error', async error => {
377
+ // Abort the watch.
378
+ abort = true;
379
+ // Report this and save the report, so it is not lost.
380
+ console.log(
381
+ `ERROR ${error.code} in report submission: ${reportLogStart}error message ${error.message}\n`
382
+ );
383
+ reportLost = true;
384
+ await saveFailedReport(report);
385
+ finish();
386
+ })
387
+ // Finish submitting the report.
388
+ .end(responseJSON);
389
+ }
390
+ catch(error) {
391
+ console.log(`ERROR performing job ${id} (${error.message})`);
392
+ // Wait and conclude the check.
393
+ finishAfterWait();
236
394
  }
237
- }
238
- // Otherwise, i.e. if it is a message:
239
- else {
240
- // Report it.
241
- console.log(`${logStart}${JSON.stringify(contentObj, null, 2)}`);
242
- // Wait for the specified interval.
243
- await wait(1000 * intervalInSeconds);
244
- resolve(true);
245
395
  }
246
396
  }
247
- // Otherwise, i.e. if it is not JSON:
248
- catch(error) {
249
- // Report this.
250
- console.log(`ERROR: ${logStart}status ${response.statusCode}, error message ${error.message}, and non-JSON response ${content.slice(0, 1000)}\n`);
251
- // Wait for the specified interval.
252
- await wait(1000 * intervalInSeconds);
253
- resolve(true);
254
- };
255
- });
256
- })
257
- // If the job request throws an error:
258
- .on('error', async error => {
259
- // If it is a refusal to connect:
260
- if (error.code && error.code.includes('ECONNREFUSED')) {
261
- // Report this.
262
- console.log(`${logStart}no connection`);
263
- }
264
- // Otherwise, if it was a DNS failure:
265
- else if (error.code && error.code.includes('ENOTFOUND')) {
266
- // Report this.
267
- console.log(`${logStart}no domain name resolution`);
268
- }
269
- // Otherwise, if it was any other error with a message:
270
- else if (error.message) {
271
- // Report this.
272
- console.log(`ERROR: ${logStart}got error message ${error.message.slice(0, 200)}`);
397
+ // Otherwise, i.e. if it is a message:
398
+ else {
399
+ // Report it.
400
+ console.log(`${logStart}${JSON.stringify(contentObj, null, 2)}`);
401
+ // Wait and conclude the check.
402
+ finishAfterWait();
403
+ }
273
404
  }
274
- // Otherwise, i.e. if it was any other error with no message:
275
- else {
405
+ // Otherwise, i.e. if it is not JSON:
406
+ catch(error) {
276
407
  // Report this.
277
- console.log(`ERROR: ${logStart}got an error with no message`);
408
+ console.log(`ERROR: ${logStart}status ${response.statusCode}, error message ${error.message}, and non-JSON response ${content.slice(0, 1000)}\n`);
409
+ // Wait and conclude the check.
410
+ finishAfterWait();
278
411
  }
279
- // Wait for the specified interval.
280
- await wait(1000 * intervalInSeconds);
281
- resolve(true);
282
- })
283
- // Finish sending the job request.
284
- .end(JSON.stringify({
285
- agentPW
286
- }));
287
- }
288
- // If requesting a job throws an error:
289
- catch(error) {
290
- // Report this.
291
- console.log(`ERROR requesting a network job (${error.message})`);
292
- // Wait for the specified interval.
293
- await wait(1000 * intervalInSeconds);
294
- resolve(true);
295
- }
296
- });
297
- }
298
- console.log(`Watching ${abort ? 'aborted' : 'complete'}`);
299
- }
300
- // Otherwise, i.e. if the job or report URL does not exist or is invalid:
301
- else {
302
- // Report this.
303
- console.log('ERROR: Job or report URL does not exist or is invalid');
412
+ });
413
+ })
414
+ // If the job request throws an error:
415
+ .on('error', error => {
416
+ // If it is a refusal to connect:
417
+ if (error.code && error.code.includes('ECONNREFUSED')) {
418
+ // Report this.
419
+ console.log(`${logStart}no connection`);
420
+ }
421
+ // Otherwise, if it was a DNS failure:
422
+ else if (error.code && error.code.includes('ENOTFOUND')) {
423
+ // Report this.
424
+ console.log(`${logStart}no domain name resolution`);
425
+ }
426
+ // Otherwise, if it was any other error with a message:
427
+ else if (error.message) {
428
+ // Report this.
429
+ console.log(`ERROR: ${logStart}got error message ${error.message.slice(0, 200)}`);
430
+ }
431
+ // Otherwise, i.e. if it was any other error with no message:
432
+ else {
433
+ // Report this.
434
+ console.log(`ERROR: ${logStart}got an error with no message`);
435
+ }
436
+ // Unless a job is already being performed, wait and conclude the check.
437
+ if (! jobDispatched) {
438
+ finishAfterWait();
439
+ }
440
+ })
441
+ // Finish sending the job request.
442
+ .end(jobRequestJSON);
443
+ }
444
+ // If requesting a job throws an error:
445
+ catch(error) {
446
+ // Report this.
447
+ console.log(`ERROR requesting a network job (${error.message})`);
448
+ // Wait and conclude the check.
449
+ finishAfterWait();
450
+ }
451
+ });
304
452
  }
453
+ console.log(`Watching ${abort ? 'aborted' : 'complete'}`);
454
+ return ! (abort || reportLost);
305
455
  };
456
+ exports.basicAuthHeader = basicAuthHeader;