diff --git a/apify/loaders/getActorRun.ts b/apify/loaders/getActorRun.ts index 87b4e96ac..78e7bef70 100644 --- a/apify/loaders/getActorRun.ts +++ b/apify/loaders/getActorRun.ts @@ -47,10 +47,10 @@ export default async function getActorRun( if (props.includeDatasetItems && result.data.defaultDatasetId) { const datasetItemsResponse = await ctx.api - ["GET /v2/datasets/:datasetId/items"]({ - datasetId: result.data.defaultDatasetId, - format: "json", - }); + ["GET /v2/datasets/:datasetId/items"]({ + datasetId: result.data.defaultDatasetId, + format: "json", + }); result.data.results = await datasetItemsResponse.json(); // Place dataset items in the response. } diff --git a/figma/README.MD b/figma/README.MD index a3fd8f33b..4cdf12d05 100644 --- a/figma/README.MD +++ b/figma/README.MD @@ -212,12 +212,19 @@ const simplifiedDocument = simplifyDocument(document); ## Configuration -To use this app, you need to provide a Figma API access token. +To use this app, you need to configure OAuth 2.0 with your Figma application. + +First, set up the environment variables: +- `FIGMA_CLIENT_ID`: Your Figma OAuth app client ID +- `FIGMA_CLIENT_SECRET`: Your Figma OAuth app client secret ```typescript import { App } from "figma/mod.ts"; const figmaApp = App({ - accessToken: "your_token_here", + clientId: Deno.env.get("FIGMA_CLIENT_ID"), + clientSecret: Deno.env.get("FIGMA_CLIENT_SECRET"), }); -``` \ No newline at end of file +``` + +The OAuth flow will handle token management automatically, including refresh when needed. \ No newline at end of file diff --git a/figma/actions/oauth/callback.ts b/figma/actions/oauth/callback.ts new file mode 100644 index 000000000..0f8e83d3c --- /dev/null +++ b/figma/actions/oauth/callback.ts @@ -0,0 +1,84 @@ +import { AppContext } from "../../mod.ts"; + +interface OAuthCallbackResponse { + access_token: string; + expires_in: number; + refresh_token: string; + scope: string; + token_type: string; +} + +export interface Props { + code: string; + installId: string; + clientId: string; + clientSecret: string; + redirectUri: string; +} + +/** + * @name OAUTH_CALLBACK + * @title OAuth Callback + * @description Exchanges the authorization code for access tokens + */ +export default async function callback( + { code, installId, clientId, clientSecret, redirectUri }: Props, + req: Request, + ctx: AppContext, +): Promise<{ installId: string; account?: string }> { + const { client } = ctx; + + const finalRedirectUri = redirectUri || + new URL("/oauth/callback", req.url).href; + + try { + const credentials = btoa(`${clientId}:${clientSecret}`); + + const response = await client["POST /token"]({}, { + body: { + code, + redirect_uri: finalRedirectUri, + grant_type: "authorization_code", + }, + headers: { + "Authorization": `Basic ${credentials}`, + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Token exchange failed: ${response.status} ${errorText}`); + } + + const tokenData = await response.json() as OAuthCallbackResponse; + + const currentTime = Math.floor(Date.now() / 1000); + + const currentCtx = await ctx.getConfiguration(); + await ctx.configure({ + ...currentCtx, + tokens: { + access_token: tokenData.access_token, + refresh_token: tokenData.refresh_token, + expires_in: tokenData.expires_in, + scope: tokenData.scope, + token_type: tokenData.token_type, + tokenObtainedAt: currentTime, + }, + clientSecret: clientSecret, + clientId: clientId, + }); + + const account = await ctx.invoke["figma"].loaders.oauth.whoami({ + accessToken: tokenData.access_token, + }) + .then((user: { email: string; handle: string }) => + user.email || user.handle + ) + .catch(console.error) || undefined; + + return { installId, account }; + } catch (_error) { + return { installId, account: "error oauth" }; + } +} diff --git a/figma/client.ts b/figma/client.ts deleted file mode 100644 index d327475cd..000000000 --- a/figma/client.ts +++ /dev/null @@ -1,294 +0,0 @@ -/** - * @description Figma API response with success/error information - */ -export interface FigmaResponse { - err?: string; - status?: number; - data?: T; -} - -/** - * @description Figma document node - */ -export interface FigmaNode { - id: string; - name: string; - type: string; - [key: string]: - | string - | number - | boolean - | FigmaNode - | FigmaComponent - | FigmaComponentSet - | FigmaStyle - | undefined; // Other specific fields of the node -} - -/** - * @description Figma component - */ -export interface FigmaComponent { - key: string; - name: string; - description: string; - [key: string]: string | number | boolean | undefined; // Other specific fields of the component -} - -/** - * @description Figma component set - */ -export interface FigmaComponentSet { - key: string; - name: string; - [key: string]: string | number | boolean | undefined; // Other specific fields of the component set -} - -/** - * @description Figma style - */ -export interface FigmaStyle { - key: string; - name: string; - [key: string]: string | number | boolean | undefined; // Other specific fields of the style -} - -/** - * @description Figma file - */ -export interface FigmaFile { - name: string; - role: string; - lastModified: string; - editorType: string; - thumbnailUrl: string; - version: string; - document: FigmaNode; - components: Record; - componentSets: Record; - schemaVersion: number; - styles: Record; - mainFileKey?: string; - branches?: Array<{ - key: string; - name: string; - thumbnail_url: string; - last_modified: string; - link_access: string; - }>; -} - -/** - * @description Client to interact with Figma APIs - */ -export class FigmaClient { - private headers: { "X-FIGMA-TOKEN": string }; - - constructor(accessToken: string) { - this.headers = { - "X-FIGMA-TOKEN": accessToken, - }; - } - - /** - * @description Gets a file JSON from Figma - * @param fileKey File key - * @param options Request options - */ - async getFile( - fileKey: string, - options?: { - version?: string; - ids?: string[]; - depth?: number; - geometry?: "paths"; - plugin_data?: string; - branch_data?: boolean; - }, - ): Promise> { - const params = new URLSearchParams(); - - if (options?.version) params.append("version", options.version); - if (options?.ids) params.append("ids", options.ids.join(",")); - if (options?.depth) params.append("depth", options.depth.toString()); - if (options?.geometry) params.append("geometry", options.geometry); - if (options?.plugin_data) params.append("plugin_data", options.plugin_data); - if (options?.branch_data) { - params.append("branch_data", options.branch_data.toString()); - } - - const response = await fetch( - `https://api.figma.com/v1/files/${fileKey}${ - params.toString() ? `?${params}` : "" - }`, - { headers: this.headers }, - ); - - return response.json(); - } - - /** - * @description Get JSON from specific nodes in a file - * @param fileKey File key - * @param nodeIds Node IDs - * @param options Request options - */ - async getFileNodes( - fileKey: string, - nodeIds: string[], - options?: { - version?: string; - depth?: number; - geometry?: "paths"; - plugin_data?: string; - }, - ): Promise< - FigmaResponse<{ - nodes: Record; - componentSets: Record; - styles: Record; - schemaVersion: number; - }>; - }> - > { - const params = new URLSearchParams({ - ids: nodeIds.join(","), - }); - - if (options?.version) params.append("version", options.version); - if (options?.depth) params.append("depth", options.depth.toString()); - if (options?.geometry) params.append("geometry", options.geometry); - if (options?.plugin_data) params.append("plugin_data", options.plugin_data); - - const response = await fetch( - `https://api.figma.com/v1/files/${fileKey}/nodes?${params}`, - { headers: this.headers }, - ); - - return response.json(); - } - - /** - * @description Renders images from a file - * @param fileKey File key - * @param nodeIds Node IDs - * @param options Rendering options - */ - async getImages( - fileKey: string, - nodeIds: string[], - options?: { - scale?: number; - format?: "jpg" | "png" | "svg" | "pdf"; - svg_outline_text?: boolean; - svg_include_id?: boolean; - svg_include_node_id?: boolean; - svg_simplify_stroke?: boolean; - contents_only?: boolean; - use_absolute_bounds?: boolean; - version?: string; - }, - ): Promise< - FigmaResponse<{ - images: Record; - }> - > { - const params = new URLSearchParams({ - ids: nodeIds.join(","), - }); - - if (options?.scale) params.append("scale", options.scale.toString()); - if (options?.format) params.append("format", options.format); - if (options?.svg_outline_text !== undefined) { - params.append("svg_outline_text", options.svg_outline_text.toString()); - } - if (options?.svg_include_id !== undefined) { - params.append("svg_include_id", options.svg_include_id.toString()); - } - if (options?.svg_include_node_id !== undefined) { - params.append( - "svg_include_node_id", - options.svg_include_node_id.toString(), - ); - } - if (options?.svg_simplify_stroke !== undefined) { - params.append( - "svg_simplify_stroke", - options.svg_simplify_stroke.toString(), - ); - } - if (options?.contents_only !== undefined) { - params.append("contents_only", options.contents_only.toString()); - } - if (options?.use_absolute_bounds !== undefined) { - params.append( - "use_absolute_bounds", - options.use_absolute_bounds.toString(), - ); - } - if (options?.version) { - params.append("version", options.version); - } - - const response = await fetch( - `https://api.figma.com/v1/images/${fileKey}?${params}`, - { headers: this.headers }, - ); - - return response.json(); - } - - /** - * @description Renders images from a file. if no error occurs, `"images"` will be populated with a map from node ids to urls of the rendered images, and `"status"` will be omitted. the image assets will expire after 30 days. images up to 32 megapixels can be exported. any images that are larger will be scaled down. important: the image map may contain values that are `null`. this indicates that rendering of that specific node has failed. this may be due to the node id not existing, or other reasons such has the node having no renderable components. it is guaranteed that any node that was requested for rendering will be represented in this map whether or not the render succeeded. to render multiple images from the same file, use the `ids` query parameter to specify multiple node ids. - * @param fileKey File key - * @param nodeIds One or more node IDs (comma-separated string or array) - */ - async getImageFromNode( - fileKey: string, - nodeIds: string[], - ): Promise< - FigmaResponse<{ - images: Record; - }> - > { - const ids = Array.isArray(nodeIds) ? nodeIds.join(",") : nodeIds; - - const response = await fetch( - `https://api.figma.com/v1/images/${fileKey}?ids=${ - encodeURIComponent(ids) - }`, - { - headers: this.headers, - }, - ); - - if (!response.ok) { - throw new Error( - `Figma API error: ${response.status} ${response.statusText}`, - ); - } - - return await response.json(); - } - - /** - * @description Gets download URLs for all images present in image fills - * @param fileKey File key - */ - async getImageFills( - fileKey: string, - ): Promise<{ - meta: { - images: Record; - }; - }> { - const response = await fetch( - `https://api.figma.com/v1/files/${fileKey}/images`, - { headers: this.headers }, - ); - - return response.json(); - } -} diff --git a/figma/loaders/getComponents.ts b/figma/loaders/getComponents.ts index e05d87388..1af913d60 100644 --- a/figma/loaders/getComponents.ts +++ b/figma/loaders/getComponents.ts @@ -1,5 +1,5 @@ import type { AppContext } from "../mod.ts"; -import type { FigmaFile, FigmaResponse } from "../client.ts"; +import type { FigmaFile, FigmaResponse } from "../utils/client.ts"; export interface Props { /** @@ -35,33 +35,30 @@ export default async function getFileComponents( ctx: AppContext, ): Promise> { const { fileKey, version, depth, branch_data } = props; - if (!ctx.figma) { - throw new Error("Figma client not found"); - } - const response = await ctx.figma.getFile(fileKey, { + const response = await ctx.client["GET /v1/files/:fileKey"]({ + fileKey, version, depth, branch_data, }); - // If there's an error in the response, return the original response - if (response.err) { - return response; + if (!response.ok) { + return { + err: `HTTP ${response.status}: ${response.statusText}`, + status: response.status, + }; } - // If there's no data, return the original response - if (!response.data) { - return response; - } + const data = await response.json(); // Return only the components of the file return { - ...response, + status: response.status, data: { - ...response.data, - document: response.data.document, - components: response.data.components, - componentSets: response.data.componentSets, + ...data, + document: data.document, + components: data.components, + componentSets: data.componentSets, }, }; } diff --git a/figma/loaders/getFileImages.ts b/figma/loaders/getFileImages.ts index aa5ced4f9..b327d035d 100644 --- a/figma/loaders/getFileImages.ts +++ b/figma/loaders/getFileImages.ts @@ -1,5 +1,5 @@ import type { AppContext } from "../mod.ts"; -import type { FigmaResponse } from "../client.ts"; +import type { FigmaResponse } from "../utils/client.ts"; export interface Props { /** @@ -90,11 +90,9 @@ export default async function getFileImages( version, } = props; - if (!ctx.figma) { - throw new Error("Figma client not found"); - } - - return await ctx.figma.getImages(fileKey, nodeIds, { + const response = await ctx.client["GET /v1/images/:fileKey"]({ + fileKey, + ids: nodeIds.join(","), scale, format, svg_outline_text, @@ -105,4 +103,18 @@ export default async function getFileImages( use_absolute_bounds, version, }); + + if (!response.ok) { + return { + err: `HTTP ${response.status}: ${response.statusText}`, + status: response.status, + }; + } + + const data = await response.json(); + + return { + status: response.status, + data: data, + }; } diff --git a/figma/loaders/getImagesSpecificNode.ts b/figma/loaders/getImagesSpecificNode.ts index b666a021f..104d136f4 100644 --- a/figma/loaders/getImagesSpecificNode.ts +++ b/figma/loaders/getImagesSpecificNode.ts @@ -1,5 +1,5 @@ import type { AppContext } from "../mod.ts"; -import type { FigmaResponse } from "../client.ts"; +import type { FigmaImagesResponse, FigmaResponse } from "../utils/client.ts"; export interface Props { /** @@ -24,13 +24,25 @@ export default async function getFileImageSpecificNode( _req: Request, ctx: AppContext, ): Promise< - FigmaResponse<{ - images: Record; - }> + FigmaResponse > { const { fileKey, nodeIds } = props; - if (!ctx.figma) { - throw new Error("Figma client not found"); + const response = await ctx.client["GET /v1/images/:fileKey"]({ + fileKey, + ids: nodeIds.join(","), + }); + + if (!response.ok) { + return { + err: `HTTP ${response.status}: ${response.statusText}`, + status: response.status, + }; } - return await ctx.figma.getImageFromNode(fileKey, nodeIds); + + const data = await response.json(); + + return { + status: response.status, + data: data, + }; } diff --git a/figma/loaders/getImagesToFills.ts b/figma/loaders/getImagesToFills.ts index e125c40e7..bb0556994 100644 --- a/figma/loaders/getImagesToFills.ts +++ b/figma/loaders/getImagesToFills.ts @@ -1,4 +1,5 @@ import type { AppContext } from "../mod.ts"; +import type { FigmaResponse } from "../utils/client.ts"; export interface Props { /** @@ -21,21 +22,27 @@ export default async function getFileImageFills( props: Props, _req: Request, ctx: AppContext, -): Promise> { +): Promise< + FigmaResponse<{ + images: Record; + }> +> { const { fileKey } = props; - if (!ctx.figma) { - throw new Error("Figma client not found"); - } - const images = (await ctx.figma.getImageFills(fileKey))?.meta?.images; + const response = await ctx.client["GET /v1/files/:fileKey/images"]({ + fileKey, + }); - let imagesToReturn: Record = {}; - if (props.imageRef) { - props.imageRef.forEach((ref) => { - imagesToReturn[ref] = images?.[ref] ?? ""; - }); - } else { - imagesToReturn = images; + if (!response.ok) { + return { + err: `HTTP ${response.status}: ${response.statusText}`, + status: response.status, + }; } - return imagesToReturn; + const data = await response.json(); + + return { + status: response.status, + data: data, + }; } diff --git a/figma/loaders/getNodes.ts b/figma/loaders/getNodes.ts index 8f9a6f2b1..1e0cbf574 100644 --- a/figma/loaders/getNodes.ts +++ b/figma/loaders/getNodes.ts @@ -5,7 +5,7 @@ import type { FigmaNode, FigmaResponse, FigmaStyle, -} from "../client.ts"; +} from "../utils/client.ts"; export interface Props { /** @@ -57,12 +57,25 @@ export default async function getFileNodes( ctx: AppContext, ): Promise> { const { fileKey, nodeIds, version, depth, geometry } = props; - if (!ctx.figma) { - throw new Error("Figma client not found"); - } - return await ctx.figma.getFileNodes(fileKey, nodeIds, { + const response = await ctx.client["GET /v1/files/:fileKey/nodes"]({ + fileKey, + ids: nodeIds.join(","), version, depth, geometry, }); + + if (!response.ok) { + return { + err: `HTTP ${response.status}: ${response.statusText}`, + status: response.status, + }; + } + + const data = await response.json(); + + return { + status: response.status, + data: data, + }; } diff --git a/figma/loaders/getSimplified.ts b/figma/loaders/getSimplified.ts index b862ca9bd..86df59467 100644 --- a/figma/loaders/getSimplified.ts +++ b/figma/loaders/getSimplified.ts @@ -5,7 +5,7 @@ import type { FigmaNode, FigmaResponse, FigmaStyle, -} from "../client.ts"; +} from "../utils/client.ts"; import { simplifyComponent, simplifyComponentSet, @@ -60,24 +60,26 @@ export default async function getFileSimplified( ctx: AppContext, ): Promise> { const { fileKey, version, depth, branch_data } = props; - if (!ctx.figma) { - throw new Error("Figma client not found"); - } - const response = await ctx.figma.getFile(fileKey, { + const response = await ctx.client["GET /v1/files/:fileKey"]({ + fileKey, version, depth, branch_data, }); - // If there's an error in the response, return the original response - if (response.err || !response.data) { - return response; + if (!response.ok) { + return { + err: `HTTP ${response.status}: ${response.statusText}`, + status: response.status, + }; } + const data = await response.json(); + // Simplify the data const simplifiedComponents: Record = {}; for ( - const [key, component] of Object.entries(response.data.components || {}) + const [key, component] of Object.entries(data.components || {}) ) { const simplified = simplifyComponent(component); if (simplified) { @@ -88,7 +90,7 @@ export default async function getFileSimplified( const simplifiedComponentSets: Record = {}; for ( const [key, componentSet] of Object.entries( - response.data.componentSets || {}, + data.componentSets || {}, ) ) { const simplified = simplifyComponentSet(componentSet); @@ -98,28 +100,28 @@ export default async function getFileSimplified( } const simplifiedStyles: Record = {}; - for (const [key, style] of Object.entries(response.data.styles || {})) { + for (const [key, style] of Object.entries(data.styles || {})) { const simplified = simplifyStyle(style); if (simplified) { simplifiedStyles[key] = simplified as FigmaStyle; } } - const simplifiedDoc = simplifyDocument(response.data.document); + const simplifiedDoc = simplifyDocument(data.document); if (!simplifiedDoc) { throw new Error("Failed to simplify document"); } // Return the simplified data return { - ...response, + status: response.status, data: { - name: response.data.name, - role: response.data.role, - lastModified: response.data.lastModified, - editorType: response.data.editorType, - thumbnailUrl: response.data.thumbnailUrl, - version: response.data.version, + name: data.name, + role: data.role, + lastModified: data.lastModified, + editorType: data.editorType, + thumbnailUrl: data.thumbnailUrl, + version: data.version, document: simplifiedDoc as FigmaNode, components: simplifiedComponents, componentSets: simplifiedComponentSets, diff --git a/figma/loaders/getSimplifiedNodes.ts b/figma/loaders/getSimplifiedNodes.ts index 3c769048c..4cff70091 100644 --- a/figma/loaders/getSimplifiedNodes.ts +++ b/figma/loaders/getSimplifiedNodes.ts @@ -1,12 +1,12 @@ import type { AppContext } from "../mod.ts"; -import type { FigmaResponse } from "../client.ts"; +import type { FigmaResponse } from "../utils/client.ts"; import { simplifyNode } from "../utils/simplifier.ts"; import type { FigmaComponent, FigmaComponentSet, FigmaNode, FigmaStyle, -} from "../client.ts"; +} from "../utils/client.ts"; export interface Props { /** @@ -60,42 +60,53 @@ export default async function getFileSimplifiedNodes( ctx: AppContext, ): Promise> { const { fileKey, nodeIds, version, depth, geometry } = props; - if (!ctx.figma) { - throw new Error("Figma client not found"); - } - const client = ctx.figma; - const response = await client.getFileNodes(fileKey, nodeIds, { + + const response = await ctx.client["GET /v1/files/:fileKey/nodes"]({ + fileKey, + ids: nodeIds.join(","), version, depth, geometry, }); - // If there's an error in the response, return the original response - if (response.err || !response.data) { - return response; + if (!response.ok) { + return { + err: `HTTP ${response.status}: ${response.statusText}`, + status: response.status, + }; } + const data = await response.json(); + // Simplify the nodes const simplifiedNodes: Record = {}; - for (const [nodeId, nodeData] of Object.entries(response.data.nodes)) { + for (const [nodeId, nodeData] of Object.entries(data.nodes)) { if (!nodeData) { simplifiedNodes[nodeId] = null; continue; } + const node = nodeData as { + document: FigmaNode; + components: Record; + componentSets: Record; + styles: Record; + schemaVersion: number; + }; + simplifiedNodes[nodeId] = { - document: simplifyNode(nodeData.document), - components: nodeData.components || {}, - componentSets: nodeData.componentSets || {}, - styles: nodeData.styles || {}, - schemaVersion: nodeData.schemaVersion || 0, + document: simplifyNode(node.document), + components: node.components || {}, + componentSets: node.componentSets || {}, + styles: node.styles || {}, + schemaVersion: node.schemaVersion || 0, }; } // Return the simplified nodes return { - ...response, + status: response.status, data: { nodes: simplifiedNodes, }, diff --git a/figma/loaders/getUserInfo.ts b/figma/loaders/getUserInfo.ts new file mode 100644 index 000000000..91e7458a5 --- /dev/null +++ b/figma/loaders/getUserInfo.ts @@ -0,0 +1,44 @@ +import type { AppContext } from "../mod.ts"; +import type { FigmaResponse } from "../utils/client.ts"; + +/** + * @title Get User Info + * @description Retrieves information about the authenticated user + */ +export default async function getUserInfo( + _props: Record, + _req: Request, + ctx: AppContext, +): Promise< + FigmaResponse<{ + id: string; + email: string; + handle: string; + img_url: string; + }> +> { + try { + const response = await ctx.client["GET /v1/me"]({}); + + if (!response.ok) { + return { + err: `HTTP ${response.status}: ${response.statusText}`, + status: response.status, + }; + } + + const data = await response.json(); + + return { + status: response.status, + data: data, + }; + } catch (error) { + return { + err: `Erro interno: ${ + error instanceof Error ? error.message : String(error) + }`, + status: 500, + }; + } +} diff --git a/figma/loaders/oauth/start.ts b/figma/loaders/oauth/start.ts new file mode 100644 index 000000000..e574dcfee --- /dev/null +++ b/figma/loaders/oauth/start.ts @@ -0,0 +1,21 @@ +import { OAUTH_URL_AUTH, SCOPES } from "../../utils/constant.ts"; + +export interface Props { + clientId: string; + redirectUri: string; + state: string; +} + +export default function start(props: Props) { + const authParams = new URLSearchParams({ + client_id: props.clientId, + redirect_uri: props.redirectUri, + response_type: "code", + scope: SCOPES.join(" "), + state: props.state, + }); + + const authorizationUrl = `${OAUTH_URL_AUTH}?${authParams.toString()}`; + + return Response.redirect(authorizationUrl); +} diff --git a/figma/loaders/oauth/whoami.ts b/figma/loaders/oauth/whoami.ts new file mode 100644 index 000000000..6642f9fdd --- /dev/null +++ b/figma/loaders/oauth/whoami.ts @@ -0,0 +1,64 @@ +import type { AppContext } from "../../mod.ts"; + +export interface Props { + /** + * @title Access Token + * @description Override access token for the request + */ + accessToken?: string; +} + +export interface FigmaUserInfo { + id: string; + email: string; + handle: string; + img_url: string; +} + +/** + * @title Get Current User (Whoami) + * @description Retrieves the current user's information from Figma + */ +export default async function whoami( + props: Props, + _req: Request, + ctx: AppContext, +): Promise { + try { + // Use custom access token if provided, otherwise use context client + let response; + + if (props.accessToken) { + // Make direct fetch with custom token + response = await fetch("https://api.figma.com/v1/me", { + headers: { + "Authorization": `Bearer ${props.accessToken}`, + "Accept": "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + return data as FigmaUserInfo; + } else { + // Use context client + response = await ctx.client["GET /v1/me"]({}); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + return data as FigmaUserInfo; + } + } catch (error) { + throw new Error( + `Erro ao obter informações do usuário: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + } +} diff --git a/figma/manifest.gen.ts b/figma/manifest.gen.ts index e489cd295..035da4504 100644 --- a/figma/manifest.gen.ts +++ b/figma/manifest.gen.ts @@ -2,6 +2,7 @@ // This file SHOULD be checked into source version control. // This file is automatically updated during development when running `dev.ts`. +import * as $$$$$$$$$0 from "./actions/oauth/callback.ts"; import * as $$$0 from "./loaders/getComponents.ts"; import * as $$$1 from "./loaders/getFileImages.ts"; import * as $$$2 from "./loaders/getImagesSpecificNode.ts"; @@ -9,6 +10,9 @@ import * as $$$3 from "./loaders/getImagesToFills.ts"; import * as $$$4 from "./loaders/getNodes.ts"; import * as $$$5 from "./loaders/getSimplified.ts"; import * as $$$6 from "./loaders/getSimplifiedNodes.ts"; +import * as $$$7 from "./loaders/getUserInfo.ts"; +import * as $$$8 from "./loaders/oauth/start.ts"; +import * as $$$9 from "./loaders/oauth/whoami.ts"; const manifest = { "loaders": { @@ -19,6 +23,12 @@ const manifest = { "figma/loaders/getNodes.ts": $$$4, "figma/loaders/getSimplified.ts": $$$5, "figma/loaders/getSimplifiedNodes.ts": $$$6, + "figma/loaders/getUserInfo.ts": $$$7, + "figma/loaders/oauth/start.ts": $$$8, + "figma/loaders/oauth/whoami.ts": $$$9, + }, + "actions": { + "figma/actions/oauth/callback.ts": $$$$$$$$$0, }, "name": "figma", "baseUrl": import.meta.url, diff --git a/figma/mod.ts b/figma/mod.ts index 393f2e546..8c5ed7838 100644 --- a/figma/mod.ts +++ b/figma/mod.ts @@ -1,44 +1,98 @@ -import type { App, FnContext } from "@deco/deco"; +import { createOAuthHttpClient } from "../mcp/utils/httpClient.ts"; import manifest, { Manifest } from "./manifest.gen.ts"; -import { FigmaClient } from "./client.ts"; -import { Secret } from "../website/loaders/secret.ts"; +import type { FnContext } from "@deco/deco"; +import { McpContext } from "../mcp/context.ts"; +import { + API_URL, + OAUTH_URL, + OAUTH_URL_AUTH, + SCOPES, +} from "./utils/constant.ts"; +import { AuthClient, FigmaClient } from "./utils/client.ts"; +import { + DEFAULT_OAUTH_HEADERS, + OAuthClientOptions, + OAuthClients, + OAuthProvider, + OAuthTokens, +} from "../mcp/oauth.ts"; + +export const FigmaProvider: OAuthProvider = { + name: "Figma", + authUrl: OAUTH_URL_AUTH, + tokenUrl: OAUTH_URL, + scopes: SCOPES, + clientId: "", + clientSecret: "", +}; export interface Props { - /** - * @description Figma API access token for authentication - */ - accessToken: string | Secret; + tokens?: OAuthTokens; + clientSecret?: string; + clientId?: string; } export interface State extends Props { - figma: FigmaClient | null; + client: OAuthClients; } -export type AppContext = FnContext; +export type AppContext = FnContext, Manifest>; /** - * @appName figma * @title Figma - * @description Fetch structured design data directly from your Figma files. + * @description Integração com Figma usando OAuth 2.0 com refresh automático de tokens + * @category Design * @logo https://assets.decocache.com/mcp/eb714f8a-404b-4b8e-bfc4-f3ce5bde6f51/Figma.svg */ -export default function App(props: Props): App { - const figma = props.accessToken - ? new FigmaClient( - typeof props.accessToken === "string" - ? props.accessToken - : props.accessToken.get()!, - ) - : null; +export default function App( + props: Props, + _req: Request, + ctx?: McpContext, +) { + const { tokens, clientId, clientSecret } = props; - return { - state: { - ...props, - figma, + const figmaProvider: OAuthProvider = { + ...FigmaProvider, + clientId: clientId ?? "", + clientSecret: clientSecret ?? "", + }; + + const options: OAuthClientOptions = { + headers: DEFAULT_OAUTH_HEADERS, + authClientConfig: { + headers: new Headers({ + "Accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded", + }), }, + }; + + const client = createOAuthHttpClient({ + provider: figmaProvider, + apiBaseUrl: API_URL, + tokens, + options, + onTokenRefresh: async (newTokens: OAuthTokens) => { + if (ctx) { + await ctx.configure({ + ...ctx, + tokens: newTokens, + }); + } + }, + }); + + const state: State = { + ...props, + tokens, + client, + }; + + return { + state, manifest, }; } // Re-exports client types for convenience -export * from "./client.ts"; +export * from "./utils/client.ts"; diff --git a/figma/utils/client.ts b/figma/utils/client.ts new file mode 100644 index 000000000..a82b53ec9 --- /dev/null +++ b/figma/utils/client.ts @@ -0,0 +1,192 @@ +// ============================ +// Figma API Types +// ============================ + +/** + * @description Figma API response with success/error information + */ +export interface FigmaResponse { + err?: string; + status?: number; + data?: T; +} + +/** + * @description Figma document node + */ +export interface FigmaNode { + id: string; + name: string; + type: string; + [key: string]: + | string + | number + | boolean + | FigmaNode + | FigmaComponent + | FigmaComponentSet + | FigmaStyle + | undefined; // Other specific fields of the node +} + +/** + * @description Figma component + */ +export interface FigmaComponent { + key: string; + name: string; + description: string; + [key: string]: string | number | boolean | undefined; // Other specific fields of the component +} + +/** + * @description Figma component set + */ +export interface FigmaComponentSet { + key: string; + name: string; + [key: string]: string | number | boolean | undefined; // Other specific fields of the component set +} + +/** + * @description Figma style + */ +export interface FigmaStyle { + key: string; + name: string; + [key: string]: string | number | boolean | undefined; // Other specific fields of the style +} + +/** + * @description Figma file + */ +export interface FigmaFile { + name: string; + role: string; + lastModified: string; + editorType: string; + thumbnailUrl: string; + version: string; + document: FigmaNode; + components: Record; + componentSets: Record; + schemaVersion: number; + styles: Record; + mainFileKey?: string; + branches?: Array<{ + key: string; + name: string; + thumbnail_url: string; + last_modified: string; + link_access: string; + }>; +} + +/** + * @description Figma file nodes response + */ +export interface FigmaFileNodesResponse { + nodes: Record; + componentSets: Record; + styles: Record; + schemaVersion: number; + }>; +} + +/** + * @description Figma images response + */ +export interface FigmaImagesResponse { + images: Record; +} + +/** + * @description Figma image fills response + */ +export interface FigmaImageFillsResponse { + images: Record; +} + +// ============================ +// Figma Client Interface +// ============================ + +export interface FigmaClient { + // Files + "GET /v1/files/:fileKey": { + response: FigmaFile; + searchParams?: { + version?: string; + ids?: string; + depth?: number; + geometry?: "paths"; + plugin_data?: string; + branch_data?: boolean; + }; + }; + + "GET /v1/files/:fileKey/nodes": { + response: FigmaFileNodesResponse; + searchParams: { + ids: string; + version?: string; + depth?: number; + geometry?: "paths"; + plugin_data?: string; + }; + }; + + // Images + "GET /v1/images/:fileKey": { + response: FigmaImagesResponse; + searchParams: { + ids: string; + scale?: number; + format?: "jpg" | "png" | "svg" | "pdf"; + svg_outline_text?: boolean; + svg_include_id?: boolean; + svg_include_node_id?: boolean; + svg_simplify_stroke?: boolean; + contents_only?: boolean; + use_absolute_bounds?: boolean; + version?: string; + }; + }; + + "GET /v1/files/:fileKey/images": { + response: FigmaImageFillsResponse; + }; + + // User + "GET /v1/me": { + response: { + id: string; + email: string; + handle: string; + img_url: string; + }; + }; +} + +// Auth client for OAuth token operations +export interface AuthClient { + "POST /token": { + body: { + grant_type: string; + code?: string; + refresh_token?: string; + client_id?: string; + client_secret?: string; + redirect_uri?: string; + }; + response: { + access_token: string; + refresh_token?: string; + expires_in?: number; + token_type?: string; + scope?: string; + }; + }; +} diff --git a/figma/utils/constant.ts b/figma/utils/constant.ts new file mode 100644 index 000000000..96fc64269 --- /dev/null +++ b/figma/utils/constant.ts @@ -0,0 +1,7 @@ +export const SCOPES = [ + "file_read", +]; + +export const API_URL = "https://api.figma.com"; +export const OAUTH_URL = "https://api.figma.com/v1/oauth"; +export const OAUTH_URL_AUTH = "https://www.figma.com/oauth"; diff --git a/figma/utils/simplifier.ts b/figma/utils/simplifier.ts index 65be48b69..d7ab8c878 100644 --- a/figma/utils/simplifier.ts +++ b/figma/utils/simplifier.ts @@ -3,7 +3,7 @@ import type { FigmaComponentSet, FigmaNode, FigmaStyle, -} from "../client.ts"; +} from "./client.ts"; type SimplifiedNode = Omit, "children"> & { children?: SimplifiedNode[]; diff --git a/hubspot/actions/conversations/sendThreadComment.ts b/hubspot/actions/conversations/sendThreadComment.ts index 58f2eb0e6..b58a7d842 100644 --- a/hubspot/actions/conversations/sendThreadComment.ts +++ b/hubspot/actions/conversations/sendThreadComment.ts @@ -2,122 +2,122 @@ import type { AppContext } from "../../mod.ts"; import { HubSpotClient } from "../../utils/client.ts"; export interface Attachment { - /** - * @title Attachment Type - * @description Type of attachment - */ - type: "FILE" | "QUICK_REPLIES" | "SOCIAL_MEDIA_METADATA"; - - /** - * @title File ID - * @description ID of the file (required for FILE type) - */ - fileId?: string; - - /** - * @title Name - * @description Name of the attachment - */ - name?: string; - - /** - * @title URL - * @description URL of the attachment - */ - url?: string; - - /** - * @title File Usage Type - * @description Usage type for the file - */ - fileUsageType?: string; + /** + * @title Attachment Type + * @description Type of attachment + */ + type: "FILE" | "QUICK_REPLIES" | "SOCIAL_MEDIA_METADATA"; + + /** + * @title File ID + * @description ID of the file (required for FILE type) + */ + fileId?: string; + + /** + * @title Name + * @description Name of the attachment + */ + name?: string; + + /** + * @title URL + * @description URL of the attachment + */ + url?: string; + + /** + * @title File Usage Type + * @description Usage type for the file + */ + fileUsageType?: string; } export interface Props { - /** - * @title Thread ID - * @description The ID of the conversation thread - */ - threadId: string; - - /** - * @title Comment Text - * @description The text content of the comment - */ - text: string; - - /** - * @title Rich Text - * @description Rich text content (HTML) - */ - richText?: string; - - /** - * @title Attachments - * @description Array of attachments - */ - attachments?: Attachment[]; + /** + * @title Thread ID + * @description The ID of the conversation thread + */ + threadId: string; + + /** + * @title Comment Text + * @description The text content of the comment + */ + text: string; + + /** + * @title Rich Text + * @description Rich text content (HTML) + */ + richText?: string; + + /** + * @title Attachments + * @description Array of attachments + */ + attachments?: Attachment[]; } export interface Client { - clientType: "HUBSPOT"; - integrationAppId: number; + clientType: "HUBSPOT"; + integrationAppId: number; } export interface Sender { - actorId: string; - name?: string; - senderField?: string; - deliveryIdentifier?: { - type: string; - value: string; - }; + actorId: string; + name?: string; + senderField?: string; + deliveryIdentifier?: { + type: string; + value: string; + }; } export interface MessageRecipient { - actorId: string; - name?: string; - deliveryIdentifier?: { - type: string; - value: string; - }; - recipientField?: string; + actorId: string; + name?: string; + deliveryIdentifier?: { + type: string; + value: string; + }; + recipientField?: string; } export interface FailureDetails { - errorMessageTokens: Record; - errorMessage?: string; + errorMessageTokens: Record; + errorMessage?: string; } export interface MessageStatus { - statusType: "SENT" | "FAILED" | "PENDING"; - failureDetails?: FailureDetails; + statusType: "SENT" | "FAILED" | "PENDING"; + failureDetails?: FailureDetails; } export interface CommentResponse { - type: "COMMENT"; - id: string; - conversationsThreadId: string; - createdAt: string; - updatedAt?: string; - createdBy: string; - client: Client; - senders: Sender[]; - recipients: MessageRecipient[]; - archived: boolean; - text?: string; - richText?: string; - attachments: Attachment[]; - subject?: string; - truncationStatus: - | "NOT_TRUNCATED" - | "TRUNCATED_TO_MOST_RECENT_REPLY" - | "TRUNCATED"; - inReplyToId?: string; - status?: MessageStatus; - direction: "INCOMING" | "OUTGOING"; - channelId: string; - channelAccountId: string; + type: "COMMENT"; + id: string; + conversationsThreadId: string; + createdAt: string; + updatedAt?: string; + createdBy: string; + client: Client; + senders: Sender[]; + recipients: MessageRecipient[]; + archived: boolean; + text?: string; + richText?: string; + attachments: Attachment[]; + subject?: string; + truncationStatus: + | "NOT_TRUNCATED" + | "TRUNCATED_TO_MOST_RECENT_REPLY" + | "TRUNCATED"; + inReplyToId?: string; + status?: MessageStatus; + direction: "INCOMING" | "OUTGOING"; + channelId: string; + channelAccountId: string; } /** @@ -125,25 +125,25 @@ export interface CommentResponse { * @description Send a comment to a conversation thread */ export default async function sendThreadComment( - props: Props, - _req: Request, - ctx: AppContext, + props: Props, + _req: Request, + ctx: AppContext, ): Promise { - const { threadId, text, richText, attachments = [] } = props; + const { threadId, text, richText, attachments = [] } = props; - const client = new HubSpotClient(ctx); + const client = new HubSpotClient(ctx); - const commentData: Record = { - type: "COMMENT", - text, - ...(richText && { richText }), - ...(attachments.length > 0 && { attachments }), - }; + const commentData: Record = { + type: "COMMENT", + text, + ...(richText && { richText }), + ...(attachments.length > 0 && { attachments }), + }; - const response = await client.post( - `/conversations/v3/conversations/threads/${threadId}/messages`, - commentData, - ); + const response = await client.post( + `/conversations/v3/conversations/threads/${threadId}/messages`, + commentData, + ); - return response; + return response; } diff --git a/hubspot/actions/conversations/sendThreadMessage.ts b/hubspot/actions/conversations/sendThreadMessage.ts index baa2699b1..b78b04fc7 100644 --- a/hubspot/actions/conversations/sendThreadMessage.ts +++ b/hubspot/actions/conversations/sendThreadMessage.ts @@ -2,192 +2,192 @@ import type { AppContext } from "../../mod.ts"; import { HubSpotClient } from "../../utils/client.ts"; export interface Attachment { - /** - * @title Attachment Type - * @description Type of attachment - */ - type: "FILE" | "QUICK_REPLIES" | "SOCIAL_MEDIA_METADATA"; - - /** - * @title File ID - * @description ID of the file (required for FILE type) - */ - fileId?: string; - - /** - * @title Name - * @description Name of the attachment - */ - name?: string; - - /** - * @title URL - * @description URL of the attachment - */ - url?: string; - - /** - * @title File Usage Type - * @description Usage type for the file - */ - fileUsageType?: string; + /** + * @title Attachment Type + * @description Type of attachment + */ + type: "FILE" | "QUICK_REPLIES" | "SOCIAL_MEDIA_METADATA"; + + /** + * @title File ID + * @description ID of the file (required for FILE type) + */ + fileId?: string; + + /** + * @title Name + * @description Name of the attachment + */ + name?: string; + + /** + * @title URL + * @description URL of the attachment + */ + url?: string; + + /** + * @title File Usage Type + * @description Usage type for the file + */ + fileUsageType?: string; } export interface DeliveryIdentifier { - /** - * @title Type - * @description Type of delivery identifier - */ - type: string; - - /** - * @title Value - * @description Value of the delivery identifier - */ - value: string; + /** + * @title Type + * @description Type of delivery identifier + */ + type: string; + + /** + * @title Value + * @description Value of the delivery identifier + */ + value: string; } export interface Recipient { - /** - * @title Delivery Identifiers - * @description Array of delivery identifiers - */ - deliveryIdentifiers?: DeliveryIdentifier[]; - - /** - * @title Actor ID - * @description ID of the actor - */ - actorId?: string; - - /** - * @title Name - * @description Name of the recipient - */ - name?: string; - - /** - * @title Delivery Identifier - * @description Single delivery identifier - */ - deliveryIdentifier?: DeliveryIdentifier; - - /** - * @title Recipient Field - * @description Field identifier for the recipient - */ - recipientField?: string; + /** + * @title Delivery Identifiers + * @description Array of delivery identifiers + */ + deliveryIdentifiers?: DeliveryIdentifier[]; + + /** + * @title Actor ID + * @description ID of the actor + */ + actorId?: string; + + /** + * @title Name + * @description Name of the recipient + */ + name?: string; + + /** + * @title Delivery Identifier + * @description Single delivery identifier + */ + deliveryIdentifier?: DeliveryIdentifier; + + /** + * @title Recipient Field + * @description Field identifier for the recipient + */ + recipientField?: string; } export interface Props { - /** - * @title Thread ID - * @description The ID of the conversation thread - */ - threadId: string; - - /** - * @title Message Text - * @description The text content of the message - */ - text: string; - - /** - * @title Sender Actor ID - * @description ID of the sender actor - */ - senderActorId: string; - - /** - * @title Channel ID - * @description The channel ID - */ - channelId: string; - - /** - * @title Channel Account ID - * @description The channel account ID - */ - channelAccountId: string; - - /** - * @title Recipients - * @description Array of recipients - */ - recipients?: Recipient[]; - - /** - * @title Subject - * @description Subject of the message - */ - subject?: string; - - /** - * @title Rich Text - * @description Rich text content (HTML) - */ - richText?: string; - - /** - * @title Attachments - * @description Array of attachments - */ - attachments?: Attachment[]; + /** + * @title Thread ID + * @description The ID of the conversation thread + */ + threadId: string; + + /** + * @title Message Text + * @description The text content of the message + */ + text: string; + + /** + * @title Sender Actor ID + * @description ID of the sender actor + */ + senderActorId: string; + + /** + * @title Channel ID + * @description The channel ID + */ + channelId: string; + + /** + * @title Channel Account ID + * @description The channel account ID + */ + channelAccountId: string; + + /** + * @title Recipients + * @description Array of recipients + */ + recipients?: Recipient[]; + + /** + * @title Subject + * @description Subject of the message + */ + subject?: string; + + /** + * @title Rich Text + * @description Rich text content (HTML) + */ + richText?: string; + + /** + * @title Attachments + * @description Array of attachments + */ + attachments?: Attachment[]; } export interface Client { - clientType: "HUBSPOT"; - integrationAppId: number; + clientType: "HUBSPOT"; + integrationAppId: number; } export interface Sender { - actorId: string; - name?: string; - senderField?: string; - deliveryIdentifier?: DeliveryIdentifier; + actorId: string; + name?: string; + senderField?: string; + deliveryIdentifier?: DeliveryIdentifier; } export interface MessageRecipient { - actorId: string; - name?: string; - deliveryIdentifier?: DeliveryIdentifier; - recipientField?: string; + actorId: string; + name?: string; + deliveryIdentifier?: DeliveryIdentifier; + recipientField?: string; } export interface FailureDetails { - errorMessageTokens: Record; - errorMessage?: string; + errorMessageTokens: Record; + errorMessage?: string; } export interface MessageStatus { - statusType: "SENT" | "FAILED" | "PENDING"; - failureDetails?: FailureDetails; + statusType: "SENT" | "FAILED" | "PENDING"; + failureDetails?: FailureDetails; } export interface MessageResponse { - type: "MESSAGE"; - id: string; - conversationsThreadId: string; - createdAt: string; - updatedAt?: string; - createdBy: string; - client: Client; - senders: Sender[]; - recipients: MessageRecipient[]; - archived: boolean; - text?: string; - richText?: string; - attachments: Attachment[]; - subject?: string; - truncationStatus: - | "NOT_TRUNCATED" - | "TRUNCATED_TO_MOST_RECENT_REPLY" - | "TRUNCATED"; - inReplyToId?: string; - status?: MessageStatus; - direction: "INCOMING" | "OUTGOING"; - channelId: string; - channelAccountId: string; + type: "MESSAGE"; + id: string; + conversationsThreadId: string; + createdAt: string; + updatedAt?: string; + createdBy: string; + client: Client; + senders: Sender[]; + recipients: MessageRecipient[]; + archived: boolean; + text?: string; + richText?: string; + attachments: Attachment[]; + subject?: string; + truncationStatus: + | "NOT_TRUNCATED" + | "TRUNCATED_TO_MOST_RECENT_REPLY" + | "TRUNCATED"; + inReplyToId?: string; + status?: MessageStatus; + direction: "INCOMING" | "OUTGOING"; + channelId: string; + channelAccountId: string; } /** @@ -195,40 +195,40 @@ export interface MessageResponse { * @description Send a message to a conversation thread */ export default async function sendThreadMessage( - props: Props, - _req: Request, - ctx: AppContext, + props: Props, + _req: Request, + ctx: AppContext, ): Promise { - const { - threadId, - text, - senderActorId, - channelId, - channelAccountId, - recipients = [], - subject, - richText, - attachments = [], - } = props; - - const client = new HubSpotClient(ctx); - - const messageData: Record = { - type: "MESSAGE", - text, - senderActorId, - channelId, - channelAccountId, - ...(richText && { richText }), - ...(attachments.length > 0 && { attachments }), - ...(recipients.length > 0 && { recipients }), - ...(subject && { subject }), - }; - - const response = await client.post( - `/conversations/v3/conversations/threads/${threadId}/messages`, - messageData, - ); - - return response; -} \ No newline at end of file + const { + threadId, + text, + senderActorId, + channelId, + channelAccountId, + recipients = [], + subject, + richText, + attachments = [], + } = props; + + const client = new HubSpotClient(ctx); + + const messageData: Record = { + type: "MESSAGE", + text, + senderActorId, + channelId, + channelAccountId, + ...(richText && { richText }), + ...(attachments.length > 0 && { attachments }), + ...(recipients.length > 0 && { recipients }), + ...(subject && { subject }), + }; + + const response = await client.post( + `/conversations/v3/conversations/threads/${threadId}/messages`, + messageData, + ); + + return response; +} diff --git a/hubspot/loaders/conversations/getChannelAccounts.ts b/hubspot/loaders/conversations/getChannelAccounts.ts index b1adf2682..02d177b2b 100644 --- a/hubspot/loaders/conversations/getChannelAccounts.ts +++ b/hubspot/loaders/conversations/getChannelAccounts.ts @@ -2,51 +2,51 @@ import type { AppContext } from "../../mod.ts"; import { HubSpotClient } from "../../utils/client.ts"; export interface Props { - /** - * @title Limit - * @description Maximum number of channel accounts to return (default: 20, max: 100) - */ - limit?: number; + /** + * @title Limit + * @description Maximum number of channel accounts to return (default: 20, max: 100) + */ + limit?: number; - /** - * @title After - * @description Cursor for pagination - use the 'after' value from the previous response - */ - after?: string; - /** - * @title Channel ID - * @description The ID of the channel to get channel accounts for - */ - channelId?: string; + /** + * @title After + * @description Cursor for pagination - use the 'after' value from the previous response + */ + after?: string; + /** + * @title Channel ID + * @description The ID of the channel to get channel accounts for + */ + channelId?: string; } export interface DeliveryIdentifier { - type: string; - value: string; + type: string; + value: string; } export interface ChannelAccount { - createdAt: string; - archivedAt?: string; - archived: boolean; - authorized: boolean; - name: string; - active: boolean; - deliveryIdentifier: DeliveryIdentifier; - id: string; - inboxId: string; - channelId: string; + createdAt: string; + archivedAt?: string; + archived: boolean; + authorized: boolean; + name: string; + active: boolean; + deliveryIdentifier: DeliveryIdentifier; + id: string; + inboxId: string; + channelId: string; } export interface ChannelAccountsResponse { - total: number; - paging: { - next?: { - link: string; - after: string; - }; + total: number; + paging: { + next?: { + link: string; + after: string; }; - results: ChannelAccount[]; + }; + results: ChannelAccount[]; } /** @@ -54,46 +54,46 @@ export interface ChannelAccountsResponse { * @description Retrieve a list of channel accounts from HubSpot Conversations API */ export default async function getChannelAccounts( - props: Props, - _req: Request, - ctx: AppContext, + props: Props, + _req: Request, + ctx: AppContext, ): Promise { - const { limit = 20, after, channelId } = props; + const { limit = 20, after, channelId } = props; - try { - const client = new HubSpotClient(ctx); + try { + const client = new HubSpotClient(ctx); - const searchParams: Record< - string, - string | number | boolean | undefined - > = {}; + const searchParams: Record< + string, + string | number | boolean | undefined + > = {}; - if (limit) { - searchParams.limit = Math.min(limit, 100); - } - if (after) { - searchParams.after = after; - } - if (channelId) { - searchParams.channelId = channelId; - } + if (limit) { + searchParams.limit = Math.min(limit, 100); + } + if (after) { + searchParams.after = after; + } + if (channelId) { + searchParams.channelId = channelId; + } - const response = await client.get( - "/conversations/v3/conversations/channel-accounts", - searchParams, - ); + const response = await client.get( + "/conversations/v3/conversations/channel-accounts", + searchParams, + ); - return { - total: response.total || 0, - paging: response.paging || { next: undefined }, - results: response.results || [], - }; - } catch (error) { - console.error("Error fetching channel accounts:", error); - return { - total: 0, - paging: { next: undefined }, - results: [], - }; - } + return { + total: response.total || 0, + paging: response.paging || { next: undefined }, + results: response.results || [], + }; + } catch (error) { + console.error("Error fetching channel accounts:", error); + return { + total: 0, + paging: { next: undefined }, + results: [], + }; + } }