mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
improvement(jsm): destructured outputs for jsm, jira, and added 1password integration (#3174)
* improvement(jsm): destructured outputs for jsm, jira, and added 1password integration * update 1password to support cloud & locally hosted * updated & tested 1pass * added an additional wandConfig for OnePassword & jira search issues * finished jira * removed unused route * updated types * restore old outputs * updated types
This commit is contained in:
@@ -90,16 +90,24 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
const attachments = await response.json()
|
||||
const attachmentIds = Array.isArray(attachments)
|
||||
? attachments.map((attachment) => attachment.id).filter(Boolean)
|
||||
: []
|
||||
const jiraAttachments = await response.json()
|
||||
const attachmentsList = Array.isArray(jiraAttachments) ? jiraAttachments : []
|
||||
|
||||
const attachmentIds = attachmentsList.map((att: any) => att.id).filter(Boolean)
|
||||
const attachments = attachmentsList.map((att: any) => ({
|
||||
id: att.id ?? '',
|
||||
filename: att.filename ?? '',
|
||||
mimeType: att.mimeType ?? '',
|
||||
size: att.size ?? 0,
|
||||
content: att.content ?? '',
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueKey: validatedData.issueKey,
|
||||
attachments,
|
||||
attachmentIds,
|
||||
files: filesOutput,
|
||||
},
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId } from '@/tools/jira/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JiraIssueAPI')
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const auth = await checkSessionOrInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { domain, accessToken, issueId, cloudId: providedCloudId } = await request.json()
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!issueId) {
|
||||
logger.error('Missing issue ID in request')
|
||||
return NextResponse.json({ error: 'Issue ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = providedCloudId || (await getJiraCloudId(domain, accessToken))
|
||||
logger.info('Using cloud ID:', cloudId)
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const issueIdValidation = validateJiraIssueKey(issueId, 'issueId')
|
||||
if (!issueIdValidation.isValid) {
|
||||
return NextResponse.json({ error: issueIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${issueId}`
|
||||
|
||||
logger.info('Fetching Jira issue from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
logger.error('Jira API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
})
|
||||
|
||||
let errorMessage
|
||||
try {
|
||||
const errorData = await response.json()
|
||||
logger.error('Error details:', errorData)
|
||||
errorMessage = errorData.message || `Failed to fetch issue (${response.status})`
|
||||
} catch (_e) {
|
||||
errorMessage = `Failed to fetch issue: ${response.status} ${response.statusText}`
|
||||
}
|
||||
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
logger.info('Successfully fetched issue:', data.key)
|
||||
|
||||
const issueInfo: any = {
|
||||
id: data.key,
|
||||
name: data.fields.summary,
|
||||
mimeType: 'jira/issue',
|
||||
url: `https://${domain}/browse/${data.key}`,
|
||||
modifiedTime: data.fields.updated,
|
||||
webViewLink: `https://${domain}/browse/${data.key}`,
|
||||
status: data.fields.status?.name,
|
||||
description: data.fields.description,
|
||||
priority: data.fields.priority?.name,
|
||||
assignee: data.fields.assignee?.displayName,
|
||||
reporter: data.fields.reporter?.displayName,
|
||||
project: {
|
||||
key: data.fields.project?.key,
|
||||
name: data.fields.project?.name,
|
||||
},
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
issue: issueInfo,
|
||||
cloudId,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error processing request:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to retrieve Jira issue',
|
||||
details: (error as Error).message,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,16 @@ const jiraUpdateSchema = z.object({
|
||||
summary: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
status: z.string().optional(),
|
||||
priority: z.string().optional(),
|
||||
assignee: z.string().optional(),
|
||||
labels: z.array(z.string()).optional(),
|
||||
components: z.array(z.string()).optional(),
|
||||
duedate: z.string().optional(),
|
||||
fixVersions: z.array(z.string()).optional(),
|
||||
environment: z.string().optional(),
|
||||
customFieldId: z.string().optional(),
|
||||
customFieldValue: z.string().optional(),
|
||||
notifyUsers: z.boolean().optional(),
|
||||
cloudId: z.string().optional(),
|
||||
})
|
||||
|
||||
@@ -45,9 +52,16 @@ export async function PUT(request: NextRequest) {
|
||||
summary,
|
||||
title,
|
||||
description,
|
||||
status,
|
||||
priority,
|
||||
assignee,
|
||||
labels,
|
||||
components,
|
||||
duedate,
|
||||
fixVersions,
|
||||
environment,
|
||||
customFieldId,
|
||||
customFieldValue,
|
||||
notifyUsers,
|
||||
cloudId: providedCloudId,
|
||||
} = validation.data
|
||||
|
||||
@@ -64,7 +78,8 @@ export async function PUT(request: NextRequest) {
|
||||
return NextResponse.json({ error: issueKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${issueKey}`
|
||||
const notifyParam = notifyUsers === false ? '?notifyUsers=false' : ''
|
||||
const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${issueKey}${notifyParam}`
|
||||
|
||||
logger.info('Updating Jira issue at:', url)
|
||||
|
||||
@@ -93,24 +108,65 @@ export async function PUT(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
if (status !== undefined && status !== null && status !== '') {
|
||||
fields.status = {
|
||||
name: status,
|
||||
}
|
||||
}
|
||||
|
||||
if (priority !== undefined && priority !== null && priority !== '') {
|
||||
fields.priority = {
|
||||
name: priority,
|
||||
}
|
||||
const isNumericId = /^\d+$/.test(priority)
|
||||
fields.priority = isNumericId ? { id: priority } : { name: priority }
|
||||
}
|
||||
|
||||
if (assignee !== undefined && assignee !== null && assignee !== '') {
|
||||
fields.assignee = {
|
||||
id: assignee,
|
||||
accountId: assignee,
|
||||
}
|
||||
}
|
||||
|
||||
if (labels !== undefined && labels !== null && labels.length > 0) {
|
||||
fields.labels = labels
|
||||
}
|
||||
|
||||
if (components !== undefined && components !== null && components.length > 0) {
|
||||
fields.components = components.map((name) => ({ name }))
|
||||
}
|
||||
|
||||
if (duedate !== undefined && duedate !== null && duedate !== '') {
|
||||
fields.duedate = duedate
|
||||
}
|
||||
|
||||
if (fixVersions !== undefined && fixVersions !== null && fixVersions.length > 0) {
|
||||
fields.fixVersions = fixVersions.map((name) => ({ name }))
|
||||
}
|
||||
|
||||
if (environment !== undefined && environment !== null && environment !== '') {
|
||||
fields.environment = {
|
||||
type: 'doc',
|
||||
version: 1,
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: environment,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
customFieldId !== undefined &&
|
||||
customFieldId !== null &&
|
||||
customFieldId !== '' &&
|
||||
customFieldValue !== undefined &&
|
||||
customFieldValue !== null &&
|
||||
customFieldValue !== ''
|
||||
) {
|
||||
const fieldId = customFieldId.startsWith('customfield_')
|
||||
? customFieldId
|
||||
: `customfield_${customFieldId}`
|
||||
fields[fieldId] = customFieldValue
|
||||
}
|
||||
|
||||
const requestBody = { fields }
|
||||
|
||||
const response = await fetch(url, {
|
||||
|
||||
@@ -32,6 +32,8 @@ export async function POST(request: NextRequest) {
|
||||
environment,
|
||||
customFieldId,
|
||||
customFieldValue,
|
||||
components,
|
||||
fixVersions,
|
||||
} = await request.json()
|
||||
|
||||
if (!domain) {
|
||||
@@ -73,10 +75,9 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
logger.info('Creating Jira issue at:', url)
|
||||
|
||||
const isNumericProjectId = /^\d+$/.test(projectId)
|
||||
const fields: Record<string, any> = {
|
||||
project: {
|
||||
id: projectId,
|
||||
},
|
||||
project: isNumericProjectId ? { id: projectId } : { key: projectId },
|
||||
issuetype: {
|
||||
name: normalizedIssueType,
|
||||
},
|
||||
@@ -114,13 +115,31 @@ export async function POST(request: NextRequest) {
|
||||
fields.labels = labels
|
||||
}
|
||||
|
||||
if (
|
||||
components !== undefined &&
|
||||
components !== null &&
|
||||
Array.isArray(components) &&
|
||||
components.length > 0
|
||||
) {
|
||||
fields.components = components.map((name: string) => ({ name }))
|
||||
}
|
||||
|
||||
if (duedate !== undefined && duedate !== null && duedate !== '') {
|
||||
fields.duedate = duedate
|
||||
}
|
||||
|
||||
if (
|
||||
fixVersions !== undefined &&
|
||||
fixVersions !== null &&
|
||||
Array.isArray(fixVersions) &&
|
||||
fixVersions.length > 0
|
||||
) {
|
||||
fields.fixVersions = fixVersions.map((name: string) => ({ name }))
|
||||
}
|
||||
|
||||
if (reporter !== undefined && reporter !== null && reporter !== '') {
|
||||
fields.reporter = {
|
||||
id: reporter,
|
||||
accountId: reporter,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,8 +239,10 @@ export async function POST(request: NextRequest) {
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
id: responseData.id || '',
|
||||
issueKey: issueKey,
|
||||
summary: responseData.fields?.summary || 'Issue created',
|
||||
self: responseData.self || '',
|
||||
summary: responseData.fields?.summary || summary || 'Issue created',
|
||||
success: true,
|
||||
url: `https://${domain}/browse/${issueKey}`,
|
||||
...(assigneeId && { assigneeId }),
|
||||
|
||||
@@ -165,8 +165,26 @@ export async function POST(request: NextRequest) {
|
||||
issueIdOrKey,
|
||||
approvalId,
|
||||
decision,
|
||||
success: true,
|
||||
id: data.id ?? null,
|
||||
name: data.name ?? null,
|
||||
finalDecision: data.finalDecision ?? null,
|
||||
canAnswerApproval: data.canAnswerApproval ?? null,
|
||||
approvers: (data.approvers ?? []).map((a: Record<string, unknown>) => {
|
||||
const approver = a.approver as Record<string, unknown> | undefined
|
||||
return {
|
||||
approver: {
|
||||
accountId: approver?.accountId ?? null,
|
||||
displayName: approver?.displayName ?? null,
|
||||
emailAddress: approver?.emailAddress ?? null,
|
||||
active: approver?.active ?? null,
|
||||
},
|
||||
approverDecision: a.approverDecision ?? null,
|
||||
}
|
||||
}),
|
||||
createdDate: data.createdDate ?? null,
|
||||
completedDate: data.completedDate ?? null,
|
||||
approval: data,
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -95,6 +95,14 @@ export async function POST(request: NextRequest) {
|
||||
commentId: data.id,
|
||||
body: data.body,
|
||||
isPublic: data.public,
|
||||
author: data.author
|
||||
? {
|
||||
accountId: data.author.accountId ?? null,
|
||||
displayName: data.author.displayName ?? null,
|
||||
emailAddress: data.author.emailAddress ?? null,
|
||||
}
|
||||
: null,
|
||||
createdDate: data.created ?? null,
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -23,6 +23,7 @@ export async function POST(request: NextRequest) {
|
||||
issueIdOrKey,
|
||||
isPublic,
|
||||
internal,
|
||||
expand,
|
||||
start,
|
||||
limit,
|
||||
} = body
|
||||
@@ -57,8 +58,9 @@ export async function POST(request: NextRequest) {
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const params = new URLSearchParams()
|
||||
if (isPublic) params.append('public', isPublic)
|
||||
if (internal) params.append('internal', internal)
|
||||
if (isPublic !== undefined) params.append('public', String(isPublic))
|
||||
if (internal !== undefined) params.append('internal', String(internal))
|
||||
if (expand) params.append('expand', expand)
|
||||
if (start) params.append('start', start)
|
||||
if (limit) params.append('limit', limit)
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ export async function POST(request: NextRequest) {
|
||||
query,
|
||||
start,
|
||||
limit,
|
||||
accountIds,
|
||||
emails,
|
||||
} = body
|
||||
|
||||
@@ -56,24 +57,27 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const parsedEmails = emails
|
||||
? typeof emails === 'string'
|
||||
? emails
|
||||
const rawIds = accountIds || emails
|
||||
const parsedAccountIds = rawIds
|
||||
? typeof rawIds === 'string'
|
||||
? rawIds
|
||||
.split(',')
|
||||
.map((email: string) => email.trim())
|
||||
.filter((email: string) => email)
|
||||
: emails
|
||||
.map((id: string) => id.trim())
|
||||
.filter((id: string) => id)
|
||||
: Array.isArray(rawIds)
|
||||
? rawIds
|
||||
: []
|
||||
: []
|
||||
|
||||
const isAddOperation = parsedEmails.length > 0
|
||||
const isAddOperation = parsedAccountIds.length > 0
|
||||
|
||||
if (isAddOperation) {
|
||||
const url = `${baseUrl}/servicedesk/${serviceDeskId}/customer`
|
||||
|
||||
logger.info('Adding customers to:', url, { emails: parsedEmails })
|
||||
logger.info('Adding customers to:', url, { accountIds: parsedAccountIds })
|
||||
|
||||
const requestBody: Record<string, unknown> = {
|
||||
usernames: parsedEmails,
|
||||
accountIds: parsedAccountIds,
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
|
||||
@@ -31,6 +31,9 @@ export async function POST(request: NextRequest) {
|
||||
description,
|
||||
raiseOnBehalfOf,
|
||||
requestFieldValues,
|
||||
requestParticipants,
|
||||
channel,
|
||||
expand,
|
||||
} = body
|
||||
|
||||
if (!domain) {
|
||||
@@ -80,6 +83,19 @@ export async function POST(request: NextRequest) {
|
||||
if (raiseOnBehalfOf) {
|
||||
requestBody.raiseOnBehalfOf = raiseOnBehalfOf
|
||||
}
|
||||
if (requestParticipants) {
|
||||
requestBody.requestParticipants = Array.isArray(requestParticipants)
|
||||
? requestParticipants
|
||||
: typeof requestParticipants === 'string'
|
||||
? requestParticipants
|
||||
.split(',')
|
||||
.map((id: string) => id.trim())
|
||||
.filter(Boolean)
|
||||
: []
|
||||
}
|
||||
if (channel) {
|
||||
requestBody.channel = channel
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
@@ -111,6 +127,21 @@ export async function POST(request: NextRequest) {
|
||||
issueKey: data.issueKey,
|
||||
requestTypeId: data.requestTypeId,
|
||||
serviceDeskId: data.serviceDeskId,
|
||||
createdDate: data.createdDate ?? null,
|
||||
currentStatus: data.currentStatus
|
||||
? {
|
||||
status: data.currentStatus.status ?? null,
|
||||
statusCategory: data.currentStatus.statusCategory ?? null,
|
||||
statusDate: data.currentStatus.statusDate ?? null,
|
||||
}
|
||||
: null,
|
||||
reporter: data.reporter
|
||||
? {
|
||||
accountId: data.reporter.accountId ?? null,
|
||||
displayName: data.reporter.displayName ?? null,
|
||||
emailAddress: data.reporter.emailAddress ?? null,
|
||||
}
|
||||
: null,
|
||||
success: true,
|
||||
url: `https://${domain}/browse/${data.issueKey}`,
|
||||
},
|
||||
@@ -126,7 +157,10 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: issueIdOrKeyValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const url = `${baseUrl}/request/${issueIdOrKey}`
|
||||
const params = new URLSearchParams()
|
||||
if (expand) params.append('expand', expand)
|
||||
|
||||
const url = `${baseUrl}/request/${issueIdOrKey}${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching request from:', url)
|
||||
|
||||
@@ -155,6 +189,32 @@ export async function POST(request: NextRequest) {
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueId: data.issueId ?? null,
|
||||
issueKey: data.issueKey ?? null,
|
||||
requestTypeId: data.requestTypeId ?? null,
|
||||
serviceDeskId: data.serviceDeskId ?? null,
|
||||
createdDate: data.createdDate ?? null,
|
||||
currentStatus: data.currentStatus
|
||||
? {
|
||||
status: data.currentStatus.status ?? null,
|
||||
statusCategory: data.currentStatus.statusCategory ?? null,
|
||||
statusDate: data.currentStatus.statusDate ?? null,
|
||||
}
|
||||
: null,
|
||||
reporter: data.reporter
|
||||
? {
|
||||
accountId: data.reporter.accountId ?? null,
|
||||
displayName: data.reporter.displayName ?? null,
|
||||
emailAddress: data.reporter.emailAddress ?? null,
|
||||
active: data.reporter.active ?? true,
|
||||
}
|
||||
: null,
|
||||
requestFieldValues: (data.requestFieldValues ?? []).map((fv: Record<string, unknown>) => ({
|
||||
fieldId: fv.fieldId ?? null,
|
||||
label: fv.label ?? null,
|
||||
value: fv.value ?? null,
|
||||
})),
|
||||
url: `https://${domain}/browse/${data.issueKey}`,
|
||||
request: data,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import {
|
||||
validateAlphanumericId,
|
||||
validateEnum,
|
||||
validateJiraCloudId,
|
||||
} from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -23,7 +27,9 @@ export async function POST(request: NextRequest) {
|
||||
serviceDeskId,
|
||||
requestOwnership,
|
||||
requestStatus,
|
||||
requestTypeId,
|
||||
searchTerm,
|
||||
expand,
|
||||
start,
|
||||
limit,
|
||||
} = body
|
||||
@@ -52,17 +58,45 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
const VALID_REQUEST_OWNERSHIP = [
|
||||
'OWNED_REQUESTS',
|
||||
'PARTICIPATED_REQUESTS',
|
||||
'APPROVER',
|
||||
'ALL_REQUESTS',
|
||||
] as const
|
||||
const VALID_REQUEST_STATUS = ['OPEN_REQUESTS', 'CLOSED_REQUESTS', 'ALL_REQUESTS'] as const
|
||||
|
||||
if (requestOwnership) {
|
||||
const ownershipValidation = validateEnum(
|
||||
requestOwnership,
|
||||
VALID_REQUEST_OWNERSHIP,
|
||||
'requestOwnership'
|
||||
)
|
||||
if (!ownershipValidation.isValid) {
|
||||
return NextResponse.json({ error: ownershipValidation.error }, { status: 400 })
|
||||
}
|
||||
}
|
||||
|
||||
if (requestStatus) {
|
||||
const statusValidation = validateEnum(requestStatus, VALID_REQUEST_STATUS, 'requestStatus')
|
||||
if (!statusValidation.isValid) {
|
||||
return NextResponse.json({ error: statusValidation.error }, { status: 400 })
|
||||
}
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const params = new URLSearchParams()
|
||||
if (serviceDeskId) params.append('serviceDeskId', serviceDeskId)
|
||||
if (requestOwnership && requestOwnership !== 'ALL_REQUESTS') {
|
||||
if (requestOwnership) {
|
||||
params.append('requestOwnership', requestOwnership)
|
||||
}
|
||||
if (requestStatus && requestStatus !== 'ALL') {
|
||||
if (requestStatus) {
|
||||
params.append('requestStatus', requestStatus)
|
||||
}
|
||||
if (requestTypeId) params.append('requestTypeId', requestTypeId)
|
||||
if (searchTerm) params.append('searchTerm', searchTerm)
|
||||
if (expand) params.append('expand', expand)
|
||||
if (start) params.append('start', start)
|
||||
if (limit) params.append('limit', limit)
|
||||
|
||||
|
||||
119
apps/sim/app/api/tools/jsm/requesttypefields/route.ts
Normal file
119
apps/sim/app/api/tools/jsm/requesttypefields/route.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('JsmRequestTypeFieldsAPI')
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { domain, accessToken, cloudId: cloudIdParam, serviceDeskId, requestTypeId } = body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('Missing access token in request')
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!serviceDeskId) {
|
||||
logger.error('Missing serviceDeskId in request')
|
||||
return NextResponse.json({ error: 'Service Desk ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!requestTypeId) {
|
||||
logger.error('Missing requestTypeId in request')
|
||||
return NextResponse.json({ error: 'Request Type ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
|
||||
|
||||
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
|
||||
if (!cloudIdValidation.isValid) {
|
||||
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const serviceDeskIdValidation = validateAlphanumericId(serviceDeskId, 'serviceDeskId')
|
||||
if (!serviceDeskIdValidation.isValid) {
|
||||
return NextResponse.json({ error: serviceDeskIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const requestTypeIdValidation = validateAlphanumericId(requestTypeId, 'requestTypeId')
|
||||
if (!requestTypeIdValidation.isValid) {
|
||||
return NextResponse.json({ error: requestTypeIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
const url = `${baseUrl}/servicedesk/${serviceDeskId}/requesttype/${requestTypeId}/field`
|
||||
|
||||
logger.info('Fetching request type fields from:', url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: getJsmHeaders(accessToken),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('JSM API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `JSM API error: ${response.status} ${response.statusText}`, details: errorText },
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
serviceDeskId,
|
||||
requestTypeId,
|
||||
canAddRequestParticipants: data.canAddRequestParticipants ?? false,
|
||||
canRaiseOnBehalfOf: data.canRaiseOnBehalfOf ?? false,
|
||||
requestTypeFields: (data.requestTypeFields ?? []).map((field: Record<string, unknown>) => ({
|
||||
fieldId: field.fieldId ?? null,
|
||||
name: field.name ?? null,
|
||||
description: field.description ?? null,
|
||||
required: field.required ?? false,
|
||||
visible: field.visible ?? true,
|
||||
validValues: field.validValues ?? [],
|
||||
presetValues: field.presetValues ?? [],
|
||||
defaultValues: field.defaultValues ?? [],
|
||||
jiraSchema: field.jiraSchema ?? null,
|
||||
})),
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error fetching request type fields:', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : 'Internal server error',
|
||||
success: false,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,17 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { domain, accessToken, cloudId: cloudIdParam, serviceDeskId, start, limit } = body
|
||||
const {
|
||||
domain,
|
||||
accessToken,
|
||||
cloudId: cloudIdParam,
|
||||
serviceDeskId,
|
||||
searchQuery,
|
||||
groupId,
|
||||
expand,
|
||||
start,
|
||||
limit,
|
||||
} = body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
@@ -48,6 +58,9 @@ export async function POST(request: NextRequest) {
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const params = new URLSearchParams()
|
||||
if (searchQuery) params.append('searchQuery', searchQuery)
|
||||
if (groupId) params.append('groupId', groupId)
|
||||
if (expand) params.append('expand', expand)
|
||||
if (start) params.append('start', start)
|
||||
if (limit) params.append('limit', limit)
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { domain, accessToken, cloudId: cloudIdParam, start, limit } = body
|
||||
const { domain, accessToken, cloudId: cloudIdParam, expand, start, limit } = body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
@@ -38,6 +38,7 @@ export async function POST(request: NextRequest) {
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const params = new URLSearchParams()
|
||||
if (expand) params.append('expand', expand)
|
||||
if (start) params.append('start', start)
|
||||
if (limit) params.append('limit', limit)
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey } = body
|
||||
const { domain, accessToken, cloudId: cloudIdParam, issueIdOrKey, start, limit } = body
|
||||
|
||||
if (!domain) {
|
||||
logger.error('Missing domain in request')
|
||||
@@ -47,7 +47,11 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const baseUrl = getJsmApiBaseUrl(cloudId)
|
||||
|
||||
const url = `${baseUrl}/request/${issueIdOrKey}/transition`
|
||||
const params = new URLSearchParams()
|
||||
if (start) params.append('start', start)
|
||||
if (limit) params.append('limit', limit)
|
||||
|
||||
const url = `${baseUrl}/request/${issueIdOrKey}/transition${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching transitions from:', url)
|
||||
|
||||
@@ -78,6 +82,8 @@ export async function POST(request: NextRequest) {
|
||||
ts: new Date().toISOString(),
|
||||
issueIdOrKey,
|
||||
transitions: data.values || [],
|
||||
total: data.size || 0,
|
||||
isLastPage: data.isLastPage ?? true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
113
apps/sim/app/api/tools/onepassword/create-item/route.ts
Normal file
113
apps/sim/app/api/tools/onepassword/create-item/route.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import type { ItemCreateParams } from '@1password/sdk'
|
||||
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('OnePasswordCreateItemAPI')
|
||||
|
||||
const CreateItemSchema = 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'),
|
||||
category: z.string().min(1, 'Category is required'),
|
||||
title: z.string().nullish(),
|
||||
tags: z.string().nullish(),
|
||||
fields: 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 create-item attempt`)
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = CreateItemSchema.parse(body)
|
||||
const creds = resolveCredentials(params)
|
||||
|
||||
logger.info(`[${requestId}] Creating item in vault ${params.vaultId} (${creds.mode} mode)`)
|
||||
|
||||
if (creds.mode === 'service_account') {
|
||||
const client = await createOnePasswordClient(creds.serviceAccountToken!)
|
||||
|
||||
const parsedTags = params.tags
|
||||
? params.tags
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean)
|
||||
: undefined
|
||||
|
||||
const parsedFields = params.fields
|
||||
? (JSON.parse(params.fields) as Array<Record<string, any>>).map((f) => ({
|
||||
id: f.id || randomUUID().slice(0, 8),
|
||||
title: f.label || f.title || '',
|
||||
fieldType: toSdkFieldType(f.type || 'STRING'),
|
||||
value: f.value || '',
|
||||
sectionId: f.section?.id ?? f.sectionId,
|
||||
}))
|
||||
: undefined
|
||||
|
||||
const item = await client.items.create({
|
||||
vaultId: params.vaultId,
|
||||
category: toSdkCategory(params.category),
|
||||
title: params.title || '',
|
||||
tags: parsedTags,
|
||||
fields: parsedFields,
|
||||
} as ItemCreateParams)
|
||||
|
||||
return NextResponse.json(normalizeSdkItem(item))
|
||||
}
|
||||
|
||||
const connectBody: Record<string, unknown> = {
|
||||
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 })
|
||||
}
|
||||
}
|
||||
70
apps/sim/app/api/tools/onepassword/delete-item/route.ts
Normal file
70
apps/sim/app/api/tools/onepassword/delete-item/route.ts
Normal file
@@ -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<string, string>).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 })
|
||||
}
|
||||
}
|
||||
75
apps/sim/app/api/tools/onepassword/get-item/route.ts
Normal file
75
apps/sim/app/api/tools/onepassword/get-item/route.ts
Normal file
@@ -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 })
|
||||
}
|
||||
}
|
||||
78
apps/sim/app/api/tools/onepassword/get-vault/route.ts
Normal file
78
apps/sim/app/api/tools/onepassword/get-vault/route.ts
Normal file
@@ -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 })
|
||||
}
|
||||
}
|
||||
87
apps/sim/app/api/tools/onepassword/list-items/route.ts
Normal file
87
apps/sim/app/api/tools/onepassword/list-items/route.ts
Normal file
@@ -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 })
|
||||
}
|
||||
}
|
||||
85
apps/sim/app/api/tools/onepassword/list-vaults/route.ts
Normal file
85
apps/sim/app/api/tools/onepassword/list-vaults/route.ts
Normal file
@@ -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 })
|
||||
}
|
||||
}
|
||||
117
apps/sim/app/api/tools/onepassword/replace-item/route.ts
Normal file
117
apps/sim/app/api/tools/onepassword/replace-item/route.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import type { Item } from '@1password/sdk'
|
||||
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 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<Record<string, any>>).map((f) => ({
|
||||
id: f.id || randomUUID().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<Record<string, any>>).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<string, any>) => ({
|
||||
url: u.href || u.url || '',
|
||||
label: u.label || '',
|
||||
autofillBehavior: 'AnywhereOnWebsite' as const,
|
||||
}))
|
||||
: existing.websites,
|
||||
} as Item
|
||||
|
||||
const result = await client.items.put(sdkItem)
|
||||
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 })
|
||||
}
|
||||
}
|
||||
59
apps/sim/app/api/tools/onepassword/resolve-secret/route.ts
Normal file
59
apps/sim/app/api/tools/onepassword/resolve-secret/route.ts
Normal file
@@ -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({
|
||||
value: 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 })
|
||||
}
|
||||
}
|
||||
136
apps/sim/app/api/tools/onepassword/update-item/route.ts
Normal file
136
apps/sim/app/api/tools/onepassword/update-item/route.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
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 JsonPatchOperation[]
|
||||
|
||||
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!)
|
||||
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
||||
interface JsonPatchOperation {
|
||||
op: 'add' | 'remove' | 'replace'
|
||||
path: string
|
||||
value?: unknown
|
||||
}
|
||||
|
||||
/** Apply a single RFC6902 JSON Patch operation to a mutable object. */
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function applyPatch(item: Record<string, any>, op: JsonPatchOperation) {
|
||||
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
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
}
|
||||
}
|
||||
357
apps/sim/app/api/tools/onepassword/utils.ts
Normal file
357
apps/sim/app/api/tools/onepassword/utils.ts
Normal file
@@ -0,0 +1,357 @@
|
||||
import type {
|
||||
Item,
|
||||
ItemCategory,
|
||||
ItemField,
|
||||
ItemFieldType,
|
||||
ItemOverview,
|
||||
ItemSection,
|
||||
VaultOverview,
|
||||
Website,
|
||||
} from '@1password/sdk'
|
||||
|
||||
/** Connect-format field type strings returned by normalization. */
|
||||
type ConnectFieldType =
|
||||
| 'STRING'
|
||||
| 'CONCEALED'
|
||||
| 'EMAIL'
|
||||
| 'URL'
|
||||
| 'OTP'
|
||||
| 'PHONE'
|
||||
| 'DATE'
|
||||
| 'MONTH_YEAR'
|
||||
| 'MENU'
|
||||
| 'ADDRESS'
|
||||
| 'REFERENCE'
|
||||
| 'SSHKEY'
|
||||
| 'CREDIT_CARD_NUMBER'
|
||||
| 'CREDIT_CARD_TYPE'
|
||||
|
||||
/** Connect-format category strings returned by normalization. */
|
||||
type ConnectCategory =
|
||||
| 'LOGIN'
|
||||
| 'PASSWORD'
|
||||
| 'API_CREDENTIAL'
|
||||
| 'SECURE_NOTE'
|
||||
| 'SERVER'
|
||||
| 'DATABASE'
|
||||
| 'CREDIT_CARD'
|
||||
| 'IDENTITY'
|
||||
| 'SSH_KEY'
|
||||
| 'DOCUMENT'
|
||||
| 'SOFTWARE_LICENSE'
|
||||
| 'EMAIL_ACCOUNT'
|
||||
| 'MEMBERSHIP'
|
||||
| 'PASSPORT'
|
||||
| 'REWARD_PROGRAM'
|
||||
| 'DRIVER_LICENSE'
|
||||
| 'BANK_ACCOUNT'
|
||||
| 'MEDICAL_RECORD'
|
||||
| 'OUTDOOR_LICENSE'
|
||||
| 'WIRELESS_ROUTER'
|
||||
| 'SOCIAL_SECURITY_NUMBER'
|
||||
| 'CUSTOM'
|
||||
|
||||
/** Normalized vault shape matching the Connect API response. */
|
||||
export interface NormalizedVault {
|
||||
id: string
|
||||
name: string
|
||||
description: null
|
||||
attributeVersion: number
|
||||
contentVersion: number
|
||||
items: number
|
||||
type: string
|
||||
createdAt: string | null
|
||||
updatedAt: string | null
|
||||
}
|
||||
|
||||
/** Normalized item overview shape matching the Connect API response. */
|
||||
export interface NormalizedItemOverview {
|
||||
id: string
|
||||
title: string
|
||||
vault: { id: string }
|
||||
category: ConnectCategory
|
||||
urls: Array<{ href: string; label: string | null; primary: boolean }>
|
||||
favorite: boolean
|
||||
tags: string[]
|
||||
version: number
|
||||
state: string | null
|
||||
createdAt: string | null
|
||||
updatedAt: string | null
|
||||
lastEditedBy: null
|
||||
}
|
||||
|
||||
/** Normalized field shape matching the Connect API response. */
|
||||
export interface NormalizedField {
|
||||
id: string
|
||||
label: string
|
||||
type: ConnectFieldType
|
||||
purpose: string
|
||||
value: string | null
|
||||
section: { id: string } | null
|
||||
generate: boolean
|
||||
recipe: null
|
||||
entropy: null
|
||||
}
|
||||
|
||||
/** Normalized full item shape matching the Connect API response. */
|
||||
export interface NormalizedItem extends NormalizedItemOverview {
|
||||
fields: NormalizedField[]
|
||||
sections: Array<{ id: string; label: string }>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<string, ConnectFieldType> = {
|
||||
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<string, ConnectCategory> = {
|
||||
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<string, `${ItemCategory}`> = {
|
||||
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, `${ItemFieldType}`> = {
|
||||
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 | null
|
||||
serviceAccountToken?: string | null
|
||||
serverUrl?: string | null
|
||||
apiKey?: string | null
|
||||
}
|
||||
|
||||
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<Response> {
|
||||
const base = options.serverUrl.replace(/\/$/, '')
|
||||
const queryStr = options.query ? `?${options.query}` : ''
|
||||
const url = `${base}${options.path}${queryStr}`
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${options.apiKey}`,
|
||||
}
|
||||
|
||||
if (options.body) {
|
||||
headers['Content-Type'] = 'application/json'
|
||||
}
|
||||
|
||||
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: VaultOverview): NormalizedVault {
|
||||
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: ItemOverview): NormalizedItemOverview {
|
||||
return {
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
vault: { id: item.vaultId },
|
||||
category: SDK_TO_CONNECT_CATEGORY[item.category] ?? 'CUSTOM',
|
||||
urls: (item.websites ?? []).map((w: Website) => ({
|
||||
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: Item): NormalizedItem {
|
||||
return {
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
vault: { id: item.vaultId },
|
||||
category: SDK_TO_CONNECT_CATEGORY[item.category] ?? 'CUSTOM',
|
||||
urls: (item.websites ?? []).map((w: Website) => ({
|
||||
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: ItemField) => ({
|
||||
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: ItemSection) => ({
|
||||
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): `${ItemCategory}` {
|
||||
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): `${ItemFieldType}` {
|
||||
return CONNECT_TO_SDK_FIELD_TYPE[type] ?? 'Text'
|
||||
}
|
||||
Reference in New Issue
Block a user