From 60dd6893beb3152306ee8fc3e75f204cde74551d Mon Sep 17 00:00:00 2001 From: Dorian Date: Fri, 13 Mar 2026 14:06:34 +0000 Subject: [PATCH] fix: replace download with inline copy-paste setup guide, fix CSP for WASM + Nostr relays - Replace file download with collapsible inline guide + COPY ALL button - Guide content has bot_id/secret pre-filled from credentials - JoinBoutPage: "COPY GUIDE + CREDENTIALS" eagerly loads guide content - BotProfilePage: webhook/polling guide selector with copy after secret regen - CSP: add wasm-unsafe-eval to scriptSrc (fixes Kokoro TTS WASM) - CSP: add wss://relay.damus.io, wss://relay.nostr.band, wss://nos.lol to connectSrc Co-Authored-By: Claude Opus 4.6 --- frontend/src/pages/BotProfilePage.vue | 87 +++++++++++----- frontend/src/pages/JoinBoutPage.vue | 139 ++++++++++++++------------ server/src/app.ts | 4 +- 3 files changed, 138 insertions(+), 92 deletions(-) diff --git a/frontend/src/pages/BotProfilePage.vue b/frontend/src/pages/BotProfilePage.vue index f6d2a13..c2b9100 100644 --- a/frontend/src/pages/BotProfilePage.vue +++ b/frontend/src/pages/BotProfilePage.vue @@ -106,12 +106,16 @@ const webhookTestResult = ref<{ reachable: boolean; validResponse: boolean; late const webhookError = ref('') const webhookSuccess = ref('') -// Setup guide download +// Setup guide const showSetupGuide = ref(false) const isRegenerating = ref(false) const regeneratedSecret = ref('') const regeneratedBotId = ref('') const regenError = ref('') +const guideContent = ref('') +const guideLoading = ref(false) +const guideCopied = ref(false) +const guideMode = ref<'webhook' | 'polling'>('webhook') const ARCHETYPES = [ 'standard', 'lobster', 'sheep', 'cyborg', 'blob', 'tank', 'dog', 'cat', @@ -290,26 +294,28 @@ async function handleRegenerateSecret() { isRegenerating.value = false } -async function downloadGuide(mode: 'webhook' | 'polling') { +async function loadGuide(mode: 'webhook' | 'polling') { + guideMode.value = mode + guideLoading.value = true + guideContent.value = '' + guideCopied.value = false const path = mode === 'polling' ? '/docs/BOTFIGHTS-POLLING.md' : '/docs/BOTFIGHTS-WEBHOOK.md' - const fileName = mode === 'polling' ? 'BOTFIGHTS-POLLING.md' : 'BOTFIGHTS-WEBHOOK.md' try { const res = await fetch(path) let content = await res.text() content = content.replace(/YOUR_BOT_ID/g, regeneratedBotId.value) content = content.replace(/YOUR_BOT_SECRET/g, regeneratedSecret.value) - const blob = new Blob([content], { type: 'text/markdown' }) - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url - a.download = fileName - document.body.appendChild(a) - a.click() - document.body.removeChild(a) - URL.revokeObjectURL(url) + guideContent.value = content } catch { - window.open(path, '_blank') + guideContent.value = '# Failed to load setup guide' } + guideLoading.value = false +} + +function copyGuide() { + navigator.clipboard.writeText(guideContent.value) + guideCopied.value = true + setTimeout(() => { guideCopied.value = false }, 2000) } onMounted(async () => { @@ -835,11 +841,11 @@ const tierClass = (t: number) => `tier-${t}`

- Download the setup guide with your credentials embedded. + Get the setup guide with your credentials embedded. This requires regenerating your bot secret (your old secret will stop working).

- + @@ -882,7 +917,7 @@ const tierClass = (t: number) => `tier-${t}` @click="handleRegenerateSecret" > - {{ isRegenerating ? 'REGENERATING...' : 'REGENERATE SECRET & DOWNLOAD' }} + {{ isRegenerating ? 'REGENERATING...' : 'REGENERATE SECRET' }}

