webspresso 0.0.97 → 0.0.99

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 (136) hide show
  1. package/.agents/ADMIN.md +163 -5
  2. package/.agents/AGENTS.md +1 -1
  3. package/.agents/ARCHITECTURE.md +10 -0
  4. package/.agents/AUTH.md +169 -0
  5. package/.agents/CLI.md +195 -1
  6. package/.agents/ORM.md +88 -4
  7. package/.agents/PLUGINS.md +221 -9
  8. package/.agents/QUEUE.md +127 -0
  9. package/.agents/ROUTING.md +127 -7
  10. package/AGENTS.md +1 -0
  11. package/bin/commands/add-esbuild.js +80 -0
  12. package/bin/commands/add-tailwind.js +76 -31
  13. package/bin/commands/analytics-prune.js +57 -0
  14. package/bin/commands/api.js +207 -15
  15. package/bin/commands/build.js +99 -0
  16. package/bin/commands/dev.js +64 -35
  17. package/bin/commands/doctor.js +26 -0
  18. package/bin/commands/email-prune.js +47 -0
  19. package/bin/commands/make-job.js +86 -0
  20. package/bin/commands/make-model.js +250 -0
  21. package/bin/commands/new.js +45 -28
  22. package/bin/commands/openapi.js +102 -0
  23. package/bin/commands/page.js +6 -3
  24. package/bin/commands/queue-prune.js +59 -0
  25. package/bin/commands/routes.js +52 -0
  26. package/bin/commands/service.js +6 -3
  27. package/bin/commands/typegen.js +106 -0
  28. package/bin/commands/upload-prune.js +79 -0
  29. package/bin/webspresso.js +22 -0
  30. package/core/applySchema.js +13 -6
  31. package/core/auth/hash.js +21 -12
  32. package/core/auth/index.js +60 -0
  33. package/core/auth/manager.js +134 -0
  34. package/core/auth/middleware.js +88 -0
  35. package/core/auth/oauth.js +517 -0
  36. package/core/auth/qrcode.js +499 -0
  37. package/core/auth/totp.js +504 -0
  38. package/core/compression/middleware.js +23 -17
  39. package/core/openapi/build-from-api-routes.js +160 -29
  40. package/core/orm/cache/fingerprint.js +15 -1
  41. package/core/orm/cache/memory-provider.js +27 -2
  42. package/core/orm/eager-loader.js +144 -10
  43. package/core/orm/index.js +20 -7
  44. package/core/orm/model.js +53 -7
  45. package/core/orm/query-builder.js +90 -4
  46. package/core/orm/repository.js +44 -3
  47. package/core/orm/search.js +213 -0
  48. package/core/queue/adapters/database.js +94 -0
  49. package/core/queue/adapters/memory.js +62 -0
  50. package/core/queue/adapters/redis.js +80 -0
  51. package/core/queue/cron.js +331 -0
  52. package/core/queue/index.js +8 -1
  53. package/core/queue/manager.js +205 -1
  54. package/core/queue/scheduler.js +359 -0
  55. package/core/sse/index.js +19 -0
  56. package/core/sse/stream.js +430 -0
  57. package/core/ssr/stream.js +11 -0
  58. package/index.d.ts +1188 -10
  59. package/index.js +88 -96
  60. package/package.json +1 -1
  61. package/plugins/admin-panel/api.js +279 -17
  62. package/plugins/admin-panel/app.js +114 -22
  63. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
  64. package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
  65. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +333 -36
  66. package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
  67. package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
  68. package/plugins/admin-panel/client/parts/08-record-list.js +248 -23
  69. package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
  70. package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
  71. package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
  72. package/plugins/admin-panel/core/admin-module.js +13 -7
  73. package/plugins/admin-panel/core/api-extensions.js +30 -2
  74. package/plugins/admin-panel/core/registry.js +96 -16
  75. package/plugins/admin-panel/field-renderers/file-upload.js +187 -104
  76. package/plugins/admin-panel/field-renderers/index.js +2 -0
  77. package/plugins/admin-panel/index.js +96 -0
  78. package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
  79. package/plugins/admin-panel/modules/admin-users.js +16 -1
  80. package/plugins/admin-panel/modules/bulk-actions.js +13 -5
  81. package/plugins/admin-panel/modules/custom-pages.js +492 -3
  82. package/plugins/admin-panel/modules/dashboard.js +283 -0
  83. package/plugins/admin-panel/modules/menu.js +56 -18
  84. package/plugins/admin-panel/modules/user-management.js +24 -6
  85. package/plugins/api-key/crypto.js +101 -0
  86. package/plugins/api-key/index.js +288 -0
  87. package/plugins/api-key/middleware.js +461 -0
  88. package/plugins/api-key/migration-template.js +48 -0
  89. package/plugins/api-key/model.js +50 -0
  90. package/plugins/api-key/scopes.js +125 -0
  91. package/plugins/audit-log/admin-component.js +487 -59
  92. package/plugins/audit-log/api-handlers.js +148 -0
  93. package/plugins/audit-log/index.js +153 -3
  94. package/plugins/audit-log/purge.js +60 -10
  95. package/plugins/dashboard/app.js +152 -27
  96. package/plugins/dashboard/index.js +143 -39
  97. package/plugins/dashboard/styles.js +78 -0
  98. package/plugins/data-exchange/export-xlsx.js +9 -2
  99. package/plugins/data-exchange/import.js +9 -2
  100. package/plugins/data-exchange/index.js +6 -3
  101. package/plugins/email/index.js +132 -2
  102. package/plugins/email/purge.js +60 -10
  103. package/plugins/file-manager/admin-component.js +20 -4
  104. package/plugins/file-manager/index.js +21 -4
  105. package/plugins/index.js +10 -0
  106. package/plugins/oauth/index.js +338 -0
  107. package/plugins/orm-cache-admin/index.js +4 -2
  108. package/plugins/queue/index.js +15 -0
  109. package/plugins/schema-explorer/admin-component.js +714 -0
  110. package/plugins/schema-explorer/relations-graph.js +205 -0
  111. package/plugins/schema-explorer.js +248 -69
  112. package/plugins/site-analytics/admin-component.js +23 -0
  113. package/plugins/site-analytics/api-handlers.js +42 -0
  114. package/plugins/site-analytics/index.js +147 -2
  115. package/plugins/site-analytics/purge.js +168 -0
  116. package/plugins/swagger.js +261 -13
  117. package/plugins/upload/index.js +26 -0
  118. package/plugins/upload/orphan-pruner.js +292 -0
  119. package/src/api/api-loader.js +4 -1
  120. package/src/bundle/index.js +517 -0
  121. package/src/bundle/manifest.js +165 -0
  122. package/src/dev/banner.js +296 -0
  123. package/src/dev/colors.js +70 -0
  124. package/src/dev/index.js +62 -0
  125. package/src/dev/profiler.js +354 -0
  126. package/src/dev/typegen.js +497 -0
  127. package/src/dev-reload/index.js +611 -0
  128. package/src/discovery/file-route-parser.js +44 -1
  129. package/src/discovery/scan-routes.js +2 -2
  130. package/src/file-router.js +347 -162
  131. package/src/helpers.js +967 -98
  132. package/src/njk-frontmatter.js +12 -0
  133. package/src/pages/page-loader.js +61 -11
  134. package/src/routing/cascading.js +378 -0
  135. package/src/routing/middleware-resolver.js +92 -0
  136. package/src/server.js +635 -63
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso CLI - typegen / types Command
5
+ * Generate TypeScript type definitions and IDE auto-complete for models, services, and routes
6
+ * @module bin/commands/typegen
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { generateTypes } = require('../../src/dev/typegen');
12
+
13
+ function registerCommand(program) {
14
+ program
15
+ .command('typegen')
16
+ .alias('types')
17
+ .alias('generate:types')
18
+ .description('Generate TypeScript type definitions and IDE auto-complete for models, services, and routes')
19
+ .option('-o, --out <file>', 'Output declaration file path (default: .webspresso/types.d.ts)')
20
+ .option('-r, --root <dir>', 'Root directory of the project', process.cwd())
21
+ .option('--models <dir>', 'Models directory relative to root')
22
+ .option('--services <dir>', 'Services directory relative to root')
23
+ .option('--pages <dir>', 'Pages directory relative to root', 'pages')
24
+ .option('--no-env-file', 'Skip generating webspresso-env.d.ts at project root')
25
+ .option('-w, --watch', 'Watch models, services, and pages directories for changes')
26
+ .option('-s, --silent', 'Suppress console output', false)
27
+ .action((options) => {
28
+ const rootDir = path.resolve(options.root || process.cwd());
29
+
30
+ const runOnce = () => {
31
+ const startTime = Date.now();
32
+ const result = generateTypes({
33
+ rootDir,
34
+ modelsDir: options.models,
35
+ servicesDir: options.services,
36
+ pagesDir: options.pages,
37
+ outFile: options.out,
38
+ createEnvFile: options.envFile !== false,
39
+ silent: true,
40
+ });
41
+
42
+ const elapsed = Date.now() - startTime;
43
+ if (!options.silent) {
44
+ const relOut = path.relative(rootDir, result.outFile);
45
+ console.log(`\n⚡ [Webspresso TypeGen] Completed in ${elapsed}ms`);
46
+ console.log(` ✓ Models: ${result.modelsCount}`);
47
+ console.log(` ✓ Services: ${result.servicesCount}`);
48
+ console.log(` ✓ Routes: ${result.routesCount}`);
49
+ console.log(` → Output: ${relOut}`);
50
+ if (result.envFile) {
51
+ console.log(` → Env Ref: ${path.relative(rootDir, result.envFile)}`);
52
+ }
53
+ console.log('');
54
+ }
55
+ return result;
56
+ };
57
+
58
+ runOnce();
59
+
60
+ // Watch mode
61
+ if (options.watch) {
62
+ if (!options.silent) {
63
+ console.log('👀 Watching models, services, and pages for changes... (Press Ctrl+C to stop)\n');
64
+ }
65
+
66
+ const watchDirs = [
67
+ options.models || 'models',
68
+ options.models || 'src/models',
69
+ options.services || 'services',
70
+ options.services || 'src/services',
71
+ options.pages || 'pages',
72
+ options.pages || 'src/pages',
73
+ ]
74
+ .map((d) => path.resolve(rootDir, d))
75
+ .filter((d) => fs.existsSync(d));
76
+
77
+ const uniqueDirs = Array.from(new Set(watchDirs));
78
+ let debounceTimer = null;
79
+
80
+ for (const dir of uniqueDirs) {
81
+ try {
82
+ fs.watch(dir, { recursive: true }, (eventType, filename) => {
83
+ if (
84
+ filename &&
85
+ (filename.endsWith('.js') || filename.endsWith('.ts') || filename.endsWith('.njk'))
86
+ ) {
87
+ if (debounceTimer) clearTimeout(debounceTimer);
88
+ debounceTimer = setTimeout(() => {
89
+ if (!options.silent) {
90
+ console.log(`🔄 Detected change in ${filename}, regenerating types...`);
91
+ }
92
+ runOnce();
93
+ }, 150);
94
+ }
95
+ });
96
+ } catch {
97
+ // Watch unsupported on platform or directory
98
+ }
99
+ }
100
+ }
101
+ });
102
+ }
103
+
104
+ module.exports = {
105
+ registerCommand,
106
+ };
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Prune unreferenced / orphaned uploaded files (CLI)
3
+ */
4
+
5
+ 'use strict';
6
+
7
+ const path = require('path');
8
+ const { loadDbConfig, createDbInstance } = require('../utils/db');
9
+ const { pruneOrphanedUploads } = require('../../plugins/upload/orphan-pruner');
10
+
11
+ function registerCommand(program) {
12
+ program
13
+ .command('upload:prune')
14
+ .description('Scan and delete orphaned files in the uploads directory not referenced by any database model')
15
+ .option('--dir <path>', 'Custom uploads directory path (default: public/uploads)')
16
+ .option('--url-base <path>', 'Public URL base path (default: /uploads)', '/uploads')
17
+ .option('--dry-run', 'Scan and list orphaned files without deleting them', false)
18
+ .option('-e, --env <environment>', 'Environment (development, production)', 'development')
19
+ .option('-c, --config <path>', 'Path to database config file')
20
+ .action(async (options) => {
21
+ const { config, path: configPath } = loadDbConfig(options.config);
22
+ console.log(`\n📦 Using config: ${configPath}`);
23
+ console.log(` Environment: ${options.env}\n`);
24
+
25
+ const knex = await createDbInstance(config, options.env);
26
+ const db = { knex };
27
+
28
+ try {
29
+ const destDir = options.dir
30
+ ? path.resolve(process.cwd(), options.dir)
31
+ : path.resolve(process.cwd(), 'public', 'uploads');
32
+
33
+ console.log(`🔍 Scanning directory: ${destDir}`);
34
+ console.log(` Public URL base: ${options.urlBase}`);
35
+ if (options.dryRun) {
36
+ console.log(` Mode: DRY RUN (no files will be deleted)\n`);
37
+ } else {
38
+ console.log(` Mode: PRUNE & DELETE\n`);
39
+ }
40
+
41
+ const result = await pruneOrphanedUploads({
42
+ db,
43
+ destDir,
44
+ publicBasePath: options.urlBase,
45
+ dryRun: options.dryRun,
46
+ });
47
+
48
+ console.log(`📊 Scan Summary:`);
49
+ console.log(` Total files on disk: ${result.totalFiles} (${result.totalBytesFormatted})`);
50
+ console.log(` Referenced by DB: ${result.referencedCount}`);
51
+ console.log(` Orphaned files: ${result.orphanedCount} (${result.orphanedBytesFormatted})\n`);
52
+
53
+ if (result.orphanedCount === 0) {
54
+ console.log(`✅ No orphaned files found. Storage is clean!\n`);
55
+ return;
56
+ }
57
+
58
+ if (options.dryRun) {
59
+ console.log(`ℹ️ [DRY RUN] The following ${result.orphanedCount} file(s) would be deleted:`);
60
+ for (const file of result.orphanedFiles.slice(0, 20)) {
61
+ console.log(` - ${file.relativePath} (${file.publicUrl})`);
62
+ }
63
+ if (result.orphanedFiles.length > 20) {
64
+ console.log(` ... and ${result.orphanedFiles.length - 20} more file(s)`);
65
+ }
66
+ console.log(`\n💡 To permanently delete these files, run without --dry-run.\n`);
67
+ } else {
68
+ console.log(`🗑️ Successfully deleted ${result.deletedCount} orphaned file(s) (${result.deletedBytesFormatted} reclaimed).\n`);
69
+ }
70
+ } catch (err) {
71
+ console.error('❌ upload:prune failed:', err.message);
72
+ process.exit(1);
73
+ } finally {
74
+ await knex.destroy();
75
+ }
76
+ });
77
+ }
78
+
79
+ module.exports = { registerCommand };
package/bin/webspresso.js CHANGED
@@ -18,7 +18,9 @@ const { registerCommand: registerPage } = require('./commands/page');
18
18
  const { registerCommand: registerApi } = require('./commands/api');
