Skip to content

Commit 3eaaeae

Browse files
committed
feat(uptimerobot): add UptimeRobot v3 integration
- 24 tools across monitors, incidents, maintenance windows, alert contacts, public status pages, and account (UptimeRobot v3 REST API, Bearer auth) - Block with operation-scoped subBlocks, status-page logo/icon file uploads via internal multipart routes, and BlockMeta templates + skills - Registered tools/block, added icon, generated docs - Updated add-integration/add-block/validate-integration docs links to /integrations
1 parent c7eda5b commit 3eaaeae

44 files changed

Lines changed: 5411 additions & 7 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/commands/add-block.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const {ServiceName}Block: BlockConfig = {
2727
name: '{Service Name}', // Human readable
2828
description: 'Brief description', // One sentence
2929
longDescription: 'Detailed description for docs',
30-
docsLink: 'https://docs.sim.ai/tools/{service}',
30+
docsLink: 'https://docs.sim.ai/integrations/{service}',
3131
category: 'tools', // 'tools' | 'blocks' | 'triggers'
3232
integrationType: IntegrationType.X, // Primary category (see IntegrationType enum)
3333
tags: ['oauth', 'api'], // Cross-cutting tags (see IntegrationTag type)
@@ -626,7 +626,7 @@ export const ServiceBlock: BlockConfig = {
626626
name: 'Service',
627627
description: 'Integrate with Service API',
628628
longDescription: 'Full description for documentation...',
629-
docsLink: 'https://docs.sim.ai/tools/service',
629+
docsLink: 'https://docs.sim.ai/integrations/service',
630630
category: 'tools',
631631
integrationType: IntegrationType.DeveloperTools,
632632
tags: ['oauth', 'api'],

.claude/commands/add-integration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export const {Service}Block: BlockConfig = {
121121
name: '{Service}',
122122
description: '...',
123123
longDescription: '...',
124-
docsLink: 'https://docs.sim.ai/tools/{service}',
124+
docsLink: 'https://docs.sim.ai/integrations/{service}',
125125
category: 'tools',
126126
integrationType: IntegrationType.X, // Primary category (see IntegrationType enum)
127127
tags: ['oauth', 'api'], // Cross-cutting tags (see IntegrationTag type)

.claude/commands/validate-integration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ For **each tool** in `tools.access`:
185185
- [ ] `name` is human-readable (e.g., `'X'`, `'Cloudflare'`)
186186
- [ ] `description` is a concise one-liner
187187
- [ ] `longDescription` provides detail for docs
188-
- [ ] `docsLink` points to `'https://docs.sim.ai/tools/{service}'`
188+
- [ ] `docsLink` points to `'https://docs.sim.ai/integrations/{service}'`
189189
- [ ] `category` is `'tools'`
190190
- [ ] `bgColor` uses the service's brand color hex
191191
- [ ] `icon` references the correct icon component from `@/components/icons`

apps/docs/components/icons.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7706,6 +7706,17 @@ export function UpstashIcon(props: SVGProps<SVGSVGElement>) {
77067706
)
77077707
}
77087708

7709+
export function UptimeRobotIcon(props: SVGProps<SVGSVGElement>) {
7710+
return (
7711+
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 298 298' width='24' height='24'>
7712+
<g fill='#3BD771' transform='translate(.9 .9)'>
7713+
<circle cx='148.1' cy='148.1' r='148.1' opacity='.3' />
7714+
<circle cx='148.1' cy='148.1' r='98.9' />
7715+
</g>
7716+
</svg>
7717+
)
7718+
}
7719+
77097720
export function RevenueCatIcon(props: SVGProps<SVGSVGElement>) {
77107721
return (
77117722
<svg

apps/docs/components/ui/icon-mapping.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ import {
216216
TwilioIcon,
217217
TypeformIcon,
218218
UpstashIcon,
219+
UptimeRobotIcon,
219220
VantaIcon,
220221
VercelIcon,
221222
VideoIcon,
@@ -475,6 +476,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
475476
twilio_voice: TwilioIcon,
476477
typeform: TypeformIcon,
477478
upstash: UpstashIcon,
479+
uptimerobot: UptimeRobotIcon,
478480
vanta: VantaIcon,
479481
vercel: VercelIcon,
480482
video_generator: VideoIcon,

apps/docs/content/docs/en/integrations/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@
218218
"twilio_voice",
219219
"typeform",
220220
"upstash",
221+
"uptimerobot",
221222
"vanta",
222223
"vercel",
223224
"wealthbox",

apps/docs/content/docs/en/integrations/uptimerobot.mdx

Lines changed: 915 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { createLogger } from '@sim/logger'
2+
import { getErrorMessage } from '@sim/utils/errors'
3+
import { type NextRequest, NextResponse } from 'next/server'
4+
import { uptimeRobotCreatePspContract } from '@/lib/api/contracts/tools/uptimerobot'
5+
import { parseRequest } from '@/lib/api/server'
6+
import { checkInternalAuth } from '@/lib/auth/hybrid'
7+
import { generateRequestId } from '@/lib/core/utils/request'
8+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
9+
import { forwardPspRequest } from '@/app/api/tools/uptimerobot/server-utils'
10+
11+
export const dynamic = 'force-dynamic'
12+
13+
const logger = createLogger('UptimeRobotCreatePspAPI')
14+
15+
export const POST = withRouteHandler(async (request: NextRequest) => {
16+
const requestId = generateRequestId()
17+
18+
try {
19+
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
20+
if (!authResult.success || !authResult.userId) {
21+
logger.warn(`[${requestId}] Unauthorized UptimeRobot create-psp request: ${authResult.error}`)
22+
return NextResponse.json(
23+
{ success: false, error: authResult.error || 'Authentication required' },
24+
{ status: 401 }
25+
)
26+
}
27+
28+
const parsed = await parseRequest(uptimeRobotCreatePspContract, request, {})
29+
if (!parsed.success) return parsed.response
30+
const body = parsed.data.body
31+
32+
return forwardPspRequest({
33+
apiKey: body.apiKey,
34+
method: 'POST',
35+
path: '/psps',
36+
fields: body,
37+
userId: authResult.userId,
38+
requestId,
39+
logger,
40+
})
41+
} catch (error) {
42+
logger.error(`[${requestId}] Unexpected error creating status page:`, error)
43+
return NextResponse.json(
44+
{ success: false, error: getErrorMessage(error, 'Unknown error') },
45+
{ status: 500 }
46+
)
47+
}
48+
})
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import type { Logger } from '@sim/logger'
2+
import { NextResponse } from 'next/server'
3+
import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils'
4+
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
5+
import { assertToolFileAccess } from '@/app/api/files/authorization'
6+
import { mapPsp, UPTIMEROBOT_API_BASE } from '@/tools/uptimerobot/types'
7+
8+
/** Fields shared by the PSP create and update routes (before the files). */
9+
interface PspFormFields {
10+
friendlyName?: string | null
11+
monitorIds?: string | null
12+
status?: string | null
13+
password?: string | null
14+
customDomain?: string | null
15+
hideUrlLinks?: boolean | null
16+
noIndex?: boolean | null
17+
logo?: unknown
18+
icon?: unknown
19+
}
20+
21+
/**
22+
* Appends a single optional image file (logo or icon) to the form after
23+
* downloading it from storage and verifying the caller may access it.
24+
*
25+
* @returns an error `NextResponse` if access is denied, otherwise `null`.
26+
*/
27+
async function appendPspImage(
28+
form: FormData,
29+
field: 'logo' | 'icon',
30+
file: unknown,
31+
userId: string,
32+
requestId: string,
33+
logger: Logger
34+
): Promise<NextResponse | null> {
35+
const userFiles = processFilesToUserFiles([file as RawFileInput], requestId, logger)
36+
if (userFiles.length === 0) return null
37+
38+
const userFile = userFiles[0]
39+
const denied = await assertToolFileAccess(userFile.key, userId, requestId, logger)
40+
if (denied) return denied
41+
42+
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
43+
const mimeType = userFile.type || 'application/octet-stream'
44+
form.append(field, new Blob([new Uint8Array(buffer)], { type: mimeType }), userFile.name)
45+
return null
46+
}
47+
48+
/**
49+
* Builds the multipart form for a PSP request, downloads any referenced
50+
* logo/icon files, forwards the request to UptimeRobot, and returns a typed
51+
* `{ success, output: { psp } }` envelope as a `NextResponse`.
52+
*/
53+
export async function forwardPspRequest(options: {
54+
apiKey: string
55+
method: 'POST' | 'PATCH'
56+
path: string
57+
fields: PspFormFields
58+
userId: string
59+
requestId: string
60+
logger: Logger
61+
}): Promise<NextResponse> {
62+
const { apiKey, method, path, fields, userId, requestId, logger } = options
63+
64+
const form = new FormData()
65+
if (fields.friendlyName) form.append('friendlyName', fields.friendlyName)
66+
if (fields.status) form.append('status', fields.status)
67+
if (fields.password) form.append('password', fields.password)
68+
if (fields.customDomain) form.append('customDomain', fields.customDomain)
69+
if (typeof fields.hideUrlLinks === 'boolean') {
70+
form.append('hideUrlLinks', String(fields.hideUrlLinks))
71+
}
72+
if (typeof fields.noIndex === 'boolean') form.append('noIndex', String(fields.noIndex))
73+
if (fields.monitorIds) {
74+
for (const id of fields.monitorIds.split(',')) {
75+
const trimmed = id.trim()
76+
if (trimmed) form.append('monitorIds', trimmed)
77+
}
78+
}
79+
80+
if (fields.logo) {
81+
const denied = await appendPspImage(form, 'logo', fields.logo, userId, requestId, logger)
82+
if (denied) return denied
83+
}
84+
if (fields.icon) {
85+
const denied = await appendPspImage(form, 'icon', fields.icon, userId, requestId, logger)
86+
if (denied) return denied
87+
}
88+
89+
const response = await fetch(`${UPTIMEROBOT_API_BASE}${path}`, {
90+
method,
91+
headers: { Authorization: `Bearer ${apiKey}`, Accept: 'application/json' },
92+
body: form,
93+
})
94+
95+
const text = await response.text()
96+
if (!response.ok) {
97+
let message: string | undefined
98+
try {
99+
message = JSON.parse(text)?.message
100+
} catch {
101+
message = undefined
102+
}
103+
logger.error(`[${requestId}] UptimeRobot PSP request failed`, {
104+
status: response.status,
105+
body: text,
106+
})
107+
return NextResponse.json(
108+
{ success: false, error: message || `UptimeRobot API error (HTTP ${response.status})` },
109+
{ status: response.status }
110+
)
111+
}
112+
113+
const data = text ? JSON.parse(text) : {}
114+
return NextResponse.json({ success: true, output: { psp: mapPsp(data) } })
115+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { createLogger } from '@sim/logger'
2+
import { getErrorMessage } from '@sim/utils/errors'
3+
import { type NextRequest, NextResponse } from 'next/server'
4+
import { uptimeRobotUpdatePspContract } from '@/lib/api/contracts/tools/uptimerobot'
5+
import { parseRequest } from '@/lib/api/server'
6+
import { checkInternalAuth } from '@/lib/auth/hybrid'
7+
import { generateRequestId } from '@/lib/core/utils/request'
8+
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
9+
import { forwardPspRequest } from '@/app/api/tools/uptimerobot/server-utils'
10+
11+
export const dynamic = 'force-dynamic'
12+
13+
const logger = createLogger('UptimeRobotUpdatePspAPI')
14+
15+
export const POST = withRouteHandler(async (request: NextRequest) => {
16+
const requestId = generateRequestId()
17+
18+
try {
19+
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
20+
if (!authResult.success || !authResult.userId) {
21+
logger.warn(`[${requestId}] Unauthorized UptimeRobot update-psp request: ${authResult.error}`)
22+
return NextResponse.json(
23+
{ success: false, error: authResult.error || 'Authentication required' },
24+
{ status: 401 }
25+
)
26+
}
27+
28+
const parsed = await parseRequest(uptimeRobotUpdatePspContract, request, {})
29+
if (!parsed.success) return parsed.response
30+
const body = parsed.data.body
31+
32+
return forwardPspRequest({
33+
apiKey: body.apiKey,
34+
method: 'PATCH',
35+
path: `/psps/${body.pspId}`,
36+
fields: body,
37+
userId: authResult.userId,
38+
requestId,
39+
logger,
40+
})
41+
} catch (error) {
42+
logger.error(`[${requestId}] Unexpected error updating status page:`, error)
43+
return NextResponse.json(
44+
{ success: false, error: getErrorMessage(error, 'Unknown error') },
45+
{ status: 500 }
46+
)
47+
}
48+
})

0 commit comments

Comments
 (0)