feat: new twitter plugin with example for inputs in the sidepanel

This commit is contained in:
Codetrauma
2025-07-20 11:04:48 -07:00
parent 0b65e52b15
commit cc4d74df0a
11 changed files with 491 additions and 16 deletions

View File

@@ -4,6 +4,7 @@
"steps": [
{
"title": "Visit Spotify webplayer",
"cta": "Go",
"action": "start"
},
@@ -34,4 +35,4 @@
"method": "GET"
}
]
}
}

View File

@@ -1,14 +0,0 @@
{
"compilerOptions": {
"lib": [],
"types": [
"@extism/js-pdk"
],
"noEmit": true,
"resolveJsonModule": true,
"esModuleInterop": true,
},
"include": [
"src/**/*"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -0,0 +1,69 @@
{
"title": "Twitter Profile",
"description": "Notarize ownership of a twitter profile",
"steps": [
{
"title": "Add Custom Note",
"description": "Enter a custom note to include with your Twitter profile notarization",
"cta": "Continue with Note",
"action": "collectUserNote",
"inputs": [
{
"name": "userNote",
"label": "Custom Note",
"type": "textarea",
"placeholder": "Enter any custom message or note for this notarization...",
"required": false,
"defaultValue": "Verified my Twitter profile"
},
{
"name": "notarizeReason",
"label": "Reason for Notarization",
"type": "select",
"required": true,
"options": [
{ "value": "identity_verification", "label": "Identity Verification" },
{ "value": "account_proof", "label": "Account Ownership Proof" },
{ "value": "profile_backup", "label": "Profile Backup" },
{ "value": "other", "label": "Other" }
]
}
]
},
{
"title": "Navigate to Twitter/X",
"description": "Ensure you're logged in to Twitter/X to verify your profile",
"cta": "Check Page",
"action": "start"
},
{
"title": "Collect Credentials",
"description": "Gathering your Twitter authentication cookies and headers",
"cta": "Collect Data",
"action": "two"
},
{
"title": "Notarize Twitter Profile",
"description": "Create a notarized proof of your Twitter profile",
"cta": "Notarize",
"action": "three",
"prover": true
}
],
"hostFunctions": [
"redirect",
"notarize"
],
"cookies": [
"https://api.x.com/1.1/account/settings.json"
],
"headers": [
"https://api.x.com/1.1/account/settings.json"
],
"requests": [
{
"url": "https://api.x.com/1.1/account/settings.json",
"method": "GET"
}
]
}

View File

@@ -33,4 +33,4 @@ async function build() {
}
}
build();
build();

View File

@@ -0,0 +1,143 @@
{
"name": "twitter_profile",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "twitter_profile",
"version": "1.0.0",
"license": "MIT",
"devDependencies": {
"@extism/js-pdk": "^1.0.1",
"esbuild": "^0.19.6",
"typescript": "^5.3.2"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz",
"integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@extism/js-pdk": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@extism/js-pdk/-/js-pdk-1.0.1.tgz",
"integrity": "sha512-YJWfHGeOuJnQw4V8NPNHvbSr6S8iDd2Ga6VEukwlRP7tu62ozTxIgokYw8i+rajD/16zz/gK0KYARBpm2qPAmQ==",
"dev": true
},
"node_modules/esbuild": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
"integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
"dev": true,
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.19.12",
"@esbuild/android-arm": "0.19.12",
"@esbuild/android-arm64": "0.19.12",
"@esbuild/android-x64": "0.19.12",
"@esbuild/darwin-arm64": "0.19.12",
"@esbuild/darwin-x64": "0.19.12",
"@esbuild/freebsd-arm64": "0.19.12",
"@esbuild/freebsd-x64": "0.19.12",
"@esbuild/linux-arm": "0.19.12",
"@esbuild/linux-arm64": "0.19.12",
"@esbuild/linux-ia32": "0.19.12",
"@esbuild/linux-loong64": "0.19.12",
"@esbuild/linux-mips64el": "0.19.12",
"@esbuild/linux-ppc64": "0.19.12",
"@esbuild/linux-riscv64": "0.19.12",
"@esbuild/linux-s390x": "0.19.12",
"@esbuild/linux-x64": "0.19.12",
"@esbuild/netbsd-x64": "0.19.12",
"@esbuild/openbsd-x64": "0.19.12",
"@esbuild/sunos-x64": "0.19.12",
"@esbuild/win32-arm64": "0.19.12",
"@esbuild/win32-ia32": "0.19.12",
"@esbuild/win32-x64": "0.19.12"
}
},
"node_modules/typescript": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
}
},
"dependencies": {
"@esbuild/darwin-arm64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz",
"integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==",
"dev": true,
"optional": true
},
"@extism/js-pdk": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@extism/js-pdk/-/js-pdk-1.0.1.tgz",
"integrity": "sha512-YJWfHGeOuJnQw4V8NPNHvbSr6S8iDd2Ga6VEukwlRP7tu62ozTxIgokYw8i+rajD/16zz/gK0KYARBpm2qPAmQ==",
"dev": true
},
"esbuild": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
"integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
"dev": true,
"requires": {
"@esbuild/aix-ppc64": "0.19.12",
"@esbuild/android-arm": "0.19.12",
"@esbuild/android-arm64": "0.19.12",
"@esbuild/android-x64": "0.19.12",
"@esbuild/darwin-arm64": "0.19.12",
"@esbuild/darwin-x64": "0.19.12",
"@esbuild/freebsd-arm64": "0.19.12",
"@esbuild/freebsd-x64": "0.19.12",
"@esbuild/linux-arm": "0.19.12",
"@esbuild/linux-arm64": "0.19.12",
"@esbuild/linux-ia32": "0.19.12",
"@esbuild/linux-loong64": "0.19.12",
"@esbuild/linux-mips64el": "0.19.12",
"@esbuild/linux-ppc64": "0.19.12",
"@esbuild/linux-riscv64": "0.19.12",
"@esbuild/linux-s390x": "0.19.12",
"@esbuild/linux-x64": "0.19.12",
"@esbuild/netbsd-x64": "0.19.12",
"@esbuild/openbsd-x64": "0.19.12",
"@esbuild/sunos-x64": "0.19.12",
"@esbuild/win32-arm64": "0.19.12",
"@esbuild/win32-ia32": "0.19.12",
"@esbuild/win32-x64": "0.19.12"
}
},
"typescript": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
"dev": true
}
}
}

