claude-multiacc 2.0.65 → 2.0.67
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/bin/claude-accounts +11 -197
- package/docs/INSTALLATION.md +4 -3
- package/docs/RELEASING.md +75 -0
- package/lib/claude_oauth_lock.py +172 -0
- package/lib/claude_oauth_refresh.py +225 -0
- package/package.json +3 -2
- package/tests/autoresume_cross_tmux_environment.py +4 -1
- package/tests/claude_oauth_fixture.py +100 -0
- package/tests/run-tests.sh +3 -1
- package/tests/test_claude_oauth_lock.py +138 -0
- package/tests/test_claude_oauth_refresh.py +106 -0
- package/tests/test_claude_oauth_refresh_failures.py +112 -0
- package/tests/test_release_validation.py +826 -0
- package/lib/__pycache__/audit.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_cross.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_dispatch.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_receipt.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_handoff.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_probe.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_receipt.cpython-314.pyc +0 -0
- package/lib/__pycache__/claude_client_error.cpython-314.pyc +0 -0
- package/lib/__pycache__/claude_reset.cpython-314.pyc +0 -0
- package/lib/__pycache__/claude_token_identity.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_autoresume_mode.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_config_edit.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_credit_controls.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_credit_local_scope.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_credit_selection.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_credits.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_limits_lock.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_mcp_oauth.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_python.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_resume_cwd.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_speed.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_trust.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_usage_fetch.cpython-314.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-314.pyc +0 -0
- package/lib/__pycache__/mcp_mirror.cpython-314.pyc +0 -0
- package/lib/__pycache__/mcp_oauth.cpython-314.pyc +0 -0
- package/lib/__pycache__/mcp_registry.cpython-314.pyc +0 -0
- package/lib/__pycache__/model_catalog.cpython-314.pyc +0 -0
- package/lib/__pycache__/model_match.cpython-314.pyc +0 -0
- package/lib/__pycache__/report.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_credit_scope.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_credits.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_funding.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_policy.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_primitives.cpython-314.pyc +0 -0
- package/lib/__pycache__/session_settings.cpython-314.pyc +0 -0
- package/lib/__pycache__/shim_path.cpython-314.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_receipt_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-314.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_unit_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/autoresume_tmux_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/packaged_command_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_catalog.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_catalog_cli.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_records.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_trust.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_guards.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_holders.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_origin.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_policy.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_receipt.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tokens.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cursor.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd_handoff.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd_reader.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_fallback.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_argv.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_history.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_origins.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_mode.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_probe.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_receipt.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_settings.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux_modes.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_trust_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_claude_reset.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_claude_token_identity.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_exec_stdin.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_mcp_oauth.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_polling.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_report.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_fetch.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_polling.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_reporting.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_windows.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_mcp_mirror.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_mcp_mirror_args.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_mcp_oauth.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_mcp_registry.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_cached_credits.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_scope.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_shim.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_credits.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_spread.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_token_identity_ceremony.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_token_identity_integration.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_token_identity_retry.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_token_identity_sync.cpython-314.pyc +0 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""A failed refresh belongs only to its posted grant, never a newer sign-in."""
|
|
3
|
+
|
|
4
|
+
import copy
|
|
5
|
+
import io
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import unittest
|
|
9
|
+
from unittest import mock
|
|
10
|
+
from urllib.error import HTTPError
|
|
11
|
+
|
|
12
|
+
from claude_oauth_fixture import AcquireCallbackLock, OAuthRefreshFixture, claude_oauth_refresh
|
|
13
|
+
|
|
14
|
+
PRIVATE_FIXTURE = "fixture-private-provider-detail"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def invalid_grant():
|
|
18
|
+
body = json.dumps({"error": "invalid_grant", "detail": PRIVATE_FIXTURE}).encode()
|
|
19
|
+
return HTTPError("https://provider.invalid/token", 400, PRIVATE_FIXTURE, {}, io.BytesIO(body))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ClaudeOAuthRefreshFailureTest(OAuthRefreshFixture, unittest.TestCase):
|
|
23
|
+
def assert_locks_released(self):
|
|
24
|
+
for path in (self.account / ".oauth_refresh.lock", Path(str(self.account.resolve()) + ".lock"),
|
|
25
|
+
self.account / ".storage-write.lock"):
|
|
26
|
+
self.assertFalse(path.exists(), f"refresh left its lock behind: {path.name}")
|
|
27
|
+
|
|
28
|
+
def test_changed_login_is_not_parked_after_old_grant_denial(self):
|
|
29
|
+
newer = copy.deepcopy(self.original)
|
|
30
|
+
newer["claudeAiOauth"].update(accessToken="fixture-new-login-access",
|
|
31
|
+
refreshToken="fixture-new-login-refresh",
|
|
32
|
+
expiresAt=int((self.now + 3600) * 1000))
|
|
33
|
+
self.on_post = lambda: self.write(newer)
|
|
34
|
+
self.error = invalid_grant()
|
|
35
|
+
self.assertIsNone(self.refresh())
|
|
36
|
+
self.park.assert_not_called()
|
|
37
|
+
self.assertFalse((self.account / ".oauth-refresh.json").exists())
|
|
38
|
+
self.assertEqual(self.read(), newer)
|
|
39
|
+
self.assert_locks_released()
|
|
40
|
+
|
|
41
|
+
def test_logout_during_denial_is_not_recreated_or_parked(self):
|
|
42
|
+
self.on_post = self.path.unlink
|
|
43
|
+
self.error = invalid_grant()
|
|
44
|
+
self.assertIsNone(self.refresh())
|
|
45
|
+
self.park.assert_not_called()
|
|
46
|
+
self.assertFalse((self.account / ".oauth-refresh.json").exists())
|
|
47
|
+
self.assertFalse(self.path.exists())
|
|
48
|
+
self.assert_locks_released()
|
|
49
|
+
|
|
50
|
+
def test_matching_invalid_grant_parks_backs_off_and_releases_locks(self):
|
|
51
|
+
self.error = invalid_grant()
|
|
52
|
+
self.assertIsNone(self.refresh())
|
|
53
|
+
self.park.assert_called_once()
|
|
54
|
+
self.assertEqual(self.park.call_args.args[2], "refresh-denied-http-400")
|
|
55
|
+
backoff = (self.account / ".oauth-refresh.json").read_text(encoding="utf-8")
|
|
56
|
+
state = json.loads(backoff)
|
|
57
|
+
self.assertEqual(state["denials"], 1)
|
|
58
|
+
self.assertEqual(state["retry_after"], int(self.now + 21600))
|
|
59
|
+
self.assertEqual(self.read(), self.original)
|
|
60
|
+
recorded = "\n".join(self.messages) + backoff + str(self.park.call_args)
|
|
61
|
+
for secret in (PRIVATE_FIXTURE, "fixture-old-access", "fixture-old-refresh"):
|
|
62
|
+
self.assertNotIn(secret, recorded)
|
|
63
|
+
self.assert_locks_released()
|
|
64
|
+
|
|
65
|
+
def test_signin_during_denial_write_lock_acquisition_is_not_parked(self):
|
|
66
|
+
newer = copy.deepcopy(self.original)
|
|
67
|
+
newer["claudeAiOauth"]["refreshToken"] = "fixture-new-login-refresh"
|
|
68
|
+
storage_lock = AcquireCallbackLock(lambda: self.write(newer))
|
|
69
|
+
self.error = invalid_grant()
|
|
70
|
+
with mock.patch.object(claude_oauth_refresh, "ClientLock", return_value=storage_lock):
|
|
71
|
+
self.assertIsNone(self.refresh())
|
|
72
|
+
self.park.assert_not_called()
|
|
73
|
+
self.assertFalse((self.account / ".oauth-refresh.json").exists())
|
|
74
|
+
self.assertEqual(self.read(), newer)
|
|
75
|
+
self.assertTrue(storage_lock.released)
|
|
76
|
+
self.assert_locks_released()
|
|
77
|
+
|
|
78
|
+
def test_transient_failure_after_changed_login_leaves_state_unchanged(self):
|
|
79
|
+
newer = copy.deepcopy(self.original)
|
|
80
|
+
newer["claudeAiOauth"]["refreshToken"] = "fixture-new-login-refresh"
|
|
81
|
+
self.on_post = lambda: self.write(newer)
|
|
82
|
+
self.error = RuntimeError(PRIVATE_FIXTURE)
|
|
83
|
+
self.assertIsNone(self.refresh())
|
|
84
|
+
self.park.assert_not_called()
|
|
85
|
+
self.assertFalse((self.account / ".oauth-refresh.json").exists())
|
|
86
|
+
self.assertEqual(self.read(), newer)
|
|
87
|
+
self.assertNotIn(PRIVATE_FIXTURE, "\n".join(self.messages))
|
|
88
|
+
self.assert_locks_released()
|
|
89
|
+
|
|
90
|
+
def test_transient_failure_never_logs_provider_secret(self):
|
|
91
|
+
self.error = RuntimeError(PRIVATE_FIXTURE)
|
|
92
|
+
self.assertIsNone(self.refresh())
|
|
93
|
+
self.park.assert_not_called()
|
|
94
|
+
backoff = (self.account / ".oauth-refresh.json").read_text(encoding="utf-8")
|
|
95
|
+
self.assertEqual(json.loads(backoff)["error"], "RuntimeError")
|
|
96
|
+
self.assertNotIn(PRIVATE_FIXTURE, "\n".join(self.messages) + backoff)
|
|
97
|
+
self.assert_locks_released()
|
|
98
|
+
|
|
99
|
+
def test_malformed_success_after_changed_login_does_not_back_off(self):
|
|
100
|
+
newer = copy.deepcopy(self.original)
|
|
101
|
+
newer["claudeAiOauth"]["refreshToken"] = "fixture-new-login-refresh"
|
|
102
|
+
self.on_post = lambda: self.write(newer)
|
|
103
|
+
self.reply = {}
|
|
104
|
+
self.assertIsNone(self.refresh())
|
|
105
|
+
self.park.assert_not_called()
|
|
106
|
+
self.assertFalse((self.account / ".oauth-refresh.json").exists())
|
|
107
|
+
self.assertEqual(self.read(), newer)
|
|
108
|
+
self.assert_locks_released()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
unittest.main()
|