millas 0.2.12-beta-2 → 0.2.13-beta

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 (57) hide show
  1. package/package.json +3 -2
  2. package/src/admin/Admin.js +122 -38
  3. package/src/admin/ViewContext.js +12 -3
  4. package/src/admin/resources/AdminResource.js +10 -0
  5. package/src/admin/static/admin.css +95 -14
  6. package/src/admin/views/layouts/base.njk +23 -34
  7. package/src/admin/views/pages/detail.njk +16 -5
  8. package/src/admin/views/pages/error.njk +65 -0
  9. package/src/admin/views/pages/list.njk +127 -2
  10. package/src/admin/views/partials/form-scripts.njk +7 -3
  11. package/src/admin/views/partials/form-widget.njk +2 -1
  12. package/src/admin/views/partials/icons.njk +64 -0
  13. package/src/ai/AIManager.js +954 -0
  14. package/src/ai/AITokenBudget.js +250 -0
  15. package/src/ai/PromptGuard.js +216 -0
  16. package/src/ai/agents.js +218 -0
  17. package/src/ai/conversation.js +213 -0
  18. package/src/ai/drivers.js +734 -0
  19. package/src/ai/files.js +249 -0
  20. package/src/ai/media.js +303 -0
  21. package/src/ai/pricing.js +152 -0
  22. package/src/ai/provider_tools.js +114 -0
  23. package/src/ai/types.js +356 -0
  24. package/src/commands/createsuperuser.js +17 -4
  25. package/src/commands/serve.js +2 -4
  26. package/src/container/AppInitializer.js +39 -15
  27. package/src/container/Application.js +31 -1
  28. package/src/core/foundation.js +1 -1
  29. package/src/errors/HttpError.js +32 -16
  30. package/src/facades/AI.js +411 -0
  31. package/src/facades/Hash.js +67 -0
  32. package/src/facades/Process.js +144 -0
  33. package/src/hashing/Hash.js +262 -0
  34. package/src/http/HtmlEscape.js +162 -0
  35. package/src/http/MillasRequest.js +63 -7
  36. package/src/http/MillasResponse.js +70 -4
  37. package/src/http/ResponseDispatcher.js +21 -27
  38. package/src/http/SafeFilePath.js +195 -0
  39. package/src/http/SafeRedirect.js +62 -0
  40. package/src/http/SecurityBootstrap.js +70 -0
  41. package/src/http/helpers.js +40 -125
  42. package/src/http/index.js +10 -1
  43. package/src/http/middleware/CsrfMiddleware.js +258 -0
  44. package/src/http/middleware/RateLimiter.js +314 -0
  45. package/src/http/middleware/SecurityHeaders.js +281 -0
  46. package/src/i18n/Translator.js +10 -2
  47. package/src/logger/LogRedactor.js +247 -0
  48. package/src/logger/Logger.js +1 -1
  49. package/src/logger/formatters/JsonFormatter.js +11 -4
  50. package/src/logger/formatters/PrettyFormatter.js +3 -1
  51. package/src/logger/formatters/SimpleFormatter.js +14 -3
  52. package/src/middleware/ThrottleMiddleware.js +27 -4
  53. package/src/process/Process.js +333 -0
  54. package/src/router/MiddlewareRegistry.js +27 -2
  55. package/src/scaffold/templates.js +3 -0
  56. package/src/validation/Validator.js +348 -607
  57. package/src/admin.zip +0 -0
