millas 0.2.13 → 0.2.15

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/package.json +6 -3
  2. package/src/admin/Admin.js +107 -1027
  3. package/src/admin/AdminAuth.js +1 -1
  4. package/src/admin/ViewContext.js +1 -1
  5. package/src/admin/handlers/ActionHandler.js +103 -0
  6. package/src/admin/handlers/ApiHandler.js +113 -0
  7. package/src/admin/handlers/AuthHandler.js +76 -0
  8. package/src/admin/handlers/ExportHandler.js +70 -0
  9. package/src/admin/handlers/InlineHandler.js +71 -0
  10. package/src/admin/handlers/PageHandler.js +351 -0
  11. package/src/admin/resources/AdminResource.js +22 -1
  12. package/src/admin/static/SelectFilter2.js +34 -0
  13. package/src/admin/static/actions.js +201 -0
  14. package/src/admin/static/admin.css +7 -0
  15. package/src/admin/static/change_form.js +585 -0
  16. package/src/admin/static/core.js +128 -0
  17. package/src/admin/static/login.js +76 -0
  18. package/src/admin/static/vendor/bi/bootstrap-icons.min.css +5 -0
  19. package/src/admin/static/vendor/bi/fonts/bootstrap-icons.woff +0 -0
  20. package/src/admin/static/vendor/bi/fonts/bootstrap-icons.woff2 +0 -0
  21. package/src/admin/static/vendor/jquery.min.js +2 -0
  22. package/src/admin/views/layouts/base.njk +30 -113
  23. package/src/admin/views/pages/detail.njk +10 -9
  24. package/src/admin/views/pages/form.njk +4 -4
  25. package/src/admin/views/pages/list.njk +11 -193
  26. package/src/admin/views/pages/login.njk +19 -64
  27. package/src/admin/views/partials/form-field.njk +1 -1
  28. package/src/admin/views/partials/form-scripts.njk +4 -478
  29. package/src/admin/views/partials/form-widget.njk +10 -10
  30. package/src/ai/AITokenBudget.js +1 -1
  31. package/src/auth/Auth.js +112 -3
  32. package/src/auth/AuthMiddleware.js +18 -15
  33. package/src/auth/Hasher.js +15 -43
  34. package/src/cli.js +3 -0
  35. package/src/commands/call.js +190 -0
  36. package/src/commands/createsuperuser.js +3 -4
  37. package/src/commands/key.js +97 -0
  38. package/src/commands/make.js +16 -2
  39. package/src/commands/new.js +16 -1
  40. package/src/commands/serve.js +5 -5
  41. package/src/console/Command.js +337 -0
  42. package/src/console/CommandLoader.js +165 -0
  43. package/src/console/index.js +6 -0
  44. package/src/container/AppInitializer.js +48 -1
  45. package/src/container/Application.js +3 -1
  46. package/src/container/HttpServer.js +0 -1
  47. package/src/container/MillasConfig.js +48 -0
  48. package/src/controller/Controller.js +13 -11
  49. package/src/core/docs.js +6 -0
  50. package/src/core/foundation.js +8 -0
  51. package/src/core/http.js +20 -10
  52. package/src/core/validation.js +58 -27
  53. package/src/docs/Docs.js +268 -0
  54. package/src/docs/DocsServiceProvider.js +80 -0
  55. package/src/docs/SchemaInferrer.js +131 -0
  56. package/src/docs/handlers/ApiHandler.js +305 -0
  57. package/src/docs/handlers/PageHandler.js +47 -0
  58. package/src/docs/index.js +13 -0
  59. package/src/docs/resources/ApiResource.js +402 -0
  60. package/src/docs/static/docs.css +723 -0
  61. package/src/docs/static/docs.js +1181 -0
  62. package/src/encryption/Encrypter.js +381 -0
  63. package/src/facades/Auth.js +5 -2
  64. package/src/facades/Crypt.js +166 -0
  65. package/src/facades/Docs.js +43 -0
  66. package/src/facades/Mail.js +1 -1
  67. package/src/http/MillasRequest.js +7 -31
  68. package/src/http/RequestContext.js +11 -7
  69. package/src/http/SecurityBootstrap.js +24 -2
  70. package/src/http/Shape.js +168 -0
  71. package/src/http/adapters/ExpressAdapter.js +9 -5
  72. package/src/middleware/CorsMiddleware.js +3 -0
  73. package/src/middleware/ThrottleMiddleware.js +10 -7
  74. package/src/orm/model/Model.js +20 -2
  75. package/src/providers/EncryptionServiceProvider.js +66 -0
  76. package/src/router/MiddlewareRegistry.js +79 -54
  77. package/src/router/Route.js +9 -4
  78. package/src/router/RouteEntry.js +91 -0
  79. package/src/router/Router.js +71 -1
  80. package/src/scaffold/maker.js +138 -1
  81. package/src/scaffold/templates.js +12 -0
  82. package/src/serializer/Serializer.js +239 -0
  83. package/src/support/Str.js +1080 -0
  84. package/src/validation/BaseValidator.js +45 -5
  85. package/src/validation/Validator.js +67 -61
  86. package/src/validation/types.js +490 -0
  87. package/src/middleware/AuthMiddleware.js +0 -46
  88. package/src/middleware/MiddlewareRegistry.js +0 -106