View File

@@ -0,0 +1,16 @@
declare module 'main' {
// Extism exports take no params and return an I32
export function start(): I32;
export function two(): I32;
export function parseTwitterResp(): I32;
export function three(): I32;
export function config(): I32;
export function collectUserNote(): I32;
}
declare module 'extism:host' {
interface user {
redirect(ptr: I64): void;
notarize(ptr: I64): I64;
}
}

View File

@@ -0,0 +1,206 @@
import icon from '../assets/icon.png';
import config_json from '../config.json';
import { redirect, notarize, outputJSON, getCookiesByHost, getHeadersByHost } from './utils/hf.js';
/**
* Plugin configuration with added input field step
* This configurations defines the plugin, most importantly:
* * the different steps (now including a user input step)
* * the user data (headers, cookies) it will access
* * the web requests it will query (or notarize)
*/
export function config() {
outputJSON({
...config_json,
icon: icon,
});
}
function isValidHost(urlString: string) {
const url = new URL(urlString);
return url.hostname === 'twitter.com' || url.hostname === 'x.com';
}
export function collectUserNote() {
const inputString = Host.inputString();
var inputData;
try {
if (inputString && inputString.trim() !== '') {
inputData = JSON.parse(inputString);
} else {
inputData = {};
}
} catch (e) {
inputData = {};
}
// Store the user's input to pass to subsequent steps
outputJSON({
userNote: inputData.userNote || 'No note provided',
notarizeReason: inputData.notarizeReason || 'identity_verification',
timestamp: new Date().toISOString()
});
}
/**
* Step 2: Implementation of the original start step
* Now receives the user input data from the previous step
*/
export function start() {
const inputString = Host.inputString();
var previousData = {};
// Handle case where input might be empty or invalid JSON
try {
if (inputString && inputString.trim() !== '') {
previousData = JSON.parse(inputString);
}
} catch (e) {
previousData = {};
}
console.log('Start step - parsed data:', previousData);
if (!isValidHost(Config.get('tabUrl'))) {
redirect('https://x.com');
outputJSON(false);
return;
}
// Pass along the user data to the next step
outputJSON({
...previousData,
pageValidated: true
});
}
/**
* Step 3: Implementation of the original "two" step
* This step collects and validates authentication cookies and headers for 'api.x.com'.
* If all required information is present, it creates the request object.
*/
export function two() {
const inputString = Host.inputString();
var previousData = {};
// Handle case where input might be empty or invalid JSON
try {
if (inputString && inputString.trim() !== '') {
previousData = JSON.parse(inputString);
}
} catch (e) {
console.log('Two step - failed to parse input, using empty object:', e);
previousData = {};
}
console.log('Two step - parsed data:', previousData);
const cookies = getCookiesByHost('https://api.x.com/1.1/account/settings.json');
const headers = getHeadersByHost('https://api.x.com/1.1/account/settings.json');
if (
!cookies.auth_token ||
!cookies.ct0 ||
!headers['x-csrf-token'] ||
!headers['authorization'] ||
!headers['x-client-transaction-id']
) {
outputJSON(false);
return;
}
const cookieString = Object.entries(cookies).map(([name, value]) => `${name}=${value}`).join('; ')
outputJSON({
...previousData, // Include user input data
requestData: {
url: 'https://api.x.com/1.1/account/settings.json',
method: 'GET',
headers: {
Cookie: cookieString,
'x-csrf-token': headers['x-csrf-token'],
'x-client-transaction-id': headers['x-client-transaction-id'],
Host: 'api.x.com',
authorization: headers.authorization,
'Accept-Encoding': 'identity',
Connection: 'close',
},
secretHeaders: [
`x-csrf-token: ${headers['x-csrf-token']}`,
`x-client-transaction-id: ${headers['x-client-transaction-id']}`,
`cookie: ${cookieString}`,
`authorization: ${headers.authorization}`,
],
}
});
}
/**
* This method is used to parse the Twitter response and specify what information is revealed (i.e. **not** redacted)
* This method is optional in the notarization request. When it is not specified nothing is redacted.
*
* In this example it locates the `screen_name` and excludes that range from the revealed response.
*/
export function parseTwitterResp() {
const bodyString = Host.inputString();
const params = JSON.parse(bodyString);
if (params.screen_name) {
const revealed = `"screen_name":"${params.screen_name}"`;
const selectionStart = bodyString.indexOf(revealed);
const selectionEnd =
selectionStart + revealed.length;
const secretResps = [
bodyString.substring(0, selectionStart),
bodyString.substring(selectionEnd, bodyString.length),
];
outputJSON(secretResps);
} else {
outputJSON(false);
}
}
/**
* Step 4: calls the `notarize` host function (original "three" step)
* Now includes the user's custom note and reason in the metadata
*/
export function three() {
const inputString = Host.inputString();
console.log('Three step - raw input string:', inputString);
var allData = {};
// Handle case where input might be empty or invalid JSON
try {
if (inputString && inputString.trim() !== '') {
allData = JSON.parse(inputString);
}
} catch (e) {
allData = {};
}
console.log('Three step - parsed data:', allData);
if (!allData.requestData) {
outputJSON(false);
return;
}
// Include user input in the notarization metadata
const notarizationRequest = {
...allData.requestData,
getSecretResponse: 'parseTwitterResp'
};
// Add metadata directly to the request
notarizationRequest.metadata = {
userNote: allData.userNote,
notarizeReason: allData.notarizeReason,
};
const id = notarize(notarizationRequest);
outputJSON(id);
}

