@dwk/webmention 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.
Files changed (53) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +140 -0
  3. package/dist/discovery.d.ts +43 -0
  4. package/dist/discovery.d.ts.map +1 -0
  5. package/dist/discovery.js +128 -0
  6. package/dist/discovery.js.map +1 -0
  7. package/dist/fetch.d.ts +28 -0
  8. package/dist/fetch.d.ts.map +1 -0
  9. package/dist/fetch.js +73 -0
  10. package/dist/fetch.js.map +1 -0
  11. package/dist/html.d.ts +68 -0
  12. package/dist/html.d.ts.map +1 -0
  13. package/dist/html.js +183 -0
  14. package/dist/html.js.map +1 -0
  15. package/dist/inbox.d.ts +41 -0
  16. package/dist/inbox.d.ts.map +1 -0
  17. package/dist/inbox.js +73 -0
  18. package/dist/inbox.js.map +1 -0
  19. package/dist/index.d.ts +96 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +161 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/log.d.ts +42 -0
  24. package/dist/log.d.ts.map +1 -0
  25. package/dist/log.js +40 -0
  26. package/dist/log.js.map +1 -0
  27. package/dist/safe-fetch.d.ts +101 -0
  28. package/dist/safe-fetch.d.ts.map +1 -0
  29. package/dist/safe-fetch.js +348 -0
  30. package/dist/safe-fetch.js.map +1 -0
  31. package/dist/sender.d.ts +43 -0
  32. package/dist/sender.d.ts.map +1 -0
  33. package/dist/sender.js +80 -0
  34. package/dist/sender.js.map +1 -0
  35. package/dist/validate.d.ts +47 -0
  36. package/dist/validate.d.ts.map +1 -0
  37. package/dist/validate.js +76 -0
  38. package/dist/validate.js.map +1 -0
  39. package/dist/verify.d.ts +61 -0
  40. package/dist/verify.d.ts.map +1 -0
  41. package/dist/verify.js +216 -0
  42. package/dist/verify.js.map +1 -0
  43. package/package.json +45 -0
  44. package/src/discovery.ts +167 -0
  45. package/src/fetch.ts +84 -0
  46. package/src/html.ts +206 -0
  47. package/src/inbox.ts +121 -0
  48. package/src/index.ts +297 -0
  49. package/src/log.ts +44 -0
  50. package/src/safe-fetch.ts +405 -0
  51. package/src/sender.ts +131 -0
  52. package/src/validate.ts +116 -0
  53. package/src/verify.ts +294 -0
