Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions apify/loaders/getActorRun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}

Expand Down
13 changes: 10 additions & 3 deletions figma/README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
});
```
```

The OAuth flow will handle token management automatically, including refresh when needed.
84 changes: 84 additions & 0 deletions figma/actions/oauth/callback.ts
Original file line number Diff line number Diff line change
@@ -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;

Comment on lines +31 to +33
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Security: validate OAuth state

There’s no state verification, which weakens CSRF protections. Validate the state returned on the callback against the one issued in start.

Example patch:

-  const finalRedirectUri = redirectUri ||
-    new URL("/oauth/callback", req.url).href;
+  const finalRedirectUri = redirectUri ||
+    new URL("/oauth/callback", req.url).href;
+  const stateFromReq = new URL(req.url).searchParams.get("state");
+  // TODO: compare stateFromReq with a previously stored state value
+  // e.g., from props or server-side storage. Abort if mismatch.
+  // if (!stateFromReq || stateFromReq !== expectedState) {
+  //   throw new Error("Invalid OAuth state");
+  // }

Also consider adding state to Props or retrieving expected state from ctx.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const finalRedirectUri = redirectUri ||
new URL("/oauth/callback", req.url).href;
const finalRedirectUri = redirectUri ||
new URL("/oauth/callback", req.url).href;
const stateFromReq = new URL(req.url).searchParams.get("state");
// TODO: compare stateFromReq with a previously stored state value
// e.g., from props or server-side storage. Abort if mismatch.
// if (!stateFromReq || stateFromReq !== expectedState) {
// throw new Error("Invalid OAuth state");
// }

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}`,
},
});
Comment on lines +37 to +46
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Increase token endpoint compatibility

Include client_id and client_secret in the body and set Accept; some providers require form-like params or both Basic and body credentials.

-    const response = await client["POST /token"]({}, {
-      body: {
-        code,
-        redirect_uri: finalRedirectUri,
-        grant_type: "authorization_code",
-      },
-      headers: {
-        "Authorization": `Basic ${credentials}`,
-      },
-    });
+    const response = await client["POST /token"]({}, {
+      body: {
+        code,
+        redirect_uri: finalRedirectUri,
+        grant_type: "authorization_code",
+        client_id: clientId,
+        client_secret: clientSecret,
+      },
+      headers: {
+        "Authorization": `Basic ${credentials}`,
+        "Accept": "application/json",
+      },
+    });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const response = await client["POST /token"]({}, {
body: {
code,
redirect_uri: finalRedirectUri,
grant_type: "authorization_code",
},
headers: {
"Authorization": `Basic ${credentials}`,
},
});
const response = await client["POST /token"]({}, {
body: {
code,
redirect_uri: finalRedirectUri,
grant_type: "authorization_code",
client_id: clientId,
client_secret: clientSecret,
},
headers: {
"Authorization": `Basic ${credentials}`,
"Accept": "application/json",
},
});
🤖 Prompt for AI Agents
In figma/actions/oauth/callback.ts around lines 37 to 46, the token request only
sends code, redirect_uri and uses Basic auth; to maximize compatibility include
client_id and client_secret in the request body and set appropriate headers. Add
client_id and client_secret to the body payload alongside
code/redirect_uri/grant_type, and set headers to include "Accept":
"application/json" and "Content-Type": "application/x-www-form-urlencoded" (or
serialise the body as form-encoded) while retaining the existing Authorization
header so providers that require both Basic and body credentials are supported.


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" };
}
}
Loading
Loading