@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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/websub` — signed content distribution.
|
|
3
|
+
*
|
|
4
|
+
* On publish, the hub fetches the topic's current content and `POST`s it to every
|
|
5
|
+
* active subscriber's callback (WebSub §7). When a subscriber registered a
|
|
6
|
+
* `hub.secret`, the body is authenticated with an HMAC signature in the
|
|
7
|
+
* `X-Hub-Signature: <method>=<hex>` header so the subscriber can verify the
|
|
8
|
+
* delivery came from this hub (§8). The digest method is a hub-level config
|
|
9
|
+
* option (`sha256` by default; `sha1`/`sha384`/`sha512` are also permitted by
|
|
10
|
+
* §8 and selected via `signatureAlgorithm`). Deliveries carry `Link` headers
|
|
11
|
+
* advertising
|
|
12
|
+
* the hub (`rel="hub"`) and the topic (`rel="self"`). Every POST goes through
|
|
13
|
+
* {@link safeFetch}. See `spec/packages/websub.md`.
|
|
14
|
+
*
|
|
15
|
+
* @packageDocumentation
|
|
16
|
+
*/
|
|
17
|
+
import { type Logger, type Metrics } from "@dwk/log";
|
|
18
|
+
import type { FetchLike } from "./fetch";
|
|
19
|
+
import type { Subscription } from "./store";
|
|
20
|
+
/** A topic's current content, as fetched from the topic URL. */
|
|
21
|
+
export interface TopicContent {
|
|
22
|
+
/** The raw body bytes to forward to subscribers. */
|
|
23
|
+
readonly body: Uint8Array;
|
|
24
|
+
/** The topic's `Content-Type`, forwarded verbatim to subscribers. */
|
|
25
|
+
readonly contentType: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The HMAC digest methods WebSub §8 permits for `X-Hub-Signature`. The method
|
|
29
|
+
* name is emitted verbatim as the header's `<method>=` prefix, so it must match
|
|
30
|
+
* the WebSub spelling (`sha1`, not `sha-1`).
|
|
31
|
+
*/
|
|
32
|
+
export type SignatureAlgorithm = "sha1" | "sha256" | "sha384" | "sha512";
|
|
33
|
+
/** WebSub's secure default signature method; SHA-1 interop is opt-in only. */
|
|
34
|
+
export declare const DEFAULT_SIGNATURE_ALGORITHM: SignatureAlgorithm;
|
|
35
|
+
/** Outcome of delivering content to one subscriber. */
|
|
36
|
+
export interface DeliveryResult {
|
|
37
|
+
readonly callback: string;
|
|
38
|
+
/** Whether the callback accepted the delivery (2xx). */
|
|
39
|
+
readonly delivered: boolean;
|
|
40
|
+
/** The delivery's HTTP status (`0` when the POST threw or was blocked). */
|
|
41
|
+
readonly status: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Compute the `X-Hub-Signature` value for `body` under `secret`:
|
|
45
|
+
* `<method>=<lowercase hex HMAC>` (WebSub §8). `method` defaults to the secure
|
|
46
|
+
* `sha256`; WebSub also permits `sha1`/`sha384`/`sha512`. The header prefix is
|
|
47
|
+
* the WebSub method name verbatim (e.g. `sha1=`, not `sha-1=`).
|
|
48
|
+
*/
|
|
49
|
+
export declare function contentSignature(secret: string, body: Uint8Array, method?: SignatureAlgorithm): Promise<string>;
|
|
50
|
+
/** Build the `Link` header advertising this hub and the topic (WebSub §5.1). */
|
|
51
|
+
export declare function buildLinkHeader(hubUrl: string, topic: string): string;
|
|
52
|
+
/** Inputs shared by distribution helpers. */
|
|
53
|
+
export interface DistributeOptions {
|
|
54
|
+
readonly fetch?: FetchLike;
|
|
55
|
+
readonly logger?: Logger;
|
|
56
|
+
readonly metrics?: Metrics;
|
|
57
|
+
/**
|
|
58
|
+
* HMAC method used for the `X-Hub-Signature` header. WebSub §8 has no
|
|
59
|
+
* per-request method parameter, so this is a hub-level choice; it defaults to
|
|
60
|
+
* the secure {@link DEFAULT_SIGNATURE_ALGORITHM} (`sha256`). Set it to `sha1`
|
|
61
|
+
* only for interop with subscribers that require the legacy method.
|
|
62
|
+
*/
|
|
63
|
+
readonly signatureAlgorithm?: SignatureAlgorithm;
|
|
64
|
+
/**
|
|
65
|
+
* Media type to forward when the topic response declares no `Content-Type`.
|
|
66
|
+
* WebSub §7 requires the distribution `Content-Type` to correspond to the
|
|
67
|
+
* topic's, so the hub never fabricates a generic `application/octet-stream`:
|
|
68
|
+
* when the topic omits the header and no fallback is configured here, the
|
|
69
|
+
* content is refused rather than mislabeled.
|
|
70
|
+
*/
|
|
71
|
+
readonly defaultContentType?: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Outcome of {@link fetchTopicContent}, telling the caller what to do with the
|
|
75
|
+
* queue message:
|
|
76
|
+
*
|
|
77
|
+
* - **`ok`** — the topic's current content, ready to fan out.
|
|
78
|
+
* - **`retry`** — a transient failure (topic unreachable, non-2xx, or an
|
|
79
|
+
* over-cap body that may be a truncated response): re-enqueue and try later.
|
|
80
|
+
* - **`drop`** — a deterministic refusal that re-fetching cannot fix, so the
|
|
81
|
+
* caller acks rather than burning retries and re-hammering the topic. Today
|
|
82
|
+
* this is a topic that declares no `Content-Type` and for which no
|
|
83
|
+
* {@link DistributeOptions.defaultContentType} fallback is configured, since
|
|
84
|
+
* forwarding it would mislabel the feed (WebSub §7).
|
|
85
|
+
*/
|
|
86
|
+
export type TopicFetchResult = {
|
|
87
|
+
readonly kind: "ok";
|
|
88
|
+
readonly content: TopicContent;
|
|
89
|
+
} | {
|
|
90
|
+
readonly kind: "retry";
|
|
91
|
+
} | {
|
|
92
|
+
readonly kind: "drop";
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Fetch the topic's current content through {@link safeFetch}, classifying the
|
|
96
|
+
* outcome as `ok` / `retry` / `drop` (see {@link TopicFetchResult}). A missing,
|
|
97
|
+
* unlabelable `Content-Type` is a `drop` — a permanent format/config error that
|
|
98
|
+
* retrying would only turn into a self-inflicted hammering of the topic — while
|
|
99
|
+
* unreachable/non-2xx/over-cap responses are transient `retry`s.
|
|
100
|
+
*/
|
|
101
|
+
export declare function fetchTopicContent(topic: string, options?: DistributeOptions): Promise<TopicFetchResult>;
|
|
102
|
+
/**
|
|
103
|
+
* Deliver `content` to one subscriber. POSTs the body with the topic's
|
|
104
|
+
* `Content-Type`, the hub/self `Link` header, and — when the subscription
|
|
105
|
+
* carries a secret — the `X-Hub-Signature`. Never throws: a failed or blocked
|
|
106
|
+
* POST is reported as `delivered: false`.
|
|
107
|
+
*/
|
|
108
|
+
export declare function deliverToSubscriber(subscription: Subscription, content: TopicContent, hubUrl: string, options?: DistributeOptions): Promise<DeliveryResult>;
|
|
109
|
+
//# sourceMappingURL=distribute.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"distribute.d.ts","sourceRoot":"","sources":["../src/distribute.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAIL,KAAK,MAAM,EACX,KAAK,OAAO,EACb,MAAM,UAAU,CAAC;AAClB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAIzC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C,gEAAgE;AAChE,MAAM,WAAW,YAAY;IAC3B,oDAAoD;IACpD,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,qEAAqE;IACrE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAUzE,8EAA8E;AAC9E,eAAO,MAAM,2BAA2B,EAAE,kBAA6B,CAAC;AAExE,uDAAuD;AACvD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,wDAAwD;IACxD,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,2EAA2E;IAC3E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,UAAU,EAChB,MAAM,GAAE,kBAAgD,GACvD,OAAO,CAAC,MAAM,CAAC,CAajB;AAED,gFAAgF;AAChF,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAErE;AAED,6CAA6C;AAC7C,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;OAKG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACjD;;;;;;OAMG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACtC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,gBAAgB,GACxB;IAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAA;CAAE,GACvD;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAC1B;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9B;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,gBAAgB,CAAC,CAoD3B;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,YAAY,EAAE,YAAY,EAC1B,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,cAAc,CAAC,CA8CzB"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/websub` — signed content distribution.
|
|
3
|
+
*
|
|
4
|
+
* On publish, the hub fetches the topic's current content and `POST`s it to every
|
|
5
|
+
* active subscriber's callback (WebSub §7). When a subscriber registered a
|
|
6
|
+
* `hub.secret`, the body is authenticated with an HMAC signature in the
|
|
7
|
+
* `X-Hub-Signature: <method>=<hex>` header so the subscriber can verify the
|
|
8
|
+
* delivery came from this hub (§8). The digest method is a hub-level config
|
|
9
|
+
* option (`sha256` by default; `sha1`/`sha384`/`sha512` are also permitted by
|
|
10
|
+
* §8 and selected via `signatureAlgorithm`). Deliveries carry `Link` headers
|
|
11
|
+
* advertising
|
|
12
|
+
* the hub (`rel="hub"`) and the topic (`rel="self"`). Every POST goes through
|
|
13
|
+
* {@link safeFetch}. See `spec/packages/websub.md`.
|
|
14
|
+
*
|
|
15
|
+
* @packageDocumentation
|
|
16
|
+
*/
|
|
17
|
+
import { hostFromUrl, noopLogger, noopMetrics, } from "@dwk/log";
|
|
18
|
+
import { readBytesCapped } from "./fetch";
|
|
19
|
+
import { WebSubLogEvent } from "./log";
|
|
20
|
+
import { safeFetch } from "./safe-fetch";
|
|
21
|
+
/** Map a WebSub signature method name to its WebCrypto SHA hash name. */
|
|
22
|
+
const HASH_FOR_METHOD = {
|
|
23
|
+
sha1: "SHA-1",
|
|
24
|
+
sha256: "SHA-256",
|
|
25
|
+
sha384: "SHA-384",
|
|
26
|
+
sha512: "SHA-512",
|
|
27
|
+
};
|
|
28
|
+
/** WebSub's secure default signature method; SHA-1 interop is opt-in only. */
|
|
29
|
+
export const DEFAULT_SIGNATURE_ALGORITHM = "sha256";
|
|
30
|
+
/**
|
|
31
|
+
* Compute the `X-Hub-Signature` value for `body` under `secret`:
|
|
32
|
+
* `<method>=<lowercase hex HMAC>` (WebSub §8). `method` defaults to the secure
|
|
33
|
+
* `sha256`; WebSub also permits `sha1`/`sha384`/`sha512`. The header prefix is
|
|
34
|
+
* the WebSub method name verbatim (e.g. `sha1=`, not `sha-1=`).
|
|
35
|
+
*/
|
|
36
|
+
export async function contentSignature(secret, body, method = DEFAULT_SIGNATURE_ALGORITHM) {
|
|
37
|
+
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: HASH_FOR_METHOD[method] }, false, ["sign"]);
|
|
38
|
+
const mac = await crypto.subtle.sign("HMAC", key, body);
|
|
39
|
+
const hex = [...new Uint8Array(mac)]
|
|
40
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
41
|
+
.join("");
|
|
42
|
+
return `${method}=${hex}`;
|
|
43
|
+
}
|
|
44
|
+
/** Build the `Link` header advertising this hub and the topic (WebSub §5.1). */
|
|
45
|
+
export function buildLinkHeader(hubUrl, topic) {
|
|
46
|
+
return `<${hubUrl}>; rel="hub", <${topic}>; rel="self"`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Fetch the topic's current content through {@link safeFetch}, classifying the
|
|
50
|
+
* outcome as `ok` / `retry` / `drop` (see {@link TopicFetchResult}). A missing,
|
|
51
|
+
* unlabelable `Content-Type` is a `drop` — a permanent format/config error that
|
|
52
|
+
* retrying would only turn into a self-inflicted hammering of the topic — while
|
|
53
|
+
* unreachable/non-2xx/over-cap responses are transient `retry`s.
|
|
54
|
+
*/
|
|
55
|
+
export async function fetchTopicContent(topic, options) {
|
|
56
|
+
const doFetch = options?.fetch ?? ((input, init) => fetch(input, init));
|
|
57
|
+
const logger = options?.logger ?? noopLogger;
|
|
58
|
+
const metrics = options?.metrics ?? noopMetrics;
|
|
59
|
+
let response;
|
|
60
|
+
try {
|
|
61
|
+
const result = await safeFetch(doFetch, topic, { method: "GET" }, { logger, metrics });
|
|
62
|
+
response = result.response;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
const fields = { topicHost: hostFromUrl(topic), status: 0 };
|
|
66
|
+
logger.warn(WebSubLogEvent.TopicFetchFailed, fields);
|
|
67
|
+
metrics.count(WebSubLogEvent.TopicFetchFailed, fields);
|
|
68
|
+
return { kind: "retry" };
|
|
69
|
+
}
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
await response.body?.cancel().catch(() => undefined);
|
|
72
|
+
const fields = { topicHost: hostFromUrl(topic), status: response.status };
|
|
73
|
+
logger.warn(WebSubLogEvent.TopicFetchFailed, fields);
|
|
74
|
+
metrics.count(WebSubLogEvent.TopicFetchFailed, fields);
|
|
75
|
+
return { kind: "retry" };
|
|
76
|
+
}
|
|
77
|
+
const contentType = response.headers.get("content-type") ?? options?.defaultContentType;
|
|
78
|
+
if (contentType === undefined || contentType === "") {
|
|
79
|
+
// WebSub §7: the distribution Content-Type MUST correspond to the topic's.
|
|
80
|
+
// With neither a topic header nor a configured fallback, forwarding would
|
|
81
|
+
// mislabel the feed. Re-fetching can't conjure a Content-Type, so drop the
|
|
82
|
+
// job (the caller acks) rather than retry — retrying would only clog the
|
|
83
|
+
// queue and re-hammer the topic for a permanent configuration error.
|
|
84
|
+
await response.body?.cancel().catch(() => undefined);
|
|
85
|
+
const fields = { topicHost: hostFromUrl(topic), status: response.status };
|
|
86
|
+
logger.warn(WebSubLogEvent.TopicContentTypeMissing, fields);
|
|
87
|
+
metrics.count(WebSubLogEvent.TopicContentTypeMissing, fields);
|
|
88
|
+
return { kind: "drop" };
|
|
89
|
+
}
|
|
90
|
+
const body = await readBytesCapped(response);
|
|
91
|
+
if (body === null) {
|
|
92
|
+
const fields = { topicHost: hostFromUrl(topic), status: response.status };
|
|
93
|
+
logger.warn(WebSubLogEvent.TopicFetchFailed, fields);
|
|
94
|
+
metrics.count(WebSubLogEvent.TopicFetchFailed, fields);
|
|
95
|
+
return { kind: "retry" };
|
|
96
|
+
}
|
|
97
|
+
return { kind: "ok", content: { body, contentType } };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Deliver `content` to one subscriber. POSTs the body with the topic's
|
|
101
|
+
* `Content-Type`, the hub/self `Link` header, and — when the subscription
|
|
102
|
+
* carries a secret — the `X-Hub-Signature`. Never throws: a failed or blocked
|
|
103
|
+
* POST is reported as `delivered: false`.
|
|
104
|
+
*/
|
|
105
|
+
export async function deliverToSubscriber(subscription, content, hubUrl, options) {
|
|
106
|
+
const doFetch = options?.fetch ?? ((input, init) => fetch(input, init));
|
|
107
|
+
const logger = options?.logger ?? noopLogger;
|
|
108
|
+
const metrics = options?.metrics ?? noopMetrics;
|
|
109
|
+
const headers = {
|
|
110
|
+
"content-type": content.contentType,
|
|
111
|
+
link: buildLinkHeader(hubUrl, subscription.topic),
|
|
112
|
+
};
|
|
113
|
+
if (subscription.secret !== null) {
|
|
114
|
+
headers["x-hub-signature"] = await contentSignature(subscription.secret, content.body, options?.signatureAlgorithm ?? DEFAULT_SIGNATURE_ALGORITHM);
|
|
115
|
+
}
|
|
116
|
+
const finish = (delivered, status) => {
|
|
117
|
+
const fields = {
|
|
118
|
+
callbackHost: hostFromUrl(subscription.callback),
|
|
119
|
+
delivered,
|
|
120
|
+
status,
|
|
121
|
+
};
|
|
122
|
+
logger.info(WebSubLogEvent.DeliveryCompleted, fields);
|
|
123
|
+
metrics.count(WebSubLogEvent.DeliveryCompleted, fields);
|
|
124
|
+
return { callback: subscription.callback, delivered, status };
|
|
125
|
+
};
|
|
126
|
+
try {
|
|
127
|
+
const result = await safeFetch(doFetch, subscription.callback, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers,
|
|
130
|
+
// `content.body` is a Uint8Array; pass its backing buffer as the body.
|
|
131
|
+
body: content.body,
|
|
132
|
+
}, { logger, metrics });
|
|
133
|
+
await result.response.body?.cancel().catch(() => undefined);
|
|
134
|
+
return finish(result.response.ok, result.response.status);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return finish(false, 0);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=distribute.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"distribute.js","sourceRoot":"","sources":["../src/distribute.ts"],"names":[],"mappings":"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"}
|
package/dist/fetch.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/websub` — injectable `fetch` type and a body-size cap.
|
|
3
|
+
*
|
|
4
|
+
* Intent verification, topic fetching, and content distribution all perform HTTP
|
|
5
|
+
* I/O. They accept a {@link FetchLike} so callers can inject a stub in tests (no
|
|
6
|
+
* network) and so the package never reaches for a global it didn't receive.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
/** A minimal, injectable `fetch` signature. */
|
|
11
|
+
export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
12
|
+
/**
|
|
13
|
+
* Default cap on a fetched body (4 MB). A challenge echo is tiny and a feed is
|
|
14
|
+
* modest; a larger body is almost certainly hostile or irrelevant, and buffering
|
|
15
|
+
* it would risk an OOM (the Worker memory limit is 128 MB). See
|
|
16
|
+
* `spec/non-functional-requirements.md`.
|
|
17
|
+
*/
|
|
18
|
+
export declare const MAX_BODY_BYTES: number;
|
|
19
|
+
/**
|
|
20
|
+
* Read a response body as a `Uint8Array`, refusing bodies larger than `maxBytes`.
|
|
21
|
+
*
|
|
22
|
+
* A declared `Content-Length` over the cap is rejected up front; the stream is
|
|
23
|
+
* then read incrementally and aborted the moment the cap is exceeded, so a
|
|
24
|
+
* missing or lying `Content-Length` cannot force the whole body into memory.
|
|
25
|
+
* Returns `null` when the body is too large or cannot be read.
|
|
26
|
+
*/
|
|
27
|
+
export declare function readBytesCapped(response: Response, maxBytes?: number): Promise<Uint8Array | null>;
|
|
28
|
+
//# sourceMappingURL=fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../src/fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,+CAA+C;AAC/C,MAAM,MAAM,SAAS,GAAG,CACtB,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE,WAAW,KACf,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEvB;;;;;GAKG;AACH,eAAO,MAAM,cAAc,QAAkB,CAAC;AAE9C;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,QAAQ,EAClB,QAAQ,SAAiB,GACxB,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAgD5B"}
|
package/dist/fetch.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/websub` — injectable `fetch` type and a body-size cap.
|
|
3
|
+
*
|
|
4
|
+
* Intent verification, topic fetching, and content distribution all perform HTTP
|
|
5
|
+
* I/O. They accept a {@link FetchLike} so callers can inject a stub in tests (no
|
|
6
|
+
* network) and so the package never reaches for a global it didn't receive.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Default cap on a fetched body (4 MB). A challenge echo is tiny and a feed is
|
|
12
|
+
* modest; a larger body is almost certainly hostile or irrelevant, and buffering
|
|
13
|
+
* it would risk an OOM (the Worker memory limit is 128 MB). See
|
|
14
|
+
* `spec/non-functional-requirements.md`.
|
|
15
|
+
*/
|
|
16
|
+
export const MAX_BODY_BYTES = 4 * 1024 * 1024;
|
|
17
|
+
/**
|
|
18
|
+
* Read a response body as a `Uint8Array`, refusing bodies larger than `maxBytes`.
|
|
19
|
+
*
|
|
20
|
+
* A declared `Content-Length` over the cap is rejected up front; the stream is
|
|
21
|
+
* then read incrementally and aborted the moment the cap is exceeded, so a
|
|
22
|
+
* missing or lying `Content-Length` cannot force the whole body into memory.
|
|
23
|
+
* Returns `null` when the body is too large or cannot be read.
|
|
24
|
+
*/
|
|
25
|
+
export async function readBytesCapped(response, maxBytes = MAX_BODY_BYTES) {
|
|
26
|
+
const declared = response.headers.get("content-length");
|
|
27
|
+
if (declared !== null) {
|
|
28
|
+
const length = Number.parseInt(declared, 10);
|
|
29
|
+
if (Number.isFinite(length) && length > maxBytes) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const body = response.body;
|
|
34
|
+
if (body === null) {
|
|
35
|
+
try {
|
|
36
|
+
const buffer = await response.arrayBuffer();
|
|
37
|
+
return buffer.byteLength > maxBytes ? null : new Uint8Array(buffer);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const reader = body.getReader();
|
|
44
|
+
const chunks = [];
|
|
45
|
+
let total = 0;
|
|
46
|
+
try {
|
|
47
|
+
for (;;) {
|
|
48
|
+
const { done, value } = await reader.read();
|
|
49
|
+
if (done) {
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
if (value !== undefined) {
|
|
53
|
+
total += value.byteLength;
|
|
54
|
+
if (total > maxBytes) {
|
|
55
|
+
await reader.cancel();
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
chunks.push(value);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const merged = new Uint8Array(total);
|
|
66
|
+
let offset = 0;
|
|
67
|
+
for (const chunk of chunks) {
|
|
68
|
+
merged.set(chunk, offset);
|
|
69
|
+
offset += chunk.byteLength;
|
|
70
|
+
}
|
|
71
|
+
return merged;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=fetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.js","sourceRoot":"","sources":["../src/fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAQH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAE9C;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,QAAkB,EAClB,QAAQ,GAAG,cAAc;IAEzB,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IACxD,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QAC7C,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,GAAG,QAAQ,EAAE,CAAC;YACjD,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC;IAC3B,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAClB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC;YAC5C,OAAO,MAAM,CAAC,UAAU,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC;QACtE,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;IAChC,MAAM,MAAM,GAAiB,EAAE,CAAC;IAChC,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,CAAC;QACH,SAAS,CAAC;YACR,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;YAC5C,IAAI,IAAI,EAAE,CAAC;gBACT,MAAM;YACR,CAAC;YACD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACxB,KAAK,IAAI,KAAK,CAAC,UAAU,CAAC;gBAC1B,IAAI,KAAK,GAAG,QAAQ,EAAE,CAAC;oBACrB,MAAM,MAAM,CAAC,MAAM,EAAE,CAAC;oBACtB,OAAO,IAAI,CAAC;gBACd,CAAC;gBACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACrB,CAAC;QACH,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC;IACrC,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,UAAU,CAAC;IAC7B,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/websub` — the hub `fetch` handler and publish-notifier factory.
|
|
3
|
+
*
|
|
4
|
+
* `createWebSub` builds the WebSub hub endpoint: a single `POST`-only handler,
|
|
5
|
+
* mountable under any path prefix, that fields subscribe/unsubscribe requests and
|
|
6
|
+
* publish pings. Slow work (intent verification, content fan-out) is validated
|
|
7
|
+
* synchronously and pushed onto the queue, so the handler returns `202 Accepted`
|
|
8
|
+
* fast and the queue consumer (see {@link createWebSubQueueConsumer}) does the
|
|
9
|
+
* I/O with retries. `createPublishNotifier` is the in-process equivalent of a
|
|
10
|
+
* publish ping, for the Micropub write / Anglesite rebuild path. See
|
|
11
|
+
* `spec/packages/websub.md`.
|
|
12
|
+
*
|
|
13
|
+
* @packageDocumentation
|
|
14
|
+
*/
|
|
15
|
+
import type { ExecutionContext } from "@cloudflare/workers-types";
|
|
16
|
+
import { type WebSubConfig, type WebSubEnv } from "./config";
|
|
17
|
+
/** A `fetch`-compatible Worker handler. */
|
|
18
|
+
export type WebSubHandler = (request: Request, env: WebSubEnv, ctx: ExecutionContext) => Promise<Response>;
|
|
19
|
+
/**
|
|
20
|
+
* An in-process publish notifier: call it from the Micropub write path or an
|
|
21
|
+
* Anglesite rebuild hook to enqueue distribution of `topic` to its subscribers.
|
|
22
|
+
*/
|
|
23
|
+
export type PublishNotifier = (env: WebSubEnv, topic: string) => Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Build the WebSub hub handler from configuration.
|
|
26
|
+
*
|
|
27
|
+
* Accepts a form-encoded `POST`. `hub.mode=subscribe|unsubscribe` is validated
|
|
28
|
+
* and the verification-of-intent job enqueued (`202`); `hub.mode=publish`
|
|
29
|
+
* enqueues a distribution job (`202`). Invalid requests get `400` with a stable
|
|
30
|
+
* error code; other methods get `405`. Fails loudly if `WEBSUB_QUEUE` is missing.
|
|
31
|
+
*/
|
|
32
|
+
export declare function createWebSub(config: WebSubConfig): WebSubHandler;
|
|
33
|
+
/**
|
|
34
|
+
* Build an in-process publish notifier. The returned function enqueues a
|
|
35
|
+
* distribution job for `topic` (after checking it is a topic this hub serves),
|
|
36
|
+
* so a Micropub write or static rebuild can fan out a push without going through
|
|
37
|
+
* the HTTP publish endpoint.
|
|
38
|
+
*
|
|
39
|
+
* @throws when `topic` is not one this hub serves — a caller wiring this into its
|
|
40
|
+
* own write path should only ever publish its own feeds.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createPublishNotifier(config: WebSubConfig): PublishNotifier;
|
|
43
|
+
//# sourceMappingURL=handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,SAAS,EACf,MAAM,UAAU,CAAC;AAIlB,2CAA2C;AAC3C,MAAM,MAAM,aAAa,GAAG,CAC1B,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,SAAS,EACd,GAAG,EAAE,gBAAgB,KAClB,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEvB;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAyB/E;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,aAAa,CAmEhE;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,YAAY,GAAG,eAAe,CAc3E"}
|
package/dist/handler.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/websub` — the hub `fetch` handler and publish-notifier factory.
|
|
3
|
+
*
|
|
4
|
+
* `createWebSub` builds the WebSub hub endpoint: a single `POST`-only handler,
|
|
5
|
+
* mountable under any path prefix, that fields subscribe/unsubscribe requests and
|
|
6
|
+
* publish pings. Slow work (intent verification, content fan-out) is validated
|
|
7
|
+
* synchronously and pushed onto the queue, so the handler returns `202 Accepted`
|
|
8
|
+
* fast and the queue consumer (see {@link createWebSubQueueConsumer}) does the
|
|
9
|
+
* I/O with retries. `createPublishNotifier` is the in-process equivalent of a
|
|
10
|
+
* publish ping, for the Micropub write / Anglesite rebuild path. See
|
|
11
|
+
* `spec/packages/websub.md`.
|
|
12
|
+
*
|
|
13
|
+
* @packageDocumentation
|
|
14
|
+
*/
|
|
15
|
+
import { hostFromUrl } from "@dwk/log";
|
|
16
|
+
import { resolveConfig, } from "./config";
|
|
17
|
+
import { WebSubLogEvent } from "./log";
|
|
18
|
+
import { readHubParams, validatePublish, validateSubscribe } from "./validate";
|
|
19
|
+
function textResponse(status, body) {
|
|
20
|
+
return new Response(body, {
|
|
21
|
+
status,
|
|
22
|
+
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function requireQueue(env) {
|
|
26
|
+
if (env.WEBSUB_QUEUE === undefined) {
|
|
27
|
+
throw new Error("@dwk/websub: missing required binding WEBSUB_QUEUE.");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function emit(config, level, event, fields) {
|
|
31
|
+
config.logger[level](event, fields);
|
|
32
|
+
config.metrics.count(event, fields);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Build the WebSub hub handler from configuration.
|
|
36
|
+
*
|
|
37
|
+
* Accepts a form-encoded `POST`. `hub.mode=subscribe|unsubscribe` is validated
|
|
38
|
+
* and the verification-of-intent job enqueued (`202`); `hub.mode=publish`
|
|
39
|
+
* enqueues a distribution job (`202`). Invalid requests get `400` with a stable
|
|
40
|
+
* error code; other methods get `405`. Fails loudly if `WEBSUB_QUEUE` is missing.
|
|
41
|
+
*/
|
|
42
|
+
export function createWebSub(config) {
|
|
43
|
+
const resolved = resolveConfig(config);
|
|
44
|
+
return async (request, env, _ctx) => {
|
|
45
|
+
if (request.method !== "POST") {
|
|
46
|
+
return new Response("Method Not Allowed", {
|
|
47
|
+
status: 405,
|
|
48
|
+
headers: { allow: "POST" },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
requireQueue(env);
|
|
52
|
+
let form;
|
|
53
|
+
try {
|
|
54
|
+
form = await request.formData();
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return textResponse(400, "invalid_request");
|
|
58
|
+
}
|
|
59
|
+
// Lift the form into a string-only URLSearchParams; a `File`-valued field
|
|
60
|
+
// (multipart upload) is not a valid `hub.*` parameter and is dropped.
|
|
61
|
+
const search = new URLSearchParams();
|
|
62
|
+
for (const [key, value] of form.entries()) {
|
|
63
|
+
if (typeof value === "string") {
|
|
64
|
+
search.append(key, value);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const params = readHubParams(search);
|
|
68
|
+
if (params.mode === "publish") {
|
|
69
|
+
const result = validatePublish(params, resolved);
|
|
70
|
+
if (!result.ok) {
|
|
71
|
+
emit(resolved, "warn", WebSubLogEvent.PublishRejected, {
|
|
72
|
+
reason: result.error,
|
|
73
|
+
});
|
|
74
|
+
return textResponse(400, result.error);
|
|
75
|
+
}
|
|
76
|
+
await env.WEBSUB_QUEUE.send({ kind: "distribute", topic: result.topic });
|
|
77
|
+
emit(resolved, "info", WebSubLogEvent.PublishAccepted, {
|
|
78
|
+
topicHost: hostFromUrl(result.topic),
|
|
79
|
+
});
|
|
80
|
+
return new Response(null, { status: 202 });
|
|
81
|
+
}
|
|
82
|
+
const result = validateSubscribe(params, resolved);
|
|
83
|
+
if (!result.ok) {
|
|
84
|
+
emit(resolved, "warn", WebSubLogEvent.RequestRejected, {
|
|
85
|
+
reason: result.error,
|
|
86
|
+
});
|
|
87
|
+
return textResponse(400, result.error);
|
|
88
|
+
}
|
|
89
|
+
await env.WEBSUB_QUEUE.send({
|
|
90
|
+
kind: "verify",
|
|
91
|
+
mode: result.mode,
|
|
92
|
+
callback: result.callback,
|
|
93
|
+
topic: result.topic,
|
|
94
|
+
leaseSeconds: result.leaseSeconds,
|
|
95
|
+
...(result.secret !== undefined ? { secret: result.secret } : {}),
|
|
96
|
+
});
|
|
97
|
+
emit(resolved, "info", WebSubLogEvent.RequestAccepted, {
|
|
98
|
+
mode: result.mode,
|
|
99
|
+
callbackHost: hostFromUrl(result.callback),
|
|
100
|
+
topicHost: hostFromUrl(result.topic),
|
|
101
|
+
});
|
|
102
|
+
return new Response(null, { status: 202 });
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Build an in-process publish notifier. The returned function enqueues a
|
|
107
|
+
* distribution job for `topic` (after checking it is a topic this hub serves),
|
|
108
|
+
* so a Micropub write or static rebuild can fan out a push without going through
|
|
109
|
+
* the HTTP publish endpoint.
|
|
110
|
+
*
|
|
111
|
+
* @throws when `topic` is not one this hub serves — a caller wiring this into its
|
|
112
|
+
* own write path should only ever publish its own feeds.
|
|
113
|
+
*/
|
|
114
|
+
export function createPublishNotifier(config) {
|
|
115
|
+
const resolved = resolveConfig(config);
|
|
116
|
+
return async (env, topic) => {
|
|
117
|
+
requireQueue(env);
|
|
118
|
+
if (!resolved.isAllowedTopic(topic)) {
|
|
119
|
+
throw new Error(`@dwk/websub: refusing to publish unsupported topic ${hostFromUrl(topic) ?? "<invalid>"}.`);
|
|
120
|
+
}
|
|
121
|
+
await env.WEBSUB_QUEUE.send({ kind: "distribute", topic });
|
|
122
|
+
emit(resolved, "info", WebSubLogEvent.PublishAccepted, {
|
|
123
|
+
topicHost: hostFromUrl(topic),
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.js","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/websub` — WebSub (W3C) hub.
|
|
3
|
+
*
|
|
4
|
+
* Endpoint package. The publish-side, real-time complement to `@dwk/webmention`:
|
|
5
|
+
* subscribers receive a push when the user's feed changes instead of polling. The
|
|
6
|
+
* hub fields `hub.mode=subscribe|unsubscribe` requests (validated synchronously,
|
|
7
|
+
* verified-of-intent and persisted asynchronously) and publish pings, keeps a
|
|
8
|
+
* strongly-consistent D1 subscription store with lease expiry, and on publish
|
|
9
|
+
* fans the topic's content out to every verified callback — signing the body with
|
|
10
|
+
* HMAC-SHA256 (`X-Hub-Signature`) when the subscriber registered a secret. The
|
|
11
|
+
* feeds themselves are static SSG artifacts (Anglesite's job, including the
|
|
12
|
+
* `Link rel="hub"`/`rel="self"` advertisement); this package is only the dynamic
|
|
13
|
+
* layer. Cloudflare specifics (D1, Queue) are confined here; validation, lease
|
|
14
|
+
* math, verification, and signing are pure and unit-test without a Workers
|
|
15
|
+
* runtime.
|
|
16
|
+
*
|
|
17
|
+
* @see spec/packages/websub.md
|
|
18
|
+
* @packageDocumentation
|
|
19
|
+
*/
|
|
20
|
+
export { createWebSub, createPublishNotifier, type WebSubHandler, type PublishNotifier, } from "./handler";
|
|
21
|
+
export { createWebSubQueueConsumer, type WebSubQueueConsumer, type ConsumerOptions, } from "./consumer";
|
|
22
|
+
export { resolveConfig, normalizeTopic, clampLease, DEFAULT_MIN_LEASE_SECONDS, DEFAULT_MAX_LEASE_SECONDS, type WebSubConfig, type WebSubEnv, type ResolvedConfig, } from "./config";
|
|
23
|
+
export { createD1SubscriptionStore, type SubscriptionStore, type Subscription, type SubscriptionUpsert, type D1StoreOptions, } from "./store";
|
|
24
|
+
export { validateSubscribe, validatePublish, readHubParams, MAX_SECRET_BYTES, type SubscribeResult, type SubscribeRequest, type SubscribeError, type PublishResult, type PublishRequest, type PublishError, type RawHubParams, } from "./validate";
|
|
25
|
+
export { verifyIntent, buildVerificationUrl, generateChallenge, notifyDenial, buildDenialUrl, type VerifyIntentOptions, type VerifyIntentResult, type NotifyDenialOptions, } from "./verify";
|
|
26
|
+
export { contentSignature, buildLinkHeader, fetchTopicContent, deliverToSubscriber, DEFAULT_SIGNATURE_ALGORITHM, type SignatureAlgorithm, type TopicContent, type TopicFetchResult, type DeliveryResult, type DistributeOptions, } from "./distribute";
|
|
27
|
+
export type { WebSubJob, VerifyJob, DistributeJob } from "./queue";
|
|
28
|
+
export type { FetchLike } from "./fetch";
|
|
29
|
+
export { safeFetch, assertPublicUrl, isPrivateOrReservedHost, SsrfError, DEFAULT_MAX_REDIRECTS, DEFAULT_TIMEOUT_MS, type SafeFetchOptions, type SafeFetchResult, type SsrfReason, } from "./safe-fetch";
|
|
30
|
+
export { WebSubLogEvent } from "./log";
|
|
31
|
+
export type { Logger, Metrics } from "@dwk/log";
|
|
32
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EACL,YAAY,EACZ,qBAAqB,EACrB,KAAK,aAAa,EAClB,KAAK,eAAe,GACrB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,yBAAyB,EACzB,KAAK,mBAAmB,EACxB,KAAK,eAAe,GACrB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,aAAa,EACb,cAAc,EACd,UAAU,EACV,yBAAyB,EACzB,yBAAyB,EACzB,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,cAAc,GACpB,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,yBAAyB,EACzB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,KAAK,cAAc,GACpB,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,aAAa,EACb,gBAAgB,EAChB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,YAAY,GAClB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,YAAY,EACZ,oBAAoB,EACpB,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,GACzB,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,mBAAmB,EACnB,2BAA2B,EAC3B,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,iBAAiB,GACvB,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACnE,YAAY,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EACL,SAAS,EACT,eAAe,EACf,uBAAuB,EACvB,SAAS,EACT,qBAAqB,EACrB,kBAAkB,EAClB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,UAAU,GAChB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AACvC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dwk/websub` — WebSub (W3C) hub.
|
|
3
|
+
*
|
|
4
|
+
* Endpoint package. The publish-side, real-time complement to `@dwk/webmention`:
|
|
5
|
+
* subscribers receive a push when the user's feed changes instead of polling. The
|
|
6
|
+
* hub fields `hub.mode=subscribe|unsubscribe` requests (validated synchronously,
|
|
7
|
+
* verified-of-intent and persisted asynchronously) and publish pings, keeps a
|
|
8
|
+
* strongly-consistent D1 subscription store with lease expiry, and on publish
|
|
9
|
+
* fans the topic's content out to every verified callback — signing the body with
|
|
10
|
+
* HMAC-SHA256 (`X-Hub-Signature`) when the subscriber registered a secret. The
|
|
11
|
+
* feeds themselves are static SSG artifacts (Anglesite's job, including the
|
|
12
|
+
* `Link rel="hub"`/`rel="self"` advertisement); this package is only the dynamic
|
|
13
|
+
* layer. Cloudflare specifics (D1, Queue) are confined here; validation, lease
|
|
14
|
+
* math, verification, and signing are pure and unit-test without a Workers
|
|
15
|
+
* runtime.
|
|
16
|
+
*
|
|
17
|
+
* @see spec/packages/websub.md
|
|
18
|
+
* @packageDocumentation
|
|
19
|
+
*/
|
|
20
|
+
export { createWebSub, createPublishNotifier, } from "./handler";
|
|
21
|
+
export { createWebSubQueueConsumer, } from "./consumer";
|
|
22
|
+
export { resolveConfig, normalizeTopic, clampLease, DEFAULT_MIN_LEASE_SECONDS, DEFAULT_MAX_LEASE_SECONDS, } from "./config";
|
|
23
|
+
export { createD1SubscriptionStore, } from "./store";
|
|
24
|
+
export { validateSubscribe, validatePublish, readHubParams, MAX_SECRET_BYTES, } from "./validate";
|
|
25
|
+
export { verifyIntent, buildVerificationUrl, generateChallenge, notifyDenial, buildDenialUrl, } from "./verify";
|
|
26
|
+
export { contentSignature, buildLinkHeader, fetchTopicContent, deliverToSubscriber, DEFAULT_SIGNATURE_ALGORITHM, } from "./distribute";
|
|
27
|
+
export { safeFetch, assertPublicUrl, isPrivateOrReservedHost, SsrfError, DEFAULT_MAX_REDIRECTS, DEFAULT_TIMEOUT_MS, } from "./safe-fetch";
|
|
28
|
+
export { WebSubLogEvent } from "./log";
|
|
29
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EACL,YAAY,EACZ,qBAAqB,GAGtB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,yBAAyB,GAG1B,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,aAAa,EACb,cAAc,EACd,UAAU,EACV,yBAAyB,EACzB,yBAAyB,GAI1B,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,yBAAyB,GAK1B,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,aAAa,EACb,gBAAgB,GAQjB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,YAAY,EACZ,oBAAoB,EACpB,iBAAiB,EACjB,YAAY,EACZ,cAAc,GAIf,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,mBAAmB,EACnB,2BAA2B,GAM5B,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,SAAS,EACT,eAAe,EACf,uBAAuB,EACvB,SAAS,EACT,qBAAqB,EACrB,kBAAkB,GAInB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC"}
|