@prosopo/user-access-policy 3.12.31 → 3.12.33
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 +11 -10
- package/.turbo/turbo-build$colon$tsc.log +14 -14
- package/.turbo/turbo-build.log +12 -11
- package/CHANGELOG.md +74 -0
- package/dist/.export.d.ts +1 -0
- package/dist/.export.d.ts.map +1 -1
- package/dist/.export.js +2 -1
- package/dist/.export.js.map +1 -1
- package/dist/cjs/.export.cjs +3 -0
- package/dist/cjs/classifyBrowser.cjs +44 -0
- package/dist/cjs/mongoose/mongooseRuleSchema.cjs +4 -0
- package/dist/cjs/redis/reader/redisRulesReader.cjs +3 -0
- package/dist/cjs/redis/reader/redisRulesSplitQuery.cjs +3 -1
- package/dist/cjs/redis/redisRuleIndex.cjs +4 -0
- package/dist/cjs/ruleInput/userScopeInput.cjs +2 -1
- package/dist/cjs/ruleRecord.cjs +2 -1
- package/dist/classifyBrowser.d.ts +4 -0
- package/dist/classifyBrowser.d.ts.map +1 -0
- package/dist/classifyBrowser.js +43 -0
- package/dist/classifyBrowser.js.map +1 -0
- package/dist/mongoose/mongooseRuleSchema.d.ts.map +1 -1
- package/dist/mongoose/mongooseRuleSchema.js +4 -0
- package/dist/mongoose/mongooseRuleSchema.js.map +1 -1
- package/dist/redis/reader/redisRulesReader.d.ts.map +1 -1
- package/dist/redis/reader/redisRulesReader.js +3 -0
- package/dist/redis/reader/redisRulesReader.js.map +1 -1
- package/dist/redis/reader/redisRulesSplitQuery.d.ts.map +1 -1
- package/dist/redis/reader/redisRulesSplitQuery.js +3 -1
- package/dist/redis/reader/redisRulesSplitQuery.js.map +1 -1
- package/dist/redis/redisRuleIndex.d.ts.map +1 -1
- package/dist/redis/redisRuleIndex.js +4 -0
- package/dist/redis/redisRuleIndex.js.map +1 -1
- package/dist/rule.d.ts +1 -0
- package/dist/rule.d.ts.map +1 -1
- package/dist/ruleInput/ruleInput.d.ts +6 -0
- package/dist/ruleInput/ruleInput.d.ts.map +1 -1
- package/dist/ruleInput/userScopeInput.d.ts +8 -0
- package/dist/ruleInput/userScopeInput.d.ts.map +1 -1
- package/dist/ruleInput/userScopeInput.js +2 -1
- package/dist/ruleInput/userScopeInput.js.map +1 -1
- package/dist/ruleRecord.d.ts +2 -2
- package/dist/ruleRecord.d.ts.map +1 -1
- package/dist/ruleRecord.js +2 -1
- package/dist/ruleRecord.js.map +1 -1
- package/dist/tests/classifyBrowser.unit.test.d.ts +2 -0
- package/dist/tests/classifyBrowser.unit.test.d.ts.map +1 -0
- package/dist/tests/classifyBrowser.unit.test.js +99 -0
- package/dist/tests/classifyBrowser.unit.test.js.map +1 -0
- package/dist/tests/redis/reader/redisRulesQuery.unit.test.js +3 -3
- package/dist/tests/redis/reader/redisRulesQuery.unit.test.js.map +1 -1
- package/dist/tests/redis/reader/redisRulesSplitQuery.unit.test.js +9 -0
- package/dist/tests/redis/reader/redisRulesSplitQuery.unit.test.js.map +1 -1
- package/dist/tests/transformRule.unit.test.js +1 -0
- package/dist/tests/transformRule.unit.test.js.map +1 -1
- package/package.json +12 -12
- package/src/.export.ts +6 -0
- package/src/classifyBrowser.ts +77 -0
- package/src/mongoose/mongooseRuleSchema.ts +1 -0
- package/src/redis/reader/redisRulesReader.ts +3 -0
- package/src/redis/reader/redisRulesSplitQuery.ts +2 -0
- package/src/redis/redisRuleIndex.ts +1 -0
- package/src/rule.ts +1 -0
- package/src/ruleInput/userScopeInput.ts +1 -0
- package/src/ruleRecord.ts +1 -0
- package/src/tests/classifyBrowser.unit.test.ts +138 -0
- package/src/tests/redis/reader/redisRulesQuery.unit.test.ts +3 -3
- package/src/tests/redis/reader/redisRulesSplitQuery.unit.test.ts +17 -0
- package/src/tests/transformRule.unit.test.ts +1 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/.export.ts
CHANGED
|
@@ -32,6 +32,12 @@ export { describeMatchedRule } from "./matchedRule.js";
|
|
|
32
32
|
|
|
33
33
|
export { classifyOs, OS_NAMES, type OsName } from "./classifyOs.js";
|
|
34
34
|
|
|
35
|
+
export {
|
|
36
|
+
classifyBrowser,
|
|
37
|
+
BROWSER_NAMES,
|
|
38
|
+
type BrowserName,
|
|
39
|
+
} from "./classifyBrowser.js";
|
|
40
|
+
|
|
35
41
|
export {
|
|
36
42
|
type AccessRulesFilter,
|
|
37
43
|
type AccessRulesStorage,
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
/**
|
|
16
|
+
* Kept in step with `@prosopo/decision-machines`' `uaClassify` — as `classifyOs`
|
|
17
|
+
* is — but defined here because the provider request path cannot depend on that
|
|
18
|
+
* package.
|
|
19
|
+
*/
|
|
20
|
+
export const BROWSER_NAMES = [
|
|
21
|
+
"chrome",
|
|
22
|
+
"safari",
|
|
23
|
+
"firefox",
|
|
24
|
+
"edge",
|
|
25
|
+
"opera",
|
|
26
|
+
"samsung_internet",
|
|
27
|
+
"wechat",
|
|
28
|
+
"facebook",
|
|
29
|
+
"instagram",
|
|
30
|
+
"ie",
|
|
31
|
+
"unknown",
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
export type BrowserName = (typeof BROWSER_NAMES)[number];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Branch order is load-bearing: every browser below Chrome also carries a
|
|
38
|
+
* `chrome/` token, and Safari's signature appears in nearly every WebKit UA.
|
|
39
|
+
*
|
|
40
|
+
* Reads the User-Agent rather than the `sec-ch-ua` client hint because a client
|
|
41
|
+
* can simply omit client hints, but stripping the UA breaks far more.
|
|
42
|
+
*/
|
|
43
|
+
export const classifyBrowser = (userAgent: string | undefined): BrowserName => {
|
|
44
|
+
const ua = (userAgent || "").toLowerCase();
|
|
45
|
+
|
|
46
|
+
if (/\bedg(?:e|a|ios)?\//.test(ua)) {
|
|
47
|
+
return "edge";
|
|
48
|
+
}
|
|
49
|
+
if (/samsungbrowser/.test(ua)) {
|
|
50
|
+
return "samsung_internet";
|
|
51
|
+
}
|
|
52
|
+
if (/\bopr\/|opera/.test(ua)) {
|
|
53
|
+
return "opera";
|
|
54
|
+
}
|
|
55
|
+
if (/micromessenger/.test(ua)) {
|
|
56
|
+
return "wechat";
|
|
57
|
+
}
|
|
58
|
+
if (/fban|fbav|fbios/.test(ua)) {
|
|
59
|
+
return "facebook";
|
|
60
|
+
}
|
|
61
|
+
if (/instagram/.test(ua)) {
|
|
62
|
+
return "instagram";
|
|
63
|
+
}
|
|
64
|
+
if (/firefox|fxios/.test(ua)) {
|
|
65
|
+
return "firefox";
|
|
66
|
+
}
|
|
67
|
+
if (/msie|trident/.test(ua)) {
|
|
68
|
+
return "ie";
|
|
69
|
+
}
|
|
70
|
+
if (/\bchrome\/|\bcrios\//.test(ua)) {
|
|
71
|
+
return "chrome";
|
|
72
|
+
}
|
|
73
|
+
if (/safari\//.test(ua)) {
|
|
74
|
+
return "safari";
|
|
75
|
+
}
|
|
76
|
+
return "unknown";
|
|
77
|
+
};
|
|
@@ -32,6 +32,7 @@ const userAttributesSchema: SchemaDefinition<UserAttributesRecord> = {
|
|
|
32
32
|
countryCode: { type: String, required: false },
|
|
33
33
|
asn: { type: Number, required: false },
|
|
34
34
|
os: { type: String, required: false },
|
|
35
|
+
browser: { type: String, required: false },
|
|
35
36
|
} satisfies AllKeys<UserAttributesRecord>;
|
|
36
37
|
|
|
37
38
|
const userIpSchema: SchemaDefinition<UserIpRecord> = {
|
|
@@ -95,6 +95,7 @@ const SPECIFICITY_EXPR = [
|
|
|
95
95
|
"exists(@countryCode)",
|
|
96
96
|
"exists(@asn)",
|
|
97
97
|
"exists(@os)",
|
|
98
|
+
"exists(@browser)",
|
|
98
99
|
"exists(@numericIp)",
|
|
99
100
|
"exists(@numericIpMaskMin)",
|
|
100
101
|
].join(" + ");
|
|
@@ -135,6 +136,7 @@ const RULE_LOAD_FIELDS = [
|
|
|
135
136
|
"@countryCode",
|
|
136
137
|
"@asn",
|
|
137
138
|
"@os",
|
|
139
|
+
"@browser",
|
|
138
140
|
"@numericIp",
|
|
139
141
|
"@numericIpMaskMin",
|
|
140
142
|
"@numericIpMaskMax",
|
|
@@ -156,6 +158,7 @@ const readerSpecificity = (rule: AccessRule): number => {
|
|
|
156
158
|
if (rule.countryCode !== undefined) score++;
|
|
157
159
|
if (rule.asn !== undefined) score++;
|
|
158
160
|
if (rule.os !== undefined) score++;
|
|
161
|
+
if (rule.browser !== undefined) score++;
|
|
159
162
|
if (rule.numericIp !== undefined) score++;
|
|
160
163
|
if (rule.numericIpMaskMin !== undefined) score++;
|
|
161
164
|
return score;
|
|
@@ -41,6 +41,7 @@ export const userAttributesRedisSchema: RediSearchSchema = {
|
|
|
41
41
|
countryCode: { type: SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
|
|
42
42
|
asn: { type: SCHEMA_FIELD_TYPE.NUMERIC, INDEXMISSING: true },
|
|
43
43
|
os: { type: SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
|
|
44
|
+
browser: { type: SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
|
|
44
45
|
} satisfies AllKeys<UserAttributes>;
|
|
45
46
|
|
|
46
47
|
export const userScopeRedisSchema: RediSearchSchema = {
|
package/src/rule.ts
CHANGED
|
@@ -36,6 +36,7 @@ const userAttributesSchema = z.object({
|
|
|
36
36
|
// countryCode: a stale/unknown value just never matches a request rather
|
|
37
37
|
// than failing the whole rule parse.
|
|
38
38
|
os: z.coerce.string().optional(),
|
|
39
|
+
browser: z.coerce.string().optional(),
|
|
39
40
|
} satisfies AllKeys<UserAttributes>) satisfies ZodType<UserAttributes>;
|
|
40
41
|
|
|
41
42
|
const userAttributesInput = z
|
package/src/ruleRecord.ts
CHANGED
|
@@ -0,0 +1,138 @@
|
|
|
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
|
+
BROWSER_NAMES,
|
|
18
|
+
type BrowserName,
|
|
19
|
+
classifyBrowser,
|
|
20
|
+
} from "#policy/classifyBrowser.js";
|
|
21
|
+
|
|
22
|
+
describe("classifyBrowser", () => {
|
|
23
|
+
const cases: Array<{ name: string; ua: string; expected: BrowserName }> = [
|
|
24
|
+
{
|
|
25
|
+
name: "Chrome on Windows",
|
|
26
|
+
ua: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
27
|
+
expected: "chrome",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "Chrome on iOS",
|
|
31
|
+
ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/120.0.0.0 Mobile/15E148 Safari/604.1",
|
|
32
|
+
expected: "chrome",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "Safari on macOS",
|
|
36
|
+
ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15",
|
|
37
|
+
expected: "safari",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "Firefox on Linux",
|
|
41
|
+
ua: "Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0",
|
|
42
|
+
expected: "firefox",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "Firefox on iOS",
|
|
46
|
+
ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/120.0 Mobile/15E148 Safari/605.1.15",
|
|
47
|
+
expected: "firefox",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "Edge on Windows",
|
|
51
|
+
ua: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0",
|
|
52
|
+
expected: "edge",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "Opera",
|
|
56
|
+
ua: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 OPR/106.0.0.0",
|
|
57
|
+
expected: "opera",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "Samsung Internet",
|
|
61
|
+
ua: "Mozilla/5.0 (Linux; Android 14; SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/23.0 Chrome/115.0.0.0 Mobile Safari/537.36",
|
|
62
|
+
expected: "samsung_internet",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "WeChat in-app browser",
|
|
66
|
+
ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.42",
|
|
67
|
+
expected: "wechat",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "Facebook in-app browser",
|
|
71
|
+
ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 [FBAN/FBIOS;FBAV/442.0.0]",
|
|
72
|
+
expected: "facebook",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "Instagram in-app browser",
|
|
76
|
+
ua: "Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36 Instagram 310.0.0.0 Android",
|
|
77
|
+
expected: "instagram",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "Internet Explorer 11",
|
|
81
|
+
ua: "Mozilla/5.0 (Windows NT 10.0; Trident/7.0; rv:11.0) like Gecko",
|
|
82
|
+
expected: "ie",
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
for (const { name, ua, expected } of cases) {
|
|
87
|
+
it(`classifies ${name} as ${expected}`, () => {
|
|
88
|
+
expect(classifyBrowser(ua)).toBe(expected);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
it("returns 'unknown' for an empty User-Agent", () => {
|
|
93
|
+
expect(classifyBrowser("")).toBe("unknown");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("returns 'unknown' for an undefined User-Agent", () => {
|
|
97
|
+
expect(classifyBrowser(undefined)).toBe("unknown");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("returns 'unknown' for an unrecognised User-Agent", () => {
|
|
101
|
+
expect(classifyBrowser("curl/8.4.0")).toBe("unknown");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("does not classify a Chrome UA as Safari", () => {
|
|
105
|
+
expect(
|
|
106
|
+
classifyBrowser(
|
|
107
|
+
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
|
|
108
|
+
),
|
|
109
|
+
).toBe("chrome");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("does not classify an Edge UA as Chrome", () => {
|
|
113
|
+
expect(
|
|
114
|
+
classifyBrowser(
|
|
115
|
+
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0",
|
|
116
|
+
),
|
|
117
|
+
).toBe("edge");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("does not classify a Samsung Internet UA as Chrome", () => {
|
|
121
|
+
expect(
|
|
122
|
+
classifyBrowser(
|
|
123
|
+
"Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 SamsungBrowser/23.0 Chrome/115.0.0.0 Mobile Safari/537.36",
|
|
124
|
+
),
|
|
125
|
+
).toBe("samsung_internet");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("is case-insensitive", () => {
|
|
129
|
+
expect(classifyBrowser("MOZILLA/5.0 FIREFOX/120.0")).toBe("firefox");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("only ever returns a value from BROWSER_NAMES", () => {
|
|
133
|
+
const uas = [...cases.map((c) => c.ua), "", "curl/8.4.0", "garbage"];
|
|
134
|
+
for (const ua of uas) {
|
|
135
|
+
expect(BROWSER_NAMES).toContain(classifyBrowser(ua));
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -50,7 +50,7 @@ describe("getRulesRedisQuery", () => {
|
|
|
50
50
|
const query = getRulesRedisQuery(filter, true);
|
|
51
51
|
|
|
52
52
|
expect(query).toBe(
|
|
53
|
-
"( ( @numericIp:[100 100] | ( @numericIpMaskMin:[-inf 100] @numericIpMaskMax:[100 +inf] ) ) @ja4Hash:{ja4Hash} ismissing(@userAgentHash) ismissing(@userId) ismissing(@headersHash) ismissing(@headHash) ismissing(@coords) ismissing(@countryCode) ismissing(@asn) ismissing(@os) )",
|
|
53
|
+
"( ( @numericIp:[100 100] | ( @numericIpMaskMin:[-inf 100] @numericIpMaskMax:[100 +inf] ) ) @ja4Hash:{ja4Hash} ismissing(@userAgentHash) ismissing(@userId) ismissing(@headersHash) ismissing(@headHash) ismissing(@coords) ismissing(@countryCode) ismissing(@asn) ismissing(@os) ismissing(@browser) )",
|
|
54
54
|
);
|
|
55
55
|
});
|
|
56
56
|
|
|
@@ -126,7 +126,7 @@ describe("getRulesRedisQuery", () => {
|
|
|
126
126
|
const query = getRulesRedisQuery(filter, true);
|
|
127
127
|
|
|
128
128
|
expect(query).toBe(
|
|
129
|
-
"( ( @numericIp:[100 100] | ( @numericIpMaskMin:[-inf 100] @numericIpMaskMax:[100 +inf] ) ) @ja4Hash:{ja4Hash} ismissing(@userAgentHash) ismissing(@headersHash) ismissing(@userId) ismissing(@headHash) ismissing(@coords) ismissing(@countryCode) ismissing(@asn) ismissing(@os) )",
|
|
129
|
+
"( ( @numericIp:[100 100] | ( @numericIpMaskMin:[-inf 100] @numericIpMaskMax:[100 +inf] ) ) @ja4Hash:{ja4Hash} ismissing(@userAgentHash) ismissing(@headersHash) ismissing(@userId) ismissing(@headHash) ismissing(@coords) ismissing(@countryCode) ismissing(@asn) ismissing(@os) ismissing(@browser) )",
|
|
130
130
|
);
|
|
131
131
|
});
|
|
132
132
|
|
|
@@ -146,7 +146,7 @@ describe("getRulesRedisQuery", () => {
|
|
|
146
146
|
const query = getRulesRedisQuery(filter, true);
|
|
147
147
|
|
|
148
148
|
expect(query).toBe(
|
|
149
|
-
"( @numericIpMaskMin:[-inf 100] @numericIpMaskMax:[200 +inf] @ja4Hash:{ja4Hash} ismissing(@userAgentHash) ismissing(@headersHash) ismissing(@userId) ismissing(@headHash) ismissing(@coords) ismissing(@countryCode) ismissing(@asn) ismissing(@os) )",
|
|
149
|
+
"( @numericIpMaskMin:[-inf 100] @numericIpMaskMax:[200 +inf] @ja4Hash:{ja4Hash} ismissing(@userAgentHash) ismissing(@headersHash) ismissing(@userId) ismissing(@headHash) ismissing(@coords) ismissing(@countryCode) ismissing(@asn) ismissing(@os) ismissing(@browser) )",
|
|
150
150
|
);
|
|
151
151
|
});
|
|
152
152
|
|
|
@@ -101,6 +101,8 @@ describe("buildScopedBlockSubQueries", () => {
|
|
|
101
101
|
"coords",
|
|
102
102
|
"countryCode",
|
|
103
103
|
"asn",
|
|
104
|
+
"os",
|
|
105
|
+
"browser",
|
|
104
106
|
"numericIp",
|
|
105
107
|
"numericIpMaskMin",
|
|
106
108
|
"numericIpMaskMax",
|
|
@@ -109,6 +111,21 @@ describe("buildScopedBlockSubQueries", () => {
|
|
|
109
111
|
}
|
|
110
112
|
});
|
|
111
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
|
+
|
|
112
129
|
it("uses NUMERIC range syntax for asn, not TAG syntax", () => {
|
|
113
130
|
const subs = buildScopedBlockSubQueries({ asn: 205016 }, "client-A");
|
|
114
131
|
|