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.
Files changed (118) hide show
  1. package/bin/claude-accounts +11 -197
  2. package/docs/INSTALLATION.md +4 -3
  3. package/docs/RELEASING.md +75 -0
  4. package/lib/claude_oauth_lock.py +172 -0
  5. package/lib/claude_oauth_refresh.py +225 -0
  6. package/package.json +3 -2
  7. package/tests/autoresume_cross_tmux_environment.py +4 -1
  8. package/tests/claude_oauth_fixture.py +100 -0
  9. package/tests/run-tests.sh +3 -1
  10. package/tests/test_claude_oauth_lock.py +138 -0
  11. package/tests/test_claude_oauth_refresh.py +106 -0
  12. package/tests/test_claude_oauth_refresh_failures.py +112 -0
  13. package/tests/test_release_validation.py +826 -0
  14. package/lib/__pycache__/audit.cpython-314.pyc +0 -0
  15. package/lib/__pycache__/autoresume.cpython-314.pyc +0 -0
  16. package/lib/__pycache__/autoresume_cross.cpython-314.pyc +0 -0
  17. package/lib/__pycache__/autoresume_cross_dispatch.cpython-314.pyc +0 -0
  18. package/lib/__pycache__/autoresume_cross_receipt.cpython-314.pyc +0 -0
  19. package/lib/__pycache__/autoresume_handoff.cpython-314.pyc +0 -0
  20. package/lib/__pycache__/autoresume_probe.cpython-314.pyc +0 -0
  21. package/lib/__pycache__/autoresume_receipt.cpython-314.pyc +0 -0
  22. package/lib/__pycache__/claude_client_error.cpython-314.pyc +0 -0
  23. package/lib/__pycache__/claude_reset.cpython-314.pyc +0 -0
  24. package/lib/__pycache__/claude_token_identity.cpython-314.pyc +0 -0
  25. package/lib/__pycache__/codex_autoresume_mode.cpython-314.pyc +0 -0
  26. package/lib/__pycache__/codex_config_edit.cpython-314.pyc +0 -0
  27. package/lib/__pycache__/codex_credit_controls.cpython-314.pyc +0 -0
  28. package/lib/__pycache__/codex_credit_local_scope.cpython-314.pyc +0 -0
  29. package/lib/__pycache__/codex_credit_selection.cpython-314.pyc +0 -0
  30. package/lib/__pycache__/codex_credits.cpython-314.pyc +0 -0
  31. package/lib/__pycache__/codex_limits_lock.cpython-314.pyc +0 -0
  32. package/lib/__pycache__/codex_mcp_oauth.cpython-314.pyc +0 -0
  33. package/lib/__pycache__/codex_python.cpython-314.pyc +0 -0
  34. package/lib/__pycache__/codex_resume_cwd.cpython-314.pyc +0 -0
  35. package/lib/__pycache__/codex_speed.cpython-314.pyc +0 -0
  36. package/lib/__pycache__/codex_trust.cpython-314.pyc +0 -0
  37. package/lib/__pycache__/codex_usage_fetch.cpython-314.pyc +0 -0
  38. package/lib/__pycache__/keychain.cpython-314.pyc +0 -0
  39. package/lib/__pycache__/mcp_mirror.cpython-314.pyc +0 -0
  40. package/lib/__pycache__/mcp_oauth.cpython-314.pyc +0 -0
  41. package/lib/__pycache__/mcp_registry.cpython-314.pyc +0 -0
  42. package/lib/__pycache__/model_catalog.cpython-314.pyc +0 -0
  43. package/lib/__pycache__/model_match.cpython-314.pyc +0 -0
  44. package/lib/__pycache__/report.cpython-314.pyc +0 -0
  45. package/lib/__pycache__/selector_credit_scope.cpython-314.pyc +0 -0
  46. package/lib/__pycache__/selector_credits.cpython-314.pyc +0 -0
  47. package/lib/__pycache__/selector_funding.cpython-314.pyc +0 -0
  48. package/lib/__pycache__/selector_policy.cpython-314.pyc +0 -0
  49. package/lib/__pycache__/selector_primitives.cpython-314.pyc +0 -0
  50. package/lib/__pycache__/session_settings.cpython-314.pyc +0 -0
  51. package/lib/__pycache__/shim_path.cpython-314.pyc +0 -0
  52. package/tests/__pycache__/autoresume_cross_receipt_support.cpython-314.pyc +0 -0
  53. package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-314.pyc +0 -0
  54. package/tests/__pycache__/autoresume_cross_tmux_support.cpython-314.pyc +0 -0
  55. package/tests/__pycache__/autoresume_cross_unit_support.cpython-314.pyc +0 -0
  56. package/tests/__pycache__/autoresume_tmux_support.cpython-314.pyc +0 -0
  57. package/tests/__pycache__/packaged_command_support.cpython-314.pyc +0 -0
  58. package/tests/__pycache__/test_autoresume.cpython-314.pyc +0 -0
  59. package/tests/__pycache__/test_autoresume_catalog.cpython-314.pyc +0 -0
  60. package/tests/__pycache__/test_autoresume_catalog_cli.cpython-314.pyc +0 -0
  61. package/tests/__pycache__/test_autoresume_codex_records.cpython-314.pyc +0 -0
  62. package/tests/__pycache__/test_autoresume_codex_trust.cpython-314.pyc +0 -0
  63. package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-314.pyc +0 -0
  64. package/tests/__pycache__/test_autoresume_cross_guards.cpython-314.pyc +0 -0
  65. package/tests/__pycache__/test_autoresume_cross_holders.cpython-314.pyc +0 -0
  66. package/tests/__pycache__/test_autoresume_cross_origin.cpython-314.pyc +0 -0
  67. package/tests/__pycache__/test_autoresume_cross_policy.cpython-314.pyc +0 -0
  68. package/tests/__pycache__/test_autoresume_cross_receipt.cpython-314.pyc +0 -0
  69. package/tests/__pycache__/test_autoresume_cross_tmux.cpython-314.pyc +0 -0
  70. package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-314.pyc +0 -0
  71. package/tests/__pycache__/test_autoresume_cross_tokens.cpython-314.pyc +0 -0
  72. package/tests/__pycache__/test_autoresume_cursor.cpython-314.pyc +0 -0
  73. package/tests/__pycache__/test_autoresume_cwd.cpython-314.pyc +0 -0
  74. package/tests/__pycache__/test_autoresume_cwd_handoff.cpython-314.pyc +0 -0
  75. package/tests/__pycache__/test_autoresume_cwd_reader.cpython-314.pyc +0 -0
  76. package/tests/__pycache__/test_autoresume_cwd_tmux.cpython-314.pyc +0 -0
  77. package/tests/__pycache__/test_autoresume_fallback.cpython-314.pyc +0 -0
  78. package/tests/__pycache__/test_autoresume_handoff_argv.cpython-314.pyc +0 -0
  79. package/tests/__pycache__/test_autoresume_handoff_history.cpython-314.pyc +0 -0
  80. package/tests/__pycache__/test_autoresume_handoff_origins.cpython-314.pyc +0 -0
  81. package/tests/__pycache__/test_autoresume_memory.cpython-314.pyc +0 -0
  82. package/tests/__pycache__/test_autoresume_memory_tmux.cpython-314.pyc +0 -0
  83. package/tests/__pycache__/test_autoresume_mode.cpython-314.pyc +0 -0
  84. package/tests/__pycache__/test_autoresume_probe.cpython-314.pyc +0 -0
  85. package/tests/__pycache__/test_autoresume_receipt.cpython-314.pyc +0 -0
  86. package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-314.pyc +0 -0
  87. package/tests/__pycache__/test_autoresume_settings.cpython-314.pyc +0 -0
  88. package/tests/__pycache__/test_autoresume_sidechain.cpython-314.pyc +0 -0
  89. package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-314.pyc +0 -0
  90. package/tests/__pycache__/test_autoresume_tmux.cpython-314.pyc +0 -0
  91. package/tests/__pycache__/test_autoresume_tmux_modes.cpython-314.pyc +0 -0
  92. package/tests/__pycache__/test_autoresume_trust_tmux.cpython-314.pyc +0 -0
  93. package/tests/__pycache__/test_claude_reset.cpython-314.pyc +0 -0
  94. package/tests/__pycache__/test_claude_token_identity.cpython-314.pyc +0 -0
  95. package/tests/__pycache__/test_codex_exec_stdin.cpython-314.pyc +0 -0
  96. package/tests/__pycache__/test_codex_mcp_oauth.cpython-314.pyc +0 -0
  97. package/tests/__pycache__/test_codex_reset.cpython-314.pyc +0 -0
  98. package/tests/__pycache__/test_codex_reset_credits.cpython-314.pyc +0 -0
  99. package/tests/__pycache__/test_codex_reset_credits_polling.cpython-314.pyc +0 -0
  100. package/tests/__pycache__/test_codex_reset_credits_report.cpython-314.pyc +0 -0
  101. package/tests/__pycache__/test_codex_reset_fetch.cpython-314.pyc +0 -0
  102. package/tests/__pycache__/test_codex_reset_polling.cpython-314.pyc +0 -0
  103. package/tests/__pycache__/test_codex_reset_reporting.cpython-314.pyc +0 -0
  104. package/tests/__pycache__/test_codex_reset_windows.cpython-314.pyc +0 -0
  105. package/tests/__pycache__/test_mcp_mirror.cpython-314.pyc +0 -0
  106. package/tests/__pycache__/test_mcp_mirror_args.cpython-314.pyc +0 -0
  107. package/tests/__pycache__/test_mcp_oauth.cpython-314.pyc +0 -0
  108. package/tests/__pycache__/test_mcp_registry.cpython-314.pyc +0 -0
  109. package/tests/__pycache__/test_selector.cpython-314.pyc +0 -0
  110. package/tests/__pycache__/test_selector_cached_credits.cpython-314.pyc +0 -0
  111. package/tests/__pycache__/test_selector_credit_scope.cpython-314.pyc +0 -0
  112. package/tests/__pycache__/test_selector_credit_shim.cpython-314.pyc +0 -0
  113. package/tests/__pycache__/test_selector_credits.cpython-314.pyc +0 -0
  114. package/tests/__pycache__/test_selector_spread.cpython-314.pyc +0 -0
  115. package/tests/__pycache__/test_token_identity_ceremony.cpython-314.pyc +0 -0
  116. package/tests/__pycache__/test_token_identity_integration.cpython-314.pyc +0 -0
  117. package/tests/__pycache__/test_token_identity_retry.cpython-314.pyc +0 -0
  118. 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()