create-authhero 0.46.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 (26) hide show
  1. package/dist/cloudflare-control-plane/.dev.vars.example +17 -0
  2. package/dist/cloudflare-control-plane/README.md +59 -0
  3. package/dist/cloudflare-control-plane/copy-assets.js +132 -0
  4. package/dist/cloudflare-control-plane/drizzle.config.ts +17 -0
  5. package/dist/cloudflare-control-plane/scripts/decrypt-field.mjs +33 -0
  6. package/dist/cloudflare-control-plane/scripts/generate-encryption-key.mjs +7 -0
  7. package/dist/cloudflare-control-plane/seed-helper.js +113 -0
  8. package/dist/cloudflare-control-plane/src/app.ts +74 -0
  9. package/dist/cloudflare-control-plane/src/index.ts +72 -0
  10. package/dist/cloudflare-control-plane/src/seed.ts +56 -0
  11. package/dist/cloudflare-control-plane/src/types.ts +14 -0
  12. package/dist/cloudflare-control-plane/tsconfig.json +14 -0
  13. package/dist/cloudflare-control-plane/wrangler.toml +46 -0
  14. package/dist/cloudflare-wfp-tenant/.dev.vars.example +17 -0
  15. package/dist/cloudflare-wfp-tenant/README.md +62 -0
  16. package/dist/cloudflare-wfp-tenant/copy-assets.js +132 -0
  17. package/dist/cloudflare-wfp-tenant/drizzle.config.ts +17 -0
  18. package/dist/cloudflare-wfp-tenant/scripts/decrypt-field.mjs +33 -0
  19. package/dist/cloudflare-wfp-tenant/scripts/generate-encryption-key.mjs +7 -0
  20. package/dist/cloudflare-wfp-tenant/src/app.ts +37 -0
  21. package/dist/cloudflare-wfp-tenant/src/index.ts +69 -0
  22. package/dist/cloudflare-wfp-tenant/src/types.ts +16 -0
  23. package/dist/cloudflare-wfp-tenant/tsconfig.json +14 -0
  24. package/dist/cloudflare-wfp-tenant/wrangler.toml +46 -0
  25. package/dist/create-authhero.js +134 -27
  26. package/package.json +1 -1
