Skip to content

Commit 0134a5b

Browse files
authored
fix(onedrive): align tools with live Graph API docs, add missing endpoints (#5478)
* fix(onedrive): align tools with live Graph API docs, add missing endpoints - wire up search/move/copy/create_share_link tools into block operation switch (were registered but unreachable, would throw 'Invalid OneDrive operation') - fix tools.config.params to remap new canonical subBlock ids to correct tool param names - add onedrive_get_item and onedrive_get_drive_info tools (item metadata, drive quota) — both within existing Files.Read/Files.ReadWrite scope - add missing block inputs/outputs for new operations - alphabetize onedrive registry entries * fix(onedrive): escape single quotes in search query, document embed link type - encodeURIComponent doesn't escape single quotes, breaking the OData string literal for filenames containing an apostrophe - clarify create_share_link's linkType description to include 'embed', which the block UI already exposes as a valid option * fix(onedrive): add real pagination continuation to search - add pageToken param that follows the @odata.nextLink continuation URL directly, so nextPageToken output is actually consumable instead of a dead end - wire pageToken through the block as an advanced-mode field on the search operation * fix(onedrive): prevent SSRF/token exfiltration via search pageToken pageToken was used verbatim as the request URL with no host validation, while the Authorization header is always attached — a crafted pageToken pointing at an attacker-controlled host would leak the OAuth access token. Pin the continuation URL to graph.microsoft.com before using it. * fix(onedrive): make search query optional for pageToken-only continuation requests * fix(onedrive): fix folder-targeting wiring, OData escaping, and downloadUrl selection - upload/create_folder/list all read params.folderSelector/manualFolderId, but the block only ever sends folderId — folder targeting silently fell back to drive root. Consolidate to a single folderId param matching what the block sends (same pattern already used for destinationFolderId on move/copy) - search's percent-encode-then-replace('%27') "escape" is undone by Graph's standard URL-decode-before-parse, so an apostrophe in a query still breaks the OData string literal; list's $filter had no escaping at all. Both now double literal quotes (the correct OData V4 escape) via a shared escapeODataStringLiteral helper before encoding - get_item/list/search all omitted @microsoft.graph.downloadUrl from $select, so webContentLink was always undefined on their outputs; added it - list gains the same pageToken continuation support search already has, and the block's Page Token field is now shown for both operations * fix(onedrive): reuse shared assertGraphNextPageUrl for page-token validation Hostname-only check let http://graph.microsoft.com/... continuation tokens through, which would send the Bearer token over cleartext. Sibling Graph integrations (sharepoint, microsoft_teams, microsoft_planner, microsoft_ad) already share assertGraphNextPageUrl/getGraphNextPageUrl in tools/sharepoint/utils.ts, which checks the full origin (scheme + host). Reuse it in list/search instead of hand-rolling the check twice.
1 parent 8fcce51 commit 0134a5b

14 files changed

Lines changed: 977 additions & 36 deletions

File tree

apps/sim/blocks/blocks/onedrive.ts

Lines changed: 309 additions & 5 deletions
Large diffs are not rendered by default.

apps/sim/tools/onedrive/copy.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { createLogger } from '@sim/logger'
2+
import type { OneDriveCopyResponse, OneDriveToolParams } from '@/tools/onedrive/types'
3+
import type { ToolConfig } from '@/tools/types'
4+
5+
const logger = createLogger('OneDriveCopyTool')
6+
7+
/**
8+
* Microsoft Graph processes driveItem copies asynchronously: a successful request returns
9+
* `202 Accepted` with a `Location` header pointing to a monitor URL, not the copied item itself.
10+
* See https://learn.microsoft.com/en-us/graph/api/driveitem-copy
11+
*/
12+
export const copyTool: ToolConfig<OneDriveToolParams, OneDriveCopyResponse> = {
13+
id: 'onedrive_copy',
14+
name: 'Copy OneDrive File',
15+
description: 'Copy a file or folder to another location within OneDrive',
16+
version: '1.0',
17+
18+
oauth: {
19+
required: true,
20+
provider: 'onedrive',
21+
},
22+
23+
params: {
24+
accessToken: {
25+
type: 'string',
26+
required: true,
27+
visibility: 'hidden',
28+
description: 'The access token for the OneDrive API',
29+
},
30+
fileId: {
31+
type: 'string',
32+
required: true,
33+
visibility: 'user-or-llm',
34+
description: 'The ID of the file or folder to copy',
35+
},
36+
destinationFolderId: {
37+
type: 'string',
38+
required: true,
39+
visibility: 'user-or-llm',
40+
description: 'The ID of the destination parent folder',
41+
},
42+
destinationFileName: {
43+
type: 'string',
44+
required: false,
45+
visibility: 'user-or-llm',
46+
description: 'Optional new name for the copy (defaults to the original name)',
47+
},
48+
},
49+
50+
request: {
51+
url: (params) =>
52+
`https://graph.microsoft.com/v1.0/me/drive/items/${encodeURIComponent(params.fileId || '')}/copy`,
53+
method: 'POST',
54+
headers: (params) => ({
55+
Authorization: `Bearer ${params.accessToken}`,
56+
'Content-Type': 'application/json',
57+
}),
58+
body: (params) => ({
59+
parentReference: { id: params.destinationFolderId },
60+
...(params.destinationFileName && { name: params.destinationFileName }),
61+
}),
62+
},
63+
64+
transformResponse: async (response: Response, params?: OneDriveToolParams) => {
65+
if (response.status !== 202) {
66+
const data = await response.json().catch(() => ({}))
67+
throw new Error(data.error?.message || 'Failed to start OneDrive copy')
68+
}
69+
70+
const monitorUrl = response.headers.get('location') || undefined
71+
72+
logger.info('OneDrive copy accepted for async processing', {
73+
fileId: params?.fileId,
74+
monitorUrl,
75+
})
76+
77+
return {
78+
success: true,
79+
output: {
80+
sourceFileId: params?.fileId || '',
81+
name: params?.destinationFileName,
82+
monitorUrl,
83+
},
84+
}
85+
},
86+
87+
outputs: {
88+
success: { type: 'boolean', description: 'Whether the copy request was accepted' },
89+
sourceFileId: { type: 'string', description: 'The ID of the file or folder that was copied' },
90+
name: { type: 'string', description: 'The requested name for the copy, if provided' },
91+
monitorUrl: {
92+
type: 'string',
93+
description:
94+
'URL to poll for the status of the asynchronous copy operation (copy completes in the background)',
95+
},
96+
},
97+
}

apps/sim/tools/onedrive/create_folder.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,19 @@ export const createFolderTool: ToolConfig<OneDriveToolParams, OneDriveUploadResp
2525
visibility: 'user-or-llm',
2626
description: 'Name of the folder to create (e.g., "My Documents", "Project Files")',
2727
},
28-
folderSelector: {
28+
folderId: {
2929
type: 'string',
3030
required: false,
3131
visibility: 'user-or-llm',
3232
description:
3333
'Parent folder ID to create the folder in (e.g., "01BYE5RZ6QN3ZWBTUFOFD3GSPGOHDJD36M")',
3434
},
35-
manualFolderId: {
36-
type: 'string',
37-
required: false,
38-
visibility: 'hidden',
39-
description: 'Manually entered parent folder ID (advanced mode)',
40-
},
4135
},
4236

