From f39af2797796ae12d142c9b82820242e333d275b Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 2 Jul 2026 00:01:41 -0700 Subject: [PATCH 1/5] fix(onepassword): validate integration against API docs, add file downloads - add onepassword_get_item_file tool + route for downloading item file attachments (SDK items.files.read / Connect files/{id}/content), backed by newly-exposed item.files metadata on get/create/replace/update item - fix update_item JSON Patch applying array indices instead of 1Password's documented field-ID addressing (/fields/{fieldId}/...), which silently dropped field edits in Service Account mode - fix Service Account mode's list-vaults/list-items filter to honor SCIM `eq` exact-match semantics instead of always substring-matching - expand the create-item category dropdown from 9 to 19 real, creatable 1Password categories (was missing SOFTWARE_LICENSE, EMAIL_ACCOUNT, MEMBERSHIP, PASSPORT, REWARD_PROGRAM, DRIVER_LICENSE, BANK_ACCOUNT, MEDICAL_RECORD, OUTDOOR_LICENSE, WIRELESS_ROUTER, SOCIAL_SECURITY_NUMBER) - replace the block's single opaque `response: json` output with typed, per-operation output fields matching repo convention - remove incorrect password-masking on the Vault ID field (not a secret) - re-export tool types from the onepassword barrel --- .../tools/onepassword/get-item-file/route.ts | 106 +++++++++++ .../api/tools/onepassword/list-items/route.ts | 9 +- .../tools/onepassword/list-vaults/route.ts | 8 +- .../tools/onepassword/update-item/route.ts | 28 ++- apps/sim/app/api/tools/onepassword/utils.ts | 58 ++++++ apps/sim/blocks/blocks/onepassword.ts | 169 +++++++++++++++++- .../lib/api/contracts/tools/onepassword.ts | 23 +++ apps/sim/tools/onepassword/create_item.ts | 2 +- apps/sim/tools/onepassword/get_item_file.ts | 103 +++++++++++ apps/sim/tools/onepassword/index.ts | 4 + apps/sim/tools/onepassword/types.ts | 23 +++ apps/sim/tools/onepassword/utils.ts | 26 +++ apps/sim/tools/registry.ts | 2 + scripts/check-api-validation-contracts.ts | 4 +- 14 files changed, 543 insertions(+), 22 deletions(-) create mode 100644 apps/sim/app/api/tools/onepassword/get-item-file/route.ts create mode 100644 apps/sim/tools/onepassword/get_item_file.ts diff --git a/apps/sim/app/api/tools/onepassword/get-item-file/route.ts b/apps/sim/app/api/tools/onepassword/get-item-file/route.ts new file mode 100644 index 00000000000..65efec88e06 --- /dev/null +++ b/apps/sim/app/api/tools/onepassword/get-item-file/route.ts @@ -0,0 +1,106 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { generateId } from '@sim/utils/id' +import { type NextRequest, NextResponse } from 'next/server' +import { onePasswordGetItemFileContract } from '@/lib/api/contracts/tools/onepassword' +import { parseRequest, validationErrorResponse } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { + connectRequest, + createOnePasswordClient, + findItemFileAttributes, + resolveCredentials, +} from '../utils' + +const logger = createLogger('OnePasswordGetItemFileAPI') + +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateId().slice(0, 8) + + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + logger.warn(`[${requestId}] Unauthorized 1Password get-item-file attempt`) + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + try { + const parsed = await parseRequest( + onePasswordGetItemFileContract, + request, + {}, + { + validationErrorResponse: (error) => validationErrorResponse(error, 'Invalid request data'), + } + ) + if (!parsed.success) return parsed.response + const params = parsed.data.body + const creds = resolveCredentials(params) + + logger.info( + `[${requestId}] Downloading file ${params.fileId} from item ${params.itemId} (${creds.mode} mode)` + ) + + if (creds.mode === 'service_account') { + const client = await createOnePasswordClient(creds.serviceAccountToken!) + const item = await client.items.get(params.vaultId, params.itemId) + const attr = findItemFileAttributes(item, params.fileId) + if (!attr) { + return NextResponse.json({ error: 'File not found on item' }, { status: 404 }) + } + + const content = await client.items.files.read(params.vaultId, params.itemId, attr) + return NextResponse.json({ + file: { + name: attr.name, + mimeType: 'application/octet-stream', + data: Buffer.from(content).toString('base64'), + size: attr.size, + }, + }) + } + + const metaResponse = await connectRequest({ + serverUrl: creds.serverUrl!, + apiKey: creds.apiKey!, + path: `/v1/vaults/${params.vaultId}/items/${params.itemId}/files/${params.fileId}`, + method: 'GET', + }) + if (!metaResponse.ok) { + const metaData = await metaResponse.json().catch(() => ({})) + return NextResponse.json( + { error: metaData.message || 'Failed to get file metadata' }, + { status: metaResponse.status } + ) + } + const meta = await metaResponse.json() + + const contentResponse = await connectRequest({ + serverUrl: creds.serverUrl!, + apiKey: creds.apiKey!, + path: `/v1/vaults/${params.vaultId}/items/${params.itemId}/files/${params.fileId}/content`, + method: 'GET', + }) + if (!contentResponse.ok) { + const errorData = await contentResponse.json().catch(() => ({})) + return NextResponse.json( + { error: errorData.message || 'Failed to download file content' }, + { status: contentResponse.status } + ) + } + + const buffer = Buffer.from(await contentResponse.arrayBuffer()) + return NextResponse.json({ + file: { + name: meta.name ?? 'attachment', + mimeType: contentResponse.headers.get('content-type') || 'application/octet-stream', + data: buffer.toString('base64'), + size: meta.size ?? buffer.length, + }, + }) + } catch (error) { + const message = getErrorMessage(error, 'Unknown error') + logger.error(`[${requestId}] Get item file failed:`, error) + return NextResponse.json({ error: `Failed to get item file: ${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 index daeb42e807d..395d0955ced 100644 --- a/apps/sim/app/api/tools/onepassword/list-items/route.ts +++ b/apps/sim/app/api/tools/onepassword/list-items/route.ts @@ -9,6 +9,7 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { connectRequest, createOnePasswordClient, + matchesFilter, normalizeSdkItemOverview, resolveCredentials, } from '../utils' @@ -45,11 +46,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => { 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) + const filter = params.filter + const filtered = normalized.filter((item) => + matchesFilter(item.title ?? '', item.id ?? '', filter) ) return NextResponse.json(filtered) } diff --git a/apps/sim/app/api/tools/onepassword/list-vaults/route.ts b/apps/sim/app/api/tools/onepassword/list-vaults/route.ts index fa4011daa70..3638db1c2d7 100644 --- a/apps/sim/app/api/tools/onepassword/list-vaults/route.ts +++ b/apps/sim/app/api/tools/onepassword/list-vaults/route.ts @@ -9,6 +9,7 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { connectRequest, createOnePasswordClient, + matchesFilter, normalizeSdkVault, resolveCredentials, } from '../utils' @@ -45,11 +46,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => { 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) - ) + const filter = params.filter + const filtered = normalized.filter((v) => matchesFilter(v.name ?? '', v.id ?? '', filter)) return NextResponse.json(filtered) } diff --git a/apps/sim/app/api/tools/onepassword/update-item/route.ts b/apps/sim/app/api/tools/onepassword/update-item/route.ts index f027e95c45d..4db04552038 100644 --- a/apps/sim/app/api/tools/onepassword/update-item/route.ts +++ b/apps/sim/app/api/tools/onepassword/update-item/route.ts @@ -104,7 +104,7 @@ function applyPatch(item: Record, op: JsonPatchOperation) { for (let i = 0; i < segments.length - 1; i++) { const seg = segments[i] if (Array.isArray(target)) { - target = target[Number(seg)] + target = arrayElementForSegment(target, seg) } else { target = target[seg] } @@ -117,15 +117,37 @@ function applyPatch(item: Record, op: JsonPatchOperation) { if (Array.isArray(target) && lastSeg === '-') { target.push(op.value) } else if (Array.isArray(target)) { - target[Number(lastSeg)] = op.value + const index = arrayIndexForSegment(target, lastSeg) + if (index !== -1) target[index] = op.value } else { target[lastSeg] = op.value } } else if (op.op === 'remove') { if (Array.isArray(target)) { - target.splice(Number(lastSeg), 1) + const index = arrayIndexForSegment(target, lastSeg) + if (index !== -1) target.splice(index, 1) } else { delete target[lastSeg] } } } + +/** + * Resolves an array element for a JSON Patch path segment. 1Password's PATCH API + * addresses items in the `fields`/`sections` arrays by their `id`, not by numeric + * array index (e.g. `/fields/{fieldId}/value`), so a numeric-looking segment is + * only treated as a literal index when no element's `id` matches it. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function arrayIndexForSegment(target: any[], segment: string): number { + const byId = target.findIndex((el) => el && typeof el === 'object' && el.id === segment) + if (byId !== -1) return byId + const index = Number(segment) + return Number.isInteger(index) && index >= 0 && index < target.length ? index : -1 +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function arrayElementForSegment(target: any[], segment: string): any { + const index = arrayIndexForSegment(target, segment) + return index === -1 ? undefined : target[index] +} diff --git a/apps/sim/app/api/tools/onepassword/utils.ts b/apps/sim/app/api/tools/onepassword/utils.ts index 87c5e090da0..13f82a324a1 100644 --- a/apps/sim/app/api/tools/onepassword/utils.ts +++ b/apps/sim/app/api/tools/onepassword/utils.ts @@ -1,9 +1,11 @@ import dns from 'dns/promises' import type { + FileAttributes, Item, ItemCategory, ItemField, ItemFieldType, + ItemFile, ItemOverview, ItemSection, VaultOverview, @@ -102,10 +104,19 @@ interface NormalizedField { entropy: null } +/** Normalized attached-file metadata shape matching the Connect API response. */ +export interface NormalizedItemFile { + id: string + name: string + size: number + section: { id: string } | null +} + /** Normalized full item shape matching the Connect API response. */ export interface NormalizedItem extends NormalizedItemOverview { fields: NormalizedField[] sections: Array<{ id: string; label: string }> + files: NormalizedItemFile[] } /** @@ -323,9 +334,11 @@ export interface ConnectResponse { ok: boolean status: number statusText: string + headers: { get: (name: string) => string | null } // eslint-disable-next-line @typescript-eslint/no-explicit-any json: () => Promise text: () => Promise + arrayBuffer: () => Promise } /** Proxy a request to the 1Password Connect Server. */ @@ -431,6 +444,24 @@ export function normalizeSdkItem(item: Item): NormalizedItem { id: section.id, label: section.title, })), + files: [ + ...(item.files ?? []).map((file: ItemFile) => ({ + id: file.attributes.id, + name: file.attributes.name, + size: file.attributes.size, + section: file.sectionId ? { id: file.sectionId } : null, + })), + ...(item.document + ? [ + { + id: item.document.id, + name: item.document.name, + size: item.document.size, + section: null, + }, + ] + : []), + ], createdAt: item.createdAt instanceof Date ? item.createdAt.toISOString() : (item.createdAt ?? null), updatedAt: @@ -439,6 +470,33 @@ export function normalizeSdkItem(item: Item): NormalizedItem { } } +/** + * Find an attached file's SDK {@link FileAttributes} on an item by file ID. + * Checks both the `files` array and the single `document` attribute that + * Document-category items carry instead of a `files` entry. + */ +export function findItemFileAttributes(item: Item, fileId: string): FileAttributes | undefined { + if (item.document?.id === fileId) return item.document + return item.files?.find((file) => file.attributes.id === fileId)?.attributes +} + +/** + * Best-effort SCIM `eq` filter matcher for Service Account mode, which has no + * server-side filtering (unlike Connect, whose `filter` query param is forwarded + * verbatim and evaluated by the Connect server). Recognizes `field eq "value"` + * (quotes optional) as an exact, case-insensitive match; anything else falls back + * to a case-insensitive substring match against the name/id so the field remains + * useful for free-text search. + */ +export function matchesFilter(value: string, id: string, filter: string): boolean { + const eqMatch = filter.match(/^\s*\S+\s+eq\s+"?([^"]*)"?\s*$/i) + if (eqMatch) { + return value.toLowerCase() === eqMatch[1].toLowerCase() + } + const needle = filter.toLowerCase() + return value.toLowerCase().includes(needle) || id.toLowerCase().includes(needle) +} + /** Convert a Connect-style category string to the SDK category string. */ export function toSdkCategory(category: string): `${ItemCategory}` { return CONNECT_TO_SDK_CATEGORY[category] ?? 'Login' diff --git a/apps/sim/blocks/blocks/onepassword.ts b/apps/sim/blocks/blocks/onepassword.ts index c5e9b61271d..72d73ee33dd 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 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.', + '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, download attached files, create new items, update existing ones, delete items, and resolve secret references.', docsLink: 'https://docs.sim.ai/integrations/onepassword', category: 'tools', integrationType: IntegrationType.Security, @@ -24,6 +24,7 @@ export const OnePasswordBlock: BlockConfig = { { label: 'Get Vault', id: 'get_vault' }, { label: 'List Items', id: 'list_items' }, { label: 'Get Item', id: 'get_item' }, + { label: 'Get Item File', id: 'get_item_file' }, { label: 'Create Item', id: 'create_item' }, { label: 'Replace Item', id: 'replace_item' }, { label: 'Update Item', id: 'update_item' }, @@ -93,13 +94,13 @@ Return ONLY the op:// URI - no explanations, no quotes, no markdown.`, title: 'Vault ID', type: 'short-input', placeholder: 'Enter vault UUID', - password: true, required: { field: 'operation', value: [ 'get_vault', 'list_items', 'get_item', + 'get_item_file', 'create_item', 'replace_item', 'update_item', @@ -119,19 +120,38 @@ Return ONLY the op:// URI - no explanations, no quotes, no markdown.`, placeholder: 'Enter item UUID', required: { field: 'operation', - value: ['get_item', 'replace_item', 'update_item', 'delete_item'], + value: ['get_item', 'get_item_file', 'replace_item', 'update_item', 'delete_item'], }, condition: { field: 'operation', - value: ['get_item', 'replace_item', 'update_item', 'delete_item'], + value: ['get_item', 'get_item_file', 'replace_item', 'update_item', 'delete_item'], }, }, + { + id: 'fileId', + title: 'File ID', + type: 'short-input', + placeholder: 'Enter file ID (from Get Item output)', + required: { field: 'operation', value: 'get_item_file' }, + condition: { field: 'operation', value: 'get_item_file' }, + }, { id: 'filter', title: 'Filter', type: 'short-input', placeholder: 'SCIM filter (e.g., name eq "My Vault")', condition: { field: 'operation', value: ['list_vaults', 'list_items'] }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: `Generate a SCIM filter expression for a 1Password vault or item list based on the user's description. +Examples: +- name eq "My Vault" +- title eq "API Key" +- tag eq "production" + +Return ONLY the SCIM filter expression - no explanations, no quotes, no markdown.`, + }, }, { id: 'category', @@ -147,6 +167,17 @@ Return ONLY the op:// URI - no explanations, no quotes, no markdown.`, { label: 'Credit Card', id: 'CREDIT_CARD' }, { label: 'Identity', id: 'IDENTITY' }, { label: 'SSH Key', id: 'SSH_KEY' }, + { label: 'Software License', id: 'SOFTWARE_LICENSE' }, + { label: 'Email Account', id: 'EMAIL_ACCOUNT' }, + { label: 'Membership', id: 'MEMBERSHIP' }, + { label: 'Passport', id: 'PASSPORT' }, + { label: 'Reward Program', id: 'REWARD_PROGRAM' }, + { label: 'Driver License', id: 'DRIVER_LICENSE' }, + { label: 'Bank Account', id: 'BANK_ACCOUNT' }, + { label: 'Medical Record', id: 'MEDICAL_RECORD' }, + { label: 'Outdoor License', id: 'OUTDOOR_LICENSE' }, + { label: 'Wireless Router', id: 'WIRELESS_ROUTER' }, + { label: 'Social Security Number', id: 'SOCIAL_SECURITY_NUMBER' }, ], value: () => 'LOGIN', required: { field: 'operation', value: 'create_item' }, @@ -231,6 +262,7 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`, 'onepassword_get_vault', 'onepassword_list_items', 'onepassword_get_item', + 'onepassword_get_item_file', 'onepassword_create_item', 'onepassword_replace_item', 'onepassword_update_item', @@ -251,6 +283,7 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`, secretReference: { type: 'string', description: 'Secret reference URI (op://...)' }, vaultId: { type: 'string', description: 'Vault UUID' }, itemId: { type: 'string', description: 'Item UUID' }, + fileId: { type: 'string', description: 'File ID of an attachment on the item' }, filter: { type: 'string', description: 'SCIM filter expression' }, category: { type: 'string', description: 'Item category' }, title: { type: 'string', description: 'Item title' }, @@ -261,9 +294,133 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`, }, outputs: { - response: { + vaults: { + type: 'json', + description: + 'List of accessible vaults [{id, name, description, items, type, createdAt, updatedAt}]', + condition: { field: 'operation', value: 'list_vaults' }, + }, + id: { + type: 'string', + description: 'Vault or item ID', + condition: { + field: 'operation', + value: ['get_vault', 'get_item', 'create_item', 'replace_item', 'update_item'], + }, + }, + name: { + type: 'string', + description: 'Vault name', + condition: { field: 'operation', value: 'get_vault' }, + }, + description: { + type: 'string', + description: 'Vault description', + condition: { field: 'operation', value: 'get_vault' }, + }, + items: { + type: 'json', + description: + 'Number of items in the vault (Get Vault) or item summaries [{id, title, category, tags, favorite, version, updatedAt}] (List Items)', + condition: { field: 'operation', value: ['get_vault', 'list_items'] }, + }, + type: { + type: 'string', + description: 'Vault type (USER_CREATED, PERSONAL, EVERYONE, TRANSFER)', + condition: { field: 'operation', value: 'get_vault' }, + }, + title: { + type: 'string', + description: 'Item title', + condition: { + field: 'operation', + value: ['get_item', 'create_item', 'replace_item', 'update_item'], + }, + }, + category: { + type: 'string', + description: 'Item category (e.g., LOGIN, API_CREDENTIAL, SECURE_NOTE)', + condition: { + field: 'operation', + value: ['get_item', 'create_item', 'replace_item', 'update_item'], + }, + }, + vault: { + type: 'json', + description: 'Vault reference the item belongs to {id}', + condition: { + field: 'operation', + value: ['get_item', 'create_item', 'replace_item', 'update_item'], + }, + }, + fields: { + type: 'json', + description: 'Item fields including secrets [{id, label, type, purpose, value}]', + condition: { + field: 'operation', + value: ['get_item', 'create_item', 'replace_item', 'update_item'], + }, + }, + sections: { + type: 'json', + description: 'Item sections [{id, label}]', + condition: { + field: 'operation', + value: ['get_item', 'create_item', 'replace_item', 'update_item'], + }, + }, + files: { + type: 'json', + description: + 'Files attached to the item [{id, name, size, section}] — fetch content with Get Item File', + condition: { + field: 'operation', + value: ['get_item', 'create_item', 'replace_item', 'update_item'], + }, + }, + tags: { type: 'json', - description: 'Operation response data', + description: 'Item tags', + condition: { + field: 'operation', + value: ['get_item', 'create_item', 'replace_item', 'update_item'], + }, + }, + createdAt: { + type: 'string', + description: 'Creation timestamp', + condition: { + field: 'operation', + value: ['get_vault', 'get_item', 'create_item', 'replace_item', 'update_item'], + }, + }, + updatedAt: { + type: 'string', + description: 'Last update timestamp', + condition: { + field: 'operation', + value: ['get_vault', 'get_item', 'create_item', 'replace_item', 'update_item'], + }, + }, + success: { + type: 'boolean', + description: 'Whether the item was successfully deleted', + condition: { field: 'operation', value: 'delete_item' }, + }, + value: { + type: 'string', + description: 'The resolved secret value', + condition: { field: 'operation', value: 'resolve_secret' }, + }, + reference: { + type: 'string', + description: 'The original secret reference URI', + condition: { field: 'operation', value: 'resolve_secret' }, + }, + file: { + type: 'file', + description: 'Downloaded file attachment', + condition: { field: 'operation', value: 'get_item_file' }, }, }, } diff --git a/apps/sim/lib/api/contracts/tools/onepassword.ts b/apps/sim/lib/api/contracts/tools/onepassword.ts index b67c7fe12e1..349b13296f9 100644 --- a/apps/sim/lib/api/contracts/tools/onepassword.ts +++ b/apps/sim/lib/api/contracts/tools/onepassword.ts @@ -43,6 +43,10 @@ export const onePasswordResolveSecretBodySchema = onePasswordCredentialsBodySche secretReference: z.string().min(1, 'Secret reference is required'), }) +export const onePasswordGetItemFileBodySchema = onePasswordGetItemBodySchema.extend({ + fileId: z.string().min(1, 'File ID is required'), +}) + export const onePasswordListVaultsContract = defineRouteContract({ method: 'POST', path: '/api/tools/onepassword/list-vaults', @@ -148,3 +152,22 @@ export const onePasswordResolveSecretContract = defineRouteContract({ schema: onePasswordResolveSecretResponseSchema, }, }) + +const onePasswordGetItemFileResponseSchema = z.object({ + file: z.object({ + name: z.string(), + mimeType: z.string(), + data: z.string(), + size: z.number(), + }), +}) + +export const onePasswordGetItemFileContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/onepassword/get-item-file', + body: onePasswordGetItemFileBodySchema, + response: { + mode: 'json', + schema: onePasswordGetItemFileResponseSchema, + }, +}) diff --git a/apps/sim/tools/onepassword/create_item.ts b/apps/sim/tools/onepassword/create_item.ts index 5f9b70a0715..feb717f3af2 100644 --- a/apps/sim/tools/onepassword/create_item.ts +++ b/apps/sim/tools/onepassword/create_item.ts @@ -68,7 +68,7 @@ export const createItemTool: ToolConfig< required: false, visibility: 'user-or-llm', description: - 'JSON array of field objects (e.g., [{"label":"username","value":"admin","type":"STRING","purpose":"USERNAME"}])', + 'JSON array of field objects (e.g., [{"label":"username","value":"admin","type":"STRING","purpose":"USERNAME"}]). "purpose" is honored in Connect Server mode; in Service Account mode 1Password infers it from the field label/type instead.', }, }, diff --git a/apps/sim/tools/onepassword/get_item_file.ts b/apps/sim/tools/onepassword/get_item_file.ts new file mode 100644 index 00000000000..f5934886f4f --- /dev/null +++ b/apps/sim/tools/onepassword/get_item_file.ts @@ -0,0 +1,103 @@ +import type { + OnePasswordGetItemFileParams, + OnePasswordGetItemFileResponse, +} from '@/tools/onepassword/types' +import type { ToolConfig } from '@/tools/types' + +export const getItemFileTool: ToolConfig< + OnePasswordGetItemFileParams, + OnePasswordGetItemFileResponse +> = { + id: 'onepassword_get_item_file', + name: '1Password Get Item File', + description: 'Download the content of a file attached to an item', + 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: false, + visibility: 'user-only', + description: '1Password Connect API token (for Connect Server mode)', + }, + serverUrl: { + type: 'string', + required: false, + visibility: 'user-only', + description: '1Password Connect server URL (for Connect Server mode)', + }, + vaultId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The vault UUID', + }, + itemId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The item UUID the file is attached to', + }, + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The file ID (from the item\'s "files" array, e.g. via Get Item)', + }, + }, + + request: { + url: '/api/tools/onepassword/get-item-file', + 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, + fileId: params.fileId, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + if (data.error) { + return { + success: false, + output: { file: { name: '', mimeType: '', data: '', size: 0 } }, + error: data.error, + } + } + return { + success: true, + output: { + file: { + name: data.file.name, + mimeType: data.file.mimeType, + data: data.file.data, + size: data.file.size, + }, + }, + } + }, + + outputs: { + file: { + type: 'file', + description: 'Downloaded file attachment', + }, + }, +} diff --git a/apps/sim/tools/onepassword/index.ts b/apps/sim/tools/onepassword/index.ts index f51526b0661..6912f7bee7b 100644 --- a/apps/sim/tools/onepassword/index.ts +++ b/apps/sim/tools/onepassword/index.ts @@ -1,6 +1,7 @@ import { createItemTool } from '@/tools/onepassword/create_item' import { deleteItemTool } from '@/tools/onepassword/delete_item' import { getItemTool } from '@/tools/onepassword/get_item' +import { getItemFileTool } from '@/tools/onepassword/get_item_file' import { getVaultTool } from '@/tools/onepassword/get_vault' import { listItemsTool } from '@/tools/onepassword/list_items' import { listVaultsTool } from '@/tools/onepassword/list_vaults' @@ -8,9 +9,12 @@ import { replaceItemTool } from '@/tools/onepassword/replace_item' import { resolveSecretTool } from '@/tools/onepassword/resolve_secret' import { updateItemTool } from '@/tools/onepassword/update_item' +export * from '@/tools/onepassword/types' + export const onepasswordCreateItemTool = createItemTool export const onepasswordDeleteItemTool = deleteItemTool export const onepasswordGetItemTool = getItemTool +export const onepasswordGetItemFileTool = getItemFileTool export const onepasswordGetVaultTool = getVaultTool export const onepasswordListItemsTool = listItemsTool export const onepasswordListVaultsTool = listVaultsTool diff --git a/apps/sim/tools/onepassword/types.ts b/apps/sim/tools/onepassword/types.ts index cd21c7b51ed..1da33cfad42 100644 --- a/apps/sim/tools/onepassword/types.ts +++ b/apps/sim/tools/onepassword/types.ts @@ -51,6 +51,12 @@ export interface OnePasswordDeleteItemParams extends OnePasswordBaseParams { itemId: string } +export interface OnePasswordGetItemFileParams extends OnePasswordBaseParams { + vaultId: string + itemId: string + fileId: string +} + export interface OnePasswordResolveSecretParams extends OnePasswordBaseParams { secretReference: string } @@ -134,6 +140,12 @@ interface OnePasswordFullItemResponse extends ToolResponse { id: string label: string | null }> + files: Array<{ + id: string | null + name: string | null + size: number + section: { id: string } | null + }> createdAt: string | null updatedAt: string | null lastEditedBy: string | null @@ -151,6 +163,17 @@ export interface OnePasswordDeleteItemResponse extends ToolResponse { } } +export interface OnePasswordGetItemFileResponse extends ToolResponse { + output: { + file: { + name: string + mimeType: string + data: string + size: number + } + } +} + export interface OnePasswordResolveSecretResponse extends ToolResponse { output: { value: string diff --git a/apps/sim/tools/onepassword/utils.ts b/apps/sim/tools/onepassword/utils.ts index d1abbf9632c..bc2cf7d1ff3 100644 --- a/apps/sim/tools/onepassword/utils.ts +++ b/apps/sim/tools/onepassword/utils.ts @@ -37,6 +37,12 @@ export function transformFullItem(data: any) { id: section.id ?? null, label: section.label ?? null, })), + files: (data.files ?? []).map((file: any) => ({ + id: file.id ?? null, + name: file.name ?? null, + size: file.size ?? 0, + section: file.section ?? null, + })), createdAt: data.createdAt ?? null, updatedAt: data.updatedAt ?? null, lastEditedBy: data.lastEditedBy ?? null, @@ -142,6 +148,26 @@ export const FULL_ITEM_OUTPUTS: Record< }, }, }, + files: { + type: 'array', + description: 'Files attached to the item (fetch content with Get Item File)', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'File ID' }, + name: { type: 'string', description: 'File name' }, + size: { type: 'number', description: 'File size in bytes' }, + section: { + type: 'object', + description: 'Section reference this file belongs to', + optional: true, + properties: { + id: { type: 'string', description: 'Section ID' }, + }, + }, + }, + }, + }, createdAt: { type: 'string', description: 'Creation timestamp', optional: true }, updatedAt: { type: 'string', description: 'Last update timestamp', optional: true }, lastEditedBy: { type: 'string', description: 'ID of the last editor', optional: true }, diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 2bb910c4eef..e25a92dd960 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -2394,6 +2394,7 @@ import { import { onepasswordCreateItemTool, onepasswordDeleteItemTool, + onepasswordGetItemFileTool, onepasswordGetItemTool, onepasswordGetVaultTool, onepasswordListItemsTool, @@ -5333,6 +5334,7 @@ export const tools: Record = { onepassword_get_vault: onepasswordGetVaultTool, onepassword_list_items: onepasswordListItemsTool, onepassword_get_item: onepasswordGetItemTool, + onepassword_get_item_file: onepasswordGetItemFileTool, onepassword_create_item: onepasswordCreateItemTool, onepassword_replace_item: onepasswordReplaceItemTool, onepassword_update_item: onepasswordUpdateItemTool, diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index 0d0d94230aa..80f935786b0 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries') const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors') const BASELINE = { - totalRoutes: 882, - zodRoutes: 882, + totalRoutes: 883, + zodRoutes: 883, nonZodRoutes: 0, } as const From e0cc4bc88d5d29131859ac79a3048554b3ab4f83 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 2 Jul 2026 00:11:12 -0700 Subject: [PATCH 2/5] fix(onepassword): honor SCIM attribute name in filter matcher matchesFilter always compared against name/title regardless of the attribute named in the eq expression, so `id eq "..."` incorrectly matched against the display name instead of the id. --- apps/sim/app/api/tools/onepassword/utils.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/sim/app/api/tools/onepassword/utils.ts b/apps/sim/app/api/tools/onepassword/utils.ts index 13f82a324a1..e8b74b2981a 100644 --- a/apps/sim/app/api/tools/onepassword/utils.ts +++ b/apps/sim/app/api/tools/onepassword/utils.ts @@ -483,15 +483,19 @@ export function findItemFileAttributes(item: Item, fileId: string): FileAttribut /** * Best-effort SCIM `eq` filter matcher for Service Account mode, which has no * server-side filtering (unlike Connect, whose `filter` query param is forwarded - * verbatim and evaluated by the Connect server). Recognizes `field eq "value"` - * (quotes optional) as an exact, case-insensitive match; anything else falls back - * to a case-insensitive substring match against the name/id so the field remains - * useful for free-text search. + * verbatim and evaluated by the Connect server). Recognizes `attribute eq "value"` + * (quotes optional) as an exact, case-insensitive match against the named attribute + * — `id` compares against the id, anything else (name/title/etc.) against the + * display value; anything that doesn't parse as `eq` falls back to a + * case-insensitive substring match against both so the field remains useful for + * free-text search. */ export function matchesFilter(value: string, id: string, filter: string): boolean { - const eqMatch = filter.match(/^\s*\S+\s+eq\s+"?([^"]*)"?\s*$/i) + const eqMatch = filter.match(/^\s*(\S+)\s+eq\s+"?([^"]*)"?\s*$/i) if (eqMatch) { - return value.toLowerCase() === eqMatch[1].toLowerCase() + const [, attribute, needle] = eqMatch + const target = attribute.toLowerCase() === 'id' ? id : value + return target.toLowerCase() === needle.toLowerCase() } const needle = filter.toLowerCase() return value.toLowerCase().includes(needle) || id.toLowerCase().includes(needle) From 5a1d5359a5a03ac8b8e20aaf4d1e704be0440ac2 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 2 Jul 2026 08:38:46 -0700 Subject: [PATCH 3/5] fix(onepassword): close output-parity and doc-string gaps from final audit - restore a deprecated no-op 'response' output so pre-existing saved workflows referencing it fail soft (empty) instead of hard-erroring now that per-operation outputs replace it - add missing block outputs (urls, favorite, version, state, lastEditedBy) for get/create/replace/update item so all real FULL_ITEM fields are discoverable as references - hide Connect Server credential fields for Resolve Secret (Service Account only) instead of leaving them selectable and silently ignored - correct two doc-string enum lists that advertised values the API doesn't return (vault type TRANSFER, item state DELETED) --- apps/sim/blocks/blocks/onepassword.ts | 71 +++++++++++++++++++++-- apps/sim/tools/onepassword/get_vault.ts | 2 +- apps/sim/tools/onepassword/list_items.ts | 2 +- apps/sim/tools/onepassword/list_vaults.ts | 2 +- apps/sim/tools/onepassword/utils.ts | 6 +- 5 files changed, 74 insertions(+), 9 deletions(-) diff --git a/apps/sim/blocks/blocks/onepassword.ts b/apps/sim/blocks/blocks/onepassword.ts index 72d73ee33dd..084ef242d92 100644 --- a/apps/sim/blocks/blocks/onepassword.ts +++ b/apps/sim/blocks/blocks/onepassword.ts @@ -57,8 +57,16 @@ export const OnePasswordBlock: BlockConfig = { title: 'Server URL', type: 'short-input', placeholder: 'http://localhost:8080', - required: { field: 'connectionMode', value: 'connect' }, - condition: { field: 'connectionMode', value: 'connect' }, + required: { + field: 'connectionMode', + value: 'connect', + and: { field: 'operation', value: 'resolve_secret', not: true }, + }, + condition: { + field: 'connectionMode', + value: 'connect', + and: { field: 'operation', value: 'resolve_secret', not: true }, + }, }, { id: 'apiKey', @@ -66,8 +74,16 @@ export const OnePasswordBlock: BlockConfig = { type: 'short-input', placeholder: 'Enter your 1Password Connect token', password: true, - required: { field: 'connectionMode', value: 'connect' }, - condition: { field: 'connectionMode', value: 'connect' }, + required: { + field: 'connectionMode', + value: 'connect', + and: { field: 'operation', value: 'resolve_secret', not: true }, + }, + condition: { + field: 'connectionMode', + value: 'connect', + and: { field: 'operation', value: 'resolve_secret', not: true }, + }, }, { id: 'secretReference', @@ -294,6 +310,11 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`, }, outputs: { + response: { + type: 'json', + description: + 'Deprecated — kept for backward compatibility with workflows saved before per-operation outputs were added below. Never populated; use the operation-specific outputs instead.', + }, vaults: { type: 'json', description: @@ -326,7 +347,7 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`, }, type: { type: 'string', - description: 'Vault type (USER_CREATED, PERSONAL, EVERYONE, TRANSFER)', + description: 'Vault type (USER_CREATED, PERSONAL, or EVERYONE)', condition: { field: 'operation', value: 'get_vault' }, }, title: { @@ -386,6 +407,46 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`, value: ['get_item', 'create_item', 'replace_item', 'update_item'], }, }, + urls: { + type: 'json', + description: 'URLs associated with the item [{href, label, primary}]', + condition: { + field: 'operation', + value: ['get_item', 'create_item', 'replace_item', 'update_item'], + }, + }, + favorite: { + type: 'boolean', + description: 'Whether the item is favorited', + condition: { + field: 'operation', + value: ['get_item', 'create_item', 'replace_item', 'update_item'], + }, + }, + version: { + type: 'number', + description: 'Item version number', + condition: { + field: 'operation', + value: ['get_item', 'create_item', 'replace_item', 'update_item'], + }, + }, + state: { + type: 'string', + description: 'Item state (ARCHIVED, or absent/null when active)', + condition: { + field: 'operation', + value: ['get_item', 'create_item', 'replace_item', 'update_item'], + }, + }, + lastEditedBy: { + type: 'string', + description: 'ID of the last editor', + condition: { + field: 'operation', + value: ['get_item', 'create_item', 'replace_item', 'update_item'], + }, + }, createdAt: { type: 'string', description: 'Creation timestamp', diff --git a/apps/sim/tools/onepassword/get_vault.ts b/apps/sim/tools/onepassword/get_vault.ts index cf2b63a4479..30415c78363 100644 --- a/apps/sim/tools/onepassword/get_vault.ts +++ b/apps/sim/tools/onepassword/get_vault.ts @@ -99,7 +99,7 @@ export const getVaultTool: ToolConfig Date: Thu, 2 Jul 2026 09:51:55 -0700 Subject: [PATCH 4/5] fix(onepassword): fix silent data loss in update_item (Service Account mode) update_item applied user JSON Patch ops (documented/typed against the Connect-shaped vocabulary get_item returns: label/type/section.id) directly onto the raw SDK item, whose vocabulary differs (title/ fieldType/sectionId, and SDK category enum strings vs Connect's SCREAMING_SNAKE_CASE). Most patches beyond /title, /tags/-, and /fields/{id}/value silently no-opped or could corrupt the item while still reporting success. Extracted the Connect->SDK item conversion already used by replace_item into a shared connectItemToSdkItem helper. update_item now normalizes the fetched item to Connect shape, applies patches to that, then converts back before calling items.put() -- matching create/replace's existing translation pattern. Found via an adversarial final-verification pass that traced concrete patch operations by hand against the SDK's actual field vocabulary. --- .../tools/onepassword/replace-item/route.ts | 39 +------------ .../tools/onepassword/update-item/route.ts | 15 ++++- apps/sim/app/api/tools/onepassword/utils.ts | 55 +++++++++++++++++++ 3 files changed, 69 insertions(+), 40 deletions(-) diff --git a/apps/sim/app/api/tools/onepassword/replace-item/route.ts b/apps/sim/app/api/tools/onepassword/replace-item/route.ts index 0f2ee44b76b..67d7a7b10f8 100644 --- a/apps/sim/app/api/tools/onepassword/replace-item/route.ts +++ b/apps/sim/app/api/tools/onepassword/replace-item/route.ts @@ -1,4 +1,3 @@ -import type { Item } from '@1password/sdk' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' @@ -8,12 +7,11 @@ import { parseRequest, validationErrorResponse } from '@/lib/api/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { + connectItemToSdkItem, connectRequest, createOnePasswordClient, normalizeSdkItem, resolveCredentials, - toSdkCategory, - toSdkFieldType, } from '../utils' const logger = createLogger('OnePasswordReplaceItemAPI') @@ -49,40 +47,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const client = await createOnePasswordClient(creds.serviceAccountToken!) const existing = await client.items.get(params.vaultId, params.itemId) - - const sdkItem = { - ...existing, - id: params.itemId, - title: itemData.title || existing.title, - category: itemData.category ? toSdkCategory(itemData.category) : existing.category, - vaultId: params.vaultId, - fields: itemData.fields - ? (itemData.fields as Array>).map((f) => ({ - id: f.id || generateId().slice(0, 8), - title: f.label || f.title || '', - fieldType: toSdkFieldType(f.type || 'STRING'), - value: f.value || '', - sectionId: f.section?.id ?? f.sectionId, - })) - : existing.fields, - sections: itemData.sections - ? (itemData.sections as Array>).map((s) => ({ - id: s.id || '', - title: s.label || s.title || '', - })) - : existing.sections, - notes: itemData.notes ?? existing.notes, - tags: itemData.tags ?? existing.tags, - websites: - itemData.urls || itemData.websites - ? (itemData.urls ?? itemData.websites ?? []).map((u: Record) => ({ - url: u.href || u.url || '', - label: u.label || '', - autofillBehavior: 'AnywhereOnWebsite' as const, - })) - : existing.websites, - } as Item - + const sdkItem = connectItemToSdkItem(itemData, existing) const result = await client.items.put(sdkItem) return NextResponse.json(normalizeSdkItem(result)) } diff --git a/apps/sim/app/api/tools/onepassword/update-item/route.ts b/apps/sim/app/api/tools/onepassword/update-item/route.ts index 4db04552038..70fa927b992 100644 --- a/apps/sim/app/api/tools/onepassword/update-item/route.ts +++ b/apps/sim/app/api/tools/onepassword/update-item/route.ts @@ -7,6 +7,7 @@ import { parseRequest, validationErrorResponse } from '@/lib/api/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { + connectItemToSdkItem, connectRequest, createOnePasswordClient, normalizeSdkItem, @@ -45,13 +46,21 @@ export const POST = withRouteHandler(async (request: NextRequest) => { if (creds.mode === 'service_account') { const client = await createOnePasswordClient(creds.serviceAccountToken!) - const item = await client.items.get(params.vaultId, params.itemId) + const existing = await client.items.get(params.vaultId, params.itemId) + // Patch operations are documented and typed against the Connect-shaped + // vocabulary (label/type/section.id) that get_item/create_item/replace_item + // return — apply them to that normalized view, then convert back to the + // SDK's vocabulary (title/fieldType/sectionId) before writing. Patching the + // raw SDK item directly would silently no-op most field/category writes. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const connectItem = normalizeSdkItem(existing) as Record for (const op of ops) { - applyPatch(item, op) + applyPatch(connectItem, op) } - const result = await client.items.put(item) + const sdkItem = connectItemToSdkItem(connectItem, existing) + const result = await client.items.put(sdkItem) return NextResponse.json(normalizeSdkItem(result)) } diff --git a/apps/sim/app/api/tools/onepassword/utils.ts b/apps/sim/app/api/tools/onepassword/utils.ts index e8b74b2981a..acd9a12d9be 100644 --- a/apps/sim/app/api/tools/onepassword/utils.ts +++ b/apps/sim/app/api/tools/onepassword/utils.ts @@ -13,6 +13,7 @@ import type { } from '@1password/sdk' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' +import { generateId } from '@sim/utils/id' import * as ipaddr from 'ipaddr.js' import { isHosted } from '@/lib/core/config/env-flags' import { @@ -480,6 +481,60 @@ export function findItemFileAttributes(item: Item, fileId: string): FileAttribut return item.files?.find((file) => file.attributes.id === fileId)?.attributes } +/** + * Convert a Connect-shaped item (the vocabulary `normalizeSdkItem` produces and + * this integration's tools document — `label`/`type`/`section: {id}`) back into + * an SDK-compatible {@link Item} for `client.items.put()`. Falls back to `existing` + * for any array the caller didn't provide, so partial input (e.g. Replace Item's + * optional fields) is preserved. + * + * Service Account mode must always convert through this function before calling + * `put()` — never apply a Connect-shaped JSON Patch directly onto a raw SDK + * {@link Item}, since SDK field/category vocabulary differs from Connect's + * (`title` vs `label`, `fieldType` vs `type`, `sectionId` vs `section.id`, SDK + * category enum strings vs Connect's SCREAMING_SNAKE_CASE) and silently no-ops or + * corrupts the write otherwise. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function connectItemToSdkItem(connectItem: Record, existing: Item): Item { + return { + ...existing, + id: existing.id, + vaultId: existing.vaultId, + title: connectItem.title ?? existing.title, + category: connectItem.category ? toSdkCategory(connectItem.category) : existing.category, + fields: Array.isArray(connectItem.fields) + ? // eslint-disable-next-line @typescript-eslint/no-explicit-any + connectItem.fields.map((f: Record) => ({ + id: f.id || generateId().slice(0, 8), + title: f.label || f.title || '', + fieldType: toSdkFieldType(f.type || 'STRING'), + value: f.value || '', + sectionId: f.section?.id ?? f.sectionId, + })) + : existing.fields, + sections: Array.isArray(connectItem.sections) + ? // eslint-disable-next-line @typescript-eslint/no-explicit-any + connectItem.sections.map((s: Record) => ({ + id: s.id || '', + title: s.label || s.title || '', + })) + : existing.sections, + notes: connectItem.notes ?? existing.notes, + tags: connectItem.tags ?? existing.tags, + websites: Array.isArray(connectItem.urls ?? connectItem.websites) + ? (connectItem.urls ?? connectItem.websites).map( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (u: Record) => ({ + url: u.href || u.url || '', + label: u.label || '', + autofillBehavior: 'AnywhereOnWebsite' as const, + }) + ) + : existing.websites, + } as Item +} + /** * Best-effort SCIM `eq` filter matcher for Service Account mode, which has no * server-side filtering (unlike Connect, whose `filter` query param is forwarded From e78839e3f15717a4764fb2c0384dbea7a9f3fc2b Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 2 Jul 2026 10:06:21 -0700 Subject: [PATCH 5/5] fix(onepassword): preserve field metadata and empty-title fallback connectItemToSdkItem rebuilt every field as a bare object, dropping SDK-only metadata (e.g. password-generation details) that a raw patch/replace previously left untouched. Now merges onto the existing SDK field by id before applying the translated properties, and only starts fields bare when they're genuinely new. Also restored the || (not ??) fallback on title to match replace_item's prior behavior of treating an explicitly empty title as "not provided". --- apps/sim/app/api/tools/onepassword/utils.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/api/tools/onepassword/utils.ts b/apps/sim/app/api/tools/onepassword/utils.ts index acd9a12d9be..b78cf0d511c 100644 --- a/apps/sim/app/api/tools/onepassword/utils.ts +++ b/apps/sim/app/api/tools/onepassword/utils.ts @@ -497,15 +497,21 @@ export function findItemFileAttributes(item: Item, fileId: string): FileAttribut */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function connectItemToSdkItem(connectItem: Record, existing: Item): Item { + const existingFieldsById = new Map((existing.fields ?? []).map((f) => [f.id, f])) + const existingSectionsById = new Map((existing.sections ?? []).map((s) => [s.id, s])) + return { ...existing, id: existing.id, vaultId: existing.vaultId, - title: connectItem.title ?? existing.title, + title: connectItem.title || existing.title, category: connectItem.category ? toSdkCategory(connectItem.category) : existing.category, fields: Array.isArray(connectItem.fields) ? // eslint-disable-next-line @typescript-eslint/no-explicit-any connectItem.fields.map((f: Record) => ({ + // Preserve any SDK-only metadata (e.g. password-generation `details`) + // on fields that already existed — only brand-new fields start bare. + ...(f.id ? existingFieldsById.get(f.id) : undefined), id: f.id || generateId().slice(0, 8), title: f.label || f.title || '', fieldType: toSdkFieldType(f.type || 'STRING'), @@ -516,6 +522,7 @@ export function connectItemToSdkItem(connectItem: Record, existing: sections: Array.isArray(connectItem.sections) ? // eslint-disable-next-line @typescript-eslint/no-explicit-any connectItem.sections.map((s: Record) => ({ + ...(s.id ? existingSectionsById.get(s.id) : undefined), id: s.id || '', title: s.label || s.title || '', }))