@jaimevalasek/aioson 1.3.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 (288) hide show
  1. package/CHANGELOG.md +456 -0
  2. package/CODE_OF_CONDUCT.md +12 -0
  3. package/CONTRIBUTING.md +13 -0
  4. package/LICENSE +21 -0
  5. package/README.md +254 -0
  6. package/bin/aioson.js +4 -0
  7. package/docs/en/cli-reference.md +398 -0
  8. package/docs/en/i18n.md +52 -0
  9. package/docs/en/json-schemas.md +41 -0
  10. package/docs/en/mcp.md +56 -0
  11. package/docs/en/parallel.md +82 -0
  12. package/docs/en/qa-browser.md +339 -0
  13. package/docs/en/release-flow.md +22 -0
  14. package/docs/en/release-notes-template.md +41 -0
  15. package/docs/en/release.md +28 -0
  16. package/docs/en/schemas/agent-prompt.schema.json +17 -0
  17. package/docs/en/schemas/agents.schema.json +32 -0
  18. package/docs/en/schemas/context-validate.schema.json +36 -0
  19. package/docs/en/schemas/doctor.schema.json +89 -0
  20. package/docs/en/schemas/error.schema.json +24 -0
  21. package/docs/en/schemas/i18n-add.schema.json +15 -0
  22. package/docs/en/schemas/index.json +116 -0
  23. package/docs/en/schemas/info.schema.json +39 -0
  24. package/docs/en/schemas/init.schema.json +48 -0
  25. package/docs/en/schemas/install.schema.json +60 -0
  26. package/docs/en/schemas/locale-apply.schema.json +30 -0
  27. package/docs/en/schemas/mcp-doctor.schema.json +95 -0
  28. package/docs/en/schemas/mcp-init.schema.json +122 -0
  29. package/docs/en/schemas/package-test.schema.json +24 -0
  30. package/docs/en/schemas/parallel-assign.schema.json +57 -0
  31. package/docs/en/schemas/parallel-doctor.schema.json +86 -0
  32. package/docs/en/schemas/parallel-init.schema.json +53 -0
  33. package/docs/en/schemas/parallel-status.schema.json +94 -0
  34. package/docs/en/schemas/setup-context.schema.json +39 -0
  35. package/docs/en/schemas/smoke.schema.json +23 -0
  36. package/docs/en/schemas/update.schema.json +48 -0
  37. package/docs/en/schemas/workflow-plan.schema.json +30 -0
  38. package/docs/en/web3.md +54 -0
  39. package/docs/pt/README.md +46 -0
  40. package/docs/pt/advisor-spec.md +335 -0
  41. package/docs/pt/agentes.md +453 -0
  42. package/docs/pt/cenarios.md +1230 -0
  43. package/docs/pt/clientes-ai.md +224 -0
  44. package/docs/pt/comandos-cli.md +511 -0
  45. package/docs/pt/genome-3.0-spec.md +296 -0
  46. package/docs/pt/guia-engineer.md +226 -0
  47. package/docs/pt/inicio-rapido.md +138 -0
  48. package/docs/pt/profiler-system.md +214 -0
  49. package/docs/pt/runtime-observability.md +72 -0
  50. package/docs/pt/squad-genoma.md +777 -0
  51. package/docs/pt/web3.md +797 -0
  52. package/docs/testing/genome-2.0-manual-regression.md +23 -0
  53. package/docs/testing/genome-2.0-matrix.md +36 -0
  54. package/docs/testing/genome-2.0-rollout.md +184 -0
  55. package/package.json +50 -0
  56. package/src/agents.js +56 -0
  57. package/src/cli.js +497 -0
  58. package/src/commands/agents.js +142 -0
  59. package/src/commands/cloud.js +1767 -0
  60. package/src/commands/config.js +90 -0
  61. package/src/commands/context-validate.js +91 -0
  62. package/src/commands/doctor.js +123 -0
  63. package/src/commands/genome-doctor.js +41 -0
  64. package/src/commands/genome-migrate.js +49 -0
  65. package/src/commands/i18n-add.js +56 -0
  66. package/src/commands/info.js +41 -0
  67. package/src/commands/init.js +75 -0
  68. package/src/commands/install.js +68 -0
  69. package/src/commands/locale-apply.js +51 -0
  70. package/src/commands/locale-diff.js +126 -0
  71. package/src/commands/mcp-doctor.js +406 -0
  72. package/src/commands/mcp-init.js +379 -0
  73. package/src/commands/package-e2e.js +273 -0
  74. package/src/commands/parallel-assign.js +403 -0
  75. package/src/commands/parallel-doctor.js +437 -0
  76. package/src/commands/parallel-init.js +249 -0
  77. package/src/commands/parallel-status.js +290 -0
  78. package/src/commands/qa-doctor.js +185 -0
  79. package/src/commands/qa-init.js +161 -0
  80. package/src/commands/qa-report.js +58 -0
  81. package/src/commands/qa-run.js +873 -0
  82. package/src/commands/qa-scan.js +337 -0
  83. package/src/commands/runtime.js +948 -0
  84. package/src/commands/scan-project.js +1107 -0
  85. package/src/commands/setup-context.js +650 -0
  86. package/src/commands/smoke.js +426 -0
  87. package/src/commands/squad-doctor.js +358 -0
  88. package/src/commands/squad-export.js +46 -0
  89. package/src/commands/squad-pipeline.js +97 -0
  90. package/src/commands/squad-repair-genomes.js +39 -0
  91. package/src/commands/squad-status.js +424 -0
  92. package/src/commands/squad-validate.js +230 -0
  93. package/src/commands/test-agents.js +194 -0
  94. package/src/commands/update.js +55 -0
  95. package/src/commands/workflow-next.js +594 -0
  96. package/src/commands/workflow-plan.js +108 -0
  97. package/src/constants.js +314 -0
  98. package/src/context-parse-reason.js +22 -0
  99. package/src/context-writer.js +150 -0
  100. package/src/context.js +217 -0
  101. package/src/detector.js +261 -0
  102. package/src/doctor.js +289 -0
  103. package/src/execution-gateway.js +461 -0
  104. package/src/genome-files.js +198 -0
  105. package/src/genome-format.js +442 -0
  106. package/src/genome-schema.js +215 -0
  107. package/src/genomes/bindings.js +281 -0
  108. package/src/genomes.js +467 -0
  109. package/src/i18n/index.js +103 -0
  110. package/src/i18n/messages/en.js +784 -0
  111. package/src/i18n/messages/es.js +718 -0
  112. package/src/i18n/messages/fr.js +725 -0
  113. package/src/i18n/messages/pt-BR.js +818 -0
  114. package/src/i18n/scaffold.js +64 -0
  115. package/src/installer.js +232 -0
  116. package/src/lib/genomes/compat.js +206 -0
  117. package/src/lib/genomes/migrate.js +90 -0
  118. package/src/lib/squads/genome-repair.js +49 -0
  119. package/src/locales.js +84 -0
  120. package/src/onboarding.js +305 -0
  121. package/src/parser.js +53 -0
  122. package/src/prompt-tool.js +20 -0
  123. package/src/qa-html-report.js +472 -0
  124. package/src/runtime-store.js +1527 -0
  125. package/src/squads/apply-genome.js +21 -0
  126. package/src/squads/genome-binding-service.js +154 -0
  127. package/src/updater.js +32 -0
  128. package/src/utils.js +46 -0
  129. package/src/version.js +50 -0
  130. package/template/.aioson/advisors/.gitkeep +1 -0
  131. package/template/.aioson/agents/analyst.md +225 -0
  132. package/template/.aioson/agents/architect.md +221 -0
  133. package/template/.aioson/agents/dev.md +201 -0
  134. package/template/.aioson/agents/discovery-design-doc.md +196 -0
  135. package/template/.aioson/agents/genoma.md +300 -0
  136. package/template/.aioson/agents/orchestrator.md +107 -0
  137. package/template/.aioson/agents/pm.md +89 -0
  138. package/template/.aioson/agents/product.md +361 -0
  139. package/template/.aioson/agents/profiler-enricher.md +266 -0
  140. package/template/.aioson/agents/profiler-forge.md +188 -0
  141. package/template/.aioson/agents/profiler-researcher.md +245 -0
  142. package/template/.aioson/agents/qa.md +344 -0
  143. package/template/.aioson/agents/setup.md +381 -0
  144. package/template/.aioson/agents/squad.md +837 -0
  145. package/template/.aioson/agents/ux-ui.md +416 -0
  146. package/template/.aioson/config.md +56 -0
  147. package/template/.aioson/context/.gitkeep +0 -0
  148. package/template/.aioson/context/parallel/.gitkeep +0 -0
  149. package/template/.aioson/context/spec.md.template +37 -0
  150. package/template/.aioson/genomas/.gitkeep +0 -0
  151. package/template/.aioson/locales/en/agents/analyst.md +214 -0
  152. package/template/.aioson/locales/en/agents/architect.md +210 -0
  153. package/template/.aioson/locales/en/agents/dev.md +187 -0
  154. package/template/.aioson/locales/en/agents/discovery-design-doc.md +27 -0
  155. package/template/.aioson/locales/en/agents/genoma.md +212 -0
  156. package/template/.aioson/locales/en/agents/orchestrator.md +105 -0
  157. package/template/.aioson/locales/en/agents/pm.md +77 -0
  158. package/template/.aioson/locales/en/agents/product.md +310 -0
  159. package/template/.aioson/locales/en/agents/profiler-enricher.md +5 -0
  160. package/template/.aioson/locales/en/agents/profiler-forge.md +5 -0
  161. package/template/.aioson/locales/en/agents/profiler-researcher.md +5 -0
  162. package/template/.aioson/locales/en/agents/qa.md +214 -0
  163. package/template/.aioson/locales/en/agents/setup.md +342 -0
  164. package/template/.aioson/locales/en/agents/squad.md +247 -0
  165. package/template/.aioson/locales/en/agents/ux-ui.md +320 -0
  166. package/template/.aioson/locales/es/agents/analyst.md +203 -0
  167. package/template/.aioson/locales/es/agents/architect.md +208 -0
  168. package/template/.aioson/locales/es/agents/dev.md +183 -0
  169. package/template/.aioson/locales/es/agents/discovery-design-doc.md +19 -0
  170. package/template/.aioson/locales/es/agents/genoma.md +102 -0
  171. package/template/.aioson/locales/es/agents/orchestrator.md +108 -0
  172. package/template/.aioson/locales/es/agents/pm.md +81 -0
  173. package/template/.aioson/locales/es/agents/product.md +310 -0
  174. package/template/.aioson/locales/es/agents/profiler-enricher.md +5 -0
  175. package/template/.aioson/locales/es/agents/profiler-forge.md +5 -0
  176. package/template/.aioson/locales/es/agents/profiler-researcher.md +5 -0
  177. package/template/.aioson/locales/es/agents/qa.md +163 -0
  178. package/template/.aioson/locales/es/agents/setup.md +347 -0
  179. package/template/.aioson/locales/es/agents/squad.md +247 -0
  180. package/template/.aioson/locales/es/agents/ux-ui.md +201 -0
  181. package/template/.aioson/locales/fr/agents/analyst.md +203 -0
  182. package/template/.aioson/locales/fr/agents/architect.md +208 -0
  183. package/template/.aioson/locales/fr/agents/dev.md +183 -0
  184. package/template/.aioson/locales/fr/agents/discovery-design-doc.md +19 -0
  185. package/template/.aioson/locales/fr/agents/genoma.md +102 -0
  186. package/template/.aioson/locales/fr/agents/orchestrator.md +108 -0
  187. package/template/.aioson/locales/fr/agents/pm.md +81 -0
  188. package/template/.aioson/locales/fr/agents/product.md +310 -0
  189. package/template/.aioson/locales/fr/agents/profiler-enricher.md +5 -0
  190. package/template/.aioson/locales/fr/agents/profiler-forge.md +5 -0
  191. package/template/.aioson/locales/fr/agents/profiler-researcher.md +5 -0
  192. package/template/.aioson/locales/fr/agents/qa.md +163 -0
  193. package/template/.aioson/locales/fr/agents/setup.md +347 -0
  194. package/template/.aioson/locales/fr/agents/squad.md +247 -0
  195. package/template/.aioson/locales/fr/agents/ux-ui.md +201 -0
  196. package/template/.aioson/locales/pt-BR/agents/analyst.md +217 -0
  197. package/template/.aioson/locales/pt-BR/agents/architect.md +213 -0
  198. package/template/.aioson/locales/pt-BR/agents/dev.md +198 -0
  199. package/template/.aioson/locales/pt-BR/agents/discovery-design-doc.md +198 -0
  200. package/template/.aioson/locales/pt-BR/agents/genoma.md +297 -0
  201. package/template/.aioson/locales/pt-BR/agents/orchestrator.md +108 -0
  202. package/template/.aioson/locales/pt-BR/agents/pm.md +81 -0
  203. package/template/.aioson/locales/pt-BR/agents/product.md +316 -0
  204. package/template/.aioson/locales/pt-BR/agents/profiler-enricher.md +5 -0
  205. package/template/.aioson/locales/pt-BR/agents/profiler-forge.md +5 -0
  206. package/template/.aioson/locales/pt-BR/agents/profiler-researcher.md +5 -0
  207. package/template/.aioson/locales/pt-BR/agents/qa.md +217 -0
  208. package/template/.aioson/locales/pt-BR/agents/setup.md +371 -0
  209. package/template/.aioson/locales/pt-BR/agents/squad.md +772 -0
  210. package/template/.aioson/locales/pt-BR/agents/ux-ui.md +322 -0
  211. package/template/.aioson/mcp/servers.md +24 -0
  212. package/template/.aioson/profiler-reports/.gitkeep +1 -0
  213. package/template/.aioson/schemas/content-blueprint.schema.json +30 -0
  214. package/template/.aioson/schemas/genome-meta.schema.json +150 -0
  215. package/template/.aioson/schemas/genome.schema.json +115 -0
  216. package/template/.aioson/schemas/readiness.schema.json +27 -0
  217. package/template/.aioson/schemas/squad-blueprint.schema.json +172 -0
  218. package/template/.aioson/schemas/squad-manifest.schema.json +276 -0
  219. package/template/.aioson/skills/dynamic/README.md +30 -0
  220. package/template/.aioson/skills/dynamic/cardano-docs.md +16 -0
  221. package/template/.aioson/skills/dynamic/ethereum-docs.md +17 -0
  222. package/template/.aioson/skills/dynamic/flux-ui-docs.md +13 -0
  223. package/template/.aioson/skills/dynamic/laravel-docs.md +41 -0
  224. package/template/.aioson/skills/dynamic/npm-packages.md +16 -0
  225. package/template/.aioson/skills/dynamic/solana-docs.md +16 -0
  226. package/template/.aioson/skills/references/premium-command-center-ui/master-application-prompt.md +79 -0
  227. package/template/.aioson/skills/references/premium-command-center-ui/operational-ux-playbook.md +253 -0
  228. package/template/.aioson/skills/references/premium-command-center-ui/quality-validation-checklist.md +82 -0
  229. package/template/.aioson/skills/references/premium-command-center-ui/visual-system-and-component-patterns.md +270 -0
  230. package/template/.aioson/skills/static/django-patterns.md +342 -0
  231. package/template/.aioson/skills/static/fastapi-patterns.md +344 -0
  232. package/template/.aioson/skills/static/filament-patterns.md +267 -0
  233. package/template/.aioson/skills/static/flux-ui-components.md +262 -0
  234. package/template/.aioson/skills/static/git-conventions.md +227 -0
  235. package/template/.aioson/skills/static/interface-design.md +372 -0
  236. package/template/.aioson/skills/static/jetstream-setup.md +200 -0
  237. package/template/.aioson/skills/static/laravel-conventions.md +491 -0
  238. package/template/.aioson/skills/static/nextjs-patterns.md +321 -0
  239. package/template/.aioson/skills/static/node-express-patterns.md +317 -0
  240. package/template/.aioson/skills/static/node-typescript-patterns.md +282 -0
  241. package/template/.aioson/skills/static/premium-command-center-ui.md +190 -0
  242. package/template/.aioson/skills/static/rails-conventions.md +307 -0
  243. package/template/.aioson/skills/static/react-motion-patterns.md +577 -0
  244. package/template/.aioson/skills/static/static-html-patterns.md +1935 -0
  245. package/template/.aioson/skills/static/tall-stack-patterns.md +286 -0
  246. package/template/.aioson/skills/static/ui-ux-modern.md +75 -0
  247. package/template/.aioson/skills/static/web3-cardano-patterns.md +337 -0
  248. package/template/.aioson/skills/static/web3-ethereum-patterns.md +310 -0
  249. package/template/.aioson/skills/static/web3-security-checklist.md +284 -0
  250. package/template/.aioson/skills/static/web3-solana-patterns.md +324 -0
  251. package/template/.aioson/squads/.artisan/.gitkeep +0 -0
  252. package/template/.aioson/squads/.gitkeep +0 -0
  253. package/template/.aioson/squads/memory.md +5 -0
  254. package/template/.aioson/tasks/squad-analyze.md +83 -0
  255. package/template/.aioson/tasks/squad-create.md +99 -0
  256. package/template/.aioson/tasks/squad-design.md +100 -0
  257. package/template/.aioson/tasks/squad-export.md +20 -0
  258. package/template/.aioson/tasks/squad-extend.md +68 -0
  259. package/template/.aioson/tasks/squad-pipeline.md +122 -0
  260. package/template/.aioson/tasks/squad-repair.md +85 -0
  261. package/template/.aioson/tasks/squad-validate.md +58 -0
  262. package/template/.aioson/templates/squads/content-basic/template.json +21 -0
  263. package/template/.aioson/templates/squads/media-channel/template.json +24 -0
  264. package/template/.aioson/templates/squads/research-analysis/template.json +22 -0
  265. package/template/.aioson/templates/squads/software-delivery/template.json +21 -0
  266. package/template/.claude/commands/aioson/analyst.md +5 -0
  267. package/template/.claude/commands/aioson/architect.md +5 -0
  268. package/template/.claude/commands/aioson/dev.md +5 -0
  269. package/template/.claude/commands/aioson/orchestrator.md +5 -0
  270. package/template/.claude/commands/aioson/pm.md +5 -0
  271. package/template/.claude/commands/aioson/qa.md +5 -0
  272. package/template/.claude/commands/aioson/setup.md +5 -0
  273. package/template/.claude/commands/aioson/ux-ui.md +5 -0
  274. package/template/.gemini/GEMINI.md +10 -0
  275. package/template/.gemini/commands/aios-analyst.toml +4 -0
  276. package/template/.gemini/commands/aios-architect.toml +7 -0
  277. package/template/.gemini/commands/aios-dev.toml +8 -0
  278. package/template/.gemini/commands/aios-discovery-design-doc.toml +4 -0
  279. package/template/.gemini/commands/aios-orchestrator.toml +8 -0
  280. package/template/.gemini/commands/aios-pm.toml +8 -0
  281. package/template/.gemini/commands/aios-product.toml +4 -0
  282. package/template/.gemini/commands/aios-qa.toml +6 -0
  283. package/template/.gemini/commands/aios-setup.toml +3 -0
  284. package/template/.gemini/commands/aios-ux-ui.toml +8 -0
  285. package/template/AGENTS.md +67 -0
  286. package/template/CLAUDE.md +31 -0
  287. package/template/OPENCODE.md +24 -0
  288. package/template/aioson-models.json +40 -0
