create-authhero 0.5.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.
- package/dist/cloudflare-multitenant/.dev.vars.example +9 -0
- package/dist/cloudflare-multitenant/README.md +177 -0
- package/dist/cloudflare-multitenant/src/app.ts +47 -0
- package/dist/cloudflare-multitenant/src/database-factory.ts +220 -0
- package/dist/cloudflare-multitenant/src/index.ts +71 -0
- package/dist/cloudflare-multitenant/src/types.ts +24 -0
- package/dist/cloudflare-multitenant/tsconfig.json +14 -0
- package/dist/cloudflare-multitenant/wrangler.toml +38 -0
- package/dist/cloudflare-simple/README.md +75 -0
- package/dist/cloudflare-simple/src/app.ts +26 -0
- package/dist/cloudflare-simple/src/index.ts +27 -0
- package/dist/cloudflare-simple/src/types.ts +5 -0
- package/dist/cloudflare-simple/tsconfig.json +14 -0
- package/dist/cloudflare-simple/wrangler.toml +21 -0
- package/dist/create-authhero.js +344 -0
- package/dist/local/README.md +50 -0
- package/dist/local/src/app.ts +26 -0
- package/dist/local/src/index.ts +117 -0
- package/dist/local/src/migrate.ts +25 -0
- package/dist/local/tsconfig.json +16 -0
- package/package.json +1 -1
|
@@ -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).
|