servcraft 0.1.0 → 0.1.3
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/.claude/settings.local.json +30 -0
- package/.github/CODEOWNERS +18 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
- package/.github/dependabot.yml +59 -0
- package/.github/workflows/ci.yml +188 -0
- package/.github/workflows/release.yml +195 -0
- package/AUDIT.md +602 -0
- package/LICENSE +21 -0
- package/README.md +1102 -1
- package/dist/cli/index.cjs +2026 -2168
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +2026 -2168
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +595 -616
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -52
- package/dist/index.d.ts +114 -52
- package/dist/index.js +595 -616
- package/dist/index.js.map +1 -1
- package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
- package/docs/DATABASE_MULTI_ORM.md +399 -0
- package/docs/PHASE1_BREAKDOWN.md +346 -0
- package/docs/PROGRESS.md +550 -0
- package/docs/modules/ANALYTICS.md +226 -0
- package/docs/modules/API-VERSIONING.md +252 -0
- package/docs/modules/AUDIT.md +192 -0
- package/docs/modules/AUTH.md +431 -0
- package/docs/modules/CACHE.md +346 -0
- package/docs/modules/EMAIL.md +254 -0
- package/docs/modules/FEATURE-FLAG.md +291 -0
- package/docs/modules/I18N.md +294 -0
- package/docs/modules/MEDIA-PROCESSING.md +281 -0
- package/docs/modules/MFA.md +266 -0
- package/docs/modules/NOTIFICATION.md +311 -0
- package/docs/modules/OAUTH.md +237 -0
- package/docs/modules/PAYMENT.md +804 -0
- package/docs/modules/QUEUE.md +540 -0
- package/docs/modules/RATE-LIMIT.md +339 -0
- package/docs/modules/SEARCH.md +288 -0
- package/docs/modules/SECURITY.md +327 -0
- package/docs/modules/SESSION.md +382 -0
- package/docs/modules/SWAGGER.md +305 -0
- package/docs/modules/UPLOAD.md +296 -0
- package/docs/modules/USER.md +505 -0
- package/docs/modules/VALIDATION.md +294 -0
- package/docs/modules/WEBHOOK.md +270 -0
- package/docs/modules/WEBSOCKET.md +691 -0
- package/package.json +53 -38
- package/prisma/schema.prisma +395 -1
- package/src/cli/commands/add-module.ts +520 -87
- package/src/cli/commands/db.ts +3 -4
- package/src/cli/commands/docs.ts +256 -6
- package/src/cli/commands/generate.ts +12 -19
- package/src/cli/commands/init.ts +384 -214
- package/src/cli/index.ts +0 -4
- package/src/cli/templates/repository.ts +6 -1
- package/src/cli/templates/routes.ts +6 -21
- package/src/cli/utils/docs-generator.ts +6 -7
- package/src/cli/utils/env-manager.ts +717 -0
- package/src/cli/utils/field-parser.ts +16 -7
- package/src/cli/utils/interactive-prompt.ts +223 -0
- package/src/cli/utils/template-manager.ts +346 -0
- package/src/config/database.config.ts +183 -0
- package/src/config/env.ts +0 -10
- package/src/config/index.ts +0 -14
- package/src/core/server.ts +1 -1
- package/src/database/adapters/mongoose.adapter.ts +132 -0
- package/src/database/adapters/prisma.adapter.ts +118 -0
- package/src/database/connection.ts +190 -0
- package/src/database/interfaces/database.interface.ts +85 -0
- package/src/database/interfaces/index.ts +7 -0
- package/src/database/interfaces/repository.interface.ts +129 -0
- package/src/database/models/mongoose/index.ts +7 -0
- package/src/database/models/mongoose/payment.schema.ts +347 -0
- package/src/database/models/mongoose/user.schema.ts +154 -0
- package/src/database/prisma.ts +1 -4
- package/src/database/redis.ts +101 -0
- package/src/database/repositories/mongoose/index.ts +7 -0
- package/src/database/repositories/mongoose/payment.repository.ts +380 -0
- package/src/database/repositories/mongoose/user.repository.ts +255 -0
- package/src/database/seed.ts +6 -1
- package/src/index.ts +9 -20
- package/src/middleware/security.ts +2 -6
- package/src/modules/analytics/analytics.routes.ts +80 -0
- package/src/modules/analytics/analytics.service.ts +364 -0
- package/src/modules/analytics/index.ts +18 -0
- package/src/modules/analytics/types.ts +180 -0
- package/src/modules/api-versioning/index.ts +15 -0
- package/src/modules/api-versioning/types.ts +86 -0
- package/src/modules/api-versioning/versioning.middleware.ts +120 -0
- package/src/modules/api-versioning/versioning.routes.ts +54 -0
- package/src/modules/api-versioning/versioning.service.ts +189 -0
- package/src/modules/audit/audit.repository.ts +206 -0
- package/src/modules/audit/audit.service.ts +27 -59
- package/src/modules/auth/auth.controller.ts +2 -2
- package/src/modules/auth/auth.middleware.ts +3 -9
- package/src/modules/auth/auth.routes.ts +10 -107
- package/src/modules/auth/auth.service.ts +126 -23
- package/src/modules/auth/index.ts +3 -4
- package/src/modules/cache/cache.service.ts +367 -0
- package/src/modules/cache/index.ts +10 -0
- package/src/modules/cache/types.ts +44 -0
- package/src/modules/email/email.service.ts +3 -10
- package/src/modules/email/templates.ts +2 -8
- package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
- package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
- package/src/modules/feature-flag/feature-flag.service.ts +566 -0
- package/src/modules/feature-flag/index.ts +20 -0
- package/src/modules/feature-flag/types.ts +192 -0
- package/src/modules/i18n/i18n.middleware.ts +186 -0
- package/src/modules/i18n/i18n.routes.ts +191 -0
- package/src/modules/i18n/i18n.service.ts +456 -0
- package/src/modules/i18n/index.ts +18 -0
- package/src/modules/i18n/types.ts +118 -0
- package/src/modules/media-processing/index.ts +17 -0
- package/src/modules/media-processing/media-processing.routes.ts +111 -0
- package/src/modules/media-processing/media-processing.service.ts +245 -0
- package/src/modules/media-processing/types.ts +156 -0
- package/src/modules/mfa/index.ts +20 -0
- package/src/modules/mfa/mfa.repository.ts +206 -0
- package/src/modules/mfa/mfa.routes.ts +595 -0
- package/src/modules/mfa/mfa.service.ts +572 -0
- package/src/modules/mfa/totp.ts +150 -0
- package/src/modules/mfa/types.ts +57 -0
- package/src/modules/notification/index.ts +20 -0
- package/src/modules/notification/notification.repository.ts +356 -0
- package/src/modules/notification/notification.service.ts +483 -0
- package/src/modules/notification/types.ts +119 -0
- package/src/modules/oauth/index.ts +20 -0
- package/src/modules/oauth/oauth.repository.ts +219 -0
- package/src/modules/oauth/oauth.routes.ts +446 -0
- package/src/modules/oauth/oauth.service.ts +293 -0
- package/src/modules/oauth/providers/apple.provider.ts +250 -0
- package/src/modules/oauth/providers/facebook.provider.ts +181 -0
- package/src/modules/oauth/providers/github.provider.ts +248 -0
- package/src/modules/oauth/providers/google.provider.ts +189 -0
- package/src/modules/oauth/providers/twitter.provider.ts +214 -0
- package/src/modules/oauth/types.ts +94 -0
- package/src/modules/payment/index.ts +19 -0
- package/src/modules/payment/payment.repository.ts +733 -0
- package/src/modules/payment/payment.routes.ts +390 -0
- package/src/modules/payment/payment.service.ts +354 -0
- package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
- package/src/modules/payment/providers/paypal.provider.ts +190 -0
- package/src/modules/payment/providers/stripe.provider.ts +215 -0
- package/src/modules/payment/types.ts +140 -0
- package/src/modules/queue/cron.ts +438 -0
- package/src/modules/queue/index.ts +87 -0
- package/src/modules/queue/queue.routes.ts +600 -0
- package/src/modules/queue/queue.service.ts +842 -0
- package/src/modules/queue/types.ts +222 -0
- package/src/modules/queue/workers.ts +366 -0
- package/src/modules/rate-limit/index.ts +59 -0
- package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
- package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
- package/src/modules/rate-limit/rate-limit.service.ts +348 -0
- package/src/modules/rate-limit/stores/memory.store.ts +165 -0
- package/src/modules/rate-limit/stores/redis.store.ts +322 -0
- package/src/modules/rate-limit/types.ts +153 -0
- package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
- package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
- package/src/modules/search/adapters/memory.adapter.ts +278 -0
- package/src/modules/search/index.ts +21 -0
- package/src/modules/search/search.service.ts +234 -0
- package/src/modules/search/types.ts +214 -0
- package/src/modules/security/index.ts +40 -0
- package/src/modules/security/sanitize.ts +223 -0
- package/src/modules/security/security-audit.service.ts +388 -0
- package/src/modules/security/security.middleware.ts +398 -0
- package/src/modules/session/index.ts +3 -0
- package/src/modules/session/session.repository.ts +159 -0
- package/src/modules/session/session.service.ts +340 -0
- package/src/modules/session/types.ts +38 -0
- package/src/modules/swagger/index.ts +7 -1
- package/src/modules/swagger/schema-builder.ts +16 -4
- package/src/modules/swagger/swagger.service.ts +9 -10
- package/src/modules/swagger/types.ts +0 -2
- package/src/modules/upload/index.ts +14 -0
- package/src/modules/upload/types.ts +83 -0
- package/src/modules/upload/upload.repository.ts +199 -0
- package/src/modules/upload/upload.routes.ts +311 -0
- package/src/modules/upload/upload.service.ts +448 -0
- package/src/modules/user/index.ts +3 -3
- package/src/modules/user/user.controller.ts +15 -9
- package/src/modules/user/user.repository.ts +237 -113
- package/src/modules/user/user.routes.ts +39 -164
- package/src/modules/user/user.service.ts +4 -3
- package/src/modules/validation/validator.ts +12 -17
- package/src/modules/webhook/index.ts +91 -0
- package/src/modules/webhook/retry.ts +196 -0
- package/src/modules/webhook/signature.ts +135 -0
- package/src/modules/webhook/types.ts +181 -0
- package/src/modules/webhook/webhook.repository.ts +358 -0
- package/src/modules/webhook/webhook.routes.ts +442 -0
- package/src/modules/webhook/webhook.service.ts +457 -0
- package/src/modules/websocket/features.ts +504 -0
- package/src/modules/websocket/index.ts +106 -0
- package/src/modules/websocket/middlewares.ts +298 -0
- package/src/modules/websocket/types.ts +181 -0
- package/src/modules/websocket/websocket.service.ts +692 -0
- package/src/utils/errors.ts +7 -0
- package/src/utils/pagination.ts +4 -1
- package/tests/helpers/db-check.ts +79 -0
- package/tests/integration/auth-redis.test.ts +94 -0
- package/tests/integration/cache-redis.test.ts +387 -0
- package/tests/integration/mongoose-repositories.test.ts +410 -0
- package/tests/integration/payment-prisma.test.ts +637 -0
- package/tests/integration/queue-bullmq.test.ts +417 -0
- package/tests/integration/user-prisma.test.ts +441 -0
- package/tests/integration/websocket-socketio.test.ts +552 -0
- package/tests/setup.ts +11 -9
- package/vitest.config.ts +3 -8
- package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
- package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
- package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
- package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
- package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
export type SearchEngineType = 'elasticsearch' | 'meilisearch' | 'memory';
|
|
2
|
+
|
|
3
|
+
export interface SearchConfig {
|
|
4
|
+
/** Search engine to use */
|
|
5
|
+
engine?: SearchEngineType;
|
|
6
|
+
/** Elasticsearch config */
|
|
7
|
+
elasticsearch?: {
|
|
8
|
+
node: string;
|
|
9
|
+
auth?: {
|
|
10
|
+
username: string;
|
|
11
|
+
password: string;
|
|
12
|
+
};
|
|
13
|
+
apiKey?: string;
|
|
14
|
+
};
|
|
15
|
+
/** Meilisearch config */
|
|
16
|
+
meilisearch?: {
|
|
17
|
+
host: string;
|
|
18
|
+
apiKey?: string;
|
|
19
|
+
};
|
|
20
|
+
/** Default index settings */
|
|
21
|
+
defaultSettings?: IndexSettings;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface IndexSettings {
|
|
25
|
+
/** Searchable attributes */
|
|
26
|
+
searchableAttributes?: string[];
|
|
27
|
+
/** Attributes to display in results */
|
|
28
|
+
displayedAttributes?: string[];
|
|
29
|
+
/** Filterable attributes */
|
|
30
|
+
filterableAttributes?: string[];
|
|
31
|
+
/** Sortable attributes */
|
|
32
|
+
sortableAttributes?: string[];
|
|
33
|
+
/** Ranking rules */
|
|
34
|
+
rankingRules?: string[];
|
|
35
|
+
/** Stop words */
|
|
36
|
+
stopWords?: string[];
|
|
37
|
+
/** Synonyms */
|
|
38
|
+
synonyms?: Record<string, string[]>;
|
|
39
|
+
/** Number of replicas */
|
|
40
|
+
numberOfReplicas?: number;
|
|
41
|
+
/** Number of shards */
|
|
42
|
+
numberOfShards?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SearchDocument {
|
|
46
|
+
/** Document unique ID */
|
|
47
|
+
id: string;
|
|
48
|
+
/** Document data */
|
|
49
|
+
[key: string]: unknown;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface SearchQuery {
|
|
53
|
+
/** Search query string */
|
|
54
|
+
query: string;
|
|
55
|
+
/** Filters to apply */
|
|
56
|
+
filters?: SearchFilter[];
|
|
57
|
+
/** Fields to search in */
|
|
58
|
+
fields?: string[];
|
|
59
|
+
/** Limit results */
|
|
60
|
+
limit?: number;
|
|
61
|
+
/** Offset for pagination */
|
|
62
|
+
offset?: number;
|
|
63
|
+
/** Sort options */
|
|
64
|
+
sort?: SearchSort[];
|
|
65
|
+
/** Facets to compute */
|
|
66
|
+
facets?: string[];
|
|
67
|
+
/** Highlight matches */
|
|
68
|
+
highlight?: boolean;
|
|
69
|
+
/** Fuzzy search threshold (0-1) */
|
|
70
|
+
fuzziness?: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface SearchFilter {
|
|
74
|
+
/** Field to filter on */
|
|
75
|
+
field: string;
|
|
76
|
+
/** Filter operator */
|
|
77
|
+
operator: 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'nin' | 'contains' | 'exists';
|
|
78
|
+
/** Filter value */
|
|
79
|
+
value: unknown;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface SearchSort {
|
|
83
|
+
/** Field to sort by */
|
|
84
|
+
field: string;
|
|
85
|
+
/** Sort direction */
|
|
86
|
+
direction: 'asc' | 'desc';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface SearchResult<T = SearchDocument> {
|
|
90
|
+
/** Matched documents */
|
|
91
|
+
hits: SearchHit<T>[];
|
|
92
|
+
/** Total number of matches */
|
|
93
|
+
total: number;
|
|
94
|
+
/** Query processing time (ms) */
|
|
95
|
+
processingTime: number;
|
|
96
|
+
/** Current page */
|
|
97
|
+
page?: number;
|
|
98
|
+
/** Total pages */
|
|
99
|
+
totalPages?: number;
|
|
100
|
+
/** Facet results */
|
|
101
|
+
facets?: Record<string, FacetResult>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface SearchHit<T = SearchDocument> {
|
|
105
|
+
/** Document */
|
|
106
|
+
document: T;
|
|
107
|
+
/** Relevance score */
|
|
108
|
+
score?: number;
|
|
109
|
+
/** Highlighted fields */
|
|
110
|
+
highlights?: Record<string, string[]>;
|
|
111
|
+
/** Matched terms */
|
|
112
|
+
matchedTerms?: string[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface FacetResult {
|
|
116
|
+
/** Facet values and counts */
|
|
117
|
+
values: Array<{
|
|
118
|
+
value: string;
|
|
119
|
+
count: number;
|
|
120
|
+
}>;
|
|
121
|
+
/** Total unique values */
|
|
122
|
+
total: number;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface AutocompleteResult {
|
|
126
|
+
/** Suggestions */
|
|
127
|
+
suggestions: string[];
|
|
128
|
+
/** Documents matching suggestions */
|
|
129
|
+
documents?: SearchDocument[];
|
|
130
|
+
/** Processing time (ms) */
|
|
131
|
+
processingTime: number;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface IndexStats {
|
|
135
|
+
/** Index name */
|
|
136
|
+
name: string;
|
|
137
|
+
/** Total documents */
|
|
138
|
+
documentCount: number;
|
|
139
|
+
/** Index size in bytes */
|
|
140
|
+
size: number;
|
|
141
|
+
/** Is indexing in progress */
|
|
142
|
+
isIndexing: boolean;
|
|
143
|
+
/** Last update timestamp */
|
|
144
|
+
lastUpdate?: Date;
|
|
145
|
+
/** Health status */
|
|
146
|
+
health?: 'green' | 'yellow' | 'red';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface BulkIndexOperation {
|
|
150
|
+
/** Operation type */
|
|
151
|
+
operation: 'index' | 'update' | 'delete';
|
|
152
|
+
/** Document ID */
|
|
153
|
+
id: string;
|
|
154
|
+
/** Document data (for index/update) */
|
|
155
|
+
document?: SearchDocument;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface BulkIndexResult {
|
|
159
|
+
/** Successful operations */
|
|
160
|
+
success: number;
|
|
161
|
+
/** Failed operations */
|
|
162
|
+
failed: number;
|
|
163
|
+
/** Error details */
|
|
164
|
+
errors?: Array<{
|
|
165
|
+
id: string;
|
|
166
|
+
error: string;
|
|
167
|
+
}>;
|
|
168
|
+
/** Processing time (ms) */
|
|
169
|
+
processingTime: number;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface SearchSuggestion {
|
|
173
|
+
/** Suggestion text */
|
|
174
|
+
text: string;
|
|
175
|
+
/** Score/relevance */
|
|
176
|
+
score: number;
|
|
177
|
+
/** Highlighted version */
|
|
178
|
+
highlighted?: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface SearchEngine {
|
|
182
|
+
/** Index a document */
|
|
183
|
+
index(indexName: string, id: string, document: SearchDocument): Promise<void>;
|
|
184
|
+
|
|
185
|
+
/** Bulk index documents */
|
|
186
|
+
bulkIndex(indexName: string, operations: BulkIndexOperation[]): Promise<BulkIndexResult>;
|
|
187
|
+
|
|
188
|
+
/** Search documents */
|
|
189
|
+
search<T = SearchDocument>(indexName: string, query: SearchQuery): Promise<SearchResult<T>>;
|
|
190
|
+
|
|
191
|
+
/** Delete document */
|
|
192
|
+
delete(indexName: string, id: string): Promise<void>;
|
|
193
|
+
|
|
194
|
+
/** Update document */
|
|
195
|
+
update(indexName: string, id: string, document: Partial<SearchDocument>): Promise<void>;
|
|
196
|
+
|
|
197
|
+
/** Get document by ID */
|
|
198
|
+
get(indexName: string, id: string): Promise<SearchDocument | null>;
|
|
199
|
+
|
|
200
|
+
/** Create index */
|
|
201
|
+
createIndex(indexName: string, settings?: IndexSettings): Promise<void>;
|
|
202
|
+
|
|
203
|
+
/** Delete index */
|
|
204
|
+
deleteIndex(indexName: string): Promise<void>;
|
|
205
|
+
|
|
206
|
+
/** Update index settings */
|
|
207
|
+
updateSettings(indexName: string, settings: IndexSettings): Promise<void>;
|
|
208
|
+
|
|
209
|
+
/** Get index stats */
|
|
210
|
+
getStats(indexName: string): Promise<IndexStats>;
|
|
211
|
+
|
|
212
|
+
/** Autocomplete */
|
|
213
|
+
autocomplete(indexName: string, query: string, limit?: number): Promise<AutocompleteResult>;
|
|
214
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Module
|
|
3
|
+
* Comprehensive security features for the application
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Sanitization
|
|
7
|
+
export {
|
|
8
|
+
escapeHtml,
|
|
9
|
+
stripDangerousHtml,
|
|
10
|
+
sanitizeString,
|
|
11
|
+
sanitizeObject,
|
|
12
|
+
sanitizeSqlPatterns,
|
|
13
|
+
sanitizeUrl,
|
|
14
|
+
sanitizeFilename,
|
|
15
|
+
sanitizeForJson,
|
|
16
|
+
containsDangerousContent,
|
|
17
|
+
type SanitizeOptions,
|
|
18
|
+
type SanitizeMiddlewareOptions,
|
|
19
|
+
} from './sanitize.js';
|
|
20
|
+
|
|
21
|
+
// Middleware
|
|
22
|
+
export {
|
|
23
|
+
sanitizeInput,
|
|
24
|
+
csrfProtection,
|
|
25
|
+
generateCsrfToken,
|
|
26
|
+
hppProtection,
|
|
27
|
+
securityHeaders,
|
|
28
|
+
requestSizeLimit,
|
|
29
|
+
suspiciousActivityDetection,
|
|
30
|
+
registerSecurityMiddlewares,
|
|
31
|
+
} from './security.middleware.js';
|
|
32
|
+
|
|
33
|
+
// Security Audit
|
|
34
|
+
export {
|
|
35
|
+
SecurityAuditService,
|
|
36
|
+
getSecurityAuditService,
|
|
37
|
+
type SecurityEvent,
|
|
38
|
+
type SecurityEventType,
|
|
39
|
+
type SecurityEventInput,
|
|
40
|
+
} from './security-audit.service.js';
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input Sanitization Module
|
|
3
|
+
* Prevents XSS attacks by sanitizing user input
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// HTML entities to escape
|
|
7
|
+
const HTML_ENTITIES: Record<string, string> = {
|
|
8
|
+
'&': '&',
|
|
9
|
+
'<': '<',
|
|
10
|
+
'>': '>',
|
|
11
|
+
'"': '"',
|
|
12
|
+
"'": ''',
|
|
13
|
+
'/': '/',
|
|
14
|
+
'`': '`',
|
|
15
|
+
'=': '=',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Regex patterns for dangerous content
|
|
19
|
+
const SCRIPT_PATTERN = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi;
|
|
20
|
+
const EVENT_HANDLER_PATTERN = /\s*on\w+\s*=\s*(['"])[^'"]*\1/gi;
|
|
21
|
+
const JAVASCRIPT_URL_PATTERN = /javascript\s*:/gi;
|
|
22
|
+
const DATA_URL_PATTERN = /data\s*:/gi;
|
|
23
|
+
const EXPRESSION_PATTERN = /expression\s*\(/gi;
|
|
24
|
+
const VBSCRIPT_PATTERN = /vbscript\s*:/gi;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Escape HTML special characters
|
|
28
|
+
*/
|
|
29
|
+
export function escapeHtml(str: string): string {
|
|
30
|
+
if (typeof str !== 'string') return str;
|
|
31
|
+
return str.replace(/[&<>"'`=/]/g, (char) => HTML_ENTITIES[char] || char);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Remove dangerous HTML tags and attributes
|
|
36
|
+
*/
|
|
37
|
+
export function stripDangerousHtml(str: string): string {
|
|
38
|
+
if (typeof str !== 'string') return str;
|
|
39
|
+
|
|
40
|
+
return str
|
|
41
|
+
.replace(SCRIPT_PATTERN, '')
|
|
42
|
+
.replace(EVENT_HANDLER_PATTERN, '')
|
|
43
|
+
.replace(JAVASCRIPT_URL_PATTERN, '')
|
|
44
|
+
.replace(DATA_URL_PATTERN, '')
|
|
45
|
+
.replace(EXPRESSION_PATTERN, '')
|
|
46
|
+
.replace(VBSCRIPT_PATTERN, '');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Sanitize a string for safe output
|
|
51
|
+
*/
|
|
52
|
+
export function sanitizeString(str: string, options: SanitizeOptions = {}): string {
|
|
53
|
+
if (typeof str !== 'string') return str;
|
|
54
|
+
|
|
55
|
+
const { escapeHtmlChars = true, stripScripts = true, trim = true, maxLength } = options;
|
|
56
|
+
|
|
57
|
+
let result = str;
|
|
58
|
+
|
|
59
|
+
if (trim) {
|
|
60
|
+
result = result.trim();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (stripScripts) {
|
|
64
|
+
result = stripDangerousHtml(result);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (escapeHtmlChars) {
|
|
68
|
+
result = escapeHtml(result);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (maxLength && result.length > maxLength) {
|
|
72
|
+
result = result.substring(0, maxLength);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Sanitize an object recursively
|
|
80
|
+
*/
|
|
81
|
+
export function sanitizeObject<T extends Record<string, unknown>>(
|
|
82
|
+
obj: T,
|
|
83
|
+
options: SanitizeOptions = {}
|
|
84
|
+
): T {
|
|
85
|
+
if (obj === null || typeof obj !== 'object') {
|
|
86
|
+
return obj;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (Array.isArray(obj)) {
|
|
90
|
+
return obj.map((item) => {
|
|
91
|
+
if (typeof item === 'string') {
|
|
92
|
+
return sanitizeString(item, options);
|
|
93
|
+
}
|
|
94
|
+
if (typeof item === 'object' && item !== null) {
|
|
95
|
+
return sanitizeObject(item as Record<string, unknown>, options);
|
|
96
|
+
}
|
|
97
|
+
return item;
|
|
98
|
+
}) as unknown as T;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const sanitized: Record<string, unknown> = {};
|
|
102
|
+
|
|
103
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
104
|
+
// Sanitize the key as well (prevent prototype pollution)
|
|
105
|
+
const safeKey = sanitizeString(key, { escapeHtmlChars: false, stripScripts: true });
|
|
106
|
+
|
|
107
|
+
// Skip dangerous keys
|
|
108
|
+
if (safeKey === '__proto__' || safeKey === 'constructor' || safeKey === 'prototype') {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (typeof value === 'string') {
|
|
113
|
+
sanitized[safeKey] = sanitizeString(value, options);
|
|
114
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
115
|
+
sanitized[safeKey] = sanitizeObject(value as Record<string, unknown>, options);
|
|
116
|
+
} else {
|
|
117
|
+
sanitized[safeKey] = value;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return sanitized as T;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Sanitize SQL-like patterns (additional protection layer)
|
|
126
|
+
* Note: Prisma already handles SQL injection, this is defense-in-depth
|
|
127
|
+
*/
|
|
128
|
+
export function sanitizeSqlPatterns(str: string): string {
|
|
129
|
+
if (typeof str !== 'string') return str;
|
|
130
|
+
|
|
131
|
+
return str
|
|
132
|
+
.replace(/--/g, '')
|
|
133
|
+
.replace(/;/g, '')
|
|
134
|
+
.replace(/\/\*/g, '')
|
|
135
|
+
.replace(/\*\//g, '')
|
|
136
|
+
.replace(/xp_/gi, '')
|
|
137
|
+
.replace(/union\s+select/gi, '')
|
|
138
|
+
.replace(/insert\s+into/gi, '')
|
|
139
|
+
.replace(/drop\s+table/gi, '')
|
|
140
|
+
.replace(/delete\s+from/gi, '');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Sanitize for safe URL usage
|
|
145
|
+
*/
|
|
146
|
+
export function sanitizeUrl(url: string): string {
|
|
147
|
+
if (typeof url !== 'string') return '';
|
|
148
|
+
|
|
149
|
+
const sanitized = url.trim().toLowerCase();
|
|
150
|
+
|
|
151
|
+
// Block dangerous protocols
|
|
152
|
+
if (
|
|
153
|
+
sanitized.startsWith('javascript:') ||
|
|
154
|
+
sanitized.startsWith('data:') ||
|
|
155
|
+
sanitized.startsWith('vbscript:')
|
|
156
|
+
) {
|
|
157
|
+
return '';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return url;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Sanitize filename for safe storage
|
|
165
|
+
*/
|
|
166
|
+
export function sanitizeFilename(filename: string): string {
|
|
167
|
+
if (typeof filename !== 'string') return '';
|
|
168
|
+
|
|
169
|
+
return filename
|
|
170
|
+
.replace(/[^a-zA-Z0-9._-]/g, '_') // Replace unsafe chars with underscore
|
|
171
|
+
.replace(/\.{2,}/g, '.') // Remove multiple dots
|
|
172
|
+
.replace(/^\.+|\.+$/g, '') // Remove leading/trailing dots
|
|
173
|
+
.substring(0, 255); // Limit length
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Sanitize for JSON output (prevent JSON injection)
|
|
178
|
+
*/
|
|
179
|
+
export function sanitizeForJson(str: string): string {
|
|
180
|
+
if (typeof str !== 'string') return str;
|
|
181
|
+
|
|
182
|
+
return str
|
|
183
|
+
.replace(/\\/g, '\\\\')
|
|
184
|
+
.replace(/"/g, '\\"')
|
|
185
|
+
.replace(/\n/g, '\\n')
|
|
186
|
+
.replace(/\r/g, '\\r')
|
|
187
|
+
.replace(/\t/g, '\\t');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Check if a string contains potentially dangerous content
|
|
192
|
+
*/
|
|
193
|
+
export function containsDangerousContent(str: string): boolean {
|
|
194
|
+
if (typeof str !== 'string') return false;
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
SCRIPT_PATTERN.test(str) ||
|
|
198
|
+
EVENT_HANDLER_PATTERN.test(str) ||
|
|
199
|
+
JAVASCRIPT_URL_PATTERN.test(str) ||
|
|
200
|
+
/<iframe/i.test(str) ||
|
|
201
|
+
/<object/i.test(str) ||
|
|
202
|
+
/<embed/i.test(str) ||
|
|
203
|
+
/<form/i.test(str)
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface SanitizeOptions {
|
|
208
|
+
/** Escape HTML characters (default: true) */
|
|
209
|
+
escapeHtmlChars?: boolean;
|
|
210
|
+
/** Strip script tags and event handlers (default: true) */
|
|
211
|
+
stripScripts?: boolean;
|
|
212
|
+
/** Trim whitespace (default: true) */
|
|
213
|
+
trim?: boolean;
|
|
214
|
+
/** Maximum length of string */
|
|
215
|
+
maxLength?: number;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface SanitizeMiddlewareOptions extends SanitizeOptions {
|
|
219
|
+
/** Fields to skip sanitization */
|
|
220
|
+
skipFields?: string[];
|
|
221
|
+
/** Only sanitize these fields */
|
|
222
|
+
onlyFields?: string[];
|
|
223
|
+
}
|