webspresso 0.0.104 → 0.0.106
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/.agents/AGENTS.md +21 -2
- package/.agents/ARCHITECTURE.md +23 -0
- package/.agents/CACHE.md +147 -0
- package/.agents/CLI.md +220 -8
- package/.agents/EARLY_HINTS.md +120 -0
- package/.agents/FLAGS.md +118 -0
- package/.agents/HEALTH.md +118 -0
- package/.agents/LIMITS.md +109 -0
- package/.agents/LOGGING.md +93 -0
- package/.agents/METRICS.md +127 -0
- package/.agents/MIGRATION_JOBS.md +95 -0
- package/.agents/ORM.md +155 -1
- package/.agents/PLUGINS.md +38 -3
- package/.agents/QUEUE.md +42 -0
- package/.agents/RATE_LIMIT.md +147 -0
- package/.agents/RESILIENCE.md +235 -0
- package/.agents/SCALE_OUT.md +109 -0
- package/.agents/SECRETS.md +117 -0
- package/AGENTS.md +16 -0
- package/README.md +2 -2
- package/bin/commands/add-aws.js +396 -0
- package/bin/commands/add-ci.js +523 -0
- package/bin/commands/add-cloudflare.js +240 -0
- package/bin/commands/add-cloudrun.js +381 -0
- package/bin/commands/add-docker.js +6 -6
- package/bin/commands/add-migration-job.js +396 -0
- package/bin/commands/assets-sync.js +209 -0
- package/bin/commands/client-generate.js +52 -0
- package/bin/commands/console.js +53 -0
- package/bin/commands/db-migrate.js +47 -2
- package/bin/commands/db-studio.js +84 -0
- package/bin/commands/deploy-check.js +242 -13
- package/bin/commands/dev.js +1 -1
- package/bin/commands/export.js +87 -0
- package/bin/commands/new.js +5 -5
- package/bin/commands/openapi.js +1 -1
- package/bin/commands/secrets-pull.js +208 -0
- package/bin/commands/start.js +1 -1
- package/bin/utils/db.js +7 -1
- package/bin/utils/project.js +2 -2
- package/bin/webspresso.js +22 -0
- package/core/app.js +421 -0
- package/core/auth/authenticate.js +93 -0
- package/core/auth/hash.js +5 -4
- package/core/auth/index.js +34 -4
- package/core/auth/jwt.js +5 -54
- package/core/auth/magic-link.js +4 -5
- package/core/auth/manager.js +53 -10
- package/core/auth/middleware.js +16 -68
- package/core/auth/oauth.js +8 -17
- package/core/auth/redis-session.js +1008 -0
- package/core/auth/sessions.js +156 -0
- package/core/auth/totp.js +2 -1
- package/core/cache/byte-size.js +123 -0
- package/core/cache/cdn.js +296 -0
- package/core/cache/index.js +33 -0
- package/core/cache/lru.js +477 -0
- package/core/cache/tagged-lru.js +354 -0
- package/core/commands/registry.js +6 -20
- package/core/console/commands.js +148 -0
- package/core/console/context.js +260 -0
- package/core/console/eval.js +70 -0
- package/core/console/index.js +22 -0
- package/core/console/repl.js +102 -0
- package/core/early-hints/index.js +267 -0
- package/core/errors/domain.js +49 -0
- package/core/errors/http.js +31 -0
- package/core/errors/index.js +10 -0
- package/core/errors/normalize.js +88 -2
- package/core/events/discovery.js +6 -24
- package/core/events/dispatcher.js +9 -7
- package/core/events/signal.js +7 -0
- package/core/export/discovery.js +191 -0
- package/core/export/exporter.js +277 -0
- package/core/export/index.js +24 -0
- package/core/flags/evaluator.js +252 -0
- package/core/flags/hasher.js +37 -0
- package/core/flags/index.js +21 -0
- package/core/flags/manager.js +263 -0
- package/core/flags/store.js +126 -0
- package/core/health/index.js +41 -0
- package/core/health/middleware.js +213 -0
- package/core/health/normalize.js +140 -0
- package/core/health/probes.js +203 -0
- package/core/http/adapter.js +74 -0
- package/core/http/express-adapter.js +46 -0
- package/core/http/hono-adapter.js +531 -0
- package/core/http/index.js +41 -0
- package/core/http/pipeline.js +485 -0
- package/core/index.js +336 -0
- package/core/limits/index.js +34 -0
- package/core/limits/middleware.js +325 -0
- package/core/limits/normalize.js +222 -0
- package/core/logger/formatters.js +264 -0
- package/core/logger/index.js +260 -0
- package/core/logger/trace.js +137 -0
- package/core/metrics/collectors.js +125 -0
- package/core/metrics/index.js +45 -0
- package/core/metrics/middleware.js +230 -0
- package/core/metrics/registry.js +366 -0
- package/core/orm/batch.js +70 -0
- package/core/orm/cache/fingerprint.js +2 -2
- package/core/orm/cache/memory-provider.js +27 -110
- package/core/orm/d1.js +265 -0
- package/core/orm/index.js +176 -13
- package/core/orm/json-fields.js +22 -8
- package/core/orm/migrate-cloud.js +194 -0
- package/core/orm/model.js +10 -0
- package/core/orm/pool.js +97 -0
- package/core/orm/query-builder.js +72 -9
- package/core/orm/repository.js +318 -19
- package/core/orm/schema-helpers.js +8 -10
- package/core/orm/scopes.js +321 -1
- package/core/orm/transaction.js +10 -2
- package/core/orm/utils.js +4 -86
- package/core/queue/adapters/cloudflare.js +131 -0
- package/core/queue/adapters/database.js +4 -8
- package/core/queue/adapters/pubsub.js +400 -0
- package/core/queue/adapters/sqs.js +419 -0
- package/core/queue/adapters/wait-until.js +77 -0
- package/core/queue/index.js +8 -0
- package/core/queue/manager.js +66 -41
- package/core/rate-limit/index.js +22 -0
- package/core/rate-limit/limiter.js +219 -0
- package/core/rate-limit/stores/memory.js +129 -0
- package/core/rate-limit/stores/redis.js +151 -0
- package/core/rate-limit/utils.js +112 -0
- package/core/resilience/circuit-breaker.js +422 -0
- package/core/resilience/errors.js +63 -0
- package/core/resilience/index.js +53 -0
- package/core/resilience/registry.js +97 -0
- package/core/resilience/retry.js +203 -0
- package/core/schema-cache.js +9 -2
- package/core/secrets/index.js +113 -0
- package/core/secrets/manager.js +308 -0
- package/core/secrets/providers/aws.js +288 -0
- package/core/secrets/providers/env.js +44 -0
- package/core/secrets/providers/gcp.js +307 -0
- package/core/secrets/providers/memory.js +76 -0
- package/core/shutdown/http-adapter.js +4 -1
- package/core/shutdown/shutdown-manager.js +129 -19
- package/core/stream/index.js +19 -0
- package/core/stream/text-stream.js +326 -0
- package/core/studio/index.js +43 -0
- package/core/studio/inspector.js +258 -0
- package/core/studio/server.js +325 -0
- package/core/studio/ui.js +681 -0
- package/core/utils/array.js +12 -0
- package/core/utils/base64url.js +8 -0
- package/core/utils/crypto.js +118 -0
- package/core/utils/ensure-leading-slash.js +6 -0
- package/core/utils/escape-regexp.js +7 -0
- package/core/utils/fetch.js +6 -5
- package/core/utils/fs.js +144 -0
- package/core/utils/hmac.js +6 -0
- package/core/utils/json.js +98 -0
- package/core/utils/object.js +106 -0
- package/core/utils/random.js +9 -0
- package/core/utils/regex.js +77 -0
- package/core/utils/sha256.js +6 -0
- package/core/utils/string.js +87 -0
- package/core/utils/strip-trailing-slash.js +6 -0
- package/core/utils/time.js +96 -0
- package/core/utils/timeout.js +348 -0
- package/core/utils/url.js +106 -0
- package/core/utils/walk.js +7 -0
- package/index.d.ts +1549 -2
- package/index.js +272 -2
- package/package.json +3 -2
- package/plugins/admin-panel/core/api-extensions.js +2 -5
- package/plugins/admin-panel/index.js +13 -6
- package/plugins/admin-panel/modules/menu.js +1 -1
- package/plugins/api-key/crypto.js +3 -3
- package/plugins/api-key/index.js +3 -2
- package/plugins/api-key/scopes.js +5 -7
- package/plugins/audit-log/api-handlers.js +6 -15
- package/plugins/audit-log/index.js +3 -1
- package/plugins/audit-log/parse.js +3 -3
- package/plugins/basic-auth/index.js +3 -2
- package/plugins/cors.js +3 -2
- package/plugins/csrf/index.js +9 -12
- package/plugins/csv/index.js +8 -5
- package/plugins/dashboard/app.js +2 -2
- package/plugins/dashboard/index.js +5 -5
- package/plugins/data-exchange/record-selection.js +2 -5
- package/plugins/email/api-handlers.js +5 -18
- package/plugins/email/auth-bridge.js +3 -2
- package/plugins/email/index.js +11 -9
- package/plugins/health-check.js +2 -1
- package/plugins/index.js +5 -0
- package/plugins/lazy.js +5 -6
- package/plugins/mcp/discovery.js +18 -17
- package/plugins/polar/src/api.js +2 -1
- package/plugins/polar/src/webhooks.js +5 -6
- package/plugins/queue/index.js +18 -2
- package/plugins/rate-limit/index.js +3 -1
- package/plugins/redirect/index.js +16 -9
- package/plugins/rls/index.js +170 -0
- package/plugins/seo-checker/index.js +2 -2
- package/plugins/site-analytics/index.js +3 -1
- package/plugins/site-analytics/tracking.js +2 -2
- package/plugins/sitemap.js +3 -2
- package/plugins/swagger.js +12 -5
- package/plugins/upload/gcs-provider.js +148 -0
- package/plugins/upload/index.js +136 -77
- package/plugins/upload/orphan-pruner.js +4 -3
- package/plugins/upload/s3-provider.js +257 -0
- package/plugins/webhooks/crypto.js +6 -11
- package/plugins/webhooks/dispatcher.js +8 -8
- package/plugins/webhooks/index.js +33 -23
- package/plugins/webhooks/matcher.js +7 -11
- package/plugins/webhooks/sender.js +8 -11
- package/plugins/xlsx/index.js +7 -5
- package/src/api/api-loader.js +12 -95
- package/src/api/define-api.js +1 -0
- package/src/app-context.js +41 -5
- package/src/client-runtime/constants.js +10 -0
- package/src/client-runtime/mount.js +2 -3
- package/src/dev/banner.js +6 -2
- package/src/dev/client-gen.js +458 -0
- package/src/discovery/scan-routes.js +5 -2
- package/src/file-router.js +3 -3
- package/src/helpers.js +212 -24
- package/src/modules/module-discovery.js +1 -9
- package/src/pages/page-loader.js +13 -154
- package/src/plugin-manager.js +99 -18
- package/src/server.js +335 -133
- package/src/services/discovery.js +19 -40
- package/src/services/executor.js +95 -25
- package/src/services/memoize.js +3 -25
- package/src/services/registry.js +15 -1
- package/src/services/validator.js +38 -20
- package/src/template/precompile.js +8 -19
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Stateless Scale-Out & Database Connection Pool Management Guide
|
|
2
|
+
|
|
3
|
+
When deploying Webspresso to serverless or auto-scaling cloud environments (Google Cloud Run, AWS App Runner, ECS Fargate, AWS Lambda, Kubernetes, or PM2 Cluster), multiple container instances run concurrently and scale up/down dynamically.
|
|
4
|
+
|
|
5
|
+
To ensure seamless operation without lost user sessions or database exhaustion, Webspresso provides native zero-dependency **Stateless Scale-Out** primitives:
|
|
6
|
+
1. **Distributed Redis Session Store** (`createRedisSessionStore`)
|
|
7
|
+
2. **Distributed Redis Session & Device Tracker** (`createRedisSessionTracker`)
|
|
8
|
+
3. **Built-in Lightweight RESP Redis Client** (`createMiniRedisClient`) & `InMemoryRedisClient`
|
|
9
|
+
4. **Serverless Database Connection Pool Optimizer** (`normalizeServerlessPoolConfig`, `isServerlessEnvironment`)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 🎯 Key Capabilities
|
|
14
|
+
|
|
15
|
+
### 1. Distributed Redis Session Store (`createRedisSessionStore`)
|
|
16
|
+
- Drop-in replacement for `express-session` MemoryStore.
|
|
17
|
+
- Stores session data in Redis with automatic TTL expiration synced to session cookies (`maxAge`).
|
|
18
|
+
- Implements the complete `express-session` Store contract: `get`, `set`, `destroy`, `touch`, `all`, `length`, `clear`.
|
|
19
|
+
- Zero external dependencies: works with any standard client (`ioredis`, `redis` v4/v3, or the built-in `createMiniRedisClient`).
|
|
20
|
+
- **Auto-Detection**: Setting `REDIS_URL=redis://...` or `SESSION_STORE=redis` in your environment automatically wires the Redis session store without manual boilerplate!
|
|
21
|
+
|
|
22
|
+
### 2. Distributed Redis Device & Session Tracker (`createRedisSessionTracker`)
|
|
23
|
+
- Replaces or supplements Knex database session tracking across ephemeral instances.
|
|
24
|
+
- Maintains user active sessions (`user:sessions:<userId>`) and device metadata (`sess:device:<sessionId>`).
|
|
25
|
+
- Supports remote single-session revocation, "revoke other sessions", and "force logout from all devices".
|
|
26
|
+
|
|
27
|
+
### 3. Serverless DB Connection Pool Optimizer (`normalizeServerlessPoolConfig`)
|
|
28
|
+
- Prevents database connection exhaustion on managed databases (Google Cloud SQL, AWS RDS / Aurora) when container replicas scale from 1 to 50+.
|
|
29
|
+
- Automatically detects serverless platforms (`K_SERVICE` on Cloud Run, `AWS_EXECUTION_ENV` / `AWS_LAMBDA_FUNCTION_NAME` on AWS):
|
|
30
|
+
- Sets `pool.min: 0` (so idle containers release database sockets).
|
|
31
|
+
- Clamps `pool.max: 5` (or `2` on Lambda) per container instance.
|
|
32
|
+
- Reduces `acquireTimeoutMillis` from Knex's 60s default to 10s.
|
|
33
|
+
- Sets safe `idleTimeoutMillis: 30000`, `createTimeoutMillis: 5000`.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 🚀 Quick Start & Usage
|
|
38
|
+
|
|
39
|
+
### 1. Zero-Configuration Cloud Run / ECS Deployment
|
|
40
|
+
|
|
41
|
+
Simply provide `REDIS_URL` in your container environment variables:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# In Cloud Run or AWS ECS Task Definition
|
|
45
|
+
REDIS_URL=redis://default:secret@memorystore-or-elasticache:6379
|
|
46
|
+
SESSION_SECRET=a_very_secure_random_production_secret_32_chars
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Webspresso automatically detects `REDIS_URL` and activates both `createRedisSessionStore` and `normalizeServerlessPoolConfig`.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
### 2. Explicit Auth Session Configuration
|
|
54
|
+
|
|
55
|
+
```javascript
|
|
56
|
+
// server.js
|
|
57
|
+
const { createApp, createRedisSessionStore } = require('webspresso');
|
|
58
|
+
|
|
59
|
+
const app = createApp({
|
|
60
|
+
auth: {
|
|
61
|
+
session: {
|
|
62
|
+
secret: process.env.SESSION_SECRET,
|
|
63
|
+
// Pass custom store or let Webspresso auto-detect REDIS_URL
|
|
64
|
+
store: createRedisSessionStore({
|
|
65
|
+
url: process.env.REDIS_URL,
|
|
66
|
+
prefix: 'myapp:sess:',
|
|
67
|
+
ttl: 86400, // 24 hours
|
|
68
|
+
}),
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
server: {
|
|
72
|
+
compression: true,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
### 3. Explicit Database Connection Pool Configuration
|
|
80
|
+
|
|
81
|
+
```javascript
|
|
82
|
+
// webspresso.db.js
|
|
83
|
+
module.exports = {
|
|
84
|
+
client: 'pg',
|
|
85
|
+
connection: process.env.DATABASE_URL,
|
|
86
|
+
// Enable serverless pool optimization explicitly
|
|
87
|
+
serverlessPool: true,
|
|
88
|
+
pool: {
|
|
89
|
+
// Override defaults if using a pooler like PgBouncer or RDS Proxy
|
|
90
|
+
min: 0,
|
|
91
|
+
max: 5,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
### 4. Production Preflight Audit (`webspresso deploy:check`)
|
|
99
|
+
|
|
100
|
+
Run the preflight check before deploying to verify that your session store and connection pool are properly configured for scale-out:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npx webspresso deploy:check
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The preflight check will:
|
|
107
|
+
- ✅ Verify distributed session store (Redis) is active in production instead of ephemeral MemoryStore.
|
|
108
|
+
- ⚠️ Flag database connection pools with excessively large `max` (> 10) on autoscaling serverless containers.
|
|
109
|
+
- ⚠️ Check that `pool.min` is set to 0 for serverless scale-to-zero workloads.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Cloud Secrets Management Guide (`core/secrets` & `secrets:pull`)
|
|
2
|
+
|
|
3
|
+
Webspresso features an enterprise, zero-dependency Cloud Secrets Management system (`core/secrets`) designed to eliminate plain-text `.env` files and secret leakage in containerized and serverless environments (Google Cloud Run, AWS App Runner, ECS Fargate, Kubernetes).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🎯 Key Capabilities
|
|
8
|
+
|
|
9
|
+
1. **Zero External Dependencies**:
|
|
10
|
+
- Interacts directly with cloud vault REST APIs using native Node.js standard libraries (`https`, `crypto`).
|
|
11
|
+
- **Google Cloud Secret Manager**: Automatically acquires OAuth2 tokens from the local GCP Metadata Server in Cloud Run/GKE, exchanges RSA-SHA256 signed JWTs from `GOOGLE_APPLICATION_CREDENTIALS`, or uses static tokens.
|
|
12
|
+
- **AWS Secrets Manager & SSM Parameter Store**: Employs an embedded, standards-compliant AWS Signature Version 4 (SigV4) signer with HMAC-SHA256.
|
|
13
|
+
2. **In-Memory Caching with TTL & Invalidation**:
|
|
14
|
+
- Caches secret values in RAM with a configurable TTL (default: 5 minutes) to avoid redundant HTTP roundtrips.
|
|
15
|
+
- Supports single secret cache busting (`secrets.invalidate(name)`) and full cache clearing (`secrets.clearCache()`) for automated secret rotation.
|
|
16
|
+
3. **JSON Secret Bundle Unpacking & Dot-Notation Subkeys**:
|
|
17
|
+
- Access specific fields within JSON bundles using dot-notation:
|
|
18
|
+
`await secrets.get('prod/app/config.DATABASE_URL')`
|
|
19
|
+
- Unpack entire JSON bundles into `process.env`:
|
|
20
|
+
`await secrets.loadBundle('prod/app/config')`
|
|
21
|
+
4. **Local Developer Synchronization (`webspresso secrets:pull`)**:
|
|
22
|
+
- Pulls secrets from Google Cloud or AWS into a local `.env.local` file for local development without committing credentials to Git.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 🚀 Usage in Application Bootstrap
|
|
27
|
+
|
|
28
|
+
### 1. Hydrating Environment at Application Boot
|
|
29
|
+
|
|
30
|
+
Call `loadSecrets()` before initializing your database or server:
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
// server.js
|
|
34
|
+
const { createApp, loadSecrets } = require('webspresso');
|
|
35
|
+
|
|
36
|
+
async function start() {
|
|
37
|
+
// Load secrets from Google Cloud Secret Manager or AWS before bootstrapping
|
|
38
|
+
await loadSecrets({
|
|
39
|
+
provider: 'auto', // 'cloudrun' | 'gcp' | 'aws' | 'auto'
|
|
40
|
+
secrets: {
|
|
41
|
+
SESSION_SECRET: 'session-secret:latest',
|
|
42
|
+
DATABASE_URL: 'rds-database-url:latest',
|
|
43
|
+
},
|
|
44
|
+
// Or unpack a single JSON bundle holding all config:
|
|
45
|
+
// bundle: 'prod/webspresso/app-secrets',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const app = createApp({
|
|
49
|
+
server: { port: process.env.PORT || 3000 },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await app.listen();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
start();
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 🛠️ Standalone `SecretManager` Usage
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
const { createSecretManager } = require('webspresso/core/secrets');
|
|
64
|
+
|
|
65
|
+
const secrets = createSecretManager({
|
|
66
|
+
provider: 'auto', // Auto-detects GCP or AWS based on environment markers
|
|
67
|
+
cacheTtl: '10m', // 10 minutes cache TTL
|
|
68
|
+
projectId: 'my-gcp-project', // optional for GCP
|
|
69
|
+
region: 'us-east-1', // optional for AWS
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// 1. Fetch a single secret
|
|
73
|
+
const apiKey = await secrets.get('STRIPE_SECRET_KEY');
|
|
74
|
+
|
|
75
|
+
// 2. Fetch with fallback or enforce required
|
|
76
|
+
const optionalToken = await secrets.get('SLACK_WEBHOOK', { fallback: null });
|
|
77
|
+
const mustExist = await secrets.get('APP_KEY', { required: true });
|
|
78
|
+
|
|
79
|
+
// 3. Dot-notation subkey access inside JSON secret bundles
|
|
80
|
+
const dbPassword = await secrets.get('prod/database.password');
|
|
81
|
+
|
|
82
|
+
// 4. Invalidate cache on rotation
|
|
83
|
+
secrets.invalidate('STRIPE_SECRET_KEY');
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 💻 CLI Synchronization Tool (`secrets:pull`)
|
|
89
|
+
|
|
90
|
+
Pulls cloud secrets into `.env.local` for local development:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Auto-detect cloud platform and pull expected secrets into .env.local
|
|
94
|
+
npx webspresso secrets:pull
|
|
95
|
+
|
|
96
|
+
# Explicitly pull from Google Cloud Run / Secret Manager
|
|
97
|
+
npx webspresso secrets:pull --platform cloudrun -s SESSION_SECRET DATABASE_URL
|
|
98
|
+
|
|
99
|
+
# Explicitly pull from AWS Secrets Manager
|
|
100
|
+
npx webspresso secrets:pull --platform aws -s SESSION_SECRET DATABASE_URL
|
|
101
|
+
|
|
102
|
+
# Unpack a JSON bundle to stdout (dry run)
|
|
103
|
+
npx webspresso secrets:pull --bundle prod/app/config --dry-run
|
|
104
|
+
|
|
105
|
+
# Output as JSON
|
|
106
|
+
npx webspresso secrets:pull -s API_KEY DB_PASS --format json -o secrets.json
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 🔒 Security Best Practices
|
|
112
|
+
|
|
113
|
+
1. **Never commit `.env` or `.env.local`**: Ensure `.env.local` is listed in `.gitignore` and `.gcloudignore`.
|
|
114
|
+
2. **Minimal IAM Permissions**:
|
|
115
|
+
- GCP: Grant the Cloud Run service account `roles/secretmanager.secretAccessor`.
|
|
116
|
+
- AWS: Grant the ECS Task role `secretsmanager:GetSecretValue` and `ssm:GetParameter`.
|
|
117
|
+
3. **Use Short TTL for Rotating Secrets**: If secrets rotate frequently (e.g. daily), configure `cacheTtl: '15m'` or invalidate on rotation webhook triggers.
|
package/AGENTS.md
CHANGED
|
@@ -18,6 +18,13 @@ When tasked with a feature or bugfix, **immediately consult the corresponding to
|
|
|
18
18
|
| **Authentication (Session / JWT / Guard)** | [`.agents/AUTH.md`](.agents/AUTH.md) | `core/auth/` |
|
|
19
19
|
| **Background Jobs & Worker Queue** | [`.agents/QUEUE.md`](.agents/QUEUE.md) | `jobs/` |
|
|
20
20
|
| **Decoupled Events & Signals** | [`.agents/EVENTS.md`](.agents/EVENTS.md) | `core/events/`, `listeners/` |
|
|
21
|
+
| **Fault Tolerance / Circuit Breaker / Retry** | [`.agents/RESILIENCE.md`](.agents/RESILIENCE.md) | `core/resilience/` |
|
|
22
|
+
| **Stateless Scale-Out (Redis Sessions & DB Pool)** | [`.agents/SCALE_OUT.md`](.agents/SCALE_OUT.md) | `core/auth/redis-session.js`, `core/orm/pool.js` |
|
|
23
|
+
| **Cloud Secrets (GCP & AWS Secret Manager)** | [`.agents/SECRETS.md`](.agents/SECRETS.md) | `core/secrets/`, `bin/commands/secrets-pull.js` |
|
|
24
|
+
| **Cloud Metrics & Distributed Tracing** | [`.agents/METRICS.md`](.agents/METRICS.md) | `core/metrics/`, `/metrics` |
|
|
25
|
+
| **Cloud Migration Jobs (Zero-Downtime DB Deploy)** | [`.agents/MIGRATION_JOBS.md`](.agents/MIGRATION_JOBS.md) | `bin/commands/add-migration-job.js`, `core/orm/migrate-cloud.js` |
|
|
26
|
+
| **Rate Limiting Engine (Zero-Dependency & Redis)** | [`.agents/RATE_LIMIT.md`](.agents/RATE_LIMIT.md) | `core/rate-limit/` |
|
|
27
|
+
| **Feature Flags & Canary Splitter (`core/flags`)** | [`.agents/FLAGS.md`](.agents/FLAGS.md) | `core/flags/` |
|
|
21
28
|
| **API Signatures & Types Verification** | [`index.d.ts`](index.d.ts) | **Canonical Ground Truth** |
|
|
22
29
|
|
|
23
30
|
---
|
|
@@ -57,6 +64,15 @@ Every coding agent working on Webspresso **MUST** follow this 4-step execution d
|
|
|
57
64
|
- **Framework Exceptions & Central Error Boundary**: Built-in exception hierarchy (`WebspressoError`, `HttpError`, `ValidationError`, `SecurityError`, `RequestAbortedError`, `ConfigurationError`, `PluginError`, `RouteNotFoundError`) and custom handler hook (`app.setErrorHandler`). Async route handlers automatically propagate rejected promises. Production responses mask 500 stack traces and error details.
|
|
58
65
|
- **Graceful Shutdown & Lifecycle Management**: Configured via `createApp({ server: { shutdown: { enabled: true, mode: 'graceful'|'force', timeout: 10000 } } })`. Socket connection tracking, request draining with `Connection: close`, and reverse-order plugin disposer cleanup.
|
|
59
66
|
- **HTTP Response Compression**: Zero-dependency streaming zlib compression configured via `createApp({ server: { compression: true } })` with Brotli (`br`), Gzip (`gzip`), Deflate (`deflate`), threshold buffering (1 KB), and route-level opt-out (`res.compress(false)`).
|
|
67
|
+
- **HTTP 103 Early Hints & Critical Asset Preloading**: Zero-dependency RFC 8288 / RFC 8297 Early Hints dispatcher configured via `createApp({ earlyHints: true })` or per-page `definePage({ earlyHints })`. Emits informational 103 status code with route CSS/JS preloads before async `load()` finishes to minimize TTFB, FCP and LCP, with safe fallback and `res.earlyHints(links)` / `ctx.earlyHints(links)` helpers.
|
|
68
|
+
- **Configurable Payload Size Limits & DoS Protection**: Zero-dependency body limit enforcement configured via `createApp({ limits: { json: '1mb', urlencoded: '1mb', raw: false, text: false, parameterLimit: 1000, captureRawBody: false } })` or shorthand `limits: '5mb'`, with automatic HTTP 413 `PayloadTooLargeError` and HTTP 400 `BadRequestError` mapping, route overrides (`limits.routes`), streaming bypass (`limits.exclude`), and raw body capture on `req.rawBody` for webhook signatures.
|
|
69
|
+
- **Native Cloud & Kubernetes Health & Readiness Probes**: Zero-dependency RFC-compliant liveness (`/healthz`), readiness (`/readyz`), and combined diagnostic (`/health`) endpoints configured via `createApp({ healthCheck: true })`. Includes non-blocking event loop lag monitoring (`perf_hooks.monitorEventLoopDelay`), database/queue connectivity tests, and automatic HTTP 503 draining integration with `ShutdownManager` when pods enter graceful termination.
|
|
70
|
+
- **High-Performance In-Memory LRU & Tagged Cache Engine**: Zero-dependency memory-bounded caching (`core/cache`) configured via `createApp({ cache: { maxEntries: 1000, maxMemoryMb: 50, defaultTtl: '15m' } })` or shorthand `cache: true|false`. Provides O(1) LRU eviction, byte budget constraints preventing OOM, leak-free tag invalidation (`TaggedLruCache`), stampede protection (`getOrSet`), and powers ORM query caching and Zod schema caching.
|
|
71
|
+
- **Enterprise Resilience & Fault Tolerance (Circuit Breaker & Retry Engine)**: Zero-dependency resilience system (`core/resilience`) featuring a three-state `CircuitBreaker` (`CLOSED`, `OPEN`, `HALF_OPEN`), rolling window failure rate tracking, per-call execution timeouts, graceful fallbacks, and an exponential backoff retry engine (`retry`, `withRetry`) with randomized jitter and transient error detection (`ECONNRESET`, 429, 5xx). Seamlessly integrated into the Services Layer (`defineService({ circuitBreaker, retry })`) and exposed via `app.breakers`, `app.getBreaker()`, `req.breaker()`, and `req.retry()`.
|
|
72
|
+
- **Stateless Scale-Out & Database Connection Pool Management**: Zero-dependency Redis session store (`createRedisSessionStore`), distributed device tracking (`createRedisSessionTracker`), lightweight RESP Redis client, serverless DB connection pool optimizer (`normalizeServerlessPoolConfig`), and scale-out audits in `webspresso deploy:check`. Automatically activates distributed sessions when `REDIS_URL` or `SESSION_STORE=redis` is configured.
|
|
73
|
+
- **Enterprise Cloud Secrets Management**: Zero-dependency integration with Google Cloud Secret Manager & AWS Secrets Manager/SSM Parameter Store (`core/secrets`), in-memory caching with TTL (`cacheTtl: '5m'`), JSON secret bundle unpacking, dot-notation subkeys (`app.DB_PASSWORD`), bootstrap environment hydration (`loadSecrets()`), and local sync CLI (`webspresso secrets:pull`).
|
|
74
|
+
- **Cloud Metrics & Distributed Tracing**: Zero-dependency Prometheus exposition format (`/metrics`), JSON telemetry (`/metrics.json`), HTTP latency histograms, Node.js event loop/memory/pool/cache collectors, and W3C `traceparent` & `x-trace-id` propagation (`core/metrics`) configured via `createApp({ metrics: true })`.
|
|
75
|
+
- **Cloud Migration Jobs & Zero-Downtime Database Deploy**: Pre-deploy Cloud Run Jobs, AWS ECS Fargate one-off tasks, and Kubernetes Helm hooks (`add:migration-job`), safe retry connections, and automatic stale lock healing (`core/orm/migrate-cloud`, `webspresso db:migrate --safe`).
|
|
60
76
|
- **Custom Error & 404 Pages**: Configured via `createApp({ errorPages: { notFound, serverError, timeout } })`. Supports template paths (e.g. `notFound: '404.njk'`) or custom handler functions, passing `{ fsy, locale, isDev, url, method }` to templates.
|
|
61
77
|
- **Cold Start & High-Performance Optimizations**:
|
|
62
78
|
- Unified single-pass route scanner (`discoverAppRoutes`).
|
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ cd my-app
|
|
|
22
22
|
npm run dev
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
Open **`http://localhost:
|
|
25
|
+
Open **`http://localhost:7331`** in your browser.
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
@@ -147,7 +147,7 @@ const { app } = createApp({
|
|
|
147
147
|
},
|
|
148
148
|
});
|
|
149
149
|
|
|
150
|
-
const PORT = process.env.PORT ||
|
|
150
|
+
const PORT = process.env.PORT || 7331;
|
|
151
151
|
app.listen(PORT, () => {
|
|
152
152
|
console.log(`Server running at http://localhost:${PORT}`);
|
|
153
153
|
});
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add AWS Command
|
|
3
|
+
* Generate production-ready AWS configuration (apprunner.yaml, ecs-task-definition.json, deploy-aws.sh)
|
|
4
|
+
* Supports AWS App Runner and AWS ECS (Fargate) container deployments
|
|
5
|
+
* @module bin/commands/add-aws
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { generateDockerfile, generateDockerignore } = require('./add-docker');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Normalize CPU value to ECS Fargate units (256, 512, 1024, 2048, 4096)
|
|
16
|
+
* @param {string|number} cpu
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
function normalizeEcsCpu(cpu) {
|
|
20
|
+
if (!cpu) return '512';
|
|
21
|
+
const str = String(cpu).trim().toLowerCase();
|
|
22
|
+
if (str === '0.25' || str === '0.25 vcpu') return '256';
|
|
23
|
+
if (str === '0.5' || str === '0.5 vcpu') return '512';
|
|
24
|
+
if (str === '1' || str === '1 vcpu') return '1024';
|
|
25
|
+
if (str === '2' || str === '2 vcpu') return '2048';
|
|
26
|
+
if (str === '4' || str === '4 vcpu') return '4096';
|
|
27
|
+
return str.replace(/[^0-9]/g, '') || '512';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Normalize Memory value to ECS Fargate MiB (512, 1024, 2048, etc.)
|
|
32
|
+
* @param {string|number} memory
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
function normalizeEcsMemory(memory) {
|
|
36
|
+
if (!memory) return '1024';
|
|
37
|
+
const str = String(memory).trim().toLowerCase();
|
|
38
|
+
if (str.endsWith('gb') || str.endsWith('g')) {
|
|
39
|
+
const num = parseFloat(str);
|
|
40
|
+
return String(Math.round(num * 1024));
|
|
41
|
+
}
|
|
42
|
+
if (str.endsWith('mib') || str.endsWith('mb') || str.endsWith('mi')) {
|
|
43
|
+
return str.replace(/[^0-9]/g, '') || '1024';
|
|
44
|
+
}
|
|
45
|
+
return str.replace(/[^0-9]/g, '') || '1024';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Generate apprunner.yaml for AWS App Runner
|
|
50
|
+
* @param {Object} options
|
|
51
|
+
* @param {string} options.appName
|
|
52
|
+
* @param {number} [options.port=7331]
|
|
53
|
+
* @param {string} [options.nodeVersion='nodejs20']
|
|
54
|
+
* @returns {string}
|
|
55
|
+
*/
|
|
56
|
+
function generateAppRunnerYaml(options = {}) {
|
|
57
|
+
const {
|
|
58
|
+
port = 7331,
|
|
59
|
+
nodeVersion = 'nodejs20',
|
|
60
|
+
} = options;
|
|
61
|
+
|
|
62
|
+
return `# ==============================================================================
|
|
63
|
+
# AWS App Runner Configuration for Webspresso
|
|
64
|
+
# Fully-managed container & code runtime with auto-scaling and zero-ops TLS
|
|
65
|
+
# Docs: https://docs.aws.amazon.com/apprunner/latest/dg/config-file.html
|
|
66
|
+
# ==============================================================================
|
|
67
|
+
|
|
68
|
+
version: 1.0
|
|
69
|
+
runtime: ${nodeVersion}
|
|
70
|
+
|
|
71
|
+
build:
|
|
72
|
+
commands:
|
|
73
|
+
build:
|
|
74
|
+
- npm ci --omit=dev
|
|
75
|
+
- npm run build --if-present
|
|
76
|
+
env:
|
|
77
|
+
- name: NODE_ENV
|
|
78
|
+
value: production
|
|
79
|
+
|
|
80
|
+
run:
|
|
81
|
+
command: npm start
|
|
82
|
+
network:
|
|
83
|
+
port: "${port}"
|
|
84
|
+
env: PORT
|
|
85
|
+
env:
|
|
86
|
+
- name: NODE_ENV
|
|
87
|
+
value: production
|
|
88
|
+
- name: PORT
|
|
89
|
+
value: "${port}"
|
|
90
|
+
`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Generate AWS ECS Task Definition JSON for AWS Fargate
|
|
95
|
+
* @param {Object} options
|
|
96
|
+
* @param {string} options.appName
|
|
97
|
+
* @param {string} [options.region='us-east-1']
|
|
98
|
+
* @param {string|number} [options.cpu='512']
|
|
99
|
+
* @param {string|number} [options.memory='1024']
|
|
100
|
+
* @param {number} [options.port=7331]
|
|
101
|
+
* @returns {string}
|
|
102
|
+
*/
|
|
103
|
+
function generateEcsTaskDefinition(options = {}) {
|
|
104
|
+
const {
|
|
105
|
+
appName = 'webspresso-app',
|
|
106
|
+
region = 'us-east-1',
|
|
107
|
+
cpu = '512',
|
|
108
|
+
memory = '1024',
|
|
109
|
+
port = 7331,
|
|
110
|
+
} = options;
|
|
111
|
+
|
|
112
|
+
const normalizedCpu = normalizeEcsCpu(cpu);
|
|
113
|
+
const normalizedMemory = normalizeEcsMemory(memory);
|
|
114
|
+
|
|
115
|
+
const taskDef = {
|
|
116
|
+
family: appName,
|
|
117
|
+
networkMode: 'awsvpc',
|
|
118
|
+
requiresCompatibilities: ['FARGATE'],
|
|
119
|
+
cpu: normalizedCpu,
|
|
120
|
+
memory: normalizedMemory,
|
|
121
|
+
executionRoleArn: 'arn:aws:iam::YOUR_AWS_ACCOUNT_ID:role/ecsTaskExecutionRole',
|
|
122
|
+
taskRoleArn: 'arn:aws:iam::YOUR_AWS_ACCOUNT_ID:role/ecsTaskRole',
|
|
123
|
+
containerDefinitions: [
|
|
124
|
+
{
|
|
125
|
+
name: appName,
|
|
126
|
+
image: `YOUR_AWS_ACCOUNT_ID.dkr.ecr.${region}.amazonaws.com/${appName}:latest`,
|
|
127
|
+
essential: true,
|
|
128
|
+
portMappings: [
|
|
129
|
+
{
|
|
130
|
+
containerPort: port,
|
|
131
|
+
hostPort: port,
|
|
132
|
+
protocol: 'tcp',
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
environment: [
|
|
136
|
+
{ name: 'NODE_ENV', value: 'production' },
|
|
137
|
+
{ name: 'PORT', value: String(port) },
|
|
138
|
+
],
|
|
139
|
+
healthCheck: {
|
|
140
|
+
command: [
|
|
141
|
+
'CMD-SHELL',
|
|
142
|
+
`node -e 'require("http").get("http://localhost:${port}/healthz", (r) => process.exit(r.statusCode === 200 ? 0 : 1))' || exit 1`,
|
|
143
|
+
],
|
|
144
|
+
interval: 30,
|
|
145
|
+
timeout: 5,
|
|
146
|
+
retries: 3,
|
|
147
|
+
startPeriod: 15,
|
|
148
|
+
},
|
|
149
|
+
logConfiguration: {
|
|
150
|
+
logDriver: 'awslogs',
|
|
151
|
+
options: {
|
|
152
|
+
'awslogs-group': `/ecs/${appName}`,
|
|
153
|
+
'awslogs-region': region,
|
|
154
|
+
'awslogs-stream-prefix': 'ecs',
|
|
155
|
+
'awslogs-create-group': 'true',
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return JSON.stringify(taskDef, null, 2) + '\n';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Generate deploy-aws.sh script for automated deployment to ECR, App Runner, or ECS
|
|
167
|
+
* @param {Object} options
|
|
168
|
+
* @param {string} options.appName
|
|
169
|
+
* @param {string} [options.region='us-east-1']
|
|
170
|
+
* @param {number} [options.port=7331]
|
|
171
|
+
* @returns {string}
|
|
172
|
+
*/
|
|
173
|
+
function generateAwsDeployScript(options = {}) {
|
|
174
|
+
const {
|
|
175
|
+
appName = 'webspresso-app',
|
|
176
|
+
region = 'us-east-1',
|
|
177
|
+
port = 7331,
|
|
178
|
+
} = options;
|
|
179
|
+
|
|
180
|
+
return `#!/usr/bin/env bash
|
|
181
|
+
set -euo pipefail
|
|
182
|
+
|
|
183
|
+
# ==============================================================================
|
|
184
|
+
# Webspresso AWS Deployment Automation Script
|
|
185
|
+
# Builds hardened Docker image, pushes to Amazon ECR, and updates App Runner / ECS
|
|
186
|
+
# Usage: ./deploy-aws.sh [APP_NAME] [REGION]
|
|
187
|
+
# ==============================================================================
|
|
188
|
+
|
|
189
|
+
APP_NAME="\${1:-${appName}}"
|
|
190
|
+
REGION="\${2:-\${AWS_REGION:-${region}}}"
|
|
191
|
+
PORT="\${PORT:-${port}}"
|
|
192
|
+
|
|
193
|
+
echo "☁️ Webspresso AWS Deployment Target: '\$APP_NAME' in '\$REGION'"
|
|
194
|
+
|
|
195
|
+
# 1. Verify AWS CLI
|
|
196
|
+
if ! command -v aws &> /dev/null; then
|
|
197
|
+
echo "❌ Error: 'aws' CLI is not installed or not in PATH."
|
|
198
|
+
echo "Please install AWS CLI: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html"
|
|
199
|
+
exit 1
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
# 2. Verify Docker
|
|
203
|
+
if ! command -v docker &> /dev/null; then
|
|
204
|
+
echo "❌ Error: 'docker' is not installed or not running."
|
|
205
|
+
exit 1
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
# 3. Retrieve AWS Account ID
|
|
209
|
+
AWS_ACCOUNT_ID="\$(aws sts get-caller-identity --query Account --output text 2>/dev/null || true)"
|
|
210
|
+
if [ -z "\$AWS_ACCOUNT_ID" ]; then
|
|
211
|
+
echo "❌ Error: Could not determine AWS Account ID. Are your AWS credentials configured?"
|
|
212
|
+
echo "Run: aws configure"
|
|
213
|
+
exit 1
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
ECR_URI="\$AWS_ACCOUNT_ID.dkr.ecr.\$REGION.amazonaws.com/\$APP_NAME"
|
|
217
|
+
IMAGE_TAG="\$(git rev-parse --short HEAD 2>/dev/null || echo 'latest')"
|
|
218
|
+
|
|
219
|
+
echo "🔑 Authenticated as AWS Account: \$AWS_ACCOUNT_ID"
|
|
220
|
+
echo "📦 Target ECR Repository: \$ECR_URI"
|
|
221
|
+
|
|
222
|
+
# 4. Ensure ECR Repository exists
|
|
223
|
+
aws ecr describe-repositories --repository-names "\$APP_NAME" --region "\$REGION" &>/dev/null || {
|
|
224
|
+
echo "📁 Creating ECR repository '\$APP_NAME'..."
|
|
225
|
+
aws ecr create-repository \\
|
|
226
|
+
--repository-name "\$APP_NAME" \\
|
|
227
|
+
--image-scanning-configuration scanOnPush=true \\
|
|
228
|
+
--region "\$REGION" >/dev/null
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
# 5. Authenticate Docker with Amazon ECR
|
|
232
|
+
echo "🔐 Logging in to Amazon ECR..."
|
|
233
|
+
aws ecr get-login-password --region "\$REGION" | docker login --username AWS --password-stdin "\$AWS_ACCOUNT_ID.dkr.ecr.\$REGION.amazonaws.com"
|
|
234
|
+
|
|
235
|
+
# 6. Build production multi-stage Docker image
|
|
236
|
+
echo "🔨 Building Docker image..."
|
|
237
|
+
docker build -t "\$APP_NAME:\$IMAGE_TAG" -t "\$ECR_URI:\$IMAGE_TAG" -t "\$ECR_URI:latest" .
|
|
238
|
+
|
|
239
|
+
# 7. Push to ECR
|
|
240
|
+
echo "🚀 Pushing image to Amazon ECR..."
|
|
241
|
+
docker push "\$ECR_URI:\$IMAGE_TAG"
|
|
242
|
+
docker push "\$ECR_URI:latest"
|
|
243
|
+
|
|
244
|
+
echo ""
|
|
245
|
+
echo "✅ Image successfully built and pushed to Amazon ECR:"
|
|
246
|
+
echo " \$ECR_URI:\$IMAGE_TAG"
|
|
247
|
+
echo " \$ECR_URI:latest"
|
|
248
|
+
echo ""
|
|
249
|
+
echo "Next steps to complete deployment:"
|
|
250
|
+
echo " Option A (AWS App Runner):"
|
|
251
|
+
echo " aws apprunner create-service --service-name \$APP_NAME \\"
|
|
252
|
+
echo " --source-configuration ImageRepository='{ImageIdentifier=\"'\$ECR_URI:latest'\",ImageRepositoryType=\"ECR\",ImageConfiguration={Port=\"'\$PORT'\"}}'"
|
|
253
|
+
echo ""
|
|
254
|
+
echo " Option B (AWS ECS Fargate):"
|
|
255
|
+
echo " aws ecs register-task-definition --cli-input-json file://ecs-task-definition.json"
|
|
256
|
+
echo " aws ecs update-service --cluster \$APP_NAME-cluster --service \$APP_NAME-service --force-new-deployment"
|
|
257
|
+
echo ""
|
|
258
|
+
`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Register command with Commander
|
|
263
|
+
* @param {import('commander').Command} program
|
|
264
|
+
*/
|
|
265
|
+
function registerCommand(program) {
|
|
266
|
+
program
|
|
267
|
+
.command('add:aws')
|
|
268
|
+
.alias('aws')
|
|
269
|
+
.alias('add-aws')
|
|
270
|
+
.description('Generate production-ready AWS deployment configuration (App Runner & ECS Fargate)')
|
|
271
|
+
.option('-r, --region <region>', 'AWS region', 'us-east-1')
|
|
272
|
+
.option('--port <port>', 'Container port (Webspresso default is 7331)', (v) => parseInt(v, 10), 7331)
|
|
273
|
+
.option('--cpu <cpu>', 'CPU allocation for ECS/App Runner (e.g. 512, 1024 or 1 vCPU)', '512')
|
|
274
|
+
.option('--memory <memory>', 'Memory allocation for ECS/App Runner (e.g. 1024, 1GB)', '1024')
|
|
275
|
+
.option('-f, --force', 'Overwrite existing files without prompting', false)
|
|
276
|
+
.action(async (...actionArgs) => {
|
|
277
|
+
const cmd = actionArgs[actionArgs.length - 1];
|
|
278
|
+
const options = (cmd && typeof cmd.opts === 'function')
|
|
279
|
+
? cmd.opts()
|
|
280
|
+
: (actionArgs.find((a) => a && typeof a === 'object') || {});
|
|
281
|
+
|
|
282
|
+
const cwd = process.cwd();
|
|
283
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
284
|
+
|
|
285
|
+
if (!fs.existsSync(pkgPath)) {
|
|
286
|
+
console.error('❌ Not a Webspresso project! package.json was not found in current directory.');
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let pkg = {};
|
|
291
|
+
try {
|
|
292
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
293
|
+
} catch (err) {
|
|
294
|
+
console.error(`❌ Failed to parse package.json: ${err.message}`);
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const appName = pkg.name ? pkg.name.replace(/[^a-zA-Z0-9_-]/g, '-') : 'webspresso-app';
|
|
299
|
+
const force = Boolean(options.force);
|
|
300
|
+
const region = options.region || 'us-east-1';
|
|
301
|
+
const port = options.port || 7331;
|
|
302
|
+
const cpu = options.cpu || '512';
|
|
303
|
+
const memory = options.memory || '1024';
|
|
304
|
+
|
|
305
|
+
const appRunnerPath = path.join(cwd, 'apprunner.yaml');
|
|
306
|
+
const ecsTaskDefPath = path.join(cwd, 'ecs-task-definition.json');
|
|
307
|
+
const scriptPath = path.join(cwd, 'deploy-aws.sh');
|
|
308
|
+
const dockerfilePath = path.join(cwd, 'Dockerfile');
|
|
309
|
+
const dockerignorePath = path.join(cwd, '.dockerignore');
|
|
310
|
+
|
|
311
|
+
console.log(`\n☁️ Scaffolding AWS deployment configuration for "${appName}"...`);
|
|
312
|
+
|
|
313
|
+
// 1. Ensure Dockerfile exists (needed by ECR & container deploy)
|
|
314
|
+
if (!fs.existsSync(dockerfilePath)) {
|
|
315
|
+
const dockerfileContent = generateDockerfile({ port, node: '22-alpine' });
|
|
316
|
+
fs.writeFileSync(dockerfilePath, dockerfileContent, 'utf8');
|
|
317
|
+
console.log(`✅ Created Dockerfile (configured for port ${port})`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (!fs.existsSync(dockerignorePath)) {
|
|
321
|
+
const dockerignoreContent = generateDockerignore();
|
|
322
|
+
fs.writeFileSync(dockerignorePath, dockerignoreContent, 'utf8');
|
|
323
|
+
console.log(`✅ Created .dockerignore`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// 2. apprunner.yaml
|
|
327
|
+
if (fs.existsSync(appRunnerPath) && !force) {
|
|
328
|
+
console.log(`⚠️ ${path.basename(appRunnerPath)} already exists. Skipping (use --force to overwrite).`);
|
|
329
|
+
} else {
|
|
330
|
+
const appRunnerContent = generateAppRunnerYaml({
|
|
331
|
+
appName,
|
|
332
|
+
port,
|
|
333
|
+
});
|
|
334
|
+
fs.writeFileSync(appRunnerPath, appRunnerContent, 'utf8');
|
|
335
|
+
console.log(`✅ Created ${path.basename(appRunnerPath)} (AWS App Runner)`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// 3. ecs-task-definition.json
|
|
339
|
+
if (fs.existsSync(ecsTaskDefPath) && !force) {
|
|
340
|
+
console.log(`⚠️ ${path.basename(ecsTaskDefPath)} already exists. Skipping (use --force to overwrite).`);
|
|
341
|
+
} else {
|
|
342
|
+
const taskDefContent = generateEcsTaskDefinition({
|
|
343
|
+
appName,
|
|
344
|
+
region,
|
|
345
|
+
cpu,
|
|
346
|
+
memory,
|
|
347
|
+
port,
|
|
348
|
+
});
|
|
349
|
+
fs.writeFileSync(ecsTaskDefPath, taskDefContent, 'utf8');
|
|
350
|
+
console.log(`✅ Created ${path.basename(ecsTaskDefPath)} (AWS ECS Fargate Task Definition)`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// 4. deploy-aws.sh
|
|
354
|
+
if (fs.existsSync(scriptPath) && !force) {
|
|
355
|
+
console.log(`⚠️ ${path.basename(scriptPath)} already exists. Skipping (use --force to overwrite).`);
|
|
356
|
+
} else {
|
|
357
|
+
const scriptContent = generateAwsDeployScript({
|
|
358
|
+
appName,
|
|
359
|
+
region,
|
|
360
|
+
port,
|
|
361
|
+
});
|
|
362
|
+
fs.writeFileSync(scriptPath, scriptContent, { encoding: 'utf8', mode: 0o755 });
|
|
363
|
+
console.log(`✅ Created ${path.basename(scriptPath)} (executable)`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 5. Update package.json scripts
|
|
367
|
+
let modifiedPkg = false;
|
|
368
|
+
if (!pkg.scripts) pkg.scripts = {};
|
|
369
|
+
if (!pkg.scripts['aws:deploy']) {
|
|
370
|
+
pkg.scripts['aws:deploy'] = './deploy-aws.sh';
|
|
371
|
+
modifiedPkg = true;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (modifiedPkg) {
|
|
375
|
+
try {
|
|
376
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
|
377
|
+
console.log(`✅ Added "aws:deploy" script to package.json`);
|
|
378
|
+
} catch (_) {}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
console.log('\n🎉 AWS deployment configuration complete!');
|
|
382
|
+
console.log('\nNext steps:');
|
|
383
|
+
console.log(' 1. Configure AWS CLI: `aws configure`');
|
|
384
|
+
console.log(' 2. Review ecs-task-definition.json and apprunner.yaml');
|
|
385
|
+
console.log(' 3. Deploy container via ECR: `npm run aws:deploy`\n');
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
module.exports = {
|
|
390
|
+
registerCommand,
|
|
391
|
+
generateAppRunnerYaml,
|
|
392
|
+
generateEcsTaskDefinition,
|
|
393
|
+
generateAwsDeployScript,
|
|
394
|
+
normalizeEcsCpu,
|
|
395
|
+
normalizeEcsMemory,
|
|
396
|
+
};
|