diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 4750caa45d..6f5df2b76b 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -961,6 +961,24 @@ export function PeopleDataLabsIcon(props: SVGProps) { ) } +export function PersonaIcon(props: SVGProps) { + return ( + + + + ) +} + export function PerplexityIcon(props: SVGProps) { return ( diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index a0e3bf30e6..7a8e927a2c 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -145,6 +145,7 @@ import { ParallelIcon, PeopleDataLabsIcon, PerplexityIcon, + PersonaIcon, PineconeIcon, PipedriveIcon, PolymarketIcon, @@ -379,6 +380,7 @@ export const blockTypeToIconMap: Record = { parallel_ai: ParallelIcon, peopledatalabs: PeopleDataLabsIcon, perplexity: PerplexityIcon, + persona: PersonaIcon, pinecone: PineconeIcon, pipedrive: PipedriveIcon, polymarket: PolymarketIcon, diff --git a/apps/docs/content/docs/en/integrations/meta.json b/apps/docs/content/docs/en/integrations/meta.json index 8b89b0bd92..dd4558a99c 100644 --- a/apps/docs/content/docs/en/integrations/meta.json +++ b/apps/docs/content/docs/en/integrations/meta.json @@ -145,6 +145,7 @@ "parallel_ai", "peopledatalabs", "perplexity", + "persona", "pinecone", "pipedrive", "polymarket", diff --git a/apps/docs/content/docs/en/integrations/persona.mdx b/apps/docs/content/docs/en/integrations/persona.mdx new file mode 100644 index 0000000000..4cf1dc8379 --- /dev/null +++ b/apps/docs/content/docs/en/integrations/persona.mdx @@ -0,0 +1,527 @@ +--- +title: Persona +description: Verify identities with Persona +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[Persona](https://withpersona.com/) is an identity verification platform that helps businesses verify who their users are. Persona handles the full identity lifecycle — collecting government IDs, selfies, and documents through hosted verification flows, screening individuals against global watchlists and sanctions lists, and routing edge cases to human review. + +With Persona, you can: + +- **Verify identities end to end**: Create inquiries from your verification templates, send customers one-time verification links, and read back collected fields and decision results +- **Automate KYC and compliance decisions**: Approve or decline inquiries programmatically, run watchlist, adverse media, and politically-exposed-person screening reports, and monitor cases that need manual review +- **Manage your verified user base**: Create and look up accounts, bulk-import existing users from CSV, and keep your user model in sync with Persona via reference IDs +- **Keep an audit trail**: Download inquiry summary PDFs and retrieve the underlying verifications and documents behind every decision + +In Sim, the Persona block lets your agents drive identity verification as part of real workflows. Trigger verification when a customer signs up, route on approval status, screen names against watchlists before activating accounts, post pending reviews to Slack, or archive verification PDFs to cloud storage — all using your Persona API key and templates. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrate Persona identity verification into the workflow. Manage the full inquiry lifecycle (create, update, approve, decline, review, resume, expire, redact), generate one-time verification links and PDF summaries, manage accounts including CSV bulk import, run watchlist and adverse media reports, review cases, retrieve verifications and documents, and discover inquiry templates. + + + +## Actions + +### `persona_create_inquiry` + +Create a new identity verification inquiry from an inquiry template. Returns the created inquiry, which can then be completed by the individual via a one-time link. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Persona API key | +| `inquiryTemplateId` | string | Yes | Inquiry template ID \(starts with itmpl_\), inquiry template version ID \(starts with itmplv_\), or legacy template ID \(starts with tmpl_\) | +| `accountId` | string | No | Account ID \(starts with act_\) to associate with this inquiry | +| `referenceId` | string | No | Reference ID that refers to an entity in your user model. An account is auto-created for it if one does not exist. | +| `fields` | json | No | JSON object of field name to field value pairs to pre-fill, as defined by the inquiry template \(e.g. \{"name-first": "Jane"\}\) | +| `note` | string | No | Free-form note to attach to the inquiry | +| `redirectUri` | string | No | URI to redirect the individual to after completing the inquiry flow | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `inquiry` | object | The created inquiry | + +### `persona_get_inquiry` + +Retrieve a single identity verification inquiry by ID, including its status, collected fields, and decision timestamps. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Persona API key | +| `inquiryId` | string | Yes | Inquiry ID to retrieve \(starts with inq_\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `inquiry` | object | The retrieved inquiry | + +### `persona_list_inquiries` + +List identity verification inquiries, optionally filtered by status, account ID, reference ID, or creation date range. Results are cursor-paginated. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Persona API key | +| `status` | string | No | Filter by inquiry status \(created, pending, completed, failed, expired, needs_review, approved, declined\) | +| `accountId` | string | No | Filter by account ID \(starts with act_\); comma-separate multiple IDs | +| `referenceId` | string | No | Filter by reference ID | +| `createdAtStart` | string | No | Filter to inquiries created at or after this ISO 8601 timestamp | +| `createdAtEnd` | string | No | Filter to inquiries created at or before this ISO 8601 timestamp | +| `pageSize` | number | No | Number of inquiries to return per page \(1-100, default 10\) | +| `pageAfter` | string | No | Pagination cursor: return inquiries after this inquiry ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `inquiries` | array | Inquiries matching the filters | +| `nextCursor` | string | Cursor for the next page \(pass as pageAfter\), or null on the last page | + +### `persona_update_inquiry` + +Update an inquiry’s note, fields, tags, or redirect URI. Only the provided values are changed. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Persona API key | +| `inquiryId` | string | Yes | Inquiry ID to update \(starts with inq_\) | +| `note` | string | No | Free-form note to set on the inquiry | +| `fields` | json | No | JSON object of field name to field value pairs to set, as defined by the inquiry template \(e.g. \{"name-first": "Jane"\}\) | +| `tags` | array | No | JSON array of tag names to set on the inquiry \(e.g. \["vip"\]\) | +| `redirectUri` | string | No | URI to redirect the individual to after completing the inquiry flow | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `inquiry` | object | The updated inquiry | + +### `persona_approve_inquiry` + +Approve an identity verification inquiry. Approving prevents further progress on the inquiry and triggers any associated workflows and webhooks. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Persona API key | +| `inquiryId` | string | Yes | Inquiry ID to approve \(starts with inq_\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `inquiry` | object | The approved inquiry | + +### `persona_decline_inquiry` + +Decline an identity verification inquiry. Declining prevents further progress on the inquiry and triggers any associated workflows and webhooks. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Persona API key | +| `inquiryId` | string | Yes | Inquiry ID to decline \(starts with inq_\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `inquiry` | object | The declined inquiry | + +### `persona_mark_inquiry_for_review` + +Mark an identity verification inquiry for manual review, moving it to the needs_review status. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Persona API key | +| `inquiryId` | string | Yes | Inquiry ID to mark for review \(starts with inq_\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `inquiry` | object | The inquiry marked for review | + +### `persona_resume_inquiry` + +Resume a pending or expired inquiry, creating a new session so the individual can continue verification. Returns a session token. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Persona API key | +| `inquiryId` | string | Yes | Inquiry ID to resume \(starts with inq_\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `inquiry` | object | The resumed inquiry | +| `sessionToken` | string | Session token for the new inquiry session, used to continue the flow in embedded SDKs | + +### `persona_expire_inquiry` + +Expire an in-progress inquiry, invalidating its sessions and one-time links so the individual can no longer continue it. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Persona API key | +| `inquiryId` | string | Yes | Inquiry ID to expire \(starts with inq_\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `inquiry` | object | The expired inquiry | + +### `persona_generate_inquiry_link` + +Generate a one-time link for an inquiry that the individual can open to complete their identity verification. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Persona API key | +| `inquiryId` | string | Yes | Inquiry ID to generate a one-time link for \(starts with inq_\) | +| `expiresInSeconds` | number | No | Number of seconds from now until the link expires \(must be greater than 0; defaults to the inquiry template setting, typically 24 hours\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `inquiry` | object | The inquiry the link was generated for | +| `oneTimeLink` | string | One-time link the individual can open to complete the inquiry | +| `oneTimeLinkShort` | string | Shortened version of the one-time link | + +### `persona_print_inquiry_pdf` + +Download a PDF summary of an inquiry, including its collected information and verification results. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Persona API key | +| `inquiryId` | string | Yes | Inquiry ID to print \(starts with inq_\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `file` | file | PDF summary of the inquiry, stored in execution files | + +### `persona_redact_inquiry` + +Permanently delete all personally identifiable information collected by an inquiry, for example to honor a data deletion request. This cannot be undone. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Persona API key | +| `inquiryId` | string | Yes | Inquiry ID to redact \(starts with inq_\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `inquiry` | object | The redacted inquiry \(PII fields are removed\) | + +### `persona_create_account` + +Create an account that represents an individual in Persona. Accounts consolidate inquiries, verifications, and reports for the same person. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Persona API key | +| `accountTypeId` | string | No | Account type ID to create the account for \(starts with acttp_\); defaults to your organization default | +| `referenceId` | string | No | Reference ID that refers to an entity in your user model | +| `countryCode` | string | No | ISO 3166-1 alpha-2 country code \(e.g. US\) | +| `fields` | json | No | JSON object of field name to field value pairs, as defined by the account type \(e.g. \{"name-first": "Jane"\}\) | +| `tags` | array | No | JSON array of tag names to associate with the account \(e.g. \["vip"\]\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `account` | object | The created account | + +### `persona_get_account` + +Retrieve a single account by ID, including its reference ID, fields, tags, and status. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Persona API key | +| `accountId` | string | Yes | Account ID to retrieve \(starts with act_\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `account` | object | The retrieved account | + +### `persona_list_accounts` + +List accounts in your Persona organization, optionally filtered by reference ID. Results are cursor-paginated. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Persona API key | +| `referenceId` | string | No | Filter by reference ID | +| `pageSize` | number | No | Number of accounts to return per page \(1-100, default 10\) | +| `pageAfter` | string | No | Pagination cursor: return accounts after this account ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `accounts` | array | Accounts matching the filters | +| `nextCursor` | string | Cursor for the next page \(pass as pageAfter\), or null on the last page | + +### `persona_update_account` + +Update an account’s reference ID, country code, fields, or tags. Only the provided values are changed. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Persona API key | +| `accountId` | string | Yes | Account ID to update \(starts with act_\) | +| `referenceId` | string | No | Reference ID that refers to an entity in your user model | +| `countryCode` | string | No | ISO 3166-1 alpha-2 country code \(e.g. US\) | +| `fields` | json | No | JSON object of field name to field value pairs to set, as defined by the account type \(e.g. \{"name-first": "Jane"\}\) | +| `tags` | array | No | JSON array of tag names to set on the account \(e.g. \["vip"\]\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `account` | object | The updated account | + +### `persona_import_accounts` + +Bulk-import accounts into Persona from a CSV file. Returns an importer whose status can be polled until processing completes. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Persona API key | +| `file` | file | Yes | CSV file of accounts to import | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `importer` | object | The created account importer | + +### `persona_redact_account` + +Permanently delete all personally identifiable information stored on an account, for example to honor a data deletion request. This cannot be undone. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Persona API key | +| `accountId` | string | Yes | Account ID to redact \(starts with act_\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `account` | object | The redacted account \(PII fields are removed\) | + +### `persona_list_cases` + +List manual review cases, optionally filtered by status, account ID, or reference ID. Results are cursor-paginated. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Persona API key | +| `status` | string | No | Filter by case status \(e.g. Open, Resolved\) | +| `accountId` | string | No | Filter by account ID \(starts with act_\) | +| `referenceId` | string | No | Filter by reference ID | +| `pageSize` | number | No | Number of cases to return per page \(1-100, default 10\) | +| `pageAfter` | string | No | Pagination cursor: return cases after this case ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `cases` | array | Cases matching the filters | +| `nextCursor` | string | Cursor for the next page \(pass as pageAfter\), or null on the last page | + +### `persona_get_case` + +Retrieve a single manual review case by ID, including its status, resolution, and assignee. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Persona API key | +| `caseId` | string | Yes | Case ID to retrieve \(starts with case_\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `case` | object | The retrieved case | + +### `persona_create_report` + +Run a screening report (watchlist, adverse media, or politically exposed person) against an individual by name or search term. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Persona API key | +| `reportType` | string | Yes | Type of report to run: watchlist, adverse-media, or politically-exposed-person | +| `reportTemplateId` | string | Yes | Report template ID to run \(starts with rptp_\) | +| `term` | string | No | Full-name search term \(e.g. "Jane Q Doe"\). Provide this or the separate name parts. | +| `nameFirst` | string | No | First name of the individual to search | +| `nameMiddle` | string | No | Middle name of the individual to search | +| `nameLast` | string | No | Last name of the individual to search | +| `birthdate` | string | No | Birthdate of the individual, formatted as YYYY-MM-DD | +| `countryCode` | string | No | ISO 3166-1 alpha-2 country code \(e.g. US\) | +| `accountId` | string | No | Account ID \(starts with act_\) to associate with this report | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `report` | object | The created report. Reports run asynchronously; poll until status is ready. | + +### `persona_get_report` + +Retrieve a single screening report by ID, including its status, match results, and full type-specific attributes. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Persona API key | +| `reportId` | string | Yes | Report ID to retrieve \(starts with rep_\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `report` | object | The retrieved report | + +### `persona_list_reports` + +List screening reports, optionally filtered by account ID or reference ID. Results are cursor-paginated. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Persona API key | +| `accountId` | string | No | Filter by account ID \(starts with act_\) | +| `referenceId` | string | No | Filter by reference ID | +| `pageSize` | number | No | Number of reports to return per page \(1-100, default 10\) | +| `pageAfter` | string | No | Pagination cursor: return reports after this report ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `reports` | array | Reports matching the filters | +| `nextCursor` | string | Cursor for the next page \(pass as pageAfter\), or null on the last page | + +### `persona_get_verification` + +Retrieve a single verification by ID (government ID, selfie, document, database, and more), including its status and the checks that ran. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Persona API key | +| `verificationId` | string | Yes | Verification ID to retrieve \(starts with ver_\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `verification` | object | The retrieved verification | + +### `persona_get_document` + +Retrieve a single document by ID (government ID, generic document, and more), including its processing status and uploaded files. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Persona API key | +| `documentId` | string | Yes | Document ID to retrieve \(starts with doc_\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `document` | object | The retrieved document | + +### `persona_list_inquiry_templates` + +List the inquiry templates in your Persona organization, to discover template IDs for creating inquiries. Results are cursor-paginated. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Persona API key | +| `pageSize` | number | No | Number of templates to return per page \(1-100, default 10\) | +| `pageAfter` | string | No | Pagination cursor: return templates after this template ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `inquiryTemplates` | array | Inquiry templates in the organization | +| `nextCursor` | string | Cursor for the next page \(pass as pageAfter\), or null on the last page | + + diff --git a/apps/sim/app/api/tools/persona/import-accounts/route.ts b/apps/sim/app/api/tools/persona/import-accounts/route.ts new file mode 100644 index 0000000000..8837881b99 --- /dev/null +++ b/apps/sim/app/api/tools/persona/import-accounts/route.ts @@ -0,0 +1,130 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { personaImportAccountsContract } from '@/lib/api/contracts/tools/persona' +import { parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' +import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess, FileAccessDeniedError } from '@/app/api/files/authorization' +import { + buildPersonaHeaders, + extractPersonaErrorMessage, + mapImporter, + PERSONA_API_BASE, +} from '@/tools/persona/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('PersonaImportAccountsAPI') + +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + + if (!authResult.success || !authResult.userId) { + logger.warn(`[${requestId}] Unauthorized Persona import attempt`, { + error: authResult.error || 'Missing userId', + }) + return NextResponse.json( + { success: false, error: authResult.error || 'Unauthorized' }, + { status: 401 } + ) + } + + const userId = authResult.userId + + const parsed = await parseRequest(personaImportAccountsContract, request, {}) + if (!parsed.success) return parsed.response + + const { apiKey, file } = parsed.data.body + + const userFiles = processFilesToUserFiles([file], requestId, logger) + if (userFiles.length === 0) { + return NextResponse.json( + { success: false, error: 'Invalid file input: a stored CSV file is required' }, + { status: 400 } + ) + } + + const userFile = userFiles[0] + const denied = await assertToolFileAccess(userFile.key, userId, requestId, logger) + if (denied) return denied + + const buffer = await downloadFileFromStorage(userFile, requestId, logger) + + logger.info(`[${requestId}] Importing accounts into Persona`, { + fileName: userFile.name, + fileSize: buffer.length, + userId, + }) + + const personaResponse = await fetch(`${PERSONA_API_BASE}/importer/accounts`, { + method: 'POST', + headers: buildPersonaHeaders(apiKey), + body: JSON.stringify({ + data: { + attributes: { + file: { + data: buffer.toString('base64'), + filename: userFile.name, + }, + }, + }, + }), + }) + + const personaData = await personaResponse.json().catch(() => null) + + if (!personaResponse.ok) { + const personaError = extractPersonaErrorMessage( + personaData, + `Persona API error: ${personaResponse.statusText}` + ) + logger.error(`[${requestId}] Persona import accounts failed`, { + status: personaResponse.status, + error: personaError, + }) + return NextResponse.json( + { success: false, error: personaError }, + { status: personaResponse.status } + ) + } + + const importer = mapImporter(personaData?.data ?? {}) + if (!importer.id) { + logger.error(`[${requestId}] Persona import accounts returned an unexpected response body`, { + status: personaResponse.status, + }) + return NextResponse.json( + { success: false, error: 'Persona returned an unexpected response for the account import' }, + { status: 502 } + ) + } + + logger.info(`[${requestId}] Persona account import created`, { + importerId: importer.id, + }) + + return NextResponse.json({ + success: true, + output: { + importer, + }, + }) + } catch (error) { + if (error instanceof FileAccessDeniedError) { + return NextResponse.json({ success: false, error: 'File not found' }, { status: 404 }) + } + + logger.error(`[${requestId}] Error importing accounts into Persona:`, error) + return NextResponse.json( + { success: false, error: getErrorMessage(error, 'Internal server error') }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/blocks/blocks/persona.ts b/apps/sim/blocks/blocks/persona.ts new file mode 100644 index 0000000000..b8d1d832c4 --- /dev/null +++ b/apps/sim/blocks/blocks/persona.ts @@ -0,0 +1,690 @@ +import { PersonaIcon } from '@/components/icons' +import { AuthMode, type BlockConfig, type BlockMeta, IntegrationType } from '@/blocks/types' +import { normalizeFileInput } from '@/blocks/utils' +import type { PersonaResponse } from '@/tools/persona/types' + +export const PersonaBlock: BlockConfig = { + type: 'persona', + name: 'Persona', + description: 'Verify identities with Persona', + longDescription: + 'Integrate Persona identity verification into the workflow. Manage the full inquiry lifecycle (create, update, approve, decline, review, resume, expire, redact), generate one-time verification links and PDF summaries, manage accounts including CSV bulk import, run watchlist and adverse media reports, review cases, retrieve verifications and documents, and discover inquiry templates.', + docsLink: 'https://docs.sim.ai/integrations/persona', + category: 'tools', + integrationType: IntegrationType.Security, + bgColor: '#FFFFFF', + icon: PersonaIcon, + authMode: AuthMode.ApiKey, + + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Create Inquiry', id: 'create_inquiry' }, + { label: 'Get Inquiry', id: 'get_inquiry' }, + { label: 'List Inquiries', id: 'list_inquiries' }, + { label: 'Update Inquiry', id: 'update_inquiry' }, + { label: 'Approve Inquiry', id: 'approve_inquiry' }, + { label: 'Decline Inquiry', id: 'decline_inquiry' }, + { label: 'Mark Inquiry for Review', id: 'mark_inquiry_for_review' }, + { label: 'Resume Inquiry', id: 'resume_inquiry' }, + { label: 'Expire Inquiry', id: 'expire_inquiry' }, + { label: 'Generate Inquiry Link', id: 'generate_inquiry_link' }, + { label: 'Print Inquiry PDF', id: 'print_inquiry_pdf' }, + { label: 'Redact Inquiry', id: 'redact_inquiry' }, + { label: 'Create Account', id: 'create_account' }, + { label: 'Get Account', id: 'get_account' }, + { label: 'List Accounts', id: 'list_accounts' }, + { label: 'Update Account', id: 'update_account' }, + { label: 'Import Accounts (CSV)', id: 'import_accounts' }, + { label: 'Redact Account', id: 'redact_account' }, + { label: 'List Cases', id: 'list_cases' }, + { label: 'Get Case', id: 'get_case' }, + { label: 'Create Report', id: 'create_report' }, + { label: 'Get Report', id: 'get_report' }, + { label: 'List Reports', id: 'list_reports' }, + { label: 'Get Verification', id: 'get_verification' }, + { label: 'Get Document', id: 'get_document' }, + { label: 'List Inquiry Templates', id: 'list_inquiry_templates' }, + ], + value: () => 'create_inquiry', + }, + { + id: 'inquiryTemplateId', + title: 'Inquiry Template ID', + type: 'short-input', + placeholder: 'itmpl_ABC123', + condition: { field: 'operation', value: 'create_inquiry' }, + required: true, + }, + { + id: 'inquiryId', + title: 'Inquiry ID', + type: 'short-input', + placeholder: 'inq_ABC123', + condition: { + field: 'operation', + value: [ + 'get_inquiry', + 'update_inquiry', + 'approve_inquiry', + 'decline_inquiry', + 'mark_inquiry_for_review', + 'resume_inquiry', + 'expire_inquiry', + 'generate_inquiry_link', + 'print_inquiry_pdf', + 'redact_inquiry', + ], + }, + required: { + field: 'operation', + value: [ + 'get_inquiry', + 'update_inquiry', + 'approve_inquiry', + 'decline_inquiry', + 'mark_inquiry_for_review', + 'resume_inquiry', + 'expire_inquiry', + 'generate_inquiry_link', + 'print_inquiry_pdf', + 'redact_inquiry', + ], + }, + }, + { + id: 'accountId', + title: 'Account ID', + type: 'short-input', + placeholder: 'act_ABC123', + condition: { + field: 'operation', + value: [ + 'create_inquiry', + 'get_account', + 'update_account', + 'redact_account', + 'list_inquiries', + 'list_cases', + 'create_report', + 'list_reports', + ], + }, + required: { + field: 'operation', + value: ['get_account', 'update_account', 'redact_account'], + }, + }, + { + id: 'referenceId', + title: 'Reference ID', + type: 'short-input', + placeholder: 'ID of this user in your system', + condition: { + field: 'operation', + value: [ + 'create_inquiry', + 'create_account', + 'update_account', + 'list_inquiries', + 'list_accounts', + 'list_cases', + 'list_reports', + ], + }, + }, + { + id: 'fields', + title: 'Fields', + type: 'long-input', + placeholder: '{"name-first": "Jane", "name-last": "Doe"}', + condition: { + field: 'operation', + value: ['create_inquiry', 'update_inquiry', 'create_account', 'update_account'], + }, + wandConfig: { + enabled: true, + prompt: + 'Generate a JSON object of Persona field name to field value pairs (e.g. {"name-first": "Jane", "name-last": "Doe", "email-address": "jane@example.com"}). Field names are defined by the inquiry template or account type. Return ONLY the JSON object.', + generationType: 'json-object', + placeholder: 'Describe the fields to pre-fill...', + }, + }, + { + id: 'note', + title: 'Note', + type: 'short-input', + placeholder: 'Free-form note for the inquiry', + mode: 'advanced', + condition: { field: 'operation', value: ['create_inquiry', 'update_inquiry'] }, + }, + { + id: 'redirectUri', + title: 'Redirect URI', + type: 'short-input', + placeholder: 'https://example.com/verified', + mode: 'advanced', + condition: { field: 'operation', value: ['create_inquiry', 'update_inquiry'] }, + }, + { + id: 'expiresInSeconds', + title: 'Link Expiry (Seconds)', + type: 'short-input', + placeholder: '3600', + mode: 'advanced', + condition: { field: 'operation', value: 'generate_inquiry_link' }, + }, + { + id: 'accountTypeId', + title: 'Account Type ID', + type: 'short-input', + placeholder: 'acttp_ABC123', + mode: 'advanced', + condition: { field: 'operation', value: 'create_account' }, + }, + { + id: 'tags', + title: 'Tags', + type: 'long-input', + placeholder: '["vip", "beta-user"]', + mode: 'advanced', + condition: { + field: 'operation', + value: ['create_account', 'update_account', 'update_inquiry'], + }, + wandConfig: { + enabled: true, + prompt: + 'Generate a JSON array of tag name strings (e.g. ["vip", "beta-user"]). The output must be a JSON array, not an object. Return ONLY the JSON array.', + placeholder: 'Describe the tags to apply...', + }, + }, + { + id: 'importFile', + title: 'CSV File', + type: 'file-upload', + canonicalParamId: 'file', + acceptedTypes: 'text/csv', + placeholder: 'Upload a CSV of accounts to import', + mode: 'basic', + multiple: false, + condition: { field: 'operation', value: 'import_accounts' }, + required: true, + }, + { + id: 'importFileRef', + title: 'CSV File Reference', + type: 'short-input', + canonicalParamId: 'file', + placeholder: 'Reference a CSV file from a previous block', + mode: 'advanced', + condition: { field: 'operation', value: 'import_accounts' }, + required: true, + }, + { + id: 'status', + title: 'Status Filter', + type: 'short-input', + placeholder: 'e.g. approved (inquiries) or Open (cases)', + condition: { field: 'operation', value: ['list_inquiries', 'list_cases'] }, + }, + { + id: 'createdAtStart', + title: 'Created After', + type: 'short-input', + placeholder: '2024-01-01T00:00:00Z', + mode: 'advanced', + condition: { field: 'operation', value: 'list_inquiries' }, + wandConfig: { + enabled: true, + prompt: 'Generate an ISO 8601 timestamp. Return ONLY the timestamp string.', + generationType: 'timestamp', + placeholder: 'Describe the start of the date range...', + }, + }, + { + id: 'createdAtEnd', + title: 'Created Before', + type: 'short-input', + placeholder: '2024-12-31T23:59:59Z', + mode: 'advanced', + condition: { field: 'operation', value: 'list_inquiries' }, + wandConfig: { + enabled: true, + prompt: 'Generate an ISO 8601 timestamp. Return ONLY the timestamp string.', + generationType: 'timestamp', + placeholder: 'Describe the end of the date range...', + }, + }, + { + id: 'pageSize', + title: 'Page Size', + type: 'short-input', + placeholder: '10', + mode: 'advanced', + condition: { + field: 'operation', + value: [ + 'list_inquiries', + 'list_accounts', + 'list_cases', + 'list_reports', + 'list_inquiry_templates', + ], + }, + }, + { + id: 'pageAfter', + title: 'Page After Cursor', + type: 'short-input', + placeholder: 'Object ID to paginate after', + mode: 'advanced', + condition: { + field: 'operation', + value: [ + 'list_inquiries', + 'list_accounts', + 'list_cases', + 'list_reports', + 'list_inquiry_templates', + ], + }, + }, + { + id: 'caseId', + title: 'Case ID', + type: 'short-input', + placeholder: 'case_ABC123', + condition: { field: 'operation', value: 'get_case' }, + required: true, + }, + { + id: 'reportType', + title: 'Report Type', + type: 'dropdown', + options: [ + { label: 'Watchlist', id: 'watchlist' }, + { label: 'Adverse Media', id: 'adverse-media' }, + { label: 'Politically Exposed Person', id: 'politically-exposed-person' }, + ], + value: () => 'watchlist', + condition: { field: 'operation', value: 'create_report' }, + required: true, + }, + { + id: 'reportTemplateId', + title: 'Report Template ID', + type: 'short-input', + placeholder: 'rptp_ABC123', + condition: { field: 'operation', value: 'create_report' }, + required: true, + }, + { + id: 'term', + title: 'Search Term', + type: 'short-input', + placeholder: 'Jane Q Doe (or use the name fields below)', + condition: { field: 'operation', value: 'create_report' }, + }, + { + id: 'nameFirst', + title: 'First Name', + type: 'short-input', + placeholder: 'Jane', + condition: { field: 'operation', value: 'create_report' }, + }, + { + id: 'nameMiddle', + title: 'Middle Name', + type: 'short-input', + placeholder: 'Q', + mode: 'advanced', + condition: { field: 'operation', value: 'create_report' }, + }, + { + id: 'nameLast', + title: 'Last Name', + type: 'short-input', + placeholder: 'Doe', + condition: { field: 'operation', value: 'create_report' }, + }, + { + id: 'birthdate', + title: 'Birthdate', + type: 'short-input', + placeholder: '1991-10-07', + mode: 'advanced', + condition: { field: 'operation', value: 'create_report' }, + }, + { + id: 'countryCode', + title: 'Country Code', + type: 'short-input', + placeholder: 'US', + mode: 'advanced', + condition: { + field: 'operation', + value: ['create_account', 'update_account', 'create_report'], + }, + }, + { + id: 'reportId', + title: 'Report ID', + type: 'short-input', + placeholder: 'rep_ABC123', + condition: { field: 'operation', value: 'get_report' }, + required: true, + }, + { + id: 'verificationId', + title: 'Verification ID', + type: 'short-input', + placeholder: 'ver_ABC123', + condition: { field: 'operation', value: 'get_verification' }, + required: true, + }, + { + id: 'documentId', + title: 'Document ID', + type: 'short-input', + placeholder: 'doc_ABC123', + condition: { field: 'operation', value: 'get_document' }, + required: true, + }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + placeholder: 'Enter your Persona API key', + password: true, + required: true, + }, + ], + + tools: { + access: [ + 'persona_create_inquiry', + 'persona_get_inquiry', + 'persona_list_inquiries', + 'persona_update_inquiry', + 'persona_approve_inquiry', + 'persona_decline_inquiry', + 'persona_mark_inquiry_for_review', + 'persona_resume_inquiry', + 'persona_expire_inquiry', + 'persona_generate_inquiry_link', + 'persona_print_inquiry_pdf', + 'persona_redact_inquiry', + 'persona_create_account', + 'persona_get_account', + 'persona_list_accounts', + 'persona_update_account', + 'persona_import_accounts', + 'persona_redact_account', + 'persona_list_cases', + 'persona_get_case', + 'persona_create_report', + 'persona_get_report', + 'persona_list_reports', + 'persona_get_verification', + 'persona_get_document', + 'persona_list_inquiry_templates', + ], + config: { + tool: (params) => `persona_${params.operation || 'create_inquiry'}`, + params: (params) => { + const result: Record = {} + + if (params.operation === 'import_accounts') { + const file = normalizeFileInput(params.file, { single: true }) + if (!file) { + throw new Error('A CSV file is required to import accounts') + } + result.file = file + } + + if (params.pageSize) { + result.pageSize = Number(params.pageSize) + } + if (params.expiresInSeconds) { + result.expiresInSeconds = Number(params.expiresInSeconds) + } + + return result + }, + }, + }, + + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'Persona API key' }, + inquiryTemplateId: { + type: 'string', + description: 'Inquiry template ID (itmpl_/itmplv_/tmpl_)', + }, + inquiryId: { type: 'string', description: 'Inquiry ID (inq_)' }, + accountId: { type: 'string', description: 'Account ID (act_)' }, + referenceId: { type: 'string', description: 'Reference ID in your user model' }, + fields: { type: 'json', description: 'Field name to field value pairs' }, + note: { type: 'string', description: 'Free-form inquiry note' }, + redirectUri: { type: 'string', description: 'Redirect URI after inquiry completion' }, + expiresInSeconds: { type: 'number', description: 'One-time link expiry in seconds' }, + accountTypeId: { type: 'string', description: 'Account type ID (acttp_)' }, + tags: { type: 'json', description: 'Tag names to set on the account or inquiry' }, + file: { type: 'file', description: 'CSV file of accounts to import' }, + status: { type: 'string', description: 'Status filter for list operations' }, + createdAtStart: { type: 'string', description: 'Created-after ISO 8601 filter' }, + createdAtEnd: { type: 'string', description: 'Created-before ISO 8601 filter' }, + pageSize: { type: 'number', description: 'Results per page (1-100)' }, + pageAfter: { type: 'string', description: 'Pagination cursor' }, + caseId: { type: 'string', description: 'Case ID (case_)' }, + reportType: { + type: 'string', + description: 'Report type (watchlist, adverse-media, politically-exposed-person)', + }, + reportTemplateId: { type: 'string', description: 'Report template ID (rptp_)' }, + term: { type: 'string', description: 'Full-name search term for reports' }, + nameFirst: { type: 'string', description: 'First name to screen' }, + nameMiddle: { type: 'string', description: 'Middle name to screen' }, + nameLast: { type: 'string', description: 'Last name to screen' }, + birthdate: { type: 'string', description: 'Birthdate (YYYY-MM-DD)' }, + countryCode: { type: 'string', description: 'ISO 3166-1 alpha-2 country code' }, + reportId: { type: 'string', description: 'Report ID (rep_)' }, + verificationId: { type: 'string', description: 'Verification ID (ver_)' }, + documentId: { type: 'string', description: 'Document ID (doc_)' }, + }, + + outputs: { + inquiry: { + type: 'json', + description: + 'Inquiry (id, status, referenceId, note, tags, fields, createdAt, startedAt, completedAt, failedAt, expiredAt, decisionedAt)', + }, + inquiries: { + type: 'json', + description: + 'List of inquiries [{id, status, referenceId, note, tags, fields, createdAt, startedAt, completedAt, failedAt, expiredAt, decisionedAt}]', + }, + oneTimeLink: { type: 'string', description: 'One-time inquiry link' }, + oneTimeLinkShort: { type: 'string', description: 'Shortened one-time inquiry link' }, + sessionToken: { type: 'string', description: 'Session token from resuming an inquiry' }, + file: { type: 'file', description: 'Inquiry PDF stored in execution files' }, + account: { + type: 'json', + description: + 'Account (id, referenceId, accountTypeName, accountStatus, tags, fields, createdAt, updatedAt)', + }, + accounts: { + type: 'json', + description: + 'List of accounts [{id, referenceId, accountTypeName, accountStatus, tags, fields, createdAt, updatedAt}]', + }, + importer: { + type: 'json', + description: + 'Account importer (id, status, successfulCount, errorCount, duplicateCount, createdAt, completedAt)', + }, + case: { + type: 'json', + description: + 'Case (id, status, name, resolution, assigneeId, tags, fields, createdAt, assignedAt, resolvedAt)', + }, + cases: { + type: 'json', + description: + 'List of cases [{id, status, name, resolution, assigneeId, tags, fields, createdAt, assignedAt, resolvedAt}]', + }, + report: { + type: 'json', + description: 'Report (id, type, status, hasMatch, tags, createdAt, completedAt, attributes)', + }, + reports: { + type: 'json', + description: + 'List of reports [{id, type, status, hasMatch, tags, createdAt, completedAt, attributes}]', + }, + verification: { + type: 'json', + description: + 'Verification (id, type, status, checks, countryCode, createdAt, submittedAt, completedAt, attributes)', + }, + document: { + type: 'json', + description: 'Document (id, type, status, kind, files, createdAt, processedAt, attributes)', + }, + inquiryTemplates: { + type: 'json', + description: 'List of inquiry templates [{id, name, status}]', + }, + nextCursor: { type: 'string', description: 'Cursor for the next page of list results' }, + }, +} + +export const PersonaBlockMeta = { + tags: ['identity'], + templates: [ + { + icon: PersonaIcon, + title: 'Customer onboarding identity verification', + prompt: + 'Build a workflow triggered when a new customer signs up that creates a Persona inquiry from our KYC template with their name and email pre-filled, generates a one-time verification link, and emails it to the customer.', + modules: ['workflows', 'agent'], + category: 'operations', + tags: ['onboarding', 'compliance'], + alsoIntegrations: ['gmail'], + }, + { + icon: PersonaIcon, + title: 'Verification decision router', + prompt: + 'Build a workflow that takes an inquiry ID, fetches the inquiry from Persona, and routes on its status: approved customers get a welcome email, needs-review inquiries post to a compliance Slack channel with a summary, and declined inquiries update our CRM.', + modules: ['workflows', 'agent'], + category: 'operations', + tags: ['compliance', 'automation'], + alsoIntegrations: ['slack', 'gmail'], + }, + { + icon: PersonaIcon, + title: 'Daily pending-review digest', + prompt: + 'Build a scheduled workflow that runs every morning, lists Persona inquiries with needs_review status from the last 24 hours, summarizes each one, and posts a digest to the compliance team in Slack.', + modules: ['scheduled', 'workflows', 'agent'], + category: 'operations', + tags: ['compliance', 'monitoring'], + alsoIntegrations: ['slack'], + }, + { + icon: PersonaIcon, + title: 'Watchlist screening on signup', + prompt: + "Build a workflow that takes a new user's name and birthdate, runs a Persona watchlist report against them, polls until the report is ready, and creates a case in our tracking table if the report has a match.", + modules: ['workflows', 'tables', 'agent'], + category: 'operations', + tags: ['compliance', 'screening'], + }, + { + icon: PersonaIcon, + title: 'Bulk account import from CRM export', + prompt: + 'Build a workflow that takes an uploaded CSV export of customers, imports them into Persona as accounts using the account importer, polls the importer status, and reports how many rows succeeded, errored, or were duplicates.', + modules: ['files', 'workflows', 'agent'], + category: 'operations', + tags: ['migration', 'automation'], + }, + { + icon: PersonaIcon, + title: 'Compliance audit PDF archive', + prompt: + 'Build a workflow that takes an approved inquiry ID, downloads the inquiry summary PDF from Persona, and uploads it to a compliance archive folder in Google Drive named by customer reference ID.', + modules: ['workflows', 'files', 'agent'], + category: 'operations', + tags: ['compliance', 'audit'], + alsoIntegrations: ['google_drive'], + }, + { + icon: PersonaIcon, + title: 'Manual review case triage agent', + prompt: + 'Build an agent that lists open Persona cases, fetches the linked inquiry and verification details for each, drafts a recommended approve/decline decision with reasoning, and posts the triage summary to Slack for a human reviewer.', + modules: ['agent', 'workflows'], + category: 'support', + tags: ['compliance', 'triage'], + alsoIntegrations: ['slack'], + }, + { + icon: PersonaIcon, + title: 'Re-verification campaign for stale accounts', + prompt: + 'Build a scheduled workflow that lists Persona accounts, finds ones whose latest approved inquiry is older than a year, creates a new inquiry for each from our re-verification template, and emails customers a one-time verification link.', + modules: ['scheduled', 'workflows', 'agent'], + category: 'operations', + tags: ['compliance', 'lifecycle'], + alsoIntegrations: ['gmail'], + }, + ], + skills: [ + { + name: 'verify-customer-identity', + description: + 'Create a Persona inquiry for a customer and send them a one-time verification link.', + content: + '# Verify Customer Identity\n\nStart an identity verification for a customer using Persona.\n\n## Steps\n1. Use the Create Inquiry operation with your inquiry template ID. Pass the customer reference ID so the inquiry links to an account in your user model, and pre-fill known fields (e.g. {"name-first": "Jane", "name-last": "Doe"}).\n2. Use the Generate Inquiry Link operation with the new inquiry ID to mint a one-time verification link. Set a custom expiry only if the default (24 hours) does not fit.\n3. Deliver the one-time link to the customer (email, SMS, or chat).\n\n## Output\nReturn the inquiry ID, its status, and the one-time link. Note when the link expires so follow-ups can be scheduled.', + }, + { + name: 'check-verification-status', + description: + 'Look up a Persona inquiry and report whether the individual passed, failed, or needs review.', + content: + '# Check Verification Status\n\nRead the current state of an identity verification from Persona.\n\n## Steps\n1. Use the Get Inquiry operation with the inquiry ID (or List Inquiries filtered by reference ID to find it).\n2. Read the status: approved or completed means verified; needs_review means a human should look; failed, expired, or declined means not verified.\n3. For deeper detail, use Get Verification with a verification ID to inspect the individual checks that ran.\n\n## Output\nReturn the status, decision timestamps, and collected fields. Recommend the next action (proceed, route to review, or re-verify).', + }, + { + name: 'screen-against-watchlists', + description: + 'Run a Persona watchlist, adverse media, or PEP report on a person and surface matches.', + content: + '# Screen Against Watchlists\n\nScreen an individual for sanctions, adverse media, or political exposure using Persona reports.\n\n## Steps\n1. Use the Create Report operation with the report type (watchlist, adverse-media, or politically-exposed-person) and your report template ID. Provide the name parts or a full-name term, plus birthdate and country code when known to reduce false positives.\n2. Reports run asynchronously: poll Get Report with the report ID until the status is ready.\n3. Check hasMatch and the report attributes for matched lists and match details.\n\n## Output\nReturn whether the screening found matches, the matched lists, and the report ID for the audit trail.', + }, + { + name: 'triage-pending-reviews', + description: + 'List Persona inquiries awaiting manual review and approve or decline them after assessment.', + content: + '# Triage Pending Reviews\n\nWork through identity verifications that need a manual decision in Persona.\n\n## Steps\n1. Use the List Inquiries operation with status needs_review (optionally bounded by created-after/created-before).\n2. For each inquiry, use Get Inquiry for collected fields and List Cases / Get Case to see any linked review case.\n3. After assessment, use Approve Inquiry or Decline Inquiry. Both are final: they prevent further progress and trigger associated workflows and webhooks.\n\n## Output\nReturn a summary of inquiries reviewed and the decision taken for each, with inquiry IDs for the audit trail.', + }, + { + name: 'import-accounts-from-csv', + description: 'Bulk-import existing users into Persona as accounts from a CSV file.', + content: + '# Import Accounts from CSV\n\nMigrate an existing user base into Persona using the account importer.\n\n## Steps\n1. Prepare a CSV of users (one row per account, with reference IDs and account fields).\n2. Use the Import Accounts (CSV) operation with the file.\n3. The importer runs asynchronously and returns pending at first; report the importer ID and counts once available.\n\n## Output\nReturn the importer ID, status, and the successful, errored, and duplicate row counts.', + }, + { + name: 'archive-verification-pdf', + description: 'Download the PDF summary of a Persona inquiry for compliance record-keeping.', + content: + '# Archive Verification PDF\n\nKeep a permanent record of an identity verification decision.\n\n## Steps\n1. Use the Print Inquiry PDF operation with the inquiry ID.\n2. The PDF lands in execution files; pass it to a storage integration (Google Drive, S3, SharePoint) named by customer reference ID and date.\n\n## Output\nReturn the stored file and where it was archived.', + }, + ], +} as const satisfies BlockMeta diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 2ccc000300..2703ead3bd 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -214,6 +214,7 @@ import { PagerDutyBlock, PagerDutyBlockMeta } from '@/blocks/blocks/pagerduty' import { ParallelBlock, ParallelBlockMeta } from '@/blocks/blocks/parallel' import { PeopleDataLabsBlock, PeopleDataLabsBlockMeta } from '@/blocks/blocks/peopledatalabs' import { PerplexityBlock, PerplexityBlockMeta } from '@/blocks/blocks/perplexity' +import { PersonaBlock, PersonaBlockMeta } from '@/blocks/blocks/persona' import { PineconeBlock, PineconeBlockMeta } from '@/blocks/blocks/pinecone' import { PipedriveBlock, PipedriveBlockMeta } from '@/blocks/blocks/pipedrive' import { PolymarketBlock, PolymarketBlockMeta } from '@/blocks/blocks/polymarket' @@ -500,6 +501,7 @@ const BLOCK_REGISTRY: Record = { parallel_ai: ParallelBlock, peopledatalabs: PeopleDataLabsBlock, perplexity: PerplexityBlock, + persona: PersonaBlock, pinecone: PineconeBlock, pipedrive: PipedriveBlock, polymarket: PolymarketBlock, @@ -760,6 +762,7 @@ const BLOCK_META_REGISTRY: Record = { parallel_ai: ParallelBlockMeta, peopledatalabs: PeopleDataLabsBlockMeta, perplexity: PerplexityBlockMeta, + persona: PersonaBlockMeta, pinecone: PineconeBlockMeta, pipedrive: PipedriveBlockMeta, polymarket: PolymarketBlockMeta, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 4750caa45d..6f5df2b76b 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -961,6 +961,24 @@ export function PeopleDataLabsIcon(props: SVGProps) { ) } +export function PersonaIcon(props: SVGProps) { + return ( + + + + ) +} + export function PerplexityIcon(props: SVGProps) { return ( diff --git a/apps/sim/lib/api/contracts/tools/index.ts b/apps/sim/lib/api/contracts/tools/index.ts index 294eee7c47..f65acf4cb6 100644 --- a/apps/sim/lib/api/contracts/tools/index.ts +++ b/apps/sim/lib/api/contracts/tools/index.ts @@ -17,6 +17,7 @@ export * from './mail' export * from './media' export * from './microsoft' export * from './onepassword' +export * from './persona' export * from './pipedrive' export * from './quiver' export * from './sap' diff --git a/apps/sim/lib/api/contracts/tools/persona.ts b/apps/sim/lib/api/contracts/tools/persona.ts new file mode 100644 index 0000000000..146d321f36 --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/persona.ts @@ -0,0 +1,38 @@ +import { z } from 'zod' +import type { ContractBody, ContractJsonResponse } from '@/lib/api/contracts/types' +import { defineRouteContract } from '@/lib/api/contracts/types' +import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas' + +const personaImporterSchema = z.object({ + id: z.string(), + status: z.string().nullable(), + successfulCount: z.number(), + errorCount: z.number(), + duplicateCount: z.number(), + createdAt: z.string().nullable(), + completedAt: z.string().nullable(), +}) + +const personaImportAccountsResponseSchema = z.object({ + success: z.literal(true), + output: z.object({ + importer: personaImporterSchema, + }), +}) + +export const personaImportAccountsBodySchema = z.object({ + apiKey: z.string().min(1, 'Persona API key is required'), + file: RawFileInputSchema, +}) + +export const personaImportAccountsContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/persona/import-accounts', + body: personaImportAccountsBodySchema, + response: { mode: 'json', schema: personaImportAccountsResponseSchema }, +}) + +export type PersonaImportAccountsBody = ContractBody +export type PersonaImportAccountsRouteResponse = ContractJsonResponse< + typeof personaImportAccountsContract +> diff --git a/apps/sim/lib/integrations/icon-mapping.ts b/apps/sim/lib/integrations/icon-mapping.ts index b4be8a5551..b7d26906bb 100644 --- a/apps/sim/lib/integrations/icon-mapping.ts +++ b/apps/sim/lib/integrations/icon-mapping.ts @@ -144,6 +144,7 @@ import { ParallelIcon, PeopleDataLabsIcon, PerplexityIcon, + PersonaIcon, PineconeIcon, PipedriveIcon, PolymarketIcon, @@ -358,6 +359,7 @@ export const blockTypeToIconMap: Record = { parallel_ai: ParallelIcon, peopledatalabs: PeopleDataLabsIcon, perplexity: PerplexityIcon, + persona: PersonaIcon, pinecone: PineconeIcon, pipedrive: PipedriveIcon, polymarket: PolymarketIcon, diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index 3bca9507eb..b6b6beeaf4 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -10629,6 +10629,129 @@ "integrationType": "ai", "tags": ["llm", "web-scraping", "agentic"] }, + { + "type": "persona", + "slug": "persona", + "name": "Persona", + "description": "Verify identities with Persona", + "longDescription": "Integrate Persona identity verification into the workflow. Manage the full inquiry lifecycle (create, update, approve, decline, review, resume, expire, redact), generate one-time verification links and PDF summaries, manage accounts including CSV bulk import, run watchlist and adverse media reports, review cases, retrieve verifications and documents, and discover inquiry templates.", + "bgColor": "#FFFFFF", + "iconName": "PersonaIcon", + "docsUrl": "https://docs.sim.ai/integrations/persona", + "operations": [ + { + "name": "Create Inquiry", + "description": "Create a new identity verification inquiry from an inquiry template. Returns the created inquiry, which can then be completed by the individual via a one-time link." + }, + { + "name": "Get Inquiry", + "description": "Retrieve a single identity verification inquiry by ID, including its status, collected fields, and decision timestamps." + }, + { + "name": "List Inquiries", + "description": "List identity verification inquiries, optionally filtered by status, account ID, reference ID, or creation date range. Results are cursor-paginated." + }, + { + "name": "Update Inquiry", + "description": "Update an inquiry’s note, fields, tags, or redirect URI. Only the provided values are changed." + }, + { + "name": "Approve Inquiry", + "description": "Approve an identity verification inquiry. Approving prevents further progress on the inquiry and triggers any associated workflows and webhooks." + }, + { + "name": "Decline Inquiry", + "description": "Decline an identity verification inquiry. Declining prevents further progress on the inquiry and triggers any associated workflows and webhooks." + }, + { + "name": "Mark Inquiry for Review", + "description": "Mark an identity verification inquiry for manual review, moving it to the needs_review status." + }, + { + "name": "Resume Inquiry", + "description": "Resume a pending or expired inquiry, creating a new session so the individual can continue verification. Returns a session token." + }, + { + "name": "Expire Inquiry", + "description": "Expire an in-progress inquiry, invalidating its sessions and one-time links so the individual can no longer continue it." + }, + { + "name": "Generate Inquiry Link", + "description": "Generate a one-time link for an inquiry that the individual can open to complete their identity verification." + }, + { + "name": "Print Inquiry PDF", + "description": "Download a PDF summary of an inquiry, including its collected information and verification results." + }, + { + "name": "Redact Inquiry", + "description": "Permanently delete all personally identifiable information collected by an inquiry, for example to honor a data deletion request. This cannot be undone." + }, + { + "name": "Create Account", + "description": "Create an account that represents an individual in Persona. Accounts consolidate inquiries, verifications, and reports for the same person." + }, + { + "name": "Get Account", + "description": "Retrieve a single account by ID, including its reference ID, fields, tags, and status." + }, + { + "name": "List Accounts", + "description": "List accounts in your Persona organization, optionally filtered by reference ID. Results are cursor-paginated." + }, + { + "name": "Update Account", + "description": "Update an account’s reference ID, country code, fields, or tags. Only the provided values are changed." + }, + { + "name": "Import Accounts (CSV)", + "description": "Bulk-import accounts into Persona from a CSV file. Returns an importer whose status can be polled until processing completes." + }, + { + "name": "Redact Account", + "description": "Permanently delete all personally identifiable information stored on an account, for example to honor a data deletion request. This cannot be undone." + }, + { + "name": "List Cases", + "description": "List manual review cases, optionally filtered by status, account ID, or reference ID. Results are cursor-paginated." + }, + { + "name": "Get Case", + "description": "Retrieve a single manual review case by ID, including its status, resolution, and assignee." + }, + { + "name": "Create Report", + "description": "Run a screening report (watchlist, adverse media, or politically exposed person) against an individual by name or search term." + }, + { + "name": "Get Report", + "description": "Retrieve a single screening report by ID, including its status, match results, and full type-specific attributes." + }, + { + "name": "List Reports", + "description": "List screening reports, optionally filtered by account ID or reference ID. Results are cursor-paginated." + }, + { + "name": "Get Verification", + "description": "Retrieve a single verification by ID (government ID, selfie, document, database, and more), including its status and the checks that ran." + }, + { + "name": "Get Document", + "description": "Retrieve a single document by ID (government ID, generic document, and more), including its processing status and uploaded files." + }, + { + "name": "List Inquiry Templates", + "description": "List the inquiry templates in your Persona organization, to discover template IDs for creating inquiries. Results are cursor-paginated." + } + ], + "operationCount": 26, + "triggers": [], + "triggerCount": 0, + "authType": "api-key", + "category": "tools", + "integrationType": "security", + "tags": ["identity"] + }, { "type": "pinecone", "slug": "pinecone", diff --git a/apps/sim/tools/persona/approve_inquiry.ts b/apps/sim/tools/persona/approve_inquiry.ts new file mode 100644 index 0000000000..7c8263bf4a --- /dev/null +++ b/apps/sim/tools/persona/approve_inquiry.ts @@ -0,0 +1,61 @@ +import type { PersonaApproveInquiryParams, PersonaInquiryResponse } from '@/tools/persona/types' +import { + asResource, + buildPersonaHeaders, + INQUIRY_OUTPUT_PROPERTIES, + mapInquiry, + PERSONA_API_BASE, + parsePersonaResponse, +} from '@/tools/persona/utils' +import type { ToolConfig } from '@/tools/types' + +export const personaApproveInquiryTool: ToolConfig< + PersonaApproveInquiryParams, + PersonaInquiryResponse +> = { + id: 'persona_approve_inquiry', + name: 'Persona Approve Inquiry', + description: + 'Approve an identity verification inquiry. Approving prevents further progress on the inquiry and triggers any associated workflows and webhooks.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Persona API key', + }, + inquiryId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Inquiry ID to approve (starts with inq_)', + }, + }, + + request: { + url: (params) => + `${PERSONA_API_BASE}/inquiries/${encodeURIComponent(params.inquiryId.trim())}/approve`, + method: 'POST', + headers: (params) => buildPersonaHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parsePersonaResponse(response) + return { + success: true, + output: { + inquiry: mapInquiry(asResource(data.data)), + }, + } + }, + + outputs: { + inquiry: { + type: 'object', + description: 'The approved inquiry', + properties: INQUIRY_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/persona/create_account.ts b/apps/sim/tools/persona/create_account.ts new file mode 100644 index 0000000000..808947c2ff --- /dev/null +++ b/apps/sim/tools/persona/create_account.ts @@ -0,0 +1,109 @@ +import type { PersonaAccountResponse, PersonaCreateAccountParams } from '@/tools/persona/types' +import { + ACCOUNT_OUTPUT_PROPERTIES, + asResource, + buildPersonaHeaders, + mapAccount, + PERSONA_API_BASE, + parseJsonObjectParam, + parsePersonaResponse, + parseStringArrayParam, +} from '@/tools/persona/utils' +import type { ToolConfig } from '@/tools/types' + +export const personaCreateAccountTool: ToolConfig< + PersonaCreateAccountParams, + PersonaAccountResponse +> = { + id: 'persona_create_account', + name: 'Persona Create Account', + description: + 'Create an account that represents an individual in Persona. Accounts consolidate inquiries, verifications, and reports for the same person.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Persona API key', + }, + accountTypeId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Account type ID to create the account for (starts with acttp_); defaults to your organization default', + }, + referenceId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Reference ID that refers to an entity in your user model', + }, + countryCode: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'ISO 3166-1 alpha-2 country code (e.g. US)', + }, + fields: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: + 'JSON object of field name to field value pairs, as defined by the account type (e.g. {"name-first": "Jane"})', + }, + tags: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'JSON array of tag names to associate with the account (e.g. ["vip"])', + }, + }, + + request: { + url: `${PERSONA_API_BASE}/accounts`, + method: 'POST', + headers: (params) => buildPersonaHeaders(params.apiKey), + body: (params) => { + const attributes: Record = {} + if (params.accountTypeId?.trim()) { + attributes['account-type-id'] = params.accountTypeId.trim() + } + if (params.referenceId?.trim()) { + attributes['reference-id'] = params.referenceId.trim() + } + if (params.countryCode?.trim()) { + attributes['country-code'] = params.countryCode.trim() + } + const fields = parseJsonObjectParam(params.fields, 'Fields') + if (fields) { + attributes.fields = fields + } + const tags = parseStringArrayParam(params.tags, 'Tags') + if (tags) { + attributes.tags = tags + } + return { data: { attributes } } + }, + }, + + transformResponse: async (response) => { + const data = await parsePersonaResponse(response) + return { + success: true, + output: { + account: mapAccount(asResource(data.data)), + }, + } + }, + + outputs: { + account: { + type: 'object', + description: 'The created account', + properties: ACCOUNT_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/persona/create_inquiry.ts b/apps/sim/tools/persona/create_inquiry.ts new file mode 100644 index 0000000000..a64f1c3818 --- /dev/null +++ b/apps/sim/tools/persona/create_inquiry.ts @@ -0,0 +1,131 @@ +import type { PersonaCreateInquiryParams, PersonaInquiryResponse } from '@/tools/persona/types' +import { + asResource, + buildPersonaHeaders, + INQUIRY_OUTPUT_PROPERTIES, + mapInquiry, + PERSONA_API_BASE, + parseJsonObjectParam, + parsePersonaResponse, +} from '@/tools/persona/utils' +import type { ToolConfig } from '@/tools/types' + +export const personaCreateInquiryTool: ToolConfig< + PersonaCreateInquiryParams, + PersonaInquiryResponse +> = { + id: 'persona_create_inquiry', + name: 'Persona Create Inquiry', + description: + 'Create a new identity verification inquiry from an inquiry template. Returns the created inquiry, which can then be completed by the individual via a one-time link.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Persona API key', + }, + inquiryTemplateId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Inquiry template ID (starts with itmpl_), inquiry template version ID (starts with itmplv_), or legacy template ID (starts with tmpl_)', + }, + accountId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Account ID (starts with act_) to associate with this inquiry', + }, + referenceId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Reference ID that refers to an entity in your user model. An account is auto-created for it if one does not exist.', + }, + fields: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: + 'JSON object of field name to field value pairs to pre-fill, as defined by the inquiry template (e.g. {"name-first": "Jane"})', + }, + note: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Free-form note to attach to the inquiry', + }, + redirectUri: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'URI to redirect the individual to after completing the inquiry flow', + }, + }, + + request: { + url: `${PERSONA_API_BASE}/inquiries`, + method: 'POST', + headers: (params) => buildPersonaHeaders(params.apiKey), + body: (params) => { + const templateId = params.inquiryTemplateId?.trim() + if (!templateId) { + throw new Error('Inquiry template ID is required') + } + + const attributes: Record = {} + if (templateId.startsWith('itmplv_')) { + attributes['inquiry-template-version-id'] = templateId + } else if (templateId.startsWith('itmpl_')) { + attributes['inquiry-template-id'] = templateId + } else if (templateId.startsWith('tmpl_')) { + attributes['template-id'] = templateId + } else { + throw new Error('Inquiry template ID must start with itmpl_, itmplv_, or tmpl_') + } + + if (params.accountId?.trim()) { + attributes['account-id'] = params.accountId.trim() + } + const fields = parseJsonObjectParam(params.fields, 'Fields') + if (fields) { + attributes.fields = fields + } + if (params.note?.trim()) { + attributes.note = params.note.trim() + } + if (params.redirectUri?.trim()) { + attributes['redirect-uri'] = params.redirectUri.trim() + } + + const body: Record = { data: { attributes } } + if (params.referenceId?.trim()) { + body.meta = { 'auto-create-account-reference-id': params.referenceId.trim() } + } + return body + }, + }, + + transformResponse: async (response) => { + const data = await parsePersonaResponse(response) + return { + success: true, + output: { + inquiry: mapInquiry(asResource(data.data)), + }, + } + }, + + outputs: { + inquiry: { + type: 'object', + description: 'The created inquiry', + properties: INQUIRY_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/persona/create_report.ts b/apps/sim/tools/persona/create_report.ts new file mode 100644 index 0000000000..c36dd77b32 --- /dev/null +++ b/apps/sim/tools/persona/create_report.ts @@ -0,0 +1,155 @@ +import type { PersonaCreateReportParams, PersonaReportResponse } from '@/tools/persona/types' +import { + asResource, + buildPersonaHeaders, + mapReport, + PERSONA_API_BASE, + parsePersonaResponse, + REPORT_OUTPUT_PROPERTIES, +} from '@/tools/persona/utils' +import type { ToolConfig } from '@/tools/types' + +const SUPPORTED_REPORT_TYPES = ['watchlist', 'adverse-media', 'politically-exposed-person'] as const + +export const personaCreateReportTool: ToolConfig = + { + id: 'persona_create_report', + name: 'Persona Create Report', + description: + 'Run a screening report (watchlist, adverse media, or politically exposed person) against an individual by name or search term.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Persona API key', + }, + reportType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Type of report to run: watchlist, adverse-media, or politically-exposed-person', + }, + reportTemplateId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Report template ID to run (starts with rptp_)', + }, + term: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Full-name search term (e.g. "Jane Q Doe"). Provide this or the separate name parts.', + }, + nameFirst: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'First name of the individual to search', + }, + nameMiddle: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Middle name of the individual to search', + }, + nameLast: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Last name of the individual to search', + }, + birthdate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Birthdate of the individual, formatted as YYYY-MM-DD', + }, + countryCode: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'ISO 3166-1 alpha-2 country code (e.g. US)', + }, + accountId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Account ID (starts with act_) to associate with this report', + }, + }, + + request: { + url: `${PERSONA_API_BASE}/reports`, + method: 'POST', + headers: (params) => buildPersonaHeaders(params.apiKey), + body: (params) => { + const reportType = params.reportType?.trim() + if ( + !SUPPORTED_REPORT_TYPES.includes(reportType as (typeof SUPPORTED_REPORT_TYPES)[number]) + ) { + throw new Error(`Report type must be one of: ${SUPPORTED_REPORT_TYPES.join(', ')}`) + } + + const reportTemplateId = params.reportTemplateId?.trim() + if (!reportTemplateId) { + throw new Error('Report template ID is required (starts with rptp_)') + } + + const query: Record = {} + if (params.term?.trim()) query.term = params.term.trim() + if (params.nameFirst?.trim()) query['name-first'] = params.nameFirst.trim() + if (params.nameMiddle?.trim()) query['name-middle'] = params.nameMiddle.trim() + if (params.nameLast?.trim()) query['name-last'] = params.nameLast.trim() + if (params.birthdate?.trim()) query.birthdate = params.birthdate.trim() + if (params.countryCode?.trim()) { + query[reportType === 'watchlist' ? 'country-code' : 'address-country-code'] = + params.countryCode.trim() + } + + if (!query.term && !query['name-first'] && !query['name-middle'] && !query['name-last']) { + throw new Error( + 'At least one of term, nameFirst, nameMiddle, or nameLast is required to run a report' + ) + } + + const attributes: Record = { + 'report-template-id': reportTemplateId, + query, + } + if (params.accountId?.trim()) { + attributes['account-id'] = params.accountId.trim() + } + + return { + data: { + type: `report/${reportType}`, + attributes, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await parsePersonaResponse(response) + return { + success: true, + output: { + report: mapReport(asResource(data.data)), + }, + } + }, + + outputs: { + report: { + type: 'object', + description: 'The created report. Reports run asynchronously; poll until status is ready.', + properties: REPORT_OUTPUT_PROPERTIES, + }, + }, + } diff --git a/apps/sim/tools/persona/decline_inquiry.ts b/apps/sim/tools/persona/decline_inquiry.ts new file mode 100644 index 0000000000..e51f39dafe --- /dev/null +++ b/apps/sim/tools/persona/decline_inquiry.ts @@ -0,0 +1,61 @@ +import type { PersonaDeclineInquiryParams, PersonaInquiryResponse } from '@/tools/persona/types' +import { + asResource, + buildPersonaHeaders, + INQUIRY_OUTPUT_PROPERTIES, + mapInquiry, + PERSONA_API_BASE, + parsePersonaResponse, +} from '@/tools/persona/utils' +import type { ToolConfig } from '@/tools/types' + +export const personaDeclineInquiryTool: ToolConfig< + PersonaDeclineInquiryParams, + PersonaInquiryResponse +> = { + id: 'persona_decline_inquiry', + name: 'Persona Decline Inquiry', + description: + 'Decline an identity verification inquiry. Declining prevents further progress on the inquiry and triggers any associated workflows and webhooks.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Persona API key', + }, + inquiryId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Inquiry ID to decline (starts with inq_)', + }, + }, + + request: { + url: (params) => + `${PERSONA_API_BASE}/inquiries/${encodeURIComponent(params.inquiryId.trim())}/decline`, + method: 'POST', + headers: (params) => buildPersonaHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parsePersonaResponse(response) + return { + success: true, + output: { + inquiry: mapInquiry(asResource(data.data)), + }, + } + }, + + outputs: { + inquiry: { + type: 'object', + description: 'The declined inquiry', + properties: INQUIRY_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/persona/expire_inquiry.ts b/apps/sim/tools/persona/expire_inquiry.ts new file mode 100644 index 0000000000..728e2c0101 --- /dev/null +++ b/apps/sim/tools/persona/expire_inquiry.ts @@ -0,0 +1,61 @@ +import type { PersonaExpireInquiryParams, PersonaInquiryResponse } from '@/tools/persona/types' +import { + asResource, + buildPersonaHeaders, + INQUIRY_OUTPUT_PROPERTIES, + mapInquiry, + PERSONA_API_BASE, + parsePersonaResponse, +} from '@/tools/persona/utils' +import type { ToolConfig } from '@/tools/types' + +export const personaExpireInquiryTool: ToolConfig< + PersonaExpireInquiryParams, + PersonaInquiryResponse +> = { + id: 'persona_expire_inquiry', + name: 'Persona Expire Inquiry', + description: + 'Expire an in-progress inquiry, invalidating its sessions and one-time links so the individual can no longer continue it.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Persona API key', + }, + inquiryId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Inquiry ID to expire (starts with inq_)', + }, + }, + + request: { + url: (params) => + `${PERSONA_API_BASE}/inquiries/${encodeURIComponent(params.inquiryId.trim())}/expire`, + method: 'POST', + headers: (params) => buildPersonaHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parsePersonaResponse(response) + return { + success: true, + output: { + inquiry: mapInquiry(asResource(data.data)), + }, + } + }, + + outputs: { + inquiry: { + type: 'object', + description: 'The expired inquiry', + properties: INQUIRY_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/persona/generate_inquiry_link.ts b/apps/sim/tools/persona/generate_inquiry_link.ts new file mode 100644 index 0000000000..862aab3d68 --- /dev/null +++ b/apps/sim/tools/persona/generate_inquiry_link.ts @@ -0,0 +1,101 @@ +import type { + PersonaGenerateInquiryLinkParams, + PersonaGenerateInquiryLinkResponse, +} from '@/tools/persona/types' +import { + asResource, + buildPersonaHeaders, + INQUIRY_OUTPUT_PROPERTIES, + mapInquiry, + PERSONA_API_BASE, + parsePersonaResponse, +} from '@/tools/persona/utils' +import type { ToolConfig } from '@/tools/types' + +export const personaGenerateInquiryLinkTool: ToolConfig< + PersonaGenerateInquiryLinkParams, + PersonaGenerateInquiryLinkResponse +> = { + id: 'persona_generate_inquiry_link', + name: 'Persona Generate Inquiry Link', + description: + 'Generate a one-time link for an inquiry that the individual can open to complete their identity verification.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Persona API key', + }, + inquiryId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Inquiry ID to generate a one-time link for (starts with inq_)', + }, + expiresInSeconds: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: + 'Number of seconds from now until the link expires (must be greater than 0; defaults to the inquiry template setting, typically 24 hours)', + }, + }, + + request: { + url: (params) => + `${PERSONA_API_BASE}/inquiries/${encodeURIComponent(params.inquiryId.trim())}/generate-one-time-link`, + method: 'POST', + headers: (params) => buildPersonaHeaders(params.apiKey), + body: (params) => { + if (params.expiresInSeconds === undefined || params.expiresInSeconds === null) { + return {} + } + const expiresInSeconds = Number(params.expiresInSeconds) + if (!Number.isInteger(expiresInSeconds) || expiresInSeconds <= 0) { + throw new Error('Link expiry must be a positive whole number of seconds') + } + return { meta: { 'expires-in-seconds': expiresInSeconds } } + }, + }, + + transformResponse: async (response) => { + const data = await parsePersonaResponse(response) + const oneTimeLink = data.meta?.['one-time-link'] + const oneTimeLinkShort = data.meta?.['one-time-link-short'] + if (typeof oneTimeLink !== 'string' || oneTimeLink.length === 0) { + throw new Error( + 'Persona did not return a one-time link; check the inquiry status and template settings' + ) + } + return { + success: true, + output: { + inquiry: mapInquiry(asResource(data.data)), + oneTimeLink, + oneTimeLinkShort: + typeof oneTimeLinkShort === 'string' && oneTimeLinkShort.length > 0 + ? oneTimeLinkShort + : oneTimeLink, + }, + } + }, + + outputs: { + inquiry: { + type: 'object', + description: 'The inquiry the link was generated for', + properties: INQUIRY_OUTPUT_PROPERTIES, + }, + oneTimeLink: { + type: 'string', + description: 'One-time link the individual can open to complete the inquiry', + }, + oneTimeLinkShort: { + type: 'string', + description: 'Shortened version of the one-time link', + }, + }, +} diff --git a/apps/sim/tools/persona/get_account.ts b/apps/sim/tools/persona/get_account.ts new file mode 100644 index 0000000000..d8fab14286 --- /dev/null +++ b/apps/sim/tools/persona/get_account.ts @@ -0,0 +1,57 @@ +import type { PersonaAccountResponse, PersonaGetAccountParams } from '@/tools/persona/types' +import { + ACCOUNT_OUTPUT_PROPERTIES, + asResource, + buildPersonaHeaders, + mapAccount, + PERSONA_API_BASE, + parsePersonaResponse, +} from '@/tools/persona/utils' +import type { ToolConfig } from '@/tools/types' + +export const personaGetAccountTool: ToolConfig = { + id: 'persona_get_account', + name: 'Persona Get Account', + description: + 'Retrieve a single account by ID, including its reference ID, fields, tags, and status.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Persona API key', + }, + accountId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Account ID to retrieve (starts with act_)', + }, + }, + + request: { + url: (params) => `${PERSONA_API_BASE}/accounts/${encodeURIComponent(params.accountId.trim())}`, + method: 'GET', + headers: (params) => buildPersonaHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parsePersonaResponse(response) + return { + success: true, + output: { + account: mapAccount(asResource(data.data)), + }, + } + }, + + outputs: { + account: { + type: 'object', + description: 'The retrieved account', + properties: ACCOUNT_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/persona/get_case.ts b/apps/sim/tools/persona/get_case.ts new file mode 100644 index 0000000000..6474bb08e2 --- /dev/null +++ b/apps/sim/tools/persona/get_case.ts @@ -0,0 +1,57 @@ +import type { PersonaCaseResponse, PersonaGetCaseParams } from '@/tools/persona/types' +import { + asResource, + buildPersonaHeaders, + CASE_OUTPUT_PROPERTIES, + mapCase, + PERSONA_API_BASE, + parsePersonaResponse, +} from '@/tools/persona/utils' +import type { ToolConfig } from '@/tools/types' + +export const personaGetCaseTool: ToolConfig = { + id: 'persona_get_case', + name: 'Persona Get Case', + description: + 'Retrieve a single manual review case by ID, including its status, resolution, and assignee.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Persona API key', + }, + caseId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Case ID to retrieve (starts with case_)', + }, + }, + + request: { + url: (params) => `${PERSONA_API_BASE}/cases/${encodeURIComponent(params.caseId.trim())}`, + method: 'GET', + headers: (params) => buildPersonaHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parsePersonaResponse(response) + return { + success: true, + output: { + case: mapCase(asResource(data.data)), + }, + } + }, + + outputs: { + case: { + type: 'object', + description: 'The retrieved case', + properties: CASE_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/persona/get_document.ts b/apps/sim/tools/persona/get_document.ts new file mode 100644 index 0000000000..53bf8fc87d --- /dev/null +++ b/apps/sim/tools/persona/get_document.ts @@ -0,0 +1,59 @@ +import type { PersonaDocumentResponse, PersonaGetDocumentParams } from '@/tools/persona/types' +import { + asResource, + buildPersonaHeaders, + DOCUMENT_OUTPUT_PROPERTIES, + mapDocument, + PERSONA_API_BASE, + parsePersonaResponse, +} from '@/tools/persona/utils' +import type { ToolConfig } from '@/tools/types' + +export const personaGetDocumentTool: ToolConfig = + { + id: 'persona_get_document', + name: 'Persona Get Document', + description: + 'Retrieve a single document by ID (government ID, generic document, and more), including its processing status and uploaded files.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Persona API key', + }, + documentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Document ID to retrieve (starts with doc_)', + }, + }, + + request: { + url: (params) => + `${PERSONA_API_BASE}/documents/${encodeURIComponent(params.documentId.trim())}`, + method: 'GET', + headers: (params) => buildPersonaHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parsePersonaResponse(response) + return { + success: true, + output: { + document: mapDocument(asResource(data.data)), + }, + } + }, + + outputs: { + document: { + type: 'object', + description: 'The retrieved document', + properties: DOCUMENT_OUTPUT_PROPERTIES, + }, + }, + } diff --git a/apps/sim/tools/persona/get_inquiry.ts b/apps/sim/tools/persona/get_inquiry.ts new file mode 100644 index 0000000000..0883c3ebc5 --- /dev/null +++ b/apps/sim/tools/persona/get_inquiry.ts @@ -0,0 +1,57 @@ +import type { PersonaGetInquiryParams, PersonaInquiryResponse } from '@/tools/persona/types' +import { + asResource, + buildPersonaHeaders, + INQUIRY_OUTPUT_PROPERTIES, + mapInquiry, + PERSONA_API_BASE, + parsePersonaResponse, +} from '@/tools/persona/utils' +import type { ToolConfig } from '@/tools/types' + +export const personaGetInquiryTool: ToolConfig = { + id: 'persona_get_inquiry', + name: 'Persona Get Inquiry', + description: + 'Retrieve a single identity verification inquiry by ID, including its status, collected fields, and decision timestamps.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Persona API key', + }, + inquiryId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Inquiry ID to retrieve (starts with inq_)', + }, + }, + + request: { + url: (params) => `${PERSONA_API_BASE}/inquiries/${encodeURIComponent(params.inquiryId.trim())}`, + method: 'GET', + headers: (params) => buildPersonaHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parsePersonaResponse(response) + return { + success: true, + output: { + inquiry: mapInquiry(asResource(data.data)), + }, + } + }, + + outputs: { + inquiry: { + type: 'object', + description: 'The retrieved inquiry', + properties: INQUIRY_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/persona/get_report.ts b/apps/sim/tools/persona/get_report.ts new file mode 100644 index 0000000000..56f0a3e638 --- /dev/null +++ b/apps/sim/tools/persona/get_report.ts @@ -0,0 +1,57 @@ +import type { PersonaGetReportParams, PersonaReportResponse } from '@/tools/persona/types' +import { + asResource, + buildPersonaHeaders, + mapReport, + PERSONA_API_BASE, + parsePersonaResponse, + REPORT_OUTPUT_PROPERTIES, +} from '@/tools/persona/utils' +import type { ToolConfig } from '@/tools/types' + +export const personaGetReportTool: ToolConfig = { + id: 'persona_get_report', + name: 'Persona Get Report', + description: + 'Retrieve a single screening report by ID, including its status, match results, and full type-specific attributes.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Persona API key', + }, + reportId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Report ID to retrieve (starts with rep_)', + }, + }, + + request: { + url: (params) => `${PERSONA_API_BASE}/reports/${encodeURIComponent(params.reportId.trim())}`, + method: 'GET', + headers: (params) => buildPersonaHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parsePersonaResponse(response) + return { + success: true, + output: { + report: mapReport(asResource(data.data)), + }, + } + }, + + outputs: { + report: { + type: 'object', + description: 'The retrieved report', + properties: REPORT_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/persona/get_verification.ts b/apps/sim/tools/persona/get_verification.ts new file mode 100644 index 0000000000..69724df03b --- /dev/null +++ b/apps/sim/tools/persona/get_verification.ts @@ -0,0 +1,64 @@ +import type { + PersonaGetVerificationParams, + PersonaVerificationResponse, +} from '@/tools/persona/types' +import { + asResource, + buildPersonaHeaders, + mapVerification, + PERSONA_API_BASE, + parsePersonaResponse, + VERIFICATION_OUTPUT_PROPERTIES, +} from '@/tools/persona/utils' +import type { ToolConfig } from '@/tools/types' + +export const personaGetVerificationTool: ToolConfig< + PersonaGetVerificationParams, + PersonaVerificationResponse +> = { + id: 'persona_get_verification', + name: 'Persona Get Verification', + description: + 'Retrieve a single verification by ID (government ID, selfie, document, database, and more), including its status and the checks that ran.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Persona API key', + }, + verificationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Verification ID to retrieve (starts with ver_)', + }, + }, + + request: { + url: (params) => + `${PERSONA_API_BASE}/verifications/${encodeURIComponent(params.verificationId.trim())}`, + method: 'GET', + headers: (params) => buildPersonaHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parsePersonaResponse(response) + return { + success: true, + output: { + verification: mapVerification(asResource(data.data)), + }, + } + }, + + outputs: { + verification: { + type: 'object', + description: 'The retrieved verification', + properties: VERIFICATION_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/persona/import_accounts.ts b/apps/sim/tools/persona/import_accounts.ts new file mode 100644 index 0000000000..0cc63a0a99 --- /dev/null +++ b/apps/sim/tools/persona/import_accounts.ts @@ -0,0 +1,63 @@ +import type { + PersonaImportAccountsParams, + PersonaImportAccountsResponse, +} from '@/tools/persona/types' +import { IMPORTER_OUTPUT_PROPERTIES } from '@/tools/persona/utils' +import type { ToolConfig } from '@/tools/types' + +export const personaImportAccountsTool: ToolConfig< + PersonaImportAccountsParams, + PersonaImportAccountsResponse +> = { + id: 'persona_import_accounts', + name: 'Persona Import Accounts', + description: + 'Bulk-import accounts into Persona from a CSV file. Returns an importer whose status can be polled until processing completes.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Persona API key', + }, + file: { + type: 'file', + required: true, + visibility: 'user-only', + description: 'CSV file of accounts to import', + }, + }, + + request: { + url: '/api/tools/persona/import-accounts', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + apiKey: params.apiKey, + file: params.file, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!data.success) { + throw new Error(data.error || 'Failed to import accounts into Persona') + } + return { + success: true, + output: data.output, + } + }, + + outputs: { + importer: { + type: 'object', + description: 'The created account importer', + properties: IMPORTER_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/persona/index.ts b/apps/sim/tools/persona/index.ts new file mode 100644 index 0000000000..1331b1174b --- /dev/null +++ b/apps/sim/tools/persona/index.ts @@ -0,0 +1,26 @@ +export { personaApproveInquiryTool } from '@/tools/persona/approve_inquiry' +export { personaCreateAccountTool } from '@/tools/persona/create_account' +export { personaCreateInquiryTool } from '@/tools/persona/create_inquiry' +export { personaCreateReportTool } from '@/tools/persona/create_report' +export { personaDeclineInquiryTool } from '@/tools/persona/decline_inquiry' +export { personaExpireInquiryTool } from '@/tools/persona/expire_inquiry' +export { personaGenerateInquiryLinkTool } from '@/tools/persona/generate_inquiry_link' +export { personaGetAccountTool } from '@/tools/persona/get_account' +export { personaGetCaseTool } from '@/tools/persona/get_case' +export { personaGetDocumentTool } from '@/tools/persona/get_document' +export { personaGetInquiryTool } from '@/tools/persona/get_inquiry' +export { personaGetReportTool } from '@/tools/persona/get_report' +export { personaGetVerificationTool } from '@/tools/persona/get_verification' +export { personaImportAccountsTool } from '@/tools/persona/import_accounts' +export { personaListAccountsTool } from '@/tools/persona/list_accounts' +export { personaListCasesTool } from '@/tools/persona/list_cases' +export { personaListInquiriesTool } from '@/tools/persona/list_inquiries' +export { personaListInquiryTemplatesTool } from '@/tools/persona/list_inquiry_templates' +export { personaListReportsTool } from '@/tools/persona/list_reports' +export { personaMarkInquiryForReviewTool } from '@/tools/persona/mark_inquiry_for_review' +export { personaPrintInquiryPdfTool } from '@/tools/persona/print_inquiry_pdf' +export { personaRedactAccountTool } from '@/tools/persona/redact_account' +export { personaRedactInquiryTool } from '@/tools/persona/redact_inquiry' +export { personaResumeInquiryTool } from '@/tools/persona/resume_inquiry' +export { personaUpdateAccountTool } from '@/tools/persona/update_account' +export { personaUpdateInquiryTool } from '@/tools/persona/update_inquiry' diff --git a/apps/sim/tools/persona/list_accounts.ts b/apps/sim/tools/persona/list_accounts.ts new file mode 100644 index 0000000000..e7659284ca --- /dev/null +++ b/apps/sim/tools/persona/list_accounts.ts @@ -0,0 +1,92 @@ +import type { PersonaListAccountsParams, PersonaListAccountsResponse } from '@/tools/persona/types' +import { + ACCOUNT_OUTPUT_PROPERTIES, + asResourceList, + buildPersonaHeaders, + getNextCursor, + mapAccount, + PERSONA_API_BASE, + parsePersonaResponse, +} from '@/tools/persona/utils' +import type { ToolConfig } from '@/tools/types' + +export const personaListAccountsTool: ToolConfig< + PersonaListAccountsParams, + PersonaListAccountsResponse +> = { + id: 'persona_list_accounts', + name: 'Persona List Accounts', + description: + 'List accounts in your Persona organization, optionally filtered by reference ID. Results are cursor-paginated.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Persona API key', + }, + referenceId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by reference ID', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of accounts to return per page (1-100, default 10)', + }, + pageAfter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor: return accounts after this account ID', + }, + }, + + request: { + url: (params) => { + const searchParams = new URLSearchParams() + if (params.referenceId?.trim()) { + searchParams.set('filter[reference-id]', params.referenceId.trim()) + } + if (params.pageSize) searchParams.set('page[size]', String(params.pageSize)) + if (params.pageAfter?.trim()) searchParams.set('page[after]', params.pageAfter.trim()) + const query = searchParams.toString() + return `${PERSONA_API_BASE}/accounts${query ? `?${query}` : ''}` + }, + method: 'GET', + headers: (params) => buildPersonaHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parsePersonaResponse(response) + const accounts = asResourceList(data.data) + return { + success: true, + output: { + accounts: accounts.map(mapAccount), + nextCursor: getNextCursor(data.links), + }, + } + }, + + outputs: { + accounts: { + type: 'array', + description: 'Accounts matching the filters', + items: { + type: 'object', + properties: ACCOUNT_OUTPUT_PROPERTIES, + }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for the next page (pass as pageAfter), or null on the last page', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/persona/list_cases.ts b/apps/sim/tools/persona/list_cases.ts new file mode 100644 index 0000000000..2f04d3af04 --- /dev/null +++ b/apps/sim/tools/persona/list_cases.ts @@ -0,0 +1,103 @@ +import type { PersonaListCasesParams, PersonaListCasesResponse } from '@/tools/persona/types' +import { + asResourceList, + buildPersonaHeaders, + CASE_OUTPUT_PROPERTIES, + getNextCursor, + mapCase, + PERSONA_API_BASE, + parsePersonaResponse, +} from '@/tools/persona/utils' +import type { ToolConfig } from '@/tools/types' + +export const personaListCasesTool: ToolConfig = { + id: 'persona_list_cases', + name: 'Persona List Cases', + description: + 'List manual review cases, optionally filtered by status, account ID, or reference ID. Results are cursor-paginated.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Persona API key', + }, + status: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by case status (e.g. Open, Resolved)', + }, + accountId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by account ID (starts with act_)', + }, + referenceId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by reference ID', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of cases to return per page (1-100, default 10)', + }, + pageAfter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor: return cases after this case ID', + }, + }, + + request: { + url: (params) => { + const searchParams = new URLSearchParams() + if (params.status?.trim()) searchParams.set('filter[status]', params.status.trim()) + if (params.accountId?.trim()) searchParams.set('filter[account-id]', params.accountId.trim()) + if (params.referenceId?.trim()) { + searchParams.set('filter[reference-id]', params.referenceId.trim()) + } + if (params.pageSize) searchParams.set('page[size]', String(params.pageSize)) + if (params.pageAfter?.trim()) searchParams.set('page[after]', params.pageAfter.trim()) + const query = searchParams.toString() + return `${PERSONA_API_BASE}/cases${query ? `?${query}` : ''}` + }, + method: 'GET', + headers: (params) => buildPersonaHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parsePersonaResponse(response) + const cases = asResourceList(data.data) + return { + success: true, + output: { + cases: cases.map(mapCase), + nextCursor: getNextCursor(data.links), + }, + } + }, + + outputs: { + cases: { + type: 'array', + description: 'Cases matching the filters', + items: { + type: 'object', + properties: CASE_OUTPUT_PROPERTIES, + }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for the next page (pass as pageAfter), or null on the last page', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/persona/list_inquiries.ts b/apps/sim/tools/persona/list_inquiries.ts new file mode 100644 index 0000000000..b4284f2e2b --- /dev/null +++ b/apps/sim/tools/persona/list_inquiries.ts @@ -0,0 +1,128 @@ +import type { + PersonaListInquiriesParams, + PersonaListInquiriesResponse, +} from '@/tools/persona/types' +import { + asResourceList, + buildPersonaHeaders, + getNextCursor, + INQUIRY_OUTPUT_PROPERTIES, + mapInquiry, + PERSONA_API_BASE, + parsePersonaResponse, +} from '@/tools/persona/utils' +import type { ToolConfig } from '@/tools/types' + +export const personaListInquiriesTool: ToolConfig< + PersonaListInquiriesParams, + PersonaListInquiriesResponse +> = { + id: 'persona_list_inquiries', + name: 'Persona List Inquiries', + description: + 'List identity verification inquiries, optionally filtered by status, account ID, reference ID, or creation date range. Results are cursor-paginated.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Persona API key', + }, + status: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Filter by inquiry status (created, pending, completed, failed, expired, needs_review, approved, declined)', + }, + accountId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by account ID (starts with act_); comma-separate multiple IDs', + }, + referenceId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by reference ID', + }, + createdAtStart: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter to inquiries created at or after this ISO 8601 timestamp', + }, + createdAtEnd: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter to inquiries created at or before this ISO 8601 timestamp', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of inquiries to return per page (1-100, default 10)', + }, + pageAfter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor: return inquiries after this inquiry ID', + }, + }, + + request: { + url: (params) => { + const searchParams = new URLSearchParams() + if (params.status?.trim()) searchParams.set('filter[status]', params.status.trim()) + if (params.accountId?.trim()) searchParams.set('filter[account-id]', params.accountId.trim()) + if (params.referenceId?.trim()) { + searchParams.set('filter[reference-id]', params.referenceId.trim()) + } + if (params.createdAtStart?.trim()) { + searchParams.set('filter[created-at-start]', params.createdAtStart.trim()) + } + if (params.createdAtEnd?.trim()) { + searchParams.set('filter[created-at-end]', params.createdAtEnd.trim()) + } + if (params.pageSize) searchParams.set('page[size]', String(params.pageSize)) + if (params.pageAfter?.trim()) searchParams.set('page[after]', params.pageAfter.trim()) + const query = searchParams.toString() + return `${PERSONA_API_BASE}/inquiries${query ? `?${query}` : ''}` + }, + method: 'GET', + headers: (params) => buildPersonaHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parsePersonaResponse(response) + const inquiries = asResourceList(data.data) + return { + success: true, + output: { + inquiries: inquiries.map(mapInquiry), + nextCursor: getNextCursor(data.links), + }, + } + }, + + outputs: { + inquiries: { + type: 'array', + description: 'Inquiries matching the filters', + items: { + type: 'object', + properties: INQUIRY_OUTPUT_PROPERTIES, + }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for the next page (pass as pageAfter), or null on the last page', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/persona/list_inquiry_templates.ts b/apps/sim/tools/persona/list_inquiry_templates.ts new file mode 100644 index 0000000000..7735727f21 --- /dev/null +++ b/apps/sim/tools/persona/list_inquiry_templates.ts @@ -0,0 +1,86 @@ +import type { + PersonaListInquiryTemplatesParams, + PersonaListInquiryTemplatesResponse, +} from '@/tools/persona/types' +import { + asResourceList, + buildPersonaHeaders, + getNextCursor, + INQUIRY_TEMPLATE_OUTPUT_PROPERTIES, + mapInquiryTemplate, + PERSONA_API_BASE, + parsePersonaResponse, +} from '@/tools/persona/utils' +import type { ToolConfig } from '@/tools/types' + +export const personaListInquiryTemplatesTool: ToolConfig< + PersonaListInquiryTemplatesParams, + PersonaListInquiryTemplatesResponse +> = { + id: 'persona_list_inquiry_templates', + name: 'Persona List Inquiry Templates', + description: + 'List the inquiry templates in your Persona organization, to discover template IDs for creating inquiries. Results are cursor-paginated.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Persona API key', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of templates to return per page (1-100, default 10)', + }, + pageAfter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor: return templates after this template ID', + }, + }, + + request: { + url: (params) => { + const searchParams = new URLSearchParams() + if (params.pageSize) searchParams.set('page[size]', String(params.pageSize)) + if (params.pageAfter?.trim()) searchParams.set('page[after]', params.pageAfter.trim()) + const query = searchParams.toString() + return `${PERSONA_API_BASE}/inquiry-templates${query ? `?${query}` : ''}` + }, + method: 'GET', + headers: (params) => buildPersonaHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parsePersonaResponse(response) + const templates = asResourceList(data.data) + return { + success: true, + output: { + inquiryTemplates: templates.map(mapInquiryTemplate), + nextCursor: getNextCursor(data.links), + }, + } + }, + + outputs: { + inquiryTemplates: { + type: 'array', + description: 'Inquiry templates in the organization', + items: { + type: 'object', + properties: INQUIRY_TEMPLATE_OUTPUT_PROPERTIES, + }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for the next page (pass as pageAfter), or null on the last page', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/persona/list_reports.ts b/apps/sim/tools/persona/list_reports.ts new file mode 100644 index 0000000000..170778c4eb --- /dev/null +++ b/apps/sim/tools/persona/list_reports.ts @@ -0,0 +1,99 @@ +import type { PersonaListReportsParams, PersonaListReportsResponse } from '@/tools/persona/types' +import { + asResourceList, + buildPersonaHeaders, + getNextCursor, + mapReport, + PERSONA_API_BASE, + parsePersonaResponse, + REPORT_OUTPUT_PROPERTIES, +} from '@/tools/persona/utils' +import type { ToolConfig } from '@/tools/types' + +export const personaListReportsTool: ToolConfig< + PersonaListReportsParams, + PersonaListReportsResponse +> = { + id: 'persona_list_reports', + name: 'Persona List Reports', + description: + 'List screening reports, optionally filtered by account ID or reference ID. Results are cursor-paginated.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Persona API key', + }, + accountId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by account ID (starts with act_)', + }, + referenceId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by reference ID', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of reports to return per page (1-100, default 10)', + }, + pageAfter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor: return reports after this report ID', + }, + }, + + request: { + url: (params) => { + const searchParams = new URLSearchParams() + if (params.accountId?.trim()) searchParams.set('filter[account-id]', params.accountId.trim()) + if (params.referenceId?.trim()) { + searchParams.set('filter[reference-id]', params.referenceId.trim()) + } + if (params.pageSize) searchParams.set('page[size]', String(params.pageSize)) + if (params.pageAfter?.trim()) searchParams.set('page[after]', params.pageAfter.trim()) + const query = searchParams.toString() + return `${PERSONA_API_BASE}/reports${query ? `?${query}` : ''}` + }, + method: 'GET', + headers: (params) => buildPersonaHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parsePersonaResponse(response) + const reports = asResourceList(data.data) + return { + success: true, + output: { + reports: reports.map(mapReport), + nextCursor: getNextCursor(data.links), + }, + } + }, + + outputs: { + reports: { + type: 'array', + description: 'Reports matching the filters', + items: { + type: 'object', + properties: REPORT_OUTPUT_PROPERTIES, + }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for the next page (pass as pageAfter), or null on the last page', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/persona/mark_inquiry_for_review.ts b/apps/sim/tools/persona/mark_inquiry_for_review.ts new file mode 100644 index 0000000000..52cdf63f05 --- /dev/null +++ b/apps/sim/tools/persona/mark_inquiry_for_review.ts @@ -0,0 +1,64 @@ +import type { + PersonaInquiryResponse, + PersonaMarkInquiryForReviewParams, +} from '@/tools/persona/types' +import { + asResource, + buildPersonaHeaders, + INQUIRY_OUTPUT_PROPERTIES, + mapInquiry, + PERSONA_API_BASE, + parsePersonaResponse, +} from '@/tools/persona/utils' +import type { ToolConfig } from '@/tools/types' + +export const personaMarkInquiryForReviewTool: ToolConfig< + PersonaMarkInquiryForReviewParams, + PersonaInquiryResponse +> = { + id: 'persona_mark_inquiry_for_review', + name: 'Persona Mark Inquiry for Review', + description: + 'Mark an identity verification inquiry for manual review, moving it to the needs_review status.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Persona API key', + }, + inquiryId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Inquiry ID to mark for review (starts with inq_)', + }, + }, + + request: { + url: (params) => + `${PERSONA_API_BASE}/inquiries/${encodeURIComponent(params.inquiryId.trim())}/mark-for-review`, + method: 'POST', + headers: (params) => buildPersonaHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parsePersonaResponse(response) + return { + success: true, + output: { + inquiry: mapInquiry(asResource(data.data)), + }, + } + }, + + outputs: { + inquiry: { + type: 'object', + description: 'The inquiry marked for review', + properties: INQUIRY_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/persona/print_inquiry_pdf.ts b/apps/sim/tools/persona/print_inquiry_pdf.ts new file mode 100644 index 0000000000..a4eb206554 --- /dev/null +++ b/apps/sim/tools/persona/print_inquiry_pdf.ts @@ -0,0 +1,83 @@ +import type { + PersonaPrintInquiryPdfParams, + PersonaPrintInquiryPdfResponse, +} from '@/tools/persona/types' +import { + extractPersonaErrorMessage, + PERSONA_API_BASE, + PERSONA_API_VERSION, +} from '@/tools/persona/utils' +import type { ToolConfig } from '@/tools/types' + +export const personaPrintInquiryPdfTool: ToolConfig< + PersonaPrintInquiryPdfParams, + PersonaPrintInquiryPdfResponse +> = { + id: 'persona_print_inquiry_pdf', + name: 'Persona Print Inquiry PDF', + description: + 'Download a PDF summary of an inquiry, including its collected information and verification results.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Persona API key', + }, + inquiryId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Inquiry ID to print (starts with inq_)', + }, + }, + + request: { + url: (params) => + `${PERSONA_API_BASE}/inquiries/${encodeURIComponent(params.inquiryId.trim())}/print`, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Persona-Version': PERSONA_API_VERSION, + Accept: 'application/pdf', + }), + }, + + transformResponse: async (response, params) => { + if (!response.ok) { + const fallback = `Persona API error: ${response.status} ${response.statusText}` + const errorBody: unknown = await response + .text() + .then((text) => JSON.parse(text)) + .catch(() => null) + throw new Error(extractPersonaErrorMessage(errorBody, fallback)) + } + const arrayBuffer = await response.arrayBuffer() + const buffer = Buffer.from(arrayBuffer) + const inquiryId = params?.inquiryId?.trim() ?? 'inquiry' + return { + success: true, + output: { + file: { + name: `${inquiryId}.pdf`, + mimeType: 'application/pdf', + data: buffer.toString('base64'), + size: buffer.length, + }, + }, + } + }, + + outputs: { + file: { + type: 'file', + description: 'PDF summary of the inquiry, stored in execution files', + fileConfig: { + mimeType: 'application/pdf', + extension: 'pdf', + }, + }, + }, +} diff --git a/apps/sim/tools/persona/redact_account.ts b/apps/sim/tools/persona/redact_account.ts new file mode 100644 index 0000000000..8d75985ec6 --- /dev/null +++ b/apps/sim/tools/persona/redact_account.ts @@ -0,0 +1,60 @@ +import type { PersonaAccountResponse, PersonaRedactAccountParams } from '@/tools/persona/types' +import { + ACCOUNT_OUTPUT_PROPERTIES, + asResource, + buildPersonaHeaders, + mapAccount, + PERSONA_API_BASE, + parsePersonaResponse, +} from '@/tools/persona/utils' +import type { ToolConfig } from '@/tools/types' + +export const personaRedactAccountTool: ToolConfig< + PersonaRedactAccountParams, + PersonaAccountResponse +> = { + id: 'persona_redact_account', + name: 'Persona Redact Account', + description: + 'Permanently delete all personally identifiable information stored on an account, for example to honor a data deletion request. This cannot be undone.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Persona API key', + }, + accountId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Account ID to redact (starts with act_)', + }, + }, + + request: { + url: (params) => `${PERSONA_API_BASE}/accounts/${encodeURIComponent(params.accountId.trim())}`, + method: 'DELETE', + headers: (params) => buildPersonaHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parsePersonaResponse(response) + return { + success: true, + output: { + account: mapAccount(asResource(data.data)), + }, + } + }, + + outputs: { + account: { + type: 'object', + description: 'The redacted account (PII fields are removed)', + properties: ACCOUNT_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/persona/redact_inquiry.ts b/apps/sim/tools/persona/redact_inquiry.ts new file mode 100644 index 0000000000..1508d7c72b --- /dev/null +++ b/apps/sim/tools/persona/redact_inquiry.ts @@ -0,0 +1,60 @@ +import type { PersonaInquiryResponse, PersonaRedactInquiryParams } from '@/tools/persona/types' +import { + asResource, + buildPersonaHeaders, + INQUIRY_OUTPUT_PROPERTIES, + mapInquiry, + PERSONA_API_BASE, + parsePersonaResponse, +} from '@/tools/persona/utils' +import type { ToolConfig } from '@/tools/types' + +export const personaRedactInquiryTool: ToolConfig< + PersonaRedactInquiryParams, + PersonaInquiryResponse +> = { + id: 'persona_redact_inquiry', + name: 'Persona Redact Inquiry', + description: + 'Permanently delete all personally identifiable information collected by an inquiry, for example to honor a data deletion request. This cannot be undone.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Persona API key', + }, + inquiryId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Inquiry ID to redact (starts with inq_)', + }, + }, + + request: { + url: (params) => `${PERSONA_API_BASE}/inquiries/${encodeURIComponent(params.inquiryId.trim())}`, + method: 'DELETE', + headers: (params) => buildPersonaHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parsePersonaResponse(response) + return { + success: true, + output: { + inquiry: mapInquiry(asResource(data.data)), + }, + } + }, + + outputs: { + inquiry: { + type: 'object', + description: 'The redacted inquiry (PII fields are removed)', + properties: INQUIRY_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/persona/resume_inquiry.ts b/apps/sim/tools/persona/resume_inquiry.ts new file mode 100644 index 0000000000..5be169cd32 --- /dev/null +++ b/apps/sim/tools/persona/resume_inquiry.ts @@ -0,0 +1,74 @@ +import type { + PersonaResumeInquiryParams, + PersonaResumeInquiryResponse, +} from '@/tools/persona/types' +import { + asResource, + buildPersonaHeaders, + INQUIRY_OUTPUT_PROPERTIES, + mapInquiry, + PERSONA_API_BASE, + parsePersonaResponse, +} from '@/tools/persona/utils' +import type { ToolConfig } from '@/tools/types' + +export const personaResumeInquiryTool: ToolConfig< + PersonaResumeInquiryParams, + PersonaResumeInquiryResponse +> = { + id: 'persona_resume_inquiry', + name: 'Persona Resume Inquiry', + description: + 'Resume a pending or expired inquiry, creating a new session so the individual can continue verification. Returns a session token.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Persona API key', + }, + inquiryId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Inquiry ID to resume (starts with inq_)', + }, + }, + + request: { + url: (params) => + `${PERSONA_API_BASE}/inquiries/${encodeURIComponent(params.inquiryId.trim())}/resume`, + method: 'POST', + headers: (params) => buildPersonaHeaders(params.apiKey), + }, + + transformResponse: async (response) => { + const data = await parsePersonaResponse(response) + const sessionToken = data.meta?.['session-token'] + if (typeof sessionToken !== 'string' || sessionToken.length === 0) { + throw new Error('Persona did not return a session token; check the inquiry status') + } + return { + success: true, + output: { + inquiry: mapInquiry(asResource(data.data)), + sessionToken, + }, + } + }, + + outputs: { + inquiry: { + type: 'object', + description: 'The resumed inquiry', + properties: INQUIRY_OUTPUT_PROPERTIES, + }, + sessionToken: { + type: 'string', + description: + 'Session token for the new inquiry session, used to continue the flow in embedded SDKs', + }, + }, +} diff --git a/apps/sim/tools/persona/types.ts b/apps/sim/tools/persona/types.ts new file mode 100644 index 0000000000..ce9a2f0bcf --- /dev/null +++ b/apps/sim/tools/persona/types.ts @@ -0,0 +1,388 @@ +import type { ToolResponse } from '@/tools/types' + +interface PersonaBaseParams { + apiKey: string +} + +/** + * Flattened representation of a Persona Inquiry resource. + */ +export interface PersonaInquiry { + id: string + status: string | null + referenceId: string | null + note: string | null + tags: string[] + fields: Record | null + createdAt: string | null + startedAt: string | null + completedAt: string | null + failedAt: string | null + expiredAt: string | null + decisionedAt: string | null +} + +/** + * Flattened representation of a Persona Account resource. + */ +export interface PersonaAccount { + id: string + referenceId: string | null + accountTypeName: string | null + accountStatus: string | null + tags: string[] + fields: Record | null + createdAt: string | null + updatedAt: string | null +} + +/** + * Flattened representation of a Persona Case resource. + */ +export interface PersonaCase { + id: string + status: string | null + name: string | null + resolution: string | null + assigneeId: string | null + tags: string[] + fields: Record | null + createdAt: string | null + assignedAt: string | null + resolvedAt: string | null +} + +/** + * Flattened representation of a Persona Report resource. The `attributes` + * payload varies by report type, so the full attributes are preserved. + */ +export interface PersonaReport { + id: string + type: string + status: string | null + hasMatch: boolean | null + tags: string[] + createdAt: string | null + completedAt: string | null + attributes: Record +} + +/** + * Flattened representation of a Persona Verification resource. The + * `attributes` payload varies by verification type, so the full attributes + * are preserved. + */ +export interface PersonaVerification { + id: string + type: string + status: string | null + checks: Array> + countryCode: string | null + createdAt: string | null + submittedAt: string | null + completedAt: string | null + attributes: Record +} + +/** + * Flattened representation of a Persona Document resource. The `attributes` + * payload varies by document type, so the full attributes are preserved. + */ +export interface PersonaDocument { + id: string + type: string + status: string | null + kind: string | null + files: Array<{ filename: string | null; url: string | null; byteSize: number | null }> + createdAt: string | null + processedAt: string | null + attributes: Record +} + +/** + * Flattened representation of a Persona Account Importer resource. + */ +export interface PersonaImporter { + id: string + status: string | null + successfulCount: number + errorCount: number + duplicateCount: number + createdAt: string | null + completedAt: string | null +} + +/** + * Flattened representation of a Persona Inquiry Template resource. + */ +export interface PersonaInquiryTemplate { + id: string + name: string | null + status: string | null +} + +export interface PersonaCreateInquiryParams extends PersonaBaseParams { + inquiryTemplateId: string + accountId?: string + referenceId?: string + fields?: Record | string + note?: string + redirectUri?: string +} + +export interface PersonaGetInquiryParams extends PersonaBaseParams { + inquiryId: string +} + +export interface PersonaListInquiriesParams extends PersonaBaseParams { + status?: string + accountId?: string + referenceId?: string + createdAtStart?: string + createdAtEnd?: string + pageSize?: number + pageAfter?: string +} + +export interface PersonaUpdateInquiryParams extends PersonaBaseParams { + inquiryId: string + note?: string + fields?: Record | string + tags?: string[] | string + redirectUri?: string +} + +export interface PersonaApproveInquiryParams extends PersonaBaseParams { + inquiryId: string +} + +export interface PersonaMarkInquiryForReviewParams extends PersonaBaseParams { + inquiryId: string +} + +export interface PersonaResumeInquiryParams extends PersonaBaseParams { + inquiryId: string +} + +export interface PersonaExpireInquiryParams extends PersonaBaseParams { + inquiryId: string +} + +export interface PersonaRedactInquiryParams extends PersonaBaseParams { + inquiryId: string +} + +export interface PersonaDeclineInquiryParams extends PersonaBaseParams { + inquiryId: string +} + +export interface PersonaGenerateInquiryLinkParams extends PersonaBaseParams { + inquiryId: string + expiresInSeconds?: number +} + +export interface PersonaPrintInquiryPdfParams extends PersonaBaseParams { + inquiryId: string +} + +export interface PersonaCreateAccountParams extends PersonaBaseParams { + accountTypeId?: string + referenceId?: string + countryCode?: string + fields?: Record | string + tags?: string[] | string +} + +export interface PersonaGetAccountParams extends PersonaBaseParams { + accountId: string +} + +export interface PersonaUpdateAccountParams extends PersonaBaseParams { + accountId: string + referenceId?: string + countryCode?: string + fields?: Record | string + tags?: string[] | string +} + +export interface PersonaRedactAccountParams extends PersonaBaseParams { + accountId: string +} + +export interface PersonaListAccountsParams extends PersonaBaseParams { + referenceId?: string + pageSize?: number + pageAfter?: string +} + +export interface PersonaImportAccountsParams extends PersonaBaseParams { + file: Record +} + +export interface PersonaListCasesParams extends PersonaBaseParams { + status?: string + accountId?: string + referenceId?: string + pageSize?: number + pageAfter?: string +} + +export interface PersonaGetCaseParams extends PersonaBaseParams { + caseId: string +} + +export interface PersonaCreateReportParams extends PersonaBaseParams { + reportType: string + reportTemplateId: string + term?: string + nameFirst?: string + nameMiddle?: string + nameLast?: string + birthdate?: string + countryCode?: string + accountId?: string +} + +export interface PersonaGetReportParams extends PersonaBaseParams { + reportId: string +} + +export interface PersonaListReportsParams extends PersonaBaseParams { + accountId?: string + referenceId?: string + pageSize?: number + pageAfter?: string +} + +export interface PersonaListInquiryTemplatesParams extends PersonaBaseParams { + pageSize?: number + pageAfter?: string +} + +export interface PersonaGetVerificationParams extends PersonaBaseParams { + verificationId: string +} + +export interface PersonaGetDocumentParams extends PersonaBaseParams { + documentId: string +} + +export interface PersonaInquiryResponse extends ToolResponse { + output: { + inquiry: PersonaInquiry + } +} + +export interface PersonaListInquiriesResponse extends ToolResponse { + output: { + inquiries: PersonaInquiry[] + nextCursor: string | null + } +} + +export interface PersonaResumeInquiryResponse extends ToolResponse { + output: { + inquiry: PersonaInquiry + sessionToken: string + } +} + +export interface PersonaGenerateInquiryLinkResponse extends ToolResponse { + output: { + inquiry: PersonaInquiry + oneTimeLink: string + oneTimeLinkShort: string + } +} + +export interface PersonaPrintInquiryPdfResponse extends ToolResponse { + output: { + file: { + name: string + mimeType: string + data: string + size: number + } + } +} + +export interface PersonaAccountResponse extends ToolResponse { + output: { + account: PersonaAccount + } +} + +export interface PersonaListAccountsResponse extends ToolResponse { + output: { + accounts: PersonaAccount[] + nextCursor: string | null + } +} + +export interface PersonaImportAccountsResponse extends ToolResponse { + output: { + importer: PersonaImporter + } +} + +export interface PersonaCaseResponse extends ToolResponse { + output: { + case: PersonaCase + } +} + +export interface PersonaListCasesResponse extends ToolResponse { + output: { + cases: PersonaCase[] + nextCursor: string | null + } +} + +export interface PersonaReportResponse extends ToolResponse { + output: { + report: PersonaReport + } +} + +export interface PersonaListReportsResponse extends ToolResponse { + output: { + reports: PersonaReport[] + nextCursor: string | null + } +} + +export interface PersonaListInquiryTemplatesResponse extends ToolResponse { + output: { + inquiryTemplates: PersonaInquiryTemplate[] + nextCursor: string | null + } +} + +export interface PersonaVerificationResponse extends ToolResponse { + output: { + verification: PersonaVerification + } +} + +export interface PersonaDocumentResponse extends ToolResponse { + output: { + document: PersonaDocument + } +} + +export type PersonaResponse = + | PersonaInquiryResponse + | PersonaListInquiriesResponse + | PersonaResumeInquiryResponse + | PersonaGenerateInquiryLinkResponse + | PersonaListReportsResponse + | PersonaListInquiryTemplatesResponse + | PersonaPrintInquiryPdfResponse + | PersonaAccountResponse + | PersonaListAccountsResponse + | PersonaImportAccountsResponse + | PersonaCaseResponse + | PersonaListCasesResponse + | PersonaReportResponse + | PersonaVerificationResponse + | PersonaDocumentResponse diff --git a/apps/sim/tools/persona/update_account.ts b/apps/sim/tools/persona/update_account.ts new file mode 100644 index 0000000000..f1e45a30c4 --- /dev/null +++ b/apps/sim/tools/persona/update_account.ts @@ -0,0 +1,110 @@ +import type { PersonaAccountResponse, PersonaUpdateAccountParams } from '@/tools/persona/types' +import { + ACCOUNT_OUTPUT_PROPERTIES, + asResource, + buildPersonaHeaders, + mapAccount, + PERSONA_API_BASE, + parseJsonObjectParam, + parsePersonaResponse, + parseStringArrayParam, +} from '@/tools/persona/utils' +import type { ToolConfig } from '@/tools/types' + +export const personaUpdateAccountTool: ToolConfig< + PersonaUpdateAccountParams, + PersonaAccountResponse +> = { + id: 'persona_update_account', + name: 'Persona Update Account', + description: + 'Update an account’s reference ID, country code, fields, or tags. Only the provided values are changed.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Persona API key', + }, + accountId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Account ID to update (starts with act_)', + }, + referenceId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Reference ID that refers to an entity in your user model', + }, + countryCode: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'ISO 3166-1 alpha-2 country code (e.g. US)', + }, + fields: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: + 'JSON object of field name to field value pairs to set, as defined by the account type (e.g. {"name-first": "Jane"})', + }, + tags: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'JSON array of tag names to set on the account (e.g. ["vip"])', + }, + }, + + request: { + url: (params) => `${PERSONA_API_BASE}/accounts/${encodeURIComponent(params.accountId.trim())}`, + method: 'PATCH', + headers: (params) => buildPersonaHeaders(params.apiKey), + body: (params) => { + const attributes: Record = {} + if (params.referenceId?.trim()) { + attributes['reference-id'] = params.referenceId.trim() + } + if (params.countryCode?.trim()) { + attributes['country-code'] = params.countryCode.trim() + } + const fields = parseJsonObjectParam(params.fields, 'Fields') + if (fields) { + attributes.fields = fields + } + const tags = parseStringArrayParam(params.tags, 'Tags') + if (tags) { + attributes.tags = tags + } + if (Object.keys(attributes).length === 0) { + throw new Error( + 'Provide at least one of referenceId, countryCode, fields, or tags to update' + ) + } + return { data: { attributes } } + }, + }, + + transformResponse: async (response) => { + const data = await parsePersonaResponse(response) + return { + success: true, + output: { + account: mapAccount(asResource(data.data)), + }, + } + }, + + outputs: { + account: { + type: 'object', + description: 'The updated account', + properties: ACCOUNT_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/persona/update_inquiry.ts b/apps/sim/tools/persona/update_inquiry.ts new file mode 100644 index 0000000000..7f1305c166 --- /dev/null +++ b/apps/sim/tools/persona/update_inquiry.ts @@ -0,0 +1,108 @@ +import type { PersonaInquiryResponse, PersonaUpdateInquiryParams } from '@/tools/persona/types' +import { + asResource, + buildPersonaHeaders, + INQUIRY_OUTPUT_PROPERTIES, + mapInquiry, + PERSONA_API_BASE, + parseJsonObjectParam, + parsePersonaResponse, + parseStringArrayParam, +} from '@/tools/persona/utils' +import type { ToolConfig } from '@/tools/types' + +export const personaUpdateInquiryTool: ToolConfig< + PersonaUpdateInquiryParams, + PersonaInquiryResponse +> = { + id: 'persona_update_inquiry', + name: 'Persona Update Inquiry', + description: + 'Update an inquiry’s note, fields, tags, or redirect URI. Only the provided values are changed.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Persona API key', + }, + inquiryId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Inquiry ID to update (starts with inq_)', + }, + note: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Free-form note to set on the inquiry', + }, + fields: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: + 'JSON object of field name to field value pairs to set, as defined by the inquiry template (e.g. {"name-first": "Jane"})', + }, + tags: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'JSON array of tag names to set on the inquiry (e.g. ["vip"])', + }, + redirectUri: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'URI to redirect the individual to after completing the inquiry flow', + }, + }, + + request: { + url: (params) => `${PERSONA_API_BASE}/inquiries/${encodeURIComponent(params.inquiryId.trim())}`, + method: 'PATCH', + headers: (params) => buildPersonaHeaders(params.apiKey), + body: (params) => { + const attributes: Record = {} + if (params.note?.trim()) { + attributes.note = params.note.trim() + } + const fields = parseJsonObjectParam(params.fields, 'Fields') + if (fields) { + attributes.fields = fields + } + const tags = parseStringArrayParam(params.tags, 'Tags') + if (tags) { + attributes.tags = tags + } + if (params.redirectUri?.trim()) { + attributes['redirect-uri'] = params.redirectUri.trim() + } + if (Object.keys(attributes).length === 0) { + throw new Error('Provide at least one of note, fields, tags, or redirectUri to update') + } + return { data: { attributes } } + }, + }, + + transformResponse: async (response) => { + const data = await parsePersonaResponse(response) + return { + success: true, + output: { + inquiry: mapInquiry(asResource(data.data)), + }, + } + }, + + outputs: { + inquiry: { + type: 'object', + description: 'The updated inquiry', + properties: INQUIRY_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/persona/utils.ts b/apps/sim/tools/persona/utils.ts new file mode 100644 index 0000000000..c4c51e9f79 --- /dev/null +++ b/apps/sim/tools/persona/utils.ts @@ -0,0 +1,523 @@ +import type { + PersonaAccount, + PersonaCase, + PersonaDocument, + PersonaImporter, + PersonaInquiry, + PersonaInquiryTemplate, + PersonaReport, + PersonaVerification, +} from '@/tools/persona/types' +import type { OutputProperty } from '@/tools/types' + +export const PERSONA_API_BASE = 'https://api.withpersona.com/api/v1' + +/** + * Persona API version pinned for this integration so responses keep a stable + * shape regardless of the organization's dashboard default. + */ +export const PERSONA_API_VERSION = '2025-12-08' + +/** + * Raw JSON:API resource object returned by the Persona API. + */ +export interface PersonaResourceData { + type?: string + id?: string + attributes?: Record + relationships?: Record +} + +/** + * Top-level JSON:API envelope returned by the Persona API. + */ +export interface PersonaApiEnvelope { + data?: unknown + links?: unknown + meta?: Record +} + +/** + * Extracts a human-readable message from a Persona JSON:API error body. + */ +export function extractPersonaErrorMessage(body: unknown, fallback: string): string { + if (body !== null && typeof body === 'object') { + const errors = (body as Record).errors + if (Array.isArray(errors) && errors.length > 0) { + const first = errors[0] + if (first !== null && typeof first === 'object') { + const title = (first as Record).title + if (typeof title === 'string' && title.length > 0) { + return title + } + } + } + } + return fallback +} + +/** + * Reads a Persona JSON response, throwing a descriptive error for non-2xx + * responses using the JSON:API error format. + */ +export async function parsePersonaResponse(response: Response): Promise { + const body: unknown = await response.json().catch(() => null) + if (!response.ok) { + throw new Error( + extractPersonaErrorMessage( + body, + `Persona API error: ${response.status} ${response.statusText}` + ) + ) + } + return body !== null && typeof body === 'object' ? (body as PersonaApiEnvelope) : {} +} + +/** + * Narrows an unknown JSON:API `data` value to a single resource object. + */ +export function asResource(value: unknown): PersonaResourceData { + return value !== null && typeof value === 'object' && !Array.isArray(value) + ? (value as PersonaResourceData) + : {} +} + +/** + * Narrows an unknown JSON:API `data` value to a list of resource objects. + */ +export function asResourceList(value: unknown): PersonaResourceData[] { + return Array.isArray(value) ? value.map(asResource) : [] +} + +/** + * Builds the standard headers for Persona API requests. + */ +export function buildPersonaHeaders(apiKey: string): Record { + return { + Authorization: `Bearer ${apiKey}`, + 'Persona-Version': PERSONA_API_VERSION, + 'Content-Type': 'application/json', + Accept: 'application/json', + } +} + +/** + * Parses a JSON object param that may arrive as an object or a JSON string. + * Throws a descriptive error when the value is not a JSON object. + */ +export function parseJsonObjectParam( + value: Record | string | undefined, + label: string +): Record | undefined { + if (value === undefined || value === '') return undefined + let parsed: unknown = value + if (typeof value === 'string') { + try { + parsed = JSON.parse(value) + } catch { + throw new Error(`${label} must be a valid JSON object`) + } + } + if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) { + throw new Error(`${label} must be a JSON object of key-value pairs`) + } + return parsed as Record +} + +/** + * Parses a string array param that may arrive as an array or a JSON string. + * Throws a descriptive error when the value is not an array of strings. + */ +export function parseStringArrayParam( + value: string[] | string | undefined, + label: string +): string[] | undefined { + if (value === undefined || value === '') return undefined + let parsed: unknown = value + if (typeof value === 'string') { + try { + parsed = JSON.parse(value) + } catch { + throw new Error(`${label} must be a valid JSON array of strings`) + } + } + if (!Array.isArray(parsed) || !parsed.every((item) => typeof item === 'string')) { + throw new Error(`${label} must be a JSON array of strings`) + } + return parsed +} + +function getString(attrs: Record, key: string): string | null { + const value = attrs[key] + return typeof value === 'string' ? value : null +} + +function getBoolean(attrs: Record, key: string): boolean | null { + const value = attrs[key] + return typeof value === 'boolean' ? value : null +} + +function getNumber(attrs: Record, key: string, fallback: number): number { + const value = attrs[key] + return typeof value === 'number' ? value : fallback +} + +function getStringArray(attrs: Record, key: string): string[] { + const value = attrs[key] + return Array.isArray(value) + ? value.filter((item): item is string => typeof item === 'string') + : [] +} + +function getObject(attrs: Record, key: string): Record | null { + const value = attrs[key] + return value !== null && typeof value === 'object' && !Array.isArray(value) + ? (value as Record) + : null +} + +/** + * Extracts the `page[after]` cursor from a JSON:API `links.next` URL. + */ +export function getNextCursor(links: unknown): string | null { + if (links === null || typeof links !== 'object') return null + const next = (links as Record).next + if (typeof next !== 'string' || next.length === 0) return null + const queryIndex = next.indexOf('?') + if (queryIndex === -1) return null + const searchParams = new URLSearchParams(next.slice(queryIndex + 1)) + return searchParams.get('page[after]') +} + +/** + * Maps a raw Persona Inquiry resource to its flattened representation. + */ +export function mapInquiry(data: PersonaResourceData): PersonaInquiry { + const attrs = data.attributes ?? {} + return { + id: data.id ?? '', + status: getString(attrs, 'status'), + referenceId: getString(attrs, 'reference-id'), + note: getString(attrs, 'note'), + tags: getStringArray(attrs, 'tags'), + fields: getObject(attrs, 'fields'), + createdAt: getString(attrs, 'created-at'), + startedAt: getString(attrs, 'started-at'), + completedAt: getString(attrs, 'completed-at'), + failedAt: getString(attrs, 'failed-at'), + expiredAt: getString(attrs, 'expired-at'), + decisionedAt: getString(attrs, 'decisioned-at'), + } +} + +/** + * Maps a raw Persona Account resource to its flattened representation. + */ +export function mapAccount(data: PersonaResourceData): PersonaAccount { + const attrs = data.attributes ?? {} + return { + id: data.id ?? '', + referenceId: getString(attrs, 'reference-id'), + accountTypeName: getString(attrs, 'account-type-name'), + accountStatus: getString(attrs, 'account-status'), + tags: getStringArray(attrs, 'tags'), + fields: getObject(attrs, 'fields'), + createdAt: getString(attrs, 'created-at'), + updatedAt: getString(attrs, 'updated-at'), + } +} + +/** + * Maps a raw Persona Case resource to its flattened representation. + */ +export function mapCase(data: PersonaResourceData): PersonaCase { + const attrs = data.attributes ?? {} + return { + id: data.id ?? '', + status: getString(attrs, 'status'), + name: getString(attrs, 'name'), + resolution: getString(attrs, 'resolution'), + assigneeId: getString(attrs, 'assignee-id'), + tags: getStringArray(attrs, 'tags'), + fields: getObject(attrs, 'fields'), + createdAt: getString(attrs, 'created-at'), + assignedAt: getString(attrs, 'assigned-at'), + resolvedAt: getString(attrs, 'resolved-at'), + } +} + +/** + * Maps a raw Persona Report resource to its flattened representation. + */ +export function mapReport(data: PersonaResourceData): PersonaReport { + const attrs = data.attributes ?? {} + return { + id: data.id ?? '', + type: data.type ?? '', + status: getString(attrs, 'status'), + hasMatch: getBoolean(attrs, 'has-match'), + tags: getStringArray(attrs, 'tags'), + createdAt: getString(attrs, 'created-at'), + completedAt: getString(attrs, 'completed-at'), + attributes: attrs, + } +} + +/** + * Maps a raw Persona Verification resource to its flattened representation. + */ +export function mapVerification(data: PersonaResourceData): PersonaVerification { + const attrs = data.attributes ?? {} + const checks = attrs.checks + return { + id: data.id ?? '', + type: data.type ?? '', + status: getString(attrs, 'status'), + checks: Array.isArray(checks) + ? checks.filter( + (check): check is Record => check !== null && typeof check === 'object' + ) + : [], + countryCode: getString(attrs, 'country-code'), + createdAt: getString(attrs, 'created-at'), + submittedAt: getString(attrs, 'submitted-at'), + completedAt: getString(attrs, 'completed-at'), + attributes: attrs, + } +} + +/** + * Maps a raw Persona Document resource to its flattened representation. + */ +export function mapDocument(data: PersonaResourceData): PersonaDocument { + const attrs = data.attributes ?? {} + const files = attrs.files + return { + id: data.id ?? '', + type: data.type ?? '', + status: getString(attrs, 'status'), + kind: getString(attrs, 'kind'), + files: Array.isArray(files) + ? files + .filter( + (file): file is Record => file !== null && typeof file === 'object' + ) + .map((file) => ({ + filename: typeof file.filename === 'string' ? file.filename : null, + url: typeof file.url === 'string' ? file.url : null, + byteSize: typeof file['byte-size'] === 'number' ? file['byte-size'] : null, + })) + : [], + createdAt: getString(attrs, 'created-at'), + processedAt: getString(attrs, 'processed-at'), + attributes: attrs, + } +} + +/** + * Maps a raw Persona Inquiry Template resource to its flattened representation. + */ +export function mapInquiryTemplate(data: PersonaResourceData): PersonaInquiryTemplate { + const attrs = data.attributes ?? {} + return { + id: data.id ?? '', + name: getString(attrs, 'name'), + status: getString(attrs, 'status'), + } +} + +/** + * Maps a raw Persona Account Importer resource to its flattened representation. + */ +export function mapImporter(data: PersonaResourceData): PersonaImporter { + const attrs = data.attributes ?? {} + return { + id: data.id ?? '', + status: getString(attrs, 'status'), + successfulCount: getNumber(attrs, 'successful-count', 0), + errorCount: getNumber(attrs, 'error-count', 0), + duplicateCount: getNumber(attrs, 'duplicate-count', 0), + createdAt: getString(attrs, 'created-at'), + completedAt: getString(attrs, 'completed-at'), + } +} + +export const INQUIRY_OUTPUT_PROPERTIES: Record = { + id: { type: 'string', description: 'Inquiry ID (starts with inq_)' }, + status: { + type: 'string', + description: + 'Inquiry status (created, pending, completed, failed, expired, needs_review, approved, declined)', + nullable: true, + }, + referenceId: { + type: 'string', + description: 'Reference ID linking the inquiry to an entity in your user model', + nullable: true, + }, + note: { type: 'string', description: 'Free-form note on the inquiry', nullable: true }, + tags: { + type: 'array', + description: 'Tags associated with the inquiry', + items: { type: 'string' }, + }, + fields: { + type: 'json', + description: 'Field name to field value pairs collected by the inquiry template', + nullable: true, + }, + createdAt: { type: 'string', description: 'ISO 8601 creation timestamp', nullable: true }, + startedAt: { type: 'string', description: 'ISO 8601 start timestamp', nullable: true }, + completedAt: { type: 'string', description: 'ISO 8601 completion timestamp', nullable: true }, + failedAt: { type: 'string', description: 'ISO 8601 failure timestamp', nullable: true }, + expiredAt: { type: 'string', description: 'ISO 8601 expiration timestamp', nullable: true }, + decisionedAt: { type: 'string', description: 'ISO 8601 decision timestamp', nullable: true }, +} + +export const ACCOUNT_OUTPUT_PROPERTIES: Record = { + id: { type: 'string', description: 'Account ID (starts with act_)' }, + referenceId: { + type: 'string', + description: 'Reference ID linking the account to an entity in your user model', + nullable: true, + }, + accountTypeName: { type: 'string', description: 'Name of the account type', nullable: true }, + accountStatus: { type: 'string', description: 'Status set on the account', nullable: true }, + tags: { + type: 'array', + description: 'Tags associated with the account', + items: { type: 'string' }, + }, + fields: { + type: 'json', + description: 'Field name to field value pairs defined by the account type', + nullable: true, + }, + createdAt: { type: 'string', description: 'ISO 8601 creation timestamp', nullable: true }, + updatedAt: { type: 'string', description: 'ISO 8601 last update timestamp', nullable: true }, +} + +export const CASE_OUTPUT_PROPERTIES: Record = { + id: { type: 'string', description: 'Case ID (starts with case_)' }, + status: { type: 'string', description: 'Case status', nullable: true }, + name: { type: 'string', description: 'Case name', nullable: true }, + resolution: { type: 'string', description: 'Case resolution', nullable: true }, + assigneeId: { type: 'string', description: 'ID of the assigned reviewer', nullable: true }, + tags: { type: 'array', description: 'Tags associated with the case', items: { type: 'string' } }, + fields: { + type: 'json', + description: 'Field name to field value pairs defined by the case template', + nullable: true, + }, + createdAt: { type: 'string', description: 'ISO 8601 creation timestamp', nullable: true }, + assignedAt: { type: 'string', description: 'ISO 8601 assignment timestamp', nullable: true }, + resolvedAt: { type: 'string', description: 'ISO 8601 resolution timestamp', nullable: true }, +} + +export const REPORT_OUTPUT_PROPERTIES: Record = { + id: { type: 'string', description: 'Report ID (starts with rep_)' }, + type: { type: 'string', description: 'Report type (e.g. report/watchlist)' }, + status: { + type: 'string', + description: 'Report status (pending, ready, errored)', + nullable: true, + }, + hasMatch: { + type: 'boolean', + description: 'Whether the report found at least one match', + nullable: true, + optional: true, + }, + tags: { + type: 'array', + description: 'Tags associated with the report', + items: { type: 'string' }, + }, + createdAt: { type: 'string', description: 'ISO 8601 creation timestamp', nullable: true }, + completedAt: { type: 'string', description: 'ISO 8601 completion timestamp', nullable: true }, + attributes: { + type: 'json', + description: 'Full report attributes, which vary by report type', + }, +} + +export const VERIFICATION_OUTPUT_PROPERTIES: Record = { + id: { type: 'string', description: 'Verification ID (starts with ver_)' }, + type: { type: 'string', description: 'Verification type (e.g. verification/government-id)' }, + status: { + type: 'string', + description: + 'Verification status (initiated, submitted, passed, failed, requires_retry, canceled)', + nullable: true, + }, + checks: { + type: 'array', + description: 'Individual checks run as part of the verification', + items: { type: 'object' }, + }, + countryCode: { type: 'string', description: 'ISO 3166-1 alpha-2 country code', nullable: true }, + createdAt: { type: 'string', description: 'ISO 8601 creation timestamp', nullable: true }, + submittedAt: { type: 'string', description: 'ISO 8601 submission timestamp', nullable: true }, + completedAt: { type: 'string', description: 'ISO 8601 completion timestamp', nullable: true }, + attributes: { + type: 'json', + description: 'Full verification attributes, which vary by verification type', + }, +} + +export const DOCUMENT_OUTPUT_PROPERTIES: Record = { + id: { type: 'string', description: 'Document ID (starts with doc_)' }, + type: { type: 'string', description: 'Document type (e.g. document/government-id)' }, + status: { + type: 'string', + description: 'Document status (initiated, submitted, processed, errored)', + nullable: true, + }, + kind: { type: 'string', description: 'Kind of document collected', nullable: true }, + files: { + type: 'array', + description: 'Files uploaded to the document, with Persona-hosted download URLs', + items: { + type: 'object', + properties: { + filename: { type: 'string', description: 'Original file name', nullable: true }, + url: { + type: 'string', + description: 'Persona-hosted file URL (requires API key to download)', + nullable: true, + }, + byteSize: { type: 'number', description: 'File size in bytes', nullable: true }, + }, + }, + }, + createdAt: { type: 'string', description: 'ISO 8601 creation timestamp', nullable: true }, + processedAt: { type: 'string', description: 'ISO 8601 processing timestamp', nullable: true }, + attributes: { + type: 'json', + description: 'Full document attributes, which vary by document type', + }, +} + +export const INQUIRY_TEMPLATE_OUTPUT_PROPERTIES: Record = { + id: { type: 'string', description: 'Inquiry template ID (starts with itmpl_)' }, + name: { type: 'string', description: 'Name of the inquiry template', nullable: true }, + status: { + type: 'string', + description: 'Inquiry template status (active, inactive)', + nullable: true, + }, +} + +export const IMPORTER_OUTPUT_PROPERTIES: Record = { + id: { type: 'string', description: 'Importer ID (starts with mprt_)' }, + status: { + type: 'string', + description: 'Importer status (pending, ready, errored)', + nullable: true, + }, + successfulCount: { type: 'number', description: 'Number of rows imported successfully' }, + errorCount: { type: 'number', description: 'Number of rows that failed to import' }, + duplicateCount: { type: 'number', description: 'Number of duplicate rows skipped' }, + createdAt: { type: 'string', description: 'ISO 8601 creation timestamp', nullable: true }, + completedAt: { type: 'string', description: 'ISO 8601 completion timestamp', nullable: true }, +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index f0821ff3e2..35572109fb 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -2182,6 +2182,34 @@ import { pdlPersonSearchTool, } from '@/tools/peopledatalabs' import { perplexityChatTool, perplexitySearchTool } from '@/tools/perplexity' +import { + personaApproveInquiryTool, + personaCreateAccountTool, + personaCreateInquiryTool, + personaCreateReportTool, + personaDeclineInquiryTool, + personaExpireInquiryTool, + personaGenerateInquiryLinkTool, + personaGetAccountTool, + personaGetCaseTool, + personaGetDocumentTool, + personaGetInquiryTool, + personaGetReportTool, + personaGetVerificationTool, + personaImportAccountsTool, + personaListAccountsTool, + personaListCasesTool, + personaListInquiriesTool, + personaListInquiryTemplatesTool, + personaListReportsTool, + personaMarkInquiryForReviewTool, + personaPrintInquiryPdfTool, + personaRedactAccountTool, + personaRedactInquiryTool, + personaResumeInquiryTool, + personaUpdateAccountTool, + personaUpdateInquiryTool, +} from '@/tools/persona' import { pineconeFetchTool, pineconeGenerateEmbeddingsTool, @@ -5055,6 +5083,32 @@ export const tools: Record = { pdl_autocomplete: pdlAutocompleteTool, perplexity_chat: perplexityChatTool, perplexity_search: perplexitySearchTool, + persona_approve_inquiry: personaApproveInquiryTool, + persona_create_account: personaCreateAccountTool, + persona_create_inquiry: personaCreateInquiryTool, + persona_create_report: personaCreateReportTool, + persona_decline_inquiry: personaDeclineInquiryTool, + persona_expire_inquiry: personaExpireInquiryTool, + persona_generate_inquiry_link: personaGenerateInquiryLinkTool, + persona_get_account: personaGetAccountTool, + persona_get_case: personaGetCaseTool, + persona_get_document: personaGetDocumentTool, + persona_get_inquiry: personaGetInquiryTool, + persona_get_report: personaGetReportTool, + persona_get_verification: personaGetVerificationTool, + persona_import_accounts: personaImportAccountsTool, + persona_list_accounts: personaListAccountsTool, + persona_list_cases: personaListCasesTool, + persona_list_inquiries: personaListInquiriesTool, + persona_list_inquiry_templates: personaListInquiryTemplatesTool, + persona_list_reports: personaListReportsTool, + persona_mark_inquiry_for_review: personaMarkInquiryForReviewTool, + persona_print_inquiry_pdf: personaPrintInquiryPdfTool, + persona_redact_account: personaRedactAccountTool, + persona_redact_inquiry: personaRedactInquiryTool, + persona_resume_inquiry: personaResumeInquiryTool, + persona_update_account: personaUpdateAccountTool, + persona_update_inquiry: personaUpdateInquiryTool, profound_bot_logs: profoundBotLogsTool, profound_bots_report: profoundBotsReportTool, profound_category_assets: profoundCategoryAssetsTool,