noormme 1.0.0 → 1.0.2
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/LICENSE +89 -21
- package/README.md +195 -578
- package/dist/cjs/cli/commands/analyze.js +4 -1
- package/dist/cjs/cli/commands/generate.js +48 -8
- package/dist/cjs/cli/commands/init.js +54 -14
- package/dist/cjs/cli/commands/inspect.js +10 -3
- package/dist/cjs/cli/commands/migrate.js +38 -2
- package/dist/cjs/cli/commands/optimize.js +4 -1
- package/dist/cjs/cli/commands/status.js +41 -5
- package/dist/cjs/cli/commands/watch.js +4 -1
- package/dist/cjs/cli/index.js +4 -1
- package/dist/cjs/dialect/database-introspector.js +16 -21
- package/dist/cjs/dialect/sqlite/sqlite-introspector.js +13 -24
- package/dist/cjs/dynamic/dynamic.d.ts +20 -0
- package/dist/cjs/dynamic/dynamic.js +25 -0
- package/dist/cjs/edge-runtime/edge-config.d.ts +125 -0
- package/dist/cjs/edge-runtime/edge-config.js +323 -0
- package/dist/cjs/errors/NoormError.d.ts +27 -4
- package/dist/cjs/errors/NoormError.js +142 -21
- package/dist/cjs/logging/logger.d.ts +7 -2
- package/dist/cjs/logging/logger.js +21 -7
- package/dist/cjs/noormme.d.ts +12 -8
- package/dist/cjs/noormme.js +133 -61
- package/dist/cjs/operation-node/column-node.js +4 -0
- package/dist/cjs/operation-node/identifier-node.js +4 -0
- package/dist/cjs/operation-node/table-node.js +7 -0
- package/dist/cjs/parser/reference-parser.js +5 -0
- package/dist/cjs/parser/table-parser.js +2 -0
- package/dist/cjs/performance/index.d.ts +44 -0
- package/dist/cjs/performance/index.js +64 -0
- package/dist/cjs/performance/query-optimizer.d.ts +134 -0
- package/dist/cjs/performance/query-optimizer.js +391 -0
- package/dist/cjs/performance/services/cache-service.d.ts +177 -0
- package/dist/cjs/performance/services/cache-service.js +415 -0
- package/dist/cjs/performance/services/connection-factory.d.ts +198 -0
- package/dist/cjs/performance/services/connection-factory.js +498 -0
- package/dist/cjs/performance/services/metrics-collector.d.ts +162 -0
- package/dist/cjs/performance/services/metrics-collector.js +406 -0
- package/dist/cjs/performance/utils/query-parser.d.ts +123 -0
- package/dist/cjs/performance/utils/query-parser.js +295 -0
- package/dist/cjs/raw-builder/sql.d.ts +73 -26
- package/dist/cjs/raw-builder/sql.js +9 -0
- package/dist/cjs/repository/repository-factory.d.ts +10 -42
- package/dist/cjs/repository/repository-factory.js +276 -394
- package/dist/cjs/schema/core/coordinators/schema-discovery.coordinator.js +6 -4
- package/dist/cjs/schema/core/factories/discovery-factory.js +5 -5
- package/dist/cjs/schema/core/utils/name-generator.js +34 -2
- package/dist/cjs/schema/core/utils/type-mapper.d.ts +19 -14
- package/dist/cjs/schema/core/utils/type-mapper.js +4 -7
- package/dist/cjs/schema/dialects/sqlite/discovery/sqlite-constraint-discovery.js +3 -2
- package/dist/cjs/schema/dialects/sqlite/sqlite-discovery.coordinator.d.ts +2 -0
- package/dist/cjs/schema/dialects/sqlite/sqlite-discovery.coordinator.js +19 -5
- package/dist/cjs/schema/test/dialect-capabilities.test.js +6 -0
- package/dist/cjs/schema/test/error-handling.test.js +52 -33
- package/dist/cjs/schema/test/integration.test.js +51 -5
- package/dist/cjs/schema/test/sqlite-discovery-coordinator.test.js +8 -4
- package/dist/cjs/sqlite-migration/index.js +35 -2
- package/dist/cjs/testing/test-utils.d.ts +5 -0
- package/dist/cjs/testing/test-utils.js +66 -6
- package/dist/cjs/types/index.d.ts +78 -13
- package/dist/cjs/types/index.js +23 -0
- package/dist/cjs/types/type-generator.d.ts +8 -0
- package/dist/cjs/types/type-generator.js +86 -17
- package/dist/cjs/util/safe-sql-helpers.d.ts +124 -0
- package/dist/cjs/util/safe-sql-helpers.js +221 -0
- package/dist/cjs/util/security-validator.d.ts +44 -0
- package/dist/cjs/util/security-validator.js +256 -0
- package/dist/cjs/util/security.d.ts +60 -0
- package/dist/cjs/util/security.js +137 -0
- package/dist/cjs/watch/schema-watcher.js +26 -7
- package/dist/esm/cli/commands/generate.js +10 -6
- package/dist/esm/cli/commands/init.js +15 -11
- package/dist/esm/cli/commands/inspect.js +6 -2
- package/dist/esm/cli/commands/status.js +3 -3
- package/dist/esm/dialect/database-introspector.js +16 -21
- package/dist/esm/dialect/sqlite/sqlite-introspector.js +13 -24
- package/dist/esm/dynamic/dynamic.d.ts +20 -0
- package/dist/esm/dynamic/dynamic.js +25 -0
- package/dist/esm/edge-runtime/edge-config.d.ts +125 -0
- package/dist/esm/edge-runtime/edge-config.js +281 -0
- package/dist/esm/errors/NoormError.d.ts +27 -4
- package/dist/esm/errors/NoormError.js +134 -18
- package/dist/esm/logging/logger.d.ts +7 -2
- package/dist/esm/logging/logger.js +21 -7
- package/dist/esm/noormme.d.ts +12 -8
- package/dist/esm/noormme.js +130 -61
- package/dist/esm/operation-node/column-node.js +4 -0
- package/dist/esm/operation-node/identifier-node.js +4 -0
- package/dist/esm/operation-node/table-node.js +7 -0
- package/dist/esm/parser/reference-parser.js +5 -0
- package/dist/esm/parser/table-parser.js +2 -0
- package/dist/esm/performance/index.d.ts +44 -0
- package/dist/esm/performance/index.js +48 -0
- package/dist/esm/performance/query-optimizer.d.ts +134 -0
- package/dist/esm/performance/query-optimizer.js +387 -0
- package/dist/esm/performance/services/cache-service.d.ts +177 -0
- package/dist/esm/performance/services/cache-service.js +410 -0
- package/dist/esm/performance/services/connection-factory.d.ts +198 -0
- package/dist/esm/performance/services/connection-factory.js +493 -0
- package/dist/esm/performance/services/metrics-collector.d.ts +162 -0
- package/dist/esm/performance/services/metrics-collector.js +402 -0
- package/dist/esm/performance/utils/query-parser.d.ts +123 -0
- package/dist/esm/performance/utils/query-parser.js +292 -0
- package/dist/esm/raw-builder/sql.d.ts +73 -26
- package/dist/esm/raw-builder/sql.js +9 -0
- package/dist/esm/repository/repository-factory.d.ts +10 -42
- package/dist/esm/repository/repository-factory.js +277 -395
- package/dist/esm/schema/core/coordinators/schema-discovery.coordinator.js +6 -4
- package/dist/esm/schema/core/factories/discovery-factory.js +5 -5
- package/dist/esm/schema/core/utils/name-generator.js +34 -2
- package/dist/esm/schema/core/utils/type-mapper.d.ts +19 -14
- package/dist/esm/schema/core/utils/type-mapper.js +4 -7
- package/dist/esm/schema/dialects/sqlite/discovery/sqlite-constraint-discovery.js +3 -2
- package/dist/esm/schema/dialects/sqlite/sqlite-discovery.coordinator.d.ts +2 -0
- package/dist/esm/schema/dialects/sqlite/sqlite-discovery.coordinator.js +19 -5
- package/dist/esm/schema/test/dialect-capabilities.test.js +6 -0
- package/dist/esm/schema/test/error-handling.test.js +52 -33
- package/dist/esm/schema/test/integration.test.js +18 -5
- package/dist/esm/schema/test/sqlite-discovery-coordinator.test.js +8 -4
- package/dist/esm/testing/test-utils.d.ts +5 -0
- package/dist/esm/testing/test-utils.js +66 -6
- package/dist/esm/types/index.d.ts +78 -13
- package/dist/esm/types/index.js +22 -1
- package/dist/esm/types/type-generator.d.ts +8 -0
- package/dist/esm/types/type-generator.js +86 -17
- package/dist/esm/util/safe-sql-helpers.d.ts +124 -0
- package/dist/esm/util/safe-sql-helpers.js +209 -0
- package/dist/esm/util/security-validator.d.ts +44 -0
- package/dist/esm/util/security-validator.js +246 -0
- package/dist/esm/util/security.d.ts +60 -0
- package/dist/esm/util/security.js +114 -0
- package/dist/esm/watch/schema-watcher.js +26 -7
- package/package.json +1 -1
- package/dist/cjs/performance/query-analyzer.d.ts +0 -89
- package/dist/cjs/performance/query-analyzer.js +0 -263
- package/dist/esm/performance/query-analyzer.d.ts +0 -89
- package/dist/esm/performance/query-analyzer.js +0 -260
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Safe SQL helper utilities
|
|
4
|
+
* Provides secure alternatives to dangerous sql.raw() and sql.lit() methods
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.SafeSQLExamples = exports.SafeSQLKeywords = void 0;
|
|
8
|
+
exports.safeOrderDirection = safeOrderDirection;
|
|
9
|
+
exports.safeLimit = safeLimit;
|
|
10
|
+
exports.safeOffset = safeOffset;
|
|
11
|
+
exports.safeKeyword = safeKeyword;
|
|
12
|
+
exports.safeLockMode = safeLockMode;
|
|
13
|
+
exports.safeOrderBy = safeOrderBy;
|
|
14
|
+
exports.safeBoolean = safeBoolean;
|
|
15
|
+
exports.safeCaseStatement = safeCaseStatement;
|
|
16
|
+
exports.validateEnum = validateEnum;
|
|
17
|
+
exports.validateNumericRange = validateNumericRange;
|
|
18
|
+
const sql_js_1 = require("../raw-builder/sql.js");
|
|
19
|
+
/**
|
|
20
|
+
* Safely creates a SQL ORDER BY direction clause
|
|
21
|
+
* Only allows 'ASC' or 'DESC'
|
|
22
|
+
*/
|
|
23
|
+
function safeOrderDirection(direction) {
|
|
24
|
+
const normalized = direction.toUpperCase().trim();
|
|
25
|
+
if (normalized !== 'ASC' && normalized !== 'DESC') {
|
|
26
|
+
throw new Error(`Invalid order direction: "${direction}". Only "ASC" and "DESC" are allowed.`);
|
|
27
|
+
}
|
|
28
|
+
return sql_js_1.sql.raw(normalized);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Safely creates a SQL LIMIT clause
|
|
32
|
+
* Validates that the limit is a positive integer
|
|
33
|
+
*/
|
|
34
|
+
function safeLimit(limit) {
|
|
35
|
+
const numLimit = typeof limit === 'string' ? parseInt(limit, 10) : limit;
|
|
36
|
+
if (!Number.isInteger(numLimit) || numLimit <= 0 || numLimit > 10000) {
|
|
37
|
+
throw new Error(`Invalid limit: "${limit}". Must be a positive integer between 1 and 10000.`);
|
|
38
|
+
}
|
|
39
|
+
return sql_js_1.sql.raw(numLimit.toString());
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Safely creates a SQL OFFSET clause
|
|
43
|
+
* Validates that the offset is a non-negative integer
|
|
44
|
+
*/
|
|
45
|
+
function safeOffset(offset) {
|
|
46
|
+
const numOffset = typeof offset === 'string' ? parseInt(offset, 10) : offset;
|
|
47
|
+
if (!Number.isInteger(numOffset) || numOffset < 0 || numOffset > 1000000) {
|
|
48
|
+
throw new Error(`Invalid offset: "${offset}". Must be a non-negative integer between 0 and 1000000.`);
|
|
49
|
+
}
|
|
50
|
+
return sql_js_1.sql.raw(numOffset.toString());
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Safely creates SQL keywords from a whitelist
|
|
54
|
+
* Useful for database-specific syntax that must be hardcoded
|
|
55
|
+
*/
|
|
56
|
+
function safeKeyword(keyword, allowedKeywords) {
|
|
57
|
+
const normalized = keyword.toUpperCase().trim();
|
|
58
|
+
const allowedNormalized = allowedKeywords.map(k => k.toUpperCase().trim());
|
|
59
|
+
if (!allowedNormalized.includes(normalized)) {
|
|
60
|
+
throw new Error(`Invalid keyword: "${keyword}". Allowed keywords: ${allowedKeywords.join(', ')}`);
|
|
61
|
+
}
|
|
62
|
+
return sql_js_1.sql.raw(normalized);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Type-safe enum for common SQL keywords
|
|
66
|
+
*/
|
|
67
|
+
exports.SafeSQLKeywords = {
|
|
68
|
+
/**
|
|
69
|
+
* Lock modes for SELECT statements
|
|
70
|
+
*/
|
|
71
|
+
LockMode: {
|
|
72
|
+
FOR_UPDATE: 'FOR UPDATE',
|
|
73
|
+
FOR_SHARE: 'FOR SHARE',
|
|
74
|
+
FOR_NO_KEY_UPDATE: 'FOR NO KEY UPDATE',
|
|
75
|
+
FOR_KEY_SHARE: 'FOR KEY SHARE',
|
|
76
|
+
},
|
|
77
|
+
/**
|
|
78
|
+
* Join types
|
|
79
|
+
*/
|
|
80
|
+
JoinType: {
|
|
81
|
+
INNER: 'INNER',
|
|
82
|
+
LEFT: 'LEFT',
|
|
83
|
+
RIGHT: 'RIGHT',
|
|
84
|
+
FULL: 'FULL',
|
|
85
|
+
CROSS: 'CROSS',
|
|
86
|
+
},
|
|
87
|
+
/**
|
|
88
|
+
* Set operations
|
|
89
|
+
*/
|
|
90
|
+
SetOperation: {
|
|
91
|
+
UNION: 'UNION',
|
|
92
|
+
UNION_ALL: 'UNION ALL',
|
|
93
|
+
INTERSECT: 'INTERSECT',
|
|
94
|
+
EXCEPT: 'EXCEPT',
|
|
95
|
+
},
|
|
96
|
+
/**
|
|
97
|
+
* SQLite-specific pragmas (use with extreme caution)
|
|
98
|
+
*/
|
|
99
|
+
SafePragma: {
|
|
100
|
+
OPTIMIZE: 'OPTIMIZE',
|
|
101
|
+
WAL_CHECKPOINT: 'WAL_CHECKPOINT',
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Safely creates a lock mode clause for SELECT statements
|
|
106
|
+
*/
|
|
107
|
+
function safeLockMode(mode) {
|
|
108
|
+
return sql_js_1.sql.raw(exports.SafeSQLKeywords.LockMode[mode]);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Creates a safe ORDER BY clause from validated columns
|
|
112
|
+
*/
|
|
113
|
+
function safeOrderBy(orderBy, allowedColumns) {
|
|
114
|
+
if (orderBy.length === 0) {
|
|
115
|
+
throw new Error('At least one order by clause is required');
|
|
116
|
+
}
|
|
117
|
+
const clauses = orderBy.map(({ column, direction = 'ASC' }) => {
|
|
118
|
+
if (!allowedColumns.includes(column)) {
|
|
119
|
+
throw new Error(`Invalid order by column: "${column}". Allowed columns: ${allowedColumns.join(', ')}`);
|
|
120
|
+
}
|
|
121
|
+
const dir = direction.toUpperCase();
|
|
122
|
+
if (dir !== 'ASC' && dir !== 'DESC') {
|
|
123
|
+
throw new Error(`Invalid direction: "${direction}". Must be ASC or DESC`);
|
|
124
|
+
}
|
|
125
|
+
return `${column} ${dir}`;
|
|
126
|
+
});
|
|
127
|
+
return sql_js_1.sql.raw(clauses.join(', '));
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Validates and creates a safe boolean value for SQL
|
|
131
|
+
*/
|
|
132
|
+
function safeBoolean(value) {
|
|
133
|
+
let boolValue;
|
|
134
|
+
if (typeof value === 'boolean') {
|
|
135
|
+
boolValue = value;
|
|
136
|
+
}
|
|
137
|
+
else if (value === 'true' || value === '1' || value === 1) {
|
|
138
|
+
boolValue = true;
|
|
139
|
+
}
|
|
140
|
+
else if (value === 'false' || value === '0' || value === 0) {
|
|
141
|
+
boolValue = false;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
throw new Error(`Invalid boolean value: "${value}". Must be true, false, 1, 0, "true", or "false"`);
|
|
145
|
+
}
|
|
146
|
+
// Use parameterized value instead of raw SQL
|
|
147
|
+
return (0, sql_js_1.sql) `${boolValue}`;
|
|
148
|
+
}
|
|
149
|
+
function safeCaseStatement(cases, elseResult) {
|
|
150
|
+
if (cases.length === 0) {
|
|
151
|
+
throw new Error('At least one WHEN clause is required');
|
|
152
|
+
}
|
|
153
|
+
// Note: This function needs to be refactored to not use sql.raw()
|
|
154
|
+
// For now, we construct the CASE statement carefully
|
|
155
|
+
const whenClauses = cases
|
|
156
|
+
.map((c, i) => {
|
|
157
|
+
// Conditions must be RawBuilder instances to ensure they're safe
|
|
158
|
+
const conditionSql = c.condition.toOperationNode();
|
|
159
|
+
return `WHEN ${conditionSql} THEN ${c.result}`;
|
|
160
|
+
})
|
|
161
|
+
.join(' ');
|
|
162
|
+
return sql_js_1.sql.raw(`CASE ${whenClauses} ELSE ${elseResult} END`);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Example usage and best practices
|
|
166
|
+
*/
|
|
167
|
+
exports.SafeSQLExamples = {
|
|
168
|
+
/**
|
|
169
|
+
* Example: Safe pagination with user input
|
|
170
|
+
*/
|
|
171
|
+
safePagination: () => {
|
|
172
|
+
const userPage = '2'; // From request
|
|
173
|
+
const userLimit = '10'; // From request
|
|
174
|
+
const offset = (parseInt(userPage) - 1) * parseInt(userLimit);
|
|
175
|
+
return {
|
|
176
|
+
limit: safeLimit(userLimit),
|
|
177
|
+
offset: safeOffset(offset),
|
|
178
|
+
};
|
|
179
|
+
},
|
|
180
|
+
/**
|
|
181
|
+
* Example: Safe sorting with user input
|
|
182
|
+
*/
|
|
183
|
+
safeSorting: (userSortColumn, userDirection) => {
|
|
184
|
+
const allowedColumns = ['name', 'email', 'created_at', 'updated_at'];
|
|
185
|
+
if (!allowedColumns.includes(userSortColumn)) {
|
|
186
|
+
throw new Error('Invalid sort column');
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
column: sql_js_1.sql.ref(userSortColumn),
|
|
190
|
+
direction: safeOrderDirection(userDirection),
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
/**
|
|
194
|
+
* Example: Safe lock mode for transactions
|
|
195
|
+
*/
|
|
196
|
+
safeTransactionLock: () => {
|
|
197
|
+
return safeLockMode('FOR_UPDATE');
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
/**
|
|
201
|
+
* Helper to validate that a value is from a specific enum
|
|
202
|
+
*/
|
|
203
|
+
function validateEnum(value, enumValues, fieldName = 'value') {
|
|
204
|
+
if (!enumValues.includes(value)) {
|
|
205
|
+
throw new Error(`Invalid ${fieldName}: "${value}". Allowed values: ${enumValues.join(', ')}`);
|
|
206
|
+
}
|
|
207
|
+
return value;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Helper to validate numeric ranges
|
|
211
|
+
*/
|
|
212
|
+
function validateNumericRange(value, min, max, fieldName = 'value') {
|
|
213
|
+
const num = typeof value === 'string' ? parseFloat(value) : value;
|
|
214
|
+
if (isNaN(num)) {
|
|
215
|
+
throw new Error(`Invalid ${fieldName}: "${value}". Must be a number`);
|
|
216
|
+
}
|
|
217
|
+
if (num < min || num > max) {
|
|
218
|
+
throw new Error(`Invalid ${fieldName}: ${num}. Must be between ${min} and ${max}`);
|
|
219
|
+
}
|
|
220
|
+
return num;
|
|
221
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security validation utilities for NOORMME
|
|
3
|
+
* Provides input validation and sanitization to prevent injection attacks
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Validates identifier names (table names, column names, schema names)
|
|
7
|
+
* to prevent SQL injection through dynamic identifiers
|
|
8
|
+
*/
|
|
9
|
+
export declare function validateIdentifier(identifier: string, context?: string): void;
|
|
10
|
+
/**
|
|
11
|
+
* Validates table reference for dynamic table operations
|
|
12
|
+
*/
|
|
13
|
+
export declare function validateTableReference(tableRef: string): void;
|
|
14
|
+
/**
|
|
15
|
+
* Validates column reference for dynamic column operations
|
|
16
|
+
*/
|
|
17
|
+
export declare function validateColumnReference(columnRef: string): void;
|
|
18
|
+
/**
|
|
19
|
+
* Validates file paths to prevent path traversal attacks
|
|
20
|
+
*/
|
|
21
|
+
export declare function validateFilePath(filePath: string, allowedExtensions?: string[]): void;
|
|
22
|
+
/**
|
|
23
|
+
* Sanitizes database path for CLI commands
|
|
24
|
+
*/
|
|
25
|
+
export declare function sanitizeDatabasePath(dbPath: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Validates output directory for code generation
|
|
28
|
+
*/
|
|
29
|
+
export declare function validateOutputDirectory(dirPath: string): void;
|
|
30
|
+
/**
|
|
31
|
+
* Validates migration name
|
|
32
|
+
*/
|
|
33
|
+
export declare function validateMigrationName(name: string): void;
|
|
34
|
+
/**
|
|
35
|
+
* Rate limiting for security-sensitive operations
|
|
36
|
+
*/
|
|
37
|
+
export declare class RateLimiter {
|
|
38
|
+
private maxAttempts;
|
|
39
|
+
private windowMs;
|
|
40
|
+
private attempts;
|
|
41
|
+
constructor(maxAttempts?: number, windowMs?: number);
|
|
42
|
+
checkLimit(key: string): void;
|
|
43
|
+
private cleanup;
|
|
44
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Security validation utilities for NOORMME
|
|
4
|
+
* Provides input validation and sanitization to prevent injection attacks
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.RateLimiter = void 0;
|
|
8
|
+
exports.validateIdentifier = validateIdentifier;
|
|
9
|
+
exports.validateTableReference = validateTableReference;
|
|
10
|
+
exports.validateColumnReference = validateColumnReference;
|
|
11
|
+
exports.validateFilePath = validateFilePath;
|
|
12
|
+
exports.sanitizeDatabasePath = sanitizeDatabasePath;
|
|
13
|
+
exports.validateOutputDirectory = validateOutputDirectory;
|
|
14
|
+
exports.validateMigrationName = validateMigrationName;
|
|
15
|
+
/**
|
|
16
|
+
* Validates identifier names (table names, column names, schema names)
|
|
17
|
+
* to prevent SQL injection through dynamic identifiers
|
|
18
|
+
*/
|
|
19
|
+
function validateIdentifier(identifier, context = 'identifier') {
|
|
20
|
+
if (typeof identifier !== 'string') {
|
|
21
|
+
throw new Error(`${context} must be a string`);
|
|
22
|
+
}
|
|
23
|
+
if (identifier.length === 0) {
|
|
24
|
+
throw new Error(`${context} cannot be empty`);
|
|
25
|
+
}
|
|
26
|
+
if (identifier.length > 255) {
|
|
27
|
+
throw new Error(`${context} exceeds maximum length of 255 characters`);
|
|
28
|
+
}
|
|
29
|
+
// Check for SQL injection patterns
|
|
30
|
+
const dangerousPatterns = [
|
|
31
|
+
/;/, // SQL statement separator
|
|
32
|
+
/--/, // SQL comment
|
|
33
|
+
/\/\*/, // Multi-line comment start
|
|
34
|
+
/\*\//, // Multi-line comment end
|
|
35
|
+
/\bUNION\b/i, // UNION injection
|
|
36
|
+
/\bSELECT\b.*\bFROM\b/i, // SELECT injection
|
|
37
|
+
/\bINSERT\b.*\bINTO\b/i, // INSERT injection
|
|
38
|
+
/\bUPDATE\b.*\bSET\b/i, // UPDATE injection
|
|
39
|
+
/\bDELETE\b.*\bFROM\b/i, // DELETE injection
|
|
40
|
+
/\bDROP\b/i, // DROP injection
|
|
41
|
+
/\bEXEC\b/i, // EXEC injection
|
|
42
|
+
/\bEXECUTE\b/i, // EXECUTE injection
|
|
43
|
+
/\bCREATE\b/i, // CREATE injection
|
|
44
|
+
/\bALTER\b/i, // ALTER injection
|
|
45
|
+
/\bTRUNCATE\b/i, // TRUNCATE injection
|
|
46
|
+
/['"`]/, // Quote characters
|
|
47
|
+
/\\/, // Backslash escape
|
|
48
|
+
/\x00/, // Null byte
|
|
49
|
+
];
|
|
50
|
+
for (const pattern of dangerousPatterns) {
|
|
51
|
+
if (pattern.test(identifier)) {
|
|
52
|
+
throw new Error(`Invalid ${context}: "${identifier}" contains potentially dangerous characters or SQL keywords. ` +
|
|
53
|
+
`Identifiers must contain only alphanumeric characters, underscores, and dots (for schema.table references).`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Validate format: alphanumeric, underscore, and dots for schema.table.column
|
|
57
|
+
const validIdentifierPattern = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$/;
|
|
58
|
+
if (!validIdentifierPattern.test(identifier)) {
|
|
59
|
+
throw new Error(`Invalid ${context}: "${identifier}". ` +
|
|
60
|
+
`Identifiers must start with a letter or underscore, followed by alphanumeric characters or underscores. ` +
|
|
61
|
+
`Schema/table references can use dots (e.g., "schema.table" or "table.column").`);
|
|
62
|
+
}
|
|
63
|
+
// Additional check: prevent reserved SQLite keywords that could be dangerous
|
|
64
|
+
const reservedKeywords = [
|
|
65
|
+
'PRAGMA', 'ATTACH', 'DETACH', 'VACUUM', 'ANALYZE', 'REINDEX'
|
|
66
|
+
];
|
|
67
|
+
for (const keyword of reservedKeywords) {
|
|
68
|
+
if (identifier.toUpperCase() === keyword) {
|
|
69
|
+
throw new Error(`Invalid ${context}: "${identifier}" is a reserved SQLite keyword and cannot be used as an identifier`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Validates table reference for dynamic table operations
|
|
75
|
+
*/
|
|
76
|
+
function validateTableReference(tableRef) {
|
|
77
|
+
validateIdentifier(tableRef, 'table reference');
|
|
78
|
+
// Additional validation for table references
|
|
79
|
+
const parts = tableRef.split('.');
|
|
80
|
+
if (parts.length > 2) {
|
|
81
|
+
throw new Error(`Invalid table reference: "${tableRef}". ` +
|
|
82
|
+
`Table references can have at most 2 parts (schema.table).`);
|
|
83
|
+
}
|
|
84
|
+
// Validate each part
|
|
85
|
+
parts.forEach((part, index) => {
|
|
86
|
+
const context = index === 0 ? (parts.length === 2 ? 'schema name' : 'table name') : 'table name';
|
|
87
|
+
validateIdentifier(part, context);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Validates column reference for dynamic column operations
|
|
92
|
+
*/
|
|
93
|
+
function validateColumnReference(columnRef) {
|
|
94
|
+
validateIdentifier(columnRef, 'column reference');
|
|
95
|
+
// Column references can have: column, table.column, or schema.table.column
|
|
96
|
+
const parts = columnRef.split('.');
|
|
97
|
+
if (parts.length > 3) {
|
|
98
|
+
throw new Error(`Invalid column reference: "${columnRef}". ` +
|
|
99
|
+
`Column references can have at most 3 parts (schema.table.column).`);
|
|
100
|
+
}
|
|
101
|
+
// Validate each part
|
|
102
|
+
parts.forEach((part, index) => {
|
|
103
|
+
let context;
|
|
104
|
+
if (parts.length === 1) {
|
|
105
|
+
context = 'column name';
|
|
106
|
+
}
|
|
107
|
+
else if (parts.length === 2) {
|
|
108
|
+
context = index === 0 ? 'table name' : 'column name';
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
context = index === 0 ? 'schema name' : (index === 1 ? 'table name' : 'column name');
|
|
112
|
+
}
|
|
113
|
+
validateIdentifier(part, context);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Validates file paths to prevent path traversal attacks
|
|
118
|
+
*/
|
|
119
|
+
function validateFilePath(filePath, allowedExtensions) {
|
|
120
|
+
if (typeof filePath !== 'string') {
|
|
121
|
+
throw new Error('File path must be a string');
|
|
122
|
+
}
|
|
123
|
+
if (filePath.length === 0) {
|
|
124
|
+
throw new Error('File path cannot be empty');
|
|
125
|
+
}
|
|
126
|
+
// Check for path traversal patterns
|
|
127
|
+
const pathTraversalPatterns = [
|
|
128
|
+
/\.\./, // Parent directory reference
|
|
129
|
+
/~\//, // Home directory reference
|
|
130
|
+
/^\/+/, // Absolute path (starts with /)
|
|
131
|
+
/^[a-zA-Z]:\\/, // Windows absolute path (C:\)
|
|
132
|
+
/\\/, // Backslash (Windows path separator)
|
|
133
|
+
/\x00/, // Null byte injection
|
|
134
|
+
];
|
|
135
|
+
for (const pattern of pathTraversalPatterns) {
|
|
136
|
+
if (pattern.test(filePath)) {
|
|
137
|
+
throw new Error(`Invalid file path: "${filePath}" contains path traversal or absolute path patterns. ` +
|
|
138
|
+
`Only relative paths within the current directory are allowed.`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Validate file extension if specified
|
|
142
|
+
if (allowedExtensions && allowedExtensions.length > 0) {
|
|
143
|
+
const hasValidExtension = allowedExtensions.some(ext => filePath.toLowerCase().endsWith(ext.toLowerCase()));
|
|
144
|
+
if (!hasValidExtension) {
|
|
145
|
+
throw new Error(`Invalid file extension for "${filePath}". ` +
|
|
146
|
+
`Allowed extensions: ${allowedExtensions.join(', ')}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Check for suspicious file names
|
|
150
|
+
const suspiciousNames = [
|
|
151
|
+
/^\.+$/, // Only dots
|
|
152
|
+
/^\s*$/, // Only whitespace
|
|
153
|
+
/[<>:"|?*]/, // Windows forbidden characters
|
|
154
|
+
];
|
|
155
|
+
for (const pattern of suspiciousNames) {
|
|
156
|
+
if (pattern.test(filePath)) {
|
|
157
|
+
throw new Error(`Invalid file path: "${filePath}" contains forbidden characters or is malformed`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Sanitizes database path for CLI commands
|
|
163
|
+
*/
|
|
164
|
+
function sanitizeDatabasePath(dbPath) {
|
|
165
|
+
validateFilePath(dbPath, ['.db', '.sqlite', '.sqlite3', '.db3']);
|
|
166
|
+
// Ensure the path doesn't escape current directory
|
|
167
|
+
if (dbPath.includes('..') || dbPath.startsWith('/') || /^[a-zA-Z]:/.test(dbPath)) {
|
|
168
|
+
throw new Error(`Database path "${dbPath}" must be a relative path within the current directory`);
|
|
169
|
+
}
|
|
170
|
+
return dbPath;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Validates output directory for code generation
|
|
174
|
+
*/
|
|
175
|
+
function validateOutputDirectory(dirPath) {
|
|
176
|
+
if (typeof dirPath !== 'string') {
|
|
177
|
+
throw new Error('Output directory must be a string');
|
|
178
|
+
}
|
|
179
|
+
if (dirPath.length === 0) {
|
|
180
|
+
throw new Error('Output directory cannot be empty');
|
|
181
|
+
}
|
|
182
|
+
// Prevent path traversal
|
|
183
|
+
if (dirPath.includes('..')) {
|
|
184
|
+
throw new Error(`Output directory "${dirPath}" cannot contain parent directory references (..)`);
|
|
185
|
+
}
|
|
186
|
+
// Prevent absolute paths
|
|
187
|
+
if (dirPath.startsWith('/') || /^[a-zA-Z]:/.test(dirPath)) {
|
|
188
|
+
throw new Error(`Output directory "${dirPath}" must be a relative path`);
|
|
189
|
+
}
|
|
190
|
+
// Validate format
|
|
191
|
+
const validDirPattern = /^[a-zA-Z0-9_\-./]+$/;
|
|
192
|
+
if (!validDirPattern.test(dirPath)) {
|
|
193
|
+
throw new Error(`Invalid output directory: "${dirPath}". ` +
|
|
194
|
+
`Directory paths must contain only alphanumeric characters, underscores, hyphens, dots, and forward slashes.`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Validates migration name
|
|
199
|
+
*/
|
|
200
|
+
function validateMigrationName(name) {
|
|
201
|
+
if (typeof name !== 'string') {
|
|
202
|
+
throw new Error('Migration name must be a string');
|
|
203
|
+
}
|
|
204
|
+
if (name.length === 0) {
|
|
205
|
+
throw new Error('Migration name cannot be empty');
|
|
206
|
+
}
|
|
207
|
+
if (name.length > 100) {
|
|
208
|
+
throw new Error('Migration name exceeds maximum length of 100 characters');
|
|
209
|
+
}
|
|
210
|
+
// Allow only alphanumeric, underscores, and hyphens
|
|
211
|
+
const validNamePattern = /^[a-zA-Z0-9_\-]+$/;
|
|
212
|
+
if (!validNamePattern.test(name)) {
|
|
213
|
+
throw new Error(`Invalid migration name: "${name}". ` +
|
|
214
|
+
`Migration names must contain only alphanumeric characters, underscores, and hyphens.`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Rate limiting for security-sensitive operations
|
|
219
|
+
*/
|
|
220
|
+
class RateLimiter {
|
|
221
|
+
maxAttempts;
|
|
222
|
+
windowMs;
|
|
223
|
+
attempts = new Map();
|
|
224
|
+
constructor(maxAttempts = 10, windowMs = 60000 // 1 minute
|
|
225
|
+
) {
|
|
226
|
+
this.maxAttempts = maxAttempts;
|
|
227
|
+
this.windowMs = windowMs;
|
|
228
|
+
}
|
|
229
|
+
checkLimit(key) {
|
|
230
|
+
const now = Date.now();
|
|
231
|
+
const attempts = this.attempts.get(key) || [];
|
|
232
|
+
// Remove old attempts outside the window
|
|
233
|
+
const recentAttempts = attempts.filter(time => now - time < this.windowMs);
|
|
234
|
+
if (recentAttempts.length >= this.maxAttempts) {
|
|
235
|
+
throw new Error(`Rate limit exceeded for ${key}. Please wait before trying again.`);
|
|
236
|
+
}
|
|
237
|
+
recentAttempts.push(now);
|
|
238
|
+
this.attempts.set(key, recentAttempts);
|
|
239
|
+
// Cleanup old entries periodically
|
|
240
|
+
if (this.attempts.size > 1000) {
|
|
241
|
+
this.cleanup(now);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
cleanup(now) {
|
|
245
|
+
for (const [key, attempts] of this.attempts.entries()) {
|
|
246
|
+
const recentAttempts = attempts.filter(time => now - time < this.windowMs);
|
|
247
|
+
if (recentAttempts.length === 0) {
|
|
248
|
+
this.attempts.delete(key);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
this.attempts.set(key, recentAttempts);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
exports.RateLimiter = RateLimiter;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security utilities for NOORMME
|
|
3
|
+
* Export all security-related functions for easy import
|
|
4
|
+
*/
|
|
5
|
+
export { validateIdentifier, validateTableReference, validateColumnReference, validateFilePath, sanitizeDatabasePath, validateOutputDirectory, validateMigrationName, RateLimiter, } from './security-validator.js';
|
|
6
|
+
export { safeOrderDirection, safeLimit, safeOffset, safeKeyword, safeLockMode, safeOrderBy, safeBoolean, safeCaseStatement, SafeSQLKeywords, SafeSQLExamples, validateEnum, validateNumericRange, type SafeOrderBy, type SafeCaseWhen, } from './safe-sql-helpers.js';
|
|
7
|
+
/**
|
|
8
|
+
* Security best practices and guidelines
|
|
9
|
+
*/
|
|
10
|
+
export declare const SecurityGuidelines: {
|
|
11
|
+
/**
|
|
12
|
+
* NEVER use sql.raw() or sql.lit() with user input
|
|
13
|
+
*/
|
|
14
|
+
readonly NEVER_USE_RAW_WITH_USER_INPUT: "Use safe alternatives or parameterized queries";
|
|
15
|
+
/**
|
|
16
|
+
* ALWAYS validate dynamic identifiers
|
|
17
|
+
*/
|
|
18
|
+
readonly ALWAYS_VALIDATE_IDENTIFIERS: "Use validateColumnReference() or validateTableReference()";
|
|
19
|
+
/**
|
|
20
|
+
* ALWAYS use whitelists for user-controlled values
|
|
21
|
+
*/
|
|
22
|
+
readonly ALWAYS_USE_WHITELISTS: "Define allowed values and validate against them";
|
|
23
|
+
/**
|
|
24
|
+
* ALWAYS sanitize file paths
|
|
25
|
+
*/
|
|
26
|
+
readonly ALWAYS_SANITIZE_PATHS: "Use validateFilePath() or sanitizeDatabasePath()";
|
|
27
|
+
/**
|
|
28
|
+
* PREFER parameterized queries
|
|
29
|
+
*/
|
|
30
|
+
readonly PREFER_PARAMETERIZED_QUERIES: "Use sql`${value}` instead of sql.raw()";
|
|
31
|
+
/**
|
|
32
|
+
* PREFER safe helpers over raw SQL
|
|
33
|
+
*/
|
|
34
|
+
readonly PREFER_SAFE_HELPERS: "Use safeOrderDirection(), safeLimit(), etc.";
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Quick security checklist for developers
|
|
38
|
+
*/
|
|
39
|
+
export declare function securityChecklist(): string;
|
|
40
|
+
/**
|
|
41
|
+
* Common security patterns
|
|
42
|
+
*/
|
|
43
|
+
export declare const SecurityPatterns: {
|
|
44
|
+
/**
|
|
45
|
+
* Safe column selection from user input
|
|
46
|
+
*/
|
|
47
|
+
safeColumnSelection: (userColumn: string, allowedColumns: string[]) => string;
|
|
48
|
+
/**
|
|
49
|
+
* Safe table selection from user input
|
|
50
|
+
*/
|
|
51
|
+
safeTableSelection: (userTable: string, allowedTables: string[]) => string;
|
|
52
|
+
/**
|
|
53
|
+
* Safe enum validation
|
|
54
|
+
*/
|
|
55
|
+
safeEnumValue: <T extends string>(value: string, allowedValues: readonly T[]) => T;
|
|
56
|
+
/**
|
|
57
|
+
* Safe numeric range validation
|
|
58
|
+
*/
|
|
59
|
+
safeNumericValue: (value: number | string, min: number, max: number) => number;
|
|
60
|
+
};
|