dav-mcp 3.0.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 +45 -0
- package/LICENSE +21 -0
- package/README.md +260 -0
- package/package.json +80 -0
- package/src/error-handler.js +215 -0
- package/src/formatters.js +754 -0
- package/src/logger.js +144 -0
- package/src/server-http.js +402 -0
- package/src/server-stdio.js +225 -0
- package/src/tool-call-logger.js +148 -0
- package/src/tools/calendar/calendar-multi-get.js +38 -0
- package/src/tools/calendar/calendar-query.js +98 -0
- package/src/tools/calendar/create-event.js +79 -0
- package/src/tools/calendar/delete-calendar.js +36 -0
- package/src/tools/calendar/delete-event.js +38 -0
- package/src/tools/calendar/index.js +16 -0
- package/src/tools/calendar/list-calendars.js +21 -0
- package/src/tools/calendar/list-events.js +43 -0
- package/src/tools/calendar/make-calendar.js +80 -0
- package/src/tools/calendar/update-calendar.js +106 -0
- package/src/tools/calendar/update-event-fields.js +119 -0
- package/src/tools/calendar/update-event-raw.js +45 -0
- package/src/tools/contacts/addressbook-multi-get.js +38 -0
- package/src/tools/contacts/addressbook-query.js +85 -0
- package/src/tools/contacts/create-contact.js +84 -0
- package/src/tools/contacts/delete-contact.js +38 -0
- package/src/tools/contacts/index.js +13 -0
- package/src/tools/contacts/list-addressbooks.js +21 -0
- package/src/tools/contacts/list-contacts.js +32 -0
- package/src/tools/contacts/update-contact-fields.js +135 -0
- package/src/tools/contacts/update-contact-raw.js +45 -0
- package/src/tools/index.js +57 -0
- package/src/tools/shared/helpers.js +132 -0
- package/src/tools/todos/create-todo.js +101 -0
- package/src/tools/todos/delete-todo.js +38 -0
- package/src/tools/todos/index.js +12 -0
- package/src/tools/todos/list-todos.js +30 -0
- package/src/tools/todos/todo-multi-get.js +37 -0
- package/src/tools/todos/todo-query.js +112 -0
- package/src/tools/todos/update-todo-fields.js +119 -0
- package/src/tools/todos/update-todo-raw.js +46 -0
- package/src/tsdav-client.js +199 -0
- package/src/utils/tool-helpers.js +388 -0
- package/src/validation.js +245 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { tsdavManager } from '../../tsdav-client.js';
|
|
2
|
+
import { validateInput, todoQuerySchema } from '../../validation.js';
|
|
3
|
+
import { formatTodoList } from '../../formatters.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Search and filter todos efficiently
|
|
7
|
+
*/
|
|
8
|
+
export const todoQuery = {
|
|
9
|
+
name: 'todo_query',
|
|
10
|
+
description: '⭐ PREFERRED: Search and filter todos efficiently. Use instead of list_todos to conserve tokens. Omit calendar_url to search across ALL calendars automatically.',
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
calendar_url: {
|
|
15
|
+
type: 'string',
|
|
16
|
+
description: 'Optional: Specific calendar URL. Omit to search ALL calendars (recommended).',
|
|
17
|
+
},
|
|
18
|
+
summary_filter: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description: 'Search todo summaries/titles containing this text (case-insensitive). Example: "write report" or "review PR". Can be used alone as sufficient filter.',
|
|
21
|
+
},
|
|
22
|
+
status_filter: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
enum: ['NEEDS-ACTION', 'IN-PROCESS', 'COMPLETED', 'CANCELLED'],
|
|
25
|
+
description: 'Filter by todo status. Use "NEEDS-ACTION" for pending tasks, "COMPLETED" for done tasks. Can be used alone as sufficient filter.',
|
|
26
|
+
},
|
|
27
|
+
time_range_start: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
description: 'Start datetime for due date filtering (ISO 8601). If provided, time_range_end is REQUIRED. Both dates together form a complete filter.',
|
|
30
|
+
},
|
|
31
|
+
time_range_end: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
description: 'End datetime for due date filtering (ISO 8601). If provided, time_range_start is REQUIRED. Both dates together form a complete filter.',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
required: [],
|
|
37
|
+
},
|
|
38
|
+
handler: async (args) => {
|
|
39
|
+
const validated = validateInput(todoQuerySchema, args);
|
|
40
|
+
const client = tsdavManager.getCalDavClient();
|
|
41
|
+
const calendars = await client.fetchCalendars();
|
|
42
|
+
|
|
43
|
+
// If specific calendar requested, use it
|
|
44
|
+
let calendarsToSearch = calendars;
|
|
45
|
+
if (validated.calendar_url) {
|
|
46
|
+
const calendar = calendars.find(c => c.url === validated.calendar_url);
|
|
47
|
+
if (!calendar) {
|
|
48
|
+
const availableUrls = calendars.map(c => c.url).join('\n- ');
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Calendar not found: ${validated.calendar_url}\n\n` +
|
|
51
|
+
`Available calendar URLs:\n- ${availableUrls}\n\n` +
|
|
52
|
+
`Tip: Omit calendar_url to search across all calendars automatically.`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
calendarsToSearch = [calendar];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Fetch todos from all selected calendars
|
|
59
|
+
let todos = [];
|
|
60
|
+
for (const calendar of calendarsToSearch) {
|
|
61
|
+
const calendarTodos = await client.fetchTodos({ calendar });
|
|
62
|
+
// Add calendar info to each todo
|
|
63
|
+
calendarTodos.forEach(todo => {
|
|
64
|
+
todo._calendarName = calendar.displayName || calendar.url;
|
|
65
|
+
});
|
|
66
|
+
todos = todos.concat(calendarTodos);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Client-side filtering (tsdav doesn't support server-side VTODO filtering yet)
|
|
70
|
+
if (validated.summary_filter) {
|
|
71
|
+
const summaryLower = validated.summary_filter.toLowerCase();
|
|
72
|
+
todos = todos.filter(todo => {
|
|
73
|
+
const summary = todo.data?.match(/SUMMARY:(.+)/)?.[1] || '';
|
|
74
|
+
return summary.toLowerCase().includes(summaryLower);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (validated.status_filter) {
|
|
79
|
+
todos = todos.filter(todo => {
|
|
80
|
+
const status = todo.data?.match(/STATUS:(.+)/)?.[1] || 'NEEDS-ACTION';
|
|
81
|
+
return status === validated.status_filter;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (validated.time_range_start && validated.time_range_end) {
|
|
86
|
+
const startTime = new Date(validated.time_range_start).getTime();
|
|
87
|
+
const endTime = new Date(validated.time_range_end).getTime();
|
|
88
|
+
|
|
89
|
+
todos = todos.filter(todo => {
|
|
90
|
+
const dueMatch = todo.data?.match(/DUE:(\d{8}T\d{6}Z?)/);
|
|
91
|
+
if (!dueMatch) return false;
|
|
92
|
+
|
|
93
|
+
const dueStr = dueMatch[1];
|
|
94
|
+
const year = parseInt(dueStr.substr(0, 4));
|
|
95
|
+
const month = parseInt(dueStr.substr(4, 2)) - 1;
|
|
96
|
+
const day = parseInt(dueStr.substr(6, 2));
|
|
97
|
+
const hour = parseInt(dueStr.substr(9, 2));
|
|
98
|
+
const minute = parseInt(dueStr.substr(11, 2));
|
|
99
|
+
const dueTime = new Date(Date.UTC(year, month, day, hour, minute)).getTime();
|
|
100
|
+
|
|
101
|
+
return dueTime >= startTime && dueTime <= endTime;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Determine calendar name for display
|
|
106
|
+
const calendarName = calendarsToSearch.length === 1
|
|
107
|
+
? (calendarsToSearch[0].displayName || calendarsToSearch[0].url)
|
|
108
|
+
: `All Calendars (${calendarsToSearch.length})`;
|
|
109
|
+
|
|
110
|
+
return formatTodoList(todos, calendarName);
|
|
111
|
+
},
|
|
112
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { tsdavManager } from '../../tsdav-client.js';
|
|
2
|
+
import { validateInput } from '../../validation.js';
|
|
3
|
+
import { formatSuccess, formatError } from '../../formatters.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { updateFields } from 'tsdav-utils';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Schema for field-based todo updates
|
|
9
|
+
* Supports all RFC 5545 VTODO properties via tsdav-utils
|
|
10
|
+
* Common fields: SUMMARY, DESCRIPTION, STATUS, PRIORITY, DUE, PERCENT-COMPLETE
|
|
11
|
+
* Custom properties: Any X-* property
|
|
12
|
+
*/
|
|
13
|
+
const updateTodoFieldsSchema = z.object({
|
|
14
|
+
todo_url: z.string().url('Todo URL must be a valid URL'),
|
|
15
|
+
todo_etag: z.string().min(1, 'Todo etag is required'),
|
|
16
|
+
fields: z.record(z.string()).optional()
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Field-agnostic todo update tool powered by tsdav-utils
|
|
21
|
+
* Supports all RFC 5545 VTODO properties without validation
|
|
22
|
+
*
|
|
23
|
+
* Features:
|
|
24
|
+
* - Any standard VTODO property (SUMMARY, DESCRIPTION, STATUS, PRIORITY, DUE, etc.)
|
|
25
|
+
* - Custom X-* properties for extensions
|
|
26
|
+
* - Field-agnostic: no pre-defined field list required
|
|
27
|
+
*/
|
|
28
|
+
export const updateTodoFields = {
|
|
29
|
+
name: 'update_todo',
|
|
30
|
+
description: 'PREFERRED: Update todo fields without iCal formatting. Supports: SUMMARY (title), DESCRIPTION (details), STATUS (NEEDS-ACTION/IN-PROCESS/COMPLETED/CANCELLED), PRIORITY (0-9), DUE (due date), PERCENT-COMPLETE (0-100), and any RFC 5545 VTODO property including custom X-* properties.',
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
properties: {
|
|
34
|
+
todo_url: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
description: 'The URL of the todo to update'
|
|
37
|
+
},
|
|
38
|
+
todo_etag: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
description: 'The etag of the todo (required for conflict detection)'
|
|
41
|
+
},
|
|
42
|
+
fields: {
|
|
43
|
+
type: 'object',
|
|
44
|
+
description: 'Fields to update - use UPPERCASE property names (e.g., SUMMARY, STATUS, PRIORITY). Any RFC 5545 VTODO property or custom X-* property is supported.',
|
|
45
|
+
additionalProperties: {
|
|
46
|
+
type: 'string'
|
|
47
|
+
},
|
|
48
|
+
properties: {
|
|
49
|
+
SUMMARY: {
|
|
50
|
+
type: 'string',
|
|
51
|
+
description: 'Todo title/summary'
|
|
52
|
+
},
|
|
53
|
+
DESCRIPTION: {
|
|
54
|
+
type: 'string',
|
|
55
|
+
description: 'Todo description/details'
|
|
56
|
+
},
|
|
57
|
+
STATUS: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
description: 'Todo status: NEEDS-ACTION, IN-PROCESS, COMPLETED, or CANCELLED'
|
|
60
|
+
},
|
|
61
|
+
PRIORITY: {
|
|
62
|
+
type: 'string',
|
|
63
|
+
description: 'Priority level: 0 (undefined), 1 (highest) to 9 (lowest)'
|
|
64
|
+
},
|
|
65
|
+
DUE: {
|
|
66
|
+
type: 'string',
|
|
67
|
+
description: 'Due date (ISO 8601 or iCal format: 20250128T100000Z)'
|
|
68
|
+
},
|
|
69
|
+
'PERCENT-COMPLETE': {
|
|
70
|
+
type: 'string',
|
|
71
|
+
description: 'Completion percentage: 0-100'
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
required: ['todo_url', 'todo_etag']
|
|
77
|
+
},
|
|
78
|
+
handler: async (args) => {
|
|
79
|
+
try {
|
|
80
|
+
const validated = validateInput(updateTodoFieldsSchema, args);
|
|
81
|
+
const client = tsdavManager.getCalDavClient();
|
|
82
|
+
|
|
83
|
+
// Step 1: Fetch the current todo from server
|
|
84
|
+
const calendarUrl = validated.todo_url.substring(0, validated.todo_url.lastIndexOf('/') + 1);
|
|
85
|
+
const currentTodos = await client.fetchTodos({
|
|
86
|
+
calendar: { url: calendarUrl },
|
|
87
|
+
objectUrls: [validated.todo_url]
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (!currentTodos || currentTodos.length === 0) {
|
|
91
|
+
throw new Error('Todo not found');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const todoObject = currentTodos[0];
|
|
95
|
+
|
|
96
|
+
// Step 2: Update fields using tsdav-utils (field-agnostic)
|
|
97
|
+
// Accepts any RFC 5545 VTODO property name (UPPERCASE)
|
|
98
|
+
const updatedData = updateFields(todoObject, validated.fields || {});
|
|
99
|
+
|
|
100
|
+
// Step 3: Send the updated todo back to server
|
|
101
|
+
const updateResponse = await client.updateTodo({
|
|
102
|
+
todo: {
|
|
103
|
+
url: validated.todo_url,
|
|
104
|
+
data: updatedData,
|
|
105
|
+
etag: validated.todo_etag
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return formatSuccess('Todo updated successfully', {
|
|
110
|
+
etag: updateResponse.etag,
|
|
111
|
+
updated_fields: Object.keys(validated.fields || {}),
|
|
112
|
+
message: `Updated ${Object.keys(validated.fields || {}).length} field(s): ${Object.keys(validated.fields || {}).join(', ')}`
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return formatError('update_todo', error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { tsdavManager } from '../../tsdav-client.js';
|
|
2
|
+
import { validateInput, updateTodoSchema } from '../../validation.js';
|
|
3
|
+
import { formatSuccess } from '../../formatters.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Update an existing todo/task with raw VTODO iCal data
|
|
7
|
+
*/
|
|
8
|
+
export const updateTodoRaw = {
|
|
9
|
+
name: 'update_todo_raw',
|
|
10
|
+
description: 'ADVANCED: Update todo with raw VTODO iCal data. Requires manual iCal formatting - use update_todo instead for simple field updates (summary, description, status). Only use this if you have complete pre-formatted VTODO data or need to update advanced iCal properties.',
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
todo_url: {
|
|
15
|
+
type: 'string',
|
|
16
|
+
description: 'The URL of the todo to update',
|
|
17
|
+
},
|
|
18
|
+
todo_etag: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description: 'The current ETag of the todo (required for conflict detection)',
|
|
21
|
+
},
|
|
22
|
+
updated_ical_data: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
description: 'Complete updated VTODO iCalendar data',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
required: ['todo_url', 'todo_etag', 'updated_ical_data'],
|
|
28
|
+
},
|
|
29
|
+
handler: async (args) => {
|
|
30
|
+
const validated = validateInput(updateTodoSchema, args);
|
|
31
|
+
const client = tsdavManager.getCalDavClient();
|
|
32
|
+
|
|
33
|
+
const result = await client.updateTodo({
|
|
34
|
+
todo: {
|
|
35
|
+
url: validated.todo_url,
|
|
36
|
+
data: validated.updated_ical_data,
|
|
37
|
+
etag: validated.todo_etag,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return formatSuccess('Todo updated successfully', {
|
|
42
|
+
url: result.url,
|
|
43
|
+
etag: result.etag,
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { DAVClient } from 'tsdav';
|
|
2
|
+
import { logger } from './logger.js';
|
|
3
|
+
import { CalDAVError, CardDAVError } from './error-handler.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Singleton CalDAV/CardDAV Client Manager
|
|
7
|
+
*
|
|
8
|
+
* Supports both Basic Auth and OAuth2 authentication:
|
|
9
|
+
* - Basic Auth: Standard CalDAV servers (Radicale, Baikal, Nextcloud)
|
|
10
|
+
* - OAuth2: Google Calendar and other OAuth2-enabled CalDAV servers
|
|
11
|
+
*/
|
|
12
|
+
class TsdavClientManager {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.calDavClient = null;
|
|
15
|
+
this.cardDavClient = null;
|
|
16
|
+
this.config = null;
|
|
17
|
+
this.authMethod = null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Initialize clients with configuration
|
|
22
|
+
*
|
|
23
|
+
* @param {Object} config - Client configuration
|
|
24
|
+
* @param {string} config.serverUrl - CalDAV/CardDAV server URL
|
|
25
|
+
* @param {string} config.authMethod - 'Basic' or 'OAuth' (note: tsdav uses 'Oauth')
|
|
26
|
+
*
|
|
27
|
+
* For Basic Auth:
|
|
28
|
+
* @param {string} config.username - Username
|
|
29
|
+
* @param {string} config.password - Password
|
|
30
|
+
*
|
|
31
|
+
* For OAuth2:
|
|
32
|
+
* @param {string} config.username - User email (for OAuth2)
|
|
33
|
+
* @param {string} config.clientId - OAuth2 client ID
|
|
34
|
+
* @param {string} config.clientSecret - OAuth2 client secret
|
|
35
|
+
* @param {string} config.refreshToken - OAuth2 refresh token
|
|
36
|
+
* @param {string} config.tokenUrl - OAuth2 token endpoint (default: Google's)
|
|
37
|
+
*/
|
|
38
|
+
async initialize(config) {
|
|
39
|
+
this.config = config;
|
|
40
|
+
this.authMethod = config.authMethod || 'Basic';
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// Determine authentication method
|
|
44
|
+
const useOAuth = this.authMethod === 'OAuth' || this.authMethod === 'Oauth';
|
|
45
|
+
|
|
46
|
+
if (useOAuth) {
|
|
47
|
+
logger.info({ serverUrl: config.serverUrl }, 'Initializing tsdav clients with OAuth2');
|
|
48
|
+
await this._initializeOAuth(config);
|
|
49
|
+
} else {
|
|
50
|
+
logger.info({ serverUrl: config.serverUrl }, 'Initializing tsdav clients with Basic Auth');
|
|
51
|
+
await this._initializeBasicAuth(config);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
logger.info({
|
|
55
|
+
serverUrl: config.serverUrl,
|
|
56
|
+
authMethod: this.authMethod
|
|
57
|
+
}, 'tsdav clients initialized and logged in');
|
|
58
|
+
} catch (error) {
|
|
59
|
+
logger.error({
|
|
60
|
+
error: error.message,
|
|
61
|
+
serverUrl: config.serverUrl,
|
|
62
|
+
authMethod: this.authMethod
|
|
63
|
+
}, 'Failed to initialize tsdav clients');
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Initialize clients with Basic Authentication
|
|
70
|
+
* @private
|
|
71
|
+
*/
|
|
72
|
+
async _initializeBasicAuth(config) {
|
|
73
|
+
// Validate required fields
|
|
74
|
+
if (!config.username || !config.password) {
|
|
75
|
+
throw new Error('Basic Auth requires username and password');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// CalDAV Client
|
|
79
|
+
this.calDavClient = new DAVClient({
|
|
80
|
+
serverUrl: config.serverUrl,
|
|
81
|
+
credentials: {
|
|
82
|
+
username: config.username,
|
|
83
|
+
password: config.password,
|
|
84
|
+
},
|
|
85
|
+
authMethod: 'Basic',
|
|
86
|
+
defaultAccountType: 'caldav',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// CardDAV Client
|
|
90
|
+
this.cardDavClient = new DAVClient({
|
|
91
|
+
serverUrl: config.serverUrl,
|
|
92
|
+
credentials: {
|
|
93
|
+
username: config.username,
|
|
94
|
+
password: config.password,
|
|
95
|
+
},
|
|
96
|
+
authMethod: 'Basic',
|
|
97
|
+
defaultAccountType: 'carddav',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Login to both clients
|
|
101
|
+
await this.calDavClient.login();
|
|
102
|
+
logger.debug({ accountType: 'caldav' }, 'CalDAV client logged in (Basic Auth)');
|
|
103
|
+
|
|
104
|
+
await this.cardDavClient.login();
|
|
105
|
+
logger.debug({ accountType: 'carddav' }, 'CardDAV client logged in (Basic Auth)');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Initialize clients with OAuth2 Authentication
|
|
110
|
+
* @private
|
|
111
|
+
*/
|
|
112
|
+
async _initializeOAuth(config) {
|
|
113
|
+
// Validate required OAuth fields
|
|
114
|
+
if (!config.username) {
|
|
115
|
+
throw new Error('OAuth requires username (user email)');
|
|
116
|
+
}
|
|
117
|
+
if (!config.clientId || !config.clientSecret || !config.refreshToken) {
|
|
118
|
+
throw new Error('OAuth requires clientId, clientSecret, and refreshToken');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Default to Google's token endpoint if not specified
|
|
122
|
+
const tokenUrl = config.tokenUrl || 'https://accounts.google.com/o/oauth2/token';
|
|
123
|
+
|
|
124
|
+
const oauthCredentials = {
|
|
125
|
+
tokenUrl,
|
|
126
|
+
username: config.username,
|
|
127
|
+
refreshToken: config.refreshToken,
|
|
128
|
+
clientId: config.clientId,
|
|
129
|
+
clientSecret: config.clientSecret,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
logger.debug({
|
|
133
|
+
username: config.username,
|
|
134
|
+
tokenUrl,
|
|
135
|
+
serverUrl: config.serverUrl
|
|
136
|
+
}, 'Configuring OAuth2 credentials');
|
|
137
|
+
|
|
138
|
+
// CalDAV Client with OAuth
|
|
139
|
+
this.calDavClient = new DAVClient({
|
|
140
|
+
serverUrl: config.serverUrl,
|
|
141
|
+
credentials: oauthCredentials,
|
|
142
|
+
authMethod: 'Oauth', // Note: tsdav expects 'Oauth' with capital O
|
|
143
|
+
defaultAccountType: 'caldav',
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// CardDAV Client with OAuth
|
|
147
|
+
// Note: Google Calendar doesn't support CardDAV, but we initialize it anyway
|
|
148
|
+
// for compatibility with other OAuth2 CalDAV/CardDAV servers
|
|
149
|
+
this.cardDavClient = new DAVClient({
|
|
150
|
+
serverUrl: config.serverUrl,
|
|
151
|
+
credentials: oauthCredentials,
|
|
152
|
+
authMethod: 'Oauth',
|
|
153
|
+
defaultAccountType: 'carddav',
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Login to CalDAV client
|
|
157
|
+
await this.calDavClient.login();
|
|
158
|
+
logger.debug({ accountType: 'caldav' }, 'CalDAV client logged in (OAuth2)');
|
|
159
|
+
|
|
160
|
+
// Try to login to CardDAV client, but don't fail if it doesn't work
|
|
161
|
+
// (Google Calendar doesn't support CardDAV)
|
|
162
|
+
try {
|
|
163
|
+
await this.cardDavClient.login();
|
|
164
|
+
logger.debug({ accountType: 'carddav' }, 'CardDAV client logged in (OAuth2)');
|
|
165
|
+
} catch (error) {
|
|
166
|
+
logger.warn({
|
|
167
|
+
error: error.message
|
|
168
|
+
}, 'CardDAV login failed (expected for Google Calendar)');
|
|
169
|
+
// Don't throw - CardDAV is optional for OAuth2 providers like Google
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get CalDAV client
|
|
175
|
+
*/
|
|
176
|
+
getCalDavClient() {
|
|
177
|
+
if (!this.calDavClient) {
|
|
178
|
+
const error = new CalDAVError('CalDAV client not initialized. Call initialize() first.');
|
|
179
|
+
logger.error('CalDAV client not initialized');
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
return this.calDavClient;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get CardDAV client
|
|
187
|
+
*/
|
|
188
|
+
getCardDavClient() {
|
|
189
|
+
if (!this.cardDavClient) {
|
|
190
|
+
const error = new CardDAVError('CardDAV client not initialized. Call initialize() first.');
|
|
191
|
+
logger.error('CardDAV client not initialized');
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
return this.cardDavClient;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Export singleton instance
|
|
199
|
+
export const tsdavManager = new TsdavClientManager();
|