create-authhero 0.44.0 → 0.46.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/aws-sst/README.md +22 -0
- package/dist/aws-sst/scripts/decrypt-field.mjs +33 -0
- package/dist/aws-sst/scripts/generate-encryption-key.mjs +7 -0
- package/dist/aws-sst/src/index.ts +22 -1
- package/dist/aws-sst/sst.config.ts +4 -0
- package/dist/cloudflare/.dev.vars.example +10 -0
- package/dist/cloudflare/README.md +25 -0
- package/dist/cloudflare/scripts/decrypt-field.mjs +33 -0
- package/dist/cloudflare/scripts/generate-encryption-key.mjs +7 -0
- package/dist/cloudflare/src/index.ts +18 -7
- package/dist/cloudflare/src/types.ts +5 -0
- package/dist/cloudflare/wrangler.toml +1 -1
- package/dist/cloudflare-wfp-dispatcher/README.md +94 -0
- package/dist/cloudflare-wfp-dispatcher/src/index.ts +72 -0
- package/dist/cloudflare-wfp-dispatcher/src/types.ts +17 -0
- package/dist/cloudflare-wfp-dispatcher/tsconfig.json +14 -0
- package/dist/cloudflare-wfp-dispatcher/wrangler.toml +56 -0
- package/dist/create-authhero.js +178 -102
- package/dist/local/README.md +22 -0
- package/dist/local/scripts/decrypt-field.mjs +33 -0
- package/dist/local/scripts/generate-encryption-key.mjs +7 -0
- package/dist/local/src/index.ts +9 -1
- package/dist/proxy/src/index.ts +3 -7
- package/package.json +1 -1
package/dist/aws-sst/README.md
CHANGED
|
@@ -133,6 +133,28 @@ Enable auto-scaling or increase provisioned capacity in `sst.config.ts`.
|
|
|
133
133
|
|
|
134
134
|
Ensure the S3 bucket and CloudFront are properly configured. Check browser console for CORS errors.
|
|
135
135
|
|
|
136
|
+
## Encryption at rest
|
|
137
|
+
|
|
138
|
+
Sensitive credential fields (client secrets, connection secrets, email
|
|
139
|
+
credentials, TOTP secrets, migration-source secrets) are encrypted at rest.
|
|
140
|
+
A random `ENCRYPTION_KEY` was generated into `.env` when this project was
|
|
141
|
+
created. `sst.config.ts` forwards it to the Lambda, and `src/index.ts` enables
|
|
142
|
+
encryption whenever the key is present.
|
|
143
|
+
|
|
144
|
+
> **The key is load-bearing.** If you delete, rotate, or lose `ENCRYPTION_KEY`,
|
|
145
|
+
> any values already encrypted with it become unreadable. Treat the key as a
|
|
146
|
+
> long-lived secret, back it up, and use a separate key per deployment stage.
|
|
147
|
+
|
|
148
|
+
For production, set `ENCRYPTION_KEY` in your deploy environment / secret store
|
|
149
|
+
rather than reusing the generated dev key.
|
|
150
|
+
|
|
151
|
+
Helper scripts:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
npm run gen:key # print a fresh base64 key
|
|
155
|
+
npm run decrypt -- "enc:v1:..." # decrypt a stored value using ENCRYPTION_KEY from .env
|
|
156
|
+
```
|
|
157
|
+
|
|
136
158
|
## Learn More
|
|
137
159
|
|
|
138
160
|
- [SST Documentation](https://sst.dev/docs)
|
|
@@ -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"));
|
|
@@ -2,6 +2,11 @@ import { handle } from "hono/aws-lambda";
|
|
|
2
2
|
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
3
3
|
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
|
|
4
4
|
import createAdapters from "@authhero/aws";
|
|
5
|
+
import {
|
|
6
|
+
DataAdapters,
|
|
7
|
+
createEncryptedDataAdapter,
|
|
8
|
+
loadEncryptionKey,
|
|
9
|
+
} from "authhero";
|
|
5
10
|
import createApp from "./app";
|
|
6
11
|
import type { APIGatewayProxyEventV2, Context } from "aws-lambda";
|
|
7
12
|
|
|
@@ -23,6 +28,22 @@ const dataAdapter = createAdapters(docClient, {
|
|
|
23
28
|
tableName: process.env.TABLE_NAME,
|
|
24
29
|
});
|
|
25
30
|
|
|
31
|
+
// Encrypt sensitive credential fields at rest when ENCRYPTION_KEY is set.
|
|
32
|
+
// The wrapped adapter is built once and cached across warm invocations.
|
|
33
|
+
let encryptedAdapterPromise: Promise<DataAdapters> | null = null;
|
|
34
|
+
async function getDataAdapter(): Promise<DataAdapters> {
|
|
35
|
+
if (!process.env.ENCRYPTION_KEY) {
|
|
36
|
+
return dataAdapter;
|
|
37
|
+
}
|
|
38
|
+
if (!encryptedAdapterPromise) {
|
|
39
|
+
const rawKey = process.env.ENCRYPTION_KEY;
|
|
40
|
+
encryptedAdapterPromise = loadEncryptionKey(rawKey).then((key) =>
|
|
41
|
+
createEncryptedDataAdapter(dataAdapter, key),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return encryptedAdapterPromise;
|
|
45
|
+
}
|
|
46
|
+
|
|
26
47
|
export async function handler(event: APIGatewayProxyEventV2, context: Context) {
|
|
27
48
|
// Compute issuer from the request
|
|
28
49
|
const host = event.headers.host || event.requestContext.domainName;
|
|
@@ -51,7 +72,7 @@ export async function handler(event: APIGatewayProxyEventV2, context: Context) {
|
|
|
51
72
|
// Create app instance per request to avoid issuer contamination
|
|
52
73
|
// Lambda containers are reused, so we can't mutate process.env.ISSUER globally
|
|
53
74
|
const appWithIssuer = createApp({
|
|
54
|
-
dataAdapter,
|
|
75
|
+
dataAdapter: await getDataAdapter(),
|
|
55
76
|
allowedOrigins,
|
|
56
77
|
widgetUrl,
|
|
57
78
|
});
|
|
@@ -55,6 +55,10 @@ export default $config({
|
|
|
55
55
|
environment: {
|
|
56
56
|
TABLE_NAME: table.name,
|
|
57
57
|
WIDGET_URL: assets.url,
|
|
58
|
+
// At-rest encryption key for sensitive credentials. Sourced from the
|
|
59
|
+
// environment (e.g. a generated .env loaded by SST, or your CI/secret
|
|
60
|
+
// store). Encryption is skipped when this is empty.
|
|
61
|
+
ENCRYPTION_KEY: process.env.ENCRYPTION_KEY ?? "",
|
|
58
62
|
},
|
|
59
63
|
nodejs: {
|
|
60
64
|
install: ["@authhero/aws"],
|
|
@@ -5,6 +5,16 @@
|
|
|
5
5
|
# The .dev.vars file is used by wrangler for local development.
|
|
6
6
|
# ============================================================================
|
|
7
7
|
|
|
8
|
+
# ============================================================================
|
|
9
|
+
# Encryption at rest
|
|
10
|
+
# ============================================================================
|
|
11
|
+
# Base64-encoded 32-byte key used to encrypt sensitive credential fields
|
|
12
|
+
# (client secrets, connection secrets, email credentials, TOTP secrets, etc.).
|
|
13
|
+
# `create-authhero` writes a generated key into .dev.vars for local dev.
|
|
14
|
+
# In production, set it as a secret instead: `wrangler secret put ENCRYPTION_KEY`.
|
|
15
|
+
# Generate one with: openssl rand -base64 32
|
|
16
|
+
# ENCRYPTION_KEY=
|
|
17
|
+
|
|
8
18
|
# ============================================================================
|
|
9
19
|
# OPTIONAL: Analytics Engine Configuration
|
|
10
20
|
# ============================================================================
|
|
@@ -404,3 +404,28 @@ Cloudflare Rate Limiting helps protect your authentication endpoints from abuse.
|
|
|
404
404
|
| `limit` | Max requests allowed | `100` |
|
|
405
405
|
| `period` | Time window in seconds | `60` |
|
|
406
406
|
| `namespace_id` | Unique ID (string) for the limiter | `"1001"` |
|
|
407
|
+
|
|
408
|
+
## Encryption at rest
|
|
409
|
+
|
|
410
|
+
Sensitive credential fields (client secrets, connection secrets, email
|
|
411
|
+
credentials, TOTP secrets, migration-source secrets) are encrypted at rest.
|
|
412
|
+
A random `ENCRYPTION_KEY` was generated into `.dev.vars` when this project was
|
|
413
|
+
created, and `src/index.ts` enables encryption whenever the key is present.
|
|
414
|
+
|
|
415
|
+
> **The key is load-bearing.** If you delete, rotate, or lose `ENCRYPTION_KEY`,
|
|
416
|
+
> any values already encrypted with it become unreadable. In local dev you can
|
|
417
|
+
> recover by recreating the D1 database and re-seeding. In production, treat the
|
|
418
|
+
> key as a long-lived secret and back it up.
|
|
419
|
+
|
|
420
|
+
For production, set the key as a Worker secret (do **not** reuse the dev key):
|
|
421
|
+
|
|
422
|
+
```bash
|
|
423
|
+
wrangler secret put ENCRYPTION_KEY
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Helper scripts:
|
|
427
|
+
|
|
428
|
+
```bash
|
|
429
|
+
npm run gen:key # print a fresh base64 key
|
|
430
|
+
npm run decrypt -- "enc:v1:..." # decrypt a stored value using ENCRYPTION_KEY from .dev.vars
|
|
431
|
+
```
|
|
@@ -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"));
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import
|
|
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
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
AuthHeroConfig,
|
|
8
|
+
createEncryptedDataAdapter,
|
|
9
|
+
loadEncryptionKey,
|
|
10
|
+
} from "authhero";
|
|
7
11
|
|
|
8
12
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
9
13
|
// OPTIONAL: Uncomment to enable Cloudflare adapters (Analytics Engine, etc.)
|
|
@@ -18,9 +22,16 @@ export default {
|
|
|
18
22
|
// Get the origin from the request for dynamic CORS
|
|
19
23
|
const origin = request.headers.get("Origin") || "";
|
|
20
24
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
const db = drizzle(env.AUTH_DB, { schema });
|
|
26
|
+
let dataAdapter = createAdapters(db, { useTransactions: false });
|
|
27
|
+
|
|
28
|
+
// Encrypt sensitive credential fields at rest when ENCRYPTION_KEY is set.
|
|
29
|
+
// In local dev it comes from .dev.vars; in production set it with
|
|
30
|
+
// `wrangler secret put ENCRYPTION_KEY`. Without it, behavior is unchanged.
|
|
31
|
+
if (env.ENCRYPTION_KEY) {
|
|
32
|
+
const encryptionKey = await loadEncryptionKey(env.ENCRYPTION_KEY);
|
|
33
|
+
dataAdapter = createEncryptedDataAdapter(dataAdapter, encryptionKey);
|
|
34
|
+
}
|
|
24
35
|
|
|
25
36
|
// ────────────────────────────────────────────────────────────────────────
|
|
26
37
|
// OPTIONAL: Cloudflare Analytics Engine for centralized logging
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
export interface Env {
|
|
7
7
|
AUTH_DB: D1Database;
|
|
8
8
|
|
|
9
|
+
// Base64-encoded 32-byte key for at-rest encryption of sensitive credential
|
|
10
|
+
// fields. Set in .dev.vars locally and via `wrangler secret put ENCRYPTION_KEY`
|
|
11
|
+
// in production. Optional — encryption is skipped when unset.
|
|
12
|
+
ENCRYPTION_KEY?: string;
|
|
13
|
+
|
|
9
14
|
// ──────────────────────────────────────────────────────────────────────────
|
|
10
15
|
// OPTIONAL: Analytics Engine for centralized logging
|
|
11
16
|
// Uncomment to enable:
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# AuthHero WFP Dispatcher
|
|
2
|
+
|
|
3
|
+
Thin Cloudflare Worker that fronts a Workers-for-Platforms deployment of AuthHero. Resolves an incoming request's `Host` header to a tenant via the shared platform D1 (`custom_domains` table) and dispatches the request to that tenant's full authhero auth server, deployed as a script in a Cloudflare dispatch namespace.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Internet
|
|
9
|
+
|
|
|
10
|
+
v
|
|
11
|
+
[This worker — the dispatcher]
|
|
12
|
+
1. Host header -> custom_domains -> tenant_id
|
|
13
|
+
2. env.DISPATCHER.get('tenant-<id>-auth').fetch(request)
|
|
14
|
+
|
|
|
15
|
+
v
|
|
16
|
+
[authhero-tenants dispatch namespace]
|
|
17
|
+
|- tenant-acme-auth (full authhero, deployed from the `cloudflare` template)
|
|
18
|
+
|- tenant-bob-auth
|
|
19
|
+
|- ...
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Tenant workers come from the **`cloudflare` create-authhero template** — they're the same single-tenant auth server, deployed into the namespace instead of standalone.
|
|
23
|
+
|
|
24
|
+
## Prerequisites
|
|
25
|
+
|
|
26
|
+
- A Cloudflare account on the **Workers for Platforms** plan (required for dispatch namespaces).
|
|
27
|
+
- [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/install-and-update/).
|
|
28
|
+
- A D1 database (shared with the tenant workers).
|
|
29
|
+
|
|
30
|
+
## One-time platform setup
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# 1. Create the dispatch namespace
|
|
34
|
+
npx wrangler dispatch-namespace create authhero-tenants
|
|
35
|
+
|
|
36
|
+
# 2. Create the shared D1 (or reuse the one your tenant workers use)
|
|
37
|
+
npx wrangler d1 create authhero-db
|
|
38
|
+
|
|
39
|
+
# 3. Copy wrangler.toml -> wrangler.local.toml and paste the database_id
|
|
40
|
+
|
|
41
|
+
# 4. Install project dependencies (provides the local wrangler used by the
|
|
42
|
+
# db:migrate:* scripts)
|
|
43
|
+
npm install
|
|
44
|
+
|
|
45
|
+
# 5. Apply migrations to the shared D1
|
|
46
|
+
npm run db:migrate:remote
|
|
47
|
+
# Or, without installing dependencies first:
|
|
48
|
+
# npx wrangler d1 migrations apply AUTH_DB --remote --config wrangler.local.toml
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Deploy the dispatcher
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm run deploy
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Onboard a tenant
|
|
58
|
+
|
|
59
|
+
For each publisher:
|
|
60
|
+
|
|
61
|
+
1. **Provision the tenant in D1** — insert a `tenants` row, then a `custom_domains` row mapping their domain to the tenant_id. (Either via the authhero management API or by direct D1 query.)
|
|
62
|
+
|
|
63
|
+
2. **Deploy their auth worker into the namespace.** From the sibling `cloudflare` template:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# In the tenant's directory (scaffolded from `create-authhero --template=cloudflare`)
|
|
67
|
+
npx wrangler deploy \
|
|
68
|
+
--dispatch-namespace=authhero-tenants \
|
|
69
|
+
--name=tenant-<tenant_id>-auth
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
3. **Point their custom domain at this dispatcher worker** (Cloudflare → Workers → Triggers → Add Custom Domain on this worker, or via DNS to the worker's `*.workers.dev` route).
|
|
73
|
+
|
|
74
|
+
Once those three steps are done, a request to `auth.<their-domain>/authorize?...` flows to this dispatcher → resolved via custom_domains → dispatched to their tenant worker.
|
|
75
|
+
|
|
76
|
+
## Per-tenant routing customization
|
|
77
|
+
|
|
78
|
+
By default, hosts with no `proxy_routes` rows get a single catch-all that dispatches to `tenant-<tenant_id>-auth`. If a tenant needs richer routing — different middleware chains, CORS, a special path that bypasses the namespace — insert `proxy_routes` rows for that `custom_domain_id`. The dispatcher uses the configured routes verbatim instead of the default.
|
|
79
|
+
|
|
80
|
+
The script-name template (`tenant-{tenant_id}-auth`) can be overridden globally via the `SCRIPT_NAME_TEMPLATE` env var. Supported placeholders: `{tenant_id}`, `{custom_domain_id}`, `{domain}`, `{host}`.
|
|
81
|
+
|
|
82
|
+
## Local development
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npm run dev
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`wrangler dev` runs against a **local SQLite-backed D1**. The dispatch namespace cannot be emulated locally — for end-to-end tests, deploy to a real Cloudflare account using `npm run dev:remote`.
|
|
89
|
+
|
|
90
|
+
## Files
|
|
91
|
+
|
|
92
|
+
- `src/index.ts` — dispatcher worker entrypoint
|
|
93
|
+
- `src/types.ts` — Env interface (D1 binding + DISPATCHER namespace binding)
|
|
94
|
+
- `wrangler.toml` — Cloudflare config (assets, D1, dispatch namespace)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { drizzle } from "drizzle-orm/d1";
|
|
2
|
+
import { createProxyDataAdapter } from "@authhero/drizzle";
|
|
3
|
+
import * as schema from "@authhero/drizzle/schema/sqlite";
|
|
4
|
+
import {
|
|
5
|
+
createProxyApp,
|
|
6
|
+
type ProxyDataAdapter,
|
|
7
|
+
type ResolvedHost,
|
|
8
|
+
} from "@authhero/proxy";
|
|
9
|
+
import type { Env } from "./types";
|
|
10
|
+
|
|
11
|
+
// `tenant-{tenant_id}-auth` is the deploy convention used by this template's
|
|
12
|
+
// per-tenant worker setup. Override with the SCRIPT_NAME_TEMPLATE env var or
|
|
13
|
+
// by configuring proxy_routes rows per tenant for richer routing.
|
|
14
|
+
const DEFAULT_SCRIPT_NAME_TEMPLATE = "tenant-{tenant_id}-auth";
|
|
15
|
+
|
|
16
|
+
// If a host resolves to a known tenant but has no proxy_routes configured,
|
|
17
|
+
// synthesize a single catch-all that dispatches to the tenant's auth worker
|
|
18
|
+
// in the namespace. Operators who need middleware (CORS, headers, rate
|
|
19
|
+
// limiting) per tenant can add real proxy_routes rows and this fallback
|
|
20
|
+
// stays out of the way.
|
|
21
|
+
function withDefaultDispatchRoute(
|
|
22
|
+
inner: ProxyDataAdapter,
|
|
23
|
+
binding: string,
|
|
24
|
+
scriptNameTemplate: string,
|
|
25
|
+
): ProxyDataAdapter {
|
|
26
|
+
return {
|
|
27
|
+
proxyRoutes: inner.proxyRoutes,
|
|
28
|
+
resolveHost: async (host): Promise<ResolvedHost | null> => {
|
|
29
|
+
const resolved = await inner.resolveHost(host);
|
|
30
|
+
if (!resolved || resolved.routes.length > 0) return resolved;
|
|
31
|
+
const now = new Date(0).toISOString();
|
|
32
|
+
return {
|
|
33
|
+
...resolved,
|
|
34
|
+
routes: [
|
|
35
|
+
{
|
|
36
|
+
id: `default-${resolved.custom_domain_id}`,
|
|
37
|
+
tenant_id: resolved.tenant_id,
|
|
38
|
+
custom_domain_id: resolved.custom_domain_id,
|
|
39
|
+
priority: 1000,
|
|
40
|
+
match: { path: "/*" },
|
|
41
|
+
handlers: [
|
|
42
|
+
{
|
|
43
|
+
type: "dispatch_namespace",
|
|
44
|
+
options: { binding, script_name: scriptNameTemplate },
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
created_at: now,
|
|
48
|
+
updated_at: now,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default {
|
|
57
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
58
|
+
const db = drizzle(env.AUTH_DB, { schema });
|
|
59
|
+
const data = withDefaultDispatchRoute(
|
|
60
|
+
createProxyDataAdapter(db),
|
|
61
|
+
"DISPATCHER",
|
|
62
|
+
env.SCRIPT_NAME_TEMPLATE || DEFAULT_SCRIPT_NAME_TEMPLATE,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const app = createProxyApp({
|
|
66
|
+
data,
|
|
67
|
+
bindings: { DISPATCHER: env.DISPATCHER },
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return app.fetch(request);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
+
|
|
3
|
+
export interface Env {
|
|
4
|
+
// The shared platform D1 database. Holds custom_domains (host -> tenant)
|
|
5
|
+
// and proxy_routes (optional per-host route overrides).
|
|
6
|
+
AUTH_DB: D1Database;
|
|
7
|
+
|
|
8
|
+
// The Cloudflare Workers for Platforms dispatch namespace where each
|
|
9
|
+
// tenant's auth worker is deployed. Wrangler binding configured in
|
|
10
|
+
// wrangler.toml as `[[dispatch_namespaces]] binding = "DISPATCHER"`.
|
|
11
|
+
DISPATCHER: DispatchNamespace;
|
|
12
|
+
|
|
13
|
+
// Optional template for resolving a tenant to a dispatch namespace
|
|
14
|
+
// script name. Defaults to `tenant-{tenant_id}-auth`. Supported
|
|
15
|
+
// placeholders: {tenant_id}, {custom_domain_id}, {domain}, {host}.
|
|
16
|
+
SCRIPT_NAME_TEMPLATE?: string;
|
|
17
|
+
}
|
|
@@ -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,56 @@
|
|
|
1
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
# AuthHero WFP Dispatcher Configuration
|
|
3
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
4
|
+
# Thin Cloudflare Worker that resolves a request's Host header to a tenant
|
|
5
|
+
# (via the shared platform D1) and dispatches to that tenant's full
|
|
6
|
+
# authhero auth server, deployed as a script in the `authhero-tenants`
|
|
7
|
+
# dispatch namespace.
|
|
8
|
+
#
|
|
9
|
+
# Tenant workers are deployed separately via the `cloudflare` template,
|
|
10
|
+
# using `wrangler deploy --dispatch-namespace=authhero-tenants
|
|
11
|
+
# --name=tenant-<id>-auth`.
|
|
12
|
+
#
|
|
13
|
+
# Sensitive IDs (database_id) go in wrangler.local.toml (gitignored) or
|
|
14
|
+
# GitHub Secrets, not this file.
|
|
15
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
16
|
+
|
|
17
|
+
name = "authhero-wfp-dispatcher"
|
|
18
|
+
main = "src/index.ts"
|
|
19
|
+
compatibility_date = "2026-05-01"
|
|
20
|
+
compatibility_flags = ["nodejs_compat"]
|
|
21
|
+
|
|
22
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
23
|
+
# Dispatch namespace binding
|
|
24
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
25
|
+
# Create the namespace once before the first deploy:
|
|
26
|
+
# npx wrangler dispatch-namespace create authhero-tenants
|
|
27
|
+
[[dispatch_namespaces]]
|
|
28
|
+
binding = "DISPATCHER"
|
|
29
|
+
namespace = "authhero-tenants"
|
|
30
|
+
|
|
31
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
32
|
+
# Shared platform D1 database
|
|
33
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
34
|
+
# Same D1 that the tenant workers read/write. Holds custom_domains and
|
|
35
|
+
# proxy_routes that drive the dispatcher's host -> tenant resolution.
|
|
36
|
+
[[d1_databases]]
|
|
37
|
+
binding = "AUTH_DB"
|
|
38
|
+
database_name = "authhero-db"
|
|
39
|
+
database_id = "local" # Use "local" for local dev, or your actual ID in wrangler.local.toml
|
|
40
|
+
migrations_dir = "node_modules/@authhero/drizzle/drizzle"
|
|
41
|
+
|
|
42
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
43
|
+
# OPTIONAL: Custom Domain
|
|
44
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
45
|
+
# Point your platform's wildcard or per-publisher custom domains at this
|
|
46
|
+
# worker. The publishers' subdomains must resolve here so the dispatcher
|
|
47
|
+
# can route into the namespace.
|
|
48
|
+
#
|
|
49
|
+
# [[routes]]
|
|
50
|
+
# pattern = "*.auth.yourplatform.com/*"
|
|
51
|
+
# zone_name = "yourplatform.com"
|
|
52
|
+
|
|
53
|
+
# Optional: Enable observability for `wrangler tail` insight into dispatch
|
|
54
|
+
# decisions and namespace invocation latency.
|
|
55
|
+
# [observability]
|
|
56
|
+
# enabled = true
|
package/dist/create-authhero.js
CHANGED
|
@@ -3,10 +3,14 @@ import { Command as e } from "commander";
|
|
|
3
3
|
import t from "inquirer";
|
|
4
4
|
import n from "fs";
|
|
5
5
|
import r from "path";
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
6
|
+
import i from "crypto";
|
|
7
|
+
import { fileURLToPath as a } from "url";
|
|
8
|
+
import { spawn as o } from "child_process";
|
|
8
9
|
//#region src/index.ts
|
|
9
|
-
|
|
10
|
+
function s() {
|
|
11
|
+
return i.randomBytes(32).toString("base64");
|
|
12
|
+
}
|
|
13
|
+
var c = new e(), l = {
|
|
10
14
|
local: {
|
|
11
15
|
name: "Local (SQLite)",
|
|
12
16
|
description: "Local development setup with SQLite database - great for getting started",
|
|
@@ -18,10 +22,12 @@ var o = new e(), s = {
|
|
|
18
22
|
version: "1.0.0",
|
|
19
23
|
type: "module",
|
|
20
24
|
scripts: {
|
|
21
|
-
dev: "npx tsx watch src/index.ts",
|
|
22
|
-
start: "npx tsx src/index.ts",
|
|
25
|
+
dev: "npx tsx watch --env-file=.env src/index.ts",
|
|
26
|
+
start: "npx tsx --env-file=.env src/index.ts",
|
|
23
27
|
migrate: "npx tsx src/migrate.ts",
|
|
24
|
-
seed: "npx tsx src/seed.ts"
|
|
28
|
+
seed: "npx tsx --env-file=.env src/seed.ts",
|
|
29
|
+
"gen:key": "node scripts/generate-encryption-key.mjs",
|
|
30
|
+
decrypt: "node --env-file=.env scripts/decrypt-field.mjs"
|
|
25
31
|
},
|
|
26
32
|
dependencies: {
|
|
27
33
|
"@authhero/kysely-adapter": a,
|
|
@@ -69,26 +75,25 @@ var o = new e(), s = {
|
|
|
69
75
|
"seed:local": "node seed-helper.js",
|
|
70
76
|
"seed:remote": "node seed-helper.js '' '' remote",
|
|
71
77
|
seed: "node seed-helper.js",
|
|
72
|
-
setup: "cp wrangler.toml wrangler.local.toml && cp .dev.vars.example .dev.vars && echo '✅ Created wrangler.local.toml and .dev.vars - update with your IDs'"
|
|
78
|
+
setup: "cp wrangler.toml wrangler.local.toml && cp .dev.vars.example .dev.vars && echo '✅ Created wrangler.local.toml and .dev.vars - update with your IDs'",
|
|
79
|
+
"gen:key": "node scripts/generate-encryption-key.mjs",
|
|
80
|
+
decrypt: "node --env-file=.dev.vars scripts/decrypt-field.mjs"
|
|
73
81
|
},
|
|
74
82
|
dependencies: {
|
|
75
83
|
"@authhero/drizzle": a,
|
|
76
|
-
"@authhero/kysely-adapter": a,
|
|
77
84
|
...i && { "@authhero/admin": a },
|
|
78
85
|
"@authhero/widget": a,
|
|
79
86
|
"@hono/swagger-ui": "^0.5.0",
|
|
80
87
|
"@hono/zod-openapi": "^0.19.0",
|
|
81
88
|
authhero: a,
|
|
89
|
+
"drizzle-orm": "^0.44.0",
|
|
82
90
|
hono: "^4.6.0",
|
|
83
|
-
kysely: "latest",
|
|
84
|
-
"kysely-d1": "latest",
|
|
85
91
|
...t && { "@authhero/multi-tenancy": a },
|
|
86
92
|
...n && { bcryptjs: "latest" }
|
|
87
93
|
},
|
|
88
94
|
devDependencies: {
|
|
89
95
|
"@cloudflare/workers-types": "^4.0.0",
|
|
90
96
|
"drizzle-kit": "^0.31.0",
|
|
91
|
-
"drizzle-orm": "^0.44.0",
|
|
92
97
|
typescript: "^5.5.0",
|
|
93
98
|
wrangler: "^3.0.0"
|
|
94
99
|
}
|
|
@@ -96,6 +101,40 @@ var o = new e(), s = {
|
|
|
96
101
|
},
|
|
97
102
|
seedFile: "seed.ts"
|
|
98
103
|
},
|
|
104
|
+
"cloudflare-wfp-dispatcher": {
|
|
105
|
+
name: "Cloudflare Workers for Platforms — Dispatcher",
|
|
106
|
+
description: "Thin dispatcher worker that routes per-publisher custom domains to tenant auth workers in a dispatch namespace (pair with the `cloudflare` template for tenant workers)",
|
|
107
|
+
templateDir: "cloudflare-wfp-dispatcher",
|
|
108
|
+
packageJson: (e, t, n, r) => {
|
|
109
|
+
let i = r ? "workspace:*" : "latest";
|
|
110
|
+
return {
|
|
111
|
+
name: e,
|
|
112
|
+
version: "1.0.0",
|
|
113
|
+
type: "module",
|
|
114
|
+
scripts: {
|
|
115
|
+
dev: "wrangler dev --port 3001 --local-protocol https",
|
|
116
|
+
"dev:remote": "wrangler dev --port 3001 --local-protocol https --remote --config wrangler.local.toml",
|
|
117
|
+
deploy: "wrangler deploy --config wrangler.local.toml",
|
|
118
|
+
"db:migrate:local": "wrangler d1 migrations apply AUTH_DB --local",
|
|
119
|
+
"db:migrate:remote": "wrangler d1 migrations apply AUTH_DB --remote --config wrangler.local.toml",
|
|
120
|
+
migrate: "wrangler d1 migrations apply AUTH_DB --local",
|
|
121
|
+
setup: "cp wrangler.toml wrangler.local.toml && echo '✅ Created wrangler.local.toml - update with your IDs'"
|
|
122
|
+
},
|
|
123
|
+
dependencies: {
|
|
124
|
+
"@authhero/drizzle": i,
|
|
125
|
+
"@authhero/proxy": i,
|
|
126
|
+
"drizzle-orm": "^0.44.0",
|
|
127
|
+
hono: "^4.6.0"
|
|
128
|
+
},
|
|
129
|
+
devDependencies: {
|
|
130
|
+
"@cloudflare/workers-types": "^4.0.0",
|
|
131
|
+
"drizzle-kit": "^0.31.0",
|
|
132
|
+
typescript: "^5.5.0",
|
|
133
|
+
wrangler: "^3.0.0"
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
},
|
|
99
138
|
proxy: {
|
|
100
139
|
name: "Proxy (Cloudflare Workers)",
|
|
101
140
|
description: "Host-based reverse proxy on Cloudflare Workers — static config, no DB",
|
|
@@ -135,8 +174,10 @@ var o = new e(), s = {
|
|
|
135
174
|
dev: "sst dev",
|
|
136
175
|
deploy: "sst deploy --stage production",
|
|
137
176
|
remove: "sst remove",
|
|
138
|
-
seed: "npx tsx src/seed.ts",
|
|
139
|
-
"copy-assets": "node copy-assets.js"
|
|
177
|
+
seed: "npx tsx --env-file=.env src/seed.ts",
|
|
178
|
+
"copy-assets": "node copy-assets.js",
|
|
179
|
+
"gen:key": "node scripts/generate-encryption-key.mjs",
|
|
180
|
+
decrypt: "node --env-file=.env scripts/decrypt-field.mjs"
|
|
140
181
|
},
|
|
141
182
|
dependencies: {
|
|
142
183
|
"@authhero/aws": a,
|
|
@@ -163,13 +204,13 @@ var o = new e(), s = {
|
|
|
163
204
|
seedFile: "seed.ts"
|
|
164
205
|
}
|
|
165
206
|
};
|
|
166
|
-
function
|
|
207
|
+
function u(e, t) {
|
|
167
208
|
n.readdirSync(e).forEach((i) => {
|
|
168
209
|
let a = r.join(e, i), o = r.join(t, i);
|
|
169
|
-
n.lstatSync(a).isDirectory() ? (n.mkdirSync(o, { recursive: !0 }),
|
|
210
|
+
n.lstatSync(a).isDirectory() ? (n.mkdirSync(o, { recursive: !0 }), u(a, o)) : n.copyFileSync(a, o);
|
|
170
211
|
});
|
|
171
212
|
}
|
|
172
|
-
function
|
|
213
|
+
function d(e, t = !1, n = "authhero-local", r) {
|
|
173
214
|
let i = e ? "control_plane" : "main", a = e ? "Control Plane" : "Main", o = [
|
|
174
215
|
"https://manage.authhero.net/auth-callback",
|
|
175
216
|
"https://local.authhero.net/auth-callback",
|
|
@@ -314,7 +355,7 @@ function l(e, t = !1, n = "authhero-local", r) {
|
|
|
314
355
|
return `import { SqliteDialect, Kysely } from "kysely";
|
|
315
356
|
import Database from "better-sqlite3";
|
|
316
357
|
import createAdapters from "@authhero/kysely-adapter";
|
|
317
|
-
import { seed${t ? ", USERNAME_PASSWORD_PROVIDER" : ""} } from "authhero";
|
|
358
|
+
import { seed, createEncryptedDataAdapter, loadEncryptionKey${t ? ", USERNAME_PASSWORD_PROVIDER" : ""} } from "authhero";
|
|
318
359
|
|
|
319
360
|
interface ExtraClient {
|
|
320
361
|
client_id: string;
|
|
@@ -368,7 +409,13 @@ async function main() {
|
|
|
368
409
|
});
|
|
369
410
|
|
|
370
411
|
const db = new Kysely<any>({ dialect });
|
|
371
|
-
|
|
412
|
+
let adapters = createAdapters(db);
|
|
413
|
+
|
|
414
|
+
// Match the server: encrypt seeded secrets at rest when a key is configured.
|
|
415
|
+
if (process.env.ENCRYPTION_KEY) {
|
|
416
|
+
const encryptionKey = await loadEncryptionKey(process.env.ENCRYPTION_KEY);
|
|
417
|
+
adapters = createEncryptedDataAdapter(adapters, encryptionKey);
|
|
418
|
+
}
|
|
372
419
|
|
|
373
420
|
const seedResult = await seed(adapters, {
|
|
374
421
|
adminUsername,
|
|
@@ -417,7 +464,7 @@ main().catch((err) => {
|
|
|
417
464
|
});
|
|
418
465
|
`;
|
|
419
466
|
}
|
|
420
|
-
function
|
|
467
|
+
function f(e, t) {
|
|
421
468
|
let n = t ? "import fs from \"fs\";\n" : "", r = t ? "\nconst adminDistPath = path.resolve(\n __dirname,\n \"../node_modules/@authhero/admin/dist\",\n);\nconst adminIndexPath = path.join(adminDistPath, \"index.html\");\n" : "", i = t ? `
|
|
422
469
|
// Add admin UI handler if the package is installed
|
|
423
470
|
if (fs.existsSync(adminIndexPath)) {
|
|
@@ -546,14 +593,15 @@ ${i}
|
|
|
546
593
|
}
|
|
547
594
|
`;
|
|
548
595
|
}
|
|
549
|
-
function
|
|
550
|
-
return `import {
|
|
551
|
-
import
|
|
552
|
-
import
|
|
553
|
-
import { seed } from "authhero";
|
|
596
|
+
function p(e) {
|
|
597
|
+
return `import { drizzle } from "drizzle-orm/d1";
|
|
598
|
+
import createAdapters from "@authhero/drizzle";
|
|
599
|
+
import * as schema from "@authhero/drizzle/schema/sqlite";
|
|
600
|
+
import { seed, createEncryptedDataAdapter, loadEncryptionKey } from "authhero";
|
|
554
601
|
|
|
555
602
|
interface Env {
|
|
556
603
|
AUTH_DB: D1Database;
|
|
604
|
+
ENCRYPTION_KEY?: string;
|
|
557
605
|
}
|
|
558
606
|
|
|
559
607
|
export default {
|
|
@@ -565,9 +613,13 @@ export default {
|
|
|
565
613
|
const issuer = \`\${url.protocol}//\${url.host}/\`;
|
|
566
614
|
|
|
567
615
|
try {
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
|
|
616
|
+
const db = drizzle(env.AUTH_DB, { schema });
|
|
617
|
+
let adapters = createAdapters(db, { useTransactions: false });
|
|
618
|
+
|
|
619
|
+
if (env.ENCRYPTION_KEY) {
|
|
620
|
+
const encryptionKey = await loadEncryptionKey(env.ENCRYPTION_KEY);
|
|
621
|
+
adapters = createEncryptedDataAdapter(adapters, encryptionKey);
|
|
622
|
+
}
|
|
571
623
|
|
|
572
624
|
const result = await seed(adapters, {
|
|
573
625
|
adminUsername,
|
|
@@ -607,7 +659,7 @@ export default {
|
|
|
607
659
|
};
|
|
608
660
|
`;
|
|
609
661
|
}
|
|
610
|
-
function
|
|
662
|
+
function m(e, t) {
|
|
611
663
|
let n = t ? "import adminIndexHtml from \"./admin-index-html\";\n" : "", r = t ? " adminIndexHtml,\n" : "";
|
|
612
664
|
return e ? `import { Context } from "hono";
|
|
613
665
|
import { swaggerUI } from "@hono/swagger-ui";
|
|
@@ -690,14 +742,14 @@ ${r} });
|
|
|
690
742
|
}
|
|
691
743
|
`;
|
|
692
744
|
}
|
|
693
|
-
function
|
|
745
|
+
function h(e) {
|
|
694
746
|
return e ? "import { Context } from \"hono\";\nimport { swaggerUI } from \"@hono/swagger-ui\";\nimport { AuthHeroConfig, DataAdapters } from \"authhero\";\nimport { initMultiTenant } from \"@authhero/multi-tenancy\";\n\n// Control plane configuration\nconst CONTROL_PLANE_TENANT_ID = \"control_plane\";\nconst CONTROL_PLANE_CLIENT_ID = \"default\";\n\ninterface AppConfig extends AuthHeroConfig {\n dataAdapter: DataAdapters;\n widgetUrl: string;\n}\n\nexport default function createApp(config: AppConfig) {\n // Initialize multi-tenant AuthHero\n const { app } = initMultiTenant({\n ...config,\n controlPlane: {\n tenantId: CONTROL_PLANE_TENANT_ID,\n clientId: CONTROL_PLANE_CLIENT_ID,\n },\n });\n\n app\n .onError((err, ctx) => {\n if (err && typeof err === \"object\" && \"getResponse\" in err) {\n return (err as { getResponse: () => Response }).getResponse();\n }\n console.error(err);\n return ctx.text(err instanceof Error ? err.message : \"Internal Server Error\", 500);\n })\n .get(\"/\", async (ctx: Context) => {\n return ctx.json({\n name: \"AuthHero Multi-Tenant Server (AWS)\",\n version: \"1.0.0\",\n status: \"running\",\n docs: \"/docs\",\n controlPlaneTenant: CONTROL_PLANE_TENANT_ID,\n });\n })\n .get(\"/docs\", swaggerUI({ url: \"/api/v2/spec\" }))\n // Redirect widget requests to S3/CloudFront\n .get(\"/u/widget/*\", async (ctx) => {\n const file = ctx.req.path.replace(\"/u/widget/\", \"\");\n return ctx.redirect(`${config.widgetUrl}/u/widget/${file}`);\n })\n .get(\"/u/*\", async (ctx) => {\n const file = ctx.req.path.replace(\"/u/\", \"\");\n return ctx.redirect(`${config.widgetUrl}/u/${file}`);\n });\n\n return app;\n}\n" : "import { Context } from \"hono\";\nimport { cors } from \"hono/cors\";\nimport { AuthHeroConfig, init, DataAdapters } from \"authhero\";\nimport { swaggerUI } from \"@hono/swagger-ui\";\n\ninterface AppConfig extends AuthHeroConfig {\n dataAdapter: DataAdapters;\n widgetUrl: string;\n}\n\nexport default function createApp(config: AppConfig) {\n const { app } = init(config);\n\n // Enable CORS for all origins in development\n app.use(\"*\", cors({\n origin: (origin) => origin || \"*\",\n allowMethods: [\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\", \"OPTIONS\"],\n allowHeaders: [\"Content-Type\", \"Authorization\", \"Auth0-Client\"],\n exposeHeaders: [\"Content-Length\"],\n credentials: true,\n }));\n\n app\n .onError((err, ctx) => {\n if (err && typeof err === \"object\" && \"getResponse\" in err) {\n return (err as { getResponse: () => Response }).getResponse();\n }\n console.error(err);\n return ctx.text(err instanceof Error ? err.message : \"Internal Server Error\", 500);\n })\n .get(\"/\", async (ctx: Context) => {\n return ctx.json({\n name: \"AuthHero Server (AWS)\",\n status: \"running\",\n });\n })\n .get(\"/docs\", swaggerUI({ url: \"/api/v2/spec\" }))\n // Redirect widget requests to S3/CloudFront\n .get(\"/u/widget/*\", async (ctx) => {\n const file = ctx.req.path.replace(\"/u/widget/\", \"\");\n return ctx.redirect(`${config.widgetUrl}/u/widget/${file}`);\n })\n .get(\"/u/*\", async (ctx) => {\n const file = ctx.req.path.replace(\"/u/\", \"\");\n return ctx.redirect(`${config.widgetUrl}/u/${file}`);\n });\n\n return app;\n}\n";
|
|
695
747
|
}
|
|
696
|
-
function
|
|
748
|
+
function g(e) {
|
|
697
749
|
return `import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
698
750
|
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
|
|
699
751
|
import createAdapters from "@authhero/aws";
|
|
700
|
-
import { seed } from "authhero";
|
|
752
|
+
import { seed, createEncryptedDataAdapter, loadEncryptionKey } from "authhero";
|
|
701
753
|
|
|
702
754
|
async function main() {
|
|
703
755
|
const adminUsername = process.argv[2] || process.env.ADMIN_USERNAME || "admin";
|
|
@@ -717,7 +769,12 @@ async function main() {
|
|
|
717
769
|
},
|
|
718
770
|
});
|
|
719
771
|
|
|
720
|
-
|
|
772
|
+
let adapters = createAdapters(docClient, { tableName });
|
|
773
|
+
|
|
774
|
+
if (process.env.ENCRYPTION_KEY) {
|
|
775
|
+
const encryptionKey = await loadEncryptionKey(process.env.ENCRYPTION_KEY);
|
|
776
|
+
adapters = createEncryptedDataAdapter(adapters, encryptionKey);
|
|
777
|
+
}
|
|
721
778
|
|
|
722
779
|
await seed(adapters, {
|
|
723
780
|
adminUsername,
|
|
@@ -733,18 +790,21 @@ async function main() {
|
|
|
733
790
|
main().catch(console.error);
|
|
734
791
|
`;
|
|
735
792
|
}
|
|
736
|
-
function
|
|
793
|
+
function _(e, t) {
|
|
737
794
|
let i = r.join(e, "src");
|
|
738
|
-
n.writeFileSync(r.join(i, "app.ts"),
|
|
795
|
+
n.writeFileSync(r.join(i, "app.ts"), h(t)), n.writeFileSync(r.join(i, "seed.ts"), g(t)), n.writeFileSync(r.join(e, ".env"), `# At-rest encryption key for sensitive credentials. Generated automatically.
|
|
796
|
+
# Keep this stable and secret — losing it makes encrypted data unrecoverable.
|
|
797
|
+
ENCRYPTION_KEY=${s()}
|
|
798
|
+
`);
|
|
739
799
|
}
|
|
740
|
-
function
|
|
800
|
+
function v() {
|
|
741
801
|
console.log("\\n" + "─".repeat(50)), console.log("🔐 AuthHero deployed to AWS!"), console.log("📚 Check SST output for your API URL"), console.log("🚀 Open your server URL /setup to complete initial setup"), console.log("🌐 Portal available at https://local.authhero.net"), console.log("─".repeat(50) + "\\n");
|
|
742
802
|
}
|
|
743
|
-
function
|
|
803
|
+
function y(e) {
|
|
744
804
|
let t = r.join(e, ".github", "workflows");
|
|
745
805
|
n.mkdirSync(t, { recursive: !0 }), n.writeFileSync(r.join(t, "unit-tests.yml"), "name: Unit tests\n\non: push\n\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n\n - name: Setup Node.js\n uses: actions/setup-node@v4\n with:\n node-version: \"22\"\n cache: \"npm\"\n\n - name: Install dependencies\n run: npm ci\n\n - run: npm run type-check\n - run: npm test\n"), n.writeFileSync(r.join(t, "deploy-dev.yml"), "name: Deploy to Dev\n\non:\n push:\n branches:\n - main\n\njobs:\n release:\n name: Release and Deploy\n runs-on: ubuntu-latest\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n with:\n fetch-depth: 0\n\n - name: Setup Node.js\n uses: actions/setup-node@v4\n with:\n node-version: \"22\"\n cache: \"npm\"\n\n - name: Install dependencies\n run: npm ci\n\n - name: Release\n env:\n GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: npx semantic-release\n\n - name: Deploy to Cloudflare (Dev)\n uses: cloudflare/wrangler-action@v3\n with:\n apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n command: deploy\n"), n.writeFileSync(r.join(t, "release.yml"), "name: Deploy to Production\n\non:\n release:\n types: [\"released\"]\n\njobs:\n deploy:\n name: Deploy to Production\n runs-on: ubuntu-latest\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n\n - name: Setup Node.js\n uses: actions/setup-node@v4\n with:\n node-version: \"22\"\n cache: \"npm\"\n\n - name: Install dependencies\n run: npm ci\n\n - name: Deploy to Cloudflare (Production)\n uses: cloudflare/wrangler-action@v3\n with:\n apiToken: ${{ secrets.PROD_CLOUDFLARE_API_TOKEN }}\n command: deploy --env production\n"), console.log("\\n📦 GitHub CI workflows created!");
|
|
746
806
|
}
|
|
747
|
-
function
|
|
807
|
+
function b(e) {
|
|
748
808
|
n.writeFileSync(r.join(e, ".releaserc.json"), JSON.stringify({
|
|
749
809
|
branches: ["main"],
|
|
750
810
|
plugins: [
|
|
@@ -763,9 +823,9 @@ function v(e) {
|
|
|
763
823
|
"type-check": "tsc --noEmit"
|
|
764
824
|
}, n.writeFileSync(t, JSON.stringify(i, null, 2));
|
|
765
825
|
}
|
|
766
|
-
function
|
|
826
|
+
function x(e, t) {
|
|
767
827
|
return new Promise((n, r) => {
|
|
768
|
-
let i =
|
|
828
|
+
let i = o(e, [], {
|
|
769
829
|
cwd: t,
|
|
770
830
|
shell: !0,
|
|
771
831
|
stdio: "inherit"
|
|
@@ -775,107 +835,123 @@ function y(e, t) {
|
|
|
775
835
|
}), i.on("error", r);
|
|
776
836
|
});
|
|
777
837
|
}
|
|
778
|
-
function
|
|
838
|
+
function S(e, t, i) {
|
|
779
839
|
let a = r.join(e, "src");
|
|
780
|
-
n.writeFileSync(r.join(a, "app.ts"),
|
|
840
|
+
n.writeFileSync(r.join(a, "app.ts"), m(t, i)), n.writeFileSync(r.join(a, "seed.ts"), p(t));
|
|
781
841
|
}
|
|
782
|
-
function
|
|
842
|
+
function C() {
|
|
783
843
|
console.log("\n" + "─".repeat(50)), console.log("🔐 AuthHero server running at https://localhost:3000"), console.log("🚀 Open https://localhost:3000/setup to complete initial setup"), console.log("─".repeat(50) + "\n");
|
|
784
844
|
}
|
|
785
|
-
function
|
|
845
|
+
function w() {
|
|
846
|
+
console.log("\n" + "─".repeat(50)), console.log("🛰️ WFP dispatcher running at https://localhost:3001"), console.log("📦 Pair with the `cloudflare` template to deploy tenant workers:"), console.log(" wrangler deploy --dispatch-namespace=authhero-tenants --name=tenant-<id>-auth"), console.log("📖 See README.md for the full onboarding workflow"), console.log("─".repeat(50) + "\n");
|
|
847
|
+
}
|
|
848
|
+
function T() {
|
|
786
849
|
console.log("\n" + "─".repeat(50)), console.log("🛰️ AuthHero proxy running at http://localhost:8787"), console.log("✏️ Edit src/proxy.config.ts to add hosts and routes"), console.log("📖 See README.md for deployment instructions"), console.log("─".repeat(50) + "\n");
|
|
787
850
|
}
|
|
788
|
-
function
|
|
851
|
+
function E() {
|
|
789
852
|
console.log("\n" + "─".repeat(50)), console.log("✅ Self-signed certificates generated with openssl"), console.log("⚠️ You may need to trust the certificate in your browser"), console.log("🔐 AuthHero server running at http://localhost:3000"), console.log("📚 API documentation available at http://localhost:3000/docs"), console.log("🚀 Open http://localhost:3000/setup to complete initial setup"), console.log("─".repeat(50) + "\n");
|
|
790
853
|
}
|
|
791
|
-
|
|
792
|
-
let o =
|
|
854
|
+
c.version("1.0.0").description("Create a new AuthHero project").argument("[project-name]", "name of the project").option("-t, --template <type>", "template type: local, cloudflare, aws-sst, or proxy").option("--package-manager <pm>", "package manager to use: npm, yarn, pnpm, or bun").option("--multi-tenant", "enable multi-tenant mode").option("--admin-ui", "include admin UI at /admin").option("--skip-install", "skip installing dependencies").option("--skip-migrate", "skip running database migrations").option("--skip-start", "skip starting the development server").option("--github-ci", "include GitHub CI workflows with semantic versioning").option("--conformance", "add OpenID conformance suite test clients").option("--conformance-alias <alias>", "alias for conformance suite (default: authhero-local)").option("--workspace", "use workspace:* dependencies for local monorepo development").option("-y, --yes", "skip all prompts and use defaults/provided options").action(async (e, i) => {
|
|
855
|
+
let o = i.yes === !0;
|
|
793
856
|
console.log("\n🔐 Welcome to AuthHero!\n");
|
|
794
|
-
let
|
|
795
|
-
|
|
857
|
+
let c = e;
|
|
858
|
+
c || (o ? (c = "auth-server", console.log(`Using default project name: ${c}`)) : c = (await t.prompt([{
|
|
796
859
|
type: "input",
|
|
797
860
|
name: "projectName",
|
|
798
861
|
message: "Project name:",
|
|
799
862
|
default: "auth-server",
|
|
800
863
|
validate: (e) => e !== "" || "Project name cannot be empty"
|
|
801
864
|
}])).projectName);
|
|
802
|
-
let
|
|
803
|
-
n.existsSync(
|
|
804
|
-
let
|
|
805
|
-
|
|
865
|
+
let p = r.join(process.cwd(), c);
|
|
866
|
+
n.existsSync(p) && (console.error(`❌ Project "${c}" already exists.`), process.exit(1));
|
|
867
|
+
let m;
|
|
868
|
+
i.template ? ([
|
|
806
869
|
"local",
|
|
807
870
|
"cloudflare",
|
|
871
|
+
"cloudflare-wfp-dispatcher",
|
|
808
872
|
"aws-sst",
|
|
809
873
|
"proxy"
|
|
810
|
-
].includes(
|
|
874
|
+
].includes(i.template) || (console.error(`❌ Invalid template: ${i.template}`), console.error("Valid options: local, cloudflare, cloudflare-wfp-dispatcher, aws-sst, proxy"), process.exit(1)), m = i.template, console.log(`Using template: ${l[m].name}`)) : m = (await t.prompt([{
|
|
811
875
|
type: "list",
|
|
812
876
|
name: "setupType",
|
|
813
877
|
message: "Select your setup type:",
|
|
814
878
|
choices: [
|
|
815
879
|
{
|
|
816
|
-
name: `${
|
|
880
|
+
name: `${l.local.name}\n ${l.local.description}`,
|
|
817
881
|
value: "local",
|
|
818
|
-
short:
|
|
882
|
+
short: l.local.name
|
|
819
883
|
},
|
|
820
884
|
{
|
|
821
|
-
name: `${
|
|
885
|
+
name: `${l.cloudflare.name}\n ${l.cloudflare.description}`,
|
|
822
886
|
value: "cloudflare",
|
|
823
|
-
short:
|
|
887
|
+
short: l.cloudflare.name
|
|
888
|
+
},
|
|
889
|
+
{
|
|
890
|
+
name: `${l["cloudflare-wfp-dispatcher"].name}\n ${l["cloudflare-wfp-dispatcher"].description}`,
|
|
891
|
+
value: "cloudflare-wfp-dispatcher",
|
|
892
|
+
short: l["cloudflare-wfp-dispatcher"].name
|
|
824
893
|
},
|
|
825
894
|
{
|
|
826
|
-
name: `${
|
|
895
|
+
name: `${l["aws-sst"].name}\n ${l["aws-sst"].description}`,
|
|
827
896
|
value: "aws-sst",
|
|
828
|
-
short:
|
|
897
|
+
short: l["aws-sst"].name
|
|
829
898
|
},
|
|
830
899
|
{
|
|
831
|
-
name: `${
|
|
900
|
+
name: `${l.proxy.name}\n ${l.proxy.description}`,
|
|
832
901
|
value: "proxy",
|
|
833
|
-
short:
|
|
902
|
+
short: l.proxy.name
|
|
834
903
|
}
|
|
835
904
|
]
|
|
836
905
|
}])).setupType;
|
|
837
|
-
let
|
|
838
|
-
|
|
906
|
+
let h;
|
|
907
|
+
h = m === "proxy" || m === "cloudflare-wfp-dispatcher" ? !1 : i.multiTenant === void 0 ? o ? !1 : (await t.prompt([{
|
|
839
908
|
type: "confirm",
|
|
840
909
|
name: "multiTenant",
|
|
841
910
|
message: "Would you like to enable multi-tenant mode?",
|
|
842
911
|
default: !1
|
|
843
|
-
}])).multiTenant :
|
|
844
|
-
let
|
|
845
|
-
(
|
|
912
|
+
}])).multiTenant : i.multiTenant, h && console.log("Multi-tenant mode: enabled");
|
|
913
|
+
let g = !1;
|
|
914
|
+
(m === "local" || m === "cloudflare") && (g = i.adminUi === void 0 ? o ? !0 : (await t.prompt([{
|
|
846
915
|
type: "confirm",
|
|
847
916
|
name: "adminUi",
|
|
848
917
|
message: "Would you like to include the admin UI at /admin?",
|
|
849
918
|
default: !0
|
|
850
|
-
}])).adminUi :
|
|
851
|
-
let
|
|
852
|
-
|
|
853
|
-
let
|
|
854
|
-
|
|
855
|
-
let
|
|
856
|
-
n.mkdirSync(
|
|
857
|
-
let
|
|
858
|
-
if (
|
|
859
|
-
let e = r.join(
|
|
860
|
-
n.existsSync(e) && n.copyFileSync(e, t)
|
|
861
|
-
|
|
862
|
-
|
|
919
|
+
}])).adminUi : i.adminUi, g && console.log("Admin UI: enabled (available at /admin)"));
|
|
920
|
+
let D = i.conformance || !1, O = i.conformanceAlias || "authhero-local";
|
|
921
|
+
D && console.log(`OpenID Conformance Suite: enabled (alias: ${O})`);
|
|
922
|
+
let k = i.workspace || !1;
|
|
923
|
+
k && console.log("Workspace mode: enabled (using workspace:* dependencies)");
|
|
924
|
+
let A = l[m];
|
|
925
|
+
n.mkdirSync(p, { recursive: !0 }), n.writeFileSync(r.join(p, "package.json"), JSON.stringify(A.packageJson(c, h, D, k, g), null, 2));
|
|
926
|
+
let j = A.templateDir, M = r.dirname(a(import.meta.url)), N = [r.join(M, j), r.join(M, "..", "templates", j)], P = N.find((e) => n.existsSync(e));
|
|
927
|
+
if (P ? u(P, p) : (console.error(`❌ Template directory not found. Looked in:\n ${N.join("\n ")}`), process.exit(1)), m === "cloudflare" && S(p, h, g), m === "cloudflare" || m === "cloudflare-wfp-dispatcher") {
|
|
928
|
+
let e = r.join(p, "wrangler.toml"), t = r.join(p, "wrangler.local.toml");
|
|
929
|
+
if (n.existsSync(e) && !n.existsSync(t) && n.copyFileSync(e, t), m === "cloudflare") {
|
|
930
|
+
let e = r.join(p, ".dev.vars.example"), t = r.join(p, ".dev.vars");
|
|
931
|
+
n.existsSync(e) && (n.copyFileSync(e, t), n.appendFileSync(t, `\n# Generated at-rest encryption key (local dev). Use a separate secret in production.\nENCRYPTION_KEY=${s()}\n`), console.log("🔒 Added a generated ENCRYPTION_KEY to .dev.vars")), console.log("📁 Created wrangler.local.toml and .dev.vars for local development");
|
|
932
|
+
} else console.log("📁 Created wrangler.local.toml for local development");
|
|
863
933
|
}
|
|
864
|
-
let
|
|
865
|
-
if (
|
|
934
|
+
let F = !1;
|
|
935
|
+
if (m === "cloudflare" && (i.githubCi === void 0 ? o || (F = (await t.prompt([{
|
|
866
936
|
type: "confirm",
|
|
867
937
|
name: "includeGithubCi",
|
|
868
938
|
message: "Would you like to include GitHub CI with semantic versioning?",
|
|
869
939
|
default: !1
|
|
870
|
-
}])).includeGithubCi) : (
|
|
871
|
-
let e =
|
|
872
|
-
n.writeFileSync(r.join(
|
|
873
|
-
let t =
|
|
874
|
-
n.writeFileSync(r.join(
|
|
940
|
+
}])).includeGithubCi) : (F = i.githubCi, F && console.log("Including GitHub CI workflows with semantic versioning")), F && (y(p), b(p))), m === "local") {
|
|
941
|
+
let e = d(h, D, O, g);
|
|
942
|
+
n.writeFileSync(r.join(p, "src/seed.ts"), e);
|
|
943
|
+
let t = f(h, g);
|
|
944
|
+
n.writeFileSync(r.join(p, "src/app.ts"), t);
|
|
945
|
+
let i = `# Encryption key for at-rest encryption of sensitive credentials.
|
|
946
|
+
# Generated automatically. Keep this stable and secret — losing it makes
|
|
947
|
+
# existing encrypted data unrecoverable. Use a separate key in production.
|
|
948
|
+
ENCRYPTION_KEY=${s()}
|
|
949
|
+
`;
|
|
950
|
+
n.writeFileSync(r.join(p, ".env"), i), console.log("🔒 Generated .env with an at-rest encryption key");
|
|
875
951
|
}
|
|
876
|
-
if (
|
|
952
|
+
if (m === "aws-sst" && _(p, h), D) {
|
|
877
953
|
let e = {
|
|
878
|
-
alias:
|
|
954
|
+
alias: O,
|
|
879
955
|
description: "AuthHero Conformance Test",
|
|
880
956
|
server: { discoveryUrl: "http://host.docker.internal:3000/.well-known/openid-configuration" },
|
|
881
957
|
client: {
|
|
@@ -888,24 +964,24 @@ o.version("1.0.0").description("Create a new AuthHero project").argument("[proje
|
|
|
888
964
|
},
|
|
889
965
|
resource: { resourceUrl: "http://host.docker.internal:3000/userinfo" }
|
|
890
966
|
};
|
|
891
|
-
n.writeFileSync(r.join(
|
|
967
|
+
n.writeFileSync(r.join(p, "conformance-config.json"), JSON.stringify(e, null, 2)), console.log("📝 Created conformance-config.json for OpenID Conformance Suite");
|
|
892
968
|
}
|
|
893
|
-
let
|
|
894
|
-
console.log(`\n✅ Project "${
|
|
895
|
-
let
|
|
896
|
-
if (
|
|
969
|
+
let I = h ? "multi-tenant" : "single-tenant";
|
|
970
|
+
console.log(`\n✅ Project "${c}" has been created with ${A.name} (${I}) setup!\n`);
|
|
971
|
+
let L;
|
|
972
|
+
if (L = i.skipInstall ? !1 : o ? !0 : (await t.prompt([{
|
|
897
973
|
type: "confirm",
|
|
898
974
|
name: "shouldInstall",
|
|
899
975
|
message: "Would you like to install dependencies now?",
|
|
900
976
|
default: !0
|
|
901
|
-
}])).shouldInstall,
|
|
977
|
+
}])).shouldInstall, L) {
|
|
902
978
|
let e;
|
|
903
|
-
|
|
979
|
+
i.packageManager ? ([
|
|
904
980
|
"npm",
|
|
905
981
|
"yarn",
|
|
906
982
|
"pnpm",
|
|
907
983
|
"bun"
|
|
908
|
-
].includes(
|
|
984
|
+
].includes(i.packageManager) || (console.error(`❌ Invalid package manager: ${i.packageManager}`), console.error("Valid options: npm, yarn, pnpm, bun"), process.exit(1)), e = i.packageManager) : e = o ? "pnpm" : (await t.prompt([{
|
|
909
985
|
type: "list",
|
|
910
986
|
name: "packageManager",
|
|
911
987
|
message: "Which package manager would you like to use?",
|
|
@@ -930,26 +1006,26 @@ o.version("1.0.0").description("Create a new AuthHero project").argument("[proje
|
|
|
930
1006
|
default: "pnpm"
|
|
931
1007
|
}])).packageManager, console.log(`\n📦 Installing dependencies with ${e}...\n`);
|
|
932
1008
|
try {
|
|
933
|
-
if (await
|
|
1009
|
+
if (await x(e === "pnpm" ? "pnpm install --ignore-workspace" : `${e} install`, p), m === "local" && (console.log("\n🔧 Building native modules...\n"), await x("npm rebuild better-sqlite3", p)), console.log("\n✅ Dependencies installed successfully!\n"), (m === "local" || m === "cloudflare") && !i.skipMigrate) {
|
|
934
1010
|
let n;
|
|
935
1011
|
n = o ? !0 : (await t.prompt([{
|
|
936
1012
|
type: "confirm",
|
|
937
1013
|
name: "shouldMigrate",
|
|
938
1014
|
message: "Would you like to run database migrations?",
|
|
939
1015
|
default: !0
|
|
940
|
-
}])).shouldMigrate, n && (console.log("\n🔄 Running migrations...\n"), await
|
|
1016
|
+
}])).shouldMigrate, n && (console.log("\n🔄 Running migrations...\n"), await x(`${e} run migrate`, p));
|
|
941
1017
|
}
|
|
942
1018
|
let n;
|
|
943
|
-
n =
|
|
1019
|
+
n = i.skipStart || o ? !1 : (await t.prompt([{
|
|
944
1020
|
type: "confirm",
|
|
945
1021
|
name: "shouldStart",
|
|
946
1022
|
message: "Would you like to start the development server?",
|
|
947
1023
|
default: !0
|
|
948
|
-
}])).shouldStart, n && (
|
|
1024
|
+
}])).shouldStart, n && (m === "cloudflare" ? C() : m === "cloudflare-wfp-dispatcher" ? w() : m === "aws-sst" ? v() : m === "proxy" ? T() : E(), console.log("🚀 Starting development server...\n"), await x(`${e} run dev`, p)), o && !n && (console.log("\n✅ Setup complete!"), console.log("\nTo start the development server:"), console.log(` cd ${c}`), console.log(" npm run dev"), m === "cloudflare" ? C() : m === "cloudflare-wfp-dispatcher" ? w() : m === "aws-sst" ? v() : m === "proxy" ? T() : E());
|
|
949
1025
|
} catch (e) {
|
|
950
1026
|
console.error("\n❌ An error occurred:", e), process.exit(1);
|
|
951
1027
|
}
|
|
952
1028
|
}
|
|
953
|
-
|
|
954
|
-
}),
|
|
1029
|
+
L || (console.log("Next steps:"), console.log(` cd ${c}`), m === "local" ? (console.log(" npm install"), console.log(" npm run migrate"), console.log(" npm run dev"), console.log("\nOpen http://localhost:3000/setup to complete initial setup")) : m === "cloudflare" ? (console.log(" npm install"), console.log(" npm run migrate # or npm run db:migrate:remote for production"), console.log(" npm run dev # or npm run dev:remote for production"), console.log("\nOpen https://localhost:3000/setup to complete initial setup")) : m === "cloudflare-wfp-dispatcher" ? (console.log(" npm install"), console.log(" npm run setup # creates wrangler.local.toml — paste your database_id"), console.log(" npx wrangler dispatch-namespace create authhero-tenants"), console.log(" npm run dev # or npm run dev:remote for production"), console.log("\nDeploy tenant workers separately (`cloudflare` template):"), console.log(" wrangler deploy --dispatch-namespace=authhero-tenants --name=tenant-<id>-auth")) : m === "aws-sst" ? (console.log(" npm install"), console.log(" npm run dev # Deploys to AWS in development mode"), console.log("\nOpen your server URL /setup to complete initial setup")) : m === "proxy" && (console.log(" npm install"), console.log(" npm run dev"), console.log("\nEdit src/proxy.config.ts to add hosts and routes")), console.log(`\nServer will be available at: http://localhost:${m === "proxy" ? 8787 : m === "cloudflare-wfp-dispatcher" ? 3001 : 3e3}`), D && (console.log("\n🧪 OpenID Conformance Suite Testing:"), console.log(" 1. Clone and start the conformance suite (if not already running):"), console.log(" git clone https://gitlab.com/openid/conformance-suite.git"), console.log(" cd conformance-suite && mvn clean package"), console.log(" docker-compose up -d"), console.log(" 2. Open https://localhost.emobix.co.uk:8443"), console.log(" 3. Create a test plan and use conformance-config.json for settings"), console.log(` 4. Use alias: ${O}`)), console.log("\nFor more information, visit: https://authhero.net/docs\n"));
|
|
1030
|
+
}), c.parse(process.argv);
|
|
955
1031
|
//#endregion
|
package/dist/local/README.md
CHANGED
|
@@ -47,4 +47,26 @@ You can customize the AuthHero configuration in `src/app.ts`. Common options inc
|
|
|
47
47
|
- Custom email templates
|
|
48
48
|
- Session configuration
|
|
49
49
|
|
|
50
|
+
## Encryption at rest
|
|
51
|
+
|
|
52
|
+
Sensitive credential fields (client secrets, connection secrets, email
|
|
53
|
+
credentials, TOTP secrets, migration-source secrets) are encrypted at rest.
|
|
54
|
+
A random `ENCRYPTION_KEY` was generated into `.env` when this project was
|
|
55
|
+
created, and the dev/seed scripts load it via `--env-file=.env`.
|
|
56
|
+
|
|
57
|
+
> **The key is load-bearing.** If you delete, rotate, or lose `ENCRYPTION_KEY`,
|
|
58
|
+
> any values already encrypted with it become unreadable. In local dev you can
|
|
59
|
+
> recover by deleting `db.sqlite` and re-running `npm run migrate && npm run seed`.
|
|
60
|
+
> In production, treat the key as a long-lived secret and back it up.
|
|
61
|
+
|
|
62
|
+
For production, set your own `ENCRYPTION_KEY` in the deployment environment
|
|
63
|
+
rather than reusing the generated dev key.
|
|
64
|
+
|
|
65
|
+
Helper scripts:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm run gen:key # print a fresh base64 key
|
|
69
|
+
npm run decrypt -- "enc:v1:..." # decrypt a stored value using ENCRYPTION_KEY from .env
|
|
70
|
+
```
|
|
71
|
+
|
|
50
72
|
For more information, visit [https://authhero.net/docs](https://authhero.net/docs).
|
|
@@ -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"));
|
package/dist/local/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { SqliteDialect } from "kysely";
|
|
|
3
3
|
import { Kysely } from "kysely";
|
|
4
4
|
import Database from "better-sqlite3";
|
|
5
5
|
import createAdapters from "@authhero/kysely-adapter";
|
|
6
|
+
import { createEncryptedDataAdapter, loadEncryptionKey } from "authhero";
|
|
6
7
|
import createApp from "./app";
|
|
7
8
|
import fs from "fs";
|
|
8
9
|
import path from "path";
|
|
@@ -102,7 +103,14 @@ try {
|
|
|
102
103
|
process.exit(1);
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
|
|
106
|
+
let dataAdapter = createAdapters(db);
|
|
107
|
+
|
|
108
|
+
// Encrypt sensitive credential fields at rest when ENCRYPTION_KEY is set
|
|
109
|
+
// (generated into .env at scaffold time). Without it, behavior is unchanged.
|
|
110
|
+
if (process.env.ENCRYPTION_KEY) {
|
|
111
|
+
const encryptionKey = await loadEncryptionKey(process.env.ENCRYPTION_KEY);
|
|
112
|
+
dataAdapter = createEncryptedDataAdapter(dataAdapter, encryptionKey);
|
|
113
|
+
}
|
|
106
114
|
|
|
107
115
|
// Create the AuthHero app
|
|
108
116
|
const app = createApp({
|
package/dist/proxy/src/index.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
import {
|
|
3
|
-
createProxyApp,
|
|
4
|
-
createStaticProxyAdapter,
|
|
5
|
-
} from "@authhero/proxy";
|
|
2
|
+
import { createProxyApp, createStaticProxyAdapter } from "@authhero/proxy";
|
|
6
3
|
import { proxyConfig } from "./proxy.config";
|
|
7
4
|
|
|
8
5
|
// AsyncLocalStorage threads each request's ExecutionContext through to the
|
|
@@ -32,9 +29,8 @@ const app = createProxyApp({
|
|
|
32
29
|
|
|
33
30
|
export default {
|
|
34
31
|
fetch(request: Request, _env: unknown, ctx: ExecutionContext) {
|
|
35
|
-
return requestCtx.run(
|
|
36
|
-
|
|
37
|
-
() => app.fetch(request),
|
|
32
|
+
return requestCtx.run({ waitUntil: ctx.waitUntil.bind(ctx) }, () =>
|
|
33
|
+
app.fetch(request),
|
|
38
34
|
);
|
|
39
35
|
},
|
|
40
36
|
};
|