@nebulacomponents/citable 1.19.0 → 1.20.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 (239) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +12 -1
  3. package/dist/universal/.agents/skills/citable/SKILL.md +11 -2
  4. package/dist/universal/.agents/skills/citable/VERSION +1 -1
  5. package/dist/universal/.agents/skills/citable/manifest.json +15 -12
  6. package/dist/universal/.agents/skills/citable/schemas/action-plan.schema.json +38 -1
  7. package/dist/universal/.agents/skills/citable/schemas/audit-coverage.schema.json +118 -0
  8. package/dist/universal/.agents/skills/citable/schemas/collection-result.schema.json +35 -0
  9. package/dist/universal/.agents/skills/citable/schemas/config.schema.json +2 -0
  10. package/dist/universal/.agents/skills/citable/schemas/cro-report.schema.json +8 -1
  11. package/dist/universal/.agents/skills/citable/schemas/finding.schema.json +105 -2
  12. package/dist/universal/.agents/skills/citable/schemas/remediation-verification.schema.json +12 -1
  13. package/dist/universal/.agents/skills/citable/schemas/run.schema.json +15 -2
  14. package/dist/universal/.agents/skills/citable/schemas/search-report.schema.json +8 -1
  15. package/dist/universal/.agents/skills/citable/schemas/summary.schema.json +41 -0
  16. package/dist/universal/.claude/agents/citable/manifest.json +2 -2
  17. package/dist/universal/.claude/skills/citable/SKILL.md +11 -2
  18. package/dist/universal/.claude/skills/citable/VERSION +1 -1
  19. package/dist/universal/.claude/skills/citable/manifest.json +15 -12
  20. package/dist/universal/.claude/skills/citable/schemas/action-plan.schema.json +38 -1
  21. package/dist/universal/.claude/skills/citable/schemas/audit-coverage.schema.json +118 -0
  22. package/dist/universal/.claude/skills/citable/schemas/collection-result.schema.json +35 -0
  23. package/dist/universal/.claude/skills/citable/schemas/config.schema.json +2 -0
  24. package/dist/universal/.claude/skills/citable/schemas/cro-report.schema.json +8 -1
  25. package/dist/universal/.claude/skills/citable/schemas/finding.schema.json +105 -2
  26. package/dist/universal/.claude/skills/citable/schemas/remediation-verification.schema.json +12 -1
  27. package/dist/universal/.claude/skills/citable/schemas/run.schema.json +15 -2
  28. package/dist/universal/.claude/skills/citable/schemas/search-report.schema.json +8 -1
  29. package/dist/universal/.claude/skills/citable/schemas/summary.schema.json +41 -0
  30. package/dist/universal/.cursor/skills/citable/SKILL.md +11 -2
  31. package/dist/universal/.cursor/skills/citable/VERSION +1 -1
  32. package/dist/universal/.cursor/skills/citable/manifest.json +15 -12
  33. package/dist/universal/.cursor/skills/citable/schemas/action-plan.schema.json +38 -1
  34. package/dist/universal/.cursor/skills/citable/schemas/audit-coverage.schema.json +118 -0
  35. package/dist/universal/.cursor/skills/citable/schemas/collection-result.schema.json +35 -0
  36. package/dist/universal/.cursor/skills/citable/schemas/config.schema.json +2 -0
  37. package/dist/universal/.cursor/skills/citable/schemas/cro-report.schema.json +8 -1
  38. package/dist/universal/.cursor/skills/citable/schemas/finding.schema.json +105 -2
  39. package/dist/universal/.cursor/skills/citable/schemas/remediation-verification.schema.json +12 -1
  40. package/dist/universal/.cursor/skills/citable/schemas/run.schema.json +15 -2
  41. package/dist/universal/.cursor/skills/citable/schemas/search-report.schema.json +8 -1
  42. package/dist/universal/.cursor/skills/citable/schemas/summary.schema.json +41 -0
  43. package/dist/universal/.gemini/skills/citable/SKILL.md +11 -2
  44. package/dist/universal/.gemini/skills/citable/VERSION +1 -1
  45. package/dist/universal/.gemini/skills/citable/manifest.json +15 -12
  46. package/dist/universal/.gemini/skills/citable/schemas/action-plan.schema.json +38 -1
  47. package/dist/universal/.gemini/skills/citable/schemas/audit-coverage.schema.json +118 -0
  48. package/dist/universal/.gemini/skills/citable/schemas/collection-result.schema.json +35 -0
  49. package/dist/universal/.gemini/skills/citable/schemas/config.schema.json +2 -0
  50. package/dist/universal/.gemini/skills/citable/schemas/cro-report.schema.json +8 -1
  51. package/dist/universal/.gemini/skills/citable/schemas/finding.schema.json +105 -2
  52. package/dist/universal/.gemini/skills/citable/schemas/remediation-verification.schema.json +12 -1
  53. package/dist/universal/.gemini/skills/citable/schemas/run.schema.json +15 -2
  54. package/dist/universal/.gemini/skills/citable/schemas/search-report.schema.json +8 -1
  55. package/dist/universal/.gemini/skills/citable/schemas/summary.schema.json +41 -0
  56. package/dist/universal/.github/skills/citable/SKILL.md +11 -2
  57. package/dist/universal/.github/skills/citable/VERSION +1 -1
  58. package/dist/universal/.github/skills/citable/manifest.json +15 -12
  59. package/dist/universal/.github/skills/citable/schemas/action-plan.schema.json +38 -1
  60. package/dist/universal/.github/skills/citable/schemas/audit-coverage.schema.json +118 -0
  61. package/dist/universal/.github/skills/citable/schemas/collection-result.schema.json +35 -0
  62. package/dist/universal/.github/skills/citable/schemas/config.schema.json +2 -0
  63. package/dist/universal/.github/skills/citable/schemas/cro-report.schema.json +8 -1
  64. package/dist/universal/.github/skills/citable/schemas/finding.schema.json +105 -2
  65. package/dist/universal/.github/skills/citable/schemas/remediation-verification.schema.json +12 -1
  66. package/dist/universal/.github/skills/citable/schemas/run.schema.json +15 -2
  67. package/dist/universal/.github/skills/citable/schemas/search-report.schema.json +8 -1
  68. package/dist/universal/.github/skills/citable/schemas/summary.schema.json +41 -0
  69. package/dist/universal/.kiro/skills/citable/SKILL.md +11 -2
  70. package/dist/universal/.kiro/skills/citable/VERSION +1 -1
  71. package/dist/universal/.kiro/skills/citable/manifest.json +15 -12
  72. package/dist/universal/.kiro/skills/citable/schemas/action-plan.schema.json +38 -1
  73. package/dist/universal/.kiro/skills/citable/schemas/audit-coverage.schema.json +118 -0
  74. package/dist/universal/.kiro/skills/citable/schemas/collection-result.schema.json +35 -0
  75. package/dist/universal/.kiro/skills/citable/schemas/config.schema.json +2 -0
  76. package/dist/universal/.kiro/skills/citable/schemas/cro-report.schema.json +8 -1
  77. package/dist/universal/.kiro/skills/citable/schemas/finding.schema.json +105 -2
  78. package/dist/universal/.kiro/skills/citable/schemas/remediation-verification.schema.json +12 -1
  79. package/dist/universal/.kiro/skills/citable/schemas/run.schema.json +15 -2
  80. package/dist/universal/.kiro/skills/citable/schemas/search-report.schema.json +8 -1
  81. package/dist/universal/.kiro/skills/citable/schemas/summary.schema.json +41 -0
  82. package/dist/universal/.opencode/skills/citable/SKILL.md +11 -2
  83. package/dist/universal/.opencode/skills/citable/VERSION +1 -1
  84. package/dist/universal/.opencode/skills/citable/manifest.json +15 -12
  85. package/dist/universal/.opencode/skills/citable/schemas/action-plan.schema.json +38 -1
  86. package/dist/universal/.opencode/skills/citable/schemas/audit-coverage.schema.json +118 -0
  87. package/dist/universal/.opencode/skills/citable/schemas/collection-result.schema.json +35 -0
  88. package/dist/universal/.opencode/skills/citable/schemas/config.schema.json +2 -0
  89. package/dist/universal/.opencode/skills/citable/schemas/cro-report.schema.json +8 -1
  90. package/dist/universal/.opencode/skills/citable/schemas/finding.schema.json +105 -2
  91. package/dist/universal/.opencode/skills/citable/schemas/remediation-verification.schema.json +12 -1
  92. package/dist/universal/.opencode/skills/citable/schemas/run.schema.json +15 -2
  93. package/dist/universal/.opencode/skills/citable/schemas/search-report.schema.json +8 -1
  94. package/dist/universal/.opencode/skills/citable/schemas/summary.schema.json +41 -0
  95. package/dist/universal/.pi/agent/skills/citable/SKILL.md +11 -2
  96. package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
  97. package/dist/universal/.pi/agent/skills/citable/manifest.json +15 -12
  98. package/dist/universal/.pi/agent/skills/citable/schemas/action-plan.schema.json +38 -1
  99. package/dist/universal/.pi/agent/skills/citable/schemas/audit-coverage.schema.json +118 -0
  100. package/dist/universal/.pi/agent/skills/citable/schemas/collection-result.schema.json +35 -0
  101. package/dist/universal/.pi/agent/skills/citable/schemas/config.schema.json +2 -0
  102. package/dist/universal/.pi/agent/skills/citable/schemas/cro-report.schema.json +8 -1
  103. package/dist/universal/.pi/agent/skills/citable/schemas/finding.schema.json +105 -2
  104. package/dist/universal/.pi/agent/skills/citable/schemas/remediation-verification.schema.json +12 -1
  105. package/dist/universal/.pi/agent/skills/citable/schemas/run.schema.json +15 -2
  106. package/dist/universal/.pi/agent/skills/citable/schemas/search-report.schema.json +8 -1
  107. package/dist/universal/.pi/agent/skills/citable/schemas/summary.schema.json +41 -0
  108. package/dist/universal/.qoder/skills/citable/SKILL.md +11 -2
  109. package/dist/universal/.qoder/skills/citable/VERSION +1 -1
  110. package/dist/universal/.qoder/skills/citable/manifest.json +15 -12
  111. package/dist/universal/.qoder/skills/citable/schemas/action-plan.schema.json +38 -1
  112. package/dist/universal/.qoder/skills/citable/schemas/audit-coverage.schema.json +118 -0
  113. package/dist/universal/.qoder/skills/citable/schemas/collection-result.schema.json +35 -0
  114. package/dist/universal/.qoder/skills/citable/schemas/config.schema.json +2 -0
  115. package/dist/universal/.qoder/skills/citable/schemas/cro-report.schema.json +8 -1
  116. package/dist/universal/.qoder/skills/citable/schemas/finding.schema.json +105 -2
  117. package/dist/universal/.qoder/skills/citable/schemas/remediation-verification.schema.json +12 -1
  118. package/dist/universal/.qoder/skills/citable/schemas/run.schema.json +15 -2
  119. package/dist/universal/.qoder/skills/citable/schemas/search-report.schema.json +8 -1
  120. package/dist/universal/.qoder/skills/citable/schemas/summary.schema.json +41 -0
  121. package/dist/universal/.rovodev/skills/citable/SKILL.md +11 -2
  122. package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
  123. package/dist/universal/.rovodev/skills/citable/manifest.json +15 -12
  124. package/dist/universal/.rovodev/skills/citable/schemas/action-plan.schema.json +38 -1
  125. package/dist/universal/.rovodev/skills/citable/schemas/audit-coverage.schema.json +118 -0
  126. package/dist/universal/.rovodev/skills/citable/schemas/collection-result.schema.json +35 -0
  127. package/dist/universal/.rovodev/skills/citable/schemas/config.schema.json +2 -0
  128. package/dist/universal/.rovodev/skills/citable/schemas/cro-report.schema.json +8 -1
  129. package/dist/universal/.rovodev/skills/citable/schemas/finding.schema.json +105 -2
  130. package/dist/universal/.rovodev/skills/citable/schemas/remediation-verification.schema.json +12 -1
  131. package/dist/universal/.rovodev/skills/citable/schemas/run.schema.json +15 -2
  132. package/dist/universal/.rovodev/skills/citable/schemas/search-report.schema.json +8 -1
  133. package/dist/universal/.rovodev/skills/citable/schemas/summary.schema.json +41 -0
  134. package/dist/universal/.trae/skills/citable/SKILL.md +11 -2
  135. package/dist/universal/.trae/skills/citable/VERSION +1 -1
  136. package/dist/universal/.trae/skills/citable/manifest.json +15 -12
  137. package/dist/universal/.trae/skills/citable/schemas/action-plan.schema.json +38 -1
  138. package/dist/universal/.trae/skills/citable/schemas/audit-coverage.schema.json +118 -0
  139. package/dist/universal/.trae/skills/citable/schemas/collection-result.schema.json +35 -0
  140. package/dist/universal/.trae/skills/citable/schemas/config.schema.json +2 -0
  141. package/dist/universal/.trae/skills/citable/schemas/cro-report.schema.json +8 -1
  142. package/dist/universal/.trae/skills/citable/schemas/finding.schema.json +105 -2
  143. package/dist/universal/.trae/skills/citable/schemas/remediation-verification.schema.json +12 -1
  144. package/dist/universal/.trae/skills/citable/schemas/run.schema.json +15 -2
  145. package/dist/universal/.trae/skills/citable/schemas/search-report.schema.json +8 -1
  146. package/dist/universal/.trae/skills/citable/schemas/summary.schema.json +41 -0
  147. package/dist/universal/.trae-cn/skills/citable/SKILL.md +11 -2
  148. package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
  149. package/dist/universal/.trae-cn/skills/citable/manifest.json +15 -12
  150. package/dist/universal/.trae-cn/skills/citable/schemas/action-plan.schema.json +38 -1
  151. package/dist/universal/.trae-cn/skills/citable/schemas/audit-coverage.schema.json +118 -0
  152. package/dist/universal/.trae-cn/skills/citable/schemas/collection-result.schema.json +35 -0
  153. package/dist/universal/.trae-cn/skills/citable/schemas/config.schema.json +2 -0
  154. package/dist/universal/.trae-cn/skills/citable/schemas/cro-report.schema.json +8 -1
  155. package/dist/universal/.trae-cn/skills/citable/schemas/finding.schema.json +105 -2
  156. package/dist/universal/.trae-cn/skills/citable/schemas/remediation-verification.schema.json +12 -1
  157. package/dist/universal/.trae-cn/skills/citable/schemas/run.schema.json +15 -2
  158. package/dist/universal/.trae-cn/skills/citable/schemas/search-report.schema.json +8 -1
  159. package/dist/universal/.trae-cn/skills/citable/schemas/summary.schema.json +41 -0
  160. package/dist/universal/manifest.json +184 -148
  161. package/package.json +1 -1
  162. package/schemas/action-plan.schema.json +38 -1
  163. package/schemas/audit-coverage.schema.json +118 -0
  164. package/schemas/collection-result.schema.json +35 -0
  165. package/schemas/config.schema.json +2 -0
  166. package/schemas/cro-report.schema.json +8 -1
  167. package/schemas/finding.schema.json +105 -2
  168. package/schemas/remediation-verification.schema.json +12 -1
  169. package/schemas/run.schema.json +15 -2
  170. package/schemas/search-report.schema.json +8 -1
  171. package/schemas/summary.schema.json +41 -0
  172. package/skill/SKILL.md +11 -2
  173. package/src/acceptance/reproducibility.js +5 -25
  174. package/src/analysis/croRoadmap.js +3 -3
  175. package/src/analysis/strategicRoadmap.js +3 -3
  176. package/src/artifacts/interchange.js +2 -6
  177. package/src/cli/index.js +15 -3
  178. package/src/commands/actionPlan.js +46 -5
  179. package/src/commands/applyRemediation.js +5 -5
  180. package/src/commands/audit.js +75 -8
  181. package/src/commands/ciWorkflow.js +7 -2
  182. package/src/commands/compareSnapshots.js +155 -47
  183. package/src/commands/connect.js +24 -4
  184. package/src/commands/context.js +41 -2
  185. package/src/commands/croSuite.js +51 -13
  186. package/src/commands/delivery.js +7 -7
  187. package/src/commands/governance.js +9 -10
  188. package/src/commands/implementationKit.js +2 -1
  189. package/src/commands/init.js +8 -1
  190. package/src/commands/previewCro.js +1 -1
  191. package/src/commands/prioritize.js +3 -2
  192. package/src/commands/reviews.js +31 -5
  193. package/src/commands/roadmapCmd.js +7 -13
  194. package/src/commands/sweep.js +15 -2
  195. package/src/commands/verifyRemediation.js +94 -19
  196. package/src/connectors/collectionResult.js +80 -0
  197. package/src/connectors/ga4.js +46 -11
  198. package/src/connectors/gsc.js +41 -6
  199. package/src/connectors/index.js +1 -1
  200. package/src/connectors/webflow.js +140 -10
  201. package/src/connectors/wordpress.js +92 -8
  202. package/src/crawler/fetch.js +220 -24
  203. package/src/crawler/resourceValidity.js +119 -0
  204. package/src/crawler/sitemap.js +435 -16
  205. package/src/crawler/sitemapCollector.js +364 -0
  206. package/src/crawler/urlIdentity.js +97 -0
  207. package/src/detectors/agent.js +11 -0
  208. package/src/detectors/ans.js +16 -1
  209. package/src/detectors/arch.js +6 -0
  210. package/src/detectors/claim.js +9 -0
  211. package/src/detectors/crawl.js +7 -0
  212. package/src/detectors/cro.js +21 -0
  213. package/src/detectors/cwv.js +4 -0
  214. package/src/detectors/entity.js +7 -0
  215. package/src/detectors/evd.js +9 -0
  216. package/src/detectors/framework.js +28 -2
  217. package/src/detectors/geoReco.js +16 -0
  218. package/src/detectors/hreflang.js +3 -0
  219. package/src/detectors/lifeMeas.js +9 -0
  220. package/src/detectors/link.js +6 -1
  221. package/src/detectors/page.js +11 -0
  222. package/src/detectors/schemaData.js +16 -1
  223. package/src/detectors/tech.js +34 -3
  224. package/src/evidence/coverage.js +448 -0
  225. package/src/evidence/determination.js +157 -0
  226. package/src/evidence/downstream.js +110 -0
  227. package/src/evidence/hashes.js +109 -0
  228. package/src/evidence/run.js +68 -8
  229. package/src/extractor/page.js +29 -2
  230. package/src/extractor/site.js +298 -50
  231. package/src/reporting/executiveCroReport.js +36 -13
  232. package/src/reporting/executiveExport.js +45 -16
  233. package/src/reporting/executiveSearchReport.js +56 -22
  234. package/src/reporting/report.js +56 -9
  235. package/src/shared/epistemicLanguage.js +86 -0
  236. package/src/shared/evidenceSourceResolver.js +48 -17
  237. package/src/shared/htmlEscape.js +8 -4
  238. package/src/shared/runPackageVerifier.js +99 -8
  239. package/src/shared/verifiedRunLoader.js +129 -0
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { readJson, sha256File } from './io.js';
3
+ import { sha256 } from './io.js';
4
4
  import { validateAgainst } from './schemaValidator.js';