Your current bot secret will be invalidated and replaced with a new one. diff --git a/frontend/src/pages/JoinBoutPage.vue b/frontend/src/pages/JoinBoutPage.vue index f009ad3..6479a1f 100644 --- a/frontend/src/pages/JoinBoutPage.vue +++ b/frontend/src/pages/JoinBoutPage.vue @@ -527,6 +527,11 @@ async function confirmPolling() { } } +const showSetupContent = ref(false) +const setupContent = ref('') +const setupContentLoading = ref(false) +const setupContentCopied = ref(false) + function setupDocPath() { return connectionMode.value === 'polling' ? '/docs/BOTFIGHTS-POLLING.md' : '/docs/BOTFIGHTS-WEBHOOK.md' } @@ -535,29 +540,42 @@ function setupDocName() { return connectionMode.value === 'polling' ? 'BOTFIGHTS-POLLING.md' : 'BOTFIGHTS-WEBHOOK.md' } -async function downloadSetupGuide() { - try { - const res = await fetch(setupDocPath()) - let content = await res.text() - content = content.replace(/YOUR_BOT_ID/g, botId.value) - content = content.replace(/YOUR_BOT_SECRET/g, botSecret.value) - const blob = new Blob([content], { type: 'text/markdown' }) - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url - a.download = setupDocName() - document.body.appendChild(a) - a.click() - document.body.removeChild(a) - URL.revokeObjectURL(url) - } catch { - window.open(setupDocPath(), '_blank') +async function toggleSetupContent() { + showSetupContent.value = !showSetupContent.value + if (showSetupContent.value && !setupContent.value) { + setupContentLoading.value = true + try { + const res = await fetch(setupDocPath()) + let content = await res.text() + content = content.replace(/YOUR_BOT_ID/g, botId.value) + content = content.replace(/YOUR_BOT_SECRET/g, botSecret.value) + setupContent.value = content + } catch { + setupContent.value = '# Failed to load setup guide' + } + setupContentLoading.value = false } } -function copyFullPrompt() { - const docName = setupDocName() - const text = `Read ${docName} and follow the setup instructions. Here are my credentials:\nBOT_ID=${botId.value}\nBOT_SECRET=${botSecret.value}` +function copySetupContent() { + navigator.clipboard.writeText(setupContent.value) + setupContentCopied.value = true + setTimeout(() => { setupContentCopied.value = false }, 2000) +} + +async function copyFullPrompt() { + if (!setupContent.value) { + try { + const res = await fetch(setupDocPath()) + let content = await res.text() + content = content.replace(/YOUR_BOT_ID/g, botId.value) + content = content.replace(/YOUR_BOT_SECRET/g, botSecret.value) + setupContent.value = content + } catch { /* fall through with empty content */ } + } + const text = setupContent.value + ? setupContent.value + : `Read ${setupDocName()} and follow the setup instructions.\n\nBOT_ID=${botId.value}\nBOT_SECRET=${botSecret.value}` navigator.clipboard.writeText(text) setupGuideCopied.value = true setTimeout(() => { setupGuideCopied.value = false }, 2000) @@ -991,20 +1009,37 @@ function handleSignOut() {

- +
-

- Put this file in your workspace — your AI reads it to build the bot -

+ +
+
+ {{ setupDocName() }} + +
+
+

Loading...

+
+
{{ setupContent }}
+
@@ -1297,49 +1332,25 @@ function handleSignOut() {

- 2 STEPS — THEN YOU'RE FIGHTING + COPY THIS TO YOUR AI

- -
- 1 -
-

Download {{ setupDocName() }} into your workspace

- -
+
+
BOT_ID={{ botId }}
+
BOT_SECRET={{ botSecret }}
- -
- 2 -
-

Tell your AI this:

-
-

Read {{ setupDocName() }} and follow the setup instructions. Here are my credentials:

-
-
BOT_ID={{ botId }}
-
BOT_SECRET={{ botSecret }}
-
-
- -
-
+ -

- Your AI reads the guide, creates the bot, and starts it for you. +

+ Paste this into your AI (Claude, ChatGPT, etc.) — it includes the full setup guide with your credentials. Save these credentials — the secret won't be shown again.

diff --git a/server/src/app.ts b/server/src/app.ts index 7f4e597..22f8a69 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -54,10 +54,10 @@ app.use('*', async (c, next) => { app.use('*', secureHeaders({ contentSecurityPolicy: process.env.NODE_ENV === 'production' ? { defaultSrc: ["'self'"], - scriptSrc: ["'self'", 'blob:'], + scriptSrc: ["'self'", 'blob:', "'wasm-unsafe-eval'"], styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'], imgSrc: ["'self'", 'data:', 'blob:'], - connectSrc: ["'self'", 'https://huggingface.co', 'https://*.huggingface.co', 'https://*.hf.co', 'https://cdn.jsdelivr.net'], + connectSrc: ["'self'", 'https://huggingface.co', 'https://*.huggingface.co', 'https://*.hf.co', 'https://cdn.jsdelivr.net', 'wss://relay.damus.io', 'wss://relay.nostr.band', 'wss://nos.lol'], fontSrc: ["'self'", 'https://fonts.gstatic.com'], workerSrc: ["'self'", 'blob:'], } : undefined,