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.
Files changed (92) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +916 -0
  3. package/package.json +50 -0
  4. package/src/cli.ts +100 -0
  5. package/src/commands/auto-reply.ts +182 -0
  6. package/src/commands/calendar.ts +576 -0
  7. package/src/commands/counter.ts +87 -0
  8. package/src/commands/create-event.ts +544 -0
  9. package/src/commands/delegates.ts +286 -0
  10. package/src/commands/delete-event.ts +321 -0
  11. package/src/commands/drafts.ts +502 -0
  12. package/src/commands/files.ts +532 -0
  13. package/src/commands/find.ts +195 -0
  14. package/src/commands/findtime.ts +270 -0
  15. package/src/commands/folders.ts +177 -0
  16. package/src/commands/forward-event.ts +49 -0
  17. package/src/commands/graph-calendar.ts +217 -0
  18. package/src/commands/login.ts +195 -0
  19. package/src/commands/mail.ts +950 -0
  20. package/src/commands/oof.ts +263 -0
  21. package/src/commands/outlook-categories.ts +173 -0
  22. package/src/commands/outlook-graph.ts +880 -0
  23. package/src/commands/planner.ts +1678 -0
  24. package/src/commands/respond.ts +291 -0
  25. package/src/commands/rooms.ts +210 -0
  26. package/src/commands/rules.ts +511 -0
  27. package/src/commands/schedule.ts +109 -0
  28. package/src/commands/send.ts +204 -0
  29. package/src/commands/serve.ts +14 -0
  30. package/src/commands/sharepoint.ts +179 -0
  31. package/src/commands/site-pages.ts +163 -0
  32. package/src/commands/subscribe.ts +103 -0
  33. package/src/commands/subscriptions.ts +29 -0
  34. package/src/commands/suggest.ts +155 -0
  35. package/src/commands/todo.ts +2092 -0
  36. package/src/commands/update-event.ts +608 -0
  37. package/src/commands/update.ts +88 -0
  38. package/src/commands/verify-token.ts +62 -0
  39. package/src/commands/whoami.ts +74 -0
  40. package/src/index.ts +190 -0
  41. package/src/lib/atomic-write.ts +20 -0
  42. package/src/lib/attach-link-spec.test.ts +24 -0
  43. package/src/lib/attach-link-spec.ts +70 -0
  44. package/src/lib/attachments.ts +79 -0
  45. package/src/lib/auth.ts +192 -0
  46. package/src/lib/calendar-range.test.ts +41 -0
  47. package/src/lib/calendar-range.ts +103 -0
  48. package/src/lib/dates.test.ts +74 -0
  49. package/src/lib/dates.ts +137 -0
  50. package/src/lib/delegate-client.test.ts +74 -0
  51. package/src/lib/delegate-client.ts +322 -0
  52. package/src/lib/ews-client.ts +3418 -0
  53. package/src/lib/git-commit.ts +4 -0
  54. package/src/lib/glitchtip-eligibility.ts +220 -0
  55. package/src/lib/glitchtip.ts +253 -0
  56. package/src/lib/global-env.ts +3 -0
  57. package/src/lib/graph-auth.ts +223 -0
  58. package/src/lib/graph-calendar-client.test.ts +118 -0
  59. package/src/lib/graph-calendar-client.ts +112 -0
  60. package/src/lib/graph-client.test.ts +107 -0
  61. package/src/lib/graph-client.ts +1058 -0
  62. package/src/lib/graph-constants.ts +12 -0
  63. package/src/lib/graph-directory.ts +116 -0
  64. package/src/lib/graph-event.ts +134 -0
  65. package/src/lib/graph-schedule.ts +173 -0
  66. package/src/lib/graph-subscriptions.ts +94 -0
  67. package/src/lib/graph-user-path.ts +13 -0
  68. package/src/lib/jwt-utils.ts +34 -0
  69. package/src/lib/markdown.test.ts +21 -0
  70. package/src/lib/markdown.ts +174 -0
  71. package/src/lib/mime-type.ts +106 -0
  72. package/src/lib/oof-client.test.ts +59 -0
  73. package/src/lib/oof-client.ts +122 -0
  74. package/src/lib/outlook-graph-client.test.ts +146 -0
  75. package/src/lib/outlook-graph-client.ts +649 -0
  76. package/src/lib/outlook-master-categories.ts +145 -0
  77. package/src/lib/package-info.ts +59 -0
  78. package/src/lib/places-client.ts +144 -0
  79. package/src/lib/planner-client.ts +1226 -0
  80. package/src/lib/rules-client.ts +178 -0
  81. package/src/lib/sharepoint-client.ts +101 -0
  82. package/src/lib/site-pages-client.ts +73 -0
  83. package/src/lib/todo-client.test.ts +298 -0
  84. package/src/lib/todo-client.ts +1309 -0
  85. package/src/lib/url-validation.ts +40 -0
  86. package/src/lib/utils.ts +45 -0
  87. package/src/lib/webhook-server.ts +51 -0
  88. package/src/test/auth.test.ts +104 -0
  89. package/src/test/cli.integration.test.ts +1083 -0
  90. package/src/test/ews-client.test.ts +268 -0
  91. package/src/test/mocks/index.ts +375 -0
  92. package/src/test/mocks/responses.ts +861 -0