@@ -0,0 +1,337 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+ const fs = require('node:fs/promises');
5
+ const { ensureDir } = require('../utils');
6
+
7
+ const SECRET_PATTERNS = [
8
+ { name: 'OpenAI key', regex: /sk-[a-zA-Z0-9]{20,}/ },
9
+ { name: 'Stripe live key', regex: /pk_live_[a-zA-Z0-9]{20,}/ },
10
+ { name: 'AWS access key', regex: /AKIA[A-Z0-9]{16}/ },
11
+ { name: 'Google API key', regex: /AIzaSy[a-zA-Z0-9_-]{33}/ },
12
+ { name: 'GitHub token', regex: /gh[ps]_[a-zA-Z0-9]{36}/ },
13
+ { name: 'Generic secret', regex: /(SECRET|TOKEN|PASSWORD|PRIVATE_KEY)\s*[:=]\s*['"]?[a-zA-Z0-9_/+=-]{16,}/i }
14
+ ];
15
+
16
+ const SENSITIVE_FILE_PATHS = [
17
+ '/.env', '/.env.local', '/.env.production', '/.git/config',
18
+ '/config.js', '/api/config', '/application.yml'
19
+ ];
20
+
21
+ function requirePlaywright() {
22
+ try { return require('playwright'); } catch { return null; }
23
+ }
24
+
25
+ async function loadConfig(targetDir) {
26
+ try {
27
+ const raw = await fs.readFile(path.join(targetDir, 'aios-qa.config.json'), 'utf8');
28
+ return JSON.parse(raw);
29
+ } catch { return null; }
30
+ }
31
+
32
+ let _counter = 0;
33
+ function makeFinding(severity, category, title, location, risk, fix) {
34
+ _counter++;
35
+ const id = `${severity[0].toUpperCase()}-${String(_counter).padStart(2, '0')}`;
36
+ return { id, severity, category, title, location, risk, fix, screenshot: '', route: location };
37
+ }
38
+
39
+ async function takeScreenshot(page, screenshotsDir, id) {
40
+ try {
41
+ const file = path.join(screenshotsDir, `${id}.png`);
42
+ await page.screenshot({ path: file, fullPage: false });
43
+ return file;
44
+ } catch { return ''; }
45
+ }
46
+
47
+ // --- Crawl all routes from base URL ---
48
+ async function crawlRoutes(page, baseUrl, maxDepth, maxPages) {
49
+ const visited = new Set();
50
+ const queue = [{ url: baseUrl, depth: 0 }];
51
+ const normalizeUrl = (href) => {
52
+ try {
53
+ const u = new URL(href);
54
+ u.hash = '';
55
+ return u.toString().replace(/\/$/, '');
56
+ } catch { return ''; }
57
+ };
58
+
59
+ while (queue.length > 0 && visited.size < maxPages) {
60
+ const { url, depth } = queue.shift();
61
+ const normalized = normalizeUrl(url);
62
+ if (!normalized || visited.has(normalized)) continue;
63
+ if (!normalized.startsWith(baseUrl)) continue;
64
+ visited.add(normalized);
65
+
66
+ if (depth >= maxDepth) continue;
67
+
68
+ try {
69
+ await page.goto(normalized, { waitUntil: 'domcontentloaded', timeout: 8000 });
70
+ const links = await page.$$eval('a[href]', (els) => els.map((el) => el.href)).catch(() => []);
71
+ for (const link of links) {
72
+ const n = normalizeUrl(link);
73
+ if (n && n.startsWith(baseUrl) && !visited.has(n)) {
74
+ queue.push({ url: n, depth: depth + 1 });
75
+ }
76
+ }
77
+ } catch { /* unreachable route — skip */ }
78
+ }
79
+
80
+ return Array.from(visited);
81
+ }
82
+
83
+ // --- Per-route security scan ---
84
+ async function scanRoute(page, route, baseUrl, findings, screenshotsDir) {
85
+ try {
86
+ await page.goto(route, { waitUntil: 'domcontentloaded', timeout: 10000 });
87
+ } catch { return; }
88
+
89
+ // Check exposed secrets in HTML source
90
+ const html = await page.content().catch(() => '');
91
+ for (const { name, regex } of SECRET_PATTERNS) {
92
+ if (regex.test(html)) {
93
+ findings.push(makeFinding(
94
+ 'critical', 'security',
95
+ `${name} found in HTML source`,
96
+ route,
97
+ `${name} is embedded in the HTML and visible to any browser user.`,
98
+ 'Remove from client-side rendering. Serve secrets only from server-side APIs.'
99
+ ));
100
+ }
101
+ }
102
+
103
+ // Check window globals
104
+ const exposed = await page.evaluate((patterns) => {
105
+ const sources = { '__NEXT_DATA__': window.__NEXT_DATA__, '__env__': window.__env__, 'ENV': window.ENV };
106
+ const found = [];
107
+ for (const [src, val] of Object.entries(sources)) {
108
+ if (!val) continue;
109
+ const str = JSON.stringify(val);
110
+ for (const { name, regex } of patterns) {
111
+ if (new RegExp(regex).test(str)) found.push({ source: src, keyType: name });
112
+ }
113
+ }
114
+ return found;
115
+ }, SECRET_PATTERNS.map((p) => ({ name: p.name, regex: p.regex.source }))).catch(() => []);
116
+
117
+ for (const item of exposed) {
118
+ const f = makeFinding(
119
+ 'critical', 'security',
120
+ `${item.keyType} exposed in window.${item.source}`,
121
+ route,
122
+ `${item.keyType} visible to any user via the global object on this route.`,
123
+ 'Move to server-side only. Never expose via NEXT_PUBLIC_ or client-side globals.'
124
+ );
125
+ f.screenshot = await takeScreenshot(page, screenshotsDir, f.id);
126
+ findings.push(f);
127
+ }
128
+
129
+ // Console error leakage
130
+ const consoleLogs = [];
131
+ page.on('console', (msg) => consoleLogs.push({ type: msg.type(), text: msg.text() }));
132
+ await page.waitForTimeout(300).catch(() => {});
133
+ const stackLeaks = consoleLogs.filter((l) => l.type === 'error' && /at\s+\w+\s+\(/.test(l.text));
134
+ if (stackLeaks.length > 0) {
135
+ findings.push(makeFinding(
136
+ 'medium', 'security',
137
+ `Console exposes ${stackLeaks.length} stack trace(s)`,
138
+ route,
139
+ 'Stack traces reveal application internals and library versions.',
140
+ 'Disable verbose error logging in production. Use a centralized error service.'
141
+ ));
142
+ }
143
+
144
+ // Accessibility quick check
145
+ const a11yIssues = await page.evaluate(() => {
146
+ const r = [];
147
+ const imgs = document.querySelectorAll('img:not([alt])');
148
+ if (imgs.length) r.push(`${imgs.length} image(s) missing alt`);
149
+ if (!document.querySelector('html[lang]')) r.push('html missing lang attribute');
150
+ return r;
151
+ }).catch(() => []);
152
+
153
+ if (a11yIssues.length > 0) {
154
+ findings.push(makeFinding(
155
+ 'medium', 'accessibility',
156
+ `Accessibility issues: ${a11yIssues.join('; ')}`,
157
+ route,
158
+ 'WCAG violations affect screen reader users.',
159
+ 'Add alt attributes to images and lang attribute to <html> element.'
160
+ ));
161
+ }
162
+
163
+ // Mobile overflow check
164
+ const hasOverflow = await page.evaluate(() => document.body.scrollWidth > window.innerWidth + 5).catch(() => false);
165
+ if (hasOverflow) {
166
+ findings.push(makeFinding(
167
+ 'medium', 'ux',
168
+ 'Horizontal overflow detected',
169
+ route,
170
+ 'Content overflows horizontally. Breaks mobile layout.',
171
+ 'Audit for fixed-width elements. Use responsive CSS (max-width: 100%, flexbox, grid).'
172
+ ));
173
+ }
174
+ }
175
+
176
+ // --- Check sensitive files (once per domain) ---
177
+ async function scanSensitiveFiles(page, baseUrl, findings) {
178
+ for (const filePath of SENSITIVE_FILE_PATHS) {
179
+ try {
180
+ const response = await page.goto(`${baseUrl}${filePath}`, { waitUntil: 'commit', timeout: 5000 });
181
+ if (response && response.status() === 200) {
182
+ const body = await response.text().catch(() => '');
183
+ if (/[A-Z_]{3,}=/.test(body) || /(SECRET|PASSWORD|TOKEN|KEY)/i.test(body)) {
184
+ findings.push(makeFinding(
185
+ 'critical', 'security',
186
+ `Sensitive file publicly accessible: ${filePath}`,
187
+ `${baseUrl}${filePath}`,
188
+ 'Configuration file exposes credentials, connection strings, or infrastructure details.',
189
+ `Block ${filePath} in your web server. Never deploy .env files to public directories.`
190
+ ));
191
+ }
192
+ }
193
+ } catch { /* not accessible — good */ }
194
+ }
195
+ }
196
+
197
+ // --- Report ---
198
+ function buildScanReport(projectName, baseUrl, routes, findings) {
199
+ const sorted = [...findings].sort((a, b) => {
200
+ const o = { critical: 0, high: 1, medium: 2, low: 3 };
201
+ return (o[a.severity] ?? 4) - (o[b.severity] ?? 4);
202
+ });
203
+ const bySev = (s) => sorted.filter((f) => f.severity === s);
204
+ const date = new Date().toISOString().split('T')[0];
205
+
206
+ let md = `## QA Scan Report — ${projectName} — ${date}\n\n`;
207
+ md += `> Generated by: \`aioson qa:scan\` \n`;
208
+ md += `> Mode: autonomous crawl \n`;
209
+ md += `> Browser: Chromium | URL: ${baseUrl} \n`;
210
+ md += `> Routes scanned: ${routes.length}\n\n`;
211
+
212
+ md += `### Routes discovered\n`;
213
+ for (const r of routes.slice(0, 30)) md += `- ${r}\n`;
214
+ if (routes.length > 30) md += `- ... and ${routes.length - 30} more\n`;
215
+ md += '\n';
216
+
217
+ md += `### Findings\n\n`;
218
+ for (const [label, group] of [['Critical', bySev('critical')], ['High', bySev('high')], ['Medium', bySev('medium')], ['Low', bySev('low')]]) {
219
+ if (group.length === 0) continue;
220
+ md += `#### ${label}\n`;
221
+ for (const f of group) {
222
+ md += `**[${f.id}] ${f.title}** \n`;
223
+ md += `Location: \`${f.location}\` \n`;
224
+ md += `Risk: ${f.risk} \n`;
225
+ md += `Fix: ${f.fix} \n`;
226
+ if (f.screenshot) md += `Screenshot: ${f.screenshot} \n`;
227
+ md += '\n';
228
+ }
229
+ }
230
+
231
+ md += `### Residual risks\n`;
232
+ md += `- Scan does not test authenticated routes (no credentials provided).\n`;
233
+ md += `- Dynamic routes with user-specific IDs were not enumerated.\n`;
234
+ md += `- Full security audit requires manual penetration testing.\n\n`;
235
+
236
+ const c = bySev('critical').length, h = bySev('high').length, m = bySev('medium').length, l = bySev('low').length;
237
+ md += `### Summary\n- Routes: ${routes.length} | Critical: ${c} | High: ${h} | Medium: ${m} | Low: ${l}\n`;
238
+
239
+ return md;
240
+ }
241
+
242
+ async function runQaScan({ args, options = {}, logger, t }) {
243
+ const targetDir = path.resolve(process.cwd(), args[0] || '.');
244
+
245
+ const pw = requirePlaywright();
246
+ if (!pw) {
247
+ logger.error(t('qa_scan.playwright_missing'));
248
+ process.exitCode = 1;
249
+ return { ok: false, error: 'playwright_not_installed' };
250
+ }
251
+
252
+ const config = await loadConfig(targetDir);
253
+ if (!config) {
254
+ logger.error(t('qa_scan.config_missing'));
255
+ process.exitCode = 1;
256
+ return { ok: false, error: 'config_not_found' };
257
+ }
258
+
259
+ const url = String(options.url || config.url || '');
260
+ if (!url) {
261
+ logger.error(t('qa_scan.url_missing'));
262
+ process.exitCode = 1;
263
+ return { ok: false, error: 'url_not_configured' };
264
+ }
265
+
266
+ const projectName = config.project_name || path.basename(targetDir) || 'Project';
267
+ const maxDepth = parseInt(String(options.depth || '3'), 10) || 3;
268
+ const maxPages = parseInt(String(options['max-pages'] || '50'), 10) || 50;
269
+ const headed = Boolean(options.headed);
270
+ const screenshotsDir = path.join(targetDir, 'aios-qa-screenshots');
271
+
272
+ _counter = 0;
273
+ const findings = [];
274
+
275
+ logger.log(t('qa_scan.starting', { url }));
276
+ logger.log(t('qa_scan.crawling', { depth: maxDepth, pages: maxPages }));
277
+ await ensureDir(screenshotsDir);
278
+
279
+ const browser = await pw.chromium.launch({ headless: !headed });
280
+ const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
281
+ const page = await context.newPage();
282
+
283
+ try {
284
+ // Phase 1: crawl all routes
285
+ const routes = await crawlRoutes(page, url, maxDepth, maxPages);
286
+ logger.log(t('qa_scan.routes_found', { count: routes.length }));
287
+
288
+ // Phase 2: scan sensitive files (once)
289
+ await scanSensitiveFiles(page, url, findings).catch(() => {});
290
+
291
+ // Phase 3: scan each route
292
+ for (const route of routes) {
293
+ logger.log(t('qa_scan.scanning_route', { route }));
294
+ await scanRoute(page, route, url, findings, screenshotsDir).catch(() => {});
295
+ }
296
+
297
+ // Write reports
298
+ const mdContent = buildScanReport(projectName, url, routes, findings);
299
+ const mdPath = path.join(targetDir, 'aios-qa-report.md');
300
+ const jsonPath = path.join(targetDir, 'aios-qa-report.json');
301
+
302
+ const bySev = (s) => findings.filter((f) => f.severity === s).length;
303
+ const jsonReport = {
304
+ generated_at: new Date().toISOString(),
305
+ project: projectName, url, mode: 'scan',
306
+ routes_scanned: routes.length,
307
+ summary: { critical: bySev('critical'), high: bySev('high'), medium: bySev('medium'), low: bySev('low') },
308
+ findings
309
+ };
310
+
311
+ await fs.writeFile(mdPath, mdContent, 'utf8');
312
+ await fs.writeFile(jsonPath, `${JSON.stringify(jsonReport, null, 2)}\n`, 'utf8');
313
+
314
+ logger.log(t('qa_scan.done'));
315
+ logger.log(t('qa_scan.report_written', { path: mdPath }));
316
+
317
+ const summary = jsonReport.summary;
318
+ logger.log(t('qa_scan.findings_summary', summary));
319
+
320
+ // HTML report (optional, additive — does not replace MD/JSON)
321
+ let htmlPath;
322
+ if (options.html) {
323
+ const { writeHtmlReport } = require('../qa-html-report');
324
+ const result = await writeHtmlReport(targetDir, projectName, url, findings, [], null, 'scan', screenshotsDir, { routes });
325
+ htmlPath = result.htmlPath;
326
+ logger.log(t('qa_scan.html_report_written', { path: htmlPath }));
327
+ }
328
+
329
+ const output = { ok: true, targetDir, url, routesScanned: routes.length, summary, mdPath, jsonPath, findings, ...(htmlPath ? { htmlPath } : {}) };
330
+ if (options.json) return output;
331
+ return output;
332
+ } finally {
333
+ await browser.close().catch(() => {});
334
+ }
335
+ }
336
+
337
+ module.exports = { runQaScan };