@tuturuuu/utils 0.33.3 → 0.35.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 (54) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/package.json +6 -4
  3. package/src/__tests__/api-proxy-bearer-trust.test.ts +3 -0
  4. package/src/__tests__/api-proxy-guard.test.ts +6 -13
  5. package/src/__tests__/api-proxy-offline-response.test.ts +48 -0
  6. package/src/__tests__/api-proxy-test-request.ts +19 -0
  7. package/src/__tests__/calendar-provider-readonly.test.ts +45 -0
  8. package/src/__tests__/calendar-recurrence-edit.test.ts +140 -0
  9. package/src/__tests__/calendar-recurrence.test.ts +479 -0
  10. package/src/__tests__/daily-operation-download-isolation.test.ts +137 -0
  11. package/src/__tests__/offline-download-guard.test.ts +340 -0
  12. package/src/__tests__/permissions.test.tsx +21 -0
  13. package/src/__tests__/request-workspace.test.ts +63 -0
  14. package/src/__tests__/tutoring-teacher-hours.test.ts +274 -0
  15. package/src/__tests__/username-policy.test.ts +70 -0
  16. package/src/account-benefits-server.ts +43 -0
  17. package/src/account-benefits.ts +45 -0
  18. package/src/api-proxy-guard.ts +8 -42
  19. package/src/api-proxy-read-policy.ts +43 -0
  20. package/src/calendar-provider-readonly.ts +49 -0
  21. package/src/calendar-recurrence-edit.ts +148 -0
  22. package/src/calendar-recurrence-expansion.ts +131 -0
  23. package/src/calendar-recurrence-local.ts +45 -0
  24. package/src/calendar-recurrence-pattern.ts +54 -0
  25. package/src/calendar-recurrence-providers.ts +361 -0
  26. package/src/calendar-recurrence-validation.ts +180 -0
  27. package/src/calendar-recurrence.ts +5 -0
  28. package/src/calendar-utils.ts +41 -3
  29. package/src/common/metadata.test.ts +0 -6
  30. package/src/constants.ts +3 -0
  31. package/src/deployment-vault-permissions.tsx +26 -0
  32. package/src/desktop-signing-profiles.ts +20 -0
  33. package/src/google-calendar-import-fence.test.ts +182 -0
  34. package/src/google-calendar-import-fence.ts +166 -0
  35. package/src/next-config.ts +8 -1
  36. package/src/offline-download-guard.ts +160 -0
  37. package/src/permissions.tsx +2 -10
  38. package/src/platform-release.test.ts +1 -1
  39. package/src/platform-release.ts +1 -1
  40. package/src/playground-schema.test.ts +119 -0
  41. package/src/playground-schema.ts +173 -0
  42. package/src/playground-templates.ts +65 -0
  43. package/src/release-note-policy-core.cjs +28 -0
  44. package/src/release-note-policy-core.d.cts +5 -0
  45. package/src/release-note-policy.json +9 -0
  46. package/src/release-note-policy.ts +4 -0
  47. package/src/request-workspace.ts +29 -0
  48. package/src/seo-policy.json +196 -0
  49. package/src/seo.test.ts +168 -0
  50. package/src/seo.ts +49 -0
  51. package/src/tutoring-teacher-hours.ts +220 -0
  52. package/src/username-policy.json +87 -0
  53. package/src/username-policy.ts +18 -0
  54. package/vitest.config.ts +7 -0
