@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,188 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const {
4
+ createAdminClientMock,
5
+ createClientMock,
6
+ getUserMock,
7
+ verifyWorkspaceMembershipTypeMock,
8
+ } = vi.hoisted(() => ({
9
+ createAdminClientMock: vi.fn(),
10
+ createClientMock: vi.fn(),
11
+ getUserMock: vi.fn(),
12
+ verifyWorkspaceMembershipTypeMock: vi.fn(),
13
+ }));
14
+
15
+ vi.mock('@tuturuuu/supabase/next/server', () => ({
16
+ createAdminClient: createAdminClientMock,
17
+ createClient: createClientMock,
18
+ }));
19
+
20
+ vi.mock('./workspace-helper', () => ({
21
+ verifyWorkspaceMembershipType: verifyWorkspaceMembershipTypeMock,
22
+ }));
23
+
24
+ import { getPlan, normalizeMeetTogetherPlanId } from './plan-helpers';
25
+
26
+ function createPlanClient(result: { data: unknown; error: unknown }) {
27
+ const eqCalls: Array<[string, unknown]> = [];
28
+ const query = {
29
+ eq: vi.fn((column: string, value: unknown) => {
30
+ eqCalls.push([column, value]);
31
+ return query;
32
+ }),
33
+ maybeSingle: vi.fn(async () => result),
34
+ select: vi.fn(() => query),
35
+ };
36
+ const client = {
37
+ from: vi.fn((table: string) => {
38
+ if (table !== 'meet_together_plans') {
39
+ throw new Error(`unexpected table ${table}`);
40
+ }
41
+
42
+ return query;
43
+ }),
44
+ };
45
+
46
+ return {
47
+ client,
48
+ eqCalls,
49
+ query,
50
+ };
51
+ }
52
+
53
+ describe('meet plan helpers', () => {
54
+ beforeEach(() => {
55
+ vi.clearAllMocks();
56
+ createClientMock.mockResolvedValue({
57
+ auth: {
58
+ getUser: getUserMock,
59
+ },
60
+ });
61
+ getUserMock.mockResolvedValue({
62
+ data: { user: null },
63
+ });
64
+ verifyWorkspaceMembershipTypeMock.mockResolvedValue({ ok: true });
65
+ });
66
+
67
+ it('normalizes compact legacy plan IDs', () => {
68
+ expect(
69
+ normalizeMeetTogetherPlanId('0123456789abcdef0123456789abcdef')
70
+ ).toBe('01234567-89ab-cdef-0123-456789abcdef');
71
+ expect(
72
+ normalizeMeetTogetherPlanId('01234567-89ab-cdef-0123-456789abcdef')
73
+ ).toBe('01234567-89ab-cdef-0123-456789abcdef');
74
+ });
75
+
76
+ it('allows anonymous reads for public non-workspace plans', async () => {
77
+ const plan = {
78
+ id: 'plan-1',
79
+ is_public: true,
80
+ ws_id: null,
81
+ };
82
+ const mocks = createPlanClient({
83
+ data: plan,
84
+ error: null,
85
+ });
86
+ createAdminClientMock.mockResolvedValue(mocks.client);
87
+
88
+ await expect(getPlan('plan-1', { actorUserId: null })).resolves.toBe(plan);
89
+
90
+ expect(verifyWorkspaceMembershipTypeMock).not.toHaveBeenCalled();
91
+ });
92
+
93
+ it('rejects anonymous reads for public workspace plans', async () => {
94
+ const mocks = createPlanClient({
95
+ data: {
96
+ id: 'plan-1',
97
+ is_public: true,
98
+ ws_id: 'ws-1',
99
+ },
100
+ error: null,
101
+ });
102
+ createAdminClientMock.mockResolvedValue(mocks.client);
103
+
104
+ await expect(getPlan('plan-1', { actorUserId: null })).resolves.toBeNull();
105
+
106
+ expect(verifyWorkspaceMembershipTypeMock).not.toHaveBeenCalled();
107
+ });
108
+
109
+ it('allows workspace members to read public workspace plans', async () => {
110
+ const plan = {
111
+ creator_id: 'creator-1',
112
+ id: 'plan-1',
113
+ is_public: true,
114
+ ws_id: 'ws-1',
115
+ };
116
+ const mocks = createPlanClient({
117
+ data: plan,
118
+ error: null,
119
+ });
120
+ createAdminClientMock.mockResolvedValue(mocks.client);
121
+
122
+ await expect(getPlan('plan-1', { actorUserId: 'member-1' })).resolves.toBe(
123
+ plan
124
+ );
125
+
126
+ expect(verifyWorkspaceMembershipTypeMock).toHaveBeenCalledWith({
127
+ requiredType: 'MEMBER',
128
+ supabase: mocks.client,
129
+ userId: 'member-1',
130
+ wsId: 'ws-1',
131
+ });
132
+ });
133
+
134
+ it('rejects private workspace plans for non-creator members', async () => {
135
+ const mocks = createPlanClient({
136
+ data: {
137
+ creator_id: 'creator-1',
138
+ id: 'plan-1',
139
+ is_public: false,
140
+ ws_id: 'ws-1',
141
+ },
142
+ error: null,
143
+ });
144
+ createAdminClientMock.mockResolvedValue(mocks.client);
145
+
146
+ await expect(
147
+ getPlan('plan-1', { actorUserId: 'member-1' })
148
+ ).resolves.toBeNull();
149
+ });
150
+
151
+ it('allows private workspace plans for creator members', async () => {
152
+ const plan = {
153
+ creator_id: 'creator-1',
154
+ id: 'plan-1',
155
+ is_public: false,
156
+ ws_id: 'ws-1',
157
+ };
158
+ const mocks = createPlanClient({
159
+ data: plan,
160
+ error: null,
161
+ });
162
+ createAdminClientMock.mockResolvedValue(mocks.client);
163
+
164
+ await expect(getPlan('plan-1', { actorUserId: 'creator-1' })).resolves.toBe(
165
+ plan
166
+ );
167
+ });
168
+
169
+ it('falls back to the current Supabase user when no actor is provided', async () => {
170
+ const plan = {
171
+ creator_id: 'creator-1',
172
+ id: 'plan-1',
173
+ is_public: false,
174
+ ws_id: null,
175
+ };
176
+ const mocks = createPlanClient({
177
+ data: plan,
178
+ error: null,
179
+ });
180
+ createAdminClientMock.mockResolvedValue(mocks.client);
181
+ getUserMock.mockResolvedValue({
182
+ data: { user: { id: 'creator-1' } },
183
+ });
184
+
185
+ await expect(getPlan('plan-1')).resolves.toBe(plan);
186
+ expect(createClientMock).toHaveBeenCalled();
187
+ });
188
+ });
@@ -0,0 +1,80 @@
1
+ import {
2
+ createAdminClient,
3
+ createClient,
4
+ } from '@tuturuuu/supabase/next/server';
5
+ import type { MeetTogetherPlan } from '@tuturuuu/types/primitives/MeetTogetherPlan';
6
+ import { verifyWorkspaceMembershipType } from './workspace-helper';
7
+
8
+ interface GetPlanOptions {
9
+ actorUserId?: string | null;
10
+ }
11
+
12
+ export function normalizeMeetTogetherPlanId(planId: string) {
13
+ const trimmed = planId.trim();
14
+
15
+ return trimmed.replace(
16
+ /^([0-9a-f]{8})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{12})$/iu,
17
+ '$1-$2-$3-$4-$5'
18
+ );
19
+ }
20
+
21
+ async function getCurrentSupabaseUserId() {
22
+ const supabase = await createClient();
23
+ const {
24
+ data: { user },
25
+ } = await supabase.auth.getUser();
26
+
27
+ return user?.id ?? null;
28
+ }
29
+
30
+ export async function getPlan(planId: string, options: GetPlanOptions = {}) {
31
+ const sbAdmin = await createAdminClient();
32
+ const normalizedPlanId = normalizeMeetTogetherPlanId(planId);
33
+
34
+ const { data, error } = await sbAdmin
35
+ .from('meet_together_plans')
36
+ .select('*')
37
+ .eq('id', normalizedPlanId)
38
+ .maybeSingle();
39
+
40
+ if (error || !data) {
41
+ console.error(error);
42
+ return null;
43
+ }
44
+
45
+ const plan = data as MeetTogetherPlan;
46
+
47
+ if (plan.is_public && !plan.ws_id) {
48
+ return plan;
49
+ }
50
+
51
+ const actorUserId =
52
+ options.actorUserId === undefined
53
+ ? await getCurrentSupabaseUserId()
54
+ : options.actorUserId;
55
+
56
+ if (!actorUserId) {
57
+ return null;
58
+ }
59
+
60
+ if (!plan.ws_id) {
61
+ return plan.creator_id === actorUserId ? plan : null;
62
+ }
63
+
64
+ const membership = await verifyWorkspaceMembershipType({
65
+ requiredType: 'MEMBER',
66
+ supabase: sbAdmin,
67
+ userId: actorUserId,
68
+ wsId: plan.ws_id,
69
+ });
70
+
71
+ if (!membership.ok) {
72
+ return null;
73
+ }
74
+
75
+ if (plan.is_public || plan.creator_id === actorUserId) {
76
+ return plan;
77
+ }
78
+
79
+ return null;
80
+ }
@@ -0,0 +1,74 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ getPlatformReleaseInfo,
4
+ normalizePlatformBuildMetadata,
5
+ TUTURUUU_PLATFORM_VERSION,
6
+ } from './platform-release';
7
+
8
+ describe('platform release metadata', () => {
9
+ it('uses the centralized shared browser app version', () => {
10
+ expect(TUTURUUU_PLATFORM_VERSION).toBe('0.8.0'); // x-release-please-version
11
+ });
12
+
13
+ it('normalizes generated metadata and derives a short hash', () => {
14
+ expect(
15
+ normalizePlatformBuildMetadata({
16
+ builtAt: '2026-05-27T10:00:00.000Z',
17
+ commitHash: 'abcdef1234567890',
18
+ commitMessage: 'feat: ship version badge',
19
+ deploymentStamp: 'deploy-2026-05-27',
20
+ deploymentUrl: 'https://apps.tuturuuu.com',
21
+ environment: 'production',
22
+ refName: 'production',
23
+ })
24
+ ).toEqual({
25
+ builtAt: '2026-05-27T10:00:00.000Z',
26
+ commitHash: 'abcdef1234567890',
27
+ commitMessage: 'feat: ship version badge',
28
+ deploymentStamp: 'deploy-2026-05-27',
29
+ deploymentUrl: 'https://apps.tuturuuu.com',
30
+ environment: 'production',
31
+ refName: 'production',
32
+ shortCommitHash: 'abcdef1',
33
+ });
34
+ });
35
+
36
+ it('keeps local metadata stable when build values are missing', () => {
37
+ expect(normalizePlatformBuildMetadata({})).toMatchObject({
38
+ builtAt: 'local',
39
+ commitHash: 'local',
40
+ commitMessage: 'Unknown',
41
+ deploymentStamp: null,
42
+ deploymentUrl: null,
43
+ environment: 'local',
44
+ refName: 'local',
45
+ shortCommitHash: 'local',
46
+ });
47
+ });
48
+
49
+ it('prefers explicit runtime platform build metadata over generated fallback values', () => {
50
+ expect(
51
+ getPlatformReleaseInfo('Tuturuuu', {
52
+ PLATFORM_BUILD_BUILT_AT: '2026-05-28T06:00:00.000Z',
53
+ PLATFORM_BUILD_COMMIT_HASH: '1234567890abcdef',
54
+ PLATFORM_BUILD_COMMIT_MESSAGE: 'fix(web): infer blue green metadata',
55
+ PLATFORM_BUILD_COMMIT_SHORT_HASH: '1234567',
56
+ PLATFORM_BUILD_DEPLOYMENT_STAMP: '2026-05-28T06-00-00Z',
57
+ PLATFORM_BUILD_DEPLOYMENT_URL: 'tuturuuu.com',
58
+ PLATFORM_BUILD_ENVIRONMENT: 'production',
59
+ PLATFORM_BUILD_REF_NAME: 'production',
60
+ })
61
+ ).toMatchObject({
62
+ appName: 'Tuturuuu',
63
+ builtAt: '2026-05-28T06:00:00.000Z',
64
+ commitHash: '1234567890abcdef',
65
+ commitMessage: 'fix(web): infer blue green metadata',
66
+ deploymentStamp: '2026-05-28T06-00-00Z',
67
+ deploymentUrl: 'https://tuturuuu.com',
68
+ environment: 'production',
69
+ refName: 'production',
70
+ shortCommitHash: '1234567',
71
+ version: TUTURUUU_PLATFORM_VERSION,
72
+ });
73
+ });
74
+ });
@@ -0,0 +1,155 @@
1
+ import { PLATFORM_BUILD_METADATA } from './generated/platform-build-metadata';
2
+
3
+ export const TUTURUUU_PLATFORM_VERSION = '0.8.0'; // x-release-please-version
4
+
5
+ export type PlatformBuildMetadataInput = {
6
+ builtAt?: string | null;
7
+ commitHash?: string | null;
8
+ commitMessage?: string | null;
9
+ deploymentStamp?: string | null;
10
+ deploymentUrl?: string | null;
11
+ environment?: string | null;
12
+ refName?: string | null;
13
+ shortCommitHash?: string | null;
14
+ };
15
+
16
+ export type PlatformBuildMetadata = {
17
+ builtAt: string;
18
+ commitHash: string;
19
+ commitMessage: string;
20
+ deploymentStamp: string | null;
21
+ deploymentUrl: string | null;
22
+ environment: string;
23
+ refName: string;
24
+ shortCommitHash: string;
25
+ };
26
+
27
+ export type PlatformReleaseInfo = PlatformBuildMetadata & {
28
+ appName: string;
29
+ version: string;
30
+ };
31
+
32
+ export type PlatformBuildRuntimeEnv = Partial<
33
+ Record<
34
+ | 'PLATFORM_BUILD_BUILT_AT'
35
+ | 'PLATFORM_BUILD_COMMIT_HASH'
36
+ | 'PLATFORM_BUILD_COMMIT_MESSAGE'
37
+ | 'PLATFORM_BUILD_COMMIT_SHORT_HASH'
38
+ | 'PLATFORM_BUILD_DEPLOYMENT_STAMP'
39
+ | 'PLATFORM_BUILD_DEPLOYMENT_URL'
40
+ | 'PLATFORM_BUILD_ENVIRONMENT'
41
+ | 'PLATFORM_BUILD_REF_NAME',
42
+ string | null | undefined
43
+ >
44
+ >;
45
+
46
+ function cleanString(value: string | null | undefined) {
47
+ const trimmed = value?.trim();
48
+ return trimmed ? trimmed : null;
49
+ }
50
+
51
+ function normalizeOptionalString(value: string | null | undefined) {
52
+ return cleanString(value);
53
+ }
54
+
55
+ function normalizeUrl(value: string | null | undefined) {
56
+ const url = cleanString(value);
57
+
58
+ if (!url) {
59
+ return null;
60
+ }
61
+
62
+ return /^https?:\/\//iu.test(url) ? url : `https://${url}`;
63
+ }
64
+
65
+ function readRuntimePlatformBuildMetadata(
66
+ env: PlatformBuildRuntimeEnv
67
+ ): PlatformBuildMetadataInput {
68
+ return {
69
+ builtAt: cleanString(env.PLATFORM_BUILD_BUILT_AT),
70
+ commitHash: cleanString(env.PLATFORM_BUILD_COMMIT_HASH),
71
+ commitMessage: cleanString(env.PLATFORM_BUILD_COMMIT_MESSAGE),
72
+ deploymentStamp: cleanString(env.PLATFORM_BUILD_DEPLOYMENT_STAMP),
73
+ deploymentUrl: normalizeUrl(env.PLATFORM_BUILD_DEPLOYMENT_URL),
74
+ environment: cleanString(env.PLATFORM_BUILD_ENVIRONMENT),
75
+ refName: cleanString(env.PLATFORM_BUILD_REF_NAME),
76
+ shortCommitHash: cleanString(env.PLATFORM_BUILD_COMMIT_SHORT_HASH),
77
+ };
78
+ }
79
+
80
+ function getRuntimeEnv(): PlatformBuildRuntimeEnv {
81
+ if (typeof process === 'undefined') {
82
+ return {};
83
+ }
84
+
85
+ return {
86
+ PLATFORM_BUILD_BUILT_AT: process.env.PLATFORM_BUILD_BUILT_AT,
87
+ PLATFORM_BUILD_COMMIT_HASH: process.env.PLATFORM_BUILD_COMMIT_HASH,
88
+ PLATFORM_BUILD_COMMIT_MESSAGE: process.env.PLATFORM_BUILD_COMMIT_MESSAGE,
89
+ PLATFORM_BUILD_COMMIT_SHORT_HASH:
90
+ process.env.PLATFORM_BUILD_COMMIT_SHORT_HASH,
91
+ PLATFORM_BUILD_DEPLOYMENT_STAMP:
92
+ process.env.PLATFORM_BUILD_DEPLOYMENT_STAMP,
93
+ PLATFORM_BUILD_DEPLOYMENT_URL: process.env.PLATFORM_BUILD_DEPLOYMENT_URL,
94
+ PLATFORM_BUILD_ENVIRONMENT: process.env.PLATFORM_BUILD_ENVIRONMENT,
95
+ PLATFORM_BUILD_REF_NAME: process.env.PLATFORM_BUILD_REF_NAME,
96
+ };
97
+ }
98
+
99
+ function mergePlatformBuildMetadata(
100
+ generated: PlatformBuildMetadataInput,
101
+ runtime: PlatformBuildMetadataInput
102
+ ): PlatformBuildMetadataInput {
103
+ return {
104
+ builtAt: runtime.builtAt ?? generated.builtAt,
105
+ commitHash: runtime.commitHash ?? generated.commitHash,
106
+ commitMessage: runtime.commitMessage ?? generated.commitMessage,
107
+ deploymentStamp: runtime.deploymentStamp ?? generated.deploymentStamp,
108
+ deploymentUrl: runtime.deploymentUrl ?? generated.deploymentUrl,
109
+ environment: runtime.environment ?? generated.environment,
110
+ refName: runtime.refName ?? generated.refName,
111
+ shortCommitHash: runtime.shortCommitHash ?? generated.shortCommitHash,
112
+ };
113
+ }
114
+
115
+ export function normalizePlatformBuildMetadata(
116
+ input: PlatformBuildMetadataInput
117
+ ): PlatformBuildMetadata {
118
+ const commitHash = cleanString(input.commitHash) ?? 'local';
119
+ const shortCommitHash =
120
+ cleanString(input.shortCommitHash) ??
121
+ (commitHash === 'local' ? 'local' : commitHash.slice(0, 7));
122
+
123
+ return {
124
+ builtAt: cleanString(input.builtAt) ?? 'local',
125
+ commitHash,
126
+ commitMessage: cleanString(input.commitMessage) ?? 'Unknown',
127
+ deploymentStamp: normalizeOptionalString(input.deploymentStamp),
128
+ deploymentUrl: normalizeUrl(input.deploymentUrl),
129
+ environment: cleanString(input.environment) ?? 'local',
130
+ refName: cleanString(input.refName) ?? 'local',
131
+ shortCommitHash,
132
+ };
133
+ }
134
+
135
+ export const TUTURUUU_PLATFORM_BUILD_METADATA = normalizePlatformBuildMetadata(
136
+ PLATFORM_BUILD_METADATA
137
+ );
138
+
139
+ export function getPlatformReleaseInfo(
140
+ appName: string,
141
+ env: PlatformBuildRuntimeEnv = getRuntimeEnv()
142
+ ): PlatformReleaseInfo {
143
+ const metadata = normalizePlatformBuildMetadata(
144
+ mergePlatformBuildMetadata(
145
+ PLATFORM_BUILD_METADATA,
146
+ readRuntimePlatformBuildMetadata(env)
147
+ )
148
+ );
149
+
150
+ return {
151
+ ...metadata,
152
+ appName,
153
+ version: TUTURUUU_PLATFORM_VERSION,
154
+ };
155
+ }
@@ -0,0 +1,124 @@
1
+ export const TUTURUUU_PORTLESS_ROOT_HOST = 'tuturuuu.localhost';
2
+ export const TUTURUUU_PORTLESS_ROOT_ORIGIN = `https://${TUTURUUU_PORTLESS_ROOT_HOST}`;
3
+ export const TUTURUUU_PORTLESS_ALLOWED_DEV_ORIGINS = [
4
+ TUTURUUU_PORTLESS_ROOT_HOST,
5
+ `*.${TUTURUUU_PORTLESS_ROOT_HOST}`,
6
+ ] as const;
7
+
8
+ export const TUTURUUU_PORTLESS_APP_ORIGINS = {
9
+ apps: `https://apps.${TUTURUUU_PORTLESS_ROOT_HOST}`,
10
+ calendar: `https://calendar.${TUTURUUU_PORTLESS_ROOT_HOST}`,
11
+ chat: `https://chat.${TUTURUUU_PORTLESS_ROOT_HOST}`,
12
+ cms: `https://cms.${TUTURUUU_PORTLESS_ROOT_HOST}`,
13
+ drive: `https://drive.${TUTURUUU_PORTLESS_ROOT_HOST}`,
14
+ external: `https://external.${TUTURUUU_PORTLESS_ROOT_HOST}`,
15
+ finance: `https://finance.${TUTURUUU_PORTLESS_ROOT_HOST}`,
16
+ hive: `https://hive.${TUTURUUU_PORTLESS_ROOT_HOST}`,
17
+ 'hive-realtime': `https://realtime.hive.${TUTURUUU_PORTLESS_ROOT_HOST}`,
18
+ inventory: `https://inventory.${TUTURUUU_PORTLESS_ROOT_HOST}`,
19
+ learn: `https://learn.${TUTURUUU_PORTLESS_ROOT_HOST}`,
20
+ mail: `https://mail.${TUTURUUU_PORTLESS_ROOT_HOST}`,
21
+ meet: `https://meet.${TUTURUUU_PORTLESS_ROOT_HOST}`,
22
+ mind: `https://mind.${TUTURUUU_PORTLESS_ROOT_HOST}`,
23
+ nova: `https://nova.${TUTURUUU_PORTLESS_ROOT_HOST}`,
24
+ platform: TUTURUUU_PORTLESS_ROOT_ORIGIN,
25
+ playground: `https://playground.${TUTURUUU_PORTLESS_ROOT_HOST}`,
26
+ qr: `https://qr.${TUTURUUU_PORTLESS_ROOT_HOST}`,
27
+ rewise: `https://rewise.${TUTURUUU_PORTLESS_ROOT_HOST}`,
28
+ shortener: `https://shortener.${TUTURUUU_PORTLESS_ROOT_HOST}`,
29
+ storefront: `https://storefront.${TUTURUUU_PORTLESS_ROOT_HOST}`,
30
+ tasks: `https://tasks.${TUTURUUU_PORTLESS_ROOT_HOST}`,
31
+ teach: `https://teach.${TUTURUUU_PORTLESS_ROOT_HOST}`,
32
+ track: `https://track.${TUTURUUU_PORTLESS_ROOT_HOST}`,
33
+ } as const;
34
+
35
+ export type TuturuuuPortlessAppName =
36
+ keyof typeof TUTURUUU_PORTLESS_APP_ORIGINS;
37
+
38
+ export const TUTURUUU_PORTLESS_APP_HOSTS = Object.fromEntries(
39
+ Object.entries(TUTURUUU_PORTLESS_APP_ORIGINS).map(([appName, origin]) => [
40
+ appName,
41
+ new URL(origin).hostname,
42
+ ])
43
+ ) as {
44
+ [K in TuturuuuPortlessAppName]: string;
45
+ };
46
+
47
+ function getPortlessWorktreePrefix(portlessUrl?: string) {
48
+ if (!portlessUrl) {
49
+ return null;
50
+ }
51
+
52
+ let hostname: string;
53
+
54
+ try {
55
+ hostname = new URL(portlessUrl).hostname;
56
+ } catch {
57
+ return null;
58
+ }
59
+
60
+ for (const baseHost of Object.values(TUTURUUU_PORTLESS_APP_HOSTS).sort(
61
+ (a, b) => b.length - a.length
62
+ )) {
63
+ if (hostname === baseHost) {
64
+ return null;
65
+ }
66
+
67
+ const suffix = `.${baseHost}`;
68
+
69
+ if (!hostname.endsWith(suffix)) {
70
+ continue;
71
+ }
72
+
73
+ const prefix = hostname.slice(0, -suffix.length);
74
+
75
+ if (prefix && !prefix.includes('.')) {
76
+ return prefix;
77
+ }
78
+ }
79
+
80
+ return null;
81
+ }
82
+
83
+ function getPortlessHostname(portlessUrl?: string) {
84
+ if (!portlessUrl) {
85
+ return null;
86
+ }
87
+
88
+ try {
89
+ return new URL(portlessUrl).hostname;
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+
95
+ export function getTuturuuuPortlessAllowedDevOrigins(
96
+ portlessUrl = process.env.PORTLESS_URL
97
+ ) {
98
+ const portlessHostname = getPortlessHostname(portlessUrl);
99
+ const origins: string[] = [...TUTURUUU_PORTLESS_ALLOWED_DEV_ORIGINS];
100
+
101
+ if (
102
+ portlessHostname &&
103
+ (portlessHostname === TUTURUUU_PORTLESS_ROOT_HOST ||
104
+ portlessHostname.endsWith(`.${TUTURUUU_PORTLESS_ROOT_HOST}`))
105
+ ) {
106
+ origins.push(portlessHostname);
107
+ }
108
+
109
+ return Array.from(new Set(origins));
110
+ }
111
+
112
+ export function getTuturuuuPortlessAppOrigin(
113
+ appName: TuturuuuPortlessAppName,
114
+ options: {
115
+ portlessUrl?: string;
116
+ } = {}
117
+ ) {
118
+ const host = TUTURUUU_PORTLESS_APP_HOSTS[appName];
119
+ const prefix = getPortlessWorktreePrefix(
120
+ options.portlessUrl ?? process.env.PORTLESS_URL
121
+ );
122
+
123
+ return `https://${prefix ? `${prefix}.${host}` : host}`;
124
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Priority styling utilities for task priority badges and labels.
3
+ * Provides consistent styling and user-friendly labels across the application.
4
+ */
5
+
6
+ /**
7
+ * Get Tailwind CSS classes for priority badge styling.
8
+ * Returns appropriate background and text color classes based on priority level.
9
+ *
10
+ * @param priority - Priority level (0-3: None, Low, Medium, High)
11
+ * @returns Tailwind CSS class string for badge styling
12
+ */
13
+ export function getPriorityBadgeStyles(priority: number): string {
14
+ switch (priority) {
15
+ case 1:
16
+ return 'bg-dynamic-blue/10 text-dynamic-blue';
17
+ case 2:
18
+ return 'bg-dynamic-yellow/10 text-dynamic-yellow';
19
+ case 3:
20
+ return 'bg-dynamic-red/10 text-dynamic-red';
21
+ default:
22
+ return 'bg-foreground/10 text-foreground/80';
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Get user-friendly label for priority level.
28
+ * Provides localized (or localizable) labels for each priority.
29
+ *
30
+ * @param priority - Priority level (0-3)
31
+ * @param t - Translation function (optional, for i18n support)
32
+ * @returns User-friendly priority label
33
+ */
34
+ export function getPriorityLabel(
35
+ priority: number,
36
+ t?: (key: string) => string
37
+ ): string {
38
+ const labels = ['None', 'Low', 'Medium', 'High'];
39
+ return t
40
+ ? t(`priority.${labels[priority]?.toLowerCase() || 'none'}`)
41
+ : labels[priority] || 'None';
42
+ }