@prosopo/user-access-policy 3.14.13 → 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.
Files changed (65) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +2 -5
  2. package/.turbo/turbo-build$colon$tsc.log +14 -17
  3. package/.turbo/turbo-build.log +3 -9
  4. package/CHANGELOG.md +36 -0
  5. package/LICENSE +201 -0
  6. package/package.json +83 -81
  7. package/src/.export.ts +0 -65
  8. package/src/api/.export.ts +0 -25
  9. package/src/api/accessRulesApiClient.ts +0 -13
  10. package/src/api/delete/.export.ts +0 -18
  11. package/src/api/delete/deleteAllRules.ts +0 -47
  12. package/src/api/delete/deleteRuleGroups.ts +0 -96
  13. package/src/api/delete/deleteRules.ts +0 -87
  14. package/src/api/read/.export.ts +0 -25
  15. package/src/api/read/fetchRules.ts +0 -96
  16. package/src/api/read/findRuleIds.ts +0 -97
  17. package/src/api/read/getMissingIds.ts +0 -81
  18. package/src/api/ruleApiRoutes.ts +0 -146
  19. package/src/api/rulesApiClient.ts +0 -154
  20. package/src/api/write/.export.ts +0 -15
  21. package/src/api/write/insertRules.ts +0 -185
  22. package/src/api/write/rehashRules.ts +0 -85
  23. package/src/classifyBrowser.ts +0 -77
  24. package/src/classifyOs.ts +0 -74
  25. package/src/headerMatch.ts +0 -180
  26. package/src/matchedRule.ts +0 -99
  27. package/src/mongoose/.export.ts +0 -15
  28. package/src/mongoose/mongooseRuleSchema.ts +0 -75
  29. package/src/redis/.export.ts +0 -17
  30. package/src/redis/reader/redisAggregate.ts +0 -157
  31. package/src/redis/reader/redisRulesQuery.ts +0 -268
  32. package/src/redis/reader/redisRulesReader.ts +0 -738
  33. package/src/redis/reader/redisRulesSplitQuery.ts +0 -234
  34. package/src/redis/redisClient.ts +0 -125
  35. package/src/redis/redisRuleIndex.ts +0 -121
  36. package/src/redis/redisRulesStorage.ts +0 -68
  37. package/src/redis/redisRulesWriter.ts +0 -172
  38. package/src/rule.ts +0 -122
  39. package/src/ruleInput/.export.ts +0 -19
  40. package/src/ruleInput/policyInput.ts +0 -120
  41. package/src/ruleInput/ruleInput.ts +0 -123
  42. package/src/ruleInput/userScopeInput.ts +0 -142
  43. package/src/ruleRecord.ts +0 -77
  44. package/src/rulesStorage.ts +0 -90
  45. package/src/tests/blockingRuleShapes.unit.test.ts +0 -244
  46. package/src/tests/classifyBrowser.unit.test.ts +0 -138
  47. package/src/tests/classifyOs.unit.test.ts +0 -97
  48. package/src/tests/headerMatch.unit.test.ts +0 -350
  49. package/src/tests/insertRulesEndpoint.unit.test.ts +0 -89
  50. package/src/tests/matchedRule.unit.test.ts +0 -184
  51. package/src/tests/policyInput.unit.test.ts +0 -218
  52. package/src/tests/redis/deferredPoolClause.benchmark.ts +0 -249
  53. package/src/tests/redis/reader/redisAggregate.unit.test.ts +0 -121
  54. package/src/tests/redis/reader/redisRulesQuery.unit.test.ts +0 -349
  55. package/src/tests/redis/reader/redisRulesSplitQuery.unit.test.ts +0 -297
  56. package/src/tests/redis/redisRulesReaderLoad.benchmark.integration.test.ts +0 -520
  57. package/src/tests/redis/redisRulesReaderRank.benchmark.integration.test.ts +0 -350
  58. package/src/tests/redis/redisRulesStorage.integration.test.ts +0 -1811
  59. package/src/tests/testLogger.ts +0 -38
  60. package/src/tests/transformRule.unit.test.ts +0 -345
  61. package/src/transformRule.ts +0 -133
  62. package/tsconfig.cjs.json +0 -41
  63. package/tsconfig.json +0 -47
  64. package/tsconfig.tsbuildinfo +0 -1
  65. package/tsconfig.types.json +0 -9
