pi-chrome 0.15.13 → 0.15.15
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/CHANGELOG.md +8 -0
- package/README.md +5 -5
- package/SECURITY.md +3 -3
- package/docs/COMPARISON.md +1 -1
- package/docs/EXAMPLES.md +1 -1
- package/docs/FAQ.md +2 -2
- package/extensions/chrome-profile-bridge/browser-extension/manifest.json +1 -1
- package/extensions/chrome-profile-bridge/browser-extension/service_worker.js +0 -89
- package/extensions/chrome-profile-bridge/index.ts +10 -49
- package/package.json +1 -1
- package/extensions/chrome-profile-bridge/browser-extension/consent.css +0 -141
- package/extensions/chrome-profile-bridge/browser-extension/consent.html +0 -33
- package/extensions/chrome-profile-bridge/browser-extension/consent.js +0 -75
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable user-facing changes to `pi-chrome`.
|
|
4
4
|
|
|
5
|
+
## 0.15.15 — 2026-05-14
|
|
6
|
+
|
|
7
|
+
- **Terminal authorization restored.** `/chrome authorize` is back to terminal-based confirmation. Removed the browser-side Chrome consent page and companion-extension consent polling.
|
|
8
|
+
|
|
9
|
+
## 0.15.14 — 2026-05-14
|
|
10
|
+
|
|
11
|
+
- **Clearer consent wait state.** After the Chrome approval page opens, Pi now says “Approve or deny the Chrome approval page to continue” instead of looking stuck at the launch step.
|
|
12
|
+
|
|
5
13
|
## 0.15.13 — 2026-05-14
|
|
6
14
|
|
|
7
15
|
- **Fix Chrome-side consent hang.** `/chrome authorize` now launches the browser consent page as a short command, then polls for the decision. This avoids holding one long extension command open while the user reads/clicks the page, which could leave Pi stuck at “Opening Chrome approval page…”.
|
package/README.md
CHANGED
|
@@ -30,11 +30,11 @@ Then in Pi:
|
|
|
30
30
|
|
|
31
31
|
On macOS this opens `chrome://extensions`, reveals the bundled `browser-extension/` folder in Finder, and copies its path to your clipboard. In Chrome: **Developer mode** → **Load unpacked** → paste the path. Done.
|
|
32
32
|
|
|
33
|
-
Verify, then authorize current Pi session in
|
|
33
|
+
Verify, then authorize current Pi session in Pi:
|
|
34
34
|
|
|
35
35
|
```text
|
|
36
36
|
/chrome doctor
|
|
37
|
-
/chrome authorize
|
|
37
|
+
/chrome authorize
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
```text
|
|
@@ -162,7 +162,7 @@ Each tool is documented inline in Pi — agents see the parameters and gotchas (
|
|
|
162
162
|
|
|
163
163
|
### Authorization
|
|
164
164
|
|
|
165
|
-
Chrome control is locked by default. Before any agent can use `chrome_*` tools, explicitly authorize the current Pi session
|
|
165
|
+
Chrome control is locked by default. Before any agent can use `chrome_*` tools, explicitly authorize the current Pi session from the terminal with `/chrome authorize`.
|
|
166
166
|
|
|
167
167
|
```text
|
|
168
168
|
/chrome authorize # default: authorize for 15 minutes
|
|
@@ -173,7 +173,7 @@ Chrome control is locked by default. Before any agent can use `chrome_*` tools,
|
|
|
173
173
|
/chrome status # shows connection + auth + background
|
|
174
174
|
```
|
|
175
175
|
|
|
176
|
-
This protects your signed-in Chrome profile from accidental agent use
|
|
176
|
+
This protects your signed-in Chrome profile from accidental agent use. The loopback bridge also rejects browser-origin command requests so arbitrary web pages cannot call into `127.0.0.1:17318` through CORS.
|
|
177
177
|
|
|
178
178
|
### Run in background / watch modes
|
|
179
179
|
|
|
@@ -239,7 +239,7 @@ If you build a competing tool, please open a PR with your scores. We benchmark i
|
|
|
239
239
|
|
|
240
240
|
**Unpacked on purpose.** pi-chrome ships as an inspectable, MIT-licensed extension folder you load once with Developer Mode, so the local bridge and browser permissions are easy to audit and update without a Web Store release cycle. Every line is yours to read in [`extensions/chrome-profile-bridge/browser-extension/`](./extensions/chrome-profile-bridge/browser-extension). `/chrome doctor` reports the loaded extension version and warns when it drifts from your installed `pi-chrome`.
|
|
241
241
|
|
|
242
|
-
The companion extension runs in the Chrome profile where you install it and has broad tab/scripting permissions. Only install it from a package source you trust. Even after install, `chrome_*` tools stay locked until you run `/chrome authorize` in Pi
|
|
242
|
+
The companion extension runs in the Chrome profile where you install it and has broad tab/scripting permissions. Only install it from a package source you trust. Even after install, `chrome_*` tools stay locked until you run `/chrome authorize` in Pi. Use `/chrome revoke` to lock them again.
|
|
243
243
|
|
|
244
244
|
The Pi side listens on `127.0.0.1:17318` and rejects browser-origin command requests; ordinary web pages cannot use CORS to drive the bridge. The bundled Chrome extension currently polls that default port, so custom bridge ports are not supported without editing the extension source and reloading it.
|
|
245
245
|
|
package/SECURITY.md
CHANGED
|
@@ -9,13 +9,13 @@ Open a GitHub issue prefixed with `[security]` at https://github.com/tianrendong
|
|
|
9
9
|
`pi-chrome` is a developer tool you install knowingly. It is **not** designed to defend against:
|
|
10
10
|
|
|
11
11
|
- Hostile pages running in your Chrome trying to detect or escape automation. (Standard browser security boundaries still apply, but a hostile page that already runs in your tab can do anything that page can already do.)
|
|
12
|
-
- Other processes on your local machine. The bridge binds to `127.0.0.1:17318` (loopback only) and chrome_* tools require `/chrome authorize`
|
|
12
|
+
- Other processes on your local machine. The bridge binds to `127.0.0.1:17318` (loopback only) and chrome_* tools require `/chrome authorize` inside Pi, but the bridge does not authenticate arbitrary non-browser local callers. If your threat model includes hostile local processes running as you, run pi-chrome on a separate user account.
|
|
13
13
|
|
|
14
14
|
`pi-chrome` **is** designed to:
|
|
15
15
|
|
|
16
16
|
- Never exfiltrate page state to the network. All communication is loopback (`127.0.0.1`).
|
|
17
17
|
- Surface every action with an honest result envelope so the agent can't silently do the wrong thing.
|
|
18
|
-
- Keep Chrome control locked until the user explicitly runs `/chrome authorize` in the current Pi session
|
|
18
|
+
- Keep Chrome control locked until the user explicitly runs `/chrome authorize` in the current Pi session.
|
|
19
19
|
- Reject browser-origin command requests to the loopback bridge so ordinary web pages cannot use CORS to drive Chrome.
|
|
20
20
|
|
|
21
21
|
## The companion extension
|
|
@@ -26,7 +26,7 @@ The Chrome extension under `extensions/chrome-profile-bridge/browser-extension/`
|
|
|
26
26
|
|
|
27
27
|
- Loopback bridge only. No remote port. No telemetry.
|
|
28
28
|
- Chrome real input layer for interactive controls.
|
|
29
|
-
- Chrome control locked by default; `/chrome authorize`
|
|
29
|
+
- Chrome control locked by default; `/chrome authorize` unlocks current Pi session after terminal confirmation, `/chrome revoke` locks it again.
|
|
30
30
|
- Run-in-background optional; tab/window focus is observable by default (the user can see Pi acting).
|
|
31
31
|
|
|
32
32
|
## Custom ports
|
package/docs/COMPARISON.md
CHANGED
|
@@ -126,7 +126,7 @@ Yes — you can export cookies and replay them, or point Playwright at your exis
|
|
|
126
126
|
Different security boundary, not strictly safer.
|
|
127
127
|
|
|
128
128
|
- **CDP-based tools** require `chrome --remote-debugging-port=...`. That port is unauthenticated and exposes the whole browser to any local process. Easy to misconfigure.
|
|
129
|
-
- **pi-chrome** runs through an extension you install yourself with broad permissions (tabs, scripting, debugger, webNavigation). The bridge listens on `127.0.0.1:17318` loopback only, rejects browser-origin command requests, and keeps chrome_* tools locked until `/chrome authorize` is run in the current Pi session
|
|
129
|
+
- **pi-chrome** runs through an extension you install yourself with broad permissions (tabs, scripting, debugger, webNavigation). The bridge listens on `127.0.0.1:17318` loopback only, rejects browser-origin command requests, and keeps chrome_* tools locked until `/chrome authorize` is run in the current Pi session. **Only install the bundled extension if you trust the source you got the npm package from.**
|
|
130
130
|
|
|
131
131
|
If your threat model excludes extensions with broad permissions, neither approach is a fit — you want a sandboxed CI runner.
|
|
132
132
|
|
package/docs/EXAMPLES.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-chrome examples
|
|
2
2
|
|
|
3
|
-
Real, useful agent prompts. Drop any of these into Pi after running `/chrome onboard`, then `/chrome authorize
|
|
3
|
+
Real, useful agent prompts. Drop any of these into Pi after running `/chrome onboard`, then `/chrome authorize`. Each one uses Chrome tabs and accounts you already have.
|
|
4
4
|
|
|
5
5
|
## Daily workflow
|
|
6
6
|
|
package/docs/FAQ.md
CHANGED
|
@@ -26,11 +26,11 @@ That's Chrome's built-in warning when an extension uses `chrome.debugger`. pi-ch
|
|
|
26
26
|
|
|
27
27
|
No — pages cannot directly talk to extensions. Commands flow agent → local bridge (`127.0.0.1:17318`) → extension → tab. The bridge binds to loopback only and rejects browser-origin command requests, so ordinary web pages cannot use CORS to drive it.
|
|
28
28
|
|
|
29
|
-
Chrome control is also locked per Pi session until you run `/chrome authorize
|
|
29
|
+
Chrome control is also locked per Pi session until you run `/chrome authorize`; `/chrome revoke` locks it again. The remaining risk surface is **other local processes running as you** that can connect to loopback and imitate Pi. If that's in your threat model, run pi-chrome in a separate OS user account.
|
|
30
30
|
|
|
31
31
|
## Can multiple Pi sessions use it at once?
|
|
32
32
|
|
|
33
|
-
Yes. The first session opens the local bridge; later sessions detect it and pipe their commands through the same bridge. Each Pi session must be authorized with `/chrome authorize`
|
|
33
|
+
Yes. The first session opens the local bridge; later sessions detect it and pipe their commands through the same bridge. Each Pi session must be authorized with `/chrome authorize` before its chrome_* tools work.
|
|
34
34
|
|
|
35
35
|
## Why can't this be on the Chrome Web Store?
|
|
36
36
|
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
const BRIDGE_URL = "http://127.0.0.1:17318";
|
|
2
2
|
const CLIENT_NAME = `Pi Chrome Connector ${chrome.runtime.id}`;
|
|
3
3
|
const POLL_ERROR_BACKOFF_MS = 2000;
|
|
4
|
-
const CONSENT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
5
4
|
let polling = false;
|
|
6
|
-
let nextConsentRequestId = 1;
|
|
7
|
-
const pendingConsentRequests = new Map(); // id -> { request, timer, tabId }
|
|
8
|
-
const completedConsentRequests = new Map(); // id -> { approved, reason, id, decidedAt }
|
|
9
5
|
|
|
10
6
|
// =================== Chrome input (CDP) layer ===================
|
|
11
7
|
// Tracks which tabs we have attached chrome.debugger to.
|
|
@@ -595,87 +591,6 @@ async function chromeInputUpload(params) {
|
|
|
595
591
|
// ===============================================================
|
|
596
592
|
|
|
597
593
|
|
|
598
|
-
function consentRequestSnapshot(id, request) {
|
|
599
|
-
return {
|
|
600
|
-
id,
|
|
601
|
-
extensionVersion: chrome.runtime.getManifest().version,
|
|
602
|
-
extensionId: chrome.runtime.id,
|
|
603
|
-
workspace: String(request.workspace || ""),
|
|
604
|
-
pid: request.pid ?? null,
|
|
605
|
-
durationLabel: String(request.durationLabel || "15 minutes"),
|
|
606
|
-
requestedAt: request.requestedAt || Date.now(),
|
|
607
|
-
piChromeVersion: String(request.piChromeVersion || ""),
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
function completeBrowserConsent(id, approved, reason, closeTab = true) {
|
|
612
|
-
const pending = pendingConsentRequests.get(id);
|
|
613
|
-
if (!pending) return completedConsentRequests.get(id) || null;
|
|
614
|
-
pendingConsentRequests.delete(id);
|
|
615
|
-
clearTimeout(pending.timer);
|
|
616
|
-
if (closeTab && pending.tabId) chrome.tabs.remove(pending.tabId).catch(() => undefined);
|
|
617
|
-
const result = { approved, reason, id, decidedAt: Date.now() };
|
|
618
|
-
completedConsentRequests.set(id, result);
|
|
619
|
-
setTimeout(() => completedConsentRequests.delete(id), 10 * 60 * 1000);
|
|
620
|
-
return result;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
async function requestBrowserConsent(params) {
|
|
624
|
-
const id = String(nextConsentRequestId++);
|
|
625
|
-
const request = {
|
|
626
|
-
...params,
|
|
627
|
-
requestedAt: Date.now(),
|
|
628
|
-
};
|
|
629
|
-
const timer = setTimeout(() => completeBrowserConsent(id, false, "timed out waiting for browser approval"), CONSENT_TIMEOUT_MS);
|
|
630
|
-
pendingConsentRequests.set(id, { request, timer, tabId: null });
|
|
631
|
-
const url = chrome.runtime.getURL(`consent.html?id=${encodeURIComponent(id)}`);
|
|
632
|
-
try {
|
|
633
|
-
const tab = await chrome.tabs.create({ url, active: true });
|
|
634
|
-
if (tab.windowId !== undefined) await chrome.windows.update(tab.windowId, { focused: true }).catch(() => undefined);
|
|
635
|
-
const pending = pendingConsentRequests.get(id);
|
|
636
|
-
if (pending) pending.tabId = tab.id;
|
|
637
|
-
} catch (error) {
|
|
638
|
-
completeBrowserConsent(id, false, `could not open consent tab: ${error?.message || error}`, false);
|
|
639
|
-
}
|
|
640
|
-
return { id, opened: pendingConsentRequests.has(id), timeoutMs: CONSENT_TIMEOUT_MS };
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
function browserConsentStatus(params) {
|
|
644
|
-
const id = String(params?.id || "");
|
|
645
|
-
const completed = completedConsentRequests.get(id);
|
|
646
|
-
if (completed) return { state: "done", ...completed };
|
|
647
|
-
const pending = pendingConsentRequests.get(id);
|
|
648
|
-
if (pending) return { state: "pending", id, requestedAt: pending.request.requestedAt, timeoutMs: CONSENT_TIMEOUT_MS };
|
|
649
|
-
return { state: "missing", id, approved: false, reason: "consent request expired or not found" };
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
653
|
-
if (!message || typeof message !== "object") return false;
|
|
654
|
-
if (message.type === "piChromeConsentGet") {
|
|
655
|
-
const id = String(message.id || "");
|
|
656
|
-
const pending = pendingConsentRequests.get(id);
|
|
657
|
-
sendResponse(pending ? { ok: true, request: consentRequestSnapshot(id, pending.request) } : { ok: false, error: "Consent request expired or not found" });
|
|
658
|
-
return true;
|
|
659
|
-
}
|
|
660
|
-
if (message.type === "piChromeConsentDecision") {
|
|
661
|
-
const id = String(message.id || "");
|
|
662
|
-
const result = completeBrowserConsent(id, message.approved === true, message.approved === true ? "approved in Chrome" : "denied in Chrome");
|
|
663
|
-
if (!result) {
|
|
664
|
-
sendResponse({ ok: false, error: "Consent request expired or not found" });
|
|
665
|
-
return true;
|
|
666
|
-
}
|
|
667
|
-
sendResponse({ ok: true });
|
|
668
|
-
return true;
|
|
669
|
-
}
|
|
670
|
-
return false;
|
|
671
|
-
});
|
|
672
|
-
|
|
673
|
-
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
674
|
-
for (const [id, pending] of pendingConsentRequests) {
|
|
675
|
-
if (pending.tabId === tabId) completeBrowserConsent(id, false, "consent tab closed", false);
|
|
676
|
-
}
|
|
677
|
-
});
|
|
678
|
-
|
|
679
594
|
function armKeepaliveAlarm() {
|
|
680
595
|
chrome.alarms.create("pi-bridge-keepalive", { periodInMinutes: 0.5 });
|
|
681
596
|
}
|
|
@@ -771,10 +686,6 @@ async function dispatch(action, params) {
|
|
|
771
686
|
bridgeUrl: BRIDGE_URL,
|
|
772
687
|
userAgent: navigator.userAgent,
|
|
773
688
|
};
|
|
774
|
-
case "consent.request":
|
|
775
|
-
return requestBrowserConsent(params);
|
|
776
|
-
case "consent.status":
|
|
777
|
-
return browserConsentStatus(params);
|
|
778
689
|
case "tab.list":
|
|
779
690
|
return (await chrome.tabs.query({})).map(formatTab);
|
|
780
691
|
case "tab.new": {
|
|
@@ -463,8 +463,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
463
463
|
return bridge.send(action, params, timeoutMs);
|
|
464
464
|
};
|
|
465
465
|
|
|
466
|
-
const sleep = (ms: number) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
467
|
-
|
|
468
466
|
// Translate the public `background` parameter (default false = visible/foreground) into the
|
|
469
467
|
// service worker's wire-level `foreground` flag, accepting legacy `foreground` as a fallback.
|
|
470
468
|
const withBackground = <T extends Record<string, unknown>>(params: T): T => {
|
|
@@ -611,53 +609,16 @@ Usage rules:
|
|
|
611
609
|
};
|
|
612
610
|
|
|
613
611
|
const authorizeFor = async (ctx: ExtensionContext, label: string, until: number | "indefinite") => {
|
|
614
|
-
ctx.ui.
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
pid: process.pid,
|
|
621
|
-
piChromeVersion: PI_CHROME_VERSION,
|
|
622
|
-
}, 10_000)) as { id?: string; opened?: boolean; timeoutMs?: number };
|
|
623
|
-
} catch (error) {
|
|
624
|
-
const message = (error as Error).message;
|
|
625
|
-
const hint = message.includes("Unknown action: consent.request")
|
|
626
|
-
? "Open chrome://extensions and reload 'Pi Chrome Connector', then try /chrome authorize again."
|
|
627
|
-
: "Run /chrome doctor if the companion extension is not responding.";
|
|
628
|
-
ctx.ui.notify(`Chrome approval failed: ${message}\n${hint}`, "warning");
|
|
629
|
-
return;
|
|
630
|
-
}
|
|
631
|
-
if (!request.id) {
|
|
632
|
-
ctx.ui.notify("Chrome approval failed: companion extension did not return a consent request id.", "warning");
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
635
|
-
if (!request.opened) {
|
|
636
|
-
ctx.ui.notify("Chrome approval failed: companion extension could not open the consent page.", "warning");
|
|
612
|
+
const ok = await ctx.ui.confirm(
|
|
613
|
+
"Authorize pi-chrome control?",
|
|
614
|
+
`This Pi session will be allowed to inspect and control your existing Chrome profile for ${label}.\n\nChrome actions use your signed-in browser state and real input. Only approve if you trust the current agent/task.`,
|
|
615
|
+
);
|
|
616
|
+
if (!ok) {
|
|
617
|
+
ctx.ui.notify("Chrome control remains locked.", "info");
|
|
637
618
|
return;
|
|
638
619
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
while (Date.now() < deadline + 2_000) {
|
|
642
|
-
await sleep(700);
|
|
643
|
-
let status: { state?: string; approved?: boolean; reason?: string };
|
|
644
|
-
try {
|
|
645
|
-
status = (await bridge.send("consent.status", { id: request.id }, 5_000)) as { state?: string; approved?: boolean; reason?: string };
|
|
646
|
-
} catch (error) {
|
|
647
|
-
ctx.ui.notify(`Chrome approval failed: ${(error as Error).message}`, "warning");
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
|
-
if (status.state !== "pending") {
|
|
651
|
-
if (!status.approved) {
|
|
652
|
-
ctx.ui.notify(`Chrome control remains locked${status.reason ? ` (${status.reason})` : ""}.`, "info");
|
|
653
|
-
return;
|
|
654
|
-
}
|
|
655
|
-
chromeAuthorizedUntil = until;
|
|
656
|
-
ctx.ui.notify(`Chrome control authorized for ${label}.`, "info");
|
|
657
|
-
return;
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
ctx.ui.notify("Chrome control remains locked (timed out waiting for browser approval).", "info");
|
|
620
|
+
chromeAuthorizedUntil = until;
|
|
621
|
+
ctx.ui.notify(`Chrome control authorized for ${label}.`, "info");
|
|
661
622
|
};
|
|
662
623
|
|
|
663
624
|
const parseAuthorizeArg = (arg: string): { label: string; until: number | "indefinite" } | undefined => {
|
|
@@ -782,7 +743,7 @@ Usage rules:
|
|
|
782
743
|
|
|
783
744
|
pi.registerCommand("chrome", {
|
|
784
745
|
description:
|
|
785
|
-
"All pi-chrome controls in one place.\n /chrome authorize [15m|30m|<minutes>|indefinite] —
|
|
746
|
+
"All pi-chrome controls in one place.\n /chrome authorize [15m|30m|<minutes>|indefinite] — allow this Pi session to use chrome_* tools.\n /chrome revoke — lock Chrome control.\n /chrome status — one-line snapshot of connection, auth, and background setting.\n /chrome doctor — full health check.\n /chrome onboard — install the Chrome companion extension.\n /chrome background [on|off|status|toggle] — whether pi-chrome runs without focusing Chrome.\nRun with no arguments for an interactive picker that shows current state.",
|
|
786
747
|
getArgumentCompletions: (prefix) => {
|
|
787
748
|
const raw = prefix;
|
|
788
749
|
const trimmedRight = raw.replace(/\s+$/, "");
|
|
@@ -799,7 +760,7 @@ Usage rules:
|
|
|
799
760
|
let candidates: Item[] = [];
|
|
800
761
|
if (path.length === 0) {
|
|
801
762
|
candidates = [
|
|
802
|
-
{ fullValue: "authorize", label: "authorize", description: "
|
|
763
|
+
{ fullValue: "authorize", label: "authorize", description: "Allow this Pi session to use chrome_* tools." },
|
|
803
764
|
{ fullValue: "revoke", label: "revoke", description: "Lock Chrome control for this Pi session." },
|
|
804
765
|
{ fullValue: "status", label: "status", description: "One-line summary: connection, auth, and background setting." },
|
|
805
766
|
{ fullValue: "doctor", label: "doctor", description: "Full health check. Tells you if Chrome is connected and what's wrong if it isn't." },
|
package/package.json
CHANGED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
:root {
|
|
2
|
-
color-scheme: light dark;
|
|
3
|
-
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
4
|
-
background: #0f172a;
|
|
5
|
-
color: #e5e7eb;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
* { box-sizing: border-box; }
|
|
9
|
-
|
|
10
|
-
body {
|
|
11
|
-
margin: 0;
|
|
12
|
-
min-height: 100vh;
|
|
13
|
-
display: grid;
|
|
14
|
-
place-items: center;
|
|
15
|
-
padding: 32px;
|
|
16
|
-
background:
|
|
17
|
-
radial-gradient(circle at 20% 10%, rgba(79, 70, 229, 0.35), transparent 30%),
|
|
18
|
-
radial-gradient(circle at 80% 80%, rgba(14, 165, 233, 0.2), transparent 32%),
|
|
19
|
-
#0f172a;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
.card {
|
|
23
|
-
width: min(680px, 100%);
|
|
24
|
-
background: rgba(15, 23, 42, 0.9);
|
|
25
|
-
border: 1px solid rgba(148, 163, 184, 0.25);
|
|
26
|
-
border-radius: 24px;
|
|
27
|
-
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.4);
|
|
28
|
-
padding: 32px;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
.badge {
|
|
32
|
-
display: inline-flex;
|
|
33
|
-
align-items: center;
|
|
34
|
-
border: 1px solid rgba(129, 140, 248, 0.45);
|
|
35
|
-
border-radius: 999px;
|
|
36
|
-
padding: 6px 12px;
|
|
37
|
-
color: #c7d2fe;
|
|
38
|
-
background: rgba(79, 70, 229, 0.18);
|
|
39
|
-
font-size: 13px;
|
|
40
|
-
font-weight: 700;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
h1 {
|
|
44
|
-
margin: 18px 0 8px;
|
|
45
|
-
font-size: clamp(32px, 6vw, 48px);
|
|
46
|
-
line-height: 1;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
.lead {
|
|
50
|
-
margin: 0 0 24px;
|
|
51
|
-
color: #cbd5e1;
|
|
52
|
-
font-size: 18px;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
.details {
|
|
56
|
-
display: grid;
|
|
57
|
-
gap: 12px;
|
|
58
|
-
margin: 24px 0;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
.details div {
|
|
62
|
-
display: grid;
|
|
63
|
-
grid-template-columns: 140px 1fr;
|
|
64
|
-
gap: 16px;
|
|
65
|
-
align-items: start;
|
|
66
|
-
padding: 14px 16px;
|
|
67
|
-
border-radius: 14px;
|
|
68
|
-
background: rgba(30, 41, 59, 0.72);
|
|
69
|
-
border: 1px solid rgba(148, 163, 184, 0.14);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.details span {
|
|
73
|
-
color: #94a3b8;
|
|
74
|
-
font-size: 13px;
|
|
75
|
-
font-weight: 700;
|
|
76
|
-
text-transform: uppercase;
|
|
77
|
-
letter-spacing: 0.05em;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
.details strong {
|
|
81
|
-
color: #f8fafc;
|
|
82
|
-
font-size: 15px;
|
|
83
|
-
overflow-wrap: anywhere;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.warning {
|
|
87
|
-
margin: 0 0 24px;
|
|
88
|
-
padding: 16px;
|
|
89
|
-
border-radius: 14px;
|
|
90
|
-
background: rgba(245, 158, 11, 0.12);
|
|
91
|
-
border: 1px solid rgba(245, 158, 11, 0.35);
|
|
92
|
-
color: #fde68a;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
.actions {
|
|
96
|
-
display: flex;
|
|
97
|
-
justify-content: flex-end;
|
|
98
|
-
gap: 12px;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
button {
|
|
102
|
-
border: 0;
|
|
103
|
-
border-radius: 12px;
|
|
104
|
-
padding: 12px 18px;
|
|
105
|
-
font: inherit;
|
|
106
|
-
font-weight: 800;
|
|
107
|
-
cursor: pointer;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
button:focus-visible {
|
|
111
|
-
outline: 3px solid #93c5fd;
|
|
112
|
-
outline-offset: 2px;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
.primary {
|
|
116
|
-
background: #4f46e5;
|
|
117
|
-
color: white;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
.primary:hover { background: #4338ca; }
|
|
121
|
-
|
|
122
|
-
.secondary {
|
|
123
|
-
background: rgba(148, 163, 184, 0.16);
|
|
124
|
-
color: #e2e8f0;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
.secondary:hover { background: rgba(148, 163, 184, 0.26); }
|
|
128
|
-
|
|
129
|
-
.status {
|
|
130
|
-
min-height: 20px;
|
|
131
|
-
margin: 18px 0 0;
|
|
132
|
-
color: #cbd5e1;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
@media (max-width: 560px) {
|
|
136
|
-
body { padding: 16px; }
|
|
137
|
-
.card { padding: 22px; }
|
|
138
|
-
.details div { grid-template-columns: 1fr; gap: 6px; }
|
|
139
|
-
.actions { flex-direction: column-reverse; }
|
|
140
|
-
button { width: 100%; }
|
|
141
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>Authorize pi-chrome</title>
|
|
7
|
-
<link rel="stylesheet" href="consent.css">
|
|
8
|
-
</head>
|
|
9
|
-
<body>
|
|
10
|
-
<main class="card">
|
|
11
|
-
<div class="badge">Pi Chrome Connector</div>
|
|
12
|
-
<h1>Authorize Chrome control?</h1>
|
|
13
|
-
<p class="lead">Pi is asking to inspect and control this Chrome profile.</p>
|
|
14
|
-
|
|
15
|
-
<section class="details" aria-label="Request details">
|
|
16
|
-
<div><span>Duration</span><strong id="duration">—</strong></div>
|
|
17
|
-
<div><span>Workspace</span><strong id="workspace">—</strong></div>
|
|
18
|
-
<div><span>Pi process</span><strong id="pid">—</strong></div>
|
|
19
|
-
<div><span>Extension</span><strong id="extension">—</strong></div>
|
|
20
|
-
</section>
|
|
21
|
-
|
|
22
|
-
<p class="warning">Approve only if you trust this Pi session and current task. Approved actions use your signed-in browser state and real Chrome input.</p>
|
|
23
|
-
|
|
24
|
-
<div class="actions">
|
|
25
|
-
<button id="deny" class="secondary" type="button">Deny</button>
|
|
26
|
-
<button id="approve" class="primary" type="button" autofocus>Authorize</button>
|
|
27
|
-
</div>
|
|
28
|
-
|
|
29
|
-
<p id="status" class="status" role="status"></p>
|
|
30
|
-
</main>
|
|
31
|
-
<script src="consent.js"></script>
|
|
32
|
-
</body>
|
|
33
|
-
</html>
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
const params = new URLSearchParams(location.search);
|
|
2
|
-
const id = params.get("id") || "";
|
|
3
|
-
|
|
4
|
-
const els = {
|
|
5
|
-
duration: document.getElementById("duration"),
|
|
6
|
-
workspace: document.getElementById("workspace"),
|
|
7
|
-
pid: document.getElementById("pid"),
|
|
8
|
-
extension: document.getElementById("extension"),
|
|
9
|
-
approve: document.getElementById("approve"),
|
|
10
|
-
deny: document.getElementById("deny"),
|
|
11
|
-
status: document.getElementById("status"),
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
function setStatus(text) {
|
|
15
|
-
els.status.textContent = text || "";
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function setDisabled(disabled) {
|
|
19
|
-
els.approve.disabled = disabled;
|
|
20
|
-
els.deny.disabled = disabled;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function render(request) {
|
|
24
|
-
els.duration.textContent = request.durationLabel || "—";
|
|
25
|
-
els.workspace.textContent = request.workspace || "unknown workspace";
|
|
26
|
-
els.pid.textContent = request.pid ? String(request.pid) : "unknown";
|
|
27
|
-
const versions = [];
|
|
28
|
-
if (request.extensionVersion) versions.push(`extension ${request.extensionVersion}`);
|
|
29
|
-
if (request.piChromeVersion) versions.push(`pi-chrome ${request.piChromeVersion}`);
|
|
30
|
-
els.extension.textContent = versions.join(" · ") || request.extensionId || "—";
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function send(message) {
|
|
34
|
-
return await chrome.runtime.sendMessage(message);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function decide(approved) {
|
|
38
|
-
setDisabled(true);
|
|
39
|
-
setStatus(approved ? "Authorizing…" : "Denying…");
|
|
40
|
-
try {
|
|
41
|
-
const response = await send({ type: "piChromeConsentDecision", id, approved });
|
|
42
|
-
if (!response?.ok) throw new Error(response?.error || "Consent request failed");
|
|
43
|
-
setStatus(approved ? "Authorized. You can close this tab." : "Denied. You can close this tab.");
|
|
44
|
-
window.close();
|
|
45
|
-
} catch (error) {
|
|
46
|
-
setDisabled(false);
|
|
47
|
-
setStatus(error?.message || String(error));
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async function init() {
|
|
52
|
-
if (!id) {
|
|
53
|
-
setDisabled(true);
|
|
54
|
-
setStatus("Missing consent request id.");
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
setStatus("Loading request…");
|
|
58
|
-
try {
|
|
59
|
-
const response = await send({ type: "piChromeConsentGet", id });
|
|
60
|
-
if (!response?.ok) throw new Error(response?.error || "Consent request not found");
|
|
61
|
-
render(response.request || {});
|
|
62
|
-
setStatus("");
|
|
63
|
-
} catch (error) {
|
|
64
|
-
setDisabled(true);
|
|
65
|
-
setStatus(error?.message || String(error));
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
els.approve.addEventListener("click", () => decide(true));
|
|
70
|
-
els.deny.addEventListener("click", () => decide(false));
|
|
71
|
-
document.addEventListener("keydown", (event) => {
|
|
72
|
-
if (event.key === "Escape") decide(false);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
void init();
|