@tuturuuu/utils 0.0.3 → 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 +120 -1
- 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,411 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
decryptCalendarEventFields,
|
|
4
|
+
decryptCalendarEvents,
|
|
5
|
+
encryptCalendarEventFields,
|
|
6
|
+
encryptCalendarEvents,
|
|
7
|
+
} from '../encryption-service';
|
|
8
|
+
import type { CalendarEventWithEncryption } from '../types';
|
|
9
|
+
import { generateWorkspaceKey } from './test-helpers';
|
|
10
|
+
|
|
11
|
+
describe('encryption-service', () => {
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Calendar Event Field Encryption Tests
|
|
14
|
+
// ============================================================================
|
|
15
|
+
describe('calendar event field encryption', () => {
|
|
16
|
+
let workspaceKey: Buffer;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
workspaceKey = generateWorkspaceKey();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should encrypt and decrypt calendar event fields', () => {
|
|
23
|
+
const event = {
|
|
24
|
+
title: 'Team Standup',
|
|
25
|
+
description: 'Daily standup meeting',
|
|
26
|
+
location: 'Conference Room A',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const encrypted = encryptCalendarEventFields(event, workspaceKey);
|
|
30
|
+
|
|
31
|
+
expect(encrypted.title).not.toBe(event.title);
|
|
32
|
+
expect(encrypted.description).not.toBe(event.description);
|
|
33
|
+
expect(encrypted.location).not.toBe(event.location);
|
|
34
|
+
|
|
35
|
+
const decrypted = decryptCalendarEventFields(encrypted, workspaceKey);
|
|
36
|
+
|
|
37
|
+
expect(decrypted.title).toBe(event.title);
|
|
38
|
+
expect(decrypted.description).toBe(event.description);
|
|
39
|
+
expect(decrypted.location).toBe(event.location);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should handle undefined location', () => {
|
|
43
|
+
const event = {
|
|
44
|
+
title: 'Phone Call',
|
|
45
|
+
description: 'Quick sync call',
|
|
46
|
+
location: undefined,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const encrypted = encryptCalendarEventFields(event, workspaceKey);
|
|
50
|
+
expect(encrypted.location).toBeUndefined();
|
|
51
|
+
|
|
52
|
+
const decrypted = decryptCalendarEventFields(encrypted, workspaceKey);
|
|
53
|
+
expect(decrypted.location).toBeUndefined();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should handle empty title and description', () => {
|
|
57
|
+
const event = {
|
|
58
|
+
title: '',
|
|
59
|
+
description: '',
|
|
60
|
+
location: undefined,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const encrypted = encryptCalendarEventFields(event, workspaceKey);
|
|
64
|
+
const decrypted = decryptCalendarEventFields(encrypted, workspaceKey);
|
|
65
|
+
|
|
66
|
+
expect(decrypted.title).toBe('');
|
|
67
|
+
expect(decrypted.description).toBe('');
|
|
68
|
+
expect(decrypted.location).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should handle null title and description', () => {
|
|
72
|
+
const event = {
|
|
73
|
+
title: null as unknown as string,
|
|
74
|
+
description: null as unknown as string,
|
|
75
|
+
location: undefined,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const encrypted = encryptCalendarEventFields(event, workspaceKey);
|
|
79
|
+
const decrypted = decryptCalendarEventFields(encrypted, workspaceKey);
|
|
80
|
+
|
|
81
|
+
expect(decrypted.title).toBe('');
|
|
82
|
+
expect(decrypted.description).toBe('');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should preserve non-encrypted fields', () => {
|
|
86
|
+
const event = {
|
|
87
|
+
title: 'Meeting',
|
|
88
|
+
description: 'Important meeting',
|
|
89
|
+
location: 'Room 101',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const encrypted = encryptCalendarEventFields(event, workspaceKey);
|
|
93
|
+
|
|
94
|
+
// Only title, description, location should be in the result
|
|
95
|
+
expect(Object.keys(encrypted)).toEqual([
|
|
96
|
+
'title',
|
|
97
|
+
'description',
|
|
98
|
+
'location',
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ============================================================================
|
|
104
|
+
// Batch Encryption/Decryption Tests
|
|
105
|
+
// ============================================================================
|
|
106
|
+
describe('batch calendar event operations', () => {
|
|
107
|
+
let workspaceKey: Buffer;
|
|
108
|
+
|
|
109
|
+
beforeEach(() => {
|
|
110
|
+
workspaceKey = generateWorkspaceKey();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('encryptCalendarEvents', () => {
|
|
114
|
+
it('should encrypt multiple events', () => {
|
|
115
|
+
const events = [
|
|
116
|
+
{ title: 'Event 1', description: 'Desc 1', location: 'Loc 1' },
|
|
117
|
+
{ title: 'Event 2', description: 'Desc 2', location: undefined },
|
|
118
|
+
{ title: 'Event 3', description: '', location: 'Loc 3' },
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const encrypted = encryptCalendarEvents(events, workspaceKey);
|
|
122
|
+
|
|
123
|
+
expect(encrypted).toHaveLength(3);
|
|
124
|
+
encrypted.forEach((event, index) => {
|
|
125
|
+
expect(event.is_encrypted).toBe(true);
|
|
126
|
+
// Non-empty fields should be encrypted (different from original)
|
|
127
|
+
if (events[index]!.title) {
|
|
128
|
+
expect(event.title).not.toBe(events[index]!.title);
|
|
129
|
+
}
|
|
130
|
+
if (events[index]!.description) {
|
|
131
|
+
expect(event.description).not.toBe(events[index]!.description);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should handle empty array', () => {
|
|
137
|
+
const encrypted = encryptCalendarEvents([], workspaceKey);
|
|
138
|
+
expect(encrypted).toEqual([]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should preserve additional event properties', () => {
|
|
142
|
+
const events = [
|
|
143
|
+
{
|
|
144
|
+
title: 'Event',
|
|
145
|
+
description: 'Desc',
|
|
146
|
+
location: 'Loc',
|
|
147
|
+
start_at: '2024-01-01T10:00:00Z',
|
|
148
|
+
end_at: '2024-01-01T11:00:00Z',
|
|
149
|
+
color: 'blue',
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
const encrypted = encryptCalendarEvents(events, workspaceKey);
|
|
154
|
+
|
|
155
|
+
expect(encrypted[0]!.start_at).toBe('2024-01-01T10:00:00Z');
|
|
156
|
+
expect(encrypted[0]!.end_at).toBe('2024-01-01T11:00:00Z');
|
|
157
|
+
expect(encrypted[0]!.color).toBe('blue');
|
|
158
|
+
expect(encrypted[0]!.is_encrypted).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('decryptCalendarEvents', () => {
|
|
163
|
+
it('should decrypt multiple encrypted events', () => {
|
|
164
|
+
const originalEvents = [
|
|
165
|
+
{ title: 'Event 1', description: 'Desc 1', location: 'Loc 1' },
|
|
166
|
+
{ title: 'Event 2', description: 'Desc 2', location: 'Loc 2' },
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
const encrypted = encryptCalendarEvents(originalEvents, workspaceKey);
|
|
170
|
+
|
|
171
|
+
// Add required fields for CalendarEventWithEncryption
|
|
172
|
+
const eventsWithMetadata = encrypted.map((event, index) => ({
|
|
173
|
+
...event,
|
|
174
|
+
id: `event-${index}`,
|
|
175
|
+
start_at: '2024-01-01T10:00:00Z',
|
|
176
|
+
end_at: '2024-01-01T11:00:00Z',
|
|
177
|
+
color: 'blue' as const,
|
|
178
|
+
ws_id: 'ws-123',
|
|
179
|
+
}));
|
|
180
|
+
|
|
181
|
+
const decrypted = decryptCalendarEvents(
|
|
182
|
+
eventsWithMetadata,
|
|
183
|
+
workspaceKey
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
expect(decrypted).toHaveLength(2);
|
|
187
|
+
decrypted.forEach((event, index) => {
|
|
188
|
+
expect(event.title).toBe(originalEvents[index]!.title);
|
|
189
|
+
expect(event.description).toBe(originalEvents[index]!.description);
|
|
190
|
+
expect(event.location).toBe(originalEvents[index]!.location);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should set is_encrypted to false after successful decryption', () => {
|
|
195
|
+
// This test verifies that after decryption, is_encrypted is set to false
|
|
196
|
+
// so that filtering logic can correctly identify decrypted vs failed events
|
|
197
|
+
const originalEvents = [
|
|
198
|
+
{ title: 'Event 1', description: 'Desc 1', location: 'Loc 1' },
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
const encrypted = encryptCalendarEvents(originalEvents, workspaceKey);
|
|
202
|
+
|
|
203
|
+
// Verify encrypted event has is_encrypted: true
|
|
204
|
+
expect(encrypted[0]!.is_encrypted).toBe(true);
|
|
205
|
+
|
|
206
|
+
const eventsWithMetadata = encrypted.map((event, index) => ({
|
|
207
|
+
...event,
|
|
208
|
+
id: `event-${index}`,
|
|
209
|
+
start_at: '2024-01-01T10:00:00Z',
|
|
210
|
+
end_at: '2024-01-01T11:00:00Z',
|
|
211
|
+
color: 'blue' as const,
|
|
212
|
+
ws_id: 'ws-123',
|
|
213
|
+
}));
|
|
214
|
+
|
|
215
|
+
const decrypted = decryptCalendarEvents(
|
|
216
|
+
eventsWithMetadata,
|
|
217
|
+
workspaceKey
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// After successful decryption, is_encrypted should be false
|
|
221
|
+
expect(decrypted[0]!.is_encrypted).toBe(false);
|
|
222
|
+
expect(decrypted[0]!.title).toBe('Event 1');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should skip non-encrypted events', () => {
|
|
226
|
+
const events = [
|
|
227
|
+
{
|
|
228
|
+
id: 'event-1',
|
|
229
|
+
title: 'Plaintext Event',
|
|
230
|
+
description: 'Plaintext Desc',
|
|
231
|
+
location: 'Plaintext Loc',
|
|
232
|
+
start_at: '2024-01-01T10:00:00Z',
|
|
233
|
+
end_at: '2024-01-01T11:00:00Z',
|
|
234
|
+
color: 'blue' as const,
|
|
235
|
+
ws_id: 'ws-123',
|
|
236
|
+
is_encrypted: false,
|
|
237
|
+
},
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
const result = decryptCalendarEvents(events, workspaceKey);
|
|
241
|
+
|
|
242
|
+
expect(result[0]!.title).toBe('Plaintext Event');
|
|
243
|
+
expect(result[0]!.description).toBe('Plaintext Desc');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should handle mixed encrypted and non-encrypted events', () => {
|
|
247
|
+
const encryptedEvent = encryptCalendarEventFields(
|
|
248
|
+
{ title: 'Encrypted', description: 'Secret', location: 'Hidden' },
|
|
249
|
+
workspaceKey
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const events = [
|
|
253
|
+
{
|
|
254
|
+
id: 'event-1',
|
|
255
|
+
...encryptedEvent,
|
|
256
|
+
location: encryptedEvent.location ?? null,
|
|
257
|
+
start_at: '2024-01-01T10:00:00Z',
|
|
258
|
+
end_at: '2024-01-01T11:00:00Z',
|
|
259
|
+
color: 'blue' as const,
|
|
260
|
+
ws_id: 'ws-123',
|
|
261
|
+
is_encrypted: true,
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
id: 'event-2',
|
|
265
|
+
title: 'Public',
|
|
266
|
+
description: 'Visible',
|
|
267
|
+
location: 'Open' as string | null,
|
|
268
|
+
start_at: '2024-01-01T12:00:00Z',
|
|
269
|
+
end_at: '2024-01-01T13:00:00Z',
|
|
270
|
+
color: 'red' as const,
|
|
271
|
+
ws_id: 'ws-123',
|
|
272
|
+
is_encrypted: false,
|
|
273
|
+
},
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
const decrypted = decryptCalendarEvents(events, workspaceKey);
|
|
277
|
+
|
|
278
|
+
expect(decrypted[0]!.title).toBe('Encrypted');
|
|
279
|
+
expect(decrypted[1]!.title).toBe('Public');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should handle empty array', () => {
|
|
283
|
+
const result = decryptCalendarEvents([], workspaceKey);
|
|
284
|
+
expect(result).toEqual([]);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should handle events with null fields and return normalized values', () => {
|
|
288
|
+
const events = [
|
|
289
|
+
{
|
|
290
|
+
id: 'event-1',
|
|
291
|
+
title: null as unknown as string,
|
|
292
|
+
description: null as unknown as string,
|
|
293
|
+
location: null,
|
|
294
|
+
start_at: '2024-01-01T10:00:00Z',
|
|
295
|
+
end_at: '2024-01-01T11:00:00Z',
|
|
296
|
+
color: 'blue' as const,
|
|
297
|
+
ws_id: 'ws-123',
|
|
298
|
+
is_encrypted: true, // Marked as encrypted but with null values
|
|
299
|
+
},
|
|
300
|
+
];
|
|
301
|
+
|
|
302
|
+
// Call decryptCalendarEvents and capture result
|
|
303
|
+
const result = decryptCalendarEvents(
|
|
304
|
+
events as unknown as CalendarEventWithEncryption[],
|
|
305
|
+
workspaceKey
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
// Verify the result is an array with one event
|
|
309
|
+
expect(result).toHaveLength(1);
|
|
310
|
+
|
|
311
|
+
const decryptedEvent = result[0]!;
|
|
312
|
+
|
|
313
|
+
// Non-sensitive fields should remain unchanged
|
|
314
|
+
expect(decryptedEvent.id).toBe('event-1');
|
|
315
|
+
expect(decryptedEvent.start_at).toBe('2024-01-01T10:00:00Z');
|
|
316
|
+
expect(decryptedEvent.end_at).toBe('2024-01-01T11:00:00Z');
|
|
317
|
+
expect(decryptedEvent.color).toBe('blue');
|
|
318
|
+
expect(decryptedEvent.ws_id).toBe('ws-123');
|
|
319
|
+
|
|
320
|
+
// Null fields get normalized to empty strings by decryptField
|
|
321
|
+
expect(decryptedEvent.title).toBe('');
|
|
322
|
+
expect(decryptedEvent.description).toBe('');
|
|
323
|
+
expect(decryptedEvent.location).toBeUndefined(); // null location becomes undefined
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe('negative tests and degradations', () => {
|
|
327
|
+
it('should return original ciphertext when decryption fails due to incorrect key', () => {
|
|
328
|
+
const events = [
|
|
329
|
+
{ title: 'Secret', description: 'Desc', location: 'Loc' },
|
|
330
|
+
];
|
|
331
|
+
const encrypted = encryptCalendarEvents(events, workspaceKey);
|
|
332
|
+
|
|
333
|
+
// Use a different key
|
|
334
|
+
const wrongKey = generateWorkspaceKey();
|
|
335
|
+
|
|
336
|
+
// Add metadata
|
|
337
|
+
const eventsWithMetadata = encrypted.map((e) => ({
|
|
338
|
+
...e,
|
|
339
|
+
id: '1',
|
|
340
|
+
ws_id: 'ws',
|
|
341
|
+
start_at: '2024-01-01T10:00:00Z',
|
|
342
|
+
end_at: '2024-01-01T11:00:00Z',
|
|
343
|
+
color: 'blue' as const,
|
|
344
|
+
}));
|
|
345
|
+
|
|
346
|
+
const result = decryptCalendarEvents(eventsWithMetadata, wrongKey);
|
|
347
|
+
|
|
348
|
+
// Should contain ciphertext (not decrypted, not empty)
|
|
349
|
+
// The service returns the original ciphertext on failure check
|
|
350
|
+
expect(result[0]!.title).toBe(encrypted[0]!.title);
|
|
351
|
+
expect(result[0]!.title).not.toBe('Secret');
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('should handle corrupted encrypted fields gracefully', () => {
|
|
355
|
+
const events = [{ title: 'Secret', description: 'Desc' }];
|
|
356
|
+
const encrypted = encryptCalendarEvents(events, workspaceKey);
|
|
357
|
+
|
|
358
|
+
// Corrupt the title
|
|
359
|
+
// We modify the middle of the string to invalidate the auth tag or ciphertext
|
|
360
|
+
const originalTitleCipher = encrypted[0]!.title!;
|
|
361
|
+
// Ensure we have enough length to splice. Base64 is usually long enough.
|
|
362
|
+
const corruptedTitle =
|
|
363
|
+
originalTitleCipher.substring(0, 10) +
|
|
364
|
+
(originalTitleCipher.charAt(10) === 'A' ? 'B' : 'A') +
|
|
365
|
+
originalTitleCipher.substring(11);
|
|
366
|
+
|
|
367
|
+
const eventsWithMetadata = [
|
|
368
|
+
{
|
|
369
|
+
...encrypted[0],
|
|
370
|
+
title: corruptedTitle,
|
|
371
|
+
id: '1',
|
|
372
|
+
ws_id: 'ws',
|
|
373
|
+
start_at: '2024-01-01T10:00:00Z',
|
|
374
|
+
end_at: '2024-01-01T11:00:00Z',
|
|
375
|
+
color: 'blue' as const,
|
|
376
|
+
location: undefined,
|
|
377
|
+
},
|
|
378
|
+
];
|
|
379
|
+
|
|
380
|
+
const result = decryptCalendarEvents(
|
|
381
|
+
eventsWithMetadata as unknown as CalendarEventWithEncryption[],
|
|
382
|
+
workspaceKey
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
// Should return the corrupted string (fallback)
|
|
386
|
+
expect(result[0]!.title).toBe(corruptedTitle);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('should correct invalid input formats even if marked encrypted', () => {
|
|
390
|
+
const events = [
|
|
391
|
+
{
|
|
392
|
+
id: '1',
|
|
393
|
+
ws_id: 'ws',
|
|
394
|
+
start_at: '2024-01-01T10:00:00Z',
|
|
395
|
+
end_at: '2024-01-01T11:00:00Z',
|
|
396
|
+
color: 'blue' as const,
|
|
397
|
+
title: 'too-short', // Short string, not valid encrypted format
|
|
398
|
+
description: 'not-base-64-!!', // Likely fails invalid length or decode
|
|
399
|
+
location: null,
|
|
400
|
+
is_encrypted: true,
|
|
401
|
+
} as unknown as CalendarEventWithEncryption,
|
|
402
|
+
];
|
|
403
|
+
|
|
404
|
+
const result = decryptCalendarEvents(events, workspaceKey);
|
|
405
|
+
// Short string returns as is because length check fails
|
|
406
|
+
expect(result[0]!.title).toBe('too-short');
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
encryptField,
|
|
4
|
+
getMasterKey,
|
|
5
|
+
isEncryptionEnabled,
|
|
6
|
+
} from '../encryption-service';
|
|
7
|
+
import {
|
|
8
|
+
generateWorkspaceKey,
|
|
9
|
+
setupEncryptionEnv,
|
|
10
|
+
TEST_MASTER_KEY,
|
|
11
|
+
} from './test-helpers';
|
|
12
|
+
|
|
13
|
+
describe('encryption-service', () => {
|
|
14
|
+
setupEncryptionEnv();
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Environment and Configuration Tests
|
|
18
|
+
// ============================================================================
|
|
19
|
+
describe('isEncryptionEnabled', () => {
|
|
20
|
+
it('should return false when master key is not set', () => {
|
|
21
|
+
delete process.env.ENCRYPTION_MASTER_KEY;
|
|
22
|
+
expect(isEncryptionEnabled()).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should return true when master key is set', () => {
|
|
26
|
+
process.env.ENCRYPTION_MASTER_KEY = TEST_MASTER_KEY;
|
|
27
|
+
expect(isEncryptionEnabled()).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should return false for empty string', () => {
|
|
31
|
+
process.env.ENCRYPTION_MASTER_KEY = '';
|
|
32
|
+
expect(isEncryptionEnabled()).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should return false for whitespace-only key', () => {
|
|
36
|
+
process.env.ENCRYPTION_MASTER_KEY = ' ';
|
|
37
|
+
expect(isEncryptionEnabled()).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('getMasterKey', () => {
|
|
42
|
+
it('should return master key when set', () => {
|
|
43
|
+
process.env.ENCRYPTION_MASTER_KEY = TEST_MASTER_KEY;
|
|
44
|
+
expect(getMasterKey()).toBe(TEST_MASTER_KEY);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should throw when master key is not set', () => {
|
|
48
|
+
delete process.env.ENCRYPTION_MASTER_KEY;
|
|
49
|
+
expect(() => getMasterKey()).toThrow(
|
|
50
|
+
'ENCRYPTION_MASTER_KEY environment variable is not configured'
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should throw for empty string', () => {
|
|
55
|
+
process.env.ENCRYPTION_MASTER_KEY = '';
|
|
56
|
+
expect(() => getMasterKey()).toThrow(
|
|
57
|
+
'ENCRYPTION_MASTER_KEY environment variable is not configured'
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ============================================================================
|
|
63
|
+
// Ciphertext Format and Security Tests
|
|
64
|
+
// ============================================================================
|
|
65
|
+
describe('ciphertext format validation', () => {
|
|
66
|
+
let workspaceKey: Buffer;
|
|
67
|
+
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
workspaceKey = generateWorkspaceKey();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should produce base64-encoded ciphertext', () => {
|
|
73
|
+
const ciphertext = encryptField('test', workspaceKey);
|
|
74
|
+
|
|
75
|
+
// Valid base64 pattern
|
|
76
|
+
expect(ciphertext).toMatch(/^[A-Za-z0-9+/]+=*$/);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should produce ciphertext with minimum length', () => {
|
|
80
|
+
const ciphertext = encryptField('x', workspaceKey);
|
|
81
|
+
const decoded = Buffer.from(ciphertext, 'base64');
|
|
82
|
+
|
|
83
|
+
// Minimum: 12 (IV) + 1 (ciphertext for 'x') + 16 (auth tag)
|
|
84
|
+
expect(decoded.length).toBeGreaterThanOrEqual(29);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('ciphertext length should grow with plaintext length', () => {
|
|
88
|
+
const short = encryptField('a', workspaceKey);
|
|
89
|
+
const medium = encryptField('a'.repeat(100), workspaceKey);
|
|
90
|
+
const long = encryptField('a'.repeat(1000), workspaceKey);
|
|
91
|
+
|
|
92
|
+
const shortLen = Buffer.from(short, 'base64').length;
|
|
93
|
+
const mediumLen = Buffer.from(medium, 'base64').length;
|
|
94
|
+
const longLen = Buffer.from(long, 'base64').length;
|
|
95
|
+
|
|
96
|
+
expect(mediumLen).toBeGreaterThan(shortLen);
|
|
97
|
+
expect(longLen).toBeGreaterThan(mediumLen);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should not leak plaintext in ciphertext', () => {
|
|
101
|
+
const plaintext = 'TOP SECRET MESSAGE';
|
|
102
|
+
const ciphertext = encryptField(plaintext, workspaceKey);
|
|
103
|
+
|
|
104
|
+
// Plaintext should not appear in ciphertext
|
|
105
|
+
expect(ciphertext).not.toContain('TOP');
|
|
106
|
+
expect(ciphertext).not.toContain('SECRET');
|
|
107
|
+
expect(ciphertext).not.toContain('MESSAGE');
|
|
108
|
+
|
|
109
|
+
// Also check the raw bytes
|
|
110
|
+
const decoded = Buffer.from(ciphertext, 'base64');
|
|
111
|
+
expect(decoded.toString('utf8')).not.toContain('TOP');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|