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.
Files changed (34) hide show
  1. package/dist/cloudflare/src/index.ts +4 -5
  2. package/dist/cloudflare/wrangler.toml +1 -1
  3. package/dist/cloudflare-control-plane/.dev.vars.example +17 -0
  4. package/dist/cloudflare-control-plane/README.md +59 -0
  5. package/dist/cloudflare-control-plane/copy-assets.js +132 -0
  6. package/dist/cloudflare-control-plane/drizzle.config.ts +17 -0
  7. package/dist/cloudflare-control-plane/scripts/decrypt-field.mjs +33 -0
  8. package/dist/cloudflare-control-plane/scripts/generate-encryption-key.mjs +7 -0
  9. package/dist/cloudflare-control-plane/seed-helper.js +113 -0
  10. package/dist/cloudflare-control-plane/src/app.ts +74 -0
  11. package/dist/cloudflare-control-plane/src/index.ts +72 -0
  12. package/dist/cloudflare-control-plane/src/seed.ts +56 -0
  13. package/dist/cloudflare-control-plane/src/types.ts +14 -0
  14. package/dist/cloudflare-control-plane/tsconfig.json +14 -0
  15. package/dist/cloudflare-control-plane/wrangler.toml +46 -0
  16. package/dist/cloudflare-wfp-dispatcher/README.md +94 -0
  17. package/dist/cloudflare-wfp-dispatcher/src/index.ts +72 -0
  18. package/dist/cloudflare-wfp-dispatcher/src/types.ts +17 -0
  19. package/dist/cloudflare-wfp-dispatcher/tsconfig.json +14 -0
  20. package/dist/cloudflare-wfp-dispatcher/wrangler.toml +56 -0
  21. package/dist/cloudflare-wfp-tenant/.dev.vars.example +17 -0
  22. package/dist/cloudflare-wfp-tenant/README.md +62 -0
  23. package/dist/cloudflare-wfp-tenant/copy-assets.js +132 -0
  24. package/dist/cloudflare-wfp-tenant/drizzle.config.ts +17 -0
  25. package/dist/cloudflare-wfp-tenant/scripts/decrypt-field.mjs +33 -0
  26. package/dist/cloudflare-wfp-tenant/scripts/generate-encryption-key.mjs +7 -0
  27. package/dist/cloudflare-wfp-tenant/src/app.ts +37 -0
  28. package/dist/cloudflare-wfp-tenant/src/index.ts +69 -0
  29. package/dist/cloudflare-wfp-tenant/src/types.ts +16 -0
  30. package/dist/cloudflare-wfp-tenant/tsconfig.json +14 -0
  31. package/dist/cloudflare-wfp-tenant/wrangler.toml +46 -0
  32. package/dist/create-authhero.js +184 -37
  33. package/dist/proxy/src/index.ts +3 -7
  34. 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
+ }