package/dist/html.js ADDED
@@ -0,0 +1,183 @@
1
+ /**
2
+ * `@dwk/webmention` — HTML / `Link`-header parsing helpers.
3
+ *
4
+ * Shared by endpoint discovery (sender) and source verification (receiver).
5
+ * `Link`-header parsing is plain string scanning; HTML scanning uses the
6
+ * Workers runtime's streaming `HTMLRewriter` rather than regex tag matching, so
7
+ * it handles comments, attribute quoting, and malformed markup correctly
8
+ * without pulling a parser into the bundle (`HTMLRewriter` is built into the
9
+ * runtime — zero script-size cost; see `spec/non-functional-requirements.md`).
10
+ *
11
+ * Because `HTMLRewriter` is a `workerd` global, the HTML scanners are async and
12
+ * exercised under the Workers test pool, not bare Node.
13
+ *
14
+ * @packageDocumentation
15
+ */
16
+ /**
17
+ * Parse an HTTP `Link` header into entries. Handles multiple comma-separated
18
+ * links and semicolon-separated parameters, e.g.
19
+ * `<https://a.example/webmention>; rel="webmention"`.
20
+ *
21
+ * Entries with no `rel` parameter are dropped; an empty URI (`<>`) is kept so
22
+ * the caller can resolve it against the document URL (a Webmention endpoint
23
+ * advertised at the page itself).
24
+ */
25
+ export function parseLinkHeader(value) {
26
+ if (value === null || value.trim() === "") {
27
+ return [];
28
+ }
29
+ const entries = [];
30
+ for (const part of splitLinks(value)) {
31
+ const match = /^\s*<([^>]*)>\s*(.*)$/.exec(part);
32
+ if (match === null) {
33
+ continue;
34
+ }
35
+ const uri = match[1] ?? "";
36
+ const rels = splitTokens(extractRel(match[2] ?? ""));
37
+ if (rels.length > 0) {
38
+ entries.push({ uri, rels });
39
+ }
40
+ }
41
+ return entries;
42
+ }
43
+ /**
44
+ * Split a `Link` header on top-level commas, respecting both the angle-bracket
45
+ * URI reference and double-quoted parameter values — so a comma inside
46
+ * `title="A, B"` or inside `<…>` does not split the entry.
47
+ */
48
+ function splitLinks(value) {
49
+ const result = [];
50
+ let depth = 0;
51
+ let inQuotes = false;
52
+ let current = "";
53
+ for (let i = 0; i < value.length; i++) {
54
+ const char = value[i];
55
+ if (char === '"' && value[i - 1] !== "\\") {
56
+ inQuotes = !inQuotes;
57
+ }
58
+ else if (!inQuotes && char === "<") {
59
+ depth++;
60
+ }
61
+ else if (!inQuotes && char === ">") {
62
+ depth--;
63
+ }
64
+ if (char === "," && depth === 0 && !inQuotes) {
65
+ result.push(current);
66
+ current = "";
67
+ }
68
+ else {
69
+ current += char;
70
+ }
71
+ }
72
+ if (current.trim() !== "") {
73
+ result.push(current);
74
+ }
75
+ return result;
76
+ }
77
+ /**
78
+ * Split a `Link` entry's parameter string on top-level semicolons, respecting
79
+ * double-quoted values so a `;` inside a quoted value does not split a param.
80
+ */
81
+ function splitParams(paramString) {
82
+ const result = [];
83
+ let inQuotes = false;
84
+ let current = "";
85
+ for (let i = 0; i < paramString.length; i++) {
86
+ const char = paramString[i];
87
+ if (char === '"' && paramString[i - 1] !== "\\") {
88
+ inQuotes = !inQuotes;
89
+ }
90
+ if (char === ";" && !inQuotes) {
91
+ result.push(current);
92
+ current = "";
93
+ }
94
+ else {
95
+ current += char;
96
+ }
97
+ }
98
+ if (current.trim() !== "") {
99
+ result.push(current);
100
+ }
101
+ return result;
102
+ }
103
+ /**
104
+ * Extract the `rel` parameter from a `Link` entry's parameter string. Matches
105
+ * the `rel` parameter exactly (per-parameter), so a `rel=` substring inside
106
+ * another parameter's quoted value (e.g. `title="my rel=x"`) is not mistaken
107
+ * for it.
108
+ */
109
+ function extractRel(paramString) {
110
+ for (const param of splitParams(paramString)) {
111
+ const match = /^\s*rel\s*=\s*("([^"]*)"|'([^']*)'|[^;\s]+)\s*$/i.exec(param);
112
+ if (match !== null) {
113
+ return match[2] ?? match[3] ?? match[1] ?? null;
114
+ }
115
+ }
116
+ return null;
117
+ }
118
+ /**
119
+ * Whether a `Content-Type` value names an HTML document (`text/html` or
120
+ * `application/xhtml+xml`). Compares the media type's essence — the part before
121
+ * any `;` parameters — case-insensitively, so `text/html; charset=utf-8`
122
+ * matches but an unrelated type carrying `text/html` inside a parameter does
123
+ * not.
124
+ */
125
+ export function isHtmlContentType(contentType) {
126
+ const essence = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
127
+ return essence === "text/html" || essence === "application/xhtml+xml";
128
+ }
129
+ /**
130
+ * Whether a `Content-Type` value names a JSON document (`application/json` or a
131
+ * `+json`-suffixed type such as `application/activity+json`). Compares the media
132
+ * type's essence — the part before any `;` parameters — case-insensitively.
133
+ */
134
+ export function isJsonContentType(contentType) {
135
+ const essence = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
136
+ return essence === "application/json" || essence.endsWith("+json");
137
+ }
138
+ /** Split a whitespace-separated token list (e.g. a `rel` value) into tokens. */
139
+ export function splitTokens(value) {
140
+ if (value === null) {
141
+ return [];
142
+ }
143
+ return value
144
+ .trim()
145
+ .split(/\s+/)
146
+ .filter((token) => token !== "");
147
+ }
148
+ /** Resolve `uri` against `base`, returning a normalized absolute URL or `null`. */
149
+ export function resolveUrl(uri, base) {
150
+ try {
151
+ return new URL(uri, base).toString();
152
+ }
153
+ catch {
154
+ return null;
155
+ }
156
+ }
157
+ /**
158
+ * Scan `html` with the runtime's streaming `HTMLRewriter`, returning — in
159
+ * document order — every element matching `selector` together with the
160
+ * requested attribute values.
161
+ *
162
+ * Using a real tokenizer (rather than regex) means elements inside comments are
163
+ * never reported (the parser treats comment contents as text, satisfying
164
+ * webmention.rocks discovery test 13), attribute quoting is handled correctly,
165
+ * and a `data-href` attribute is never mistaken for `href`. An absent attribute
166
+ * is reported as `null`; a present-but-empty one (`href=""`) as `""`.
167
+ */
168
+ export async function scanElements(html, selector, attrNames) {
169
+ const elements = [];
170
+ const rewriter = new HTMLRewriter().on(selector, {
171
+ element(el) {
172
+ const attrs = {};
173
+ for (const name of attrNames) {
174
+ attrs[name] = el.getAttribute(name);
175
+ }
176
+ elements.push({ name: el.tagName, attrs });
177
+ },
178
+ });
179
+ // Drive the parser to completion by consuming the transformed body.
180
+ await rewriter.transform(new Response(html)).text();
181
+ return elements;
182
+ }
183
+ //# sourceMappingURL=html.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.js","sourceRoot":"","sources":["../src/html.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * `@dwk/webmention` — inbox store.
3
+ *
4
+ * Verified mentions are persisted to an inbox so they can be surfaced on the
5
+ * target resource. The default is a D1-backed store (strongly consistent —
6
+ * never KV, per `spec/non-functional-requirements.md`); when composed into a
7
+ * Solid Pod, a caller can supply an {@link InboxStore} backed by the
8
+ * `@dwk/solid-pod` Durable Object instead. The store keys on the
9
+ * `(source, target)` pair so re-verifying a mention updates it in place and a
10
+ * source that drops the link can be removed. See `spec/packages/webmention.md`.
11
+ *
12
+ * @packageDocumentation
13
+ */
14
+ import type { D1Database } from "@cloudflare/workers-types";
15
+ /** A verified Webmention: `source` links to `target`, confirmed at `verifiedAt`. */
16
+ export interface VerifiedMention {
17
+ readonly source: string;
18
+ readonly target: string;
19
+ /** Verification time, epoch milliseconds. */
20
+ readonly verifiedAt: number;
21
+ }
22
+ /** Persistence surface for verified mentions. */
23
+ export interface InboxStore {
24
+ /** Upsert a verified mention, keyed on `(source, target)`. */
25
+ store(mention: VerifiedMention): Promise<void>;
26
+ /** Remove a mention (e.g. the source dropped the link); no-op when absent. */
27
+ remove(source: string, target: string): Promise<void>;
28
+ /** List mentions, newest first; scoped to `target` when given. */
29
+ list(target?: string): Promise<VerifiedMention[]>;
30
+ }
31
+ /** Options for {@link createD1Inbox}. */
32
+ export interface D1InboxOptions {
33
+ /** Table name to use; created if absent. Defaults to `webmentions`. */
34
+ readonly table?: string;
35
+ }
36
+ /**
37
+ * Build a D1-backed {@link InboxStore}. The backing table is created on first
38
+ * use if it does not already exist.
39
+ */
40
+ export declare function createD1Inbox(db: D1Database, options?: D1InboxOptions): InboxStore;
41
+ //# sourceMappingURL=inbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbox.d.ts","sourceRoot":"","sources":["../src/inbox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D,oFAAoF;AACpF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,6CAA6C;IAC7C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,iDAAiD;AACjD,MAAM,WAAW,UAAU;IACzB,8DAA8D;IAC9D,KAAK,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,8EAA8E;IAC9E,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD,kEAAkE;IAClE,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;CACnD;AAED,yCAAyC;AACzC,MAAM,WAAW,cAAc;IAC7B,uEAAuE;IACvE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAQD;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,EAAE,EAAE,UAAU,EACd,OAAO,CAAC,EAAE,cAAc,GACvB,UAAU,CAmEZ"}
package/dist/inbox.js ADDED
@@ -0,0 +1,73 @@
1
+ /**
2
+ * `@dwk/webmention` — inbox store.
3
+ *
4
+ * Verified mentions are persisted to an inbox so they can be surfaced on the
5
+ * target resource. The default is a D1-backed store (strongly consistent —
6
+ * never KV, per `spec/non-functional-requirements.md`); when composed into a
7
+ * Solid Pod, a caller can supply an {@link InboxStore} backed by the
8
+ * `@dwk/solid-pod` Durable Object instead. The store keys on the
9
+ * `(source, target)` pair so re-verifying a mention updates it in place and a
10
+ * source that drops the link can be removed. See `spec/packages/webmention.md`.
11
+ *
12
+ * @packageDocumentation
13
+ */
14
+ /**
15
+ * Build a D1-backed {@link InboxStore}. The backing table is created on first
16
+ * use if it does not already exist.
17
+ */
18
+ export function createD1Inbox(db, options) {
19
+ const table = options?.table ?? "webmentions";
20
+ // Guard the identifier: it is interpolated into DDL, so only allow a safe
21
+ // set of characters rather than trusting the caller blindly.
22
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table)) {
23
+ throw new Error(`@dwk/webmention: invalid inbox table name "${table}".`);
24
+ }
25
+ let ready = null;
26
+ const ensureSchema = () => {
27
+ ready ??= db
28
+ .prepare(`CREATE TABLE IF NOT EXISTS ${table} (` +
29
+ `source TEXT NOT NULL, ` +
30
+ `target TEXT NOT NULL, ` +
31
+ `verified_at INTEGER NOT NULL, ` +
32
+ `PRIMARY KEY (source, target))`)
33
+ .run()
34
+ .then(() => undefined);
35
+ return ready;
36
+ };
37
+ return {
38
+ async store(mention) {
39
+ await ensureSchema();
40
+ await db
41
+ .prepare(`INSERT INTO ${table} (source, target, verified_at) ` +
42
+ `VALUES (?1, ?2, ?3) ` +
43
+ `ON CONFLICT (source, target) ` +
44
+ `DO UPDATE SET verified_at = excluded.verified_at`)
45
+ .bind(mention.source, mention.target, mention.verifiedAt)
46
+ .run();
47
+ },
48
+ async remove(source, target) {
49
+ await ensureSchema();
50
+ await db
51
+ .prepare(`DELETE FROM ${table} WHERE source = ?1 AND target = ?2`)
52
+ .bind(source, target)
53
+ .run();
54
+ },
55
+ async list(target) {
56
+ await ensureSchema();
57
+ const statement = target === undefined
58
+ ? db.prepare(`SELECT source, target, verified_at FROM ${table} ` +
59
+ `ORDER BY verified_at DESC`)
60
+ : db
61
+ .prepare(`SELECT source, target, verified_at FROM ${table} ` +
62
+ `WHERE target = ?1 ORDER BY verified_at DESC`)
63
+ .bind(target);
64
+ const { results } = await statement.all();
65
+ return results.map((row) => ({
66
+ source: row.source,
67
+ target: row.target,
68
+ verifiedAt: row.verified_at,
69
+ }));
70
+ },
71
+ };
72
+ }
73
+ //# sourceMappingURL=inbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbox.js","sourceRoot":"","sources":["../src/inbox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAkCH;;;GAGG;AACH,MAAM,UAAU,aAAa,CAC3B,EAAc,EACd,OAAwB;IAExB,MAAM,KAAK,GAAG,OAAO,EAAE,KAAK,IAAI,aAAa,CAAC;IAC9C,0EAA0E;IAC1E,6DAA6D;IAC7D,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5C,MAAM,IAAI,KAAK,CAAC,8CAA8C,KAAK,IAAI,CAAC,CAAC;IAC3E,CAAC;IAED,IAAI,KAAK,GAAyB,IAAI,CAAC;IACvC,MAAM,YAAY,GAAG,GAAkB,EAAE;QACvC,KAAK,KAAK,EAAE;aACT,OAAO,CACN,8BAA8B,KAAK,IAAI;YACrC,wBAAwB;YACxB,wBAAwB;YACxB,gCAAgC;YAChC,+BAA+B,CAClC;aACA,GAAG,EAAE;aACL,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACzB,OAAO,KAAK,CAAC;IACf,CAAC,CAAC;IAEF,OAAO;QACL,KAAK,CAAC,KAAK,CAAC,OAAO;YACjB,MAAM,YAAY,EAAE,CAAC;YACrB,MAAM,EAAE;iBACL,OAAO,CACN,eAAe,KAAK,iCAAiC;gBACnD,sBAAsB;gBACtB,+BAA+B;gBAC/B,kDAAkD,CACrD;iBACA,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC;iBACxD,GAAG,EAAE,CAAC;QACX,CAAC;QAED,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM;YACzB,MAAM,YAAY,EAAE,CAAC;YACrB,MAAM,EAAE;iBACL,OAAO,CAAC,eAAe,KAAK,oCAAoC,CAAC;iBACjE,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC;iBACpB,GAAG,EAAE,CAAC;QACX,CAAC;QAED,KAAK,CAAC,IAAI,CAAC,MAAM;YACf,MAAM,YAAY,EAAE,CAAC;YACrB,MAAM,SAAS,GACb,MAAM,KAAK,SAAS;gBAClB,CAAC,CAAC,EAAE,CAAC,OAAO,CACR,2CAA2C,KAAK,GAAG;oBACjD,2BAA2B,CAC9B;gBACH,CAAC,CAAC,EAAE;qBACC,OAAO,CACN,2CAA2C,KAAK,GAAG;oBACjD,6CAA6C,CAChD;qBACA,IAAI,CAAC,MAAM,CAAC,CAAC;YACtB,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,SAAS,CAAC,GAAG,EAAc,CAAC;YACtD,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;gBAC3B,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,UAAU,EAAE,GAAG,CAAC,WAAW;aAC5B,CAAC,CAAC,CAAC;QACN,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * `@dwk/webmention` — Webmention (W3C) receiver + sender.
3
+ *
4
+ * Endpoint package. The receiver validates `source`/`target` synchronously,
5
+ * returns `202 Accepted`, and enqueues the pair for asynchronous link
6
+ * verification; the queue consumer fetches the source, confirms it links to the
7
+ * target, and persists (or removes) the mention in an inbox. The sender
8
+ * discovers a target's Webmention endpoint and notifies it on publish. Cloud
9
+ * specifics (Queue, D1) are confined here; HTML scanning uses the runtime's
10
+ * streaming `HTMLRewriter`, so the parsing/verification helpers are async and
11
+ * exercised under the Workers test pool.
12
+ *
13
+ * @see spec/packages/webmention.md
14
+ * @packageDocumentation
15
+ */
16
+ import type { D1Database, ExecutionContext, MessageBatch, Queue } from "@cloudflare/workers-types";
17
+ import { type Logger, type Metrics } from "@dwk/log";
18
+ import { type InboxStore } from "./inbox";
19
+ import type { FetchLike } from "./fetch";
20
+ export { validateWebmentionParams, type ValidateParams, type ValidationResult, type WebmentionValidationError, } from "./validate";
21
+ export { discoverEndpoint, findWebmentionEndpoint, type DiscoverOptions, } from "./discovery";
22
+ export { sendWebmention, sendWebmentions, type SendOptions, type SendResult, } from "./sender";
23
+ export { verifySource, sourceLinksTo, extractLinks, type VerifyOptions, type VerifyResult, } from "./verify";
24
+ export { createD1Inbox, type InboxStore, type VerifiedMention, type D1InboxOptions, } from "./inbox";
25
+ export type { FetchLike } from "./fetch";
26
+ export { safeFetch, assertPublicUrl, isPrivateOrReservedHost, SsrfError, DEFAULT_MAX_REDIRECTS, DEFAULT_TIMEOUT_MS, type SafeFetchOptions, type SafeFetchResult, type SsrfReason, } from "./safe-fetch";
27
+ export { WebmentionLogEvent } from "./log";
28
+ export type { Logger, Metrics } from "@dwk/log";
29
+ /** A queued verification job: confirm that `source` links to `target`. */
30
+ export interface WebmentionJob {
31
+ readonly source: string;
32
+ readonly target: string;
33
+ }
34
+ /** Cloudflare bindings required by the Webmention handler and queue consumer. */
35
+ export interface WebmentionEnv {
36
+ /** Queue producer for async verification of received mentions. */
37
+ readonly WEBMENTION_QUEUE: Queue<WebmentionJob>;
38
+ /**
39
+ * D1 database backing the default inbox. Optional only when an
40
+ * {@link InboxStore} is supplied via {@link WebmentionConfig.inbox} (e.g. a
41
+ * Solid Pod DO-backed inbox).
42
+ */
43
+ readonly WEBMENTION_INBOX?: D1Database;
44
+ }
45
+ /** Configuration passed to {@link createWebmention}. */
46
+ export interface WebmentionConfig {
47
+ /** Base URL of this receiver; a `target` must live under its origin. */
48
+ readonly baseUrl: string;
49
+ /** Additional controlled hostnames besides `baseUrl`'s. */
50
+ readonly allowedHosts?: readonly string[];
51
+ /**
52
+ * Inbox store for verified mentions. Defaults to a D1 store built from
53
+ * {@link WebmentionEnv.WEBMENTION_INBOX}; supply one to back the inbox with
54
+ * the `@dwk/solid-pod` Durable Object instead.
55
+ */
56
+ readonly inbox?: InboxStore;
57
+ /** `fetch` implementation for verification; defaults to the global `fetch`. */
58
+ readonly fetch?: FetchLike;
59
+ /**
60
+ * Logger for receiver/queue events; defaults to a no-op. Wire a real logger
61
+ * (see `@dwk/log`) to surface SSRF blocks, validation rejections, and
62
+ * poison-message retries instead of swallowing them.
63
+ */
64
+ readonly logger?: Logger;
65
+ /**
66
+ * Metrics sink for receiver/queue counters; defaults to a no-op. Wire an
67
+ * adapter (e.g. `analyticsEngineMetrics` from `@dwk/log`, bound to an
68
+ * `AnalyticsEngineDataset`) to chart the same events the logger names —
69
+ * "SSRF blocks/min", "verification success rate", "queue retries by reason".
70
+ */
71
+ readonly metrics?: Metrics;
72
+ }
73
+ /** A `fetch`-compatible Worker handler. */
74
+ export type WebmentionHandler = (request: Request, env: WebmentionEnv, ctx: ExecutionContext) => Promise<Response>;
75
+ /** A Queue consumer for asynchronous Webmention verification. */
76
+ export type WebmentionQueueConsumer = (batch: MessageBatch<WebmentionJob>, env: WebmentionEnv, ctx: ExecutionContext) => Promise<void>;
77
+ /**
78
+ * Build the Webmention receiver handler from configuration.
79
+ *
80
+ * The returned handler is mountable under any path prefix. It accepts a
81
+ * form-encoded `POST` (`source` + `target`), validates synchronously, enqueues
82
+ * the pair for verification, and returns `202 Accepted`. Invalid requests get
83
+ * `400`; other methods get `405`. Fails loudly if the required `WEBMENTION_QUEUE`
84
+ * binding is missing.
85
+ */
86
+ export declare function createWebmention(config: WebmentionConfig): WebmentionHandler;
87
+ /**
88
+ * Build the Queue consumer that verifies received mentions.
89
+ *
90
+ * For each job it fetches the source and checks for a link to the target: a
91
+ * verified mention is upserted into the inbox, while a source that no longer
92
+ * links is removed. A job that throws is retried; otherwise it is acked. Fails
93
+ * loudly if no inbox is configured.
94
+ */
95
+ export declare function createWebmentionQueueConsumer(config: WebmentionConfig): WebmentionQueueConsumer;
96
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,161 @@
1
+ /**
2
+ * `@dwk/webmention` — Webmention (W3C) receiver + sender.
3
+ *
4
+ * Endpoint package. The receiver validates `source`/`target` synchronously,
5
+ * returns `202 Accepted`, and enqueues the pair for asynchronous link
6
+ * verification; the queue consumer fetches the source, confirms it links to the
7
+ * target, and persists (or removes) the mention in an inbox. The sender
8
+ * discovers a target's Webmention endpoint and notifies it on publish. Cloud
9
+ * specifics (Queue, D1) are confined here; HTML scanning uses the runtime's
10
+ * streaming `HTMLRewriter`, so the parsing/verification helpers are async and
11
+ * exercised under the Workers test pool.
12
+ *
13
+ * @see spec/packages/webmention.md
14
+ * @packageDocumentation
15
+ */
16
+ import { hostFromUrl, noopLogger, noopMetrics, } from "@dwk/log";
17
+ import { createD1Inbox } from "./inbox";
18
+ import { WebmentionLogEvent } from "./log";
19
+ import { validateWebmentionParams } from "./validate";
20
+ import { verifySource } from "./verify";
21
+ export { validateWebmentionParams, } from "./validate";
22
+ export { discoverEndpoint, findWebmentionEndpoint, } from "./discovery";
23
+ export { sendWebmention, sendWebmentions, } from "./sender";
24
+ export { verifySource, sourceLinksTo, extractLinks, } from "./verify";
25
+ export { createD1Inbox, } from "./inbox";
26
+ export { safeFetch, assertPublicUrl, isPrivateOrReservedHost, SsrfError, DEFAULT_MAX_REDIRECTS, DEFAULT_TIMEOUT_MS, } from "./safe-fetch";
27
+ export { WebmentionLogEvent } from "./log";
28
+ function textResponse(status, body) {
29
+ return new Response(body, {
30
+ status,
31
+ headers: { "content-type": "text/plain; charset=utf-8" },
32
+ });
33
+ }
34
+ function resolveInbox(config, env) {
35
+ if (config.inbox !== undefined) {
36
+ return config.inbox;
37
+ }
38
+ if (env.WEBMENTION_INBOX !== undefined) {
39
+ return createD1Inbox(env.WEBMENTION_INBOX);
40
+ }
41
+ throw new Error("@dwk/webmention: no inbox configured — provide config.inbox or bind " +
42
+ "WEBMENTION_INBOX (D1).");
43
+ }
44
+ function formValue(value) {
45
+ return typeof value === "string" ? value : null;
46
+ }
47
+ /**
48
+ * Whether the request body is `application/x-www-form-urlencoded` — the encoding
49
+ * Webmention §3.1.3 requires. `Request.formData()` would also accept
50
+ * `multipart/form-data`, so the essence is checked up front rather than relying
51
+ * on it.
52
+ */
53
+ function isFormUrlEncoded(contentType) {
54
+ const essence = contentType?.split(";")[0]?.trim().toLowerCase() ?? "";
55
+ return essence === "application/x-www-form-urlencoded";
56
+ }
57
+ /**
58
+ * Build the Webmention receiver handler from configuration.
59
+ *
60
+ * The returned handler is mountable under any path prefix. It accepts a
61
+ * form-encoded `POST` (`source` + `target`), validates synchronously, enqueues
62
+ * the pair for verification, and returns `202 Accepted`. Invalid requests get
63
+ * `400`; other methods get `405`. Fails loudly if the required `WEBMENTION_QUEUE`
64
+ * binding is missing.
65
+ */
66
+ export function createWebmention(config) {
67
+ const logger = config.logger ?? noopLogger;
68
+ const metrics = config.metrics ?? noopMetrics;
69
+ return async (request, env, _ctx) => {
70
+ if (request.method !== "POST") {
71
+ return new Response("Method Not Allowed", {
72
+ status: 405,
73
+ headers: { allow: "POST" },
74
+ });
75
+ }
76
+ if (env.WEBMENTION_QUEUE === undefined) {
77
+ throw new Error("@dwk/webmention: missing required binding WEBMENTION_QUEUE.");
78
+ }
79
+ if (!isFormUrlEncoded(request.headers.get("content-type"))) {
80
+ const fields = { reason: "invalid_content_type" };
81
+ logger.warn(WebmentionLogEvent.ReceiveRejected, fields);
82
+ metrics.count(WebmentionLogEvent.ReceiveRejected, fields);
83
+ return textResponse(400, "invalid_request: Content-Type must be application/x-www-form-urlencoded");
84
+ }
85
+ let form;
86
+ try {
87
+ form = await request.formData();
88
+ }
89
+ catch {
90
+ return textResponse(400, "invalid_request: expected a form-encoded body with source and target");
91
+ }
92
+ const result = validateWebmentionParams({
93
+ source: formValue(form.get("source")),
94
+ target: formValue(form.get("target")),
95
+ baseUrl: config.baseUrl,
96
+ allowedHosts: config.allowedHosts,
97
+ });
98
+ if (!result.ok) {
99
+ const fields = { reason: result.error };
100
+ logger.warn(WebmentionLogEvent.ReceiveRejected, fields);
101
+ metrics.count(WebmentionLogEvent.ReceiveRejected, fields);
102
+ return textResponse(400, result.error);
103
+ }
104
+ await env.WEBMENTION_QUEUE.send({
105
+ source: result.source,
106
+ target: result.target,
107
+ });
108
+ const fields = {
109
+ sourceHost: hostFromUrl(result.source),
110
+ targetHost: hostFromUrl(result.target),
111
+ };
112
+ logger.info(WebmentionLogEvent.ReceiveAccepted, fields);
113
+ metrics.count(WebmentionLogEvent.ReceiveAccepted, fields);
114
+ return new Response(null, { status: 202 });
115
+ };
116
+ }
117
+ /**
118
+ * Build the Queue consumer that verifies received mentions.
119
+ *
120
+ * For each job it fetches the source and checks for a link to the target: a
121
+ * verified mention is upserted into the inbox, while a source that no longer
122
+ * links is removed. A job that throws is retried; otherwise it is acked. Fails
123
+ * loudly if no inbox is configured.
124
+ */
125
+ export function createWebmentionQueueConsumer(config) {
126
+ const logger = config.logger ?? noopLogger;
127
+ const metrics = config.metrics ?? noopMetrics;
128
+ return async (batch, env, _ctx) => {
129
+ const inbox = resolveInbox(config, env);
130
+ for (const message of batch.messages) {
131
+ const { source, target } = message.body;
132
+ try {
133
+ const result = await verifySource(source, target, {
134
+ fetch: config.fetch,
135
+ logger,
136
+ metrics,
137
+ });
138
+ if (result.links) {
139
+ await inbox.store({ source, target, verifiedAt: Date.now() });
140
+ }
141
+ else {
142
+ await inbox.remove(source, target);
143
+ }
144
+ message.ack();
145
+ }
146
+ catch (err) {
147
+ // A poison message must not retry silently — record why so an operator
148
+ // can tell a transient failure from a wedged one.
149
+ const fields = {
150
+ sourceHost: hostFromUrl(source),
151
+ targetHost: hostFromUrl(target),
152
+ error: err instanceof Error ? err.name : "unknown",
153
+ };
154
+ logger.warn(WebmentionLogEvent.QueueRetry, fields);
155
+ metrics.count(WebmentionLogEvent.QueueRetry, fields);
156
+ message.retry();
157
+ }
158
+ }
159
+ };
160
+ }
161
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/log.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * `@dwk/webmention` — structured observability event taxonomy.
3
+ *
4
+ * The package's logging and metrics are opt-in via an injected {@link Logger}
5
+ * and {@link Metrics} (see `@dwk/log`), and **share this one vocabulary**: the
6
+ * same dotted event name is passed to `logger.warn(...)` and
7
+ * `metrics.count(...)` so a log line and its counter line up. Event names are
8
+ * stable, dotted, and queryable — operators filter on these rather than grepping
9
+ * free text. Security-relevant events (a blocked SSRF attempt, a receiver
10
+ * rejection, a poison-message retry) are first-class here so that being actively
11
+ * probed produces a distinct signal instead of looking like a dead link. See
12
+ * `spec/observability.md`.
13
+ *
14
+ * @packageDocumentation
15
+ */
16
+ /**
17
+ * Stable event names emitted by `@dwk/webmention`. Fields logged alongside each
18
+ * event use `hostFromUrl` for any URL so an attacker-supplied path/query is
19
+ * never recorded; tokens and bodies are never logged.
20
+ */
21
+ export declare const WebmentionLogEvent: {
22
+ /**
23
+ * An outbound fetch was refused on SSRF grounds. Fields: `reason`
24
+ * (machine-readable cause), `host` (sanitized, when known).
25
+ */
26
+ readonly SsrfBlocked: "webmention.ssrf.blocked";
27
+ /** A received mention passed validation and was enqueued. */
28
+ readonly ReceiveAccepted: "webmention.receive.accepted";
29
+ /** A received mention was rejected at validation. Field: `reason`. */
30
+ readonly ReceiveRejected: "webmention.receive.rejected";
31
+ /** Source verification finished. Fields: `links`, `status`. */
32
+ readonly VerifyCompleted: "webmention.verify.completed";
33
+ /** The source fetch failed/was blocked during verification. Field: `error`. */
34
+ readonly VerifyFetchFailed: "webmention.verify.fetch_failed";
35
+ /** A queue message threw and is being retried. Field: `error`. */
36
+ readonly QueueRetry: "webmention.queue.retry";
37
+ /** A send attempt finished. Fields: `endpointHost`, `delivered`, `status`. */
38
+ readonly SendCompleted: "webmention.send.completed";
39
+ };
40
+ /** Union of the event-name string literals in {@link WebmentionLogEvent}. */
41
+ export type WebmentionLogEvent = (typeof WebmentionLogEvent)[keyof typeof WebmentionLogEvent];
42
+ //# sourceMappingURL=log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;GAIG;AACH,eAAO,MAAM,kBAAkB;IAC7B;;;OAGG;;IAEH,6DAA6D;;IAE7D,sEAAsE;;IAEtE,+DAA+D;;IAE/D,+EAA+E;;IAE/E,kEAAkE;;IAElE,8EAA8E;;CAEtE,CAAC;AAEX,6EAA6E;AAC7E,MAAM,MAAM,kBAAkB,GAC5B,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,OAAO,kBAAkB,CAAC,CAAC"}