thoughtleaders-cli 0.7.4__tar.gz → 0.7.5__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.
Files changed (133) hide show
  1. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/.claude-plugin/plugin.json +1 -1
  2. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/PKG-INFO +1 -1
  3. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/pyproject.toml +1 -1
  4. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/__init__.py +1 -1
  5. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/auth/commands.py +23 -5
  6. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/auth/login.py +23 -0
  7. thoughtleaders_cli-0.7.5/tests/test_auth.py +145 -0
  8. thoughtleaders_cli-0.7.4/src/tl_cli/auth/finalize.py +0 -88
  9. thoughtleaders_cli-0.7.4/tests/test_auth.py +0 -78
  10. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/.claude-plugin/marketplace.json +0 -0
  11. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/.github/workflows/python-publish.yml +0 -0
  12. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/.gitignore +0 -0
  13. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/AGENTS.md +0 -0
  14. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/API.md +0 -0
  15. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/CLAUDE.md +0 -0
  16. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/LICENSE +0 -0
  17. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/README.md +0 -0
  18. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/agents/tl-analyst.md +0 -0
  19. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/agents/youtube-comment-classifier.md +0 -0
  20. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/hooks/hooks.json +0 -0
  21. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/hooks/scripts/load-tl-skill.mjs +0 -0
  22. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/hooks/scripts/post-usage.sh +0 -0
  23. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/hooks/scripts/pre-check.sh +0 -0
  24. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl/SKILL.md +0 -0
  25. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl/references/business-glossary.md +0 -0
  26. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl/references/elasticsearch-schema.md +0 -0
  27. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl/references/firebolt-schema.md +0 -0
  28. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl/references/postgres-schema.md +0 -0
  29. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-channel-authenticity/.gitignore +0 -0
  30. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-channel-authenticity/SKILL.md +0 -0
  31. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-channel-authenticity/references/comment-patterns.md +0 -0
  32. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-channel-authenticity/references/peer-cohort.md +0 -0
  33. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-channel-authenticity/references/red-flags.md +0 -0
  34. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-channel-authenticity/references/scoring.md +0 -0
  35. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-channel-authenticity/scripts/_io_utf8.py +0 -0
  36. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-channel-authenticity/scripts/analyze_channel.py +0 -0
  37. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-channel-authenticity/scripts/anomaly_detector.py +0 -0
  38. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-channel-authenticity/scripts/comment_analyzer.py +0 -0
  39. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-channel-authenticity/scripts/comment_scraper.py +0 -0
  40. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-channel-authenticity/scripts/engagement_ratios.py +0 -0
  41. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-channel-authenticity/scripts/peer_cohort.py +0 -0
  42. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-channel-authenticity/scripts/report.py +0 -0
  43. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-channel-authenticity/scripts/resolve_channel.py +0 -0
  44. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-channel-authenticity/scripts/score.py +0 -0
  45. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-channel-authenticity/scripts/tl_cli.py +0 -0
  46. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-channel-authenticity/scripts/video_integrity.py +0 -0
  47. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-channel-authenticity/scripts/view_curves.py +0 -0
  48. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-import/SKILL.md +0 -0
  49. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-keyword-research/SKILL.md +0 -0
  50. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-keyword-research/scripts/probe.py +0 -0
  51. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-report-builder/SKILL.md +0 -0
  52. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-report-builder/examples/e2e_findings.md +0 -0
  53. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-report-builder/examples/golden_queries.md +0 -0
  54. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-report-builder/references/columns_brands.md +0 -0
  55. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-report-builder/references/columns_channels.md +0 -0
  56. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-report-builder/references/columns_content.md +0 -0
  57. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-report-builder/references/columns_sponsorships.md +0 -0
  58. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-report-builder/references/intelligence_filterset_schema.json +0 -0
  59. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-report-builder/references/intelligence_widget_schema.json +0 -0
  60. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-report-builder/references/report_glossary.md +0 -0
  61. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-report-builder/references/sortable_columns.json +0 -0
  62. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-report-builder/references/sponsorship_filterset_schema.json +0 -0
  63. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-report-builder/references/sponsorship_widget_schema.json +0 -0
  64. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-report-builder/references/widgets.md +0 -0
  65. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-report-builder/tools/column_builder.md +0 -0
  66. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-report-builder/tools/database_query.md +0 -0
  67. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-report-builder/tools/name_resolver.md +0 -0
  68. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-report-builder/tools/sample_judge.md +0 -0
  69. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-report-builder/tools/similar_channels.md +0 -0
  70. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-report-builder/tools/topic_matcher.md +0 -0
  71. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-report-builder/tools/widget_builder.md +0 -0
  72. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-save-report/SKILL.md +0 -0
  73. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-save-report/references/columns_brands.md +0 -0
  74. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-save-report/references/columns_channels.md +0 -0
  75. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-save-report/references/columns_content.md +0 -0
  76. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-save-report/references/columns_sponsorships.md +0 -0
  77. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-save-report/references/intelligence_filterset_schema.json +0 -0
  78. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-save-report/references/intelligence_widget_schema.json +0 -0
  79. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-save-report/references/report_glossary.md +0 -0
  80. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-save-report/references/sortable_columns.json +0 -0
  81. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-save-report/references/sponsorship_filterset_schema.json +0 -0
  82. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-save-report/references/sponsorship_widget_schema.json +0 -0
  83. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-save-report/references/widgets.md +0 -0
  84. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-top-partnerships/SKILL.md +0 -0
  85. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-top-partnerships/scripts/top_partnerships.py +0 -0
  86. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-views-guarantee/SKILL.md +0 -0
  87. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/skills/tl-views-guarantee/scripts/vg.py +0 -0
  88. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/_completions.py +0 -0
  89. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/_typer_utils.py +0 -0
  90. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/auth/__init__.py +0 -0
  91. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/auth/pkce.py +0 -0
  92. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/auth/token_store.py +0 -0
  93. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/client/__init__.py +0 -0
  94. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/client/errors.py +0 -0
  95. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/client/http.py +0 -0
  96. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/commands/__init__.py +0 -0
  97. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/commands/_comments_common.py +0 -0
  98. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/commands/balance.py +0 -0
  99. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/commands/brands.py +0 -0
  100. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/commands/bulk_import.py +0 -0
  101. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/commands/changelog.py +0 -0
  102. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/commands/channels.py +0 -0
  103. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/commands/credits.py +0 -0
  104. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/commands/db.py +0 -0
  105. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/commands/deals.py +0 -0
  106. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/commands/describe.py +0 -0
  107. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/commands/doctor.py +0 -0
  108. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/commands/matches.py +0 -0
  109. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/commands/proposals.py +0 -0
  110. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/commands/recommender.py +0 -0
  111. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/commands/reports.py +0 -0
  112. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/commands/schema.py +0 -0
  113. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/commands/setup.py +0 -0
  114. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/commands/snapshots.py +0 -0
  115. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/commands/sponsorships.py +0 -0
  116. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/commands/uploads.py +0 -0
  117. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/commands/whoami.py +0 -0
  118. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/config.py +0 -0
  119. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/filters.py +0 -0
  120. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/hints.py +0 -0
  121. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/main.py +0 -0
  122. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/output/__init__.py +0 -0
  123. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/output/formatter.py +0 -0
  124. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/src/tl_cli/self_update.py +0 -0
  125. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/tests/__init__.py +0 -0
  126. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/tests/test_describe.py +0 -0
  127. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/tests/test_filters.py +0 -0
  128. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/tests/test_http_auth.py +0 -0
  129. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/tests/test_output.py +0 -0
  130. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/tests/test_reports.py +0 -0
  131. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/tests/test_setup.py +0 -0
  132. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/tests/test_sponsorships.py +0 -0
  133. {thoughtleaders_cli-0.7.4 → thoughtleaders_cli-0.7.5}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tl-cli",
