@nebulacomponents/citable 1.15.1 → 1.17.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 (264) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +18 -5
  3. package/dist/universal/.agents/skills/citable/SKILL.md +51 -1
  4. package/dist/universal/.agents/skills/citable/VERSION +1 -1
  5. package/dist/universal/.agents/skills/citable/commands/README.md +3 -1
  6. package/dist/universal/.agents/skills/citable/commands/governance.md +12 -0
  7. package/dist/universal/.agents/skills/citable/commands/measurement.md +20 -0
  8. package/dist/universal/.agents/skills/citable/commands/reporting.md +16 -0
  9. package/dist/universal/.agents/skills/citable/manifest.json +17 -12
  10. package/dist/universal/.agents/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  11. package/dist/universal/.agents/skills/citable/schemas/experiment.schema.json +1 -1
  12. package/dist/universal/.agents/skills/citable/schemas/finding.schema.json +69 -0
  13. package/dist/universal/.agents/skills/citable/schemas/fleet.schema.json +93 -0
  14. package/dist/universal/.agents/skills/citable/schemas/funnel.schema.json +116 -0
  15. package/dist/universal/.agents/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  16. package/dist/universal/.agents/skills/citable/schemas/observation.schema.json +1 -1
  17. package/dist/universal/.agents/skills/citable/schemas/remediation-verification.schema.json +98 -0
  18. package/dist/universal/.claude/agents/citable/manifest.json +2 -2
  19. package/dist/universal/.claude/skills/citable/SKILL.md +51 -1
  20. package/dist/universal/.claude/skills/citable/VERSION +1 -1
  21. package/dist/universal/.claude/skills/citable/commands/README.md +3 -1
  22. package/dist/universal/.claude/skills/citable/commands/governance.md +12 -0
  23. package/dist/universal/.claude/skills/citable/commands/measurement.md +20 -0
  24. package/dist/universal/.claude/skills/citable/commands/reporting.md +16 -0
  25. package/dist/universal/.claude/skills/citable/manifest.json +17 -12
  26. package/dist/universal/.claude/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  27. package/dist/universal/.claude/skills/citable/schemas/experiment.schema.json +1 -1
  28. package/dist/universal/.claude/skills/citable/schemas/finding.schema.json +69 -0
  29. package/dist/universal/.claude/skills/citable/schemas/fleet.schema.json +93 -0
  30. package/dist/universal/.claude/skills/citable/schemas/funnel.schema.json +116 -0
  31. package/dist/universal/.claude/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  32. package/dist/universal/.claude/skills/citable/schemas/observation.schema.json +1 -1
  33. package/dist/universal/.claude/skills/citable/schemas/remediation-verification.schema.json +98 -0
  34. package/dist/universal/.cursor/skills/citable/SKILL.md +51 -1
  35. package/dist/universal/.cursor/skills/citable/VERSION +1 -1
  36. package/dist/universal/.cursor/skills/citable/commands/README.md +3 -1
  37. package/dist/universal/.cursor/skills/citable/commands/governance.md +12 -0
  38. package/dist/universal/.cursor/skills/citable/commands/measurement.md +20 -0
  39. package/dist/universal/.cursor/skills/citable/commands/reporting.md +16 -0
  40. package/dist/universal/.cursor/skills/citable/manifest.json +17 -12
  41. package/dist/universal/.cursor/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  42. package/dist/universal/.cursor/skills/citable/schemas/experiment.schema.json +1 -1
  43. package/dist/universal/.cursor/skills/citable/schemas/finding.schema.json +69 -0
  44. package/dist/universal/.cursor/skills/citable/schemas/fleet.schema.json +93 -0
  45. package/dist/universal/.cursor/skills/citable/schemas/funnel.schema.json +116 -0
  46. package/dist/universal/.cursor/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  47. package/dist/universal/.cursor/skills/citable/schemas/observation.schema.json +1 -1
  48. package/dist/universal/.cursor/skills/citable/schemas/remediation-verification.schema.json +98 -0
  49. package/dist/universal/.gemini/skills/citable/SKILL.md +51 -1
  50. package/dist/universal/.gemini/skills/citable/VERSION +1 -1
  51. package/dist/universal/.gemini/skills/citable/commands/README.md +3 -1
  52. package/dist/universal/.gemini/skills/citable/commands/governance.md +12 -0
  53. package/dist/universal/.gemini/skills/citable/commands/measurement.md +20 -0
  54. package/dist/universal/.gemini/skills/citable/commands/reporting.md +16 -0
  55. package/dist/universal/.gemini/skills/citable/manifest.json +17 -12
  56. package/dist/universal/.gemini/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  57. package/dist/universal/.gemini/skills/citable/schemas/experiment.schema.json +1 -1
  58. package/dist/universal/.gemini/skills/citable/schemas/finding.schema.json +69 -0
  59. package/dist/universal/.gemini/skills/citable/schemas/fleet.schema.json +93 -0
  60. package/dist/universal/.gemini/skills/citable/schemas/funnel.schema.json +116 -0
  61. package/dist/universal/.gemini/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  62. package/dist/universal/.gemini/skills/citable/schemas/observation.schema.json +1 -1
  63. package/dist/universal/.gemini/skills/citable/schemas/remediation-verification.schema.json +98 -0
  64. package/dist/universal/.github/skills/citable/SKILL.md +51 -1
  65. package/dist/universal/.github/skills/citable/VERSION +1 -1
  66. package/dist/universal/.github/skills/citable/commands/README.md +3 -1
  67. package/dist/universal/.github/skills/citable/commands/governance.md +12 -0
  68. package/dist/universal/.github/skills/citable/commands/measurement.md +20 -0
  69. package/dist/universal/.github/skills/citable/commands/reporting.md +16 -0
  70. package/dist/universal/.github/skills/citable/manifest.json +17 -12
  71. package/dist/universal/.github/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  72. package/dist/universal/.github/skills/citable/schemas/experiment.schema.json +1 -1
  73. package/dist/universal/.github/skills/citable/schemas/finding.schema.json +69 -0
  74. package/dist/universal/.github/skills/citable/schemas/fleet.schema.json +93 -0
  75. package/dist/universal/.github/skills/citable/schemas/funnel.schema.json +116 -0
  76. package/dist/universal/.github/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  77. package/dist/universal/.github/skills/citable/schemas/observation.schema.json +1 -1
  78. package/dist/universal/.github/skills/citable/schemas/remediation-verification.schema.json +98 -0
  79. package/dist/universal/.kiro/skills/citable/SKILL.md +51 -1
  80. package/dist/universal/.kiro/skills/citable/VERSION +1 -1
  81. package/dist/universal/.kiro/skills/citable/commands/README.md +3 -1
  82. package/dist/universal/.kiro/skills/citable/commands/governance.md +12 -0
  83. package/dist/universal/.kiro/skills/citable/commands/measurement.md +20 -0
  84. package/dist/universal/.kiro/skills/citable/commands/reporting.md +16 -0
  85. package/dist/universal/.kiro/skills/citable/manifest.json +17 -12
  86. package/dist/universal/.kiro/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  87. package/dist/universal/.kiro/skills/citable/schemas/experiment.schema.json +1 -1
  88. package/dist/universal/.kiro/skills/citable/schemas/finding.schema.json +69 -0
  89. package/dist/universal/.kiro/skills/citable/schemas/fleet.schema.json +93 -0
  90. package/dist/universal/.kiro/skills/citable/schemas/funnel.schema.json +116 -0
  91. package/dist/universal/.kiro/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  92. package/dist/universal/.kiro/skills/citable/schemas/observation.schema.json +1 -1
  93. package/dist/universal/.kiro/skills/citable/schemas/remediation-verification.schema.json +98 -0
  94. package/dist/universal/.opencode/skills/citable/SKILL.md +51 -1
  95. package/dist/universal/.opencode/skills/citable/VERSION +1 -1
  96. package/dist/universal/.opencode/skills/citable/commands/README.md +3 -1
  97. package/dist/universal/.opencode/skills/citable/commands/governance.md +12 -0
  98. package/dist/universal/.opencode/skills/citable/commands/measurement.md +20 -0
  99. package/dist/universal/.opencode/skills/citable/commands/reporting.md +16 -0
  100. package/dist/universal/.opencode/skills/citable/manifest.json +17 -12
  101. package/dist/universal/.opencode/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  102. package/dist/universal/.opencode/skills/citable/schemas/experiment.schema.json +1 -1
  103. package/dist/universal/.opencode/skills/citable/schemas/finding.schema.json +69 -0
  104. package/dist/universal/.opencode/skills/citable/schemas/fleet.schema.json +93 -0
  105. package/dist/universal/.opencode/skills/citable/schemas/funnel.schema.json +116 -0
  106. package/dist/universal/.opencode/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  107. package/dist/universal/.opencode/skills/citable/schemas/observation.schema.json +1 -1
  108. package/dist/universal/.opencode/skills/citable/schemas/remediation-verification.schema.json +98 -0
  109. package/dist/universal/.pi/agent/skills/citable/SKILL.md +51 -1
  110. package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
  111. package/dist/universal/.pi/agent/skills/citable/commands/README.md +3 -1
  112. package/dist/universal/.pi/agent/skills/citable/commands/governance.md +12 -0
  113. package/dist/universal/.pi/agent/skills/citable/commands/measurement.md +20 -0
  114. package/dist/universal/.pi/agent/skills/citable/commands/reporting.md +16 -0
  115. package/dist/universal/.pi/agent/skills/citable/manifest.json +17 -12
  116. package/dist/universal/.pi/agent/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  117. package/dist/universal/.pi/agent/skills/citable/schemas/experiment.schema.json +1 -1
  118. package/dist/universal/.pi/agent/skills/citable/schemas/finding.schema.json +69 -0
  119. package/dist/universal/.pi/agent/skills/citable/schemas/fleet.schema.json +93 -0
  120. package/dist/universal/.pi/agent/skills/citable/schemas/funnel.schema.json +116 -0
  121. package/dist/universal/.pi/agent/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  122. package/dist/universal/.pi/agent/skills/citable/schemas/observation.schema.json +1 -1
  123. package/dist/universal/.pi/agent/skills/citable/schemas/remediation-verification.schema.json +98 -0
  124. package/dist/universal/.qoder/skills/citable/SKILL.md +51 -1
  125. package/dist/universal/.qoder/skills/citable/VERSION +1 -1
  126. package/dist/universal/.qoder/skills/citable/commands/README.md +3 -1
  127. package/dist/universal/.qoder/skills/citable/commands/governance.md +12 -0
  128. package/dist/universal/.qoder/skills/citable/commands/measurement.md +20 -0
  129. package/dist/universal/.qoder/skills/citable/commands/reporting.md +16 -0
  130. package/dist/universal/.qoder/skills/citable/manifest.json +17 -12
  131. package/dist/universal/.qoder/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  132. package/dist/universal/.qoder/skills/citable/schemas/experiment.schema.json +1 -1
  133. package/dist/universal/.qoder/skills/citable/schemas/finding.schema.json +69 -0
  134. package/dist/universal/.qoder/skills/citable/schemas/fleet.schema.json +93 -0
  135. package/dist/universal/.qoder/skills/citable/schemas/funnel.schema.json +116 -0
  136. package/dist/universal/.qoder/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  137. package/dist/universal/.qoder/skills/citable/schemas/observation.schema.json +1 -1
  138. package/dist/universal/.qoder/skills/citable/schemas/remediation-verification.schema.json +98 -0
  139. package/dist/universal/.rovodev/skills/citable/SKILL.md +51 -1
  140. package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
  141. package/dist/universal/.rovodev/skills/citable/commands/README.md +3 -1
  142. package/dist/universal/.rovodev/skills/citable/commands/governance.md +12 -0
  143. package/dist/universal/.rovodev/skills/citable/commands/measurement.md +20 -0
  144. package/dist/universal/.rovodev/skills/citable/commands/reporting.md +16 -0
  145. package/dist/universal/.rovodev/skills/citable/manifest.json +17 -12
  146. package/dist/universal/.rovodev/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  147. package/dist/universal/.rovodev/skills/citable/schemas/experiment.schema.json +1 -1
  148. package/dist/universal/.rovodev/skills/citable/schemas/finding.schema.json +69 -0
  149. package/dist/universal/.rovodev/skills/citable/schemas/fleet.schema.json +93 -0
  150. package/dist/universal/.rovodev/skills/citable/schemas/funnel.schema.json +116 -0
  151. package/dist/universal/.rovodev/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  152. package/dist/universal/.rovodev/skills/citable/schemas/observation.schema.json +1 -1
  153. package/dist/universal/.rovodev/skills/citable/schemas/remediation-verification.schema.json +98 -0
  154. package/dist/universal/.trae/skills/citable/SKILL.md +51 -1
  155. package/dist/universal/.trae/skills/citable/VERSION +1 -1
  156. package/dist/universal/.trae/skills/citable/commands/README.md +3 -1
  157. package/dist/universal/.trae/skills/citable/commands/governance.md +12 -0
  158. package/dist/universal/.trae/skills/citable/commands/measurement.md +20 -0
  159. package/dist/universal/.trae/skills/citable/commands/reporting.md +16 -0
  160. package/dist/universal/.trae/skills/citable/manifest.json +17 -12
  161. package/dist/universal/.trae/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  162. package/dist/universal/.trae/skills/citable/schemas/experiment.schema.json +1 -1
  163. package/dist/universal/.trae/skills/citable/schemas/finding.schema.json +69 -0
  164. package/dist/universal/.trae/skills/citable/schemas/fleet.schema.json +93 -0
  165. package/dist/universal/.trae/skills/citable/schemas/funnel.schema.json +116 -0
  166. package/dist/universal/.trae/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  167. package/dist/universal/.trae/skills/citable/schemas/observation.schema.json +1 -1
  168. package/dist/universal/.trae/skills/citable/schemas/remediation-verification.schema.json +98 -0
  169. package/dist/universal/.trae-cn/skills/citable/SKILL.md +51 -1
  170. package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
  171. package/dist/universal/.trae-cn/skills/citable/commands/README.md +3 -1
  172. package/dist/universal/.trae-cn/skills/citable/commands/governance.md +12 -0
  173. package/dist/universal/.trae-cn/skills/citable/commands/measurement.md +20 -0
  174. package/dist/universal/.trae-cn/skills/citable/commands/reporting.md +16 -0
  175. package/dist/universal/.trae-cn/skills/citable/manifest.json +17 -12
  176. package/dist/universal/.trae-cn/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  177. package/dist/universal/.trae-cn/skills/citable/schemas/experiment.schema.json +1 -1
  178. package/dist/universal/.trae-cn/skills/citable/schemas/finding.schema.json +69 -0
  179. package/dist/universal/.trae-cn/skills/citable/schemas/fleet.schema.json +93 -0
  180. package/dist/universal/.trae-cn/skills/citable/schemas/funnel.schema.json +116 -0
  181. package/dist/universal/.trae-cn/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  182. package/dist/universal/.trae-cn/skills/citable/schemas/observation.schema.json +1 -1
  183. package/dist/universal/.trae-cn/skills/citable/schemas/remediation-verification.schema.json +98 -0
  184. package/dist/universal/manifest.json +208 -148
  185. package/package.json +1 -1
  186. package/schemas/cli-output-envelope.schema.json +30 -0
  187. package/schemas/experiment.schema.json +1 -1
  188. package/schemas/finding.schema.json +69 -0
  189. package/schemas/fleet.schema.json +93 -0
  190. package/schemas/funnel.schema.json +116 -0
  191. package/schemas/mcp-transport-envelope.schema.json +244 -0
  192. package/schemas/observation.schema.json +1 -1
  193. package/schemas/remediation-verification.schema.json +98 -0
  194. package/skill/SKILL.md +51 -1
  195. package/skill/commands/README.md +3 -1
  196. package/skill/commands/governance.md +12 -0
  197. package/skill/commands/measurement.md +20 -0
  198. package/skill/commands/reporting.md +16 -0
  199. package/src/analysis/saliency.js +119 -0
  200. package/src/cli/index.js +651 -13
  201. package/src/commands/attributeImpact.js +54 -0
  202. package/src/commands/audit.js +2 -2
  203. package/src/commands/ciWorkflow.js +63 -0
  204. package/src/commands/compatibility.js +205 -0
  205. package/src/commands/connect.js +3 -0
  206. package/src/commands/context.js +2 -1
  207. package/src/commands/edgeRules.js +196 -0
  208. package/src/commands/edgeSecurity.js +192 -0
  209. package/src/commands/exceptions.js +241 -0
  210. package/src/commands/experimentGuardrails.js +213 -0
  211. package/src/commands/fleet.js +104 -0
  212. package/src/commands/generateLlmsTxt.js +111 -0
  213. package/src/commands/goldenCorpus.js +155 -0
  214. package/src/commands/implementationKit.js +202 -0
  215. package/src/commands/inspectAeo.js +74 -0
  216. package/src/commands/inspectCro.js +142 -0
  217. package/src/commands/inspectGeo.js +52 -0
  218. package/src/commands/inspectSerp.js +317 -0
  219. package/src/commands/lintComponents.js +132 -0
  220. package/src/commands/monitor.js +138 -0
  221. package/src/commands/observe.js +6 -1
  222. package/src/commands/planExperiment.js +120 -0
  223. package/src/commands/previewCro.js +241 -0
  224. package/src/commands/probe.js +70 -0
  225. package/src/commands/remediate.js +461 -0
  226. package/src/commands/reportConsensus.js +48 -0
  227. package/src/commands/testFunnel.js +107 -0
  228. package/src/commands/verifyRemediation.js +204 -0
  229. package/src/commands/visualRegression.js +165 -0
  230. package/src/components/index.js +556 -0
  231. package/src/connectors/index.js +6 -1
  232. package/src/connectors/indexnow.js +431 -0
  233. package/src/connectors/mcp/allowlist.js +107 -0
  234. package/src/connectors/mcp/client.js +239 -0
  235. package/src/connectors/mcp/diagnostics.js +214 -0
  236. package/src/connectors/mcp/gscServer.js +155 -0
  237. package/src/connectors/mcp/http.js +130 -0
  238. package/src/connectors/mcp/index.js +7 -0
  239. package/src/connectors/mcp/pilot.js +138 -0
  240. package/src/connectors/mcp/pilotServer.js +80 -0
  241. package/src/connectors/mcp/sanitizer.js +38 -0
  242. package/src/connectors/mcp/stdio.js +178 -0
  243. package/src/crawler/ipRanges.js +161 -0
  244. package/src/detectors/agent.js +127 -0
  245. package/src/detectors/ans.js +282 -0
  246. package/src/detectors/crawl.js +46 -0
  247. package/src/detectors/cro.js +825 -0
  248. package/src/detectors/cwv.js +38 -1
  249. package/src/detectors/evd.js +101 -0
  250. package/src/detectors/framework.js +32 -2
  251. package/src/detectors/geoReco.js +408 -0
  252. package/src/detectors/index.js +4 -2
  253. package/src/detectors/link.js +112 -1
  254. package/src/detectors/page.js +101 -0
  255. package/src/detectors/schemaData.js +529 -0
  256. package/src/detectors/tech.js +306 -0
  257. package/src/extractor/page.js +186 -2
  258. package/src/extractor/site.js +9 -2
  259. package/src/installer/index.js +45 -2
  260. package/src/monitoring/alertDelivery.js +101 -7
  261. package/src/observations/attribution.js +217 -0
  262. package/src/registries/index.js +14 -0
  263. package/src/reporting/consensus.js +416 -0
  264. package/src/reporting/executiveExport.js +244 -0
