@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,119 @@
|
|
|
1
|
+
import { parseXml, toArray, textOf } from "../xml.js";
|
|
2
|
+
import { deriveSlug } from "../slug.js";
|
|
3
|
+
/** Conventional feed paths, richest first. Probed only when no explicit `feedUrl` is set.
|
|
4
|
+
* `/feed/` (trailing slash) is WordPress's canonical feed — `/feed` 301-redirects to it, and the
|
|
5
|
+
* guarded fetcher deliberately does NOT chase redirects (off-origin redirect chasing is blocked),
|
|
6
|
+
* so without the slash variant every WordPress site would be missed by the RSS fallback. */
|
|
7
|
+
const CONVENTIONAL_PATHS = ["/feed", "/feed/", "/rss", "/rss.xml", "/feed.xml", "/atom.xml", "/index.xml", "/feeds/posts/default"];
|
|
8
|
+
async function fetchFeedXml(ctx) {
|
|
9
|
+
const candidates = ctx.config.feedUrl
|
|
10
|
+
? [ctx.config.feedUrl]
|
|
11
|
+
: CONVENTIONAL_PATHS.map((p) => new URL(p, ctx.origin).toString());
|
|
12
|
+
for (const url of candidates) {
|
|
13
|
+
try {
|
|
14
|
+
const res = await ctx.fetch(url);
|
|
15
|
+
if (!res.ok)
|
|
16
|
+
continue;
|
|
17
|
+
const body = await res.text();
|
|
18
|
+
if (/<(rss|feed)[\s>]/i.test(body))
|
|
19
|
+
return body; // looks like RSS or Atom
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// off-origin override, network error — try the next candidate.
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
/** Pull the canonical link out of an Atom entry (`rel="alternate"` or the first link). */
|
|
28
|
+
function atomLink(entry) {
|
|
29
|
+
for (const l of toArray(entry["link"])) {
|
|
30
|
+
if (l && typeof l === "object") {
|
|
31
|
+
const o = l;
|
|
32
|
+
const rel = o["@_rel"];
|
|
33
|
+
if (rel === undefined || rel === "alternate")
|
|
34
|
+
return String(o["@_href"] ?? "");
|
|
35
|
+
}
|
|
36
|
+
else if (typeof l === "string") {
|
|
37
|
+
return l;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
function authorsFromRssItem(item) {
|
|
43
|
+
const names = new Set();
|
|
44
|
+
for (const raw of [...toArray(item["dc:creator"]), ...toArray(item["author"])]) {
|
|
45
|
+
const t = textOf(raw).trim();
|
|
46
|
+
if (!t)
|
|
47
|
+
continue;
|
|
48
|
+
const m = t.match(/\(([^)]+)\)\s*$/); // "a@b.com (Jane Roe)" → "Jane Roe"
|
|
49
|
+
names.add(m ? m[1].trim() : t);
|
|
50
|
+
}
|
|
51
|
+
return [...names].map((name) => ({ name }));
|
|
52
|
+
}
|
|
53
|
+
function authorsFromAtomEntry(entry) {
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const a of toArray(entry["author"])) {
|
|
56
|
+
if (a && typeof a === "object") {
|
|
57
|
+
const name = textOf(a["name"]).trim();
|
|
58
|
+
if (name)
|
|
59
|
+
out.push({ name });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
/** Normalize a feed date to ISO-8601, or undefined when unparseable (never throw). */
|
|
65
|
+
function normDate(raw) {
|
|
66
|
+
const t = raw.trim();
|
|
67
|
+
if (!t)
|
|
68
|
+
return undefined;
|
|
69
|
+
const ms = Date.parse(t);
|
|
70
|
+
return Number.isFinite(ms) ? new Date(ms).toISOString() : undefined;
|
|
71
|
+
}
|
|
72
|
+
function discover(ctx, xml) {
|
|
73
|
+
const doc = parseXml(xml);
|
|
74
|
+
const out = [];
|
|
75
|
+
// RSS 2.0: rss > channel > item
|
|
76
|
+
const channel = doc["rss"]?.["channel"];
|
|
77
|
+
for (const item of toArray(channel?.["item"])) {
|
|
78
|
+
const it = item;
|
|
79
|
+
const url = textOf(it["link"]).trim();
|
|
80
|
+
const slug = url ? deriveSlug(url, ctx.articlePrefixes) : null;
|
|
81
|
+
if (!url || !slug)
|
|
82
|
+
continue;
|
|
83
|
+
out.push({
|
|
84
|
+
slug,
|
|
85
|
+
url,
|
|
86
|
+
title: textOf(it["title"]).trim(),
|
|
87
|
+
authors: authorsFromRssItem(it),
|
|
88
|
+
publishedAt: normDate(textOf(it["pubDate"]) || textOf(it["dc:date"])),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
// Atom: feed > entry
|
|
92
|
+
for (const entry of toArray(doc["feed"]?.["entry"])) {
|
|
93
|
+
const e = entry;
|
|
94
|
+
const url = atomLink(e).trim();
|
|
95
|
+
const slug = url ? deriveSlug(url, ctx.articlePrefixes) : null;
|
|
96
|
+
if (!url || !slug)
|
|
97
|
+
continue;
|
|
98
|
+
out.push({
|
|
99
|
+
slug,
|
|
100
|
+
url,
|
|
101
|
+
title: textOf(e["title"]).trim(),
|
|
102
|
+
authors: authorsFromAtomEntry(e),
|
|
103
|
+
publishedAt: normDate(textOf(e["published"]) || textOf(e["updated"])),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
export const rssAdapter = {
|
|
109
|
+
id: "rss",
|
|
110
|
+
rank: 10, // fallback — any platform-native adapter outranks it
|
|
111
|
+
async detect(ctx) {
|
|
112
|
+
return (await fetchFeedXml(ctx)) !== null;
|
|
113
|
+
},
|
|
114
|
+
async discover(ctx) {
|
|
115
|
+
const xml = await fetchFeedXml(ctx);
|
|
116
|
+
return xml ? discover(ctx, xml) : [];
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
//# sourceMappingURL=rss.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rss.js","sourceRoot":"","sources":["../../../src/crawl/adapters/rss.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* crawl/adapters/sitemap.ts — the sitemap.xml URL-discovery adapter.
|
|
3
|
+
*
|
|
4
|
+
* The other always-available fallback. A sitemap is a flat list of URLs (and an INDEX is a
|
|
5
|
+
* list of sitemaps); it carries no author or title, so this adapter yields URL + slug +
|
|
6
|
+
* lastmod only — author resolution then falls to `defaultWallet`. Include/exclude globs do
|
|
7
|
+
* the article filtering, since a sitemap lists every URL, not just articles. Index recursion
|
|
8
|
+
* is depth- and child-count-capped to bound a hostile or huge sitemap.
|
|
9
|
+
*/
|
|
10
|
+
import type { SourceAdapter } from "../types.ts";
|
|
11
|
+
export declare const sitemapAdapter: SourceAdapter;
|
|
12
|
+
//# sourceMappingURL=sitemap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sitemap.d.ts","sourceRoot":"","sources":["../../../src/crawl/adapters/sitemap.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAqC,aAAa,EAAE,MAAM,aAAa,CAAC;AA4FpF,eAAO,MAAM,cAAc,EAAE,aAW5B,CAAC"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { parseXml, toArray, textOf } from "../xml.js";
|
|
2
|
+
import { deriveSlug } from "../slug.js";
|
|
3
|
+
import { passesGlobs } from "../glob.js";
|
|
4
|
+
const CONVENTIONAL_PATHS = ["/sitemap.xml", "/sitemap_index.xml", "/sitemap-index.xml"];
|
|
5
|
+
const MAX_INDEX_DEPTH = 2; // index → child sitemaps → (no deeper)
|
|
6
|
+
const MAX_CHILD_SITEMAPS = 50;
|
|
7
|
+
async function fetchSitemapXml(ctx, url) {
|
|
8
|
+
try {
|
|
9
|
+
const res = await ctx.fetch(url);
|
|
10
|
+
if (!res.ok)
|
|
11
|
+
return null;
|
|
12
|
+
const body = await res.text();
|
|
13
|
+
return /<(urlset|sitemapindex)[\s>]/i.test(body) ? body : null;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Resolve the root sitemap: explicit `feedUrl` if it points at a sitemap, else conventional. */
|
|
20
|
+
async function rootSitemap(ctx) {
|
|
21
|
+
const candidates = ctx.config.feedUrl?.includes("sitemap")
|
|
22
|
+
? [ctx.config.feedUrl]
|
|
23
|
+
: CONVENTIONAL_PATHS.map((p) => new URL(p, ctx.origin).toString());
|
|
24
|
+
for (const url of candidates) {
|
|
25
|
+
const xml = await fetchSitemapXml(ctx, url);
|
|
26
|
+
if (xml)
|
|
27
|
+
return xml;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
/** Collect `<loc>`s from a sitemap or (recursively, capped) a sitemap index. */
|
|
32
|
+
async function collectUrls(ctx, xml, depth) {
|
|
33
|
+
const doc = parseXml(xml);
|
|
34
|
+
const index = doc["sitemapindex"];
|
|
35
|
+
if (index) {
|
|
36
|
+
if (depth >= MAX_INDEX_DEPTH)
|
|
37
|
+
return [];
|
|
38
|
+
const out = [];
|
|
39
|
+
let fetched = 0;
|
|
40
|
+
for (const sm of toArray(index["sitemap"])) {
|
|
41
|
+
if (fetched >= MAX_CHILD_SITEMAPS)
|
|
42
|
+
break;
|
|
43
|
+
const loc = textOf(sm["loc"]).trim();
|
|
44
|
+
if (!loc)
|
|
45
|
+
continue;
|
|
46
|
+
fetched++;
|
|
47
|
+
const childXml = await fetchSitemapXml(ctx, loc);
|
|
48
|
+
if (childXml)
|
|
49
|
+
out.push(...(await collectUrls(ctx, childXml, depth + 1)));
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
const urlset = doc["urlset"];
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const u of toArray(urlset?.["url"])) {
|
|
56
|
+
const o = u;
|
|
57
|
+
const loc = textOf(o["loc"]).trim();
|
|
58
|
+
if (loc)
|
|
59
|
+
out.push({ loc, lastmod: textOf(o["lastmod"]).trim() || undefined });
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
function toArticles(ctx, urls) {
|
|
64
|
+
const out = [];
|
|
65
|
+
const seen = new Set();
|
|
66
|
+
for (const { loc, lastmod } of urls) {
|
|
67
|
+
let pathname;
|
|
68
|
+
try {
|
|
69
|
+
pathname = new URL(loc).pathname;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (!passesGlobs(pathname, ctx.config.includeGlobs, ctx.config.excludeGlobs))
|
|
75
|
+
continue;
|
|
76
|
+
const slug = deriveSlug(loc, ctx.articlePrefixes);
|
|
77
|
+
if (!slug || seen.has(slug))
|
|
78
|
+
continue;
|
|
79
|
+
seen.add(slug);
|
|
80
|
+
out.push({
|
|
81
|
+
slug,
|
|
82
|
+
url: loc,
|
|
83
|
+
title: "", // sitemaps carry no title — the merge falls back to the slug
|
|
84
|
+
authors: [], // no author signal → resolution falls to defaultWallet
|
|
85
|
+
publishedAt: lastmod && Number.isFinite(Date.parse(lastmod)) ? new Date(lastmod).toISOString() : undefined,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
export const sitemapAdapter = {
|
|
91
|
+
id: "sitemap",
|
|
92
|
+
rank: 5, // lowest — no author/title, pure URL discovery
|
|
93
|
+
async detect(ctx) {
|
|
94
|
+
return (await rootSitemap(ctx)) !== null;
|
|
95
|
+
},
|
|
96
|
+
async discover(ctx) {
|
|
97
|
+
const xml = await rootSitemap(ctx);
|
|
98
|
+
if (!xml)
|
|
99
|
+
return [];
|
|
100
|
+
return toArticles(ctx, await collectUrls(ctx, xml, 0));
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
//# sourceMappingURL=sitemap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sitemap.js","sourceRoot":"","sources":["../../../src/crawl/adapters/sitemap.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* crawl/adapters/wordpress.ts — the WordPress REST adapter, highest-value.
|
|
3
|
+
*
|
|
4
|
+
* `GET {origin}/wp-json/wp/v2/posts?_embed` returns real post objects with REAL author objects
|
|
5
|
+
* (`_embedded.author[].name`) — far richer than a feed's free-text `<author>`. Self-hosted
|
|
6
|
+
* WordPress serves this from the publisher's own origin, so the guarded origin fetcher covers
|
|
7
|
+
* it (no off-origin call, no API key). Paginated and page-capped to bound a large catalog.
|
|
8
|
+
*
|
|
9
|
+
* `_embed=1` is the rich mode but also the heavy one: on a big media site it inflates each post
|
|
10
|
+
* with its full author/media/term objects, so a page can dwarf a bare listing (TechCrunch's
|
|
11
|
+
* `_embed` page times out where the bare page is ~1.4 MiB / 2 s). So each page tries `_embed`
|
|
12
|
+
* first, and on a fetch failure (timeout, or a body past the fetcher's 8 MiB cap that won't
|
|
13
|
+
* parse) falls back to the BARE listing for that page — you still capture the full catalog
|
|
14
|
+
* (slug/title/date), only the author NAMES are lost, so resolution falls to `defaultWallet`
|
|
15
|
+
* (money is never inferred). A partial-but-complete catalog beats failing on the biggest sites.
|
|
16
|
+
*/
|
|
17
|
+
import type { SourceAdapter } from "../types.ts";
|
|
18
|
+
export declare const wordpressAdapter: SourceAdapter;
|
|
19
|
+
//# sourceMappingURL=wordpress.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wordpress.d.ts","sourceRoot":"","sources":["../../../src/crawl/adapters/wordpress.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAuD,aAAa,EAAE,MAAM,aAAa,CAAC;AA0CtG,eAAO,MAAM,gBAAgB,EAAE,aAyD9B,CAAC"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { deriveSlug } from "../slug.js";
|
|
2
|
+
const PER_PAGE = 50;
|
|
3
|
+
const MAX_PAGES = 40; // ≤ 2000 posts/crawl
|
|
4
|
+
function authorsOf(post) {
|
|
5
|
+
const out = [];
|
|
6
|
+
for (const a of post._embedded?.author ?? []) {
|
|
7
|
+
const name = (a?.name ?? "").trim();
|
|
8
|
+
if (name)
|
|
9
|
+
out.push({ name, externalId: a?.id !== undefined ? String(a.id) : a?.slug });
|
|
10
|
+
}
|
|
11
|
+
return out;
|
|
12
|
+
}
|
|
13
|
+
async function fetchPostsPage(ctx, page, embed) {
|
|
14
|
+
const url = new URL(`/wp-json/wp/v2/posts?${embed ? "_embed=1&" : ""}per_page=${PER_PAGE}&page=${page}&status=publish`, ctx.origin).toString();
|
|
15
|
+
try {
|
|
16
|
+
const res = await ctx.fetch(url);
|
|
17
|
+
if (!res.ok)
|
|
18
|
+
return "stop"; // WP returns 400 past the last page → stop
|
|
19
|
+
const body = await res.json();
|
|
20
|
+
if (!Array.isArray(body))
|
|
21
|
+
return "stop";
|
|
22
|
+
return body;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return "error";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export const wordpressAdapter = {
|
|
29
|
+
id: "wordpress",
|
|
30
|
+
rank: 100, // real author objects → outranks feeds
|
|
31
|
+
async detect(ctx) {
|
|
32
|
+
try {
|
|
33
|
+
const res = await ctx.fetch(new URL("/wp-json/wp/v2/posts?per_page=1", ctx.origin).toString());
|
|
34
|
+
if (!res.ok)
|
|
35
|
+
return false;
|
|
36
|
+
return Array.isArray(await res.json());
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
async discover(ctx) {
|
|
43
|
+
const out = [];
|
|
44
|
+
// Try `_embed` (author names) until it fails once; a site that times out an `_embed` page
|
|
45
|
+
// times out every one, so latch to bare after the first failure rather than eating the
|
|
46
|
+
// fetcher timeout on all MAX_PAGES pages (that turned a big embed-hostile site into minutes).
|
|
47
|
+
let useEmbed = true;
|
|
48
|
+
for (let page = 1; page <= MAX_PAGES; page++) {
|
|
49
|
+
let posts = await fetchPostsPage(ctx, page, useEmbed);
|
|
50
|
+
if (posts === "error" && useEmbed) {
|
|
51
|
+
// The `_embed` page was too heavy (timeout / oversized body). Drop to BARE for this page
|
|
52
|
+
// and every page after — the catalog survives; only author names are lost (→ resolves to
|
|
53
|
+
// `defaultWallet`, never inferred).
|
|
54
|
+
useEmbed = false;
|
|
55
|
+
posts = await fetchPostsPage(ctx, page, false);
|
|
56
|
+
}
|
|
57
|
+
if (posts === "error") {
|
|
58
|
+
// Even the bare listing failed. `detect` already proved page 1 serves posts, so a
|
|
59
|
+
// page-1 failure is a real fault — surface it loudly, don't draft an empty credits.json.
|
|
60
|
+
// A later-page failure keeps what earlier pages yielded (a partial catalog beats none).
|
|
61
|
+
if (page === 1) {
|
|
62
|
+
throw new Error(`WordPress REST discovery failed on the first page (${new URL("/wp-json/wp/v2/posts", ctx.origin).toString()}) — even without _embed. The origin may be timing out or blocking the crawler.`);
|
|
63
|
+
}
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
if (posts === "stop" || posts.length === 0)
|
|
67
|
+
break;
|
|
68
|
+
for (const post of posts) {
|
|
69
|
+
const link = (post.link ?? "").trim();
|
|
70
|
+
const slug = link ? deriveSlug(link, ctx.articlePrefixes) : null;
|
|
71
|
+
if (!link || !slug)
|
|
72
|
+
continue;
|
|
73
|
+
const date = (post.date_gmt ?? "").trim();
|
|
74
|
+
out.push({
|
|
75
|
+
slug,
|
|
76
|
+
url: link,
|
|
77
|
+
title: (post.title?.rendered ?? "").trim(),
|
|
78
|
+
authors: authorsOf(post), // bare pages have no `_embedded` → [] → resolves to defaultWallet
|
|
79
|
+
// WP date_gmt has no zone suffix; it IS UTC → append Z before parsing.
|
|
80
|
+
publishedAt: date && Number.isFinite(Date.parse(`${date}Z`)) ? new Date(`${date}Z`).toISOString() : undefined,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (posts.length < PER_PAGE)
|
|
84
|
+
break; // last page
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
//# sourceMappingURL=wordpress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wordpress.js","sourceRoot":"","sources":["../../../src/crawl/adapters/wordpress.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* crawl/authors.ts — author → wallet resolution.
|
|
3
|
+
*
|
|
4
|
+
* The single point where the catalog plane (a feed author STRING) meets the money plane (a
|
|
5
|
+
* payTo). It is DETERMINISTIC and never invents an address — the load-bearing custody-free
|
|
6
|
+
* invariant. Resolution order, exactly:
|
|
7
|
+
* 1. the primary author's feed string → `authorWalletMap[name]` (human-supplied)
|
|
8
|
+
* 2. else → `defaultWallet` (human-supplied fallback)
|
|
9
|
+
* 3. else → null → the article is UNMAPPED: reported to the operator, never written to a guess.
|
|
10
|
+
*
|
|
11
|
+
* A mapped value is re-checked against the EVM address shape: a malformed entry resolves as if
|
|
12
|
+
* absent, rather than tolling to a bad address.
|
|
13
|
+
*/
|
|
14
|
+
import type { CrawlConfig, DiscoveredArticle } from "./types.ts";
|
|
15
|
+
export interface ResolvedAuthor {
|
|
16
|
+
/** The primary author's feed string (the map key), or null when the source stated none. */
|
|
17
|
+
author: string | null;
|
|
18
|
+
/** The human-mapped payTo, or null when unmapped (→ reported, never auto-tolled). */
|
|
19
|
+
wallet: string | null;
|
|
20
|
+
/** True when no valid wallet resolved — the "leave it for the human" signal. */
|
|
21
|
+
unmapped: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** The shared EVM-address gate: returns the address if well-formed, else null. */
|
|
24
|
+
export declare function validWallet(addr: string | undefined): string | null;
|
|
25
|
+
/** Resolve the primary author of a discovered article to a payout wallet, deterministically. */
|
|
26
|
+
export declare function resolveAuthorWallet(article: DiscoveredArticle, config: CrawlConfig): ResolvedAuthor;
|
|
27
|
+
//# sourceMappingURL=authors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authors.d.ts","sourceRoot":"","sources":["../../src/crawl/authors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAIjE,MAAM,WAAW,cAAc;IAC7B,2FAA2F;IAC3F,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,qFAAqF;IACrF,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,gFAAgF;IAChF,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,kFAAkF;AAClF,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAEnE;AAED,gGAAgG;AAChG,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,iBAAiB,EAAE,MAAM,EAAE,WAAW,GAAG,cAAc,CAKnG"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const EVM_ADDRESS = /^0x[0-9a-fA-F]{40}$/;
|
|
2
|
+
/** The shared EVM-address gate: returns the address if well-formed, else null. */
|
|
3
|
+
export function validWallet(addr) {
|
|
4
|
+
return addr && EVM_ADDRESS.test(addr) ? addr : null;
|
|
5
|
+
}
|
|
6
|
+
/** Resolve the primary author of a discovered article to a payout wallet, deterministically. */
|
|
7
|
+
export function resolveAuthorWallet(article, config) {
|
|
8
|
+
const primary = article.authors[0]?.name?.trim() || null;
|
|
9
|
+
const mapped = primary ? validWallet(config.authorWalletMap[primary]) : null;
|
|
10
|
+
const wallet = mapped ?? validWallet(config.defaultWallet);
|
|
11
|
+
return { author: primary, wallet, unmapped: wallet === null };
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=authors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authors.js","sourceRoot":"","sources":["../../src/crawl/authors.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,GAAG,qBAAqB,CAAC;AAW1C,kFAAkF;AAClF,MAAM,UAAU,WAAW,CAAC,IAAwB;IAClD,OAAO,IAAI,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;AACtD,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,mBAAmB,CAAC,OAA0B,EAAE,MAAmB;IACjF,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,IAAI,CAAC;IACzD,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC7E,MAAM,MAAM,GAAG,MAAM,IAAI,WAAW,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;IAC3D,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,EAAE,CAAC;AAChE,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* crawl/crawl.ts — the pure crawl orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Ties the pieces together with NO I/O: pick an adapter (forced or auto-detected), discover
|
|
5
|
+
* the catalog through the injected guarded fetch, and merge into the existing credits map
|
|
6
|
+
* (insert-only, custody-free). The CLI shell (`cli/crawl.ts`) supplies the real guarded
|
|
7
|
+
* fetcher, reads/writes `credits.json`, and prints the summary — this stays testable with a
|
|
8
|
+
* plain fake fetch, the same split as `buildInitPlan` vs the `init` CLI.
|
|
9
|
+
*/
|
|
10
|
+
import type { ArticleCredits } from "../contract/credits.ts";
|
|
11
|
+
import { type MergeResult } from "./credits-file.ts";
|
|
12
|
+
import type { CrawlConfig, Fetcher, SourceAdapterId } from "./types.ts";
|
|
13
|
+
export interface CrawlOptions {
|
|
14
|
+
/** The verified origin `scheme://host[:port]`. */
|
|
15
|
+
origin: string;
|
|
16
|
+
/** Gateable path prefixes (no leading slash) — the slug key derivation. */
|
|
17
|
+
articlePrefixes: string[];
|
|
18
|
+
/** The human-authored crawl policy (wallet map, default wallet, globs, feed override). */
|
|
19
|
+
config: CrawlConfig;
|
|
20
|
+
/** The current `credits.json` map (empty `{}` for a first crawl). */
|
|
21
|
+
existing: Record<string, ArticleCredits>;
|
|
22
|
+
/** The guarded fetcher (origin-bound + SSRF-blocked). Injected so tests use a fake. */
|
|
23
|
+
fetch: Fetcher;
|
|
24
|
+
/** Force a specific adapter instead of auto-detecting. */
|
|
25
|
+
forceAdapterId?: SourceAdapterId;
|
|
26
|
+
}
|
|
27
|
+
export interface CrawlResult extends MergeResult {
|
|
28
|
+
/** The adapter that ran, or null when none detected. */
|
|
29
|
+
adapterId: SourceAdapterId | null;
|
|
30
|
+
/** How many articles the adapter discovered (before the merge's custody-free/insert filters). */
|
|
31
|
+
discovered: number;
|
|
32
|
+
}
|
|
33
|
+
/** Run one crawl pass. Never mutates `existing`; returns the merged map + a summary. */
|
|
34
|
+
export declare function runCrawl(opts: CrawlOptions): Promise<CrawlResult>;
|
|
35
|
+
//# sourceMappingURL=crawl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crawl.d.ts","sourceRoot":"","sources":["../../src/crawl/crawl.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAgB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEnE,OAAO,KAAK,EAAkB,WAAW,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAExF,MAAM,WAAW,YAAY;IAC3B,kDAAkD;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,0FAA0F;IAC1F,MAAM,EAAE,WAAW,CAAC;IACpB,qEAAqE;IACrE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACzC,uFAAuF;IACvF,KAAK,EAAE,OAAO,CAAC;IACf,0DAA0D;IAC1D,cAAc,CAAC,EAAE,eAAe,CAAC;CAClC;AAED,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC9C,wDAAwD;IACxD,SAAS,EAAE,eAAe,GAAG,IAAI,CAAC;IAClC,iGAAiG;IACjG,UAAU,EAAE,MAAM,CAAC;CACpB;AAID,wFAAwF;AACxF,wBAAsB,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAuBvE"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { mergeCredits } from "./credits-file.js";
|
|
2
|
+
import { adapterById, selectAdapter } from "./registry.js";
|
|
3
|
+
const EMPTY = { added: [], keptExisting: [], unmapped: [] };
|
|
4
|
+
/** Run one crawl pass. Never mutates `existing`; returns the merged map + a summary. */
|
|
5
|
+
export async function runCrawl(opts) {
|
|
6
|
+
const ctx = {
|
|
7
|
+
origin: opts.origin,
|
|
8
|
+
articlePrefixes: opts.articlePrefixes,
|
|
9
|
+
config: opts.config,
|
|
10
|
+
fetch: opts.fetch,
|
|
11
|
+
};
|
|
12
|
+
let adapter;
|
|
13
|
+
if (opts.forceAdapterId) {
|
|
14
|
+
adapter = adapterById(opts.forceAdapterId);
|
|
15
|
+
if (!adapter)
|
|
16
|
+
throw new Error(`unknown adapter: ${opts.forceAdapterId}`);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
adapter = await selectAdapter(ctx);
|
|
20
|
+
}
|
|
21
|
+
if (!adapter) {
|
|
22
|
+
return { credits: { ...opts.existing }, ...EMPTY, adapterId: null, discovered: 0 };
|
|
23
|
+
}
|
|
24
|
+
const discovered = await adapter.discover(ctx);
|
|
25
|
+
const merged = mergeCredits(opts.existing, discovered, opts.config);
|
|
26
|
+
return { ...merged, adapterId: adapter.id, discovered: discovered.length };
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=crawl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crawl.js","sourceRoot":"","sources":["../../src/crawl/crawl.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,YAAY,EAAoB,MAAM,mBAAmB,CAAC;AACnE,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAyB3D,MAAM,KAAK,GAAiC,EAAE,KAAK,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;AAE1F,wFAAwF;AACxF,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAkB;IAC/C,MAAM,GAAG,GAAmB;QAC1B,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,eAAe,EAAE,IAAI,CAAC,eAAe;QACrC,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,KAAK,EAAE,IAAI,CAAC,KAAK;KAClB,CAAC;IAEF,IAAI,OAAO,CAAC;IACZ,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;QACxB,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QAC3C,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,oBAAoB,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC;IAC3E,CAAC;SAAM,CAAC;QACN,OAAO,GAAG,MAAM,aAAa,CAAC,GAAG,CAAC,CAAC;IACrC,CAAC;IAED,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,EAAE,OAAO,EAAE,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE,EAAE,GAAG,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC;IACrF,CAAC;IAED,MAAM,UAAU,GAAG,MAAM,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;IAC/C,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IACpE,OAAO,EAAE,GAAG,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,EAAE,UAAU,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC;AAC7E,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* crawl/credits-file.ts — merge discovered articles into a `credits.json` map.
|
|
3
|
+
*
|
|
4
|
+
* The credits file is a `{ [slug]: ArticleCredits }` map (the same shape `naulon init`
|
|
5
|
+
* writes and the gate reads via `CREDITS_FIXTURES`). Merging obeys two hard rules:
|
|
6
|
+
*
|
|
7
|
+
* 1. INSERT-ONLY BY SLUG. A slug already in the file is a human decision — a hand-edited
|
|
8
|
+
* title, a curated co-author split. A re-crawl NEVER clobbers it. Only genuinely-new
|
|
9
|
+
* slugs are added. (Mirrors the hosted crawler's `(tenant, slug)` insert-only stage.)
|
|
10
|
+
*
|
|
11
|
+
* 2. MONEY IS NEVER INFERRED. An article whose author resolves to no valid wallet
|
|
12
|
+
* (`authorWalletMap` miss + no `defaultWallet`) is NOT written — it is reported back
|
|
13
|
+
* so the operator can map the author, then re-crawl. A drafted entry always has a
|
|
14
|
+
* human-supplied wallet; the crawler never guesses an address.
|
|
15
|
+
*
|
|
16
|
+
* Every drafted entry is validated through the real `creditsSchema` before it lands, so the
|
|
17
|
+
* crawler can never emit a file the gate would reject at boot.
|
|
18
|
+
*/
|
|
19
|
+
import { type ArticleCredits } from "../contract/credits.ts";
|
|
20
|
+
import type { CrawlConfig, DiscoveredArticle } from "./types.ts";
|
|
21
|
+
/** An article the crawler found but could not draft, because no wallet mapped to its author. */
|
|
22
|
+
export interface UnmappedArticle {
|
|
23
|
+
slug: string;
|
|
24
|
+
/** The feed author string that had no mapping, or null when the source stated none. */
|
|
25
|
+
author: string | null;
|
|
26
|
+
}
|
|
27
|
+
export interface MergeResult {
|
|
28
|
+
/** The merged map — existing entries untouched, new mapped articles added. */
|
|
29
|
+
credits: Record<string, ArticleCredits>;
|
|
30
|
+
/** Slugs newly drafted this merge. */
|
|
31
|
+
added: string[];
|
|
32
|
+
/** Slugs skipped because they already existed (insert-only). */
|
|
33
|
+
keptExisting: string[];
|
|
34
|
+
/** Articles skipped because no wallet mapped (money never inferred) — the operator's to-do. */
|
|
35
|
+
unmapped: UnmappedArticle[];
|
|
36
|
+
}
|
|
37
|
+
/** Merge discovered articles into an existing credits map. Pure — no I/O. */
|
|
38
|
+
export declare function mergeCredits(existing: Record<string, ArticleCredits>, discovered: DiscoveredArticle[], config: CrawlConfig): MergeResult;
|
|
39
|
+
//# sourceMappingURL=credits-file.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credits-file.d.ts","sourceRoot":"","sources":["../../src/crawl/credits-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAG3E,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEjE,gGAAgG;AAChG,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,uFAAuF;IACvF,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,8EAA8E;IAC9E,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACxC,sCAAsC;IACtC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,gEAAgE;IAChE,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,+FAA+F;IAC/F,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC7B;AAED,6EAA6E;AAC7E,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,EACxC,UAAU,EAAE,iBAAiB,EAAE,EAC/B,MAAM,EAAE,WAAW,GAClB,WAAW,CA8Bb"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* crawl/credits-file.ts — merge discovered articles into a `credits.json` map.
|
|
3
|
+
*
|
|
4
|
+
* The credits file is a `{ [slug]: ArticleCredits }` map (the same shape `naulon init`
|
|
5
|
+
* writes and the gate reads via `CREDITS_FIXTURES`). Merging obeys two hard rules:
|
|
6
|
+
*
|
|
7
|
+
* 1. INSERT-ONLY BY SLUG. A slug already in the file is a human decision — a hand-edited
|
|
8
|
+
* title, a curated co-author split. A re-crawl NEVER clobbers it. Only genuinely-new
|
|
9
|
+
* slugs are added. (Mirrors the hosted crawler's `(tenant, slug)` insert-only stage.)
|
|
10
|
+
*
|
|
11
|
+
* 2. MONEY IS NEVER INFERRED. An article whose author resolves to no valid wallet
|
|
12
|
+
* (`authorWalletMap` miss + no `defaultWallet`) is NOT written — it is reported back
|
|
13
|
+
* so the operator can map the author, then re-crawl. A drafted entry always has a
|
|
14
|
+
* human-supplied wallet; the crawler never guesses an address.
|
|
15
|
+
*
|
|
16
|
+
* Every drafted entry is validated through the real `creditsSchema` before it lands, so the
|
|
17
|
+
* crawler can never emit a file the gate would reject at boot.
|
|
18
|
+
*/
|
|
19
|
+
import { parseCredits } from "../contract/credits.js";
|
|
20
|
+
import { walletAddress } from "../contract/wallet.js";
|
|
21
|
+
import { resolveAuthorWallet } from "./authors.js";
|
|
22
|
+
/** Merge discovered articles into an existing credits map. Pure — no I/O. */
|
|
23
|
+
export function mergeCredits(existing, discovered, config) {
|
|
24
|
+
const credits = { ...existing };
|
|
25
|
+
const added = [];
|
|
26
|
+
const keptExisting = [];
|
|
27
|
+
const unmapped = [];
|
|
28
|
+
for (const article of discovered) {
|
|
29
|
+
// (1) insert-only — never overwrite a human-owned entry.
|
|
30
|
+
if (Object.prototype.hasOwnProperty.call(existing, article.slug)) {
|
|
31
|
+
keptExisting.push(article.slug);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
// (2) money never inferred — no valid wallet ⇒ report, don't write.
|
|
35
|
+
const resolved = resolveAuthorWallet(article, config);
|
|
36
|
+
if (resolved.unmapped || resolved.wallet === null) {
|
|
37
|
+
unmapped.push({ slug: article.slug, author: resolved.author });
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const entry = {
|
|
41
|
+
slug: article.slug,
|
|
42
|
+
title: article.title.trim() || article.slug, // sitemaps carry no title → the slug is a fair draft
|
|
43
|
+
contributors: [{ authorId: resolved.author?.trim() || "author", wallet: walletAddress(resolved.wallet) }],
|
|
44
|
+
};
|
|
45
|
+
// Never emit a file the gate would reject — validate through the real contract.
|
|
46
|
+
parseCredits(entry, `crawled credits for "${article.slug}"`);
|
|
47
|
+
credits[article.slug] = entry;
|
|
48
|
+
added.push(article.slug);
|
|
49
|
+
}
|
|
50
|
+
return { credits, added, keptExisting, unmapped };
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=credits-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credits-file.js","sourceRoot":"","sources":["../../src/crawl/credits-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,YAAY,EAAuB,MAAM,wBAAwB,CAAC;AAC3E,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAqBnD,6EAA6E;AAC7E,MAAM,UAAU,YAAY,CAC1B,QAAwC,EACxC,UAA+B,EAC/B,MAAmB;IAEnB,MAAM,OAAO,GAAmC,EAAE,GAAG,QAAQ,EAAE,CAAC;IAChE,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,YAAY,GAAa,EAAE,CAAC;IAClC,MAAM,QAAQ,GAAsB,EAAE,CAAC;IAEvC,KAAK,MAAM,OAAO,IAAI,UAAU,EAAE,CAAC;QACjC,yDAAyD;QACzD,IAAI,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACjE,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAChC,SAAS;QACX,CAAC;QACD,oEAAoE;QACpE,MAAM,QAAQ,GAAG,mBAAmB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACtD,IAAI,QAAQ,CAAC,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;YAClD,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;YAC/D,SAAS;QACX,CAAC;QACD,MAAM,KAAK,GAAmB;YAC5B,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,OAAO,CAAC,IAAI,EAAE,qDAAqD;YAClG,YAAY,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,QAAQ,EAAE,MAAM,EAAE,aAAa,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;SAC1G,CAAC;QACF,gFAAgF;QAChF,YAAY,CAAC,KAAK,EAAE,wBAAwB,OAAO,CAAC,IAAI,GAAG,CAAC,CAAC;QAC7D,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;QAC9B,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC;AACpD,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Fetcher } from "./types.ts";
|
|
2
|
+
export interface GuardedFetcherOpts {
|
|
3
|
+
/** The verified origin, `scheme://host[:port]`. The fetcher rejects any other host. */
|
|
4
|
+
origin: string;
|
|
5
|
+
/** Per-request timeout (ms). */
|
|
6
|
+
timeoutMs?: number;
|
|
7
|
+
/** Dev-only escape hatch: permit private/loopback + http (so a local fixture origin works).
|
|
8
|
+
* NEVER set in production — it disables the SSRF block. Defaults false. */
|
|
9
|
+
allowPrivate?: boolean;
|
|
10
|
+
/** Injected fetch (tests/fakes). Absent → the real node-http guarded path. */
|
|
11
|
+
fetchImpl?: typeof fetch;
|
|
12
|
+
}
|
|
13
|
+
/** Build the guarded fetcher bound to one origin. */
|
|
14
|
+
export declare function makeGuardedFetcher(opts: GuardedFetcherOpts): Fetcher;
|
|
15
|
+
//# sourceMappingURL=fetcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetcher.d.ts","sourceRoot":"","sources":["../../src/crawl/fetcher.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,OAAO,EAAe,MAAM,YAAY,CAAC;AAKvD,MAAM,WAAW,kBAAkB;IACjC,uFAAuF;IACvF,MAAM,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;gFAC4E;IAC5E,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED,qDAAqD;AACrD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAkDpE"}
|