@prosopo/user-access-policy 3.14.14 → 3.14.16
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/.turbo/turbo-build$colon$cjs.log +3 -6
- package/.turbo/turbo-build$colon$tsc.log +14 -17
- package/.turbo/turbo-build.log +4 -10
- package/CHANGELOG.md +37 -0
- package/LICENSE +201 -0
- package/dist/cjs/ruleInput/policyInput.cjs +1 -1
- package/dist/ruleInput/policyInput.d.ts +2 -2
- package/dist/ruleInput/policyInput.d.ts.map +1 -1
- package/dist/ruleInput/policyInput.js +2 -2
- package/dist/ruleInput/policyInput.js.map +1 -1
- package/package.json +83 -81
- package/src/.export.ts +0 -65
- package/src/api/.export.ts +0 -25
- package/src/api/accessRulesApiClient.ts +0 -13
- package/src/api/delete/.export.ts +0 -18
- package/src/api/delete/deleteAllRules.ts +0 -47
- package/src/api/delete/deleteRuleGroups.ts +0 -96
- package/src/api/delete/deleteRules.ts +0 -87
- package/src/api/read/.export.ts +0 -25
- package/src/api/read/fetchRules.ts +0 -96
- package/src/api/read/findRuleIds.ts +0 -97
- package/src/api/read/getMissingIds.ts +0 -81
- package/src/api/ruleApiRoutes.ts +0 -146
- package/src/api/rulesApiClient.ts +0 -154
- package/src/api/write/.export.ts +0 -15
- package/src/api/write/insertRules.ts +0 -185
- package/src/api/write/rehashRules.ts +0 -85
- package/src/classifyBrowser.ts +0 -77
- package/src/classifyOs.ts +0 -74
- package/src/headerMatch.ts +0 -180
- package/src/matchedRule.ts +0 -99
- package/src/mongoose/.export.ts +0 -15
- package/src/mongoose/mongooseRuleSchema.ts +0 -75
- package/src/redis/.export.ts +0 -17
- package/src/redis/reader/redisAggregate.ts +0 -157
- package/src/redis/reader/redisRulesQuery.ts +0 -268
- package/src/redis/reader/redisRulesReader.ts +0 -738
- package/src/redis/reader/redisRulesSplitQuery.ts +0 -234
- package/src/redis/redisClient.ts +0 -125
- package/src/redis/redisRuleIndex.ts +0 -121
- package/src/redis/redisRulesStorage.ts +0 -68
- package/src/redis/redisRulesWriter.ts +0 -172
- package/src/rule.ts +0 -122
- package/src/ruleInput/.export.ts +0 -19
- package/src/ruleInput/policyInput.ts +0 -120
- package/src/ruleInput/ruleInput.ts +0 -123
- package/src/ruleInput/userScopeInput.ts +0 -142
- package/src/ruleRecord.ts +0 -77
- package/src/rulesStorage.ts +0 -90
- package/src/tests/blockingRuleShapes.unit.test.ts +0 -244
- package/src/tests/classifyBrowser.unit.test.ts +0 -138
- package/src/tests/classifyOs.unit.test.ts +0 -97
- package/src/tests/headerMatch.unit.test.ts +0 -350
- package/src/tests/insertRulesEndpoint.unit.test.ts +0 -89
- package/src/tests/matchedRule.unit.test.ts +0 -184
- package/src/tests/policyInput.unit.test.ts +0 -218
- package/src/tests/redis/deferredPoolClause.benchmark.ts +0 -249
- package/src/tests/redis/reader/redisAggregate.unit.test.ts +0 -121
- package/src/tests/redis/reader/redisRulesQuery.unit.test.ts +0 -349
- package/src/tests/redis/reader/redisRulesSplitQuery.unit.test.ts +0 -297
- package/src/tests/redis/redisRulesReaderLoad.benchmark.integration.test.ts +0 -520
- package/src/tests/redis/redisRulesReaderRank.benchmark.integration.test.ts +0 -350
- package/src/tests/redis/redisRulesStorage.integration.test.ts +0 -1811
- package/src/tests/testLogger.ts +0 -38
- package/src/tests/transformRule.unit.test.ts +0 -345
- package/src/transformRule.ts +0 -133
- package/tsconfig.cjs.json +0 -41
- package/tsconfig.json +0 -47
- package/tsconfig.tsbuildinfo +0 -1
- package/tsconfig.types.json +0 -9
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
import { CaptchaType, MatchedAccessRuleSchema } from "@prosopo/types";
|
|
16
|
-
import { Address4 } from "ip-address";
|
|
17
|
-
import { describe, expect, it } from "vitest";
|
|
18
|
-
import { describeMatchedRule } from "#policy/matchedRule.js";
|
|
19
|
-
import { AccessPolicyType, type AccessRule } from "#policy/rule.js";
|
|
20
|
-
import { userScopeRecordFields } from "#policy/ruleRecord.js";
|
|
21
|
-
import { makeAccessRuleHash } from "#policy/transformRule.js";
|
|
22
|
-
|
|
23
|
-
describe("describeMatchedRule", () => {
|
|
24
|
-
it("carries the policy half of the rule", () => {
|
|
25
|
-
const rule: AccessRule = {
|
|
26
|
-
type: AccessPolicyType.Restrict,
|
|
27
|
-
captchaType: CaptchaType.image,
|
|
28
|
-
description: "6 rounds for this ASN",
|
|
29
|
-
deferToVerify: true,
|
|
30
|
-
clientId: "site-key",
|
|
31
|
-
groupId: "group-1",
|
|
32
|
-
ja4Hash: "t13d1516h2_8daaf6152771_b186095e22b6",
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
expect(describeMatchedRule(rule)).toEqual({
|
|
36
|
-
ruleHash: makeAccessRuleHash(rule),
|
|
37
|
-
policyType: AccessPolicyType.Restrict,
|
|
38
|
-
captchaType: CaptchaType.image,
|
|
39
|
-
description: "6 rounds for this ASN",
|
|
40
|
-
deferToVerify: true,
|
|
41
|
-
clientId: "site-key",
|
|
42
|
-
// `groupId` on the rule is `ruleGroupId` everywhere else — the
|
|
43
|
-
// portal joins the audit row back to its Access Control entry on
|
|
44
|
-
// this, so the rename has to happen here.
|
|
45
|
-
ruleGroupId: "group-1",
|
|
46
|
-
conditions: [
|
|
47
|
-
{
|
|
48
|
-
field: "ja4Hash",
|
|
49
|
-
value: "t13d1516h2_8daaf6152771_b186095e22b6",
|
|
50
|
-
},
|
|
51
|
-
],
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("omits policy fields the rule did not set rather than emitting undefined", () => {
|
|
56
|
-
const described = describeMatchedRule({ type: AccessPolicyType.Block });
|
|
57
|
-
|
|
58
|
-
expect(described).toEqual({
|
|
59
|
-
ruleHash: makeAccessRuleHash({ type: AccessPolicyType.Block }),
|
|
60
|
-
policyType: AccessPolicyType.Block,
|
|
61
|
-
conditions: [],
|
|
62
|
-
});
|
|
63
|
-
expect(Object.keys(described)).not.toContain("description");
|
|
64
|
-
expect(Object.keys(described)).not.toContain("ruleGroupId");
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("reports a single IP in the form the operator typed, not the numeric one", () => {
|
|
68
|
-
const { conditions } = describeMatchedRule({
|
|
69
|
-
type: AccessPolicyType.Block,
|
|
70
|
-
numericIp: new Address4("192.0.2.44").bigInt(),
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
expect(conditions).toEqual([{ field: "ip", value: "192.0.2.44" }]);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("collapses an IP range back to its CIDR", () => {
|
|
77
|
-
const { conditions } = describeMatchedRule({
|
|
78
|
-
type: AccessPolicyType.Block,
|
|
79
|
-
numericIpMaskMin: new Address4("198.51.100.64").bigInt(),
|
|
80
|
-
numericIpMaskMax: new Address4("198.51.100.127").bigInt(),
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
expect(conditions).toEqual([
|
|
84
|
-
{ field: "ipMask", value: "198.51.100.64/26" },
|
|
85
|
-
]);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("stringifies a numeric asn", () => {
|
|
89
|
-
const { conditions } = describeMatchedRule({
|
|
90
|
-
type: AccessPolicyType.Block,
|
|
91
|
-
asn: 205016,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
expect(conditions).toEqual([{ field: "asn", value: "205016" }]);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("reports every populated scope field, in record vocabulary", () => {
|
|
98
|
-
const { conditions } = describeMatchedRule({
|
|
99
|
-
type: AccessPolicyType.Block,
|
|
100
|
-
userId: "user",
|
|
101
|
-
ja4Hash: "ja4",
|
|
102
|
-
headersHash: "headers",
|
|
103
|
-
// Hashed on the rule, but surfaces under the record-side name the
|
|
104
|
-
// portal labels conditions with.
|
|
105
|
-
userAgentHash: "ua",
|
|
106
|
-
headHash: "head",
|
|
107
|
-
coords: "[[1,2]]",
|
|
108
|
-
countryCode: "GB",
|
|
109
|
-
asn: 1,
|
|
110
|
-
os: "macos",
|
|
111
|
-
numericIp: new Address4("203.0.113.5").bigInt(),
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
expect(conditions.map((condition) => condition.field)).toEqual([
|
|
115
|
-
"userId",
|
|
116
|
-
"ja4Hash",
|
|
117
|
-
"headersHash",
|
|
118
|
-
"userAgent",
|
|
119
|
-
"headHash",
|
|
120
|
-
"coords",
|
|
121
|
-
"countryCode",
|
|
122
|
-
"asn",
|
|
123
|
-
"os",
|
|
124
|
-
"ip",
|
|
125
|
-
]);
|
|
126
|
-
// Every emitted field is one the portal knows how to label.
|
|
127
|
-
for (const condition of conditions) {
|
|
128
|
-
expect(userScopeRecordFields).toContain(condition.field);
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("truncates an oversized coords polygon and flags it", () => {
|
|
133
|
-
const coords = JSON.stringify([Array.from({ length: 200 }, () => [1, 2])]);
|
|
134
|
-
const { conditions } = describeMatchedRule({
|
|
135
|
-
type: AccessPolicyType.Block,
|
|
136
|
-
coords,
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
const [condition] = conditions;
|
|
140
|
-
expect(condition?.truncated).toBe(true);
|
|
141
|
-
expect(condition?.value.length).toBe(257);
|
|
142
|
-
expect(coords.startsWith(condition?.value.slice(0, -1) ?? "")).toBe(true);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("leaves a value at the cap untruncated", () => {
|
|
146
|
-
const coords = "x".repeat(256);
|
|
147
|
-
const { conditions } = describeMatchedRule({
|
|
148
|
-
type: AccessPolicyType.Block,
|
|
149
|
-
coords,
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
expect(conditions).toEqual([{ field: "coords", value: coords }]);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("still describes a rule whose scope cannot be transformed", () => {
|
|
156
|
-
// An inverted mask range makes the record transform throw. Enforcement
|
|
157
|
-
// must not care: the block still happens, the snapshot just lacks
|
|
158
|
-
// conditions.
|
|
159
|
-
const rule: AccessRule = {
|
|
160
|
-
type: AccessPolicyType.Block,
|
|
161
|
-
description: "unrepresentable range",
|
|
162
|
-
numericIpMaskMin: new Address4("198.51.100.200").bigInt(),
|
|
163
|
-
numericIpMaskMax: new Address4("198.51.100.1").bigInt(),
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
expect(() => describeMatchedRule(rule)).not.toThrow();
|
|
167
|
-
expect(describeMatchedRule(rule)).toEqual({
|
|
168
|
-
ruleHash: makeAccessRuleHash(rule),
|
|
169
|
-
policyType: AccessPolicyType.Block,
|
|
170
|
-
description: "unrepresentable range",
|
|
171
|
-
conditions: [],
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it("produces a value the persisted schema accepts", () => {
|
|
176
|
-
const described = describeMatchedRule({
|
|
177
|
-
type: AccessPolicyType.Restrict,
|
|
178
|
-
captchaType: CaptchaType.pow,
|
|
179
|
-
countryCode: "CN",
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
expect(MatchedAccessRuleSchema.safeParse(described).success).toBe(true);
|
|
183
|
-
});
|
|
184
|
-
});
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
import { CaptchaType } from "@prosopo/types";
|
|
16
|
-
import { describe, expect, it } from "vitest";
|
|
17
|
-
import { AccessPolicyType } from "#policy/rule.js";
|
|
18
|
-
import {
|
|
19
|
-
accessPolicyInput,
|
|
20
|
-
sanitizeAccessPolicy,
|
|
21
|
-
} from "#policy/ruleInput/policyInput.js";
|
|
22
|
-
|
|
23
|
-
describe("sanitizeAccessPolicy", () => {
|
|
24
|
-
describe("block policies", () => {
|
|
25
|
-
it("should remove captchaType from block policies", () => {
|
|
26
|
-
const input = {
|
|
27
|
-
type: AccessPolicyType.Block,
|
|
28
|
-
captchaType: CaptchaType.image,
|
|
29
|
-
description: "test block policy",
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const result = sanitizeAccessPolicy(input);
|
|
33
|
-
|
|
34
|
-
expect(result).toEqual({
|
|
35
|
-
type: AccessPolicyType.Block,
|
|
36
|
-
description: "test block policy",
|
|
37
|
-
});
|
|
38
|
-
expect(result.captchaType).toBeUndefined();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("should remove solvedImagesCount from block policies", () => {
|
|
42
|
-
const input = {
|
|
43
|
-
type: AccessPolicyType.Block,
|
|
44
|
-
solvedImagesCount: 5,
|
|
45
|
-
description: "test block policy",
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const result = sanitizeAccessPolicy(input);
|
|
49
|
-
|
|
50
|
-
expect(result).toEqual({
|
|
51
|
-
type: AccessPolicyType.Block,
|
|
52
|
-
description: "test block policy",
|
|
53
|
-
});
|
|
54
|
-
expect(result.solvedImagesCount).toBeUndefined();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("should remove both captchaType and solvedImagesCount from block policies", () => {
|
|
58
|
-
const input = {
|
|
59
|
-
type: AccessPolicyType.Block,
|
|
60
|
-
captchaType: CaptchaType.image,
|
|
61
|
-
solvedImagesCount: 5,
|
|
62
|
-
description: "test block policy",
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const result = sanitizeAccessPolicy(input);
|
|
66
|
-
|
|
67
|
-
expect(result).toEqual({
|
|
68
|
-
type: AccessPolicyType.Block,
|
|
69
|
-
description: "test block policy",
|
|
70
|
-
});
|
|
71
|
-
expect(result.captchaType).toBeUndefined();
|
|
72
|
-
expect(result.solvedImagesCount).toBeUndefined();
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("should keep other fields in block policies", () => {
|
|
76
|
-
const input = {
|
|
77
|
-
type: AccessPolicyType.Block,
|
|
78
|
-
captchaType: CaptchaType.image,
|
|
79
|
-
solvedImagesCount: 5,
|
|
80
|
-
description: "test block policy",
|
|
81
|
-
imageThreshold: 0.5,
|
|
82
|
-
powDifficulty: 10,
|
|
83
|
-
unsolvedImagesCount: 3,
|
|
84
|
-
frictionlessScore: 100,
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const result = sanitizeAccessPolicy(input);
|
|
88
|
-
|
|
89
|
-
expect(result).toEqual({
|
|
90
|
-
type: AccessPolicyType.Block,
|
|
91
|
-
description: "test block policy",
|
|
92
|
-
imageThreshold: 0.5,
|
|
93
|
-
powDifficulty: 10,
|
|
94
|
-
unsolvedImagesCount: 3,
|
|
95
|
-
frictionlessScore: 100,
|
|
96
|
-
});
|
|
97
|
-
expect(result.captchaType).toBeUndefined();
|
|
98
|
-
expect(result.solvedImagesCount).toBeUndefined();
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
describe("restrict policies", () => {
|
|
103
|
-
it("should keep captchaType in restrict policies", () => {
|
|
104
|
-
const input = {
|
|
105
|
-
type: AccessPolicyType.Restrict,
|
|
106
|
-
captchaType: CaptchaType.image,
|
|
107
|
-
description: "test restrict policy",
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
const result = sanitizeAccessPolicy(input);
|
|
111
|
-
|
|
112
|
-
expect(result).toEqual({
|
|
113
|
-
type: AccessPolicyType.Restrict,
|
|
114
|
-
captchaType: CaptchaType.image,
|
|
115
|
-
description: "test restrict policy",
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("should keep solvedImagesCount in restrict policies", () => {
|
|
120
|
-
const input = {
|
|
121
|
-
type: AccessPolicyType.Restrict,
|
|
122
|
-
solvedImagesCount: 5,
|
|
123
|
-
description: "test restrict policy",
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
const result = sanitizeAccessPolicy(input);
|
|
127
|
-
|
|
128
|
-
expect(result).toEqual({
|
|
129
|
-
type: AccessPolicyType.Restrict,
|
|
130
|
-
solvedImagesCount: 5,
|
|
131
|
-
description: "test restrict policy",
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("should keep both captchaType and solvedImagesCount in restrict policies", () => {
|
|
136
|
-
const input = {
|
|
137
|
-
type: AccessPolicyType.Restrict,
|
|
138
|
-
captchaType: CaptchaType.image,
|
|
139
|
-
solvedImagesCount: 5,
|
|
140
|
-
description: "test restrict policy",
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
const result = sanitizeAccessPolicy(input);
|
|
144
|
-
|
|
145
|
-
expect(result).toEqual({
|
|
146
|
-
type: AccessPolicyType.Restrict,
|
|
147
|
-
captchaType: CaptchaType.image,
|
|
148
|
-
solvedImagesCount: 5,
|
|
149
|
-
description: "test restrict policy",
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
// Write-time input schema — reject the fields the sanitiser used to
|
|
156
|
-
// silently strip. The prior "silently strip" behaviour meant operators
|
|
157
|
-
// writing `--block --captchaType image` got a rule that blocked EVERY
|
|
158
|
-
// captcha type (not just image); rejecting at input surfaces the
|
|
159
|
-
// mismatch so they can switch to a Restrict policy instead.
|
|
160
|
-
describe("accessPolicyInput refinement", () => {
|
|
161
|
-
it("rejects Block + captchaType with a clear message pointing at Restrict", () => {
|
|
162
|
-
const result = accessPolicyInput.safeParse({
|
|
163
|
-
type: AccessPolicyType.Block,
|
|
164
|
-
captchaType: CaptchaType.image,
|
|
165
|
-
});
|
|
166
|
-
expect(result.success).toBe(false);
|
|
167
|
-
if (!result.success) {
|
|
168
|
-
const issue = result.error.issues.find((i) =>
|
|
169
|
-
i.path.includes("captchaType"),
|
|
170
|
-
);
|
|
171
|
-
expect(issue).toBeDefined();
|
|
172
|
-
expect(issue?.message).toMatch(/Block policies cannot pin a captchaType/);
|
|
173
|
-
expect(issue?.message).toMatch(/Restrict policy/);
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it("rejects Block + solvedImagesCount", () => {
|
|
178
|
-
const result = accessPolicyInput.safeParse({
|
|
179
|
-
type: AccessPolicyType.Block,
|
|
180
|
-
solvedImagesCount: 3,
|
|
181
|
-
});
|
|
182
|
-
expect(result.success).toBe(false);
|
|
183
|
-
if (!result.success) {
|
|
184
|
-
const issue = result.error.issues.find((i) =>
|
|
185
|
-
i.path.includes("solvedImagesCount"),
|
|
186
|
-
);
|
|
187
|
-
expect(issue).toBeDefined();
|
|
188
|
-
expect(issue?.message).toMatch(
|
|
189
|
-
/Block policies cannot set solvedImagesCount/,
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it("accepts Block without the disallowed fields", () => {
|
|
195
|
-
const result = accessPolicyInput.safeParse({
|
|
196
|
-
type: AccessPolicyType.Block,
|
|
197
|
-
description: "clean block",
|
|
198
|
-
});
|
|
199
|
-
expect(result.success).toBe(true);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it("accepts Block + deferToVerify (deferToVerify is orthogonal to the type-pin restriction)", () => {
|
|
203
|
-
const result = accessPolicyInput.safeParse({
|
|
204
|
-
type: AccessPolicyType.Block,
|
|
205
|
-
deferToVerify: true,
|
|
206
|
-
});
|
|
207
|
-
expect(result.success).toBe(true);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it("accepts Restrict + captchaType + solvedImagesCount (these are Restrict's whole point)", () => {
|
|
211
|
-
const result = accessPolicyInput.safeParse({
|
|
212
|
-
type: AccessPolicyType.Restrict,
|
|
213
|
-
captchaType: CaptchaType.image,
|
|
214
|
-
solvedImagesCount: 5,
|
|
215
|
-
});
|
|
216
|
-
expect(result.success).toBe(true);
|
|
217
|
-
});
|
|
218
|
-
});
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
// Standalone benchmark for the hard-block candidate-pool clauses.
|
|
16
|
-
//
|
|
17
|
-
// Compares the previous single-field clause against the two new
|
|
18
|
-
// two-field clauses on the shapes they actually run in:
|
|
19
|
-
//
|
|
20
|
-
// baseline @type:{block} (old, both callers)
|
|
21
|
-
// middleware @type:{block} -@deferToVerify:{true} (new, request time)
|
|
22
|
-
// verify (@type:{block} | @deferToVerify:{true}) (new, verify)
|
|
23
|
-
//
|
|
24
|
-
// Also runs the MISGROUPED union as a control. `|` binds looser than the
|
|
25
|
-
// implicit AND, so `(@a)|(@b) scope ip` lets the left operand escape the
|
|
26
|
-
// scope/IP filters — it matches every Block rule in the index. That
|
|
27
|
-
// misgrouping is both wrong and ~7x slower, and it is invisible on a
|
|
28
|
-
// toy dataset; it only shows up at population scale, which is why this
|
|
29
|
-
// benchmark exists.
|
|
30
|
-
//
|
|
31
|
-
// Population mirrors redisRulesReaderLoad.benchmark.integration.test.ts
|
|
32
|
-
// (the 17k bulk-ban incident shape) plus a deferred-rule cohort.
|
|
33
|
-
//
|
|
34
|
-
// Run: npx tsx <this file> (needs redis-stack on localhost:6379)
|
|
35
|
-
|
|
36
|
-
import { createClient } from "redis";
|
|
37
|
-
|
|
38
|
-
const IP_RULE_COUNT = 17_000;
|
|
39
|
-
const CIDR_RULE_COUNT = 1_300;
|
|
40
|
-
const MIXED_RULE_COUNT = 1_000;
|
|
41
|
-
// Deferred rules as a share of the population. Production currently has
|
|
42
|
-
// ~380 deferred against ~4k live rules; 2_000 here is a deliberate
|
|
43
|
-
// over-weight so the union has real work to merge.
|
|
44
|
-
const DEFERRED_RULE_COUNT = 2_000;
|
|
45
|
-
|
|
46
|
-
const WARMUP = 50;
|
|
47
|
-
const ITERATIONS = 500;
|
|
48
|
-
|
|
49
|
-
const INDEX = "bench:idx";
|
|
50
|
-
const PREFIX = "bench:";
|
|
51
|
-
|
|
52
|
-
type Clause = { name: string; clause: string };
|
|
53
|
-
|
|
54
|
-
const CLAUSES: Clause[] = [
|
|
55
|
-
{ name: "baseline @type:{block}", clause: "@type:{block}" },
|
|
56
|
-
{
|
|
57
|
-
name: "middleware @type:{block} -@deferToVerify:{true}",
|
|
58
|
-
clause: "@type:{block} -@deferToVerify:{true}",
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
name: "verify (@type:{block} | @deferToVerify:{true})",
|
|
62
|
-
clause: "(@type:{block} | @deferToVerify:{true})",
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
name: "MISGROUPED (@type:{block})|(@deferToVerify:{true})",
|
|
66
|
-
clause: "(@type:{block})|(@deferToVerify:{true})",
|
|
67
|
-
},
|
|
68
|
-
];
|
|
69
|
-
|
|
70
|
-
const ipv4 = (i: number): bigint => (10n << 24n) + BigInt(i);
|
|
71
|
-
|
|
72
|
-
const ipv4CidrRange = (i: number): { min: bigint; max: bigint } => {
|
|
73
|
-
const octet = i % 256;
|
|
74
|
-
const base = (192n << 24n) + (168n << 16n) + (BigInt(octet) << 8n);
|
|
75
|
-
return { min: base, max: base + 255n };
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const percentile = (sorted: number[], p: number): number => {
|
|
79
|
-
const idx = Math.min(
|
|
80
|
-
sorted.length - 1,
|
|
81
|
-
Math.floor((p / 100) * sorted.length),
|
|
82
|
-
);
|
|
83
|
-
return sorted[idx] ?? 0;
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const main = async (): Promise<void> => {
|
|
87
|
-
const client = createClient({
|
|
88
|
-
url: "redis://localhost:6379",
|
|
89
|
-
password: "root",
|
|
90
|
-
});
|
|
91
|
-
await client.connect();
|
|
92
|
-
|
|
93
|
-
await client.flushAll();
|
|
94
|
-
|
|
95
|
-
// Real index schema shape: type + deferToVerify as TAG INDEXMISSING,
|
|
96
|
-
// IP fields NUMERIC INDEXMISSING.
|
|
97
|
-
await client.sendCommand([
|
|
98
|
-
"FT.CREATE",
|
|
99
|
-
INDEX,
|
|
100
|
-
"ON",
|
|
101
|
-
"HASH",
|
|
102
|
-
"PREFIX",
|
|
103
|
-
"1",
|
|
104
|
-
PREFIX,
|
|
105
|
-
"SCHEMA",
|
|
106
|
-
"type",
|
|
107
|
-
"TAG",
|
|
108
|
-
"INDEXMISSING",
|
|
109
|
-
"deferToVerify",
|
|
110
|
-
"TAG",
|
|
111
|
-
"INDEXMISSING",
|
|
112
|
-
"clientId",
|
|
113
|
-
"TAG",
|
|
114
|
-
"INDEXMISSING",
|
|
115
|
-
"numericIp",
|
|
116
|
-
"NUMERIC",
|
|
117
|
-
"INDEXMISSING",
|
|
118
|
-
"numericIpMaskMin",
|
|
119
|
-
"NUMERIC",
|
|
120
|
-
"INDEXMISSING",
|
|
121
|
-
"numericIpMaskMax",
|
|
122
|
-
"NUMERIC",
|
|
123
|
-
"INDEXMISSING",
|
|
124
|
-
]);
|
|
125
|
-
|
|
126
|
-
let n = 0;
|
|
127
|
-
const write = async (fields: Record<string, string>): Promise<void> => {
|
|
128
|
-
await client.hSet(`${PREFIX}${n++}`, fields);
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
for (let i = 0; i < IP_RULE_COUNT; i++) {
|
|
132
|
-
await write({
|
|
133
|
-
type: "block",
|
|
134
|
-
description: `bulk-ban-ip-${i}`,
|
|
135
|
-
numericIp: ipv4(i).toString(),
|
|
136
|
-
clientId: "global",
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
for (let i = 0; i < CIDR_RULE_COUNT; i++) {
|
|
140
|
-
const { min, max } = ipv4CidrRange(i);
|
|
141
|
-
await write({
|
|
142
|
-
type: "block",
|
|
143
|
-
description: `bulk-ban-cidr-${i}`,
|
|
144
|
-
numericIpMaskMin: min.toString(),
|
|
145
|
-
numericIpMaskMax: max.toString(),
|
|
146
|
-
clientId: "global",
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
for (let i = 0; i < MIXED_RULE_COUNT; i++) {
|
|
150
|
-
await write({
|
|
151
|
-
type: i % 2 === 0 ? "restrict" : "block",
|
|
152
|
-
description: `mixed-${i}`,
|
|
153
|
-
numericIp: ipv4(IP_RULE_COUNT + i).toString(),
|
|
154
|
-
clientId: `client-${i % 20}`,
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
// Deferred cohort — the shape this change is about. Restrict, so the
|
|
158
|
-
// baseline clause would miss them entirely.
|
|
159
|
-
for (let i = 0; i < DEFERRED_RULE_COUNT; i++) {
|
|
160
|
-
await write({
|
|
161
|
-
type: "restrict",
|
|
162
|
-
description: `deferred-${i}`,
|
|
163
|
-
deferToVerify: "true",
|
|
164
|
-
numericIp: ipv4(IP_RULE_COUNT + MIXED_RULE_COUNT + i).toString(),
|
|
165
|
-
clientId: `client-${i % 20}`,
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const total = n;
|
|
170
|
-
// Let RediSearch finish indexing.
|
|
171
|
-
for (;;) {
|
|
172
|
-
const info = (await client.sendCommand(["FT.INFO", INDEX])) as unknown[];
|
|
173
|
-
const idx = info.findIndex((v) => String(v) === "indexing");
|
|
174
|
-
if (idx === -1 || String(info[idx + 1]) === "0") break;
|
|
175
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
console.log(`population: ${total} rules`);
|
|
179
|
-
console.log(
|
|
180
|
-
` ${IP_RULE_COUNT} ip-block, ${CIDR_RULE_COUNT} cidr-block, ${MIXED_RULE_COUNT} mixed, ${DEFERRED_RULE_COUNT} deferred-restrict\n`,
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
// The two probe shapes that dominate the split query: an exact-IP hit
|
|
184
|
-
// (the common ban case) and the CIDR mask-range probe (the expensive
|
|
185
|
-
// one — it walks two numeric ranges).
|
|
186
|
-
const hitIp = ipv4(1234).toString();
|
|
187
|
-
const cidrIp = ((192n << 24n) + (168n << 16n) + (7n << 8n) + 9n).toString();
|
|
188
|
-
const scope = "( @clientId:{client-3} | @clientId:{global} )";
|
|
189
|
-
|
|
190
|
-
const probes: Array<{ label: string; suffix: string }> = [
|
|
191
|
-
{ label: "ip:exact", suffix: `${scope} @numericIp:[${hitIp} ${hitIp}]` },
|
|
192
|
-
{
|
|
193
|
-
label: "ip:mask ",
|
|
194
|
-
suffix: `${scope} @numericIpMaskMin:[-inf ${cidrIp}] @numericIpMaskMax:[${cidrIp} +inf]`,
|
|
195
|
-
},
|
|
196
|
-
];
|
|
197
|
-
|
|
198
|
-
for (const probe of probes) {
|
|
199
|
-
console.log(`--- probe: ${probe.label} ---`);
|
|
200
|
-
for (const { name, clause } of CLAUSES) {
|
|
201
|
-
const query = `${clause} ${probe.suffix}`;
|
|
202
|
-
|
|
203
|
-
for (let i = 0; i < WARMUP; i++) {
|
|
204
|
-
await client.sendCommand([
|
|
205
|
-
"FT.SEARCH",
|
|
206
|
-
INDEX,
|
|
207
|
-
query,
|
|
208
|
-
"NOCONTENT",
|
|
209
|
-
"DIALECT",
|
|
210
|
-
"2",
|
|
211
|
-
]);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const samples: number[] = [];
|
|
215
|
-
let hits = 0;
|
|
216
|
-
for (let i = 0; i < ITERATIONS; i++) {
|
|
217
|
-
const t0 = process.hrtime.bigint();
|
|
218
|
-
const res = (await client.sendCommand([
|
|
219
|
-
"FT.SEARCH",
|
|
220
|
-
INDEX,
|
|
221
|
-
query,
|
|
222
|
-
"NOCONTENT",
|
|
223
|
-
"DIALECT",
|
|
224
|
-
"2",
|
|
225
|
-
])) as unknown[];
|
|
226
|
-
const t1 = process.hrtime.bigint();
|
|
227
|
-
samples.push(Number(t1 - t0) / 1e6);
|
|
228
|
-
hits = Number(res[0]);
|
|
229
|
-
}
|
|
230
|
-
samples.sort((a, b) => a - b);
|
|
231
|
-
const mean = samples.reduce((s, v) => s + v, 0) / samples.length;
|
|
232
|
-
console.log(
|
|
233
|
-
` ${name.padEnd(52)} hits=${String(hits).padStart(5)} ` +
|
|
234
|
-
`mean=${mean.toFixed(3)}ms p50=${percentile(samples, 50).toFixed(3)}ms ` +
|
|
235
|
-
`p95=${percentile(samples, 95).toFixed(3)}ms p99=${percentile(samples, 99).toFixed(3)}ms`,
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
console.log();
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
await client.sendCommand(["FT.DROPINDEX", INDEX]);
|
|
242
|
-
await client.flushAll();
|
|
243
|
-
await client.quit();
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
main().catch((e: unknown) => {
|
|
247
|
-
console.error(e);
|
|
248
|
-
process.exit(1);
|
|
249
|
-
});
|