veryfront 0.1.344 → 0.1.346

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -92,32 +92,6 @@ export default {
92
92
  "store.ts": "import { ragStore } from \"veryfront/embedding\";\n\nexport const store = ragStore({\n storagePath: \"data/index.json\",\n contentDir: \"content\",\n});\n"
93
93
  }
94
94
  },
95
- "integration:mailchimp": {
96
- "files": {
97
- "tools/list-campaigns.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listCampaigns } from \"../../lib/mailchimp-client.ts\";\n\nexport default tool({\n id: \"list-campaigns\",\n description:\n \"List email campaigns from Mailchimp. Can filter by status (save, paused, schedule, sending, sent).\",\n inputSchema: z.object({\n status: z\n .enum([\"save\", \"paused\", \"schedule\", \"sending\", \"sent\"])\n .optional()\n .describe(\"Filter campaigns by status\"),\n limit: z\n .number()\n .min(1)\n .max(50)\n .default(20)\n .describe(\"Maximum number of campaigns to return\"),\n }),\n async execute({ status, limit }) {\n const campaigns = await listCampaigns({ status, count: limit });\n\n return campaigns.map((campaign) => ({\n id: campaign.id,\n webId: campaign.web_id,\n type: campaign.type,\n status: campaign.status,\n title: campaign.settings.title,\n subject: campaign.settings.subject_line,\n fromName: campaign.settings.from_name,\n listName: campaign.recipients.list_name,\n emailsSent: campaign.emails_sent,\n sendTime: campaign.send_time,\n createdAt: campaign.create_time,\n archiveUrl: campaign.archive_url,\n reportSummary: campaign.report_summary\n ? {\n opens: campaign.report_summary.opens,\n uniqueOpens: campaign.report_summary.unique_opens,\n openRate: campaign.report_summary.open_rate,\n clicks: campaign.report_summary.clicks,\n clickRate: campaign.report_summary.click_rate,\n }\n : undefined,\n }));\n },\n});\n",
98
- "tools/list-lists.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listLists } from \"../../lib/mailchimp-client.ts\";\n\nexport default tool({\n id: \"list-lists\",\n description: \"List all audience lists (mailing lists) in Mailchimp with their statistics.\",\n inputSchema: z.object({\n limit: z.number().min(1).max(50).default(20).describe(\"Maximum number of lists to return\"),\n }),\n async execute({ limit }) {\n const lists = await listLists({ count: limit });\n\n return lists.map((list) => {\n const { contact, campaign_defaults: campaignDefaults, stats } = list;\n\n return {\n id: list.id,\n webId: list.web_id,\n name: list.name,\n dateCreated: list.date_created,\n listRating: list.list_rating,\n subscribeUrl: list.subscribe_url_short,\n contact: {\n company: contact.company,\n city: contact.city,\n state: contact.state,\n country: contact.country,\n },\n campaignDefaults: {\n fromName: campaignDefaults.from_name,\n fromEmail: campaignDefaults.from_email,\n subject: campaignDefaults.subject,\n language: campaignDefaults.language,\n },\n stats: {\n memberCount: stats.member_count,\n totalContacts: stats.total_contacts,\n unsubscribeCount: stats.unsubscribe_count,\n cleanedCount: stats.cleaned_count,\n campaignCount: stats.campaign_count,\n openRate: stats.open_rate,\n clickRate: stats.click_rate,\n },\n };\n });\n },\n});\n",
99
- "tools/get-campaign.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getCampaign } from \"../../lib/mailchimp-client.ts\";\n\nexport default tool({\n id: \"get-campaign\",\n description: \"Get details of a specific Mailchimp campaign by its ID.\",\n inputSchema: z.object({\n campaignId: z.string().describe(\"The ID of the campaign to retrieve\"),\n }),\n async execute({ campaignId }) {\n const campaign = await getCampaign(campaignId);\n const reportSummary = campaign.report_summary;\n\n return {\n id: campaign.id,\n webId: campaign.web_id,\n type: campaign.type,\n status: campaign.status,\n title: campaign.settings.title,\n subject: campaign.settings.subject_line,\n previewText: campaign.settings.preview_text,\n fromName: campaign.settings.from_name,\n replyTo: campaign.settings.reply_to,\n listId: campaign.recipients.list_id,\n listName: campaign.recipients.list_name,\n segmentText: campaign.recipients.segment_text,\n emailsSent: campaign.emails_sent,\n sendTime: campaign.send_time,\n createdAt: campaign.create_time,\n archiveUrl: campaign.archive_url,\n longArchiveUrl: campaign.long_archive_url,\n tracking: campaign.tracking,\n reportSummary: reportSummary\n ? {\n opens: reportSummary.opens,\n uniqueOpens: reportSummary.unique_opens,\n openRate: reportSummary.open_rate,\n clicks: reportSummary.clicks,\n subscriberClicks: reportSummary.subscriber_clicks,\n clickRate: reportSummary.click_rate,\n }\n : undefined,\n };\n },\n});\n",
100
- "tools/list-members.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listMembers } from \"../../lib/mailchimp-client.ts\";\n\nexport default tool({\n id: \"list-members\",\n description:\n \"List subscribers/members in a Mailchimp audience list. Can filter by subscription status.\",\n inputSchema: z.object({\n listId: z.string().describe(\"The ID of the audience list to get members from\"),\n status: z\n .enum([\"subscribed\", \"unsubscribed\", \"cleaned\", \"pending\", \"transactional\"])\n .optional()\n .describe(\"Filter members by subscription status\"),\n limit: z.number().min(1).max(50).default(20).describe(\"Maximum number of members to return\"),\n }),\n async execute({ listId, status, limit }) {\n const members = await listMembers(listId, { status, count: limit });\n\n return members.map((member) => {\n const location = member.location\n ? {\n countryCode: member.location.country_code,\n timezone: member.location.timezone,\n latitude: member.location.latitude,\n longitude: member.location.longitude,\n }\n : undefined;\n\n return {\n id: member.id,\n emailAddress: member.email_address,\n uniqueEmailId: member.unique_email_id,\n contactId: member.contact_id,\n fullName: member.full_name,\n status: member.status,\n emailType: member.email_type,\n vip: member.vip,\n language: member.language,\n memberRating: member.member_rating,\n lastChanged: member.last_changed,\n timestampSignup: member.timestamp_signup,\n timestampOpt: member.timestamp_opt,\n ipSignup: member.ip_signup,\n ipOpt: member.ip_opt,\n stats: {\n avgOpenRate: member.stats.avg_open_rate,\n avgClickRate: member.stats.avg_click_rate,\n },\n mergeFields: member.merge_fields,\n tags: member.tags.map(({ id, name }) => ({ id, name })),\n location,\n };\n });\n },\n});\n",
101
- "tools/get-list.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getList } from \"../../lib/mailchimp-client.ts\";\n\nexport default tool({\n id: \"get-list\",\n description: \"Get details of a specific Mailchimp audience list by its ID.\",\n inputSchema: z.object({\n listId: z.string().describe(\"The ID of the audience list to retrieve\"),\n }),\n async execute({ listId }) {\n const {\n id,\n web_id,\n name,\n date_created,\n list_rating,\n permission_reminder,\n subscribe_url_short,\n subscribe_url_long,\n contact,\n campaign_defaults,\n stats,\n } = await getList(listId);\n\n return {\n id,\n webId: web_id,\n name,\n dateCreated: date_created,\n listRating: list_rating,\n permissionReminder: permission_reminder,\n subscribeUrlShort: subscribe_url_short,\n subscribeUrlLong: subscribe_url_long,\n contact: {\n company: contact.company,\n address1: contact.address1,\n city: contact.city,\n state: contact.state,\n zip: contact.zip,\n country: contact.country,\n },\n campaignDefaults: {\n fromName: campaign_defaults.from_name,\n fromEmail: campaign_defaults.from_email,\n subject: campaign_defaults.subject,\n language: campaign_defaults.language,\n },\n stats: {\n memberCount: stats.member_count,\n totalContacts: stats.total_contacts,\n unsubscribeCount: stats.unsubscribe_count,\n cleanedCount: stats.cleaned_count,\n memberCountSinceSend: stats.member_count_since_send,\n unsubscribeCountSinceSend: stats.unsubscribe_count_since_send,\n cleanedCountSinceSend: stats.cleaned_count_since_send,\n campaignCount: stats.campaign_count,\n openRate: stats.open_rate,\n clickRate: stats.click_rate,\n },\n };\n },\n});\n",
102
- ".env.example": "# Mailchimp OAuth Configuration\n# Get your credentials from https://admin.mailchimp.com/account/oauth2/\nMAILCHIMP_CLIENT_ID=your-client-id\nMAILCHIMP_CLIENT_SECRET=your-client-secret\n",
103
- "lib/mailchimp-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nlet MAILCHIMP_BASE_URL = \"https://us1.api.mailchimp.com/3.0\";\n\ninterface MailchimpCampaign {\n id: string;\n web_id: number;\n type: string;\n create_time: string;\n archive_url: string;\n long_archive_url: string;\n status: string;\n emails_sent: number;\n send_time?: string;\n content_type: string;\n needs_block_refresh: boolean;\n recipients: {\n list_id: string;\n list_name: string;\n segment_text?: string;\n };\n settings: {\n subject_line: string;\n preview_text?: string;\n title: string;\n from_name: string;\n reply_to: string;\n };\n tracking: {\n opens: boolean;\n html_clicks: boolean;\n text_clicks: boolean;\n };\n report_summary?: {\n opens: number;\n unique_opens: number;\n open_rate: number;\n clicks: number;\n subscriber_clicks: number;\n click_rate: number;\n };\n}\n\ninterface MailchimpList {\n id: string;\n web_id: number;\n name: string;\n contact: {\n company: string;\n address1: string;\n city: string;\n state: string;\n zip: string;\n country: string;\n };\n permission_reminder: string;\n campaign_defaults: {\n from_name: string;\n from_email: string;\n subject: string;\n language: string;\n };\n stats: {\n member_count: number;\n total_contacts: number;\n unsubscribe_count: number;\n cleaned_count: number;\n member_count_since_send: number;\n unsubscribe_count_since_send: number;\n cleaned_count_since_send: number;\n campaign_count: number;\n open_rate: number;\n click_rate: number;\n };\n date_created: string;\n list_rating: number;\n subscribe_url_short: string;\n subscribe_url_long: string;\n}\n\ninterface MailchimpMember {\n id: string;\n email_address: string;\n unique_email_id: string;\n contact_id: string;\n full_name: string;\n web_id: number;\n email_type: string;\n status: \"subscribed\" | \"unsubscribed\" | \"cleaned\" | \"pending\" | \"transactional\";\n merge_fields: Record<string, unknown>;\n stats: {\n avg_open_rate: number;\n avg_click_rate: number;\n };\n ip_signup?: string;\n timestamp_signup?: string;\n ip_opt?: string;\n timestamp_opt?: string;\n member_rating: number;\n last_changed: string;\n language: string;\n vip: boolean;\n email_client?: string;\n location?: {\n latitude: number;\n longitude: number;\n gmtoff: number;\n dstoff: number;\n country_code: string;\n timezone: string;\n };\n tags: Array<{ id: number; name: string }>;\n}\n\ninterface MailchimpMetadata {\n dc: string;\n role: string;\n accountname: string;\n user_id: string;\n login: {\n email: string;\n avatar?: string;\n login_id: string;\n login_name: string;\n login_email: string;\n };\n}\n\nfunction buildQueryString(params: Record<string, string | number | undefined>): string {\n const searchParams = new URLSearchParams();\n\n for (const [key, value] of Object.entries(params)) {\n if (value == null) continue;\n searchParams.set(key, String(value));\n }\n\n const query = searchParams.toString();\n return query ? `?${query}` : \"\";\n}\n\nasync function fetchMetadata(token: string): Promise<MailchimpMetadata> {\n const response = await fetch(\"https://login.mailchimp.com/oauth2/metadata\", {\n headers: { Authorization: `OAuth ${token}` },\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch Mailchimp metadata: ${response.statusText}`);\n }\n\n return response.json();\n}\n\nasync function initializeBaseUrl(): Promise<void> {\n const token = await getAccessToken();\n if (!token) return;\n\n try {\n const metadata = await fetchMetadata(token);\n MAILCHIMP_BASE_URL = `https://${metadata.dc}.api.mailchimp.com/3.0`;\n } catch (error) {\n // Fallback to us1 if metadata fetch fails\n console.error(\"Failed to fetch Mailchimp metadata:\", error);\n }\n}\n\nasync function requireAccessToken(): Promise<string> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Mailchimp. Please connect your account.\");\n }\n return token;\n}\n\nasync function mailchimpFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await requireAccessToken();\n\n if (MAILCHIMP_BASE_URL === \"https://us1.api.mailchimp.com/3.0\") {\n await initializeBaseUrl();\n }\n\n const response = await fetch(`${MAILCHIMP_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({}))) as { detail?: string };\n throw new Error(`Mailchimp API error: ${response.status} ${error.detail ?? response.statusText}`);\n }\n\n return response.json();\n}\n\nexport async function listCampaigns(options?: {\n status?: \"save\" | \"paused\" | \"schedule\" | \"sending\" | \"sent\";\n count?: number;\n offset?: number;\n}): Promise<MailchimpCampaign[]> {\n const query = buildQueryString({\n status: options?.status,\n count: options?.count,\n offset: options?.offset,\n });\n\n const response = await mailchimpFetch<{ campaigns: MailchimpCampaign[] }>(`/campaigns${query}`);\n return response.campaigns;\n}\n\nexport async function getCampaign(campaignId: string): Promise<MailchimpCampaign> {\n return mailchimpFetch<MailchimpCampaign>(`/campaigns/${campaignId}`);\n}\n\nexport async function listLists(options?: { count?: number; offset?: number }): Promise<MailchimpList[]> {\n const query = buildQueryString({\n count: options?.count,\n offset: options?.offset,\n });\n\n const response = await mailchimpFetch<{ lists: MailchimpList[] }>(`/lists${query}`);\n return response.lists;\n}\n\nexport async function getList(listId: string): Promise<MailchimpList> {\n return mailchimpFetch<MailchimpList>(`/lists/${listId}`);\n}\n\nexport async function listMembers(\n listId: string,\n options?: {\n status?: \"subscribed\" | \"unsubscribed\" | \"cleaned\" | \"pending\" | \"transactional\";\n count?: number;\n offset?: number;\n },\n): Promise<MailchimpMember[]> {\n const query = buildQueryString({\n status: options?.status,\n count: options?.count,\n offset: options?.offset,\n });\n\n const response = await mailchimpFetch<{ members: MailchimpMember[] }>(\n `/lists/${listId}/members${query}`,\n );\n return response.members;\n}\n\nexport async function getMetadata(): Promise<MailchimpMetadata> {\n const token = await requireAccessToken();\n return fetchMetadata(token);\n}\n",
104
- "app/api/auth/mailchimp/callback/route.ts": "import { createOAuthCallbackHandler, mailchimpConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n async getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(mailchimpConfig, { tokenStore: hybridTokenStore });\n",
105
- "app/api/auth/mailchimp/route.ts": "import { createOAuthInitHandler, mailchimpConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT).\n// NEVER return a shared constant in production - it breaks per-user token isolation (VULN-AUTH-2).\nfunction getUserId(_request: Request): string {\n return \"current-user\";\n}\n\nexport const GET = createOAuthInitHandler(mailchimpConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});"
106
- }
107
- },
108
- "integration:intercom": {
109
- "files": {
110
- "tools/get-conversation.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getConversation } from \"../../lib/intercom-client.ts\";\n\nfunction toIsoFromSeconds(seconds: number): string {\n return new Date(seconds * 1000).toISOString();\n}\n\nfunction toIsoFromSecondsOrNull(seconds?: number | null): string | null {\n return seconds ? toIsoFromSeconds(seconds) : null;\n}\n\nexport default tool({\n id: \"get-conversation\",\n description:\n \"Get details of a specific conversation from Intercom, including all conversation parts/messages.\",\n inputSchema: z.object({\n conversationId: z.string().describe(\"The ID of the conversation to retrieve\"),\n }),\n async execute({ conversationId }): Promise<unknown> {\n const conversation = await getConversation(conversationId);\n\n const source = conversation.source;\n const sourceAuthor = source.author;\n\n return {\n id: conversation.id,\n title: conversation.title,\n state: conversation.state,\n read: conversation.read,\n priority: conversation.priority,\n createdAt: toIsoFromSeconds(conversation.created_at),\n updatedAt: toIsoFromSeconds(conversation.updated_at),\n waitingSince: toIsoFromSecondsOrNull(conversation.waiting_since),\n snoozedUntil: toIsoFromSecondsOrNull(conversation.snoozed_until),\n source: {\n type: source.type,\n subject: source.subject,\n body: source.body,\n author: {\n type: sourceAuthor.type,\n id: sourceAuthor.id,\n name: sourceAuthor.name,\n email: sourceAuthor.email,\n },\n },\n conversationParts:\n conversation.conversation_parts?.conversation_parts.map((part) => {\n const author = part.author;\n\n return {\n id: part.id,\n partType: part.part_type,\n body: part.body,\n createdAt: toIsoFromSeconds(part.created_at),\n author: {\n type: author.type,\n id: author.id,\n name: author.name,\n email: author.email,\n },\n };\n }) ?? [],\n contactIds: conversation.contacts?.map((c) => c.id),\n teammateIds: conversation.teammates?.map((t) => t.id),\n };\n },\n});\n",
111
- "tools/list-contacts.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listContacts } from \"../../lib/intercom-client.ts\";\n\nexport default tool({\n id: \"list-contacts\",\n description:\n \"List contacts from Intercom workspace. Returns contact information including email, name, and metadata.\",\n inputSchema: z.object({\n page: z.number().min(1).default(1).describe(\"Page number for pagination\"),\n perPage: z\n .number()\n .min(1)\n .max(150)\n .default(50)\n .describe(\"Number of contacts per page (max 150)\"),\n limit: z\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of contacts to return\"),\n }),\n async execute({ page, perPage, limit }) {\n const { contacts, hasMore } = await listContacts({ page, perPage });\n\n return {\n contacts: contacts.slice(0, limit).map((contact) => {\n const createdAt = new Date(contact.created_at * 1000).toISOString();\n const updatedAt = new Date(contact.updated_at * 1000).toISOString();\n const lastSeenAt = contact.last_seen_at\n ? new Date(contact.last_seen_at * 1000).toISOString()\n : null;\n\n return {\n id: contact.id,\n email: contact.email,\n name: contact.name,\n phone: contact.phone,\n role: contact.role,\n createdAt,\n updatedAt,\n lastSeenAt,\n ownerId: contact.owner_id,\n tags: contact.tags?.map((tag) => tag.name),\n };\n }),\n hasMore,\n page,\n };\n },\n});\n",
112
- "tools/list-conversations.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listConversations } from \"../../lib/intercom-client.ts\";\n\nfunction toIsoSeconds(seconds?: number | null): string | null {\n if (seconds == null) return null;\n return new Date(seconds * 1000).toISOString();\n}\n\nexport default tool({\n id: \"list-conversations\",\n description: \"List conversations from Intercom. Can filter by open/closed status.\",\n inputSchema: z.object({\n page: z.number().min(1).default(1).describe(\"Page number for pagination\"),\n perPage: z\n .number()\n .min(1)\n .max(150)\n .default(50)\n .describe(\"Number of conversations per page (max 150)\"),\n open: z\n .boolean()\n .optional()\n .describe(\"Filter by open (true) or closed (false) conversations\"),\n limit: z\n .number()\n .min(1)\n .max(50)\n .default(20)\n .describe(\"Maximum number of conversations to return\"),\n }),\n async execute({ page, perPage, open, limit }) {\n const { conversations, hasMore } = await listConversations({ page, perPage, open });\n\n return {\n conversations: conversations.slice(0, limit).map((conv) => ({\n id: conv.id,\n title: conv.title,\n state: conv.state,\n read: conv.read,\n priority: conv.priority,\n createdAt: toIsoSeconds(conv.created_at) as string,\n updatedAt: toIsoSeconds(conv.updated_at) as string,\n waitingSince: toIsoSeconds(conv.waiting_since),\n snoozedUntil: toIsoSeconds(conv.snoozed_until),\n source: {\n type: conv.source.type,\n subject: conv.source.subject,\n body: conv.source.body,\n author: {\n id: conv.source.author.id,\n name: conv.source.author.name,\n email: conv.source.author.email,\n },\n },\n contactIds: conv.contacts?.map((c) => c.id),\n teammateIds: conv.teammates?.map((t) => t.id),\n })),\n hasMore,\n page,\n };\n },\n});\n",
113
- "tools/send-message.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { sendMessage } from \"../../lib/intercom-client.ts\";\n\nexport default tool({\n id: \"send-message\",\n description: \"Send a message or reply to an existing conversation in Intercom.\",\n inputSchema: z.object({\n conversationId: z.string().describe(\"The ID of the conversation to reply to\"),\n body: z.string().describe(\"The message content to send\"),\n messageType: z\n .enum([\"comment\", \"note\"])\n .default(\"comment\")\n .describe(\"Type of message: 'comment' (visible to user) or 'note' (internal only)\"),\n adminId: z.string().optional().describe(\"The ID of the admin sending the message\"),\n }),\n async execute({ conversationId, body, messageType, adminId }) {\n const conversation = await sendMessage({ conversationId, body, messageType, adminId });\n\n return {\n success: true,\n conversation: {\n id: conversation.id,\n state: conversation.state,\n updatedAt: new Date(conversation.updated_at * 1000).toISOString(),\n },\n message: `Message sent successfully to conversation ${conversationId}`,\n };\n },\n});\n",
114
- "tools/get-contact.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getContact } from \"../../lib/intercom-client.ts\";\n\nfunction toIsoOrNull(timestamp?: number | null): string | null {\n if (!timestamp) return null;\n return new Date(timestamp * 1000).toISOString();\n}\n\nfunction toIso(timestamp: number): string {\n return new Date(timestamp * 1000).toISOString();\n}\n\nexport default tool({\n id: \"get-contact\",\n description: \"Get details of a specific contact from Intercom by their ID.\",\n inputSchema: z.object({\n contactId: z.string().describe(\"The ID of the contact to retrieve\"),\n }),\n async execute({ contactId }) {\n const contact = await getContact(contactId);\n\n return {\n id: contact.id,\n email: contact.email,\n name: contact.name,\n phone: contact.phone,\n role: contact.role,\n externalId: contact.external_id,\n avatar: contact.avatar,\n createdAt: toIso(contact.created_at),\n updatedAt: toIso(contact.updated_at),\n signedUpAt: toIsoOrNull(contact.signed_up_at),\n lastSeenAt: toIsoOrNull(contact.last_seen_at),\n ownerId: contact.owner_id,\n customAttributes: contact.custom_attributes,\n tags: contact.tags?.map((tag) => ({ id: tag.id, name: tag.name })),\n };\n },\n});\n",
115
- ".env.example": "# Intercom OAuth Configuration\n# Get your credentials from https://developers.intercom.com/building-apps/\nINTERCOM_CLIENT_ID=your-client-id\nINTERCOM_CLIENT_SECRET=your-client-secret\n",
116
- "lib/intercom-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst INTERCOM_BASE_URL = \"https://api.intercom.io\";\n\ninterface IntercomResponse<T> {\n type: string;\n data?: T;\n pages?: {\n next?: string | null;\n page: number;\n per_page: number;\n total_pages: number;\n };\n}\n\ninterface IntercomContact {\n type: \"contact\";\n id: string;\n external_id?: string;\n email?: string;\n phone?: string;\n name?: string;\n avatar?: string;\n role?: string;\n created_at: number;\n updated_at: number;\n signed_up_at?: number;\n last_seen_at?: number;\n owner_id?: number;\n custom_attributes?: Record<string, unknown>;\n tags?: Array<{ id: string; name: string }>;\n}\n\ninterface IntercomConversation {\n type: \"conversation\";\n id: string;\n created_at: number;\n updated_at: number;\n source: {\n type: string;\n id: string;\n delivered_as: string;\n subject?: string;\n body?: string;\n author: {\n type: string;\n id: string;\n name?: string;\n email?: string;\n };\n };\n contacts?: Array<{\n type: string;\n id: string;\n }>;\n teammates?: Array<{\n type: string;\n id: string;\n }>;\n title?: string;\n state: \"open\" | \"closed\" | \"snoozed\";\n read: boolean;\n waiting_since?: number;\n snoozed_until?: number;\n priority?: \"priority\" | \"not_priority\";\n conversation_parts?: {\n type: string;\n conversation_parts: Array<{\n type: string;\n id: string;\n part_type: string;\n body: string;\n created_at: number;\n updated_at: number;\n author: {\n type: string;\n id: string;\n name?: string;\n email?: string;\n };\n }>;\n };\n}\n\ninterface IntercomMessageRequest {\n message_type: \"inapp\" | \"email\" | \"comment\";\n body: string;\n from: {\n type: \"admin\" | \"user\" | \"contact\";\n id: string;\n };\n to?: {\n type: \"user\" | \"contact\";\n id?: string;\n email?: string;\n };\n}\n\nfunction hasMorePages(pages?: IntercomResponse<unknown>[\"pages\"]): boolean {\n return pages ? pages.page < pages.total_pages : false;\n}\n\nasync function intercomFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) throw new Error(\"Not authenticated with Intercom. Please connect your account.\");\n\n const response = await fetch(`${INTERCOM_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n \"Intercom-Version\": \"2.11\",\n ...options.headers,\n },\n });\n\n if (response.ok) return (await response.json()) as T;\n\n const error = (await response.json().catch(() => ({}))) as {\n errors?: Array<{ message?: string }>;\n message?: string;\n };\n\n const message = error.errors?.[0]?.message ?? error.message ?? response.statusText;\n throw new Error(`Intercom API error: ${response.status} ${message}`);\n}\n\nexport async function listContacts(\n options: { page?: number; perPage?: number } = {},\n): Promise<{ contacts: IntercomContact[]; hasMore: boolean }> {\n const params = new URLSearchParams({ per_page: String(options.perPage ?? 50) });\n if (options.page) params.set(\"page\", String(options.page));\n\n const response = await intercomFetch<IntercomResponse<IntercomContact[]>>(`/contacts?${params}`);\n\n return { contacts: response.data ?? [], hasMore: hasMorePages(response.pages) };\n}\n\nexport async function getContact(contactId: string): Promise<IntercomContact> {\n return intercomFetch<IntercomContact>(`/contacts/${contactId}`);\n}\n\nexport async function searchContacts(query: { email?: string; name?: string }): Promise<IntercomContact[]> {\n const value: Array<Record<string, unknown>> = [];\n\n if (query.email) value.push({ field: \"email\", operator: \"=\", value: query.email });\n if (query.name) value.push({ field: \"name\", operator: \"~\", value: query.name });\n\n const response = await intercomFetch<IntercomResponse<IntercomContact[]>>(\"/contacts/search\", {\n method: \"POST\",\n body: JSON.stringify({ query: { operator: \"AND\", value } }),\n });\n\n return response.data ?? [];\n}\n\nexport async function listConversations(\n options: { page?: number; perPage?: number; open?: boolean } = {},\n): Promise<{ conversations: IntercomConversation[]; hasMore: boolean }> {\n const params = new URLSearchParams({\n per_page: String(options.perPage ?? 50),\n display_as: \"plaintext\",\n });\n\n if (options.page) params.set(\"page\", String(options.page));\n if (options.open !== undefined) params.set(\"state\", options.open ? \"open\" : \"closed\");\n\n const response = await intercomFetch<IntercomResponse<IntercomConversation[]>>(\n `/conversations?${params}`,\n );\n\n return { conversations: response.data ?? [], hasMore: hasMorePages(response.pages) };\n}\n\nexport async function getConversation(conversationId: string): Promise<IntercomConversation> {\n return intercomFetch<IntercomConversation>(`/conversations/${conversationId}`);\n}\n\nexport async function sendMessage(options: {\n conversationId?: string;\n body: string;\n messageType?: \"comment\" | \"note\";\n adminId?: string;\n}): Promise<IntercomConversation> {\n if (!options.conversationId) throw new Error(\"conversationId is required to send a message\");\n\n const payload: Record<string, unknown> = {\n message_type: options.messageType ?? \"comment\",\n type: \"admin\",\n body: options.body,\n ...(options.adminId ? { admin_id: options.adminId } : {}),\n };\n\n return intercomFetch<IntercomConversation>(`/conversations/${options.conversationId}/reply`, {\n method: \"POST\",\n body: JSON.stringify(payload),\n });\n}\n\nexport async function createMessage(options: {\n contactId?: string;\n email?: string;\n body: string;\n messageType: \"inapp\" | \"email\";\n fromId: string;\n}): Promise<{ type: string; id: string }> {\n const messageBody: IntercomMessageRequest = {\n message_type: options.messageType,\n body: options.body,\n from: { type: \"admin\", id: options.fromId },\n };\n\n if (options.contactId) {\n messageBody.to = { type: \"contact\", id: options.contactId };\n } else if (options.email) {\n messageBody.to = { type: \"contact\", email: options.email };\n } else {\n throw new Error(\"Either contactId or email is required\");\n }\n\n return intercomFetch<{ type: string; id: string }>(\"/messages\", {\n method: \"POST\",\n body: JSON.stringify(messageBody),\n });\n}\n\nexport async function getMe(): Promise<{ type: string; id: string; name: string; email: string }> {\n return intercomFetch<{ type: string; id: string; name: string; email: string }>(\"/me\");\n}\n",
117
- "app/api/auth/intercom/callback/route.ts": "import { createOAuthCallbackHandler, intercomConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(intercomConfig, { tokenStore: hybridTokenStore });\n",
118
- "app/api/auth/intercom/route.ts": "import { createOAuthInitHandler, intercomConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT).\n// NEVER return a shared constant in production - it breaks per-user token isolation (VULN-AUTH-2).\nfunction getUserId(_request: Request): string {\n return \"current-user\";\n}\n\nexport const GET = createOAuthInitHandler(intercomConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});"
119
- }
120
- },
121
95
  "integration:neon": {
122
96
  "files": {
123
97
  "tools/list-tables.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getTableRowCount, listTables } from \"../../lib/neon-client.ts\";\n\nexport default tool({\n id: \"list-tables\",\n description:\n \"List all tables in the connected database. Returns table names, schemas, and row counts to help understand the database structure.\",\n inputSchema: z.object({\n schema: z.string().default(\"public\").describe(\"Schema name to list tables from\"),\n includeRowCounts: z\n .boolean()\n .default(false)\n .describe(\"Whether to include row counts for each table (slower but more informative)\"),\n }),\n async execute({ schema, includeRowCounts }) {\n const tables = await listTables(schema);\n\n const results = await Promise.all(\n tables.map(async (table) => {\n const result: {\n tablename: string;\n schemaname: string;\n tableowner: string;\n rowCount?: number;\n } = {\n tablename: table.tablename,\n schemaname: table.schemaname,\n tableowner: table.tableowner,\n };\n\n if (!includeRowCounts) return result;\n\n try {\n result.rowCount = await getTableRowCount(table.tablename, schema);\n } catch {\n result.rowCount = undefined;\n }\n\n return result;\n }),\n );\n\n return {\n schema,\n tableCount: results.length,\n tables: results,\n };\n },\n});\n",
@@ -143,17 +117,6 @@ export default {
143
117
  "app/api/auth/gitlab/route.ts": "import { createOAuthInitHandler, gitlabConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT).\n// NEVER return a shared constant in production - it breaks per-user token isolation (VULN-AUTH-2).\nfunction getUserId(_request: Request): string {\n return \"current-user\";\n}\n\nexport const GET = createOAuthInitHandler(gitlabConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});"
144
118
  }
145
119
  },
146
- "integration:twitter": {
147
- "files": {
148
- "tools/post-tweet.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createTwitterClient } from \"../../lib/twitter-client.ts\";\n\nexport default tool({\n id: \"post-tweet\",\n description: \"Post a new tweet to Twitter/X. Maximum length is 280 characters.\",\n inputSchema: z.object({\n text: z\n .string()\n .min(1)\n .max(280)\n .describe(\"Tweet text content (max 280 characters)\"),\n }),\n execute: async ({ text }, context) => {\n const userId = context?.userId ?? \"current-user\";\n\n try {\n const twitter = createTwitterClient(userId);\n const result = await twitter.postTweet(text);\n\n return {\n success: true,\n tweetId: result.id,\n text: result.text,\n message: \"Tweet posted successfully!\",\n url: `https://twitter.com/i/web/status/${result.id}`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Twitter not connected. Please connect your Twitter account.\",\n connectUrl: \"/api/auth/twitter\",\n };\n }\n throw error;\n }\n },\n});\n",
149
- "tools/get-timeline.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createTwitterClient } from \"../../lib/twitter-client.ts\";\n\nexport default tool({\n id: \"get-timeline\",\n description: \"Get the authenticated user's home timeline (tweets from followed accounts)\",\n inputSchema: z.object({\n maxResults: z\n .number()\n .min(1)\n .max(100)\n .optional()\n .describe(\"Maximum number of tweets to return (default: 10)\"),\n }),\n execute: async ({ maxResults }, context) => {\n const userId = context?.userId ?? \"current-user\";\n\n try {\n const twitter = createTwitterClient(userId);\n const tweets = await twitter.getTimeline({ maxResults: maxResults ?? 10 });\n\n if (!tweets?.length) {\n return {\n success: true,\n tweets: [],\n count: 0,\n message: \"No tweets found in your timeline.\",\n };\n }\n\n return {\n success: true,\n tweets: tweets.map((tweet) => ({\n id: tweet.id,\n text: tweet.text,\n author_id: tweet.author_id,\n created_at: tweet.created_at,\n metrics: tweet.public_metrics,\n hashtags: tweet.entities?.hashtags?.map((h) => h.tag),\n mentions: tweet.entities?.mentions?.map((m) => m.username),\n })),\n count: tweets.length,\n message: `Retrieved ${tweets.length} tweets from your timeline.`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Twitter not connected. Please connect your Twitter account.\",\n connectUrl: \"/api/auth/twitter\",\n };\n }\n throw error;\n }\n },\n});\n",
150
- "tools/search-tweets.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createTwitterClient } from \"../../lib/twitter-client.ts\";\n\nexport default tool({\n id: \"search-tweets\",\n description:\n \"Search recent tweets on Twitter/X. Returns up to 10 recent tweets matching the query.\",\n inputSchema: z.object({\n query: z\n .string()\n .min(1)\n .describe(\n \"Search query (supports Twitter search operators like 'from:', 'to:', '#hashtag', etc.)\",\n ),\n maxResults: z\n .number()\n .min(1)\n .max(100)\n .optional()\n .describe(\"Maximum number of tweets to return (default: 10)\"),\n sortOrder: z\n .enum([\"recency\", \"relevancy\"])\n .optional()\n .describe(\"Sort order: 'recency' (default) or 'relevancy'\"),\n }),\n execute: async ({ query, maxResults, sortOrder }, context) => {\n const userId = context?.userId ?? \"current-user\";\n\n try {\n const twitter = createTwitterClient(userId);\n const result = await twitter.searchTweets(query, {\n maxResults: maxResults ?? 10,\n sortOrder: sortOrder ?? \"recency\",\n });\n\n const tweets = result.data ?? [];\n const count = result.meta.result_count;\n\n if (tweets.length === 0) {\n return {\n success: true,\n tweets: [],\n count: 0,\n message: `No tweets found matching query: \"${query}\"`,\n };\n }\n\n return {\n success: true,\n tweets: tweets.map((tweet) => ({\n id: tweet.id,\n text: tweet.text,\n author_id: tweet.author_id,\n created_at: tweet.created_at,\n metrics: tweet.public_metrics,\n hashtags: tweet.entities?.hashtags?.map((h) => h.tag),\n mentions: tweet.entities?.mentions?.map((m) => m.username),\n })),\n count,\n message: `Found ${count} tweets matching query: \"${query}\"`,\n };\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"not connected\")) {\n return {\n error: \"Twitter not connected. Please connect your Twitter account.\",\n connectUrl: \"/api/auth/twitter\",\n };\n }\n throw error;\n }\n },\n});\n",
151
- ".env.example": "# Twitter / X Integration\n# Create an app at https://developer.twitter.com/en/portal/dashboard\n# Enable OAuth 2.0 and add callback URL: http://localhost:3000/api/auth/twitter/callback\n\nTWITTER_CLIENT_ID=your_client_id_here\nTWITTER_CLIENT_SECRET=your_client_secret_here\n",
152
- "lib/twitter-client.ts": "/**\n * Twitter API Client\n *\n * Provides a type-safe interface to Twitter API v2 operations.\n */\n\nimport { getValidToken } from \"./oauth.ts\";\n\nfunction getEnv(key: string): string | undefined {\n // @ts-ignore - Deno global\n if (typeof Deno !== \"undefined\") return Deno.env.get(key);\n // @ts-ignore - process global\n if (typeof process !== \"undefined\" && process.env) return process.env[key];\n return undefined;\n}\n\nconst TWITTER_API_BASE = \"https://api.twitter.com/2\";\n\nexport interface TwitterUser {\n id: string;\n name: string;\n username: string;\n created_at?: string;\n description?: string;\n location?: string;\n profile_image_url?: string;\n public_metrics?: {\n followers_count: number;\n following_count: number;\n tweet_count: number;\n listed_count: number;\n };\n verified?: boolean;\n}\n\nexport interface Tweet {\n id: string;\n text: string;\n author_id?: string;\n created_at?: string;\n public_metrics?: {\n retweet_count: number;\n reply_count: number;\n like_count: number;\n quote_count: number;\n };\n referenced_tweets?: Array<{\n type: string;\n id: string;\n }>;\n entities?: {\n hashtags?: Array<{ tag: string }>;\n mentions?: Array<{ username: string; id: string }>;\n urls?: Array<{ url: string; expanded_url: string }>;\n };\n}\n\nexport interface SearchResult {\n data: Tweet[];\n meta: {\n newest_id?: string;\n oldest_id?: string;\n result_count: number;\n next_token?: string;\n };\n}\n\nexport const twitterOAuthProvider = {\n name: \"twitter\",\n authorizationUrl: \"https://twitter.com/i/oauth2/authorize\",\n tokenUrl: \"https://api.twitter.com/2/oauth2/token\",\n clientId: getEnv(\"TWITTER_CLIENT_ID\") ?? \"\",\n clientSecret: getEnv(\"TWITTER_CLIENT_SECRET\") ?? \"\",\n scopes: [\n \"tweet.read\",\n \"tweet.write\",\n \"users.read\",\n \"follows.read\",\n \"offline.access\",\n ],\n callbackPath: \"/api/auth/twitter/callback\",\n usePKCE: true,\n};\n\nexport function createTwitterClient(userId: string): {\n getMe: () => Promise<TwitterUser>;\n getUserById: (userId: string) => Promise<TwitterUser>;\n getTweets: (\n userId: string,\n options?: { maxResults?: number; excludeReplies?: boolean },\n ) => Promise<Tweet[]>;\n getTweet: (tweetId: string) => Promise<Tweet>;\n postTweet: (text: string) => Promise<{ id: string; text: string }>;\n searchTweets: (\n query: string,\n options?: { maxResults?: number; sortOrder?: \"recency\" | \"relevancy\" },\n ) => Promise<SearchResult>;\n getTimeline: (options?: { maxResults?: number }) => Promise<Tweet[]>;\n} {\n async function getAccessToken(): Promise<string> {\n const token = await getValidToken(twitterOAuthProvider, userId, \"twitter\");\n if (token) return token;\n\n throw new Error(\n \"Twitter not connected. Please connect your Twitter account first.\",\n );\n }\n\n async function twitterFetch<T>(\n endpoint: string,\n options: RequestInit = {},\n ): Promise<T> {\n const accessToken = await getAccessToken();\n\n const response = await fetch(`${TWITTER_API_BASE}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${accessToken}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (response.ok) return response.json();\n\n const error = await response\n .json()\n .catch(() => ({ detail: response.statusText }));\n\n throw new Error(\n `Twitter API error: ${error.detail || error.title || response.statusText}`,\n );\n }\n\n function createTweetFieldsParams(\n options: { maxResults?: number },\n tweetFields: string,\n ): URLSearchParams {\n return new URLSearchParams({\n max_results: String(options.maxResults ?? 10),\n \"tweet.fields\": tweetFields,\n });\n }\n\n return {\n async getMe(): Promise<TwitterUser> {\n const result = await twitterFetch<{ data: TwitterUser }>(\n \"/users/me?user.fields=created_at,description,location,profile_image_url,public_metrics,verified\",\n );\n return result.data;\n },\n\n async getUserById(userId: string): Promise<TwitterUser> {\n const result = await twitterFetch<{ data: TwitterUser }>(\n `/users/${userId}?user.fields=created_at,description,location,profile_image_url,public_metrics,verified`,\n );\n return result.data;\n },\n\n async getTweets(\n userId: string,\n options: { maxResults?: number; excludeReplies?: boolean } = {},\n ): Promise<Tweet[]> {\n const params = createTweetFieldsParams(\n options,\n \"created_at,public_metrics,referenced_tweets,entities\",\n );\n\n if (options.excludeReplies) params.set(\"exclude\", \"replies\");\n\n const result = await twitterFetch<{ data: Tweet[] }>(\n `/users/${userId}/tweets?${params.toString()}`,\n );\n return result.data ?? [];\n },\n\n async getTweet(tweetId: string): Promise<Tweet> {\n const result = await twitterFetch<{ data: Tweet }>(\n `/tweets/${tweetId}?tweet.fields=created_at,public_metrics,referenced_tweets,entities,author_id`,\n );\n return result.data;\n },\n\n async postTweet(text: string): Promise<{ id: string; text: string }> {\n const result = await twitterFetch<{ data: { id: string; text: string } }>(\n \"/tweets\",\n {\n method: \"POST\",\n body: JSON.stringify({ text }),\n },\n );\n return result.data;\n },\n\n searchTweets(\n query: string,\n options: { maxResults?: number; sortOrder?: \"recency\" | \"relevancy\" } = {},\n ): Promise<SearchResult> {\n const params = new URLSearchParams({\n query,\n max_results: String(options.maxResults ?? 10),\n \"tweet.fields\":\n \"created_at,public_metrics,referenced_tweets,entities,author_id\",\n sort_order: options.sortOrder ?? \"recency\",\n });\n\n return twitterFetch<SearchResult>(\n `/tweets/search/recent?${params.toString()}`,\n );\n },\n\n async getTimeline(options: { maxResults?: number } = {}): Promise<Tweet[]> {\n const me = await twitterFetch<{ data: TwitterUser }>(\"/users/me\");\n const params = createTweetFieldsParams(\n options,\n \"created_at,public_metrics,referenced_tweets,entities,author_id\",\n );\n\n const result = await twitterFetch<{ data: Tweet[] }>(\n `/users/${me.data.id}/timelines/reverse_chronological?${params.toString()}`,\n );\n return result.data ?? [];\n },\n };\n}\n\nexport type TwitterClient = ReturnType<typeof createTwitterClient>;\n",
153
- "app/api/auth/twitter/callback/route.ts": "import { createOAuthCallbackHandler, twitterConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(twitterConfig, { tokenStore: hybridTokenStore });\n",
154
- "app/api/auth/twitter/route.ts": "import { createOAuthInitHandler, twitterConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT).\n// NEVER return a shared constant in production - it breaks per-user token isolation (VULN-AUTH-2).\nfunction getUserId(_request: Request): string {\n return \"current-user\";\n}\n\nexport const GET = createOAuthInitHandler(twitterConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});"
155
- }
156
- },
157
120
  "integration:anthropic": {
158
121
  "files": {
159
122
  "tools/list-workspaces.ts": "import { tool } from 'veryfront/tool';\nimport { z } from 'zod';\nimport { getAnthropicAdminClient } from '../../lib/anthropic-admin-client';\n\nexport const listWorkspaces = tool({\n id: 'list_workspaces',\n description:\n 'List all workspaces in the Anthropic organization. Workspaces allow you to organize API keys, usage, and permissions for different teams or projects.',\n inputSchema: z.object({}),\n execute: async () => {\n try {\n const client = getAnthropicAdminClient();\n const { workspaces } = await client.listWorkspaces();\n const count = workspaces.length;\n\n return {\n success: true,\n workspaces,\n count,\n message: `Found ${count} workspace(s)`,\n };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to list workspaces',\n workspaces: [],\n };\n }\n },\n});\n\nexport default listWorkspaces;\n",
@@ -189,19 +152,6 @@ export default {
189
152
  "lib/snowflake-client.ts": "import {\n getSnowflakeAccount,\n getSnowflakeDatabase,\n getSnowflakePassword,\n getSnowflakeSchema,\n getSnowflakeUsername,\n getSnowflakeWarehouse,\n} from \"./token-store.ts\";\n\ninterface SnowflakeStatementResponse {\n statementHandle: string;\n statementStatusUrl: string;\n message?: string;\n code?: string;\n}\n\ninterface SnowflakeQueryResult {\n resultSetMetaData: {\n rowType: Array<{\n name: string;\n type: string;\n nullable: boolean;\n scale?: number;\n precision?: number;\n length?: number;\n }>;\n numRows: number;\n format?: string;\n partitionInfo?: Array<{\n rowCount: number;\n uncompressedSize: number;\n }>;\n };\n data: unknown[][];\n code?: string;\n message?: string;\n statementHandle?: string;\n statementStatusUrl?: string;\n}\n\ninterface SnowflakeQueryStatusResponse {\n message: string;\n code: string;\n statementHandle: string;\n statementStatusUrl: string;\n sqlText?: string;\n resultSetMetaData?: SnowflakeQueryResult[\"resultSetMetaData\"];\n data?: unknown[][];\n stats?: {\n numRowsInserted?: number;\n numRowsUpdated?: number;\n numRowsDeleted?: number;\n numDuplicateRowsUpdated?: number;\n };\n}\n\ninterface DatabaseInfo {\n name: string;\n created_on: string;\n owner: string;\n comment?: string;\n}\n\ninterface SchemaInfo {\n name: string;\n database_name: string;\n created_on: string;\n owner: string;\n comment?: string;\n}\n\ninterface TableInfo {\n name: string;\n database_name: string;\n schema_name: string;\n kind: string;\n created_on: string;\n row_count?: number;\n bytes?: number;\n owner: string;\n comment?: string;\n}\n\ninterface ColumnInfo {\n name: string;\n type: string;\n kind: string;\n null?: string;\n default?: string;\n primary_key?: string;\n unique_key?: string;\n check?: string;\n expression?: string;\n comment?: string;\n}\n\ninterface SnowflakeError extends Error {\n code?: string;\n sqlState?: string;\n}\n\n/** Validate a Snowflake identifier (database, schema, or table name). */\nfunction validateIdentifier(value: string, label: string): string {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {\n throw new Error(\n `Invalid ${label}: must start with a letter or underscore and contain only letters, numbers, and underscores`,\n );\n }\n return value;\n}\n\nasync function snowflakeFetch<T>(\n endpoint: string,\n options: RequestInit = {},\n): Promise<T> {\n const account = getSnowflakeAccount();\n const username = getSnowflakeUsername();\n const password = getSnowflakePassword();\n\n const baseUrl = `https://${account}.snowflakecomputing.com/api/v2`;\n const authHeader = `Basic ${btoa(`${username}:${password}`)}`;\n\n const response = await fetch(`${baseUrl}${endpoint}`, {\n ...options,\n headers: {\n Authorization: authHeader,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n \"X-Snowflake-Authorization-Token-Type\": \"KEYPAIR_JWT\",\n ...options.headers,\n },\n });\n\n if (response.ok) return await response.json();\n\n const errorData = (await response.json().catch(() => ({}))) as Partial<SnowflakeError>;\n const errorMessage =\n errorData.message ??\n `Snowflake API error: ${response.status} ${response.statusText}`;\n\n const err: SnowflakeError = new Error(errorMessage);\n err.code = errorData.code;\n err.sqlState = errorData.sqlState;\n throw err;\n}\n\nasync function submitStatement(\n sqlText: string,\n database?: string,\n schema?: string,\n timeout?: number,\n async_exec = false,\n): Promise<SnowflakeStatementResponse | SnowflakeQueryResult> {\n const warehouse = getSnowflakeWarehouse();\n\n const requestBody = {\n statement: sqlText,\n warehouse,\n database: database ?? getSnowflakeDatabase(),\n schema: schema ?? getSnowflakeSchema(),\n timeout: timeout ?? 60,\n resultSetMetaData: { format: \"json\" },\n parameters: {},\n };\n\n const endpoint = async_exec ? \"/statements?async=true\" : \"/statements\";\n\n return await snowflakeFetch<SnowflakeStatementResponse | SnowflakeQueryResult>(\n endpoint,\n {\n method: \"POST\",\n body: JSON.stringify(requestBody),\n },\n );\n}\n\nexport async function getQueryStatus(\n statementHandle: string,\n): Promise<SnowflakeQueryStatusResponse> {\n return await snowflakeFetch<SnowflakeQueryStatusResponse>(\n `/statements/${statementHandle}`,\n );\n}\n\nexport async function cancelQuery(statementHandle: string): Promise<void> {\n await snowflakeFetch(`/statements/${statementHandle}/cancel`, {\n method: \"POST\",\n });\n}\n\nfunction transformResults(result: SnowflakeQueryResult): Record<string, unknown>[] {\n if (result.data.length === 0) return [];\n\n const columns = result.resultSetMetaData.rowType.map((col) => col.name);\n\n return result.data.map((row) => {\n const obj: Record<string, unknown> = {};\n for (let i = 0; i < columns.length; i++) obj[columns[i]] = row[i];\n return obj;\n });\n}\n\nexport async function runQuery(\n sql: string,\n database?: string,\n schema?: string,\n options: {\n timeout?: number;\n async?: boolean;\n } = {},\n): Promise<{\n columns: Array<{ name: string; type: string; nullable: boolean }>;\n rows: Record<string, unknown>[];\n rowCount: number;\n statementHandle?: string;\n}> {\n const result = await submitStatement(\n sql,\n database,\n schema,\n options.timeout,\n options.async,\n );\n\n if (\"statementHandle\" in result && !(\"data\" in result)) {\n return {\n columns: [],\n rows: [],\n rowCount: 0,\n statementHandle: result.statementHandle,\n };\n }\n\n const queryResult = result as SnowflakeQueryResult;\n\n return {\n columns: queryResult.resultSetMetaData.rowType.map((col) => ({\n name: col.name,\n type: col.type,\n nullable: col.nullable,\n })),\n rows: transformResults(queryResult),\n rowCount: queryResult.resultSetMetaData.numRows,\n statementHandle: queryResult.statementHandle,\n };\n}\n\nexport async function listDatabases(): Promise<DatabaseInfo[]> {\n const result = await runQuery(\"SHOW DATABASES\");\n return result.rows as DatabaseInfo[];\n}\n\nexport async function listSchemas(database: string): Promise<SchemaInfo[]> {\n validateIdentifier(database, \"database name\");\n const result = await runQuery(`SHOW SCHEMAS IN DATABASE ${database}`);\n return result.rows as SchemaInfo[];\n}\n\nexport async function listTables(\n database: string,\n schema: string,\n): Promise<TableInfo[]> {\n validateIdentifier(database, \"database name\");\n validateIdentifier(schema, \"schema name\");\n const result = await runQuery(`SHOW TABLES IN ${database}.${schema}`);\n return result.rows as TableInfo[];\n}\n\nexport async function describeTable(\n database: string,\n schema: string,\n table: string,\n): Promise<{\n columns: ColumnInfo[];\n primaryKeys: string[];\n}> {\n validateIdentifier(database, \"database name\");\n validateIdentifier(schema, \"schema name\");\n validateIdentifier(table, \"table name\");\n const result = await runQuery(`DESCRIBE TABLE ${database}.${schema}.${table}`);\n\n const columns = result.rows as ColumnInfo[];\n const primaryKeys = columns\n .filter((col) => col.primary_key === \"Y\")\n .map((col) => col.name);\n\n return { columns, primaryKeys };\n}\n\nexport async function getTableRowCount(\n database: string,\n schema: string,\n table: string,\n): Promise<number> {\n validateIdentifier(database, \"database name\");\n validateIdentifier(schema, \"schema name\");\n validateIdentifier(table, \"table name\");\n const result = await runQuery(\n `SELECT COUNT(*) as count FROM ${database}.${schema}.${table}`,\n );\n\n const count = result.rows[0]?.count;\n return count == null ? 0 : Number(count);\n}\n\nexport async function getSessionInfo(): Promise<{\n version: string;\n warehouse: string;\n database?: string;\n schema?: string;\n user: string;\n role?: string;\n}> {\n const result = await runQuery(`\n SELECT\n CURRENT_VERSION() as version,\n CURRENT_WAREHOUSE() as warehouse,\n CURRENT_DATABASE() as database,\n CURRENT_SCHEMA() as schema,\n CURRENT_USER() as user,\n CURRENT_ROLE() as role\n `);\n\n const row = result.rows[0];\n if (!row) throw new Error(\"Failed to get session info\");\n\n return row as {\n version: string;\n warehouse: string;\n database?: string;\n schema?: string;\n user: string;\n role?: string;\n };\n}\n"
190
153
  }
191
154
  },
192
- "integration:monday": {
193
- "files": {
194
- "tools/list-boards.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listBoards } from \"../../lib/monday-client.ts\";\n\nexport default tool({\n id: \"list-boards\",\n description: \"List all boards in Monday.com. Can optionally filter by workspace IDs.\",\n inputSchema: z.object({\n workspaceIds: z\n .array(z.string())\n .optional()\n .describe(\"Optional list of workspace IDs to filter boards\"),\n limit: z\n .number()\n .min(1)\n .max(100)\n .default(50)\n .describe(\"Maximum number of boards to return\"),\n page: z.number().min(1).default(1).describe(\"Page number for pagination\"),\n }),\n async execute({ workspaceIds, limit, page }) {\n const boards = await listBoards({ workspaceIds, limit, page });\n\n return boards.map((board) => ({\n id: board.id,\n name: board.name,\n description: board.description,\n boardKind: board.board_kind,\n state: board.state,\n workspaceId: board.workspace_id,\n }));\n },\n});\n",
195
- "tools/get-item.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getItem } from \"../../lib/monday-client.ts\";\n\nexport default tool({\n id: \"get-item\",\n description: \"Get details of a specific Monday.com item by its ID.\",\n inputSchema: z.object({\n itemId: z.string().describe(\"The ID of the item to retrieve\"),\n }),\n async execute({ itemId }) {\n const item = await getItem(itemId);\n\n return {\n id: item.id,\n name: item.name,\n state: item.state,\n board: item.board,\n group: item.group,\n columnValues: item.column_values?.map((columnValue) => ({\n id: columnValue.id,\n title: columnValue.title,\n text: columnValue.text,\n type: columnValue.type,\n value: columnValue.value,\n })),\n createdAt: item.created_at,\n updatedAt: item.updated_at,\n };\n },\n});\n",
196
- "tools/create-item.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createItem } from \"../../lib/monday-client.ts\";\n\nexport default tool({\n id: \"create-item\",\n description: \"Create a new item in a Monday.com board. Items are the rows in a board.\",\n inputSchema: z.object({\n boardId: z.string().describe(\"The ID of the board to create the item in\"),\n itemName: z.string().describe(\"The name/title of the item\"),\n groupId: z.string().optional().describe(\"Optional group ID within the board to add the item to\"),\n columnValues: z\n .record(z.unknown())\n .optional()\n .describe(\n \"Optional column values as a key-value object. Keys are column IDs, values depend on column type.\",\n ),\n }),\n async execute({ boardId, itemName, groupId, columnValues }) {\n const item = await createItem({ boardId, itemName, groupId, columnValues });\n\n return {\n success: true,\n item: {\n id: item.id,\n name: item.name,\n state: item.state,\n board: item.board,\n group: item.group,\n createdAt: item.created_at,\n },\n };\n },\n});\n",
197
- "tools/update-item.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { updateItem } from \"../../lib/monday-client.ts\";\n\nexport default tool({\n id: \"update-item\",\n description: \"Update an existing Monday.com item. Can update the name and/or column values.\",\n inputSchema: z.object({\n itemId: z.string().describe(\"The ID of the item to update\"),\n name: z.string().optional().describe(\"New name/title for the item\"),\n columnValues: z\n .record(z.unknown())\n .optional()\n .describe(\n \"Column values to update as a key-value object. Keys are column IDs, values depend on column type.\",\n ),\n }),\n async execute({ itemId, name, columnValues }) {\n const item = await updateItem(itemId, { name, columnValues });\n\n return {\n success: true,\n item: {\n id: item.id,\n name: item.name,\n state: item.state,\n columnValues: item.column_values?.map((column) => ({\n id: column.id,\n title: column.title,\n text: column.text,\n type: column.type,\n })),\n },\n };\n },\n});\n",
198
- "tools/list-items.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listItems } from \"../../lib/monday-client.ts\";\n\nexport default tool({\n id: \"list-items\",\n description: \"List items from a Monday.com board. Items are the rows in a board.\",\n inputSchema: z.object({\n boardId: z.string().describe(\"The ID of the board to list items from\"),\n limit: z.number().min(1).max(100).default(50).describe(\"Maximum number of items to return\"),\n page: z.number().min(1).default(1).describe(\"Page number for pagination\"),\n }),\n async execute({ boardId, limit, page }) {\n const items = await listItems({ boardId, limit, page });\n\n return items.map((item) => ({\n id: item.id,\n name: item.name,\n state: item.state,\n board: item.board,\n group: item.group,\n columnValues: item.column_values?.map((col) => ({\n id: col.id,\n title: col.title,\n text: col.text,\n type: col.type,\n })),\n createdAt: item.created_at,\n updatedAt: item.updated_at,\n }));\n },\n});\n",
199
- ".env.example": "# Monday.com OAuth Configuration\n# Get your credentials from https://monday.com/developers/apps\nMONDAY_CLIENT_ID=your-client-id\nMONDAY_CLIENT_SECRET=your-client-secret\n",
200
- "lib/monday-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst MONDAY_API_URL = \"https://api.monday.com/v2\";\n\ninterface MondayResponse<T> {\n data: T;\n account_id?: number;\n errors?: Array<{ message: string; locations?: unknown[] }>;\n}\n\ninterface MondayBoard {\n id: string;\n name: string;\n description?: string;\n board_kind: string;\n state: string;\n workspace_id?: string;\n created_at?: string;\n updated_at?: string;\n}\n\ninterface MondayItem {\n id: string;\n name: string;\n state?: string;\n board?: {\n id: string;\n name: string;\n };\n group?: {\n id: string;\n title: string;\n };\n column_values?: Array<{\n id: string;\n text?: string;\n title?: string;\n type?: string;\n value?: string;\n }>;\n created_at?: string;\n updated_at?: string;\n}\n\ninterface MondayUser {\n id: string;\n name: string;\n email: string;\n account: {\n id: string;\n name: string;\n };\n}\n\nasync function mondayFetch<T>(query: string, variables: Record<string, unknown> = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Monday.com. Please connect your account.\");\n }\n\n const response = await fetch(MONDAY_API_URL, {\n method: \"POST\",\n headers: {\n Authorization: token,\n \"Content-Type\": \"application/json\",\n \"API-Version\": \"2024-10\",\n },\n body: JSON.stringify({ query, variables }),\n });\n\n if (!response.ok) {\n throw new Error(`Monday.com API error: ${response.status} ${response.statusText}`);\n }\n\n const result: MondayResponse<T> = await response.json();\n const errorMessage = result.errors?.[0]?.message;\n if (errorMessage) {\n throw new Error(`Monday.com GraphQL error: ${errorMessage}`);\n }\n\n return result.data;\n}\n\nexport async function getMe(): Promise<MondayUser> {\n const query = `\n query {\n me {\n id\n name\n email\n account {\n id\n name\n }\n }\n }\n `;\n\n const data = await mondayFetch<{ me: MondayUser }>(query);\n return data.me;\n}\n\nexport async function listBoards(options?: {\n limit?: number;\n page?: number;\n workspaceIds?: string[];\n}): Promise<MondayBoard[]> {\n const limit = options?.limit ?? 50;\n const page = options?.page ?? 1;\n\n const workspaceIds = options?.workspaceIds?.length\n ? options.workspaceIds.map((id) => parseInt(id, 10))\n : null;\n\n const workspaceFilter = workspaceIds ? `, workspace_ids: [${workspaceIds.join(\",\")}]` : \"\";\n\n const query = `\n query {\n boards(limit: ${limit}, page: ${page}${workspaceFilter}) {\n id\n name\n description\n board_kind\n state\n workspace_id\n }\n }\n `;\n\n const data = await mondayFetch<{ boards: MondayBoard[] }>(query);\n return data.boards;\n}\n\nexport async function getBoard(boardId: string): Promise<MondayBoard> {\n const query = `\n query {\n boards(ids: [${boardId}]) {\n id\n name\n description\n board_kind\n state\n workspace_id\n }\n }\n `;\n\n const data = await mondayFetch<{ boards: MondayBoard[] }>(query);\n const board = data.boards?.[0];\n if (!board) {\n throw new Error(`Board with ID ${boardId} not found`);\n }\n\n return board;\n}\n\nexport async function listItems(options: {\n boardId: string;\n limit?: number;\n page?: number;\n}): Promise<MondayItem[]> {\n const limit = options.limit ?? 50;\n const page = options.page ?? 1;\n\n const query = `\n query {\n boards(ids: [${options.boardId}]) {\n items_page(limit: ${limit}, query_params: {page: ${page}}) {\n items {\n id\n name\n state\n board {\n id\n name\n }\n group {\n id\n title\n }\n column_values {\n id\n text\n title\n type\n value\n }\n created_at\n updated_at\n }\n }\n }\n }\n `;\n\n const data = await mondayFetch<{ boards: Array<{ items_page: { items: MondayItem[] } }> }>(query);\n return data.boards?.[0]?.items_page.items ?? [];\n}\n\nexport async function getItem(itemId: string): Promise<MondayItem> {\n const query = `\n query {\n items(ids: [${itemId}]) {\n id\n name\n state\n board {\n id\n name\n }\n group {\n id\n title\n }\n column_values {\n id\n text\n title\n type\n value\n }\n created_at\n updated_at\n }\n }\n `;\n\n const data = await mondayFetch<{ items: MondayItem[] }>(query);\n const item = data.items?.[0];\n if (!item) {\n throw new Error(`Item with ID ${itemId} not found`);\n }\n\n return item;\n}\n\nexport async function createItem(options: {\n boardId: string;\n groupId?: string;\n itemName: string;\n columnValues?: Record<string, unknown>;\n}): Promise<MondayItem> {\n const groupId = options.groupId ? `group_id: \"${options.groupId}\",` : \"\";\n const columnValues = options.columnValues\n ? `column_values: ${JSON.stringify(JSON.stringify(options.columnValues))},`\n : \"\";\n\n const query = `\n mutation {\n create_item(\n board_id: ${options.boardId},\n ${groupId}\n item_name: \"${options.itemName}\",\n ${columnValues}\n ) {\n id\n name\n state\n board {\n id\n name\n }\n group {\n id\n title\n }\n created_at\n }\n }\n `;\n\n const data = await mondayFetch<{ create_item: MondayItem }>(query);\n return data.create_item;\n}\n\nexport async function updateItem(\n itemId: string,\n updates: {\n columnValues?: Record<string, unknown>;\n name?: string;\n },\n): Promise<MondayItem> {\n if (updates.name) {\n const nameQuery = `\n mutation {\n change_simple_column_value(\n item_id: ${itemId},\n column_id: \"name\",\n value: \"${updates.name}\"\n ) {\n id\n name\n }\n }\n `;\n await mondayFetch<{ change_simple_column_value: MondayItem }>(nameQuery);\n }\n\n if (!updates.columnValues) {\n return getItem(itemId);\n }\n\n const columnValuesStr = JSON.stringify(JSON.stringify(updates.columnValues));\n const query = `\n mutation {\n change_multiple_column_values(\n item_id: ${itemId},\n column_values: ${columnValuesStr}\n ) {\n id\n name\n state\n column_values {\n id\n text\n title\n type\n value\n }\n }\n }\n `;\n\n const data = await mondayFetch<{ change_multiple_column_values: MondayItem }>(query);\n return data.change_multiple_column_values;\n}\n",
201
- "app/api/auth/monday/callback/route.ts": "import { createOAuthCallbackHandler, mondayConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(mondayConfig, { tokenStore: hybridTokenStore });\n",
202
- "app/api/auth/monday/route.ts": "import { createOAuthInitHandler, mondayConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT).\n// NEVER return a shared constant in production - it breaks per-user token isolation (VULN-AUTH-2).\nfunction getUserId(_request: Request): string {\n return \"current-user\";\n}\n\nexport const GET = createOAuthInitHandler(mondayConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});"
203
- }
204
- },
205
155
  "integration:_base": {
206
156
  "files": {
207
157
  "SETUP.md": "# Integration Setup Guide\n\nThis guide helps you set up credentials for all 50+ service integrations available in Veryfront.\n\n## Quick Start\n\n```bash\n# Create a new project with integrations\nveryfront init my-app --with ai --integrations slack,github,notion\n\n# Start development\ncd my-app\nveryfront dev\n```\n\nVisit `http://localhost:3000/api/auth/{service}` to connect each service.\n\n---\n\n## Table of Contents\n\n- [Google Services](#google-services) (Gmail, Calendar, Drive, Docs, Sheets)\n- [Microsoft Services](#microsoft-services) (Outlook, Teams, SharePoint, OneDrive)\n- [Atlassian Services](#atlassian-services) (Jira, Confluence)\n- [Communication](#communication) (Slack, Discord, Twilio, Zoom, Webex)\n- [Project Management](#project-management) (Asana, Monday, Trello, ClickUp, Linear, Notion)\n- [Developer Tools](#developer-tools) (GitHub, GitLab, Bitbucket, Figma, Sentry, PostHog)\n- [CRM & Sales](#crm--sales) (Salesforce, HubSpot, Pipedrive, Intercom, Zendesk, Freshdesk)\n- [Databases](#databases) (Supabase, Neon, Airtable, Snowflake)\n- [Cloud & Storage](#cloud--storage) (AWS, Dropbox, Box)\n- [Finance](#finance) (Stripe, QuickBooks, Xero)\n- [Marketing](#marketing) (Mailchimp, Twitter)\n- [E-commerce](#e-commerce) (Shopify)\n- [AI & Analytics](#ai--analytics) (Anthropic, Mixpanel)\n\n---\n\n## Google Services\n\n**Gmail, Calendar, Drive, Docs, Sheets** all use the same Google OAuth credentials.\n\n### Setup Steps\n\n1. Go to [Google Cloud Console](https://console.cloud.google.com/apis/credentials)\n2. Create a new project or select existing\n3. Enable required APIs:\n - Gmail API\n - Google Calendar API\n - Google Drive API\n - Google Docs API\n - Google Sheets API\n4. Go to **OAuth consent screen**:\n - User Type: External (or Internal for Workspace)\n - Add scopes for each API you need\n5. Go to **Credentials** > **Create Credentials** > **OAuth client ID**:\n - Application type: Web application\n - Authorized redirect URIs:\n ```\n http://localhost:3000/api/auth/gmail/callback\n http://localhost:3000/api/auth/calendar/callback\n http://localhost:3000/api/auth/drive/callback\n http://localhost:3000/api/auth/docs-google/callback\n http://localhost:3000/api/auth/sheets/callback\n ```\n\n### Environment Variables\n\n```env\nGOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com\nGOOGLE_CLIENT_SECRET=your-client-secret\n```\n\n### Required Scopes by Service\n\n| Service | Scopes |\n|---------|--------|\n| Gmail | `gmail.readonly`, `gmail.send`, `gmail.modify` |\n| Calendar | `calendar.readonly`, `calendar.events` |\n| Drive | `drive.readonly`, `drive.file` |\n| Docs | `documents.readonly`, `documents` |\n| Sheets | `spreadsheets.readonly`, `spreadsheets` |\n\n---\n\n## Microsoft Services\n\n**Outlook, Teams, SharePoint, OneDrive** use Microsoft OAuth (Azure AD).\n\n### Setup Steps\n\n1. Go to [Azure Portal](https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade)\n2. Click **New registration**:\n - Name: Your app name\n - Supported account types: Accounts in any organizational directory\n - Redirect URI: Web, `http://localhost:3000/api/auth/outlook/callback`\n3. After creation, go to **Certificates & secrets**:\n - Create a new client secret\n4. Go to **API permissions**:\n - Add Microsoft Graph permissions\n\n### Environment Variables\n\n```env\nMICROSOFT_CLIENT_ID=your-application-client-id\nMICROSOFT_CLIENT_SECRET=your-client-secret\nMICROSOFT_TENANT_ID=common\n```\n\n### Required Scopes by Service\n\n| Service | Scopes |\n|---------|--------|\n| Outlook | `Mail.Read`, `Mail.Send`, `Calendars.ReadWrite` |\n| Teams | `Team.ReadBasic.All`, `Chat.ReadWrite`, `ChannelMessage.Send` |\n| SharePoint | `Sites.Read.All`, `Files.ReadWrite.All` |\n| OneDrive | `Files.Read`, `Files.ReadWrite` |\n\n---\n\n## Atlassian Services\n\n**Jira and Confluence** use Atlassian OAuth 2.0 (3LO).\n\n### Setup Steps\n\n1. Go to [Atlassian Developer Console](https://developer.atlassian.com/console/myapps/)\n2. Click **Create** > **OAuth 2.0 integration**\n3. Configure:\n - Name: Your app name\n - Callback URL: `http://localhost:3000/api/auth/jira/callback`\n4. Add required scopes in **Permissions**\n5. Get your Cloud ID: Visit `https://your-domain.atlassian.net/_edge/tenant_info`\n\n### Environment Variables\n\n```env\nATLASSIAN_CLIENT_ID=your-client-id\nATLASSIAN_CLIENT_SECRET=your-client-secret\nATLASSIAN_CLOUD_ID=your-cloud-id\n```\n\n### Required Scopes\n\n| Service | Scopes |\n|---------|--------|\n| Jira | `read:jira-work`, `write:jira-work`, `read:jira-user` |\n| Confluence | `read:confluence-content.all`, `write:confluence-content` |\n\n---\n\n## Communication\n\n### Slack\n\n1. Go to [Slack API Apps](https://api.slack.com/apps)\n2. Click **Create New App** > **From scratch**\n3. Go to **OAuth & Permissions**:\n - Add redirect URL: `http://localhost:3000/api/auth/slack/callback`\n - Add scopes: `channels:read`, `chat:write`, `users:read`, `im:write`\n4. **Install to Workspace**\n\n```env\nSLACK_CLIENT_ID=your-client-id\nSLACK_CLIENT_SECRET=your-client-secret\n```\n\n### Discord\n\n1. Go to [Discord Developer Portal](https://discord.com/developers/applications)\n2. Create **New Application**\n3. Go to **OAuth2**:\n - Add redirect: `http://localhost:3000/api/auth/discord/callback`\n - Scopes: `identify`, `guilds`, `messages.read`\n\n```env\nDISCORD_CLIENT_ID=your-client-id\nDISCORD_CLIENT_SECRET=your-client-secret\n```\n\n### Twilio (SMS/WhatsApp)\n\n1. Go to [Twilio Console](https://console.twilio.com/)\n2. Get Account SID and Auth Token from dashboard\n3. Get or buy a phone number for sending\n\n```env\nTWILIO_ACCOUNT_SID=your-account-sid\nTWILIO_AUTH_TOKEN=your-auth-token\nTWILIO_PHONE_NUMBER=+1234567890\n```\n\n### Zoom\n\n1. Go to [Zoom App Marketplace](https://marketplace.zoom.us/develop/create)\n2. Create **OAuth App**\n3. Configure redirect: `http://localhost:3000/api/auth/zoom/callback`\n4. Add scopes: `meeting:read`, `meeting:write`, `user:read`\n\n```env\nZOOM_CLIENT_ID=your-client-id\nZOOM_CLIENT_SECRET=your-client-secret\n```\n\n### Webex\n\n1. Go to [Webex for Developers](https://developer.webex.com/my-apps)\n2. Create new integration\n3. Redirect URI: `http://localhost:3000/api/auth/webex/callback`\n4. Scopes: `spark:messages_read`, `spark:messages_write`, `spark:rooms_read`\n\n```env\nWEBEX_CLIENT_ID=your-client-id\nWEBEX_CLIENT_SECRET=your-client-secret\n```\n\n---\n\n## Project Management\n\n### Asana\n\n1. Go to [Asana Developer Console](https://app.asana.com/0/developer-console)\n2. Create new app\n3. Set redirect URL: `http://localhost:3000/api/auth/asana/callback`\n\n```env\nASANA_CLIENT_ID=your-client-id\nASANA_CLIENT_SECRET=your-client-secret\n```\n\n### Monday.com\n\n1. Go to [Monday Apps](https://auth.monday.com/oauth2/authorize)\n2. Create new app in your account's Developer section\n3. Configure OAuth with redirect: `http://localhost:3000/api/auth/monday/callback`\n\n```env\nMONDAY_CLIENT_ID=your-client-id\nMONDAY_CLIENT_SECRET=your-client-secret\n```\n\n### Trello\n\n1. Go to [Trello Power-Ups Admin](https://trello.com/power-ups/admin)\n2. Create new Power-Up\n3. Configure OAuth redirect: `http://localhost:3000/api/auth/trello/callback`\n\n```env\nTRELLO_API_KEY=your-api-key\nTRELLO_API_SECRET=your-api-secret\n```\n\n### ClickUp\n\n1. Go to [ClickUp API Settings](https://app.clickup.com/settings/apps)\n2. Create new app\n3. Redirect URL: `http://localhost:3000/api/auth/clickup/callback`\n\n```env\nCLICKUP_CLIENT_ID=your-client-id\nCLICKUP_CLIENT_SECRET=your-client-secret\n```\n\n### Linear\n\n1. Go to [Linear Settings > API](https://linear.app/settings/api)\n2. Create OAuth application\n3. Callback URL: `http://localhost:3000/api/auth/linear/callback`\n\n```env\nLINEAR_CLIENT_ID=your-client-id\nLINEAR_CLIENT_SECRET=your-client-secret\n```\n\n### Notion\n\n1. Go to [Notion Integrations](https://www.notion.so/my-integrations)\n2. Create new **public** integration (for OAuth)\n3. Set redirect URI: `http://localhost:3000/api/auth/notion/callback`\n4. **Important**: Share pages with your integration\n\n```env\nNOTION_CLIENT_ID=your-oauth-client-id\nNOTION_CLIENT_SECRET=your-oauth-client-secret\n```\n\n---\n\n## Developer Tools\n\n### GitHub\n\n1. Go to [GitHub Developer Settings](https://github.com/settings/developers)\n2. Create **New OAuth App**\n3. Authorization callback: `http://localhost:3000/api/auth/github/callback`\n\n```env\nGITHUB_CLIENT_ID=your-client-id\nGITHUB_CLIENT_SECRET=your-client-secret\n```\n\n### GitLab\n\n1. Go to [GitLab Applications](https://gitlab.com/-/profile/applications)\n2. Create new application\n3. Redirect URI: `http://localhost:3000/api/auth/gitlab/callback`\n4. Scopes: `read_user`, `read_api`, `read_repository`\n\n```env\nGITLAB_CLIENT_ID=your-application-id\nGITLAB_CLIENT_SECRET=your-secret\n```\n\n### Bitbucket\n\n1. Go to [Bitbucket App Passwords](https://bitbucket.org/account/settings/app-passwords/) or create OAuth consumer\n2. For OAuth: Workspace settings > OAuth consumers\n3. Callback URL: `http://localhost:3000/api/auth/bitbucket/callback`\n\n```env\nBITBUCKET_CLIENT_ID=your-client-id\nBITBUCKET_CLIENT_SECRET=your-client-secret\n```\n\n### Figma\n\n1. Go to [Figma Developers](https://www.figma.com/developers/apps)\n2. Create new app\n3. Callback URL: `http://localhost:3000/api/auth/figma/callback`\n\n```env\nFIGMA_CLIENT_ID=your-client-id\nFIGMA_CLIENT_SECRET=your-client-secret\n```\n\n### Sentry\n\n1. Go to [Sentry Developer Settings](https://sentry.io/settings/developer-settings/)\n2. Create new public integration\n3. Redirect URL: `http://localhost:3000/api/auth/sentry/callback`\n\n```env\nSENTRY_CLIENT_ID=your-client-id\nSENTRY_CLIENT_SECRET=your-client-secret\n```\n\n### PostHog\n\nUses API key authentication (no OAuth).\n\n1. Go to your PostHog project settings\n2. Create a personal API key\n\n```env\nPOSTHOG_API_KEY=phx_your-api-key\nPOSTHOG_HOST=https://app.posthog.com\n```\n\n---\n\n## CRM & Sales\n\n### Salesforce\n\n1. Go to [Salesforce Setup](https://login.salesforce.com/) > App Manager\n2. Create **New Connected App**\n3. Enable OAuth, add callback: `http://localhost:3000/api/auth/salesforce/callback`\n4. Required scopes: `api`, `refresh_token`\n\n```env\nSALESFORCE_CLIENT_ID=your-consumer-key\nSALESFORCE_CLIENT_SECRET=your-consumer-secret\n```\n\n### HubSpot\n\n1. Go to [HubSpot Developers](https://developers.hubspot.com/)\n2. Create app in your developer account\n3. Configure OAuth redirect: `http://localhost:3000/api/auth/hubspot/callback`\n4. Select required scopes\n\n```env\nHUBSPOT_CLIENT_ID=your-client-id\nHUBSPOT_CLIENT_SECRET=your-client-secret\n```\n\n### Pipedrive\n\n1. Go to [Pipedrive Marketplace Manager](https://developers.pipedrive.com/)\n2. Create new app\n3. OAuth redirect: `http://localhost:3000/api/auth/pipedrive/callback`\n\n```env\nPIPEDRIVE_CLIENT_ID=your-client-id\nPIPEDRIVE_CLIENT_SECRET=your-client-secret\n```\n\n### Intercom\n\n1. Go to [Intercom Developer Hub](https://developers.intercom.com/)\n2. Create new app\n3. Configure OAuth: `http://localhost:3000/api/auth/intercom/callback`\n\n```env\nINTERCOM_CLIENT_ID=your-client-id\nINTERCOM_CLIENT_SECRET=your-client-secret\n```\n\n### Zendesk\n\n1. Go to Admin Center > Apps and integrations > APIs > Zendesk API\n2. Create OAuth client\n3. Redirect URL: `http://localhost:3000/api/auth/zendesk/callback`\n\n```env\nZENDESK_CLIENT_ID=your-client-id\nZENDESK_CLIENT_SECRET=your-client-secret\nZENDESK_SUBDOMAIN=your-subdomain\n```\n\n### Freshdesk\n\nUses API key authentication.\n\n1. Go to Profile Settings in Freshdesk\n2. Find your API Key\n\n```env\nFRESHDESK_API_KEY=your-api-key\nFRESHDESK_DOMAIN=your-domain.freshdesk.com\n```\n\n---\n\n## Databases\n\n### Supabase\n\nUses API key (no OAuth needed).\n\n1. Go to your Supabase project dashboard\n2. Go to Settings > API\n3. Copy the `anon` or `service_role` key\n\n```env\nSUPABASE_URL=https://your-project.supabase.co\nSUPABASE_ANON_KEY=your-anon-key\nSUPABASE_SERVICE_ROLE_KEY=your-service-role-key\n```\n\n### Neon\n\nUses API key authentication.\n\n1. Go to [Neon Console](https://console.neon.tech/)\n2. Create API key in Account Settings\n\n```env\nNEON_API_KEY=your-api-key\nNEON_PROJECT_ID=your-project-id\n```\n\n### Airtable\n\n1. Go to [Airtable Account](https://airtable.com/account)\n2. Create personal access token or OAuth app\n3. For OAuth: [Airtable OAuth](https://airtable.com/create/oauth)\n\n```env\nAIRTABLE_API_KEY=your-api-key\n# Or for OAuth:\nAIRTABLE_CLIENT_ID=your-client-id\nAIRTABLE_CLIENT_SECRET=your-client-secret\n```\n\n### Snowflake\n\nUses account credentials (key-pair or password).\n\n1. Get your Snowflake account identifier\n2. Create a user with appropriate permissions\n3. (Optional) Set up key-pair authentication\n\n```env\nSNOWFLAKE_ACCOUNT=your-account-identifier\nSNOWFLAKE_USERNAME=your-username\nSNOWFLAKE_PASSWORD=your-password\nSNOWFLAKE_WAREHOUSE=your-warehouse\nSNOWFLAKE_DATABASE=your-database\n```\n\n---\n\n## Cloud & Storage\n\n### AWS\n\nUses IAM credentials.\n\n1. Go to [AWS IAM Console](https://console.aws.amazon.com/iam/)\n2. Create a new IAM user with programmatic access\n3. Attach policies for services you need (S3, EC2, Lambda, etc.)\n\n```env\nAWS_ACCESS_KEY_ID=your-access-key\nAWS_SECRET_ACCESS_KEY=your-secret-key\nAWS_REGION=us-east-1\n```\n\n### Dropbox\n\n1. Go to [Dropbox App Console](https://www.dropbox.com/developers/apps)\n2. Create app with Full Dropbox or App folder access\n3. OAuth2 redirect: `http://localhost:3000/api/auth/dropbox/callback`\n\n```env\nDROPBOX_CLIENT_ID=your-app-key\nDROPBOX_CLIENT_SECRET=your-app-secret\n```\n\n### Box\n\n1. Go to [Box Developer Console](https://app.box.com/developers/console)\n2. Create new app with OAuth 2.0\n3. Redirect URI: `http://localhost:3000/api/auth/box/callback`\n\n```env\nBOX_CLIENT_ID=your-client-id\nBOX_CLIENT_SECRET=your-client-secret\n```\n\n---\n\n## Finance\n\n### Stripe\n\nUses API key (no OAuth for basic usage).\n\n1. Go to [Stripe Dashboard](https://dashboard.stripe.com/apikeys)\n2. Get your secret key (use test key for development)\n\n```env\nSTRIPE_SECRET_KEY=sk_test_your-secret-key\nSTRIPE_PUBLISHABLE_KEY=pk_test_your-publishable-key\n```\n\n### QuickBooks\n\n1. Go to [Intuit Developer](https://developer.intuit.com/)\n2. Create app and get OAuth credentials\n3. Redirect URI: `http://localhost:3000/api/auth/quickbooks/callback`\n\n```env\nQUICKBOOKS_CLIENT_ID=your-client-id\nQUICKBOOKS_CLIENT_SECRET=your-client-secret\n```\n\n### Xero\n\n1. Go to [Xero Developer](https://developer.xero.com/app/manage)\n2. Create app\n3. Redirect URI: `http://localhost:3000/api/auth/xero/callback`\n\n```env\nXERO_CLIENT_ID=your-client-id\nXERO_CLIENT_SECRET=your-client-secret\n```\n\n---\n\n## Marketing\n\n### Mailchimp\n\n1. Go to [Mailchimp Account API Keys](https://us1.admin.mailchimp.com/account/api/)\n2. For OAuth: Register app at [Mailchimp OAuth](https://admin.mailchimp.com/account/oauth2/)\n3. Redirect: `http://localhost:3000/api/auth/mailchimp/callback`\n\n```env\nMAILCHIMP_CLIENT_ID=your-client-id\nMAILCHIMP_CLIENT_SECRET=your-client-secret\n# Or API key:\nMAILCHIMP_API_KEY=your-api-key-us1\n```\n\n### Twitter/X\n\n1. Go to [Twitter Developer Portal](https://developer.twitter.com/en/portal/dashboard)\n2. Create project and app\n3. Enable OAuth 2.0\n4. Callback URL: `http://localhost:3000/api/auth/twitter/callback`\n\n```env\nTWITTER_CLIENT_ID=your-client-id\nTWITTER_CLIENT_SECRET=your-client-secret\n```\n\n---\n\n## E-commerce\n\n### Shopify\n\n1. Go to [Shopify Partners](https://partners.shopify.com/)\n2. Create new app\n3. App URL and redirect: `http://localhost:3000/api/auth/shopify/callback`\n\n```env\nSHOPIFY_CLIENT_ID=your-api-key\nSHOPIFY_CLIENT_SECRET=your-api-secret\nSHOPIFY_SHOP_NAME=your-store.myshopify.com\n```\n\n---\n\n## AI & Analytics\n\n### Anthropic (Admin API)\n\nFor organization management and usage tracking.\n\n1. Go to [Anthropic Console](https://console.anthropic.com/)\n2. Create Admin API key (requires admin access)\n\n```env\nANTHROPIC_ADMIN_API_KEY=your-admin-api-key\n```\n\n### Mixpanel\n\nUses API key/secret for data export.\n\n1. Go to [Mixpanel Project Settings](https://mixpanel.com/settings/project)\n2. Get Project Token for tracking\n3. Get API Secret for data export\n\n```env\nMIXPANEL_PROJECT_TOKEN=your-project-token\nMIXPANEL_API_SECRET=your-api-secret\n```\n\n---\n\n## Testing Your Setup\n\nAfter configuring credentials:\n\n```bash\n# Start the dev server\nveryfront dev\n\n# Test each integration by visiting:\n# http://localhost:3000/api/auth/{service}\n\n# Check connection status\ncurl http://localhost:3000/api/connections\n```\n\n## Troubleshooting\n\n### Common Issues\n\n| Error | Solution |\n|-------|----------|\n| \"Invalid redirect URI\" | Ensure callback URL matches exactly (including trailing slash) |\n| \"Invalid client\" | Check CLIENT_ID is correct and app is published |\n| \"Access denied\" | Verify all required scopes are added |\n| \"Token expired\" | Implement refresh token flow or re-authenticate |\n\n### Debug Mode\n\nEnable debug logging:\n\n```bash\nDEBUG=veryfront:oauth veryfront dev\n```\n\n### Token Storage\n\nBy default, tokens are stored in memory. For production:\n\n1. Implement `TokenStore` interface in `lib/token-store.ts`\n2. Use Redis, database, or encrypted file storage\n3. Handle token refresh automatically\n\n## Production Checklist\n\n- [ ] Update all redirect URIs to production domain\n- [ ] Implement persistent token storage\n- [ ] Set up token encryption\n- [ ] Configure rate limiting\n- [ ] Add error monitoring (Sentry)\n- [ ] Test OAuth flows end-to-end\n- [ ] Review and minimize required scopes\n\n## Need Help?\n\n- Run `veryfront doctor` to diagnose issues\n- Check the [Veryfront Documentation](https://veryfront.com/docs)\n- Join our [Discord community](https://discord.gg/veryfront)\n",
@@ -288,19 +238,6 @@ export default {
288
238
  "app/api/auth/github/route.ts": "import { createOAuthInitHandler, githubConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT).\n// NEVER return a shared constant in production - it breaks per-user token isolation (VULN-AUTH-2).\nfunction getUserId(_request: Request): string {\n return \"current-user\";\n}\n\nexport const GET = createOAuthInitHandler(githubConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});"
289
239
  }
290
240
  },
291
- "integration:xero": {
292
- "files": {
293
- "tools/get-invoice.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getInvoice } from \"../../lib/xero-client.ts\";\n\nexport default tool({\n id: \"get-invoice\",\n description: \"Get details of a specific Xero invoice by its ID.\",\n inputSchema: z.object({\n invoiceId: z.string().describe(\"The ID of the invoice to retrieve\"),\n }),\n async execute({ invoiceId }) {\n const invoice = await getInvoice(invoiceId);\n\n return {\n invoiceId: invoice.InvoiceID,\n invoiceNumber: invoice.InvoiceNumber,\n type: invoice.Type,\n status: invoice.Status,\n contact: {\n contactId: invoice.Contact.ContactID,\n name: invoice.Contact.Name,\n },\n lineItems: invoice.LineItems.map((item) => ({\n lineItemId: item.LineItemID,\n description: item.Description,\n quantity: item.Quantity,\n unitAmount: item.UnitAmount,\n lineAmount: item.LineAmount,\n accountCode: item.AccountCode,\n taxType: item.TaxType,\n })),\n date: invoice.Date,\n dueDate: invoice.DueDate,\n subTotal: invoice.SubTotal,\n totalTax: invoice.TotalTax,\n total: invoice.Total,\n amountDue: invoice.AmountDue,\n amountPaid: invoice.AmountPaid,\n currencyCode: invoice.CurrencyCode,\n reference: invoice.Reference,\n updatedDateUTC: invoice.UpdatedDateUTC,\n };\n },\n});\n",
294
- "tools/list-invoices.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listInvoices } from \"../../lib/xero-client.ts\";\n\nexport default tool({\n id: \"list-invoices\",\n description: \"List invoices from Xero. Can filter by status, type, or contact.\",\n inputSchema: z.object({\n status: z\n .enum([\"DRAFT\", \"SUBMITTED\", \"AUTHORISED\", \"PAID\", \"VOIDED\"])\n .optional()\n .describe(\"Filter by invoice status\"),\n type: z\n .enum([\"ACCREC\", \"ACCPAY\"])\n .optional()\n .describe(\"Filter by invoice type (ACCREC = sales invoice, ACCPAY = bill)\"),\n contactId: z.string().optional().describe(\"Filter by contact ID\"),\n limit: z\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of invoices to return\"),\n }),\n async execute({ status, type, contactId, limit }) {\n const invoices = await listInvoices({ status, type, contactId, limit });\n\n return invoices.map(\n ({\n InvoiceID,\n InvoiceNumber,\n Type,\n Status,\n Contact,\n Date,\n DueDate,\n SubTotal,\n TotalTax,\n Total,\n AmountDue,\n AmountPaid,\n CurrencyCode,\n Reference,\n }) => ({\n invoiceId: InvoiceID,\n invoiceNumber: InvoiceNumber,\n type: Type,\n status: Status,\n contact: Contact.Name,\n date: Date,\n dueDate: DueDate,\n subTotal: SubTotal,\n totalTax: TotalTax,\n total: Total,\n amountDue: AmountDue,\n amountPaid: AmountPaid,\n currencyCode: CurrencyCode,\n reference: Reference,\n }),\n );\n },\n});\n",
295
- "tools/create-invoice.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createInvoice } from \"../../lib/xero-client.ts\";\n\nexport default tool({\n id: \"create-invoice\",\n description: \"Create a new invoice in Xero.\",\n inputSchema: z.object({\n contactId: z.string().describe(\"The ID of the contact for the invoice\"),\n type: z\n .enum([\"ACCREC\", \"ACCPAY\"])\n .describe(\"Invoice type (ACCREC = sales invoice, ACCPAY = bill)\"),\n date: z.string().describe(\"Invoice date in YYYY-MM-DD format\"),\n dueDate: z.string().describe(\"Due date in YYYY-MM-DD format\"),\n lineItems: z\n .array(\n z.object({\n description: z.string().describe(\"Line item description\"),\n quantity: z.number().describe(\"Quantity\"),\n unitAmount: z.number().describe(\"Unit price/amount\"),\n accountCode: z.string().optional().describe(\"Account code\"),\n taxType: z\n .string()\n .optional()\n .describe(\"Tax type (e.g., 'NONE', 'OUTPUT2', 'INPUT2')\"),\n }),\n )\n .describe(\"Line items for the invoice\"),\n reference: z.string().optional().describe(\"Optional reference number\"),\n status: z\n .enum([\"DRAFT\", \"SUBMITTED\", \"AUTHORISED\"])\n .optional()\n .describe(\"Invoice status (defaults to DRAFT)\"),\n }),\n async execute({\n contactId,\n type,\n date,\n dueDate,\n lineItems,\n reference,\n status,\n }) {\n const invoice = await createInvoice({\n contactId,\n type,\n date,\n dueDate,\n lineItems,\n reference,\n status,\n });\n\n return {\n success: true,\n invoice: {\n invoiceId: invoice.InvoiceID,\n invoiceNumber: invoice.InvoiceNumber,\n type: invoice.Type,\n status: invoice.Status,\n contact: invoice.Contact.Name,\n date: invoice.Date,\n dueDate: invoice.DueDate,\n total: invoice.Total,\n amountDue: invoice.AmountDue,\n },\n };\n },\n});\n",
296
- "tools/list-contacts.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listContacts } from \"../../lib/xero-client.ts\";\n\nexport default tool({\n id: \"list-contacts\",\n description: \"List contacts from Xero. Can filter by customer or supplier type.\",\n inputSchema: z.object({\n isCustomer: z.boolean().optional().describe(\"Filter for customers only\"),\n isSupplier: z.boolean().optional().describe(\"Filter for suppliers only\"),\n limit: z\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of contacts to return\"),\n }),\n async execute({ isCustomer, isSupplier, limit }) {\n const contacts = await listContacts({ isCustomer, isSupplier, limit });\n\n return contacts.map((contact) => ({\n contactId: contact.ContactID,\n contactNumber: contact.ContactNumber,\n name: contact.Name,\n firstName: contact.FirstName,\n lastName: contact.LastName,\n emailAddress: contact.EmailAddress,\n isCustomer: contact.IsCustomer,\n isSupplier: contact.IsSupplier,\n addresses: contact.Addresses?.map((addr) => ({\n addressType: addr.AddressType,\n city: addr.City,\n region: addr.Region,\n postalCode: addr.PostalCode,\n country: addr.Country,\n })),\n phones: contact.Phones?.map((phone) => ({\n phoneType: phone.PhoneType,\n phoneNumber: phone.PhoneNumber,\n })),\n }));\n },\n});\n",
297
- "tools/get-contact.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getContact } from \"../../lib/xero-client.ts\";\n\nexport default tool({\n id: \"get-contact\",\n description: \"Get details of a specific Xero contact by its ID.\",\n inputSchema: z.object({\n contactId: z.string().describe(\"The ID of the contact to retrieve\"),\n }),\n async execute({ contactId }) {\n const contact = await getContact(contactId);\n\n return {\n contactId: contact.ContactID,\n contactNumber: contact.ContactNumber,\n name: contact.Name,\n firstName: contact.FirstName,\n lastName: contact.LastName,\n emailAddress: contact.EmailAddress,\n isCustomer: contact.IsCustomer,\n isSupplier: contact.IsSupplier,\n addresses: contact.Addresses?.map((addr) => ({\n addressType: addr.AddressType,\n city: addr.City,\n region: addr.Region,\n postalCode: addr.PostalCode,\n country: addr.Country,\n attentionTo: addr.AttentionTo,\n })),\n phones: contact.Phones?.map((phone) => ({\n phoneType: phone.PhoneType,\n phoneNumber: phone.PhoneNumber,\n phoneAreaCode: phone.PhoneAreaCode,\n phoneCountryCode: phone.PhoneCountryCode,\n })),\n updatedDateUTC: contact.UpdatedDateUTC,\n };\n },\n});\n",
298
- ".env.example": "# Xero OAuth Configuration\n# Get your credentials from https://developer.xero.com/app/manage\nXERO_CLIENT_ID=your-client-id\nXERO_CLIENT_SECRET=your-client-secret\n",
299
- "lib/xero-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst XERO_BASE_URL = \"https://api.xero.com/api.xro/2.0\";\n\ninterface XeroResponse<T> {\n Id: string;\n Status: string;\n ProviderName: string;\n DateTimeUTC: string;\n [key: string]: T | string;\n}\n\ninterface XeroInvoice {\n InvoiceID: string;\n InvoiceNumber: string;\n Type: \"ACCREC\" | \"ACCPAY\";\n Status: string;\n LineAmountTypes: string;\n Contact: {\n ContactID: string;\n Name: string;\n };\n LineItems: Array<{\n LineItemID?: string;\n Description: string;\n Quantity: number;\n UnitAmount: number;\n AccountCode?: string;\n TaxType?: string;\n LineAmount: number;\n }>;\n Date: string;\n DueDate: string;\n SubTotal: number;\n TotalTax: number;\n Total: number;\n AmountDue: number;\n AmountPaid: number;\n CurrencyCode: string;\n Reference?: string;\n UpdatedDateUTC: string;\n}\n\ninterface XeroContact {\n ContactID: string;\n ContactNumber?: string;\n Name: string;\n FirstName?: string;\n LastName?: string;\n EmailAddress?: string;\n Addresses?: Array<{\n AddressType: string;\n City?: string;\n Region?: string;\n PostalCode?: string;\n Country?: string;\n AttentionTo?: string;\n }>;\n Phones?: Array<{\n PhoneType: string;\n PhoneNumber?: string;\n PhoneAreaCode?: string;\n PhoneCountryCode?: string;\n }>;\n UpdatedDateUTC: string;\n IsSupplier: boolean;\n IsCustomer: boolean;\n}\n\ninterface XeroTenant {\n tenantId: string;\n tenantType: string;\n tenantName: string;\n}\n\nasync function requireAccessToken(): Promise<string> {\n const token = await getAccessToken();\n if (!token) throw new Error(\"Not authenticated with Xero. Please connect your account.\");\n return token;\n}\n\nasync function getTenantId(token: string): Promise<string> {\n const response = await fetch(\"https://api.xero.com/connections\", {\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n },\n });\n\n if (!response.ok) throw new Error(`Failed to get Xero tenants: ${response.status}`);\n\n const tenants: XeroTenant[] = await response.json();\n const tenantId = tenants[0]?.tenantId;\n\n if (!tenantId) {\n throw new Error(\"No Xero organizations found. Please connect to a Xero organization.\");\n }\n\n return tenantId;\n}\n\nfunction getCollection<T extends Record<string, unknown>, K extends keyof T>(\n response: XeroResponse<T>,\n key: K,\n): T[K] {\n return response[key] as T[K];\n}\n\nasync function xeroFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await requireAccessToken();\n const tenantId = await getTenantId(token);\n\n const response = await fetch(`${XERO_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"xero-tenant-id\": tenantId,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n ...(options.headers ?? {}),\n },\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({}))) as {\n Message?: string;\n Detail?: string;\n };\n\n const message = error.Message ?? error.Detail ?? response.statusText;\n throw new Error(`Xero API error: ${response.status} ${message}`);\n }\n\n return response.json();\n}\n\nexport async function listInvoices(options?: {\n status?: \"DRAFT\" | \"SUBMITTED\" | \"AUTHORISED\" | \"PAID\" | \"VOIDED\";\n type?: \"ACCREC\" | \"ACCPAY\";\n contactId?: string;\n limit?: number;\n}): Promise<XeroInvoice[]> {\n const params = new URLSearchParams();\n const where: string[] = [];\n\n if (options?.status) where.push(`Status == \"${options.status}\"`);\n if (options?.type) where.push(`Type == \"${options.type}\"`);\n if (options?.contactId) where.push(`Contact.ContactID == Guid(\"${options.contactId}\")`);\n\n if (where.length) params.set(\"where\", where.join(\" AND \"));\n if (options?.limit) params.set(\"page\", \"1\");\n\n const queryString = params.toString();\n const endpoint = `/Invoices${queryString ? `?${queryString}` : \"\"}`;\n\n const response = await xeroFetch<XeroResponse<{ Invoices: XeroInvoice[] }>>(endpoint);\n const invoices = (getCollection(response, \"Invoices\") as XeroInvoice[] | undefined) ?? [];\n\n return options?.limit ? invoices.slice(0, options.limit) : invoices;\n}\n\nexport async function getInvoice(invoiceId: string): Promise<XeroInvoice> {\n const response = await xeroFetch<XeroResponse<{ Invoices: XeroInvoice[] }>>(\n `/Invoices/${invoiceId}`,\n );\n\n const invoices = getCollection(response, \"Invoices\") as XeroInvoice[] | undefined;\n const invoice = invoices?.[0];\n\n if (!invoice) throw new Error(`Invoice not found: ${invoiceId}`);\n return invoice;\n}\n\nexport async function createInvoice(options: {\n contactId: string;\n type: \"ACCREC\" | \"ACCPAY\";\n date: string;\n dueDate: string;\n lineItems: Array<{\n description: string;\n quantity: number;\n unitAmount: number;\n accountCode?: string;\n taxType?: string;\n }>;\n reference?: string;\n status?: \"DRAFT\" | \"SUBMITTED\" | \"AUTHORISED\";\n}): Promise<XeroInvoice> {\n const invoice = {\n Type: options.type,\n Contact: { ContactID: options.contactId },\n Date: options.date,\n DueDate: options.dueDate,\n LineItems: options.lineItems.map((item) => ({\n Description: item.description,\n Quantity: item.quantity,\n UnitAmount: item.unitAmount,\n AccountCode: item.accountCode,\n TaxType: item.taxType ?? \"NONE\",\n })),\n Reference: options.reference,\n Status: options.status ?? \"DRAFT\",\n LineAmountTypes: \"Exclusive\",\n };\n\n const response = await xeroFetch<XeroResponse<{ Invoices: XeroInvoice[] }>>(\"/Invoices\", {\n method: \"POST\",\n body: JSON.stringify({ Invoices: [invoice] }),\n });\n\n const invoices = getCollection(response, \"Invoices\") as XeroInvoice[] | undefined;\n const created = invoices?.[0];\n\n if (!created) throw new Error(\"Failed to create invoice\");\n return created;\n}\n\nexport async function listContacts(options?: {\n isCustomer?: boolean;\n isSupplier?: boolean;\n limit?: number;\n}): Promise<XeroContact[]> {\n const params = new URLSearchParams();\n const where: string[] = [];\n\n if (options?.isCustomer !== undefined) where.push(`IsCustomer == ${options.isCustomer}`);\n if (options?.isSupplier !== undefined) where.push(`IsSupplier == ${options.isSupplier}`);\n\n if (where.length) params.set(\"where\", where.join(\" AND \"));\n\n const queryString = params.toString();\n const endpoint = `/Contacts${queryString ? `?${queryString}` : \"\"}`;\n\n const response = await xeroFetch<XeroResponse<{ Contacts: XeroContact[] }>>(endpoint);\n const contacts = (getCollection(response, \"Contacts\") as XeroContact[] | undefined) ?? [];\n\n return options?.limit ? contacts.slice(0, options.limit) : contacts;\n}\n\nexport async function getContact(contactId: string): Promise<XeroContact> {\n const response = await xeroFetch<XeroResponse<{ Contacts: XeroContact[] }>>(\n `/Contacts/${contactId}`,\n );\n\n const contacts = getCollection(response, \"Contacts\") as XeroContact[] | undefined;\n const contact = contacts?.[0];\n\n if (!contact) throw new Error(`Contact not found: ${contactId}`);\n return contact;\n}\n\nexport async function getCurrentUser(): Promise<{\n userId: string;\n userName: string;\n email: string;\n}> {\n const token = await requireAccessToken();\n\n const response = await fetch(`${XERO_BASE_URL}/Organisation`, {\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n },\n });\n\n if (!response.ok) throw new Error(`Failed to get user info: ${response.status}`);\n\n const data: { Organisations: Array<{ Name: string }> } = await response.json();\n\n return {\n userId: \"current-user\",\n userName: data.Organisations[0]?.Name ?? \"Xero User\",\n email: \"user@xero.com\",\n };\n}\n",
300
- "app/api/auth/xero/callback/route.ts": "import { createOAuthCallbackHandler, xeroConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(xeroConfig, { tokenStore: hybridTokenStore });\n",
301
- "app/api/auth/xero/route.ts": "import { createOAuthInitHandler, xeroConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT).\n// NEVER return a shared constant in production - it breaks per-user token isolation (VULN-AUTH-2).\nfunction getUserId(_request: Request): string {\n return \"current-user\";\n}\n\nexport const GET = createOAuthInitHandler(xeroConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});"
302
- }
303
- },
304
241
  "integration:stripe": {
305
242
  "files": {
306
243
  "tools/list-subscriptions.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { formatAmount, formatDate, listSubscriptions } from \"../../lib/stripe-client.ts\";\n\nexport default tool({\n id: \"list-subscriptions\",\n description:\n \"List Stripe subscriptions. Supports filtering by customer, status, and creation date range.\",\n inputSchema: z.object({\n limit: z\n .number()\n .min(1)\n .max(100)\n .default(10)\n .describe(\"Maximum number of subscriptions to retrieve\"),\n customerId: z.string().optional().describe(\"Filter by customer ID (starts with cus_)\"),\n status: z\n .enum([\n \"incomplete\",\n \"incomplete_expired\",\n \"trialing\",\n \"active\",\n \"past_due\",\n \"canceled\",\n \"unpaid\",\n \"paused\",\n ])\n .optional()\n .describe(\"Filter by subscription status\"),\n createdAfter: z\n .number()\n .optional()\n .describe(\"Filter subscriptions created after this Unix timestamp\"),\n createdBefore: z\n .number()\n .optional()\n .describe(\"Filter subscriptions created before this Unix timestamp\"),\n }),\n async execute({ limit, customerId, status, createdAfter, createdBefore }) {\n const created =\n createdAfter || createdBefore ? { gte: createdAfter, lte: createdBefore } : undefined;\n\n const subscriptions = await listSubscriptions({\n limit,\n customer: customerId,\n status,\n created,\n });\n\n return subscriptions.map((subscription) => ({\n id: subscription.id,\n customer: subscription.customer,\n status: subscription.status,\n currentPeriodStart: formatDate(subscription.current_period_start),\n currentPeriodEnd: formatDate(subscription.current_period_end),\n created: formatDate(subscription.created),\n canceledAt: subscription.canceled_at ? formatDate(subscription.canceled_at) : null,\n items: subscription.items.data.map((item) => ({\n id: item.id,\n priceId: item.price.id,\n amount: formatAmount(item.price.unit_amount, item.price.currency),\n amountRaw: item.price.unit_amount,\n currency: item.price.currency,\n interval: item.price.recurring.interval,\n intervalCount: item.price.recurring.interval_count,\n })),\n metadata: subscription.metadata,\n }));\n },\n});\n",
@@ -375,19 +312,6 @@ export default {
375
312
  "lib/posthog-client.ts": "import { getApiKey } from \"./token-store.ts\";\n\nconst DEFAULT_POSTHOG_HOST = \"https://app.posthog.com\";\n\nexport interface PostHogInsight {\n id: number;\n name: string;\n derived_name: string | null;\n description: string;\n filters: Record<string, unknown>;\n result: unknown;\n created_at: string;\n created_by: {\n id: number;\n uuid: string;\n distinct_id: string;\n first_name: string;\n email: string;\n } | null;\n}\n\nexport interface PostHogTrend {\n action: {\n id: string;\n name: string;\n type: string;\n };\n label: string;\n count: number;\n data: number[];\n labels: string[];\n days: string[];\n}\n\nexport interface PostHogFunnel {\n id: number;\n name: string;\n steps: Array<{\n action_id: string;\n name: string;\n order: number;\n count: number;\n average_conversion_time: number | null;\n }>;\n filters: Record<string, unknown>;\n}\n\nexport interface PostHogFeatureFlag {\n id: number;\n name: string;\n key: string;\n filters: {\n groups: Array<{\n properties: unknown[];\n rollout_percentage: number | null;\n }>;\n };\n deleted: boolean;\n active: boolean;\n created_at: string;\n created_by: {\n id: number;\n uuid: string;\n distinct_id: string;\n first_name: string;\n email: string;\n } | null;\n is_simple_flag: boolean;\n rollout_percentage: number | null;\n ensure_experience_continuity: boolean;\n}\n\nexport interface PostHogPerson {\n id: string;\n name: string;\n distinct_ids: string[];\n properties: Record<string, unknown>;\n created_at: string;\n uuid: string;\n}\n\nexport interface PostHogEvent {\n event: string;\n distinct_id: string;\n properties?: Record<string, unknown>;\n timestamp?: string;\n}\n\ninterface PostHogListResponse<T> {\n next: string | null;\n previous: string | null;\n results: T[];\n}\n\ninterface PostHogError {\n detail?: string;\n}\n\nfunction getPostHogHost(): string {\n return process.env.POSTHOG_HOST ?? DEFAULT_POSTHOG_HOST;\n}\n\nfunction buildParams(\n options?: Record<string, string | number | boolean | undefined>,\n): Record<string, string | number | boolean> | undefined {\n if (!options) return undefined;\n\n const params: Record<string, string | number | boolean> = {};\n for (const [key, value] of Object.entries(options)) {\n if (value !== undefined) params[key] = value;\n }\n\n return Object.keys(params).length ? params : undefined;\n}\n\nasync function posthogFetch<T>(\n endpoint: string,\n options: RequestInit & { params?: Record<string, string | number | boolean> } = {},\n): Promise<T> {\n const apiKey = getApiKey();\n if (!apiKey) {\n throw new Error(\"Not authenticated with PostHog. Please set POSTHOG_API_KEY.\");\n }\n\n const url = new URL(`${getPostHogHost()}/api${endpoint}`);\n for (const [key, value] of Object.entries(options.params ?? {})) {\n url.searchParams.append(key, String(value));\n }\n\n const headers: Record<string, string> = {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n ...(options.headers as Record<string, string> | undefined),\n };\n\n const response = await fetch(url.toString(), { ...options, headers });\n const data: unknown = await response.json();\n\n if (!response.ok) {\n const error = data as PostHogError;\n throw new Error(`PostHog API error: ${response.status} ${error.detail ?? response.statusText}`);\n }\n\n return data as T;\n}\n\nexport function getInsights(options?: {\n limit?: number;\n}): Promise<PostHogListResponse<PostHogInsight>> {\n return posthogFetch<PostHogListResponse<PostHogInsight>>(\"/projects/@current/insights/\", {\n params: buildParams({ limit: options?.limit }),\n });\n}\n\nexport function getTrends(options: {\n events?: Array<{ id: string; name?: string; type?: string }>;\n date_from?: string;\n date_to?: string;\n interval?: \"hour\" | \"day\" | \"week\" | \"month\";\n properties?: Record<string, unknown>[];\n}): Promise<PostHogTrend[]> {\n const body = {\n events: options.events ?? [{ id: \"$pageview\", name: \"$pageview\", type: \"events\" }],\n date_from: options.date_from ?? \"-7d\",\n date_to: options.date_to ?? \"now\",\n interval: options.interval ?? \"day\",\n properties: options.properties ?? [],\n };\n\n return posthogFetch<PostHogTrend[]>(\"/projects/@current/insights/trend/\", {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n}\n\nexport function getFunnels(options: {\n events?: Array<{ id: string; name?: string; order: number }>;\n date_from?: string;\n date_to?: string;\n}): Promise<PostHogFunnel> {\n const body = {\n events: options.events ?? [],\n date_from: options.date_from ?? \"-7d\",\n date_to: options.date_to ?? \"now\",\n };\n\n return posthogFetch<PostHogFunnel>(\"/projects/@current/insights/funnel/\", {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n}\n\nexport function getFeatureFlags(options?: {\n limit?: number;\n}): Promise<PostHogListResponse<PostHogFeatureFlag>> {\n return posthogFetch<PostHogListResponse<PostHogFeatureFlag>>(\n \"/projects/@current/feature_flags/\",\n { params: buildParams({ limit: options?.limit }) },\n );\n}\n\nexport function getFeatureFlag(flagId: number): Promise<PostHogFeatureFlag> {\n return posthogFetch<PostHogFeatureFlag>(`/projects/@current/feature_flags/${flagId}/`);\n}\n\nexport function listPersons(options?: {\n limit?: number;\n search?: string;\n}): Promise<PostHogListResponse<PostHogPerson>> {\n return posthogFetch<PostHogListResponse<PostHogPerson>>(\"/projects/@current/persons/\", {\n params: buildParams({ limit: options?.limit, search: options?.search }),\n });\n}\n\nexport function getPerson(personId: string): Promise<PostHogPerson> {\n return posthogFetch<PostHogPerson>(`/projects/@current/persons/${personId}/`);\n}\n\nexport function captureEvent(event: PostHogEvent): Promise<{ status: number }> {\n const body = {\n api_key: getApiKey(),\n event: event.event,\n distinct_id: event.distinct_id,\n properties: event.properties ?? {},\n timestamp: event.timestamp ?? new Date().toISOString(),\n };\n\n return posthogFetch<{ status: number }>(\"/capture/\", {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n}\n\nexport function formatDate(dateString: string): string {\n return new Date(dateString).toISOString();\n}\n\nexport function calculateConversionRate(funnel: PostHogFunnel): number {\n if (funnel.steps.length < 2) return 0;\n\n const firstStep = funnel.steps[0];\n if (firstStep.count === 0) return 0;\n\n const lastStep = funnel.steps[funnel.steps.length - 1];\n return (lastStep.count / firstStep.count) * 100;\n}\n"
376
313
  }
377
314
  },
378
- "integration:webex": {
379
- "files": {
380
- "tools/list-meetings.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listMeetings } from \"../../lib/webex-client.ts\";\n\nexport default tool({\n id: \"list-meetings\",\n description:\n \"List scheduled Webex meetings. Can filter by date range and meeting state.\",\n inputSchema: z.object({\n max: z\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of meetings to return\"),\n from: z\n .string()\n .optional()\n .describe(\"Start date in ISO 8601 format (e.g., 2024-01-01T00:00:00Z)\"),\n to: z\n .string()\n .optional()\n .describe(\"End date in ISO 8601 format (e.g., 2024-12-31T23:59:59Z)\"),\n state: z\n .enum([\"active\", \"scheduled\", \"ended\", \"missed\", \"inProgress\"])\n .optional()\n .describe(\"Filter by meeting state\"),\n }),\n async execute({ max, from, to, state }) {\n return listMeetings({ max, from, to, state });\n },\n});\n",
381
- "tools/create-meeting.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createMeeting } from \"../../lib/webex-client.ts\";\n\nexport default tool({\n id: \"create-meeting\",\n description: \"Schedule a new Webex meeting with specified details.\",\n inputSchema: z.object({\n title: z.string().describe(\"Title of the meeting\"),\n agenda: z.string().optional().describe(\"Meeting agenda or description\"),\n start: z\n .string()\n .describe(\n \"Start date and time in ISO 8601 format (e.g., 2024-01-15T14:00:00Z)\",\n ),\n end: z\n .string()\n .describe(\n \"End date and time in ISO 8601 format (e.g., 2024-01-15T15:00:00Z)\",\n ),\n timezone: z\n .string()\n .default(\"UTC\")\n .describe(\"Timezone for the meeting (e.g., America/New_York, Europe/London)\"),\n enabledAutoRecordMeeting: z\n .boolean()\n .default(false)\n .describe(\"Automatically record the meeting\"),\n allowAnyUserToBeCoHost: z\n .boolean()\n .default(false)\n .describe(\"Allow any user to be a co-host\"),\n invitees: z\n .array(\n z.object({\n email: z.string().email().describe(\"Email address of the invitee\"),\n displayName: z.string().optional().describe(\"Display name of the invitee\"),\n coHost: z.boolean().default(false).describe(\"Make this invitee a co-host\"),\n }),\n )\n .optional()\n .describe(\"List of invitees to the meeting\"),\n }),\n async execute(input) {\n const meeting = await createMeeting(input);\n\n return {\n id: meeting.id,\n title: meeting.title,\n agenda: meeting.agenda,\n start: meeting.start,\n end: meeting.end,\n timezone: meeting.timezone,\n hostEmail: meeting.hostEmail,\n hostDisplayName: meeting.hostDisplayName,\n webLink: meeting.webLink,\n meetingNumber: meeting.meetingNumber,\n message: `Meeting \"${meeting.title}\" created successfully. Join URL: ${meeting.webLink}`,\n };\n },\n});\n",
382
- "tools/get-meeting.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getMeeting } from \"../../lib/webex-client.ts\";\n\nexport default tool({\n id: \"get-meeting\",\n description: \"Get detailed information about a specific Webex meeting by its ID.\",\n inputSchema: z.object({\n meetingId: z.string().describe(\"The unique ID of the meeting\"),\n }),\n async execute({ meetingId }) {\n return getMeeting(meetingId);\n },\n});\n",
383
- "tools/list-rooms.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listRooms } from \"../../lib/webex-client.ts\";\n\nexport default tool({\n id: \"list-rooms\",\n description:\n \"List Webex spaces/rooms. Can filter by type (direct messages or group spaces).\",\n inputSchema: z.object({\n max: z\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of rooms to return\"),\n type: z\n .enum([\"direct\", \"group\"])\n .optional()\n .describe(\n \"Filter by room type: 'direct' for 1:1 conversations, 'group' for team spaces\",\n ),\n sortBy: z\n .enum([\"id\", \"lastactivity\", \"created\"])\n .default(\"lastactivity\")\n .describe(\"Sort rooms by id, lastactivity, or created date\"),\n }),\n async execute({ max, type, sortBy }) {\n const rooms = await listRooms({ max, type, sortBy });\n\n return rooms.map((room) => ({\n id: room.id,\n title: room.title,\n type: room.type,\n isLocked: room.isLocked,\n lastActivity: room.lastActivity,\n created: room.created,\n }));\n },\n});\n",
384
- "tools/send-message.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { sendMessage } from \"../../lib/webex-client.ts\";\n\nexport default tool({\n id: \"send-message\",\n description:\n \"Send a text or markdown message to a Webex room/space or directly to a person.\",\n inputSchema: z\n .object({\n roomId: z\n .string()\n .optional()\n .describe(\"Room ID to send the message to (use this OR toPersonEmail)\"),\n toPersonEmail: z\n .string()\n .email()\n .optional()\n .describe(\"Email address to send a direct message (use this OR roomId)\"),\n text: z\n .string()\n .optional()\n .describe(\"Plain text message content (use this OR markdown)\"),\n markdown: z\n .string()\n .optional()\n .describe(\"Markdown formatted message content (use this OR text)\"),\n })\n .refine((data) => data.roomId || data.toPersonEmail, {\n message: \"Must specify either roomId or toPersonEmail\",\n })\n .refine((data) => data.text || data.markdown, {\n message: \"Must specify either text or markdown\",\n }),\n async execute({ roomId, toPersonEmail, text, markdown }) {\n const message = await sendMessage({ roomId, toPersonEmail, text, markdown });\n\n const destination = roomId ? `room ${roomId}` : toPersonEmail;\n\n return {\n id: message.id,\n roomId: message.roomId,\n text: message.text,\n markdown: message.markdown,\n personEmail: message.personEmail,\n created: message.created,\n message: `Message sent successfully to ${destination}`,\n };\n },\n});\n",
385
- ".env.example": "# Webex OAuth Configuration\n# Get your credentials from https://developer.webex.com/my-apps\nWEBEX_CLIENT_ID=your-client-id\nWEBEX_CLIENT_SECRET=your-client-secret\n",
386
- "lib/webex-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst WEBEX_BASE_URL = \"https://webexapis.com/v1\";\n\ninterface WebexMeeting {\n id: string;\n title: string;\n agenda?: string;\n start: string;\n end: string;\n timezone: string;\n hostEmail: string;\n hostDisplayName: string;\n webLink: string;\n sipAddress?: string;\n meetingNumber?: string;\n state: string;\n enabledAutoRecordMeeting?: boolean;\n allowAnyUserToBeCoHost?: boolean;\n}\n\ninterface WebexRoom {\n id: string;\n title: string;\n type: \"direct\" | \"group\";\n isLocked: boolean;\n lastActivity: string;\n creatorId: string;\n created: string;\n}\n\ninterface WebexMessage {\n id: string;\n roomId: string;\n roomType: string;\n text?: string;\n markdown?: string;\n personId: string;\n personEmail: string;\n created: string;\n}\n\ninterface WebexPerson {\n id: string;\n emails: string[];\n displayName: string;\n firstName?: string;\n lastName?: string;\n avatar?: string;\n orgId: string;\n created: string;\n lastActivity?: string;\n status?: string;\n type: string;\n}\n\nasync function webexFetch<T>(\n endpoint: string,\n options: RequestInit = {},\n): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Webex. Please connect your account.\");\n }\n\n const response = await fetch(`${WEBEX_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({}))) as {\n message?: string;\n };\n\n throw new Error(\n `Webex API error: ${response.status} ${error.message ?? response.statusText}`,\n );\n }\n\n return response.json() as Promise<T>;\n}\n\nfunction setOptionalParam(\n params: URLSearchParams,\n key: string,\n value: string | number | undefined,\n): void {\n if (value === undefined) return;\n params.set(key, String(value));\n}\n\nexport async function getMe(): Promise<WebexPerson> {\n return webexFetch<WebexPerson>(\"/people/me\");\n}\n\n/**\n * List meetings for the authenticated user\n */\nexport async function listMeetings(options?: {\n max?: number;\n from?: string;\n to?: string;\n meetingType?: \"meeting\" | \"webinar\" | \"personalRoomMeeting\";\n state?: \"active\" | \"scheduled\" | \"ended\" | \"missed\" | \"inProgress\";\n}): Promise<WebexMeeting[]> {\n const params = new URLSearchParams();\n\n setOptionalParam(params, \"max\", options?.max);\n setOptionalParam(params, \"from\", options?.from);\n setOptionalParam(params, \"to\", options?.to);\n setOptionalParam(params, \"meetingType\", options?.meetingType);\n setOptionalParam(params, \"state\", options?.state);\n\n const response = await webexFetch<{ items?: WebexMeeting[] }>(\n `/meetings?${params}`,\n );\n\n return response.items ?? [];\n}\n\n/**\n * Get details of a specific meeting\n */\nexport async function getMeeting(meetingId: string): Promise<WebexMeeting> {\n return webexFetch<WebexMeeting>(`/meetings/${meetingId}`);\n}\n\n/**\n * Create a new Webex meeting\n */\nexport async function createMeeting(options: {\n title: string;\n agenda?: string;\n start: string;\n end: string;\n timezone?: string;\n enabledAutoRecordMeeting?: boolean;\n allowAnyUserToBeCoHost?: boolean;\n invitees?: Array<{ email: string; displayName?: string; coHost?: boolean }>;\n}): Promise<WebexMeeting> {\n const body: Record<string, unknown> = {\n title: options.title,\n start: options.start,\n end: options.end,\n timezone: options.timezone ?? \"UTC\",\n };\n\n if (options.agenda) body.agenda = options.agenda;\n if (options.enabledAutoRecordMeeting !== undefined) {\n body.enabledAutoRecordMeeting = options.enabledAutoRecordMeeting;\n }\n if (options.allowAnyUserToBeCoHost !== undefined) {\n body.allowAnyUserToBeCoHost = options.allowAnyUserToBeCoHost;\n }\n if (options.invitees?.length) body.invitees = options.invitees;\n\n return webexFetch<WebexMeeting>(\"/meetings\", {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n}\n\n/**\n * Update an existing meeting\n */\nexport async function updateMeeting(\n meetingId: string,\n updates: {\n title?: string;\n agenda?: string;\n start?: string;\n end?: string;\n timezone?: string;\n enabledAutoRecordMeeting?: boolean;\n },\n): Promise<WebexMeeting> {\n return webexFetch<WebexMeeting>(`/meetings/${meetingId}`, {\n method: \"PATCH\",\n body: JSON.stringify(updates),\n });\n}\n\n/**\n * Delete a meeting\n */\nexport async function deleteMeeting(meetingId: string): Promise<void> {\n await webexFetch<void>(`/meetings/${meetingId}`, { method: \"DELETE\" });\n}\n\n/**\n * List Webex rooms (spaces)\n */\nexport async function listRooms(options?: {\n max?: number;\n type?: \"direct\" | \"group\";\n sortBy?: \"id\" | \"lastactivity\" | \"created\";\n}): Promise<WebexRoom[]> {\n const params = new URLSearchParams();\n\n setOptionalParam(params, \"max\", options?.max);\n setOptionalParam(params, \"type\", options?.type);\n setOptionalParam(params, \"sortBy\", options?.sortBy);\n\n const response = await webexFetch<{ items?: WebexRoom[] }>(`/rooms?${params}`);\n return response.items ?? [];\n}\n\n/**\n * Get details of a specific room\n */\nexport async function getRoom(roomId: string): Promise<WebexRoom> {\n return webexFetch<WebexRoom>(`/rooms/${roomId}`);\n}\n\n/**\n * Create a new room\n */\nexport async function createRoom(options: {\n title: string;\n teamId?: string;\n}): Promise<WebexRoom> {\n return webexFetch<WebexRoom>(\"/rooms\", {\n method: \"POST\",\n body: JSON.stringify(options),\n });\n}\n\n/**\n * Send a message to a Webex room\n */\nexport async function sendMessage(options: {\n roomId?: string;\n toPersonId?: string;\n toPersonEmail?: string;\n text?: string;\n markdown?: string;\n files?: string[];\n}): Promise<WebexMessage> {\n if (!options.roomId && !options.toPersonId && !options.toPersonEmail) {\n throw new Error(\"Must specify roomId, toPersonId, or toPersonEmail\");\n }\n\n if (!options.text && !options.markdown && !options.files) {\n throw new Error(\"Must specify text, markdown, or files\");\n }\n\n return webexFetch<WebexMessage>(\"/messages\", {\n method: \"POST\",\n body: JSON.stringify(options),\n });\n}\n\n/**\n * List messages in a room\n */\nexport async function listMessages(options: {\n roomId: string;\n max?: number;\n before?: string;\n beforeMessage?: string;\n}): Promise<WebexMessage[]> {\n const params = new URLSearchParams({ roomId: options.roomId });\n\n setOptionalParam(params, \"max\", options.max);\n setOptionalParam(params, \"before\", options.before);\n setOptionalParam(params, \"beforeMessage\", options.beforeMessage);\n\n const response = await webexFetch<{ items?: WebexMessage[] }>(\n `/messages?${params}`,\n );\n\n return response.items ?? [];\n}\n\n/**\n * Delete a message\n */\nexport async function deleteMessage(messageId: string): Promise<void> {\n await webexFetch<void>(`/messages/${messageId}`, { method: \"DELETE\" });\n}\n",
387
- "app/api/auth/webex/callback/route.ts": "import { createOAuthCallbackHandler, webexConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n async getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(webexConfig, { tokenStore: hybridTokenStore });\n",
388
- "app/api/auth/webex/route.ts": "import { createOAuthInitHandler, webexConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT).\n// NEVER return a shared constant in production - it breaks per-user token isolation (VULN-AUTH-2).\nfunction getUserId(_request: Request): string {\n return \"current-user\";\n}\n\nexport const GET = createOAuthInitHandler(webexConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});"
389
- }
390
- },
391
315
  "integration:sentry": {
392
316
  "files": {
393
317
  "tools/resolve-issue.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { resolveIssue } from \"../../lib/sentry-client.ts\";\n\nexport default tool({\n id: \"resolve-issue\",\n description:\n \"Mark a Sentry issue as resolved. Use this after you've fixed a bug or determined an issue is no longer relevant.\",\n inputSchema: z.object({\n issueId: z.string().describe(\"The ID of the issue to resolve\"),\n }),\n async execute({ issueId }) {\n const issue = await resolveIssue(issueId);\n\n return {\n success: true,\n issue,\n message: `Issue ${issue.shortId} has been marked as resolved.`,\n };\n },\n});\n",
@@ -424,19 +348,6 @@ export default {
424
348
  "app/api/auth/bitbucket/route.ts": "import { bitbucketConfig, createOAuthInitHandler } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT).\n// NEVER return a shared constant in production - it breaks per-user token isolation (VULN-AUTH-2).\nfunction getUserId(_request: Request): string {\n return \"current-user\";\n}\n\nexport const GET = createOAuthInitHandler(bitbucketConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});"
425
349
  }
426
350
  },
427
- "integration:pipedrive": {
428
- "files": {
429
- "tools/update-deal.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { updateDeal } from \"../../lib/pipedrive-client.ts\";\n\nexport default tool({\n id: \"update-deal\",\n description: \"Update an existing deal in Pipedrive with new information.\",\n inputSchema: z.object({\n dealId: z.number().describe(\"The ID of the deal to update\"),\n title: z.string().optional().describe(\"New title/name for the deal\"),\n value: z.number().optional().describe(\"New monetary value for the deal\"),\n status: z.string().optional().describe(\"New status (e.g., open, won, lost)\"),\n stageId: z.number().optional().describe(\"New pipeline stage ID\"),\n personId: z.number().optional().describe(\"New person/contact ID\"),\n orgId: z.number().optional().describe(\"New organization ID\"),\n expectedCloseDate: z\n .string()\n .optional()\n .describe(\"New expected close date in YYYY-MM-DD format\"),\n }),\n async execute(input) {\n const deal = await updateDeal(input.dealId, {\n title: input.title,\n value: input.value,\n status: input.status,\n stageId: input.stageId,\n personId: input.personId,\n orgId: input.orgId,\n expectedCloseDate: input.expectedCloseDate,\n });\n\n return {\n success: true,\n deal: {\n id: deal.id,\n title: deal.title,\n value: deal.value,\n status: deal.status,\n stageId: deal.stage_id,\n personName: deal.person_name,\n orgName: deal.org_name,\n expectedCloseDate: deal.expected_close_date,\n updateTime: deal.update_time,\n },\n };\n },\n});\n",
430
- "tools/get-deal.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getDeal } from \"../../lib/pipedrive-client.ts\";\n\nexport default tool({\n id: \"get-deal\",\n description: \"Get detailed information about a specific deal in Pipedrive by its ID.\",\n inputSchema: z.object({\n dealId: z.number().describe(\"The ID of the deal to retrieve\"),\n }),\n async execute({ dealId }) {\n const deal = await getDeal(dealId);\n\n return {\n id: deal.id,\n title: deal.title,\n value: deal.value,\n currency: deal.currency,\n status: deal.status,\n stageId: deal.stage_id,\n personId: deal.person_id,\n personName: deal.person_name,\n orgId: deal.org_id,\n orgName: deal.org_name,\n ownerName: deal.owner_name,\n expectedCloseDate: deal.expected_close_date,\n addTime: deal.add_time,\n updateTime: deal.update_time,\n wonTime: deal.won_time,\n lostTime: deal.lost_time,\n closeTime: deal.close_time,\n };\n },\n});\n",
431
- "tools/list-deals.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listDeals } from \"../../lib/pipedrive-client.ts\";\n\nexport default tool({\n id: \"list-deals\",\n description:\n \"List deals from Pipedrive. Can filter by status, owner, or stage to get specific deals in the sales pipeline.\",\n inputSchema: z.object({\n status: z\n .enum([\"open\", \"won\", \"lost\", \"all\"])\n .default(\"open\")\n .describe(\"Filter deals by status\"),\n ownerId: z.number().optional().describe(\"Filter deals by owner user ID\"),\n stageId: z.number().optional().describe(\"Filter deals by stage ID\"),\n limit: z\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of deals to return\"),\n }),\n async execute({ status, ownerId, stageId, limit }) {\n const deals = await listDeals({ status, ownerId, stageId, limit });\n\n return deals.map((deal) => ({\n id: deal.id,\n title: deal.title,\n value: deal.value,\n currency: deal.currency,\n status: deal.status,\n stageId: deal.stage_id,\n personName: deal.person_name,\n orgName: deal.org_name,\n ownerName: deal.owner_name,\n expectedCloseDate: deal.expected_close_date,\n addTime: deal.add_time,\n updateTime: deal.update_time,\n }));\n },\n});\n",
432
- "tools/create-deal.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createDeal } from \"../../lib/pipedrive-client.ts\";\n\nexport default tool({\n id: \"create-deal\",\n description: \"Create a new deal in the Pipedrive sales pipeline.\",\n inputSchema: z.object({\n title: z.string().describe(\"The title/name of the deal\"),\n value: z.number().optional().describe(\"The monetary value of the deal\"),\n currency: z\n .string()\n .default(\"USD\")\n .describe(\"Currency code (e.g., USD, EUR, GBP)\"),\n personId: z\n .number()\n .optional()\n .describe(\"ID of the person/contact associated with the deal\"),\n orgId: z\n .number()\n .optional()\n .describe(\"ID of the organization associated with the deal\"),\n stageId: z.number().optional().describe(\"ID of the pipeline stage for the deal\"),\n expectedCloseDate: z\n .string()\n .optional()\n .describe(\"Expected close date in YYYY-MM-DD format\"),\n }),\n async execute(input) {\n const deal = await createDeal(input);\n\n return {\n success: true,\n deal: {\n id: deal.id,\n title: deal.title,\n value: deal.value,\n currency: deal.currency,\n stageId: deal.stage_id,\n personName: deal.person_name,\n orgName: deal.org_name,\n expectedCloseDate: deal.expected_close_date,\n },\n };\n },\n});\n",
433
- "tools/list-persons.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listPersons } from \"../../lib/pipedrive-client.ts\";\n\nexport default tool({\n id: \"list-persons\",\n description: \"List contacts/persons from Pipedrive. Can optionally search by name or email.\",\n inputSchema: z.object({\n searchTerm: z.string().optional().describe(\"Search term to filter persons by name or email\"),\n limit: z.number().min(1).max(100).default(20).describe(\"Maximum number of persons to return\"),\n }),\n async execute({ searchTerm, limit }) {\n const persons = await listPersons({ searchTerm, limit });\n\n return persons.map((person) => ({\n id: person.id,\n name: person.name,\n firstName: person.first_name,\n lastName: person.last_name,\n email: person.email?.[0]?.value ?? null,\n phone: person.phone?.[0]?.value ?? null,\n orgId: person.org_id,\n orgName: person.org_name,\n ownerName: person.owner_name,\n addTime: person.add_time,\n updateTime: person.update_time,\n }));\n },\n});\n",
434
- ".env.example": "# Pipedrive OAuth Configuration\n# Get your credentials from https://developers.pipedrive.com/docs/marketplace\nPIPEDRIVE_CLIENT_ID=your-client-id\nPIPEDRIVE_CLIENT_SECRET=your-client-secret\n",
435
- "lib/pipedrive-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst PIPEDRIVE_BASE_URL = \"https://api.pipedrive.com/v1\";\n\ninterface PipedriveResponse<T> {\n success: boolean;\n data: T;\n additional_data?: {\n pagination?: {\n start: number;\n limit: number;\n more_items_in_collection: boolean;\n next_start?: number;\n };\n };\n}\n\ninterface PipedriveDeal {\n id: number;\n title: string;\n value: number;\n currency: string;\n status: string;\n stage_id: number;\n person_id: number | null;\n person_name: string | null;\n org_id: number | null;\n org_name: string | null;\n owner_name: string;\n expected_close_date: string | null;\n add_time: string;\n update_time: string;\n won_time: string | null;\n lost_time: string | null;\n close_time: string | null;\n}\n\ninterface PipedrivePerson {\n id: number;\n name: string;\n first_name: string;\n last_name: string;\n email: Array<{ value: string; primary: boolean }>;\n phone: Array<{ value: string; primary: boolean }>;\n org_id: number | null;\n org_name: string | null;\n owner_id: number;\n owner_name: string;\n add_time: string;\n update_time: string;\n}\n\ninterface PipedriveStage {\n id: number;\n name: string;\n order_nr: number;\n pipeline_id: number;\n pipeline_name: string;\n}\n\nasync function pipedriveFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Pipedrive. Please connect your account.\");\n }\n\n const url = new URL(`${PIPEDRIVE_BASE_URL}${endpoint}`);\n // SECURITY: Pipedrive's v1 API requires the token as a query parameter.\n // Tokens in query params may be recorded in browser history, server/proxy\n // access logs, and leaked via the Referer header. The Referrer-Policy\n // header (set by Veryfront's security middleware) mitigates the Referer leak.\n // This is an API design limitation — there is no Authorization header alternative.\n url.searchParams.set(\"api_token\", token);\n\n const response = await fetch(url.toString(), {\n ...options,\n headers: {\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({} as { error?: string }));\n throw new Error(`Pipedrive API error: ${response.status} ${error.error ?? response.statusText}`);\n }\n\n return response.json();\n}\n\nfunction buildEndpoint(path: string, params: URLSearchParams): string {\n const queryString = params.toString();\n return queryString ? `${path}?${queryString}` : path;\n}\n\nexport async function listDeals(options?: {\n status?: \"open\" | \"won\" | \"lost\" | \"all\";\n ownerId?: number;\n stageId?: number;\n limit?: number;\n}): Promise<PipedriveDeal[]> {\n const params = new URLSearchParams();\n\n if (options?.status) params.set(\"status\", options.status);\n if (options?.ownerId) params.set(\"user_id\", options.ownerId.toString());\n if (options?.stageId) params.set(\"stage_id\", options.stageId.toString());\n if (options?.limit) params.set(\"limit\", options.limit.toString());\n\n const response = await pipedriveFetch<PipedriveResponse<PipedriveDeal[]>>(\n buildEndpoint(\"/deals\", params),\n );\n\n return response.data || [];\n}\n\nexport async function getDeal(dealId: number): Promise<PipedriveDeal> {\n const response = await pipedriveFetch<PipedriveResponse<PipedriveDeal>>(`/deals/${dealId}`);\n return response.data;\n}\n\nexport async function createDeal(options: {\n title: string;\n value?: number;\n currency?: string;\n personId?: number;\n orgId?: number;\n stageId?: number;\n expectedCloseDate?: string;\n}): Promise<PipedriveDeal> {\n const body: Record<string, unknown> = { title: options.title };\n\n if (options.value !== undefined) body.value = options.value;\n if (options.currency) body.currency = options.currency;\n if (options.personId) body.person_id = options.personId;\n if (options.orgId) body.org_id = options.orgId;\n if (options.stageId) body.stage_id = options.stageId;\n if (options.expectedCloseDate) body.expected_close_date = options.expectedCloseDate;\n\n const response = await pipedriveFetch<PipedriveResponse<PipedriveDeal>>(\"/deals\", {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n\n return response.data;\n}\n\nexport async function updateDeal(\n dealId: number,\n updates: {\n title?: string;\n value?: number;\n status?: string;\n stageId?: number;\n personId?: number;\n orgId?: number;\n expectedCloseDate?: string;\n },\n): Promise<PipedriveDeal> {\n const body: Record<string, unknown> = {};\n\n if (updates.title !== undefined) body.title = updates.title;\n if (updates.value !== undefined) body.value = updates.value;\n if (updates.status !== undefined) body.status = updates.status;\n if (updates.stageId !== undefined) body.stage_id = updates.stageId;\n if (updates.personId !== undefined) body.person_id = updates.personId;\n if (updates.orgId !== undefined) body.org_id = updates.orgId;\n if (updates.expectedCloseDate !== undefined) body.expected_close_date = updates.expectedCloseDate;\n\n const response = await pipedriveFetch<PipedriveResponse<PipedriveDeal>>(`/deals/${dealId}`, {\n method: \"PUT\",\n body: JSON.stringify(body),\n });\n\n return response.data;\n}\n\nexport async function listPersons(options?: {\n searchTerm?: string;\n limit?: number;\n}): Promise<PipedrivePerson[]> {\n const params = new URLSearchParams();\n\n if (options?.searchTerm) params.set(\"term\", options.searchTerm);\n if (options?.limit) params.set(\"limit\", options.limit.toString());\n\n const response = await pipedriveFetch<PipedriveResponse<PipedrivePerson[]>>(\n buildEndpoint(\"/persons\", params),\n );\n\n return response.data || [];\n}\n\nexport async function getPerson(personId: number): Promise<PipedrivePerson> {\n const response = await pipedriveFetch<PipedriveResponse<PipedrivePerson>>(`/persons/${personId}`);\n return response.data;\n}\n\nexport async function createPerson(options: {\n name: string;\n email?: string;\n phone?: string;\n orgId?: number;\n}): Promise<PipedrivePerson> {\n const body: Record<string, unknown> = { name: options.name };\n\n if (options.email) body.email = [{ value: options.email, primary: true }];\n if (options.phone) body.phone = [{ value: options.phone, primary: true }];\n if (options.orgId) body.org_id = options.orgId;\n\n const response = await pipedriveFetch<PipedriveResponse<PipedrivePerson>>(\"/persons\", {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n\n return response.data;\n}\n\nexport async function listStages(): Promise<PipedriveStage[]> {\n const response = await pipedriveFetch<PipedriveResponse<PipedriveStage[]>>(\"/stages\");\n return response.data || [];\n}\n\nexport async function getCurrentUser(): Promise<{ id: number; name: string; email: string }> {\n const response = await pipedriveFetch<PipedriveResponse<{ id: number; name: string; email: string }>>(\n \"/users/me\",\n );\n return response.data;\n}\n",
436
- "app/api/auth/pipedrive/callback/route.ts": "import { createOAuthCallbackHandler, pipedriveConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(pipedriveConfig, { tokenStore: hybridTokenStore });\n",
437
- "app/api/auth/pipedrive/route.ts": "import { createOAuthInitHandler, pipedriveConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT).\n// NEVER return a shared constant in production - it breaks per-user token isolation (VULN-AUTH-2).\nfunction getUserId(_request: Request): string {\n return \"current-user\";\n}\n\nexport const GET = createOAuthInitHandler(pipedriveConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});"
438
- }
439
- },
440
351
  "integration:servicenow": {
441
352
  "files": {
442
353
  "tools/create-incident.ts": "import { z } from \"zod\";\nimport { getServiceNowClient } from \"../../lib/servicenow-client.ts\";\nimport { isServiceNowConnected } from \"../../lib/token-store.ts\";\n\nexport default defineTool({\n id: \"servicenow-create-incident\",\n description: \"Create a new incident in ServiceNow\",\n inputSchema: z.object({\n short_description: z.string().describe(\"Brief description of the incident\"),\n description: z.string().optional().describe(\"Detailed description of the incident\"),\n urgency: z\n .enum([\"1\", \"2\", \"3\"])\n .optional()\n .describe(\"Urgency level (1=High, 2=Medium, 3=Low)\"),\n impact: z\n .enum([\"1\", \"2\", \"3\"])\n .optional()\n .describe(\"Impact level (1=High, 2=Medium, 3=Low)\"),\n category: z.string().optional().describe(\"Incident category\"),\n subcategory: z.string().optional().describe(\"Incident subcategory\"),\n }),\n async execute(input) {\n const connected = await isServiceNowConnected();\n if (!connected) {\n return {\n error: \"ServiceNow not connected\",\n action: \"Please connect ServiceNow via /api/auth/servicenow\",\n };\n }\n\n try {\n const client = getServiceNowClient();\n const incident = await client.createIncident(input);\n\n return {\n success: true,\n number: incident.number,\n sys_id: incident.sys_id,\n short_description: incident.short_description,\n state: incident.state,\n priority: incident.priority,\n message: `Incident ${incident.number} created successfully`,\n };\n } catch (error) {\n return {\n error: error instanceof Error ? error.message : \"Failed to create incident\",\n };\n }\n },\n});\n",
@@ -511,19 +422,6 @@ export default {
511
422
  "app/api/auth/confluence/route.ts": "import { confluenceConfig, createOAuthInitHandler } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT).\n// NEVER return a shared constant in production - it breaks per-user token isolation (VULN-AUTH-2).\nfunction getUserId(_request: Request): string {\n return \"current-user\";\n}\n\nexport const GET = createOAuthInitHandler(confluenceConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});"
512
423
  }
513
424
  },
514
- "integration:clickup": {
515
- "files": {
516
- "tools/list-tasks.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getAuthorizedUser, getTeams, listSpaces, listTasks } from \"../../lib/clickup-client.ts\";\n\nexport default tool({\n id: \"list-tasks\",\n description:\n \"List tasks from ClickUp. Can filter by list, folder, space, or get tasks assigned to the current user.\",\n inputSchema: z.object({\n listId: z.string().optional().describe(\"List ID to list tasks from\"),\n folderId: z.string().optional().describe(\"Folder ID to list tasks from\"),\n spaceId: z.string().optional().describe(\"Space ID to list tasks from\"),\n assignedToMe: z.boolean().default(false).describe(\"List tasks assigned to the current user\"),\n includeClosed: z.boolean().default(false).describe(\"Include completed/closed tasks\"),\n statuses: z.array(z.string()).optional().describe(\"Filter by specific status names\"),\n orderBy: z\n .string()\n .optional()\n .describe(\"Order results by field (e.g., 'due_date', 'created', 'updated')\"),\n limit: z.number().min(1).max(50).default(20).describe(\"Maximum number of tasks to return\"),\n }),\n async execute({ listId, folderId, spaceId, assignedToMe, includeClosed, statuses, orderBy, limit }) {\n if (!assignedToMe && !listId && !folderId && !spaceId) {\n return {\n tasks: [],\n message: \"Please specify either a listId, folderId, spaceId, or set assignedToMe to true\",\n };\n }\n\n let tasks;\n\n if (assignedToMe) {\n const user = await getAuthorizedUser();\n const teams = await getTeams();\n const teamId = teams[0]?.id;\n\n if (!teamId) return { tasks: [], message: \"No teams found\" };\n\n const spaces = await listSpaces(teamId);\n const firstSpaceId = spaces[0]?.id;\n\n if (!firstSpaceId) return { tasks: [], message: \"No spaces found in team\" };\n\n tasks = await listTasks({\n spaceId: firstSpaceId,\n assignees: [user.user.id],\n includeClosed,\n statuses,\n orderBy,\n });\n } else {\n const params = { includeClosed, statuses, orderBy };\n\n if (listId) tasks = await listTasks({ ...params, listId });\n else if (folderId) tasks = await listTasks({ ...params, folderId });\n else tasks = await listTasks({ ...params, spaceId: spaceId! });\n }\n\n return tasks.slice(0, limit).map((task) => ({\n id: task.id,\n name: task.name,\n status: task.status.status,\n dueDate: task.due_date ? new Date(parseInt(task.due_date)).toISOString() : null,\n priority: task.priority?.priority ?? \"none\",\n assignees: task.assignees.map((a) => a.username),\n tags: task.tags.map((t) => t.name),\n list: task.list.name,\n folder: task.folder.name,\n space: task.space.name,\n }));\n },\n});\n",
517
- "tools/list-lists.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport {\n getTeams,\n listFolders,\n listFolderlessLists,\n listLists,\n listSpaces,\n} from \"../../lib/clickup-client.ts\";\n\ntype ClickUpList = {\n id: string;\n name: string;\n task_count: number;\n due_date?: string | null;\n status?: { status?: string | null } | null;\n priority?: { priority?: string | null } | null;\n assignee?: { username?: string | null } | null;\n folder: { name: string };\n space: { name: string };\n archived: boolean;\n};\n\nfunction mapList(\n list: ClickUpList,\n folderName?: string,\n): {\n id: string;\n name: string;\n taskCount: number;\n dueDate: string | null;\n status: string;\n priority: string;\n assignee: string | null;\n folder: string;\n space: string;\n archived: boolean;\n} {\n return {\n id: list.id,\n name: list.name,\n taskCount: list.task_count,\n dueDate: list.due_date ? new Date(parseInt(list.due_date)).toISOString() : null,\n status: list.status?.status ?? \"active\",\n priority: list.priority?.priority ?? \"none\",\n assignee: list.assignee?.username ?? null,\n folder: folderName ?? list.folder.name,\n space: list.space.name,\n archived: list.archived,\n };\n}\n\nexport default tool({\n id: \"list-lists\",\n description:\n \"List all lists in ClickUp. Can filter by folder or space. Lists are containers for tasks.\",\n inputSchema: z.object({\n folderId: z.string().optional().describe(\"Folder ID to list lists from\"),\n spaceId: z.string().optional().describe(\"Space ID to list folderless lists from\"),\n includeAll: z\n .boolean()\n .default(false)\n .describe(\"List all lists from all folders in the first space\"),\n }),\n async execute({ folderId, spaceId, includeAll }) {\n if (folderId) {\n const lists = await listLists(folderId);\n return lists.map(mapList);\n }\n\n if (spaceId) {\n const lists = await listFolderlessLists(spaceId);\n return lists.map(mapList);\n }\n\n if (!includeAll) {\n return {\n lists: [],\n message: \"Please specify either a folderId, spaceId, or set includeAll to true\",\n };\n }\n\n const teams = await getTeams();\n if (teams.length === 0) return { lists: [], message: \"No teams found\" };\n\n const spaces = await listSpaces(teams[0].id);\n if (spaces.length === 0) return { lists: [], message: \"No spaces found in team\" };\n\n const firstSpace = spaces[0];\n const [folders, folderlessLists] = await Promise.all([\n listFolders(firstSpace.id),\n listFolderlessLists(firstSpace.id),\n ]);\n\n const allLists = folderlessLists.map((list) => mapList(list, \"No Folder\"));\n\n for (const folder of folders) {\n const lists = await listLists(folder.id);\n allLists.push(...lists.map((list) => mapList(list, folder.name)));\n }\n\n return allLists;\n },\n});\n",
518
- "tools/get-task.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getTask } from \"../../lib/clickup-client.ts\";\n\nfunction toIsoDate(value?: string | null): string | null {\n if (!value) return null;\n return new Date(Number.parseInt(value, 10)).toISOString();\n}\n\nexport default tool({\n id: \"get-task\",\n description: \"Get detailed information about a specific ClickUp task by ID.\",\n inputSchema: z.object({\n taskId: z.string().describe(\"The ID of the task to retrieve\"),\n includeSubtasks: z.boolean().default(false).describe(\"Include subtasks in the response\"),\n customTaskIds: z.boolean().default(false).describe(\"Use custom task IDs instead of internal IDs\"),\n teamId: z.string().optional().describe(\"Team ID (required when using custom task IDs)\"),\n }),\n async execute({ taskId, includeSubtasks, customTaskIds, teamId }) {\n const task = await getTask(taskId, { includeSubtasks, customTaskIds, teamId });\n\n const { creator, list, folder, space } = task;\n\n return {\n id: task.id,\n name: task.name,\n description: task.description,\n status: task.status.status,\n priority: task.priority?.priority ?? \"none\",\n dueDate: toIsoDate(task.due_date),\n startDate: toIsoDate(task.start_date),\n dateCreated: task.date_created,\n dateUpdated: task.date_updated,\n dateClosed: task.date_closed,\n creator: {\n id: creator.id,\n username: creator.username,\n email: creator.email,\n },\n assignees: task.assignees.map(({ id, username, email }) => ({ id, username, email })),\n tags: task.tags.map(({ name }) => name),\n list: {\n id: list.id,\n name: list.name,\n },\n folder: {\n id: folder.id,\n name: folder.name,\n },\n space: {\n id: space.id,\n name: space.name,\n },\n };\n },\n});\n",
519
- "tools/update-task.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { updateTask } from \"../../lib/clickup-client.ts\";\n\nexport default tool({\n id: \"update-task\",\n description: \"Update an existing ClickUp task.\",\n inputSchema: z.object({\n taskId: z.string().describe(\"The ID of the task to update\"),\n name: z.string().optional().describe(\"New name/title for the task\"),\n description: z.string().optional().describe(\"New description for the task\"),\n status: z.string().optional().describe(\"New status name for the task\"),\n priority: z\n .number()\n .min(1)\n .max(4)\n .optional()\n .describe(\n \"Priority level: 1 (urgent), 2 (high), 3 (normal), 4 (low). Use null to remove priority.\",\n ),\n dueDate: z\n .number()\n .optional()\n .describe(\"Due date in Unix timestamp (milliseconds). Use null to remove due date.\"),\n startDate: z\n .number()\n .optional()\n .describe(\"Start date in Unix timestamp (milliseconds). Use null to remove start date.\"),\n timeEstimate: z\n .number()\n .optional()\n .describe(\"Time estimate in milliseconds. Use null to remove time estimate.\"),\n addAssignees: z.array(z.number()).optional().describe(\"Array of user IDs to add as assignees\"),\n removeAssignees: z\n .array(z.number())\n .optional()\n .describe(\"Array of user IDs to remove from assignees\"),\n archived: z.boolean().optional().describe(\"Archive or unarchive the task\"),\n customTaskIds: z.boolean().default(false).describe(\"Use custom task IDs instead of internal IDs\"),\n teamId: z.string().optional().describe(\"Team ID (required when using custom task IDs)\"),\n }),\n async execute({\n taskId,\n name,\n description,\n status,\n priority,\n dueDate,\n startDate,\n timeEstimate,\n addAssignees,\n removeAssignees,\n archived,\n customTaskIds,\n teamId,\n }) {\n const updates: Record<string, unknown> = {};\n\n if (name !== undefined) updates.name = name;\n if (description !== undefined) updates.description = description;\n if (status !== undefined) updates.status = status;\n if (priority !== undefined) updates.priority = priority;\n if (dueDate !== undefined) updates.dueDate = dueDate;\n if (startDate !== undefined) updates.startDate = startDate;\n if (timeEstimate !== undefined) updates.timeEstimate = timeEstimate;\n if (archived !== undefined) updates.archived = archived;\n\n if (addAssignees || removeAssignees) {\n updates.assignees = {\n ...(addAssignees ? { add: addAssignees } : {}),\n ...(removeAssignees ? { rem: removeAssignees } : {}),\n };\n }\n\n const task = await updateTask(\n taskId,\n updates,\n customTaskIds ? { customTaskIds, teamId } : undefined,\n );\n\n return {\n success: true,\n task: {\n id: task.id,\n name: task.name,\n status: task.status.status,\n dueDate: task.due_date ? new Date(Number(task.due_date)).toISOString() : null,\n priority: task.priority?.priority ?? \"none\",\n assignees: task.assignees.map((a) => a.username),\n url: `https://app.clickup.com/t/${task.id}`,\n },\n };\n },\n});\n",
520
- "tools/create-task.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createTask } from \"../../lib/clickup-client.ts\";\n\nexport default tool({\n id: \"create-task\",\n description: \"Create a new task in a ClickUp list.\",\n inputSchema: z.object({\n listId: z.string().describe(\"The ID of the list to create the task in\"),\n name: z.string().describe(\"The name/title of the task\"),\n description: z.string().optional().describe(\"Description or details for the task\"),\n assignees: z.array(z.number()).optional().describe(\"Array of user IDs to assign the task to\"),\n tags: z.array(z.string()).optional().describe(\"Array of tag names to add to the task\"),\n status: z.string().optional().describe(\"Status name for the task\"),\n priority: z\n .number()\n .min(1)\n .max(4)\n .optional()\n .describe(\"Priority level: 1 (urgent), 2 (high), 3 (normal), 4 (low)\"),\n dueDate: z.number().optional().describe(\"Due date in Unix timestamp (milliseconds)\"),\n startDate: z.number().optional().describe(\"Start date in Unix timestamp (milliseconds)\"),\n timeEstimate: z.number().optional().describe(\"Time estimate in milliseconds\"),\n notifyAll: z.boolean().default(false).describe(\"Notify all assignees when task is created\"),\n }),\n async execute(input) {\n const task = await createTask(input);\n const dueDate = task.due_date ? new Date(Number(task.due_date)).toISOString() : null;\n\n return {\n success: true,\n task: {\n id: task.id,\n name: task.name,\n status: task.status.status,\n dueDate,\n priority: task.priority?.priority ?? \"none\",\n assignees: task.assignees.map((assignee) => assignee.username),\n list: task.list.name,\n url: `https://app.clickup.com/t/${task.id}`,\n },\n };\n },\n});\n",
521
- ".env.example": "# ClickUp OAuth Configuration\n# Get your credentials from https://app.clickup.com/settings/apps\nCLICKUP_CLIENT_ID=your-client-id\nCLICKUP_CLIENT_SECRET=your-client-secret\n",
522
- "lib/clickup-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst CLICKUP_BASE_URL = \"https://api.clickup.com/api/v2\";\n\ninterface ClickUpTask {\n id: string;\n name: string;\n description: string;\n status: {\n status: string;\n color: string;\n type: string;\n };\n date_created: string;\n date_updated: string;\n date_closed: string | null;\n creator: {\n id: number;\n username: string;\n email: string;\n };\n assignees: Array<{\n id: number;\n username: string;\n email: string;\n }>;\n tags: Array<{\n name: string;\n tag_fg: string;\n tag_bg: string;\n }>;\n due_date: string | null;\n start_date: string | null;\n priority: {\n id: string;\n priority: string;\n color: string;\n } | null;\n list: {\n id: string;\n name: string;\n };\n folder: {\n id: string;\n name: string;\n };\n space: {\n id: string;\n name: string;\n };\n}\n\ninterface ClickUpList {\n id: string;\n name: string;\n orderindex: number;\n content: string;\n status: {\n status: string;\n color: string;\n };\n priority: {\n priority: string;\n color: string;\n } | null;\n assignee: {\n id: number;\n username: string;\n email: string;\n } | null;\n task_count: number;\n due_date: string | null;\n start_date: string | null;\n folder: {\n id: string;\n name: string;\n hidden: boolean;\n access: boolean;\n };\n space: {\n id: string;\n name: string;\n access: boolean;\n };\n archived: boolean;\n}\n\ninterface ClickUpFolder {\n id: string;\n name: string;\n orderindex: number;\n override_statuses: boolean;\n hidden: boolean;\n space: {\n id: string;\n name: string;\n };\n task_count: string;\n lists: ClickUpList[];\n}\n\ninterface ClickUpSpace {\n id: string;\n name: string;\n private: boolean;\n statuses: Array<{\n status: string;\n type: string;\n orderindex: number;\n color: string;\n }>;\n multiple_assignees: boolean;\n features: {\n due_dates: {\n enabled: boolean;\n start_date: boolean;\n remap_due_dates: boolean;\n remap_closed_due_date: boolean;\n };\n time_tracking: {\n enabled: boolean;\n };\n tags: {\n enabled: boolean;\n };\n time_estimates: {\n enabled: boolean;\n };\n checklists: {\n enabled: boolean;\n };\n custom_fields: {\n enabled: boolean;\n };\n remap_dependencies: {\n enabled: boolean;\n };\n dependency_warning: {\n enabled: boolean;\n };\n portfolios: {\n enabled: boolean;\n };\n };\n}\n\nfunction buildCustomTaskParams(options?: { customTaskIds?: boolean; teamId?: string }): URLSearchParams {\n const params = new URLSearchParams();\n\n if (!options?.customTaskIds) return params;\n\n params.set(\"custom_task_ids\", \"true\");\n if (options.teamId) params.set(\"team_id\", options.teamId);\n\n return params;\n}\n\nfunction withQuery(endpoint: string, params: URLSearchParams): string {\n const queryString = params.toString();\n return queryString ? `${endpoint}?${queryString}` : endpoint;\n}\n\nasync function clickupFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) throw new Error(\"Not authenticated with ClickUp. Please connect your account.\");\n\n const response = await fetch(`${CLICKUP_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: token,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (response.ok) return response.json();\n\n const error = await response.json().catch(() => ({} as { err?: string; error?: string }));\n const message = error.err ?? error.error ?? response.statusText;\n\n throw new Error(`ClickUp API error: ${response.status} ${message}`);\n}\n\nexport async function listSpaces(teamId: string): Promise<ClickUpSpace[]> {\n const response = await clickupFetch<{ spaces: ClickUpSpace[] }>(`/team/${teamId}/space`);\n return response.spaces;\n}\n\nexport async function listFolders(spaceId: string): Promise<ClickUpFolder[]> {\n const response = await clickupFetch<{ folders: ClickUpFolder[] }>(`/space/${spaceId}/folder`);\n return response.folders;\n}\n\nexport async function listLists(folderId: string): Promise<ClickUpList[]> {\n const response = await clickupFetch<{ lists: ClickUpList[] }>(`/folder/${folderId}/list`);\n return response.lists;\n}\n\nexport async function listFolderlessLists(spaceId: string): Promise<ClickUpList[]> {\n const response = await clickupFetch<{ lists: ClickUpList[] }>(`/space/${spaceId}/list`);\n return response.lists;\n}\n\nexport async function listTasks(options: {\n listId?: string;\n spaceId?: string;\n folderId?: string;\n assignees?: number[];\n statuses?: string[];\n includeClosed?: boolean;\n orderBy?: string;\n subtasks?: boolean;\n}): Promise<ClickUpTask[]> {\n const params = new URLSearchParams();\n\n options.assignees?.forEach((assignee) => params.append(\"assignees[]\", assignee.toString()));\n options.statuses?.forEach((status) => params.append(\"statuses[]\", status));\n\n if (options.includeClosed !== undefined) params.set(\"include_closed\", options.includeClosed.toString());\n if (options.orderBy) params.set(\"order_by\", options.orderBy);\n if (options.subtasks !== undefined) params.set(\"subtasks\", options.subtasks.toString());\n\n let endpoint = \"/task\";\n if (options.listId) endpoint = `/list/${options.listId}/task`;\n else if (options.folderId) endpoint = `/folder/${options.folderId}/task`;\n else if (options.spaceId) endpoint = `/space/${options.spaceId}/task`;\n\n const response = await clickupFetch<{ tasks: ClickUpTask[] }>(withQuery(endpoint, params));\n return response.tasks;\n}\n\nexport async function getTask(\n taskId: string,\n options?: {\n customTaskIds?: boolean;\n teamId?: string;\n includeSubtasks?: boolean;\n },\n): Promise<ClickUpTask> {\n const params = buildCustomTaskParams(options);\n if (options?.includeSubtasks) params.set(\"include_subtasks\", \"true\");\n\n return clickupFetch<ClickUpTask>(withQuery(`/task/${taskId}`, params));\n}\n\nexport async function createTask(options: {\n listId: string;\n name: string;\n description?: string;\n assignees?: number[];\n tags?: string[];\n status?: string;\n priority?: number;\n dueDate?: number;\n dueDateTime?: boolean;\n timeEstimate?: number;\n startDate?: number;\n startDateTime?: boolean;\n notifyAll?: boolean;\n parent?: string;\n linksTo?: string;\n checkRequired?: boolean;\n customTaskIds?: boolean;\n teamId?: string;\n}): Promise<ClickUpTask> {\n const body: Record<string, unknown> = { name: options.name };\n\n if (options.description) body.description = options.description;\n if (options.assignees) body.assignees = options.assignees;\n if (options.tags) body.tags = options.tags;\n if (options.status) body.status = options.status;\n if (options.priority !== undefined) body.priority = options.priority;\n\n if (options.dueDate) {\n body.due_date = options.dueDate;\n if (options.dueDateTime !== undefined) body.due_date_time = options.dueDateTime;\n }\n\n if (options.timeEstimate) body.time_estimate = options.timeEstimate;\n\n if (options.startDate) {\n body.start_date = options.startDate;\n if (options.startDateTime !== undefined) body.start_date_time = options.startDateTime;\n }\n\n if (options.notifyAll !== undefined) body.notify_all = options.notifyAll;\n if (options.parent) body.parent = options.parent;\n if (options.linksTo) body.links_to = options.linksTo;\n\n const url = withQuery(`/list/${options.listId}/task`, buildCustomTaskParams(options));\n\n return clickupFetch<ClickUpTask>(url, {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n}\n\nexport async function updateTask(\n taskId: string,\n updates: {\n name?: string;\n description?: string;\n status?: string;\n priority?: number | null;\n dueDate?: number | null;\n dueDateTime?: boolean;\n timeEstimate?: number | null;\n startDate?: number | null;\n startDateTime?: boolean;\n assignees?: {\n add?: number[];\n rem?: number[];\n };\n archived?: boolean;\n },\n options?: {\n customTaskIds?: boolean;\n teamId?: string;\n },\n): Promise<ClickUpTask> {\n const body: Record<string, unknown> = {};\n\n if (updates.name !== undefined) body.name = updates.name;\n if (updates.description !== undefined) body.description = updates.description;\n if (updates.status !== undefined) body.status = updates.status;\n if (updates.priority !== undefined) body.priority = updates.priority;\n\n if (updates.dueDate !== undefined) {\n body.due_date = updates.dueDate;\n if (updates.dueDateTime !== undefined) body.due_date_time = updates.dueDateTime;\n }\n\n if (updates.timeEstimate !== undefined) body.time_estimate = updates.timeEstimate;\n\n if (updates.startDate !== undefined) {\n body.start_date = updates.startDate;\n if (updates.startDateTime !== undefined) body.start_date_time = updates.startDateTime;\n }\n\n if (updates.assignees) body.assignees = updates.assignees;\n if (updates.archived !== undefined) body.archived = updates.archived;\n\n const url = withQuery(`/task/${taskId}`, buildCustomTaskParams(options));\n\n return clickupFetch<ClickUpTask>(url, {\n method: \"PUT\",\n body: JSON.stringify(body),\n });\n}\n\nexport async function getAuthorizedUser(): Promise<{\n user: {\n id: number;\n username: string;\n email: string;\n color: string;\n profilePicture: string;\n };\n}> {\n return clickupFetch<{\n user: {\n id: number;\n username: string;\n email: string;\n color: string;\n profilePicture: string;\n };\n }>(\"/user\");\n}\n\nexport async function getTeams(): Promise<\n Array<{\n id: string;\n name: string;\n color: string;\n avatar: string | null;\n members: Array<{\n user: {\n id: number;\n username: string;\n email: string;\n };\n }>;\n }>\n> {\n const response = await clickupFetch<{\n teams: Array<{\n id: string;\n name: string;\n color: string;\n avatar: string | null;\n members: Array<{\n user: {\n id: number;\n username: string;\n email: string;\n };\n }>;\n }>;\n }>(\"/team\");\n\n return response.teams;\n}\n",
523
- "app/api/auth/clickup/callback/route.ts": "import { clickupConfig, createOAuthCallbackHandler } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(clickupConfig, { tokenStore: hybridTokenStore });\n",
524
- "app/api/auth/clickup/route.ts": "import { clickupConfig, createOAuthInitHandler } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT).\n// NEVER return a shared constant in production - it breaks per-user token isolation (VULN-AUTH-2).\nfunction getUserId(_request: Request): string {\n return \"current-user\";\n}\n\nexport const GET = createOAuthInitHandler(clickupConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});"
525
- }
526
- },
527
425
  "integration:jira": {
528
426
  "files": {
529
427
  "tools/create-issue.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createIssue } from \"../../lib/jira-client.ts\";\n\nexport default tool({\n id: \"create-issue\",\n description:\n \"Create a new Jira issue in a project. Requires project key, summary, and issue type. Optionally set description, priority, assignee, and labels.\",\n inputSchema: z.object({\n projectKey: z.string().describe('The project key (e.g., \"PROJ\", \"DEV\")'),\n summary: z.string().describe(\"Brief summary/title of the issue\"),\n issueType: z.string().describe('Type of issue: \"Task\", \"Bug\", \"Story\", \"Epic\", etc.'),\n description: z.string().optional().describe(\"Detailed description of the issue\"),\n priority: z\n .string()\n .optional()\n .describe('Priority: \"Highest\", \"High\", \"Medium\", \"Low\", \"Lowest\"'),\n assigneeId: z.string().optional().describe(\"Atlassian account ID of the assignee (optional)\"),\n labels: z.array(z.string()).optional().describe(\"Array of labels to add to the issue\"),\n }),\n async execute({ projectKey, summary, issueType, description, priority, assigneeId, labels }) {\n const { key, id, fields } = await createIssue({\n projectKey,\n summary,\n issueType,\n description,\n priority,\n assigneeId,\n labels,\n });\n\n return {\n key,\n id,\n summary: fields.summary,\n status: fields.status.name,\n type: fields.issuetype.name,\n priority: fields.priority?.name,\n assignee: fields.assignee?.displayName,\n project: {\n key: fields.project.key,\n name: fields.project.name,\n },\n created: fields.created,\n message: `Issue ${key} created successfully`,\n };\n },\n});\n",
@@ -536,18 +434,6 @@ export default {
536
434
  "app/api/auth/jira/route.ts": "import { createOAuthInitHandler, jiraConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT).\n// NEVER return a shared constant in production - it breaks per-user token isolation (VULN-AUTH-2).\nfunction getUserId(_request: Request): string {\n return \"current-user\";\n}\n\nexport const GET = createOAuthInitHandler(jiraConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});"
537
435
  }
538
436
  },
539
- "integration:zendesk": {
540
- "files": {
541
- "tools/get-ticket.ts": "import { z } from \"zod\";\nimport { getZendeskClient } from \"../../lib/zendesk-client.ts\";\nimport { isZendeskConnected } from \"../../lib/token-store.ts\";\n\nexport default defineTool({\n id: \"zendesk-get-ticket\",\n description: \"Get detailed information about a specific Zendesk ticket by ID\",\n inputSchema: z.object({\n ticketId: z.number().describe(\"The ticket ID to retrieve\"),\n }),\n async execute(input) {\n if (!(await isZendeskConnected())) {\n return {\n error: \"Zendesk not connected\",\n action: \"Please connect Zendesk via /api/auth/zendesk\",\n };\n }\n\n try {\n const client = getZendeskClient();\n const ticket = await client.getTicket(input.ticketId);\n\n return {\n ticket: {\n id: ticket.id,\n url: ticket.url,\n subject: ticket.subject,\n description: ticket.description,\n status: ticket.status,\n priority: ticket.priority,\n type: ticket.type,\n requester_id: ticket.requester_id,\n submitter_id: ticket.submitter_id,\n assignee_id: ticket.assignee_id,\n tags: ticket.tags,\n created_at: ticket.created_at,\n updated_at: ticket.updated_at,\n due_at: ticket.due_at,\n },\n };\n } catch (error) {\n return {\n error: error instanceof Error ? error.message : \"Failed to get ticket\",\n };\n }\n },\n});\n",
542
- "tools/list-tickets.ts": "import { z } from \"zod\";\nimport { getZendeskClient } from \"../../lib/zendesk-client.ts\";\nimport { isZendeskConnected } from \"../../lib/token-store.ts\";\n\nexport default defineTool({\n id: \"zendesk-list-tickets\",\n description: \"List tickets from Zendesk with optional filters for status, priority, or assignee\",\n inputSchema: z.object({\n limit: z.number().optional().describe(\"Maximum number of tickets to return (default: 20)\"),\n status: z\n .enum([\"new\", \"open\", \"pending\", \"hold\", \"solved\", \"closed\"])\n .optional()\n .describe(\"Filter by ticket status\"),\n priority: z\n .enum([\"urgent\", \"high\", \"normal\", \"low\"])\n .optional()\n .describe(\"Filter by priority level\"),\n assigneeId: z.number().optional().describe(\"Filter by assignee user ID\"),\n }),\n async execute(input) {\n if (!(await isZendeskConnected())) {\n return {\n error: \"Zendesk not connected\",\n action: \"Please connect Zendesk via /api/auth/zendesk\",\n };\n }\n\n try {\n const client = getZendeskClient();\n const tickets = await client.listTickets(input);\n\n return {\n count: tickets.length,\n tickets: tickets.map(\n ({\n id,\n subject,\n status,\n priority,\n type,\n requester_id,\n assignee_id,\n tags,\n created_at,\n updated_at,\n }) => ({\n id,\n subject,\n status,\n priority,\n type,\n requester_id,\n assignee_id,\n tags,\n created_at,\n updated_at,\n }),\n ),\n };\n } catch (error) {\n return {\n error: error instanceof Error ? error.message : \"Failed to list tickets\",\n };\n }\n },\n});\n",
543
- "tools/create-ticket.ts": "import { z } from \"zod\";\nimport { getZendeskClient } from \"../../lib/zendesk-client.ts\";\nimport { isZendeskConnected } from \"../../lib/token-store.ts\";\n\ntype TicketData = {\n subject: string;\n comment: { body: string };\n requester?: { name: string; email: string };\n priority?: \"urgent\" | \"high\" | \"normal\" | \"low\";\n type?: \"problem\" | \"incident\" | \"question\" | \"task\";\n tags?: string[];\n assignee_id?: number;\n};\n\nexport default defineTool({\n id: \"zendesk-create-ticket\",\n description: \"Create a new ticket in Zendesk\",\n inputSchema: z.object({\n subject: z.string().describe(\"Subject/title of the ticket\"),\n body: z.string().describe(\"Description/body content of the ticket\"),\n priority: z\n .enum([\"urgent\", \"high\", \"normal\", \"low\"])\n .optional()\n .describe(\"Priority level of the ticket\"),\n type: z\n .enum([\"problem\", \"incident\", \"question\", \"task\"])\n .optional()\n .describe(\"Type of ticket\"),\n tags: z.array(z.string()).optional().describe(\"Tags to add to the ticket\"),\n assigneeId: z.number().optional().describe(\"User ID to assign the ticket to\"),\n requesterName: z\n .string()\n .optional()\n .describe(\"Name of the requester (if creating on behalf)\"),\n requesterEmail: z\n .string()\n .optional()\n .describe(\"Email of the requester (if creating on behalf)\"),\n }),\n async execute(input) {\n if (!(await isZendeskConnected())) {\n return {\n error: \"Zendesk not connected\",\n action: \"Please connect Zendesk via /api/auth/zendesk\",\n };\n }\n\n try {\n const client = getZendeskClient();\n\n let requester: TicketData[\"requester\"];\n if (input.requesterName && input.requesterEmail) {\n requester = { name: input.requesterName, email: input.requesterEmail };\n }\n\n const ticketData: TicketData = {\n subject: input.subject,\n comment: { body: input.body },\n priority: input.priority,\n type: input.type,\n tags: input.tags,\n assignee_id: input.assigneeId,\n requester,\n };\n\n const ticket = await client.createTicket(ticketData);\n\n return {\n success: true,\n id: ticket.id,\n url: ticket.url,\n subject: ticket.subject,\n status: ticket.status,\n priority: ticket.priority,\n type: ticket.type,\n message: `Ticket #${ticket.id} created successfully`,\n };\n } catch (error) {\n return {\n error: error instanceof Error ? error.message : \"Failed to create ticket\",\n };\n }\n },\n});\n",
544
- "tools/search-tickets.ts": "import { z } from \"zod\";\nimport { getZendeskClient } from \"../../lib/zendesk-client.ts\";\nimport { isZendeskConnected } from \"../../lib/token-store.ts\";\n\nexport default defineTool({\n id: \"zendesk-search-tickets\",\n description:\n \"Search tickets using Zendesk query syntax. Examples: 'status:open priority:urgent', 'subject:bug', 'tags:billing'\",\n inputSchema: z.object({\n query: z\n .string()\n .describe(\n \"Search query using Zendesk syntax (e.g., 'status:open priority:urgent', 'subject:refund', 'tags:billing')\",\n ),\n limit: z.number().optional().describe(\"Maximum number of results to return (default: 20)\"),\n }),\n async execute(input) {\n if (!(await isZendeskConnected())) {\n return {\n error: \"Zendesk not connected\",\n action: \"Please connect Zendesk via /api/auth/zendesk\",\n };\n }\n\n try {\n const client = getZendeskClient();\n const tickets = await client.searchTickets(input.query, input.limit);\n\n return {\n count: tickets.length,\n query: input.query,\n tickets: tickets.map((ticket) => ({\n id: ticket.id,\n subject: ticket.subject,\n description: ticket.description,\n status: ticket.status,\n priority: ticket.priority,\n type: ticket.type,\n requester_id: ticket.requester_id,\n assignee_id: ticket.assignee_id,\n tags: ticket.tags,\n created_at: ticket.created_at,\n updated_at: ticket.updated_at,\n })),\n };\n } catch (error) {\n return {\n error: error instanceof Error ? error.message : \"Failed to search tickets\",\n };\n }\n },\n});\n",
545
- ".env.example": "# Zendesk OAuth Configuration\n# Get these from your Zendesk Admin Center: Apps and integrations > APIs > Zendesk API > OAuth Clients\nZENDESK_SUBDOMAIN=your-subdomain\nZENDESK_CLIENT_ID=your_client_id\nZENDESK_CLIENT_SECRET=your_client_secret\n",
546
- "lib/zendesk-client.ts": "import { getZendeskTokens } from \"./token-store.ts\";\n\nfunction getEnv(name: string): string | undefined {\n if (typeof Deno !== \"undefined\") {\n // @ts-ignore: Deno global\n return Deno.env.get(name);\n }\n\n // @ts-ignore: Node process\n return globalThis.process?.env?.[name];\n}\n\nexport interface ZendeskTicket {\n id: number;\n url: string;\n subject: string;\n description: string;\n status: \"new\" | \"open\" | \"pending\" | \"hold\" | \"solved\" | \"closed\";\n priority: \"urgent\" | \"high\" | \"normal\" | \"low\" | null;\n type: \"problem\" | \"incident\" | \"question\" | \"task\" | null;\n requester_id: number;\n submitter_id: number;\n assignee_id: number | null;\n tags: string[];\n created_at: string;\n updated_at: string;\n due_at: string | null;\n}\n\nexport interface ZendeskUser {\n id: number;\n url: string;\n name: string;\n email: string;\n role: \"end-user\" | \"agent\" | \"admin\";\n phone: string | null;\n photo: { url: string } | null;\n created_at: string;\n updated_at: string;\n}\n\nexport interface ZendeskComment {\n id: number;\n type: \"Comment\" | \"VoiceComment\";\n author_id: number;\n body: string;\n html_body: string;\n public: boolean;\n created_at: string;\n}\n\nexport interface ZendeskResponse<T> {\n [key: string]: T;\n}\n\nexport interface ZendeskListResponse<T> {\n [key: string]: T[];\n count?: number;\n next_page?: string | null;\n previous_page?: string | null;\n}\n\nexport class ZendeskClient {\n private subdomain: string;\n private accessToken: string | null = null;\n\n constructor() {\n const subdomain = getEnv(\"ZENDESK_SUBDOMAIN\");\n if (!subdomain) throw new Error(\"ZENDESK_SUBDOMAIN not configured\");\n this.subdomain = subdomain;\n }\n\n private get baseUrl(): string {\n return `https://${this.subdomain}.zendesk.com/api/v2`;\n }\n\n async ensureAuthenticated(): Promise<void> {\n const tokens = await getZendeskTokens();\n if (!tokens) {\n throw new Error(\n \"Zendesk not connected. Please connect via /api/auth/zendesk\",\n );\n }\n this.accessToken = tokens.accessToken;\n }\n\n private async request<T>(\n endpoint: string,\n options: RequestInit = {},\n ): Promise<T> {\n await this.ensureAuthenticated();\n\n const response = await fetch(`${this.baseUrl}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${this.accessToken}`,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Zendesk API error: ${response.status} ${errorText}`);\n }\n\n return response.json();\n }\n\n async listTickets(options: {\n limit?: number;\n status?: string;\n priority?: string;\n assigneeId?: number;\n } = {}): Promise<ZendeskTicket[]> {\n const { limit, status, priority, assigneeId } = options;\n\n const queryParts: string[] = [];\n if (status) queryParts.push(`status:${status}`);\n if (priority) queryParts.push(`priority:${priority}`);\n if (assigneeId) queryParts.push(`assignee:${assigneeId}`);\n\n let endpoint = \"/tickets.json\";\n\n if (queryParts.length > 0) {\n endpoint = `/search.json?query=type:ticket ${queryParts.join(\" \")}`;\n if (limit) endpoint += `&per_page=${limit}`;\n } else if (limit) {\n endpoint += `?per_page=${limit}`;\n }\n\n const response = await this.request<ZendeskListResponse<ZendeskTicket>>(\n endpoint,\n );\n\n return response.tickets ?? response.results ?? [];\n }\n\n async getTicket(ticketId: number): Promise<ZendeskTicket> {\n const response = await this.request<ZendeskResponse<ZendeskTicket>>(\n `/tickets/${ticketId}.json`,\n );\n return response.ticket;\n }\n\n async createTicket(data: {\n subject: string;\n comment: { body: string };\n requester?: { name: string; email: string };\n priority?: \"urgent\" | \"high\" | \"normal\" | \"low\";\n type?: \"problem\" | \"incident\" | \"question\" | \"task\";\n tags?: string[];\n assignee_id?: number;\n }): Promise<ZendeskTicket> {\n const response = await this.request<ZendeskResponse<ZendeskTicket>>(\n \"/tickets.json\",\n {\n method: \"POST\",\n body: JSON.stringify({ ticket: data }),\n },\n );\n return response.ticket;\n }\n\n async updateTicket(\n ticketId: number,\n data: Partial<{\n subject: string;\n comment: { body: string; public: boolean };\n status: \"new\" | \"open\" | \"pending\" | \"hold\" | \"solved\" | \"closed\";\n priority: \"urgent\" | \"high\" | \"normal\" | \"low\";\n assignee_id: number;\n tags: string[];\n }>,\n ): Promise<ZendeskTicket> {\n const response = await this.request<ZendeskResponse<ZendeskTicket>>(\n `/tickets/${ticketId}.json`,\n {\n method: \"PUT\",\n body: JSON.stringify({ ticket: data }),\n },\n );\n return response.ticket;\n }\n\n async listUsers(options: { limit?: number; role?: string } = {}): Promise<\n ZendeskUser[]\n > {\n const params = new URLSearchParams();\n if (options.limit) params.set(\"per_page\", String(options.limit));\n if (options.role) params.set(\"role\", options.role);\n\n const query = params.toString();\n const endpoint = `/users.json${query ? `?${query}` : \"\"}`;\n\n const response = await this.request<ZendeskListResponse<ZendeskUser>>(\n endpoint,\n );\n return response.users ?? [];\n }\n\n async getUser(userId: number): Promise<ZendeskUser> {\n const response = await this.request<ZendeskResponse<ZendeskUser>>(\n `/users/${userId}.json`,\n );\n return response.user;\n }\n\n async searchTickets(query: string, limit = 20): Promise<ZendeskTicket[]> {\n const params = new URLSearchParams({\n query: `type:ticket ${query}`,\n per_page: String(limit),\n });\n\n const response = await this.request<ZendeskListResponse<ZendeskTicket>>(\n `/search.json?${params}`,\n );\n return response.results ?? [];\n }\n\n addComment(\n ticketId: number,\n body: string,\n isPublic = true,\n ): Promise<ZendeskTicket> {\n return this.updateTicket(ticketId, { comment: { body, public: isPublic } });\n }\n}\n\nlet client: ZendeskClient | null = null;\n\nexport function getZendeskClient(): ZendeskClient {\n client ??= new ZendeskClient();\n return client;\n}\n",
547
- "app/api/auth/zendesk/callback/route.ts": "import { setZendeskTokens } from \"../../../../../lib/token-store.ts\";\n\nfunction getEnv(name: string): string | undefined {\n if (typeof Deno !== \"undefined\") {\n // @ts-ignore: Deno global\n return Deno.env.get(name);\n }\n\n // @ts-ignore: Node process\n return globalThis.process?.env?.[name];\n}\n\nexport async function GET(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const code = url.searchParams.get(\"code\");\n const error = url.searchParams.get(\"error\");\n const errorDescription = url.searchParams.get(\"error_description\");\n\n const configuredUrl = getEnv(\"NEXT_PUBLIC_APP_URL\");\n if (!configuredUrl) {\n return Response.json(\n { error: \"NEXT_PUBLIC_APP_URL environment variable is required\" },\n { status: 500 },\n );\n }\n const baseUrl = configuredUrl;\n\n if (error) {\n console.error(\"Zendesk OAuth error:\", error, errorDescription);\n const description = encodeURIComponent(errorDescription ?? error);\n return Response.redirect(\n `${baseUrl}/?error=zendesk_oauth_failed&description=${description}`,\n 302,\n );\n }\n\n if (!code) return Response.redirect(`${baseUrl}/?error=no_code`, 302);\n\n const subdomain = getEnv(\"ZENDESK_SUBDOMAIN\");\n const clientId = getEnv(\"ZENDESK_CLIENT_ID\");\n const clientSecret = getEnv(\"ZENDESK_CLIENT_SECRET\");\n\n if (!subdomain || !clientId || !clientSecret) {\n return Response.redirect(`${baseUrl}/?error=zendesk_not_configured`, 302);\n }\n\n const redirectUri = `${baseUrl}/api/auth/zendesk/callback`;\n\n try {\n const tokenResponse = await fetch(\n `https://${subdomain}.zendesk.com/oauth/tokens`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n grant_type: \"authorization_code\",\n code,\n client_id: clientId,\n client_secret: clientSecret,\n redirect_uri: redirectUri,\n scope: \"read write\",\n }),\n },\n );\n\n if (!tokenResponse.ok) {\n const errorText = await tokenResponse.text();\n console.error(\"Zendesk token exchange failed:\", errorText);\n return Response.redirect(`${baseUrl}/?error=token_exchange_failed`, 302);\n }\n\n const tokens = await tokenResponse.json();\n\n await setZendeskTokens({\n accessToken: tokens.access_token,\n refreshToken: tokens.refresh_token,\n expiresAt: tokens.expires_in\n ? Date.now() + tokens.expires_in * 1000\n : undefined,\n subdomain,\n });\n\n return Response.redirect(`${baseUrl}/?connected=zendesk`, 302);\n } catch (error) {\n console.error(\"Zendesk OAuth error:\", error);\n return Response.redirect(`${baseUrl}/?error=zendesk_oauth_failed`, 302);\n }\n}\n",
548
- "app/api/auth/zendesk/route.ts": "const getEnv = (name: string): string | undefined => {\n // @ts-ignore: Deno global\n if (typeof Deno !== \"undefined\") return Deno.env.get(name);\n // @ts-ignore: Node process\n return globalThis.process?.env?.[name];\n};\n\nexport function GET(request: Request): Response {\n const subdomain = getEnv(\"ZENDESK_SUBDOMAIN\");\n const clientId = getEnv(\"ZENDESK_CLIENT_ID\");\n\n if (!subdomain || !clientId) {\n return Response.json({ error: \"Zendesk OAuth not configured\" }, { status: 500 });\n }\n\n const baseUrl = getEnv(\"NEXT_PUBLIC_APP_URL\");\n if (!baseUrl) {\n return Response.json(\n { error: \"NEXT_PUBLIC_APP_URL environment variable is required\" },\n { status: 500 },\n );\n }\n const redirectUri = `${baseUrl}/api/auth/zendesk/callback`;\n\n const authUrl = new URL(`https://${subdomain}.zendesk.com/oauth/authorizations/new`);\n authUrl.searchParams.set(\"response_type\", \"code\");\n authUrl.searchParams.set(\"client_id\", clientId);\n authUrl.searchParams.set(\"redirect_uri\", redirectUri);\n authUrl.searchParams.set(\"scope\", \"read write\");\n authUrl.searchParams.set(\"state\", crypto.randomUUID());\n\n return Response.redirect(authUrl.toString(), 302);\n}\n"
549
- }
550
- },
551
437
  "integration:trello": {
552
438
  "files": {
553
439
  "tools/update-card.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { updateCard } from \"../../lib/trello-client.ts\";\n\nexport default tool({\n id: \"update-card\",\n description: \"Update an existing Trello card.\",\n inputSchema: z.object({\n cardId: z.string().describe(\"The ID of the card to update\"),\n name: z.string().optional().describe(\"New name/title for the card\"),\n desc: z.string().optional().describe(\"New description or details\"),\n closed: z.boolean().optional().describe(\"Archive or unarchive the card\"),\n idList: z.string().optional().describe(\"Move the card to a different list by list ID\"),\n due: z\n .string()\n .nullable()\n .optional()\n .describe(\"New due date in ISO 8601 format, or null to remove due date\"),\n dueComplete: z.boolean().optional().describe(\"Mark the due date as complete or incomplete\"),\n pos: z\n .union([z.string(), z.number()])\n .optional()\n .describe('New position: \"top\", \"bottom\", or a positive number'),\n idMembers: z\n .array(z.string())\n .optional()\n .describe(\"Array of member IDs to assign to the card (replaces existing)\"),\n idLabels: z\n .array(z.string())\n .optional()\n .describe(\"Array of label IDs for the card (replaces existing)\"),\n }),\n async execute({ cardId, ...updates }) {\n const {\n id,\n name,\n desc,\n url,\n closed,\n idList,\n due,\n dueComplete,\n labels,\n } = await updateCard(cardId, updates);\n\n return {\n success: true,\n card: {\n id,\n name,\n desc,\n url,\n closed,\n idList,\n due,\n dueComplete,\n labels: labels.map(({ id, name, color }) => ({ id, name, color })),\n },\n };\n },\n});\n",
@@ -572,19 +458,6 @@ export default {
572
458
  "lib/mixpanel-client.ts": "import { getApiSecret, getProjectId, getProjectToken } from \"./token-store.ts\";\n\nconst MIXPANEL_API_BASE = \"https://mixpanel.com/api\";\nconst MIXPANEL_TRACK_BASE = \"https://api.mixpanel.com\";\nconst MIXPANEL_DATA_BASE = \"https://data.mixpanel.com/api/2.0\";\n\nexport interface MixpanelEvent {\n event: string;\n properties: Record<string, unknown>;\n}\n\nexport interface MixpanelEventQuery {\n event?: string;\n from_date: string;\n to_date: string;\n where?: string;\n limit?: number;\n}\n\nexport interface MixpanelEventResult {\n event: string;\n properties: Record<string, unknown>;\n}\n\nexport interface MixpanelFunnel {\n funnel_id: number;\n name: string;\n steps: Array<{\n event: string;\n count: number;\n avg_time: number | null;\n overall_conv_ratio: number;\n step_conv_ratio: number;\n }>;\n data: {\n series: string[];\n values: Record<string, number[]>;\n };\n}\n\nexport interface MixpanelRetention {\n date: string;\n count: number;\n retention: Array<{\n day: number;\n count: number;\n rate: number;\n }>;\n}\n\nexport interface MixpanelCohort {\n id: number;\n name: string;\n description: string;\n count: number;\n created: string;\n is_visible: boolean;\n project_id: number;\n}\n\ninterface MixpanelError {\n error: string;\n request: string;\n}\n\nfunction getAuthHeader(): string {\n const apiSecret = getApiSecret();\n if (!apiSecret) {\n throw new Error(\n \"Not authenticated with Mixpanel. Please set MIXPANEL_API_SECRET.\",\n );\n }\n\n // Mixpanel uses Basic auth with API secret as username and empty password\n return `Basic ${btoa(`${apiSecret}:`)}`;\n}\n\nasync function mixpanelFetch<T>(\n baseUrl: string,\n endpoint: string,\n options: RequestInit & { params?: Record<string, string | number | boolean> } = {},\n): Promise<T> {\n const url = new URL(`${baseUrl}${endpoint}`);\n\n for (const [key, value] of Object.entries(options.params ?? {})) {\n url.searchParams.append(key, String(value));\n }\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n ...(options.headers as Record<string, string> | undefined),\n };\n\n if (baseUrl === MIXPANEL_DATA_BASE || baseUrl === MIXPANEL_API_BASE) {\n headers.Authorization = getAuthHeader();\n }\n\n const response = await fetch(url.toString(), { ...options, headers });\n\n if (response.ok) return (await response.json()) as T;\n\n let errorMessage = `Mixpanel API error: ${response.status} ${response.statusText}`;\n\n try {\n const errorData = (await response.json()) as MixpanelError;\n if (errorData.error) errorMessage = `Mixpanel API error: ${errorData.error}`;\n } catch {\n // If parsing JSON fails, use default error message\n }\n\n throw new Error(errorMessage);\n}\n\nexport async function trackEvent(\n event: string,\n properties: Record<string, unknown>,\n distinctId: string,\n): Promise<{ status: number; error?: string }> {\n const projectToken = getProjectToken();\n if (!projectToken) {\n throw new Error(\n \"Not authenticated with Mixpanel. Please set MIXPANEL_PROJECT_TOKEN.\",\n );\n }\n\n const payload = {\n event,\n properties: {\n ...properties,\n token: projectToken,\n distinct_id: distinctId,\n time: Date.now(),\n },\n };\n\n const response = await fetch(`${MIXPANEL_TRACK_BASE}/track`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify([payload]),\n });\n\n if (response.ok) {\n return (await response.json()) as { status: number; error?: string };\n }\n\n const text = await response.text();\n return {\n status: 0,\n error: `Failed to track event: ${response.status} ${text}`,\n };\n}\n\nexport async function queryEvents(\n from: string,\n to: string,\n event?: string,\n): Promise<MixpanelEventResult[]> {\n const projectId = getProjectId();\n if (!projectId) {\n throw new Error(\"Project ID not set. Please set MIXPANEL_PROJECT_ID.\");\n }\n\n const params: Record<string, string> = { from_date: from, to_date: to };\n if (event) params.event = JSON.stringify([event]);\n\n const response = await mixpanelFetch<string[]>(\n MIXPANEL_DATA_BASE,\n \"/export\",\n { params },\n );\n\n if (!Array.isArray(response)) return [];\n\n const events: MixpanelEventResult[] = [];\n\n for (const line of response) {\n if (typeof line !== \"string\" || !line.trim()) continue;\n\n try {\n const parsed = JSON.parse(line) as MixpanelEventResult;\n events.push({ event: parsed.event, properties: parsed.properties });\n } catch {\n // Skip malformed lines\n }\n }\n\n return events;\n}\n\nexport async function getFunnel(\n funnelId: number,\n from: string,\n to: string,\n): Promise<MixpanelFunnel> {\n const params: Record<string, string | number> = {\n funnel_id: funnelId,\n from_date: from,\n to_date: to,\n unit: \"day\",\n };\n\n return mixpanelFetch<MixpanelFunnel>(MIXPANEL_DATA_BASE, \"/funnels\", {\n params,\n });\n}\n\nexport async function getRetention(\n from: string,\n to: string,\n event: string,\n retentionType: \"birth\" | \"compounded\" = \"birth\",\n): Promise<MixpanelRetention[]> {\n const params: Record<string, string> = {\n from_date: from,\n to_date: to,\n retention_type: retentionType,\n born_event: event,\n event,\n unit: \"day\",\n };\n\n const response = await mixpanelFetch<Record<string, MixpanelRetention>>(\n MIXPANEL_DATA_BASE,\n \"/retention\",\n { params },\n );\n\n return Object.entries(response).map(([date, data]) => ({ date, ...data }));\n}\n\nexport async function listCohorts(): Promise<MixpanelCohort[]> {\n const projectId = getProjectId();\n if (!projectId) {\n throw new Error(\"Project ID not set. Please set MIXPANEL_PROJECT_ID.\");\n }\n\n return mixpanelFetch<MixpanelCohort[]>(\n MIXPANEL_API_BASE,\n \"/2.0/cohorts/list\",\n { params: { project_id: projectId } },\n );\n}\n\nexport function formatDate(date: Date): string {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const day = String(date.getDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n\nexport function getDateRange(days: number): { from: string; to: string } {\n const to = new Date();\n const from = new Date();\n from.setDate(from.getDate() - days);\n\n return { from: formatDate(from), to: formatDate(to) };\n}\n\nexport function calculateFunnelConversionRate(funnel: MixpanelFunnel): number {\n if (!funnel.steps || funnel.steps.length < 2) return 0;\n\n const firstStep = funnel.steps[0];\n const lastStep = funnel.steps[funnel.steps.length - 1];\n\n if (!firstStep || !lastStep || firstStep.count === 0) return 0;\n\n return (lastStep.count / firstStep.count) * 100;\n}\n"
573
459
  }
574
460
  },
575
- "integration:zoom": {
576
- "files": {
577
- "tools/list-meetings.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listMeetings } from \"../../lib/zoom-client.ts\";\n\nexport default tool({\n id: \"list-meetings\",\n description:\n \"List Zoom meetings for the current user. Can filter by meeting type (scheduled, live, upcoming, etc.).\",\n inputSchema: z.object({\n type: z\n .enum([\"scheduled\", \"live\", \"upcoming\", \"upcoming_meetings\", \"previous_meetings\"])\n .default(\"scheduled\")\n .describe(\"Type of meetings to list\"),\n pageSize: z\n .number()\n .min(1)\n .max(300)\n .default(30)\n .describe(\"Number of meetings to return per page\"),\n pageNumber: z.number().min(1).default(1).describe(\"Page number for pagination\"),\n }),\n async execute({ type, pageSize, pageNumber }) {\n const meetings = await listMeetings({ type, pageSize, pageNumber });\n\n return meetings.map((meeting) => ({\n id: meeting.id,\n uuid: meeting.uuid,\n topic: meeting.topic,\n type: meeting.type,\n startTime: meeting.start_time,\n duration: meeting.duration,\n timezone: meeting.timezone,\n agenda: meeting.agenda,\n joinUrl: meeting.join_url,\n password: meeting.password,\n status: meeting.status,\n }));\n },\n});\n",
578
- "tools/create-meeting.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createMeeting } from \"../../lib/zoom-client.ts\";\n\nexport default tool({\n id: \"create-meeting\",\n description: \"Create a new Zoom meeting with specified settings.\",\n inputSchema: z.object({\n topic: z.string().describe(\"The topic/title of the meeting\"),\n type: z\n .enum([\"1\", \"2\", \"3\", \"8\"])\n .transform((val) => parseInt(val, 10) as 1 | 2 | 3 | 8)\n .default(\"2\")\n .describe(\n \"Meeting type: 1=Instant, 2=Scheduled, 3=Recurring with no fixed time, 8=Recurring with fixed time\",\n ),\n startTime: z\n .string()\n .optional()\n .describe(\"Start time in ISO 8601 format (e.g., 2024-12-07T10:00:00Z)\"),\n duration: z.number().min(1).optional().describe(\"Meeting duration in minutes\"),\n timezone: z\n .string()\n .optional()\n .describe(\"Timezone (e.g., America/New_York, Europe/London)\"),\n password: z.string().optional().describe(\"Meeting password\"),\n agenda: z.string().optional().describe(\"Meeting agenda or description\"),\n hostVideo: z.boolean().default(true).describe(\"Start video when host joins\"),\n participantVideo: z\n .boolean()\n .default(true)\n .describe(\"Start video when participants join\"),\n joinBeforeHost: z\n .boolean()\n .default(false)\n .describe(\"Allow participants to join before host\"),\n muteUponEntry: z\n .boolean()\n .default(false)\n .describe(\"Mute participants upon entry\"),\n autoRecording: z\n .enum([\"local\", \"cloud\", \"none\"])\n .default(\"none\")\n .describe(\"Automatic recording setting\"),\n }),\n async execute(input) {\n const meeting = await createMeeting({\n topic: input.topic,\n type: input.type,\n startTime: input.startTime,\n duration: input.duration,\n timezone: input.timezone,\n password: input.password,\n agenda: input.agenda,\n settings: {\n hostVideo: input.hostVideo,\n participantVideo: input.participantVideo,\n joinBeforeHost: input.joinBeforeHost,\n muteUponEntry: input.muteUponEntry,\n autoRecording: input.autoRecording,\n audio: \"both\",\n },\n });\n\n return {\n success: true,\n meeting: {\n id: meeting.id,\n uuid: meeting.uuid,\n topic: meeting.topic,\n startTime: meeting.start_time,\n duration: meeting.duration,\n timezone: meeting.timezone,\n joinUrl: meeting.join_url,\n password: meeting.password,\n hostEmail: meeting.host_email,\n },\n };\n },\n});\n",
579
- "tools/update-meeting.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { updateMeeting } from \"../../lib/zoom-client.ts\";\n\nexport default tool({\n id: \"update-meeting\",\n description: \"Update an existing Zoom meeting with new settings.\",\n inputSchema: z.object({\n meetingId: z.union([z.string(), z.number()]).describe(\"The meeting ID to update\"),\n topic: z.string().optional().describe(\"The new topic/title of the meeting\"),\n type: z\n .enum([\"1\", \"2\", \"3\", \"8\"])\n .transform((val) => parseInt(val, 10) as 1 | 2 | 3 | 8)\n .optional()\n .describe(\n \"Meeting type: 1=Instant, 2=Scheduled, 3=Recurring with no fixed time, 8=Recurring with fixed time\",\n ),\n startTime: z.string().optional().describe(\"New start time in ISO 8601 format\"),\n duration: z.number().min(1).optional().describe(\"New meeting duration in minutes\"),\n timezone: z.string().optional().describe(\"New timezone\"),\n password: z.string().optional().describe(\"New meeting password\"),\n agenda: z.string().optional().describe(\"New meeting agenda or description\"),\n hostVideo: z.boolean().optional().describe(\"Start video when host joins\"),\n participantVideo: z.boolean().optional().describe(\"Start video when participants join\"),\n joinBeforeHost: z.boolean().optional().describe(\"Allow participants to join before host\"),\n muteUponEntry: z.boolean().optional().describe(\"Mute participants upon entry\"),\n autoRecording: z.enum([\"local\", \"cloud\", \"none\"]).optional().describe(\"Automatic recording setting\"),\n }),\n async execute({\n meetingId,\n topic,\n type,\n startTime,\n duration,\n timezone,\n password,\n agenda,\n hostVideo,\n participantVideo,\n joinBeforeHost,\n muteUponEntry,\n autoRecording,\n }): Promise<{ success: true; message: string }> {\n const settings = { hostVideo, participantVideo, joinBeforeHost, muteUponEntry, autoRecording };\n const hasSettings = Object.values(settings).some((value) => value !== undefined);\n\n await updateMeeting(meetingId, {\n topic,\n type,\n startTime,\n duration,\n timezone,\n password,\n agenda,\n settings: hasSettings ? settings : undefined,\n });\n\n return { success: true, message: `Meeting ${meetingId} updated successfully` };\n },\n});\n",
580
- "tools/get-meeting.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getMeeting } from \"../../lib/zoom-client.ts\";\n\nexport default tool({\n id: \"get-meeting\",\n description: \"Get detailed information about a specific Zoom meeting by its ID.\",\n inputSchema: z.object({\n meetingId: z.union([z.string(), z.number()]).describe(\"The meeting ID or UUID\"),\n }),\n async execute({ meetingId }) {\n const meeting = await getMeeting(meetingId);\n const settings = meeting.settings;\n\n return {\n id: meeting.id,\n uuid: meeting.uuid,\n topic: meeting.topic,\n type: meeting.type,\n startTime: meeting.start_time,\n duration: meeting.duration,\n timezone: meeting.timezone,\n agenda: meeting.agenda,\n joinUrl: meeting.join_url,\n password: meeting.password,\n hostId: meeting.host_id,\n hostEmail: meeting.host_email,\n status: meeting.status,\n createdAt: meeting.created_at,\n settings: settings && {\n hostVideo: settings.host_video,\n participantVideo: settings.participant_video,\n joinBeforeHost: settings.join_before_host,\n muteUponEntry: settings.mute_upon_entry,\n watermark: settings.watermark,\n audio: settings.audio,\n autoRecording: settings.auto_recording,\n },\n };\n },\n});\n",
581
- "tools/delete-meeting.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { deleteMeeting } from \"../../lib/zoom-client.ts\";\n\nexport default tool({\n id: \"delete-meeting\",\n description: \"Delete a scheduled Zoom meeting.\",\n inputSchema: z.object({\n meetingId: z.union([z.string(), z.number()]).describe(\"The meeting ID to delete\"),\n occurrenceId: z\n .string()\n .optional()\n .describe(\"The meeting occurrence ID for recurring meetings\"),\n scheduleForReminder: z\n .boolean()\n .default(false)\n .describe(\"Whether to send a reminder email to participants\"),\n }),\n async execute({ meetingId, occurrenceId, scheduleForReminder }) {\n await deleteMeeting(meetingId, { occurrenceId, scheduleForReminder });\n\n return {\n success: true,\n message: `Meeting ${meetingId} deleted successfully`,\n };\n },\n});\n",
582
- ".env.example": "# Zoom OAuth Configuration\n# Get your credentials from https://marketplace.zoom.us/develop/create\nZOOM_CLIENT_ID=your-client-id\nZOOM_CLIENT_SECRET=your-client-secret\n",
583
- "lib/zoom-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst ZOOM_BASE_URL = \"https://api.zoom.us/v2\";\n\ninterface ZoomMeeting {\n id: number;\n uuid: string;\n topic: string;\n type: number;\n start_time: string;\n duration: number;\n timezone: string;\n agenda: string;\n created_at: string;\n join_url: string;\n password?: string;\n host_id: string;\n host_email: string;\n status: string;\n settings?: {\n host_video: boolean;\n participant_video: boolean;\n join_before_host: boolean;\n mute_upon_entry: boolean;\n watermark: boolean;\n audio: string;\n auto_recording: string;\n };\n}\n\ninterface ZoomUser {\n id: string;\n first_name: string;\n last_name: string;\n email: string;\n type: number;\n pmi: number;\n timezone: string;\n verified: number;\n created_at: string;\n last_login_time: string;\n pic_url: string;\n}\n\ninterface ZoomMeetingList {\n meetings: ZoomMeeting[];\n page_count: number;\n page_number: number;\n page_size: number;\n total_records: number;\n}\n\ninterface MeetingSettingsInput {\n hostVideo?: boolean;\n participantVideo?: boolean;\n joinBeforeHost?: boolean;\n muteUponEntry?: boolean;\n watermark?: boolean;\n audio?: \"both\" | \"telephony\" | \"voip\";\n autoRecording?: \"local\" | \"cloud\" | \"none\";\n}\n\nfunction toZoomSettings(\n settings?: MeetingSettingsInput,\n): Record<string, unknown> | undefined {\n if (!settings) return undefined;\n\n return {\n host_video: settings.hostVideo,\n participant_video: settings.participantVideo,\n join_before_host: settings.joinBeforeHost,\n mute_upon_entry: settings.muteUponEntry,\n watermark: settings.watermark,\n audio: settings.audio,\n auto_recording: settings.autoRecording,\n };\n}\n\nasync function zoomFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Zoom. Please connect your account.\");\n }\n\n const response = await fetch(`${ZOOM_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({}))) as { message?: string };\n throw new Error(`Zoom API error: ${response.status} ${error.message ?? response.statusText}`);\n }\n\n return response.json();\n}\n\nexport async function getUser(): Promise<ZoomUser> {\n return zoomFetch<ZoomUser>(\"/users/me\");\n}\n\nexport async function listMeetings(\n options: {\n userId?: string;\n type?: \"scheduled\" | \"live\" | \"upcoming\" | \"upcoming_meetings\" | \"previous_meetings\";\n pageSize?: number;\n pageNumber?: number;\n } = {},\n): Promise<ZoomMeeting[]> {\n const userId = options.userId ?? \"me\";\n const params = new URLSearchParams({\n type: options.type ?? \"scheduled\",\n page_size: String(options.pageSize ?? 30),\n page_number: String(options.pageNumber ?? 1),\n });\n\n const response = await zoomFetch<ZoomMeetingList>(`/users/${userId}/meetings?${params}`);\n return response.meetings;\n}\n\nexport async function getMeeting(meetingId: string | number): Promise<ZoomMeeting> {\n return zoomFetch<ZoomMeeting>(`/meetings/${meetingId}`);\n}\n\nexport async function createMeeting(options: {\n userId?: string;\n topic: string;\n type?: 1 | 2 | 3 | 8; // 1=Instant, 2=Scheduled, 3=Recurring with no fixed time, 8=Recurring with fixed time\n startTime?: string; // ISO 8601 format\n duration?: number; // In minutes\n timezone?: string;\n password?: string;\n agenda?: string;\n settings?: MeetingSettingsInput;\n}): Promise<ZoomMeeting> {\n const userId = options.userId ?? \"me\";\n const body: Record<string, unknown> = {\n topic: options.topic,\n type: options.type ?? 2,\n };\n\n if (options.startTime) body.start_time = options.startTime;\n if (options.duration) body.duration = options.duration;\n if (options.timezone) body.timezone = options.timezone;\n if (options.password) body.password = options.password;\n if (options.agenda) body.agenda = options.agenda;\n\n const settings = toZoomSettings(options.settings);\n if (settings) body.settings = settings;\n\n return zoomFetch<ZoomMeeting>(`/users/${userId}/meetings`, {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n}\n\nexport async function updateMeeting(\n meetingId: string | number,\n updates: {\n topic?: string;\n type?: 1 | 2 | 3 | 8;\n startTime?: string;\n duration?: number;\n timezone?: string;\n password?: string;\n agenda?: string;\n settings?: MeetingSettingsInput;\n },\n): Promise<void> {\n const body: Record<string, unknown> = {};\n\n if (updates.topic !== undefined) body.topic = updates.topic;\n if (updates.type !== undefined) body.type = updates.type;\n if (updates.startTime !== undefined) body.start_time = updates.startTime;\n if (updates.duration !== undefined) body.duration = updates.duration;\n if (updates.timezone !== undefined) body.timezone = updates.timezone;\n if (updates.password !== undefined) body.password = updates.password;\n if (updates.agenda !== undefined) body.agenda = updates.agenda;\n\n const settings = toZoomSettings(updates.settings);\n if (settings) body.settings = settings;\n\n await zoomFetch<void>(`/meetings/${meetingId}`, {\n method: \"PATCH\",\n body: JSON.stringify(body),\n });\n}\n\nexport async function deleteMeeting(\n meetingId: string | number,\n options?: {\n occurrenceId?: string;\n scheduleForReminder?: boolean;\n },\n): Promise<void> {\n const params = new URLSearchParams();\n if (options?.occurrenceId) params.set(\"occurrence_id\", options.occurrenceId);\n if (options?.scheduleForReminder !== undefined) {\n params.set(\"schedule_for_reminder\", String(options.scheduleForReminder));\n }\n\n const queryString = params.toString();\n await zoomFetch<void>(`/meetings/${meetingId}${queryString ? `?${queryString}` : \"\"}`, {\n method: \"DELETE\",\n });\n}\n",
584
- "app/api/auth/zoom/callback/route.ts": "import { createOAuthCallbackHandler, zoomConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(zoomConfig, { tokenStore: hybridTokenStore });\n",
585
- "app/api/auth/zoom/route.ts": "import { createOAuthInitHandler, zoomConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT).\n// NEVER return a shared constant in production - it breaks per-user token isolation (VULN-AUTH-2).\nfunction getUserId(_request: Request): string {\n return \"current-user\";\n}\n\nexport const GET = createOAuthInitHandler(zoomConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});"
586
- }
587
- },
588
461
  "integration:airtable": {
589
462
  "files": {
590
463
  "tools/get-record.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getRecord } from \"../../lib/airtable-client.ts\";\n\nexport default tool({\n id: \"get-record\",\n description:\n \"Get a specific record from an Airtable table by its ID. Returns the full record with all field values.\",\n inputSchema: z.object({\n baseId: z.string().describe('The ID of the Airtable base (starts with \"app\")'),\n tableIdOrName: z.string().describe(\"The ID or name of the table\"),\n recordId: z.string().describe('The ID of the record to retrieve (starts with \"rec\")'),\n }),\n execute: async ({ baseId, tableIdOrName, recordId }) =>\n getRecord(baseId, tableIdOrName, recordId),\n});\n",
@@ -597,19 +470,6 @@ export default {
597
470
  "app/api/auth/airtable/route.ts": "import { airtableConfig, createOAuthInitHandler } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT).\n// NEVER return a shared constant in production - it breaks per-user token isolation (VULN-AUTH-2).\nfunction getUserId(_request: Request): string {\n return \"current-user\";\n}\n\nexport const GET = createOAuthInitHandler(airtableConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});"
598
471
  }
599
472
  },
600
- "integration:quickbooks": {
601
- "files": {
602
- "tools/get-invoice.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getInvoice } from \"../../lib/quickbooks-client.ts\";\n\nexport default tool({\n id: \"get-invoice\",\n description: \"Get details of a specific QuickBooks invoice by its ID.\",\n inputSchema: z.object({\n invoiceId: z.string().describe(\"The ID of the invoice to retrieve\"),\n }),\n async execute({ invoiceId }) {\n const invoice = await getInvoice(invoiceId);\n\n return {\n id: invoice.Id,\n docNumber: invoice.DocNumber,\n txnDate: invoice.TxnDate,\n dueDate: invoice.DueDate,\n totalAmount: invoice.TotalAmt,\n balance: invoice.Balance,\n customer: {\n id: invoice.CustomerRef.value,\n name: invoice.CustomerRef.name,\n },\n status: invoice.TxnStatus,\n emailStatus: invoice.EmailStatus,\n billEmail: invoice.BillEmail?.Address,\n lineItems: invoice.Line.map((line) => {\n const detail = line.SalesItemLineDetail;\n\n return {\n id: line.Id,\n lineNum: line.LineNum,\n description: line.Description,\n amount: line.Amount,\n detailType: line.DetailType,\n salesItemLineDetail: detail\n ? {\n itemName: detail.ItemRef.name,\n quantity: detail.Qty,\n unitPrice: detail.UnitPrice,\n }\n : undefined,\n };\n }),\n metadata: {\n createTime: invoice.MetaData?.CreateTime,\n lastUpdatedTime: invoice.MetaData?.LastUpdatedTime,\n },\n };\n },\n});\n",
603
- "tools/list-invoices.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listInvoices } from \"../../lib/quickbooks-client.ts\";\n\nexport default tool({\n id: \"list-invoices\",\n description: \"List invoices from QuickBooks. Can optionally filter by customer ID.\",\n inputSchema: z.object({\n customerId: z.string().optional().describe(\"Customer ID to filter invoices by\"),\n maxResults: z\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of invoices to return\"),\n }),\n async execute({ customerId, maxResults }) {\n const invoices = await listInvoices({ customerId, maxResults });\n\n return invoices.map((invoice) => {\n const customerRef = invoice.CustomerRef;\n\n return {\n id: invoice.Id,\n docNumber: invoice.DocNumber,\n txnDate: invoice.TxnDate,\n dueDate: invoice.DueDate,\n totalAmount: invoice.TotalAmt,\n balance: invoice.Balance,\n customer: {\n id: customerRef.value,\n name: customerRef.name,\n },\n status: invoice.TxnStatus,\n emailStatus: invoice.EmailStatus,\n lineItems: invoice.Line.map((line) => {\n const detail = line.SalesItemLineDetail;\n\n return {\n description: line.Description,\n amount: line.Amount,\n quantity: detail?.Qty,\n unitPrice: detail?.UnitPrice,\n };\n }),\n };\n });\n },\n});\n",
604
- "tools/create-invoice.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createInvoice } from \"../../lib/quickbooks-client.ts\";\n\nexport default tool({\n id: \"create-invoice\",\n description: \"Create a new invoice in QuickBooks.\",\n inputSchema: z.object({\n customerId: z.string().describe(\"The ID of the customer to invoice\"),\n lineItems: z\n .array(\n z.object({\n description: z.string().optional().describe(\"Description of the line item\"),\n amount: z.number().describe(\"Total amount for this line item\"),\n itemId: z.string().optional().describe(\"QuickBooks item/service ID\"),\n quantity: z.number().optional().describe(\"Quantity of items\"),\n unitPrice: z.number().optional().describe(\"Price per unit\"),\n }),\n )\n .describe(\"Line items for the invoice\"),\n txnDate: z.string().optional().describe(\"Transaction date in YYYY-MM-DD format\"),\n dueDate: z.string().optional().describe(\"Due date in YYYY-MM-DD format\"),\n customerMemo: z.string().optional().describe(\"Memo/note for the customer\"),\n }),\n async execute({ customerId, lineItems, txnDate, dueDate, customerMemo }) {\n const invoice = await createInvoice({\n customerId,\n lineItems,\n txnDate,\n dueDate,\n customerMemo,\n });\n\n return {\n success: true,\n invoice: {\n id: invoice.Id,\n docNumber: invoice.DocNumber,\n txnDate: invoice.TxnDate,\n dueDate: invoice.DueDate,\n totalAmount: invoice.TotalAmt,\n balance: invoice.Balance,\n customer: {\n id: invoice.CustomerRef.value,\n name: invoice.CustomerRef.name,\n },\n },\n };\n },\n});\n",
605
- "tools/list-customers.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listCustomers } from \"../../lib/quickbooks-client.ts\";\n\nexport default tool({\n id: \"list-customers\",\n description:\n \"List customers from QuickBooks. Can optionally filter by active status.\",\n inputSchema: z.object({\n active: z\n .boolean()\n .optional()\n .describe(\n \"Filter by active status (true for active, false for inactive)\",\n ),\n maxResults: z\n .number()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Maximum number of customers to return\"),\n }),\n async execute({ active, maxResults }) {\n const customers = await listCustomers({ active, maxResults });\n\n return customers.map((customer) => {\n const billAddr = customer.BillAddr;\n\n const address = billAddr\n ? {\n line1: billAddr.Line1,\n city: billAddr.City,\n state: billAddr.CountrySubDivisionCode,\n postalCode: billAddr.PostalCode,\n }\n : undefined;\n\n return {\n id: customer.Id,\n displayName: customer.DisplayName,\n companyName: customer.CompanyName,\n givenName: customer.GivenName,\n familyName: customer.FamilyName,\n email: customer.PrimaryEmailAddr?.Address,\n phone: customer.PrimaryPhone?.FreeFormNumber,\n address,\n balance: customer.Balance,\n active: customer.Active,\n };\n });\n },\n});\n",
606
- "tools/get-customer.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getCustomer } from \"../../lib/quickbooks-client.ts\";\n\nexport default tool({\n id: \"get-customer\",\n description: \"Get details of a specific QuickBooks customer by their ID.\",\n inputSchema: z.object({\n customerId: z.string().describe(\"The ID of the customer to retrieve\"),\n }),\n async execute({ customerId }) {\n const customer = await getCustomer(customerId);\n const billAddr = customer.BillAddr;\n\n return {\n id: customer.Id,\n displayName: customer.DisplayName,\n companyName: customer.CompanyName,\n givenName: customer.GivenName,\n familyName: customer.FamilyName,\n email: customer.PrimaryEmailAddr?.Address,\n phone: customer.PrimaryPhone?.FreeFormNumber,\n address: billAddr\n ? {\n line1: billAddr.Line1,\n city: billAddr.City,\n state: billAddr.CountrySubDivisionCode,\n postalCode: billAddr.PostalCode,\n }\n : undefined,\n balance: customer.Balance,\n active: customer.Active,\n metadata: {\n createTime: customer.MetaData?.CreateTime,\n lastUpdatedTime: customer.MetaData?.LastUpdatedTime,\n },\n };\n },\n});\n",
607
- ".env.example": "# QuickBooks OAuth Configuration\n# Get your credentials from https://developer.intuit.com/app/developer/myapps\nQUICKBOOKS_CLIENT_ID=your-client-id\nQUICKBOOKS_CLIENT_SECRET=your-client-secret\n",
608
- "lib/quickbooks-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst QUICKBOOKS_BASE_URL = \"https://quickbooks.api.intuit.com/v3\";\n\nfunction getRealmId(): string {\n const realmId = process.env.QUICKBOOKS_REALM_ID;\n if (!realmId) {\n throw new Error(\"QUICKBOOKS_REALM_ID environment variable is required\");\n }\n return realmId;\n}\n\ninterface QuickBooksResponse<T> {\n QueryResponse?: {\n [key: string]: T[] | number | undefined;\n maxResults?: number;\n startPosition?: number;\n };\n Invoice?: T;\n Customer?: T;\n time?: string;\n}\n\ninterface QuickBooksInvoice {\n Id: string;\n DocNumber: string;\n TxnDate: string;\n DueDate?: string;\n TotalAmt: number;\n Balance: number;\n CustomerRef: {\n value: string;\n name: string;\n };\n Line: Array<{\n Id: string;\n LineNum: number;\n Description?: string;\n Amount: number;\n DetailType: string;\n SalesItemLineDetail?: {\n ItemRef: {\n value: string;\n name: string;\n };\n Qty?: number;\n UnitPrice?: number;\n };\n }>;\n EmailStatus?: string;\n BillEmail?: {\n Address: string;\n };\n TxnStatus?: string;\n MetaData?: {\n CreateTime: string;\n LastUpdatedTime: string;\n };\n}\n\ninterface QuickBooksCustomer {\n Id: string;\n DisplayName: string;\n CompanyName?: string;\n GivenName?: string;\n FamilyName?: string;\n PrimaryEmailAddr?: {\n Address: string;\n };\n PrimaryPhone?: {\n FreeFormNumber: string;\n };\n BillAddr?: {\n Line1?: string;\n City?: string;\n CountrySubDivisionCode?: string;\n PostalCode?: string;\n };\n Balance: number;\n Active: boolean;\n MetaData?: {\n CreateTime: string;\n LastUpdatedTime: string;\n };\n}\n\nasync function quickbooksFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with QuickBooks. Please connect your account.\");\n }\n\n const url = `${QUICKBOOKS_BASE_URL}/company/${getRealmId()}${endpoint}`;\n\n const response = await fetch(url, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n const message = error.Fault?.Error?.[0]?.Message ?? response.statusText;\n throw new Error(`QuickBooks API error: ${response.status} ${message}`);\n }\n\n return response.json();\n}\n\nexport async function listInvoices(options?: {\n customerId?: string;\n maxResults?: number;\n}): Promise<QuickBooksInvoice[]> {\n const maxResults = options?.maxResults ?? 100;\n\n if (options?.customerId && !/^[a-zA-Z0-9_\\-:.]+$/.test(options.customerId)) {\n throw new Error('Invalid customerId: must contain only alphanumeric characters, underscores, hyphens, colons, or periods');\n }\n\n let query = `SELECT * FROM Invoice MAXRESULTS ${maxResults}`;\n if (options?.customerId) {\n query = `SELECT * FROM Invoice WHERE CustomerRef = '${options.customerId}' MAXRESULTS ${maxResults}`;\n }\n\n const response = await quickbooksFetch<QuickBooksResponse<QuickBooksInvoice>>(\n `/query?query=${encodeURIComponent(query)}`,\n );\n\n return response.QueryResponse?.Invoice ?? [];\n}\n\nexport async function getInvoice(invoiceId: string): Promise<QuickBooksInvoice> {\n const response = await quickbooksFetch<QuickBooksResponse<QuickBooksInvoice>>(\n `/invoice/${invoiceId}`,\n );\n\n const invoice = response.Invoice;\n if (!invoice) {\n throw new Error(`Invoice ${invoiceId} not found`);\n }\n\n return invoice;\n}\n\nexport async function createInvoice(options: {\n customerId: string;\n lineItems: Array<{\n description?: string;\n amount: number;\n itemId?: string;\n quantity?: number;\n unitPrice?: number;\n }>;\n txnDate?: string;\n dueDate?: string;\n customerMemo?: string;\n}): Promise<QuickBooksInvoice> {\n const lines = options.lineItems.map((item, index) => {\n const line: Record<string, unknown> = {\n LineNum: index + 1,\n Amount: item.amount,\n DetailType: \"SalesItemLineDetail\",\n };\n\n if (item.description) {\n line.Description = item.description;\n }\n\n if (item.itemId) {\n line.SalesItemLineDetail = {\n ItemRef: { value: item.itemId },\n Qty: item.quantity ?? 1,\n UnitPrice: item.unitPrice ?? item.amount,\n };\n }\n\n return line;\n });\n\n const invoiceData: Record<string, unknown> = {\n CustomerRef: { value: options.customerId },\n Line: lines,\n };\n\n if (options.txnDate) invoiceData.TxnDate = options.txnDate;\n if (options.dueDate) invoiceData.DueDate = options.dueDate;\n if (options.customerMemo) invoiceData.CustomerMemo = { value: options.customerMemo };\n\n const response = await quickbooksFetch<QuickBooksResponse<QuickBooksInvoice>>(\"/invoice\", {\n method: \"POST\",\n body: JSON.stringify(invoiceData),\n });\n\n const invoice = response.Invoice;\n if (!invoice) {\n throw new Error(\"Failed to create invoice\");\n }\n\n return invoice;\n}\n\nexport async function listCustomers(options?: {\n maxResults?: number;\n active?: boolean;\n}): Promise<QuickBooksCustomer[]> {\n const maxResults = options?.maxResults ?? 100;\n\n let query = `SELECT * FROM Customer MAXRESULTS ${maxResults}`;\n if (options?.active !== undefined) {\n query = `SELECT * FROM Customer WHERE Active = ${options.active} MAXRESULTS ${maxResults}`;\n }\n\n const response = await quickbooksFetch<QuickBooksResponse<QuickBooksCustomer>>(\n `/query?query=${encodeURIComponent(query)}`,\n );\n\n return response.QueryResponse?.Customer ?? [];\n}\n\nexport async function getCustomer(customerId: string): Promise<QuickBooksCustomer> {\n const response = await quickbooksFetch<QuickBooksResponse<QuickBooksCustomer>>(\n `/customer/${customerId}`,\n );\n\n const customer = response.Customer;\n if (!customer) {\n throw new Error(`Customer ${customerId} not found`);\n }\n\n return customer;\n}\n",
609
- "app/api/auth/quickbooks/callback/route.ts": "import { createOAuthCallbackHandler, quickbooksConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(quickbooksConfig, { tokenStore: hybridTokenStore });\n",
610
- "app/api/auth/quickbooks/route.ts": "import { createOAuthInitHandler, quickbooksConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT).\n// NEVER return a shared constant in production - it breaks per-user token isolation (VULN-AUTH-2).\nfunction getUserId(_request: Request): string {\n return \"current-user\";\n}\n\nexport const GET = createOAuthInitHandler(quickbooksConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});"
611
- }
612
- },
613
473
  "integration:outlook": {
614
474
  "files": {
615
475
  "tools/get-email.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getEmail } from \"../../lib/outlook-client.ts\";\n\nexport default tool({\n id: \"get-email\",\n description:\n \"Get detailed information about a specific email, including full body content, recipients, and metadata.\",\n inputSchema: z.object({\n messageId: z.string().describe(\"The ID of the email message to retrieve\"),\n includeBody: z\n .boolean()\n .default(true)\n .describe(\"Include full email body content\"),\n }),\n async execute({ messageId, includeBody }) {\n const message = await getEmail(messageId);\n\n const body = includeBody\n ? {\n contentType: message.body.contentType,\n content: message.body.content,\n }\n : undefined;\n\n return {\n id: message.id,\n subject: message.subject,\n from: {\n name: message.from.emailAddress.name,\n email: message.from.emailAddress.address,\n },\n to: message.toRecipients.map(({ emailAddress }) => ({\n name: emailAddress.name,\n email: emailAddress.address,\n })),\n cc: message.ccRecipients?.map(({ emailAddress }) => ({\n name: emailAddress.name,\n email: emailAddress.address,\n })),\n body,\n bodyPreview: message.bodyPreview,\n receivedAt: message.receivedDateTime,\n sentAt: message.sentDateTime,\n isRead: message.isRead,\n hasAttachments: message.hasAttachments,\n importance: message.importance,\n conversationId: message.conversationId,\n webLink: message.webLink,\n };\n },\n});\n",
@@ -657,19 +517,6 @@ export default {
657
517
  "lib/twilio-client.ts": "import { getTwilioCredentials } from \"./token-store.ts\";\n\nconst TWILIO_API_VERSION = \"2010-04-01\";\n\nexport interface TwilioMessage {\n sid: string;\n account_sid: string;\n from: string;\n to: string;\n body: string;\n status: \"queued\" | \"sending\" | \"sent\" | \"failed\" | \"delivered\" | \"undelivered\" | \"receiving\" | \"received\";\n direction: \"inbound\" | \"outbound-api\" | \"outbound-call\" | \"outbound-reply\";\n date_created: string;\n date_updated: string;\n date_sent: string | null;\n price: string | null;\n price_unit: string | null;\n error_code: number | null;\n error_message: string | null;\n uri: string;\n num_segments: string;\n num_media: string;\n messaging_service_sid: string | null;\n}\n\nexport interface TwilioCall {\n sid: string;\n account_sid: string;\n from: string;\n to: string;\n status: \"queued\" | \"ringing\" | \"in-progress\" | \"completed\" | \"busy\" | \"failed\" | \"no-answer\" | \"canceled\";\n direction: \"inbound\" | \"outbound-api\" | \"outbound-dial\";\n date_created: string;\n date_updated: string;\n start_time: string | null;\n end_time: string | null;\n duration: string | null;\n price: string | null;\n price_unit: string | null;\n uri: string;\n answered_by: string | null;\n}\n\nexport interface TwilioListResponse<T> {\n messages?: T[];\n calls?: T[];\n first_page_uri: string;\n next_page_uri: string | null;\n previous_page_uri: string | null;\n uri: string;\n page: number;\n page_size: number;\n}\n\ninterface TwilioErrorResponse {\n code: number;\n message: string;\n more_info: string;\n status: number;\n}\n\nfunction buildParams(params: Record<string, string | number>): string {\n return new URLSearchParams(\n Object.entries(params).map(([key, value]) => [key, String(value)]),\n ).toString();\n}\n\nfunction addMediaUrls(params: Record<string, string>, mediaUrl?: string[]): void {\n if (!mediaUrl?.length) return;\n\n for (const [index, url] of mediaUrl.entries()) {\n params[`MediaUrl[${index}]`] = url;\n }\n}\n\nfunction ensureTwilioCredentials(): NonNullable<ReturnType<typeof getTwilioCredentials>> {\n const credentials = getTwilioCredentials();\n if (!credentials) throw new Error(\"Twilio credentials not configured\");\n return credentials;\n}\n\nasync function twilioFetch<T>(\n endpoint: string,\n options: RequestInit & { params?: Record<string, string | number> } = {},\n): Promise<T> {\n const credentials = getTwilioCredentials();\n if (!credentials) {\n throw new Error(\n \"Twilio not configured. Please set TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_PHONE_NUMBER environment variables.\",\n );\n }\n\n const { accountSid, authToken } = credentials;\n const baseUrl = `https://api.twilio.com/${TWILIO_API_VERSION}/Accounts/${accountSid}`;\n let url = `${baseUrl}${endpoint}`;\n\n const headers: Record<string, string> = {\n Authorization: `Basic ${btoa(`${accountSid}:${authToken}`)}`,\n };\n\n let body: string | undefined;\n const encodedParams = options.params ? buildParams(options.params) : undefined;\n\n if (encodedParams) {\n if (options.method === \"POST\") {\n body = encodedParams;\n headers[\"Content-Type\"] = \"application/x-www-form-urlencoded\";\n } else {\n url += `?${encodedParams}`;\n }\n }\n\n const response = await fetch(url, { ...options, headers, body });\n const data: unknown = await response.json();\n\n if (!response.ok) {\n const error = data as TwilioErrorResponse;\n throw new Error(`Twilio API error (${error.code}): ${error.message}\\nMore info: ${error.more_info}`);\n }\n\n return data as T;\n}\n\nexport async function sendSMS(\n to: string,\n body: string,\n options?: {\n mediaUrl?: string[];\n statusCallback?: string;\n },\n): Promise<TwilioMessage> {\n const { phoneNumber } = ensureTwilioCredentials();\n\n const params: Record<string, string> = {\n To: to,\n From: phoneNumber,\n Body: body,\n };\n\n addMediaUrls(params, options?.mediaUrl);\n\n if (options?.statusCallback) params.StatusCallback = options.statusCallback;\n\n return twilioFetch<TwilioMessage>(\"/Messages.json\", { method: \"POST\", params });\n}\n\nexport async function sendWhatsApp(\n to: string,\n body: string,\n options?: {\n mediaUrl?: string[];\n statusCallback?: string;\n },\n): Promise<TwilioMessage> {\n const { phoneNumber } = ensureTwilioCredentials();\n\n const whatsappTo = to.startsWith(\"whatsapp:\") ? to : `whatsapp:${to}`;\n const whatsappFrom = phoneNumber.startsWith(\"whatsapp:\") ? phoneNumber : `whatsapp:${phoneNumber}`;\n\n const params: Record<string, string> = {\n To: whatsappTo,\n From: whatsappFrom,\n Body: body,\n };\n\n addMediaUrls(params, options?.mediaUrl);\n\n if (options?.statusCallback) params.StatusCallback = options.statusCallback;\n\n return twilioFetch<TwilioMessage>(\"/Messages.json\", { method: \"POST\", params });\n}\n\nexport async function listMessages(options?: {\n to?: string;\n from?: string;\n dateSent?: string;\n limit?: number;\n}): Promise<TwilioMessage[]> {\n const params: Record<string, string | number> = {};\n\n if (options?.to) params.To = options.to;\n if (options?.from) params.From = options.from;\n if (options?.dateSent) params.DateSent = options.dateSent;\n if (options?.limit) params.PageSize = options.limit;\n\n const response = await twilioFetch<TwilioListResponse<TwilioMessage>>(\"/Messages.json\", { params });\n return response.messages ?? [];\n}\n\nexport function getMessage(messageSid: string): Promise<TwilioMessage> {\n return twilioFetch<TwilioMessage>(`/Messages/${messageSid}.json`);\n}\n\nexport async function listCalls(options?: {\n to?: string;\n from?: string;\n status?: \"queued\" | \"ringing\" | \"in-progress\" | \"completed\" | \"busy\" | \"failed\" | \"no-answer\" | \"canceled\";\n startTime?: string;\n limit?: number;\n}): Promise<TwilioCall[]> {\n const params: Record<string, string | number> = {};\n\n if (options?.to) params.To = options.to;\n if (options?.from) params.From = options.from;\n if (options?.status) params.Status = options.status;\n if (options?.startTime) params.StartTime = options.startTime;\n if (options?.limit) params.PageSize = options.limit;\n\n const response = await twilioFetch<TwilioListResponse<TwilioCall>>(\"/Calls.json\", { params });\n return response.calls ?? [];\n}\n\nexport function getCall(callSid: string): Promise<TwilioCall> {\n return twilioFetch<TwilioCall>(`/Calls/${callSid}.json`);\n}\n\nexport async function makeCall(\n to: string,\n twiml: string,\n options?: {\n twimlUrl?: string;\n statusCallback?: string;\n statusCallbackMethod?: \"GET\" | \"POST\";\n timeout?: number;\n },\n): Promise<TwilioCall> {\n const { phoneNumber } = ensureTwilioCredentials();\n\n const params: Record<string, string | number> = {\n To: to,\n From: phoneNumber,\n };\n\n if (options?.twimlUrl) {\n params.Url = options.twimlUrl;\n } else {\n params.Twiml = twiml;\n }\n\n if (options?.statusCallback) params.StatusCallback = options.statusCallback;\n if (options?.statusCallbackMethod) params.StatusCallbackMethod = options.statusCallbackMethod;\n if (options?.timeout) params.Timeout = options.timeout;\n\n return twilioFetch<TwilioCall>(\"/Calls.json\", { method: \"POST\", params });\n}\n\nexport function formatPhoneNumber(phone: string, defaultCountryCode = \"+1\"): string {\n const digits = phone.replace(/\\D/g, \"\");\n\n if (phone.startsWith(\"+\")) return phone;\n if (digits.length === 10) return `${defaultCountryCode}${digits}`;\n if (digits.length === 11 && digits.startsWith(\"1\")) return `+${digits}`;\n return `+${digits}`;\n}\n\nexport function formatDate(date: Date): string {\n return date.toISOString().split(\"T\")[0];\n}\n\nexport function parseDate(dateString: string): Date {\n return new Date(dateString);\n}\n"
658
518
  }
659
519
  },
660
- "integration:freshdesk": {
661
- "files": {
662
- "tools/get-ticket.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getTicket, TicketPriority, TicketStatus } from \"../../lib/freshdesk-client.ts\";\n\nconst statusMap: Record<number, string> = {\n [TicketStatus.OPEN]: \"open\",\n [TicketStatus.PENDING]: \"pending\",\n [TicketStatus.RESOLVED]: \"resolved\",\n [TicketStatus.CLOSED]: \"closed\",\n};\n\nconst priorityMap: Record<number, string> = {\n [TicketPriority.LOW]: \"low\",\n [TicketPriority.MEDIUM]: \"medium\",\n [TicketPriority.HIGH]: \"high\",\n [TicketPriority.URGENT]: \"urgent\",\n};\n\nexport default tool({\n id: \"get-ticket\",\n description: \"Get details of a specific Freshdesk support ticket by its ID.\",\n inputSchema: z.object({\n ticketId: z.number().describe(\"The ID of the ticket to retrieve\"),\n }),\n async execute({ ticketId }) {\n const ticket = await getTicket(ticketId);\n\n return {\n id: ticket.id,\n subject: ticket.subject,\n description: ticket.description,\n descriptionText: ticket.description_text,\n status: statusMap[ticket.status] ?? \"unknown\",\n priority: priorityMap[ticket.priority] ?? \"unknown\",\n type: ticket.type,\n requesterId: ticket.requester_id,\n responderId: ticket.responder_id,\n dueBy: ticket.due_by,\n firstResponseDueBy: ticket.fr_due_by,\n createdAt: ticket.created_at,\n updatedAt: ticket.updated_at,\n tags: ticket.tags,\n customFields: ticket.custom_fields,\n };\n },\n});\n",
663
- "tools/list-tickets.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listTickets, TicketPriority, TicketStatus } from \"../../lib/freshdesk-client.ts\";\n\nconst statusMap = {\n open: TicketStatus.OPEN,\n pending: TicketStatus.PENDING,\n resolved: TicketStatus.RESOLVED,\n closed: TicketStatus.CLOSED,\n} as const;\n\nconst priorityMap = {\n low: TicketPriority.LOW,\n medium: TicketPriority.MEDIUM,\n high: TicketPriority.HIGH,\n urgent: TicketPriority.URGENT,\n} as const;\n\nfunction getKeyByValue<T extends Record<string, unknown>>(map: T, value: T[keyof T]): string {\n for (const [key, mapValue] of Object.entries(map)) {\n if (mapValue === value) return key;\n }\n return \"unknown\";\n}\n\nexport default tool({\n id: \"list-tickets\",\n description: \"List support tickets from Freshdesk. Can filter by status, priority, and type.\",\n inputSchema: z.object({\n status: z.enum([\"open\", \"pending\", \"resolved\", \"closed\"]).optional().describe(\"Filter by ticket status\"),\n priority: z.enum([\"low\", \"medium\", \"high\", \"urgent\"]).optional().describe(\"Filter by ticket priority\"),\n type: z\n .string()\n .optional()\n .describe(\"Filter by ticket type (e.g., 'Question', 'Incident', 'Problem', 'Feature Request')\"),\n limit: z.number().min(1).max(100).default(30).describe(\"Maximum number of tickets to return\"),\n }),\n async execute({ status, priority, type, limit }) {\n const tickets = await listTickets({\n status: status ? statusMap[status] : undefined,\n priority: priority ? priorityMap[priority] : undefined,\n type,\n perPage: limit,\n });\n\n return tickets.map((ticket) => ({\n id: ticket.id,\n subject: ticket.subject,\n description: ticket.description_text,\n status: getKeyByValue(statusMap, ticket.status),\n priority: getKeyByValue(priorityMap, ticket.priority),\n type: ticket.type,\n dueBy: ticket.due_by,\n createdAt: ticket.created_at,\n updatedAt: ticket.updated_at,\n tags: ticket.tags,\n }));\n },\n});\n",
664
- "tools/create-ticket.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createTicket, TicketPriority, TicketStatus } from \"../../lib/freshdesk-client.ts\";\n\nconst priorityMap = {\n low: TicketPriority.LOW,\n medium: TicketPriority.MEDIUM,\n high: TicketPriority.HIGH,\n urgent: TicketPriority.URGENT,\n} as const;\n\nconst statusMap = {\n open: TicketStatus.OPEN,\n pending: TicketStatus.PENDING,\n} as const;\n\nfunction getKeyByValue<T extends Record<string, unknown>>(map: T, value: T[keyof T]): string {\n for (const [key, mapValue] of Object.entries(map)) {\n if (mapValue === value) return key;\n }\n return \"unknown\";\n}\n\nexport default tool({\n id: \"create-ticket\",\n description: \"Create a new support ticket in Freshdesk.\",\n inputSchema: z.object({\n subject: z.string().describe(\"The subject/title of the ticket\"),\n description: z.string().describe(\"Description or details of the ticket\"),\n email: z.string().email().describe(\"Email address of the requester\"),\n priority: z\n .enum([\"low\", \"medium\", \"high\", \"urgent\"])\n .default(\"medium\")\n .describe(\"Priority level of the ticket\"),\n status: z.enum([\"open\", \"pending\"]).default(\"open\").describe(\"Initial status of the ticket\"),\n type: z\n .string()\n .optional()\n .describe(\"Type of ticket (e.g., 'Question', 'Incident', 'Problem', 'Feature Request')\"),\n tags: z.array(z.string()).optional().describe(\"Tags to add to the ticket\"),\n }),\n async execute({ subject, description, email, priority, status, type, tags }) {\n const ticket = await createTicket({\n subject,\n description,\n email,\n priority: priorityMap[priority],\n status: statusMap[status],\n type,\n tags,\n });\n\n return {\n success: true,\n ticket: {\n id: ticket.id,\n subject: ticket.subject,\n status: getKeyByValue(statusMap, ticket.status),\n priority: getKeyByValue(priorityMap, ticket.priority),\n createdAt: ticket.created_at,\n },\n };\n },\n});\n",
665
- "tools/update-ticket.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { updateTicket, TicketPriority, TicketStatus } from \"../../lib/freshdesk-client.ts\";\n\nconst priorityMap = {\n low: TicketPriority.LOW,\n medium: TicketPriority.MEDIUM,\n high: TicketPriority.HIGH,\n urgent: TicketPriority.URGENT,\n} as const;\n\nconst statusMap = {\n open: TicketStatus.OPEN,\n pending: TicketStatus.PENDING,\n resolved: TicketStatus.RESOLVED,\n closed: TicketStatus.CLOSED,\n} as const;\n\nfunction getKeyByValue<T extends Record<string, unknown>>(map: T, value: T[keyof T]): string {\n for (const [key, mapValue] of Object.entries(map)) {\n if (mapValue === value) return key;\n }\n return \"unknown\";\n}\n\nexport default tool({\n id: \"update-ticket\",\n description: \"Update an existing Freshdesk support ticket.\",\n inputSchema: z.object({\n ticketId: z.number().describe(\"The ID of the ticket to update\"),\n subject: z.string().optional().describe(\"New subject/title for the ticket\"),\n description: z.string().optional().describe(\"New description or details\"),\n status: z.enum([\"open\", \"pending\", \"resolved\", \"closed\"]).optional().describe(\"New status for the ticket\"),\n priority: z.enum([\"low\", \"medium\", \"high\", \"urgent\"]).optional().describe(\"New priority level\"),\n type: z\n .string()\n .optional()\n .describe(\"New type of ticket (e.g., 'Question', 'Incident', 'Problem', 'Feature Request')\"),\n tags: z.array(z.string()).optional().describe(\"New tags for the ticket (replaces existing tags)\"),\n }),\n async execute({ ticketId, subject, description, status, priority, type, tags }) {\n const ticket = await updateTicket(ticketId, {\n subject,\n description,\n status: status ? statusMap[status] : undefined,\n priority: priority ? priorityMap[priority] : undefined,\n type,\n tags,\n });\n\n return {\n success: true,\n ticket: {\n id: ticket.id,\n subject: ticket.subject,\n status: getKeyByValue(statusMap, ticket.status),\n priority: getKeyByValue(priorityMap, ticket.priority),\n updatedAt: ticket.updated_at,\n },\n };\n },\n});\n",
666
- "tools/list-contacts.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listContacts } from \"../../lib/freshdesk-client.ts\";\n\nexport default tool({\n id: \"list-contacts\",\n description:\n \"List customer contacts from Freshdesk. Can filter by email, phone, mobile, or company ID.\",\n inputSchema: z.object({\n email: z.string().optional().describe(\"Filter by contact email address\"),\n phone: z.string().optional().describe(\"Filter by contact phone number\"),\n mobile: z.string().optional().describe(\"Filter by contact mobile number\"),\n companyId: z.number().optional().describe(\"Filter by company ID\"),\n limit: z\n .number()\n .min(1)\n .max(100)\n .default(30)\n .describe(\"Maximum number of contacts to return\"),\n }),\n async execute({ email, phone, mobile, companyId, limit }) {\n const contacts = await listContacts({\n email,\n phone,\n mobile,\n companyId,\n perPage: limit,\n });\n\n return contacts.map((contact) => ({\n id: contact.id,\n name: contact.name,\n email: contact.email,\n phone: contact.phone,\n mobile: contact.mobile,\n companyId: contact.company_id,\n createdAt: contact.created_at,\n updatedAt: contact.updated_at,\n tags: contact.tags,\n }));\n },\n});\n",
667
- ".env.example": "# Freshdesk OAuth Configuration\n# Get your credentials from https://developers.freshdesk.com/apps/\nFRESHDESK_CLIENT_ID=your-client-id\nFRESHDESK_CLIENT_SECRET=your-client-secret\n",
668
- "lib/freshdesk-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst FRESHDESK_BASE_URL = \"https://domain.freshdesk.com/api/v2\";\n\ninterface FreshdeskTicket {\n id: number;\n subject: string;\n description: string;\n description_text: string;\n status: number;\n priority: number;\n type: string;\n requester_id: number;\n responder_id: number | null;\n due_by: string;\n fr_due_by: string;\n created_at: string;\n updated_at: string;\n tags: string[];\n custom_fields: Record<string, unknown>;\n}\n\ninterface FreshdeskContact {\n id: number;\n name: string;\n email: string;\n phone: string | null;\n mobile: string | null;\n company_id: number | null;\n created_at: string;\n updated_at: string;\n tags: string[];\n custom_fields: Record<string, unknown>;\n}\n\nasync function freshdeskFetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Freshdesk. Please connect your account.\");\n }\n\n const response = await fetch(`${FRESHDESK_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const error = (await response.json().catch(() => ({}))) as { description?: string };\n throw new Error(\n `Freshdesk API error: ${response.status} ${error.description ?? response.statusText}`,\n );\n }\n\n return response.json() as Promise<T>;\n}\n\nfunction buildEndpoint(path: string, params: URLSearchParams): string {\n const queryString = params.toString();\n if (!queryString) return path;\n return `${path}?${queryString}`;\n}\n\nexport async function listTickets(\n options: {\n status?: number;\n priority?: number;\n type?: string;\n page?: number;\n perPage?: number;\n } = {},\n): Promise<FreshdeskTicket[]> {\n const params = new URLSearchParams();\n\n if (options.status !== undefined) params.set(\"status\", String(options.status));\n if (options.priority !== undefined) params.set(\"priority\", String(options.priority));\n if (options.type) params.set(\"type\", options.type);\n if (options.page) params.set(\"page\", String(options.page));\n if (options.perPage) params.set(\"per_page\", String(options.perPage));\n\n return freshdeskFetch<FreshdeskTicket[]>(buildEndpoint(\"/tickets\", params));\n}\n\nexport async function getTicket(ticketId: number): Promise<FreshdeskTicket> {\n return freshdeskFetch<FreshdeskTicket>(`/tickets/${ticketId}`);\n}\n\nexport async function createTicket(options: {\n subject: string;\n description: string;\n email: string;\n priority?: number;\n status?: number;\n type?: string;\n tags?: string[];\n}): Promise<FreshdeskTicket> {\n const body: Record<string, unknown> = {\n subject: options.subject,\n description: options.description,\n email: options.email,\n priority: options.priority ?? 1,\n status: options.status ?? 2,\n };\n\n if (options.type) body.type = options.type;\n if (options.tags) body.tags = options.tags;\n\n return freshdeskFetch<FreshdeskTicket>(\"/tickets\", {\n method: \"POST\",\n body: JSON.stringify(body),\n });\n}\n\nexport async function updateTicket(\n ticketId: number,\n updates: {\n subject?: string;\n description?: string;\n status?: number;\n priority?: number;\n type?: string;\n tags?: string[];\n },\n): Promise<FreshdeskTicket> {\n const body: Record<string, unknown> = {};\n\n if (updates.subject !== undefined) body.subject = updates.subject;\n if (updates.description !== undefined) body.description = updates.description;\n if (updates.status !== undefined) body.status = updates.status;\n if (updates.priority !== undefined) body.priority = updates.priority;\n if (updates.type !== undefined) body.type = updates.type;\n if (updates.tags !== undefined) body.tags = updates.tags;\n\n return freshdeskFetch<FreshdeskTicket>(`/tickets/${ticketId}`, {\n method: \"PUT\",\n body: JSON.stringify(body),\n });\n}\n\nexport async function listContacts(\n options: {\n email?: string;\n mobile?: string;\n phone?: string;\n companyId?: number;\n page?: number;\n perPage?: number;\n } = {},\n): Promise<FreshdeskContact[]> {\n const params = new URLSearchParams();\n\n if (options.email) params.set(\"email\", options.email);\n if (options.mobile) params.set(\"mobile\", options.mobile);\n if (options.phone) params.set(\"phone\", options.phone);\n if (options.companyId) params.set(\"company_id\", String(options.companyId));\n if (options.page) params.set(\"page\", String(options.page));\n if (options.perPage) params.set(\"per_page\", String(options.perPage));\n\n return freshdeskFetch<FreshdeskContact[]>(buildEndpoint(\"/contacts\", params));\n}\n\nexport const TicketStatus = {\n OPEN: 2,\n PENDING: 3,\n RESOLVED: 4,\n CLOSED: 5,\n} as const;\n\nexport const TicketPriority = {\n LOW: 1,\n MEDIUM: 2,\n HIGH: 3,\n URGENT: 4,\n} as const;\n",
669
- "app/api/auth/freshdesk/callback/route.ts": "/**\n * Freshdesk OAuth Callback\n *\n * Handles the OAuth callback from Freshdesk and stores the tokens.\n */\n\nimport { createOAuthCallbackHandler, freshdeskConfig } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\ntype Tokens = { accessToken: string; refreshToken?: string; expiresAt?: number };\ntype StateMeta = {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n};\n\n// Hybrid adapter: uses framework's oauthMemoryTokenStore for state (PKCE),\n// but user's tokenStore for actual token storage. Tokens are keyed by\n// (serviceId, userId) — NEVER share a single slot across users.\nconst hybridTokenStore = {\n getTokens(serviceId: string, userId: string): Promise<Tokens | null> {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(serviceId: string, userId: string, tokens: Tokens): Promise<void> {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string): Promise<void> {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(state: string, meta: StateMeta): ReturnType<typeof oauthMemoryTokenStore.setState> {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string): ReturnType<typeof oauthMemoryTokenStore.consumeState> {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(freshdeskConfig, { tokenStore: hybridTokenStore });\n",
670
- "app/api/auth/freshdesk/route.ts": "import { createOAuthInitHandler, freshdeskConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT).\n// NEVER return a shared constant in production - it breaks per-user token isolation (VULN-AUTH-2).\nfunction getUserId(_request: Request): string {\n return \"current-user\";\n}\n\nexport const GET = createOAuthInitHandler(freshdeskConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});"
671
- }
672
- },
673
520
  "integration:teams": {
674
521
  "files": {
675
522
  "tools/get-messages.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getChatMessages, getPlainTextContent } from \"../../lib/teams-client.ts\";\n\nexport default tool({\n id: \"get-messages\",\n description:\n \"Get messages from a specific Microsoft Teams chat. Returns message content, sender information, and timestamps. Use list-chats first to get chat IDs.\",\n inputSchema: z.object({\n chatId: z.string().describe(\"The ID of the chat to get messages from\"),\n limit: z\n .number()\n .min(1)\n .max(50)\n .default(20)\n .describe(\"Maximum number of messages to return (1-50)\"),\n includeHtml: z\n .boolean()\n .default(false)\n .describe(\"Include HTML formatted content in addition to plain text\"),\n }),\n async execute({ chatId, limit, includeHtml }) {\n const messages = await getChatMessages(chatId, {\n limit,\n orderBy: \"createdDateTime desc\",\n });\n\n return messages\n .filter((msg) => msg.messageType === \"message\")\n .map((msg) => {\n const attachments = msg.attachments ?? [];\n const mentions = msg.mentions ?? [];\n const reactions = msg.reactions ?? [];\n\n return {\n id: msg.id,\n content: getPlainTextContent(msg),\n htmlContent: includeHtml ? msg.body.content : undefined,\n contentType: msg.body.contentType,\n sender: {\n id: msg.from.user?.id,\n displayName: msg.from.user?.displayName,\n },\n createdAt: msg.createdDateTime,\n lastModified: msg.lastModifiedDateTime,\n importance: msg.importance,\n subject: msg.subject,\n hasAttachments: attachments.length > 0,\n attachmentCount: attachments.length,\n attachments: attachments.map((att) => ({\n id: att.id,\n name: att.name,\n contentType: att.contentType,\n contentUrl: att.contentUrl,\n })),\n mentions: mentions.map((mention) => ({\n text: mention.mentionText,\n userId: mention.mentioned.user.id,\n displayName: mention.mentioned.user.displayName,\n })),\n reactionCount: reactions.length,\n };\n });\n },\n});\n",
@@ -682,19 +529,6 @@ export default {
682
529
  "app/api/auth/teams/route.ts": "import { createOAuthInitHandler, teamsConfig } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT).\n// NEVER return a shared constant in production - it breaks per-user token isolation (VULN-AUTH-2).\nfunction getUserId(_request: Request): string {\n return \"current-user\";\n}\n\nexport const GET = createOAuthInitHandler(teamsConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});"
683
530
  }
684
531
  },
685
- "integration:box": {
686
- "files": {
687
- "tools/create-folder.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { createFolder } from \"../../lib/box-client.ts\";\n\nexport default tool({\n id: \"create-folder\",\n description: \"Create a new folder in Box. Use '0' as parent folder ID to create in the root folder.\",\n inputSchema: z.object({\n parentFolderId: z.string().describe(\"The ID of the parent folder (use '0' for root folder)\"),\n name: z.string().describe(\"The name of the folder to create\"),\n }),\n async execute({ parentFolderId, name }) {\n const folder = await createFolder({ parentFolderId, name });\n const path = folder.path_collection?.entries.map((entry) => entry.name).join(\"/\") ?? \"/\";\n\n return {\n success: true,\n folder: {\n id: folder.id,\n name: folder.name,\n createdAt: folder.created_at,\n path,\n },\n };\n },\n});\n",
688
- "tools/get-file.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getFile } from \"../../lib/box-client.ts\";\n\nexport default tool({\n id: \"get-file\",\n description: \"Get detailed information about a specific file or folder in Box.\",\n inputSchema: z.object({\n itemId: z.string().describe(\"The ID of the file or folder\"),\n itemType: z\n .enum([\"file\", \"folder\"])\n .default(\"file\")\n .describe(\"Whether the item is a file or folder\"),\n }),\n async execute({ itemId, itemType }) {\n const item = await getFile(itemId, itemType);\n const isFile = item.type === \"file\";\n const path = item.path_collection?.entries.map((e) => e.name).join(\"/\") ?? \"/\";\n\n return {\n id: item.id,\n type: item.type,\n name: item.name,\n size: isFile ? item.size : undefined,\n description: item.description,\n createdAt: item.created_at,\n modifiedAt: item.modified_at,\n createdBy: item.created_by?.name,\n modifiedBy: item.modified_by?.name,\n path,\n sharedLink: isFile ? item.shared_link?.url : undefined,\n };\n },\n});\n",
689
- "tools/upload-file.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { uploadFile } from \"../../lib/box-client.ts\";\n\nexport default tool({\n id: \"upload-file\",\n description:\n \"Upload a file to Box. Provide the file content as a string. Use '0' as parent folder ID to upload to the root folder.\",\n inputSchema: z.object({\n parentFolderId: z\n .string()\n .describe(\"The ID of the parent folder to upload to (use '0' for root folder)\"),\n fileName: z\n .string()\n .describe(\"The name of the file including extension (e.g., 'document.txt')\"),\n fileContent: z.string().describe(\"The content of the file as a string\"),\n }),\n async execute({ parentFolderId, fileName, fileContent }) {\n const file = await uploadFile({ parentFolderId, fileName, fileContent });\n\n const path =\n file.path_collection?.entries.map((entry) => entry.name).join(\"/\") ?? \"/\";\n\n return {\n success: true,\n file: {\n id: file.id,\n name: file.name,\n size: file.size,\n createdAt: file.created_at,\n path,\n },\n };\n },\n});\n",
690
- "tools/search-files.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { searchFiles } from \"../../lib/box-client.ts\";\n\nexport default tool({\n id: \"search-files\",\n description:\n \"Search for files and folders in Box by name or content. Returns matching items with their details.\",\n inputSchema: z.object({\n query: z.string().describe(\"Search query string to find files and folders\"),\n limit: z.number().min(1).max(100).default(50).describe(\"Maximum number of results to return\"),\n offset: z.number().min(0).default(0).describe(\"Number of results to skip for pagination\"),\n contentTypes: z\n .array(z.string())\n .optional()\n .describe(\"Filter by content types (e.g., ['name', 'description', 'file_content'])\"),\n }),\n async execute({ query, limit, offset, contentTypes }) {\n const results = await searchFiles({ query, limit, offset, contentTypes });\n\n return results.map((item) => {\n const path = item.path_collection?.entries.map((e) => e.name).join(\"/\") ?? \"/\";\n\n return {\n id: item.id,\n type: item.type,\n name: item.name,\n size: item.size,\n createdAt: item.created_at,\n modifiedAt: item.modified_at,\n path,\n };\n });\n },\n});\n",
691
- "tools/list-files.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { listFiles } from \"../../lib/box-client.ts\";\n\nexport default tool({\n id: \"list-files\",\n description:\n \"List files and folders from a Box folder. Use folder ID '0' for the root folder.\",\n inputSchema: z.object({\n folderId: z\n .string()\n .default(\"0\")\n .describe(\"Folder ID to list files from (use '0' for root folder)\"),\n limit: z\n .number()\n .min(1)\n .max(100)\n .default(50)\n .describe(\"Maximum number of items to return\"),\n offset: z\n .number()\n .min(0)\n .default(0)\n .describe(\"Number of items to skip for pagination\"),\n }),\n async execute({ folderId, limit, offset }) {\n const items = await listFiles({ folderId, limit, offset });\n\n return items.map((item) => {\n const pathEntries = item.path_collection?.entries;\n\n return {\n id: item.id,\n type: item.type,\n name: item.name,\n size: item.type === \"file\" ? item.size : undefined,\n createdAt: item.created_at,\n modifiedAt: item.modified_at,\n createdBy: item.created_by?.name,\n modifiedBy: item.modified_by?.name,\n path: pathEntries ? pathEntries.map((e) => e.name).join(\"/\") : \"/\",\n };\n });\n },\n});\n",
692
- ".env.example": "# Box OAuth Configuration\n# Get your credentials from https://app.box.com/developers/console\nBOX_CLIENT_ID=your-client-id\nBOX_CLIENT_SECRET=your-client-secret\n",
693
- "lib/box-client.ts": "import { getAccessToken } from \"./token-store.ts\";\n\nconst BOX_BASE_URL = \"https://api.box.com/2.0\";\nconst BOX_UPLOAD_URL = \"https://upload.box.com/api/2.0/files/content\";\n\ninterface BoxItemCollection<T> {\n total_count: number;\n entries: T[];\n offset: number;\n limit: number;\n}\n\ninterface BoxFile {\n id: string;\n type: \"file\";\n name: string;\n size: number;\n created_at: string;\n modified_at: string;\n description: string;\n path_collection: {\n entries: Array<{ id: string; name: string }>;\n };\n created_by: {\n id: string;\n name: string;\n };\n modified_by: {\n id: string;\n name: string;\n };\n shared_link?: {\n url: string;\n };\n}\n\ninterface BoxFolder {\n id: string;\n type: \"folder\";\n name: string;\n created_at: string;\n modified_at: string;\n description: string;\n path_collection: {\n entries: Array<{ id: string; name: string }>;\n };\n created_by: {\n id: string;\n name: string;\n };\n modified_by: {\n id: string;\n name: string;\n };\n item_collection?: {\n total_count: number;\n };\n}\n\ntype BoxItem = BoxFile | BoxFolder;\n\ninterface BoxSearchResult {\n type: \"file\" | \"folder\";\n id: string;\n name: string;\n size?: number;\n created_at: string;\n modified_at: string;\n path_collection: {\n entries: Array<{ id: string; name: string }>;\n };\n}\n\nasync function requireAccessToken(): Promise<string> {\n const token = await getAccessToken();\n if (!token) {\n throw new Error(\"Not authenticated with Box. Please connect your account.\");\n }\n return token;\n}\n\nasync function parseErrorMessage(\n response: Response,\n fallback: string,\n): Promise<string> {\n const error = await response.json().catch(() => ({} as { message?: string }));\n return `${fallback}: ${response.status} ${error.message ?? response.statusText}`;\n}\n\nasync function boxFetch<T>(\n endpoint: string,\n options: RequestInit = {},\n): Promise<T> {\n const token = await requireAccessToken();\n\n const response = await fetch(`${BOX_BASE_URL}${endpoint}`, {\n ...options,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n throw new Error(await parseErrorMessage(response, \"Box API error\"));\n }\n\n return (await response.json()) as T;\n}\n\nfunction toUploadBlob(fileContent: string | Buffer | Blob): Blob {\n if (typeof fileContent === \"string\") {\n return new Blob([fileContent], { type: \"text/plain\" });\n }\n\n if (fileContent instanceof Buffer) {\n return new Blob([fileContent]);\n }\n\n return fileContent;\n}\n\n/**\n * List files and folders in a Box folder\n */\nexport async function listFiles(options: {\n folderId?: string;\n limit?: number;\n offset?: number;\n}): Promise<BoxItem[]> {\n const { folderId = \"0\", limit = 100, offset = 0 } = options;\n\n const params = new URLSearchParams({\n limit: limit.toString(),\n offset: offset.toString(),\n fields:\n \"id,type,name,size,created_at,modified_at,description,path_collection,created_by,modified_by\",\n });\n\n const response = await boxFetch<BoxItemCollection<BoxItem>>(\n `/folders/${folderId}/items?${params}`,\n );\n\n return response.entries;\n}\n\n/**\n * Get details of a specific file or folder\n */\nexport async function getFile(\n itemId: string,\n itemType: \"file\" | \"folder\" = \"file\",\n): Promise<BoxItem> {\n const endpoint = itemType === \"file\" ? `/files/${itemId}` : `/folders/${itemId}`;\n\n const params = new URLSearchParams({\n fields:\n \"id,type,name,size,created_at,modified_at,description,path_collection,created_by,modified_by,shared_link\",\n });\n\n return boxFetch<BoxItem>(`${endpoint}?${params}`);\n}\n\n/**\n * Upload a file to Box\n */\nexport async function uploadFile(options: {\n parentFolderId: string;\n fileName: string;\n fileContent: string | Buffer | Blob;\n}): Promise<BoxFile> {\n const { parentFolderId, fileName, fileContent } = options;\n\n const token = await requireAccessToken();\n\n const formData = new FormData();\n formData.append(\n \"attributes\",\n JSON.stringify({\n name: fileName,\n parent: { id: parentFolderId },\n }),\n );\n formData.append(\"file\", toUploadBlob(fileContent), fileName);\n\n const response = await fetch(BOX_UPLOAD_URL, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${token}`,\n },\n body: formData,\n });\n\n if (!response.ok) {\n throw new Error(await parseErrorMessage(response, \"Box upload error\"));\n }\n\n const result = (await response.json()) as { entries: BoxFile[] };\n return result.entries[0];\n}\n\n/**\n * Download a file from Box\n */\nexport async function downloadFile(fileId: string): Promise<{\n content: ArrayBuffer;\n fileName: string;\n mimeType: string;\n}> {\n const token = await requireAccessToken();\n const fileInfo = (await getFile(fileId, \"file\")) as BoxFile;\n\n const response = await fetch(`${BOX_BASE_URL}/files/${fileId}/content`, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n\n if (!response.ok) {\n throw new Error(await parseErrorMessage(response, \"Box download error\"));\n }\n\n const content = await response.arrayBuffer();\n const mimeType = response.headers.get(\"content-type\") ?? \"application/octet-stream\";\n\n return { content, fileName: fileInfo.name, mimeType };\n}\n\n/**\n * Create a new folder in Box\n */\nexport async function createFolder(options: {\n parentFolderId: string;\n name: string;\n}): Promise<BoxFolder> {\n const { parentFolderId, name } = options;\n\n return boxFetch<BoxFolder>(\"/folders\", {\n method: \"POST\",\n body: JSON.stringify({\n name,\n parent: { id: parentFolderId },\n }),\n });\n}\n\n/**\n * Search for files and folders in Box\n */\nexport async function searchFiles(options: {\n query: string;\n limit?: number;\n offset?: number;\n contentTypes?: string[];\n}): Promise<BoxSearchResult[]> {\n const { query, limit = 100, offset = 0, contentTypes } = options;\n\n const params = new URLSearchParams({\n query,\n limit: limit.toString(),\n offset: offset.toString(),\n fields: \"id,type,name,size,created_at,modified_at,path_collection\",\n });\n\n if (contentTypes?.length) {\n params.set(\"content_types\", contentTypes.join(\",\"));\n }\n\n const response = await boxFetch<BoxItemCollection<BoxSearchResult>>(\n `/search?${params}`,\n );\n\n return response.entries;\n}\n\n/**\n * Get current user info\n */\nexport async function getMe(): Promise<{ id: string; name: string; login: string }> {\n return boxFetch<{ id: string; name: string; login: string }>(\"/users/me\");\n}\n",
694
- "app/api/auth/box/callback/route.ts": "import { boxConfig, createOAuthCallbackHandler } from \"veryfront/oauth\";\nimport { tokenStore } from \"../../../../../lib/token-store.ts\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\nconst hybridTokenStore = {\n async getTokens(serviceId: string, userId: string) {\n return tokenStore.getToken(userId, serviceId);\n },\n async setTokens(\n serviceId: string,\n userId: string,\n tokens: { accessToken: string; refreshToken?: string; expiresAt?: number },\n ) {\n await tokenStore.setToken(userId, serviceId, tokens);\n },\n async clearTokens(serviceId: string, userId: string) {\n await tokenStore.revokeToken(userId, serviceId);\n },\n setState(\n state: string,\n meta: {\n userId: string;\n serviceId: string;\n codeVerifier?: string;\n redirectUri?: string;\n scopes?: string[];\n createdAt: number;\n },\n ) {\n return oauthMemoryTokenStore.setState(state, meta);\n },\n consumeState(state: string) {\n return oauthMemoryTokenStore.consumeState(state);\n },\n};\n\nexport const GET = createOAuthCallbackHandler(boxConfig, { tokenStore: hybridTokenStore });\n",
695
- "app/api/auth/box/route.ts": "import { boxConfig, createOAuthInitHandler } from \"veryfront/oauth\";\nimport { oauthMemoryTokenStore } from \"../../../../../lib/oauth-memory-store.ts\";\n\n// TODO: Replace with real user ID from your auth system (e.g., session cookie, JWT).\n// NEVER return a shared constant in production - it breaks per-user token isolation (VULN-AUTH-2).\nfunction getUserId(_request: Request): string {\n return \"current-user\";\n}\n\nexport const GET = createOAuthInitHandler(boxConfig, {\n tokenStore: oauthMemoryTokenStore,\n getUserId,\n});"
696
- }
697
- },
698
532
  "integration:supabase": {
699
533
  "files": {
700
534
  "tools/list-tables.ts": "import { tool } from \"veryfront/tool\";\nimport { z } from \"zod\";\nimport { getTableColumns, listTables } from \"../../lib/supabase-client.ts\";\n\nexport default tool({\n id: \"list-tables\",\n description: \"List all tables in your Supabase database with their schema information.\",\n inputSchema: z.object({\n includeColumns: z\n .boolean()\n .default(false)\n .describe(\"Include column information for each table\"),\n }),\n async execute({ includeColumns }): Promise<{\n count: number;\n tables: Array<{\n name: string;\n schema: string;\n type: string;\n columns?: Array<{\n name: string;\n type: string;\n nullable: boolean;\n default: unknown;\n }>;\n error?: string;\n }>;\n }> {\n const tables = await listTables();\n\n if (!includeColumns) {\n const baseTables = tables.map((t) => ({\n name: t.table_name,\n schema: t.table_schema,\n type: t.table_type,\n }));\n\n return { count: baseTables.length, tables: baseTables };\n }\n\n const tablesWithColumns = await Promise.all(\n tables.map(async (table) => {\n const base = {\n name: table.table_name,\n schema: table.table_schema,\n type: table.table_type,\n };\n\n try {\n const columns = await getTableColumns(table.table_name);\n\n return {\n ...base,\n columns: columns.map((c) => ({\n name: c.column_name,\n type: c.data_type,\n nullable: c.is_nullable === \"YES\",\n default: c.column_default,\n })),\n };\n } catch (error) {\n return {\n ...base,\n columns: [],\n error: error instanceof Error ? error.message : \"Failed to fetch columns\",\n };\n }\n }),\n );\n\n return { count: tablesWithColumns.length, tables: tablesWithColumns };\n },\n});\n",