create-authhero 0.45.0 → 0.47.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/dist/cloudflare/src/index.ts +4 -5
- package/dist/cloudflare/wrangler.toml +1 -1
- package/dist/cloudflare-control-plane/.dev.vars.example +17 -0
- package/dist/cloudflare-control-plane/README.md +59 -0
- package/dist/cloudflare-control-plane/copy-assets.js +132 -0
- package/dist/cloudflare-control-plane/drizzle.config.ts +17 -0
- package/dist/cloudflare-control-plane/scripts/decrypt-field.mjs +33 -0
- package/dist/cloudflare-control-plane/scripts/generate-encryption-key.mjs +7 -0
- package/dist/cloudflare-control-plane/seed-helper.js +113 -0
- package/dist/cloudflare-control-plane/src/app.ts +74 -0
- package/dist/cloudflare-control-plane/src/index.ts +72 -0
- package/dist/cloudflare-control-plane/src/seed.ts +56 -0
- package/dist/cloudflare-control-plane/src/types.ts +14 -0
- package/dist/cloudflare-control-plane/tsconfig.json +14 -0
- package/dist/cloudflare-control-plane/wrangler.toml +46 -0
- package/dist/cloudflare-wfp-dispatcher/README.md +94 -0
- package/dist/cloudflare-wfp-dispatcher/src/index.ts +72 -0
- package/dist/cloudflare-wfp-dispatcher/src/types.ts +17 -0
- package/dist/cloudflare-wfp-dispatcher/tsconfig.json +14 -0
- package/dist/cloudflare-wfp-dispatcher/wrangler.toml +56 -0
- package/dist/cloudflare-wfp-tenant/.dev.vars.example +17 -0
- package/dist/cloudflare-wfp-tenant/README.md +62 -0
- package/dist/cloudflare-wfp-tenant/copy-assets.js +132 -0
- package/dist/cloudflare-wfp-tenant/drizzle.config.ts +17 -0
- package/dist/cloudflare-wfp-tenant/scripts/decrypt-field.mjs +33 -0
- package/dist/cloudflare-wfp-tenant/scripts/generate-encryption-key.mjs +7 -0
- package/dist/cloudflare-wfp-tenant/src/app.ts +37 -0
- package/dist/cloudflare-wfp-tenant/src/index.ts +69 -0
- package/dist/cloudflare-wfp-tenant/src/types.ts +16 -0
- package/dist/cloudflare-wfp-tenant/tsconfig.json +14 -0
- package/dist/cloudflare-wfp-tenant/wrangler.toml +46 -0
- package/dist/create-authhero.js +184 -37
- package/dist/proxy/src/index.ts +3 -7
- package/package.json +1 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
# AuthHero Control Plane Worker
|
|
3
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
4
|
+
# The management surface + rollout source for a Workers-for-Platforms setup.
|
|
5
|
+
# It manages tenants (/api/v2/tenants), serves colocated tenants, and projects
|
|
6
|
+
# the control plane's default connections/prompts/branding into each WFP
|
|
7
|
+
# tenant's own database via POST /internal/tenants/:id/sync-defaults.
|
|
8
|
+
#
|
|
9
|
+
# Pair with:
|
|
10
|
+
# - cloudflare-wfp-dispatcher (front door)
|
|
11
|
+
# - cloudflare-wfp-tenant (per-tenant Workers)
|
|
12
|
+
#
|
|
13
|
+
# Sensitive IDs (database_id) go in wrangler.local.toml (gitignored).
|
|
14
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
15
|
+
|
|
16
|
+
name = "authhero-control-plane"
|
|
17
|
+
main = "src/index.ts"
|
|
18
|
+
compatibility_date = "2026-05-01"
|
|
19
|
+
compatibility_flags = ["nodejs_compat"]
|
|
20
|
+
|
|
21
|
+
[assets]
|
|
22
|
+
directory = "./dist/assets"
|
|
23
|
+
|
|
24
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
25
|
+
# Control plane database (shared platform D1)
|
|
26
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
27
|
+
# Holds the control plane tenant's rows (the defaults bundle), tenant records,
|
|
28
|
+
# custom_domains and proxy_routes. The dispatcher reads this same database.
|
|
29
|
+
[[d1_databases]]
|
|
30
|
+
binding = "AUTH_DB"
|
|
31
|
+
database_name = "authhero-db"
|
|
32
|
+
database_id = "local" # Use "local" for local dev, or your actual ID in wrangler.local.toml
|
|
33
|
+
migrations_dir = "node_modules/@authhero/drizzle/drizzle"
|
|
34
|
+
|
|
35
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
36
|
+
# Encryption keys (set as secrets, not here)
|
|
37
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
38
|
+
# wrangler secret put ENCRYPTION_KEY # control plane's own key
|
|
39
|
+
# wrangler secret put CONTROL_PLANE_ENCRYPTION_KEY # shared "cp" key
|
|
40
|
+
#
|
|
41
|
+
# CONTROL_PLANE_ENCRYPTION_KEY must be byte-identical to the key each tenant
|
|
42
|
+
# Worker holds, so projected secrets decrypt on the tenant side.
|
|
43
|
+
|
|
44
|
+
# Optional: Enable observability
|
|
45
|
+
# [observability]
|
|
46
|
+
# enabled = true
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# AuthHero WFP Dispatcher
|
|
2
|
+
|
|
3
|
+
Thin Cloudflare Worker that fronts a Workers-for-Platforms deployment of AuthHero. Resolves an incoming request's `Host` header to a tenant via the shared platform D1 (`custom_domains` table) and dispatches the request to that tenant's full authhero auth server, deployed as a script in a Cloudflare dispatch namespace.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Internet
|
|
9
|
+
|
|
|
10
|
+
v
|
|
11
|
+
[This worker — the dispatcher]
|
|
12
|
+
1. Host header -> custom_domains -> tenant_id
|
|
13
|
+
2. env.DISPATCHER.get('tenant-<id>-auth').fetch(request)
|
|
14
|
+
|
|
|
15
|
+
v
|
|
16
|
+
[authhero-tenants dispatch namespace]
|
|
17
|
+
|- tenant-acme-auth (full authhero, deployed from the `cloudflare` template)
|
|
18
|
+
|- tenant-bob-auth
|
|
19
|
+
|- ...
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Tenant workers come from the **`cloudflare` create-authhero template** — they're the same single-tenant auth server, deployed into the namespace instead of standalone.
|
|
23
|
+
|
|
24
|
+
## Prerequisites
|
|
25
|
+
|
|
26
|
+
- A Cloudflare account on the **Workers for Platforms** plan (required for dispatch namespaces).
|
|
27
|
+
- [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/install-and-update/).
|
|
28
|
+
- A D1 database (shared with the tenant workers).
|
|
29
|
+
|
|
30
|
+
## One-time platform setup
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# 1. Create the dispatch namespace
|
|
34
|
+
npx wrangler dispatch-namespace create authhero-tenants
|
|
35
|
+
|
|
36
|
+
# 2. Create the shared D1 (or reuse the one your tenant workers use)
|
|
37
|
+
npx wrangler d1 create authhero-db
|
|
38
|
+
|
|
39
|
+
# 3. Copy wrangler.toml -> wrangler.local.toml and paste the database_id
|
|
40
|
+
|
|
41
|
+
# 4. Install project dependencies (provides the local wrangler used by the
|
|
42
|
+
# db:migrate:* scripts)
|
|
43
|
+
npm install
|
|
44
|
+
|
|
45
|
+
# 5. Apply migrations to the shared D1
|
|
46
|
+
npm run db:migrate:remote
|
|
47
|
+
# Or, without installing dependencies first:
|
|
48
|
+
# npx wrangler d1 migrations apply AUTH_DB --remote --config wrangler.local.toml
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Deploy the dispatcher
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm run deploy
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Onboard a tenant
|
|
58
|
+
|
|
59
|
+
For each publisher:
|
|
60
|
+
|
|
61
|
+
1. **Provision the tenant in D1** — insert a `tenants` row, then a `custom_domains` row mapping their domain to the tenant_id. (Either via the authhero management API or by direct D1 query.)
|
|
62
|
+
|
|
63
|
+
2. **Deploy their auth worker into the namespace.** From the sibling `cloudflare` template:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# In the tenant's directory (scaffolded from `create-authhero --template=cloudflare`)
|
|
67
|
+
npx wrangler deploy \
|
|
68
|
+
--dispatch-namespace=authhero-tenants \
|
|
69
|
+
--name=tenant-<tenant_id>-auth
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
3. **Point their custom domain at this dispatcher worker** (Cloudflare → Workers → Triggers → Add Custom Domain on this worker, or via DNS to the worker's `*.workers.dev` route).
|
|
73
|
+
|
|
74
|
+
Once those three steps are done, a request to `auth.<their-domain>/authorize?...` flows to this dispatcher → resolved via custom_domains → dispatched to their tenant worker.
|
|
75
|
+
|
|
76
|
+
## Per-tenant routing customization
|
|
77
|
+
|
|
78
|
+
By default, hosts with no `proxy_routes` rows get a single catch-all that dispatches to `tenant-<tenant_id>-auth`. If a tenant needs richer routing — different middleware chains, CORS, a special path that bypasses the namespace — insert `proxy_routes` rows for that `custom_domain_id`. The dispatcher uses the configured routes verbatim instead of the default.
|
|
79
|
+
|
|
80
|
+
The script-name template (`tenant-{tenant_id}-auth`) can be overridden globally via the `SCRIPT_NAME_TEMPLATE` env var. Supported placeholders: `{tenant_id}`, `{custom_domain_id}`, `{domain}`, `{host}`.
|
|
81
|
+
|
|
82
|
+
## Local development
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npm run dev
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`wrangler dev` runs against a **local SQLite-backed D1**. The dispatch namespace cannot be emulated locally — for end-to-end tests, deploy to a real Cloudflare account using `npm run dev:remote`.
|
|
89
|
+
|
|
90
|
+
## Files
|
|
91
|
+
|
|
92
|
+
- `src/index.ts` — dispatcher worker entrypoint
|
|
93
|
+
- `src/types.ts` — Env interface (D1 binding + DISPATCHER namespace binding)
|
|
94
|
+
- `wrangler.toml` — Cloudflare config (assets, D1, dispatch namespace)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { drizzle } from "drizzle-orm/d1";
|
|
2
|
+
import { createProxyDataAdapter } from "@authhero/drizzle";
|
|
3
|
+
import * as schema from "@authhero/drizzle/schema/sqlite";
|
|
4
|
+
import {
|
|
5
|
+
createProxyApp,
|
|
6
|
+
type ProxyDataAdapter,
|
|
7
|
+
type ResolvedHost,
|
|
8
|
+
} from "@authhero/proxy";
|
|
9
|
+
import type { Env } from "./types";
|
|
10
|
+
|
|
11
|
+
// `tenant-{tenant_id}-auth` is the deploy convention used by this template's
|
|
12
|
+
// per-tenant worker setup. Override with the SCRIPT_NAME_TEMPLATE env var or
|
|
13
|
+
// by configuring proxy_routes rows per tenant for richer routing.
|
|
14
|
+
const DEFAULT_SCRIPT_NAME_TEMPLATE = "tenant-{tenant_id}-auth";
|
|
15
|
+
|
|
16
|
+
// If a host resolves to a known tenant but has no proxy_routes configured,
|
|
17
|
+
// synthesize a single catch-all that dispatches to the tenant's auth worker
|
|
18
|
+
// in the namespace. Operators who need middleware (CORS, headers, rate
|
|
19
|
+
// limiting) per tenant can add real proxy_routes rows and this fallback
|
|
20
|
+
// stays out of the way.
|
|
21
|
+
function withDefaultDispatchRoute(
|
|
22
|
+
inner: ProxyDataAdapter,
|
|
23
|
+
binding: string,
|
|
24
|
+
scriptNameTemplate: string,
|
|
25
|
+
): ProxyDataAdapter {
|
|
26
|
+
return {
|
|
27
|
+
proxyRoutes: inner.proxyRoutes,
|
|
28
|
+
resolveHost: async (host): Promise<ResolvedHost | null> => {
|
|
29
|
+
const resolved = await inner.resolveHost(host);
|
|
30
|
+
if (!resolved || resolved.routes.length > 0) return resolved;
|
|
31
|
+
const now = new Date(0).toISOString();
|
|
32
|
+
return {
|
|
33
|
+
...resolved,
|
|
34
|
+
routes: [
|
|
35
|
+
{
|
|
36
|
+
id: `default-${resolved.custom_domain_id}`,
|
|
37
|
+
tenant_id: resolved.tenant_id,
|
|
38
|
+
custom_domain_id: resolved.custom_domain_id,
|
|
39
|
+
priority: 1000,
|
|
40
|
+
match: { path: "/*" },
|
|
41
|
+
handlers: [
|
|
42
|
+
{
|
|
43
|
+
type: "dispatch_namespace",
|
|
44
|
+
options: { binding, script_name: scriptNameTemplate },
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
created_at: now,
|
|
48
|
+
updated_at: now,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default {
|
|
57
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
58
|
+
const db = drizzle(env.AUTH_DB, { schema });
|
|
59
|
+
const data = withDefaultDispatchRoute(
|
|
60
|
+
createProxyDataAdapter(db),
|
|
61
|
+
"DISPATCHER",
|
|
62
|
+
env.SCRIPT_NAME_TEMPLATE || DEFAULT_SCRIPT_NAME_TEMPLATE,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const app = createProxyApp({
|
|
66
|
+
data,
|
|
67
|
+
bindings: { DISPATCHER: env.DISPATCHER },
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return app.fetch(request);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
+
|
|
3
|
+
export interface Env {
|
|
4
|
+
// The shared platform D1 database. Holds custom_domains (host -> tenant)
|
|
5
|
+
// and proxy_routes (optional per-host route overrides).
|
|
6
|
+
AUTH_DB: D1Database;
|
|
7
|
+
|
|
8
|
+
// The Cloudflare Workers for Platforms dispatch namespace where each
|
|
9
|
+
// tenant's auth worker is deployed. Wrangler binding configured in
|
|
10
|
+
// wrangler.toml as `[[dispatch_namespaces]] binding = "DISPATCHER"`.
|
|
11
|
+
DISPATCHER: DispatchNamespace;
|
|
12
|
+
|
|
13
|
+
// Optional template for resolving a tenant to a dispatch namespace
|
|
14
|
+
// script name. Defaults to `tenant-{tenant_id}-auth`. Supported
|
|
15
|
+
// placeholders: {tenant_id}, {custom_domain_id}, {domain}, {host}.
|
|
16
|
+
SCRIPT_NAME_TEMPLATE?: string;
|
|
17
|
+
}
|
|
@@ -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"]
|
|
11
|
+
},
|
|
12
|
+
"include": ["src/**/*"],
|
|
13
|
+
"exclude": ["node_modules"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
# AuthHero WFP Dispatcher Configuration
|
|
3
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
4
|
+
# Thin Cloudflare Worker that resolves a request's Host header to a tenant
|
|
5
|
+
# (via the shared platform D1) and dispatches to that tenant's full
|
|
6
|
+
# authhero auth server, deployed as a script in the `authhero-tenants`
|
|
7
|
+
# dispatch namespace.
|
|
8
|
+
#
|
|
9
|
+
# Tenant workers are deployed separately via the `cloudflare` template,
|
|
10
|
+
# using `wrangler deploy --dispatch-namespace=authhero-tenants
|
|
11
|
+
# --name=tenant-<id>-auth`.
|
|
12
|
+
#
|
|
13
|
+
# Sensitive IDs (database_id) go in wrangler.local.toml (gitignored) or
|
|
14
|
+
# GitHub Secrets, not this file.
|
|
15
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
16
|
+
|
|
17
|
+
name = "authhero-wfp-dispatcher"
|
|
18
|
+
main = "src/index.ts"
|
|
19
|
+
compatibility_date = "2026-05-01"
|
|
20
|
+
compatibility_flags = ["nodejs_compat"]
|
|
21
|
+
|
|
22
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
23
|
+
# Dispatch namespace binding
|
|
24
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
25
|
+
# Create the namespace once before the first deploy:
|
|
26
|
+
# npx wrangler dispatch-namespace create authhero-tenants
|
|
27
|
+
[[dispatch_namespaces]]
|
|
28
|
+
binding = "DISPATCHER"
|
|
29
|
+
namespace = "authhero-tenants"
|
|
30
|
+
|
|
31
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
32
|
+
# Shared platform D1 database
|
|
33
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
34
|
+
# Same D1 that the tenant workers read/write. Holds custom_domains and
|
|
35
|
+
# proxy_routes that drive the dispatcher's host -> tenant resolution.
|
|
36
|
+
[[d1_databases]]
|
|
37
|
+
binding = "AUTH_DB"
|
|
38
|
+
database_name = "authhero-db"
|
|
39
|
+
database_id = "local" # Use "local" for local dev, or your actual ID in wrangler.local.toml
|
|
40
|
+
migrations_dir = "node_modules/@authhero/drizzle/drizzle"
|
|
41
|
+
|
|
42
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
43
|
+
# OPTIONAL: Custom Domain
|
|
44
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
45
|
+
# Point your platform's wildcard or per-publisher custom domains at this
|
|
46
|
+
# worker. The publishers' subdomains must resolve here so the dispatcher
|
|
47
|
+
# can route into the namespace.
|
|
48
|
+
#
|
|
49
|
+
# [[routes]]
|
|
50
|
+
# pattern = "*.auth.yourplatform.com/*"
|
|
51
|
+
# zone_name = "yourplatform.com"
|
|
52
|
+
|
|
53
|
+
# Optional: Enable observability for `wrangler tail` insight into dispatch
|
|
54
|
+
# decisions and namespace invocation latency.
|
|
55
|
+
# [observability]
|
|
56
|
+
# enabled = true
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# Development Environment Variables — WFP Tenant Worker
|
|
3
|
+
# ============================================================================
|
|
4
|
+
# Copy this file to .dev.vars and fill in your values.
|
|
5
|
+
# ============================================================================
|
|
6
|
+
|
|
7
|
+
# This tenant's own at-rest encryption key (base64-encoded 32 bytes).
|
|
8
|
+
# `create-authhero` writes a generated key here for local dev. In production:
|
|
9
|
+
# wrangler secret put ENCRYPTION_KEY
|
|
10
|
+
# Generate one with: openssl rand -base64 32
|
|
11
|
+
# ENCRYPTION_KEY=
|
|
12
|
+
|
|
13
|
+
# The CONTROL PLANE key (key id "cp"). Decrypts the shared secrets the control
|
|
14
|
+
# plane projected into this tenant's database. Must be byte-identical to the
|
|
15
|
+
# control plane's CONTROL_PLANE_ENCRYPTION_KEY. In production:
|
|
16
|
+
# wrangler secret put CONTROL_PLANE_ENCRYPTION_KEY
|
|
17
|
+
# CONTROL_PLANE_ENCRYPTION_KEY=
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# AuthHero — WFP Tenant Worker
|
|
2
|
+
|
|
3
|
+
The full `authhero` app for **one tenant**, deployed into a Workers-for-Platforms
|
|
4
|
+
dispatch namespace. It reads only its **own D1** and inherits the control
|
|
5
|
+
plane's defaults (shared social logins, prompts, branding, system resource
|
|
6
|
+
servers, inheritable hooks) from rows the **control plane rollout** projects
|
|
7
|
+
into that database.
|
|
8
|
+
|
|
9
|
+
This is one of three pieces:
|
|
10
|
+
|
|
11
|
+
| Piece | Template |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| Front door (host → tenant → dispatch) | `cloudflare-wfp-dispatcher` |
|
|
14
|
+
| **This tenant Worker** | `cloudflare-wfp-tenant` |
|
|
15
|
+
| Control plane (rollout source + management) | `cloudflare-control-plane` |
|
|
16
|
+
|
|
17
|
+
## How defaults work
|
|
18
|
+
|
|
19
|
+
`src/index.ts` layers the data adapter:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
D1 → keyed encryption (tenant key + "cp" key) → withRuntimeFallback(control_plane)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`withRuntimeFallback` resolves the control-plane rows that the rollout wrote into
|
|
26
|
+
this database under the `control_plane` tenant id — the same read path a
|
|
27
|
+
control-plane-colocated tenant uses. **No request-time call to the control
|
|
28
|
+
plane.**
|
|
29
|
+
|
|
30
|
+
## Secrets
|
|
31
|
+
|
|
32
|
+
Two keys, both Worker secrets (never in the database):
|
|
33
|
+
|
|
34
|
+
- `ENCRYPTION_KEY` — this tenant's own secrets.
|
|
35
|
+
- `CONTROL_PLANE_ENCRYPTION_KEY` — the shared `cp` key. Decrypts the inherited
|
|
36
|
+
secrets (e.g. Google `client_secret`). It must be **byte-identical** to the
|
|
37
|
+
control plane's key, or the inherited secrets won't decrypt. A raw export of
|
|
38
|
+
`AUTH_DB` keeps those secrets opaque without it.
|
|
39
|
+
|
|
40
|
+
## Setup
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install
|
|
44
|
+
npm run setup # creates wrangler.local.toml + .dev.vars (ENCRYPTION_KEY generated)
|
|
45
|
+
# paste CONTROL_PLANE_ENCRYPTION_KEY (from the control plane) into .dev.vars
|
|
46
|
+
npm run migrate # apply schema to this tenant's D1
|
|
47
|
+
npm run dev
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Deploy into the namespace
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# one Worker per tenant
|
|
54
|
+
wrangler deploy --dispatch-namespace=authhero-tenants --name=tenant-<id>-auth
|
|
55
|
+
wrangler secret put ENCRYPTION_KEY --name tenant-<id>-auth
|
|
56
|
+
wrangler secret put CONTROL_PLANE_ENCRYPTION_KEY --name tenant-<id>-auth
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
After the Worker and its D1 exist, run the control plane's
|
|
60
|
+
`sync-defaults` for this tenant so its inherited rows are populated. See the
|
|
61
|
+
[Control Plane Defaults](https://authhero.net/docs/customization/multi-tenancy/control-plane-defaults)
|
|
62
|
+
docs for the full flow.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Copy AuthHero assets to dist directory
|
|
5
|
+
*
|
|
6
|
+
* This script copies static assets from the authhero package to the dist directory
|
|
7
|
+
* so they can be served as static files. Most deployment targets do not support
|
|
8
|
+
* serving files directly from node_modules.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = path.dirname(__filename);
|
|
17
|
+
|
|
18
|
+
const sourceDir = path.join(
|
|
19
|
+
__dirname,
|
|
20
|
+
"node_modules",
|
|
21
|
+
"authhero",
|
|
22
|
+
"dist",
|
|
23
|
+
"assets",
|
|
24
|
+
);
|
|
25
|
+
const targetDir = path.join(__dirname, "dist", "assets");
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Recursively copy directory contents
|
|
29
|
+
*/
|
|
30
|
+
function copyDirectory(src, dest) {
|
|
31
|
+
// Create destination directory if it doesn't exist
|
|
32
|
+
if (!fs.existsSync(dest)) {
|
|
33
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Read source directory
|
|
37
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
38
|
+
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
const srcPath = path.join(src, entry.name);
|
|
41
|
+
const destPath = path.join(dest, entry.name);
|
|
42
|
+
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
copyDirectory(srcPath, destPath);
|
|
45
|
+
} else {
|
|
46
|
+
fs.copyFileSync(srcPath, destPath);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
console.log("📦 Copying AuthHero assets...");
|
|
53
|
+
|
|
54
|
+
if (!fs.existsSync(sourceDir)) {
|
|
55
|
+
console.error(`❌ Source directory not found: ${sourceDir}`);
|
|
56
|
+
console.error("Make sure the authhero package is installed.");
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Clean target directory to remove stale files from previous builds
|
|
61
|
+
if (fs.existsSync(targetDir)) {
|
|
62
|
+
fs.rmSync(targetDir, { recursive: true });
|
|
63
|
+
console.log("🧹 Cleaned old assets");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
copyDirectory(sourceDir, targetDir);
|
|
67
|
+
|
|
68
|
+
// Also copy widget files from @authhero/widget package
|
|
69
|
+
const widgetSourceDir = path.join(
|
|
70
|
+
__dirname,
|
|
71
|
+
"node_modules",
|
|
72
|
+
"@authhero",
|
|
73
|
+
"widget",
|
|
74
|
+
"dist",
|
|
75
|
+
"authhero-widget",
|
|
76
|
+
);
|
|
77
|
+
const widgetTargetDir = path.join(targetDir, "u", "widget");
|
|
78
|
+
|
|
79
|
+
if (fs.existsSync(widgetSourceDir)) {
|
|
80
|
+
console.log("📦 Copying widget assets...");
|
|
81
|
+
copyDirectory(widgetSourceDir, widgetTargetDir);
|
|
82
|
+
} else {
|
|
83
|
+
console.warn(`⚠️ Widget directory not found: ${widgetSourceDir}`);
|
|
84
|
+
console.warn(
|
|
85
|
+
"Widget features may not work. Install @authhero/widget to enable.",
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Copy admin UI files from @authhero/admin package
|
|
90
|
+
const adminSourceDir = path.join(
|
|
91
|
+
__dirname,
|
|
92
|
+
"node_modules",
|
|
93
|
+
"@authhero",
|
|
94
|
+
"admin",
|
|
95
|
+
"dist",
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
if (fs.existsSync(adminSourceDir)) {
|
|
99
|
+
console.log("📦 Copying admin UI assets...");
|
|
100
|
+
const adminTargetDir = path.join(targetDir, "admin");
|
|
101
|
+
copyDirectory(adminSourceDir, adminTargetDir);
|
|
102
|
+
|
|
103
|
+
// Inject runtime config into index.html
|
|
104
|
+
// Uses window.location.origin so the admin UI automatically points to its own server
|
|
105
|
+
const adminIndexPath = path.join(adminSourceDir, "index.html");
|
|
106
|
+
const adminHtml = fs
|
|
107
|
+
.readFileSync(adminIndexPath, "utf-8")
|
|
108
|
+
.replace(/src="\.\/assets\//g, 'src="/admin/assets/')
|
|
109
|
+
.replace(/href="\.\/assets\//g, 'href="/admin/assets/');
|
|
110
|
+
const configScript = `<script>window.__AUTHHERO_ADMIN_CONFIG__={domain:window.location.origin,clientId:"default",basePath:"/admin"}</script>`;
|
|
111
|
+
const injectedHtml = adminHtml.replace(
|
|
112
|
+
"</head>",
|
|
113
|
+
configScript + "\n</head>",
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Write injected HTML to CDN assets (for direct /admin/ access)
|
|
117
|
+
fs.writeFileSync(path.join(adminTargetDir, "index.html"), injectedHtml);
|
|
118
|
+
|
|
119
|
+
// Write as TS module for worker to import (for SPA fallback on deep links)
|
|
120
|
+
const srcDir = path.join(__dirname, "src");
|
|
121
|
+
fs.writeFileSync(
|
|
122
|
+
path.join(srcDir, "admin-index-html.ts"),
|
|
123
|
+
`export default ${JSON.stringify(injectedHtml)};\n`,
|
|
124
|
+
);
|
|
125
|
+
console.log("✅ Admin UI assets copied and configured");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.log(`✅ Assets copied to ${targetDir}`);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error("❌ Error copying assets:", error.message);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineConfig } from "drizzle-kit";
|
|
2
|
+
|
|
3
|
+
// ⚠️ WARNING: Do not run `drizzle-kit generate` or `npm run db:generate`
|
|
4
|
+
//
|
|
5
|
+
// This configuration is for reference only. Migrations are pre-generated and
|
|
6
|
+
// shipped with the @authhero/drizzle package. The schema is managed by AuthHero
|
|
7
|
+
// and should not be customized to ensure compatibility with future updates.
|
|
8
|
+
//
|
|
9
|
+
// To apply migrations:
|
|
10
|
+
// Local: npm run migrate
|
|
11
|
+
// Remote: npm run db:migrate:remote
|
|
12
|
+
|
|
13
|
+
export default defineConfig({
|
|
14
|
+
out: "./node_modules/@authhero/drizzle/drizzle",
|
|
15
|
+
schema: "./node_modules/@authhero/drizzle/src/schema/sqlite/index.ts",
|
|
16
|
+
dialect: "sqlite",
|
|
17
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { loadEncryptionKey, decryptField } from "authhero";
|
|
3
|
+
|
|
4
|
+
// Decrypt a stored field value using ENCRYPTION_KEY from the environment.
|
|
5
|
+
// Usage: node --env-file=.env scripts/decrypt-field.mjs "enc:v1:..."
|
|
6
|
+
// Values without the enc:v1: prefix (legacy plaintext) are printed unchanged.
|
|
7
|
+
const value = process.argv[2];
|
|
8
|
+
|
|
9
|
+
if (!value) {
|
|
10
|
+
console.error(
|
|
11
|
+
'Usage: node --env-file=<env> scripts/decrypt-field.mjs "<value>"',
|
|
12
|
+
);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const keyB64 = process.env.ENCRYPTION_KEY;
|
|
17
|
+
if (!keyB64) {
|
|
18
|
+
console.error(
|
|
19
|
+
"ENCRYPTION_KEY is not set. Pass it via --env-file or the environment.",
|
|
20
|
+
);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const key = await loadEncryptionKey(keyB64);
|
|
26
|
+
console.log(await decryptField(value, key));
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error(
|
|
29
|
+
"Failed to decrypt:",
|
|
30
|
+
error instanceof Error ? error.message : error,
|
|
31
|
+
);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
|
|
4
|
+
// Print a fresh base64-encoded 32-byte (AES-256) key suitable for
|
|
5
|
+
// ENCRYPTION_KEY. Copy the output into your env file (.env / .dev.vars) or set
|
|
6
|
+
// it as a production secret.
|
|
7
|
+
console.log(crypto.randomBytes(32).toString("base64"));
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Context } from "hono";
|
|
2
|
+
import { AuthHeroConfig, init } from "authhero";
|
|
3
|
+
import { swaggerUI } from "@hono/swagger-ui";
|
|
4
|
+
|
|
5
|
+
// A WFP tenant Worker serves a single tenant; its defaults are inherited from
|
|
6
|
+
// the control plane via the rows projected into its own database (see
|
|
7
|
+
// src/index.ts). No multi-tenancy routing is needed here.
|
|
8
|
+
export default function createApp(config: AuthHeroConfig) {
|
|
9
|
+
const { app } = init(config);
|
|
10
|
+
|
|
11
|
+
app
|
|
12
|
+
.onError((err, ctx) => {
|
|
13
|
+
// Duck-typing avoids instanceof issues with bundled dependencies.
|
|
14
|
+
if (
|
|
15
|
+
err &&
|
|
16
|
+
typeof err === "object" &&
|
|
17
|
+
"getResponse" in err &&
|
|
18
|
+
typeof (err as { getResponse?: unknown }).getResponse === "function"
|
|
19
|
+
) {
|
|
20
|
+
return (err as { getResponse: () => Response }).getResponse();
|
|
21
|
+
}
|
|
22
|
+
console.error(err);
|
|
23
|
+
return ctx.text(
|
|
24
|
+
err instanceof Error ? err.message : "Internal Server Error",
|
|
25
|
+
500,
|
|
26
|
+
);
|
|
27
|
+
})
|
|
28
|
+
.get("/", async (ctx: Context) => {
|
|
29
|
+
return ctx.json({
|
|
30
|
+
name: "AuthHero WFP Tenant Server",
|
|
31
|
+
status: "running",
|
|
32
|
+
});
|
|
33
|
+
})
|
|
34
|
+
.get("/docs", swaggerUI({ url: "/api/v2/spec" }));
|
|
35
|
+
|
|
36
|
+
return app;
|
|
37
|
+
}
|