3
- "version": "0.7.4",
3
+ "version": "0.7.5",
4
4
  "description": "ThoughtLeaders CLI — query sponsorship deals, channels, brands, uploads, and intelligence from the terminal",
5
5
  "author": {
6
6
  "name": "ThoughtLeaders",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: thoughtleaders-cli
3
- Version: 0.7.4
3
+ Version: 0.7.5
4
4
  Summary: ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence
5
5
  Project-URL: Homepage, https://thoughtleaders.io
6
6
  Project-URL: Repository, https://github.com/ThoughtLeaders-io/thoughtleaders-cli
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "thoughtleaders-cli"
7
- version = "0.7.4"
7
+ version = "0.7.5"
8
8
  description = "ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,3 +1,3 @@
1
1
  """ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence."""
2
2
 
3
- __version__ = "0.7.4"
3
+ __version__ = "0.7.5"
@@ -8,9 +8,9 @@ from tl_cli._typer_utils import AlphaSortedTyperGroup
8
8
  from rich.console import Console
9
9
  from rich.prompt import Prompt
10
10
 
11
- from tl_cli.auth.finalize import finalize_signup
12
- from tl_cli.auth.login import login_browser, login_device_code
11
+ from tl_cli.auth.login import login_browser, login_device_code, revoke_refresh_token
13
12
  from tl_cli.auth.token_store import KIND_API_KEY, StoredTokens, clear_tokens, load_tokens, save_tokens
13
+ from tl_cli.config import get_config
14
14
 
15
15
  app = typer.Typer(cls=AlphaSortedTyperGroup, help="Authentication commands")
16
16
  console = Console(stderr=True)
@@ -106,8 +106,6 @@ def login_cmd() -> None:
106
106
  else:
107
107
  login_browser()
108
108
 
109
- finalize_signup()
110
-
111
109
 
112
110
  def _login_api_key() -> None:
113
111
  """Store a user-supplied API key as the active credential.
@@ -171,7 +169,27 @@ def _login_api_key() -> None:
171
169
 
172
170
  @app.command("logout")
173
171
  def logout_cmd() -> None:
174
- """Clear stored authentication tokens."""
172
+ """Log out: revoke the refresh token at Auth0, then clear stored tokens."""
173
+ tokens = load_tokens()
174
+ # Revoke the long-lived credential server-side so a leaked/synced copy of
175
+ # the local token store can't keep minting access tokens. Best-effort —
176
+ # API-key auth has no refresh token, and an offline revoke must not block
177
+ # clearing local credentials.
178
+ if tokens and not tokens.is_api_key and tokens.refresh_token:
179
+ if revoke_refresh_token(tokens.refresh_token):
180
+ console.print("[dim]Refresh token revoked at Auth0.[/dim]")
181
+ else:
182
+ console.print(
183
+ "[yellow]Could not reach Auth0 to revoke the refresh token; "
184
+ "clearing local credentials anyway.[/yellow]"
185
+ )
186
+ # Revoking the refresh token doesn't end the browser SSO session that
187
+ # the interactive login established. Point the user at Auth0's logout
188
+ # URL so the next `tl auth login` doesn't silently SSO straight back in.
189
+ logout_url = f"https://{get_config().auth0_domain}/logout"
190
+ console.print(
191
+ f"To end your Auth0 browser session, visit: [cyan]{logout_url}[/cyan]"
192
+ )
175
193
  clear_tokens()
176
194
  console.print("[green]Logged out successfully.[/green]")
177
195
 
@@ -212,6 +212,29 @@ def refresh_access_token(refresh_token: str) -> StoredTokens:
212
212
  return tokens
213
213
 
214
214
 
215
+ def revoke_refresh_token(refresh_token: str) -> bool:
216
+ """Best-effort revocation of a refresh token at Auth0 (RFC 7009).
217
+
218
+ Invalidates the long-lived credential server-side so it can no longer mint
219
+ new access tokens. Public-client call — `client_id` only, no secret. Returns
220
+ True on success; never raises — network / Auth0 errors are swallowed so
221
+ `tl auth logout` can still clear the local credentials when offline.
222
+ """
223
+ config = get_config()
224
+ try:
225
+ response = httpx.post(
226
+ f"https://{config.auth0_domain}/oauth/revoke",
227
+ json={
228
+ "client_id": config.auth0_client_id,
229
+ "token": refresh_token,
230
+ },
231
+ timeout=10,
232
+ )
233
+ except httpx.HTTPError:
234
+ return False
235
+ return response.status_code == 200
236
+
237
+
215
238
  def _exchange_code(
216
239
  code: str,
217
240
  code_verifier: str,
@@ -0,0 +1,145 @@
1
+ """Tests for PKCE and token storage."""
2
+
3
+ import httpx
4
+ from typer.testing import CliRunner
5
+
6
+ from tl_cli.auth import commands as auth_commands
7
+ from tl_cli.auth import login as auth_login
8
+ from tl_cli.auth.commands import app as auth_app
9
+ from tl_cli.auth.login import revoke_refresh_token
10
+ from tl_cli.auth.pkce import generate_pkce_pair
11
+ from tl_cli.auth.token_store import KIND_API_KEY, KIND_BEARER, StoredTokens
12
+
13
+ runner = CliRunner()
14
+
15
+
16
+ class TestPKCE:
17
+ def test_generates_pair(self):
18
+ verifier, challenge = generate_pkce_pair()
19
+ assert len(verifier) > 40
20
+ assert len(challenge) > 20
21
+ assert verifier != challenge
22
+
23
+ def test_different_each_time(self):
24
+ v1, c1 = generate_pkce_pair()
25
+ v2, c2 = generate_pkce_pair()
26
+ assert v1 != v2
27
+ assert c1 != c2
28
+
29
+
30
+ class TestStoredTokens:
31
+ def test_roundtrip_json(self):
32
+ tokens = StoredTokens(
33
+ access_token="abc",
34
+ refresh_token="def",
35
+ expires_at=9999999999.0,
36
+ email="test@example.com",
37
+ )
38
+ json_str = tokens.to_json()
39
+ restored = StoredTokens.from_json(json_str)
40
+ assert restored.access_token == "abc"
41
+ assert restored.refresh_token == "def"
42
+ assert restored.email == "test@example.com"
43
+
44
+ def test_is_expired(self):
45
+ tokens = StoredTokens(
46
+ access_token="abc", refresh_token=None, expires_at=0.0
47
+ )
48
+ assert tokens.is_expired
49
+
50
+ def test_not_expired(self):
51
+ tokens = StoredTokens(
52
+ access_token="abc", refresh_token=None, expires_at=9999999999.0
53
+ )
54
+ assert not tokens.is_expired
55
+
56
+
57
+ class TestStoredTokensKind:
58
+ def test_default_kind_is_bearer(self):
59
+ tokens = StoredTokens(access_token="x", refresh_token=None, expires_at=9e9)
60
+ assert tokens.kind == KIND_BEARER
61
+ assert not tokens.is_api_key
62
+
63
+ def test_api_key_never_expires(self):
64
+ tokens = StoredTokens(
65
+ access_token="k", refresh_token=None, expires_at=0.0, kind=KIND_API_KEY,
66
+ )
67
+ assert tokens.is_api_key
68
+ # 0.0 would mark a bearer token as expired; API keys ignore expiry.
69
+ assert not tokens.is_expired
70
+
71
+ def test_kind_roundtrips_through_json(self):
72
+ tokens = StoredTokens(
73
+ access_token="k", refresh_token=None, expires_at=0.0,
74
+ email="user@example.com", kind=KIND_API_KEY,
75
+ )
76
+ restored = StoredTokens.from_json(tokens.to_json())
77
+ assert restored.kind == KIND_API_KEY
78
+ assert restored.is_api_key
79
+ assert restored.email == "user@example.com"
80
+
81
+ def test_legacy_payload_without_kind_defaults_to_bearer(self):
82
+ # Pre-API-key clients wrote payloads with no `kind` field. Loading
83
+ # those must still produce a working bearer token.
84
+ legacy = '{"access_token": "x", "refresh_token": "y", "expires_at": 1.0, "email": "e"}'
85
+ restored = StoredTokens.from_json(legacy)
86
+ assert restored.kind == KIND_BEARER
87
+ assert not restored.is_api_key
88
+
89
+
90
+ class _FakeResponse:
91
+ def __init__(self, status_code: int) -> None:
92
+ self.status_code = status_code
93
+
94
+
95
+ class TestRevokeRefreshToken:
96
+ def test_returns_true_on_200(self, monkeypatch) -> None:
97
+ monkeypatch.setattr(auth_login.httpx, "post", lambda *a, **k: _FakeResponse(200))
98
+ assert revoke_refresh_token("rt") is True
99
+
100
+ def test_returns_false_on_non_200(self, monkeypatch) -> None:
101
+ monkeypatch.setattr(auth_login.httpx, "post", lambda *a, **k: _FakeResponse(400))
102
+ assert revoke_refresh_token("rt") is False
103
+
104
+ def test_swallows_network_error(self, monkeypatch) -> None:
105
+ def boom(*a, **k):
106
+ raise httpx.ConnectError("offline")
107
+ monkeypatch.setattr(auth_login.httpx, "post", boom)
108
+ # Must not raise — logout has to proceed offline.
109
+ assert revoke_refresh_token("rt") is False
110
+
111
+
112
+ class TestLogoutCommand:
113
+ def _patch(self, monkeypatch, tokens):
114
+ calls = {"revoked": None, "cleared": False}
115
+ monkeypatch.setattr(auth_commands, "load_tokens", lambda: tokens)
116
+ monkeypatch.setattr(auth_commands, "clear_tokens", lambda: calls.__setitem__("cleared", True))
117
+ monkeypatch.setattr(auth_commands, "revoke_refresh_token", lambda rt: calls.__setitem__("revoked", rt) or True)
118
+ return calls
119
+
120
+ def test_bearer_logout_revokes_then_clears(self, monkeypatch) -> None:
121
+ tokens = StoredTokens(access_token="a", refresh_token="rt", expires_at=None, email="e@x.com")
122
+ calls = self._patch(monkeypatch, tokens)
123
+ result = runner.invoke(auth_app, ["logout"])
124
+ assert result.exit_code == 0
125
+ assert calls["revoked"] == "rt" # revoked with the stored refresh token
126
+ assert calls["cleared"] is True # local tokens still cleared
127
+ # Points the user at Auth0's session-logout URL built from auth0_domain.
128
+ assert "/logout" in result.output
129
+ assert auth_commands.get_config().auth0_domain in result.output
130
+
131
+ def test_api_key_logout_skips_revoke(self, monkeypatch) -> None:
132
+ tokens = StoredTokens(access_token="k", refresh_token=None, expires_at=None, email=None, kind=KIND_API_KEY)
133
+ calls = self._patch(monkeypatch, tokens)
134
+ result = runner.invoke(auth_app, ["logout"])
135
+ assert result.exit_code == 0
136
+ assert calls["revoked"] is None # no refresh token → no Auth0 call
137
+ assert calls["cleared"] is True
138
+ assert "/logout" not in result.output # no browser session for API-key auth
139
+
140
+ def test_logged_out_already_just_clears(self, monkeypatch) -> None:
141
+ calls = self._patch(monkeypatch, None)
142
+ result = runner.invoke(auth_app, ["logout"])
143
+ assert result.exit_code == 0
144
+ assert calls["revoked"] is None
145
+ assert calls["cleared"] is True
@@ -1,88 +0,0 @@
1
- """Server-side signup finalize, called once per login.
2
-
3
- After Auth0 returns a valid token the CLI asks the server whether an
4
- account exists for the email. If yes, this is a no-op. If no, the CLI
5
- prompts for a persona (Media Buyer or Creator) and POSTs it back; the
6
- server creates the User, Organization, Profile and CreditAccount.
7
-
8
- Errors here never abort login — the user can always retry the prompt
9
- on the next call. We do print a clear message so it's obvious whether
10
- the account is fully set up.
11
- """
12
-
13
- from __future__ import annotations
14
-
15
- import typer
16
- from rich.console import Console
17
- from rich.prompt import Prompt
18
-
19
- from tl_cli.client.errors import ApiError
20
- from tl_cli.client.http import get_client
21
-
22
- console = Console(stderr=True)
23
-
24
- PERSONA_LABEL_TO_KEY = {
25
- "Media Buyer": "media_buyer",
26
- "Creator": "creator",
27
- }
28
-
29
-
30
- def finalize_signup() -> None:
31
- """POST /auth/finalize, prompting for persona if the server asks for one."""
32
- client = get_client()
33
- try:
34
- # First call: no body. Server tells us whether persona is required.
35
- try:
36
- result = client.post("/auth/finalize", {})
37
- except ApiError as exc:
38
- if exc.status_code == 400 and isinstance(exc.raw, dict) and exc.raw.get("code") == "persona_required":
39
- result = _prompt_and_finalize(client, exc.raw.get("allowed_personas") or [])
40
- elif exc.status_code == 404:
41
- # Server predates this endpoint — silently skip; legacy
42
- # accounts already exist and don't need provisioning.
43
- return
44
- else:
45
- console.print(f"[yellow]Could not finalize signup: {exc.detail}[/yellow]")
46
- return
47
-
48
- if result.get("created"):
49
- org = result.get("organization", {})
50
- console.print(
51
- f"[green]Account created for {org.get('name', 'your organization')}.[/green] "
52
- "Run [bold]tl balance[/bold] to see your starter credits."
53
- )
54
- finally:
55
- client.close()
56
-
57
-
58
- def _prompt_and_finalize(client, allowed: list[str]) -> dict:
59
- """Prompt the user for a persona, then retry /auth/finalize."""
60
- console.print()
61
- console.print("[bold]Welcome to ThoughtLeaders![/bold] We need one more detail to set up your account.")
62
- console.print(" [cyan]1[/cyan] — Media Buyer (brands and agencies buying sponsorships)")
63
- console.print(" [cyan]2[/cyan] — Creator (channels selling sponsorships)")
64
-
65
- persona_key: str | None = None
66
- while persona_key is None:
67
- choice = Prompt.ask("I am a", choices=["1", "2"], default="1", console=console)
68
- candidate = "media_buyer" if choice == "1" else "creator"
69
- if allowed and candidate not in allowed:
70
- console.print(f"[yellow]Server rejects persona '{candidate}'. Allowed: {', '.join(allowed)}.[/yellow]")
71
- continue
72
- persona_key = candidate
73
-
74
- org_name = Prompt.ask(
75
- "Organization name (optional, leave blank to use your email)",
76
- default="",
77
- console=console,
78
- ).strip()
79
-
80
- body = {"persona": persona_key}
81
- if org_name:
82
- body["organization_name"] = org_name
83
-
84
- try:
85
- return client.post("/auth/finalize", body)
86
- except ApiError as exc:
87
- console.print(f"[red]Signup failed:[/red] {exc.detail}")
88
- raise typer.Exit(1)
@@ -1,78 +0,0 @@
1
- """Tests for PKCE and token storage."""
2
-
3
- from tl_cli.auth.pkce import generate_pkce_pair
4
- from tl_cli.auth.token_store import KIND_API_KEY, KIND_BEARER, StoredTokens
5
-
6
-
7
- class TestPKCE:
8
- def test_generates_pair(self):
9
- verifier, challenge = generate_pkce_pair()
10
- assert len(verifier) > 40
11
- assert len(challenge) > 20
12
- assert verifier != challenge
13
-
14
- def test_different_each_time(self):
15
- v1, c1 = generate_pkce_pair()
16
- v2, c2 = generate_pkce_pair()
17
- assert v1 != v2
18
- assert c1 != c2
19
-
20
-
21
- class TestStoredTokens:
22
- def test_roundtrip_json(self):
23
- tokens = StoredTokens(
24
- access_token="abc",
25
- refresh_token="def",
26
- expires_at=9999999999.0,
27
- email="test@example.com",
28
- )
29
- json_str = tokens.to_json()
30
- restored = StoredTokens.from_json(json_str)
31
- assert restored.access_token == "abc"
32
- assert restored.refresh_token == "def"
33
- assert restored.email == "test@example.com"
34
-
35
- def test_is_expired(self):
36
- tokens = StoredTokens(
37
- access_token="abc", refresh_token=None, expires_at=0.0
38
- )
39
- assert tokens.is_expired
40
-
41
- def test_not_expired(self):
42
- tokens = StoredTokens(
43
- access_token="abc", refresh_token=None, expires_at=9999999999.0
44
- )
45
- assert not tokens.is_expired
46
-
47
-
48
- class TestStoredTokensKind:
49
- def test_default_kind_is_bearer(self):
50
- tokens = StoredTokens(access_token="x", refresh_token=None, expires_at=9e9)
51
- assert tokens.kind == KIND_BEARER
52
- assert not tokens.is_api_key
53
-
54
- def test_api_key_never_expires(self):
55
- tokens = StoredTokens(
56
- access_token="k", refresh_token=None, expires_at=0.0, kind=KIND_API_KEY,
57
- )
58
- assert tokens.is_api_key
59
- # 0.0 would mark a bearer token as expired; API keys ignore expiry.
60
- assert not tokens.is_expired
61
-
62
- def test_kind_roundtrips_through_json(self):
63
- tokens = StoredTokens(
64
- access_token="k", refresh_token=None, expires_at=0.0,
65
- email="user@example.com", kind=KIND_API_KEY,
66
- )
67
- restored = StoredTokens.from_json(tokens.to_json())
68
- assert restored.kind == KIND_API_KEY
69
- assert restored.is_api_key
70
- assert restored.email == "user@example.com"
71
-
72
- def test_legacy_payload_without_kind_defaults_to_bearer(self):
73
- # Pre-API-key clients wrote payloads with no `kind` field. Loading
74
- # those must still produce a working bearer token.
75
- legacy = '{"access_token": "x", "refresh_token": "y", "expires_at": 1.0, "email": "e"}'
76
- restored = StoredTokens.from_json(legacy)
77
- assert restored.kind == KIND_BEARER
78
- assert not restored.is_api_key