@prosopo/user-access-policy 3.12.8 → 3.12.10

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 (84) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +45 -41
  2. package/.turbo/turbo-build$colon$tsc.log +14 -14
  3. package/.turbo/turbo-build.log +46 -42
  4. package/CHANGELOG.md +24 -0
  5. package/dist/.export.js +7 -21
  6. package/dist/_virtual/_rolldown/runtime.js +3 -0
  7. package/dist/api/.export.js +2 -9
  8. package/dist/api/accessRulesApiClient.js +1 -1
  9. package/dist/api/delete/.export.js +2 -1
  10. package/dist/api/delete/deleteAllRules.js +22 -24
  11. package/dist/api/delete/deleteRuleGroups.js +39 -51
  12. package/dist/api/delete/deleteRules.js +37 -41
  13. package/dist/api/read/.export.js +2 -1
  14. package/dist/api/read/fetchRules.js +34 -42
  15. package/dist/api/read/findRuleIds.js +35 -48
  16. package/dist/api/read/getMissingIds.js +36 -40
  17. package/dist/api/ruleApiRoutes.js +62 -120
  18. package/dist/api/rulesApiClient.js +51 -87
  19. package/dist/api/write/.export.js +2 -1
  20. package/dist/api/write/insertRules.js +84 -102
  21. package/dist/api/write/rehashRules.js +48 -56
  22. package/dist/cjs/.export.cjs +21 -23
  23. package/dist/cjs/_virtual/_rolldown/runtime.cjs +23 -0
  24. package/dist/cjs/api/.export.cjs +6 -10
  25. package/dist/cjs/api/accessRulesApiClient.cjs +0 -0
  26. package/dist/cjs/api/delete/.export.cjs +0 -1
  27. package/dist/cjs/api/delete/deleteAllRules.cjs +22 -24
  28. package/dist/cjs/api/delete/deleteRuleGroups.cjs +39 -51
  29. package/dist/cjs/api/delete/deleteRules.cjs +41 -42
  30. package/dist/cjs/api/read/.export.cjs +0 -1
  31. package/dist/cjs/api/read/fetchRules.cjs +34 -41
  32. package/dist/cjs/api/read/findRuleIds.cjs +37 -48
  33. package/dist/cjs/api/read/getMissingIds.cjs +36 -39
  34. package/dist/cjs/api/ruleApiRoutes.cjs +69 -125
  35. package/dist/cjs/api/rulesApiClient.cjs +54 -90
  36. package/dist/cjs/api/write/.export.cjs +0 -1
  37. package/dist/cjs/api/write/insertRules.cjs +85 -103
  38. package/dist/cjs/api/write/rehashRules.cjs +48 -56
  39. package/dist/cjs/classifyOs.cjs +46 -26
  40. package/dist/cjs/mongoose/.export.cjs +2 -3
  41. package/dist/cjs/mongoose/mongooseRuleSchema.cjs +97 -36
  42. package/dist/cjs/redis/.export.cjs +4 -5
  43. package/dist/cjs/redis/reader/redisAggregate.cjs +59 -84
  44. package/dist/cjs/redis/reader/redisRulesQuery.cjs +74 -116
  45. package/dist/cjs/redis/reader/redisRulesReader.cjs +322 -503
  46. package/dist/cjs/redis/reader/redisRulesSplitQuery.cjs +94 -72
  47. package/dist/cjs/redis/redisClient.cjs +53 -58
  48. package/dist/cjs/redis/redisRuleIndex.cjs +93 -49
  49. package/dist/cjs/redis/redisRulesStorage.cjs +21 -30
  50. package/dist/cjs/redis/redisRulesWriter.cjs +86 -112
  51. package/dist/cjs/rule.cjs +8 -7
  52. package/dist/cjs/ruleInput/.export.cjs +7 -8
  53. package/dist/cjs/ruleInput/policyInput.cjs +35 -58
  54. package/dist/cjs/ruleInput/ruleInput.cjs +37 -42
  55. package/dist/cjs/ruleInput/userScopeInput.cjs +47 -55
  56. package/dist/cjs/ruleRecord.cjs +15 -22
  57. package/dist/cjs/rulesStorage.cjs +7 -6
  58. package/dist/cjs/transformRule.cjs +36 -70
  59. package/dist/classifyOs.js +48 -29
  60. package/dist/mongoose/.export.js +2 -3
  61. package/dist/mongoose/mongooseRuleSchema.js +99 -38
  62. package/dist/redis/.export.js +3 -5
  63. package/dist/redis/reader/redisAggregate.js +60 -86
  64. package/dist/redis/reader/redisRulesQuery.js +75 -118
  65. package/dist/redis/reader/redisRulesReader.js +312 -487
  66. package/dist/redis/reader/redisRulesSplitQuery.js +96 -75
  67. package/dist/redis/redisClient.js +54 -62
  68. package/dist/redis/redisRuleIndex.js +93 -58
  69. package/dist/redis/redisRulesStorage.js +21 -30
  70. package/dist/redis/redisRulesWriter.js +82 -113
  71. package/dist/rule.js +10 -10
  72. package/dist/ruleInput/.export.js +3 -7
  73. package/dist/ruleInput/policyInput.js +35 -61
  74. package/dist/ruleInput/ruleInput.js +36 -44
  75. package/dist/ruleInput/userScopeInput.js +44 -55
  76. package/dist/ruleRecord.js +17 -27
  77. package/dist/rulesStorage.js +9 -8
  78. package/dist/tests/redis/redisRulesStorage.integration.test.js +1 -3
  79. package/dist/tests/redis/redisRulesStorage.integration.test.js.map +1 -1
  80. package/dist/transformRule.js +34 -73
  81. package/entries.ts +25 -6
  82. package/package.json +13 -12
  83. package/src/tests/redis/redisRulesStorage.integration.test.ts +191 -195
  84. package/tsconfig.tsbuildinfo +1 -1
