create-authhero 0.4.0 → 0.6.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.
@@ -0,0 +1,9 @@
1
+ # Cloudflare API credentials for multi-tenant database management
2
+ CLOUDFLARE_ACCOUNT_ID=your_account_id_here
3
+ CLOUDFLARE_API_TOKEN=your_api_token_here
4
+
5
+ # Optional: Separate token for Analytics Engine (if different from main token)
6
+ # ANALYTICS_ENGINE_API_TOKEN=your_analytics_token_here
7
+
8
+ # Optional: Base domain for subdomain routing
9
+ # BASE_DOMAIN=auth.example.com
@@ -0,0 +1,177 @@
1
+ # AuthHero Multi-Tenant Server
2
+
3
+ A production-grade multi-tenant AuthHero authentication server using Cloudflare Workers with:
4
+
5
+ - Per-tenant D1 databases (dynamically created via REST API)
6
+ - Analytics Engine for centralized logging
7
+ - Subdomain-based tenant routing (optional)
8
+
9
+ ## Architecture
10
+
11
+ ```
12
+ ┌─────────────────────────────────────────┐
13
+ │ Cloudflare Worker │
14
+ │ │
15
+ │ ┌─────────────────────────────────┐ │
16
+ │ │ AuthHero Multi-Tenant │ │
17
+ │ └─────────────────────────────────┘ │
18
+ │ │ │
19
+ │ ▼ │
20
+ │ ┌─────────────────────────────────┐ │
21
+ │ │ Multi-Tenancy Plugin │ │
22
+ │ │ - Access Control │ │
23
+ │ │ - Subdomain Routing │ │
24
+ │ │ - Database Resolution │ │
25
+ │ └─────────────────────────────────┘ │
26
+ │ │ │
27
+ └──────────────┼──────────────────────────┘
28
+
29
+ ┌────────────────────────┼────────────────────────┐
30
+ │ │ │
31
+ ▼ ▼ ▼
32
+ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
33
+ │ Main D1 │ │ Tenant D1 │ │ Analytics │
34
+ │ Database │ │ Databases │ │ Engine │
35
+ │ (Bound) │ │ (via REST) │ │ (Logs) │
36
+ └─────────────┘ └─────────────┘ └─────────────┘
37
+ ```
38
+
39
+ ## Prerequisites
40
+
41
+ - [Cloudflare Account](https://dash.cloudflare.com/sign-up) with Workers Paid plan
42
+ - [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/install-and-update/)
43
+ - Cloudflare API Token with D1 and Workers permissions
44
+
45
+ ## Getting Started
46
+
47
+ 1. Install dependencies:
48
+
49
+ ```bash
50
+ npm install
51
+ ```
52
+
53
+ 2. Create the main D1 database:
54
+
55
+ ```bash
56
+ wrangler d1 create authhero-main-db
57
+ ```
58
+
59
+ 3. Update `wrangler.toml` with your database ID.
60
+
61
+ 4. Create `.dev.vars` from the example:
62
+
63
+ ```bash
64
+ cp .dev.vars.example .dev.vars
65
+ ```
66
+
67
+ 5. Update `.dev.vars` with your Cloudflare credentials.
68
+
69
+ 6. Run local database migrations:
70
+
71
+ ```bash
72
+ npm run db:migrate
73
+ ```
74
+
75
+ 7. Start the development server:
76
+ ```bash
77
+ npm run dev
78
+ ```
79
+
80
+ ## Cloudflare API Token
81
+
82
+ Create an API token with the following permissions:
83
+
84
+ - **Account** > D1 > Edit
85
+ - **Account** > Workers Analytics Engine > Edit
86
+ - **Zone** > Workers Routes > Edit (if using custom domains)
87
+
88
+ ## Multi-Tenant Features
89
+
90
+ ### Per-Tenant Database Isolation
91
+
92
+ Each tenant gets its own D1 database, created dynamically when the tenant is provisioned:
93
+
94
+ - Main tenant uses the bound D1 database (low latency)
95
+ - Other tenants use D1 databases via REST API
96
+
97
+ ### Access Control
98
+
99
+ - Users authenticate against the main tenant
100
+ - Organization membership grants access to specific tenants
101
+ - Tokens with `org` claim can access the matching tenant
102
+
103
+ ### Subdomain Routing (Optional)
104
+
105
+ Enable subdomain routing to route requests based on subdomain:
106
+
107
+ - `tenant1.auth.example.com` → tenant1
108
+ - `tenant2.auth.example.com` → tenant2
109
+
110
+ Configure in `wrangler.toml`:
111
+
112
+ ```toml
113
+ [vars]
114
+ BASE_DOMAIN = "auth.example.com"
115
+ ```
116
+
117
+ ### Analytics Engine Logs
118
+
119
+ All authentication events are logged to Analytics Engine for:
120
+
121
+ - Centralized logging across all tenants
122
+ - Real-time analytics
123
+ - Low-latency writes
124
+
125
+ ## Deployment
126
+
127
+ 1. Deploy to Cloudflare:
128
+
129
+ ```bash
130
+ npm run deploy
131
+ ```
132
+
133
+ 2. Run production migrations:
134
+
135
+ ```bash
136
+ npm run db:migrate:prod
137
+ ```
138
+
139
+ 3. Set production secrets:
140
+ ```bash
141
+ wrangler secret put CLOUDFLARE_API_TOKEN
142
+ ```
143
+
144
+ ## Project Structure
145
+
146
+ ```
147
+ ├── src/
148
+ │ ├── index.ts # Worker entry point
149
+ │ ├── app.ts # AuthHero app configuration
150
+ │ ├── types.ts # TypeScript type definitions
151
+ │ └── database-factory.ts # Multi-tenant database factory
152
+ ├── wrangler.toml # Cloudflare Worker configuration
153
+ ├── .dev.vars.example # Example environment variables
154
+ └── package.json
155
+ ```
156
+
157
+ ## API Documentation
158
+
159
+ Visit your worker URL with `/docs` to see the Swagger UI documentation.
160
+
161
+ ## Tenant Management
162
+
163
+ Tenants are managed via the `/management/tenants` endpoint:
164
+
165
+ ```bash
166
+ # Create a new tenant
167
+ curl -X POST https://your-worker.workers.dev/management/tenants \
168
+ -H "Authorization: Bearer <token>" \
169
+ -H "Content-Type: application/json" \
170
+ -d '{"id": "tenant1", "name": "Tenant 1"}'
171
+
172
+ # List all tenants
173
+ curl https://your-worker.workers.dev/management/tenants \
174
+ -H "Authorization: Bearer <token>"
175
+ ```
176
+
177
+ For more information, visit [https://authhero.net/docs](https://authhero.net/docs).
@@ -0,0 +1,47 @@
1
+ import { Context } from "hono";
2
+ import { HTTPException } from "hono/http-exception";
3
+ import { AuthHeroConfig, init } from "authhero";
4
+ import { AuthHeroPlugin } from "@authhero/multi-tenancy";
5
+ import { swaggerUI } from "@hono/swagger-ui";
6
+
7
+ export default function createApp(
8
+ config: AuthHeroConfig,
9
+ multiTenancyPlugin: AuthHeroPlugin,
10
+ ) {
11
+ const { app } = init(config);
12
+
13
+ // Apply multi-tenancy middleware
14
+ if (multiTenancyPlugin.middleware) {
15
+ app.use("*", multiTenancyPlugin.middleware);
16
+ }
17
+
18
+ // Mount multi-tenancy routes
19
+ if (multiTenancyPlugin.routes) {
20
+ for (const route of multiTenancyPlugin.routes) {
21
+ app.route(route.path, route.handler);
22
+ }
23
+ }
24
+
25
+ app
26
+ .onError((err, ctx) => {
27
+ if (err instanceof HTTPException) {
28
+ return err.getResponse();
29
+ }
30
+ console.error(err);
31
+ return ctx.text(err.message, 500);
32
+ })
33
+ .get("/", async (ctx: Context) => {
34
+ return ctx.json({
35
+ name: "AuthHero Multi-Tenant Server",
36
+ status: "running",
37
+ });
38
+ })
39
+ .get("/docs", swaggerUI({ url: "/api/v2/spec" }));
40
+
41
+ // Call onRegister if defined
42
+ if (multiTenancyPlugin.onRegister) {
43
+ multiTenancyPlugin.onRegister(app);
44
+ }
45
+
46
+ return app;
47
+ }
@@ -0,0 +1,220 @@
1
+ import { DataAdapters } from "@authhero/adapter-interfaces";
2
+ import createKyselyAdapters from "@authhero/kysely-adapter";
3
+ import { D1Dialect } from "kysely-d1";
4
+ import { Kysely, SqliteQueryCompiler } from "kysely";
5
+ import wretch from "wretch";
6
+
7
+ interface TenantDatabase {
8
+ database_id: string;
9
+ database_name: string;
10
+ }
11
+
12
+ // Cache for tenant database adapters
13
+ const adapterCache = new Map<string, DataAdapters>();
14
+
15
+ /**
16
+ * Create a database factory for multi-tenant D1 databases.
17
+ *
18
+ * This factory:
19
+ * - Uses the main D1 database for the main tenant
20
+ * - Creates/connects to per-tenant D1 databases via REST API for other tenants
21
+ */
22
+ export function createDatabaseFactory(
23
+ mainDb: D1Database,
24
+ accountId: string,
25
+ apiToken: string,
26
+ mainTenantId: string,
27
+ ) {
28
+ const mainDialect = new D1Dialect({ database: mainDb });
29
+ const mainKysely = new Kysely<any>({ dialect: mainDialect });
30
+ const mainAdapters = createKyselyAdapters(mainKysely);
31
+
32
+ return {
33
+ /**
34
+ * Get adapters for a specific tenant
35
+ */
36
+ async getAdapters(tenantId: string): Promise<DataAdapters> {
37
+ // Main tenant uses the bound D1 database
38
+ if (tenantId === mainTenantId) {
39
+ return mainAdapters;
40
+ }
41
+
42
+ // Check cache first
43
+ const cached = adapterCache.get(tenantId);
44
+ if (cached) {
45
+ return cached;
46
+ }
47
+
48
+ // For other tenants, connect via REST API
49
+ const adapters = await createRestD1Adapters(
50
+ tenantId,
51
+ mainKysely,
52
+ accountId,
53
+ apiToken,
54
+ );
55
+ adapterCache.set(tenantId, adapters);
56
+ return adapters;
57
+ },
58
+
59
+ /**
60
+ * Provision a new database for a tenant
61
+ */
62
+ async provision(tenantId: string): Promise<void> {
63
+ console.log(`Provisioning database for tenant: ${tenantId}`);
64
+
65
+ // Create a new D1 database via Cloudflare API
66
+ const response = await wretch(
67
+ `https://api.cloudflare.com/client/v4/accounts/${accountId}/d1/database`,
68
+ )
69
+ .auth(`Bearer ${apiToken}`)
70
+ .post({ name: `authhero-tenant-${tenantId}` })
71
+ .json<{ success: boolean; result: TenantDatabase; errors: any[] }>();
72
+
73
+ if (!response.success) {
74
+ throw new Error(
75
+ `Failed to create database: ${JSON.stringify(response.errors)}`,
76
+ );
77
+ }
78
+
79
+ // Store the database mapping in the main database
80
+ await mainKysely
81
+ .insertInto("tenant_databases")
82
+ .values({
83
+ tenant_id: tenantId,
84
+ database_id: response.result.database_id,
85
+ database_name: response.result.database_name,
86
+ created_at: new Date().toISOString(),
87
+ })
88
+ .execute();
89
+
90
+ console.log(
91
+ `Database provisioned for tenant ${tenantId}: ${response.result.database_id}`,
92
+ );
93
+ },
94
+
95
+ /**
96
+ * Deprovision (delete) a tenant's database
97
+ */
98
+ async deprovision(tenantId: string): Promise<void> {
99
+ console.log(`Deprovisioning database for tenant: ${tenantId}`);
100
+
101
+ // Get the database ID from the main database
102
+ const mapping = await mainKysely
103
+ .selectFrom("tenant_databases")
104
+ .where("tenant_id", "=", tenantId)
105
+ .selectAll()
106
+ .executeTakeFirst();
107
+
108
+ if (!mapping) {
109
+ console.warn(`No database mapping found for tenant: ${tenantId}`);
110
+ return;
111
+ }
112
+
113
+ // Delete the D1 database via Cloudflare API
114
+ await wretch(
115
+ `https://api.cloudflare.com/client/v4/accounts/${accountId}/d1/database/${mapping.database_id}`,
116
+ )
117
+ .auth(`Bearer ${apiToken}`)
118
+ .delete()
119
+ .json();
120
+
121
+ // Remove the mapping from the main database
122
+ await mainKysely
123
+ .deleteFrom("tenant_databases")
124
+ .where("tenant_id", "=", tenantId)
125
+ .execute();
126
+
127
+ // Clear from cache
128
+ adapterCache.delete(tenantId);
129
+
130
+ console.log(`Database deprovisioned for tenant: ${tenantId}`);
131
+ },
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Create adapters that connect to a D1 database via REST API
137
+ */
138
+ async function createRestD1Adapters(
139
+ tenantId: string,
140
+ mainKysely: Kysely<any>,
141
+ accountId: string,
142
+ apiToken: string,
143
+ ): Promise<DataAdapters> {
144
+ // Get the database ID from the tenant_databases table
145
+ const mapping = await mainKysely
146
+ .selectFrom("tenant_databases")
147
+ .where("tenant_id", "=", tenantId)
148
+ .select("database_id")
149
+ .executeTakeFirst();
150
+
151
+ if (!mapping) {
152
+ throw new Error(`No database found for tenant: ${tenantId}`);
153
+ }
154
+
155
+ // Create a REST-based D1 adapter
156
+ // Note: This uses the Cloudflare D1 HTTP API
157
+ const restDialect = createRestD1Dialect(
158
+ mapping.database_id,
159
+ accountId,
160
+ apiToken,
161
+ );
162
+ const db = new Kysely<any>({ dialect: restDialect });
163
+
164
+ return createKyselyAdapters(db);
165
+ }
166
+
167
+ /**
168
+ * Create a Kysely dialect that uses D1 REST API
169
+ */
170
+ function createRestD1Dialect(
171
+ databaseId: string,
172
+ accountId: string,
173
+ apiToken: string,
174
+ ) {
175
+ return {
176
+ createAdapter: () => ({
177
+ supportsCreateIfNotExists: () => true,
178
+ supportsReturning: () => true,
179
+ supportsTransactionalDdl: () => false,
180
+ }),
181
+ createDriver: () => ({
182
+ init: async () => {},
183
+ destroy: async () => {},
184
+ acquireConnection: async () => ({
185
+ executeQuery: async <R>(compiledQuery: {
186
+ sql: string;
187
+ parameters: readonly unknown[];
188
+ }) => {
189
+ const response = await wretch(
190
+ `https://api.cloudflare.com/client/v4/accounts/${accountId}/d1/database/${databaseId}/query`,
191
+ )
192
+ .auth(`Bearer ${apiToken}`)
193
+ .post({
194
+ sql: compiledQuery.sql,
195
+ params: compiledQuery.parameters,
196
+ })
197
+ .json<{ success: boolean; result: Array<{ results: R[] }> }>();
198
+
199
+ if (!response.success) {
200
+ throw new Error("D1 REST query failed");
201
+ }
202
+
203
+ return {
204
+ rows: response.result[0]?.results ?? [],
205
+ numAffectedRows: BigInt(0),
206
+ insertId: undefined,
207
+ };
208
+ },
209
+ releaseConnection: async () => {},
210
+ }),
211
+ releaseConnection: async () => {},
212
+ }),
213
+ createIntrospector: (_db: Kysely<any>) => ({
214
+ getSchemas: async () => [],
215
+ getTables: async () => [],
216
+ getMetadata: async () => ({ schemas: [], tables: [] }),
217
+ }),
218
+ createQueryCompiler: () => new SqliteQueryCompiler(),
219
+ };
220
+ }
@@ -0,0 +1,71 @@
1
+ import { OpenAPIHono } from "@hono/zod-openapi";
2
+ import { D1Dialect } from "kysely-d1";
3
+ import { Kysely } from "kysely";
4
+ import createKyselyAdapters from "@authhero/kysely-adapter";
5
+ import createCloudflareAdapters from "@authhero/cloudflare-adapter";
6
+ import { createMultiTenancyPlugin } from "@authhero/multi-tenancy";
7
+ import createApp from "./app";
8
+ import { Env } from "./types";
9
+ import { AuthHeroConfig, Bindings, Variables } from "authhero";
10
+ import { createDatabaseFactory } from "./database-factory";
11
+
12
+ let app: OpenAPIHono<{ Bindings: Bindings; Variables: Variables }> | undefined;
13
+
14
+ export default {
15
+ async fetch(request: Request, env: Env): Promise<Response> {
16
+ if (!app) {
17
+ // Create main database adapters
18
+ const mainDialect = new D1Dialect({ database: env.MAIN_DB });
19
+ const mainDb = new Kysely<any>({ dialect: mainDialect });
20
+ const mainDataAdapter = createKyselyAdapters(mainDb);
21
+
22
+ // Create database factory for multi-tenant database isolation
23
+ const databaseFactory = createDatabaseFactory(
24
+ env.MAIN_DB,
25
+ env.CLOUDFLARE_ACCOUNT_ID,
26
+ env.CLOUDFLARE_API_TOKEN,
27
+ env.MAIN_TENANT_ID,
28
+ );
29
+
30
+ // Create Cloudflare-specific adapters (cache, custom domains, geo)
31
+ const cloudflareAdapters = createCloudflareAdapters({
32
+ accountId: env.CLOUDFLARE_ACCOUNT_ID,
33
+ apiToken: env.CLOUDFLARE_API_TOKEN,
34
+ // Analytics Engine for logs
35
+ analyticsEngineLogs: {
36
+ analyticsEngineBinding: env.AUTH_LOGS,
37
+ accountId: env.CLOUDFLARE_ACCOUNT_ID,
38
+ apiToken: env.ANALYTICS_ENGINE_API_TOKEN || env.CLOUDFLARE_API_TOKEN,
39
+ dataset: "authhero_logs",
40
+ },
41
+ });
42
+
43
+ // Create multi-tenancy plugin
44
+ const multiTenancyPlugin = createMultiTenancyPlugin({
45
+ accessControl: {
46
+ mainTenantId: env.MAIN_TENANT_ID,
47
+ defaultPermissions: ["tenant:admin", "tenant:read", "tenant:write"],
48
+ },
49
+ subdomainRouting: env.BASE_DOMAIN
50
+ ? {
51
+ baseDomain: env.BASE_DOMAIN,
52
+ }
53
+ : undefined,
54
+ databaseIsolation: {
55
+ getAdapters: databaseFactory.getAdapters,
56
+ onProvision: databaseFactory.provision,
57
+ onDeprovision: databaseFactory.deprovision,
58
+ },
59
+ });
60
+
61
+ const config: AuthHeroConfig = {
62
+ dataAdapter: mainDataAdapter,
63
+ ...cloudflareAdapters,
64
+ };
65
+
66
+ app = createApp(config, multiTenancyPlugin);
67
+ }
68
+
69
+ return app.fetch(request, env);
70
+ },
71
+ };
@@ -0,0 +1,24 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+
3
+ import { AnalyticsEngineDataset } from "@authhero/cloudflare-adapter";
4
+
5
+ export interface Env {
6
+ // Main D1 database for tenant registry and main tenant data
7
+ MAIN_DB: D1Database;
8
+
9
+ // Analytics Engine for logs
10
+ AUTH_LOGS: AnalyticsEngineDataset;
11
+
12
+ // Cloudflare API credentials for dynamic D1 database management
13
+ CLOUDFLARE_ACCOUNT_ID: string;
14
+ CLOUDFLARE_API_TOKEN: string;
15
+
16
+ // Optional: Analytics Engine API token (may be different from main API token)
17
+ ANALYTICS_ENGINE_API_TOKEN?: string;
18
+
19
+ // Base domain for subdomain routing (e.g., "auth.example.com")
20
+ BASE_DOMAIN?: string;
21
+
22
+ // Main tenant ID
23
+ MAIN_TENANT_ID: string;
24
+ }
@@ -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,38 @@
1
+ name = "authhero-multitenant"
2
+ main = "src/index.ts"
3
+ compatibility_date = "2024-11-20"
4
+
5
+ # Main D1 Database (stores tenant registry and main tenant data)
6
+ # Run: wrangler d1 create authhero-main-db
7
+ # Then update the database_id below:
8
+ [[d1_databases]]
9
+ binding = "MAIN_DB"
10
+ database_name = "authhero-main-db"
11
+ database_id = "local"
12
+
13
+ # Analytics Engine for logs
14
+ # Create in Cloudflare Dashboard: Workers & Pages > Analytics Engine
15
+ [[analytics_engine_datasets]]
16
+ binding = "AUTH_LOGS"
17
+ dataset = "authhero_logs"
18
+
19
+ # Environment variables
20
+ # For local development, create a .dev.vars file with these values:
21
+ # CLOUDFLARE_ACCOUNT_ID=your_account_id
22
+ # CLOUDFLARE_API_TOKEN=your_api_token
23
+ # ANALYTICS_ENGINE_API_TOKEN=your_analytics_token (optional)
24
+ # MAIN_TENANT_ID=main
25
+ # BASE_DOMAIN=auth.example.com (optional)
26
+
27
+ [vars]
28
+ MAIN_TENANT_ID = "main"
29
+ # BASE_DOMAIN = "auth.example.com"
30
+
31
+ # Optional: Enable observability
32
+ [observability]
33
+ enabled = true
34
+
35
+ # Optional: Custom domain routing
36
+ # routes = [
37
+ # { pattern = "*.auth.example.com", custom_domain = true }
38
+ # ]
@@ -0,0 +1,75 @@
1
+ # AuthHero Cloudflare Simple Server
2
+
3
+ A single-tenant AuthHero authentication server using Cloudflare Workers and D1.
4
+
5
+ ## Prerequisites
6
+
7
+ - [Cloudflare Account](https://dash.cloudflare.com/sign-up)
8
+ - [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/install-and-update/)
9
+
10
+ ## Getting Started
11
+
12
+ 1. Install dependencies:
13
+
14
+ ```bash
15
+ npm install
16
+ ```
17
+
18
+ 2. Create a D1 database:
19
+
20
+ ```bash
21
+ wrangler d1 create authhero-db
22
+ ```
23
+
24
+ 3. Update `wrangler.toml` with your database ID from the output above.
25
+
26
+ 4. Run local database migrations:
27
+
28
+ ```bash
29
+ npm run db:migrate
30
+ ```
31
+
32
+ 5. Start the development server:
33
+ ```bash
34
+ npm run dev
35
+ ```
36
+
37
+ ## Deployment
38
+
39
+ 1. Deploy to Cloudflare:
40
+
41
+ ```bash
42
+ npm run deploy
43
+ ```
44
+
45
+ 2. Run production migrations:
46
+ ```bash
47
+ npm run db:migrate:prod
48
+ ```
49
+
50
+ ## Project Structure
51
+
52
+ ```
53
+ ├── src/
54
+ │ ├── index.ts # Worker entry point
55
+ │ ├── app.ts # AuthHero app configuration
56
+ │ └── types.ts # TypeScript type definitions
57
+ ├── wrangler.toml # Cloudflare Worker configuration
58
+ └── package.json
59
+ ```
60
+
61
+ ## API Documentation
62
+
63
+ Visit your worker URL with `/docs` to see the Swagger UI documentation.
64
+
65
+ ## Custom Domain
66
+
67
+ To add a custom domain, update `wrangler.toml`:
68
+
69
+ ```toml
70
+ routes = [
71
+ { pattern = "auth.yourdomain.com", custom_domain = true }
72
+ ]
73
+ ```
74
+
75
+ For more information, visit [https://authhero.net/docs](https://authhero.net/docs).