- Encrypted browser state kept under stable names. + Sign-ins your extensions declare, kept as encrypted browser state.
- Give the sign-in a stable slug, then open its login window to authenticate. -
-No browser sessions yet.
+No installed extension declares a browser session.
)} {sessions.length > 0 && ({CONNECTION_COPY[connection]}
diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 30b918e3..95a420c5 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -1242,8 +1242,6 @@ textarea.set-textarea { background-image: none; height: auto; min-height: 96px; .svc-alt:hover { color: var(--text); } .svc-pane a.set-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } -.browser-session-create { display: grid; grid-template-columns: minmax(150px, 0.8fr) minmax(180px, 1fr) minmax(150px, 0.8fr) auto; gap: 12px; align-items: end; } -.browser-session-create-button { white-space: nowrap; } .browser-session-list { display: flex; flex-direction: column; gap: 8px; } .browser-session-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px 18px; padding: 13px 14px; } .browser-session-identity { display: flex; align-items: baseline; gap: 9px; min-width: 0; } @@ -1260,7 +1258,7 @@ textarea.set-textarea { background-image: none; height: auto; min-height: 96px; .browser-session-times dt { font-size: 11.5px; color: var(--text-faint); } .browser-session-times dd { margin: 0; font-family: var(--font-mono); font-size: 11.5px; color: var(--text-mid); } .browser-session-actions { display: flex; justify-content: flex-end; gap: 8px; } -.browser-session-rename { grid-column: 1 / -1; display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 8px; } +.browser-session-undeclared { align-self: center; font-size: 11.5px; color: var(--text-faint); } .page-login-window .page-shell-body { flex: 1; min-height: 0; background: #080a0c; } .login-window-head { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 16px 20px; border-bottom: 1px solid var(--border); background: var(--surface); } @@ -1279,7 +1277,6 @@ textarea.set-textarea { background-image: none; height: auto; min-height: 96px; @media (max-width: 720px) { .set-extension-toggles { grid-template-columns: 1fr; } .svc-grid { grid-template-columns: 1fr; } - .browser-session-create { grid-template-columns: 1fr 1fr; } .set-thead { display: none; } .set-trow { grid-template-columns: 1fr 1fr; gap: 8px; padding: 12px 14px; } .skill-add { flex-direction: column; align-items: stretch; } @@ -1297,7 +1294,6 @@ textarea.set-textarea { background-image: none; height: auto; min-height: 96px; } @media (max-width: 460px) { - .browser-session-create { grid-template-columns: 1fr; } .browser-session-times { flex-direction: column; gap: 5px; } } diff --git a/scripts/import_browser_session.py b/scripts/import_browser_session.py index 9a3c2f97..838ff336 100755 --- a/scripts/import_browser_session.py +++ b/scripts/import_browser_session.py @@ -15,7 +15,9 @@ def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Capture a headed Chromium login and import it into the Druks session vault." ) - parser.add_argument("--name", required=True, help="Browser-session slug, for example x-main.") + parser.add_argument( + "--name", required=True, help="Declared browser-session name, for example x_me.x." + ) parser.add_argument("--site-url", required=True, help="Login page to open in Chromium.") parser.add_argument( "--druks-url", @@ -62,13 +64,14 @@ def request_json( except urllib.error.URLError as error: raise RuntimeError(f"Could not reach Druks at {base_url}: {error.reason}") from error with response: - try: - return json.load(response) - except json.JSONDecodeError as error: - raise RuntimeError( - f"Druks returned a non-JSON response for {method} {path}; " - "check the dashboard URL and session headers." - ) from error + body = response.read() + try: + return json.loads(body) if body else None + except json.JSONDecodeError as error: + raise RuntimeError( + f"Druks returned a non-JSON response for {method} {path}; " + "check the dashboard URL and session headers." + ) from error def load_playwright() -> Any: @@ -94,26 +97,10 @@ def main() -> int: sync_playwright = load_playwright() sessions = request_json(args.druks_url, "/api/browser-sessions", headers=headers) - browser_session = next((row for row in sessions if row["name"] == args.name), None) - if browser_session: - if browser_session["payloadFormat"] != "storage_state": - raise RuntimeError( - f"Browser session {args.name!r} uses {browser_session['payloadFormat']!r}; " - "this importer captures storage_state." - ) - else: - browser_session = request_json( - args.druks_url, - "/api/browser-sessions", - headers=headers, - method="POST", - data=json.dumps( - { - "name": args.name, - "payloadFormat": "storage_state", - "site": urllib.parse.urlparse(args.site_url).hostname, - } - ).encode(), + if args.name not in {row["name"] for row in sessions if row["isDeclared"]}: + raise RuntimeError( + f"Browser session {args.name!r} is not declared by any installed " + "extension; declare it on the extension class first." ) with tempfile.TemporaryDirectory(prefix="druks-browser-session-") as temporary_directory: @@ -127,16 +114,17 @@ def main() -> int: context.storage_state(path=str(storage_state_path)) browser.close() - imported = request_json( + request_json( args.druks_url, - f"/api/browser-sessions/{browser_session['id']}/state", + f"/api/browser-sessions/{urllib.parse.quote(args.name)}/state" + "?payloadFormat=storage_state", headers=headers, method="PUT", data=storage_state_path.read_bytes(), content_type="application/octet-stream", ) - print(f"Imported {imported['name']}: {imported['status']}") + print(f"Imported {args.name}.") return 0