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.
- 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-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 +134 -27
- 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
|