Skip to content

Commit 8e58493

Browse files
committed
Fix build
1 parent f2095ea commit 8e58493

7 files changed

Lines changed: 64 additions & 180 deletions

File tree

apps/sim/app/api/mothership/execute/route.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
9797
messages,
9898
responseFormat,
9999
workspaceId,
100-
userId,
100+
userId: bodyUserId,
101101
chatId,
102102
messageId: providedMessageId,
103103
requestId: providedRequestId,
@@ -107,6 +107,23 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
107107
userMetadata,
108108
} = validation.data.body
109109

110+
// Bind the billing actor to the authenticated identity. The executor mints
111+
// the internal JWT with the workflow owner's userId, so a token issued for
112+
// one user must never be used to attribute mothership-block cost to another
113+
// user via a forged body.userId. When the token carries a userId we require
114+
// the body to match it; the JWT userId is authoritative.
115+
if (auth.userId && auth.userId !== bodyUserId) {
116+
logger.warn('Mothership execute userId does not match authenticated identity', {
117+
tokenUserId: auth.userId,
118+
bodyUserId,
119+
})
120+
return NextResponse.json(
121+
{ error: 'userId does not match authenticated identity' },
122+
{ status: 403 }
123+
)
124+
}
125+
const userId = auth.userId ?? bodyUserId
126+
110127
await assertActiveWorkspaceAccess(workspaceId, userId)
111128

112129
const effectiveChatId = chatId || generateId()

apps/sim/app/api/workflows/[id]/execute/route.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -827,7 +827,10 @@ async function handleExecutePost(
827827
}
828828

829829
const effectiveWorkflowStateOverride =
830-
sanitizedWorkflowStateOverride || cachedWorkflowData || undefined
830+
// double-cast-allowed: workflowStateSchema is structurally a supertype of the executor's reactflow-typed override (edges[].style is Record<string, unknown> vs CSSProperties); validated bodies carry store-shaped values so the runtime shape matches
831+
(sanitizedWorkflowStateOverride as unknown as ExecutionMetadata['workflowStateOverride']) ||
832+
cachedWorkflowData ||
833+
undefined
831834
const largeValueExecutionIds = [executionId]
832835
const largeValueKeys: string[] = []
833836
const fileKeys: string[] = []

apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok-key-manager.tsx

