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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.mcp.json +0 -0
- package/CHANGELOG.md +17 -0
- package/LICENSE +0 -0
- package/NOTICE +0 -0
- package/README.md +0 -0
- package/bin/agentlinkops.mjs +0 -0
- package/bin/linktrail.mjs +0 -0
- package/cli/adapters/csv.js +0 -0
- package/cli/adapters/sqlite-crm.js +0 -0
- package/cli/adapters/suppliers.js +0 -0
- package/cli/admission.js +130 -0
- package/cli/agent-setup.js +1014 -101
- package/cli/agentlinkops.mjs +0 -0
- package/cli/args.js +0 -0
- package/cli/catalog.js +12837 -4694
- package/cli/check.js +0 -0
- package/cli/citation.js +181 -32
- package/cli/client.js +1 -1
- package/cli/commands.js +3 -3
- package/cli/config.js +2 -1
- package/cli/connection.js +0 -0
- package/cli/context-mcp.js +0 -0
- package/cli/context.js +0 -0
- package/cli/delivery.js +0 -0
- package/cli/disavow.js +54 -0
- package/cli/doctor.js +7 -8
- package/cli/env.js +0 -0
- package/cli/fleet.js +0 -0
- package/cli/gsc-browser.js +0 -0
- package/cli/import.js +0 -0
- package/cli/index-observations.js +89 -0
- package/cli/ledger.js +7 -1
- package/cli/lifecycle.js +40 -0
- package/cli/linktrail.mjs +0 -0
- package/cli/local-result.js +0 -0
- package/cli/main.js +99 -7
- package/cli/mirror.js +0 -0
- package/cli/mix.js +0 -0
- package/cli/platforms.js +0 -0
- package/cli/receipt-performance.js +0 -0
- package/cli/receipts.js +0 -0
- package/cli/report.js +79 -6
- package/cli/setup-plan.js +0 -0
- package/cli/skill.js +41 -4
- package/cli/state.js +0 -0
- package/cli/status.js +0 -0
- package/cli/sync.js +23 -1
- package/package.json +3 -2
- package/references/citation-panels.md +26 -0
- package/references/cli.md +0 -0
- package/references/disavow.md +0 -0
- package/references/discovery-handoff.md +32 -0
- package/references/recipes/catalog.json +148 -0
- package/references/recipes/command-notes.md +9 -0
- package/references/recipes/placement-reconciliation.md +41 -0
- package/references/recipes/qualified-campaign-handoff.md +56 -0
- package/references/recipes/site-context-brief.md +25 -0
- package/references/recipes/sourced-linkable-asset.md +25 -0
- package/references/reports.md +0 -0
- package/scripts/agentlinkops.py +0 -0
- package/scripts/disavow.py +0 -0
- package/scripts/linktrail.py +0 -0
- package/scripts/migrations/001_crm.sql +0 -0
- package/scripts/migrations/002_cloud_events.sql +0 -0
- package/scripts/migrations/003_disavow.sql +0 -0
- package/scripts/migrations/004_placement_costs.sql +0 -0
- package/scripts/report.py +0 -0
- package/shared/admission-contract.js +16 -0
- package/shared/admission-report-contract.js +8 -0
- package/shared/agent-clients.js +0 -0
- package/shared/citation-evidence-contract.js +27 -0
- package/shared/disavow-contract.js +18 -0
- package/shared/index-observation-contract.js +14 -0
- package/shared/lifecycle-contract.js +22 -0
- package/shared/lifecycle-money.js +24 -0
- package/shared/observation-event.js +12 -1
- package/shared/opportunity-record.js +259 -0
- package/shared/response-contract.js +2 -0
- package/shared/search-connection-contract.js +0 -0
- package/skills/agentlinkops-assets/SKILL.md +2 -0
- package/skills/agentlinkops-assets/agents/openai.yaml +0 -0
- package/skills/agentlinkops-campaigns/SKILL.md +5 -1
- package/skills/agentlinkops-campaigns/agents/openai.yaml +0 -0
- package/skills/agentlinkops-connect/SKILL.md +8 -0
- package/skills/agentlinkops-connect/agents/openai.yaml +0 -0
- package/skills/agentlinkops-connect/references/agentlinkops.md +9 -5
- package/skills/agentlinkops-crm/SKILL.md +2 -0
- package/skills/agentlinkops-crm/agents/openai.yaml +0 -0
- package/skills/agentlinkops-discovery/SKILL.md +17 -0
- package/skills/content-defingerprinting/SKILL.md +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/README.md +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/UPSTREAM.json +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/revendor.sh +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/LICENSE.upstream +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/clean_text.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/common.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/inspect_text.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/score_stylometry.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/text_unicode.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/verify-vendor.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/wm-scan.py +0 -0
- package/skills/internal-linking-optimizer/SKILL.md +0 -0
- package/skills/internal-linking-optimizer/references/source-prompt.md +0 -0
- package/skills/pre-publish-review/SKILL.md +0 -0
- package/skills/source-cited-content-builder/SKILL.md +0 -0
- package/skills/source-cited-content-builder/references/source-prompt.md +0 -0
- package/skills/source-cited-humanizer/SKILL.md +0 -0
- package/src/citations/adapter.js +102 -21
- package/src/citations/alert.js +38 -13
- package/src/citations/browser/engine.js +145 -50
- package/src/citations/browser/gen/core/providers/_shared/rawSourcesDom.js +0 -0
- package/src/citations/browser/gen/core/providers/_shared/resetProviderPage.js +0 -0
- package/src/citations/browser/gen/core/providers/_shared/sourceUtils.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/index.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/expand.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/pageLifecycle.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/session.js +0 -0
- package/src/citations/browser/gen/core/providers/chatgpt/index.js +0 -0
- package/src/citations/browser/gen/core/providers/chatgpt/lib/dismissAuthModal.js +0 -0
- package/src/citations/browser/gen/core/providers/chatgpt/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/chatgpt/lib/pageLifecycle.js +0 -0
- package/src/citations/browser/gen/core/providers/claude/index.js +0 -0
- package/src/citations/browser/gen/core/providers/claude/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/claude/lib/pageLifecycle.js +0 -0
- package/src/citations/browser/gen/core/providers/gemini/index.js +0 -0
- package/src/citations/browser/gen/core/providers/gemini/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/gemini/lib/pageLifecycle.js +0 -0
- package/src/citations/browser/gen/core/providers/gemini/lib/session.js +0 -0
- package/src/citations/browser/gen/core/providers/index.js +0 -0
- package/src/citations/browser/gen/core/providers/perplexity/index.js +0 -0
- package/src/citations/browser/gen/core/providers/perplexity/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/perplexity/lib/navigation.js +0 -0
- package/src/citations/browser/gen/lib/browser/camoufox.js +0 -0
- package/src/citations/browser/gen/lib/browser/display.js +0 -0
- package/src/citations/browser/gen/lib/browser/domOps.js +0 -0
- package/src/citations/browser/gen/lib/browser/humanBehavior.js +0 -0
- package/src/citations/browser/gen/lib/browser/navigate.js +0 -0
- package/src/citations/browser/gen/lib/extraction/sourceUtils.js +0 -0
- package/src/citations/browser/gen/lib/input/editor/clearInput.js +0 -0
- package/src/citations/browser/gen/lib/input/editor/findEditor.js +0 -0
- package/src/citations/browser/gen/lib/input/editor/promptInput.js +0 -0
- package/src/citations/browser/gen/lib/input/markdown/converter.js +0 -0
- package/src/citations/browser/gen/lib/input/markdown/toMarkdown.js +0 -0
- package/src/citations/browser/gen/lib/input/response/detectBotPage.js +0 -0
- package/src/citations/browser/gen/lib/input/response/isGenerating.js +0 -0
- package/src/citations/browser/gen/lib/input/response/waitForFinish.js +0 -0
- package/src/citations/browser/gen/lib/input/sources/findButton.js +0 -0
- package/src/citations/browser/gen/lib/input/sources/openPanel.js +0 -0
- package/src/citations/browser/gen/utils/agent-constants.js +0 -0
- package/src/citations/browser/providers.js +3 -1
- package/src/citations/browser/shims/errors.js +0 -0
- package/src/citations/browser/shims/types.js +0 -0
- package/src/citations/browser/shims/utils.js +1 -0
- package/src/citations/contract.js +55 -9
- package/src/citations/inventory.js +106 -0
- package/src/citations/login.js +122 -0
- package/src/citations/match.js +12 -0
- package/src/citations/panel-builder.js +44 -39
- package/src/citations/report.js +88 -21
- package/src/citations/runner.js +59 -31
- package/src/citations/stats.js +133 -61
- package/src/citations/sync.js +131 -0
- package/src/citations/watch.js +57 -77
- package/src/competitors/contract.js +0 -0
- package/src/competitors/domain-mix.js +0 -0
- package/src/context/ga4.js +0 -0
- package/src/context/gsc.js +0 -0
- package/src/context/site-profile.js +0 -0
- package/src/context/util.js +0 -0
- package/src/context-tool-registry.js +0 -0
- package/src/corpus/acquire.js +47 -8
- package/src/corpus/seeds.js +132 -19
- package/src/discovery/contract.js +0 -0
- package/src/discovery/import.js +0 -0
- package/src/evidence-reference.js +112 -0
- package/src/index-observation.js +161 -0
- package/src/locator.js +112 -0
- package/src/tool-views.js +6 -2
- package/src/verifier/fetch.js +42 -6
- package/src/verifier/index.js +0 -0
- package/src/verifier/observation-kind.js +0 -0
- package/src/verifier/parser.js +0 -0
- package/src/verifier/readiness.js +0 -0
- package/src/verifier/render-evidence.js +0 -0
- package/src/verifier/robots.js +0 -0
- package/src/verifier/transition.js +0 -0
- package/src/verifier/url.js +0 -0
- package/templates/SITE.template.md +0 -0
- package/templates/campaigns.v1.json +0 -0
- package/src/citations/browser/gen/core/providers/_shared/google.js +0 -4
- package/src/citations/browser/gen/core/providers/types.js +0 -0
- package/src/citations/browser/gen/lib/browser/playwrightCompat.js +0 -321
- package/src/citations/browser/gen/lib/browser/proxy/forwarder.js +0 -251
- package/src/citations/browser/gen/lib/browser/runtimeTypes.js +0 -0
- package/src/citations/browser/gen/lib/input/editor/assertNotBlocked.js +0 -54
- package/src/citations/browser/gen/lib/input/editor/findSendButton.js +0 -21
- package/src/citations/browser/gen/lib/input/response/getText.js +0 -10
- 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 =
|
|
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.'];
|
package/src/verifier/fetch.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
},
|
package/src/verifier/index.js
CHANGED
|
File without changes
|
|
File without changes
|
package/src/verifier/parser.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/verifier/robots.js
CHANGED
|
File without changes
|
|
File without changes
|
package/src/verifier/url.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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
|
-
};
|