@daniellins/power-claude 0.16.3

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.

Potentially problematic release.


This version of @daniellins/power-claude might be problematic. Click here for more details.

Files changed (467) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +121 -0
  3. package/bin/cli.js +124 -0
  4. package/installer/__tests__/doctor-ready-oracles.test.cjs +140 -0
  5. package/installer/__tests__/merge-settings.test.cjs +330 -0
  6. package/installer/adopt.js +161 -0
  7. package/installer/copy.js +203 -0
  8. package/installer/doctor.js +266 -0
  9. package/installer/install.js +155 -0
  10. package/installer/manifest.js +102 -0
  11. package/installer/motor-scripts.js +50 -0
  12. package/installer/motor-scripts.json +39 -0
  13. package/installer/uninstall.js +99 -0
  14. package/installer/update.js +376 -0
  15. package/installer/util.js +113 -0
  16. package/installer/wizard.js +64 -0
  17. package/package.json +26 -0
  18. package/scripts/__fixtures__/anti-path-demo/agents/ap-writer.md +13 -0
  19. package/scripts/__fixtures__/anti-path-demo/templates/write-tmpl.md +7 -0
  20. package/scripts/__fixtures__/anti-path-demo/workflows/wf-ap.smoke.json +10 -0
  21. package/scripts/__fixtures__/anti-path-demo/workflows/wf-ap.yaml +25 -0
  22. package/scripts/__fixtures__/assess-tier/retrofit-leve/pod/agents/content-chief.md +1 -0
  23. package/scripts/__fixtures__/assess-tier/retrofit-leve/pod/workflows/wf-criacao-redteam.yaml +3 -0
  24. package/scripts/__fixtures__/assess-tier/retrofit-leve/pod-spec.json +13 -0
  25. package/scripts/__fixtures__/fidelity/fake-green-average-in-band.yaml +21 -0
  26. package/scripts/__fixtures__/fidelity/fake-green-average.yaml +18 -0
  27. package/scripts/__fixtures__/fidelity/fake-green-denominator.yaml +19 -0
  28. package/scripts/__fixtures__/fidelity/fake-green-missing-evidence.yaml +18 -0
  29. package/scripts/__fixtures__/fidelity/fake-green-threshold.yaml +18 -0
  30. package/scripts/__fixtures__/fidelity/fake-green-weight.yaml +18 -0
  31. package/scripts/__fixtures__/fidelity/golden.yaml +31 -0
  32. package/scripts/__fixtures__/fidelity/trindade-incoherent.yaml +15 -0
  33. package/scripts/__fixtures__/forge-canonical/pod-spec-example.json +37 -0
  34. package/scripts/__fixtures__/migrate-squad/authored/agents/offer-engineer.md +17 -0
  35. package/scripts/__fixtures__/migrate-squad/authored/knowledge/no-source.md +7 -0
  36. package/scripts/__fixtures__/migrate-squad/authored/knowledge/value-stack-method.md +8 -0
  37. package/scripts/__fixtures__/migrate-squad/authored/specs/wf-sample.smoke.json +24 -0
  38. package/scripts/__fixtures__/migrate-squad/authored/specs/wf-sample.spec.json +45 -0
  39. package/scripts/__fixtures__/migrate-squad/legacy-squad/HEADLINE.md +3 -0
  40. package/scripts/__fixtures__/migrate-squad/legacy-squad/README.md +4 -0
  41. package/scripts/__fixtures__/migrate-squad/legacy-squad/agents/hormozi-offer.md +14 -0
  42. package/scripts/__fixtures__/migrate-squad/legacy-squad/checklists/dead-redundant.md +4 -0
  43. package/scripts/__fixtures__/migrate-squad/legacy-squad/checklists/method-guide.md +6 -0
  44. package/scripts/__fixtures__/migrate-squad/legacy-squad/checklists/veto-screen.md +4 -0
  45. package/scripts/__fixtures__/migrate-squad/legacy-squad/config/config.yaml +2 -0
  46. package/scripts/__fixtures__/migrate-squad/legacy-squad/data/case-library.md +3 -0
  47. package/scripts/__fixtures__/migrate-squad/legacy-squad/scripts/validate-mvn.py +16 -0
  48. package/scripts/__fixtures__/migrate-squad/legacy-squad/sops/sop-sample.md +3 -0
  49. package/scripts/__fixtures__/migrate-squad/legacy-squad/squad-io.yaml +14 -0
  50. package/scripts/__fixtures__/migrate-squad/legacy-squad/tasks/gather-context.md +7 -0
  51. package/scripts/__fixtures__/migrate-squad/legacy-squad/templates/output-tmpl.md +5 -0
  52. package/scripts/__fixtures__/migrate-squad/legacy-squad/workflows/wf-sample.yaml +24 -0
  53. package/scripts/__fixtures__/pipeline/pipelines/pipeline-checkpoint.yaml +21 -0
  54. package/scripts/__fixtures__/pipeline/pipelines/pipeline-demo.yaml +22 -0
  55. package/scripts/__fixtures__/pipeline/pods/pipe-a/agents/maker.md +13 -0
  56. package/scripts/__fixtures__/pipeline/pods/pipe-a/memory/learnings.consolidated.md +8 -0
  57. package/scripts/__fixtures__/pipeline/pods/pipe-a/memory/learnings.jsonl +83 -0
  58. package/scripts/__fixtures__/pipeline/pods/pipe-a/templates/make-tmpl.md +8 -0
  59. package/scripts/__fixtures__/pipeline/pods/pipe-a/workflows/wf-a.yaml +16 -0
  60. package/scripts/__fixtures__/pipeline/pods/pipe-b/agents/user.md +13 -0
  61. package/scripts/__fixtures__/pipeline/pods/pipe-b/memory/learnings.consolidated.md +8 -0
  62. package/scripts/__fixtures__/pipeline/pods/pipe-b/memory/learnings.jsonl +14 -0
  63. package/scripts/__fixtures__/pipeline/pods/pipe-b/templates/use-tmpl.md +6 -0
  64. package/scripts/__fixtures__/pipeline/pods/pipe-b/workflows/wf-b.yaml +16 -0
  65. package/scripts/__fixtures__/pod-run/agents/forge-chief.md +13 -0
  66. package/scripts/__fixtures__/pod-run/agents/pod-critic.md +13 -0
  67. package/scripts/__fixtures__/pod-run/templates/build-tmpl.md +8 -0
  68. package/scripts/__fixtures__/pod-run/templates/gate-tmpl.md +6 -0
  69. package/scripts/__fixtures__/pod-run/workflows/wf-min.yaml +43 -0
  70. package/scripts/__fixtures__/pod-run/workflows/wf-skip-modes.yaml +50 -0
  71. package/scripts/__fixtures__/pod-workflow/invalid-checkpoint-all-skippable.yaml +30 -0
  72. package/scripts/__fixtures__/pod-workflow/invalid-checkpoint-criteria-escalar-de-bloco.yaml +40 -0
  73. package/scripts/__fixtures__/pod-workflow/invalid-checkpoint-skip-mode-desconhecido.yaml +30 -0
  74. package/scripts/__fixtures__/pod-workflow/invalid-checkpoint-unknown-key.yaml +29 -0
  75. package/scripts/__fixtures__/pod-workflow/invalid-cycle.yaml +26 -0
  76. package/scripts/__fixtures__/pod-workflow/invalid-fix-loop-backedge.yaml +29 -0
  77. package/scripts/__fixtures__/pod-workflow/invalid-fix-loop-forward.yaml +29 -0
  78. package/scripts/__fixtures__/pod-workflow/invalid-handoff-open.yaml +27 -0
  79. package/scripts/__fixtures__/pod-workflow/invalid-knowledge-shape.yaml +18 -0
  80. package/scripts/__fixtures__/pod-workflow/invalid-missing-agent.yaml +15 -0
  81. package/scripts/__fixtures__/pod-workflow/invalid-missing-outputs.yaml +15 -0
  82. package/scripts/__fixtures__/pod-workflow/invalid-missing-template.yaml +15 -0
  83. package/scripts/__fixtures__/pod-workflow/invalid-missing-veto.yaml +13 -0
  84. package/scripts/__fixtures__/pod-workflow/invalid-template-not-on-disk.yaml +16 -0
  85. package/scripts/__fixtures__/pod-workflow/invalid-veto-prose.yaml +17 -0
  86. package/scripts/__fixtures__/pod-workflow/knowledge/a.md +4 -0
  87. package/scripts/__fixtures__/pod-workflow/knowledge/b.md +4 -0
  88. package/scripts/__fixtures__/pod-workflow/knowledge/c.md +4 -0
  89. package/scripts/__fixtures__/pod-workflow/templates/step-tmpl.md +7 -0
  90. package/scripts/__fixtures__/pod-workflow/valid-checkpoint-skip-modes.yaml +29 -0
  91. package/scripts/__fixtures__/pod-workflow/valid-fix-loop.yaml +30 -0
  92. package/scripts/__fixtures__/pod-workflow/valid-knowledge.yaml +30 -0
  93. package/scripts/__fixtures__/pod-workflow/valid-veto-always-true.yaml +20 -0
  94. package/scripts/__fixtures__/pod-workflow/valid.yaml +27 -0
  95. package/scripts/__fixtures__/smoke-canary/agents/canary-reporter.md +13 -0
  96. package/scripts/__fixtures__/smoke-canary/agents/canary-researcher.md +13 -0
  97. package/scripts/__fixtures__/smoke-canary/agents/canary-synth.md +13 -0
  98. package/scripts/__fixtures__/smoke-canary/memory/learnings.consolidated.md +10 -0
  99. package/scripts/__fixtures__/smoke-canary/memory/learnings.jsonl +448 -0
  100. package/scripts/__fixtures__/smoke-canary/templates/report-tmpl.md +9 -0
  101. package/scripts/__fixtures__/smoke-canary/templates/research-tmpl.md +7 -0
  102. package/scripts/__fixtures__/smoke-canary/templates/synth-tmpl.md +9 -0
  103. package/scripts/__fixtures__/smoke-canary/workflows/wf-canary.smoke.json +11 -0
  104. package/scripts/__fixtures__/smoke-canary/workflows/wf-canary.yaml +53 -0
  105. package/scripts/__fixtures__/source-audit/fake-green-bronze-unsignaled.yaml +10 -0
  106. package/scripts/__fixtures__/source-audit/fake-green-counts.yaml +9 -0
  107. package/scripts/__fixtures__/source-audit/fake-green-missing-evidence.yaml +9 -0
  108. package/scripts/__fixtures__/source-audit/fake-green-unclassified.yaml +10 -0
  109. package/scripts/__fixtures__/source-audit/fake-green-verdict.yaml +10 -0
  110. package/scripts/__fixtures__/source-audit/golden-blocked.yaml +10 -0
  111. package/scripts/__fixtures__/source-audit/golden.yaml +11 -0
  112. package/scripts/__fixtures__/sweep-forms/agents/forms-count.md +13 -0
  113. package/scripts/__fixtures__/sweep-forms/agents/forms-field.md +13 -0
  114. package/scripts/__fixtures__/sweep-forms/agents/forms-fileref.md +13 -0
  115. package/scripts/__fixtures__/sweep-forms/agents/forms-json.md +13 -0
  116. package/scripts/__fixtures__/sweep-forms/agents/forms-section.md +13 -0
  117. package/scripts/__fixtures__/sweep-forms/specs/wf-forms.spec.json +5 -0
  118. package/scripts/__fixtures__/sweep-forms/templates/t_count.md +5 -0
  119. package/scripts/__fixtures__/sweep-forms/templates/t_field.md +3 -0
  120. package/scripts/__fixtures__/sweep-forms/templates/t_fileref.md +3 -0
  121. package/scripts/__fixtures__/sweep-forms/templates/t_json.md +0 -0
  122. package/scripts/__fixtures__/sweep-forms/templates/t_section.md +3 -0
  123. package/scripts/__fixtures__/sweep-forms/workflows/wf-forms.smoke.json +13 -0
  124. package/scripts/__fixtures__/sweep-forms/workflows/wf-forms.yaml +67 -0
  125. package/scripts/__fixtures__/validate-story-acs-corpus/corpus-1-noop-state-json.fixture.md +18 -0
  126. package/scripts/__fixtures__/validate-story-acs-corpus/corpus-2-crash-buildworkflowyaml.fixture.md +26 -0
  127. package/scripts/__fixtures__/validate-story-acs-corpus/corpus-3-nasce-verde-skip-modes.fixture.md +29 -0
  128. package/scripts/__fixtures__/validate-story-acs-corpus/corpus-4-guarda-sem-irmao.fixture.md +17 -0
  129. package/scripts/__fixtures__/validate-story-acs-corpus/corpus-5a-diff-stat-sem-guarda.fixture.md +17 -0
  130. package/scripts/__fixtures__/validate-story-acs-corpus/corpus-5b-test-f-sobre-diretorio.fixture.md +20 -0
  131. package/scripts/__fixtures__/veto-checks/copy-clean.md +4 -0
  132. package/scripts/__fixtures__/veto-checks/copy-dashes.md +4 -0
  133. package/scripts/__fixtures__/veto-checks/n8n-bad.md +18 -0
  134. package/scripts/__fixtures__/veto-checks/n8n-ok.md +22 -0
  135. package/scripts/__fixtures__/veto-checks/page-bad.html +7 -0
  136. package/scripts/__fixtures__/veto-checks/phash/copy.png +0 -0
  137. package/scripts/__fixtures__/veto-checks/phash/distinct.png +0 -0
  138. package/scripts/__fixtures__/veto-checks/phash/distinct2.png +0 -0
  139. package/scripts/__fixtures__/veto-checks/phash/make-fixtures.cjs +203 -0
  140. package/scripts/__fixtures__/veto-checks/phash/ref.png +0 -0
  141. package/scripts/__fixtures__/veto-checks/slide-ok.html +15 -0
  142. package/scripts/__fixtures__/veto-checks/slide-overflow.html +14 -0
  143. package/scripts/__fixtures__/veto-checks/sweep-pod/agents/vc-checker.md +13 -0
  144. package/scripts/__fixtures__/veto-checks/sweep-pod/templates/check-tmpl.md +6 -0
  145. package/scripts/__fixtures__/veto-checks/sweep-pod/workflows/wf-vc.smoke.json +10 -0
  146. package/scripts/__fixtures__/veto-checks/sweep-pod/workflows/wf-vc.yaml +19 -0
  147. package/scripts/__fixtures__/voz-do-dono/README.md +23 -0
  148. package/scripts/__fixtures__/voz-do-dono/dono-escalar.yaml +9 -0
  149. package/scripts/__fixtures__/voz-do-dono/dono-guia.yaml +10 -0
  150. package/scripts/__fixtures__/voz-do-dono/peca-3a-pessoa.md +6 -0
  151. package/scripts/__fixtures__/voz-do-dono/peca-com-lexico-proibido.md +6 -0
  152. package/scripts/__fixtures__/voz-do-dono/peca-limpa-1a-pessoa.md +6 -0
  153. package/scripts/__tests__/derive-ready-transitions.test.cjs +269 -0
  154. package/scripts/__tests__/enforce-cycle-authority.test.cjs +869 -0
  155. package/scripts/__tests__/enforce-git-push-authority.test.cjs +701 -0
  156. package/scripts/__tests__/enforce-ready-oracles.test.cjs +745 -0
  157. package/scripts/__tests__/motor-scripts.test.cjs +339 -0
  158. package/scripts/__tests__/pc-run-pod-counter.test.cjs +58 -0
  159. package/scripts/__tests__/pc-run-pod.test.cjs +578 -0
  160. package/scripts/__tests__/validate-pod-workflow.test.cjs +90 -0
  161. package/scripts/__tests__/validate-story-acs.test.cjs +607 -0
  162. package/scripts/anti-path.test.cjs +163 -0
  163. package/scripts/arena.cjs +514 -0
  164. package/scripts/arena.test.cjs +213 -0
  165. package/scripts/brief-leitura-obrigatoria.test.cjs +364 -0
  166. package/scripts/consolidate-knowledge-runner.test.cjs +249 -0
  167. package/scripts/default-funcional.test.cjs +153 -0
  168. package/scripts/derive-ready-transitions.cjs +325 -0
  169. package/scripts/forge-additive.test.cjs +158 -0
  170. package/scripts/forge-assess-tier.cjs +205 -0
  171. package/scripts/forge-assess-tier.test.cjs +114 -0
  172. package/scripts/forge-canonical.test.cjs +624 -0
  173. package/scripts/forge-generate.cjs +1303 -0
  174. package/scripts/forge-harden-learning.cjs +921 -0
  175. package/scripts/forge-harden-learning.test.cjs +242 -0
  176. package/scripts/forge-ingest-memory.cjs +136 -0
  177. package/scripts/forge-upgrade-pod.cjs +368 -0
  178. package/scripts/forge.test.cjs +609 -0
  179. package/scripts/harden-knowledge.test.cjs +612 -0
  180. package/scripts/harness-g5-lite.cjs +243 -0
  181. package/scripts/harness-g5-lite.test.cjs +134 -0
  182. package/scripts/hn-carousel-contrato-entrada.test.cjs +96 -0
  183. package/scripts/migrate-squad-orchestration.test.cjs +146 -0
  184. package/scripts/migration-manifest-sf3.2.test.cjs +382 -0
  185. package/scripts/migration-manifest-sf3.3.test.cjs +170 -0
  186. package/scripts/migration-manifest-sf5.5.test.cjs +189 -0
  187. package/scripts/migration-manifest.cjs +1146 -0
  188. package/scripts/migration-manifest.test.cjs +186 -0
  189. package/scripts/npm-scope-invocation.test.cjs +194 -0
  190. package/scripts/owner-layer.cjs +442 -0
  191. package/scripts/owner-layer.test.cjs +358 -0
  192. package/scripts/pc-run-pipeline.cjs +289 -0
  193. package/scripts/pc-run-pipeline.test.cjs +256 -0
  194. package/scripts/pc-run-pod-countgte.test.cjs +66 -0
  195. package/scripts/pc-run-pod.cjs +1355 -0
  196. package/scripts/phash-gate.test.cjs +433 -0
  197. package/scripts/pod-export-compile.cjs +487 -0
  198. package/scripts/pod-export-compile.test.cjs +312 -0
  199. package/scripts/pod-export-executor.cjs +678 -0
  200. package/scripts/pod-export-executor.test.cjs +1341 -0
  201. package/scripts/pod-export-hardening.test.cjs +531 -0
  202. package/scripts/pod-export-postprocess.cjs +427 -0
  203. package/scripts/pod-export-postprocess.test.cjs +558 -0
  204. package/scripts/pod-export-queue-race.test.cjs +865 -0
  205. package/scripts/pod-export-release.cjs +473 -0
  206. package/scripts/pod-export-release.test.cjs +267 -0
  207. package/scripts/pod-export-service.cjs +1297 -0
  208. package/scripts/pod-export-service.test.cjs +489 -0
  209. package/scripts/pod-export-tools.cjs +470 -0
  210. package/scripts/pod-export-ui.cjs +805 -0
  211. package/scripts/pod-export-ui.test.cjs +301 -0
  212. package/scripts/pod-memory-budget.test.cjs +309 -0
  213. package/scripts/pod-memory-note.cjs +357 -0
  214. package/scripts/pod-memory-note.test.cjs +452 -0
  215. package/scripts/pod-memory.cjs +1252 -0
  216. package/scripts/pod-memory.test.cjs +951 -0
  217. package/scripts/render-png.test.cjs +307 -0
  218. package/scripts/smoke-isolation.test.cjs +100 -0
  219. package/scripts/smoke-pod-workflow.cjs +755 -0
  220. package/scripts/smoke-sweep.test.cjs +170 -0
  221. package/scripts/validate-adopt.js +235 -0
  222. package/scripts/validate-branding.js +84 -0
  223. package/scripts/validate-budgets.js +140 -0
  224. package/scripts/validate-fidelity-output.cjs +267 -0
  225. package/scripts/validate-fidelity-output.test.cjs +101 -0
  226. package/scripts/validate-install.js +230 -0
  227. package/scripts/validate-no-clone.cjs +220 -0
  228. package/scripts/validate-no-clone.test.cjs +77 -0
  229. package/scripts/validate-pipeline.cjs +145 -0
  230. package/scripts/validate-pod-workflow.cjs +878 -0
  231. package/scripts/validate-source-audit.cjs +170 -0
  232. package/scripts/validate-source-audit.test.cjs +90 -0
  233. package/scripts/validate-story-acs.cjs +862 -0
  234. package/scripts/validate-update-scope-motor.js +421 -0
  235. package/scripts/veto-checks/html-valid.cjs +92 -0
  236. package/scripts/veto-checks/lexical-check.cjs +62 -0
  237. package/scripts/veto-checks/phash-gate.cjs +589 -0
  238. package/scripts/veto-checks/render-gate.cjs +165 -0
  239. package/scripts/veto-checks/render-png.cjs +358 -0
  240. package/scripts/veto-checks.test.cjs +219 -0
  241. package/scripts/watcher-audit.cjs +178 -0
  242. package/template/.claude/CLAUDE.md +103 -0
  243. package/template/.claude/agents/pc-analyst.md +100 -0
  244. package/template/.claude/agents/pc-architect.md +32 -0
  245. package/template/.claude/agents/pc-dev.md +27 -0
  246. package/template/.claude/agents/pc-devops.md +45 -0
  247. package/template/.claude/agents/pc-planner.md +32 -0
  248. package/template/.claude/agents/pc-po.md +25 -0
  249. package/template/.claude/agents/pc-qa.md +25 -0
  250. package/template/.claude/commands/power/academy.md +9 -0
  251. package/template/.claude/commands/power/automation.md +9 -0
  252. package/template/.claude/commands/power/content-carousel.md +267 -0
  253. package/template/.claude/commands/power/content.md +9 -0
  254. package/template/.claude/commands/power/dev.md +9 -0
  255. package/template/.claude/commands/power/forge.md +224 -0
  256. package/template/.claude/commands/power/hormozi.md +221 -0
  257. package/template/.claude/commands/power/modo.md +57 -0
  258. package/template/.claude/hooks/README.md +114 -0
  259. package/template/.claude/hooks/context-budget.cjs +64 -0
  260. package/template/.claude/hooks/enforce-cycle-authority.cjs +457 -0
  261. package/template/.claude/hooks/enforce-git-push-authority.cjs +925 -0
  262. package/template/.claude/hooks/enforce-ready-oracles.cjs +502 -0
  263. package/template/.claude/rules/authority.md +76 -0
  264. package/template/.claude/rules/curadoria-de-conhecimento.md +30 -0
  265. package/template/.claude/rules/model-matrix.md +34 -0
  266. package/template/.claude/rules/orchestration.md +133 -0
  267. package/template/.claude/settings.json +50 -0
  268. package/template/.claude/skills/pc-apply-qa-fixes/SKILL.md +108 -0
  269. package/template/.claude/skills/pc-apply-qa-fixes/references/fix-loop.md +40 -0
  270. package/template/.claude/skills/pc-apply-qa-fixes/references/fix-request-template.md +70 -0
  271. package/template/.claude/skills/pc-arena/SKILL.md +96 -0
  272. package/template/.claude/skills/pc-close-story/SKILL.md +105 -0
  273. package/template/.claude/skills/pc-commit/SKILL.md +108 -0
  274. package/template/.claude/skills/pc-develop-story/SKILL.md +116 -0
  275. package/template/.claude/skills/pc-full-cycle/SKILL.md +150 -0
  276. package/template/.claude/skills/pc-pipeline/SKILL.md +63 -0
  277. package/template/.claude/skills/pc-pipeline/references/gate-templates.md +46 -0
  278. package/template/.claude/skills/pc-review-story/SKILL.md +129 -0
  279. package/template/.claude/skills/pc-review-story/references/false-positive-detection.md +50 -0
  280. package/template/.claude/skills/pc-review-story/references/nfr-assess.md +47 -0
  281. package/template/.claude/skills/pc-review-story/references/risk-profile.md +75 -0
  282. package/template/.claude/skills/pc-review-story/references/security-checklist.md +69 -0
  283. package/template/.claude/skills/pc-review-story/references/test-design.md +37 -0
  284. package/template/.claude/skills/pc-review-story/references/trace-requirements.md +46 -0
  285. package/template/.claude/skills/pc-run-pod/SKILL.md +123 -0
  286. package/template/.claude/skills/pc-validate-story/SKILL.md +114 -0
  287. package/template/.claude/skills/pc-wave/SKILL.md +133 -0
  288. package/template/.env.example +10 -0
  289. package/template/knowledge/analyst/brainstorming.md +40 -0
  290. package/template/knowledge/analyst/brief-skeleton.md +32 -0
  291. package/template/knowledge/analyst/competitor-skeleton.md +39 -0
  292. package/template/knowledge/analyst/elicitation-menu.md +49 -0
  293. package/template/knowledge/analyst/market-research-skeleton.md +36 -0
  294. package/template/knowledge/analyst/research-prompt-structure.md +48 -0
  295. package/template/knowledge/analyst/roi-skeleton.md +51 -0
  296. package/template/knowledge/voice-dna/README.md +99 -0
  297. package/template/knowledge/voice-dna/voice-dna.example.yaml +157 -0
  298. package/template/pods/README.md +64 -0
  299. package/template/pods/academy/README.md +37 -0
  300. package/template/pods/academy/agents/academy-chief.md +27 -0
  301. package/template/pods/academy/examples/palestra.example.md +142 -0
  302. package/template/pods/academy/pod-spec.json +44 -0
  303. package/template/pods/academy/templates/materiais-tmpl.md +17 -0
  304. package/template/pods/academy/templates/revisao-didatica-tmpl.md +20 -0
  305. package/template/pods/academy/workflows/wf-materiais-revisao.yaml +30 -0
  306. package/template/pods/authority-content/README.md +43 -0
  307. package/template/pods/authority-content/agents/content-chief.md +28 -0
  308. package/template/pods/authority-content/pod-spec.json +45 -0
  309. package/template/pods/authority-content/templates/criacao-na-voz-tmpl.md +17 -0
  310. package/template/pods/authority-content/templates/red-team-factual-tmpl.md +23 -0
  311. package/template/pods/authority-content/workflows/wf-criacao-redteam.yaml +30 -0
  312. package/template/pods/automation/README.md +37 -0
  313. package/template/pods/automation/agents/langgraph-architect.md +30 -0
  314. package/template/pods/automation/agents/n8n-architect.md +30 -0
  315. package/template/pods/automation/examples/README.md +46 -0
  316. package/template/pods/automation/examples/n8n-content-radar.example.json +380 -0
  317. package/template/pods/automation/knowledge/langgraph-patterns.md +126 -0
  318. package/template/pods/automation/knowledge/n8n-patterns.md +131 -0
  319. package/template/pods/automation/memory/README.md +16 -0
  320. package/template/pods/automation/memory/learnings.consolidated.md +8 -0
  321. package/template/pods/automation/memory/learnings.jsonl +0 -0
  322. package/template/pods/automation/pod-spec.json +109 -0
  323. package/template/pods/automation/specs/wf-langgraph-build.spec.json +107 -0
  324. package/template/pods/automation/squad-io.yaml +28 -0
  325. package/template/pods/automation/templates/lg-build-tmpl.md +17 -0
  326. package/template/pods/automation/templates/lg-design-tmpl.md +23 -0
  327. package/template/pods/automation/templates/lg-discover-tmpl.md +20 -0
  328. package/template/pods/automation/templates/lg-handoff-tmpl.md +20 -0
  329. package/template/pods/automation/templates/lg-test-tmpl.md +20 -0
  330. package/template/pods/automation/templates/n8n-build-tmpl.md +14 -0
  331. package/template/pods/automation/templates/n8n-design-tmpl.md +23 -0
  332. package/template/pods/automation/templates/n8n-discover-tmpl.md +20 -0
  333. package/template/pods/automation/templates/n8n-handoff-tmpl.md +20 -0
  334. package/template/pods/automation/templates/n8n-test-tmpl.md +20 -0
  335. package/template/pods/automation/workflows/wf-langgraph-build.smoke.json +11 -0
  336. package/template/pods/automation/workflows/wf-langgraph-build.yaml +64 -0
  337. package/template/pods/automation/workflows/wf-n8n-build.smoke.json +11 -0
  338. package/template/pods/automation/workflows/wf-n8n-build.yaml +65 -0
  339. package/template/pods/content-carousel/README.md +91 -0
  340. package/template/pods/content-carousel/agents/carousel-builder.md +28 -0
  341. package/template/pods/content-carousel/agents/carousel-visual.md +24 -0
  342. package/template/pods/content-carousel/agents/hn-curator.md +74 -0
  343. package/template/pods/content-carousel/config/heuristic-watcher-rubric.yaml +59 -0
  344. package/template/pods/content-carousel/knowledge/aquisicao-ancora.md +443 -0
  345. package/template/pods/content-carousel/knowledge/cta-brief.md +25 -0
  346. package/template/pods/content-carousel/knowledge/dono.example.yaml +45 -0
  347. package/template/pods/content-carousel/knowledge/identidade-visual-mascote.md +62 -0
  348. package/template/pods/content-carousel/knowledge/image-generation.md +62 -0
  349. package/template/pods/content-carousel/knowledge/selecao-de-ancora.md +60 -0
  350. package/template/pods/content-carousel/knowledge/slides-html.md +56 -0
  351. package/template/pods/content-carousel/memory/README.md +18 -0
  352. package/template/pods/content-carousel/memory/learnings.consolidated.md +6 -0
  353. package/template/pods/content-carousel/memory/learnings.jsonl +0 -0
  354. package/template/pods/content-carousel/pod-spec.json +100 -0
  355. package/template/pods/content-carousel/squad-io.yaml +10 -0
  356. package/template/pods/content-carousel/templates/assembly-tmpl.md +23 -0
  357. package/template/pods/content-carousel/templates/digest-tmpl.md +29 -0
  358. package/template/pods/content-carousel/templates/visuals-tmpl.md +20 -0
  359. package/template/pods/content-carousel/workflows/wf-content-carousel.smoke.json +23 -0
  360. package/template/pods/content-carousel/workflows/wf-content-carousel.yaml +80 -0
  361. package/template/pods/dev-ia/README.md +45 -0
  362. package/template/pods/dev-ia/agents/db-architect.md +33 -0
  363. package/template/pods/dev-ia/agents/ux-architect.md +29 -0
  364. package/template/pods/dev-ia/knowledge/db/migration-protocol.md +95 -0
  365. package/template/pods/dev-ia/knowledge/db/rls-patterns.md +101 -0
  366. package/template/pods/dev-ia/knowledge/db/schema-review-checklist.md +76 -0
  367. package/template/pods/dev-ia/memory/README.md +16 -0
  368. package/template/pods/dev-ia/memory/learnings.consolidated.md +8 -0
  369. package/template/pods/dev-ia/memory/learnings.jsonl +0 -0
  370. package/template/pods/dev-ia/pod-spec.json +34 -0
  371. package/template/pods/dev-ia/templates/schema-review-tmpl.md +23 -0
  372. package/template/pods/dev-ia/templates/ux-flow-review-tmpl.md +23 -0
  373. package/template/pods/dev-ia/tools/db-ops/backup-manager.sh +321 -0
  374. package/template/pods/dev-ia/tools/db-ops/best-practices-auditor.sh +534 -0
  375. package/template/pods/dev-ia/tools/db-ops/bootstrap-runner.sh +255 -0
  376. package/template/pods/dev-ia/tools/db-ops/data-loader.sh +616 -0
  377. package/template/pods/dev-ia/tools/db-ops/detect-database.sh +113 -0
  378. package/template/pods/dev-ia/tools/db-ops/explain-analyzer.sh +285 -0
  379. package/template/pods/dev-ia/tools/db-ops/health-checker.sh +323 -0
  380. package/template/pods/dev-ia/tools/db-ops/migration-runner.sh +308 -0
  381. package/template/pods/dev-ia/tools/db-ops/postgresql-schema-loader.sh +334 -0
  382. package/template/pods/dev-ia/tools/db-ops/query-runner.sh +171 -0
  383. package/template/pods/dev-ia/tools/db-ops/rls-policy-installer.sh +480 -0
  384. package/template/pods/dev-ia/tools/db-ops/rls-test-suite.sh +172 -0
  385. package/template/pods/dev-ia/tools/db-ops/schema-context-loader.sh +176 -0
  386. package/template/pods/hormozi-method/README.md +52 -0
  387. package/template/pods/hormozi-method/agents/acquisition-strategist.md +33 -0
  388. package/template/pods/hormozi-method/agents/conversion-copywriter.md +34 -0
  389. package/template/pods/hormozi-method/agents/monetization-architect.md +35 -0
  390. package/template/pods/hormozi-method/agents/offer-engineer.md +37 -0
  391. package/template/pods/hormozi-method/knowledge/core-four-leads.md +51 -0
  392. package/template/pods/hormozi-method/knowledge/grand-slam-offer.md +66 -0
  393. package/template/pods/hormozi-method/knowledge/growth-decision.md +38 -0
  394. package/template/pods/hormozi-method/knowledge/hooks-and-ads.md +56 -0
  395. package/template/pods/hormozi-method/knowledge/launch-email.md +31 -0
  396. package/template/pods/hormozi-method/knowledge/money-model.md +46 -0
  397. package/template/pods/hormozi-method/knowledge/opportunity-screening.md +44 -0
  398. package/template/pods/hormozi-method/knowledge/pages.md +25 -0
  399. package/template/pods/hormozi-method/knowledge/pos-lancamento-leads.md +56 -0
  400. package/template/pods/hormozi-method/knowledge/pricing.md +36 -0
  401. package/template/pods/hormozi-method/knowledge/retention-ltv.md +37 -0
  402. package/template/pods/hormozi-method/knowledge/scale-roadmap.md +35 -0
  403. package/template/pods/hormozi-method/knowledge/swipefiles-curados.md +44 -0
  404. package/template/pods/hormozi-method/knowledge/value-equation-diagnostic.md +77 -0
  405. package/template/pods/hormozi-method/memory/README.md +16 -0
  406. package/template/pods/hormozi-method/memory/learnings.consolidated.md +6 -0
  407. package/template/pods/hormozi-method/memory/learnings.jsonl +0 -0
  408. package/template/pods/hormozi-method/pod-spec.json +50 -0
  409. package/template/pods/hormozi-method/specs/wf-full-launch-sequence.spec.json +149 -0
  410. package/template/pods/hormozi-method/specs/wf-lead-magnet-pipeline.spec.json +125 -0
  411. package/template/pods/hormozi-method/specs/wf-money-model-design.spec.json +117 -0
  412. package/template/pods/hormozi-method/specs/wf-paid-ads-campaign.spec.json +132 -0
  413. package/template/pods/hormozi-method/specs/wf-scaling-diagnostic.spec.json +121 -0
  414. package/template/pods/hormozi-method/squad-io.yaml +43 -0
  415. package/template/pods/hormozi-method/templates/conversion-copywriter-tmpl.md +20 -0
  416. package/template/pods/hormozi-method/templates/launch-execucao-tmpl.md +17 -0
  417. package/template/pods/hormozi-method/templates/launch-fechamento-tmpl.md +17 -0
  418. package/template/pods/hormozi-method/templates/launch-pos-tmpl.md +17 -0
  419. package/template/pods/hormozi-method/templates/launch-preparacao-tmpl.md +17 -0
  420. package/template/pods/hormozi-method/templates/lm-copy-tmpl.md +17 -0
  421. package/template/pods/hormozi-method/templates/lm-distribuicao-tmpl.md +18 -0
  422. package/template/pods/hormozi-method/templates/lm-estrategia-tmpl.md +17 -0
  423. package/template/pods/hormozi-method/templates/mm-arquitetura-tmpl.md +18 -0
  424. package/template/pods/hormozi-method/templates/mm-copy-tmpl.md +17 -0
  425. package/template/pods/hormozi-method/templates/mm-diagnostico-tmpl.md +14 -0
  426. package/template/pods/hormozi-method/templates/monetization-architect-tmpl.md +17 -0
  427. package/template/pods/hormozi-method/templates/offer-engineer-tmpl.md +20 -0
  428. package/template/pods/hormozi-method/templates/paid-criativos-tmpl.md +19 -0
  429. package/template/pods/hormozi-method/templates/paid-fundacao-tmpl.md +17 -0
  430. package/template/pods/hormozi-method/templates/paid-teste-escala-tmpl.md +17 -0
  431. package/template/pods/hormozi-method/templates/scaling-diagnostico-tmpl.md +17 -0
  432. package/template/pods/hormozi-method/templates/scaling-priorizacao-tmpl.md +17 -0
  433. package/template/pods/hormozi-method/templates/scaling-roadmap-tmpl.md +17 -0
  434. package/template/pods/hormozi-method/workflows/wf-full-launch-sequence.smoke.json +12 -0
  435. package/template/pods/hormozi-method/workflows/wf-full-launch-sequence.yaml +56 -0
  436. package/template/pods/hormozi-method/workflows/wf-grand-slam-offer.smoke.json +11 -0
  437. package/template/pods/hormozi-method/workflows/wf-grand-slam-offer.yaml +45 -0
  438. package/template/pods/hormozi-method/workflows/wf-lead-magnet-pipeline.smoke.json +9 -0
  439. package/template/pods/hormozi-method/workflows/wf-lead-magnet-pipeline.yaml +45 -0
  440. package/template/pods/hormozi-method/workflows/wf-money-model-design.smoke.json +11 -0
  441. package/template/pods/hormozi-method/workflows/wf-money-model-design.yaml +45 -0
  442. package/template/pods/hormozi-method/workflows/wf-paid-ads-campaign.smoke.json +9 -0
  443. package/template/pods/hormozi-method/workflows/wf-paid-ads-campaign.yaml +46 -0
  444. package/template/pods/hormozi-method/workflows/wf-scaling-diagnostic.smoke.json +11 -0
  445. package/template/pods/hormozi-method/workflows/wf-scaling-diagnostic.yaml +44 -0
  446. package/template/pods/squad-forge/README.md +53 -0
  447. package/template/pods/squad-forge/agents/forge-chief.md +31 -0
  448. package/template/pods/squad-forge/agents/mind-extractor.md +29 -0
  449. package/template/pods/squad-forge/agents/pod-critic.md +26 -0
  450. package/template/pods/squad-forge/config/heuristic-watcher-rubric.yaml +74 -0
  451. package/template/pods/squad-forge/knowledge/fidelity-rubric.yaml +176 -0
  452. package/template/pods/squad-forge/knowledge/g6-memory-check.md +17 -0
  453. package/template/pods/squad-forge/knowledge/memory-ingest.md +16 -0
  454. package/template/pods/squad-forge/knowledge/source-audit-rubric.yaml +60 -0
  455. package/template/pods/squad-forge/memory/README.md +16 -0
  456. package/template/pods/squad-forge/memory/learnings.consolidated.md +8 -0
  457. package/template/pods/squad-forge/memory/learnings.jsonl +0 -0
  458. package/template/pods/squad-forge/pod-spec.json +166 -0
  459. package/template/pods/squad-forge/squad-io.yaml +13 -0
  460. package/template/pods/squad-forge/tasks/pipeline.md +158 -0
  461. package/template/pods/squad-forge/templates/discovery-tmpl.md +40 -0
  462. package/template/pods/squad-forge/templates/generation-tmpl.md +18 -0
  463. package/template/pods/squad-forge/templates/red-team-tmpl.md +17 -0
  464. package/template/pods/squad-forge/templates/register-tmpl.md +17 -0
  465. package/template/pods/squad-forge/templates/workflow-tmpl.yaml +101 -0
  466. package/template/pods/squad-forge/workflows/wf-forge.smoke.json +12 -0
  467. package/template/pods/squad-forge/workflows/wf-forge.yaml +60 -0
