@tuturuuu/utils 0.6.0 → 0.7.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,26 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.0](https://github.com/tutur3u/platform/compare/utils-v0.6.1...utils-v0.7.0) (2026-06-15)
4
+
5
+
6
+ ### Features
7
+
8
+ * **infrastructure:** protect mobile deployment vault ([bea31f2](https://github.com/tutur3u/platform/commit/bea31f2f71de509c5bc5e1b154ba62928ecea9c6))
9
+ * **web:** add external user profile-completion links ([0effeb8](https://github.com/tutur3u/platform/commit/0effeb860f999227f673a55212d2cfd0c822105a))
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * **users:** keep database search recoverable ([46066aa](https://github.com/tutur3u/platform/commit/46066aac9b281fb8365038193e420a90bb522e88))
15
+
16
+ ## [0.6.1](https://github.com/tutur3u/platform/compare/utils-v0.6.0...utils-v0.6.1) (2026-06-13)
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+ * **auth:** allow local portless e2e auth ([928da7d](https://github.com/tutur3u/platform/commit/928da7d75ff72298bd1f0d2af6872e951decae3b))
22
+ * **ui:** make package graph installable ([f3eb0ff](https://github.com/tutur3u/platform/commit/f3eb0ff3cbed2e43fd77dfb8164e60c5d195a36b))
23
+
3
24
  ## [0.6.0](https://github.com/tutur3u/platform/compare/utils-v0.5.1...utils-v0.6.0) (2026-06-11)
4
25
 
5
26
 
package/biome.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "root": false,
3
- "$schema": "https://biomejs.dev/schemas/2.4.16/schema.json",
3
+ "$schema": "https://biomejs.dev/schemas/2.5.0/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.6.0",
4
+ "version": "0.7.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/tutur3u/platform",
@@ -22,9 +22,9 @@
22
22
  "@tanstack/react-query": "^5.101.0",
23
23
  "@tiptap/core": "3.26.1",
24
24
  "@tiptap/react": "3.26.1",
25
- "@tuturuuu/google": "0.0.1",
26
- "@tuturuuu/icons": "0.0.5",
27
- "@tuturuuu/internal-api": "0.5.0",
25
+ "@tuturuuu/google": "0.0.2",
26
+ "@tuturuuu/icons": "0.0.6",
27
+ "@tuturuuu/internal-api": "0.8.0",
28
28
  "@tuturuuu/supabase": "0.3.3",
29
29
  "@upstash/ratelimit": "^2.0.8",
30
30
  "@upstash/redis": "^1.38.0",
@@ -44,7 +44,7 @@
44
44
  "zod": "^4.4.3"
45
45
  },
46
46
  "devDependencies": {
47
- "@tuturuuu/types": "0.7.0",
47
+ "@tuturuuu/types": "0.9.0",
48
48
  "@tuturuuu/typescript-config": "0.1.1",
49
49
  "@types/diff": "^8.0.0",
50
50
  "@types/node": "^25.9.3",
@@ -542,6 +542,90 @@ describe('guardApiProxyRequest', () => {
542
542
  });
543
543
  });
544
544
 
545
+ it('uses the users database read-over-post bucket for search and filter POST reads', async () => {
546
+ vi.stubEnv('NODE_ENV', 'production');
547
+ vi.stubEnv('UPSTASH_REDIS_REST_URL', 'https://redis.test');
548
+ vi.stubEnv('UPSTASH_REDIS_REST_TOKEN', 'token');
549
+ mocks.redis.mockReturnValue({});
550
+ mocks.extractIp.mockReturnValue('1.2.3.4');
551
+ mocks.isBlocked.mockResolvedValue(null);
552
+ mocks.limit.mockResolvedValue({
553
+ success: false,
554
+ limit: 300,
555
+ remaining: 0,
556
+ reset: Date.now() + 15_000,
557
+ });
558
+
559
+ const { guardApiProxyRequest, clearApiProxyGuardLimiterCache } =
560
+ await import('../api-proxy-guard.js');
561
+ clearApiProxyGuardLimiterCache();
562
+
563
+ for (const path of [
564
+ '/api/v1/workspaces/ws-1/users/database',
565
+ '/api/v1/workspaces/ws-1/users/groups/featured-counts',
566
+ '/api/v1/workspaces/ws-1/users/groups/possible-excluded',
567
+ ]) {
568
+ const response = await guardApiProxyRequest(makeRequest(path, 'POST'), {
569
+ prefixBase: 'proxy:test:api',
570
+ });
571
+
572
+ expect(response?.status).toBe(429);
573
+ expect(response?.headers.get('X-RateLimit-Limit')).toBe('300');
574
+ expect(response?.headers.get('X-RateLimit-Policy')).toBe(
575
+ 'users-database-read-over-post'
576
+ );
577
+ }
578
+
579
+ expect(mocks.ratelimitConfigs).toContainEqual({
580
+ limit: 300,
581
+ window: '1 m',
582
+ });
583
+ expect(mocks.ratelimitConfigs).toContainEqual({
584
+ limit: 3000,
585
+ window: '1 h',
586
+ });
587
+ expect(mocks.ratelimitConfigs).toContainEqual({
588
+ limit: 20_000,
589
+ window: '1 d',
590
+ });
591
+ expect(mocks.ratelimitPrefixes).toContain(
592
+ 'proxy:test:api:users-database-read-over-post:anonymous:mutate:minute'
593
+ );
594
+ });
595
+
596
+ it('keeps users mutations on the default mutation bucket', async () => {
597
+ vi.stubEnv('NODE_ENV', 'production');
598
+ vi.stubEnv('UPSTASH_REDIS_REST_URL', 'https://redis.test');
599
+ vi.stubEnv('UPSTASH_REDIS_REST_TOKEN', 'token');
600
+ mocks.redis.mockReturnValue({});
601
+ mocks.extractIp.mockReturnValue('1.2.3.4');
602
+ mocks.isBlocked.mockResolvedValue(null);
603
+ mocks.limit.mockResolvedValue({
604
+ success: false,
605
+ limit: 30,
606
+ remaining: 0,
607
+ reset: Date.now() + 15_000,
608
+ });
609
+
610
+ const { guardApiProxyRequest, clearApiProxyGuardLimiterCache } =
611
+ await import('../api-proxy-guard.js');
612
+ clearApiProxyGuardLimiterCache();
613
+
614
+ const response = await guardApiProxyRequest(
615
+ makeRequest(
616
+ '/api/v1/workspaces/ws-1/users/11111111-1111-4111-8111-111111111111/referrals',
617
+ 'POST'
618
+ ),
619
+ {
620
+ prefixBase: 'proxy:test:api',
621
+ }
622
+ );
623
+
624
+ expect(response?.status).toBe(429);
625
+ expect(response?.headers.get('X-RateLimit-Limit')).toBe('30');
626
+ expect(response?.headers.get('X-RateLimit-Policy')).toBe('default');
627
+ });
628
+
545
629
  it('keeps finance invoice mutations on the default mutation bucket', async () => {
546
630
  vi.stubEnv('NODE_ENV', 'production');
547
631
  vi.stubEnv('UPSTASH_REDIS_REST_URL', 'https://redis.test');
@@ -259,6 +259,15 @@ const TASK_BOARD_READ_RATE_LIMITS: RateLimitProfile = {
259
259
  mutate: DEFAULT_MUTATE_RATE_LIMITS,
260
260
  };
261
261
 
262
+ const USERS_DATABASE_READ_OVER_POST_RATE_LIMITS: RateLimitProfile = {
263
+ get: NO_READ_RATE_LIMITS,
264
+ mutate: [
265
+ { window: 'minute', limit: 300, duration: '1 m' },
266
+ { window: 'hour', limit: 3000, duration: '1 h' },
267
+ { window: 'day', limit: 20_000, duration: '1 d' },
268
+ ],
269
+ };
270
+
262
271
  const FINANCE_READ_RATE_LIMITS: RateLimitProfile = {
263
272
  get: [
264
273
  createConfig('minute', '1 m', 1200, 'API_PROXY_FINANCE_READ_LIMIT_MINUTE', [
@@ -299,6 +308,16 @@ function isFinanceRead(req: NextRequest) {
299
308
  );
300
309
  }
301
310
 
311
+ function isUsersDatabaseReadOverPost(req: NextRequest) {
312
+ if (req.method !== 'POST') {
313
+ return false;
314
+ }
315
+
316
+ return /^\/api\/v1\/workspaces\/[^/]+\/users\/(?:database|groups\/(?:featured-counts|possible-excluded))\/?$/u.test(
317
+ req.nextUrl.pathname
318
+ );
319
+ }
320
+
302
321
  const DEFAULT_ROUTE_POLICIES: ProxyRoutePolicy[] = [
303
322
  {
304
323
  key: 'cron',
@@ -363,6 +382,11 @@ const DEFAULT_ROUTE_POLICIES: ProxyRoutePolicy[] = [
363
382
  )),
364
383
  rateLimits: TASK_BOARD_READ_RATE_LIMITS,
365
384
  },
385
+ {
386
+ key: 'users-database-read-over-post',
387
+ matches: isUsersDatabaseReadOverPost,
388
+ rateLimits: USERS_DATABASE_READ_OVER_POST_RATE_LIMITS,
389
+ },
366
390
  {
367
391
  key: 'finance-read',
368
392
  matches: isFinanceRead,
@@ -42,6 +42,7 @@ import {
42
42
  UserCheck,
43
43
  UserCog,
44
44
  UserMinus,
45
+ UserPen,
45
46
  UserPlus,
46
47
  Users,
47
48
  UserX,
@@ -120,6 +121,16 @@ export const permissionGroups = ({
120
121
  disableOnProduction: false,
121
122
  disabled: false,
122
123
  },
124
+ {
125
+ id: 'manage_mobile_deployment_vault' as PermissionId,
126
+ icon: <FileKey2 />,
127
+ title: t('ws-roles.manage_mobile_deployment_vault'),
128
+ description: t(
129
+ 'ws-roles.manage_mobile_deployment_vault_description'
130
+ ),
131
+ disableOnProduction: false,
132
+ disabled: false,
133
+ },
123
134
  {
124
135
  id: 'manage_external_migrations',
125
136
  icon: <DatabaseZap />,
@@ -531,6 +542,14 @@ export const permissionGroups = ({
531
542
  disableOnProduction: false,
532
543
  disabled: false,
533
544
  },
545
+ {
546
+ id: 'manage_user_profile_links',
547
+ icon: <UserPen />,
548
+ title: t('ws-roles.manage_user_profile_links'),
549
+ description: t('ws-roles.manage_user_profile_links_description'),
550
+ disableOnProduction: false,
551
+ disabled: false,
552
+ },
534
553
  {
535
554
  id: 'delete_users',
536
555
  icon: <UserMinus />,
@@ -7,7 +7,7 @@ import {
7
7
 
8
8
  describe('platform release metadata', () => {
9
9
  it('uses the centralized shared browser app version', () => {
10
- expect(TUTURUUU_PLATFORM_VERSION).toBe('0.8.0'); // x-release-please-version
10
+ expect(TUTURUUU_PLATFORM_VERSION).toBe('0.11.0'); // x-release-please-version
11
11
  });
12
12
 
13
13
  it('normalizes generated metadata and derives a short hash', () => {
@@ -1,6 +1,6 @@
1
1
  import { PLATFORM_BUILD_METADATA } from './generated/platform-build-metadata';
2
2
 
3
- export const TUTURUUU_PLATFORM_VERSION = '0.8.0'; // x-release-please-version
3
+ export const TUTURUUU_PLATFORM_VERSION = '0.11.0'; // x-release-please-version
4
4
 
5
5
  export type PlatformBuildMetadataInput = {
6
6
  builtAt?: string | null;