@@ -113,223 +113,219 @@ describe("redisAccessRulesStorage", () => {
113
113
  redisClient = await result.getClient();
114
114
  });
115
115
 
116
- describe(
117
- "writer",
118
- () => {
119
- let accessRulesWriter: AccessRulesWriter;
116
+ describe("writer", () => {
117
+ let accessRulesWriter: AccessRulesWriter;
120
118
 
121
- beforeAll(() => {
122
- accessRulesWriter = new RedisRulesWriter(redisClient, mockLogger);
123
- });
119
+ beforeAll(() => {
120
+ accessRulesWriter = new RedisRulesWriter(redisClient, mockLogger);
121
+ });
124
122
 
125
- test("inserts rule", async () => {
126
- const testIndexName = indexName;
127
- // given
128
- const accessRule: AccessRule = {
129
- type: AccessPolicyType.Block,
130
- clientId: "clientId",
131
- };
132
- const accessRuleKey = getAccessRuleRedisKey(accessRule);
123
+ test("inserts rule", async () => {
124
+ const testIndexName = indexName;
125
+ // given
126
+ const accessRule: AccessRule = {
127
+ type: AccessPolicyType.Block,
128
+ clientId: "clientId",
129
+ };
130
+ const accessRuleKey = getAccessRuleRedisKey(accessRule);
133
131
 
134
- // when
135
- await accessRulesWriter.insertRules([
136
- {
137
- rule: accessRule,
138
- },
139
- ]);
132
+ // when
133
+ await accessRulesWriter.insertRules([
134
+ {
135
+ rule: accessRule,
136
+ },
137
+ ]);
140
138
 
141
- // then
142
- const insertedAccessRule = await redisClient.hGetAll(accessRuleKey);
143
- const indexRecordsCount = await getIndexRecordsCount(testIndexName);
139
+ // then
140
+ const insertedAccessRule = await redisClient.hGetAll(accessRuleKey);
141
+ const indexRecordsCount = await getIndexRecordsCount(testIndexName);
144
142
 
145
- expect(insertedAccessRule).toEqual(accessRule);
146
- expect(indexRecordsCount).toEqual(1);
147
- });
143
+ expect(insertedAccessRule).toEqual(accessRule);
144
+ expect(indexRecordsCount).toEqual(1);
145
+ });
148
146
 
149
- test("inserts time limited rule", async () => {
150
- // given
151
- const accessRule: AccessRule = {
152
- type: AccessPolicyType.Block,
153
- clientId: "clientId",
154
- };
155
- const accessRuleKey = getAccessRuleRedisKey(accessRule);
156
- // 1 hour from now.
157
- const expireIn = 60 * 60; // seconds
158
- const expirationTimestamp = new Date(
159
- Date.now() + expireIn * 1000,
160
- ).getTime();
161
- const expirationTimestampInSeconds = Math.floor(
162
- expirationTimestamp / 1000,
163
- );
147
+ test("inserts time limited rule", async () => {
148
+ // given
149
+ const accessRule: AccessRule = {
150
+ type: AccessPolicyType.Block,
151
+ clientId: "clientId",
152
+ };
153
+ const accessRuleKey = getAccessRuleRedisKey(accessRule);
154
+ // 1 hour from now.
155
+ const expireIn = 60 * 60; // seconds
156
+ const expirationTimestamp = new Date(
157
+ Date.now() + expireIn * 1000,
158
+ ).getTime();
159
+ const expirationTimestampInSeconds = Math.floor(
160
+ expirationTimestamp / 1000,
161
+ );
164
162
 
165
- // when
166
- await accessRulesWriter.insertRules([
167
- {
168
- rule: accessRule,
169
- expiresUnixTimestamp: expirationTimestampInSeconds,
170
- },
171
- ]);
172
- const ruleKey = getAccessRuleRedisKey(accessRule);
173
- // then
174
- const insertedAccessRule = await redisClient.hGetAll(accessRuleKey);
175
- const insertedExpirationResult = await redisClient.expireAt(
176
- ruleKey,
177
- expirationTimestampInSeconds,
178
- );
179
- const indexRecordsCount = await getIndexRecordsCount(indexName);
163
+ // when
164
+ await accessRulesWriter.insertRules([
165
+ {
166
+ rule: accessRule,
167
+ expiresUnixTimestamp: expirationTimestampInSeconds,
168
+ },
169
+ ]);
170
+ const ruleKey = getAccessRuleRedisKey(accessRule);
171
+ // then
172
+ const insertedAccessRule = await redisClient.hGetAll(accessRuleKey);
173
+ const insertedExpirationResult = await redisClient.expireAt(
174
+ ruleKey,
175
+ expirationTimestampInSeconds,
176
+ );
177
+ const indexRecordsCount = await getIndexRecordsCount(indexName);
180
178
 
181
- const recordExpirySeconds = await redisClient.ttl(ruleKey);
179
+ const recordExpirySeconds = await redisClient.ttl(ruleKey);
182
180
 
183
- expect(insertedAccessRule).toEqual(accessRule);
184
- expect(insertedExpirationResult).toBe(1);
185
- expect(recordExpirySeconds).toBeLessThanOrEqual(
186
- expirationTimestampInSeconds,
187
- );
181
+ expect(insertedAccessRule).toEqual(accessRule);
182
+ expect(insertedExpirationResult).toBe(1);
183
+ expect(recordExpirySeconds).toBeLessThanOrEqual(
184
+ expirationTimestampInSeconds,
185
+ );
188
186
 
189
- expect(indexRecordsCount).toBe(1);
190
- });
187
+ expect(indexRecordsCount).toBe(1);
188
+ });
191
189
 
192
- // The existing "inserts time limited rule" test above only
193
- // checks that the TTL is *set* on the Redis key. Neither
194
- // side actually verifies the rule stops matching once its
195
- // TTL fires — which is the property operators care about
196
- // (temporary bans should self-clear). Guard against a
197
- // regression that persists rules past their expiry (e.g. a
198
- // future writer that swaps `expireAt` for `persist` by
199
- // accident, or an index rebuild that resurrects expired
200
- // records).
201
- test("expired rule is no longer returned by the reader after its TTL fires", async () => {
202
- const accessRule: AccessRule = {
203
- type: AccessPolicyType.Block,
204
- clientId: `expiring-${getUniqueString()}`,
205
- };
206
- const ruleKey = getAccessRuleRedisKey(accessRule);
207
- // 2 s expiry — long enough that the "before expiry" check
208
- // races the write cleanly, short enough that the test
209
- // completes in a reasonable timeout. Redis TTL granularity
210
- // is 1 s so anything shorter is under-resolution.
211
- const expirationTimestampInSeconds = Math.floor(Date.now() / 1000) + 2;
212
-
213
- await accessRulesWriter.insertRules([
214
- {
215
- rule: accessRule,
216
- expiresUnixTimestamp: expirationTimestampInSeconds,
217
- },
218
- ]);
219
-
220
- // Sanity: the rule exists immediately after write.
221
- const initialRecord = await redisClient.hGetAll(ruleKey);
222
- expect(initialRecord).toEqual(accessRule);
223
-
224
- // Wait past the TTL. Add ~1s slack for Redis's per-key
225
- // expiry sweep — the docs guarantee expired keys stop
226
- // answering reads but the actual key deletion is lazy.
227
- await new Promise((resolve) => setTimeout(resolve, 3500));
228
-
229
- // After expiry: hGetAll returns {} (Redis treats an
230
- // expired key as non-existent for reads).
231
- const expiredRecord = await redisClient.hGetAll(ruleKey);
232
- expect(expiredRecord).toEqual({});
233
-
234
- // And the RediSearch index no longer counts it.
235
- const indexRecordsCount = await getIndexRecordsCount(indexName);
236
- expect(indexRecordsCount).toBe(0);
237
- }, 10_000);
238
-
239
- test("deletes rules", async () => {
240
- // given
241
- const johnAccessRule: AccessRule = {
242
- type: AccessPolicyType.Block,
243
- clientId: getUniqueString(),
244
- };
245
- const johnAccessRuleKey = getAccessRuleRedisKey(johnAccessRule);
190
+ // The existing "inserts time limited rule" test above only
191
+ // checks that the TTL is *set* on the Redis key. Neither
192
+ // side actually verifies the rule stops matching once its
193
+ // TTL fires — which is the property operators care about
194
+ // (temporary bans should self-clear). Guard against a
195
+ // regression that persists rules past their expiry (e.g. a
196
+ // future writer that swaps `expireAt` for `persist` by
197
+ // accident, or an index rebuild that resurrects expired
198
+ // records).
199
+ test("expired rule is no longer returned by the reader after its TTL fires", async () => {
200
+ const accessRule: AccessRule = {
201
+ type: AccessPolicyType.Block,
202
+ clientId: `expiring-${getUniqueString()}`,
203
+ };
204
+ const ruleKey = getAccessRuleRedisKey(accessRule);
205
+ // 2 s expiry — long enough that the "before expiry" check
206
+ // races the write cleanly, short enough that the test
207
+ // completes in a reasonable timeout. Redis TTL granularity
208
+ // is 1 s so anything shorter is under-resolution.
209
+ const expirationTimestampInSeconds = Math.floor(Date.now() / 1000) + 2;
246
210
 
247
- const doeAccessRule: AccessRule = {
248
- type: AccessPolicyType.Block,
249
- clientId: getUniqueString(),
250
- };
251
- const doeAccessRuleKey = getAccessRuleRedisKey(doeAccessRule);
211
+ await accessRulesWriter.insertRules([
212
+ {
213
+ rule: accessRule,
214
+ expiresUnixTimestamp: expirationTimestampInSeconds,
215
+ },
216
+ ]);
252
217
 
253
- await insertRules([johnAccessRule, doeAccessRule]);
218
+ // Sanity: the rule exists immediately after write.
219
+ const initialRecord = await redisClient.hGetAll(ruleKey);
220
+ expect(initialRecord).toEqual(accessRule);
254
221
 
255
- // when
256
- await accessRulesWriter.deleteRules([
257
- johnAccessRuleKey.slice(ACCESS_RULE_REDIS_KEY_PREFIX.length),
258
- ]);
222
+ // Wait past the TTL. Add ~1s slack for Redis's per-key
223
+ // expiry sweep — the docs guarantee expired keys stop
224
+ // answering reads but the actual key deletion is lazy.
225
+ await new Promise((resolve) => setTimeout(resolve, 3500));
259
226
 
260
- // then
261
- const presentAccessRule = await redisClient.hGetAll(doeAccessRuleKey);
262
- const indexRecordsCount = await getIndexRecordsCount(indexName);
227
+ // After expiry: hGetAll returns {} (Redis treats an
228
+ // expired key as non-existent for reads).
229
+ const expiredRecord = await redisClient.hGetAll(ruleKey);
230
+ expect(expiredRecord).toEqual({});
263
231
 
264
- expect(presentAccessRule).toEqual(doeAccessRule);
265
- expect(indexRecordsCount).toBe(1);
266
- });
232
+ // And the RediSearch index no longer counts it.
233
+ const indexRecordsCount = await getIndexRecordsCount(indexName);
234
+ expect(indexRecordsCount).toBe(0);
235
+ }, 10_000);
267
236
 
268
- test("deletes all rules", async () => {
269
- // given
270
- const johnAccessRule: AccessRule = {
271
- type: AccessPolicyType.Block,
272
- clientId: getUniqueString(),
273
- };
274
- const doeAccessRule: AccessRule = {
275
- type: AccessPolicyType.Block,
276
- clientId: getUniqueString(),
277
- };
237
+ test("deletes rules", async () => {
238
+ // given
239
+ const johnAccessRule: AccessRule = {
240
+ type: AccessPolicyType.Block,
241
+ clientId: getUniqueString(),
242
+ };
243
+ const johnAccessRuleKey = getAccessRuleRedisKey(johnAccessRule);
278
244
 
279
- await insertRules([johnAccessRule, doeAccessRule]);
245
+ const doeAccessRule: AccessRule = {
246
+ type: AccessPolicyType.Block,
247
+ clientId: getUniqueString(),
248
+ };
249
+ const doeAccessRuleKey = getAccessRuleRedisKey(doeAccessRule);
250
+
251
+ await insertRules([johnAccessRule, doeAccessRule]);
280
252
 
281
- // when
282
- await accessRulesWriter.deleteAllRules();
253
+ // when
254
+ await accessRulesWriter.deleteRules([
255
+ johnAccessRuleKey.slice(ACCESS_RULE_REDIS_KEY_PREFIX.length),
256
+ ]);
283
257
 
284
- // then
285
- const indexRecordsCount = await getIndexRecordsCount(indexName);
258
+ // then
259
+ const presentAccessRule = await redisClient.hGetAll(doeAccessRuleKey);
260
+ const indexRecordsCount = await getIndexRecordsCount(indexName);
286
261
 
287
- expect(indexRecordsCount).toBe(0);
288
- });
262
+ expect(presentAccessRule).toEqual(doeAccessRule);
263
+ expect(indexRecordsCount).toBe(1);
264
+ });
289
265
 
290
- test("deletes all rules when there are 1 million rules", async () => {
291
- // given
292
- const rulesCount = 1_000_000;
293
- const batchSize = 10_000;
294
- const numBatches = Math.ceil(rulesCount / batchSize);
295
-
296
- // Insert rules in batches to avoid memory exhaustion
297
- // Don't create 1M objects in memory at once!
298
- for (let i = 0; i < numBatches; i++) {
299
- const currentBatchSize = Math.min(
300
- batchSize,
301
- rulesCount - i * batchSize,
302
- );
303
- const batchRules: AccessRule[] = Array.from(
304
- { length: currentBatchSize },
305
- () => ({
306
- type: AccessPolicyType.Block,
307
- clientId: getUniqueString(),
308
- }),
309
- );
310
-
311
- await insertRules(batchRules);
312
- }
313
-
314
- // verify that there are 1 million rules in the database
315
- const beforeDeleteIndexRecordsCount =
316
- await getIndexRecordsCount(indexName);
317
- expect(beforeDeleteIndexRecordsCount).toBe(rulesCount);
318
-
319
- // when
320
- await accessRulesWriter.deleteAllRules();
321
-
322
- // then
323
- const afterDeleteIndexRecordsCount =
324
- await getIndexRecordsCount(indexName);
325
-
326
- expect(afterDeleteIndexRecordsCount).toBe(0);
327
- });
328
- },
329
- {
330
- timeout: 240_000,
331
- },
332
- );
266
+ test("deletes all rules", async () => {
267
+ // given
268
+ const johnAccessRule: AccessRule = {
269
+ type: AccessPolicyType.Block,
270
+ clientId: getUniqueString(),
271
+ };
272
+ const doeAccessRule: AccessRule = {
273
+ type: AccessPolicyType.Block,
274
+ clientId: getUniqueString(),
275
+ };
276
+
277
+ await insertRules([johnAccessRule, doeAccessRule]);
278
+
279
+ // when
280
+ await accessRulesWriter.deleteAllRules();
281
+
282
+ // then
283
+ const indexRecordsCount = await getIndexRecordsCount(indexName);
284
+
285
+ expect(indexRecordsCount).toBe(0);
286
+ });
287
+
288
+ test("deletes all rules when there are 1 million rules", async () => {
289
+ // given
290
+ const rulesCount = 1_000_000;
291
+ const batchSize = 10_000;
292
+ const numBatches = Math.ceil(rulesCount / batchSize);
293
+
294
+ // Insert rules in batches to avoid memory exhaustion
295
+ // Don't create 1M objects in memory at once!
296
+ for (let i = 0; i < numBatches; i++) {
297
+ const currentBatchSize = Math.min(
298
+ batchSize,
299
+ rulesCount - i * batchSize,
300
+ );
301
+ const batchRules: AccessRule[] = Array.from(
302
+ { length: currentBatchSize },
303
+ () => ({
304
+ type: AccessPolicyType.Block,
305
+ clientId: getUniqueString(),
306
+ }),
307
+ );
308
+
309
+ await insertRules(batchRules);
310
+ }
311
+
312
+ // verify that there are 1 million rules in the database
313
+ const beforeDeleteIndexRecordsCount =
314
+ await getIndexRecordsCount(indexName);
315
+ expect(beforeDeleteIndexRecordsCount).toBe(rulesCount);
316
+
317
+ // when
318
+ await accessRulesWriter.deleteAllRules();
319
+
320
+ // then
321
+ const afterDeleteIndexRecordsCount =
322
+ await getIndexRecordsCount(indexName);
323
+
324
+ expect(afterDeleteIndexRecordsCount).toBe(0);
325
+ });
326
+ // The trailing 240_000 is the suite timeout. vitest 4 dropped the
327
+ // options-object overload for describe, so it has to be a plain number.
328
+ }, 240_000);
333
329
 
334
330
  describe("reader", () => {
335
331
  let accessRulesReader: AccessRulesReader;