@tuturuuu/utils 0.28.0 → 0.29.1

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 (36) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/biome.json +1 -1
  3. package/package.json +17 -16
  4. package/src/__tests__/api-proxy-guard.test.ts +20 -27
  5. package/src/__tests__/app-url.test.ts +1 -0
  6. package/src/__tests__/workspace-principal-resolution.test.ts +34 -0
  7. package/src/abuse-protection/__tests__/reputation.test.ts +107 -0
  8. package/src/abuse-protection/reputation.ts +42 -3
  9. package/src/api-proxy-guard.ts +0 -15
  10. package/src/commercial-comparison-0.ts +294 -0
  11. package/src/commercial-comparison-1.ts +283 -0
  12. package/src/commercial-comparison-2.ts +262 -0
  13. package/src/commercial-comparison-types.ts +20 -0
  14. package/src/commercial-comparison.test.ts +32 -0
  15. package/src/commercial-comparison.ts +96 -0
  16. package/src/coordination-protocol.ts +61 -0
  17. package/src/coordination.test.ts +84 -0
  18. package/src/coordination.ts +57 -0
  19. package/src/feature-tier-policy.ts +26 -0
  20. package/src/internal-domains.ts +3 -0
  21. package/src/launchable-apps.ts +13 -0
  22. package/src/meet-creation-policy.test.ts +11 -0
  23. package/src/meet-creation-policy.ts +9 -3
  24. package/src/meet-duration.ts +71 -0
  25. package/src/meet-hosting.test.ts +58 -0
  26. package/src/meet-hosting.ts +19 -0
  27. package/src/meeting-invitations.test.ts +102 -0
  28. package/src/meeting-invitations.ts +81 -0
  29. package/src/onboarding.ts +1 -0
  30. package/src/platform-release.test.ts +1 -1
  31. package/src/platform-release.ts +1 -1
  32. package/src/portless.ts +1 -0
  33. package/src/task-helper/task-sort.test.ts +22 -3
  34. package/src/task-helper/task-sort.ts +33 -3
  35. package/src/workspace-helper.ts +3 -3
  36. package/src/workspace-limits.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.29.1](https://github.com/tutur3u/platform/compare/utils-v0.29.0...utils-v0.29.1) (2026-09-23)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **auth:** reuse completed session-bound challenges ([b63d510](https://github.com/tutur3u/platform/commit/b63d51046b427350d959137cf37058223e544788))
9
+ * **mobile:** repair Live persistence and reuse session verification ([#5436](https://github.com/tutur3u/platform/issues/5436)) ([cb452e1](https://github.com/tutur3u/platform/commit/cb452e1908eab3db2dce9dfd1d57d721284a2e49))
10
+
11
+ ## [0.29.0](https://github.com/tutur3u/platform/compare/utils-v0.28.0...utils-v0.29.0) (2026-09-20)
12
+
13
+
14
+ ### Features
15
+
16
+ * **infrastructure:** coordinate authenticator and meeting leases on Cloudflare ([2582bf2](https://github.com/tutur3u/platform/commit/2582bf2f72670e1f2696de80820eddd118c27d29))
17
+ * **infrastructure:** use Cloudflare for authenticator and meeting coordination ([#5411](https://github.com/tutur3u/platform/issues/5411)) ([a154602](https://github.com/tutur3u/platform/commit/a15460282959831fe617494126d9955b772957aa))
18
+ * **lettin:** add Cloudflare worldbuilding satellite ([0c8cc22](https://github.com/tutur3u/platform/commit/0c8cc22168477440e326bdec08427daf9721d9bf)) ([#5351](https://github.com/tutur3u/platform/issues/5351)) ([2631103](https://github.com/tutur3u/platform/commit/263110318bb9bab6d26a1bdd539fc52a40b25bec))
19
+ * **meet:** publish invite previews and extend paid call limits ([7cf3d6b](https://github.com/tutur3u/platform/commit/7cf3d6b97b0f353e83e894dcc8c83e0d2b6d7a3b))
20
+ * **meet:** turn notes into reviewed calendar and task follow-ups ([#5359](https://github.com/tutur3u/platform/issues/5359)) ([3914a41](https://github.com/tutur3u/platform/commit/3914a41828e6e837add7f55a3f05a5b42e31f98e))
21
+ * **meet:** unlock hosting for verified paid accounts ([00a0168](https://github.com/tutur3u/platform/commit/00a016891845b43ba0297730937fa68d6e465de8))
22
+ * **meet:** unlock paid hosting, longer calls and public previews ([#5364](https://github.com/tutur3u/platform/issues/5364)) ([a6687ff](https://github.com/tutur3u/platform/commit/a6687ffccda096f2ab9da850bf0ddfe8e04c3cf2))
23
+ * **pricing:** add full app comparison and evolving pitch deck ([c967e8d](https://github.com/tutur3u/platform/commit/c967e8d2db75fc65159c7dcb31ed1b27673ae924))
24
+ * **pricing:** compare every app and add an evolving pitch deck ([#5335](https://github.com/tutur3u/platform/issues/5335)) ([962ed75](https://github.com/tutur3u/platform/commit/962ed75e086eb85a06a878720a5d30b747e7ba82))
25
+
26
+
27
+ ### Bug Fixes
28
+
29
+ * **billing:** fail closed on unknown usage and subscription capacity ([ccfcba4](https://github.com/tutur3u/platform/commit/ccfcba46f236981b1d4e59e1c389bee8472b2ca8)) ([#5333](https://github.com/tutur3u/platform/issues/5333)) ([a308a89](https://github.com/tutur3u/platform/commit/a308a8996f17a81c76c8fb0d498a14d8dbf289c3))
30
+ * **deps:** align React types and published consumer ranges ([3612cd8](https://github.com/tutur3u/platform/commit/3612cd81723fb5a6982152688227ed21bc4095fd))
31
+ * **deps:** preserve React peer compatibility and clarify audit ([777b9ed](https://github.com/tutur3u/platform/commit/777b9ed7f42095bb9361e6bed94d989310c222de))
32
+ * **meet:** fail closed and release rejected follow-up receipts ([c13a295](https://github.com/tutur3u/platform/commit/c13a295c1fabcf8605fb05b2d6a5042db3213426))
33
+ * **payment:** protect webhook ingress and restore order cron ([e059fc7](https://github.com/tutur3u/platform/commit/e059fc74f4eff9e938823b1bd02b42093adc632e))
34
+ * **payment:** restore catalog webhook delivery and guard publication ([#5342](https://github.com/tutur3u/platform/issues/5342)) ([b5f61ec](https://github.com/tutur3u/platform/commit/b5f61ec25617b09245c117e47081544edafa454a))
35
+ * **pricing:** reconcile live catalog displays and tier comparisons ([3aa65c2](https://github.com/tutur3u/platform/commit/3aa65c2221d0d099ff5bd50de0c90bb4237d1bbd))
36
+ * **tasks:** align filtered counts and local task visibility ([913388b](https://github.com/tutur3u/platform/commit/913388b78767f927685df695398da030ad73bf82))
37
+ * **tasks:** show unprioritized tasks first across paginated views ([1a5dffc](https://github.com/tutur3u/platform/commit/1a5dffccf45fc1a9c20353d19d843d3c66c22379))
38
+ * **tasks:** surface unprioritized tasks before paginating ([#5315](https://github.com/tutur3u/platform/issues/5315)) ([496627b](https://github.com/tutur3u/platform/commit/496627b85688b0030199310ad21bd21cf2a92f72))
39
+ * **utils:** declare server-only runtime dependency ([172674d](https://github.com/tutur3u/platform/commit/172674d9168092acf1270fb09050a59d9efcbed6))
40
+
3
41
  ## [0.28.0](https://github.com/tutur3u/platform/compare/utils-v0.27.1...utils-v0.28.0) (2026-09-07)
4
42
 
5
43
 
package/biome.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "root": false,
3
- "$schema": "https://biomejs.dev/schemas/2.5.12/schema.json",
3
+ "$schema": "https://biomejs.dev/schemas/2.5.13/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.28.0",
4
+ "version": "0.29.1",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/tutur3u/platform",
@@ -20,38 +20,39 @@
20
20
  "@noble/ciphers": "^2.4.0",
21
21
  "@noble/hashes": "^2.4.0",
22
22
  "@tanstack/react-query": "^5.102.8",
23
- "@tiptap/core": "3.31.2",
24
- "@tiptap/react": "3.31.2",
25
- "@tuturuuu/google": "0.1.0",
26
- "@tuturuuu/icons": "0.1.0",
27
- "@tuturuuu/internal-api": "0.34.0",
28
- "@tuturuuu/supabase": "0.5.1",
23
+ "@tiptap/core": "3.31.3",
24
+ "@tiptap/react": "3.31.3",
25
+ "@tuturuuu/google": "0.1.1",
26
+ "@tuturuuu/icons": "0.1.1",
27
+ "@tuturuuu/internal-api": "0.40.0",
28
+ "@tuturuuu/supabase": "0.5.2",
29
29
  "@upstash/ratelimit": "^2.0.8",
30
- "@upstash/redis": "^1.38.3",
31
- "cn": "^0.2.5",
30
+ "@upstash/redis": "^1.38.4",
31
+ "cn": "^0.3.0",
32
32
  "date-fns": "^4.4.0",
33
33
  "dayjs": "^1.11.23",
34
34
  "diff": "^9.0.0",
35
- "effect": "^3.22.1",
35
+ "effect": "^3.22.2",
36
36
  "moment": "^2.30.1",
37
- "next": "^16.3.4",
37
+ "next": "^16.3.5",
38
38
  "prosemirror-model": "^1.25.11",
39
39
  "react": "^19.2.8",
40
40
  "react-dom": "^19.2.8",
41
+ "server-only": "^0.0.1",
41
42
  "uuid": "^14.0.2",
42
43
  "y-prosemirror": "^1.3.7",
43
44
  "yjs": "^13.6.32",
44
- "zod": "^4.5.4"
45
+ "zod": "^4.6.2"
45
46
  },
46
47
  "devDependencies": {
47
- "@tuturuuu/types": "0.29.0",
48
+ "@tuturuuu/types": "0.31.0",
48
49
  "@tuturuuu/typescript-config": "0.1.2",
49
50
  "@types/diff": "^8.0.0",
50
- "@types/node": "^26.4.1",
51
- "@types/react": "^19.2.18",
51
+ "@types/node": "^26.5.1",
52
+ "@types/react": "~19.2.18",
52
53
  "@vitejs/plugin-react": "^6.1.1",
53
54
  "typescript": "7.0.2",
54
- "vite": "^8.2.2",
55
+ "vite": "^8.3.0",
55
56
  "vitest": "^5.0.0"
56
57
  },
57
58
  "exports": {
@@ -2231,45 +2231,38 @@ describe('guardApiProxyRequest', () => {
2231
2231
  expect(blockedResponse?.headers.get('X-RateLimit-Policy')).toBe('cron');
2232
2232
  });
2233
2233
 
2234
- it('bypasses trusted webhook traffic only with required signature headers', async () => {
2234
+ it('rate limits webhook requests even with forged signature headers', async () => {
2235
2235
  vi.stubEnv('NODE_ENV', 'production');
2236
2236
  vi.stubEnv('POLAR_WEBHOOK_SECRET', 'polar-secret');
2237
2237
  vi.stubEnv('UPSTASH_REDIS_REST_URL', 'https://redis.test');
2238
2238
  vi.stubEnv('UPSTASH_REDIS_REST_TOKEN', 'token');
2239
2239
  mocks.redis.mockReturnValue({});
2240
-
2241
- const { guardApiProxyRequest, clearApiProxyGuardLimiterCache } =
2242
- await import('../api-proxy-guard.js');
2243
- clearApiProxyGuardLimiterCache();
2244
-
2245
- const allowedResponse = await guardApiProxyRequest(
2246
- makeRequest('/api/payment/webhooks', 'POST', {
2247
- 'webhook-id': 'id',
2248
- 'webhook-signature': 'sig',
2249
- 'webhook-timestamp': 'ts',
2250
- }),
2251
- { prefixBase: 'proxy:test:api' }
2252
- );
2253
-
2254
- expect(allowedResponse).toBeNull();
2255
-
2256
2240
  mocks.extractIp.mockReturnValue('1.2.3.4');
2257
2241
  mocks.isBlocked.mockResolvedValue(null);
2258
- mocks.limit.mockResolvedValueOnce({
2242
+ mocks.limit.mockResolvedValue({
2259
2243
  success: false,
2260
- limit: 12,
2244
+ limit: 30,
2261
2245
  remaining: 0,
2262
2246
  reset: Date.now() + 15_000,
2263
2247
  });
2264
-
2265
- const blockedResponse = await guardApiProxyRequest(
2266
- makeRequest('/api/payment/webhooks', 'POST', {
2248
+ const { guardApiProxyRequest } = await import('../api-proxy-guard.js');
2249
+ const attempts: Record<string, string>[] = [
2250
+ { 'webhook-id': 'id' },
2251
+ {
2267
2252
  'webhook-id': 'id',
2268
- }),
2269
- { prefixBase: 'proxy:test:api' }
2270
- );
2271
-
2272
- expect(blockedResponse?.status).toBe(429);
2253
+ 'webhook-signature': 'sig',
2254
+ 'webhook-timestamp': 'ts',
2255
+ },
2256
+ ];
2257
+ for (const headers of attempts) {
2258
+ const response = await guardApiProxyRequest(
2259
+ makeRequest('/api/payment/webhooks', 'POST', headers),
2260
+ { prefixBase: 'proxy:test:api' }
2261
+ );
2262
+ expect(response?.status).toBe(429);
2263
+ expect(response?.headers.get('X-RateLimit-Policy')).toBe('default');
2264
+ }
2265
+ expect(mocks.limit).toHaveBeenCalledTimes(2);
2273
2266
  });
2274
2267
 
2275
2268
  it('delegates to emoji validation after rate limiting passes', async () => {
@@ -305,6 +305,7 @@ describe('Portless app origin registry', () => {
305
305
  'https://infra.tuturuuu.localhost',
306
306
  'https://inventory.tuturuuu.localhost',
307
307
  'https://learn.tuturuuu.localhost',
308
+ 'https://lettin.tuturuuu.localhost',
308
309
  'https://mail.tuturuuu.localhost',
309
310
  'https://meet.tuturuuu.localhost',
310
311
  'https://mind.tuturuuu.localhost',
@@ -0,0 +1,34 @@
1
+ import { expect, it, vi } from 'vitest';
2
+
3
+ vi.mock('@tuturuuu/supabase/next/server', () => ({
4
+ createAdminClient: vi.fn(),
5
+ createClient: vi.fn(),
6
+ }));
7
+
8
+ import {
9
+ resolveWorkspaceIdForPrincipal,
10
+ WorkspaceNotFoundError,
11
+ WorkspaceResolutionError,
12
+ } from '../workspace-helper';
13
+
14
+ it.each([
15
+ { error: null, Expected: WorkspaceNotFoundError },
16
+ { error: { message: 'unavailable' }, Expected: WorkspaceResolutionError },
17
+ ])(
18
+ 'distinguishes a missing personal workspace from a database lookup failure (%s)',
19
+ async ({ error, Expected }) => {
20
+ const query = {
21
+ select: vi.fn().mockReturnThis(),
22
+ eq: vi.fn().mockReturnThis(),
23
+ maybeSingle: vi.fn().mockResolvedValue({ data: null, error }),
24
+ };
25
+ const db = { from: () => query };
26
+ await expect(
27
+ resolveWorkspaceIdForPrincipal({
28
+ authorizationClient: db as never,
29
+ principal: { id: 'actor', email: null },
30
+ wsId: 'personal',
31
+ })
32
+ ).rejects.toBeInstanceOf(Expected);
33
+ }
34
+ );
@@ -1,8 +1,10 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
2
 
3
3
  const mockRpc = vi.fn();
4
+ const mockFrom = vi.fn();
4
5
  const mockCreateAdminClient = vi.fn(() => ({
5
6
  rpc: mockRpc,
7
+ from: mockFrom,
6
8
  }));
7
9
 
8
10
  vi.mock('@tuturuuu/supabase/next/server', () => ({
@@ -28,6 +30,111 @@ describe('adaptive abuse reputation', () => {
28
30
  mockRpc.mockResolvedValue({ data: [], error: null });
29
31
  });
30
32
 
33
+ it('binds native subjects to authenticated bearer credentials, never a raw token or an anonymous caller', () => {
34
+ const subjects = (token: string, userId?: string) =>
35
+ buildAbuseRiskSubjects({
36
+ headers: { authorization: `Bearer ${token}` },
37
+ userId,
38
+ });
39
+ expect(subjects('secret', 'caller')).toContainEqual(
40
+ expect.objectContaining({ subject_type: 'session' })
41
+ );
42
+ expect(JSON.stringify(subjects('secret', 'caller'))).not.toContain(
43
+ 'secret'
44
+ );
45
+ expect(subjects('secret', 'caller')).not.toEqual(
46
+ subjects('rotated', 'caller')
47
+ );
48
+ expect(subjects('secret')).toEqual([]);
49
+ });
50
+
51
+ it.each([true, false])(
52
+ 'reuses only a recent passed challenge for this authenticated session: %s',
53
+ async (passed) => {
54
+ const lookup = {
55
+ select: vi.fn().mockReturnThis(),
56
+ eq: vi.fn().mockReturnThis(),
57
+ gt: vi.fn().mockReturnThis(),
58
+ gte: vi.fn().mockReturnThis(),
59
+ order: vi.fn().mockReturnThis(),
60
+ limit: vi.fn().mockResolvedValue({
61
+ data: passed ? [{ id: 'proof' }] : [],
62
+ error: null,
63
+ }),
64
+ };
65
+ mockFrom.mockReturnValue(lookup);
66
+ const decision = await resolveAbuseRiskDecision({
67
+ authKind: 'session',
68
+ headers: {
69
+ authorization: 'Bearer session-token',
70
+ 'user-agent': 'Dart/3.9 (dart:io)',
71
+ },
72
+ isRead: false,
73
+ method: 'POST',
74
+ route: '/tasks',
75
+ userId: 'caller',
76
+ });
77
+ expect(decision.tier).toBe(passed ? 'standard' : 'challenge_required');
78
+ expect(decision.trustMultiplier).toBe(1);
79
+ expect(lookup.eq).toHaveBeenCalledWith('user_id', 'caller');
80
+ expect(lookup.eq).toHaveBeenCalledWith(
81
+ 'subject_key',
82
+ decision.subjects.find((subject) => subject.subject_type === 'session')
83
+ ?.subject_key
84
+ );
85
+ expect(lookup.eq).toHaveBeenCalledWith('status', 'passed');
86
+ expect(lookup.gt).toHaveBeenCalledWith(
87
+ 'expires_at',
88
+ '2026-05-17T00:00:00.000Z'
89
+ );
90
+ expect(lookup.gte).toHaveBeenCalledWith(
91
+ 'completed_at',
92
+ '2026-05-16T23:45:00.000Z'
93
+ );
94
+ }
95
+ );
96
+
97
+ it.each(['restricted', 'challenge_required'])(
98
+ 'never overrides explicit server policy: %s',
99
+ async (tier) => {
100
+ mockRpc.mockResolvedValue({
101
+ data: [{ tier, trust_multiplier: 1, decision_source: 'override' }],
102
+ error: null,
103
+ });
104
+ const decision = await resolveAbuseRiskDecision({
105
+ authKind: 'session',
106
+ headers: {
107
+ authorization: 'Bearer session-token',
108
+ 'user-agent': 'Dart/3.9 (dart:io)',
109
+ },
110
+ isRead: false,
111
+ method: 'POST',
112
+ route: '/tasks',
113
+ userId: 'caller',
114
+ });
115
+ expect(decision.tier).toBe(tier);
116
+ expect(mockFrom).not.toHaveBeenCalled();
117
+ }
118
+ );
119
+
120
+ it('fails closed when challenge storage is unavailable', async () => {
121
+ mockFrom.mockImplementation(() => {
122
+ throw new Error('offline');
123
+ });
124
+ const decision = await resolveAbuseRiskDecision({
125
+ authKind: 'session',
126
+ headers: {
127
+ authorization: 'Bearer session-token',
128
+ 'user-agent': 'Dart/3.9 (dart:io)',
129
+ },
130
+ isRead: false,
131
+ method: 'POST',
132
+ route: '/tasks',
133
+ userId: 'caller',
134
+ });
135
+ expect(decision.tier).toBe('challenge_required');
136
+ });
137
+
31
138
  it('does not earn trust from old account and browser shape alone', async () => {
32
139
  const decision = await resolveAbuseRiskDecision({
33
140
  authKind: 'session',
@@ -192,6 +192,9 @@ function readHeader(
192
192
  function getSessionSubjectKey(
193
193
  headers: Headers | Map<string, string> | Record<string, string | null>
194
194
  ) {
195
+ const authorization = readHeader(headers, 'authorization');
196
+ const bearer = authorization?.match(/^Bearer ([^\s]+)$/i)?.[1];
197
+ if (bearer) return `session:${hashStableSubject(`bearer:${bearer}`)}`;
195
198
  const cookies = parseCookieHeader(readHeader(headers, 'cookie'));
196
199
  const authCookie = cookies.find(
197
200
  (cookie) =>
@@ -304,7 +307,7 @@ export function buildAbuseRiskSubjects({
304
307
  subjects.push({ subject_type: 'user', subject_key: `user:${userId}` });
305
308
  }
306
309
 
307
- if (sessionKey) {
310
+ if (sessionKey && userId && !apiKeyId) {
308
311
  subjects.push({ subject_type: 'session', subject_key: sessionKey });
309
312
  }
310
313
 
@@ -365,6 +368,34 @@ async function loadServerTrustDecision({
365
368
  }
366
369
  }
367
370
 
371
+ async function hasRecentSessionChallenge(
372
+ userId: string | null | undefined,
373
+ subjects: AbuseRiskSubject[]
374
+ ) {
375
+ const session = subjects.find(
376
+ (subject) => subject.subject_type === 'session'
377
+ );
378
+ if (!userId || !session) return false;
379
+ const supabase = await getSupabaseAdmin();
380
+ if (!supabase) return false;
381
+ try {
382
+ const now = new Date();
383
+ const { data, error } = await supabase
384
+ .from('abuse_step_up_challenges')
385
+ .select('id')
386
+ .eq('user_id', userId)
387
+ .eq('subject_key', session.subject_key)
388
+ .eq('status', 'passed')
389
+ .gt('expires_at', now.toISOString())
390
+ .gte('completed_at', new Date(now.getTime() - 15 * 60_000).toISOString())
391
+ .order('completed_at', { ascending: false })
392
+ .limit(1);
393
+ return !error && Boolean(data?.length);
394
+ } catch {
395
+ return false;
396
+ }
397
+ }
398
+
368
399
  export async function resolveAbuseRiskDecision(
369
400
  input: ResolveAbuseRiskDecisionInput
370
401
  ): Promise<AbuseRiskDecision> {
@@ -432,11 +463,19 @@ export async function resolveAbuseRiskDecision(
432
463
  (!likelyBrowser || userAgentClassification.riskLevel === 'block');
433
464
 
434
465
  if (suspiciousBrowserMutation && tier !== 'restricted') {
435
- tier = 'challenge_required';
466
+ // A completed session-bound challenge satisfies the browser-shape
467
+ // heuristic only. Server restrictions and explicit challenges still win.
468
+ const cleared =
469
+ serverDecision?.tier !== 'challenge_required' &&
470
+ (input.authKind === 'session' || input.authKind === 'app-session') &&
471
+ (await hasRecentSessionChallenge(input.userId, subjects));
472
+ tier = cleared ? 'standard' : 'challenge_required';
436
473
  trustMultiplier = 1;
437
474
  decisionSource = 'heuristic';
438
475
  subjectKey ??= subjects[0]?.subject_key ?? null;
439
- reasons.push('suspicious_browser_mutation');
476
+ reasons.push(
477
+ cleared ? 'session_challenge_passed' : 'suspicious_browser_mutation'
478
+ );
440
479
  } else if (
441
480
  tier === 'trusted' &&
442
481
  userAgentClassification.riskLevel === 'block'
@@ -628,14 +628,6 @@ function hasHeaderToken(
628
628
  return secrets.some((secret) => !!secret && headerValue === secret);
629
629
  }
630
630
 
631
- function hasPolarWebhookSignatureHeaders(headers: Headers) {
632
- return (
633
- !!headers.get('webhook-id') &&
634
- !!headers.get('webhook-timestamp') &&
635
- !!headers.get('webhook-signature')
636
- );
637
- }
638
-
639
631
  const DEFAULT_TRUSTED_BYPASS_RULES: TrustedProxyBypassRule[] = [
640
632
  {
641
633
  matches: (pathname, headers) =>
@@ -654,13 +646,6 @@ const DEFAULT_TRUSTED_BYPASS_RULES: TrustedProxyBypassRule[] = [
654
646
  process.env.VERCEL_CRON_SECRET,
655
647
  ])),
656
648
  },
657
- {
658
- matches: (pathname, headers) =>
659
- (pathname === '/api/payment/webhooks' ||
660
- pathname.startsWith('/api/payment/webhooks/')) &&
661
- !!process.env.POLAR_WEBHOOK_SECRET &&
662
- hasPolarWebhookSignatureHeaders(headers),
663
- },
664
649
  {
665
650
  matches: (pathname, headers) =>
666
651
  (pathname === '/api/v1/webhooks' ||