@adaptic/backend-legacy 0.0.70 → 0.0.72
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/client.cjs +1 -1
- package/config/jwtConfig.cjs +52 -0
- package/config/jwtConfig.d.ts +16 -0
- package/config/jwtConfig.d.ts.map +1 -0
- package/config/jwtConfig.js.map +1 -0
- package/config/metrics.cjs +261 -0
- package/config/metrics.d.ts +88 -0
- package/config/metrics.d.ts.map +1 -0
- package/config/metrics.js.map +1 -0
- package/config/persisted-queries.cjs +122 -0
- package/config/persisted-queries.d.ts +40 -0
- package/config/persisted-queries.d.ts.map +1 -0
- package/config/persisted-queries.js.map +1 -0
- package/config/tracing.cjs +128 -0
- package/config/tracing.d.ts +24 -0
- package/config/tracing.d.ts.map +1 -0
- package/config/tracing.js.map +1 -0
- package/middleware/audit-logger.cjs +223 -0
- package/middleware/audit-logger.d.ts +85 -0
- package/middleware/audit-logger.d.ts.map +1 -0
- package/middleware/audit-logger.js.map +1 -0
- package/middleware/auth.cjs +44 -0
- package/middleware/auth.d.ts +6 -0
- package/middleware/auth.d.ts.map +1 -0
- package/middleware/auth.js.map +1 -0
- package/middleware/graphql-validation-plugin.cjs +164 -0
- package/middleware/graphql-validation-plugin.d.ts +37 -0
- package/middleware/graphql-validation-plugin.d.ts.map +1 -0
- package/middleware/graphql-validation-plugin.js.map +1 -0
- package/middleware/index.cjs +46 -0
- package/middleware/index.d.ts +13 -0
- package/middleware/index.d.ts.map +1 -0
- package/middleware/index.js.map +1 -0
- package/middleware/input-validator.cjs +220 -0
- package/middleware/input-validator.d.ts +63 -0
- package/middleware/input-validator.d.ts.map +1 -0
- package/middleware/input-validator.js.map +1 -0
- package/middleware/query-complexity.cjs +182 -0
- package/middleware/query-complexity.d.ts +56 -0
- package/middleware/query-complexity.d.ts.map +1 -0
- package/middleware/query-complexity.js.map +1 -0
- package/middleware/rate-limiter.cjs +112 -0
- package/middleware/rate-limiter.d.ts +16 -0
- package/middleware/rate-limiter.d.ts.map +1 -0
- package/middleware/rate-limiter.js.map +1 -0
- package/middleware/soft-delete.cjs +175 -0
- package/middleware/soft-delete.d.ts +146 -0
- package/middleware/soft-delete.d.ts.map +1 -0
- package/middleware/soft-delete.js.map +1 -0
- package/middleware/types.cjs +17 -0
- package/middleware/types.d.ts +87 -0
- package/middleware/types.d.ts.map +1 -0
- package/middleware/types.js.map +1 -0
- package/middleware/validation-examples.cjs +403 -0
- package/middleware/validation-examples.d.ts +76 -0
- package/middleware/validation-examples.d.ts.map +1 -0
- package/middleware/validation-examples.js.map +1 -0
- package/package.json +5 -1
- package/utils/index.cjs +75 -0
- package/utils/index.d.ts +20 -0
- package/utils/index.d.ts.map +1 -0
- package/utils/index.js.map +1 -0
- package/utils/logger.cjs +31 -0
- package/utils/logger.d.ts +9 -0
- package/utils/logger.d.ts.map +1 -0
- package/utils/logger.js.map +1 -0
- package/validators/allocation-validator.cjs +85 -0
- package/validators/allocation-validator.d.ts +32 -0
- package/validators/allocation-validator.d.ts.map +1 -0
- package/validators/allocation-validator.js.map +1 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkQueryComplexity = checkQueryComplexity;
|
|
4
|
+
exports.createQueryComplexityPlugin = createQueryComplexityPlugin;
|
|
5
|
+
const graphql_query_complexity_1 = require("graphql-query-complexity");
|
|
6
|
+
const logger_1 = require("../utils/logger.cjs");
|
|
7
|
+
/**
|
|
8
|
+
* Default query complexity limits per authentication level.
|
|
9
|
+
* These can be overridden via environment variables.
|
|
10
|
+
*/
|
|
11
|
+
const DEFAULT_MAX_COMPLEXITY_AUTHENTICATED = 1000;
|
|
12
|
+
const DEFAULT_MAX_COMPLEXITY_UNAUTHENTICATED = 200;
|
|
13
|
+
const DEFAULT_MAX_DEPTH = 10;
|
|
14
|
+
/**
|
|
15
|
+
* Resolves the maximum allowed query complexity from environment variables or defaults.
|
|
16
|
+
*
|
|
17
|
+
* @param isAuthenticated - Whether the request is from an authenticated user
|
|
18
|
+
* @returns The maximum allowed complexity score
|
|
19
|
+
*/
|
|
20
|
+
function getMaxComplexity(isAuthenticated) {
|
|
21
|
+
if (isAuthenticated) {
|
|
22
|
+
const envValue = process.env.GRAPHQL_MAX_COMPLEXITY_AUTH;
|
|
23
|
+
if (envValue) {
|
|
24
|
+
const parsed = parseInt(envValue, 10);
|
|
25
|
+
if (!isNaN(parsed) && parsed > 0)
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
return DEFAULT_MAX_COMPLEXITY_AUTHENTICATED;
|
|
29
|
+
}
|
|
30
|
+
const envValue = process.env.GRAPHQL_MAX_COMPLEXITY_UNAUTH;
|
|
31
|
+
if (envValue) {
|
|
32
|
+
const parsed = parseInt(envValue, 10);
|
|
33
|
+
if (!isNaN(parsed) && parsed > 0)
|
|
34
|
+
return parsed;
|
|
35
|
+
}
|
|
36
|
+
return DEFAULT_MAX_COMPLEXITY_UNAUTHENTICATED;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Resolves the maximum allowed query depth.
|
|
40
|
+
*/
|
|
41
|
+
function getMaxDepth() {
|
|
42
|
+
const envValue = process.env.GRAPHQL_MAX_DEPTH;
|
|
43
|
+
if (envValue) {
|
|
44
|
+
const parsed = parseInt(envValue, 10);
|
|
45
|
+
if (!isNaN(parsed) && parsed > 0)
|
|
46
|
+
return parsed;
|
|
47
|
+
}
|
|
48
|
+
return DEFAULT_MAX_DEPTH;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Computes the complexity of a GraphQL document against a schema.
|
|
52
|
+
*
|
|
53
|
+
* Uses two estimators:
|
|
54
|
+
* 1. fieldExtensionsEstimator: Reads complexity from field extensions (set via schema directives)
|
|
55
|
+
* 2. simpleEstimator: Assigns a default cost of 1 per field as fallback
|
|
56
|
+
*
|
|
57
|
+
* @param schema - The GraphQL schema
|
|
58
|
+
* @param document - The parsed GraphQL document (AST)
|
|
59
|
+
* @param variables - Query variables (used for list size estimation)
|
|
60
|
+
* @param isAuthenticated - Whether the request is authenticated (affects limits)
|
|
61
|
+
* @returns The complexity check result
|
|
62
|
+
*/
|
|
63
|
+
function checkQueryComplexity(schema, document, variables, isAuthenticated) {
|
|
64
|
+
const maxComplexity = getMaxComplexity(isAuthenticated);
|
|
65
|
+
try {
|
|
66
|
+
const complexity = (0, graphql_query_complexity_1.getComplexity)({
|
|
67
|
+
schema,
|
|
68
|
+
query: document,
|
|
69
|
+
variables,
|
|
70
|
+
estimators: [
|
|
71
|
+
(0, graphql_query_complexity_1.fieldExtensionsEstimator)(),
|
|
72
|
+
(0, graphql_query_complexity_1.simpleEstimator)({ defaultComplexity: 1 }),
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
complexity,
|
|
77
|
+
exceeded: complexity > maxComplexity,
|
|
78
|
+
maxComplexity,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
catch (estimationError) {
|
|
82
|
+
logger_1.logger.warn('Failed to estimate query complexity', {
|
|
83
|
+
error: estimationError instanceof Error
|
|
84
|
+
? estimationError.message
|
|
85
|
+
: String(estimationError),
|
|
86
|
+
});
|
|
87
|
+
// On failure, allow the query to proceed to avoid blocking legitimate requests
|
|
88
|
+
return {
|
|
89
|
+
complexity: 0,
|
|
90
|
+
exceeded: false,
|
|
91
|
+
maxComplexity,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Checks query depth by counting nested selections.
|
|
97
|
+
*
|
|
98
|
+
* @param document - The parsed GraphQL document (AST)
|
|
99
|
+
* @returns The maximum depth found in the query
|
|
100
|
+
*/
|
|
101
|
+
function computeQueryDepth(document) {
|
|
102
|
+
let maxDepth = 0;
|
|
103
|
+
function traverse(node, depth) {
|
|
104
|
+
if (depth > maxDepth) {
|
|
105
|
+
maxDepth = depth;
|
|
106
|
+
}
|
|
107
|
+
if (node.selectionSet) {
|
|
108
|
+
for (const selection of node.selectionSet.selections) {
|
|
109
|
+
traverse(selection, depth + 1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
for (const definition of document.definitions) {
|
|
114
|
+
traverse(definition, 0);
|
|
115
|
+
}
|
|
116
|
+
return maxDepth;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Creates an Apollo Server plugin that enforces query complexity and depth limits.
|
|
120
|
+
*
|
|
121
|
+
* The plugin runs before query execution and rejects queries that exceed
|
|
122
|
+
* configured complexity or depth thresholds.
|
|
123
|
+
*
|
|
124
|
+
* Environment variables:
|
|
125
|
+
* - GRAPHQL_MAX_COMPLEXITY_AUTH: Max complexity for authenticated users (default: 1000)
|
|
126
|
+
* - GRAPHQL_MAX_COMPLEXITY_UNAUTH: Max complexity for unauthenticated users (default: 200)
|
|
127
|
+
* - GRAPHQL_MAX_DEPTH: Max query depth (default: 10)
|
|
128
|
+
* - GRAPHQL_COMPLEXITY_ENABLED: Explicit on/off ('true'/'false'). Defaults to on in production/staging.
|
|
129
|
+
*
|
|
130
|
+
* @param schema - The GraphQL schema to validate against
|
|
131
|
+
* @returns Apollo Server plugin configuration
|
|
132
|
+
*/
|
|
133
|
+
function createQueryComplexityPlugin(schema) {
|
|
134
|
+
const isEnabled = () => {
|
|
135
|
+
const explicitSetting = process.env.GRAPHQL_COMPLEXITY_ENABLED;
|
|
136
|
+
if (explicitSetting !== undefined) {
|
|
137
|
+
return explicitSetting === 'true' || explicitSetting === '1';
|
|
138
|
+
}
|
|
139
|
+
const env = process.env.NODE_ENV || 'development';
|
|
140
|
+
return env === 'production' || env === 'staging';
|
|
141
|
+
};
|
|
142
|
+
return {
|
|
143
|
+
async requestDidStart() {
|
|
144
|
+
return {
|
|
145
|
+
async didResolveOperation(requestContext) {
|
|
146
|
+
if (!isEnabled())
|
|
147
|
+
return;
|
|
148
|
+
const { document, request, contextValue } = requestContext;
|
|
149
|
+
const variables = (request.variables || {});
|
|
150
|
+
const isAuthenticated = Boolean(contextValue.user);
|
|
151
|
+
// Check depth limit
|
|
152
|
+
const maxDepth = getMaxDepth();
|
|
153
|
+
const depth = computeQueryDepth(document);
|
|
154
|
+
if (depth > maxDepth) {
|
|
155
|
+
logger_1.logger.warn('Query depth exceeded', {
|
|
156
|
+
depth,
|
|
157
|
+
maxDepth,
|
|
158
|
+
isAuthenticated,
|
|
159
|
+
});
|
|
160
|
+
throw new Error(`Query depth of ${depth} exceeds maximum allowed depth of ${maxDepth}`);
|
|
161
|
+
}
|
|
162
|
+
// Check complexity limit
|
|
163
|
+
const result = checkQueryComplexity(schema, document, variables, isAuthenticated);
|
|
164
|
+
if (result.exceeded) {
|
|
165
|
+
logger_1.logger.warn('Query complexity exceeded', {
|
|
166
|
+
complexity: result.complexity,
|
|
167
|
+
maxComplexity: result.maxComplexity,
|
|
168
|
+
isAuthenticated,
|
|
169
|
+
});
|
|
170
|
+
throw new Error(`Query complexity of ${result.complexity} exceeds maximum allowed complexity of ${result.maxComplexity}`);
|
|
171
|
+
}
|
|
172
|
+
logger_1.logger.debug('Query complexity check passed', {
|
|
173
|
+
complexity: result.complexity,
|
|
174
|
+
maxComplexity: result.maxComplexity,
|
|
175
|
+
depth,
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=query-complexity.js.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { GraphQLSchema, DocumentNode } from 'graphql';
|
|
2
|
+
/**
|
|
3
|
+
* Result of a query complexity check.
|
|
4
|
+
*/
|
|
5
|
+
interface ComplexityCheckResult {
|
|
6
|
+
/** The computed complexity score */
|
|
7
|
+
complexity: number;
|
|
8
|
+
/** Whether the query exceeds the maximum allowed complexity */
|
|
9
|
+
exceeded: boolean;
|
|
10
|
+
/** The maximum allowed complexity for this request */
|
|
11
|
+
maxComplexity: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Computes the complexity of a GraphQL document against a schema.
|
|
15
|
+
*
|
|
16
|
+
* Uses two estimators:
|
|
17
|
+
* 1. fieldExtensionsEstimator: Reads complexity from field extensions (set via schema directives)
|
|
18
|
+
* 2. simpleEstimator: Assigns a default cost of 1 per field as fallback
|
|
19
|
+
*
|
|
20
|
+
* @param schema - The GraphQL schema
|
|
21
|
+
* @param document - The parsed GraphQL document (AST)
|
|
22
|
+
* @param variables - Query variables (used for list size estimation)
|
|
23
|
+
* @param isAuthenticated - Whether the request is authenticated (affects limits)
|
|
24
|
+
* @returns The complexity check result
|
|
25
|
+
*/
|
|
26
|
+
export declare function checkQueryComplexity(schema: GraphQLSchema, document: DocumentNode, variables: Record<string, unknown>, isAuthenticated: boolean): ComplexityCheckResult;
|
|
27
|
+
/**
|
|
28
|
+
* Creates an Apollo Server plugin that enforces query complexity and depth limits.
|
|
29
|
+
*
|
|
30
|
+
* The plugin runs before query execution and rejects queries that exceed
|
|
31
|
+
* configured complexity or depth thresholds.
|
|
32
|
+
*
|
|
33
|
+
* Environment variables:
|
|
34
|
+
* - GRAPHQL_MAX_COMPLEXITY_AUTH: Max complexity for authenticated users (default: 1000)
|
|
35
|
+
* - GRAPHQL_MAX_COMPLEXITY_UNAUTH: Max complexity for unauthenticated users (default: 200)
|
|
36
|
+
* - GRAPHQL_MAX_DEPTH: Max query depth (default: 10)
|
|
37
|
+
* - GRAPHQL_COMPLEXITY_ENABLED: Explicit on/off ('true'/'false'). Defaults to on in production/staging.
|
|
38
|
+
*
|
|
39
|
+
* @param schema - The GraphQL schema to validate against
|
|
40
|
+
* @returns Apollo Server plugin configuration
|
|
41
|
+
*/
|
|
42
|
+
export declare function createQueryComplexityPlugin(schema: GraphQLSchema): {
|
|
43
|
+
requestDidStart: () => Promise<{
|
|
44
|
+
didResolveOperation: (requestContext: {
|
|
45
|
+
document: DocumentNode;
|
|
46
|
+
request: {
|
|
47
|
+
variables?: Record<string, unknown> | null;
|
|
48
|
+
};
|
|
49
|
+
contextValue: {
|
|
50
|
+
user?: unknown;
|
|
51
|
+
};
|
|
52
|
+
}) => Promise<void>;
|
|
53
|
+
}>;
|
|
54
|
+
};
|
|
55
|
+
export {};
|
|
56
|
+
//# sourceMappingURL=query-complexity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query-complexity.d.ts","sourceRoot":"","sources":["../../src/middleware/query-complexity.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AA+CtD;;GAEG;AACH,UAAU,qBAAqB;IAC7B,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,QAAQ,EAAE,OAAO,CAAC;IAClB,sDAAsD;IACtD,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,aAAa,EACrB,QAAQ,EAAE,YAAY,EACtB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClC,eAAe,EAAE,OAAO,GACvB,qBAAqB,CAiCvB;AAgDD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,aAAa,GAAG;IAClE,eAAe,EAAE,MAAM,OAAO,CAAC;QAC7B,mBAAmB,EAAE,CAAC,cAAc,EAAE;YACpC,QAAQ,EAAE,YAAY,CAAC;YACvB,OAAO,EAAE;gBAAE,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;aAAE,CAAC;YACxD,YAAY,EAAE;gBAAE,IAAI,CAAC,EAAE,OAAO,CAAA;aAAE,CAAC;SAClC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KACrB,CAAC,CAAC;CACJ,CAgEA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query-complexity.js","sourceRoot":"","sources":["../../src/middleware/query-complexity.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Integration: add to server.ts - app.use('/graphql', graphqlRateLimiter) and app.use('/auth', authRateLimiter)
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.authRateLimiter = exports.graphqlRateLimiter = void 0;
|
|
5
|
+
/**
|
|
6
|
+
* Checks whether a request carries a valid-looking authentication token.
|
|
7
|
+
* Does not verify the token -- only checks for its presence in the
|
|
8
|
+
* Authorization header as a Bearer token with three dot-separated parts
|
|
9
|
+
* (standard JWT structure).
|
|
10
|
+
*/
|
|
11
|
+
function isAuthenticated(req) {
|
|
12
|
+
const authHeader = req.headers.authorization || '';
|
|
13
|
+
if (!authHeader.startsWith('Bearer ')) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
const token = authHeader.slice(7);
|
|
17
|
+
// Google OAuth tokens (ya29.) and JWTs (three dot-separated segments) count
|
|
18
|
+
if (token.startsWith('ya29.')) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
return token.split('.').length === 3;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Creates a simple in-memory rate limiter middleware with separate limits
|
|
25
|
+
* for authenticated and unauthenticated requests.
|
|
26
|
+
*
|
|
27
|
+
* Response headers (when standardHeaders is enabled):
|
|
28
|
+
* X-RateLimit-Limit - maximum requests allowed in the current window
|
|
29
|
+
* X-RateLimit-Remaining - requests remaining in the current window
|
|
30
|
+
* X-RateLimit-Reset - seconds until the current window resets
|
|
31
|
+
* Retry-After - seconds to wait before retrying (only on 429)
|
|
32
|
+
*
|
|
33
|
+
* @param config - Rate limit configuration
|
|
34
|
+
* @returns Express middleware function
|
|
35
|
+
*/
|
|
36
|
+
function createRateLimiter(config) {
|
|
37
|
+
const store = {};
|
|
38
|
+
// Clean up expired entries every minute
|
|
39
|
+
setInterval(() => {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
Object.keys(store).forEach((key) => {
|
|
42
|
+
if (store[key].resetTime < now) {
|
|
43
|
+
delete store[key];
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}, 60000);
|
|
47
|
+
return (req, res, next) => {
|
|
48
|
+
const identifier = req.ip || req.connection.remoteAddress || 'unknown';
|
|
49
|
+
const authenticated = isAuthenticated(req);
|
|
50
|
+
const effectiveMax = authenticated
|
|
51
|
+
? config.maxAuthenticated
|
|
52
|
+
: config.maxUnauthenticated;
|
|
53
|
+
const storeKey = `${identifier}:${authenticated ? 'auth' : 'anon'}`;
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
if (!store[storeKey] || store[storeKey].resetTime < now) {
|
|
56
|
+
store[storeKey] = {
|
|
57
|
+
count: 1,
|
|
58
|
+
resetTime: now + config.windowMs,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
store[storeKey].count += 1;
|
|
63
|
+
}
|
|
64
|
+
const current = store[storeKey];
|
|
65
|
+
const remaining = Math.max(0, effectiveMax - current.count);
|
|
66
|
+
const resetSeconds = Math.ceil((current.resetTime - now) / 1000);
|
|
67
|
+
// Add rate limit headers
|
|
68
|
+
if (config.standardHeaders !== false) {
|
|
69
|
+
res.setHeader('X-RateLimit-Limit', effectiveMax.toString());
|
|
70
|
+
res.setHeader('X-RateLimit-Remaining', remaining.toString());
|
|
71
|
+
res.setHeader('X-RateLimit-Reset', resetSeconds.toString());
|
|
72
|
+
}
|
|
73
|
+
if (current.count > effectiveMax) {
|
|
74
|
+
// Include Retry-After header on 429 responses (RFC 6585 / RFC 7231)
|
|
75
|
+
res.setHeader('Retry-After', resetSeconds.toString());
|
|
76
|
+
res.status(429).json(config.message);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
next();
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Rate limiter for GraphQL endpoint.
|
|
84
|
+
*
|
|
85
|
+
* Authenticated requests: 1000 requests per 15 minutes (configurable via RATE_LIMIT_MAX)
|
|
86
|
+
* Unauthenticated requests: 200 requests per 15 minutes (configurable via RATE_LIMIT_MAX_UNAUTH)
|
|
87
|
+
*/
|
|
88
|
+
exports.graphqlRateLimiter = createRateLimiter({
|
|
89
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
90
|
+
maxAuthenticated: parseInt(process.env.RATE_LIMIT_MAX || '1000', 10),
|
|
91
|
+
maxUnauthenticated: parseInt(process.env.RATE_LIMIT_MAX_UNAUTH || '200', 10),
|
|
92
|
+
standardHeaders: true,
|
|
93
|
+
legacyHeaders: false,
|
|
94
|
+
message: {
|
|
95
|
+
errors: [{ message: 'Too many requests, please try again later.' }],
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
/**
|
|
99
|
+
* Rate limiter for authentication endpoints.
|
|
100
|
+
*
|
|
101
|
+
* Authenticated requests: 50 requests per 15 minutes
|
|
102
|
+
* Unauthenticated requests: 20 requests per 15 minutes
|
|
103
|
+
*/
|
|
104
|
+
exports.authRateLimiter = createRateLimiter({
|
|
105
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
106
|
+
maxAuthenticated: 50,
|
|
107
|
+
maxUnauthenticated: 20,
|
|
108
|
+
standardHeaders: true,
|
|
109
|
+
legacyHeaders: false,
|
|
110
|
+
message: { errors: [{ message: 'Too many authentication attempts.' }] },
|
|
111
|
+
});
|
|
112
|
+
//# sourceMappingURL=rate-limiter.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
/**
|
|
3
|
+
* Rate limiter for GraphQL endpoint.
|
|
4
|
+
*
|
|
5
|
+
* Authenticated requests: 1000 requests per 15 minutes (configurable via RATE_LIMIT_MAX)
|
|
6
|
+
* Unauthenticated requests: 200 requests per 15 minutes (configurable via RATE_LIMIT_MAX_UNAUTH)
|
|
7
|
+
*/
|
|
8
|
+
export declare const graphqlRateLimiter: (req: Request, res: Response, next: NextFunction) => void;
|
|
9
|
+
/**
|
|
10
|
+
* Rate limiter for authentication endpoints.
|
|
11
|
+
*
|
|
12
|
+
* Authenticated requests: 50 requests per 15 minutes
|
|
13
|
+
* Unauthenticated requests: 20 requests per 15 minutes
|
|
14
|
+
*/
|
|
15
|
+
export declare const authRateLimiter: (req: Request, res: Response, next: NextFunction) => void;
|
|
16
|
+
//# sourceMappingURL=rate-limiter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../../src/middleware/rate-limiter.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAyG1D;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,QA9ChB,OAAO,OAAO,QAAQ,QAAQ,YAAY,KAAG,IAuD1D,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,eAAe,QA/Db,OAAO,OAAO,QAAQ,QAAQ,YAAY,KAAG,IAsE1D,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limiter.js","sourceRoot":"","sources":["../../src/middleware/rate-limiter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Soft Delete Utilities
|
|
4
|
+
*
|
|
5
|
+
* Provides utilities for soft-delete behavior on critical models.
|
|
6
|
+
* Instead of removing records from the database, a `deletedAt` timestamp
|
|
7
|
+
* is set, and query helpers filter out soft-deleted records by default.
|
|
8
|
+
*
|
|
9
|
+
* Models with soft delete support: User, BrokerageAccount, Trade, Action
|
|
10
|
+
*
|
|
11
|
+
* Usage patterns:
|
|
12
|
+
* 1. Use `softDeleteFilter()` in where clauses to exclude deleted records
|
|
13
|
+
* 2. Use `softDeleteRecord()` to soft-delete a record (set deletedAt)
|
|
14
|
+
* 3. Use `hardDelete()` for permanent administrative deletion
|
|
15
|
+
* 4. Pass `includeDeleted: true` to admin queries to see all records
|
|
16
|
+
*
|
|
17
|
+
* Note: Prisma 6 removed the $use() middleware API. Soft delete behavior
|
|
18
|
+
* is implemented via utility functions called from resolvers and services.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.SOFT_DELETE_MODELS = void 0;
|
|
22
|
+
exports.softDeleteFilter = softDeleteFilter;
|
|
23
|
+
exports.deletedOnlyFilter = deletedOnlyFilter;
|
|
24
|
+
exports.isSoftDeleteModel = isSoftDeleteModel;
|
|
25
|
+
exports.softDeleteRecord = softDeleteRecord;
|
|
26
|
+
exports.restoreRecord = restoreRecord;
|
|
27
|
+
exports.hardDelete = hardDelete;
|
|
28
|
+
const logger_1 = require("../utils/logger.cjs");
|
|
29
|
+
/**
|
|
30
|
+
* Models that support soft deletion via the deletedAt field.
|
|
31
|
+
* Only these models have the deletedAt column in the database.
|
|
32
|
+
*/
|
|
33
|
+
const SOFT_DELETE_MODELS = new Set([
|
|
34
|
+
'User',
|
|
35
|
+
'BrokerageAccount',
|
|
36
|
+
'Trade',
|
|
37
|
+
'Action',
|
|
38
|
+
]);
|
|
39
|
+
exports.SOFT_DELETE_MODELS = SOFT_DELETE_MODELS;
|
|
40
|
+
/**
|
|
41
|
+
* Returns a where clause filter that excludes soft-deleted records.
|
|
42
|
+
* Can be spread into any Prisma where clause for soft-delete-aware queries.
|
|
43
|
+
*
|
|
44
|
+
* @param includeDeleted - When true, returns an empty filter (includes deleted records)
|
|
45
|
+
* @returns An object to spread into a Prisma where clause
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* // Exclude soft-deleted users (default)
|
|
50
|
+
* const users = await prisma.user.findMany({
|
|
51
|
+
* where: { role: 'USER', ...softDeleteFilter() },
|
|
52
|
+
* });
|
|
53
|
+
*
|
|
54
|
+
* // Include soft-deleted users (admin query)
|
|
55
|
+
* const allUsers = await prisma.user.findMany({
|
|
56
|
+
* where: { role: 'USER', ...softDeleteFilter(true) },
|
|
57
|
+
* });
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
function softDeleteFilter(includeDeleted = false) {
|
|
61
|
+
if (includeDeleted) {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
return { deletedAt: null };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Returns a where clause filter for finding only soft-deleted records.
|
|
68
|
+
* Useful for admin interfaces that need to list deleted records for restoration.
|
|
69
|
+
*
|
|
70
|
+
* @returns An object to spread into a Prisma where clause
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```typescript
|
|
74
|
+
* // Find only soft-deleted users
|
|
75
|
+
* const deletedUsers = await prisma.user.findMany({
|
|
76
|
+
* where: { ...deletedOnlyFilter() },
|
|
77
|
+
* });
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
function deletedOnlyFilter() {
|
|
81
|
+
return { deletedAt: { not: null } };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Checks whether a given model supports soft deletion.
|
|
85
|
+
*
|
|
86
|
+
* @param modelName - The Prisma model name (PascalCase)
|
|
87
|
+
* @returns True if the model has a deletedAt field
|
|
88
|
+
*/
|
|
89
|
+
function isSoftDeleteModel(modelName) {
|
|
90
|
+
return SOFT_DELETE_MODELS.has(modelName);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Soft-deletes a record by setting its deletedAt timestamp.
|
|
94
|
+
* This is the recommended way to "delete" records in soft-delete-enabled models.
|
|
95
|
+
*
|
|
96
|
+
* @param delegate - The Prisma model delegate (e.g., prisma.user)
|
|
97
|
+
* @param id - The record ID to soft-delete
|
|
98
|
+
* @param modelName - The model name for logging purposes
|
|
99
|
+
* @returns The updated record
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```typescript
|
|
103
|
+
* // Soft-delete a user
|
|
104
|
+
* await softDeleteRecord(prisma.user, 'user-123', 'User');
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
async function softDeleteRecord(delegate, id, modelName) {
|
|
108
|
+
logger_1.logger.info('Soft delete: Setting deletedAt on record', {
|
|
109
|
+
model: modelName,
|
|
110
|
+
id,
|
|
111
|
+
});
|
|
112
|
+
return delegate.update({
|
|
113
|
+
where: { id },
|
|
114
|
+
data: { deletedAt: new Date() },
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Restores a soft-deleted record by clearing its deletedAt timestamp.
|
|
119
|
+
*
|
|
120
|
+
* @param delegate - The Prisma model delegate (e.g., prisma.user)
|
|
121
|
+
* @param id - The record ID to restore
|
|
122
|
+
* @param modelName - The model name for logging purposes
|
|
123
|
+
* @returns The updated record
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```typescript
|
|
127
|
+
* // Restore a soft-deleted user
|
|
128
|
+
* await restoreRecord(prisma.user, 'user-123', 'User');
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
async function restoreRecord(delegate, id, modelName) {
|
|
132
|
+
logger_1.logger.info('Soft delete: Restoring record (clearing deletedAt)', {
|
|
133
|
+
model: modelName,
|
|
134
|
+
id,
|
|
135
|
+
});
|
|
136
|
+
return delegate.update({
|
|
137
|
+
where: { id },
|
|
138
|
+
data: { deletedAt: null },
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Mapping from model names to their database table names.
|
|
143
|
+
* Used for raw SQL hard-delete operations.
|
|
144
|
+
*/
|
|
145
|
+
const TABLE_NAME_MAP = {
|
|
146
|
+
User: 'users',
|
|
147
|
+
BrokerageAccount: 'brokerage_accounts',
|
|
148
|
+
Trade: 'trades',
|
|
149
|
+
Action: 'actions',
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Permanently deletes a record from the database.
|
|
153
|
+
* This bypasses soft delete and removes the row entirely.
|
|
154
|
+
* Should only be used for administrative cleanup operations.
|
|
155
|
+
*
|
|
156
|
+
* @param prisma - The Prisma client instance (with $executeRawUnsafe)
|
|
157
|
+
* @param model - The model name (PascalCase)
|
|
158
|
+
* @param id - The record ID to permanently delete
|
|
159
|
+
* @returns The number of rows deleted
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* // Permanently delete a soft-deleted user
|
|
164
|
+
* const deleted = await hardDelete(prisma, 'User', 'user-123');
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
async function hardDelete(prisma, model, id) {
|
|
168
|
+
const tableName = TABLE_NAME_MAP[model];
|
|
169
|
+
if (!tableName) {
|
|
170
|
+
throw new Error(`Model "${model}" does not support hard delete`);
|
|
171
|
+
}
|
|
172
|
+
logger_1.logger.warn('Hard delete: Permanently removing record', { model, id });
|
|
173
|
+
return prisma.$executeRawUnsafe(`DELETE FROM "${tableName}" WHERE "id" = $1`, id);
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=soft-delete.js.map
|