@tuturuuu/utils 0.28.0 → 0.29.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,35 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.29.0](https://github.com/tutur3u/platform/compare/utils-v0.28.0...utils-v0.29.0) (2026-09-20)
4
+
5
+
6
+ ### Features
7
+
8
+ * **infrastructure:** coordinate authenticator and meeting leases on Cloudflare ([2582bf2](https://github.com/tutur3u/platform/commit/2582bf2f72670e1f2696de80820eddd118c27d29))
9
+ * **infrastructure:** use Cloudflare for authenticator and meeting coordination ([#5411](https://github.com/tutur3u/platform/issues/5411)) ([a154602](https://github.com/tutur3u/platform/commit/a15460282959831fe617494126d9955b772957aa))
10
+ * **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))
11
+ * **meet:** publish invite previews and extend paid call limits ([7cf3d6b](https://github.com/tutur3u/platform/commit/7cf3d6b97b0f353e83e894dcc8c83e0d2b6d7a3b))
12
+ * **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))
13
+ * **meet:** unlock hosting for verified paid accounts ([00a0168](https://github.com/tutur3u/platform/commit/00a016891845b43ba0297730937fa68d6e465de8))
14
+ * **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))
15
+ * **pricing:** add full app comparison and evolving pitch deck ([c967e8d](https://github.com/tutur3u/platform/commit/c967e8d2db75fc65159c7dcb31ed1b27673ae924))
16
+ * **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))
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+ * **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))
22
+ * **deps:** align React types and published consumer ranges ([3612cd8](https://github.com/tutur3u/platform/commit/3612cd81723fb5a6982152688227ed21bc4095fd))
23
+ * **deps:** preserve React peer compatibility and clarify audit ([777b9ed](https://github.com/tutur3u/platform/commit/777b9ed7f42095bb9361e6bed94d989310c222de))
24
+ * **meet:** fail closed and release rejected follow-up receipts ([c13a295](https://github.com/tutur3u/platform/commit/c13a295c1fabcf8605fb05b2d6a5042db3213426))
25
+ * **payment:** protect webhook ingress and restore order cron ([e059fc7](https://github.com/tutur3u/platform/commit/e059fc74f4eff9e938823b1bd02b42093adc632e))
26
+ * **payment:** restore catalog webhook delivery and guard publication ([#5342](https://github.com/tutur3u/platform/issues/5342)) ([b5f61ec](https://github.com/tutur3u/platform/commit/b5f61ec25617b09245c117e47081544edafa454a))
27
+ * **pricing:** reconcile live catalog displays and tier comparisons ([3aa65c2](https://github.com/tutur3u/platform/commit/3aa65c2221d0d099ff5bd50de0c90bb4237d1bbd))
28
+ * **tasks:** align filtered counts and local task visibility ([913388b](https://github.com/tutur3u/platform/commit/913388b78767f927685df695398da030ad73bf82))
29
+ * **tasks:** show unprioritized tasks first across paginated views ([1a5dffc](https://github.com/tutur3u/platform/commit/1a5dffccf45fc1a9c20353d19d843d3c66c22379))
30
+ * **tasks:** surface unprioritized tasks before paginating ([#5315](https://github.com/tutur3u/platform/issues/5315)) ([496627b](https://github.com/tutur3u/platform/commit/496627b85688b0030199310ad21bd21cf2a92f72))
31
+ * **utils:** declare server-only runtime dependency ([172674d](https://github.com/tutur3u/platform/commit/172674d9168092acf1270fb09050a59d9efcbed6))
32
+
3
33
  ## [0.28.0](https://github.com/tutur3u/platform/compare/utils-v0.27.1...utils-v0.28.0) (2026-09-07)
4
34
 
5
35
 
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.0",
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.39.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
+ );
@@ -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' ||
@@ -0,0 +1,294 @@
1
+ import type { ComparisonFeature } from './commercial-comparison-types';
2
+ export const features0: ComparisonFeature[] = [
3
+ {
4
+ id: 'attachments',
5
+ app: 'capacity',
6
+ category: 'capacity',
7
+ values: ['10', '10', '10', '10'],
8
+ detail: 'attachment',
9
+ pending: false,
10
+ },
11
+ {
12
+ id: 'publishers',
13
+ app: 'capacity',
14
+ category: 'capacity',
15
+ values: ['8 / 96', '8 / 96', '8 / 96', '8 / 96'],
16
+ detail: 'meetSafety',
17
+ pending: false,
18
+ },
19
+ {
20
+ id: 'duration',
21
+ app: 'capacity',
22
+ category: 'capacity',
23
+ values: ['2 h', '2 h', '2 h', '2 h'],
24
+ detail: 'meetSafety',
25
+ pending: false,
26
+ },
27
+ {
28
+ id: 'workspaces',
29
+ app: 'capacity',
30
+ category: 'capacity',
31
+ values: ['10', '10', '10', '10'],
32
+ detail: 'account',
33
+ pending: false,
34
+ },
35
+ {
36
+ id: 'board',
37
+ app: 'capacity',
38
+ category: 'capacity',
39
+ values: ['10', '50', '50', 'contract'],
40
+ detail: 'board',
41
+ pending: false,
42
+ },
43
+ {
44
+ id: 'responses',
45
+ app: 'capacity',
46
+ category: 'capacity',
47
+ values: ['100', '1,000', '5,000', 'contract'],
48
+ detail: 'monthlyAllowance',
49
+ pending: true,
50
+ },
51
+ {
52
+ id: 'automation',
53
+ app: 'capacity',
54
+ category: 'capacity',
55
+ values: ['100', '1,000', '5,000', 'contract'],
56
+ detail: 'monthlyAllowance',
57
+ pending: true,
58
+ },
59
+ {
60
+ id: 'aiCredits',
61
+ app: 'capacity',
62
+ category: 'capacity',
63
+ values: ['1,000', '10,000', '30,000', 'contract'],
64
+ detail: 'aiAllowance',
65
+ pending: true,
66
+ },
67
+ {
68
+ id: 'storage',
69
+ app: 'capacity',
70
+ category: 'capacity',
71
+ values: ['1 GiB', '20 GiB', '100 GiB', 'contract'],
72
+ detail: 'storageAllowance',
73
+ pending: true,
74
+ },
75
+ {
76
+ id: 'guests',
77
+ app: 'capacity',
78
+ category: 'capacity',
79
+ values: ['10', '50', '250', 'contract'],
80
+ detail: 'guests',
81
+ pending: true,
82
+ },
83
+ {
84
+ id: 'members',
85
+ app: 'capacity',
86
+ category: 'capacity',
87
+ values: ['5', 'seats', 'seats', 'contract'],
88
+ detail: 'members',
89
+ pending: true,
90
+ },
91
+ {
92
+ id: 'platform_0',
93
+ app: 'platform',
94
+ category: 'work',
95
+ values: ['included', 'included', 'included', 'included'],
96
+ detail: 'standard',
97
+ },
98
+ {
99
+ id: 'platform_1',
100
+ app: 'platform',
101
+ category: 'work',
102
+ values: ['included', 'included', 'included', 'included'],
103
+ detail: 'standard',
104
+ },
105
+ {
106
+ id: 'platform_2',
107
+ app: 'platform',
108
+ category: 'work',
109
+ values: ['included', 'included', 'included', 'included'],
110
+ detail: 'standard',
111
+ },
112
+ {
113
+ id: 'platform_3',
114
+ app: 'platform',
115
+ category: 'work',
116
+ values: ['included', 'included', 'included', 'included'],
117
+ detail: 'standard',
118
+ },
119
+ {
120
+ id: 'platform_4',
121
+ app: 'platform',
122
+ category: 'work',
123
+ values: ['included', 'included', 'included', 'included'],
124
+ detail: 'standard',
125
+ },
126
+ {
127
+ id: 'platform_5',
128
+ app: 'platform',
129
+ category: 'work',
130
+ values: ['included', 'included', 'included', 'included'],
131
+ detail: 'standard',
132
+ },
133
+ {
134
+ id: 'tasks_0',
135
+ app: 'tasks',
136
+ category: 'work',
137
+ values: ['included', 'included', 'included', 'included'],
138
+ detail: 'standard',
139
+ },
140
+ {
141
+ id: 'tasks_1',
142
+ app: 'tasks',
143
+ category: 'work',
144
+ values: ['included', 'included', 'included', 'included'],
145
+ detail: 'standard',
146
+ },
147
+ {
148
+ id: 'tasks_2',
149
+ app: 'tasks',
150
+ category: 'work',
151
+ values: ['included', 'included', 'included', 'included'],
152
+ detail: 'standard',
153
+ },
154
+ {
155
+ id: 'tasks_3',
156
+ app: 'tasks',
157
+ category: 'work',
158
+ values: ['included', 'included', 'included', 'included'],
159
+ detail: 'standard',
160
+ },
161
+ {
162
+ id: 'tasks_4',
163
+ app: 'tasks',
164
+ category: 'work',
165
+ values: ['included', 'included', 'included', 'included'],
166
+ detail: 'standard',
167
+ },
168
+ {
169
+ id: 'calendar_0',
170
+ app: 'calendar',
171
+ category: 'work',
172
+ values: ['included', 'included', 'included', 'included'],
173
+ detail: 'standard',
174
+ },
175
+ {
176
+ id: 'calendar_1',
177
+ app: 'calendar',
178
+ category: 'work',
179
+ values: ['included', 'included', 'included', 'included'],
180
+ detail: 'standard',
181
+ },
182
+ {
183
+ id: 'calendar_2',
184
+ app: 'calendar',
185
+ category: 'work',
186
+ values: ['included', 'included', 'included', 'included'],
187
+ detail: 'standard',
188
+ },
189
+ {
190
+ id: 'track_0',
191
+ app: 'track',
192
+ category: 'work',
193
+ values: ['included', 'included', 'included', 'included'],
194
+ detail: 'standard',
195
+ },
196
+ {
197
+ id: 'track_1',
198
+ app: 'track',
199
+ category: 'work',
200
+ values: ['included', 'included', 'included', 'included'],
201
+ detail: 'standard',
202
+ },
203
+ {
204
+ id: 'track_2',
205
+ app: 'track',
206
+ category: 'work',
207
+ values: ['included', 'included', 'included', 'included'],
208
+ detail: 'standard',
209
+ },
210
+ {
211
+ id: 'track_3',
212
+ app: 'track',
213
+ category: 'work',
214
+ values: ['included', 'included', 'included', 'included'],
215
+ detail: 'standard',
216
+ },
217
+ {
218
+ id: 'drive_0',
219
+ app: 'drive',
220
+ category: 'work',
221
+ values: ['included', 'included', 'included', 'included'],
222
+ detail: 'storage',
223
+ },
224
+ {
225
+ id: 'drive_1',
226
+ app: 'drive',
227
+ category: 'work',
228
+ values: ['included', 'included', 'included', 'included'],
229
+ detail: 'storage',
230
+ },
231
+ {
232
+ id: 'drive_2',
233
+ app: 'drive',
234
+ category: 'work',
235
+ values: ['included', 'included', 'included', 'included'],
236
+ detail: 'storage',
237
+ },
238
+ {
239
+ id: 'drive_3',
240
+ app: 'drive',
241
+ category: 'work',
242
+ values: ['included', 'included', 'included', 'included'],
243
+ detail: 'storage',
244
+ },
245
+ {
246
+ id: 'chat_0',
247
+ app: 'chat',
248
+ category: 'work',
249
+ values: ['excluded', 'included', 'included', 'included'],
250
+ detail: 'storage',
251
+ },
252
+ {
253
+ id: 'chat_1',
254
+ app: 'chat',
255
+ category: 'work',
256
+ values: ['excluded', 'included', 'included', 'included'],
257
+ detail: 'storage',
258
+ },
259
+ {
260
+ id: 'chat_2',
261
+ app: 'chat',
262
+ category: 'work',
263
+ values: ['excluded', 'included', 'included', 'included'],
264
+ detail: 'storage',
265
+ },
266
+ {
267
+ id: 'meet_0',
268
+ app: 'meet',
269
+ category: 'work',
270
+ values: ['preview', 'preview', 'preview', 'preview'],
271
+ detail: 'standard',
272
+ },
273
+ {
274
+ id: 'meet_1',
275
+ app: 'meet',
276
+ category: 'work',
277
+ values: ['preview', 'preview', 'preview', 'preview'],
278
+ detail: 'standard',
279
+ },
280
+ {
281
+ id: 'meet_2',
282
+ app: 'meet',
283
+ category: 'work',
284
+ values: ['preview', 'preview', 'preview', 'preview'],
285
+ detail: 'standard',
286
+ },
287
+ {
288
+ id: 'meet_3',
289
+ app: 'meet',
290
+ category: 'work',
291
+ values: ['preview', 'preview', 'preview', 'preview'],
292
+ detail: 'standard',
293
+ },
294
+ ];