@tuturuuu/utils 0.0.2 → 0.6.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.
Files changed (186) hide show
  1. package/CHANGELOG.md +305 -0
  2. package/biome.json +5 -0
  3. package/jsr.json +8 -8
  4. package/package.json +63 -32
  5. package/src/__tests__/ai-temp-auth.test.ts +309 -0
  6. package/src/__tests__/api-proxy-guard.test.ts +1451 -0
  7. package/src/__tests__/app-url.test.ts +270 -0
  8. package/src/__tests__/avatar-url.test.ts +97 -0
  9. package/src/__tests__/color-helper.test.ts +179 -0
  10. package/src/__tests__/constants.test.ts +351 -0
  11. package/src/__tests__/crypto.test.ts +107 -0
  12. package/src/__tests__/date-helper.test.ts +408 -0
  13. package/src/__tests__/fixtures/task-description-full-featured.json +456 -0
  14. package/src/__tests__/format.test.ts +317 -0
  15. package/src/__tests__/html-sanitizer.test.ts +360 -0
  16. package/src/__tests__/interest-calculator.test.ts +336 -0
  17. package/src/__tests__/interest-detector.test.ts +222 -0
  18. package/src/__tests__/label-colors.test.ts +241 -0
  19. package/src/__tests__/name-helper.test.ts +158 -0
  20. package/src/__tests__/node-diff.test.ts +576 -0
  21. package/src/__tests__/notification-service.test.ts +210 -0
  22. package/src/__tests__/onboarding-helper.test.ts +331 -0
  23. package/src/__tests__/path-helper.test.ts +152 -0
  24. package/src/__tests__/permissions.test.tsx +81 -0
  25. package/src/__tests__/request-emoji-limit.test.ts +172 -0
  26. package/src/__tests__/search-helper.test.ts +51 -0
  27. package/src/__tests__/storage-display-name.test.ts +37 -0
  28. package/src/__tests__/storage-path.test.ts +238 -0
  29. package/src/__tests__/tag-utils.test.ts +205 -0
  30. package/src/__tests__/task-description-yjs-state.test.ts +581 -0
  31. package/src/__tests__/task-helper-board-api-routing.test.ts +94 -0
  32. package/src/__tests__/task-helper-create-task.test.ts +129 -0
  33. package/src/__tests__/task-helpers.test.ts +464 -0
  34. package/src/__tests__/task-overrides.test.ts +305 -0
  35. package/src/__tests__/task-reorder-cache.test.ts +74 -0
  36. package/src/__tests__/task-sort-keys.test.ts +36 -0
  37. package/src/__tests__/task-transformers.test.ts +62 -0
  38. package/src/__tests__/text-helper.test.ts +776 -0
  39. package/src/__tests__/time-helper.test.ts +70 -0
  40. package/src/__tests__/time-tracker-period.test.ts +55 -0
  41. package/src/__tests__/timezone.test.ts +117 -0
  42. package/src/__tests__/upstash-rest.test.ts +77 -0
  43. package/src/__tests__/uuid-helper.test.ts +133 -0
  44. package/src/__tests__/workspace-helper.test.ts +859 -0
  45. package/src/__tests__/workspace-limits.test.ts +255 -0
  46. package/src/__tests__/yjs-helper.test.ts +581 -0
  47. package/src/abuse-protection/__tests__/backend-rate-limit.test.ts +113 -0
  48. package/src/abuse-protection/__tests__/edge.test.ts +136 -0
  49. package/src/abuse-protection/__tests__/index.test.ts +562 -0
  50. package/src/abuse-protection/__tests__/reputation.test.ts +192 -0
  51. package/src/abuse-protection/backend-rate-limit.ts +44 -0
  52. package/src/abuse-protection/constants.ts +117 -0
  53. package/src/abuse-protection/edge.ts +223 -0
  54. package/src/abuse-protection/index.ts +1545 -0
  55. package/src/abuse-protection/reputation.ts +587 -0
  56. package/src/abuse-protection/types.ts +97 -0
  57. package/src/abuse-protection/user-agent.ts +124 -0
  58. package/src/abuse-protection/user-suspension.ts +231 -0
  59. package/src/ai-temp-auth.ts +315 -0
  60. package/src/api-proxy-guard.ts +965 -0
  61. package/src/app-url.ts +96 -0
  62. package/src/avatar-url.ts +64 -0
  63. package/src/break-duration.ts +84 -0
  64. package/src/calendar-auth-token.test.ts +37 -0
  65. package/src/calendar-auth-token.ts +19 -0
  66. package/src/calendar-sync-coordination.md +197 -0
  67. package/src/calendar-utils.test.ts +169 -0
  68. package/src/calendar-utils.ts +91 -0
  69. package/src/color-helper.ts +110 -0
  70. package/src/common/nextjs.tsx +99 -0
  71. package/src/common/scan.tsx +15 -0
  72. package/src/configs/reports.ts +160 -0
  73. package/src/constants.ts +85 -0
  74. package/src/crypto.ts +21 -0
  75. package/src/currencies.ts +97 -0
  76. package/src/date-helper.ts +313 -0
  77. package/src/editor/convert-to-task.ts +264 -0
  78. package/src/editor/index.ts +5 -0
  79. package/src/email/__tests__/client.test.ts +141 -0
  80. package/src/email/__tests__/validation.test.ts +46 -0
  81. package/src/email/client.ts +92 -0
  82. package/src/email/server.ts +128 -0
  83. package/src/email/validation.ts +11 -0
  84. package/src/encryption/__tests__/calendar-events.test.ts +411 -0
  85. package/src/encryption/__tests__/configuration.test.ts +114 -0
  86. package/src/encryption/__tests__/field-encryption.test.ts +232 -0
  87. package/src/encryption/__tests__/key-generation.test.ts +30 -0
  88. package/src/encryption/__tests__/performance-edge-cases.test.ts +187 -0
  89. package/src/encryption/__tests__/test-helpers.ts +22 -0
  90. package/src/encryption/__tests__/workspace-key-encryption.test.ts +129 -0
  91. package/src/encryption/encryption-service.ts +343 -0
  92. package/src/encryption/index.ts +25 -0
  93. package/src/encryption/types.ts +57 -0
  94. package/src/exchange-rates.ts +49 -0
  95. package/src/feature-flags/__tests__/feature-flags.test.ts +302 -0
  96. package/src/feature-flags/core.ts +322 -0
  97. package/src/feature-flags/data.ts +16 -0
  98. package/src/feature-flags/default.ts +18 -0
  99. package/src/feature-flags/index.ts +7 -0
  100. package/src/feature-flags/requestable-features.ts +79 -0
  101. package/src/feature-flags/types.ts +4 -0
  102. package/src/fetcher.ts +2 -0
  103. package/src/finance/index.ts +4 -0
  104. package/src/finance/interest-calculator.ts +456 -0
  105. package/src/finance/interest-detector.ts +141 -0
  106. package/src/finance/transform-invoice-results.ts +219 -0
  107. package/src/finance/wallet-permissions.test.ts +169 -0
  108. package/src/finance/wallet-permissions.ts +82 -0
  109. package/src/format.ts +122 -3
  110. package/src/generated/platform-build-metadata.ts +11 -0
  111. package/src/hooks/use-platform.ts +64 -0
  112. package/src/html-sanitizer.ts +155 -0
  113. package/src/internal-domains.ts +497 -0
  114. package/src/keyboard-preset.ts +109 -0
  115. package/src/label-colors.ts +213 -0
  116. package/src/launchable-apps.test.ts +126 -0
  117. package/src/launchable-apps.ts +490 -0
  118. package/src/name-helper.ts +269 -0
  119. package/src/next-config.test.ts +234 -0
  120. package/src/next-config.ts +203 -0
  121. package/src/node-diff.ts +375 -0
  122. package/src/notification-service.ts +379 -0
  123. package/src/nova/scores/__tests__/calculate.test.ts +254 -0
  124. package/src/nova/scores/calculate.ts +132 -0
  125. package/src/nova/submissions/check-permission.ts +132 -0
  126. package/src/onboarding-helper.ts +213 -0
  127. package/src/path-helper.ts +93 -0
  128. package/src/permissions.tsx +1170 -0
  129. package/src/plan-helpers.test.ts +188 -0
  130. package/src/plan-helpers.ts +80 -0
  131. package/src/platform-release.test.ts +74 -0
  132. package/src/platform-release.ts +155 -0
  133. package/src/portless.ts +124 -0
  134. package/src/priority-styles.ts +42 -0
  135. package/src/request-emoji-limit.ts +335 -0
  136. package/src/search-helper.ts +18 -0
  137. package/src/search.test.ts +89 -0
  138. package/src/search.ts +355 -0
  139. package/src/storage-display-name.ts +30 -0
  140. package/src/storage-path.ts +147 -0
  141. package/src/tag-utils.ts +159 -0
  142. package/src/task/reorder.ts +245 -0
  143. package/src/task/transformers.ts +149 -0
  144. package/src/task-date-timezone.ts +133 -0
  145. package/src/task-description-content.ts +240 -0
  146. package/src/task-helper/board.ts +193 -0
  147. package/src/task-helper/bulk-actions.ts +564 -0
  148. package/src/task-helper/personal-external-staging.ts +21 -0
  149. package/src/task-helper/recycle-bin.ts +202 -0
  150. package/src/task-helper/relationships.ts +346 -0
  151. package/src/task-helper/shared.ts +109 -0
  152. package/src/task-helper/sort-keys.ts +337 -0
  153. package/src/task-helper/task-hooks-basic.ts +342 -0
  154. package/src/task-helper/task-hooks-move.ts +264 -0
  155. package/src/task-helper/task-operations.ts +278 -0
  156. package/src/task-helper.ts +12 -0
  157. package/src/task-helpers.ts +241 -0
  158. package/src/task-list-status.ts +62 -0
  159. package/src/task-overrides.ts +82 -0
  160. package/src/task-snapshot.ts +374 -0
  161. package/src/text-diff.ts +81 -0
  162. package/src/text-helper.ts +537 -0
  163. package/src/time-helper.ts +63 -0
  164. package/src/time-tracker-period.ts +73 -0
  165. package/src/timeblock-helper.ts +418 -0
  166. package/src/timezone.ts +190 -0
  167. package/src/timezones.json +1271 -0
  168. package/src/upstash-rest.ts +56 -0
  169. package/src/user-helper.ts +296 -0
  170. package/src/uuid-helper.ts +11 -0
  171. package/src/workspace-handle.ts +10 -0
  172. package/src/workspace-helper.ts +1408 -0
  173. package/src/workspace-limits.ts +68 -0
  174. package/src/yjs-helper.ts +217 -0
  175. package/src/yjs-task-description.ts +81 -0
  176. package/tsconfig.json +3 -5
  177. package/tsconfig.typecheck.json +33 -0
  178. package/vitest.config.ts +36 -0
  179. package/dist/index.d.ts +0 -8
  180. package/dist/index.js +0 -2
  181. package/dist/index.js.map +0 -1
  182. package/dist/index.mjs +0 -2
  183. package/dist/index.mjs.map +0 -1
  184. package/eslint.config.mjs +0 -20
  185. package/rollup.config.js +0 -41
  186. package/src/index.ts +0 -1
