chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,178 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { sharepointDownloadFileContract } from '@/lib/api/contracts/tools/microsoft'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
secureFetchWithPinnedIP,
validateUrlWithDNS,
} from '@/lib/core/security/input-validation.server'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { MAX_FILE_SIZE } from '@/lib/uploads/utils/validation'
export const dynamic = 'force-dynamic'
/** Microsoft Graph API error response structure */
interface GraphApiError {
error?: {
code?: string
message?: string
}
}
/** Microsoft Graph API drive item metadata response */
interface DriveItemMetadata {
id?: string
name?: string
folder?: Record<string, unknown>
file?: {
mimeType?: string
}
}
const logger = createLogger('SharepointDownloadFileAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized SharePoint download attempt: ${authResult.error}`)
return NextResponse.json(
{
success: false,
error: authResult.error || 'Authentication required',
},
{ status: 401 }
)
}
const parsed = await parseRequest(sharepointDownloadFileContract, request, {})
if (!parsed.success) return parsed.response
const { accessToken, driveId, itemId, fileName } = parsed.data.body
const authHeader = `Bearer ${accessToken}`
logger.info(`[${requestId}] Getting file metadata from SharePoint`, { driveId, itemId })
const metadataUrl = `https://graph.microsoft.com/v1.0/drives/${encodeURIComponent(driveId)}/items/${encodeURIComponent(itemId)}`
const metadataUrlValidation = await validateUrlWithDNS(metadataUrl, 'metadataUrl')
if (!metadataUrlValidation.isValid) {
return NextResponse.json(
{ success: false, error: metadataUrlValidation.error },
{ status: 400 }
)
}
const metadataResponse = await secureFetchWithPinnedIP(
metadataUrl,
metadataUrlValidation.resolvedIP!,
{
headers: { Authorization: authHeader },
}
)
if (!metadataResponse.ok) {
const errorDetails = (await metadataResponse.json().catch(() => ({}))) as GraphApiError
logger.error(`[${requestId}] Failed to get file metadata`, {
status: metadataResponse.status,
error: errorDetails,
})
return NextResponse.json(
{ success: false, error: errorDetails.error?.message || 'Failed to get file metadata' },
{ status: 400 }
)
}
const metadata = (await metadataResponse.json()) as DriveItemMetadata
if (metadata.folder && !metadata.file) {
logger.error(`[${requestId}] Attempted to download a folder`, {
itemId: metadata.id,
itemName: metadata.name,
})
return NextResponse.json(
{
success: false,
error: `Cannot download folder "${metadata.name}". Please select a file instead.`,
},
{ status: 400 }
)
}
const mimeType = metadata.file?.mimeType || 'application/octet-stream'
logger.info(`[${requestId}] Downloading file from SharePoint`, { driveId, itemId, mimeType })
const downloadUrl = `https://graph.microsoft.com/v1.0/drives/${encodeURIComponent(driveId)}/items/${encodeURIComponent(itemId)}/content`
const downloadUrlValidation = await validateUrlWithDNS(downloadUrl, 'downloadUrl')
if (!downloadUrlValidation.isValid) {
return NextResponse.json(
{ success: false, error: downloadUrlValidation.error },
{ status: 400 }
)
}
const downloadResponse = await secureFetchWithPinnedIP(
downloadUrl,
downloadUrlValidation.resolvedIP!,
{
headers: { Authorization: authHeader },
// The content endpoint 302s to a preauthenticated URL on a different origin that needs no auth.
stripAuthOnRedirect: true,
maxResponseBytes: MAX_FILE_SIZE,
}
)
if (!downloadResponse.ok) {
const downloadError = (await downloadResponse.json().catch(() => ({}))) as GraphApiError
logger.error(`[${requestId}] Failed to download file`, {
status: downloadResponse.status,
error: downloadError,
})
return NextResponse.json(
{ success: false, error: downloadError.error?.message || 'Failed to download file' },
{ status: 400 }
)
}
const arrayBuffer = await downloadResponse.arrayBuffer()
const fileBuffer = Buffer.from(arrayBuffer)
const resolvedName = fileName || metadata.name || 'download'
logger.info(`[${requestId}] File downloaded successfully`, {
driveId,
itemId,
name: resolvedName,
size: fileBuffer.length,
mimeType,
})
const base64Data = fileBuffer.toString('base64')
return NextResponse.json({
success: true,
output: {
file: {
name: resolvedName,
mimeType,
data: base64Data,
size: fileBuffer.length,
},
},
})
} catch (error) {
logger.error(`[${requestId}] Error downloading SharePoint file:`, error)
return NextResponse.json(
{
success: false,
error: getErrorMessage(error, 'Unknown error occurred'),
},
{ status: 500 }
)
}
})
@@ -0,0 +1,119 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { sharepointListsSelectorContract } from '@/lib/api/contracts/selectors'
import { parseRequest } from '@/lib/api/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { validateSharePointSiteId } from '@/lib/core/security/input-validation'
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 { assertGraphNextPageUrl, getGraphNextPageUrl } from '@/tools/sharepoint/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('SharePointListsAPI')
/**
* Upper bound on Microsoft Graph pages drained when listing SharePoint lists.
* Each page returns up to `$top=999` lists, so this caps the result set at
* roughly 10k lists while preventing an unbounded server-side loop.
*/
const MAX_LISTS_PAGES = 10
interface SharePointList {
id: string
displayName: string
description?: string
webUrl?: string
list?: {
hidden?: boolean
}
}
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const parsed = await parseRequest(sharepointListsSelectorContract, request, {})
if (!parsed.success) {
logger.warn(`[${requestId}] Invalid lists request data`)
return parsed.response
}
const { credential, workflowId, siteId } = parsed.data.body
const siteIdValidation = validateSharePointSiteId(siteId)
if (!siteIdValidation.isValid) {
logger.error(`[${requestId}] Invalid siteId: ${siteIdValidation.error}`)
return NextResponse.json({ error: siteIdValidation.error }, { status: 400 })
}
const authz = await authorizeCredentialUse(request, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error(`[${requestId}] Failed to obtain valid access token`)
return NextResponse.json(
{ error: 'Failed to obtain valid access token', authRequired: true },
{ status: 401 }
)
}
let nextUrl: string | undefined =
`https://graph.microsoft.com/v1.0/sites/${siteIdValidation.sanitized}/lists?$select=id,displayName,description,webUrl&$expand=list($select=hidden)&$top=999`
const rawLists: SharePointList[] = []
for (let page = 0; page < MAX_LISTS_PAGES && nextUrl; page++) {
const response = await fetch(nextUrl, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const errorData = await response
.json()
.catch(() => ({ error: { message: 'Unknown error' } }))
return NextResponse.json(
{ error: errorData.error?.message || 'Failed to fetch lists from SharePoint' },
{ status: response.status }
)
}
const data = await response.json()
if (Array.isArray(data.value)) {
rawLists.push(...data.value)
}
const nextLink = getGraphNextPageUrl(data)
nextUrl = nextLink ? assertGraphNextPageUrl(nextLink) : undefined
if (nextUrl && page === MAX_LISTS_PAGES - 1) {
logger.warn(
`[${requestId}] SharePoint lists pagination hit ${MAX_LISTS_PAGES}-page cap; result may be incomplete`
)
}
}
const lists = rawLists
.filter((list: SharePointList) => list.list?.hidden !== true)
.map((list: SharePointList) => ({
id: list.id,
displayName: list.displayName,
}))
logger.info(`[${requestId}] Successfully fetched ${lists.length} SharePoint lists`)
return NextResponse.json({ lists }, { status: 200 })
} catch (error) {
logger.error(`[${requestId}] Error fetching lists from SharePoint`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
})
@@ -0,0 +1,96 @@
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { sharepointSiteQuerySchema } from '@/lib/api/contracts/selectors/sharepoint'
import { getValidationErrorMessage } from '@/lib/api/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('SharePointSiteAPI')
export const GET = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const { searchParams } = new URL(request.url)
const validation = sharepointSiteQuerySchema.safeParse({
credentialId: searchParams.get('credentialId') ?? '',
siteId: searchParams.get('siteId') ?? '',
})
if (!validation.success) {
return NextResponse.json(
{ error: getValidationErrorMessage(validation.error, 'Invalid request') },
{ status: 400 }
)
}
const { credentialId, siteId } = validation.data
const siteIdValidation = validateMicrosoftGraphId(siteId, 'siteId')
if (!siteIdValidation.isValid) {
return NextResponse.json({ error: siteIdValidation.error }, { status: 400 })
}
const authz = await authorizeCredentialUse(request, { credentialId })
if (!authz.ok || !authz.credentialOwnerUserId || !authz.resolvedCredentialId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
authz.resolvedCredentialId,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
}
let endpoint: string
if (siteId === 'root') {
endpoint = 'sites/root'
} else if (siteId.includes(':')) {
endpoint = `sites/${siteId}`
} else if (siteId.includes('groups/')) {
endpoint = siteId
} else {
endpoint = `sites/${siteId}`
}
const response = await fetch(
`https://graph.microsoft.com/v1.0/${endpoint}?$select=id,name,displayName,webUrl,createdDateTime,lastModifiedDateTime`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
)
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: { message: 'Unknown error' } }))
return NextResponse.json(
{ error: errorData.error?.message || 'Failed to fetch site from SharePoint' },
{ status: response.status }
)
}
const site = await response.json()
const transformedSite = {
id: site.id,
name: site.displayName || site.name,
mimeType: 'application/vnd.microsoft.graph.site',
webViewLink: site.webUrl,
createdTime: site.createdDateTime,
modifiedTime: site.lastModifiedDateTime,
}
logger.info(`[${requestId}] Successfully fetched SharePoint site: ${transformedSite.name}`)
return NextResponse.json({ site: transformedSite }, { status: 200 })
} catch (error) {
logger.error(`[${requestId}] Error fetching site from SharePoint`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
})
@@ -0,0 +1,106 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { sharepointSitesSelectorContract } from '@/lib/api/contracts/selectors'
import { parseRequest } from '@/lib/api/server'
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 type { SharepointSite } from '@/tools/sharepoint/types'
import { assertGraphNextPageUrl, getGraphNextPageUrl } from '@/tools/sharepoint/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('SharePointSitesAPI')
/**
* Upper bound on Microsoft Graph pages drained when listing SharePoint sites.
* Each page returns up to `$top=999` sites, so this caps the result set at
* roughly 10k sites while preventing an unbounded server-side loop.
*/
const MAX_SITES_PAGES = 10
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const parsed = await parseRequest(sharepointSitesSelectorContract, request, {})
if (!parsed.success) {
logger.warn(`[${requestId}] Invalid sites request data`)
return parsed.response
}
const { credential, workflowId, query } = parsed.data.body
const authz = await authorizeCredentialUse(request, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error(`[${requestId}] Failed to obtain valid access token`)
return NextResponse.json(
{ error: 'Failed to obtain valid access token', authRequired: true },
{ status: 401 }
)
}
const searchQuery = query || '*'
let nextUrl: string | undefined =
`https://graph.microsoft.com/v1.0/sites?search=${encodeURIComponent(searchQuery)}&$select=id,name,displayName,webUrl,createdDateTime,lastModifiedDateTime&$top=999`
const rawSites: SharepointSite[] = []
for (let page = 0; page < MAX_SITES_PAGES && nextUrl; page++) {
const response = await fetch(nextUrl, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const errorData = await response
.json()
.catch(() => ({ error: { message: 'Unknown error' } }))
return NextResponse.json(
{ error: errorData.error?.message || 'Failed to fetch sites from SharePoint' },
{ status: response.status }
)
}
const data = await response.json()
if (Array.isArray(data.value)) {
rawSites.push(...data.value)
}
const nextLink = getGraphNextPageUrl(data)
nextUrl = nextLink ? assertGraphNextPageUrl(nextLink) : undefined
if (nextUrl && page === MAX_SITES_PAGES - 1) {
logger.warn(
`[${requestId}] SharePoint sites pagination hit ${MAX_SITES_PAGES}-page cap; result may be incomplete`
)
}
}
const sites = rawSites.map((site: SharepointSite) => ({
id: site.id,
name: site.displayName || site.name,
mimeType: 'application/vnd.microsoft.graph.site',
webViewLink: site.webUrl,
createdTime: site.createdDateTime,
modifiedTime: site.lastModifiedDateTime,
}))
logger.info(`[${requestId}] Successfully fetched ${sites.length} SharePoint sites`)
return NextResponse.json({ files: sites }, { status: 200 })
} catch (error) {
logger.error(`[${requestId}] Error fetching sites from SharePoint`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
})
@@ -0,0 +1,275 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { sharepointUploadContract } from '@/lib/api/contracts/tools/microsoft'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { docNotReadyResponse } from '@/lib/uploads/utils/servable-file-response'
import { assertToolFileAccess } from '@/app/api/files/authorization'
import type { MicrosoftGraphDriveItem } from '@/tools/onedrive/types'
import type { SharepointSkippedFile, SharepointUploadError } from '@/tools/sharepoint/types'
export const dynamic = 'force-dynamic'
const logger = createLogger('SharepointUploadAPI')
const MAX_SHAREPOINT_UPLOAD_BYTES = 250 * 1024 * 1024
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized SharePoint upload attempt: ${authResult.error}`)
return NextResponse.json(
{
success: false,
error: authResult.error || 'Authentication required',
},
{ status: 401 }
)
}
logger.info(
`[${requestId}] Authenticated SharePoint upload request via ${authResult.authType}`,
{
userId: authResult.userId,
}
)
const parsed = await parseRequest(sharepointUploadContract, request, {})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body
logger.info(`[${requestId}] Uploading files to SharePoint`, {
siteId: validatedData.siteId,
driveId: validatedData.driveId,
folderPath: validatedData.folderPath,
hasFiles: !!(validatedData.files && validatedData.files.length > 0),
fileCount: validatedData.files?.length || 0,
})
if (!validatedData.files || validatedData.files.length === 0) {
return NextResponse.json(
{
success: false,
error: 'At least one file is required for upload',
},
{ status: 400 }
)
}
const userFiles = processFilesToUserFiles(validatedData.files, requestId, logger)
if (userFiles.length === 0) {
return NextResponse.json(
{
success: false,
error: 'No valid files to upload',
},
{ status: 400 }
)
}
const siteId = validatedData.siteId.trim() || 'root'
const driveId = validatedData.driveId?.trim() || null
const uploadedFiles: MicrosoftGraphDriveItem[] = []
const skippedFiles: SharepointSkippedFile[] = []
const errors: SharepointUploadError[] = []
for (const userFile of userFiles) {
const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
if (denied) return denied
logger.info(`[${requestId}] Uploading file: ${userFile.name}`)
let buffer: Buffer
let downloadedContentType = ''
try {
const result = await downloadServableFileFromStorage(userFile, requestId, logger)
buffer = result.buffer
downloadedContentType = result.contentType
} catch (error) {
const notReady = docNotReadyResponse(error)
if (notReady) return notReady
throw error
}
const fileName = validatedData.fileName || userFile.name
const folderPath = validatedData.folderPath?.trim() || ''
const fileSizeMB = buffer.length / (1024 * 1024)
if (buffer.length > MAX_SHAREPOINT_UPLOAD_BYTES) {
logger.warn(
`[${requestId}] File ${fileName} is ${fileSizeMB.toFixed(2)}MB, exceeds 250MB limit`
)
skippedFiles.push({
name: fileName,
size: buffer.length,
limit: MAX_SHAREPOINT_UPLOAD_BYTES,
reason: 'File exceeds the 250 MB Microsoft Graph small upload limit',
})
continue
}
let uploadPath = ''
if (folderPath) {
const normalizedPath = folderPath.startsWith('/') ? folderPath : `/${folderPath}`
const cleanPath = normalizedPath.endsWith('/')
? normalizedPath.slice(0, -1)
: normalizedPath
uploadPath = `${cleanPath}/${fileName}`
} else {
uploadPath = `/${fileName}`
}
const encodedPath = uploadPath
.split('/')
.map((segment) => (segment ? encodeURIComponent(segment) : ''))
.join('/')
const uploadUrl = driveId
? `https://graph.microsoft.com/v1.0/drives/${encodeURIComponent(driveId)}/root:${encodedPath}:/content`
: `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/drive/root:${encodedPath}:/content`
logger.info(`[${requestId}] Uploading to: ${uploadUrl}`)
const uploadResponse = await secureFetchWithValidation(
uploadUrl,
{
method: 'PUT',
headers: {
Authorization: `Bearer ${validatedData.accessToken}`,
'Content-Type': downloadedContentType || userFile.type || 'application/octet-stream',
},
body: buffer,
},
'uploadUrl'
)
if (!uploadResponse.ok) {
const errorData = await uploadResponse.json().catch(() => ({}))
logger.error(`[${requestId}] Failed to upload file ${fileName}:`, errorData)
if (uploadResponse.status === 409) {
// File exists - retry with conflict behavior set to replace
logger.warn(`[${requestId}] File ${fileName} already exists, retrying with replace`)
const replaceUrl = `${uploadUrl}?@microsoft.graph.conflictBehavior=replace`
const replaceResponse = await secureFetchWithValidation(
replaceUrl,
{
method: 'PUT',
headers: {
Authorization: `Bearer ${validatedData.accessToken}`,
'Content-Type':
downloadedContentType || userFile.type || 'application/octet-stream',
},
body: buffer,
},
'replaceUrl'
)
if (!replaceResponse.ok) {
const replaceErrorData = (await replaceResponse.json().catch(() => ({}))) as {
error?: { message?: string }
}
logger.error(`[${requestId}] Failed to replace file ${fileName}:`, replaceErrorData)
errors.push({
name: fileName,
status: replaceResponse.status,
error: replaceErrorData.error?.message || `Failed to replace file: ${fileName}`,
})
continue
}
const replaceData = (await replaceResponse.json()) as {
id: string
name: string
webUrl: string
size: number
createdDateTime: string
lastModifiedDateTime: string
}
logger.info(`[${requestId}] File replaced successfully: ${fileName}`)
uploadedFiles.push({
id: replaceData.id,
name: replaceData.name,
webUrl: replaceData.webUrl,
size: replaceData.size,
createdDateTime: replaceData.createdDateTime,
lastModifiedDateTime: replaceData.lastModifiedDateTime,
})
continue
}
errors.push({
name: fileName,
status: uploadResponse.status,
error:
(errorData as { error?: { message?: string } }).error?.message ||
`Failed to upload file: ${fileName}`,
})
continue
}
const uploadData = (await uploadResponse.json()) as MicrosoftGraphDriveItem
logger.info(`[${requestId}] File uploaded successfully: ${fileName}`)
uploadedFiles.push({
id: uploadData.id,
name: uploadData.name,
webUrl: uploadData.webUrl,
size: uploadData.size,
createdDateTime: uploadData.createdDateTime,
lastModifiedDateTime: uploadData.lastModifiedDateTime,
})
}
if (uploadedFiles.length === 0) {
return NextResponse.json({
success: false,
error: 'No files were uploaded successfully',
output: {
uploadedFiles,
fileCount: 0,
skippedFiles,
skippedCount: skippedFiles.length,
errors,
},
})
}
logger.info(`[${requestId}] Completed SharePoint upload`, {
uploadedCount: uploadedFiles.length,
skippedCount: skippedFiles.length,
errorCount: errors.length,
})
return NextResponse.json({
success: true,
output: {
uploadedFiles,
fileCount: uploadedFiles.length,
skippedFiles,
skippedCount: skippedFiles.length,
errors,
},
})
} catch (error) {
logger.error(`[${requestId}] Error uploading files to SharePoint:`, error)
return NextResponse.json(
{
success: false,
error: toError(error).message,
},
{ status: 500 }
)
}
})