@x12i/content-fetcher 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -0
- package/dist/adapters/default-http-client.d.ts +17 -0
- package/dist/adapters/default-http-client.d.ts.map +1 -0
- package/dist/adapters/default-http-client.js +74 -0
- package/dist/adapters/default-http-client.js.map +1 -0
- package/dist/channels.d.ts +34 -0
- package/dist/channels.d.ts.map +1 -0
- package/dist/channels.js +164 -0
- package/dist/channels.js.map +1 -0
- package/dist/classify.d.ts +13 -0
- package/dist/classify.d.ts.map +1 -0
- package/dist/classify.js +122 -0
- package/dist/classify.js.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +197 -0
- package/dist/config.js.map +1 -0
- package/dist/create-content-fetcher.d.ts +9 -0
- package/dist/create-content-fetcher.d.ts.map +1 -0
- package/dist/create-content-fetcher.js +836 -0
- package/dist/create-content-fetcher.js.map +1 -0
- package/dist/errors.d.ts +42 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +73 -0
- package/dist/errors.js.map +1 -0
- package/dist/handoff.d.ts +8 -0
- package/dist/handoff.d.ts.map +1 -0
- package/dist/handoff.js +20 -0
- package/dist/handoff.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/ratelimit.d.ts +27 -0
- package/dist/ratelimit.d.ts.map +1 -0
- package/dist/ratelimit.js +43 -0
- package/dist/ratelimit.js.map +1 -0
- package/dist/robots/robots-service.d.ts +40 -0
- package/dist/robots/robots-service.d.ts.map +1 -0
- package/dist/robots/robots-service.js +131 -0
- package/dist/robots/robots-service.js.map +1 -0
- package/dist/robots/robots-txt.d.ts +24 -0
- package/dist/robots/robots-txt.d.ts.map +1 -0
- package/dist/robots/robots-txt.js +91 -0
- package/dist/robots/robots-txt.js.map +1 -0
- package/dist/runtime-utils.d.ts +11 -0
- package/dist/runtime-utils.d.ts.map +1 -0
- package/dist/runtime-utils.js +32 -0
- package/dist/runtime-utils.js.map +1 -0
- package/dist/security/dns.d.ts +33 -0
- package/dist/security/dns.d.ts.map +1 -0
- package/dist/security/dns.js +60 -0
- package/dist/security/dns.js.map +1 -0
- package/dist/security/headers.d.ts +16 -0
- package/dist/security/headers.d.ts.map +1 -0
- package/dist/security/headers.js +54 -0
- package/dist/security/headers.js.map +1 -0
- package/dist/security/net-guards.d.ts +9 -0
- package/dist/security/net-guards.d.ts.map +1 -0
- package/dist/security/net-guards.js +102 -0
- package/dist/security/net-guards.js.map +1 -0
- package/dist/security/url.d.ts +32 -0
- package/dist/security/url.d.ts.map +1 -0
- package/dist/security/url.js +82 -0
- package/dist/security/url.js.map +1 -0
- package/dist/storage.d.ts +38 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +109 -0
- package/dist/storage.js.map +1 -0
- package/dist/types.d.ts +232 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/util/bytes.d.ts +21 -0
- package/dist/util/bytes.d.ts.map +1 -0
- package/dist/util/bytes.js +44 -0
- package/dist/util/bytes.js.map +1 -0
- package/dist/util/hash.d.ts +5 -0
- package/dist/util/hash.d.ts.map +1 -0
- package/dist/util/hash.js +11 -0
- package/dist/util/hash.js.map +1 -0
- package/dist/util/ids.d.ts +3 -0
- package/dist/util/ids.d.ts.map +1 -0
- package/dist/util/ids.js +8 -0
- package/dist/util/ids.js.map +1 -0
- package/dist/validate-request.d.ts +9 -0
- package/dist/validate-request.d.ts.map +1 -0
- package/dist/validate-request.js +137 -0
- package/dist/validate-request.js.map +1 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# @x12i/content-fetcher
|
|
2
|
+
|
|
3
|
+
Policy-aware content acquisition for known, approved sources.
|
|
4
|
+
|
|
5
|
+
The fetcher receives an **exact** resource (URL, feed entry, API resource, archive reference, licensed-provider reference, or manually supplied file), validates it against a source-policy registry, retrieves the permitted content through an approved channel, and preserves the original bytes together with provenance, rights, and acquisition outcomes. Downstream packages (extraction, OCR, transcription, Markdown normalization) consume the retained artifacts.
|
|
6
|
+
|
|
7
|
+
It **never** discovers URLs, crawls, schedules, extracts text, uses an LLM, or bypasses access restrictions (CAPTCHAs, WAFs, bot challenges, logins, paywalls, geo-blocks, rate limits). Restrictions are detected and reported, not circumvented.
|
|
8
|
+
|
|
9
|
+
- **Runtime:** Node.js ≥ 20, ESM only, zero runtime dependencies.
|
|
10
|
+
- **Not included:** URL discovery, crawling, scheduling, text extraction, Markdown generation, classification, summarization.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @x12i/content-fetcher
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import {
|
|
22
|
+
createContentFetcher,
|
|
23
|
+
toFetchedArtifactReference,
|
|
24
|
+
type SourcePolicy,
|
|
25
|
+
} from "@x12i/content-fetcher";
|
|
26
|
+
|
|
27
|
+
const policies: SourcePolicy[] = [
|
|
28
|
+
{
|
|
29
|
+
sourceId: "metr",
|
|
30
|
+
publisher: "Metropolitan Review",
|
|
31
|
+
allowedDomains: ["example.com"],
|
|
32
|
+
allowedProtocols: ["https"],
|
|
33
|
+
allowedChannels: ["direct_http", "rss_or_atom"],
|
|
34
|
+
channelOrder: ["direct_http"],
|
|
35
|
+
robotsPolicy: "required",
|
|
36
|
+
browserAccess: "disabled",
|
|
37
|
+
archiveAccess: "disabled",
|
|
38
|
+
licensedProviderAccess: "disabled",
|
|
39
|
+
rateLimit: { maxRequestsPerMinute: 30, maxConcurrentRequests: 2 },
|
|
40
|
+
limits: { maxRedirects: 3, maxResponseBytes: 8 * 1024 * 1024 },
|
|
41
|
+
rights: {
|
|
42
|
+
profileId: "metr-public-v1",
|
|
43
|
+
allowRawRetention: true,
|
|
44
|
+
allowExtraction: true,
|
|
45
|
+
allowDisplay: true,
|
|
46
|
+
allowTraining: false,
|
|
47
|
+
retentionClass: "public-web",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const fetcher = createContentFetcher({
|
|
53
|
+
sourcePolicies: policies,
|
|
54
|
+
artifactStore: myArtifactStore, // your implementation
|
|
55
|
+
// httpClient, browserAdapter, authProvider, dnsResolver are optional
|
|
56
|
+
// adapters; see "Adapters" below. Default HTTP and DNS adapters are used
|
|
57
|
+
// when omitted (httpClient is required if any policy enables a network
|
|
58
|
+
// channel).
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const result = await fetcher.fetch({
|
|
62
|
+
requestId: "req_0001",
|
|
63
|
+
sourceId: "metr",
|
|
64
|
+
resource: { url: "https://example.com/articles/article.html" },
|
|
65
|
+
previousVersion: { etag: '"v1"' },
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (result.status === "collected") {
|
|
69
|
+
const reference = toFetchedArtifactReference(result);
|
|
70
|
+
// hand `reference` to the extraction / normalization pipeline
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## How a request flows
|
|
75
|
+
|
|
76
|
+
1. **Request validation** — malformed requests throw `InvalidFetchRequestError`; an unknown `sourceId` throws `UnknownSourcePolicyError`. Expected retrieval outcomes are returned as statuses, never thrown.
|
|
77
|
+
2. **Channel selection** — the resource kind maps to candidate channels (`url` → `direct_http`/`permitted_browser`, `feedEntryUrl` → `rss_or_atom`, `apiResource` → `official_api`, `archiveReference` → `approved_archive`, `licensedResourceId` → `licensed_provider`, `manualFile` → `manual_file`). The policy's `channelOrder` (restricted to `allowedChannels`) decides the plan. Fallback to the next channel happens only for `temporarily_unavailable`; access-restriction outcomes are terminal — retrying through another channel would amount to a bypass attempt.
|
|
78
|
+
3. **Security gates (before any network request)** — URL parsing rejects non-HTTP(S) schemes and embedded credentials (`UnsafeUrlError`); blocked hostnames (`localhost`, cloud-metadata names, `*.internal`) and blocked IP ranges (loopback, private, shared/CGNAT, link-local, multicast, reserved, documentation — including IPv4-mapped IPv6) throw `UnsafeUrlError`; DNS is resolved and **every** resolved address is re-validated. A hostname that cannot be resolved yields `temporarily_unavailable` (`dns_resolution_failed`) — nothing is fetched unverifiable.
|
|
79
|
+
4. **Policy gates** — protocol and domain allowlists; violations return `source_not_authorised` before any request is made.
|
|
80
|
+
5. **Robots** — when `robotsPolicy: "required"` (applies to `direct_http`, `rss_or_atom`, `permitted_browser`), robots.txt is fetched (per-origin TTL cache), parsed per RFC 9309 semantics, and evaluated for the configured user agent. Disallowed → `robots_disallowed` without fetching the resource. robots.txt unavailable (401/403/5xx/network) → `manual_review_required`. 404 → permissive.
|
|
81
|
+
6. **Rate limiting** — per-source sliding one-minute window plus concurrency cap. Local refusals return `rate_limited` with `retryAfterSeconds`.
|
|
82
|
+
7. **Retrieval** — `If-None-Match`/`If-Modified-Since` are sent from `previousVersion`; `304` → `not_modified`. Redirects are followed one hop at a time; **each destination is re-validated** (scheme, credentials, protocol/domain policy, address safety, robots) before the next request. Violations → `redirect_disallowed`.
|
|
83
|
+
8. **Classification** — status codes plus high-precision body markers map to explicit statuses (`login_required`, `paywalled`, `waf_or_bot_challenge`, `rate_limited`, `geo_restricted`, …). Detection never triggers a bypass.
|
|
84
|
+
9. **Content gates** — non-identity content encodings are rejected (`unsupported_content_type` / `unsupported_content_encoding`) so retained bytes are exactly what the server sent; `expectedContentTypes` mismatches are rejected; responses are streamed under `maxResponseBytes`; declared `Content-Length` mismatches are flagged `integrity_error`.
|
|
85
|
+
10. **Persistence** — SHA-256 is computed; identical content per source is deduplicated (`findByContentHash`, never overwriting an existing artifact, `version.duplicateOfArtifactId` set) while every acquisition keeps its own metadata record and audit record. When `rights.allowRawRetention` is false the bytes are hashed but **not** stored and the result is `metadata_only`.
|
|
86
|
+
|
|
87
|
+
## Acquisition statuses
|
|
88
|
+
|
|
89
|
+
`collected`, `not_modified`, `metadata_only`, `robots_disallowed`, `rate_limited`, `waf_or_bot_challenge`, `login_required`, `paywalled`, `geo_restricted`, `source_not_authorised`, `not_found`, `temporarily_unavailable`, `redirect_disallowed`, `content_too_large`, `unsupported_content_type`, `integrity_error`, `manual_review_required`
|
|
90
|
+
|
|
91
|
+
There is deliberately no generic `failed` status.
|
|
92
|
+
|
|
93
|
+
## Adapters
|
|
94
|
+
|
|
95
|
+
The package has no I/O of its own; everything injectable:
|
|
96
|
+
|
|
97
|
+
| Adapter | Purpose | Default |
|
|
98
|
+
| --- | --- | --- |
|
|
99
|
+
| `ArtifactStore` | Immutable raw artifact + metadata + audit storage. **Required.** | none |
|
|
100
|
+
| `HttpClient` | HTTP transport. Must not follow redirects itself; the fetcher validates each hop. | `DefaultHttpClient` (platform `fetch`, manual redirects, `accept-encoding: identity`, per-request timeout) |
|
|
101
|
+
| `DnsResolver` | Address resolution so SSRF validation can run after DNS. | `SystemDnsResolver` (`node:dns`) |
|
|
102
|
+
| `BrowserAdapter` | `permitted_browser` channel. Must report `blockedReason` instead of attempting to bypass challenges. Required at fetch time when a browser acquisition is attempted. | none |
|
|
103
|
+
| `AuthProvider` | Supplies credentials for a policy's `authProfileId`. Credentials are attached to requests only — never stored, logged, or echoed in results. Required at fetch time when an authenticated channel is used. | none |
|
|
104
|
+
|
|
105
|
+
Notes on resource fields: `apiResource`, `feedEntryUrl`, `archiveReference`, and `licensedResourceId` must each be an absolute URL to the exact resource; they are validated against the same policy as `url`. Wayback-style archive references additionally record capture timestamp and original resource URL in the acquisition metadata.
|
|
106
|
+
|
|
107
|
+
## Security model
|
|
108
|
+
|
|
109
|
+
- Validation occurs before the first request, after DNS resolution, and before/after every redirect hop (the fetcher drives redirects manually, so every destination is validated before it is contacted).
|
|
110
|
+
- Stored/returned URLs are sanitized: credentials, query strings, and fragments are removed before anything is persisted, returned, or audited.
|
|
111
|
+
- Only an allowlist of benign response headers (`content-type`, `etag`, `last-modified`, `date`, `cache-control`, `expires`, `content-length`, `content-encoding`, `x-archive-*`) is retained; cookies and authentication material never reach storage.
|
|
112
|
+
- Audit records contain identifiers, statuses, timestamps, and hashes only.
|
|
113
|
+
- Diagnostic messages come from a fixed vocabulary (`sanitizedMessage`); raw error text is never surfaced.
|
|
114
|
+
- Known limitation: validate-then-connect leaves a small DNS-rebinding window. Deployments that need connection-level pinning should wrap `undici`'s `Agent` with a custom `connect` that pins the validated address, or front the process with an egress proxy enforcing the same policy.
|
|
115
|
+
|
|
116
|
+
## Downstream handoff
|
|
117
|
+
|
|
118
|
+
`toFetchedArtifactReference(result)` returns the `FetchedArtifactReference` (acquisition id, artifact id, source, file name, content type, hash, provenance, rights) for any result with a retained artifact, or `null` otherwise (e.g. `metadata_only`). Extraction, OCR, transcription, canonical construction, and Markdown normalization are the next package's job.
|
|
119
|
+
|
|
120
|
+
## Typed errors
|
|
121
|
+
|
|
122
|
+
`InvalidFetchRequestError`, `UnknownSourcePolicyError`, `SourcePolicyViolationError`, `UnsafeUrlError`, `UnsupportedChannelError`, `ArtifactStoreError`, `ConfigurationError` — thrown for invalid usage, configuration, or deployment wiring. Everything expected during retrieval is an `AcquisitionResult` status.
|
|
123
|
+
|
|
124
|
+
## Development
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npm install
|
|
128
|
+
npm run build # tsc -> dist/
|
|
129
|
+
npm test # vitest
|
|
130
|
+
npm run typecheck
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The test suite covers all acceptance criteria from the package requirement: byte-exact retrieval, domain/redirect policy enforcement, robots compliance, conditional retrieval, deduplication, rights gating, browser gating, explicit restriction statuses, SSRF blocking, output sanitization, and downstream handoff.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { HttpClient, HttpClientRequestInit, HttpClientResponse } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Default HTTP transport built on the platform `fetch`.
|
|
4
|
+
*
|
|
5
|
+
* - Redirects are NEVER followed automatically; the fetcher validates each
|
|
6
|
+
* hop against the source policy before following it.
|
|
7
|
+
* - `accept-encoding: identity` keeps the stored bytes identical to what the
|
|
8
|
+
* server sent, honouring the preserve-original-bytes requirement.
|
|
9
|
+
* - The injected AbortSignal (timeouts, size aborts) is chained through.
|
|
10
|
+
*/
|
|
11
|
+
export declare class DefaultHttpClient implements HttpClient {
|
|
12
|
+
private readonly userAgent;
|
|
13
|
+
private readonly requestTimeoutMs;
|
|
14
|
+
constructor(userAgent: string, requestTimeoutMs: number);
|
|
15
|
+
request(url: string, init?: HttpClientRequestInit): Promise<HttpClientResponse>;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=default-http-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"default-http-client.d.ts","sourceRoot":"","sources":["../../src/adapters/default-http-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,qBAAqB,EACrB,kBAAkB,EACnB,MAAM,aAAa,CAAC;AAErB;;;;;;;;GAQG;AACH,qBAAa,iBAAkB,YAAW,UAAU;IAEhD,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,gBAAgB;gBADhB,SAAS,EAAE,MAAM,EACjB,gBAAgB,EAAE,MAAM;IAGrC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,kBAAkB,CAAC;CA0CtF"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default HTTP transport built on the platform `fetch`.
|
|
3
|
+
*
|
|
4
|
+
* - Redirects are NEVER followed automatically; the fetcher validates each
|
|
5
|
+
* hop against the source policy before following it.
|
|
6
|
+
* - `accept-encoding: identity` keeps the stored bytes identical to what the
|
|
7
|
+
* server sent, honouring the preserve-original-bytes requirement.
|
|
8
|
+
* - The injected AbortSignal (timeouts, size aborts) is chained through.
|
|
9
|
+
*/
|
|
10
|
+
export class DefaultHttpClient {
|
|
11
|
+
userAgent;
|
|
12
|
+
requestTimeoutMs;
|
|
13
|
+
constructor(userAgent, requestTimeoutMs) {
|
|
14
|
+
this.userAgent = userAgent;
|
|
15
|
+
this.requestTimeoutMs = requestTimeoutMs;
|
|
16
|
+
}
|
|
17
|
+
async request(url, init) {
|
|
18
|
+
const controller = new AbortController();
|
|
19
|
+
const timer = setTimeout(() => controller.abort(new Error("request timed out")), this.requestTimeoutMs);
|
|
20
|
+
const onCallerAbort = () => controller.abort(new Error("request aborted"));
|
|
21
|
+
init?.signal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
22
|
+
try {
|
|
23
|
+
const response = await fetch(url, {
|
|
24
|
+
method: init?.method ?? "GET",
|
|
25
|
+
headers: {
|
|
26
|
+
"user-agent": this.userAgent,
|
|
27
|
+
accept: "*/*",
|
|
28
|
+
"accept-encoding": "identity",
|
|
29
|
+
...(init?.headers ?? {}),
|
|
30
|
+
},
|
|
31
|
+
redirect: "manual",
|
|
32
|
+
signal: controller.signal,
|
|
33
|
+
});
|
|
34
|
+
const headers = {};
|
|
35
|
+
const seen = new Map();
|
|
36
|
+
response.headers.forEach((value, name) => {
|
|
37
|
+
const key = name.toLowerCase();
|
|
38
|
+
const list = seen.get(key) ?? [];
|
|
39
|
+
list.push(value);
|
|
40
|
+
seen.set(key, list);
|
|
41
|
+
});
|
|
42
|
+
for (const [key, values] of seen)
|
|
43
|
+
headers[key] = values.join(", ");
|
|
44
|
+
const body = response.body
|
|
45
|
+
? streamToAsyncIterable(response.body)
|
|
46
|
+
: undefined;
|
|
47
|
+
return { status: response.status, headers, body };
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
init?.signal?.removeEventListener("abort", onCallerAbort);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function streamToAsyncIterable(stream) {
|
|
56
|
+
return {
|
|
57
|
+
[Symbol.asyncIterator]() {
|
|
58
|
+
const reader = stream.getReader();
|
|
59
|
+
return {
|
|
60
|
+
async next() {
|
|
61
|
+
const { done, value } = await reader.read();
|
|
62
|
+
if (done)
|
|
63
|
+
return { done: true, value: undefined };
|
|
64
|
+
return { done: false, value: value };
|
|
65
|
+
},
|
|
66
|
+
async return() {
|
|
67
|
+
await reader.cancel().catch(() => undefined);
|
|
68
|
+
return { done: true, value: undefined };
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=default-http-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"default-http-client.js","sourceRoot":"","sources":["../../src/adapters/default-http-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ResourceKind } from "./validate-request.js";
|
|
2
|
+
import type { AcquisitionStatus, AcquisitionChannel, SourcePolicy } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Maps a request's resource kind to the channels that can acquire it.
|
|
5
|
+
* Channel fallback then follows the policy's channelOrder restricted to this
|
|
6
|
+
* candidate set.
|
|
7
|
+
*/
|
|
8
|
+
export declare const KIND_CANDIDATE_CHANNELS: Record<ResourceKind, readonly AcquisitionChannel[]>;
|
|
9
|
+
/** Channels governed by robots.txt when the policy requires it. */
|
|
10
|
+
export declare const ROBOTS_GOVERNED_CHANNELS: readonly AcquisitionChannel[];
|
|
11
|
+
/**
|
|
12
|
+
* Select the ordered channel plan for a request: policy channelOrder entries
|
|
13
|
+
* that are both allowed by the policy and compatible with the resource kind.
|
|
14
|
+
*/
|
|
15
|
+
export declare function selectChannelPlan(kind: ResourceKind, policy: SourcePolicy): AcquisitionChannel[];
|
|
16
|
+
/** Additional policy gates beyond channel allowlisting. */
|
|
17
|
+
export declare function channelPermittedByPolicy(channel: AcquisitionChannel, policy: SourcePolicy): boolean;
|
|
18
|
+
export declare const FALLTHROUGH_STATUSES: readonly AcquisitionStatus[];
|
|
19
|
+
export declare function sanitizedMessageFor(reasonCode: string): string;
|
|
20
|
+
export declare function requireBrowserAdapter(browserAdapter: unknown): asserts browserAdapter is NonNullable<unknown>;
|
|
21
|
+
export declare function isTokenHeaderName(name: string): boolean;
|
|
22
|
+
export declare function contentTypeMatches(actual: string, expected: readonly string[]): boolean;
|
|
23
|
+
/** Extract a feed entry identifier from a fragment or common query parameters, if present. */
|
|
24
|
+
export declare function extractFeedEntryId(raw: string): string | undefined;
|
|
25
|
+
export interface ArchiveReferenceDetails {
|
|
26
|
+
captureTimestamp?: string;
|
|
27
|
+
originalResourceUrl?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Parse wayback-style archive references (`/web/<timestamp>/<original-url>`)
|
|
31
|
+
* for provenance. Returns empty details for other archive URL shapes.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseArchiveReference(url: URL): ArchiveReferenceDetails;
|
|
34
|
+
//# sourceMappingURL=channels.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channels.d.ts","sourceRoot":"","sources":["../src/channels.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAEtF;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAAC,YAAY,EAAE,SAAS,kBAAkB,EAAE,CAOvF,CAAC;AAEF,mEAAmE;AACnE,eAAO,MAAM,wBAAwB,EAAE,SAAS,kBAAkB,EAIjE,CAAC;AAEF;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,YAAY,EAClB,MAAM,EAAE,YAAY,GACnB,kBAAkB,EAAE,CAKtB;AAED,2DAA2D;AAC3D,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,kBAAkB,EAC3B,MAAM,EAAE,YAAY,GACnB,OAAO,CAWT;AAED,eAAO,MAAM,oBAAoB,EAAE,SAAS,iBAAiB,EAE5D,CAAC;AA+DF,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED,wBAAgB,qBAAqB,CACnC,cAAc,EAAE,OAAO,GACtB,OAAO,CAAC,cAAc,IAAI,WAAW,CAAC,OAAO,CAAC,CAOhD;AAID,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEvD;AAID,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAWvF;AAED,8FAA8F;AAC9F,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAalE;AAED,MAAM,WAAW,uBAAuB;IACtC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,GAAG,GAAG,uBAAuB,CAYvE"}
|
package/dist/channels.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { UnsupportedChannelError } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Maps a request's resource kind to the channels that can acquire it.
|
|
4
|
+
* Channel fallback then follows the policy's channelOrder restricted to this
|
|
5
|
+
* candidate set.
|
|
6
|
+
*/
|
|
7
|
+
export const KIND_CANDIDATE_CHANNELS = {
|
|
8
|
+
url: ["direct_http", "permitted_browser"],
|
|
9
|
+
api_resource: ["official_api"],
|
|
10
|
+
feed_entry: ["rss_or_atom"],
|
|
11
|
+
archive_reference: ["approved_archive"],
|
|
12
|
+
licensed_resource: ["licensed_provider"],
|
|
13
|
+
manual_file: ["manual_file"],
|
|
14
|
+
};
|
|
15
|
+
/** Channels governed by robots.txt when the policy requires it. */
|
|
16
|
+
export const ROBOTS_GOVERNED_CHANNELS = [
|
|
17
|
+
"direct_http",
|
|
18
|
+
"rss_or_atom",
|
|
19
|
+
"permitted_browser",
|
|
20
|
+
];
|
|
21
|
+
/**
|
|
22
|
+
* Select the ordered channel plan for a request: policy channelOrder entries
|
|
23
|
+
* that are both allowed by the policy and compatible with the resource kind.
|
|
24
|
+
*/
|
|
25
|
+
export function selectChannelPlan(kind, policy) {
|
|
26
|
+
const candidates = KIND_CANDIDATE_CHANNELS[kind];
|
|
27
|
+
return policy.channelOrder.filter((channel) => candidates.includes(channel) && policy.allowedChannels.includes(channel));
|
|
28
|
+
}
|
|
29
|
+
/** Additional policy gates beyond channel allowlisting. */
|
|
30
|
+
export function channelPermittedByPolicy(channel, policy) {
|
|
31
|
+
if (!policy.allowedChannels.includes(channel))
|
|
32
|
+
return false;
|
|
33
|
+
if (channel === "permitted_browser" && policy.browserAccess !== "permitted")
|
|
34
|
+
return false;
|
|
35
|
+
if (channel === "approved_archive" && policy.archiveAccess !== "permitted")
|
|
36
|
+
return false;
|
|
37
|
+
if (channel === "licensed_provider" &&
|
|
38
|
+
policy.licensedProviderAccess !== "permitted") {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
export const FALLTHROUGH_STATUSES = [
|
|
44
|
+
"temporarily_unavailable",
|
|
45
|
+
];
|
|
46
|
+
/** Fixed, safe diagnostic messages. Raw error text never reaches results. */
|
|
47
|
+
const SANITIZED_MESSAGES = {
|
|
48
|
+
domain_not_allowed: "The resource host is not allowed by the source policy.",
|
|
49
|
+
protocol_not_allowed: "The resource protocol is not allowed by the source policy.",
|
|
50
|
+
channel_not_permitted: "No permitted acquisition channel supports this resource.",
|
|
51
|
+
local_rate_limit: "The source rate limit defined by policy has been reached.",
|
|
52
|
+
window_exhausted: "The source rate limit defined by policy has been reached.",
|
|
53
|
+
concurrency_exhausted: "The source concurrency limit defined by policy has been reached.",
|
|
54
|
+
robots_disallowed_path: "robots.txt disallows this resource for the configured user agent.",
|
|
55
|
+
redirect_target_robots_disallowed: "robots.txt disallows the redirect destination for the configured user agent.",
|
|
56
|
+
robots_txt_unavailable: "robots.txt could not be retrieved; permission to fetch cannot be verified.",
|
|
57
|
+
http_429: "The source responded with a rate-limit status.",
|
|
58
|
+
challenge_detected: "A bot challenge or WAF interstitial was detected.",
|
|
59
|
+
login_marker_detected: "The resource requires authentication.",
|
|
60
|
+
paywall_marker_detected: "The resource is paywalled.",
|
|
61
|
+
geo_marker_detected: "The resource is geo-restricted.",
|
|
62
|
+
http_401: "The resource requires authentication.",
|
|
63
|
+
http_402: "The resource requires payment.",
|
|
64
|
+
http_451: "The resource is unavailable for legal or geographic reasons.",
|
|
65
|
+
max_redirects_exceeded: "The redirect chain exceeded the policy limit.",
|
|
66
|
+
redirect_domain_not_allowed: "A redirect destination is not allowed by the source policy.",
|
|
67
|
+
unsafe_redirect_address: "A redirect destination targets an address the package must never contact.",
|
|
68
|
+
unsafe_redirect_scheme: "A redirect destination uses a protocol other than http or https.",
|
|
69
|
+
redirect_credentials_forbidden: "A redirect destination contained credentials.",
|
|
70
|
+
redirect_location_invalid: "A redirect destination could not be parsed.",
|
|
71
|
+
redirect_missing_location: "A redirect response carried no Location header.",
|
|
72
|
+
redirect_protocol_not_allowed: "A redirect destination protocol is not allowed by the source policy.",
|
|
73
|
+
dns_resolution_failed: "The resource host could not be resolved.",
|
|
74
|
+
network_error: "The request failed at the network level.",
|
|
75
|
+
request_timed_out: "The request timed out.",
|
|
76
|
+
unsupported_content_encoding: "The response used a content encoding the package does not accept.",
|
|
77
|
+
content_type_mismatch: "The response content type does not match the expected content types.",
|
|
78
|
+
max_bytes_exceeded: "The response exceeded the maximum response size.",
|
|
79
|
+
declared_length_exceeded: "The declared response length exceeded the maximum response size.",
|
|
80
|
+
content_length_mismatch: "The response length did not match its declared Content-Length.",
|
|
81
|
+
auth_unavailable: "Authentication required by the source policy could not be supplied.",
|
|
82
|
+
auth_invalid: "The configured authentication produced an invalid header.",
|
|
83
|
+
browser_adapter_reported_block: "The browser adapter reported an access restriction; no bypass was attempted.",
|
|
84
|
+
empty_browser_response: "The browser adapter returned no document.",
|
|
85
|
+
previous_hash_mismatch: "The supplied bytes do not match the declared previous content hash.",
|
|
86
|
+
http_success: "The resource was retrieved successfully.",
|
|
87
|
+
http_304: "The resource was not modified since the previous acquisition.",
|
|
88
|
+
duplicate_content: "The content matches an already retained artifact.",
|
|
89
|
+
manual_file_supplied: "A manually supplied file was accepted for acquisition.",
|
|
90
|
+
raw_retention_not_allowed: "The rights profile for this source does not permit raw retention; only metadata was retained.",
|
|
91
|
+
empty_response: "The response contained no content.",
|
|
92
|
+
};
|
|
93
|
+
export function sanitizedMessageFor(reasonCode) {
|
|
94
|
+
return SANITIZED_MESSAGES[reasonCode] ?? "The acquisition ended with an explicit outcome.";
|
|
95
|
+
}
|
|
96
|
+
export function requireBrowserAdapter(browserAdapter) {
|
|
97
|
+
if (browserAdapter === undefined || browserAdapter === null) {
|
|
98
|
+
throw new UnsupportedChannelError("Channel 'permitted_browser' was selected but no browser adapter is configured.", "permitted_browser");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const HEADER_NAME_RE = /^[\w-]+$/;
|
|
102
|
+
export function isTokenHeaderName(name) {
|
|
103
|
+
return HEADER_NAME_RE.test(name);
|
|
104
|
+
}
|
|
105
|
+
// Match a response content type against expected patterns such as
|
|
106
|
+
// "text/html", "application/" + "*", or the any-type wildcard pattern.
|
|
107
|
+
export function contentTypeMatches(actual, expected) {
|
|
108
|
+
const [type, subtype] = actual.split(";")[0].trim().toLowerCase().split("/");
|
|
109
|
+
if (type === undefined || subtype === undefined)
|
|
110
|
+
return false;
|
|
111
|
+
return expected.some((pattern) => {
|
|
112
|
+
const [expectedType, expectedSubtype] = pattern.trim().toLowerCase().split("/");
|
|
113
|
+
if (expectedType === undefined || expectedSubtype === undefined)
|
|
114
|
+
return false;
|
|
115
|
+
return ((expectedType === "*" || expectedType === type) &&
|
|
116
|
+
(expectedSubtype === "*" || expectedSubtype === subtype));
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/** Extract a feed entry identifier from a fragment or common query parameters, if present. */
|
|
120
|
+
export function extractFeedEntryId(raw) {
|
|
121
|
+
try {
|
|
122
|
+
const url = new URL(raw);
|
|
123
|
+
const fragment = url.hash.replace(/^#/, "");
|
|
124
|
+
if (fragment !== "")
|
|
125
|
+
return fragment;
|
|
126
|
+
for (const key of ["entry", "id", "guid"]) {
|
|
127
|
+
const value = url.searchParams.get(key);
|
|
128
|
+
if (value !== null && value !== "")
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Parse wayback-style archive references (`/web/<timestamp>/<original-url>`)
|
|
139
|
+
* for provenance. Returns empty details for other archive URL shapes.
|
|
140
|
+
*/
|
|
141
|
+
export function parseArchiveReference(url) {
|
|
142
|
+
let pathname;
|
|
143
|
+
try {
|
|
144
|
+
pathname = decodeURIComponent(url.pathname);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
pathname = url.pathname;
|
|
148
|
+
}
|
|
149
|
+
const match = pathname.match(/^\/web\/[^/]*?(\d{8,14})[^/]*?\/(https?:\/\/.+)$/i);
|
|
150
|
+
if (match?.[1] && match[2]) {
|
|
151
|
+
return { captureTimestamp: match[1], originalResourceUrl: sanitizeForProvenance(match[2]) };
|
|
152
|
+
}
|
|
153
|
+
return {};
|
|
154
|
+
}
|
|
155
|
+
import { sanitizeUrl } from "./security/url.js";
|
|
156
|
+
function sanitizeForProvenance(raw) {
|
|
157
|
+
try {
|
|
158
|
+
return sanitizeUrl(raw);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return raw;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=channels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channels.js","sourceRoot":"","sources":["../src/channels.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AcquisitionStatus } from "./types.js";
|
|
2
|
+
export interface ClassifiedResponse {
|
|
3
|
+
status: AcquisitionStatus;
|
|
4
|
+
reasonCode: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Classify a response from its status code. `sniffPrefix` (first bytes of the
|
|
8
|
+
* body, text-decoded) refines 403-family and 200-family outcomes.
|
|
9
|
+
*/
|
|
10
|
+
export declare function classifyHttpResponse(httpStatus: number, sniffPrefix: string | null): ClassifiedResponse;
|
|
11
|
+
/** True when a 2xx body should be sniffed for challenge markers. */
|
|
12
|
+
export declare function shouldSniffSuccessBody(contentType: string, byteLength: number): boolean;
|
|
13
|
+
//# sourceMappingURL=classify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../src/classify.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AA4DpD,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,GAAG,IAAI,GACzB,kBAAkB,CAqDpB;AAED,oEAAoE;AACpE,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAEvF"}
|
package/dist/classify.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps an HTTP response to an acquisition status. Uses high-precision markers
|
|
3
|
+
* only, to avoid misclassifying legitimate content that merely mentions
|
|
4
|
+
* CAPTCHAs or subscriptions. Detection never triggers bypass behaviour; the
|
|
5
|
+
* package simply reports what it found.
|
|
6
|
+
*/
|
|
7
|
+
const WAF_CHALLENGE_MARKERS = [
|
|
8
|
+
"just a moment",
|
|
9
|
+
"checking your browser",
|
|
10
|
+
"attention required",
|
|
11
|
+
"cf-chl",
|
|
12
|
+
"cf_chl_",
|
|
13
|
+
"cdn-cgi/challenge-platform",
|
|
14
|
+
"verify you are human",
|
|
15
|
+
"verify you are not a robot",
|
|
16
|
+
"enable javascript and cookies",
|
|
17
|
+
"distil_r_captcha",
|
|
18
|
+
"geo-captcha",
|
|
19
|
+
"px-captcha",
|
|
20
|
+
"_incapsula_resource",
|
|
21
|
+
"sucuri website cloud",
|
|
22
|
+
"bot verification",
|
|
23
|
+
"blocked by ibm security",
|
|
24
|
+
];
|
|
25
|
+
const LOGIN_MARKERS = [
|
|
26
|
+
"sign in to continue",
|
|
27
|
+
"log in to continue",
|
|
28
|
+
"login required",
|
|
29
|
+
"please log in",
|
|
30
|
+
"sign in to view",
|
|
31
|
+
];
|
|
32
|
+
const PAYWALL_MARKERS = [
|
|
33
|
+
"subscribe to continue",
|
|
34
|
+
"subscription required",
|
|
35
|
+
"subscribe to read",
|
|
36
|
+
"premium content",
|
|
37
|
+
"purchase to access",
|
|
38
|
+
"this article is for subscribers",
|
|
39
|
+
];
|
|
40
|
+
const GEO_MARKERS = [
|
|
41
|
+
"not available in your country",
|
|
42
|
+
"not available in your region",
|
|
43
|
+
"access denied due to geographic",
|
|
44
|
+
"geo-restricted",
|
|
45
|
+
];
|
|
46
|
+
function containsMarker(prefix, markers) {
|
|
47
|
+
const lower = prefix.toLowerCase();
|
|
48
|
+
for (const marker of markers) {
|
|
49
|
+
if (lower.includes(marker))
|
|
50
|
+
return marker;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Classify a response from its status code. `sniffPrefix` (first bytes of the
|
|
56
|
+
* body, text-decoded) refines 403-family and 200-family outcomes.
|
|
57
|
+
*/
|
|
58
|
+
export function classifyHttpResponse(httpStatus, sniffPrefix) {
|
|
59
|
+
if (httpStatus === 304)
|
|
60
|
+
return { status: "not_modified", reasonCode: "http_304" };
|
|
61
|
+
if (httpStatus === 401)
|
|
62
|
+
return { status: "login_required", reasonCode: "http_401" };
|
|
63
|
+
if (httpStatus === 402)
|
|
64
|
+
return { status: "paywalled", reasonCode: "http_402" };
|
|
65
|
+
if (httpStatus === 429)
|
|
66
|
+
return { status: "rate_limited", reasonCode: "http_429" };
|
|
67
|
+
if (httpStatus === 451)
|
|
68
|
+
return { status: "geo_restricted", reasonCode: "http_451" };
|
|
69
|
+
if (httpStatus === 404 || httpStatus === 410) {
|
|
70
|
+
return { status: "not_found", reasonCode: `http_${httpStatus}` };
|
|
71
|
+
}
|
|
72
|
+
if (httpStatus === 408) {
|
|
73
|
+
return { status: "temporarily_unavailable", reasonCode: "http_408" };
|
|
74
|
+
}
|
|
75
|
+
if (httpStatus === 413)
|
|
76
|
+
return { status: "content_too_large", reasonCode: "http_413" };
|
|
77
|
+
if (httpStatus === 415) {
|
|
78
|
+
return { status: "unsupported_content_type", reasonCode: "http_415" };
|
|
79
|
+
}
|
|
80
|
+
if (httpStatus === 418 || httpStatus === 403) {
|
|
81
|
+
// Teapot and forbidden are challenge-shaped unless a more specific marker says otherwise.
|
|
82
|
+
if (sniffPrefix !== null) {
|
|
83
|
+
if (containsMarker(sniffPrefix, WAF_CHALLENGE_MARKERS)) {
|
|
84
|
+
return { status: "waf_or_bot_challenge", reasonCode: "challenge_detected" };
|
|
85
|
+
}
|
|
86
|
+
if (containsMarker(sniffPrefix, LOGIN_MARKERS)) {
|
|
87
|
+
return { status: "login_required", reasonCode: "login_marker_detected" };
|
|
88
|
+
}
|
|
89
|
+
if (containsMarker(sniffPrefix, PAYWALL_MARKERS)) {
|
|
90
|
+
return { status: "paywalled", reasonCode: "paywall_marker_detected" };
|
|
91
|
+
}
|
|
92
|
+
if (containsMarker(sniffPrefix, GEO_MARKERS)) {
|
|
93
|
+
return { status: "geo_restricted", reasonCode: "geo_marker_detected" };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { status: "waf_or_bot_challenge", reasonCode: `http_${httpStatus}` };
|
|
97
|
+
}
|
|
98
|
+
if (httpStatus >= 400 && httpStatus < 500) {
|
|
99
|
+
// Remaining 4xx: the request as issued is not permitted by the server.
|
|
100
|
+
return { status: "source_not_authorised", reasonCode: `http_${httpStatus}` };
|
|
101
|
+
}
|
|
102
|
+
if (httpStatus >= 500) {
|
|
103
|
+
return { status: "temporarily_unavailable", reasonCode: `http_${httpStatus}` };
|
|
104
|
+
}
|
|
105
|
+
if (httpStatus >= 200 && httpStatus < 300) {
|
|
106
|
+
// A successful status with an interstitial challenge shell (small HTML).
|
|
107
|
+
if (sniffPrefix !== null && containsMarker(sniffPrefix, WAF_CHALLENGE_MARKERS)) {
|
|
108
|
+
return { status: "waf_or_bot_challenge", reasonCode: "challenge_detected" };
|
|
109
|
+
}
|
|
110
|
+
return { status: "collected", reasonCode: "http_success" };
|
|
111
|
+
}
|
|
112
|
+
if (httpStatus >= 300 && httpStatus < 400) {
|
|
113
|
+
// Should have been handled by the redirect loop.
|
|
114
|
+
return { status: "temporarily_unavailable", reasonCode: "unresolved_redirect" };
|
|
115
|
+
}
|
|
116
|
+
return { status: "temporarily_unavailable", reasonCode: `http_${httpStatus}` };
|
|
117
|
+
}
|
|
118
|
+
/** True when a 2xx body should be sniffed for challenge markers. */
|
|
119
|
+
export function shouldSniffSuccessBody(contentType, byteLength) {
|
|
120
|
+
return contentType.includes("text/html") && byteLength <= 65_536;
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=classify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classify.js","sourceRoot":"","sources":["../src/classify.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AcquisitionChannel, ContentFetcherOptions, SourcePolicy } from "./types.js";
|
|
2
|
+
export declare function isAcquisitionChannel(value: unknown): value is AcquisitionChannel;
|
|
3
|
+
/** Deep runtime validation of one source policy. Throws SourcePolicyViolationError. */
|
|
4
|
+
export declare function validateSourcePolicy(policy: unknown): SourcePolicy;
|
|
5
|
+
/** Validate the whole fetcher configuration. Throws ConfigurationError / SourcePolicyViolationError. */
|
|
6
|
+
export declare function validateConfiguration(options: unknown): asserts options is ContentFetcherOptions;
|
|
7
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAqB1F,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,kBAAkB,CAEhF;AA8BD,uFAAuF;AACvF,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,GAAG,YAAY,CA4KlE;AAED,wGAAwG;AACxG,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,qBAAqB,CAkEhG"}
|