mirror of
https://github.com/tlsnotary/tlsn-plugin-boilerplate.git
synced 2026-01-08 20:38:05 -05:00
feat: new twitter plugin with example for inputs in the sidepanel
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
"steps": [
|
||||
{
|
||||
"title": "Visit Spotify webplayer",
|
||||
|
||||
"cta": "Go",
|
||||
"action": "start"
|
||||
},
|
||||
@@ -34,4 +35,4 @@
|
||||
"method": "GET"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [],
|
||||
"types": [
|
||||
"@extism/js-pdk"
|
||||
],
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
BIN
examples/twitter_profile_ts_with_inputs/assets/icon.png
Normal file
BIN
examples/twitter_profile_ts_with_inputs/assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
69
examples/twitter_profile_ts_with_inputs/config.json
Normal file
69
examples/twitter_profile_ts_with_inputs/config.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -33,4 +33,4 @@ async function build() {
|
||||
}
|
||||
}
|
||||
|
||||
build();
|
||||
build();
|
||||
143
examples/twitter_profile_ts_with_inputs/package-lock.json
generated
Normal file
143
examples/twitter_profile_ts_with_inputs/package-lock.json
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
16
examples/twitter_profile_ts_with_inputs/src/index.d.ts
vendored
Normal file
16
examples/twitter_profile_ts_with_inputs/src/index.d.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
206
examples/twitter_profile_ts_with_inputs/src/index.ts
Normal file
206
examples/twitter_profile_ts_with_inputs/src/index.ts
Normal 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);
|
||||
}
|
||||
53
examples/twitter_profile_ts_with_inputs/src/utils/hf.js
Normal file
53
examples/twitter_profile_ts_with_inputs/src/utils/hf.js
Normal 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
1
src/index.d.ts
vendored
@@ -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' {
|
||||
|
||||
Reference in New Issue
Block a user