@digitraffic/common 2026.8.6-1 → 2026.8.31-1
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/dist/__test__/infra/acl-builder.test.js +18 -0
- package/dist/aws/infra/acl-builder.d.ts +21 -2
- package/dist/aws/infra/acl-builder.js +54 -6
- package/dist/aws/infra/stack/dt-function.js +6 -4
- package/dist/aws/runtime/dt-logger.d.ts +1 -0
- package/dist/aws/runtime/dt-logger.js +5 -0
- package/dist/aws/runtime/environment.d.ts +1 -0
- package/dist/aws/runtime/environment.js +1 -0
- package/package.json +1 -1
|
@@ -51,6 +51,24 @@ describe("acl-builder tests", () => {
|
|
|
51
51
|
expect(throttleRule.action.block).toBeDefined();
|
|
52
52
|
}
|
|
53
53
|
});
|
|
54
|
+
test("anonymous uri-path throttle can exclude a path", () => {
|
|
55
|
+
const acl = createBuilder()
|
|
56
|
+
.withThrottleAnonymousUserIpAndUriPath(100, /^\/roadnetwork\//)
|
|
57
|
+
.build();
|
|
58
|
+
const rule = acl.rules[0];
|
|
59
|
+
const statement = rule?.statement;
|
|
60
|
+
const rateBased = statement.rateBasedStatement;
|
|
61
|
+
const scopeDown = rateBased.scopeDownStatement;
|
|
62
|
+
const scopeDownStatement = scopeDown;
|
|
63
|
+
const andStatement = scopeDownStatement.andStatement;
|
|
64
|
+
const scopeDownStatements = (andStatement.statements ??
|
|
65
|
+
[]);
|
|
66
|
+
const excludedStatement = scopeDownStatements[1];
|
|
67
|
+
const excludedPathStatement = excludedStatement.notStatement;
|
|
68
|
+
const regexMatchStatement = excludedPathStatement.statement.regexMatchStatement;
|
|
69
|
+
expect(scopeDownStatements).toHaveLength(2);
|
|
70
|
+
expect(regexMatchStatement.regexString).toBe("^\\/roadnetwork\\/");
|
|
71
|
+
});
|
|
54
72
|
test("Cannot define two rules with the same name", () => {
|
|
55
73
|
expect(() => createBuilder()
|
|
56
74
|
.withThrottleAnonymousUserIp(10)
|
|
@@ -32,13 +32,32 @@ export declare class AclBuilder {
|
|
|
32
32
|
* Allow access only from the given addresses
|
|
33
33
|
*/
|
|
34
34
|
withIpWhitelistRule(addresses: string[]): this;
|
|
35
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Adds a rate-based WAF rule for either blocking or counting requests.
|
|
37
|
+
*
|
|
38
|
+
* @param name WAF rule name and CloudWatch metric name.
|
|
39
|
+
* @param limit Maximum requests allowed in the WAF evaluation window.
|
|
40
|
+
* @param isHeaderRequired Whether the rule applies when the digitraffic-user header is present.
|
|
41
|
+
* @param isBasedOnIpAndUriPath Whether requests are aggregated by IP address and URI path.
|
|
42
|
+
* @param customResponseBodyKey Custom response body key; when provided, matching requests are blocked instead of counted.
|
|
43
|
+
* @param path Optional URI path pattern that limits which requests are included.
|
|
44
|
+
* @param excludedPath Optional URI path pattern that excludes matching requests from the rule.
|
|
45
|
+
*/
|
|
46
|
+
withThrottleRule(name: string, limit: number, isHeaderRequired: boolean, isBasedOnIpAndUriPath: boolean, customResponseBodyKey?: string, path?: RegExp, excludedPath?: RegExp): this;
|
|
36
47
|
withCustomResponseBody(key: string, customResponseBody: CfnWebACL.CustomResponseBodyProperty): this;
|
|
37
48
|
withThrottleDigitrafficUserIp(limit: number | undefined): this;
|
|
38
49
|
withThrottleDigitrafficUserIpAndUriPath(limit: number | undefined): this;
|
|
39
50
|
withThrottleAnonymousUserIp(limit: number | undefined): AclBuilder;
|
|
40
51
|
withThrottleAnonymousUserIpByUriPath(limit: number | undefined, path: RegExp | undefined): AclBuilder;
|
|
41
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Throttles anonymous requests by IP address and URI path.
|
|
54
|
+
*
|
|
55
|
+
* Existing callers that omit `excludedPath` retain the previous behavior.
|
|
56
|
+
*
|
|
57
|
+
* @param limit Maximum requests allowed for each IP and URI path combination.
|
|
58
|
+
* @param excludedPath Optional URI path pattern excluded from this throttle rule.
|
|
59
|
+
*/
|
|
60
|
+
withThrottleAnonymousUserIpAndUriPath(limit: number | undefined, excludedPath?: RegExp): this;
|
|
42
61
|
withCountDigitrafficUserIp(limit: number | undefined): this;
|
|
43
62
|
withCountDigitrafficUserIpAndUriPath(limit: number | undefined): this;
|
|
44
63
|
withCountAnonymousUserIp(limit: number | undefined): this;
|
|
@@ -91,7 +91,18 @@ export class AclBuilder {
|
|
|
91
91
|
});
|
|
92
92
|
return this;
|
|
93
93
|
}
|
|
94
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Adds a rate-based WAF rule for either blocking or counting requests.
|
|
96
|
+
*
|
|
97
|
+
* @param name WAF rule name and CloudWatch metric name.
|
|
98
|
+
* @param limit Maximum requests allowed in the WAF evaluation window.
|
|
99
|
+
* @param isHeaderRequired Whether the rule applies when the digitraffic-user header is present.
|
|
100
|
+
* @param isBasedOnIpAndUriPath Whether requests are aggregated by IP address and URI path.
|
|
101
|
+
* @param customResponseBodyKey Custom response body key; when provided, matching requests are blocked instead of counted.
|
|
102
|
+
* @param path Optional URI path pattern that limits which requests are included.
|
|
103
|
+
* @param excludedPath Optional URI path pattern that excludes matching requests from the rule.
|
|
104
|
+
*/
|
|
105
|
+
withThrottleRule(name, limit, isHeaderRequired, isBasedOnIpAndUriPath, customResponseBodyKey, path, excludedPath) {
|
|
95
106
|
const isBlockRule = !!customResponseBodyKey;
|
|
96
107
|
const rules = isBlockRule ? this._blockRules : this._countRules;
|
|
97
108
|
const action = isBlockRule
|
|
@@ -114,7 +125,7 @@ export class AclBuilder {
|
|
|
114
125
|
metricName: name,
|
|
115
126
|
},
|
|
116
127
|
action,
|
|
117
|
-
statement: createThrottleStatement(limit, isHeaderRequired, isBasedOnIpAndUriPath, path),
|
|
128
|
+
statement: createThrottleStatement(limit, isHeaderRequired, isBasedOnIpAndUriPath, path, excludedPath),
|
|
118
129
|
});
|
|
119
130
|
return this;
|
|
120
131
|
}
|
|
@@ -160,13 +171,21 @@ export class AclBuilder {
|
|
|
160
171
|
this._addThrottleResponseBody(customResponseBodyKey, limit);
|
|
161
172
|
return this.withThrottleRule("ThrottleRuleWithAnonymousUserByPath", limit, false, false, customResponseBodyKey, path);
|
|
162
173
|
}
|
|
163
|
-
|
|
174
|
+
/**
|
|
175
|
+
* Throttles anonymous requests by IP address and URI path.
|
|
176
|
+
*
|
|
177
|
+
* Existing callers that omit `excludedPath` retain the previous behavior.
|
|
178
|
+
*
|
|
179
|
+
* @param limit Maximum requests allowed for each IP and URI path combination.
|
|
180
|
+
* @param excludedPath Optional URI path pattern excluded from this throttle rule.
|
|
181
|
+
*/
|
|
182
|
+
withThrottleAnonymousUserIpAndUriPath(limit, excludedPath) {
|
|
164
183
|
if (limit === undefined) {
|
|
165
184
|
return this;
|
|
166
185
|
}
|
|
167
186
|
const customResponseBodyKey = `IP_PATH_THROTTLE_ANONYMOUS_USER_${limit}`;
|
|
168
187
|
this._addThrottleResponseBody(customResponseBodyKey, limit);
|
|
169
|
-
return this.withThrottleRule("ThrottleRuleIPQueryWithAnonymousUser", limit, false, true, customResponseBodyKey);
|
|
188
|
+
return this.withThrottleRule("ThrottleRuleIPQueryWithAnonymousUser", limit, false, true, customResponseBodyKey, undefined, excludedPath);
|
|
170
189
|
}
|
|
171
190
|
withCountDigitrafficUserIp(limit) {
|
|
172
191
|
if (limit === undefined) {
|
|
@@ -289,8 +308,18 @@ function notStatement(statement) {
|
|
|
289
308
|
},
|
|
290
309
|
};
|
|
291
310
|
}
|
|
292
|
-
|
|
293
|
-
|
|
311
|
+
/**
|
|
312
|
+
* Creates the scope-down and rate-based statement used by a throttle rule.
|
|
313
|
+
*
|
|
314
|
+
* @param limit Maximum requests allowed in the WAF evaluation window.
|
|
315
|
+
* @param isHeaderRequired Whether the statement matches requests with the digitraffic-user header.
|
|
316
|
+
* @param isBasedOnIpAndUriPath Whether requests are aggregated by IP address and URI path.
|
|
317
|
+
* @param path Optional URI path pattern that limits which requests are included.
|
|
318
|
+
* @param excludedPath Optional URI path pattern that is explicitly excluded from the statement.
|
|
319
|
+
*/
|
|
320
|
+
function createThrottleStatement(limit, isHeaderRequired, isBasedOnIpAndUriPath, path, excludedPath) {
|
|
321
|
+
// Matches requests with a non-empty digitraffic-user header. For anonymous
|
|
322
|
+
// requests, the inverted statement matches requests without the header.
|
|
294
323
|
const matchStatement = {
|
|
295
324
|
sizeConstraintStatement: {
|
|
296
325
|
comparisonOperator: isHeaderRequired ? "GT" : "GE",
|
|
@@ -324,6 +353,25 @@ function createThrottleStatement(limit, isHeaderRequired, isBasedOnIpAndUriPath,
|
|
|
324
353
|
},
|
|
325
354
|
};
|
|
326
355
|
}
|
|
356
|
+
if (excludedPath) {
|
|
357
|
+
const excludedPathMatchStatement = {
|
|
358
|
+
regexMatchStatement: {
|
|
359
|
+
fieldToMatch: {
|
|
360
|
+
uriPath: {},
|
|
361
|
+
},
|
|
362
|
+
regexString: excludedPath.source,
|
|
363
|
+
textTransformations: [{ priority: 0, type: "NONE" }],
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
scopeDownStatement = {
|
|
367
|
+
andStatement: {
|
|
368
|
+
statements: [
|
|
369
|
+
scopeDownStatement,
|
|
370
|
+
notStatement(excludedPathMatchStatement),
|
|
371
|
+
],
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
}
|
|
327
375
|
if (isBasedOnIpAndUriPath) {
|
|
328
376
|
return {
|
|
329
377
|
rateBasedStatement: {
|
|
@@ -3,7 +3,7 @@ import { Duration } from "aws-cdk-lib";
|
|
|
3
3
|
import { SnsAction } from "aws-cdk-lib/aws-cloudwatch-actions";
|
|
4
4
|
import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
5
5
|
import { ApplicationLogLevel, Architecture, AssetCode, Function as AwsFunction, Code, LoggingFormat, Runtime, SystemLogLevel, } from "aws-cdk-lib/aws-lambda";
|
|
6
|
-
import { camelCase, startCase } from "es-toolkit";
|
|
6
|
+
import { camelCase, kebabCase, startCase } from "es-toolkit";
|
|
7
7
|
import { DatabaseEnvironmentKeys } from "../../../database/database.js";
|
|
8
8
|
import { EnvKeys } from "../../runtime/environment.js";
|
|
9
9
|
import { DtFunctionAlarms } from "./dt-function-alarms.js";
|
|
@@ -260,14 +260,16 @@ export class FunctionBuilder {
|
|
|
260
260
|
}
|
|
261
261
|
}
|
|
262
262
|
getEnvironment() {
|
|
263
|
-
let environment = {
|
|
264
|
-
|
|
263
|
+
let environment = {
|
|
264
|
+
[EnvKeys.APP_NAME]: `${this._stack.configuration.trafficType.toLowerCase()}-${kebabCase(this._stack.configuration.shortName)}`,
|
|
265
|
+
};
|
|
266
|
+
if (this._features.secretAccess && this._stack.configuration.secretId) {
|
|
265
267
|
environment = {
|
|
266
268
|
...environment,
|
|
267
269
|
[EnvKeys.SECRET_ID]: this._stack.configuration.secretId,
|
|
268
270
|
};
|
|
269
271
|
}
|
|
270
|
-
if (this._features.databaseAccess) {
|
|
272
|
+
if (this._features.databaseAccess && this._stack.configuration.shortName) {
|
|
271
273
|
environment = {
|
|
272
274
|
...environment,
|
|
273
275
|
[DatabaseEnvironmentKeys.DB_APPLICATION]: this._stack.configuration.shortName,
|
|
@@ -61,6 +61,7 @@ export interface LoggableType extends CustomParams {
|
|
|
61
61
|
* * the actual message (as json or as string)
|
|
62
62
|
*/
|
|
63
63
|
export declare class DtLogger {
|
|
64
|
+
readonly appName?: string;
|
|
64
65
|
readonly lambdaName?: string;
|
|
65
66
|
readonly runtime?: string;
|
|
66
67
|
readonly writeStream: Writable;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { lowerFirst, mapKeys } from "es-toolkit";
|
|
2
2
|
import { getEnvVariableOrElse } from "../../utils/utils.js";
|
|
3
|
+
import { EnvKeys } from "./environment.js";
|
|
3
4
|
/**
|
|
4
5
|
* Helper class for json-logging.
|
|
5
6
|
*
|
|
@@ -10,6 +11,7 @@ import { getEnvVariableOrElse } from "../../utils/utils.js";
|
|
|
10
11
|
* * the actual message (as json or as string)
|
|
11
12
|
*/
|
|
12
13
|
export class DtLogger {
|
|
14
|
+
appName;
|
|
13
15
|
lambdaName;
|
|
14
16
|
runtime;
|
|
15
17
|
writeStream;
|
|
@@ -19,6 +21,7 @@ export class DtLogger {
|
|
|
19
21
|
* @param {LoggerConfiguration?} [config] - Accepts configuration options @see {@link LoggerConfiguration}
|
|
20
22
|
*/
|
|
21
23
|
constructor(config) {
|
|
24
|
+
this.appName = getEnvVariableOrElse(EnvKeys.APP_NAME, undefined);
|
|
22
25
|
this.lambdaName =
|
|
23
26
|
config?.lambdaName ??
|
|
24
27
|
getEnvVariableOrElse("AWS_LAMBDA_FUNCTION_NAME", "unknown lambda name");
|
|
@@ -38,6 +41,7 @@ export class DtLogger {
|
|
|
38
41
|
const logMessage = {
|
|
39
42
|
message,
|
|
40
43
|
level: "DEBUG",
|
|
44
|
+
app: this.appName,
|
|
41
45
|
lambdaName: this.lambdaName,
|
|
42
46
|
runtime: this.runtime,
|
|
43
47
|
};
|
|
@@ -112,6 +116,7 @@ export class DtLogger {
|
|
|
112
116
|
...messageFields,
|
|
113
117
|
error,
|
|
114
118
|
stack,
|
|
119
|
+
app: this.appName,
|
|
115
120
|
lambdaName: this.lambdaName,
|
|
116
121
|
runtime: this.runtime,
|
|
117
122
|
};
|