View File

@@ -0,0 +1,53 @@
function redirect(url) {
const { redirect } = Host.getFunctions();
const mem = Memory.fromString(url);
redirect(mem.offset);
}
function notarize(options) {
const { notarize } = Host.getFunctions();
const mem = Memory.fromString(JSON.stringify(options));
const idOffset = notarize(mem.offset);
const id = Memory.find(idOffset).readString();
return id;
}
function outputJSON(json) {
Host.outputString(
JSON.stringify(json),
);
}
function getCookiesByHost(hostname) {
const cookies = JSON.parse(Config.get('cookies'));
if (!cookies[hostname]) throw new Error(`cannot find cookies for ${hostname}`);
return cookies[hostname];
}
function getHeadersByHost(hostname) {
const headers = JSON.parse(Config.get('headers'));
if (!headers[hostname]) throw new Error(`cannot find headers for ${hostname}`);
return headers[hostname];
}
function getLocalStorageByHost(hostname) {
const localStorage = JSON.parse(Config.get('localStorage'));
if (!localStorage[hostname]) throw new Error(`cannot find local storage for ${hostname}`);
return localStorage[hostname];
}
function getSessionStorageByHost(hostname) {
const sessionStorage = JSON.parse(Config.get('sessionStorage'));
if (!sessionStorage[hostname]) throw new Error(`cannot find session storage for ${hostname}`);
return sessionStorage[hostname];
}
module.exports = {
redirect,
notarize,
outputJSON,
getCookiesByHost,
getHeadersByHost,
getLocalStorageByHost,
getSessionStorageByHost,
};

1
src/index.d.ts vendored
View File

@@ -5,6 +5,7 @@ declare module 'main' {
export function parseTwitterResp(): I32;
export function three(): I32;
export function config(): I32;
export function collectUserNote(): I32;
}
declare module 'extism:host' {