@redirhub/cloudflare-redirects 0.1.0 → 0.2.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/README.md CHANGED
@@ -22,6 +22,21 @@ and this package passes its [golden sample](spec/v1). [RedirHub](https://www.red
22
22
  publishes the file for you: you manage the redirects in its dashboard, API or MCP server and see
23
23
  the redirects and 404s in its analytics.
24
24
 
25
+ ## For AI coding assistants
26
+
27
+ To put this in front of a site on Cloudflare:
28
+
29
+ 1. `npm install @redirhub/cloudflare-redirects` in a Worker project.
30
+ 2. `export default createWorker({ settings: (env) => ({ rulesUrl: env.ACME_RULES_URL, key: env.ACME_INTEGRATION_KEY }) })`
31
+ as the Worker's entry; pass `origin` only if requests should go somewhere other than `fetch(request)`.
32
+ 3. In `wrangler.toml`: a route `<hostname>/*` on the zone, the rules address as a plain `[vars]`
33
+ value, and the integration key as a secret (`wrangler secret put ACME_INTEGRATION_KEY`). Names
34
+ come from the RedirHub dashboard.
35
+ 4. Check: `GET https://<hostname>/_redirhub/check` answers `token=… etag=…`, and a rule's path
36
+ redirects with its status. Then press **Check now** in the dashboard.
37
+
38
+ Do not reimplement the rules format: this package passes the specification's golden sample.
39
+
25
40
  ## When to use it
26
41
 
27
42
  Cloudflare's own Redirect Rules and Bulk Redirects are managed in the Cloudflare dashboard or API.
@@ -84,12 +99,47 @@ add your own logic, pass `origin: (request, env, ctx) => …`.
84
99
  | Option | |
85
100
  | --- | --- |
86
101
  | `settings(env)` | The rules address and the key from the Worker's bindings. Empty address: off, every request goes to the origin. |
102
+ | `report` | `false`, or `(request) => boolean`: whether to report, over the key. Default: whenever there is a key. |
87
103
  | `origin(request, env, ctx)` | Where requests no rule answers go; default `fetch(request)`. |
88
104
  | `report404` | `rules` (default): 404s of hostnames with **Report 404 pages** on; `all`: every hostname the Worker serves. |
89
- | `clientIp(request)` | The visitor's address; default `CF-Connecting-IP`. |
105
+ | `hostname(request)` | The hostname to look up in the rules; `null` leaves the request alone (straight to the origin, nothing reported). Default the `Host` header. |
106
+ | `path(request)` | The path to look up in the rules, percent-encoded; `null` leaves the request alone. Default the request URL's path. |
107
+ | `location(location, request)` | The `Location` to send, from the one the rule gives; also the reported destination. |
108
+ | `clientIp(request)` | The visitor's address in reports; default `CF-Connecting-IP`. `null` writes `-`. |
109
+ | `userAgent(request)` | The User-Agent in reports; default the `User-Agent` header. `null` writes `-`. |
90
110
  | `anonymizeIp` | Report IPv4 with the last byte zeroed, IPv6 cut to /48. |
91
111
  | `logger` | Default `console`; `null` for silence. Failures are logged at most once per ten minutes. |
92
112
 
113
+ ## Customize
114
+
115
+ Each option that reads the request is a function of it, so a Worker decides with ordinary code:
116
+
117
+ ```ts
118
+ export default createWorker<Env>({
119
+ settings: (env) => ({ rulesUrl: env.ACME_RULES_URL, key: env.ACME_INTEGRATION_KEY }),
120
+ // Which hostname to look up in the rules. null: leave the request alone.
121
+ hostname: (request) => {
122
+ const host = new URL(request.url).hostname
123
+ if (host === 'api.example.com') return null // never through the rules
124
+ if (host.endsWith('.workers.dev')) return 'shop.example.com' // testing on workers.dev
125
+ return host
126
+ },
127
+ // Paths that never go through the rules.
128
+ path: (request) => {
129
+ const { pathname } = new URL(request.url)
130
+ return pathname.startsWith('/api/') ? null : pathname
131
+ },
132
+ // Testing on workers.dev redirects, but is not reported.
133
+ report: (request) => !new URL(request.url).hostname.endsWith('.workers.dev'),
134
+ anonymizeIp: true,
135
+ })
136
+ ```
137
+
138
+ The hostname and path these return are the ones matched, answered on the check path, appended by
139
+ a catch-all and reported, so they always agree. Options that identify the request (`hostname`,
140
+ `path`) leave it alone on `null`: it goes straight to the origin, and the rules are not even loaded
141
+ for it. Options that are only reported (`clientIp`, `userAgent`) write `-` on `null`.
142
+
93
143
  ## Behaviour
94
144
 
95
145
  Only **GET and HEAD** on a **hostname listed** in the rules are answered; everything else goes to
@@ -108,10 +158,12 @@ Matching follows format v1: the hostname in any case, port ignored; the path cas
108
158
  percent-decoded, leading and trailing slashes ignored; scheme and query ignored. Every redirect
109
159
  carries `X-Powered-By` when the rules file has `@powered-by`.
110
160
 
111
- **Rules download.** The first request of an isolate downloads the file (5 second timeout) and
112
- waits for it; after that the copy in memory is used, and once it is older than `@refresh` seconds
113
- (60 by default) it is revalidated in `ctx.waitUntil` with `If-None-Match`, so no visitor waits.
114
- Any failure keeps the last good copy and waits 30 seconds before trying again. `@notify` (push) is
161
+ **Rules download.** The first request of a new isolate starts the download and waits for it at
162
+ most one second; if the rules server is slower, that request goes to the origin without rules and
163
+ the download (5 second timeout) finishes in `ctx.waitUntil`. Requests that arrive meanwhile do not
164
+ wait. After that the copy in memory is used, and once it is older than `@refresh` seconds (60 by
165
+ default) it is revalidated in `ctx.waitUntil` with `If-None-Match`, so no visitor waits. Any
166
+ failure keeps the last good copy and waits 30 seconds before trying again. `@notify` (push) is
115
167
  ignored: a Worker keeps no connection open.
116
168
 
117
169
  **Reports.** A line queued while nothing is being sent goes out at once; lines queued while a
@@ -139,7 +191,8 @@ rules file, with your key.
139
191
 
140
192
  ## Limits
141
193
 
142
- - The rules live in each isolate's memory: a new isolate waits for one download.
194
+ - The rules live in each isolate's memory: a new isolate waits up to one second for them, and
195
+ serves no redirects until they arrive.
143
196
  - Changes are live within `@refresh` seconds per isolate (60 by default).
144
197
  - The rules file is at most 32 MB, and must fit the Worker's memory.
145
198
 
@@ -160,6 +213,14 @@ npm run check:core # src/core and test/core are the same as redirhub/nextjs-re
160
213
  are shared, file for file, with [nextjs-redirects](https://github.com/redirhub/nextjs-redirects).
161
214
  Change both repositories together; CI flags a difference. `src/index.ts` is the Workers part.
162
215
 
216
+ ## Releasing
217
+
218
+ Bump `version` in `package.json` and `VERSION` in `src/version.ts` (a test checks they match) and
219
+ merge to `main`. The Release workflow sees a version without a tag, runs the checks, publishes to
220
+ npm through Trusted Publishing (with provenance, no npm token stored anywhere), then tags the
221
+ commit `v<version>` and creates the GitHub Release. Nobody pushes a tag by hand; a push without a
222
+ version change releases nothing.
223
+
163
224
  ## License
164
225
 
165
226
  Apache License 2.0, see [LICENSE](LICENSE). Copyright 2026 RedirHub.
@@ -13,22 +13,60 @@ export interface RedirectsOptions {
13
13
  rulesUrl: string | null | undefined;
14
14
  /** The integration key. Without it redirects work and nothing is reported. */
15
15
  key?: string | null;
16
+ /**
17
+ * Whether to report, over the key: `false` reports nothing, a function decides per request
18
+ * (e.g. not for preview deployments). Default: report whenever there is a key.
19
+ */
20
+ report?: boolean | ((request: Request) => boolean);
16
21
  /**
17
22
  * Which 404 answers of the site are reported (where the site's answer is seen, `afterSite`):
18
23
  * `rules` (default) for hostnames whose `@host` line has `report404`, `all` for every hostname.
19
24
  */
20
25
  report404?: 'rules' | 'all';
21
- /** The visitor's address: only a value the platform sets, never a header a client can forge. */
26
+ /**
27
+ * The hostname to look up in the rules, report and answer the check path for. Return null to
28
+ * leave the request alone: no rules are loaded for it and nothing is reported. Default: the
29
+ * `Host` header (behind `next start` the request URL names the server, not the site), else the
30
+ * request URL's host. Any letter case and a port are fine.
31
+ */
32
+ hostname?: (request: Request) => string | null;
33
+ /**
34
+ * The path the visitor asked for, percent-encoded as in a URL (`/caf%C3%A9`): it is matched,
35
+ * appended by a catch-all with `p` and reported. Return null to leave the request alone, as with
36
+ * `hostname`. Default: the request URL's path. Set it where the platform changed the path before
37
+ * your code sees it, e.g. a proxy that strips a prefix.
38
+ */
39
+ path?: (request: Request) => string | null;
40
+ /**
41
+ * The `Location` to send, from the one the rule gives (e.g. a same-site target in the visitor's
42
+ * language). It is also what the report names as the destination. Default: as the rule gives it.
43
+ */
44
+ location?: (location: string, request: Request) => string;
45
+ /**
46
+ * The visitor's address in reports: only a value the platform sets, never a header a client can
47
+ * forge. Null writes `-`.
48
+ */
22
49
  clientIp?: (request: Request) => string | null;
50
+ /** The User-Agent in reports. Default: the `User-Agent` header. Null writes `-`. */
51
+ userAgent?: (request: Request) => string | null;
23
52
  /** `http` or `https` as the visitor asked; default the request URL's scheme. */
24
53
  scheme?: (request: Request) => string;
25
54
  /** Report IPv4 addresses with the last byte zeroed and IPv6 addresses cut to /48. */
26
55
  anonymizeIp?: boolean;
27
- fetch?: typeof fetch;
28
56
  /** Default `console`; null logs nothing. */
29
57
  logger?: Logger | null;
58
+ }
59
+ /**
60
+ * What the platform entry and the tests set, and users do not: the platform entries export a
61
+ * `createRedirects` that takes RedirectsOptions only.
62
+ */
63
+ export interface InternalOptions extends RedirectsOptions {
64
+ /** Next.js passes one that sets `cache: 'no-store'`; tests pass fakes. */
65
+ fetch?: typeof fetch;
30
66
  /** The clock report lines are stamped with; tests fix it. */
31
67
  now?: () => Date;
68
+ /** How long the request that starts the first rules download waits for it; default 1000. */
69
+ firstWaitMs?: number;
32
70
  }
33
71
  /** What lets work outlive the response: Next's `event`, a Worker's `ctx`. */
34
72
  export interface WaitUntil {
@@ -50,6 +88,6 @@ export interface Redirects {
50
88
  /** Sends the report lines still waiting (tests, graceful shutdown). */
51
89
  flush(): Promise<void>;
52
90
  }
53
- export declare function createRedirects(options: RedirectsOptions): Redirects;
91
+ export declare function createRedirects(options: InternalOptions): Redirects;
54
92
  /** IPv4 with the last byte zeroed, IPv6 cut to its first 48 bits; anything else unchanged. */
55
93
  export declare function anonymize(ip: string): string;
@@ -15,7 +15,7 @@ const WARN_EVERY_MS = 10 * 60_000;
15
15
  export function createRedirects(options) {
16
16
  const logger = options.logger === undefined ? console : (options.logger ?? undefined);
17
17
  const rulesUrl = options.rulesUrl?.trim() ?? '';
18
- const source = rulesUrl === '' ? null : new RulesSource({ url: rulesUrl, fetch: options.fetch, logger });
18
+ const source = rulesUrl === '' ? null : new RulesSource({ url: rulesUrl, fetch: options.fetch, firstWaitMs: options.firstWaitMs, logger });
19
19
  const key = options.key?.trim() ?? '';
20
20
  const reporter = source && key !== '' ? new Reporter({ key, hitsUrl: () => source.current?.hitsUrl ?? null, fetch: options.fetch, logger }) : null;
21
21
  const now = options.now ?? (() => new Date());
@@ -26,9 +26,24 @@ export function createRedirects(options) {
26
26
  logger?.warn(`${PACKAGE}: request passed on after an error (${error instanceof Error ? error.message : String(error)})`);
27
27
  }
28
28
  };
29
+ /**
30
+ * What the visitor asked for: the hostname as listed in the rules (lowercase, no port) and the
31
+ * URL with the visitor's path, or null when `hostname` or `path` leaves the request alone.
32
+ */
33
+ const visitOf = (request) => {
34
+ const url = new URL(request.url);
35
+ const hostname = options.hostname ? options.hostname(request) : (request.headers.get('host') ?? url.host);
36
+ const name = hostname === null ? '' : normalizeHost(hostname);
37
+ const path = options.path ? options.path(request) : url.pathname;
38
+ if (name === '' || path === null) {
39
+ return null;
40
+ }
41
+ url.pathname = path;
42
+ return { name, url };
43
+ };
29
44
  const keepAliveOf = (context) => (context ? (work) => context.waitUntil(work) : undefined);
30
45
  const report = (request, url, host, status, size, keepAlive, redirect) => {
31
- if (!reporter) {
46
+ if (!reporter || options.report === false || (typeof options.report === 'function' && !options.report(request))) {
32
47
  return;
33
48
  }
34
49
  const ip = options.clientIp?.(request) ?? null;
@@ -41,12 +56,13 @@ export function createRedirects(options) {
41
56
  status,
42
57
  size,
43
58
  referer: request.headers.get('referer'),
44
- userAgent: request.headers.get('user-agent'),
59
+ userAgent: options.userAgent ? options.userAgent(request) : request.headers.get('user-agent'),
45
60
  redirect,
46
61
  }, now()), keepAlive);
47
62
  };
48
63
  const redirect = (request, url, rules, host, rule, keepAlive) => {
49
- const location = buildLocation(rule, url);
64
+ const target = buildLocation(rule, url);
65
+ const location = options.location ? options.location(target, request) : target;
50
66
  const headers = new Headers({ Location: byteString(location) });
51
67
  if (rules.poweredBy) {
52
68
  headers.set('X-Powered-By', rules.poweredBy);
@@ -59,13 +75,18 @@ export function createRedirects(options) {
59
75
  return null;
60
76
  }
61
77
  try {
78
+ // Before the rules: a request the integration leaves alone never waits for them.
79
+ const visit = visitOf(request);
80
+ if (visit === null) {
81
+ return null;
82
+ }
83
+ const { name, url } = visit;
62
84
  const keepAlive = keepAliveOf(context);
63
85
  const rules = await source.get(keepAlive);
64
- const host = rules?.hosts.get(hostnameOf(request));
86
+ const host = rules?.hosts.get(name);
65
87
  if (!rules || !host) {
66
88
  return null;
67
89
  }
68
- const url = new URL(request.url);
69
90
  if (url.pathname === rules.checkPath) {
70
91
  const body = `token=${host.token} etag=${rules.etag}`;
71
92
  return new Response(request.method === 'HEAD' ? null : body, {
@@ -90,15 +111,18 @@ export function createRedirects(options) {
90
111
  if (!rules) {
91
112
  return response;
92
113
  }
93
- const name = hostnameOf(request);
114
+ const visit = visitOf(request);
115
+ if (visit === null) {
116
+ return response;
117
+ }
118
+ const { name, url } = visit;
94
119
  const host = rules.hosts.get(name);
95
120
  const keepAlive = keepAliveOf(context);
96
- const url = new URL(request.url);
97
121
  if (host?.catchAll) {
98
122
  await response.body?.cancel();
99
123
  return redirect(request, url, rules, host, host.catchAll, keepAlive);
100
124
  }
101
- if ((host?.report404 || options.report404 === 'all') && name !== '') {
125
+ if (host?.report404 || options.report404 === 'all') {
102
126
  report(request, url, name, 404, Number(response.headers.get('content-length') ?? 0) || 0, keepAlive);
103
127
  }
104
128
  return response;
@@ -119,10 +143,6 @@ export function createRedirects(options) {
119
143
  },
120
144
  };
121
145
  }
122
- /** The request's hostname: the `Host` header (a self-hosted server's URL names itself), else the URL's. */
123
- function hostnameOf(request) {
124
- return normalizeHost(request.headers.get('host') ?? new URL(request.url).host);
125
- }
126
146
  /** Header values carry bytes: a UTF-8 target is sent as its UTF-8 bytes, as other implementations do. */
127
147
  function byteString(value) {
128
148
  return /^[\x00-\x7f]*$/.test(value) ? value : String.fromCharCode(...new TextEncoder().encode(value));
@@ -86,13 +86,15 @@ export class Reporter {
86
86
  #dropped = 0;
87
87
  #lastDropLog = Number.NEGATIVE_INFINITY;
88
88
  constructor(options) {
89
+ // `??` per field, not a spread: an option passed as undefined keeps its default.
89
90
  this.#options = {
91
+ key: options.key,
92
+ hitsUrl: options.hitsUrl,
90
93
  fetch: options.fetch ?? ((input, init) => fetch(input, init)),
91
- timeoutMs: 2000,
92
- maxQueue: 10_000,
93
- budgetMs: 10_000,
94
- ...options,
95
94
  logger: options.logger,
95
+ timeoutMs: options.timeoutMs ?? 2000,
96
+ maxQueue: options.maxQueue ?? 10_000,
97
+ budgetMs: options.budgetMs ?? 10_000,
96
98
  };
97
99
  }
98
100
  /** Queues one line; `keepAlive` holds the invocation open while this call has to send. */
@@ -1,9 +1,15 @@
1
1
  /**
2
2
  * Downloads the rules file and keeps the last good copy in module memory, for runtimes that keep
3
3
  * no process or connection between requests (serverless functions, Workers): no timer, no push
4
- * channel. The first download is awaited; after that a stale copy is answered at once and
5
- * refreshed in the background, so no visitor waits for it. Fails open: before the first good copy,
6
- * or with no rules address, there are no rules, and nothing throws.
4
+ * channel. Fails open: before the first good copy, or with no rules address, there are no rules,
5
+ * and nothing throws.
6
+ *
7
+ * No visitor waits long for a download. The request that starts a new instance's first download
8
+ * waits for it at most `firstWaitMs`; if it takes longer, that request goes on without rules and
9
+ * the download finishes in the background. Requests that arrive while a download is running do
10
+ * not wait at all. After the first copy, a stale copy is used at once and refreshed in the
11
+ * background. Only the request that starts a download keeps it alive (`keepAlive`): no request
12
+ * waits on another request's work.
7
13
  */
8
14
  import type { Logger } from './log.ts';
9
15
  import type { KeepAlive } from './report.ts';
@@ -16,6 +22,8 @@ export interface RulesSourceOptions {
16
22
  logger?: Logger;
17
23
  /** Per download; default 5000. */
18
24
  timeoutMs?: number;
25
+ /** How long the request that starts the first download waits for it; default 1000. */
26
+ firstWaitMs?: number;
19
27
  /** After a failed download, wait this long before the next try; default 30,000. */
20
28
  retryMs?: number;
21
29
  /** A larger file counts as a failed download; default 32 MB. */
@@ -26,6 +34,6 @@ export declare class RulesSource {
26
34
  constructor(options: RulesSourceOptions);
27
35
  /** The rules in use now, without downloading. */
28
36
  get current(): RuleSet | null;
29
- /** The rules in use, downloading them when due; null until the first download succeeds. */
37
+ /** The rules in use, starting a download when one is due; null while there are none. */
30
38
  get(keepAlive?: KeepAlive): Promise<RuleSet | null>;
31
39
  }
@@ -1,9 +1,15 @@
1
1
  /**
2
2
  * Downloads the rules file and keeps the last good copy in module memory, for runtimes that keep
3
3
  * no process or connection between requests (serverless functions, Workers): no timer, no push
4
- * channel. The first download is awaited; after that a stale copy is answered at once and
5
- * refreshed in the background, so no visitor waits for it. Fails open: before the first good copy,
6
- * or with no rules address, there are no rules, and nothing throws.
4
+ * channel. Fails open: before the first good copy, or with no rules address, there are no rules,
5
+ * and nothing throws.
6
+ *
7
+ * No visitor waits long for a download. The request that starts a new instance's first download
8
+ * waits for it at most `firstWaitMs`; if it takes longer, that request goes on without rules and
9
+ * the download finishes in the background. Requests that arrive while a download is running do
10
+ * not wait at all. After the first copy, a stale copy is used at once and refreshed in the
11
+ * background. Only the request that starts a download keeps it alive (`keepAlive`): no request
12
+ * waits on another request's work.
7
13
  */
8
14
  import { parseRules } from "./rules.js";
9
15
  import { PACKAGE, USER_AGENT } from "../version.js";
@@ -18,31 +24,38 @@ export class RulesSource {
18
24
  #inflight = null;
19
25
  #lastWarn = Number.NEGATIVE_INFINITY;
20
26
  constructor(options) {
27
+ // `??` per field, not a spread: an option passed as undefined keeps its default.
21
28
  this.#options = {
29
+ url: options.url,
22
30
  fetch: options.fetch ?? ((input, init) => fetch(input, init)),
23
- timeoutMs: 5000,
24
- retryMs: 30_000,
25
- maxBytes: 32 << 20,
26
- ...options,
27
31
  logger: options.logger,
32
+ timeoutMs: options.timeoutMs ?? 5000,
33
+ firstWaitMs: options.firstWaitMs ?? 1000,
34
+ retryMs: options.retryMs ?? 30_000,
35
+ maxBytes: options.maxBytes ?? 32 << 20,
28
36
  };
29
37
  }
30
38
  /** The rules in use now, without downloading. */
31
39
  get current() {
32
40
  return this.#rules;
33
41
  }
34
- /** The rules in use, downloading them when due; null until the first download succeeds. */
42
+ /** The rules in use, starting a download when one is due; null while there are none. */
35
43
  async get(keepAlive) {
36
44
  const now = Date.now();
37
45
  const due = this.#rules === null || now - this.#fetchedAt >= this.#rules.refresh * 1000;
38
- if (due && now >= this.#nextTry) {
39
- this.#inflight ??= this.#download();
40
- if (this.#rules === null) {
41
- await this.#inflight;
42
- }
43
- else {
44
- keepAlive?.(this.#inflight);
45
- }
46
+ if (!due || now < this.#nextTry || this.#inflight !== null) {
47
+ return this.#rules;
48
+ }
49
+ // Cleared once settled; set first, so even a download that fails at once is cleared.
50
+ const download = this.#download().finally(() => {
51
+ this.#inflight = null;
52
+ });
53
+ this.#inflight = download;
54
+ keepAlive?.(download);
55
+ if (this.#rules === null) {
56
+ let timer;
57
+ await Promise.race([download, new Promise((resolve) => (timer = setTimeout(resolve, this.#options.firstWaitMs)))]);
58
+ clearTimeout(timer);
46
59
  }
47
60
  return this.#rules;
48
61
  }
@@ -84,9 +97,6 @@ export class RulesSource {
84
97
  catch (error) {
85
98
  this.#fail(error instanceof Error ? `${error.name}: ${error.message}` : 'error');
86
99
  }
87
- finally {
88
- this.#inflight = null;
89
- }
90
100
  }
91
101
  #fail(reason) {
92
102
  const now = Date.now();
package/dist/index.d.ts CHANGED
@@ -3,7 +3,9 @@
3
3
  * check path and exact rules, passes everything else to the origin, and where the origin answers
4
4
  * 404 redirects to the catch-all or reports the 404.
5
5
  */
6
- import { type RedirectsOptions, type WaitUntil } from './core/redirects.ts';
6
+ import { type Redirects, type RedirectsOptions, type WaitUntil } from './core/redirects.ts';
7
+ export type { Logger } from './core/log.ts';
8
+ export type { Redirects, RedirectsOptions, WaitUntil };
7
9
  export interface WorkerSettings {
8
10
  rulesUrl: string | null | undefined;
9
11
  key?: string | null;
@@ -26,11 +28,8 @@ export interface WorkerOptions<Env> extends Omit<RedirectsOptions, 'rulesUrl' |
26
28
  export declare function createWorker<Env>(options: WorkerOptions<Env>): {
27
29
  fetch(request: Request, env: Env, context: WaitUntil): Promise<Response>;
28
30
  };
29
- /** `CF-Connecting-IP`, which Cloudflare sets. */
30
- export declare function cloudflareClientIp(request: Request): string | null;
31
- export { anonymize, createRedirects, type Redirects, type RedirectsOptions, type WaitUntil } from './core/redirects.ts';
32
- export type { Logger } from './core/log.ts';
33
- export { hitLine, type Hit, type KeepAlive, Reporter, type ReporterOptions } from './core/report.ts';
34
- export { buildLocation, DEFAULT_CHECK_PATH, FORMAT_MAJOR, type HostRules, matchExact, matchPath, normalizeHost, parseRules, type RedirectStatus, type Rule, type RuleSet, UnknownFormatError, } from './core/rules.ts';
35
- export { RulesSource, type RulesSourceOptions } from './core/source.ts';
36
- export { PACKAGE, USER_AGENT, VERSION } from './version.ts';
31
+ /**
32
+ * The integration in two steps, for a Worker with its own routing: `handle` before the origin,
33
+ * `afterSite` after it, or `serve` for both. The visitor's address defaults to `CF-Connecting-IP`.
34
+ */
35
+ export declare function createRedirects(options: RedirectsOptions): Redirects;
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * check path and exact rules, passes everything else to the origin, and where the origin answers
4
4
  * 404 redirects to the catch-all or reports the 404.
5
5
  */
6
- import { createRedirects } from "./core/redirects.js";
6
+ import { createRedirects as create } from "./core/redirects.js";
7
7
  /**
8
8
  * A module Worker:
9
9
  *
@@ -22,18 +22,20 @@ export function createWorker(options) {
22
22
  const { rulesUrl, key } = settings(env);
23
23
  const id = `${rulesUrl ?? ''}\0${key ?? ''}`;
24
24
  if (instance?.id !== id) {
25
- instance = { id, redirects: createRedirects({ clientIp: cloudflareClientIp, ...rest, rulesUrl, key }) };
25
+ instance = { id, redirects: createRedirects({ ...rest, rulesUrl, key }) };
26
26
  }
27
27
  return instance.redirects.serve(request, (next) => origin(next, env, context), context);
28
28
  },
29
29
  };
30
30
  }
31
+ /**
32
+ * The integration in two steps, for a Worker with its own routing: `handle` before the origin,
33
+ * `afterSite` after it, or `serve` for both. The visitor's address defaults to `CF-Connecting-IP`.
34
+ */
35
+ export function createRedirects(options) {
36
+ return create({ ...options, clientIp: options.clientIp ?? cloudflareClientIp });
37
+ }
31
38
  /** `CF-Connecting-IP`, which Cloudflare sets. */
32
- export function cloudflareClientIp(request) {
39
+ function cloudflareClientIp(request) {
33
40
  return request.headers.get('cf-connecting-ip');
34
41
  }
35
- export { anonymize, createRedirects } from "./core/redirects.js";
36
- export { hitLine, Reporter } from "./core/report.js";
37
- export { buildLocation, DEFAULT_CHECK_PATH, FORMAT_MAJOR, matchExact, matchPath, normalizeHost, parseRules, UnknownFormatError, } from "./core/rules.js";
38
- export { RulesSource } from "./core/source.js";
39
- export { PACKAGE, USER_AGENT, VERSION } from "./version.js";
package/dist/version.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** This package: names its log lines and the User-Agent of rules downloads and reports. */
2
2
  export declare const PACKAGE = "cloudflare-redirects";
3
3
  /** Equal to package.json's version (a test checks it). */
4
- export declare const VERSION = "0.1.0";
5
- export declare const USER_AGENT = "cloudflare-redirects/0.1.0";
4
+ export declare const VERSION = "0.2.0";
5
+ export declare const USER_AGENT = "cloudflare-redirects/0.2.0";
package/dist/version.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /** This package: names its log lines and the User-Agent of rules downloads and reports. */
2
2
  export const PACKAGE = 'cloudflare-redirects';
3
3
  /** Equal to package.json's version (a test checks it). */
4
- export const VERSION = '0.1.0';
4
+ export const VERSION = '0.2.0';
5
5
  export const USER_AGENT = `${PACKAGE}/${VERSION}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redirhub/cloudflare-redirects",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Bulk redirects and 404 monitoring for Cloudflare Workers from a remote rules file: thousands of 301/302 redirects, short links and website migrations in front of your origin, live within a minute without a redeploy. RedirHub Integration Mode.",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -59,5 +59,9 @@
59
59
  "devDependencies": {
60
60
  "@types/node": "^22.10.0",
61
61
  "typescript": "^5.9.0"
62
+ },
63
+ "publishConfig": {
64
+ "access": "public",
65
+ "provenance": true
62
66
  }
63
67
  }