Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 6 additions & 14 deletions apps/sim/app/api/tools/microsoft_excel/drives/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { validatePathSegment, validateSharePointSiteId } from '@/lib/core/securi
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { GRAPH_ID_PATTERN } from '@/tools/microsoft_excel/utils'
import { extractGraphError, GRAPH_ID_PATTERN } from '@/tools/microsoft_excel/utils'

export const dynamic = 'force-dynamic'

Expand Down Expand Up @@ -76,13 +76,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
})

if (!response.ok) {
const errorData = await response
.json()
.catch(() => ({ error: { message: 'Unknown error' } }))
return NextResponse.json(
{ error: errorData.error?.message || 'Failed to fetch drive' },
{ status: response.status }
)
const errorMessage = await extractGraphError(response)
return NextResponse.json({ error: errorMessage }, { status: response.status })
}

const data: GraphDrive = await response.json()
Expand All @@ -102,15 +97,12 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
})

if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: { message: 'Unknown error' } }))
const errorMessage = await extractGraphError(response)
logger.error(`[${requestId}] Microsoft Graph API error fetching drives`, {
status: response.status,
error: errorData.error?.message,
error: errorMessage,
})
return NextResponse.json(
{ error: errorData.error?.message || 'Failed to fetch drives' },
{ status: response.status }
)
return NextResponse.json({ error: errorMessage }, { status: response.status })
}

const data = await response.json()
Expand Down
14 changes: 4 additions & 10 deletions apps/sim/app/api/tools/microsoft_excel/sheets/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { getItemBasePath } from '@/tools/microsoft_excel/utils'
import { extractGraphError, getItemBasePath } from '@/tools/microsoft_excel/utils'

export const dynamic = 'force-dynamic'

Expand Down Expand Up @@ -73,18 +73,12 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
})

if (!worksheetsResponse.ok) {
const errorData = await worksheetsResponse
.text()
.then((text) => JSON.parse(text))
.catch(() => ({ error: { message: 'Unknown error' } }))
const errorMessage = await extractGraphError(worksheetsResponse)
logger.error(`[${requestId}] Microsoft Graph API error`, {
status: worksheetsResponse.status,
error: errorData.error?.message || 'Failed to fetch worksheets',
error: errorMessage,
})
return NextResponse.json(
{ error: errorData.error?.message || 'Failed to fetch worksheets' },
{ status: worksheetsResponse.status }
)
return NextResponse.json({ error: errorMessage }, { status: worksheetsResponse.status })
}

const data: WorksheetsResponse = await worksheetsResponse.json()
Expand Down
50 changes: 50 additions & 0 deletions apps/sim/app/chat/[identifier]/office-embed-init.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use client'

import Script from 'next/script'

declare global {
interface Window {
Office?: {
onReady: () => Promise<{ host: string | null; platform: string | null }>
}
}
}

/**
* Office.js nullifies window.history.replaceState and pushState (a legacy
* IE10 workaround inside the library) which breaks Next.js's client-side
* router. Cache the originals at module load — before <Script> renders
* Office.js into the DOM — so we can restore them after it loads.
*
* See https://learn.microsoft.com/en-us/answers/questions/1070090/using-office-javascript-api-in-next-js.
*/
const cachedHistory =
typeof window !== 'undefined'
? {
replaceState: window.history.replaceState.bind(window.history),
pushState: window.history.pushState.bind(window.history),
}
: null

/**
* Loads Office.js and signals readiness so Office host applications
* (Excel, Word, PowerPoint, Outlook) recognize this page as a valid add-in.
*
* Office.onReady() must be called once Office.js is loaded — see
* https://learn.microsoft.com/en-us/javascript/api/office#office-office-onready-function(1).
*/
export function OfficeEmbedInit() {
return (
<Script
src='https://appsforoffice.microsoft.com/lib/1/hosted/office.js'
strategy='afterInteractive'
onReady={() => {
if (cachedHistory) {
window.history.replaceState = cachedHistory.replaceState
window.history.pushState = cachedHistory.pushState
}
void window.Office?.onReady()
}}
/>
)
}
19 changes: 17 additions & 2 deletions apps/sim/app/chat/[identifier]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
import type { Metadata } from 'next'
import ChatClient from '@/app/chat/[identifier]/chat'
import { OfficeEmbedInit } from '@/app/chat/[identifier]/office-embed-init'

