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.
- package/README.md +25 -10
- package/dist/{s3db.cjs.js → s3db.cjs} +38801 -32446
- package/dist/s3db.cjs.map +1 -0
- package/dist/s3db.es.js +38653 -32291
- package/dist/s3db.es.js.map +1 -1
- package/package.json +218 -22
- package/src/concerns/id.js +90 -6
- package/src/concerns/index.js +2 -1
- package/src/concerns/password-hashing.js +150 -0
- package/src/database.class.js +6 -2
- package/src/plugins/api/auth/basic-auth.js +40 -10
- package/src/plugins/api/auth/index.js +49 -3
- package/src/plugins/api/auth/oauth2-auth.js +171 -0
- package/src/plugins/api/auth/oidc-auth.js +789 -0
- package/src/plugins/api/auth/oidc-client.js +462 -0
- package/src/plugins/api/auth/path-auth-matcher.js +284 -0
- package/src/plugins/api/concerns/event-emitter.js +134 -0
- package/src/plugins/api/concerns/failban-manager.js +651 -0
- package/src/plugins/api/concerns/guards-helpers.js +402 -0
- package/src/plugins/api/concerns/metrics-collector.js +346 -0
- package/src/plugins/api/index.js +510 -57
- package/src/plugins/api/middlewares/failban.js +305 -0
- package/src/plugins/api/middlewares/rate-limit.js +301 -0
- package/src/plugins/api/middlewares/request-id.js +74 -0
- package/src/plugins/api/middlewares/security-headers.js +120 -0
- package/src/plugins/api/middlewares/session-tracking.js +194 -0
- package/src/plugins/api/routes/auth-routes.js +119 -78
- package/src/plugins/api/routes/resource-routes.js +73 -30
- package/src/plugins/api/server.js +1139 -45
- package/src/plugins/api/utils/custom-routes.js +102 -0
- package/src/plugins/api/utils/guards.js +213 -0
- package/src/plugins/api/utils/mime-types.js +154 -0
- package/src/plugins/api/utils/openapi-generator.js +91 -12
- package/src/plugins/api/utils/path-matcher.js +173 -0
- package/src/plugins/api/utils/static-filesystem.js +262 -0
- package/src/plugins/api/utils/static-s3.js +231 -0
- package/src/plugins/api/utils/template-engine.js +188 -0
- package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +853 -0
- package/src/plugins/cloud-inventory/drivers/aws-driver.js +2554 -0
- package/src/plugins/cloud-inventory/drivers/azure-driver.js +637 -0
- package/src/plugins/cloud-inventory/drivers/base-driver.js +99 -0
- package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +620 -0
- package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +698 -0
- package/src/plugins/cloud-inventory/drivers/gcp-driver.js +645 -0
- package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +559 -0
- package/src/plugins/cloud-inventory/drivers/linode-driver.js +614 -0
- package/src/plugins/cloud-inventory/drivers/mock-drivers.js +449 -0
- package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +771 -0
- package/src/plugins/cloud-inventory/drivers/oracle-driver.js +768 -0
- package/src/plugins/cloud-inventory/drivers/vultr-driver.js +636 -0
- package/src/plugins/cloud-inventory/index.js +20 -0
- package/src/plugins/cloud-inventory/registry.js +146 -0
- package/src/plugins/cloud-inventory/terraform-exporter.js +362 -0
- package/src/plugins/cloud-inventory.plugin.js +1333 -0
- package/src/plugins/concerns/plugin-dependencies.js +62 -2
- package/src/plugins/eventual-consistency/analytics.js +1 -0
- package/src/plugins/eventual-consistency/consolidation.js +2 -2
- package/src/plugins/eventual-consistency/garbage-collection.js +2 -2
- package/src/plugins/eventual-consistency/install.js +2 -2
- package/src/plugins/identity/README.md +335 -0
- package/src/plugins/identity/concerns/mfa-manager.js +204 -0
- package/src/plugins/identity/concerns/password.js +138 -0
- package/src/plugins/identity/concerns/resource-schemas.js +273 -0
- package/src/plugins/identity/concerns/token-generator.js +172 -0
- package/src/plugins/identity/email-service.js +422 -0
- package/src/plugins/identity/index.js +1052 -0
- package/src/plugins/identity/oauth2-server.js +1033 -0
- package/src/plugins/identity/oidc-discovery.js +285 -0
- package/src/plugins/identity/rsa-keys.js +323 -0
- package/src/plugins/identity/server.js +500 -0
- package/src/plugins/identity/session-manager.js +453 -0
- package/src/plugins/identity/ui/layouts/base.js +251 -0
- package/src/plugins/identity/ui/middleware.js +135 -0
- package/src/plugins/identity/ui/pages/admin/client-form.js +247 -0
- package/src/plugins/identity/ui/pages/admin/clients.js +179 -0
- package/src/plugins/identity/ui/pages/admin/dashboard.js +181 -0
- package/src/plugins/identity/ui/pages/admin/user-form.js +283 -0
- package/src/plugins/identity/ui/pages/admin/users.js +263 -0
- package/src/plugins/identity/ui/pages/consent.js +262 -0
- package/src/plugins/identity/ui/pages/forgot-password.js +104 -0
- package/src/plugins/identity/ui/pages/login.js +144 -0
- package/src/plugins/identity/ui/pages/mfa-backup-codes.js +180 -0
- package/src/plugins/identity/ui/pages/mfa-enrollment.js +187 -0
- package/src/plugins/identity/ui/pages/mfa-verification.js +178 -0
- package/src/plugins/identity/ui/pages/oauth-error.js +225 -0
- package/src/plugins/identity/ui/pages/profile.js +361 -0
- package/src/plugins/identity/ui/pages/register.js +226 -0
- package/src/plugins/identity/ui/pages/reset-password.js +128 -0
- package/src/plugins/identity/ui/pages/verify-email.js +172 -0
- package/src/plugins/identity/ui/routes.js +2541 -0
- package/src/plugins/identity/ui/styles/main.css +465 -0
- package/src/plugins/index.js +4 -1
- package/src/plugins/ml/base-model.class.js +65 -16
- package/src/plugins/ml/classification-model.class.js +1 -1
- package/src/plugins/ml/timeseries-model.class.js +3 -1
- package/src/plugins/ml.plugin.js +584 -31
- package/src/plugins/shared/error-handler.js +147 -0
- package/src/plugins/shared/index.js +9 -0
- package/src/plugins/shared/middlewares/compression.js +117 -0
- package/src/plugins/shared/middlewares/cors.js +49 -0
- package/src/plugins/shared/middlewares/index.js +11 -0
- package/src/plugins/shared/middlewares/logging.js +54 -0
- package/src/plugins/shared/middlewares/rate-limit.js +73 -0
- package/src/plugins/shared/middlewares/security.js +158 -0
- package/src/plugins/shared/response-formatter.js +264 -0
- package/src/plugins/state-machine.plugin.js +57 -2
- package/src/resource.class.js +140 -12
- package/src/schema.class.js +30 -1
- package/src/validator.class.js +57 -6
- 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
|
+
};
|