@@ -198,13 +198,15 @@ module.exports = function (program) {
198
198
  program
199
199
  .command('serve')
200
200
  .description('Start the development server with hot reload')
201
- .option('-p, --port <port>', 'Port to listen on', '3000')
201
+ .option('-p, --port <port>', 'Port to listen on')
202
202
  .option('-h, --host <host>', 'Host to bind to', 'localhost')
203
203
  .option('--no-reload', 'Disable hot reload (run once, like production)')
204
204
  .action((options) => {
205
205
 
206
+ require('dotenv').config({ path: path.resolve(process.cwd(), '.env') });
207
+
206
208
  const restoreAfterPatch = patchConsole(Logger,"SystemOut")
207
- const appBootstrap = path.resolve(process.cwd(), 'bootstrap/app.js');
209
+ let appBootstrap = path.resolve(process.cwd(), 'bootstrap/app.js');
208
210
 
209
211
  if (!fs.existsSync(appBootstrap)) {
210
212
  process.stderr.write(chalk.red('\n ✖ No Millas project found here.\n'));
@@ -212,8 +214,7 @@ module.exports = function (program) {
212
214
  process.exit(1);
213
215
  }
214
216
 
215
-
216
- const publicPort = parseInt(options.port, 10) || 3000;
217
+ const publicPort = parseInt(options.port ||process.env.APP_PORT, 10) || 3000;
217
218
  const publicHost = options.host || 'localhost';
218
219
 
219
220
  const env = {
@@ -227,7 +228,6 @@ module.exports = function (program) {
227
228
  printBanner(publicHost, publicPort);
228
229
 
229
230
  if (options.reload !== false) {
230
-
231
231
  new HotReloader(appBootstrap, publicPort, publicHost).start();
232
232
  } else {
233
233
  try {
@@ -0,0 +1,337 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+
5
+ /**
6
+ * Command
7
+ *
8
+ * Base class for all custom Millas console commands.
9
+ * Mirrors Laravel's Artisan command / Django's BaseCommand.
10
+ *
11
+ * ── Defining a command ────────────────────────────────────────────────────────
12
+ *
13
+ * // app/commands/SendDigestCommand.js
14
+ * const { Command } = require('millas/console');
15
+ *
16
+ * class SendDigestCommand extends Command {
17
+ * static signature = 'email:digest';
18
+ * static description = 'Send the weekly digest email to all subscribers';
19
+ *
20
+ * // Optional: define arguments and options
21
+ * static args = [
22
+ * { name: 'type', description: 'Digest type (weekly|daily)', default: 'weekly' },
23
+ * ];
24
+ *
25
+ * static options = [
26
+ * { flag: '--dry-run', description: 'Preview without sending' },
27
+ * { flag: '--limit <n>', description: 'Max recipients', default: '100' },
28
+ * ];
29
+ *
30
+ * async handle() {
31
+ * const type = this.argument('type');
32
+ * const limit = this.option('limit');
33
+ * const dry = this.option('dryRun');
34
+ *
35
+ * this.info(`Sending ${type} digest to up to ${limit} users…`);
36
+ *
37
+ * if (dry) {
38
+ * this.warn('Dry run — no emails sent.');
39
+ * return;
40
+ * }
41
+ *
42
+ * // … your logic …
43
+ * this.success('Done!');
44
+ * }
45
+ * }
46
+ *
47
+ * module.exports = SendDigestCommand;
48
+ *
49
+ * ── Running ───────────────────────────────────────────────────────────────────
50
+ *
51
+ * millas call email:digest
52
+ * millas call email:digest weekly --limit 50 --dry-run
53
+ *
54
+ * ── Output helpers ────────────────────────────────────────────────────────────
55
+ *
56
+ * this.line(msg) — plain output
57
+ * this.info(msg) — cyan
58
+ * this.success(msg) — green ✔
59
+ * this.warn(msg) — yellow ⚠
60
+ * this.error(msg) — red ✖
61
+ * this.comment(msg) — dim / gray
62
+ * this.newLine() — blank line
63
+ * this.table(headers, rows) — formatted table
64
+ *
65
+ * ── Input helpers ─────────────────────────────────────────────────────────────
66
+ *
67
+ * this.argument(name) — positional arg value
68
+ * this.option(name) — option value (camelCase: --dry-run → dryRun)
69
+ * this.ask(question) — interactive prompt, returns Promise<string>
70
+ * this.secret(question) — hidden input (passwords), returns Promise<string>
71
+ * this.confirm(question, default?) — yes/no prompt, returns Promise<boolean>
72
+ *
73
+ * ── Exit ──────────────────────────────────────────────────────────────────────
74
+ *
75
+ * return; — success (exit 0)
76
+ * this.fail(msg) — print error + exit 1
77
+ */
78
+ class Command {
79
+ /**
80
+ * The CLI signature — used as the command name.
81
+ * Use colons for namespacing: 'email:digest', 'cache:clear', 'db:seed'
82
+ *
83
+ * @type {string}
84
+ */
85
+ static signature = '';
86
+
87
+ /**
88
+ * Short description shown in `millas --help` and `millas list`.
89
+ *
90
+ * @type {string}
91
+ */
92
+ static description = '';
93
+
94
+ /**
95
+ * Positional arguments.
96
+ * Each entry: { name: string, description?: string, default?: * }
97
+ *
98
+ * @type {Array<{ name: string, description?: string, default?: * }>}
99
+ */
100
+ static args = [];
101
+
102
+ /**
103
+ * Named options / flags.
104
+ * Each entry: { flag: string, description?: string, default?: * }
105
+ * flag examples: '--dry-run', '--limit <n>', '-f, --force'
106
+ *
107
+ * @type {Array<{ flag: string, description?: string, default?: * }>}
108
+ */
109
+ static options = [];
110
+
111
+ // ── Internal ───────────────────────────────────────────────────────────────
112
+
113
+ constructor() {
114
+ this._args = {};
115
+ this._opts = {};
116
+ }
117
+
118
+ /**
119
+ * Populate the command with parsed CLI values.
120
+ * Called by CommandLoader before handle().
121
+ *
122
+ * @param {object} args — { argName: value, … }
123
+ * @param {object} options — { optName: value, … }
124
+ * @internal
125
+ */
126
+ _hydrate(args, options) {
127
+ this._args = args || {};
128
+ this._opts = options || {};
129
+ }
130
+
131
+ // ── Input ──────────────────────────────────────────────────────────────────
132
+
133
+ /**
134
+ * Get a positional argument value by name.
135
+ *
136
+ * const name = this.argument('name');
137
+ *
138
+ * @param {string} name
139
+ * @returns {*}
140
+ */
141
+ argument(name) {
142
+ return this._args[name];
143
+ }
144
+
145
+ /**
146
+ * Get an option value by name (camelCase).
147
+ * Flags like --dry-run become dryRun.
148
+ *
149
+ * const limit = this.option('limit');
150
+ * const dry = this.option('dryRun');
151
+ *
152
+ * @param {string} name
153
+ * @returns {*}
154
+ */
155
+ option(name) {
156
+ return this._opts[name];
157
+ }
158
+
159
+ /**
160
+ * Prompt the user for input.
161
+ *
162
+ * const name = await this.ask('What is your name?');
163
+ *
164
+ * @param {string} question
165
+ * @returns {Promise<string>}
166
+ */
167
+ ask(question) {
168
+ return _prompt(question + ' ');
169
+ }
170
+
171
+ /**
172
+ * Prompt the user for a secret (input hidden — for passwords).
173
+ *
174
+ * const pass = await this.secret('Password:');
175
+ *
176
+ * @param {string} question
177
+ * @returns {Promise<string>}
178
+ */
179
+ secret(question) {
180
+ return _promptSecret(question + ' ');
181
+ }
182
+
183
+ /**
184
+ * Prompt for a yes/no confirmation.
185
+ *
186
+ * const ok = await this.confirm('Are you sure?');
187
+ * const ok = await this.confirm('Delete all records?', false);
188
+ *
189
+ * @param {string} question
190
+ * @param {boolean} [defaultValue=true]
191
+ * @returns {Promise<boolean>}
192
+ */
193
+ async confirm(question, defaultValue = true) {
194
+ const hint = defaultValue ? '(Y/n)' : '(y/N)';
195
+ const answer = await _prompt(`${question} ${hint} `);
196
+ const trimmed = (answer || '').trim().toLowerCase();
197
+ if (!trimmed) return defaultValue;
198
+ return trimmed === 'y' || trimmed === 'yes';
199
+ }
200
+
201
+ // ── Output ─────────────────────────────────────────────────────────────────
202
+
203
+ /**
204
+ * Write a plain line to stdout.
205
+ * @param {string} [msg='']
206
+ */
207
+ line(msg = '') {
208
+ process.stdout.write(msg + '\n');
209
+ }
210
+
211
+ /**
212
+ * Write a blank line.
213
+ */
214
+ newLine() {
215
+ process.stdout.write('\n');
216
+ }
217
+
218
+ /**
219
+ * Informational message (cyan).
220
+ * @param {string} msg
221
+ */
222
+ info(msg) {
223
+ this.line(chalk.cyan(` ${msg}`));
224
+ }
225
+
226
+ /**
227
+ * Success message (green ✔).
228
+ * @param {string} msg
229
+ */
230
+ success(msg) {
231
+ this.line(chalk.green(` ✔ ${msg}`));
232
+ }
233
+
234
+ /**
235
+ * Warning message (yellow ⚠).
236
+ * @param {string} msg
237
+ */
238
+ warn(msg) {
239
+ this.line(chalk.yellow(` ⚠ ${msg}`));
240
+ }
241
+
242
+ /**
243
+ * Error message (red ✖). Does NOT exit — use fail() to exit.
244
+ * @param {string} msg
245
+ */
246
+ error(msg) {
247
+ this.line(chalk.red(` ✖ ${msg}`));
248
+ }
249
+
250
+ /**
251
+ * Dimmed / comment message.
252
+ * @param {string} msg
253
+ */
254
+ comment(msg) {
255
+ this.line(chalk.dim(` // ${msg}`));
256
+ }
257
+
258
+ /**
259
+ * Print an error and exit with code 1.
260
+ * @param {string} msg
261
+ */
262
+ fail(msg) {
263
+ this.error(msg);
264
+ process.exit(1);
265
+ }
266
+
267
+ /**
268
+ * Render a simple table.
269
+ *
270
+ * this.table(
271
+ * ['ID', 'Name', 'Email'],
272
+ * [[1, 'Alice', 'alice@example.com'],
273
+ * [2, 'Bob', 'bob@example.com']],
274
+ * );
275
+ *
276
+ * @param {string[]} headers
277
+ * @param {Array[]} rows
278
+ */
279
+ table(headers, rows) {
280
+ if (!headers.length) return;
281
+
282
+ // Compute column widths
283
+ const widths = headers.map((h, i) => {
284
+ const colValues = [String(h), ...rows.map(r => String(r[i] ?? ''))];
285
+ return Math.max(...colValues.map(v => v.length));
286
+ });
287
+
288
+ const hr = ' ' + widths.map(w => '─'.repeat(w + 2)).join('┼') ;
289
+ const fmt = (cells, color) => ' ' + cells
290
+ .map((c, i) => color(String(c ?? '').padEnd(widths[i])))
291
+ .join(chalk.dim(' │ '));
292
+
293
+ this.newLine();
294
+ this.line(fmt(headers, chalk.bold));
295
+ this.line(chalk.dim(hr));
296
+ for (const row of rows) {
297
+ this.line(fmt(row, v => v));
298
+ }
299
+ this.newLine();
300
+ }
301
+
302
+ // ── Lifecycle ──────────────────────────────────────────────────────────────
303
+
304
+ /**
305
+ * The command's entry point. Override this in your command.
306
+ *
307
+ * @returns {Promise<void>}
308
+ */
309
+ async handle() {
310
+ throw new Error(`[Command] "${this.constructor.signature}" must implement handle().`);
311
+ }
312
+ }
313
+
314
+ // ── Prompt helpers ────────────────────────────────────────────────────────────
315
+
316
+ function _prompt(question) {
317
+ const readline = require('readline');
318
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
319
+ return new Promise(resolve => {
320
+ rl.question(question, answer => { rl.close(); resolve(answer); });
321
+ });
322
+ }
323
+
324
+ function _promptSecret(question) {
325
+ if (!process.stdin.isTTY) return _prompt(question);
326
+ return new Promise(resolve => {
327
+ process.stdout.write(question);
328
+ const rl = require('readline').createInterface({
329
+ input: process.stdin,
330
+ output: new (require('stream').Writable)({ write(c, e, cb) { cb(); } }),
331
+ terminal: true,
332
+ });
333
+ rl.question('', answer => { rl.close(); process.stdout.write('\n'); resolve(answer); });
334
+ });
335
+ }
336
+
337
+ module.exports = Command;
@@ -0,0 +1,165 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * CommandLoader
8
+ *
9
+ * Discovers all custom commands in app/commands/ and registers
10
+ * them as Commander sub-commands under `millas call <signature>`.
11
+ *
12
+ * Each file must export a class that extends Command:
13
+ *
14
+ * module.exports = SendDigestCommand;
15
+ *
16
+ * CommandLoader reads the static `signature`, `description`, `args`,
17
+ * and `options` properties to wire up Commander automatically —
18
+ * no manual registration needed.
19
+ */
20
+ class CommandLoader {
21
+ /**
22
+ * @param {string} commandsDir — absolute path to app/commands/
23
+ */
24
+ constructor(commandsDir) {
25
+ this._dir = commandsDir;
26
+ this._commands = new Map(); // signature → CommandClass
27
+ }
28
+
29
+ /**
30
+ * Scan commandsDir and load all valid Command subclasses.
31
+ * Files that throw on require() are reported as warnings — never silently skipped.
32
+ * Files that don't export a valid Command subclass are skipped with a clear message.
33
+ *
34
+ * @returns {Map<string, typeof Command>}
35
+ */
36
+ load() {
37
+ if (!fs.existsSync(this._dir)) return this._commands;
38
+
39
+ const chalk = require('chalk');
40
+
41
+ const files = fs.readdirSync(this._dir)
42
+ .filter(f => f.endsWith('.js') && !f.startsWith('.') && !f.startsWith('_'));
43
+
44
+ for (const file of files) {
45
+ const filePath = path.join(this._dir, file);
46
+
47
+ let CommandClass;
48
+ try {
49
+ CommandClass = require(filePath);
50
+ } catch (err) {
51
+ process.stderr.write(
52
+ chalk.red(`\n ✖ Failed to load command file: ${chalk.bold(file)}\n`) +
53
+ chalk.dim(` ${err.message}\n`) +
54
+ (process.env.DEBUG ? chalk.dim(err.stack + '\n') : '') +
55
+ '\n'
56
+ );
57
+ continue;
58
+ }
59
+
60
+ if (!CommandClass || typeof CommandClass !== 'function') {
61
+ process.stderr.write(
62
+ chalk.yellow(` ⚠ Skipping ${chalk.bold(file)} — does not export a class.\n`)
63
+ );
64
+ continue;
65
+ }
66
+
67
+ if (!CommandClass.signature) {
68
+ process.stderr.write(
69
+ chalk.yellow(` ⚠ Skipping ${chalk.bold(file)} — missing static signature.\n`)
70
+ );
71
+ continue;
72
+ }
73
+
74
+ if (typeof CommandClass.prototype.handle !== 'function') {
75
+ process.stderr.write(
76
+ chalk.yellow(` ⚠ Skipping ${chalk.bold(file)} — missing handle() method.\n`)
77
+ );
78
+ continue;
79
+ }
80
+
81
+ this._commands.set(CommandClass.signature, CommandClass);
82
+ }
83
+
84
+ return this._commands;
85
+ }
86
+
87
+ /**
88
+ * Register all loaded commands onto a Commander `program` instance.
89
+ * Called by the `millas call` command to dynamically attach sub-commands.
90
+ *
91
+ * @param {import('commander').Command} program
92
+ */
93
+ register(program) {
94
+ this.load();
95
+
96
+ for (const [, CommandClass] of this._commands) {
97
+ this._registerOne(program, CommandClass);
98
+ }
99
+ }
100
+
101
+ // ── Internal ───────────────────────────────────────────────────────────────
102
+
103
+ _registerOne(program, CommandClass) {
104
+ const sig = CommandClass.signature;
105
+ const desc = CommandClass.description || '';
106
+ const args = CommandClass.args || [];
107
+ const opts = CommandClass.options || [];
108
+
109
+ // Build Commander argument string: <required> or [optional]
110
+ const argStr = args
111
+ .map(a => a.default !== undefined ? `[${a.name}]` : `<${a.name}>`)
112
+ .join(' ');
113
+
114
+ const fullSig = argStr ? `${sig} ${argStr}` : sig;
115
+
116
+ let cmd = program.command(fullSig).description(desc);
117
+
118
+ // Register each option
119
+ for (const opt of opts) {
120
+ if (opt.default !== undefined) {
121
+ cmd = cmd.option(opt.flag, opt.description || '', opt.default);
122
+ } else {
123
+ cmd = cmd.option(opt.flag, opt.description || '');
124
+ }
125
+ }
126
+
127
+ // Action handler
128
+ cmd.action(async (...cliArgs) => {
129
+ // Commander passes positional args first, then the options object last
130
+ const options = cliArgs[cliArgs.length - 1];
131
+ const posArgs = cliArgs.slice(0, cliArgs.length - 1);
132
+
133
+ // Build named arg map
134
+ const argMap = {};
135
+ for (let i = 0; i < args.length; i++) {
136
+ argMap[args[i].name] = posArgs[i] !== undefined
137
+ ? posArgs[i]
138
+ : args[i].default;
139
+ }
140
+
141
+ const instance = new CommandClass();
142
+ instance._hydrate(argMap, options);
143
+
144
+ try {
145
+ await instance.handle();
146
+ } catch (err) {
147
+ const chalk = require('chalk');
148
+ process.stderr.write(chalk.red(`\n ✖ ${sig} failed: ${err.message}\n`));
149
+ if (process.env.DEBUG) process.stderr.write(err.stack + '\n');
150
+ process.exit(1);
151
+ }
152
+ });
153
+ }
154
+
155
+ /**
156
+ * Return all loaded command signatures.
157
+ * @returns {string[]}
158
+ */
159
+ signatures() {
160
+ this.load();
161
+ return [...this._commands.keys()];
162
+ }
163
+ }
164
+
165
+ module.exports = CommandLoader;
@@ -0,0 +1,6 @@
1
+ 'use strict';
2
+
3
+ const Command = require('./Command');
4
+ const CommandLoader = require('./CommandLoader');
5
+
6
+ module.exports = { Command, CommandLoader };
@@ -55,7 +55,10 @@ class AppInitializer {
55
55
  */
56
56
  async boot() {
57
57
  await this.bootKernel();
58
- await this._serve();
58
+ if (!process.env.MILLAS_CLI_MODE) {
59
+ await this._serve();
60
+ }
61
+ return this._kernel;
59
62
  }
60
63
 
61
64
  /**
@@ -82,6 +85,18 @@ class AppInitializer {
82
85
  const basePath = cfg.basePath || process.cwd();
83
86
  const appConfig = SecurityBootstrap.loadConfig(basePath + '/config/app');
84
87
  SecurityBootstrap.apply(this._adapter.nativeApp || expressApp, appConfig);
88
+
89
+ // ── CORS — applied immediately after security, before routes ──────────
90
+ // Only active when .withCors() was called in bootstrap/app.js.
91
+ // Config is read from the cors: {} block in config/app.js (already loaded
92
+ // above as appConfig). No cors key = CorsMiddleware class defaults apply.
93
+ if (cfg.cors !== null && cfg.cors !== undefined) {
94
+ const CorsMiddleware = require('../middleware/CorsMiddleware');
95
+ const corsMiddleware = new CorsMiddleware(appConfig.cors || {});
96
+ (this._adapter.nativeApp || expressApp).use(
97
+ this._adapter.wrapMiddleware(corsMiddleware, null)
98
+ );
99
+ }
85
100
  // ─────────────────────────────────────────────────────────────────────
86
101
 
87
102
  for (const mw of (cfg.adapterMiddleware || [])) {
@@ -130,6 +145,23 @@ class AppInitializer {
130
145
  }
131
146
  }
132
147
 
148
+ // ── Docs panel ────────────────────────────────────────────────
149
+ if (cfg.docs !== null && cfg.docs !== undefined) {
150
+ try {
151
+ const Docs = require('../docs/Docs');
152
+ // Wire live RouteRegistry now that routes are fully mounted
153
+ try {
154
+ Docs.setRouteRegistry(this._kernel.route.getRegistry());
155
+ } catch {}
156
+ if (cfg.docs && Object.keys(cfg.docs).length) {
157
+ Docs.configure(cfg.docs);
158
+ }
159
+ Docs.mount(this._adapter.nativeApp);
160
+ } catch (err) {
161
+ process.stderr.write(`[millas] Docs mount failed: ${err.message}\n`);
162
+ }
163
+ }
164
+
133
165
  this._kernel.mountFallbacks();
134
166
 
135
167
  const server = new HttpServer(this._kernel, {
@@ -178,6 +210,12 @@ class AppInitializer {
178
210
  if (p) providers.push(p);
179
211
  }
180
212
 
213
+ // ── 4b. Docs — on when .withDocs() was called ────────────────────────
214
+ if (cfg.docs !== null && cfg.docs !== undefined) {
215
+ const p = load('../docs/DocsServiceProvider');
216
+ if (p) providers.push(p);
217
+ }
218
+
181
219
  // ── 5. Cache + Storage ───────────────────────────────────────────────
182
220
  if (cfg.cache !== false || cfg.storage !== false) {
183
221
  const p = load('../providers/CacheStorageServiceProvider');
@@ -208,6 +246,15 @@ class AppInitializer {
208
246
  // ── 9. i18n — opt-in via config/app.js use_i18n: true ───────────────
209
247
  // Mirrors Django's USE_I18N = True in settings.py.
210
248
  // Booted last so translations are available in all request handlers.
249
+ // ── 9. Encryption — always on (APP_KEY drives it) ────────────────────
250
+ // Mirrors Laravel: the encrypter is always bound so Crypt / Encrypt
251
+ // facades work out of the box. If APP_KEY is absent a clear error is
252
+ // thrown on first use, not at boot — apps without encryption still start.
253
+ {
254
+ const p = load('../providers/EncryptionServiceProvider');
255
+ if (p) providers.push(p);
256
+ }
257
+
211
258
  if (this._resolveI18nEnabled(cfg)) {
212
259
  const p = load('../i18n/I18nServiceProvider');
213
260
  if (p) providers.push(p);
@@ -304,7 +304,9 @@ class Application {
304
304
  this._container.instance('url', urlGenerator);
305
305
 
306
306
  const { HashManager } = require('../hashing/Hash');
307
- const hashManager = new HashManager({ default: 'bcrypt', bcrypt: { rounds: 12 } });
307
+ let _hashRounds = 12;
308
+ try { const _appCfg = require(basePath + '/config/app'); if (_appCfg.hashing?.rounds) _hashRounds = _appCfg.hashing.rounds; } catch {}
309
+ const hashManager = new HashManager({ default: 'bcrypt', bcrypt: { rounds: _hashRounds } });
308
310
  this._container.instance('hash', hashManager);
309
311
 
310
312
  const ProcessManager = require('../process/Process').ProcessManager;
@@ -92,7 +92,6 @@ class HttpServer {
92
92
  this._options.port ||
93
93
  parseInt(process.env.MILLAS_INTERNAL_PORT, 10) ||
94
94
  parseInt(process.env.APP_PORT, 10) ||
95
- parseInt(process.env.PORT, 10) ||
96
95
  3000;
97
96
 
98
97
  return this._normalizePort(raw);