Skip to content

Commit 34d92fa

Browse files
authored
v0.5.96: sim oauth provider, slack ephemeral message tool and blockkit support
2 parents 67aa4bb + a98463a commit 34d92fa

File tree

36 files changed

+13092
-99
lines changed

36 files changed

+13092
-99
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,6 @@ jobs:
144144
tags: ${{ steps.meta.outputs.tags }}
145145
provenance: false
146146
sbom: false
147-
no-cache: true
148147

149148
# Build ARM64 images for GHCR (main branch only, runs in parallel)
150149
build-ghcr-arm64:
@@ -205,7 +204,6 @@ jobs:
205204
tags: ${{ steps.meta.outputs.tags }}
206205
provenance: false
207206
sbom: false
208-
no-cache: true
209207

210208
# Create GHCR multi-arch manifests (only for main, after both builds)
211209
create-ghcr-manifests:

.github/workflows/images.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,6 @@ jobs:
9797
tags: ${{ steps.meta.outputs.tags }}
9898
provenance: false
9999
sbom: false
100-
no-cache: true
101100

102101
build-ghcr-arm64:
103102
name: Build ARM64 (GHCR Only)
@@ -144,11 +143,10 @@ jobs:
144143
tags: ${{ steps.meta.outputs.tags }}
145144
provenance: false
146145
sbom: false
147-
no-cache: true
148146

149147
create-ghcr-manifests:
150148
name: Create GHCR Manifests
151-
runs-on: blacksmith-8vcpu-ubuntu-2404
149+
runs-on: blacksmith-2vcpu-ubuntu-2404
152150
needs: [build-amd64, build-ghcr-arm64]
153151
if: github.ref == 'refs/heads/main'
154152
strategy:

.github/workflows/test-build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ jobs:
110110
RESEND_API_KEY: 'dummy_key_for_ci_only'
111111
AWS_REGION: 'us-west-2'
112112
ENCRYPTION_KEY: '7cf672e460e430c1fba707575c2b0e2ad5a99dddf9b7b7e3b5646e630861db1c' # dummy key for CI only
113-
run: bun run build
113+
run: bunx turbo run build --filter=sim
114114

115115
- name: Upload coverage to Codecov
116116
uses: codecov/codecov-action@v5

apps/docs/content/docs/en/tools/jira_service_management.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ Create a new service request in Jira Service Management
116116
| `summary` | string | Yes | Summary/title for the service request |
117117
| `description` | string | No | Description for the service request |
118118
| `raiseOnBehalfOf` | string | No | Account ID of customer to raise request on behalf of |
119-
| `requestFieldValues` | json | No | Custom field values as key-value pairs \(overrides summary/description if provided\) |
119+
| `requestFieldValues` | json | No | Request field values as key-value pairs \(overrides summary/description if provided\) |
120120
| `requestParticipants` | string | No | Comma-separated account IDs to add as request participants |
121121
| `channel` | string | No | Channel the request originates from \(e.g., portal, email\) |
122122

apps/docs/content/docs/en/tools/slack.mdx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Slack
3-
description: Send, update, delete messages, add reactions in Slack or trigger workflows from Slack events
3+
description: Send, update, delete messages, send ephemeral messages, add reactions in Slack or trigger workflows from Slack events
44
---
55

