From b7228d57f7a47d1bfb78cf00a3becf54242215fa Mon Sep 17 00:00:00 2001 From: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com> Date: Tue, 16 Dec 2025 21:16:09 -0800 Subject: [PATCH] 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 --- apps/docs/components/icons.tsx | 18 ++ apps/docs/components/ui/icon-mapping.ts | 2 + apps/docs/content/docs/en/tools/meta.json | 1 + .../docs/content/docs/en/tools/servicenow.mdx | 111 ++++++++ apps/sim/app/api/auth/oauth/utils.test.ts | 4 +- apps/sim/app/api/auth/oauth/utils.ts | 26 +- .../auth/oauth2/callback/servicenow/route.ts | 166 +++++++++++ .../api/auth/oauth2/servicenow/store/route.ts | 142 ++++++++++ .../api/auth/servicenow/authorize/route.ts | 264 ++++++++++++++++++ .../components/oauth-required-modal.tsx | 7 + apps/sim/blocks/blocks/servicenow.ts | 257 +++++++++++++++++ apps/sim/blocks/registry.ts | 2 + apps/sim/components/icons.tsx | 18 ++ apps/sim/hooks/queries/oauth-connections.ts | 7 + apps/sim/lib/core/config/env.ts | 2 + apps/sim/lib/oauth/oauth.ts | 51 +++- apps/sim/stores/undo-redo/store.ts | 3 +- apps/sim/tools/registry.ts | 10 + apps/sim/tools/servicenow/create_record.ts | 107 +++++++ apps/sim/tools/servicenow/delete_record.ts | 107 +++++++ apps/sim/tools/servicenow/index.ts | 11 + apps/sim/tools/servicenow/read_record.ts | 149 ++++++++++ apps/sim/tools/servicenow/types.ts | 80 ++++++ apps/sim/tools/servicenow/update_record.ts | 114 ++++++++ bun.lock | 86 +++--- 25 files changed, 1691 insertions(+), 54 deletions(-) create mode 100644 apps/docs/content/docs/en/tools/servicenow.mdx create mode 100644 apps/sim/app/api/auth/oauth2/callback/servicenow/route.ts create mode 100644 apps/sim/app/api/auth/oauth2/servicenow/store/route.ts create mode 100644 apps/sim/app/api/auth/servicenow/authorize/route.ts create mode 100644 apps/sim/blocks/blocks/servicenow.ts create mode 100644 apps/sim/tools/servicenow/create_record.ts create mode 100644 apps/sim/tools/servicenow/delete_record.ts create mode 100644 apps/sim/tools/servicenow/index.ts create mode 100644 apps/sim/tools/servicenow/read_record.ts create mode 100644 apps/sim/tools/servicenow/types.ts create mode 100644 apps/sim/tools/servicenow/update_record.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 12ead996f..2e668f913 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -3335,6 +3335,24 @@ export function SalesforceIcon(props: SVGProps) { ) } +export function ServiceNowIcon(props: SVGProps) { + return ( + + + + ) +} + export function ApolloIcon(props: SVGProps) { return ( = { webflow: WebflowIcon, pinecone: PineconeIcon, apollo: ApolloIcon, + servicenow: ServiceNowIcon, whatsapp: WhatsAppIcon, typeform: TypeformIcon, qdrant: QdrantIcon, diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index 7ca67d7a9..42771ff86 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -80,6 +80,7 @@ "sendgrid", "sentry", "serper", + "servicenow", "sftp", "sharepoint", "shopify", diff --git a/apps/docs/content/docs/en/tools/servicenow.mdx b/apps/docs/content/docs/en/tools/servicenow.mdx new file mode 100644 index 000000000..affb455af --- /dev/null +++ b/apps/docs/content/docs/en/tools/servicenow.mdx @@ -0,0 +1,111 @@ +--- +title: ServiceNow +description: Create, read, update, delete, and bulk import ServiceNow records +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## 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` diff --git a/apps/sim/app/api/auth/oauth/utils.test.ts b/apps/sim/app/api/auth/oauth/utils.test.ts index af5588626..95b3894a6 100644 --- a/apps/sim/app/api/auth/oauth/utils.test.ts +++ b/apps/sim/app/api/auth/oauth/utils.test.ts @@ -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') diff --git a/apps/sim/app/api/auth/oauth/utils.ts b/apps/sim/app/api/auth/oauth/utils.ts index da5e35246..66ea033fd 100644 --- a/apps/sim/app/api/auth/oauth/utils.ts +++ b/apps/sim/app/api/auth/oauth/utils.ts @@ -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`) diff --git a/apps/sim/app/api/auth/oauth2/callback/servicenow/route.ts b/apps/sim/app/api/auth/oauth2/callback/servicenow/route.ts new file mode 100644 index 000000000..0a84066f6 --- /dev/null +++ b/apps/sim/app/api/auth/oauth2/callback/servicenow/route.ts @@ -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`) + } +} diff --git a/apps/sim/app/api/auth/oauth2/servicenow/store/route.ts b/apps/sim/app/api/auth/oauth2/servicenow/store/route.ts new file mode 100644 index 000000000..9029af8c0 --- /dev/null +++ b/apps/sim/app/api/auth/oauth2/servicenow/store/route.ts @@ -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 | 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`) + } +} diff --git a/apps/sim/app/api/auth/servicenow/authorize/route.ts b/apps/sim/app/api/auth/servicenow/authorize/route.ts new file mode 100644 index 000000000..a505ddd60 --- /dev/null +++ b/apps/sim/app/api/auth/servicenow/authorize/route.ts @@ -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( + ` + + + Connect ServiceNow Instance + + + + + +
+

Connect Your ServiceNow Instance

+

Enter your ServiceNow instance URL to continue

+
+
+ + +
+

Your instance URL looks like: https://yourcompany.service-now.com

+
+ + + +`, + { + 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 }) + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx index 818defe02..c03422dd7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx @@ -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, diff --git a/apps/sim/blocks/blocks/servicenow.ts b/apps/sim/blocks/blocks/servicenow.ts new file mode 100644 index 000000000..110323dc1 --- /dev/null +++ b/apps/sim/blocks/blocks/servicenow.ts @@ -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 = { + 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 | 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 = { + ...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' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index ca1f30e84..bd5b96f6b 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -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 = { search: SearchBlock, sendgrid: SendGridBlock, sentry: SentryBlock, + servicenow: ServiceNowBlock, serper: SerperBlock, sharepoint: SharepointBlock, shopify: ShopifyBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 12ead996f..2e668f913 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -3335,6 +3335,24 @@ export function SalesforceIcon(props: SVGProps) { ) } +export function ServiceNowIcon(props: SVGProps) { + return ( + + + + ) +} + export function ApolloIcon(props: SVGProps) { return ( = { }, 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(), diff --git a/apps/sim/stores/undo-redo/store.ts b/apps/sim/stores/undo-redo/store.ts index b5575bc49..af2867a73 100644 --- a/apps/sim/stores/undo-redo/store.ts +++ b/apps/sim/stores/undo-redo/store.ts @@ -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 => { diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index ed28b15e1..f6830d462 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -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 = { 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, diff --git a/apps/sim/tools/servicenow/create_record.ts b/apps/sim/tools/servicenow/create_record.ts new file mode 100644 index 000000000..a8ee81e07 --- /dev/null +++ b/apps/sim/tools/servicenow/create_record.ts @@ -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 = { + 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', + }, + }, +} diff --git a/apps/sim/tools/servicenow/delete_record.ts b/apps/sim/tools/servicenow/delete_record.ts new file mode 100644 index 000000000..25021dbca --- /dev/null +++ b/apps/sim/tools/servicenow/delete_record.ts @@ -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 = { + 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', + }, + }, +} diff --git a/apps/sim/tools/servicenow/index.ts b/apps/sim/tools/servicenow/index.ts new file mode 100644 index 000000000..905b22d8a --- /dev/null +++ b/apps/sim/tools/servicenow/index.ts @@ -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, +} diff --git a/apps/sim/tools/servicenow/read_record.ts b/apps/sim/tools/servicenow/read_record.ts new file mode 100644 index 000000000..93b81c06b --- /dev/null +++ b/apps/sim/tools/servicenow/read_record.ts @@ -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 = { + 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', + }, + }, +} diff --git a/apps/sim/tools/servicenow/types.ts b/apps/sim/tools/servicenow/types.ts new file mode 100644 index 000000000..07a6c073e --- /dev/null +++ b/apps/sim/tools/servicenow/types.ts @@ -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 +} + +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 +} + +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 diff --git a/apps/sim/tools/servicenow/update_record.ts b/apps/sim/tools/servicenow/update_record.ts new file mode 100644 index 000000000..629468e7d --- /dev/null +++ b/apps/sim/tools/servicenow/update_record.ts @@ -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 = { + 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', + }, + }, +} diff --git a/bun.lock b/bun.lock index 41edf429b..c5863930c 100644 --- a/bun.lock +++ b/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=="],