s3db.js 13.4.0 → 13.6.0

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 (110) hide show
  1. package/README.md +25 -10
  2. package/dist/{s3db.cjs.js → s3db.cjs} +38801 -32446
  3. package/dist/s3db.cjs.map +1 -0
  4. package/dist/s3db.es.js +38653 -32291
  5. package/dist/s3db.es.js.map +1 -1
  6. package/package.json +218 -22
  7. package/src/concerns/id.js +90 -6
  8. package/src/concerns/index.js +2 -1
  9. package/src/concerns/password-hashing.js +150 -0
  10. package/src/database.class.js +6 -2
  11. package/src/plugins/api/auth/basic-auth.js +40 -10
  12. package/src/plugins/api/auth/index.js +49 -3
  13. package/src/plugins/api/auth/oauth2-auth.js +171 -0
  14. package/src/plugins/api/auth/oidc-auth.js +789 -0
  15. package/src/plugins/api/auth/oidc-client.js +462 -0
  16. package/src/plugins/api/auth/path-auth-matcher.js +284 -0
  17. package/src/plugins/api/concerns/event-emitter.js +134 -0
  18. package/src/plugins/api/concerns/failban-manager.js +651 -0
  19. package/src/plugins/api/concerns/guards-helpers.js +402 -0
  20. package/src/plugins/api/concerns/metrics-collector.js +346 -0
  21. package/src/plugins/api/index.js +510 -57
  22. package/src/plugins/api/middlewares/failban.js +305 -0
  23. package/src/plugins/api/middlewares/rate-limit.js +301 -0
  24. package/src/plugins/api/middlewares/request-id.js +74 -0
  25. package/src/plugins/api/middlewares/security-headers.js +120 -0
  26. package/src/plugins/api/middlewares/session-tracking.js +194 -0
  27. package/src/plugins/api/routes/auth-routes.js +119 -78
  28. package/src/plugins/api/routes/resource-routes.js +73 -30
  29. package/src/plugins/api/server.js +1139 -45
  30. package/src/plugins/api/utils/custom-routes.js +102 -0
  31. package/src/plugins/api/utils/guards.js +213 -0
  32. package/src/plugins/api/utils/mime-types.js +154 -0
  33. package/src/plugins/api/utils/openapi-generator.js +91 -12
  34. package/src/plugins/api/utils/path-matcher.js +173 -0
  35. package/src/plugins/api/utils/static-filesystem.js +262 -0
  36. package/src/plugins/api/utils/static-s3.js +231 -0
  37. package/src/plugins/api/utils/template-engine.js +188 -0
  38. package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +853 -0
  39. package/src/plugins/cloud-inventory/drivers/aws-driver.js +2554 -0
  40. package/src/plugins/cloud-inventory/drivers/azure-driver.js +637 -0
  41. package/src/plugins/cloud-inventory/drivers/base-driver.js +99 -0
  42. package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +620 -0
  43. package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +698 -0
  44. package/src/plugins/cloud-inventory/drivers/gcp-driver.js +645 -0
  45. package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +559 -0
  46. package/src/plugins/cloud-inventory/drivers/linode-driver.js +614 -0
  47. package/src/plugins/cloud-inventory/drivers/mock-drivers.js +449 -0
  48. package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +771 -0
  49. package/src/plugins/cloud-inventory/drivers/oracle-driver.js +768 -0
  50. package/src/plugins/cloud-inventory/drivers/vultr-driver.js +636 -0
  51. package/src/plugins/cloud-inventory/index.js +20 -0
  52. package/src/plugins/cloud-inventory/registry.js +146 -0
  53. package/src/plugins/cloud-inventory/terraform-exporter.js +362 -0
  54. package/src/plugins/cloud-inventory.plugin.js +1333 -0
  55. package/src/plugins/concerns/plugin-dependencies.js +62 -2
  56. package/src/plugins/eventual-consistency/analytics.js +1 -0
  57. package/src/plugins/eventual-consistency/consolidation.js +2 -2
  58. package/src/plugins/eventual-consistency/garbage-collection.js +2 -2
  59. package/src/plugins/eventual-consistency/install.js +2 -2
  60. package/src/plugins/identity/README.md +335 -0
  61. package/src/plugins/identity/concerns/mfa-manager.js +204 -0
  62. package/src/plugins/identity/concerns/password.js +138 -0
  63. package/src/plugins/identity/concerns/resource-schemas.js +273 -0
  64. package/src/plugins/identity/concerns/token-generator.js +172 -0
  65. package/src/plugins/identity/email-service.js +422 -0
  66. package/src/plugins/identity/index.js +1052 -0
  67. package/src/plugins/identity/oauth2-server.js +1033 -0
  68. package/src/plugins/identity/oidc-discovery.js +285 -0
  69. package/src/plugins/identity/rsa-keys.js +323 -0
  70. package/src/plugins/identity/server.js +500 -0
  71. package/src/plugins/identity/session-manager.js +453 -0
  72. package/src/plugins/identity/ui/layouts/base.js +251 -0
  73. package/src/plugins/identity/ui/middleware.js +135 -0
  74. package/src/plugins/identity/ui/pages/admin/client-form.js +247 -0
  75. package/src/plugins/identity/ui/pages/admin/clients.js +179 -0
  76. package/src/plugins/identity/ui/pages/admin/dashboard.js +181 -0
  77. package/src/plugins/identity/ui/pages/admin/user-form.js +283 -0
  78. package/src/plugins/identity/ui/pages/admin/users.js +263 -0
  79. package/src/plugins/identity/ui/pages/consent.js +262 -0
  80. package/src/plugins/identity/ui/pages/forgot-password.js +104 -0
  81. package/src/plugins/identity/ui/pages/login.js +144 -0
  82. package/src/plugins/identity/ui/pages/mfa-backup-codes.js +180 -0
  83. package/src/plugins/identity/ui/pages/mfa-enrollment.js +187 -0
  84. package/src/plugins/identity/ui/pages/mfa-verification.js +178 -0
  85. package/src/plugins/identity/ui/pages/oauth-error.js +225 -0
  86. package/src/plugins/identity/ui/pages/profile.js +361 -0
  87. package/src/plugins/identity/ui/pages/register.js +226 -0
  88. package/src/plugins/identity/ui/pages/reset-password.js +128 -0
  89. package/src/plugins/identity/ui/pages/verify-email.js +172 -0
  90. package/src/plugins/identity/ui/routes.js +2541 -0
  91. package/src/plugins/identity/ui/styles/main.css +465 -0
  92. package/src/plugins/index.js +4 -1
  93. package/src/plugins/ml/base-model.class.js +65 -16
  94. package/src/plugins/ml/classification-model.class.js +1 -1
  95. package/src/plugins/ml/timeseries-model.class.js +3 -1
  96. package/src/plugins/ml.plugin.js +584 -31
  97. package/src/plugins/shared/error-handler.js +147 -0
  98. package/src/plugins/shared/index.js +9 -0
  99. package/src/plugins/shared/middlewares/compression.js +117 -0
  100. package/src/plugins/shared/middlewares/cors.js +49 -0
  101. package/src/plugins/shared/middlewares/index.js +11 -0
  102. package/src/plugins/shared/middlewares/logging.js +54 -0
  103. package/src/plugins/shared/middlewares/rate-limit.js +73 -0
  104. package/src/plugins/shared/middlewares/security.js +158 -0
  105. package/src/plugins/shared/response-formatter.js +264 -0
  106. package/src/plugins/state-machine.plugin.js +57 -2
  107. package/src/resource.class.js +140 -12
  108. package/src/schema.class.js +30 -1
  109. package/src/validator.class.js +57 -6
  110. package/dist/s3db.cjs.js.map +0 -1
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Password Management - Validation and Generation
3
+ *
4
+ * Uses S3DB native 'password' type for one-way bcrypt hashing.
5
+ * Passwords are hashed automatically on insert/update using bcrypt.
6
+ * Provides password strength validation according to policy.
7
+ */
8
+
9
+ import { verifyPassword as bcryptVerify } from '../../../concerns/password-hashing.js';
10
+
11
+ /**
12
+ * Default password policy
13
+ */
14
+ const DEFAULT_PASSWORD_POLICY = {
15
+ minLength: 8,
16
+ maxLength: 128,
17
+ requireUppercase: true,
18
+ requireLowercase: true,
19
+ requireNumbers: true,
20
+ requireSymbols: false
21
+ };
22
+
23
+ /**
24
+ * Verify a plaintext password against a stored bcrypt hash
25
+ *
26
+ * NOTE: With S3DB's `password` type, passwords are auto-hashed on insert/update
27
+ * using bcrypt with compaction (60 → 53 bytes). This function verifies the
28
+ * plaintext password against the stored hash using bcrypt.compare().
29
+ *
30
+ * @param {string} plaintext - Plaintext password to verify
31
+ * @param {string} storedHash - Stored bcrypt hash (compacted, 53 bytes)
32
+ * @returns {Promise<boolean>} True if password matches, false otherwise
33
+ */
34
+ export async function verifyPassword(plaintext, storedHash) {
35
+ // Use bcrypt verification from password-hashing.js
36
+ // It handles both full (60 bytes) and compacted (53 bytes) hashes
37
+ return await bcryptVerify(plaintext, storedHash);
38
+ }
39
+
40
+ /**
41
+ * Validate password against policy
42
+ * @param {string} password - Password to validate
43
+ * @param {Object} [policy=DEFAULT_PASSWORD_POLICY] - Password policy rules
44
+ * @returns {{valid: boolean, errors: string[]}} Validation result
45
+ */
46
+ export function validatePassword(password, policy = DEFAULT_PASSWORD_POLICY) {
47
+ const errors = [];
48
+
49
+ if (!password || typeof password !== 'string') {
50
+ return { valid: false, errors: ['Password must be a string'] };
51
+ }
52
+
53
+ // Merge with defaults
54
+ const rules = { ...DEFAULT_PASSWORD_POLICY, ...policy };
55
+
56
+ // Length checks
57
+ if (password.length < rules.minLength) {
58
+ errors.push(`Password must be at least ${rules.minLength} characters long`);
59
+ }
60
+
61
+ if (password.length > rules.maxLength) {
62
+ errors.push(`Password must not exceed ${rules.maxLength} characters`);
63
+ }
64
+
65
+ // Character type checks
66
+ if (rules.requireUppercase && !/[A-Z]/.test(password)) {
67
+ errors.push('Password must contain at least one uppercase letter');
68
+ }
69
+
70
+ if (rules.requireLowercase && !/[a-z]/.test(password)) {
71
+ errors.push('Password must contain at least one lowercase letter');
72
+ }
73
+
74
+ if (rules.requireNumbers && !/[0-9]/.test(password)) {
75
+ errors.push('Password must contain at least one number');
76
+ }
77
+
78
+ if (rules.requireSymbols && !/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) {
79
+ errors.push('Password must contain at least one symbol');
80
+ }
81
+
82
+ return {
83
+ valid: errors.length === 0,
84
+ errors
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Generate a random password that meets policy requirements
90
+ * @param {Object} [policy=DEFAULT_PASSWORD_POLICY] - Password policy rules
91
+ * @returns {string} Generated password
92
+ */
93
+ export function generatePassword(policy = DEFAULT_PASSWORD_POLICY) {
94
+ const rules = { ...DEFAULT_PASSWORD_POLICY, ...policy };
95
+
96
+ const lowercase = 'abcdefghijklmnopqrstuvwxyz';
97
+ const uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
98
+ const numbers = '0123456789';
99
+ const symbols = '!@#$%^&*()_+-=[]{};\':"|,.<>/?';
100
+
101
+ let chars = '';
102
+ let password = '';
103
+
104
+ // Always include lowercase
105
+ chars += lowercase;
106
+ password += lowercase[Math.floor(Math.random() * lowercase.length)];
107
+
108
+ if (rules.requireUppercase) {
109
+ chars += uppercase;
110
+ password += uppercase[Math.floor(Math.random() * uppercase.length)];
111
+ }
112
+
113
+ if (rules.requireNumbers) {
114
+ chars += numbers;
115
+ password += numbers[Math.floor(Math.random() * numbers.length)];
116
+ }
117
+
118
+ if (rules.requireSymbols) {
119
+ chars += symbols;
120
+ password += symbols[Math.floor(Math.random() * symbols.length)];
121
+ }
122
+
123
+ // Fill remaining length with random characters from allowed set
124
+ const remaining = rules.minLength - password.length;
125
+ for (let i = 0; i < remaining; i++) {
126
+ password += chars[Math.floor(Math.random() * chars.length)];
127
+ }
128
+
129
+ // Shuffle password
130
+ return password.split('').sort(() => Math.random() - 0.5).join('');
131
+ }
132
+
133
+ export default {
134
+ verifyPassword,
135
+ validatePassword,
136
+ generatePassword,
137
+ DEFAULT_PASSWORD_POLICY
138
+ };
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Resource Schema Definitions - Base attributes for Identity Plugin resources
3
+ *
4
+ * These are the REQUIRED attributes that the Identity Plugin needs to function.
5
+ * Users can extend these with custom attributes, but cannot override base fields.
6
+ */
7
+
8
+ /**
9
+ * Base attributes for Users resource
10
+ *
11
+ * Required by Identity Plugin for authentication and authorization
12
+ */
13
+ export const BASE_USER_ATTRIBUTES = {
14
+ // Authentication
15
+ email: 'string|required|email',
16
+ password: 'password|required',
17
+ emailVerified: 'boolean|default:false',
18
+
19
+ // Profile
20
+ name: 'string|optional',
21
+ givenName: 'string|optional',
22
+ familyName: 'string|optional',
23
+ nickname: 'string|optional',
24
+ picture: 'string|optional',
25
+ locale: 'string|optional',
26
+
27
+ // Authorization
28
+ scopes: 'array|items:string|optional',
29
+ roles: 'array|items:string|optional',
30
+
31
+ // Multi-tenancy
32
+ tenantId: 'string|optional', // Tenant the user belongs to
33
+
34
+ // Status
35
+ active: 'boolean|default:true',
36
+
37
+ // Account Lockout (Brute Force Protection)
38
+ failedLoginAttempts: 'number|default:0', // Count of failed login attempts
39
+ lockedUntil: 'string|optional', // ISO timestamp when account unlocks
40
+ lastFailedLogin: 'string|optional', // ISO timestamp of last failed attempt
41
+
42
+ // Metadata
43
+ metadata: 'object|optional'
44
+ };
45
+
46
+ /**
47
+ * Base attributes for Tenants resource
48
+ *
49
+ * Required by Identity Plugin for multi-tenancy support
50
+ */
51
+ export const BASE_TENANT_ATTRIBUTES = {
52
+ // Identity
53
+ name: 'string|required',
54
+ slug: 'string|required', // URL-friendly identifier
55
+
56
+ // Settings
57
+ settings: 'object|optional',
58
+
59
+ // Status
60
+ active: 'boolean|default:true',
61
+
62
+ // Metadata
63
+ metadata: 'object|optional'
64
+ };
65
+
66
+ /**
67
+ * Base attributes for OAuth2 Clients resource
68
+ *
69
+ * Required by Identity Plugin for OAuth2/OIDC flows
70
+ */
71
+ export const BASE_CLIENT_ATTRIBUTES = {
72
+ // OAuth2 Identity
73
+ clientId: 'string|required',
74
+ clientSecret: 'secret|required',
75
+
76
+ // Client Info
77
+ name: 'string|required',
78
+ description: 'string|optional',
79
+
80
+ // OAuth2 Configuration
81
+ redirectUris: 'array|items:string|required',
82
+ allowedScopes: 'array|items:string|optional',
83
+ grantTypes: 'array|items:string|default:["authorization_code","refresh_token"]',
84
+ responseTypes: 'array|items:string|optional',
85
+
86
+ // Multi-tenancy
87
+ tenantId: 'string|optional', // Tenant the client belongs to
88
+
89
+ // Security
90
+ tokenEndpointAuthMethod: 'string|default:client_secret_post',
91
+ requirePkce: 'boolean|default:false',
92
+
93
+ // Status
94
+ active: 'boolean|default:true',
95
+
96
+ // Metadata
97
+ metadata: 'object|optional'
98
+ };
99
+
100
+ /**
101
+ * Deep merge two objects
102
+ * @param {Object} target - Target object
103
+ * @param {Object} source - Source object
104
+ * @returns {Object} Merged object
105
+ */
106
+ function deepMerge(target, source) {
107
+ const output = { ...target };
108
+
109
+ for (const key in source) {
110
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
111
+ if (target[key] && typeof target[key] === 'object' && !Array.isArray(target[key])) {
112
+ output[key] = deepMerge(target[key], source[key]);
113
+ } else {
114
+ output[key] = source[key];
115
+ }
116
+ } else {
117
+ output[key] = source[key];
118
+ }
119
+ }
120
+
121
+ return output;
122
+ }
123
+
124
+ /**
125
+ * Validate that user-provided attributes don't conflict with base attributes
126
+ * and that optional fields have defaults
127
+ *
128
+ * @param {Object} baseAttributes - Base attributes from plugin
129
+ * @param {Object} userAttributes - User-provided extra attributes
130
+ * @param {string} resourceType - Type of resource (for error messages)
131
+ * @returns {Object} result - { valid: boolean, errors: string[] }
132
+ */
133
+ export function validateExtraAttributes(baseAttributes, userAttributes, resourceType) {
134
+ const errors = [];
135
+
136
+ if (!userAttributes || typeof userAttributes !== 'object') {
137
+ return { valid: true, errors: [] }; // No extras = valid
138
+ }
139
+
140
+ // Check for conflicts with base attributes
141
+ for (const fieldName of Object.keys(userAttributes)) {
142
+ if (baseAttributes[fieldName]) {
143
+ errors.push(
144
+ `Cannot override base attribute '${fieldName}' in ${resourceType} resource. ` +
145
+ `Base attributes are managed by IdentityPlugin.`
146
+ );
147
+ }
148
+ }
149
+
150
+ // Check that optional fields have defaults
151
+ for (const [fieldName, fieldSchema] of Object.entries(userAttributes)) {
152
+ const isOptional = typeof fieldSchema === 'string' && fieldSchema.includes('optional');
153
+ const hasDefault = typeof fieldSchema === 'string' && fieldSchema.includes('default:');
154
+
155
+ if (isOptional && !hasDefault) {
156
+ errors.push(
157
+ `Extra attribute '${fieldName}' in ${resourceType} resource is optional but has no default value. ` +
158
+ `Add "|default:value" to the schema or make it required.`
159
+ );
160
+ }
161
+ }
162
+
163
+ return {
164
+ valid: errors.length === 0,
165
+ errors
166
+ };
167
+ }
168
+
169
+ /**
170
+ * Merge base resource config with user-provided config (deep merge)
171
+ *
172
+ * @param {Object} baseConfig - Base resource config from plugin
173
+ * @param {Object} userConfig - User-provided resource config
174
+ * @param {string} resourceType - Type of resource (for error messages)
175
+ * @returns {Object} mergedConfig - Combined resource configuration
176
+ * @throws {Error} If validation fails
177
+ */
178
+ export function mergeResourceConfig(baseConfig, userConfig = {}, resourceType) {
179
+ // Validate user attributes if provided
180
+ if (userConfig.attributes) {
181
+ const validation = validateExtraAttributes(
182
+ baseConfig.attributes,
183
+ userConfig.attributes,
184
+ resourceType
185
+ );
186
+
187
+ if (!validation.valid) {
188
+ const errorMsg = [
189
+ `Invalid extra attributes for ${resourceType} resource:`,
190
+ ...validation.errors.map(err => ` - ${err}`)
191
+ ].join('\n');
192
+ throw new Error(errorMsg);
193
+ }
194
+ }
195
+
196
+ // Deep merge: user config first, then base config (base takes precedence)
197
+ const merged = deepMerge(userConfig, baseConfig);
198
+
199
+ // Merge attributes specially to ensure base attributes are preserved
200
+ if (userConfig.attributes || baseConfig.attributes) {
201
+ merged.attributes = {
202
+ ...(userConfig.attributes || {}), // User extras first
203
+ ...(baseConfig.attributes || {}) // Base overrides (protection)
204
+ };
205
+ }
206
+
207
+ return merged;
208
+ }
209
+
210
+ /**
211
+ * Validate required resource configuration from user
212
+ *
213
+ * @param {Object} resourcesConfig - User-provided resources configuration
214
+ * @returns {Object} result - { valid: boolean, errors: string[] }
215
+ */
216
+ export function validateResourcesConfig(resourcesConfig) {
217
+ const errors = [];
218
+
219
+ if (!resourcesConfig || typeof resourcesConfig !== 'object') {
220
+ errors.push('IdentityPlugin requires "resources" configuration object');
221
+ return { valid: false, errors };
222
+ }
223
+
224
+ // Validate users resource
225
+ if (!resourcesConfig.users) {
226
+ errors.push(
227
+ 'IdentityPlugin requires "resources.users" configuration.\n' +
228
+ 'Example: resources: { users: { name: "users", attributes: {...}, hooks: {...} } }'
229
+ );
230
+ } else {
231
+ if (!resourcesConfig.users.name || typeof resourcesConfig.users.name !== 'string') {
232
+ errors.push('resources.users.name is required and must be a string');
233
+ }
234
+ }
235
+
236
+ // Validate tenants resource
237
+ if (!resourcesConfig.tenants) {
238
+ errors.push(
239
+ 'IdentityPlugin requires "resources.tenants" configuration.\n' +
240
+ 'Example: resources: { tenants: { name: "tenants", attributes: {...}, partitions: {...} } }'
241
+ );
242
+ } else {
243
+ if (!resourcesConfig.tenants.name || typeof resourcesConfig.tenants.name !== 'string') {
244
+ errors.push('resources.tenants.name is required and must be a string');
245
+ }
246
+ }
247
+
248
+ // Validate clients resource
249
+ if (!resourcesConfig.clients) {
250
+ errors.push(
251
+ 'IdentityPlugin requires "resources.clients" configuration.\n' +
252
+ 'Example: resources: { clients: { name: "oauth_clients", attributes: {...}, behavior: "..." } }'
253
+ );
254
+ } else {
255
+ if (!resourcesConfig.clients.name || typeof resourcesConfig.clients.name !== 'string') {
256
+ errors.push('resources.clients.name is required and must be a string');
257
+ }
258
+ }
259
+
260
+ return {
261
+ valid: errors.length === 0,
262
+ errors
263
+ };
264
+ }
265
+
266
+ export default {
267
+ BASE_USER_ATTRIBUTES,
268
+ BASE_TENANT_ATTRIBUTES,
269
+ BASE_CLIENT_ATTRIBUTES,
270
+ validateExtraAttributes,
271
+ mergeResourceConfig,
272
+ validateResourcesConfig
273
+ };
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Secure Token Generator
3
+ *
4
+ * Generates cryptographically secure random tokens for various use cases:
5
+ * - Password reset tokens
6
+ * - Email verification tokens
7
+ * - API tokens
8
+ * - Session IDs
9
+ */
10
+
11
+ import { randomBytes } from 'crypto';
12
+ import { idGenerator } from '../../../concerns/id.js';
13
+
14
+ /**
15
+ * Generate a secure random token
16
+ * @param {number} [bytes=32] - Number of random bytes (default: 32 bytes = 256 bits)
17
+ * @param {string} [encoding='hex'] - Output encoding ('hex', 'base64', 'base64url')
18
+ * @returns {string} Random token
19
+ */
20
+ export function generateToken(bytes = 32, encoding = 'hex') {
21
+ const buffer = randomBytes(bytes);
22
+
23
+ switch (encoding) {
24
+ case 'hex':
25
+ return buffer.toString('hex');
26
+
27
+ case 'base64':
28
+ return buffer.toString('base64');
29
+
30
+ case 'base64url':
31
+ return buffer.toString('base64url');
32
+
33
+ default:
34
+ throw new Error(`Invalid encoding: ${encoding}. Use 'hex', 'base64', or 'base64url'.`);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Generate a password reset token (URL-safe)
40
+ * @returns {string} 64-character hex token (32 bytes)
41
+ */
42
+ export function generatePasswordResetToken() {
43
+ return generateToken(32, 'hex');
44
+ }
45
+
46
+ /**
47
+ * Generate an email verification token (URL-safe)
48
+ * @returns {string} 64-character hex token (32 bytes)
49
+ */
50
+ export function generateEmailVerificationToken() {
51
+ return generateToken(32, 'hex');
52
+ }
53
+
54
+ /**
55
+ * Generate a session ID using nanoid
56
+ * @returns {string} 22-character session ID
57
+ */
58
+ export function generateSessionId() {
59
+ return idGenerator();
60
+ }
61
+
62
+ /**
63
+ * Generate an API key (longer, more secure)
64
+ * @returns {string} 64-character hex API key (32 bytes)
65
+ */
66
+ export function generateAPIKey() {
67
+ return generateToken(32, 'hex');
68
+ }
69
+
70
+ /**
71
+ * Generate a short numeric code (for 2FA, OTP, etc.)
72
+ * @param {number} [length=6] - Number of digits (default: 6)
73
+ * @returns {string} Numeric code (e.g., "123456")
74
+ */
75
+ export function generateNumericCode(length = 6) {
76
+ const max = Math.pow(10, length);
77
+ const min = Math.pow(10, length - 1);
78
+
79
+ // Generate random number in range [min, max)
80
+ const randomNum = Math.floor(min + Math.random() * (max - min));
81
+
82
+ return randomNum.toString().padStart(length, '0');
83
+ }
84
+
85
+ /**
86
+ * Generate a short alphanumeric code (for invite codes, etc.)
87
+ * @param {number} [length=8] - Number of characters (default: 8)
88
+ * @returns {string} Alphanumeric code (e.g., "A3B7K9M2")
89
+ */
90
+ export function generateAlphanumericCode(length = 8) {
91
+ const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // Excludes similar chars (I, O, 0, 1)
92
+ let code = '';
93
+
94
+ const buffer = randomBytes(length);
95
+
96
+ for (let i = 0; i < length; i++) {
97
+ code += chars[buffer[i] % chars.length];
98
+ }
99
+
100
+ return code;
101
+ }
102
+
103
+ /**
104
+ * Generate a CSRF token (medium security)
105
+ * @returns {string} 32-character hex CSRF token (16 bytes)
106
+ */
107
+ export function generateCSRFToken() {
108
+ return generateToken(16, 'hex');
109
+ }
110
+
111
+ /**
112
+ * Calculate expiration timestamp
113
+ * @param {string|number} duration - Duration string ('15m', '1h', '7d') or milliseconds
114
+ * @returns {number} Unix timestamp (milliseconds)
115
+ */
116
+ export function calculateExpiration(duration) {
117
+ let ms;
118
+
119
+ if (typeof duration === 'number') {
120
+ ms = duration;
121
+ } else if (typeof duration === 'string') {
122
+ const match = duration.match(/^(\d+)([smhd])$/);
123
+
124
+ if (!match) {
125
+ throw new Error(`Invalid duration format: ${duration}. Use '15m', '1h', '7d', etc.`);
126
+ }
127
+
128
+ const value = parseInt(match[1], 10);
129
+ const unit = match[2];
130
+
131
+ switch (unit) {
132
+ case 's': ms = value * 1000; break; // seconds
133
+ case 'm': ms = value * 60 * 1000; break; // minutes
134
+ case 'h': ms = value * 60 * 60 * 1000; break; // hours
135
+ case 'd': ms = value * 24 * 60 * 60 * 1000; break; // days
136
+ default:
137
+ throw new Error(`Invalid duration unit: ${unit}`);
138
+ }
139
+ } else {
140
+ throw new Error('Duration must be a string or number');
141
+ }
142
+
143
+ return Date.now() + ms;
144
+ }
145
+
146
+ /**
147
+ * Check if token/timestamp is expired
148
+ * @param {number|string} expiresAt - Expiration timestamp (Unix ms) or ISO string
149
+ * @returns {boolean} True if expired, false otherwise
150
+ */
151
+ export function isExpired(expiresAt) {
152
+ if (!expiresAt) {
153
+ return true;
154
+ }
155
+
156
+ const timestamp = typeof expiresAt === 'string' ? new Date(expiresAt).getTime() : expiresAt;
157
+
158
+ return Date.now() > timestamp;
159
+ }
160
+
161
+ export default {
162
+ generateToken,
163
+ generatePasswordResetToken,
164
+ generateEmailVerificationToken,
165
+ generateSessionId,
166
+ generateAPIKey,
167
+ generateNumericCode,
168
+ generateAlphanumericCode,
169
+ generateCSRFToken,
170
+ calculateExpiration,
171
+ isExpired
172
+ };