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
291 changes: 291 additions & 0 deletions .agents/skills/migrate-application-operation/SKILL.md

Large diffs are not rendered by default.

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
interface:
display_name: "Migrate Application Operation"
short_description: "Share one operation across API and tool surfaces"
default_prompt: "Use $migrate-application-operation to migrate one resource operation across internal APIs, public APIs, Copilot, and other tools."
5 changes: 3 additions & 2 deletions .agents/skills/ship/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,11 +66,12 @@ When the user runs `/ship`:
exit 1
}
rm -f /tmp/ship-audit-results
for s in check:boundaries check:api-validation:strict check:desktop-bridge check:desktop-ipc \
for s in check:boundaries check:api-validation:strict check:openapi \
check:desktop-bridge check:desktop-ipc \
check:utils check:zustand-v5 \
check:react-query check:client-boundary check:bare-icons check:icon-paths \
check:realtime-prune check:tool-registry-boundary check:tool-request-boundary \
tool-metadata:check \
check:sql-date-binding tool-metadata:check \
integration-catalog:check skills:check agent-stream-docs:check; do
( bun run "$s" >"/tmp/ship-audit-${s//:/-}.log" 2>&1; echo "$? $s" >>/tmp/ship-audit-results ) &
done
Expand Down
290 changes: 290 additions & 0 deletions .claude/commands/migrate-application-operation.md

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions .claude/commands/ship.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,11 +65,12 @@ When the user runs `/ship`:
exit 1
}
rm -f /tmp/ship-audit-results
for s in check:boundaries check:api-validation:strict check:desktop-bridge check:desktop-ipc \
for s in check:boundaries check:api-validation:strict check:openapi \
check:desktop-bridge check:desktop-ipc \
check:utils check:zustand-v5 \
check:react-query check:client-boundary check:bare-icons check:icon-paths \
check:realtime-prune check:tool-registry-boundary check:tool-request-boundary \
tool-metadata:check \
check:sql-date-binding tool-metadata:check \
integration-catalog:check skills:check agent-stream-docs:check; do
( bun run "$s" >"/tmp/ship-audit-${s//:/-}.log" 2>&1; echo "$? $s" >>/tmp/ship-audit-results ) &
done
Expand Down
286 changes: 286 additions & 0 deletions .cursor/commands/migrate-application-operation.md

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions .cursor/commands/ship.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,11 +60,12 @@ When the user runs `/ship`:
exit 1
}
rm -f /tmp/ship-audit-results
for s in check:boundaries check:api-validation:strict check:desktop-bridge check:desktop-ipc \
for s in check:boundaries check:api-validation:strict check:openapi \
check:desktop-bridge check:desktop-ipc \
check:utils check:zustand-v5 \
check:react-query check:client-boundary check:bare-icons check:icon-paths \
check:realtime-prune check:tool-registry-boundary check:tool-request-boundary \
tool-metadata:check \
check:sql-date-binding tool-metadata:check \
integration-catalog:check skills:check agent-stream-docs:check; do
( bun run "$s" >"/tmp/ship-audit-${s//:/-}.log" 2>&1; echo "$? $s" >>/tmp/ship-audit-results ) &
done
Expand Down
23 changes: 10 additions & 13 deletions apps/sim/app/api/files/uploads/[uploadId]/complete/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,9 +2,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { completeInternalFileUploadContract } from '@/lib/api/contracts/upload-sessions'
import { parseRequest } from '@/lib/api/server'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { completeUploadSession, getOwnedUploadSession } from '@/lib/uploads/upload-session/service'
import { finalizeUploadPurpose } from '@/app/api/files/uploads/finalizers'
import { reauthorizeUploadPurpose } from '@/app/api/files/uploads/purposes'
import { completeInternalUploadSession } from '@/lib/uploads/upload-session/application'
import {
requireUploadUser,
toInternalUploadSession,
Expand All@@ -22,16 +20,15 @@ export const POST = withRouteHandler(async (request: NextRequest, context: Uploa
if (!parsed.success) return parsed.response

try {
const session = await getOwnedUploadSession({
uploadId: parsed.data.params.uploadId,
uploadToken: parsed.data.headers['upload-token'],
userId: actor.id,
})
await reauthorizeUploadPurpose(actor.id, session)
const completed = await completeUploadSession({
session,
finalize: (claimed) => finalizeUploadPurpose({ session: claimed, actor, request }),
})
const completed = await completeInternalUploadSession(
actor.principal,
{
uploadId: parsed.data.params.uploadId,
uploadToken: parsed.data.headers['upload-token'],
actor,
},
request
)
return NextResponse.json({
data: toInternalUploadSession(completed.session, completed.value),
})
Expand Down
25 changes: 11 additions & 14 deletions apps/sim/app/api/files/uploads/[uploadId]/parts/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,8 +2,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { createInternalFileUploadPartUrlsContract } from '@/lib/api/contracts/upload-sessions'
import { parseRequest } from '@/lib/api/server'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createUploadPartUrls, getOwnedUploadSession } from '@/lib/uploads/upload-session/service'
import { reauthorizeUploadPurpose } from '@/app/api/files/uploads/purposes'
import { issueInternalUploadPartUrls } from '@/lib/uploads/upload-session/application'
import { requireUploadUser, uploadSessionErrorResponse } from '@/app/api/files/uploads/utils'

interface UploadRouteParams {
Expand All@@ -17,18 +16,16 @@ export const POST = withRouteHandler(async (request: NextRequest, context: Uploa
if (!parsed.success) return parsed.response

try {
const session = await getOwnedUploadSession({
uploadId: parsed.data.params.uploadId,
uploadToken: parsed.data.headers['upload-token'],
userId: actor.id,
})
await reauthorizeUploadPurpose(actor.id, session)
const parts = await createUploadPartUrls({
session,
partNumbers: parsed.data.body.partNumbers,
localOrigin: request.nextUrl.origin,
})
return NextResponse.json({ data: { parts } })
const parts = await issueInternalUploadPartUrls(
actor.principal,
{
uploadId: parsed.data.params.uploadId,
uploadToken: parsed.data.headers['upload-token'],
partNumbers: parsed.data.body.partNumbers,
},
request
)
return NextResponse.json({ data: parts })
} catch (error) {
const classified = uploadSessionErrorResponse(error)
if (classified) return classified
Expand Down
8 changes: 2 additions & 6 deletions apps/sim/app/api/files/uploads/[uploadId]/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,8 +2,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { abortInternalFileUploadContract } from '@/lib/api/contracts/upload-sessions'
import { parseRequest } from '@/lib/api/server'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { abortUploadSession, getOwnedUploadSession } from '@/lib/uploads/upload-session/service'
import { reauthorizeUploadPurpose } from '@/app/api/files/uploads/purposes'
import { abortInternalUploadSession } from '@/lib/uploads/upload-session/application'
import {
requireUploadUser,
toInternalUploadSession,
Expand All@@ -21,13 +20,10 @@ export const DELETE = withRouteHandler(async (request: NextRequest, context: Upl
if (!parsed.success) return parsed.response

try {
const session = await getOwnedUploadSession({
const aborted = await abortInternalUploadSession(actor.principal, {
uploadId: parsed.data.params.uploadId,
uploadToken: parsed.data.headers['upload-token'],
userId: actor.id,
})
await reauthorizeUploadPurpose(actor.id, session)
const aborted = await abortUploadSession(session)
return NextResponse.json({ data: toInternalUploadSession(aborted, null) })
} catch (error) {
const classified = uploadSessionErrorResponse(error)
Expand Down
59 changes: 55 additions & 4 deletions apps/sim/app/api/files/uploads/finalizers.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -73,6 +73,7 @@ import { finalizeUploadPurpose } from '@/app/api/files/uploads/finalizers'

const now = new Date('2026-08-04T12:00:00.000Z')
const actor = { id: 'user-1', name: 'Ada', email: 'ada@example.com' }
const principal = { kind: 'session' as const, userId: actor.id, sessionId: 'session-1' }
const metadataRow = {
id: 'file-1',
key: 'workspace-logos/upload-1-logo.png',
Expand DownExpand Up@@ -147,8 +148,8 @@ describe('upload purpose finalizers', () => {
mockInsertReturning.mockResolvedValueOnce([metadataRow])
const request = new NextRequest('http://localhost/api/files/uploads/upload-1/complete')

const first = await finalizeUploadPurpose({ session: uploadSession, actor, request })
const retry = await finalizeUploadPurpose({ session: uploadSession, actor, request })
const first = await finalizeUploadPurpose({ session: uploadSession, actor, principal, request })
const retry = await finalizeUploadPurpose({ session: uploadSession, actor, principal, request })

expect(first.value).toEqual({
path: `/api/files/serve/s3/${encodeURIComponent(metadataRow.key)}?context=workspace-logos`,
Expand All@@ -169,6 +170,7 @@ describe('upload purpose finalizers', () => {
finalizeUploadPurpose({
session: uploadSession,
actor,
principal,
request: new NextRequest('http://localhost/api/files/uploads/upload-1/complete'),
})
).rejects.toMatchObject({ code: 'conflict' })
Expand All@@ -185,6 +187,7 @@ describe('upload purpose finalizers', () => {
finalizeUploadPurpose({
session: uploadSession,
actor,
principal,
request: new NextRequest('http://localhost/api/files/uploads/upload-1/complete'),
})
).rejects.toMatchObject({ code: 'conflict' })
Expand All@@ -208,10 +211,23 @@ describe('upload purpose finalizers', () => {
mockGetWorkspaceFile.mockResolvedValue(workspaceFile)
const request = new NextRequest('http://localhost/api/files/uploads/upload-1/complete')

const first = await finalizeUploadPurpose({ session: workspaceSession, actor, request })
const retry = await finalizeUploadPurpose({ session: workspaceSession, actor, request })
const first = await finalizeUploadPurpose({
session: workspaceSession,
actor,
principal,
request,
})
const retry = await finalizeUploadPurpose({
session: workspaceSession,
actor,
principal,
request,
})

expect(retry.value).toEqual(first.value)
expect(mockRegisterUploadedWorkspaceFile).toHaveBeenCalledWith(
expect.objectContaining({ uploadSessionId: workspaceSession.id })
)
expect(mockNotifyWorkspaceFilesChanged).toHaveBeenCalledTimes(1)
expect(mockRecordAudit).toHaveBeenCalledTimes(1)
expect(mockCaptureServerEvent).toHaveBeenCalledTimes(1)
Expand All@@ -236,11 +252,46 @@ describe('upload purpose finalizers', () => {
finalizeUploadPurpose({
session: workspaceSession,
actor,
principal,
request: new NextRequest('http://localhost/api/files/uploads/upload-1/complete'),
})
).rejects.toMatchObject({ code: 'conflict' })
expect(mockNotifyWorkspaceFilesChanged).not.toHaveBeenCalled()
expect(mockRecordAudit).not.toHaveBeenCalled()
expect(mockCaptureServerEvent).not.toHaveBeenCalled()
})

it('uses the current billing owner only for workspace-key legacy attribution', async () => {
const workspaceSession = {
...uploadSession,
purpose: 'workspace_file' as const,
storageContext: 'workspace' as const,
storageKey: workspaceFile.key,
finalKey: workspaceFile.key,
fileName: workspaceFile.name,
contentType: workspaceFile.type,
}
mockRegisterUploadedWorkspaceFile.mockResolvedValueOnce({
file: { id: workspaceFile.id },
created: true,
})
mockGetWorkspaceFile.mockResolvedValue(workspaceFile)
const request = new NextRequest('http://localhost/api/files/uploads/upload-1/complete')

await finalizeUploadPurpose({
session: workspaceSession,
actor: { id: 'current-owner' },
principal: {
kind: 'workspace_api_key',
workspaceId: 'workspace-1',
keyId: 'key-1',
},
request,
})

expect(mockRegisterUploadedWorkspaceFile).toHaveBeenCalledWith(
expect.objectContaining({ userId: 'current-owner' })
)
expect(mockCaptureServerEvent).not.toHaveBeenCalled()
})
})
Loading
Loading