zyket 1.2.17 → 1.2.19

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 (69) hide show
  1. package/SECURITY.md +40 -0
  2. package/bin/cli.js +282 -202
  3. package/index.js +4 -3
  4. package/package.json +5 -1
  5. package/src/extensions/bullboard/index.js +14 -3
  6. package/src/extensions/interactive-storage/index.js +25 -9
  7. package/src/extensions/interactive-storage/routes/delete-folder.js +13 -1
  8. package/src/extensions/interactive-storage/routes/delete.js +14 -3
  9. package/src/extensions/interactive-storage/routes/download.js +22 -3
  10. package/src/extensions/interactive-storage/routes/info.js +13 -0
  11. package/src/services/auth/index.js +66 -42
  12. package/src/services/express/Express.js +32 -15
  13. package/src/services/express/RequireAdminMiddleware.js +14 -0
  14. package/src/services/express/RequireAuthMiddleware.js +46 -0
  15. package/src/services/express/index.js +7 -5
  16. package/src/services/socketio/AuthGuard.js +33 -0
  17. package/src/services/socketio/SocketIO.js +3 -1
  18. package/src/services/socketio/index.js +2 -1
  19. package/src/services/template-manager/index.js +1 -0
  20. package/src/templates/api-rest/.env.example +24 -0
  21. package/src/templates/api-rest/README.md +50 -0
  22. package/src/templates/api-rest/index.js +18 -0
  23. package/src/templates/api-rest/src/models/Task.js +18 -0
  24. package/src/templates/api-rest/src/routes/tasks/[id].js +42 -0
  25. package/src/templates/api-rest/src/routes/tasks/index.js +26 -0
  26. package/src/templates/api-rest/src/services/auth/auth.js +9 -0
  27. package/src/templates/api-rest/src/services/auth/index.js +23 -0
  28. package/src/templates/realtime-chat/.env.example +26 -0
  29. package/src/templates/realtime-chat/README.md +38 -0
  30. package/src/templates/realtime-chat/frontend/.env.example +3 -0
  31. package/src/templates/realtime-chat/frontend/index.html +12 -0
  32. package/src/templates/realtime-chat/frontend/main.jsx +18 -0
  33. package/src/templates/realtime-chat/frontend/src/hooks/useAuth.jsx +27 -0
  34. package/src/templates/realtime-chat/frontend/src/hooks/useChatSocket.jsx +29 -0
  35. package/src/templates/realtime-chat/frontend/src/middlewares/LoggedMiddleware.jsx +12 -0
  36. package/src/templates/realtime-chat/frontend/src/middlewares/NotLoggedMiddleware.jsx +12 -0
  37. package/src/templates/realtime-chat/frontend/src/store/storeAuth.jsx +11 -0
  38. package/src/templates/realtime-chat/frontend/src/views/AuthView.jsx +70 -0
  39. package/src/templates/realtime-chat/frontend/src/views/ChatView.jsx +69 -0
  40. package/src/templates/realtime-chat/frontend/styles.css +1 -0
  41. package/src/templates/realtime-chat/frontend/vite.config.js +7 -0
  42. package/src/templates/realtime-chat/index.js +14 -0
  43. package/src/templates/realtime-chat/src/guards/auth.js +3 -0
  44. package/src/templates/realtime-chat/src/handlers/connection.js +23 -0
  45. package/src/templates/realtime-chat/src/handlers/message.js +29 -0
  46. package/src/templates/realtime-chat/src/services/auth/auth.js +8 -0
  47. package/src/templates/realtime-chat/src/services/auth/index.js +19 -0
  48. package/src/templates/saas-multitenant/.env.example +22 -0
  49. package/src/templates/saas-multitenant/README.md +71 -0
  50. package/src/templates/saas-multitenant/frontend/.env.example +3 -0
  51. package/src/templates/saas-multitenant/frontend/index.html +12 -0
  52. package/src/templates/saas-multitenant/frontend/main.jsx +18 -0
  53. package/src/templates/saas-multitenant/frontend/src/hooks/useAuth.jsx +27 -0
  54. package/src/templates/saas-multitenant/frontend/src/hooks/useProjects.jsx +41 -0
  55. package/src/templates/saas-multitenant/frontend/src/middlewares/LoggedMiddleware.jsx +12 -0
  56. package/src/templates/saas-multitenant/frontend/src/middlewares/NotLoggedMiddleware.jsx +12 -0
  57. package/src/templates/saas-multitenant/frontend/src/store/storeAuth.jsx +13 -0
  58. package/src/templates/saas-multitenant/frontend/src/views/AuthView.jsx +70 -0
  59. package/src/templates/saas-multitenant/frontend/src/views/DashboardView.jsx +131 -0
  60. package/src/templates/saas-multitenant/frontend/styles.css +1 -0
  61. package/src/templates/saas-multitenant/frontend/vite.config.js +7 -0
  62. package/src/templates/saas-multitenant/index.js +14 -0
  63. package/src/templates/saas-multitenant/src/middlewares/RequireOrganization.js +22 -0
  64. package/src/templates/saas-multitenant/src/models/Project.js +17 -0
  65. package/src/templates/saas-multitenant/src/routes/admin/stats.js +15 -0
  66. package/src/templates/saas-multitenant/src/routes/projects/index.js +34 -0
  67. package/src/templates/saas-multitenant/src/services/auth/auth.js +8 -0
  68. package/src/templates/saas-multitenant/src/services/auth/index.js +43 -0
  69. package/src/utils/EnvManager.js +23 -0
