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,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,246 @@
|
|
|
1
|
+
/// <reference types="./security-validator.d.ts" />
|
|
2
|
+
/**
|
|
3
|
+
* Security validation utilities for NOORMME
|
|
4
|
+
* Provides input validation and sanitization to prevent injection attacks
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Validates identifier names (table names, column names, schema names)
|
|
8
|
+
* to prevent SQL injection through dynamic identifiers
|
|
9
|
+
*/
|
|
10
|
+
export function validateIdentifier(identifier, context = 'identifier') {
|
|
11
|
+
if (typeof identifier !== 'string') {
|
|
12
|
+
throw new Error(`${context} must be a string`);
|
|
13
|
+
}
|
|
14
|
+
if (identifier.length === 0) {
|
|
15
|
+
throw new Error(`${context} cannot be empty`);
|
|
16
|
+
}
|
|
17
|
+
if (identifier.length > 255) {
|
|
18
|
+
throw new Error(`${context} exceeds maximum length of 255 characters`);
|
|
19
|
+
}
|
|
20
|
+
// Check for SQL injection patterns
|
|
21
|
+
const dangerousPatterns = [
|
|
22
|
+
/;/, // SQL statement separator
|
|
23
|
+
/--/, // SQL comment
|
|
24
|
+
/\/\*/, // Multi-line comment start
|
|
25
|
+
/\*\//, // Multi-line comment end
|
|
26
|
+
/\bUNION\b/i, // UNION injection
|
|
27
|
+
/\bSELECT\b.*\bFROM\b/i, // SELECT injection
|
|
28
|
+
/\bINSERT\b.*\bINTO\b/i, // INSERT injection
|
|
29
|
+
/\bUPDATE\b.*\bSET\b/i, // UPDATE injection
|
|
30
|
+
/\bDELETE\b.*\bFROM\b/i, // DELETE injection
|
|
31
|
+
/\bDROP\b/i, // DROP injection
|
|
32
|
+
/\bEXEC\b/i, // EXEC injection
|
|
33
|
+
/\bEXECUTE\b/i, // EXECUTE injection
|
|
34
|
+
/\bCREATE\b/i, // CREATE injection
|
|
35
|
+
/\bALTER\b/i, // ALTER injection
|
|
36
|
+
/\bTRUNCATE\b/i, // TRUNCATE injection
|
|
37
|
+
/['"`]/, // Quote characters
|
|
38
|
+
/\\/, // Backslash escape
|
|
39
|
+
/\x00/, // Null byte
|
|
40
|
+
];
|
|
41
|
+
for (const pattern of dangerousPatterns) {
|
|
42
|
+
if (pattern.test(identifier)) {
|
|
43
|
+
throw new Error(`Invalid ${context}: "${identifier}" contains potentially dangerous characters or SQL keywords. ` +
|
|
44
|
+
`Identifiers must contain only alphanumeric characters, underscores, and dots (for schema.table references).`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Validate format: alphanumeric, underscore, and dots for schema.table.column
|
|
48
|
+
const validIdentifierPattern = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$/;
|
|
49
|
+
if (!validIdentifierPattern.test(identifier)) {
|
|
50
|
+
throw new Error(`Invalid ${context}: "${identifier}". ` +
|
|
51
|
+
`Identifiers must start with a letter or underscore, followed by alphanumeric characters or underscores. ` +
|
|
52
|
+
`Schema/table references can use dots (e.g., "schema.table" or "table.column").`);
|
|
53
|
+
}
|
|
54
|
+
// Additional check: prevent reserved SQLite keywords that could be dangerous
|
|
55
|
+
const reservedKeywords = [
|
|
56
|
+
'PRAGMA', 'ATTACH', 'DETACH', 'VACUUM', 'ANALYZE', 'REINDEX'
|
|
57
|
+
];
|
|
58
|
+
for (const keyword of reservedKeywords) {
|
|
59
|
+
if (identifier.toUpperCase() === keyword) {
|
|
60
|
+
throw new Error(`Invalid ${context}: "${identifier}" is a reserved SQLite keyword and cannot be used as an identifier`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Validates table reference for dynamic table operations
|
|
66
|
+
*/
|
|
67
|
+
export function validateTableReference(tableRef) {
|
|
68
|
+
validateIdentifier(tableRef, 'table reference');
|
|
69
|
+
// Additional validation for table references
|
|
70
|
+
const parts = tableRef.split('.');
|
|
71
|
+
if (parts.length > 2) {
|
|
72
|
+
throw new Error(`Invalid table reference: "${tableRef}". ` +
|
|
73
|
+
`Table references can have at most 2 parts (schema.table).`);
|
|
74
|
+
}
|
|
75
|
+
// Validate each part
|
|
76
|
+
parts.forEach((part, index) => {
|
|
77
|
+
const context = index === 0 ? (parts.length === 2 ? 'schema name' : 'table name') : 'table name';
|
|
78
|
+
validateIdentifier(part, context);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Validates column reference for dynamic column operations
|
|
83
|
+
*/
|
|
84
|
+
export function validateColumnReference(columnRef) {
|
|
85
|
+
validateIdentifier(columnRef, 'column reference');
|
|
86
|
+
// Column references can have: column, table.column, or schema.table.column
|
|
87
|
+
const parts = columnRef.split('.');
|
|
88
|
+
if (parts.length > 3) {
|
|
89
|
+
throw new Error(`Invalid column reference: "${columnRef}". ` +
|
|
90
|
+
`Column references can have at most 3 parts (schema.table.column).`);
|
|
91
|
+
}
|
|
92
|
+
// Validate each part
|
|
93
|
+
parts.forEach((part, index) => {
|
|
94
|
+
let context;
|
|
95
|
+
if (parts.length === 1) {
|
|
96
|
+
context = 'column name';
|
|
97
|
+
}
|
|
98
|
+
else if (parts.length === 2) {
|
|
99
|
+
context = index === 0 ? 'table name' : 'column name';
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
context = index === 0 ? 'schema name' : (index === 1 ? 'table name' : 'column name');
|
|
103
|
+
}
|
|
104
|
+
validateIdentifier(part, context);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Validates file paths to prevent path traversal attacks
|
|
109
|
+
*/
|
|
110
|
+
export function validateFilePath(filePath, allowedExtensions) {
|
|
111
|
+
if (typeof filePath !== 'string') {
|
|
112
|
+
throw new Error('File path must be a string');
|
|
113
|
+
}
|
|
114
|
+
if (filePath.length === 0) {
|
|
115
|
+
throw new Error('File path cannot be empty');
|
|
116
|
+
}
|
|
117
|
+
// Check for path traversal patterns
|
|
118
|
+
const pathTraversalPatterns = [
|
|
119
|
+
/\.\./, // Parent directory reference
|
|
120
|
+
/~\//, // Home directory reference
|
|
121
|
+
/^\/+/, // Absolute path (starts with /)
|
|
122
|
+
/^[a-zA-Z]:\\/, // Windows absolute path (C:\)
|
|
123
|
+
/\\/, // Backslash (Windows path separator)
|
|
124
|
+
/\x00/, // Null byte injection
|
|
125
|
+
];
|
|
126
|
+
for (const pattern of pathTraversalPatterns) {
|
|
127
|
+
if (pattern.test(filePath)) {
|
|
128
|
+
throw new Error(`Invalid file path: "${filePath}" contains path traversal or absolute path patterns. ` +
|
|
129
|
+
`Only relative paths within the current directory are allowed.`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Validate file extension if specified
|
|
133
|
+
if (allowedExtensions && allowedExtensions.length > 0) {
|
|
134
|
+
const hasValidExtension = allowedExtensions.some(ext => filePath.toLowerCase().endsWith(ext.toLowerCase()));
|
|
135
|
+
if (!hasValidExtension) {
|
|
136
|
+
throw new Error(`Invalid file extension for "${filePath}". ` +
|
|
137
|
+
`Allowed extensions: ${allowedExtensions.join(', ')}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Check for suspicious file names
|
|
141
|
+
const suspiciousNames = [
|
|
142
|
+
/^\.+$/, // Only dots
|
|
143
|
+
/^\s*$/, // Only whitespace
|
|
144
|
+
/[<>:"|?*]/, // Windows forbidden characters
|
|
145
|
+
];
|
|
146
|
+
for (const pattern of suspiciousNames) {
|
|
147
|
+
if (pattern.test(filePath)) {
|
|
148
|
+
throw new Error(`Invalid file path: "${filePath}" contains forbidden characters or is malformed`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Sanitizes database path for CLI commands
|
|
154
|
+
*/
|
|
155
|
+
export function sanitizeDatabasePath(dbPath) {
|
|
156
|
+
validateFilePath(dbPath, ['.db', '.sqlite', '.sqlite3', '.db3']);
|
|
157
|
+
// Ensure the path doesn't escape current directory
|
|
158
|
+
if (dbPath.includes('..') || dbPath.startsWith('/') || /^[a-zA-Z]:/.test(dbPath)) {
|
|
159
|
+
throw new Error(`Database path "${dbPath}" must be a relative path within the current directory`);
|
|
160
|
+
}
|
|
161
|
+
return dbPath;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Validates output directory for code generation
|
|
165
|
+
*/
|
|
166
|
+
export function validateOutputDirectory(dirPath) {
|
|
167
|
+
if (typeof dirPath !== 'string') {
|
|
168
|
+
throw new Error('Output directory must be a string');
|
|
169
|
+
}
|
|
170
|
+
if (dirPath.length === 0) {
|
|
171
|
+
throw new Error('Output directory cannot be empty');
|
|
172
|
+
}
|
|
173
|
+
// Prevent path traversal
|
|
174
|
+
if (dirPath.includes('..')) {
|
|
175
|
+
throw new Error(`Output directory "${dirPath}" cannot contain parent directory references (..)`);
|
|
176
|
+
}
|
|
177
|
+
// Prevent absolute paths
|
|
178
|
+
if (dirPath.startsWith('/') || /^[a-zA-Z]:/.test(dirPath)) {
|
|
179
|
+
throw new Error(`Output directory "${dirPath}" must be a relative path`);
|
|
180
|
+
}
|
|
181
|
+
// Validate format
|
|
182
|
+
const validDirPattern = /^[a-zA-Z0-9_\-./]+$/;
|
|
183
|
+
if (!validDirPattern.test(dirPath)) {
|
|
184
|
+
throw new Error(`Invalid output directory: "${dirPath}". ` +
|
|
185
|
+
`Directory paths must contain only alphanumeric characters, underscores, hyphens, dots, and forward slashes.`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Validates migration name
|
|
190
|
+
*/
|
|
191
|
+
export function validateMigrationName(name) {
|
|
192
|
+
if (typeof name !== 'string') {
|
|
193
|
+
throw new Error('Migration name must be a string');
|
|
194
|
+
}
|
|
195
|
+
if (name.length === 0) {
|
|
196
|
+
throw new Error('Migration name cannot be empty');
|
|
197
|
+
}
|
|
198
|
+
if (name.length > 100) {
|
|
199
|
+
throw new Error('Migration name exceeds maximum length of 100 characters');
|
|
200
|
+
}
|
|
201
|
+
// Allow only alphanumeric, underscores, and hyphens
|
|
202
|
+
const validNamePattern = /^[a-zA-Z0-9_\-]+$/;
|
|
203
|
+
if (!validNamePattern.test(name)) {
|
|
204
|
+
throw new Error(`Invalid migration name: "${name}". ` +
|
|
205
|
+
`Migration names must contain only alphanumeric characters, underscores, and hyphens.`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Rate limiting for security-sensitive operations
|
|
210
|
+
*/
|
|
211
|
+
export class RateLimiter {
|
|
212
|
+
maxAttempts;
|
|
213
|
+
windowMs;
|
|
214
|
+
attempts = new Map();
|
|
215
|
+
constructor(maxAttempts = 10, windowMs = 60000 // 1 minute
|
|
216
|
+
) {
|
|
217
|
+
this.maxAttempts = maxAttempts;
|
|
218
|
+
this.windowMs = windowMs;
|
|
219
|
+
}
|
|
220
|
+
checkLimit(key) {
|
|
221
|
+
const now = Date.now();
|
|
222
|
+
const attempts = this.attempts.get(key) || [];
|
|
223
|
+
// Remove old attempts outside the window
|
|
224
|
+
const recentAttempts = attempts.filter(time => now - time < this.windowMs);
|
|
225
|
+
if (recentAttempts.length >= this.maxAttempts) {
|
|
226
|
+
throw new Error(`Rate limit exceeded for ${key}. Please wait before trying again.`);
|
|
227
|
+
}
|
|
228
|
+
recentAttempts.push(now);
|
|
229
|
+
this.attempts.set(key, recentAttempts);
|
|
230
|
+
// Cleanup old entries periodically
|
|
231
|
+
if (this.attempts.size > 1000) {
|
|
232
|
+
this.cleanup(now);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
cleanup(now) {
|
|
236
|
+
for (const [key, attempts] of this.attempts.entries()) {
|
|
237
|
+
const recentAttempts = attempts.filter(time => now - time < this.windowMs);
|
|
238
|
+
if (recentAttempts.length === 0) {
|
|
239
|
+
this.attempts.delete(key);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
this.attempts.set(key, recentAttempts);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/// <reference types="./security.d.ts" />
|
|
2
|
+
/**
|
|
3
|
+
* Security utilities for NOORMME
|
|
4
|
+
* Export all security-related functions for easy import
|
|
5
|
+
*/
|
|
6
|
+
// Input validation and sanitization
|
|
7
|
+
export { validateIdentifier, validateTableReference, validateColumnReference, validateFilePath, sanitizeDatabasePath, validateOutputDirectory, validateMigrationName, RateLimiter, } from './security-validator.js';
|
|
8
|
+
// Safe SQL helpers
|
|
9
|
+
export { safeOrderDirection, safeLimit, safeOffset, safeKeyword, safeLockMode, safeOrderBy, safeBoolean, safeCaseStatement, SafeSQLKeywords, SafeSQLExamples, validateEnum, validateNumericRange, } from './safe-sql-helpers.js';
|
|
10
|
+
/**
|
|
11
|
+
* Security best practices and guidelines
|
|
12
|
+
*/
|
|
13
|
+
export const SecurityGuidelines = {
|
|
14
|
+
/**
|
|
15
|
+
* NEVER use sql.raw() or sql.lit() with user input
|
|
16
|
+
*/
|
|
17
|
+
NEVER_USE_RAW_WITH_USER_INPUT: 'Use safe alternatives or parameterized queries',
|
|
18
|
+
/**
|
|
19
|
+
* ALWAYS validate dynamic identifiers
|
|
20
|
+
*/
|
|
21
|
+
ALWAYS_VALIDATE_IDENTIFIERS: 'Use validateColumnReference() or validateTableReference()',
|
|
22
|
+
/**
|
|
23
|
+
* ALWAYS use whitelists for user-controlled values
|
|
24
|
+
*/
|
|
25
|
+
ALWAYS_USE_WHITELISTS: 'Define allowed values and validate against them',
|
|
26
|
+
/**
|
|
27
|
+
* ALWAYS sanitize file paths
|
|
28
|
+
*/
|
|
29
|
+
ALWAYS_SANITIZE_PATHS: 'Use validateFilePath() or sanitizeDatabasePath()',
|
|
30
|
+
/**
|
|
31
|
+
* PREFER parameterized queries
|
|
32
|
+
*/
|
|
33
|
+
PREFER_PARAMETERIZED_QUERIES: 'Use sql`${value}` instead of sql.raw()',
|
|
34
|
+
/**
|
|
35
|
+
* PREFER safe helpers over raw SQL
|
|
36
|
+
*/
|
|
37
|
+
PREFER_SAFE_HELPERS: 'Use safeOrderDirection(), safeLimit(), etc.',
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Quick security checklist for developers
|
|
41
|
+
*/
|
|
42
|
+
export function securityChecklist() {
|
|
43
|
+
return `
|
|
44
|
+
NOORMME Security Checklist:
|
|
45
|
+
|
|
46
|
+
✅ Input Validation
|
|
47
|
+
□ All user inputs validated with whitelists
|
|
48
|
+
□ Dynamic columns/tables use validateColumnReference()/validateTableReference()
|
|
49
|
+
□ File paths use validateFilePath() or sanitizeDatabasePath()
|
|
50
|
+
|
|
51
|
+
✅ SQL Injection Prevention
|
|
52
|
+
□ No sql.raw() with user input
|
|
53
|
+
□ No sql.lit() with user input
|
|
54
|
+
□ Use safe alternatives: safeOrderDirection(), safeLimit(), etc.
|
|
55
|
+
□ Parameterized queries used for all values
|
|
56
|
+
|
|
57
|
+
✅ Path Traversal Prevention
|
|
58
|
+
□ All file operations validated
|
|
59
|
+
□ No parent directory references (..) allowed
|
|
60
|
+
□ No absolute paths accepted
|
|
61
|
+
|
|
62
|
+
✅ Production Readiness
|
|
63
|
+
□ Environment variables for sensitive config
|
|
64
|
+
□ Error messages don't leak sensitive info
|
|
65
|
+
□ Rate limiting enabled for auth endpoints
|
|
66
|
+
□ Database file permissions set (600/640)
|
|
67
|
+
□ Dependencies updated regularly
|
|
68
|
+
□ Backups encrypted and secure
|
|
69
|
+
|
|
70
|
+
Use this checklist before deploying to production!
|
|
71
|
+
`;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Common security patterns
|
|
75
|
+
*/
|
|
76
|
+
export const SecurityPatterns = {
|
|
77
|
+
/**
|
|
78
|
+
* Safe column selection from user input
|
|
79
|
+
*/
|
|
80
|
+
safeColumnSelection: (userColumn, allowedColumns) => {
|
|
81
|
+
if (!allowedColumns.includes(userColumn)) {
|
|
82
|
+
throw new Error(`Invalid column: ${userColumn}`);
|
|
83
|
+
}
|
|
84
|
+
return userColumn;
|
|
85
|
+
},
|
|
86
|
+
/**
|
|
87
|
+
* Safe table selection from user input
|
|
88
|
+
*/
|
|
89
|
+
safeTableSelection: (userTable, allowedTables) => {
|
|
90
|
+
if (!allowedTables.includes(userTable)) {
|
|
91
|
+
throw new Error(`Invalid table: ${userTable}`);
|
|
92
|
+
}
|
|
93
|
+
return userTable;
|
|
94
|
+
},
|
|
95
|
+
/**
|
|
96
|
+
* Safe enum validation
|
|
97
|
+
*/
|
|
98
|
+
safeEnumValue: (value, allowedValues) => {
|
|
99
|
+
if (!allowedValues.includes(value)) {
|
|
100
|
+
throw new Error(`Invalid value: ${value}. Allowed: ${allowedValues.join(', ')}`);
|
|
101
|
+
}
|
|
102
|
+
return value;
|
|
103
|
+
},
|
|
104
|
+
/**
|
|
105
|
+
* Safe numeric range validation
|
|
106
|
+
*/
|
|
107
|
+
safeNumericValue: (value, min, max) => {
|
|
108
|
+
const num = typeof value === 'string' ? parseFloat(value) : value;
|
|
109
|
+
if (isNaN(num) || num < min || num > max) {
|
|
110
|
+
throw new Error(`Invalid number: ${value}. Must be between ${min} and ${max}`);
|
|
111
|
+
}
|
|
112
|
+
return num;
|
|
113
|
+
},
|
|
114
|
+
};
|
|
@@ -16,11 +16,13 @@ export class SchemaWatcher {
|
|
|
16
16
|
this.schemaDiscovery = schemaDiscovery;
|
|
17
17
|
this.logger = logger;
|
|
18
18
|
this.options = options;
|
|
19
|
+
// Merge options, giving priority to explicitly passed values
|
|
20
|
+
const defaultEnabled = process.env.NODE_ENV === 'development';
|
|
19
21
|
this.options = {
|
|
20
22
|
pollInterval: 5000,
|
|
21
23
|
ignoreViews: true,
|
|
22
24
|
ignoredTables: [],
|
|
23
|
-
enabled:
|
|
25
|
+
enabled: options.enabled !== undefined ? options.enabled : defaultEnabled,
|
|
24
26
|
...options
|
|
25
27
|
};
|
|
26
28
|
}
|
|
@@ -38,29 +40,40 @@ export class SchemaWatcher {
|
|
|
38
40
|
}
|
|
39
41
|
this.logger.info('Starting schema change monitoring...');
|
|
40
42
|
// Get initial schema snapshot
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
try {
|
|
44
|
+
const initialSchema = await this.getCurrentSchema();
|
|
45
|
+
this.lastSchemaHash = this.hashSchema(initialSchema);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
this.logger.error('Failed to get initial schema snapshot:', error);
|
|
49
|
+
// Set a default hash to allow watching to continue
|
|
50
|
+
this.lastSchemaHash = '0';
|
|
51
|
+
}
|
|
43
52
|
this.isWatching = true;
|
|
44
53
|
this.intervalId = setInterval(() => {
|
|
45
54
|
this.checkForChanges().catch(error => {
|
|
46
55
|
this.logger.error('Error checking for schema changes:', error);
|
|
47
56
|
});
|
|
48
57
|
}, this.options.pollInterval);
|
|
58
|
+
// Unref the interval to allow the process to exit
|
|
59
|
+
if (this.intervalId && typeof this.intervalId.unref === 'function') {
|
|
60
|
+
this.intervalId.unref();
|
|
61
|
+
}
|
|
49
62
|
this.logger.info(`Schema watcher started (polling every ${this.options.pollInterval}ms)`);
|
|
50
63
|
}
|
|
51
64
|
/**
|
|
52
65
|
* Stop watching for schema changes
|
|
53
66
|
*/
|
|
54
67
|
stopWatching() {
|
|
55
|
-
if (!this.isWatching) {
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
68
|
if (this.intervalId) {
|
|
59
69
|
clearInterval(this.intervalId);
|
|
60
70
|
this.intervalId = null;
|
|
61
71
|
}
|
|
62
72
|
this.isWatching = false;
|
|
63
|
-
this.
|
|
73
|
+
this.callbacks = []; // Clear callbacks on stop
|
|
74
|
+
if (this.logger) {
|
|
75
|
+
this.logger.info('Schema watcher stopped');
|
|
76
|
+
}
|
|
64
77
|
}
|
|
65
78
|
/**
|
|
66
79
|
* Register callback for schema changes
|
|
@@ -84,6 +97,8 @@ export class SchemaWatcher {
|
|
|
84
97
|
try {
|
|
85
98
|
const currentSchema = await this.getCurrentSchema();
|
|
86
99
|
const currentHash = this.hashSchema(currentSchema);
|
|
100
|
+
// Debug logging
|
|
101
|
+
this.logger.debug(`Schema check - Last hash: ${this.lastSchemaHash}, Current hash: ${currentHash}, Tables: ${currentSchema.tables.length}`);
|
|
87
102
|
if (this.lastSchemaHash && currentHash !== this.lastSchemaHash) {
|
|
88
103
|
this.logger.info('Schema changes detected, analyzing...');
|
|
89
104
|
// Since we only have the hash, we'll need to do a full schema comparison
|
|
@@ -96,6 +111,10 @@ export class SchemaWatcher {
|
|
|
96
111
|
}
|
|
97
112
|
return changes;
|
|
98
113
|
}
|
|
114
|
+
// Update hash even if no changes detected yet (for first check after initialization)
|
|
115
|
+
if (!this.lastSchemaHash) {
|
|
116
|
+
this.lastSchemaHash = currentHash;
|
|
117
|
+
}
|
|
99
118
|
return [];
|
|
100
119
|
}
|
|
101
120
|
catch (error) {
|
package/package.json
CHANGED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import type { Kysely } from '../kysely.js';
|
|
2
|
-
import { Logger } from '../logging/logger.js';
|
|
3
|
-
import { SchemaInfo } from '../types/index.js';
|
|
4
|
-
export interface QueryMetrics {
|
|
5
|
-
query: string;
|
|
6
|
-
executionTime: number;
|
|
7
|
-
timestamp: Date;
|
|
8
|
-
table?: string;
|
|
9
|
-
operation?: string;
|
|
10
|
-
}
|
|
11
|
-
export interface PerformanceWarning {
|
|
12
|
-
type: 'n_plus_one' | 'missing_index' | 'slow_query' | 'full_table_scan' | 'large_result_set';
|
|
13
|
-
message: string;
|
|
14
|
-
suggestion: string;
|
|
15
|
-
query?: string;
|
|
16
|
-
table?: string;
|
|
17
|
-
severity: 'low' | 'medium' | 'high';
|
|
18
|
-
}
|
|
19
|
-
export interface QueryAnalyzerOptions {
|
|
20
|
-
enabled?: boolean;
|
|
21
|
-
slowQueryThreshold?: number;
|
|
22
|
-
nPlusOneDetection?: boolean;
|
|
23
|
-
missingIndexDetection?: boolean;
|
|
24
|
-
largeResultSetThreshold?: number;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Analyzes query performance and provides warnings for optimization
|
|
28
|
-
*/
|
|
29
|
-
export declare class QueryAnalyzer {
|
|
30
|
-
private db;
|
|
31
|
-
private logger;
|
|
32
|
-
private schemaInfo;
|
|
33
|
-
private options;
|
|
34
|
-
private queryHistory;
|
|
35
|
-
private recentQueries;
|
|
36
|
-
private maxHistorySize;
|
|
37
|
-
constructor(db: Kysely<any>, logger: Logger, schemaInfo: SchemaInfo, options?: QueryAnalyzerOptions);
|
|
38
|
-
/**
|
|
39
|
-
* Record a query execution for analysis
|
|
40
|
-
*/
|
|
41
|
-
recordQuery(query: string, executionTime: number, resultCount?: number, table?: string): void;
|
|
42
|
-
/**
|
|
43
|
-
* Analyze a query for performance issues
|
|
44
|
-
*/
|
|
45
|
-
private analyzeQuery;
|
|
46
|
-
/**
|
|
47
|
-
* Detect N+1 query patterns
|
|
48
|
-
*/
|
|
49
|
-
private detectNPlusOne;
|
|
50
|
-
/**
|
|
51
|
-
* Check for missing indexes based on WHERE clauses
|
|
52
|
-
*/
|
|
53
|
-
private checkForMissingIndexes;
|
|
54
|
-
/**
|
|
55
|
-
* Normalize query for comparison (remove dynamic values)
|
|
56
|
-
*/
|
|
57
|
-
private normalizeQuery;
|
|
58
|
-
/**
|
|
59
|
-
* Extract operation type from query
|
|
60
|
-
*/
|
|
61
|
-
private extractOperation;
|
|
62
|
-
/**
|
|
63
|
-
* Log performance warning
|
|
64
|
-
*/
|
|
65
|
-
private logWarning;
|
|
66
|
-
/**
|
|
67
|
-
* Get emoji for warning type
|
|
68
|
-
*/
|
|
69
|
-
private getWarningEmoji;
|
|
70
|
-
/**
|
|
71
|
-
* Get performance statistics
|
|
72
|
-
*/
|
|
73
|
-
getPerformanceStats(): {
|
|
74
|
-
totalQueries: number;
|
|
75
|
-
averageExecutionTime: number;
|
|
76
|
-
slowQueries: number;
|
|
77
|
-
warningCount: {
|
|
78
|
-
[key: string]: number;
|
|
79
|
-
};
|
|
80
|
-
};
|
|
81
|
-
/**
|
|
82
|
-
* Clear query history
|
|
83
|
-
*/
|
|
84
|
-
clearHistory(): void;
|
|
85
|
-
/**
|
|
86
|
-
* Update schema info for index checking
|
|
87
|
-
*/
|
|
88
|
-
updateSchema(schemaInfo: SchemaInfo): void;
|
|
89
|
-
}
|