s3db.js 13.5.1 → 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} +30323 -24958
- package/dist/s3db.cjs.map +1 -0
- package/dist/s3db.es.js +24026 -18654
- package/dist/s3db.es.js.map +1 -1
- package/package.json +216 -20
- 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 +4 -0
- package/src/plugins/api/auth/basic-auth.js +23 -1
- 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 +503 -54
- 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 +23 -3
- package/src/plugins/api/routes/resource-routes.js +71 -29
- package/src/plugins/api/server.js +1017 -94
- 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 +44 -11
- 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 +61 -1
- package/src/plugins/eventual-consistency/analytics.js +1 -0
- 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 +32 -7
- 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 +124 -32
- 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/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
package/src/resource.class.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { join } from "path";
|
|
2
2
|
import { createHash } from "crypto";
|
|
3
3
|
import AsyncEventEmitter from "./concerns/async-event-emitter.js";
|
|
4
|
-
import { customAlphabet, urlAlphabet } from 'nanoid';
|
|
5
4
|
import jsonStableStringify from "json-stable-stringify";
|
|
6
5
|
import { PromisePool } from "@supercharge/promise-pool";
|
|
7
6
|
import { chunk, cloneDeep, merge, isEmpty, isObject } from "lodash-es";
|
|
@@ -12,7 +11,7 @@ import { streamToString } from "./stream/index.js";
|
|
|
12
11
|
import tryFn, { tryFnSync } from "./concerns/try-fn.js";
|
|
13
12
|
import { ResourceReader, ResourceWriter } from "./stream/index.js"
|
|
14
13
|
import { getBehavior, DEFAULT_BEHAVIOR } from "./behaviors/index.js";
|
|
15
|
-
import { idGenerator as defaultIdGenerator } from "./concerns/id.js";
|
|
14
|
+
import { idGenerator as defaultIdGenerator, createCustomGenerator, getUrlAlphabet } from "./concerns/id.js";
|
|
16
15
|
import { calculateTotalSize, calculateEffectiveLimit } from "./concerns/calculator.js";
|
|
17
16
|
import { mapAwsError, InvalidResourceItem, ResourceError, PartitionError, ValidationError } from "./errors.js";
|
|
18
17
|
|
|
@@ -26,7 +25,8 @@ export class Resource extends AsyncEventEmitter {
|
|
|
26
25
|
* @param {string} [config.version='v1'] - Resource version
|
|
27
26
|
* @param {Object} [config.attributes={}] - Resource attributes schema
|
|
28
27
|
* @param {string} [config.behavior='user-managed'] - Resource behavior strategy
|
|
29
|
-
* @param {string} [config.passphrase='secret'] - Encryption passphrase
|
|
28
|
+
* @param {string} [config.passphrase='secret'] - Encryption passphrase (for 'secret' type)
|
|
29
|
+
* @param {number} [config.bcryptRounds=10] - Bcrypt rounds (for 'password' type)
|
|
30
30
|
* @param {number} [config.parallelism=10] - Parallelism for bulk operations
|
|
31
31
|
* @param {Array} [config.observers=[]] - Observer instances
|
|
32
32
|
* @param {boolean} [config.cache=false] - Enable caching
|
|
@@ -123,6 +123,7 @@ export class Resource extends AsyncEventEmitter {
|
|
|
123
123
|
attributes = {},
|
|
124
124
|
behavior = DEFAULT_BEHAVIOR,
|
|
125
125
|
passphrase = 'secret',
|
|
126
|
+
bcryptRounds = 10,
|
|
126
127
|
parallelism = 10,
|
|
127
128
|
observers = [],
|
|
128
129
|
cache = false,
|
|
@@ -140,7 +141,8 @@ export class Resource extends AsyncEventEmitter {
|
|
|
140
141
|
asyncEvents = true,
|
|
141
142
|
asyncPartitions = true,
|
|
142
143
|
strictPartitions = false,
|
|
143
|
-
createdBy = 'user'
|
|
144
|
+
createdBy = 'user',
|
|
145
|
+
guard
|
|
144
146
|
} = config;
|
|
145
147
|
|
|
146
148
|
// Set instance properties
|
|
@@ -151,6 +153,7 @@ export class Resource extends AsyncEventEmitter {
|
|
|
151
153
|
this.observers = observers;
|
|
152
154
|
this.parallelism = parallelism;
|
|
153
155
|
this.passphrase = passphrase ?? 'secret';
|
|
156
|
+
this.bcryptRounds = bcryptRounds;
|
|
154
157
|
this.versioningEnabled = versioningEnabled;
|
|
155
158
|
this.strictValidation = strictValidation;
|
|
156
159
|
|
|
@@ -281,6 +284,10 @@ export class Resource extends AsyncEventEmitter {
|
|
|
281
284
|
}
|
|
282
285
|
}
|
|
283
286
|
|
|
287
|
+
// --- GUARDS SYSTEM ---
|
|
288
|
+
// Normalize and store guards (framework-agnostic authorization)
|
|
289
|
+
this.guard = this._normalizeGuard(guard);
|
|
290
|
+
|
|
284
291
|
// --- MIDDLEWARE SYSTEM ---
|
|
285
292
|
this._initMiddleware();
|
|
286
293
|
// Debug: print method names and typeof update at construction
|
|
@@ -303,11 +310,11 @@ export class Resource extends AsyncEventEmitter {
|
|
|
303
310
|
}
|
|
304
311
|
// If customIdGenerator is a number (size), create a generator with that size
|
|
305
312
|
if (typeof customIdGenerator === 'number' && customIdGenerator > 0) {
|
|
306
|
-
return
|
|
313
|
+
return createCustomGenerator(getUrlAlphabet(), customIdGenerator);
|
|
307
314
|
}
|
|
308
315
|
// If idSize is provided, create a generator with that size
|
|
309
316
|
if (typeof idSize === 'number' && idSize > 0 && idSize !== 22) {
|
|
310
|
-
return
|
|
317
|
+
return createCustomGenerator(getUrlAlphabet(), idSize);
|
|
311
318
|
}
|
|
312
319
|
// Default to the standard idGenerator (22 chars)
|
|
313
320
|
return defaultIdGenerator;
|
|
@@ -400,6 +407,7 @@ export class Resource extends AsyncEventEmitter {
|
|
|
400
407
|
name: this.name,
|
|
401
408
|
attributes: this.attributes,
|
|
402
409
|
passphrase: this.passphrase,
|
|
410
|
+
bcryptRounds: this.bcryptRounds,
|
|
403
411
|
version: this.version,
|
|
404
412
|
options: {
|
|
405
413
|
autoDecrypt: this.config.autoDecrypt,
|
|
@@ -1060,9 +1068,7 @@ export class Resource extends AsyncEventEmitter {
|
|
|
1060
1068
|
* });
|
|
1061
1069
|
*/
|
|
1062
1070
|
async insert({ id, ...attributes }) {
|
|
1063
|
-
const
|
|
1064
|
-
if (exists) throw new Error(`Resource with id '${id}' already exists`);
|
|
1065
|
-
const keyDebug = this.getResourceKey(id || '(auto)');
|
|
1071
|
+
const providedId = id !== undefined && id !== null && String(id).trim() !== '';
|
|
1066
1072
|
if (this.config.timestamps) {
|
|
1067
1073
|
attributes.createdAt = new Date().toISOString();
|
|
1068
1074
|
attributes.updatedAt = new Date().toISOString();
|
|
@@ -1086,11 +1092,12 @@ export class Resource extends AsyncEventEmitter {
|
|
|
1086
1092
|
const extraData = {};
|
|
1087
1093
|
for (const k of extraProps) extraData[k] = preProcessedData[k];
|
|
1088
1094
|
|
|
1095
|
+
const shouldValidateId = preProcessedData.id !== undefined && preProcessedData.id !== null;
|
|
1089
1096
|
const {
|
|
1090
1097
|
errors,
|
|
1091
1098
|
isValid,
|
|
1092
1099
|
data: validated,
|
|
1093
|
-
} = await this.validate(preProcessedData, { includeId:
|
|
1100
|
+
} = await this.validate(preProcessedData, { includeId: shouldValidateId });
|
|
1094
1101
|
|
|
1095
1102
|
if (!isValid) {
|
|
1096
1103
|
const errorMsg = (errors && errors.length && errors[0].message) ? errors[0].message : 'Insert failed';
|
|
@@ -1109,7 +1116,7 @@ export class Resource extends AsyncEventEmitter {
|
|
|
1109
1116
|
Object.assign(validatedAttributes, extraData);
|
|
1110
1117
|
|
|
1111
1118
|
// Generate ID with fallback for empty generators
|
|
1112
|
-
let finalId = validatedId || id;
|
|
1119
|
+
let finalId = validatedId || preProcessedData.id || id;
|
|
1113
1120
|
if (!finalId) {
|
|
1114
1121
|
finalId = this.idGenerator();
|
|
1115
1122
|
// Fallback to default generator if custom generator returns empty
|
|
@@ -1133,6 +1140,31 @@ export class Resource extends AsyncEventEmitter {
|
|
|
1133
1140
|
|
|
1134
1141
|
// Add version metadata (required for all objects)
|
|
1135
1142
|
const finalMetadata = processedMetadata;
|
|
1143
|
+
|
|
1144
|
+
if (!finalId || String(finalId).trim() === '') {
|
|
1145
|
+
throw new InvalidResourceItem({
|
|
1146
|
+
bucket: this.client.config.bucket,
|
|
1147
|
+
resourceName: this.name,
|
|
1148
|
+
attributes: preProcessedData,
|
|
1149
|
+
validation: [{ message: 'Generated ID is invalid', field: 'id' }],
|
|
1150
|
+
message: 'Generated ID is invalid'
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
const shouldCheckExists = providedId || shouldValidateId || validatedId !== undefined;
|
|
1155
|
+
if (shouldCheckExists) {
|
|
1156
|
+
const alreadyExists = await this.exists(finalId);
|
|
1157
|
+
if (alreadyExists) {
|
|
1158
|
+
throw new InvalidResourceItem({
|
|
1159
|
+
bucket: this.client.config.bucket,
|
|
1160
|
+
resourceName: this.name,
|
|
1161
|
+
attributes: preProcessedData,
|
|
1162
|
+
validation: [{ message: `Resource with id '${finalId}' already exists`, field: 'id' }],
|
|
1163
|
+
message: `Resource with id '${finalId}' already exists`
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1136
1168
|
const key = this.getResourceKey(finalId);
|
|
1137
1169
|
// Determine content type based on body content
|
|
1138
1170
|
let contentType = undefined;
|
|
@@ -3653,6 +3685,102 @@ export class Resource extends AsyncEventEmitter {
|
|
|
3653
3685
|
return filtered;
|
|
3654
3686
|
}
|
|
3655
3687
|
|
|
3688
|
+
// --- GUARDS SYSTEM ---
|
|
3689
|
+
/**
|
|
3690
|
+
* Normalize guard configuration
|
|
3691
|
+
* @param {Object|Array|undefined} guard - Guard configuration
|
|
3692
|
+
* @returns {Object|null} Normalized guard config
|
|
3693
|
+
* @private
|
|
3694
|
+
*/
|
|
3695
|
+
_normalizeGuard(guard) {
|
|
3696
|
+
if (!guard) return null;
|
|
3697
|
+
|
|
3698
|
+
// String array simples → aplica para todas as operações
|
|
3699
|
+
if (Array.isArray(guard)) {
|
|
3700
|
+
return { '*': guard };
|
|
3701
|
+
}
|
|
3702
|
+
|
|
3703
|
+
return guard;
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3706
|
+
/**
|
|
3707
|
+
* Execute guard for operation
|
|
3708
|
+
* @param {string} operation - Operation name (list, get, insert, update, etc)
|
|
3709
|
+
* @param {Object} context - Framework-agnostic context
|
|
3710
|
+
* @param {Object} context.user - Decoded JWT token
|
|
3711
|
+
* @param {Object} context.params - Route params
|
|
3712
|
+
* @param {Object} context.body - Request body
|
|
3713
|
+
* @param {Object} context.query - Query string
|
|
3714
|
+
* @param {Object} context.headers - Request headers
|
|
3715
|
+
* @param {Function} context.setPartition - Helper to set partition
|
|
3716
|
+
* @param {Object} [resource] - Resource record (for get/update/delete)
|
|
3717
|
+
* @returns {Promise<boolean>} True if allowed, false if denied
|
|
3718
|
+
*/
|
|
3719
|
+
async executeGuard(operation, context, resource = null) {
|
|
3720
|
+
if (!this.guard) return true; // No guard = allow
|
|
3721
|
+
|
|
3722
|
+
// 1. Try operation-specific guard
|
|
3723
|
+
let guardFn = this.guard[operation];
|
|
3724
|
+
|
|
3725
|
+
// 2. Fallback to wildcard
|
|
3726
|
+
if (!guardFn) {
|
|
3727
|
+
guardFn = this.guard['*'];
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
// 3. No guard = allow
|
|
3731
|
+
if (!guardFn) return true;
|
|
3732
|
+
|
|
3733
|
+
// 4. Boolean simple
|
|
3734
|
+
if (typeof guardFn === 'boolean') {
|
|
3735
|
+
return guardFn;
|
|
3736
|
+
}
|
|
3737
|
+
|
|
3738
|
+
// 5. Array of roles/scopes
|
|
3739
|
+
if (Array.isArray(guardFn)) {
|
|
3740
|
+
return this._checkRolesScopes(guardFn, context.user);
|
|
3741
|
+
}
|
|
3742
|
+
|
|
3743
|
+
// 6. Custom function
|
|
3744
|
+
if (typeof guardFn === 'function') {
|
|
3745
|
+
try {
|
|
3746
|
+
const result = await guardFn(context, resource);
|
|
3747
|
+
return result === true; // Force boolean
|
|
3748
|
+
} catch (err) {
|
|
3749
|
+
// Guard error = deny access
|
|
3750
|
+
console.error(`Guard error for ${operation}:`, err);
|
|
3751
|
+
return false;
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
|
|
3755
|
+
return false; // Default: deny
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3758
|
+
/**
|
|
3759
|
+
* Check if user has required roles or scopes
|
|
3760
|
+
* @param {Array<string>} requiredRolesScopes - Required roles/scopes
|
|
3761
|
+
* @param {Object} user - User from JWT token
|
|
3762
|
+
* @returns {boolean} True if user has any of required roles/scopes
|
|
3763
|
+
* @private
|
|
3764
|
+
*/
|
|
3765
|
+
_checkRolesScopes(requiredRolesScopes, user) {
|
|
3766
|
+
if (!user) return false;
|
|
3767
|
+
|
|
3768
|
+
// User scopes (OpenID scope claim)
|
|
3769
|
+
const userScopes = user.scope?.split(' ') || [];
|
|
3770
|
+
|
|
3771
|
+
// User roles - support multiple formats (Keycloak, Azure AD)
|
|
3772
|
+
const clientId = user.azp || process.env.CLIENT_ID || 'default';
|
|
3773
|
+
const clientRoles = user.resource_access?.[clientId]?.roles || [];
|
|
3774
|
+
const realmRoles = user.realm_access?.roles || [];
|
|
3775
|
+
const azureRoles = user.roles || [];
|
|
3776
|
+
const userRoles = [...clientRoles, ...realmRoles, ...azureRoles];
|
|
3777
|
+
|
|
3778
|
+
// Check if user has any of required
|
|
3779
|
+
return requiredRolesScopes.some(required => {
|
|
3780
|
+
return userScopes.includes(required) || userRoles.includes(required);
|
|
3781
|
+
});
|
|
3782
|
+
}
|
|
3783
|
+
|
|
3656
3784
|
// --- MIDDLEWARE SYSTEM ---
|
|
3657
3785
|
_initMiddleware() {
|
|
3658
3786
|
// Map of methodName -> array of middleware functions
|
|
@@ -4012,4 +4140,4 @@ function validateResourceConfig(config) {
|
|
|
4012
4140
|
};
|
|
4013
4141
|
}
|
|
4014
4142
|
|
|
4015
|
-
export default Resource;
|
|
4143
|
+
export default Resource;
|
package/src/schema.class.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "lodash-es";
|
|
14
14
|
|
|
15
15
|
import { encrypt, decrypt } from "./concerns/crypto.js";
|
|
16
|
+
import { hashPassword, compactHash } from "./concerns/password-hashing.js";
|
|
16
17
|
import { ValidatorManager } from "./validator.class.js";
|
|
17
18
|
import { tryFn, tryFnSync } from "./concerns/try-fn.js";
|
|
18
19
|
import { SchemaError } from "./errors.js";
|
|
@@ -121,6 +122,16 @@ export const SchemaActions = {
|
|
|
121
122
|
return raw;
|
|
122
123
|
},
|
|
123
124
|
|
|
125
|
+
hashPassword: async (value, { bcryptRounds = 10 }) => {
|
|
126
|
+
if (value === null || value === undefined) return value;
|
|
127
|
+
// Hash with bcrypt
|
|
128
|
+
const [okHash, errHash, hash] = await tryFn(() => hashPassword(String(value), bcryptRounds));
|
|
129
|
+
if (!okHash) return value; // Return original on error
|
|
130
|
+
// Compact hash to save space (60 → 53 bytes)
|
|
131
|
+
const [okCompact, errCompact, compacted] = tryFnSync(() => compactHash(hash));
|
|
132
|
+
return okCompact ? compacted : hash; // Return compacted or fallback to full hash
|
|
133
|
+
},
|
|
134
|
+
|
|
124
135
|
toString: (value) => value == null ? value : String(value),
|
|
125
136
|
|
|
126
137
|
fromArray: (value, { separator }) => {
|
|
@@ -535,6 +546,7 @@ export class Schema {
|
|
|
535
546
|
name,
|
|
536
547
|
attributes,
|
|
537
548
|
passphrase,
|
|
549
|
+
bcryptRounds,
|
|
538
550
|
version = 1,
|
|
539
551
|
options = {},
|
|
540
552
|
_pluginAttributeMetadata,
|
|
@@ -545,6 +557,7 @@ export class Schema {
|
|
|
545
557
|
this.version = version;
|
|
546
558
|
this.attributes = attributes || {};
|
|
547
559
|
this.passphrase = passphrase ?? "secret";
|
|
560
|
+
this.bcryptRounds = bcryptRounds ?? 10;
|
|
548
561
|
this.options = merge({}, this.defaultOptions(), options);
|
|
549
562
|
this.allNestedObjectsOptional = this.options.allNestedObjectsOptional ?? false;
|
|
550
563
|
|
|
@@ -555,7 +568,11 @@ export class Schema {
|
|
|
555
568
|
// Preprocess attributes to handle nested objects for validator compilation
|
|
556
569
|
const processedAttributes = this.preprocessAttributesForValidation(this.attributes);
|
|
557
570
|
|
|
558
|
-
this.validator = new ValidatorManager({
|
|
571
|
+
this.validator = new ValidatorManager({
|
|
572
|
+
autoEncrypt: false,
|
|
573
|
+
passphrase: this.passphrase,
|
|
574
|
+
bcryptRounds: this.bcryptRounds
|
|
575
|
+
}).compile(merge(
|
|
559
576
|
{ $$async: true, $$strict: false },
|
|
560
577
|
processedAttributes,
|
|
561
578
|
))
|
|
@@ -824,6 +841,16 @@ export class Schema {
|
|
|
824
841
|
continue;
|
|
825
842
|
}
|
|
826
843
|
|
|
844
|
+
// Handle passwords
|
|
845
|
+
if (defStr.includes("password") || defType === 'password') {
|
|
846
|
+
if (this.options.autoEncrypt) {
|
|
847
|
+
this.addHook("beforeMap", name, "hashPassword");
|
|
848
|
+
}
|
|
849
|
+
// No afterUnmap hook - passwords are one-way hashed
|
|
850
|
+
// Skip other processing for passwords
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
|
|
827
854
|
// Handle ip4 type
|
|
828
855
|
if (defStr.includes("ip4") || defType === 'ip4') {
|
|
829
856
|
this.addHook("beforeMap", name, "encodeIPv4");
|
|
@@ -1067,6 +1094,7 @@ export class Schema {
|
|
|
1067
1094
|
if (value !== undefined && typeof SchemaActions[actionName] === 'function') {
|
|
1068
1095
|
set(cloned, attribute, await SchemaActions[actionName](value, {
|
|
1069
1096
|
passphrase: this.passphrase,
|
|
1097
|
+
bcryptRounds: this.bcryptRounds,
|
|
1070
1098
|
separator: this.options.arraySeparator,
|
|
1071
1099
|
...actionParams // Merge custom parameters (currency, precision, etc.)
|
|
1072
1100
|
}))
|
|
@@ -1181,6 +1209,7 @@ export class Schema {
|
|
|
1181
1209
|
if (typeof SchemaActions[actionName] === 'function') {
|
|
1182
1210
|
parsedValue = await SchemaActions[actionName](parsedValue, {
|
|
1183
1211
|
passphrase: this.passphrase,
|
|
1212
|
+
bcryptRounds: this.bcryptRounds,
|
|
1184
1213
|
separator: this.options.arraySeparator,
|
|
1185
1214
|
...actionParams // Merge custom parameters (currency, precision, etc.)
|
|
1186
1215
|
});
|
package/src/validator.class.js
CHANGED
|
@@ -2,10 +2,11 @@ import { merge, isString } from "lodash-es";
|
|
|
2
2
|
import FastestValidator from "fastest-validator";
|
|
3
3
|
|
|
4
4
|
import { encrypt } from "./concerns/crypto.js";
|
|
5
|
+
import { hashPassword, hashPasswordSync, compactHash } from "./concerns/password-hashing.js";
|
|
5
6
|
import tryFn, { tryFnSync } from "./concerns/try-fn.js";
|
|
6
7
|
import { ValidationError } from "./errors.js";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
function secretHandler (actual, errors, schema) {
|
|
9
10
|
if (!this.passphrase) {
|
|
10
11
|
errors.push(new ValidationError("Missing configuration for secrets encryption.", {
|
|
11
12
|
actual,
|
|
@@ -15,7 +16,7 @@ async function secretHandler (actual, errors, schema) {
|
|
|
15
16
|
return actual;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
const [ok, err, res] =
|
|
19
|
+
const [ok, err, res] = tryFnSync(() => encrypt(String(actual), this.passphrase));
|
|
19
20
|
if (ok) return res;
|
|
20
21
|
errors.push(new ValidationError("Problem encrypting secret.", {
|
|
21
22
|
actual,
|
|
@@ -26,7 +27,44 @@ async function secretHandler (actual, errors, schema) {
|
|
|
26
27
|
return actual;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
function passwordHandler (actual, errors, schema) {
|
|
31
|
+
if (!this.bcryptRounds) {
|
|
32
|
+
errors.push(new ValidationError("Missing bcrypt rounds configuration.", {
|
|
33
|
+
actual,
|
|
34
|
+
type: "bcryptRoundsMissing",
|
|
35
|
+
suggestion: "Provide bcryptRounds in database configuration."
|
|
36
|
+
}));
|
|
37
|
+
return actual;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Hash password with bcrypt (synchronous)
|
|
41
|
+
const [okHash, errHash, hash] = tryFnSync(() => hashPasswordSync(String(actual), this.bcryptRounds));
|
|
42
|
+
if (!okHash) {
|
|
43
|
+
errors.push(new ValidationError("Problem hashing password.", {
|
|
44
|
+
actual,
|
|
45
|
+
type: "passwordHashingProblem",
|
|
46
|
+
error: errHash,
|
|
47
|
+
suggestion: "Check the bcryptRounds configuration and password value."
|
|
48
|
+
}));
|
|
49
|
+
return actual;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Compact hash to save space (60 bytes → 53 bytes)
|
|
53
|
+
const [okCompact, errCompact, compacted] = tryFnSync(() => compactHash(hash));
|
|
54
|
+
if (!okCompact) {
|
|
55
|
+
errors.push(new ValidationError("Problem compacting password hash.", {
|
|
56
|
+
actual,
|
|
57
|
+
type: "hashCompactionProblem",
|
|
58
|
+
error: errCompact,
|
|
59
|
+
suggestion: "Bcrypt hash format may be invalid."
|
|
60
|
+
}));
|
|
61
|
+
return hash; // Return uncompacted as fallback
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return compacted;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function jsonHandler (actual, errors, schema) {
|
|
30
68
|
if (isString(actual)) return actual;
|
|
31
69
|
const [ok, err, json] = tryFnSync(() => JSON.stringify(actual));
|
|
32
70
|
if (!ok) throw new ValidationError("Failed to stringify JSON", { original: err, input: actual });
|
|
@@ -34,13 +72,15 @@ async function jsonHandler (actual, errors, schema) {
|
|
|
34
72
|
}
|
|
35
73
|
|
|
36
74
|
export class Validator extends FastestValidator {
|
|
37
|
-
constructor({ options, passphrase, autoEncrypt = true } = {}) {
|
|
75
|
+
constructor({ options, passphrase, bcryptRounds = 10, autoEncrypt = true, autoHash = true } = {}) {
|
|
38
76
|
super(merge({}, {
|
|
39
77
|
useNewCustomCheckerFunction: true,
|
|
40
78
|
|
|
41
79
|
messages: {
|
|
42
80
|
encryptionKeyMissing: "Missing configuration for secrets encryption.",
|
|
43
81
|
encryptionProblem: "Problem encrypting secret. Actual: {actual}. Error: {error}",
|
|
82
|
+
bcryptRoundsMissing: "Missing bcrypt rounds configuration for password hashing.",
|
|
83
|
+
passwordHashingProblem: "Problem hashing password. Error: {error}",
|
|
44
84
|
},
|
|
45
85
|
|
|
46
86
|
defaults: {
|
|
@@ -57,7 +97,9 @@ export class Validator extends FastestValidator {
|
|
|
57
97
|
}, options))
|
|
58
98
|
|
|
59
99
|
this.passphrase = passphrase;
|
|
60
|
-
this.
|
|
100
|
+
this.bcryptRounds = bcryptRounds;
|
|
101
|
+
this.autoEncrypt = autoEncrypt; // Controls automatic encryption of 'secret' type fields
|
|
102
|
+
this.autoHash = autoHash; // Controls automatic hashing of 'password' type fields
|
|
61
103
|
|
|
62
104
|
this.alias('secret', {
|
|
63
105
|
type: "string",
|
|
@@ -73,11 +115,20 @@ export class Validator extends FastestValidator {
|
|
|
73
115
|
custom: this.autoEncrypt ? secretHandler : undefined,
|
|
74
116
|
})
|
|
75
117
|
|
|
76
|
-
this.alias('secretNumber', {
|
|
118
|
+
this.alias('secretNumber', {
|
|
77
119
|
type: "number",
|
|
78
120
|
custom: this.autoEncrypt ? secretHandler : undefined,
|
|
79
121
|
})
|
|
80
122
|
|
|
123
|
+
this.alias('password', {
|
|
124
|
+
type: "string",
|
|
125
|
+
custom: this.autoHash ? passwordHandler : undefined,
|
|
126
|
+
messages: {
|
|
127
|
+
string: "The '{field}' field must be a string.",
|
|
128
|
+
stringMin: "This password '{field}' field length must be at least {expected} long.",
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
|
|
81
132
|
this.alias('json', {
|
|
82
133
|
type: "any",
|
|
83
134
|
custom: this.autoEncrypt ? jsonHandler : undefined,
|