@prosopo/user-access-policy 3.14.14 → 3.14.15
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 +2 -5
- package/.turbo/turbo-build$colon$tsc.log +14 -17
- package/.turbo/turbo-build.log +3 -9
- package/CHANGELOG.md +18 -0
- package/LICENSE +201 -0
- 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,297 +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 { describe, expect, it } from "vitest";
|
|
16
|
-
import {
|
|
17
|
-
buildScopedBlockSubQueries,
|
|
18
|
-
buildScopedRulesSubQueries,
|
|
19
|
-
} from "#policy/redis/reader/redisRulesSplitQuery.js";
|
|
20
|
-
|
|
21
|
-
describe("buildScopedBlockSubQueries", () => {
|
|
22
|
-
it("emits one sub-query per populated user-scope field", () => {
|
|
23
|
-
const subs = buildScopedBlockSubQueries(
|
|
24
|
-
{
|
|
25
|
-
ja4Hash: "abc",
|
|
26
|
-
userAgentHash: "def",
|
|
27
|
-
},
|
|
28
|
-
"client-A",
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
const kinds = subs.map((s) => s.kind).sort();
|
|
32
|
-
// Two field probes + one fall-through for "no user-scope
|
|
33
|
-
// constraint" rules. No IP sub-queries because request has no IP.
|
|
34
|
-
expect(kinds).toEqual([
|
|
35
|
-
"field:ja4Hash",
|
|
36
|
-
"field:userAgentHash",
|
|
37
|
-
"no-user-scope",
|
|
38
|
-
]);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("emits both ip-exact and ip-mask sub-queries when request has an IP", () => {
|
|
42
|
-
const subs = buildScopedBlockSubQueries(
|
|
43
|
-
{
|
|
44
|
-
numericIp: 3232235777n, // 192.168.1.1
|
|
45
|
-
},
|
|
46
|
-
"client-A",
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
const kinds = subs.map((s) => s.kind);
|
|
50
|
-
expect(kinds).toContain("ip:exact");
|
|
51
|
-
expect(kinds).toContain("ip:mask");
|
|
52
|
-
|
|
53
|
-
const ipExact = subs.find((s) => s.kind === "ip:exact");
|
|
54
|
-
expect(ipExact?.query).toContain("@numericIp:[3232235777 3232235777]");
|
|
55
|
-
|
|
56
|
-
const ipMask = subs.find((s) => s.kind === "ip:mask");
|
|
57
|
-
expect(ipMask?.query).toContain("@numericIpMaskMin:[-inf 3232235777]");
|
|
58
|
-
expect(ipMask?.query).toContain("@numericIpMaskMax:[3232235777 +inf]");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("scopes every sub-query with @type:{block} and @clientId probe", () => {
|
|
62
|
-
const subs = buildScopedBlockSubQueries({ ja4Hash: "abc" }, "client-A");
|
|
63
|
-
|
|
64
|
-
for (const sub of subs) {
|
|
65
|
-
expect(sub.query).toContain("@type:{block}");
|
|
66
|
-
// Every sub-query must include the client-or-global scope
|
|
67
|
-
// probe. `@clientId:{client-A}` matches client-scoped rules;
|
|
68
|
-
// `@clientId:{global}` matches new-format global rules;
|
|
69
|
-
// `ismissing(@clientId)` matches legacy pre-sentinel rules.
|
|
70
|
-
expect(sub.query).toMatch(/@clientId:\{client-A\}/);
|
|
71
|
-
expect(sub.query).toMatch(/@clientId:\{global\}/);
|
|
72
|
-
expect(sub.query).toMatch(/ismissing\(@clientId\)/);
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("skips the client-specific probe when no clientId is passed", () => {
|
|
77
|
-
const subs = buildScopedBlockSubQueries({ ja4Hash: "abc" }, undefined);
|
|
78
|
-
|
|
79
|
-
for (const sub of subs) {
|
|
80
|
-
// No client-scoped probe — request is scoped to global rules
|
|
81
|
-
// only. `global` sentinel and `ismissing()` fallback remain.
|
|
82
|
-
expect(sub.query).toMatch(/@clientId:\{global\}/);
|
|
83
|
-
expect(sub.query).toMatch(/ismissing\(@clientId\)/);
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("emits a no-user-scope fall-through query with ismissing() over every user-scope field", () => {
|
|
88
|
-
const subs = buildScopedBlockSubQueries({ ja4Hash: "abc" }, "client-A");
|
|
89
|
-
|
|
90
|
-
const fallThrough = subs.find((s) => s.kind === "no-user-scope");
|
|
91
|
-
expect(fallThrough).toBeDefined();
|
|
92
|
-
// Every user-scope field must be constrained as missing so the
|
|
93
|
-
// probe returns only rules that are truly client-wide blocks
|
|
94
|
-
// (no user-scope constraint at all).
|
|
95
|
-
for (const field of [
|
|
96
|
-
"userId",
|
|
97
|
-
"ja4Hash",
|
|
98
|
-
"headersHash",
|
|
99
|
-
"userAgentHash",
|
|
100
|
-
"headHash",
|
|
101
|
-
"coords",
|
|
102
|
-
"countryCode",
|
|
103
|
-
"asn",
|
|
104
|
-
"os",
|
|
105
|
-
"browser",
|
|
106
|
-
"numericIp",
|
|
107
|
-
"numericIpMaskMin",
|
|
108
|
-
"numericIpMaskMax",
|
|
109
|
-
]) {
|
|
110
|
-
expect(fallThrough?.query).toContain(`ismissing(@${field})`);
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("emits field probes for the UA-derived os and browser scopes", () => {
|
|
115
|
-
const subs = buildScopedBlockSubQueries(
|
|
116
|
-
{ os: "windows", browser: "firefox" },
|
|
117
|
-
"client-A",
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
// Without its own probe a browser-only rule is reachable only via the
|
|
121
|
-
// no-user-scope fall-through, sharing that probe's candidate budget.
|
|
122
|
-
const osSub = subs.find((s) => s.kind === "field:os");
|
|
123
|
-
expect(osSub?.query).toContain("@os:{windows}");
|
|
124
|
-
|
|
125
|
-
const browserSub = subs.find((s) => s.kind === "field:browser");
|
|
126
|
-
expect(browserSub?.query).toContain("@browser:{firefox}");
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("uses NUMERIC range syntax for asn, not TAG syntax", () => {
|
|
130
|
-
const subs = buildScopedBlockSubQueries({ asn: 205016 }, "client-A");
|
|
131
|
-
|
|
132
|
-
const asnSub = subs.find((s) => s.kind === "field:asn");
|
|
133
|
-
expect(asnSub).toBeDefined();
|
|
134
|
-
// asn is indexed as NUMERIC; TAG syntax (@asn:{...}) would
|
|
135
|
-
// silently fail to match. Must use range syntax.
|
|
136
|
-
expect(asnSub?.query).toContain("@asn:[205016 205016]");
|
|
137
|
-
expect(asnSub?.query).not.toContain("@asn:{");
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("escapes JSON special characters in coords tag queries", () => {
|
|
141
|
-
const subs = buildScopedBlockSubQueries(
|
|
142
|
-
{ coords: "[[[100,200]]]" },
|
|
143
|
-
"client-A",
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
const coordsSub = subs.find((s) => s.kind === "field:coords");
|
|
147
|
-
expect(coordsSub).toBeDefined();
|
|
148
|
-
// Coords hold JSON with brackets and commas — every one must be
|
|
149
|
-
// backslash-escaped or the TAG query silently returns no matches.
|
|
150
|
-
expect(coordsSub?.query).toContain("@coords:{\\[\\[\\[100\\,200\\]\\]\\]}");
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
describe("buildScopedRulesSubQueries", () => {
|
|
155
|
-
it("omits @type:{block} when blockOnly is not set", () => {
|
|
156
|
-
// The non-block path — used by the frictionless access-policy
|
|
157
|
-
// lookup — must not filter on rule type so both Block and
|
|
158
|
-
// Restrict rules are returned. JS-side rankCandidateRules then
|
|
159
|
-
// picks the right one via severity + specificity.
|
|
160
|
-
const subs = buildScopedRulesSubQueries(
|
|
161
|
-
{ numericIp: 1376899398n },
|
|
162
|
-
"client-A",
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
for (const sub of subs) {
|
|
166
|
-
expect(sub.query).not.toContain("@type:{block}");
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it("includes @type:{block} when blockOnly is true", () => {
|
|
171
|
-
const subs = buildScopedRulesSubQueries(
|
|
172
|
-
{ numericIp: 1376899398n },
|
|
173
|
-
"client-A",
|
|
174
|
-
{ blockOnly: true },
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
for (const sub of subs) {
|
|
178
|
-
expect(sub.query).toContain("@type:{block}");
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it("emits a separate probe set for deferred rules at verify", () => {
|
|
183
|
-
// Two disjoint pools, each with its own
|
|
184
|
-
// SPLIT_MAX_CANDIDATES_PER_SUB budget, so a dense deferred cohort
|
|
185
|
-
// cannot truncate hard blocks out of the candidate set.
|
|
186
|
-
const subs = buildScopedRulesSubQueries(
|
|
187
|
-
{ numericIp: 1376899398n },
|
|
188
|
-
"clientA",
|
|
189
|
-
{ blockOnly: true, includeDeferred: true },
|
|
190
|
-
);
|
|
191
|
-
|
|
192
|
-
const block = subs.filter((s) => s.kind.startsWith("block:"));
|
|
193
|
-
const deferred = subs.filter((s) => s.kind.startsWith("deferred:"));
|
|
194
|
-
|
|
195
|
-
expect(block.length).toBeGreaterThan(0);
|
|
196
|
-
expect(deferred.length).toBe(block.length);
|
|
197
|
-
|
|
198
|
-
// Hard-block probes exclude deferred rules...
|
|
199
|
-
for (const sub of block) {
|
|
200
|
-
expect(sub.query).toContain("@type:{block} -@deferToVerify:{true}");
|
|
201
|
-
}
|
|
202
|
-
// ...and the deferred probes carry no type constraint, so a
|
|
203
|
-
// deferred Restrict is fetched at verify.
|
|
204
|
-
for (const sub of deferred) {
|
|
205
|
-
expect(sub.query).toContain("@deferToVerify:{true}");
|
|
206
|
-
expect(sub.query).not.toContain("@type:{block}");
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it("never merges the two pools into one probe", () => {
|
|
211
|
-
// A merged `(@type:{block} | @deferToVerify:{true})` probe shares
|
|
212
|
-
// one candidate budget between both populations — measured at 800
|
|
213
|
-
// candidates against a cap of 500 on a concentrated scope.
|
|
214
|
-
const subs = buildScopedRulesSubQueries(
|
|
215
|
-
{ numericIp: 1376899398n },
|
|
216
|
-
"clientA",
|
|
217
|
-
{ blockOnly: true, includeDeferred: true },
|
|
218
|
-
);
|
|
219
|
-
|
|
220
|
-
for (const sub of subs) {
|
|
221
|
-
expect(sub.query).not.toContain("| @deferToVerify:{true}");
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it("excludes deferred rules from the request-time pool", () => {
|
|
226
|
-
// blockOnly without includeDeferred is the middleware. Deferred
|
|
227
|
-
// rules are never enforceable there, so they are filtered out in
|
|
228
|
-
// Redis rather than fetched and discarded in JS.
|
|
229
|
-
const subs = buildScopedRulesSubQueries(
|
|
230
|
-
{ numericIp: 1376899398n },
|
|
231
|
-
"clientA",
|
|
232
|
-
{ blockOnly: true },
|
|
233
|
-
);
|
|
234
|
-
|
|
235
|
-
for (const sub of subs) {
|
|
236
|
-
expect(sub.query).toContain("-@deferToVerify:{true}");
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it("ignores includeDeferred when blockOnly is not set", () => {
|
|
241
|
-
// Without blockOnly there is no type clause to narrow; every rule
|
|
242
|
-
// type is already in the pool.
|
|
243
|
-
const subs = buildScopedRulesSubQueries(
|
|
244
|
-
{ numericIp: 1376899398n },
|
|
245
|
-
"clientA",
|
|
246
|
-
{ includeDeferred: true },
|
|
247
|
-
);
|
|
248
|
-
|
|
249
|
-
for (const sub of subs) {
|
|
250
|
-
expect(sub.query).not.toContain("@deferToVerify");
|
|
251
|
-
expect(sub.query).not.toContain("@type:{block}");
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it("emits the same probe kinds regardless of blockOnly", () => {
|
|
256
|
-
// The probe set is driven by populated request scope, not by
|
|
257
|
-
// rule-type filter. blockOnly only narrows what each probe
|
|
258
|
-
// returns; the shape of the split stays identical.
|
|
259
|
-
const withoutBlock = buildScopedRulesSubQueries(
|
|
260
|
-
{ numericIp: 1376899398n, ja4Hash: "abc" },
|
|
261
|
-
"client-A",
|
|
262
|
-
);
|
|
263
|
-
const withBlock = buildScopedRulesSubQueries(
|
|
264
|
-
{ numericIp: 1376899398n, ja4Hash: "abc" },
|
|
265
|
-
"client-A",
|
|
266
|
-
{ blockOnly: true },
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
expect(withoutBlock.map((s) => s.kind).sort()).toEqual(
|
|
270
|
-
withBlock.map((s) => s.kind).sort(),
|
|
271
|
-
);
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it("produces an ip-exact probe that hits the numericIp posting list", () => {
|
|
275
|
-
// This is the regression guard for the Twickets rule-lookup bug:
|
|
276
|
-
// a request with a specific IP must generate a probe that hits
|
|
277
|
-
// the @numericIp posting list, so a Restrict rule scoped only
|
|
278
|
-
// to `clientId + numericIp` is fetched even when the tenant has
|
|
279
|
-
// thousands of higher-specificity irrelevant rules on other
|
|
280
|
-
// scopes. The previous FT.AGGREGATE ranker capped candidates at
|
|
281
|
-
// top-20 by populated-field count and truncated the specific-IP
|
|
282
|
-
// rule below higher-specificity SUDDEN_VOLUME_INCREASE / SIMD
|
|
283
|
-
// rules that also matched the greedy `ismissing(@field)`
|
|
284
|
-
// disjunction.
|
|
285
|
-
const subs = buildScopedRulesSubQueries(
|
|
286
|
-
{ numericIp: 1376899398n },
|
|
287
|
-
"5EZVvsHMrKCFKp5NYNoTyDjTjetoVo1Z4UNNbTwJf1GfN6Xm",
|
|
288
|
-
);
|
|
289
|
-
|
|
290
|
-
const ipExact = subs.find((s) => s.kind === "ip:exact");
|
|
291
|
-
expect(ipExact).toBeDefined();
|
|
292
|
-
expect(ipExact?.query).toContain("@numericIp:[1376899398 1376899398]");
|
|
293
|
-
expect(ipExact?.query).toContain(
|
|
294
|
-
"@clientId:{5EZVvsHMrKCFKp5NYNoTyDjTjetoVo1Z4UNNbTwJf1GfN6Xm}",
|
|
295
|
-
);
|
|
296
|
-
});
|
|
297
|
-
});
|