@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.
- package/LICENSE +15 -0
- package/README.md +155 -0
- package/dist/config.d.ts +122 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +96 -0
- package/dist/config.js.map +1 -0
- package/dist/consumer.d.ts +39 -0
- package/dist/consumer.d.ts.map +1 -0
- package/dist/consumer.js +146 -0
- package/dist/consumer.js.map +1 -0
- package/dist/distribute.d.ts +109 -0
- package/dist/distribute.d.ts.map +1 -0
- package/dist/distribute.js +140 -0
- package/dist/distribute.js.map +1 -0
- package/dist/fetch.d.ts +28 -0
- package/dist/fetch.d.ts.map +1 -0
- package/dist/fetch.js +73 -0
- package/dist/fetch.js.map +1 -0
- package/dist/handler.d.ts +43 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +127 -0
- package/dist/handler.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/log.d.ts +54 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +52 -0
- package/dist/log.js.map +1 -0
- package/dist/queue.d.ts +38 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/queue.js +12 -0
- package/dist/queue.js.map +1 -0
- package/dist/safe-fetch.d.ts +101 -0
- package/dist/safe-fetch.d.ts.map +1 -0
- package/dist/safe-fetch.js +354 -0
- package/dist/safe-fetch.js.map +1 -0
- package/dist/store.d.ts +61 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +110 -0
- package/dist/store.js.map +1 -0
- package/dist/validate.d.ts +67 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +106 -0
- package/dist/validate.js.map +1 -0
- package/dist/verify.d.ts +85 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +149 -0
- package/dist/verify.js.map +1 -0
- package/package.json +46 -0
- package/src/config.ts +199 -0
- package/src/consumer.ts +187 -0
- package/src/distribute.ts +257 -0
- package/src/fetch.ts +84 -0
- package/src/handler.ts +163 -0
- package/src/index.ts +98 -0
- package/src/log.ts +56 -0
- package/src/queue.ts +40 -0
- package/src/safe-fetch.ts +412 -0
- package/src/store.ts +190 -0
- package/src/validate.ts +179 -0
- package/src/verify.ts +229 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 David W. Keith
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# `@dwk/websub`
|
|
2
|
+
|
|
3
|
+
> WebSub (W3C) hub. Endpoint package.
|
|
4
|
+
|
|
5
|
+
Part of the [`@dwk` IndieWeb + Solid cohort](../../README.md). See the
|
|
6
|
+
[package specification](../../spec/packages/websub.md) for the full
|
|
7
|
+
requirements.
|
|
8
|
+
|
|
9
|
+
A WebSub hub is the **publish-side, real-time complement** to
|
|
10
|
+
[`@dwk/webmention`](../webmention)'s interaction side: subscribers receive a
|
|
11
|
+
push when the user's feed changes instead of polling. Subscribe/unsubscribe
|
|
12
|
+
requests are validated **synchronously** (fast `202 Accepted`); the
|
|
13
|
+
verification-of-intent callback and content fan-out run **asynchronously** on a
|
|
14
|
+
queue with retries and backoff. The subscription store is **D1 (strongly
|
|
15
|
+
consistent), never KV** — a stale or lost subscription is a correctness bug. On
|
|
16
|
+
publish the hub fetches the topic and POSTs it to every verified callback,
|
|
17
|
+
signing the body with HMAC-SHA256 (`X-Hub-Signature`) when the subscriber
|
|
18
|
+
registered a secret.
|
|
19
|
+
|
|
20
|
+
## What this package does *not* do
|
|
21
|
+
|
|
22
|
+
The **feeds themselves are static** — RSS / Atom / JSON Feed and `h-feed` /
|
|
23
|
+
`h-entry` microformats are SSG build artifacts that **Anglesite generates**, so
|
|
24
|
+
there is no `@dwk/feeds` package and feed generation is out of scope here. The
|
|
25
|
+
feed's `Link rel="hub"` / `rel="self"` advertisement is likewise Anglesite's to
|
|
26
|
+
emit. This package is only the dynamic hub a static host can't provide.
|
|
27
|
+
|
|
28
|
+
## Hub endpoint
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { createWebSub } from "@dwk/websub";
|
|
32
|
+
|
|
33
|
+
const websub = createWebSub({
|
|
34
|
+
baseUrl: "https://hub.example.com",
|
|
35
|
+
// The feeds this hub serves; a subscribe/publish for any other topic is 400.
|
|
36
|
+
allowedTopics: ["https://example.com/feed.xml"],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// In your Worker's fetch handler, mount under any path prefix:
|
|
40
|
+
// POST /websub (application/x-www-form-urlencoded)
|
|
41
|
+
return websub(request, env, ctx);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`createWebSub` parses the form body and routes on `hub.mode`:
|
|
45
|
+
|
|
46
|
+
- **`subscribe` / `unsubscribe`** — validates `hub.callback`, `hub.topic`,
|
|
47
|
+
optional `hub.lease_seconds` (clamped into the hub's bounds), and optional
|
|
48
|
+
`hub.secret` (< 200 bytes); enqueues a verification job and returns `202`.
|
|
49
|
+
- **`publish`** — validates `hub.url` (or legacy `hub.topic`) against the
|
|
50
|
+
allowed topics; enqueues a distribution job and returns `202`.
|
|
51
|
+
|
|
52
|
+
Invalid requests get `400` with a stable error code in the body; other methods
|
|
53
|
+
get `405`. The handler **fails loudly** if the required `WEBSUB_QUEUE` binding
|
|
54
|
+
is missing.
|
|
55
|
+
|
|
56
|
+
### Async work (queue consumer)
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { createWebSub, createWebSubQueueConsumer } from "@dwk/websub";
|
|
60
|
+
|
|
61
|
+
const config = {
|
|
62
|
+
baseUrl: "https://hub.example.com",
|
|
63
|
+
allowedTopics: ["https://example.com/feed.xml"],
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export default {
|
|
67
|
+
fetch: createWebSub(config),
|
|
68
|
+
queue: createWebSubQueueConsumer(config), // bound to WEBSUB_QUEUE
|
|
69
|
+
};
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The consumer handles both job kinds:
|
|
73
|
+
|
|
74
|
+
- **verify** — issues the `hub.challenge` GET to the callback; on a confirming
|
|
75
|
+
`2xx` echo it activates the subscription (subscribe) or removes it
|
|
76
|
+
(unsubscribe). A subscription row is written **only after** verification
|
|
77
|
+
succeeds, so an unverified callback never lands in the store.
|
|
78
|
+
- **distribute** — prunes expired leases, fetches the topic's current content,
|
|
79
|
+
and POSTs it (signed per-subscriber when a secret is set) to every active
|
|
80
|
+
callback.
|
|
81
|
+
|
|
82
|
+
A store/queue failure — or a distribution that can't fetch the topic — is
|
|
83
|
+
retried; everything else is acked.
|
|
84
|
+
|
|
85
|
+
### Publishing from your own write path
|
|
86
|
+
|
|
87
|
+
To ping the hub in-process when [`@dwk/micropub`](../micropub) writes or a static
|
|
88
|
+
rebuild finishes — instead of POSTing the HTTP publish endpoint — use the
|
|
89
|
+
notifier:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { createPublishNotifier } from "@dwk/websub";
|
|
93
|
+
|
|
94
|
+
const notifyPublish = createPublishNotifier(config);
|
|
95
|
+
// After a write that changed the feed:
|
|
96
|
+
await notifyPublish(env, "https://example.com/feed.xml");
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Bindings (`Env` fragment)
|
|
100
|
+
|
|
101
|
+
| Binding | Type | Required | Purpose |
|
|
102
|
+
| -------------- | ------------ | -------- | ------------------------------------------------ |
|
|
103
|
+
| `WEBSUB_DB` | `D1Database` | yes | Strongly-consistent subscription store. |
|
|
104
|
+
| `WEBSUB_QUEUE` | `Queue` | yes | Verification + distribution fan-out and retries. |
|
|
105
|
+
|
|
106
|
+
The subscription store **MUST** be D1 (or another strongly-consistent store),
|
|
107
|
+
**never KV**: staleness here is a correctness/security bug, not a safe-to-be-stale
|
|
108
|
+
cache (see [`spec/non-functional-requirements.md`](../../spec/non-functional-requirements.md)).
|
|
109
|
+
|
|
110
|
+
## Config
|
|
111
|
+
|
|
112
|
+
| Field | Type | Default | Purpose |
|
|
113
|
+
| --------------------- | ----------------------------- | ---------------- | ------------------------------------------------------------- |
|
|
114
|
+
| `baseUrl` | `string` | — | Hub base URL. |
|
|
115
|
+
| `hubUrl` | `string` | `baseUrl` | URL advertised in the `rel="hub"` `Link` header. |
|
|
116
|
+
| `allowedTopics` | `string[]` | — | Topics this hub serves (normalized match). |
|
|
117
|
+
| `isAllowedTopic` | `(topic) => boolean` | from `allowed…` | Predicate alternative for a dynamic feed set. |
|
|
118
|
+
| `minLeaseSeconds` | `number` | `300` | Lower bound on a granted lease. |
|
|
119
|
+
| `maxLeaseSeconds` | `number` | `864000` | Upper bound on a granted lease (10 days). |
|
|
120
|
+
| `defaultLeaseSeconds` | `number` | `864000` | Lease when the subscriber omits `hub.lease_seconds`. |
|
|
121
|
+
| `fetch` | `FetchLike` | global `fetch` | Override `fetch` (verification / distribution). |
|
|
122
|
+
| `logger` | `Logger` | `noopLogger` | Structured logs (see `@dwk/log`). |
|
|
123
|
+
| `metrics` | `Metrics` | `noopMetrics` | Queryable counters (see `@dwk/log`). |
|
|
124
|
+
|
|
125
|
+
Either `allowedTopics` or `isAllowedTopic` is required — a hub must know which
|
|
126
|
+
topics it serves.
|
|
127
|
+
|
|
128
|
+
## Security
|
|
129
|
+
|
|
130
|
+
Every outbound request — the verification GET and each distribution POST — goes
|
|
131
|
+
through an SSRF-safe `fetch`: the callback host (and every redirect hop) is
|
|
132
|
+
validated against private, loopback, link-local (incl. the cloud metadata IP),
|
|
133
|
+
and reserved ranges, redirects are followed manually with re-validation and a
|
|
134
|
+
hop cap, and the whole operation is bounded by a timeout. Credential-bearing
|
|
135
|
+
headers (including `X-Hub-Signature`) are stripped on a cross-origin redirect.
|
|
136
|
+
|
|
137
|
+
## Observability
|
|
138
|
+
|
|
139
|
+
Logging and metrics are **opt-in and injected** (see [`@dwk/log`](../log)),
|
|
140
|
+
defaulting to no-ops. Both seams share one event vocabulary (`WebSubLogEvent`),
|
|
141
|
+
so a log line and its counter line up — SSRF blocks (by reason), request
|
|
142
|
+
accepted/rejected, verification outcomes (by confirmed/status), subscription
|
|
143
|
+
activated/removed, deliveries (by delivered/status), topic-fetch failures, and
|
|
144
|
+
queue retries. Only sanitized hosts, status, reason codes, booleans, and counts
|
|
145
|
+
are recorded — never secrets, bodies, or full URLs.
|
|
146
|
+
|
|
147
|
+
## Conformance
|
|
148
|
+
|
|
149
|
+
The hub targets the [websub.rocks](https://websub.rocks/) hub test suite. The
|
|
150
|
+
lease math, request validation, intent verification, and HMAC signing are
|
|
151
|
+
unit-tested without a Workers runtime; the D1 store is tested under Miniflare.
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
[ISC](../../LICENSE)
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/websub` — injected configuration and the Cloudflare `Env` fragment.
|
|
3
|
+
*
|
|
4
|
+
* Per the composition contract, a package never reads the global environment
|
|
5
|
+
* directly: all config (base URL, the topics this hub serves, lease bounds) is
|
|
6
|
+
* passed into {@link createWebSub}, so the hub can be instantiated multiple times
|
|
7
|
+
* and unit-tested in isolation. The Cloudflare bindings the hub needs are
|
|
8
|
+
* declared as a TypeScript `Env` fragment that the composed Worker's `Env` is a
|
|
9
|
+
* superset of. See `spec/composition-contract.md` and `spec/packages/websub.md`.
|
|
10
|
+
*
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
import { type Logger, type Metrics } from "@dwk/log";
|
|
14
|
+
import type { D1Database, Queue } from "@cloudflare/workers-types";
|
|
15
|
+
import { type SignatureAlgorithm } from "./distribute";
|
|
16
|
+
import type { FetchLike } from "./fetch";
|
|
17
|
+
import type { WebSubJob } from "./queue";
|
|
18
|
+
/**
|
|
19
|
+
* Cloudflare bindings required by the hub handler and its queue consumer.
|
|
20
|
+
*
|
|
21
|
+
* The subscription store **MUST** be strongly consistent — D1 (session
|
|
22
|
+
* consistency), never KV: a stale or lost subscription is a correctness bug, not
|
|
23
|
+
* a safe-to-be-stale cache (`spec/non-functional-requirements.md`).
|
|
24
|
+
*/
|
|
25
|
+
export interface WebSubEnv {
|
|
26
|
+
/** D1 database backing the subscription table. */
|
|
27
|
+
readonly WEBSUB_DB: D1Database;
|
|
28
|
+
/** Queue for intent verification and content-distribution fan-out + retries. */
|
|
29
|
+
readonly WEBSUB_QUEUE: Queue<WebSubJob>;
|
|
30
|
+
}
|
|
31
|
+
/** Configuration passed to {@link createWebSub}. */
|
|
32
|
+
export interface WebSubConfig {
|
|
33
|
+
/** Base URL of this hub; informational and used for self-advertisement. */
|
|
34
|
+
readonly baseUrl: string;
|
|
35
|
+
/**
|
|
36
|
+
* Absolute URL of the hub endpoint itself, advertised to subscribers in the
|
|
37
|
+
* `rel="hub"` `Link` header on each delivery. Defaults to {@link baseUrl};
|
|
38
|
+
* set it when the hub is mounted under a path prefix.
|
|
39
|
+
*/
|
|
40
|
+
readonly hubUrl?: string;
|
|
41
|
+
/**
|
|
42
|
+
* The topic URLs this hub will serve (the user's own feeds). A subscribe or
|
|
43
|
+
* publish request for any other topic is rejected. Compared by normalized URL
|
|
44
|
+
* (default ports and a trailing-slash-only path are folded). Provide either
|
|
45
|
+
* this or {@link isAllowedTopic}.
|
|
46
|
+
*/
|
|
47
|
+
readonly allowedTopics?: readonly string[];
|
|
48
|
+
/**
|
|
49
|
+
* Predicate deciding whether a topic URL is one this hub serves, for callers
|
|
50
|
+
* whose feed set is dynamic. Takes precedence over {@link allowedTopics}.
|
|
51
|
+
*/
|
|
52
|
+
readonly isAllowedTopic?: (topic: string) => boolean;
|
|
53
|
+
/** Minimum lease (seconds) the hub will grant. Default 300 (5 minutes). */
|
|
54
|
+
readonly minLeaseSeconds?: number;
|
|
55
|
+
/** Maximum lease (seconds) the hub will grant. Default 864000 (10 days). */
|
|
56
|
+
readonly maxLeaseSeconds?: number;
|
|
57
|
+
/**
|
|
58
|
+
* Lease granted when a subscriber omits `hub.lease_seconds`. Default 864000
|
|
59
|
+
* (10 days). Clamped into `[minLeaseSeconds, maxLeaseSeconds]`.
|
|
60
|
+
*/
|
|
61
|
+
readonly defaultLeaseSeconds?: number;
|
|
62
|
+
/**
|
|
63
|
+
* HMAC digest method for the `X-Hub-Signature` on signed deliveries (WebSub
|
|
64
|
+
* §8 permits `sha1`/`sha256`/`sha384`/`sha512`). WebSub has no per-request
|
|
65
|
+
* method parameter, so this is a hub-level choice; it defaults to the secure
|
|
66
|
+
* `sha256`. Set it to `sha1` **only** when a subscriber requires the legacy
|
|
67
|
+
* method for interop — SHA-1 is weaker and not the default for that reason.
|
|
68
|
+
*/
|
|
69
|
+
readonly signatureAlgorithm?: SignatureAlgorithm;
|
|
70
|
+
/**
|
|
71
|
+
* Media type to forward on distribution when the topic response declares no
|
|
72
|
+
* `Content-Type`. WebSub §7 requires the distribution `Content-Type` to
|
|
73
|
+
* correspond to the topic's, so the hub never fabricates a generic
|
|
74
|
+
* `application/octet-stream`. Set this to the type the hub's feeds are served
|
|
75
|
+
* as (e.g. `application/atom+xml`) so a topic that omits the header is still
|
|
76
|
+
* distributable; left unset, such a topic is refused rather than mislabeled.
|
|
77
|
+
*/
|
|
78
|
+
readonly defaultContentType?: string;
|
|
79
|
+
/** `fetch` implementation for verification/distribution; defaults to global `fetch`. */
|
|
80
|
+
readonly fetch?: FetchLike;
|
|
81
|
+
/** Logger; defaults to a no-op (see `@dwk/log`). */
|
|
82
|
+
readonly logger?: Logger;
|
|
83
|
+
/** Metrics sink; defaults to a no-op (see `@dwk/log`). */
|
|
84
|
+
readonly metrics?: Metrics;
|
|
85
|
+
}
|
|
86
|
+
/** Default lower bound on a granted lease (5 minutes). */
|
|
87
|
+
export declare const DEFAULT_MIN_LEASE_SECONDS = 300;
|
|
88
|
+
/** Default upper bound on a granted lease (10 days). */
|
|
89
|
+
export declare const DEFAULT_MAX_LEASE_SECONDS = 864000;
|
|
90
|
+
/** Config with defaults resolved and the topic check normalized to a predicate. */
|
|
91
|
+
export interface ResolvedConfig {
|
|
92
|
+
readonly baseUrl: string;
|
|
93
|
+
readonly hubUrl: string;
|
|
94
|
+
readonly isAllowedTopic: (topic: string) => boolean;
|
|
95
|
+
readonly minLeaseSeconds: number;
|
|
96
|
+
readonly maxLeaseSeconds: number;
|
|
97
|
+
readonly defaultLeaseSeconds: number;
|
|
98
|
+
readonly signatureAlgorithm: SignatureAlgorithm;
|
|
99
|
+
/** Fallback distribution `Content-Type`; `undefined` refuses to mislabel. */
|
|
100
|
+
readonly defaultContentType?: string;
|
|
101
|
+
readonly fetch: FetchLike;
|
|
102
|
+
readonly logger: Logger;
|
|
103
|
+
readonly metrics: Metrics;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Normalize a topic URL for comparison: lowercase host, default port dropped,
|
|
107
|
+
* a bare `/` path elided, and fragment removed. Query strings are preserved
|
|
108
|
+
* (a feed may legitimately carry one). Returns `null` when `raw` is unparseable
|
|
109
|
+
* or not `http(s)`.
|
|
110
|
+
*/
|
|
111
|
+
export declare function normalizeTopic(raw: string): string | null;
|
|
112
|
+
/**
|
|
113
|
+
* Resolve {@link WebSubConfig} into {@link ResolvedConfig}, filling defaults and
|
|
114
|
+
* turning the topic allowlist into a single predicate.
|
|
115
|
+
*
|
|
116
|
+
* @throws when neither `allowedTopics` nor `isAllowedTopic` is supplied — a hub
|
|
117
|
+
* with no topics would accept subscriptions it can never serve.
|
|
118
|
+
*/
|
|
119
|
+
export declare function resolveConfig(config: WebSubConfig): ResolvedConfig;
|
|
120
|
+
/** Clamp a requested lease into the hub's `[min, max]` bounds. */
|
|
121
|
+
export declare function clampLease(requested: number, config: ResolvedConfig): number;
|
|
122
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAA2B,KAAK,MAAM,EAAE,KAAK,OAAO,EAAE,MAAM,UAAU,CAAC;AAC9E,OAAO,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC;;;;;;GAMG;AACH,MAAM,WAAW,SAAS;IACxB,kDAAkD;IAClD,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC;IAC/B,gFAAgF;IAChF,QAAQ,CAAC,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;CACzC;AAED,oDAAoD;AACpD,MAAM,WAAW,YAAY;IAC3B,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;IACrD,2EAA2E;IAC3E,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,4EAA4E;IAC5E,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC;;;OAGG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IACtC;;;;;;OAMG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACjD;;;;;;;OAOG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,wFAAwF;IACxF,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC;IAC3B,oDAAoD;IACpD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,0DAA0D;IAC1D,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,0DAA0D;AAC1D,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAC7C,wDAAwD;AACxD,eAAO,MAAM,yBAAyB,SAAU,CAAC;AAEjD,mFAAmF;AACnF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;IACpD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,kBAAkB,EAAE,kBAAkB,CAAC;IAChD,6EAA6E;IAC7E,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAazD;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,cAAc,CAiDlE;AAED,kEAAkE;AAClE,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,MAAM,CAK5E"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/websub` — injected configuration and the Cloudflare `Env` fragment.
|
|
3
|
+
*
|
|
4
|
+
* Per the composition contract, a package never reads the global environment
|
|
5
|
+
* directly: all config (base URL, the topics this hub serves, lease bounds) is
|
|
6
|
+
* passed into {@link createWebSub}, so the hub can be instantiated multiple times
|
|
7
|
+
* and unit-tested in isolation. The Cloudflare bindings the hub needs are
|
|
8
|
+
* declared as a TypeScript `Env` fragment that the composed Worker's `Env` is a
|
|
9
|
+
* superset of. See `spec/composition-contract.md` and `spec/packages/websub.md`.
|
|
10
|
+
*
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
import { noopLogger, noopMetrics } from "@dwk/log";
|
|
14
|
+
import { DEFAULT_SIGNATURE_ALGORITHM, } from "./distribute";
|
|
15
|
+
/** Default lower bound on a granted lease (5 minutes). */
|
|
16
|
+
export const DEFAULT_MIN_LEASE_SECONDS = 300;
|
|
17
|
+
/** Default upper bound on a granted lease (10 days). */
|
|
18
|
+
export const DEFAULT_MAX_LEASE_SECONDS = 864_000;
|
|
19
|
+
/**
|
|
20
|
+
* Normalize a topic URL for comparison: lowercase host, default port dropped,
|
|
21
|
+
* a bare `/` path elided, and fragment removed. Query strings are preserved
|
|
22
|
+
* (a feed may legitimately carry one). Returns `null` when `raw` is unparseable
|
|
23
|
+
* or not `http(s)`.
|
|
24
|
+
*/
|
|
25
|
+
export function normalizeTopic(raw) {
|
|
26
|
+
let url;
|
|
27
|
+
try {
|
|
28
|
+
url = new URL(raw);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
url.hash = "";
|
|
37
|
+
const path = url.pathname === "/" ? "" : url.pathname;
|
|
38
|
+
return `${url.protocol}//${url.host}${path}${url.search}`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve {@link WebSubConfig} into {@link ResolvedConfig}, filling defaults and
|
|
42
|
+
* turning the topic allowlist into a single predicate.
|
|
43
|
+
*
|
|
44
|
+
* @throws when neither `allowedTopics` nor `isAllowedTopic` is supplied — a hub
|
|
45
|
+
* with no topics would accept subscriptions it can never serve.
|
|
46
|
+
*/
|
|
47
|
+
export function resolveConfig(config) {
|
|
48
|
+
const min = config.minLeaseSeconds ?? DEFAULT_MIN_LEASE_SECONDS;
|
|
49
|
+
const max = config.maxLeaseSeconds ?? DEFAULT_MAX_LEASE_SECONDS;
|
|
50
|
+
if (min > max) {
|
|
51
|
+
throw new Error(`@dwk/websub: minLeaseSeconds (${min}) exceeds maxLeaseSeconds (${max}).`);
|
|
52
|
+
}
|
|
53
|
+
const fallback = config.defaultLeaseSeconds ?? DEFAULT_MAX_LEASE_SECONDS;
|
|
54
|
+
const defaultLeaseSeconds = Math.min(Math.max(fallback, min), max);
|
|
55
|
+
let isAllowedTopic;
|
|
56
|
+
if (config.isAllowedTopic !== undefined) {
|
|
57
|
+
isAllowedTopic = config.isAllowedTopic;
|
|
58
|
+
}
|
|
59
|
+
else if (config.allowedTopics !== undefined) {
|
|
60
|
+
const allowed = new Set();
|
|
61
|
+
for (const topic of config.allowedTopics) {
|
|
62
|
+
const normalized = normalizeTopic(topic);
|
|
63
|
+
if (normalized !== null) {
|
|
64
|
+
allowed.add(normalized);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
isAllowedTopic = (topic) => {
|
|
68
|
+
const normalized = normalizeTopic(topic);
|
|
69
|
+
return normalized !== null && allowed.has(normalized);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
throw new Error("@dwk/websub: configure allowedTopics or isAllowedTopic — a hub must " +
|
|
74
|
+
"know which topics it serves.");
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
baseUrl: config.baseUrl,
|
|
78
|
+
hubUrl: config.hubUrl ?? config.baseUrl,
|
|
79
|
+
isAllowedTopic,
|
|
80
|
+
minLeaseSeconds: min,
|
|
81
|
+
maxLeaseSeconds: max,
|
|
82
|
+
defaultLeaseSeconds,
|
|
83
|
+
signatureAlgorithm: config.signatureAlgorithm ?? DEFAULT_SIGNATURE_ALGORITHM,
|
|
84
|
+
...(config.defaultContentType !== undefined
|
|
85
|
+
? { defaultContentType: config.defaultContentType }
|
|
86
|
+
: {}),
|
|
87
|
+
fetch: config.fetch ?? ((input, init) => fetch(input, init)),
|
|
88
|
+
logger: config.logger ?? noopLogger,
|
|
89
|
+
metrics: config.metrics ?? noopMetrics,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** Clamp a requested lease into the hub's `[min, max]` bounds. */
|
|
93
|
+
export function clampLease(requested, config) {
|
|
94
|
+
return Math.min(Math.max(requested, config.minLeaseSeconds), config.maxLeaseSeconds);
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/websub` — the queue consumer.
|
|
3
|
+
*
|
|
4
|
+
* The hub's slow work runs here, off the request path, with the queue providing
|
|
5
|
+
* retries and backoff. Two job kinds flow through:
|
|
6
|
+
*
|
|
7
|
+
* - **verify** — issue the verification-of-intent GET; on a confirmed subscribe,
|
|
8
|
+
* write the subscription to the D1 store with its lease expiry, and on a
|
|
9
|
+
* confirmed unsubscribe, remove it. A subscription row is created only after
|
|
10
|
+
* verification succeeds, so an unverified callback never lands in the store.
|
|
11
|
+
* - **distribute** — prune expired leases, fetch the topic's current content, and
|
|
12
|
+
* fan it out (signed per-subscriber when a secret is set) to every active
|
|
13
|
+
* subscriber.
|
|
14
|
+
*
|
|
15
|
+
* A job whose store/fetch work throws — or a distribution that cannot fetch the
|
|
16
|
+
* topic — is retried; everything else is acked. See `spec/packages/websub.md`.
|
|
17
|
+
*
|
|
18
|
+
* @packageDocumentation
|
|
19
|
+
*/
|
|
20
|
+
import type { ExecutionContext, MessageBatch } from "@cloudflare/workers-types";
|
|
21
|
+
import { type WebSubConfig, type WebSubEnv } from "./config";
|
|
22
|
+
import type { WebSubJob } from "./queue";
|
|
23
|
+
import { type SubscriptionStore } from "./store";
|
|
24
|
+
/** A Queue consumer for WebSub verification and distribution jobs. */
|
|
25
|
+
export type WebSubQueueConsumer = (batch: MessageBatch<WebSubJob>, env: WebSubEnv, ctx: ExecutionContext) => Promise<void>;
|
|
26
|
+
/** Extra wiring for the consumer, primarily to inject a store in tests. */
|
|
27
|
+
export interface ConsumerOptions {
|
|
28
|
+
/** Override the subscription store; defaults to a D1 store over `WEBSUB_DB`. */
|
|
29
|
+
readonly store?: SubscriptionStore;
|
|
30
|
+
/** Clock injection for deterministic tests; defaults to `Date.now`. */
|
|
31
|
+
readonly now?: () => number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build the Queue consumer that performs intent verification and content
|
|
35
|
+
* distribution. Fails loudly if no store is configured (neither
|
|
36
|
+
* `options.store` nor the `WEBSUB_DB` binding).
|
|
37
|
+
*/
|
|
38
|
+
export declare function createWebSubQueueConsumer(config: WebSubConfig, options?: ConsumerOptions): WebSubQueueConsumer;
|
|
39
|
+
//# sourceMappingURL=consumer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consumer.d.ts","sourceRoot":"","sources":["../src/consumer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAChF,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,SAAS,EACf,MAAM,UAAU,CAAC;AAGlB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EAA6B,KAAK,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAG5E,sEAAsE;AACtE,MAAM,MAAM,mBAAmB,GAAG,CAChC,KAAK,EAAE,YAAY,CAAC,SAAS,CAAC,EAC9B,GAAG,EAAE,SAAS,EACd,GAAG,EAAE,gBAAgB,KAClB,OAAO,CAAC,IAAI,CAAC,CAAC;AAEnB,2EAA2E;AAC3E,MAAM,WAAW,eAAe;IAC9B,gFAAgF;IAChF,QAAQ,CAAC,KAAK,CAAC,EAAE,iBAAiB,CAAC;IACnC,uEAAuE;IACvE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAyBD;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,YAAY,EACpB,OAAO,CAAC,EAAE,eAAe,GACxB,mBAAmB,CA0GrB"}
|
package/dist/consumer.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/websub` — the queue consumer.
|
|
3
|
+
*
|
|
4
|
+
* The hub's slow work runs here, off the request path, with the queue providing
|
|
5
|
+
* retries and backoff. Two job kinds flow through:
|
|
6
|
+
*
|
|
7
|
+
* - **verify** — issue the verification-of-intent GET; on a confirmed subscribe,
|
|
8
|
+
* write the subscription to the D1 store with its lease expiry, and on a
|
|
9
|
+
* confirmed unsubscribe, remove it. A subscription row is created only after
|
|
10
|
+
* verification succeeds, so an unverified callback never lands in the store.
|
|
11
|
+
* - **distribute** — prune expired leases, fetch the topic's current content, and
|
|
12
|
+
* fan it out (signed per-subscriber when a secret is set) to every active
|
|
13
|
+
* subscriber.
|
|
14
|
+
*
|
|
15
|
+
* A job whose store/fetch work throws — or a distribution that cannot fetch the
|
|
16
|
+
* topic — is retried; everything else is acked. See `spec/packages/websub.md`.
|
|
17
|
+
*
|
|
18
|
+
* @packageDocumentation
|
|
19
|
+
*/
|
|
20
|
+
import { hostFromUrl } from "@dwk/log";
|
|
21
|
+
import { resolveConfig, } from "./config";
|
|
22
|
+
import { deliverToSubscriber, fetchTopicContent } from "./distribute";
|
|
23
|
+
import { WebSubLogEvent } from "./log";
|
|
24
|
+
import { createD1SubscriptionStore } from "./store";
|
|
25
|
+
import { notifyDenial, verifyIntent } from "./verify";
|
|
26
|
+
function emit(config, level, event, fields) {
|
|
27
|
+
config.logger[level](event, fields);
|
|
28
|
+
config.metrics.count(event, fields);
|
|
29
|
+
}
|
|
30
|
+
function resolveStore(options, env) {
|
|
31
|
+
if (options?.store !== undefined) {
|
|
32
|
+
return options.store;
|
|
33
|
+
}
|
|
34
|
+
if (env.WEBSUB_DB === undefined) {
|
|
35
|
+
throw new Error("@dwk/websub: missing required binding WEBSUB_DB.");
|
|
36
|
+
}
|
|
37
|
+
return createD1SubscriptionStore(env.WEBSUB_DB);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Build the Queue consumer that performs intent verification and content
|
|
41
|
+
* distribution. Fails loudly if no store is configured (neither
|
|
42
|
+
* `options.store` nor the `WEBSUB_DB` binding).
|
|
43
|
+
*/
|
|
44
|
+
export function createWebSubQueueConsumer(config, options) {
|
|
45
|
+
const resolved = resolveConfig(config);
|
|
46
|
+
const clock = options?.now ?? (() => Date.now());
|
|
47
|
+
return async (batch, env, _ctx) => {
|
|
48
|
+
const store = resolveStore(options, env);
|
|
49
|
+
for (const message of batch.messages) {
|
|
50
|
+
const job = message.body;
|
|
51
|
+
try {
|
|
52
|
+
if (job.kind === "verify") {
|
|
53
|
+
const result = await verifyIntent(job.callback, job.topic, {
|
|
54
|
+
mode: job.mode,
|
|
55
|
+
leaseSeconds: job.mode === "subscribe" ? job.leaseSeconds : undefined,
|
|
56
|
+
fetch: resolved.fetch,
|
|
57
|
+
logger: resolved.logger,
|
|
58
|
+
metrics: resolved.metrics,
|
|
59
|
+
});
|
|
60
|
+
if (result.confirmed) {
|
|
61
|
+
if (job.mode === "subscribe") {
|
|
62
|
+
await store.upsert({
|
|
63
|
+
callback: job.callback,
|
|
64
|
+
topic: job.topic,
|
|
65
|
+
secret: job.secret,
|
|
66
|
+
leaseSeconds: job.leaseSeconds,
|
|
67
|
+
now: clock(),
|
|
68
|
+
});
|
|
69
|
+
emit(resolved, "info", WebSubLogEvent.SubscriptionActivated, {
|
|
70
|
+
callbackHost: hostFromUrl(job.callback),
|
|
71
|
+
topicHost: hostFromUrl(job.topic),
|
|
72
|
+
leaseSeconds: job.leaseSeconds,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
await store.remove(job.callback, job.topic);
|
|
77
|
+
emit(resolved, "info", WebSubLogEvent.SubscriptionRemoved, {
|
|
78
|
+
callbackHost: hostFromUrl(job.callback),
|
|
79
|
+
topicHost: hostFromUrl(job.topic),
|
|
80
|
+
reason: "unsubscribed",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else if (job.mode === "subscribe") {
|
|
85
|
+
// Intent unconfirmed: signal denial to the subscriber (WebSub §5.2)
|
|
86
|
+
// instead of silently dropping the request. (An unconfirmed
|
|
87
|
+
// unsubscribe simply leaves the existing subscription in place.)
|
|
88
|
+
await notifyDenial(job.callback, job.topic, {
|
|
89
|
+
reason: "verification_failed",
|
|
90
|
+
fetch: resolved.fetch,
|
|
91
|
+
logger: resolved.logger,
|
|
92
|
+
metrics: resolved.metrics,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
message.ack();
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
// kind === "distribute"
|
|
99
|
+
const now = clock();
|
|
100
|
+
await store.pruneExpired(now);
|
|
101
|
+
const fetched = await fetchTopicContent(job.topic, {
|
|
102
|
+
fetch: resolved.fetch,
|
|
103
|
+
logger: resolved.logger,
|
|
104
|
+
metrics: resolved.metrics,
|
|
105
|
+
defaultContentType: resolved.defaultContentType,
|
|
106
|
+
});
|
|
107
|
+
if (fetched.kind === "retry") {
|
|
108
|
+
// The topic was unreachable / non-2xx — retry the whole job later
|
|
109
|
+
// rather than dropping the push.
|
|
110
|
+
message.retry();
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (fetched.kind === "drop") {
|
|
114
|
+
// A permanent, deterministic refusal (e.g. an unlabelable topic):
|
|
115
|
+
// retrying can't fix it, so ack and move on rather than clog the
|
|
116
|
+
// queue and re-hammer the topic. fetchTopicContent already logged why.
|
|
117
|
+
message.ack();
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const content = fetched.content;
|
|
121
|
+
const subscribers = await store.listActive(job.topic, now);
|
|
122
|
+
// Fan out in parallel: deliverToSubscriber never throws (it reports a
|
|
123
|
+
// failed/blocked POST as delivered:false), so one slow or dead callback
|
|
124
|
+
// must not head-of-line block the rest and risk timing out the whole
|
|
125
|
+
// consumer invocation.
|
|
126
|
+
await Promise.all(subscribers.map((subscriber) => deliverToSubscriber(subscriber, content, resolved.hubUrl, {
|
|
127
|
+
fetch: resolved.fetch,
|
|
128
|
+
logger: resolved.logger,
|
|
129
|
+
metrics: resolved.metrics,
|
|
130
|
+
signatureAlgorithm: resolved.signatureAlgorithm,
|
|
131
|
+
})));
|
|
132
|
+
message.ack();
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
// A store/queue failure must not retry silently — name the kind and
|
|
136
|
+
// error so an operator can tell a transient blip from a wedged job.
|
|
137
|
+
emit(resolved, "warn", WebSubLogEvent.QueueRetry, {
|
|
138
|
+
kind: job.kind,
|
|
139
|
+
error: err instanceof Error ? err.name : "unknown",
|
|
140
|
+
});
|
|
141
|
+
message.retry();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=consumer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consumer.js","sourceRoot":"","sources":["../src/consumer.ts"],"names":[],"mappings":"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"}
|