testaro 78.0.8 → 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 (356) 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 +24 -18
  7. package/CONTAINERS.md +9 -6
  8. package/CONTRIBUTING.md +29 -3
  9. package/Dockerfile +1 -1
  10. package/README.md +73 -19
  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 +372 -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 +312 -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 +62 -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/job.js +115 -1
  38. package/procs/launch.d.ts +40 -0
  39. package/procs/launch.js +239 -108
  40. package/procs/nu.d.ts +48 -0
  41. package/procs/scope.js +196 -0
  42. package/procs/shoot.d.ts +33 -0
  43. package/procs/standard.d.ts +18 -0
  44. package/procs/standard.js +83 -0
  45. package/procs/standard.ts +126 -0
  46. package/procs/testAct.js +145 -0
  47. package/procs/testaro.d.ts +20 -0
  48. package/procs/testaro.js +236 -189
  49. package/procs/testaro.ts +321 -0
  50. package/procs/userPath.js +126 -0
  51. package/procs/xPath.d.ts +4 -0
  52. package/procs/xPath.js +81 -69
  53. package/procs/xPath.ts +106 -0
  54. package/procs/xPathScript.d.ts +6 -0
  55. package/procs/xPathScript.js +70 -0
  56. package/run.js +3 -2
  57. package/surea11y/README.md +44 -0
  58. package/surea11y/surea11y.browser.js +14 -0
  59. package/testaro/adbID.d.ts +3 -0
  60. package/testaro/adbID.js +38 -41
  61. package/testaro/adbID.ts +66 -0
  62. package/testaro/allCapStyle.d.ts +3 -0
  63. package/testaro/allCapStyle.js +31 -34
  64. package/testaro/allCapStyle.ts +55 -0
  65. package/testaro/allCaps.d.ts +16 -0
  66. package/testaro/allCaps.js +179 -151
  67. package/testaro/allCaps.ts +228 -0
  68. package/testaro/allHidden.d.ts +13 -0
  69. package/testaro/allHidden.js +29 -30
  70. package/testaro/allHidden.ts +50 -0
  71. package/testaro/allSlanted.d.ts +3 -0
  72. package/testaro/allSlanted.js +30 -33
  73. package/testaro/allSlanted.ts +54 -0
  74. package/testaro/altScheme.d.ts +3 -0
  75. package/testaro/altScheme.js +27 -30
  76. package/testaro/altScheme.ts +50 -0
  77. package/testaro/attVal.d.ts +3 -0
  78. package/testaro/attVal.js +20 -21
  79. package/testaro/attVal.ts +52 -0
  80. package/testaro/autocomplete.d.ts +3 -0
  81. package/testaro/autocomplete.js +59 -75
  82. package/testaro/autocomplete.ts +101 -0
  83. package/testaro/bulk.d.ts +13 -0
  84. package/testaro/bulk.js +32 -33
  85. package/testaro/bulk.ts +55 -0
  86. package/testaro/buttonMenu.d.ts +9 -0
  87. package/testaro/buttonMenu.js +317 -319
  88. package/testaro/buttonMenu.ts +391 -0
  89. package/testaro/captionLoc.d.ts +3 -0
  90. package/testaro/captionLoc.js +17 -20
  91. package/testaro/captionLoc.ts +40 -0
  92. package/testaro/datalistRef.d.ts +3 -0
  93. package/testaro/datalistRef.js +33 -36
  94. package/testaro/datalistRef.ts +55 -0
  95. package/testaro/distortion.d.ts +3 -0
  96. package/testaro/distortion.js +57 -26
  97. package/testaro/distortion.ts +81 -0
  98. package/testaro/docType.d.ts +15 -0
  99. package/testaro/docType.js +25 -25
  100. package/testaro/docType.ts +45 -0
  101. package/testaro/dupAtt.d.ts +16 -0
  102. package/testaro/dupAtt.js +113 -104
  103. package/testaro/dupAtt.ts +144 -0
  104. package/testaro/elements.d.ts +6 -0
  105. package/testaro/elements.js +153 -153
  106. package/testaro/elements.ts +215 -0
  107. package/testaro/embAc.d.ts +3 -0
  108. package/testaro/embAc.js +19 -20
  109. package/testaro/embAc.ts +40 -0
  110. package/testaro/focAll.d.ts +13 -0
  111. package/testaro/focAll.js +185 -191
  112. package/testaro/focAll.ts +217 -0
  113. package/testaro/focAndOp.d.ts +3 -0
  114. package/testaro/focAndOp.js +100 -104
  115. package/testaro/focAndOp.ts +128 -0
  116. package/testaro/focInd.d.ts +3 -0
  117. package/testaro/focInd.js +64 -66
  118. package/testaro/focInd.ts +96 -0
  119. package/testaro/focVis.d.ts +3 -0
  120. package/testaro/focVis.js +28 -30
  121. package/testaro/focVis.ts +52 -0
  122. package/testaro/headEl.d.ts +10 -0
  123. package/testaro/headEl.js +61 -62
  124. package/testaro/headEl.ts +82 -0
  125. package/testaro/headingAmb.d.ts +3 -0
  126. package/testaro/headingAmb.js +54 -63
  127. package/testaro/headingAmb.ts +72 -0
  128. package/testaro/hovInd.d.ts +12 -0
  129. package/testaro/hovInd.js +163 -130
  130. package/testaro/hovInd.ts +199 -0
  131. package/testaro/hover.d.ts +3 -0
  132. package/testaro/hover.js +155 -126
  133. package/testaro/hover.ts +154 -0
  134. package/testaro/hr.d.ts +3 -0
  135. package/testaro/hr.js +14 -17
  136. package/testaro/hr.ts +36 -0
  137. package/testaro/imageLink.d.ts +3 -0
  138. package/testaro/imageLink.js +17 -20
  139. package/testaro/imageLink.ts +42 -0
  140. package/testaro/labClash.d.ts +3 -0
  141. package/testaro/labClash.js +31 -33
  142. package/testaro/labClash.ts +54 -0
  143. package/testaro/legendLoc.d.ts +3 -0
  144. package/testaro/legendLoc.js +17 -20
  145. package/testaro/legendLoc.ts +42 -0
  146. package/testaro/lineHeight.d.ts +3 -0
  147. package/testaro/lineHeight.js +43 -48
  148. package/testaro/lineHeight.ts +70 -0
  149. package/testaro/linkAmb.d.ts +7 -0
  150. package/testaro/linkAmb.js +79 -80
  151. package/testaro/linkAmb.ts +105 -0
  152. package/testaro/linkExt.d.ts +3 -0
  153. package/testaro/linkExt.js +13 -16
  154. package/testaro/linkExt.ts +35 -0
  155. package/testaro/linkOldAtt.d.ts +3 -0
  156. package/testaro/linkOldAtt.js +25 -28
  157. package/testaro/linkOldAtt.ts +48 -0
  158. package/testaro/linkTo.d.ts +3 -0
  159. package/testaro/linkTo.js +22 -22
  160. package/testaro/linkTo.ts +43 -0
  161. package/testaro/linkUl.d.ts +3 -0
  162. package/testaro/linkUl.js +30 -34
  163. package/testaro/linkUl.ts +54 -0
  164. package/testaro/miniText.d.ts +3 -0
  165. package/testaro/miniText.js +41 -44
  166. package/testaro/miniText.ts +68 -0
  167. package/testaro/motion.d.ts +10 -0
  168. package/testaro/motion.js +92 -96
  169. package/testaro/motion.ts +125 -0
  170. package/testaro/nonTable.d.ts +3 -0
  171. package/testaro/nonTable.js +39 -45
  172. package/testaro/nonTable.ts +66 -0
  173. package/testaro/optRoleSel.d.ts +3 -0
  174. package/testaro/optRoleSel.js +16 -19
  175. package/testaro/optRoleSel.ts +41 -0
  176. package/testaro/phOnly.d.ts +3 -0
  177. package/testaro/phOnly.js +18 -21
  178. package/testaro/phOnly.ts +43 -0
  179. package/testaro/pseudoP.d.ts +3 -0
  180. package/testaro/pseudoP.js +36 -38
  181. package/testaro/pseudoP.ts +59 -0
  182. package/testaro/radioSet.d.ts +3 -0
  183. package/testaro/radioSet.js +57 -59
  184. package/testaro/radioSet.ts +79 -0
  185. package/testaro/registry.d.ts +63 -0
  186. package/testaro/registry.js +67 -0
  187. package/testaro/registry.ts +141 -0
  188. package/testaro/role.d.ts +3 -0
  189. package/testaro/role.js +27 -29
  190. package/testaro/role.ts +53 -0
  191. package/testaro/secHeading.d.ts +3 -0
  192. package/testaro/secHeading.js +30 -33
  193. package/testaro/secHeading.ts +53 -0
  194. package/testaro/styleDiff.d.ts +25 -0
  195. package/testaro/styleDiff.js +248 -252
  196. package/testaro/styleDiff.ts +303 -0
  197. package/testaro/tabNav.d.ts +33 -0
  198. package/testaro/tabNav.js +272 -342
  199. package/testaro/tabNav.ts +454 -0
  200. package/testaro/targetsNear.d.ts +9 -0
  201. package/testaro/targetsNear.js +130 -132
  202. package/testaro/targetsNear.ts +160 -0
  203. package/testaro/textNodes.d.ts +6 -0
  204. package/testaro/textNodes.js +139 -135
  205. package/testaro/textNodes.ts +185 -0
  206. package/testaro/textSem.d.ts +3 -0
  207. package/testaro/textSem.js +25 -28
  208. package/testaro/textSem.ts +47 -0
  209. package/testaro/title.d.ts +9 -0
  210. package/testaro/title.js +16 -13
  211. package/testaro/title.ts +31 -0
  212. package/testaro/titledEl.d.ts +3 -0
  213. package/testaro/titledEl.js +15 -18
  214. package/testaro/titledEl.ts +38 -0
  215. package/testaro/zIndex.d.ts +3 -0
  216. package/testaro/zIndex.js +19 -22
  217. package/testaro/zIndex.ts +42 -0
  218. package/tests/alfa.d.ts +45 -0
  219. package/tests/alfa.js +138 -141
  220. package/tests/alfa.ts +214 -0
  221. package/tests/aslint.d.ts +33 -0
  222. package/tests/aslint.js +272 -249
  223. package/tests/aslint.ts +301 -0
  224. package/tests/axe.d.ts +27 -0
  225. package/tests/axe.js +199 -200
  226. package/tests/axe.ts +277 -0
  227. package/tests/ed11y.d.ts +28 -0
  228. package/tests/ed11y.js +141 -99
  229. package/tests/ed11y.ts +178 -0
  230. package/tests/htmlcs.d.ts +21 -0
  231. package/tests/htmlcs.js +174 -140
  232. package/tests/htmlcs.ts +181 -0
  233. package/tests/ibm.d.ts +52 -0
  234. package/tests/ibm.js +164 -166
  235. package/tests/ibm.ts +251 -0
  236. package/tests/nuVal.d.ts +13 -0
  237. package/tests/nuVal.js +107 -112
  238. package/tests/nuVal.ts +145 -0
  239. package/tests/nuVnu.d.ts +15 -0
  240. package/tests/nuVnu.js +141 -111
  241. package/tests/nuVnu.ts +144 -0
  242. package/tests/pour.d.ts +31 -0
  243. package/tests/pour.js +242 -0
  244. package/tests/pour.ts +273 -0
  245. package/tests/qualWeb.d.ts +39 -0
  246. package/tests/qualWeb.js +302 -272
  247. package/tests/qualWeb.ts +415 -0
  248. package/tests/surea11y.d.ts +33 -0
  249. package/tests/surea11y.js +288 -0
  250. package/tests/surea11y.ts +334 -0
  251. package/tests/testaro.d.ts +25 -0
  252. package/tests/testaro.js +745 -652
  253. package/tests/testaro.ts +862 -0
  254. package/tests/wave.d.ts +46 -0
  255. package/tests/wave.js +166 -177
  256. package/tests/wave.ts +252 -0
  257. package/tsconfig.json +17 -0
  258. package/types.d.ts +243 -0
  259. package/types.js +10 -0
  260. package/types.ts +376 -0
  261. package/validation/act/README.md +46 -0
  262. package/validation/act/capture.js +424 -0
  263. package/validation/act/chromium-issue-draft.md +66 -0
  264. package/validation/act/fp-triage-2026-08-22.md +86 -0
  265. package/validation/act/isolation-notes.md +159 -0
  266. package/validation/act/playwright-issue-draft.md +73 -0
  267. package/validation/act/propose-mappings.js +0 -0
  268. package/validation/act/repro-cdp-raw.js +105 -0
  269. package/validation/act/repro-metarefresh.js +65 -0
  270. package/validation/act/score.js +184 -0
  271. package/validation/act/stage3a-stress-report.md +69 -0
  272. package/validation/act/stage3b-mapping-proposals.md +94 -0
  273. package/validation/act/stage3b-triage-draft.md +138 -0
  274. package/validation/act/surea11y-track-a-2026-09-01.md +50 -0
  275. package/validation/executors/netWatch.js +180 -90
  276. package/validation/executors/test.js +17 -2
  277. package/validation/executors/tests.js +118 -10
  278. package/validation/jobs/reports/raw/260901T1000-surea11y-validation.json +964 -0
  279. package/validation/jobs/todo/240101T1200-simple-example.json +14 -6
  280. package/validation/jobs/todo/240101T1300-shoot-example.json +2 -1
  281. package/validation/jobs/todo/260821T1900-pour-validation.json +45 -0
  282. package/validation/jobs/todo/260901T1000-surea11y-validation.json +45 -0
  283. package/validation/knownFailures.json +1 -0
  284. package/validation/tests/jobProperties/adbID.json +27 -2
  285. package/validation/tests/jobProperties/{focOp.json → allCapStyle.json} +56 -53
  286. package/validation/tests/jobProperties/allCaps.json +25 -0
  287. package/validation/tests/jobProperties/allHidden.json +133 -13
  288. package/validation/tests/jobProperties/allSlanted.json +3 -3
  289. package/validation/tests/jobProperties/altScheme.json +23 -8
  290. package/validation/tests/jobProperties/attVal.json +57 -57
  291. package/validation/tests/jobProperties/autocomplete.json +11 -1
  292. package/validation/tests/jobProperties/bulk.json +6 -1
  293. package/validation/tests/jobProperties/buttonMenu.json +63 -42
  294. package/validation/tests/jobProperties/captionLoc.json +1 -6
  295. package/validation/tests/jobProperties/checkpoint-browser.json +401 -0
  296. package/validation/tests/jobProperties/checkpoint-page.json +397 -0
  297. package/validation/tests/jobProperties/checkpoint.json +395 -0
  298. package/validation/tests/jobProperties/datalistRef.json +20 -5
  299. package/validation/tests/jobProperties/distortion.json +28 -3
  300. package/validation/tests/jobProperties/docType.json +2 -2
  301. package/validation/tests/jobProperties/dupAtt.json +48 -33
  302. package/validation/tests/jobProperties/elements.json +28 -28
  303. package/validation/tests/jobProperties/embAc.json +36 -31
  304. package/validation/tests/jobProperties/focAndOp.json +284 -0
  305. package/validation/tests/jobProperties/focInd.json +34 -34
  306. package/validation/tests/jobProperties/focVis.json +1 -1
  307. package/validation/tests/jobProperties/hover.json +39 -37
  308. package/validation/tests/jobProperties/hr.json +13 -3
  309. package/validation/tests/jobProperties/imageLink.json +0 -5
  310. package/validation/tests/jobProperties/labClash.json +68 -33
  311. package/validation/tests/jobProperties/legendLoc.json +1 -6
  312. package/validation/tests/jobProperties/lineHeight.json +2 -2
  313. package/validation/tests/jobProperties/linkAmb.json +13 -18
  314. package/validation/tests/jobProperties/linkExt.json +1 -1
  315. package/validation/tests/jobProperties/linkOldAtt.json +12 -2
  316. package/validation/tests/jobProperties/linkTo.json +1 -1
  317. package/validation/tests/jobProperties/linkUl.json +65 -65
  318. package/validation/tests/jobProperties/miniText.json +7 -2
  319. package/validation/tests/jobProperties/motion.json +4 -50
  320. package/validation/tests/jobProperties/nonTable.json +48 -3
  321. package/validation/tests/jobProperties/optRoleSel.json +12 -2
  322. package/validation/tests/jobProperties/phOnly.json +6 -16
  323. package/validation/tests/jobProperties/pseudoP.json +17 -2
  324. package/validation/tests/jobProperties/radioSet.json +30 -30
  325. package/validation/tests/jobProperties/role.json +25 -5
  326. package/validation/tests/jobProperties/secHeading.json +26 -21
  327. package/validation/tests/jobProperties/styleDiff.json +35 -5
  328. package/validation/tests/jobProperties/tabNav.json +3 -1
  329. package/validation/tests/jobProperties/targetsNear.json +237 -0
  330. package/validation/tests/jobProperties/textNodes.json +29 -29
  331. package/validation/tests/jobProperties/textSem.json +26 -1
  332. package/validation/tests/jobProperties/title.json +12 -2
  333. package/validation/tests/jobProperties/titledEl.json +39 -9
  334. package/validation/tests/jobProperties/userPath.json +317 -0
  335. package/validation/tests/jobProperties/zIndex.json +40 -35
  336. package/validation/tests/targets/allCapStyle/index.html +26 -0
  337. package/validation/tests/targets/checkpoint/index.html +38 -0
  338. package/validation/tests/targets/datalistRef/index.html +1 -1
  339. package/validation/tests/targets/focAndOp/bad.html +29 -0
  340. package/validation/tests/targets/{focOp → focAndOp}/good.html +3 -1
  341. package/validation/tests/targets/focInd/bad.html +2 -1
  342. package/validation/tests/targets/headEl/index.html +10 -1
  343. package/validation/tests/targets/{targetSmall → targetsNear}/index.html +15 -1
  344. package/validation/tests/targets/userPath/index.html +42 -0
  345. package/validation/validateTest.js +61 -9
  346. package/.claude/settings.local.json +0 -11
  347. package/.eslintrc.json +0 -41
  348. package/htmlcs/.eslintrc.json +0 -67
  349. package/validation/tests/jobProperties/linkTitle.json +0 -127
  350. package/validation/tests/jobProperties/opFoc.json +0 -164
  351. package/validation/tests/jobProperties/targetSmall.json +0 -152
  352. package/validation/tests/jobProperties/targetTiny.json +0 -142
  353. package/validation/tests/targets/focOp/bad.html +0 -25
  354. package/validation/tests/targets/linkTitle/index.html +0 -24
  355. package/validation/tests/targets/opFoc/bad.html +0 -26
  356. package/validation/tests/targets/opFoc/good.html +0 -23
