wafer-cli 0.2.8__py3-none-any.whl → 0.2.9__py3-none-any.whl

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.
wafer/auth.py CHANGED
@@ -345,3 +345,88 @@ def browser_login(timeout: int = 120, port: int | None = None) -> tuple[str, str
345
345
 
346
346
  server.server_close()
347
347
  raise TimeoutError(f"No response within {timeout} seconds")
348
+
349
+
350
+ def device_code_login(timeout: int = 600) -> tuple[str, str | None]:
351
+ """Authenticate using state-based flow (no browser/port forwarding needed).
352
+
353
+ This is the SSH-friendly auth flow similar to GitHub CLI:
354
+ 1. Request a state token from the API
355
+ 2. Display the auth URL with state parameter
356
+ 3. User visits URL on any device and signs in normally
357
+ 4. Poll API until user completes authentication
358
+
359
+ Args:
360
+ timeout: Seconds to wait for authentication (default 600 = 10 minutes)
361
+
362
+ Returns:
363
+ Tuple of (access_token, refresh_token). refresh_token may be None.
364
+
365
+ Raises:
366
+ TimeoutError: If user doesn't authenticate within timeout
367
+ RuntimeError: If auth flow failed
368
+ """
369
+ api_url = get_api_url()
370
+
371
+ # Request state and auth URL
372
+ with httpx.Client(timeout=10.0) as client:
373
+ response = client.post(f"{api_url}/v1/auth/cli-auth/start", json={})
374
+ response.raise_for_status()
375
+ data = response.json()
376
+
377
+ state = data["state"]
378
+ auth_url = data["auth_url"]
379
+ expires_in = data["expires_in"]
380
+
381
+ # Display instructions to user
382
+ print("\n" + "=" * 60)
383
+ print(" WAFER CLI - Authentication")
384
+ print("=" * 60)
385
+ print(f"\n Visit: {auth_url}")
386
+ print("\n Sign in with GitHub to complete authentication")
387
+ print("\n" + "=" * 60 + "\n")
388
+
389
+ # Poll for authentication
390
+ start = time.time()
391
+ poll_interval = 5 # Poll every 5 seconds
392
+ last_poll = 0.0
393
+
394
+ print("Waiting for authentication", end="", flush=True)
395
+
396
+ while time.time() - start < min(timeout, expires_in):
397
+ # Show progress dots
398
+ if time.time() - last_poll >= poll_interval:
399
+ print(".", end="", flush=True)
400
+
401
+ # Poll the API
402
+ with httpx.Client(timeout=10.0) as client:
403
+ try:
404
+ response = client.post(f"{api_url}/v1/auth/cli-auth/token", json={"state": state})
405
+
406
+ if response.status_code == 200:
407
+ # Success!
408
+ data = response.json()
409
+ print(f" {CHECK}\n")
410
+ return data["access_token"], data.get("refresh_token")
411
+
412
+ if response.status_code == 428:
413
+ # Still waiting
414
+ last_poll = time.time()
415
+ time.sleep(1)
416
+ continue
417
+
418
+ # Some other error
419
+ print(f" {CROSS}\n")
420
+ raise RuntimeError(f"CLI auth flow failed: {response.status_code} {response.text}")
421
+
422
+ except httpx.RequestError as e:
423
+ # Network error, retry
424
+ print("!", end="", flush=True)
425
+ last_poll = time.time()
426
+ time.sleep(1)
427
+ continue
428
+
429
+ time.sleep(0.5) # Small sleep to avoid busy loop
430
+
431
+ print(f" {CROSS}\n")
432
+ raise TimeoutError(f"Authentication not completed within {expires_in} seconds")