claude-multiacc 1.0.11 → 1.0.13
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/codex-accounts +110 -2
- package/package.json +1 -1
- package/tests/run-tests.sh +119 -0
package/bin/codex-accounts
CHANGED
|
@@ -146,6 +146,36 @@ os.replace(path + '.tmp', path)
|
|
|
146
146
|
PYEOF
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
# ---- per-account login lock ----------------------------------------------------
|
|
150
|
+
# Two concurrent logins for the SAME account corrupt each other: the later one
|
|
151
|
+
# snapshots the OLD credential and its abort/refusal "restores" that stale
|
|
152
|
+
# credential over the fresh one the first sign-in produced. A ceremony can take
|
|
153
|
+
# minutes, so the lock is only reclaimed after 30 minutes (holder presumed dead).
|
|
154
|
+
# Stale reclaim is ATOMIC between contenders: rename steals the stale lock
|
|
155
|
+
# (exactly one mv succeeds); the loser finds a live lock and refuses. Release is
|
|
156
|
+
# OWNERSHIP-GUARDED (owner.<pid> marker), so a process can never remove a lock a
|
|
157
|
+
# later contender legitimately re-took.
|
|
158
|
+
login_lock_acquire() { # $1 = lock dir; nonzero when someone else holds it
|
|
159
|
+
local L="$1"
|
|
160
|
+
if ! mkdir "$L" 2>/dev/null; then
|
|
161
|
+
[ $(( $(epoch_now) - $(file_mtime "$L") )) -gt 1800 ] || return 1
|
|
162
|
+
mv "$L" "$L.stale.$$" 2>/dev/null || return 1
|
|
163
|
+
rm -rf "$L.stale.$$" 2>/dev/null
|
|
164
|
+
mkdir "$L" 2>/dev/null || return 1
|
|
165
|
+
fi
|
|
166
|
+
: > "$L/owner.$$" 2>/dev/null || true
|
|
167
|
+
return 0
|
|
168
|
+
}
|
|
169
|
+
login_lock_release() { # $1 = lock dir; removes it ONLY if this process owns it
|
|
170
|
+
local L="$1"
|
|
171
|
+
[ -e "$L/owner.$$" ] && rm -rf "$L" 2>/dev/null
|
|
172
|
+
return 0
|
|
173
|
+
}
|
|
174
|
+
login_lock_busy() { # $1 = acct dir; true when a LIVE login ceremony holds it
|
|
175
|
+
[ -d "$1/.login-lock" ] \
|
|
176
|
+
&& [ $(( $(epoch_now) - $(file_mtime "$1/.login-lock") )) -le 1800 ]
|
|
177
|
+
}
|
|
178
|
+
|
|
149
179
|
auto_sync() { # best effort after mutations, Mac only, loud on failure
|
|
150
180
|
[ "$(machine_kind)" = "mac" ] || return 0
|
|
151
181
|
[ "${CODEX_MULTIACC_NO_SYNC:-0}" = "1" ] && return 0
|
|
@@ -421,8 +451,25 @@ cmd_add() {
|
|
|
421
451
|
mutate_lock || die "could not acquire the account lock — try again"
|
|
422
452
|
owner="$(email_owner "$got")"
|
|
423
453
|
if [ -n "$owner" ]; then
|
|
424
|
-
|
|
425
|
-
|
|
454
|
+
# The sign-in itself succeeded and produced a fresh credential for an account
|
|
455
|
+
# that is already registered — don't throw it away: save it to that account,
|
|
456
|
+
# exactly as `login $owner` would have. This machine now runs $owner.
|
|
457
|
+
# Needs the owner's login lock (non-blocking): writing under a mid-ceremony
|
|
458
|
+
# login for that account would corrupt it.
|
|
459
|
+
local od="$ACC_ROOT/$owner"
|
|
460
|
+
seed_account_dir "$od"
|
|
461
|
+
if login_lock_acquire "$od/.login-lock" && mv -f "$d/auth.json" "$od/auth.json" 2>/dev/null; then
|
|
462
|
+
chmod 600 "$od/auth.json" 2>/dev/null || true
|
|
463
|
+
clear_auth_markers "$od"
|
|
464
|
+
login_lock_release "$od/.login-lock"
|
|
465
|
+
mutate_unlock
|
|
466
|
+
log_to ops.log "add redirect: signed in as $got (already $owner) — credential saved to $owner"
|
|
467
|
+
echo "$got is already added as $owner — the fresh sign-in was saved to $owner (usable on this machine now)."
|
|
468
|
+
else
|
|
469
|
+
login_lock_release "$od/.login-lock"
|
|
470
|
+
mutate_unlock
|
|
471
|
+
echo "$got is already added as $owner — skipping (nothing added)."
|
|
472
|
+
fi
|
|
426
473
|
return 0 # RESERVED_DIR still set -> trap removes the temp dir
|
|
427
474
|
fi
|
|
428
475
|
if [ -n "$email" ] && [ "$got" != "$email" ]; then
|
|
@@ -619,6 +666,10 @@ cmd_remove() {
|
|
|
619
666
|
read -r ans
|
|
620
667
|
case "$ans" in y|Y|yes) ;; *) echo "aborted"; return 1 ;; esac
|
|
621
668
|
fi
|
|
669
|
+
# Never delete an account out from under a live sign-in ceremony: the login's
|
|
670
|
+
# cleanup would then operate on a removed (or later recreated) path.
|
|
671
|
+
login_lock_busy "$ACC_ROOT/$id" \
|
|
672
|
+
&& die "a login for $id is in progress — finish or abort it first"
|
|
622
673
|
if [ -L "$ACC_ROOT/$id" ]; then
|
|
623
674
|
rm -f "${ACC_ROOT:?}/${id:?}" # adopted account: remove the symlink, never the target
|
|
624
675
|
else
|
|
@@ -658,6 +709,18 @@ for a in json.load(open(sys.argv[1])).get('accounts', []):
|
|
|
658
709
|
break
|
|
659
710
|
PYEOF
|
|
660
711
|
)"
|
|
712
|
+
# PER-ACCOUNT login lock (observed in the field: parallel relogin runs left a
|
|
713
|
+
# months-dead token inside a minutes-old auth.json — see login_lock_acquire).
|
|
714
|
+
local llock="$d/.login-lock"
|
|
715
|
+
login_lock_acquire "$llock" \
|
|
716
|
+
|| die "another login for $id is already in progress (this terminal or another) — finish or abort it first"
|
|
717
|
+
# Released on EVERY exit: normal returns, die (exit fires EXIT), Ctrl-C (the INT
|
|
718
|
+
# handler exits, which fires EXIT). Runs in a subshell under relogin, so the trap
|
|
719
|
+
# scope is exactly this one login. Ownership-guarded: never removes a lock a
|
|
720
|
+
# later contender re-took.
|
|
721
|
+
# shellcheck disable=SC2064
|
|
722
|
+
trap "login_lock_release '$llock'" EXIT
|
|
723
|
+
|
|
661
724
|
echo "Sign in as $email for $id."
|
|
662
725
|
# Snapshot the credential BEFORE the ceremony: `codex login` writes auth.json into
|
|
663
726
|
# the dir directly, so a sign-in to the WRONG account would otherwise leave that
|
|
@@ -692,6 +755,36 @@ PYEOF
|
|
|
692
755
|
got_lc="$(printf '%s' "$got" | tr '[:upper:]' '[:lower:]')"
|
|
693
756
|
email_lc="$(printf '%s' "$email" | tr '[:upper:]' '[:lower:]')"
|
|
694
757
|
if [ -n "$got" ] && [ "$got_lc" != "$email_lc" ] && [ "$force" != "1" ]; then
|
|
758
|
+
# Signed in as a DIFFERENT account. If that account is registered in this pool,
|
|
759
|
+
# the sign-in was not a mistake worth discarding — route the fresh credential to
|
|
760
|
+
# ITS account dir (what `login <owner>` would have produced), restore this
|
|
761
|
+
# account's prior state, and report honestly that THIS account is still unfixed.
|
|
762
|
+
local owner
|
|
763
|
+
owner="$(email_owner "$got")"
|
|
764
|
+
if [ -n "$owner" ] && [ "$owner" != "$id" ]; then
|
|
765
|
+
# The redirect mutates the OWNER's credential, so it needs the owner's login
|
|
766
|
+
# lock too (non-blocking: if a login for that account is mid-ceremony, writing
|
|
767
|
+
# under it would recreate exactly the corruption this lock prevents — skip the
|
|
768
|
+
# redirect and fall through to the plain refusal instead).
|
|
769
|
+
local od="$ACC_ROOT/$owner"
|
|
770
|
+
seed_account_dir "$od"
|
|
771
|
+
if login_lock_acquire "$od/.login-lock"; then
|
|
772
|
+
if mv -f "$d/auth.json" "$od/auth.json" 2>/dev/null; then
|
|
773
|
+
chmod 600 "$od/auth.json" 2>/dev/null || true
|
|
774
|
+
clear_auth_markers "$od"
|
|
775
|
+
login_lock_release "$od/.login-lock"
|
|
776
|
+
restore_snap
|
|
777
|
+
trap - INT TERM
|
|
778
|
+
log_to ops.log "login redirect: signed in as $got -> credential saved to $owner (target was $id)"
|
|
779
|
+
echo "you signed in as $got — that is $owner, so the credential was saved to $owner (usable now)."
|
|
780
|
+
echo "$id ($email) still needs its own sign-in: codex-accounts login $id"
|
|
781
|
+
return 1
|
|
782
|
+
fi
|
|
783
|
+
login_lock_release "$od/.login-lock"
|
|
784
|
+
else
|
|
785
|
+
echo "note: a login for $owner is in progress — not saving this credential there."
|
|
786
|
+
fi
|
|
787
|
+
fi
|
|
695
788
|
restore_snap
|
|
696
789
|
trap - INT TERM
|
|
697
790
|
die "you signed in as $got but $id is $email — nothing saved, previous credential restored (use --force to override)"
|
|
@@ -819,12 +912,27 @@ cmd_relogin() {
|
|
|
819
912
|
read -r ans
|
|
820
913
|
case "$ans" in y|Y|yes) ;; *) echo "aborted"; return 1 ;; esac
|
|
821
914
|
fi
|
|
915
|
+
# Accounts that were BROKEN when this run started: if one of them turns healthy
|
|
916
|
+
# mid-run (a redirected sign-in from an earlier iteration landed its credential),
|
|
917
|
+
# asking the user to sign it in again would be pure friction — it gets skipped.
|
|
918
|
+
# Accounts that were healthy at start (explicit ids / --all) are never skipped:
|
|
919
|
+
# the user asked to re-authenticate them on purpose.
|
|
920
|
+
local pre_bad
|
|
921
|
+
pre_bad=" $(accounts_needing_login 2>/dev/null | tr '\n' ' ') "
|
|
822
922
|
# One sync at the end instead of one per account: each auto_sync is an ssh round trip.
|
|
823
923
|
local prev_no_sync="${CODEX_MULTIACC_NO_SYNC:-0}" failed="" done_ok=0
|
|
824
924
|
export CODEX_MULTIACC_NO_SYNC=1
|
|
825
925
|
for id in $ids; do
|
|
826
926
|
echo
|
|
827
927
|
echo "=== $id ==============================================================="
|
|
928
|
+
case "$pre_bad" in
|
|
929
|
+
*" $id "*)
|
|
930
|
+
if [ "$(account_audit 2>/dev/null | awk -F'\t' -v i="$id" '$1 == i { print $4 }')" = "ok" ]; then
|
|
931
|
+
echo "$id already has a working login (a credential landed during an earlier sign-in) — skipping."
|
|
932
|
+
done_ok=$((done_ok + 1))
|
|
933
|
+
continue
|
|
934
|
+
fi ;;
|
|
935
|
+
esac
|
|
828
936
|
if [ "$browser" = "1" ]; then
|
|
829
937
|
( cmd_login "$id" --browser ) && done_ok=$((done_ok + 1)) || failed="$failed $id"
|
|
830
938
|
else
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-multiacc",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.13",
|
|
4
4
|
"description": "Multi-account addon for Claude Code and OpenAI Codex CLI: every claude / claude -p and every codex / codex exec runs under a randomly-picked subscription account with the most usage headroom. Mirrors to a deploy server. No API keys.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/tests/run-tests.sh
CHANGED
|
@@ -1757,6 +1757,62 @@ EOF
|
|
|
1757
1757
|
)"
|
|
1758
1758
|
[ "$got_email" = "d@cx" ] && t_ok "codex: refused login restores the prior credential" \
|
|
1759
1759
|
|| t_fail "codex login restore" "auth.json now belongs to: $got_email"
|
|
1760
|
+
# signing in as a DIFFERENT-but-REGISTERED account routes the credential to that
|
|
1761
|
+
# account instead of discarding it (and the target account stays honestly unfixed)
|
|
1762
|
+
rm -f "$CX/acct-01/auth.json"
|
|
1763
|
+
out="$(FAKE_EMAIL=a@cx codex-accounts login acct-04 2>&1)"
|
|
1764
|
+
rc=$?
|
|
1765
|
+
check "codex: cross-account sign-in is redirected, not discarded" "credential was saved to acct-01" "$out"
|
|
1766
|
+
check "codex: redirect says the target still needs its sign-in" "acct-04 (d@cx) still needs its own sign-in" "$out"
|
|
1767
|
+
[ "$rc" != "0" ] && t_ok "codex: redirected login still exits nonzero for the target" \
|
|
1768
|
+
|| t_fail "codex redirect rc" "rc=0 for an account that was not fixed"
|
|
1769
|
+
[ -s "$CX/acct-01/auth.json" ] && t_ok "codex: redirected credential landed at its owner" \
|
|
1770
|
+
|| t_fail "codex redirect landing" "acct-01/auth.json missing"
|
|
1771
|
+
got_email="$(python3 - "$CX/acct-04/auth.json" <<'EOF'
|
|
1772
|
+
import base64, json, sys
|
|
1773
|
+
t = json.load(open(sys.argv[1]))['tokens']['id_token']
|
|
1774
|
+
p = t.split('.')[1]; p += '=' * (-len(p) % 4)
|
|
1775
|
+
print(json.loads(base64.urlsafe_b64decode(p)).get('email', ''))
|
|
1776
|
+
EOF
|
|
1777
|
+
)"
|
|
1778
|
+
[ "$got_email" = "d@cx" ] && t_ok "codex: redirect restored the target's prior credential" \
|
|
1779
|
+
|| t_fail "codex redirect restore" "acct-04 auth.json belongs to: $got_email"
|
|
1780
|
+
# `add` while signed into an already-registered account also keeps the credential
|
|
1781
|
+
rm -f "$CX/acct-01/auth.json"
|
|
1782
|
+
before_dirs="$(ls "$CX" | sort)"
|
|
1783
|
+
out="$(FAKE_EMAIL=a@cx codex-accounts add 2>&1)"
|
|
1784
|
+
rc=$?
|
|
1785
|
+
check "codex: add of a registered account saves the fresh sign-in to it" "fresh sign-in was saved to acct-01" "$out"
|
|
1786
|
+
[ "$rc" = "0" ] && t_ok "codex: add redirect exits 0 (nothing new to register)" || t_fail "codex add redirect rc" "rc=$rc"
|
|
1787
|
+
[ -s "$CX/acct-01/auth.json" ] && t_ok "codex: add redirect landed the credential" \
|
|
1788
|
+
|| t_fail "codex add redirect" "acct-01/auth.json missing"
|
|
1789
|
+
[ "$before_dirs" = "$(ls "$CX" | sort)" ] && t_ok "codex: add redirect leaves no reserved dir behind" \
|
|
1790
|
+
|| t_fail "codex add redirect cleanup" "pool dirs changed"
|
|
1791
|
+
n="$(grep -c '"email": "a@cx"' "$CX/accounts.json")"
|
|
1792
|
+
[ "$n" = "1" ] && t_ok "codex: add redirect creates no duplicate entry" || t_fail "codex add redirect dup" "count=$n"
|
|
1793
|
+
# relogin end-to-end: a redirect mid-run fixes the OTHER account, whose own turn is
|
|
1794
|
+
# then skipped instead of demanding a second sign-in. Both accounts' home must be
|
|
1795
|
+
# THIS machine's kind, or a credless account reads as 'remote' (grant lives
|
|
1796
|
+
# elsewhere) on the other platform and never enters the worklist.
|
|
1797
|
+
cx_mk=mac; [ "$(uname -s)" = "Darwin" ] || cx_mk=linux
|
|
1798
|
+
python3 - "$CX/accounts.json" "$cx_mk" <<'EOF'
|
|
1799
|
+
import json, os, sys
|
|
1800
|
+
d = json.load(open(sys.argv[1]))
|
|
1801
|
+
for a in d['accounts']:
|
|
1802
|
+
if a['id'] in ('acct-01', 'acct-04'):
|
|
1803
|
+
a['home'] = sys.argv[2]
|
|
1804
|
+
json.dump(d, open(sys.argv[1] + '.tmp', 'w'), indent=2); os.replace(sys.argv[1] + '.tmp', sys.argv[1])
|
|
1805
|
+
EOF
|
|
1806
|
+
rm -f "$CX/acct-01/auth.json" "$CX/acct-04/auth.json"
|
|
1807
|
+
out="$(FAKE_EMAIL=d@cx codex-accounts relogin --yes 2>&1)"
|
|
1808
|
+
rc=$?
|
|
1809
|
+
check "codex: relogin redirect saves the mis-ordered sign-in" "credential was saved to acct-04" "$out"
|
|
1810
|
+
check "codex: relogin skips an account fixed mid-run" "already has a working login" "$out"
|
|
1811
|
+
check "codex: relogin counts the redirect-fixed account" "re-authenticated 1 of 2" "$out"
|
|
1812
|
+
check "codex: relogin still reports the unfixed account" "still failing: acct-01" "$out"
|
|
1813
|
+
[ "$rc" != "0" ] && t_ok "codex: relogin with an unfixed account exits nonzero" || t_fail "codex relogin redirect rc" "rc=0"
|
|
1814
|
+
mk_cx_auth "$CX/acct-01/auth.json" a@cx "$FUTURE_EXP"
|
|
1815
|
+
mk_cx_auth "$CX/acct-04/auth.json" d@cx "$FUTURE_EXP"
|
|
1760
1816
|
# the sign-in ceremony is DEVICE-CODE by default (the localhost browser callback
|
|
1761
1817
|
# cannot reach a remote/SSH machine); --browser opts into the callback flow
|
|
1762
1818
|
export FAKE_LOGIN_ARGS="$WORK/cx-login-args"
|
|
@@ -1772,6 +1828,69 @@ rc=$?
|
|
|
1772
1828
|
{ [ "$rc" = "0" ] && [ "$(cat "$FAKE_LOGIN_ARGS")" = "" ]; } \
|
|
1773
1829
|
&& t_ok "codex: --browser opts into the localhost callback flow" \
|
|
1774
1830
|
|| t_fail "codex --browser opt-out" "rc=$rc login args: '$(cat "$FAKE_LOGIN_ARGS")'"
|
|
1831
|
+
# lock released after a completed login
|
|
1832
|
+
[ ! -d "$CX/acct-04/.login-lock" ] && t_ok "codex: login lock released after completion" \
|
|
1833
|
+
|| { t_fail "codex login lock release" ".login-lock survived a completed login"; rmdir "$CX/acct-04/.login-lock" 2>/dev/null; }
|
|
1834
|
+
# a SECOND concurrent login for the same account is refused before any ceremony —
|
|
1835
|
+
# a parallel run snapshotting the old credential could otherwise "restore" it over
|
|
1836
|
+
# the fresh one (field incident: months-dead token inside a minutes-old auth.json)
|
|
1837
|
+
mkdir "$CX/acct-04/.login-lock"
|
|
1838
|
+
export FAKE_LOGIN_ARGS="$WORK/cx-login-args"
|
|
1839
|
+
: > "$FAKE_LOGIN_ARGS"
|
|
1840
|
+
cp "$CX/acct-04/auth.json" "$WORK/cx-lock-auth.bak"
|
|
1841
|
+
out="$(FAKE_EMAIL=d@cx codex-accounts login acct-04 2>&1)"
|
|
1842
|
+
rc=$?
|
|
1843
|
+
check "codex: concurrent login for the same account is refused" "already in progress" "$out"
|
|
1844
|
+
[ "$rc" != "0" ] && t_ok "codex: concurrent login exits nonzero" || t_fail "codex login lock rc" "rc=0"
|
|
1845
|
+
[ ! -s "$FAKE_LOGIN_ARGS" ] && t_ok "codex: refused concurrent login never ran a ceremony" \
|
|
1846
|
+
|| t_fail "codex login lock ceremony" "codex login was invoked despite the lock"
|
|
1847
|
+
cmp -s "$CX/acct-04/auth.json" "$WORK/cx-lock-auth.bak" \
|
|
1848
|
+
&& t_ok "codex: refused concurrent login left the credential untouched" \
|
|
1849
|
+
|| t_fail "codex login lock credential" "auth.json changed"
|
|
1850
|
+
# a STALE lock (holder died >30min ago) is reclaimed, not fatal
|
|
1851
|
+
python3 - "$CX/acct-04/.login-lock" <<'EOF'
|
|
1852
|
+
import os, sys, time
|
|
1853
|
+
t = time.time() - 2000
|
|
1854
|
+
os.utime(sys.argv[1], (t, t))
|
|
1855
|
+
EOF
|
|
1856
|
+
out="$(FAKE_EMAIL=d@cx codex-accounts login acct-04 2>&1)"
|
|
1857
|
+
rc=$?
|
|
1858
|
+
[ "$rc" = "0" ] && t_ok "codex: stale login lock is reclaimed" || t_fail "codex stale login lock" "rc=$rc: $out"
|
|
1859
|
+
[ ! -d "$CX/acct-04/.login-lock" ] && t_ok "codex: reclaimed lock released again" \
|
|
1860
|
+
|| { t_fail "codex stale lock release" "lock left behind"; rmdir "$CX/acct-04/.login-lock" 2>/dev/null; }
|
|
1861
|
+
# lock released after a FAILED ceremony too
|
|
1862
|
+
FAKE_LOGIN_FAIL=1 FAKE_EMAIL=d@cx codex-accounts login acct-04 >/dev/null 2>&1
|
|
1863
|
+
[ ! -d "$CX/acct-04/.login-lock" ] && t_ok "codex: login lock released after a failed ceremony" \
|
|
1864
|
+
|| { t_fail "codex login lock on failure" "lock left behind"; rmdir "$CX/acct-04/.login-lock" 2>/dev/null; }
|
|
1865
|
+
# remove refuses to delete an account under a live ceremony
|
|
1866
|
+
mkdir "$CX/acct-04/.login-lock"
|
|
1867
|
+
out="$(codex-accounts remove acct-04 --yes 2>&1)"
|
|
1868
|
+
rc=$?
|
|
1869
|
+
check "codex: remove refuses during a live login" "login for acct-04 is in progress" "$out"
|
|
1870
|
+
[ "$rc" != "0" ] && [ -d "$CX/acct-04" ] && t_ok "codex: account survives a remove during login" \
|
|
1871
|
+
|| t_fail "codex remove guard" "rc=$rc dir_exists=$([ -d "$CX/acct-04" ] && echo yes || echo no)"
|
|
1872
|
+
rmdir "$CX/acct-04/.login-lock"
|
|
1873
|
+
# a redirect never writes under a mid-ceremony login on the OWNER account either:
|
|
1874
|
+
# it skips the save and falls back to the plain refusal
|
|
1875
|
+
mkdir "$CX/acct-01/.login-lock"
|
|
1876
|
+
mv "$CX/acct-01/auth.json" "$WORK/cx-a01.hold"
|
|
1877
|
+
out="$(FAKE_EMAIL=a@cx codex-accounts login acct-04 2>&1)"
|
|
1878
|
+
rc=$?
|
|
1879
|
+
check "codex: redirect skipped while the owner has a login in progress" "login for acct-01 is in progress" "$out"
|
|
1880
|
+
[ "$rc" != "0" ] && t_ok "codex: skipped redirect still refuses" || t_fail "codex busy-owner redirect rc" "rc=0"
|
|
1881
|
+
[ ! -f "$CX/acct-01/auth.json" ] && t_ok "codex: no write under the owner's live ceremony" \
|
|
1882
|
+
|| t_fail "codex busy-owner redirect" "auth.json written under an active login lock"
|
|
1883
|
+
rmdir "$CX/acct-01/.login-lock"
|
|
1884
|
+
mv "$WORK/cx-a01.hold" "$CX/acct-01/auth.json"
|
|
1885
|
+
got_email="$(python3 - "$CX/acct-04/auth.json" <<'EOF'
|
|
1886
|
+
import base64, json, sys
|
|
1887
|
+
t = json.load(open(sys.argv[1]))['tokens']['id_token']
|
|
1888
|
+
p = t.split('.')[1]; p += '=' * (-len(p) % 4)
|
|
1889
|
+
print(json.loads(base64.urlsafe_b64decode(p)).get('email', ''))
|
|
1890
|
+
EOF
|
|
1891
|
+
)"
|
|
1892
|
+
[ "$got_email" = "d@cx" ] && t_ok "codex: busy-owner refusal restored the target credential" \
|
|
1893
|
+
|| t_fail "codex busy-owner restore" "acct-04 auth belongs to: $got_email"
|
|
1775
1894
|
unset FAKE_LOGIN_ARGS
|
|
1776
1895
|
|
|
1777
1896
|
# a parked account shows in expired and relogin fixes exactly that
|