s3db.js 11.3.2 → 12.0.1

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 (83) hide show
  1. package/README.md +102 -8
  2. package/dist/s3db.cjs.js +36945 -15510
  3. package/dist/s3db.cjs.js.map +1 -1
  4. package/dist/s3db.d.ts +66 -1
  5. package/dist/s3db.es.js +36914 -15534
  6. package/dist/s3db.es.js.map +1 -1
  7. package/mcp/entrypoint.js +58 -0
  8. package/mcp/tools/documentation.js +434 -0
  9. package/mcp/tools/index.js +4 -0
  10. package/package.json +35 -15
  11. package/src/behaviors/user-managed.js +13 -6
  12. package/src/client.class.js +79 -49
  13. package/src/concerns/base62.js +85 -0
  14. package/src/concerns/dictionary-encoding.js +294 -0
  15. package/src/concerns/geo-encoding.js +256 -0
  16. package/src/concerns/high-performance-inserter.js +34 -30
  17. package/src/concerns/ip.js +325 -0
  18. package/src/concerns/metadata-encoding.js +345 -66
  19. package/src/concerns/money.js +193 -0
  20. package/src/concerns/partition-queue.js +7 -4
  21. package/src/concerns/plugin-storage.js +97 -47
  22. package/src/database.class.js +76 -74
  23. package/src/errors.js +0 -4
  24. package/src/plugins/api/auth/api-key-auth.js +88 -0
  25. package/src/plugins/api/auth/basic-auth.js +154 -0
  26. package/src/plugins/api/auth/index.js +112 -0
  27. package/src/plugins/api/auth/jwt-auth.js +169 -0
  28. package/src/plugins/api/index.js +544 -0
  29. package/src/plugins/api/middlewares/index.js +15 -0
  30. package/src/plugins/api/middlewares/validator.js +185 -0
  31. package/src/plugins/api/routes/auth-routes.js +241 -0
  32. package/src/plugins/api/routes/resource-routes.js +304 -0
  33. package/src/plugins/api/server.js +354 -0
  34. package/src/plugins/api/utils/error-handler.js +147 -0
  35. package/src/plugins/api/utils/openapi-generator.js +1240 -0
  36. package/src/plugins/api/utils/response-formatter.js +218 -0
  37. package/src/plugins/backup/streaming-exporter.js +132 -0
  38. package/src/plugins/backup.plugin.js +103 -50
  39. package/src/plugins/cache/s3-cache.class.js +95 -47
  40. package/src/plugins/cache.plugin.js +107 -9
  41. package/src/plugins/concerns/plugin-dependencies.js +313 -0
  42. package/src/plugins/concerns/prometheus-formatter.js +255 -0
  43. package/src/plugins/consumers/rabbitmq-consumer.js +4 -0
  44. package/src/plugins/consumers/sqs-consumer.js +4 -0
  45. package/src/plugins/costs.plugin.js +255 -39
  46. package/src/plugins/eventual-consistency/helpers.js +15 -1
  47. package/src/plugins/geo.plugin.js +873 -0
  48. package/src/plugins/importer/index.js +1020 -0
  49. package/src/plugins/index.js +11 -0
  50. package/src/plugins/metrics.plugin.js +163 -4
  51. package/src/plugins/queue-consumer.plugin.js +6 -27
  52. package/src/plugins/relation.errors.js +139 -0
  53. package/src/plugins/relation.plugin.js +1242 -0
  54. package/src/plugins/replicator.plugin.js +2 -1
  55. package/src/plugins/replicators/bigquery-replicator.class.js +180 -8
  56. package/src/plugins/replicators/dynamodb-replicator.class.js +383 -0
  57. package/src/plugins/replicators/index.js +28 -3
  58. package/src/plugins/replicators/mongodb-replicator.class.js +391 -0
  59. package/src/plugins/replicators/mysql-replicator.class.js +558 -0
  60. package/src/plugins/replicators/planetscale-replicator.class.js +409 -0
  61. package/src/plugins/replicators/postgres-replicator.class.js +182 -7
  62. package/src/plugins/replicators/s3db-replicator.class.js +1 -12
  63. package/src/plugins/replicators/schema-sync.helper.js +601 -0
  64. package/src/plugins/replicators/sqs-replicator.class.js +11 -9
  65. package/src/plugins/replicators/turso-replicator.class.js +416 -0
  66. package/src/plugins/replicators/webhook-replicator.class.js +612 -0
  67. package/src/plugins/state-machine.plugin.js +122 -68
  68. package/src/plugins/tfstate/README.md +745 -0
  69. package/src/plugins/tfstate/base-driver.js +80 -0
  70. package/src/plugins/tfstate/errors.js +112 -0
  71. package/src/plugins/tfstate/filesystem-driver.js +129 -0
  72. package/src/plugins/tfstate/index.js +2660 -0
  73. package/src/plugins/tfstate/s3-driver.js +192 -0
  74. package/src/plugins/ttl.plugin.js +536 -0
  75. package/src/resource.class.js +315 -36
  76. package/src/s3db.d.ts +66 -1
  77. package/src/schema.class.js +366 -32
  78. package/SECURITY.md +0 -76
  79. package/src/partition-drivers/base-partition-driver.js +0 -106
  80. package/src/partition-drivers/index.js +0 -66
  81. package/src/partition-drivers/memory-partition-driver.js +0 -289
  82. package/src/partition-drivers/sqs-partition-driver.js +0 -337
  83. package/src/partition-drivers/sync-partition-driver.js +0 -38
