@dwk/websub 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 (63) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +155 -0
  3. package/dist/config.d.ts +122 -0
  4. package/dist/config.d.ts.map +1 -0
  5. package/dist/config.js +96 -0
  6. package/dist/config.js.map +1 -0
  7. package/dist/consumer.d.ts +39 -0
  8. package/dist/consumer.d.ts.map +1 -0
  9. package/dist/consumer.js +146 -0
  10. package/dist/consumer.js.map +1 -0
  11. package/dist/distribute.d.ts +109 -0
  12. package/dist/distribute.d.ts.map +1 -0
  13. package/dist/distribute.js +140 -0
  14. package/dist/distribute.js.map +1 -0
  15. package/dist/fetch.d.ts +28 -0
  16. package/dist/fetch.d.ts.map +1 -0
  17. package/dist/fetch.js +73 -0
  18. package/dist/fetch.js.map +1 -0
  19. package/dist/handler.d.ts +43 -0
  20. package/dist/handler.d.ts.map +1 -0
  21. package/dist/handler.js +127 -0
  22. package/dist/handler.js.map +1 -0
  23. package/dist/index.d.ts +32 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +29 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/log.d.ts +54 -0
  28. package/dist/log.d.ts.map +1 -0
  29. package/dist/log.js +52 -0
  30. package/dist/log.js.map +1 -0
  31. package/dist/queue.d.ts +38 -0
  32. package/dist/queue.d.ts.map +1 -0
  33. package/dist/queue.js +12 -0
  34. package/dist/queue.js.map +1 -0
  35. package/dist/safe-fetch.d.ts +101 -0
  36. package/dist/safe-fetch.d.ts.map +1 -0
  37. package/dist/safe-fetch.js +354 -0
  38. package/dist/safe-fetch.js.map +1 -0
  39. package/dist/store.d.ts +61 -0
  40. package/dist/store.d.ts.map +1 -0
  41. package/dist/store.js +110 -0
  42. package/dist/store.js.map +1 -0
  43. package/dist/validate.d.ts +67 -0
  44. package/dist/validate.d.ts.map +1 -0
  45. package/dist/validate.js +106 -0
  46. package/dist/validate.js.map +1 -0
  47. package/dist/verify.d.ts +85 -0
  48. package/dist/verify.d.ts.map +1 -0
  49. package/dist/verify.js +149 -0
  50. package/dist/verify.js.map +1 -0
  51. package/package.json +46 -0
  52. package/src/config.ts +199 -0
  53. package/src/consumer.ts +187 -0
  54. package/src/distribute.ts +257 -0
  55. package/src/fetch.ts +84 -0
  56. package/src/handler.ts +163 -0
  57. package/src/index.ts +98 -0
  58. package/src/log.ts +56 -0
  59. package/src/queue.ts +40 -0
  60. package/src/safe-fetch.ts +412 -0
  61. package/src/store.ts +190 -0
  62. package/src/validate.ts +179 -0
  63. package/src/verify.ts +229 -0
