@hraness/kb 0.17.1
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/DISCLOSURE +43 -0
- package/LICENSE +21 -0
- package/README.md +545 -0
- package/dist/agent-context.js +35 -0
- package/dist/agent-guide-audit.js +18 -0
- package/dist/attachments.js +19 -0
- package/dist/authoring.js +33 -0
- package/dist/benchmark.js +19 -0
- package/dist/capture.js +49 -0
- package/dist/cli.js +3646 -0
- package/dist/clip/acquire.js +57 -0
- package/dist/clip/args.js +21 -0
- package/dist/clip/bounded-byte-buffer.js +9 -0
- package/dist/clip/browser-profiles.js +17 -0
- package/dist/clip/bundle-reader.js +11 -0
- package/dist/clip/cli.js +27 -0
- package/dist/clip/cookies.js +21 -0
- package/dist/clip/defuddle-worker.js +84 -0
- package/dist/clip/doctor.js +32 -0
- package/dist/clip/jobs.js +784 -0
- package/dist/clip/network-proxy.js +9 -0
- package/dist/clip/network.js +28 -0
- package/dist/clip/persist.js +30 -0
- package/dist/clip/refresh.js +11 -0
- package/dist/clip/terminal.js +11 -0
- package/dist/evaluation-builder.js +12238 -0
- package/dist/evaluation-kb.js +26 -0
- package/dist/evaluation.js +35 -0
- package/dist/git.js +39 -0
- package/dist/graph.js +39 -0
- package/dist/index-01jj6rbv.js +1103 -0
- package/dist/index-06c9ctr6.js +738 -0
- package/dist/index-0kavxzqj.js +2480 -0
- package/dist/index-1gwbassd.js +743 -0
- package/dist/index-1n418kb9.js +827 -0
- package/dist/index-1vrd1rmn.js +56 -0
- package/dist/index-1xxnjn0d.js +171 -0
- package/dist/index-3rm7cz6h.js +373 -0
- package/dist/index-3v2z4f0q.js +328 -0
- package/dist/index-48pz4jpc.js +392 -0
- package/dist/index-4j3tt0c3.js +234 -0
- package/dist/index-5n05se68.js +307 -0
- package/dist/index-5vwpzb5a.js +764 -0
- package/dist/index-6g2pv9d2.js +370 -0
- package/dist/index-7fzc32gf.js +712 -0
- package/dist/index-84x0vjjp.js +432 -0
- package/dist/index-adx6khj5.js +488 -0
- package/dist/index-b88v3vtm.js +805 -0
- package/dist/index-cv6fh7z5.js +462 -0
- package/dist/index-cxfrakt7.js +1067 -0
- package/dist/index-d13v9ckt.js +150 -0
- package/dist/index-dycjbhw2.js +1943 -0
- package/dist/index-dyqwejk5.js +531 -0
- package/dist/index-e5fbsywq.js +658 -0
- package/dist/index-f984hw45.js +927 -0
- package/dist/index-gh719d91.js +63 -0
- package/dist/index-hgve9rh2.js +2046 -0
- package/dist/index-hya40gb2.js +474 -0
- package/dist/index-j4zgmzjr.js +152 -0
- package/dist/index-jsmvyyvf.js +1166 -0
- package/dist/index-kh0pwvdd.js +1974 -0
- package/dist/index-mqx4nd6v.js +119 -0
- package/dist/index-mxxxytys.js +1254 -0
- package/dist/index-n5dd7r0v.js +711 -0
- package/dist/index-npg9z1a4.js +431 -0
- package/dist/index-pj501bh1.js +140 -0
- package/dist/index-qry4vhxk.js +1041 -0
- package/dist/index-s2gw5aw9.js +199 -0
- package/dist/index-vvdj6kca.js +70 -0
- package/dist/index-w2zc0vwa.js +571 -0
- package/dist/index-x3fthpsc.js +585 -0
- package/dist/index-zr53sf63.js +44 -0
- package/dist/index-zxdy5pby.js +2728 -0
- package/dist/index-zzhgcwyt.js +966 -0
- package/dist/index.js +453 -0
- package/dist/navigation.js +13 -0
- package/dist/pdf.js +85 -0
- package/dist/percolate.js +24 -0
- package/dist/portfolio.js +77 -0
- package/dist/query.js +29 -0
- package/dist/repository-memory.js +60 -0
- package/dist/sdk.js +36 -0
- package/dist/search-rules.js +34 -0
- package/dist/search.js +22 -0
- package/dist/semantic.js +39 -0
- package/dist/source-inbox.js +17 -0
- package/dist/untrusted-content.js +32 -0
- package/dist/url-intelligence.js +74 -0
- package/dist/workflow.js +23 -0
- package/dist/workflows/decision-context.js +20 -0
- package/dist/workflows/explain-change.js +8 -0
- package/dist/workflows/index.js +28 -0
- package/dist/workflows/plan-radar.js +10 -0
- package/package.json +409 -0
- package/skills/kb/AGENTS.md +19 -0
- package/skills/kb/SKILL.md +82 -0
- package/skills/kb/agents/openai.yaml +4 -0
- package/skills/kb/references/pdf-review.md +51 -0
- package/skills/kb/references/percolate.md +144 -0
- package/skills/kb/references/plan-structure.md +105 -0
- package/skills/kb/references/plan.md +115 -0
- package/skills/kb/references/query.md +231 -0
- package/skills/kb/references/refresh.md +133 -0
- package/skills/kb/references/save-pdf.md +125 -0
- package/skills/kb/references/save-url.md +178 -0
- package/skills/kb/references/url-authentication.md +75 -0
- package/skills/kb/references/url-platforms.md +25 -0
- package/src/agent-context.ts +1241 -0
- package/src/agent-guide-audit.ts +750 -0
- package/src/attachments.ts +723 -0
- package/src/authoring.ts +1725 -0
- package/src/benchmark.ts +362 -0
- package/src/capture.ts +68 -0
- package/src/cli.ts +3660 -0
- package/src/clip/acquire.ts +1273 -0
- package/src/clip/archive-today.ts +402 -0
- package/src/clip/args.ts +423 -0
- package/src/clip/assets.ts +262 -0
- package/src/clip/bounded-byte-buffer.ts +75 -0
- package/src/clip/browser-profiles.ts +372 -0
- package/src/clip/bundle-reader.ts +608 -0
- package/src/clip/capture.ts +1054 -0
- package/src/clip/cli.ts +181 -0
- package/src/clip/cookies.ts +512 -0
- package/src/clip/doctor.ts +1110 -0
- package/src/clip/extract.ts +868 -0
- package/src/clip/jobs.ts +959 -0
- package/src/clip/lib.ts +861 -0
- package/src/clip/media.ts +1474 -0
- package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
- package/src/clip/metadata-search-tool/Cargo.toml +18 -0
- package/src/clip/metadata-search-tool/runner.ts +685 -0
- package/src/clip/metadata-search-tool/src/main.rs +389 -0
- package/src/clip/metadata-search.ts +687 -0
- package/src/clip/network-proxy.ts +627 -0
- package/src/clip/network.ts +950 -0
- package/src/clip/package-root.ts +54 -0
- package/src/clip/persist.ts +1472 -0
- package/src/clip/platforms.ts +1824 -0
- package/src/clip/refresh.ts +197 -0
- package/src/clip/structured.ts +432 -0
- package/src/clip/terminal.ts +197 -0
- package/src/clip/url-intelligence.ts +581 -0
- package/src/clip/url-metadata-backfill.ts +616 -0
- package/src/clip/url-metadata-cli.ts +251 -0
- package/src/clip/url-metadata.ts +1397 -0
- package/src/evaluation-analysis-v2.ts +1440 -0
- package/src/evaluation-builder.ts +2539 -0
- package/src/evaluation-corpus-authoring.ts +2052 -0
- package/src/evaluation-evidence.ts +1147 -0
- package/src/evaluation-execution-v2.ts +426 -0
- package/src/evaluation-implementation.ts +131 -0
- package/src/evaluation-kb-closure.ts +1430 -0
- package/src/evaluation-kb-runner-v2.ts +735 -0
- package/src/evaluation-kb-v2.ts +2567 -0
- package/src/evaluation-kb.ts +1018 -0
- package/src/evaluation-measurement-v2.ts +1462 -0
- package/src/evaluation-packing-v2.ts +452 -0
- package/src/evaluation-redaction.ts +28 -0
- package/src/evaluation-v2.ts +3857 -0
- package/src/evaluation.ts +1220 -0
- package/src/git.ts +1276 -0
- package/src/graph.ts +1487 -0
- package/src/index.ts +20 -0
- package/src/init.ts +123 -0
- package/src/navigation.ts +277 -0
- package/src/note-lock.ts +529 -0
- package/src/pdf/args.ts +202 -0
- package/src/pdf/capture.ts +298 -0
- package/src/pdf/cli.ts +159 -0
- package/src/pdf/extract.ts +303 -0
- package/src/pdf/layout.ts +618 -0
- package/src/pdf/markdown.ts +154 -0
- package/src/pdf/model.ts +234 -0
- package/src/pdf/ocr.ts +190 -0
- package/src/pdf/persist.ts +314 -0
- package/src/pdf/source.ts +135 -0
- package/src/pdf/tools.ts +146 -0
- package/src/pdf.ts +92 -0
- package/src/percolate.ts +877 -0
- package/src/portfolio-audit.ts +574 -0
- package/src/portfolio-identity.ts +162 -0
- package/src/portfolio-registry.ts +473 -0
- package/src/portfolio.ts +765 -0
- package/src/query.ts +646 -0
- package/src/repository-memory.ts +1150 -0
- package/src/sdk.ts +1452 -0
- package/src/search-rules.ts +712 -0
- package/src/search.ts +710 -0
- package/src/semantic-runtime.ts +1139 -0
- package/src/semantic.ts +2480 -0
- package/src/source-inbox.ts +232 -0
- package/src/untrusted-content.ts +401 -0
- package/src/url-intelligence.ts +104 -0
- package/src/vault.ts +636 -0
- package/src/workflow.ts +653 -0
- package/src/workflows/decision-context.ts +81 -0
- package/src/workflows/explain-change.ts +73 -0
- package/src/workflows/index.ts +3 -0
- package/src/workflows/plan-radar.ts +101 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import type { AcquiredPage } from "./acquire.js";
|
|
2
|
+
import {
|
|
3
|
+
assertSafeNetworkUrl,
|
|
4
|
+
decodeBytes,
|
|
5
|
+
safeFetch,
|
|
6
|
+
type SafeFetchOptions,
|
|
7
|
+
type SafeFetchResult,
|
|
8
|
+
} from "./network.js";
|
|
9
|
+
import {
|
|
10
|
+
MAX_URL_INTELLIGENCE_URL_UTF8_BYTES,
|
|
11
|
+
normalizeSourceUrlIdentity,
|
|
12
|
+
parseArchiveTodayMementoUrl,
|
|
13
|
+
type ArchiveTodayMemento,
|
|
14
|
+
} from "./url-intelligence.js";
|
|
15
|
+
import { sanitizeArtifactUrl } from "./persist.js";
|
|
16
|
+
|
|
17
|
+
const archiveTodayDiscoveryOrigin = "https://archive.ph";
|
|
18
|
+
const archiveTodayDiscoveryPrefix = `${archiveTodayDiscoveryOrigin}/newest/`;
|
|
19
|
+
const defaultUserAgent = "@hraness/kb archive-today fallback";
|
|
20
|
+
const defaultTimeoutMs = 10_000;
|
|
21
|
+
const defaultMaximumBytes = 8 * 1024 * 1024;
|
|
22
|
+
const discoveryMaximumBytes = 256 * 1024;
|
|
23
|
+
const maximumSourceUrlBytes = 16 * 1024;
|
|
24
|
+
const maximumUserAgentBytes = 512;
|
|
25
|
+
|
|
26
|
+
export type ArchiveTodayFetch = (
|
|
27
|
+
url: URL,
|
|
28
|
+
options: SafeFetchOptions,
|
|
29
|
+
) => Promise<SafeFetchResult>;
|
|
30
|
+
|
|
31
|
+
export type ArchiveTodayDependencies = {
|
|
32
|
+
/** Test and composition seam. Discovery requires `assertNetworkUrl` with a custom transport. */
|
|
33
|
+
readonly fetch?: ArchiveTodayFetch;
|
|
34
|
+
/** Test seam used to reject future-dated mementos deterministically. */
|
|
35
|
+
readonly now?: () => Date;
|
|
36
|
+
/** Test seam for enforcing one deadline across validation and provider requests. */
|
|
37
|
+
readonly monotonicNow?: () => number;
|
|
38
|
+
/** Composition seam for validating the source before disclosing it to Archive.today. */
|
|
39
|
+
readonly assertNetworkUrl?: typeof assertSafeNetworkUrl;
|
|
40
|
+
readonly timeoutMs?: number;
|
|
41
|
+
readonly maxBytes?: number;
|
|
42
|
+
readonly userAgent?: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type ArchiveTodaySnapshot = {
|
|
46
|
+
readonly url: string;
|
|
47
|
+
readonly capturedAt: string;
|
|
48
|
+
readonly sourceUrl: string;
|
|
49
|
+
readonly discovery: "newest";
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type ArchiveTodayUnavailableReason =
|
|
53
|
+
| "request-failed"
|
|
54
|
+
| "unexpected-response"
|
|
55
|
+
| "invalid-snapshot";
|
|
56
|
+
|
|
57
|
+
export type ArchiveTodayDiscovery =
|
|
58
|
+
| {
|
|
59
|
+
readonly status: "found";
|
|
60
|
+
readonly sourceUrl: string;
|
|
61
|
+
readonly snapshot: ArchiveTodaySnapshot;
|
|
62
|
+
}
|
|
63
|
+
| {
|
|
64
|
+
readonly status: "not-found";
|
|
65
|
+
readonly sourceUrl: string;
|
|
66
|
+
}
|
|
67
|
+
| {
|
|
68
|
+
readonly status: "throttled";
|
|
69
|
+
readonly sourceUrl: string;
|
|
70
|
+
}
|
|
71
|
+
| {
|
|
72
|
+
readonly status: "unavailable";
|
|
73
|
+
readonly sourceUrl: string;
|
|
74
|
+
readonly reason: ArchiveTodayUnavailableReason;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type ArchiveTodayFailureCode =
|
|
78
|
+
| "invalid-source-url"
|
|
79
|
+
| "invalid-snapshot"
|
|
80
|
+
| "invalid-clock"
|
|
81
|
+
| "invalid-options"
|
|
82
|
+
| "request-failed"
|
|
83
|
+
| "not-found"
|
|
84
|
+
| "throttled"
|
|
85
|
+
| "unexpected-response"
|
|
86
|
+
| "unsupported-content";
|
|
87
|
+
|
|
88
|
+
const failureMessages: Readonly<Record<ArchiveTodayFailureCode, string>> = Object.freeze({
|
|
89
|
+
"invalid-source-url": "Archive.today requires a bounded public HTTP(S) source URL.",
|
|
90
|
+
"invalid-snapshot": "Archive.today returned a snapshot that could not be bound to the source URL.",
|
|
91
|
+
"invalid-clock": "Archive.today validation requires a valid current time.",
|
|
92
|
+
"invalid-options": "Archive.today adapter options are outside their supported bounds.",
|
|
93
|
+
"request-failed": "Archive.today could not be reached.",
|
|
94
|
+
"not-found": "The validated Archive.today snapshot is no longer available.",
|
|
95
|
+
"throttled": "Archive.today throttled the request.",
|
|
96
|
+
"unexpected-response": "Archive.today returned an unexpected response.",
|
|
97
|
+
"unsupported-content": "Archive.today did not return a non-empty HTML document.",
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
/** A categorical provider failure that never includes response bodies or remote error text. */
|
|
101
|
+
export class ArchiveTodayFailure extends Error {
|
|
102
|
+
readonly code: ArchiveTodayFailureCode;
|
|
103
|
+
|
|
104
|
+
constructor(code: ArchiveTodayFailureCode) {
|
|
105
|
+
super(failureMessages[code]);
|
|
106
|
+
this.name = "ArchiveTodayFailure";
|
|
107
|
+
this.code = code;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
type ResolvedDependencies = {
|
|
112
|
+
readonly fetch: ArchiveTodayFetch;
|
|
113
|
+
readonly now: Date;
|
|
114
|
+
readonly monotonicNow: () => number;
|
|
115
|
+
readonly timeoutMs: number;
|
|
116
|
+
readonly maxBytes: number;
|
|
117
|
+
readonly userAgent: string;
|
|
118
|
+
readonly assertNetworkUrl: typeof assertSafeNetworkUrl;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
function utf8Length(value: string): number {
|
|
122
|
+
return new TextEncoder().encode(value).byteLength;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function containsUnsafeLocationControl(value: string): boolean {
|
|
126
|
+
for (const character of value) {
|
|
127
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
128
|
+
if (
|
|
129
|
+
codePoint <= 0x1f
|
|
130
|
+
|| (codePoint >= 0x7f && codePoint <= 0x9f)
|
|
131
|
+
|| codePoint === 0x061c
|
|
132
|
+
|| codePoint === 0x200e
|
|
133
|
+
|| codePoint === 0x200f
|
|
134
|
+
|| (codePoint >= 0x202a && codePoint <= 0x202e)
|
|
135
|
+
|| (codePoint >= 0x2066 && codePoint <= 0x2069)
|
|
136
|
+
) return true;
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function boundedInteger(
|
|
142
|
+
value: number | undefined,
|
|
143
|
+
fallback: number,
|
|
144
|
+
minimum: number,
|
|
145
|
+
maximum: number,
|
|
146
|
+
): number {
|
|
147
|
+
const selected = value ?? fallback;
|
|
148
|
+
if (!Number.isSafeInteger(selected) || selected < minimum || selected > maximum) {
|
|
149
|
+
throw new ArchiveTodayFailure("invalid-options");
|
|
150
|
+
}
|
|
151
|
+
return selected;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function resolveDependencies(dependencies: ArchiveTodayDependencies): ResolvedDependencies {
|
|
155
|
+
let now: Date;
|
|
156
|
+
try {
|
|
157
|
+
now = dependencies.now?.() ?? new Date();
|
|
158
|
+
} catch {
|
|
159
|
+
throw new ArchiveTodayFailure("invalid-clock");
|
|
160
|
+
}
|
|
161
|
+
if (!(now instanceof Date) || !Number.isFinite(now.getTime())) {
|
|
162
|
+
throw new ArchiveTodayFailure("invalid-clock");
|
|
163
|
+
}
|
|
164
|
+
const userAgent = dependencies.userAgent ?? defaultUserAgent;
|
|
165
|
+
if (
|
|
166
|
+
userAgent.trim() === ""
|
|
167
|
+
|| utf8Length(userAgent) > maximumUserAgentBytes
|
|
168
|
+
|| containsUnsafeLocationControl(userAgent)
|
|
169
|
+
) {
|
|
170
|
+
throw new ArchiveTodayFailure("invalid-options");
|
|
171
|
+
}
|
|
172
|
+
return Object.freeze({
|
|
173
|
+
fetch: dependencies.fetch ?? safeFetch,
|
|
174
|
+
assertNetworkUrl: dependencies.assertNetworkUrl ?? assertSafeNetworkUrl,
|
|
175
|
+
now: new Date(now.getTime()),
|
|
176
|
+
monotonicNow: dependencies.monotonicNow ?? Date.now,
|
|
177
|
+
timeoutMs: boundedInteger(dependencies.timeoutMs, defaultTimeoutMs, 250, 60_000),
|
|
178
|
+
maxBytes: boundedInteger(dependencies.maxBytes, defaultMaximumBytes, 1, 64 * 1024 * 1024),
|
|
179
|
+
userAgent,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function monotonicTime(resolved: ResolvedDependencies): number {
|
|
184
|
+
let value: number;
|
|
185
|
+
try {
|
|
186
|
+
value = resolved.monotonicNow();
|
|
187
|
+
} catch {
|
|
188
|
+
throw new ArchiveTodayFailure("invalid-clock");
|
|
189
|
+
}
|
|
190
|
+
if (!Number.isFinite(value)) throw new ArchiveTodayFailure("invalid-clock");
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function remainingTimeout(deadline: number, resolved: ResolvedDependencies): number {
|
|
195
|
+
return Math.floor(deadline - monotonicTime(resolved));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function normalizeSourceUrl(value: string | URL): URL {
|
|
199
|
+
const identity = normalizeSourceUrlIdentity(value);
|
|
200
|
+
if (identity === null || utf8Length(identity) > maximumSourceUrlBytes) {
|
|
201
|
+
throw new ArchiveTodayFailure("invalid-source-url");
|
|
202
|
+
}
|
|
203
|
+
// The lookup discloses the complete URL to Archive.today. Reject URLs whose
|
|
204
|
+
// credential-shaped query data would be removed from a durable artifact.
|
|
205
|
+
if (sanitizeArtifactUrl(identity) !== identity) {
|
|
206
|
+
throw new ArchiveTodayFailure("invalid-source-url");
|
|
207
|
+
}
|
|
208
|
+
return new URL(identity);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function parseMemento(
|
|
212
|
+
value: string | URL,
|
|
213
|
+
source: URL,
|
|
214
|
+
now: Date,
|
|
215
|
+
): ArchiveTodayMemento {
|
|
216
|
+
try {
|
|
217
|
+
return parseArchiveTodayMementoUrl(value, { originalUrl: source, now });
|
|
218
|
+
} catch {
|
|
219
|
+
throw new ArchiveTodayFailure("invalid-snapshot");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function snapshotFromMemento(memento: ArchiveTodayMemento): ArchiveTodaySnapshot {
|
|
224
|
+
return Object.freeze({
|
|
225
|
+
url: memento.url,
|
|
226
|
+
capturedAt: memento.capturedAt,
|
|
227
|
+
sourceUrl: memento.originalUrl,
|
|
228
|
+
discovery: "newest",
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function unavailable(
|
|
233
|
+
sourceUrl: string,
|
|
234
|
+
reason: ArchiveTodayUnavailableReason,
|
|
235
|
+
): ArchiveTodayDiscovery {
|
|
236
|
+
return Object.freeze({ status: "unavailable", sourceUrl, reason });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Find Archive.today's newest existing snapshot without creating one.
|
|
241
|
+
*
|
|
242
|
+
* This performs one manual-redirect request against the fixed `archive.ph`
|
|
243
|
+
* lookup route. It never calls Archive.today's submission endpoint, never
|
|
244
|
+
* retries, and never probes alternate aliases when the provider refuses the
|
|
245
|
+
* request.
|
|
246
|
+
*/
|
|
247
|
+
export async function discoverArchiveTodaySnapshot(
|
|
248
|
+
sourceUrl: string | URL,
|
|
249
|
+
dependencies: ArchiveTodayDependencies = {},
|
|
250
|
+
): Promise<ArchiveTodayDiscovery> {
|
|
251
|
+
if (dependencies.fetch !== undefined && dependencies.assertNetworkUrl === undefined) {
|
|
252
|
+
throw new ArchiveTodayFailure("invalid-options");
|
|
253
|
+
}
|
|
254
|
+
const source = normalizeSourceUrl(sourceUrl);
|
|
255
|
+
const resolved = resolveDependencies(dependencies);
|
|
256
|
+
const deadline = monotonicTime(resolved) + resolved.timeoutMs;
|
|
257
|
+
try {
|
|
258
|
+
await resolved.assertNetworkUrl(source, false, resolved.timeoutMs);
|
|
259
|
+
} catch {
|
|
260
|
+
return unavailable(source.href, "request-failed");
|
|
261
|
+
}
|
|
262
|
+
const remaining = remainingTimeout(deadline, resolved);
|
|
263
|
+
if (remaining <= 0) return unavailable(source.href, "request-failed");
|
|
264
|
+
const lookupUrl = new URL(`${archiveTodayDiscoveryPrefix}${source.href}`);
|
|
265
|
+
let response: SafeFetchResult;
|
|
266
|
+
try {
|
|
267
|
+
response = await resolved.fetch(lookupUrl, {
|
|
268
|
+
timeoutMs: remaining,
|
|
269
|
+
maxBytes: discoveryMaximumBytes,
|
|
270
|
+
allowPrivateNetwork: false,
|
|
271
|
+
userAgent: resolved.userAgent,
|
|
272
|
+
accept: "text/html,application/xhtml+xml;q=0.9,*/*;q=0.1",
|
|
273
|
+
retries: 0,
|
|
274
|
+
maxRedirects: 0,
|
|
275
|
+
redirect: "manual",
|
|
276
|
+
acceptStatuses: [403, 404, 429],
|
|
277
|
+
});
|
|
278
|
+
} catch {
|
|
279
|
+
return unavailable(source.href, "request-failed");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (response.status === 404) {
|
|
283
|
+
return Object.freeze({ status: "not-found", sourceUrl: source.href });
|
|
284
|
+
}
|
|
285
|
+
if (response.status === 403 || response.status === 429) {
|
|
286
|
+
return Object.freeze({ status: "throttled", sourceUrl: source.href });
|
|
287
|
+
}
|
|
288
|
+
if (response.status < 300 || response.status >= 400) {
|
|
289
|
+
return unavailable(source.href, "unexpected-response");
|
|
290
|
+
}
|
|
291
|
+
if (typeof response.location !== "string" || response.location.trim() === "") {
|
|
292
|
+
return unavailable(source.href, "invalid-snapshot");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
if (
|
|
297
|
+
response.location !== response.location.trim()
|
|
298
|
+
|| containsUnsafeLocationControl(response.location)
|
|
299
|
+
|| utf8Length(response.location) > MAX_URL_INTELLIGENCE_URL_UTF8_BYTES
|
|
300
|
+
) throw new ArchiveTodayFailure("invalid-snapshot");
|
|
301
|
+
const location = new URL(response.location, lookupUrl);
|
|
302
|
+
const memento = parseMemento(location, source, resolved.now);
|
|
303
|
+
return Object.freeze({
|
|
304
|
+
status: "found",
|
|
305
|
+
sourceUrl: source.href,
|
|
306
|
+
snapshot: snapshotFromMemento(memento),
|
|
307
|
+
});
|
|
308
|
+
} catch {
|
|
309
|
+
return unavailable(source.href, "invalid-snapshot");
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function isHtmlContentType(value: string | null): boolean {
|
|
314
|
+
if (value === null) return false;
|
|
315
|
+
const mediaType = value.split(";", 1)[0]?.trim().toLowerCase();
|
|
316
|
+
return mediaType === "text/html" || mediaType === "application/xhtml+xml";
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Fetch one already validated snapshot, preserving the original URL binding across redirects. */
|
|
320
|
+
export async function acquireArchiveTodaySnapshot(
|
|
321
|
+
sourceUrl: string | URL,
|
|
322
|
+
snapshotUrl: string | URL,
|
|
323
|
+
dependencies: ArchiveTodayDependencies = {},
|
|
324
|
+
): Promise<AcquiredPage> {
|
|
325
|
+
if (dependencies.fetch !== undefined && dependencies.assertNetworkUrl === undefined) {
|
|
326
|
+
throw new ArchiveTodayFailure("invalid-options");
|
|
327
|
+
}
|
|
328
|
+
const source = normalizeSourceUrl(sourceUrl);
|
|
329
|
+
const resolved = resolveDependencies(dependencies);
|
|
330
|
+
const requested = parseMemento(snapshotUrl, source, resolved.now);
|
|
331
|
+
let current = new URL(requested.url);
|
|
332
|
+
let response: SafeFetchResult | null = null;
|
|
333
|
+
const deadline = monotonicTime(resolved) + resolved.timeoutMs;
|
|
334
|
+
try {
|
|
335
|
+
await resolved.assertNetworkUrl(source, false, resolved.timeoutMs);
|
|
336
|
+
} catch {
|
|
337
|
+
throw new ArchiveTodayFailure("request-failed");
|
|
338
|
+
}
|
|
339
|
+
for (let redirects = 0; redirects <= 4; redirects += 1) {
|
|
340
|
+
const remaining = remainingTimeout(deadline, resolved);
|
|
341
|
+
if (remaining <= 0) throw new ArchiveTodayFailure("request-failed");
|
|
342
|
+
try {
|
|
343
|
+
response = await resolved.fetch(current, {
|
|
344
|
+
timeoutMs: remaining,
|
|
345
|
+
maxBytes: resolved.maxBytes,
|
|
346
|
+
allowPrivateNetwork: false,
|
|
347
|
+
userAgent: resolved.userAgent,
|
|
348
|
+
accept: "text/html,application/xhtml+xml;q=0.9",
|
|
349
|
+
retries: 0,
|
|
350
|
+
maxRedirects: 0,
|
|
351
|
+
redirect: "manual",
|
|
352
|
+
acceptStatuses: [403, 404, 429],
|
|
353
|
+
});
|
|
354
|
+
} catch {
|
|
355
|
+
throw new ArchiveTodayFailure("request-failed");
|
|
356
|
+
}
|
|
357
|
+
if (response.finalUrl.href !== current.href) throw new ArchiveTodayFailure("invalid-snapshot");
|
|
358
|
+
if (response.status < 300 || response.status >= 400) break;
|
|
359
|
+
if (redirects === 4 || typeof response.location !== "string") {
|
|
360
|
+
throw new ArchiveTodayFailure("invalid-snapshot");
|
|
361
|
+
}
|
|
362
|
+
if (
|
|
363
|
+
response.location !== response.location.trim()
|
|
364
|
+
|| containsUnsafeLocationControl(response.location)
|
|
365
|
+
|| utf8Length(response.location) > MAX_URL_INTELLIGENCE_URL_UTF8_BYTES
|
|
366
|
+
) throw new ArchiveTodayFailure("invalid-snapshot");
|
|
367
|
+
let next: URL;
|
|
368
|
+
try {
|
|
369
|
+
next = new URL(response.location, current);
|
|
370
|
+
} catch {
|
|
371
|
+
throw new ArchiveTodayFailure("invalid-snapshot");
|
|
372
|
+
}
|
|
373
|
+
const redirected = parseMemento(next, source, resolved.now);
|
|
374
|
+
if (redirected.timestamp !== requested.timestamp) throw new ArchiveTodayFailure("invalid-snapshot");
|
|
375
|
+
current = new URL(redirected.url);
|
|
376
|
+
}
|
|
377
|
+
if (response === null) throw new ArchiveTodayFailure("request-failed");
|
|
378
|
+
|
|
379
|
+
if (response.status === 403 || response.status === 429) {
|
|
380
|
+
throw new ArchiveTodayFailure("throttled");
|
|
381
|
+
}
|
|
382
|
+
if (response.status === 404) throw new ArchiveTodayFailure("not-found");
|
|
383
|
+
if (response.status < 200 || response.status >= 300) {
|
|
384
|
+
throw new ArchiveTodayFailure("unexpected-response");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const finalMemento = parseMemento(current, source, resolved.now);
|
|
388
|
+
if (finalMemento.timestamp !== requested.timestamp) throw new ArchiveTodayFailure("invalid-snapshot");
|
|
389
|
+
if (!isHtmlContentType(response.contentType)) {
|
|
390
|
+
throw new ArchiveTodayFailure("unsupported-content");
|
|
391
|
+
}
|
|
392
|
+
const body = decodeBytes(response.bytes, response.contentType);
|
|
393
|
+
if (body.trim() === "") throw new ArchiveTodayFailure("unsupported-content");
|
|
394
|
+
|
|
395
|
+
return Object.freeze({
|
|
396
|
+
body,
|
|
397
|
+
contentType: response.contentType,
|
|
398
|
+
finalUrl: new URL(finalMemento.url),
|
|
399
|
+
method: "archive-is",
|
|
400
|
+
warnings: Object.freeze(["Captured from a validated Archive.today snapshot."]),
|
|
401
|
+
});
|
|
402
|
+
}
|