4337
request: {
4438
url: (params) => {
4539
// Use specific parent folder URL if parentId is provided
46-
const parentFolderId = params.manualFolderId || params.folderSelector
40+
const parentFolderId = params.folderId?.trim()
4741
if (parentFolderId) {
4842
return `https://graph.microsoft.com/v1.0/me/drive/items/${encodeURIComponent(parentFolderId)}/children`
4943
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import type { OneDriveShareLinkResponse, OneDriveToolParams } from '@/tools/onedrive/types'
2+
import type { ToolConfig } from '@/tools/types'
3+
4+
export const createShareLinkTool: ToolConfig<OneDriveToolParams, OneDriveShareLinkResponse> = {
5+
id: 'onedrive_create_share_link',
6+
name: 'Create OneDrive Sharing Link',
7+
description: 'Create a view or edit sharing link for a OneDrive file or folder',
8+
version: '1.0',
9+
10+
oauth: {
11+
required: true,
12+
provider: 'onedrive',
13+
},
14+
15+
params: {
16+
accessToken: {
17+
type: 'string',
18+
required: true,
19+
visibility: 'hidden',
20+
description: 'The access token for the OneDrive API',
21+
},
22+
fileId: {
23+
type: 'string',
24+
required: true,
25+
visibility: 'user-or-llm',
26+
description: 'The ID of the file or folder to share',
27+
},
28+
linkType: {
29+
type: 'string',
30+
required: false,
31+
visibility: 'user-or-llm',
32+
description: 'Type of link to create: "view" (read-only), "edit" (read-write), or "embed"',
33+
},
34+
linkScope: {
35+
type: 'string',
36+
required: false,
37+
visibility: 'user-or-llm',
38+
description:
39+
'Who can use the link: "anonymous" (anyone), "organization" (tenant members), or "users" (specific people)',
40+
},
41+
},
42+
43+
request: {
44+
url: (params) =>
45+
`https://graph.microsoft.com/v1.0/me/drive/items/${encodeURIComponent(params.fileId || '')}/createLink`,
46+
method: 'POST',
47+
headers: (params) => ({
48+
Authorization: `Bearer ${params.accessToken}`,
49+
'Content-Type': 'application/json',
50+
}),
51+
body: (params) => ({
52+
type: params.linkType || 'view',
53+
...(params.linkScope && { scope: params.linkScope }),
54+
}),
55+
},
56+
57+
transformResponse: async (response: Response) => {
58+
const data = await response.json()
59+
60+
return {
61+
success: true,
62+
output: {
63+
link: {
64+
type: data.link?.type,
65+
scope: data.link?.scope,
66+
webUrl: data.link?.webUrl,
67+
webHtml: data.link?.webHtml,
68+
},
69+
},
70+
}
71+
},
72+
73+
outputs: {
74+
success: { type: 'boolean', description: 'Whether the sharing link was created successfully' },
75+
link: {
76+
type: 'object',
77+
description: 'The created sharing link, including its type, scope, and URL',
78+
},
79+
},
80+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type { OneDriveGetDriveInfoResponse, OneDriveToolParams } from '@/tools/onedrive/types'
2+
import type { ToolConfig } from '@/tools/types'
3+
4+
export const getDriveInfoTool: ToolConfig<OneDriveToolParams, OneDriveGetDriveInfoResponse> = {
5+
id: 'onedrive_get_drive_info',
6+
name: 'Get OneDrive Info',
7+
description: 'Get information about the OneDrive drive, including storage quota',
8+
version: '1.0',
9+
10+
oauth: {
11+
required: true,
12+
provider: 'onedrive',
13+
},
14+
15+
params: {
16+
accessToken: {
17+
type: 'string',
18+
required: true,
19+
visibility: 'hidden',
20+
description: 'The access token for the OneDrive API',
21+
},
22+
},
23+
24+
request: {
25+
url: () => 'https://graph.microsoft.com/v1.0/me/drive',
26+
method: 'GET',
27+
headers: (params) => ({
28+
Authorization: `Bearer ${params.accessToken}`,
29+
}),
30+
},
31+
32+
transformResponse: async (response: Response) => {
33+
const data = await response.json()
34+
35+
return {
36+
success: true,
37+
output: {
38+
driveId: data.id,
39+
driveType: data.driveType,
40+
webUrl: data.webUrl,
41+
owner: data.owner?.user?.displayName ?? null,
42+
quota: {
43+
total: data.quota?.total ?? 0,
44+
used: data.quota?.used ?? 0,
45+
remaining: data.quota?.remaining ?? 0,
46+
deleted: data.quota?.deleted ?? 0,
47+
state: data.quota?.state ?? 'normal',
48+
},
49+
},
50+
}
51+
},
52+
53+
outputs: {
54+
success: { type: 'boolean', description: 'Whether the drive info was retrieved' },
55+
driveId: { type: 'string', description: 'The ID of the drive' },
56+
driveType: { type: 'string', description: 'The type of drive (e.g., "personal", "business")' },
57+
webUrl: { type: 'string', description: 'URL to the drive in the browser' },
58+
owner: { type: 'string', description: 'Display name of the drive owner', optional: true },
59+
quota: {
60+
type: 'object',
61+
description: 'Storage quota information in bytes (total, used, remaining, deleted, state)',
62+
},
63+
},
64+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import type {
2+
MicrosoftGraphDriveItem,
3+
OneDriveGetItemResponse,
4+
OneDriveToolParams,
5+
} from '@/tools/onedrive/types'
6+
import type { ToolConfig } from '@/tools/types'
7+
8+
export const getItemTool: ToolConfig<OneDriveToolParams, OneDriveGetItemResponse> = {
9+
id: 'onedrive_get_item',
10+
name: 'Get OneDrive Item Metadata',
11+
description: 'Get metadata for a specific OneDrive file or folder by ID, or the drive root',
12+
version: '1.0',
13+
14+
oauth: {
15+
required: true,
16+
provider: 'onedrive',
17+
},
18+
19+
params: {
20+
accessToken: {
21+
type: 'string',
22+
required: true,
23+
visibility: 'hidden',
24+
description: 'The access token for the OneDrive API',
25+
},
26+
fileId: {
27+
type: 'string',
28+
required: false,
29+
visibility: 'user-or-llm',
30+
description:
31+
'The ID of the file or folder to retrieve (e.g., "01BYE5RZ6QN3ZWBTUFOFD3GSPGOHDJD36M"). Leave empty to get the drive root folder',
32+
},
33+
},
34+
35+
request: {
36+
url: (params) => {
37+
const fileId = params.fileId?.trim()
38+
const baseUrl = fileId
39+
? `https://graph.microsoft.com/v1.0/me/drive/items/${encodeURIComponent(fileId)}`
40+
: 'https://graph.microsoft.com/v1.0/me/drive/root'
41+
42+
const url = new URL(baseUrl)
43+
url.searchParams.append(
44+
'$select',
45+
'id,name,file,folder,webUrl,size,createdDateTime,lastModifiedDateTime,parentReference,@microsoft.graph.downloadUrl'
46+
)
47+
return url.toString()
48+
},
49+
method: 'GET',
50+
headers: (params) => ({
51+
Authorization: `Bearer ${params.accessToken}`,
52+
}),
53+
},
54+
55+
transformResponse: async (response: Response) => {
56+
const data: MicrosoftGraphDriveItem = await response.json()
57+
58+
return {
59+
success: true,
60+
output: {
61+
file: {
62+
id: data.id,
63+
name: data.name,
64+
mimeType: data.file?.mimeType || (data.folder ? 'application/folder' : 'unknown'),
65+
webViewLink: data.webUrl,
66+
webContentLink: data['@microsoft.graph.downloadUrl'],
67+
size: data.size?.toString() || '0',
68+
createdTime: data.createdDateTime,
69+
modifiedTime: data.lastModifiedDateTime,
70+
parents: data.parentReference ? [data.parentReference.id] : [],
71+
},
72+
},
73+
}
74+
},
75+
76+
outputs: {
77+
success: { type: 'boolean', description: 'Whether the item metadata was retrieved' },
78+
file: {
79+
type: 'object',
80+
description:
81+
'The file or folder metadata, including id, name, webViewLink, size, and timestamps',
82+
},
83+
},
84+
}

apps/sim/tools/onedrive/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
1+
import { copyTool } from '@/tools/onedrive/copy'
12
import { createFolderTool } from '@/tools/onedrive/create_folder'
3+
import { createShareLinkTool } from '@/tools/onedrive/create_share_link'
24
import { deleteTool } from '@/tools/onedrive/delete'
35
import { downloadTool } from '@/tools/onedrive/download'
6+
import { getDriveInfoTool } from '@/tools/onedrive/get_drive_info'
7+
import { getItemTool } from '@/tools/onedrive/get_item'
48
import { listTool } from '@/tools/onedrive/list'
9+
import { moveTool } from '@/tools/onedrive/move'
10+
import { searchTool } from '@/tools/onedrive/search'
511
import { uploadTool } from '@/tools/onedrive/upload'
612

13+
export const onedriveCopyTool = copyTool
714
export const onedriveCreateFolderTool = createFolderTool
15+
export const onedriveCreateShareLinkTool = createShareLinkTool
816
export const onedriveDeleteTool = deleteTool
917
export const onedriveDownloadTool = downloadTool
18+
export const onedriveGetDriveInfoTool = getDriveInfoTool
19+
export const onedriveGetItemTool = getItemTool
1020
export const onedriveListTool = listTool
21+
export const onedriveMoveTool = moveTool
22+
export const onedriveSearchTool = searchTool
1123
export const onedriveUploadTool = uploadTool

0 commit comments

Comments
 (0)