@bridgekitux/browser-bridge 0.1.0

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/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # @bridgekitux/browser-bridge
2
+
3
+ Host-page and browser-extension bridge helpers for connecting restricted browser runtimes to a local BridgeKit agent.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install @bridgekitux/browser-bridge
9
+ ```
10
+
11
+ ## Use
12
+
13
+ ```ts
14
+ import { createHostPageBridge } from '@bridgekitux/browser-bridge';
15
+
16
+ const bridge = createHostPageBridge({
17
+ allowedOriginPatterns: ['https://bolt.new', 'https://*.bolt.new']
18
+ });
19
+ ```
20
+
21
+ Use this package from trusted host pages or extension contexts. Keep origin allowlists strict in production.
@@ -0,0 +1,93 @@
1
+ import { bridgeError, getPolicy, isOriginAllowed } from './policy.js';
2
+
3
+ let socket;
4
+ let activeAgentUrl;
5
+ const pending = new Map();
6
+ const ports = new Set();
7
+ const requestOwners = new Map();
8
+
9
+ function validBridgeMessage(message) {
10
+ return message && message.protocolVersion === '0.1.0' && typeof message.type === 'string' && (message.id === undefined || typeof message.id === 'string');
11
+ }
12
+
13
+ async function ensureSocket() {
14
+ const policy = await getPolicy();
15
+ if (socket && activeAgentUrl === policy.agentUrl && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) return socket;
16
+ if (socket) socket.close();
17
+ activeAgentUrl = policy.agentUrl;
18
+ socket = new WebSocket(policy.agentUrl);
19
+ socket.addEventListener('message', (event) => {
20
+ let message;
21
+ try {
22
+ message = JSON.parse(event.data);
23
+ } catch {
24
+ return;
25
+ }
26
+ if (message.type === 'event') {
27
+ for (const port of ports) port.postMessage({ source: 'bridgekit-extension', type: 'bridgekit.event', message });
28
+ return;
29
+ }
30
+ const owner = message.id ? requestOwners.get(message.id) : undefined;
31
+ const callback = message.id ? pending.get(message.id) : undefined;
32
+ if (message.id) {
33
+ pending.delete(message.id);
34
+ requestOwners.delete(message.id);
35
+ }
36
+ if (owner && ports.has(owner)) owner.postMessage({ source: 'bridgekit-extension', type: 'bridgekit.response', message });
37
+ else if (callback) callback(message);
38
+ });
39
+ socket.addEventListener('close', () => {
40
+ for (const [id, owner] of requestOwners.entries()) {
41
+ if (ports.has(owner)) owner.postMessage({ source: 'bridgekit-extension', type: 'bridgekit.response', message: bridgeError('bridge_closed', 'BridgeKit agent connection closed', id) });
42
+ }
43
+ requestOwners.clear();
44
+ pending.clear();
45
+ });
46
+ return socket;
47
+ }
48
+
49
+ async function isSenderAllowed(sender, pageOrigin) {
50
+ const policy = await getPolicy();
51
+ const senderOrigin = sender?.url ? new URL(sender.url).origin : pageOrigin;
52
+ return Boolean(pageOrigin && senderOrigin === pageOrigin && isOriginAllowed(pageOrigin, policy.allowedOrigins));
53
+ }
54
+
55
+ chrome.runtime.onConnect.addListener((port) => {
56
+ if (port.name !== 'bridgekit-page') return;
57
+ ports.add(port);
58
+ port.onDisconnect.addListener(() => {
59
+ ports.delete(port);
60
+ for (const [id, owner] of requestOwners.entries()) {
61
+ if (owner === port) requestOwners.delete(id);
62
+ }
63
+ });
64
+ port.onMessage.addListener((request) => {
65
+ void (async () => {
66
+ const pageOrigin = typeof request?.pageOrigin === 'string' ? request.pageOrigin : undefined;
67
+ if (!request || request.source !== 'bridgekit-client') return;
68
+ if (!(await isSenderAllowed(port.sender, pageOrigin))) {
69
+ port.postMessage({ source: 'bridgekit-extension', type: 'bridgekit.response', message: bridgeError('origin_not_allowed', `BridgeKit origin is not allowed: ${pageOrigin ?? 'unknown'}`, request.message?.id) });
70
+ return;
71
+ }
72
+ if (request.type === 'bridgekit.detect') {
73
+ const policy = await getPolicy();
74
+ port.postMessage({ source: 'bridgekit-extension', type: 'bridgekit.available', agentUrl: policy.agentUrl, allowed: true });
75
+ return;
76
+ }
77
+ if (request.type !== 'bridgekit.request' || !validBridgeMessage(request.message)) {
78
+ port.postMessage({ source: 'bridgekit-extension', type: 'bridgekit.response', message: bridgeError('invalid_bridge_message', 'Invalid BridgeKit message', request.message?.id) });
79
+ return;
80
+ }
81
+ const ws = await ensureSocket();
82
+ const send = () => {
83
+ if (request.message.id) requestOwners.set(request.message.id, port);
84
+ ws.send(JSON.stringify(request.message));
85
+ };
86
+ if (ws.readyState === WebSocket.OPEN) send();
87
+ else ws.addEventListener('open', send, { once: true });
88
+ ws.addEventListener('error', () => {
89
+ port.postMessage({ source: 'bridgekit-extension', type: 'bridgekit.response', message: bridgeError('agent_unavailable', 'BridgeKit local agent is not reachable', request.message.id) });
90
+ }, { once: true });
91
+ })();
92
+ });
93
+ });
@@ -0,0 +1,24 @@
1
+ const port = chrome.runtime.connect({ name: 'bridgekit-page' });
2
+ const pageOrigin = window.location.origin;
3
+
4
+ port.onMessage.addListener((message) => {
5
+ if (!message || message.source !== 'bridgekit-extension') return;
6
+ if (message.type === 'bridgekit.event') {
7
+ window.postMessage({ source: 'bridgekit-bridge', type: 'bridgekit.event', message: message.message }, pageOrigin);
8
+ return;
9
+ }
10
+ if (message.type === 'bridgekit.available') {
11
+ window.postMessage({ source: 'bridgekit-bridge', type: 'bridgekit.available', agentUrl: message.agentUrl, allowed: message.allowed }, pageOrigin);
12
+ return;
13
+ }
14
+ window.postMessage({ source: 'bridgekit-bridge', type: 'bridgekit.response', message: message.message }, pageOrigin);
15
+ });
16
+
17
+ window.addEventListener('message', (event) => {
18
+ if (event.source !== window || event.origin !== pageOrigin) return;
19
+ const data = event.data;
20
+ if (!data || data.source !== 'bridgekit-client') return;
21
+ port.postMessage({ ...data, pageOrigin });
22
+ });
23
+
24
+ port.postMessage({ source: 'bridgekit-client', type: 'bridgekit.detect', pageOrigin });
@@ -0,0 +1,30 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "BridgeKit Browser Bridge",
4
+ "version": "0.1.0",
5
+ "description": "Relays approved BridgeKit requests from WebContainers and browser sandboxes to the local BridgeKit agent.",
6
+ "permissions": ["storage"],
7
+ "host_permissions": ["http://127.0.0.1/*", "ws://127.0.0.1/*"],
8
+ "background": {
9
+ "service_worker": "background.js",
10
+ "type": "module"
11
+ },
12
+ "options_ui": {
13
+ "page": "options.html",
14
+ "open_in_tab": true
15
+ },
16
+ "content_scripts": [
17
+ {
18
+ "matches": [
19
+ "https://bolt.new/*",
20
+ "https://*.bolt.new/*",
21
+ "https://stackblitz.com/*",
22
+ "https://*.stackblitz.com/*",
23
+ "http://localhost/*",
24
+ "http://127.0.0.1/*"
25
+ ],
26
+ "js": ["content-script.js"],
27
+ "run_at": "document_start"
28
+ }
29
+ ]
30
+ }
@@ -0,0 +1,28 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>BridgeKit Browser Bridge Options</title>
6
+ <style>
7
+ body { font-family: system-ui, sans-serif; max-width: 760px; margin: 40px auto; padding: 0 20px; color: #0f172a; }
8
+ label { display: block; font-weight: 700; margin: 18px 0 8px; }
9
+ input, textarea { width: 100%; box-sizing: border-box; padding: 10px; border: 1px solid #cbd5e1; border-radius: 10px; font: inherit; }
10
+ textarea { min-height: 180px; font-family: ui-monospace, monospace; }
11
+ button { margin-top: 16px; margin-right: 8px; border: 0; border-radius: 999px; padding: 10px 14px; background: #0284c7; color: white; font-weight: 700; cursor: pointer; }
12
+ button.secondary { background: #475569; }
13
+ #status { margin-top: 16px; color: #166534; }
14
+ </style>
15
+ </head>
16
+ <body>
17
+ <h1>BridgeKit Browser Bridge</h1>
18
+ <p>Only origins listed here can ask the extension to relay BridgeKit requests to the local agent.</p>
19
+ <label for="agentUrl">Local agent WebSocket URL</label>
20
+ <input id="agentUrl" />
21
+ <label for="allowedOrigins">Allowed origin patterns, one per line</label>
22
+ <textarea id="allowedOrigins"></textarea>
23
+ <button id="save">Save</button>
24
+ <button id="defaults" class="secondary">Restore defaults</button>
25
+ <p id="status"></p>
26
+ <script type="module" src="options.js"></script>
27
+ </body>
28
+ </html>
@@ -0,0 +1,27 @@
1
+ import { DEFAULT_AGENT_URL, DEFAULT_ALLOWED_ORIGINS } from './policy.js';
2
+
3
+ const agentUrl = document.querySelector('#agentUrl');
4
+ const allowedOrigins = document.querySelector('#allowedOrigins');
5
+ const status = document.querySelector('#status');
6
+
7
+ async function load() {
8
+ const stored = await chrome.storage.local.get(['agentUrl', 'allowedOrigins']);
9
+ agentUrl.value = stored.agentUrl || DEFAULT_AGENT_URL;
10
+ allowedOrigins.value = (stored.allowedOrigins || DEFAULT_ALLOWED_ORIGINS).join('\n');
11
+ }
12
+
13
+ async function save() {
14
+ const origins = allowedOrigins.value.split('\n').map((line) => line.trim()).filter(Boolean);
15
+ await chrome.storage.local.set({ agentUrl: agentUrl.value.trim() || DEFAULT_AGENT_URL, allowedOrigins: origins.length ? origins : DEFAULT_ALLOWED_ORIGINS });
16
+ status.textContent = 'Saved BridgeKit browser bridge settings.';
17
+ }
18
+
19
+ async function restoreDefaults() {
20
+ agentUrl.value = DEFAULT_AGENT_URL;
21
+ allowedOrigins.value = DEFAULT_ALLOWED_ORIGINS.join('\n');
22
+ await save();
23
+ }
24
+
25
+ document.querySelector('#save').addEventListener('click', () => void save());
26
+ document.querySelector('#defaults').addEventListener('click', () => void restoreDefaults());
27
+ void load();
@@ -0,0 +1,53 @@
1
+ export const DEFAULT_AGENT_URL = 'ws://127.0.0.1:7777/bridgekit';
2
+ export const DEFAULT_ALLOWED_ORIGINS = [
3
+ 'https://bolt.new',
4
+ 'https://*.bolt.new',
5
+ 'https://stackblitz.com',
6
+ 'https://*.stackblitz.com',
7
+ 'http://localhost:*',
8
+ 'http://127.0.0.1:*'
9
+ ];
10
+
11
+ export async function getPolicy() {
12
+ const stored = await chrome.storage.local.get(['agentUrl', 'allowedOrigins']);
13
+ return {
14
+ agentUrl: typeof stored.agentUrl === 'string' && stored.agentUrl.length > 0 ? stored.agentUrl : DEFAULT_AGENT_URL,
15
+ allowedOrigins: Array.isArray(stored.allowedOrigins) && stored.allowedOrigins.length > 0 ? stored.allowedOrigins : DEFAULT_ALLOWED_ORIGINS
16
+ };
17
+ }
18
+
19
+ export function isOriginAllowed(origin, patterns) {
20
+ let parsed;
21
+ try {
22
+ parsed = new URL(origin);
23
+ } catch {
24
+ return false;
25
+ }
26
+ return patterns.some((pattern) => originMatchesPattern(parsed, pattern));
27
+ }
28
+
29
+ function originMatchesPattern(origin, pattern) {
30
+ let parsedPattern;
31
+ try {
32
+ parsedPattern = new URL(pattern.replace(':*', ':0'));
33
+ } catch {
34
+ return false;
35
+ }
36
+ if (parsedPattern.protocol !== origin.protocol) return false;
37
+ const rawHost = pattern.replace(/^[a-z]+:\/\//, '').replace(/:\*$/, '').replace(/:\d+$/, '');
38
+ const portWildcard = pattern.endsWith(':*');
39
+ const explicitPort = pattern.match(/:(\d+)$/)?.[1];
40
+ if (!portWildcard && explicitPort && origin.port !== explicitPort) return false;
41
+ if (!portWildcard && !explicitPort && origin.port !== parsedPattern.port) return false;
42
+ if (rawHost.startsWith('*.')) {
43
+ const suffix = rawHost.slice(2);
44
+ return origin.hostname === suffix || origin.hostname.endsWith(`.${suffix}`);
45
+ }
46
+ return origin.hostname === rawHost;
47
+ }
48
+
49
+ export function bridgeError(code, message, id) {
50
+ return id
51
+ ? { protocolVersion: '0.1.0', type: 'error', id, error: { code, message } }
52
+ : { protocolVersion: '0.1.0', type: 'error', error: { code, message } };
53
+ }
@@ -0,0 +1,13 @@
1
+ export declare const DEFAULT_ALLOWED_ORIGIN_PATTERNS: readonly ["https://bolt.new", "https://*.bolt.new", "https://stackblitz.com", "https://*.stackblitz.com", "http://localhost:*", "http://127.0.0.1:*"];
2
+ export interface HostPageBridgeOptions {
3
+ agentUrl?: string;
4
+ allowedOrigins?: string[];
5
+ allowedOriginPatterns?: string[];
6
+ targetOrigin?: string;
7
+ }
8
+ export interface HostPageBridgeController {
9
+ close(): void;
10
+ }
11
+ export declare function isOriginAllowed(origin: string, patterns?: readonly string[]): boolean;
12
+ export declare function createHostPageBridge(options?: HostPageBridgeOptions): HostPageBridgeController;
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,+BAA+B,uJAOlC,CAAC;AAEX,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAC;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,wBAAwB;IACvC,KAAK,IAAI,IAAI,CAAC;CACf;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAE,SAAS,MAAM,EAAoC,GAAG,OAAO,CAQtH;AAsBD,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,qBAA0B,GAAG,wBAAwB,CAkFlG"}
@@ -0,0 +1,128 @@
1
+ import { parseMessage } from '@bridgekitux/protocol';
2
+ export const DEFAULT_ALLOWED_ORIGIN_PATTERNS = [
3
+ 'https://bolt.new',
4
+ 'https://*.bolt.new',
5
+ 'https://stackblitz.com',
6
+ 'https://*.stackblitz.com',
7
+ 'http://localhost:*',
8
+ 'http://127.0.0.1:*'
9
+ ];
10
+ export function isOriginAllowed(origin, patterns = DEFAULT_ALLOWED_ORIGIN_PATTERNS) {
11
+ let parsed;
12
+ try {
13
+ parsed = new URL(origin);
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ return patterns.some((pattern) => originMatchesPattern(parsed, pattern));
19
+ }
20
+ function originMatchesPattern(origin, pattern) {
21
+ let parsedPattern;
22
+ try {
23
+ parsedPattern = new URL(pattern.replace(':*', ':0'));
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ if (parsedPattern.protocol !== origin.protocol)
29
+ return false;
30
+ const rawHost = pattern.replace(/^[a-z]+:\/\//, '').replace(/:\*$/, '').replace(/:\d+$/, '');
31
+ const portWildcard = pattern.endsWith(':*');
32
+ const explicitPort = pattern.match(/:(\d+)$/)?.[1];
33
+ if (!portWildcard && explicitPort && origin.port !== explicitPort)
34
+ return false;
35
+ if (!portWildcard && !explicitPort && origin.port !== parsedPattern.port)
36
+ return false;
37
+ if (rawHost.startsWith('*.')) {
38
+ const suffix = rawHost.slice(2);
39
+ return origin.hostname === suffix || origin.hostname.endsWith(`.${suffix}`);
40
+ }
41
+ return origin.hostname === rawHost;
42
+ }
43
+ export function createHostPageBridge(options = {}) {
44
+ const agentUrl = options.agentUrl ?? 'ws://127.0.0.1:7777/bridgekit';
45
+ const allowedPatterns = options.allowedOriginPatterns ?? options.allowedOrigins ?? [...DEFAULT_ALLOWED_ORIGIN_PATTERNS];
46
+ let socket;
47
+ const queue = [];
48
+ const requestTargets = new Map();
49
+ const activeTargets = new Map();
50
+ function postToSource(source, message, origin) {
51
+ if (source && 'postMessage' in source) {
52
+ source.postMessage(message, origin);
53
+ return;
54
+ }
55
+ window.postMessage(message, origin);
56
+ }
57
+ function ensureSocket() {
58
+ if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING))
59
+ return socket;
60
+ socket = new WebSocket(agentUrl);
61
+ socket.addEventListener('open', () => {
62
+ for (const queued of queue.splice(0))
63
+ socket.send(JSON.stringify(queued));
64
+ });
65
+ socket.addEventListener('message', (event) => {
66
+ const message = parseMessage(JSON.parse(String(event.data)));
67
+ if (message.type === 'event') {
68
+ for (const target of activeTargets.values()) {
69
+ postToSource(target.source, { source: 'bridgekit-bridge', type: 'bridgekit.event', message }, target.origin);
70
+ }
71
+ return;
72
+ }
73
+ const responseTarget = message.id ? requestTargets.get(message.id) : undefined;
74
+ postToSource(responseTarget?.source, { source: 'bridgekit-bridge', type: 'bridgekit.response', message }, responseTarget?.origin ?? options.targetOrigin ?? window.location.origin);
75
+ if (message.id)
76
+ requestTargets.delete(message.id);
77
+ });
78
+ return socket;
79
+ }
80
+ function handler(event) {
81
+ if (!isOriginAllowed(event.origin, allowedPatterns))
82
+ return;
83
+ const data = event.data;
84
+ if (!data || data.source !== 'bridgekit-client')
85
+ return;
86
+ activeTargets.set(event.origin, { origin: event.origin, source: event.source });
87
+ if (data.type === 'bridgekit.detect') {
88
+ postToSource(event.source, { source: 'bridgekit-bridge', type: 'bridgekit.available', agentUrl }, event.origin);
89
+ return;
90
+ }
91
+ if (data.type !== 'bridgekit.request')
92
+ return;
93
+ let message;
94
+ try {
95
+ message = parseMessage(data.message);
96
+ }
97
+ catch (error) {
98
+ const bridgeError = error instanceof Error ? error.message : 'Invalid BridgeKit message';
99
+ postToSource(event.source, {
100
+ source: 'bridgekit-bridge',
101
+ type: 'bridgekit.response',
102
+ message: {
103
+ protocolVersion: '0.1.0',
104
+ type: 'error',
105
+ error: { code: 'invalid_bridge_message', message: bridgeError }
106
+ }
107
+ }, event.origin);
108
+ return;
109
+ }
110
+ if (message.id)
111
+ requestTargets.set(message.id, { origin: event.origin, source: event.source });
112
+ const active = ensureSocket();
113
+ if (active.readyState === WebSocket.OPEN)
114
+ active.send(JSON.stringify(message));
115
+ else
116
+ queue.push(message);
117
+ }
118
+ window.addEventListener('message', handler);
119
+ return {
120
+ close() {
121
+ window.removeEventListener('message', handler);
122
+ socket?.close();
123
+ requestTargets.clear();
124
+ activeTargets.clear();
125
+ }
126
+ };
127
+ }
128
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,93 @@
1
+ import { bridgeError, getPolicy, isOriginAllowed } from './policy.js';
2
+
3
+ let socket;
4
+ let activeAgentUrl;
5
+ const pending = new Map();
6
+ const ports = new Set();
7
+ const requestOwners = new Map();
8
+
9
+ function validBridgeMessage(message) {
10
+ return message && message.protocolVersion === '0.1.0' && typeof message.type === 'string' && (message.id === undefined || typeof message.id === 'string');
11
+ }
12
+
13
+ async function ensureSocket() {
14
+ const policy = await getPolicy();
15
+ if (socket && activeAgentUrl === policy.agentUrl && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) return socket;
16
+ if (socket) socket.close();
17
+ activeAgentUrl = policy.agentUrl;
18
+ socket = new WebSocket(policy.agentUrl);
19
+ socket.addEventListener('message', (event) => {
20
+ let message;
21
+ try {
22
+ message = JSON.parse(event.data);
23
+ } catch {
24
+ return;
25
+ }
26
+ if (message.type === 'event') {
27
+ for (const port of ports) port.postMessage({ source: 'bridgekit-extension', type: 'bridgekit.event', message });
28
+ return;
29
+ }
30
+ const owner = message.id ? requestOwners.get(message.id) : undefined;
31
+ const callback = message.id ? pending.get(message.id) : undefined;
32
+ if (message.id) {
33
+ pending.delete(message.id);
34
+ requestOwners.delete(message.id);
35
+ }
36
+ if (owner && ports.has(owner)) owner.postMessage({ source: 'bridgekit-extension', type: 'bridgekit.response', message });
37
+ else if (callback) callback(message);
38
+ });
39
+ socket.addEventListener('close', () => {
40
+ for (const [id, owner] of requestOwners.entries()) {
41
+ if (ports.has(owner)) owner.postMessage({ source: 'bridgekit-extension', type: 'bridgekit.response', message: bridgeError('bridge_closed', 'BridgeKit agent connection closed', id) });
42
+ }
43
+ requestOwners.clear();
44
+ pending.clear();
45
+ });
46
+ return socket;
47
+ }
48
+
49
+ async function isSenderAllowed(sender, pageOrigin) {
50
+ const policy = await getPolicy();
51
+ const senderOrigin = sender?.url ? new URL(sender.url).origin : pageOrigin;
52
+ return Boolean(pageOrigin && senderOrigin === pageOrigin && isOriginAllowed(pageOrigin, policy.allowedOrigins));
53
+ }
54
+
55
+ chrome.runtime.onConnect.addListener((port) => {
56
+ if (port.name !== 'bridgekit-page') return;
57
+ ports.add(port);
58
+ port.onDisconnect.addListener(() => {
59
+ ports.delete(port);
60
+ for (const [id, owner] of requestOwners.entries()) {
61
+ if (owner === port) requestOwners.delete(id);
62
+ }
63
+ });
64
+ port.onMessage.addListener((request) => {
65
+ void (async () => {
66
+ const pageOrigin = typeof request?.pageOrigin === 'string' ? request.pageOrigin : undefined;
67
+ if (!request || request.source !== 'bridgekit-client') return;
68
+ if (!(await isSenderAllowed(port.sender, pageOrigin))) {
69
+ port.postMessage({ source: 'bridgekit-extension', type: 'bridgekit.response', message: bridgeError('origin_not_allowed', `BridgeKit origin is not allowed: ${pageOrigin ?? 'unknown'}`, request.message?.id) });
70
+ return;
71
+ }
72
+ if (request.type === 'bridgekit.detect') {
73
+ const policy = await getPolicy();
74
+ port.postMessage({ source: 'bridgekit-extension', type: 'bridgekit.available', agentUrl: policy.agentUrl, allowed: true });
75
+ return;
76
+ }
77
+ if (request.type !== 'bridgekit.request' || !validBridgeMessage(request.message)) {
78
+ port.postMessage({ source: 'bridgekit-extension', type: 'bridgekit.response', message: bridgeError('invalid_bridge_message', 'Invalid BridgeKit message', request.message?.id) });
79
+ return;
80
+ }
81
+ const ws = await ensureSocket();
82
+ const send = () => {
83
+ if (request.message.id) requestOwners.set(request.message.id, port);
84
+ ws.send(JSON.stringify(request.message));
85
+ };
86
+ if (ws.readyState === WebSocket.OPEN) send();
87
+ else ws.addEventListener('open', send, { once: true });
88
+ ws.addEventListener('error', () => {
89
+ port.postMessage({ source: 'bridgekit-extension', type: 'bridgekit.response', message: bridgeError('agent_unavailable', 'BridgeKit local agent is not reachable', request.message.id) });
90
+ }, { once: true });
91
+ })();
92
+ });
93
+ });
@@ -0,0 +1,24 @@
1
+ const port = chrome.runtime.connect({ name: 'bridgekit-page' });
2
+ const pageOrigin = window.location.origin;
3
+
4
+ port.onMessage.addListener((message) => {
5
+ if (!message || message.source !== 'bridgekit-extension') return;
6
+ if (message.type === 'bridgekit.event') {
7
+ window.postMessage({ source: 'bridgekit-bridge', type: 'bridgekit.event', message: message.message }, pageOrigin);
8
+ return;
9
+ }
10
+ if (message.type === 'bridgekit.available') {
11
+ window.postMessage({ source: 'bridgekit-bridge', type: 'bridgekit.available', agentUrl: message.agentUrl, allowed: message.allowed }, pageOrigin);
12
+ return;
13
+ }
14
+ window.postMessage({ source: 'bridgekit-bridge', type: 'bridgekit.response', message: message.message }, pageOrigin);
15
+ });
16
+
17
+ window.addEventListener('message', (event) => {
18
+ if (event.source !== window || event.origin !== pageOrigin) return;
19
+ const data = event.data;
20
+ if (!data || data.source !== 'bridgekit-client') return;
21
+ port.postMessage({ ...data, pageOrigin });
22
+ });
23
+
24
+ port.postMessage({ source: 'bridgekit-client', type: 'bridgekit.detect', pageOrigin });
@@ -0,0 +1,30 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "BridgeKit Browser Bridge",
4
+ "version": "0.1.0",
5
+ "description": "Relays approved BridgeKit requests from WebContainers and browser sandboxes to the local BridgeKit agent.",
6
+ "permissions": ["storage"],
7
+ "host_permissions": ["http://127.0.0.1/*", "ws://127.0.0.1/*"],
8
+ "background": {
9
+ "service_worker": "background.js",
10
+ "type": "module"
11
+ },
12
+ "options_ui": {
13
+ "page": "options.html",
14
+ "open_in_tab": true
15
+ },
16
+ "content_scripts": [
17
+ {
18
+ "matches": [
19
+ "https://bolt.new/*",
20
+ "https://*.bolt.new/*",
21
+ "https://stackblitz.com/*",
22
+ "https://*.stackblitz.com/*",
23
+ "http://localhost/*",
24
+ "http://127.0.0.1/*"
25
+ ],
26
+ "js": ["content-script.js"],
27
+ "run_at": "document_start"
28
+ }
29
+ ]
30
+ }
@@ -0,0 +1,28 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>BridgeKit Browser Bridge Options</title>
6
+ <style>
7
+ body { font-family: system-ui, sans-serif; max-width: 760px; margin: 40px auto; padding: 0 20px; color: #0f172a; }
8
+ label { display: block; font-weight: 700; margin: 18px 0 8px; }
9
+ input, textarea { width: 100%; box-sizing: border-box; padding: 10px; border: 1px solid #cbd5e1; border-radius: 10px; font: inherit; }
10
+ textarea { min-height: 180px; font-family: ui-monospace, monospace; }
11
+ button { margin-top: 16px; margin-right: 8px; border: 0; border-radius: 999px; padding: 10px 14px; background: #0284c7; color: white; font-weight: 700; cursor: pointer; }
12
+ button.secondary { background: #475569; }
13
+ #status { margin-top: 16px; color: #166534; }
14
+ </style>
15
+ </head>
16
+ <body>
17
+ <h1>BridgeKit Browser Bridge</h1>
18
+ <p>Only origins listed here can ask the extension to relay BridgeKit requests to the local agent.</p>
19
+ <label for="agentUrl">Local agent WebSocket URL</label>
20
+ <input id="agentUrl" />
21
+ <label for="allowedOrigins">Allowed origin patterns, one per line</label>
22
+ <textarea id="allowedOrigins"></textarea>
23
+ <button id="save">Save</button>
24
+ <button id="defaults" class="secondary">Restore defaults</button>
25
+ <p id="status"></p>
26
+ <script type="module" src="options.js"></script>
27
+ </body>
28
+ </html>
@@ -0,0 +1,27 @@
1
+ import { DEFAULT_AGENT_URL, DEFAULT_ALLOWED_ORIGINS } from './policy.js';
2
+
3
+ const agentUrl = document.querySelector('#agentUrl');
4
+ const allowedOrigins = document.querySelector('#allowedOrigins');
5
+ const status = document.querySelector('#status');
6
+
7
+ async function load() {
8
+ const stored = await chrome.storage.local.get(['agentUrl', 'allowedOrigins']);
9
+ agentUrl.value = stored.agentUrl || DEFAULT_AGENT_URL;
10
+ allowedOrigins.value = (stored.allowedOrigins || DEFAULT_ALLOWED_ORIGINS).join('\n');
11
+ }
12
+
13
+ async function save() {
14
+ const origins = allowedOrigins.value.split('\n').map((line) => line.trim()).filter(Boolean);
15
+ await chrome.storage.local.set({ agentUrl: agentUrl.value.trim() || DEFAULT_AGENT_URL, allowedOrigins: origins.length ? origins : DEFAULT_ALLOWED_ORIGINS });
16
+ status.textContent = 'Saved BridgeKit browser bridge settings.';
17
+ }
18
+
19
+ async function restoreDefaults() {
20
+ agentUrl.value = DEFAULT_AGENT_URL;
21
+ allowedOrigins.value = DEFAULT_ALLOWED_ORIGINS.join('\n');
22
+ await save();
23
+ }
24
+
25
+ document.querySelector('#save').addEventListener('click', () => void save());
26
+ document.querySelector('#defaults').addEventListener('click', () => void restoreDefaults());
27
+ void load();
@@ -0,0 +1,53 @@
1
+ export const DEFAULT_AGENT_URL = 'ws://127.0.0.1:7777/bridgekit';
2
+ export const DEFAULT_ALLOWED_ORIGINS = [
3
+ 'https://bolt.new',
4
+ 'https://*.bolt.new',
5
+ 'https://stackblitz.com',
6
+ 'https://*.stackblitz.com',
7
+ 'http://localhost:*',
8
+ 'http://127.0.0.1:*'
9
+ ];
10
+
11
+ export async function getPolicy() {
12
+ const stored = await chrome.storage.local.get(['agentUrl', 'allowedOrigins']);
13
+ return {
14
+ agentUrl: typeof stored.agentUrl === 'string' && stored.agentUrl.length > 0 ? stored.agentUrl : DEFAULT_AGENT_URL,
15
+ allowedOrigins: Array.isArray(stored.allowedOrigins) && stored.allowedOrigins.length > 0 ? stored.allowedOrigins : DEFAULT_ALLOWED_ORIGINS
16
+ };
17
+ }
18
+
19
+ export function isOriginAllowed(origin, patterns) {
20
+ let parsed;
21
+ try {
22
+ parsed = new URL(origin);
23
+ } catch {
24
+ return false;
25
+ }
26
+ return patterns.some((pattern) => originMatchesPattern(parsed, pattern));
27
+ }
28
+
29
+ function originMatchesPattern(origin, pattern) {
30
+ let parsedPattern;
31
+ try {
32
+ parsedPattern = new URL(pattern.replace(':*', ':0'));
33
+ } catch {
34
+ return false;
35
+ }
36
+ if (parsedPattern.protocol !== origin.protocol) return false;
37
+ const rawHost = pattern.replace(/^[a-z]+:\/\//, '').replace(/:\*$/, '').replace(/:\d+$/, '');
38
+ const portWildcard = pattern.endsWith(':*');
39
+ const explicitPort = pattern.match(/:(\d+)$/)?.[1];
40
+ if (!portWildcard && explicitPort && origin.port !== explicitPort) return false;
41
+ if (!portWildcard && !explicitPort && origin.port !== parsedPattern.port) return false;
42
+ if (rawHost.startsWith('*.')) {
43
+ const suffix = rawHost.slice(2);
44
+ return origin.hostname === suffix || origin.hostname.endsWith(`.${suffix}`);
45
+ }
46
+ return origin.hostname === rawHost;
47
+ }
48
+
49
+ export function bridgeError(code, message, id) {
50
+ return id
51
+ ? { protocolVersion: '0.1.0', type: 'error', id, error: { code, message } }
52
+ : { protocolVersion: '0.1.0', type: 'error', error: { code, message } };
53
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@bridgekitux/browser-bridge",
3
+ "version": "0.1.0",
4
+ "license": "Apache-2.0",
5
+ "description": "BridgeKit host-page and browser-extension bridge helpers.",
6
+ "type": "module",
7
+ "main": "./dist/src/index.js",
8
+ "types": "./dist/src/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/src/index.d.ts",
12
+ "import": "./dist/src/index.js"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "tsc -b",
17
+ "test": "node --test dist/test/*.test.js",
18
+ "package:extension": "node scripts/package-extension.mjs"
19
+ },
20
+ "dependencies": {
21
+ "@bridgekitux/protocol": "0.1.0"
22
+ },
23
+ "files": [
24
+ "dist/src",
25
+ "dist/bridgekit-browser-bridge-extension.zip",
26
+ "dist/extension-unpacked",
27
+ "extension",
28
+ "README.md"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "keywords": [
34
+ "bridgekit",
35
+ "webcontainer",
36
+ "sandbox",
37
+ "capability",
38
+ "local-agent",
39
+ "browser-ide",
40
+ "browser-bridge"
41
+ ]
42
+ }