@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 +21 -0
- package/biome.json +1 -1
- package/package.json +5 -5
- package/src/__tests__/api-proxy-guard.test.ts +84 -0
- package/src/api-proxy-guard.ts +24 -0
- package/src/permissions.tsx +19 -0
- package/src/platform-release.test.ts +1 -1
- package/src/platform-release.ts +1 -1
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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuturuuu/utils",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "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.
|
|
26
|
-
"@tuturuuu/icons": "0.0.
|
|
27
|
-
"@tuturuuu/internal-api": "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.
|
|
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');
|
package/src/api-proxy-guard.ts
CHANGED
|
@@ -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,
|
package/src/permissions.tsx
CHANGED
|
@@ -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.
|
|
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', () => {
|
package/src/platform-release.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PLATFORM_BUILD_METADATA } from './generated/platform-build-metadata';
|
|
2
2
|
|
|
3
|
-
export const TUTURUUU_PLATFORM_VERSION = '0.
|
|
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;
|