@@ -15,7 +15,10 @@ import { encrypt, decrypt } from "./concerns/crypto.js";
15
15
  import { ValidatorManager } from "./validator.class.js";
16
16
  import { tryFn, tryFnSync } from "./concerns/try-fn.js";
17
17
  import { SchemaError } from "./errors.js";
18
- import { encode as toBase62, decode as fromBase62, encodeDecimal, decodeDecimal, encodeFixedPoint, decodeFixedPoint } from "./concerns/base62.js";
18
+ import { encode as toBase62, decode as fromBase62, encodeDecimal, decodeDecimal, encodeFixedPoint, decodeFixedPoint, encodeFixedPointBatch, decodeFixedPointBatch } from "./concerns/base62.js";
19
+ import { encodeIPv4, decodeIPv4, encodeIPv6, decodeIPv6, isValidIPv4, isValidIPv6 } from "./concerns/ip.js";
20
+ import { encodeMoney, decodeMoney, getCurrencyDecimals } from "./concerns/money.js";
21
+ import { encodeGeoLat, decodeGeoLat, encodeGeoLon, decodeGeoLon, encodeGeoPoint, decodeGeoPoint } from "./concerns/geo-encoding.js";
19
22
 
20
23
  /**
21
24
  * Generate base62 mapping for attributes
@@ -279,29 +282,33 @@ export const SchemaActions = {
279
282
  return value;
280
283
  }
281
284
  if (value.length === 0) {
282
- return '';
285
+ return '^[]';
283
286
  }
284
- const encodedItems = value.map(item => {
285
- if (typeof item === 'number' && !isNaN(item)) {
286
- return encodeFixedPoint(item, precision);
287
- }
288
- // fallback: try to parse as number, else keep as is
289
- const n = Number(item);
290
- return isNaN(n) ? '' : encodeFixedPoint(n, precision);
291
- });
292
- return encodedItems.join(separator);
287
+ // Use batch encoding for massive compression (17% additional savings)
288
+ // Format: ^[val1,val2,val3,...] instead of ^val1,^val2,^val3,...
289
+ return encodeFixedPointBatch(value, precision);
293
290
  },
294
291
  toArrayOfEmbeddings: (value, { separator, precision = 6 }) => {
295
292
  if (Array.isArray(value)) {
296
- return value.map(v => (typeof v === 'number' ? v : decodeFixedPoint(v, precision)));
293
+ // Already an array, return as-is
294
+ return value;
297
295
  }
298
296
  if (value === null || value === undefined) {
299
297
  return value;
300
298
  }
301
- if (value === '') {
299
+ if (value === '' || value === '^[]') {
302
300
  return [];
303
301
  }
302
+
304
303
  const str = String(value);
304
+
305
+ // Check if this is batch-encoded (^[...])
306
+ if (str.startsWith('^[')) {
307
+ return decodeFixedPointBatch(str, precision);
308
+ }
309
+
310
+ // Fallback: Legacy format with individual prefixes (^val,^val,^val)
311
+ // This maintains backwards compatibility with data encoded before batch optimization
305
312
  const items = [];
306
313
  let current = '';
307
314
  let i = 0;
@@ -329,6 +336,129 @@ export const SchemaActions = {
329
336
  });
330
337
  },
331
338
 
339
+ encodeIPv4: (value) => {
340
+ if (value === null || value === undefined) return value;
341
+ if (typeof value !== 'string') return value;
342
+ if (!isValidIPv4(value)) return value;
343
+ const [ok, err, encoded] = tryFnSync(() => encodeIPv4(value));
344
+ return ok ? encoded : value;
345
+ },
346
+ decodeIPv4: (value) => {
347
+ if (value === null || value === undefined) return value;
348
+ if (typeof value !== 'string') return value;
349
+ const [ok, err, decoded] = tryFnSync(() => decodeIPv4(value));
350
+ return ok ? decoded : value;
351
+ },
352
+
353
+ encodeIPv6: (value) => {
354
+ if (value === null || value === undefined) return value;
355
+ if (typeof value !== 'string') return value;
356
+ if (!isValidIPv6(value)) return value;
357
+ const [ok, err, encoded] = tryFnSync(() => encodeIPv6(value));
358
+ return ok ? encoded : value;
359
+ },
360
+ decodeIPv6: (value) => {
361
+ if (value === null || value === undefined) return value;
362
+ if (typeof value !== 'string') return value;
363
+ const [ok, err, decoded] = tryFnSync(() => decodeIPv6(value));
364
+ return ok ? decoded : value;
365
+ },
366
+
367
+ // Money type - Integer-based (banking standard)
368
+ // Simplified approach: decimals instead of currency
369
+ encodeMoney: (value, { decimals = 2 } = {}) => {
370
+ if (value === null || value === undefined) return value;
371
+ if (typeof value !== 'number') return value;
372
+
373
+ // Use decimal places directly instead of currency lookup
374
+ const multiplier = Math.pow(10, decimals);
375
+ const integerValue = Math.round(value * multiplier);
376
+
377
+ // Encode as base62 with $ prefix
378
+ const [ok, err, encoded] = tryFnSync(() => '$' + toBase62(integerValue));
379
+ return ok ? encoded : value;
380
+ },
381
+ decodeMoney: (value, { decimals = 2 } = {}) => {
382
+ if (value === null || value === undefined) return value;
383
+ if (typeof value !== 'string') return value;
384
+ if (!value.startsWith('$')) return value;
385
+
386
+ // Decode base62 and convert back to decimal
387
+ const [ok, err, integerValue] = tryFnSync(() => fromBase62(value.slice(1)));
388
+ if (!ok || isNaN(integerValue)) return value;
389
+
390
+ const divisor = Math.pow(10, decimals);
391
+ return integerValue / divisor;
392
+ },
393
+
394
+ // Decimal type - Fixed-point for non-monetary decimals
395
+ encodeDecimalFixed: (value, { precision = 2 } = {}) => {
396
+ if (value === null || value === undefined) return value;
397
+ if (typeof value !== 'number') return value;
398
+ const [ok, err, encoded] = tryFnSync(() => encodeFixedPoint(value, precision));
399
+ return ok ? encoded : value;
400
+ },
401
+ decodeDecimalFixed: (value, { precision = 2 } = {}) => {
402
+ if (value === null || value === undefined) return value;
403
+ if (typeof value !== 'string') return value;
404
+ const [ok, err, decoded] = tryFnSync(() => decodeFixedPoint(value, precision));
405
+ return ok ? decoded : value;
406
+ },
407
+
408
+ // Geo types - Latitude
409
+ encodeGeoLatitude: (value, { precision = 6 } = {}) => {
410
+ if (value === null || value === undefined) return value;
411
+ if (typeof value !== 'number') return value;
412
+ const [ok, err, encoded] = tryFnSync(() => encodeGeoLat(value, precision));
413
+ return ok ? encoded : value;
414
+ },
415
+ decodeGeoLatitude: (value, { precision = 6 } = {}) => {
416
+ if (value === null || value === undefined) return value;
417
+ if (typeof value !== 'string') return value;
418
+ const [ok, err, decoded] = tryFnSync(() => decodeGeoLat(value, precision));
419
+ return ok ? decoded : value;
420
+ },
421
+
422
+ // Geo types - Longitude
423
+ encodeGeoLongitude: (value, { precision = 6 } = {}) => {
424
+ if (value === null || value === undefined) return value;
425
+ if (typeof value !== 'number') return value;
426
+ const [ok, err, encoded] = tryFnSync(() => encodeGeoLon(value, precision));
427
+ return ok ? encoded : value;
428
+ },
429
+ decodeGeoLongitude: (value, { precision = 6 } = {}) => {
430
+ if (value === null || value === undefined) return value;
431
+ if (typeof value !== 'string') return value;
432
+ const [ok, err, decoded] = tryFnSync(() => decodeGeoLon(value, precision));
433
+ return ok ? decoded : value;
434
+ },
435
+
436
+ // Geo types - Point (lat+lon pair)
437
+ encodeGeoPointPair: (value, { precision = 6 } = {}) => {
438
+ if (value === null || value === undefined) return value;
439
+ // Accept object with lat/lon or array [lat, lon]
440
+ if (Array.isArray(value) && value.length === 2) {
441
+ const [ok, err, encoded] = tryFnSync(() => encodeGeoPoint(value[0], value[1], precision));
442
+ return ok ? encoded : value;
443
+ }
444
+ if (typeof value === 'object' && value.lat !== undefined && value.lon !== undefined) {
445
+ const [ok, err, encoded] = tryFnSync(() => encodeGeoPoint(value.lat, value.lon, precision));
446
+ return ok ? encoded : value;
447
+ }
448
+ if (typeof value === 'object' && value.latitude !== undefined && value.longitude !== undefined) {
449
+ const [ok, err, encoded] = tryFnSync(() => encodeGeoPoint(value.latitude, value.longitude, precision));
450
+ return ok ? encoded : value;
451
+ }
452
+ return value;
453
+ },
454
+ decodeGeoPointPair: (value, { precision = 6 } = {}) => {
455
+ if (value === null || value === undefined) return value;
456
+ if (typeof value !== 'string') return value;
457
+ const [ok, err, decoded] = tryFnSync(() => decodeGeoPoint(value, precision));
458
+ // Return as object { latitude, longitude }
459
+ return ok ? decoded : value;
460
+ },
461
+
332
462
  }
333
463
 
334
464
  export class Schema {
@@ -353,7 +483,7 @@ export class Schema {
353
483
  const processedAttributes = this.preprocessAttributesForValidation(this.attributes);
354
484
 
355
485
  this.validator = new ValidatorManager({ autoEncrypt: false }).compile(merge(
356
- { $$async: true },
486
+ { $$async: true, $$strict: false },
357
487
  processedAttributes,
358
488
  ))
359
489
 
@@ -398,9 +528,11 @@ export class Schema {
398
528
  }
399
529
  }
400
530
 
401
- addHook(hook, attribute, action) {
531
+ addHook(hook, attribute, action, params = {}) {
402
532
  if (!this.options.hooks[hook][attribute]) this.options.hooks[hook][attribute] = [];
403
- this.options.hooks[hook][attribute] = uniq([...this.options.hooks[hook][attribute], action])
533
+ // Store action with parameters if provided
534
+ const hookEntry = Object.keys(params).length > 0 ? { action, params } : action;
535
+ this.options.hooks[hook][attribute] = uniq([...this.options.hooks[hook][attribute], hookEntry])
404
536
  }
405
537
 
406
538
  extractObjectKeys(obj, prefix = '') {
@@ -573,6 +705,97 @@ export class Schema {
573
705
  continue;
574
706
  }
575
707
 
708
+ // Handle ip4 type
709
+ if (defStr.includes("ip4") || defType === 'ip4') {
710
+ this.addHook("beforeMap", name, "encodeIPv4");
711
+ this.addHook("afterUnmap", name, "decodeIPv4");
712
+ continue;
713
+ }
714
+
715
+ // Handle ip6 type
716
+ if (defStr.includes("ip6") || defType === 'ip6') {
717
+ this.addHook("beforeMap", name, "encodeIPv6");
718
+ this.addHook("afterUnmap", name, "decodeIPv6");
719
+ continue;
720
+ }
721
+
722
+ // Handle money type (integer-based, decimal-aware)
723
+ if (defStr.includes("money") || defType === 'money' || defStr.includes("crypto") || defType === 'crypto') {
724
+ // Extract decimals from money:8 or crypto:8 notation
725
+ let decimals = 2; // Default for fiat money (2 decimal places)
726
+
727
+ // If it's crypto, default to 8 decimals (satoshi/standard crypto)
728
+ if (defStr.includes("crypto") || defType === 'crypto') {
729
+ decimals = 8;
730
+ }
731
+
732
+ // Override with explicit decimals if provided: money:8, crypto:18
733
+ const decimalsMatch = defStr.match(/(?:money|crypto):(\d+)/i);
734
+ if (decimalsMatch) {
735
+ decimals = parseInt(decimalsMatch[1], 10);
736
+ }
737
+
738
+ this.addHook("beforeMap", name, "encodeMoney", { decimals });
739
+ this.addHook("afterUnmap", name, "decodeMoney", { decimals });
740
+ continue;
741
+ }
742
+
743
+ // Handle decimal type (fixed-point for non-monetary decimals)
744
+ if (defStr.includes("decimal") || defType === 'decimal') {
745
+ // Extract precision from decimal:4 notation
746
+ let precision = 2; // Default precision
747
+ const precisionMatch = defStr.match(/decimal:(\d+)/);
748
+ if (precisionMatch) {
749
+ precision = parseInt(precisionMatch[1], 10);
750
+ }
751
+
752
+ this.addHook("beforeMap", name, "encodeDecimalFixed", { precision });
753
+ this.addHook("afterUnmap", name, "decodeDecimalFixed", { precision });
754
+ continue;
755
+ }
756
+
757
+ // Handle geo:lat type (latitude)
758
+ if (defStr.includes("geo:lat") || (defType === 'geo' && defStr.includes('lat'))) {
759
+ // Extract precision from geo:lat:6 notation
760
+ let precision = 6; // Default precision (GPS standard)
761
+ const precisionMatch = defStr.match(/geo:lat:(\d+)/);
762
+ if (precisionMatch) {
763
+ precision = parseInt(precisionMatch[1], 10);
764
+ }
765
+
766
+ this.addHook("beforeMap", name, "encodeGeoLatitude", { precision });
767
+ this.addHook("afterUnmap", name, "decodeGeoLatitude", { precision });
768
+ continue;
769
+ }
770
+
771
+ // Handle geo:lon type (longitude)
772
+ if (defStr.includes("geo:lon") || (defType === 'geo' && defStr.includes('lon'))) {
773
+ // Extract precision from geo:lon:6 notation
774
+ let precision = 6; // Default precision
775
+ const precisionMatch = defStr.match(/geo:lon:(\d+)/);
776
+ if (precisionMatch) {
777
+ precision = parseInt(precisionMatch[1], 10);
778
+ }
779
+
780
+ this.addHook("beforeMap", name, "encodeGeoLongitude", { precision });
781
+ this.addHook("afterUnmap", name, "decodeGeoLongitude", { precision });
782
+ continue;
783
+ }
784
+
785
+ // Handle geo:point type (lat+lon pair)
786
+ if (defStr.includes("geo:point") || defType === 'geo:point') {
787
+ // Extract precision from geo:point:6 notation
788
+ let precision = 6; // Default precision
789
+ const precisionMatch = defStr.match(/geo:point:(\d+)/);
790
+ if (precisionMatch) {
791
+ precision = parseInt(precisionMatch[1], 10);
792
+ }
793
+
794
+ this.addHook("beforeMap", name, "encodeGeoPointPair", { precision });
795
+ this.addHook("afterUnmap", name, "decodeGeoPointPair", { precision });
796
+ continue;
797
+ }
798
+
576
799
  // Handle numbers (only for non-array fields)
577
800
  if (defStr.includes("number") || defType === 'number') {
578
801
  // Check if it's specifically an integer field
@@ -704,12 +927,17 @@ export class Schema {
704
927
  async applyHooksActions(resourceItem, hook) {
705
928
  const cloned = cloneDeep(resourceItem);
706
929
  for (const [attribute, actions] of Object.entries(this.options.hooks[hook])) {
707
- for (const action of actions) {
930
+ for (const actionEntry of actions) {
931
+ // Support both string actions and {action, params} objects
932
+ const actionName = typeof actionEntry === 'string' ? actionEntry : actionEntry.action;
933
+ const actionParams = typeof actionEntry === 'object' ? actionEntry.params : {};
934
+
708
935
  const value = get(cloned, attribute)
709
- if (value !== undefined && typeof SchemaActions[action] === 'function') {
710
- set(cloned, attribute, await SchemaActions[action](value, {
936
+ if (value !== undefined && typeof SchemaActions[actionName] === 'function') {
937
+ set(cloned, attribute, await SchemaActions[actionName](value, {
711
938
  passphrase: this.passphrase,
712
939
  separator: this.options.arraySeparator,
940
+ ...actionParams // Merge custom parameters (currency, precision, etc.)
713
941
  }))
714
942
  }
715
943
  }
@@ -785,27 +1013,35 @@ export class Schema {
785
1013
  }
786
1014
  }
787
1015
  // PATCH: ensure arrays are always arrays
1016
+ // Skip automatic array conversion if there's an afterUnmap hook that will handle it
788
1017
  if (this.attributes) {
789
1018
  if (typeof attrDef === 'string' && attrDef.includes('array')) {
790
- if (Array.isArray(parsedValue)) {
791
- // Already an array
792
- } else if (typeof parsedValue === 'string' && parsedValue.trim().startsWith('[')) {
793
- const [okArr, errArr, arr] = tryFnSync(() => JSON.parse(parsedValue));
794
- if (okArr && Array.isArray(arr)) {
795
- parsedValue = arr;
1019
+ if (!hasAfterUnmapHook) {
1020
+ if (Array.isArray(parsedValue)) {
1021
+ // Already an array
1022
+ } else if (typeof parsedValue === 'string' && parsedValue.trim().startsWith('[')) {
1023
+ const [okArr, errArr, arr] = tryFnSync(() => JSON.parse(parsedValue));
1024
+ if (okArr && Array.isArray(arr)) {
1025
+ parsedValue = arr;
1026
+ }
1027
+ } else {
1028
+ parsedValue = SchemaActions.toArray(parsedValue, { separator: this.options.arraySeparator });
796
1029
  }
797
- } else {
798
- parsedValue = SchemaActions.toArray(parsedValue, { separator: this.options.arraySeparator });
799
1030
  }
800
1031
  }
801
1032
  }
802
1033
  // PATCH: apply afterUnmap hooks for type restoration
803
1034
  if (this.options.hooks && this.options.hooks.afterUnmap && this.options.hooks.afterUnmap[originalKey]) {
804
- for (const action of this.options.hooks.afterUnmap[originalKey]) {
805
- if (typeof SchemaActions[action] === 'function') {
806
- parsedValue = await SchemaActions[action](parsedValue, {
1035
+ for (const actionEntry of this.options.hooks.afterUnmap[originalKey]) {
1036
+ // Support both string actions and {action, params} objects
1037
+ const actionName = typeof actionEntry === 'string' ? actionEntry : actionEntry.action;
1038
+ const actionParams = typeof actionEntry === 'object' ? actionEntry.params : {};
1039
+
1040
+ if (typeof SchemaActions[actionName] === 'function') {
1041
+ parsedValue = await SchemaActions[actionName](parsedValue, {
807
1042
  passphrase: this.passphrase,
808
1043
  separator: this.options.arraySeparator,
1044
+ ...actionParams // Merge custom parameters (currency, precision, etc.)
809
1045
  });
810
1046
  }
811
1047
  }
@@ -843,6 +1079,66 @@ export class Schema {
843
1079
 
844
1080
  for (const [key, value] of Object.entries(attributes)) {
845
1081
  if (typeof value === 'string') {
1082
+ // Expand ip4 shorthand to string type with custom validation
1083
+ if (value === 'ip4' || value.startsWith('ip4|')) {
1084
+ processed[key] = value.replace(/^ip4/, 'string');
1085
+ continue;
1086
+ }
1087
+ // Expand ip6 shorthand to string type with custom validation
1088
+ if (value === 'ip6' || value.startsWith('ip6|')) {
1089
+ processed[key] = value.replace(/^ip6/, 'string');
1090
+ continue;
1091
+ }
1092
+ // Expand money/crypto shorthand to number type with min validation
1093
+ if (value === 'money' || value.startsWith('money:') || value.startsWith('money|') ||
1094
+ value === 'crypto' || value.startsWith('crypto:') || value.startsWith('crypto|')) {
1095
+ // Extract any modifiers after money:N or crypto:N
1096
+ const rest = value.replace(/^(?:money|crypto)(?::\d+)?/, '');
1097
+ // Money must be non-negative
1098
+ const hasMin = rest.includes('min:');
1099
+ processed[key] = hasMin ? `number${rest}` : `number|min:0${rest}`;
1100
+ continue;
1101
+ }
1102
+ // Expand decimal shorthand to number type
1103
+ if (value === 'decimal' || value.startsWith('decimal:') || value.startsWith('decimal|')) {
1104
+ // Extract any modifiers after decimal:PRECISION
1105
+ const rest = value.replace(/^decimal(:\d+)?/, '');
1106
+ processed[key] = `number${rest}`;
1107
+ continue;
1108
+ }
1109
+ // Expand geo:lat shorthand to number type with range validation
1110
+ if (value.startsWith('geo:lat')) {
1111
+ // Extract any modifiers after geo:lat:PRECISION
1112
+ const rest = value.replace(/^geo:lat(:\d+)?/, '');
1113
+ // Latitude range: -90 to 90
1114
+ const hasMin = rest.includes('min:');
1115
+ const hasMax = rest.includes('max:');
1116
+ let validation = 'number';
1117
+ if (!hasMin) validation += '|min:-90';
1118
+ if (!hasMax) validation += '|max:90';
1119
+ processed[key] = validation + rest;
1120
+ continue;
1121
+ }
1122
+ // Expand geo:lon shorthand to number type with range validation
1123
+ if (value.startsWith('geo:lon')) {
1124
+ // Extract any modifiers after geo:lon:PRECISION
1125
+ const rest = value.replace(/^geo:lon(:\d+)?/, '');
1126
+ // Longitude range: -180 to 180
1127
+ const hasMin = rest.includes('min:');
1128
+ const hasMax = rest.includes('max:');
1129
+ let validation = 'number';
1130
+ if (!hasMin) validation += '|min:-180';
1131
+ if (!hasMax) validation += '|max:180';
1132
+ processed[key] = validation + rest;
1133
+ continue;
1134
+ }
1135
+ // Expand geo:point shorthand to object with lat/lon
1136
+ if (value.startsWith('geo:point')) {
1137
+ // geo:point is an object or array with lat/lon
1138
+ // For simplicity, allow it as any type (will be validated in hooks)
1139
+ processed[key] = 'any';
1140
+ continue;
1141
+ }
846
1142
  // Expand embedding:XXX shorthand to array|items:number|length:XXX
847
1143
  if (value.startsWith('embedding:')) {
848
1144
  const lengthMatch = value.match(/embedding:(\d+)/);
@@ -866,8 +1162,46 @@ export class Schema {
866
1162
  const hasValidatorType = value.type !== undefined && key !== '$$type';
867
1163
 
868
1164
  if (hasValidatorType) {
869
- // This is a validator type definition (e.g., { type: 'array', items: 'number' }), pass it through
870
- processed[key] = value;
1165
+ // Handle ip4 and ip6 object notation
1166
+ if (value.type === 'ip4') {
1167
+ processed[key] = { ...value, type: 'string' };
1168
+ } else if (value.type === 'ip6') {
1169
+ processed[key] = { ...value, type: 'string' };
1170
+ } else if (value.type === 'money' || value.type === 'crypto') {
1171
+ // Money/crypto type → number with min:0
1172
+ processed[key] = { ...value, type: 'number', min: value.min !== undefined ? value.min : 0 };
1173
+ } else if (value.type === 'decimal') {
1174
+ // Decimal type → number
1175
+ processed[key] = { ...value, type: 'number' };
1176
+ } else if (value.type === 'geo:lat' || value.type === 'geo-lat') {
1177
+ // Geo latitude → number with range [-90, 90]
1178
+ processed[key] = {
1179
+ ...value,
1180
+ type: 'number',
1181
+ min: value.min !== undefined ? value.min : -90,
1182
+ max: value.max !== undefined ? value.max : 90
1183
+ };
1184
+ } else if (value.type === 'geo:lon' || value.type === 'geo-lon') {
1185
+ // Geo longitude → number with range [-180, 180]
1186
+ processed[key] = {
1187
+ ...value,
1188
+ type: 'number',
1189
+ min: value.min !== undefined ? value.min : -180,
1190
+ max: value.max !== undefined ? value.max : 180
1191
+ };
1192
+ } else if (value.type === 'geo:point' || value.type === 'geo-point') {
1193
+ // Geo point → any (will be validated in hooks)
1194
+ processed[key] = { ...value, type: 'any' };
1195
+ } else if (value.type === 'object' && value.properties) {
1196
+ // Recursively process nested object properties
1197
+ processed[key] = {
1198
+ ...value,
1199
+ properties: this.preprocessAttributesForValidation(value.properties)
1200
+ };
1201
+ } else {
1202
+ // This is a validator type definition (e.g., { type: 'array', items: 'number' }), pass it through
1203
+ processed[key] = value;
1204
+ }
871
1205
  } else {
872
1206
  // This is a nested object structure, wrap it for validation
873
1207
  const isExplicitRequired = value.$$type && value.$$type.includes('required');
package/SECURITY.md DELETED
@@ -1,76 +0,0 @@
1
- # Security Policy
2
-
3
- ## Supported Versions
4
-
5
- | Version | Supported |
6
- | ------- | ------------------ |
7
- | 11.x.x | :white_check_mark: |
8
- | < 11.0 | :x: |
9
-
10
- ## Known Security Advisories
11
-
12
- ### Development Dependencies
13
-
14
- The following vulnerabilities exist in **development-only** dependencies and **do not affect** the published npm package or runtime security:
15
-
16
- #### pkg (GHSA-22r3-9w55-cj54) - MODERATE
17
- - **Status**: Acknowledged, monitored
18
- - **Impact**: Local privilege escalation
19
- - **Scope**: Only affects developers running `pnpm run build:binaries`
20
- - **Mitigation**: pkg is deprecated and archived. No patched version available (`<0.0.0`).
21
- - **Risk Assessment**: LOW - Only used for creating standalone binaries during release process
22
- - **Future Plans**: Migrate to Node.js Single Executable Applications (SEA) when stable
23
-
24
- #### tar-fs - HIGH
25
- - **Status**: RESOLVED in v11.1.1+
26
- - **Fix**: Updated to patched version 2.1.4+
27
-
28
- ## Reporting a Vulnerability
29
-
30
- If you discover a security vulnerability in the **runtime code** (not dev dependencies), please report it by:
31
-
32
- 1. **DO NOT** open a public issue
33
- 2. Email: [security contact - update this]
34
- 3. Include:
35
- - Description of the vulnerability
36
- - Steps to reproduce
37
- - Potential impact
38
- - Suggested fix (if any)
39
-
40
- ### Response Timeline
41
-
42
- - **Initial Response**: Within 48 hours
43
- - **Status Update**: Within 7 days
44
- - **Fix Timeline**: Depends on severity
45
- - Critical: 7 days
46
- - High: 14 days
47
- - Medium: 30 days
48
- - Low: 60 days
49
-
50
- ## Security Best Practices
51
-
52
- ### For Users
53
-
54
- 1. **Always encrypt sensitive data**: Use `secret` field type for passwords, tokens, etc.
55
- 2. **Validate credentials**: Never commit AWS credentials to version control
56
- 3. **Use IAM policies**: Implement least-privilege access for S3 buckets
57
- 4. **Enable paranoid mode**: For production, use `paranoid: true` for soft deletes
58
- 5. **Audit hooks**: Review serialized functions before deploying to production
59
-
60
- ### For Contributors
61
-
62
- 1. **No secrets in tests**: Use environment variables or LocalStack
63
- 2. **Validate input**: All user input should be validated before S3 operations
64
- 3. **Handle errors safely**: Never expose AWS error details to end users
65
- 4. **Review dependencies**: Run `pnpm audit` before submitting PRs
66
- 5. **Test encryption**: Verify `secret` fields are actually encrypted in S3
67
-
68
- ## Audit Configuration
69
-
70
- This project uses `audit-level=high` in `.npmrc` to focus on critical vulnerabilities affecting production. Moderate/low severity issues in dev-only dependencies are monitored but may not block releases if:
71
-
72
- - They only affect development tools
73
- - No patch is available
74
- - The risk is assessed as acceptable
75
-
76
- Current audit threshold: **HIGH** (ignores moderate/low in dev dependencies)
@@ -1,106 +0,0 @@
1
- import { EventEmitter } from 'events';
2
- import { PartitionDriverError } from '../errors.js';
3
-
4
- /**
5
- * Base class for all partition drivers
6
- * Defines the interface that all drivers must implement
7
- */
8
- export class BasePartitionDriver extends EventEmitter {
9
- constructor(options = {}) {
10
- super();
11
- this.options = options;
12
- this.stats = {
13
- queued: 0,
14
- processed: 0,
15
- failed: 0,
16
- processing: 0
17
- };
18
- }
19
-
20
- /**
21
- * Initialize the driver
22
- */
23
- async initialize() {
24
- // Override in subclasses if needed
25
- }
26
-
27
- /**
28
- * Queue partition operations for processing
29
- * @param {Object} operation - The partition operation to queue
30
- * @param {string} operation.type - 'create', 'update', or 'delete'
31
- * @param {Object} operation.resource - The resource instance
32
- * @param {Object} operation.data - The data for the operation
33
- */
34
- async queue(operation) {
35
- throw new PartitionDriverError('queue() must be implemented by subclass', {
36
- driver: this.name || 'BasePartitionDriver',
37
- operation: 'queue',
38
- suggestion: 'Extend BasePartitionDriver and implement the queue() method'
39
- });
40
- }
41
-
42
- /**
43
- * Process a single partition operation
44
- */
45
- async processOperation(operation) {
46
- const { type, resource, data } = operation;
47
-
48
- try {
49
- this.stats.processing++;
50
-
51
- switch (type) {
52
- case 'create':
53
- await resource.createPartitionReferences(data.object);
54
- break;
55
-
56
- case 'update':
57
- await resource.handlePartitionReferenceUpdates(data.original, data.updated);
58
- break;
59
-
60
- case 'delete':
61
- await resource.deletePartitionReferences(data.object);
62
- break;
63
-
64
- default:
65
- throw new PartitionDriverError(`Unknown partition operation type: ${type}`, {
66
- driver: this.name || 'BasePartitionDriver',
67
- operation: type,
68
- availableOperations: ['create', 'update', 'delete'],
69
- suggestion: 'Use one of the supported partition operations: create, update, or delete'
70
- });
71
- }
72
-
73
- this.stats.processed++;
74
- this.emit('processed', operation);
75
-
76
- } catch (error) {
77
- this.stats.failed++;
78
- this.emit('error', { operation, error });
79
- throw error;
80
- } finally {
81
- this.stats.processing--;
82
- }
83
- }
84
-
85
- /**
86
- * Flush any pending operations
87
- */
88
- async flush() {
89
- // Override in subclasses if needed
90
- }
91
-
92
- /**
93
- * Get driver statistics
94
- */
95
- getStats() {
96
- return { ...this.stats };
97
- }
98
-
99
- /**
100
- * Shutdown the driver
101
- */
102
- async shutdown() {
103
- await this.flush();
104
- this.removeAllListeners();
105
- }
106
- }