@prosopo/user-access-policy 3.6.0 → 3.7.11
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 +21 -19
- package/.turbo/turbo-build$colon$tsc.log +16 -13
- package/.turbo/turbo-build.log +22 -20
- package/CHANGELOG.md +332 -0
- package/dist/api/delete/deleteAllRules.d.ts +2 -2
- package/dist/api/delete/deleteAllRules.d.ts.map +1 -1
- package/dist/api/delete/deleteAllRules.js +3 -2
- package/dist/api/delete/deleteAllRules.js.map +1 -1
- package/dist/api/delete/deleteRuleGroups.d.ts +2 -2
- package/dist/api/delete/deleteRuleGroups.d.ts.map +1 -1
- package/dist/api/delete/deleteRuleGroups.js +3 -2
- package/dist/api/delete/deleteRuleGroups.js.map +1 -1
- package/dist/api/delete/deleteRules.d.ts +2 -2
- package/dist/api/delete/deleteRules.d.ts.map +1 -1
- package/dist/api/delete/deleteRules.js +3 -2
- package/dist/api/delete/deleteRules.js.map +1 -1
- package/dist/api/read/fetchRules.d.ts +2 -2
- package/dist/api/read/fetchRules.d.ts.map +1 -1
- package/dist/api/read/fetchRules.js +4 -3
- package/dist/api/read/fetchRules.js.map +1 -1
- package/dist/api/read/findRuleIds.d.ts +2 -2
- package/dist/api/read/findRuleIds.d.ts.map +1 -1
- package/dist/api/read/findRuleIds.js +3 -2
- package/dist/api/read/findRuleIds.js.map +1 -1
- package/dist/api/read/getMissingIds.d.ts +2 -2
- package/dist/api/read/getMissingIds.d.ts.map +1 -1
- package/dist/api/read/getMissingIds.js +4 -3
- package/dist/api/read/getMissingIds.js.map +1 -1
- package/dist/api/ruleApiRoutes.d.ts +1 -1
- package/dist/api/ruleApiRoutes.d.ts.map +1 -1
- package/dist/api/ruleApiRoutes.js.map +1 -1
- package/dist/api/rulesApiClient.d.ts +9 -9
- package/dist/api/rulesApiClient.d.ts.map +1 -1
- package/dist/api/rulesApiClient.js +18 -19
- package/dist/api/rulesApiClient.js.map +1 -1
- package/dist/api/write/insertRules.d.ts +2 -2
- package/dist/api/write/insertRules.d.ts.map +1 -1
- package/dist/api/write/insertRules.js +7 -6
- package/dist/api/write/insertRules.js.map +1 -1
- package/dist/api/write/rehashRules.d.ts +2 -2
- package/dist/api/write/rehashRules.d.ts.map +1 -1
- package/dist/api/write/rehashRules.js +7 -6
- package/dist/api/write/rehashRules.js.map +1 -1
- package/dist/cjs/api/delete/deleteAllRules.cjs +3 -2
- package/dist/cjs/api/delete/deleteRuleGroups.cjs +3 -2
- package/dist/cjs/api/delete/deleteRules.cjs +3 -2
- package/dist/cjs/api/read/fetchRules.cjs +4 -3
- package/dist/cjs/api/read/findRuleIds.cjs +3 -2
- package/dist/cjs/api/read/getMissingIds.cjs +4 -3
- package/dist/cjs/api/rulesApiClient.cjs +18 -19
- package/dist/cjs/api/write/insertRules.cjs +9 -8
- package/dist/cjs/api/write/rehashRules.cjs +7 -6
- package/dist/cjs/mongoose/mongooseRuleSchema.cjs +2 -1
- package/dist/cjs/redis/reader/redisRulesQuery.cjs +6 -0
- package/dist/cjs/redis/reader/redisRulesReader.cjs +13 -4
- package/dist/cjs/redis/redisRuleIndex.cjs +2 -1
- package/dist/cjs/ruleInput/userScopeInput.cjs +2 -1
- package/dist/cjs/ruleRecord.cjs +2 -1
- package/dist/mongoose/mongooseRuleSchema.d.ts.map +1 -1
- package/dist/mongoose/mongooseRuleSchema.js +2 -1
- package/dist/mongoose/mongooseRuleSchema.js.map +1 -1
- package/dist/redis/reader/redisAggregate.d.ts +1 -1
- package/dist/redis/reader/redisRulesQuery.d.ts.map +1 -1
- package/dist/redis/reader/redisRulesQuery.js +6 -0
- package/dist/redis/reader/redisRulesQuery.js.map +1 -1
- package/dist/redis/reader/redisRulesReader.d.ts +1 -1
- package/dist/redis/reader/redisRulesReader.d.ts.map +1 -1
- package/dist/redis/reader/redisRulesReader.js +14 -5
- package/dist/redis/reader/redisRulesReader.js.map +1 -1
- package/dist/redis/redisClient.d.ts +1 -1
- package/dist/redis/redisRuleIndex.d.ts.map +1 -1
- package/dist/redis/redisRuleIndex.js +2 -1
- package/dist/redis/redisRuleIndex.js.map +1 -1
- package/dist/redis/redisRulesStorage.d.ts +1 -1
- package/dist/redis/redisRulesWriter.d.ts +1 -1
- package/dist/redis/redisRulesWriter.d.ts.map +1 -1
- package/dist/redis/redisRulesWriter.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/insertRulesEndpoint.unit.test.d.ts +2 -0
- package/dist/tests/insertRulesEndpoint.unit.test.d.ts.map +1 -0
- package/dist/tests/insertRulesEndpoint.unit.test.js +57 -0
- package/dist/tests/insertRulesEndpoint.unit.test.js.map +1 -0
- package/dist/tests/redis/reader/redisRulesQuery.unit.test.js +42 -3
- package/dist/tests/redis/reader/redisRulesQuery.unit.test.js.map +1 -1
- package/dist/tests/redis/redisRulesStorage.integration.test.js +126 -1
- package/dist/tests/redis/redisRulesStorage.integration.test.js.map +1 -1
- package/dist/tests/testLogger.d.ts +1 -1
- package/dist/tests/transformRule.unit.test.js +1 -0
- package/dist/tests/transformRule.unit.test.js.map +1 -1
- package/package.json +15 -10
- package/src/.export.ts +44 -0
- package/src/api/.export.ts +25 -0
- package/src/api/accessRulesApiClient.ts +13 -0
- package/src/api/delete/.export.ts +18 -0
- package/src/api/delete/deleteAllRules.ts +47 -0
- package/src/api/delete/deleteRuleGroups.ts +96 -0
- package/src/api/delete/deleteRules.ts +81 -0
- package/src/api/read/.export.ts +25 -0
- package/src/api/read/fetchRules.ts +88 -0
- package/src/api/read/findRuleIds.ts +95 -0
- package/src/api/read/getMissingIds.ts +81 -0
- package/src/api/ruleApiRoutes.ts +146 -0
- package/src/api/rulesApiClient.ts +154 -0
- package/src/api/write/.export.ts +15 -0
- package/src/api/write/insertRules.ts +183 -0
- package/src/api/write/rehashRules.ts +85 -0
- package/src/mongoose/.export.ts +15 -0
- package/src/mongoose/mongooseRuleSchema.ts +65 -0
- package/src/redis/.export.ts +17 -0
- package/src/redis/reader/redisAggregate.ts +103 -0
- package/src/redis/reader/redisRulesQuery.ts +217 -0
- package/src/redis/reader/redisRulesReader.ts +318 -0
- package/src/redis/redisClient.ts +120 -0
- package/src/redis/redisRuleIndex.ts +85 -0
- package/src/redis/redisRulesStorage.ts +68 -0
- package/src/redis/redisRulesWriter.ts +158 -0
- package/src/rule.ts +59 -0
- package/src/ruleInput/.export.ts +19 -0
- package/src/ruleInput/policyInput.ts +51 -0
- package/src/ruleInput/ruleInput.ts +103 -0
- package/src/ruleInput/userScopeInput.ts +108 -0
- package/src/ruleRecord.ts +69 -0
- package/src/rulesStorage.ts +72 -0
- package/src/tests/insertRulesEndpoint.unit.test.ts +89 -0
- package/src/tests/policyInput.unit.test.ts +150 -0
- package/src/tests/redis/reader/redisRulesQuery.unit.test.ts +284 -0
- package/src/tests/redis/redisRulesStorage.integration.test.ts +1156 -0
- package/src/tests/testLogger.ts +38 -0
- package/src/tests/transformRule.unit.test.ts +255 -0
- package/src/transformRule.ts +128 -0
- package/tsconfig.cjs.json +41 -0
- package/tsconfig.json +47 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsconfig.types.json +9 -0
|
@@ -0,0 +1,158 @@
|
|
|
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 { chunkIntoBatches, executeBatchesSequentially } from "@prosopo/common";
|
|
16
|
+
import type { Logger } from "@prosopo/logger";
|
|
17
|
+
import type { RedisClientType } from "redis";
|
|
18
|
+
import { REDIS_BATCH_SIZE } from "#policy/redis/redisClient.js";
|
|
19
|
+
import type { AccessRule } from "#policy/rule.js";
|
|
20
|
+
import type {
|
|
21
|
+
AccessRuleEntry,
|
|
22
|
+
AccessRulesWriter,
|
|
23
|
+
} from "#policy/rulesStorage.js";
|
|
24
|
+
import {
|
|
25
|
+
ACCESS_RULE_REDIS_KEY_PREFIX,
|
|
26
|
+
getAccessRuleRedisKey,
|
|
27
|
+
} from "./redisRuleIndex.js";
|
|
28
|
+
|
|
29
|
+
export class RedisRulesWriter implements AccessRulesWriter {
|
|
30
|
+
constructor(
|
|
31
|
+
private readonly client: RedisClientType,
|
|
32
|
+
private readonly logger: Logger,
|
|
33
|
+
) {}
|
|
34
|
+
|
|
35
|
+
async insertRules(ruleEntries: AccessRuleEntry[]): Promise<string[]> {
|
|
36
|
+
const entryBatches = chunkIntoBatches(ruleEntries, REDIS_BATCH_SIZE);
|
|
37
|
+
|
|
38
|
+
const keyBatches = await executeBatchesSequentially(
|
|
39
|
+
entryBatches,
|
|
40
|
+
async (entriesBatch) => this.insertRuleEntries(entriesBatch),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return keyBatches.flatMap((ruleKey) =>
|
|
44
|
+
ruleKey.slice(ACCESS_RULE_REDIS_KEY_PREFIX.length),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async deleteRules(ruleIds: string[]): Promise<void> {
|
|
49
|
+
const ruleKeys = ruleIds.map(
|
|
50
|
+
(ruleId) => ACCESS_RULE_REDIS_KEY_PREFIX + ruleId,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const keyBatches = chunkIntoBatches(ruleKeys, REDIS_BATCH_SIZE);
|
|
54
|
+
|
|
55
|
+
await executeBatchesSequentially(keyBatches, async (keysBatch) => {
|
|
56
|
+
const queries = this.client.multi();
|
|
57
|
+
|
|
58
|
+
for (const ruleKey of keysBatch) {
|
|
59
|
+
queries.del(ruleKey);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await queries.exec();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async deleteAllRules(): Promise<number> {
|
|
67
|
+
let cursor = "0";
|
|
68
|
+
let total = 0;
|
|
69
|
+
|
|
70
|
+
do {
|
|
71
|
+
const reply = await this.client.scan(cursor, {
|
|
72
|
+
MATCH: `${ACCESS_RULE_REDIS_KEY_PREFIX}*`,
|
|
73
|
+
COUNT: REDIS_BATCH_SIZE,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const ids = reply.keys.map((key) =>
|
|
77
|
+
key.slice(ACCESS_RULE_REDIS_KEY_PREFIX.length),
|
|
78
|
+
);
|
|
79
|
+
await this.deleteRules(ids);
|
|
80
|
+
|
|
81
|
+
total += ids.length;
|
|
82
|
+
cursor = reply.cursor;
|
|
83
|
+
} while ("0" !== cursor);
|
|
84
|
+
|
|
85
|
+
return total;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
protected async insertRuleEntries(
|
|
89
|
+
ruleEntries: AccessRuleEntry[],
|
|
90
|
+
): Promise<string[]> {
|
|
91
|
+
const queries = this.client.multi();
|
|
92
|
+
|
|
93
|
+
const ruleKeys = ruleEntries.map((ruleEntry) => {
|
|
94
|
+
const { rule, expiresUnixTimestamp } = ruleEntry;
|
|
95
|
+
|
|
96
|
+
const ruleKey = getAccessRuleRedisKey(rule);
|
|
97
|
+
const ruleValue = getRedisRuleValue(rule);
|
|
98
|
+
|
|
99
|
+
queries.hSet(ruleKey, ruleValue);
|
|
100
|
+
|
|
101
|
+
if (expiresUnixTimestamp) {
|
|
102
|
+
// Redis expireAt expects seconds. Validate that timestamp is in seconds, not milliseconds.
|
|
103
|
+
// Unix timestamps in milliseconds (since 1970) are > 10 billion
|
|
104
|
+
// Unix timestamps in seconds won't reach 10 billion until year 2286
|
|
105
|
+
const MILLISECOND_THRESHOLD = 10_000_000_000;
|
|
106
|
+
if (expiresUnixTimestamp > MILLISECOND_THRESHOLD) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Invalid expiry timestamp: ${expiresUnixTimestamp}. Timestamp must be in seconds, not milliseconds.`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
queries.expireAt(ruleKey, expiresUnixTimestamp);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return ruleKey;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await queries.exec();
|
|
118
|
+
|
|
119
|
+
return ruleKeys;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export const getRedisRuleValue = (rule: AccessRule): Record<string, string> =>
|
|
124
|
+
Object.fromEntries(
|
|
125
|
+
Object.entries(rule).map(([key, value]) => [key, String(value)]),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
export class DummyRedisRulesWriter implements AccessRulesWriter {
|
|
129
|
+
constructor(private readonly logger: Logger) {}
|
|
130
|
+
|
|
131
|
+
async insertRules(ruleEntries: AccessRuleEntry[]): Promise<string[]> {
|
|
132
|
+
this.logger.info(() => ({
|
|
133
|
+
msg: "Dummy insertRules() has no effect (redis is not ready)",
|
|
134
|
+
data: {
|
|
135
|
+
ruleEntries,
|
|
136
|
+
},
|
|
137
|
+
}));
|
|
138
|
+
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async deleteRules(ruleIds: string[]): Promise<void> {
|
|
143
|
+
this.logger.info(() => ({
|
|
144
|
+
msg: "Dummy deleteRules() has no effect (redis is not ready)",
|
|
145
|
+
data: {
|
|
146
|
+
ruleIds,
|
|
147
|
+
},
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async deleteAllRules(): Promise<number> {
|
|
152
|
+
this.logger.info(() => ({
|
|
153
|
+
msg: "Dummy deleteAllRules() has no effect (redis is not ready)",
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
}
|
package/src/rule.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
import type { CaptchaType } from "@prosopo/types";
|
|
15
|
+
|
|
16
|
+
export enum AccessPolicyType {
|
|
17
|
+
Block = "block",
|
|
18
|
+
Restrict = "restrict",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type AccessPolicy = {
|
|
22
|
+
type: AccessPolicyType;
|
|
23
|
+
captchaType?: CaptchaType;
|
|
24
|
+
description?: string;
|
|
25
|
+
solvedImagesCount?: number;
|
|
26
|
+
imageThreshold?: number;
|
|
27
|
+
powDifficulty?: number;
|
|
28
|
+
unsolvedImagesCount?: number;
|
|
29
|
+
frictionlessScore?: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type PolicyScope = {
|
|
33
|
+
clientId?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type UserIp = {
|
|
37
|
+
numericIp?: bigint;
|
|
38
|
+
numericIpMaskMin?: bigint;
|
|
39
|
+
numericIpMaskMax?: bigint;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type UserAttributes = {
|
|
43
|
+
userId?: string;
|
|
44
|
+
ja4Hash?: string;
|
|
45
|
+
headersHash?: string;
|
|
46
|
+
userAgentHash?: string;
|
|
47
|
+
headHash?: string;
|
|
48
|
+
coords?: string;
|
|
49
|
+
countryCode?: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type UserScope = UserAttributes & UserIp;
|
|
53
|
+
|
|
54
|
+
// flat structure is used to fit the Redis requirements
|
|
55
|
+
export type AccessRule = AccessPolicy &
|
|
56
|
+
PolicyScope &
|
|
57
|
+
UserScope & {
|
|
58
|
+
groupId?: string;
|
|
59
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
export { accessRuleInput, type AccessRulesFilterInput } from "./ruleInput.js";
|
|
16
|
+
|
|
17
|
+
export { accessPolicyInput, policyScopeInput } from "./policyInput.js";
|
|
18
|
+
|
|
19
|
+
export { userScopeInput } from "./userScopeInput.js";
|
|
@@ -0,0 +1,51 @@
|
|
|
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 type { AllKeys } from "@prosopo/common";
|
|
16
|
+
import { CaptchaTypeSchema } from "@prosopo/types";
|
|
17
|
+
import { type ZodType, z } from "zod";
|
|
18
|
+
import {
|
|
19
|
+
type AccessPolicy,
|
|
20
|
+
AccessPolicyType,
|
|
21
|
+
type PolicyScope,
|
|
22
|
+
} from "#policy/rule.js";
|
|
23
|
+
|
|
24
|
+
export const accessPolicyInput = z.object({
|
|
25
|
+
type: z.nativeEnum(AccessPolicyType),
|
|
26
|
+
captchaType: CaptchaTypeSchema.optional(),
|
|
27
|
+
description: z.coerce.string().optional(),
|
|
28
|
+
// Redis stores values as strings, so coerce is needed to parse properly
|
|
29
|
+
solvedImagesCount: z.coerce.number().optional(),
|
|
30
|
+
// the percentage of image panels that must be solved per image CAPTCHA
|
|
31
|
+
imageThreshold: z.coerce.number().optional(),
|
|
32
|
+
// the Proof-of-Work difficulty level
|
|
33
|
+
powDifficulty: z.coerce.number().optional(),
|
|
34
|
+
// the number of unsolved image CAPTCHA challenges to serve
|
|
35
|
+
unsolvedImagesCount: z.coerce.number().optional(),
|
|
36
|
+
// used to increase the user's score
|
|
37
|
+
frictionlessScore: z.coerce.number().optional(),
|
|
38
|
+
} satisfies AllKeys<AccessPolicy>) satisfies ZodType<AccessPolicy>;
|
|
39
|
+
|
|
40
|
+
// Sanitize block policies by removing captchaType and solvedImagesCount
|
|
41
|
+
export const sanitizeAccessPolicy = (policy: AccessPolicy): AccessPolicy => {
|
|
42
|
+
if (policy.type === AccessPolicyType.Block) {
|
|
43
|
+
const { captchaType, solvedImagesCount, ...blockPolicy } = policy;
|
|
44
|
+
return blockPolicy;
|
|
45
|
+
}
|
|
46
|
+
return policy;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const policyScopeInput = z.object({
|
|
50
|
+
clientId: z.coerce.string().optional(),
|
|
51
|
+
} satisfies AllKeys<PolicyScope>) satisfies ZodType<PolicyScope>;
|
|
@@ -0,0 +1,103 @@
|
|
|
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 type { AllKeys } from "@prosopo/common";
|
|
16
|
+
import { type ZodType, z } from "zod";
|
|
17
|
+
import type { AccessPolicy, AccessRule, PolicyScope } from "#policy/rule.js";
|
|
18
|
+
import {
|
|
19
|
+
type AccessRuleEntry,
|
|
20
|
+
type AccessRulesFilter,
|
|
21
|
+
FilterScopeMatch,
|
|
22
|
+
} from "#policy/rulesStorage.js";
|
|
23
|
+
import { accessPolicyInput, policyScopeInput } from "./policyInput.js";
|
|
24
|
+
import { type UserScopeInput, userScopeInput } from "./userScopeInput.js";
|
|
25
|
+
|
|
26
|
+
type RuleGroupInput = {
|
|
27
|
+
groupId?: string;
|
|
28
|
+
ruleGroupId?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type AccessRuleInput = AccessPolicy &
|
|
32
|
+
PolicyScope &
|
|
33
|
+
UserScopeInput &
|
|
34
|
+
RuleGroupInput;
|
|
35
|
+
|
|
36
|
+
const ruleGroupInput = z
|
|
37
|
+
.object({
|
|
38
|
+
groupId: z.coerce.string().optional(),
|
|
39
|
+
ruleGroupId: z.coerce.string().optional(),
|
|
40
|
+
} satisfies AllKeys<RuleGroupInput>)
|
|
41
|
+
.transform((ruleGroupInput: RuleGroupInput) => {
|
|
42
|
+
const { ruleGroupId, ...ruleGroup } = ruleGroupInput;
|
|
43
|
+
|
|
44
|
+
if ("string" === typeof ruleGroupId) {
|
|
45
|
+
ruleGroup.groupId = ruleGroupId;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return ruleGroup;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export const accessRuleInput: ZodType<AccessRule> = z
|
|
52
|
+
.object({
|
|
53
|
+
...accessPolicyInput.shape,
|
|
54
|
+
...policyScopeInput.shape,
|
|
55
|
+
})
|
|
56
|
+
.and(userScopeInput)
|
|
57
|
+
.and(ruleGroupInput)
|
|
58
|
+
// transform is used for type safety only - plain "satisfies ZodType<x>" doesn't work after ".and()"
|
|
59
|
+
.transform((ruleInput: AccessRuleInput): AccessRule => ruleInput);
|
|
60
|
+
|
|
61
|
+
export const ruleEntryInput = z.object({
|
|
62
|
+
rule: accessRuleInput,
|
|
63
|
+
expiresUnixTimestamp: z.coerce.number().optional(),
|
|
64
|
+
} satisfies AllKeys<AccessRuleEntry>) satisfies ZodType<AccessRuleEntry>;
|
|
65
|
+
|
|
66
|
+
export type AccessRulesFilterInput = AccessRulesFilter & {
|
|
67
|
+
userScope?: UserScopeInput;
|
|
68
|
+
policyScopes?: PolicyScope[];
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const accessRulesFilterInput = z.object({
|
|
72
|
+
policyScope: policyScopeInput.optional(),
|
|
73
|
+
policyScopes: z.array(policyScopeInput).optional(),
|
|
74
|
+
policyScopeMatch: z
|
|
75
|
+
.nativeEnum(FilterScopeMatch)
|
|
76
|
+
.default(FilterScopeMatch.Exact),
|
|
77
|
+
userScope: userScopeInput.optional(),
|
|
78
|
+
userScopeMatch: z
|
|
79
|
+
.nativeEnum(FilterScopeMatch)
|
|
80
|
+
.default(FilterScopeMatch.Exact),
|
|
81
|
+
groupId: z.string().optional(),
|
|
82
|
+
} satisfies AllKeys<AccessRulesFilterInput>) satisfies ZodType<AccessRulesFilterInput>;
|
|
83
|
+
|
|
84
|
+
export const getAccessRuleFiltersFromInput = (
|
|
85
|
+
filterInput: AccessRulesFilterInput,
|
|
86
|
+
): AccessRulesFilter[] => {
|
|
87
|
+
const { policyScopes, policyScope, ...filterBase } = filterInput;
|
|
88
|
+
|
|
89
|
+
const allPolicyScopes = policyScopes || [];
|
|
90
|
+
|
|
91
|
+
if (policyScope) {
|
|
92
|
+
allPolicyScopes.push(policyScope);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (allPolicyScopes.length > 0) {
|
|
96
|
+
return allPolicyScopes.map((policyScope) => ({
|
|
97
|
+
...filterBase,
|
|
98
|
+
policyScope,
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return [filterBase];
|
|
103
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
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 crypto from "node:crypto";
|
|
16
|
+
import type { AllKeys } from "@prosopo/common";
|
|
17
|
+
import { getIPAddress } from "@prosopo/util";
|
|
18
|
+
import { Address4 } from "ip-address";
|
|
19
|
+
import { type ZodType, z } from "zod";
|
|
20
|
+
import type { UserAttributes, UserIp, UserScope } from "#policy/rule.js";
|
|
21
|
+
import type { UserAttributesRecord, UserIpRecord } from "#policy/ruleRecord.js";
|
|
22
|
+
|
|
23
|
+
export type UserAttributesInput = UserAttributes & UserAttributesRecord;
|
|
24
|
+
|
|
25
|
+
const userAttributesSchema = z.object({
|
|
26
|
+
// coerce is used for safety, as e.g., incoming userId can be digital
|
|
27
|
+
userId: z.coerce.string().optional(),
|
|
28
|
+
ja4Hash: z.coerce.string().optional(),
|
|
29
|
+
headersHash: z.coerce.string().optional(),
|
|
30
|
+
userAgentHash: z.coerce.string().optional(),
|
|
31
|
+
headHash: z.coerce.string().optional(),
|
|
32
|
+
coords: z.coerce.string().optional(),
|
|
33
|
+
countryCode: z.coerce.string().optional(),
|
|
34
|
+
} satisfies AllKeys<UserAttributes>) satisfies ZodType<UserAttributes>;
|
|
35
|
+
|
|
36
|
+
const userAttributesInput = z
|
|
37
|
+
.object({
|
|
38
|
+
...userAttributesSchema.shape,
|
|
39
|
+
userAgent: z.coerce.string().optional(),
|
|
40
|
+
} satisfies AllKeys<UserAttributesInput>)
|
|
41
|
+
.transform((userAttributesInput: UserAttributesInput): UserAttributes => {
|
|
42
|
+
// this line creates a new "userAttributes", without userAgent
|
|
43
|
+
const { userAgent, ...userScope } = userAttributesInput;
|
|
44
|
+
|
|
45
|
+
if ("string" === typeof userAgent) {
|
|
46
|
+
userScope.userAgentHash = hashUserAgent(userAgent);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return userScope;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const hashUserAgent = (userAgent: string): string =>
|
|
53
|
+
crypto.createHash("sha256").update(userAgent).digest("hex");
|
|
54
|
+
|
|
55
|
+
export type UserIpInput = UserIp & UserIpRecord;
|
|
56
|
+
|
|
57
|
+
const userIpSchema = z.object({
|
|
58
|
+
numericIp: z.coerce.bigint().optional(),
|
|
59
|
+
numericIpMaskMin: z.coerce.bigint().optional(),
|
|
60
|
+
numericIpMaskMax: z.coerce.bigint().optional(),
|
|
61
|
+
} satisfies AllKeys<UserIp>) satisfies ZodType<UserIp>;
|
|
62
|
+
|
|
63
|
+
const userIpInput = z
|
|
64
|
+
.object({
|
|
65
|
+
...userIpSchema.shape,
|
|
66
|
+
ip: z.string().optional(),
|
|
67
|
+
ipMask: z.string().optional(),
|
|
68
|
+
} satisfies AllKeys<UserIpInput>)
|
|
69
|
+
.transform((userIpInput: UserIpInput): UserIp => {
|
|
70
|
+
// this line creates a new "userScope", without ip and ipMask
|
|
71
|
+
const { ip, ipMask, ...numericUserIp } = userIpInput;
|
|
72
|
+
|
|
73
|
+
if ("string" === typeof ip) {
|
|
74
|
+
numericUserIp.numericIp = getIPAddress(ip).bigInt();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Assuming ipMask is already validated to be a string in CIDR format
|
|
78
|
+
if ("string" === typeof ipMask) {
|
|
79
|
+
// Create an Address4 object from the CIDR string.
|
|
80
|
+
// Address4 automatically understands CIDR notation and represents the entire network range.
|
|
81
|
+
const ipObject = new Address4(ipMask);
|
|
82
|
+
|
|
83
|
+
// The minimum IP in the CIDR range is the start address of the network.
|
|
84
|
+
numericUserIp.numericIpMaskMin = ipObject.startAddress().bigInt();
|
|
85
|
+
|
|
86
|
+
// The maximum IP in the CIDR range is the end address of the network.
|
|
87
|
+
numericUserIp.numericIpMaskMax = ipObject.endAddress().bigInt();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return numericUserIp;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export type UserScopeInput = UserAttributesInput & UserIpInput;
|
|
94
|
+
|
|
95
|
+
export const userScopeSchema = z.object({
|
|
96
|
+
...userIpSchema.shape,
|
|
97
|
+
...userAttributesSchema.shape,
|
|
98
|
+
} satisfies AllKeys<UserScope>) satisfies ZodType<UserScope>;
|
|
99
|
+
|
|
100
|
+
export const userScopeInput = z
|
|
101
|
+
.object({})
|
|
102
|
+
// unlike ...shape(), .and() includes transformations
|
|
103
|
+
.and(userIpInput)
|
|
104
|
+
.and(userAttributesInput)
|
|
105
|
+
.transform(
|
|
106
|
+
// transform is used for type safety only - plain "satisfies ZodType<x>" doesn't work after ".and()"
|
|
107
|
+
(userScopeInput): UserScopeInput => userScopeInput,
|
|
108
|
+
);
|
|
@@ -0,0 +1,69 @@
|
|
|
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 type {
|
|
16
|
+
AccessPolicy,
|
|
17
|
+
PolicyScope,
|
|
18
|
+
UserAttributes,
|
|
19
|
+
} from "#policy/rule.js";
|
|
20
|
+
|
|
21
|
+
export type UserAttributesRecord = Omit<UserAttributes, "userAgentHash"> & {
|
|
22
|
+
userAgent?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const userAttributesRecordFields = [
|
|
26
|
+
"userId",
|
|
27
|
+
"ja4Hash",
|
|
28
|
+
"headersHash",
|
|
29
|
+
"userAgent",
|
|
30
|
+
"headHash",
|
|
31
|
+
"coords",
|
|
32
|
+
"countryCode",
|
|
33
|
+
] as const satisfies (keyof UserAttributesRecord)[];
|
|
34
|
+
|
|
35
|
+
export type UserIpRecord = {
|
|
36
|
+
// human-friendly ip versions.
|
|
37
|
+
ip?: string;
|
|
38
|
+
// 127.0.0.1/24
|
|
39
|
+
ipMask?: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const userIpRecordFields = [
|
|
43
|
+
"ip",
|
|
44
|
+
"ipMask",
|
|
45
|
+
] as const satisfies (keyof UserIpRecord)[];
|
|
46
|
+
|
|
47
|
+
export type UserScopeRecord = UserAttributesRecord & UserIpRecord;
|
|
48
|
+
|
|
49
|
+
export const userScopeRecordFields = [
|
|
50
|
+
...userAttributesRecordFields,
|
|
51
|
+
...userIpRecordFields,
|
|
52
|
+
] as const satisfies (keyof UserScopeRecord)[];
|
|
53
|
+
|
|
54
|
+
export type UserScopeRecordField = (typeof userScopeRecordFields)[number];
|
|
55
|
+
|
|
56
|
+
export type AccessRuleRecord = AccessPolicy &
|
|
57
|
+
PolicyScope &
|
|
58
|
+
UserScopeRecord & {
|
|
59
|
+
ruleGroupId?: string;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const getUserScopeRecordFromAccessRuleRecord = (
|
|
63
|
+
ruleRecord: AccessRuleRecord,
|
|
64
|
+
): UserScopeRecord =>
|
|
65
|
+
Object.fromEntries(
|
|
66
|
+
userScopeRecordFields
|
|
67
|
+
.map((field) => [field, ruleRecord[field]])
|
|
68
|
+
.filter(([, value]) => value !== undefined),
|
|
69
|
+
);
|
|
@@ -0,0 +1,72 @@
|
|
|
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 type { AccessRule, PolicyScope, UserScope } from "#policy/rule.js";
|
|
16
|
+
|
|
17
|
+
export enum FilterScopeMatch {
|
|
18
|
+
Exact = "exact",
|
|
19
|
+
Greedy = "greedy",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type AccessRulesFilter = {
|
|
23
|
+
policyScope?: PolicyScope;
|
|
24
|
+
/**
|
|
25
|
+
* Exact: "clientId" => client rules, "undefined" => global rules. Used by the API
|
|
26
|
+
* Greedy: "clientId" => client + global rules, "undefined" => any rules. Used by the Express middleware
|
|
27
|
+
*/
|
|
28
|
+
policyScopeMatch?: FilterScopeMatch;
|
|
29
|
+
userScope?: UserScope;
|
|
30
|
+
/**
|
|
31
|
+
* Exact: "clientId" => client rules, "undefined" => global rules. Used by the API
|
|
32
|
+
* Greedy: "clientId" => client + global rules, "undefined" => any rules. Used by the Express middleware
|
|
33
|
+
*/
|
|
34
|
+
userScopeMatch?: FilterScopeMatch;
|
|
35
|
+
groupId?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type AccessRuleEntry = {
|
|
39
|
+
rule: AccessRule;
|
|
40
|
+
expiresUnixTimestamp?: number;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type AccessRulesReader = {
|
|
44
|
+
fetchRules(ruleIds: string[]): Promise<AccessRuleEntry[]>;
|
|
45
|
+
|
|
46
|
+
getMissingRuleIds(ruleIds: string[]): Promise<string[]>;
|
|
47
|
+
|
|
48
|
+
findRules(
|
|
49
|
+
filter: AccessRulesFilter,
|
|
50
|
+
matchingFieldsOnly?: boolean,
|
|
51
|
+
skipEmptyUserScopes?: boolean,
|
|
52
|
+
): Promise<AccessRule[]>;
|
|
53
|
+
|
|
54
|
+
findRuleIds(
|
|
55
|
+
filter: AccessRulesFilter,
|
|
56
|
+
matchingFieldsOnly?: boolean,
|
|
57
|
+
): Promise<string[]>;
|
|
58
|
+
|
|
59
|
+
fetchAllRuleIds(
|
|
60
|
+
batchHandler: (ruleIds: string[]) => Promise<void>,
|
|
61
|
+
): Promise<void>;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type AccessRulesWriter = {
|
|
65
|
+
insertRules(ruleEntries: AccessRuleEntry[]): Promise<string[]>;
|
|
66
|
+
|
|
67
|
+
deleteRules(ruleIds: string[]): Promise<void>;
|
|
68
|
+
|
|
69
|
+
deleteAllRules(): Promise<number>;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type AccessRulesStorage = AccessRulesReader & AccessRulesWriter;
|