create-authhero 0.42.0 → 0.44.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.
@@ -0,0 +1,161 @@
1
+ # AuthHero Proxy
2
+
3
+ A Cloudflare Worker that proxies incoming requests to upstream services based on the request's `Host` header. Built on [@authhero/proxy](https://www.npmjs.com/package/@authhero/proxy).
4
+
5
+ This template ships with a static, in-file config so you can run it immediately. For production you'll typically swap that adapter for one that reads routes from your authhero deployment — see [Data sources](#data-sources) below.
6
+
7
+ ## Configure your routes (default)
8
+
9
+ Edit [src/proxy.config.ts](src/proxy.config.ts) to map each public hostname to one or more upstream routes. Path patterns support `*` and `:param` segments, and routes are matched in priority order (lower wins).
10
+
11
+ ```ts
12
+ export const proxyConfig: StaticProxyAdapterOptions = {
13
+ hosts: {
14
+ "id.example.com": {
15
+ tenant_id: "example",
16
+ routes: [
17
+ {
18
+ path_pattern: "/*",
19
+ upstream_type: "http",
20
+ upstream_url: "https://upstream.example.com",
21
+ },
22
+ ],
23
+ },
24
+ },
25
+ };
26
+ ```
27
+
28
+ ## Data sources
29
+
30
+ The proxy reads its routes through a `ProxyDataAdapter`. Three implementations are common:
31
+
32
+ | Adapter | Best for | Notes |
33
+ | --- | --- | --- |
34
+ | **Static** (default) | Local dev, small fixed deployments | Routes baked into the worker bundle; re-deploy to change them. |
35
+ | **Database** | Same-process or co-located deployments | Reads directly from the proxy_routes table that authhero writes to. |
36
+ | **HTTP / management API** | Geographically distributed proxies, hosted Workers | Calls `/api/v2/proxy-routes` on your authhero server with a service token. |
37
+
38
+ The authhero server exposes the management API (`/api/v2/proxy-routes`) and creates the underlying table automatically once the standard adapter migrations have run — see the `local` template's [src/index.ts](../local/src/index.ts).
39
+
40
+ ### Database-backed (Kysely)
41
+
42
+ Add the adapter and your Kysely driver, then swap the data line:
43
+
44
+ ```bash
45
+ npm install @authhero/kysely-adapter kysely
46
+ ```
47
+
48
+ ```ts
49
+ import { Kysely } from "kysely";
50
+ import { createProxyApp } from "@authhero/proxy";
51
+ import { createProxyDataAdapter } from "@authhero/kysely-adapter";
52
+
53
+ const db = new Kysely({ dialect: /* your dialect */ });
54
+ const app = createProxyApp({
55
+ data: createProxyDataAdapter(db),
56
+ });
57
+ ```
58
+
59
+ Cloudflare Workers can't open SQLite files, so on Workers this path means D1, Hyperdrive, or a remote MySQL/Postgres. For a local Node process, `better-sqlite3` pointing at the same `db.sqlite` your authhero server uses works out of the box.
60
+
61
+ ### HTTP-backed (management API)
62
+
63
+ The proxy authenticates to authhero's management API with a service token. Issue the token from authhero with the scopes `read:proxy_routes` and `read:custom_domains`, then store it as a worker secret:
64
+
65
+ ```bash
66
+ wrangler secret put AUTHHERO_SERVICE_TOKEN
67
+ ```
68
+
69
+ `wrangler.toml` vars:
70
+
71
+ ```toml
72
+ [vars]
73
+ AUTHHERO_API_URL = "https://auth.example.com"
74
+ AUTHHERO_TENANT_ID = "example"
75
+ ```
76
+
77
+ Then build an adapter that resolves the host via `/api/v2/custom-domains` and the routes via `/api/v2/proxy-routes`:
78
+
79
+ ```ts
80
+ import type { ProxyDataAdapter, ResolvedHost } from "@authhero/proxy";
81
+
82
+ interface Env {
83
+ AUTHHERO_API_URL: string;
84
+ AUTHHERO_TENANT_ID: string;
85
+ AUTHHERO_SERVICE_TOKEN: string;
86
+ }
87
+
88
+ function createHttpProxyAdapter(env: Env): ProxyDataAdapter {
89
+ const headers = {
90
+ "authorization": `Bearer ${env.AUTHHERO_SERVICE_TOKEN}`,
91
+ "tenant-id": env.AUTHHERO_TENANT_ID,
92
+ };
93
+
94
+ async function api<T>(path: string): Promise<T> {
95
+ const res = await fetch(`${env.AUTHHERO_API_URL}${path}`, { headers });
96
+ if (!res.ok) throw new Error(`${path}: ${res.status}`);
97
+ return res.json() as Promise<T>;
98
+ }
99
+
100
+ return {
101
+ // The proxy data plane only needs resolveHost; the CRUD methods on
102
+ // proxyRoutes stay unused (writes always go through authhero directly).
103
+ proxyRoutes: {
104
+ list: () => { throw new Error("read-only proxy adapter"); },
105
+ get: () => { throw new Error("read-only proxy adapter"); },
106
+ create: () => { throw new Error("read-only proxy adapter"); },
107
+ update: () => { throw new Error("read-only proxy adapter"); },
108
+ remove: () => { throw new Error("read-only proxy adapter"); },
109
+ },
110
+ async resolveHost(host): Promise<ResolvedHost | null> {
111
+ const domains = await api<{ custom_domains: Array<{
112
+ custom_domain_id: string; domain: string;
113
+ }> }>("/api/v2/custom-domains");
114
+ const match = domains.custom_domains.find((d) => d.domain === host);
115
+ if (!match) return null;
116
+
117
+ const routes = await api<{ proxy_routes: unknown[] }>(
118
+ `/api/v2/proxy-routes?custom_domain_id=${match.custom_domain_id}&per_page=200`,
119
+ );
120
+ return {
121
+ tenant_id: env.AUTHHERO_TENANT_ID,
122
+ custom_domain_id: match.custom_domain_id,
123
+ domain: host,
124
+ routes: routes.proxy_routes as ResolvedHost["routes"],
125
+ };
126
+ },
127
+ };
128
+ }
129
+ ```
130
+
131
+ Cache aggressively (see below) — every cold cache miss is two API round-trips.
132
+
133
+ ## Caching
134
+
135
+ Resolved hosts are cached in-memory per Worker isolate with a stale-while-revalidate strategy:
136
+
137
+ - **Fresh** for 5 minutes — served directly from cache.
138
+ - **Stale** for the next hour — served from cache while a background refresh runs.
139
+ - **Negative** (host not found) — cached for 30 seconds so newly-added hosts come online quickly.
140
+
141
+ For the static adapter the "refresh" is just a re-read of the in-memory config, so the SWR window mainly matters when you swap to the HTTP- or database-backed adapter.
142
+
143
+ ## Develop locally
144
+
145
+ ```bash
146
+ npm run dev
147
+ ```
148
+
149
+ The worker is served at `http://localhost:8787`. To exercise a specific host, send the `Host` header:
150
+
151
+ ```bash
152
+ curl http://localhost:8787/login -H "Host: id.example.com"
153
+ ```
154
+
155
+ ## Deploy
156
+
157
+ ```bash
158
+ npm run deploy
159
+ ```
160
+
161
+ Add a custom-domain route in `wrangler.toml` for each hostname you've configured, or attach the worker to existing routes in the Cloudflare dashboard.
@@ -0,0 +1,40 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import {
3
+ createProxyApp,
4
+ createStaticProxyAdapter,
5
+ } from "@authhero/proxy";
6
+ import { proxyConfig } from "./proxy.config";
7
+
8
+ // AsyncLocalStorage threads each request's ExecutionContext through to the
9
+ // host cache so background SWR refreshes keep running after the response
10
+ // returns. Requires the `nodejs_compat` compatibility flag.
11
+ interface RequestCtx {
12
+ waitUntil: (promise: Promise<unknown>) => void;
13
+ }
14
+ const requestCtx = new AsyncLocalStorage<RequestCtx>();
15
+
16
+ const data = createStaticProxyAdapter(proxyConfig);
17
+
18
+ const app = createProxyApp({
19
+ data,
20
+ cache: {
21
+ // Serve cached values directly for 5 minutes.
22
+ freshTtlMs: 5 * 60_000,
23
+ // For the next hour, keep serving the cached value and refresh in the
24
+ // background. After that, the next request blocks on a fresh fetch.
25
+ staleTtlMs: 60 * 60_000,
26
+ // Don't cache "host not found" for as long — new hosts should become
27
+ // reachable quickly after being added to the config.
28
+ negativeTtlMs: 30_000,
29
+ waitUntil: (promise) => requestCtx.getStore()?.waitUntil(promise),
30
+ },
31
+ });
32
+
33
+ export default {
34
+ fetch(request: Request, _env: unknown, ctx: ExecutionContext) {
35
+ return requestCtx.run(
36
+ { waitUntil: ctx.waitUntil.bind(ctx) },
37
+ () => app.fetch(request),
38
+ );
39
+ },
40
+ };
@@ -0,0 +1,32 @@
1
+ import type { StaticProxyAdapterOptions } from "@authhero/proxy";
2
+
3
+ // Map each public hostname to the routes the proxy should serve for it.
4
+ // Routes are matched in priority order (lower priority wins). The path
5
+ // pattern in `match.path` supports `*` and `:param` segments (Hono syntax).
6
+ //
7
+ // Each route is an ordered list of handlers. The last handler is the
8
+ // terminal — it produces the response (e.g. `http`, `redirect`, `static`,
9
+ // `service_binding`). Earlier handlers wrap it, like Hono middleware.
10
+ //
11
+ // Edit this file to add your hosts, then re-deploy.
12
+ export const proxyConfig: StaticProxyAdapterOptions = {
13
+ hosts: {
14
+ "id.example.com": {
15
+ tenant_id: "example",
16
+ routes: [
17
+ {
18
+ match: { path: "/*" },
19
+ handlers: [
20
+ {
21
+ type: "http",
22
+ options: {
23
+ upstream_url: "https://upstream.example.com",
24
+ preserve_host: false,
25
+ },
26
+ },
27
+ ],
28
+ },
29
+ ],
30
+ },
31
+ },
32
+ };
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "types": ["@cloudflare/workers-types", "node"]
11
+ },
12
+ "include": ["src/**/*"],
13
+ "exclude": ["node_modules"]
14
+ }
@@ -0,0 +1,22 @@
1
+ # ════════════════════════════════════════════════════════════════════════════
2
+ # AuthHero Proxy — Cloudflare Worker Configuration
3
+ # ════════════════════════════════════════════════════════════════════════════
4
+
5
+ name = "authhero-proxy"
6
+ main = "src/index.ts"
7
+ compatibility_date = "2025-05-23"
8
+ compatibility_flags = ["nodejs_compat"]
9
+
10
+ # ════════════════════════════════════════════════════════════════════════════
11
+ # OPTIONAL: Custom Domains
12
+ # ════════════════════════════════════════════════════════════════════════════
13
+ # Route the public hostnames you configured in src/proxy.config.ts to this
14
+ # worker. Each host should be a custom domain (or have a route added) in your
15
+ # Cloudflare account.
16
+ #
17
+ # [[routes]]
18
+ # pattern = "id.example.com"
19
+ # custom_domain = true
20
+
21
+ [observability]
22
+ enabled = true
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "type": "git",
6
6
  "url": "https://github.com/markusahlstrand/authhero"
7
7
  },
8
- "version": "0.42.0",
8
+ "version": "0.44.0",
9
9
  "type": "module",
10
10
  "main": "dist/create-authhero.js",
11
11
  "bin": {
@@ -19,10 +19,10 @@
19
19
  "@rollup/plugin-commonjs": "^26.0.1",
20
20
  "@rollup/plugin-node-resolve": "^15.2.3",
21
21
  "@types/inquirer": "^9.0.7",
22
- "@types/node": "^20.14.9",
23
- "tsx": "^4.19.4",
22
+ "@types/node": "^20.19.41",
23
+ "tsx": "^4.22.3",
24
24
  "typescript": "^5.5.2",
25
- "vite": "^5.3.2"
25
+ "vite": "^8.0.14"
26
26
  },
27
27
  "dependencies": {
28
28
  "commander": "^12.1.0",