@juppytt/fws 0.1.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.
@@ -0,0 +1,483 @@
1
+ import { Router } from 'express';
2
+ import { getStore } from '../../store/index.js';
3
+ import { generateId, generateEtag } from '../../util/id.js';
4
+ import { DEFAULT_USER_EMAIL } from '../../store/seed.js';
5
+
6
+ export function calendarRoutes(): Router {
7
+ const r = Router();
8
+ const PREFIX = '/calendar/v3';
9
+
10
+ function resolveCalendarId(raw: string): string {
11
+ if (raw === 'primary') {
12
+ const store = getStore();
13
+ const primary = Object.values(store.calendar.calendarList).find(c => c.primary);
14
+ return primary?.id || Object.keys(store.calendar.calendars)[0] || raw;
15
+ }
16
+ return raw;
17
+ }
18
+
19
+ // LIST calendarList
20
+ r.get(`${PREFIX}/users/me/calendarList`, (_req, res) => {
21
+ const items = Object.values(getStore().calendar.calendarList);
22
+ res.json({ kind: 'calendar#calendarList', etag: generateEtag(), items });
23
+ });
24
+
25
+ // GET calendarList entry
26
+ r.get(`${PREFIX}/users/me/calendarList/:calendarId`, (req, res) => {
27
+ const id = resolveCalendarId(req.params.calendarId);
28
+ const entry = getStore().calendar.calendarList[id];
29
+ if (!entry) {
30
+ return res.status(404).json({
31
+ error: { code: 404, message: 'Not Found', status: 'NOT_FOUND' },
32
+ });
33
+ }
34
+ res.json(entry);
35
+ });
36
+
37
+ // INSERT calendarList entry
38
+ r.post(`${PREFIX}/users/me/calendarList`, (req, res) => {
39
+ const store = getStore();
40
+ const id = req.body.id;
41
+ if (!id || !store.calendar.calendars[id]) {
42
+ return res.status(404).json({
43
+ error: { code: 404, message: 'Not Found', status: 'NOT_FOUND' },
44
+ });
45
+ }
46
+ const cal = store.calendar.calendars[id];
47
+ const etag = generateEtag();
48
+ store.calendar.calendarList[id] = {
49
+ kind: 'calendar#calendarListEntry',
50
+ id,
51
+ summary: cal.summary,
52
+ description: cal.description,
53
+ timeZone: cal.timeZone,
54
+ accessRole: req.body.accessRole || 'reader',
55
+ defaultReminders: req.body.defaultReminders || [],
56
+ selected: req.body.selected ?? true,
57
+ etag,
58
+ };
59
+ res.json(store.calendar.calendarList[id]);
60
+ });
61
+
62
+ // PATCH calendarList entry
63
+ r.patch(`${PREFIX}/users/me/calendarList/:calendarId`, (req, res) => {
64
+ const id = resolveCalendarId(req.params.calendarId);
65
+ const store = getStore();
66
+ const entry = store.calendar.calendarList[id];
67
+ if (!entry) {
68
+ return res.status(404).json({
69
+ error: { code: 404, message: 'Not Found', status: 'NOT_FOUND' },
70
+ });
71
+ }
72
+ Object.assign(entry, req.body, { id, kind: entry.kind });
73
+ entry.etag = generateEtag();
74
+ res.json(entry);
75
+ });
76
+
77
+ // UPDATE calendarList entry (PUT)
78
+ r.put(`${PREFIX}/users/me/calendarList/:calendarId`, (req, res) => {
79
+ const id = resolveCalendarId(req.params.calendarId);
80
+ const store = getStore();
81
+ if (!store.calendar.calendarList[id]) {
82
+ return res.status(404).json({
83
+ error: { code: 404, message: 'Not Found', status: 'NOT_FOUND' },
84
+ });
85
+ }
86
+ store.calendar.calendarList[id] = {
87
+ kind: 'calendar#calendarListEntry',
88
+ ...req.body,
89
+ id,
90
+ etag: generateEtag(),
91
+ };
92
+ res.json(store.calendar.calendarList[id]);
93
+ });
94
+
95
+ // DELETE calendarList entry
96
+ r.delete(`${PREFIX}/users/me/calendarList/:calendarId`, (req, res) => {
97
+ const id = resolveCalendarId(req.params.calendarId);
98
+ const store = getStore();
99
+ if (!store.calendar.calendarList[id]) {
100
+ return res.status(404).json({
101
+ error: { code: 404, message: 'Not Found', status: 'NOT_FOUND' },
102
+ });
103
+ }
104
+ delete store.calendar.calendarList[id];
105
+ res.status(204).send();
106
+ });
107
+
108
+ // CREATE calendar
109
+ r.post(`${PREFIX}/calendars`, (req, res) => {
110
+ const store = getStore();
111
+ const id = generateId();
112
+ const etag = generateEtag();
113
+ const cal = {
114
+ kind: 'calendar#calendar' as const,
115
+ id,
116
+ summary: req.body.summary || 'Untitled',
117
+ description: req.body.description,
118
+ timeZone: req.body.timeZone || 'UTC',
119
+ etag,
120
+ };
121
+ store.calendar.calendars[id] = cal;
122
+ store.calendar.events[id] = {};
123
+ store.calendar.calendarList[id] = {
124
+ kind: 'calendar#calendarListEntry',
125
+ id,
126
+ summary: cal.summary,
127
+ description: cal.description,
128
+ timeZone: cal.timeZone,
129
+ accessRole: 'owner',
130
+ defaultReminders: [],
131
+ selected: true,
132
+ etag,
133
+ };
134
+ res.json(cal);
135
+ });
136
+
137
+ // GET calendar
138
+ r.get(`${PREFIX}/calendars/:calendarId`, (req, res) => {
139
+ const id = resolveCalendarId(req.params.calendarId);
140
+ const cal = getStore().calendar.calendars[id];
141
+ if (!cal) {
142
+ return res.status(404).json({
143
+ error: { code: 404, message: 'Not Found', status: 'NOT_FOUND' },
144
+ });
145
+ }
146
+ res.json(cal);
147
+ });
148
+
149
+ // PATCH calendar
150
+ r.patch(`${PREFIX}/calendars/:calendarId`, (req, res) => {
151
+ const id = resolveCalendarId(req.params.calendarId);
152
+ const store = getStore();
153
+ const cal = store.calendar.calendars[id];
154
+ if (!cal) {
155
+ return res.status(404).json({
156
+ error: { code: 404, message: 'Not Found', status: 'NOT_FOUND' },
157
+ });
158
+ }
159
+ Object.assign(cal, req.body, { id, kind: cal.kind });
160
+ cal.etag = generateEtag();
161
+ // Also update calendarList entry
162
+ const listEntry = store.calendar.calendarList[id];
163
+ if (listEntry) {
164
+ if (req.body.summary) listEntry.summary = req.body.summary;
165
+ if (req.body.description) listEntry.description = req.body.description;
166
+ listEntry.etag = cal.etag;
167
+ }
168
+ res.json(cal);
169
+ });
170
+
171
+ // UPDATE calendar (PUT)
172
+ r.put(`${PREFIX}/calendars/:calendarId`, (req, res) => {
173
+ const id = resolveCalendarId(req.params.calendarId);
174
+ const store = getStore();
175
+ if (!store.calendar.calendars[id]) {
176
+ return res.status(404).json({
177
+ error: { code: 404, message: 'Not Found', status: 'NOT_FOUND' },
178
+ });
179
+ }
180
+ const etag = generateEtag();
181
+ store.calendar.calendars[id] = {
182
+ kind: 'calendar#calendar',
183
+ ...req.body,
184
+ id,
185
+ etag,
186
+ };
187
+ const listEntry = store.calendar.calendarList[id];
188
+ if (listEntry) {
189
+ listEntry.summary = req.body.summary || listEntry.summary;
190
+ listEntry.description = req.body.description;
191
+ listEntry.timeZone = req.body.timeZone || listEntry.timeZone;
192
+ listEntry.etag = etag;
193
+ }
194
+ res.json(store.calendar.calendars[id]);
195
+ });
196
+
197
+ // CLEAR calendar (delete all events)
198
+ r.post(`${PREFIX}/calendars/:calendarId/clear`, (req, res) => {
199
+ const id = resolveCalendarId(req.params.calendarId);
200
+ const store = getStore();
201
+ if (!store.calendar.events[id]) {
202
+ return res.status(404).json({
203
+ error: { code: 404, message: 'Not Found', status: 'NOT_FOUND' },
204
+ });
205
+ }
206
+ store.calendar.events[id] = {};
207
+ res.status(204).send();
208
+ });
209
+
210
+ // DELETE calendar
211
+ r.delete(`${PREFIX}/calendars/:calendarId`, (req, res) => {
212
+ const id = resolveCalendarId(req.params.calendarId);
213
+ const store = getStore();
214
+ if (!store.calendar.calendars[id]) {
215
+ return res.status(404).json({
216
+ error: { code: 404, message: 'Not Found', status: 'NOT_FOUND' },
217
+ });
218
+ }
219
+ delete store.calendar.calendars[id];
220
+ delete store.calendar.events[id];
221
+ delete store.calendar.calendarList[id];
222
+ res.status(204).send();
223
+ });
224
+
225
+ // LIST events
226
+ r.get(`${PREFIX}/calendars/:calendarId/events`, (req, res) => {
227
+ const id = resolveCalendarId(req.params.calendarId);
228
+ const store = getStore();
229
+ const eventsMap = store.calendar.events[id];
230
+ if (!eventsMap) {
231
+ return res.status(404).json({
232
+ error: { code: 404, message: 'Not Found', status: 'NOT_FOUND' },
233
+ });
234
+ }
235
+
236
+ let events = Object.values(eventsMap);
237
+
238
+ // Filter cancelled unless showDeleted
239
+ if (req.query.showDeleted !== 'true') {
240
+ events = events.filter(e => e.status !== 'cancelled');
241
+ }
242
+
243
+ // Time range filtering
244
+ const timeMin = req.query.timeMin as string | undefined;
245
+ const timeMax = req.query.timeMax as string | undefined;
246
+ if (timeMin) {
247
+ const min = new Date(timeMin).getTime();
248
+ events = events.filter(e => {
249
+ const end = new Date(e.end.dateTime || e.end.date || '').getTime();
250
+ return end > min;
251
+ });
252
+ }
253
+ if (timeMax) {
254
+ const max = new Date(timeMax).getTime();
255
+ events = events.filter(e => {
256
+ const start = new Date(e.start.dateTime || e.start.date || '').getTime();
257
+ return start < max;
258
+ });
259
+ }
260
+
261
+ // Search by q
262
+ const q = req.query.q as string | undefined;
263
+ if (q) {
264
+ const lower = q.toLowerCase();
265
+ events = events.filter(e =>
266
+ (e.summary || '').toLowerCase().includes(lower) ||
267
+ (e.description || '').toLowerCase().includes(lower)
268
+ );
269
+ }
270
+
271
+ // Sort by start time
272
+ events.sort((a, b) => {
273
+ const aTime = new Date(a.start.dateTime || a.start.date || '').getTime();
274
+ const bTime = new Date(b.start.dateTime || b.start.date || '').getTime();
275
+ return aTime - bTime;
276
+ });
277
+
278
+ const maxResults = parseInt(req.query.maxResults as string) || 250;
279
+ events = events.slice(0, maxResults);
280
+
281
+ res.json({
282
+ kind: 'calendar#events',
283
+ summary: store.calendar.calendars[id]?.summary || '',
284
+ updated: new Date().toISOString(),
285
+ items: events,
286
+ });
287
+ });
288
+
289
+ // CREATE event
290
+ r.post(`${PREFIX}/calendars/:calendarId/events`, (req, res) => {
291
+ const calId = resolveCalendarId(req.params.calendarId);
292
+ const store = getStore();
293
+ if (!store.calendar.events[calId]) {
294
+ return res.status(404).json({
295
+ error: { code: 404, message: 'Not Found', status: 'NOT_FOUND' },
296
+ });
297
+ }
298
+
299
+ const eventId = generateId();
300
+ const now = new Date().toISOString();
301
+ const userEmail = store.gmail.profile.emailAddress;
302
+
303
+ const event = {
304
+ kind: 'calendar#event' as const,
305
+ id: eventId,
306
+ status: (req.body.status || 'confirmed') as 'confirmed',
307
+ summary: req.body.summary,
308
+ description: req.body.description,
309
+ location: req.body.location,
310
+ start: req.body.start || { dateTime: now },
311
+ end: req.body.end || { dateTime: now },
312
+ created: now,
313
+ updated: now,
314
+ creator: { email: userEmail },
315
+ organizer: { email: userEmail, self: true },
316
+ attendees: req.body.attendees,
317
+ etag: generateEtag(),
318
+ htmlLink: `https://calendar.google.com/event?eid=${eventId}`,
319
+ iCalUID: `${eventId}@example.com`,
320
+ };
321
+
322
+ store.calendar.events[calId][eventId] = event;
323
+ res.json(event);
324
+ });
325
+
326
+ // GET event
327
+ r.get(`${PREFIX}/calendars/:calendarId/events/:eventId`, (req, res) => {
328
+ const calId = resolveCalendarId(req.params.calendarId);
329
+ const event = getStore().calendar.events[calId]?.[req.params.eventId];
330
+ if (!event) {
331
+ return res.status(404).json({
332
+ error: { code: 404, message: 'Not Found', status: 'NOT_FOUND' },
333
+ });
334
+ }
335
+ res.json(event);
336
+ });
337
+
338
+ // PATCH event
339
+ r.patch(`${PREFIX}/calendars/:calendarId/events/:eventId`, (req, res) => {
340
+ const calId = resolveCalendarId(req.params.calendarId);
341
+ const store = getStore();
342
+ const event = store.calendar.events[calId]?.[req.params.eventId];
343
+ if (!event) {
344
+ return res.status(404).json({
345
+ error: { code: 404, message: 'Not Found', status: 'NOT_FOUND' },
346
+ });
347
+ }
348
+ Object.assign(event, req.body, { id: event.id, kind: event.kind });
349
+ event.updated = new Date().toISOString();
350
+ event.etag = generateEtag();
351
+ res.json(event);
352
+ });
353
+
354
+ // PUT event (full replace)
355
+ r.put(`${PREFIX}/calendars/:calendarId/events/:eventId`, (req, res) => {
356
+ const calId = resolveCalendarId(req.params.calendarId);
357
+ const store = getStore();
358
+ if (!store.calendar.events[calId]?.[req.params.eventId]) {
359
+ return res.status(404).json({
360
+ error: { code: 404, message: 'Not Found', status: 'NOT_FOUND' },
361
+ });
362
+ }
363
+ const userEmail = store.gmail.profile.emailAddress;
364
+ const event = {
365
+ kind: 'calendar#event' as const,
366
+ ...req.body,
367
+ id: req.params.eventId,
368
+ updated: new Date().toISOString(),
369
+ creator: req.body.creator || { email: userEmail },
370
+ organizer: req.body.organizer || { email: userEmail, self: true },
371
+ etag: generateEtag(),
372
+ htmlLink: `https://calendar.google.com/event?eid=${req.params.eventId}`,
373
+ iCalUID: req.body.iCalUID || `${req.params.eventId}@example.com`,
374
+ };
375
+ store.calendar.events[calId][req.params.eventId] = event;
376
+ res.json(event);
377
+ });
378
+
379
+ // DELETE event
380
+ r.delete(`${PREFIX}/calendars/:calendarId/events/:eventId`, (req, res) => {
381
+ const calId = resolveCalendarId(req.params.calendarId);
382
+ const store = getStore();
383
+ if (!store.calendar.events[calId]?.[req.params.eventId]) {
384
+ return res.status(404).json({
385
+ error: { code: 404, message: 'Not Found', status: 'NOT_FOUND' },
386
+ });
387
+ }
388
+ delete store.calendar.events[calId][req.params.eventId];
389
+ res.status(204).send();
390
+ });
391
+
392
+ // IMPORT event (like insert but preserves iCalUID)
393
+ r.post(`${PREFIX}/calendars/:calendarId/events/import`, (req, res) => {
394
+ const calId = resolveCalendarId(req.params.calendarId);
395
+ const store = getStore();
396
+ if (!store.calendar.events[calId]) {
397
+ return res.status(404).json({
398
+ error: { code: 404, message: 'Not Found', status: 'NOT_FOUND' },
399
+ });
400
+ }
401
+ const eventId = generateId();
402
+ const now = new Date().toISOString();
403
+ const userEmail = store.gmail.profile.emailAddress;
404
+ const event = {
405
+ kind: 'calendar#event' as const,
406
+ ...req.body,
407
+ id: eventId,
408
+ status: req.body.status || 'confirmed',
409
+ created: now,
410
+ updated: now,
411
+ creator: req.body.creator || { email: userEmail },
412
+ organizer: req.body.organizer || { email: userEmail, self: true },
413
+ etag: generateEtag(),
414
+ htmlLink: `https://calendar.google.com/event?eid=${eventId}`,
415
+ iCalUID: req.body.iCalUID || `${eventId}@example.com`,
416
+ };
417
+ store.calendar.events[calId][eventId] = event;
418
+ res.json(event);
419
+ });
420
+
421
+ // MOVE event to another calendar
422
+ r.post(`${PREFIX}/calendars/:calendarId/events/:eventId/move`, (req, res) => {
423
+ const srcCalId = resolveCalendarId(req.params.calendarId);
424
+ const destCalId = resolveCalendarId(req.query.destination as string);
425
+ const store = getStore();
426
+ const event = store.calendar.events[srcCalId]?.[req.params.eventId];
427
+ if (!event) {
428
+ return res.status(404).json({
429
+ error: { code: 404, message: 'Not Found', status: 'NOT_FOUND' },
430
+ });
431
+ }
432
+ if (!store.calendar.events[destCalId]) {
433
+ return res.status(404).json({
434
+ error: { code: 404, message: 'Destination calendar not found', status: 'NOT_FOUND' },
435
+ });
436
+ }
437
+ // Move: delete from source, add to destination
438
+ delete store.calendar.events[srcCalId][req.params.eventId];
439
+ store.calendar.events[destCalId][req.params.eventId] = event;
440
+ event.updated = new Date().toISOString();
441
+ event.etag = generateEtag();
442
+ res.json(event);
443
+ });
444
+
445
+ // QUICK ADD event (from text)
446
+ r.post(`${PREFIX}/calendars/:calendarId/events/quickAdd`, (req, res) => {
447
+ const calId = resolveCalendarId(req.params.calendarId);
448
+ const store = getStore();
449
+ if (!store.calendar.events[calId]) {
450
+ return res.status(404).json({
451
+ error: { code: 404, message: 'Not Found', status: 'NOT_FOUND' },
452
+ });
453
+ }
454
+ const text = req.query.text as string || '';
455
+ const eventId = generateId();
456
+ const now = new Date().toISOString();
457
+ const userEmail = store.gmail.profile.emailAddress;
458
+
459
+ // Simple parsing: use the text as summary, default to 1h from now
460
+ const start = new Date();
461
+ const end = new Date(start.getTime() + 3600000);
462
+
463
+ const event = {
464
+ kind: 'calendar#event' as const,
465
+ id: eventId,
466
+ status: 'confirmed' as const,
467
+ summary: text,
468
+ start: { dateTime: start.toISOString() },
469
+ end: { dateTime: end.toISOString() },
470
+ created: now,
471
+ updated: now,
472
+ creator: { email: userEmail },
473
+ organizer: { email: userEmail, self: true },
474
+ etag: generateEtag(),
475
+ htmlLink: `https://calendar.google.com/event?eid=${eventId}`,
476
+ iCalUID: `${eventId}@example.com`,
477
+ };
478
+ store.calendar.events[calId][eventId] = event;
479
+ res.json(event);
480
+ });
481
+
482
+ return r;
483
+ }
@@ -0,0 +1,151 @@
1
+ import { Router } from 'express';
2
+ import { getStore, resetStore, loadStore, serializeStore } from '../../store/index.js';
3
+ import { generateId, generateEtag } from '../../util/id.js';
4
+
5
+ export function controlRoutes(): Router {
6
+ const r = Router();
7
+
8
+ r.get('/__fws/status', (_req, res) => {
9
+ res.json({ status: 'ok' });
10
+ });
11
+
12
+ r.post('/__fws/snapshot/save', (_req, res) => {
13
+ const json = serializeStore();
14
+ res.type('application/json').send(json);
15
+ });
16
+
17
+ r.post('/__fws/snapshot/load', (req, res) => {
18
+ const data = req.body;
19
+ loadStore(data);
20
+ res.json({ status: 'loaded' });
21
+ });
22
+
23
+ r.post('/__fws/reset', (_req, res) => {
24
+ resetStore();
25
+ res.json({ status: 'reset' });
26
+ });
27
+
28
+ // Setup convenience endpoints
29
+ r.post('/__fws/setup/gmail/message', (req, res) => {
30
+ const store = getStore();
31
+ const { from, to, subject, body, labels, date } = req.body;
32
+ const id = generateId();
33
+ const threadId = generateId();
34
+ const now = date || new Date().toISOString();
35
+ const internalDate = String(new Date(now).getTime());
36
+ const snippet = (body || '').slice(0, 100);
37
+ const msgLabels = labels || ['INBOX', 'UNREAD'];
38
+ const userEmail = store.gmail.profile.emailAddress;
39
+
40
+ store.gmail.messages[id] = {
41
+ id,
42
+ threadId,
43
+ labelIds: msgLabels,
44
+ snippet,
45
+ historyId: String(store.gmail.nextHistoryId++),
46
+ internalDate,
47
+ sizeEstimate: (body || '').length,
48
+ payload: {
49
+ partId: '',
50
+ mimeType: 'text/plain',
51
+ filename: '',
52
+ headers: [
53
+ { name: 'From', value: from || 'unknown@example.com' },
54
+ { name: 'To', value: to || userEmail },
55
+ { name: 'Subject', value: subject || '(no subject)' },
56
+ { name: 'Date', value: now },
57
+ ],
58
+ body: {
59
+ size: (body || '').length,
60
+ data: Buffer.from(body || '').toString('base64url'),
61
+ },
62
+ },
63
+ };
64
+ store.gmail.profile.messagesTotal++;
65
+ store.gmail.profile.threadsTotal++;
66
+
67
+ res.json({ id, threadId });
68
+ });
69
+
70
+ r.post('/__fws/setup/calendar/event', (req, res) => {
71
+ const store = getStore();
72
+ const { summary, description, location, start, duration, calendar, attendees } = req.body;
73
+ const calendarId = calendar || Object.keys(store.calendar.calendars)[0];
74
+ if (!store.calendar.events[calendarId]) {
75
+ store.calendar.events[calendarId] = {};
76
+ }
77
+
78
+ const id = generateId();
79
+ const now = new Date().toISOString();
80
+ const startDate = new Date(start);
81
+ const durationMs = parseDuration(duration || '1h');
82
+ const endDate = new Date(startDate.getTime() + durationMs);
83
+ const userEmail = store.gmail.profile.emailAddress;
84
+
85
+ const event = {
86
+ kind: 'calendar#event' as const,
87
+ id,
88
+ status: 'confirmed' as const,
89
+ summary: summary || '(no title)',
90
+ description,
91
+ location,
92
+ start: { dateTime: startDate.toISOString() },
93
+ end: { dateTime: endDate.toISOString() },
94
+ created: now,
95
+ updated: now,
96
+ creator: { email: userEmail },
97
+ organizer: { email: userEmail, self: true },
98
+ attendees: attendees
99
+ ? attendees.map((email: string) => ({ email, responseStatus: 'needsAction' }))
100
+ : undefined,
101
+ etag: generateEtag(),
102
+ htmlLink: `https://calendar.google.com/event?eid=${id}`,
103
+ iCalUID: `${id}@example.com`,
104
+ };
105
+
106
+ store.calendar.events[calendarId][id] = event;
107
+ res.json({ id, calendarId });
108
+ });
109
+
110
+ r.post('/__fws/setup/drive/file', (req, res) => {
111
+ const store = getStore();
112
+ const { name, mimeType, parent, size, description } = req.body;
113
+ const id = generateId();
114
+ const now = new Date().toISOString();
115
+ const userEmail = store.gmail.profile.emailAddress;
116
+
117
+ store.drive.files[id] = {
118
+ kind: 'drive#file',
119
+ id,
120
+ name: name || 'Untitled',
121
+ mimeType: mimeType || 'application/octet-stream',
122
+ parents: parent ? [parent] : ['root'],
123
+ createdTime: now,
124
+ modifiedTime: now,
125
+ size: size ? String(size) : undefined,
126
+ trashed: false,
127
+ starred: false,
128
+ owners: [{ emailAddress: userEmail, displayName: 'Test User' }],
129
+ description,
130
+ };
131
+
132
+ res.json({ id });
133
+ });
134
+
135
+ return r;
136
+ }
137
+
138
+ function parseDuration(d: string): number {
139
+ const match = d.match(/^(\d+)(ms|s|m|h|d)$/);
140
+ if (!match) return 3600000; // default 1h
141
+ const val = parseInt(match[1]);
142
+ const unit = match[2];
143
+ switch (unit) {
144
+ case 'ms': return val;
145
+ case 's': return val * 1000;
146
+ case 'm': return val * 60000;
147
+ case 'h': return val * 3600000;
148
+ case 'd': return val * 86400000;
149
+ default: return 3600000;
150
+ }
151
+ }