export const metadata: Metadata = {
title: 'Chat',
}

export default async function ChatPage({ params }: { params: Promise<{ identifier: string }> }) {
export default async function ChatPage({
params,
searchParams,
}: {
params: Promise<{ identifier: string }>
searchParams: Promise<Record<string, string | string[] | undefined>>
}) {
const { identifier } = await params
return <ChatClient key={identifier} identifier={identifier} />
const { embed } = await searchParams
const isOfficeEmbed = embed === 'office' || (Array.isArray(embed) && embed.includes('office'))

return (
<>
{isOfficeEmbed && <OfficeEmbedInit />}
<ChatClient key={identifier} identifier={identifier} />
</>
)
}
19 changes: 19 additions & 0 deletions apps/sim/lib/core/security/csp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
buildTimeCSPDirectives,
type CSPDirectives,
generateRuntimeCSP,
getChatEmbedCSPPolicy,
getMainCSPPolicy,
getWorkflowExecutionCSPPolicy,
removeCSPSource,
Expand Down Expand Up @@ -278,3 +279,21 @@ describe('buildTimeCSPDirectives', () => {
expect(buildTimeCSPDirectives['img-src']).toContain('blob:')
})
})

describe('getChatEmbedCSPPolicy', () => {
it('allows iframe embedding from any origin', () => {
expect(getChatEmbedCSPPolicy()).toContain('frame-ancestors *')
})

it('allows Office.js to load from Microsoft for Excel/Word add-in embedding', () => {
const policy = getChatEmbedCSPPolicy()
expect(policy).toMatch(/script-src[^;]*https:\/\/appsforoffice\.microsoft\.com/)
expect(policy).toMatch(/connect-src[^;]*https:\/\/appsforoffice\.microsoft\.com/)
})

it('does not regress object-src or base-uri restrictions', () => {
const policy = getChatEmbedCSPPolicy()
expect(policy).toContain("object-src 'none'")
expect(policy).toContain("base-uri 'self'")
})
})
15 changes: 13 additions & 2 deletions apps/sim/lib/core/security/csp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,10 +236,21 @@ function getEmbedCSPPolicy(): string {
}

/**
* CSP for embeddable chat pages
* CSP for embeddable chat pages.
* Extends the shared embed policy with Microsoft Office.js sources so the
* chat page can serve as an Office (Excel/Word/Outlook) add-in surface
* when loaded with `?embed=office`.
*/
export function getChatEmbedCSPPolicy(): string {
return getEmbedCSPPolicy()
return buildCSPString({
...buildTimeCSPDirectives,
'script-src': [...STATIC_SCRIPT_SRC, 'https://appsforoffice.microsoft.com'],
'connect-src': [
...(buildTimeCSPDirectives['connect-src'] ?? []),
'https://appsforoffice.microsoft.com',
],
'frame-ancestors': ['*'],
})
}

/**
Expand Down
10 changes: 10 additions & 0 deletions apps/sim/tools/error-extractors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
* 2. Add the ID to ErrorExtractorId constant at the bottom of this file
*/

import { parseGraphErrorFromData } from '@/tools/microsoft_excel/utils'

