agentlinkops 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/.mcp.json +0 -0
  4. package/CHANGELOG.md +17 -0
  5. package/LICENSE +0 -0
  6. package/NOTICE +0 -0
  7. package/README.md +0 -0
  8. package/bin/agentlinkops.mjs +0 -0
  9. package/bin/linktrail.mjs +0 -0
  10. package/cli/adapters/csv.js +0 -0
  11. package/cli/adapters/sqlite-crm.js +0 -0
  12. package/cli/adapters/suppliers.js +0 -0
  13. package/cli/admission.js +130 -0
  14. package/cli/agent-setup.js +1014 -101
  15. package/cli/agentlinkops.mjs +0 -0
  16. package/cli/args.js +0 -0
  17. package/cli/catalog.js +12837 -4694
  18. package/cli/check.js +0 -0
  19. package/cli/citation.js +181 -32
  20. package/cli/client.js +1 -1
  21. package/cli/commands.js +3 -3
  22. package/cli/config.js +2 -1
  23. package/cli/connection.js +0 -0
  24. package/cli/context-mcp.js +0 -0
  25. package/cli/context.js +0 -0
  26. package/cli/delivery.js +0 -0
  27. package/cli/disavow.js +54 -0
  28. package/cli/doctor.js +7 -8
  29. package/cli/env.js +0 -0
  30. package/cli/fleet.js +0 -0
  31. package/cli/gsc-browser.js +0 -0
  32. package/cli/import.js +0 -0
  33. package/cli/index-observations.js +89 -0
  34. package/cli/ledger.js +7 -1
  35. package/cli/lifecycle.js +40 -0
  36. package/cli/linktrail.mjs +0 -0
  37. package/cli/local-result.js +0 -0
  38. package/cli/main.js +99 -7
  39. package/cli/mirror.js +0 -0
  40. package/cli/mix.js +0 -0
  41. package/cli/platforms.js +0 -0
  42. package/cli/receipt-performance.js +0 -0
  43. package/cli/receipts.js +0 -0
  44. package/cli/report.js +79 -6
  45. package/cli/setup-plan.js +0 -0
  46. package/cli/skill.js +41 -4
  47. package/cli/state.js +0 -0
  48. package/cli/status.js +0 -0
  49. package/cli/sync.js +23 -1
  50. package/package.json +3 -2
  51. package/references/citation-panels.md +26 -0
  52. package/references/cli.md +0 -0
  53. package/references/disavow.md +0 -0
  54. package/references/discovery-handoff.md +32 -0
  55. package/references/recipes/catalog.json +148 -0
  56. package/references/recipes/command-notes.md +9 -0
  57. package/references/recipes/placement-reconciliation.md +41 -0
  58. package/references/recipes/qualified-campaign-handoff.md +56 -0
  59. package/references/recipes/site-context-brief.md +25 -0
  60. package/references/recipes/sourced-linkable-asset.md +25 -0
  61. package/references/reports.md +0 -0
  62. package/scripts/agentlinkops.py +0 -0
  63. package/scripts/disavow.py +0 -0
  64. package/scripts/linktrail.py +0 -0
  65. package/scripts/migrations/001_crm.sql +0 -0
  66. package/scripts/migrations/002_cloud_events.sql +0 -0
  67. package/scripts/migrations/003_disavow.sql +0 -0
  68. package/scripts/migrations/004_placement_costs.sql +0 -0
  69. package/scripts/report.py +0 -0
  70. package/shared/admission-contract.js +16 -0
  71. package/shared/admission-report-contract.js +8 -0
  72. package/shared/agent-clients.js +0 -0
  73. package/shared/citation-evidence-contract.js +27 -0
  74. package/shared/disavow-contract.js +18 -0
  75. package/shared/index-observation-contract.js +14 -0
  76. package/shared/lifecycle-contract.js +22 -0
  77. package/shared/lifecycle-money.js +24 -0
  78. package/shared/observation-event.js +12 -1
  79. package/shared/opportunity-record.js +259 -0
  80. package/shared/response-contract.js +2 -0
  81. package/shared/search-connection-contract.js +0 -0
  82. package/skills/agentlinkops-assets/SKILL.md +2 -0
  83. package/skills/agentlinkops-assets/agents/openai.yaml +0 -0
  84. package/skills/agentlinkops-campaigns/SKILL.md +5 -1
  85. package/skills/agentlinkops-campaigns/agents/openai.yaml +0 -0
  86. package/skills/agentlinkops-connect/SKILL.md +8 -0
  87. package/skills/agentlinkops-connect/agents/openai.yaml +0 -0
  88. package/skills/agentlinkops-connect/references/agentlinkops.md +9 -5
  89. package/skills/agentlinkops-crm/SKILL.md +2 -0
  90. package/skills/agentlinkops-crm/agents/openai.yaml +0 -0
  91. package/skills/agentlinkops-discovery/SKILL.md +17 -0
  92. package/skills/content-defingerprinting/SKILL.md +0 -0
  93. package/skills/content-defingerprinting/scripts/watermarks/README.md +0 -0
  94. package/skills/content-defingerprinting/scripts/watermarks/UPSTREAM.json +0 -0
  95. package/skills/content-defingerprinting/scripts/watermarks/revendor.sh +0 -0
  96. package/skills/content-defingerprinting/scripts/watermarks/vendor/LICENSE.upstream +0 -0
  97. package/skills/content-defingerprinting/scripts/watermarks/vendor/clean_text.py +0 -0
  98. package/skills/content-defingerprinting/scripts/watermarks/vendor/common.py +0 -0
  99. package/skills/content-defingerprinting/scripts/watermarks/vendor/inspect_text.py +0 -0
  100. package/skills/content-defingerprinting/scripts/watermarks/vendor/score_stylometry.py +0 -0
  101. package/skills/content-defingerprinting/scripts/watermarks/vendor/text_unicode.py +0 -0
  102. package/skills/content-defingerprinting/scripts/watermarks/verify-vendor.py +0 -0
  103. package/skills/content-defingerprinting/scripts/watermarks/wm-scan.py +0 -0
  104. package/skills/internal-linking-optimizer/SKILL.md +0 -0
  105. package/skills/internal-linking-optimizer/references/source-prompt.md +0 -0
  106. package/skills/pre-publish-review/SKILL.md +0 -0
  107. package/skills/source-cited-content-builder/SKILL.md +0 -0
  108. package/skills/source-cited-content-builder/references/source-prompt.md +0 -0
  109. package/skills/source-cited-humanizer/SKILL.md +0 -0
  110. package/src/citations/adapter.js +102 -21
  111. package/src/citations/alert.js +38 -13
  112. package/src/citations/browser/engine.js +145 -50
  113. package/src/citations/browser/gen/core/providers/_shared/rawSourcesDom.js +0 -0
  114. package/src/citations/browser/gen/core/providers/_shared/resetProviderPage.js +0 -0
  115. package/src/citations/browser/gen/core/providers/_shared/sourceUtils.js +0 -0
  116. package/src/citations/browser/gen/core/providers/ai-overview/index.js +0 -0
  117. package/src/citations/browser/gen/core/providers/ai-overview/lib/expand.js +0 -0
  118. package/src/citations/browser/gen/core/providers/ai-overview/lib/extractSources.js +0 -0
  119. package/src/citations/browser/gen/core/providers/ai-overview/lib/pageLifecycle.js +0 -0
  120. package/src/citations/browser/gen/core/providers/ai-overview/lib/session.js +0 -0
  121. package/src/citations/browser/gen/core/providers/chatgpt/index.js +0 -0
  122. package/src/citations/browser/gen/core/providers/chatgpt/lib/dismissAuthModal.js +0 -0
  123. package/src/citations/browser/gen/core/providers/chatgpt/lib/extractSources.js +0 -0
  124. package/src/citations/browser/gen/core/providers/chatgpt/lib/pageLifecycle.js +0 -0
  125. package/src/citations/browser/gen/core/providers/claude/index.js +0 -0
  126. package/src/citations/browser/gen/core/providers/claude/lib/extractSources.js +0 -0
  127. package/src/citations/browser/gen/core/providers/claude/lib/pageLifecycle.js +0 -0
  128. package/src/citations/browser/gen/core/providers/gemini/index.js +0 -0
  129. package/src/citations/browser/gen/core/providers/gemini/lib/extractSources.js +0 -0
  130. package/src/citations/browser/gen/core/providers/gemini/lib/pageLifecycle.js +0 -0
  131. package/src/citations/browser/gen/core/providers/gemini/lib/session.js +0 -0
  132. package/src/citations/browser/gen/core/providers/index.js +0 -0
  133. package/src/citations/browser/gen/core/providers/perplexity/index.js +0 -0
  134. package/src/citations/browser/gen/core/providers/perplexity/lib/extractSources.js +0 -0
  135. package/src/citations/browser/gen/core/providers/perplexity/lib/navigation.js +0 -0
  136. package/src/citations/browser/gen/lib/browser/camoufox.js +0 -0
  137. package/src/citations/browser/gen/lib/browser/display.js +0 -0
  138. package/src/citations/browser/gen/lib/browser/domOps.js +0 -0
  139. package/src/citations/browser/gen/lib/browser/humanBehavior.js +0 -0
  140. package/src/citations/browser/gen/lib/browser/navigate.js +0 -0
  141. package/src/citations/browser/gen/lib/extraction/sourceUtils.js +0 -0
  142. package/src/citations/browser/gen/lib/input/editor/clearInput.js +0 -0
  143. package/src/citations/browser/gen/lib/input/editor/findEditor.js +0 -0
  144. package/src/citations/browser/gen/lib/input/editor/promptInput.js +0 -0
  145. package/src/citations/browser/gen/lib/input/markdown/converter.js +0 -0
  146. package/src/citations/browser/gen/lib/input/markdown/toMarkdown.js +0 -0
  147. package/src/citations/browser/gen/lib/input/response/detectBotPage.js +0 -0
  148. package/src/citations/browser/gen/lib/input/response/isGenerating.js +0 -0
  149. package/src/citations/browser/gen/lib/input/response/waitForFinish.js +0 -0
  150. package/src/citations/browser/gen/lib/input/sources/findButton.js +0 -0
  151. package/src/citations/browser/gen/lib/input/sources/openPanel.js +0 -0
  152. package/src/citations/browser/gen/utils/agent-constants.js +0 -0
  153. package/src/citations/browser/providers.js +3 -1
  154. package/src/citations/browser/shims/errors.js +0 -0
  155. package/src/citations/browser/shims/types.js +0 -0
  156. package/src/citations/browser/shims/utils.js +1 -0
  157. package/src/citations/contract.js +55 -9
  158. package/src/citations/inventory.js +106 -0
  159. package/src/citations/login.js +122 -0
  160. package/src/citations/match.js +12 -0
  161. package/src/citations/panel-builder.js +44 -39
  162. package/src/citations/report.js +88 -21
  163. package/src/citations/runner.js +59 -31
  164. package/src/citations/stats.js +133 -61
  165. package/src/citations/sync.js +131 -0
  166. package/src/citations/watch.js +57 -77
  167. package/src/competitors/contract.js +0 -0
  168. package/src/competitors/domain-mix.js +0 -0
  169. package/src/context/ga4.js +0 -0
  170. package/src/context/gsc.js +0 -0
  171. package/src/context/site-profile.js +0 -0
  172. package/src/context/util.js +0 -0
  173. package/src/context-tool-registry.js +0 -0
  174. package/src/corpus/acquire.js +47 -8
  175. package/src/corpus/seeds.js +132 -19
  176. package/src/discovery/contract.js +0 -0
  177. package/src/discovery/import.js +0 -0
  178. package/src/evidence-reference.js +112 -0
  179. package/src/index-observation.js +161 -0
  180. package/src/locator.js +112 -0
  181. package/src/tool-views.js +6 -2
  182. package/src/verifier/fetch.js +42 -6
  183. package/src/verifier/index.js +0 -0
  184. package/src/verifier/observation-kind.js +0 -0
  185. package/src/verifier/parser.js +0 -0
  186. package/src/verifier/readiness.js +0 -0
  187. package/src/verifier/render-evidence.js +0 -0
  188. package/src/verifier/robots.js +0 -0
  189. package/src/verifier/transition.js +0 -0
  190. package/src/verifier/url.js +0 -0
  191. package/templates/SITE.template.md +0 -0
  192. package/templates/campaigns.v1.json +0 -0
  193. package/src/citations/browser/gen/core/providers/_shared/google.js +0 -4
  194. package/src/citations/browser/gen/core/providers/types.js +0 -0
  195. package/src/citations/browser/gen/lib/browser/playwrightCompat.js +0 -321
  196. package/src/citations/browser/gen/lib/browser/proxy/forwarder.js +0 -251
  197. package/src/citations/browser/gen/lib/browser/runtimeTypes.js +0 -0
  198. package/src/citations/browser/gen/lib/input/editor/assertNotBlocked.js +0 -54
  199. package/src/citations/browser/gen/lib/input/editor/findSendButton.js +0 -21
  200. package/src/citations/browser/gen/lib/input/response/getText.js +0 -10
  201. package/src/citations/fixtures/dataforseo-aio.json +0 -82
