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.
Files changed (105) hide show
  1. package/README.md +25 -10
  2. package/dist/{s3db.cjs.js → s3db.cjs} +30323 -24958
  3. package/dist/s3db.cjs.map +1 -0
  4. package/dist/s3db.es.js +24026 -18654
  5. package/dist/s3db.es.js.map +1 -1
  6. package/package.json +216 -20
  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 +4 -0
  11. package/src/plugins/api/auth/basic-auth.js +23 -1
  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 +503 -54
  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 +23 -3
  28. package/src/plugins/api/routes/resource-routes.js +71 -29
  29. package/src/plugins/api/server.js +1017 -94
  30. package/src/plugins/api/utils/guards.js +213 -0
  31. package/src/plugins/api/utils/mime-types.js +154 -0
  32. package/src/plugins/api/utils/openapi-generator.js +44 -11
  33. package/src/plugins/api/utils/path-matcher.js +173 -0
  34. package/src/plugins/api/utils/static-filesystem.js +262 -0
  35. package/src/plugins/api/utils/static-s3.js +231 -0
  36. package/src/plugins/api/utils/template-engine.js +188 -0
  37. package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +853 -0
  38. package/src/plugins/cloud-inventory/drivers/aws-driver.js +2554 -0
  39. package/src/plugins/cloud-inventory/drivers/azure-driver.js +637 -0
  40. package/src/plugins/cloud-inventory/drivers/base-driver.js +99 -0
  41. package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +620 -0
  42. package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +698 -0
  43. package/src/plugins/cloud-inventory/drivers/gcp-driver.js +645 -0
  44. package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +559 -0
  45. package/src/plugins/cloud-inventory/drivers/linode-driver.js +614 -0
  46. package/src/plugins/cloud-inventory/drivers/mock-drivers.js +449 -0
  47. package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +771 -0
  48. package/src/plugins/cloud-inventory/drivers/oracle-driver.js +768 -0
  49. package/src/plugins/cloud-inventory/drivers/vultr-driver.js +636 -0
  50. package/src/plugins/cloud-inventory/index.js +20 -0
  51. package/src/plugins/cloud-inventory/registry.js +146 -0
  52. package/src/plugins/cloud-inventory/terraform-exporter.js +362 -0
  53. package/src/plugins/cloud-inventory.plugin.js +1333 -0
  54. package/src/plugins/concerns/plugin-dependencies.js +61 -1
  55. package/src/plugins/eventual-consistency/analytics.js +1 -0
  56. package/src/plugins/identity/README.md +335 -0
  57. package/src/plugins/identity/concerns/mfa-manager.js +204 -0
  58. package/src/plugins/identity/concerns/password.js +138 -0
  59. package/src/plugins/identity/concerns/resource-schemas.js +273 -0
  60. package/src/plugins/identity/concerns/token-generator.js +172 -0
  61. package/src/plugins/identity/email-service.js +422 -0
  62. package/src/plugins/identity/index.js +1052 -0
  63. package/src/plugins/identity/oauth2-server.js +1033 -0
  64. package/src/plugins/identity/oidc-discovery.js +285 -0
  65. package/src/plugins/identity/rsa-keys.js +323 -0
  66. package/src/plugins/identity/server.js +500 -0
  67. package/src/plugins/identity/session-manager.js +453 -0
  68. package/src/plugins/identity/ui/layouts/base.js +251 -0
  69. package/src/plugins/identity/ui/middleware.js +135 -0
  70. package/src/plugins/identity/ui/pages/admin/client-form.js +247 -0
  71. package/src/plugins/identity/ui/pages/admin/clients.js +179 -0
  72. package/src/plugins/identity/ui/pages/admin/dashboard.js +181 -0
  73. package/src/plugins/identity/ui/pages/admin/user-form.js +283 -0
  74. package/src/plugins/identity/ui/pages/admin/users.js +263 -0
  75. package/src/plugins/identity/ui/pages/consent.js +262 -0
  76. package/src/plugins/identity/ui/pages/forgot-password.js +104 -0
  77. package/src/plugins/identity/ui/pages/login.js +144 -0
  78. package/src/plugins/identity/ui/pages/mfa-backup-codes.js +180 -0
  79. package/src/plugins/identity/ui/pages/mfa-enrollment.js +187 -0
  80. package/src/plugins/identity/ui/pages/mfa-verification.js +178 -0
  81. package/src/plugins/identity/ui/pages/oauth-error.js +225 -0
  82. package/src/plugins/identity/ui/pages/profile.js +361 -0
  83. package/src/plugins/identity/ui/pages/register.js +226 -0
  84. package/src/plugins/identity/ui/pages/reset-password.js +128 -0
  85. package/src/plugins/identity/ui/pages/verify-email.js +172 -0
  86. package/src/plugins/identity/ui/routes.js +2541 -0
  87. package/src/plugins/identity/ui/styles/main.css +465 -0
  88. package/src/plugins/index.js +4 -1
  89. package/src/plugins/ml/base-model.class.js +32 -7
  90. package/src/plugins/ml/classification-model.class.js +1 -1
  91. package/src/plugins/ml/timeseries-model.class.js +3 -1
  92. package/src/plugins/ml.plugin.js +124 -32
  93. package/src/plugins/shared/error-handler.js +147 -0
  94. package/src/plugins/shared/index.js +9 -0
  95. package/src/plugins/shared/middlewares/compression.js +117 -0
  96. package/src/plugins/shared/middlewares/cors.js +49 -0
  97. package/src/plugins/shared/middlewares/index.js +11 -0
  98. package/src/plugins/shared/middlewares/logging.js +54 -0
  99. package/src/plugins/shared/middlewares/rate-limit.js +73 -0
  100. package/src/plugins/shared/middlewares/security.js +158 -0
  101. package/src/plugins/shared/response-formatter.js +264 -0
  102. package/src/resource.class.js +140 -12
  103. package/src/schema.class.js +30 -1
  104. package/src/validator.class.js +57 -6
  105. package/dist/s3db.cjs.js.map +0 -1
@@ -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 customAlphabet(urlAlphabet, customIdGenerator);
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 customAlphabet(urlAlphabet, idSize);
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 exists = await this.exists(id);
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: true });
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;
@@ -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({ autoEncrypt: false }).compile(merge(
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
  });
@@ -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
- async function secretHandler (actual, errors, schema) {
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] = await tryFn(() => encrypt(String(actual), this.passphrase));
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
- async function jsonHandler (actual, errors, schema) {
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.autoEncrypt = autoEncrypt;
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,