5
5
 
6
6
  export class RunVerificationError extends Error {
@@ -12,6 +12,17 @@ export class RunVerificationError extends Error {
12
12
  }
13
13
  }
14
14
 
15
+ function readStableBytes(file, runDir, relative) {
16
+ const first = fs.readFileSync(file);
17
+ const second = fs.readFileSync(file);
18
+ if (!first.equals(second)) {
19
+ throw new RunVerificationError(`Artifact changed while being verified: ${relative}`, {
20
+ code: 'PACKAGE_MUTATION', runDir, file: relative,
21
+ });
22
+ }
23
+ return first;
24
+ }
25
+
15
26
  /**
16
27
  * Verifies the cryptographic and structural integrity of an audit run evidence package.
17
28
  *
@@ -28,13 +39,24 @@ export function verifyRunPackage(runDir, options = {}) {
28
39
  requireFindings = true,
29
40
  requireCompleted = false,
30
41
  requireChecksums = true,
42
+ ignoredFiles = [],
31
43
  } = options;
32
44
 
33
45
  if (!runDir || !fs.existsSync(runDir)) {
34
46
  throw new RunVerificationError(`Run directory not found: ${runDir}`, { code: 'RUN_NOT_FOUND', runDir });
35
47
  }
48
+ let rootStat;
49
+ try { rootStat = fs.lstatSync(runDir); } catch (error) {
50
+ throw new RunVerificationError(`Run directory cannot be inspected: ${error.message}`, { code: 'RUN_NOT_FOUND', runDir });
51
+ }
52
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
53
+ throw new RunVerificationError(`Run path is not a real directory: ${runDir}`, { code: 'RUN_PATH_INVALID', runDir });
54
+ }
36
55
 
37
56
  const manifestPath = path.join(runDir, 'manifest.json');
57
+ if (fs.existsSync(manifestPath) && fs.lstatSync(manifestPath).isSymbolicLink()) {
58
+ throw new RunVerificationError('Run manifest.json is a symbolic link', { code: 'PACKAGE_SYMLINK', runDir, file: 'manifest.json' });
59
+ }
38
60
  if (!fs.existsSync(manifestPath)) {
39
61
  throw new RunVerificationError(`Run package is missing manifest.json at ${manifestPath}`, {
40
62
  code: 'MANIFEST_MISSING',
@@ -44,7 +66,7 @@ export function verifyRunPackage(runDir, options = {}) {
44
66
 
45
67
  let manifest;
46
68
  try {
47
- manifest = readJson(manifestPath);
69
+ manifest = JSON.parse(readStableBytes(manifestPath, runDir, 'manifest.json').toString('utf8'));
48
70
  } catch (err) {
49
71
  throw new RunVerificationError(`Run manifest.json is invalid JSON: ${err.message}`, {
50
72
  code: 'MANIFEST_INVALID',
@@ -53,6 +75,12 @@ export function verifyRunPackage(runDir, options = {}) {
53
75
  }
54
76
 
55
77
  const schemaValidation = validateAgainst('run.schema.json', manifest);
78
+ if (Object.prototype.hasOwnProperty.call(manifest, 'schema_version')
79
+ && ![1, 2].includes(manifest.schema_version)) {
80
+ throw new RunVerificationError(`Unsupported run manifest schema_version: ${String(manifest.schema_version)}`, {
81
+ code: 'MANIFEST_SCHEMA_UNSUPPORTED', runDir, schema_version: manifest.schema_version,
82
+ });
83
+ }
56
84
  if (!schemaValidation.valid) {
57
85
  throw new RunVerificationError(`Run manifest violates run.schema.json: ${schemaValidation.errors.join('; ')}`, {
58
86
  code: 'MANIFEST_SCHEMA_INVALID',
@@ -69,42 +97,94 @@ export function verifyRunPackage(runDir, options = {}) {
69
97
  });
70
98
  }
71
99
 
100
+ // A sealed run is closed-world: every regular file except the checksum
101
+ // index itself must be listed, and every listed path must be safe. Do not
102
+ // use stat() here because it follows symlinks and could verify bytes outside
103
+ // the package directory.
104
+ const packageFiles = [];
105
+ const walk = (directory) => {
106
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
107
+ const absolute = path.join(directory, entry.name);
108
+ const relative = path.relative(runDir, absolute).split(path.sep).join('/');
109
+ if (entry.isSymbolicLink()) {
110
+ throw new RunVerificationError(`Run package contains symbolic link: ${relative}`, {
111
+ code: 'PACKAGE_SYMLINK', runDir, file: relative,
112
+ });
113
+ }
114
+ if (entry.isDirectory()) walk(absolute);
115
+ else if (entry.isFile()) packageFiles.push(relative);
116
+ else throw new RunVerificationError(`Run package contains unsupported filesystem entry: ${relative}`, {
117
+ code: 'PACKAGE_ENTRY_INVALID', runDir, file: relative,
118
+ });
119
+ }
120
+ };
121
+ walk(runDir);
122
+ const ignored = new Set(ignoredFiles);
123
+
72
124
  // Verify checksums.json
73
125
  const checksumsPath = path.join(runDir, 'checksums.json');
74
126
  let checksumsVerified = false;
127
+ let artifactHashes = {};
128
+ const artifactBytes = new Map();
129
+ const retainedArtifactNames = new Set(['findings.json', 'coverage.json', 'summary.json']);
75
130
  if (fs.existsSync(checksumsPath)) {
131
+ if (fs.lstatSync(checksumsPath).isSymbolicLink()) throw new RunVerificationError('checksums.json is a symbolic link', { code: 'PACKAGE_SYMLINK', runDir, file: 'checksums.json' });
76
132
  let checksums;
77
133
  try {
78
- checksums = readJson(checksumsPath);
134
+ checksums = JSON.parse(readStableBytes(checksumsPath, runDir, 'checksums.json').toString('utf8'));
79
135
  } catch (err) {
80
136
  throw new RunVerificationError(`checksums.json is invalid JSON: ${err.message}`, {
81
137
  code: 'CHECKSUMS_INVALID',
82
138
  runDir,
83
139
  });
84
140
  }
141
+ artifactHashes = checksums;
85
142
 
86
143
  const tamperedFiles = [];
87
144
  const missingFiles = [];
88
145
 
146
+ const safePath = (value) => typeof value === 'string'
147
+ && value.length > 0
148
+ && !path.isAbsolute(value)
149
+ && !value.includes('\\')
150
+ && !value.split('/').includes('..')
151
+ && !value.split('/').includes('')
152
+ && !/^[a-z][a-z0-9+.-]*:/i.test(value);
89
153
  for (const [relPath, expectedHash] of Object.entries(checksums)) {
154
+ if (!safePath(relPath) || relPath === 'checksums.json') {
155
+ throw new RunVerificationError(`unsafe checksum path (escapes the run package): ${relPath}`, {
156
+ code: 'CHECKSUM_PATH_INVALID', runDir, file: relPath,
157
+ });
158
+ }
90
159
  const artifactPath = path.join(runDir, relPath);
91
160
  if (!fs.existsSync(artifactPath)) {
92
161
  missingFiles.push(relPath);
93
162
  continue;
94
163
  }
95
- const actualHash = sha256File(artifactPath);
164
+ const artifactStat = fs.lstatSync(artifactPath);
165
+ if (artifactStat.isSymbolicLink() || !artifactStat.isFile()) {
166
+ throw new RunVerificationError(`checksum artifact is not a regular file: ${relPath}`, { code: 'PACKAGE_SYMLINK', runDir, file: relPath });
167
+ }
168
+ const bytes = readStableBytes(artifactPath, runDir, relPath);
169
+ if (retainedArtifactNames.has(relPath)) artifactBytes.set(relPath, bytes);
170
+ const actualHash = sha256(bytes);
96
171
  if (actualHash !== expectedHash) {
97
172
  tamperedFiles.push({ file: relPath, expected: expectedHash, actual: actualHash });
98
173
  }
99
174
  }
100
175
 
101
- if (tamperedFiles.length > 0 || missingFiles.length > 0) {
176
+ const expectedFiles = Object.keys(checksums).sort();
177
+ const actualFiles = packageFiles.filter((file) => file !== 'checksums.json' && !ignored.has(file)).sort();
178
+ const unexpectedFiles = actualFiles.filter((file) => !expectedFiles.includes(file));
179
+ const unlistedFiles = expectedFiles.filter((file) => !actualFiles.includes(file));
180
+ if (tamperedFiles.length > 0 || missingFiles.length > 0 || unexpectedFiles.length > 0 || unlistedFiles.length > 0) {
102
181
  throw new RunVerificationError(
103
- `Cryptographic verification failed for run package: ${tamperedFiles.length} tampered files, ${missingFiles.length} missing files`,
182
+ `Cryptographic verification failed for run package (checksum mismatch; integrity failed; completeness failure; unsealed): ${tamperedFiles.length} tampered files, ${missingFiles.length + unlistedFiles.length} missing files, ${unexpectedFiles.length} unexpected files`,
104
183
  {
105
184
  code: 'CHECKSUM_MISMATCH',
106
185
  tamperedFiles,
107
- missingFiles,
186
+ missingFiles: [...missingFiles, ...unlistedFiles],
187
+ unexpectedFiles,
108
188
  runDir,
109
189
  }
110
190
  );
@@ -122,10 +202,19 @@ export function verifyRunPackage(runDir, options = {}) {
122
202
  let findings = null;
123
203
  if (fs.existsSync(findingsPath)) {
124
204
  try {
125
- findings = readJson(findingsPath);
205
+ if (fs.lstatSync(findingsPath).isSymbolicLink()) throw new Error('findings.json is a symbolic link');
206
+ const findingsBytes = artifactBytes.get('findings.json') || readStableBytes(findingsPath, runDir, 'findings.json');
207
+ artifactBytes.set('findings.json', findingsBytes);
208
+ findings = JSON.parse(findingsBytes.toString('utf8'));
126
209
  if (!Array.isArray(findings)) {
127
210
  throw new Error('findings.json must contain an array of findings');
128
211
  }
212
+ for (const [index, finding] of findings.entries()) {
213
+ const findingValidation = validateAgainst('finding.schema.json', finding);
214
+ if (!findingValidation.valid) {
215
+ throw new Error(`finding[${index}] violates finding.schema.json: ${findingValidation.errors.join('; ')}`);
216
+ }
217
+ }
129
218
  } catch (err) {
130
219
  throw new RunVerificationError(`findings.json is malformed: ${err.message}`, {
131
220
  code: 'FINDINGS_MALFORMED',
@@ -149,5 +238,7 @@ export function verifyRunPackage(runDir, options = {}) {
149
238
  findingsPath: fs.existsSync(findingsPath) ? findingsPath : null,
150
239
  findingsCount: Array.isArray(findings) ? findings.length : 0,
151
240
  findings,
241
+ artifactHashes,
242
+ artifactBytes,
152
243
  };
153
244
  }
@@ -0,0 +1,129 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { readJson, sha256File } from './io.js';
4
+ import { verifyRunPackage, RunVerificationError } from './runPackageVerifier.js';
5
+ import { validateAgainst } from './schemaValidator.js';
6
+
7
+ export const VERIFIED_RUN_LOADER_VERSION = 'verified-run-loader-v1';
8
+
9
+ export class VerifiedRunLoadError extends Error {
10
+ constructor(message, details = {}) {
11
+ super(message);
12
+ this.name = 'VerifiedRunLoadError';
13
+ this.code = details.code || 'VERIFIED_RUN_LOAD_FAILED';
14
+ this.details = details;
15
+ }
16
+ }
17
+
18
+ function readContractArtifact(runDir, file, schema, { required = true, bytes = null } = {}) {
19
+ const filePath = path.join(runDir, file);
20
+ if (!fs.existsSync(filePath)) {
21
+ if (required) throw new VerifiedRunLoadError(`Verified run is missing ${file}`, { code: 'ARTIFACT_MISSING', file });
22
+ return null;
23
+ }
24
+ if (fs.lstatSync(filePath).isSymbolicLink()) {
25
+ throw new VerifiedRunLoadError(`${file} is a symbolic link`, { code: 'PACKAGE_SYMLINK', file });
26
+ }
27
+ let value;
28
+ try {
29
+ value = bytes ? JSON.parse(bytes.toString('utf8')) : readJson(filePath);
30
+ } catch (error) {
31
+ throw new VerifiedRunLoadError(`${file} is invalid JSON: ${error.message}`, { code: 'ARTIFACT_INVALID', file });
32
+ }
33
+ const validation = schema ? validateAgainst(schema, value) : { valid: true, errors: [] };
34
+ if (!validation.valid) {
35
+ throw new VerifiedRunLoadError(`${file} violates ${schema}: ${validation.errors.join('; ')}`, {
36
+ code: 'ARTIFACT_SCHEMA_INVALID', file, errors: validation.errors,
37
+ });
38
+ }
39
+ return value;
40
+ }
41
+
42
+ function readFindingsArtifact(runDir, bytes = null) {
43
+ const findings = readContractArtifact(runDir, 'findings.json', null, { bytes });
44
+ if (!Array.isArray(findings)) throw new VerifiedRunLoadError('findings.json must contain an array of findings', { code: 'ARTIFACT_SCHEMA_INVALID', file: 'findings.json' });
45
+ for (const [index, finding] of findings.entries()) {
46
+ const validation = validateAgainst('finding.schema.json', finding);
47
+ if (!validation.valid) throw new VerifiedRunLoadError(`findings.json finding[${index}] violates finding.schema.json: ${validation.errors.join('; ')}`, { code: 'ARTIFACT_SCHEMA_INVALID', file: 'findings.json' });
48
+ }
49
+ return findings;
50
+ }
51
+
52
+ /**
53
+ * Load a run only after its closed-world checksum seal and execution contracts
54
+ * have been verified. Consumers must use this API rather than reading
55
+ * findings.json directly: the returned findings are bound to the verified
56
+ * package, manifest, and (when present) coverage artifact.
57
+ *
58
+ * Legacy packages can be opened explicitly for migration. Their coverage is
59
+ * indeterminate and never receives a synthesized complete default.
60
+ */
61
+ export function loadVerifiedRun(runDir, {
62
+ requireCompletedExecution = true,
63
+ allowLegacy = false,
64
+ requireCoverage = !allowLegacy,
65
+ } = {}) {
66
+ try {
67
+ let verification;
68
+ try {
69
+ verification = verifyRunPackage(runDir, {
70
+ requireFindings: true,
71
+ requireCompleted: requireCompletedExecution,
72
+ requireChecksums: !allowLegacy,
73
+ });
74
+ } catch (error) {
75
+ if (!allowLegacy || !['CHECKSUMS_MISSING', 'MANIFEST_MISSING'].includes(error.code)) throw error;
76
+ const manifestPath = path.join(runDir, 'manifest.json');
77
+ const manifest = fs.existsSync(manifestPath) ? readJson(manifestPath) : { run_id: path.basename(runDir), status: 'incomplete' };
78
+ const findings = readFindingsArtifact(runDir);
79
+ const coverageFile = path.join(runDir, 'coverage.json');
80
+ const coverage = fs.existsSync(coverageFile) ? readContractArtifact(runDir, 'coverage.json', 'audit-coverage.schema.json') : null;
81
+ const summary = readContractArtifact(runDir, 'summary.json', 'summary.schema.json', { required: false });
82
+ return {
83
+ verified: false, verification_version: VERIFIED_RUN_LOADER_VERSION,
84
+ integrity_mode: 'legacy_unverified', package_dir: path.resolve(runDir),
85
+ package_hash: null, manifest, findings, findingsCount: findings.length,
86
+ coverage, summary, legacy: true,
87
+ coverage_status: coverage?.coverage_status || 'indeterminate', determination_status: 'indeterminate',
88
+ checksumsVerified: false,
89
+ artifactHashes: {
90
+ ...(fs.existsSync(path.join(runDir, 'manifest.json')) ? { 'manifest.json': sha256File(path.join(runDir, 'manifest.json')) } : {}),
91
+ 'findings.json': sha256File(path.join(runDir, 'findings.json')),
92
+ ...(fs.existsSync(coverageFile) ? { 'coverage.json': sha256File(coverageFile) } : {}),
93
+ },
94
+ };
95
+ }
96
+
97
+ const coveragePath = path.join(runDir, 'coverage.json');
98
+ const hasCoverage = fs.existsSync(coveragePath);
99
+ if (!hasCoverage && !allowLegacy && requireCoverage) {
100
+ throw new VerifiedRunLoadError('Verified run is missing coverage.json; use explicit legacy mode to open historical packages', {
101
+ code: 'COVERAGE_MISSING',
102
+ });
103
+ }
104
+ const coverage = hasCoverage ? readContractArtifact(runDir, 'coverage.json', 'audit-coverage.schema.json', { bytes: verification.artifactBytes.get('coverage.json') }) : null;
105
+ const summary = readContractArtifact(runDir, 'summary.json', 'summary.schema.json', { required: false, bytes: verification.artifactBytes.get('summary.json') });
106
+
107
+ return {
108
+ ...verification,
109
+ verified: hasCoverage,
110
+ verification_version: VERIFIED_RUN_LOADER_VERSION,
111
+ integrity_mode: hasCoverage ? 'sealed' : 'legacy_unverified',
112
+ package_dir: path.resolve(runDir),
113
+ package_hash: verification.checksumsVerified ? sha256File(path.join(runDir, 'checksums.json')) : null,
114
+ findings: verification.findings,
115
+ coverage,
116
+ summary,
117
+ legacy: !hasCoverage,
118
+ coverage_status: coverage?.coverage_status || 'indeterminate',
119
+ determination_status: coverage ? verification.manifest.determination_status : 'indeterminate',
120
+ artifactHashes: verification.artifactHashes,
121
+ };
122
+ } catch (error) {
123
+ if (error instanceof VerifiedRunLoadError) throw error;
124
+ if (error instanceof RunVerificationError) {
125
+ throw new VerifiedRunLoadError(error.message, { code: error.code, cause: error });
126
+ }
127
+ throw new VerifiedRunLoadError(error.message, { cause: error });
128
+ }
129
+ }