package/netWatch.js CHANGED
@@ -18,6 +18,8 @@
18
18
 
19
19
  // Module to keep secrets.
20
20
  require('dotenv').config();
21
+ // Module to access files.
22
+ const fs = require('fs/promises');
21
23
  // Module to validate jobs.
22
24
  const {isValidJob} = require('./procs/job');
23
25
  // Modules to make requests to servers.
@@ -30,9 +32,16 @@ const {nowString} = require('./procs/dateTime');
30
32
 
31
33
  // CONSTANTS
32
34
 
33
- // The netWatch environment variables are required only for network watching,
34
- // but call.js loads this module for every command, so their absence or
35
- // 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.
36
45
  const toURL = urlString => {
37
46
  try {
38
47
  return new URL(urlString);
@@ -41,14 +50,6 @@ const toURL = urlString => {
41
50
  return null;
42
51
  }
43
52
  };
44
- const jobURL = toURL(process.env.NETWATCH_URL_JOB);
45
- const jobHost = jobURL && jobURL.host;
46
- const reportURL = toURL(process.env.NETWATCH_URL_REPORT);
47
- const reportHost = reportURL && reportURL.host;
48
- const agentPW = process.env.NETWATCH_URL_AUTH;
49
-
50
- // FUNCTIONS
51
-
52
53
  // Waits.
53
54
  const wait = ms => {
54
55
  return new Promise(resolve => {
@@ -57,142 +58,301 @@ const wait = ms => {
57
58
  }, ms);
58
59
  });
59
60
  };