package/dist/log.d.ts ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * `@dwk/websub` — structured observability event taxonomy.
3
+ *
4
+ * Like the rest of the cohort, this package's logging and metrics are opt-in via
5
+ * an injected {@link Logger} and {@link Metrics} (see `@dwk/log`) and **share one
6
+ * vocabulary**: the same dotted event name is passed to `logger.*(...)` and
7
+ * `metrics.count(...)` so a log line and its counter line up. Event names are
8
+ * stable, dotted, and queryable. Security-relevant events (a blocked SSRF
9
+ * attempt, a verification rejection) are first-class so being actively probed
10
+ * produces a distinct signal rather than silence. See `spec/observability.md`.
11
+ *
12
+ * @packageDocumentation
13
+ */
14
+ /**
15
+ * Stable event names emitted by `@dwk/websub`. Fields logged alongside each
16
+ * event use `hostFromUrl` for any URL so an attacker-supplied path/query is
17
+ * never recorded; secrets and bodies are never logged.
18
+ */
19
+ export declare const WebSubLogEvent: {
20
+ /**
21
+ * An outbound fetch was refused on SSRF grounds. Fields: `reason`
22
+ * (machine-readable cause), `host` (sanitized, when known).
23
+ */
24
+ readonly SsrfBlocked: "websub.ssrf.blocked";
25
+ /** A subscribe/unsubscribe request passed validation and was enqueued. Fields: `mode`, `callbackHost`, `topicHost`. */
26
+ readonly RequestAccepted: "websub.request.accepted";
27
+ /** A subscribe/unsubscribe request was rejected at validation. Field: `reason`. */
28
+ readonly RequestRejected: "websub.request.rejected";
29
+ /** A publish ping passed validation and distribution was enqueued. Field: `topicHost`. */
30
+ readonly PublishAccepted: "websub.publish.accepted";
31
+ /** A publish ping was rejected at validation. Field: `reason`. */
32
+ readonly PublishRejected: "websub.publish.rejected";
33
+ /** Intent verification of a callback completed. Fields: `mode`, `callbackHost`, `confirmed`, `status`. */
34
+ readonly VerifyCompleted: "websub.verify.completed";
35
+ /** The verification GET to the callback failed/was blocked. Field: `error`. */
36
+ readonly VerifyFetchFailed: "websub.verify.fetch_failed";
37
+ /** A subscription was activated (verified subscribe). Fields: `callbackHost`, `topicHost`, `leaseSeconds`. */
38
+ readonly SubscriptionActivated: "websub.subscription.activated";
39
+ /** A subscription was removed (verified unsubscribe or pruned). Fields: `callbackHost`, `topicHost`, `reason`. */
40
+ readonly SubscriptionRemoved: "websub.subscription.removed";
41
+ /** A subscription was denied (`hub.mode=denied`). Emitted whether or not the callback was reachable; `notified` records whether it accepted the GET. Fields: `callbackHost`, `topicHost`, `notified`, `reason`. */
42
+ readonly SubscriptionDenied: "websub.subscription.denied";
43
+ /** A content-distribution delivery to one subscriber finished. Fields: `callbackHost`, `delivered`, `status`. */
44
+ readonly DeliveryCompleted: "websub.delivery.completed";
45
+ /** A distribution job could not fetch the topic content. Fields: `topicHost`, `status`. */
46
+ readonly TopicFetchFailed: "websub.topic.fetch_failed";
47
+ /** The topic declared no `Content-Type` and no fallback was configured, so distribution was refused rather than mislabeled (WebSub §7). Fields: `topicHost`, `status`. */
48
+ readonly TopicContentTypeMissing: "websub.topic.content_type_missing";
49
+ /** A queue message threw and is being retried. Fields: `kind`, `error`. */
50
+ readonly QueueRetry: "websub.queue.retry";
51
+ };
52
+ /** Union of the event-name string literals in {@link WebSubLogEvent}. */
53
+ export type WebSubLogEvent = (typeof WebSubLogEvent)[keyof typeof WebSubLogEvent];
54
+ //# sourceMappingURL=log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;GAIG;AACH,eAAO,MAAM,cAAc;IACzB;;;OAGG;;IAEH,uHAAuH;;IAEvH,mFAAmF;;IAEnF,0FAA0F;;IAE1F,kEAAkE;;IAElE,0GAA0G;;IAE1G,+EAA+E;;IAE/E,8GAA8G;;IAE9G,kHAAkH;;IAElH,mNAAmN;;IAEnN,iHAAiH;;IAEjH,2FAA2F;;IAE3F,0KAA0K;;IAE1K,2EAA2E;;CAEnE,CAAC;AAEX,yEAAyE;AACzE,MAAM,MAAM,cAAc,GACxB,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,OAAO,cAAc,CAAC,CAAC"}
package/dist/log.js ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * `@dwk/websub` — structured observability event taxonomy.
3
+ *
4
+ * Like the rest of the cohort, this package's logging and metrics are opt-in via
5
+ * an injected {@link Logger} and {@link Metrics} (see `@dwk/log`) and **share one
6
+ * vocabulary**: the same dotted event name is passed to `logger.*(...)` and
7
+ * `metrics.count(...)` so a log line and its counter line up. Event names are
8
+ * stable, dotted, and queryable. Security-relevant events (a blocked SSRF
9
+ * attempt, a verification rejection) are first-class so being actively probed
10
+ * produces a distinct signal rather than silence. See `spec/observability.md`.
11
+ *
12
+ * @packageDocumentation
13
+ */
14
+ /**
15
+ * Stable event names emitted by `@dwk/websub`. Fields logged alongside each
16
+ * event use `hostFromUrl` for any URL so an attacker-supplied path/query is
17
+ * never recorded; secrets and bodies are never logged.
18
+ */
19
+ export const WebSubLogEvent = {
20
+ /**
21
+ * An outbound fetch was refused on SSRF grounds. Fields: `reason`
22
+ * (machine-readable cause), `host` (sanitized, when known).
23
+ */
24
+ SsrfBlocked: "websub.ssrf.blocked",
25
+ /** A subscribe/unsubscribe request passed validation and was enqueued. Fields: `mode`, `callbackHost`, `topicHost`. */
26
+ RequestAccepted: "websub.request.accepted",
27
+ /** A subscribe/unsubscribe request was rejected at validation. Field: `reason`. */
28
+ RequestRejected: "websub.request.rejected",
29
+ /** A publish ping passed validation and distribution was enqueued. Field: `topicHost`. */
30
+ PublishAccepted: "websub.publish.accepted",
31
+ /** A publish ping was rejected at validation. Field: `reason`. */
32
+ PublishRejected: "websub.publish.rejected",
33
+ /** Intent verification of a callback completed. Fields: `mode`, `callbackHost`, `confirmed`, `status`. */
34
+ VerifyCompleted: "websub.verify.completed",
35
+ /** The verification GET to the callback failed/was blocked. Field: `error`. */
36
+ VerifyFetchFailed: "websub.verify.fetch_failed",
37
+ /** A subscription was activated (verified subscribe). Fields: `callbackHost`, `topicHost`, `leaseSeconds`. */
38
+ SubscriptionActivated: "websub.subscription.activated",
39
+ /** A subscription was removed (verified unsubscribe or pruned). Fields: `callbackHost`, `topicHost`, `reason`. */
40
+ SubscriptionRemoved: "websub.subscription.removed",
41
+ /** A subscription was denied (`hub.mode=denied`). Emitted whether or not the callback was reachable; `notified` records whether it accepted the GET. Fields: `callbackHost`, `topicHost`, `notified`, `reason`. */
42
+ SubscriptionDenied: "websub.subscription.denied",
43
+ /** A content-distribution delivery to one subscriber finished. Fields: `callbackHost`, `delivered`, `status`. */
44
+ DeliveryCompleted: "websub.delivery.completed",
45
+ /** A distribution job could not fetch the topic content. Fields: `topicHost`, `status`. */
46
+ TopicFetchFailed: "websub.topic.fetch_failed",
47
+ /** The topic declared no `Content-Type` and no fallback was configured, so distribution was refused rather than mislabeled (WebSub §7). Fields: `topicHost`, `status`. */
48
+ TopicContentTypeMissing: "websub.topic.content_type_missing",
49
+ /** A queue message threw and is being retried. Fields: `kind`, `error`. */
50
+ QueueRetry: "websub.queue.retry",
51
+ };
52
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.js","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B;;;OAGG;IACH,WAAW,EAAE,qBAAqB;IAClC,uHAAuH;IACvH,eAAe,EAAE,yBAAyB;IAC1C,mFAAmF;IACnF,eAAe,EAAE,yBAAyB;IAC1C,0FAA0F;IAC1F,eAAe,EAAE,yBAAyB;IAC1C,kEAAkE;IAClE,eAAe,EAAE,yBAAyB;IAC1C,0GAA0G;IAC1G,eAAe,EAAE,yBAAyB;IAC1C,+EAA+E;IAC/E,iBAAiB,EAAE,4BAA4B;IAC/C,8GAA8G;IAC9G,qBAAqB,EAAE,+BAA+B;IACtD,kHAAkH;IAClH,mBAAmB,EAAE,6BAA6B;IAClD,mNAAmN;IACnN,kBAAkB,EAAE,4BAA4B;IAChD,iHAAiH;IACjH,iBAAiB,EAAE,2BAA2B;IAC9C,2FAA2F;IAC3F,gBAAgB,EAAE,2BAA2B;IAC7C,0KAA0K;IAC1K,uBAAuB,EAAE,mCAAmC;IAC5D,2EAA2E;IAC3E,UAAU,EAAE,oBAAoB;CACxB,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `@dwk/websub` — queued job shapes.
3
+ *
4
+ * The hub does its slow, failure-prone work — the verification-of-intent GET and
5
+ * content-distribution fan-out — off the request path, on a queue with retries
6
+ * and backoff (`spec/packages/websub.md`). Two job kinds flow through the one
7
+ * queue, discriminated by `kind`.
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+ /**
12
+ * Verify a subscriber's intent: GET the callback with `hub.challenge` and,
13
+ * on a confirming 2xx echo, activate (subscribe) or remove (unsubscribe) the
14
+ * subscription. Carries everything needed to persist the subscription so the
15
+ * store write happens only after verification succeeds.
16
+ */
17
+ export interface VerifyJob {
18
+ readonly kind: "verify";
19
+ readonly mode: "subscribe" | "unsubscribe";
20
+ readonly callback: string;
21
+ readonly topic: string;
22
+ /** Lease to grant on a confirmed subscribe (seconds); ignored for unsubscribe. */
23
+ readonly leaseSeconds: number;
24
+ /** Optional HMAC secret registered by the subscriber, stored on activation. */
25
+ readonly secret?: string;
26
+ }
27
+ /**
28
+ * Distribute a topic's current content to every active subscriber: fetch the
29
+ * topic, then POST the body (signed per-subscriber when a secret is set) to each
30
+ * verified callback.
31
+ */
32
+ export interface DistributeJob {
33
+ readonly kind: "distribute";
34
+ readonly topic: string;
35
+ }
36
+ /** A job on the WebSub queue: either intent verification or content distribution. */
37
+ export type WebSubJob = VerifyJob | DistributeJob;
38
+ //# sourceMappingURL=queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue.d.ts","sourceRoot":"","sources":["../src/queue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,WAAW,GAAG,aAAa,CAAC;IAC3C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,kFAAkF;IAClF,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,qFAAqF;AACrF,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,aAAa,CAAC"}
package/dist/queue.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `@dwk/websub` — queued job shapes.
3
+ *
4
+ * The hub does its slow, failure-prone work — the verification-of-intent GET and
5
+ * content-distribution fan-out — off the request path, on a queue with retries
6
+ * and backoff (`spec/packages/websub.md`). Two job kinds flow through the one
7
+ * queue, discriminated by `kind`.
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue.js","sourceRoot":"","sources":["../src/queue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
@@ -0,0 +1,101 @@
1
+ /**
2
+ * `@dwk/websub` — SSRF-safe outbound fetch.
3
+ *
4
+ * A WebSub hub fetches attacker-supplied URLs: the verification GET hits a
5
+ * subscriber-chosen `hub.callback`, and content distribution POSTs to every
6
+ * registered callback. Without guardrails a subscriber could point a callback at
7
+ * the Worker's own network — loopback, the link-local cloud metadata IP
8
+ * (`169.254.169.254`), or RFC 1918 ranges — to exfiltrate credentials or probe
9
+ * internal services. This module is the single choke point every outbound fetch
10
+ * in the package goes through. It:
11
+ *
12
+ * 1. rejects URLs whose host is a private, loopback, link-local, or otherwise
13
+ * non-public address (or a name like `localhost` / `*.internal`),
14
+ * 2. follows redirects manually, re-validating the host on every `Location`
15
+ * hop so a public-looking host cannot 302 to an internal one, and capping
16
+ * the hop count, and
17
+ * 3. bounds the whole operation with a single timeout, so a slow-loris callback
18
+ * cannot pin a queue-consumer invocation.
19
+ *
20
+ * Host validation is purely syntactic on the URL host — DNS rebinding (a name
21
+ * that resolves to a private IP) is out of scope here, as the Workers runtime
22
+ * does not expose name resolution to user code. See `spec/packages/websub.md`
23
+ * and `spec/non-functional-requirements.md`.
24
+ *
25
+ * @packageDocumentation
26
+ */
27
+ import { type Logger, type Metrics } from "@dwk/log";
28
+ import type { FetchLike } from "./fetch";
29
+ /** Default cap on redirect hops before a fetch is abandoned. */
30
+ export declare const DEFAULT_MAX_REDIRECTS = 5;
31
+ /** Default overall timeout (ms) bounding a fetch, redirects included. */
32
+ export declare const DEFAULT_TIMEOUT_MS = 10000;
33
+ /**
34
+ * Machine-readable cause of an {@link SsrfError}, suitable for logging as a
35
+ * structured field (no free-text parsing required).
36
+ */
37
+ export type SsrfReason = "invalid_url" | "disallowed_scheme" | "blocked_host" | "too_many_redirects";
38
+ /**
39
+ * Raised when a request is refused on SSRF grounds (blocked host, disallowed
40
+ * scheme, or too many redirects). Callers catch this exactly like a network
41
+ * failure — a blocked attempt looks the same as an unreachable host — but
42
+ * {@link safeFetch} logs it first (event `websub.ssrf.blocked`) so the single
43
+ * most security-relevant event in the package still produces a signal.
44
+ *
45
+ * Carries the structured {@link reason} and, when known, the sanitized
46
+ * {@link host} so a logger can record them as queryable fields.
47
+ */
48
+ export declare class SsrfError extends Error {
49
+ /** Machine-readable cause. */
50
+ readonly reason: SsrfReason;
51
+ /** The offending host (name plus any port), when one is known. */
52
+ readonly host?: string;
53
+ constructor(message: string, reason: SsrfReason, host?: string);
54
+ }
55
+ /**
56
+ * Decide whether a URL host is private, loopback, link-local, or otherwise
57
+ * not safe to fetch from inside the Worker's network. Accepts the raw
58
+ * `URL.hostname` form (IPv6 hosts may arrive wrapped in `[...]`).
59
+ */
60
+ export declare function isPrivateOrReservedHost(hostname: string): boolean;
61
+ /**
62
+ * Validate that `rawUrl` is a fetchable public `http(s)` URL, returning the
63
+ * parsed {@link URL}. Throws {@link SsrfError} for an unparseable URL, a
64
+ * non-`http(s)` scheme (e.g. `file:`, `javascript:`), or a private/reserved
65
+ * host.
66
+ */
67
+ export declare function assertPublicUrl(rawUrl: string): URL;
68
+ /** Tunables for {@link safeFetch}. */
69
+ export interface SafeFetchOptions {
70
+ /** Maximum redirect hops to follow (default {@link DEFAULT_MAX_REDIRECTS}). */
71
+ readonly maxRedirects?: number;
72
+ /** Overall timeout in ms, redirects included (default {@link DEFAULT_TIMEOUT_MS}). */
73
+ readonly timeoutMs?: number;
74
+ /** Logger for SSRF blocks; defaults to a no-op (see `@dwk/log`). */
75
+ readonly logger?: Logger;
76
+ /** Metrics sink for SSRF-block counters; defaults to a no-op (see `@dwk/log`). */
77
+ readonly metrics?: Metrics;
78
+ }
79
+ /** A completed {@link safeFetch}: the final response and the URL it came from. */
80
+ export interface SafeFetchResult {
81
+ /** The final, non-redirect response. */
82
+ readonly response: Response;
83
+ /** The fully-resolved URL the response came from. */
84
+ readonly url: string;
85
+ }
86
+ /**
87
+ * Fetch `rawUrl` through `doFetch` with SSRF guardrails.
88
+ *
89
+ * The initial host and every redirect target are validated with
90
+ * {@link assertPublicUrl}; redirects are followed manually (`redirect:
91
+ * "manual"`) up to `maxRedirects` hops; and a single {@link AbortSignal.timeout}
92
+ * bounds the whole chain. The request method, headers, and body from `init` are
93
+ * preserved across hops — a redirected `POST` notification re-POSTs to the
94
+ * (re-validated) new location rather than silently degrading to `GET`.
95
+ *
96
+ * @throws {SsrfError} when a host is blocked, a scheme is disallowed, or the
97
+ * redirect cap is exceeded. Other failures (network, timeout) propagate as the
98
+ * underlying fetch rejection. Callers treat any throw as "fetch failed".
99
+ */
100
+ export declare function safeFetch(doFetch: FetchLike, rawUrl: string, init: RequestInit, options?: SafeFetchOptions): Promise<SafeFetchResult>;
101
+ //# sourceMappingURL=safe-fetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-fetch.d.ts","sourceRoot":"","sources":["../src/safe-fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAA2B,KAAK,MAAM,EAAE,KAAK,OAAO,EAAE,MAAM,UAAU,CAAC;AAC9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAGzC,gEAAgE;AAChE,eAAO,MAAM,qBAAqB,IAAI,CAAC;AACvC,yEAAyE;AACzE,eAAO,MAAM,kBAAkB,QAAS,CAAC;AAKzC;;;GAGG;AACH,MAAM,MAAM,UAAU,GAClB,aAAa,GACb,mBAAmB,GACnB,cAAc,GACd,oBAAoB,CAAC;AAEzB;;;;;;;;;GASG;AACH,qBAAa,SAAU,SAAQ,KAAK;IAClC,8BAA8B;IAC9B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,kEAAkE;IAClE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;gBACX,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,MAAM;CAM/D;AAuKD;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAsBjE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,CAsBnD;AAED,sCAAsC;AACtC,MAAM,WAAW,gBAAgB;IAC/B,+EAA+E;IAC/E,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,sFAAsF;IACtF,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,oEAAoE;IACpE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,kFAAkF;IAClF,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,kFAAkF;AAClF,MAAM,WAAW,eAAe;IAC9B,wCAAwC;IACxC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,qDAAqD;IACrD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,SAAS,CAC7B,OAAO,EAAE,SAAS,EAClB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,WAAW,EACjB,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,eAAe,CAAC,CA4E1B"}
@@ -0,0 +1,354 @@
1
+ /**
2
+ * `@dwk/websub` — SSRF-safe outbound fetch.
3
+ *
4
+ * A WebSub hub fetches attacker-supplied URLs: the verification GET hits a
5
+ * subscriber-chosen `hub.callback`, and content distribution POSTs to every
6
+ * registered callback. Without guardrails a subscriber could point a callback at
7
+ * the Worker's own network — loopback, the link-local cloud metadata IP
8
+ * (`169.254.169.254`), or RFC 1918 ranges — to exfiltrate credentials or probe
9
+ * internal services. This module is the single choke point every outbound fetch
10
+ * in the package goes through. It:
11
+ *
12
+ * 1. rejects URLs whose host is a private, loopback, link-local, or otherwise
13
+ * non-public address (or a name like `localhost` / `*.internal`),
14
+ * 2. follows redirects manually, re-validating the host on every `Location`
15
+ * hop so a public-looking host cannot 302 to an internal one, and capping
16
+ * the hop count, and
17
+ * 3. bounds the whole operation with a single timeout, so a slow-loris callback
18
+ * cannot pin a queue-consumer invocation.
19
+ *
20
+ * Host validation is purely syntactic on the URL host — DNS rebinding (a name
21
+ * that resolves to a private IP) is out of scope here, as the Workers runtime
22
+ * does not expose name resolution to user code. See `spec/packages/websub.md`
23
+ * and `spec/non-functional-requirements.md`.
24
+ *
25
+ * @packageDocumentation
26
+ */
27
+ import { noopLogger, noopMetrics } from "@dwk/log";
28
+ import { WebSubLogEvent } from "./log";
29
+ /** Default cap on redirect hops before a fetch is abandoned. */
30
+ export const DEFAULT_MAX_REDIRECTS = 5;
31
+ /** Default overall timeout (ms) bounding a fetch, redirects included. */
32
+ export const DEFAULT_TIMEOUT_MS = 10_000;
33
+ /** HTTP status codes that carry a `Location` we may follow. */
34
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
35
+ /**
36
+ * Raised when a request is refused on SSRF grounds (blocked host, disallowed
37
+ * scheme, or too many redirects). Callers catch this exactly like a network
38
+ * failure — a blocked attempt looks the same as an unreachable host — but
39
+ * {@link safeFetch} logs it first (event `websub.ssrf.blocked`) so the single
40
+ * most security-relevant event in the package still produces a signal.
41
+ *
42
+ * Carries the structured {@link reason} and, when known, the sanitized
43
+ * {@link host} so a logger can record them as queryable fields.
44
+ */
45
+ export class SsrfError extends Error {
46
+ /** Machine-readable cause. */
47
+ reason;
48
+ /** The offending host (name plus any port), when one is known. */
49
+ host;
50
+ constructor(message, reason, host) {
51
+ super(message);
52
+ this.name = "SsrfError";
53
+ this.reason = reason;
54
+ this.host = host;
55
+ }
56
+ }
57
+ /** Parse a canonical dotted-decimal IPv4 host into its four octets. */
58
+ function parseIPv4(host) {
59
+ const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
60
+ if (match === null) {
61
+ return null;
62
+ }
63
+ const octets = [];
64
+ for (let group = 1; group <= 4; group++) {
65
+ const part = match[group];
66
+ if (part === undefined) {
67
+ return null;
68
+ }
69
+ const octet = Number.parseInt(part, 10);
70
+ if (octet > 255) {
71
+ return null;
72
+ }
73
+ octets.push(octet);
74
+ }
75
+ return octets;
76
+ }
77
+ /**
78
+ * True when `octets` falls in a range that must never be fetched from inside
79
+ * the Worker's network: this-network, loopback, link-local (incl. the cloud
80
+ * metadata IP), the RFC 1918 private blocks, CGNAT, IETF protocol/benchmark
81
+ * assignments, and the multicast/reserved/broadcast space.
82
+ */
83
+ function isPrivateIPv4(octets) {
84
+ const [a, b, c] = octets;
85
+ if (a === 0)
86
+ return true; // 0.0.0.0/8 ("this network", incl. 0.0.0.0)
87
+ if (a === 10)
88
+ return true; // 10.0.0.0/8 private
89
+ if (a === 127)
90
+ return true; // 127.0.0.0/8 loopback
91
+ if (a === 100 && b >= 64 && b <= 127)
92
+ return true; // 100.64.0.0/10 CGNAT
93
+ if (a === 169 && b === 254)
94
+ return true; // 169.254.0.0/16 link-local (metadata)
95
+ if (a === 172 && b >= 16 && b <= 31)
96
+ return true; // 172.16.0.0/12 private
97
+ if (a === 192 && b === 0 && c === 0)
98
+ return true; // 192.0.0.0/24 IETF protocol
99
+ if (a === 192 && b === 0 && c === 2)
100
+ return true; // 192.0.2.0/24 TEST-NET-1
101
+ if (a === 192 && b === 168)
102
+ return true; // 192.168.0.0/16 private
103
+ if (a === 198 && b === 51 && c === 100)
104
+ return true; // 198.51.100.0/24 TEST-NET-2
105
+ if (a === 198 && (b === 18 || b === 19))
106
+ return true; // 198.18.0.0/15 benchmark
107
+ if (a === 203 && b === 0 && c === 113)
108
+ return true; // 203.0.113.0/24 TEST-NET-3
109
+ if (a >= 224)
110
+ return true; // 224.0.0.0/4 multicast + 240.0.0.0/4 reserved + broadcast
111
+ return false;
112
+ }
113
+ /**
114
+ * Parse an IPv6 host (brackets already stripped) into its eight 16-bit groups,
115
+ * expanding `::` compression and any trailing embedded IPv4 literal. Returns
116
+ * `null` when `host` is not a valid IPv6 address.
117
+ */
118
+ function parseIPv6(host) {
119
+ if (!host.includes(":")) {
120
+ return null;
121
+ }
122
+ let str = host;
123
+ // Fold a trailing embedded IPv4 literal (e.g. ::ffff:127.0.0.1) into two
124
+ // hex groups so the rest can be parsed uniformly.
125
+ const v4Match = /(?:^|:)((?:\d{1,3}\.){3}\d{1,3})$/.exec(str);
126
+ const v4Str = v4Match?.[1];
127
+ if (v4Str !== undefined) {
128
+ const v4 = parseIPv4(v4Str);
129
+ if (v4 === null) {
130
+ return null;
131
+ }
132
+ const hi = ((v4[0] << 8) | v4[1]).toString(16);
133
+ const lo = ((v4[2] << 8) | v4[3]).toString(16);
134
+ str = `${str.slice(0, str.length - v4Str.length)}${hi}:${lo}`;
135
+ }
136
+ // At most one "::" compression marker is allowed.
137
+ if (str.indexOf("::") !== str.lastIndexOf("::")) {
138
+ return null;
139
+ }
140
+ const toGroups = (part) => {
141
+ if (part === "") {
142
+ return [];
143
+ }
144
+ const groups = [];
145
+ for (const token of part.split(":")) {
146
+ if (!/^[0-9a-fA-F]{1,4}$/.test(token)) {
147
+ return null;
148
+ }
149
+ groups.push(Number.parseInt(token, 16));
150
+ }
151
+ return groups;
152
+ };
153
+ if (str.includes("::")) {
154
+ const parts = str.split("::");
155
+ const left = toGroups(parts[0] ?? "");
156
+ const right = toGroups(parts[1] ?? "");
157
+ if (left === null || right === null) {
158
+ return null;
159
+ }
160
+ const missing = 8 - left.length - right.length;
161
+ if (missing < 1) {
162
+ return null;
163
+ }
164
+ return [...left, ...new Array(missing).fill(0), ...right];
165
+ }
166
+ const all = toGroups(str);
167
+ if (all === null || all.length !== 8) {
168
+ return null;
169
+ }
170
+ return all;
171
+ }
172
+ /**
173
+ * True when `groups` (eight 16-bit values) is an IPv6 address that must never
174
+ * be fetched: unspecified, loopback, link-local, site-local, unique-local,
175
+ * multicast, the documentation prefix, or an address that embeds an IPv4
176
+ * (IPv4-mapped `::ffff:0:0/96`, deprecated IPv4-compatible `::/96`, or NAT64
177
+ * `64:ff9b::/96`) whose embedded IPv4 is itself private.
178
+ */
179
+ function isPrivateIPv6(groups) {
180
+ const first = groups[0] ?? 0;
181
+ const g6 = groups[6] ?? 0;
182
+ const g7 = groups[7] ?? 0;
183
+ if (groups.every((group) => group === 0))
184
+ return true; // :: unspecified
185
+ if (groups.slice(0, 7).every((group) => group === 0) && g7 === 1)
186
+ return true; // ::1 loopback
187
+ if ((first & 0xffc0) === 0xfe80)
188
+ return true; // fe80::/10 link-local
189
+ if ((first & 0xffc0) === 0xfec0)
190
+ return true; // fec0::/10 site-local (deprecated)
191
+ if ((first & 0xfe00) === 0xfc00)
192
+ return true; // fc00::/7 unique local
193
+ if ((first & 0xff00) === 0xff00)
194
+ return true; // ff00::/8 multicast
195
+ if (first === 0x2001 && groups[1] === 0x0db8)
196
+ return true; // 2001:db8::/32 documentation
197
+ // Extract the IPv4 embedded in the low 32 bits.
198
+ const embeddedV4 = [
199
+ g6 >> 8,
200
+ g6 & 0xff,
201
+ g7 >> 8,
202
+ g7 & 0xff,
203
+ ];
204
+ // ::ffff:0:0/96 IPv4-mapped and ::/96 deprecated IPv4-compatible.
205
+ if (groups.slice(0, 5).every((group) => group === 0) &&
206
+ (groups[5] === 0xffff || groups[5] === 0x0000)) {
207
+ return isPrivateIPv4(embeddedV4);
208
+ }
209
+ // 64:ff9b::/96 NAT64 well-known prefix.
210
+ if (first === 0x0064 &&
211
+ groups[1] === 0xff9b &&
212
+ groups.slice(2, 6).every((group) => group === 0)) {
213
+ return isPrivateIPv4(embeddedV4);
214
+ }
215
+ return false;
216
+ }
217
+ /** Hostnames (non-IP) that are never public and must never be fetched. */
218
+ function isBlockedHostname(host) {
219
+ const lower = host.toLowerCase();
220
+ return (lower === "localhost" ||
221
+ lower.endsWith(".localhost") ||
222
+ lower.endsWith(".local") ||
223
+ lower.endsWith(".internal"));
224
+ }
225
+ /**
226
+ * Decide whether a URL host is private, loopback, link-local, or otherwise
227
+ * not safe to fetch from inside the Worker's network. Accepts the raw
228
+ * `URL.hostname` form (IPv6 hosts may arrive wrapped in `[...]`).
229
+ */
230
+ export function isPrivateOrReservedHost(hostname) {
231
+ if (hostname === "") {
232
+ return true;
233
+ }
234
+ // Strip IPv6 brackets and a trailing dot. A trailing dot makes a name an
235
+ // FQDN that still resolves (e.g. `localhost.` → 127.0.0.1) but would slip
236
+ // past the string checks below if left in place.
237
+ const host = (hostname.startsWith("[") && hostname.endsWith("]")
238
+ ? hostname.slice(1, -1)
239
+ : hostname).replace(/\.$/, "");
240
+ const v4 = parseIPv4(host);
241
+ if (v4 !== null) {
242
+ return isPrivateIPv4(v4);
243
+ }
244
+ const v6 = parseIPv6(host);
245
+ if (v6 !== null) {
246
+ return isPrivateIPv6(v6);
247
+ }
248
+ return isBlockedHostname(host);
249
+ }
250
+ /**
251
+ * Validate that `rawUrl` is a fetchable public `http(s)` URL, returning the
252
+ * parsed {@link URL}. Throws {@link SsrfError} for an unparseable URL, a
253
+ * non-`http(s)` scheme (e.g. `file:`, `javascript:`), or a private/reserved
254
+ * host.
255
+ */
256
+ export function assertPublicUrl(rawUrl) {
257
+ let url;
258
+ try {
259
+ url = new URL(rawUrl);
260
+ }
261
+ catch {
262
+ throw new SsrfError(`invalid URL: ${rawUrl}`, "invalid_url");
263
+ }
264
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
265
+ throw new SsrfError(`disallowed scheme: ${url.protocol}`, "disallowed_scheme", url.hostname);
266
+ }
267
+ if (isPrivateOrReservedHost(url.hostname)) {
268
+ throw new SsrfError(`blocked host: ${url.hostname}`, "blocked_host", url.hostname);
269
+ }
270
+ return url;
271
+ }
272
+ /**
273
+ * Fetch `rawUrl` through `doFetch` with SSRF guardrails.
274
+ *
275
+ * The initial host and every redirect target are validated with
276
+ * {@link assertPublicUrl}; redirects are followed manually (`redirect:
277
+ * "manual"`) up to `maxRedirects` hops; and a single {@link AbortSignal.timeout}
278
+ * bounds the whole chain. The request method, headers, and body from `init` are
279
+ * preserved across hops — a redirected `POST` notification re-POSTs to the
280
+ * (re-validated) new location rather than silently degrading to `GET`.
281
+ *
282
+ * @throws {SsrfError} when a host is blocked, a scheme is disallowed, or the
283
+ * redirect cap is exceeded. Other failures (network, timeout) propagate as the
284
+ * underlying fetch rejection. Callers treat any throw as "fetch failed".
285
+ */
286
+ export async function safeFetch(doFetch, rawUrl, init, options) {
287
+ const maxRedirects = options?.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
288
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
289
+ const logger = options?.logger ?? noopLogger;
290
+ const metrics = options?.metrics ?? noopMetrics;
291
+ // Bound the chain with our own timeout, but don't clobber a caller's signal
292
+ // (e.g. a worker-shutdown abort): combine them so either can cancel.
293
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
294
+ const signal = init.signal != null
295
+ ? AbortSignal.any([init.signal, timeoutSignal])
296
+ : timeoutSignal;
297
+ // A blocked request is the single most security-relevant event here, so log
298
+ // it (with its structured reason + sanitized host) before re-throwing — an
299
+ // operator being actively probed sees a distinct signal instead of silence.
300
+ try {
301
+ let currentUrl = assertPublicUrl(rawUrl).toString();
302
+ let currentInit = { ...init };
303
+ for (let hop = 0;; hop++) {
304
+ const response = await doFetch(currentUrl, {
305
+ ...currentInit,
306
+ redirect: "manual",
307
+ signal,
308
+ });
309
+ if (!REDIRECT_STATUSES.has(response.status)) {
310
+ return { response, url: currentUrl };
311
+ }
312
+ const location = response.headers.get("location");
313
+ if (location === null || location === "") {
314
+ // A redirect with nothing to follow — hand back as the final response.
315
+ return { response, url: currentUrl };
316
+ }
317
+ if (hop >= maxRedirects) {
318
+ throw new SsrfError(`too many redirects (> ${maxRedirects})`, "too_many_redirects", new URL(currentUrl).host);
319
+ }
320
+ // Resolve the next hop against the current URL and re-validate its host
321
+ // before following — a public host must not be able to bounce us inward.
322
+ const next = assertPublicUrl(new URL(location, currentUrl).toString());
323
+ // Drain the redirect body so the connection can be reused/closed.
324
+ await response.body?.cancel().catch(() => undefined);
325
+ // Strip credential-bearing headers on a cross-origin hop, matching what a
326
+ // browser's `fetch` does, so a redirect cannot leak them to a new origin.
327
+ if (currentInit.headers && new URL(currentUrl).origin !== next.origin) {
328
+ const headers = new Headers(currentInit.headers);
329
+ for (const name of [
330
+ "authorization",
331
+ "cookie",
332
+ "cookie2",
333
+ "proxy-authorization",
334
+ "set-cookie",
335
+ "x-hub-signature",
336
+ ]) {
337
+ headers.delete(name);
338
+ }
339
+ currentInit = { ...currentInit, headers };
340
+ }
341
+ currentUrl = next.toString();
342
+ }
343
+ }
344
+ catch (err) {
345
+ if (err instanceof SsrfError) {
346
+ const fields = { reason: err.reason, host: err.host };
347
+ logger.warn(WebSubLogEvent.SsrfBlocked, fields);
348
+ // Mirror the log as a counter so "SSRF blocks/min by reason" is chartable.
349
+ metrics.count(WebSubLogEvent.SsrfBlocked, fields);
350
+ }
351
+ throw err;
352
+ }
353
+ }
354
+ //# sourceMappingURL=safe-fetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-fetch.js","sourceRoot":"","sources":["../src/safe-fetch.ts"],"names":[],"mappings":"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"}