package/SECURITY.md ADDED
@@ -0,0 +1,40 @@
1
+ # Seguridad — Zyket
2
+
3
+ Auditoría de seguridad del framework y estado de las correcciones.
4
+ Leyenda: `[x]` hecho · `[ ]` pendiente · 🔴 crítico · 🟠 alto · 🟡 medio · 🔵 bajo
5
+
6
+ ---
7
+
8
+ ## ✅ Resuelto
9
+
10
+ - [x] 🔴 **`.env` ignorado en git.** Añadido `.env` / `*.sqlite` al `.gitignore` para evitar fuga de `AUTH_SECRET`, claves S3 y credenciales de BD.
11
+ - [x] 🔴 **`AUTH_SECRET` aleatorio + fail-closed.** `#addAuthEnvVariables` genera ahora un secreto con `crypto.randomBytes(32)` por proyecto y lo inyecta en `process.env` para el primer arranque. `#requireAuthSecret()` aborta el boot si el secreto falta o es uno de los valores estáticos conocidos. → [src/services/auth/index.js](src/services/auth/index.js)
12
+ - [x] 🔴 **Autenticación en Storage realmente aplicada.** Antes los `middlewares` del constructor se guardaban pero no se usaban; ahora se aplican a todas las rutas y métodos (auth antes de multer en `upload`). La autenticación se inyecta vía `new InteractiveStorageExtension({ middlewares: [authMiddleware] })`. → [src/extensions/interactive-storage/index.js](src/extensions/interactive-storage/index.js)
13
+ - [x] 🔴 **BullBoard fail-closed.** Si no hay `BULLBOARD_ADMIN_PASSWORD` ni `middlewares`, el panel **no se monta** (antes quedaba público). Soporta `middlewares` por constructor. → [src/extensions/bullboard/index.js](src/extensions/bullboard/index.js)
14
+ - [x] 🟠 **Inyección de cabecera en descarga.** `Content-Disposition` ahora sanea el nombre (basename + ASCII filtrado + `filename*=UTF-8''`) evitando breakout de comillas/CRLF. → [src/extensions/interactive-storage/routes/download.js](src/extensions/interactive-storage/routes/download.js)
15
+ - [x] 🟠 **Path traversal en `normalizePath`.** Se eliminan segmentos `..` / `.` además de normalizar slashes. → [src/extensions/interactive-storage/index.js](src/extensions/interactive-storage/index.js#L150)
16
+ - [x] 🟡 **Límite en borrado masivo.** `delete` ahora rechaza lotes mayores a `maxDeleteBatch` (por defecto 100, configurable en el constructor de la extensión) → evita borrado masivo / agotamiento de recursos en una sola petición. → [src/extensions/interactive-storage/routes/delete.js](src/extensions/interactive-storage/routes/delete.js)
17
+ - [x] 🟡 **`requireEmailVerification` personalizable.** Nuevo getter `requireEmailVerification` (por defecto `false`, manteniendo el comportamiento previo) sobreescribible al extender `AuthService`. → [src/services/auth/index.js](src/services/auth/index.js#L71)
18
+ - [x] 🟡 **Límite en borrado de carpeta.** `delete-folder` rechaza prefijos con más de `maxDeleteBatch` archivos (mismo tope configurable que `delete`). → [src/extensions/interactive-storage/routes/delete-folder.js](src/extensions/interactive-storage/routes/delete-folder.js)
19
+ - [x] 🟠 **Validación de `fileName` en `download`/`info`.** Se rechazan (`400`) claves con segmentos `..`/`.`, backslashes o que empiecen por `/` antes de llegar al cliente S3. → [download.js](src/extensions/interactive-storage/routes/download.js), [info.js](src/extensions/interactive-storage/routes/info.js)
20
+ - [x] 🟡 **Límites de payload configurables (default 10 MB).** Body JSON vía `HTTP_JSON_LIMIT` y socket vía `SOCKET_MAX_HTTP_BUFFER_SIZE` (ambos por defecto 10 MB, incluidos en el `.env` generado). → [Express.js](src/services/express/Express.js#L32), [SocketIO.js](src/services/socketio/SocketIO.js#L32), [EnvManager.js](src/utils/EnvManager.js)
21
+ - [x] 🟡 **Swagger protegible opcionalmente.** `SWAGGER_PASSWORD` activa HTTP Basic auth (usuario configurable con `SWAGGER_USER`, default `admin`); `DISABLE_SWAGGER=true` lo desactiva por completo. Si queda abierto se emite un warning. → [src/services/express/Express.js](src/services/express/Express.js#L44)
22
+ - [x] 🟡 **No se filtra `error.message` al cliente.** Las 4 respuestas `500` (rutas y middlewares, en `boot` y `registerRoutes`) devuelven ahora un genérico `Internal Server Error`; el detalle (incluido el stack) queda solo en los logs del servidor. → [src/services/express/Express.js](src/services/express/Express.js)
23
+ - [x] 🟠 **Cookies dependientes del entorno.** Ya no se fuerza `sameSite:"none"` + `secure` + cross-subdomain siempre. Por defecto `sameSite:"lax"` y `secure` solo en producción (login funciona en `http://localhost`); `AUTH_CROSS_DOMAIN=true` activa `none`+`secure`+cross-subdomain para front/back en dominios distintos (HTTPS). → [src/services/auth/index.js](src/services/auth/index.js#L126-L165)
24
+ - [x] 🟢 **Helpers de autorización en el framework.** Nuevos `RequireAuthMiddleware`, `RequireAdminMiddleware` (rutas) y `AuthGuard` (sockets), exportados desde `zyket`, para proteger rutas/eventos con la sesión de better-auth. → [src/services/express/RequireAuthMiddleware.js](src/services/express/RequireAuthMiddleware.js), [src/services/socketio/AuthGuard.js](src/services/socketio/AuthGuard.js)
25
+
26
+ ---
27
+
28
+ ## ⏳ Pendiente
29
+
30
+ ### 🟠 Alto
31
+ - [ ] **CSRF de OAuth / account linking.** `account.skipStateCookieCheck: true` junto a `accountLinking.enabled: true` desactiva la verificación de `state` → riesgo de account takeover. Quitar `skipStateCookieCheck`. → [src/services/auth/index.js](src/services/auth/index.js#L189-L191)
32
+ - [ ] **Socket.IO CORS `origin:"*"` + guard por defecto que no bloquea.** Restringir origins (reutilizar `TRUSTED_ORIGINS`) y que el guard por defecto deniegue sin sesión válida. → [src/services/socketio/SocketIO.js](src/services/socketio/SocketIO.js#L32), [src/templates/default/src/guards/default.js](src/templates/default/src/guards/default.js)
33
+
34
+ ### 🟡 Medio
35
+ - [ ] **Rate limiting** (login, reset password, upload) con `express-rate-limit`. → [src/services/express/Express.js](src/services/express/Express.js)
36
+ - [ ] **Cabeceras de seguridad** con `helmet`. → [src/services/express/Express.js](src/services/express/Express.js)
37
+
38
+ ### 🔵 Bajo / Higiene
39
+ - [ ] **Reducir superficie de drivers de BD** (`sqlite3` + `better-sqlite3` + `pg` + `mariadb`).
40
+ - [ ] **Documentar guía de despliegue seguro** (variables obligatorias en producción, HTTPS, secretos).
package/bin/cli.js CHANGED
@@ -1,202 +1,282 @@
1
- #!/usr/bin/env node
2
- const prompts = require('prompts');
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { spawn } = require('child_process');
6
- const TemplateManager = require('../src/services/template-manager');
7
- const EnvManager = require('../src/utils/EnvManager');
8
- const templateManager = new TemplateManager();
9
-
10
- (async () => {
11
- process.stdout.write("\u001b[2J\u001b[0;0H");
12
- await templateManager.boot();
13
-
14
- // Check for direct command (e.g., npx zyket init)
15
- const args = process.argv.slice(2);
16
- const directCommand = args[0];
17
-
18
- let actionToRun = null;
19
-
20
- if (directCommand === 'init') {
21
- actionToRun = 'init-project';
22
- } else {
23
- // Show interactive menu
24
- const response = await prompts({
25
- type: 'select',
26
- name: 'value',
27
- message: '[ZYKET] What do you want to do?',
28
- choices: [
29
- { title: 'Initialize Project', value: 'init-project', description: 'Set up a new Zyket project', disabled: false },
30
- { title: 'Install Template', value: 'install-template', description: 'Install a new template', disabled: false },
31
- /*{ title: 'Remove Template', value: 'remove-template', description: 'Remove an existing template', disabled: false },*/
32
- ],
33
- initial: 0
34
- });
35
- actionToRun = response.value;
36
- }
37
-
38
- if (!actionToRun) {
39
- console.log('[ZYKET] No action selected. Exiting.');
40
- return;
41
- }
42
-
43
- const actions = {
44
- 'init-project': async () => {
45
- const indexPath = path.join(process.cwd(), 'index.js');
46
- const envPath = path.join(process.cwd(), '.env');
47
- const packageJsonPath = path.join(process.cwd(), 'package.json');
48
-
49
- // Check if index.js already exists
50
- if (fs.existsSync(indexPath)) {
51
- const overwrite = await prompts({
52
- type: 'confirm',
53
- name: 'value',
54
- message: '[ZYKET] index.js already exists. Overwrite?',
55
- initial: false
56
- });
57
- if (!overwrite.value) {
58
- console.log('[ZYKET] Initialization cancelled.');
59
- return;
60
- }
61
- }
62
-
63
- // Create .env file
64
- console.log('[ZYKET] Creating .env file...');
65
- EnvManager.createEnvFile(envPath);
66
-
67
- // Install default backend template files (src and config)
68
- console.log('[ZYKET] Installing default backend template files...');
69
- const defaultTemplate = templateManager.getTemplate('default');
70
- const backendFiles = defaultTemplate.filter(file => {
71
- const route = file.route;
72
- // Only install src and config files, not frontend
73
- // Skip guards and handlers folders since socket is disabled by default
74
- return (route.startsWith('default/src/') || route.startsWith('default/config/'))
75
- && !route.startsWith('default/src/guards/')
76
- && !route.startsWith('default/src/handlers/');
77
- });
78
-
79
- for (const file of backendFiles) {
80
- const fileName = file.route.split('/').slice(1).join('/');
81
- const fileLocation = path.join(process.cwd(), fileName);
82
- const folderLocation = path.dirname(fileLocation);
83
-
84
- // Create directory if it doesn't exist
85
- if (!fs.existsSync(folderLocation)) {
86
- fs.mkdirSync(folderLocation, { recursive: true });
87
- }
88
-
89
- // Write file if it doesn't exist
90
- if (!fs.existsSync(fileLocation)) {
91
- fs.writeFileSync(fileLocation, file.content);
92
- }
93
- }
94
- console.log('[ZYKET] ✅ Backend template files installed');
95
-
96
- // Create index.js with boilerplate code
97
- console.log('[ZYKET] Creating index.js...');
98
- const indexContent = `const { Kernel } = require('zyket');
99
-
100
- const kernel = new Kernel({
101
- services: [
102
- ['auth', require('./src/services/auth'), ["@service_container"]],
103
- ]
104
- });
105
-
106
- kernel.boot().then(() => {
107
- console.log('Kernel booted successfully!');
108
- }).catch((error) => {
109
- console.error('Error booting kernel:', error);
110
- });
111
- `;
112
- fs.writeFileSync(indexPath, indexContent);
113
-
114
- // Create package.json if it doesn't exist
115
- if (!fs.existsSync(packageJsonPath)) {
116
- console.log('[ZYKET] Creating package.json...');
117
- const packageJson = {
118
- name: path.basename(process.cwd()),
119
- version: "1.0.0",
120
- description: "Zyket application",
121
- main: "index.js",
122
- scripts: {
123
- dev: "node index.js"
124
- },
125
- keywords: [],
126
- author: "",
127
- license: "ISC",
128
- dependencies: {
129
- zyket: "^1.2.3"
130
- }
131
- };
132
- fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
133
- }
134
-
135
- console.log('\n[ZYKET] Project initialized successfully!');
136
-
137
- // Install dependencies automatically
138
- console.log('\n[ZYKET] Installing dependencies...');
139
- await new Promise((resolve, reject) => {
140
- const npmInstall = spawn('npm', ['install'], {
141
- cwd: process.cwd(),
142
- stdio: 'inherit',
143
- shell: true
144
- });
145
-
146
- npmInstall.on('close', (code) => {
147
- if (code === 0) {
148
- console.log('\n[ZYKET] ✅ Dependencies installed successfully!');
149
- resolve();
150
- } else {
151
- reject(new Error(`npm install exited with code ${code}`));
152
- }
153
- });
154
-
155
- npmInstall.on('error', (error) => {
156
- reject(error);
157
- });
158
- });
159
-
160
- // Start the project automatically
161
- console.log('\n[ZYKET] Starting project...\n');
162
- const nodeStart = spawn('node', ['index.js'], {
163
- cwd: process.cwd(),
164
- stdio: 'inherit',
165
- shell: true
166
- });
167
-
168
- nodeStart.on('error', (error) => {
169
- console.error('[ZYKET] Error starting project:', error);
170
- });
171
- },
172
- 'install-template': async () => {
173
- const templates = templateManager.getTemplates();
174
- const response = await prompts({
175
- type: 'select',
176
- name: 'templateToInstall',
177
- message: '[ZYKET] What template would you like to install?',
178
- choices: [
179
- ...templates.map((template) => ({ title: template.toUpperCase(), value: template, description: '', disabled: false })),
180
- ],
181
- initial: 0
182
- });
183
- if(!templateManager.exists(response.templateToInstall)) throw new Error(`Template ${response.templateToInstall} not found`);
184
- templateManager.installTemplate(response.templateToInstall);
185
- },
186
- /*'remove-template': async () => {
187
- const response = await prompts({
188
- type: 'select',
189
- name: 'templateToRemove',
190
- message: '[ZYKET] What template would you like to remove?',
191
- choices: [
192
- { title: 'Auth', value: 'auth', description: 'Authentication template', disabled: false },
193
- { title: 'Chat', value: 'chat', description: 'Chat template', disabled: false },
194
- ],
195
- initial: 0
196
- });
197
- console.log(`Removing template: ${response.templateToRemove}`);
198
- }*/
199
- };
200
-
201
- await actions[actionToRun]();
202
- })();
1
+ #!/usr/bin/env node
2
+ const prompts = require('prompts');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawn } = require('child_process');
6
+ const TemplateManager = require('../src/services/template-manager');
7
+ const EnvManager = require('../src/utils/EnvManager');
8
+ const templateManager = new TemplateManager();
9
+
10
+ const ZYKET_PKG = (() => {
11
+ try { return require('../package.json'); } catch { return { version: '1.0.0', dependencies: {} }; }
12
+ })();
13
+ const ZYKET_VERSION = ZYKET_PKG.version || '1.0.0';
14
+
15
+ const TEMPLATE_DESCRIPTIONS = {
16
+ default: 'Full-stack starter (React frontend + auth)',
17
+ 'api-rest': 'Backend-only REST API (auth + CRUD)',
18
+ 'saas-multitenant': 'Multi-tenant SaaS (orgs, roles) + dashboard',
19
+ 'realtime-chat': 'Authenticated real-time chat (Socket.IO) + UI',
20
+ };
21
+
22
+ // ---- helpers ----------------------------------------------------------------
23
+
24
+ function templateChoices() {
25
+ const all = templateManager.getTemplates();
26
+ // Show "default" first, then the rest.
27
+ const ordered = ['default', ...all.filter((t) => t !== 'default')];
28
+ return ordered
29
+ .filter((t) => templateManager.exists(t))
30
+ .map((t) => ({ title: t, value: t, description: TEMPLATE_DESCRIPTIONS[t] || '' }));
31
+ }
32
+
33
+ // Write every file of a template into the project, stripping the template-name
34
+ // prefix. Existing files are left untouched. Returns whether the template ships
35
+ // its own index.js. `exclude(rel)` can skip specific files.
36
+ function writeTemplateFiles(templateName, { exclude = () => false } = {}) {
37
+ const files = templateManager.getTemplate(templateName);
38
+ let hasIndex = false;
39
+ for (const file of files) {
40
+ const rel = file.route.split('/').slice(1).join('/');
41
+ if (!rel || exclude(rel)) continue;
42
+ if (rel === 'index.js') hasIndex = true;
43
+ const dest = path.join(process.cwd(), rel);
44
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
45
+ if (!fs.existsSync(dest)) fs.writeFileSync(dest, file.content);
46
+ }
47
+ return { hasIndex };
48
+ }
49
+
50
+ // Merge KEY=VALUE pairs from a copied .env.example onto the generated .env so a
51
+ // template can override defaults (e.g. DISABLE_VITE=false).
52
+ function applyEnvOverridesFromExample(envPath) {
53
+ const examplePath = path.join(process.cwd(), '.env.example');
54
+ if (!fs.existsSync(examplePath)) return;
55
+ for (const line of fs.readFileSync(examplePath, 'utf-8').split(/\r?\n/)) {
56
+ const trimmed = line.trim();
57
+ if (!trimmed || trimmed.startsWith('#')) continue;
58
+ const eq = trimmed.indexOf('=');
59
+ if (eq === -1) continue;
60
+ const key = trimmed.slice(0, eq).trim();
61
+ const value = trimmed.slice(eq + 1).trim();
62
+ if (key) EnvManager.setEnvVariable(envPath, key, value);
63
+ }
64
+ }
65
+
66
+ function writeDefaultIndex() {
67
+ const indexContent = `const { Kernel } = require('zyket');
68
+
69
+ const kernel = new Kernel({
70
+ services: [
71
+ ['auth', require('./src/services/auth'), ["@service_container"]],
72
+ ]
73
+ });
74
+
75
+ kernel.boot().then(() => {
76
+ console.log('Kernel booted successfully!');
77
+ }).catch((error) => {
78
+ console.error('Error booting kernel:', error);
79
+ });
80
+ `;
81
+ fs.writeFileSync(path.join(process.cwd(), 'index.js'), indexContent);
82
+ }
83
+
84
+ function templateRoutes(name) {
85
+ return templateManager.getTemplate(name).map((f) => f.route);
86
+ }
87
+
88
+ function templateHasFrontend(name) {
89
+ return templateRoutes(name).some((r) => r.startsWith(`${name}/frontend/`));
90
+ }
91
+
92
+ function templateUsesAuth(name) {
93
+ return templateRoutes(name).some((r) => r.startsWith(`${name}/src/services/auth/`));
94
+ }
95
+
96
+ // Build the dependency set a generated project needs. The user's own code
97
+ // (frontend configs, handlers, stores) imports these DIRECTLY, so they must be
98
+ // real project dependencies not just transitive deps of zyket. Versions are
99
+ // pinned to whatever this zyket release uses.
100
+ function depsForTemplate(template) {
101
+ const deps = { zyket: `^${ZYKET_VERSION}` };
102
+ if (template === 'default') return deps; // default init is backend-only
103
+
104
+ const copy = (names) => names.forEach((n) => {
105
+ const v = ZYKET_PKG.dependencies?.[n];
106
+ if (v) deps[n] = v;
107
+ });
108
+
109
+ if (templateUsesAuth(template)) copy(['better-auth']);
110
+ if (templateHasFrontend(template)) {
111
+ copy([
112
+ 'react', 'react-dom', 'react-router-dom', 'zustand', 'prop-types',
113
+ 'vite', '@vitejs/plugin-react', '@tailwindcss/vite', 'tailwindcss',
114
+ ]);
115
+ }
116
+ if (template === 'realtime-chat') copy(['socket.io-client']);
117
+
118
+ return deps;
119
+ }
120
+
121
+ function ensurePackageJson(template) {
122
+ const packageJsonPath = path.join(process.cwd(), 'package.json');
123
+ if (fs.existsSync(packageJsonPath)) return;
124
+ const packageJson = {
125
+ name: path.basename(process.cwd()),
126
+ version: '1.0.0',
127
+ description: 'Zyket application',
128
+ main: 'index.js',
129
+ scripts: { dev: 'node index.js' },
130
+ keywords: [],
131
+ author: '',
132
+ license: 'ISC',
133
+ dependencies: depsForTemplate(template),
134
+ };
135
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
136
+ }
137
+
138
+ function npmInstall() {
139
+ return new Promise((resolve, reject) => {
140
+ const npmInstallProcess = spawn('npm', ['install'], { cwd: process.cwd(), stdio: 'inherit', shell: true });
141
+ npmInstallProcess.on('close', (code) => {
142
+ if (code === 0) resolve();
143
+ else reject(new Error(`npm install exited with code ${code}`));
144
+ });
145
+ npmInstallProcess.on('error', reject);
146
+ });
147
+ }
148
+
149
+ // ---- main -------------------------------------------------------------------
150
+
151
+ (async () => {
152
+ process.stdout.write("");
153
+ await templateManager.boot();
154
+
155
+ const args = process.argv.slice(2);
156
+ const directCommand = args[0];
157
+
158
+ let actionToRun = null;
159
+ let templateArg = null;
160
+
161
+ if (directCommand === 'init') {
162
+ actionToRun = 'init-project';
163
+ templateArg = args[1] || null; // e.g. `npx zyket init api-rest`
164
+ } else {
165
+ const response = await prompts({
166
+ type: 'select',
167
+ name: 'value',
168
+ message: '[ZYKET] What do you want to do?',
169
+ choices: [
170
+ { title: 'Initialize Project', value: 'init-project', description: 'Scaffold a new Zyket project from a template', disabled: false },
171
+ { title: 'Install Template', value: 'install-template', description: 'Add a template into the current project', disabled: false },
172
+ ],
173
+ initial: 0,
174
+ });
175
+ actionToRun = response.value;
176
+ }
177
+
178
+ if (!actionToRun) {
179
+ console.log('[ZYKET] No action selected. Exiting.');
180
+ return;
181
+ }
182
+
183
+ const actions = {
184
+ 'init-project': async (preselectedTemplate) => {
185
+ // Resolve which template to scaffold.
186
+ let template = preselectedTemplate && templateManager.exists(preselectedTemplate) ? preselectedTemplate : null;
187
+ if (preselectedTemplate && !template) {
188
+ console.log(`[ZYKET] Template '${preselectedTemplate}' not found. Pick one:`);
189
+ }
190
+ if (!template) {
191
+ const response = await prompts({
192
+ type: 'select',
193
+ name: 'value',
194
+ message: '[ZYKET] Choose a template to initialize',
195
+ choices: templateChoices(),
196
+ initial: 0,
197
+ });
198
+ template = response.value;
199
+ }
200
+
201
+ if (!template) {
202
+ console.log('[ZYKET] No template selected. Exiting.');
203
+ return;
204
+ }
205
+
206
+ const indexPath = path.join(process.cwd(), 'index.js');
207
+ const envPath = path.join(process.cwd(), '.env');
208
+
209
+ if (fs.existsSync(indexPath)) {
210
+ const overwrite = await prompts({
211
+ type: 'confirm',
212
+ name: 'value',
213
+ message: '[ZYKET] index.js already exists. Continue scaffolding (existing files are kept)?',
214
+ initial: false,
215
+ });
216
+ if (!overwrite.value) {
217
+ console.log('[ZYKET] Initialization cancelled.');
218
+ return;
219
+ }
220
+ }
221
+
222
+ console.log(`[ZYKET] Initializing with template '${template}'...`);
223
+ EnvManager.createEnvFile(envPath);
224
+
225
+ if (template === 'default') {
226
+ // Curated default: backend files only (no frontend, no socket guards/handlers).
227
+ writeTemplateFiles('default', {
228
+ exclude: (rel) => !(
229
+ (rel.startsWith('src/') || rel.startsWith('config/')) &&
230
+ !rel.startsWith('src/guards/') &&
231
+ !rel.startsWith('src/handlers/')
232
+ ),
233
+ });
234
+ writeDefaultIndex();
235
+ } else {
236
+ // Self-contained template: copy everything (incl. its index.js and frontend).
237
+ const { hasIndex } = writeTemplateFiles(template);
238
+ applyEnvOverridesFromExample(envPath);
239
+ if (!hasIndex) writeDefaultIndex();
240
+ }
241
+
242
+ ensurePackageJson(template);
243
+
244
+ console.log('\n[ZYKET] Installing dependencies...');
245
+ await npmInstall();
246
+
247
+ console.log(`\n[ZYKET] ✅ Project initialized with template '${template}'!`);
248
+
249
+ if (template === 'default') {
250
+ // Preserve previous behavior: start immediately.
251
+ console.log('\n[ZYKET] Starting project...\n');
252
+ spawn('node', ['index.js'], { cwd: process.cwd(), stdio: 'inherit', shell: true })
253
+ .on('error', (error) => console.error('[ZYKET] Error starting project:', error));
254
+ return;
255
+ }
256
+
257
+ // Templates that ship auth need their tables created first.
258
+ const usesAuth = fs.existsSync(path.join(process.cwd(), 'src', 'services', 'auth'));
259
+ console.log('\nNext steps:');
260
+ if (usesAuth) console.log(' npx @better-auth/cli migrate # create the auth tables');
261
+ console.log(' node index.js\n');
262
+ if (fs.existsSync(path.join(process.cwd(), 'README.md'))) {
263
+ console.log('See README.md for template-specific details.\n');
264
+ }
265
+ },
266
+
267
+ 'install-template': async () => {
268
+ const templates = templateManager.getTemplates();
269
+ const response = await prompts({
270
+ type: 'select',
271
+ name: 'templateToInstall',
272
+ message: '[ZYKET] What template would you like to install?',
273
+ choices: templates.map((template) => ({ title: template.toUpperCase(), value: template, description: TEMPLATE_DESCRIPTIONS[template] || '', disabled: false })),
274
+ initial: 0,
275
+ });
276
+ if (!templateManager.exists(response.templateToInstall)) throw new Error(`Template ${response.templateToInstall} not found`);
277
+ templateManager.installTemplate(response.templateToInstall);
278
+ },
279
+ };
280
+
281
+ await actions[actionToRun](templateArg);
282
+ })();
package/index.js CHANGED
@@ -2,8 +2,8 @@ const Kernel = require("./src/kernel");
2
2
  const Service = require("./src/services/Service");
3
3
  const EnvManager = require("./src/utils/EnvManager");
4
4
 
5
- const {Route, Middleware, Express} = require("./src/services/express");
6
- const { Handler, Guard } = require("./src/services/socketio");
5
+ const {Route, Middleware, Express, RequireAuthMiddleware, RequireAdminMiddleware} = require("./src/services/express");
6
+ const { Handler, Guard, AuthGuard } = require("./src/services/socketio");
7
7
  const Schedule = require("./src/services/scheduler/Schedule");
8
8
  const Event = require("./src/services/events/Event");
9
9
  const Worker = require("./src/services/bullmq/Worker");
@@ -20,7 +20,8 @@ module.exports = {
20
20
  Express,
21
21
  Kernel, Service,
22
22
  Route, Middleware,
23
- Handler, Guard,
23
+ RequireAuthMiddleware, RequireAdminMiddleware,
24
+ Handler, Guard, AuthGuard,
24
25
  Schedule, Event,
25
26
  Worker,
26
27
  EnvManager,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zyket",
3
- "version": "1.2.17",
3
+ "version": "1.2.19",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -39,6 +39,7 @@
39
39
  "redis": "^5.11.0",
40
40
  "sequelize": "^6.37.8",
41
41
  "socket.io": "^4.8.3",
42
+ "socket.io-client": "^4.8.3",
42
43
  "sqlite3": "^6.0.1",
43
44
  "swagger-jsdoc": "^6.2.8",
44
45
  "swagger-ui-express": "^5.0.1",
@@ -50,5 +51,8 @@
50
51
  "optionalDependencies": {
51
52
  "@socket.io/redis-adapter": "^8.3.0",
52
53
  "ioredis": "^5.6.1"
54
+ },
55
+ "overrides": {
56
+ "ajv": "^8.17.1"
53
57
  }
54
58
  }
@@ -7,14 +7,17 @@ const basicAuth = require('express-basic-auth')
7
7
  module.exports = class BullBoardExtension extends Extension {
8
8
  path;
9
9
 
10
- constructor({ path = '/bullboard', basePath = '' } = {}) {
10
+ constructor({ path = '/bullboard', basePath = '', middlewares = [] } = {}) {
11
11
  super("BullBoardExtension");
12
12
  this.path = path || '/bullboard';
13
13
  this.basePath = basePath;
14
+ // Optional Express middlewares (e.g. custom auth) applied before the board.
15
+ this.middlewares = middlewares || [];
14
16
  }
15
17
 
16
18
  load(container) {
17
- if (!container.get('bullmq')) return container.get('logger').warn('BullBoardExtension: bullmq service not found, skipping BullBoard setup');
19
+ const logger = container.get('logger')
20
+ if (!container.get('bullmq')) return logger.warn('BullBoardExtension: bullmq service not found, skipping BullBoard setup');
18
21
  const bull = container.get('bullmq')
19
22
  const serverAdapter = new ExpressAdapter()
20
23
  serverAdapter.setBasePath(this.basePath + this.path)
@@ -25,7 +28,7 @@ module.exports = class BullBoardExtension extends Extension {
25
28
  })
26
29
 
27
30
  const app = container.get('express').app()
28
- const middlewares = []
31
+ const middlewares = [...this.middlewares]
29
32
 
30
33
  if (process.env.BULLBOARD_ADMIN_PASSWORD) {
31
34
  middlewares.push(basicAuth({
@@ -34,6 +37,14 @@ module.exports = class BullBoardExtension extends Extension {
34
37
  }))
35
38
  }
36
39
 
40
+ // Fail closed: never expose the queue dashboard (jobs, payloads, retry/delete)
41
+ // without authentication. Require a password or custom auth middlewares.
42
+ if (middlewares.length === 0) {
43
+ return logger.warn(
44
+ `BullBoardExtension: no authentication configured. Set BULLBOARD_ADMIN_PASSWORD or pass "middlewares" to expose the dashboard. Skipping mount of ${this.path}.`
45
+ )
46
+ }
47
+
37
48
  app.use(this.path, ...middlewares, serverAdapter.getRouter())
38
49
  }
39
50
  }