@@ -0,0 +1,805 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /**
4
+ * pod-export-ui.cjs — UI WEB DE CHECKPOINT do épico pod-export (F3, Story SF10.5).
5
+ *
6
+ * PROCESSO PRÓPRIO, SEPARADO do serviço-irmão (`pod-export-service.cjs`, que continua SEM porta —
7
+ * DF2-5 intacto). Servidor HTTP zero-dep (`node:http`, NÃO Next.js/Express — ADR-018 DF3-2: menor
8
+ * superfície de ataque + doutrina zero-dep do motor/serviço). Bind SÓ em `127.0.0.1` (NUNCA
9
+ * `0.0.0.0`); a exposição via nginx TLS é ação de operador (ver docs/mecanismos/pod-export-ui.md).
10
+ *
11
+ * A UI é o "mais um escritor na fila" que DF2-5 previu: REUSA por IMPORT de pod-export-service.cjs
12
+ * (`loadConfig`, `loadIdentities`, `listPending`/leitura de `awaiting/`, `approveRun`, `enqueue`,
13
+ * `verifyApproverSecret`) — NUNCA reimplementa fila/estado/enforcement. O gate real de "só Daniel
14
+ * aprova" vive no BACKEND (`approveRun` de SF10.4): a UI apenas coleta o segredo e o repassa; chamar
15
+ * o backend direto sem/segredo-errado é RECUSADO independentemente da UI.
16
+ *
17
+ * TRÊS CAMADAS DISTINTAS (DF3-3), nunca confundidas:
18
+ * 1. LOGIN (ACESSO) — credencial ÚNICA de equipe (scrypt hash em env + cookie HMAC-assinado
19
+ * SameSite=Strict). Prova só que a pessoa está no time; NÃO diz quem é.
20
+ * 2. SELETOR (ATRIBUIÇÃO) — operador escolhido vira `started_by`. Seleção honesta, barreira FRACA
21
+ * por design; qualquer um seleciona qualquer nome (DF3-3). NÃO é auth.
22
+ * 3. SEGREDO (AUTORIZAÇÃO) — o 4º fator que só o aprovador detém, digitado NO MOMENTO de aprovar,
23
+ * verificado server-side por `approveRun` (scrypt+timingSafeEqual). O gate.
24
+ *
25
+ * SEGURANÇA (o /security-review ataca isto):
26
+ * - Preview XSS: as 7 peças são HTML gerado por LLM (não confiável). Renderizadas em
27
+ * `<iframe sandbox>` (sem allow-same-origin + allow-scripts) servidas por /preview com
28
+ * `Content-Security-Policy: sandbox; script-src 'none'` — origem opaca, sem script, sem acesso
29
+ * ao cookie/sessão da UI. NUNCA `srcdoc`+scripts nem same-origin.
30
+ * - CSRF: cookie SameSite=Strict + verificação de Origin/Referer nas ações de estado + token CSRF
31
+ * por sessão (HMAC do sid).
32
+ * - Sessão: cookie HMAC-SHA256 assinado (não-forjável sem PODEXPORT_UI_SESSION_KEY), HttpOnly,
33
+ * Secure (atrás de TLS), expiração. timingSafeEqual em toda verificação.
34
+ * - Nenhum segredo/hash é logado ou ecoado; erros de aprovação são genéricos (sem oracle).
35
+ * - Rate-limit em memória (ADR-004) em login e approve.
36
+ *
37
+ * CONFIG por ENV (instância na VPS, fora do commit):
38
+ * PODEXPORT_QUEUE_DIR / PODEXPORT_RUNS_DIR / PODEXPORT_BUNDLE_DIR / PODEXPORT_IDENTITIES — de SF10.3/4.
39
+ * PODEXPORT_APPROVER_SECRET_HASH — hash do segredo do aprovador (o CLARO nunca reside na UI).
40
+ * PODEXPORT_UI_TEAM_HASH (obrigatório) — hash scrypt da senha da equipe (gere com `hash-team`).
41
+ * PODEXPORT_UI_SESSION_KEY (obrigatório) — chave HMAC de assinatura do cookie de sessão (>=32 ch).
42
+ * PODEXPORT_UI_PORT (default 4600) — porta local (bind 127.0.0.1).
43
+ * PODEXPORT_UI_SESSION_TTL_MS (default 8h) — validade da sessão.
44
+ * PODEXPORT_UI_INSECURE=1 — desliga o flag Secure do cookie (SÓ para dev local sem TLS).
45
+ *
46
+ * Node >= 18, zero dep externa. Relógio/rede/approve/identidades INJETÁVEIS → suíte CI hermética.
47
+ */
48
+
49
+ const http = require('node:http');
50
+ const fs = require('node:fs');
51
+ const path = require('node:path');
52
+ const crypto = require('node:crypto');
53
+
54
+ const svc = require('./pod-export-service.cjs');
55
+ const motor = require('./pc-run-pod.cjs');
56
+
57
+ const COOKIE_NAME = 'pex_ui';
58
+ const DEFAULT_PORT = 4600;
59
+ const DEFAULT_TTL_MS = 8 * 60 * 60 * 1000;
60
+ const BODY_LIMIT = 64 * 1024;
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Config (reusa svc.loadConfig para os campos compartilhados; adiciona os da UI).
64
+ // ---------------------------------------------------------------------------
65
+
66
+ function loadUiConfig(env = process.env, overrides = {}) {
67
+ const base = svc.loadConfig(env, overrides);
68
+ return {
69
+ ...base,
70
+ host: '127.0.0.1', // FORÇADO — nunca 0.0.0.0 (DF3-2). A exposição TLS é do nginx do operador.
71
+ port: Number(overrides.port || env.PODEXPORT_UI_PORT || DEFAULT_PORT),
72
+ teamHash: overrides.teamHash || env.PODEXPORT_UI_TEAM_HASH || null,
73
+ sessionKey: overrides.sessionKey || env.PODEXPORT_UI_SESSION_KEY || null,
74
+ approverSecretHash: overrides.approverSecretHash || env.PODEXPORT_APPROVER_SECRET_HASH || null,
75
+ secure: (overrides.secure != null) ? overrides.secure : (env.PODEXPORT_UI_INSECURE === '1' ? false : true),
76
+ sessionTtlMs: Number(overrides.sessionTtlMs || env.PODEXPORT_UI_SESSION_TTL_MS || DEFAULT_TTL_MS),
77
+ };
78
+ }
79
+
80
+ /** Fail-loud: a UI RECUSA subir sem os segredos essenciais (nunca sobe verde-falso, ADR-015). */
81
+ function assertUiConfig(cfg) {
82
+ if (!cfg.queueDir) throw new Error('[FAIL-LOUD] PODEXPORT_QUEUE_DIR ausente — a UI lê a fila (awaiting/). Defina no env.');
83
+ if (!cfg.teamHash || String(cfg.teamHash).trim() === '') {
84
+ throw new Error('[FAIL-LOUD] PODEXPORT_UI_TEAM_HASH ausente — o login compartilhado exige o hash da senha da equipe (gere com `node scripts/pod-export-ui.cjs hash-team`). Sem ele, sem acesso.');
85
+ }
86
+ if (!cfg.sessionKey || String(cfg.sessionKey).length < 32) {
87
+ throw new Error('[FAIL-LOUD] PODEXPORT_UI_SESSION_KEY ausente ou curta (>=32 ch) — a sessão assinada exige uma chave HMAC forte. Gere com `openssl rand -hex 32`.');
88
+ }
89
+ if (!cfg.approverSecretHash || String(cfg.approverSecretHash).trim() === '') {
90
+ throw new Error('[FAIL-LOUD] PODEXPORT_APPROVER_SECRET_HASH ausente — o gate de aprovação (SF10.4) exige o hash do segredo do aprovador. Sem ele, nada é aprovável.');
91
+ }
92
+ return true;
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Cripto de sessão/CSRF (zero-dep, node:crypto). HMAC-SHA256 + timingSafeEqual.
97
+ // ---------------------------------------------------------------------------
98
+
99
+ function b64url(buf) {
100
+ return Buffer.from(buf).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
101
+ }
102
+ function b64urlDecode(s) {
103
+ return Buffer.from(String(s).replace(/-/g, '+').replace(/_/g, '/'), 'base64');
104
+ }
105
+ function hmac(key, data) {
106
+ return crypto.createHmac('sha256', String(key)).update(data).digest();
107
+ }
108
+ function timingEq(a, b) {
109
+ const ba = Buffer.from(String(a));
110
+ const bb = Buffer.from(String(b));
111
+ if (ba.length !== bb.length) return false;
112
+ return crypto.timingSafeEqual(ba, bb);
113
+ }
114
+
115
+ /** Assina um payload de sessão como `<body>.<sig>` (HMAC). Corpo é JSON base64url. */
116
+ function signSession(payload, key) {
117
+ const body = b64url(JSON.stringify(payload));
118
+ const sig = b64url(hmac(key, body));
119
+ return `${body}.${sig}`;
120
+ }
121
+
122
+ /** Verifica e decodifica um token de sessão. Retorna o payload ou null (assinatura/expiração inválida). */
123
+ function verifySession(token, key, now = Date.now()) {
124
+ if (typeof token !== 'string' || token.indexOf('.') < 0) return null;
125
+ const idx = token.lastIndexOf('.');
126
+ const body = token.slice(0, idx);
127
+ const sig = token.slice(idx + 1);
128
+ const expected = b64url(hmac(key, body));
129
+ if (!timingEq(sig, expected)) return null; // não-forjável sem a chave.
130
+ let payload;
131
+ try { payload = JSON.parse(b64urlDecode(body).toString('utf8')); } catch { return null; }
132
+ if (!payload || typeof payload !== 'object') return null;
133
+ if (typeof payload.exp === 'number' && now > payload.exp) return null; // sessão expirada.
134
+ return payload;
135
+ }
136
+
137
+ /** Token CSRF derivado do sid da sessão (estável enquanto a sessão vive). */
138
+ function csrfToken(sid, key) { return b64url(hmac(key, `csrf:${sid}`)); }
139
+ function verifyCsrf(token, sid, key) { return typeof token === 'string' && timingEq(token, csrfToken(sid, key)); }
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // Rate-limit em memória (ADR-004). Janela deslizante por chave (IP no login, sid no approve).
143
+ // ---------------------------------------------------------------------------
144
+
145
+ function createRateLimiter(max, windowMs) {
146
+ const hits = new Map();
147
+ return {
148
+ check(key, now = Date.now()) {
149
+ const arr = (hits.get(key) || []).filter((t) => now - t < windowMs);
150
+ if (arr.length >= max) { hits.set(key, arr); return { allowed: false, retryAfterMs: windowMs - (now - arr[0]) }; }
151
+ arr.push(now); hits.set(key, arr);
152
+ return { allowed: true };
153
+ },
154
+ reset(key) { hits.delete(key); },
155
+ };
156
+ }
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // HTML escaping — toda interpolação de dado externo passa por aqui (anti-XSS na própria UI).
160
+ // ---------------------------------------------------------------------------
161
+
162
+ function escapeHtml(s) {
163
+ return String(s == null ? '' : s)
164
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
165
+ .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
166
+ }
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // Leitura da fila/runs (SEMPRE do disco — DF2-5, nunca um segundo motor de estado).
170
+ // ---------------------------------------------------------------------------
171
+
172
+ // SF10.9/AC5 — a regra do runId passou a viver no BACKEND (`pod-export-service.cjs`), que agora
173
+ // também a impõe em `approveRun` (antes o valor chegava cru do form até o spawn). A UI REUSA o
174
+ // mesmo predicado: uma única fonte, sem duas cópias que podem divergir. Export mantido (callers/tests).
175
+ const safeRunId = svc.safeRunId;
176
+
177
+ /** Lista os runs pausados em awaiting/ lendo os `<runId>.result.json` (fonte de verdade = disco). */
178
+ function listCheckpoints(cfg) {
179
+ const dir = path.join(cfg.queueDir, 'awaiting');
180
+ if (!fs.existsSync(dir)) return [];
181
+ const out = [];
182
+ for (const f of fs.readdirSync(dir).sort()) {
183
+ if (!f.endsWith('.result.json')) continue;
184
+ const runId = f.slice(0, -('.result.json'.length));
185
+ if (!safeRunId(runId)) continue;
186
+ let res = {};
187
+ try { res = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')); } catch { res = {}; }
188
+ let payload = {};
189
+ try { payload = JSON.parse(fs.readFileSync(path.join(dir, `${runId}.json`), 'utf8')); } catch { payload = {}; }
190
+ out.push({
191
+ runId,
192
+ startedBy: res.started_by || payload.started_by || null,
193
+ pod: payload.pod || null,
194
+ phase: res.awaiting || null,
195
+ at: res.at || null,
196
+ });
197
+ }
198
+ return out;
199
+ }
200
+
201
+ /** runDir real de um run (via motor.findStateByRunId — o mesmo do executor). null se não existe. */
202
+ function runDirFor(cfg, runId, deps = {}) {
203
+ if (!safeRunId(runId)) return null;
204
+ const findState = deps.findStateByRunId || motor.findStateByRunId;
205
+ const sp = findState(cfg.runsRoot, runId);
206
+ return sp ? path.dirname(sp) : null;
207
+ }
208
+
209
+ /** Lista os arquivos HTML (as peças) sob o runDir, confinado, excluindo artifacts/. Rel paths. */
210
+ function listPieceFiles(runDir) {
211
+ const out = [];
212
+ const walk = (dir, rel) => {
213
+ let entries = [];
214
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
215
+ for (const e of entries) {
216
+ if (e.name.startsWith('.')) continue;
217
+ const abs = path.join(dir, e.name);
218
+ const r = rel ? `${rel}/${e.name}` : e.name;
219
+ if (e.isDirectory()) { if (e.name === 'artifacts') continue; walk(abs, r); }
220
+ else if (/\.html?$/i.test(e.name)) out.push(r);
221
+ }
222
+ };
223
+ walk(runDir, '');
224
+ return out.sort();
225
+ }
226
+
227
+ /** Critério do checkpoint (best-effort): lê workflow.yaml do bundle e extrai `criteria` da fase. */
228
+ function checkpointCriterion(cfg, phase) {
229
+ const fallback = 'Julgamento humano: avalie as peças geradas antes de aprovar.';
230
+ if (!cfg.bundleDir || !phase) return fallback;
231
+ const wf = path.join(cfg.bundleDir, 'workflow.yaml');
232
+ let text;
233
+ try { text = fs.readFileSync(wf, 'utf8'); } catch { return fallback; }
234
+ // Procura o bloco do step `phase` e um `criteria: "..."` até o próximo `- id:`.
235
+ const idIdx = text.indexOf(`id: ${phase}`);
236
+ if (idIdx < 0) return fallback;
237
+ const rest = text.slice(idIdx);
238
+ const nextStep = rest.indexOf('\n - id:');
239
+ const block = nextStep > 0 ? rest.slice(0, nextStep) : rest;
240
+ const m = block.match(/criteria:\s*["']?([^"'\n]+)["']?/);
241
+ return m ? m[1].trim() : fallback;
242
+ }
243
+
244
+ /**
245
+ * Confina um relPath ao runDir; retorna abs ou null se traversal/fora.
246
+ *
247
+ * SF10.9/AC4 — `decodeURIComponent` é parse de entrada NÃO-CONFIÁVEL: `%zz` lança `URIError`. Sob
248
+ * try/catch (mesmo padrão do `new URL` do router), entrada malformada vira `null` → 400, nunca uma
249
+ * exceção não capturada no handler.
250
+ *
251
+ * SF10.9/AC3 — containment por REALPATH (mesmo padrão do Guard B de `command_check`,
252
+ * `pc-run-pod.cjs`): o guard lexical (`path.resolve`) não enxerga symlink CRIADO DENTRO do runDir
253
+ * apontando para fora. Quando o alvo (ou o ancestral existente mais próximo) resolve em disco, o
254
+ * realpath tem que permanecer sob o realpath do runDir — senão RECUSA. Aditivo: o retorno do
255
+ * caminho feliz continua sendo o `abs` lexical (o caller lê/serve exatamente o mesmo arquivo).
256
+ */
257
+ function confineToRunDir(runDir, relPath) {
258
+ if (typeof relPath !== 'string' || relPath === '') return null;
259
+ let decoded;
260
+ try { decoded = decodeURIComponent(relPath); } catch { return null; } // %zz e cia → recusa, não URIError.
261
+ decoded = decoded.replace(/^\/+/, '');
262
+ if (decoded.includes('\0')) return null;
263
+ const root = path.resolve(runDir);
264
+ const abs = path.resolve(runDir, decoded);
265
+ if (abs !== root && !abs.startsWith(root + path.sep)) return null;
266
+ // Guard realpath (defesa em profundidade). Se nem o runDir resolve em disco, não há o que servir:
267
+ // devolve o abs lexical (o caller ainda confere existsSync/isFile e responde 400).
268
+ let realRoot;
269
+ try { realRoot = fs.realpathSync(root); } catch { return abs; }
270
+ let probe = abs;
271
+ for (;;) {
272
+ let real;
273
+ try { real = fs.realpathSync(probe); } catch {
274
+ const parent = path.dirname(probe);
275
+ if (parent === probe || parent.length < root.length) return abs; // nada existe ainda sob o runDir.
276
+ probe = parent;
277
+ continue;
278
+ }
279
+ return (real === realRoot || real.startsWith(realRoot + path.sep)) ? abs : null;
280
+ }
281
+ }
282
+
283
+ const CONTENT_TYPES = {
284
+ '.html': 'text/html; charset=utf-8', '.htm': 'text/html; charset=utf-8',
285
+ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
286
+ '.webp': 'image/webp', '.svg': 'image/svg+xml', '.css': 'text/css; charset=utf-8',
287
+ };
288
+
289
+ // ---------------------------------------------------------------------------
290
+ // Reject: ação de QUEUE-WRITER (DF2-5) — parkea o run em blocked/ com o motivo. NÃO reimplementa
291
+ // approve nem toca o motor/executor. Dívida declarada (F4/F5): re-rodar a fase via `revise` do motor
292
+ // não é wired em v1; reject = triagem de qualidade que remove o run de awaiting/ com motivo registrado.
293
+ // ---------------------------------------------------------------------------
294
+
295
+ function rejectRun(cfg, runId, { reason, rejectedBy } = {}) {
296
+ if (!safeRunId(runId)) return { ok: false, reason: 'runId inválido' };
297
+ const dirs = svc.ensureQueue(cfg.queueDir);
298
+ const src = path.join(dirs.awaiting, `${runId}.json`);
299
+ // SF10.16/AC1 (DF-SF10.16-2.1) — REIVINDICAR ANTES DE AGIR: o marcador vai para blocked/ ANTES de qualquer
300
+ // escrita de motivo. O `existsSync`+`renameSync` anterior era TOCTOU cross-processo: com dois
301
+ // cliques em `reject` (ou reject × approve), o perdedor lançava `ENOENT` DENTRO do handler async do
302
+ // node:http → unhandled rejection → **o processo da UI inteiro MORRIA** (não há
303
+ // process.on('uncaughtException') aqui). Agora o rename que sucede é a autorização; perder a
304
+ // disputa devolve `gone`, que o router já traduz em `flash=gone` ("já tratado por outra pessoa").
305
+ if (!svc.claimByRename(src, path.join(dirs.blocked, `${runId}.json`))) {
306
+ return { ok: false, gone: true, reason: 'run não está em awaiting/ (já tratado?)' };
307
+ }
308
+ // Daqui em diante somos o dono EXCLUSIVO do item (ninguém mais pode reivindicá-lo).
309
+ const srcRes = path.join(dirs.awaiting, `${runId}.result.json`);
310
+ try { fs.unlinkSync(srcRes); }
311
+ catch (e) {
312
+ if (!e || e.code !== 'ENOENT') {
313
+ // Não é catch-all: ENOENT é o caso normal (nem todo marcador tem companion). Outro errno é
314
+ // ANOMALIA e vai LOGADO ALTO — mas não relançado: a transição de estado que importa (o rename)
315
+ // já ocorreu, e derrubar a UI aqui recriaria o defeito que esta story fecha (DF-SF10.16-3: nada pode
316
+ // tornar a superfície de aprovação indisponível).
317
+ console.error(`[reject][AVISO ALTO] ${runId}: falha ao remover ${srcRes} (${e.code || e.message}) — o run JÁ está em blocked/; investigue permissões da fila.`);
318
+ }
319
+ }
320
+ fs.writeFileSync(path.join(dirs.blocked, `${runId}.result.json`), JSON.stringify({
321
+ runId, kind: 'blocked', ok: false, rejected: true,
322
+ rejected_by: rejectedBy || null, reason: String(reason || '').slice(0, 2000),
323
+ at: new Date().toISOString(),
324
+ }, null, 2));
325
+ return { ok: true, kind: 'blocked' };
326
+ }
327
+
328
+ // ---------------------------------------------------------------------------
329
+ // HTTP helpers.
330
+ // ---------------------------------------------------------------------------
331
+
332
+ function parseCookies(header) {
333
+ const out = {};
334
+ if (typeof header !== 'string') return out;
335
+ for (const part of header.split(';')) {
336
+ const i = part.indexOf('=');
337
+ if (i < 0) continue;
338
+ out[part.slice(0, i).trim()] = part.slice(i + 1).trim();
339
+ }
340
+ return out;
341
+ }
342
+
343
+ function readBody(req, limit = BODY_LIMIT) {
344
+ return new Promise((resolve, reject) => {
345
+ let data = ''; let size = 0;
346
+ req.on('data', (c) => {
347
+ size += c.length;
348
+ if (size > limit) { reject(new Error('payload muito grande')); req.destroy(); return; }
349
+ data += c;
350
+ });
351
+ req.on('end', () => resolve(data));
352
+ req.on('error', reject);
353
+ });
354
+ }
355
+
356
+ function parseForm(body) {
357
+ const out = {};
358
+ const p = new URLSearchParams(body);
359
+ for (const [k, v] of p) out[k] = v;
360
+ return out;
361
+ }
362
+
363
+ function clientIp(req) {
364
+ const xff = req.headers['x-forwarded-for'];
365
+ if (typeof xff === 'string' && xff.trim()) return xff.split(',')[0].trim();
366
+ return (req.socket && req.socket.remoteAddress) || 'unknown';
367
+ }
368
+
369
+ /** Origin/Referer host deve casar com o Host (defesa em profundidade além do SameSite=Strict). */
370
+ function sameOrigin(req) {
371
+ const host = req.headers['x-forwarded-host'] || req.headers.host;
372
+ const origin = req.headers.origin;
373
+ if (origin) { try { return new URL(origin).host === host; } catch { return false; } }
374
+ const ref = req.headers.referer;
375
+ if (ref) { try { return new URL(ref).host === host; } catch { return false; } }
376
+ return false; // sem Origin/Referer numa ação de estado → recusa.
377
+ }
378
+
379
+ function baseHeaders(extra = {}) {
380
+ return {
381
+ 'Content-Type': 'text/html; charset=utf-8',
382
+ 'X-Content-Type-Options': 'nosniff',
383
+ 'X-Frame-Options': 'DENY',
384
+ 'Referrer-Policy': 'no-referrer',
385
+ // A própria UI: sem script inline algum (default-src 'none'), estilos inline permitidos, imagens
386
+ // próprias/data, frames só de 'self' (as peças via /preview, que por sua vez são sandbox-CSP).
387
+ 'Content-Security-Policy':
388
+ "default-src 'none'; style-src 'unsafe-inline'; img-src 'self' data:; frame-src 'self'; " +
389
+ "form-action 'self'; base-uri 'none'; frame-ancestors 'none'",
390
+ ...extra,
391
+ };
392
+ }
393
+
394
+ function setSessionCookie(res, token, cfg) {
395
+ const parts = [`${COOKIE_NAME}=${token}`, 'HttpOnly', 'SameSite=Strict', 'Path=/', `Max-Age=${Math.floor(cfg.sessionTtlMs / 1000)}`];
396
+ if (cfg.secure) parts.push('Secure');
397
+ res.setHeader('Set-Cookie', parts.join('; '));
398
+ }
399
+ function clearSessionCookie(res, cfg) {
400
+ const parts = [`${COOKIE_NAME}=`, 'HttpOnly', 'SameSite=Strict', 'Path=/', 'Max-Age=0'];
401
+ if (cfg.secure) parts.push('Secure');
402
+ res.setHeader('Set-Cookie', parts.join('; '));
403
+ }
404
+
405
+ function send(res, code, headers, body) {
406
+ res.writeHead(code, headers);
407
+ res.end(body);
408
+ }
409
+ function redirect(res, location, cfg, extraHeaders = {}) {
410
+ send(res, 303, { Location: location, 'Content-Type': 'text/plain; charset=utf-8', ...extraHeaders }, 'redirecionando...');
411
+ }
412
+
413
+ // ---------------------------------------------------------------------------
414
+ // Views (strings HTML). Toda interpolação externa passa por escapeHtml.
415
+ // ---------------------------------------------------------------------------
416
+
417
+ const STYLE = `
418
+ :root{--fg:#1a1a1a;--muted:#666;--bg:#fafafa;--card:#fff;--line:#ddd;--danger:#b00020;--ok:#1b7a3d;--accent:#2a5db0}
419
+ *{box-sizing:border-box} body{font:15px/1.5 system-ui,sans-serif;color:var(--fg);background:var(--bg);margin:0;padding:24px}
420
+ .wrap{max-width:920px;margin:0 auto} h1{font-size:20px;margin:0 0 4px} .muted{color:var(--muted);font-size:13px}
421
+ .card{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:20px;margin:16px 0}
422
+ label{display:block;font-weight:600;margin:12px 0 4px} input[type=password],input[type=text],textarea{width:100%;padding:10px;border:1px solid var(--line);border-radius:6px;font:inherit}
423
+ button{cursor:pointer;font:inherit;padding:10px 18px;border-radius:6px;border:1px solid var(--line);background:#fff}
424
+ button:focus,a:focus,input:focus,[tabindex]:focus{outline:2px solid var(--accent);outline-offset:2px}
425
+ .primary{background:var(--accent);color:#fff;border-color:var(--accent)} .danger{color:var(--danger);border-color:var(--danger)}
426
+ .err{color:var(--danger);font-weight:600;margin:8px 0} .ok{color:var(--ok);font-weight:600;margin:8px 0}
427
+ table{width:100%;border-collapse:collapse} th,td{text-align:left;padding:8px;border-bottom:1px solid var(--line)}
428
+ .bar{display:flex;justify-content:space-between;align-items:center;gap:12px}
429
+ .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px}
430
+ .piece{border:1px solid var(--line);border-radius:6px;overflow:hidden;background:#fff}
431
+ .piece iframe{width:100%;height:260px;border:0;display:block;background:#fff}
432
+ .piece .cap{padding:6px 8px;font-size:12px;color:var(--muted);border-top:1px solid var(--line)}
433
+ .crit{background:#fff8e6;border:1px solid #f0e0b0;border-radius:6px;padding:14px;font-size:14px}
434
+ .modal{background:#fff;border:2px solid var(--accent);border-radius:8px;padding:20px;margin:16px 0}
435
+ fieldset{border:1px solid var(--line);border-radius:6px;padding:8px 14px}
436
+ `;
437
+
438
+ function layout(title, inner, opts = {}) {
439
+ return `<!doctype html><html lang="pt-BR"><head><meta charset="utf-8">` +
440
+ `<meta name="viewport" content="width=device-width,initial-scale=1">` +
441
+ `<title>${escapeHtml(title)}</title><style>${STYLE}</style></head>` +
442
+ `<body><div class="wrap">${inner}</div></body></html>`;
443
+ }
444
+
445
+ function loginView(err) {
446
+ return layout('pod-export · checkpoint', `
447
+ <div class="card">
448
+ <h1>pod-export · checkpoint</h1>
449
+ ${err ? `<p class="err">${escapeHtml(err)}</p>` : ''}
450
+ <form method="POST" action="/login">
451
+ <label for="pw">Senha da equipe</label>
452
+ <input id="pw" name="password" type="password" autocomplete="off" autofocus>
453
+ <p style="margin-top:16px"><button class="primary" type="submit">Entrar</button></p>
454
+ </form>
455
+ <p class="muted">Esta senha dá acesso à ferramenta — ela NÃO identifica quem está usando.
456
+ Na próxima tela você escolhe seu nome.</p>
457
+ </div>`);
458
+ }
459
+
460
+ function operatorView(cfg, session, err) {
461
+ let model;
462
+ try { model = svc.loadIdentities({ env: process.env, file: cfg.identitiesFile }); }
463
+ catch { return layout('Seletor', `<div class="card"><h1>Quem está usando?</h1>
464
+ <p class="err">Nenhuma identidade configurada — contate o operador.</p></div>`); }
465
+ const csrf = csrfToken(session.sid, cfg.sessionKey);
466
+ const opts = model.identities.map((it, i) => `
467
+ <label style="font-weight:400;display:flex;gap:8px;align-items:center;margin:6px 0">
468
+ <input type="radio" name="operator" value="${escapeHtml(it.id)}" ${i === 0 ? 'required' : ''}>
469
+ ${escapeHtml(it.label || it.id)}</label>`).join('');
470
+ return layout('Seletor de operador', `
471
+ <div class="card">
472
+ <h1>Quem está usando a ferramenta agora?</h1>
473
+ ${err ? `<p class="err">${escapeHtml(err)}</p>` : ''}
474
+ <form method="POST" action="/operator">
475
+ <input type="hidden" name="_csrf" value="${escapeHtml(csrf)}">
476
+ <fieldset><legend class="muted">Operador</legend>${opts}</fieldset>
477
+ <p style="margin-top:16px"><button class="primary" type="submit">Continuar</button></p>
478
+ </form>
479
+ <p class="muted">Isto registra quem iniciou/decidiu cada ação para a equipe (rastreabilidade
480
+ interna) — não é uma senha e não prova identidade. Escolha seu nome com honestidade.</p>
481
+ </div>`);
482
+ }
483
+
484
+ function listView(cfg, session, flash) {
485
+ const items = listCheckpoints(cfg);
486
+ const op = escapeHtml(session.operator);
487
+ const rows = items.length ? items.map((c) => `
488
+ <tr>
489
+ <td>${escapeHtml(c.pod || '—')}</td>
490
+ <td>${escapeHtml(c.startedBy || '—')}</td>
491
+ <td>${escapeHtml(c.phase || '—')}</td>
492
+ <td class="muted">${escapeHtml(c.at || '')}</td>
493
+ <td><a href="/checkpoint?run=${encodeURIComponent(c.runId)}">Revisar →</a></td>
494
+ </tr>`).join('') : '';
495
+ return layout('Checkpoints pendentes', `
496
+ <div class="bar"><h1>Checkpoints pendentes</h1>
497
+ <span class="muted">Operando como: <strong>${op}</strong> ·
498
+ <form method="POST" action="/operator/change" style="display:inline">
499
+ <input type="hidden" name="_csrf" value="${escapeHtml(csrfToken(session.sid, cfg.sessionKey))}">
500
+ <button type="submit" style="padding:2px 8px">Trocar</button></form></span>
501
+ </div>
502
+ ${flash === 'approved' ? '<p class="ok">✓ Aprovado. O run continua automaticamente.</p>' : ''}
503
+ ${flash === 'rejected' ? '<p class="ok">✕ Rejeitado. O run foi devolvido com o motivo.</p>' : ''}
504
+ ${flash === 'gone' ? '<p class="err">Este checkpoint já foi tratado por outra pessoa.</p>' : ''}
505
+ <div class="card">
506
+ ${items.length ? `<table><thead><tr><th>Pod</th><th>Iniciado por</th><th>Fase</th><th>Quando</th><th></th></tr></thead>
507
+ <tbody>${rows}</tbody></table>`
508
+ : '<p class="muted">Tudo revisado. Volte quando um run novo pausar em checkpoint.</p>'}
509
+ </div>`);
510
+ }
511
+
512
+ function checkpointView(cfg, session, runId, err) {
513
+ const runDir = runDirFor(cfg, runId);
514
+ const inList = listCheckpoints(cfg).find((c) => c.runId === runId);
515
+ if (!inList) return { gone: true };
516
+ const pieces = runDir ? listPieceFiles(runDir) : [];
517
+ const criterion = checkpointCriterion(cfg, inList.phase);
518
+ let model;
519
+ try { model = svc.loadIdentities({ env: process.env, file: cfg.identitiesFile }); } catch { model = null; }
520
+ const isApprover = model && model.approver && model.approver.id === session.operator;
521
+ const csrf = csrfToken(session.sid, cfg.sessionKey);
522
+ const rid = escapeHtml(runId);
523
+
524
+ const previews = pieces.length ? pieces.map((rel, i) => `
525
+ <div class="piece">
526
+ <iframe sandbox referrerpolicy="no-referrer" loading="lazy"
527
+ src="/preview/${encodeURIComponent(runId)}/${rel.split('/').map(encodeURIComponent).join('/')}"
528
+ title="Peça ${i + 1}"></iframe>
529
+ <div class="cap">${i + 1}. ${escapeHtml(rel)}</div>
530
+ </div>`).join('')
531
+ : '<p class="muted">Não foi possível pré-visualizar as peças (nenhum HTML encontrado no run).</p>';
532
+
533
+ const approveBlock = isApprover ? `
534
+ <div class="modal">
535
+ <h2 style="margin-top:0">Confirmar aprovação</h2>
536
+ <p>Selecionar "${escapeHtml(session.operator)}" não autoriza sozinho — digite o segredo do
537
+ aprovador para confirmar.</p>
538
+ <form method="POST" action="/approve">
539
+ <input type="hidden" name="_csrf" value="${escapeHtml(csrf)}">
540
+ <input type="hidden" name="run" value="${rid}">
541
+ <label for="sec">Segredo do aprovador</label>
542
+ <input id="sec" name="secret" type="password" autocomplete="off">
543
+ <p style="margin-top:16px"><button class="primary" type="submit">✓ Aprovar</button></p>
544
+ </form>
545
+ </div>`
546
+ : `<p class="muted">Aprovação restrita à identidade aprovadora designada. Peça para o aprovador
547
+ revisar, ou troque de operador se for ele.</p>`;
548
+
549
+ const inner = `
550
+ <div class="bar"><h1>Checkpoint · ${escapeHtml(inList.pod || '')} · ${rid}</h1>
551
+ <span class="muted">Operando: <strong>${escapeHtml(session.operator)}</strong></span></div>
552
+ ${err ? `<p class="err">${escapeHtml(err)}</p>` : ''}
553
+ <div class="crit"><strong>Critério deste checkpoint:</strong><br>${escapeHtml(criterion)}</div>
554
+ <div class="card"><div class="grid">${previews}</div></div>
555
+ <div class="card">
556
+ <div class="bar">
557
+ <form method="POST" action="/reject">
558
+ <input type="hidden" name="_csrf" value="${escapeHtml(csrf)}">
559
+ <input type="hidden" name="run" value="${rid}">
560
+ <label for="mot">Motivo da rejeição</label>
561
+ <textarea id="mot" name="reason" rows="2" required></textarea>
562
+ <p style="margin-top:10px"><button class="danger" type="submit">✕ Rejeitar</button></p>
563
+ </form>
564
+ </div>
565
+ ${approveBlock}
566
+ </div>
567
+ <p><a href="/">← voltar à lista</a></p>`;
568
+ return { html: layout(`Checkpoint ${runId}`, inner) };
569
+ }
570
+
571
+ // ---------------------------------------------------------------------------
572
+ // Router.
573
+ // ---------------------------------------------------------------------------
574
+
575
+ function getSession(req, cfg, now) {
576
+ const cookies = parseCookies(req.headers.cookie);
577
+ return verifySession(cookies[COOKIE_NAME], cfg.sessionKey, now);
578
+ }
579
+
580
+ function makeHandler(cfg, deps = {}) {
581
+ const now = deps.now || (() => Date.now());
582
+ const approveFn = deps.approveRun || svc.approveRun;
583
+ const loginLimiter = deps.loginLimiter || createRateLimiter(5, 60 * 1000);
584
+ const approveLimiter = deps.approveLimiter || createRateLimiter(5, 60 * 1000);
585
+
586
+ return async function handle(req, res) {
587
+ let url;
588
+ try { url = new URL(req.url, 'http://localhost'); } catch { return send(res, 400, baseHeaders(), 'bad request'); }
589
+ const pathname = url.pathname;
590
+ const method = req.method;
591
+
592
+ // Health (sem auth, mínimo).
593
+ if (method === 'GET' && pathname === '/health') {
594
+ return send(res, 200, { 'Content-Type': 'text/plain; charset=utf-8', 'X-Content-Type-Options': 'nosniff' }, 'ok');
595
+ }
596
+
597
+ // Preview das peças (sandbox-CSP; a peça é HTML não confiável — origem opaca, sem script).
598
+ if (method === 'GET' && pathname.startsWith('/preview/')) {
599
+ const session = getSession(req, cfg, now());
600
+ if (!session) return send(res, 403, { 'Content-Type': 'text/plain; charset=utf-8', 'X-Content-Type-Options': 'nosniff' }, 'sessão necessária');
601
+ const after = pathname.slice('/preview/'.length);
602
+ const slash = after.indexOf('/');
603
+ if (slash < 0) return send(res, 400, baseHeaders(), 'bad preview path');
604
+ // SF10.9/AC4 — parse de entrada não-confiável: `%zz` no segmento do runId lança `URIError`.
605
+ // Malformado → 400 (mesmo padrão do try/catch do `new URL` acima), nunca exceção não capturada.
606
+ let runId;
607
+ try { runId = decodeURIComponent(after.slice(0, slash)); }
608
+ catch { return send(res, 400, baseHeaders(), 'bad preview path'); }
609
+ const rel = after.slice(slash + 1);
610
+ const runDir = runDirFor(cfg, runId);
611
+ if (!runDir) return send(res, 404, { 'Content-Type': 'text/plain; charset=utf-8', 'X-Content-Type-Options': 'nosniff' }, 'run não encontrado');
612
+ const abs = confineToRunDir(runDir, rel);
613
+ if (!abs || !fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
614
+ return send(res, 400, { 'Content-Type': 'text/plain; charset=utf-8', 'X-Content-Type-Options': 'nosniff' }, 'peça inválida');
615
+ }
616
+ const ext = path.extname(abs).toLowerCase();
617
+ const ctype = CONTENT_TYPES[ext] || 'application/octet-stream';
618
+ // sandbox-CSP: origem OPACA, script-src 'none' — o HTML da peça não roda JS nem lê o cookie da
619
+ // UI (não tem allow-same-origin nem allow-scripts). NUNCA Set-Cookie aqui.
620
+ const headers = {
621
+ 'Content-Type': ctype,
622
+ 'X-Content-Type-Options': 'nosniff',
623
+ 'Referrer-Policy': 'no-referrer',
624
+ 'Cross-Origin-Resource-Policy': 'same-origin',
625
+ 'Content-Security-Policy':
626
+ "sandbox; default-src 'none'; script-src 'none'; style-src 'unsafe-inline'; img-src 'self' data:; font-src data:",
627
+ };
628
+ return send(res, 200, headers, fs.readFileSync(abs));
629
+ }
630
+
631
+ const session = getSession(req, cfg, now());
632
+
633
+ // ---- Login ----
634
+ if (method === 'GET' && pathname === '/login') {
635
+ return send(res, 200, baseHeaders(), loginView(url.searchParams.get('e')));
636
+ }
637
+ if (method === 'POST' && pathname === '/login') {
638
+ const rl = loginLimiter.check(clientIp(req), now());
639
+ if (!rl.allowed) return send(res, 429, baseHeaders(), loginView('Muitas tentativas. Aguarde 1 minuto.'));
640
+ let form;
641
+ try { form = parseForm(await readBody(req)); } catch { return send(res, 413, baseHeaders(), loginView('Requisição inválida.')); }
642
+ if (!svc.verifyApproverSecret(form.password || '', cfg.teamHash)) {
643
+ return send(res, 401, baseHeaders(), loginView('Senha incorreta.'));
644
+ }
645
+ const sid = crypto.randomBytes(16).toString('hex');
646
+ const token = signSession({ sid, operator: null, exp: now() + cfg.sessionTtlMs }, cfg.sessionKey);
647
+ setSessionCookie(res, token, cfg);
648
+ return redirect(res, '/', cfg);
649
+ }
650
+
651
+ // Tudo abaixo exige sessão.
652
+ if (!session) return redirect(res, '/login', cfg);
653
+
654
+ // ---- Logout ----
655
+ if (method === 'POST' && pathname === '/logout') {
656
+ clearSessionCookie(res, cfg);
657
+ return redirect(res, '/login', cfg);
658
+ }
659
+
660
+ // ---- Seletor de operador ----
661
+ if (method === 'POST' && pathname === '/operator') {
662
+ const form = parseForm(await readBody(req));
663
+ if (!sameOrigin(req) || !verifyCsrf(form._csrf, session.sid, cfg.sessionKey)) {
664
+ return send(res, 403, baseHeaders(), operatorView(cfg, session, 'Sessão inválida (CSRF). Tente novamente.'));
665
+ }
666
+ let model;
667
+ try { model = svc.loadIdentities({ env: process.env, file: cfg.identitiesFile }); } catch { model = null; }
668
+ if (!model || !svc.isKnownIdentity(model, form.operator)) {
669
+ return send(res, 400, baseHeaders(), operatorView(cfg, session, 'Selecione um operador válido.'));
670
+ }
671
+ const token = signSession({ sid: session.sid, operator: form.operator, exp: now() + cfg.sessionTtlMs }, cfg.sessionKey);
672
+ setSessionCookie(res, token, cfg);
673
+ return redirect(res, '/', cfg);
674
+ }
675
+ if (method === 'POST' && pathname === '/operator/change') {
676
+ const form = parseForm(await readBody(req));
677
+ if (!sameOrigin(req) || !verifyCsrf(form._csrf, session.sid, cfg.sessionKey)) return send(res, 403, baseHeaders(), 'CSRF');
678
+ const token = signSession({ sid: session.sid, operator: null, exp: now() + cfg.sessionTtlMs }, cfg.sessionKey);
679
+ setSessionCookie(res, token, cfg);
680
+ return redirect(res, '/', cfg);
681
+ }
682
+
683
+ // ---- Approve ----
684
+ if (method === 'POST' && pathname === '/approve') {
685
+ const form = parseForm(await readBody(req));
686
+ if (!sameOrigin(req) || !verifyCsrf(form._csrf, session.sid, cfg.sessionKey)) {
687
+ return send(res, 403, baseHeaders(), 'CSRF: origem/token inválido.');
688
+ }
689
+ if (!session.operator) return redirect(res, '/', cfg);
690
+ const rl = approveLimiter.check(session.sid, now());
691
+ if (!rl.allowed) {
692
+ const v = checkpointView(cfg, session, form.run, 'Muitas tentativas. Aguarde e tente novamente.');
693
+ return send(res, 429, baseHeaders(), v.gone ? listView(cfg, session, 'gone') : v.html);
694
+ }
695
+ // O GATE REAL é o backend (SF10.4 approveRun): exige is_approver E o segredo verificado
696
+ // server-side. A UI apenas repassa asId (operador da sessão) e o segredo do form. Uma
697
+ // requisição forjada com operador errado / sem segredo passa por aqui e é RECUSADA pelo backend.
698
+ const r = await approveFn(cfg, form.run, {
699
+ env: process.env,
700
+ asId: session.operator,
701
+ approverSecret: form.secret,
702
+ approverSecretHash: cfg.approverSecretHash,
703
+ identitiesFile: cfg.identitiesFile,
704
+ });
705
+ if (r && r.code === 0 && !r.denied) return redirect(res, '/?flash=approved', cfg);
706
+ // SF10.16 — disputa PERDIDA (o checkpoint foi reivindicado por outra aprovação/rejeição entre o
707
+ // clique e o backend): estado que a UI já sabe renderizar. Não é erro de segredo, e dizer
708
+ // "confira o segredo" seria mentir ao operador. Sem oracle: a recusa por autorização continua
709
+ // genérica abaixo, e `gone` só é sinalizado DEPOIS de o gate de DF3-3 ter AUTORIZADO.
710
+ if (r && r.gone) return redirect(res, '/?flash=gone', cfg);
711
+ // Erro GENÉRICO (sem oracle: não distingue segredo-errado de não-aprovador).
712
+ const v = checkpointView(cfg, session, form.run, 'Não foi possível aprovar. Confira o segredo e tente novamente.');
713
+ return send(res, 403, baseHeaders(), v.gone ? listView(cfg, session, 'gone') : v.html);
714
+ }
715
+
716
+ // ---- Reject ----
717
+ if (method === 'POST' && pathname === '/reject') {
718
+ const form = parseForm(await readBody(req));
719
+ if (!sameOrigin(req) || !verifyCsrf(form._csrf, session.sid, cfg.sessionKey)) {
720
+ return send(res, 403, baseHeaders(), 'CSRF: origem/token inválido.');
721
+ }
722
+ if (!session.operator) return redirect(res, '/', cfg);
723
+ if (!form.reason || String(form.reason).trim() === '') {
724
+ const v = checkpointView(cfg, session, form.run, 'Informe um motivo para rejeitar.');
725
+ return send(res, 400, baseHeaders(), v.gone ? listView(cfg, session, 'gone') : v.html);
726
+ }
727
+ const r = rejectRun(cfg, form.run, { reason: form.reason, rejectedBy: session.operator });
728
+ return redirect(res, r.ok ? '/?flash=rejected' : '/?flash=gone', cfg);
729
+ }
730
+
731
+ // ---- GET / (lista ou seletor) ----
732
+ if (method === 'GET' && pathname === '/') {
733
+ if (!session.operator) return send(res, 200, baseHeaders(), operatorView(cfg, session));
734
+ const flash = url.searchParams.get('flash');
735
+ return send(res, 200, baseHeaders(), listView(cfg, session, flash));
736
+ }
737
+
738
+ // ---- GET /checkpoint ----
739
+ if (method === 'GET' && pathname === '/checkpoint') {
740
+ if (!session.operator) return redirect(res, '/', cfg);
741
+ const runId = url.searchParams.get('run');
742
+ const v = checkpointView(cfg, session, runId);
743
+ if (v.gone) return redirect(res, '/?flash=gone', cfg);
744
+ return send(res, 200, baseHeaders(), v.html);
745
+ }
746
+
747
+ return send(res, 404, baseHeaders(), layout('404', '<div class="card"><h1>Não encontrado</h1></div>'));
748
+ };
749
+ }
750
+
751
+ // ---------------------------------------------------------------------------
752
+ // Server.
753
+ // ---------------------------------------------------------------------------
754
+
755
+ function createServer(cfg, deps = {}) {
756
+ return http.createServer(makeHandler(cfg, deps));
757
+ }
758
+
759
+ function start(cfg, deps = {}) {
760
+ assertUiConfig(cfg);
761
+ const server = createServer(cfg, deps);
762
+ return new Promise((resolve) => {
763
+ server.listen(cfg.port, cfg.host, () => {
764
+ console.log(`[pod-export-ui] escutando em http://${cfg.host}:${cfg.port} (bind localhost — exponha via nginx TLS). queue=${cfg.queueDir}`);
765
+ resolve(server);
766
+ });
767
+ });
768
+ }
769
+
770
+ // ---------------------------------------------------------------------------
771
+ // CLI: start | hash-team.
772
+ // ---------------------------------------------------------------------------
773
+
774
+ async function main() {
775
+ const cmd = process.argv[2];
776
+ if (cmd === 'start') {
777
+ const cfg = loadUiConfig();
778
+ try { assertUiConfig(cfg); } catch (e) { console.error(e.message); process.exit(1); }
779
+ await start(cfg);
780
+ } else if (cmd === 'hash-team') {
781
+ // Gera o hash a colocar em PODEXPORT_UI_TEAM_HASH a partir de PODEXPORT_UI_TEAM_SECRET (o claro
782
+ // nunca é logado; reusa o scrypt do serviço).
783
+ const s = process.env.PODEXPORT_UI_TEAM_SECRET;
784
+ if (!s || String(s).trim() === '') { console.error('defina PODEXPORT_UI_TEAM_SECRET no env para gerar o hash (o claro nunca é logado).'); process.exit(2); }
785
+ console.log(svc.hashApproverSecret(s));
786
+ } else {
787
+ console.error('Uso: node scripts/pod-export-ui.cjs <start|hash-team>');
788
+ console.error('start exige PODEXPORT_QUEUE_DIR, PODEXPORT_UI_TEAM_HASH, PODEXPORT_UI_SESSION_KEY, PODEXPORT_APPROVER_SECRET_HASH no env (ver docs/mecanismos/pod-export-ui.md).');
789
+ process.exit(2);
790
+ }
791
+ }
792
+
793
+ if (require.main === module) {
794
+ main().catch((e) => { console.error('[ERRO]', e.message); process.exit(1); });
795
+ }
796
+
797
+ module.exports = {
798
+ loadUiConfig, assertUiConfig,
799
+ b64url, b64urlDecode, hmac, timingEq,
800
+ signSession, verifySession, csrfToken, verifyCsrf,
801
+ createRateLimiter, escapeHtml, parseCookies, parseForm, sameOrigin, clientIp,
802
+ safeRunId, listCheckpoints, runDirFor, listPieceFiles, checkpointCriterion, confineToRunDir,
803
+ rejectRun, loginView, operatorView, listView, checkpointView, baseHeaders,
804
+ makeHandler, createServer, start, COOKIE_NAME,
805
+ };