Document who can call the loopback API and let the user pick the listen port.
This commit is contained in:
+64
-1
@@ -2,6 +2,7 @@
|
||||
|
||||
const HOST_NAME = "com.easygoingaming.firefox_agent_bridge";
|
||||
const RECONNECT_MS = 2000;
|
||||
const DEFAULT_PORT = 17634;
|
||||
|
||||
const ALLOWED = {
|
||||
"meta.ping": async () => ({ ok: true, extension: "bookmarks-api" }),
|
||||
@@ -22,6 +23,47 @@ const ALLOWED = {
|
||||
let port = null;
|
||||
let reconnectTimer = null;
|
||||
let hostUp = false;
|
||||
let listenPort = DEFAULT_PORT;
|
||||
let bindError = "";
|
||||
let configWaiter = null;
|
||||
|
||||
function normalizePort(value) {
|
||||
const n = Number(value);
|
||||
if (!Number.isInteger(n) || n < 1024 || n > 65535) {
|
||||
throw new Error("Port must be an integer from 1024 to 65535");
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
async function loadListenPort() {
|
||||
const data = await browser.storage.local.get("listenPort");
|
||||
try {
|
||||
return normalizePort(data.listenPort == null ? DEFAULT_PORT : data.listenPort);
|
||||
} catch (err) {
|
||||
return DEFAULT_PORT;
|
||||
}
|
||||
}
|
||||
|
||||
async function pushConfig() {
|
||||
if (!port) {
|
||||
return;
|
||||
}
|
||||
listenPort = await loadListenPort();
|
||||
await new Promise((resolve) => {
|
||||
configWaiter = resolve;
|
||||
port.postMessage({ type: "config", port: listenPort });
|
||||
setTimeout(resolve, 2500);
|
||||
});
|
||||
}
|
||||
|
||||
function statusInfo() {
|
||||
return {
|
||||
hostUp,
|
||||
port: listenPort,
|
||||
listenPort,
|
||||
bindError,
|
||||
};
|
||||
}
|
||||
|
||||
function asArray(value) {
|
||||
if (value === undefined || value === null) {
|
||||
@@ -43,6 +85,16 @@ function attach(nextPort) {
|
||||
port = nextPort;
|
||||
hostUp = true;
|
||||
port.onMessage.addListener(async (message) => {
|
||||
if (message && message.type === "config-result") {
|
||||
listenPort = message.port || listenPort;
|
||||
bindError = message.ok ? "" : (message.error || "could not bind port");
|
||||
if (configWaiter) {
|
||||
const done = configWaiter;
|
||||
configWaiter = null;
|
||||
done();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (message && message.type === "auth") {
|
||||
try {
|
||||
const client = await findClientByTokenHash(message.token_hash);
|
||||
@@ -80,8 +132,10 @@ function attach(nextPort) {
|
||||
port.onDisconnect.addListener(() => {
|
||||
port = null;
|
||||
hostUp = false;
|
||||
bindError = "";
|
||||
scheduleReconnect();
|
||||
});
|
||||
pushConfig();
|
||||
}
|
||||
|
||||
function scheduleReconnect() {
|
||||
@@ -106,7 +160,16 @@ function connect() {
|
||||
|
||||
browser.runtime.onMessage.addListener((message) => {
|
||||
if (message && message.type === "status") {
|
||||
return Promise.resolve({ hostUp, port: 17634 });
|
||||
return Promise.resolve(statusInfo());
|
||||
}
|
||||
if (message && message.type === "set-port") {
|
||||
return (async () => {
|
||||
const n = normalizePort(message.port);
|
||||
await browser.storage.local.set({ listenPort: n });
|
||||
listenPort = n;
|
||||
await pushConfig();
|
||||
return statusInfo();
|
||||
})();
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "Bookmarks API for Scripting and AI",
|
||||
"short_name": "Bookmarks API",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.2",
|
||||
"description": "An API add-on for Firefox to allow agentic and script-based management of user bookmarks.",
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
<header>
|
||||
<h1>Bookmarks API for Scripting and AI</h1>
|
||||
<p class="lead">An API add-on for Firefox to allow agentic and script-based management of user bookmarks.</p>
|
||||
<section class="usage">
|
||||
<h2>Who can use this</h2>
|
||||
<p>Tools on <strong>this computer</strong> can call the API — a local script, an editor agent, or a session you opened here (SSH, remote desktop). They need a secret you issue below.</p>
|
||||
<p>It does not work from chatgpt.com or other sites in a browser tab. Those run elsewhere and cannot see this machine’s loopback port. Pages inside Firefox are blocked too.</p>
|
||||
<p class="muted">If this Firefox profile syncs bookmarks with a Mozilla account, changes made here can show up on your other devices. The API itself still only accepts connections on this machine.</p>
|
||||
</section>
|
||||
<ol class="process">
|
||||
<li>Issue a secret for each agent or script that should be allowed to work with your bookmarks.</li>
|
||||
<li>Store that secret with your secrets. This add-on keeps only a hash.</li>
|
||||
@@ -17,6 +23,16 @@
|
||||
<p id="status" class="muted"></p>
|
||||
</header>
|
||||
|
||||
<section class="settings">
|
||||
<h2>Listen port</h2>
|
||||
<p class="muted">Loopback only (<code>127.0.0.1</code>). Change this if the default is already in use. Your tools must use the same port.</p>
|
||||
<div class="row">
|
||||
<label class="sr" for="port">Listen port</label>
|
||||
<input id="port" type="number" min="1024" max="65535" step="1" value="17634">
|
||||
<button id="save-port" type="button">Save port</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="row">
|
||||
<input id="name" type="text" maxlength="64" placeholder="Client name" autocomplete="off">
|
||||
<button id="create" type="button">Generate secret</button>
|
||||
|
||||
+25
-4
@@ -5,11 +5,26 @@ const nameInput = document.getElementById("name");
|
||||
const createBtn = document.getElementById("create");
|
||||
const errorEl = document.getElementById("error");
|
||||
const statusEl = document.getElementById("status");
|
||||
const portInput = document.getElementById("port");
|
||||
const savePortBtn = document.getElementById("save-port");
|
||||
const modal = document.getElementById("secret-modal");
|
||||
const secretEl = document.getElementById("secret");
|
||||
const copyBtn = document.getElementById("copy");
|
||||
const closeBtn = document.getElementById("close");
|
||||
|
||||
function applyStatus(info) {
|
||||
if (info && info.listenPort) {
|
||||
portInput.value = String(info.listenPort);
|
||||
}
|
||||
if (info && info.bindError) {
|
||||
statusEl.textContent = `Could not listen on ${info.listenPort}: ${info.bindError}`;
|
||||
return;
|
||||
}
|
||||
statusEl.textContent = info && info.hostUp
|
||||
? `This add-on is ready on 127.0.0.1:${info.listenPort} while Firefox is open.`
|
||||
: "You can issue and revoke secrets here anytime.";
|
||||
}
|
||||
|
||||
function showError(text) {
|
||||
errorEl.hidden = !text;
|
||||
errorEl.textContent = text || "";
|
||||
@@ -43,15 +58,21 @@ function escapeHtml(value) {
|
||||
async function refresh() {
|
||||
render(await listClients());
|
||||
try {
|
||||
const info = await browser.runtime.sendMessage({ type: "status" });
|
||||
statusEl.textContent = info && info.hostUp
|
||||
? "This add-on is ready for agents and scripts while Firefox is open."
|
||||
: "You can issue and revoke secrets here anytime.";
|
||||
applyStatus(await browser.runtime.sendMessage({ type: "status" }));
|
||||
} catch (err) {
|
||||
statusEl.textContent = "You can issue and revoke secrets here anytime.";
|
||||
}
|
||||
}
|
||||
|
||||
savePortBtn.addEventListener("click", async () => {
|
||||
showError("");
|
||||
try {
|
||||
applyStatus(await browser.runtime.sendMessage({ type: "set-port", port: portInput.value }));
|
||||
} catch (err) {
|
||||
showError(err.message);
|
||||
}
|
||||
});
|
||||
|
||||
createBtn.addEventListener("click", async () => {
|
||||
showError("");
|
||||
try {
|
||||
|
||||
+8
-4
@@ -4,11 +4,15 @@ const statusEl = document.getElementById("status");
|
||||
const manage = document.getElementById("manage");
|
||||
|
||||
browser.runtime.sendMessage({ type: "status" }).then((info) => {
|
||||
if (info && info.hostUp) {
|
||||
statusEl.textContent = "Ready for agents and scripts while Firefox is open.";
|
||||
} else {
|
||||
statusEl.textContent = "You can issue and revoke secrets here.";
|
||||
if (info && info.bindError) {
|
||||
statusEl.textContent = `Port ${info.listenPort} is not available.`;
|
||||
return;
|
||||
}
|
||||
if (info && info.hostUp) {
|
||||
statusEl.textContent = `Ready on 127.0.0.1:${info.listenPort} while Firefox is open.`;
|
||||
return;
|
||||
}
|
||||
statusEl.textContent = "You can issue and revoke secrets here.";
|
||||
});
|
||||
|
||||
manage.addEventListener("click", () => {
|
||||
|
||||
@@ -18,6 +18,12 @@ body {
|
||||
}
|
||||
h1, h2 { margin: 0 0 8px; font-size: 1.15rem; }
|
||||
.lead { margin: 0 0 12px; }
|
||||
.usage, .settings { margin: 0 0 16px; }
|
||||
.usage p { margin: 0 0 8px; }
|
||||
input[type="number"] {
|
||||
width: 8rem;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
.process { margin: 0 0 16px; padding-left: 1.2rem; }
|
||||
.process li { margin: 6px 0; }
|
||||
.muted { color: GrayText; }
|
||||
|
||||
Reference in New Issue
Block a user