cookiesync-cli 0.1.2__tar.gz → 0.1.4__tar.gz
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.
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/PKG-INFO +29 -38
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/README.md +28 -37
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/cli.py +9 -4
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/cookie/consent.py +3 -3
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/daemon/server.py +7 -5
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/service.py +7 -4
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/pyproject.toml +1 -1
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/LICENSE +0 -0
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/__init__.py +0 -0
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/__main__.py +0 -0
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/cookie/__init__.py +0 -0
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/cookie/backend.py +0 -0
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/cookie/browsers.py +0 -0
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/cookie/crypto.py +0 -0
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/cookie/domains.py +0 -0
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/cookie/getcookie.py +0 -0
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/cookie/merge.py +0 -0
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/cookie/models.py +0 -0
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/cookie/pipeline.py +0 -0
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/cookie/serialize.py +0 -0
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/cookie/stores.py +0 -0
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/daemon/__init__.py +0 -0
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/daemon/backend_ssh.py +0 -0
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/daemon/cache.py +0 -0
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/daemon/engine.py +0 -0
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/daemon/rpc.py +0 -0
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/daemon/session.py +0 -0
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/daemon/sync.py +0 -0
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/daemon/wire.py +0 -0
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/helper.py +0 -0
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/paths.py +0 -0
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/py.typed +0 -0
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/registry.py +0 -0
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/state.py +0 -0
- {cookiesync_cli-0.1.2 → cookiesync_cli-0.1.4}/cookiesync/transport.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cookiesync-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Sync your browser cookies across machines.
|
|
5
5
|
Keywords:
|
|
6
6
|
Author: Yasyf Mohamedali
|
|
@@ -38,53 +38,46 @@ Description-Content-Type: text/markdown
|
|
|
38
38
|
[](https://pypi.org/project/cookiesync-cli/)
|
|
39
39
|
[](https://github.com/yasyf/cookiesync/blob/main/LICENSE)
|
|
40
40
|
|
|
41
|
-
Sync your browser cookies across machines.
|
|
41
|
+
Sync your browser cookies across machines — land on a new laptop already logged in.
|
|
42
42
|
|
|
43
|
-
cookiesync copies the cookies your browser already holds on one machine and
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
cookiesync copies the cookies your browser already holds on one machine and replays
|
|
44
|
+
them on another, reusing your live session instead of asking for passwords again — so
|
|
45
|
+
logins, 2FA, and SSO state carry over without re-authenticating. It's browser-agnostic,
|
|
46
|
+
and you pick which machines and which sites it touches. Automation can borrow a
|
|
47
|
+
logged-in session too: hand a CI job or an agent the cookies it needs, never a password.
|
|
48
48
|
|
|
49
|
-
> **macOS only.** cookiesync keeps your browser's Safe Storage key behind a
|
|
50
|
-
>
|
|
51
|
-
>
|
|
49
|
+
> **macOS only.** cookiesync keeps your browser's Safe Storage key behind a Touch ID
|
|
50
|
+
> prompt and a Secure Enclave–bound daemon, so decrypted cookies never land on disk.
|
|
51
|
+
> The key helper is a Developer-ID-signed, notarized `.app`.
|
|
52
52
|
|
|
53
53
|
## Install
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
command. You'll reach for it often, so install it onto your PATH with
|
|
57
|
-
[uv](https://docs.astral.sh/uv/):
|
|
55
|
+
Install the `cookiesync` command with [uv](https://docs.astral.sh/uv/):
|
|
58
56
|
|
|
59
57
|
```bash
|
|
60
58
|
uv tool install cookiesync-cli
|
|
61
59
|
cookiesync --help
|
|
62
60
|
```
|
|
63
61
|
|
|
64
|
-
To add it to a project instead:
|
|
65
|
-
|
|
66
|
-
```bash
|
|
67
|
-
uv add cookiesync-cli
|
|
68
|
-
```
|
|
69
|
-
|
|
70
62
|
## Quickstart
|
|
71
63
|
|
|
72
64
|
```bash
|
|
73
|
-
# Fetch the signed key helper and
|
|
74
|
-
cookiesync install
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
cookiesync
|
|
65
|
+
# Fetch the signed key helper and install the LaunchAgents (one time)
|
|
66
|
+
$ cookiesync install
|
|
67
|
+
Installing the signed key helper via Homebrew (brew install yasyf/tap/cookiesync-keyhelper)…
|
|
68
|
+
Installed and verified key helper: /Applications/cookiesync-keyhelper.app
|
|
69
|
+
Installed cookiesync agents.
|
|
78
70
|
|
|
79
71
|
# Track a browser to sync between this Mac and another host
|
|
80
|
-
cookiesync browser add other-host chrome
|
|
72
|
+
$ cookiesync browser add other-host chrome
|
|
73
|
+
Tracking other-host/chrome/Default
|
|
81
74
|
|
|
82
|
-
# Hand a logged-in session to a script
|
|
83
|
-
cookiesync cookies https://example.com --browser chrome
|
|
75
|
+
# Hand a logged-in session to a script — no password
|
|
76
|
+
$ cookiesync cookies https://example.com --browser chrome
|
|
84
77
|
```
|
|
85
78
|
|
|
86
|
-
Once a browser is tracked, the resident daemon watches its cookie store and
|
|
87
|
-
|
|
79
|
+
Once a browser is tracked, the resident daemon watches its cookie store and converges
|
|
80
|
+
it across your hosts. Run `cookiesync reconcile` to force a full pass.
|
|
88
81
|
|
|
89
82
|
## Commands
|
|
90
83
|
|
|
@@ -104,16 +97,14 @@ converges it across your hosts. Run `cookiesync reconcile` to force a full pass.
|
|
|
104
97
|
|
|
105
98
|
Run `cookiesync --help`, or `cookiesync <command> --help`, for the full reference.
|
|
106
99
|
|
|
107
|
-
##
|
|
100
|
+
## How it works
|
|
108
101
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
- Automation needs a logged-in session but should never hold a password. Hand a
|
|
116
|
-
CI job or an agent the cookies it needs instead of a credential it can leak.
|
|
102
|
+
`cookiesync install` fetches the notarized key helper and starts a resident daemon. The
|
|
103
|
+
daemon watches each tracked browser's cookie store, and on a change it converges that
|
|
104
|
+
group across your hosts over SSH — extracting and re-applying cookies through the same
|
|
105
|
+
RPC the peers speak. Decryption needs the browser's Safe Storage key, which the helper
|
|
106
|
+
releases only behind a Touch ID tap (`cookiesync auth`) and caches in the
|
|
107
|
+
Secure-Enclave-bound daemon for a short window, never on disk.
|
|
117
108
|
|
|
118
109
|
## License
|
|
119
110
|
|
|
@@ -6,53 +6,46 @@
|
|
|
6
6
|
[](https://pypi.org/project/cookiesync-cli/)
|
|
7
7
|
[](https://github.com/yasyf/cookiesync/blob/main/LICENSE)
|
|
8
8
|
|
|
9
|
-
Sync your browser cookies across machines.
|
|
9
|
+
Sync your browser cookies across machines — land on a new laptop already logged in.
|
|
10
10
|
|
|
11
|
-
cookiesync copies the cookies your browser already holds on one machine and
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
cookiesync copies the cookies your browser already holds on one machine and replays
|
|
12
|
+
them on another, reusing your live session instead of asking for passwords again — so
|
|
13
|
+
logins, 2FA, and SSO state carry over without re-authenticating. It's browser-agnostic,
|
|
14
|
+
and you pick which machines and which sites it touches. Automation can borrow a
|
|
15
|
+
logged-in session too: hand a CI job or an agent the cookies it needs, never a password.
|
|
16
16
|
|
|
17
|
-
> **macOS only.** cookiesync keeps your browser's Safe Storage key behind a
|
|
18
|
-
>
|
|
19
|
-
>
|
|
17
|
+
> **macOS only.** cookiesync keeps your browser's Safe Storage key behind a Touch ID
|
|
18
|
+
> prompt and a Secure Enclave–bound daemon, so decrypted cookies never land on disk.
|
|
19
|
+
> The key helper is a Developer-ID-signed, notarized `.app`.
|
|
20
20
|
|
|
21
21
|
## Install
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
command. You'll reach for it often, so install it onto your PATH with
|
|
25
|
-
[uv](https://docs.astral.sh/uv/):
|
|
23
|
+
Install the `cookiesync` command with [uv](https://docs.astral.sh/uv/):
|
|
26
24
|
|
|
27
25
|
```bash
|
|
28
26
|
uv tool install cookiesync-cli
|
|
29
27
|
cookiesync --help
|
|
30
28
|
```
|
|
31
29
|
|
|
32
|
-
To add it to a project instead:
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
uv add cookiesync-cli
|
|
36
|
-
```
|
|
37
|
-
|
|
38
30
|
## Quickstart
|
|
39
31
|
|
|
40
32
|
```bash
|
|
41
|
-
# Fetch the signed key helper and
|
|
42
|
-
cookiesync install
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
cookiesync
|
|
33
|
+
# Fetch the signed key helper and install the LaunchAgents (one time)
|
|
34
|
+
$ cookiesync install
|
|
35
|
+
Installing the signed key helper via Homebrew (brew install yasyf/tap/cookiesync-keyhelper)…
|
|
36
|
+
Installed and verified key helper: /Applications/cookiesync-keyhelper.app
|
|
37
|
+
Installed cookiesync agents.
|
|
46
38
|
|
|
47
39
|
# Track a browser to sync between this Mac and another host
|
|
48
|
-
cookiesync browser add other-host chrome
|
|
40
|
+
$ cookiesync browser add other-host chrome
|
|
41
|
+
Tracking other-host/chrome/Default
|
|
49
42
|
|
|
50
|
-
# Hand a logged-in session to a script
|
|
51
|
-
cookiesync cookies https://example.com --browser chrome
|
|
43
|
+
# Hand a logged-in session to a script — no password
|
|
44
|
+
$ cookiesync cookies https://example.com --browser chrome
|
|
52
45
|
```
|
|
53
46
|
|
|
54
|
-
Once a browser is tracked, the resident daemon watches its cookie store and
|
|
55
|
-
|
|
47
|
+
Once a browser is tracked, the resident daemon watches its cookie store and converges
|
|
48
|
+
it across your hosts. Run `cookiesync reconcile` to force a full pass.
|
|
56
49
|
|
|
57
50
|
## Commands
|
|
58
51
|
|
|
@@ -72,16 +65,14 @@ converges it across your hosts. Run `cookiesync reconcile` to force a full pass.
|
|
|
72
65
|
|
|
73
66
|
Run `cookiesync --help`, or `cookiesync <command> --help`, for the full reference.
|
|
74
67
|
|
|
75
|
-
##
|
|
68
|
+
## How it works
|
|
76
69
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
- Automation needs a logged-in session but should never hold a password. Hand a
|
|
84
|
-
CI job or an agent the cookies it needs instead of a credential it can leak.
|
|
70
|
+
`cookiesync install` fetches the notarized key helper and starts a resident daemon. The
|
|
71
|
+
daemon watches each tracked browser's cookie store, and on a change it converges that
|
|
72
|
+
group across your hosts over SSH — extracting and re-applying cookies through the same
|
|
73
|
+
RPC the peers speak. Decryption needs the browser's Safe Storage key, which the helper
|
|
74
|
+
releases only behind a Touch ID tap (`cookiesync auth`) and caches in the
|
|
75
|
+
Secure-Enclave-bound daemon for a short window, never on disk.
|
|
85
76
|
|
|
86
77
|
## License
|
|
87
78
|
|
|
@@ -119,6 +119,9 @@ async def run_watch() -> None:
|
|
|
119
119
|
from cookiesync.daemon import Daemon
|
|
120
120
|
|
|
121
121
|
logger.debug("starting cookiesync daemon")
|
|
122
|
+
# The daemon needs the signed helper for the SE key vault and cache; install it
|
|
123
|
+
# if absent so the helper is fully CLI-managed (the user never runs brew by hand).
|
|
124
|
+
await ensure_helper()
|
|
122
125
|
await (await Daemon.build()).watch()
|
|
123
126
|
|
|
124
127
|
|
|
@@ -217,16 +220,18 @@ async def run_sync(browser_name: str) -> None:
|
|
|
217
220
|
@main.command()
|
|
218
221
|
@click.option("--browser", "browser_name", default="chrome", show_default=True, help="The browser to authenticate.")
|
|
219
222
|
@click.option("--profile", default="Default", show_default=True, help="The profile to authenticate.")
|
|
223
|
+
@click.option("--reason", default=None, help="What the Touch ID prompt should say you're unlocking the cookies to do.")
|
|
220
224
|
@click.option("--ttl", default=None, help="Override the cache TTL (Go-style duration, e.g. 15m).")
|
|
221
|
-
def auth(browser_name: str, profile: str, ttl: str | None) -> None:
|
|
225
|
+
def auth(browser_name: str, profile: str, reason: str | None, ttl: str | None) -> None:
|
|
222
226
|
"""Release the Safe Storage key behind one Touch ID tap and cache it for a short window."""
|
|
223
|
-
anyio.run(run_auth, browser_name, profile, ttl)
|
|
227
|
+
anyio.run(run_auth, browser_name, profile, reason, ttl)
|
|
224
228
|
|
|
225
229
|
|
|
226
|
-
async def run_auth(browser_name: str, profile: str, ttl: str | None) -> None:
|
|
230
|
+
async def run_auth(browser_name: str, profile: str, reason: str | None, ttl: str | None) -> None:
|
|
227
231
|
if ttl is not None:
|
|
228
232
|
await state.update(lambda s: replace(s, settings=replace(s.settings, auth_ttl=parse_duration(ttl))))
|
|
229
|
-
|
|
233
|
+
params = {"browser": browser_name, "profile": profile} | ({"reason": reason} if reason else {})
|
|
234
|
+
result = await daemon_call("prime_auth", params)
|
|
230
235
|
click.echo(f"Authenticated {result['endpoint']}.")
|
|
231
236
|
|
|
232
237
|
|
|
@@ -46,12 +46,12 @@ class ConsentError(Exception):
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def compose_reason(host: str, reason: str) -> str:
|
|
49
|
-
"""Touch ID prompt text:
|
|
49
|
+
"""Touch ID prompt text: concise and specific — what is unlocked, then a short why.
|
|
50
50
|
|
|
51
51
|
``reason`` is collapsed to a single line and capped, since it surfaces verbatim in
|
|
52
|
-
|
|
52
|
+
the Touch ID dialog.
|
|
53
53
|
"""
|
|
54
|
-
return f"
|
|
54
|
+
return f"unlock your {host} cookies to {' '.join(reason.split())[:REASON_CAP]}"
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
class Consent(Protocol):
|
|
@@ -65,7 +65,7 @@ if TYPE_CHECKING:
|
|
|
65
65
|
PEER_METHODS = ("sync", "reconcile", "extract", "apply", "whoami")
|
|
66
66
|
LOCAL_METHODS = ("prime_auth", "get_cookies", "auth_status", "request_consent")
|
|
67
67
|
|
|
68
|
-
CONSENT_REASON = "sync
|
|
68
|
+
CONSENT_REASON = "sync them across your Macs"
|
|
69
69
|
DEFAULT_PROFILE = "Default"
|
|
70
70
|
|
|
71
71
|
|
|
@@ -274,10 +274,12 @@ class Daemon:
|
|
|
274
274
|
state = await self.load_state()
|
|
275
275
|
browser = BrowserId(params["browser"])
|
|
276
276
|
profile = params.get("profile", DEFAULT_PROFILE)
|
|
277
|
-
await self.prime_auth(browser, profile, state)
|
|
277
|
+
await self.prime_auth(browser, profile, state, reason=params.get("reason") or CONSENT_REASON)
|
|
278
278
|
return {"primed": True, "endpoint": endpoint_id(state.self_target, browser, profile)}
|
|
279
279
|
|
|
280
|
-
async def prime_auth(
|
|
280
|
+
async def prime_auth(
|
|
281
|
+
self, browser: BrowserId, profile: str, state: State, *, reason: str = CONSENT_REASON
|
|
282
|
+
) -> AesKey:
|
|
281
283
|
"""Obtain the Safe Storage key and cache it under the endpoint's TTL.
|
|
282
284
|
|
|
283
285
|
A live local session releases the key behind one Touch-ID tap here. Otherwise the user
|
|
@@ -288,7 +290,7 @@ class Daemon:
|
|
|
288
290
|
:class:`AuthRequired` when no peer can approve or the reply fails to bind.
|
|
289
291
|
"""
|
|
290
292
|
if await has_active_session(probe=self.probe):
|
|
291
|
-
key = await self.consent.obtain_key(browser_for(browser), reason=
|
|
293
|
+
key = await self.consent.obtain_key(browser_for(browser), reason=reason)
|
|
292
294
|
else:
|
|
293
295
|
key = await self.routed_release(browser, profile, state)
|
|
294
296
|
await self.cache.put(
|
|
@@ -368,7 +370,7 @@ class Daemon:
|
|
|
368
370
|
if not await has_active_session(probe=self.probe):
|
|
369
371
|
return {"status": "unavailable"}
|
|
370
372
|
try:
|
|
371
|
-
await self.consent.obtain_key(browser_for(browser), reason=f"
|
|
373
|
+
await self.consent.obtain_key(browser_for(browser), reason=f"sync them to {endpoint}")
|
|
372
374
|
except ConsentError:
|
|
373
375
|
return {"status": "denied"}
|
|
374
376
|
return {"status": "approved", "nonce": nonce, "endpoint": endpoint}
|
|
@@ -41,8 +41,11 @@ SESSION_TYPE = "Aqua"
|
|
|
41
41
|
|
|
42
42
|
RECONCILE_INTERVAL = 900
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
# launchctl's tolerated-error strings vary by macOS version: pre-13 said "Could not
|
|
45
|
+
# find specified service" / "service already loaded"; 13+ says "No such process" (exit 3)
|
|
46
|
+
# for a not-loaded bootout. Tolerate every spelling so install/uninstall stay idempotent.
|
|
47
|
+
ALREADY_LOADED = ("service already loaded", "service already bootstrapped")
|
|
48
|
+
NOT_LOADED = ("Could not find specified service", "No such process")
|
|
46
49
|
|
|
47
50
|
|
|
48
51
|
class ServiceError(Exception):
|
|
@@ -165,12 +168,12 @@ def agent_for(label: Label, program_args: Sequence[str]) -> AgentSpec:
|
|
|
165
168
|
raise ServiceError(f"{label} runs {agent.command!r}, not {list(program_args)!r}")
|
|
166
169
|
|
|
167
170
|
|
|
168
|
-
async def run_launchctl(*args: str, tolerate: str) -> None:
|
|
171
|
+
async def run_launchctl(*args: str, tolerate: tuple[str, ...]) -> None:
|
|
169
172
|
result = await anyio.run_process(["launchctl", *args], check=False)
|
|
170
173
|
match result.returncode:
|
|
171
174
|
case 0:
|
|
172
175
|
return
|
|
173
|
-
case _ if
|
|
176
|
+
case _ if any(t in result.stderr.decode() for t in tolerate):
|
|
174
177
|
return
|
|
175
178
|
case code:
|
|
176
179
|
raise ServiceError(f"launchctl {args[0]}: exit {code}: {result.stderr.decode().strip()}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|