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,161 @@
/**
* @vitest-environment node
*/
import {
authMockFns,
permissionsMock,
permissionsMockFns,
storageServiceMock,
storageServiceMockFns,
} from '@sim/testing'
import { NextRequest } from 'next/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { mockCheckStorageQuota, mockGenerateWorkspaceFileKey, mockUseBlobStorage } = vi.hoisted(
() => ({
mockCheckStorageQuota: vi.fn(),
mockGenerateWorkspaceFileKey: vi.fn(),
mockUseBlobStorage: { value: false },
})
)
vi.mock('@/lib/billing/storage', () => ({
checkStorageQuota: mockCheckStorageQuota,
}))
vi.mock('@/lib/uploads/core/storage-service', () => storageServiceMock)
vi.mock('@/lib/uploads/contexts/workspace/workspace-file-manager', () => ({
generateWorkspaceFileKey: mockGenerateWorkspaceFileKey,
}))
vi.mock('@/lib/uploads/config', () => ({
get USE_BLOB_STORAGE() {
return mockUseBlobStorage.value
},
}))
vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock)
const WS = '7727ef3f-8cf6-4686-b063-2bb006a10785'
import { POST } from '@/app/api/workspaces/[id]/files/presigned/route'
const params = (id = WS) => ({ params: Promise.resolve({ id }) })
const makeRequest = (body: unknown) =>
new NextRequest(`http://localhost/api/workspaces/${WS}/files/presigned`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
const validBody = {
fileName: 'video.mp4',
contentType: 'video/mp4',
fileSize: 10 * 1024 * 1024,
}
describe('POST /api/workspaces/[id]/files/presigned', () => {
beforeEach(() => {
vi.clearAllMocks()
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
permissionsMockFns.mockGetUserEntityPermissions.mockResolvedValue('write')
mockCheckStorageQuota.mockResolvedValue({ allowed: true })
storageServiceMockFns.mockHasCloudStorage.mockReturnValue(true)
mockGenerateWorkspaceFileKey.mockReturnValue(`workspace/${WS}/123-abc-video.mp4`)
storageServiceMockFns.mockGeneratePresignedUploadUrl.mockResolvedValue({
url: 'https://s3/presigned',
key: `workspace/${WS}/123-abc-video.mp4`,
uploadHeaders: { 'Content-Type': 'video/mp4' },
})
})
it('returns 401 when unauthenticated', async () => {
authMockFns.mockGetSession.mockResolvedValueOnce(null)
const res = await POST(makeRequest(validBody), params())
expect(res.status).toBe(401)
})
it('returns 403 when user has read-only permission', async () => {
permissionsMockFns.mockGetUserEntityPermissions.mockResolvedValueOnce('read')
const res = await POST(makeRequest(validBody), params())
expect(res.status).toBe(403)
})
it('returns 400 for missing fileName', async () => {
const res = await POST(makeRequest({ ...validBody, fileName: '' }), params())
expect(res.status).toBe(400)
})
it('returns 400 for negative fileSize', async () => {
const res = await POST(makeRequest({ ...validBody, fileSize: -1 }), params())
expect(res.status).toBe(400)
})
it('accepts fileSize === 0 (empty new files)', async () => {
const res = await POST(makeRequest({ ...validBody, fileSize: 0 }), params())
expect(res.status).toBe(200)
})
it('returns 413 when fileSize exceeds 5 GiB ceiling', async () => {
const res = await POST(
makeRequest({ ...validBody, fileSize: 6 * 1024 * 1024 * 1024 }),
params()
)
expect(res.status).toBe(413)
})
it('returns 413 when storage quota would be exceeded', async () => {
mockCheckStorageQuota.mockResolvedValueOnce({ allowed: false, error: 'Over quota' })
const res = await POST(makeRequest(validBody), params())
const body = await res.json()
expect(res.status).toBe(413)
expect(body.error).toBe('Over quota')
})
it('returns local fallback signal when cloud storage is not configured', async () => {
storageServiceMockFns.mockHasCloudStorage.mockReturnValueOnce(false)
const res = await POST(makeRequest(validBody), params())
const body = await res.json()
expect(res.status).toBe(200)
expect(body.directUploadSupported).toBe(false)
expect(body.presignedUrl).toBe('')
expect(body.fileInfo.name).toBe('video.mp4')
expect(storageServiceMockFns.mockGeneratePresignedUploadUrl).not.toHaveBeenCalled()
})
it('issues a presigned URL bound to the workspace', async () => {
const res = await POST(makeRequest(validBody), params())
const body = await res.json()
expect(res.status).toBe(200)
expect(body.directUploadSupported).toBe(true)
expect(body.presignedUrl).toBe('https://s3/presigned')
expect(body.fileInfo.key).toBe(`workspace/${WS}/123-abc-video.mp4`)
expect(body.fileInfo.path).toContain('?context=workspace')
expect(body.fileInfo.path).toContain('s3')
expect(body.uploadHeaders).toEqual({ 'Content-Type': 'video/mp4' })
expect(mockGenerateWorkspaceFileKey).toHaveBeenCalledWith(WS, 'video.mp4')
expect(storageServiceMockFns.mockGeneratePresignedUploadUrl).toHaveBeenCalledWith(
expect.objectContaining({
context: 'workspace',
userId: 'user-1',
customKey: `workspace/${WS}/123-abc-video.mp4`,
metadata: { workspaceId: WS },
})
)
})
it('serves blob path when blob storage is configured', async () => {
mockUseBlobStorage.value = true
try {
const res = await POST(makeRequest(validBody), params())
const body = await res.json()
expect(body.fileInfo.path).toContain('/blob/')
} finally {
mockUseBlobStorage.value = false
}
})
})
@@ -0,0 +1,109 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { workspacePresignedUploadContract } from '@/lib/api/contracts/workspace-files'
import { parseRequest } from '@/lib/api/server'
import { getSession } from '@/lib/auth'
import { checkStorageQuota } from '@/lib/billing/storage'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { USE_BLOB_STORAGE } from '@/lib/uploads/config'
import { assertWorkspaceFileFolderTarget } from '@/lib/uploads/contexts/workspace'
import { generateWorkspaceFileKey } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
import { generatePresignedUploadUrl, hasCloudStorage } from '@/lib/uploads/core/storage-service'
import { MAX_WORKSPACE_FILE_SIZE } from '@/lib/uploads/shared/types'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
const logger = createLogger('WorkspacePresignedAPI')
/**
* POST /api/workspaces/[id]/files/presigned
* Returns a presigned PUT URL for a workspace-scoped object key. The client
* uploads the bytes directly to S3/Blob, then calls /files/register to
* insert metadata.
*/
export const POST = withRouteHandler(
async (request: NextRequest, context: { params: Promise<{ id: string }> }) => {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const userId = session.user.id
const parsed = await parseRequest(workspacePresignedUploadContract, request, context)
if (!parsed.success) return parsed.response
const { params, body } = parsed.data
const workspaceId = params.id
const { fileName, contentType, fileSize, folderId } = body
const permission = await getUserEntityPermissions(userId, 'workspace', workspaceId)
if (permission !== 'admin' && permission !== 'write') {
logger.warn(`User ${userId} lacks write permission for ${workspaceId}`)
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
if (fileSize > MAX_WORKSPACE_FILE_SIZE) {
return NextResponse.json(
{ error: `File size exceeds maximum of ${MAX_WORKSPACE_FILE_SIZE} bytes` },
{ status: 413 }
)
}
let targetFolderId: string | null
try {
targetFolderId = await assertWorkspaceFileFolderTarget(workspaceId, folderId)
} catch (error) {
return NextResponse.json(
{ error: getErrorMessage(error, 'Invalid target folder') },
{ status: 400 }
)
}
if (!hasCloudStorage()) {
logger.info(`Local storage detected, signaling API fallback for ${fileName}`)
return NextResponse.json({
fileName,
presignedUrl: '',
fileInfo: { path: '', key: '', name: fileName, size: fileSize, type: contentType },
directUploadSupported: false,
})
}
const quotaCheck = await checkStorageQuota(userId, fileSize)
if (!quotaCheck.allowed) {
return NextResponse.json(
{ error: quotaCheck.error || 'Storage limit exceeded' },
{ status: 413 }
)
}
const key = generateWorkspaceFileKey(workspaceId, fileName)
const presigned = await generatePresignedUploadUrl({
fileName,
contentType,
fileSize,
context: 'workspace',
userId,
customKey: key,
expirationSeconds: 3600,
metadata: { workspaceId, ...(targetFolderId ? { folderId: targetFolderId } : {}) },
})
const finalPath = `/api/files/serve/${USE_BLOB_STORAGE ? 'blob' : 's3'}/${encodeURIComponent(key)}?context=workspace`
logger.info(`Issued workspace presigned URL for ${fileName} -> ${key}`)
return NextResponse.json({
fileName,
presignedUrl: presigned.url,
fileInfo: {
path: finalPath,
key: presigned.key,
name: fileName,
size: fileSize,
type: contentType,
},
uploadHeaders: presigned.uploadHeaders,
directUploadSupported: true,
})
}
)