webspresso 0.0.98 → 0.0.100

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.
Files changed (88) hide show
  1. package/.agents/ADMIN.md +11 -1
  2. package/.agents/AGENTS.md +2 -2
  3. package/.agents/AUTH.md +415 -0
  4. package/.agents/CLI.md +47 -0
  5. package/.agents/ERRORS.md +59 -0
  6. package/.agents/ORM.md +178 -4
  7. package/.agents/PLUGINS.md +113 -8
  8. package/.agents/QUEUE.md +92 -0
  9. package/.agents/ROUTING.md +160 -7
  10. package/.agents/STREAMING.md +35 -0
  11. package/AGENTS.md +2 -0
  12. package/README.md +145 -93
  13. package/bin/commands/add-docker.js +467 -0
  14. package/bin/commands/deploy-check.js +339 -0
  15. package/bin/webspresso.js +4 -0
  16. package/core/auth/index.js +123 -1
  17. package/core/auth/magic-link.js +170 -0
  18. package/core/auth/manager.js +477 -3
  19. package/core/auth/middleware.js +118 -5
  20. package/core/auth/oauth.js +517 -0
  21. package/core/auth/qrcode.js +499 -0
  22. package/core/auth/session-manager.js +255 -0
  23. package/core/auth/sessions.js +351 -0
  24. package/core/auth/tokens.js +1 -0
  25. package/core/auth/totp.js +507 -0
  26. package/core/auth/user-agent.js +161 -0
  27. package/core/orm/eager-loader.js +144 -10
  28. package/core/orm/index.js +10 -1
  29. package/core/orm/model.js +55 -1
  30. package/core/orm/query-builder.js +90 -4
  31. package/core/orm/repository.js +327 -16
  32. package/core/orm/scopes.js +3 -2
  33. package/core/orm/search.js +213 -0
  34. package/core/orm/types.js +6 -1
  35. package/core/queue/cron.js +544 -0
  36. package/core/queue/index.js +8 -1
  37. package/core/queue/manager.js +100 -1
  38. package/core/queue/scheduler.js +415 -0
  39. package/core/sse/index.js +19 -0
  40. package/core/sse/stream.js +430 -0
  41. package/core/ssr/stream.js +62 -2
  42. package/index.d.ts +794 -6
  43. package/index.js +48 -0
  44. package/package.json +7 -5
  45. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +323 -31
  46. package/plugins/admin-panel/client/parts/08-record-list.js +5 -0
  47. package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
  48. package/plugins/admin-panel/field-renderers/file-upload.js +187 -104
  49. package/plugins/admin-panel/field-renderers/index.js +2 -0
  50. package/plugins/admin-panel/modules/admin-users.js +1 -1
  51. package/plugins/api-key/crypto.js +101 -0
  52. package/plugins/api-key/index.js +288 -0
  53. package/plugins/api-key/middleware.js +461 -0
  54. package/plugins/api-key/migration-template.js +48 -0
  55. package/plugins/api-key/model.js +50 -0
  56. package/plugins/api-key/scopes.js +125 -0
  57. package/plugins/audit-log/admin-component.js +487 -70
  58. package/plugins/audit-log/api-handlers.js +114 -0
  59. package/plugins/audit-log/index.js +24 -0
  60. package/plugins/content/client/inline-edit.js +2 -46
  61. package/plugins/cors.js +13 -2
  62. package/plugins/email/auth-bridge.js +66 -0
  63. package/plugins/email/bundled-templates.js +15 -0
  64. package/plugins/email/templates/auth-magic-link.mjml +13 -0
  65. package/plugins/file-manager/admin-component.js +69 -53
  66. package/plugins/file-manager/index.js +25 -8
  67. package/plugins/index.js +8 -0
  68. package/plugins/oauth/index.js +338 -0
  69. package/plugins/queue/index.js +4 -0
  70. package/plugins/webhooks/admin-component.js +608 -0
  71. package/plugins/webhooks/api-handlers.js +237 -0
  72. package/plugins/webhooks/crypto.js +144 -0
  73. package/plugins/webhooks/dispatcher.js +708 -0
  74. package/plugins/webhooks/index.js +263 -0
  75. package/plugins/webhooks/matcher.js +113 -0
  76. package/plugins/webhooks/migration-template.js +75 -0
  77. package/plugins/webhooks/models.js +98 -0
  78. package/plugins/webhooks/sender.js +176 -0
  79. package/src/api/api-loader.js +1 -0
  80. package/src/bundle/index.js +2 -1
  81. package/src/discovery/file-route-parser.js +44 -1
  82. package/src/discovery/scan-routes.js +2 -2
  83. package/src/file-router.js +310 -104
  84. package/src/helpers.js +16 -1
  85. package/src/njk-frontmatter.js +3 -0
  86. package/src/routing/cascading.js +490 -0
  87. package/src/routing/middleware-resolver.js +92 -0
  88. package/src/server.js +6 -0