@@ -0,0 +1,56 @@
1
+ import type { Redis } from '@upstash/redis';
2
+
3
+ export type UpstashRestRedisClient = Pick<
4
+ Redis,
5
+ 'decr' | 'del' | 'expire' | 'get' | 'incr' | 'set' | 'ttl'
6
+ >;
7
+ export type UpstashRatelimitRedisClient = Pick<
8
+ Redis,
9
+ 'eval' | 'evalsha' | 'get' | 'set'
10
+ >;
11
+
12
+ export function hasUpstashRestEnv(): boolean {
13
+ return Boolean(
14
+ process.env.UPSTASH_REDIS_REST_URL?.trim() &&
15
+ process.env.UPSTASH_REDIS_REST_TOKEN?.trim()
16
+ );
17
+ }
18
+
19
+ export async function getUpstashRestRedisClient(): Promise<UpstashRestRedisClient | null> {
20
+ if (!hasUpstashRestEnv()) {
21
+ return null;
22
+ }
23
+
24
+ const { Redis } = await import('@upstash/redis');
25
+ const client = Redis.fromEnv();
26
+
27
+ const restClient: UpstashRestRedisClient = {
28
+ del: (...keys) => client.del(...keys),
29
+ decr: (key) => client.decr(key),
30
+ expire: (key, seconds) => client.expire(key, seconds),
31
+ get: <T = unknown>(key: string) => client.get<T>(key),
32
+ incr: (key) => client.incr(key),
33
+ set: (key, value, options) => client.set(key, value, options),
34
+ ttl: (key) => client.ttl(key),
35
+ };
36
+
37
+ return restClient;
38
+ }
39
+
40
+ export async function getUpstashRatelimitRedisClient(): Promise<UpstashRatelimitRedisClient | null> {
41
+ if (!hasUpstashRestEnv()) {
42
+ return null;
43
+ }
44
+
45
+ const { Redis } = await import('@upstash/redis');
46
+ const client = Redis.fromEnv();
47
+
48
+ const ratelimitClient: UpstashRatelimitRedisClient = {
49
+ eval: (...args) => client.eval(...args),
50
+ evalsha: (...args) => client.evalsha(...args),
51
+ get: <T = unknown>(key: string) => client.get<T>(key),
52
+ set: (key, value, options) => client.set(key, value, options),
53
+ };
54
+
55
+ return ratelimitClient;
56
+ }
@@ -0,0 +1,296 @@
1
+ import type { TypedSupabaseClient } from '@tuturuuu/supabase/next/client';
2
+ import {
3
+ createAdminClient,
4
+ createClient,
5
+ } from '@tuturuuu/supabase/next/server';
6
+ import type { User, UserPrivateDetails } from '@tuturuuu/types';
7
+ import type { WorkspaceUser } from '@tuturuuu/types/primitives/WorkspaceUser';
8
+
9
+ import { resolveWorkspaceId } from './constants';
10
+ import { verifyWorkspaceMembershipType } from './workspace-helper';
11
+
12
+ async function resolveCurrentUserId(
13
+ supabase: TypedSupabaseClient
14
+ ): Promise<string | null> {
15
+ try {
16
+ const { data: claimsData, error: claimsError } =
17
+ await supabase.auth.getClaims();
18
+
19
+ if (!claimsError && claimsData?.claims?.sub) {
20
+ return claimsData.claims.sub;
21
+ }
22
+ } catch {
23
+ console.warn(
24
+ '[resolveCurrentUserId] getClaims is unavailable, falling back to getUser. This may be expected in testing environments or older Supabase clients.'
25
+ );
26
+ // Fall back to getUser when getClaims is unavailable in mocks/older clients.
27
+ }
28
+
29
+ const {
30
+ data: { user },
31
+ } = await supabase.auth.getUser();
32
+
33
+ return user?.id ?? null;
34
+ }
35
+
36
+ export async function getCurrentSupabaseUser() {
37
+ const supabase = await createClient();
38
+
39
+ const {
40
+ data: { user },
41
+ } = await supabase.auth.getUser();
42
+
43
+ return user;
44
+ }
45
+
46
+ export interface WorkspaceUserLink {
47
+ platform_user_id: string;
48
+ virtual_user_id: string;
49
+ ws_id: string;
50
+ created_at: string;
51
+ workspace_users?: WorkspaceUser;
52
+ }
53
+
54
+ export interface GetCurrentWorkspaceUserOptions {
55
+ /**
56
+ * If true (default), automatically creates a missing workspace_user_linked_users entry
57
+ * when a workspace member doesn't have one. This uses the ensure_workspace_user_link RPC.
58
+ */
59
+ autoRepair?: boolean;
60
+ }
61
+
62
+ /**
63
+ * Gets the current user's workspace user link for the specified workspace.
64
+ * Optionally auto-repairs missing links (enabled by default).
65
+ *
66
+ * @param wsId - The workspace ID (can be a UUID or special identifier like 'personal')
67
+ * @param options - Configuration options
68
+ * @returns The workspace user link with optional nested workspace_users data, or null if not found
69
+ */
70
+ export async function getCurrentWorkspaceUser(
71
+ wsId: string,
72
+ options: GetCurrentWorkspaceUserOptions = {}
73
+ ): Promise<WorkspaceUserLink | null> {
74
+ const { autoRepair = true } = options;
75
+
76
+ const supabase = await createClient();
77
+ const userId = await resolveCurrentUserId(supabase as TypedSupabaseClient);
78
+ if (!userId) return null;
79
+
80
+ const resolvedWsId = resolveWorkspaceId(wsId);
81
+
82
+ // First attempt to get the workspace user link
83
+ const { data: workspaceUser } = await supabase
84
+ .from('workspace_user_linked_users')
85
+ .select(
86
+ 'platform_user_id, virtual_user_id, ws_id, created_at, workspace_users!virtual_user_id(*)'
87
+ )
88
+ .eq('platform_user_id', userId)
89
+ .eq('ws_id', resolvedWsId)
90
+ .limit(1)
91
+ .maybeSingle();
92
+
93
+ // If found, return it
94
+ if (workspaceUser) {
95
+ const linkedData = workspaceUser.workspace_users;
96
+ return {
97
+ platform_user_id: workspaceUser.platform_user_id,
98
+ virtual_user_id: workspaceUser.virtual_user_id,
99
+ ws_id: workspaceUser.ws_id,
100
+ created_at: workspaceUser.created_at,
101
+ ...(linkedData ? { workspace_users: linkedData as WorkspaceUser } : {}),
102
+ };
103
+ }
104
+
105
+ // If not found and auto-repair is disabled, return null
106
+ if (!autoRepair) return null;
107
+
108
+ const membership = await verifyWorkspaceMembershipType({
109
+ wsId: resolvedWsId,
110
+ userId,
111
+ supabase,
112
+ requiredType: 'MEMBER',
113
+ });
114
+
115
+ if (!membership.ok) return null;
116
+
117
+ // Try to repair the missing link using the RPC function
118
+ try {
119
+ const sbAdmin = await createAdminClient();
120
+ // Note: ensure_workspace_user_link is defined in migration 20260112060000
121
+ // Using type assertion since RPC types are generated after migration is applied
122
+ // IMPORTANT: Must use .bind() to preserve the Supabase client's `this` context
123
+ const rpc = sbAdmin.rpc.bind(sbAdmin) as unknown as (
124
+ fn: string,
125
+ args: Record<string, unknown>
126
+ ) => Promise<{ error: Error | null }>;
127
+ const { error: repairError } = await rpc('ensure_workspace_user_link', {
128
+ target_user_id: userId,
129
+ target_ws_id: resolvedWsId,
130
+ });
131
+
132
+ if (repairError) {
133
+ console.error(
134
+ '[getCurrentWorkspaceUser] Failed to auto-repair workspace user link:',
135
+ repairError
136
+ );
137
+ return null;
138
+ }
139
+
140
+ // Fetch the newly created link
141
+ const { data: repairedUser } = await supabase
142
+ .from('workspace_user_linked_users')
143
+ .select(
144
+ 'platform_user_id, virtual_user_id, ws_id, created_at, workspace_users!virtual_user_id(*)'
145
+ )
146
+ .eq('platform_user_id', userId)
147
+ .eq('ws_id', resolvedWsId)
148
+ .limit(1)
149
+ .maybeSingle();
150
+
151
+ if (repairedUser) {
152
+ const linkedData = repairedUser.workspace_users;
153
+ return {
154
+ platform_user_id: repairedUser.platform_user_id,
155
+ virtual_user_id: repairedUser.virtual_user_id,
156
+ ws_id: repairedUser.ws_id,
157
+ created_at: repairedUser.created_at,
158
+ ...(linkedData ? { workspace_users: linkedData as WorkspaceUser } : {}),
159
+ };
160
+ }
161
+ } catch (err) {
162
+ console.error('[getCurrentWorkspaceUser] Error during auto-repair:', err);
163
+ }
164
+
165
+ return null;
166
+ }
167
+
168
+ export async function getCurrentUser() {
169
+ const supabase = await createClient();
170
+
171
+ const userId = await resolveCurrentUserId(supabase as TypedSupabaseClient);
172
+
173
+ if (!userId) {
174
+ return null;
175
+ }
176
+
177
+ const { data, error } = await supabase
178
+ .from('users')
179
+ .select(
180
+ 'id, display_name, avatar_url, bio, handle, created_at, user_private_details(email, new_email, birthday, full_name, default_workspace_id)'
181
+ )
182
+ .eq('id', userId)
183
+ .single();
184
+
185
+ if (error) {
186
+ console.error('Error getting user:', error);
187
+ return null;
188
+ }
189
+
190
+ const { user_private_details, ...rest } = data;
191
+ return { ...rest, ...user_private_details } as
192
+ | (User & UserPrivateDetails)
193
+ | WorkspaceUser;
194
+ }
195
+
196
+ export async function getUserDefaultWorkspace(client?: TypedSupabaseClient) {
197
+ try {
198
+ const supabase = client || (await createClient());
199
+
200
+ const userId = await resolveCurrentUserId(supabase as TypedSupabaseClient);
201
+
202
+ if (!userId) return null;
203
+
204
+ const { data: userData, error: userError } = await supabase
205
+ .from('user_private_details')
206
+ .select('default_workspace_id')
207
+ .eq('user_id', userId)
208
+ .single();
209
+
210
+ if (userError || !userData) return null;
211
+
212
+ const defaultWorkspaceId = userData.default_workspace_id;
213
+
214
+ // If user has a default workspace set, validate it exists and user has access
215
+ if (defaultWorkspaceId) {
216
+ const { data: workspace, error } = await supabase
217
+ .from('workspaces')
218
+ .select('id, name, personal, workspace_members!inner(user_id)')
219
+ .eq('id', defaultWorkspaceId)
220
+ .eq('workspace_members.user_id', userId)
221
+ .single();
222
+
223
+ if (!error && workspace) {
224
+ return workspace;
225
+ }
226
+ }
227
+
228
+ // If no default workspace or invalid, get the personal workspace
229
+ const { data: personalWorkspace, error } = await supabase
230
+ .from('workspaces')
231
+ .select('id, name, personal, workspace_members!inner(user_id)')
232
+ .eq('workspace_members.user_id', userId)
233
+ .eq('personal', true)
234
+ .limit(1)
235
+ .maybeSingle();
236
+
237
+ if (error || !personalWorkspace) {
238
+ return null;
239
+ }
240
+
241
+ return personalWorkspace;
242
+ } catch (error) {
243
+ console.error('Error getting user default workspace:', error);
244
+ return null;
245
+ }
246
+ }
247
+
248
+ export async function updateUserDefaultWorkspace(
249
+ workspaceId: string,
250
+ client?: TypedSupabaseClient
251
+ ) {
252
+ const supabase = client || (await createClient());
253
+
254
+ const userId = await resolveCurrentUserId(supabase as TypedSupabaseClient);
255
+
256
+ if (!userId) return { error: 'User not found' };
257
+
258
+ // Verify user has access to the workspace
259
+ const { data: workspace, error: workspaceError } = await supabase
260
+ .from('workspaces')
261
+ .select('id, workspace_members!inner(user_id)')
262
+ .eq('id', workspaceId)
263
+ .eq('workspace_members.user_id', userId)
264
+ .single();
265
+
266
+ if (workspaceError || !workspace) {
267
+ return { error: 'Workspace not found or access denied' };
268
+ }
269
+
270
+ // Update the user's default workspace
271
+ const { error } = await supabase
272
+ .from('user_private_details')
273
+ .update({ default_workspace_id: workspaceId })
274
+ .eq('user_id', userId);
275
+
276
+ if (error) {
277
+ return { error: error.message };
278
+ }
279
+
280
+ return { success: true };
281
+ }
282
+
283
+ // Function to fetch workspace users
284
+ export async function fetchWorkspaceUsers(
285
+ wsId: string
286
+ ): Promise<WorkspaceUser[]> {
287
+ const supabase = await createClient();
288
+ const { data, error } = await supabase
289
+ .from('workspace_users')
290
+ .select('id, full_name, email, avatar_url')
291
+ .eq('ws_id', wsId)
292
+ .order('full_name', { ascending: true });
293
+
294
+ if (error) throw error;
295
+ return data || [];
296
+ }
@@ -0,0 +1,11 @@
1
+ import { v4 as UUIDv4, v5 as UUIDv5 } from 'uuid';
2
+
3
+ export function generateUUID(...uuids: string[]): string {
4
+ const name = uuids.join('-');
5
+ const namespace = '5b5b7b9f-6432-4c40-b97b-9bd0abb080cf';
6
+ return UUIDv5(name, namespace);
7
+ }
8
+
9
+ export function generateRandomUUID(): string {
10
+ return UUIDv4();
11
+ }
@@ -0,0 +1,10 @@
1
+ import { z } from 'zod';
2
+
3
+ export const WORKSPACE_HANDLE_REGEX = /^[a-z0-9](?:[a-z0-9_-]{0,62}[a-z0-9])?$/;
4
+
5
+ export const workspaceHandleSchema = z
6
+ .string()
7
+ .trim()
8
+ .min(1)
9
+ .max(64)
10
+ .regex(WORKSPACE_HANDLE_REGEX);