@@ -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
+ }
@@ -0,0 +1,69 @@
1
+ import { drizzle } from "drizzle-orm/d1";
2
+ import createAdapters from "@authhero/drizzle";
3
+ import * as schema from "@authhero/drizzle/schema/sqlite";
4
+ import {
5
+ AuthHeroConfig,
6
+ DataAdapters,
7
+ createEncryptedDataAdapter,
8
+ createEncryptedDataAdapterWithKeyRing,
9
+ loadEncryptionKey,
10
+ type KeyRing,
11
+ } from "authhero";
12
+ import { withRuntimeFallback } from "@authhero/multi-tenancy";
13
+ import createApp from "./app";
14
+ import { Env } from "./types";
15
+
16
+ // The control plane tenant id whose projected defaults this tenant inherits.
17
+ // Must match the control plane Worker's CONTROL_PLANE_TENANT_ID.
18
+ const CONTROL_PLANE_TENANT_ID = "control_plane";
19
+
20
+ export default {
21
+ async fetch(request: Request, env: Env): Promise<Response> {
22
+ const url = new URL(request.url);
23
+ const issuer = `${url.protocol}//${url.host}/`;
24
+ const origin = request.headers.get("Origin") || "";
25
+
26
+ const db = drizzle(env.AUTH_DB, { schema });
27
+ let dataAdapter: DataAdapters = createAdapters(db, {
28
+ useTransactions: false,
29
+ });
30
+
31
+ // Encrypt at rest. With both keys present, this tenant's own secrets use
32
+ // ENCRYPTION_KEY while control-plane-tenant rows (the inherited Google
33
+ // secret, etc.) use the "cp" key id — readable by this Worker, but opaque
34
+ // in a raw export of AUTH_DB.
35
+ if (env.ENCRYPTION_KEY && env.CONTROL_PLANE_ENCRYPTION_KEY) {
36
+ const ring: KeyRing = {
37
+ default: await loadEncryptionKey(env.ENCRYPTION_KEY),
38
+ keys: { cp: await loadEncryptionKey(env.CONTROL_PLANE_ENCRYPTION_KEY) },
39
+ };
40
+ dataAdapter = createEncryptedDataAdapterWithKeyRing(dataAdapter, ring, {
41
+ resolveEncryptKeyId: (tenantId) =>
42
+ tenantId === CONTROL_PLANE_TENANT_ID ? "cp" : undefined,
43
+ });
44
+ } else if (env.ENCRYPTION_KEY) {
45
+ // Single-key fallback (no inherited control-plane secrets).
46
+ dataAdapter = createEncryptedDataAdapter(
47
+ dataAdapter,
48
+ await loadEncryptionKey(env.ENCRYPTION_KEY),
49
+ );
50
+ }
51
+
52
+ // Resolve inherited defaults (connections by strategy, is_system resource
53
+ // servers, inheritable hooks, email provider) from the control-plane rows
54
+ // the rollout projected into THIS tenant's database — identical read path
55
+ // to a control-plane-colocated tenant.
56
+ dataAdapter = withRuntimeFallback(dataAdapter, {
57
+ controlPlaneTenantId: CONTROL_PLANE_TENANT_ID,
58
+ });
59
+
60
+ const config: AuthHeroConfig = {
61
+ dataAdapter,
62
+ allowedOrigins: [origin].filter(Boolean),
63
+ };
64
+
65
+ const app = createApp(config);
66
+
67
+ return app.fetch(request, { ...env, ISSUER: issuer });
68
+ },
69
+ };
@@ -0,0 +1,16 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+
3
+ export interface Env {
4
+ // This tenant's own D1 database. Each WFP tenant Worker has its own.
5
+ AUTH_DB: D1Database;
6
+
7
+ // Base64-encoded 32-byte key for this tenant's own secrets at rest.
8
+ // `wrangler secret put ENCRYPTION_KEY` (per tenant Worker).
9
+ ENCRYPTION_KEY?: string;
10
+
11
+ // Base64-encoded 32-byte CONTROL PLANE key. Decrypts the shared secrets
12
+ // (e.g. Google client_secret) that the control plane projected into this
13
+ // tenant's database under the "cp" key id. The Worker holds it as a binding;
14
+ // a raw export of AUTH_DB cannot be decrypted without it.
15
+ CONTROL_PLANE_ENCRYPTION_KEY?: string;
16
+ }
@@ -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,46 @@
1
+ # ════════════════════════════════════════════════════════════════════════════
2
+ # AuthHero WFP Tenant Worker
3
+ # ════════════════════════════════════════════════════════════════════════════
4
+ # The full authhero app for ONE tenant, deployed into the `authhero-tenants`
5
+ # dispatch namespace and fronted by the WFP dispatcher. It reads only its own
6
+ # D1 and inherits the control plane's defaults from rows the control plane
7
+ # rollout projected into that D1.
8
+ #
9
+ # Deploy into the namespace (one per tenant):
10
+ # wrangler deploy --dispatch-namespace=authhero-tenants --name=tenant-<id>-auth
11
+ #
12
+ # Sensitive IDs (database_id) go in wrangler.local.toml (gitignored).
13
+ # ════════════════════════════════════════════════════════════════════════════
14
+
15
+ name = "tenant-auth"
16
+ main = "src/index.ts"
17
+ compatibility_date = "2026-05-01"
18
+ compatibility_flags = ["nodejs_compat"]
19
+
20
+ [assets]
21
+ directory = "./dist/assets"
22
+
23
+ # ════════════════════════════════════════════════════════════════════════════
24
+ # This tenant's own D1 database
25
+ # ════════════════════════════════════════════════════════════════════════════
26
+ # Each tenant Worker has its own database. Create one per tenant and set its id
27
+ # in wrangler.local.toml:
28
+ # npx wrangler d1 create tenant-<id>-db
29
+ [[d1_databases]]
30
+ binding = "AUTH_DB"
31
+ database_name = "tenant-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 # this tenant'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 the control
42
+ # plane used to project this tenant's inherited secrets, or they won't decrypt.
43
+
44
+ # Optional: Enable observability
45
+ # [observability]
46
+ # enabled = true