ts-prorm-orm 1.1.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/CHANGELOG.md +1111 -0
- package/LICENSE +21 -0
- package/README.md +573 -0
- package/dist/audit/history-query.js +180 -0
- package/dist/audit/index.js +23 -0
- package/dist/audit/logger.js +236 -0
- package/dist/cache/cache-manager.js +84 -0
- package/dist/cache/index.js +16 -0
- package/dist/cache/redis-cluster-cache.js +557 -0
- package/dist/cli.js +2200 -0
- package/dist/compliance/audit-trail.js +68 -0
- package/dist/compliance/backup-verification.js +685 -0
- package/dist/compliance/breach-detector.js +505 -0
- package/dist/compliance/consent-record.js +227 -0
- package/dist/compliance/consent-versioning.js +331 -0
- package/dist/compliance/cross-border-log.js +530 -0
- package/dist/compliance/data-classifier.js +258 -0
- package/dist/compliance/data-lineage.js +565 -0
- package/dist/compliance/data-masker.js +303 -0
- package/dist/compliance/data-portability.js +265 -0
- package/dist/compliance/data-retention.js +195 -0
- package/dist/compliance/dsar-workflow.js +342 -0
- package/dist/compliance/field-encryption.js +223 -0
- package/dist/compliance/immutable-record.js +148 -0
- package/dist/compliance/index.js +260 -0
- package/dist/compliance/privacy-impact-assessment.js +364 -0
- package/dist/compliance/pseudonymization.js +269 -0
- package/dist/compliance/query-firewall.js +1182 -0
- package/dist/compliance/rate-limiter.js +580 -0
- package/dist/compliance/right-to-erasure.js +117 -0
- package/dist/compliance/row-level-security.js +246 -0
- package/dist/compliance/security-decorator.js +574 -0
- package/dist/compliance/security-monitor.js +525 -0
- package/dist/compliance/sensitive-data-discovery.js +476 -0
- package/dist/compliance/session-isolation.js +472 -0
- package/dist/compliance/tls-enforcer.js +108 -0
- package/dist/compliance/worm-storage.js +712 -0
- package/dist/connection-manager.js +198 -0
- package/dist/connection-pool.js +519 -0
- package/dist/decorators/audit.js +136 -0
- package/dist/decorators/belongs-to-many.js +115 -0
- package/dist/decorators/belongs-to.js +115 -0
- package/dist/decorators/check.js +435 -0
- package/dist/decorators/collate.js +329 -0
- package/dist/decorators/comment.js +205 -0
- package/dist/decorators/database-settings.js +236 -0
- package/dist/decorators/default.js +244 -0
- package/dist/decorators/encryption.js +235 -0
- package/dist/decorators/engine.js +97 -0
- package/dist/decorators/fk-constraints.js +594 -0
- package/dist/decorators/foreign-table.js +136 -0
- package/dist/decorators/generated.js +274 -0
- package/dist/decorators/has-many.js +127 -0
- package/dist/decorators/has-one.js +116 -0
- package/dist/decorators/hstore.js +129 -0
- package/dist/decorators/index.js +355 -0
- package/dist/decorators/json-column.js +83 -0
- package/dist/decorators/jsonb.js +101 -0
- package/dist/decorators/orm-decorators.js +425 -0
- package/dist/decorators/permissions.js +294 -0
- package/dist/decorators/procedure.js +210 -0
- package/dist/decorators/query-options.js +556 -0
- package/dist/decorators/range.js +167 -0
- package/dist/decorators/set-column.js +83 -0
- package/dist/decorators/spatial.js +114 -0
- package/dist/decorators/storage.js +580 -0
- package/dist/decorators/timezone.js +512 -0
- package/dist/decorators/trigger.js +90 -0
- package/dist/decorators/uuid.js +135 -0
- package/dist/decorators/view.js +258 -0
- package/dist/diagrams/chen-diagram.js +354 -0
- package/dist/diagrams/class-diagram.js +384 -0
- package/dist/diagrams/dependency-diagram.js +432 -0
- package/dist/diagrams/er-diagram.js +605 -0
- package/dist/diagrams/flow-diagram.js +394 -0
- package/dist/diagrams/gantt-diagram.js +411 -0
- package/dist/diagrams/index-diagram.js +353 -0
- package/dist/diagrams/index.js +184 -0
- package/dist/diagrams/migration-diagram.js +316 -0
- package/dist/diagrams/model-diagram.js +616 -0
- package/dist/diagrams/package-diagram.js +376 -0
- package/dist/diagrams/palette.js +85 -0
- package/dist/diagrams/relational-diagram.js +455 -0
- package/dist/diagrams/schemadoc-diagram.js +309 -0
- package/dist/diagrams/sequence-diagram.js +307 -0
- package/dist/diagrams/state-diagram.js +344 -0
- package/dist/diagrams/svg-dom.js +111 -0
- package/dist/diagrams/tree-diagram.js +250 -0
- package/dist/dialects/clickhouse/index.js +1742 -0
- package/dist/dialects/cockroachdb/index.js +4677 -0
- package/dist/dialects/cratedb/index.js +441 -0
- package/dist/dialects/databricks/index.js +517 -0
- package/dist/dialects/db2/index.js +2225 -0
- package/dist/dialects/dialect.js +720 -0
- package/dist/dialects/duckdb/index.js +2014 -0
- package/dist/dialects/exasol/index.js +357 -0
- package/dist/dialects/firebird/index.js +495 -0
- package/dist/dialects/greenplum/index.js +457 -0
- package/dist/dialects/hana/index.js +1741 -0
- package/dist/dialects/mariadb/index.js +3590 -0
- package/dist/dialects/mssql/index.js +2296 -0
- package/dist/dialects/mysql/index.js +4058 -0
- package/dist/dialects/oracle/index.js +2999 -0
- package/dist/dialects/postgres/index.js +5319 -0
- package/dist/dialects/query-stream-helper.js +152 -0
- package/dist/dialects/questdb/index.js +433 -0
- package/dist/dialects/redshift/index.js +2277 -0
- package/dist/dialects/singlestore/index.js +354 -0
- package/dist/dialects/snowflake/index.js +2082 -0
- package/dist/dialects/spanner/index.js +1768 -0
- package/dist/dialects/sqlite/index.js +3382 -0
- package/dist/dialects/tidb/index.js +377 -0
- package/dist/dialects/timescaledb/index.js +164 -0
- package/dist/dialects/trino/index.js +420 -0
- package/dist/dialects/turso/index.js +227 -0
- package/dist/dialects/vertica/index.js +317 -0
- package/dist/dialects/yugabytedb/index.js +424 -0
- package/dist/errors/index.js +472 -0
- package/dist/errors/utils.js +340 -0
- package/dist/errors.js +21 -0
- package/dist/extensions/catalog/cloud-warehouse-features.js +207 -0
- package/dist/extensions/catalog/mssql-features.js +147 -0
- package/dist/extensions/catalog/mysql-mariadb-plugins.js +229 -0
- package/dist/extensions/catalog/oracle-db2-features.js +196 -0
- package/dist/extensions/catalog/postgres-extensions.js +516 -0
- package/dist/extensions/index.js +71 -0
- package/dist/extensions/types.js +13 -0
- package/dist/foreign-data.js +173 -0
- package/dist/hooks/hooks-manager.js +350 -0
- package/dist/hooks/index.js +37 -0
- package/dist/index.js +463 -0
- package/dist/logging.js +335 -0
- package/dist/migrations/index.js +40 -0
- package/dist/migrations/migration.js +196 -0
- package/dist/migrations/migrator.js +411 -0
- package/dist/migrations/prormmigration.js +275 -0
- package/dist/migrations/query-interface.js +435 -0
- package/dist/migrations/seeder.js +353 -0
- package/dist/models/associations.js +852 -0
- package/dist/models/constraints.js +288 -0
- package/dist/models/data-types.js +2518 -0
- package/dist/models/decorators.js +445 -0
- package/dist/models/index.js +33 -0
- package/dist/models/indexes.js +531 -0
- package/dist/models/methods.js +382 -0
- package/dist/models/model-manager.js +103 -0
- package/dist/models/model.js +5349 -0
- package/dist/models/operators.js +67 -0
- package/dist/models/scopes.js +189 -0
- package/dist/models/typescript-types.js +26 -0
- package/dist/nosql/aerospike/index.js +205 -0
- package/dist/nosql/allegrograph/index.js +169 -0
- package/dist/nosql/arangodb/index.js +364 -0
- package/dist/nosql/azure-blob/index.js +206 -0
- package/dist/nosql/beanstalkd/index.js +231 -0
- package/dist/nosql/beequeue/index.js +210 -0
- package/dist/nosql/bigchaindb/index.js +195 -0
- package/dist/nosql/bigtable/index.js +224 -0
- package/dist/nosql/blazegraph/index.js +173 -0
- package/dist/nosql/bullmq/index.js +191 -0
- package/dist/nosql/cassandra/index.js +174 -0
- package/dist/nosql/chroma/index.js +190 -0
- package/dist/nosql/cloudflare-kv/index.js +220 -0
- package/dist/nosql/coherence/index.js +200 -0
- package/dist/nosql/cosmosdb/index.js +157 -0
- package/dist/nosql/couchbase/index.js +213 -0
- package/dist/nosql/dax/index.js +212 -0
- package/dist/nosql/deno-kv/index.js +206 -0
- package/dist/nosql/dgraph/index.js +171 -0
- package/dist/nosql/doris/index.js +169 -0
- package/dist/nosql/druid/index.js +162 -0
- package/dist/nosql/dynamodb/index.js +937 -0
- package/dist/nosql/elasticsearch/index.js +377 -0
- package/dist/nosql/etcd/index.js +502 -0
- package/dist/nosql/eventhubs/index.js +213 -0
- package/dist/nosql/eventstore/index.js +254 -0
- package/dist/nosql/faunadb/index.js +188 -0
- package/dist/nosql/firestore/index.js +177 -0
- package/dist/nosql/fluree/index.js +148 -0
- package/dist/nosql/fuseki/index.js +170 -0
- package/dist/nosql/gcs/index.js +172 -0
- package/dist/nosql/gearman/index.js +160 -0
- package/dist/nosql/geode/index.js +196 -0
- package/dist/nosql/graphdb/index.js +169 -0
- package/dist/nosql/graylog/index.js +188 -0
- package/dist/nosql/gridgain/index.js +171 -0
- package/dist/nosql/hazelcast/index.js +162 -0
- package/dist/nosql/hbase/index.js +230 -0
- package/dist/nosql/ignite/index.js +173 -0
- package/dist/nosql/immudb/index.js +184 -0
- package/dist/nosql/index.js +232 -0
- package/dist/nosql/infinispan/index.js +200 -0
- package/dist/nosql/influxdb/index.js +0 -0
- package/dist/nosql/kafka/index.js +234 -0
- package/dist/nosql/keyspaces/index.js +189 -0
- package/dist/nosql/kinesis/index.js +253 -0
- package/dist/nosql/leveldb/index.js +153 -0
- package/dist/nosql/lmdb/index.js +160 -0
- package/dist/nosql/loki/index.js +201 -0
- package/dist/nosql/marklogic/index.js +204 -0
- package/dist/nosql/materialize/index.js +145 -0
- package/dist/nosql/meilisearch/index.js +154 -0
- package/dist/nosql/memcached/index.js +223 -0
- package/dist/nosql/milvus/index.js +410 -0
- package/dist/nosql/minio/index.js +265 -0
- package/dist/nosql/momento/index.js +179 -0
- package/dist/nosql/mongodb/index.js +461 -0
- package/dist/nosql/nats/index.js +247 -0
- package/dist/nosql/nedb/index.js +164 -0
- package/dist/nosql/neo4j/index.js +450 -0
- package/dist/nosql/neptune/index.js +470 -0
- package/dist/nosql/nsq/index.js +200 -0
- package/dist/nosql/opensearch/index.js +186 -0
- package/dist/nosql/orientdb/index.js +175 -0
- package/dist/nosql/papertrail/index.js +200 -0
- package/dist/nosql/pinecone/index.js +0 -0
- package/dist/nosql/pinot/index.js +133 -0
- package/dist/nosql/pouchdb/index.js +172 -0
- package/dist/nosql/prometheus/index.js +174 -0
- package/dist/nosql/provendb/index.js +147 -0
- package/dist/nosql/pubsub/index.js +187 -0
- package/dist/nosql/pulsar/index.js +232 -0
- package/dist/nosql/qdrant/index.js +295 -0
- package/dist/nosql/qldb/index.js +192 -0
- package/dist/nosql/r2/index.js +281 -0
- package/dist/nosql/rabbitmq/index.js +237 -0
- package/dist/nosql/ravendb/index.js +175 -0
- package/dist/nosql/redis/index.js +607 -0
- package/dist/nosql/redpanda/index.js +237 -0
- package/dist/nosql/resque/index.js +203 -0
- package/dist/nosql/rethinkdb/index.js +232 -0
- package/dist/nosql/rocksdb/index.js +152 -0
- package/dist/nosql/rockset/index.js +126 -0
- package/dist/nosql/s3/index.js +298 -0
- package/dist/nosql/scylladb/index.js +178 -0
- package/dist/nosql/signoz/index.js +227 -0
- package/dist/nosql/sns/index.js +201 -0
- package/dist/nosql/solr/index.js +231 -0
- package/dist/nosql/splunk/index.js +227 -0
- package/dist/nosql/sqs/index.js +246 -0
- package/dist/nosql/stardog/index.js +169 -0
- package/dist/nosql/starrocks/index.js +170 -0
- package/dist/nosql/store.js +2 -0
- package/dist/nosql/sumologic/index.js +213 -0
- package/dist/nosql/surrealdb/index.js +179 -0
- package/dist/nosql/terminusdb/index.js +151 -0
- package/dist/nosql/tigergraph/index.js +469 -0
- package/dist/nosql/typesense/index.js +148 -0
- package/dist/nosql/unqlite/index.js +157 -0
- package/dist/nosql/upstash/index.js +167 -0
- package/dist/nosql/vercel-kv/index.js +166 -0
- package/dist/nosql/victoriametrics/index.js +213 -0
- package/dist/nosql/virtuoso/index.js +169 -0
- package/dist/nosql/weaviate/index.js +276 -0
- package/dist/operators/index.js +108 -0
- package/dist/operators.js +2690 -0
- package/dist/prisma-migrate/index.js +38 -0
- package/dist/prisma-migrate/migration-generator.js +125 -0
- package/dist/prisma-migrate/model-generator.js +172 -0
- package/dist/prisma-migrate/relations.js +100 -0
- package/dist/prisma-migrate/schema-parser.js +167 -0
- package/dist/prisma-migrate/type-mapper.js +40 -0
- package/dist/prorm.js +6832 -0
- package/dist/query-builders/cte-builder.js +80 -0
- package/dist/query-builders/functions/aggregate.js +390 -0
- package/dist/query-builders/functions/conditional.js +503 -0
- package/dist/query-builders/functions/datetime.js +695 -0
- package/dist/query-builders/functions/fulltext.js +439 -0
- package/dist/query-builders/functions/index.js +93 -0
- package/dist/query-builders/functions/json.js +427 -0
- package/dist/query-builders/functions/math.js +399 -0
- package/dist/query-builders/functions/string.js +518 -0
- package/dist/query-builders/functions/window.js +328 -0
- package/dist/query-builders/include-builder.js +323 -0
- package/dist/query-builders/index-expression-builder.js +242 -0
- package/dist/query-builders/index.js +161 -0
- package/dist/query-builders/insert-builder.js +164 -0
- package/dist/query-builders/model-helpers.js +38 -0
- package/dist/query-builders/order-limit-builder.js +239 -0
- package/dist/query-builders/sql-compiler.js +2040 -0
- package/dist/query-builders/subquery-builder.js +152 -0
- package/dist/query-builders/update-builder.js +182 -0
- package/dist/query-builders/view-builder.js +218 -0
- package/dist/query-builders/where-builder.js +1532 -0
- package/dist/query-interface.js +622 -0
- package/dist/query-optimizers/batch-optimizer.js +426 -0
- package/dist/query-optimizers/explain-plans.js +415 -0
- package/dist/query-optimizers/index.js +51 -0
- package/dist/query-optimizers/prepared-statement-cache.js +423 -0
- package/dist/query-optimizers/query-hints.js +438 -0
- package/dist/query-optimizers/query-optimizer.js +278 -0
- package/dist/query-optimizers/slow-query-logger.js +271 -0
- package/dist/replica-manager.js +507 -0
- package/dist/schema/index.js +15 -0
- package/dist/schema/migration-generator.js +374 -0
- package/dist/schema/schema-differ.js +549 -0
- package/dist/schema/types.js +6 -0
- package/dist/sql-constants.js +300 -0
- package/dist/sqlite-advanced.js +1047 -0
- package/dist/streams/index.js +12 -0
- package/dist/streams/transforms.js +178 -0
- package/dist/transaction.js +456 -0
- package/dist/types/index.js +180 -0
- package/dist/user-management.js +127 -0
- package/dist/utils/date.js +300 -0
- package/dist/utils/index.js +1079 -0
- package/dist/utils/string.js +161 -0
- package/dist/validators/index.js +19 -0
- package/dist/validators/validator.js +911 -0
- package/package.json +190 -0
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* API Rate Limiter
|
|
4
|
+
*
|
|
5
|
+
* A comprehensive rate limiting system that:
|
|
6
|
+
* - Limits queries per user/role to prevent enumeration attacks
|
|
7
|
+
* - Per-IP rate limiting
|
|
8
|
+
* - Per-endpoint rate limiting
|
|
9
|
+
* - Configurable thresholds with block/throttle actions
|
|
10
|
+
* - OWASP Top 10, NIS2, DORA compliance
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* // Initialize rate limiter
|
|
14
|
+
* const limiter = RateLimiter.create(prorm, {
|
|
15
|
+
* user: { perMinute: 100, perHour: 1000 },
|
|
16
|
+
* ip: { perMinute: 50, perHour: 500 },
|
|
17
|
+
* endpoint: { perMinute: 200, perHour: 2000 },
|
|
18
|
+
* onLimitExceeded: (info) => console.log('Rate limited:', info),
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* // Check if request is allowed
|
|
22
|
+
* const result = limiter.checkLimit(userId, ip, endpoint);
|
|
23
|
+
* if (!result.allowed) {
|
|
24
|
+
* return res.status(429).json({ error: 'Rate limit exceeded' });
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* // Get statistics
|
|
28
|
+
* const stats = limiter.getStats();
|
|
29
|
+
*
|
|
30
|
+
* // Reset counters
|
|
31
|
+
* limiter.reset();
|
|
32
|
+
*
|
|
33
|
+
* Decorator usage:
|
|
34
|
+
* @RateLimit({ perMinute: 100, perHour: 1000 })
|
|
35
|
+
* class User extends Model { ... }
|
|
36
|
+
*/
|
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
exports.RateLimiter = void 0;
|
|
39
|
+
exports.RateLimit = RateLimit;
|
|
40
|
+
exports.getRateLimitOptions = getRateLimitOptions;
|
|
41
|
+
exports.getGlobalLimiter = getGlobalLimiter;
|
|
42
|
+
exports.initRateLimiter = initRateLimiter;
|
|
43
|
+
exports.checkRateLimit = checkRateLimit;
|
|
44
|
+
exports.satisfiesCompliance = satisfiesCompliance;
|
|
45
|
+
exports.getRecommendedLimits = getRecommendedLimits;
|
|
46
|
+
// In-memory storage (default)
|
|
47
|
+
class RateLimitStore {
|
|
48
|
+
constructor() {
|
|
49
|
+
this.entries = new Map();
|
|
50
|
+
}
|
|
51
|
+
getKey(identifier, identifierType, endpoint) {
|
|
52
|
+
return `${identifierType}:${identifier}${endpoint ? `:${endpoint}` : ''}`;
|
|
53
|
+
}
|
|
54
|
+
get(identifier, identifierType, endpoint) {
|
|
55
|
+
return this.entries.get(this.getKey(identifier, identifierType, endpoint));
|
|
56
|
+
}
|
|
57
|
+
set(entry) {
|
|
58
|
+
const key = this.getKey(entry.identifier, entry.identifierType, entry.endpoint);
|
|
59
|
+
this.entries.set(key, entry);
|
|
60
|
+
}
|
|
61
|
+
delete(key) {
|
|
62
|
+
this.entries.delete(key);
|
|
63
|
+
}
|
|
64
|
+
clear() {
|
|
65
|
+
this.entries.clear();
|
|
66
|
+
}
|
|
67
|
+
getAll() {
|
|
68
|
+
return Array.from(this.entries.values());
|
|
69
|
+
}
|
|
70
|
+
getSize() {
|
|
71
|
+
return this.entries.size;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// ==================== Rate Limiter Class ====================
|
|
75
|
+
class RateLimiter {
|
|
76
|
+
constructor(prorm, options = {}) {
|
|
77
|
+
this.dbModel = null;
|
|
78
|
+
this.totalRequests = 0;
|
|
79
|
+
this.blockedRequests = 0;
|
|
80
|
+
this.throttledRequests = 0;
|
|
81
|
+
this.prorm = prorm;
|
|
82
|
+
this.store = new RateLimitStore();
|
|
83
|
+
this.options = {
|
|
84
|
+
user: options.user ?? { perMinute: 100, perHour: 1000, perDay: 10000 },
|
|
85
|
+
anonymous: options.anonymous ?? { perMinute: 20, perHour: 200, perDay: 1000 },
|
|
86
|
+
ip: options.ip ?? { perMinute: 50, perHour: 500, perDay: 5000 },
|
|
87
|
+
endpoint: options.endpoint ?? { perMinute: 200, perHour: 2000, perDay: 20000 },
|
|
88
|
+
role: options.role ?? {},
|
|
89
|
+
action: options.action ?? 'block',
|
|
90
|
+
throttleDelay: options.throttleDelay ?? 1000,
|
|
91
|
+
logEvents: options.logEvents ?? true,
|
|
92
|
+
onLimitExceeded: options.onLimitExceeded ?? (() => { }),
|
|
93
|
+
storeInDb: options.storeInDb ?? false,
|
|
94
|
+
rateLimitModelName: options.rateLimitModelName ?? 'RateLimitEntry',
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Factory method to create a RateLimiter instance
|
|
99
|
+
*/
|
|
100
|
+
static create(prorm, options = {}) {
|
|
101
|
+
return new RateLimiter(prorm, options);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Initialize database storage (optional)
|
|
105
|
+
*/
|
|
106
|
+
async initDatabase() {
|
|
107
|
+
if (!this.options.storeInDb)
|
|
108
|
+
return;
|
|
109
|
+
const modelName = this.options.rateLimitModelName;
|
|
110
|
+
// Check if model already exists
|
|
111
|
+
const existingModel = this.prorm.models[modelName];
|
|
112
|
+
if (existingModel) {
|
|
113
|
+
this.dbModel = existingModel;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Create the model
|
|
117
|
+
this.dbModel = this.prorm.define(modelName, {
|
|
118
|
+
id: {
|
|
119
|
+
type: this.prorm.Prorm.INTEGER,
|
|
120
|
+
primaryKey: true,
|
|
121
|
+
autoIncrement: true,
|
|
122
|
+
},
|
|
123
|
+
identifier: {
|
|
124
|
+
type: this.prorm.Prorm.STRING,
|
|
125
|
+
allowNull: false,
|
|
126
|
+
},
|
|
127
|
+
identifierType: {
|
|
128
|
+
type: this.prorm.Prorm.STRING,
|
|
129
|
+
allowNull: false,
|
|
130
|
+
},
|
|
131
|
+
endpoint: {
|
|
132
|
+
type: this.prorm.Prorm.STRING,
|
|
133
|
+
allowNull: true,
|
|
134
|
+
},
|
|
135
|
+
countMinute: {
|
|
136
|
+
type: this.prorm.Prorm.INTEGER,
|
|
137
|
+
defaultValue: 0,
|
|
138
|
+
},
|
|
139
|
+
countHour: {
|
|
140
|
+
type: this.prorm.Prorm.INTEGER,
|
|
141
|
+
defaultValue: 0,
|
|
142
|
+
},
|
|
143
|
+
countDay: {
|
|
144
|
+
type: this.prorm.Prorm.INTEGER,
|
|
145
|
+
defaultValue: 0,
|
|
146
|
+
},
|
|
147
|
+
windowStartMinute: {
|
|
148
|
+
type: this.prorm.Prorm.BIGINT,
|
|
149
|
+
defaultValue: 0,
|
|
150
|
+
},
|
|
151
|
+
windowStartHour: {
|
|
152
|
+
type: this.prorm.Prorm.BIGINT,
|
|
153
|
+
defaultValue: 0,
|
|
154
|
+
},
|
|
155
|
+
windowStartDay: {
|
|
156
|
+
type: this.prorm.Prorm.BIGINT,
|
|
157
|
+
defaultValue: 0,
|
|
158
|
+
},
|
|
159
|
+
blocked: {
|
|
160
|
+
type: this.prorm.Prorm.BOOLEAN,
|
|
161
|
+
defaultValue: false,
|
|
162
|
+
},
|
|
163
|
+
blockUntil: {
|
|
164
|
+
type: this.prorm.Prorm.DATE,
|
|
165
|
+
allowNull: true,
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
await this.dbModel.sync({ alter: true });
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Check if a request is within rate limits
|
|
172
|
+
*/
|
|
173
|
+
checkLimit(userId, ip, endpoint, role) {
|
|
174
|
+
this.totalRequests++;
|
|
175
|
+
const now = Date.now();
|
|
176
|
+
const currentMinute = Math.floor(now / 60000);
|
|
177
|
+
const currentHour = Math.floor(now / 3600000);
|
|
178
|
+
const currentDay = Math.floor(now / 86400000);
|
|
179
|
+
// Determine which limits apply
|
|
180
|
+
const limits = [];
|
|
181
|
+
// User limit
|
|
182
|
+
if (userId) {
|
|
183
|
+
limits.push({ type: 'user', config: this.options.user, identifier: userId });
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
// Anonymous user limit
|
|
187
|
+
limits.push({ type: 'user', config: this.options.anonymous, identifier: `anon:${ip}` });
|
|
188
|
+
}
|
|
189
|
+
// Role limit
|
|
190
|
+
if (role && this.options.role[role]) {
|
|
191
|
+
limits.push({ type: 'role', config: this.options.role[role], identifier: `role:${role}` });
|
|
192
|
+
}
|
|
193
|
+
// IP limit
|
|
194
|
+
limits.push({ type: 'ip', config: this.options.ip, identifier: ip });
|
|
195
|
+
// Endpoint limit
|
|
196
|
+
limits.push({ type: 'endpoint', config: this.options.endpoint, identifier: endpoint });
|
|
197
|
+
// Check all limits
|
|
198
|
+
let overallAllowed = true;
|
|
199
|
+
let minRemaining = Infinity;
|
|
200
|
+
let minResetIn = Infinity;
|
|
201
|
+
for (const limit of limits) {
|
|
202
|
+
const entry = this.store.get(limit.identifier, limit.type, endpoint);
|
|
203
|
+
let countMinute = 0;
|
|
204
|
+
let countHour = 0;
|
|
205
|
+
let countDay = 0;
|
|
206
|
+
if (entry) {
|
|
207
|
+
// Check if we need to reset windows
|
|
208
|
+
if (entry.windowStartMinute !== currentMinute) {
|
|
209
|
+
entry.countMinute = 0;
|
|
210
|
+
entry.windowStartMinute = currentMinute;
|
|
211
|
+
}
|
|
212
|
+
if (entry.windowStartHour !== currentHour) {
|
|
213
|
+
entry.countHour = 0;
|
|
214
|
+
entry.windowStartHour = currentHour;
|
|
215
|
+
}
|
|
216
|
+
if (entry.windowStartDay !== currentDay) {
|
|
217
|
+
entry.countDay = 0;
|
|
218
|
+
entry.windowStartDay = currentDay;
|
|
219
|
+
}
|
|
220
|
+
// Check if blocked
|
|
221
|
+
if (entry.blocked && entry.blockUntil && entry.blockUntil.getTime() > now) {
|
|
222
|
+
overallAllowed = false;
|
|
223
|
+
minRemaining = 0;
|
|
224
|
+
minResetIn = Math.min(minResetIn, entry.blockUntil.getTime() - now);
|
|
225
|
+
this.blockedRequests++;
|
|
226
|
+
this.options.onLimitExceeded({
|
|
227
|
+
identifier: limit.identifier,
|
|
228
|
+
identifierType: limit.type,
|
|
229
|
+
limit: limit.config.perMinute,
|
|
230
|
+
current: entry.countMinute + 1,
|
|
231
|
+
window: 'minute',
|
|
232
|
+
action: this.options.action,
|
|
233
|
+
timestamp: new Date(),
|
|
234
|
+
});
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
countMinute = entry.countMinute;
|
|
238
|
+
countHour = entry.countHour;
|
|
239
|
+
countDay = entry.countDay;
|
|
240
|
+
}
|
|
241
|
+
// Check per-minute, per-hour, and per-day limits. Each window is
|
|
242
|
+
// tracked and enforced independently (a request can be within the
|
|
243
|
+
// per-minute budget but still exceed the per-hour or per-day
|
|
244
|
+
// budget), matching what RateLimitConfig documents but the original
|
|
245
|
+
// implementation never actually enforced for hour/day.
|
|
246
|
+
const windows = [
|
|
247
|
+
{ window: 'minute', limitVal: limit.config.perMinute, current: countMinute, resetIn: 60000 - (now % 60000) },
|
|
248
|
+
{ window: 'hour', limitVal: limit.config.perHour, current: countHour, resetIn: 3600000 - (now % 3600000) },
|
|
249
|
+
{ window: 'day', limitVal: limit.config.perDay, current: countDay, resetIn: 86400000 - (now % 86400000) },
|
|
250
|
+
];
|
|
251
|
+
const breached = windows.find((w) => w.limitVal !== undefined && w.current + 1 > w.limitVal);
|
|
252
|
+
if (breached) {
|
|
253
|
+
if (this.options.action === 'block') {
|
|
254
|
+
overallAllowed = false;
|
|
255
|
+
minRemaining = 0;
|
|
256
|
+
minResetIn = Math.min(minResetIn, breached.resetIn);
|
|
257
|
+
// Block the identifier until the breached window resets
|
|
258
|
+
this.blockEntry(limit.identifier, limit.type, endpoint, now, breached.resetIn);
|
|
259
|
+
this.options.onLimitExceeded({
|
|
260
|
+
identifier: limit.identifier,
|
|
261
|
+
identifierType: limit.type,
|
|
262
|
+
limit: breached.limitVal,
|
|
263
|
+
current: breached.current + 1,
|
|
264
|
+
window: breached.window,
|
|
265
|
+
action: 'block',
|
|
266
|
+
timestamp: new Date(),
|
|
267
|
+
});
|
|
268
|
+
this.blockedRequests++;
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
// Throttle
|
|
272
|
+
minRemaining = 0;
|
|
273
|
+
minResetIn = Math.min(minResetIn, breached.resetIn);
|
|
274
|
+
this.throttledRequests++;
|
|
275
|
+
this.options.onLimitExceeded({
|
|
276
|
+
identifier: limit.identifier,
|
|
277
|
+
identifierType: limit.type,
|
|
278
|
+
limit: breached.limitVal,
|
|
279
|
+
current: breached.current + 1,
|
|
280
|
+
window: breached.window,
|
|
281
|
+
action: 'throttle',
|
|
282
|
+
timestamp: new Date(),
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
for (const w of windows) {
|
|
288
|
+
if (w.limitVal !== undefined) {
|
|
289
|
+
minRemaining = Math.min(minRemaining, w.limitVal - w.current);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// Update entry - increments minute/hour/day counters independently
|
|
294
|
+
this.updateEntry(limit.identifier, limit.type, endpoint, now);
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
allowed: overallAllowed,
|
|
298
|
+
remaining: minRemaining === Infinity ? Math.min(this.options.user.perMinute, this.options.ip.perMinute, this.options.endpoint.perMinute) : minRemaining,
|
|
299
|
+
resetIn: minResetIn === Infinity ? 60000 - (now % 60000) : minResetIn,
|
|
300
|
+
action: overallAllowed ? undefined : this.options.action,
|
|
301
|
+
throttleDelay: this.options.action === 'throttle' && !overallAllowed ? this.options.throttleDelay : undefined,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Block an identifier until `now + blockDurationMs` (defaults to the
|
|
306
|
+
* standard 1 minute block window when no specific breached window was
|
|
307
|
+
* given, e.g. for callers outside checkLimit).
|
|
308
|
+
*/
|
|
309
|
+
blockEntry(identifier, type, endpoint, now, blockDurationMs = 60000) {
|
|
310
|
+
let entry = this.store.get(identifier, type, endpoint);
|
|
311
|
+
if (!entry) {
|
|
312
|
+
entry = {
|
|
313
|
+
identifier,
|
|
314
|
+
identifierType: type,
|
|
315
|
+
endpoint,
|
|
316
|
+
countMinute: 0,
|
|
317
|
+
countHour: 0,
|
|
318
|
+
countDay: 0,
|
|
319
|
+
windowStartMinute: Math.floor(now / 60000),
|
|
320
|
+
windowStartHour: Math.floor(now / 3600000),
|
|
321
|
+
windowStartDay: Math.floor(now / 86400000),
|
|
322
|
+
lastRequest: new Date(),
|
|
323
|
+
blocked: false,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
entry.blocked = true;
|
|
327
|
+
const newBlockUntil = now + Math.max(blockDurationMs, 0);
|
|
328
|
+
// Never shorten an existing block (e.g. a daily breach shouldn't be
|
|
329
|
+
// clobbered by a shorter minute-level block computed afterwards).
|
|
330
|
+
const existingUntil = entry.blockUntil?.getTime() ?? 0;
|
|
331
|
+
entry.blockUntil = new Date(Math.max(newBlockUntil, existingUntil));
|
|
332
|
+
this.store.set(entry);
|
|
333
|
+
if (this.options.logEvents) {
|
|
334
|
+
console.log(`[RateLimiter] Blocked: ${identifier} (${type}) until ${entry.blockUntil}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Update or create an entry, independently incrementing the minute,
|
|
339
|
+
* hour, and day counters for the current request. Each window is reset
|
|
340
|
+
* to 0 when its bucket boundary rolls over.
|
|
341
|
+
*/
|
|
342
|
+
updateEntry(identifier, type, endpoint, now) {
|
|
343
|
+
const currentMinute = Math.floor(now / 60000);
|
|
344
|
+
const currentHour = Math.floor(now / 3600000);
|
|
345
|
+
const currentDay = Math.floor(now / 86400000);
|
|
346
|
+
let entry = this.store.get(identifier, type, endpoint);
|
|
347
|
+
if (!entry) {
|
|
348
|
+
entry = {
|
|
349
|
+
identifier,
|
|
350
|
+
identifierType: type,
|
|
351
|
+
endpoint,
|
|
352
|
+
countMinute: 0,
|
|
353
|
+
countHour: 0,
|
|
354
|
+
countDay: 0,
|
|
355
|
+
windowStartMinute: currentMinute,
|
|
356
|
+
windowStartHour: currentHour,
|
|
357
|
+
windowStartDay: currentDay,
|
|
358
|
+
lastRequest: new Date(),
|
|
359
|
+
blocked: false,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
// Reset counters whose window has rolled over
|
|
363
|
+
if (entry.windowStartMinute !== currentMinute) {
|
|
364
|
+
entry.countMinute = 0;
|
|
365
|
+
entry.windowStartMinute = currentMinute;
|
|
366
|
+
}
|
|
367
|
+
if (entry.windowStartHour !== currentHour) {
|
|
368
|
+
entry.countHour = 0;
|
|
369
|
+
entry.windowStartHour = currentHour;
|
|
370
|
+
}
|
|
371
|
+
if (entry.windowStartDay !== currentDay) {
|
|
372
|
+
entry.countDay = 0;
|
|
373
|
+
entry.windowStartDay = currentDay;
|
|
374
|
+
}
|
|
375
|
+
// Each window tracks its own independent count of requests seen within
|
|
376
|
+
// it, rather than all three being forced to the same value.
|
|
377
|
+
entry.countMinute += 1;
|
|
378
|
+
entry.countHour += 1;
|
|
379
|
+
entry.countDay += 1;
|
|
380
|
+
entry.lastRequest = new Date();
|
|
381
|
+
// Unblock if needed
|
|
382
|
+
if (entry.blocked && (!entry.blockUntil || entry.blockUntil.getTime() <= now)) {
|
|
383
|
+
entry.blocked = false;
|
|
384
|
+
entry.blockUntil = undefined;
|
|
385
|
+
}
|
|
386
|
+
this.store.set(entry);
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Get rate limit statistics
|
|
390
|
+
*/
|
|
391
|
+
getStats() {
|
|
392
|
+
const entries = this.store.getAll();
|
|
393
|
+
const byUser = {};
|
|
394
|
+
const byIP = {};
|
|
395
|
+
const byEndpoint = {};
|
|
396
|
+
const uniqueUsers = new Set();
|
|
397
|
+
const uniqueIPs = new Set();
|
|
398
|
+
const uniqueEndpoints = new Set();
|
|
399
|
+
for (const entry of entries) {
|
|
400
|
+
if (entry.identifierType === 'user') {
|
|
401
|
+
byUser[entry.identifier] = {
|
|
402
|
+
minute: entry.countMinute,
|
|
403
|
+
hour: entry.countHour,
|
|
404
|
+
day: entry.countDay,
|
|
405
|
+
};
|
|
406
|
+
uniqueUsers.add(entry.identifier);
|
|
407
|
+
}
|
|
408
|
+
else if (entry.identifierType === 'ip') {
|
|
409
|
+
byIP[entry.identifier] = {
|
|
410
|
+
minute: entry.countMinute,
|
|
411
|
+
hour: entry.countHour,
|
|
412
|
+
day: entry.countDay,
|
|
413
|
+
};
|
|
414
|
+
uniqueIPs.add(entry.identifier);
|
|
415
|
+
}
|
|
416
|
+
else if (entry.identifierType === 'endpoint') {
|
|
417
|
+
byEndpoint[entry.identifier] = {
|
|
418
|
+
minute: entry.countMinute,
|
|
419
|
+
hour: entry.countHour,
|
|
420
|
+
day: entry.countDay,
|
|
421
|
+
};
|
|
422
|
+
uniqueEndpoints.add(entry.identifier);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return {
|
|
426
|
+
totalRequests: this.totalRequests,
|
|
427
|
+
blockedRequests: this.blockedRequests,
|
|
428
|
+
throttledRequests: this.throttledRequests,
|
|
429
|
+
uniqueUsers: uniqueUsers.size,
|
|
430
|
+
uniqueIPs: uniqueIPs.size,
|
|
431
|
+
uniqueEndpoints: uniqueEndpoints.size,
|
|
432
|
+
byUser,
|
|
433
|
+
byIP,
|
|
434
|
+
byEndpoint,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Reset all rate limit counters
|
|
439
|
+
*/
|
|
440
|
+
reset() {
|
|
441
|
+
this.store.clear();
|
|
442
|
+
this.totalRequests = 0;
|
|
443
|
+
this.blockedRequests = 0;
|
|
444
|
+
this.throttledRequests = 0;
|
|
445
|
+
if (this.options.logEvents) {
|
|
446
|
+
console.log('[RateLimiter] Counters reset');
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Reset specific user/IP
|
|
451
|
+
*/
|
|
452
|
+
resetIdentifier(identifier, type, endpoint) {
|
|
453
|
+
const key = `${type}:${identifier}${endpoint ? `:${endpoint}` : ''}`;
|
|
454
|
+
this.store.delete(key);
|
|
455
|
+
if (this.options.logEvents) {
|
|
456
|
+
console.log(`[RateLimiter] Reset: ${key}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Get current window reset time
|
|
461
|
+
*/
|
|
462
|
+
getResetTime() {
|
|
463
|
+
const now = Date.now();
|
|
464
|
+
return 60000 - (now % 60000); // Reset in X seconds
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
exports.RateLimiter = RateLimiter;
|
|
468
|
+
/**
|
|
469
|
+
* Rate limit decorator for models
|
|
470
|
+
*
|
|
471
|
+
* Usage:
|
|
472
|
+
* @RateLimit({ perMinute: 100, perHour: 1000 })
|
|
473
|
+
* class User extends Model { ... }
|
|
474
|
+
*/
|
|
475
|
+
function RateLimit(options) {
|
|
476
|
+
return function (target) {
|
|
477
|
+
// Store rate limit metadata on the model
|
|
478
|
+
target.rateLimitOptions = {
|
|
479
|
+
perMinute: options.perMinute ?? 100,
|
|
480
|
+
perHour: options.perHour ?? 1000,
|
|
481
|
+
perDay: options.perDay ?? 10000,
|
|
482
|
+
endpoint: options.endpoint,
|
|
483
|
+
roles: options.roles,
|
|
484
|
+
};
|
|
485
|
+
return target;
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Get rate limit options from a model
|
|
490
|
+
*/
|
|
491
|
+
function getRateLimitOptions(model) {
|
|
492
|
+
return model.rateLimitOptions ?? null;
|
|
493
|
+
}
|
|
494
|
+
// ==================== Utility Functions ====================
|
|
495
|
+
let globalLimiter = null;
|
|
496
|
+
/**
|
|
497
|
+
* Get the global rate limiter instance
|
|
498
|
+
*/
|
|
499
|
+
function getGlobalLimiter() {
|
|
500
|
+
return globalLimiter;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Initialize the global rate limiter
|
|
504
|
+
*/
|
|
505
|
+
function initRateLimiter(prorm, options) {
|
|
506
|
+
globalLimiter = new RateLimiter(prorm, options);
|
|
507
|
+
return globalLimiter;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Quick rate limit check using global limiter
|
|
511
|
+
*/
|
|
512
|
+
function checkRateLimit(userId, ip, endpoint, role) {
|
|
513
|
+
if (!globalLimiter) {
|
|
514
|
+
return { allowed: true, remaining: 100, resetIn: 60000 };
|
|
515
|
+
}
|
|
516
|
+
return globalLimiter.checkLimit(userId, ip, endpoint, role);
|
|
517
|
+
}
|
|
518
|
+
// ==================== Compliance Helpers ====================
|
|
519
|
+
/**
|
|
520
|
+
* Check if rate limiting satisfies a compliance framework
|
|
521
|
+
*/
|
|
522
|
+
function satisfiesCompliance(framework) {
|
|
523
|
+
const frameworks = [
|
|
524
|
+
'OWASP_Top_10',
|
|
525
|
+
'NIST_800_53',
|
|
526
|
+
'NIST_CSF',
|
|
527
|
+
'ISO_27001',
|
|
528
|
+
'SOC_2',
|
|
529
|
+
'DORA',
|
|
530
|
+
'NIS2',
|
|
531
|
+
'PCI_DSS',
|
|
532
|
+
];
|
|
533
|
+
return frameworks.includes(framework);
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Get recommended rate limits for a framework
|
|
537
|
+
*/
|
|
538
|
+
function getRecommendedLimits(framework) {
|
|
539
|
+
switch (framework) {
|
|
540
|
+
case 'OWASP_Top_10':
|
|
541
|
+
// Prevent enumeration attacks
|
|
542
|
+
return {
|
|
543
|
+
user: { perMinute: 100, perHour: 1000 },
|
|
544
|
+
ip: { perMinute: 50, perHour: 500 },
|
|
545
|
+
endpoint: { perMinute: 200, perHour: 2000 },
|
|
546
|
+
action: 'block',
|
|
547
|
+
};
|
|
548
|
+
case 'DORA':
|
|
549
|
+
// Digital Operational Resilience Act
|
|
550
|
+
return {
|
|
551
|
+
user: { perMinute: 200, perHour: 2000, perDay: 10000 },
|
|
552
|
+
ip: { perMinute: 100, perHour: 1000, perDay: 5000 },
|
|
553
|
+
endpoint: { perMinute: 300, perHour: 3000, perDay: 30000 },
|
|
554
|
+
action: 'throttle',
|
|
555
|
+
throttleDelay: 500,
|
|
556
|
+
};
|
|
557
|
+
case 'NIS2':
|
|
558
|
+
// Network and Information Security Directive
|
|
559
|
+
return {
|
|
560
|
+
user: { perMinute: 150, perHour: 1500, perDay: 10000 },
|
|
561
|
+
ip: { perMinute: 75, perHour: 750, perDay: 5000 },
|
|
562
|
+
endpoint: { perMinute: 250, perHour: 2500, perDay: 25000 },
|
|
563
|
+
action: 'block',
|
|
564
|
+
logEvents: true,
|
|
565
|
+
};
|
|
566
|
+
case 'PCI_DSS':
|
|
567
|
+
return {
|
|
568
|
+
user: { perMinute: 50, perHour: 500, perDay: 2000 },
|
|
569
|
+
ip: { perMinute: 25, perHour: 250, perDay: 1000 },
|
|
570
|
+
endpoint: { perMinute: 100, perHour: 1000, perDay: 10000 },
|
|
571
|
+
action: 'block',
|
|
572
|
+
};
|
|
573
|
+
default:
|
|
574
|
+
return {
|
|
575
|
+
user: { perMinute: 100, perHour: 1000 },
|
|
576
|
+
ip: { perMinute: 50, perHour: 500 },
|
|
577
|
+
endpoint: { perMinute: 200, perHour: 2000 },
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* RightToErasure
|
|
4
|
+
*
|
|
5
|
+
* Handles Data Subject Access Requests (DSARs) for the right to be forgotten.
|
|
6
|
+
* Supports hard deletion or field-level anonymization (GDPR-preferred),
|
|
7
|
+
* and cascades through associated models.
|
|
8
|
+
*
|
|
9
|
+
* Covers ~57 compliance frameworks including:
|
|
10
|
+
* GDPR Art 17, CCPA, CPRA, LGPD, APPI, PIPA (South Korea),
|
|
11
|
+
* PDPA (Thailand, Singapore, Philippines, Malaysia), PoPI, DPDP India,
|
|
12
|
+
* and all US state privacy laws with deletion rights.
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* // Hard delete all records for a user:
|
|
16
|
+
* const result = await RightToErasure.erase(User, { where: { id: userId } });
|
|
17
|
+
*
|
|
18
|
+
* // Anonymize instead of delete (preserves referential integrity):
|
|
19
|
+
* const result = await RightToErasure.erase(User, { where: { id: userId } }, {
|
|
20
|
+
* mode: 'anonymize',
|
|
21
|
+
* piiFields: ['email', 'name', 'phoneNumber', 'address'],
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // Cascade to associated models:
|
|
25
|
+
* const result = await RightToErasure.erase(User, { where: { id: userId } }, {
|
|
26
|
+
* mode: 'anonymize',
|
|
27
|
+
* cascade: [
|
|
28
|
+
* { model: Order, foreignKey: 'userId', piiFields: ['shippingAddress'] },
|
|
29
|
+
* { model: Payment, foreignKey: 'userId', piiFields: ['cardLastFour'] },
|
|
30
|
+
* ],
|
|
31
|
+
* });
|
|
32
|
+
*/
|
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
+
exports.RightToErasure = void 0;
|
|
35
|
+
function generateRequestId() {
|
|
36
|
+
return `dsar-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
37
|
+
}
|
|
38
|
+
async function anonymizeRecords(records, piiFields, redactedValue) {
|
|
39
|
+
let count = 0;
|
|
40
|
+
for (const record of records) {
|
|
41
|
+
const patch = {};
|
|
42
|
+
for (const field of piiFields) {
|
|
43
|
+
patch[field] = redactedValue;
|
|
44
|
+
}
|
|
45
|
+
if (Object.keys(patch).length > 0) {
|
|
46
|
+
await record.update(patch);
|
|
47
|
+
count++;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return count;
|
|
51
|
+
}
|
|
52
|
+
async function processErasure(model, where, options, requestId) {
|
|
53
|
+
const modelName = model.name ?? model.tableName ?? 'Unknown';
|
|
54
|
+
let affected = 0;
|
|
55
|
+
if (options.mode === 'delete') {
|
|
56
|
+
affected = (await model.destroy({ where, force: options.force })) ?? 0;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
const records = await model.findAll({ where });
|
|
60
|
+
const pii = options.piiFields.length > 0 ? options.piiFields : Object.keys(records[0]?.dataValues ?? {}).filter((k) => k !== 'id' && k !== 'createdAt' && k !== 'updatedAt');
|
|
61
|
+
affected = await anonymizeRecords(records, pii, options.redactedValue);
|
|
62
|
+
}
|
|
63
|
+
const cascadeResults = [];
|
|
64
|
+
for (const target of options.cascade) {
|
|
65
|
+
// Find the primary key values of the erased subject to cascade
|
|
66
|
+
const subjectIds = await model.findAll({ where, attributes: ['id'] });
|
|
67
|
+
const ids = subjectIds.map((r) => r.id ?? r.dataValues?.id);
|
|
68
|
+
if (ids.length === 0)
|
|
69
|
+
continue;
|
|
70
|
+
const cascadeWhere = { [target.foreignKey]: ids };
|
|
71
|
+
const cascadeResult = await processErasure(target.model, cascadeWhere, {
|
|
72
|
+
mode: options.mode,
|
|
73
|
+
piiFields: target.piiFields ?? [],
|
|
74
|
+
redactedValue: options.redactedValue,
|
|
75
|
+
force: options.force,
|
|
76
|
+
cascade: target.cascade ?? [],
|
|
77
|
+
}, requestId);
|
|
78
|
+
cascadeResults.push(cascadeResult);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
model: modelName,
|
|
82
|
+
mode: options.mode,
|
|
83
|
+
affected,
|
|
84
|
+
cascadeResults: cascadeResults.length > 0 ? cascadeResults : undefined,
|
|
85
|
+
timestamp: new Date(),
|
|
86
|
+
requestId,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
class RightToErasure {
|
|
90
|
+
/**
|
|
91
|
+
* Erase personal data for one or more data subjects.
|
|
92
|
+
*
|
|
93
|
+
* @param model - The primary model to erase from
|
|
94
|
+
* @param query - Where clause identifying the data subject(s)
|
|
95
|
+
* @param options - Erasure mode, fields, and cascade configuration
|
|
96
|
+
* @returns An ErasureResult with a unique request ID for DSAR audit purposes
|
|
97
|
+
*/
|
|
98
|
+
static async erase(model, query, options = {}) {
|
|
99
|
+
const resolved = {
|
|
100
|
+
mode: options.mode ?? 'delete',
|
|
101
|
+
piiFields: options.piiFields ?? [],
|
|
102
|
+
redactedValue: options.redactedValue ?? '[REDACTED]',
|
|
103
|
+
force: options.force !== false,
|
|
104
|
+
cascade: options.cascade ?? [],
|
|
105
|
+
};
|
|
106
|
+
const requestId = generateRequestId();
|
|
107
|
+
return processErasure(model, query.where, resolved, requestId);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Check if records exist for a given subject (for verification before erasure).
|
|
111
|
+
*/
|
|
112
|
+
static async exists(model, query) {
|
|
113
|
+
const count = await model.count({ where: query.where });
|
|
114
|
+
return count > 0;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
exports.RightToErasure = RightToErasure;
|