@tuturuuu/utils 0.12.1 → 0.15.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,74 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.15.0](https://github.com/tutur3u/platform/compare/utils-v0.14.0...utils-v0.15.0) (2026-07-11)
4
+
5
+
6
+ ### Features
7
+
8
+ * **contacts:** migrate users attendance; add satellite-safe workspace-user link helper ([8f53453](https://github.com/tutur3u/platform/commit/8f534533793af8d25e7c2eca6e0147e459d78bbe))
9
+ * **contacts:** migrate users topic-announcements module ([01e19ab](https://github.com/tutur3u/platform/commit/01e19ab60461ccd94a0374bed74ce6e4d5d38a3b))
10
+ * **contacts:** scaffold contacts.tuturuuu.com satellite shell + monorepo registration ([7e335fc](https://github.com/tutur3u/platform/commit/7e335fc036c4a45ed189095ecd10a43ee002294b))
11
+ * **infrastructure:** add abuse protection controls ([0e09692](https://github.com/tutur3u/platform/commit/0e09692a553cb10fbbb521e921951c2a6863281d))
12
+ * **infrastructure:** add rate-limit appeals ([f1e50e9](https://github.com/tutur3u/platform/commit/f1e50e9fd805dca6c80da8daab60ed24199f24ab))
13
+ * **pay:** scaffold apps/pay satellite (pay.tuturuuu.com, port 7826) ([3d6e45c](https://github.com/tutur3u/platform/commit/3d6e45cca6a315fe67213e438144b73103bae2f5))
14
+ * **tools:** add apps/tools public tools app, remove apps/qr ([204aae8](https://github.com/tutur3u/platform/commit/204aae89a04b4163f2859561f8e832526642e271))
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * **auth:** share account preference cookies ([8c1848a](https://github.com/tutur3u/platform/commit/8c1848a941c1b3f91337104c975e4bc0d8f68fc9))
20
+ * **ci:** keep cache components enabled in production ([75e6e2b](https://github.com/tutur3u/platform/commit/75e6e2bbbf6fa432bc496ee95b75ade5f8847f9f))
21
+ * **cli:** preserve markdown tables in task descriptions ([9faf8ba](https://github.com/tutur3u/platform/commit/9faf8ba44b92565a9dfc2949c496654fc1fea9b3))
22
+ * **infrastructure:** default docker web to native builds ([7d89f00](https://github.com/tutur3u/platform/commit/7d89f004f75ab12eddc3cb92df5a1e6a7255fe0d))
23
+ * **infrastructure:** restore canonical auth host access ([9f6715f](https://github.com/tutur3u/platform/commit/9f6715fb1c5a89598556cba33b16938b76995fc6))
24
+ * restore apps launcher categories ([5c5b571](https://github.com/tutur3u/platform/commit/5c5b571ce920013bcfa5f327c4bb208ea9fbf23e))
25
+ * **satellite:** widen apps launcher grid ([90e3106](https://github.com/tutur3u/platform/commit/90e31066830f535a7fdd0f7bc249e5bc8b273cfc))
26
+ * **security:** limit auth recovery code attempts ([72d454c](https://github.com/tutur3u/platform/commit/72d454c507b0d4b7e69c7b38ad2099dbb83193b2))
27
+ * **tasks:** route native personal task moves normally ([5bbba3c](https://github.com/tutur3u/platform/commit/5bbba3c3f193e722cc87cdfe2eb2734f61442cd0))
28
+ * update launchable app catalog ([cb31207](https://github.com/tutur3u/platform/commit/cb312076aee227de9a8f99105d681911d14a63ac))
29
+ * **web:** fail open without redis ([74bfcc7](https://github.com/tutur3u/platform/commit/74bfcc7ea5f188a56f49802c6a48bde0b109e02d))
30
+
31
+
32
+ ### Performance Improvements
33
+
34
+ * **ci:** enable repository-wide remote caching ([6250f91](https://github.com/tutur3u/platform/commit/6250f91d745ef987a4fc86c797aedf41542f421b))
35
+
36
+ ## [0.14.0](https://github.com/tutur3u/platform/compare/utils-v0.13.0...utils-v0.14.0) (2026-07-11)
37
+
38
+
39
+ ### Features
40
+
41
+ * **contacts:** migrate users attendance; add satellite-safe workspace-user link helper ([8f53453](https://github.com/tutur3u/platform/commit/8f534533793af8d25e7c2eca6e0147e459d78bbe))
42
+ * **contacts:** migrate users topic-announcements module ([01e19ab](https://github.com/tutur3u/platform/commit/01e19ab60461ccd94a0374bed74ce6e4d5d38a3b))
43
+ * **contacts:** scaffold contacts.tuturuuu.com satellite shell + monorepo registration ([7e335fc](https://github.com/tutur3u/platform/commit/7e335fc036c4a45ed189095ecd10a43ee002294b))
44
+ * **pay:** scaffold apps/pay satellite (pay.tuturuuu.com, port 7826) ([3d6e45c](https://github.com/tutur3u/platform/commit/3d6e45cca6a315fe67213e438144b73103bae2f5))
45
+
46
+
47
+ ### Bug Fixes
48
+
49
+ * **auth:** share account preference cookies ([8c1848a](https://github.com/tutur3u/platform/commit/8c1848a941c1b3f91337104c975e4bc0d8f68fc9))
50
+ * restore apps launcher categories ([5c5b571](https://github.com/tutur3u/platform/commit/5c5b571ce920013bcfa5f327c4bb208ea9fbf23e))
51
+ * **satellite:** widen apps launcher grid ([90e3106](https://github.com/tutur3u/platform/commit/90e31066830f535a7fdd0f7bc249e5bc8b273cfc))
52
+ * update launchable app catalog ([cb31207](https://github.com/tutur3u/platform/commit/cb312076aee227de9a8f99105d681911d14a63ac))
53
+
54
+
55
+ ### Performance Improvements
56
+
57
+ * **ci:** enable repository-wide remote caching ([6250f91](https://github.com/tutur3u/platform/commit/6250f91d745ef987a4fc86c797aedf41542f421b))
58
+
59
+ ## [0.13.0](https://github.com/tutur3u/platform/compare/utils-v0.12.1...utils-v0.13.0) (2026-07-05)
60
+
61
+
62
+ ### Features
63
+
64
+ * **tools:** add apps/tools public tools app, remove apps/qr ([204aae8](https://github.com/tutur3u/platform/commit/204aae89a04b4163f2859561f8e832526642e271))
65
+
66
+
67
+ ### Bug Fixes
68
+
69
+ * **tasks:** route native personal task moves normally ([5bbba3c](https://github.com/tutur3u/platform/commit/5bbba3c3f193e722cc87cdfe2eb2734f61442cd0))
70
+ * **web:** fail open without redis ([74bfcc7](https://github.com/tutur3u/platform/commit/74bfcc7ea5f188a56f49802c6a48bde0b109e02d))
71
+
3
72
  ## [0.12.1](https://github.com/tutur3u/platform/compare/utils-v0.12.0...utils-v0.12.1) (2026-07-03)
4
73
 
5
74
 
package/biome.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "root": false,
3
- "$schema": "https://biomejs.dev/schemas/2.5.1/schema.json",
3
+ "$schema": "https://biomejs.dev/schemas/2.5.3/schema.json",
4
4
  "extends": "//"
5
5
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tuturuuu/utils",
3
3
  "license": "MIT",
4
- "version": "0.12.1",
4
+ "version": "0.15.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/tutur3u/platform",
@@ -20,11 +20,11 @@
20
20
  "@noble/ciphers": "^2.2.0",
21
21
  "@noble/hashes": "^2.2.0",
22
22
  "@tanstack/react-query": "^5.101.2",
23
- "@tiptap/core": "3.27.1",
24
- "@tiptap/react": "3.27.1",
23
+ "@tiptap/core": "3.27.3",
24
+ "@tiptap/react": "3.27.3",
25
25
  "@tuturuuu/google": "0.0.3",
26
26
  "@tuturuuu/icons": "0.0.6",
27
- "@tuturuuu/internal-api": "0.14.0",
27
+ "@tuturuuu/internal-api": "0.17.0",
28
28
  "@tuturuuu/supabase": "0.4.0",
29
29
  "@upstash/ratelimit": "^2.0.8",
30
30
  "@upstash/redis": "^1.38.0",
@@ -35,7 +35,7 @@
35
35
  "effect": "^3.21.4",
36
36
  "moment": "^2.30.1",
37
37
  "next": "^16.3.0-preview.5",
38
- "prosemirror-model": "^1.25.9",
38
+ "prosemirror-model": "^1.25.11",
39
39
  "react": "^19.2.7",
40
40
  "react-dom": "^19.2.7",
41
41
  "tailwind-merge": "^3.6.0",
@@ -45,15 +45,15 @@
45
45
  "zod": "^4.4.3"
46
46
  },
47
47
  "devDependencies": {
48
- "@tuturuuu/types": "0.14.0",
48
+ "@tuturuuu/types": "0.16.0",
49
49
  "@tuturuuu/typescript-config": "0.1.1",
50
50
  "@types/diff": "^8.0.0",
51
- "@types/node": "^26.0.1",
51
+ "@types/node": "^26.1.1",
52
52
  "@types/react": "^19.2.17",
53
53
  "@vitejs/plugin-react": "^6.0.3",
54
- "typescript": "7.0.1-rc",
55
- "vite": "^8.1.2",
56
- "vitest": "^4.1.9"
54
+ "typescript": "7.0.2",
55
+ "vite": "^8.1.4",
56
+ "vitest": "^4.1.10"
57
57
  },
58
58
  "exports": {
59
59
  "./timezones": "./src/timezones.json",
@@ -278,6 +278,36 @@ describe('guardApiProxyRequest', () => {
278
278
  expect(mocks.validateEmoji).toHaveBeenCalled();
279
279
  });
280
280
 
281
+ it('skips proxy route buckets when Redis is not configured', async () => {
282
+ vi.stubEnv('NODE_ENV', 'production');
283
+ mocks.extractIp.mockReturnValue('1.2.3.4');
284
+ mocks.isBlocked.mockResolvedValue(null);
285
+ mocks.limit.mockResolvedValue({
286
+ success: false,
287
+ limit: 1,
288
+ remaining: 0,
289
+ reset: Date.now() + 60_000,
290
+ });
291
+ mocks.validateEmoji.mockResolvedValue(null);
292
+
293
+ const { guardApiProxyRequest, clearApiProxyGuardLimiterCache } =
294
+ await import('../api-proxy-guard.js');
295
+ clearApiProxyGuardLimiterCache();
296
+
297
+ const response = await guardApiProxyRequest(
298
+ makeRequest('/api/test', 'GET'),
299
+ {
300
+ prefixBase: 'proxy:test:api',
301
+ routePolicies: singleReadRoutePolicies(),
302
+ }
303
+ );
304
+
305
+ expect(response).toBeNull();
306
+ expect(mocks.redis).not.toHaveBeenCalled();
307
+ expect(mocks.limit).not.toHaveBeenCalled();
308
+ expect(mocks.validateEmoji).toHaveBeenCalled();
309
+ });
310
+
281
311
  it('allows a blocked IP to submit the rate-limit appeal endpoint only', async () => {
282
312
  vi.stubEnv('NODE_ENV', 'production');
283
313
  vi.stubEnv('UPSTASH_REDIS_REST_URL', 'https://redis.test');
@@ -1186,7 +1216,7 @@ describe('guardApiProxyRequest', () => {
1186
1216
  expect(mocks.validateEmoji).not.toHaveBeenCalled();
1187
1217
  });
1188
1218
 
1189
- it('uses local rate limits when the Redis rate limiter is unreachable', async () => {
1219
+ it('skips proxy route buckets when the Redis rate limiter is unreachable', async () => {
1190
1220
  vi.stubEnv('NODE_ENV', 'production');
1191
1221
  vi.stubEnv(
1192
1222
  'UPSTASH_REDIS_REST_URL',
@@ -1196,7 +1226,7 @@ describe('guardApiProxyRequest', () => {
1196
1226
  mocks.redis.mockReturnValue({});
1197
1227
  mocks.extractIp.mockReturnValue('1.2.3.4');
1198
1228
  mocks.isBlocked.mockResolvedValue(null);
1199
- mocks.limit.mockRejectedValueOnce(new TypeError('fetch failed'));
1229
+ mocks.limit.mockRejectedValue(new TypeError('fetch failed'));
1200
1230
  mocks.validateEmoji.mockResolvedValue(null);
1201
1231
 
1202
1232
  const { guardApiProxyRequest, clearApiProxyGuardLimiterCache } =
@@ -1216,22 +1246,17 @@ describe('guardApiProxyRequest', () => {
1216
1246
  expect(response).toBeNull();
1217
1247
  expect(mocks.limit).toHaveBeenCalledTimes(1);
1218
1248
 
1219
- const blockedResponse = await guardApiProxyRequest(
1249
+ const allowedResponse = await guardApiProxyRequest(
1220
1250
  makeRequest('/api/test', 'GET'),
1221
1251
  options
1222
1252
  );
1223
1253
 
1224
- expect(blockedResponse?.status).toBe(429);
1225
- expect(blockedResponse?.headers.get('X-Proxy-Block-Reason')).toBe(
1226
- 'route-rate-limit'
1227
- );
1228
- expect(blockedResponse?.headers.get('X-RateLimit-Policy')).toBe(
1229
- 'redis-fallback-test'
1230
- );
1231
- expect(mocks.limit).toHaveBeenCalledTimes(1);
1254
+ expect(allowedResponse).toBeNull();
1255
+ expect(mocks.limit).toHaveBeenCalledTimes(2);
1256
+ expect(mocks.validateEmoji).toHaveBeenCalledTimes(2);
1232
1257
  });
1233
1258
 
1234
- it('uses local rate limits when Redis client initialization fails', async () => {
1259
+ it('skips proxy route buckets when Redis client initialization fails', async () => {
1235
1260
  vi.stubEnv('NODE_ENV', 'production');
1236
1261
  vi.stubEnv('UPSTASH_REDIS_REST_URL', 'https://redis.test');
1237
1262
  vi.stubEnv('UPSTASH_REDIS_REST_TOKEN', 'token');
@@ -1258,16 +1283,14 @@ describe('guardApiProxyRequest', () => {
1258
1283
 
1259
1284
  expect(response).toBeNull();
1260
1285
 
1261
- const blockedResponse = await guardApiProxyRequest(
1286
+ const allowedResponse = await guardApiProxyRequest(
1262
1287
  makeRequest('/api/test', 'GET'),
1263
1288
  options
1264
1289
  );
1265
1290
 
1266
- expect(blockedResponse?.status).toBe(429);
1267
- expect(blockedResponse?.headers.get('X-Proxy-Block-Reason')).toBe(
1268
- 'route-rate-limit'
1269
- );
1291
+ expect(allowedResponse).toBeNull();
1270
1292
  expect(mocks.limit).not.toHaveBeenCalled();
1293
+ expect(mocks.validateEmoji).toHaveBeenCalledTimes(2);
1271
1294
  });
1272
1295
 
1273
1296
  it('rate limits anonymous users/me reads', async () => {
@@ -231,8 +231,10 @@ describe('getLocalInternalAppUrl', () => {
231
231
  ['cms', 'https://cms.tuturuuu.localhost'],
232
232
  ['calendar', 'https://calendar.tuturuuu.localhost'],
233
233
  ['chat', 'https://chat.tuturuuu.localhost'],
234
+ ['docs', 'https://docs.tuturuuu.localhost'],
234
235
  ['drive', 'https://drive.tuturuuu.localhost'],
235
236
  ['qr', 'https://qr.tuturuuu.localhost'],
237
+ ['tools', 'https://tools.tuturuuu.localhost'],
236
238
  ['nova', 'https://nova.tuturuuu.localhost'],
237
239
  ['rewise', 'https://rewise.tuturuuu.localhost'],
238
240
  ['tasks', 'https://tasks.tuturuuu.localhost'],
@@ -289,6 +291,8 @@ describe('Portless app origin registry', () => {
289
291
  'https://calendar.tuturuuu.localhost',
290
292
  'https://chat.tuturuuu.localhost',
291
293
  'https://cms.tuturuuu.localhost',
294
+ 'https://contacts.tuturuuu.localhost',
295
+ 'https://docs.tuturuuu.localhost',
292
296
  'https://drive.tuturuuu.localhost',
293
297
  'https://external.tuturuuu.localhost',
294
298
  'https://finance.tuturuuu.localhost',
@@ -301,10 +305,12 @@ describe('Portless app origin registry', () => {
301
305
  'https://meet.tuturuuu.localhost',
302
306
  'https://mind.tuturuuu.localhost',
303
307
  'https://nova.tuturuuu.localhost',
308
+ 'https://pay.tuturuuu.localhost',
304
309
  'https://tuturuuu.localhost',
305
310
  'https://playground.tuturuuu.localhost',
306
311
  'https://qr.tuturuuu.localhost',
307
312
  'https://rewise.tuturuuu.localhost',
313
+ 'https://tools.tuturuuu.localhost',
308
314
  'https://shortener.tuturuuu.localhost',
309
315
  'https://storefront.tuturuuu.localhost',
310
316
  'https://tanstack.tuturuuu.localhost',
@@ -58,5 +58,26 @@ describe('transformTaskRecord', () => {
58
58
  expect(transformed.projects).toEqual([
59
59
  { id: 'project-1', name: 'Platform', status: 'active' },
60
60
  ]);
61
+ expect(transformed).toMatchObject({
62
+ is_personal_external: false,
63
+ is_personal_external_default: false,
64
+ });
65
+ });
66
+
67
+ it('preserves explicit personal external flags from API responses', () => {
68
+ const transformed = transformTaskRecord({
69
+ id: 'task-1',
70
+ name: 'Task',
71
+ list_id: 'list-1',
72
+ display_number: 1,
73
+ created_at: '2026-01-01T00:00:00.000Z',
74
+ is_personal_external: true,
75
+ is_personal_external_default: true,
76
+ });
77
+
78
+ expect(transformed).toMatchObject({
79
+ is_personal_external: true,
80
+ is_personal_external_default: true,
81
+ });
61
82
  });
62
83
  });
@@ -0,0 +1,93 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ describe('abuse protection Redis fail-open behavior', () => {
4
+ afterEach(() => {
5
+ vi.doUnmock('../../upstash-rest');
6
+ vi.doUnmock('@tuturuuu/supabase/next/server');
7
+ vi.resetModules();
8
+ vi.restoreAllMocks();
9
+ });
10
+
11
+ it('allows checks and skips persistent IP block lookups when Redis is not configured', async () => {
12
+ const from = vi.fn(() => ({
13
+ insert: vi.fn(async () => ({ error: null })),
14
+ }));
15
+ const rpc = vi.fn();
16
+ const createAdminClient = vi.fn(async () => ({ from, rpc }));
17
+ const getRedis = vi.fn();
18
+
19
+ vi.doMock('../../upstash-rest', () => ({
20
+ getUpstashRestRedisClient: getRedis,
21
+ hasUpstashRestEnv: () => false,
22
+ }));
23
+ vi.doMock('@tuturuuu/supabase/next/server', () => ({
24
+ createAdminClient,
25
+ }));
26
+
27
+ const { checkPasswordLoginLimit, isIPBlocked, recordPasswordLoginFailure } =
28
+ await import('../index.js');
29
+
30
+ for (let attempt = 0; attempt < 20; attempt += 1) {
31
+ await recordPasswordLoginFailure('203.0.113.80', 'user@example.com');
32
+ }
33
+
34
+ await expect(isIPBlocked('203.0.113.80')).resolves.toBeNull();
35
+ await expect(
36
+ checkPasswordLoginLimit('203.0.113.80', 'user@example.com')
37
+ ).resolves.toMatchObject({
38
+ allowed: true,
39
+ });
40
+ expect(getRedis).not.toHaveBeenCalled();
41
+ expect(rpc).not.toHaveBeenCalledWith('get_active_ip_block', {
42
+ p_ip_address: '203.0.113.80',
43
+ });
44
+ expect(from).not.toHaveBeenCalledWith('blocked_ips');
45
+ });
46
+
47
+ it('allows checks and skips counter writes after Redis commands fail', async () => {
48
+ const from = vi.fn(() => ({
49
+ insert: vi.fn(async () => ({ error: null })),
50
+ }));
51
+ const rpc = vi.fn();
52
+ const createAdminClient = vi.fn(async () => ({ from, rpc }));
53
+ const redis = {
54
+ del: vi.fn(),
55
+ expire: vi.fn(),
56
+ get: vi.fn(async () => {
57
+ throw new Error('redis unavailable');
58
+ }),
59
+ incr: vi.fn(async () => {
60
+ throw new Error('redis unavailable');
61
+ }),
62
+ set: vi.fn(),
63
+ ttl: vi.fn(),
64
+ };
65
+
66
+ vi.doMock('../../upstash-rest', () => ({
67
+ getUpstashRestRedisClient: async () => redis,
68
+ hasUpstashRestEnv: () => true,
69
+ }));
70
+ vi.doMock('@tuturuuu/supabase/next/server', () => ({
71
+ createAdminClient,
72
+ }));
73
+
74
+ const { checkPasswordLoginLimit, isIPBlocked, recordPasswordLoginFailure } =
75
+ await import('../index.js');
76
+
77
+ await expect(isIPBlocked('203.0.113.81')).resolves.toBeNull();
78
+ await expect(
79
+ checkPasswordLoginLimit('203.0.113.81', 'user@example.com')
80
+ ).resolves.toMatchObject({
81
+ allowed: true,
82
+ });
83
+ await expect(
84
+ recordPasswordLoginFailure('203.0.113.81', 'user@example.com')
85
+ ).resolves.toBeUndefined();
86
+ expect(redis.expire).not.toHaveBeenCalled();
87
+ expect(redis.set).not.toHaveBeenCalled();
88
+ expect(rpc).not.toHaveBeenCalledWith('get_active_ip_block', {
89
+ p_ip_address: '203.0.113.81',
90
+ });
91
+ expect(from).not.toHaveBeenCalledWith('blocked_ips');
92
+ });
93
+ });
@@ -2,7 +2,76 @@
2
2
  * Unit tests for OTP Abuse Protection System
3
3
  */
4
4
 
5
- import { afterEach, describe, expect, it, vi } from 'vitest';
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
+
7
+ const mocks = vi.hoisted(() => {
8
+ const store = new Map<string, { expiresAt: number | null; value: unknown }>();
9
+
10
+ function getEntry(key: string) {
11
+ const entry = store.get(key);
12
+ if (!entry) return null;
13
+
14
+ if (entry.expiresAt != null && entry.expiresAt <= Date.now()) {
15
+ store.delete(key);
16
+ return null;
17
+ }
18
+
19
+ return entry;
20
+ }
21
+
22
+ return {
23
+ getRedis: vi.fn(),
24
+ redis: {
25
+ del: vi.fn(async (...keys: string[]) => {
26
+ let deleted = 0;
27
+ for (const key of keys) {
28
+ if (store.delete(key)) deleted += 1;
29
+ }
30
+ return deleted;
31
+ }),
32
+ expire: vi.fn(async (key: string, seconds: number) => {
33
+ const entry = getEntry(key);
34
+ if (!entry) return 0;
35
+ entry.expiresAt = Date.now() + seconds * 1000;
36
+ return 1;
37
+ }),
38
+ get: vi.fn(async <T = unknown>(key: string): Promise<T | null> => {
39
+ return (getEntry(key)?.value as T | undefined) ?? null;
40
+ }),
41
+ incr: vi.fn(async (key: string) => {
42
+ const entry = getEntry(key);
43
+ const nextValue =
44
+ typeof entry?.value === 'number' ? entry.value + 1 : 1;
45
+ store.set(key, {
46
+ expiresAt: entry?.expiresAt ?? null,
47
+ value: nextValue,
48
+ });
49
+ return nextValue;
50
+ }),
51
+ set: vi.fn(
52
+ async (key: string, value: unknown, options?: { ex?: number }) => {
53
+ store.set(key, {
54
+ expiresAt: options?.ex ? Date.now() + options.ex * 1000 : null,
55
+ value,
56
+ });
57
+ return 'OK';
58
+ }
59
+ ),
60
+ ttl: vi.fn(async (key: string) => {
61
+ const entry = getEntry(key);
62
+ if (!entry) return -2;
63
+ if (entry.expiresAt == null) return -1;
64
+ return Math.ceil((entry.expiresAt - Date.now()) / 1000);
65
+ }),
66
+ },
67
+ store,
68
+ };
69
+ });
70
+
71
+ vi.mock('../../upstash-rest', () => ({
72
+ getUpstashRestRedisClient: () => mocks.getRedis(),
73
+ hasUpstashRestEnv: () => true,
74
+ }));
6
75
 
7
76
  import {
8
77
  ABUSE_THRESHOLDS,
@@ -29,6 +98,12 @@ import {
29
98
  } from '../index';
30
99
 
31
100
  describe('abuse-protection', () => {
101
+ beforeEach(() => {
102
+ vi.clearAllMocks();
103
+ mocks.store.clear();
104
+ mocks.getRedis.mockResolvedValue(mocks.redis);
105
+ });
106
+
32
107
  afterEach(() => {
33
108
  vi.useRealTimers();
34
109
  vi.unstubAllEnvs();