dcl-ops-lib 9.3.1 → 9.5.0
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/StaticWebsite.d.ts +1 -0
- package/buildStatic.js +1 -0
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/package.json +2 -1
- package/rateLimiting.d.ts +84 -0
- package/rateLimiting.js +120 -0
package/StaticWebsite.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ export type StaticWebsite = {
|
|
|
30
30
|
destroy?: boolean;
|
|
31
31
|
priceClass?: "PriceClass_All" | "PriceClass_200" | "PriceClass_100";
|
|
32
32
|
cachePolicyId?: Input<string>;
|
|
33
|
+
responseHeadersPolicyId?: Input<string>;
|
|
33
34
|
objectOwnership?: string;
|
|
34
35
|
overrideContentBucketACL?: string;
|
|
35
36
|
};
|
package/buildStatic.js
CHANGED
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -41,6 +41,7 @@ __exportStar(require("./lambda"), exports);
|
|
|
41
41
|
__exportStar(require("./network"), exports);
|
|
42
42
|
__exportStar(require("./nlb"), exports);
|
|
43
43
|
__exportStar(require("./prometheus"), exports);
|
|
44
|
+
__exportStar(require("./rateLimiting"), exports);
|
|
44
45
|
__exportStar(require("./scheduledTaskBase"), exports);
|
|
45
46
|
__exportStar(require("./secrets"), exports);
|
|
46
47
|
__exportStar(require("./slack"), exports);
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dcl-ops-lib",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.5.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"build": "tsc && cp bin/* . && node test.js",
|
|
6
|
+
"test": "tsc -p tsconfig.test.json && ENVIRONMENT=dev node bin-test/rateLimiting.test.js",
|
|
6
7
|
"clean": "rm *.d.ts *.js *.js.map"
|
|
7
8
|
},
|
|
8
9
|
"files": [
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as cf from "@pulumi/cloudflare";
|
|
2
|
+
export type RateLimitRule = {
|
|
3
|
+
/** The maximum number of requests allowed within the period. */
|
|
4
|
+
requestsPerPeriod: number;
|
|
5
|
+
/** The time window in seconds to count requests (10, 60, 120, 300, or 600). */
|
|
6
|
+
period: 10 | 60 | 120 | 300 | 600;
|
|
7
|
+
/** The action to take when the threshold is exceeded. */
|
|
8
|
+
action: "block" | "challenge" | "managed_challenge" | "js_challenge" | "log";
|
|
9
|
+
/**
|
|
10
|
+
* How long (in seconds) to block requests after the threshold is exceeded.
|
|
11
|
+
* Required when action is "block". Must be >= period. Ignored for other actions.
|
|
12
|
+
*/
|
|
13
|
+
mitigationTimeout?: number;
|
|
14
|
+
/**
|
|
15
|
+
* A Cloudflare Firewall Rules expression to match requests.
|
|
16
|
+
* The hostname condition is prepended automatically.
|
|
17
|
+
* Examples:
|
|
18
|
+
* - `http.request.uri.path eq "/login"` — matches a single path
|
|
19
|
+
* - `http.request.uri.path matches "^/api/"` — matches a path prefix with regex
|
|
20
|
+
* - `http.request.uri.path eq "/login" and http.request.method eq "POST"` — path + method
|
|
21
|
+
*
|
|
22
|
+
* If not provided, matches all traffic for the domain.
|
|
23
|
+
* @see https://developers.cloudflare.com/ruleset-engine/rules-language/
|
|
24
|
+
*/
|
|
25
|
+
expression?: string;
|
|
26
|
+
/** Optional description for the rule. */
|
|
27
|
+
description?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Whether to count all requests (including those served from CF cache) towards
|
|
30
|
+
* the rate limit threshold. Defaults to true.
|
|
31
|
+
*
|
|
32
|
+
* When false, only requests forwarded to the origin count. An attacker hitting
|
|
33
|
+
* a cached endpoint would not trigger the rate limit.
|
|
34
|
+
*/
|
|
35
|
+
countCachedRequests?: boolean;
|
|
36
|
+
};
|
|
37
|
+
export type ServiceRateLimitConfig = {
|
|
38
|
+
/** The service name, used for resource naming and descriptions. */
|
|
39
|
+
serviceName: string;
|
|
40
|
+
/** The public hostname for this service (e.g. "my-service.decentraland.org"). */
|
|
41
|
+
hostname: string;
|
|
42
|
+
/** The rate limiting rules for this service. */
|
|
43
|
+
rules: RateLimitRule[];
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Validates a single rate limit rule and returns a Cloudflare Ruleset rule object.
|
|
47
|
+
*
|
|
48
|
+
* @param globalIndex - A unique index across all rules in the ruleset, used for
|
|
49
|
+
* resource naming and error messages.
|
|
50
|
+
* @throws if the rule configuration is invalid.
|
|
51
|
+
* @internal Exported for testing.
|
|
52
|
+
*/
|
|
53
|
+
export declare function buildRulesetRule(serviceName: string, hostname: string, rule: RateLimitRule, globalIndex: number): {
|
|
54
|
+
action: string;
|
|
55
|
+
expression: string;
|
|
56
|
+
description: string;
|
|
57
|
+
enabled: boolean;
|
|
58
|
+
ratelimit: {
|
|
59
|
+
characteristics: string[];
|
|
60
|
+
period: 120 | 60 | 10 | 300 | 600;
|
|
61
|
+
requestsPerPeriod: number;
|
|
62
|
+
mitigationTimeout: number;
|
|
63
|
+
requestsToOrigin: boolean;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
/** @internal Reset module state for testing. */
|
|
67
|
+
export declare function _resetForTesting(): void;
|
|
68
|
+
/**
|
|
69
|
+
* Creates a single zone-level Cloudflare Ruleset containing rate limiting rules
|
|
70
|
+
* aggregated from all services. This must be called from a centralized Pulumi
|
|
71
|
+
* stack — not from individual service stacks — because Cloudflare only supports
|
|
72
|
+
* one `http_ratelimit` phase ruleset per zone.
|
|
73
|
+
*
|
|
74
|
+
* This function must only be called once per Pulumi program. A second call will
|
|
75
|
+
* throw to prevent silent overwrites of the zone-level ruleset.
|
|
76
|
+
*
|
|
77
|
+
* Rules are ordered so that rules with a custom expression (narrower scope) are
|
|
78
|
+
* evaluated before hostname-only rules (broader scope). Within each group, the
|
|
79
|
+
* original insertion order is preserved.
|
|
80
|
+
*
|
|
81
|
+
* @param services - Array of service rate limiting configurations to aggregate.
|
|
82
|
+
* @returns The created Ruleset, or undefined if no services have rate limiting rules.
|
|
83
|
+
*/
|
|
84
|
+
export declare function createZoneRateLimitRuleset(services: ServiceRateLimitConfig[]): Promise<cf.Ruleset | undefined>;
|
package/rateLimiting.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createZoneRateLimitRuleset = exports._resetForTesting = exports.buildRulesetRule = void 0;
|
|
4
|
+
const cf = require("@pulumi/cloudflare");
|
|
5
|
+
const domain_1 = require("./domain");
|
|
6
|
+
const cloudflare_1 = require("./cloudflare");
|
|
7
|
+
const ACTION_MAP = {
|
|
8
|
+
block: "block",
|
|
9
|
+
challenge: "challenge",
|
|
10
|
+
managed_challenge: "managedChallenge",
|
|
11
|
+
js_challenge: "jsChallenge",
|
|
12
|
+
log: "log",
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Matches a valid hostname: labels separated by dots, each label alphanumeric
|
|
16
|
+
* or hyphen (no leading/trailing hyphen), ending in a TLD of 2+ chars.
|
|
17
|
+
*/
|
|
18
|
+
const HOSTNAME_RE = /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/i;
|
|
19
|
+
const MAX_RULES_PER_ZONE = 100;
|
|
20
|
+
/**
|
|
21
|
+
* Validates a single rate limit rule and returns a Cloudflare Ruleset rule object.
|
|
22
|
+
*
|
|
23
|
+
* @param globalIndex - A unique index across all rules in the ruleset, used for
|
|
24
|
+
* resource naming and error messages.
|
|
25
|
+
* @throws if the rule configuration is invalid.
|
|
26
|
+
* @internal Exported for testing.
|
|
27
|
+
*/
|
|
28
|
+
function buildRulesetRule(serviceName, hostname, rule, globalIndex) {
|
|
29
|
+
if (!hostname || !HOSTNAME_RE.test(hostname)) {
|
|
30
|
+
throw new Error(`Service "${serviceName}" rate limit rule ${globalIndex}: invalid hostname "${hostname}". ` +
|
|
31
|
+
`Must be a valid domain (e.g. "my-service.decentraland.org").`);
|
|
32
|
+
}
|
|
33
|
+
if (!rule.requestsPerPeriod || rule.requestsPerPeriod < 1) {
|
|
34
|
+
throw new Error(`Service "${serviceName}" rate limit rule ${globalIndex}: requestsPerPeriod must be >= 1, got ${rule.requestsPerPeriod}.`);
|
|
35
|
+
}
|
|
36
|
+
if (rule.action === "block") {
|
|
37
|
+
if (rule.mitigationTimeout == null) {
|
|
38
|
+
throw new Error(`Service "${serviceName}" rate limit rule ${globalIndex}: mitigationTimeout is required when action is "block".`);
|
|
39
|
+
}
|
|
40
|
+
if (rule.mitigationTimeout < rule.period) {
|
|
41
|
+
throw new Error(`Service "${serviceName}" rate limit rule ${globalIndex}: mitigationTimeout (${rule.mitigationTimeout}) must be >= period (${rule.period}).`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const hostnameCondition = `(http.host eq "${hostname}")`;
|
|
45
|
+
const expression = rule.expression
|
|
46
|
+
? `${hostnameCondition} and (${rule.expression})`
|
|
47
|
+
: hostnameCondition;
|
|
48
|
+
return {
|
|
49
|
+
action: ACTION_MAP[rule.action],
|
|
50
|
+
expression,
|
|
51
|
+
description: rule.description || `Rate limit rule ${globalIndex} (auto-created by services-pipeline for ${serviceName})`,
|
|
52
|
+
enabled: true,
|
|
53
|
+
ratelimit: {
|
|
54
|
+
characteristics: ["cf.colo.id", "ip.src"],
|
|
55
|
+
period: rule.period,
|
|
56
|
+
requestsPerPeriod: rule.requestsPerPeriod,
|
|
57
|
+
mitigationTimeout: rule.action === "block" ? rule.mitigationTimeout : 0,
|
|
58
|
+
requestsToOrigin: rule.countCachedRequests === false,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
exports.buildRulesetRule = buildRulesetRule;
|
|
63
|
+
let rulesetCreated = false;
|
|
64
|
+
/** @internal Reset module state for testing. */
|
|
65
|
+
function _resetForTesting() {
|
|
66
|
+
rulesetCreated = false;
|
|
67
|
+
}
|
|
68
|
+
exports._resetForTesting = _resetForTesting;
|
|
69
|
+
/**
|
|
70
|
+
* Creates a single zone-level Cloudflare Ruleset containing rate limiting rules
|
|
71
|
+
* aggregated from all services. This must be called from a centralized Pulumi
|
|
72
|
+
* stack — not from individual service stacks — because Cloudflare only supports
|
|
73
|
+
* one `http_ratelimit` phase ruleset per zone.
|
|
74
|
+
*
|
|
75
|
+
* This function must only be called once per Pulumi program. A second call will
|
|
76
|
+
* throw to prevent silent overwrites of the zone-level ruleset.
|
|
77
|
+
*
|
|
78
|
+
* Rules are ordered so that rules with a custom expression (narrower scope) are
|
|
79
|
+
* evaluated before hostname-only rules (broader scope). Within each group, the
|
|
80
|
+
* original insertion order is preserved.
|
|
81
|
+
*
|
|
82
|
+
* @param services - Array of service rate limiting configurations to aggregate.
|
|
83
|
+
* @returns The created Ruleset, or undefined if no services have rate limiting rules.
|
|
84
|
+
*/
|
|
85
|
+
async function createZoneRateLimitRuleset(services) {
|
|
86
|
+
if (services.length === 0)
|
|
87
|
+
return undefined;
|
|
88
|
+
if (rulesetCreated) {
|
|
89
|
+
throw new Error("createZoneRateLimitRuleset has already been called in this program. " +
|
|
90
|
+
"Cloudflare only supports one http_ratelimit phase ruleset per zone.");
|
|
91
|
+
}
|
|
92
|
+
rulesetCreated = true;
|
|
93
|
+
const zoneId = await (0, cloudflare_1.getZoneId)();
|
|
94
|
+
let globalIndex = 0;
|
|
95
|
+
const rules = services.flatMap((svc) => svc.rules.map((rule) => buildRulesetRule(svc.serviceName, svc.hostname, rule, globalIndex++)));
|
|
96
|
+
if (rules.length > MAX_RULES_PER_ZONE) {
|
|
97
|
+
throw new Error(`Total rate limiting rules (${rules.length}) exceeds the maximum of ${MAX_RULES_PER_ZONE} per zone. ` +
|
|
98
|
+
`Reduce the number of rules across services.`);
|
|
99
|
+
}
|
|
100
|
+
// Sort: rules with a custom expression (narrower scope) first, then
|
|
101
|
+
// hostname-only rules (broader scope). Preserves relative order within
|
|
102
|
+
// each group via stable sort.
|
|
103
|
+
const sorted = rules.sort((a, b) => {
|
|
104
|
+
const aHasExpr = a.expression.includes(" and ");
|
|
105
|
+
const bHasExpr = b.expression.includes(" and ");
|
|
106
|
+
if (aHasExpr === bHasExpr)
|
|
107
|
+
return 0;
|
|
108
|
+
return aHasExpr ? -1 : 1;
|
|
109
|
+
});
|
|
110
|
+
return new cf.Ruleset(`zone-rate-limits-${domain_1.publicDomain}`, {
|
|
111
|
+
zoneId,
|
|
112
|
+
name: `Rate limits for ${domain_1.publicDomain}`,
|
|
113
|
+
description: `Auto-created by services-pipeline — aggregated rate limiting rules for all services`,
|
|
114
|
+
kind: "zone",
|
|
115
|
+
phase: "http_ratelimit",
|
|
116
|
+
rules: sorted,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
exports.createZoneRateLimitRuleset = createZoneRateLimitRuleset;
|
|
120
|
+
//# sourceMappingURL=rateLimiting.js.map
|