@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.
- package/CHANGELOG.md +305 -0
- package/biome.json +5 -0
- package/jsr.json +8 -8
- package/package.json +63 -32
- package/src/__tests__/ai-temp-auth.test.ts +309 -0
- package/src/__tests__/api-proxy-guard.test.ts +1451 -0
- package/src/__tests__/app-url.test.ts +270 -0
- package/src/__tests__/avatar-url.test.ts +97 -0
- package/src/__tests__/color-helper.test.ts +179 -0
- package/src/__tests__/constants.test.ts +351 -0
- package/src/__tests__/crypto.test.ts +107 -0
- package/src/__tests__/date-helper.test.ts +408 -0
- package/src/__tests__/fixtures/task-description-full-featured.json +456 -0
- package/src/__tests__/format.test.ts +317 -0
- package/src/__tests__/html-sanitizer.test.ts +360 -0
- package/src/__tests__/interest-calculator.test.ts +336 -0
- package/src/__tests__/interest-detector.test.ts +222 -0
- package/src/__tests__/label-colors.test.ts +241 -0
- package/src/__tests__/name-helper.test.ts +158 -0
- package/src/__tests__/node-diff.test.ts +576 -0
- package/src/__tests__/notification-service.test.ts +210 -0
- package/src/__tests__/onboarding-helper.test.ts +331 -0
- package/src/__tests__/path-helper.test.ts +152 -0
- package/src/__tests__/permissions.test.tsx +81 -0
- package/src/__tests__/request-emoji-limit.test.ts +172 -0
- package/src/__tests__/search-helper.test.ts +51 -0
- package/src/__tests__/storage-display-name.test.ts +37 -0
- package/src/__tests__/storage-path.test.ts +238 -0
- package/src/__tests__/tag-utils.test.ts +205 -0
- package/src/__tests__/task-description-yjs-state.test.ts +581 -0
- package/src/__tests__/task-helper-board-api-routing.test.ts +94 -0
- package/src/__tests__/task-helper-create-task.test.ts +129 -0
- package/src/__tests__/task-helpers.test.ts +464 -0
- package/src/__tests__/task-overrides.test.ts +305 -0
- package/src/__tests__/task-reorder-cache.test.ts +74 -0
- package/src/__tests__/task-sort-keys.test.ts +36 -0
- package/src/__tests__/task-transformers.test.ts +62 -0
- package/src/__tests__/text-helper.test.ts +776 -0
- package/src/__tests__/time-helper.test.ts +70 -0
- package/src/__tests__/time-tracker-period.test.ts +55 -0
- package/src/__tests__/timezone.test.ts +117 -0
- package/src/__tests__/upstash-rest.test.ts +77 -0
- package/src/__tests__/uuid-helper.test.ts +133 -0
- package/src/__tests__/workspace-helper.test.ts +859 -0
- package/src/__tests__/workspace-limits.test.ts +255 -0
- package/src/__tests__/yjs-helper.test.ts +581 -0
- package/src/abuse-protection/__tests__/backend-rate-limit.test.ts +113 -0
- package/src/abuse-protection/__tests__/edge.test.ts +136 -0
- package/src/abuse-protection/__tests__/index.test.ts +562 -0
- package/src/abuse-protection/__tests__/reputation.test.ts +192 -0
- package/src/abuse-protection/backend-rate-limit.ts +44 -0
- package/src/abuse-protection/constants.ts +117 -0
- package/src/abuse-protection/edge.ts +223 -0
- package/src/abuse-protection/index.ts +1545 -0
- package/src/abuse-protection/reputation.ts +587 -0
- package/src/abuse-protection/types.ts +97 -0
- package/src/abuse-protection/user-agent.ts +124 -0
- package/src/abuse-protection/user-suspension.ts +231 -0
- package/src/ai-temp-auth.ts +315 -0
- package/src/api-proxy-guard.ts +965 -0
- package/src/app-url.ts +96 -0
- package/src/avatar-url.ts +64 -0
- package/src/break-duration.ts +84 -0
- package/src/calendar-auth-token.test.ts +37 -0
- package/src/calendar-auth-token.ts +19 -0
- package/src/calendar-sync-coordination.md +197 -0
- package/src/calendar-utils.test.ts +169 -0
- package/src/calendar-utils.ts +91 -0
- package/src/color-helper.ts +110 -0
- package/src/common/nextjs.tsx +99 -0
- package/src/common/scan.tsx +15 -0
- package/src/configs/reports.ts +160 -0
- package/src/constants.ts +85 -0
- package/src/crypto.ts +21 -0
- package/src/currencies.ts +97 -0
- package/src/date-helper.ts +313 -0
- package/src/editor/convert-to-task.ts +264 -0
- package/src/editor/index.ts +5 -0
- package/src/email/__tests__/client.test.ts +141 -0
- package/src/email/__tests__/validation.test.ts +46 -0
- package/src/email/client.ts +92 -0
- package/src/email/server.ts +128 -0
- package/src/email/validation.ts +11 -0
- package/src/encryption/__tests__/calendar-events.test.ts +411 -0
- package/src/encryption/__tests__/configuration.test.ts +114 -0
- package/src/encryption/__tests__/field-encryption.test.ts +232 -0
- package/src/encryption/__tests__/key-generation.test.ts +30 -0
- package/src/encryption/__tests__/performance-edge-cases.test.ts +187 -0
- package/src/encryption/__tests__/test-helpers.ts +22 -0
- package/src/encryption/__tests__/workspace-key-encryption.test.ts +129 -0
- package/src/encryption/encryption-service.ts +343 -0
- package/src/encryption/index.ts +25 -0
- package/src/encryption/types.ts +57 -0
- package/src/exchange-rates.ts +49 -0
- package/src/feature-flags/__tests__/feature-flags.test.ts +302 -0
- package/src/feature-flags/core.ts +322 -0
- package/src/feature-flags/data.ts +16 -0
- package/src/feature-flags/default.ts +18 -0
- package/src/feature-flags/index.ts +7 -0
- package/src/feature-flags/requestable-features.ts +79 -0
- package/src/feature-flags/types.ts +4 -0
- package/src/fetcher.ts +2 -0
- package/src/finance/index.ts +4 -0
- package/src/finance/interest-calculator.ts +456 -0
- package/src/finance/interest-detector.ts +141 -0
- package/src/finance/transform-invoice-results.ts +219 -0
- package/src/finance/wallet-permissions.test.ts +169 -0
- package/src/finance/wallet-permissions.ts +82 -0
- package/src/format.ts +122 -3
- package/src/generated/platform-build-metadata.ts +11 -0
- package/src/hooks/use-platform.ts +64 -0
- package/src/html-sanitizer.ts +155 -0
- package/src/internal-domains.ts +497 -0
- package/src/keyboard-preset.ts +109 -0
- package/src/label-colors.ts +213 -0
- package/src/launchable-apps.test.ts +126 -0
- package/src/launchable-apps.ts +490 -0
- package/src/name-helper.ts +269 -0
- package/src/next-config.test.ts +234 -0
- package/src/next-config.ts +203 -0
- package/src/node-diff.ts +375 -0
- package/src/notification-service.ts +379 -0
- package/src/nova/scores/__tests__/calculate.test.ts +254 -0
- package/src/nova/scores/calculate.ts +132 -0
- package/src/nova/submissions/check-permission.ts +132 -0
- package/src/onboarding-helper.ts +213 -0
- package/src/path-helper.ts +93 -0
- package/src/permissions.tsx +1170 -0
- package/src/plan-helpers.test.ts +188 -0
- package/src/plan-helpers.ts +80 -0
- package/src/platform-release.test.ts +74 -0
- package/src/platform-release.ts +155 -0
- package/src/portless.ts +124 -0
- package/src/priority-styles.ts +42 -0
- package/src/request-emoji-limit.ts +335 -0
- package/src/search-helper.ts +18 -0
- package/src/search.test.ts +89 -0
- package/src/search.ts +355 -0
- package/src/storage-display-name.ts +30 -0
- package/src/storage-path.ts +147 -0
- package/src/tag-utils.ts +159 -0
- package/src/task/reorder.ts +245 -0
- package/src/task/transformers.ts +149 -0
- package/src/task-date-timezone.ts +133 -0
- package/src/task-description-content.ts +240 -0
- package/src/task-helper/board.ts +193 -0
- package/src/task-helper/bulk-actions.ts +564 -0
- package/src/task-helper/personal-external-staging.ts +21 -0
- package/src/task-helper/recycle-bin.ts +202 -0
- package/src/task-helper/relationships.ts +346 -0
- package/src/task-helper/shared.ts +109 -0
- package/src/task-helper/sort-keys.ts +337 -0
- package/src/task-helper/task-hooks-basic.ts +342 -0
- package/src/task-helper/task-hooks-move.ts +264 -0
- package/src/task-helper/task-operations.ts +278 -0
- package/src/task-helper.ts +12 -0
- package/src/task-helpers.ts +241 -0
- package/src/task-list-status.ts +62 -0
- package/src/task-overrides.ts +82 -0
- package/src/task-snapshot.ts +374 -0
- package/src/text-diff.ts +81 -0
- package/src/text-helper.ts +537 -0
- package/src/time-helper.ts +63 -0
- package/src/time-tracker-period.ts +73 -0
- package/src/timeblock-helper.ts +418 -0
- package/src/timezone.ts +190 -0
- package/src/timezones.json +1271 -0
- package/src/upstash-rest.ts +56 -0
- package/src/user-helper.ts +296 -0
- package/src/uuid-helper.ts +11 -0
- package/src/workspace-handle.ts +10 -0
- package/src/workspace-helper.ts +1408 -0
- package/src/workspace-limits.ts +68 -0
- package/src/yjs-helper.ts +217 -0
- package/src/yjs-task-description.ts +81 -0
- package/tsconfig.json +3 -5
- package/tsconfig.typecheck.json +33 -0
- package/vitest.config.ts +36 -0
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -2
- package/dist/index.mjs.map +0 -1
- package/eslint.config.mjs +0 -20
- package/rollup.config.js +0 -41
- 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
|
+
}
|
package/src/portless.ts
ADDED
|
@@ -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
|
+
}
|