@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.
Files changed (70) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +3 -6
  2. package/.turbo/turbo-build$colon$tsc.log +14 -17
  3. package/.turbo/turbo-build.log +4 -10
  4. package/CHANGELOG.md +37 -0
  5. package/LICENSE +201 -0
  6. package/dist/cjs/ruleInput/policyInput.cjs +1 -1
  7. package/dist/ruleInput/policyInput.d.ts +2 -2
  8. package/dist/ruleInput/policyInput.d.ts.map +1 -1
  9. package/dist/ruleInput/policyInput.js +2 -2
  10. package/dist/ruleInput/policyInput.js.map +1 -1
  11. package/package.json +83 -81
  12. package/src/.export.ts +0 -65
  13. package/src/api/.export.ts +0 -25
  14. package/src/api/accessRulesApiClient.ts +0 -13
  15. package/src/api/delete/.export.ts +0 -18
  16. package/src/api/delete/deleteAllRules.ts +0 -47
  17. package/src/api/delete/deleteRuleGroups.ts +0 -96
  18. package/src/api/delete/deleteRules.ts +0 -87
  19. package/src/api/read/.export.ts +0 -25
  20. package/src/api/read/fetchRules.ts +0 -96
  21. package/src/api/read/findRuleIds.ts +0 -97
  22. package/src/api/read/getMissingIds.ts +0 -81
  23. package/src/api/ruleApiRoutes.ts +0 -146
  24. package/src/api/rulesApiClient.ts +0 -154
  25. package/src/api/write/.export.ts +0 -15
  26. package/src/api/write/insertRules.ts +0 -185
  27. package/src/api/write/rehashRules.ts +0 -85
  28. package/src/classifyBrowser.ts +0 -77
  29. package/src/classifyOs.ts +0 -74
  30. package/src/headerMatch.ts +0 -180
  31. package/src/matchedRule.ts +0 -99
  32. package/src/mongoose/.export.ts +0 -15
  33. package/src/mongoose/mongooseRuleSchema.ts +0 -75
  34. package/src/redis/.export.ts +0 -17
  35. package/src/redis/reader/redisAggregate.ts +0 -157
  36. package/src/redis/reader/redisRulesQuery.ts +0 -268
  37. package/src/redis/reader/redisRulesReader.ts +0 -738
  38. package/src/redis/reader/redisRulesSplitQuery.ts +0 -234
  39. package/src/redis/redisClient.ts +0 -125
  40. package/src/redis/redisRuleIndex.ts +0 -121
  41. package/src/redis/redisRulesStorage.ts +0 -68
  42. package/src/redis/redisRulesWriter.ts +0 -172
  43. package/src/rule.ts +0 -122
  44. package/src/ruleInput/.export.ts +0 -19
  45. package/src/ruleInput/policyInput.ts +0 -120
  46. package/src/ruleInput/ruleInput.ts +0 -123
  47. package/src/ruleInput/userScopeInput.ts +0 -142
  48. package/src/ruleRecord.ts +0 -77
  49. package/src/rulesStorage.ts +0 -90
  50. package/src/tests/blockingRuleShapes.unit.test.ts +0 -244
  51. package/src/tests/classifyBrowser.unit.test.ts +0 -138
  52. package/src/tests/classifyOs.unit.test.ts +0 -97
  53. package/src/tests/headerMatch.unit.test.ts +0 -350
  54. package/src/tests/insertRulesEndpoint.unit.test.ts +0 -89
  55. package/src/tests/matchedRule.unit.test.ts +0 -184
  56. package/src/tests/policyInput.unit.test.ts +0 -218
  57. package/src/tests/redis/deferredPoolClause.benchmark.ts +0 -249
  58. package/src/tests/redis/reader/redisAggregate.unit.test.ts +0 -121
  59. package/src/tests/redis/reader/redisRulesQuery.unit.test.ts +0 -349
  60. package/src/tests/redis/reader/redisRulesSplitQuery.unit.test.ts +0 -297
  61. package/src/tests/redis/redisRulesReaderLoad.benchmark.integration.test.ts +0 -520
  62. package/src/tests/redis/redisRulesReaderRank.benchmark.integration.test.ts +0 -350
  63. package/src/tests/redis/redisRulesStorage.integration.test.ts +0 -1811
  64. package/src/tests/testLogger.ts +0 -38
  65. package/src/tests/transformRule.unit.test.ts +0 -345
  66. package/src/transformRule.ts +0 -133
  67. package/tsconfig.cjs.json +0 -41
  68. package/tsconfig.json +0 -47
  69. package/tsconfig.tsbuildinfo +0 -1
  70. 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
- });