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
@@ -1,6 +1,6 @@
1
- import { D1Dialect } from "kysely-d1";
2
- import { Kysely } from "kysely";
3
- import createAdapters from "@authhero/kysely-adapter";
1
+ import { drizzle } from "drizzle-orm/d1";
2
+ import createAdapters from "@authhero/drizzle";
3
+ import * as schema from "@authhero/drizzle/schema/sqlite";
4
4
  import createApp from "./app";
5
5
  import { Env } from "./types";
6
6
  import {
@@ -22,8 +22,7 @@ export default {
22
22
  // Get the origin from the request for dynamic CORS
23
23
  const origin = request.headers.get("Origin") || "";
24
24
 
25
- const dialect = new D1Dialect({ database: env.AUTH_DB });
26
- const db = new Kysely<any>({ dialect });
25
+ const db = drizzle(env.AUTH_DB, { schema });
27
26
  let dataAdapter = createAdapters(db, { useTransactions: false });
28
27
 
29
28
  // Encrypt sensitive credential fields at rest when ENCRYPTION_KEY is set.
@@ -13,7 +13,7 @@
13
13
 
14
14
  name = "authhero-server"
15
15
  main = "src/index.ts"
16
- compatibility_date = "2024-11-20"
16
+ compatibility_date = "2026-05-01"
17
17
  compatibility_flags = ["nodejs_compat"]
18
18
 
19
19
  # ════════════════════════════════════════════════════════════════════════════
@@ -0,0 +1,17 @@
1
+ # ============================================================================
2
+ # Development Environment Variables — Control Plane Worker
3
+ # ============================================================================
4
+ # Copy this file to .dev.vars and fill in your values.
5
+ # ============================================================================
6
+
7
+ # The control plane'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 shared CONTROL PLANE key (key id "cp"). The rollout encrypts projected
14
+ # secrets under this key; each tenant Worker holds the same key to decrypt them.
15
+ # Must be byte-identical across the control plane and every tenant Worker.
16
+ # wrangler secret put CONTROL_PLANE_ENCRYPTION_KEY
17
+ # CONTROL_PLANE_ENCRYPTION_KEY=
@@ -0,0 +1,59 @@
1
+ # AuthHero — Control Plane Worker
2
+
3
+ The **management surface and rollout source** for a Workers-for-Platforms setup.
4
+ It manages tenants, serves colocated tenants from the shared database, and
5
+ projects the control plane's defaults (shared social logins, prompts, branding,
6
+ system resource servers, inheritable hooks) into each WFP tenant's own database.
7
+
8
+ This is one of three pieces:
9
+
10
+ | Piece | Template |
11
+ | --- | --- |
12
+ | Front door (host → tenant → dispatch) | `cloudflare-wfp-dispatcher` |
13
+ | Per-tenant Workers | `cloudflare-wfp-tenant` |
14
+ | **This control plane** | `cloudflare-control-plane` |
15
+
16
+ ## The rollout
17
+
18
+ `src/index.ts` builds a `createDirectRolloutAdapter` (inline execution). The app
19
+ exposes:
20
+
21
+ ```
22
+ POST /internal/tenants/:id/sync-defaults
23
+ ```
24
+
25
+ Call it **after a tenant is provisioned** and **after rotating a shared secret**.
26
+ It reads the control plane tenant's inheritable rows and upserts them (by id,
27
+ idempotently) into the target tenant's database under the `control_plane`
28
+ tenant id, re-encrypting secrets under the `cp` key.
29
+
30
+ ::: warning Two things to wire before production
31
+ 1. **`buildTenantAdapters(env, tenantId)`** in `src/index.ts` is a stub. Return
32
+ the `DataAdapters` over the target tenant's own D1, wrapped with the same key
33
+ ring the tenant Worker uses (`{ default, keys: { cp } }`). How you reach a
34
+ tenant's D1 is platform-specific (per-tenant binding or the D1 HTTP API).
35
+ Until implemented, the endpoint returns `501`.
36
+ 2. **Protect the `/internal/...` route** (service binding, mTLS, or admin token).
37
+ It re-keys tenant databases.
38
+ :::
39
+
40
+ ## Secrets
41
+
42
+ - `ENCRYPTION_KEY` — the control plane's own secrets at rest.
43
+ - `CONTROL_PLANE_ENCRYPTION_KEY` — the shared `cp` key the rollout encrypts
44
+ projected secrets under. Every tenant Worker holds the **same** key to decrypt
45
+ them; keep it byte-identical everywhere.
46
+
47
+ ## Setup
48
+
49
+ ```bash
50
+ npm install
51
+ npm run setup # creates wrangler.local.toml + .dev.vars (ENCRYPTION_KEY generated)
52
+ # add CONTROL_PLANE_ENCRYPTION_KEY (openssl rand -base64 32) to .dev.vars
53
+ npm run migrate
54
+ npm run seed # seed the control plane tenant + admin
55
+ npm run dev
56
+ ```
57
+
58
+ See [Control Plane Defaults](https://authhero.net/docs/customization/multi-tenancy/control-plane-defaults)
59
+ for the full architecture and request flows.
@@ -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,113 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Helper script to seed the Cloudflare D1 database
5
+ * This script starts the seed worker, makes a request to it, and then stops it
6
+ */
7
+
8
+ import { spawn } from "child_process";
9
+ import { setTimeout } from "timers/promises";
10
+
11
+ const adminUsername = process.argv[2] || process.env.ADMIN_USERNAME || "admin";
12
+ const adminPassword = process.argv[3] || process.env.ADMIN_PASSWORD || "admin";
13
+ const mode = process.argv[4] || "local";
14
+
15
+ console.log(`Starting seed worker in ${mode} mode...`);
16
+
17
+ const wranglerArgs = ["dev", "src/seed.ts"];
18
+ if (mode === "remote") {
19
+ wranglerArgs.push("--remote");
20
+ }
21
+
22
+ const worker = spawn("wrangler", wranglerArgs, {
23
+ stdio: ["inherit", "pipe", "inherit"],
24
+ });
25
+
26
+ let workerUrl = "http://localhost:8787";
27
+ let workerReady = false;
28
+
29
+ // Listen for the worker output to get the URL
30
+ worker.stdout?.on("data", (data) => {
31
+ const output = data.toString();
32
+ process.stdout.write(output);
33
+
34
+ // Look for the URL in the output
35
+ const urlMatch = output.match(/http:\/\/[^\s]+/);
36
+ if (urlMatch) {
37
+ workerUrl = urlMatch[0].replace(/\/$/, ""); // Remove trailing slash
38
+ }
39
+
40
+ // Detect when the worker is ready
41
+ if (output.includes("Ready on") || output.includes("Listening on")) {
42
+ workerReady = true;
43
+ }
44
+ });
45
+
46
+ // Function to wait for worker to be ready with retries
47
+ async function waitForWorker(maxAttempts = 30, delayMs = 1000) {
48
+ for (let i = 0; i < maxAttempts; i++) {
49
+ try {
50
+ // Just check if the server responds (even with an error is fine)
51
+ const response = await fetch(workerUrl, {
52
+ signal: AbortSignal.timeout(2000),
53
+ });
54
+ // Any response means the server is up
55
+ return true;
56
+ } catch (e) {
57
+ // ECONNREFUSED means server not ready yet
58
+ if (e.cause?.code !== "ECONNREFUSED") {
59
+ // Other errors might mean the server is actually responding
60
+ return true;
61
+ }
62
+ }
63
+
64
+ if (workerReady) {
65
+ // Give it a bit more time after wrangler reports ready
66
+ await setTimeout(500);
67
+ return true;
68
+ }
69
+
70
+ await setTimeout(delayMs);
71
+ if (i > 0 && i % 5 === 0) {
72
+ console.log(
73
+ `Still waiting for worker... (attempt ${i + 1}/${maxAttempts})`,
74
+ );
75
+ }
76
+ }
77
+ return false;
78
+ }
79
+
80
+ console.log("Waiting for seed worker to start...");
81
+ const isReady = await waitForWorker();
82
+
83
+ if (!isReady) {
84
+ console.error("\n❌ Seed worker failed to start within timeout");
85
+ worker.kill();
86
+ process.exit(1);
87
+ }
88
+
89
+ console.log(`\nMaking seed request to ${workerUrl}...`);
90
+
91
+ try {
92
+ const url = `${workerUrl}/?username=${encodeURIComponent(adminUsername)}&password=${encodeURIComponent(adminPassword)}`;
93
+
94
+ const response = await fetch(url);
95
+ const result = await response.json();
96
+
97
+ if (response.ok) {
98
+ console.log("\n✅ Seed completed successfully!");
99
+ console.log(JSON.stringify(result, null, 2));
100
+ } else {
101
+ console.error("\n❌ Seed failed:");
102
+ console.error(JSON.stringify(result, null, 2));
103
+ process.exit(1);
104
+ }
105
+ } catch (error) {
106
+ console.error("\n❌ Failed to connect to seed worker:");
107
+ console.error(error);
108
+ process.exit(1);
109
+ } finally {
110
+ // Stop the worker
111
+ console.log("\nStopping seed worker...");
112
+ worker.kill();
113
+ }
@@ -0,0 +1,74 @@
1
+ import { Context } from "hono";
2
+ import { swaggerUI } from "@hono/swagger-ui";
3
+ import { AuthHeroConfig, DataAdapters } from "authhero";
4
+ import {
5
+ initMultiTenant,
6
+ type ControlPlaneRolloutAdapter,
7
+ } from "@authhero/multi-tenancy";
8
+
9
+ // Control plane configuration. Tenants inherit this tenant's defaults.
10
+ const CONTROL_PLANE_TENANT_ID = "control_plane";
11
+ const CONTROL_PLANE_CLIENT_ID = "default";
12
+
13
+ export default function createApp(
14
+ config: AuthHeroConfig & { dataAdapter: DataAdapters },
15
+ rollout: ControlPlaneRolloutAdapter,
16
+ ) {
17
+ // initMultiTenant syncs resource servers and roles, mounts /api/v2/tenants,
18
+ // and wraps adapters with runtime fallback for colocated tenants.
19
+ const { app } = initMultiTenant({
20
+ ...config,
21
+ controlPlane: {
22
+ tenantId: CONTROL_PLANE_TENANT_ID,
23
+ clientId: CONTROL_PLANE_CLIENT_ID,
24
+ },
25
+ });
26
+
27
+ app
28
+ .onError((err, ctx) => {
29
+ // Duck-typing avoids instanceof issues with bundled dependencies.
30
+ if (
31
+ err &&
32
+ typeof err === "object" &&
33
+ "getResponse" in err &&
34
+ typeof (err as { getResponse?: unknown }).getResponse === "function"
35
+ ) {
36
+ return (err as { getResponse: () => Response }).getResponse();
37
+ }
38
+ console.error(err);
39
+ return ctx.text(
40
+ err instanceof Error ? err.message : "Internal Server Error",
41
+ 500,
42
+ );
43
+ })
44
+ .get("/", async (ctx: Context) => {
45
+ return ctx.json({
46
+ name: "AuthHero Control Plane",
47
+ status: "running",
48
+ docs: "/docs",
49
+ controlPlaneTenant: CONTROL_PLANE_TENANT_ID,
50
+ });
51
+ })
52
+ .get("/docs", swaggerUI({ url: "/api/v2/spec" }))
53
+ // Project the control plane defaults into a WFP tenant's own database.
54
+ // Call after a tenant is provisioned, and after rotating shared secrets.
55
+ //
56
+ // ⚠️ Protect this route before production (service binding, mTLS, or an
57
+ // admin token). It re-keys tenant databases.
58
+ .post("/internal/tenants/:id/sync-defaults", async (ctx: Context) => {
59
+ try {
60
+ const result = await rollout.syncDefaults(ctx.req.param("id"));
61
+ return ctx.json({ ok: true, result });
62
+ } catch (err) {
63
+ return ctx.json(
64
+ {
65
+ ok: false,
66
+ error: err instanceof Error ? err.message : String(err),
67
+ },
68
+ 500,
69
+ );
70
+ }
71
+ });
72
+
73
+ return app;
74
+ }
@@ -0,0 +1,72 @@
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
+ loadEncryptionKey,
9
+ } from "authhero";
10
+ import { createDirectRolloutAdapter } from "@authhero/multi-tenancy";
11
+ import createApp from "./app";
12
+ import { Env } from "./types";
13
+
14
+ const CONTROL_PLANE_TENANT_ID = "control_plane";
15
+
16
+ export default {
17
+ async fetch(request: Request, env: Env): Promise<Response> {
18
+ const url = new URL(request.url);
19
+ const issuer = `${url.protocol}//${url.host}/`;
20
+ const origin = request.headers.get("Origin") || "";
21
+
22
+ const db = drizzle(env.AUTH_DB, { schema });
23
+ let dataAdapter: DataAdapters = createAdapters(db, {
24
+ useTransactions: false,
25
+ });
26
+
27
+ if (env.ENCRYPTION_KEY) {
28
+ dataAdapter = createEncryptedDataAdapter(
29
+ dataAdapter,
30
+ await loadEncryptionKey(env.ENCRYPTION_KEY),
31
+ );
32
+ }
33
+
34
+ // Rollout source: project the control plane's inheritable defaults into a
35
+ // WFP tenant's own database. Runs inline here; swap for a Cloudflare
36
+ // Workflows implementation of ControlPlaneRolloutAdapter when you outgrow it.
37
+ const rollout = createDirectRolloutAdapter({
38
+ controlPlaneTenantId: CONTROL_PLANE_TENANT_ID,
39
+ getControlPlaneAdapters: async () => dataAdapter,
40
+ getAdapters: (tenantId) => buildTenantAdapters(env, tenantId),
41
+ });
42
+
43
+ const config: AuthHeroConfig & { dataAdapter: DataAdapters } = {
44
+ dataAdapter,
45
+ allowedOrigins: [origin].filter(Boolean),
46
+ };
47
+
48
+ const app = createApp(config, rollout);
49
+
50
+ return app.fetch(request, { ...env, ISSUER: issuer });
51
+ },
52
+ };
53
+
54
+ /**
55
+ * Return the DataAdapters over the given tenant's OWN D1, wrapped with the same
56
+ * control-plane key ring the tenant Worker uses (see the cloudflare-wfp-tenant
57
+ * template), so projected secrets are re-encrypted under the "cp" key id.
58
+ *
59
+ * How you reach a tenant's D1 is platform-specific — a per-tenant binding, or
60
+ * the Cloudflare D1 HTTP API. Implement this before calling sync-defaults; until
61
+ * then the /internal sync endpoint returns 501 with this message.
62
+ */
63
+ function buildTenantAdapters(
64
+ _env: Env,
65
+ _tenantId: string,
66
+ ): Promise<DataAdapters> {
67
+ throw new Error(
68
+ "buildTenantAdapters is not configured: return the DataAdapters over the " +
69
+ "tenant's own D1, wrapped with the control-plane key ring (default key + " +
70
+ "{ cp: CONTROL_PLANE_ENCRYPTION_KEY }).",
71
+ );
72
+ }
@@ -0,0 +1,56 @@
1
+ import { drizzle } from "drizzle-orm/d1";
2
+ import createAdapters from "@authhero/drizzle";
3
+ import * as schema from "@authhero/drizzle/schema/sqlite";
4
+ import { seed, createEncryptedDataAdapter, loadEncryptionKey } from "authhero";
5
+
6
+ interface Env {
7
+ AUTH_DB: D1Database;
8
+ ENCRYPTION_KEY?: string;
9
+ }
10
+
11
+ export default {
12
+ async fetch(request: Request, env: Env): Promise<Response> {
13
+ const url = new URL(request.url);
14
+ const adminUsername = url.searchParams.get("username") || "admin";
15
+ const adminPassword = url.searchParams.get("password") || "admin";
16
+ const issuer = `${url.protocol}//${url.host}/`;
17
+
18
+ try {
19
+ const db = drizzle(env.AUTH_DB, { schema });
20
+ let adapters = createAdapters(db, { useTransactions: false });
21
+
22
+ if (env.ENCRYPTION_KEY) {
23
+ const encryptionKey = await loadEncryptionKey(env.ENCRYPTION_KEY);
24
+ adapters = createEncryptedDataAdapter(adapters, encryptionKey);
25
+ }
26
+
27
+ const result = await seed(adapters, {
28
+ adminUsername,
29
+ adminPassword,
30
+ issuer,
31
+ tenantId: "control_plane",
32
+ tenantName: "Control Plane",
33
+ isControlPlane: true,
34
+ clientId: "default",
35
+ });
36
+
37
+ return new Response(
38
+ JSON.stringify({
39
+ success: true,
40
+ message: "Control plane seeded successfully",
41
+ result,
42
+ }),
43
+ { status: 200, headers: { "Content-Type": "application/json" } },
44
+ );
45
+ } catch (error) {
46
+ console.error("Seed error:", error);
47
+ return new Response(
48
+ JSON.stringify({
49
+ error: "Failed to seed database",
50
+ message: error instanceof Error ? error.message : String(error),
51
+ }),
52
+ { status: 500, headers: { "Content-Type": "application/json" } },
53
+ );
54
+ }
55
+ },
56
+ };
@@ -0,0 +1,14 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+
3
+ export interface Env {
4
+ // The control plane's shared database (also home to colocated tenants).
5
+ AUTH_DB: D1Database;
6
+
7
+ // Base64-encoded 32-byte key for the control plane's own secrets at rest.
8
+ ENCRYPTION_KEY?: string;
9
+
10
+ // Base64-encoded 32-byte key (key id "cp") that shared secrets are encrypted
11
+ // under when projected into a tenant's database. The control plane holds it
12
+ // so the rollout can re-encrypt secrets the tenant Worker will decrypt.
13
+ CONTROL_PLANE_ENCRYPTION_KEY?: string;
14
+ }
@@ -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
+ }