@@ -0,0 +1,62 @@
1
+ import { Command } from 'commander';
2
+ import { resolveGraphAuth } from '../lib/graph-auth.js';
3
+
4
+ export const verifyTokenCommand = new Command('verify-token')
5
+ .description('Verify Graph API token scopes and permissions')
6
+ .option('--token <token>', 'Use a specific Graph token')
7
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
8
+ .option('--json', 'Output as JSON')
9
+ .action(async (options: { token?: string; json?: boolean; identity?: string }) => {
10
+ const authResult = await resolveGraphAuth({ token: options.token, identity: options.identity });
11
+ if (!authResult.success || !authResult.token) {
12
+ if (options.json) {
13
+ console.log(JSON.stringify({ error: authResult.error || 'Failed to resolve auth token' }, null, 2));
14
+ } else {
15
+ console.error(`Error: ${authResult.error || 'Failed to resolve auth token'}`);
16
+ }
17
+ process.exit(1);
18
+ }
19
+
20
+ const token = authResult.token;
21
+ try {
22
+ const parts = token.split('.');
23
+ if (parts.length !== 3) throw new Error('Invalid JWT format');
24
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
25
+
26
+ if (options.json) {
27
+ console.log(JSON.stringify(payload, null, 2));
28
+ return;
29
+ }
30
+
31
+ console.log('\u2713 Token Verified\n');
32
+ console.log(` App ID: ${payload.appid || 'N/A'}`);
33
+ console.log(` Tenant: ${payload.tid || 'N/A'}`);
34
+ console.log(` User: ${payload.upn || payload.email || 'N/A'}`);
35
+ console.log(` Name: ${payload.name || 'N/A'}`);
36
+
37
+ if (payload.scp) {
38
+ console.log('\n Delegated Scopes (scp):');
39
+ payload.scp.split(' ').forEach((scope: string) => {
40
+ console.log(` - ${scope}`);
41
+ });
42
+ }
43
+
44
+ if (payload.roles && Array.isArray(payload.roles)) {
45
+ console.log('\n Application Roles (roles):');
46
+ payload.roles.forEach((role: string) => {
47
+ console.log(` - ${role}`);
48
+ });
49
+ }
50
+
51
+ if (!payload.scp && (!payload.roles || payload.roles.length === 0)) {
52
+ console.log('\n No scopes or roles found in token.');
53
+ }
54
+ } catch (err: any) {
55
+ if (options.json) {
56
+ console.log(JSON.stringify({ error: `Failed to parse token: ${err.message}` }, null, 2));
57
+ } else {
58
+ console.error(`Failed to parse token: ${err.message}`);
59
+ }
60
+ process.exit(1);
61
+ }
62
+ });
@@ -0,0 +1,74 @@
1
+ import { Command } from 'commander';
2
+ import { resolveAuth } from '../lib/auth.js';
3
+ import { getOwaUserInfo } from '../lib/ews-client.js';
4
+
5
+ export const whoamiCommand = new Command('whoami')
6
+ .description('Show authenticated user information')
7
+ .option('--json', 'Output as JSON')
8
+ .option('--token <token>', 'Use a specific token')
9
+ .option('--identity <name>', 'Use a specific authentication identity (default: default)')
10
+ .action(async (options: { json?: boolean; token?: string; identity?: string }) => {
11
+ const authResult = await resolveAuth({
12
+ token: options.token,
13
+ identity: options.identity
14
+ });
15
+
16
+ if (!authResult.success) {
17
+ if (options.json) {
18
+ console.log(JSON.stringify({ error: authResult.error }, null, 2));
19
+ } else {
20
+ console.error(`Error: ${authResult.error}`);
21
+ console.error('\nCheck your .env file for EWS_CLIENT_ID and EWS_REFRESH_TOKEN.');
22
+ }
23
+ process.exit(1);
24
+ }
25
+
26
+ const userInfo = await getOwaUserInfo(authResult.token!);
27
+
28
+ if (!userInfo.ok || !userInfo.data) {
29
+ if (options.json) {
30
+ console.log(
31
+ JSON.stringify(
32
+ {
33
+ error: userInfo.error?.message || 'Failed to fetch user info',
34
+ authenticated: true
35
+ },
36
+ null,
37
+ 2
38
+ )
39
+ );
40
+ } else {
41
+ console.log('\u2713 Authenticated');
42
+ console.log(' Could not fetch user details from EWS API');
43
+ }
44
+ process.exit(0);
45
+ }
46
+
47
+ const { displayName, email } = userInfo.data;
48
+
49
+ if (options.json) {
50
+ const result: { displayName: string; email: string; authenticated: boolean; identity?: string } = {
51
+ displayName,
52
+ email,
53
+ authenticated: true
54
+ };
55
+
56
+ // Only include identity if token-based auth was not used
57
+ if (!options.token) {
58
+ result.identity = options.identity || 'default';
59
+ }
60
+
61
+ console.log(JSON.stringify(result, null, 2));
62
+ } else {
63
+ console.log('\u2713 Authenticated');
64
+
65
+ // Only display identity if token-based auth was not used
66
+ if (!options.token) {
67
+ const identity = options.identity || 'default';
68
+ console.log(` Identity: ${identity}`);
69
+ }
70
+
71
+ console.log(` Name: ${displayName}`);
72
+ console.log(` Email: ${email}`);
73
+ }
74
+ });
package/src/index.ts ADDED
@@ -0,0 +1,190 @@
1
+ // Library exports for programmatic usage
2
+
3
+ export type { AuthResult } from './lib/auth.js';
4
+ export { resolveAuth } from './lib/auth.js';
5
+ export type {
6
+ AddDelegateOptions,
7
+ DelegateFolderPermissionLevel,
8
+ DelegateInfo,
9
+ DelegatePermissions,
10
+ DeliverMeetingRequests,
11
+ RemoveDelegateOptions,
12
+ UpdateDelegateOptions
13
+ } from './lib/delegate-client.js';
14
+ // Delegate management
15
+ export {
16
+ addDelegate,
17
+ getDelegates,
18
+ removeDelegate,
19
+ updateDelegate
20
+ } from './lib/delegate-client.js';
21
+ export type {
22
+ Attachment,
23
+ AttachmentListResponse,
24
+ AutoReplyRule,
25
+ CalendarAttendee,
26
+ CalendarEvent,
27
+ CreatedEvent,
28
+ CreateEventOptions,
29
+ EmailAttachment,
30
+ EmailListResponse,
31
+ EmailMessage,
32
+ FreeBusySlot,
33
+ GetEmailsOptions,
34
+ MailFolder,
35
+ MailFolderListResponse,
36
+ OwaError,
37
+ OwaResponse,
38
+ OwaUserInfo,
39
+ Recurrence,
40
+ RecurrencePattern,
41
+ RecurrenceRange,
42
+ ReferenceAttachmentInput,
43
+ RespondToEventOptions,
44
+ ResponseType,
45
+ Room,
46
+ RoomList,
47
+ ScheduleInfo,
48
+ UpdateEventOptions
49
+ } from './lib/ews-client.js';
50
+ export {
51
+ addAttachmentToDraft,
52
+ addCalendarEventAttachments,
53
+ addReferenceAttachmentToDraft,
54
+ cancelEvent,
55
+ createDraft,
56
+ createEvent,
57
+ createMailFolder,
58
+ deleteDraftById,
59
+ deleteEvent,
60
+ deleteMailFolder,
61
+ forwardEmail,
62
+ getAttachment,
63
+ getAttachments,
64
+ getAutoReplyRule,
65
+ getCalendarEvent,
66
+ getCalendarEvents,
67
+ getEmail,
68
+ getEmails,
69
+ getFreeBusy,
70
+ getMailFolders,
71
+ getMyFreeBusySlots,
72
+ getOwaUserInfo,
73
+ getRoomLists,
74
+ getRooms,
75
+ getScheduleViaOutlook,
76
+ moveEmail,
77
+ replyToEmail,
78
+ replyToEmailDraft,
79
+ resolveNames,
80
+ respondToEvent,
81
+ searchRooms,
82
+ sendDraftById,
83
+ sendEmail,
84
+ setAutoReplyRule,
85
+ updateDraft,
86
+ updateEmail,
87
+ updateEvent,
88
+ updateMailFolder,
89
+ validateSession
90
+ } from './lib/ews-client.js';
91
+ export type { GraphAuthResult } from './lib/graph-auth.js';
92
+ export { resolveGraphAuth } from './lib/graph-auth.js';
93
+ export type {
94
+ CheckinResult,
95
+ DriveItem,
96
+ DriveItemListResponse,
97
+ DriveItemReference,
98
+ GraphError,
99
+ GraphResponse,
100
+ OfficeCollabLinkResult,
101
+ SharingLinkResult,
102
+ UploadLargeResult
103
+ } from './lib/graph-client.js';
104
+ export {
105
+ checkinFile,
106
+ checkoutFile,
107
+ cleanupDownloadedFile,
108
+ createOfficeCollaborationLink,
109
+ defaultDownloadPath,
110
+ deleteFile,
111
+ downloadFile,
112
+ getFileMetadata,
113
+ listFiles,
114
+ searchFiles,
115
+ shareFile,
116
+ uploadFile,
117
+ uploadLargeFile
118
+ } from './lib/graph-client.js';
119
+ export type {
120
+ Group,
121
+ Person,
122
+ User
123
+ } from './lib/graph-directory.js';
124
+ export {
125
+ expandGroup,
126
+ searchGroups,
127
+ searchPeople,
128
+ searchUsers
129
+ } from './lib/graph-directory.js';
130
+ export type {
131
+ AttendeeBase,
132
+ FindMeetingTimesRequest,
133
+ FindMeetingTimesResponse,
134
+ GetScheduleRequest,
135
+ GetScheduleResponse,
136
+ MeetingTimeSuggestion,
137
+ ScheduleInformation,
138
+ TimeConstraint
139
+ } from './lib/graph-schedule.js';
140
+ export { findMeetingTimes, getSchedule } from './lib/graph-schedule.js';
141
+ export type { Subscription } from './lib/graph-subscriptions.js';
142
+ export {
143
+ createSubscription,
144
+ deleteSubscription,
145
+ listSubscriptions,
146
+ renewSubscription
147
+ } from './lib/graph-subscriptions.js';
148
+ export { graphUserPath } from './lib/graph-user-path.js';
149
+ export type { AutomaticRepliesSetting, MailboxSettings, OofStatus } from './lib/oof-client.js';
150
+ export { getMailboxSettings, setMailboxSettings } from './lib/oof-client.js';
151
+ export type { Place as PlaceRoom, RoomList as PlaceRoomList } from './lib/places-client.js';
152
+ // places-client re-exports (aliases from EWS: getRoomLists/getRooms conflict)
153
+ export { listPlaceRoomLists as getPlaceRoomLists, listRoomsInRoomList as getPlaceRooms } from './lib/places-client.js';
154
+ export type {
155
+ CreateMessageRulePayload,
156
+ MessageRule,
157
+ MessageRuleAction,
158
+ MessageRuleCondition,
159
+ UpdateMessageRulePayload
160
+ } from './lib/rules-client.js';
161
+ // Inbox rules
162
+ export {
163
+ createMessageRule,
164
+ deleteMessageRule,
165
+ getMessageRule,
166
+ listMessageRules,
167
+ updateMessageRule
168
+ } from './lib/rules-client.js';
169
+ export type {
170
+ CreateTaskOptions,
171
+ TodoChecklistItem,
172
+ TodoImportance,
173
+ TodoLinkedResource,
174
+ TodoList,
175
+ TodoStatus,
176
+ TodoTask,
177
+ UpdateTaskOptions
178
+ } from './lib/todo-client.js';
179
+ // To-Do
180
+ export {
181
+ addChecklistItem,
182
+ createTask,
183
+ deleteChecklistItem,
184
+ deleteTask,
185
+ getTask,
186
+ getTasks,
187
+ getTodoList,
188
+ getTodoLists,
189
+ updateTask
190
+ } from './lib/todo-client.js';
@@ -0,0 +1,20 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { mkdir, rename, unlink, writeFile } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+
5
+ /**
6
+ * Write UTF-8 text atomically (temp file in same directory, then rename).
7
+ * Avoids readers observing partial writes and pairs well with validation before persist.
8
+ */
9
+ export async function atomicWriteUtf8File(targetPath: string, data: string, mode: number): Promise<void> {
10
+ const dir = dirname(targetPath);
11
+ await mkdir(dir, { recursive: true, mode: 0o700 });
12
+ const tmp = join(dir, `.${randomBytes(16).toString('hex')}.tmp`);
13
+ try {
14
+ await writeFile(tmp, data, { encoding: 'utf8', mode });
15
+ await rename(tmp, targetPath);
16
+ } catch (err) {
17
+ await unlink(tmp).catch(() => {});
18
+ throw err;
19
+ }
20
+ }
@@ -0,0 +1,24 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { AttachmentLinkSpecError, parseAttachLinkSpec, validateHttpsUrlForAttachment } from './attach-link-spec.js';
3
+
4
+ describe('attach-link-spec', () => {
5
+ test('validateHttpsUrlForAttachment accepts https', () => {
6
+ expect(validateHttpsUrlForAttachment('https://example.com/path?q=1')).toBe('https://example.com/path?q=1');
7
+ });
8
+
9
+ test('validateHttpsUrlForAttachment rejects http', () => {
10
+ expect(() => validateHttpsUrlForAttachment('http://example.com')).toThrow(AttachmentLinkSpecError);
11
+ });
12
+
13
+ test('parseAttachLinkSpec splits name and url', () => {
14
+ const p = parseAttachLinkSpec('Agenda|https://example.com/doc.pdf');
15
+ expect(p.name).toBe('Agenda');
16
+ expect(p.url).toBe('https://example.com/doc.pdf');
17
+ });
18
+
19
+ test('parseAttachLinkSpec bare url derives name', () => {
20
+ const p = parseAttachLinkSpec('https://example.com/folder/doc.pdf');
21
+ expect(p.url).toBe('https://example.com/folder/doc.pdf');
22
+ expect(p.name).toBe('doc.pdf');
23
+ });
24
+ });
@@ -0,0 +1,70 @@
1
+ /** Parse and validate --attach-link values: "Display name|https://..." or a bare https URL. */
2
+
3
+ export class AttachmentLinkSpecError extends Error {
4
+ constructor(message: string) {
5
+ super(message);
6
+ this.name = 'AttachmentLinkSpecError';
7
+ }
8
+ }
9
+
10
+ function deriveNameFromUrl(urlStr: string): string {
11
+ try {
12
+ const u = new URL(urlStr);
13
+ const path = u.pathname.split('/').filter(Boolean);
14
+ const last = path[path.length - 1];
15
+ if (last && last.length > 0 && last.length < 120) {
16
+ return decodeURIComponent(last.replace(/\+/g, ' '));
17
+ }
18
+ return u.hostname || 'Link';
19
+ } catch {
20
+ return 'Link';
21
+ }
22
+ }
23
+
24
+ /** Only https URLs (no javascript:, file:, etc.). */
25
+ export function validateHttpsUrlForAttachment(urlRaw: string): string {
26
+ const trimmed = urlRaw.trim();
27
+ if (!trimmed) {
28
+ throw new AttachmentLinkSpecError('Attachment link URL is empty');
29
+ }
30
+ let url: URL;
31
+ try {
32
+ url = new URL(trimmed);
33
+ } catch {
34
+ throw new AttachmentLinkSpecError(`Invalid attachment link URL: ${trimmed}`);
35
+ }
36
+ if (url.protocol !== 'https:') {
37
+ throw new AttachmentLinkSpecError('Attachment link URL must use https://');
38
+ }
39
+ if (url.username || url.password) {
40
+ throw new AttachmentLinkSpecError('Attachment link URL must not include credentials');
41
+ }
42
+ return url.toString();
43
+ }
44
+
45
+ export interface ParsedAttachLink {
46
+ name: string;
47
+ url: string;
48
+ }
49
+
50
+ export function parseAttachLinkSpec(spec: string): ParsedAttachLink {
51
+ const s = spec.trim();
52
+ if (!s) {
53
+ throw new AttachmentLinkSpecError('Empty --attach-link value');
54
+ }
55
+ const pipe = s.indexOf('|');
56
+ let name: string;
57
+ let urlRaw: string;
58
+ if (pipe === -1) {
59
+ urlRaw = s;
60
+ name = deriveNameFromUrl(urlRaw);
61
+ } else {
62
+ name = s.slice(0, pipe).trim();
63
+ urlRaw = s.slice(pipe + 1).trim();
64
+ if (!name) {
65
+ name = deriveNameFromUrl(urlRaw);
66
+ }
67
+ }
68
+ const url = validateHttpsUrlForAttachment(urlRaw);
69
+ return { name, url };
70
+ }
@@ -0,0 +1,79 @@
1
+ import { lstat, realpath, stat } from 'node:fs/promises';
2
+ import { isAbsolute, normalize, resolve, sep } from 'node:path';
3
+
4
+ const MAX_ATTACHMENT_SIZE_BYTES = 25 * 1024 * 1024;
5
+
6
+ export interface ValidatedAttachmentPath {
7
+ inputPath: string;
8
+ absolutePath: string;
9
+ fileName: string;
10
+ size: number;
11
+ }
12
+
13
+ export class AttachmentPathError extends Error {
14
+ constructor(message: string) {
15
+ super(message);
16
+ this.name = 'AttachmentPathError';
17
+ }
18
+ }
19
+
20
+ function hasParentTraversal(inputPath: string): boolean {
21
+ const normalizedInput = normalize(inputPath);
22
+ return normalizedInput.split(/[\\/]+/).includes('..');
23
+ }
24
+
25
+ export async function validateAttachmentPath(
26
+ inputPath: string,
27
+ allowedBaseDir: string
28
+ ): Promise<ValidatedAttachmentPath> {
29
+ if (!inputPath?.trim()) {
30
+ throw new AttachmentPathError('Attachment path cannot be empty');
31
+ }
32
+
33
+ if (hasParentTraversal(inputPath)) {
34
+ throw new AttachmentPathError(`Path traversal is not allowed in attachment path: ${inputPath}`);
35
+ }
36
+
37
+ if (inputPath.startsWith('~')) {
38
+ throw new AttachmentPathError(`Home directory shortcuts (~) are not allowed for attachments: ${inputPath}`);
39
+ }
40
+
41
+ if (isAbsolute(inputPath)) {
42
+ throw new AttachmentPathError(`Absolute paths are not allowed for attachments: ${inputPath}`);
43
+ }
44
+
45
+ const candidatePath = resolve(allowedBaseDir, inputPath);
46
+ const realAllowedBase = await realpath(allowedBaseDir);
47
+
48
+ let realCandidatePath: string;
49
+ try {
50
+ realCandidatePath = await realpath(candidatePath);
51
+ } catch {
52
+ throw new AttachmentPathError(`Attachment does not exist: ${inputPath}`);
53
+ }
54
+
55
+ if (realCandidatePath !== realAllowedBase && !realCandidatePath.startsWith(`${realAllowedBase}${sep}`)) {
56
+ throw new AttachmentPathError(`Attachment path escapes the allowed directory: ${inputPath}`);
57
+ }
58
+
59
+ const symbolicLinkInfo = await lstat(candidatePath);
60
+ if (symbolicLinkInfo.isSymbolicLink()) {
61
+ throw new AttachmentPathError(`Symbolic links are not allowed for attachments: ${inputPath}`);
62
+ }
63
+
64
+ const fileInfo = await stat(realCandidatePath);
65
+ if (!fileInfo.isFile()) {
66
+ throw new AttachmentPathError(`Not a file: ${inputPath}`);
67
+ }
68
+
69
+ if (fileInfo.size > MAX_ATTACHMENT_SIZE_BYTES) {
70
+ throw new AttachmentPathError(`File too large (>25MB): ${inputPath}`);
71
+ }
72
+
73
+ return {
74
+ inputPath,
75
+ absolutePath: realCandidatePath,
76
+ fileName: realCandidatePath.split(/[\\/]/).pop() || inputPath,
77
+ size: fileInfo.size
78
+ };
79
+ }