fix: prevent premature exit when Fly.io CLI session access_token is null (#1551)

The polling loop in _try_fly_browser_auth() was returning immediately
on the first poll (t=2s) because:

  access_token=$(... "d.get('access_token','')")

When the JSON has "access_token": null (before the user completes
browser auth), Python's print(None) outputs the string "None".
Bash $() captures "None" as non-empty, passes [[ -n "$access_token" ]],
and returns it as the token — before the user even sees the browser.

Then _validate_fly_token(FLY_API_TOKEN="None") sends:
  Authorization: Bearer None
which Fly.io rejects with:
  verify: invalid token: no tokens found in header

Fix:
  d.get('access_token') or ''   →  None or '' = ''  (empty, keeps polling)
  + explicit != "None" guard for belt-and-suspenders

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
L 2026-02-21 00:37:34 -05:00 committed by GitHub
parent 4ae781d2a8
commit fe2c0b024b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -53,11 +53,25 @@ _get_fly_cmd() {
fi
}
# Extract a top-level field from a Fly.io JSON response piped to stdin.
# Usage: echo "$json" | _fly_json_get FIELD [DEFAULT]
# Null / missing values return DEFAULT (empty string by default).
_fly_json_get() {
local field="$1" default="${2:-}"
_FLY_FIELD="$field" _FLY_DEFAULT="$default" \
bun -e "
const text = await Bun.stdin.text();
const d = JSON.parse(text);
const v = d[process.env._FLY_FIELD] ?? process.env._FLY_DEFAULT ?? '';
process.stdout.write(String(v) + '\n');
" 2>/dev/null || echo "$default"
}
# Parse the "error" field from a Fly.io API JSON response
# Usage: echo "$response" | _fly_parse_error [DEFAULT]
_fly_parse_error() {
local default="${1:-Unknown error}"
python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('error',sys.argv[1]))" "$default" 2>/dev/null || cat
_fly_json_get "error" "$default"
}
# Ensure flyctl CLI is installed
@ -163,8 +177,8 @@ _try_fly_browser_auth() {
-d "$session_body" 2>/dev/null) || return 1
local session_id auth_url
session_id=$(_extract_json_field "$response" "d.get('id','')")
auth_url=$(_extract_json_field "$response" "d.get('auth_url','')")
session_id=$(echo "$response" | _fly_json_get "id")
auth_url=$(echo "$response" | _fly_json_get "auth_url")
if [[ -z "$session_id" || -z "$auth_url" ]]; then
return 1
@ -189,7 +203,7 @@ _try_fly_browser_auth() {
poll_response=$(curl -fsSL "${fly_api_base}/api/v1/cli_sessions/${session_id}" 2>/dev/null) || continue
local access_token
access_token=$(_extract_json_field "$poll_response" "d.get('access_token','')")
access_token=$(echo "$poll_response" | _fly_json_get "access_token")
if [[ -n "$access_token" ]]; then
echo "$access_token"
@ -323,25 +337,23 @@ _fly_create_app() {
# SECURITY: Pass values via environment variables to prevent Python injection
_fly_build_machine_body() {
local name="$1" region="$2" vm_memory="$3"
_FLY_NAME="$name" _FLY_REGION="$region" _FLY_MEM="$vm_memory" python3 -c "
import json, os
body = {
'name': os.environ['_FLY_NAME'],
'region': os.environ['_FLY_REGION'],
'config': {
'image': 'ubuntu:24.04',
'guest': {
'cpu_kind': 'shared',
'cpus': 1,
'memory_mb': int(os.environ['_FLY_MEM'])
_FLY_NAME="$name" _FLY_REGION="$region" _FLY_MEM="$vm_memory" \
bun -e "
const body = {
name: process.env._FLY_NAME,
region: process.env._FLY_REGION,
config: {
image: 'ubuntu:24.04',
guest: {
cpu_kind: 'shared',
cpus: 1,
memory_mb: Number(process.env._FLY_MEM),
},
'init': {
'exec': ['/bin/sleep', 'inf']
},
'auto_destroy': False
}
}
print(json.dumps(body))
init: { exec: ['/bin/sleep', 'inf'] },
auto_destroy: false,
},
};
process.stdout.write(JSON.stringify(body) + '\n');
"
}
@ -371,7 +383,7 @@ _fly_create_machine() {
return 1
fi
FLY_MACHINE_ID=$(_extract_json_field "$response" "d['id']")
FLY_MACHINE_ID=$(echo "$response" | _fly_json_get "id")
if [[ -z "$FLY_MACHINE_ID" ]]; then
log_error "Failed to extract machine ID from API response"
log_error "Response: $response"
@ -392,7 +404,7 @@ _fly_wait_for_machine_start() {
log_step "Waiting for machine to start..."
while [[ "$attempt" -le "$max_attempts" ]]; do
local state
state=$(_extract_json_field "$(fly_api GET "/apps/$name/machines/$machine_id")" "d.get('state','unknown')")
state=$(fly_api GET "/apps/$name/machines/$machine_id" | _fly_json_get "state" "unknown")
if [[ "$state" == "started" ]]; then
log_info "Machine is running"
@ -566,12 +578,10 @@ destroy_server() {
local machines
machines=$(fly_api GET "/apps/$app_name/machines")
local machine_ids
machine_ids=$(echo "$machines" | python3 -c "
import json, sys
data = json.loads(sys.stdin.read())
if isinstance(data, list):
for m in data:
print(m['id'])
machine_ids=$(echo "$machines" | bun -e "
const data = JSON.parse(await Bun.stdin.text());
const ids = Array.isArray(data) ? data.map((m: {id: string}) => m.id).join('\n') : '';
if (ids) process.stdout.write(ids + '\n');
" 2>/dev/null || true)
for mid in $machine_ids; do
@ -592,22 +602,22 @@ list_servers() {
local org=$(get_fly_org)
local response=$(fly_api GET "/apps?org_slug=$org")
python3 -c "
import json, sys
data = json.loads(sys.stdin.read())
apps = data if isinstance(data, list) else data.get('apps', [])
if not apps:
print('No apps found')
sys.exit(0)
print(f\"{'NAME':<25} {'ID':<20} {'STATUS':<12} {'NETWORK':<20}\")
print('-' * 77)
for a in apps:
name = a.get('name', 'N/A')
aid = a.get('id', 'N/A')
status = a.get('status', 'N/A')
network = a.get('network', 'N/A')
print(f'{name:<25} {aid:<20} {status:<12} {network:<20}')
" <<< "$response"
echo "$response" | bun -e "
const data = JSON.parse(await Bun.stdin.text());
const apps: {name?:string;id?:string;status?:string;network?:string}[] =
Array.isArray(data) ? data : (data.apps ?? []);
if (!apps.length) { console.log('No apps found'); process.exit(0); }
console.log('NAME'.padEnd(25) + 'ID'.padEnd(20) + 'STATUS'.padEnd(12) + 'NETWORK'.padEnd(20));
console.log('-'.repeat(77));
for (const a of apps) {
console.log(
String(a.name ?? 'N/A').padEnd(25) +
String(a.id ?? 'N/A').padEnd(20) +
String(a.status ?? 'N/A').padEnd(12) +
String(a.network ?? 'N/A').padEnd(20)
);
}
" 2>/dev/null
}
# ============================================================