diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 9ffc410cb..f13fc8aa8 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -33,21 +33,21 @@ export function AgentIcon(props: SVGProps) { > ) { > ) { @@ -113,7 +113,7 @@ export function NoteIcon(props: SVGProps) { width='16' height='18' rx='2.5' - stroke='#000000' + stroke='currentColor' strokeWidth='1.5' fill='none' /> @@ -139,7 +139,7 @@ export function WorkflowIcon(props: SVGProps) { cx='12' cy='6' r='3' - stroke='#000000' + stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' strokeLinejoin='round' @@ -151,7 +151,7 @@ export function WorkflowIcon(props: SVGProps) { width='8' x='2' y='16' - stroke='#000000' + stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' strokeLinejoin='round' @@ -163,7 +163,7 @@ export function WorkflowIcon(props: SVGProps) { width='8' x='14' y='16' - stroke='#000000' + stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' strokeLinejoin='round' @@ -171,7 +171,7 @@ export function WorkflowIcon(props: SVGProps) { ) { x2='12' y1='9' y2='12' - stroke='#000000' + stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' strokeLinejoin='round' @@ -203,7 +203,7 @@ export function SignalIcon(props: SVGProps) { > ) { > ) { > ) { > ) { > ) { width='18' height='16' rx='2' - stroke='#000000' + stroke='currentColor' strokeWidth='2' strokeLinecap='round' strokeLinejoin='round' /> - + @@ -350,7 +358,7 @@ export function CodeIcon(props: SVGProps) { > ) { @@ -1491,14 +1499,14 @@ export function DocumentIcon(props: SVGProps) { > @@ -3993,7 +4001,7 @@ export function SmtpIcon(props: SVGProps) { > ) { @@ -4489,14 +4497,14 @@ export function RssIcon(props: SVGProps) { > t.trim()) + .filter(Boolean) + : undefined + + const parsedFields = params.fields + ? (JSON.parse(params.fields) as Array>).map((f) => ({ + id: f.id || '', + title: f.label || f.title || '', + fieldType: toSdkFieldType(f.type || 'STRING'), + value: f.value || '', + sectionId: f.section?.id ?? f.sectionId, + })) + : undefined + + // Cast to any because toSdkCategory/toSdkFieldType return string literals + // that match SDK enum values but TypeScript can't verify this at compile time + const item = await client.items.create({ + vaultId: params.vaultId, + category: toSdkCategory(params.category) as any, + title: params.title || '', + tags: parsedTags, + fields: parsedFields as any, + }) + + return NextResponse.json(normalizeSdkItem(item)) + } + + const connectBody: Record = { + vault: { id: params.vaultId }, + category: params.category, + } + if (params.title) connectBody.title = params.title + if (params.tags) connectBody.tags = params.tags.split(',').map((t) => t.trim()) + if (params.fields) connectBody.fields = JSON.parse(params.fields) + + const response = await connectRequest({ + serverUrl: creds.serverUrl!, + apiKey: creds.apiKey!, + path: `/v1/vaults/${params.vaultId}/items`, + method: 'POST', + body: connectBody, + }) + + const data = await response.json() + if (!response.ok) { + return NextResponse.json( + { error: data.message || 'Failed to create item' }, + { status: response.status } + ) + } + + return NextResponse.json(data) + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + const message = error instanceof Error ? error.message : 'Unknown error' + logger.error(`[${requestId}] Create item failed:`, error) + return NextResponse.json({ error: `Failed to create item: ${message}` }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/onepassword/delete-item/route.ts b/apps/sim/app/api/tools/onepassword/delete-item/route.ts new file mode 100644 index 000000000..8909adf88 --- /dev/null +++ b/apps/sim/app/api/tools/onepassword/delete-item/route.ts @@ -0,0 +1,70 @@ +import { randomUUID } from 'crypto' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { connectRequest, createOnePasswordClient, resolveCredentials } from '../utils' + +const logger = createLogger('OnePasswordDeleteItemAPI') + +const DeleteItemSchema = z.object({ + connectionMode: z.enum(['service_account', 'connect']).nullish(), + serviceAccountToken: z.string().nullish(), + serverUrl: z.string().nullish(), + apiKey: z.string().nullish(), + vaultId: z.string().min(1, 'Vault ID is required'), + itemId: z.string().min(1, 'Item ID is required'), +}) + +export async function POST(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + logger.warn(`[${requestId}] Unauthorized 1Password delete-item attempt`) + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + try { + const body = await request.json() + const params = DeleteItemSchema.parse(body) + const creds = resolveCredentials(params) + + logger.info( + `[${requestId}] Deleting item ${params.itemId} from vault ${params.vaultId} (${creds.mode} mode)` + ) + + if (creds.mode === 'service_account') { + const client = await createOnePasswordClient(creds.serviceAccountToken!) + await client.items.delete(params.vaultId, params.itemId) + return NextResponse.json({ success: true }) + } + + const response = await connectRequest({ + serverUrl: creds.serverUrl!, + apiKey: creds.apiKey!, + path: `/v1/vaults/${params.vaultId}/items/${params.itemId}`, + method: 'DELETE', + }) + + if (!response.ok) { + const data = await response.json().catch(() => ({})) + return NextResponse.json( + { error: (data as Record).message || 'Failed to delete item' }, + { status: response.status } + ) + } + + return NextResponse.json({ success: true }) + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + const message = error instanceof Error ? error.message : 'Unknown error' + logger.error(`[${requestId}] Delete item failed:`, error) + return NextResponse.json({ error: `Failed to delete item: ${message}` }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/onepassword/get-item/route.ts b/apps/sim/app/api/tools/onepassword/get-item/route.ts new file mode 100644 index 000000000..63ac2906b --- /dev/null +++ b/apps/sim/app/api/tools/onepassword/get-item/route.ts @@ -0,0 +1,75 @@ +import { randomUUID } from 'crypto' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { + connectRequest, + createOnePasswordClient, + normalizeSdkItem, + resolveCredentials, +} from '../utils' + +const logger = createLogger('OnePasswordGetItemAPI') + +const GetItemSchema = z.object({ + connectionMode: z.enum(['service_account', 'connect']).nullish(), + serviceAccountToken: z.string().nullish(), + serverUrl: z.string().nullish(), + apiKey: z.string().nullish(), + vaultId: z.string().min(1, 'Vault ID is required'), + itemId: z.string().min(1, 'Item ID is required'), +}) + +export async function POST(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + logger.warn(`[${requestId}] Unauthorized 1Password get-item attempt`) + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + try { + const body = await request.json() + const params = GetItemSchema.parse(body) + const creds = resolveCredentials(params) + + logger.info( + `[${requestId}] Getting item ${params.itemId} from vault ${params.vaultId} (${creds.mode} mode)` + ) + + if (creds.mode === 'service_account') { + const client = await createOnePasswordClient(creds.serviceAccountToken!) + const item = await client.items.get(params.vaultId, params.itemId) + return NextResponse.json(normalizeSdkItem(item)) + } + + const response = await connectRequest({ + serverUrl: creds.serverUrl!, + apiKey: creds.apiKey!, + path: `/v1/vaults/${params.vaultId}/items/${params.itemId}`, + method: 'GET', + }) + + const data = await response.json() + if (!response.ok) { + return NextResponse.json( + { error: data.message || 'Failed to get item' }, + { status: response.status } + ) + } + + return NextResponse.json(data) + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + const message = error instanceof Error ? error.message : 'Unknown error' + logger.error(`[${requestId}] Get item failed:`, error) + return NextResponse.json({ error: `Failed to get item: ${message}` }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/onepassword/get-vault/route.ts b/apps/sim/app/api/tools/onepassword/get-vault/route.ts new file mode 100644 index 000000000..16343134a --- /dev/null +++ b/apps/sim/app/api/tools/onepassword/get-vault/route.ts @@ -0,0 +1,78 @@ +import { randomUUID } from 'crypto' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { + connectRequest, + createOnePasswordClient, + normalizeSdkVault, + resolveCredentials, +} from '../utils' + +const logger = createLogger('OnePasswordGetVaultAPI') + +const GetVaultSchema = z.object({ + connectionMode: z.enum(['service_account', 'connect']).nullish(), + serviceAccountToken: z.string().nullish(), + serverUrl: z.string().nullish(), + apiKey: z.string().nullish(), + vaultId: z.string().min(1, 'Vault ID is required'), +}) + +export async function POST(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + logger.warn(`[${requestId}] Unauthorized 1Password get-vault attempt`) + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + try { + const body = await request.json() + const params = GetVaultSchema.parse(body) + const creds = resolveCredentials(params) + + logger.info(`[${requestId}] Getting 1Password vault ${params.vaultId} (${creds.mode} mode)`) + + if (creds.mode === 'service_account') { + const client = await createOnePasswordClient(creds.serviceAccountToken!) + const vaults = await client.vaults.list() + const vault = vaults.find((v) => v.id === params.vaultId) + + if (!vault) { + return NextResponse.json({ error: 'Vault not found' }, { status: 404 }) + } + + return NextResponse.json(normalizeSdkVault(vault)) + } + + const response = await connectRequest({ + serverUrl: creds.serverUrl!, + apiKey: creds.apiKey!, + path: `/v1/vaults/${params.vaultId}`, + method: 'GET', + }) + + const data = await response.json() + if (!response.ok) { + return NextResponse.json( + { error: data.message || 'Failed to get vault' }, + { status: response.status } + ) + } + + return NextResponse.json(data) + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + const message = error instanceof Error ? error.message : 'Unknown error' + logger.error(`[${requestId}] Get vault failed:`, error) + return NextResponse.json({ error: `Failed to get vault: ${message}` }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/onepassword/list-items/route.ts b/apps/sim/app/api/tools/onepassword/list-items/route.ts new file mode 100644 index 000000000..0e9afabdc --- /dev/null +++ b/apps/sim/app/api/tools/onepassword/list-items/route.ts @@ -0,0 +1,87 @@ +import { randomUUID } from 'crypto' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { + connectRequest, + createOnePasswordClient, + normalizeSdkItemOverview, + resolveCredentials, +} from '../utils' + +const logger = createLogger('OnePasswordListItemsAPI') + +const ListItemsSchema = z.object({ + connectionMode: z.enum(['service_account', 'connect']).nullish(), + serviceAccountToken: z.string().nullish(), + serverUrl: z.string().nullish(), + apiKey: z.string().nullish(), + vaultId: z.string().min(1, 'Vault ID is required'), + filter: z.string().nullish(), +}) + +export async function POST(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + logger.warn(`[${requestId}] Unauthorized 1Password list-items attempt`) + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + try { + const body = await request.json() + const params = ListItemsSchema.parse(body) + const creds = resolveCredentials(params) + + logger.info(`[${requestId}] Listing items in vault ${params.vaultId} (${creds.mode} mode)`) + + if (creds.mode === 'service_account') { + const client = await createOnePasswordClient(creds.serviceAccountToken!) + const items = await client.items.list(params.vaultId) + const normalized = items.map(normalizeSdkItemOverview) + + if (params.filter) { + const filterLower = params.filter.toLowerCase() + const filtered = normalized.filter( + (item) => + item.title?.toLowerCase().includes(filterLower) || + item.id?.toLowerCase().includes(filterLower) + ) + return NextResponse.json(filtered) + } + + return NextResponse.json(normalized) + } + + const query = params.filter ? `filter=${encodeURIComponent(params.filter)}` : undefined + const response = await connectRequest({ + serverUrl: creds.serverUrl!, + apiKey: creds.apiKey!, + path: `/v1/vaults/${params.vaultId}/items`, + method: 'GET', + query, + }) + + const data = await response.json() + if (!response.ok) { + return NextResponse.json( + { error: data.message || 'Failed to list items' }, + { status: response.status } + ) + } + + return NextResponse.json(data) + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + const message = error instanceof Error ? error.message : 'Unknown error' + logger.error(`[${requestId}] List items failed:`, error) + return NextResponse.json({ error: `Failed to list items: ${message}` }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/onepassword/list-vaults/route.ts b/apps/sim/app/api/tools/onepassword/list-vaults/route.ts new file mode 100644 index 000000000..d1b08e781 --- /dev/null +++ b/apps/sim/app/api/tools/onepassword/list-vaults/route.ts @@ -0,0 +1,85 @@ +import { randomUUID } from 'crypto' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { + connectRequest, + createOnePasswordClient, + normalizeSdkVault, + resolveCredentials, +} from '../utils' + +const logger = createLogger('OnePasswordListVaultsAPI') + +const ListVaultsSchema = z.object({ + connectionMode: z.enum(['service_account', 'connect']).nullish(), + serviceAccountToken: z.string().nullish(), + serverUrl: z.string().nullish(), + apiKey: z.string().nullish(), + filter: z.string().nullish(), +}) + +export async function POST(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + logger.warn(`[${requestId}] Unauthorized 1Password list-vaults attempt`) + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + try { + const body = await request.json() + const params = ListVaultsSchema.parse(body) + const creds = resolveCredentials(params) + + logger.info(`[${requestId}] Listing 1Password vaults (${creds.mode} mode)`) + + if (creds.mode === 'service_account') { + const client = await createOnePasswordClient(creds.serviceAccountToken!) + const vaults = await client.vaults.list() + const normalized = vaults.map(normalizeSdkVault) + + if (params.filter) { + const filterLower = params.filter.toLowerCase() + const filtered = normalized.filter( + (v) => + v.name?.toLowerCase().includes(filterLower) || v.id?.toLowerCase().includes(filterLower) + ) + return NextResponse.json(filtered) + } + + return NextResponse.json(normalized) + } + + const query = params.filter ? `filter=${encodeURIComponent(params.filter)}` : undefined + const response = await connectRequest({ + serverUrl: creds.serverUrl!, + apiKey: creds.apiKey!, + path: '/v1/vaults', + method: 'GET', + query, + }) + + const data = await response.json() + if (!response.ok) { + return NextResponse.json( + { error: data.message || 'Failed to list vaults' }, + { status: response.status } + ) + } + + return NextResponse.json(data) + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + const message = error instanceof Error ? error.message : 'Unknown error' + logger.error(`[${requestId}] List vaults failed:`, error) + return NextResponse.json({ error: `Failed to list vaults: ${message}` }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/onepassword/replace-item/route.ts b/apps/sim/app/api/tools/onepassword/replace-item/route.ts new file mode 100644 index 000000000..1dc735450 --- /dev/null +++ b/apps/sim/app/api/tools/onepassword/replace-item/route.ts @@ -0,0 +1,112 @@ +import { randomUUID } from 'crypto' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { + connectRequest, + createOnePasswordClient, + normalizeSdkItem, + resolveCredentials, + toSdkCategory, + toSdkFieldType, +} from '../utils' + +const logger = createLogger('OnePasswordReplaceItemAPI') + +const ReplaceItemSchema = z.object({ + connectionMode: z.enum(['service_account', 'connect']).nullish(), + serviceAccountToken: z.string().nullish(), + serverUrl: z.string().nullish(), + apiKey: z.string().nullish(), + vaultId: z.string().min(1, 'Vault ID is required'), + itemId: z.string().min(1, 'Item ID is required'), + item: z.string().min(1, 'Item JSON is required'), +}) + +export async function POST(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + logger.warn(`[${requestId}] Unauthorized 1Password replace-item attempt`) + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + try { + const body = await request.json() + const params = ReplaceItemSchema.parse(body) + const creds = resolveCredentials(params) + const itemData = JSON.parse(params.item) + + logger.info( + `[${requestId}] Replacing item ${params.itemId} in vault ${params.vaultId} (${creds.mode} mode)` + ) + + if (creds.mode === 'service_account') { + const client = await createOnePasswordClient(creds.serviceAccountToken!) + + const sdkItem = { + id: params.itemId, + title: itemData.title || '', + category: toSdkCategory(itemData.category || 'LOGIN'), + vaultId: params.vaultId, + fields: (itemData.fields ?? []).map((f: Record) => ({ + id: f.id || '', + title: f.label || f.title || '', + fieldType: toSdkFieldType(f.type || 'STRING'), + value: f.value || '', + sectionId: f.section?.id ?? f.sectionId, + })), + sections: (itemData.sections ?? []).map((s: Record) => ({ + id: s.id || '', + title: s.label || s.title || '', + })), + notes: itemData.notes || '', + tags: itemData.tags ?? [], + websites: (itemData.urls ?? itemData.websites ?? []).map((u: Record) => ({ + url: u.href || u.url || '', + label: u.label || '', + autofillBehavior: 'AnywhereOnWebsite' as const, + })), + version: itemData.version ?? 0, + files: [], + createdAt: new Date(), + updatedAt: new Date(), + } + + // Cast to any because toSdkCategory/toSdkFieldType return string literals + // that match SDK enum values but TypeScript can't verify this at compile time + const result = await client.items.put(sdkItem as any) + return NextResponse.json(normalizeSdkItem(result)) + } + + const response = await connectRequest({ + serverUrl: creds.serverUrl!, + apiKey: creds.apiKey!, + path: `/v1/vaults/${params.vaultId}/items/${params.itemId}`, + method: 'PUT', + body: itemData, + }) + + const data = await response.json() + if (!response.ok) { + return NextResponse.json( + { error: data.message || 'Failed to replace item' }, + { status: response.status } + ) + } + + return NextResponse.json(data) + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + const message = error instanceof Error ? error.message : 'Unknown error' + logger.error(`[${requestId}] Replace item failed:`, error) + return NextResponse.json({ error: `Failed to replace item: ${message}` }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/onepassword/resolve-secret/route.ts b/apps/sim/app/api/tools/onepassword/resolve-secret/route.ts new file mode 100644 index 000000000..8359416fc --- /dev/null +++ b/apps/sim/app/api/tools/onepassword/resolve-secret/route.ts @@ -0,0 +1,59 @@ +import { randomUUID } from 'crypto' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { createOnePasswordClient, resolveCredentials } from '../utils' + +const logger = createLogger('OnePasswordResolveSecretAPI') + +const ResolveSecretSchema = z.object({ + connectionMode: z.enum(['service_account', 'connect']).nullish(), + serviceAccountToken: z.string().nullish(), + serverUrl: z.string().nullish(), + apiKey: z.string().nullish(), + secretReference: z.string().min(1, 'Secret reference is required'), +}) + +export async function POST(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + logger.warn(`[${requestId}] Unauthorized 1Password resolve-secret attempt`) + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + try { + const body = await request.json() + const params = ResolveSecretSchema.parse(body) + const creds = resolveCredentials(params) + + if (creds.mode !== 'service_account') { + return NextResponse.json( + { error: 'Resolve Secret is only available in Service Account mode' }, + { status: 400 } + ) + } + + logger.info(`[${requestId}] Resolving secret reference (service_account mode)`) + + const client = await createOnePasswordClient(creds.serviceAccountToken!) + const secret = await client.secrets.resolve(params.secretReference) + + return NextResponse.json({ + secret, + reference: params.secretReference, + }) + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + const message = error instanceof Error ? error.message : 'Unknown error' + logger.error(`[${requestId}] Resolve secret failed:`, error) + return NextResponse.json({ error: `Failed to resolve secret: ${message}` }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/onepassword/update-item/route.ts b/apps/sim/app/api/tools/onepassword/update-item/route.ts new file mode 100644 index 000000000..5482dd6b9 --- /dev/null +++ b/apps/sim/app/api/tools/onepassword/update-item/route.ts @@ -0,0 +1,135 @@ +import { randomUUID } from 'crypto' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { + connectRequest, + createOnePasswordClient, + normalizeSdkItem, + resolveCredentials, +} from '../utils' + +const logger = createLogger('OnePasswordUpdateItemAPI') + +const UpdateItemSchema = z.object({ + connectionMode: z.enum(['service_account', 'connect']).nullish(), + serviceAccountToken: z.string().nullish(), + serverUrl: z.string().nullish(), + apiKey: z.string().nullish(), + vaultId: z.string().min(1, 'Vault ID is required'), + itemId: z.string().min(1, 'Item ID is required'), + operations: z.string().min(1, 'Patch operations are required'), +}) + +export async function POST(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + logger.warn(`[${requestId}] Unauthorized 1Password update-item attempt`) + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + try { + const body = await request.json() + const params = UpdateItemSchema.parse(body) + const creds = resolveCredentials(params) + const ops = JSON.parse(params.operations) as Array<{ + op: string + path: string + value?: unknown + }> + + logger.info( + `[${requestId}] Updating item ${params.itemId} in vault ${params.vaultId} (${creds.mode} mode)` + ) + + if (creds.mode === 'service_account') { + const client = await createOnePasswordClient(creds.serviceAccountToken!) + + // SDK doesn't support PATCH — fetch, apply patches, then put + const item = await client.items.get(params.vaultId, params.itemId) + + for (const op of ops) { + applyPatch(item, op) + } + + const result = await client.items.put(item) + return NextResponse.json(normalizeSdkItem(result)) + } + + const response = await connectRequest({ + serverUrl: creds.serverUrl!, + apiKey: creds.apiKey!, + path: `/v1/vaults/${params.vaultId}/items/${params.itemId}`, + method: 'PATCH', + body: ops, + }) + + const data = await response.json() + if (!response.ok) { + return NextResponse.json( + { error: data.message || 'Failed to update item' }, + { status: response.status } + ) + } + + return NextResponse.json(data) + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + const message = error instanceof Error ? error.message : 'Unknown error' + logger.error(`[${requestId}] Update item failed:`, error) + return NextResponse.json({ error: `Failed to update item: ${message}` }, { status: 500 }) + } +} + +/** Apply a single RFC6902 JSON Patch operation to a mutable SDK Item. */ +function applyPatch(item: Record, op: { op: string; path: string; value?: unknown }) { + const segments = op.path.split('/').filter(Boolean) + + if (segments.length === 1) { + const key = segments[0] + if (op.op === 'replace' || op.op === 'add') { + item[key] = op.value + } else if (op.op === 'remove') { + delete item[key] + } + return + } + + // Navigate to parent of target + let target = item + for (let i = 0; i < segments.length - 1; i++) { + const seg = segments[i] + if (Array.isArray(target)) { + target = target[Number(seg)] + } else { + target = target[seg] + } + if (target === undefined || target === null) return + } + + const lastSeg = segments[segments.length - 1] + + if (op.op === 'replace' || op.op === 'add') { + if (Array.isArray(target) && lastSeg === '-') { + target.push(op.value) + } else if (Array.isArray(target)) { + target[Number(lastSeg)] = op.value + } else { + target[lastSeg] = op.value + } + } else if (op.op === 'remove') { + if (Array.isArray(target)) { + target.splice(Number(lastSeg), 1) + } else { + delete target[lastSeg] + } + } +} diff --git a/apps/sim/app/api/tools/onepassword/utils.ts b/apps/sim/app/api/tools/onepassword/utils.ts new file mode 100644 index 000000000..fbf6042b7 --- /dev/null +++ b/apps/sim/app/api/tools/onepassword/utils.ts @@ -0,0 +1,262 @@ +import { createLogger } from '@sim/logger' + +const logger = createLogger('OnePasswordRouteUtils') + +/** + * SDK field type string values → Connect field type mapping. + * Uses string literals instead of enum imports to avoid loading the WASM module at build time. + */ +const SDK_TO_CONNECT_FIELD_TYPE: Record = { + Text: 'STRING', + Concealed: 'CONCEALED', + Email: 'EMAIL', + Url: 'URL', + Totp: 'OTP', + Phone: 'PHONE', + Date: 'DATE', + MonthYear: 'MONTH_YEAR', + Menu: 'MENU', + Address: 'ADDRESS', + Reference: 'REFERENCE', + SshKey: 'SSHKEY', + CreditCardNumber: 'CREDIT_CARD_NUMBER', + CreditCardType: 'CREDIT_CARD_TYPE', +} + +/** SDK category string values → Connect category mapping. */ +const SDK_TO_CONNECT_CATEGORY: Record = { + Login: 'LOGIN', + Password: 'PASSWORD', + ApiCredentials: 'API_CREDENTIAL', + SecureNote: 'SECURE_NOTE', + Server: 'SERVER', + Database: 'DATABASE', + CreditCard: 'CREDIT_CARD', + Identity: 'IDENTITY', + SshKey: 'SSH_KEY', + Document: 'DOCUMENT', + SoftwareLicense: 'SOFTWARE_LICENSE', + Email: 'EMAIL_ACCOUNT', + Membership: 'MEMBERSHIP', + Passport: 'PASSPORT', + Rewards: 'REWARD_PROGRAM', + DriverLicense: 'DRIVER_LICENSE', + BankAccount: 'BANK_ACCOUNT', + MedicalRecord: 'MEDICAL_RECORD', + OutdoorLicense: 'OUTDOOR_LICENSE', + Router: 'WIRELESS_ROUTER', + SocialSecurityNumber: 'SOCIAL_SECURITY_NUMBER', + CryptoWallet: 'CUSTOM', + Person: 'CUSTOM', + Unsupported: 'CUSTOM', +} + +/** Connect category → SDK category string mapping. */ +const CONNECT_TO_SDK_CATEGORY: Record = { + LOGIN: 'Login', + PASSWORD: 'Password', + API_CREDENTIAL: 'ApiCredentials', + SECURE_NOTE: 'SecureNote', + SERVER: 'Server', + DATABASE: 'Database', + CREDIT_CARD: 'CreditCard', + IDENTITY: 'Identity', + SSH_KEY: 'SshKey', + DOCUMENT: 'Document', + SOFTWARE_LICENSE: 'SoftwareLicense', + EMAIL_ACCOUNT: 'Email', + MEMBERSHIP: 'Membership', + PASSPORT: 'Passport', + REWARD_PROGRAM: 'Rewards', + DRIVER_LICENSE: 'DriverLicense', + BANK_ACCOUNT: 'BankAccount', + MEDICAL_RECORD: 'MedicalRecord', + OUTDOOR_LICENSE: 'OutdoorLicense', + WIRELESS_ROUTER: 'Router', + SOCIAL_SECURITY_NUMBER: 'SocialSecurityNumber', +} + +/** Connect field type → SDK field type string mapping. */ +const CONNECT_TO_SDK_FIELD_TYPE: Record = { + STRING: 'Text', + CONCEALED: 'Concealed', + EMAIL: 'Email', + URL: 'Url', + OTP: 'Totp', + TOTP: 'Totp', + PHONE: 'Phone', + DATE: 'Date', + MONTH_YEAR: 'MonthYear', + MENU: 'Menu', + ADDRESS: 'Address', + REFERENCE: 'Reference', + SSHKEY: 'SshKey', + CREDIT_CARD_NUMBER: 'CreditCardNumber', + CREDIT_CARD_TYPE: 'CreditCardType', +} + +export type ConnectionMode = 'service_account' | 'connect' + +export interface CredentialParams { + connectionMode?: ConnectionMode + serviceAccountToken?: string + serverUrl?: string + apiKey?: string +} + +export interface ResolvedCredentials { + mode: ConnectionMode + serviceAccountToken?: string + serverUrl?: string + apiKey?: string +} + +/** Determine which backend to use based on provided credentials. */ +export function resolveCredentials(params: CredentialParams): ResolvedCredentials { + const mode = params.connectionMode ?? (params.serviceAccountToken ? 'service_account' : 'connect') + + if (mode === 'service_account') { + if (!params.serviceAccountToken) { + throw new Error('Service Account token is required for Service Account mode') + } + return { mode, serviceAccountToken: params.serviceAccountToken } + } + + if (!params.serverUrl || !params.apiKey) { + throw new Error('Server URL and Connect token are required for Connect Server mode') + } + return { mode, serverUrl: params.serverUrl, apiKey: params.apiKey } +} + +/** + * Create a 1Password SDK client from a service account token. + * Uses dynamic import to avoid loading the WASM module at build time. + */ +export async function createOnePasswordClient(serviceAccountToken: string) { + const { createClient } = await import('@1password/sdk') + return createClient({ + auth: serviceAccountToken, + integrationName: 'Sim Studio', + integrationVersion: '1.0.0', + }) +} + +/** Proxy a request to the 1Password Connect Server. */ +export async function connectRequest(options: { + serverUrl: string + apiKey: string + path: string + method: string + body?: unknown + query?: string +}): Promise { + const base = options.serverUrl.replace(/\/$/, '') + const queryStr = options.query ? `?${options.query}` : '' + const url = `${base}${options.path}${queryStr}` + + const headers: Record = { + Authorization: `Bearer ${options.apiKey}`, + } + + if (options.body) { + headers['Content-Type'] = 'application/json' + } + + logger.info(`Connect request: ${options.method} ${options.path}`) + + return fetch(url, { + method: options.method, + headers, + body: options.body ? JSON.stringify(options.body) : undefined, + }) +} + +/** Normalize an SDK VaultOverview to match Connect API vault shape. */ +export function normalizeSdkVault(vault: Record) { + return { + id: vault.id, + name: vault.title, + description: null, + attributeVersion: 0, + contentVersion: 0, + items: 0, + type: 'USER_CREATED', + createdAt: + vault.createdAt instanceof Date ? vault.createdAt.toISOString() : (vault.createdAt ?? null), + updatedAt: + vault.updatedAt instanceof Date ? vault.updatedAt.toISOString() : (vault.updatedAt ?? null), + } +} + +/** Normalize an SDK ItemOverview to match Connect API item summary shape. */ +export function normalizeSdkItemOverview(item: Record) { + return { + id: item.id, + title: item.title, + vault: { id: item.vaultId }, + category: SDK_TO_CONNECT_CATEGORY[item.category] ?? 'CUSTOM', + urls: (item.websites ?? []).map((w: Record) => ({ + href: w.url, + label: w.label ?? null, + primary: false, + })), + favorite: false, + tags: item.tags ?? [], + version: 0, + state: item.state === 'archived' ? 'ARCHIVED' : null, + createdAt: + item.createdAt instanceof Date ? item.createdAt.toISOString() : (item.createdAt ?? null), + updatedAt: + item.updatedAt instanceof Date ? item.updatedAt.toISOString() : (item.updatedAt ?? null), + lastEditedBy: null, + } +} + +/** Normalize a full SDK Item to match Connect API FullItem shape. */ +export function normalizeSdkItem(item: Record) { + return { + id: item.id, + title: item.title, + vault: { id: item.vaultId }, + category: SDK_TO_CONNECT_CATEGORY[item.category] ?? 'CUSTOM', + urls: (item.websites ?? []).map((w: Record) => ({ + href: w.url, + label: w.label ?? null, + primary: false, + })), + favorite: false, + tags: item.tags ?? [], + version: item.version ?? 0, + state: null, + fields: (item.fields ?? []).map((field: Record) => ({ + id: field.id, + label: field.title, + type: SDK_TO_CONNECT_FIELD_TYPE[field.fieldType] ?? 'STRING', + purpose: '', + value: field.value ?? null, + section: field.sectionId ? { id: field.sectionId } : null, + generate: false, + recipe: null, + entropy: null, + })), + sections: (item.sections ?? []).map((section: Record) => ({ + id: section.id, + label: section.title, + })), + createdAt: + item.createdAt instanceof Date ? item.createdAt.toISOString() : (item.createdAt ?? null), + updatedAt: + item.updatedAt instanceof Date ? item.updatedAt.toISOString() : (item.updatedAt ?? null), + lastEditedBy: null, + } +} + +/** Convert a Connect-style category string to the SDK category string. */ +export function toSdkCategory(category: string): string { + return CONNECT_TO_SDK_CATEGORY[category] ?? 'Login' +} + +/** Convert a Connect-style field type string to the SDK field type string. */ +export function toSdkFieldType(type: string): string { + return CONNECT_TO_SDK_FIELD_TYPE[type] ?? 'Text' +} diff --git a/apps/sim/blocks/blocks/jira.ts b/apps/sim/blocks/blocks/jira.ts index 2e2de820b..60356a728 100644 --- a/apps/sim/blocks/blocks/jira.ts +++ b/apps/sim/blocks/blocks/jira.ts @@ -704,12 +704,10 @@ Return ONLY the comment text - no explanations.`, 'jira_update', 'jira_write', 'jira_bulk_read', - 'jira_bulk_read_v2', 'jira_delete_issue', 'jira_assign_issue', 'jira_transition_issue', 'jira_search_issues', - 'jira_search_issues_v2', 'jira_add_comment', 'jira_get_comments', 'jira_update_comment', @@ -737,7 +735,7 @@ Return ONLY the comment text - no explanations.`, case 'read': // If a project is selected but no issue is chosen, route to bulk read if (effectiveProjectId && !effectiveIssueKey) { - return 'jira_bulk_read_v2' + return 'jira_bulk_read' } return 'jira_retrieve' case 'update': @@ -745,7 +743,7 @@ Return ONLY the comment text - no explanations.`, case 'write': return 'jira_write' case 'read-bulk': - return 'jira_bulk_read_v2' + return 'jira_bulk_read' case 'delete': return 'jira_delete_issue' case 'assign': @@ -753,7 +751,7 @@ Return ONLY the comment text - no explanations.`, case 'transition': return 'jira_transition_issue' case 'search': - return 'jira_search_issues_v2' + return 'jira_search_issues' case 'add_comment': return 'jira_add_comment' case 'get_comments': @@ -1140,7 +1138,7 @@ Return ONLY the comment text - no explanations.`, id: { type: 'string', description: 'Jira issue ID' }, key: { type: 'string', description: 'Jira issue key' }, - // jira_search_issues_v2 / jira_bulk_read_v2 outputs + // jira_search_issues / jira_bulk_read outputs total: { type: 'number', description: 'Total number of matching issues' }, nextPageToken: { type: 'string', description: 'Cursor token for the next page of results' }, isLast: { type: 'boolean', description: 'Whether this is the last page of results' }, diff --git a/apps/sim/blocks/blocks/onepassword.ts b/apps/sim/blocks/blocks/onepassword.ts index a5200f7ee..97687a378 100644 --- a/apps/sim/blocks/blocks/onepassword.ts +++ b/apps/sim/blocks/blocks/onepassword.ts @@ -6,7 +6,7 @@ export const OnePasswordBlock: BlockConfig = { name: '1Password', description: 'Manage secrets and items in 1Password vaults', longDescription: - 'Access and manage secrets stored in 1Password vaults using the Connect API. List vaults, retrieve items with their fields and secrets, create new items, update existing ones, and delete items.', + 'Access and manage secrets stored in 1Password vaults using the Connect API or Service Account SDK. List vaults, retrieve items with their fields and secrets, create new items, update existing ones, delete items, and resolve secret references.', docsLink: 'https://docs.sim.ai/tools/onepassword', category: 'tools', bgColor: '#E0E0E0', @@ -27,15 +27,36 @@ export const OnePasswordBlock: BlockConfig = { { label: 'Replace Item', id: 'replace_item' }, { label: 'Update Item', id: 'update_item' }, { label: 'Delete Item', id: 'delete_item' }, + { label: 'Resolve Secret', id: 'resolve_secret' }, ], value: () => 'get_item', }, + { + id: 'connectionMode', + title: 'Connection Mode', + type: 'dropdown', + options: [ + { label: 'Service Account', id: 'service_account' }, + { label: 'Connect Server', id: 'connect' }, + ], + value: () => 'service_account', + }, + { + id: 'serviceAccountToken', + title: 'Service Account Token', + type: 'short-input', + placeholder: 'Enter your 1Password Service Account token', + password: true, + required: { field: 'connectionMode', value: 'service_account' }, + condition: { field: 'connectionMode', value: 'service_account' }, + }, { id: 'serverUrl', title: 'Server URL', type: 'short-input', placeholder: 'http://localhost:8080', - required: true, + required: { field: 'connectionMode', value: 'connect' }, + condition: { field: 'connectionMode', value: 'connect' }, }, { id: 'apiKey', @@ -43,7 +64,16 @@ export const OnePasswordBlock: BlockConfig = { type: 'short-input', placeholder: 'Enter your 1Password Connect token', password: true, - required: true, + required: { field: 'connectionMode', value: 'connect' }, + condition: { field: 'connectionMode', value: 'connect' }, + }, + { + id: 'secretReference', + title: 'Secret Reference', + type: 'short-input', + placeholder: 'op://vault-name/item-name/field-name', + required: { field: 'operation', value: 'resolve_secret' }, + condition: { field: 'operation', value: 'resolve_secret' }, }, { id: 'vaultId', @@ -62,7 +92,11 @@ export const OnePasswordBlock: BlockConfig = { 'delete_item', ], }, - condition: { field: 'operation', value: 'list_vaults', not: true }, + condition: { + field: 'operation', + value: ['list_vaults', 'resolve_secret'], + not: true, + }, }, { id: 'itemId', @@ -187,6 +221,7 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`, 'onepassword_replace_item', 'onepassword_update_item', 'onepassword_delete_item', + 'onepassword_resolve_secret', ], config: { tool: (params) => `onepassword_${params.operation}`, @@ -195,8 +230,11 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`, inputs: { operation: { type: 'string', description: 'Operation to perform' }, + connectionMode: { type: 'string', description: 'Connection mode: service_account or connect' }, + serviceAccountToken: { type: 'string', description: '1Password Service Account token' }, serverUrl: { type: 'string', description: '1Password Connect server URL' }, apiKey: { type: 'string', description: '1Password Connect token' }, + secretReference: { type: 'string', description: 'Secret reference URI (op://...)' }, vaultId: { type: 'string', description: 'Vault UUID' }, itemId: { type: 'string', description: 'Item UUID' }, filter: { type: 'string', description: 'SCIM filter expression' }, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 9ffc410cb..f13fc8aa8 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -33,21 +33,21 @@ export function AgentIcon(props: SVGProps) { > ) { > ) { @@ -113,7 +113,7 @@ export function NoteIcon(props: SVGProps) { width='16' height='18' rx='2.5' - stroke='#000000' + stroke='currentColor' strokeWidth='1.5' fill='none' /> @@ -139,7 +139,7 @@ export function WorkflowIcon(props: SVGProps) { cx='12' cy='6' r='3' - stroke='#000000' + stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' strokeLinejoin='round' @@ -151,7 +151,7 @@ export function WorkflowIcon(props: SVGProps) { width='8' x='2' y='16' - stroke='#000000' + stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' strokeLinejoin='round' @@ -163,7 +163,7 @@ export function WorkflowIcon(props: SVGProps) { width='8' x='14' y='16' - stroke='#000000' + stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' strokeLinejoin='round' @@ -171,7 +171,7 @@ export function WorkflowIcon(props: SVGProps) { ) { x2='12' y1='9' y2='12' - stroke='#000000' + stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' strokeLinejoin='round' @@ -203,7 +203,7 @@ export function SignalIcon(props: SVGProps) { > ) { > ) { > ) { > ) { > ) { width='18' height='16' rx='2' - stroke='#000000' + stroke='currentColor' strokeWidth='2' strokeLinecap='round' strokeLinejoin='round' /> - + @@ -350,7 +358,7 @@ export function CodeIcon(props: SVGProps) { > ) { @@ -1491,14 +1499,14 @@ export function DocumentIcon(props: SVGProps) { > @@ -3993,7 +4001,7 @@ export function SmtpIcon(props: SVGProps) { > ) { @@ -4489,14 +4497,14 @@ export function RssIcon(props: SVGProps) { > 0) queryParams.set('startAt', String(startAt)) - - const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/search/jql?${queryParams.toString()}` - const pageResponse = await fetch(url, { - method: 'GET', - headers: { - Authorization: `Bearer ${params!.accessToken}`, - Accept: 'application/json', - }, - }) - - const pageData = await pageResponse.json() - const issues = pageData.issues || [] - total = pageData.total || issues.length - collected = collected.concat(issues) - - if (collected.length >= Math.min(total, MAX_TOTAL) || issues.length === 0) break - startAt += PAGE_SIZE - } - - return { - success: true, - output: { - ts: new Date().toISOString(), - total, - issues: collected.slice(0, MAX_TOTAL).map((issue: any) => ({ - id: issue.id ?? '', - key: issue.key ?? '', - self: issue.self ?? '', - summary: issue.fields?.summary ?? '', - description: extractAdfText(issue.fields?.description), - status: { - id: issue.fields?.status?.id ?? '', - name: issue.fields?.status?.name ?? '', - }, - issuetype: { - id: issue.fields?.issuetype?.id ?? '', - name: issue.fields?.issuetype?.name ?? '', - }, - priority: issue.fields?.priority - ? { id: issue.fields.priority.id ?? '', name: issue.fields.priority.name ?? '' } - : null, - assignee: issue.fields?.assignee - ? { - accountId: issue.fields.assignee.accountId ?? '', - displayName: issue.fields.assignee.displayName ?? '', - } - : null, - created: issue.fields?.created ?? '', - updated: issue.fields?.updated ?? '', - })), - }, - } - }, - - outputs: { - ts: TIMESTAMP_OUTPUT, - total: { type: 'number', description: 'Total number of issues in the project' }, - issues: { - type: 'array', - description: 'Array of Jira issues', - items: { - type: 'object', - properties: { - id: { type: 'string', description: 'Issue ID' }, - key: { type: 'string', description: 'Issue key (e.g., PROJ-123)' }, - self: { type: 'string', description: 'REST API URL for this issue' }, - summary: { type: 'string', description: 'Issue summary' }, - description: { type: 'string', description: 'Issue description text', optional: true }, - status: { - type: 'object', - description: 'Issue status', - properties: { - id: { type: 'string', description: 'Status ID' }, - name: { type: 'string', description: 'Status name' }, - }, - }, - issuetype: { - type: 'object', - description: 'Issue type', - properties: { - id: { type: 'string', description: 'Issue type ID' }, - name: { type: 'string', description: 'Issue type name' }, - }, - }, - priority: { - type: 'object', - description: 'Issue priority', - properties: { - id: { type: 'string', description: 'Priority ID' }, - name: { type: 'string', description: 'Priority name' }, - }, - optional: true, - }, - assignee: { - type: 'object', - description: 'Assigned user', - properties: { - accountId: { type: 'string', description: 'Atlassian account ID' }, - displayName: { type: 'string', description: 'Display name' }, - }, - optional: true, - }, - created: { type: 'string', description: 'ISO 8601 creation timestamp' }, - updated: { type: 'string', description: 'ISO 8601 last updated timestamp' }, - }, - }, - }, - }, -} - -/** - * V2 Bulk Read Tool - Uses cursor-based pagination (nextPageToken) on /rest/api/3/search/jql. - * The startAt parameter was deprecated on this endpoint as of Sept 2025. - */ -export const jiraBulkRetrieveV2Tool: ToolConfig< - JiraRetrieveBulkV2Params, - JiraRetrieveResponseBulkV2 -> = { - id: 'jira_bulk_read_v2', - name: 'Jira Bulk Read V2', - description: - 'Retrieve multiple Jira issues from a project in bulk with cursor-based pagination (V2 - uses nextPageToken)', - version: '2.0.0', - - oauth: { - required: true, - provider: 'jira', - }, - - params: { - accessToken: { - type: 'string', - required: true, - visibility: 'hidden', - description: 'OAuth access token for Jira', - }, - domain: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', - }, - projectId: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'Jira project key (e.g., PROJ)', - }, - cloudId: { - type: 'string', - required: false, - visibility: 'hidden', - description: - 'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.', - }, - }, - - request: { - url: () => 'https://api.atlassian.com/oauth/token/accessible-resources', - method: 'GET', - headers: (params: JiraRetrieveBulkV2Params) => ({ - Authorization: `Bearer ${params.accessToken}`, - Accept: 'application/json', - }), - }, - - transformResponse: async (response: Response, params?: JiraRetrieveBulkV2Params) => { - const MAX_TOTAL = 1000 - const PAGE_SIZE = 100 - - const resolveProjectKey = async (cloudId: string, accessToken: string, ref: string) => { - const refTrimmed = (ref || '').trim() - if (!refTrimmed) return refTrimmed - const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/project/${encodeURIComponent(refTrimmed)}` - const resp = await fetch(url, { - method: 'GET', - headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' }, - }) - if (!resp.ok) return refTrimmed - const project = await resp.json() - return project?.key || refTrimmed - } - - const resolveCloudId = async () => { - if (params?.cloudId) return params.cloudId - const accessibleResources = await response.json() - const normalizedInput = `https://${params?.domain}`.toLowerCase() - const matchedResource = accessibleResources.find( - (r: { url: string }) => r.url.toLowerCase() === normalizedInput - ) - if (matchedResource) return matchedResource.id - if (Array.isArray(accessibleResources) && accessibleResources.length > 0) - return accessibleResources[0].id - throw new Error('No Jira resources found') - } - - const cloudId = await resolveCloudId() - const projectKey = await resolveProjectKey(cloudId, params!.accessToken, params!.projectId) - const jql = `project = ${projectKey} ORDER BY updated DESC` - - let collected: Array> = [] let nextPageToken: string | undefined let total: number | null = null @@ -344,51 +128,46 @@ export const jiraBulkRetrieveV2Tool: ToolConfig< success: true, output: { ts: new Date().toISOString(), - issues: collected.slice(0, MAX_TOTAL).map((issue: Record) => { - const fields = (issue.fields as Record) ?? {} - const status = fields.status as Record | undefined - const issuetype = fields.issuetype as Record | undefined - const priority = fields.priority as Record | undefined - const assignee = fields.assignee as Record | undefined - return { - id: (issue.id as string) ?? '', - key: (issue.key as string) ?? '', - self: (issue.self as string) ?? '', - summary: (fields.summary as string) ?? '', - description: extractAdfText(fields.description), - status: { - id: (status?.id as string) ?? '', - name: (status?.name as string) ?? '', - }, - issuetype: { - id: (issuetype?.id as string) ?? '', - name: (issuetype?.name as string) ?? '', - }, - priority: priority - ? { - id: (priority.id as string) ?? '', - name: (priority.name as string) ?? '', - } - : null, - assignee: assignee - ? { - accountId: (assignee.accountId as string) ?? '', - displayName: (assignee.displayName as string) ?? '', - } - : null, - created: (fields.created as string) ?? '', - updated: (fields.updated as string) ?? '', - } - }), + total, + issues: collected.slice(0, MAX_TOTAL).map((issue: any) => ({ + id: issue.id ?? '', + key: issue.key ?? '', + self: issue.self ?? '', + summary: issue.fields?.summary ?? '', + description: extractAdfText(issue.fields?.description), + status: { + id: issue.fields?.status?.id ?? '', + name: issue.fields?.status?.name ?? '', + }, + issuetype: { + id: issue.fields?.issuetype?.id ?? '', + name: issue.fields?.issuetype?.name ?? '', + }, + priority: issue.fields?.priority + ? { id: issue.fields.priority.id ?? '', name: issue.fields.priority.name ?? '' } + : null, + assignee: issue.fields?.assignee + ? { + accountId: issue.fields.assignee.accountId ?? '', + displayName: issue.fields.assignee.displayName ?? '', + } + : null, + created: issue.fields?.created ?? '', + updated: issue.fields?.updated ?? '', + })), nextPageToken: nextPageToken ?? null, isLast: !nextPageToken || collected.length >= MAX_TOTAL, - total, }, } }, outputs: { ts: TIMESTAMP_OUTPUT, + total: { + type: 'number', + description: 'Total number of issues in the project (may not always be available)', + optional: true, + }, issues: { type: 'array', description: 'Array of Jira issues', @@ -445,10 +224,5 @@ export const jiraBulkRetrieveV2Tool: ToolConfig< optional: true, }, isLast: { type: 'boolean', description: 'Whether this is the last page of results' }, - total: { - type: 'number', - description: 'Total number of issues in the project (may not always be available)', - optional: true, - }, }, } diff --git a/apps/sim/tools/jira/index.ts b/apps/sim/tools/jira/index.ts index f65037438..ced24d2d0 100644 --- a/apps/sim/tools/jira/index.ts +++ b/apps/sim/tools/jira/index.ts @@ -3,7 +3,7 @@ import { jiraAddCommentTool } from '@/tools/jira/add_comment' import { jiraAddWatcherTool } from '@/tools/jira/add_watcher' import { jiraAddWorklogTool } from '@/tools/jira/add_worklog' import { jiraAssignIssueTool } from '@/tools/jira/assign_issue' -import { jiraBulkRetrieveTool, jiraBulkRetrieveV2Tool } from '@/tools/jira/bulk_read' +import { jiraBulkRetrieveTool } from '@/tools/jira/bulk_read' import { jiraCreateIssueLinkTool } from '@/tools/jira/create_issue_link' import { jiraDeleteAttachmentTool } from '@/tools/jira/delete_attachment' import { jiraDeleteCommentTool } from '@/tools/jira/delete_comment' @@ -16,7 +16,7 @@ import { jiraGetUsersTool } from '@/tools/jira/get_users' import { jiraGetWorklogsTool } from '@/tools/jira/get_worklogs' import { jiraRemoveWatcherTool } from '@/tools/jira/remove_watcher' import { jiraRetrieveTool } from '@/tools/jira/retrieve' -import { jiraSearchIssuesTool, jiraSearchIssuesV2Tool } from '@/tools/jira/search_issues' +import { jiraSearchIssuesTool } from '@/tools/jira/search_issues' import { jiraTransitionIssueTool } from '@/tools/jira/transition_issue' import { jiraUpdateTool } from '@/tools/jira/update' import { jiraUpdateCommentTool } from '@/tools/jira/update_comment' @@ -28,12 +28,10 @@ export { jiraUpdateTool, jiraWriteTool, jiraBulkRetrieveTool, - jiraBulkRetrieveV2Tool, jiraDeleteIssueTool, jiraAssignIssueTool, jiraTransitionIssueTool, jiraSearchIssuesTool, - jiraSearchIssuesV2Tool, jiraAddCommentTool, jiraAddAttachmentTool, jiraGetCommentsTool, diff --git a/apps/sim/tools/jira/search_issues.ts b/apps/sim/tools/jira/search_issues.ts index 97c4898fb..95dd00d68 100644 --- a/apps/sim/tools/jira/search_issues.ts +++ b/apps/sim/tools/jira/search_issues.ts @@ -1,9 +1,4 @@ -import type { - JiraSearchIssuesParams, - JiraSearchIssuesResponse, - JiraSearchIssuesV2Params, - JiraSearchIssuesV2Response, -} from '@/tools/jira/types' +import type { JiraSearchIssuesParams, JiraSearchIssuesResponse } from '@/tools/jira/types' import { SEARCH_ISSUE_ITEM_PROPERTIES, TIMESTAMP_OUTPUT } from '@/tools/jira/types' import { extractAdfText, getJiraCloudId, transformUser } from '@/tools/jira/utils' import type { ToolConfig } from '@/tools/types' @@ -71,173 +66,6 @@ export const jiraSearchIssuesTool: ToolConfig { - if (params.cloudId) { - const query = new URLSearchParams() - if (params.jql) query.set('jql', params.jql) - if (typeof params.startAt === 'number') query.set('startAt', String(params.startAt)) - if (typeof params.maxResults === 'number') - query.set('maxResults', String(params.maxResults)) - if (Array.isArray(params.fields) && params.fields.length > 0) - query.set('fields', params.fields.join(',')) - const qs = query.toString() - return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/search/jql${qs ? `?${qs}` : ''}` - } - return 'https://api.atlassian.com/oauth/token/accessible-resources' - }, - method: () => 'GET', - headers: (params: JiraSearchIssuesParams) => { - return { - Accept: 'application/json', - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.accessToken}`, - } - }, - body: () => undefined as any, - }, - - transformResponse: async (response: Response, params?: JiraSearchIssuesParams) => { - const performSearch = async (cloudId: string) => { - const query = new URLSearchParams() - if (params?.jql) query.set('jql', params.jql) - if (typeof params?.startAt === 'number') query.set('startAt', String(params.startAt)) - if (typeof params?.maxResults === 'number') query.set('maxResults', String(params.maxResults)) - if (Array.isArray(params?.fields) && params.fields.length > 0) - query.set('fields', params.fields.join(',')) - const searchUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/search/jql?${query.toString()}` - const searchResponse = await fetch(searchUrl, { - method: 'GET', - headers: { - Accept: 'application/json', - Authorization: `Bearer ${params!.accessToken}`, - }, - }) - - if (!searchResponse.ok) { - let message = `Failed to search Jira issues (${searchResponse.status})` - try { - const err = await searchResponse.json() - message = err?.errorMessages?.join(', ') || err?.message || message - } catch (_e) {} - throw new Error(message) - } - - return searchResponse.json() - } - - let data: any - - if (!params?.cloudId) { - const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) - data = await performSearch(cloudId) - } else { - if (!response.ok) { - let message = `Failed to search Jira issues (${response.status})` - try { - const err = await response.json() - message = err?.errorMessages?.join(', ') || err?.message || message - } catch (_e) {} - throw new Error(message) - } - data = await response.json() - } - - return { - success: true, - output: { - ts: new Date().toISOString(), - total: data?.total ?? 0, - startAt: data?.startAt ?? 0, - maxResults: data?.maxResults ?? 0, - issues: (data?.issues ?? []).map(transformSearchIssue), - }, - } - }, - - outputs: { - ts: TIMESTAMP_OUTPUT, - total: { type: 'number', description: 'Total number of matching issues' }, - startAt: { type: 'number', description: 'Pagination start index' }, - maxResults: { type: 'number', description: 'Maximum results per page' }, - issues: { - type: 'array', - description: 'Array of matching issues', - items: { - type: 'object', - properties: SEARCH_ISSUE_ITEM_PROPERTIES, - }, - }, - }, -} - -/** - * V2 Search Issues Tool - Uses cursor-based pagination (nextPageToken) on /rest/api/3/search/jql. - * The startAt parameter was deprecated on this endpoint as of Sept 2025. - */ -export const jiraSearchIssuesV2Tool: ToolConfig< - JiraSearchIssuesV2Params, - JiraSearchIssuesV2Response -> = { - id: 'jira_search_issues_v2', - name: 'Jira Search Issues V2', - description: - 'Search for Jira issues using JQL with cursor-based pagination (V2 - uses nextPageToken)', - version: '2.0.0', - - oauth: { - required: true, - provider: 'jira', - }, - params: { accessToken: { type: 'string', @@ -287,7 +115,7 @@ export const jiraSearchIssuesV2Tool: ToolConfig< }, request: { - url: (params: JiraSearchIssuesV2Params) => { + url: (params: JiraSearchIssuesParams) => { if (params.cloudId) { const query = new URLSearchParams() if (params.jql) query.set('jql', params.jql) @@ -302,7 +130,7 @@ export const jiraSearchIssuesV2Tool: ToolConfig< return 'https://api.atlassian.com/oauth/token/accessible-resources' }, method: () => 'GET', - headers: (params: JiraSearchIssuesV2Params) => ({ + headers: (params: JiraSearchIssuesParams) => ({ Accept: 'application/json', 'Content-Type': 'application/json', Authorization: `Bearer ${params.accessToken}`, @@ -310,7 +138,7 @@ export const jiraSearchIssuesV2Tool: ToolConfig< body: () => undefined as any, }, - transformResponse: async (response: Response, params?: JiraSearchIssuesV2Params) => { + transformResponse: async (response: Response, params?: JiraSearchIssuesParams) => { const performSearch = async (cloudId: string) => { const query = new URLSearchParams() if (params?.jql) query.set('jql', params.jql) diff --git a/apps/sim/tools/jira/types.ts b/apps/sim/tools/jira/types.ts index 754226a2a..99deab503 100644 --- a/apps/sim/tools/jira/types.ts +++ b/apps/sim/tools/jira/types.ts @@ -873,7 +873,7 @@ export interface JiraRetrieveBulkParams { export interface JiraRetrieveResponseBulk extends ToolResponse { output: { ts: string - total: number + total: number | null issues: Array<{ id: string key: string @@ -887,6 +887,8 @@ export interface JiraRetrieveResponseBulk extends ToolResponse { created: string updated: string }> + nextPageToken: string | null + isLast: boolean } } @@ -1034,7 +1036,7 @@ export interface JiraSearchIssuesParams { accessToken: string domain: string jql: string - startAt?: number + nextPageToken?: string maxResults?: number fields?: string[] cloudId?: string @@ -1043,9 +1045,6 @@ export interface JiraSearchIssuesParams { export interface JiraSearchIssuesResponse extends ToolResponse { output: { ts: string - total: number - startAt: number - maxResults: number issues: Array<{ id: string key: string @@ -1069,74 +1068,6 @@ export interface JiraSearchIssuesResponse extends ToolResponse { created: string updated: string }> - } -} - -export interface JiraSearchIssuesV2Params { - accessToken: string - domain: string - jql: string - nextPageToken?: string - maxResults?: number - fields?: string[] - cloudId?: string -} - -export interface JiraSearchIssuesV2Response extends ToolResponse { - output: { - ts: string - issues: Array<{ - id: string - key: string - self: string - summary: string - description: string | null - status: { - id: string - name: string - statusCategory?: { id: number; key: string; name: string; colorName: string } - } - issuetype: { id: string; name: string; subtask: boolean } - project: { id: string; key: string; name: string } - priority: { id: string; name: string } | null - assignee: { accountId: string; displayName: string } | null - reporter: { accountId: string; displayName: string } | null - labels: string[] - components: Array<{ id: string; name: string }> - resolution: { id: string; name: string } | null - duedate: string | null - created: string - updated: string - }> - nextPageToken: string | null - isLast: boolean - total: number | null - } -} - -export interface JiraRetrieveBulkV2Params { - accessToken: string - domain: string - projectId: string - cloudId?: string -} - -export interface JiraRetrieveResponseBulkV2 extends ToolResponse { - output: { - ts: string - issues: Array<{ - id: string - key: string - self: string - summary: string - description: string | null - status: { id: string; name: string } - issuetype: { id: string; name: string } - priority: { id: string; name: string } | null - assignee: { accountId: string; displayName: string } | null - created: string - updated: string - }> nextPageToken: string | null isLast: boolean total: number | null diff --git a/apps/sim/tools/onepassword/create_item.ts b/apps/sim/tools/onepassword/create_item.ts index 9c4c4bef7..5f9b70a07 100644 --- a/apps/sim/tools/onepassword/create_item.ts +++ b/apps/sim/tools/onepassword/create_item.ts @@ -15,17 +15,28 @@ export const createItemTool: ToolConfig< version: '1.0.0', params: { + connectionMode: { + type: 'string', + required: false, + description: 'Connection mode: "service_account" or "connect"', + }, + serviceAccountToken: { + type: 'string', + required: false, + visibility: 'user-only', + description: '1Password Service Account token (for Service Account mode)', + }, apiKey: { type: 'string', - required: true, + required: false, visibility: 'user-only', - description: '1Password Connect API token', + description: '1Password Connect API token (for Connect Server mode)', }, serverUrl: { type: 'string', - required: true, + required: false, visibility: 'user-only', - description: '1Password Connect server URL (e.g., http://localhost:8080)', + description: '1Password Connect server URL (for Connect Server mode)', }, vaultId: { type: 'string', @@ -62,39 +73,27 @@ export const createItemTool: ToolConfig< }, request: { - url: (params) => { - const base = params.serverUrl.replace(/\/$/, '') - return `${base}/v1/vaults/${params.vaultId}/items` - }, + url: '/api/tools/onepassword/create-item', method: 'POST', - headers: (params) => ({ - Authorization: `Bearer ${params.apiKey}`, - 'Content-Type': 'application/json', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + connectionMode: params.connectionMode, + serviceAccountToken: params.serviceAccountToken, + serverUrl: params.serverUrl, + apiKey: params.apiKey, + vaultId: params.vaultId, + category: params.category, + title: params.title, + tags: params.tags, + fields: params.fields, }), - body: (params) => { - const body: Record = { - vault: { id: params.vaultId }, - category: params.category, - } - - if (params.title) { - body.title = params.title - } - - if (params.tags) { - body.tags = params.tags.split(',').map((t) => t.trim()) - } - - if (params.fields) { - body.fields = JSON.parse(params.fields) - } - - return body - }, }, transformResponse: async (response) => { const data = await response.json() + if (data.error) { + return { success: false, output: transformFullItem({}), error: data.error } + } return { success: true, output: transformFullItem(data), diff --git a/apps/sim/tools/onepassword/delete_item.ts b/apps/sim/tools/onepassword/delete_item.ts index ef6375b16..08990a19a 100644 --- a/apps/sim/tools/onepassword/delete_item.ts +++ b/apps/sim/tools/onepassword/delete_item.ts @@ -14,17 +14,28 @@ export const deleteItemTool: ToolConfig< version: '1.0.0', params: { + connectionMode: { + type: 'string', + required: false, + description: 'Connection mode: "service_account" or "connect"', + }, + serviceAccountToken: { + type: 'string', + required: false, + visibility: 'user-only', + description: '1Password Service Account token (for Service Account mode)', + }, apiKey: { type: 'string', - required: true, + required: false, visibility: 'user-only', - description: '1Password Connect API token', + description: '1Password Connect API token (for Connect Server mode)', }, serverUrl: { type: 'string', - required: true, + required: false, visibility: 'user-only', - description: '1Password Connect server URL (e.g., http://localhost:8080)', + description: '1Password Connect server URL (for Connect Server mode)', }, vaultId: { type: 'string', @@ -41,17 +52,24 @@ export const deleteItemTool: ToolConfig< }, request: { - url: (params) => { - const base = params.serverUrl.replace(/\/$/, '') - return `${base}/v1/vaults/${params.vaultId}/items/${params.itemId}` - }, - method: 'DELETE', - headers: (params) => ({ - Authorization: `Bearer ${params.apiKey}`, + url: '/api/tools/onepassword/delete-item', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + connectionMode: params.connectionMode, + serviceAccountToken: params.serviceAccountToken, + serverUrl: params.serverUrl, + apiKey: params.apiKey, + vaultId: params.vaultId, + itemId: params.itemId, }), }, - transformResponse: async () => { + transformResponse: async (response) => { + const data = await response.json() + if (data.error) { + return { success: false, output: { success: false }, error: data.error } + } return { success: true, output: { diff --git a/apps/sim/tools/onepassword/get_item.ts b/apps/sim/tools/onepassword/get_item.ts index 9834577da..8049d7260 100644 --- a/apps/sim/tools/onepassword/get_item.ts +++ b/apps/sim/tools/onepassword/get_item.ts @@ -12,17 +12,28 @@ export const getItemTool: ToolConfig { - const base = params.serverUrl.replace(/\/$/, '') - return `${base}/v1/vaults/${params.vaultId}/items/${params.itemId}` - }, - method: 'GET', - headers: (params) => ({ - Authorization: `Bearer ${params.apiKey}`, + url: '/api/tools/onepassword/get-item', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + connectionMode: params.connectionMode, + serviceAccountToken: params.serviceAccountToken, + serverUrl: params.serverUrl, + apiKey: params.apiKey, + vaultId: params.vaultId, + itemId: params.itemId, }), }, transformResponse: async (response) => { const data = await response.json() + if (data.error) { + return { success: false, output: transformFullItem({}), error: data.error } + } return { success: true, output: transformFullItem(data), diff --git a/apps/sim/tools/onepassword/get_vault.ts b/apps/sim/tools/onepassword/get_vault.ts index 126b39f7a..cf2b63a44 100644 --- a/apps/sim/tools/onepassword/get_vault.ts +++ b/apps/sim/tools/onepassword/get_vault.ts @@ -11,17 +11,28 @@ export const getVaultTool: ToolConfig { - const base = params.serverUrl.replace(/\/$/, '') - return `${base}/v1/vaults/${params.vaultId}` - }, - method: 'GET', - headers: (params) => ({ - Authorization: `Bearer ${params.apiKey}`, + url: '/api/tools/onepassword/get-vault', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + connectionMode: params.connectionMode, + serviceAccountToken: params.serviceAccountToken, + serverUrl: params.serverUrl, + apiKey: params.apiKey, + vaultId: params.vaultId, }), }, transformResponse: async (response) => { const data = await response.json() + if (data.error) { + return { + success: false, + output: { + id: '', + name: '', + description: null, + attributeVersion: 0, + contentVersion: 0, + items: 0, + type: '', + createdAt: null, + updatedAt: null, + }, + error: data.error, + } + } return { success: true, output: { diff --git a/apps/sim/tools/onepassword/index.ts b/apps/sim/tools/onepassword/index.ts index 71107e0ad..f51526b06 100644 --- a/apps/sim/tools/onepassword/index.ts +++ b/apps/sim/tools/onepassword/index.ts @@ -5,6 +5,7 @@ import { getVaultTool } from '@/tools/onepassword/get_vault' import { listItemsTool } from '@/tools/onepassword/list_items' import { listVaultsTool } from '@/tools/onepassword/list_vaults' import { replaceItemTool } from '@/tools/onepassword/replace_item' +import { resolveSecretTool } from '@/tools/onepassword/resolve_secret' import { updateItemTool } from '@/tools/onepassword/update_item' export const onepasswordCreateItemTool = createItemTool @@ -14,4 +15,5 @@ export const onepasswordGetVaultTool = getVaultTool export const onepasswordListItemsTool = listItemsTool export const onepasswordListVaultsTool = listVaultsTool export const onepasswordReplaceItemTool = replaceItemTool +export const onepasswordResolveSecretTool = resolveSecretTool export const onepasswordUpdateItemTool = updateItemTool diff --git a/apps/sim/tools/onepassword/list_items.ts b/apps/sim/tools/onepassword/list_items.ts index 459174425..4bcad6e65 100644 --- a/apps/sim/tools/onepassword/list_items.ts +++ b/apps/sim/tools/onepassword/list_items.ts @@ -11,17 +11,28 @@ export const listItemsTool: ToolConfig { - const base = params.serverUrl.replace(/\/$/, '') - const query = params.filter ? `?filter=${encodeURIComponent(params.filter)}` : '' - return `${base}/v1/vaults/${params.vaultId}/items${query}` - }, - method: 'GET', - headers: (params) => ({ - Authorization: `Bearer ${params.apiKey}`, + url: '/api/tools/onepassword/list-items', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + connectionMode: params.connectionMode, + serviceAccountToken: params.serviceAccountToken, + serverUrl: params.serverUrl, + apiKey: params.apiKey, + vaultId: params.vaultId, + filter: params.filter, }), }, transformResponse: async (response) => { const data = await response.json() + if (data.error) { + return { success: false, output: { items: [] }, error: data.error } + } + const items = Array.isArray(data) ? data : [data] return { success: true, output: { - items: (data ?? []).map((item: any) => ({ + items: items.map((item: any) => ({ id: item.id ?? null, title: item.title ?? null, vault: item.vault ?? null, diff --git a/apps/sim/tools/onepassword/list_vaults.ts b/apps/sim/tools/onepassword/list_vaults.ts index 6bf1f8823..64af64ec7 100644 --- a/apps/sim/tools/onepassword/list_vaults.ts +++ b/apps/sim/tools/onepassword/list_vaults.ts @@ -10,21 +10,32 @@ export const listVaultsTool: ToolConfig< > = { id: 'onepassword_list_vaults', name: '1Password List Vaults', - description: 'List all vaults accessible by the Connect token', + description: 'List all vaults accessible by the Connect token or Service Account', version: '1.0.0', params: { + connectionMode: { + type: 'string', + required: false, + description: 'Connection mode: "service_account" or "connect"', + }, + serviceAccountToken: { + type: 'string', + required: false, + visibility: 'user-only', + description: '1Password Service Account token (for Service Account mode)', + }, apiKey: { type: 'string', - required: true, + required: false, visibility: 'user-only', - description: '1Password Connect API token', + description: '1Password Connect API token (for Connect Server mode)', }, serverUrl: { type: 'string', - required: true, + required: false, visibility: 'user-only', - description: '1Password Connect server URL (e.g., http://localhost:8080)', + description: '1Password Connect server URL (for Connect Server mode)', }, filter: { type: 'string', @@ -35,23 +46,28 @@ export const listVaultsTool: ToolConfig< }, request: { - url: (params) => { - const base = params.serverUrl.replace(/\/$/, '') - const query = params.filter ? `?filter=${encodeURIComponent(params.filter)}` : '' - return `${base}/v1/vaults${query}` - }, - method: 'GET', - headers: (params) => ({ - Authorization: `Bearer ${params.apiKey}`, + url: '/api/tools/onepassword/list-vaults', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + connectionMode: params.connectionMode, + serviceAccountToken: params.serviceAccountToken, + serverUrl: params.serverUrl, + apiKey: params.apiKey, + filter: params.filter, }), }, transformResponse: async (response) => { const data = await response.json() + if (data.error) { + return { success: false, output: { vaults: [] }, error: data.error } + } + const vaults = Array.isArray(data) ? data : [data] return { success: true, output: { - vaults: (data ?? []).map((vault: any) => ({ + vaults: vaults.map((vault: any) => ({ id: vault.id ?? null, name: vault.name ?? null, description: vault.description ?? null, diff --git a/apps/sim/tools/onepassword/replace_item.ts b/apps/sim/tools/onepassword/replace_item.ts index b4357c512..4d8506fb9 100644 --- a/apps/sim/tools/onepassword/replace_item.ts +++ b/apps/sim/tools/onepassword/replace_item.ts @@ -15,17 +15,28 @@ export const replaceItemTool: ToolConfig< version: '1.0.0', params: { + connectionMode: { + type: 'string', + required: false, + description: 'Connection mode: "service_account" or "connect"', + }, + serviceAccountToken: { + type: 'string', + required: false, + visibility: 'user-only', + description: '1Password Service Account token (for Service Account mode)', + }, apiKey: { type: 'string', - required: true, + required: false, visibility: 'user-only', - description: '1Password Connect API token', + description: '1Password Connect API token (for Connect Server mode)', }, serverUrl: { type: 'string', - required: true, + required: false, visibility: 'user-only', - description: '1Password Connect server URL (e.g., http://localhost:8080)', + description: '1Password Connect server URL (for Connect Server mode)', }, vaultId: { type: 'string', @@ -49,20 +60,25 @@ export const replaceItemTool: ToolConfig< }, request: { - url: (params) => { - const base = params.serverUrl.replace(/\/$/, '') - return `${base}/v1/vaults/${params.vaultId}/items/${params.itemId}` - }, - method: 'PUT', - headers: (params) => ({ - Authorization: `Bearer ${params.apiKey}`, - 'Content-Type': 'application/json', + url: '/api/tools/onepassword/replace-item', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + connectionMode: params.connectionMode, + serviceAccountToken: params.serviceAccountToken, + serverUrl: params.serverUrl, + apiKey: params.apiKey, + vaultId: params.vaultId, + itemId: params.itemId, + item: params.item, }), - body: (params) => JSON.parse(params.item), }, transformResponse: async (response) => { const data = await response.json() + if (data.error) { + return { success: false, output: transformFullItem({}), error: data.error } + } return { success: true, output: transformFullItem(data), diff --git a/apps/sim/tools/onepassword/resolve_secret.ts b/apps/sim/tools/onepassword/resolve_secret.ts new file mode 100644 index 000000000..a2a3636fd --- /dev/null +++ b/apps/sim/tools/onepassword/resolve_secret.ts @@ -0,0 +1,67 @@ +import type { + OnePasswordResolveSecretParams, + OnePasswordResolveSecretResponse, +} from '@/tools/onepassword/types' +import type { ToolConfig } from '@/tools/types' + +export const resolveSecretTool: ToolConfig< + OnePasswordResolveSecretParams, + OnePasswordResolveSecretResponse +> = { + id: 'onepassword_resolve_secret', + name: '1Password Resolve Secret', + description: + 'Resolve a secret reference (op://vault/item/field) to its value. Service Account mode only.', + version: '1.0.0', + + params: { + connectionMode: { + type: 'string', + required: false, + description: 'Connection mode: must be "service_account" for this operation', + }, + serviceAccountToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: '1Password Service Account token', + }, + secretReference: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Secret reference URI (e.g., op://vault-name/item-name/field-name or op://vault-name/item-name/section-name/field-name)', + }, + }, + + request: { + url: '/api/tools/onepassword/resolve-secret', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + connectionMode: params.connectionMode ?? 'service_account', + serviceAccountToken: params.serviceAccountToken, + secretReference: params.secretReference, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + if (data.error) { + return { success: false, output: { secret: '', reference: '' }, error: data.error } + } + return { + success: true, + output: { + secret: data.secret ?? '', + reference: data.reference ?? '', + }, + } + }, + + outputs: { + secret: { type: 'string', description: 'The resolved secret value' }, + reference: { type: 'string', description: 'The original secret reference URI' }, + }, +} diff --git a/apps/sim/tools/onepassword/types.ts b/apps/sim/tools/onepassword/types.ts index b9d47c126..937acd35f 100644 --- a/apps/sim/tools/onepassword/types.ts +++ b/apps/sim/tools/onepassword/types.ts @@ -1,34 +1,32 @@ import type { ToolResponse } from '@/tools/types' -export interface OnePasswordListVaultsParams { - apiKey: string - serverUrl: string +/** Base params shared by all 1Password tools (credential fields). */ +export interface OnePasswordBaseParams { + connectionMode?: 'service_account' | 'connect' + serviceAccountToken?: string + apiKey?: string + serverUrl?: string +} + +export interface OnePasswordListVaultsParams extends OnePasswordBaseParams { filter?: string } -export interface OnePasswordGetVaultParams { - apiKey: string - serverUrl: string +export interface OnePasswordGetVaultParams extends OnePasswordBaseParams { vaultId: string } -export interface OnePasswordListItemsParams { - apiKey: string - serverUrl: string +export interface OnePasswordListItemsParams extends OnePasswordBaseParams { vaultId: string filter?: string } -export interface OnePasswordGetItemParams { - apiKey: string - serverUrl: string +export interface OnePasswordGetItemParams extends OnePasswordBaseParams { vaultId: string itemId: string } -export interface OnePasswordCreateItemParams { - apiKey: string - serverUrl: string +export interface OnePasswordCreateItemParams extends OnePasswordBaseParams { vaultId: string category: string title?: string @@ -36,29 +34,27 @@ export interface OnePasswordCreateItemParams { fields?: string } -export interface OnePasswordUpdateItemParams { - apiKey: string - serverUrl: string +export interface OnePasswordUpdateItemParams extends OnePasswordBaseParams { vaultId: string itemId: string operations: string } -export interface OnePasswordReplaceItemParams { - apiKey: string - serverUrl: string +export interface OnePasswordReplaceItemParams extends OnePasswordBaseParams { vaultId: string itemId: string item: string } -export interface OnePasswordDeleteItemParams { - apiKey: string - serverUrl: string +export interface OnePasswordDeleteItemParams extends OnePasswordBaseParams { vaultId: string itemId: string } +export interface OnePasswordResolveSecretParams extends OnePasswordBaseParams { + secretReference: string +} + export interface OnePasswordListVaultsResponse extends ToolResponse { output: { vaults: Array<{ @@ -154,3 +150,10 @@ export interface OnePasswordDeleteItemResponse extends ToolResponse { success: boolean } } + +export interface OnePasswordResolveSecretResponse extends ToolResponse { + output: { + secret: string + reference: string + } +} diff --git a/apps/sim/tools/onepassword/update_item.ts b/apps/sim/tools/onepassword/update_item.ts index 89d24bc66..af178dc87 100644 --- a/apps/sim/tools/onepassword/update_item.ts +++ b/apps/sim/tools/onepassword/update_item.ts @@ -15,17 +15,28 @@ export const updateItemTool: ToolConfig< version: '1.0.0', params: { + connectionMode: { + type: 'string', + required: false, + description: 'Connection mode: "service_account" or "connect"', + }, + serviceAccountToken: { + type: 'string', + required: false, + visibility: 'user-only', + description: '1Password Service Account token (for Service Account mode)', + }, apiKey: { type: 'string', - required: true, + required: false, visibility: 'user-only', - description: '1Password Connect API token', + description: '1Password Connect API token (for Connect Server mode)', }, serverUrl: { type: 'string', - required: true, + required: false, visibility: 'user-only', - description: '1Password Connect server URL (e.g., http://localhost:8080)', + description: '1Password Connect server URL (for Connect Server mode)', }, vaultId: { type: 'string', @@ -49,20 +60,25 @@ export const updateItemTool: ToolConfig< }, request: { - url: (params) => { - const base = params.serverUrl.replace(/\/$/, '') - return `${base}/v1/vaults/${params.vaultId}/items/${params.itemId}` - }, - method: 'PATCH', - headers: (params) => ({ - Authorization: `Bearer ${params.apiKey}`, - 'Content-Type': 'application/json', + url: '/api/tools/onepassword/update-item', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + connectionMode: params.connectionMode, + serviceAccountToken: params.serviceAccountToken, + serverUrl: params.serverUrl, + apiKey: params.apiKey, + vaultId: params.vaultId, + itemId: params.itemId, + operations: params.operations, }), - body: (params) => JSON.parse(params.operations), }, transformResponse: async (response) => { const data = await response.json() + if (data.error) { + return { success: false, output: transformFullItem({}), error: data.error } + } return { success: true, output: transformFullItem(data), diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 4a5380721..7411c53c5 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -814,7 +814,6 @@ import { jiraAddWorklogTool, jiraAssignIssueTool, jiraBulkRetrieveTool, - jiraBulkRetrieveV2Tool, jiraCreateIssueLinkTool, jiraDeleteAttachmentTool, jiraDeleteCommentTool, @@ -828,7 +827,6 @@ import { jiraRemoveWatcherTool, jiraRetrieveTool, jiraSearchIssuesTool, - jiraSearchIssuesV2Tool, jiraTransitionIssueTool, jiraUpdateCommentTool, jiraUpdateTool, @@ -1169,6 +1167,7 @@ import { onepasswordListItemsTool, onepasswordListVaultsTool, onepasswordReplaceItemTool, + onepasswordResolveSecretTool, onepasswordUpdateItemTool, } from '@/tools/onepassword' import { openAIEmbeddingsTool, openAIImageTool } from '@/tools/openai' @@ -1936,12 +1935,10 @@ export const tools: Record = { jira_update: jiraUpdateTool, jira_write: jiraWriteTool, jira_bulk_read: jiraBulkRetrieveTool, - jira_bulk_read_v2: jiraBulkRetrieveV2Tool, jira_delete_issue: jiraDeleteIssueTool, jira_assign_issue: jiraAssignIssueTool, jira_transition_issue: jiraTransitionIssueTool, jira_search_issues: jiraSearchIssuesTool, - jira_search_issues_v2: jiraSearchIssuesV2Tool, jira_add_comment: jiraAddCommentTool, jira_get_comments: jiraGetCommentsTool, jira_update_comment: jiraUpdateCommentTool, @@ -2157,6 +2154,7 @@ export const tools: Record = { onepassword_replace_item: onepasswordReplaceItemTool, onepassword_update_item: onepasswordUpdateItemTool, onepassword_delete_item: onepasswordDeleteItemTool, + onepassword_resolve_secret: onepasswordResolveSecretTool, gmail_send: gmailSendTool, gmail_send_v2: gmailSendV2Tool, gmail_read: gmailReadTool, diff --git a/bun.lock b/bun.lock index defa6c36f..31947e602 100644 --- a/bun.lock +++ b/bun.lock @@ -54,6 +54,7 @@ "name": "sim", "version": "0.1.0", "dependencies": { + "@1password/sdk": "0.3.1", "@a2a-js/sdk": "0.3.7", "@anthropic-ai/sdk": "0.71.2", "@aws-sdk/client-bedrock-runtime": "3.940.0", @@ -326,6 +327,10 @@ "react-dom": "19.2.1", }, "packages": { + "@1password/sdk": ["@1password/sdk@0.3.1", "", { "dependencies": { "@1password/sdk-core": "0.3.1" } }, "sha512-20zbQfqsjcECT0gvnAw4zONJDt3XQgNH946pZR0NV1Qxukyaz/DKB0cBnBNCCEWZg93Bah8poaR6gJCyuNX14w=="], + + "@1password/sdk-core": ["@1password/sdk-core@0.3.1", "", {}, "sha512-zFkbRznmE47kpke10OpO/9R0AF5csNWS+naFbadgXuFX1LlxY+2C28NSKbCXhLTqmcuWifBfPdZQ728GJ1i5xg=="], + "@a2a-js/sdk": ["@a2a-js/sdk@0.3.7", "", { "dependencies": { "uuid": "^11.1.0" }, "peerDependencies": { "express": "^4.21.2 || ^5.1.0" }, "optionalPeers": ["express"] }, "sha512-1WBghkOjgiKt4rPNje8jlB9VateVQXqyjlc887bY/H8yM82Hlf0+5JW8zB98BPExKAplI5XqtXVH980J6vqi+w=="], "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="],