@tuturuuu/utils 0.0.3 → 0.6.1

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 +313 -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 +120 -1
  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,375 @@
1
+ import type { JSONContent } from '@tiptap/core';
2
+
3
+ /** Node types that have unique identifiers beyond text content */
4
+ export type IdentifiableNodeType =
5
+ | 'image'
6
+ | 'imageResize'
7
+ | 'video'
8
+ | 'youtube'
9
+ | 'mention';
10
+
11
+ /** Attribute change details */
12
+ export interface AttributeChange {
13
+ key: string;
14
+ oldValue: unknown;
15
+ newValue: unknown;
16
+ }
17
+
18
+ /** Result of comparing two nodes */
19
+ export interface NodeDiffResult {
20
+ type: 'added' | 'removed' | 'modified';
21
+ nodeType: string;
22
+ path: number[];
23
+ oldNode?: JSONContent;
24
+ newNode?: JSONContent;
25
+ attributeChanges?: AttributeChange[];
26
+ /** Human-readable display label */
27
+ displayLabel: string;
28
+ }
29
+
30
+ /** Summary of node-level changes */
31
+ export interface NodeDiffSummary {
32
+ totalChanges: number;
33
+ added: NodeDiffResult[];
34
+ removed: NodeDiffResult[];
35
+ modified: NodeDiffResult[];
36
+ /** Quick flag for images/videos/embeds changes */
37
+ hasMediaChanges: boolean;
38
+ }
39
+
40
+ /** Node with its path in the tree */
41
+ interface FlattenedNode {
42
+ node: JSONContent;
43
+ path: number[];
44
+ identifier: string;
45
+ }
46
+
47
+ /**
48
+ * Check if a node type is identifiable (has unique identifier beyond text)
49
+ */
50
+ function isIdentifiableNodeType(type: string): type is IdentifiableNodeType {
51
+ return ['image', 'imageResize', 'video', 'youtube', 'mention'].includes(type);
52
+ }
53
+
54
+ /**
55
+ * Check if a node type is a media type
56
+ */
57
+ function isMediaNodeType(type: string): boolean {
58
+ return ['image', 'imageResize', 'video', 'youtube'].includes(type);
59
+ }
60
+
61
+ /**
62
+ * Extract a unique identifier for a node.
63
+ * For images: use src URL
64
+ * For mentions: use id attribute
65
+ * For other identifiable nodes: use appropriate unique attribute
66
+ */
67
+ export function getNodeIdentifier(node: JSONContent): string {
68
+ const type = node.type || 'unknown';
69
+
70
+ switch (type) {
71
+ case 'image':
72
+ case 'imageResize':
73
+ // Use src URL as unique identifier for images
74
+ return `${type}:${node.attrs?.src || 'unknown'}`;
75
+
76
+ case 'video':
77
+ return `video:${node.attrs?.src || 'unknown'}`;
78
+
79
+ case 'youtube':
80
+ // YouTube embeds may use src or videoId
81
+ return `youtube:${node.attrs?.src || node.attrs?.videoId || 'unknown'}`;
82
+
83
+ case 'mention':
84
+ // Mentions have unique user/entity IDs
85
+ return `mention:${node.attrs?.id || 'unknown'}`;
86
+
87
+ default:
88
+ // For non-identifiable nodes, return type only (not tracked individually)
89
+ return type;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Extract filename from a URL or path
95
+ */
96
+ function extractFilename(urlOrPath: string): string {
97
+ try {
98
+ // Try to parse as URL first
99
+ const url = new URL(urlOrPath);
100
+ const pathname = url.pathname;
101
+ const filename = pathname.split('/').pop() || pathname;
102
+ // Decode and truncate if too long
103
+ const decoded = decodeURIComponent(filename);
104
+ return decoded.length > 40 ? `${decoded.slice(0, 37)}...` : decoded;
105
+ } catch {
106
+ // Not a valid URL, treat as path
107
+ const filename = urlOrPath.split('/').pop() || urlOrPath;
108
+ return filename.length > 40 ? `${filename.slice(0, 37)}...` : filename;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Get a human-readable label for a node (for display in UI).
114
+ * e.g., "[Image: cat.png]" or "[Video: intro.mp4]"
115
+ */
116
+ export function getNodeDisplayLabel(node: JSONContent): string {
117
+ const type = node.type || 'unknown';
118
+
119
+ switch (type) {
120
+ case 'image':
121
+ case 'imageResize': {
122
+ const src = node.attrs?.src;
123
+ const alt = node.attrs?.alt;
124
+ if (src) {
125
+ const filename = extractFilename(src);
126
+ return alt ? `${alt} (${filename})` : filename;
127
+ }
128
+ return alt || 'Image';
129
+ }
130
+
131
+ case 'video': {
132
+ const src = node.attrs?.src;
133
+ if (src) {
134
+ return extractFilename(src);
135
+ }
136
+ return 'Video';
137
+ }
138
+
139
+ case 'youtube': {
140
+ const src = node.attrs?.src || node.attrs?.videoId;
141
+ if (src) {
142
+ // For YouTube, show video ID or title if available
143
+ const title = node.attrs?.title;
144
+ if (title) return title;
145
+ // Extract video ID from URL if possible
146
+ try {
147
+ const url = new URL(src);
148
+ const videoId =
149
+ url.searchParams.get('v') || url.pathname.split('/').pop();
150
+ return videoId ? `YouTube: ${videoId}` : 'YouTube Video';
151
+ } catch {
152
+ return `YouTube: ${src.slice(0, 20)}...`;
153
+ }
154
+ }
155
+ return 'YouTube Video';
156
+ }
157
+
158
+ case 'mention': {
159
+ const label = node.attrs?.label || node.attrs?.id || 'mention';
160
+ return `@${label}`;
161
+ }
162
+
163
+ default:
164
+ return type;
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Flatten a JSONContent tree into a list of identifiable nodes with paths.
170
+ * Only extracts nodes that have unique identifiers (images, videos, mentions).
171
+ */
172
+ export function flattenMediaNodes(
173
+ content: JSONContent | null | undefined,
174
+ path: number[] = []
175
+ ): FlattenedNode[] {
176
+ if (!content) return [];
177
+
178
+ const result: FlattenedNode[] = [];
179
+ const type = content.type || '';
180
+
181
+ // If this is an identifiable node, add it to results
182
+ if (isIdentifiableNodeType(type)) {
183
+ result.push({
184
+ node: content,
185
+ path: [...path],
186
+ identifier: getNodeIdentifier(content),
187
+ });
188
+ }
189
+
190
+ // Recursively process children
191
+ if (content.content && Array.isArray(content.content)) {
192
+ content.content.forEach((child, index) => {
193
+ result.push(...flattenMediaNodes(child, [...path, index]));
194
+ });
195
+ }
196
+
197
+ return result;
198
+ }
199
+
200
+ /**
201
+ * Compare attributes of two nodes and return list of changes
202
+ */
203
+ function compareAttributes(
204
+ oldNode: JSONContent,
205
+ newNode: JSONContent
206
+ ): AttributeChange[] {
207
+ const changes: AttributeChange[] = [];
208
+ const oldAttrs = oldNode.attrs || {};
209
+ const newAttrs = newNode.attrs || {};
210
+
211
+ // Get all unique keys from both
212
+ const allKeys = new Set([...Object.keys(oldAttrs), ...Object.keys(newAttrs)]);
213
+
214
+ for (const key of allKeys) {
215
+ const oldVal = oldAttrs[key];
216
+ const newVal = newAttrs[key];
217
+
218
+ // Skip src since it's used for identification
219
+ if (key === 'src') continue;
220
+
221
+ // Compare values (stringify for deep comparison)
222
+ if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
223
+ changes.push({
224
+ key,
225
+ oldValue: oldVal,
226
+ newValue: newVal,
227
+ });
228
+ }
229
+ }
230
+
231
+ return changes;
232
+ }
233
+
234
+ /**
235
+ * Parse JSON content from various input types
236
+ */
237
+ export function parseJsonContent(value: unknown): JSONContent | null {
238
+ if (!value) return null;
239
+
240
+ try {
241
+ if (typeof value === 'string') {
242
+ return JSON.parse(value) as JSONContent;
243
+ }
244
+ if (typeof value === 'object') {
245
+ return value as JSONContent;
246
+ }
247
+ return null;
248
+ } catch {
249
+ return null;
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Compare two JSONContent structures at the node level.
255
+ * Returns detailed diff information for each changed node.
256
+ *
257
+ * Algorithm:
258
+ * 1. Flatten both trees to extract identifiable nodes
259
+ * 2. Build maps by identifier
260
+ * 3. Find added (in new but not old), removed (in old but not new)
261
+ * 4. Find modified (same identifier, different attributes)
262
+ */
263
+ export function computeNodeDiff(
264
+ oldContent: JSONContent | null | undefined,
265
+ newContent: JSONContent | null | undefined
266
+ ): NodeDiffSummary {
267
+ // Flatten both trees
268
+ const oldNodes = flattenMediaNodes(oldContent);
269
+ const newNodes = flattenMediaNodes(newContent);
270
+
271
+ // Build maps by identifier
272
+ // Handle duplicates by appending index if identifier already exists
273
+ const oldByIdentifier = new Map<string, FlattenedNode>();
274
+ const oldIdentifierCounts = new Map<string, number>();
275
+ for (const node of oldNodes) {
276
+ const count = oldIdentifierCounts.get(node.identifier) || 0;
277
+ const uniqueId =
278
+ count > 0 ? `${node.identifier}#${count}` : node.identifier;
279
+ oldByIdentifier.set(uniqueId, { ...node, identifier: uniqueId });
280
+ oldIdentifierCounts.set(node.identifier, count + 1);
281
+ }
282
+
283
+ const newByIdentifier = new Map<string, FlattenedNode>();
284
+ const newIdentifierCounts = new Map<string, number>();
285
+ for (const node of newNodes) {
286
+ const count = newIdentifierCounts.get(node.identifier) || 0;
287
+ const uniqueId =
288
+ count > 0 ? `${node.identifier}#${count}` : node.identifier;
289
+ newByIdentifier.set(uniqueId, { ...node, identifier: uniqueId });
290
+ newIdentifierCounts.set(node.identifier, count + 1);
291
+ }
292
+
293
+ const added: NodeDiffResult[] = [];
294
+ const removed: NodeDiffResult[] = [];
295
+ const modified: NodeDiffResult[] = [];
296
+
297
+ // Find added nodes (in new but not in old)
298
+ for (const [id, newNode] of newByIdentifier) {
299
+ if (!oldByIdentifier.has(id)) {
300
+ added.push({
301
+ type: 'added',
302
+ nodeType: newNode.node.type || 'unknown',
303
+ path: newNode.path,
304
+ newNode: newNode.node,
305
+ displayLabel: getNodeDisplayLabel(newNode.node),
306
+ });
307
+ }
308
+ }
309
+
310
+ // Find removed nodes (in old but not in new)
311
+ for (const [id, oldNode] of oldByIdentifier) {
312
+ if (!newByIdentifier.has(id)) {
313
+ removed.push({
314
+ type: 'removed',
315
+ nodeType: oldNode.node.type || 'unknown',
316
+ path: oldNode.path,
317
+ oldNode: oldNode.node,
318
+ displayLabel: getNodeDisplayLabel(oldNode.node),
319
+ });
320
+ }
321
+ }
322
+
323
+ // Find modified nodes (same identifier, different attributes)
324
+ for (const [id, oldNode] of oldByIdentifier) {
325
+ const newNode = newByIdentifier.get(id);
326
+ if (newNode) {
327
+ const attrChanges = compareAttributes(oldNode.node, newNode.node);
328
+ if (attrChanges.length > 0) {
329
+ modified.push({
330
+ type: 'modified',
331
+ nodeType: oldNode.node.type || 'unknown',
332
+ path: newNode.path,
333
+ oldNode: oldNode.node,
334
+ newNode: newNode.node,
335
+ attributeChanges: attrChanges,
336
+ displayLabel: getNodeDisplayLabel(newNode.node),
337
+ });
338
+ }
339
+ }
340
+ }
341
+
342
+ // Check if any changes involve media nodes
343
+ const hasMediaChanges = [...added, ...removed, ...modified].some((diff) =>
344
+ isMediaNodeType(diff.nodeType)
345
+ );
346
+
347
+ return {
348
+ totalChanges: added.length + removed.length + modified.length,
349
+ added,
350
+ removed,
351
+ modified,
352
+ hasMediaChanges,
353
+ };
354
+ }
355
+
356
+ /**
357
+ * Get the image source URL from a node
358
+ */
359
+ export function getNodeImageSrc(node: JSONContent): string | null {
360
+ if (node.type === 'image' || node.type === 'imageResize') {
361
+ return node.attrs?.src || null;
362
+ }
363
+ return null;
364
+ }
365
+
366
+ /**
367
+ * Check if two JSONContent structures have any node-level differences
368
+ */
369
+ export function hasNodeDifferences(
370
+ oldContent: JSONContent | null | undefined,
371
+ newContent: JSONContent | null | undefined
372
+ ): boolean {
373
+ const diff = computeNodeDiff(oldContent, newContent);
374
+ return diff.totalChanges > 0;
375
+ }