create-authhero 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,142 @@
1
+ # AuthHero - AWS SST (Lambda + DynamoDB)
2
+
3
+ A serverless AuthHero deployment using [SST](https://sst.dev) with AWS Lambda and DynamoDB.
4
+
5
+ ## Prerequisites
6
+
7
+ - Node.js 18+
8
+ - AWS CLI configured with credentials
9
+ - An AWS account
10
+
11
+ ## Setup
12
+
13
+ 1. **Install dependencies**
14
+
15
+ ```bash
16
+ npm install
17
+ ```
18
+
19
+ 2. **Start development mode**
20
+
21
+ ```bash
22
+ npm run dev
23
+ ```
24
+
25
+ This will:
26
+ - Deploy to your AWS account in development mode
27
+ - Create a DynamoDB table
28
+ - Start a Lambda function with live reloading
29
+ - Output your API URL
30
+
31
+ 3. **Seed the database**
32
+
33
+ After the dev server starts, seed your database:
34
+
35
+ ```bash
36
+ # Set your admin credentials
37
+ export ADMIN_EMAIL=admin@example.com
38
+ export ADMIN_PASSWORD=your-secure-password
39
+
40
+ npm run seed
41
+ ```
42
+
43
+ ## Commands
44
+
45
+ | Command | Description |
46
+ |---------|-------------|
47
+ | `npm run dev` | Start SST in development mode |
48
+ | `npm run deploy` | Deploy to production |
49
+ | `npm run remove` | Remove all deployed resources |
50
+ | `npm run seed` | Seed the database with initial data |
51
+
52
+ ## Project Structure
53
+
54
+ ```text
55
+ ├── sst.config.ts # SST configuration (Lambda, DynamoDB, API Gateway)
56
+ ├── src/
57
+ │ ├── index.ts # Lambda handler
58
+ │ ├── app.ts # AuthHero app configuration
59
+ │ └── seed.ts # Database seeding script
60
+ ├── copy-assets.js # Script to copy widget assets for Lambda
61
+ └── package.json
62
+ ```
63
+
64
+ ## Architecture
65
+
66
+ - **Lambda Function**: Runs the AuthHero Hono application
67
+ - **DynamoDB**: Single-table design for all AuthHero data
68
+ - **API Gateway**: HTTP API with custom domain support
69
+ - **S3 Bucket**: Serves widget assets (CSS, JS)
70
+
71
+ ## Environment Variables
72
+
73
+ Set these in SST or AWS Systems Manager:
74
+
75
+ | Variable | Description |
76
+ |----------|-------------|
77
+ | `TABLE_NAME` | DynamoDB table name (auto-set by SST) |
78
+ | `WIDGET_URL` | URL to widget assets (auto-set by SST) |
79
+ | `ADMIN_EMAIL` | Admin user email (required for initial seeding) |
80
+ | `ADMIN_PASSWORD` | Admin user password (required for initial seeding) |
81
+
82
+ ## Widget Assets
83
+
84
+ Widget assets are served from an S3 bucket with CloudFront. SST automatically:
85
+ 1. Copies assets from `node_modules/authhero/dist/assets`
86
+ 2. Uploads them to S3
87
+ 3. Creates a CloudFront distribution
88
+ 4. Sets the `WIDGET_URL` environment variable
89
+
90
+ ## Custom Domain
91
+
92
+ To use a custom domain, update `sst.config.ts`:
93
+
94
+ ```typescript
95
+ const api = new sst.aws.ApiGatewayV2("AuthHeroApi", {
96
+ domain: "auth.yourdomain.com",
97
+ });
98
+ ```
99
+
100
+ ## Production Deployment
101
+
102
+ ```bash
103
+ npm run deploy -- --stage production
104
+ ```
105
+
106
+ ## Costs
107
+
108
+ Estimated monthly costs for moderate usage:
109
+
110
+ | Service | Free Tier | After Free Tier |
111
+ |---------|-----------|-----------------|
112
+ | Lambda | 1M requests/month | $0.20/1M requests |
113
+ | DynamoDB | 25 WCU/RCU | ~$0.25/1M requests |
114
+ | API Gateway | 1M requests/month | $1.00/1M requests |
115
+ | S3 + CloudFront | 1GB + 50GB | ~$5/month |
116
+
117
+ ## Troubleshooting
118
+
119
+ ### Lambda timeout
120
+
121
+ Increase timeout in `sst.config.ts`:
122
+
123
+ ```typescript
124
+ new sst.aws.Function("AuthHero", {
125
+ timeout: "30 seconds",
126
+ });
127
+ ```
128
+
129
+ ### DynamoDB throttling
130
+
131
+ Enable auto-scaling or increase provisioned capacity in `sst.config.ts`.
132
+
133
+ ### Widget not loading
134
+
135
+ Ensure the S3 bucket and CloudFront are properly configured. Check browser console for CORS errors.
136
+
137
+ ## Learn More
138
+
139
+ - [SST Documentation](https://sst.dev/docs)
140
+ - [AuthHero Documentation](https://authhero.net/docs)
141
+ - [AWS Lambda](https://docs.aws.amazon.com/lambda/)
142
+ - [DynamoDB](https://docs.aws.amazon.com/dynamodb/)
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Copy AuthHero widget assets for Lambda deployment
5
+ *
6
+ * This script copies widget assets from node_modules to dist/assets
7
+ * so they can be uploaded to S3 and served via CloudFront.
8
+ */
9
+
10
+ import fs from "fs";
11
+ import path from "path";
12
+ import { fileURLToPath } from "url";
13
+
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+
17
+ function copyDirectory(src, dest) {
18
+ if (!fs.existsSync(dest)) {
19
+ fs.mkdirSync(dest, { recursive: true });
20
+ }
21
+
22
+ const entries = fs.readdirSync(src, { withFileTypes: true });
23
+
24
+ for (const entry of entries) {
25
+ const srcPath = path.join(src, entry.name);
26
+ const destPath = path.join(dest, entry.name);
27
+
28
+ if (entry.isDirectory()) {
29
+ copyDirectory(srcPath, destPath);
30
+ } else {
31
+ fs.copyFileSync(srcPath, destPath);
32
+ }
33
+ }
34
+ }
35
+
36
+ // Source and destination paths
37
+ const authHeroAssets = path.join(__dirname, "node_modules/authhero/dist/assets");
38
+ const widgetSource = path.join(
39
+ __dirname,
40
+ "node_modules/@authhero/widget/dist/authhero-widget"
41
+ );
42
+ const targetDir = path.join(__dirname, "dist/assets");
43
+ const widgetTarget = path.join(targetDir, "u/widget");
44
+
45
+ // Copy authhero assets
46
+ if (fs.existsSync(authHeroAssets)) {
47
+ console.log("📦 Copying AuthHero assets...");
48
+ copyDirectory(authHeroAssets, targetDir);
49
+ } else {
50
+ console.log("⚠️ AuthHero assets not found at:", authHeroAssets);
51
+ }
52
+
53
+ // Copy widget from @authhero/widget package
54
+ if (fs.existsSync(widgetSource)) {
55
+ console.log("📦 Copying widget assets...");
56
+ copyDirectory(widgetSource, widgetTarget);
57
+ } else {
58
+ console.log("⚠️ Widget assets not found at:", widgetSource);
59
+ }
60
+
61
+ console.log("✅ Assets copied to dist/assets");
@@ -0,0 +1,77 @@
1
+ import { handle } from "hono/aws-lambda";
2
+ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
3
+ import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
4
+ import createAdapters from "@authhero/aws";
5
+ import createApp from "./app";
6
+ import type { APIGatewayProxyEventV2, Context } from "aws-lambda";
7
+
8
+ // Validate required environment variables
9
+ if (!process.env.TABLE_NAME) {
10
+ throw new Error("TABLE_NAME environment variable is required");
11
+ }
12
+
13
+ // Initialize DynamoDB client outside handler for connection reuse
14
+ const client = new DynamoDBClient({});
15
+ const docClient = DynamoDBDocumentClient.from(client, {
16
+ marshallOptions: {
17
+ removeUndefinedValues: true,
18
+ },
19
+ });
20
+
21
+ // Create adapters - reused across invocations
22
+ const dataAdapter = createAdapters(docClient, {
23
+ tableName: process.env.TABLE_NAME,
24
+ });
25
+
26
+ export async function handler(event: APIGatewayProxyEventV2, context: Context) {
27
+ // Compute issuer from the request
28
+ const host = event.headers.host || event.requestContext.domainName;
29
+ const protocol = event.headers["x-forwarded-proto"] || "https";
30
+ const issuer = `${protocol}://${host}/`;
31
+
32
+ // Get origin for CORS
33
+ const origin = event.headers.origin || "";
34
+
35
+ // Widget URL from environment (set by SST)
36
+ const widgetUrl = process.env.WIDGET_URL || "";
37
+
38
+ // CORS configuration
39
+ // SECURITY: Configure ALLOWED_ORIGINS environment variable in production
40
+ // to restrict origins. Comma-separated list of allowed origins.
41
+ const allowedOrigins = process.env.ALLOWED_ORIGINS
42
+ ? process.env.ALLOWED_ORIGINS.split(",").map((o) => o.trim())
43
+ : [
44
+ // WARNING: These localhost origins are for development only
45
+ // Remove or override via ALLOWED_ORIGINS env var in production
46
+ "http://localhost:5173",
47
+ "https://localhost:3000",
48
+ origin,
49
+ ].filter(Boolean);
50
+
51
+ // Create app instance per request to avoid issuer contamination
52
+ // Lambda containers are reused, so we can't mutate process.env.ISSUER globally
53
+ const appWithIssuer = createApp({
54
+ dataAdapter,
55
+ allowedOrigins,
56
+ widgetUrl,
57
+ });
58
+
59
+ // Set issuer in a request-scoped way via middleware
60
+ appWithIssuer.use("*", async (c, next) => {
61
+ // Store issuer in context for this request
62
+ const originalIssuer = process.env.ISSUER;
63
+ process.env.ISSUER = issuer;
64
+ try {
65
+ await next();
66
+ } finally {
67
+ // Restore original value to prevent contamination
68
+ if (originalIssuer !== undefined) {
69
+ process.env.ISSUER = originalIssuer;
70
+ } else {
71
+ delete process.env.ISSUER;
72
+ }
73
+ }
74
+ });
75
+
76
+ return handle(appWithIssuer)(event, context);
77
+ }
@@ -0,0 +1,5 @@
1
+ export interface AppConfig {
2
+ dataAdapter: any;
3
+ allowedOrigins: string[];
4
+ widgetUrl: string;
5
+ }
@@ -0,0 +1,82 @@
1
+ /// <reference path="./.sst/platform/config.d.ts" />
2
+
3
+ export default $config({
4
+ app(input) {
5
+ return {
6
+ name: "authhero",
7
+ removal: input?.stage === "production" ? "retain" : "remove",
8
+ protect: ["production"].includes(input?.stage),
9
+ home: "aws",
10
+ };
11
+ },
12
+ async run() {
13
+ // ════════════════════════════════════════════════════════════════════════
14
+ // DynamoDB Table - Single-table design for all AuthHero data
15
+ // ════════════════════════════════════════════════════════════════════════
16
+ const table = new sst.aws.Dynamo("AuthHeroTable", {
17
+ fields: {
18
+ pk: "string",
19
+ sk: "string",
20
+ gsi1pk: "string",
21
+ gsi1sk: "string",
22
+ gsi2pk: "string",
23
+ gsi2sk: "string",
24
+ },
25
+ primaryIndex: { hashKey: "pk", rangeKey: "sk" },
26
+ globalIndexes: {
27
+ gsi1: { hashKey: "gsi1pk", rangeKey: "gsi1sk" },
28
+ gsi2: { hashKey: "gsi2pk", rangeKey: "gsi2sk" },
29
+ },
30
+ ttl: "expiresAt",
31
+ });
32
+
33
+ // ════════════════════════════════════════════════════════════════════════
34
+ // S3 Bucket + CloudFront for Widget Assets
35
+ // ════════════════════════════════════════════════════════════════════════
36
+ const assets = new sst.aws.StaticSite("WidgetAssets", {
37
+ path: "dist/assets",
38
+ build: {
39
+ command: "node copy-assets.js",
40
+ output: "dist/assets",
41
+ },
42
+ });
43
+
44
+ // ════════════════════════════════════════════════════════════════════════
45
+ // Lambda Function
46
+ // ════════════════════════════════════════════════════════════════════════
47
+ const api = new sst.aws.ApiGatewayV2("AuthHeroApi");
48
+
49
+ const authFunction = new sst.aws.Function("AuthHeroFunction", {
50
+ handler: "src/index.handler",
51
+ runtime: "nodejs20.x",
52
+ timeout: "30 seconds",
53
+ memory: "512 MB",
54
+ link: [table],
55
+ environment: {
56
+ TABLE_NAME: table.name,
57
+ WIDGET_URL: assets.url,
58
+ },
59
+ nodejs: {
60
+ install: ["@authhero/aws"],
61
+ },
62
+ });
63
+
64
+ api.route("$default", authFunction.arn);
65
+
66
+ // ════════════════════════════════════════════════════════════════════════
67
+ // Optional: Custom Domain
68
+ // ════════════════════════════════════════════════════════════════════════
69
+ // Uncomment and configure to use a custom domain:
70
+ //
71
+ // api.domain = {
72
+ // name: "auth.yourdomain.com",
73
+ // dns: sst.aws.dns(),
74
+ // };
75
+
76
+ return {
77
+ api: api.url,
78
+ assets: assets.url,
79
+ table: table.name,
80
+ };
81
+ },
82
+ });
@@ -0,0 +1,16 @@
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
+ "outDir": "dist",
11
+ "declaration": true,
12
+ "types": ["node"]
13
+ },
14
+ "include": ["src/**/*", "sst.config.ts"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { Command as T } from "commander";
3
- import d from "inquirer";
2
+ import { Command as P } from "commander";
3
+ import u from "inquirer";
4
4
  import s from "fs";
5
5
  import i from "path";
6
- import { spawn as D } from "child_process";
7
- const I = new T(), m = {
6
+ import { spawn as I } from "child_process";
7
+ const N = new P(), p = {
8
8
  local: {
9
9
  name: "Local (SQLite)",
10
10
  description: "Local development setup with SQLite database - great for getting started",
@@ -83,15 +83,51 @@ const I = new T(), m = {
83
83
  }
84
84
  }),
85
85
  seedFile: "seed.ts"
86
+ },
87
+ "aws-sst": {
88
+ name: "AWS SST (Lambda + DynamoDB)",
89
+ description: "Serverless AWS deployment with Lambda, DynamoDB, and SST",
90
+ templateDir: "aws-sst",
91
+ packageJson: (t, e) => ({
92
+ name: t,
93
+ version: "1.0.0",
94
+ type: "module",
95
+ scripts: {
96
+ dev: "sst dev",
97
+ deploy: "sst deploy --stage production",
98
+ remove: "sst remove",
99
+ seed: "npx tsx src/seed.ts",
100
+ "copy-assets": "node copy-assets.js"
101
+ },
102
+ dependencies: {
103
+ "@authhero/aws": "latest",
104
+ "@authhero/widget": "latest",
105
+ "@aws-sdk/client-dynamodb": "^3.0.0",
106
+ "@aws-sdk/lib-dynamodb": "^3.0.0",
107
+ "@hono/swagger-ui": "^0.5.0",
108
+ "@hono/zod-openapi": "^0.19.0",
109
+ authhero: "latest",
110
+ hono: "^4.6.0",
111
+ ...e && { "@authhero/multi-tenancy": "latest" }
112
+ },
113
+ devDependencies: {
114
+ "@types/aws-lambda": "^8.10.0",
115
+ "@types/node": "^20.0.0",
116
+ sst: "^3.0.0",
117
+ tsx: "^4.0.0",
118
+ typescript: "^5.5.0"
119
+ }
120
+ }),
121
+ seedFile: "seed.ts"
86
122
  }
87
123
  };
88
- function x(t, e) {
89
- s.readdirSync(t).forEach((o) => {
90
- const n = i.join(t, o), r = i.join(e, o);
91
- s.lstatSync(n).isDirectory() ? (s.mkdirSync(r, { recursive: !0 }), x(n, r)) : s.copyFileSync(n, r);
124
+ function k(t, e) {
125
+ s.readdirSync(t).forEach((a) => {
126
+ const n = i.join(t, a), o = i.join(e, a);
127
+ s.lstatSync(n).isDirectory() ? (s.mkdirSync(o, { recursive: !0 }), k(n, o)) : s.copyFileSync(n, o);
92
128
  });
93
129
  }
94
- function E(t) {
130
+ function _(t) {
95
131
  return `import { SqliteDialect, Kysely } from "kysely";
96
132
  import Database from "better-sqlite3";
97
133
  import createAdapters from "@authhero/kysely-adapter";
@@ -128,7 +164,7 @@ async function main() {
128
164
  main().catch(console.error);
129
165
  `;
130
166
  }
131
- function j(t) {
167
+ function R(t) {
132
168
  return t ? `import { Context } from "hono";
133
169
  import { swaggerUI } from "@hono/swagger-ui";
134
170
  import { AuthHeroConfig, DataAdapters } from "authhero";
@@ -228,7 +264,7 @@ export default function createApp(config: AuthHeroConfig) {
228
264
  }
229
265
  `;
230
266
  }
231
- function _(t) {
267
+ function M(t) {
232
268
  return `import { D1Dialect } from "kysely-d1";
233
269
  import { Kysely } from "kysely";
234
270
  import createAdapters from "@authhero/kysely-adapter";
@@ -301,7 +337,7 @@ export default {
301
337
  };
302
338
  `;
303
339
  }
304
- function R(t) {
340
+ function j(t) {
305
341
  return t ? `import { Context } from "hono";
306
342
  import { swaggerUI } from "@hono/swagger-ui";
307
343
  import { AuthHeroConfig, DataAdapters } from "authhero";
@@ -377,10 +413,171 @@ export default function createApp(config: AuthHeroConfig) {
377
413
  }
378
414
  `;
379
415
  }
380
- function M(t) {
416
+ function L(t) {
417
+ return t ? `import { Context } from "hono";
418
+ import { swaggerUI } from "@hono/swagger-ui";
419
+ import { AuthHeroConfig, DataAdapters } from "authhero";
420
+ import { initMultiTenant } from "@authhero/multi-tenancy";
421
+
422
+ // Control plane tenant ID - the tenant that manages all other tenants
423
+ const CONTROL_PLANE_TENANT_ID = "control_plane";
424
+
425
+ interface AppConfig extends AuthHeroConfig {
426
+ dataAdapter: DataAdapters;
427
+ widgetUrl: string;
428
+ }
429
+
430
+ export default function createApp(config: AppConfig) {
431
+ // Initialize multi-tenant AuthHero
432
+ const { app } = initMultiTenant({
433
+ ...config,
434
+ controlPlaneTenantId: CONTROL_PLANE_TENANT_ID,
435
+ });
436
+
437
+ app
438
+ .onError((err, ctx) => {
439
+ if (err && typeof err === "object" && "getResponse" in err) {
440
+ return (err as { getResponse: () => Response }).getResponse();
441
+ }
442
+ console.error(err);
443
+ return ctx.text(err instanceof Error ? err.message : "Internal Server Error", 500);
444
+ })
445
+ .get("/", async (ctx: Context) => {
446
+ return ctx.json({
447
+ name: "AuthHero Multi-Tenant Server (AWS)",
448
+ version: "1.0.0",
449
+ status: "running",
450
+ docs: "/docs",
451
+ controlPlaneTenant: CONTROL_PLANE_TENANT_ID,
452
+ });
453
+ })
454
+ .get("/docs", swaggerUI({ url: "/api/v2/spec" }))
455
+ // Redirect widget requests to S3/CloudFront
456
+ .get("/u/widget/*", async (ctx) => {
457
+ const file = ctx.req.path.replace("/u/widget/", "");
458
+ return ctx.redirect(\`\${config.widgetUrl}/u/widget/\${file}\`);
459
+ })
460
+ .get("/u/*", async (ctx) => {
461
+ const file = ctx.req.path.replace("/u/", "");
462
+ return ctx.redirect(\`\${config.widgetUrl}/u/\${file}\`);
463
+ });
464
+
465
+ return app;
466
+ }
467
+ ` : `import { Context } from "hono";
468
+ import { cors } from "hono/cors";
469
+ import { AuthHeroConfig, init, DataAdapters } from "authhero";
470
+ import { swaggerUI } from "@hono/swagger-ui";
471
+
472
+ interface AppConfig extends AuthHeroConfig {
473
+ dataAdapter: DataAdapters;
474
+ widgetUrl: string;
475
+ }
476
+
477
+ export default function createApp(config: AppConfig) {
478
+ const { app } = init(config);
479
+
480
+ // Enable CORS for all origins in development
481
+ app.use("*", cors({
482
+ origin: (origin) => origin || "*",
483
+ allowMethods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
484
+ allowHeaders: ["Content-Type", "Authorization", "Auth0-Client"],
485
+ exposeHeaders: ["Content-Length"],
486
+ credentials: true,
487
+ }));
488
+
489
+ app
490
+ .onError((err, ctx) => {
491
+ if (err && typeof err === "object" && "getResponse" in err) {
492
+ return (err as { getResponse: () => Response }).getResponse();
493
+ }
494
+ console.error(err);
495
+ return ctx.text(err instanceof Error ? err.message : "Internal Server Error", 500);
496
+ })
497
+ .get("/", async (ctx: Context) => {
498
+ return ctx.json({
499
+ name: "AuthHero Server (AWS)",
500
+ status: "running",
501
+ });
502
+ })
503
+ .get("/docs", swaggerUI({ url: "/api/v2/spec" }))
504
+ // Redirect widget requests to S3/CloudFront
505
+ .get("/u/widget/*", async (ctx) => {
506
+ const file = ctx.req.path.replace("/u/widget/", "");
507
+ return ctx.redirect(\`\${config.widgetUrl}/u/widget/\${file}\`);
508
+ })
509
+ .get("/u/*", async (ctx) => {
510
+ const file = ctx.req.path.replace("/u/", "");
511
+ return ctx.redirect(\`\${config.widgetUrl}/u/\${file}\`);
512
+ });
513
+
514
+ return app;
515
+ }
516
+ `;
517
+ }
518
+ function $(t) {
519
+ return `import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
520
+ import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
521
+ import createAdapters from "@authhero/aws";
522
+ import { seed } from "authhero";
523
+
524
+ async function main() {
525
+ const adminEmail = process.argv[2] || process.env.ADMIN_EMAIL;
526
+ const adminPassword = process.argv[3] || process.env.ADMIN_PASSWORD;
527
+ const tableName = process.argv[4] || process.env.TABLE_NAME;
528
+
529
+ if (!adminEmail || !adminPassword) {
530
+ console.error("Usage: npm run seed <email> <password>");
531
+ console.error(" or: ADMIN_EMAIL=... ADMIN_PASSWORD=... npm run seed");
532
+ process.exit(1);
533
+ }
534
+
535
+ if (!tableName) {
536
+ console.error("TABLE_NAME environment variable is required");
537
+ console.error("Run 'sst dev' first to get the table name from outputs");
538
+ process.exit(1);
539
+ }
540
+
541
+ const client = new DynamoDBClient({});
542
+ const docClient = DynamoDBDocumentClient.from(client, {
543
+ marshallOptions: {
544
+ removeUndefinedValues: true,
545
+ },
546
+ });
547
+
548
+ const adapters = createAdapters(docClient, { tableName });
549
+
550
+ await seed(adapters, {
551
+ adminEmail,
552
+ adminPassword,
553
+ tenantId: "${t ? "control_plane" : "main"}",
554
+ tenantName: "${t ? "Control Plane" : "Main"}",
555
+ isControlPlane: ${t},
556
+ });
557
+
558
+ console.log("✅ Database seeded successfully!");
559
+ }
560
+
561
+ main().catch(console.error);
562
+ `;
563
+ }
564
+ function O(t, e) {
565
+ const r = i.join(t, "src");
566
+ s.writeFileSync(
567
+ i.join(r, "app.ts"),
568
+ L(e)
569
+ ), s.writeFileSync(
570
+ i.join(r, "seed.ts"),
571
+ $(e)
572
+ );
573
+ }
574
+ function C(t) {
575
+ console.log("\\n" + "─".repeat(50)), console.log("🔐 AuthHero deployed to AWS!"), console.log("📚 Check SST output for your API URL"), console.log("🌐 Portal available at https://local.authhero.net"), console.log(t ? "🏢 Multi-tenant mode enabled with control_plane tenant" : "🏠 Single-tenant mode with 'main' tenant"), console.log("─".repeat(50) + "\\n");
576
+ }
577
+ function H(t) {
381
578
  const e = i.join(t, ".github", "workflows");
382
579
  s.mkdirSync(e, { recursive: !0 });
383
- const a = `name: Unit tests
580
+ const r = `name: Unit tests
384
581
 
385
582
  on: push
386
583
 
@@ -401,7 +598,7 @@ jobs:
401
598
 
402
599
  - run: npm run type-check
403
600
  - run: npm test
404
- `, o = `name: Deploy to Dev
601
+ `, a = `name: Deploy to Dev
405
602
 
406
603
  on:
407
604
  push:
@@ -466,9 +663,9 @@ jobs:
466
663
  apiToken: \${{ secrets.PROD_CLOUDFLARE_API_TOKEN }}
467
664
  command: deploy --env production
468
665
  `;
469
- s.writeFileSync(i.join(e, "unit-tests.yml"), a), s.writeFileSync(i.join(e, "deploy-dev.yml"), o), s.writeFileSync(i.join(e, "release.yml"), n), console.log("\\n📦 GitHub CI workflows created!");
666
+ s.writeFileSync(i.join(e, "unit-tests.yml"), r), s.writeFileSync(i.join(e, "deploy-dev.yml"), a), s.writeFileSync(i.join(e, "release.yml"), n), console.log("\\n📦 GitHub CI workflows created!");
470
667
  }
471
- function $(t) {
668
+ function U(t) {
472
669
  const e = {
473
670
  branches: ["main"],
474
671
  plugins: [
@@ -481,105 +678,111 @@ function $(t) {
481
678
  i.join(t, ".releaserc.json"),
482
679
  JSON.stringify(e, null, 2)
483
680
  );
484
- const a = i.join(t, "package.json"), o = JSON.parse(s.readFileSync(a, "utf-8"));
485
- o.devDependencies = {
486
- ...o.devDependencies,
681
+ const r = i.join(t, "package.json"), a = JSON.parse(s.readFileSync(r, "utf-8"));
682
+ a.devDependencies = {
683
+ ...a.devDependencies,
487
684
  "semantic-release": "^24.0.0"
488
- }, o.scripts = {
489
- ...o.scripts,
685
+ }, a.scripts = {
686
+ ...a.scripts,
490
687
  test: 'echo "No tests yet"',
491
688
  "type-check": "tsc --noEmit"
492
- }, s.writeFileSync(a, JSON.stringify(o, null, 2));
689
+ }, s.writeFileSync(r, JSON.stringify(a, null, 2));
493
690
  }
494
691
  function v(t, e) {
495
- return new Promise((a, o) => {
496
- const n = D(t, [], {
692
+ return new Promise((r, a) => {
693
+ const n = I(t, [], {
497
694
  cwd: e,
498
695
  shell: !0,
499
696
  stdio: "inherit"
500
697
  });
501
- n.on("close", (r) => {
502
- r === 0 ? a() : o(new Error(`Command failed with exit code ${r}`));
503
- }), n.on("error", o);
698
+ n.on("close", (o) => {
699
+ o === 0 ? r() : a(new Error(`Command failed with exit code ${o}`));
700
+ }), n.on("error", a);
504
701
  });
505
702
  }
506
- function b(t, e, a) {
507
- return new Promise((o, n) => {
508
- const r = D(t, [], {
703
+ function D(t, e, r) {
704
+ return new Promise((a, n) => {
705
+ const o = I(t, [], {
509
706
  cwd: e,
510
707
  shell: !0,
511
708
  stdio: "inherit",
512
- env: { ...process.env, ...a }
709
+ env: { ...process.env, ...r }
513
710
  });
514
- r.on("close", (c) => {
515
- c === 0 ? o() : n(new Error(`Command failed with exit code ${c}`));
516
- }), r.on("error", n);
711
+ o.on("close", (c) => {
712
+ c === 0 ? a() : n(new Error(`Command failed with exit code ${c}`));
713
+ }), o.on("error", n);
517
714
  });
518
715
  }
519
- function O(t, e) {
520
- const a = i.join(t, "src");
716
+ function F(t, e) {
717
+ const r = i.join(t, "src");
521
718
  s.writeFileSync(
522
- i.join(a, "app.ts"),
523
- R(e)
719
+ i.join(r, "app.ts"),
720
+ j(e)
524
721
  ), s.writeFileSync(
525
- i.join(a, "seed.ts"),
526
- _(e)
722
+ i.join(r, "seed.ts"),
723
+ M(e)
527
724
  );
528
725
  }
529
- function k(t) {
726
+ function x(t) {
530
727
  console.log(`
531
728
  ` + "─".repeat(50)), console.log("🔐 AuthHero server running at https://localhost:3000"), console.log("📚 API documentation available at https://localhost:3000/docs"), console.log("🌐 Portal available at https://local.authhero.net"), console.log(t ? "🏢 Multi-tenant mode enabled with control_plane tenant" : "🏠 Single-tenant mode with 'main' tenant"), console.log("─".repeat(50) + `
532
729
  `);
533
730
  }
534
- function C(t) {
731
+ function b(t) {
535
732
  console.log(`
536
733
  ` + "─".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 https://localhost:3000"), console.log("📚 API documentation available at https://localhost:3000/docs"), console.log("🌐 Portal available at https://local.authhero.net"), console.log(t ? "🏢 Multi-tenant mode enabled with control_plane tenant" : "🏠 Single-tenant mode with 'main' tenant"), console.log("─".repeat(50) + `
537
734
  `);
538
735
  }
539
- I.version("1.0.0").description("Create a new AuthHero project").argument("[project-name]", "name of the project").option("-t, --template <type>", "template type: local or cloudflare").option("-e, --email <email>", "admin email address").option("-p, --password <password>", "admin password (min 8 characters)").option(
736
+ N.version("1.0.0").description("Create a new AuthHero project").argument("[project-name]", "name of the project").option("-t, --template <type>", "template type: local or cloudflare").option("-e, --email <email>", "admin email address").option("-p, --password <password>", "admin password (min 8 characters)").option(
540
737
  "--package-manager <pm>",
541
738
  "package manager to use: npm, yarn, pnpm, or bun"
542
739
  ).option("--multi-tenant", "enable multi-tenant mode").option("--skip-install", "skip installing dependencies").option("--skip-migrate", "skip running database migrations").option("--skip-seed", "skip seeding the database").option("--skip-start", "skip starting the development server").option("--github-ci", "include GitHub CI workflows with semantic versioning").option("-y, --yes", "skip all prompts and use defaults/provided options").action(async (t, e) => {
543
- const a = e.yes === !0;
740
+ const r = e.yes === !0;
544
741
  console.log(`
545
742
  🔐 Welcome to AuthHero!
546
743
  `);
547
- let o = t;
548
- o || (a ? (o = "auth-server", console.log(`Using default project name: ${o}`)) : o = (await d.prompt([
744
+ let a = t;
745
+ a || (r ? (a = "auth-server", console.log(`Using default project name: ${a}`)) : a = (await u.prompt([
549
746
  {
550
747
  type: "input",
551
748
  name: "projectName",
552
749
  message: "Project name:",
553
750
  default: "auth-server",
554
- validate: (p) => p !== "" || "Project name cannot be empty"
751
+ validate: (d) => d !== "" || "Project name cannot be empty"
555
752
  }
556
753
  ])).projectName);
557
- const n = i.join(process.cwd(), o);
558
- s.existsSync(n) && (console.error(`❌ Project "${o}" already exists.`), process.exit(1));
559
- let r;
560
- e.template ? (["local", "cloudflare"].includes(e.template) || (console.error(`❌ Invalid template: ${e.template}`), console.error("Valid options: local, cloudflare"), process.exit(1)), r = e.template, console.log(`Using template: ${m[r].name}`)) : r = (await d.prompt([
754
+ const n = i.join(process.cwd(), a);
755
+ s.existsSync(n) && (console.error(`❌ Project "${a}" already exists.`), process.exit(1));
756
+ let o;
757
+ e.template ? (["local", "cloudflare", "aws-sst"].includes(e.template) || (console.error(`❌ Invalid template: ${e.template}`), console.error("Valid options: local, cloudflare, aws-sst"), process.exit(1)), o = e.template, console.log(`Using template: ${p[o].name}`)) : o = (await u.prompt([
561
758
  {
562
759
  type: "list",
563
760
  name: "setupType",
564
761
  message: "Select your setup type:",
565
762
  choices: [
566
763
  {
567
- name: `${m.local.name}
568
- ${m.local.description}`,
764
+ name: `${p.local.name}
765
+ ${p.local.description}`,
569
766
  value: "local",
570
- short: m.local.name
767
+ short: p.local.name
571
768
  },
572
769
  {
573
- name: `${m.cloudflare.name}
574
- ${m.cloudflare.description}`,
770
+ name: `${p.cloudflare.name}
771
+ ${p.cloudflare.description}`,
575
772
  value: "cloudflare",
576
- short: m.cloudflare.name
773
+ short: p.cloudflare.name
774
+ },
775
+ {
776
+ name: `${p["aws-sst"].name}
777
+ ${p["aws-sst"].description}`,
778
+ value: "aws-sst",
779
+ short: p["aws-sst"].name
577
780
  }
578
781
  ]
579
782
  }
580
783
  ])).setupType;
581
784
  let c;
582
- e.multiTenant !== void 0 ? (c = e.multiTenant, console.log(`Multi-tenant mode: ${c ? "enabled" : "disabled"}`)) : a ? c = !1 : c = (await d.prompt([
785
+ e.multiTenant !== void 0 ? (c = e.multiTenant, console.log(`Multi-tenant mode: ${c ? "enabled" : "disabled"}`)) : r ? c = !1 : c = (await u.prompt([
583
786
  {
584
787
  type: "confirm",
585
788
  name: "multiTenant",
@@ -588,56 +791,57 @@ I.version("1.0.0").description("Create a new AuthHero project").argument("[proje
588
791
  default: !1
589
792
  }
590
793
  ])).multiTenant;
591
- const S = m[r];
794
+ const A = p[o];
592
795
  s.mkdirSync(n, { recursive: !0 }), s.writeFileSync(
593
796
  i.join(n, "package.json"),
594
- JSON.stringify(S.packageJson(o, c), null, 2)
797
+ JSON.stringify(A.packageJson(a, c), null, 2)
595
798
  );
596
- const N = S.templateDir, A = i.join(
799
+ const E = A.templateDir, S = i.join(
597
800
  import.meta.url.replace("file://", "").replace("/create-authhero.js", ""),
598
- N
801
+ E
599
802
  );
600
- if (s.existsSync(A) ? x(A, n) : (console.error(`❌ Template directory not found: ${A}`), process.exit(1)), r === "cloudflare" && O(n, c), r === "cloudflare") {
601
- const l = i.join(n, "wrangler.toml"), p = i.join(n, "wrangler.local.toml");
602
- s.existsSync(l) && s.copyFileSync(l, p);
603
- const u = i.join(n, ".dev.vars.example"), g = i.join(n, ".dev.vars");
604
- s.existsSync(u) && s.copyFileSync(u, g), console.log(
803
+ if (s.existsSync(S) ? k(S, n) : (console.error(`❌ Template directory not found: ${S}`), process.exit(1)), o === "cloudflare" && F(n, c), o === "cloudflare") {
804
+ const l = i.join(n, "wrangler.toml"), d = i.join(n, "wrangler.local.toml");
805
+ s.existsSync(l) && s.copyFileSync(l, d);
806
+ const m = i.join(n, ".dev.vars.example"), g = i.join(n, ".dev.vars");
807
+ s.existsSync(m) && s.copyFileSync(m, g), console.log(
605
808
  "📁 Created wrangler.local.toml and .dev.vars for local development"
606
809
  );
607
810
  }
608
811
  let w = !1;
609
- if (r === "cloudflare" && (e.githubCi !== void 0 ? (w = e.githubCi, w && console.log("Including GitHub CI workflows with semantic versioning")) : a || (w = (await d.prompt([
812
+ if (o === "cloudflare" && (e.githubCi !== void 0 ? (w = e.githubCi, w && console.log("Including GitHub CI workflows with semantic versioning")) : r || (w = (await u.prompt([
610
813
  {
611
814
  type: "confirm",
612
815
  name: "includeGithubCi",
613
816
  message: "Would you like to include GitHub CI with semantic versioning?",
614
817
  default: !1
615
818
  }
616
- ])).includeGithubCi), w && (M(n), $(n))), r === "local") {
617
- const l = E(c);
819
+ ])).includeGithubCi), w && (H(n), U(n))), o === "local") {
820
+ const l = _(c);
618
821
  s.writeFileSync(i.join(n, "src/seed.ts"), l);
619
- const p = j(c);
620
- s.writeFileSync(i.join(n, "src/app.ts"), p);
822
+ const d = R(c);
823
+ s.writeFileSync(i.join(n, "src/app.ts"), d);
621
824
  }
622
- const P = c ? "multi-tenant" : "single-tenant";
825
+ o === "aws-sst" && O(n, c);
826
+ const T = c ? "multi-tenant" : "single-tenant";
623
827
  console.log(
624
828
  `
625
- ✅ Project "${o}" has been created with ${S.name} (${P}) setup!
829
+ ✅ Project "${a}" has been created with ${A.name} (${T}) setup!
626
830
  `
627
831
  );
628
- let f;
629
- if (e.skipInstall ? f = !1 : a ? f = !0 : f = (await d.prompt([
832
+ let h;
833
+ if (e.skipInstall ? h = !1 : r ? h = !0 : h = (await u.prompt([
630
834
  {
631
835
  type: "confirm",
632
836
  name: "shouldInstall",
633
837
  message: "Would you like to install dependencies now?",
634
838
  default: !0
635
839
  }
636
- ])).shouldInstall, f) {
840
+ ])).shouldInstall, h) {
637
841
  let l;
638
842
  e.packageManager ? (["npm", "yarn", "pnpm", "bun"].includes(e.packageManager) || (console.error(
639
843
  `❌ Invalid package manager: ${e.packageManager}`
640
- ), console.error("Valid options: npm, yarn, pnpm, bun"), process.exit(1)), l = e.packageManager) : a ? l = "pnpm" : l = (await d.prompt([
844
+ ), console.error("Valid options: npm, yarn, pnpm, bun"), process.exit(1)), l = e.packageManager) : r ? l = "pnpm" : l = (await u.prompt([
641
845
  {
642
846
  type: "list",
643
847
  name: "packageManager",
@@ -654,14 +858,14 @@ I.version("1.0.0").description("Create a new AuthHero project").argument("[proje
654
858
  📦 Installing dependencies with ${l}...
655
859
  `);
656
860
  try {
657
- const p = l === "pnpm" ? "pnpm install --ignore-workspace" : `${l} install`;
658
- if (await v(p, n), r === "local" && (console.log(`
861
+ const d = l === "pnpm" ? "pnpm install --ignore-workspace" : `${l} install`;
862
+ if (await v(d, n), o === "local" && (console.log(`
659
863
  🔧 Building native modules...
660
864
  `), await v("npm rebuild better-sqlite3", n)), console.log(`
661
865
  ✅ Dependencies installed successfully!
662
- `), r === "local" || r === "cloudflare") {
866
+ `), o === "local" || o === "cloudflare") {
663
867
  let g;
664
- if (e.skipMigrate && e.skipSeed ? g = !1 : a ? g = !e.skipMigrate || !e.skipSeed : g = (await d.prompt([
868
+ if (e.skipMigrate && e.skipSeed ? g = !1 : r ? g = !e.skipMigrate || !e.skipSeed : g = (await u.prompt([
665
869
  {
666
870
  type: "confirm",
667
871
  name: "shouldSetup",
@@ -669,11 +873,11 @@ I.version("1.0.0").description("Create a new AuthHero project").argument("[proje
669
873
  default: !0
670
874
  }
671
875
  ])).shouldSetup, g) {
672
- let h;
673
- e.email && e.password ? (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e.email) || (console.error("❌ Invalid email address provided"), process.exit(1)), e.password.length < 8 && (console.error("❌ Password must be at least 8 characters"), process.exit(1)), h = {
876
+ let f;
877
+ e.email && e.password ? (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e.email) || (console.error("❌ Invalid email address provided"), process.exit(1)), e.password.length < 8 && (console.error("❌ Password must be at least 8 characters"), process.exit(1)), f = {
674
878
  username: e.email,
675
879
  password: e.password
676
- }, console.log(`Using admin email: ${e.email}`)) : h = await d.prompt([
880
+ }, console.log(`Using admin email: ${e.email}`)) : f = await u.prompt([
677
881
  {
678
882
  type: "input",
679
883
  name: "username",
@@ -692,49 +896,51 @@ I.version("1.0.0").description("Create a new AuthHero project").argument("[proje
692
896
  🔄 Running migrations...
693
897
  `), await v(`${l} run migrate`, n)), e.skipSeed || (console.log(`
694
898
  🌱 Seeding database...
695
- `), r === "local" ? await b(
899
+ `), o === "local" ? await D(
696
900
  `${l} run seed`,
697
901
  n,
698
902
  {
699
- ADMIN_EMAIL: h.username,
700
- ADMIN_PASSWORD: h.password
903
+ ADMIN_EMAIL: f.username,
904
+ ADMIN_PASSWORD: f.password
701
905
  }
702
- ) : await b(
906
+ ) : await D(
703
907
  `${l} run seed:local`,
704
908
  n,
705
909
  {
706
- ADMIN_EMAIL: h.username,
707
- ADMIN_PASSWORD: h.password
910
+ ADMIN_EMAIL: f.username,
911
+ ADMIN_PASSWORD: f.password
708
912
  }
709
913
  ));
710
914
  }
711
915
  }
712
- let u;
713
- e.skipStart || a ? u = !1 : u = (await d.prompt([
916
+ let m;
917
+ e.skipStart || r ? m = !1 : m = (await u.prompt([
714
918
  {
715
919
  type: "confirm",
716
920
  name: "shouldStart",
717
921
  message: "Would you like to start the development server?",
718
922
  default: !0
719
923
  }
720
- ])).shouldStart, u && (r === "cloudflare" ? k(c) : C(c), console.log(`🚀 Starting development server...
721
- `), await v(`${l} run dev`, n)), a && !u && (console.log(`
924
+ ])).shouldStart, m && (o === "cloudflare" ? x(c) : o === "aws-sst" ? C(c) : b(c), console.log(`🚀 Starting development server...
925
+ `), await v(`${l} run dev`, n)), r && !m && (console.log(`
722
926
  ✅ Setup complete!`), console.log(`
723
- To start the development server:`), console.log(` cd ${o}`), console.log(" npm run dev"), r === "cloudflare" ? k(c) : C(c));
724
- } catch (p) {
927
+ To start the development server:`), console.log(` cd ${a}`), console.log(" npm run dev"), o === "cloudflare" ? x(c) : o === "aws-sst" ? C(c) : b(c));
928
+ } catch (d) {
725
929
  console.error(`
726
- ❌ An error occurred:`, p), process.exit(1);
930
+ ❌ An error occurred:`, d), process.exit(1);
727
931
  }
728
932
  }
729
- f || (console.log("Next steps:"), console.log(` cd ${o}`), r === "local" ? (console.log(" npm install"), console.log(" npm run migrate"), console.log(
933
+ h || (console.log("Next steps:"), console.log(` cd ${a}`), o === "local" ? (console.log(" npm install"), console.log(" npm run migrate"), console.log(
730
934
  " ADMIN_EMAIL=admin@example.com ADMIN_PASSWORD=yourpassword npm run seed"
731
- ), console.log(" npm run dev")) : r === "cloudflare" && (console.log(" npm install"), console.log(
935
+ ), console.log(" npm run dev")) : o === "cloudflare" ? (console.log(" npm install"), console.log(
732
936
  " npm run migrate # or npm run db:migrate:remote for production"
733
937
  ), console.log(
734
938
  " ADMIN_EMAIL=admin@example.com ADMIN_PASSWORD=yourpassword npm run seed"
735
- ), console.log(" npm run dev # or npm run dev:remote for production")), console.log(`
939
+ ), console.log(" npm run dev # or npm run dev:remote for production")) : o === "aws-sst" && (console.log(" npm install"), console.log(" npm run dev # Deploys to AWS in development mode"), console.log(" # After deploy, get TABLE_NAME from output, then:"), console.log(
940
+ " TABLE_NAME=<your-table> ADMIN_EMAIL=admin@example.com ADMIN_PASSWORD=yourpassword npm run seed"
941
+ )), console.log(`
736
942
  Server will be available at: https://localhost:3000`), console.log("Portal available at: https://local.authhero.net"), console.log(`
737
943
  For more information, visit: https://authhero.net/docs
738
944
  `));
739
945
  });
740
- I.parse(process.argv);
946
+ N.parse(process.argv);
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "type": "git",
6
6
  "url": "https://github.com/markusahlstrand/authhero"
7
7
  },
8
- "version": "0.23.0",
8
+ "version": "0.24.0",
9
9
  "type": "module",
10
10
  "main": "dist/create-authhero.js",
11
11
  "bin": {