Lines changed: 26 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Eye, EyeOff, Search } from 'lucide-react'
77
import {
88
Button,
99
Chip,
10+
ChipConfirmModal,
1011
ChipModal,
1112
ChipModalBody,
1213
ChipModalError,
@@ -293,47 +294,42 @@ export function BYOKKeyManager({
293294
</ChipModalField>
294295
<ChipModalError>{error}</ChipModalError>
295296
</ChipModalBody>
296-
<ChipModalFooter>
297-
<Chip variant='filled' flush onClick={closeEditModal} disabled={isSaving}>
298-
Cancel
299-
</Chip>
300-
<Chip
301-
variant='primary'
302-
flush
303-
onClick={handleSave}
304-
disabled={!apiKeyInput.trim() || isSaving}
305-
>
306-
{isSaving ? 'Saving...' : 'Save'}
307-
</Chip>
308-
</ChipModalFooter>
297+
<ChipModalFooter
298+
onCancel={closeEditModal}
299+
cancelDisabled={isSaving}
300+
primaryAction={{
301+
label: isSaving ? 'Saving...' : 'Save',
302+
onClick: handleSave,
303+
disabled: !apiKeyInput.trim() || isSaving,
304+
}}
305+
/>
309306
</ChipModal>
310307

311-
<ChipModal
308+
<ChipConfirmModal
312309
open={!!deleteConfirmProvider}
313-
onOpenChange={() => setDeleteConfirmProvider(null)}
310+
onOpenChange={(open) => {
311+
if (!open) setDeleteConfirmProvider(null)
312+
}}
314313
srTitle='Delete API Key'
315-
>
316-
<ChipModalHeader showDivider={false}>Delete API Key</ChipModalHeader>
317-
<ChipModalBody>
318-
<p className='px-2 text-[var(--text-secondary)] text-sm'>
314+
title='Delete API Key'
315+
description={
316+
<>
319317
Are you sure you want to delete the{' '}
320318
<span className='font-medium text-[var(--text-primary)]'>{deleteMeta?.name}</span> API
321319
key?{' '}
322320
<span className='text-[var(--text-error)]'>
323321
This workspace will revert to using platform hosted keys.
324322
</span>{' '}
325323
This action cannot be undone.
326-
</p>
327-
</ChipModalBody>
328-
<ChipModalFooter>
329-
<Chip variant='filled' flush onClick={() => setDeleteConfirmProvider(null)}>
330-
Cancel
331-
</Chip>
332-
<Chip variant='destructive' flush onClick={handleDelete} disabled={isDeleting}>
333-
{isDeleting ? 'Deleting...' : 'Delete'}
334-
</Chip>
335-
</ChipModalFooter>
336-
</ChipModal>
324+
</>
325+
}
326+
confirm={{
327+
label: 'Delete',
328+
onClick: handleDelete,
329+
pending: isDeleting,
330+
pendingLabel: 'Deleting...',
331+
}}
332+
/>
337333
</>
338334
)
339335
}

apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx

Lines changed: 0 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,6 @@
22

33
import { useMemo } from 'react'
44
import { useParams } from 'next/navigation'
5-
import {
6-
Button,
7-
ChipConfirmModal,
8-
ChipInput,
9-
ChipModal,
10-
ChipModalBody,
11-
ChipModalError,
12-
ChipModalField,
13-
ChipModalFooter,
14-
ChipModalHeader,
15-
} from '@/components/emcn'
165
import {
176
AnthropicIcon,
187
BasetenIcon,
@@ -321,128 +310,6 @@ export function BYOK() {
321310
/>
322311
</div>
323312
</div>
324-
325-
<ChipModal
326-
open={!!editingProvider}
327-
onOpenChange={(open) => {
328-
if (!open) {
329-
setEditingProvider(null)
330-
setApiKeyInput('')
331-
setShowApiKey(false)
332-
setError(null)
333-
}
334-
}}
335-
srTitle='Add/Update API Key'
336-
>
337-
<ChipModalHeader
338-
onClose={() => {
339-
setEditingProvider(null)
340-
setApiKeyInput('')
341-
setShowApiKey(false)
342-
setError(null)
343-
}}
344-
>
345-
{editingProvider && (
346-
<>
347-
{getKeyForProvider(editingProvider) ? 'Update' : 'Add'}{' '}
348-
{PROVIDERS.find((p) => p.id === editingProvider)?.name} API Key
349-
</>
350-
)}
351-
</ChipModalHeader>
352-
<ChipModalBody>
353-
<p className='px-2 text-[var(--text-secondary)] text-sm'>
354-
This key will be used for all {PROVIDERS.find((p) => p.id === editingProvider)?.name}{' '}
355-
requests in this workspace. Your key is encrypted and stored securely.
356-
</p>
357-
<ChipModalField type='custom' title='API Key' required>
358-
{/* Hidden decoy fields to prevent browser autofill */}
359-
<input
360-
type='text'
361-
name='fakeusernameremembered'
362-
autoComplete='username'
363-
style={{
364-
position: 'absolute',
365-
left: '-9999px',
366-
opacity: 0,
367-
pointerEvents: 'none',
368-
}}
369-
tabIndex={-1}
370-
readOnly
371-
/>
372-
<ChipInput
373-
type={showApiKey ? 'text' : 'password'}
374-
value={apiKeyInput}
375-
onChange={(e) => {
376-
setApiKeyInput(e.target.value)
377-
if (error) setError(null)
378-
}}
379-
placeholder={PROVIDERS.find((p) => p.id === editingProvider)?.placeholder}
380-
name='byok_api_key'
381-
autoComplete='off'
382-
autoCorrect='off'
383-
autoCapitalize='off'
384-
data-lpignore='true'
385-
data-form-type='other'
386-
endAdornment={
387-
<Button
388-
variant='ghost'
389-
className='size-[28px] p-0'
390-
onClick={() => setShowApiKey(!showApiKey)}
391-
>
392-
{showApiKey ? (
393-
<EyeOff className='size-[14px]' />
394-
) : (
395-
<Eye className='size-[14px]' />
396-
)}
397-
</Button>
398-
}
399-
/>
400-
</ChipModalField>
401-
<ChipModalError>{error}</ChipModalError>
402-
</ChipModalBody>
403-
<ChipModalFooter
404-
onCancel={() => {
405-
setEditingProvider(null)
406-
setApiKeyInput('')
407-
setShowApiKey(false)
408-
setError(null)
409-
}}
410-
cancelDisabled={upsertKey.isPending}
411-
primaryAction={{
412-
label: upsertKey.isPending ? 'Saving...' : 'Save',
413-
onClick: handleSave,
414-
disabled: !apiKeyInput.trim() || upsertKey.isPending,
415-
}}
416-
/>
417-
</ChipModal>
418-
419-
<ChipConfirmModal
420-
open={!!deleteConfirmProvider}
421-
onOpenChange={(open) => {
422-
if (!open) setDeleteConfirmProvider(null)
423-
}}
424-
srTitle='Delete API Key'
425-
title='Delete API Key'
426-
description={
427-
<>
428-
Are you sure you want to delete the{' '}
429-
<span className='font-medium text-[var(--text-primary)]'>
430-
{PROVIDERS.find((p) => p.id === deleteConfirmProvider)?.name}
431-
</span>{' '}
432-
API key?{' '}
433-
<span className='text-[var(--text-error)]'>
434-
This workspace will revert to using platform hosted keys.
435-
</span>{' '}
436-
This action cannot be undone.
437-
</>
438-
}
439-
confirm={{
440-
label: 'Delete',
441-
onClick: handleDelete,
442-
pending: deleteKey.isPending,
443-
pendingLabel: 'Deleting...',
444-
}}
445-
/>
446313
</div>
447314
)
448315
}

apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/manage-credits-modal/manage-credits-modal.tsx

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import { useEffect, useRef, useState } from 'react'
44
import { getErrorMessage } from '@sim/utils/errors'
55
import {
6-
Chip,
76
ChipModal,
87
ChipModalBody,
98
ChipModalError,
@@ -128,19 +127,15 @@ export function ManageCreditsModal({
128127
/>
129128
<ChipModalError>{error}</ChipModalError>
130129
</ChipModalBody>
131-
<ChipModalFooter>
132-
<Chip variant='filled' flush onClick={() => onOpenChange(false)} disabled={isSaving}>
133-
Cancel
134-
</Chip>
135-
<Chip
136-
variant='primary'
137-
flush
138-
onClick={handleSave}
139-
disabled={!isValid || !isDirty || isSaving || isLoading}
140-
>
141-
{isSaving ? 'Saving…' : 'Save'}
142-
</Chip>
143-
</ChipModalFooter>
130+
<ChipModalFooter
131+
onCancel={() => onOpenChange(false)}
132+
cancelDisabled={isSaving}
133+
primaryAction={{
134+
label: isSaving ? 'Saving…' : 'Save',
135+
onClick: handleSave,
136+
disabled: !isValid || !isDirty || isSaving || isLoading,
137+
}}
138+
/>
144139
</ChipModal>
145140
)
146141
}

apps/sim/lib/auth/internal.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ import { getClientIp } from '@/lib/core/utils/request'
88
const logger = createLogger('CronAuth')
99

1010
const getJwtSecret = () => {
11-
const secret = new TextEncoder().encode(env.INTERNAL_API_SECRET)
11+
// Prefer a dedicated JWT signing key so the internal-JWT trust domain is
12+
// separable from the raw INTERNAL_API_SECRET shared-bearer secret: leaking one
13+
// shouldn't grant the other (raw secret => call internal endpoints; JWT key =>
14+
// mint tokens for arbitrary userIds). Falls back to INTERNAL_API_SECRET when
15+
// unset so existing deployments keep working until the key is rotated in.
16+
const secret = new TextEncoder().encode(env.INTERNAL_JWT_SECRET || env.INTERNAL_API_SECRET)
1217
return secret
1318
}
1419

apps/sim/lib/core/config/env.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export const env = createEnv({
3535
ENCRYPTION_KEY: z.string().min(32), // Key for encrypting sensitive data
3636
API_ENCRYPTION_KEY: z.string().min(32).optional(), // Dedicated key for encrypting API keys (optional for OSS)
3737
INTERNAL_API_SECRET: z.string().min(32), // Secret for internal API authentication
38+
INTERNAL_JWT_SECRET: z.string().min(32).optional(), // Dedicated signing key for internal JWTs (falls back to INTERNAL_API_SECRET); separating limits blast radius if one leaks
3839

3940
// Copilot
4041
COPILOT_API_KEY: z.string().min(1).optional(), // Secret for internal sim agent API authentication

0 commit comments

Comments
 (0)