m365-agent-cli 1.2.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/LICENSE +22 -0
- package/README.md +916 -0
- package/package.json +50 -0
- package/src/cli.ts +100 -0
- package/src/commands/auto-reply.ts +182 -0
- package/src/commands/calendar.ts +576 -0
- package/src/commands/counter.ts +87 -0
- package/src/commands/create-event.ts +544 -0
- package/src/commands/delegates.ts +286 -0
- package/src/commands/delete-event.ts +321 -0
- package/src/commands/drafts.ts +502 -0
- package/src/commands/files.ts +532 -0
- package/src/commands/find.ts +195 -0
- package/src/commands/findtime.ts +270 -0
- package/src/commands/folders.ts +177 -0
- package/src/commands/forward-event.ts +49 -0
- package/src/commands/graph-calendar.ts +217 -0
- package/src/commands/login.ts +195 -0
- package/src/commands/mail.ts +950 -0
- package/src/commands/oof.ts +263 -0
- package/src/commands/outlook-categories.ts +173 -0
- package/src/commands/outlook-graph.ts +880 -0
- package/src/commands/planner.ts +1678 -0
- package/src/commands/respond.ts +291 -0
- package/src/commands/rooms.ts +210 -0
- package/src/commands/rules.ts +511 -0
- package/src/commands/schedule.ts +109 -0
- package/src/commands/send.ts +204 -0
- package/src/commands/serve.ts +14 -0
- package/src/commands/sharepoint.ts +179 -0
- package/src/commands/site-pages.ts +163 -0
- package/src/commands/subscribe.ts +103 -0
- package/src/commands/subscriptions.ts +29 -0
- package/src/commands/suggest.ts +155 -0
- package/src/commands/todo.ts +2092 -0
- package/src/commands/update-event.ts +608 -0
- package/src/commands/update.ts +88 -0
- package/src/commands/verify-token.ts +62 -0
- package/src/commands/whoami.ts +74 -0
- package/src/index.ts +190 -0
- package/src/lib/atomic-write.ts +20 -0
- package/src/lib/attach-link-spec.test.ts +24 -0
- package/src/lib/attach-link-spec.ts +70 -0
- package/src/lib/attachments.ts +79 -0
- package/src/lib/auth.ts +192 -0
- package/src/lib/calendar-range.test.ts +41 -0
- package/src/lib/calendar-range.ts +103 -0
- package/src/lib/dates.test.ts +74 -0
- package/src/lib/dates.ts +137 -0
- package/src/lib/delegate-client.test.ts +74 -0
- package/src/lib/delegate-client.ts +322 -0
- package/src/lib/ews-client.ts +3418 -0
- package/src/lib/git-commit.ts +4 -0
- package/src/lib/glitchtip-eligibility.ts +220 -0
- package/src/lib/glitchtip.ts +253 -0
- package/src/lib/global-env.ts +3 -0
- package/src/lib/graph-auth.ts +223 -0
- package/src/lib/graph-calendar-client.test.ts +118 -0
- package/src/lib/graph-calendar-client.ts +112 -0
- package/src/lib/graph-client.test.ts +107 -0
- package/src/lib/graph-client.ts +1058 -0
- package/src/lib/graph-constants.ts +12 -0
- package/src/lib/graph-directory.ts +116 -0
- package/src/lib/graph-event.ts +134 -0
- package/src/lib/graph-schedule.ts +173 -0
- package/src/lib/graph-subscriptions.ts +94 -0
- package/src/lib/graph-user-path.ts +13 -0
- package/src/lib/jwt-utils.ts +34 -0
- package/src/lib/markdown.test.ts +21 -0
- package/src/lib/markdown.ts +174 -0
- package/src/lib/mime-type.ts +106 -0
- package/src/lib/oof-client.test.ts +59 -0
- package/src/lib/oof-client.ts +122 -0
- package/src/lib/outlook-graph-client.test.ts +146 -0
- package/src/lib/outlook-graph-client.ts +649 -0
- package/src/lib/outlook-master-categories.ts +145 -0
- package/src/lib/package-info.ts +59 -0
- package/src/lib/places-client.ts +144 -0
- package/src/lib/planner-client.ts +1226 -0
- package/src/lib/rules-client.ts +178 -0
- package/src/lib/sharepoint-client.ts +101 -0
- package/src/lib/site-pages-client.ts +73 -0
- package/src/lib/todo-client.test.ts +298 -0
- package/src/lib/todo-client.ts +1309 -0
- package/src/lib/url-validation.ts +40 -0
- package/src/lib/utils.ts +45 -0
- package/src/lib/webhook-server.ts +51 -0
- package/src/test/auth.test.ts +104 -0
- package/src/test/cli.integration.test.ts +1083 -0
- package/src/test/ews-client.test.ts +268 -0
- package/src/test/mocks/index.ts +375 -0
- package/src/test/mocks/responses.ts +861 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { validateUrl } from './url-validation';
|
|
2
|
+
|
|
3
|
+
export const GRAPH_BASE_URL = validateUrl(
|
|
4
|
+
process.env.GRAPH_BASE_URL || 'https://graph.microsoft.com/v1.0',
|
|
5
|
+
'GRAPH_BASE_URL'
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
/** Microsoft Graph beta root (Planner favorites, roster, delta — subject to change). */
|
|
9
|
+
export const GRAPH_BETA_URL = validateUrl(
|
|
10
|
+
process.env.GRAPH_BETA_URL || 'https://graph.microsoft.com/beta',
|
|
11
|
+
'GRAPH_BETA_URL'
|
|
12
|
+
);
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { callGraph, fetchAllPages, GraphApiError, type GraphResponse, graphError } from './graph-client.js';
|
|
2
|
+
|
|
3
|
+
export interface Person {
|
|
4
|
+
id: string;
|
|
5
|
+
displayName: string;
|
|
6
|
+
givenName?: string;
|
|
7
|
+
surname?: string;
|
|
8
|
+
jobTitle?: string;
|
|
9
|
+
scoredEmailAddresses?: { address: string; name?: string }[];
|
|
10
|
+
userPrincipalName?: string;
|
|
11
|
+
department?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface User {
|
|
15
|
+
id: string;
|
|
16
|
+
displayName: string;
|
|
17
|
+
givenName?: string;
|
|
18
|
+
surname?: string;
|
|
19
|
+
jobTitle?: string;
|
|
20
|
+
mail?: string;
|
|
21
|
+
userPrincipalName?: string;
|
|
22
|
+
department?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface Group {
|
|
26
|
+
id: string;
|
|
27
|
+
displayName: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
mail?: string;
|
|
30
|
+
groupTypes?: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function searchPeople(token: string, query: string): Promise<GraphResponse<Person[]>> {
|
|
34
|
+
const escapedQuery = query.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
35
|
+
const searchParam = encodeURIComponent(`"${escapedQuery}"`);
|
|
36
|
+
let result: GraphResponse<{ value: Person[] }>;
|
|
37
|
+
try {
|
|
38
|
+
result = await callGraph<{ value: Person[] }>(token, `/me/people?$search=${searchParam}`);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (err instanceof GraphApiError) {
|
|
41
|
+
return graphError(err.message, err.code, err.status);
|
|
42
|
+
}
|
|
43
|
+
return graphError(err instanceof Error ? err.message : 'Failed to search people');
|
|
44
|
+
}
|
|
45
|
+
if (!result.ok || !result.data) {
|
|
46
|
+
return { ok: false, error: result.error };
|
|
47
|
+
}
|
|
48
|
+
return { ok: true, data: result.data.value };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function searchUsers(token: string, query: string): Promise<GraphResponse<User[]>> {
|
|
52
|
+
const escapedQuery = query.replace(/'/g, "''");
|
|
53
|
+
const filter = encodeURIComponent(`startswith(displayName,'${escapedQuery}')`);
|
|
54
|
+
let result: GraphResponse<{ value: User[] }>;
|
|
55
|
+
try {
|
|
56
|
+
result = await callGraph<{ value: User[] }>(token, `/users?$filter=${filter}&$count=true`, {
|
|
57
|
+
headers: {
|
|
58
|
+
ConsistencyLevel: 'eventual'
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
} catch (err) {
|
|
62
|
+
if (err instanceof GraphApiError) {
|
|
63
|
+
return graphError(err.message, err.code, err.status);
|
|
64
|
+
}
|
|
65
|
+
return graphError(err instanceof Error ? err.message : 'Failed to search users');
|
|
66
|
+
}
|
|
67
|
+
if (!result.ok || !result.data) {
|
|
68
|
+
return { ok: false, error: result.error };
|
|
69
|
+
}
|
|
70
|
+
return { ok: true, data: result.data.value };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function searchGroups(token: string, query: string): Promise<GraphResponse<Group[]>> {
|
|
74
|
+
const escapedQuery = query.replace(/'/g, "''");
|
|
75
|
+
const filter = encodeURIComponent(`startswith(displayName,'${escapedQuery}')`);
|
|
76
|
+
let result: GraphResponse<{ value: Group[] }>;
|
|
77
|
+
try {
|
|
78
|
+
result = await callGraph<{ value: Group[] }>(token, `/groups?$filter=${filter}&$count=true`, {
|
|
79
|
+
headers: {
|
|
80
|
+
ConsistencyLevel: 'eventual'
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
} catch (err) {
|
|
84
|
+
if (err instanceof GraphApiError) {
|
|
85
|
+
return graphError(err.message, err.code, err.status);
|
|
86
|
+
}
|
|
87
|
+
return graphError(err instanceof Error ? err.message : 'Failed to search groups');
|
|
88
|
+
}
|
|
89
|
+
if (!result.ok || !result.data) {
|
|
90
|
+
return { ok: false, error: result.error };
|
|
91
|
+
}
|
|
92
|
+
return { ok: true, data: result.data.value };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function expandGroup(token: string, groupId: string): Promise<GraphResponse<User[]>> {
|
|
96
|
+
const result = await fetchAllPages<any>(
|
|
97
|
+
token,
|
|
98
|
+
`/groups/${encodeURIComponent(groupId)}/members?$top=100`,
|
|
99
|
+
'Failed to expand group'
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if (!result.ok || !result.data) {
|
|
103
|
+
return { ok: false, error: result.error };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const userMembers = result.data.filter((member: any) => {
|
|
107
|
+
const odataType = member['@odata.type'];
|
|
108
|
+
return (
|
|
109
|
+
(typeof odataType === 'string' && odataType.toLowerCase().endsWith('.user')) ||
|
|
110
|
+
typeof member.mail === 'string' ||
|
|
111
|
+
typeof member.userPrincipalName === 'string'
|
|
112
|
+
);
|
|
113
|
+
}) as User[];
|
|
114
|
+
|
|
115
|
+
return { ok: true, data: userMembers };
|
|
116
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { callGraph, GraphApiError, type GraphResponse, graphError } from './graph-client.js';
|
|
2
|
+
import { graphUserPath } from './graph-user-path.js';
|
|
3
|
+
|
|
4
|
+
export interface ForwardEventOptions {
|
|
5
|
+
token: string;
|
|
6
|
+
eventId: string;
|
|
7
|
+
toRecipients: string[];
|
|
8
|
+
comment?: string;
|
|
9
|
+
/** User UPN or id for delegated calendar (omit for /me) */
|
|
10
|
+
user?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function forwardEvent(options: ForwardEventOptions): Promise<GraphResponse<void>> {
|
|
14
|
+
const { token, eventId, toRecipients, comment, user } = options;
|
|
15
|
+
|
|
16
|
+
const recipientsList = toRecipients.map((email) => ({
|
|
17
|
+
emailAddress: { address: email }
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
const body: { toRecipients: { emailAddress: { address: string } }[]; comment?: string } = {
|
|
21
|
+
toRecipients: recipientsList
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
if (comment) {
|
|
25
|
+
body.comment = comment;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
return await callGraph<void>(
|
|
30
|
+
token,
|
|
31
|
+
`${graphUserPath(user, `events/${encodeURIComponent(eventId)}/forward`)}`,
|
|
32
|
+
{
|
|
33
|
+
method: 'POST',
|
|
34
|
+
body: JSON.stringify(body)
|
|
35
|
+
},
|
|
36
|
+
false
|
|
37
|
+
);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
if (err instanceof GraphApiError) {
|
|
40
|
+
return graphError(err.message, err.code, err.status);
|
|
41
|
+
}
|
|
42
|
+
return graphError(err instanceof Error ? err.message : 'Failed to forward event');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ProposeNewTimeOptions {
|
|
47
|
+
token: string;
|
|
48
|
+
eventId: string;
|
|
49
|
+
startDateTime: string;
|
|
50
|
+
endDateTime: string;
|
|
51
|
+
timeZone?: string;
|
|
52
|
+
user?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function proposeNewTime(options: ProposeNewTimeOptions): Promise<GraphResponse<void>> {
|
|
56
|
+
const { token, eventId, startDateTime, endDateTime, timeZone = 'UTC', user } = options;
|
|
57
|
+
|
|
58
|
+
const body = {
|
|
59
|
+
proposedNewTime: {
|
|
60
|
+
start: { dateTime: startDateTime, timeZone },
|
|
61
|
+
end: { dateTime: endDateTime, timeZone }
|
|
62
|
+
},
|
|
63
|
+
sendResponse: true
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
return await callGraph<void>(
|
|
68
|
+
token,
|
|
69
|
+
`${graphUserPath(user, `events/${encodeURIComponent(eventId)}/tentativelyAccept`)}`,
|
|
70
|
+
{
|
|
71
|
+
method: 'POST',
|
|
72
|
+
body: JSON.stringify(body)
|
|
73
|
+
},
|
|
74
|
+
false
|
|
75
|
+
);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (err instanceof GraphApiError) {
|
|
78
|
+
return graphError(err.message, err.code, err.status);
|
|
79
|
+
}
|
|
80
|
+
return graphError(err instanceof Error ? err.message : 'Failed to propose new time');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface EventInvitationResponseOptions {
|
|
85
|
+
token: string;
|
|
86
|
+
eventId: string;
|
|
87
|
+
comment?: string;
|
|
88
|
+
/** Default true (send response to organizer). */
|
|
89
|
+
sendResponse?: boolean;
|
|
90
|
+
user?: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function acceptEventInvitation(options: EventInvitationResponseOptions): Promise<GraphResponse<void>> {
|
|
94
|
+
return postEventAction(options, 'accept');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function declineEventInvitation(options: EventInvitationResponseOptions): Promise<GraphResponse<void>> {
|
|
98
|
+
return postEventAction(options, 'decline');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Tentatively accept without proposing a new time (Graph POST .../tentativelyAccept). */
|
|
102
|
+
export async function tentativelyAcceptEventInvitation(
|
|
103
|
+
options: EventInvitationResponseOptions
|
|
104
|
+
): Promise<GraphResponse<void>> {
|
|
105
|
+
return postEventAction(options, 'tentativelyAccept');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function postEventAction(
|
|
109
|
+
options: EventInvitationResponseOptions,
|
|
110
|
+
action: 'accept' | 'decline' | 'tentativelyAccept'
|
|
111
|
+
): Promise<GraphResponse<void>> {
|
|
112
|
+
const { token, eventId, comment, user } = options;
|
|
113
|
+
const sendResponse = options.sendResponse !== false;
|
|
114
|
+
|
|
115
|
+
const body: { comment?: string; sendResponse: boolean } = { sendResponse };
|
|
116
|
+
if (comment) body.comment = comment;
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
return await callGraph<void>(
|
|
120
|
+
token,
|
|
121
|
+
`${graphUserPath(user, `events/${encodeURIComponent(eventId)}/${action}`)}`,
|
|
122
|
+
{
|
|
123
|
+
method: 'POST',
|
|
124
|
+
body: JSON.stringify(body)
|
|
125
|
+
},
|
|
126
|
+
false
|
|
127
|
+
);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
if (err instanceof GraphApiError) {
|
|
130
|
+
return graphError(err.message, err.code, err.status);
|
|
131
|
+
}
|
|
132
|
+
return graphError(err instanceof Error ? err.message : 'Failed to respond to event');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { callGraph, GraphApiError, type GraphResponse, graphError, graphResult } from './graph-client.js';
|
|
2
|
+
import { graphUserPath } from './graph-user-path.js';
|
|
3
|
+
|
|
4
|
+
export interface GetScheduleRequest {
|
|
5
|
+
schedules: string[];
|
|
6
|
+
startTime: {
|
|
7
|
+
dateTime: string;
|
|
8
|
+
timeZone: string;
|
|
9
|
+
};
|
|
10
|
+
endTime: {
|
|
11
|
+
dateTime: string;
|
|
12
|
+
timeZone: string;
|
|
13
|
+
};
|
|
14
|
+
availabilityViewInterval?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ScheduleInformation {
|
|
18
|
+
scheduleId: string;
|
|
19
|
+
availabilityView?: string;
|
|
20
|
+
scheduleItems?: Array<{
|
|
21
|
+
isPrivate?: boolean;
|
|
22
|
+
status?: string;
|
|
23
|
+
subject?: string;
|
|
24
|
+
location?: string;
|
|
25
|
+
isMeeting?: boolean;
|
|
26
|
+
isRecurring?: boolean;
|
|
27
|
+
isException?: boolean;
|
|
28
|
+
isReminderSet?: boolean;
|
|
29
|
+
start?: {
|
|
30
|
+
dateTime: string;
|
|
31
|
+
timeZone: string;
|
|
32
|
+
};
|
|
33
|
+
end?: {
|
|
34
|
+
dateTime: string;
|
|
35
|
+
timeZone: string;
|
|
36
|
+
};
|
|
37
|
+
}>;
|
|
38
|
+
workingHours?: {
|
|
39
|
+
daysOfWeek?: string[];
|
|
40
|
+
startTime?: string;
|
|
41
|
+
endTime?: string;
|
|
42
|
+
timeZone?: {
|
|
43
|
+
name?: string;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
error?: {
|
|
47
|
+
message?: string;
|
|
48
|
+
responseCode?: string;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface GetScheduleResponse {
|
|
53
|
+
value: ScheduleInformation[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function getSchedule(
|
|
57
|
+
token: string,
|
|
58
|
+
request: GetScheduleRequest,
|
|
59
|
+
user?: string
|
|
60
|
+
): Promise<GraphResponse<GetScheduleResponse>> {
|
|
61
|
+
let result: GraphResponse<GetScheduleResponse>;
|
|
62
|
+
try {
|
|
63
|
+
result = await callGraph<GetScheduleResponse>(token, graphUserPath(user, 'calendar/getSchedule'), {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
body: JSON.stringify(request),
|
|
66
|
+
headers: {
|
|
67
|
+
Prefer: 'outlook.timezone="UTC"'
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
} catch (err) {
|
|
71
|
+
if (err instanceof GraphApiError) {
|
|
72
|
+
return graphError(err.message, err.code, err.status);
|
|
73
|
+
}
|
|
74
|
+
return graphError(err instanceof Error ? err.message : 'Failed to get schedule');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!result.ok || !result.data) {
|
|
78
|
+
return graphError(result.error?.message || 'Failed to get schedule', result.error?.code, result.error?.status);
|
|
79
|
+
}
|
|
80
|
+
return graphResult(result.data);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface AttendeeBase {
|
|
84
|
+
type: 'required' | 'optional' | 'resource';
|
|
85
|
+
emailAddress: {
|
|
86
|
+
address: string;
|
|
87
|
+
name?: string;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface TimeConstraint {
|
|
92
|
+
activityDomain?: 'work' | 'personal' | 'unrestricted' | 'unknown';
|
|
93
|
+
timeSlots: Array<{
|
|
94
|
+
start: {
|
|
95
|
+
dateTime: string;
|
|
96
|
+
timeZone: string;
|
|
97
|
+
};
|
|
98
|
+
end: {
|
|
99
|
+
dateTime: string;
|
|
100
|
+
timeZone: string;
|
|
101
|
+
};
|
|
102
|
+
}>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface FindMeetingTimesRequest {
|
|
106
|
+
attendees?: AttendeeBase[];
|
|
107
|
+
locationConstraint?: {
|
|
108
|
+
isRequired?: boolean;
|
|
109
|
+
suggestLocation?: boolean;
|
|
110
|
+
locations?: Array<{
|
|
111
|
+
resolveAvailability?: boolean;
|
|
112
|
+
displayName?: string;
|
|
113
|
+
locationEmailAddress?: string;
|
|
114
|
+
}>;
|
|
115
|
+
};
|
|
116
|
+
timeConstraint?: TimeConstraint;
|
|
117
|
+
meetingDuration?: string; // e.g. "PT30M"
|
|
118
|
+
returnSuggestionReasons?: boolean;
|
|
119
|
+
minimumAttendeePercentage?: number;
|
|
120
|
+
isOrganizerOptional?: boolean;
|
|
121
|
+
maxCandidates?: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface MeetingTimeSuggestion {
|
|
125
|
+
meetingTimeSlot?: {
|
|
126
|
+
start?: { dateTime: string; timeZone: string };
|
|
127
|
+
end?: { dateTime: string; timeZone: string };
|
|
128
|
+
};
|
|
129
|
+
confidence?: number;
|
|
130
|
+
organizerAvailability?: string;
|
|
131
|
+
attendeeAvailability?: Array<{
|
|
132
|
+
availability?: string;
|
|
133
|
+
attendee?: AttendeeBase;
|
|
134
|
+
}>;
|
|
135
|
+
locations?: Array<{ displayName?: string; locationEmailAddress?: string }>;
|
|
136
|
+
suggestionReason?: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface FindMeetingTimesResponse {
|
|
140
|
+
emptySuggestionsReason?: string;
|
|
141
|
+
meetingTimeSuggestions?: MeetingTimeSuggestion[];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function findMeetingTimes(
|
|
145
|
+
token: string,
|
|
146
|
+
request: FindMeetingTimesRequest,
|
|
147
|
+
user?: string
|
|
148
|
+
): Promise<GraphResponse<FindMeetingTimesResponse>> {
|
|
149
|
+
let result: GraphResponse<FindMeetingTimesResponse>;
|
|
150
|
+
try {
|
|
151
|
+
result = await callGraph<FindMeetingTimesResponse>(token, graphUserPath(user, 'findMeetingTimes'), {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
body: JSON.stringify(request),
|
|
154
|
+
headers: {
|
|
155
|
+
Prefer: 'outlook.timezone="UTC"'
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
} catch (err) {
|
|
159
|
+
if (err instanceof GraphApiError) {
|
|
160
|
+
return graphError(err.message, err.code, err.status);
|
|
161
|
+
}
|
|
162
|
+
return graphError(err instanceof Error ? err.message : 'Failed to find meeting times');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!result.ok || !result.data) {
|
|
166
|
+
return graphError(
|
|
167
|
+
result.error?.message || 'Failed to find meeting times',
|
|
168
|
+
result.error?.code,
|
|
169
|
+
result.error?.status
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return graphResult(result.data);
|
|
173
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { resolveGraphAuth } from './graph-auth.js';
|
|
2
|
+
import { callGraph, type GraphResponse, graphError } from './graph-client.js';
|
|
3
|
+
|
|
4
|
+
export interface Subscription {
|
|
5
|
+
id: string;
|
|
6
|
+
resource: string;
|
|
7
|
+
applicationId?: string;
|
|
8
|
+
changeType: string;
|
|
9
|
+
clientState?: string;
|
|
10
|
+
notificationUrl: string;
|
|
11
|
+
expirationDateTime: string;
|
|
12
|
+
creatorId?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function getAuthToken(token?: string, identity?: string): Promise<string> {
|
|
16
|
+
const auth = await resolveGraphAuth({ token, identity });
|
|
17
|
+
if (!auth.success || !auth.token) {
|
|
18
|
+
throw new Error(auth.error || 'Failed to authenticate to Graph API');
|
|
19
|
+
}
|
|
20
|
+
return auth.token;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function createSubscription(
|
|
24
|
+
resource: string,
|
|
25
|
+
changeType: string,
|
|
26
|
+
notificationUrl: string,
|
|
27
|
+
expirationDateTime: string,
|
|
28
|
+
clientState?: string,
|
|
29
|
+
token?: string,
|
|
30
|
+
identity?: string
|
|
31
|
+
): Promise<GraphResponse<Subscription>> {
|
|
32
|
+
try {
|
|
33
|
+
const authToken = await getAuthToken(token, identity);
|
|
34
|
+
const body: Record<string, string> = {
|
|
35
|
+
changeType,
|
|
36
|
+
notificationUrl,
|
|
37
|
+
resource,
|
|
38
|
+
expirationDateTime
|
|
39
|
+
};
|
|
40
|
+
if (clientState) {
|
|
41
|
+
body.clientState = clientState;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return await callGraph<Subscription>(authToken, '/subscriptions', {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
body: JSON.stringify(body)
|
|
47
|
+
});
|
|
48
|
+
} catch (err: any) {
|
|
49
|
+
return graphError(err.message, err.code, err.status);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function listSubscriptions(token?: string, identity?: string): Promise<GraphResponse<Subscription[]>> {
|
|
54
|
+
try {
|
|
55
|
+
const authToken = await getAuthToken(token, identity);
|
|
56
|
+
const res = await callGraph<{ value: Subscription[] }>(authToken, '/subscriptions', {
|
|
57
|
+
method: 'GET'
|
|
58
|
+
});
|
|
59
|
+
if (!res.ok) return res as unknown as GraphResponse<Subscription[]>;
|
|
60
|
+
return { ok: true, data: res.data!.value };
|
|
61
|
+
} catch (err: any) {
|
|
62
|
+
return graphError(err.message, err.code, err.status);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function deleteSubscription(id: string, token?: string, identity?: string): Promise<GraphResponse<void>> {
|
|
67
|
+
try {
|
|
68
|
+
const authToken = await getAuthToken(token, identity);
|
|
69
|
+
return await callGraph<void>(authToken, `/subscriptions/${encodeURIComponent(id)}`, {
|
|
70
|
+
method: 'DELETE'
|
|
71
|
+
});
|
|
72
|
+
} catch (err: any) {
|
|
73
|
+
return graphError(err.message, err.code, err.status);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function renewSubscription(
|
|
78
|
+
id: string,
|
|
79
|
+
expirationDateTime: string,
|
|
80
|
+
token?: string,
|
|
81
|
+
identity?: string
|
|
82
|
+
): Promise<GraphResponse<void>> {
|
|
83
|
+
try {
|
|
84
|
+
const authToken = await getAuthToken(token, identity);
|
|
85
|
+
return await callGraph<void>(authToken, `/subscriptions/${encodeURIComponent(id)}`, {
|
|
86
|
+
method: 'PATCH',
|
|
87
|
+
body: JSON.stringify({
|
|
88
|
+
expirationDateTime
|
|
89
|
+
})
|
|
90
|
+
});
|
|
91
|
+
} catch (err: any) {
|
|
92
|
+
return graphError(err.message, err.code, err.status);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build `/me/...` or `/users/{upnOrId}/...` paths for Microsoft Graph delegation.
|
|
3
|
+
*
|
|
4
|
+
* @param user - User UPN, SMTP address, or object ID. Omit or empty for `/me`.
|
|
5
|
+
* @param suffix - Path after `me` or `users/{id}` (no leading slash), e.g. `mailboxSettings`, `calendar/getSchedule`.
|
|
6
|
+
*/
|
|
7
|
+
export function graphUserPath(user: string | undefined, suffix: string): string {
|
|
8
|
+
const s = suffix.replace(/^\//, '');
|
|
9
|
+
if (!user?.trim()) {
|
|
10
|
+
return `/me/${s}`;
|
|
11
|
+
}
|
|
12
|
+
return `/users/${encodeURIComponent(user.trim())}/${s}`;
|
|
13
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const VALID_TENANT_ID =
|
|
2
|
+
/^(?:common|organizations|consumers|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)$/;
|
|
3
|
+
|
|
4
|
+
export function getMicrosoftTenantPathSegment(): string {
|
|
5
|
+
const rawTenant = process.env.EWS_TENANT_ID?.trim() || 'common';
|
|
6
|
+
if (!VALID_TENANT_ID.test(rawTenant)) {
|
|
7
|
+
throw new Error(
|
|
8
|
+
'Invalid EWS_TENANT_ID. Use common/organizations/consumers, a valid tenant UUID, or a domain name.'
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
return rawTenant;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getJwtExpiration(token: string): number | null {
|
|
15
|
+
try {
|
|
16
|
+
const parts = token.split('.');
|
|
17
|
+
if (parts.length !== 3) return null;
|
|
18
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
19
|
+
return payload.exp ? payload.exp * 1000 : null;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isValidJwtStructure(token: string): boolean {
|
|
26
|
+
const parts = token.split('.');
|
|
27
|
+
if (parts.length !== 3) return false;
|
|
28
|
+
try {
|
|
29
|
+
Buffer.from(parts[1], 'base64url').toString();
|
|
30
|
+
return true;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { markdownToHtml } from './markdown.js';
|
|
3
|
+
|
|
4
|
+
describe('markdownToHtml', () => {
|
|
5
|
+
it('sanitizes javascript urls in markdown links', () => {
|
|
6
|
+
const html = markdownToHtml('[click](javascript:alert(1))');
|
|
7
|
+
expect(html).toContain('<a href="#">click</a>');
|
|
8
|
+
expect(html).not.toContain('javascript:');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('sanitizes obfuscated javascript urls in markdown links', () => {
|
|
12
|
+
const html = markdownToHtml('[click](java\nscript:alert(1))');
|
|
13
|
+
expect(html).toContain('<a href="#">click</a>');
|
|
14
|
+
expect(html).not.toContain('javascript:');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('escapes html in link labels and urls', () => {
|
|
18
|
+
const html = markdownToHtml('[<b>hi</b>](https://example.com?q=%3Cscript%3E)');
|
|
19
|
+
expect(html).toContain('<a href="https://example.com?q=%3Cscript%3E"><b>hi</b></a>');
|
|
20
|
+
});
|
|
21
|
+
});
|