@@ -0,0 +1,467 @@
1
+ /**
2
+ * Add Docker Command
3
+ * Generate production-ready multi-stage Dockerfile, .dockerignore, and docker-compose.yml
4
+ * @module bin/commands/add-docker
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ /**
13
+ * Normalize Node image version string
14
+ * @param {string} [nodeVer='22-alpine']
15
+ * @returns {string}
16
+ */
17
+ function normalizeNodeVersion(nodeVer = '22-alpine') {
18
+ let v = String(nodeVer).trim();
19
+ if (v.startsWith('node:')) {
20
+ v = v.slice(5);
21
+ }
22
+ if (!v.includes('-')) {
23
+ v = `${v}-alpine`;
24
+ }
25
+ return v;
26
+ }
27
+
28
+ /**
29
+ * Auto-detect database from package.json or knexfile.js
30
+ * @param {string} cwd
31
+ * @param {Object} [pkg]
32
+ * @returns {'postgres'|'mysql'|'sqlite'|'none'}
33
+ */
34
+ function detectDatabase(cwd, pkg = {}) {
35
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
36
+
37
+ if (deps['pg'] || deps['pg-hstore']) return 'postgres';
38
+ if (deps['mysql2'] || deps['mysql']) return 'mysql';
39
+ if (deps['better-sqlite3'] || deps['sqlite3']) return 'sqlite';
40
+
41
+ // Check knexfile.js or webspresso.db.js
42
+ const knexfilePath = path.join(cwd, 'knexfile.js');
43
+ if (fs.existsSync(knexfilePath)) {
44
+ try {
45
+ const content = fs.readFileSync(knexfilePath, 'utf8');
46
+ if (content.includes('postgresql') || content.includes('pg')) return 'postgres';
47
+ if (content.includes('mysql')) return 'mysql';
48
+ if (content.includes('sqlite')) return 'sqlite';
49
+ } catch (_) {}
50
+ }
51
+
52
+ return 'sqlite';
53
+ }
54
+
55
+ /**
56
+ * Generate production-grade multi-stage Dockerfile
57
+ * @param {Object} options
58
+ * @param {string} [options.node='22-alpine']
59
+ * @param {number} [options.port=3000]
60
+ * @param {boolean} [options.hasBuildScript=false]
61
+ * @returns {string}
62
+ */
63
+ function generateDockerfile(options = {}) {
64
+ const nodeImage = normalizeNodeVersion(options.node || '22-alpine');
65
+ const port = options.port || 3000;
66
+ const isAlpine = nodeImage.includes('alpine');
67
+
68
+ return `# ==========================================
69
+ # Webspresso Multi-Stage Production Dockerfile
70
+ # Security-hardened, non-root, minimal image size
71
+ # ==========================================
72
+
73
+ # ------------------------------------------
74
+ # Stage 1: Dependencies (Native builds cache)
75
+ # ------------------------------------------
76
+ FROM node:${nodeImage} AS deps
77
+ ${isAlpine ? 'RUN apk add --no-cache libc6-compat python3 make g++' : 'RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*'}
78
+ WORKDIR /app
79
+
80
+ # Copy dependency manifests
81
+ COPY package*.json ./
82
+ RUN npm ci --omit=dev && npm cache clean --force
83
+
84
+ # ------------------------------------------
85
+ # Stage 2: Builder (Compile assets & app)
86
+ # ------------------------------------------
87
+ FROM node:${nodeImage} AS builder
88
+ WORKDIR /app
89
+
90
+ COPY package*.json ./
91
+ RUN npm ci
92
+ COPY . .
93
+
94
+ # Run build if a build script is defined in package.json
95
+ RUN if grep -q '"build":' package.json; then npm run build; fi
96
+
97
+ # ------------------------------------------
98
+ # Stage 3: Runner (Production Image)
99
+ # ------------------------------------------
100
+ FROM node:${nodeImage} AS runner
101
+ WORKDIR /app
102
+
103
+ ENV NODE_ENV=production
104
+ ENV PORT=${port}
105
+
106
+ # Create unprivileged system group and user
107
+ ${isAlpine
108
+ ? 'RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 webspresso'
109
+ : 'RUN groupadd --system --gid 1001 nodejs && useradd --system --uid 1001 webspresso'}
110
+
111
+ # Copy dependencies and source code
112
+ COPY --from=deps --chown=webspresso:nodejs /app/node_modules ./node_modules
113
+ COPY --chown=webspresso:nodejs . .
114
+
115
+ # Copy compiled public assets from builder if present
116
+ COPY --from=builder --chown=webspresso:nodejs /app/public ./public
117
+
118
+ # Ensure upload and storage directories exist with proper write permissions
119
+ RUN mkdir -p storage/uploads public/uploads data && \\
120
+ chown -R webspresso:nodejs storage public data
121
+
122
+ # Run as non-root user
123
+ USER webspresso
124
+
125
+ EXPOSE ${port}
126
+
127
+ # Container Healthcheck (checks /health or /healthz endpoint)
128
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \\
129
+ CMD wget --no-verbose --tries=1 --spider http://localhost:${port}/health || \\
130
+ wget --no-verbose --tries=1 --spider http://localhost:${port}/healthz || \\
131
+ exit 1
132
+
133
+ # Start the Webspresso application
134
+ CMD ["node", "server.js"]
135
+ `;
136
+ }
137
+
138
+ /**
139
+ * Generate .dockerignore file content
140
+ * @returns {string}
141
+ */
142
+ function generateDockerignore() {
143
+ return `# Dependencies
144
+ node_modules
145
+ npm-debug.log*
146
+ yarn-debug.log*
147
+ yarn-error.log*
148
+ pnpm-debug.log*
149
+
150
+ # Git & IDE
151
+ .git
152
+ .gitignore
153
+ .gitattributes
154
+ .DS_Store
155
+ .idea
156
+ .vscode
157
+ *.suo
158
+ *.ntvs*
159
+ *.njsproj
160
+
161
+ # Environment & Secrets
162
+ .env
163
+ .env.*
164
+ !.env.example
165
+
166
+ # Local SQLite databases
167
+ *.sqlite
168
+ *.sqlite3
169
+ *.db
170
+ *.db-journal
171
+ *.db-wal
172
+ *.db-shm
173
+
174
+ # Tests & Coverage
175
+ tests
176
+ coverage
177
+ .nyc_output
178
+ playwright-report
179
+ test-results
180
+
181
+ # Temporary & Build Cache
182
+ dist/tmp
183
+ .turbo
184
+ .agents
185
+ docs
186
+
187
+ # Uploads directory (keep directory, exclude uploaded user media)
188
+ storage/uploads/*
189
+ !storage/uploads/.gitkeep
190
+ public/uploads/*
191
+ !public/uploads/.gitkeep
192
+ `;
193
+ }
194
+
195
+ /**
196
+ * Generate docker-compose.yml file content
197
+ * @param {Object} options
198
+ * @param {string} options.appName
199
+ * @param {'postgres'|'mysql'|'sqlite'|'none'} options.db
200
+ * @param {boolean} [options.redis=false]
201
+ * @param {number} [options.port=3000]
202
+ * @returns {string}
203
+ */
204
+ function generateDockerCompose(options = {}) {
205
+ const {
206
+ appName = 'webspresso-app',
207
+ db = 'sqlite',
208
+ redis = false,
209
+ port = 3000,
210
+ } = options;
211
+
212
+ let dbService = '';
213
+ let dbVolume = '';
214
+ let dependsOn = [];
215
+ let appEnv = [
216
+ ' - NODE_ENV=production',
217
+ ` - PORT=${port}`,
218
+ ' - SESSION_SECRET=change-me-to-a-random-32-char-string-in-production',
219
+ ];
220
+
221
+ if (db === 'postgres') {
222
+ dependsOn.push(' postgres:\n condition: service_healthy');
223
+ appEnv.push(' - DATABASE_URL=postgres://webspresso:secretpassword@postgres:5432/webspresso');
224
+ dbService = `
225
+ postgres:
226
+ image: postgres:16-alpine
227
+ container_name: ${appName}-postgres
228
+ environment:
229
+ POSTGRES_DB: webspresso
230
+ POSTGRES_USER: webspresso
231
+ POSTGRES_PASSWORD: secretpassword
232
+ ports:
233
+ - "5432:5432"
234
+ volumes:
235
+ - postgres_data:/var/lib/postgresql/data
236
+ healthcheck:
237
+ test: ["CMD-SHELL", "pg_isready -U webspresso -d webspresso"]
238
+ interval: 10s
239
+ timeout: 5s
240
+ retries: 5
241
+ restart: unless-stopped
242
+ `;
243
+ dbVolume = ' postgres_data:\n';
244
+ } else if (db === 'mysql') {
245
+ dependsOn.push(' mysql:\n condition: service_healthy');
246
+ appEnv.push(' - DATABASE_URL=mysql://webspresso:secretpassword@mysql:3306/webspresso');
247
+ dbService = `
248
+ mysql:
249
+ image: mysql:8.0
250
+ container_name: ${appName}-mysql
251
+ environment:
252
+ MYSQL_DATABASE: webspresso
253
+ MYSQL_USER: webspresso
254
+ MYSQL_PASSWORD: secretpassword
255
+ MYSQL_ROOT_PASSWORD: rootsecretpassword
256
+ ports:
257
+ - "3306:3306"
258
+ volumes:
259
+ - mysql_data:/var/lib/mysql
260
+ healthcheck:
261
+ test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
262
+ interval: 10s
263
+ timeout: 5s
264
+ retries: 5
265
+ restart: unless-stopped
266
+ `;
267
+ dbVolume = ' mysql_data:\n';
268
+ } else if (db === 'sqlite') {
269
+ appEnv.push(' - DATABASE_CLIENT=better-sqlite3');
270
+ appEnv.push(' - DATABASE_FILENAME=/app/data/app.sqlite');
271
+ }
272
+
273
+ let redisService = '';
274
+ let redisVolume = '';
275
+ if (redis) {
276
+ dependsOn.push(' redis:\n condition: service_healthy');
277
+ appEnv.push(' - REDIS_URL=redis://redis:6379');
278
+ redisService = `
279
+ redis:
280
+ image: redis:7-alpine
281
+ container_name: ${appName}-redis
282
+ ports:
283
+ - "6379:6379"
284
+ volumes:
285
+ - redis_data:/data
286
+ healthcheck:
287
+ test: ["CMD", "redis-cli", "ping"]
288
+ interval: 10s
289
+ timeout: 5s
290
+ retries: 5
291
+ restart: unless-stopped
292
+ `;
293
+ redisVolume = ' redis_data:\n';
294
+ }
295
+
296
+ let dependsOnBlock = '';
297
+ if (dependsOn.length > 0) {
298
+ dependsOnBlock = ` depends_on:\n${dependsOn.join('\n')}\n`;
299
+ }
300
+
301
+ const appVolumes = [' - ./storage/uploads:/app/storage/uploads'];
302
+ if (db === 'sqlite') {
303
+ appVolumes.push(' - ./data:/app/data');
304
+ }
305
+
306
+ let volumesSection = '';
307
+ if (dbVolume || redisVolume) {
308
+ volumesSection = `volumes:\n${dbVolume}${redisVolume}`;
309
+ }
310
+
311
+ return `services:
312
+ app:
313
+ build:
314
+ context: .
315
+ dockerfile: Dockerfile
316
+ container_name: ${appName}
317
+ ports:
318
+ - "${port}:${port}"
319
+ environment:
320
+ ${appEnv.join('\n')}
321
+ volumes:
322
+ ${appVolumes.join('\n')}
323
+ ${dependsOnBlock} restart: unless-stopped
324
+ ${dbService}${redisService}
325
+ ${volumesSection}`;
326
+ }
327
+
328
+ /**
329
+ * Register command with Commander
330
+ * @param {import('commander').Command} program
331
+ */
332
+ function registerCommand(program) {
333
+ program
334
+ .command('add:docker [docker]')
335
+ .alias('docker')
336
+ .alias('add-docker')
337
+ .description('Generate production-ready multi-stage Dockerfile, .dockerignore, and docker-compose.yml')
338
+ .option('-n, --node <version>', 'Node.js base image version (default: 22-alpine)', '22-alpine')
339
+ .option('-d, --db <type>', 'Database service to configure: postgres, mysql, sqlite, none (auto-detected by default)')
340
+ .option('-r, --redis', 'Include Redis service in docker-compose.yml', false)
341
+ .option('-p, --port <port>', 'Application container port (default: 3000)', '3000')
342
+ .option('-f, --force', 'Overwrite existing Dockerfile and docker-compose.yml without prompting', false)
343
+ .action(async (...actionArgs) => {
344
+ const cmd = actionArgs[actionArgs.length - 1];
345
+ const options = (cmd && typeof cmd.opts === 'function')
346
+ ? cmd.opts()
347
+ : (actionArgs.find((a) => a && typeof a === 'object') || {});
348
+
349
+ const cwd = process.cwd();
350
+ const pkgPath = path.join(cwd, 'package.json');
351
+
352
+ if (!fs.existsSync(pkgPath)) {
353
+ console.error('❌ Not a Webspresso project! package.json was not found in current directory.');
354
+ process.exit(1);
355
+ }
356
+
357
+ let pkg = {};
358
+ try {
359
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
360
+ } catch (err) {
361
+ console.error(`❌ Failed to parse package.json: ${err.message}`);
362
+ process.exit(1);
363
+ }
364
+
365
+ const appName = pkg.name ? pkg.name.replace(/[^a-zA-Z0-9_-]/g, '-') : 'webspresso-app';
366
+ const port = parseInt(options.port || '3000', 10) || 3000;
367
+ const dbType = options.db || detectDatabase(cwd, pkg);
368
+ const isRedis = Boolean(options.redis);
369
+ const nodeVersion = options.node || '22-alpine';
370
+ const force = Boolean(options.force);
371
+
372
+ console.log('\n🐳 Setting up Docker & Container configuration for Webspresso...\n');
373
+
374
+ const dockerfilePath = path.join(cwd, 'Dockerfile');
375
+ const dockerignorePath = path.join(cwd, '.dockerignore');
376
+ const composePath = path.join(cwd, 'docker-compose.yml');
377
+
378
+ // 1. Create .dockerignore
379
+ if (!fs.existsSync(dockerignorePath) || force) {
380
+ fs.writeFileSync(dockerignorePath, generateDockerignore(), 'utf8');
381
+ console.log(' ✅ Created .dockerignore');
382
+ } else {
383
+ console.log(' ℹ️ .dockerignore already exists (use --force to overwrite)');
384
+ }
385
+
386
+ // 2. Create Dockerfile
387
+ if (!fs.existsSync(dockerfilePath) || force) {
388
+ const dockerfileContent = generateDockerfile({
389
+ node: nodeVersion,
390
+ port,
391
+ hasBuildScript: Boolean(pkg.scripts && pkg.scripts.build),
392
+ });
393
+ fs.writeFileSync(dockerfilePath, dockerfileContent, 'utf8');
394
+ console.log(` ✅ Created Dockerfile (multi-stage Node ${nodeVersion}, non-root webspresso user)`);
395
+ } else {
396
+ console.log(' ℹ️ Dockerfile already exists (use --force to overwrite)');
397
+ }
398
+
399
+ // 3. Create docker-compose.yml
400
+ if (!fs.existsSync(composePath) || force) {
401
+ const composeContent = generateDockerCompose({
402
+ appName,
403
+ db: dbType,
404
+ redis: isRedis,
405
+ port,
406
+ });
407
+ fs.writeFileSync(composePath, composeContent, 'utf8');
408
+ console.log(` ✅ Created docker-compose.yml (app: port ${port}, db: ${dbType}${isRedis ? ', redis: true' : ''})`);
409
+ } else {
410
+ console.log(' ℹ️ docker-compose.yml already exists (use --force to overwrite)');
411
+ }
412
+
413
+ // 4. Ensure local data & upload folders exist
414
+ if (dbType === 'sqlite') {
415
+ const dataDir = path.join(cwd, 'data');
416
+ if (!fs.existsSync(dataDir)) {
417
+ fs.mkdirSync(dataDir, { recursive: true });
418
+ console.log(' ✅ Created data/ directory for persistent SQLite storage');
419
+ }
420
+ }
421
+
422
+ const storageDir = path.join(cwd, 'storage', 'uploads');
423
+ if (!fs.existsSync(storageDir)) {
424
+ fs.mkdirSync(storageDir, { recursive: true });
425
+ fs.writeFileSync(path.join(storageDir, '.gitkeep'), '', 'utf8');
426
+ }
427
+
428
+ // 5. Update package.json scripts
429
+ pkg.scripts = pkg.scripts || {};
430
+ let pkgModified = false;
431
+
432
+ const dockerScripts = {
433
+ 'docker:build': `docker build -t ${appName} .`,
434
+ 'docker:run': `docker run -p ${port}:${port} --env-file .env ${appName}`,
435
+ 'docker:up': 'docker compose up -d',
436
+ 'docker:down': 'docker compose down',
437
+ };
438
+
439
+ for (const [key, cmdStr] of Object.entries(dockerScripts)) {
440
+ if (!pkg.scripts[key] || force) {
441
+ pkg.scripts[key] = cmdStr;
442
+ pkgModified = true;
443
+ }
444
+ }
445
+
446
+ if (pkgModified) {
447
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
448
+ console.log(' ✅ Added docker:* helper scripts to package.json');
449
+ }
450
+
451
+ console.log('\n🎉 Docker setup complete!\n');
452
+ console.log('Next steps:');
453
+ console.log(` 1. Build image: npm run docker:build`);
454
+ console.log(` 2. Run container: npm run docker:run`);
455
+ console.log(` 3. Start all stack: npm run docker:up\n`);
456
+ console.log(` Run preflight check: npx webspresso deploy:check\n`);
457
+ });
458
+ }
459
+
460
+ module.exports = {
461
+ registerCommand,
462
+ generateDockerfile,
463
+ generateDockerignore,
464
+ generateDockerCompose,
465
+ detectDatabase,
466
+ normalizeNodeVersion,
467
+ };