zyket 1.2.18 → 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 +46 -28
  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
@@ -15,13 +15,15 @@ module.exports = class InteractiveStorageExtension extends Extension {
15
15
  path;
16
16
  maxFileSize;
17
17
  middlewares;
18
+ maxDeleteBatch;
18
19
 
19
- constructor({ path = '/storage', bucketName = 'dropbox', maxFileSize = 100 * 1024 * 1024, middlewares = [] } = {}) {
20
+ constructor({ path = '/storage', bucketName = 'dropbox', maxFileSize = 100 * 1024 * 1024, middlewares = [], maxDeleteBatch = 100 } = {}) {
20
21
  super("InteractiveStorageExtension");
21
22
  this.path = path || '/storage';
22
23
  InteractiveStorageExtension.bucketName = bucketName;
23
24
  this.maxFileSize = maxFileSize;
24
25
  this.middlewares = middlewares || [];
26
+ this.maxDeleteBatch = maxDeleteBatch;
25
27
  }
26
28
 
27
29
  load(container) {
@@ -47,15 +49,24 @@ module.exports = class InteractiveStorageExtension extends Extension {
47
49
  new BrowseRoute(`${this.path}/browse`, s3, InteractiveStorageExtension.bucketName, normalizePath, listFilesAndFolders),
48
50
  new DownloadRoute(`${this.path}/download/:fileName`, s3, InteractiveStorageExtension.bucketName, getFileStat),
49
51
  new InfoRoute(`${this.path}/info/:fileName`, s3, InteractiveStorageExtension.bucketName, getFileStat),
50
- new DeleteRoute(`${this.path}/delete`, s3, InteractiveStorageExtension.bucketName),
52
+ new DeleteRoute(`${this.path}/delete`, s3, InteractiveStorageExtension.bucketName, this.maxDeleteBatch),
51
53
  new CreateFolderRoute(`${this.path}/create-folder`, s3, InteractiveStorageExtension.bucketName, normalizePath),
52
- new DeleteFolderRoute(`${this.path}/delete-folder`, s3, InteractiveStorageExtension.bucketName, normalizePath, listFiles)
54
+ new DeleteFolderRoute(`${this.path}/delete-folder`, s3, InteractiveStorageExtension.bucketName, normalizePath, listFiles, this.maxDeleteBatch)
53
55
  ];
54
56
 
55
- // Add multer middleware to upload route
56
- routes[0].middlewares = {
57
- post: [new MulterMiddleware(this.maxFileSize)]
58
- };
57
+ // Apply the user-provided middlewares (e.g. authentication) to every
58
+ // storage route and method. They run before the route handler.
59
+ const userMiddlewares = this.middlewares || [];
60
+ for (const route of routes) {
61
+ route.middlewares = {
62
+ get: [...userMiddlewares],
63
+ post: [...userMiddlewares],
64
+ delete: [...userMiddlewares]
65
+ };
66
+ }
67
+
68
+ // Add multer middleware to the upload route, after any auth middlewares.
69
+ routes[0].middlewares.post = [...userMiddlewares, new MulterMiddleware(this.maxFileSize)];
59
70
 
60
71
  // Register routes using the express service pattern
61
72
  express.registerRoutes(routes);
@@ -148,8 +159,13 @@ module.exports = class InteractiveStorageExtension extends Extension {
148
159
  }
149
160
 
150
161
  #normalizePath(path) {
151
- // Remove leading/trailing slashes and normalize
152
- return path.replace(/^\/+|\/+$/g, '').replace(/\/+/g, '/');
162
+ // Normalize slashes and strip any path-traversal segments so a crafted
163
+ // "folder"/"folderPath" value cannot escape the intended prefix.
164
+ return String(path)
165
+ .replace(/\\/g, '/')
166
+ .split('/')
167
+ .filter((segment) => segment && segment !== '.' && segment !== '..')
168
+ .join('/');
153
169
  }
154
170
 
155
171
  async #getFileStat(s3, fileName) {
@@ -5,13 +5,15 @@ module.exports = class DeleteFolderRoute extends Route {
5
5
  bucketName;
6
6
  normalizePath;
7
7
  listFiles;
8
+ maxDeleteBatch;
8
9
 
9
- constructor(path, s3, bucketName, normalizePath, listFiles) {
10
+ constructor(path, s3, bucketName, normalizePath, listFiles, maxDeleteBatch = 100) {
10
11
  super(path);
11
12
  this.s3 = s3;
12
13
  this.bucketName = bucketName;
13
14
  this.normalizePath = normalizePath;
14
15
  this.listFiles = listFiles;
16
+ this.maxDeleteBatch = maxDeleteBatch;
15
17
  }
16
18
 
17
19
  async delete({ container, request }) {
@@ -36,6 +38,16 @@ module.exports = class DeleteFolderRoute extends Route {
36
38
  };
37
39
  }
38
40
 
41
+ // Cap the batch size to avoid mass-deletion / resource-exhaustion when a
42
+ // prefix contains a very large number of objects.
43
+ if (files.length > this.maxDeleteBatch) {
44
+ return {
45
+ success: false,
46
+ message: `Folder contains too many files (${files.length}). Maximum allowed per request is ${this.maxDeleteBatch}`,
47
+ status: 400
48
+ };
49
+ }
50
+
39
51
  // Delete all files
40
52
  const deletePromises = files.map(file =>
41
53
  this.s3.removeFile(this.bucketName, file.name).catch(err => ({ error: err.message, fileName: file.name }))
@@ -3,24 +3,35 @@ const { Route } = require('../../../services/express');
3
3
  module.exports = class DeleteRoute extends Route {
4
4
  s3;
5
5
  bucketName;
6
+ maxDeleteBatch;
6
7
 
7
- constructor(path, s3, bucketName) {
8
+ constructor(path, s3, bucketName, maxDeleteBatch = 100) {
8
9
  super(path);
9
10
  this.s3 = s3;
10
11
  this.bucketName = bucketName;
12
+ this.maxDeleteBatch = maxDeleteBatch;
11
13
  }
12
14
 
13
15
  async delete({ container, request }) {
14
16
  const logger = container.get('logger');
15
17
  const { fileName, fileNames } = request.body;
16
-
18
+
17
19
  // Support both single file and multiple files
18
20
  const filesToDelete = fileNames || (fileName ? [fileName] : []);
19
-
21
+
20
22
  if (!Array.isArray(filesToDelete) || filesToDelete.length === 0) {
21
23
  return { success: false, message: 'fileName or fileNames array is required', status: 400 };
22
24
  }
23
25
 
26
+ // Cap the batch size to avoid mass-deletion / resource-exhaustion in a single request.
27
+ if (filesToDelete.length > this.maxDeleteBatch) {
28
+ return {
29
+ success: false,
30
+ message: `Too many files in a single request. Maximum allowed is ${this.maxDeleteBatch}`,
31
+ status: 400
32
+ };
33
+ }
34
+
24
35
  const deletePromises = filesToDelete.map(file =>
25
36
  this.s3.removeFile(this.bucketName, file).catch(err => ({ error: err.message, fileName: file }))
26
37
  );
@@ -1,4 +1,13 @@
1
1
  const { Route } = require('../../../services/express');
2
+ const path = require('path');
3
+
4
+ // Reject object keys that try to escape the bucket prefix or are malformed,
5
+ // before they reach the S3 client.
6
+ function isUnsafeKey(key) {
7
+ if (typeof key !== 'string' || key.length === 0) return true;
8
+ if (key.includes('\\') || key.startsWith('/')) return true;
9
+ return key.split('/').some((segment) => segment === '..' || segment === '.');
10
+ }
2
11
 
3
12
  module.exports = class DownloadRoute extends Route {
4
13
  s3;
@@ -15,14 +24,24 @@ module.exports = class DownloadRoute extends Route {
15
24
  async get({ container, request, response }) {
16
25
  const logger = container.get('logger');
17
26
  const { fileName } = request.params;
18
-
27
+
28
+ if (isUnsafeKey(fileName)) {
29
+ return { success: false, message: 'Invalid file name', status: 400 };
30
+ }
31
+
19
32
  try {
20
33
  // Get file info first
21
34
  const stat = await this.getFileStat(this.s3, fileName);
22
35
 
23
- // Set response headers
36
+ // Set response headers. Sanitize the filename to prevent header
37
+ // injection (CRLF / quote breakout) from the user-controlled key.
38
+ const baseName = path.posix.basename(fileName);
39
+ const asciiName = baseName.replace(/[^\w.\- ]/g, '_') || 'download';
24
40
  response.setHeader('Content-Type', stat.metaData?.['content-type'] || 'application/octet-stream');
25
- response.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
41
+ response.setHeader(
42
+ 'Content-Disposition',
43
+ `attachment; filename="${asciiName}"; filename*=UTF-8''${encodeURIComponent(baseName)}`
44
+ );
26
45
  response.setHeader('Content-Length', stat.size);
27
46
 
28
47
  // Stream the file
@@ -1,5 +1,13 @@
1
1
  const { Route } = require('../../../services/express');
2
2
 
3
+ // Reject object keys that try to escape the bucket prefix or are malformed,
4
+ // before they reach the S3 client.
5
+ function isUnsafeKey(key) {
6
+ if (typeof key !== 'string' || key.length === 0) return true;
7
+ if (key.includes('\\') || key.startsWith('/')) return true;
8
+ return key.split('/').some((segment) => segment === '..' || segment === '.');
9
+ }
10
+
3
11
  module.exports = class InfoRoute extends Route {
4
12
  s3;
5
13
  bucketName;
@@ -17,6 +25,11 @@ module.exports = class InfoRoute extends Route {
17
25
 
18
26
  try {
19
27
  const { fileName } = request.params;
28
+
29
+ if (isUnsafeKey(fileName)) {
30
+ return { success: false, message: 'Invalid file name', status: 400 };
31
+ }
32
+
20
33
  const stat = await this.getFileStat(this.s3, fileName);
21
34
 
22
35
  return {
@@ -4,6 +4,7 @@ const { betterAuth } = require("better-auth");
4
4
  const { admin, bearer, organization } = require("better-auth/plugins");
5
5
  const { Pool } = require("pg");
6
6
  const path = require("path");
7
+ const crypto = require("crypto");
7
8
 
8
9
  module.exports = class AuthService extends Service {
9
10
  #container;
@@ -24,13 +25,26 @@ module.exports = class AuthService extends Service {
24
25
  express.regiterRawAllRoutes("/api/auth/*splat", toNodeHandler(this.auth));
25
26
  }
26
27
 
28
+ // Resolve the current better-auth session from raw Node request headers
29
+ // (e.g. `request.headers` or `socket.handshake.headers`). Lets application
30
+ // code check sessions without importing better-auth directly.
31
+ async getSession(headers) {
32
+ const { fromNodeHeaders } = require('better-auth/node');
33
+ return this.client.api.getSession({ headers: fromNodeHeaders(headers) });
34
+ }
35
+
27
36
  #addAuthEnvVariables() {
28
37
  const EnvManager = require('../../utils/EnvManager');
29
38
  const envPath = path.join(process.cwd(), '.env');
30
39
 
31
- const secretAdded = EnvManager.addEnvVariable(envPath, 'AUTH_SECRET', 'change-this-secret-in-production');
40
+ // Generate a strong, unique secret per project instead of a static value.
41
+ const generatedSecret = crypto.randomBytes(32).toString('hex');
42
+ const secretAdded = EnvManager.addEnvVariable(envPath, 'AUTH_SECRET', generatedSecret);
32
43
  if (secretAdded) {
33
- this.#container.get('logger').info('Added AUTH_SECRET to .env file');
44
+ // dotenv already loaded before this file was written, so make the freshly
45
+ // generated value available for this first run too.
46
+ process.env.AUTH_SECRET = generatedSecret;
47
+ this.#container.get('logger').info('Generated a random AUTH_SECRET and added it to .env file');
34
48
  }
35
49
 
36
50
  const originsAdded = EnvManager.addEnvVariable(envPath, 'TRUSTED_ORIGINS', 'http://localhost:5173,http://localhost:3000');
@@ -44,6 +58,15 @@ module.exports = class AuthService extends Service {
44
58
  }
45
59
  }
46
60
 
61
+ #requireAuthSecret() {
62
+ const secret = process.env.AUTH_SECRET;
63
+ // Fail closed: never fall back to a hard-coded/shared secret.
64
+ if (!secret || secret === 'change-this-secret-in-production' || secret === 'your-secret-key-change-in-production') {
65
+ throw new Error('AUTH_SECRET is missing or insecure. Set a strong, unique AUTH_SECRET in your .env file.');
66
+ }
67
+ return secret;
68
+ }
69
+
47
70
  #getDatabaseConnection() {
48
71
  const dialect = process.env.DATABASE_DIALECT;
49
72
 
@@ -68,6 +91,10 @@ module.exports = class AuthService extends Service {
68
91
  return true;
69
92
  }
70
93
 
94
+ get requireEmailVerification() {
95
+ return false;
96
+ }
97
+
71
98
  get socialProviders() {
72
99
  return {}
73
100
  }
@@ -106,6 +133,17 @@ module.exports = class AuthService extends Service {
106
133
 
107
134
  get auth() {
108
135
  const cache = this.#container.get('cache');
136
+
137
+ // Environment-aware cookie security:
138
+ // - Cross-domain front/back (different sites): set AUTH_CROSS_DOMAIN=true ->
139
+ // sameSite "none" + secure + cross-subdomain (requires HTTPS).
140
+ // - Otherwise (local dev, or same-site/same-domain prod): sameSite "lax" and
141
+ // secure only in production, so cookies work over http://localhost in dev.
142
+ const crossDomain = process.env.AUTH_CROSS_DOMAIN === 'true';
143
+ const isProduction = process.env.NODE_ENV === 'production';
144
+ const cookieSameSite = crossDomain ? 'none' : 'lax';
145
+ const cookieSecure = crossDomain || isProduction;
146
+
109
147
  return betterAuth({
110
148
  hooks: this.hooks,
111
149
  plugins: [
@@ -134,32 +172,12 @@ module.exports = class AuthService extends Service {
134
172
  socialProviders: this.socialProviders,
135
173
  database: this.#getDatabaseConnection(),
136
174
  advanced: {
137
- crossSubDomainCookies: {
138
- enabled: true,
139
- },
140
- cookie: {
141
- sameSite: "none",
142
- secure: true,
143
- path: "/",
144
- state: {
145
- attributes: {
146
- sameSite: "none",
147
- secure: true,
148
- }
149
- }
150
- },
175
+ ...(crossDomain ? { crossSubDomainCookies: { enabled: true } } : {}),
151
176
  defaultCookieAttributes: {
152
- secure: true,
153
- sameSite: "none",
177
+ sameSite: cookieSameSite,
178
+ secure: cookieSecure,
179
+ httpOnly: true,
154
180
  },
155
- cookies: {
156
- state: {
157
- attributes: {
158
- sameSite: "none",
159
- secure: true,
160
- }
161
- }
162
- }
163
181
  },
164
182
  secondaryStorage: {
165
183
  get: async (key) => {
@@ -175,7 +193,7 @@ module.exports = class AuthService extends Service {
175
193
  },
176
194
  emailAndPassword: {
177
195
  enabled: true,
178
- requireEmailVerification: false,
196
+ requireEmailVerification: this.requireEmailVerification,
179
197
  sendResetPassword: async ({ user, url, token }, request) => this.sendResetPasswordEmail({ user, url, token }, request),
180
198
  },
181
199
  emailVerification: {
@@ -195,7 +213,7 @@ module.exports = class AuthService extends Service {
195
213
  updateAge: 60 * 60 * 24
196
214
  },
197
215
  baseURL: process.env.BETTER_AUTH_URL || 'http://localhost:3000',
198
- secret: process.env.AUTH_SECRET || 'your-secret-key-change-in-production',
216
+ secret: this.#requireAuthSecret(),
199
217
  trustedOrigins: process.env.TRUSTED_ORIGINS?.split(',') || ['http://localhost:5173', 'http://localhost:6632']
200
218
  })
201
219
  }
@@ -29,19 +29,36 @@ module.exports = class Express extends Service {
29
29
  this.#httpServer = httpServer;
30
30
  this.#app = express();
31
31
 
32
- this.#app.use(express.json({ limit: `100mb` }))
32
+ // Configurable JSON body limit (defaults to 10mb) to reduce DoS surface.
33
+ this.#app.use(express.json({ limit: process.env.HTTP_JSON_LIMIT || '10mb' }))
33
34
 
34
35
  const corsOptions = await this.#loadCorsOrCreateDefault();
35
36
 
36
37
  if(corsOptions) this.#app.use(cors(corsOptions));
37
38
 
38
- // Swagger setup
39
- const swaggerOptions = {
40
- ...(await this.#loadSwaggerOrCreateDefault()),
41
- };
39
+ // Swagger setup (optional). Can be fully disabled, or protected with
40
+ // HTTP Basic auth by setting SWAGGER_PASSWORD.
41
+ if (process.env.DISABLE_SWAGGER !== 'true') {
42
+ const swaggerOptions = {
43
+ ...(await this.#loadSwaggerOrCreateDefault()),
44
+ };
45
+
46
+ const swaggerDocs = swaggerJsDoc(swaggerOptions);
47
+ const swaggerPath = process?.env?.SWAGGER_PATH || "/docs";
48
+ const swaggerMiddlewares = [];
42
49
 
43
- const swaggerDocs = swaggerJsDoc(swaggerOptions);
44
- this.#app.use(process?.env?.SWAGGER_PATH || "/docs", swaggerUi.serve, swaggerUi.setup(swaggerDocs));
50
+ if (process.env.SWAGGER_PASSWORD) {
51
+ const basicAuth = require("express-basic-auth");
52
+ swaggerMiddlewares.push(basicAuth({
53
+ users: { [process.env.SWAGGER_USER || 'admin']: process.env.SWAGGER_PASSWORD },
54
+ challenge: true,
55
+ }));
56
+ } else {
57
+ this.#container.get('logger').warn(`Swagger docs are exposed without authentication at ${swaggerPath}. Set SWAGGER_PASSWORD to protect them or DISABLE_SWAGGER=true to disable.`);
58
+ }
59
+
60
+ this.#app.use(swaggerPath, ...swaggerMiddlewares, swaggerUi.serve, swaggerUi.setup(swaggerDocs));
61
+ }
45
62
 
46
63
  const routes = await this.#loadRoutesFromFolder(path.join(process.cwd(), "src", "routes"));
47
64
 
@@ -64,8 +81,8 @@ module.exports = class Express extends Service {
64
81
  try {
65
82
  await mw.handle({ container: this.#container, request: req, response: res, next })
66
83
  } catch (error) {
67
- this.#container.get('logger').error(`Error in middleware for route [${methodName}] ${route.path}: ${error.message}`);
68
- return res.status(500).json({ success: false, message: error.message || 'Internal Server Error' });
84
+ this.#container.get('logger').error(`Error in middleware for route [${methodName}] ${route.path}: ${error.stack || error.message}`);
85
+ return res.status(500).json({ success: false, message: 'Internal Server Error' });
69
86
  }
70
87
  }),
71
88
  async (req, res) => {
@@ -87,8 +104,8 @@ module.exports = class Express extends Service {
87
104
  success: routeResponse?.success !== false,
88
105
  });
89
106
  } catch (error) {
90
- this.#container.get('logger').error(`Error in route [${methodName}] ${route.path}: ${error.message}`);
91
- return res.status(500).json({ success: false, message: error.message || 'Internal Server Error' });
107
+ this.#container.get('logger').error(`Error in route [${methodName}] ${route.path}: ${error.stack || error.message}`);
108
+ return res.status(500).json({ success: false, message: 'Internal Server Error' });
92
109
  }
93
110
  });
94
111
  }
@@ -120,8 +137,8 @@ module.exports = class Express extends Service {
120
137
  try {
121
138
  await mw.handle({ container: this.#container, request: req, response: res, next })
122
139
  } catch (error) {
123
- this.#container.get('logger').error(`Error in middleware for route [${methodName}] ${route.path}: ${error.message}`);
124
- return res.status(500).json({ success: false, message: error.message || 'Internal Server Error' });
140
+ this.#container.get('logger').error(`Error in middleware for route [${methodName}] ${route.path}: ${error.stack || error.message}`);
141
+ return res.status(500).json({ success: false, message: 'Internal Server Error' });
125
142
  }
126
143
  }),
127
144
  async (req, res) => {
@@ -143,8 +160,8 @@ module.exports = class Express extends Service {
143
160
  success: routeResponse?.success !== false,
144
161
  });
145
162
  } catch (error) {
146
- this.#container.get('logger').error(`Error in route [${methodName}] ${route.path}: ${error.message}`);
147
- return res.status(500).json({ success: false, message: error.message || 'Internal Server Error' });
163
+ this.#container.get('logger').error(`Error in route [${methodName}] ${route.path}: ${error.stack || error.message}`);
164
+ return res.status(500).json({ success: false, message: 'Internal Server Error' });
148
165
  }
149
166
  });
150
167
  }
@@ -0,0 +1,14 @@
1
+ const RequireAuthMiddleware = require("./RequireAuthMiddleware");
2
+
3
+ /**
4
+ * Convenience middleware that requires an authenticated user with the
5
+ * `admin` role (better-auth admin plugin).
6
+ *
7
+ * Usage in a Route:
8
+ * middlewares = { delete: [ new RequireAdminMiddleware() ] }
9
+ */
10
+ module.exports = class RequireAdminMiddleware extends RequireAuthMiddleware {
11
+ constructor(adminRole = 'admin') {
12
+ super([adminRole]);
13
+ }
14
+ };
@@ -0,0 +1,46 @@
1
+ const Middleware = require("./Middleware");
2
+ const { fromNodeHeaders } = require("better-auth/node");
3
+
4
+ /**
5
+ * Blocks the request unless there is a valid better-auth session.
6
+ * Optionally restricts access to a set of roles (admin plugin).
7
+ *
8
+ * On success it attaches `request.user` and `request.session`.
9
+ *
10
+ * Usage in a Route:
11
+ * middlewares = { get: [ new RequireAuthMiddleware() ] }
12
+ * middlewares = { post: [ new RequireAuthMiddleware(['admin']) ] }
13
+ */
14
+ module.exports = class RequireAuthMiddleware extends Middleware {
15
+ constructor(roles = []) {
16
+ super();
17
+ this.roles = Array.isArray(roles) ? roles : [roles].filter(Boolean);
18
+ }
19
+
20
+ async handle({ container, request, response, next }) {
21
+ const auth = container.get('auth');
22
+ if (!auth || !auth.client) {
23
+ return response.status(500).json({ success: false, message: 'Auth service not available' });
24
+ }
25
+
26
+ const session = await auth.client.api.getSession({
27
+ headers: fromNodeHeaders(request.headers)
28
+ });
29
+
30
+ if (!session || !session.user) {
31
+ return response.status(401).json({ success: false, message: 'Unauthorized' });
32
+ }
33
+
34
+ if (this.roles.length > 0) {
35
+ const userRoles = String(session.user.role || '').split(',').map((r) => r.trim());
36
+ const allowed = this.roles.some((role) => userRoles.includes(role));
37
+ if (!allowed) {
38
+ return response.status(403).json({ success: false, message: 'Forbidden' });
39
+ }
40
+ }
41
+
42
+ request.user = session.user;
43
+ request.session = session.session;
44
+ next();
45
+ }
46
+ };
@@ -1,5 +1,7 @@
1
- module.exports = {
2
- Express: require("./Express"),
3
- Route: require("./Route"),
4
- Middleware: require("./Middleware"),
5
- }
1
+ module.exports = {
2
+ Express: require("./Express"),
3
+ Route: require("./Route"),
4
+ Middleware: require("./Middleware"),
5
+ RequireAuthMiddleware: require("./RequireAuthMiddleware"),
6
+ RequireAdminMiddleware: require("./RequireAdminMiddleware"),
7
+ }
@@ -0,0 +1,33 @@
1
+ const Guard = require("./Guard");
2
+ const { fromNodeHeaders } = require("better-auth/node");
3
+
4
+ /**
5
+ * Socket.IO guard that rejects the connection/event unless there is a valid
6
+ * better-auth session in the handshake. Throwing here blocks the event
7
+ * (see SocketIO service guard handling).
8
+ *
9
+ * On success it attaches the user to `socket.data.user`.
10
+ *
11
+ * Usage: create `src/guards/auth.js` with:
12
+ * module.exports = require('zyket').AuthGuard;
13
+ * and reference it from a handler: `guards = ["auth"];`
14
+ */
15
+ module.exports = class AuthGuard extends Guard {
16
+ async handle({ container, socket }) {
17
+ const auth = container.get('auth');
18
+ if (!auth || !auth.client) {
19
+ throw new Error('Auth service not available');
20
+ }
21
+
22
+ const session = await auth.client.api.getSession({
23
+ headers: fromNodeHeaders(socket.handshake.headers)
24
+ });
25
+
26
+ if (!session || !session.user) {
27
+ throw new Error('Unauthorized');
28
+ }
29
+
30
+ socket.data.user = session.user;
31
+ socket.data.session = session.session;
32
+ }
33
+ };
@@ -29,7 +29,9 @@ module.exports = class SocketIO extends Service {
29
29
  this.#container.get('logger').debug(`Guards: ${guards.map(mdl => mdl.name).join(", ")}`);
30
30
  this.#container.get('logger').debug(`Handlers: ${handlers.map(hdl => hdl.event).join(", ")}`);
31
31
 
32
- this.io = new Server({ cors: { origin: "*" }, maxHttpBufferSize: 10 * 1024 * 1024 });
32
+ // Configurable max payload size (defaults to 10mb) to reduce DoS surface.
33
+ const maxHttpBufferSize = Number(process.env.SOCKET_MAX_HTTP_BUFFER_SIZE) || 10 * 1024 * 1024;
34
+ this.io = new Server({ cors: { origin: "*" }, maxHttpBufferSize });
33
35
 
34
36
  if (process.env.REDIS_URL) {
35
37
  await this.#attachRedisAdapter();
@@ -1,5 +1,6 @@
1
1
  module.exports = {
2
2
  SocketIO: require("./SocketIO"),
3
3
  Handler: require("./Handler"),
4
- Guard: require("./Guard")
4
+ Guard: require("./Guard"),
5
+ AuthGuard: require("./AuthGuard")
5
6
  }
@@ -13,6 +13,7 @@ module.exports = class TemplateManager extends Service {
13
13
  async boot() {
14
14
  const zyketTemplates = await fg(['**/*'], {
15
15
  cwd: path.join(__dirname, '../../templates'),
16
+ dot: true, // include dotfiles like .env.example
16
17
  });
17
18
  console.log('Found templates:', zyketTemplates);
18
19
  for (const template of zyketTemplates) {
@@ -0,0 +1,24 @@
1
+ # --- api-rest template ---
2
+ # Zyket auto-generates a .env on first run; these are the relevant values.
3
+
4
+ DEBUG=true
5
+ PORT=3000
6
+
7
+ # Backend only: no socket, no frontend
8
+ DISABLE_EXPRESS=false
9
+ DISABLE_SOCKET=true
10
+ DISABLE_VITE=true
11
+
12
+ # Database (auth requires postgresql or sqlite)
13
+ DATABASE_URL=./database.sqlite
14
+ DATABASE_DIALECT=sqlite
15
+
16
+ # Payload limits (see SECURITY.md)
17
+ HTTP_JSON_LIMIT=10mb
18
+
19
+ # Protect Swagger docs in production
20
+ # SWAGGER_PASSWORD=change-me
21
+
22
+ # Auth — AUTH_SECRET is generated automatically on first run.
23
+ # BETTER_AUTH_URL=http://localhost:3000
24
+ # TRUSTED_ORIGINS=http://localhost:3000
@@ -0,0 +1,50 @@
1
+ # api-rest template
2
+
3
+ Backend-only REST API starter with email/password auth and a protected CRUD
4
+ resource (`Task`). No frontend, no sockets.
5
+
6
+ ## What's included
7
+ - `index.js` — boots the kernel (auth + database + express) and syncs the `Task` table.
8
+ - `src/services/auth/` — auth service (email/password, no organizations).
9
+ - `src/models/Task.js` — example Sequelize model.
10
+ - `src/routes/tasks/` — full CRUD, every endpoint protected with `RequireAuthMiddleware`.
11
+
12
+ ## Endpoints (all require a valid session)
13
+ | Method | Path | Description |
14
+ |--------|------|-------------|
15
+ | GET | `/tasks` | List tasks |
16
+ | POST | `/tasks` | Create a task `{ title, description? }` |
17
+ | GET | `/tasks/:id` | Get one task |
18
+ | PUT | `/tasks/:id` | Update a task |
19
+ | DELETE | `/tasks/:id` | Delete a task |
20
+
21
+ Auth endpoints are mounted by better-auth under `/api/auth/*`.
22
+
23
+ ## Setup
24
+ ```bash
25
+ npm install
26
+ # 1) Create the better-auth tables (uses src/services/auth/auth.js)
27
+ npx @better-auth/cli migrate
28
+ # 2) Run
29
+ node index.js
30
+ ```
31
+
32
+ The `Task` table is created automatically via `sequelize.sync()` on boot.
33
+
34
+ ## Auth quick test
35
+ ```bash
36
+ # Sign up
37
+ curl -X POST http://localhost:3000/api/auth/sign-up/email \
38
+ -H "Content-Type: application/json" \
39
+ -d '{"email":"me@example.com","password":"supersecret","name":"Me"}' -c cookies.txt
40
+
41
+ # Create a task with the session cookie
42
+ curl -X POST http://localhost:3000/tasks \
43
+ -H "Content-Type: application/json" -b cookies.txt \
44
+ -d '{"title":"My first task"}'
45
+ ```
46
+
47
+ ## Notes
48
+ - `RequireAuthMiddleware` attaches `request.user` / `request.session`. Restrict by role with `new RequireAuthMiddleware(['admin'])`.
49
+ - Email sending is stubbed (logged to console) — wire a real provider in `src/services/auth/index.js`.
50
+ - Review [SECURITY.md] in the framework for hardening (Swagger password, CORS, rate limiting).
@@ -0,0 +1,18 @@
1
+ const { Kernel } = require('zyket');
2
+
3
+ const kernel = new Kernel({
4
+ services: [
5
+ // The database and express services are auto-registered by the framework
6
+ // (they activate from your .env). We only register the auth service here.
7
+ ['auth', require('./src/services/auth'), ['@service_container']],
8
+ ],
9
+ });
10
+
11
+ kernel.boot().then(async () => {
12
+ // Create the example tables (Task). better-auth tables are created via the
13
+ // better-auth CLI migrate step (see README).
14
+ await kernel.container.get('database').sync();
15
+ kernel.container.get('logger').info('api-rest template ready — try GET /tasks');
16
+ }).catch((error) => {
17
+ console.error('Error booting kernel:', error);
18
+ });