60
- // Ends a response with an object in JSON format.
61
- const respondWithObject = (object, response) => {
62
- response.setHeader('content-type', 'application/json; charset=utf-8');
63
- 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
+ }
64
143
  };
65
144
  /*
66
145
  Requests a network job and, when found, performs and reports it.
67
146
  Arguments:
68
147
  0. whether to continue watching after a job is run.
69
148
  1: interval in seconds from a no-job check to the next check.
70
- 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.
71
151
  */
72
- exports.netWatch = async (isForever, intervalInSeconds, isCertTolerant = true) => {
73
- // If the job and report URLs exist and are valid:
74
- if (jobURL && reportURL) {
75
- // Configure the watch.
76
- let noJobYet = true;
77
- let abort = false;
78
- const certInfo = `Certificate-${isCertTolerant ? '' : 'in'}tolerant`;
79
- const foreverInfo = isForever ? 'repeating' : 'one-job';
80
- 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.
81
193
  console.log(
82
- `${certInfo} ${foreverInfo} network watching started ${intervalInfo} (${nowString()})\n`
194
+ 'WARNING: Certificate tolerance disables certificate verification, so credentials and reports are exposed to interception'
83
195
  );
84
- // As long as watching is to continue:
85
- while ((isForever || noJobYet) && ! abort) {
86
- // Log the start of a check.
87
- console.log('--');
88
- // Configure the next check.
89
- const logStart = `Requested job from ${jobHost} and got `;
90
- // Perform it.
91
- await new Promise(async resolve => {
92
- try {
93
- const client = jobURL.protocol === 'https:' ? httpsClient : httpClient;
94
- // Request a job.
95
- const requestOptions = {
96
- method: 'POST',
97
- headers: {
98
- host: jobHost,
99
- '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();
100
246
  }
101
- };
102
- client.request(jobURL, requestOptions, response => {
103
- // Initialize a collection of data from the response.
104
- const chunks = [];
105
- response
106
- // If the response throws an error:
107
- .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) {
108
259
  // Report it.
109
- console.log(`${logStart}error message ${error.message}`);
110
- })
111
- // If the response delivers data:
112
- .on('data', chunk => {
113
- // Add them to the collection.
114
- chunks.push(chunk);
115
- })
116
- // When the response is completed:
117
- .on('end', async () => {
118
- const content = chunks.join('');
119
- try {
120
- // Parse it as a JSON job.
121
- let contentObj = JSON.parse(content);
122
- const {id, sources} = contentObj;
123
- // If it is a no-job message:
124
- 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) {
125
293
  // Report this.
126
- console.log(`${logStart}no job to do; waiting ${intervalInSeconds} sec before next check`);
127
- // Wait for the specified interval.
128
- await wait(1000 * intervalInSeconds);
129
- resolve(true);
294
+ console.log(`${logStart}invalid job (${jobValidity.error})`);
295
+ // Wait and conclude the check.
296
+ finishAfterWait();
130
297
  }
131
- // Otherwise, if it is a job:
132
- else if (id) {
133
- // Check it for validity.
134
- const jobValidity = isValidJob(contentObj);
135
- // If it is invalid:
136
- if (! jobValidity.isValid) {
137
- // Report this to the server.
138
- respondWithObject({
139
- message: `invalidJob`,
140
- error: jobValidity.error
141
- }, response);
142
- console.log(`${logStart}invalid job (${jobValidity.error})`);
143
- // Wait for the specified interval.
144
- await wait(1000 * intervalInSeconds);
145
- 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;
146
306
  }
147
- // Otherwise, i.e. if it is valid:
148
- else {
149
- // Prevent further watching, if unwanted.
150
- noJobYet = false;
151
- // Add the agent and the server ID to the job.
152
- sources.agent = process.env.AGENT || '';
307
+ console.log(`${logStart}job ${id} (${nowString()})`);
308
+ try {
153
309
  // Perform the job and create a report.
154
- console.log(`${logStart}job ${id} (${nowString()})`);
155
- try {
156
- const report = await doJob(contentObj);
157
- const responseObj = {
158
- agentPW,
159
- report
160
- };
161
- let responseJSON = JSON.stringify(responseObj, null, 2);
162
- console.log(`Job ${id} finished (${nowString()})`);
163
- const reportLogStart = `Submitted report ${id} to ${reportURL} and got `;
164
- const requestOptions = {
165
- method: 'POST',
166
- headers: {
167
- host: reportHost,
168
- '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);
169
350
  }
170
- };
171
- // Submit the report.
172
- const client = reportURL.protocol === 'https:' ? httpsClient : httpClient;
173
- client.request(reportURL, requestOptions, repResponse => {
174
- // Initialize a collection of data from the response.
175
- const chunks = [];
176
- repResponse
177
- // If the response to the report threw an error:
178
- .on('error', async error => {
179
- // Report this.
180
- console.log(`${reportLogStart}error message ${error.message}\n`);
181
- // Wait for the specified interval.
182
- await wait(1000 * intervalInSeconds);
183
- resolve(true);
184
- })
185
- // If the response delivers data:
186
- .on('data', chunk => {
187
- // Add them to the collection.
188
- chunks.push(chunk);
189
- })
190
- // When the response to the report is completed:
191
- .on('end', async () => {
192
- const content = chunks.join('');
351
+ // Otherwise, i.e. if the server accepted the report:
352
+ else {
193
353
  try {
194
- // Parse it as a JSON message.
195
- const ackObj = JSON.parse(content);
354
+ // Parse the acknowledgement as JSON.
355
+ const ackObj = JSON.parse(repContent);
196
356
  // Report it.
197
357
  console.log(
198
358
  `${reportLogStart}response message: ${JSON.stringify(ackObj, null, 2)}\n`
@@ -202,105 +362,95 @@ exports.netWatch = async (isForever, intervalInSeconds, isCertTolerant = true) =
202
362
  catch(error) {
203
363
  // Report this.
204
364
  console.log(
205
- `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`
206
366
  );
207
- // Wait for the specified interval.
208
- await wait(1000 * intervalInSeconds);
209
367
  }
210
- // Free the memory used by the job and the report.
211
- contentObj = {};
212
- responseJSON = '';
213
- resolve(true);
214
- });
215
- })
216
- // If the report submission throws an error:
217
- .on('error', async error => {
218
- // Abort the watch.
219
- abort = true;
220
- // Report this.
221
- console.log(
222
- `ERROR ${error.code} in report submission: ${reportLogStart}error message ${error.message}\n`
223
- );
224
- // Wait for the specified interval.
225
- await wait(1000 * intervalInSeconds);
226
- resolve(true);
227
- })
228
- // Finish submitting the report.
229
- .end(responseJSON);
230
- }
231
- catch(error) {
232
- console.log(`ERROR performing job ${id} (${error.message})`);
233
- // Wait for the specified interval.
234
- await wait(1000 * intervalInSeconds);
235
- resolve(true);
236
- }
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();
237
394
  }
238
- }
239
- // Otherwise, i.e. if it is a message:
240
- else {
241
- // Report it.
242
- console.log(`${logStart}${JSON.stringify(contentObj, null, 2)}`);
243
- // Wait for the specified interval.
244
- await wait(1000 * intervalInSeconds);
245
- resolve(true);
246
395
  }
247
396
  }
248
- // Otherwise, i.e. if it is not JSON:
249
- catch(error) {
250
- // Report this.
251
- console.log(`ERROR: ${logStart}status ${response.statusCode}, error message ${error.message}, and non-JSON response ${content.slice(0, 1000)}\n`);
252
- // Wait for the specified interval.
253
- await wait(1000 * intervalInSeconds);
254
- resolve(true);
255
- };
256
- });
257
- })
258
- // If the job request throws an error:
259
- .on('error', async error => {
260
- // If it is a refusal to connect:
261
- if (error.code && error.code.includes('ECONNREFUSED')) {
262
- // Report this.
263
- console.log(`${logStart}no connection`);
264
- }
265
- // Otherwise, if it was a DNS failure:
266
- else if (error.code && error.code.includes('ENOTFOUND')) {
267
- // Report this.
268
- console.log(`${logStart}no domain name resolution`);
269
- }
270
- // Otherwise, if it was any other error with a message:
271
- else if (error.message) {
272
- // Report this.
273
- 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
+ }
274
404
  }
275
- // Otherwise, i.e. if it was any other error with no message:
276
- else {
405
+ // Otherwise, i.e. if it is not JSON:
406
+ catch(error) {
277
407
  // Report this.
278
- 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();
279
411
  }
280
- // Wait for the specified interval.
281
- await wait(1000 * intervalInSeconds);
282
- resolve(true);
283
- })
284
- // Finish sending the job request.
285
- .end(JSON.stringify({
286
- agentPW
287
- }));
288
- }
289
- // If requesting a job throws an error:
290
- catch(error) {
291
- // Report this.
292
- console.log(`ERROR requesting a network job (${error.message})`);
293
- // Wait for the specified interval.
294
- await wait(1000 * intervalInSeconds);
295
- resolve(true);
296
- }
297
- });
298
- }
299
- console.log(`Watching ${abort ? 'aborted' : 'complete'}`);
300
- }
301
- // Otherwise, i.e. if the job or report URL does not exist or is invalid:
302
- else {
303
- // Report this.
304
- 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
+ });
305
452
  }
453
+ console.log(`Watching ${abort ? 'aborted' : 'complete'}`);
454
+ return ! (abort || reportLost);
306
455
  };
456
+ exports.basicAuthHeader = basicAuthHeader;