package/src/locator.js ADDED
@@ -0,0 +1,112 @@
1
+ // The evidence locator (DP-0004-T05): where a recorded link is, without rendering anything.
2
+ //
3
+ // An agent that has been told a link exists wants two things: to see the occurrence we recorded,
4
+ // and to go look at the page. Neither needs the captured HTML, and **the captured HTML must
5
+ // never be the thing we render.** The evidence route already refuses to: it serves the snapshot
6
+ // as `application/json`, as an attachment, `no-store`, under `default-src 'none'; sandbox`. This
7
+ // module gives the other half — a position and a way to open the real page — from metadata
8
+ // alone.
9
+ //
10
+ // Two distinctions run through everything here, and both are about not lying by omission.
11
+ //
12
+ // **The locator points into the DOCUMENT WE FETCHED, not into the live page.** Line 1,069 of
13
+ // what we captured on the 11th may be line 900 today, or gone. It is a position in evidence,
14
+ // which is exactly what makes it useful for reading the evidence and useless for anything else.
15
+ //
16
+ // **Static and rendered evidence stay apart.** A static observation proves the link was in the
17
+ // served HTML. It says nothing about whether a reader sees it, and a locator that blurred the
18
+ // two would be the most convincing place to do it.
19
+ export const LOCATOR_LIMITS = Object.freeze({ occurrences: 50, fragmentChars: 300, contextChars: 320 });
20
+
21
+ /**
22
+ * A text fragment that makes a browser scroll to and highlight the anchor on the LIVE page.
23
+ *
24
+ * `-`, `,` and `&` are delimiters inside the fragment syntax, so they are percent-encoded beyond
25
+ * what `encodeURIComponent` does. Text spanning a line break cannot be matched as one run, so
26
+ * anything with a newline is refused rather than silently truncated into a fragment that lands
27
+ * somewhere else on the page.
28
+ */
29
+ export function textFragment(sourceUrl, text) {
30
+ const phrase = String(text ?? '').trim();
31
+ if (!phrase || /[\r\n]/u.test(phrase)) return null;
32
+ // Too short to be distinctive, and a one-character fragment highlights the wrong thing.
33
+ if (phrase.length < 3) return null;
34
+ const trimmed = phrase.slice(0, LOCATOR_LIMITS.fragmentChars);
35
+ const encoded = encodeURIComponent(trimmed).replace(/-/gu, '%2D').replace(/,/gu, '%2C').replace(/&/gu, '%26');
36
+ let url;
37
+ try { url = new URL(sourceUrl); } catch { return null; }
38
+ if (!/^https?:$/u.test(url.protocol)) return null;
39
+ // A text fragment lives after any existing fragment, not instead of it.
40
+ const existing = url.hash.startsWith('#') ? url.hash.slice(1) : '';
41
+ url.hash = `${existing}:~:text=${encoded}`;
42
+ return url.href;
43
+ }
44
+
45
+ const followedOf = (rel, directives) => {
46
+ const tokens = (rel ?? []).map(value => String(value).toLowerCase());
47
+ if (tokens.includes('nofollow') || tokens.includes('sponsored') || tokens.includes('ugc')) return false;
48
+ if (directives?.nofollow) return false;
49
+ return true;
50
+ };
51
+
52
+ /**
53
+ * Turns a stored observation into somewhere to look.
54
+ *
55
+ * Takes the observation's own `result` — the object the verifier produced, which both the cloud
56
+ * and `agentlinkops check` store — so this works identically on a row from either.
57
+ */
58
+ export function locateOccurrences(observation, { limit = LOCATOR_LIMITS.occurrences } = {}) {
59
+ const result = observation?.result ?? observation ?? {};
60
+ const occurrences = Array.isArray(result.occurrences) ? result.occurrences : [];
61
+ const sourceUrl = result.finalUrl ?? result.sourceUrl ?? null;
62
+ const rendered = result.evidence?.rendered === true;
63
+ return {
64
+ v: 1,
65
+ observation_id: observation?.id ?? null,
66
+ state: result.state ?? null,
67
+ reason: result.reason ?? null,
68
+ source_url: result.sourceUrl ?? null,
69
+ // Where we actually read, which is not always where we were sent.
70
+ final_url: sourceUrl,
71
+ target_url: result.targetUrl ?? null,
72
+ observed_at: result.checkedAt ?? observation?.checked_at ?? null,
73
+ checker_version: result.evidence?.checkerVersion ?? observation?.checker_version ?? null,
74
+ // Named rather than implied. A static observation is not a claim about what a reader sees.
75
+ evidence_method: rendered ? 'rendered' : 'static_html',
76
+ rendered,
77
+ evidence: {
78
+ key: observation?.evidence_key ?? null,
79
+ sha256: result.evidence?.sha256 ?? null,
80
+ bytes: result.evidence?.bytes ?? null,
81
+ // A reader has to know the snapshot may be gone before they plan around fetching it.
82
+ retrieval: observation?.evidence_key ? 'GET /v1/observations/{id}/evidence, 410 once expired' : null,
83
+ },
84
+ occurrence_count: result.occurrenceCount ?? occurrences.length,
85
+ truncated: occurrences.length > limit || result.occurrencesTruncated === true,
86
+ occurrences: occurrences.slice(0, limit).map((item, index) => ({
87
+ index,
88
+ href: item.href ?? null,
89
+ resolved_target: item.targetUrl ?? null,
90
+ anchor: item.anchor ?? null,
91
+ rel: item.rel ?? [],
92
+ followed: followedOf(item.rel, result.directives),
93
+ context: typeof item.context === 'string' ? item.context.slice(0, LOCATOR_LIMITS.contextChars) : null,
94
+ // A position in the EVIDENCE, labelled as one.
95
+ in_captured_document: item.locator ?? null,
96
+ // A position in the LIVE page, which is where a human should actually look.
97
+ open_at: textFragment(sourceUrl, item.anchor) ?? textFragment(sourceUrl, item.context) ?? sourceUrl,
98
+ })),
99
+ page_directives: result.directives
100
+ ? { noindex: result.directives.noindex ?? null, nofollow: result.directives.nofollow ?? null, indexing_status: result.directives.indexingStatus ?? null }
101
+ : null,
102
+ // Said once, plainly, on every locate. The alternative is a reader who opens the live page,
103
+ // sees something different, and concludes the tool is wrong.
104
+ notes: [
105
+ 'Positions are offsets into the captured document, not into the page as it is now.',
106
+ rendered
107
+ ? 'This observation was rendered; occurrences may include links written by the page\'s own scripts.'
108
+ : 'Static HTML observation. JavaScript execution and visual visibility were not checked.',
109
+ 'open_at points at the live page with a text fragment. The page may have changed since it was observed.',
110
+ ],
111
+ };
112
+ }
package/src/tool-views.js CHANGED
@@ -3,12 +3,16 @@
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','discovery','library','competitors','reports','workspace','notifications','webhooks'];
6
+ export const TOOLSETS = ['monitoring','evidence','discovery','library','competitors','reports','workspace','notifications','webhooks','admission','disavow','lifecycle','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
+ admission: 'Project rules, URL previews and retained admission decisions.',
13
+ disavow: 'Propose, review and export website disavow rules.',
14
+ lifecycle: 'Placement costs, expiry, renewal events and currency reports.',
15
+ digests: 'Own-address digest preferences, delivery history and exact events.',
12
16
  monitoring: 'Watch earned links and destination URLs: create, list, update, import, export, recheck.',
13
17
  evidence: 'What a check observed: histories, snapshots, change feeds, check jobs, published contacts.',
14
18
  discovery: 'Import candidate rows, read stored runs, verify selected candidates, enroll them.',
@@ -51,7 +55,7 @@ export function oneLiner(description, max = 60) {
51
55
  }
52
56
 
53
57
  // entries: [{ name, description, toolset, tier, readOnly }]. Grouped, one line per command.
54
- export function manifestText(entries, { max = 60 } = {}) {
58
+ export function manifestText(entries, { max = 36 } = {}) {
55
59
  const groups = new Map(TOOLSETS.map(t => [t, []]));
56
60
  for (const entry of entries) (groups.get(entry.toolset) ?? groups.set(entry.toolset, []).get(entry.toolset)).push(entry);
57
61
  const lines = ['Commands by toolset (name: what it does). Use search_tools or describe_tools for the full schema before calling.'];
@@ -115,6 +115,13 @@ export function publicFetcher(options, signal) {
115
115
  try {
116
116
  response = await withAbort(Promise.resolve().then(() => {
117
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;
118
125
  return fetchImpl(checked.url, {
119
126
  method: 'GET',
120
127
  redirect: 'manual',
@@ -122,7 +129,8 @@ export function publicFetcher(options, signal) {
122
129
  signal,
123
130
  headers: {
124
131
  'User-Agent': userAgent,
125
- Accept: context.kind === 'robots' ? 'text/plain' : 'text/html',
132
+ Accept: context.kind === 'robots' ? 'text/plain' : context.accept ?? 'text/html',
133
+ ...conditional,
126
134
  },
127
135
  });
128
136
  }), signal);
@@ -239,8 +247,23 @@ export function publicFetcher(options, signal) {
239
247
  if (signal.aborted) throw new VerificationError('timeout');
240
248
  return robots;
241
249
  },
242
- async source(initialUrl, trace) {
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';
243
265
  let url = initialUrl;
266
+ let revalidators = validators;
244
267
  const seen = new Set();
245
268
  for (let redirects = 0; redirects <= maxRedirects; redirects++) {
246
269
  trace.finalUrl = url;
@@ -248,7 +271,7 @@ export function publicFetcher(options, signal) {
248
271
  seen.add(url);
249
272
  const robots = await checkRobots(url, trace);
250
273
  trace.robots = robots;
251
- const response = await request(url, { kind: 'source', crawlDelaySeconds: robots.crawlDelaySeconds });
274
+ const response = await request(url, { kind: 'source', crawlDelaySeconds: robots.crawlDelaySeconds, validators: revalidators, accept });
252
275
  trace.httpStatus = response.status;
253
276
  trace.sourceResponse = { url, httpStatus: response.status,
254
277
  contentType: /^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+/iu.exec(response.headers.get('content-type') ?? '')?.[0].slice(0, 128) ?? null,
@@ -269,6 +292,9 @@ export function publicFetcher(options, signal) {
269
292
  if (!next.valid) throw new VerificationError(`unsafe_redirect:${next.reason}`);
270
293
  trace.redirects.push({ from: url, to: next.url, status: response.status });
271
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;
272
298
  continue;
273
299
  }
274
300
  if ([404, 410].includes(response.status)) {
@@ -277,7 +303,10 @@ export function publicFetcher(options, signal) {
277
303
  }
278
304
  if (response.status === 304) {
279
305
  cancelBody(response);
280
- throw new VerificationError('not_modified_without_baseline');
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 } };
281
310
  }
282
311
  if (response.status !== 200) {
283
312
  cancelBody(response);
@@ -290,12 +319,19 @@ export function publicFetcher(options, signal) {
290
319
  retryAfterSeconds(response.headers.get('retry-after')) === null ? {}
291
320
  : { retryAfterSeconds: retryAfterSeconds(response.headers.get('retry-after')) });
292
321
  }
293
- if (!/^text\/html(?:;|$)/iu.test(response.headers.get('content-type') ?? '')) {
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)) {
294
329
  cancelBody(response);
295
330
  throw new VerificationError('unsupported_content_type');
296
331
  }
297
332
  const body = await readBounded(response, maxBytes, signal);
298
- return { response, body };
333
+ return { response, body, notModified: false,
334
+ validators: { etag: response.headers.get('etag'), lastModified: response.headers.get('last-modified') } };
299
335
  }
300
336
  throw new VerificationError('redirect_limit');
301
337
  },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -1,4 +0,0 @@
1
- const GOOGLE_CONSENT_SELECTOR = "button#L2AGLb, button#W0wltc, form[action*='consent.google.com'] button";
2
- export {
3
- GOOGLE_CONSENT_SELECTOR
4
- };
File without changes
@@ -1,321 +0,0 @@
1
- import { runPageDomOp } from "./domOps.js";
2
- class PlaywrightWorkerCompat {
3
- constructor(worker) {
4
- this.worker = worker;
5
- }
6
- worker;
7
- async evaluate(script) {
8
- return await this.worker.evaluate(script);
9
- }
10
- }
11
- class PlaywrightLocatorCompat {
12
- constructor(locator) {
13
- this.locator = locator;
14
- }
15
- locator;
16
- wrap(next) {
17
- return new PlaywrightLocatorCompat(next);
18
- }
19
- count() {
20
- return this.locator.count();
21
- }
22
- nth(index) {
23
- return this.wrap(this.locator.nth(index));
24
- }
25
- first() {
26
- return this.wrap(this.locator.first());
27
- }
28
- last() {
29
- return this.wrap(this.locator.last());
30
- }
31
- filter(options) {
32
- return this.wrap(this.locator.filter({ hasText: options.hasText }));
33
- }
34
- getByText(text) {
35
- return this.wrap(this.locator.getByText(text));
36
- }
37
- isVisible(options) {
38
- return this.locator.isVisible(options);
39
- }
40
- isEnabled() {
41
- return this.locator.isEnabled();
42
- }
43
- focus() {
44
- return this.locator.focus();
45
- }
46
- boundingBox() {
47
- return this.locator.boundingBox();
48
- }
49
- scrollIntoViewIfNeeded() {
50
- return this.locator.scrollIntoViewIfNeeded();
51
- }
52
- click(options) {
53
- return this.locator.click(options);
54
- }
55
- press(key, options) {
56
- return this.locator.press(key, options);
57
- }
58
- waitFor(options) {
59
- return this.locator.waitFor({
60
- timeout: options?.timeout,
61
- state: options?.state
62
- });
63
- }
64
- async readInputValue() {
65
- return await this.locator.evaluate((element) => {
66
- if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
67
- return element.value;
68
- }
69
- if (element instanceof HTMLElement) {
70
- return element.innerText || element.textContent || "";
71
- }
72
- return "";
73
- });
74
- }
75
- async setInputValue(value) {
76
- await this.locator.evaluate((element, nextValue) => {
77
- if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
78
- const proto = Object.getPrototypeOf(element);
79
- const descriptor = Object.getOwnPropertyDescriptor(proto, "value");
80
- if (descriptor?.set) {
81
- descriptor.set.call(element, nextValue);
82
- } else {
83
- element.value = nextValue;
84
- }
85
- element.dispatchEvent(new Event("input", { bubbles: true }));
86
- element.dispatchEvent(new Event("change", { bubbles: true }));
87
- return;
88
- }
89
- if (element instanceof HTMLElement) {
90
- element.focus();
91
- element.innerText = nextValue;
92
- element.dispatchEvent(new Event("input", { bubbles: true }));
93
- element.dispatchEvent(new Event("change", { bubbles: true }));
94
- }
95
- }, value);
96
- }
97
- async getEditableState() {
98
- return await this.locator.evaluate((element) => {
99
- function hasHiddenAncestor(target) {
100
- let current = target;
101
- while (current) {
102
- if (current.hidden || current.getAttribute("aria-hidden") === "true" || current.hasAttribute("inert")) {
103
- return true;
104
- }
105
- const style = window.getComputedStyle(current);
106
- if (style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse" || style.opacity === "0" || style.pointerEvents === "none") {
107
- return true;
108
- }
109
- current = current.parentElement;
110
- }
111
- return false;
112
- }
113
- function acceptsTextInput(target) {
114
- if (target instanceof HTMLTextAreaElement) {
115
- return true;
116
- }
117
- if (target instanceof HTMLInputElement) {
118
- const blockedTypes = /* @__PURE__ */ new Set([
119
- "hidden",
120
- "button",
121
- "checkbox",
122
- "color",
123
- "file",
124
- "image",
125
- "radio",
126
- "range",
127
- "reset",
128
- "submit"
129
- ]);
130
- return !blockedTypes.has(target.type);
131
- }
132
- return target.isContentEditable || target.getAttribute("contenteditable") === "true";
133
- }
134
- function isEnabled(target) {
135
- if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
136
- return !target.disabled && !target.readOnly;
137
- }
138
- return target.getAttribute("aria-disabled") !== "true" && !target.hasAttribute("disabled") && target.getAttribute("contenteditable") !== "false";
139
- }
140
- if (!(element instanceof HTMLElement)) {
141
- return {
142
- connected: false,
143
- visible: false,
144
- editable: false,
145
- enabled: false,
146
- acceptsTextInput: false
147
- };
148
- }
149
- const rect = element.getBoundingClientRect();
150
- const clientRects = element.getClientRects();
151
- const visibleByBrowser = typeof element.checkVisibility === "function" ? element.checkVisibility({
152
- checkOpacity: true,
153
- checkVisibilityCSS: true
154
- }) : true;
155
- const visible = element.isConnected && visibleByBrowser && !hasHiddenAncestor(element) && clientRects.length > 0 && rect.width > 0 && rect.height > 0;
156
- const enabled = isEnabled(element);
157
- const textInput = acceptsTextInput(element);
158
- return {
159
- connected: element.isConnected,
160
- visible,
161
- editable: visible && enabled && textInput,
162
- enabled,
163
- acceptsTextInput: textInput
164
- };
165
- });
166
- }
167
- async dispatchClick() {
168
- await this.locator.evaluate((element) => {
169
- if (!(element instanceof HTMLElement)) return;
170
- element.dispatchEvent(
171
- new MouseEvent("click", {
172
- bubbles: true,
173
- cancelable: true,
174
- composed: true,
175
- view: window
176
- })
177
- );
178
- });
179
- }
180
- }
181
- class PlaywrightBrowserContextCompat {
182
- constructor(context) {
183
- this.context = context;
184
- const existingPages = this.context.pages();
185
- this.initialPage = existingPages.length === 1 && existingPages[0]?.url() === "about:blank" ? existingPages[0] : null;
186
- }
187
- context;
188
- pageMap = /* @__PURE__ */ new WeakMap();
189
- initialPage;
190
- wrapPage(page) {
191
- const existing = this.pageMap.get(page);
192
- if (existing) return existing;
193
- const wrapped = new PlaywrightPageCompat(page, this);
194
- this.pageMap.set(page, wrapped);
195
- return wrapped;
196
- }
197
- getRawContext() {
198
- return this.context;
199
- }
200
- getBrowser() {
201
- const browser = this.context.browser();
202
- if (browser) return browser;
203
- return {
204
- version: () => "Camoufox",
205
- close: () => this.context.close()
206
- };
207
- }
208
- async newPage() {
209
- if (this.initialPage) {
210
- const page = this.initialPage;
211
- this.initialPage = null;
212
- return this.wrapPage(page);
213
- }
214
- return this.wrapPage(await this.context.newPage());
215
- }
216
- async close() {
217
- await this.context.close();
218
- }
219
- async storageState(options) {
220
- await this.context.storageState(options);
221
- }
222
- async addInitScript(script) {
223
- await this.context.addInitScript(script);
224
- }
225
- on(event, listener) {
226
- this.context.on(event, (page) => {
227
- listener(this.wrapPage(page));
228
- });
229
- }
230
- }
231
- class PlaywrightPageCompat {
232
- constructor(page, owner) {
233
- this.page = page;
234
- this.owner = owner;
235
- this.mouse = this.page.mouse;
236
- this.keyboard = this.page.keyboard;
237
- }
238
- page;
239
- owner;
240
- mouse;
241
- keyboard;
242
- getRawPage() {
243
- return this.page;
244
- }
245
- async goto(url, options) {
246
- await this.page.goto(url, options);
247
- }
248
- async evaluate(pageFunction, arg) {
249
- return await this.page.evaluate(pageFunction, arg);
250
- }
251
- url() {
252
- return this.page.url();
253
- }
254
- async getUrl() {
255
- return this.page.url();
256
- }
257
- waitForTimeout(ms) {
258
- return this.page.waitForTimeout(ms);
259
- }
260
- waitForLoadState(state, options) {
261
- return this.page.waitForLoadState(state, options);
262
- }
263
- async waitForSelector(selector, options) {
264
- await this.page.waitForSelector(selector, options ?? {});
265
- }
266
- locator(selector) {
267
- return new PlaywrightLocatorCompat(this.page.locator(selector));
268
- }
269
- close() {
270
- return this.page.close();
271
- }
272
- setDefaultTimeout(ms) {
273
- this.page.setDefaultTimeout(ms);
274
- }
275
- setDefaultNavigationTimeout(ms) {
276
- this.page.setDefaultNavigationTimeout(ms);
277
- }
278
- on(event, listener) {
279
- if (event === "console") {
280
- this.page.on(event, listener);
281
- return;
282
- }
283
- this.page.on(event, (worker) => {
284
- listener(
285
- new PlaywrightWorkerCompat(worker)
286
- );
287
- });
288
- }
289
- context() {
290
- return this.owner;
291
- }
292
- viewportSize() {
293
- return this.page.viewportSize();
294
- }
295
- async runDomOp(operation, params) {
296
- return await runPageDomOp(
297
- this.page,
298
- operation,
299
- params ?? {}
300
- );
301
- }
302
- async ping() {
303
- try {
304
- await Promise.race([
305
- this.page.evaluate(() => true),
306
- new Promise(
307
- (_, reject) => setTimeout(() => reject(new Error("page ping timeout")), 5e3)
308
- )
309
- ]);
310
- return true;
311
- } catch {
312
- return false;
313
- }
314
- }
315
- async screenshot(options) {
316
- return await this.page.screenshot(options ?? {});
317
- }
318
- }
319
- export {
320
- PlaywrightBrowserContextCompat
321
- };