mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-08 14:43:54 -05:00
feat(service-now): added service now block (#2404)
* feat(service-now): added service now block * fix: bun lock * improvement: fixed @trigger.dev/sdk imports and removal of sentry blocks * improvement: fixed @trigger.dev/sdk import * improvement: fixed @trigger.dev/sdk import * fix(servicenow): save accessTokenExpiresAt on initial OAuth account creation * docs(servicenow): add ServiceNow tool documentation and icon mapping * fixing bun lint issues * fixing username/password fields * fixing test file for refreshaccesstoken to support instance uri * removing basic auth and fixing undo-redo/store.ts * removed import set api code, changed CRUD operations to CRUD_record and added wand configuration to help users to generate JSON Arrays --------- Co-authored-by: priyanshu.solanki <priyanshu.solanki@saviynt.com>
This commit is contained in:
@@ -3335,6 +3335,24 @@ export function SalesforceIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function ServiceNowIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 1570 1403'
|
||||
width='48'
|
||||
height='48'
|
||||
>
|
||||
<path
|
||||
fill='#62d84e'
|
||||
fillRule='evenodd'
|
||||
d='M1228.4 138.9c129.2 88.9 228.9 214.3 286.3 360.2 57.5 145.8 70 305.5 36 458.5S1437.8 1250 1324 1357.9c-13.3 12.9-28.8 23.4-45.8 30.8-17 7.5-35.2 11.9-53.7 12.9-18.5 1.1-37.1-1.1-54.8-6.6-17.7-5.4-34.3-13.9-49.1-25.2-48.2-35.9-101.8-63.8-158.8-82.6-57.1-18.9-116.7-28.5-176.8-28.5s-119.8 9.6-176.8 28.5c-57 18.8-110.7 46.7-158.9 82.6-14.6 11.2-31 19.8-48.6 25.3s-36 7.8-54.4 6.8c-18.4-.9-36.5-5.1-53.4-12.4s-32.4-17.5-45.8-30.2C132.5 1251 53 1110.8 19 956.8s-20.9-314.6 37.6-461c58.5-146.5 159.6-272 290.3-360.3S631.8.1 789.6.5c156.8 1.3 309.6 49.6 438.8 138.4m-291.8 1014c48.2-19.2 92-48 128.7-84.6 36.7-36.7 65.5-80.4 84.7-128.6 19.2-48.1 28.4-99.7 27-151.5 0-103.9-41.3-203.5-114.8-277S889 396.4 785 396.4s-203.7 41.3-277.2 114.8S393 684.3 393 788.2c-1.4 51.8 7.8 103.4 27 151.5 19.2 48.2 48 91.9 84.7 128.6 36.7 36.6 80.5 65.4 128.6 84.6 48.2 19.2 99.8 28.4 151.7 27 51.8 1.4 103.4-7.8 151.6-27'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function ApolloIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -85,6 +85,7 @@ import {
|
||||
SendgridIcon,
|
||||
SentryIcon,
|
||||
SerperIcon,
|
||||
ServiceNowIcon,
|
||||
SftpIcon,
|
||||
ShopifyIcon,
|
||||
SlackIcon,
|
||||
@@ -139,6 +140,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
webflow: WebflowIcon,
|
||||
pinecone: PineconeIcon,
|
||||
apollo: ApolloIcon,
|
||||
servicenow: ServiceNowIcon,
|
||||
whatsapp: WhatsAppIcon,
|
||||
typeform: TypeformIcon,
|
||||
qdrant: QdrantIcon,
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
"sendgrid",
|
||||
"sentry",
|
||||
"serper",
|
||||
"servicenow",
|
||||
"sftp",
|
||||
"sharepoint",
|
||||
"shopify",
|
||||
|
||||
111
apps/docs/content/docs/en/tools/servicenow.mdx
Normal file
111
apps/docs/content/docs/en/tools/servicenow.mdx
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
title: ServiceNow
|
||||
description: Create, read, update, delete, and bulk import ServiceNow records
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="servicenow"
|
||||
color="#032D42"
|
||||
/>
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate ServiceNow into your workflow. Can create, read, update, and delete records in any ServiceNow table (incidents, tasks, users, etc.). Supports bulk import operations for data migration and ETL.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `servicenow_create_record`
|
||||
|
||||
Create a new record in a ServiceNow table
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instanceUrl` | string | Yes | ServiceNow instance URL \(e.g., https://instance.service-now.com\) |
|
||||
| `credential` | string | No | ServiceNow OAuth credential ID |
|
||||
| `tableName` | string | Yes | Table name \(e.g., incident, task, sys_user\) |
|
||||
| `fields` | json | Yes | Fields to set on the record \(JSON object\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `record` | json | Created ServiceNow record with sys_id and other fields |
|
||||
| `metadata` | json | Operation metadata |
|
||||
|
||||
### `servicenow_read_record`
|
||||
|
||||
Read records from a ServiceNow table
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instanceUrl` | string | No | ServiceNow instance URL \(auto-detected from OAuth if not provided\) |
|
||||
| `credential` | string | No | ServiceNow OAuth credential ID |
|
||||
| `tableName` | string | Yes | Table name |
|
||||
| `sysId` | string | No | Specific record sys_id |
|
||||
| `number` | string | No | Record number \(e.g., INC0010001\) |
|
||||
| `query` | string | No | Encoded query string \(e.g., "active=true^priority=1"\) |
|
||||
| `limit` | number | No | Maximum number of records to return |
|
||||
| `fields` | string | No | Comma-separated list of fields to return |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `records` | array | Array of ServiceNow records |
|
||||
| `metadata` | json | Operation metadata |
|
||||
|
||||
### `servicenow_update_record`
|
||||
|
||||
Update an existing record in a ServiceNow table
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instanceUrl` | string | No | ServiceNow instance URL \(auto-detected from OAuth if not provided\) |
|
||||
| `credential` | string | No | ServiceNow OAuth credential ID |
|
||||
| `tableName` | string | Yes | Table name |
|
||||
| `sysId` | string | Yes | Record sys_id to update |
|
||||
| `fields` | json | Yes | Fields to update \(JSON object\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `record` | json | Updated ServiceNow record |
|
||||
| `metadata` | json | Operation metadata |
|
||||
|
||||
### `servicenow_delete_record`
|
||||
|
||||
Delete a record from a ServiceNow table
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instanceUrl` | string | No | ServiceNow instance URL \(auto-detected from OAuth if not provided\) |
|
||||
| `credential` | string | No | ServiceNow OAuth credential ID |
|
||||
| `tableName` | string | Yes | Table name |
|
||||
| `sysId` | string | Yes | Record sys_id to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the deletion was successful |
|
||||
| `metadata` | json | Operation metadata |
|
||||
|
||||
|
||||
|
||||
## Notes
|
||||
|
||||
- Category: `tools`
|
||||
- Type: `servicenow`
|
||||
@@ -159,7 +159,7 @@ describe('OAuth Utils', () => {
|
||||
|
||||
const result = await refreshTokenIfNeeded('request-id', mockCredential, 'credential-id')
|
||||
|
||||
expect(mockRefreshOAuthToken).toHaveBeenCalledWith('google', 'refresh-token')
|
||||
expect(mockRefreshOAuthToken).toHaveBeenCalledWith('google', 'refresh-token', undefined)
|
||||
expect(mockDb.update).toHaveBeenCalled()
|
||||
expect(mockDb.set).toHaveBeenCalled()
|
||||
expect(result).toEqual({ accessToken: 'new-token', refreshed: true })
|
||||
@@ -239,7 +239,7 @@ describe('OAuth Utils', () => {
|
||||
|
||||
const token = await refreshAccessTokenIfNeeded('credential-id', 'test-user-id', 'request-id')
|
||||
|
||||
expect(mockRefreshOAuthToken).toHaveBeenCalledWith('google', 'refresh-token')
|
||||
expect(mockRefreshOAuthToken).toHaveBeenCalledWith('google', 'refresh-token', undefined)
|
||||
expect(mockDb.update).toHaveBeenCalled()
|
||||
expect(mockDb.set).toHaveBeenCalled()
|
||||
expect(token).toBe('new-token')
|
||||
|
||||
@@ -18,6 +18,7 @@ interface AccountInsertData {
|
||||
updatedAt: Date
|
||||
refreshToken?: string
|
||||
idToken?: string
|
||||
accessTokenExpiresAt?: Date
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,6 +104,7 @@ export async function getOAuthToken(userId: string, providerId: string): Promise
|
||||
accessToken: account.accessToken,
|
||||
refreshToken: account.refreshToken,
|
||||
accessTokenExpiresAt: account.accessTokenExpiresAt,
|
||||
idToken: account.idToken,
|
||||
})
|
||||
.from(account)
|
||||
.where(and(eq(account.userId, userId), eq(account.providerId, providerId)))
|
||||
@@ -130,7 +132,14 @@ export async function getOAuthToken(userId: string, providerId: string): Promise
|
||||
|
||||
try {
|
||||
// Use the existing refreshOAuthToken function
|
||||
const refreshResult = await refreshOAuthToken(providerId, credential.refreshToken!)
|
||||
// For ServiceNow, pass the instance URL (stored in idToken) for the token endpoint
|
||||
const instanceUrl =
|
||||
providerId === 'servicenow' ? (credential.idToken ?? undefined) : undefined
|
||||
const refreshResult = await refreshOAuthToken(
|
||||
providerId,
|
||||
credential.refreshToken!,
|
||||
instanceUrl
|
||||
)
|
||||
|
||||
if (!refreshResult) {
|
||||
logger.error(`Failed to refresh token for user ${userId}, provider ${providerId}`, {
|
||||
@@ -213,9 +222,13 @@ export async function refreshAccessTokenIfNeeded(
|
||||
if (shouldRefresh) {
|
||||
logger.info(`[${requestId}] Token expired, attempting to refresh for credential`)
|
||||
try {
|
||||
// For ServiceNow, pass the instance URL (stored in idToken) for the token endpoint
|
||||
const instanceUrl =
|
||||
credential.providerId === 'servicenow' ? (credential.idToken ?? undefined) : undefined
|
||||
const refreshedToken = await refreshOAuthToken(
|
||||
credential.providerId,
|
||||
credential.refreshToken!
|
||||
credential.refreshToken!,
|
||||
instanceUrl
|
||||
)
|
||||
|
||||
if (!refreshedToken) {
|
||||
@@ -287,7 +300,14 @@ export async function refreshTokenIfNeeded(
|
||||
}
|
||||
|
||||
try {
|
||||
const refreshResult = await refreshOAuthToken(credential.providerId, credential.refreshToken!)
|
||||
// For ServiceNow, pass the instance URL (stored in idToken) for the token endpoint
|
||||
const instanceUrl =
|
||||
credential.providerId === 'servicenow' ? (credential.idToken ?? undefined) : undefined
|
||||
const refreshResult = await refreshOAuthToken(
|
||||
credential.providerId,
|
||||
credential.refreshToken!,
|
||||
instanceUrl
|
||||
)
|
||||
|
||||
if (!refreshResult) {
|
||||
logger.error(`[${requestId}] Failed to refresh token for credential`)
|
||||
|
||||
166
apps/sim/app/api/auth/oauth2/callback/servicenow/route.ts
Normal file
166
apps/sim/app/api/auth/oauth2/callback/servicenow/route.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
const logger = createLogger('ServiceNowCallback')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const baseUrl = getBaseUrl()
|
||||
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=unauthorized`)
|
||||
}
|
||||
|
||||
const { searchParams } = request.nextUrl
|
||||
const code = searchParams.get('code')
|
||||
const state = searchParams.get('state')
|
||||
const error = searchParams.get('error')
|
||||
const errorDescription = searchParams.get('error_description')
|
||||
|
||||
// Handle OAuth errors from ServiceNow
|
||||
if (error) {
|
||||
logger.error('ServiceNow OAuth error:', { error, errorDescription })
|
||||
return NextResponse.redirect(
|
||||
`${baseUrl}/workspace?error=servicenow_auth_error&message=${encodeURIComponent(errorDescription || error)}`
|
||||
)
|
||||
}
|
||||
|
||||
const storedState = request.cookies.get('servicenow_oauth_state')?.value
|
||||
const storedInstanceUrl = request.cookies.get('servicenow_instance_url')?.value
|
||||
|
||||
const clientId = env.SERVICENOW_CLIENT_ID
|
||||
const clientSecret = env.SERVICENOW_CLIENT_SECRET
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
logger.error('ServiceNow credentials not configured')
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=servicenow_config_error`)
|
||||
}
|
||||
|
||||
// Validate state parameter
|
||||
if (!state || state !== storedState) {
|
||||
logger.error('State mismatch in ServiceNow OAuth callback')
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=servicenow_state_mismatch`)
|
||||
}
|
||||
|
||||
// Validate authorization code
|
||||
if (!code) {
|
||||
logger.error('No code received from ServiceNow')
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=servicenow_no_code`)
|
||||
}
|
||||
|
||||
// Validate instance URL
|
||||
if (!storedInstanceUrl) {
|
||||
logger.error('No instance URL stored')
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=servicenow_no_instance`)
|
||||
}
|
||||
|
||||
const redirectUri = `${baseUrl}/api/auth/oauth2/callback/servicenow`
|
||||
|
||||
// Exchange authorization code for access token
|
||||
const tokenResponse = await fetch(`${storedInstanceUrl}/oauth_token.do`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: code,
|
||||
redirect_uri: redirectUri,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
}).toString(),
|
||||
})
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
const errorText = await tokenResponse.text()
|
||||
logger.error('Failed to exchange code for token:', {
|
||||
status: tokenResponse.status,
|
||||
body: errorText,
|
||||
})
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=servicenow_token_error`)
|
||||
}
|
||||
|
||||
const tokenData = await tokenResponse.json()
|
||||
const accessToken = tokenData.access_token
|
||||
const refreshToken = tokenData.refresh_token
|
||||
const expiresIn = tokenData.expires_in
|
||||
// ServiceNow always grants 'useraccount' scope but returns empty string
|
||||
const scope = tokenData.scope || 'useraccount'
|
||||
|
||||
logger.info('ServiceNow token exchange successful:', {
|
||||
hasAccessToken: !!accessToken,
|
||||
hasRefreshToken: !!refreshToken,
|
||||
expiresIn,
|
||||
})
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('No access token in response')
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=servicenow_no_token`)
|
||||
}
|
||||
|
||||
// Redirect to store endpoint with token data in cookies
|
||||
const storeUrl = new URL(`${baseUrl}/api/auth/oauth2/servicenow/store`)
|
||||
|
||||
const response = NextResponse.redirect(storeUrl)
|
||||
|
||||
// Store token data in secure cookies for the store endpoint
|
||||
response.cookies.set('servicenow_pending_token', accessToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 60, // 1 minute
|
||||
path: '/',
|
||||
})
|
||||
|
||||
if (refreshToken) {
|
||||
response.cookies.set('servicenow_pending_refresh_token', refreshToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 60,
|
||||
path: '/',
|
||||
})
|
||||
}
|
||||
|
||||
response.cookies.set('servicenow_pending_instance', storedInstanceUrl, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 60,
|
||||
path: '/',
|
||||
})
|
||||
|
||||
response.cookies.set('servicenow_pending_scope', scope || '', {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 60,
|
||||
path: '/',
|
||||
})
|
||||
|
||||
if (expiresIn) {
|
||||
response.cookies.set('servicenow_pending_expires_in', expiresIn.toString(), {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 60,
|
||||
path: '/',
|
||||
})
|
||||
}
|
||||
|
||||
// Clean up OAuth state cookies
|
||||
response.cookies.delete('servicenow_oauth_state')
|
||||
response.cookies.delete('servicenow_instance_url')
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
logger.error('Error in ServiceNow OAuth callback:', error)
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=servicenow_callback_error`)
|
||||
}
|
||||
}
|
||||
142
apps/sim/app/api/auth/oauth2/servicenow/store/route.ts
Normal file
142
apps/sim/app/api/auth/oauth2/servicenow/store/route.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { db } from '@sim/db'
|
||||
import { account } from '@sim/db/schema'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { safeAccountInsert } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
const logger = createLogger('ServiceNowStore')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const baseUrl = getBaseUrl()
|
||||
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
logger.warn('Unauthorized attempt to store ServiceNow token')
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=unauthorized`)
|
||||
}
|
||||
|
||||
// Retrieve token data from cookies
|
||||
const accessToken = request.cookies.get('servicenow_pending_token')?.value
|
||||
const refreshToken = request.cookies.get('servicenow_pending_refresh_token')?.value
|
||||
const instanceUrl = request.cookies.get('servicenow_pending_instance')?.value
|
||||
const scope = request.cookies.get('servicenow_pending_scope')?.value
|
||||
const expiresInStr = request.cookies.get('servicenow_pending_expires_in')?.value
|
||||
|
||||
if (!accessToken || !instanceUrl) {
|
||||
logger.error('Missing token or instance URL in cookies')
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=servicenow_missing_data`)
|
||||
}
|
||||
|
||||
// Validate the token by fetching user info from ServiceNow
|
||||
const userResponse = await fetch(
|
||||
`${instanceUrl}/api/now/table/sys_user?sysparm_query=user_name=${encodeURIComponent('javascript:gs.getUserName()')}&sysparm_limit=1`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Alternative: Use the instance info endpoint instead
|
||||
let accountIdentifier = instanceUrl
|
||||
let userInfo: Record<string, unknown> | null = null
|
||||
|
||||
// Try to get current user info
|
||||
try {
|
||||
const whoamiResponse = await fetch(`${instanceUrl}/api/now/ui/user/current_user`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (whoamiResponse.ok) {
|
||||
const whoamiData = await whoamiResponse.json()
|
||||
userInfo = whoamiData.result
|
||||
if (userInfo?.user_sys_id) {
|
||||
accountIdentifier = userInfo.user_sys_id as string
|
||||
} else if (userInfo?.user_name) {
|
||||
accountIdentifier = userInfo.user_name as string
|
||||
}
|
||||
logger.info('Retrieved ServiceNow user info', { accountIdentifier })
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('Could not retrieve ServiceNow user info, using instance URL as identifier')
|
||||
}
|
||||
|
||||
// Calculate expiration time
|
||||
const now = new Date()
|
||||
const expiresIn = expiresInStr ? Number.parseInt(expiresInStr, 10) : 3600 // Default to 1 hour
|
||||
const accessTokenExpiresAt = new Date(now.getTime() + expiresIn * 1000)
|
||||
|
||||
// Check for existing ServiceNow account for this user
|
||||
const existing = await db.query.account.findFirst({
|
||||
where: and(eq(account.userId, session.user.id), eq(account.providerId, 'servicenow')),
|
||||
})
|
||||
|
||||
// ServiceNow always grants 'useraccount' scope but returns empty string
|
||||
const effectiveScope = scope?.trim() ? scope : 'useraccount'
|
||||
|
||||
const accountData = {
|
||||
accessToken: accessToken,
|
||||
refreshToken: refreshToken || null,
|
||||
accountId: accountIdentifier,
|
||||
scope: effectiveScope,
|
||||
updatedAt: now,
|
||||
accessTokenExpiresAt: accessTokenExpiresAt,
|
||||
idToken: instanceUrl, // Store instance URL in idToken for API calls
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
await db.update(account).set(accountData).where(eq(account.id, existing.id))
|
||||
logger.info('Updated existing ServiceNow account', { accountId: existing.id })
|
||||
} else {
|
||||
await safeAccountInsert(
|
||||
{
|
||||
id: `servicenow_${session.user.id}_${Date.now()}`,
|
||||
userId: session.user.id,
|
||||
providerId: 'servicenow',
|
||||
accountId: accountData.accountId,
|
||||
accessToken: accountData.accessToken,
|
||||
refreshToken: accountData.refreshToken || undefined,
|
||||
accessTokenExpiresAt: accountData.accessTokenExpiresAt,
|
||||
scope: accountData.scope,
|
||||
idToken: accountData.idToken,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
{ provider: 'ServiceNow', identifier: instanceUrl }
|
||||
)
|
||||
logger.info('Created new ServiceNow account')
|
||||
}
|
||||
|
||||
// Get return URL from cookie
|
||||
const returnUrl = request.cookies.get('servicenow_return_url')?.value
|
||||
|
||||
const redirectUrl = returnUrl || `${baseUrl}/workspace`
|
||||
const finalUrl = new URL(redirectUrl)
|
||||
finalUrl.searchParams.set('servicenow_connected', 'true')
|
||||
|
||||
const response = NextResponse.redirect(finalUrl.toString())
|
||||
|
||||
// Clean up all ServiceNow cookies
|
||||
response.cookies.delete('servicenow_pending_token')
|
||||
response.cookies.delete('servicenow_pending_refresh_token')
|
||||
response.cookies.delete('servicenow_pending_instance')
|
||||
response.cookies.delete('servicenow_pending_scope')
|
||||
response.cookies.delete('servicenow_pending_expires_in')
|
||||
response.cookies.delete('servicenow_return_url')
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
logger.error('Error storing ServiceNow token:', error)
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=servicenow_store_error`)
|
||||
}
|
||||
}
|
||||
264
apps/sim/app/api/auth/servicenow/authorize/route.ts
Normal file
264
apps/sim/app/api/auth/servicenow/authorize/route.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
const logger = createLogger('ServiceNowAuthorize')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* ServiceNow OAuth scopes
|
||||
* useraccount - Default scope for user account access
|
||||
* Note: ServiceNow always returns 'useraccount' in OAuth responses regardless of requested scopes.
|
||||
* Table API permissions are configured at the OAuth application level in ServiceNow.
|
||||
*/
|
||||
const SERVICENOW_SCOPES = 'useraccount'
|
||||
|
||||
/**
|
||||
* Validates a ServiceNow instance URL format
|
||||
*/
|
||||
function isValidInstanceUrl(url: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(url)
|
||||
return (
|
||||
parsed.protocol === 'https:' &&
|
||||
(parsed.hostname.endsWith('.service-now.com') || parsed.hostname.endsWith('.servicenow.com'))
|
||||
)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const clientId = env.SERVICENOW_CLIENT_ID
|
||||
|
||||
if (!clientId) {
|
||||
logger.error('SERVICENOW_CLIENT_ID not configured')
|
||||
return NextResponse.json({ error: 'ServiceNow client ID not configured' }, { status: 500 })
|
||||
}
|
||||
|
||||
const instanceUrl = request.nextUrl.searchParams.get('instanceUrl')
|
||||
const returnUrl = request.nextUrl.searchParams.get('returnUrl')
|
||||
|
||||
if (!instanceUrl) {
|
||||
const returnUrlParam = returnUrl ? encodeURIComponent(returnUrl) : ''
|
||||
return new NextResponse(
|
||||
`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Connect ServiceNow Instance</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
background: linear-gradient(135deg, #81B5A1 0%, #5A8A75 100%);
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
||||
text-align: center;
|
||||
max-width: 450px;
|
||||
width: 90%;
|
||||
}
|
||||
h2 {
|
||||
color: #111827;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
p {
|
||||
color: #6b7280;
|
||||
margin: 0 0 1.5rem 0;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: #81B5A1;
|
||||
box-shadow: 0 0 0 3px rgba(129, 181, 161, 0.2);
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background: #81B5A1;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
button:hover {
|
||||
background: #6A9A87;
|
||||
}
|
||||
.help {
|
||||
font-size: 0.875rem;
|
||||
color: #9ca3af;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.error {
|
||||
color: #dc2626;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 1rem;
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>Connect Your ServiceNow Instance</h2>
|
||||
<p>Enter your ServiceNow instance URL to continue</p>
|
||||
<div id="error" class="error"></div>
|
||||
<form onsubmit="handleSubmit(event)">
|
||||
<input
|
||||
type="text"
|
||||
id="instanceUrl"
|
||||
placeholder="https://mycompany.service-now.com"
|
||||
required
|
||||
/>
|
||||
<button type="submit">Connect Instance</button>
|
||||
</form>
|
||||
<p class="help">Your instance URL looks like: https://yourcompany.service-now.com</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const returnUrl = '${returnUrlParam}';
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
const errorEl = document.getElementById('error');
|
||||
let instanceUrl = document.getElementById('instanceUrl').value.trim();
|
||||
|
||||
// Ensure https:// prefix
|
||||
if (!instanceUrl.startsWith('https://') && !instanceUrl.startsWith('http://')) {
|
||||
instanceUrl = 'https://' + instanceUrl;
|
||||
}
|
||||
|
||||
// Validate the URL format
|
||||
try {
|
||||
const parsed = new URL(instanceUrl);
|
||||
if (!parsed.hostname.endsWith('.service-now.com') && !parsed.hostname.endsWith('.servicenow.com')) {
|
||||
errorEl.textContent = 'Please enter a valid ServiceNow instance URL (e.g., https://yourcompany.service-now.com)';
|
||||
errorEl.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
// Clean the URL (remove trailing slashes, paths)
|
||||
instanceUrl = parsed.origin;
|
||||
} catch {
|
||||
errorEl.textContent = 'Please enter a valid URL';
|
||||
errorEl.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
let url = window.location.pathname + '?instanceUrl=' + encodeURIComponent(instanceUrl);
|
||||
if (returnUrl) {
|
||||
url += '&returnUrl=' + returnUrl;
|
||||
}
|
||||
window.location.href = url;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'text/html; charset=utf-8',
|
||||
'Cache-Control': 'no-store, no-cache, must-revalidate',
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Validate instance URL
|
||||
if (!isValidInstanceUrl(instanceUrl)) {
|
||||
logger.error('Invalid ServiceNow instance URL:', { instanceUrl })
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
'Invalid ServiceNow instance URL. Must be a valid .service-now.com or .servicenow.com domain.',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Clean the instance URL
|
||||
const parsedUrl = new URL(instanceUrl)
|
||||
const cleanInstanceUrl = parsedUrl.origin
|
||||
|
||||
const baseUrl = getBaseUrl()
|
||||
const redirectUri = `${baseUrl}/api/auth/oauth2/callback/servicenow`
|
||||
|
||||
const state = crypto.randomUUID()
|
||||
|
||||
// ServiceNow OAuth authorization URL
|
||||
const oauthUrl =
|
||||
`${cleanInstanceUrl}/oauth_auth.do?` +
|
||||
new URLSearchParams({
|
||||
response_type: 'code',
|
||||
client_id: clientId,
|
||||
redirect_uri: redirectUri,
|
||||
state: state,
|
||||
scope: SERVICENOW_SCOPES,
|
||||
}).toString()
|
||||
|
||||
logger.info('Initiating ServiceNow OAuth:', {
|
||||
instanceUrl: cleanInstanceUrl,
|
||||
requestedScopes: SERVICENOW_SCOPES,
|
||||
redirectUri,
|
||||
returnUrl: returnUrl || 'not specified',
|
||||
})
|
||||
|
||||
const response = NextResponse.redirect(oauthUrl)
|
||||
|
||||
// Store state and instance URL in cookies for validation in callback
|
||||
response.cookies.set('servicenow_oauth_state', state, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 60 * 10, // 10 minutes
|
||||
path: '/',
|
||||
})
|
||||
|
||||
response.cookies.set('servicenow_instance_url', cleanInstanceUrl, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 60 * 10,
|
||||
path: '/',
|
||||
})
|
||||
|
||||
if (returnUrl) {
|
||||
response.cookies.set('servicenow_return_url', returnUrl, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 60 * 10,
|
||||
path: '/',
|
||||
})
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
logger.error('Error initiating ServiceNow authorization:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -347,6 +347,13 @@ export function OAuthRequiredModal({
|
||||
return
|
||||
}
|
||||
|
||||
if (providerId === 'servicenow') {
|
||||
// Pass the current URL so we can redirect back after OAuth
|
||||
const returnUrl = encodeURIComponent(window.location.href)
|
||||
window.location.href = `/api/auth/servicenow/authorize?returnUrl=${returnUrl}`
|
||||
return
|
||||
}
|
||||
|
||||
await client.oauth2.link({
|
||||
providerId,
|
||||
callbackURL: window.location.href,
|
||||
|
||||
257
apps/sim/blocks/blocks/servicenow.ts
Normal file
257
apps/sim/blocks/blocks/servicenow.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
import { ServiceNowIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { ServiceNowResponse } from '@/tools/servicenow/types'
|
||||
|
||||
export const ServiceNowBlock: BlockConfig<ServiceNowResponse> = {
|
||||
type: 'servicenow',
|
||||
name: 'ServiceNow',
|
||||
description: 'Create, read, update, delete, and bulk import ServiceNow records',
|
||||
authMode: AuthMode.OAuth,
|
||||
longDescription:
|
||||
'Integrate ServiceNow into your workflow. Can create, read, update, and delete records in any ServiceNow table (incidents, tasks, users, etc.). Supports bulk import operations for data migration and ETL.',
|
||||
docsLink: 'https://docs.sim.ai/tools/servicenow',
|
||||
category: 'tools',
|
||||
bgColor: '#032D42',
|
||||
icon: ServiceNowIcon,
|
||||
subBlocks: [
|
||||
// Operation selector
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Create Record', id: 'create' },
|
||||
{ label: 'Read Records', id: 'read' },
|
||||
{ label: 'Update Record', id: 'update' },
|
||||
{ label: 'Delete Record', id: 'delete' },
|
||||
],
|
||||
value: () => 'read',
|
||||
},
|
||||
// Instance URL
|
||||
{
|
||||
id: 'instanceUrl',
|
||||
title: 'Instance URL',
|
||||
type: 'short-input',
|
||||
placeholder: 'https://instance.service-now.com',
|
||||
required: true,
|
||||
description: 'Your ServiceNow instance URL',
|
||||
},
|
||||
// OAuth Credential
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'ServiceNow Account',
|
||||
type: 'oauth-input',
|
||||
serviceId: 'servicenow',
|
||||
requiredScopes: ['useraccount'],
|
||||
placeholder: 'Select ServiceNow account',
|
||||
required: true,
|
||||
},
|
||||
// Table Name
|
||||
{
|
||||
id: 'tableName',
|
||||
title: 'Table Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'incident, task, sys_user, etc.',
|
||||
required: true,
|
||||
description: 'ServiceNow table name',
|
||||
},
|
||||
// Create-specific: Fields
|
||||
{
|
||||
id: 'fields',
|
||||
title: 'Fields (JSON)',
|
||||
type: 'code',
|
||||
language: 'json',
|
||||
placeholder: '{\n "short_description": "Issue description",\n "priority": "1"\n}',
|
||||
condition: { field: 'operation', value: 'create' },
|
||||
required: true,
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
maintainHistory: true,
|
||||
prompt: `You are an expert ServiceNow developer. Generate ServiceNow record field objects as JSON based on the user's request.
|
||||
|
||||
### CONTEXT
|
||||
ServiceNow records use specific field names depending on the table. Common tables and their key fields include:
|
||||
- incident: short_description, description, priority (1-5), urgency (1-3), impact (1-3), caller_id, assignment_group, assigned_to, category, subcategory, state
|
||||
- task: short_description, description, priority, assignment_group, assigned_to, state
|
||||
- sys_user: user_name, first_name, last_name, email, active, department, title
|
||||
- change_request: short_description, description, type, risk, impact, priority, assignment_group
|
||||
|
||||
### RULES
|
||||
- Output ONLY valid JSON object starting with { and ending with }
|
||||
- Use correct ServiceNow field names for the target table
|
||||
- Values should be strings unless the field specifically requires another type
|
||||
- For reference fields (like caller_id, assigned_to), use sys_id values or display values
|
||||
- Do not include sys_id in create operations (it's auto-generated)
|
||||
|
||||
### EXAMPLE
|
||||
User: "Create a high priority incident for network outage"
|
||||
Output: {"short_description": "Network outage", "description": "Network connectivity issue affecting users", "priority": "1", "urgency": "1", "impact": "1", "category": "Network"}`,
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
// Read-specific: Query options
|
||||
{
|
||||
id: 'sysId',
|
||||
title: 'Record sys_id',
|
||||
type: 'short-input',
|
||||
placeholder: 'Specific record sys_id (optional)',
|
||||
condition: { field: 'operation', value: 'read' },
|
||||
},
|
||||
{
|
||||
id: 'number',
|
||||
title: 'Record Number',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g., INC0010001 (optional)',
|
||||
condition: { field: 'operation', value: 'read' },
|
||||
},
|
||||
{
|
||||
id: 'query',
|
||||
title: 'Query String',
|
||||
type: 'short-input',
|
||||
placeholder: 'active=true^priority=1',
|
||||
condition: { field: 'operation', value: 'read' },
|
||||
description: 'ServiceNow encoded query string',
|
||||
},
|
||||
{
|
||||
id: 'limit',
|
||||
title: 'Limit',
|
||||
type: 'short-input',
|
||||
placeholder: '10',
|
||||
condition: { field: 'operation', value: 'read' },
|
||||
},
|
||||
{
|
||||
id: 'fields',
|
||||
title: 'Fields to Return',
|
||||
type: 'short-input',
|
||||
placeholder: 'number,short_description,priority',
|
||||
condition: { field: 'operation', value: 'read' },
|
||||
description: 'Comma-separated list of fields',
|
||||
},
|
||||
// Update-specific: sysId and fields
|
||||
{
|
||||
id: 'sysId',
|
||||
title: 'Record sys_id',
|
||||
type: 'short-input',
|
||||
placeholder: 'Record sys_id to update',
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'fields',
|
||||
title: 'Fields to Update (JSON)',
|
||||
type: 'code',
|
||||
language: 'json',
|
||||
placeholder: '{\n "state": "2",\n "assigned_to": "user.sys_id"\n}',
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
required: true,
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
maintainHistory: true,
|
||||
prompt: `You are an expert ServiceNow developer. Generate ServiceNow record update field objects as JSON based on the user's request.
|
||||
|
||||
### CONTEXT
|
||||
ServiceNow records use specific field names depending on the table. Common update scenarios include:
|
||||
- incident: state (1=New, 2=In Progress, 3=On Hold, 6=Resolved, 7=Closed), assigned_to, work_notes, close_notes, close_code
|
||||
- task: state, assigned_to, work_notes, percent_complete
|
||||
- change_request: state, risk, approval, work_notes
|
||||
|
||||
### RULES
|
||||
- Output ONLY valid JSON object starting with { and ending with }
|
||||
- Include only the fields that need to be updated
|
||||
- Use correct ServiceNow field names for the target table
|
||||
- For state transitions, use the correct numeric state values
|
||||
- work_notes and comments fields append to existing values
|
||||
|
||||
### EXAMPLE
|
||||
User: "Assign the incident to John and set to in progress"
|
||||
Output: {"state": "2", "assigned_to": "john.doe", "work_notes": "Assigned and starting investigation"}`,
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
// Delete-specific: sysId
|
||||
{
|
||||
id: 'sysId',
|
||||
title: 'Record sys_id',
|
||||
type: 'short-input',
|
||||
placeholder: 'Record sys_id to delete',
|
||||
condition: { field: 'operation', value: 'delete' },
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: [
|
||||
'servicenow_create_record',
|
||||
'servicenow_read_record',
|
||||
'servicenow_update_record',
|
||||
'servicenow_delete_record',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
switch (params.operation) {
|
||||
case 'create':
|
||||
return 'servicenow_create_record'
|
||||
case 'read':
|
||||
return 'servicenow_read_record'
|
||||
case 'update':
|
||||
return 'servicenow_update_record'
|
||||
case 'delete':
|
||||
return 'servicenow_delete_record'
|
||||
default:
|
||||
throw new Error(`Invalid ServiceNow operation: ${params.operation}`)
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const { operation, fields, records, credential, ...rest } = params
|
||||
|
||||
// Parse JSON fields if provided
|
||||
let parsedFields: Record<string, any> | undefined
|
||||
if (fields && (operation === 'create' || operation === 'update')) {
|
||||
try {
|
||||
parsedFields = typeof fields === 'string' ? JSON.parse(fields) : fields
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Invalid JSON in fields: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate OAuth credential
|
||||
if (!credential) {
|
||||
throw new Error('ServiceNow account credential is required')
|
||||
}
|
||||
|
||||
// Build params
|
||||
const baseParams: Record<string, any> = {
|
||||
...rest,
|
||||
credential,
|
||||
}
|
||||
|
||||
if (operation === 'create' || operation === 'update') {
|
||||
return {
|
||||
...baseParams,
|
||||
fields: parsedFields,
|
||||
}
|
||||
}
|
||||
return baseParams
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'Operation to perform' },
|
||||
instanceUrl: { type: 'string', description: 'ServiceNow instance URL' },
|
||||
credential: { type: 'string', description: 'ServiceNow OAuth credential ID' },
|
||||
tableName: { type: 'string', description: 'Table name' },
|
||||
sysId: { type: 'string', description: 'Record sys_id' },
|
||||
number: { type: 'string', description: 'Record number' },
|
||||
query: { type: 'string', description: 'Query string' },
|
||||
limit: { type: 'number', description: 'Result limit' },
|
||||
fields: { type: 'json', description: 'Fields object or JSON string' },
|
||||
},
|
||||
outputs: {
|
||||
record: { type: 'json', description: 'Single ServiceNow record' },
|
||||
records: { type: 'json', description: 'Array of ServiceNow records' },
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
metadata: { type: 'json', description: 'Operation metadata' },
|
||||
},
|
||||
}
|
||||
@@ -96,6 +96,7 @@ import { SearchBlock } from '@/blocks/blocks/search'
|
||||
import { SendGridBlock } from '@/blocks/blocks/sendgrid'
|
||||
import { SentryBlock } from '@/blocks/blocks/sentry'
|
||||
import { SerperBlock } from '@/blocks/blocks/serper'
|
||||
import { ServiceNowBlock } from '@/blocks/blocks/servicenow'
|
||||
import { SftpBlock } from '@/blocks/blocks/sftp'
|
||||
import { SharepointBlock } from '@/blocks/blocks/sharepoint'
|
||||
import { ShopifyBlock } from '@/blocks/blocks/shopify'
|
||||
@@ -238,6 +239,7 @@ export const registry: Record<string, BlockConfig> = {
|
||||
search: SearchBlock,
|
||||
sendgrid: SendGridBlock,
|
||||
sentry: SentryBlock,
|
||||
servicenow: ServiceNowBlock,
|
||||
serper: SerperBlock,
|
||||
sharepoint: SharepointBlock,
|
||||
shopify: ShopifyBlock,
|
||||
|
||||
@@ -3335,6 +3335,24 @@ export function SalesforceIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function ServiceNowIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 1570 1403'
|
||||
width='48'
|
||||
height='48'
|
||||
>
|
||||
<path
|
||||
fill='#62d84e'
|
||||
fillRule='evenodd'
|
||||
d='M1228.4 138.9c129.2 88.9 228.9 214.3 286.3 360.2 57.5 145.8 70 305.5 36 458.5S1437.8 1250 1324 1357.9c-13.3 12.9-28.8 23.4-45.8 30.8-17 7.5-35.2 11.9-53.7 12.9-18.5 1.1-37.1-1.1-54.8-6.6-17.7-5.4-34.3-13.9-49.1-25.2-48.2-35.9-101.8-63.8-158.8-82.6-57.1-18.9-116.7-28.5-176.8-28.5s-119.8 9.6-176.8 28.5c-57 18.8-110.7 46.7-158.9 82.6-14.6 11.2-31 19.8-48.6 25.3s-36 7.8-54.4 6.8c-18.4-.9-36.5-5.1-53.4-12.4s-32.4-17.5-45.8-30.2C132.5 1251 53 1110.8 19 956.8s-20.9-314.6 37.6-461c58.5-146.5 159.6-272 290.3-360.3S631.8.1 789.6.5c156.8 1.3 309.6 49.6 438.8 138.4m-291.8 1014c48.2-19.2 92-48 128.7-84.6 36.7-36.7 65.5-80.4 84.7-128.6 19.2-48.1 28.4-99.7 27-151.5 0-103.9-41.3-203.5-114.8-277S889 396.4 785 396.4s-203.7 41.3-277.2 114.8S393 684.3 393 788.2c-1.4 51.8 7.8 103.4 27 151.5 19.2 48.2 48 91.9 84.7 128.6 36.7 36.6 80.5 65.4 128.6 84.6 48.2 19.2 99.8 28.4 151.7 27 51.8 1.4 103.4-7.8 151.6-27'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function ApolloIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -142,6 +142,13 @@ export function useConnectOAuthService() {
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
// ServiceNow requires a custom OAuth flow with instance URL input
|
||||
if (providerId === 'servicenow') {
|
||||
const returnUrl = encodeURIComponent(callbackURL)
|
||||
window.location.href = `/api/auth/servicenow/authorize?returnUrl=${returnUrl}`
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
await client.oauth2.link({
|
||||
providerId,
|
||||
callbackURL,
|
||||
|
||||
@@ -233,6 +233,8 @@ export const env = createEnv({
|
||||
WORDPRESS_CLIENT_SECRET: z.string().optional(), // WordPress.com OAuth client secret
|
||||
SPOTIFY_CLIENT_ID: z.string().optional(), // Spotify OAuth client ID
|
||||
SPOTIFY_CLIENT_SECRET: z.string().optional(), // Spotify OAuth client secret
|
||||
SERVICENOW_CLIENT_ID: z.string().optional(), // ServiceNow OAuth client ID
|
||||
SERVICENOW_CLIENT_SECRET: z.string().optional(), // ServiceNow OAuth client secret
|
||||
|
||||
// E2B Remote Code Execution
|
||||
E2B_ENABLED: z.string().optional(), // Enable E2B remote code execution
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
PipedriveIcon,
|
||||
RedditIcon,
|
||||
SalesforceIcon,
|
||||
ServiceNowIcon,
|
||||
ShopifyIcon,
|
||||
SlackIcon,
|
||||
SpotifyIcon,
|
||||
@@ -69,6 +70,7 @@ export type OAuthProvider =
|
||||
| 'salesforce'
|
||||
| 'linkedin'
|
||||
| 'shopify'
|
||||
| 'servicenow'
|
||||
| 'zoom'
|
||||
| 'wordpress'
|
||||
| 'spotify'
|
||||
@@ -111,6 +113,7 @@ export type OAuthService =
|
||||
| 'salesforce'
|
||||
| 'linkedin'
|
||||
| 'shopify'
|
||||
| 'servicenow'
|
||||
| 'zoom'
|
||||
| 'wordpress'
|
||||
| 'spotify'
|
||||
@@ -618,6 +621,23 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
},
|
||||
defaultService: 'shopify',
|
||||
},
|
||||
servicenow: {
|
||||
id: 'servicenow',
|
||||
name: 'ServiceNow',
|
||||
icon: (props) => ServiceNowIcon(props),
|
||||
services: {
|
||||
servicenow: {
|
||||
id: 'servicenow',
|
||||
name: 'ServiceNow',
|
||||
description: 'Manage incidents, tasks, and records in your ServiceNow instance.',
|
||||
providerId: 'servicenow',
|
||||
icon: (props) => ServiceNowIcon(props),
|
||||
baseProviderIcon: (props) => ServiceNowIcon(props),
|
||||
scopes: ['useraccount'],
|
||||
},
|
||||
},
|
||||
defaultService: 'servicenow',
|
||||
},
|
||||
slack: {
|
||||
id: 'slack',
|
||||
name: 'Slack',
|
||||
@@ -1487,6 +1507,21 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig {
|
||||
supportsRefreshTokenRotation: false,
|
||||
}
|
||||
}
|
||||
case 'servicenow': {
|
||||
// ServiceNow OAuth - token endpoint is instance-specific
|
||||
// This is a placeholder; actual token endpoint is set during authorization
|
||||
const { clientId, clientSecret } = getCredentials(
|
||||
env.SERVICENOW_CLIENT_ID,
|
||||
env.SERVICENOW_CLIENT_SECRET
|
||||
)
|
||||
return {
|
||||
tokenEndpoint: '', // Instance-specific, set during authorization
|
||||
clientId,
|
||||
clientSecret,
|
||||
useBasicAuth: false,
|
||||
supportsRefreshTokenRotation: true,
|
||||
}
|
||||
}
|
||||
case 'zoom': {
|
||||
const { clientId, clientSecret } = getCredentials(env.ZOOM_CLIENT_ID, env.ZOOM_CLIENT_SECRET)
|
||||
return {
|
||||
@@ -1565,11 +1600,13 @@ function buildAuthRequest(
|
||||
* This is a server-side utility function to refresh OAuth tokens
|
||||
* @param providerId The provider ID (e.g., 'google-drive')
|
||||
* @param refreshToken The refresh token to use
|
||||
* @param instanceUrl Optional instance URL for providers with instance-specific endpoints (e.g., ServiceNow)
|
||||
* @returns Object containing the new access token and expiration time in seconds, or null if refresh failed
|
||||
*/
|
||||
export async function refreshOAuthToken(
|
||||
providerId: string,
|
||||
refreshToken: string
|
||||
refreshToken: string,
|
||||
instanceUrl?: string
|
||||
): Promise<{ accessToken: string; expiresIn: number; refreshToken: string } | null> {
|
||||
try {
|
||||
// Get the provider from the providerId (e.g., 'google-drive' -> 'google')
|
||||
@@ -1578,11 +1615,21 @@ export async function refreshOAuthToken(
|
||||
// Get provider configuration
|
||||
const config = getProviderAuthConfig(provider)
|
||||
|
||||
// For ServiceNow, the token endpoint is instance-specific
|
||||
let tokenEndpoint = config.tokenEndpoint
|
||||
if (provider === 'servicenow') {
|
||||
if (!instanceUrl) {
|
||||
logger.error('ServiceNow token refresh requires instance URL')
|
||||
return null
|
||||
}
|
||||
tokenEndpoint = `${instanceUrl.replace(/\/$/, '')}/oauth_token.do`
|
||||
}
|
||||
|
||||
// Build authentication request
|
||||
const { headers, bodyParams } = buildAuthRequest(config, refreshToken)
|
||||
|
||||
// Refresh the token
|
||||
const response = await fetch(config.tokenEndpoint, {
|
||||
const response = await fetch(tokenEndpoint, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: new URLSearchParams(bodyParams).toString(),
|
||||
|
||||
@@ -45,8 +45,9 @@ function getStackKey(workflowId: string, userId: string): string {
|
||||
|
||||
/**
|
||||
* Custom storage adapter for Zustand's persist middleware.
|
||||
* We need this wrapper to gracefully handle 'QuotaExceededError' when localStorage is full.
|
||||
* We need this wrapper to gracefully handle 'QuotaExceededError' when localStorage is full,
|
||||
* Without this, the default storage engine would throw and crash the application.
|
||||
* and to properly handle SSR/Node.js environments.
|
||||
*/
|
||||
const safeStorageAdapter = {
|
||||
getItem: (name: string): string | null => {
|
||||
|
||||
@@ -959,6 +959,12 @@ import {
|
||||
updateProjectTool,
|
||||
} from '@/tools/sentry'
|
||||
import { serperSearchTool } from '@/tools/serper'
|
||||
import {
|
||||
servicenowCreateRecordTool,
|
||||
servicenowDeleteRecordTool,
|
||||
servicenowReadRecordTool,
|
||||
servicenowUpdateRecordTool,
|
||||
} from '@/tools/servicenow'
|
||||
import {
|
||||
sftpDeleteTool,
|
||||
sftpDownloadTool,
|
||||
@@ -1520,6 +1526,10 @@ export const tools: Record<string, ToolConfig> = {
|
||||
github_repo_info: githubRepoInfoTool,
|
||||
github_latest_commit: githubLatestCommitTool,
|
||||
serper_search: serperSearchTool,
|
||||
servicenow_create_record: servicenowCreateRecordTool,
|
||||
servicenow_read_record: servicenowReadRecordTool,
|
||||
servicenow_update_record: servicenowUpdateRecordTool,
|
||||
servicenow_delete_record: servicenowDeleteRecordTool,
|
||||
tavily_search: tavilySearchTool,
|
||||
tavily_extract: tavilyExtractTool,
|
||||
tavily_crawl: tavilyCrawlTool,
|
||||
|
||||
107
apps/sim/tools/servicenow/create_record.ts
Normal file
107
apps/sim/tools/servicenow/create_record.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { ServiceNowCreateParams, ServiceNowCreateResponse } from '@/tools/servicenow/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('ServiceNowCreateRecordTool')
|
||||
|
||||
export const createRecordTool: ToolConfig<ServiceNowCreateParams, ServiceNowCreateResponse> = {
|
||||
id: 'servicenow_create_record',
|
||||
name: 'Create ServiceNow Record',
|
||||
description: 'Create a new record in a ServiceNow table',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'servicenow',
|
||||
},
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'ServiceNow instance URL (e.g., https://instance.service-now.com)',
|
||||
},
|
||||
credential: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'ServiceNow OAuth credential ID',
|
||||
},
|
||||
tableName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name (e.g., incident, task, sys_user)',
|
||||
},
|
||||
fields: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Fields to set on the record (JSON object)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
// Use instanceUrl if provided, otherwise fall back to idToken (stored instance URL from OAuth)
|
||||
const baseUrl = (params.instanceUrl || params.idToken || '').replace(/\/$/, '')
|
||||
if (!baseUrl) {
|
||||
throw new Error('ServiceNow instance URL is required')
|
||||
}
|
||||
return `${baseUrl}/api/now/table/${params.tableName}`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('OAuth access token is required')
|
||||
}
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
if (!params.fields || typeof params.fields !== 'object') {
|
||||
throw new Error('Fields must be a JSON object')
|
||||
}
|
||||
return params.fields
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
try {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
const error = data.error || data
|
||||
throw new Error(typeof error === 'string' ? error : error.message || JSON.stringify(error))
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
record: data.result,
|
||||
metadata: {
|
||||
recordCount: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('ServiceNow create record - Error processing response:', { error })
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
record: {
|
||||
type: 'json',
|
||||
description: 'Created ServiceNow record with sys_id and other fields',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Operation metadata',
|
||||
},
|
||||
},
|
||||
}
|
||||
107
apps/sim/tools/servicenow/delete_record.ts
Normal file
107
apps/sim/tools/servicenow/delete_record.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { ServiceNowDeleteParams, ServiceNowDeleteResponse } from '@/tools/servicenow/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('ServiceNowDeleteRecordTool')
|
||||
|
||||
export const deleteRecordTool: ToolConfig<ServiceNowDeleteParams, ServiceNowDeleteResponse> = {
|
||||
id: 'servicenow_delete_record',
|
||||
name: 'Delete ServiceNow Record',
|
||||
description: 'Delete a record from a ServiceNow table',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'servicenow',
|
||||
},
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'ServiceNow instance URL (auto-detected from OAuth if not provided)',
|
||||
},
|
||||
credential: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'ServiceNow OAuth credential ID',
|
||||
},
|
||||
tableName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name',
|
||||
},
|
||||
sysId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Record sys_id to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
// Use instanceUrl if provided, otherwise fall back to idToken (stored instance URL from OAuth)
|
||||
const baseUrl = (params.instanceUrl || params.idToken || '').replace(/\/$/, '')
|
||||
if (!baseUrl) {
|
||||
throw new Error('ServiceNow instance URL is required')
|
||||
}
|
||||
return `${baseUrl}/api/now/table/${params.tableName}/${params.sysId}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('OAuth access token is required')
|
||||
}
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
Accept: 'application/json',
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: ServiceNowDeleteParams) => {
|
||||
try {
|
||||
if (!response.ok) {
|
||||
let errorData: any
|
||||
try {
|
||||
errorData = await response.json()
|
||||
} catch {
|
||||
errorData = { status: response.status, statusText: response.statusText }
|
||||
}
|
||||
throw new Error(
|
||||
typeof errorData === 'string'
|
||||
? errorData
|
||||
: errorData.error?.message || JSON.stringify(errorData)
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
metadata: {
|
||||
deletedSysId: params?.sysId || '',
|
||||
},
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('ServiceNow delete record - Error processing response:', { error })
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the deletion was successful',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Operation metadata',
|
||||
},
|
||||
},
|
||||
}
|
||||
11
apps/sim/tools/servicenow/index.ts
Normal file
11
apps/sim/tools/servicenow/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { createRecordTool } from '@/tools/servicenow/create_record'
|
||||
import { deleteRecordTool } from '@/tools/servicenow/delete_record'
|
||||
import { readRecordTool } from '@/tools/servicenow/read_record'
|
||||
import { updateRecordTool } from '@/tools/servicenow/update_record'
|
||||
|
||||
export {
|
||||
createRecordTool as servicenowCreateRecordTool,
|
||||
readRecordTool as servicenowReadRecordTool,
|
||||
updateRecordTool as servicenowUpdateRecordTool,
|
||||
deleteRecordTool as servicenowDeleteRecordTool,
|
||||
}
|
||||
149
apps/sim/tools/servicenow/read_record.ts
Normal file
149
apps/sim/tools/servicenow/read_record.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { ServiceNowReadParams, ServiceNowReadResponse } from '@/tools/servicenow/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('ServiceNowReadRecordTool')
|
||||
|
||||
export const readRecordTool: ToolConfig<ServiceNowReadParams, ServiceNowReadResponse> = {
|
||||
id: 'servicenow_read_record',
|
||||
name: 'Read ServiceNow Records',
|
||||
description: 'Read records from a ServiceNow table',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'servicenow',
|
||||
},
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'ServiceNow instance URL (auto-detected from OAuth if not provided)',
|
||||
},
|
||||
credential: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'ServiceNow OAuth credential ID',
|
||||
},
|
||||
tableName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name',
|
||||
},
|
||||
sysId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Specific record sys_id',
|
||||
},
|
||||
number: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Record number (e.g., INC0010001)',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Encoded query string (e.g., "active=true^priority=1")',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Maximum number of records to return',
|
||||
},
|
||||
fields: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Comma-separated list of fields to return',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
// Use instanceUrl if provided, otherwise fall back to idToken (stored instance URL from OAuth)
|
||||
const baseUrl = (params.instanceUrl || params.idToken || '').replace(/\/$/, '')
|
||||
if (!baseUrl) {
|
||||
throw new Error('ServiceNow instance URL is required')
|
||||
}
|
||||
let url = `${baseUrl}/api/now/table/${params.tableName}`
|
||||
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
if (params.sysId) {
|
||||
url = `${url}/${params.sysId}`
|
||||
} else if (params.number) {
|
||||
queryParams.append('number', params.number)
|
||||
}
|
||||
|
||||
if (params.query) {
|
||||
queryParams.append('sysparm_query', params.query)
|
||||
}
|
||||
|
||||
if (params.limit) {
|
||||
queryParams.append('sysparm_limit', params.limit.toString())
|
||||
}
|
||||
|
||||
if (params.fields) {
|
||||
queryParams.append('sysparm_fields', params.fields)
|
||||
}
|
||||
|
||||
const queryString = queryParams.toString()
|
||||
return queryString ? `${url}?${queryString}` : url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('OAuth access token is required')
|
||||
}
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
Accept: 'application/json',
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
try {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
const error = data.error || data
|
||||
throw new Error(typeof error === 'string' ? error : error.message || JSON.stringify(error))
|
||||
}
|
||||
|
||||
const records = Array.isArray(data.result) ? data.result : [data.result]
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
records,
|
||||
metadata: {
|
||||
recordCount: records.length,
|
||||
},
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('ServiceNow read record - Error processing response:', { error })
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
records: {
|
||||
type: 'array',
|
||||
description: 'Array of ServiceNow records',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Operation metadata',
|
||||
},
|
||||
},
|
||||
}
|
||||
80
apps/sim/tools/servicenow/types.ts
Normal file
80
apps/sim/tools/servicenow/types.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
export interface ServiceNowRecord {
|
||||
sys_id: string
|
||||
number?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface ServiceNowBaseParams {
|
||||
instanceUrl?: string
|
||||
tableName: string
|
||||
// OAuth fields (injected by the system when using OAuth)
|
||||
credential?: string
|
||||
accessToken?: string
|
||||
idToken?: string // Stores the instance URL from OAuth
|
||||
}
|
||||
|
||||
export interface ServiceNowCreateParams extends ServiceNowBaseParams {
|
||||
fields: Record<string, any>
|
||||
}
|
||||
|
||||
export interface ServiceNowCreateResponse extends ToolResponse {
|
||||
output: {
|
||||
record: ServiceNowRecord
|
||||
metadata: {
|
||||
recordCount: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ServiceNowReadParams extends ServiceNowBaseParams {
|
||||
sysId?: string
|
||||
number?: string
|
||||
query?: string
|
||||
limit?: number
|
||||
fields?: string
|
||||
}
|
||||
|
||||
export interface ServiceNowReadResponse extends ToolResponse {
|
||||
output: {
|
||||
records: ServiceNowRecord[]
|
||||
metadata: {
|
||||
recordCount: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ServiceNowUpdateParams extends ServiceNowBaseParams {
|
||||
sysId: string
|
||||
fields: Record<string, any>
|
||||
}
|
||||
|
||||
export interface ServiceNowUpdateResponse extends ToolResponse {
|
||||
output: {
|
||||
record: ServiceNowRecord
|
||||
metadata: {
|
||||
recordCount: 1
|
||||
updatedFields: string[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ServiceNowDeleteParams extends ServiceNowBaseParams {
|
||||
sysId: string
|
||||
}
|
||||
|
||||
export interface ServiceNowDeleteResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
metadata: {
|
||||
deletedSysId: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type ServiceNowResponse =
|
||||
| ServiceNowCreateResponse
|
||||
| ServiceNowReadResponse
|
||||
| ServiceNowUpdateResponse
|
||||
| ServiceNowDeleteResponse
|
||||
114
apps/sim/tools/servicenow/update_record.ts
Normal file
114
apps/sim/tools/servicenow/update_record.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { ServiceNowUpdateParams, ServiceNowUpdateResponse } from '@/tools/servicenow/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('ServiceNowUpdateRecordTool')
|
||||
|
||||
export const updateRecordTool: ToolConfig<ServiceNowUpdateParams, ServiceNowUpdateResponse> = {
|
||||
id: 'servicenow_update_record',
|
||||
name: 'Update ServiceNow Record',
|
||||
description: 'Update an existing record in a ServiceNow table',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'servicenow',
|
||||
},
|
||||
|
||||
params: {
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'ServiceNow instance URL (auto-detected from OAuth if not provided)',
|
||||
},
|
||||
credential: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'ServiceNow OAuth credential ID',
|
||||
},
|
||||
tableName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table name',
|
||||
},
|
||||
sysId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Record sys_id to update',
|
||||
},
|
||||
fields: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Fields to update (JSON object)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
// Use instanceUrl if provided, otherwise fall back to idToken (stored instance URL from OAuth)
|
||||
const baseUrl = (params.instanceUrl || params.idToken || '').replace(/\/$/, '')
|
||||
if (!baseUrl) {
|
||||
throw new Error('ServiceNow instance URL is required')
|
||||
}
|
||||
return `${baseUrl}/api/now/table/${params.tableName}/${params.sysId}`
|
||||
},
|
||||
method: 'PATCH',
|
||||
headers: (params) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('OAuth access token is required')
|
||||
}
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
if (!params.fields || typeof params.fields !== 'object') {
|
||||
throw new Error('Fields must be a JSON object')
|
||||
}
|
||||
return params.fields
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: ServiceNowUpdateParams) => {
|
||||
try {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
const error = data.error || data
|
||||
throw new Error(typeof error === 'string' ? error : error.message || JSON.stringify(error))
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
record: data.result,
|
||||
metadata: {
|
||||
recordCount: 1,
|
||||
updatedFields: params ? Object.keys(params.fields || {}) : [],
|
||||
},
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('ServiceNow update record - Error processing response:', { error })
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
record: {
|
||||
type: 'json',
|
||||
description: 'Updated ServiceNow record',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Operation metadata',
|
||||
},
|
||||
},
|
||||
}
|
||||
86
bun.lock
86
bun.lock
@@ -266,19 +266,19 @@
|
||||
"sharp",
|
||||
],
|
||||
"overrides": {
|
||||
"@next/env": "16.1.0-canary.21",
|
||||
"drizzle-orm": "^0.44.5",
|
||||
"next": "16.1.0-canary.21",
|
||||
"postgres": "^3.4.5",
|
||||
"react": "19.2.1",
|
||||
"react-dom": "19.2.1",
|
||||
"next": "16.1.0-canary.21",
|
||||
"@next/env": "16.1.0-canary.21",
|
||||
"drizzle-orm": "^0.44.5",
|
||||
"postgres": "^3.4.5",
|
||||
},
|
||||
"packages": {
|
||||
"@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="],
|
||||
|
||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XHJKu0Yvfu9SPzRfsAFESa+9T7f2YJY6TxykKMfRsAwpeWAiX/Gbx5J5uM15AzYC3Rw8tVP3oH+j7jEivENirQ=="],
|
||||
|
||||
"@ai-sdk/azure": ["@ai-sdk/azure@2.0.88", "", { "dependencies": { "@ai-sdk/openai": "2.0.86", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OMAXXZV7GiFz8qpCpzhaesTfiuiXU92WZWdvtr+K8rjfTNGm9sJWUuSLZ29z5aAeLUSRlwDMUlK4lYr8/1IewQ=="],
|
||||
"@ai-sdk/azure": ["@ai-sdk/azure@2.0.89", "", { "dependencies": { "@ai-sdk/openai": "2.0.87", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ELwVkqvvBVDtDH5DtAFhp4tltIdCWVZMwtwodc8v9y0XJyGJiCNdx1Dl9dwS/VzgJpjcj/u2pGs6vTjzBA+M9Q=="],
|
||||
|
||||
"@ai-sdk/cerebras": ["@ai-sdk/cerebras@1.0.33", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.29", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2gSSS/7kunIwMdC4td5oWsUAzoLw84ccGpz6wQbxVnrb1iWnrEnKa5tRBduaP6IXpzLWsu8wME3+dQhZy+gT7w=="],
|
||||
|
||||
@@ -286,15 +286,15 @@
|
||||
|
||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-BwV7DU/lAm3Xn6iyyvZdWgVxgLu3SNXzl5y57gMvkW4nGhAOV5269IrJzQwGt03bb107sa6H6uJwWxc77zXoGA=="],
|
||||
|
||||
"@ai-sdk/google": ["@ai-sdk/google@2.0.46", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8PK6u4sGE/kXebd7ZkTp+0aya4kNqzoqpS5m7cHY2NfTK6fhPc6GNvE+MZIZIoHQTp5ed86wGBdeBPpFaaUtyg=="],
|
||||
"@ai-sdk/google": ["@ai-sdk/google@2.0.47", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-grIlvzh+jzMoKNOnn5Xe/8fdYiJOs0ThMVetsGzqflvMkUNF3B83t5i0kf4XqiM8MwTJ8gkdOA4VeQOZKR7TkA=="],
|
||||
|
||||
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.91", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.56", "@ai-sdk/google": "2.0.46", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-SonFMMdSIlos0fjBFBff7rcZQx+q3WP4CpXdz7+YEIEWItnR/k9f5MqRCXMZilfyzcpz5wFxa7Sqlnapv3oqsA=="],
|
||||
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.92", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.56", "@ai-sdk/google": "2.0.47", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-L1hqen0UdslZEkuZZhZR8rC6RrTlMyZbtbd3wSoXGnpJiJ0SGSsUc2RFBz6YtbVhZo9GeFPtrnzD8zqIsOBtVQ=="],
|
||||
|
||||
"@ai-sdk/groq": ["@ai-sdk/groq@2.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FWGl7xNr88NBveao3y9EcVWYUt9ABPrwLFY7pIutSNgaTf32vgvyhREobaMrLU4Scr5G/2tlNqOPZ5wkYMaZig=="],
|
||||
|
||||
"@ai-sdk/mistral": ["@ai-sdk/mistral@2.0.26", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-jxDB++4WI1wEx5ONNBI+VbkmYJOYIuS8UQY13/83UGRaiW7oB/WHiH4ETe6KzbKpQPB3XruwTJQjUMsMfKyTXA=="],
|
||||
|
||||
"@ai-sdk/openai": ["@ai-sdk/openai@2.0.86", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-obsLIOyA93lbQiSt1rvBItoVQp1U2RDPs0bNG0JYhm6Gku8Dg/0Cm8e4NUWT5p5PN10/doKSb3SMSKCixwIAKA=="],
|
||||
"@ai-sdk/openai": ["@ai-sdk/openai@2.0.87", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-qywHMz8Kd+y/cluanX63SqFV/J8gLq596+W8K/MgdNroEnSabRIeikEP1/K0wwuKtSI7/KaLlVUnt1N5E3889Q=="],
|
||||
|
||||
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
|
||||
|
||||
@@ -1080,49 +1080,49 @@
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.4", "", { "os": "android", "cpu": "arm" }, "sha512-PWU3Y92H4DD0bOqorEPp1Y0tbzwAurFmIYpjcObv5axGVOtcTlB0b2UKMd2echo08MgN7jO8WQZSSysvfisFSQ=="],
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.5", "", { "os": "android", "cpu": "arm" }, "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.53.4", "", { "os": "android", "cpu": "arm64" }, "sha512-Gw0/DuVm3rGsqhMGYkSOXXIx20cC3kTlivZeuaGt4gEgILivykNyBWxeUV5Cf2tDA2nPLah26vq3emlRrWVbng=="],
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.53.5", "", { "os": "android", "cpu": "arm64" }, "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.53.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+w06QvXsgzKwdVg5qRLZpTHh1bigHZIqoIUPtiqh05ZiJVUQ6ymOxaPkXTvRPRLH88575ZCRSRM3PwIoNma01Q=="],
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.53.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.53.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-EB4Na9G2GsrRNRNFPuxfwvDRDUwQEzJPpiK1vo2zMVhEeufZ1k7J1bKnT0JYDfnPC7RNZ2H5YNQhW6/p2QKATw=="],
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.53.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.53.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-bldA8XEqPcs6OYdknoTMaGhjytnwQ0NClSPpWpmufOuGPN5dDmvIa32FygC2gneKK4A1oSx86V1l55hyUWUYFQ=="],
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.53.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.53.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-3T8GPjH6mixCd0YPn0bXtcuSXi1Lj+15Ujw2CEb7dd24j9thcKscCf88IV7n76WaAdorOzAgSSbuVRg4C8V8Qw=="],
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.53.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.53.4", "", { "os": "linux", "cpu": "arm" }, "sha512-UPMMNeC4LXW7ZSHxeP3Edv09aLsFUMaD1TSVW6n1CWMECnUIJMFFB7+XC2lZTdPtvB36tYC0cJWc86mzSsaviw=="],
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.53.5", "", { "os": "linux", "cpu": "arm" }, "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.53.4", "", { "os": "linux", "cpu": "arm" }, "sha512-H8uwlV0otHs5Q7WAMSoyvjV9DJPiy5nJ/xnHolY0QptLPjaSsuX7tw+SPIfiYH6cnVx3fe4EWFafo6gH6ekZKA=="],
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.53.5", "", { "os": "linux", "cpu": "arm" }, "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.53.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-BLRwSRwICXz0TXkbIbqJ1ibK+/dSBpTJqDClF61GWIrxTXZWQE78ROeIhgl5MjVs4B4gSLPCFeD4xML9vbzvCQ=="],
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.53.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.53.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-6bySEjOTbmVcPJAywjpGLckK793A0TJWSbIa0sVwtVGfe/Nz6gOWHOwkshUIAp9j7wg2WKcA4Snu7Y1nUZyQew=="],
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.53.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.53.4", "", { "os": "linux", "cpu": "none" }, "sha512-U0ow3bXYJZ5MIbchVusxEycBw7bO6C2u5UvD31i5IMTrnt2p4Fh4ZbHSdc/31TScIJQYHwxbj05BpevB3201ug=="],
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.53.5", "", { "os": "linux", "cpu": "none" }, "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.53.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-iujDk07ZNwGLVn0YIWM80SFN039bHZHCdCCuX9nyx3Jsa2d9V/0Y32F+YadzwbvDxhSeVo9zefkoPnXEImnM5w=="],
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.53.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.53.4", "", { "os": "linux", "cpu": "none" }, "sha512-MUtAktiOUSu+AXBpx1fkuG/Bi5rhlorGs3lw5QeJ2X3ziEGAq7vFNdWVde6XGaVqi0LGSvugwjoxSNJfHFTC0g=="],
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.53.5", "", { "os": "linux", "cpu": "none" }, "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.53.4", "", { "os": "linux", "cpu": "none" }, "sha512-btm35eAbDfPtcFEgaXCI5l3c2WXyzwiE8pArhd66SDtoLWmgK5/M7CUxmUglkwtniPzwvWioBKKl6IXLbPf2sQ=="],
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.53.5", "", { "os": "linux", "cpu": "none" }, "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.53.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-uJlhKE9ccUTCUlK+HUz/80cVtx2RayadC5ldDrrDUFaJK0SNb8/cCmC9RhBhIWuZ71Nqj4Uoa9+xljKWRogdhA=="],
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.53.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.53.4", "", { "os": "linux", "cpu": "x64" }, "sha512-jjEMkzvASQBbzzlzf4os7nzSBd/cvPrpqXCUOqoeCh1dQ4BP3RZCJk8XBeik4MUln3m+8LeTJcY54C/u8wb3DQ=="],
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.53.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.53.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lu90KG06NNH19shC5rBPkrh6mrTpq5kviFylPBXQVpdEu0yzb0mDgyxLr6XdcGdBIQTH/UAhDJnL+APZTBu1aQ=="],
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.53.5", "", { "os": "linux", "cpu": "x64" }, "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.53.4", "", { "os": "none", "cpu": "arm64" }, "sha512-dFDcmLwsUzhAm/dn0+dMOQZoONVYBtgik0VuY/d5IJUUb787L3Ko/ibvTvddqhb3RaB7vFEozYevHN4ox22R/w=="],
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.53.5", "", { "os": "none", "cpu": "arm64" }, "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.53.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-WvUpUAWmUxZKtRnQWpRKnLW2DEO8HB/l8z6oFFMNuHndMzFTJEXzaYJ5ZAmzNw0L21QQJZsUQFt2oPf3ykAD/w=="],
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.53.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.53.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-JGbeF2/FDU0x2OLySw/jgvkwWUo05BSiJK0dtuI4LyuXbz3wKiC1xHhLB1Tqm5VU6ZZDmAorj45r/IgWNWku5g=="],
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.53.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.53.4", "", { "os": "win32", "cpu": "x64" }, "sha512-zuuC7AyxLWLubP+mlUwEyR8M1ixW1ERNPHJfXm8x7eQNP4Pzkd7hS3qBuKBR70VRiQ04Kw8FNfRMF5TNxuZq2g=="],
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.53.5", "", { "os": "win32", "cpu": "x64" }, "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.53.4", "", { "os": "win32", "cpu": "x64" }, "sha512-Sbx45u/Lbb5RyptSbX7/3deP+/lzEmZ0BTSHxwxN/IMOZDZf8S0AGo0hJD5n/LQssxb5Z3B4og4P2X6Dd8acCA=="],
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.53.5", "", { "os": "win32", "cpu": "x64" }, "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ=="],
|
||||
|
||||
"@s2-dev/streamstore": ["@s2-dev/streamstore@0.17.3", "", { "dependencies": { "@protobuf-ts/runtime": "^2.11.1" }, "peerDependencies": { "typescript": "^5.9.3" } }, "sha512-UeXL5+MgZQfNkbhCgEDVm7PrV5B3bxh6Zp4C5pUzQQwaoA+iGh2QiiIptRZynWgayzRv4vh0PYfnKpTzJEXegQ=="],
|
||||
|
||||
@@ -1522,7 +1522,7 @@
|
||||
|
||||
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
||||
|
||||
"ai": ["ai@5.0.113", "", { "dependencies": { "@ai-sdk/gateway": "2.0.21", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-26vivpSO/mzZj0k1Si2IpsFspp26ttQICHRySQiMrtWcRd5mnJMX2a8sG28vmZ38C+JUn1cWmfZrsLMxkSMw9g=="],
|
||||
"ai": ["ai@5.0.114", "", { "dependencies": { "@ai-sdk/gateway": "2.0.21", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-q/lxcJA6avYn/TXTaE41VX6p9lN245mDU9bIGuPpfk6WxDMvmMoUKUIS0/aXAPYN3UmkUn/r9rvq/8C98RoCWw=="],
|
||||
|
||||
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||
|
||||
@@ -1560,7 +1560,7 @@
|
||||
|
||||
"ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="],
|
||||
|
||||
"ast-v8-to-istanbul": ["ast-v8-to-istanbul@0.3.8", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^9.0.1" } }, "sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ=="],
|
||||
"ast-v8-to-istanbul": ["ast-v8-to-istanbul@0.3.9", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^9.0.1" } }, "sha512-dSC6tJeOJxbZrPzPbv5mMd6CMiQ1ugaVXXPRad2fXUSsy1kstFn9XQWemV9VW7Y7kpxgQ/4WMoZfwdH8XSU48w=="],
|
||||
|
||||
"astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
|
||||
|
||||
@@ -1598,7 +1598,7 @@
|
||||
|
||||
"base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.7", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg=="],
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.8", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Y1fOuNDowLfgKOypdc9SPABfoWXuZHBOyCS4cD52IeZBhr4Md6CLLs6atcxVrzRmQ06E7hSlm5bHHApPKR/byA=="],
|
||||
|
||||
"basic-auth": ["basic-auth@2.0.1", "", { "dependencies": { "safe-buffer": "5.1.2" } }, "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg=="],
|
||||
|
||||
@@ -1652,8 +1652,6 @@
|
||||
|
||||
"buildcheck": ["buildcheck@0.0.7", "", {}, "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
"c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="],
|
||||
@@ -2056,7 +2054,7 @@
|
||||
|
||||
"fast-content-type-parse": ["fast-content-type-parse@2.0.1", "", {}, "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q=="],
|
||||
|
||||
"fast-copy": ["fast-copy@4.0.1", "", {}, "sha512-+uUOQlhsaswsizHFmEFAQhB3lSiQ+lisxl50N6ZP0wywlZeWsIESxSi9ftPEps8UGfiBzyYP7x27zA674WUvXw=="],
|
||||
"fast-copy": ["fast-copy@4.0.2", "", {}, "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
@@ -3022,7 +3020,7 @@
|
||||
|
||||
"rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="],
|
||||
|
||||
"rollup": ["rollup@4.53.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.4", "@rollup/rollup-android-arm64": "4.53.4", "@rollup/rollup-darwin-arm64": "4.53.4", "@rollup/rollup-darwin-x64": "4.53.4", "@rollup/rollup-freebsd-arm64": "4.53.4", "@rollup/rollup-freebsd-x64": "4.53.4", "@rollup/rollup-linux-arm-gnueabihf": "4.53.4", "@rollup/rollup-linux-arm-musleabihf": "4.53.4", "@rollup/rollup-linux-arm64-gnu": "4.53.4", "@rollup/rollup-linux-arm64-musl": "4.53.4", "@rollup/rollup-linux-loong64-gnu": "4.53.4", "@rollup/rollup-linux-ppc64-gnu": "4.53.4", "@rollup/rollup-linux-riscv64-gnu": "4.53.4", "@rollup/rollup-linux-riscv64-musl": "4.53.4", "@rollup/rollup-linux-s390x-gnu": "4.53.4", "@rollup/rollup-linux-x64-gnu": "4.53.4", "@rollup/rollup-linux-x64-musl": "4.53.4", "@rollup/rollup-openharmony-arm64": "4.53.4", "@rollup/rollup-win32-arm64-msvc": "4.53.4", "@rollup/rollup-win32-ia32-msvc": "4.53.4", "@rollup/rollup-win32-x64-gnu": "4.53.4", "@rollup/rollup-win32-x64-msvc": "4.53.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-YpXaaArg0MvrnJpvduEDYIp7uGOqKXbH9NsHGQ6SxKCOsNAjZF018MmxefFUulVP2KLtiGw1UvZbr+/ekjvlDg=="],
|
||||
"rollup": ["rollup@4.53.5", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.5", "@rollup/rollup-android-arm64": "4.53.5", "@rollup/rollup-darwin-arm64": "4.53.5", "@rollup/rollup-darwin-x64": "4.53.5", "@rollup/rollup-freebsd-arm64": "4.53.5", "@rollup/rollup-freebsd-x64": "4.53.5", "@rollup/rollup-linux-arm-gnueabihf": "4.53.5", "@rollup/rollup-linux-arm-musleabihf": "4.53.5", "@rollup/rollup-linux-arm64-gnu": "4.53.5", "@rollup/rollup-linux-arm64-musl": "4.53.5", "@rollup/rollup-linux-loong64-gnu": "4.53.5", "@rollup/rollup-linux-ppc64-gnu": "4.53.5", "@rollup/rollup-linux-riscv64-gnu": "4.53.5", "@rollup/rollup-linux-riscv64-musl": "4.53.5", "@rollup/rollup-linux-s390x-gnu": "4.53.5", "@rollup/rollup-linux-x64-gnu": "4.53.5", "@rollup/rollup-linux-x64-musl": "4.53.5", "@rollup/rollup-openharmony-arm64": "4.53.5", "@rollup/rollup-win32-arm64-msvc": "4.53.5", "@rollup/rollup-win32-ia32-msvc": "4.53.5", "@rollup/rollup-win32-x64-gnu": "4.53.5", "@rollup/rollup-win32-x64-msvc": "4.53.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ=="],
|
||||
|
||||
"rou3": ["rou3@0.5.1", "", {}, "sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ=="],
|
||||
|
||||
@@ -3372,7 +3370,7 @@
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.2.2", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA=="],
|
||||
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
@@ -3554,9 +3552,9 @@
|
||||
|
||||
"@better-auth/sso/jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
|
||||
|
||||
"@better-auth/sso/zod": ["zod@4.2.0", "", {}, "sha512-Bd5fw9wlIhtqCCxotZgdTOMwGm1a0u75wARVEY9HMs1X17trvA/lMi4+MGK5EUfYkXVTbX8UDiDKW4OgzHVUZw=="],
|
||||
"@better-auth/sso/zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="],
|
||||
|
||||
"@better-auth/stripe/zod": ["zod@4.2.0", "", {}, "sha512-Bd5fw9wlIhtqCCxotZgdTOMwGm1a0u75wARVEY9HMs1X17trvA/lMi4+MGK5EUfYkXVTbX8UDiDKW4OgzHVUZw=="],
|
||||
"@better-auth/stripe/zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="],
|
||||
|
||||
"@browserbasehq/sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
|
||||
|
||||
@@ -3776,7 +3774,7 @@
|
||||
|
||||
"better-auth/jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
|
||||
|
||||
"better-auth/zod": ["zod@4.2.0", "", {}, "sha512-Bd5fw9wlIhtqCCxotZgdTOMwGm1a0u75wARVEY9HMs1X17trvA/lMi4+MGK5EUfYkXVTbX8UDiDKW4OgzHVUZw=="],
|
||||
"better-auth/zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="],
|
||||
|
||||
"bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
||||
|
||||
@@ -3784,8 +3782,6 @@
|
||||
|
||||
"body-parser/iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw=="],
|
||||
|
||||
"bun-types/@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="],
|
||||
|
||||
"c12/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||
|
||||
"c12/confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
|
||||
@@ -3850,7 +3846,7 @@
|
||||
|
||||
"fumadocs-mdx/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
||||
|
||||
"fumadocs-mdx/zod": ["zod@4.2.0", "", {}, "sha512-Bd5fw9wlIhtqCCxotZgdTOMwGm1a0u75wARVEY9HMs1X17trvA/lMi4+MGK5EUfYkXVTbX8UDiDKW4OgzHVUZw=="],
|
||||
"fumadocs-mdx/zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="],
|
||||
|
||||
"fumadocs-ui/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
|
||||
|
||||
@@ -4240,8 +4236,6 @@
|
||||
|
||||
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"bun-types/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
|
||||
|
||||
"c12/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
"chrome-launcher/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
|
||||
|
||||
Reference in New Issue
Block a user