@nebulacomponents/citable 1.19.0 → 1.21.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.
- package/CHANGELOG.md +35 -0
- package/README.md +12 -1
- package/dist/universal/.agents/skills/citable/SKILL.md +11 -2
- package/dist/universal/.agents/skills/citable/VERSION +1 -1
- package/dist/universal/.agents/skills/citable/manifest.json +24 -12
- package/dist/universal/.agents/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.agents/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.agents/skills/citable/schemas/claim-evidence.schema.json +21 -0
- package/dist/universal/.agents/skills/citable/schemas/claim-verification.schema.json +18 -0
- package/dist/universal/.agents/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.agents/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.agents/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.agents/skills/citable/schemas/derivation.schema.json +18 -0
- package/dist/universal/.agents/skills/citable/schemas/external-comparison.schema.json +16 -0
- package/dist/universal/.agents/skills/citable/schemas/external-observation.schema.json +32 -0
- package/dist/universal/.agents/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.agents/skills/citable/schemas/prompt-set.schema.json +47 -0
- package/dist/universal/.agents/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.agents/skills/citable/schemas/representation-data.schema.json +21 -0
- package/dist/universal/.agents/skills/citable/schemas/research-dataset.schema.json +27 -0
- package/dist/universal/.agents/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.agents/skills/citable/schemas/search-discovery-data.schema.json +20 -0
- package/dist/universal/.agents/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.agents/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.claude/agents/citable/manifest.json +2 -2
- package/dist/universal/.claude/skills/citable/SKILL.md +11 -2
- package/dist/universal/.claude/skills/citable/VERSION +1 -1
- package/dist/universal/.claude/skills/citable/manifest.json +24 -12
- package/dist/universal/.claude/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.claude/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.claude/skills/citable/schemas/claim-evidence.schema.json +21 -0
- package/dist/universal/.claude/skills/citable/schemas/claim-verification.schema.json +18 -0
- package/dist/universal/.claude/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.claude/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.claude/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.claude/skills/citable/schemas/derivation.schema.json +18 -0
- package/dist/universal/.claude/skills/citable/schemas/external-comparison.schema.json +16 -0
- package/dist/universal/.claude/skills/citable/schemas/external-observation.schema.json +32 -0
- package/dist/universal/.claude/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.claude/skills/citable/schemas/prompt-set.schema.json +47 -0
- package/dist/universal/.claude/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.claude/skills/citable/schemas/representation-data.schema.json +21 -0
- package/dist/universal/.claude/skills/citable/schemas/research-dataset.schema.json +27 -0
- package/dist/universal/.claude/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.claude/skills/citable/schemas/search-discovery-data.schema.json +20 -0
- package/dist/universal/.claude/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.claude/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.cursor/skills/citable/SKILL.md +11 -2
- package/dist/universal/.cursor/skills/citable/VERSION +1 -1
- package/dist/universal/.cursor/skills/citable/manifest.json +24 -12
- package/dist/universal/.cursor/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.cursor/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.cursor/skills/citable/schemas/claim-evidence.schema.json +21 -0
- package/dist/universal/.cursor/skills/citable/schemas/claim-verification.schema.json +18 -0
- package/dist/universal/.cursor/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.cursor/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.cursor/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.cursor/skills/citable/schemas/derivation.schema.json +18 -0
- package/dist/universal/.cursor/skills/citable/schemas/external-comparison.schema.json +16 -0
- package/dist/universal/.cursor/skills/citable/schemas/external-observation.schema.json +32 -0
- package/dist/universal/.cursor/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.cursor/skills/citable/schemas/prompt-set.schema.json +47 -0
- package/dist/universal/.cursor/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.cursor/skills/citable/schemas/representation-data.schema.json +21 -0
- package/dist/universal/.cursor/skills/citable/schemas/research-dataset.schema.json +27 -0
- package/dist/universal/.cursor/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.cursor/skills/citable/schemas/search-discovery-data.schema.json +20 -0
- package/dist/universal/.cursor/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.cursor/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.gemini/skills/citable/SKILL.md +11 -2
- package/dist/universal/.gemini/skills/citable/VERSION +1 -1
- package/dist/universal/.gemini/skills/citable/manifest.json +24 -12
- package/dist/universal/.gemini/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.gemini/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.gemini/skills/citable/schemas/claim-evidence.schema.json +21 -0
- package/dist/universal/.gemini/skills/citable/schemas/claim-verification.schema.json +18 -0
- package/dist/universal/.gemini/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.gemini/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.gemini/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.gemini/skills/citable/schemas/derivation.schema.json +18 -0
- package/dist/universal/.gemini/skills/citable/schemas/external-comparison.schema.json +16 -0
- package/dist/universal/.gemini/skills/citable/schemas/external-observation.schema.json +32 -0
- package/dist/universal/.gemini/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.gemini/skills/citable/schemas/prompt-set.schema.json +47 -0
- package/dist/universal/.gemini/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.gemini/skills/citable/schemas/representation-data.schema.json +21 -0
- package/dist/universal/.gemini/skills/citable/schemas/research-dataset.schema.json +27 -0
- package/dist/universal/.gemini/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.gemini/skills/citable/schemas/search-discovery-data.schema.json +20 -0
- package/dist/universal/.gemini/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.gemini/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.github/skills/citable/SKILL.md +11 -2
- package/dist/universal/.github/skills/citable/VERSION +1 -1
- package/dist/universal/.github/skills/citable/manifest.json +24 -12
- package/dist/universal/.github/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.github/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.github/skills/citable/schemas/claim-evidence.schema.json +21 -0
- package/dist/universal/.github/skills/citable/schemas/claim-verification.schema.json +18 -0
- package/dist/universal/.github/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.github/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.github/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.github/skills/citable/schemas/derivation.schema.json +18 -0
- package/dist/universal/.github/skills/citable/schemas/external-comparison.schema.json +16 -0
- package/dist/universal/.github/skills/citable/schemas/external-observation.schema.json +32 -0
- package/dist/universal/.github/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.github/skills/citable/schemas/prompt-set.schema.json +47 -0
- package/dist/universal/.github/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.github/skills/citable/schemas/representation-data.schema.json +21 -0
- package/dist/universal/.github/skills/citable/schemas/research-dataset.schema.json +27 -0
- package/dist/universal/.github/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.github/skills/citable/schemas/search-discovery-data.schema.json +20 -0
- package/dist/universal/.github/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.github/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.kiro/skills/citable/SKILL.md +11 -2
- package/dist/universal/.kiro/skills/citable/VERSION +1 -1
- package/dist/universal/.kiro/skills/citable/manifest.json +24 -12
- package/dist/universal/.kiro/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.kiro/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.kiro/skills/citable/schemas/claim-evidence.schema.json +21 -0
- package/dist/universal/.kiro/skills/citable/schemas/claim-verification.schema.json +18 -0
- package/dist/universal/.kiro/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.kiro/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.kiro/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.kiro/skills/citable/schemas/derivation.schema.json +18 -0
- package/dist/universal/.kiro/skills/citable/schemas/external-comparison.schema.json +16 -0
- package/dist/universal/.kiro/skills/citable/schemas/external-observation.schema.json +32 -0
- package/dist/universal/.kiro/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.kiro/skills/citable/schemas/prompt-set.schema.json +47 -0
- package/dist/universal/.kiro/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.kiro/skills/citable/schemas/representation-data.schema.json +21 -0
- package/dist/universal/.kiro/skills/citable/schemas/research-dataset.schema.json +27 -0
- package/dist/universal/.kiro/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.kiro/skills/citable/schemas/search-discovery-data.schema.json +20 -0
- package/dist/universal/.kiro/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.kiro/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.opencode/skills/citable/SKILL.md +11 -2
- package/dist/universal/.opencode/skills/citable/VERSION +1 -1
- package/dist/universal/.opencode/skills/citable/manifest.json +24 -12
- package/dist/universal/.opencode/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.opencode/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.opencode/skills/citable/schemas/claim-evidence.schema.json +21 -0
- package/dist/universal/.opencode/skills/citable/schemas/claim-verification.schema.json +18 -0
- package/dist/universal/.opencode/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.opencode/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.opencode/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.opencode/skills/citable/schemas/derivation.schema.json +18 -0
- package/dist/universal/.opencode/skills/citable/schemas/external-comparison.schema.json +16 -0
- package/dist/universal/.opencode/skills/citable/schemas/external-observation.schema.json +32 -0
- package/dist/universal/.opencode/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.opencode/skills/citable/schemas/prompt-set.schema.json +47 -0
- package/dist/universal/.opencode/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.opencode/skills/citable/schemas/representation-data.schema.json +21 -0
- package/dist/universal/.opencode/skills/citable/schemas/research-dataset.schema.json +27 -0
- package/dist/universal/.opencode/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.opencode/skills/citable/schemas/search-discovery-data.schema.json +20 -0
- package/dist/universal/.opencode/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.opencode/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.pi/agent/skills/citable/SKILL.md +11 -2
- package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
- package/dist/universal/.pi/agent/skills/citable/manifest.json +24 -12
- package/dist/universal/.pi/agent/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/claim-evidence.schema.json +21 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/claim-verification.schema.json +18 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/derivation.schema.json +18 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/external-comparison.schema.json +16 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/external-observation.schema.json +32 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.pi/agent/skills/citable/schemas/prompt-set.schema.json +47 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/representation-data.schema.json +21 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/research-dataset.schema.json +27 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.pi/agent/skills/citable/schemas/search-discovery-data.schema.json +20 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.qoder/skills/citable/SKILL.md +11 -2
- package/dist/universal/.qoder/skills/citable/VERSION +1 -1
- package/dist/universal/.qoder/skills/citable/manifest.json +24 -12
- package/dist/universal/.qoder/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.qoder/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.qoder/skills/citable/schemas/claim-evidence.schema.json +21 -0
- package/dist/universal/.qoder/skills/citable/schemas/claim-verification.schema.json +18 -0
- package/dist/universal/.qoder/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.qoder/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.qoder/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.qoder/skills/citable/schemas/derivation.schema.json +18 -0
- package/dist/universal/.qoder/skills/citable/schemas/external-comparison.schema.json +16 -0
- package/dist/universal/.qoder/skills/citable/schemas/external-observation.schema.json +32 -0
- package/dist/universal/.qoder/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.qoder/skills/citable/schemas/prompt-set.schema.json +47 -0
- package/dist/universal/.qoder/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.qoder/skills/citable/schemas/representation-data.schema.json +21 -0
- package/dist/universal/.qoder/skills/citable/schemas/research-dataset.schema.json +27 -0
- package/dist/universal/.qoder/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.qoder/skills/citable/schemas/search-discovery-data.schema.json +20 -0
- package/dist/universal/.qoder/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.qoder/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.rovodev/skills/citable/SKILL.md +11 -2
- package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
- package/dist/universal/.rovodev/skills/citable/manifest.json +24 -12
- package/dist/universal/.rovodev/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.rovodev/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.rovodev/skills/citable/schemas/claim-evidence.schema.json +21 -0
- package/dist/universal/.rovodev/skills/citable/schemas/claim-verification.schema.json +18 -0
- package/dist/universal/.rovodev/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.rovodev/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.rovodev/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.rovodev/skills/citable/schemas/derivation.schema.json +18 -0
- package/dist/universal/.rovodev/skills/citable/schemas/external-comparison.schema.json +16 -0
- package/dist/universal/.rovodev/skills/citable/schemas/external-observation.schema.json +32 -0
- package/dist/universal/.rovodev/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.rovodev/skills/citable/schemas/prompt-set.schema.json +47 -0
- package/dist/universal/.rovodev/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.rovodev/skills/citable/schemas/representation-data.schema.json +21 -0
- package/dist/universal/.rovodev/skills/citable/schemas/research-dataset.schema.json +27 -0
- package/dist/universal/.rovodev/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.rovodev/skills/citable/schemas/search-discovery-data.schema.json +20 -0
- package/dist/universal/.rovodev/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.rovodev/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.trae/skills/citable/SKILL.md +11 -2
- package/dist/universal/.trae/skills/citable/VERSION +1 -1
- package/dist/universal/.trae/skills/citable/manifest.json +24 -12
- package/dist/universal/.trae/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.trae/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.trae/skills/citable/schemas/claim-evidence.schema.json +21 -0
- package/dist/universal/.trae/skills/citable/schemas/claim-verification.schema.json +18 -0
- package/dist/universal/.trae/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.trae/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.trae/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.trae/skills/citable/schemas/derivation.schema.json +18 -0
- package/dist/universal/.trae/skills/citable/schemas/external-comparison.schema.json +16 -0
- package/dist/universal/.trae/skills/citable/schemas/external-observation.schema.json +32 -0
- package/dist/universal/.trae/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.trae/skills/citable/schemas/prompt-set.schema.json +47 -0
- package/dist/universal/.trae/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.trae/skills/citable/schemas/representation-data.schema.json +21 -0
- package/dist/universal/.trae/skills/citable/schemas/research-dataset.schema.json +27 -0
- package/dist/universal/.trae/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.trae/skills/citable/schemas/search-discovery-data.schema.json +20 -0
- package/dist/universal/.trae/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.trae/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/.trae-cn/skills/citable/SKILL.md +11 -2
- package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
- package/dist/universal/.trae-cn/skills/citable/manifest.json +24 -12
- package/dist/universal/.trae-cn/skills/citable/schemas/action-plan.schema.json +38 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/audit-coverage.schema.json +118 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/claim-evidence.schema.json +21 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/claim-verification.schema.json +18 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/collection-result.schema.json +35 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/config.schema.json +2 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/cro-report.schema.json +8 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/derivation.schema.json +18 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/external-comparison.schema.json +16 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/external-observation.schema.json +32 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/finding.schema.json +105 -2
- package/dist/universal/.trae-cn/skills/citable/schemas/prompt-set.schema.json +47 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/remediation-verification.schema.json +12 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/representation-data.schema.json +21 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/research-dataset.schema.json +27 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/run.schema.json +15 -2
- package/dist/universal/.trae-cn/skills/citable/schemas/search-discovery-data.schema.json +20 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/search-report.schema.json +8 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/summary.schema.json +41 -0
- package/dist/universal/manifest.json +292 -148
- package/package.json +1 -1
- package/schemas/action-plan.schema.json +38 -1
- package/schemas/audit-coverage.schema.json +118 -0
- package/schemas/claim-evidence.schema.json +21 -0
- package/schemas/claim-verification.schema.json +18 -0
- package/schemas/collection-result.schema.json +35 -0
- package/schemas/config.schema.json +2 -0
- package/schemas/cro-report.schema.json +8 -1
- package/schemas/derivation.schema.json +18 -0
- package/schemas/external-comparison.schema.json +16 -0
- package/schemas/external-observation.schema.json +32 -0
- package/schemas/finding.schema.json +105 -2
- package/schemas/prompt-set.schema.json +47 -0
- package/schemas/remediation-verification.schema.json +12 -1
- package/schemas/representation-data.schema.json +21 -0
- package/schemas/research-dataset.schema.json +27 -0
- package/schemas/run.schema.json +15 -2
- package/schemas/search-discovery-data.schema.json +20 -0
- package/schemas/search-report.schema.json +8 -1
- package/schemas/summary.schema.json +41 -0
- package/skill/SKILL.md +11 -2
- package/src/acceptance/reproducibility.js +5 -25
- package/src/analysis/croRoadmap.js +3 -3
- package/src/analysis/strategicRoadmap.js +3 -3
- package/src/artifacts/interchange.js +2 -6
- package/src/cli/index.js +15 -3
- package/src/commands/actionPlan.js +46 -5
- package/src/commands/applyRemediation.js +5 -5
- package/src/commands/audit.js +75 -8
- package/src/commands/ciWorkflow.js +7 -2
- package/src/commands/compareSnapshots.js +155 -47
- package/src/commands/connect.js +24 -4
- package/src/commands/context.js +41 -2
- package/src/commands/croSuite.js +51 -13
- package/src/commands/delivery.js +7 -7
- package/src/commands/governance.js +9 -10
- package/src/commands/implementationKit.js +2 -1
- package/src/commands/init.js +8 -1
- package/src/commands/previewCro.js +1 -1
- package/src/commands/prioritize.js +3 -2
- package/src/commands/reviews.js +31 -5
- package/src/commands/roadmapCmd.js +7 -13
- package/src/commands/sweep.js +15 -2
- package/src/commands/verifyRemediation.js +94 -19
- package/src/connectors/collectionResult.js +80 -0
- package/src/connectors/ga4.js +46 -11
- package/src/connectors/gsc.js +45 -6
- package/src/connectors/index.js +1 -1
- package/src/connectors/webflow.js +140 -10
- package/src/connectors/wordpress.js +92 -8
- package/src/crawler/fetch.js +220 -24
- package/src/crawler/resourceValidity.js +119 -0
- package/src/crawler/sitemap.js +435 -16
- package/src/crawler/sitemapCollector.js +364 -0
- package/src/crawler/urlIdentity.js +97 -0
- package/src/detectors/agent.js +11 -0
- package/src/detectors/ans.js +16 -1
- package/src/detectors/arch.js +6 -0
- package/src/detectors/claim.js +9 -0
- package/src/detectors/crawl.js +7 -0
- package/src/detectors/cro.js +21 -0
- package/src/detectors/cwv.js +4 -0
- package/src/detectors/entity.js +7 -0
- package/src/detectors/evd.js +9 -0
- package/src/detectors/framework.js +28 -2
- package/src/detectors/geoReco.js +16 -0
- package/src/detectors/hreflang.js +3 -0
- package/src/detectors/lifeMeas.js +9 -0
- package/src/detectors/link.js +6 -1
- package/src/detectors/page.js +11 -0
- package/src/detectors/schemaData.js +16 -1
- package/src/detectors/tech.js +34 -3
- package/src/discovery/searchConsole.js +186 -0
- package/src/evidence/coverage.js +448 -0
- package/src/evidence/determination.js +157 -0
- package/src/evidence/downstream.js +110 -0
- package/src/evidence/external.js +251 -0
- package/src/evidence/hashes.js +109 -0
- package/src/evidence/run.js +68 -8
- package/src/extractor/page.js +29 -2
- package/src/extractor/site.js +298 -50
- package/src/reporting/executiveCroReport.js +36 -13
- package/src/reporting/executiveExport.js +45 -16
- package/src/reporting/executiveSearchReport.js +56 -22
- package/src/reporting/report.js +56 -9
- package/src/representation/prompts.js +117 -0
- package/src/representation/representation.js +186 -0
- package/src/shared/epistemicLanguage.js +86 -0
- package/src/shared/evidenceSourceResolver.js +48 -17
- package/src/shared/htmlEscape.js +8 -4
- package/src/shared/runPackageVerifier.js +99 -8
- package/src/shared/verifiedRunLoader.js +129 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { providerRequest } from './http.js';
|
|
2
2
|
import { sha256, nowIso } from '../shared/io.js';
|
|
3
|
+
import { collectionResult, errorMessage, paginationBoundary } from './collectionResult.js';
|
|
3
4
|
|
|
4
5
|
const BASE = 'https://api.webflow.com/v2';
|
|
6
|
+
const API_ORIGIN = new URL(BASE).origin;
|
|
5
7
|
|
|
6
8
|
const METRICS = {
|
|
7
9
|
pages: { unit: 'count', value_type: 'integer' },
|
|
@@ -24,6 +26,96 @@ function computeWebflowPayloadHash(payload) {
|
|
|
24
26
|
return sha256(JSON.stringify(canonical));
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
function safeContinuation(url, { preserveOrigin = true } = {}) {
|
|
30
|
+
try {
|
|
31
|
+
const parsed = new URL(url);
|
|
32
|
+
return { ...(preserveOrigin ? { url: `${parsed.origin}${parsed.pathname}` } : { path: parsed.pathname }), query_redacted: parsed.search.length > 0 };
|
|
33
|
+
} catch {
|
|
34
|
+
return { url: '[invalid continuation]', query_redacted: true };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function collectWebflowList(url, field, context) {
|
|
39
|
+
const maxPages = paginationBoundary(context);
|
|
40
|
+
const items = [];
|
|
41
|
+
const errors = [];
|
|
42
|
+
const limitations = [];
|
|
43
|
+
let providerTotal = null;
|
|
44
|
+
let pagesRequested = 0;
|
|
45
|
+
let pagesRetrieved = 0;
|
|
46
|
+
let offset = 0;
|
|
47
|
+
let cursor = null;
|
|
48
|
+
let nextUrl = null;
|
|
49
|
+
let continuationState = null;
|
|
50
|
+
let providerCompleteness = 'declared';
|
|
51
|
+
let connectorError = null;
|
|
52
|
+
while (pagesRequested < maxPages) {
|
|
53
|
+
pagesRequested += 1;
|
|
54
|
+
const query = cursor
|
|
55
|
+
? `?limit=100&cursor=${encodeURIComponent(cursor)}`
|
|
56
|
+
: `?limit=100${offset ? `&offset=${offset}` : ''}`;
|
|
57
|
+
let result;
|
|
58
|
+
try {
|
|
59
|
+
if (nextUrl) {
|
|
60
|
+
let continuation;
|
|
61
|
+
try { continuation = new URL(nextUrl); } catch {
|
|
62
|
+
errors.push(`${field} page ${pagesRequested}: malformed continuation URL`);
|
|
63
|
+
continuationState = { next: safeContinuation(nextUrl), reason: 'invalid_url' };
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
if (continuation.origin !== API_ORIGIN || continuation.protocol !== 'https:') {
|
|
67
|
+
errors.push(`${field} page ${pagesRequested}: continuation origin is outside the Webflow API`);
|
|
68
|
+
continuationState = { next: safeContinuation(nextUrl, { preserveOrigin: false }), reason: 'origin_mismatch' };
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
result = await providerRequest(nextUrl || `${url}${query}`, context);
|
|
73
|
+
const pageItems = Array.isArray(result?.[field]) ? result[field] : [];
|
|
74
|
+
pagesRetrieved += 1;
|
|
75
|
+
items.push(...pageItems);
|
|
76
|
+
const pagination = result.pagination || {};
|
|
77
|
+
const reported = result.total ?? pagination.total ?? pagination.totalCount ?? null;
|
|
78
|
+
if (Number.isInteger(reported) && reported >= 0) providerTotal = reported;
|
|
79
|
+
const next = result.nextCursor ?? pagination.nextCursor ?? result.next?.cursor ?? null;
|
|
80
|
+
const providerNextUrl = typeof result.next === 'string' ? result.next : (typeof pagination.next === 'string' ? pagination.next : null);
|
|
81
|
+
const nextOffset = result.nextOffset ?? pagination.nextOffset ?? null;
|
|
82
|
+
nextUrl = null;
|
|
83
|
+
if (next !== null && (typeof next !== 'string' || !next.trim())) {
|
|
84
|
+
errors.push(`${field} page ${pagesRequested}: malformed continuation cursor`);
|
|
85
|
+
continuationState = { cursor: next };
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
if (providerNextUrl !== null && (!/^https:\/\//i.test(providerNextUrl))) {
|
|
89
|
+
errors.push(`${field} page ${pagesRequested}: malformed continuation URL`);
|
|
90
|
+
continuationState = { next: safeContinuation(providerNextUrl, { preserveOrigin: false }) };
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
if (next) cursor = String(next);
|
|
94
|
+
else if (providerNextUrl) { nextUrl = providerNextUrl; cursor = null; }
|
|
95
|
+
else if (nextOffset !== null && (!Number.isInteger(nextOffset) || nextOffset <= offset)) {
|
|
96
|
+
errors.push(`${field} page ${pagesRequested}: malformed continuation offset`);
|
|
97
|
+
continuationState = { offset: nextOffset };
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
else if (providerTotal !== null && items.length >= providerTotal) break;
|
|
101
|
+
else if (Number.isInteger(nextOffset) && nextOffset >= 0) { offset = nextOffset; cursor = null; }
|
|
102
|
+
else if (pageItems.length >= 100 && (providerTotal === null || items.length < providerTotal)) offset += pageItems.length;
|
|
103
|
+
else { providerCompleteness = 'unknown'; break; }
|
|
104
|
+
if (providerTotal !== null && items.length >= providerTotal) break;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
connectorError ||= error;
|
|
107
|
+
errors.push(`${field} page ${pagesRequested}: ${errorMessage(error)}`);
|
|
108
|
+
continuationState = cursor ? { cursor } : nextUrl ? { next: safeContinuation(nextUrl) } : { offset };
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!continuationState && pagesRequested >= maxPages && (providerTotal === null || items.length < providerTotal)) {
|
|
113
|
+
continuationState = cursor ? { cursor } : nextUrl ? { next: safeContinuation(nextUrl) } : { offset: offset + 100 };
|
|
114
|
+
limitations.push(`Webflow ${field} collection reached the ${maxPages}-page boundary.`);
|
|
115
|
+
}
|
|
116
|
+
return { items, providerTotal, providerCompleteness, connectorError, pagesRequested, pagesRetrieved, continuationState, limitations, errors };
|
|
117
|
+
}
|
|
118
|
+
|
|
27
119
|
export const webflowConnector = {
|
|
28
120
|
provider: 'webflow',
|
|
29
121
|
defaultCredentialEnv: 'WEBFLOW_API_TOKEN',
|
|
@@ -62,10 +154,26 @@ export const webflowConnector = {
|
|
|
62
154
|
const siteId = connection.property_id;
|
|
63
155
|
const rows = [];
|
|
64
156
|
const metricNames = new Set(metrics.map((m) => m.external_name));
|
|
157
|
+
const allItems = [];
|
|
158
|
+
const errors = [];
|
|
159
|
+
const limitations = [];
|
|
160
|
+
let pagesRequested = 0;
|
|
161
|
+
let pagesRetrieved = 0;
|
|
162
|
+
let continuationState = null;
|
|
163
|
+
let providerTotal = 0;
|
|
164
|
+
let providerTotalKnown = true;
|
|
165
|
+
let connectorError = null;
|
|
65
166
|
|
|
66
167
|
if (metricNames.has('pages')) {
|
|
67
|
-
const pageResult = await
|
|
68
|
-
const pages = pageResult.
|
|
168
|
+
const pageResult = await collectWebflowList(`${BASE}/sites/${encodeURIComponent(siteId)}/pages`, 'pages', context);
|
|
169
|
+
const pages = pageResult.items;
|
|
170
|
+
allItems.push(...pages.map((item) => ({ ...item, _collection_type: 'pages' })));
|
|
171
|
+
errors.push(...pageResult.errors); limitations.push(...pageResult.limitations);
|
|
172
|
+
pagesRequested += pageResult.pagesRequested; pagesRetrieved += pageResult.pagesRetrieved;
|
|
173
|
+
continuationState ||= pageResult.continuationState;
|
|
174
|
+
connectorError ||= pageResult.connectorError;
|
|
175
|
+
if (pageResult.providerCompleteness === 'unknown') providerTotalKnown = false;
|
|
176
|
+
if (pageResult.providerTotal === null) providerTotalKnown = false; else providerTotal += pageResult.providerTotal;
|
|
69
177
|
const pageMetric = metrics.find((m) => m.external_name === 'pages');
|
|
70
178
|
rows.push({
|
|
71
179
|
metric: pageMetric,
|
|
@@ -76,8 +184,15 @@ export const webflowConnector = {
|
|
|
76
184
|
}
|
|
77
185
|
|
|
78
186
|
if (metricNames.has('collections') || metricNames.has('items')) {
|
|
79
|
-
const collectionsResult = await
|
|
80
|
-
const collections = collectionsResult.
|
|
187
|
+
const collectionsResult = await collectWebflowList(`${BASE}/sites/${encodeURIComponent(siteId)}/collections`, 'collections', context);
|
|
188
|
+
const collections = collectionsResult.items;
|
|
189
|
+
allItems.push(...collections.map((item) => ({ ...item, _collection_type: 'collections' })));
|
|
190
|
+
errors.push(...collectionsResult.errors); limitations.push(...collectionsResult.limitations);
|
|
191
|
+
pagesRequested += collectionsResult.pagesRequested; pagesRetrieved += collectionsResult.pagesRetrieved;
|
|
192
|
+
continuationState ||= collectionsResult.continuationState;
|
|
193
|
+
connectorError ||= collectionsResult.connectorError;
|
|
194
|
+
if (collectionsResult.providerCompleteness === 'unknown') providerTotalKnown = false;
|
|
195
|
+
if (collectionsResult.providerTotal === null) providerTotalKnown = false; else providerTotal += collectionsResult.providerTotal;
|
|
81
196
|
|
|
82
197
|
if (metricNames.has('collections')) {
|
|
83
198
|
const colMetric = metrics.find((m) => m.external_name === 'collections');
|
|
@@ -92,8 +207,15 @@ export const webflowConnector = {
|
|
|
92
207
|
if (metricNames.has('items')) {
|
|
93
208
|
let totalItems = 0;
|
|
94
209
|
for (const col of collections) {
|
|
95
|
-
const itemsResult = await
|
|
96
|
-
totalItems +=
|
|
210
|
+
const itemsResult = await collectWebflowList(`${BASE}/collections/${encodeURIComponent(col.id)}/items`, 'items', context);
|
|
211
|
+
totalItems += itemsResult.items.length;
|
|
212
|
+
allItems.push(...itemsResult.items.map((item) => ({ ...item, _collection_type: 'items', _collection_id: col.id })));
|
|
213
|
+
errors.push(...itemsResult.errors); limitations.push(...itemsResult.limitations);
|
|
214
|
+
pagesRequested += itemsResult.pagesRequested; pagesRetrieved += itemsResult.pagesRetrieved;
|
|
215
|
+
continuationState ||= itemsResult.continuationState;
|
|
216
|
+
connectorError ||= itemsResult.connectorError;
|
|
217
|
+
if (itemsResult.providerCompleteness === 'unknown') providerTotalKnown = false;
|
|
218
|
+
if (itemsResult.providerTotal === null) providerTotalKnown = false; else providerTotal += itemsResult.providerTotal;
|
|
97
219
|
}
|
|
98
220
|
const itemMetric = metrics.find((m) => m.external_name === 'items');
|
|
99
221
|
rows.push({
|
|
@@ -105,13 +227,21 @@ export const webflowConnector = {
|
|
|
105
227
|
}
|
|
106
228
|
}
|
|
107
229
|
|
|
230
|
+
const collection = collectionResult({
|
|
231
|
+
items: allItems,
|
|
232
|
+
paginationState: { pages_requested: pagesRequested, pages_retrieved: pagesRetrieved, boundary: { max_pages: paginationBoundary(context) } },
|
|
233
|
+
providerReportedTotal: providerTotalKnown ? providerTotal : null,
|
|
234
|
+
continuationState,
|
|
235
|
+
providerCompleteness: providerTotalKnown ? 'declared' : 'unknown',
|
|
236
|
+
limitations: [...new Set([...limitations, 'Webflow API enforces rate limits of 60 requests per minute.', 'Staged CMS changes require site publish to appear on live domains.'])],
|
|
237
|
+
errors,
|
|
238
|
+
});
|
|
108
239
|
return {
|
|
109
240
|
rows,
|
|
110
241
|
cursor: endDate,
|
|
111
|
-
limitations:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
],
|
|
242
|
+
limitations: collection.limitations,
|
|
243
|
+
collection,
|
|
244
|
+
...(connectorError ? { connectorError } : {}),
|
|
115
245
|
};
|
|
116
246
|
},
|
|
117
247
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { providerRequest } from './http.js';
|
|
2
2
|
import { sha256, nowIso } from '../shared/io.js';
|
|
3
|
+
import { collectionResult, errorMessage, paginationBoundary } from './collectionResult.js';
|
|
3
4
|
|
|
4
5
|
const METRICS = {
|
|
5
6
|
published_posts: { unit: 'count', value_type: 'integer' },
|
|
@@ -25,6 +26,53 @@ function computeCmsPayloadHash(payload) {
|
|
|
25
26
|
return sha256(JSON.stringify(canonical));
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
async function collectWordPressEndpoint(base, endpoint, context) {
|
|
30
|
+
const maxPages = paginationBoundary(context);
|
|
31
|
+
const items = [];
|
|
32
|
+
const limitations = [];
|
|
33
|
+
const errors = [];
|
|
34
|
+
let providerTotal = null;
|
|
35
|
+
let totalPages = null;
|
|
36
|
+
let page = 1;
|
|
37
|
+
let pagesRequested = 0;
|
|
38
|
+
let pagesRetrieved = 0;
|
|
39
|
+
let continuationState = null;
|
|
40
|
+
let providerCompleteness = 'declared';
|
|
41
|
+
let connectorError = null;
|
|
42
|
+
|
|
43
|
+
while (page <= maxPages) {
|
|
44
|
+
pagesRequested += 1;
|
|
45
|
+
const suffix = page === 1 ? '?per_page=100&status=publish' : `?per_page=100&status=publish&page=${page}`;
|
|
46
|
+
try {
|
|
47
|
+
const response = await providerRequest(`${base}/wp-json/wp/v2/${endpoint}${suffix}`, { ...context, rawResponse: true });
|
|
48
|
+
const pageItems = await response.json();
|
|
49
|
+
if (!Array.isArray(pageItems)) throw new Error(`WordPress ${endpoint} page ${page} did not return an array`);
|
|
50
|
+
pagesRetrieved += 1;
|
|
51
|
+
items.push(...pageItems.map((item) => ({ ...item, _collection_endpoint: endpoint })));
|
|
52
|
+
const headerTotal = Number(response.headers.get('x-wp-total'));
|
|
53
|
+
const headerPages = Number(response.headers.get('x-wp-totalpages'));
|
|
54
|
+
if (Number.isInteger(headerTotal) && headerTotal >= 0) providerTotal = headerTotal;
|
|
55
|
+
if (Number.isInteger(headerPages) && headerPages >= 1) totalPages = headerPages;
|
|
56
|
+
if (!totalPages) providerCompleteness = 'unknown';
|
|
57
|
+
if (pageItems.length === 0 || (totalPages && page >= totalPages)) break;
|
|
58
|
+
if (!totalPages && pageItems.length >= 100) {
|
|
59
|
+
continuationState = { endpoint, page: page + 1 };
|
|
60
|
+
limitations.push('WordPress did not report total pages for a full page of results.');
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
page += 1;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
connectorError ||= error;
|
|
66
|
+
errors.push(`${endpoint} page ${page}: ${errorMessage(error)}`);
|
|
67
|
+
continuationState = { endpoint, page };
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (!continuationState && totalPages && page < totalPages) continuationState = { endpoint, page };
|
|
72
|
+
if (continuationState && page > maxPages) limitations.push(`WordPress ${endpoint} collection reached the ${maxPages}-page boundary.`);
|
|
73
|
+
return { items, providerTotal, providerCompleteness, connectorError, pagesRequested, pagesRetrieved, continuationState, limitations, errors };
|
|
74
|
+
}
|
|
75
|
+
|
|
28
76
|
export const wordpressConnector = {
|
|
29
77
|
provider: 'wordpress',
|
|
30
78
|
defaultCredentialEnv: 'WORDPRESS_AUTH_TOKEN',
|
|
@@ -78,35 +126,71 @@ export const wordpressConnector = {
|
|
|
78
126
|
const rows = [];
|
|
79
127
|
const metricNames = new Set(metrics.map((m) => m.external_name));
|
|
80
128
|
|
|
129
|
+
const collections = [];
|
|
130
|
+
const collectionErrors = [];
|
|
131
|
+
const collectionLimitations = [];
|
|
132
|
+
let pagesRequested = 0;
|
|
133
|
+
let pagesRetrieved = 0;
|
|
134
|
+
let continuationState = null;
|
|
135
|
+
let providerTotal = 0;
|
|
136
|
+
let providerTotalKnown = true;
|
|
137
|
+
let connectorError = null;
|
|
138
|
+
|
|
81
139
|
if (metricNames.has('published_posts')) {
|
|
82
|
-
const
|
|
140
|
+
const postsResult = await collectWordPressEndpoint(base, 'posts', context);
|
|
141
|
+
collections.push(...postsResult.items);
|
|
142
|
+
collectionErrors.push(...postsResult.errors);
|
|
143
|
+
collectionLimitations.push(...postsResult.limitations);
|
|
144
|
+
pagesRequested += postsResult.pagesRequested;
|
|
145
|
+
pagesRetrieved += postsResult.pagesRetrieved;
|
|
146
|
+
continuationState ||= postsResult.continuationState;
|
|
147
|
+
connectorError ||= postsResult.connectorError;
|
|
148
|
+
if (postsResult.providerCompleteness === 'unknown') providerTotalKnown = false;
|
|
149
|
+
if (postsResult.providerTotal === null) providerTotalKnown = false; else providerTotal += postsResult.providerTotal;
|
|
83
150
|
const postMetric = metrics.find((m) => m.external_name === 'published_posts');
|
|
84
151
|
rows.push({
|
|
85
152
|
metric: postMetric,
|
|
86
|
-
value:
|
|
153
|
+
value: postsResult.items.length,
|
|
87
154
|
dimensions: { date: endDate },
|
|
88
155
|
observed_at: `${endDate}T00:00:00.000Z`,
|
|
89
156
|
});
|
|
90
157
|
}
|
|
91
158
|
|
|
92
159
|
if (metricNames.has('published_pages')) {
|
|
93
|
-
const
|
|
160
|
+
const pagesResult = await collectWordPressEndpoint(base, 'pages', context);
|
|
161
|
+
collections.push(...pagesResult.items);
|
|
162
|
+
collectionErrors.push(...pagesResult.errors);
|
|
163
|
+
collectionLimitations.push(...pagesResult.limitations);
|
|
164
|
+
pagesRequested += pagesResult.pagesRequested;
|
|
165
|
+
pagesRetrieved += pagesResult.pagesRetrieved;
|
|
166
|
+
continuationState ||= pagesResult.continuationState;
|
|
167
|
+
connectorError ||= pagesResult.connectorError;
|
|
168
|
+
if (pagesResult.providerCompleteness === 'unknown') providerTotalKnown = false;
|
|
169
|
+
if (pagesResult.providerTotal === null) providerTotalKnown = false; else providerTotal += pagesResult.providerTotal;
|
|
94
170
|
const pageMetric = metrics.find((m) => m.external_name === 'published_pages');
|
|
95
171
|
rows.push({
|
|
96
172
|
metric: pageMetric,
|
|
97
|
-
value:
|
|
173
|
+
value: pagesResult.items.length,
|
|
98
174
|
dimensions: { date: endDate },
|
|
99
175
|
observed_at: `${endDate}T00:00:00.000Z`,
|
|
100
176
|
});
|
|
101
177
|
}
|
|
102
178
|
|
|
179
|
+
const collection = collectionResult({
|
|
180
|
+
items: collections,
|
|
181
|
+
paginationState: { pages_requested: pagesRequested, pages_retrieved: pagesRetrieved, boundary: { max_pages: paginationBoundary(context) } },
|
|
182
|
+
providerReportedTotal: providerTotalKnown ? providerTotal : null,
|
|
183
|
+
continuationState,
|
|
184
|
+
providerCompleteness: providerTotalKnown ? 'declared' : 'unknown',
|
|
185
|
+
limitations: [...collectionLimitations, 'WordPress REST API collection is bounded to accessible posts and pages.', 'Custom post types outside standard posts and pages require dedicated endpoint parameters.'],
|
|
186
|
+
errors: collectionErrors,
|
|
187
|
+
});
|
|
103
188
|
return {
|
|
104
189
|
rows,
|
|
105
190
|
cursor: endDate,
|
|
106
|
-
limitations:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
],
|
|
191
|
+
limitations: collection.limitations,
|
|
192
|
+
collection,
|
|
193
|
+
...(connectorError ? { connectorError } : {}),
|
|
110
194
|
};
|
|
111
195
|
},
|
|
112
196
|
|
package/src/crawler/fetch.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import dns from 'node:dns';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import https from 'node:https';
|
|
2
4
|
import net from 'node:net';
|
|
5
|
+
import { Readable } from 'node:stream';
|
|
6
|
+
import { createBrotliDecompress, createGunzip, createInflate } from 'node:zlib';
|
|
3
7
|
|
|
4
8
|
const blockedAddresses = new net.BlockList();
|
|
9
|
+
const GUARDED_LOOKUP_TRANSPORT = Symbol('citable.guardedLookupTransport');
|
|
5
10
|
for (const [network, prefix] of [
|
|
6
11
|
['0.0.0.0', 8], ['10.0.0.0', 8], ['100.64.0.0', 10], ['127.0.0.0', 8],
|
|
7
12
|
['169.254.0.0', 16], ['172.16.0.0', 12], ['192.0.0.0', 24], ['192.0.2.0', 24],
|
|
@@ -22,18 +27,89 @@ function isBlockedAddress(address) {
|
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
export async function validatePublicUrl(url, { lookup = dns.promises.lookup } = {}) {
|
|
30
|
+
const parsed = parseNetworkUrl(url);
|
|
31
|
+
await resolvePublicAddresses(parsed.hostname, lookup);
|
|
32
|
+
return parsed;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseNetworkUrl(url) {
|
|
25
36
|
const parsed = new URL(url);
|
|
26
37
|
if (!['http:', 'https:'].includes(parsed.protocol)) throw new Error(`unsupported URL protocol: ${parsed.protocol}`);
|
|
27
38
|
if (parsed.username || parsed.password) throw new Error('URL credentials are not permitted');
|
|
28
|
-
|
|
39
|
+
return parsed;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function resolvePublicAddresses(rawHostname, lookup, options = {}) {
|
|
43
|
+
const hostname = rawHostname.replace(/^\[|\]$/g, '');
|
|
29
44
|
const literalFamily = net.isIP(hostname);
|
|
30
45
|
const addresses = literalFamily
|
|
31
46
|
? [{ address: hostname, family: literalFamily }]
|
|
32
|
-
: await lookup(hostname, { all: true, verbatim: true });
|
|
47
|
+
: await lookup(hostname, { ...options, all: true, verbatim: true });
|
|
33
48
|
if (!addresses.length || addresses.some(({ address }) => isBlockedAddress(address))) {
|
|
34
49
|
throw new Error(`refusing private, loopback, or non-public destination: ${hostname}`);
|
|
35
50
|
}
|
|
36
|
-
return
|
|
51
|
+
return addresses;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createGuardedLookup(lookup) {
|
|
55
|
+
return (hostname, options, callback) => {
|
|
56
|
+
const normalizedOptions = typeof options === 'number' ? { family: options } : (options ?? {});
|
|
57
|
+
resolvePublicAddresses(hostname, lookup, { family: normalizedOptions.family || 0 })
|
|
58
|
+
.then((addresses) => {
|
|
59
|
+
if (normalizedOptions.all) callback(null, addresses);
|
|
60
|
+
else callback(null, addresses[0].address, addresses[0].family);
|
|
61
|
+
})
|
|
62
|
+
.catch((error) => callback(error));
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function fetchWithGuardedLookup(url, { headers, signal, lookup }) {
|
|
67
|
+
const parsed = new URL(url);
|
|
68
|
+
const transport = parsed.protocol === 'https:' ? https : http;
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const request = transport.request(parsed, {
|
|
71
|
+
method: 'GET',
|
|
72
|
+
headers,
|
|
73
|
+
lookup,
|
|
74
|
+
servername: parsed.protocol === 'https:' ? parsed.hostname : undefined,
|
|
75
|
+
signal,
|
|
76
|
+
}, (response) => {
|
|
77
|
+
const responseHeaders = new Headers();
|
|
78
|
+
for (const [name, value] of Object.entries(response.headers)) {
|
|
79
|
+
if (Array.isArray(value)) for (const item of value) responseHeaders.append(name, item);
|
|
80
|
+
else if (value !== undefined) responseHeaders.set(name, value);
|
|
81
|
+
}
|
|
82
|
+
const encoding = String(response.headers['content-encoding'] ?? '').toLowerCase();
|
|
83
|
+
let body = response;
|
|
84
|
+
if (encoding === 'gzip' || encoding === 'x-gzip') body = response.pipe(createGunzip());
|
|
85
|
+
else if (encoding === 'deflate') body = response.pipe(createInflate());
|
|
86
|
+
else if (encoding === 'br') body = response.pipe(createBrotliDecompress());
|
|
87
|
+
const noBody = response.statusCode === 204 || response.statusCode === 205 || response.statusCode === 304;
|
|
88
|
+
resolve(new Response(noBody ? null : Readable.toWeb(body), {
|
|
89
|
+
status: response.statusCode,
|
|
90
|
+
statusText: response.statusMessage,
|
|
91
|
+
headers: responseHeaders,
|
|
92
|
+
}));
|
|
93
|
+
});
|
|
94
|
+
request.on('error', reject);
|
|
95
|
+
request.end();
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Declare that a custom transport uses the `lookup` callback passed by fetchUrl
|
|
101
|
+
* for its actual socket connection. Undeclared transports are rejected before
|
|
102
|
+
* execution because URL preflight alone does not prevent DNS rebinding.
|
|
103
|
+
*/
|
|
104
|
+
export function declareGuardedLookupTransport(fetchImpl) {
|
|
105
|
+
if (typeof fetchImpl !== 'function') throw new TypeError('custom transport must be a function');
|
|
106
|
+
Object.defineProperty(fetchImpl, GUARDED_LOOKUP_TRANSPORT, {
|
|
107
|
+
value: true,
|
|
108
|
+
enumerable: false,
|
|
109
|
+
configurable: false,
|
|
110
|
+
writable: false,
|
|
111
|
+
});
|
|
112
|
+
return fetchImpl;
|
|
37
113
|
}
|
|
38
114
|
|
|
39
115
|
/** Enforce the public-network policy for each Playwright request. */
|
|
@@ -62,12 +138,12 @@ export async function handlePublicBrowserRoute(route, { lookup = dns.promises.lo
|
|
|
62
138
|
}
|
|
63
139
|
}
|
|
64
140
|
|
|
65
|
-
async function readBodyLimited(res, maxBodyBytes) {
|
|
141
|
+
async function readBodyLimited(res, maxBodyBytes, responseType = 'text') {
|
|
66
142
|
const declared = Number(res.headers.get('content-length'));
|
|
67
143
|
if (Number.isFinite(declared) && declared > maxBodyBytes) {
|
|
68
|
-
throw
|
|
144
|
+
throw bodyLimitError(maxBodyBytes);
|
|
69
145
|
}
|
|
70
|
-
if (!res.body) return '';
|
|
146
|
+
if (!res.body) return { body: responseType === 'buffer' ? Buffer.alloc(0) : '', bodyBytes: Buffer.alloc(0) };
|
|
71
147
|
const reader = res.body.getReader();
|
|
72
148
|
const chunks = [];
|
|
73
149
|
let total = 0;
|
|
@@ -76,7 +152,7 @@ async function readBodyLimited(res, maxBodyBytes) {
|
|
|
76
152
|
const { done, value } = await reader.read();
|
|
77
153
|
if (done) break;
|
|
78
154
|
total += value.byteLength;
|
|
79
|
-
if (total > maxBodyBytes) throw
|
|
155
|
+
if (total > maxBodyBytes) throw bodyLimitError(maxBodyBytes);
|
|
80
156
|
chunks.push(value);
|
|
81
157
|
}
|
|
82
158
|
} finally {
|
|
@@ -85,42 +161,127 @@ async function readBodyLimited(res, maxBodyBytes) {
|
|
|
85
161
|
const bytes = new Uint8Array(total);
|
|
86
162
|
let offset = 0;
|
|
87
163
|
for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
|
|
88
|
-
|
|
164
|
+
const bodyBytes = Buffer.from(bytes);
|
|
165
|
+
return { body: responseType === 'buffer' ? bodyBytes : new TextDecoder().decode(bytes), bodyBytes };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function bodyLimitError(maxBodyBytes) {
|
|
169
|
+
const error = new Error(`response body exceeds ${maxBodyBytes} bytes`);
|
|
170
|
+
error.code = 'FETCH_BODY_LIMIT';
|
|
171
|
+
return error;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function timeoutError(timeoutMs) {
|
|
175
|
+
const error = new Error(`request timeout after ${timeoutMs}ms`);
|
|
176
|
+
error.code = 'FETCH_TIMEOUT';
|
|
177
|
+
return error;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function errorCode(error) {
|
|
181
|
+
const code = typeof error?.code === 'string' && error.code ? error.code : null;
|
|
182
|
+
if (code && /^[A-Z][A-Z0-9_]{0,63}$/.test(code)) return code;
|
|
183
|
+
if (error?.name === 'AbortError') return 'FETCH_ABORTED';
|
|
184
|
+
return 'FETCH_ERROR';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function attemptUrl(url) {
|
|
188
|
+
const parsed = new URL(url);
|
|
189
|
+
parsed.hash = '';
|
|
190
|
+
if (parsed.search) parsed.search = '?[REDACTED]';
|
|
191
|
+
return parsed.href;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function isRetryableResponseStatus(status) {
|
|
195
|
+
return status === 500 || status === 502 || status === 503 || status === 504;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function codedFetchError(message, code) {
|
|
199
|
+
const error = new Error(message);
|
|
200
|
+
error.code = code;
|
|
201
|
+
return error;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function attachAttempts(error, attempts) {
|
|
205
|
+
const target = error instanceof Error ? error : new Error('request failed');
|
|
206
|
+
Object.defineProperty(target, 'attempts', {
|
|
207
|
+
value: attempts.map((attempt) => ({ ...attempt })),
|
|
208
|
+
enumerable: true,
|
|
209
|
+
configurable: true,
|
|
210
|
+
});
|
|
211
|
+
return target;
|
|
89
212
|
}
|
|
90
213
|
|
|
91
214
|
/** Fetch a public URL with bounded retries, redirects, time, and response size. */
|
|
92
215
|
export async function fetchUrl(url, {
|
|
93
216
|
userAgent = 'CitableAudit/0.1', maxRedirects = 10, timeoutMs = 20000,
|
|
94
217
|
maxRetries = 3, retryDelayMs = 1000, maxBodyBytes = 5 * 1024 * 1024,
|
|
95
|
-
|
|
218
|
+
responseType = 'text',
|
|
219
|
+
fetchImpl, lookup = dns.promises.lookup,
|
|
220
|
+
allowUnsafeCustomTransportForTest = false,
|
|
96
221
|
} = {}) {
|
|
97
|
-
|
|
222
|
+
if (!['text', 'buffer'].includes(responseType)) throw new TypeError('responseType must be text or buffer');
|
|
223
|
+
const requested = parseNetworkUrl(url);
|
|
98
224
|
const allowedOrigin = requested.origin;
|
|
99
225
|
const chain = [];
|
|
226
|
+
const attempts = [];
|
|
227
|
+
let attemptNumber = 0;
|
|
100
228
|
let current = requested.href;
|
|
229
|
+
const customFetch = fetchImpl && fetchImpl !== globalThis.fetch;
|
|
230
|
+
if (customFetch && allowUnsafeCustomTransportForTest !== true && fetchImpl[GUARDED_LOOKUP_TRANSPORT] !== true) {
|
|
231
|
+
throw new TypeError('custom transport must declare and implement the guarded lookup contract');
|
|
232
|
+
}
|
|
233
|
+
const effectiveFetch = customFetch ? fetchImpl : fetchWithGuardedLookup;
|
|
101
234
|
|
|
102
235
|
for (let redirects = 0; redirects <= maxRedirects; redirects += 1) {
|
|
103
|
-
await validatePublicUrl(current, { lookup });
|
|
104
236
|
let res;
|
|
105
237
|
let lastError;
|
|
238
|
+
let responseStartedAtMs = null;
|
|
106
239
|
for (let attempt = 0; attempt < Math.max(1, maxRetries); attempt += 1) {
|
|
240
|
+
attemptNumber += 1;
|
|
241
|
+
const startedAtMs = performance.now();
|
|
242
|
+
try {
|
|
243
|
+
await validatePublicUrl(current, { lookup });
|
|
244
|
+
} catch (error) {
|
|
245
|
+
const endedAtMs = performance.now();
|
|
246
|
+
attempts.push({
|
|
247
|
+
attempt: attemptNumber, url: attemptUrl(current), startedAtMs, endedAtMs,
|
|
248
|
+
elapsedMs: Math.max(0, endedAtMs - startedAtMs), outcome: 'error', status: null,
|
|
249
|
+
errorCode: errorCode(error), retryDecision: 'stop',
|
|
250
|
+
});
|
|
251
|
+
throw attachAttempts(error, attempts);
|
|
252
|
+
}
|
|
107
253
|
const controller = new AbortController();
|
|
108
|
-
const timer = setTimeout(() => controller.abort(
|
|
254
|
+
const timer = setTimeout(() => controller.abort(timeoutError(timeoutMs)), timeoutMs);
|
|
109
255
|
try {
|
|
110
|
-
res = await
|
|
256
|
+
res = await effectiveFetch(current, {
|
|
111
257
|
redirect: 'manual',
|
|
112
258
|
headers: { 'user-agent': userAgent },
|
|
113
259
|
signal: controller.signal,
|
|
260
|
+
lookup: createGuardedLookup(lookup),
|
|
114
261
|
});
|
|
115
|
-
|
|
262
|
+
const retry = isRetryableResponseStatus(res.status) && attempt < Math.max(1, maxRetries) - 1;
|
|
263
|
+
if (!retry) {
|
|
116
264
|
lastError = null;
|
|
265
|
+
responseStartedAtMs = startedAtMs;
|
|
117
266
|
break;
|
|
118
267
|
}
|
|
268
|
+
const endedAtMs = performance.now();
|
|
269
|
+
attempts.push({
|
|
270
|
+
attempt: attemptNumber, url: attemptUrl(current), startedAtMs, endedAtMs,
|
|
271
|
+
elapsedMs: Math.max(0, endedAtMs - startedAtMs), outcome: 'response',
|
|
272
|
+
status: res.status, errorCode: null, retryDecision: 'retry',
|
|
273
|
+
});
|
|
119
274
|
await res.body?.cancel();
|
|
120
275
|
lastError = new Error(`server returned ${res.status}`);
|
|
121
276
|
} catch (err) {
|
|
122
|
-
lastError = err;
|
|
123
|
-
|
|
277
|
+
lastError = controller.signal.aborted ? (controller.signal.reason ?? err) : err;
|
|
278
|
+
const retry = attempt < Math.max(1, maxRetries) - 1;
|
|
279
|
+
const endedAtMs = performance.now();
|
|
280
|
+
attempts.push({
|
|
281
|
+
attempt: attemptNumber, url: attemptUrl(current), startedAtMs, endedAtMs,
|
|
282
|
+
elapsedMs: Math.max(0, endedAtMs - startedAtMs), outcome: 'error', status: null,
|
|
283
|
+
errorCode: errorCode(lastError), retryDecision: retry ? 'retry' : 'stop',
|
|
284
|
+
});
|
|
124
285
|
} finally {
|
|
125
286
|
clearTimeout(timer);
|
|
126
287
|
}
|
|
@@ -129,21 +290,56 @@ export async function fetchUrl(url, {
|
|
|
129
290
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
130
291
|
}
|
|
131
292
|
}
|
|
132
|
-
if (lastError) throw lastError;
|
|
293
|
+
if (lastError) throw attachAttempts(lastError, attempts);
|
|
133
294
|
|
|
134
295
|
const headers = Object.fromEntries(res.headers.entries());
|
|
135
296
|
if (res.status >= 300 && res.status < 400 && headers.location) {
|
|
297
|
+
const endedAtMs = performance.now();
|
|
298
|
+
attempts.push({
|
|
299
|
+
attempt: attemptNumber, url: attemptUrl(current), startedAtMs: responseStartedAtMs, endedAtMs,
|
|
300
|
+
elapsedMs: Math.max(0, endedAtMs - responseStartedAtMs), outcome: 'response',
|
|
301
|
+
status: res.status, errorCode: null,
|
|
302
|
+
retryDecision: redirects === maxRedirects ? 'stop' : 'redirect',
|
|
303
|
+
});
|
|
136
304
|
if (redirects === maxRedirects) break;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
305
|
+
let next;
|
|
306
|
+
try {
|
|
307
|
+
next = new URL(headers.location, current);
|
|
308
|
+
} catch {
|
|
309
|
+
throw attachAttempts(codedFetchError('invalid redirect location', 'FETCH_REDIRECT_INVALID'), attempts);
|
|
310
|
+
}
|
|
311
|
+
if (next.origin !== allowedOrigin) {
|
|
312
|
+
throw attachAttempts(codedFetchError('redirect leaves audited origin', 'FETCH_REDIRECT_ORIGIN'), attempts);
|
|
313
|
+
}
|
|
314
|
+
chain.push({ url: attemptUrl(current), status: res.status, location: attemptUrl(next.href) });
|
|
141
315
|
current = next.href;
|
|
142
316
|
await res.body?.cancel();
|
|
143
317
|
continue;
|
|
144
318
|
}
|
|
145
|
-
|
|
146
|
-
|
|
319
|
+
try {
|
|
320
|
+
const { body, bodyBytes } = await readBodyLimited(res, maxBodyBytes, responseType);
|
|
321
|
+
const endedAtMs = performance.now();
|
|
322
|
+
attempts.push({
|
|
323
|
+
attempt: attemptNumber, url: attemptUrl(current), startedAtMs: responseStartedAtMs, endedAtMs,
|
|
324
|
+
elapsedMs: Math.max(0, endedAtMs - responseStartedAtMs), outcome: 'response',
|
|
325
|
+
status: res.status, errorCode: null, retryDecision: res.status >= 500 ? 'stop' : 'complete',
|
|
326
|
+
});
|
|
327
|
+
return {
|
|
328
|
+
url: current, requestedUrl: url, status: res.status, headers, body, bodyBytes,
|
|
329
|
+
bodyComplete: true, redirectChain: chain, attempts,
|
|
330
|
+
};
|
|
331
|
+
} catch (error) {
|
|
332
|
+
const endedAtMs = performance.now();
|
|
333
|
+
attempts.push({
|
|
334
|
+
attempt: attemptNumber, url: attemptUrl(current), startedAtMs: responseStartedAtMs, endedAtMs,
|
|
335
|
+
elapsedMs: Math.max(0, endedAtMs - responseStartedAtMs), outcome: 'error', status: res.status,
|
|
336
|
+
errorCode: errorCode(error), retryDecision: 'stop',
|
|
337
|
+
});
|
|
338
|
+
throw attachAttempts(error, attempts);
|
|
339
|
+
}
|
|
147
340
|
}
|
|
148
|
-
throw
|
|
341
|
+
throw attachAttempts(codedFetchError(
|
|
342
|
+
`redirect chain exceeded ${maxRedirects} hops`,
|
|
343
|
+
'FETCH_REDIRECT_LIMIT',
|
|
344
|
+
), attempts);
|
|
149
345
|
}
|