agentlinkops 0.6.11 → 0.6.16

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 (311) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/.codex-plugin/plugin.json +2 -2
  3. package/.mcp.json +0 -0
  4. package/CHANGELOG.md +35 -0
  5. package/LICENSE +0 -0
  6. package/NOTICE +0 -0
  7. package/PACK-ARTIFACT.json +16 -0
  8. package/PACKS.json +591 -0
  9. package/README.md +34 -48
  10. package/bin/agentlinkops.mjs +0 -0
  11. package/bin/linktrail.mjs +0 -0
  12. package/cli/adapters/csv.js +0 -0
  13. package/cli/adapters/sqlite-crm.js +0 -0
  14. package/cli/adapters/suppliers.js +1 -1
  15. package/cli/admission.js +0 -0
  16. package/cli/agent-setup.js +101 -17
  17. package/cli/agentlinkops.mjs +0 -0
  18. package/cli/args.js +0 -0
  19. package/cli/campaign-bridge.js +325 -0
  20. package/cli/campaign-outcomes.js +107 -0
  21. package/cli/catalog.js +47686 -16398
  22. package/cli/check.js +40 -10
  23. package/cli/chronology.js +39 -0
  24. package/cli/citation.js +0 -0
  25. package/cli/client.js +2 -1
  26. package/cli/cloud-error.js +0 -0
  27. package/cli/commands.js +17 -8
  28. package/cli/config.js +0 -0
  29. package/cli/connection.js +92 -18
  30. package/cli/contact-adoption.js +91 -0
  31. package/cli/context-mcp.js +0 -0
  32. package/cli/context.js +13 -2
  33. package/cli/delivery.js +0 -0
  34. package/cli/disavow.js +0 -0
  35. package/cli/doctor.js +15 -4
  36. package/cli/env.js +0 -0
  37. package/cli/fleet.js +30 -19
  38. package/cli/gsc-browser.js +0 -0
  39. package/cli/import.js +7 -1
  40. package/cli/index-observations.js +0 -0
  41. package/cli/index-submissions.js +0 -0
  42. package/cli/key-store.js +115 -0
  43. package/cli/ledger.js +1 -0
  44. package/cli/lifecycle.js +0 -0
  45. package/cli/linktrail.mjs +0 -0
  46. package/cli/local-result.js +41 -6
  47. package/cli/main.js +85 -20
  48. package/cli/mcp-client.js +2 -2
  49. package/cli/migration.js +308 -0
  50. package/cli/mirror.js +0 -0
  51. package/cli/mix.js +0 -0
  52. package/cli/native-lock.js +0 -0
  53. package/cli/oauth-credentials.js +0 -0
  54. package/cli/oauth-flow.js +0 -0
  55. package/cli/placement-projection.js +106 -0
  56. package/cli/platforms.js +0 -0
  57. package/cli/prompt.js +25 -0
  58. package/cli/public-http.js +168 -0
  59. package/cli/receipt-performance.js +0 -0
  60. package/cli/receipts.js +0 -0
  61. package/cli/report.js +51 -19
  62. package/cli/setup-plan.js +0 -0
  63. package/cli/skill.js +0 -0
  64. package/cli/state.js +76 -29
  65. package/cli/status.js +14 -18
  66. package/cli/sync-lock.js +0 -0
  67. package/cli/sync.js +23 -4
  68. package/package.json +7 -8
  69. package/references/citation-panels.md +2 -0
  70. package/references/cli.md +2 -2
  71. package/references/disavow.md +1 -1
  72. package/references/discovery-handoff.md +1 -1
  73. package/references/local-crm-draft-handoff.md +1 -1
  74. package/references/portable-ledger-handoff.md +11 -10
  75. package/references/recipes/catalog.json +263 -7
  76. package/references/recipes/command-notes.md +5 -3
  77. package/references/recipes/internal-link-improvement.md +25 -0
  78. package/references/recipes/placement-reconciliation.md +53 -1
  79. package/references/recipes/qualified-campaign-handoff.md +0 -0
  80. package/references/recipes/site-context-brief.md +0 -0
  81. package/references/recipes/sourced-linkable-asset.md +0 -0
  82. package/references/reports.md +0 -0
  83. package/scripts/agentlinkops.py +0 -0
  84. package/scripts/disavow.py +0 -0
  85. package/scripts/linktrail.py +0 -0
  86. package/scripts/migrations/001_crm.sql +0 -0
  87. package/scripts/migrations/002_cloud_events.sql +0 -0
  88. package/scripts/migrations/003_disavow.sql +0 -0
  89. package/scripts/migrations/004_placement_costs.sql +0 -0
  90. package/scripts/outreach-crm-draft.py +0 -0
  91. package/scripts/portable-handoff.py +17 -4
  92. package/scripts/report.py +0 -0
  93. package/shared/admission-contract.js +3 -2
  94. package/shared/admission-report-contract.js +3 -2
  95. package/shared/agent-clients.js +0 -0
  96. package/shared/analytics-history-contract.js +15 -0
  97. package/shared/campaign-outcome-contract.js +12 -0
  98. package/shared/campaign-prospect-contract.js +23 -0
  99. package/shared/citation-evidence-contract.js +0 -0
  100. package/shared/classification-contract.js +54 -0
  101. package/shared/contact-record-contract.js +27 -0
  102. package/shared/disavow-contract.js +37 -9
  103. package/shared/exec-grammar.js +0 -0
  104. package/shared/index-observation-contract.js +5 -4
  105. package/shared/internal-links-contract.js +143 -0
  106. package/shared/lifecycle-contract.js +0 -0
  107. package/shared/lifecycle-money.js +0 -0
  108. package/shared/link-category-contract.js +35 -0
  109. package/shared/link-category.js +138 -0
  110. package/shared/link-labels.js +26 -0
  111. package/shared/link-summary-contract.js +17 -0
  112. package/shared/monitoring-chronology-export.js +57 -0
  113. package/shared/monitoring-read-contract.js +41 -0
  114. package/shared/observation-event.js +1 -1
  115. package/shared/onboarding.js +20 -0
  116. package/shared/opportunity-record.js +0 -0
  117. package/shared/response-contract.js +41 -5
  118. package/shared/search-connection-contract.js +2 -1
  119. package/shared/table-page-contract.js +13 -0
  120. package/skills/agentlinkops-assets/SKILL-DEPS.json +290 -0
  121. package/skills/agentlinkops-assets/SKILL.md +3 -1
  122. package/skills/agentlinkops-assets/agents/openai.yaml +1 -1
  123. package/skills/agentlinkops-assets/references/portable/references/cli.md +220 -0
  124. package/skills/agentlinkops-assets/references/portable/references/disavow.md +73 -0
  125. package/skills/agentlinkops-assets/references/portable/references/reports.md +53 -0
  126. package/skills/agentlinkops-assets/references/portable/scripts/agentlinkops.py +676 -0
  127. package/skills/agentlinkops-assets/references/portable/scripts/disavow.py +207 -0
  128. package/skills/agentlinkops-assets/references/portable/scripts/migrations/001_crm.sql +45 -0
  129. package/skills/agentlinkops-assets/references/portable/scripts/migrations/002_cloud_events.sql +13 -0
  130. package/skills/agentlinkops-assets/references/portable/scripts/migrations/003_disavow.sql +20 -0
  131. package/skills/agentlinkops-assets/references/portable/scripts/migrations/004_placement_costs.sql +2 -0
  132. package/skills/agentlinkops-assets/references/portable/scripts/report.py +113 -0
  133. package/skills/agentlinkops-assets/references/portable/templates/campaigns.v1.json +65 -0
  134. package/skills/agentlinkops-campaigns/SKILL-DEPS.json +336 -0
  135. package/skills/agentlinkops-campaigns/SKILL.md +7 -5
  136. package/skills/agentlinkops-campaigns/agents/openai.yaml +1 -1
  137. package/skills/agentlinkops-campaigns/references/first-campaign.md +31 -0
  138. package/skills/agentlinkops-campaigns/references/mailbox-campaign.md +46 -0
  139. package/skills/agentlinkops-campaigns/references/portable/references/cli.md +220 -0
  140. package/skills/agentlinkops-campaigns/references/portable/references/disavow.md +73 -0
  141. package/skills/agentlinkops-campaigns/references/portable/references/reports.md +53 -0
  142. package/skills/agentlinkops-campaigns/references/portable/scripts/agentlinkops.py +676 -0
  143. package/skills/agentlinkops-campaigns/references/portable/scripts/disavow.py +207 -0
  144. package/skills/agentlinkops-campaigns/references/portable/scripts/migrations/001_crm.sql +45 -0
  145. package/skills/agentlinkops-campaigns/references/portable/scripts/migrations/002_cloud_events.sql +13 -0
  146. package/skills/agentlinkops-campaigns/references/portable/scripts/migrations/003_disavow.sql +20 -0
  147. package/skills/agentlinkops-campaigns/references/portable/scripts/migrations/004_placement_costs.sql +2 -0
  148. package/skills/agentlinkops-campaigns/references/portable/scripts/report.py +113 -0
  149. package/skills/agentlinkops-campaigns/references/portable/templates/campaigns.v1.json +65 -0
  150. package/skills/agentlinkops-connect/SKILL-DEPS.json +104 -0
  151. package/skills/agentlinkops-connect/SKILL.md +2 -0
  152. package/skills/agentlinkops-connect/agents/openai.yaml +1 -1
  153. package/skills/agentlinkops-connect/references/agentlinkops.md +164 -61
  154. package/skills/agentlinkops-connect/references/placement-workflows.md +19 -0
  155. package/skills/agentlinkops-crm/SKILL-DEPS.json +381 -0
  156. package/skills/agentlinkops-crm/SKILL.md +26 -9
  157. package/skills/agentlinkops-crm/agents/openai.yaml +1 -1
  158. package/skills/agentlinkops-crm/references/portable/references/cli.md +220 -0
  159. package/skills/agentlinkops-crm/references/portable/references/disavow.md +73 -0
  160. package/skills/agentlinkops-crm/references/portable/references/local-crm-draft-handoff.md +76 -0
  161. package/skills/agentlinkops-crm/references/portable/references/portable-ledger-handoff.md +51 -0
  162. package/skills/agentlinkops-crm/references/portable/references/reports.md +53 -0
  163. package/skills/agentlinkops-crm/references/portable/scripts/agentlinkops.py +676 -0
  164. package/skills/agentlinkops-crm/references/portable/scripts/disavow.py +207 -0
  165. package/skills/agentlinkops-crm/references/portable/scripts/migrations/001_crm.sql +45 -0
  166. package/skills/agentlinkops-crm/references/portable/scripts/migrations/002_cloud_events.sql +13 -0
  167. package/skills/agentlinkops-crm/references/portable/scripts/migrations/003_disavow.sql +20 -0
  168. package/skills/agentlinkops-crm/references/portable/scripts/migrations/004_placement_costs.sql +2 -0
  169. package/skills/agentlinkops-crm/references/portable/scripts/outreach-crm-draft.py +121 -0
  170. package/skills/agentlinkops-crm/references/portable/scripts/portable-handoff.py +197 -0
  171. package/skills/agentlinkops-crm/references/portable/scripts/report.py +113 -0
  172. package/skills/agentlinkops-crm/references/portable/templates/campaigns.v1.json +65 -0
  173. package/skills/agentlinkops-discovery/SKILL-DEPS.json +301 -0
  174. package/skills/agentlinkops-discovery/SKILL.md +4 -2
  175. package/skills/agentlinkops-discovery/references/portable/references/cli.md +220 -0
  176. package/skills/agentlinkops-discovery/references/portable/references/disavow.md +73 -0
  177. package/skills/agentlinkops-discovery/references/portable/references/discovery-handoff.md +32 -0
  178. package/skills/agentlinkops-discovery/references/portable/references/reports.md +53 -0
  179. package/skills/agentlinkops-discovery/references/portable/scripts/agentlinkops.py +676 -0
  180. package/skills/agentlinkops-discovery/references/portable/scripts/disavow.py +207 -0
  181. package/skills/agentlinkops-discovery/references/portable/scripts/migrations/001_crm.sql +45 -0
  182. package/skills/agentlinkops-discovery/references/portable/scripts/migrations/002_cloud_events.sql +13 -0
  183. package/skills/agentlinkops-discovery/references/portable/scripts/migrations/003_disavow.sql +20 -0
  184. package/skills/agentlinkops-discovery/references/portable/scripts/migrations/004_placement_costs.sql +2 -0
  185. package/skills/agentlinkops-discovery/references/portable/scripts/report.py +113 -0
  186. package/skills/agentlinkops-discovery/references/portable/templates/campaigns.v1.json +65 -0
  187. package/skills/agentlinkops-discovery/references/saved-backlink-workflows.md +23 -0
  188. package/src/citations/adapter.js +0 -0
  189. package/src/citations/alert.js +0 -0
  190. package/src/citations/browser/chatgpt-citations.js +0 -0
  191. package/src/citations/browser/engine.js +0 -0
  192. package/src/citations/browser/gen/core/providers/_shared/rawSourcesDom.js +0 -0
  193. package/src/citations/browser/gen/core/providers/_shared/resetProviderPage.js +0 -0
  194. package/src/citations/browser/gen/core/providers/_shared/sourceUtils.js +0 -0
  195. package/src/citations/browser/gen/core/providers/ai-overview/index.js +0 -0
  196. package/src/citations/browser/gen/core/providers/ai-overview/lib/expand.js +0 -0
  197. package/src/citations/browser/gen/core/providers/ai-overview/lib/extractSources.js +0 -0
  198. package/src/citations/browser/gen/core/providers/ai-overview/lib/pageLifecycle.js +0 -0
  199. package/src/citations/browser/gen/core/providers/ai-overview/lib/session.js +0 -0
  200. package/src/citations/browser/gen/core/providers/chatgpt/index.js +0 -0
  201. package/src/citations/browser/gen/core/providers/chatgpt/lib/dismissAuthModal.js +0 -0
  202. package/src/citations/browser/gen/core/providers/chatgpt/lib/extractSources.js +0 -0
  203. package/src/citations/browser/gen/core/providers/chatgpt/lib/pageLifecycle.js +0 -0
  204. package/src/citations/browser/gen/core/providers/claude/index.js +0 -0
  205. package/src/citations/browser/gen/core/providers/claude/lib/extractSources.js +0 -0
  206. package/src/citations/browser/gen/core/providers/claude/lib/pageLifecycle.js +0 -0
  207. package/src/citations/browser/gen/core/providers/gemini/index.js +0 -0
  208. package/src/citations/browser/gen/core/providers/gemini/lib/extractSources.js +0 -0
  209. package/src/citations/browser/gen/core/providers/gemini/lib/pageLifecycle.js +0 -0
  210. package/src/citations/browser/gen/core/providers/gemini/lib/session.js +0 -0
  211. package/src/citations/browser/gen/core/providers/index.js +0 -0
  212. package/src/citations/browser/gen/core/providers/perplexity/index.js +0 -0
  213. package/src/citations/browser/gen/core/providers/perplexity/lib/extractSources.js +0 -0
  214. package/src/citations/browser/gen/core/providers/perplexity/lib/navigation.js +0 -0
  215. package/src/citations/browser/gen/lib/browser/camoufox.js +0 -0
  216. package/src/citations/browser/gen/lib/browser/display.js +0 -0
  217. package/src/citations/browser/gen/lib/browser/domOps.js +0 -0
  218. package/src/citations/browser/gen/lib/browser/humanBehavior.js +0 -0
  219. package/src/citations/browser/gen/lib/browser/navigate.js +0 -0
  220. package/src/citations/browser/gen/lib/extraction/sourceUtils.js +0 -0
  221. package/src/citations/browser/gen/lib/input/editor/clearInput.js +0 -0
  222. package/src/citations/browser/gen/lib/input/editor/findEditor.js +0 -0
  223. package/src/citations/browser/gen/lib/input/editor/promptInput.js +0 -0
  224. package/src/citations/browser/gen/lib/input/markdown/converter.js +0 -0
  225. package/src/citations/browser/gen/lib/input/markdown/toMarkdown.js +0 -0
  226. package/src/citations/browser/gen/lib/input/response/detectBotPage.js +0 -0
  227. package/src/citations/browser/gen/lib/input/response/isGenerating.js +0 -0
  228. package/src/citations/browser/gen/lib/input/response/waitForFinish.js +0 -0
  229. package/src/citations/browser/gen/lib/input/sources/findButton.js +0 -0
  230. package/src/citations/browser/gen/lib/input/sources/openPanel.js +0 -0
  231. package/src/citations/browser/gen/utils/agent-constants.js +0 -0
  232. package/src/citations/browser/providers.js +0 -0
  233. package/src/citations/browser/proxy-admission.js +0 -0
  234. package/src/citations/browser/shims/errors.js +0 -0
  235. package/src/citations/browser/shims/types.js +0 -0
  236. package/src/citations/browser/shims/utils.js +0 -0
  237. package/src/citations/contract.js +0 -0
  238. package/src/citations/egress-receipts.js +0 -0
  239. package/src/citations/evidence-integrity.js +0 -0
  240. package/src/citations/inventory.js +0 -0
  241. package/src/citations/login.js +0 -0
  242. package/src/citations/match.js +0 -0
  243. package/src/citations/panel-builder.js +0 -0
  244. package/src/citations/report.js +0 -0
  245. package/src/citations/runner.js +0 -0
  246. package/src/citations/stats.js +0 -0
  247. package/src/citations/sync.js +0 -0
  248. package/src/citations/watch.js +0 -0
  249. package/src/competitors/contract.js +17 -1
  250. package/src/competitors/domain-mix.js +0 -0
  251. package/src/context/ga4.js +0 -0
  252. package/src/context/gsc.js +0 -0
  253. package/src/context/site-profile.js +34 -121
  254. package/src/context/util.js +0 -0
  255. package/src/context-tool-registry.js +0 -0
  256. package/src/corpus/acquire.js +68 -32
  257. package/src/corpus/extract.js +147 -0
  258. package/src/corpus/frontier.js +151 -0
  259. package/src/corpus/seeds.js +0 -0
  260. package/src/discovery/contract.js +8 -0
  261. package/src/discovery/import.js +13 -1
  262. package/src/evidence-reference.js +0 -0
  263. package/src/index-observation.js +0 -0
  264. package/src/locator.js +0 -0
  265. package/src/mentions.js +193 -0
  266. package/src/page-acquisition/acquire.js +50 -0
  267. package/src/page-acquisition/bytes.js +138 -0
  268. package/src/page-acquisition/capture.js +11 -0
  269. package/src/page-acquisition/contracts.js +67 -0
  270. package/src/page-acquisition/documents.js +166 -0
  271. package/src/page-acquisition/http.js +307 -0
  272. package/src/page-acquisition/policy.js +20 -0
  273. package/src/page-acquisition/recovery.js +164 -0
  274. package/src/page-acquisition/render-failure.js +82 -0
  275. package/src/page-extraction/html.js +359 -0
  276. package/src/page-extraction/index.js +30 -0
  277. package/src/page-extraction/metadata.js +16 -0
  278. package/src/response-shaping.js +4 -3
  279. package/src/tool-views.js +23 -12
  280. package/src/verifier/fetch.js +4 -339
  281. package/src/verifier/index.js +81 -23
  282. package/src/verifier/observation-kind.js +0 -0
  283. package/src/verifier/page-quality.js +14 -0
  284. package/src/verifier/parser.js +2 -241
  285. package/src/verifier/readiness.js +64 -13
  286. package/src/verifier/render-evidence.js +25 -2
  287. package/src/verifier/robots.js +0 -0
  288. package/src/verifier/target.js +201 -0
  289. package/src/verifier/transition.js +18 -3
  290. package/src/verifier/url.js +31 -0
  291. package/src/watch-crawlability.js +144 -0
  292. package/templates/SITE.template.md +0 -0
  293. package/templates/campaigns.v1.json +0 -0
  294. package/skills/content-defingerprinting/SKILL.md +0 -29
  295. package/skills/content-defingerprinting/scripts/watermarks/README.md +0 -102
  296. package/skills/content-defingerprinting/scripts/watermarks/UPSTREAM.json +0 -26
  297. package/skills/content-defingerprinting/scripts/watermarks/revendor.sh +0 -52
  298. package/skills/content-defingerprinting/scripts/watermarks/vendor/LICENSE.upstream +0 -21
  299. package/skills/content-defingerprinting/scripts/watermarks/vendor/clean_text.py +0 -96
  300. package/skills/content-defingerprinting/scripts/watermarks/vendor/common.py +0 -432
  301. package/skills/content-defingerprinting/scripts/watermarks/vendor/inspect_text.py +0 -86
  302. package/skills/content-defingerprinting/scripts/watermarks/vendor/score_stylometry.py +0 -460
  303. package/skills/content-defingerprinting/scripts/watermarks/vendor/text_unicode.py +0 -651
  304. package/skills/content-defingerprinting/scripts/watermarks/verify-vendor.py +0 -27
  305. package/skills/content-defingerprinting/scripts/watermarks/wm-scan.py +0 -212
  306. package/skills/internal-linking-optimizer/SKILL.md +0 -56
  307. package/skills/internal-linking-optimizer/references/source-prompt.md +0 -76
  308. package/skills/pre-publish-review/SKILL.md +0 -179
  309. package/skills/source-cited-content-builder/SKILL.md +0 -56
  310. package/skills/source-cited-content-builder/references/source-prompt.md +0 -19
  311. package/skills/source-cited-humanizer/SKILL.md +0 -20
