Example plugin for Spotify top artist

This commit is contained in:
Hendrik Eeckhaut
2025-01-10 14:32:26 +01:00
parent 1c18826b1f
commit 73c2d3744f
8 changed files with 239 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,37 @@
{
"title": "Spotify Top Artist",
"description": "Notarize your favorite artist on Spotify",
"steps": [
{
"title": "Visit Spotify webplayer",
"cta": "Go",
"action": "start"
},
{
"title": "Collect credentials",
"description": "Login to your account if you haven't already",
"cta": "Go",
"action": "two"
},
{
"title": "Notarize",
"cta": "Notarize",
"action": "three",
"prover": true
}
],
"hostFunctions": [
"redirect",
"notarize"
],
"cookies": [],
"headers": [
"api.spotify.com"
],
"requests": [
{
"url": "https://api.spotify.com/v1/me/top/artists?time_range=medium_term&limit=1",
"method": "GET"
}
]
}

View File

@@ -0,0 +1,36 @@
const esbuild = require('esbuild');
const path = require('path');
const { name } = require('./package.json');
const { execSync } = require('child_process');
const outputDir = 'dist';
const entryFile = 'src/index.ts';
const outputFile = path.join(outputDir, 'index.js');
const outputWasm = path.join(outputDir, `${name}.tlsn.wasm`);
async function build() {
try {
await esbuild.build({
entryPoints: [entryFile],
bundle: true,
outdir: outputDir, // Use outdir for directory output
sourcemap: true,
minify: false, // might want to use true for production build
format: 'cjs', // needs to be CJS for now
target: ['es2020'], // don't go over es2020 because quickjs doesn't support it
loader: {'.png': 'dataurl'}
});
console.log('esbuild completed successfully.');
// Run extism-js to generate the wasm file
const extismCommand = `extism-js ${outputFile} -i src/index.d.ts -o ${outputWasm}`;
execSync(extismCommand, { stdio: 'inherit' });
console.log('extism-js completed successfully.');
} catch (error) {
console.error('Build process failed:', error);
process.exit(1);
}
}
build();

View File

@@ -0,0 +1,17 @@
{
"name": "spotify_top_artist",
"version": "1.0.0",
"description": "Demo TLSNotary plugin to notarize your top artist on Spotify",
"main": "src/index.ts",
"scripts": {
"build": "node esbuild.js"
},
"keywords": [],
"author": "TLSNotary",
"license": "MIT",
"devDependencies": {
"@extism/js-pdk": "^1.0.1",
"esbuild": "^0.19.6",
"typescript": "^5.3.2"
}
}

14
examples/spotify/src/index.d.ts vendored Normal file
View File

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

View File

@@ -0,0 +1,82 @@
import icon from '../assets/icon.png';
import config_json from '../config.json';
import { redirect, notarize, outputJSON, getCookiesByHost, getHeadersByHost } from './utils/hf.js';
/**
* Plugin configuration
* This configurations defines the plugin, most importantly:
* * the different steps
* * 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 === 'open.spotify.com';
}
/**
* Implementation of the first (start) plugin step
*/
export function start() {
if (!isValidHost(Config.get('tabUrl'))) {
redirect('https://open.spotify.com');
outputJSON(false);
return;
}
outputJSON(true);
}
/**
* Implementation of step "two".
* This step collects and validates authentication cookies and headers for 'api.x.com'.
* If all required information, it creates the request object.
* Note that the url needs to be specified in the `config` too, otherwise the request will be refused.
*/
export function two() {
const headers = getHeadersByHost('api.spotify.com');
if (
!headers['authorization']
) {
outputJSON(false);
return;
}
outputJSON({
url: 'https://api.spotify.com/v1/me/top/artists?time_range=medium_term&limit=1',
method: 'GET',
headers: {
'x-twitter-client-language': 'en',
'x-csrf-token': headers['x-csrf-token'],
Host: 'api.x.com',
authorization: headers.authorization,
Connection: 'close',
},
secretHeaders: [
`authorization: ${headers.authorization}`,
],
});
}
/**
* Step 3: calls the `notarize` host function
*/
export function three() {
const params = JSON.parse(Host.inputString());
if (!params) {
outputJSON(false);
} else {
const id = notarize({
...params,
});
outputJSON(id);
}
}

View File

@@ -0,0 +1,39 @@
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];
}
module.exports = {
redirect,
notarize,
outputJSON,
getCookiesByHost,
getHeadersByHost,
};

View File

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