19
19
  const { registerCommand: registerDev } = require('./commands/dev');
20
20
  const { registerCommand: registerStart } = require('./commands/start');
21
+ const { registerCommand: registerBuild } = require('./commands/build');
21
22
  const { registerCommand: registerAddTailwind } = require('./commands/add-tailwind');
23
+ const { registerCommand: registerAddEsbuild } = require('./commands/add-esbuild');
22
24
  const { registerCommand: registerDbMigrate } = require('./commands/db-migrate');
23
25
  const { registerCommand: registerDbRollback } = require('./commands/db-rollback');
24
26
  const { registerCommand: registerDbStatus } = require('./commands/db-status');
@@ -29,6 +31,10 @@ const { registerCommand: registerAdminSetup } = require('./commands/admin-setup'
29
31
  const { registerCommand: registerAdminPassword } = require('./commands/admin-password');
30
32
  const { registerCommand: registerFaviconGenerate } = require('./commands/favicon-generate');
31
33
  const { registerCommand: registerAuditPrune } = require('./commands/audit-prune');
34
+ const { registerCommand: registerAnalyticsPrune } = require('./commands/analytics-prune');
35
+ const { registerCommand: registerEmailPrune } = require('./commands/email-prune');
36
+ const { registerCommand: registerQueuePrune } = require('./commands/queue-prune');
37
+ const { registerCommand: registerUploadPrune } = require('./commands/upload-prune');
32
38
  const { registerCommand: registerDoctor } = require('./commands/doctor');
33
39
  const { registerCommand: registerUpgrade } = require('./commands/upgrade');
34
40
  const { registerCommand: registerOrmMap } = require('./commands/orm-map');
@@ -36,13 +42,20 @@ const { registerCommand: registerService } = require('./commands/service');
36
42
  const { registerCommand: registerMcp } = require('./commands/mcp');
37
43
  const { registerCommand: registerPolarMigrate } = require('./commands/polar-migrate');
38
44
  const { registerCommand: registerAgentsInit } = require('./commands/agents-init');
45
+ const { registerCommand: registerRoutes } = require('./commands/routes');
46
+ const { registerCommand: registerMakeModel } = require('./commands/make-model');
47
+ const { registerCommand: registerMakeJob } = require('./commands/make-job');
48
+ const { registerCommand: registerOpenapi } = require('./commands/openapi');
49
+ const { registerCommand: registerTypegen } = require('./commands/typegen');
39
50
 
40
51
  registerNew(program);
41
52
  registerPage(program);
42
53
  registerApi(program);
43
54
  registerDev(program);
44
55
  registerStart(program);
56
+ registerBuild(program);
45
57
  registerAddTailwind(program);
58
+ registerAddEsbuild(program);
46
59
  registerDbMigrate(program);
47
60
  registerDbRollback(program);
48
61
  registerDbStatus(program);
@@ -53,6 +66,10 @@ registerAdminSetup(program);
53
66
  registerAdminPassword(program);
54
67
  registerFaviconGenerate(program);
55
68
  registerAuditPrune(program);
69
+ registerAnalyticsPrune(program);
70
+ registerEmailPrune(program);
71
+ registerQueuePrune(program);
72
+ registerUploadPrune(program);
56
73
  registerDoctor(program);
57
74
  registerUpgrade(program);
58
75
  registerOrmMap(program);
@@ -60,6 +77,11 @@ registerService(program);
60
77
  registerMcp(program);
61
78
  registerPolarMigrate(program);
62
79
  registerAgentsInit(program);
80
+ registerRoutes(program);
81
+ registerMakeModel(program);
82
+ registerMakeJob(program);
83
+ registerOpenapi(program);
84
+ registerTypegen(program);
63
85
 
64
86
  // Parse arguments
65
87
  program.parse();
@@ -3,6 +3,12 @@
3
3
  * Parses and validates request input against compiled schemas
4
4
  */
5
5
 
6
+ const EMPTY_INPUT = Object.freeze({
7
+ body: undefined,
8
+ params: undefined,
9
+ query: undefined,
10
+ });
11
+
6
12
  /**
7
13
  * Apply compiled schema to request
8
14
  * Parses body, params, and query against their respective schemas
@@ -14,18 +20,19 @@
14
20
  * @throws {ZodError} If validation fails
15
21
  */
16
22
  function applySchema(req, compiledSchema) {
23
+ // No schema means no validation
24
+ if (!compiledSchema) {
25
+ req.input = EMPTY_INPUT;
26
+ return;
27
+ }
28
+
17
29
  // Initialize req.input
18
30
  req.input = {
19
31
  body: undefined,
20
32
  params: undefined,
21
- query: undefined
33
+ query: undefined,
22
34
  };
23
35
 
24
- // No schema means no validation
25
- if (!compiledSchema) {
26
- return;
27
- }
28
-
29
36
  // Parse body if schema exists
30
37
  if (compiledSchema.body) {
31
38
  req.input.body = compiledSchema.body.parse(req.body);
package/core/auth/hash.js CHANGED
@@ -4,12 +4,18 @@
4
4
  * @module core/auth/hash
5
5
  */
6
6
 
7
- let bcrypt;
8
- try {
9
- bcrypt = require('bcrypt');
10
- } catch {
11
- // bcrypt is optional, will throw if used without installation
12
- bcrypt = null;
7
+ let bcrypt = null;
8
+ let bcryptChecked = false;
9
+ function getBcrypt() {
10
+ if (!bcryptChecked) {
11
+ bcryptChecked = true;
12
+ try {
13
+ bcrypt = require('bcrypt');
14
+ } catch {
15
+ bcrypt = null;
16
+ }
17
+ }
18
+ return bcrypt;
13
19
  }
14
20
 
15
21
  /**
@@ -25,7 +31,8 @@ const DEFAULT_ROUNDS = process.env.NODE_ENV === 'test' ? 1 : 12;
25
31
  * @returns {Promise<string>} Hashed password
26
32
  */
27
33
  async function hash(password, rounds = DEFAULT_ROUNDS) {
28
- if (!bcrypt) {
34
+ const b = getBcrypt();
35
+ if (!b) {
29
36
  throw new Error('bcrypt is required for password hashing. Install it with: npm install bcrypt');
30
37
  }
31
38
 
@@ -33,7 +40,7 @@ async function hash(password, rounds = DEFAULT_ROUNDS) {
33
40
  throw new Error('Password must be a non-empty string');
34
41
  }
35
42
 
36
- return bcrypt.hash(password, rounds);
43
+ return b.hash(password, rounds);
37
44
  }
38
45
 
39
46
  /**
@@ -43,7 +50,8 @@ async function hash(password, rounds = DEFAULT_ROUNDS) {
43
50
  * @returns {Promise<boolean>} True if password matches
44
51
  */
45
52
  async function verify(password, hashedPassword) {
46
- if (!bcrypt) {
53
+ const b = getBcrypt();
54
+ if (!b) {
47
55
  throw new Error('bcrypt is required for password verification. Install it with: npm install bcrypt');
48
56
  }
49
57
 
@@ -52,7 +60,7 @@ async function verify(password, hashedPassword) {
52
60
  }
53
61
 
54
62
  try {
55
- return await bcrypt.compare(password, hashedPassword);
63
+ return await b.compare(password, hashedPassword);
56
64
  } catch {
57
65
  return false;
58
66
  }
@@ -65,7 +73,8 @@ async function verify(password, hashedPassword) {
65
73
  * @returns {boolean} True if rehash is needed
66
74
  */
67
75
  function needsRehash(hashedPassword, rounds = DEFAULT_ROUNDS) {
68
- if (!bcrypt) {
76
+ const b = getBcrypt();
77
+ if (!b) {
69
78
  throw new Error('bcrypt is required. Install it with: npm install bcrypt');
70
79
  }
71
80
 
@@ -74,7 +83,7 @@ function needsRehash(hashedPassword, rounds = DEFAULT_ROUNDS) {
74
83
  }
75
84
 
76
85
  try {
77
- const hashRounds = bcrypt.getRounds(hashedPassword);
86
+ const hashRounds = b.getRounds(hashedPassword);
78
87
  return hashRounds < rounds;
79
88
  } catch {
80
89
  return true;
@@ -15,6 +15,37 @@ const {
15
15
  dropAuthTokensTable,
16
16
  createKnexAuthTokensAdapter,
17
17
  } = require('./tokens');
18
+ const {
19
+ OAuthClient,
20
+ OAuthError,
21
+ createOAuthClient,
22
+ generatePKCE,
23
+ createSignedState,
24
+ verifySignedState,
25
+ DEFAULT_PROVIDERS,
26
+ } = require('./oauth');
27
+ const totp = require('./totp');
28
+ const {
29
+ base32Encode,
30
+ base32Decode,
31
+ generateTotpSecret,
32
+ generateHotp,
33
+ verifyHotp,
34
+ generateTotp,
35
+ verifyTotp,
36
+ generateTotpUri,
37
+ generateTotpQrSvg,
38
+ generateTotpQrDataUri,
39
+ generateRecoveryCodes,
40
+ hashRecoveryCode,
41
+ verifyRecoveryCode,
42
+ createTotpSetup,
43
+ } = totp;
44
+ const {
45
+ createQrMatrix,
46
+ generateQrSvg,
47
+ generateQrDataUri,
48
+ } = require('./qrcode');
18
49
 
19
50
  /**
20
51
  * Create authentication instance
@@ -195,6 +226,35 @@ module.exports = {
195
226
  createKnexAuthTokensAdapter,
196
227
  TOKEN_TYPES,
197
228
 
229
+ // OAuth 2.0 & Social Login
230
+ OAuthClient,
231
+ OAuthError,
232
+ createOAuthClient,
233
+ generatePKCE,
234
+ createSignedState,
235
+ verifySignedState,
236
+ DEFAULT_PROVIDERS,
237
+
238
+ // 2FA / TOTP (RFC 6238, RFC 4226, RFC 4648)
239
+ totp,
240
+ base32Encode,
241
+ base32Decode,
242
+ generateTotpSecret,
243
+ generateHotp,
244
+ verifyHotp,
245
+ generateTotp,
246
+ verifyTotp,
247
+ generateTotpUri,
248
+ generateTotpQrSvg,
249
+ generateTotpQrDataUri,
250
+ generateRecoveryCodes,
251
+ hashRecoveryCode,
252
+ verifyRecoveryCode,
253
+ createTotpSetup,
254
+ createQrMatrix,
255
+ generateQrSvg,
256
+ generateQrDataUri,
257
+
198
258
  // Config
199
259
  DEFAULT_CONFIG,
200
260
  };
@@ -60,6 +60,7 @@ const DEFAULT_CONFIG = {
60
60
  logout: '/logout',
61
61
  home: '/',
62
62
  redirectAfterLogin: '/dashboard',
63
+ twoFactor: '/2fa',
63
64
  },
64
65
  };
65
66
 
@@ -107,6 +108,10 @@ class AuthManager {
107
108
  welcome: config.notifications?.welcome || null,
108
109
  };
109
110
 
111
+ // TOTP & 2FA initialization
112
+ this.totp = require('./totp');
113
+ this.twoFactor = config.twoFactor || { enabled: false };
114
+
110
115
  if (config.jwt) {
111
116
  const jwtSecret = typeof config.jwt === 'object' && config.jwt.secret
112
117
  ? config.jwt.secret
@@ -329,6 +334,9 @@ class AuthManager {
329
334
  }
330
335
  }
331
336
 
337
+ // Clear 2FA session state
338
+ this.clear2Fa();
339
+
332
340
  // Clear user
333
341
  req.user = null;
334
342
 
@@ -377,6 +385,119 @@ class AuthManager {
377
385
  return req.user?.id || null;
378
386
  },
379
387
 
388
+ /**
389
+ * TOTP helper utilities (RFC 6238 / RFC 4226)
390
+ */
391
+ totp: self.totp,
392
+
393
+ /**
394
+ * Check if two-factor authentication has been verified in current session/request
395
+ * @returns {boolean}
396
+ */
397
+ is2FaVerified() {
398
+ if (req.session?.twoFactorVerified) return true;
399
+ if (req.jwtPayload?.twoFactorVerified) return true;
400
+ return false;
401
+ },
402
+
403
+ /**
404
+ * Check if two-factor authentication is pending verification
405
+ * @returns {boolean}
406
+ */
407
+ is2FaPending() {
408
+ return Boolean(req.session?.twoFactorPending);
409
+ },
410
+
411
+ /**
412
+ * Mark two-factor authentication as verified
413
+ * @param {boolean} [verified=true]
414
+ */
415
+ set2FaVerified(verified = true) {
416
+ if (req.session) {
417
+ req.session.twoFactorVerified = Boolean(verified);
418
+ if (verified) {
419
+ delete req.session.twoFactorPending;
420
+ delete req.session.twoFactorUserId;
421
+ }
422
+ }
423
+ },
424
+
425
+ /**
426
+ * Mark two-factor authentication as pending verification for a user
427
+ * @param {Object} [user] - User object or user ID
428
+ */
429
+ set2FaPending(user) {
430
+ if (req.session) {
431
+ req.session.twoFactorPending = true;
432
+ req.session.twoFactorVerified = false;
433
+ if (user?.id) {
434
+ req.session.twoFactorUserId = user.id;
435
+ } else if (typeof user === 'string' || typeof user === 'number') {
436
+ req.session.twoFactorUserId = user;
437
+ }
438
+ }
439
+ },
440
+
441
+ /**
442
+ * Clear two-factor authentication status from session
443
+ */
444
+ clear2Fa() {
445
+ if (req.session) {
446
+ delete req.session.twoFactorVerified;
447
+ delete req.session.twoFactorPending;
448
+ delete req.session.twoFactorUserId;
449
+ }
450
+ },
451
+
452
+ /**
453
+ * Verify submitted TOTP token or recovery code
454
+ * Automatically marks 2FA as verified upon success
455
+ * @param {string} tokenOrCode - Submitted 6-digit TOTP code or recovery code
456
+ * @param {Object|string} secretOrUser - Secret string or user object with totp_secret / recovery_codes
457
+ * @param {Object} [options]
458
+ * @returns {{ success: boolean, type?: 'totp'|'recovery_code', delta?: number, remainingHashedCodes?: string[], matchedIndex?: number }}
459
+ */
460
+ verify2Fa(tokenOrCode, secretOrUser, options = {}) {
461
+ if (!tokenOrCode) {
462
+ return { success: false };
463
+ }
464
+
465
+ let secret = null;
466
+ let recoveryCodes = null;
467
+
468
+ if (typeof secretOrUser === 'string') {
469
+ secret = secretOrUser;
470
+ } else if (secretOrUser && typeof secretOrUser === 'object') {
471
+ secret = secretOrUser.totp_secret || secretOrUser.totpSecret || secretOrUser.two_factor_secret || secretOrUser.twoFactorSecret;
472
+ recoveryCodes = secretOrUser.recovery_codes || secretOrUser.recoveryCodes || secretOrUser.two_factor_recovery_codes;
473
+ }
474
+
475
+ // Try TOTP code first
476
+ if (secret) {
477
+ const totpResult = self.totp.verifyTotp(tokenOrCode, secret, { ...options, detailed: true });
478
+ if (totpResult.valid) {
479
+ this.set2FaVerified(true);
480
+ return { success: true, type: 'totp', delta: totpResult.delta };
481
+ }
482
+ }
483
+
484
+ // Try recovery codes if provided
485
+ if (recoveryCodes && Array.isArray(recoveryCodes) && recoveryCodes.length > 0) {
486
+ const recResult = self.totp.verifyRecoveryCode(tokenOrCode, recoveryCodes);
487
+ if (recResult.valid) {
488
+ this.set2FaVerified(true);
489
+ return {
490
+ success: true,
491
+ type: 'recovery_code',
492
+ matchedIndex: recResult.matchedIndex,
493
+ remainingHashedCodes: recResult.remainingHashedCodes,
494
+ };
495
+ }
496
+ }
497
+
498
+ return { success: false };
499
+ },
500
+
380
501
  /**
381
502
  * Check if user can perform action (policy check)
382
503
  * @param {string} action - Action name
@@ -801,6 +922,19 @@ class AuthManager {
801
922
  }
802
923
  return this._authMiddleware.requireJwt(options);
803
924
  }
925
+
926
+ /**
927
+ * Get require2Fa guard middleware for Express routes
928
+ * @param {Object} [options]
929
+ * @returns {Function} Express middleware
930
+ */
931
+ require2Fa(options = {}) {
932
+ if (!this._authMiddleware) {
933
+ const { createAuthMiddleware } = require('./middleware');
934
+ this._authMiddleware = createAuthMiddleware(this);
935
+ }
936
+ return this._authMiddleware.require2Fa(options);
937
+ }
804
938
  }
805
939
 
806
940
  module.exports = {