@@ -0,0 +1,16 @@
1
+ import { parseDocument } from './html.js';
2
+
3
+ /** Pure metadata extraction; no requests, supplier calls or judgments. */
4
+ export function extractPageMetadata(html, { baseUrl } = {}) {
5
+ if (typeof html !== 'string' || !html) return {
6
+ title: null, h1s: [], meta_description: null, canonical: null,
7
+ link_counts: null, asset_candidate_reasons: null,
8
+ };
9
+ // A private resolution base lets the parser count relative physical links.
10
+ // It must never escape as a claimed publisher canonical URL.
11
+ const resolutionBase = baseUrl ?? 'https://page-metadata.invalid/';
12
+ const parsed = parseDocument(html, resolutionBase, { includeMetadata: true });
13
+ if (parsed.incomplete) throw Object.assign(new Error('incomplete_document'), { reason: 'incomplete_document' });
14
+ if (baseUrl === undefined && parsed.metadata.canonical?.startsWith('https://page-metadata.invalid/')) parsed.metadata.canonical = null;
15
+ return parsed.metadata;
16
+ }
@@ -13,17 +13,18 @@ const pick = (row, keys) => Object.fromEntries(keys.filter(key => row[key] !== u
13
13
  const attempt = value => value && typeof value === 'object' ? pick(value, ['state','reason','checkedAt','httpStatus','occurrenceCount','checkerVersion']) : value;
14
14
  export function conciseObservationState(state) {
15
15
  if (!state || typeof state !== 'object') return state ?? {};
16
- const out = pick(state, ['state','uncertain','checked_at','reason','confirmedState','lastChangedAt','firstAbsentAt','consecutiveAbsent','nextCheckAt','lastSuccessfulAt','lastHealthyAt','firstUnavailableAt','unavailableCount','retryNotBefore']);
16
+ const out = pick(state, ['state','wasEverPresent','uncertain','checked_at','reason','confirmedState','lastChangedAt','firstAbsentAt','consecutiveAbsent','nextCheckAt','lastSuccessfulAt','lastHealthyAt','firstUnavailableAt','unavailableCount','retryNotBefore']);
17
17
  if (state.latestAttempt !== undefined) out.latestAttempt = attempt(state.latestAttempt);
18
18
  if (state.lastSuccessfulObservation !== undefined) out.lastSuccessfulObservation = attempt(state.lastSuccessfulObservation);
19
19
  if (state.lastSuccessfulObservationSameAsLatestAttempt !== undefined) out.lastSuccessfulObservationSameAsLatestAttempt = state.lastSuccessfulObservationSameAsLatestAttempt;
20
20
  return out;
21
21
  }
22
22
  const withState = (row, keys) => ({ ...pick(row, keys), observation_state: conciseObservationState(row.observation_state) });
23
+ const categoryFields=['category_contract','chosen_category','page_assessment','display_category'];
23
24
  // An event snapshot (shared/observation-event.js) minus the occurrence list, expectations and
24
25
  // signatures: the state, uncertainty, check state, reason, status and timing stay.
25
26
  const conciseSnapshot = snapshot => snapshot && typeof snapshot === 'object'
26
- ? pick(snapshot, ['state','uncertain','check_state','reason','checked_at','http_status','occurrence_count','occurrences_truncated','source_url','target_url','url','next_check_at','watch_id','target_id'])
27
+ ? pick(snapshot, ['state','wasEverPresent','uncertain','check_state','reason','checked_at','http_status','occurrence_count','occurrences_truncated','source_url','target_url','url','next_check_at','watch_id','target_id'])
27
28
  : snapshot;
28
29
  const observationResult = result => result && typeof result === 'object'
29
30
  ? { ...pick(result, ['state','reason','checkedAt','httpStatus','checkerVersion','evidence','redirects']), occurrenceCount: result.occurrenceCount ?? result.occurrences?.length ?? 0 }
@@ -34,7 +35,7 @@ const observationResult = result => result && typeof result === 'object'
34
35
  export const SHAPES = {
35
36
  list_rank_history: { rows: 'items', keeps: ['overview_run_id','slot_at','rank','observed_at','retrieved_at','freshness','outcome','execution','cost'],
36
37
  row: r => pick(r, ['overview_run_id','slot_at','rank','observed_at','retrieved_at','freshness','outcome','execution','cost']) },
37
- list_link_watches: { rows: 'items', keeps: ['id','source_url','target_url','status','state','observation_state','next_check_at','local_reference'], row: r => withState(r, ['id','source_url','target_url','status','state','next_check_at','local_reference']) },
38
+ list_link_watches: { rows: 'items', keeps: ['id','workspace_id','project_id','source_url','target_url','status','state','observation_state','next_check_at','local_reference','intent','outreachStatus','lastContactedAt','campaign',...categoryFields], row: r => withState(r, ['id','workspace_id','project_id','source_url','target_url','status','state','next_check_at','local_reference','intent','outreachStatus','lastContactedAt','campaign',...categoryFields]) },
38
39
  list_targets: { rows: 'items', keeps: ['id','url','status','state','observation_state','next_check_at'], row: r => withState(r, ['id','url','status','state','next_check_at']) },
39
40
  get_target_placements: { rows: 'items', keeps: ['id','source_url','target_url','target_scope','status','state'], row: r => pick(r, ['id','source_url','target_url','target_scope','status','state']) },
40
41
  list_check_jobs: { rows: 'items', keeps: ['id','state','type','watch_id','target_id','attempt_count','error_code','created_at','completed_at'], row: r => pick(r, ['id','state','type','watch_id','target_id','attempt_count','error_code','created_at','completed_at']) },
package/src/tool-views.js CHANGED
@@ -3,24 +3,28 @@
3
3
  // script, the MCP router, the CLI index and the generators all read the same definitions.
4
4
  // Rules: docs/technical/AGENT-SURFACE-CONTRACT.md (DP-0036).
5
5
 
6
- export const TOOLSETS = ['monitoring','evidence','rank','discovery','baseline','library','competitors','reports','workspace','notifications','webhooks','admission','disavow','lifecycle','digests'];
6
+ export const TOOLSETS = ['monitoring','classification','evidence','rank','licensed','discovery','baseline','library','competitors','internal-links','reports','workspace','notifications','webhooks','admission','disavow','lifecycle','campaigns','digests'];
7
7
 
8
8
  // One line per toolset: what the group is for. Printed by the site brief, the REST index and the
9
9
  // catalog snapshot so the same wording reaches every client; a toolset without a summary is a
10
10
  // gate failure in the measurement script.
11
11
  export const TOOLSET_SUMMARIES = {
12
12
  admission: 'Project rules, URL previews and retained admission decisions.',
13
- disavow: 'Propose, review and export website disavow rules.',
13
+ disavow: 'Propose, review and export website disavow rules; check the copy Google holds (handoffs).',
14
14
  lifecycle: 'Placement costs, expiry, renewal events and currency reports.',
15
- digests: 'Own-address digest preferences, delivery history and exact events.',
15
+ campaigns: 'Campaign contacts and supplied outreach outcomes.',
16
+ digests: 'Own-address digest delivery history and exact retained events.',
16
17
  monitoring: 'Watch earned links and destination URLs: create, list, update, import, export, recheck.',
18
+ classification: 'Manual link types and tags with retained placement context and metadata.',
17
19
  evidence: 'What a check observed: histories, snapshots, change feeds, check jobs, published contacts.',
18
20
  rank: 'Synthetic DataForSEO rank schedules and dated history; hosted collection disabled.',
21
+ licensed: 'Licensed source-search evidence and charge custody; operator admission required.',
19
22
  discovery: 'Import candidate rows, read stored runs, verify selected candidates, enroll them.',
20
- baseline: 'Scoped backlink baselines; supplier admission disabled.',
23
+ baseline: 'Scoped backlink baselines; new live work requires admission in this deployment.',
21
24
  library: 'Read and export the coverage-stated opportunity library.',
22
- competitors: 'Competitor sets, dated inventories, scheduled refresh, gap and domain-mix reports.',
23
- reports: 'Profile, anchor and report summaries over the tracked dataset.',
25
+ competitors: 'Competitor sets, dated inventories, scheduled refresh and gap reports.',
26
+ 'internal-links': 'Owned-site page inventories, bounded captures, internal-link health and graph evidence.',
27
+ reports: 'Placement chronology, service assurance, backlink and domain-mix reports.',
24
28
  workspace: 'Workspace, projects, usage, members, invitations, scratch-resource cleanup.',
25
29
  notifications: 'Email notification preferences, previews, tests and deliveries.',
26
30
  webhooks: 'Webhook endpoints, state, secrets and delivery records.',
@@ -56,15 +60,22 @@ export function oneLiner(description, max = 60) {
56
60
  return `${cut.slice(0, Math.max(cut.lastIndexOf(' '), 20)).replace(/[,;:]$/, '')}…`;
57
61
  }
58
62
 
59
- // entries: [{ name, description, toolset, tier, readOnly }]. Grouped, one line per command.
60
- export function manifestText(entries, { max = 36 } = {}) {
63
+ // entries: [{ name, description, toolset, tier, readOnly }]. Grouped under each toolset's summary,
64
+ // one line per command with its core and write markers. Only core commands carry a one-liner;
65
+ // a deferred command's detail is one describe away. Per-command one-liners were cut from 28 to
66
+ // 23 characters as commands were added until the 2,500-token budget (measure:tools) had no room
67
+ // left; names under a toolset summary keep that budget without a further cut per command.
68
+ export function manifestText(entries, { max = 23 } = {}) {
61
69
  const groups = new Map(TOOLSETS.map(t => [t, []]));
62
70
  for (const entry of entries) (groups.get(entry.toolset) ?? groups.set(entry.toolset, []).get(entry.toolset)).push(entry);
63
- const lines = ['Commands by toolset (name: what it does). On /mcp use exec: `exec search <words>`, `exec describe <name>`, `exec call <name> <json>`; the CLI mirrors the verbs as `agentlinkops tools | describe | call`.'];
71
+ const lines = ['Commands by toolset. exec help / agentlinkops --help.' + (entries.some(entry=>entry.toolset==='licensed') ? ' Licensed: operator admission.' : '')];
64
72
  for (const [toolset, members] of groups) {
65
73
  if (!members.length) continue;
66
- lines.push(`\n${toolset}:`);
67
- for (const entry of members.sort((a, b) => a.name.localeCompare(b.name))) lines.push(`- ${entry.name}${entry.tier === 'core' ? ' (core)' : ''}${entry.readOnly ? '' : ' [write]'}: ${oneLiner(entry.description, max)}`);
74
+ lines.push(`\n${toolset}:${TOOLSET_SUMMARIES[toolset] ? ` ${TOOLSET_SUMMARIES[toolset]}` : ''}`);
75
+ for (const entry of members.sort((a, b) => a.name.localeCompare(b.name))) {
76
+ const core = entry.tier === 'core';
77
+ lines.push(`- ${entry.name}${core ? ' (core)' : ''}${entry.readOnly ? '' : ' [write]'}${core ? `: ${oneLiner(entry.description, max)}` : ''}`);
78
+ }
68
79
  }
69
80
  return lines.join('\n');
70
81
  }
@@ -137,7 +148,7 @@ export function viewMembers(entries, view) {
137
148
  // stays registered and callable there through exec call and by a direct tools/call.
138
149
  if (view === 'default') return [];
139
150
  if (view === 'code') return [];
140
- const m = /^toolset:([a-z]+)(:readonly)?$/.exec(view);
151
+ const m = /^toolset:([a-z]+(?:-[a-z]+)*)(:readonly)?$/.exec(view);
141
152
  if (!m) throw new Error(`Unknown view ${view}`);
142
153
  const members = visible.filter(e => e.toolset === m[1] || e.tier === 'core');
143
154
  return m[2] ? members.filter(e => e.readOnly) : members;
@@ -1,339 +1,4 @@
1
- import { validatePublicUrl } from './url.js';
2
- import { robotsDecision } from './robots.js';
3
-
4
- export class VerificationError extends Error {
5
- constructor(reason, details = {}) {
6
- super(reason);
7
- this.name = 'VerificationError';
8
- this.reason = reason;
9
- Object.assign(this, details);
10
- }
11
- }
12
-
13
- function cancelBody(response) {
14
- // Cancellation must not extend the request deadline on a misbehaving transport.
15
- response.body?.cancel().catch(() => {});
16
- }
17
-
18
- export async function readBounded(response, maxBytes, signal) {
19
- const length = response.headers.get('content-length');
20
- const encoded = response.headers.get('content-encoding');
21
- if (!encoded && length && Number(length) > maxBytes) {
22
- cancelBody(response);
23
- throw new VerificationError('body_too_large');
24
- }
25
- const reader = response.body?.getReader();
26
- if (!reader) return { text: '', bytes: 0 };
27
- const chunks = [];
28
- let bytes = 0;
29
- try {
30
- while (true) {
31
- const { done, value } = await withAbort(reader.read(), signal);
32
- if (done) break;
33
- bytes += value.byteLength;
34
- if (bytes > maxBytes) throw new VerificationError('body_too_large');
35
- chunks.push(value);
36
- }
37
- if (!encoded && length && Number(length) !== bytes) throw new VerificationError('incomplete_body');
38
- const combined = new Uint8Array(bytes);
39
- let offset = 0;
40
- for (const chunk of chunks) { combined.set(chunk, offset); offset += chunk.byteLength; }
41
- const contentType = response.headers.get('content-type') ?? '';
42
- const charset = /charset\s*=\s*["']?([^\s;"']+)/iu.exec(contentType)?.[1] ?? 'utf-8';
43
- let text;
44
- try { text = new TextDecoder(charset, { fatal: true }).decode(combined); }
45
- catch { throw new VerificationError('unsupported_or_invalid_encoding'); }
46
- return { text, bytes, data: combined };
47
- } catch (error) {
48
- reader.cancel().catch(() => {});
49
- throw error;
50
- } finally {
51
- reader.releaseLock();
52
- }
53
- }
54
-
55
- function withAbort(promise, signal) {
56
- if (signal.aborted) return Promise.reject(new VerificationError('timeout'));
57
- return new Promise((resolve, reject) => {
58
- const onAbort = () => reject(new VerificationError('timeout'));
59
- signal.addEventListener('abort', onAbort, { once: true });
60
- promise.then(resolve, reject).finally(() => signal.removeEventListener('abort', onAbort));
61
- });
62
- }
63
-
64
- /** One shared budget for robots, redirects, hooks and source-body reads. */
65
- export const PRODUCT_TOKEN = 'LinktrailBot';
66
-
67
- // A day. Anything longer is a publisher saying "not today", and a scheduler that honoured a
68
- // month-long Retry-After would silently retire the check instead of retrying it.
69
- export const MAX_RETRY_AFTER_SECONDS = 86_400;
70
-
71
- /**
72
- * RFC 9110 `Retry-After`: delta-seconds or an HTTP-date. Returns null for anything else.
73
- *
74
- * Both forms appear in the wild and a parser that reads only the integer form silently discards
75
- * the date form, which is the one a CDN tends to send.
76
- */
77
- export function retryAfterSeconds(value, now = Date.now()) {
78
- if (!value) return null;
79
- const text = String(value).trim();
80
- if (/^\d+$/u.test(text)) {
81
- const seconds = Number(text);
82
- return Number.isFinite(seconds) ? Math.min(seconds, MAX_RETRY_AFTER_SECONDS) : null;
83
- }
84
- // `Date.parse` accepts far more than an HTTP-date — it reads "-5" as a year and returns a real
85
- // timestamp, which turned a malformed header into a wait of zero. An IMF-fixdate always carries
86
- // a month and day name, so requiring a letter is enough to keep the loose parser off junk.
87
- if (!/[A-Za-z]/u.test(text)) return null;
88
- const at = Date.parse(text);
89
- if (!Number.isFinite(at)) return null;
90
- // A date already in the past means "now", never a negative wait.
91
- return Math.min(Math.max(0, Math.ceil((at - now) / 1000)), MAX_RETRY_AFTER_SECONDS);
92
- }
93
-
94
- export function publicFetcher(options, signal) {
95
- const fetchImpl = options.fetchImpl ?? globalThis.fetch;
96
- const robotsByOrigin = new Map();
97
- // The product token is what robots matches on and never changes. A contact URL may be appended
98
- // so a publisher who sees us in their logs can find out who we are — the convention every
99
- // reputable crawler follows, and the thing most likely to move a reflexive 403 or 429.
100
- //
101
- // It defaults to ABSENT on purpose. It was written before AgentLinkOps had a public domain, and a bot
102
- // advertising a URL that does not resolve is worse than one advertising nothing: it reads as
103
- // impersonation rather than identification. Set it when a real domain exists.
104
- const userAgent = options.userAgentContact
105
- ? `${PRODUCT_TOKEN}/0.1 (+${String(options.userAgentContact).slice(0, 200)})`
106
- : `${PRODUCT_TOKEN}/0.1`;
107
- const maxRedirects = Math.min(options.maxRedirects ?? 5, 5);
108
- const maxBytes = Math.min(options.maxBytes ?? 2 * 1024 * 1024, 2 * 1024 * 1024);
109
-
110
- async function request(url, context = {}) {
111
- const checked = validatePublicUrl(url);
112
- if (!checked.valid) throw new VerificationError(`unsafe_url:${checked.reason}`);
113
- if (options.beforeFetch) await withAbort(Promise.resolve().then(() => options.beforeFetch(checked.url, { ...context, signal })), signal);
114
- let response;
115
- try {
116
- response = await withAbort(Promise.resolve().then(() => {
117
- if (signal.aborted) throw new VerificationError('timeout');
118
- // Conditional headers ride only on the request that earned them. A validator belongs
119
- // to one resource; across a redirect hop it would be re-sent to a different one, so
120
- // the caller drops validators after the first hop and the redirect loop revalidates
121
- // the destination from scratch rather than trusting a moved resource's old validators.
122
- const conditional = {};
123
- if (context.kind === 'source' && context.validators?.etag) conditional['If-None-Match'] = context.validators.etag;
124
- if (context.kind === 'source' && context.validators?.lastModified) conditional['If-Modified-Since'] = context.validators.lastModified;
125
- return fetchImpl(checked.url, {
126
- method: 'GET',
127
- redirect: 'manual',
128
- credentials: 'omit',
129
- signal,
130
- headers: {
131
- 'User-Agent': userAgent,
132
- Accept: context.kind === 'robots' ? 'text/plain' : context.accept ?? 'text/html',
133
- ...conditional,
134
- },
135
- });
136
- }), signal);
137
- } catch (error) {
138
- if (error instanceof VerificationError) throw error;
139
- throw new VerificationError(signal.aborted ? 'timeout' : 'network_error');
140
- }
141
- // Custom transports must never silently follow redirects or bypass destination checks.
142
- if (response.redirected || (response.url && new URL(response.url).href !== checked.url)) {
143
- cancelBody(response);
144
- throw new VerificationError('transport_followed_redirect');
145
- }
146
- return response;
147
- }
148
-
149
- async function fetchRobots(origin, diagnostic) {
150
- let url = `${origin}/robots.txt`;
151
- const seen = new Set();
152
- for (let redirects = 0; redirects <= maxRedirects; redirects++) {
153
- diagnostic.robotsUrl = url;
154
- diagnostic.httpStatus = null;
155
- if (seen.has(url)) throw new VerificationError('robots_redirect_loop');
156
- seen.add(url);
157
- const response = await request(url, { kind: 'robots' });
158
- diagnostic.httpStatus = response.status;
159
- // A challenge replaces the requested resource, even when its status would
160
- // otherwise mean missing robots or a redirect. Never infer policy from it.
161
- if (response.headers.get('cf-mitigated') === 'challenge') {
162
- cancelBody(response);
163
- const retry = retryAfterSeconds(response.headers.get('retry-after'));
164
- if (retry !== null) diagnostic.retryAfterSeconds = retry;
165
- throw new VerificationError('robots_access_challenge', retry === null ? {} : { retryAfterSeconds: retry });
166
- }
167
- if ([301, 302, 303, 307, 308].includes(response.status)) {
168
- cancelBody(response);
169
- const location = response.headers.get('location');
170
- if (!location) throw new VerificationError('robots_redirect_missing_location');
171
- const next = validatePublicUrl(new URL(location, url).href);
172
- if (!next.valid) throw new VerificationError(`unsafe_robots_redirect:${next.reason}`);
173
- if (diagnostic.redirects.length < 5) diagnostic.redirects.push({ from: url, to: next.url, status: response.status });
174
- url = next.url;
175
- continue;
176
- }
177
- // RFC 9309 §2.3.1.3 and Google's implementation agree: a 4xx on robots.txt means the file is
178
- // UNAVAILABLE, and an unavailable robots.txt imposes no restrictions. Only 404 and 410 were
179
- // honoured that way here, so a 401 or 403 — a WAF answering before the file is even looked
180
- // for — was read as "this publisher forbids us" and refused the whole fetch. Measured on 800
181
- // real destinations, that was 39 refusals, about 5% of the run, attributed to publishers who
182
- // had said nothing.
183
- //
184
- // Proceeding is also BETTER REPORTING even when the page then fails: `target_http_403` says
185
- // the publisher refused the page, where `robots_http_403` said we never asked.
186
- if (response.status >= 400 && response.status < 500 && response.status !== 429) {
187
- cancelBody(response);
188
- return { text: '', reason: response.status === 404 || response.status === 410 ? 'robots_not_found' : `robots_unavailable_${response.status}` };
189
- }
190
- // 429 and 5xx are the other half of the same rule and stay a refusal: they mean TEMPORARILY
191
- // unavailable, and the spec's answer to that is to stay away rather than to assume consent.
192
- if (!response.ok) {
193
- cancelBody(response);
194
- const retry = retryAfterSeconds(response.headers.get('retry-after'));
195
- if (retry !== null) diagnostic.retryAfterSeconds = retry;
196
- throw new VerificationError(`robots_http_${response.status}`, retry === null ? {} : { retryAfterSeconds: retry });
197
- }
198
- const body = await readBounded(response, 512 * 1024, signal);
199
- if (/text\/html/iu.test(response.headers.get('content-type') ?? '') || /<(?:!doctype|html|body)\b/iu.test(body.text)) {
200
- throw new VerificationError('robots_not_plain_text');
201
- }
202
- return { text: body.text, reason: 'robots_fetched' };
203
- }
204
- throw new VerificationError('robots_redirect_limit');
205
- }
206
-
207
- async function checkRobots(url, trace) {
208
- const origin = new URL(url).origin;
209
- const diagnostic = { sourceUrl: url, robotsUrl: `${origin}/robots.txt`, httpStatus: null,
210
- productToken: PRODUCT_TOKEN, allowed: null, reason: null, matchedRule: null,
211
- crawlDelaySeconds: null, fetched: null, redirects: [] };
212
- // Write before fetching/evaluating so refusals keep the failing hop, not the
213
- // last allowed origin. Both histories are capped by our redirect budget.
214
- trace.robots = diagnostic;
215
- trace.robotsHistory ??= [];
216
- if (trace.robotsHistory.length < 6) trace.robotsHistory.push(diagnostic);
217
- try {
218
- if (!robotsByOrigin.has(origin)) {
219
- const record = await fetchRobots(origin, diagnostic);
220
- robotsByOrigin.set(origin, { ...record, diagnostic: structuredClone(diagnostic) });
221
- }
222
- const record = robotsByOrigin.get(origin);
223
- Object.assign(diagnostic, structuredClone(record.diagnostic), { sourceUrl: url });
224
- const decision = robotsDecision(record.text, url);
225
- Object.assign(diagnostic, decision, { fetched: record.reason });
226
- options.onRobots?.(url, decision);
227
- if (decision.allowed !== true) throw new VerificationError(decision.reason);
228
- if (decision.crawlDelaySeconds > 0 && !options.beforeFetch) {
229
- throw new VerificationError('robots_crawl_delay_requires_scheduler');
230
- }
231
- return diagnostic;
232
- } catch (error) {
233
- diagnostic.reason = error instanceof VerificationError ? error.reason : 'verification_error';
234
- throw error;
235
- }
236
- }
237
-
238
- return {
239
- // Authorize a native browser document without refetching its body through HTTP.
240
- // The caller still needs an independently verified browser egress boundary.
241
- async authorizeNavigation(url, trace = {}) {
242
- const checked = validatePublicUrl(url);
243
- if (!checked.valid) throw new VerificationError('unsafe_render_navigation');
244
- const robots = await checkRobots(checked.url, trace);
245
- if (signal.aborted) throw new VerificationError('timeout');
246
- await options.beforeFetch?.(checked.url, { kind: 'source', crawlDelaySeconds: robots.crawlDelaySeconds, signal });
247
- if (signal.aborted) throw new VerificationError('timeout');
248
- return robots;
249
- },
250
- /**
251
- * Fetches one source document. `{ validators }` turns this into a conditional GET: the
252
- * caller passes the ETag/Last-Modified a previous fetch earned, and a 304 comes back as
253
- * `{ notModified: true }` rather than an error. A 304 we did not ask for stays an error —
254
- * the server revalidating nobody's cache is a defect, not evidence of anything.
255
- *
256
- * `{ document: true }` fetches a feed or sitemap instead of a page: the Accept header and
257
- * the content-type gate admit the XML family rather than HTML. Everything else — robots per
258
- * hop, public-destination screening, the redirect budget — is identical, because a feed is
259
- * fetched from the same publishers under the same politeness rules as a page.
260
- */
261
- async source(initialUrl, trace, { validators = null, document = false } = {}) {
262
- const accept = document
263
- ? 'application/rss+xml, application/atom+xml, application/xml;q=0.9, text/xml;q=0.9, text/plain;q=0.8'
264
- : 'text/html';
265
- let url = initialUrl;
266
- let revalidators = validators;
267
- const seen = new Set();
268
- for (let redirects = 0; redirects <= maxRedirects; redirects++) {
269
- trace.finalUrl = url;
270
- if (seen.has(url)) throw new VerificationError('redirect_loop');
271
- seen.add(url);
272
- const robots = await checkRobots(url, trace);
273
- trace.robots = robots;
274
- const response = await request(url, { kind: 'source', crawlDelaySeconds: robots.crawlDelaySeconds, validators: revalidators, accept });
275
- trace.httpStatus = response.status;
276
- trace.sourceResponse = { url, httpStatus: response.status,
277
- contentType: /^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+/iu.exec(response.headers.get('content-type') ?? '')?.[0].slice(0, 128) ?? null,
278
- retryAfterSeconds: retryAfterSeconds(response.headers.get('retry-after')),
279
- challenge: response.headers.get('cf-mitigated') === 'challenge' };
280
- if (trace.sourceResponse.challenge) {
281
- cancelBody(response);
282
- const retry = trace.sourceResponse.retryAfterSeconds;
283
- throw new VerificationError('access_challenge', retry === null ? {} : { retryAfterSeconds: retry });
284
- }
285
- if ([301, 302, 303, 307, 308].includes(response.status)) {
286
- cancelBody(response);
287
- const location = response.headers.get('location');
288
- if (!location) throw new VerificationError('redirect_missing_location');
289
- let next;
290
- try { next = validatePublicUrl(new URL(location, url).href); }
291
- catch { throw new VerificationError('invalid_redirect'); }
292
- if (!next.valid) throw new VerificationError(`unsafe_redirect:${next.reason}`);
293
- trace.redirects.push({ from: url, to: next.url, status: response.status });
294
- url = next.url;
295
- // Validators were earned for the pre-redirect resource; the destination revalidates
296
- // from scratch (see the conditional-header note in `request`).
297
- revalidators = null;
298
- continue;
299
- }
300
- if ([404, 410].includes(response.status)) {
301
- cancelBody(response);
302
- return { response, body: null };
303
- }
304
- if (response.status === 304) {
305
- cancelBody(response);
306
- if (!revalidators) throw new VerificationError('not_modified_without_baseline');
307
- return { response, body: null, notModified: true,
308
- validators: { etag: response.headers.get('etag') ?? revalidators.etag ?? null,
309
- lastModified: response.headers.get('last-modified') ?? revalidators.lastModified ?? null } };
310
- }
311
- if (response.status !== 200) {
312
- cancelBody(response);
313
- // A 429 or a 503 may state WHEN to come back. We keep it and never act on it here:
314
- // sleeping inside a request holds an origin lease and a worker for someone else's
315
- // backoff, and this system already decided that **the cron is the sleep**. Surfacing it
316
- // lets a scheduler re-queue at the stated time, the same way a robots crawl delay is
317
- // handed to the scheduler rather than slept on.
318
- throw new VerificationError(`source_http_${response.status}`,
319
- retryAfterSeconds(response.headers.get('retry-after')) === null ? {}
320
- : { retryAfterSeconds: retryAfterSeconds(response.headers.get('retry-after')) });
321
- }
322
- const contentType = response.headers.get('content-type') ?? '';
323
- // The document gate admits the XML family (`application/xml`, `text/xml`,
324
- // `application/rss+xml`, `application/atom+xml`) and plain text (the sitemap protocol's
325
- // text format). It is still a gate: a feed URL serving HTML is a page wearing a feed's
326
- // name, and parsing it as a feed would read markup as entries.
327
- const expected = document ? /(?:[+/]xml|text\/plain)(?:\s*;|$)/iu : /^text\/html(?:;|$)/iu;
328
- if (!expected.test(contentType)) {
329
- cancelBody(response);
330
- throw new VerificationError('unsupported_content_type');
331
- }
332
- const body = await readBounded(response, maxBytes, signal);
333
- return { response, body, notModified: false,
334
- validators: { etag: response.headers.get('etag'), lastModified: response.headers.get('last-modified') } };
335
- }
336
- throw new VerificationError('redirect_limit');
337
- },
338
- };
339
- }
1
+ // Compatibility facade: every verifier and installed-package consumer shares the same
2
+ // HTTP policy and error identity. Keep the legacy export names stable.
3
+ export { VerificationError, readBounded, PRODUCT_TOKEN, MAX_RETRY_AFTER_SECONDS,
4
+ retryAfterSeconds, publicFetcher } from '../page-acquisition/http.js';