export interface ErrorInfo {
status?: number
statusText?: string
Expand Down Expand Up @@ -184,6 +186,13 @@ const ERROR_EXTRACTORS: ErrorExtractorConfig[] = [
examples: ['Microsoft OAuth', 'Google OAuth', 'OAuth2 providers'],
extract: (errorInfo) => errorInfo?.data?.error_description,
},
{
id: 'microsoft-graph-errors',
description:
'Microsoft Graph error format with nested innerError chain and details[] (Excel, OneDrive, SharePoint, Outlook). See https://learn.microsoft.com/en-us/graph/errors',
examples: ['Microsoft Excel', 'Microsoft OneDrive', 'Microsoft SharePoint'],
extract: (errorInfo) => parseGraphErrorFromData(errorInfo?.data),
},
{
id: 'nested-error-object',
description: 'Error field containing nested object or string',
Comment thread
waleedlatif1 marked this conversation as resolved.
Expand Down Expand Up @@ -260,6 +269,7 @@ export function extractErrorMessage(errorInfo?: ErrorInfo, extractorId?: string)

export const ErrorExtractorId = {
ATLASSIAN_ERRORS: 'atlassian-errors',
MICROSOFT_GRAPH_ERRORS: 'microsoft-graph-errors',
GRAPHQL_ERRORS: 'graphql-errors',
TWITTER_ERRORS: 'twitter-errors',
DETAILS_ARRAY: 'details-array',
Expand Down
18 changes: 17 additions & 1 deletion apps/sim/tools/microsoft_excel/read.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ErrorExtractorId } from '@/tools/error-extractors'
import type {
ExcelCellValue,
MicrosoftExcelReadResponse,
Expand All @@ -8,15 +9,25 @@ import type {
import {
getItemBasePath,
getSpreadsheetWebUrl,
parseGraphErrorMessage,
trimTrailingEmptyRowsAndColumns,
} from '@/tools/microsoft_excel/utils'
import type { ToolConfig } from '@/tools/types'

const EXCEL_RETRY_CONFIG = {
enabled: true,
maxRetries: 3,
initialDelayMs: 500,
maxDelayMs: 30000,
retryIdempotentOnly: true,
} as const

export const readTool: ToolConfig<MicrosoftExcelToolParams, MicrosoftExcelReadResponse> = {
id: 'microsoft_excel_read',
name: 'Read from Microsoft Excel',
description: 'Read data from a Microsoft Excel spreadsheet',
version: '1.0',
errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS,

oauth: {
required: true,
Expand Down Expand Up @@ -95,6 +106,7 @@ export const readTool: ToolConfig<MicrosoftExcelToolParams, MicrosoftExcelReadRe
Authorization: `Bearer ${params.accessToken}`,
}
},
retry: EXCEL_RETRY_CONFIG,
},

transformResponse: async (response: Response, params?: MicrosoftExcelToolParams) => {
Expand Down Expand Up @@ -123,8 +135,10 @@ export const readTool: ToolConfig<MicrosoftExcelToolParams, MicrosoftExcelReadRe
})

if (!rangeResp.ok) {
const errorText = await rangeResp.text().catch(() => '')
const detail = parseGraphErrorMessage(rangeResp.status, rangeResp.statusText, errorText)
throw new Error(
'Invalid range provided or worksheet not found. Provide a range like "Sheet1!A1:B2" or just the sheet name to read the whole sheet'
`Failed to read worksheet "${firstSheetName}": ${detail}. Provide a range like "Sheet1!A1:B2" or just the sheet name to read the whole sheet.`
)
}

Expand Down Expand Up @@ -209,6 +223,7 @@ export const readV2Tool: ToolConfig<MicrosoftExcelV2ToolParams, MicrosoftExcelV2
name: 'Read from Microsoft Excel V2',
description: 'Read data from a specific sheet in a Microsoft Excel spreadsheet',
version: '2.0.0',
errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS,

oauth: {
required: true,
Expand Down Expand Up @@ -284,6 +299,7 @@ export const readV2Tool: ToolConfig<MicrosoftExcelV2ToolParams, MicrosoftExcelV2
Authorization: `Bearer ${params.accessToken}`,
}
},
retry: EXCEL_RETRY_CONFIG,
},

transformResponse: async (response: Response, params?: MicrosoftExcelV2ToolParams) => {
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/tools/microsoft_excel/table_add.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ErrorExtractorId } from '@/tools/error-extractors'
import type {
MicrosoftExcelTableAddResponse,
MicrosoftExcelTableToolParams,
Expand All @@ -13,6 +14,7 @@ export const tableAddTool: ToolConfig<
name: 'Add to Microsoft Excel Table',
description: 'Add new rows to a Microsoft Excel table',
version: '1.0',
errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS,

oauth: {
required: true,
Expand Down
Loading
Loading