From 95cc30f31752937a85bece207875bdf9c943c61e Mon Sep 17 00:00:00 2001
From: ryanbetts <84613835+depatchedmode@users.noreply.github.com>
Date: Tue, 6 Feb 2024 08:52:54 -0800
Subject: [PATCH 1/2] refactor: antitheft and message validation to modules
+ moved antitheft from data -> modules
+ moved validateMessage from data -> modules
+ renamed validateMessage to validateFrameMessage to start alignment w/ frame.js API
---
api/index.js | 6 +++---
{src/data => modules}/antitheft.js | 0
src/data/message.js => modules/validateFrameMessage.js | 6 +-----
3 files changed, 4 insertions(+), 8 deletions(-)
rename {src/data => modules}/antitheft.js (100%)
rename src/data/message.js => modules/validateFrameMessage.js (88%)
diff --git a/api/index.js b/api/index.js
index 74bf0b1..36154b3 100644
--- a/api/index.js
+++ b/api/index.js
@@ -4,8 +4,8 @@ import { parseRequest, objectToURLSearchParams } from '../modules/utils';
import buildButtons from '../modules/buildButtons';
import buildInputs from '../modules/buildInputs';
import getTargetFrame from '../modules/getTargetFrame';
-import { validateMessage } from '../src/data/message';
-import { isFrameStolen } from '../src/data/antitheft';
+import validateFrameMessage from '../modules/validateFrameMessage';
+import { isFrameStolen } from '../modules/antitheft';
export default async (req, context) => {
try {
@@ -17,7 +17,7 @@ export default async (req, context) => {
if (payload) {
payload.referringFrame = from;
- payload.validData = await validateMessage(payload.trustedData.messageBytes);
+ payload.validData = await validateFrameMessage(payload.trustedData.messageBytes);
}
if (payload?.validData) {
diff --git a/src/data/antitheft.js b/modules/antitheft.js
similarity index 100%
rename from src/data/antitheft.js
rename to modules/antitheft.js
diff --git a/src/data/message.js b/modules/validateFrameMessage.js
similarity index 88%
rename from src/data/message.js
rename to modules/validateFrameMessage.js
index 5432bb0..9987c24 100644
--- a/src/data/message.js
+++ b/modules/validateFrameMessage.js
@@ -1,4 +1,4 @@
-const validateMessage = async(messageBytes) => {
+export default async(messageBytes) => {
if (!process.env.FARCASTER_HUB) throw new Error("FARCASTER_HUB is not set");
if (!messageBytes) throw new Error("No data provided");
@@ -15,7 +15,3 @@ const validateMessage = async(messageBytes) => {
})
.catch(error => console.error('Error:', error));
}
-
-export {
- validateMessage,
-}
From f18c5ff2507bc0b592e496a816789d8d19ce311a Mon Sep 17 00:00:00 2001
From: ryanbetts <84613835+depatchedmode@users.noreply.github.com>
Date: Tue, 6 Feb 2024 12:58:33 -0800
Subject: [PATCH 2/2] refactor: start using frames.js
---
api/index.js | 63 +++++++++++++++------------------
api/og-image.js | 4 +--
api/redirect.js | 6 ++--
modules/antitheft.js | 18 ++++++++--
modules/getTargetFrame.js | 29 ++++++++-------
modules/safeDecode.js | 13 -------
modules/sanitize.js | 12 +++++++
modules/validateFrameMessage.js | 17 ---------
package.json | 1 +
src/data/framer.js | 3 +-
src/frames/count.js | 16 ++++-----
11 files changed, 87 insertions(+), 95 deletions(-)
delete mode 100644 modules/safeDecode.js
create mode 100644 modules/sanitize.js
delete mode 100644 modules/validateFrameMessage.js
diff --git a/api/index.js b/api/index.js
index 36154b3..1a6fae0 100644
--- a/api/index.js
+++ b/api/index.js
@@ -1,72 +1,65 @@
+import { getFrameMessage } from "frames.js"
import landingPage from '../src/landing-page';
-import frames from '../src/frames';
import { parseRequest, objectToURLSearchParams } from '../modules/utils';
import buildButtons from '../modules/buildButtons';
import buildInputs from '../modules/buildInputs';
import getTargetFrame from '../modules/getTargetFrame';
-import validateFrameMessage from '../modules/validateFrameMessage';
-import { isFrameStolen } from '../modules/antitheft';
export default async (req, context) => {
try {
const requestURL = new URL(req.url);
const payload = await parseRequest(req);
- let from = requestURL.searchParams.get('frame');
- let buttonId = null;
- let frameIsStolen = false;
+ const frameMessage = payload ? await getFrameMessage(payload, {
+ hubHttpUrl: process.env.FARCASTER_HUB
+ }) : {};
- if (payload) {
- payload.referringFrame = from;
- payload.validData = await validateFrameMessage(payload.trustedData.messageBytes);
- }
-
- if (payload?.validData) {
- buttonId = payload.validData.data.frameActionBody.buttonIndex;
- frameIsStolen = await isFrameStolen(payload);
- }
+ // extending the frames.js frameMessage object with a few things
+ // we require
+ // TODO: see about refactoring this more towards the frames.js style
+ frameMessage.from = requestURL.searchParams.get('frame');
+ frameMessage.requestURL = payload?.untrustedData.url;
- const { targetFrameSrc, targetFrameName, redirectUrl } = getTargetFrame(from, buttonId, frames);
+ const { targetFrame, redirectURL } = await getTargetFrame(frameMessage);
- if (redirectUrl) {
- return await respondWithRedirect(redirectUrl);
- } else if (frameIsStolen) {
- return await respondWithFrame('stolen', frames['stolen'], payload);
- } else if (targetFrameSrc) {
- return await respondWithFrame(targetFrameName, targetFrameSrc, payload);
+ if (redirectURL) {
+ return await respondWithRedirect(redirectURL);
+ } else if (targetFrame) {
+ return await respondWithFrame(targetFrame, frameMessage);
} else {
- console.error(`Unknown frame requested: ${targetFrameName}`);
+ console.error(`Unknown frame requested: ${targetFrame.name}`);
}
} catch (error) {
console.error(`Error processing request: ${error}`);
}
};
-const respondWithRedirect = (redirectUrl) => {
- const internalRedirectUrl = new URL(`${process.env.URL}/redirect`)
- internalRedirectUrl.searchParams.set('redirectUrl',redirectUrl);
+const respondWithRedirect = (redirectURL) => {
+ const internalRedirectURL = new URL(`${process.env.URL}/redirect`)
+ internalRedirectURL.searchParams.set('redirectURL',redirectURL);
return new Response('
redirect
',
{
status: 302,
headers: {
- 'Location': internalRedirectUrl,
+ 'Location': internalRedirectURL,
},
}
);
}
-const respondWithFrame = async (targetFrameName, targetFrameSrc, payload) => {
+const respondWithFrame = async (targetFrame, frameMessage) => {
const searchParams = {
- targetFrameName,
- payload
+ targetFrameName: targetFrame.name,
+ frameMessage
}
+
const host = process.env.URL;
const frameContent = {
- image: targetFrameSrc.image ?
- `${host}/${targetFrameSrc.image}` :
+ image: targetFrame.image ?
+ `${host}/${targetFrame.image}` :
`${host}/og-image?${objectToURLSearchParams(searchParams)}` || '',
- buttons: targetFrameSrc.buttons ? buildButtons(targetFrameSrc.buttons) : '',
- inputs: targetFrameSrc.inputs ? buildInputs(targetFrameSrc.inputs) : '',
- postURL: `${host}/?frame=${targetFrameName}`
+ buttons: targetFrame.buttons ? buildButtons(targetFrame.buttons) : '',
+ inputs: targetFrame.inputs ? buildInputs(targetFrame.inputs) : '',
+ postURL: `${host}/?frame=${targetFrame.name}`
};
return new Response(await landingPage(frameContent),
diff --git a/api/og-image.js b/api/og-image.js
index 9ac8b33..232fbde 100644
--- a/api/og-image.js
+++ b/api/og-image.js
@@ -8,8 +8,8 @@ import { URLSearchParamsToObject } from '../modules/utils';
export default async (req, context) => {
const url = new URL(req.url);
const params = URLSearchParamsToObject(url.searchParams);
- const targetFrameSrc = frames[params.targetFrameName];
- const markup = await targetFrameSrc.build(params.payload);
+ const targetFrame = frames[params.targetFrameName];
+ const markup = await targetFrame.build(params.frameMessage);
const svg = await satori(
html(markup),
diff --git a/api/redirect.js b/api/redirect.js
index d061bdb..c89f67c 100644
--- a/api/redirect.js
+++ b/api/redirect.js
@@ -2,14 +2,14 @@
// Redirects to an external URL based on buttonIndex parameter. Used to work
// around the same origin policy on frames, which is being removed soon.
export default async (req, context) => {
- const requestUrl = new URL(req.url);
- const redirectUrl = requestUrl.searchParams.get('redirectUrl');
+ const requestURL = new URL(req.url);
+ const redirectURL = requestURL.searchParams.get('redirectURL');
return new Response('redirect
',
{
status: 302,
headers: {
- 'Location': redirectUrl,
+ 'Location': redirectURL,
},
}
);
diff --git a/modules/antitheft.js b/modules/antitheft.js
index 5557b4c..d196a32 100644
--- a/modules/antitheft.js
+++ b/modules/antitheft.js
@@ -1,4 +1,5 @@
import { getStore } from '@netlify/blobs';
+import frame from '../api/frame';
// Utility functions to abstract the fetching and setting operations
const fetchData = async (key) => {
@@ -50,14 +51,25 @@ const removeBoundCast = (castHash) => removeFromList(getBoundCasts, setBoundCast
// 1. The castAuthorID is in boundAccounts.
// 2. The castHash is in boundCasts.
// 3. Both boundCasts & boundAccounts are empty.
-const isFrameStolen = async (payload) => {
- const { fid: castAuthorID, hash: castHash } = payload.validData.data.frameActionBody.castId;
+const isFrameStolen = async (frameMessage) => {
+ console.log('isFrameStolen', frameMessage);
+ const { castId, requestURL } = frameMessage;
+ if (!castId || !requestURL) {
+ console.log('isFrameStolen:quickExit', castId, requestURL);
+ return false;
+ }
+
+ const { fid: castAuthorID, hash: castHash } = castId;
const boundCasts = await getBoundCasts();
const boundAccounts = await getBoundAccounts();
const isAuthorAllowed = boundAccounts.includes(castAuthorID) || boundAccounts.length === 0;
const isCastAllowed = boundCasts.includes(castHash) || boundCasts.length === 0;
- const isFirstParty = payload.untrustedData.url.indexOf(process.env.URL) > -1;
+ const isFirstParty = requestURL ? requestURL.indexOf(process.env.URL) > -1 : true;
+
+ console.log('isAuthorAllowed', isAuthorAllowed, castAuthorID, boundAccounts);
+ console.log('isCastAllowed', isCastAllowed, castHash, boundCasts);
+ console.log('isFirstParty', isFirstParty);
return !isFirstParty || !isAuthorAllowed || !isCastAllowed;
};
diff --git a/modules/getTargetFrame.js b/modules/getTargetFrame.js
index 0f6778f..90a8907 100644
--- a/modules/getTargetFrame.js
+++ b/modules/getTargetFrame.js
@@ -1,18 +1,23 @@
const DEFAULT_FRAME = 'poster';
+import frames from '../src/frames';
+import { isFrameStolen } from './antitheft';
-export default (name, buttonId, frames) => {
+export default async (frameMessage) => {
+ const frameIsStolen = await isFrameStolen(frameMessage);
let targetFrameName = DEFAULT_FRAME;
- let redirectUrl = null;
- if (name && buttonId) {
- const originFrame = frames[name];
- const button = originFrame.buttons[buttonId-1];
+ let redirectURL = null;
+ console.log('getTargetFrame:frameIsStolen', frameIsStolen);
+ if (frameIsStolen) {
+ targetFrameName = 'stolen';
+ } else if (frameMessage.buttonIndex) {
+ const originFrame = frames[frameMessage.from];
+ const button = originFrame.buttons[frameMessage.buttonIndex-1];
targetFrameName = button.goTo;
- redirectUrl = button.url;
- }
- const targetFrameSrc = frames[targetFrameName];
+ redirectURL = button.url;
+ }
+
return {
- targetFrameSrc,
- targetFrameName,
- redirectUrl
- };
+ targetFrame: frames[targetFrameName],
+ redirectURL
+ }
}
\ No newline at end of file
diff --git a/modules/safeDecode.js b/modules/safeDecode.js
deleted file mode 100644
index be210ef..0000000
--- a/modules/safeDecode.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import { JSDOM } from 'jsdom';
-import DOMPurify from 'dompurify';
-
-export default (inputText) => {
- try {
- const decodedInputText = atob(inputText);
- const window = new JSDOM('').window;
- const purify = DOMPurify(window);
- return purify.sanitize(decodedInputText);
- } catch {
- throw new Error(`That ain't no encoded string mfr`)
- }
-}
\ No newline at end of file
diff --git a/modules/sanitize.js b/modules/sanitize.js
new file mode 100644
index 0000000..de2dc6b
--- /dev/null
+++ b/modules/sanitize.js
@@ -0,0 +1,12 @@
+import { JSDOM } from 'jsdom';
+import DOMPurify from 'dompurify';
+
+export default (text) => {
+ try {
+ const window = new JSDOM('').window;
+ const purify = DOMPurify(window);
+ return purify.sanitize(text);
+ } catch {
+ throw new Error(`That ain't no string mfr`)
+ }
+}
\ No newline at end of file
diff --git a/modules/validateFrameMessage.js b/modules/validateFrameMessage.js
deleted file mode 100644
index 9987c24..0000000
--- a/modules/validateFrameMessage.js
+++ /dev/null
@@ -1,17 +0,0 @@
-export default async(messageBytes) => {
- if (!process.env.FARCASTER_HUB) throw new Error("FARCASTER_HUB is not set");
- if (!messageBytes) throw new Error("No data provided");
-
- return await fetch(`${process.env.FARCASTER_HUB}/v1/validateMessage`,{
- method: "POST",
- headers: {
- "Content-Type": "application/octet-stream"
- },
- body: Buffer.from(messageBytes, 'hex'),
- })
- .then(async(response) => {
- const parsedResponse = await response.json();
- return parsedResponse.valid ? parsedResponse.message : false;
- })
- .catch(error => console.error('Error:', error));
-}
diff --git a/package.json b/package.json
index cf35eff..12317f7 100644
--- a/package.json
+++ b/package.json
@@ -5,6 +5,7 @@
"dependencies": {
"@netlify/blobs": "^6.4.2",
"dompurify": "^3.0.8",
+ "frames.js": "^0.1.1",
"jsdom": "^24.0.0",
"satori": "^0.10.11",
"satori-html": "^0.3.2",
diff --git a/src/data/framer.js b/src/data/framer.js
index 0af9434..2c62b39 100644
--- a/src/data/framer.js
+++ b/src/data/framer.js
@@ -1,5 +1,6 @@
import { getStore } from '@netlify/blobs';
import { getUsername } from './username';
+import sanitize from '../../modules/sanitize';
const getFramer = async() => {
const store = getStore('gameState');
@@ -15,7 +16,7 @@ const getFramer = async() => {
const setFramer = async(fid, taunt) => {
const store = getStore('gameState');
await store.set('framer', fid);
- await store.set('taunt', taunt);
+ await store.set('taunt', sanitize(taunt));
}
export {
diff --git a/src/frames/count.js b/src/frames/count.js
index 075166d..6a6f6bc 100644
--- a/src/frames/count.js
+++ b/src/frames/count.js
@@ -1,16 +1,14 @@
import mainLayout from '../layouts/main';
import { getFramer, setFramer } from '../data/framer';
import { getCount, incrementCount } from '../data/count';
-import safeDecode from '../../modules/safeDecode';
-const build = async (payload) => {
+const build = async (frameMessage) => {
let count = await getCount();
- const validData = payload?.validData;
- if (payload.validData && payload.referringFrame == 'count') {
+ if (frameMessage.from == 'count') {
count = await incrementCount(count);
- const tauntInput = validData.data.frameActionBody.inputText;
- await setFramer(validData.data.fid, tauntInput);
+ const tauntInput = frameMessage.inputText;
+ await setFramer(frameMessage.requesterFid, tauntInput);
}
const { username, taunt } = await getFramer() || '';
@@ -18,7 +16,7 @@ const build = async (payload) => {
let tauntOutput;
tauntOutput = taunt ? `
- "${safeDecode(taunt)}"
+ "${taunt}"
` : '';
@@ -35,7 +33,7 @@ const build = async (payload) => {
`;
- return mainLayout(payload, frameHTML);
+ return mainLayout(frameMessage, frameHTML);
}
export const inputs = [
@@ -57,7 +55,7 @@ export const buttons = [
]
export default {
- name: 'stolen',
+ name: 'count',
build,
buttons,
inputs