@@ -1,234 +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 { getBlockPoolClause } from "#policy/redis/reader/redisRulesQuery.js";
16
- import { GLOBAL_CLIENT_SCOPE_SENTINEL, type UserScope } from "#policy/rule.js";
17
-
18
- // Escapes special characters in Redis TAG queries. Mirrors the escape
19
- // function in redisRulesQuery.ts — kept local to avoid coupling the two
20
- // query builders since they'll diverge in shape.
21
- const escapeTagValue = (value: string): string =>
22
- value.replace(/([,.<>{}\[\]"':;!@#$%^&*()\-+=~|/\\])/g, "\\$1");
23
-
24
- // Fields where TAG queries need escaping (they may hold JSON etc.).
25
- const FIELDS_REQUIRING_ESCAPE: ReadonlySet<keyof UserScope> = new Set([
26
- "coords",
27
- ]);
28
-
29
- // Fields indexed as NUMERIC — require range syntax `@x:[N N]` not TAG.
30
- const NUMERIC_FIELDS: ReadonlySet<keyof UserScope> = new Set(["asn"]);
31
-
32
- // Enumerating the scalar (non-IP) user-scope fields keeps the "no
33
- // user-scope constraint" fall-through query stable when userScopeSchema
34
- // grows a new field — the enumeration below drives ismissing() clauses.
35
- const SCALAR_USER_SCOPE_FIELDS: ReadonlyArray<keyof UserScope> = [
36
- "userId",
37
- "ja4Hash",
38
- "headersHash",
39
- "userAgentHash",
40
- "headHash",
41
- "coords",
42
- "countryCode",
43
- "asn",
44
- "os",
45
- "browser",
46
- ];
47
-
48
- const ALL_USER_SCOPE_FIELDS: ReadonlyArray<keyof UserScope> = [
49
- ...SCALAR_USER_SCOPE_FIELDS,
50
- "numericIp",
51
- "numericIpMaskMin",
52
- "numericIpMaskMax",
53
- ];
54
-
55
- // Global rules written under the new sentinel are found via
56
- // `@clientId:{global}`; the ismissing() fallback covers legacy rules
57
- // that predate the sentinel stamp (rehash-all migrates them). This
58
- // disjunction still hits the clientId posting list rather than walking
59
- // the full rule set the way the old single-query path did.
60
- const GLOBAL_SCOPE_INNER = `@clientId:{${GLOBAL_CLIENT_SCOPE_SENTINEL}} | ismissing(@clientId)`;
61
-
62
- const buildScopeClause = (clientId: string | undefined): string => {
63
- if (clientId === undefined) {
64
- return `( ${GLOBAL_SCOPE_INNER} )`;
65
- }
66
- return `( @clientId:{${clientId}} | ${GLOBAL_SCOPE_INNER} )`;
67
- };
68
-
69
- const buildFieldClause = (
70
- field: keyof UserScope,
71
- value: unknown,
72
- ): string | null => {
73
- if (value === undefined) {
74
- return null;
75
- }
76
- const stringValue = String(value);
77
- if (NUMERIC_FIELDS.has(field)) {
78
- return `@${field}:[${stringValue} ${stringValue}]`;
79
- }
80
- const queryValue = FIELDS_REQUIRING_ESCAPE.has(field)
81
- ? escapeTagValue(stringValue)
82
- : stringValue;
83
- return `@${field}:{${queryValue}}`;
84
- };
85
-
86
- type SubQuery = {
87
- // A short label used for logging + metrics. Not part of the query
88
- // wire format.
89
- kind: string;
90
- query: string;
91
- };
92
-
93
- /**
94
- * Build the set of FT sub-queries that together cover every rule that
95
- * could apply to a request.
96
- *
97
- * The single-query design this replaces produced one FT.AGGREGATE whose
98
- * candidate set was the entire scope's rule pool — the APPLY / SORTBY
99
- * pipeline then walked every rule per request, which degrades sharply
100
- * once rule populations reach the 10k+ range. The split approach fires
101
- * one probe per populated request field. Each probe uses the field's
102
- * own index posting list, so a 17k IP-ban population collapses to 1
103
- * result (exact IP) plus a handful (CIDR ranges containing the IP)
104
- * instead of forcing a full-set scan.
105
- *
106
- * A single fall-through probe covers rules with no user-scope constraint
107
- * (client-wide restriction); those are rare so the ismissing()-heavy
108
- * query is cheap in practice.
109
- *
110
- * Rank + strict-match filtering happens in JS via `rankCandidateRules`
111
- * after the union — the FT layer is used purely as a candidate fetcher.
112
- *
113
- * `blockOnly` narrows the candidate set to rules with `type:{block}` —
114
- * used by the hard-block middleware which never needs Restrict rules.
115
- * `includeDeferred` widens that pool back out to
116
- * `(@type:{block} | @deferToVerify:{true})` for the verify-time
117
- * hard-block lookup, where a deferred Restrict is a valid block.
118
- * Every other caller passes false so both Block and Restrict rules are
119
- * fetched; the JS-side ranker picks the right one via specificity +
120
- * severity. Merging the block and restrict paths behind one split-query
121
- * fixed the Twickets IP-rule regression: the previous FT.AGGREGATE ranker
122
- * used for non-block lookups was truncating specific-IP restrict rules
123
- * out of the top-N when many higher-specificity irrelevant rules matched
124
- * the greedy `ismissing()` disjunction.
125
- */
126
- export const buildScopedRulesSubQueries = (
127
- userScope: UserScope,
128
- clientId: string | undefined,
129
- options: { blockOnly?: boolean; includeDeferred?: boolean } = {},
130
- ): SubQuery[] => {
131
- const scopeClause = buildScopeClause(clientId);
132
-
133
- // `includeDeferred` emits the probe set twice — once for plain hard
134
- // blocks, once for deferred rules — rather than widening one type
135
- // clause to `(@type:{block} | @deferToVerify:{true})`.
136
- //
137
- // Widening merges both populations into a single probe, so they share
138
- // one SPLIT_MAX_CANDIDATES_PER_SUB budget and a dense deferred cohort
139
- // can truncate hard blocks out of the candidate set. Measured on a
140
- // scope with 400 block + 400 deferred rules on one ja4Hash: the merged
141
- // probe returns 800 against a cap of 500, so 300 candidates are
142
- // dropped with no ordering guarantee about which. That is precisely
143
- // the crowding `blockOnly` was introduced to prevent.
144
- //
145
- // Two disjoint probe sets each get their own budget, so a hard block
146
- // is never displaced by a deferred rule. The pools are disjoint by
147
- // construction (the block probe excludes deferred), so the union
148
- // double-counts nothing.
149
- // Probe `kind` is only namespaced when there are genuinely two pools;
150
- // single-pool callers keep the original unprefixed labels.
151
- const typePools: Array<{ tag: string; clause: string }> = options.blockOnly
152
- ? options.includeDeferred === true
153
- ? [
154
- { tag: "block", clause: `${getBlockPoolClause(false)} ` },
155
- { tag: "deferred", clause: "@deferToVerify:{true} " },
156
- ]
157
- : [{ tag: "", clause: `${getBlockPoolClause(false)} ` }]
158
- : [{ tag: "", clause: "" }];
159
-
160
- const subQueries: SubQuery[] = [];
161
- for (const pool of typePools) {
162
- const prefix = `${pool.clause}${scopeClause}`;
163
- const label = pool.tag === "" ? "" : `${pool.tag}:`;
164
- subQueries.push(...buildProbes(userScope, prefix, label));
165
- }
166
- return subQueries;
167
- };
168
-
169
- const buildProbes = (
170
- userScope: UserScope,
171
- prefix: string,
172
- label: string,
173
- ): SubQuery[] => {
174
- const subQueries: SubQuery[] = [];
175
-
176
- // One probe per populated scalar user-scope field. Each uses that
177
- // field's posting list rather than intersecting ismissing() across
178
- // the full set.
179
- for (const field of SCALAR_USER_SCOPE_FIELDS) {
180
- const clause = buildFieldClause(field, userScope[field]);
181
- if (clause === null) {
182
- continue;
183
- }
184
- subQueries.push({
185
- kind: `${label}field:${field}`,
186
- query: `${prefix} ${clause}`,
187
- });
188
- }
189
-
190
- // IP: two disjoint probes so each hits a single index. The exact-IP
191
- // probe catches individual-IP bans (the dominant shape in bulk-ban
192
- // scripts). The mask-range probe catches CIDR aggregates containing
193
- // the request IP. Splitting them means neither has to walk a mixed
194
- // posting list — the mask-range probe was the slower of the two
195
- // under load, so isolating it lets Redis skip it entirely for hits
196
- // on the fast path.
197
- const requestIp = userScope.numericIp;
198
- if (requestIp !== undefined) {
199
- subQueries.push({
200
- kind: `${label}ip:exact`,
201
- query: `${prefix} @numericIp:[${requestIp} ${requestIp}]`,
202
- });
203
- subQueries.push({
204
- kind: `${label}ip:mask`,
205
- query: `${prefix} @numericIpMaskMin:[-inf ${requestIp}] @numericIpMaskMax:[${requestIp} +inf]`,
206
- });
207
- }
208
-
209
- // Fall-through: rules that constrain nothing on the user scope
210
- // ("block everything for this client / everyone"). Rare in
211
- // practice — a handful per tenant at most — so the ismissing()
212
- // intersection is cheap despite touching every field.
213
- const noScopeIsmissing = ALL_USER_SCOPE_FIELDS.map(
214
- (field) => `ismissing(@${field})`,
215
- ).join(" ");
216
- subQueries.push({
217
- kind: `${label}no-user-scope`,
218
- query: `${prefix} ${noScopeIsmissing}`,
219
- });
220
-
221
- return subQueries;
222
- };
223
-
224
- /**
225
- * Backwards-compatible alias for the previous block-only builder. New
226
- * callers should use `buildScopedRulesSubQueries` directly.
227
- *
228
- * @deprecated use `buildScopedRulesSubQueries(..., { blockOnly: true })`
229
- */
230
- export const buildScopedBlockSubQueries = (
231
- userScope: UserScope,
232
- clientId: string | undefined,
233
- ): SubQuery[] =>
234
- buildScopedRulesSubQueries(userScope, clientId, { blockOnly: true });
@@ -1,125 +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 type { Logger } from "@prosopo/logger";
16
- import type { RedisClientType } from "redis";
17
- import { type ZodType, type ZodTypeDef, z } from "zod";
18
-
19
- export const REDIS_BATCH_SIZE = 1_000;
20
-
21
- export const getMissingRedisKeys = async (
22
- client: RedisClientType,
23
- keys: string[],
24
- ): Promise<string[]> => {
25
- const queries = client.multi();
26
-
27
- keys.map((key) => {
28
- queries.exists(key);
29
- });
30
-
31
- const records: unknown[] = await queries.exec();
32
-
33
- const missingKeys: string[] = [];
34
-
35
- records.map((exists, recordIndex) => {
36
- if ("0" === String(exists)) {
37
- const key = keys[recordIndex];
38
-
39
- if (key) {
40
- missingKeys.push(key);
41
- }
42
- }
43
- });
44
-
45
- return missingKeys;
46
- };
47
-
48
- export const fetchRedisHashRecords = async (
49
- client: RedisClientType,
50
- keys: string[],
51
- logger: Logger,
52
- ): Promise<{ records: object[]; expirations: (number | undefined)[] }> => {
53
- const rulesPipe = client.multi();
54
- const expirationPipe = client.multi();
55
-
56
- for (const key of keys) {
57
- rulesPipe.hGetAll(key);
58
- expirationPipe.expireTime(key);
59
- }
60
-
61
- const records = (await rulesPipe.exec()) as object[];
62
- const expirationRecords = (await expirationPipe.exec()) as unknown[];
63
-
64
- return {
65
- records: records,
66
- expirations: parseExpirationRecords(expirationRecords, logger),
67
- };
68
- };
69
-
70
- // `recordSchema` is intentionally typed with `unknown` as the input
71
- // position. Some schemas use `z.preprocess` (e.g. accessPolicyInput's
72
- // deferToVerify boolean coercion from Redis strings), which widens the
73
- // zod input type. The strict `ZodType<T, ZodTypeDef, T>` form rejects
74
- // those at the call site even though they parse correctly at runtime.
75
- export const parseRedisRecords = <T>(
76
- records: unknown[],
77
- recordSchema: ZodType<T, ZodTypeDef, unknown>,
78
- logger: Logger,
79
- ): T[] =>
80
- records.flatMap((record) => {
81
- const parseResult = recordSchema.safeParse(record);
82
-
83
- if (parseResult.success) {
84
- return [parseResult.data];
85
- }
86
-
87
- logger.error(() => ({
88
- msg: "Failed to parse Redis record",
89
- data: { record, error: parseResult.error },
90
- }));
91
-
92
- return [];
93
- });
94
-
95
- const expirationRecordSchema = z.coerce.number();
96
- // Redis returns -1 when expiration is not set
97
- const UNSET_EXPIRATION_VALUE = -1;
98
-
99
- const parseExpirationRecords = <T>(
100
- records: unknown[],
101
- logger: Logger,
102
- ): (number | undefined)[] =>
103
- records.flatMap((record) => {
104
- const parseResult = expirationRecordSchema.safeParse(record);
105
-
106
- if (parseResult.success) {
107
- const expiration =
108
- UNSET_EXPIRATION_VALUE === parseResult.data
109
- ? undefined
110
- : parseResult.data;
111
-
112
- return [expiration];
113
- }
114
-
115
- logger.error(() => ({
116
- msg: "Failed to parse Redis expiration record",
117
- data: {
118
- record,
119
- error: parseResult.error,
120
- },
121
- }));
122
-
123
- // ensure consistent output length
124
- return [undefined];
125
- });
@@ -1,121 +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 type { AllKeys, Keys } from "@prosopo/common";
16
- import type { RedisIndex } from "@prosopo/redis-client";
17
- import { type RediSearchSchema, SCHEMA_FIELD_TYPE } from "@redis/search";
18
- import type {
19
- AccessRule,
20
- PolicyScope,
21
- UserAttributes,
22
- UserIp,
23
- UserScope,
24
- } from "#policy/rule.js";
25
- import { makeAccessRuleHash } from "#policy/transformRule.js";
26
-
27
- export const userIpRedisSchema: RediSearchSchema = {
28
- numericIpMaskMin: { type: SCHEMA_FIELD_TYPE.NUMERIC, INDEXMISSING: true },
29
- numericIpMaskMax: { type: SCHEMA_FIELD_TYPE.NUMERIC, INDEXMISSING: true },
30
- numericIp: { type: SCHEMA_FIELD_TYPE.NUMERIC, INDEXMISSING: true },
31
- } satisfies AllKeys<UserIp>;
32
-
33
- export const userAttributesRedisSchema: RediSearchSchema = {
34
- userId: { type: SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
35
- ja4Hash: { type: SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
36
- headersHash: { type: SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
37
- userAgentHash: { type: SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
38
- headHash: { type: SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
39
- // Use pipe separator for coords since JSON strings contain commas
40
- coords: { type: SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true, SEPARATOR: "|" },
41
- countryCode: { type: SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
42
- asn: { type: SCHEMA_FIELD_TYPE.NUMERIC, INDEXMISSING: true },
43
- os: { type: SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
44
- browser: { type: SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
45
- // Header-restriction fields. NONE of these are queried on the hot path:
46
- // the split-query builder probes neither `headerMatch` nor the
47
- // name/value/operator triple, and a header rule populates none of the
48
- // fields that builder does probe, so it is returned by the `no-user-scope`
49
- // fall-through probe (the same route `os` rules take) and the concrete
50
- // condition is evaluated in code by `accessRuleHeaderMatches`. They are
51
- // indexed to satisfy the schema exhaustiveness check, and `headerMatch`
52
- // additionally gates candidacy JS-side in `ruleApplies` and scores one
53
- // specificity point in the reader's SPECIFICITY_EXPR.
54
- headerMatch: { type: SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
55
- headerName: { type: SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
56
- headerValue: { type: SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
57
- headerOperator: { type: SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
58
- webBotAuthAgent: { type: SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
59
- } satisfies AllKeys<UserAttributes>;
60
-
61
- export const userScopeRedisSchema: RediSearchSchema = {
62
- ...userAttributesRedisSchema,
63
- ...userIpRedisSchema,
64
- } satisfies Keys<UserScope>;
65
-
66
- export const policyScopeRedisSchema: RediSearchSchema = {
67
- clientId: {
68
- type: SCHEMA_FIELD_TYPE.TAG,
69
- INDEXMISSING: true,
70
- },
71
- } satisfies AllKeys<PolicyScope>;
72
-
73
- /**
74
- * Note on the field type decision
75
- *
76
- * TAG is designed for the exact value matching
77
- * TEXT is designed for the word-based and pattern matching
78
- *
79
- * For our goal TAG fits perfectly and, more performant
80
- */
81
- export const accessRuleRedisSchema: RediSearchSchema = {
82
- ...policyScopeRedisSchema,
83
- ...userScopeRedisSchema,
84
- groupId: { type: SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
85
- // Indexed so request-time callers that only care about Block policies
86
- // (checkForHardBlock, blockMiddleware) can pre-filter via @type:{block}
87
- // before the SERVER_SIDE_RANK_TOP_N cap kicks in. Without this, dense
88
- // Restrict / routing-Block populations push the hard-block rules out
89
- // of the top-N candidate set and the lookup silently misses them.
90
- type: { type: SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
91
- // Indexed so `checkForHardBlock` can widen its Block-only pool to
92
- // `(@type:{block} | @deferToVerify:{true})`. Deferred rules are
93
- // skipped at request time and enforced at verify, so a deferred
94
- // Restrict is a legitimate hard block — without this field in the
95
- // index it can't be selected server-side and never fires.
96
- // Stored by `getRedisRuleValue` as the string "true"/"false".
97
- deferToVerify: { type: SCHEMA_FIELD_TYPE.TAG, INDEXMISSING: true },
98
- } satisfies Keys<AccessRule>;
99
-
100
- export const ACCESS_RULES_REDIS_INDEX_NAME = "index:user-access-rules";
101
-
102
- // names take space, so we use an acronym instead of the long-tailed one
103
- export const ACCESS_RULE_REDIS_KEY_PREFIX = "uar:";
104
-
105
- // Re-exported from #policy/rule.js — that module is types-only so both
106
- // the parse side (policyInput.ts) and the write/index side (this file
107
- // and redisRulesWriter.ts) can import the sentinel without pulling
108
- // transformRule into a cycle.
109
- export { GLOBAL_CLIENT_SCOPE_SENTINEL } from "#policy/rule.js";
110
-
111
- export const accessRulesRedisIndex: RedisIndex = {
112
- name: ACCESS_RULES_REDIS_INDEX_NAME,
113
- schema: accessRuleRedisSchema,
114
- options: {
115
- ON: "HASH" as const,
116
- PREFIX: [ACCESS_RULE_REDIS_KEY_PREFIX],
117
- },
118
- };
119
-
120
- export const getAccessRuleRedisKey = (rule: AccessRule): string =>
121
- ACCESS_RULE_REDIS_KEY_PREFIX + makeAccessRuleHash(rule);
@@ -1,68 +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 type { Logger } from "@prosopo/logger";
16
- import type { RedisConnection } from "@prosopo/redis-client";
17
- import type {
18
- AccessRulesReader,
19
- AccessRulesStorage,
20
- AccessRulesWriter,
21
- } from "#policy/rulesStorage.js";
22
- import {
23
- DummyRedisRulesReader,
24
- RedisRulesReader,
25
- } from "./reader/redisRulesReader.js";
26
- import { DummyRedisRulesWriter, RedisRulesWriter } from "./redisRulesWriter.js";
27
-
28
- export const createRedisAccessRulesStorage = (
29
- connection: RedisConnection,
30
- logger: Logger,
31
- ): AccessRulesStorage => {
32
- const storage: AccessRulesStorage = composeStorage(
33
- new DummyRedisRulesReader(logger),
34
- new DummyRedisRulesWriter(logger),
35
- );
36
-
37
- connection.getClient().then((client) => {
38
- const realStorage = composeStorage(
39
- new RedisRulesReader(client, logger),
40
- new RedisRulesWriter(client, logger),
41
- );
42
-
43
- // use assigning instead of var overwriting to keep the object reference
44
- Object.assign(storage, realStorage);
45
-
46
- logger.info(() => ({
47
- msg: "RedisAccessRules storage got a ready Redis client",
48
- }));
49
- });
50
-
51
- return storage;
52
- };
53
-
54
- const composeStorage = (
55
- reader: AccessRulesReader,
56
- writer: AccessRulesWriter,
57
- ): AccessRulesStorage => ({
58
- // reader
59
- fetchRules: reader.fetchRules.bind(reader),
60
- getMissingRuleIds: reader.getMissingRuleIds.bind(reader),
61
- findRules: reader.findRules.bind(reader),
62
- findRuleIds: reader.findRuleIds.bind(reader),
63
- fetchAllRuleIds: reader.fetchAllRuleIds.bind(reader),
64
- // writer
65
- insertRules: writer.insertRules.bind(writer),
66
- deleteRules: writer.deleteRules.bind(writer),
67
- deleteAllRules: writer.deleteAllRules.bind(writer),
68
- });