1212import getpass
1313import json as _json
1414import os
15+ import re
1516import sys
1617import time
1718from typing import Any
@@ -76,10 +77,19 @@ def _resolve_tenant(client: CloudClient, want: str | None) -> dict[str, Any]:
7677 return tenants [0 ]
7778
7879
80+ _UUID_RE = re .compile (
81+ r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" , re .I
82+ )
83+
84+
7985def _resolve_tracer (client : CloudClient , ref : str ) -> dict [str , Any ]:
80- # Try by id, then by slug.
81- rows = client .db ("tracer_summary" , select = "*" , filters = {"id" : f"eq.{ ref } " })
82- if not rows :
86+ # A UUID ref is queried by id; anything else is treated as a slug. The id
87+ # column is uuid-typed, so sending a non-uuid as an id filter makes
88+ # PostgREST 400 ("invalid input syntax for type uuid") before any slug
89+ # fallback could run -- so pick the right column up front.
90+ key = "id" if _UUID_RE .match (ref or "" ) else "slug"
91+ rows = client .db ("tracer_summary" , select = "*" , filters = {key : f"eq.{ ref } " })
92+ if not rows and key == "id" :
8393 rows = client .db ("tracer_summary" , select = "*" , filters = {"slug" : f"eq.{ ref } " })
8494 if not rows :
8595 raise CloudError (f"tracer not found: { ref } " )
@@ -514,6 +524,9 @@ def _cmd_keys(client: CloudClient, args) -> None:
514524 _table (res .get ("keys" , []), ["id" , "name" , "prefix" , "last4" , "last_used_at" , "revoked_at" ])
515525 elif args .action == "create" :
516526 res = client .api ("POST" , base , json_body = {"name" : args .name })
527+ if args .json :
528+ _print_json (res )
529+ return
517530 print (_c ("✓ key (shown once):" , _GREEN ), res .get ("full_key" ))
518531 elif args .action == "revoke" :
519532 client .api ("DELETE" , f"{ base } /{ args .id } " )
@@ -532,6 +545,9 @@ def _cmd_ingest_keys(client: CloudClient, args) -> None:
532545 elif args .action == "create" :
533546 tenant = _resolve_tenant (client , args .tenant )
534547 res = client .api ("POST" , "/api/observe/keys" , json_body = {"tenant_id" : tenant ["id" ], "name" : args .name })
548+ if args .json :
549+ _print_json (res )
550+ return
535551 print (_c ("✓ ingest key (shown once):" , _GREEN ), res .get ("full_key" ))
536552 elif args .action == "revoke" :
537553 client .api ("DELETE" , f"/api/observe/keys/{ args .id } " )
@@ -574,7 +590,9 @@ def _cmd_billing(client: CloudClient, args) -> None:
574590
575591
576592def _cmd_scan (client : CloudClient , args ) -> None :
577- res = client .api_multipart ("/api/scan" , {}, args .file )
593+ # /api/scan is public (matches the anonymous web /scan flow), so the scan
594+ # works logged out; the bearer is attached only if a session exists.
595+ res = client .api_multipart ("/api/scan" , {}, args .file , auth = "optional" )
578596 # The agent may need to disambiguate the file format. Re-submit with answers
579597 # (CLI flags override the server's best-guess defaults) so the scan is
580598 # non-interactive instead of silently no-opping on an ambiguous file.
@@ -587,7 +605,9 @@ def _cmd_scan(client: CloudClient, args) -> None:
587605 if args .task :
588606 answers ["task" ] = args .task
589607 print (_c ("clarifying format" , _DIM ), _json .dumps (answers ))
590- res = client .api_multipart ("/api/scan" , {"answers" : _json .dumps (answers )}, args .file )
608+ res = client .api_multipart (
609+ "/api/scan" , {"answers" : _json .dumps (answers )}, args .file , auth = "optional"
610+ )
591611 job_id = res .get ("job_id" ) if isinstance (res , dict ) else None
592612 if not job_id :
593613 _print_json (res )
@@ -596,7 +616,7 @@ def _cmd_scan(client: CloudClient, args) -> None:
596616 print (_c ("link" , _DIM ), f"{ client .base_url } /scan/{ job_id } " )
597617 start = time .time ()
598618 while time .time () - start < 300 :
599- st = client .api ("GET" , f"/api/scan/{ job_id } " )
619+ st = client .api ("GET" , f"/api/scan/{ job_id } " , auth = "optional" )
600620 if st .get ("status" ) == "ready" :
601621 print (_c ("✓" , _GREEN ), "scan ready" )
602622 if getattr (args , "json" , False ):
@@ -964,8 +984,9 @@ def run(args) -> None:
964984 print ("usage: tracer cloud <subcommand> (try `tracer cloud --help`)" )
965985 sys .exit (1 )
966986 client = CloudClient .load ()
967- # login/logout don't require an existing session.
968- if cmd not in ("login" , "logout" ) and not client .logged_in :
987+ # login/logout don't require an existing session; `scan` is a public
988+ # endpoint (mirrors the anonymous web /scan) so it runs logged out too.
989+ if cmd not in ("login" , "logout" , "scan" ) and not client .logged_in :
969990 print (_c ("not logged in." , _RED ), "run `tracer cloud login` first." )
970991 sys .exit (1 )
971992 handler = _DISPATCH .get (cmd )
0 commit comments