@dwk/microsub 0.1.0-beta.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/LICENSE +15 -0
- package/README.md +92 -0
- package/dist/auth.d.ts +53 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +102 -0
- package/dist/auth.js.map +1 -0
- package/dist/config.d.ts +102 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +64 -0
- package/dist/config.js.map +1 -0
- package/dist/consumer.d.ts +40 -0
- package/dist/consumer.d.ts.map +1 -0
- package/dist/consumer.js +87 -0
- package/dist/consumer.js.map +1 -0
- package/dist/discovery.d.ts +59 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +190 -0
- package/dist/discovery.js.map +1 -0
- package/dist/fetch.d.ts +28 -0
- package/dist/fetch.d.ts.map +1 -0
- package/dist/fetch.js +72 -0
- package/dist/fetch.js.map +1 -0
- package/dist/handler.d.ts +24 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +434 -0
- package/dist/handler.js.map +1 -0
- package/dist/hfeed.d.ts +25 -0
- package/dist/hfeed.d.ts.map +1 -0
- package/dist/hfeed.js +252 -0
- package/dist/hfeed.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/jf2.d.ts +69 -0
- package/dist/jf2.d.ts.map +1 -0
- package/dist/jf2.js +295 -0
- package/dist/jf2.js.map +1 -0
- package/dist/log.d.ts +44 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +42 -0
- package/dist/log.js.map +1 -0
- package/dist/poll.d.ts +22 -0
- package/dist/poll.d.ts.map +1 -0
- package/dist/poll.js +39 -0
- package/dist/poll.js.map +1 -0
- package/dist/queue.d.ts +25 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/queue.js +13 -0
- package/dist/queue.js.map +1 -0
- package/dist/replay.d.ts +34 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +49 -0
- package/dist/replay.js.map +1 -0
- package/dist/safe-fetch.d.ts +86 -0
- package/dist/safe-fetch.d.ts.map +1 -0
- package/dist/safe-fetch.js +311 -0
- package/dist/safe-fetch.js.map +1 -0
- package/dist/store.d.ts +131 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +393 -0
- package/dist/store.js.map +1 -0
- package/dist/xml.d.ts +51 -0
- package/dist/xml.d.ts.map +1 -0
- package/dist/xml.js +196 -0
- package/dist/xml.js.map +1 -0
- package/package.json +49 -0
- package/src/auth.ts +184 -0
- package/src/config.ts +156 -0
- package/src/consumer.ts +140 -0
- package/src/discovery.ts +270 -0
- package/src/fetch.ts +82 -0
- package/src/handler.ts +594 -0
- package/src/hfeed.ts +287 -0
- package/src/index.ts +86 -0
- package/src/jf2.ts +394 -0
- package/src/log.ts +46 -0
- package/src/poll.ts +72 -0
- package/src/queue.ts +26 -0
- package/src/replay.ts +68 -0
- package/src/safe-fetch.ts +346 -0
- package/src/store.ts +644 -0
- package/src/xml.ts +229 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAA2B,KAAK,MAAM,EAAE,KAAK,OAAO,EAAE,MAAM,UAAU,CAAC;AAE9E,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzD,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAC;AAGjD,sDAAsD;AACtD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,oFAAoF;AACpF,MAAM,WAAW,cAAc;IAC7B,6EAA6E;IAC7E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,mDAAmD;IACnD,QAAQ,CAAC,OAAO,EAAE,SAAS,QAAQ,EAAE,CAAC;CACvC;AAED,8BAA8B;AAC9B,MAAM,WAAW,WAAW;IAC1B,mEAAmE;IACnE,QAAQ,CAAC,OAAO,EAAE,SAAS,QAAQ,EAAE,CAAC;IACtC,0DAA0D;IAC1D,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAoGD;;;;GAIG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAmDhC;AAaD;;;;GAIG;AACH,wBAAsB,SAAS,CAC7B,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,gBAAgB,EAC1B,KAAK,CAAC,EAAE;IAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAC3D,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAuC7B"}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/microsub` — feed discovery and fetching.
|
|
3
|
+
*
|
|
4
|
+
* `follow` and `preview` take a URL the user typed; the server must work out
|
|
5
|
+
* what to actually poll. {@link discoverFeed} fetches that URL (through the
|
|
6
|
+
* SSRF-safe wrapper), and:
|
|
7
|
+
*
|
|
8
|
+
* - if it is already a syndication feed (Atom / RSS / JSON Feed), parses it;
|
|
9
|
+
* - if it is HTML, looks for a `<link rel="alternate">` feed and follows the
|
|
10
|
+
* first one; failing that, parses the page's own `h-feed` microformats.
|
|
11
|
+
*
|
|
12
|
+
* {@link fetchFeed} re-fetches an already-resolved feed URL on each poll,
|
|
13
|
+
* sending the cached `ETag` / `Last-Modified` so an unchanged feed returns `304`
|
|
14
|
+
* and is skipped. Every outbound request goes through {@link safeFetch}, and the
|
|
15
|
+
* body is read with a hard size cap.
|
|
16
|
+
*
|
|
17
|
+
* @packageDocumentation
|
|
18
|
+
*/
|
|
19
|
+
import { noopLogger, noopMetrics } from "@dwk/log";
|
|
20
|
+
import { readTextCapped } from "./fetch";
|
|
21
|
+
import { parseHFeed } from "./hfeed";
|
|
22
|
+
import { parseFeed } from "./jf2";
|
|
23
|
+
import { safeFetch } from "./safe-fetch";
|
|
24
|
+
const FEED_ACCEPT = "application/atom+xml, application/rss+xml, application/feed+json, " +
|
|
25
|
+
"application/json, text/xml, application/xml, text/html;q=0.9, */*;q=0.8";
|
|
26
|
+
/** Feed media types we recognise on a `Content-Type` or a `<link type>`. */
|
|
27
|
+
function isFeedType(contentType) {
|
|
28
|
+
const essence = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
29
|
+
return (essence === "application/atom+xml" ||
|
|
30
|
+
essence === "application/rss+xml" ||
|
|
31
|
+
essence === "application/feed+json" ||
|
|
32
|
+
essence === "application/json" ||
|
|
33
|
+
essence === "text/xml" ||
|
|
34
|
+
essence === "application/xml" ||
|
|
35
|
+
essence === "application/rdf+xml");
|
|
36
|
+
}
|
|
37
|
+
function isHtmlType(contentType) {
|
|
38
|
+
const essence = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
39
|
+
return essence === "text/html" || essence === "application/xhtml+xml";
|
|
40
|
+
}
|
|
41
|
+
/** Collect `<link>` elements (rel/type/href) from an HTML document. */
|
|
42
|
+
async function findFeedLinks(html, baseUrl) {
|
|
43
|
+
const links = [];
|
|
44
|
+
const rewriter = new HTMLRewriter().on("link", {
|
|
45
|
+
element(el) {
|
|
46
|
+
const href = el.getAttribute("href");
|
|
47
|
+
if (href === null || href === "")
|
|
48
|
+
return;
|
|
49
|
+
const rels = (el.getAttribute("rel") ?? "")
|
|
50
|
+
.toLowerCase()
|
|
51
|
+
.split(/\s+/)
|
|
52
|
+
.filter(Boolean);
|
|
53
|
+
const type = (el.getAttribute("type") ?? "").toLowerCase();
|
|
54
|
+
let resolved;
|
|
55
|
+
try {
|
|
56
|
+
resolved = new URL(href, baseUrl).toString();
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
links.push({ rels, type, href: resolved });
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
await rewriter.transform(new Response(html)).text();
|
|
65
|
+
return links;
|
|
66
|
+
}
|
|
67
|
+
/** The first alternate feed `<link>` in document order, if any. */
|
|
68
|
+
function pickFeedLink(links) {
|
|
69
|
+
for (const link of links) {
|
|
70
|
+
if (!link.rels.includes("alternate") && link.rels.length > 0)
|
|
71
|
+
continue;
|
|
72
|
+
if (link.type === "application/atom+xml" ||
|
|
73
|
+
link.type === "application/rss+xml" ||
|
|
74
|
+
link.type === "application/feed+json" ||
|
|
75
|
+
link.type === "application/json") {
|
|
76
|
+
return link;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
/** Parse a fetched body into entries, choosing the parser by content type. */
|
|
82
|
+
async function parseBody(body, contentType, url) {
|
|
83
|
+
if (isHtmlType(contentType)) {
|
|
84
|
+
return parseHFeed(body, url);
|
|
85
|
+
}
|
|
86
|
+
return parseFeed(body, contentType, url);
|
|
87
|
+
}
|
|
88
|
+
function resolveDeps(options) {
|
|
89
|
+
return {
|
|
90
|
+
doFetch: options?.fetch ?? ((input, init) => fetch(input, init)),
|
|
91
|
+
logger: options?.logger ?? noopLogger,
|
|
92
|
+
metrics: options?.metrics ?? noopMetrics,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Discover the feed at `target`. Returns the URL to poll plus the entries from
|
|
97
|
+
* this first fetch, or `null` when the target is unreachable, blocked, or has no
|
|
98
|
+
* feed and no `h-entry` content.
|
|
99
|
+
*/
|
|
100
|
+
export async function discoverFeed(target, options) {
|
|
101
|
+
const { doFetch, logger, metrics } = resolveDeps(options);
|
|
102
|
+
let response;
|
|
103
|
+
let finalUrl;
|
|
104
|
+
try {
|
|
105
|
+
const result = await safeFetch(doFetch, target, { method: "GET", headers: { accept: FEED_ACCEPT } }, { logger, metrics });
|
|
106
|
+
response = result.response;
|
|
107
|
+
finalUrl = result.url;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
if (!response.ok)
|
|
113
|
+
return null;
|
|
114
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
115
|
+
const body = await readTextCapped(response);
|
|
116
|
+
if (body === null)
|
|
117
|
+
return null;
|
|
118
|
+
// A syndication feed: parse it directly.
|
|
119
|
+
if (isFeedType(contentType) ||
|
|
120
|
+
(!isHtmlType(contentType) && looksLikeFeedBody(body))) {
|
|
121
|
+
return {
|
|
122
|
+
feedUrl: finalUrl,
|
|
123
|
+
entries: parseFeed(body, contentType, finalUrl),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// HTML: prefer a declared alternate feed, else fall back to h-feed.
|
|
127
|
+
if (isHtmlType(contentType) || body.trimStart().startsWith("<")) {
|
|
128
|
+
const links = await findFeedLinks(body, finalUrl);
|
|
129
|
+
const feedLink = pickFeedLink(links);
|
|
130
|
+
if (feedLink !== null) {
|
|
131
|
+
const fetched = await fetchFeed(feedLink.href, options);
|
|
132
|
+
if (fetched !== null) {
|
|
133
|
+
return { feedUrl: feedLink.href, entries: fetched.entries };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const entries = await parseHFeed(body, finalUrl);
|
|
137
|
+
if (entries.length > 0) {
|
|
138
|
+
return { feedUrl: finalUrl, entries };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
/** Cheap sniff for a feed root when the content type is unhelpful. */
|
|
144
|
+
function looksLikeFeedBody(body) {
|
|
145
|
+
const head = body.trimStart().slice(0, 512).toLowerCase();
|
|
146
|
+
return (head.includes("<rss") ||
|
|
147
|
+
head.includes("<feed") ||
|
|
148
|
+
head.includes("<rdf:rdf") ||
|
|
149
|
+
(head.startsWith("{") && head.includes("jsonfeed.org")));
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Fetch and parse an already-resolved feed URL, sending conditional-request
|
|
153
|
+
* validators when supplied. Returns `notModified` on a `304`, the parsed
|
|
154
|
+
* entries otherwise, or `null` when the fetch fails or is blocked.
|
|
155
|
+
*/
|
|
156
|
+
export async function fetchFeed(feedUrl, options, cache) {
|
|
157
|
+
const { doFetch, logger, metrics } = resolveDeps(options);
|
|
158
|
+
const headers = { accept: FEED_ACCEPT };
|
|
159
|
+
if (cache?.etag)
|
|
160
|
+
headers["if-none-match"] = cache.etag;
|
|
161
|
+
if (cache?.lastModified)
|
|
162
|
+
headers["if-modified-since"] = cache.lastModified;
|
|
163
|
+
let response;
|
|
164
|
+
let finalUrl;
|
|
165
|
+
try {
|
|
166
|
+
const result = await safeFetch(doFetch, feedUrl, { method: "GET", headers }, { logger, metrics });
|
|
167
|
+
response = result.response;
|
|
168
|
+
finalUrl = result.url;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const etag = response.headers.get("etag");
|
|
174
|
+
const lastModified = response.headers.get("last-modified");
|
|
175
|
+
if (response.status === 304) {
|
|
176
|
+
await response.body?.cancel().catch(() => undefined);
|
|
177
|
+
return { entries: [], notModified: true, etag, lastModified };
|
|
178
|
+
}
|
|
179
|
+
if (!response.ok) {
|
|
180
|
+
await response.body?.cancel().catch(() => undefined);
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
184
|
+
const body = await readTextCapped(response);
|
|
185
|
+
if (body === null)
|
|
186
|
+
return null;
|
|
187
|
+
const entries = await parseBody(body, contentType, finalUrl);
|
|
188
|
+
return { entries, notModified: false, etag, lastModified };
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=discovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discovery.js","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"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"}
|
package/dist/fetch.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/microsub` — injectable `fetch` type and a body-size-capped text reader.
|
|
3
|
+
*
|
|
4
|
+
* Discovery, polling, preview, and search all perform HTTP I/O against
|
|
5
|
+
* attacker-influenced URLs. They accept a {@link FetchLike} so callers can
|
|
6
|
+
* inject a stub in tests (no network) and so the package never reaches for a
|
|
7
|
+
* global it did not receive.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
/** A minimal, injectable `fetch` signature. */
|
|
12
|
+
export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
13
|
+
/**
|
|
14
|
+
* Default cap on a fetched feed body (4 MB). A feed is modest; a larger body is
|
|
15
|
+
* almost certainly hostile or irrelevant, and buffering it would risk an OOM
|
|
16
|
+
* (the Worker memory limit is 128 MB). See `spec/non-functional-requirements.md`.
|
|
17
|
+
*/
|
|
18
|
+
export declare const MAX_BODY_BYTES: number;
|
|
19
|
+
/**
|
|
20
|
+
* Read a response body as text, refusing bodies larger than `maxBytes`.
|
|
21
|
+
*
|
|
22
|
+
* A declared `Content-Length` over the cap is rejected up front; the stream is
|
|
23
|
+
* then read incrementally and aborted the moment the cap is exceeded, so a
|
|
24
|
+
* missing or lying `Content-Length` cannot force the whole body into memory.
|
|
25
|
+
* Returns `null` when the body is too large or cannot be read.
|
|
26
|
+
*/
|
|
27
|
+
export declare function readTextCapped(response: Response, maxBytes?: number): Promise<string | null>;
|
|
28
|
+
//# sourceMappingURL=fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../src/fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,+CAA+C;AAC/C,MAAM,MAAM,SAAS,GAAG,CACtB,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE,WAAW,KACf,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEvB;;;;GAIG;AACH,eAAO,MAAM,cAAc,QAAkB,CAAC;AAE9C;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,QAAQ,EAClB,QAAQ,SAAiB,GACxB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA8CxB"}
|
package/dist/fetch.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/microsub` — injectable `fetch` type and a body-size-capped text reader.
|
|
3
|
+
*
|
|
4
|
+
* Discovery, polling, preview, and search all perform HTTP I/O against
|
|
5
|
+
* attacker-influenced URLs. They accept a {@link FetchLike} so callers can
|
|
6
|
+
* inject a stub in tests (no network) and so the package never reaches for a
|
|
7
|
+
* global it did not receive.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Default cap on a fetched feed body (4 MB). A feed is modest; a larger body is
|
|
13
|
+
* almost certainly hostile or irrelevant, and buffering it would risk an OOM
|
|
14
|
+
* (the Worker memory limit is 128 MB). See `spec/non-functional-requirements.md`.
|
|
15
|
+
*/
|
|
16
|
+
export const MAX_BODY_BYTES = 4 * 1024 * 1024;
|
|
17
|
+
/**
|
|
18
|
+
* Read a response body as text, refusing bodies larger than `maxBytes`.
|
|
19
|
+
*
|
|
20
|
+
* A declared `Content-Length` over the cap is rejected up front; the stream is
|
|
21
|
+
* then read incrementally and aborted the moment the cap is exceeded, so a
|
|
22
|
+
* missing or lying `Content-Length` cannot force the whole body into memory.
|
|
23
|
+
* Returns `null` when the body is too large or cannot be read.
|
|
24
|
+
*/
|
|
25
|
+
export async function readTextCapped(response, maxBytes = MAX_BODY_BYTES) {
|
|
26
|
+
const declared = response.headers.get("content-length");
|
|
27
|
+
if (declared !== null) {
|
|
28
|
+
const length = Number.parseInt(declared, 10);
|
|
29
|
+
if (Number.isFinite(length) && length > maxBytes) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const body = response.body;
|
|
34
|
+
if (body === null) {
|
|
35
|
+
try {
|
|
36
|
+
const text = await response.text();
|
|
37
|
+
return text.length > maxBytes ? null : text;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const reader = body.getReader();
|
|
44
|
+
const chunks = [];
|
|
45
|
+
let total = 0;
|
|
46
|
+
try {
|
|
47
|
+
for (;;) {
|
|
48
|
+
const { done, value } = await reader.read();
|
|
49
|
+
if (done)
|
|
50
|
+
break;
|
|
51
|
+
if (value !== undefined) {
|
|
52
|
+
total += value.byteLength;
|
|
53
|
+
if (total > maxBytes) {
|
|
54
|
+
await reader.cancel();
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
chunks.push(value);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const merged = new Uint8Array(total);
|
|
65
|
+
let offset = 0;
|
|
66
|
+
for (const chunk of chunks) {
|
|
67
|
+
merged.set(chunk, offset);
|
|
68
|
+
offset += chunk.byteLength;
|
|
69
|
+
}
|
|
70
|
+
return new TextDecoder("utf-8").decode(merged);
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=fetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.js","sourceRoot":"","sources":["../src/fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAQH;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAE9C;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,QAAkB,EAClB,QAAQ,GAAG,cAAc;IAEzB,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IACxD,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QAC7C,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,GAAG,QAAQ,EAAE,CAAC;YACjD,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC;IAC3B,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAClB,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;YACnC,OAAO,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;QAC9C,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;IAChC,MAAM,MAAM,GAAiB,EAAE,CAAC;IAChC,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,CAAC;QACH,SAAS,CAAC;YACR,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;YAC5C,IAAI,IAAI;gBAAE,MAAM;YAChB,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACxB,KAAK,IAAI,KAAK,CAAC,UAAU,CAAC;gBAC1B,IAAI,KAAK,GAAG,QAAQ,EAAE,CAAC;oBACrB,MAAM,MAAM,CAAC,MAAM,EAAE,CAAC;oBACtB,OAAO,IAAI,CAAC;gBACd,CAAC;gBACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACrB,CAAC;QACH,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC;IACrC,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,UAAU,CAAC;IAC7B,CAAC;IACD,OAAO,IAAI,WAAW,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AACjD,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Microsub fetch handler: a single endpoint whose `action` (and `method`)
|
|
3
|
+
* parameters select the operation — channel management, following, the JF2
|
|
4
|
+
* timeline, search, and preview — wired to the D1 store and `@dwk/indieauth`
|
|
5
|
+
* token validation. Routing matches the request pathname against the configured
|
|
6
|
+
* endpoint path, so the handler is mountable under any prefix.
|
|
7
|
+
*
|
|
8
|
+
* The read path serves stored entries only; it never fetches a source inline.
|
|
9
|
+
* Following a feed discovers it, populates the timeline from the discovery
|
|
10
|
+
* fetch, and enqueues a poll job so the feed's poll cache is primed.
|
|
11
|
+
*
|
|
12
|
+
* @packageDocumentation
|
|
13
|
+
*/
|
|
14
|
+
import type { ExecutionContext } from "@cloudflare/workers-types";
|
|
15
|
+
import { type MicrosubConfig, type MicrosubEnv } from "./config";
|
|
16
|
+
/** A `fetch`-compatible Worker handler. */
|
|
17
|
+
export type MicrosubHandler = (request: Request, env: MicrosubEnv, ctx: ExecutionContext) => Promise<Response>;
|
|
18
|
+
/**
|
|
19
|
+
* Create the Microsub handler. The returned handler routes by pathname against
|
|
20
|
+
* the configured endpoint URL, so it is mountable under any path prefix, then
|
|
21
|
+
* dispatches on the `action` (and `method`) parameters.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createMicrosub(config: MicrosubConfig): MicrosubHandler;
|
|
24
|
+
//# sourceMappingURL=handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAQlE,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,WAAW,EAEjB,MAAM,UAAU,CAAC;AAUlB,2CAA2C;AAC3C,MAAM,MAAM,eAAe,GAAG,CAC5B,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,WAAW,EAChB,GAAG,EAAE,gBAAgB,KAClB,OAAO,CAAC,QAAQ,CAAC,CAAC;AAyevB;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,eAAe,CAyDtE"}
|