@@ -0,0 +1,431 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { validatePublicUrl } from '../crawler/fetch.js';
5
+ import { parseSitemap } from '../crawler/sitemap.js';
6
+ import { nowIso, sha256, writeJson } from '../shared/io.js';
7
+
8
+ export const INDEXNOW_DEFAULT_ENDPOINT = 'https://api.indexnow.org/indexnow';
9
+ export const INDEXNOW_MAX_BATCH_SIZE = 10000;
10
+ export const INDEXNOW_KEY_PATTERN = /^[a-zA-Z0-9-]{8,128}$/;
11
+
12
+ /**
13
+ * Normalize an input host string or URL into a canonical hostname (including port if non-standard).
14
+ */
15
+ export function normalizeHost(input) {
16
+ if (!input || typeof input !== 'string') {
17
+ throw new Error('host must be a non-empty string');
18
+ }
19
+ let trimmed = input.trim();
20
+ if (!/^https?:\/\//i.test(trimmed)) {
21
+ trimmed = `https://${trimmed}`;
22
+ }
23
+ try {
24
+ const parsed = new URL(trimmed);
25
+ if (!parsed.host) {
26
+ throw new Error(`Invalid host: "${input}"`);
27
+ }
28
+ return parsed.host;
29
+ } catch (err) {
30
+ throw new Error(`Failed to parse host "${input}": ${err.message}`);
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Parse and validate URLs from a file path, JSON file, or comma/newline-delimited string.
36
+ */
37
+ export function parseUrlsInput(input) {
38
+ if (!input) return [];
39
+ let rawList = [];
40
+ if (Array.isArray(input)) {
41
+ rawList = input;
42
+ } else if (typeof input === 'string') {
43
+ const trimmed = input.trim();
44
+ if (fs.existsSync(trimmed)) {
45
+ const content = fs.readFileSync(trimmed, 'utf8');
46
+ if (trimmed.endsWith('.json') || content.trim().startsWith('{') || content.trim().startsWith('[')) {
47
+ try {
48
+ const parsed = JSON.parse(content);
49
+ if (Array.isArray(parsed)) {
50
+ rawList = parsed;
51
+ } else if (parsed && Array.isArray(parsed.urlList)) {
52
+ rawList = parsed.urlList;
53
+ } else if (parsed && Array.isArray(parsed.urls)) {
54
+ rawList = parsed.urls;
55
+ } else {
56
+ throw new Error('JSON file must contain an array of URLs or an object with "urlList" or "urls" array');
57
+ }
58
+ } catch (e) {
59
+ throw new Error(`Failed to parse URL JSON file: ${e.message}`);
60
+ }
61
+ } else {
62
+ rawList = content.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith('#'));
63
+ }
64
+ } else {
65
+ rawList = trimmed.split(/[\r\n,]+/).map((s) => s.trim()).filter(Boolean);
66
+ }
67
+ } else {
68
+ throw new Error('urls must be a string or array');
69
+ }
70
+
71
+ const result = [];
72
+ const seen = new Set();
73
+ for (const raw of rawList) {
74
+ const u = String(raw).trim();
75
+ if (!u) continue;
76
+ let parsed;
77
+ try {
78
+ parsed = new URL(u);
79
+ } catch {
80
+ throw new Error(`Invalid URL provided: "${u}"`);
81
+ }
82
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
83
+ throw new Error(`Unsupported URL protocol in "${u}": only http: and https: are allowed`);
84
+ }
85
+ if (parsed.username || parsed.password) {
86
+ throw new Error(`URL credentials are not permitted in "${u}"`);
87
+ }
88
+ const href = parsed.href;
89
+ if (!seen.has(href)) {
90
+ seen.add(href);
91
+ result.push(href);
92
+ }
93
+ }
94
+ return result;
95
+ }
96
+
97
+ /**
98
+ * Load URLs from a local sitemap file or remote sitemap URL.
99
+ */
100
+ export async function loadSitemapUrls(sitemapInput, { fetchImpl = globalThis.fetch, allowPrivateForTest = false, lookup } = {}) {
101
+ if (!sitemapInput) return [];
102
+ let xml = '';
103
+ const trimmed = String(sitemapInput).trim();
104
+ if (fs.existsSync(trimmed)) {
105
+ xml = fs.readFileSync(trimmed, 'utf8');
106
+ } else if (/^https?:\/\//i.test(trimmed)) {
107
+ if (!allowPrivateForTest) {
108
+ await validatePublicUrl(trimmed, { lookup });
109
+ }
110
+ const res = await fetchImpl(trimmed);
111
+ if (!res.ok) {
112
+ throw new Error(`Failed to fetch sitemap from ${trimmed}: HTTP ${res.status} ${res.statusText || ''}`.trim());
113
+ }
114
+ xml = await res.text();
115
+ } else {
116
+ throw new Error(`Sitemap input is neither an existing file nor an HTTP(S) URL: "${sitemapInput}"`);
117
+ }
118
+
119
+ const parsed = parseSitemap(xml);
120
+ if (parsed.isIndex) {
121
+ throw new Error(`Sitemap at "${sitemapInput}" is a sitemapindex containing ${parsed.children.length} sub-sitemaps. Please provide an individual urlset sitemap or submit child sitemaps individually.`);
122
+ }
123
+ if (parsed.errors.length > 0 && parsed.urls.length === 0) {
124
+ throw new Error(`Failed to parse sitemap: ${parsed.errors.join('; ')}`);
125
+ }
126
+ return parsed.urls.map((u) => u.loc);
127
+ }
128
+
129
+ /**
130
+ * Construct and validate an IndexNow submission payload.
131
+ */
132
+ export function buildIndexNowPayload({ host, key, keyLocation, urls = [] }) {
133
+ if (!host) throw new Error('host is required for IndexNow payload');
134
+ const normalizedHost = normalizeHost(host);
135
+ if (!key) throw new Error('key is required for IndexNow payload');
136
+ if (!INDEXNOW_KEY_PATTERN.test(key)) {
137
+ throw new Error('IndexNow key must be 8-128 alphanumeric characters or hyphens');
138
+ }
139
+ if (!Array.isArray(urls) || urls.length === 0) {
140
+ throw new Error('urls must be a non-empty array of URL strings');
141
+ }
142
+ if (urls.length > INDEXNOW_MAX_BATCH_SIZE) {
143
+ throw new Error(`Batch size exceeds maximum of ${INDEXNOW_MAX_BATCH_SIZE} URLs`);
144
+ }
145
+
146
+ for (const u of urls) {
147
+ let parsed;
148
+ try {
149
+ parsed = new URL(u);
150
+ } catch {
151
+ throw new Error(`Invalid URL in payload: "${u}"`);
152
+ }
153
+ if (parsed.host !== normalizedHost) {
154
+ throw new Error(`URL "${u}" host ("${parsed.host}") does not match IndexNow payload host ("${normalizedHost}")`);
155
+ }
156
+ }
157
+
158
+ if (keyLocation) {
159
+ const parsedLoc = new URL(keyLocation);
160
+ if (parsedLoc.host !== normalizedHost) {
161
+ throw new Error(`keyLocation "${keyLocation}" host ("${parsedLoc.host}") does not match IndexNow payload host ("${normalizedHost}")`);
162
+ }
163
+ }
164
+
165
+ return {
166
+ host: normalizedHost,
167
+ key,
168
+ ...(keyLocation ? { keyLocation } : {}),
169
+ urlList: urls,
170
+ };
171
+ }
172
+
173
+ /**
174
+ * Pre-flight verification probe to ensure the host key file is publicly accessible and matches the key.
175
+ */
176
+ export async function verifyHostKey(host, key, {
177
+ keyLocation,
178
+ fetchImpl = globalThis.fetch,
179
+ allowPrivateForTest = false,
180
+ lookup,
181
+ timeoutMs = 10000,
182
+ } = {}) {
183
+ const normalizedHost = normalizeHost(host);
184
+ const targetUrl = keyLocation || `https://${normalizedHost}/${key}.txt`;
185
+ let parsed;
186
+ try {
187
+ parsed = new URL(targetUrl);
188
+ } catch {
189
+ throw new Error(`Invalid key verification URL: "${targetUrl}"`);
190
+ }
191
+ if (parsed.host !== normalizedHost) {
192
+ throw new Error(`key verification URL host ("${parsed.host}") does not match host ("${normalizedHost}")`);
193
+ }
194
+
195
+ if (!allowPrivateForTest) {
196
+ await validatePublicUrl(targetUrl, { lookup });
197
+ }
198
+
199
+ const controller = new AbortController();
200
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
201
+
202
+ let res;
203
+ try {
204
+ res = await fetchImpl(targetUrl, {
205
+ method: 'GET',
206
+ signal: controller.signal,
207
+ headers: {
208
+ 'user-agent': 'citable/1.15.1 (IndexNow key verifier)',
209
+ },
210
+ });
211
+ } catch (err) {
212
+ clearTimeout(timer);
213
+ throw new Error(`IndexNow key verification failed to connect to ${targetUrl}: ${err.message}`);
214
+ } finally {
215
+ clearTimeout(timer);
216
+ }
217
+
218
+ if (!res.ok) {
219
+ throw new Error(`IndexNow key verification failed at ${targetUrl}: HTTP ${res.status} ${res.statusText || ''}`.trim());
220
+ }
221
+
222
+ const body = (await res.text()).trim();
223
+ if (body !== key && !body.startsWith(key)) {
224
+ throw new Error(`IndexNow key verification failed at ${targetUrl}: content does not match expected key`);
225
+ }
226
+
227
+ return { verified: true, keyLocation: targetUrl };
228
+ }
229
+
230
+ /**
231
+ * Submit URLs to the IndexNow protocol endpoint, with fail-closed key verification and immutable receipts.
232
+ */
233
+ export async function submitIndexNow(root = process.cwd(), {
234
+ host,
235
+ key,
236
+ keyLocation,
237
+ urls,
238
+ sitemap,
239
+ endpoint = INDEXNOW_DEFAULT_ENDPOINT,
240
+ write = false,
241
+ skipKeyVerify = false,
242
+ fetchImpl = globalThis.fetch,
243
+ allowPrivateForTest = false,
244
+ lookup,
245
+ timeoutMs = 15000,
246
+ } = {}) {
247
+ // 1. Gather URLs from input and/or sitemap
248
+ const urlListFromInput = parseUrlsInput(urls);
249
+ const urlListFromSitemap = sitemap
250
+ ? await loadSitemapUrls(sitemap, { fetchImpl, allowPrivateForTest, lookup })
251
+ : [];
252
+
253
+ const combinedUrls = [...new Set([...urlListFromInput, ...urlListFromSitemap])];
254
+ if (combinedUrls.length === 0) {
255
+ throw new Error('connect indexnow requires --urls <file|list> or --sitemap <url|file> with at least one URL');
256
+ }
257
+
258
+ // 2. Determine and normalize host
259
+ let effectiveHost = host;
260
+ if (!effectiveHost) {
261
+ const hosts = [...new Set(combinedUrls.map((u) => new URL(u).host))];
262
+ if (hosts.length === 1) {
263
+ effectiveHost = hosts[0];
264
+ } else {
265
+ throw new Error(`URLs span multiple hosts (${hosts.join(', ')}); specify --host or ensure all URLs match the same host`);
266
+ }
267
+ }
268
+ const normalizedHost = normalizeHost(effectiveHost);
269
+
270
+ // 3. Resolve and validate key
271
+ const effectiveKey = key || process.env.INDEXNOW_KEY || process.env.INDEXNOW_API_KEY;
272
+ if (!effectiveKey) {
273
+ throw new Error('IndexNow key is required. Provide --key or set INDEXNOW_KEY environment variable');
274
+ }
275
+ if (!INDEXNOW_KEY_PATTERN.test(effectiveKey)) {
276
+ throw new Error('IndexNow key must be 8-128 alphanumeric characters or hyphens');
277
+ }
278
+
279
+ // 4. Resolve keyLocation
280
+ const effectiveKeyLocation = keyLocation || `https://${normalizedHost}/${effectiveKey}.txt`;
281
+ const parsedKeyLoc = new URL(effectiveKeyLocation);
282
+ if (parsedKeyLoc.host !== normalizedHost) {
283
+ throw new Error(`keyLocation "${effectiveKeyLocation}" host does not match submission host "${normalizedHost}"`);
284
+ }
285
+
286
+ // 5. Verify host key unless skipped
287
+ let keyVerified = false;
288
+ if (!skipKeyVerify) {
289
+ await verifyHostKey(normalizedHost, effectiveKey, {
290
+ keyLocation: effectiveKeyLocation,
291
+ fetchImpl,
292
+ allowPrivateForTest,
293
+ lookup,
294
+ timeoutMs,
295
+ });
296
+ keyVerified = true;
297
+ }
298
+
299
+ // 6. Partition into batches of INDEXNOW_MAX_BATCH_SIZE
300
+ const batches = [];
301
+ for (let i = 0; i < combinedUrls.length; i += INDEXNOW_MAX_BATCH_SIZE) {
302
+ const chunk = combinedUrls.slice(i, i + INDEXNOW_MAX_BATCH_SIZE);
303
+ const payload = buildIndexNowPayload({
304
+ host: normalizedHost,
305
+ key: effectiveKey,
306
+ keyLocation: effectiveKeyLocation,
307
+ urls: chunk,
308
+ });
309
+ const payloadString = JSON.stringify(payload);
310
+ const payloadHash = sha256(Buffer.from(payloadString));
311
+
312
+ let statusCode = null;
313
+ let success = false;
314
+ let errorMessage = null;
315
+
316
+ if (write) {
317
+ if (!allowPrivateForTest) {
318
+ await validatePublicUrl(endpoint, { lookup });
319
+ }
320
+ const controller = new AbortController();
321
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
322
+
323
+ try {
324
+ const res = await fetchImpl(endpoint, {
325
+ method: 'POST',
326
+ headers: {
327
+ 'content-type': 'application/json; charset=utf-8',
328
+ 'user-agent': 'citable/1.15.1 (IndexNow connector)',
329
+ },
330
+ body: payloadString,
331
+ signal: controller.signal,
332
+ });
333
+ clearTimeout(timer);
334
+ statusCode = res.status;
335
+ success = [200, 202].includes(res.status);
336
+ if (!success) {
337
+ const text = await res.text().catch(() => '');
338
+ errorMessage = `HTTP ${res.status} ${res.statusText || 'Error'}${text ? `: ${text}` : ''}`.trim();
339
+ }
340
+ } catch (err) {
341
+ clearTimeout(timer);
342
+ errorMessage = err.name === 'AbortError' ? `Request timed out after ${timeoutMs}ms` : err.message;
343
+ }
344
+ } else {
345
+ success = true;
346
+ }
347
+
348
+ batches.push({
349
+ batch_index: Math.floor(i / INDEXNOW_MAX_BATCH_SIZE),
350
+ url_count: chunk.length,
351
+ payload_hash: payloadHash,
352
+ status_code: statusCode,
353
+ success,
354
+ error: errorMessage,
355
+ });
356
+ }
357
+
358
+ // 7. Generate delivery receipt
359
+ const deliveryId = `INDEXNOW-${nowIso().replace(/[:.]/g, '')}-${crypto.randomBytes(4).toString('hex')}`;
360
+ const deliveriesDir = path.join(root, '.citable', 'monitoring', 'deliveries');
361
+ const receiptFile = path.join(deliveriesDir, `${deliveryId}.json`);
362
+
363
+ const allSuccess = batches.every((b) => b.success);
364
+ const firstError = batches.find((b) => b.error)?.error || null;
365
+
366
+ const receipt = {
367
+ delivery_id: deliveryId,
368
+ connector: 'indexnow',
369
+ host: normalizedHost,
370
+ endpoint,
371
+ submitted_at: nowIso(),
372
+ url_count: combinedUrls.length,
373
+ batch_count: batches.length,
374
+ batches,
375
+ success: allSuccess,
376
+ dry_run: !write,
377
+ error: firstError,
378
+ key_location: effectiveKeyLocation,
379
+ key_verified: keyVerified,
380
+ };
381
+
382
+ writeJson(receiptFile, receipt);
383
+ return { ...receipt, receipt_file: receiptFile };
384
+ }
385
+
386
+ export const indexnowConnector = {
387
+ provider: 'indexnow',
388
+ defaultCredentialEnv: 'INDEXNOW_KEY',
389
+ readOnlyScopes: [],
390
+ writeScopes: ['indexnow:submit'],
391
+ describeMetrics() {
392
+ return {
393
+ urls_submitted: { unit: 'count', value_type: 'integer' },
394
+ };
395
+ },
396
+ async discoverProperties(context = {}) {
397
+ const host = context.property_id || context.host || context.siteUrl;
398
+ if (host && host !== 'api.indexnow.org') {
399
+ return [{
400
+ property_id: host,
401
+ display_name: `IndexNow (${host})`,
402
+ permission: 'publisher',
403
+ }];
404
+ }
405
+ return [{
406
+ property_id: 'api.indexnow.org',
407
+ display_name: 'IndexNow Global Submission API',
408
+ permission: 'publisher',
409
+ }];
410
+ },
411
+ async validateConnection(connection, context = {}) {
412
+ const key = context.token || context.key || process.env.INDEXNOW_KEY || process.env.INDEXNOW_API_KEY;
413
+ if (!key) {
414
+ return { valid: false, reason: 'IndexNow key not configured (set INDEXNOW_KEY)' };
415
+ }
416
+ if (!INDEXNOW_KEY_PATTERN.test(key)) {
417
+ return { valid: false, reason: 'Invalid IndexNow key format (must be 8-128 alphanumeric characters or hyphens)' };
418
+ }
419
+ const host = connection?.property_id;
420
+ if (host && host !== 'api.indexnow.org') {
421
+ try {
422
+ await verifyHostKey(host, key, context);
423
+ return { valid: true };
424
+ } catch (err) {
425
+ return { valid: false, reason: err.message };
426
+ }
427
+ }
428
+ return { valid: true };
429
+ },
430
+ submit: submitIndexNow,
431
+ };
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Explicit server and tool allowlist for Model Context Protocol (MCP) transports.
3
+ *
4
+ * Security Invariants:
5
+ * - Servers must be explicitly declared; unknown servers fail closed.
6
+ * - Tools must be explicitly declared as read_only: true; state-changing tools are rejected.
7
+ * - Transport type must be supported by the server entry.
8
+ */
9
+
10
+ export const ALLOWED_MCP_SERVERS = {
11
+ "citable-evidence-pilot": {
12
+ description: "Citable read-only evidence verification pilot server",
13
+ transports: ["stdio", "http"],
14
+ tools: {
15
+ inspect_target: {
16
+ read_only: true,
17
+ description: "Extract title, canonical URL, and meta tags for target URL",
18
+ schema: {
19
+ type: "object",
20
+ required: ["url"],
21
+ properties: {
22
+ url: { type: "string" },
23
+ },
24
+ },
25
+ },
26
+ get_canonical_evidence: {
27
+ read_only: true,
28
+ description: "Collect publisher canonical declarations and HTTP Link headers",
29
+ schema: {
30
+ type: "object",
31
+ required: ["url"],
32
+ properties: {
33
+ url: { type: "string" },
34
+ },
35
+ },
36
+ },
37
+ query_search_index: {
38
+ read_only: true,
39
+ description: "Query indexed URL status from provider search index",
40
+ schema: {
41
+ type: "object",
42
+ required: ["url"],
43
+ properties: {
44
+ url: { type: "string" },
45
+ host: { type: "string" },
46
+ },
47
+ },
48
+ },
49
+ },
50
+ },
51
+ "gsc-mcp": {
52
+ description: "Google Search Console read-only MCP adapter",
53
+ transports: ["stdio", "http"],
54
+ tools: {
55
+ inspect_url: {
56
+ read_only: true,
57
+ description: "Inspect URL indexing status via Search Console",
58
+ schema: {
59
+ type: "object",
60
+ required: ["url", "siteUrl"],
61
+ properties: {
62
+ url: { type: "string" },
63
+ siteUrl: { type: "string" },
64
+ },
65
+ },
66
+ },
67
+ query_search_analytics: {
68
+ read_only: true,
69
+ description: "Query search analytics impressions and clicks",
70
+ schema: {
71
+ type: "object",
72
+ required: ["siteUrl", "startDate", "endDate"],
73
+ properties: {
74
+ siteUrl: { type: "string" },
75
+ startDate: { type: "string" },
76
+ endDate: { type: "string" },
77
+ },
78
+ },
79
+ },
80
+ },
81
+ },
82
+ };
83
+
84
+ export function isServerAllowlisted(serverId) {
85
+ return Boolean(ALLOWED_MCP_SERVERS[serverId]);
86
+ }
87
+
88
+ export function isToolAllowlisted(serverId, toolName) {
89
+ const server = ALLOWED_MCP_SERVERS[serverId];
90
+ if (!server) return false;
91
+ return Boolean(server.tools[toolName]);
92
+ }
93
+
94
+ export function assertToolAllowed(serverId, toolName, { isReadOnly = true } = {}) {
95
+ const server = ALLOWED_MCP_SERVERS[serverId];
96
+ if (!server) {
97
+ throw new Error(`untrusted MCP server: "${serverId}" is not in the allowlist`);
98
+ }
99
+ const tool = server.tools[toolName];
100
+ if (!tool) {
101
+ throw new Error(`untrusted MCP tool: "${toolName}" is not allowlisted for server "${serverId}"`);
102
+ }
103
+ if (!tool.read_only || isReadOnly === false) {
104
+ throw new Error(`refusing state-changing MCP tool: "${toolName}" is not read-only`);
105
+ }
106
+ return tool;
107
+ }