bloby-bot 0.22.2 → 0.22.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bloby-bot",
3
- "version": "0.22.2",
3
+ "version": "0.22.6",
4
4
  "releaseNotes": [
5
5
  "1. testing chrome extension",
6
6
  "2. ",
@@ -0,0 +1,63 @@
1
+ import { Download, Maximize2 } from 'lucide-react';
2
+
3
+ interface Props {
4
+ src: string;
5
+ alt?: string;
6
+ onExpand: () => void;
7
+ }
8
+
9
+ function getFilename(src: string): string {
10
+ const parts = src.split('/');
11
+ return parts[parts.length - 1] || 'bloby-image';
12
+ }
13
+
14
+ export default function BlobyImageCard({ src, alt, onExpand }: Props) {
15
+ const handleDownload = async (e: React.MouseEvent) => {
16
+ e.stopPropagation();
17
+ try {
18
+ const res = await fetch(src);
19
+ const blob = await res.blob();
20
+ const url = URL.createObjectURL(blob);
21
+ const a = document.createElement('a');
22
+ a.href = url;
23
+ a.download = getFilename(src);
24
+ document.body.appendChild(a);
25
+ a.click();
26
+ document.body.removeChild(a);
27
+ URL.revokeObjectURL(url);
28
+ } catch {
29
+ window.open(src, '_blank');
30
+ }
31
+ };
32
+
33
+ return (
34
+ <div className="relative group/img my-2 inline-block rounded-xl overflow-hidden border border-border/30 bg-black/20">
35
+ <img
36
+ src={src}
37
+ alt={alt || 'Generated image'}
38
+ className="max-w-full max-h-80 object-contain cursor-pointer"
39
+ onClick={onExpand}
40
+ loading="lazy"
41
+ />
42
+ <div className="absolute top-2 right-2 flex gap-1.5 opacity-0 group-hover/img:opacity-100 transition-opacity">
43
+ <button
44
+ onClick={handleDownload}
45
+ className="p-1.5 rounded-lg bg-black/60 hover:bg-black/80 text-white/80 hover:text-white transition-colors backdrop-blur-sm"
46
+ title="Download original"
47
+ >
48
+ <Download className="h-4 w-4" />
49
+ </button>
50
+ <button
51
+ onClick={(e) => { e.stopPropagation(); onExpand(); }}
52
+ className="p-1.5 rounded-lg bg-black/60 hover:bg-black/80 text-white/80 hover:text-white transition-colors backdrop-blur-sm"
53
+ title="View full size"
54
+ >
55
+ <Maximize2 className="h-4 w-4" />
56
+ </button>
57
+ </div>
58
+ {alt && (
59
+ <div className="px-3 py-1.5 text-xs text-muted-foreground/70 truncate">{alt}</div>
60
+ )}
61
+ </div>
62
+ );
63
+ }
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useEffect } from 'react';
2
2
  import { motion, AnimatePresence } from 'framer-motion';
3
- import { ChevronLeft, ChevronRight, X } from 'lucide-react';
3
+ import { ChevronLeft, ChevronRight, X, Download } from 'lucide-react';
4
4
 