@@ -0,0 +1,361 @@
1
+ import { Temporal } from '@js-temporal/polyfill';
2
+ import type {
3
+ CalendarRecurrenceAnchor,
4
+ CalendarRecurrenceRule,
5
+ CalendarWeekday,
6
+ } from '@tuturuuu/types/primitives/calendar-recurrence';
7
+ import { CALENDAR_WEEKDAYS } from './calendar-recurrence-pattern';
8
+ import {
9
+ CalendarRecurrenceRuleSchema,
10
+ validateCalendarRecurrence,
11
+ } from './calendar-recurrence-validation';
12
+
13
+ const graphDays = [
14
+ 'monday',
15
+ 'tuesday',
16
+ 'wednesday',
17
+ 'thursday',
18
+ 'friday',
19
+ 'saturday',
20
+ 'sunday',
21
+ ] as const;
22
+ const indexes = ['first', 'second', 'third', 'fourth', 'last'] as const;
23
+ export class UnsupportedCalendarRecurrenceError extends Error {
24
+ constructor(message: string) {
25
+ super(message);
26
+ this.name = 'UnsupportedCalendarRecurrenceError';
27
+ }
28
+ }
29
+ export interface GraphCalendarRecurrence {
30
+ pattern: {
31
+ type: string;
32
+ interval: number;
33
+ dayOfMonth?: number;
34
+ daysOfWeek?: string[];
35
+ firstDayOfWeek?: string;
36
+ index?: string;
37
+ month?: number;
38
+ };
39
+ range: {
40
+ type: string;
41
+ startDate: string;
42
+ recurrenceTimeZone?: string;
43
+ endDate?: string;
44
+ numberOfOccurrences?: number;
45
+ };
46
+ }
47
+ function graphDay(day: CalendarWeekday) {
48
+ return graphDays[CALENDAR_WEEKDAYS.indexOf(day)]!;
49
+ }
50
+ function canonicalDay(day: string): CalendarWeekday {
51
+ const index = graphDays.indexOf(day as (typeof graphDays)[number]);
52
+ if (index < 0)
53
+ throw new UnsupportedCalendarRecurrenceError('Unknown provider weekday');
54
+ return CALENDAR_WEEKDAYS[index]!;
55
+ }
56
+
57
+ export function toGraphCalendarRecurrence(
58
+ rule: CalendarRecurrenceRule,
59
+ anchor: CalendarRecurrenceAnchor
60
+ ): GraphCalendarRecurrence {
61
+ validateCalendarRecurrence(rule, anchor);
62
+ if (
63
+ rule.monthDay &&
64
+ rule.monthDay > 28 &&
65
+ rule.monthDayOverflow !== 'last-day'
66
+ )
67
+ throw new UnsupportedCalendarRecurrenceError(
68
+ 'Outlook clamps missing month days; choose last-day overflow explicitly'
69
+ );
70
+ const relative = rule.weekIndex !== undefined;
71
+ const pattern: GraphCalendarRecurrence['pattern'] = {
72
+ type:
73
+ rule.frequency === 'monthly'
74
+ ? relative
75
+ ? 'relativeMonthly'
76
+ : 'absoluteMonthly'
77
+ : rule.frequency === 'yearly'
78
+ ? relative
79
+ ? 'relativeYearly'
80
+ : 'absoluteYearly'
81
+ : rule.frequency,
82
+ interval: rule.interval,
83
+ ...(rule.monthDay === undefined ? {} : { dayOfMonth: rule.monthDay }),
84
+ ...(rule.month === undefined ? {} : { month: rule.month }),
85
+ ...(rule.weekdays ? { daysOfWeek: rule.weekdays.map(graphDay) } : {}),
86
+ ...(rule.frequency === 'weekly'
87
+ ? { firstDayOfWeek: graphDay(rule.weekStartsOn ?? 'MO') }
88
+ : {}),
89
+ ...(relative
90
+ ? { index: indexes[rule.weekIndex === -1 ? 4 : rule.weekIndex! - 1] }
91
+ : {}),
92
+ };
93
+ return {
94
+ pattern,
95
+ range: {
96
+ type:
97
+ rule.end.type === 'count'
98
+ ? 'numbered'
99
+ : rule.end.type === 'until'
100
+ ? 'endDate'
101
+ : 'noEnd',
102
+ startDate: anchor.startLocal.slice(0, 10),
103
+ recurrenceTimeZone: rule.timeZone,
104
+ ...(rule.end.type === 'count'
105
+ ? { numberOfOccurrences: rule.end.count }
106
+ : {}),
107
+ ...(rule.end.type === 'until' ? { endDate: rule.end.date } : {}),
108
+ },
109
+ };
110
+ }
111
+
112
+ export function fromGraphCalendarRecurrence(
113
+ value: GraphCalendarRecurrence,
114
+ timeZone: string
115
+ ): CalendarRecurrenceRule {
116
+ const { pattern, range } = value;
117
+ const frequencies: Record<string, CalendarRecurrenceRule['frequency']> = {
118
+ daily: 'daily',
119
+ weekly: 'weekly',
120
+ absoluteMonthly: 'monthly',
121
+ relativeMonthly: 'monthly',
122
+ absoluteYearly: 'yearly',
123
+ relativeYearly: 'yearly',
124
+ };
125
+ const frequency = frequencies[pattern.type];
126
+ if (!frequency)
127
+ throw new UnsupportedCalendarRecurrenceError(
128
+ 'Unknown provider recurrence pattern'
129
+ );
130
+ const relative = pattern.type.startsWith('relative');
131
+ const index = indexes.indexOf(
132
+ (pattern.index ?? 'first') as (typeof indexes)[number]
133
+ );
134
+ if (relative && index < 0)
135
+ throw new UnsupportedCalendarRecurrenceError(
136
+ 'Unknown provider recurrence index'
137
+ );
138
+ if (!['numbered', 'endDate', 'noEnd'].includes(range.type))
139
+ throw new UnsupportedCalendarRecurrenceError(
140
+ 'Unknown provider recurrence range'
141
+ );
142
+ return CalendarRecurrenceRuleSchema.parse({
143
+ version: 1,
144
+ frequency,
145
+ interval: pattern.interval,
146
+ timeZone: range.recurrenceTimeZone || timeZone,
147
+ ...(frequency === 'weekly' || relative
148
+ ? { weekdays: pattern.daysOfWeek?.map(canonicalDay) }
149
+ : {}),
150
+ ...(frequency === 'weekly'
151
+ ? { weekStartsOn: canonicalDay(pattern.firstDayOfWeek ?? 'sunday') }
152
+ : {}),
153
+ ...(relative ? { weekIndex: index === 4 ? -1 : index + 1 } : {}),
154
+ ...(!relative && (frequency === 'monthly' || frequency === 'yearly')
155
+ ? {
156
+ monthDay: pattern.dayOfMonth,
157
+ ...(pattern.dayOfMonth && pattern.dayOfMonth > 28
158
+ ? { monthDayOverflow: 'last-day' }
159
+ : {}),
160
+ }
161
+ : {}),
162
+ ...(frequency === 'yearly' ? { month: pattern.month } : {}),
163
+ end:
164
+ range.type === 'numbered'
165
+ ? { type: 'count', count: range.numberOfOccurrences }
166
+ : range.type === 'endDate'
167
+ ? { type: 'until', date: range.endDate }
168
+ : { type: 'never' },
169
+ });
170
+ }
171
+
172
+ export function toGoogleCalendarRecurrence(
173
+ rule: CalendarRecurrenceRule,
174
+ anchor: CalendarRecurrenceAnchor
175
+ ): string[] {
176
+ validateCalendarRecurrence(rule, anchor);
177
+ const parts = [
178
+ `FREQ=${rule.frequency.toUpperCase()}`,
179
+ `INTERVAL=${rule.interval}`,
180
+ ];
181
+ if (rule.frequency === 'weekly')
182
+ parts.push(`WKST=${rule.weekStartsOn ?? 'MO'}`);
183
+ if (rule.weekdays) {
184
+ parts.push(
185
+ `BYDAY=${rule.weekdays.map((day) => `${rule.weekIndex ?? ''}${day}`).join(',')}`
186
+ );
187
+ if (rule.weekIndex !== undefined && rule.weekdays.length > 1)
188
+ parts.push('BYSETPOS=1');
189
+ }
190
+ if (rule.monthDay !== undefined) {
191
+ if (rule.monthDayOverflow === 'last-day' && rule.monthDay > 28) {
192
+ parts.push(
193
+ `BYMONTHDAY=${Array.from({ length: rule.monthDay - 27 }, (_, index) => index + 28).join(',')}`,
194
+ 'BYSETPOS=-1'
195
+ );
196
+ } else parts.push(`BYMONTHDAY=${rule.monthDay}`);
197
+ }
198
+ if (rule.month !== undefined) parts.push(`BYMONTH=${rule.month}`);
199
+ if (rule.end.type === 'count') parts.push(`COUNT=${rule.end.count}`);
200
+ if (rule.end.type === 'until') {
201
+ const until = anchor.allDay
202
+ ? rule.end.date.replaceAll('-', '')
203
+ : Temporal.PlainDate.from(rule.end.date)
204
+ .toPlainDateTime('23:59:59')
205
+ .toZonedDateTime(rule.timeZone)
206
+ .toInstant()
207
+ .toString()
208
+ .replaceAll('-', '')
209
+ .replaceAll(':', '');
210
+ parts.push(`UNTIL=${until}`);
211
+ }
212
+ return [`RRULE:${parts.join(';')}`];
213
+ }
214
+
215
+ /** Richer Google RRULE/RDATE/EXDATE data must be retained read-only, never dropped. */
216
+ export function fromGoogleCalendarRecurrence(
217
+ value: string[],
218
+ anchor: CalendarRecurrenceAnchor,
219
+ timeZone: string
220
+ ): CalendarRecurrenceRule {
221
+ if (value.length !== 1 || !value[0]?.startsWith('RRULE:'))
222
+ throw new UnsupportedCalendarRecurrenceError(
223
+ 'Only one common RRULE is editable; retain original provider recurrence'
224
+ );
225
+ const entries = value[0]
226
+ .slice(6)
227
+ .split(';')
228
+ .map((part) => part.split('='));
229
+ if (
230
+ entries.some((parts) => parts.length !== 2) ||
231
+ new Set(entries.map(([key]) => key)).size !== entries.length
232
+ )
233
+ throw new UnsupportedCalendarRecurrenceError(
234
+ 'Malformed or duplicate RRULE fields'
235
+ );
236
+ const fields = Object.fromEntries(entries) as Record<string, string>;
237
+ if (
238
+ Object.keys(fields).some(
239
+ (key) =>
240
+ ![
241
+ 'FREQ',
242
+ 'INTERVAL',
243
+ 'WKST',
244
+ 'BYDAY',
245
+ 'BYSETPOS',
246
+ 'BYMONTHDAY',
247
+ 'BYMONTH',
248
+ 'COUNT',
249
+ 'UNTIL',
250
+ ].includes(key)
251
+ )
252
+ )
253
+ throw new UnsupportedCalendarRecurrenceError('Unsupported RRULE selectors');
254
+ if (fields.COUNT && fields.UNTIL)
255
+ throw new UnsupportedCalendarRecurrenceError(
256
+ 'COUNT and UNTIL are mutually exclusive'
257
+ );
258
+ const frequency = fields.FREQ?.toLowerCase();
259
+ const byday = fields.BYDAY?.split(',').map((value) => {
260
+ const match = /^(-1|[1-4])?(MO|TU|WE|TH|FR|SA|SU)$/.exec(value);
261
+ if (!match)
262
+ throw new UnsupportedCalendarRecurrenceError('Unsupported BYDAY');
263
+ return {
264
+ day: match[2] as CalendarWeekday,
265
+ index: match[1] ? Number(match[1]) : undefined,
266
+ };
267
+ });
268
+ const index = byday?.[0]?.index;
269
+ if (index !== undefined && frequency !== 'monthly' && frequency !== 'yearly')
270
+ throw new UnsupportedCalendarRecurrenceError(
271
+ 'Ordinal weekdays require monthly or yearly recurrence'
272
+ );
273
+ const monthDays = fields.BYMONTHDAY?.split(',').map(Number);
274
+ const clampsMonthDay =
275
+ !byday &&
276
+ fields.BYSETPOS === '-1' &&
277
+ Boolean(
278
+ monthDays &&
279
+ monthDays.length > 1 &&
280
+ monthDays.every((day, i) => day === 28 + i) &&
281
+ monthDays.at(-1)! <= 31
282
+ );
283
+ if (
284
+ byday?.some((day) => day.index !== index) ||
285
+ (fields.BYSETPOS &&
286
+ !clampsMonthDay &&
287
+ (fields.BYSETPOS !== '1' || index === undefined)) ||
288
+ (index !== undefined && byday!.length > 1 && fields.BYSETPOS !== '1')
289
+ )
290
+ throw new UnsupportedCalendarRecurrenceError(
291
+ 'Relative RRULE is not representable by Graph'
292
+ );
293
+ const number = (value: string | undefined, fallback?: number) => {
294
+ if (value === undefined) return fallback;
295
+ if (!/^\d+$/.test(value))
296
+ throw new UnsupportedCalendarRecurrenceError(
297
+ 'Invalid numeric RRULE field'
298
+ );
299
+ return Number(value);
300
+ };
301
+ let end: CalendarRecurrenceRule['end'] = { type: 'never' };
302
+ if (fields.COUNT) end = { type: 'count', count: number(fields.COUNT)! };
303
+ if (fields.UNTIL) {
304
+ let date: Temporal.PlainDate;
305
+ if (anchor.allDay && /^\d{8}$/.test(fields.UNTIL))
306
+ date = Temporal.PlainDate.from(
307
+ `${fields.UNTIL.slice(0, 4)}-${fields.UNTIL.slice(4, 6)}-${fields.UNTIL.slice(6)}`
308
+ );
309
+ else if (!anchor.allDay && /^\d{8}T\d{6}Z$/.test(fields.UNTIL)) {
310
+ const raw = fields.UNTIL;
311
+ const cutoff = Temporal.Instant.from(
312
+ `${raw.slice(0, 4)}-${raw.slice(4, 6)}-${raw.slice(6, 8)}T${raw.slice(9, 11)}:${raw.slice(11, 13)}:${raw.slice(13, 15)}Z`
313
+ );
314
+ date = cutoff.toZonedDateTimeISO(timeZone).toPlainDate();
315
+ const slot = date
316
+ .toPlainDateTime(
317
+ Temporal.PlainDateTime.from(anchor.startLocal).toPlainTime()
318
+ )
319
+ .toZonedDateTime(timeZone)
320
+ .toInstant();
321
+ if (Temporal.Instant.compare(slot, cutoff) > 0)
322
+ date = date.subtract({ days: 1 });
323
+ } else
324
+ throw new UnsupportedCalendarRecurrenceError('Invalid UNTIL date type');
325
+ end = { type: 'until', date: date.toString() };
326
+ }
327
+ const anchorDate = Temporal.PlainDateTime.from(
328
+ anchor.startLocal
329
+ ).toPlainDate();
330
+ const rule = CalendarRecurrenceRuleSchema.parse({
331
+ version: 1,
332
+ frequency,
333
+ interval: number(fields.INTERVAL, 1),
334
+ timeZone,
335
+ ...(frequency === 'weekly'
336
+ ? {
337
+ weekdays: byday?.map((day) => day.day) ?? [
338
+ CALENDAR_WEEKDAYS[anchorDate.dayOfWeek - 1],
339
+ ],
340
+ weekStartsOn: fields.WKST ?? 'MO',
341
+ }
342
+ : byday
343
+ ? { weekdays: byday.map((day) => day.day), weekIndex: index }
344
+ : {}),
345
+ ...(fields.BYMONTHDAY
346
+ ? clampsMonthDay
347
+ ? { monthDay: monthDays!.at(-1), monthDayOverflow: 'last-day' }
348
+ : { monthDay: number(fields.BYMONTHDAY) }
349
+ : !byday && ['monthly', 'yearly'].includes(frequency ?? '')
350
+ ? { monthDay: anchorDate.day }
351
+ : {}),
352
+ ...(frequency === 'yearly'
353
+ ? { month: number(fields.BYMONTH, anchorDate.month) }
354
+ : fields.BYMONTH
355
+ ? { month: number(fields.BYMONTH) }
356
+ : {}),
357
+ end,
358
+ });
359
+ validateCalendarRecurrence(rule, anchor);
360
+ return rule;
361
+ }
@@ -0,0 +1,180 @@
1
+ import { Temporal } from '@js-temporal/polyfill';
2
+ import type {
3
+ CalendarRecurrenceAnchor,
4
+ CalendarRecurrenceRule,
5
+ } from '@tuturuuu/types/primitives/calendar-recurrence';
6
+ import { z } from 'zod';
7
+
8
+ import {
9
+ CALENDAR_WEEKDAYS,
10
+ matchesCalendarRecurrenceDate,
11
+ } from './calendar-recurrence-pattern';
12
+
13
+ const weekday = z.enum(CALENDAR_WEEKDAYS);
14
+ export const CalendarLocalDateSchema = z
15
+ .string()
16
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
17
+ .refine((value) => {
18
+ try {
19
+ Temporal.PlainDate.from(value, { overflow: 'reject' });
20
+ return true;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }, 'Invalid calendar date');
25
+ export const CalendarLocalDateTimeSchema = z
26
+ .string()
27
+ .regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/)
28
+ .refine((value) => {
29
+ try {
30
+ Temporal.PlainDateTime.from(value, { overflow: 'reject' });
31
+ return true;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }, 'Invalid local date-time');
36
+
37
+ export const CalendarRecurrenceRuleSchema = z
38
+ .object({
39
+ version: z.literal(1),
40
+ frequency: z.enum(['daily', 'weekly', 'monthly', 'yearly']),
41
+ interval: z.number().int().min(1).max(1000),
42
+ timeZone: z
43
+ .string()
44
+ .max(100)
45
+ .refine((value) => {
46
+ if (/^[+-]/.test(value)) return false;
47
+ try {
48
+ Temporal.Now.zonedDateTimeISO(value);
49
+ return true;
50
+ } catch {
51
+ return false;
52
+ }
53
+ }, 'Expected a named time zone'),
54
+ weekStartsOn: weekday.optional(),
55
+ weekdays: z
56
+ .array(weekday)
57
+ .min(1)
58
+ .max(7)
59
+ .refine((days) => new Set(days).size === days.length)
60
+ .optional(),
61
+ monthDay: z.number().int().min(1).max(31).optional(),
62
+ monthDayOverflow: z.enum(['skip', 'last-day']).optional(),
63
+ weekIndex: z
64
+ .union([
65
+ z.literal(1),
66
+ z.literal(2),
67
+ z.literal(3),
68
+ z.literal(4),
69
+ z.literal(-1),
70
+ ])
71
+ .optional(),
72
+ month: z.number().int().min(1).max(12).optional(),
73
+ end: z.discriminatedUnion('type', [
74
+ z.object({ type: z.literal('never') }).strict(),
75
+ z
76
+ .object({
77
+ type: z.literal('count'),
78
+ count: z.number().int().min(1).max(10000),
79
+ })
80
+ .strict(),
81
+ z
82
+ .object({ type: z.literal('until'), date: CalendarLocalDateSchema })
83
+ .strict(),
84
+ ]),
85
+ })
86
+ .strict()
87
+ .superRefine((rule, ctx) => {
88
+ const invalid = (message: string) =>
89
+ ctx.addIssue({ code: 'custom', message });
90
+ if (rule.monthDayOverflow && rule.monthDay === undefined)
91
+ invalid('Month-day overflow requires an absolute month day');
92
+ if (
93
+ rule.frequency === 'daily' &&
94
+ (rule.weekdays || rule.monthDay || rule.weekIndex || rule.month)
95
+ )
96
+ invalid('Daily recurrence does not accept selectors');
97
+ if (
98
+ rule.frequency === 'weekly' &&
99
+ (!rule.weekdays || rule.monthDay || rule.weekIndex || rule.month)
100
+ )
101
+ invalid('Weekly recurrence requires only weekday selectors');
102
+ if (rule.frequency === 'monthly' || rule.frequency === 'yearly') {
103
+ if (
104
+ rule.monthDay !== undefined
105
+ ? Boolean(rule.weekdays || rule.weekIndex)
106
+ : !(rule.weekdays && rule.weekIndex)
107
+ )
108
+ invalid('Choose a month day or relative weekdays and index');
109
+ if (rule.frequency === 'yearly' ? !rule.month : rule.month !== undefined)
110
+ invalid('Month is required only for yearly recurrence');
111
+ }
112
+ });
113
+ export const CalendarRecurrenceAnchorSchema = z
114
+ .object({
115
+ startLocal: CalendarLocalDateTimeSchema,
116
+ endLocal: CalendarLocalDateTimeSchema,
117
+ allDay: z.boolean(),
118
+ })
119
+ .strict()
120
+ .superRefine((anchor, ctx) => {
121
+ if (Temporal.PlainDateTime.compare(anchor.startLocal, anchor.endLocal) >= 0)
122
+ ctx.addIssue({ code: 'custom', message: 'End must follow start' });
123
+ if (
124
+ anchor.allDay &&
125
+ ![anchor.startLocal, anchor.endLocal].every((value) =>
126
+ value.endsWith('T00:00:00')
127
+ )
128
+ )
129
+ ctx.addIssue({
130
+ code: 'custom',
131
+ message: 'All-day anchors require exclusive midnight date boundaries',
132
+ });
133
+ });
134
+
135
+ export function validateCalendarRecurrence(
136
+ rule: CalendarRecurrenceRule,
137
+ anchor: CalendarRecurrenceAnchor
138
+ ) {
139
+ const validRule = CalendarRecurrenceRuleSchema.parse(rule);
140
+ const validAnchor = CalendarRecurrenceAnchorSchema.parse(anchor);
141
+ if (
142
+ validRule.end.type === 'until' &&
143
+ validRule.end.date < validAnchor.startLocal.slice(0, 10)
144
+ )
145
+ throw new RangeError('Recurrence ends before its anchor');
146
+ const first = Temporal.PlainDateTime.from(
147
+ validAnchor.startLocal
148
+ ).toPlainDate();
149
+ if (!matchesCalendarRecurrenceDate(first, first, validRule))
150
+ throw new RangeError('Series anchor must match its recurrence pattern');
151
+ return { rule: validRule, anchor: validAnchor };
152
+ }
153
+
154
+ export const CalendarRecurrenceExceptionSchema = z
155
+ .object({
156
+ originalStartLocal: CalendarLocalDateTimeSchema,
157
+ cancelled: z.boolean().optional(),
158
+ startLocal: CalendarLocalDateTimeSchema.optional(),
159
+ endLocal: CalendarLocalDateTimeSchema.optional(),
160
+ })
161
+ .strict()
162
+ .superRefine((exception, ctx) => {
163
+ if (Boolean(exception.startLocal) !== Boolean(exception.endLocal))
164
+ ctx.addIssue({
165
+ code: 'custom',
166
+ message: 'Moved occurrences require start and end',
167
+ });
168
+ if (
169
+ exception.startLocal &&
170
+ exception.endLocal &&
171
+ Temporal.PlainDateTime.compare(
172
+ exception.startLocal,
173
+ exception.endLocal
174
+ ) >= 0
175
+ )
176
+ ctx.addIssue({
177
+ code: 'custom',
178
+ message: 'Moved occurrence end must follow start',
179
+ });
180
+ });
@@ -0,0 +1,5 @@
1
+ export * from './calendar-recurrence-edit';
2
+ export * from './calendar-recurrence-expansion';
3
+ export { CALENDAR_WEEKDAYS } from './calendar-recurrence-pattern';
4
+ export * from './calendar-recurrence-providers';
5
+ export * from './calendar-recurrence-validation';
@@ -7,10 +7,17 @@ dayjs.extend(timezone);
7
7
  dayjs.extend(utc);
8
8
 
9
9
  export function isAllDayEvent(
10
- event: Pick<CalendarEvent, 'start_at' | 'end_at'>
10
+ event: Pick<CalendarEvent, 'start_at' | 'end_at'>,
11
+ displayTimezone?: string
11
12
  ): boolean {
12
- const start = dayjs(event.start_at);
13
- const end = dayjs(event.end_at);
13
+ const start =
14
+ displayTimezone && displayTimezone !== 'auto'
15
+ ? dayjs(event.start_at).tz(displayTimezone)
16
+ : dayjs(event.start_at);
17
+ const end =
18
+ displayTimezone && displayTimezone !== 'auto'
19
+ ? dayjs(event.end_at).tz(displayTimezone)
20
+ : dayjs(event.end_at);
14
21
 
15
22
  const durationMs = end.diff(start, 'millisecond');
16
23
  const isMultipleOf24Hours = durationMs % (24 * 60 * 60 * 1000) === 0;
@@ -27,6 +34,37 @@ export function isAllDayEvent(
27
34
  );
28
35
  }
29
36
 
37
+ /** Civil date carriers for all-day rendering, with an exclusive end date.
38
+ * Untagged UTC-midnight legacy imports preserve their stored YMD, matching mobile.
39
+ * Other rows project actual instants into the configured display timezone.
40
+ */
41
+ export function allDayEventCivilDates(
42
+ event: Pick<CalendarEvent, 'start_at' | 'end_at'>,
43
+ displayTimezone?: string
44
+ ) {
45
+ const rawStart = dayjs(event.start_at);
46
+ const rawEnd = dayjs(event.end_at);
47
+ if (!rawStart.isValid() || !rawEnd.isValid() || !rawEnd.isAfter(rawStart))
48
+ return null;
49
+ const utcStart = rawStart.utc();
50
+ const utcEnd = rawEnd.utc();
51
+ const midnight = (date: dayjs.Dayjs) =>
52
+ date.hour() === 0 &&
53
+ date.minute() === 0 &&
54
+ date.second() === 0 &&
55
+ date.millisecond() === 0;
56
+ const legacyDateOnly = midnight(utcStart) && midnight(utcEnd);
57
+ const project = (date: dayjs.Dayjs) =>
58
+ legacyDateOnly
59
+ ? date.utc()
60
+ : displayTimezone && displayTimezone !== 'auto'
61
+ ? date.tz(displayTimezone)
62
+ : date;
63
+ const civil = (date: dayjs.Dayjs) =>
64
+ new Date(date.year(), date.month(), date.date());
65
+ return { start: civil(project(rawStart)), end: civil(project(rawEnd)) };
66
+ }
67
+
30
68
  // Helper function to convert Google Calendar all-day events to proper timezone
31
69
  export function convertGoogleAllDayEvent(
32
70
  startDate: string | undefined,
@@ -270,10 +270,6 @@ describe('application metadata coverage', () => {
270
270
  });
271
271
 
272
272
  it('covers the non-Next public application shells', () => {
273
- const tanStackHead = readFileSync(
274
- resolve(repoRoot, 'apps/tanstack-web/src/lib/platform/app-shell.ts'),
275
- 'utf8'
276
- );
277
273
  const mobileHead = readFileSync(
278
274
  resolve(repoRoot, 'apps/mobile/web/index.html'),
279
275
  'utf8'
@@ -282,8 +278,6 @@ describe('application metadata coverage', () => {
282
278
  readFileSync(resolve(repoRoot, 'apps/docs/docs.json'), 'utf8')
283
279
  ) as { description?: string };
284
280
 
285
- expect(tanStackHead).toContain('max-image-preview:large');
286
- expect(tanStackHead).toContain("name: 'twitter:site'");
287
281
  expect(mobileHead).toContain('name="robots" content="noindex, nofollow"');
288
282
  expect(mobileHead).toContain('property="og:image"');
289
283
  expect(docsConfig.description).toMatch(/Tuturuuu platform/);
package/src/constants.ts CHANGED
@@ -79,6 +79,9 @@ export const MAX_TASK_NAME_LENGTH = 1024;
79
79
  export const MAX_TASK_DESCRIPTION_LENGTH = 100000;
80
80
  export const MAX_CHAT_MESSAGE_LENGTH = 10000;
81
81
  export const MAX_SUPPORT_INQUIRY_LENGTH = 5000;
82
+ export const MAX_SUPPORT_INQUIRY_MESSAGE_LENGTH = 512;
83
+ export const MAX_SUPPORT_INQUIRY_NAME_LENGTH = 64;
84
+ export const MAX_SUPPORT_INQUIRY_SUBJECT_LENGTH = 128;
82
85
  export const MAX_CALENDAR_EVENT_TITLE_LENGTH = 255;
83
86
  export const MAX_CALENDAR_EVENT_DESCRIPTION_LENGTH = 10000;
84
87
  export const MAX_NOTE_TITLE_LENGTH = 255;
@@ -0,0 +1,26 @@
1
+ import { FileKey2 } from '@tuturuuu/icons';
2
+ import type { RolePermission } from './permissions';
3
+
4
+ /** Called only inside the existing root/full infrastructure catalog branch. */
5
+ export function deploymentVaultPermissions(
6
+ t: (key: string) => string
7
+ ): RolePermission[] {
8
+ return [
9
+ {
10
+ id: 'manage_mobile_deployment_vault',
11
+ icon: <FileKey2 />,
12
+ title: t('ws-roles.manage_mobile_deployment_vault'),
13
+ description: t('ws-roles.manage_mobile_deployment_vault_description'),
14
+ disableOnProduction: false,
15
+ disabled: false,
16
+ },
17
+ {
18
+ id: 'manage_desktop_deployment_vault',
19
+ icon: <FileKey2 />,
20
+ title: t('ws-roles.manage_desktop_deployment_vault'),
21
+ description: t('ws-roles.manage_desktop_deployment_vault_description'),
22
+ disableOnProduction: false,
23
+ disabled: false,
24
+ },
25
+ ];
26
+ }
@@ -0,0 +1,20 @@
1
+ /** Shared pure signing allowlist; native CI imports this checkout file directly. */
2
+ export const DESKTOP_SIGNING_PROFILES = {
3
+ windows: {
4
+ files: ['windows_authenticode_certificate_pfx'],
5
+ scalars: ['WINDOWS_SIGNING_CERTIFICATE_PASSWORD'],
6
+ },
7
+ macos: {
8
+ files: [
9
+ 'macos_developer_id_certificate_p12',
10
+ 'macos_notarization_private_key_p8',
11
+ ],
12
+ scalars: [
13
+ 'MACOS_CERTIFICATE_PASSWORD',
14
+ 'MACOS_SIGNING_IDENTITY',
15
+ 'APPLE_TEAM_ID',
16
+ 'APP_STORE_CONNECT_API_KEY_ID',
17
+ 'APP_STORE_CONNECT_ISSUER_ID',
18
+ ],
19
+ },
20
+ } as const;