webspresso 0.0.99 → 0.0.101

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 (108) hide show
  1. package/.agents/ADMIN.md +47 -4
  2. package/.agents/AGENTS.md +4 -2
  3. package/.agents/AUTH.md +309 -0
  4. package/.agents/CLI.md +128 -0
  5. package/.agents/EMAIL.md +84 -2
  6. package/.agents/ERRORS.md +59 -0
  7. package/.agents/EVENTS.md +179 -0
  8. package/.agents/ORM.md +149 -0
  9. package/.agents/PLUGINS.md +44 -7
  10. package/.agents/QUEUE.md +18 -10
  11. package/.agents/ROUTING.md +97 -1
  12. package/.agents/STREAMING.md +35 -0
  13. package/AGENTS.md +2 -0
  14. package/README.md +145 -93
  15. package/bin/commands/add-docker.js +467 -0
  16. package/bin/commands/db-makemigrations.js +83 -0
  17. package/bin/commands/deploy-check.js +339 -0
  18. package/bin/commands/list.js +96 -0
  19. package/bin/commands/make-command.js +93 -0
  20. package/bin/commands/make-event.js +80 -0
  21. package/bin/commands/make-factory.js +72 -0
  22. package/bin/commands/make-mail.js +93 -0
  23. package/bin/commands/make-policy.js +106 -0
  24. package/bin/commands/run.js +195 -0
  25. package/bin/webspresso.js +21 -0
  26. package/core/auth/bouncer.js +196 -0
  27. package/core/auth/index.js +73 -1
  28. package/core/auth/magic-link.js +170 -0
  29. package/core/auth/manager.js +343 -3
  30. package/core/auth/middleware.js +45 -6
  31. package/core/auth/session-manager.js +255 -0
  32. package/core/auth/sessions.js +351 -0
  33. package/core/auth/tokens.js +1 -0
  34. package/core/auth/totp.js +4 -1
  35. package/core/auth/user-agent.js +161 -0
  36. package/core/commands/define-command.js +48 -0
  37. package/core/commands/index.js +18 -0
  38. package/core/commands/registry.js +156 -0
  39. package/core/commands/runner.js +76 -0
  40. package/core/compression/compressible.js +1 -1
  41. package/core/compression/middleware.js +35 -251
  42. package/core/content/service.js +8 -2
  43. package/core/events/discovery.js +142 -0
  44. package/core/events/dispatcher.js +552 -0
  45. package/core/events/index.js +23 -0
  46. package/core/events/signal.js +92 -0
  47. package/core/forms/flash.js +234 -0
  48. package/core/forms/form-builder.js +214 -0
  49. package/core/forms/index.js +16 -0
  50. package/core/mail/index.js +23 -0
  51. package/core/mail/mailable.js +250 -0
  52. package/core/mail/mailer.js +184 -0
  53. package/core/mail/transports.js +238 -0
  54. package/core/orm/factory.js +250 -0
  55. package/core/orm/index.js +8 -1
  56. package/core/orm/migrations/diff.js +185 -0
  57. package/core/orm/migrations/index.js +22 -4
  58. package/core/orm/model.js +55 -1
  59. package/core/orm/repository.js +283 -13
  60. package/core/orm/scopes.js +3 -2
  61. package/core/orm/types.js +6 -1
  62. package/core/queue/cron.js +252 -39
  63. package/core/queue/manager.js +20 -2
  64. package/core/queue/scheduler.js +115 -59
  65. package/core/ssr/stream.js +62 -2
  66. package/index.d.ts +540 -14
  67. package/index.js +44 -0
  68. package/package.json +11 -5
  69. package/plugins/admin-panel/api.js +509 -3
  70. package/plugins/admin-panel/index.js +17 -0
  71. package/plugins/admin-panel/modules/admin-users.js +1 -1
  72. package/plugins/audit-log/admin-component.js +54 -54
  73. package/plugins/audit-log/index.js +2 -2
  74. package/plugins/content/client/inline-edit.js +2 -46
  75. package/plugins/content/models/content-entry.js +4 -3
  76. package/plugins/content/models/content-type.js +4 -3
  77. package/plugins/cors.js +15 -4
  78. package/plugins/email/auth-bridge.js +66 -0
  79. package/plugins/email/bundled-templates.js +15 -0
  80. package/plugins/email/index.js +48 -0
  81. package/plugins/email/preview-handler.js +555 -0
  82. package/plugins/email/service.js +23 -12
  83. package/plugins/email/templates/auth-magic-link.mjml +13 -0
  84. package/plugins/email/transport.js +30 -11
  85. package/plugins/file-manager/admin-component.js +49 -49
  86. package/plugins/file-manager/index.js +4 -4
  87. package/plugins/index.js +2 -0
  88. package/plugins/redirect/index.js +1 -1
  89. package/plugins/webhooks/admin-component.js +608 -0
  90. package/plugins/webhooks/api-handlers.js +237 -0
  91. package/plugins/webhooks/crypto.js +144 -0
  92. package/plugins/webhooks/dispatcher.js +708 -0
  93. package/plugins/webhooks/index.js +263 -0
  94. package/plugins/webhooks/matcher.js +113 -0
  95. package/plugins/webhooks/migration-template.js +75 -0
  96. package/plugins/webhooks/models.js +98 -0
  97. package/plugins/webhooks/sender.js +176 -0
  98. package/src/api/api-loader.js +1 -1
  99. package/src/app-context.js +48 -6
  100. package/src/bundle/index.js +2 -1
  101. package/src/dev-reload/index.js +9 -1
  102. package/src/discovery/scan-routes.js +10 -1
  103. package/src/file-router.js +173 -4
  104. package/src/helpers.js +150 -1
  105. package/src/njk-frontmatter.js +3 -0
  106. package/src/pages/page-loader.js +1 -1
  107. package/src/routing/cascading.js +120 -4
  108. package/src/server.js +180 -26
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso CLI - make:mail Command
5
+ * Scaffolds a declarative Mailable class in emails/<name>.js
6
+ * @module bin/commands/make-mail
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ function generateMailContent(rawName) {
13
+ const baseName = rawName.replace(/email$/i, '').replace(/mail$/i, '').trim();
14
+ const cleanBase = baseName.charAt(0).toUpperCase() + baseName.slice(1);
15
+ const className = `${cleanBase}Email`;
16
+ const tplName = cleanBase.toLowerCase();
17
+
18
+ return `'use strict';
19
+
20
+ const { Mailable } = require('webspresso');
21
+
22
+ /**
23
+ * Mailable: ${className}
24
+ * Send via: req.mailer.send(new ${className}(user))
25
+ */
26
+ class ${className} extends Mailable {
27
+ constructor(recipient, data = {}) {
28
+ super();
29
+ this.recipient = recipient;
30
+ this.data = data;
31
+ }
32
+
33
+ /**
34
+ * Build email definition
35
+ */
36
+ build() {
37
+ const toAddress = typeof this.recipient === 'string' ? this.recipient : this.recipient?.email;
38
+ const toName = typeof this.recipient === 'object' ? this.recipient?.name : undefined;
39
+
40
+ this.to(toAddress, toName)
41
+ .subject('Hello from Webspresso')
42
+ .view('${tplName}', this.data);
43
+ }
44
+ }
45
+
46
+ module.exports = {
47
+ ${className},
48
+ };
49
+ `;
50
+ }
51
+
52
+ function registerCommand(program) {
53
+ program
54
+ .command('make:mail <name>')
55
+ .alias('mail:make')
56
+ .alias('make:mailable')
57
+ .description('Scaffold a declarative Mailable class in emails/<name>.js')
58
+ .option('--root <dir>', 'Root directory of the project', process.cwd())
59
+ .action((nameArg, options) => {
60
+ const rootDir = path.resolve(options.root || process.cwd());
61
+ const emailsDir = path.join(rootDir, 'emails');
62
+
63
+ if (!fs.existsSync(emailsDir)) {
64
+ fs.mkdirSync(emailsDir, { recursive: true });
65
+ }
66
+
67
+ const rawName = String(nameArg || '').trim();
68
+ if (!rawName) {
69
+ console.error('❌ Mailable name is required.');
70
+ process.exitCode = 1;
71
+ return;
72
+ }
73
+
74
+ const cleanName = rawName.replace(/\.js$/, '');
75
+ const fileName = cleanName.endsWith('Email') ? cleanName : `${cleanName}Email`;
76
+ const targetFilePath = path.join(emailsDir, `${fileName}.js`);
77
+
78
+ if (fs.existsSync(targetFilePath)) {
79
+ console.error(`❌ Mailable file already exists: ${path.relative(rootDir, targetFilePath)}`);
80
+ process.exitCode = 1;
81
+ return;
82
+ }
83
+
84
+ const content = generateMailContent(cleanName);
85
+ fs.writeFileSync(targetFilePath, content, 'utf-8');
86
+ console.log(`\n✅ Created Mailable class: ${path.relative(rootDir, targetFilePath)}\n`);
87
+ });
88
+ }
89
+
90
+ module.exports = {
91
+ registerCommand,
92
+ generateMailContent,
93
+ };
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso CLI - make:policy Command
5
+ * Scaffolds an authorization policy in policies/<name>.policy.js
6
+ * @module bin/commands/make-policy
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ function generatePolicyContent(policyName) {
13
+ const modelName = policyName.replace(/policy$/i, '').trim();
14
+ const cleanName = modelName.charAt(0).toUpperCase() + modelName.slice(1);
15
+
16
+ return `'use strict';
17
+
18
+ /**
19
+ * Authorization Policy: ${cleanName}
20
+ * Invoked via req.bouncer.authorize('${cleanName.toLowerCase()}.update', record)
21
+ * or {% if fsy.can('${cleanName.toLowerCase()}.update', record) %}
22
+ */
23
+ module.exports = {
24
+ name: '${cleanName.toLowerCase()}',
25
+
26
+ /**
27
+ * Determine whether the user can view any records
28
+ */
29
+ async viewAny(user) {
30
+ return true;
31
+ },
32
+
33
+ /**
34
+ * Determine whether the user can view the specific record
35
+ */
36
+ async view(user, record) {
37
+ return true;
38
+ },
39
+
40
+ /**
41
+ * Determine whether the user can create records
42
+ */
43
+ async create(user) {
44
+ return Boolean(user);
45
+ },
46
+
47
+ /**
48
+ * Determine whether the user can update the record
49
+ */
50
+ async update(user, record) {
51
+ if (!user) return false;
52
+ if (user.isAdmin || user.role === 'admin') return true;
53
+ return record && (record.userId === user.id || record.user_id === user.id);
54
+ },
55
+
56
+ /**
57
+ * Determine whether the user can delete the record
58
+ */
59
+ async delete(user, record) {
60
+ if (!user) return false;
61
+ return Boolean(user.isAdmin || user.role === 'admin');
62
+ },
63
+ };
64
+ `;
65
+ }
66
+
67
+ function registerCommand(program) {
68
+ program
69
+ .command('make:policy <name>')
70
+ .alias('policy:make')
71
+ .description('Scaffold an authorization policy in policies/<name>.policy.js')
72
+ .option('--root <dir>', 'Root directory of the project', process.cwd())
73
+ .action((nameArg, options) => {
74
+ const rootDir = path.resolve(options.root || process.cwd());
75
+ const policiesDir = path.join(rootDir, 'policies');
76
+
77
+ if (!fs.existsSync(policiesDir)) {
78
+ fs.mkdirSync(policiesDir, { recursive: true });
79
+ }
80
+
81
+ const rawName = String(nameArg || '').trim();
82
+ if (!rawName) {
83
+ console.error('❌ Policy name is required.');
84
+ process.exitCode = 1;
85
+ return;
86
+ }
87
+
88
+ const policyBase = rawName.replace(/\.policy(\.js)?$/, '').replace(/\.js$/, '');
89
+ const targetFilePath = path.join(policiesDir, `${policyBase}.policy.js`);
90
+
91
+ if (fs.existsSync(targetFilePath)) {
92
+ console.error(`❌ Policy file already exists: ${path.relative(rootDir, targetFilePath)}`);
93
+ process.exitCode = 1;
94
+ return;
95
+ }
96
+
97
+ const content = generatePolicyContent(policyBase);
98
+ fs.writeFileSync(targetFilePath, content, 'utf-8');
99
+ console.log(`\n✅ Created authorization policy: ${path.relative(rootDir, targetFilePath)}\n`);
100
+ });
101
+ }
102
+
103
+ module.exports = {
104
+ registerCommand,
105
+ generatePolicyContent,
106
+ };
@@ -0,0 +1,195 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso CLI - run Command
5
+ * Executes custom management commands with injected framework context
6
+ * @module bin/commands/run
7
+ */
8
+
9
+ const path = require('path');
10
+ const fs = require('fs');
11
+ const { createCommandRegistry, executeCommand } = require('../../core/commands');
12
+ const { createEventDispatcher } = require('../../core/events');
13
+ const { createMailer } = require('../../core/mail');
14
+ const { resolveDbConfigIfExists, createDbInstance } = require('../utils/db');
15
+
16
+ /**
17
+ * Helper to parse unknown CLI options into a flags object
18
+ * e.g. ['--dry-run', '--limit', '50', 'arg1'] -> flags: { dryRun: true, limit: 50 }, args: ['arg1']
19
+ * @param {string[]} rawArgs
20
+ * @returns {{ flags: Object, args: string[] }}
21
+ */
22
+ function parseRawArgs(rawArgs = []) {
23
+ const flags = {};
24
+ const args = [];
25
+
26
+ for (let i = 0; i < rawArgs.length; i++) {
27
+ const item = rawArgs[i];
28
+ if (item.startsWith('--')) {
29
+ const withoutDashes = item.slice(2);
30
+ if (withoutDashes.includes('=')) {
31
+ const [k, v] = withoutDashes.split('=');
32
+ flags[camelCase(k)] = parseVal(v);
33
+ } else {
34
+ const next = rawArgs[i + 1];
35
+ if (next && !next.startsWith('-')) {
36
+ flags[camelCase(withoutDashes)] = parseVal(next);
37
+ i++;
38
+ } else {
39
+ flags[camelCase(withoutDashes)] = true;
40
+ }
41
+ }
42
+ } else if (item.startsWith('-') && item.length === 2) {
43
+ const flagChar = item.slice(1);
44
+ const next = rawArgs[i + 1];
45
+ if (next && !next.startsWith('-')) {
46
+ flags[flagChar] = parseVal(next);
47
+ i++;
48
+ } else {
49
+ flags[flagChar] = true;
50
+ }
51
+ } else {
52
+ args.push(item);
53
+ }
54
+ }
55
+
56
+ return { flags, args };
57
+ }
58
+
59
+ function camelCase(str) {
60
+ return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
61
+ }
62
+
63
+ function parseVal(val) {
64
+ if (val === 'true') return true;
65
+ if (val === 'false') return false;
66
+ if (val === 'null') return null;
67
+ if (!Number.isNaN(Number(val)) && val.trim() !== '') return Number(val);
68
+ return val;
69
+ }
70
+
71
+ function registerCommand(program) {
72
+ program
73
+ .command('run [commandName] [args...]')
74
+ .alias('command:run')
75
+ .description('Run a custom management command from commands/ with injected framework context')
76
+ .option('-d, --dir <path>', 'Custom commands directory (default: ./commands)')
77
+ .option('-c, --config <path>', 'Path to database config file (optional)')
78
+ .option('-E, --env <environment>', 'Environment name (default: development)')
79
+ .allowUnknownOption(true)
80
+ .action(async (commandName, rawArgs = [], options = {}) => {
81
+ const rootDir = process.cwd();
82
+ const commandsDir = options.dir
83
+ ? path.resolve(rootDir, options.dir)
84
+ : path.resolve(rootDir, 'commands');
85
+
86
+ const registry = createCommandRegistry({ commandsDir });
87
+ const available = registry.list();
88
+
89
+ if (!commandName) {
90
+ if (available.length === 0) {
91
+ console.log(`\n⚠️ No custom commands found in "${path.relative(rootDir, commandsDir) || 'commands/'}".`);
92
+ console.log(' Create your first custom command with:');
93
+ console.log(' npx webspresso make:command <name>\n');
94
+ return;
95
+ }
96
+
97
+ console.log(`\n⚡ Available Custom Management Commands (${available.length}):\n`);
98
+ const maxLen = Math.max(...available.map((c) => c.name.length), 10);
99
+ for (const cmd of available) {
100
+ console.log(` webspresso run ${cmd.name.padEnd(maxLen + 2)} ${cmd.description ? `# ${cmd.description}` : ''}`);
101
+ }
102
+ console.log('');
103
+ return;
104
+ }
105
+
106
+ if (!registry.has(commandName)) {
107
+ console.error(`\n❌ Error: Command "${commandName}" not found in ${path.relative(rootDir, commandsDir) || 'commands/'}.`);
108
+ if (available.length > 0) {
109
+ console.log(` Available commands: ${registry.names().join(', ')}\n`);
110
+ } else {
111
+ console.log(' Use `npx webspresso make:command <name>` to create it.\n');
112
+ }
113
+ process.exitCode = 1;
114
+ return;
115
+ }
116
+
117
+ const cmdDef = registry.get(commandName);
118
+ const { flags, args } = parseRawArgs(rawArgs);
119
+
120
+ // Boot Context
121
+ let knexInstance = null;
122
+ let db = null;
123
+ let services = null;
124
+ const events = createEventDispatcher();
125
+ const mailer = createMailer({ backend: 'console' });
126
+
127
+ // 1. Database
128
+ try {
129
+ const dbInfo = resolveDbConfigIfExists(options.config);
130
+ if (dbInfo) {
131
+ knexInstance = await createDbInstance(dbInfo.config, options.env);
132
+ const { createDatabase } = require('../../core/orm');
133
+ db = createDatabase({ knex: knexInstance });
134
+ }
135
+ } catch (err) {
136
+ console.warn('⚠️ Could not initialize database for command:', err.message);
137
+ }
138
+
139
+ // 2. Services
140
+ try {
141
+ const servicesDir = path.resolve(rootDir, 'services');
142
+ if (fs.existsSync(servicesDir)) {
143
+ const { createServiceRegistry } = require('../../src/services');
144
+ services = createServiceRegistry({ servicesDir, isDev: true });
145
+ }
146
+ } catch (err) {
147
+ console.warn('⚠️ Could not initialize service registry for command:', err.message);
148
+ }
149
+
150
+ // 3. Queue
151
+ let queue = null;
152
+ try {
153
+ const { createQueueDispatcher } = require('../../core/queue');
154
+ queue = createQueueDispatcher({ db: knexInstance });
155
+ } catch (_) {
156
+ queue = {
157
+ dispatch: async () => {},
158
+ };
159
+ }
160
+
161
+ console.log(`\n🚀 [webspresso run] Executing "${commandName}"...`);
162
+ const executionResult = await executeCommand(cmdDef, {
163
+ args,
164
+ flags,
165
+ db,
166
+ queue,
167
+ services,
168
+ mailer,
169
+ events,
170
+ logger: console,
171
+ });
172
+
173
+ // Cleanup
174
+ if (knexInstance && typeof knexInstance.destroy === 'function') {
175
+ try {
176
+ await knexInstance.destroy();
177
+ } catch (_) {}
178
+ }
179
+
180
+ if (executionResult.success) {
181
+ console.log(`\n✨ Command "${commandName}" completed successfully in ${executionResult.durationMs}ms.\n`);
182
+ } else {
183
+ console.error(`\n❌ Command "${commandName}" failed after ${executionResult.durationMs}ms:`);
184
+ if (executionResult.error) {
185
+ console.error(executionResult.error.stack || executionResult.error.message);
186
+ }
187
+ process.exitCode = 1;
188
+ }
189
+ });
190
+ }
191
+
192
+ module.exports = {
193
+ registerCommand,
194
+ parseRawArgs,
195
+ };
package/bin/webspresso.js CHANGED
@@ -26,6 +26,7 @@ const { registerCommand: registerDbRollback } = require('./commands/db-rollback'
26
26
  const { registerCommand: registerDbStatus } = require('./commands/db-status');
27
27
  const { registerCommand: registerDbMake } = require('./commands/db-make');
28
28
  const { registerCommand: registerDbScaffold } = require('./commands/db-scaffold');
29
+ const { registerCommand: registerDbMakeMigrations } = require('./commands/db-makemigrations');
29
30
  const { registerCommand: registerSeed } = require('./commands/seed');
30
31
  const { registerCommand: registerAdminSetup } = require('./commands/admin-setup');
31
32
  const { registerCommand: registerAdminPassword } = require('./commands/admin-password');
@@ -47,6 +48,15 @@ const { registerCommand: registerMakeModel } = require('./commands/make-model');
47
48
  const { registerCommand: registerMakeJob } = require('./commands/make-job');
48
49
  const { registerCommand: registerOpenapi } = require('./commands/openapi');
49
50
  const { registerCommand: registerTypegen } = require('./commands/typegen');
51
+ const { registerCommand: registerAddDocker } = require('./commands/add-docker');
52
+ const { registerCommand: registerDeployCheck } = require('./commands/deploy-check');
53
+ const { registerCommand: registerRun } = require('./commands/run');
54
+ const { registerCommand: registerList } = require('./commands/list');
55
+ const { registerCommand: registerMakeCommand } = require('./commands/make-command');
56
+ const { registerCommand: registerMakePolicy } = require('./commands/make-policy');
57
+ const { registerCommand: registerMakeFactory } = require('./commands/make-factory');
58
+ const { registerCommand: registerMakeEvent } = require('./commands/make-event');
59
+ const { registerCommand: registerMakeMail } = require('./commands/make-mail');
50
60
 
51
61
  registerNew(program);
52
62
  registerPage(program);
@@ -56,11 +66,13 @@ registerStart(program);
56
66
  registerBuild(program);
57
67
  registerAddTailwind(program);
58
68
  registerAddEsbuild(program);
69
+ registerAddDocker(program);
59
70
  registerDbMigrate(program);
60
71
  registerDbRollback(program);
61
72
  registerDbStatus(program);
62
73
  registerDbMake(program);
63
74
  registerDbScaffold(program);
75
+ registerDbMakeMigrations(program);
64
76
  registerSeed(program);
65
77
  registerAdminSetup(program);
66
78
  registerAdminPassword(program);
@@ -71,6 +83,7 @@ registerEmailPrune(program);
71
83
  registerQueuePrune(program);
72
84
  registerUploadPrune(program);
73
85
  registerDoctor(program);
86
+ registerDeployCheck(program);
74
87
  registerUpgrade(program);
75
88
  registerOrmMap(program);
76
89
  registerService(program);
@@ -80,8 +93,16 @@ registerAgentsInit(program);
80
93
  registerRoutes(program);
81
94
  registerMakeModel(program);
82
95
  registerMakeJob(program);
96
+ registerMakeCommand(program);
97
+ registerMakePolicy(program);
98
+ registerMakeFactory(program);
99
+ registerMakeEvent(program);
100
+ registerMakeMail(program);
101
+ registerRun(program);
102
+ registerList(program);
83
103
  registerOpenapi(program);
84
104
  registerTypegen(program);
85
105
 
86
106
  // Parse arguments
87
107
  program.parse();
108
+
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Webspresso Auth - Bouncer & Policy Authorization Engine (AdonisJS Bouncer & Django Permissions-inspired)
3
+ * Context-aware authorization helper for routes, handlers, and templates
4
+ * @module core/auth/bouncer
5
+ */
6
+
7
+ const { defaultPolicyManager, AuthorizationError } = require('./policy');
8
+ const { ForbiddenError, UnauthorizedError } = require('../errors');
9
+
10
+ /**
11
+ * Context-aware Bouncer instance bound to a specific user
12
+ */
13
+ class Bouncer {
14
+ /**
15
+ * @param {Object|null} user - The subject user (or null for guest)
16
+ * @param {import('./policy').PolicyManager} [policyManager]
17
+ */
18
+ constructor(user = null, policyManager = defaultPolicyManager) {
19
+ this.user = user;
20
+ this.policyManager = policyManager;
21
+ }
22
+
23
+ /**
24
+ * Create a new Bouncer instance for another user
25
+ * @param {Object|null} user
26
+ * @returns {Bouncer}
27
+ */
28
+ forUser(user) {
29
+ return new Bouncer(user, this.policyManager);
30
+ }
31
+
32
+ /**
33
+ * Parse policy and action from arguments
34
+ * Supports:
35
+ * 1. ('Post.update', post) -> policy='Post', action='update', resource=post
36
+ * 2. ('Post', 'update', post) -> policy='Post', action='update', resource=post
37
+ * 3. ('gate-name', resource) -> policy=null, action='gate-name', resource=resource
38
+ * 4. (functionRule, resource) -> direct function check
39
+ * @protected
40
+ */
41
+ _parseArgs(actionOrPolicy, ...rest) {
42
+ if (typeof actionOrPolicy === 'function') {
43
+ return {
44
+ isFn: true,
45
+ fn: actionOrPolicy,
46
+ resource: rest[0],
47
+ };
48
+ }
49
+
50
+ if (typeof actionOrPolicy === 'string') {
51
+ // 1. Dot-notation: 'Post.update'
52
+ if (actionOrPolicy.includes('.')) {
53
+ const parts = actionOrPolicy.split('.');
54
+ const policy = parts[0];
55
+ const action = parts.slice(1).join('.');
56
+ return { policy, action, resource: rest[0] };
57
+ }
58
+
59
+ // 2. Separate strings: ('Post', 'update', post)
60
+ if (rest.length > 0 && typeof rest[0] === 'string') {
61
+ const policy = actionOrPolicy;
62
+ const action = rest[0];
63
+ const resource = rest[1];
64
+ return { policy, action, resource };
65
+ }
66
+
67
+ // 3. Single gate / permission name: ('edit-settings', setting)
68
+ return { policy: null, action: actionOrPolicy, resource: rest[0] };
69
+ }
70
+
71
+ return { policy: null, action: String(actionOrPolicy), resource: rest[0] };
72
+ }
73
+
74
+ /**
75
+ * Check if current user is allowed to perform an action
76
+ * @param {string|Function} actionOrPolicy
77
+ * @param {...any} rest
78
+ * @returns {boolean}
79
+ */
80
+ allows(actionOrPolicy, ...rest) {
81
+ const parsed = this._parseArgs(actionOrPolicy, ...rest);
82
+
83
+ // Direct function rule
84
+ if (parsed.isFn) {
85
+ try {
86
+ return Boolean(parsed.fn(this.user, parsed.resource));
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ const { policy, action, resource } = parsed;
93
+
94
+ // 1. Check Policy Manager (Gates & Policies)
95
+ if (policy) {
96
+ if (this.policyManager.hasPolicy(policy)) {
97
+ return this.policyManager.can(this.user, action, policy, resource);
98
+ }
99
+ } else {
100
+ if (this.policyManager.hasGate(action)) {
101
+ return this.policyManager.can(this.user, action, null, resource);
102
+ }
103
+ }
104
+
105
+ // 2. Django-style user permissions matrix check
106
+ // e.g. user.permissions = ['post.create', 'post.update'] or user.hasPerm(...)
107
+ if (this.user) {
108
+ const fullPerm = policy ? `${policy.toLowerCase()}.${action.toLowerCase()}` : action.toLowerCase();
109
+
110
+ // Check hasPerm / hasPermission method on user object
111
+ if (typeof this.user.hasPerm === 'function') {
112
+ if (this.user.hasPerm(fullPerm, resource)) return true;
113
+ }
114
+ if (typeof this.user.hasPermission === 'function') {
115
+ if (this.user.hasPermission(fullPerm, resource)) return true;
116
+ }
117
+
118
+ // Check permissions array or set
119
+ const perms = this.user.permissions;
120
+ if (Array.isArray(perms) || perms instanceof Set) {
121
+ const permSet = perms instanceof Set ? perms : new Set(perms);
122
+ if (permSet.has('*') || permSet.has(fullPerm)) {
123
+ return true;
124
+ }
125
+ if (policy && permSet.has(`${policy.toLowerCase()}.*`)) {
126
+ return true;
127
+ }
128
+ }
129
+
130
+ // If user has superuser / is_admin flag
131
+ if (this.user.isSuperuser || this.user.is_superuser) {
132
+ return true;
133
+ }
134
+ }
135
+
136
+ return false;
137
+ }
138
+
139
+ /**
140
+ * Check if current user is denied from performing an action
141
+ * @param {string|Function} actionOrPolicy
142
+ * @param {...any} rest
143
+ * @returns {boolean}
144
+ */
145
+ denies(actionOrPolicy, ...rest) {
146
+ return !this.allows(actionOrPolicy, ...rest);
147
+ }
148
+
149
+ /**
150
+ * Authorize an action. Throws ForbiddenError (or UnauthorizedError) if access is denied.
151
+ * @param {string|Function} actionOrPolicy
152
+ * @param {...any} rest
153
+ * @throws {ForbiddenError|UnauthorizedError}
154
+ */
155
+ authorize(actionOrPolicy, ...rest) {
156
+ if (this.denies(actionOrPolicy, ...rest)) {
157
+ if (!this.user) {
158
+ throw new UnauthorizedError('Authentication required to perform this action');
159
+ }
160
+ const label = typeof actionOrPolicy === 'string' ? actionOrPolicy : 'action';
161
+ throw new ForbiddenError(`You are not authorized to perform "${label}"`);
162
+ }
163
+ return true;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Create a Bouncer instance for a user
169
+ * @param {Object|null} user
170
+ * @param {import('./policy').PolicyManager} [policyManager]
171
+ * @returns {Bouncer}
172
+ */
173
+ function createBouncer(user = null, policyManager = defaultPolicyManager) {
174
+ return new Bouncer(user, policyManager);
175
+ }
176
+
177
+ /**
178
+ * Express middleware that attaches `req.bouncer` automatically
179
+ * @param {Object} [options={}]
180
+ * @returns {import('express').RequestHandler}
181
+ */
182
+ function createBouncerMiddleware(options = {}) {
183
+ const policyManager = options.policyManager || defaultPolicyManager;
184
+
185
+ return function bouncerMiddleware(req, res, next) {
186
+ const user = req.user || req.auth?.user || null;
187
+ req.bouncer = new Bouncer(user, policyManager);
188
+ next();
189
+ };
190
+ }
191
+
192
+ module.exports = {
193
+ Bouncer,
194
+ createBouncer,
195
+ createBouncerMiddleware,
196
+ };