@@ -0,0 +1,333 @@
1
+ 'use strict';
2
+
3
+ const { spawn, execSync } = require('child_process');
4
+
5
+ // ── ProcessResult ─────────────────────────────────────────────────────────────
6
+
7
+ class ProcessResult {
8
+ constructor({ exitCode, stdout, stderr, command }) {
9
+ this._exitCode = exitCode;
10
+ this._stdout = stdout || '';
11
+ this._stderr = stderr || '';
12
+ this._command = command || '';
13
+ }
14
+
15
+ /** Raw stdout string. */
16
+ output() { return this._stdout; }
17
+
18
+ /** Raw stderr string. */
19
+ errorOutput() { return this._stderr; }
20
+
21
+ /** Exit code. */
22
+ exitCode() { return this._exitCode; }
23
+
24
+ /** True if exit code is 0. */
25
+ get successful() { return this._exitCode === 0; }
26
+
27
+ /** True if exit code is not 0. */
28
+ get failed() { return this._exitCode !== 0; }
29
+
30
+ /** Throw ProcessFailedException if the process failed. */
31
+ throw() {
32
+ if (this.failed) throw new ProcessFailedException(this);
33
+ return this;
34
+ }
35
+
36
+ /** Alias for throw(). */
37
+ throwIfFailed() { return this.throw(); }
38
+
39
+ toString() { return this._stdout; }
40
+ }
41
+
42
+ // ── ProcessFailedException ────────────────────────────────────────────────────
43
+
44
+ class ProcessFailedException extends Error {
45
+ constructor(result) {
46
+ super(
47
+ `Process "${result._command}" failed with exit code ${result._exitCode}.\n` +
48
+ (result._stderr ? `STDERR: ${result._stderr.trim()}` : '')
49
+ );
50
+ this.name = 'ProcessFailedException';
51
+ this.result = result;
52
+ }
53
+ }
54
+
55
+ // ── PendingProcess ────────────────────────────────────────────────────────────
56
+
57
+ class PendingProcess {
58
+ constructor() {
59
+ this._cwd = process.cwd();
60
+ this._env = null;
61
+ this._timeout = 0;
62
+ this._input = null;
63
+ this._quietly = false;
64
+ this._shell = false;
65
+ this._throwOnFail = false;
66
+ this._onOutput = null;
67
+ this._onError = null;
68
+ }
69
+
70
+ // ── Builder ────────────────────────────────────────────────────────────────
71
+
72
+ /**
73
+ * Set working directory.
74
+ * Process.path('/var/www').run('ls -la')
75
+ */
76
+ path(dir) { this._cwd = dir; return this; }
77
+
78
+ /**
79
+ * Set environment variables, merged with parent env by default.
80
+ * Pass inherit=false to use only the provided vars.
81
+ * Process.env({ NODE_ENV: 'production' }).run('node server.js')
82
+ */
83
+ env(vars, inherit = true) {
84
+ this._env = inherit ? { ...process.env, ...vars } : vars;
85
+ return this;
86
+ }
87
+
88
+ /**
89
+ * Timeout in seconds. Kills the process if exceeded.
90
+ * Process.timeout(30).run('npm install')
91
+ */
92
+ timeout(seconds) { this._timeout = seconds * 1000; return this; }
93
+
94
+ /**
95
+ * Pipe data to stdin.
96
+ * Process.input('hello').run('cat')
97
+ */
98
+ input(data) { this._input = data; return this; }
99
+
100
+ /**
101
+ * Suppress stdout/stderr passthrough to parent process.
102
+ * Output is still captured on ProcessResult.
103
+ */
104
+ quietly() { this._quietly = true; return this; }
105
+
106
+ /**
107
+ * Run through /bin/sh — enables pipes, &&, ||, globs.
108
+ * Process.shell().run('ls | grep .js && echo done')
109
+ */
110
+ shell() { this._shell = true; return this; }
111
+
112
+ /**
113
+ * Throw ProcessFailedException automatically on non-zero exit.
114
+ * await Process.throwOnFailure().run('npm test')
115
+ */
116
+ throwOnFailure() { this._throwOnFail = true; return this; }
117
+
118
+ /**
119
+ * Register a line-by-line callback for stdout.
120
+ * Called for each newline-delimited line as the process runs.
121
+ * Can be combined with .run() — no need to use .pipe().
122
+ *
123
+ * await Process
124
+ * .onOutput(line => console.log('[OUT]', line))
125
+ * .onError(line => console.error('[ERR]', line))
126
+ * .run('npm test');
127
+ */
128
+ onOutput(fn) { this._onOutput = fn; return this; }
129
+
130
+ /**
131
+ * Register a line-by-line callback for stderr.
132
+ * .onError(line => logger.error(line))
133
+ */
134
+ onError(fn) { this._onError = fn; return this; }
135
+
136
+ // ── Execution ──────────────────────────────────────────────────────────────
137
+
138
+ /**
139
+ * Run asynchronously. Returns Promise<ProcessResult>.
140
+ *
141
+ * const result = await Process.run('node --version');
142
+ * const result = await Process.path('/app').timeout(60).quietly().run('npm ci');
143
+ */
144
+ run(command) {
145
+ return new Promise((resolve, reject) => {
146
+ const { cmd, args } = this._parse(command);
147
+ const child = spawn(cmd, args, {
148
+ cwd: this._cwd,
149
+ env: this._env || process.env,
150
+ shell: this._shell,
151
+ ...(this._timeout ? { timeout: this._timeout } : {}),
152
+ });
153
+
154
+ let stdout = '';
155
+ let stderr = '';
156
+
157
+ if (this._input !== null) {
158
+ child.stdin.write(String(this._input));
159
+ child.stdin.end();
160
+ }
161
+
162
+ let _outBuf = '';
163
+ let _errBuf = '';
164
+
165
+ const _flushLines = (buf, cb) => {
166
+ const lines = buf.split('\n');
167
+ buf = lines.pop();
168
+ lines.forEach(line => cb && cb(line));
169
+ return buf;
170
+ };
171
+
172
+ child.stdout.on('data', chunk => {
173
+ stdout += chunk;
174
+ if (!this._quietly) process.stdout.write(chunk);
175
+ if (this._onOutput) _outBuf = _flushLines(_outBuf + chunk, this._onOutput);
176
+ });
177
+ child.stderr.on('data', chunk => {
178
+ stderr += chunk;
179
+ if (!this._quietly) process.stderr.write(chunk);
180
+ if (this._onError) _errBuf = _flushLines(_errBuf + chunk, this._onError);
181
+ });
182
+
183
+ child.on('error', reject);
184
+ child.on('close', code => {
185
+ if (_outBuf && this._onOutput) this._onOutput(_outBuf);
186
+ if (_errBuf && this._onError) this._onError(_errBuf);
187
+ const result = new ProcessResult({ exitCode: code ?? 1, stdout, stderr, command });
188
+ if (this._throwOnFail && result.failed) return reject(new ProcessFailedException(result));
189
+ resolve(result);
190
+ });
191
+ });
192
+ }
193
+
194
+ /**
195
+ * Stream output line-by-line in real-time via callbacks.
196
+ * Returns Promise<ProcessResult>.
197
+ *
198
+ * await Process.pipe('npm install', {
199
+ * stdout: line => console.log('[out]', line),
200
+ * stderr: line => console.error('[err]', line),
201
+ * });
202
+ */
203
+ pipe(command, { stdout: onOut, stderr: onErr } = {}) {
204
+ return new Promise((resolve, reject) => {
205
+ const { cmd, args } = this._parse(command);
206
+ const child = spawn(cmd, args, {
207
+ cwd: this._cwd,
208
+ env: this._env || process.env,
209
+ shell: this._shell,
210
+ });
211
+
212
+ let outBuf = '', errBuf = '', fullOut = '', fullErr = '';
213
+
214
+ const flush = (buf, cb) => {
215
+ const lines = buf.split('\n');
216
+ const rem = lines.pop();
217
+ lines.forEach(l => cb && cb(l));
218
+ return rem;
219
+ };
220
+
221
+ child.stdout.on('data', chunk => {
222
+ fullOut += chunk; outBuf += chunk;
223
+ outBuf = flush(outBuf, onOut);
224
+ if (!this._quietly) process.stdout.write(chunk);
225
+ });
226
+ child.stderr.on('data', chunk => {
227
+ fullErr += chunk; errBuf += chunk;
228
+ errBuf = flush(errBuf, onErr);
229
+ if (!this._quietly) process.stderr.write(chunk);
230
+ });
231
+
232
+ child.on('error', reject);
233
+ child.on('close', code => {
234
+ if (outBuf) onOut && onOut(outBuf);
235
+ if (errBuf) onErr && onErr(errBuf);
236
+ const result = new ProcessResult({ exitCode: code ?? 1, stdout: fullOut, stderr: fullErr, command });
237
+ if (this._throwOnFail && result.failed) return reject(new ProcessFailedException(result));
238
+ resolve(result);
239
+ });
240
+ });
241
+ }
242
+
243
+ /**
244
+ * Run synchronously (blocking). Returns ProcessResult directly.
245
+ *
246
+ * const result = Process.quietly().runSync('node --version');
247
+ */
248
+ runSync(command) {
249
+ try {
250
+ const out = execSync(command, {
251
+ cwd: this._cwd,
252
+ env: this._env || process.env,
253
+ timeout: this._timeout || undefined,
254
+ input: this._input !== null ? String(this._input) : undefined,
255
+ stdio: 'pipe',
256
+ });
257
+ const result = new ProcessResult({ exitCode: 0, stdout: out.toString(), stderr: '', command });
258
+ if (!this._quietly) process.stdout.write(out);
259
+ return result;
260
+ } catch (err) {
261
+ const result = new ProcessResult({
262
+ exitCode: err.status ?? 1,
263
+ stdout: err.stdout?.toString() || '',
264
+ stderr: err.stderr?.toString() || err.message || '',
265
+ command,
266
+ });
267
+ if (this._throwOnFail) throw new ProcessFailedException(result);
268
+ return result;
269
+ }
270
+ }
271
+
272
+ // ── Internal ───────────────────────────────────────────────────────────────
273
+
274
+ _parse(command) {
275
+ if (this._shell) return { cmd: command, args: [] };
276
+ const parts = command.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
277
+ const cmd = parts[0];
278
+ const args = parts.slice(1).map(a => a.replace(/^["']|["']$/g, ''));
279
+ return { cmd, args };
280
+ }
281
+ }
282
+
283
+ // ── ProcessPool ───────────────────────────────────────────────────────────────
284
+
285
+ class ProcessPool {
286
+ async start(callback) {
287
+ const factory = new PendingProcess();
288
+ const tasks = callback(factory) || [];
289
+ return Promise.all(tasks);
290
+ }
291
+ }
292
+
293
+ // ── ProcessManager ────────────────────────────────────────────────────────────
294
+
295
+ class ProcessManager {
296
+ // Shorthand — execute immediately
297
+ run(command) { return new PendingProcess().run(command); }
298
+ runSync(command) { return new PendingProcess().runSync(command); }
299
+ pipe(command, handlers) { return new PendingProcess().pipe(command, handlers); }
300
+ onOutput(fn) { return new PendingProcess().onOutput(fn); }
301
+ onError(fn) { return new PendingProcess().onError(fn); }
302
+
303
+ // Builder starters — return PendingProcess for chaining
304
+ path(dir) { return new PendingProcess().path(dir); }
305
+ env(vars, inherit) { return new PendingProcess().env(vars, inherit); }
306
+ timeout(seconds) { return new PendingProcess().timeout(seconds); }
307
+ input(data) { return new PendingProcess().input(data); }
308
+ quietly() { return new PendingProcess().quietly(); }
309
+ shell() { return new PendingProcess().shell(); }
310
+ throwOnFailure() { return new PendingProcess().throwOnFailure(); }
311
+
312
+ /**
313
+ * Run multiple processes concurrently.
314
+ *
315
+ * const [lint, test, build] = await Process.pool(pool => [
316
+ * pool.quietly().run('npm run lint'),
317
+ * pool.quietly().run('npm test'),
318
+ * pool.quietly().run('npm run build'),
319
+ * ]);
320
+ */
321
+ pool(callback) { return new ProcessPool().start(callback); }
322
+ }
323
+
324
+ // ── Singleton ─────────────────────────────────────────────────────────────────
325
+
326
+ const defaultProcess = new ProcessManager();
327
+
328
+ module.exports = defaultProcess;
329
+ module.exports.ProcessManager = ProcessManager;
330
+ module.exports.PendingProcess = PendingProcess;
331
+ module.exports.ProcessResult = ProcessResult;
332
+ module.exports.ProcessPool = ProcessPool;
333
+ module.exports.ProcessFailedException = ProcessFailedException;
@@ -27,17 +27,42 @@ class MiddlewareRegistry {
27
27
 
28
28
  /**
29
29
  * Resolve a single alias to an Express-compatible function.
30
+ * Supports parameterised aliases: 'throttle:60,1' → 60 req per 1 minute.
31
+ *
30
32
  * @param {string|Function} aliasOrFn
31
33
  * @returns {Function}
32
34
  */
33
35
  resolve(aliasOrFn) {
34
36
  if (typeof aliasOrFn === 'function') return aliasOrFn;
35
37
 
36
- const Handler = this._map[aliasOrFn];
38
+ // Parse parameterised alias: 'throttle:60,1' → alias='throttle', params=['60','1']
39
+ let alias = aliasOrFn;
40
+ let params = [];
41
+ if (typeof aliasOrFn === 'string' && aliasOrFn.includes(':')) {
42
+ const colonIdx = aliasOrFn.indexOf(':');
43
+ alias = aliasOrFn.slice(0, colonIdx);
44
+ params = aliasOrFn.slice(colonIdx + 1).split(',').map(s => s.trim());
45
+ }
46
+
47
+ const Handler = this._map[alias];
37
48
  if (!Handler) {
38
49
  throw new Error(`Middleware "${aliasOrFn}" is not registered.`);
39
50
  }
40
51
 
52
+ // If params provided, instantiate the class with them via fromParams() or constructor
53
+ if (params.length > 0) {
54
+ if (typeof Handler === 'function' && Handler.prototype &&
55
+ typeof Handler.prototype.handle === 'function') {
56
+ const instance = typeof Handler.fromParams === 'function'
57
+ ? Handler.fromParams(params)
58
+ : new Handler(...params);
59
+ return (req, res, next) => {
60
+ const result = instance.handle(req, res, next);
61
+ if (result && typeof result.catch === 'function') result.catch(next);
62
+ };
63
+ }
64
+ }
65
+
41
66
  // Pre-instantiated object with handle() method (e.g. new ThrottleMiddleware())
42
67
  if (typeof Handler === 'object' && Handler !== null && typeof Handler.handle === 'function') {
43
68
  return (req, res, next) => {
@@ -79,4 +104,4 @@ class MiddlewareRegistry {
79
104
  }
80
105
  }
81
106
 
82
- module.exports = MiddlewareRegistry;
107
+ module.exports = MiddlewareRegistry;
@@ -28,6 +28,7 @@ function getProjectFiles(projectName) {
28
28
  APP_ENV=development
29
29
  APP_PORT=3000
30
30
  APP_KEY=
31
+ APP_URL=http://localhost:3000
31
32
 
32
33
  DB_CONNECTION=sqlite
33
34
  DB_HOST=127.0.0.1
@@ -71,6 +72,7 @@ database/database.sqlite
71
72
  `,
72
73
 
73
74
  // ─── bootstrap/app.js ─────────────────────────────────────────
75
+
74
76
  'bootstrap/app.js': `'use strict';
75
77
 
76
78
  require('dotenv').config();
@@ -150,6 +152,7 @@ module.exports = {
150
152
  env: process.env.APP_ENV || 'development',
151
153
  port: parseInt(process.env.APP_PORT, 10) || 3000,
152
154
  key: process.env.APP_KEY || '',
155
+ url: process.env.APP_URL || 'http://localhost:3000',
153
156
  debug: process.env.APP_ENV !== 'production',
154
157
  timezone: 'UTC',
155
158
  locale: 'en',