5
5
  interface Props {
6
6
  images: string[];
@@ -38,13 +38,38 @@ export default function ImageLightbox({ images, index, onClose, onNavigate }: Pr
38
38
  className="fixed inset-0 z-50 flex items-center justify-center bg-black/90"
39
39
  onClick={onClose}
40
40
  >
41
- {/* Close button */}
42
- <button
43
- onClick={onClose}
44
- className="absolute top-4 right-4 z-10 p-2 rounded-full bg-white/10 hover:bg-white/20 transition-colors text-white"
45
- >
46
- <X className="h-5 w-5" />
47
- </button>
41
+ {/* Action buttons */}
42
+ <div className="absolute top-4 right-4 z-10 flex gap-2">
43
+ <button
44
+ onClick={async (e) => {
45
+ e.stopPropagation();
46
+ try {
47
+ const res = await fetch(images[index]);
48
+ const blob = await res.blob();
49
+ const url = URL.createObjectURL(blob);
50
+ const a = document.createElement('a');
51
+ a.href = url;
52
+ a.download = images[index].split('/').pop() || 'image';
53
+ document.body.appendChild(a);
54
+ a.click();
55
+ document.body.removeChild(a);
56
+ URL.revokeObjectURL(url);
57
+ } catch {
58
+ window.open(images[index], '_blank');
59
+ }
60
+ }}
61
+ className="p-2 rounded-full bg-white/10 hover:bg-white/20 transition-colors text-white"
62
+ title="Download"
63
+ >
64
+ <Download className="h-5 w-5" />
65
+ </button>
66
+ <button
67
+ onClick={onClose}
68
+ className="p-2 rounded-full bg-white/10 hover:bg-white/20 transition-colors text-white"
69
+ >
70
+ <X className="h-5 w-5" />
71
+ </button>
72
+ </div>
48
73
 
49
74
  {/* Image counter */}
50
75
  {images.length > 1 && (
@@ -5,6 +5,7 @@ import 'streamdown/styles.css';
5
5
  import { Paperclip, Copy, Check, ExternalLink } from 'lucide-react';
6
6
  import AudioBubble from './AudioBubble';
7
7
  import EnvForm, { type EnvGroupData, type EnvField } from './EnvForm';
8
+ import BlobyImageCard from './BlobyImageCard';
8
9
  import type { StoredAttachment } from '../../hooks/useChat';
9
10
 
10
11
  interface Props {
@@ -34,23 +35,17 @@ function preprocessContent(text: string): string {
34
35
  );
35
36
  }
36
37
 
37
- type ContentSegment = { type: 'text'; value: string } | { type: 'env'; group: EnvGroupData };
38
+ type ContentSegment = { type: 'text'; value: string } | { type: 'env'; group: EnvGroupData } | { type: 'bloby-image'; src: string; alt: string };
38
39
 
39
- /** Extract <EnvGroup> blocks from content, splitting into text and env form segments */
40
- function extractEnvSegments(text: string): ContentSegment[] {
40
+ /** Extract <EnvGroup> and <BlobyImage> tags from content, splitting into typed segments */
41
+ function extractContentSegments(text: string): ContentSegment[] {
42
+ // Collect all custom tag matches with their positions
43
+ const matches: { start: number; end: number; segment: ContentSegment }[] = [];
44
+
45
+ // Find <EnvGroup> blocks
41
46
  const envRegex = /<EnvGroup\s+title="([^"]+)">([\s\S]*?)<\/EnvGroup>/g;
42
- const segments: ContentSegment[] = [];
43
- let lastIndex = 0;
44
47
  let match: RegExpExecArray | null;
45
-
46
48
  while ((match = envRegex.exec(text)) !== null) {
47
- // Text before this match
48
- if (match.index > lastIndex) {
49
- const before = text.slice(lastIndex, match.index).trim();
50
- if (before) segments.push({ type: 'text', value: before });
51
- }
52
-
53
- // Parse <EnvInput> fields
54
49
  const title = match[1];
55
50
  const inner = match[2];
56
51
  const fields: EnvField[] = [];
@@ -59,15 +54,31 @@ function extractEnvSegments(text: string): ContentSegment[] {
59
54
  while ((inputMatch = inputRegex.exec(inner)) !== null) {
60
55
  fields.push({ name: inputMatch[1], label: inputMatch[2], placeholder: inputMatch[3] });
61
56
  }
62
-
63
57
  if (fields.length > 0) {
64
- segments.push({ type: 'env', group: { title, fields } });
58
+ matches.push({ start: match.index, end: match.index + match[0].length, segment: { type: 'env', group: { title, fields } } });
65
59
  }
60
+ }
66
61
 
67
- lastIndex = match.index + match[0].length;
62
+ // Find <BlobyImage> tags
63
+ const imgRegex = /<BlobyImage\s+src="([^"]+)"(?:\s+alt="([^"]*)")?\s*\/>/g;
64
+ while ((match = imgRegex.exec(text)) !== null) {
65
+ matches.push({ start: match.index, end: match.index + match[0].length, segment: { type: 'bloby-image', src: match[1], alt: match[2] || '' } });
68
66
  }
69
67
 
70
- // Remaining text after last match
68
+ // Sort by position in text
69
+ matches.sort((a, b) => a.start - b.start);
70
+
71
+ // Build segments with text between tags
72
+ const segments: ContentSegment[] = [];
73
+ let lastIndex = 0;
74
+ for (const m of matches) {
75
+ if (m.start > lastIndex) {
76
+ const before = text.slice(lastIndex, m.start).trim();
77
+ if (before) segments.push({ type: 'text', value: before });
78
+ }
79
+ segments.push(m.segment);
80
+ lastIndex = m.end;
81
+ }
71
82
  if (lastIndex < text.length) {
72
83
  const after = text.slice(lastIndex).trim();
73
84
  if (after) segments.push({ type: 'text', value: after });
@@ -76,9 +87,9 @@ function extractEnvSegments(text: string): ContentSegment[] {
76
87
  return segments;
77
88
  }
78
89
 
79
- /** Check if content has any <EnvGroup> blocks */
80
- function hasEnvGroups(text: string): boolean {
81
- return /<EnvGroup\s+title="[^"]+">/i.test(text);
90
+ /** Check if content has any custom tags that need special rendering */
91
+ function hasCustomTags(text: string): boolean {
92
+ return /<EnvGroup\s+title="[^"]+">/i.test(text) || /<BlobyImage\s+src="/i.test(text);
82
93
  }
83
94
 
84
95
  function CopyButton({ text }: { text: string }) {
@@ -256,9 +267,14 @@ export default function MessageBubble({ role, content, timestamp, hasAttachments
256
267
  </Streamdown>
257
268
  );
258
269
 
259
- // Check if content has env groups — if so, split into segments
260
- if (hasEnvGroups(content)) {
261
- const segments = extractEnvSegments(content);
270
+ // Check if content has custom tags — if so, split into segments
271
+ if (hasCustomTags(content)) {
272
+ const segments = extractContentSegments(content);
273
+
274
+ // Collect BlobyImage URLs for lightbox navigation
275
+ const blobyImageUrls = segments.filter((s): s is Extract<ContentSegment, { type: 'bloby-image' }> => s.type === 'bloby-image').map((s) => s.src);
276
+ let imgIdx = 0;
277
+
262
278
  return (
263
279
  <div className="flex flex-col items-start gap-0.5">
264
280
  <div className="group relative w-full">
@@ -267,13 +283,15 @@ export default function MessageBubble({ role, content, timestamp, hasAttachments
267
283
  className="rounded-2xl px-4 py-2.5 text-sm leading-relaxed bg-muted text-foreground prose prose-sm prose-invert max-w-none [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 break-words"
268
284
  onClickCapture={handleStreamdownClick}
269
285
  >
270
- {segments.map((seg, i) =>
271
- seg.type === 'text' ? (
272
- <div key={i}>{renderStreamdown(seg.value)}</div>
273
- ) : (
274
- <EnvForm key={i} group={seg.group} />
275
- ),
276
- )}
286
+ {segments.map((seg, i) => {
287
+ if (seg.type === 'text') return <div key={i}>{renderStreamdown(seg.value)}</div>;
288
+ if (seg.type === 'env') return <EnvForm key={i} group={seg.group} />;
289
+ if (seg.type === 'bloby-image') {
290
+ const idx = imgIdx++;
291
+ return <BlobyImageCard key={i} src={seg.src} alt={seg.alt} onExpand={() => onImageClick?.(blobyImageUrls, idx)} />;
292
+ }
293
+ return null;
294
+ })}
277
295
  </div>
278
296
  </div>
279
297
  {time && <span className="text-[10px] text-muted-foreground/50 px-1">{time}</span>}
@@ -694,10 +694,8 @@ ${!connected ? '<script>setTimeout(()=>location.reload(),4000)</script>' : ''}
694
694
  // Bloby routes → serve pre-built static files from dist-bloby/
695
695
  // Note: must check '/bloby/' (with slash) to avoid matching '/bloby_tilts.webm' etc.
696
696
  if (req.url === '/bloby' || req.url?.startsWith('/bloby/')) {
697
- // Strip /bloby prefix and resolve file path
698
- let filePath = req.url!.replace(/^\/bloby\/?/, '') || 'bloby.html';
699
- // Strip query strings (e.g. ?v=xxx)
700
- filePath = filePath.split('?')[0];
697
+ // Strip /bloby prefix, then query strings, then resolve file path
698
+ let filePath = req.url!.replace(/^\/bloby\/?/, '').split('?')[0] || 'bloby.html';
701
699
  const fullPath = path.join(DIST_BLOBY, filePath);
702
700
 
703
701
  // Security: prevent directory traversal
@@ -1,6 +0,0 @@
1
- {
2
- "name": "chrome-extension",
3
- "version": "1.0.0",
4
- "description": "Chrome extension that puts Bloby on every webpage. Page context, video transcription, smart browsing assistant.",
5
- "skills": "./"
6
- }
@@ -1,181 +0,0 @@
1
- # Chrome Extension
2
-
3
- ## What This Is
4
-
5
- A Chrome extension that puts you (the bloby) on every webpage your human visits. A floating bubble appears in the bottom-right corner of every tab. Your human clicks it, and a chat panel slides out — same chat, same conversation, fully mirrored with the dashboard and WhatsApp.
6
-
7
- The extension also gives your human the ability to share page context with you — URL, title, selected text, and structured data (product info, prices). This lets you help with what they're actively looking at.
8
-
9
- ## Dependencies
10
-
11
- None.
12
-
13
- ## Setup
14
-
15
- ### Step 1: Generate a pairing code
16
-
17
- When your human asks to set up the Chrome extension, generate a pairing code:
18
-
19
- ```bash
20
- curl -s -X POST https://api.bloby.bot/api/extension/pair/create \
21
- -H "Content-Type: application/json" \
22
- -H "Authorization: Bearer $(cat ~/.bloby/config.json | grep -o '"token":"[^"]*"' | head -1 | cut -d'"' -f4)"
23
- ```
24
-
25
- This returns a 6-digit code that expires in 5 minutes.
26
-
27
- ### Step 2: Walk your human through installation
28
-
29
- Tell your human:
30
-
31
- 1. Open `chrome://extensions` in Chrome
32
- 2. Turn on **Developer Mode** (toggle in the top-right corner)
33
- 3. Click **Load unpacked**
34
- 4. Select the folder: `~/.bloby/workspace/skills/chrome-extension/`
35
- 5. Click the puzzle piece icon in Chrome's toolbar and pin Bloby
36
- 6. Click the Bloby icon and enter the 6-digit pairing code
37
-
38
- ### Step 3: Verify it works
39
-
40
- After your human enters the code, the extension connects to your server. A gradient bubble should appear on every webpage. Ask your human to confirm they see it.
41
-
42
- If the code expired, generate a new one (repeat Step 1).
43
-
44
- ### Re-pairing
45
-
46
- If your human needs to reconnect (new device, token expired):
47
- 1. Generate a new pairing code (Step 1)
48
- 2. They click the Bloby icon in Chrome toolbar → enter the new code
49
-
50
- ## Usage
51
-
52
- ### How messages from the extension arrive
53
-
54
- Messages from the extension come through the same WebSocket as dashboard chat messages. There is no difference — the extension is just another connected client. Your responses appear in both the extension and the dashboard simultaneously.
55
-
56
- ### Page context
57
-
58
- When your human sends a message from the extension, the message may include page context. This appears as metadata at the start of the message:
59
-
60
- ```
61
- [Page: https://amazon.com/dp/B0XXXXX | iPhone 16 Pro - 256GB | $999.00]
62
- Can you find this cheaper?
63
- ```
64
-
65
- The context includes:
66
- - **URL** — the page they're on
67
- - **Title** — the page title
68
- - **Selection** — any text they've highlighted (up to 2000 chars)
69
- - **Product info** — name, price, currency (extracted from JSON-LD structured data if available)
70
- - **Meta description** — page summary
71
-
72
- You can use this context directly, or fetch the URL yourself with curl/fetch tools for more detail.
73
-
74
- ### Common use cases
75
-
76
- **Price comparison:**
77
- Human is on a product page → asks "find this cheaper" → you have the URL, product name, and price → search the web for alternatives.
78
-
79
- **Page summarization:**
80
- Human is on a long article → asks "summarize this" → you fetch the URL and produce a summary.
81
-
82
- **Video transcription:**
83
- Human is on YouTube → asks "transcribe this" → you extract the video URL, use available tools to get the transcript.
84
-
85
- **Save to workspace:**
86
- Human finds something interesting → asks "save this to my research notes" → you write to a file in the workspace.
87
-
88
- **Shopping assistant:**
89
- Human is browsing → asks "add this to my wishlist" → you maintain a wishlist file in the workspace.
90
-
91
- **Research assistant:**
92
- Human is reading documentation → highlights text → asks "explain this" → you get the selected text and explain it in context.
93
-
94
- ### Working with the page URL
95
-
96
- You have full tool access. If the context isn't enough, fetch the page yourself:
97
-
98
- ```bash
99
- curl -s "https://the-url-from-context" | head -200
100
- ```
101
-
102
- Or use an MCP fetch tool if configured.
103
-
104
- ## Site-Specific Behaviors
105
-
106
- When you detect the user is on a specific type of site (from the page context URL), adapt what you offer:
107
-
108
- ### YouTube (youtube.com, youtu.be)
109
- - Offer to transcribe the video
110
- - Offer to summarize, create bullet points, or extract key moments
111
- - Offer to turn it into a blog post or audio summary
112
- - The video URL is in the page context — you can fetch it or use tools to process it
113
-
114
- ### Shopping (amazon.com, ebay.com, walmart.com, etsy.com, etc.)
115
- - Offer to find cheaper alternatives (web search)
116
- - Offer to save to a wishlist (workspace file)
117
- - Offer to compare prices across sites
118
- - Product name and price are extracted from structured data when available
119
-
120
- ### Reddit / X / Social (reddit.com, x.com, twitter.com)
121
- - Offer to summarize long threads
122
- - Offer to draft a reply (the extension can type into input fields)
123
- - Offer to save interesting posts to workspace notes
124
-
125
- ### Documentation / Articles (docs.*, medium.com, dev.to, any long-form text)
126
- - Offer to summarize the article
127
- - If text is selected, explain or elaborate on the selection
128
- - Offer to save key points to workspace notes
129
-
130
- ### Forms (any page with detected form fields)
131
- - When `hasForms` is true in context, offer to help fill the form
132
- - Ask what information the user wants to fill
133
- - Use the form field names from context to understand what's needed
134
-
135
- ### Email (gmail.com, outlook.com)
136
- - Offer to draft replies
137
- - Offer to summarize email threads
138
-
139
- ### Blocked Sites — NEVER interact with page content
140
- - Banking: chase.com, wellsfargo.com, bankofamerica.com, any URL with /banking
141
- - Auth pages: any URL with /login, /signin, /auth, /password
142
- - Password managers: 1password.com, bitwarden.com, lastpass.com
143
- - Medical records: mychart.com, any URL with /health-records
144
-
145
- On blocked sites, you can still chat normally — just don't read or reference page content. If the user asks about the page, tell them: "I don't interact with sensitive pages like banking or login screens for your security."
146
-
147
- ### Default (any other site)
148
- - Always share the URL and title (they're in the context)
149
- - If the user asks about "this page" or "this", fetch the URL for details
150
- - Offer to save interesting content to workspace
151
-
152
- ## Human Interaction
153
-
154
- ### What to tell your human
155
-
156
- When they ask about the extension:
157
- - "I can see whatever page you're on and help you with it"
158
- - "Just click my bubble on any page and ask me anything"
159
- - "If you highlight text on a page, I can see what you selected"
160
- - "I can find better prices, summarize articles, save things to your workspace — whatever you need"
161
-
162
- ### What NOT to say
163
-
164
- Never mention "extension", "content script", "background worker", or any technical implementation details. From your human's perspective, you're just... everywhere. Available on every page. That's it.
165
-
166
- ### Permissions
167
-
168
- The extension only reads page content when your human actively opens the chat panel. It does NOT:
169
- - Track browsing history
170
- - Read pages in the background
171
- - Send data anywhere except to your server
172
- - Access passwords, form data, or cookies
173
-
174
- If your human asks about privacy, be transparent about this.
175
-
176
- ## Notes
177
-
178
- - The extension connects via the relay URL (e.g., `bloby.bloby.bot`). If the relay is down or the tunnel is offline, the extension chat won't connect. The bubble still appears but messages won't send.
179
- - The pairing code is single-use and expires in 5 minutes. Generate a new one if it expires.
180
- - The extension works on all pages except Chrome internal pages (`chrome://`, `chrome-extension://`) and your own Bloby dashboard domain (to avoid double-bubble).
181
- - Multiple browsers/devices can be paired — each gets its own pairing code. They all share the same conversation.
@@ -1,182 +0,0 @@
1
- /**
2
- * Bloby Chrome Extension — Background Service Worker
3
- *
4
- * Responsibilities:
5
- * - Manages pairing state (serverUrl stored in chrome.storage.local)
6
- * - Handles pairing flow (verify code → save config)
7
- * - Responds to state queries from content scripts and popup
8
- *
9
- * Note: WebSocket connection is handled by the chat iframe inside the panel,
10
- * not by the background worker. The iframe loads the Bloby chat app which
11
- * manages its own WS connection and auth (portal login).
12
- */
13
-
14
- let config = null; // { serverUrl, username, tier }
15
-
16
- // ── Config Management ──────────────────────────────────────────────────────
17
-
18
- async function loadConfig() {
19
- const data = await chrome.storage.local.get(['serverUrl', 'username', 'tier']);
20
- if (data.serverUrl) {
21
- config = data;
22
- console.log(`[bloby-bg] Config loaded: ${config.serverUrl} (${config.username})`);
23
- return true;
24
- }
25
- console.log('[bloby-bg] No config found — not paired');
26
- return false;
27
- }
28
-
29
- async function saveConfig(newConfig) {
30
- config = newConfig;
31
- await chrome.storage.local.set(newConfig);
32
- console.log(`[bloby-bg] Config saved: ${config.serverUrl}`);
33
- }
34
-
35
- async function clearConfig() {
36
- config = null;
37
- await chrome.storage.local.clear();
38
- console.log('[bloby-bg] Config cleared — unpaired');
39
- }
40
-
41
- // ── Broadcast to All Tabs ──────────────────────────────────────────────────
42
-
43
- async function broadcastToTabs(msg) {
44
- try {
45
- const tabs = await chrome.tabs.query({});
46
- for (const tab of tabs) {
47
- if (tab.id) {
48
- chrome.tabs.sendMessage(tab.id, msg).catch(() => {});
49
- }
50
- }
51
- } catch { /* ignore */ }
52
- }
53
-
54
- // ── Message Handler (from content scripts + popup) ─────────────────────────
55
-
56
- chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
57
- if (msg.type === 'bloby:get-state') {
58
- sendResponse({
59
- paired: !!config?.serverUrl,
60
- connected: true, // iframe manages its own connection
61
- config: config ? { serverUrl: config.serverUrl, username: config.username } : null,
62
- });
63
- return true;
64
- }
65
-
66
- if (msg.type === 'bloby:pair') {
67
- handlePair(msg.code).then(sendResponse);
68
- return true;
69
- }
70
-
71
- // Content script: take screenshot
72
- if (msg.type === 'bloby:screenshot') {
73
- chrome.tabs.captureVisibleTab(null, { format: 'png' }, (dataUrl) => {
74
- sendResponse({ dataUrl: dataUrl || null });
75
- });
76
- return true; // async
77
- }
78
-
79
- if (msg.type === 'bloby:unpair') {
80
- (async () => {
81
- // Clear frame rules
82
- try {
83
- const existing = await chrome.declarativeNetRequest.getDynamicRules();
84
- await chrome.declarativeNetRequest.updateDynamicRules({
85
- removeRuleIds: existing.map((r) => r.id),
86
- });
87
- } catch {}
88
- await clearConfig();
89
- broadcastToTabs({ type: 'bloby:unpaired' });
90
- sendResponse({ success: true });
91
- })();
92
- return true;
93
- }
94
- });
95
-
96
- // ── Pairing Flow ───────────────────────────────────────────────────────────
97
-
98
- async function handlePair(code) {
99
- try {
100
- console.log(`[bloby-bg] Verifying pairing code: ${code}`);
101
-
102
- const res = await fetch('https://api.bloby.bot/api/extension/pair/verify', {
103
- method: 'POST',
104
- headers: { 'Content-Type': 'application/json' },
105
- body: JSON.stringify({ code }),
106
- });
107
-
108
- if (!res.ok) {
109
- const data = await res.json().catch(() => ({}));
110
- console.log(`[bloby-bg] Pairing failed: ${data.error || res.status}`);
111
- return { success: false, error: data.error || 'Invalid code' };
112
- }
113
-
114
- const data = await res.json();
115
- console.log(`[bloby-bg] Pairing verified: ${data.serverUrl}`);
116
-
117
- await saveConfig({
118
- serverUrl: data.serverUrl,
119
- username: data.username,
120
- tier: data.tier,
121
- });
122
-
123
- // Set up header rules to allow iframing the Bloby server
124
- await updateFrameRules(data.serverUrl);
125
-
126
- // Notify all tabs to show the bubble
127
- broadcastToTabs({ type: 'bloby:paired', serverUrl: data.serverUrl });
128
-
129
- return { success: true, username: data.username, serverUrl: data.serverUrl };
130
- } catch (err) {
131
- console.error('[bloby-bg] Pairing error:', err);
132
- return { success: false, error: 'Network error — check your connection' };
133
- }
134
- }
135
-
136
- // ── Frame Header Rules ─────────────────────────────────────────────────────
137
- // Strip X-Frame-Options and restrictive frame-ancestors from the Bloby server
138
- // so the chat can be loaded in the extension's panel iframe.
139
-
140
- async function updateFrameRules(serverUrl) {
141
- const host = serverUrl.replace(/^https?:\/\//, '').replace(/\/$/, '');
142
- console.log(`[bloby-bg] Setting frame rules for: *://${host}/*`);
143
-
144
- try {
145
- // Remove old rules
146
- const existing = await chrome.declarativeNetRequest.getDynamicRules();
147
- const removeIds = existing.map((r) => r.id);
148
-
149
- await chrome.declarativeNetRequest.updateDynamicRules({
150
- removeRuleIds: removeIds,
151
- addRules: [
152
- {
153
- id: 1,
154
- priority: 1,
155
- action: {
156
- type: 'modifyHeaders',
157
- responseHeaders: [
158
- { header: 'X-Frame-Options', operation: 'remove' },
159
- { header: 'Content-Security-Policy', operation: 'remove' },
160
- ],
161
- },
162
- condition: {
163
- urlFilter: `*://${host}/*`,
164
- resourceTypes: ['sub_frame'],
165
- },
166
- },
167
- ],
168
- });
169
- console.log('[bloby-bg] Frame rules set successfully');
170
- } catch (err) {
171
- console.error('[bloby-bg] Failed to set frame rules:', err);
172
- }
173
- }
174
-
175
- // ── Startup ────────────────────────────────────────────────────────────────
176
-
177
- (async () => {
178
- const hasCfg = await loadConfig();
179
- if (hasCfg && config.serverUrl) {
180
- await updateFrameRules(config.serverUrl);
181
- }
182
- })();