66
import { BlockInfoCard } from "@/components/ui/block-info-card"
@@ -59,7 +59,7 @@ If you encounter issues with the Slack integration, contact us at [help@sim.ai](
5959

6060
## Usage Instructions
6161

62-
Integrate Slack into the workflow. Can send, update, and delete messages, create canvases, read messages, and add reactions. Requires Bot Token instead of OAuth in advanced mode. Can be used in trigger mode to trigger a workflow when a message is sent to a channel.
62+
Integrate Slack into the workflow. Can send, update, and delete messages, send ephemeral messages visible only to a specific user, create canvases, read messages, and add reactions. Requires Bot Token instead of OAuth in advanced mode. Can be used in trigger mode to trigger a workflow when a message is sent to a channel.
6363

6464

6565

@@ -80,6 +80,7 @@ Send messages to Slack channels or direct messages. Supports Slack mrkdwn format
8080
| `dmUserId` | string | No | Slack user ID for direct messages \(e.g., U1234567890\) |
8181
| `text` | string | Yes | Message text to send \(supports Slack mrkdwn formatting\) |
8282
| `threadTs` | string | No | Thread timestamp to reply to \(creates thread reply\) |
83+
| `blocks` | json | No | Block Kit layout blocks as a JSON array. When provided, text becomes the fallback notification text. |
8384
| `files` | file[] | No | Files to attach to the message |
8485

8586
#### Output
@@ -146,6 +147,29 @@ Send messages to Slack channels or direct messages. Supports Slack mrkdwn format
146147
| `fileCount` | number | Number of files uploaded \(when files are attached\) |
147148
| `files` | file[] | Files attached to the message |
148149

150+
### `slack_ephemeral_message`
151+
152+
Send an ephemeral message visible only to a specific user in a channel. Optionally reply in a thread. The message does not persist across sessions.
153+
154+
#### Input
155+
156+
| Parameter | Type | Required | Description |
157+
| --------- | ---- | -------- | ----------- |
158+
| `authMethod` | string | No | Authentication method: oauth or bot_token |
159+
| `botToken` | string | No | Bot token for Custom Bot |
160+
| `channel` | string | Yes | Slack channel ID \(e.g., C1234567890\) |
161+
| `user` | string | Yes | User ID who will see the ephemeral message \(e.g., U1234567890\). Must be a member of the channel. |
162+
| `text` | string | Yes | Message text to send \(supports Slack mrkdwn formatting\) |
163+
| `threadTs` | string | No | Thread timestamp to reply in. When provided, the ephemeral message appears as a thread reply. |
164+
| `blocks` | json | No | Block Kit layout blocks as a JSON array. When provided, text becomes the fallback notification text. |
165+
166+
#### Output
167+
168+
| Parameter | Type | Description |
169+
| --------- | ---- | ----------- |
170+
| `messageTs` | string | Timestamp of the ephemeral message \(cannot be used with chat.update\) |
171+
| `channel` | string | Channel ID where the ephemeral message was sent |
172+
149173
### `slack_canvas`
150174

151175
Create and share Slack canvases in channels. Canvases are collaborative documents within Slack.
@@ -682,6 +706,7 @@ Update a message previously sent by the bot in Slack
682706
| `channel` | string | Yes | Channel ID where the message was posted \(e.g., C1234567890\) |
683707
| `timestamp` | string | Yes | Timestamp of the message to update \(e.g., 1405894322.002768\) |
684708
| `text` | string | Yes | New message text \(supports Slack mrkdwn formatting\) |
709+
| `blocks` | json | No | Block Kit layout blocks as a JSON array. When provided, text becomes the fallback notification text. |
685710

686711
#### Output
687712

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
'use client'
2+
3+
import { useCallback, useEffect, useState } from 'react'
4+
import { ArrowLeftRight } from 'lucide-react'
5+
import Image from 'next/image'
6+
import { useRouter, useSearchParams } from 'next/navigation'
7+
import { Button } from '@/components/emcn'
8+
import { signOut, useSession } from '@/lib/auth/auth-client'
9+
import { inter } from '@/app/_styles/fonts/inter/inter'
10+
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
11+
import { BrandedButton } from '@/app/(auth)/components/branded-button'
12+
13+
const SCOPE_DESCRIPTIONS: Record<string, string> = {
14+
openid: 'Verify your identity',
15+
profile: 'Access your basic profile information',
16+
email: 'View your email address',
17+
offline_access: 'Maintain access when you are not actively using the app',
18+
'mcp:tools': 'Use Sim workflows and tools on your behalf',
19+
} as const
20+
21+
interface ClientInfo {
22+
clientId: string
23+
name: string
24+
icon: string
25+
}
26+
27+
export default function OAuthConsentPage() {
28+
const router = useRouter()
29+
const searchParams = useSearchParams()
30+
const { data: session } = useSession()
31+
const consentCode = searchParams.get('consent_code')
32+
const clientId = searchParams.get('client_id')
33+
const scope = searchParams.get('scope')
34+
35+
const [clientInfo, setClientInfo] = useState<ClientInfo | null>(null)
36+
const [loading, setLoading] = useState(true)
37+
const [submitting, setSubmitting] = useState(false)
38+
const [error, setError] = useState<string | null>(null)
39+
40+
const scopes = scope?.split(' ').filter(Boolean) ?? []
41+
42+
useEffect(() => {
43+
if (!clientId) {
44+
setLoading(false)
45+
setError('The authorization request is missing a required client identifier.')
46+
return
47+
}
48+
49+
fetch(`/api/auth/oauth2/client/${clientId}`, { credentials: 'include' })
50+
.then(async (res) => {
51+
if (!res.ok) return
52+
const data = await res.json()
53+
setClientInfo(data)
54+
})
55+
.catch(() => {})
56+
.finally(() => {
57+
setLoading(false)
58+
})
59+
}, [clientId])
60+
61+
const handleConsent = useCallback(
62+
async (accept: boolean) => {
63+
if (!consentCode) {
64+
setError('The authorization request is missing a required consent code.')
65+
return
66+
}
67+
68+
setSubmitting(true)
69+
try {
70+
const res = await fetch('/api/auth/oauth2/consent', {
71+
method: 'POST',
72+
headers: { 'Content-Type': 'application/json' },
73+
credentials: 'include',
74+
body: JSON.stringify({ accept, consent_code: consentCode }),
75+
})
76+
77+
if (!res.ok) {
78+
const body = await res.json().catch(() => null)
79+
setError(
80+
(body as Record<string, string> | null)?.message ??
81+
'The consent request could not be processed. Please try again.'
82+
)
83+
setSubmitting(false)
84+
return
85+
}
86+
87+
const data = (await res.json()) as { redirectURI?: string }
88+
if (data.redirectURI) {
89+
window.location.href = data.redirectURI
90+
} else {
91+
setError('The server did not return a redirect. Please try again.')
92+
setSubmitting(false)
93+
}
94+
} catch {
95+
setError('Something went wrong. Please try again.')
96+
setSubmitting(false)
97+
}
98+
},
99+
[consentCode]
100+
)
101+
102+
const handleSwitchAccount = useCallback(async () => {
103+
if (!consentCode) return
104+
105+
const res = await fetch(`/api/auth/oauth2/authorize-params?consent_code=${consentCode}`, {
106+
credentials: 'include',
107+
})
108+
if (!res.ok) {
109+
setError('Unable to switch accounts. Please re-initiate the connection.')
110+
return
111+
}
112+
113+
const params = (await res.json()) as Record<string, string | null>
114+
const authorizeUrl = new URL('/api/auth/oauth2/authorize', window.location.origin)
115+
for (const [key, value] of Object.entries(params)) {
116+
if (value) authorizeUrl.searchParams.set(key, value)
117+
}
118+
119+
await signOut({
120+
fetchOptions: {
121+
onSuccess: () => {
122+
window.location.href = authorizeUrl.toString()
123+
},
124+
},
125+
})
126+
}, [consentCode])
127+
128+
if (loading) {
129+
return (
130+
<div className='flex flex-col items-center justify-center'>
131+
<div className='space-y-1 text-center'>
132+
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
133+
Authorize Application
134+
</h1>
135+
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
136+
Loading application details...
137+
</p>
138+
</div>
139+
</div>
140+
)
141+
}
142+
143+
if (error) {
144+
return (
145+
<div className='flex flex-col items-center justify-center'>
146+
<div className='space-y-1 text-center'>
147+
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
148+
Authorization Error
149+
</h1>
150+
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
151+
{error}
152+
</p>
153+
</div>
154+
<div className={`${inter.className} mt-8 w-full max-w-[410px] space-y-3`}>
155+
<BrandedButton onClick={() => router.push('/')}>Return to Home</BrandedButton>
156+
</div>
157+
</div>
158+
)
159+
}
160+
161+
const clientName = clientInfo?.name ?? clientId
162+
163+
return (
164+
<div className='flex flex-col items-center justify-center'>
165+
<div className='mb-6 flex items-center gap-4'>
166+
{clientInfo?.icon ? (
167+
<Image
168+
src={clientInfo.icon}
169+
alt={clientName ?? 'Application'}
170+
width={48}
171+
height={48}
172+
className='rounded-[10px]'
173+
unoptimized
174+
/>
175+
) : (
176+
<div className='flex h-12 w-12 items-center justify-center rounded-[10px] bg-muted font-medium text-[18px] text-muted-foreground'>
177+
{(clientName ?? '?').charAt(0).toUpperCase()}
178+
</div>
179+
)}
180+
<ArrowLeftRight className='h-5 w-5 text-muted-foreground' />
181+
<Image
182+
src='/new/logo/colorized-bg.svg'
183+
alt='Sim'
184+
width={48}
185+
height={48}
186+
className='rounded-[10px]'
187+
/>
188+
</div>
189+
190+
<div className='space-y-1 text-center'>
191+
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
192+
Authorize Application
193+
</h1>
194+
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
195+
<span className='font-medium text-foreground'>{clientName}</span> is requesting access to
196+
your account
197+
</p>
198+
</div>
199+
200+
{session?.user && (
201+
<div
202+
className={`${inter.className} mt-5 flex items-center gap-3 rounded-lg border px-4 py-3`}
203+
>
204+
{session.user.image ? (
205+
<Image
206+
src={session.user.image}
207+
alt={session.user.name ?? 'User'}
208+
width={32}
209+
height={32}
210+
className='rounded-full'
211+
unoptimized
212+
/>
213+
) : (
214+
<div className='flex h-8 w-8 items-center justify-center rounded-full bg-muted font-medium text-[13px] text-muted-foreground'>
215+
{(session.user.name ?? session.user.email ?? '?').charAt(0).toUpperCase()}
216+
</div>
217+
)}
218+
<div className='min-w-0'>
219+
{session.user.name && (
220+
<p className='truncate font-medium text-[14px]'>{session.user.name}</p>
221+
)}
222+
<p className='truncate text-[13px] text-muted-foreground'>{session.user.email}</p>
223+
</div>
224+
<button
225+
type='button'
226+
onClick={handleSwitchAccount}
227+
className='ml-auto text-[13px] text-muted-foreground underline-offset-2 transition-colors hover:text-foreground hover:underline'
228+
>
229+
Switch
230+
</button>
231+
</div>
232+
)}
233+
234+
{scopes.length > 0 && (
235+
<div className={`${inter.className} mt-5 w-full max-w-[410px]`}>
236+
<div className='rounded-lg border p-4'>
237+
<p className='mb-3 font-medium text-[14px]'>This will allow the application to:</p>
238+
<ul className='space-y-2'>
239+
{scopes.map((s) => (
240+
<li
241+
key={s}
242+
className='flex items-start gap-2 font-normal text-[13px] text-muted-foreground'
243+
>
244+
<span className='mt-0.5 text-green-500'>&#10003;</span>
245+
<span>{SCOPE_DESCRIPTIONS[s] ?? s}</span>
246+
</li>
247+
))}
248+
</ul>
249+
</div>
250+
</div>
251+
)}
252+
253+
<div className={`${inter.className} mt-6 flex w-full max-w-[410px] gap-3`}>
254+
<Button
255+
variant='outline'
256+
size='md'
257+
className='px-6 py-2'
258+
disabled={submitting}
259+
onClick={() => handleConsent(false)}
260+
>
261+
Deny
262+
</Button>
263+
<BrandedButton
264+
fullWidth
265+
showArrow={false}
266+
loading={submitting}
267+
loadingText='Authorizing'
268+
onClick={() => handleConsent(true)}
269+
>
270+
Allow
271+
</BrandedButton>
272+
</div>
273+
</div>
274+
)
275+
}

0 commit comments

Comments
 (0)