create-authhero 0.43.0 → 0.45.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 +14 -2
- package/dist/cloudflare/src/types.ts +5 -0
- package/dist/create-authhero.js +128 -92
- 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/README.md +109 -2
- package/dist/proxy/src/proxy.config.ts +15 -5
- 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"));
|
|
@@ -3,7 +3,11 @@ import { Kysely } from "kysely";
|
|
|
3
3
|
import createAdapters from "@authhero/kysely-adapter";
|
|
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.)
|
|
@@ -20,7 +24,15 @@ export default {
|
|
|
20
24
|
|
|
21
25
|
const dialect = new D1Dialect({ database: env.AUTH_DB });
|
|
22
26
|
const db = new Kysely<any>({ dialect });
|
|
23
|
-
|
|
27
|
+
let dataAdapter = createAdapters(db, { useTransactions: false });
|
|
28
|
+
|
|
29
|
+
// Encrypt sensitive credential fields at rest when ENCRYPTION_KEY is set.
|
|
30
|
+
// In local dev it comes from .dev.vars; in production set it with
|
|
31
|
+
// `wrangler secret put ENCRYPTION_KEY`. Without it, behavior is unchanged.
|
|
32
|
+
if (env.ENCRYPTION_KEY) {
|
|
33
|
+
const encryptionKey = await loadEncryptionKey(env.ENCRYPTION_KEY);
|
|
34
|
+
dataAdapter = createEncryptedDataAdapter(dataAdapter, encryptionKey);
|
|
35
|
+
}
|
|
24
36
|
|
|
25
37
|
// ────────────────────────────────────────────────────────────────────────
|
|
26
38
|
// 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:
|
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,7 +75,9 @@ 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,
|
|
@@ -135,8 +143,10 @@ var o = new e(), s = {
|
|
|
135
143
|
dev: "sst dev",
|
|
136
144
|
deploy: "sst deploy --stage production",
|
|
137
145
|
remove: "sst remove",
|
|
138
|
-
seed: "npx tsx src/seed.ts",
|
|
139
|
-
"copy-assets": "node copy-assets.js"
|
|
146
|
+
seed: "npx tsx --env-file=.env src/seed.ts",
|
|
147
|
+
"copy-assets": "node copy-assets.js",
|
|
148
|
+
"gen:key": "node scripts/generate-encryption-key.mjs",
|
|
149
|
+
decrypt: "node --env-file=.env scripts/decrypt-field.mjs"
|
|
140
150
|
},
|
|
141
151
|
dependencies: {
|
|
142
152
|
"@authhero/aws": a,
|
|
@@ -163,13 +173,13 @@ var o = new e(), s = {
|
|
|
163
173
|
seedFile: "seed.ts"
|
|
164
174
|
}
|
|
165
175
|
};
|
|
166
|
-
function
|
|
176
|
+
function u(e, t) {
|
|
167
177
|
n.readdirSync(e).forEach((i) => {
|
|
168
178
|
let a = r.join(e, i), o = r.join(t, i);
|
|
169
|
-
n.lstatSync(a).isDirectory() ? (n.mkdirSync(o, { recursive: !0 }),
|
|
179
|
+
n.lstatSync(a).isDirectory() ? (n.mkdirSync(o, { recursive: !0 }), u(a, o)) : n.copyFileSync(a, o);
|
|
170
180
|
});
|
|
171
181
|
}
|
|
172
|
-
function
|
|
182
|
+
function d(e, t = !1, n = "authhero-local", r) {
|
|
173
183
|
let i = e ? "control_plane" : "main", a = e ? "Control Plane" : "Main", o = [
|
|
174
184
|
"https://manage.authhero.net/auth-callback",
|
|
175
185
|
"https://local.authhero.net/auth-callback",
|
|
@@ -314,7 +324,7 @@ function l(e, t = !1, n = "authhero-local", r) {
|
|
|
314
324
|
return `import { SqliteDialect, Kysely } from "kysely";
|
|
315
325
|
import Database from "better-sqlite3";
|
|
316
326
|
import createAdapters from "@authhero/kysely-adapter";
|
|
317
|
-
import { seed${t ? ", USERNAME_PASSWORD_PROVIDER" : ""} } from "authhero";
|
|
327
|
+
import { seed, createEncryptedDataAdapter, loadEncryptionKey${t ? ", USERNAME_PASSWORD_PROVIDER" : ""} } from "authhero";
|
|
318
328
|
|
|
319
329
|
interface ExtraClient {
|
|
320
330
|
client_id: string;
|
|
@@ -368,7 +378,13 @@ async function main() {
|
|
|
368
378
|
});
|
|
369
379
|
|
|
370
380
|
const db = new Kysely<any>({ dialect });
|
|
371
|
-
|
|
381
|
+
let adapters = createAdapters(db);
|
|
382
|
+
|
|
383
|
+
// Match the server: encrypt seeded secrets at rest when a key is configured.
|
|
384
|
+
if (process.env.ENCRYPTION_KEY) {
|
|
385
|
+
const encryptionKey = await loadEncryptionKey(process.env.ENCRYPTION_KEY);
|
|
386
|
+
adapters = createEncryptedDataAdapter(adapters, encryptionKey);
|
|
387
|
+
}
|
|
372
388
|
|
|
373
389
|
const seedResult = await seed(adapters, {
|
|
374
390
|
adminUsername,
|
|
@@ -417,7 +433,7 @@ main().catch((err) => {
|
|
|
417
433
|
});
|
|
418
434
|
`;
|
|
419
435
|
}
|
|
420
|
-
function
|
|
436
|
+
function f(e, t) {
|
|
421
437
|
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
438
|
// Add admin UI handler if the package is installed
|
|
423
439
|
if (fs.existsSync(adminIndexPath)) {
|
|
@@ -546,14 +562,15 @@ ${i}
|
|
|
546
562
|
}
|
|
547
563
|
`;
|
|
548
564
|
}
|
|
549
|
-
function
|
|
565
|
+
function p(e) {
|
|
550
566
|
return `import { D1Dialect } from "kysely-d1";
|
|
551
567
|
import { Kysely } from "kysely";
|
|
552
568
|
import createAdapters from "@authhero/kysely-adapter";
|
|
553
|
-
import { seed } from "authhero";
|
|
569
|
+
import { seed, createEncryptedDataAdapter, loadEncryptionKey } from "authhero";
|
|
554
570
|
|
|
555
571
|
interface Env {
|
|
556
572
|
AUTH_DB: D1Database;
|
|
573
|
+
ENCRYPTION_KEY?: string;
|
|
557
574
|
}
|
|
558
575
|
|
|
559
576
|
export default {
|
|
@@ -567,7 +584,12 @@ export default {
|
|
|
567
584
|
try {
|
|
568
585
|
const dialect = new D1Dialect({ database: env.AUTH_DB });
|
|
569
586
|
const db = new Kysely<any>({ dialect });
|
|
570
|
-
|
|
587
|
+
let adapters = createAdapters(db, { useTransactions: false });
|
|
588
|
+
|
|
589
|
+
if (env.ENCRYPTION_KEY) {
|
|
590
|
+
const encryptionKey = await loadEncryptionKey(env.ENCRYPTION_KEY);
|
|
591
|
+
adapters = createEncryptedDataAdapter(adapters, encryptionKey);
|
|
592
|
+
}
|
|
571
593
|
|
|
572
594
|
const result = await seed(adapters, {
|
|
573
595
|
adminUsername,
|
|
@@ -607,7 +629,7 @@ export default {
|
|
|
607
629
|
};
|
|
608
630
|
`;
|
|
609
631
|
}
|
|
610
|
-
function
|
|
632
|
+
function m(e, t) {
|
|
611
633
|
let n = t ? "import adminIndexHtml from \"./admin-index-html\";\n" : "", r = t ? " adminIndexHtml,\n" : "";
|
|
612
634
|
return e ? `import { Context } from "hono";
|
|
613
635
|
import { swaggerUI } from "@hono/swagger-ui";
|
|
@@ -690,14 +712,14 @@ ${r} });
|
|
|
690
712
|
}
|
|
691
713
|
`;
|
|
692
714
|
}
|
|
693
|
-
function
|
|
715
|
+
function h(e) {
|
|
694
716
|
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
717
|
}
|
|
696
|
-
function
|
|
718
|
+
function g(e) {
|
|
697
719
|
return `import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
698
720
|
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
|
|
699
721
|
import createAdapters from "@authhero/aws";
|
|
700
|
-
import { seed } from "authhero";
|
|
722
|
+
import { seed, createEncryptedDataAdapter, loadEncryptionKey } from "authhero";
|
|
701
723
|
|
|
702
724
|
async function main() {
|
|
703
725
|
const adminUsername = process.argv[2] || process.env.ADMIN_USERNAME || "admin";
|
|
@@ -717,7 +739,12 @@ async function main() {
|
|
|
717
739
|
},
|
|
718
740
|
});
|
|
719
741
|
|
|
720
|
-
|
|
742
|
+
let adapters = createAdapters(docClient, { tableName });
|
|
743
|
+
|
|
744
|
+
if (process.env.ENCRYPTION_KEY) {
|
|
745
|
+
const encryptionKey = await loadEncryptionKey(process.env.ENCRYPTION_KEY);
|
|
746
|
+
adapters = createEncryptedDataAdapter(adapters, encryptionKey);
|
|
747
|
+
}
|
|
721
748
|
|
|
722
749
|
await seed(adapters, {
|
|
723
750
|
adminUsername,
|
|
@@ -733,18 +760,21 @@ async function main() {
|
|
|
733
760
|
main().catch(console.error);
|
|
734
761
|
`;
|
|
735
762
|
}
|
|
736
|
-
function
|
|
763
|
+
function _(e, t) {
|
|
737
764
|
let i = r.join(e, "src");
|
|
738
|
-
n.writeFileSync(r.join(i, "app.ts"),
|
|
765
|
+
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.
|
|
766
|
+
# Keep this stable and secret — losing it makes encrypted data unrecoverable.
|
|
767
|
+
ENCRYPTION_KEY=${s()}
|
|
768
|
+
`);
|
|
739
769
|
}
|
|
740
|
-
function
|
|
770
|
+
function v() {
|
|
741
771
|
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
772
|
}
|
|
743
|
-
function
|
|
773
|
+
function y(e) {
|
|
744
774
|
let t = r.join(e, ".github", "workflows");
|
|
745
775
|
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
776
|
}
|
|
747
|
-
function
|
|
777
|
+
function b(e) {
|
|
748
778
|
n.writeFileSync(r.join(e, ".releaserc.json"), JSON.stringify({
|
|
749
779
|
branches: ["main"],
|
|
750
780
|
plugins: [
|
|
@@ -763,9 +793,9 @@ function v(e) {
|
|
|
763
793
|
"type-check": "tsc --noEmit"
|
|
764
794
|
}, n.writeFileSync(t, JSON.stringify(i, null, 2));
|
|
765
795
|
}
|
|
766
|
-
function
|
|
796
|
+
function x(e, t) {
|
|
767
797
|
return new Promise((n, r) => {
|
|
768
|
-
let i =
|
|
798
|
+
let i = o(e, [], {
|
|
769
799
|
cwd: t,
|
|
770
800
|
shell: !0,
|
|
771
801
|
stdio: "inherit"
|
|
@@ -775,107 +805,113 @@ function y(e, t) {
|
|
|
775
805
|
}), i.on("error", r);
|
|
776
806
|
});
|
|
777
807
|
}
|
|
778
|
-
function
|
|
808
|
+
function S(e, t, i) {
|
|
779
809
|
let a = r.join(e, "src");
|
|
780
|
-
n.writeFileSync(r.join(a, "app.ts"),
|
|
810
|
+
n.writeFileSync(r.join(a, "app.ts"), m(t, i)), n.writeFileSync(r.join(a, "seed.ts"), p(t));
|
|
781
811
|
}
|
|
782
|
-
function
|
|
812
|
+
function C() {
|
|
783
813
|
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
814
|
}
|
|
785
|
-
function
|
|
815
|
+
function w() {
|
|
786
816
|
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
817
|
}
|
|
788
|
-
function
|
|
818
|
+
function T() {
|
|
789
819
|
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
820
|
}
|
|
791
|
-
|
|
792
|
-
let o =
|
|
821
|
+
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) => {
|
|
822
|
+
let o = i.yes === !0;
|
|
793
823
|
console.log("\n🔐 Welcome to AuthHero!\n");
|
|
794
|
-
let
|
|
795
|
-
|
|
824
|
+
let c = e;
|
|
825
|
+
c || (o ? (c = "auth-server", console.log(`Using default project name: ${c}`)) : c = (await t.prompt([{
|
|
796
826
|
type: "input",
|
|
797
827
|
name: "projectName",
|
|
798
828
|
message: "Project name:",
|
|
799
829
|
default: "auth-server",
|
|
800
830
|
validate: (e) => e !== "" || "Project name cannot be empty"
|
|
801
831
|
}])).projectName);
|
|
802
|
-
let
|
|
803
|
-
n.existsSync(
|
|
804
|
-
let
|
|
805
|
-
|
|
832
|
+
let p = r.join(process.cwd(), c);
|
|
833
|
+
n.existsSync(p) && (console.error(`❌ Project "${c}" already exists.`), process.exit(1));
|
|
834
|
+
let m;
|
|
835
|
+
i.template ? ([
|
|
806
836
|
"local",
|
|
807
837
|
"cloudflare",
|
|
808
838
|
"aws-sst",
|
|
809
839
|
"proxy"
|
|
810
|
-
].includes(
|
|
840
|
+
].includes(i.template) || (console.error(`❌ Invalid template: ${i.template}`), console.error("Valid options: local, cloudflare, aws-sst, proxy"), process.exit(1)), m = i.template, console.log(`Using template: ${l[m].name}`)) : m = (await t.prompt([{
|
|
811
841
|
type: "list",
|
|
812
842
|
name: "setupType",
|
|
813
843
|
message: "Select your setup type:",
|
|
814
844
|
choices: [
|
|
815
845
|
{
|
|
816
|
-
name: `${
|
|
846
|
+
name: `${l.local.name}\n ${l.local.description}`,
|
|
817
847
|
value: "local",
|
|
818
|
-
short:
|
|
848
|
+
short: l.local.name
|
|
819
849
|
},
|
|
820
850
|
{
|
|
821
|
-
name: `${
|
|
851
|
+
name: `${l.cloudflare.name}\n ${l.cloudflare.description}`,
|
|
822
852
|
value: "cloudflare",
|
|
823
|
-
short:
|
|
853
|
+
short: l.cloudflare.name
|
|
824
854
|
},
|
|
825
855
|
{
|
|
826
|
-
name: `${
|
|
856
|
+
name: `${l["aws-sst"].name}\n ${l["aws-sst"].description}`,
|
|
827
857
|
value: "aws-sst",
|
|
828
|
-
short:
|
|
858
|
+
short: l["aws-sst"].name
|
|
829
859
|
},
|
|
830
860
|
{
|
|
831
|
-
name: `${
|
|
861
|
+
name: `${l.proxy.name}\n ${l.proxy.description}`,
|
|
832
862
|
value: "proxy",
|
|
833
|
-
short:
|
|
863
|
+
short: l.proxy.name
|
|
834
864
|
}
|
|
835
865
|
]
|
|
836
866
|
}])).setupType;
|
|
837
|
-
let
|
|
838
|
-
|
|
867
|
+
let h;
|
|
868
|
+
h = m === "proxy" ? !1 : i.multiTenant === void 0 ? o ? !1 : (await t.prompt([{
|
|
839
869
|
type: "confirm",
|
|
840
870
|
name: "multiTenant",
|
|
841
871
|
message: "Would you like to enable multi-tenant mode?",
|
|
842
872
|
default: !1
|
|
843
|
-
}])).multiTenant :
|
|
844
|
-
let
|
|
845
|
-
(
|
|
873
|
+
}])).multiTenant : i.multiTenant, h && console.log("Multi-tenant mode: enabled");
|
|
874
|
+
let g = !1;
|
|
875
|
+
(m === "local" || m === "cloudflare") && (g = i.adminUi === void 0 ? o ? !0 : (await t.prompt([{
|
|
846
876
|
type: "confirm",
|
|
847
877
|
name: "adminUi",
|
|
848
878
|
message: "Would you like to include the admin UI at /admin?",
|
|
849
879
|
default: !0
|
|
850
|
-
}])).adminUi :
|
|
851
|
-
let
|
|
852
|
-
|
|
853
|
-
let
|
|
854
|
-
|
|
855
|
-
let
|
|
856
|
-
n.mkdirSync(
|
|
857
|
-
let
|
|
858
|
-
if (
|
|
859
|
-
let e = r.join(
|
|
880
|
+
}])).adminUi : i.adminUi, g && console.log("Admin UI: enabled (available at /admin)"));
|
|
881
|
+
let E = i.conformance || !1, D = i.conformanceAlias || "authhero-local";
|
|
882
|
+
E && console.log(`OpenID Conformance Suite: enabled (alias: ${D})`);
|
|
883
|
+
let O = i.workspace || !1;
|
|
884
|
+
O && console.log("Workspace mode: enabled (using workspace:* dependencies)");
|
|
885
|
+
let k = l[m];
|
|
886
|
+
n.mkdirSync(p, { recursive: !0 }), n.writeFileSync(r.join(p, "package.json"), JSON.stringify(k.packageJson(c, h, E, O, g), null, 2));
|
|
887
|
+
let A = k.templateDir, j = r.dirname(a(import.meta.url)), M = [r.join(j, A), r.join(j, "..", "templates", A)], N = M.find((e) => n.existsSync(e));
|
|
888
|
+
if (N ? u(N, p) : (console.error(`❌ Template directory not found. Looked in:\n ${M.join("\n ")}`), process.exit(1)), m === "cloudflare" && S(p, h, g), m === "cloudflare") {
|
|
889
|
+
let e = r.join(p, "wrangler.toml"), t = r.join(p, "wrangler.local.toml");
|
|
860
890
|
n.existsSync(e) && n.copyFileSync(e, t);
|
|
861
|
-
let i = r.join(
|
|
862
|
-
n.existsSync(i) && n.copyFileSync(i, a), console.log("📁 Created wrangler.local.toml and .dev.vars for local development");
|
|
891
|
+
let i = r.join(p, ".dev.vars.example"), a = r.join(p, ".dev.vars");
|
|
892
|
+
n.existsSync(i) && (n.copyFileSync(i, a), n.appendFileSync(a, `\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");
|
|
863
893
|
}
|
|
864
|
-
let
|
|
865
|
-
if (
|
|
894
|
+
let P = !1;
|
|
895
|
+
if (m === "cloudflare" && (i.githubCi === void 0 ? o || (P = (await t.prompt([{
|
|
866
896
|
type: "confirm",
|
|
867
897
|
name: "includeGithubCi",
|
|
868
898
|
message: "Would you like to include GitHub CI with semantic versioning?",
|
|
869
899
|
default: !1
|
|
870
|
-
}])).includeGithubCi) : (
|
|
871
|
-
let e =
|
|
872
|
-
n.writeFileSync(r.join(
|
|
873
|
-
let t =
|
|
874
|
-
n.writeFileSync(r.join(
|
|
900
|
+
}])).includeGithubCi) : (P = i.githubCi, P && console.log("Including GitHub CI workflows with semantic versioning")), P && (y(p), b(p))), m === "local") {
|
|
901
|
+
let e = d(h, E, D, g);
|
|
902
|
+
n.writeFileSync(r.join(p, "src/seed.ts"), e);
|
|
903
|
+
let t = f(h, g);
|
|
904
|
+
n.writeFileSync(r.join(p, "src/app.ts"), t);
|
|
905
|
+
let i = `# Encryption key for at-rest encryption of sensitive credentials.
|
|
906
|
+
# Generated automatically. Keep this stable and secret — losing it makes
|
|
907
|
+
# existing encrypted data unrecoverable. Use a separate key in production.
|
|
908
|
+
ENCRYPTION_KEY=${s()}
|
|
909
|
+
`;
|
|
910
|
+
n.writeFileSync(r.join(p, ".env"), i), console.log("🔒 Generated .env with an at-rest encryption key");
|
|
875
911
|
}
|
|
876
|
-
if (
|
|
912
|
+
if (m === "aws-sst" && _(p, h), E) {
|
|
877
913
|
let e = {
|
|
878
|
-
alias:
|
|
914
|
+
alias: D,
|
|
879
915
|
description: "AuthHero Conformance Test",
|
|
880
916
|
server: { discoveryUrl: "http://host.docker.internal:3000/.well-known/openid-configuration" },
|
|
881
917
|
client: {
|
|
@@ -888,24 +924,24 @@ o.version("1.0.0").description("Create a new AuthHero project").argument("[proje
|
|
|
888
924
|
},
|
|
889
925
|
resource: { resourceUrl: "http://host.docker.internal:3000/userinfo" }
|
|
890
926
|
};
|
|
891
|
-
n.writeFileSync(r.join(
|
|
927
|
+
n.writeFileSync(r.join(p, "conformance-config.json"), JSON.stringify(e, null, 2)), console.log("📝 Created conformance-config.json for OpenID Conformance Suite");
|
|
892
928
|
}
|
|
893
|
-
let
|
|
894
|
-
console.log(`\n✅ Project "${
|
|
895
|
-
let
|
|
896
|
-
if (
|
|
929
|
+
let F = h ? "multi-tenant" : "single-tenant";
|
|
930
|
+
console.log(`\n✅ Project "${c}" has been created with ${k.name} (${F}) setup!\n`);
|
|
931
|
+
let I;
|
|
932
|
+
if (I = i.skipInstall ? !1 : o ? !0 : (await t.prompt([{
|
|
897
933
|
type: "confirm",
|
|
898
934
|
name: "shouldInstall",
|
|
899
935
|
message: "Would you like to install dependencies now?",
|
|
900
936
|
default: !0
|
|
901
|
-
}])).shouldInstall,
|
|
937
|
+
}])).shouldInstall, I) {
|
|
902
938
|
let e;
|
|
903
|
-
|
|
939
|
+
i.packageManager ? ([
|
|
904
940
|
"npm",
|
|
905
941
|
"yarn",
|
|
906
942
|
"pnpm",
|
|
907
943
|
"bun"
|
|
908
|
-
].includes(
|
|
944
|
+
].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
945
|
type: "list",
|
|
910
946
|
name: "packageManager",
|
|
911
947
|
message: "Which package manager would you like to use?",
|
|
@@ -930,26 +966,26 @@ o.version("1.0.0").description("Create a new AuthHero project").argument("[proje
|
|
|
930
966
|
default: "pnpm"
|
|
931
967
|
}])).packageManager, console.log(`\n📦 Installing dependencies with ${e}...\n`);
|
|
932
968
|
try {
|
|
933
|
-
if (await
|
|
969
|
+
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
970
|
let n;
|
|
935
971
|
n = o ? !0 : (await t.prompt([{
|
|
936
972
|
type: "confirm",
|
|
937
973
|
name: "shouldMigrate",
|
|
938
974
|
message: "Would you like to run database migrations?",
|
|
939
975
|
default: !0
|
|
940
|
-
}])).shouldMigrate, n && (console.log("\n🔄 Running migrations...\n"), await
|
|
976
|
+
}])).shouldMigrate, n && (console.log("\n🔄 Running migrations...\n"), await x(`${e} run migrate`, p));
|
|
941
977
|
}
|
|
942
978
|
let n;
|
|
943
|
-
n =
|
|
979
|
+
n = i.skipStart || o ? !1 : (await t.prompt([{
|
|
944
980
|
type: "confirm",
|
|
945
981
|
name: "shouldStart",
|
|
946
982
|
message: "Would you like to start the development server?",
|
|
947
983
|
default: !0
|
|
948
|
-
}])).shouldStart, n && (
|
|
984
|
+
}])).shouldStart, n && (m === "cloudflare" ? C() : m === "aws-sst" ? v() : m === "proxy" ? w() : T(), 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 === "aws-sst" ? v() : m === "proxy" ? w() : T());
|
|
949
985
|
} catch (e) {
|
|
950
986
|
console.error("\n❌ An error occurred:", e), process.exit(1);
|
|
951
987
|
}
|
|
952
988
|
}
|
|
953
|
-
|
|
954
|
-
}),
|
|
989
|
+
I || (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 === "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 : 3e3}`), E && (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: ${D}`)), console.log("\nFor more information, visit: https://authhero.net/docs\n"));
|
|
990
|
+
}), c.parse(process.argv);
|
|
955
991
|
//#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/README.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
A Cloudflare Worker that proxies incoming requests to upstream services based on the request's `Host` header. Built on [@authhero/proxy](https://www.npmjs.com/package/@authhero/proxy).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
This template ships with a static, in-file config so you can run it immediately. For production you'll typically swap that adapter for one that reads routes from your authhero deployment — see [Data sources](#data-sources) below.
|
|
6
|
+
|
|
7
|
+
## Configure your routes (default)
|
|
6
8
|
|
|
7
9
|
Edit [src/proxy.config.ts](src/proxy.config.ts) to map each public hostname to one or more upstream routes. Path patterns support `*` and `:param` segments, and routes are matched in priority order (lower wins).
|
|
8
10
|
|
|
@@ -23,6 +25,111 @@ export const proxyConfig: StaticProxyAdapterOptions = {
|
|
|
23
25
|
};
|
|
24
26
|
```
|
|
25
27
|
|
|
28
|
+
## Data sources
|
|
29
|
+
|
|
30
|
+
The proxy reads its routes through a `ProxyDataAdapter`. Three implementations are common:
|
|
31
|
+
|
|
32
|
+
| Adapter | Best for | Notes |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| **Static** (default) | Local dev, small fixed deployments | Routes baked into the worker bundle; re-deploy to change them. |
|
|
35
|
+
| **Database** | Same-process or co-located deployments | Reads directly from the proxy_routes table that authhero writes to. |
|
|
36
|
+
| **HTTP / management API** | Geographically distributed proxies, hosted Workers | Calls `/api/v2/proxy-routes` on your authhero server with a service token. |
|
|
37
|
+
|
|
38
|
+
The authhero server exposes the management API (`/api/v2/proxy-routes`) and creates the underlying table automatically once the standard adapter migrations have run — see the `local` template's [src/index.ts](../local/src/index.ts).
|
|
39
|
+
|
|
40
|
+
### Database-backed (Kysely)
|
|
41
|
+
|
|
42
|
+
Add the adapter and your Kysely driver, then swap the data line:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install @authhero/kysely-adapter kysely
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { Kysely } from "kysely";
|
|
50
|
+
import { createProxyApp } from "@authhero/proxy";
|
|
51
|
+
import { createProxyDataAdapter } from "@authhero/kysely-adapter";
|
|
52
|
+
|
|
53
|
+
const db = new Kysely({ dialect: /* your dialect */ });
|
|
54
|
+
const app = createProxyApp({
|
|
55
|
+
data: createProxyDataAdapter(db),
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Cloudflare Workers can't open SQLite files, so on Workers this path means D1, Hyperdrive, or a remote MySQL/Postgres. For a local Node process, `better-sqlite3` pointing at the same `db.sqlite` your authhero server uses works out of the box.
|
|
60
|
+
|
|
61
|
+
### HTTP-backed (management API)
|
|
62
|
+
|
|
63
|
+
The proxy authenticates to authhero's management API with a service token. Issue the token from authhero with the scopes `read:proxy_routes` and `read:custom_domains`, then store it as a worker secret:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
wrangler secret put AUTHHERO_SERVICE_TOKEN
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`wrangler.toml` vars:
|
|
70
|
+
|
|
71
|
+
```toml
|
|
72
|
+
[vars]
|
|
73
|
+
AUTHHERO_API_URL = "https://auth.example.com"
|
|
74
|
+
AUTHHERO_TENANT_ID = "example"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Then build an adapter that resolves the host via `/api/v2/custom-domains` and the routes via `/api/v2/proxy-routes`:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import type { ProxyDataAdapter, ResolvedHost } from "@authhero/proxy";
|
|
81
|
+
|
|
82
|
+
interface Env {
|
|
83
|
+
AUTHHERO_API_URL: string;
|
|
84
|
+
AUTHHERO_TENANT_ID: string;
|
|
85
|
+
AUTHHERO_SERVICE_TOKEN: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function createHttpProxyAdapter(env: Env): ProxyDataAdapter {
|
|
89
|
+
const headers = {
|
|
90
|
+
"authorization": `Bearer ${env.AUTHHERO_SERVICE_TOKEN}`,
|
|
91
|
+
"tenant-id": env.AUTHHERO_TENANT_ID,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
async function api<T>(path: string): Promise<T> {
|
|
95
|
+
const res = await fetch(`${env.AUTHHERO_API_URL}${path}`, { headers });
|
|
96
|
+
if (!res.ok) throw new Error(`${path}: ${res.status}`);
|
|
97
|
+
return res.json() as Promise<T>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
// The proxy data plane only needs resolveHost; the CRUD methods on
|
|
102
|
+
// proxyRoutes stay unused (writes always go through authhero directly).
|
|
103
|
+
proxyRoutes: {
|
|
104
|
+
list: () => { throw new Error("read-only proxy adapter"); },
|
|
105
|
+
get: () => { throw new Error("read-only proxy adapter"); },
|
|
106
|
+
create: () => { throw new Error("read-only proxy adapter"); },
|
|
107
|
+
update: () => { throw new Error("read-only proxy adapter"); },
|
|
108
|
+
remove: () => { throw new Error("read-only proxy adapter"); },
|
|
109
|
+
},
|
|
110
|
+
async resolveHost(host): Promise<ResolvedHost | null> {
|
|
111
|
+
const domains = await api<{ custom_domains: Array<{
|
|
112
|
+
custom_domain_id: string; domain: string;
|
|
113
|
+
}> }>("/api/v2/custom-domains");
|
|
114
|
+
const match = domains.custom_domains.find((d) => d.domain === host);
|
|
115
|
+
if (!match) return null;
|
|
116
|
+
|
|
117
|
+
const routes = await api<{ proxy_routes: unknown[] }>(
|
|
118
|
+
`/api/v2/proxy-routes?custom_domain_id=${match.custom_domain_id}&per_page=200`,
|
|
119
|
+
);
|
|
120
|
+
return {
|
|
121
|
+
tenant_id: env.AUTHHERO_TENANT_ID,
|
|
122
|
+
custom_domain_id: match.custom_domain_id,
|
|
123
|
+
domain: host,
|
|
124
|
+
routes: routes.proxy_routes as ResolvedHost["routes"],
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Cache aggressively (see below) — every cold cache miss is two API round-trips.
|
|
132
|
+
|
|
26
133
|
## Caching
|
|
27
134
|
|
|
28
135
|
Resolved hosts are cached in-memory per Worker isolate with a stale-while-revalidate strategy:
|
|
@@ -31,7 +138,7 @@ Resolved hosts are cached in-memory per Worker isolate with a stale-while-revali
|
|
|
31
138
|
- **Stale** for the next hour — served from cache while a background refresh runs.
|
|
32
139
|
- **Negative** (host not found) — cached for 30 seconds so newly-added hosts come online quickly.
|
|
33
140
|
|
|
34
|
-
For the static adapter the "refresh" is just a re-read of the in-memory config, so the SWR window mainly matters when you swap to
|
|
141
|
+
For the static adapter the "refresh" is just a re-read of the in-memory config, so the SWR window mainly matters when you swap to the HTTP- or database-backed adapter.
|
|
35
142
|
|
|
36
143
|
## Develop locally
|
|
37
144
|
|
|
@@ -2,7 +2,11 @@ import type { StaticProxyAdapterOptions } from "@authhero/proxy";
|
|
|
2
2
|
|
|
3
3
|
// Map each public hostname to the routes the proxy should serve for it.
|
|
4
4
|
// Routes are matched in priority order (lower priority wins). The path
|
|
5
|
-
// pattern supports `*` and `:param` segments.
|
|
5
|
+
// pattern in `match.path` supports `*` and `:param` segments (Hono syntax).
|
|
6
|
+
//
|
|
7
|
+
// Each route is an ordered list of handlers. The last handler is the
|
|
8
|
+
// terminal — it produces the response (e.g. `http`, `redirect`, `static`,
|
|
9
|
+
// `service_binding`). Earlier handlers wrap it, like Hono middleware.
|
|
6
10
|
//
|
|
7
11
|
// Edit this file to add your hosts, then re-deploy.
|
|
8
12
|
export const proxyConfig: StaticProxyAdapterOptions = {
|
|
@@ -11,10 +15,16 @@ export const proxyConfig: StaticProxyAdapterOptions = {
|
|
|
11
15
|
tenant_id: "example",
|
|
12
16
|
routes: [
|
|
13
17
|
{
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
match: { path: "/*" },
|
|
19
|
+
handlers: [
|
|
20
|
+
{
|
|
21
|
+
type: "http",
|
|
22
|
+
options: {
|
|
23
|
+
upstream_url: "https://upstream.example.com",
|
|
24
|
+
preserve_host: false,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
],
|
|
18
28
|
},
|
|
19
29
|
],
|
|
20
30
|
},
|