@naulon/sdk 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/dist/cli/check.d.ts +23 -0
- package/dist/cli/check.d.ts.map +1 -0
- package/dist/cli/check.js +100 -0
- package/dist/cli/check.js.map +1 -0
- package/dist/cli/crawl.d.ts +30 -0
- package/dist/cli/crawl.d.ts.map +1 -0
- package/dist/cli/crawl.js +180 -0
- package/dist/cli/crawl.js.map +1 -0
- package/dist/cli/doctor.d.ts +11 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/doctor.js +69 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/init.d.ts +23 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +217 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/naulon.d.ts +3 -0
- package/dist/cli/naulon.d.ts.map +1 -0
- package/dist/cli/naulon.js +50 -0
- package/dist/cli/naulon.js.map +1 -0
- package/dist/contract/credits.d.ts +53 -0
- package/dist/contract/credits.d.ts.map +1 -0
- package/dist/contract/credits.js +56 -0
- package/dist/contract/credits.js.map +1 -0
- package/dist/contract/index.d.ts +4 -0
- package/dist/contract/index.d.ts.map +1 -0
- package/dist/contract/index.js +4 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/contract/settlement.d.ts +67 -0
- package/dist/contract/settlement.d.ts.map +1 -0
- package/dist/contract/settlement.js +52 -0
- package/dist/contract/settlement.js.map +1 -0
- package/dist/contract/wallet.d.ts +23 -0
- package/dist/contract/wallet.d.ts.map +1 -0
- package/dist/contract/wallet.js +26 -0
- package/dist/contract/wallet.js.map +1 -0
- package/dist/crawl/adapters/rss.d.ts +12 -0
- package/dist/crawl/adapters/rss.d.ts.map +1 -0
- package/dist/crawl/adapters/rss.js +119 -0
- package/dist/crawl/adapters/rss.js.map +1 -0
- package/dist/crawl/adapters/sitemap.d.ts +12 -0
- package/dist/crawl/adapters/sitemap.d.ts.map +1 -0
- package/dist/crawl/adapters/sitemap.js +103 -0
- package/dist/crawl/adapters/sitemap.js.map +1 -0
- package/dist/crawl/adapters/wordpress.d.ts +19 -0
- package/dist/crawl/adapters/wordpress.d.ts.map +1 -0
- package/dist/crawl/adapters/wordpress.js +89 -0
- package/dist/crawl/adapters/wordpress.js.map +1 -0
- package/dist/crawl/authors.d.ts +27 -0
- package/dist/crawl/authors.d.ts.map +1 -0
- package/dist/crawl/authors.js +13 -0
- package/dist/crawl/authors.js.map +1 -0
- package/dist/crawl/crawl.d.ts +35 -0
- package/dist/crawl/crawl.d.ts.map +1 -0
- package/dist/crawl/crawl.js +28 -0
- package/dist/crawl/crawl.js.map +1 -0
- package/dist/crawl/credits-file.d.ts +39 -0
- package/dist/crawl/credits-file.d.ts.map +1 -0
- package/dist/crawl/credits-file.js +52 -0
- package/dist/crawl/credits-file.js.map +1 -0
- package/dist/crawl/fetcher.d.ts +15 -0
- package/dist/crawl/fetcher.d.ts.map +1 -0
- package/dist/crawl/fetcher.js +106 -0
- package/dist/crawl/fetcher.js.map +1 -0
- package/dist/crawl/glob.d.ts +19 -0
- package/dist/crawl/glob.d.ts.map +1 -0
- package/dist/crawl/glob.js +59 -0
- package/dist/crawl/glob.js.map +1 -0
- package/dist/crawl/net.d.ts +12 -0
- package/dist/crawl/net.d.ts.map +1 -0
- package/dist/crawl/net.js +93 -0
- package/dist/crawl/net.js.map +1 -0
- package/dist/crawl/registry.d.ts +17 -0
- package/dist/crawl/registry.d.ts.map +1 -0
- package/dist/crawl/registry.js +19 -0
- package/dist/crawl/registry.js.map +1 -0
- package/dist/crawl/slug.d.ts +15 -0
- package/dist/crawl/slug.d.ts.map +1 -0
- package/dist/crawl/slug.js +42 -0
- package/dist/crawl/slug.js.map +1 -0
- package/dist/crawl/types.d.ts +96 -0
- package/dist/crawl/types.d.ts.map +1 -0
- package/dist/crawl/types.js +21 -0
- package/dist/crawl/types.js.map +1 -0
- package/dist/crawl/xml.d.ts +9 -0
- package/dist/crawl/xml.d.ts.map +1 -0
- package/dist/crawl/xml.js +44 -0
- package/dist/crawl/xml.js.map +1 -0
- package/dist/crypto/fixture.d.ts +13 -0
- package/dist/crypto/fixture.d.ts.map +1 -0
- package/dist/crypto/fixture.js +39 -0
- package/dist/crypto/fixture.js.map +1 -0
- package/dist/crypto/sign.d.ts +7 -0
- package/dist/crypto/sign.d.ts.map +1 -0
- package/dist/crypto/sign.js +15 -0
- package/dist/crypto/sign.js.map +1 -0
- package/dist/crypto/verify.d.ts +21 -0
- package/dist/crypto/verify.d.ts.map +1 -0
- package/dist/crypto/verify.js +58 -0
- package/dist/crypto/verify.js.map +1 -0
- package/dist/doctor/doctor.d.ts +27 -0
- package/dist/doctor/doctor.d.ts.map +1 -0
- package/dist/doctor/doctor.js +149 -0
- package/dist/doctor/doctor.js.map +1 -0
- package/dist/express/_bridge.d.ts +32 -0
- package/dist/express/_bridge.d.ts.map +1 -0
- package/dist/express/_bridge.js +34 -0
- package/dist/express/_bridge.js.map +1 -0
- package/dist/express/credits-route.d.ts +6 -0
- package/dist/express/credits-route.d.ts.map +1 -0
- package/dist/express/credits-route.js +23 -0
- package/dist/express/credits-route.js.map +1 -0
- package/dist/express/index.d.ts +10 -0
- package/dist/express/index.d.ts.map +1 -0
- package/dist/express/index.js +9 -0
- package/dist/express/index.js.map +1 -0
- package/dist/express/settlement-receiver.d.ts +14 -0
- package/dist/express/settlement-receiver.d.ts.map +1 -0
- package/dist/express/settlement-receiver.js +31 -0
- package/dist/express/settlement-receiver.js.map +1 -0
- package/dist/idempotency.d.ts +23 -0
- package/dist/idempotency.d.ts.map +1 -0
- package/dist/idempotency.js +19 -0
- package/dist/idempotency.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/init/plan.d.ts +76 -0
- package/dist/init/plan.d.ts.map +1 -0
- package/dist/init/plan.js +132 -0
- package/dist/init/plan.js.map +1 -0
- package/dist/next/credits-route.d.ts +20 -0
- package/dist/next/credits-route.d.ts.map +1 -0
- package/dist/next/credits-route.js +18 -0
- package/dist/next/credits-route.js.map +1 -0
- package/dist/next/index.d.ts +10 -0
- package/dist/next/index.d.ts.map +1 -0
- package/dist/next/index.js +10 -0
- package/dist/next/index.js.map +1 -0
- package/dist/next/settlement-receiver.d.ts +12 -0
- package/dist/next/settlement-receiver.d.ts.map +1 -0
- package/dist/next/settlement-receiver.js +42 -0
- package/dist/next/settlement-receiver.js.map +1 -0
- package/dist/resolver/fixture.d.ts +10 -0
- package/dist/resolver/fixture.d.ts.map +1 -0
- package/dist/resolver/fixture.js +33 -0
- package/dist/resolver/fixture.js.map +1 -0
- package/dist/resolver/http.d.ts +3 -0
- package/dist/resolver/http.d.ts.map +1 -0
- package/dist/resolver/http.js +23 -0
- package/dist/resolver/http.js.map +1 -0
- package/dist/resolver/types.d.ts +11 -0
- package/dist/resolver/types.d.ts.map +1 -0
- package/dist/resolver/types.js +2 -0
- package/dist/resolver/types.js.map +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* crawl/fetcher.ts — the guarded `Fetcher` the crawl orchestrator hands every adapter.
|
|
3
|
+
*
|
|
4
|
+
* It enforces three things before a byte is sent, so an adapter that derives a URL can only
|
|
5
|
+
* ever reach the one host the publisher owns:
|
|
6
|
+
* 1. VERIFIED-ORIGIN-ONLY — the request host must equal the configured origin host.
|
|
7
|
+
* 2. SSRF CIDR block — the resolved IP must not be private/loopback/link-local (`net.ts`).
|
|
8
|
+
* 3. ANTI-DNS-REBIND — the socket connects to the IP the guard validated, via the connect-
|
|
9
|
+
* time `guardedLookup` (no TOCTOU between the check and the connect).
|
|
10
|
+
*
|
|
11
|
+
* A `fetchImpl` is injectable for tests (the stub path skips the node-http machinery — moot for
|
|
12
|
+
* a fake — but STILL enforces origin + https).
|
|
13
|
+
*/
|
|
14
|
+
import http from "node:http";
|
|
15
|
+
import https from "node:https";
|
|
16
|
+
import { guardedLookup, isBlockedTarget } from "./net.js";
|
|
17
|
+
/** Cap on a fetched body — feeds/sitemaps are text, but a hostile origin must not OOM us. */
|
|
18
|
+
const MAX_BODY = 8 * 1024 * 1024; // 8 MiB
|
|
19
|
+
/** Build the guarded fetcher bound to one origin. */
|
|
20
|
+
export function makeGuardedFetcher(opts) {
|
|
21
|
+
const timeoutMs = opts.timeoutMs ?? 15_000;
|
|
22
|
+
const allowPrivate = opts.allowPrivate ?? false;
|
|
23
|
+
let originHost;
|
|
24
|
+
try {
|
|
25
|
+
originHost = new URL(opts.origin).host; // host = hostname[:port]
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new Error(`crawl fetcher: invalid origin ${opts.origin}`);
|
|
29
|
+
}
|
|
30
|
+
return async (url, init) => {
|
|
31
|
+
let u;
|
|
32
|
+
try {
|
|
33
|
+
u = new URL(url);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new Error(`crawl fetcher: invalid url ${url}`);
|
|
37
|
+
}
|
|
38
|
+
// (1) verified-origin-only — the host must match the proven origin exactly.
|
|
39
|
+
if (u.host !== originHost) {
|
|
40
|
+
throw new Error(`crawl fetcher: off-origin host blocked (${u.host} ≠ ${originHost})`);
|
|
41
|
+
}
|
|
42
|
+
// https only, unless the dev escape hatch is on (local http fixtures).
|
|
43
|
+
if (u.protocol !== "https:" && !(allowPrivate && u.protocol === "http:")) {
|
|
44
|
+
throw new Error(`crawl fetcher: url must be https (${u.protocol})`);
|
|
45
|
+
}
|
|
46
|
+
const headers = {
|
|
47
|
+
"user-agent": "naulon-crawl/1 (+catalog-draft)",
|
|
48
|
+
accept: "application/json, application/rss+xml, application/atom+xml, application/xml, text/xml;q=0.9, */*;q=0.5",
|
|
49
|
+
...init?.headers,
|
|
50
|
+
};
|
|
51
|
+
// Injected fetch (tests) — origin + https already enforced; TOCTOU is moot for a fake.
|
|
52
|
+
if (opts.fetchImpl) {
|
|
53
|
+
const controller = new AbortController();
|
|
54
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
55
|
+
try {
|
|
56
|
+
const res = await opts.fetchImpl(url, { method: "GET", headers, redirect: "manual", signal: controller.signal });
|
|
57
|
+
return wrapResponse(res.status, res.ok, await res.text().catch(() => ""));
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Real path — node http(s) with the connect-time guarded lookup (no rebind window).
|
|
64
|
+
const raw = await getViaNode(url, headers, timeoutMs, allowPrivate, u.protocol);
|
|
65
|
+
return wrapResponse(raw.status, raw.status >= 200 && raw.status < 300, raw.body);
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** A `FetchResult` over a buffered body. `json()` throws on non-JSON (the adapter narrows). */
|
|
69
|
+
function wrapResponse(status, ok, body) {
|
|
70
|
+
return {
|
|
71
|
+
ok,
|
|
72
|
+
status,
|
|
73
|
+
async text() {
|
|
74
|
+
return body;
|
|
75
|
+
},
|
|
76
|
+
async json() {
|
|
77
|
+
return JSON.parse(body);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** GET via node http(s) with the guarded lookup — the TOCTOU-safe real path. node never
|
|
82
|
+
* auto-follows redirects, so a 3xx is returned (not chased to an off-origin location). */
|
|
83
|
+
function getViaNode(urlStr, headers, timeoutMs, allowPrivate, protocol) {
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
const transport = protocol === "http:" ? http : https;
|
|
86
|
+
const req = transport.request(urlStr, { method: "GET", headers, lookup: guardedLookup(allowPrivate) }, (res) => {
|
|
87
|
+
// A private-IP literal in the URL is caught here too (lookup only runs for DNS names).
|
|
88
|
+
const ip = res.socket.remoteAddress;
|
|
89
|
+
if (ip && !allowPrivate && isBlockedTarget(ip)) {
|
|
90
|
+
res.destroy();
|
|
91
|
+
return reject(new Error(`crawl fetcher: connected IP is private/loopback (${ip})`));
|
|
92
|
+
}
|
|
93
|
+
let data = "";
|
|
94
|
+
res.setEncoding("utf8");
|
|
95
|
+
res.on("data", (c) => {
|
|
96
|
+
if (data.length < MAX_BODY)
|
|
97
|
+
data += c;
|
|
98
|
+
});
|
|
99
|
+
res.on("end", () => resolve({ status: res.statusCode ?? 0, body: data.slice(0, MAX_BODY) }));
|
|
100
|
+
});
|
|
101
|
+
req.setTimeout(timeoutMs, () => req.destroy(new Error(`crawl fetcher: timeout after ${timeoutMs}ms`)));
|
|
102
|
+
req.on("error", reject);
|
|
103
|
+
req.end();
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=fetcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetcher.js","sourceRoot":"","sources":["../../src/crawl/fetcher.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* crawl/glob.ts — include/exclude URL-path glob matching for article filtering.
|
|
3
|
+
*
|
|
4
|
+
* A sitemap lists every URL on a site, not just articles, so the sitemap adapter filters
|
|
5
|
+
* with these globs. Deterministic, no model. Supported syntax, deliberately small:
|
|
6
|
+
* `*` — any run of chars except `/`
|
|
7
|
+
* `**` — any run of chars including `/`
|
|
8
|
+
* `?` — a single char except `/`
|
|
9
|
+
* A path passes when it matches at least one include glob (or include is empty → all pass)
|
|
10
|
+
* AND matches no exclude glob (exclude wins — it is the safety lever).
|
|
11
|
+
*/
|
|
12
|
+
/** True if `path` (a URL pathname) matches `glob`. */
|
|
13
|
+
export declare function matchGlob(path: string, glob: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Article gate: passes an include (empty include ⇒ everything is in) and survives every
|
|
16
|
+
* exclude. Exclude takes precedence — it is how an owner carves `/essays/drafts/**` out.
|
|
17
|
+
*/
|
|
18
|
+
export declare function passesGlobs(path: string, include: string[], exclude: string[]): boolean;
|
|
19
|
+
//# sourceMappingURL=glob.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glob.d.ts","sourceRoot":"","sources":["../../src/crawl/glob.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAiCH,sDAAsD;AACtD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAE7D;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAIvF"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* crawl/glob.ts — include/exclude URL-path glob matching for article filtering.
|
|
3
|
+
*
|
|
4
|
+
* A sitemap lists every URL on a site, not just articles, so the sitemap adapter filters
|
|
5
|
+
* with these globs. Deterministic, no model. Supported syntax, deliberately small:
|
|
6
|
+
* `*` — any run of chars except `/`
|
|
7
|
+
* `**` — any run of chars including `/`
|
|
8
|
+
* `?` — a single char except `/`
|
|
9
|
+
* A path passes when it matches at least one include glob (or include is empty → all pass)
|
|
10
|
+
* AND matches no exclude glob (exclude wins — it is the safety lever).
|
|
11
|
+
*/
|
|
12
|
+
/** Compile one glob to an anchored RegExp. Literals are escaped; only `*`/`?` are special. */
|
|
13
|
+
function globToRe(glob) {
|
|
14
|
+
let re = "";
|
|
15
|
+
for (let i = 0; i < glob.length; i++) {
|
|
16
|
+
const ch = glob[i];
|
|
17
|
+
if (ch === "*") {
|
|
18
|
+
if (glob[i + 1] === "*") {
|
|
19
|
+
re += ".*"; // `**` → across segments
|
|
20
|
+
i++;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
re += "[^/]*"; // `*` → within a segment
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else if (ch === "?") {
|
|
27
|
+
re += "[^/]";
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
re += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return new RegExp(`^${re}$`);
|
|
34
|
+
}
|
|
35
|
+
const cache = new Map();
|
|
36
|
+
function compiled(glob) {
|
|
37
|
+
let re = cache.get(glob);
|
|
38
|
+
if (!re) {
|
|
39
|
+
re = globToRe(glob);
|
|
40
|
+
cache.set(glob, re);
|
|
41
|
+
}
|
|
42
|
+
return re;
|
|
43
|
+
}
|
|
44
|
+
/** True if `path` (a URL pathname) matches `glob`. */
|
|
45
|
+
export function matchGlob(path, glob) {
|
|
46
|
+
return compiled(glob).test(path);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Article gate: passes an include (empty include ⇒ everything is in) and survives every
|
|
50
|
+
* exclude. Exclude takes precedence — it is how an owner carves `/essays/drafts/**` out.
|
|
51
|
+
*/
|
|
52
|
+
export function passesGlobs(path, include, exclude) {
|
|
53
|
+
if (exclude.some((g) => matchGlob(path, g)))
|
|
54
|
+
return false;
|
|
55
|
+
if (include.length === 0)
|
|
56
|
+
return true;
|
|
57
|
+
return include.some((g) => matchGlob(path, g));
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=glob.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glob.js","sourceRoot":"","sources":["../../src/crawl/glob.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,8FAA8F;AAC9F,SAAS,QAAQ,CAAC,IAAY;IAC5B,IAAI,EAAE,GAAG,EAAE,CAAC;IACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,CAAE,CAAC;QACpB,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACf,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;gBACxB,EAAE,IAAI,IAAI,CAAC,CAAC,yBAAyB;gBACrC,CAAC,EAAE,CAAC;YACN,CAAC;iBAAM,CAAC;gBACN,EAAE,IAAI,OAAO,CAAC,CAAC,yBAAyB;YAC1C,CAAC;QACH,CAAC;aAAM,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACtB,EAAE,IAAI,MAAM,CAAC;QACf,CAAC;aAAM,CAAC;YACN,EAAE,IAAI,EAAE,CAAC,OAAO,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC;QAChD,CAAC;IACH,CAAC;IACD,OAAO,IAAI,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;AACxC,SAAS,QAAQ,CAAC,IAAY;IAC5B,IAAI,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,IAAI,CAAC,EAAE,EAAE,CAAC;QACR,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;QACpB,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IACtB,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,IAAY;IAClD,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACnC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,OAAiB,EAAE,OAAiB;IAC5E,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IAC1D,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACtC,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AACjD,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { LookupFunction } from "node:net";
|
|
2
|
+
/** True if `host` is a literal IP in a blocked range. A DNS name is NOT blocked here —
|
|
3
|
+
* it has no literal to check; `guardedLookup` validates the resolved IP at connect time. */
|
|
4
|
+
export declare function isBlockedTarget(host: string): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* A `lookup` implementation for `http(s).request` that rejects any resolved address in a
|
|
7
|
+
* blocked range. Passing this as the request's `lookup` closes the check/connect rebind
|
|
8
|
+
* window: the IP the socket connects to is the one that passed the guard. `allowPrivate`
|
|
9
|
+
* (a dev-only knob for local fixture origins) relaxes it.
|
|
10
|
+
*/
|
|
11
|
+
export declare function guardedLookup(allowPrivate: boolean): LookupFunction;
|
|
12
|
+
//# sourceMappingURL=net.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"net.d.ts","sourceRoot":"","sources":["../../src/crawl/net.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AA2C/C;6FAC6F;AAC7F,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAKrD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,YAAY,EAAE,OAAO,GAAG,cAAc,CAenE"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* crawl/net.ts — the SSRF guard for the crawler's network seam.
|
|
3
|
+
*
|
|
4
|
+
* `naulon crawl` fetches whatever URL an adapter derives from a publisher's origin
|
|
5
|
+
* (feed paths, `/wp-json`, sitemap `<loc>`s). Without a guard that is a pointable-at-
|
|
6
|
+
* anything footgun: a hostile or misconfigured feed could name `http://169.254.169.254`
|
|
7
|
+
* (cloud metadata) or `http://10.0.0.5` (an internal service) and the crawler would
|
|
8
|
+
* happily fetch it. So every connection is CIDR-checked against the private/loopback/
|
|
9
|
+
* link-local ranges, and — because a plain check-then-connect leaves a DNS-rebind window
|
|
10
|
+
* — the socket connects through `guardedLookup`, which validates the ACTUAL resolved IP.
|
|
11
|
+
*
|
|
12
|
+
* This is a standalone port of the cloud webhook sender's SSRF machinery, with no cloud
|
|
13
|
+
* dependencies, so the open-source gate carries the same threat model as the hosted one.
|
|
14
|
+
*/
|
|
15
|
+
import { lookup as dnsLookupCb } from "node:dns";
|
|
16
|
+
/** Parse a dotted-quad IPv4 literal to its 32-bit int, or null if it isn't one. */
|
|
17
|
+
function ipToInt(ip) {
|
|
18
|
+
const m = ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
19
|
+
if (!m)
|
|
20
|
+
return null;
|
|
21
|
+
const octets = m.slice(1).map(Number);
|
|
22
|
+
if (octets.some((o) => o > 255))
|
|
23
|
+
return null;
|
|
24
|
+
return ((octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]) >>> 0;
|
|
25
|
+
}
|
|
26
|
+
// [networkInt, maskBits] — the private/loopback/link-local v4 ranges.
|
|
27
|
+
const PRIVATE_V4 = [
|
|
28
|
+
[ipToInt("127.0.0.0"), 8], // loopback
|
|
29
|
+
[ipToInt("10.0.0.0"), 8], // private
|
|
30
|
+
[ipToInt("172.16.0.0"), 12], // private
|
|
31
|
+
[ipToInt("192.168.0.0"), 16], // private
|
|
32
|
+
[ipToInt("169.254.0.0"), 16], // link-local (incl. 169.254.169.254 cloud metadata)
|
|
33
|
+
[ipToInt("0.0.0.0"), 8], // "this" network
|
|
34
|
+
[ipToInt("100.64.0.0"), 10], // CGNAT
|
|
35
|
+
];
|
|
36
|
+
function isPrivateV4(ipInt) {
|
|
37
|
+
return PRIVATE_V4.some(([net, bits]) => {
|
|
38
|
+
const mask = bits === 0 ? 0 : (0xffffffff << (32 - bits)) >>> 0;
|
|
39
|
+
return (ipInt & mask) === (net & mask);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function isBlockedV6(ip) {
|
|
43
|
+
const h = ip.toLowerCase();
|
|
44
|
+
if (h === "::1" || h === "::")
|
|
45
|
+
return true; // loopback / unspecified
|
|
46
|
+
if (h.startsWith("fe80"))
|
|
47
|
+
return true; // link-local
|
|
48
|
+
if (h.startsWith("fc") || h.startsWith("fd"))
|
|
49
|
+
return true; // unique-local fc00::/7
|
|
50
|
+
// IPv4-mapped (::ffff:a.b.c.d) — unwrap and check as v4.
|
|
51
|
+
const mapped = h.match(/::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
52
|
+
if (mapped) {
|
|
53
|
+
const v4 = ipToInt(mapped[1]);
|
|
54
|
+
return v4 !== null && isPrivateV4(v4);
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
/** True if `host` is a literal IP in a blocked range. A DNS name is NOT blocked here —
|
|
59
|
+
* it has no literal to check; `guardedLookup` validates the resolved IP at connect time. */
|
|
60
|
+
export function isBlockedTarget(host) {
|
|
61
|
+
const v4 = ipToInt(host);
|
|
62
|
+
if (v4 !== null)
|
|
63
|
+
return isPrivateV4(v4);
|
|
64
|
+
if (host.includes(":"))
|
|
65
|
+
return isBlockedV6(host);
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A `lookup` implementation for `http(s).request` that rejects any resolved address in a
|
|
70
|
+
* blocked range. Passing this as the request's `lookup` closes the check/connect rebind
|
|
71
|
+
* window: the IP the socket connects to is the one that passed the guard. `allowPrivate`
|
|
72
|
+
* (a dev-only knob for local fixture origins) relaxes it.
|
|
73
|
+
*/
|
|
74
|
+
export function guardedLookup(allowPrivate) {
|
|
75
|
+
return (hostname, options, callback) => {
|
|
76
|
+
dnsLookupCb(hostname, { all: true, family: options.family ?? 0 }, (err, addresses) => {
|
|
77
|
+
if (err)
|
|
78
|
+
return callback(err, "", 0);
|
|
79
|
+
for (const a of addresses) {
|
|
80
|
+
if (!allowPrivate && isBlockedTarget(a.address)) {
|
|
81
|
+
return callback(new Error(`blocked target (private/loopback): ${a.address}`), "", 0);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (options.all)
|
|
85
|
+
return callback(null, addresses);
|
|
86
|
+
const first = addresses[0];
|
|
87
|
+
if (!first)
|
|
88
|
+
return callback(new Error("dns resolution returned no address"), "", 0);
|
|
89
|
+
callback(null, first.address, first.family);
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=net.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"net.js","sourceRoot":"","sources":["../../src/crawl/net.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* crawl/registry.ts — the adapter registry + auto-detect.
|
|
3
|
+
*
|
|
4
|
+
* `naulon crawl` doesn't make the operator name their platform. It probes every on-origin,
|
|
5
|
+
* no-secret adapter and picks the richest one that detects — WordPress (real author objects)
|
|
6
|
+
* over RSS (free-text authors) over sitemap (URLs only). Ties break by `rank`, highest wins.
|
|
7
|
+
* The two feed fallbacks (rss/sitemap) mean nearly any site yields at least a slug catalog.
|
|
8
|
+
*/
|
|
9
|
+
import type { AdapterContext, SourceAdapter } from "./types.ts";
|
|
10
|
+
/** All public on-origin adapters, richest first. */
|
|
11
|
+
export declare const ADAPTERS: readonly SourceAdapter[];
|
|
12
|
+
/** The adapter to force by id, or undefined for auto-detect. */
|
|
13
|
+
export declare function adapterById(id: string): SourceAdapter | undefined;
|
|
14
|
+
/** Probe origin with every adapter (highest rank first) and return the first that detects,
|
|
15
|
+
* or null when none do. Each `detect` uses only the guarded fetch and never throws on "no". */
|
|
16
|
+
export declare function selectAdapter(ctx: AdapterContext): Promise<SourceAdapter | null>;
|
|
17
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/crawl/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAKhE,oDAAoD;AACpD,eAAO,MAAM,QAAQ,EAAE,SAAS,aAAa,EAAmD,CAAC;AAEjG,gEAAgE;AAChE,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAEjE;AAED;gGACgG;AAChG,wBAAsB,aAAa,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAKtF"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { rssAdapter } from "./adapters/rss.js";
|
|
2
|
+
import { sitemapAdapter } from "./adapters/sitemap.js";
|
|
3
|
+
import { wordpressAdapter } from "./adapters/wordpress.js";
|
|
4
|
+
/** All public on-origin adapters, richest first. */
|
|
5
|
+
export const ADAPTERS = [wordpressAdapter, rssAdapter, sitemapAdapter];
|
|
6
|
+
/** The adapter to force by id, or undefined for auto-detect. */
|
|
7
|
+
export function adapterById(id) {
|
|
8
|
+
return ADAPTERS.find((a) => a.id === id);
|
|
9
|
+
}
|
|
10
|
+
/** Probe origin with every adapter (highest rank first) and return the first that detects,
|
|
11
|
+
* or null when none do. Each `detect` uses only the guarded fetch and never throws on "no". */
|
|
12
|
+
export async function selectAdapter(ctx) {
|
|
13
|
+
for (const adapter of [...ADAPTERS].sort((a, b) => b.rank - a.rank)) {
|
|
14
|
+
if (await adapter.detect(ctx))
|
|
15
|
+
return adapter;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/crawl/registry.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAE3D,oDAAoD;AACpD,MAAM,CAAC,MAAM,QAAQ,GAA6B,CAAC,gBAAgB,EAAE,UAAU,EAAE,cAAc,CAAC,CAAC;AAEjG,gEAAgE;AAChE,MAAM,UAAU,WAAW,CAAC,EAAU;IACpC,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;AAC3C,CAAC;AAED;gGACgG;AAChG,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,GAAmB;IACrD,KAAK,MAAM,OAAO,IAAI,CAAC,GAAG,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;QACpE,IAAI,MAAM,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC;YAAE,OAAO,OAAO,CAAC;IAChD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* crawl/slug.ts — credits-key slug derivation, mirroring the gate exactly.
|
|
3
|
+
*
|
|
4
|
+
* The crawler writes `credits.json[slug]`; the gate looks that exact key up via its own
|
|
5
|
+
* `slugFromPath`: the first path segment AFTER a configured article prefix,
|
|
6
|
+
* `^/(?:<prefixes>)/([^/?#]+)`. A slug derived any other way is an article the gate can
|
|
7
|
+
* never find → it would never toll. So this is a faithful re-impl of that one function.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Article slug from a URL, using the publisher's prefixes. Returns the decoded slug, or
|
|
11
|
+
* `null` when the path is not a gateable article (no prefix matches, or it is a gate
|
|
12
|
+
* control route, or the URL is malformed). Matches the gate exactly.
|
|
13
|
+
*/
|
|
14
|
+
export declare function deriveSlug(url: string, prefixes: string[]): string | null;
|
|
15
|
+
//# sourceMappingURL=slug.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slug.d.ts","sourceRoot":"","sources":["../../src/crawl/slug.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAkBzE"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* crawl/slug.ts — credits-key slug derivation, mirroring the gate exactly.
|
|
3
|
+
*
|
|
4
|
+
* The crawler writes `credits.json[slug]`; the gate looks that exact key up via its own
|
|
5
|
+
* `slugFromPath`: the first path segment AFTER a configured article prefix,
|
|
6
|
+
* `^/(?:<prefixes>)/([^/?#]+)`. A slug derived any other way is an article the gate can
|
|
7
|
+
* never find → it would never toll. So this is a faithful re-impl of that one function.
|
|
8
|
+
*/
|
|
9
|
+
/** Escape a string for use as a literal inside a RegExp. */
|
|
10
|
+
function escapeRe(s) {
|
|
11
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Article slug from a URL, using the publisher's prefixes. Returns the decoded slug, or
|
|
15
|
+
* `null` when the path is not a gateable article (no prefix matches, or it is a gate
|
|
16
|
+
* control route, or the URL is malformed). Matches the gate exactly.
|
|
17
|
+
*/
|
|
18
|
+
export function deriveSlug(url, prefixes) {
|
|
19
|
+
let path;
|
|
20
|
+
try {
|
|
21
|
+
path = new URL(url).pathname;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
// Never treat the gate's own control routes as articles, whatever the prefixes are.
|
|
27
|
+
if (path.startsWith("/.well-known/") || path.startsWith("/licenses/"))
|
|
28
|
+
return null;
|
|
29
|
+
const clean = prefixes.filter(Boolean);
|
|
30
|
+
if (clean.length === 0)
|
|
31
|
+
return null;
|
|
32
|
+
const m = path.match(new RegExp(`^/(?:${clean.map(escapeRe).join("|")})/([^/?#]+)`));
|
|
33
|
+
if (!m)
|
|
34
|
+
return null;
|
|
35
|
+
try {
|
|
36
|
+
return decodeURIComponent(m[1]);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return m[1];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=slug.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slug.js","sourceRoot":"","sources":["../../src/crawl/slug.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,4DAA4D;AAC5D,SAAS,QAAQ,CAAC,CAAS;IACzB,OAAO,CAAC,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AAClD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,GAAW,EAAE,QAAkB;IACxD,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,oFAAoF;IACpF,IAAI,IAAI,CAAC,UAAU,CAAC,eAAe,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC;QAAE,OAAO,IAAI,CAAC;IACnF,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACvC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACpC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,QAAQ,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC;IACrF,IAAI,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACpB,IAAI,CAAC;QACH,OAAO,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,CAAC,CAAC,CAAE,CAAC;IACf,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* crawl/types.ts — the ports for the open-source publisher crawler.
|
|
3
|
+
*
|
|
4
|
+
* `naulon crawl` is a ONE-SHOT drafting aid: it reads a publisher's own origin
|
|
5
|
+
* (no API key) and drafts a `credits.json` so a self-hoster doesn't hand-author
|
|
6
|
+
* one article at a time. It is deliberately the *primitive* form of discovery —
|
|
7
|
+
* the hosted product's continuous, enriched, reviewed, multi-tenant pipeline is a
|
|
8
|
+
* separate thing and stays there. This file is the seam; concrete adapters live
|
|
9
|
+
* beside it and reach the network ONLY through the guarded `Fetcher`.
|
|
10
|
+
*
|
|
11
|
+
* The load-bearing invariant, identical to the hosted crawler: the crawler auto-
|
|
12
|
+
* configs the CATALOG plane only (slugs, titles, author strings). MONEY IS NEVER
|
|
13
|
+
* INFERRED — a `DiscoveredArticle` carries the feed's author STRING, never a wallet.
|
|
14
|
+
* The human-supplied `authorWalletMap` / `defaultWallet` map that string to a payTo;
|
|
15
|
+
* an unmapped author is reported to the operator, never written to a guessed address.
|
|
16
|
+
*
|
|
17
|
+
* On-origin, no-secret adapters ONLY (`rss`, `sitemap`, `wordpress`). Keyed adapters
|
|
18
|
+
* (Ghost/Blogger), LLM enrichment, and a review queue are the operated pipeline — not here.
|
|
19
|
+
*/
|
|
20
|
+
/** The minimal response an adapter consumes — a subset of the DOM `Response`, so the real
|
|
21
|
+
* node-http path and a plain test fake both satisfy it. */
|
|
22
|
+
export interface FetchResult {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
status: number;
|
|
25
|
+
text(): Promise<string>;
|
|
26
|
+
json(): Promise<unknown>;
|
|
27
|
+
}
|
|
28
|
+
/** The ONLY way an adapter reaches the network. The crawl orchestrator injects an impl that
|
|
29
|
+
* enforces verified-origin-only + SSRF CIDR block + anti-DNS-rebind (`makeGuardedFetcher`).
|
|
30
|
+
* Adapters never import `fetch`/`node:http` — that keeps the SSRF guard un-bypassable and the
|
|
31
|
+
* whole module network-testable with a plain fake. */
|
|
32
|
+
export interface Fetcher {
|
|
33
|
+
(url: string, init?: {
|
|
34
|
+
headers?: Record<string, string>;
|
|
35
|
+
}): Promise<FetchResult>;
|
|
36
|
+
}
|
|
37
|
+
/** One author exactly as the feed/API states it. `name` is the raw string used as the
|
|
38
|
+
* `authorWalletMap` key — catalog data, NEVER a wallet (money is never inferred). */
|
|
39
|
+
export interface DiscoveredAuthor {
|
|
40
|
+
name: string;
|
|
41
|
+
/** Platform-native id when exposed (WP author id/slug) — aids stable mapping. */
|
|
42
|
+
externalId?: string;
|
|
43
|
+
}
|
|
44
|
+
/** One article an adapter found on the verified origin. Catalog plane only — no money here. */
|
|
45
|
+
export interface DiscoveredArticle {
|
|
46
|
+
/** The gate's credits key — the URL path slug, derived per `articlePrefixes`. */
|
|
47
|
+
slug: string;
|
|
48
|
+
/** Canonical article URL on the verified origin. */
|
|
49
|
+
url: string;
|
|
50
|
+
title: string;
|
|
51
|
+
/** Feed-stated authors (may be empty — then resolution falls to `defaultWallet`). */
|
|
52
|
+
authors: DiscoveredAuthor[];
|
|
53
|
+
/** ISO-8601 publish timestamp the source states, when present. */
|
|
54
|
+
publishedAt?: string;
|
|
55
|
+
}
|
|
56
|
+
/** The human-authored crawl policy the adapters read. A subset of the hosted `CrawlConfig`:
|
|
57
|
+
* no API keys (keyed adapters are hosted-only), no continuous-sweep knobs. */
|
|
58
|
+
export interface CrawlConfig {
|
|
59
|
+
/** Explicit feed/sitemap URL, overriding the conventional-path probe. */
|
|
60
|
+
feedUrl?: string;
|
|
61
|
+
/** URL-path globs deciding which discovered URLs are articles (sitemap adapter). */
|
|
62
|
+
includeGlobs: string[];
|
|
63
|
+
excludeGlobs: string[];
|
|
64
|
+
/** author STRING → payTo, human-supplied. Never inferred. */
|
|
65
|
+
authorWalletMap: Record<string, string>;
|
|
66
|
+
/** Fallback payTo when an author is unmapped or the source stated none. Optional —
|
|
67
|
+
* absent means an unmapped article is reported, not written. */
|
|
68
|
+
defaultWallet?: string;
|
|
69
|
+
}
|
|
70
|
+
/** Everything an adapter may touch: the verified origin, the human policy, the guarded fetch.
|
|
71
|
+
* An adapter that reaches outside `ctx.fetch` breaks the SSRF guarantee. */
|
|
72
|
+
export interface AdapterContext {
|
|
73
|
+
/** The verified origin `scheme://host[:port]` — the ONLY host `ctx.fetch` connects to. */
|
|
74
|
+
origin: string;
|
|
75
|
+
/** Gateable path prefixes (no leading slash). A slug MUST be derived from these (via
|
|
76
|
+
* `deriveSlug`) so it matches the gate's credits key exactly. */
|
|
77
|
+
articlePrefixes: string[];
|
|
78
|
+
config: CrawlConfig;
|
|
79
|
+
fetch: Fetcher;
|
|
80
|
+
}
|
|
81
|
+
/** The public adapter ids — on-origin, no-secret sources only. */
|
|
82
|
+
export type SourceAdapterId = "rss" | "sitemap" | "wordpress";
|
|
83
|
+
/** A source connector: one file per platform. The registry picks the richest adapter whose
|
|
84
|
+
* `detect` returns true; `rss`/`sitemap` are the always-available fallbacks (lowest rank). */
|
|
85
|
+
export interface SourceAdapter {
|
|
86
|
+
readonly id: SourceAdapterId;
|
|
87
|
+
/** Richness rank — the registry prefers the highest detected. Real author objects (WordPress)
|
|
88
|
+
* outrank feed parsing (rss), which outranks pure URL discovery (sitemap). */
|
|
89
|
+
readonly rank: number;
|
|
90
|
+
/** Cheap probe: could this adapter discover THIS origin? MUST use only `ctx.fetch`. Never
|
|
91
|
+
* throws on a normal "no" — returns false. */
|
|
92
|
+
detect(ctx: AdapterContext): Promise<boolean>;
|
|
93
|
+
/** Discover the catalog. MUST hit only the verified origin via `ctx.fetch`. */
|
|
94
|
+
discover(ctx: AdapterContext): Promise<DiscoveredArticle[]>;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/crawl/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH;4DAC4D;AAC5D,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACxB,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1B;AAED;;;uDAGuD;AACvD,MAAM,WAAW,OAAO;IACtB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CAClF;AAID;sFACsF;AACtF,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,+FAA+F;AAC/F,MAAM,WAAW,iBAAiB;IAChC,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;IACb,oDAAoD;IACpD,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,qFAAqF;IACrF,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;+EAC+E;AAC/E,MAAM,WAAW,WAAW;IAC1B,yEAAyE;IACzE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,6DAA6D;IAC7D,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC;qEACiE;IACjE,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;6EAC6E;AAC7E,MAAM,WAAW,cAAc;IAC7B,0FAA0F;IAC1F,MAAM,EAAE,MAAM,CAAC;IACf;sEACkE;IAClE,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,kEAAkE;AAClE,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,SAAS,GAAG,WAAW,CAAC;AAE9D;+FAC+F;AAC/F,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,EAAE,eAAe,CAAC;IAC7B;mFAC+E;IAC/E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;mDAC+C;IAC/C,MAAM,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9C,+EAA+E;IAC/E,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAC;CAC7D"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* crawl/types.ts — the ports for the open-source publisher crawler.
|
|
3
|
+
*
|
|
4
|
+
* `naulon crawl` is a ONE-SHOT drafting aid: it reads a publisher's own origin
|
|
5
|
+
* (no API key) and drafts a `credits.json` so a self-hoster doesn't hand-author
|
|
6
|
+
* one article at a time. It is deliberately the *primitive* form of discovery —
|
|
7
|
+
* the hosted product's continuous, enriched, reviewed, multi-tenant pipeline is a
|
|
8
|
+
* separate thing and stays there. This file is the seam; concrete adapters live
|
|
9
|
+
* beside it and reach the network ONLY through the guarded `Fetcher`.
|
|
10
|
+
*
|
|
11
|
+
* The load-bearing invariant, identical to the hosted crawler: the crawler auto-
|
|
12
|
+
* configs the CATALOG plane only (slugs, titles, author strings). MONEY IS NEVER
|
|
13
|
+
* INFERRED — a `DiscoveredArticle` carries the feed's author STRING, never a wallet.
|
|
14
|
+
* The human-supplied `authorWalletMap` / `defaultWallet` map that string to a payTo;
|
|
15
|
+
* an unmapped author is reported to the operator, never written to a guessed address.
|
|
16
|
+
*
|
|
17
|
+
* On-origin, no-secret adapters ONLY (`rss`, `sitemap`, `wordpress`). Keyed adapters
|
|
18
|
+
* (Ghost/Blogger), LLM enrichment, and a review queue are the operated pipeline — not here.
|
|
19
|
+
*/
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/crawl/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Parse an XML document. Throws on malformed input (an adapter treats a throw as "no feed"). */
|
|
2
|
+
export declare function parseXml(xml: string): Record<string, unknown>;
|
|
3
|
+
/** fast-xml-parser yields a single object for one child and an array for many. Normalize to an
|
|
4
|
+
* array so adapters iterate uniformly (`<item>`×1 and ×N look the same). */
|
|
5
|
+
export declare function toArray<T>(x: T | T[] | undefined | null): T[];
|
|
6
|
+
/** Read a node's text whether it is a bare string, a `{ "#text" }` object (mixed content), or a
|
|
7
|
+
* CDATA `{ __cdata }`. Returns "" for anything else. */
|
|
8
|
+
export declare function textOf(node: unknown): string;
|
|
9
|
+
//# sourceMappingURL=xml.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xml.d.ts","sourceRoot":"","sources":["../../src/crawl/xml.ts"],"names":[],"mappings":"AAoBA,iGAAiG;AACjG,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAE7D;AAED;6EAC6E;AAC7E,wBAAgB,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,GAAG,SAAS,GAAG,IAAI,GAAG,CAAC,EAAE,CAG7D;AAED;yDACyD;AACzD,wBAAgB,MAAM,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAQ5C"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* crawl/xml.ts — the one XML parse wrapper (fast-xml-parser).
|
|
3
|
+
*
|
|
4
|
+
* RSS/Atom/sitemap are all XML; this is the only place the parser is configured, so every
|
|
5
|
+
* adapter reads feeds the same way. Text values stay strings (a slug like `2026` must not
|
|
6
|
+
* become the number 2026), attributes surface under `@_` so `<link href=…>` (Atom) is
|
|
7
|
+
* reachable, and namespaced tags (`dc:creator`) keep their prefix.
|
|
8
|
+
*/
|
|
9
|
+
import { XMLParser } from "fast-xml-parser";
|
|
10
|
+
const parser = new XMLParser({
|
|
11
|
+
ignoreAttributes: false,
|
|
12
|
+
attributeNamePrefix: "@_",
|
|
13
|
+
parseTagValue: false, // keep `<guid>2026</guid>` a string, not the number 2026
|
|
14
|
+
parseAttributeValue: false,
|
|
15
|
+
trimValues: true,
|
|
16
|
+
cdataPropName: "__cdata", // CDATA (common in RSS) folds into the tag's text value
|
|
17
|
+
processEntities: true,
|
|
18
|
+
});
|
|
19
|
+
/** Parse an XML document. Throws on malformed input (an adapter treats a throw as "no feed"). */
|
|
20
|
+
export function parseXml(xml) {
|
|
21
|
+
return parser.parse(xml);
|
|
22
|
+
}
|
|
23
|
+
/** fast-xml-parser yields a single object for one child and an array for many. Normalize to an
|
|
24
|
+
* array so adapters iterate uniformly (`<item>`×1 and ×N look the same). */
|
|
25
|
+
export function toArray(x) {
|
|
26
|
+
if (x === undefined || x === null)
|
|
27
|
+
return [];
|
|
28
|
+
return Array.isArray(x) ? x : [x];
|
|
29
|
+
}
|
|
30
|
+
/** Read a node's text whether it is a bare string, a `{ "#text" }` object (mixed content), or a
|
|
31
|
+
* CDATA `{ __cdata }`. Returns "" for anything else. */
|
|
32
|
+
export function textOf(node) {
|
|
33
|
+
if (typeof node === "string")
|
|
34
|
+
return node;
|
|
35
|
+
if (node && typeof node === "object") {
|
|
36
|
+
const o = node;
|
|
37
|
+
if (typeof o["__cdata"] === "string")
|
|
38
|
+
return o["__cdata"];
|
|
39
|
+
if (typeof o["#text"] === "string")
|
|
40
|
+
return o["#text"];
|
|
41
|
+
}
|
|
42
|
+
return "";
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=xml.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xml.js","sourceRoot":"","sources":["../../src/crawl/xml.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;IAC3B,gBAAgB,EAAE,KAAK;IACvB,mBAAmB,EAAE,IAAI;IACzB,aAAa,EAAE,KAAK,EAAE,yDAAyD;IAC/E,mBAAmB,EAAE,KAAK;IAC1B,UAAU,EAAE,IAAI;IAChB,aAAa,EAAE,SAAS,EAAE,wDAAwD;IAClF,eAAe,EAAE,IAAI;CACtB,CAAC,CAAC;AAEH,iGAAiG;AACjG,MAAM,UAAU,QAAQ,CAAC,GAAW;IAClC,OAAO,MAAM,CAAC,KAAK,CAAC,GAAG,CAA4B,CAAC;AACtD,CAAC;AAED;6EAC6E;AAC7E,MAAM,UAAU,OAAO,CAAI,CAA6B;IACtD,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC;IAC7C,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACpC,CAAC;AAED;yDACyD;AACzD,MAAM,UAAU,MAAM,CAAC,IAAa;IAClC,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC1C,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QACrC,MAAM,CAAC,GAAG,IAA+B,CAAC;QAC1C,IAAI,OAAO,CAAC,CAAC,SAAS,CAAC,KAAK,QAAQ;YAAE,OAAO,CAAC,CAAC,SAAS,CAAC,CAAC;QAC1D,IAAI,OAAO,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ;YAAE,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC;IACxD,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC"}
|