claude-multiacc 2.0.9 → 2.0.10

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.
package/bin/claude CHANGED
@@ -559,17 +559,26 @@ client_auth_scan() { # $1 acct dir
559
559
  return 1
560
560
  }
561
561
 
562
- mark_proven_auth_dead() { # $1 acct dir, $2 fixed safe detail
563
- local marked
562
+ mark_proven_auth_dead() { # $1 acct dir, $2 fixed safe detail, $3 optional reason slug
563
+ local marked slug="${3:-auth-error}"
564
564
  marked="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
565
- { echo "$now"; echo "reason=auth-error marked_at=$marked detail=$2"; } \
565
+ { echo "$now"; echo "reason=$slug marked_at=$marked detail=$2"; } \
566
566
  2>/dev/null > "$1/.expired.$$" && mv -f "$1/.expired.$$" "$1/.expired" 2>/dev/null \
567
567
  || rm -f "$1/.expired.$$" 2>/dev/null || true
568
568
  }
569
569
 
570
570
  mark_client_auth_dead() { # $1 acct dir
571
- mark_proven_auth_dead "$1" "client session failed to authenticate"
572
- sel_log "$(basename "$1") parked (auth-error) — client-reported"
571
+ local tok
572
+ tok="$(acct_token "$1")"
573
+ if [ -n "$tok" ]; then
574
+ rm -f "$1/.server-token-verified" 2>/dev/null || true
575
+ mark_proven_auth_dead "$1" "client rejected the portable OAuth token" \
576
+ "setup-token-invalid"
577
+ sel_log "$(basename "$1") parked (setup-token-invalid) — client-reported"
578
+ else
579
+ mark_proven_auth_dead "$1" "client session failed to authenticate"
580
+ sel_log "$(basename "$1") parked (auth-error) — client-reported"
581
+ fi
573
582
  }
574
583
 
575
584
  mark_client_limit() { # $1 acct dir, $2 reset epoch, $3 rate limit type
@@ -750,8 +759,9 @@ token_preflight() { # $1 acct dir, $2 setup-token; rc 1 only for proven invalid
750
759
  if [ "$rc" -ne 0 ] \
751
760
  && grep -qiE "$token_auth_pat" "$tmpd/out" "$tmpd/err" 2>/dev/null; then
752
761
  rm -f "$marker" 2>/dev/null || true
753
- mark_proven_auth_dead "$d" "portable OAuth token rejected by inference preflight"
754
- sel_log "$(basename "$d") parked (auth-error) — setup-token preflight returned 401"
762
+ mark_proven_auth_dead "$d" "portable OAuth token rejected by inference preflight" \
763
+ "setup-token-invalid"
764
+ sel_log "$(basename "$d") parked (setup-token-invalid) — preflight returned 401"
755
765
  rm -rf "$tmpd"
756
766
  return 1
757
767
  fi
@@ -1244,9 +1254,14 @@ while :; do
1244
1254
  park_detail="the account's organization has disabled Claude Code subscription access"
1245
1255
  park_soft=$((now + PARK_SOFT_ORG))
1246
1256
  elif grep -qiE "$PARK_AUTH" "$tmpd/out" "$tmpd/err" 2>/dev/null; then
1247
- park_reason="auth-error"
1248
- park_detail="run failed to authenticate"
1249
- park_soft=$((now + PARK_SOFT_AUTH))
1257
+ if [ -n "$tok" ]; then
1258
+ park_reason="setup-token-invalid"
1259
+ park_detail="run rejected the portable OAuth token"
1260
+ else
1261
+ park_reason="auth-error"
1262
+ park_detail="run failed to authenticate"
1263
+ park_soft=$((now + PARK_SOFT_AUTH))
1264
+ fi
1250
1265
  fi
1251
1266
  # An account that ran out of ONE model has not run out. Cooling it down would
1252
1267
  # take a perfectly usable account out of the pool for every other model too —
@@ -1254,13 +1269,20 @@ while :; do
1254
1269
  model_scoped=0
1255
1270
  grep -qiE "$MODEL_LIMITPAT" "$tmpd/out" "$tmpd/err" 2>/dev/null && model_scoped=1
1256
1271
  if [ -n "$park_reason" ]; then
1257
- {
1258
- echo "$now"
1259
- echo "reason=$park_reason soft_until=$park_soft marked_at=$(date -u +%Y-%m-%dT%H:%M:%SZ) detail=$park_detail"
1260
- } 2>/dev/null > "$cur/.expired.$$" \
1272
+ if [ "$park_reason" = "setup-token-invalid" ]; then
1273
+ rm -f "$cur/.server-token-verified" 2>/dev/null || true
1274
+ park_line="reason=$park_reason marked_at=$(date -u +%Y-%m-%dT%H:%M:%SZ) detail=$park_detail"
1275
+ else
1276
+ park_line="reason=$park_reason soft_until=$park_soft marked_at=$(date -u +%Y-%m-%dT%H:%M:%SZ) detail=$park_detail"
1277
+ fi
1278
+ { echo "$now"; echo "$park_line"; } 2>/dev/null > "$cur/.expired.$$" \
1261
1279
  && mv -f "$cur/.expired.$$" "$cur/.expired" 2>/dev/null \
1262
1280
  || rm -f "$cur/.expired.$$" 2>/dev/null || true
1263
- sel_log "$(basename "$cur") parked ($park_reason until $park_soft) — see: claude-accounts expired"
1281
+ if [ "$park_reason" = "setup-token-invalid" ]; then
1282
+ sel_log "$(basename "$cur") parked ($park_reason, durable) — see: claude-accounts expired"
1283
+ else
1284
+ sel_log "$(basename "$cur") parked ($park_reason until $park_soft) — see: claude-accounts expired"
1285
+ fi
1264
1286
  elif [ "$model_scoped" = 0 ]; then
1265
1287
  {
1266
1288
  echo $((now + 600))
@@ -227,6 +227,8 @@ for a in accounts:
227
227
  flags = []
228
228
  if st['state'] == 'expired':
229
229
  flags.append('EXPIRED-LOGIN')
230
+ elif st['state'] == 'token-invalid':
231
+ flags.append('INVALID-TOKEN')
230
232
  elif st['state'] == 'blocked':
231
233
  flags.append('ORG-BLOCKED')
232
234
  elif st['state'] == 'missing':
@@ -239,7 +241,7 @@ for a in accounts:
239
241
  f"auth={'+'.join(auth) or 'NONE':<11} {' '.join(flags)}")
240
242
  bad = [a for a in accounts
241
243
  if audit_account(root, a, machine=machine)['state']
242
- in ('expired', 'blocked', 'missing')]
244
+ in ('expired', 'token-invalid', 'blocked', 'missing')]
243
245
  if bad:
244
246
  print()
245
247
  print(f"{len(bad)} account(s) are NOT usable — details: claude-accounts expired")
@@ -317,13 +319,15 @@ for a in doc.get('accounts', []):
317
319
  aid = a['id']
318
320
  d = os.path.join(root, aid)
319
321
  st = audit_account(root, a, machine=machine)
320
- if st['state'] in ('expired', 'blocked', 'missing'):
322
+ if st['state'] in ('expired', 'token-invalid', 'blocked', 'missing'):
321
323
  needs_login.append(aid)
322
324
  else:
323
325
  selectable_ids.append(aid)
324
326
  banner = {'ok': '', 'remote': ' [not logged in here — grant lives elsewhere]',
325
327
  'missing': ' ** NO LOGIN — claude-accounts relogin %s **' % aid,
326
328
  'expired': ' ** LOGIN EXPIRED — claude-accounts relogin %s **' % aid,
329
+ 'token-invalid': (' ** SETUP-TOKEN INVALID — '
330
+ 'claude-accounts login %s --token **' % aid),
327
331
  'blocked': ' ** ORG BLOCKED — Claude Code disabled for this account **',
328
332
  'locked': ' [login is in the macOS Keychain — locked for THIS session, fine from the Mac itself]',
329
333
  }[st['state']]
@@ -1040,14 +1044,17 @@ cmd_expired() {
1040
1044
  die "the manifest lists accounts but none could be audited — check $MANIFEST"
1041
1045
  fi
1042
1046
  bad="$(printf '%s\n' "$rows" \
1043
- | awk -F'\t' 'NF >= 4 && $1 != "" && $4 ~ /^(expired|blocked|missing|unverified)$/ { print $1 }')"
1047
+ | awk -F'\t' '
1048
+ NF >= 4 && $1 != "" && $4 ~ /^(expired|token-invalid|blocked|missing|unverified)$/ {
1049
+ print $1
1050
+ }')"
1044
1051
  if [ "$quiet" = "1" ]; then
1045
1052
  [ -n "$bad" ] || return 0
1046
1053
  printf '%s\n' "$bad"
1047
1054
  return 1
1048
1055
  fi
1049
1056
  printf '%s\n' "$rows" | awk -F'\t' '
1050
- BEGIN { bad = 0; ok = 0; remote = 0; relogin = 0; unverified = 0 }
1057
+ BEGIN { bad = 0; ok = 0; remote = 0; relogin = 0; retoken = 0; unverified = 0 }
1051
1058
  NF < 4 || $1 == "" { next } # never invent an account from a blank line
1052
1059
  $4 == "ok" { ok++; next }
1053
1060
  $4 == "remote" { remote++; rem = rem sprintf(" %-9s %-28s %s\n", $1, $2, $6); next }
@@ -1058,6 +1065,13 @@ cmd_expired() {
1058
1065
  printf " %-9s %-28s %-9s fix: %s\n", "", "", "", $7
1059
1066
  next
1060
1067
  }
1068
+ $4 == "token-invalid" {
1069
+ bad++
1070
+ retoken++
1071
+ printf " %-9s %-28s %-13s %s\n", $1, $2, $5, $6
1072
+ printf " %-9s %-28s %-13s fix: %s\n", "", "", "", $7
1073
+ next
1074
+ }
1061
1075
  {
1062
1076
  bad++
1063
1077
  relogin++
@@ -1075,6 +1089,10 @@ cmd_expired() {
1075
1089
  printf "\nVerify portable tokens:\n"
1076
1090
  printf " claude-accounts verify\n"
1077
1091
  }
1092
+ if (retoken > 0) {
1093
+ printf "\nReplace invalid portable setup-tokens:\n"
1094
+ printf " claude-accounts login acct-NN --token\n"
1095
+ }
1078
1096
  if (relogin > 0) {
1079
1097
  printf "\nRe-authenticate them:\n"
1080
1098
  printf " claude-accounts relogin # every account that needs it\n"
@@ -2175,8 +2193,14 @@ for acct in manifest.get('accounts', []):
2175
2193
  os.remove(os.path.join(d, '.server-token-verified'))
2176
2194
  except OSError:
2177
2195
  pass
2178
- mark_expired(d, 'auth-error', 'a real call came back not-authenticated')
2179
- hint = f' — login is dead, run: claude-accounts relogin {aid}'
2196
+ if uses_token:
2197
+ mark_expired(d, 'setup-token-invalid',
2198
+ 'portable setup-token failed a real inference')
2199
+ hint = (f' — portable setup-token is invalid, run: '
2200
+ f'claude-accounts login {aid} --token')
2201
+ else:
2202
+ mark_expired(d, 'auth-error', 'a real call came back not-authenticated')
2203
+ hint = f' — login is dead, run: claude-accounts relogin {aid}'
2180
2204
  print(f'{aid} {acct["email"]}: FAIL rc={r.returncode} '
2181
2205
  f'out={out[:120]!r} err={err!r}{hint}')
2182
2206
  failures += 1
@@ -215,7 +215,8 @@ providers (`lib/report.py`), so a consumer writes one parser:
215
215
  "accounts": [{
216
216
  "id": "acct-05", "email": "…", "home": "mac", "added_at": "…Z",
217
217
  "home_dir": "/Users/gas/.claude-accounts/acct-05", "adopted": false,
218
- "status": "active", // active|limited|expired|blocked|missing|remote
218
+ "status": "active", // active|limited|expired|token-invalid|blocked
219
+ // missing|remote
219
220
  "state": "ok", "label": "OK", "reason": "…", "fix": "…",
220
221
  "selectable": true, "needs_login": false,
221
222
  "credential_class": "portable", // portable|machine-local|none
package/lib/audit.py CHANGED
@@ -5,6 +5,7 @@ shim's selection rule (bin/claude: creds_dead / expired_marked / auth_dead).
5
5
  States:
6
6
  ok usable right now (live OAuth credential, or a portable server.token)
7
7
  expired has auth material that CANNOT authenticate — needs `claude-accounts relogin`
8
+ token-invalid a portable setup-token was rejected — needs a new setup-token, not a login
8
9
  blocked authenticates, but its organization has disabled Claude Code subscription
9
10
  access — a re-login cannot fix it, so it needs an admin (or removal)
10
11
  locked the OAuth login is in the macOS Keychain and THIS session cannot open it
@@ -193,8 +194,9 @@ def oauth_login(d, now):
193
194
  return {'store': None, 'state': None, 'reason': '', 'doc': None}
194
195
 
195
196
 
196
- LABELS = {'ok': 'OK', 'expired': 'EXPIRED', 'blocked': 'BLOCKED', 'locked': 'KEYCHAIN LOCKED',
197
- 'missing': 'NO LOGIN', 'remote': 'ELSEWHERE', 'unverified': 'UNVERIFIED'}
197
+ LABELS = {'ok': 'OK', 'expired': 'EXPIRED', 'token-invalid': 'TOKEN INVALID',
198
+ 'blocked': 'BLOCKED', 'locked': 'KEYCHAIN LOCKED', 'missing': 'NO LOGIN',
199
+ 'remote': 'ELSEWHERE', 'unverified': 'UNVERIFIED'}
198
200
 
199
201
  # `import --home` speaks mac|server; machine_kind() speaks mac|linux. Same two boxes,
200
202
  # two vocabularies — normalize, or an un-authenticated account on the very machine that
@@ -216,6 +218,8 @@ def _elsewhere(home, machine):
216
218
 
217
219
 
218
220
  def _fix_for(state, aid):
221
+ if state == 'token-invalid':
222
+ return f'claude-accounts login {aid} --token'
219
223
  if state == 'blocked':
220
224
  # A fresh sign-in re-issues the grant and in practice clears this, so it is
221
225
  # handled like any other dead login. If it comes back BLOCKED after a re-login,
@@ -266,6 +270,9 @@ def audit_account(root, acct, now=None, machine=None, require_verified_token=Fal
266
270
  row['state'] = 'blocked'
267
271
  row['reason'] = ('this account\'s organization has disabled Claude Code '
268
272
  'subscription access')
273
+ elif slug == 'setup-token-invalid':
274
+ row['state'] = 'token-invalid'
275
+ row['reason'] = 'portable setup-token was rejected by a real inference'
269
276
  else:
270
277
  row['state'] = 'expired'
271
278
  row['reason'] = f'marked dead by the pool ({detail or "authentication failed"})'
package/lib/common.sh CHANGED
@@ -495,7 +495,8 @@ clear_auth_markers() { # $1 = acct dir
495
495
  rm -f "$1/.expired" "$1/.oauth-refresh.json" 2>/dev/null || true
496
496
  }
497
497
 
498
- # TSV rows: id, email, home, state (ok|expired|blocked|missing|remote), label, reason, fix.
498
+ # TSV rows: id, email, home, state
499
+ # (ok|expired|token-invalid|blocked|missing|remote|locked|unverified), label, reason, fix.
499
500
  # Fails LOUD (nonzero, message on stderr): callers decide policy from these rows, and a
500
501
  # silently empty audit reads exactly like a perfectly healthy pool.
501
502
  account_audit() {
@@ -518,7 +519,8 @@ account_audit() {
518
519
  # Ids the shim will NOT select — everything a human has to look at.
519
520
  accounts_unusable() {
520
521
  account_audit \
521
- | awk -F'\t' 'NF >= 4 && $1 != "" && ($4 == "expired" || $4 == "blocked" || $4 == "missing") { print $1 }'
522
+ | awk -F'\t' \
523
+ 'NF >= 4 && $1 != "" && $4 ~ /^(expired|token-invalid|blocked|missing)$/ { print $1 }'
522
524
  }
523
525
 
524
526
  # Ids a re-login should be pointed at. Org-blocked accounts are included: a fresh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-multiacc",
3
- "version": "2.0.9",
3
+ "version": "2.0.10",
4
4
  "description": "Unified Claude Code and OpenAI Codex subscription pooling with quota-aware selection.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -515,7 +515,7 @@ case "$out" in
515
515
  *"API Error: 401"*) t_fail "revoked setup-token hides preflight 401" "401 reached caller" ;;
516
516
  *) t_ok "revoked setup-token hides preflight 401" ;;
517
517
  esac
518
- grep -q 'reason=auth-error' "$ACC/acct-01/.expired" 2>/dev/null \
518
+ grep -q 'reason=setup-token-invalid' "$ACC/acct-01/.expired" 2>/dev/null \
519
519
  && t_ok "revoked setup-token writes .expired" \
520
520
  || t_fail "revoked setup-token marker" ".expired missing"
521
521
  if grep -q 'soft_until=' "$ACC/acct-01/.expired" 2>/dev/null; then
@@ -523,6 +523,37 @@ if grep -q 'soft_until=' "$ACC/acct-01/.expired" 2>/dev/null; then
523
523
  else
524
524
  t_ok "proven token rejection is durable"
525
525
  fi
526
+ out="$(claude-accounts expired 2>&1)"
527
+ check "revoked setup-token is not mislabeled as a dead login" "TOKEN INVALID" "$out"
528
+ check "revoked setup-token gets the token replacement fix" \
529
+ "claude-accounts login acct-01 --token" "$out"
530
+ case "$out" in
531
+ *"claude-accounts relogin acct-01"*)
532
+ t_fail "revoked setup-token does not recommend re-login" "wrong recovery shown" ;;
533
+ *) t_ok "revoked setup-token does not recommend re-login" ;;
534
+ esac
535
+ # A token can be revoked after it passed preflight. The captured -p path must retire
536
+ # that exact verified token too, rather than giving it a one-hour generic login park.
537
+ rm -f "$ACC/acct-01/.expired"
538
+ token_digest="$(printf '%s' 'sk-ant-oat01-REVOKEDREVOKEDREVOKEDREVOKEDREVOKEDREVOKED' \
539
+ | { shasum -a 256 2>/dev/null || sha256sum; } | cut -d ' ' -f1)"
540
+ printf '%s\n' "$token_digest" > "$ACC/acct-01/.server-token-verified"
541
+ out="$(claude -p 'Reply OK' < /dev/null 2>&1)"
542
+ case "$out" in
543
+ *"API Error: 401"*) t_fail "revoked verified token hides runtime 401" "401 reached caller" ;;
544
+ *) t_ok "revoked verified token hides runtime 401" ;;
545
+ esac
546
+ grep -q 'reason=setup-token-invalid' "$ACC/acct-01/.expired" 2>/dev/null \
547
+ && t_ok "revoked verified token keeps token classification" \
548
+ || t_fail "revoked verified token marker" ".expired missing"
549
+ [ ! -f "$ACC/acct-01/.server-token-verified" ] \
550
+ && t_ok "revoked verified token clears its proof" \
551
+ || t_fail "revoked verified token proof" "verification marker survived"
552
+ if grep -q 'soft_until=' "$ACC/acct-01/.expired" 2>/dev/null; then
553
+ t_fail "revoked verified token is durably quarantined" "marker has a soft expiry"
554
+ else
555
+ t_ok "revoked verified token is durably quarantined"
556
+ fi
526
557
  out="$(CLAUDE_ACCOUNT=acct-01 claude --resume d6ccbac0-6643-4780-a99e-3afa1683478e 2>&1)"
527
558
  rc=$?
528
559
  check "pinned revoked setup-token is classified before --resume" \
@@ -2480,7 +2511,19 @@ state="$(python3 "$REPO_DIR/lib/audit.py" "$ACC" "$(uname -s | tr '[:upper:]' '[
2480
2511
  strict-tokens | awk -F'\t' '$1=="acct-01"{print $4}')"
2481
2512
  [ "$state" = "ok" ] && t_ok "verified setup-token is no longer false-unknown" \
2482
2513
  || t_fail "verified setup-token audit" "expected ok, got: $state"
2514
+ printf 'sk-ant-oat01-REVOKEDREVOKEDREVOKEDREVOKEDREVOKEDREVOKED' \
2515
+ > "$ACC/acct-01/server.token"
2516
+ rm -f "$ACC/acct-01/.server-token-verified"
2517
+ out="$(claude-accounts verify 2>&1)"
2518
+ check "verify calls a rejected setup-token what it is" "portable setup-token is invalid" "$out"
2519
+ check "verify recommends replacing a rejected setup-token" \
2520
+ "claude-accounts login acct-01 --token" "$out"
2521
+ out="$(claude-accounts expired 2>&1)"
2522
+ check "verify marker retains setup-token classification" "TOKEN INVALID" "$out"
2523
+ check "verify marker retains setup-token recovery" \
2524
+ "claude-accounts login acct-01 --token" "$out"
2483
2525
  rm -f "$ACC/acct-01/server.token"
2526
+ rm -f "$ACC/acct-01/.expired" "$ACC/acct-01/.server-token-verified"
2484
2527
  printf '{"claudeAiOauth":{"accessToken":"sk-ant-oat01-test01","refreshToken":"r","expiresAt":9999999999999,"refreshTokenExpiresAt":9999999999999}}' > "$ACC/acct-01/.credentials.json"
2485
2528
 
2486
2529
  # ---- 17c. audit: machine vocabulary (home=server vs machine_kind=linux) ---------------