claude-multiacc 2.0.13 → 2.0.15
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-accounts +98 -9
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
- package/package.json +1 -1
- package/tests/run-tests.sh +80 -20
package/bin/claude-accounts
CHANGED
|
@@ -507,7 +507,7 @@ cmd_add() {
|
|
|
507
507
|
if [ "$token" = "1" ]; then
|
|
508
508
|
# Portable setup-token variant (works on Mac AND server; needs a recent sign-in).
|
|
509
509
|
echo "Preparing $id for $elabel (portable token) — NOTHING is registered until sign-in completes."
|
|
510
|
-
run_token_ceremony "$d" || die "sign-in failed or aborted — nothing was created"
|
|
510
|
+
run_token_ceremony "$d" || die "sign-in failed or aborted — nothing was created${CEREMONY_LAST_WORDS:+ — the client said: $CEREMONY_LAST_WORDS}${CEREMONY_TRANSCRIPT:+ (transcript: $CEREMONY_TRANSCRIPT)}"
|
|
511
511
|
commit_ceremony_token "$d" "$CEREMONY_TOKEN" "$id"
|
|
512
512
|
got="$(token_email "$CEREMONY_TOKEN")"
|
|
513
513
|
else
|
|
@@ -934,9 +934,85 @@ commit_ceremony_token() {
|
|
|
934
934
|
fi
|
|
935
935
|
}
|
|
936
936
|
|
|
937
|
+
# When a ceremony ends without a token, the CLIENT said why — "Your account is on
|
|
938
|
+
# hold", a policy refusal, a subscription it could not find — and that sentence is
|
|
939
|
+
# the only thing that lets anyone fix it. It used to vanish with the capture file,
|
|
940
|
+
# leaving "no token captured" (operator, 2026-08-29). The last visible lines are
|
|
941
|
+
# kept (ANSI-stripped, spinners/logo/URL/prompt dropped, any sk-ant-… redacted) and
|
|
942
|
+
# the whole redacted transcript is written beside the pool for a closer look.
|
|
943
|
+
CEREMONY_LAST_WORDS=""
|
|
944
|
+
CEREMONY_TRANSCRIPT=""
|
|
945
|
+
ceremony_debrief() { # $1 = capture file, $2 = redacted transcript to write; prints the last words
|
|
946
|
+
"$PYBIN" - "$1" "$2" <<'PYEOF'
|
|
947
|
+
import os, re, sys
|
|
948
|
+
raw = open(sys.argv[1], 'rb').read().decode('utf-8', 'ignore')
|
|
949
|
+
txt = re.sub(r'\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)', '', raw) # OSC (hyperlink payloads)
|
|
950
|
+
txt = re.sub(r'\x1b\[[0-9;?<>=]*[A-Za-z]', '', txt) # CSI, private modes too ([>4m, [<u)
|
|
951
|
+
txt = re.sub(r'\x1b[()][A-Z0-9]|\x1b[78=>]|[\x0e\x0f]', '', txt) # charset, save/restore cursor, SI/SO
|
|
952
|
+
txt = txt.replace('\r', '\n')
|
|
953
|
+
lines = []
|
|
954
|
+
flat = re.sub(r'\s+', '', txt)
|
|
955
|
+
api_key = bool(re.search(r'sk-ant-api\d{2}-', flat)) and not re.search(r'sk-ant-oat\d{2}-', flat)
|
|
956
|
+
txt = re.sub(r'sk-ant-[A-Za-z0-9]+-[A-Za-z0-9_-]+', 'sk-ant-***', txt)
|
|
957
|
+
for ln in txt.splitlines():
|
|
958
|
+
ln = ln.strip()
|
|
959
|
+
if not ln or re.fullmatch(r'[\W_]+', ln): # spinner frames, logo art
|
|
960
|
+
continue
|
|
961
|
+
if 'https://' in ln or re.search(r'Paste\s*code\s*here', ln): # the sign-in link, the paste prompt
|
|
962
|
+
continue
|
|
963
|
+
if lines and lines[-1] == ln:
|
|
964
|
+
continue
|
|
965
|
+
lines.append(ln)
|
|
966
|
+
if api_key:
|
|
967
|
+
# Said LAST, so it is what the operator reads: the client minted an API key, not a
|
|
968
|
+
# subscription token — the browser session was signed into a Console org.
|
|
969
|
+
lines.append('the client minted an API KEY (sk-ant-api…), not a subscription setup-token: '
|
|
970
|
+
'the browser session belongs to a Console / API-billing organization — sign in '
|
|
971
|
+
'as the subscription account and try again')
|
|
972
|
+
try:
|
|
973
|
+
fd = os.open(sys.argv[2], os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
974
|
+
with os.fdopen(fd, 'w') as f:
|
|
975
|
+
f.write('\n'.join(lines) + '\n')
|
|
976
|
+
except OSError:
|
|
977
|
+
pass
|
|
978
|
+
print(' | '.join(lines[-3:])[:400])
|
|
979
|
+
PYEOF
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
# The token, out of the transcript. The client's renderer places WORDS with cursor
|
|
983
|
+
# moves instead of spaces and can emit a long token as positioned, styled segments;
|
|
984
|
+
# a narrow terminal wraps it. A raw-bytes grep therefore missed a perfectly good token
|
|
985
|
+
# on 2026-08-29 ("no token captured" after "token created successfully"). Every
|
|
986
|
+
# terminal escape is stripped first, and the token block the client prints (between
|
|
987
|
+
# "Your OAuth token …:" and "Store this token securely") is joined across spaces and
|
|
988
|
+
# line breaks before matching, so neither placement nor wrapping can split it. The
|
|
989
|
+
# real inference in commit_ceremony_token then proves whatever came out.
|
|
990
|
+
ceremony_extract() { # $1 = capture file -> the setup-token, or nothing
|
|
991
|
+
"$PYBIN" - "$1" <<'PYEOF'
|
|
992
|
+
import re, sys
|
|
993
|
+
raw = open(sys.argv[1], 'rb').read().decode('utf-8', 'ignore')
|
|
994
|
+
txt = re.sub(r'\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)', '', raw) # OSC (hyperlink payloads)
|
|
995
|
+
txt = re.sub(r'\x1b\[[0-9;?<>=]*[A-Za-z]', '', txt) # CSI, private modes too ([>4m, [<u)
|
|
996
|
+
txt = re.sub(r'\x1b[()][A-Z0-9]|\x1b[78=>]|[\x0e\x0f]', '', txt) # charset, save/restore cursor, SI/SO
|
|
997
|
+
pat = re.compile(r'sk-ant-oat\d{2}-[A-Za-z0-9_-]{40,}')
|
|
998
|
+
cands = []
|
|
999
|
+
for m in re.finditer(r'Your\s*OAuth\s*token[^\n]{0,80}?:', txt):
|
|
1000
|
+
block = txt[m.end(): m.end() + 4000]
|
|
1001
|
+
stop = re.search(r'Store\s*this\s*token', block)
|
|
1002
|
+
if stop:
|
|
1003
|
+
block = block[: stop.start()]
|
|
1004
|
+
cands += pat.findall(re.sub(r'\s+', '', block))
|
|
1005
|
+
cands += pat.findall(txt)
|
|
1006
|
+
cands += pat.findall(re.sub(r'[ \t]+', '', txt))
|
|
1007
|
+
print(max(cands, key=len) if cands else '')
|
|
1008
|
+
PYEOF
|
|
1009
|
+
}
|
|
1010
|
+
|
|
937
1011
|
CEREMONY_TOKEN=""
|
|
938
1012
|
run_token_ceremony() { # $1 = config dir
|
|
939
1013
|
CEREMONY_TOKEN=""
|
|
1014
|
+
CEREMONY_LAST_WORDS=""
|
|
1015
|
+
CEREMONY_TRANSCRIPT=""
|
|
940
1016
|
local d="$1" real cap old_umask
|
|
941
1017
|
real="$(find_real_claude "$_self")" || { warn "real claude binary not found"; return 1; }
|
|
942
1018
|
mkdir -p "$ACC_ROOT/tmp"
|
|
@@ -971,15 +1047,28 @@ TIP
|
|
|
971
1047
|
# Headless (tests / piped code): capture into the 0600 file only. Never tee the
|
|
972
1048
|
# raw token to stdout — a redirected run would write the secret to a plain log.
|
|
973
1049
|
CLAUDE_CONFIG_DIR="$d" CLAUDE_SHIM_ACTIVE=1 "$real" setup-token > "$cap" 2>&1
|
|
974
|
-
|
|
1050
|
+
# Every credential shape, not just setup-tokens: a Console session mints an API key.
|
|
1051
|
+
sed -E 's/sk-ant-[A-Za-z0-9]+-[A-Za-z0-9_-]*/sk-ant-***-<redacted>/g' "$cap"
|
|
975
1052
|
fi
|
|
976
1053
|
umask "$old_umask"
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1054
|
+
CEREMONY_TOKEN="$(ceremony_extract "$cap" 2>/dev/null)"
|
|
1055
|
+
if [ -z "$CEREMONY_TOKEN" ]; then
|
|
1056
|
+
local stamp rawkeep
|
|
1057
|
+
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
1058
|
+
CEREMONY_TRANSCRIPT="$ACC_ROOT/tmp/mint-failed.$stamp.log"
|
|
1059
|
+
rawkeep="$ACC_ROOT/tmp/mint-failed.$stamp.raw"
|
|
1060
|
+
CEREMONY_LAST_WORDS="$(ceremony_debrief "$cap" "$CEREMONY_TRANSCRIPT" 2>/dev/null)"
|
|
1061
|
+
[ -s "$CEREMONY_TRANSCRIPT" ] || CEREMONY_TRANSCRIPT=""
|
|
1062
|
+
# The raw bytes are the only record of what the renderer actually emitted, and may
|
|
1063
|
+
# hold a token this extractor did not recognise: kept 0600 beside the pool, like
|
|
1064
|
+
# the tokens themselves, so the next unknown shape can be read instead of guessed.
|
|
1065
|
+
( umask 077; cp "$cap" "$rawkeep" ) 2>/dev/null || true
|
|
1066
|
+
rm -f "$cap"
|
|
1067
|
+
[ -n "$CEREMONY_LAST_WORDS" ] && warn "the sign-in ended without a token — the client said: $CEREMONY_LAST_WORDS"
|
|
1068
|
+
[ -n "$CEREMONY_TRANSCRIPT" ] && warn "redacted transcript kept at $CEREMONY_TRANSCRIPT"
|
|
1069
|
+
return 1
|
|
1070
|
+
fi
|
|
981
1071
|
rm -f "$cap"
|
|
982
|
-
[ -n "$CEREMONY_TOKEN" ] || return 1
|
|
983
1072
|
valid_subscription_token "$CEREMONY_TOKEN" || {
|
|
984
1073
|
CEREMONY_TOKEN=""
|
|
985
1074
|
warn "captured credential is not a subscription setup-token"
|
|
@@ -1097,7 +1186,7 @@ cmd_mint() {
|
|
|
1097
1186
|
tok="$(printf '%s' "$tok" | tr -d '[:space:]')"
|
|
1098
1187
|
else
|
|
1099
1188
|
echo "Running 'claude setup-token' for $id — approve in a browser signed in as ${email:-THIS account}."
|
|
1100
|
-
run_token_ceremony "$d" || die "no token captured — mint failed"
|
|
1189
|
+
run_token_ceremony "$d" || die "no token captured — mint failed${CEREMONY_LAST_WORDS:+ — the client said: $CEREMONY_LAST_WORDS}${CEREMONY_TRANSCRIPT:+ (transcript: $CEREMONY_TRANSCRIPT)}"
|
|
1101
1190
|
tok="$CEREMONY_TOKEN"
|
|
1102
1191
|
fi
|
|
1103
1192
|
[ -n "$tok" ] || die "no token captured — mint failed"
|
|
@@ -1139,7 +1228,7 @@ cmd_login() {
|
|
|
1139
1228
|
if [ "$token" = "1" ]; then
|
|
1140
1229
|
trap 'restore_ceremony_tty; exit 130' INT TERM
|
|
1141
1230
|
trap 'restore_ceremony_tty' EXIT
|
|
1142
|
-
run_token_ceremony "$d" || die "sign-in failed or aborted — nothing changed"
|
|
1231
|
+
run_token_ceremony "$d" || die "sign-in failed or aborted — nothing changed${CEREMONY_LAST_WORDS:+ — the client said: $CEREMONY_LAST_WORDS}${CEREMONY_TRANSCRIPT:+ (transcript: $CEREMONY_TRANSCRIPT)}"
|
|
1143
1232
|
got="$(token_email "$CEREMONY_TOKEN")"
|
|
1144
1233
|
if [ -n "$got" ] && [ "$got" != "$email" ] && [ "$force" != "1" ]; then
|
|
1145
1234
|
die "you signed in as $got but $id is $email — nothing saved (use --force to override)"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/tests/run-tests.sh
CHANGED
|
@@ -73,18 +73,31 @@ if [ "${1:-}" = "auth" ] && [ "${2:-}" = "login" ]; then
|
|
|
73
73
|
fi
|
|
74
74
|
if [ "${1:-}" = "setup-token" ]; then
|
|
75
75
|
echo "Open this sign-in link: https://claude.ai/oauth/authorize?fake=1"
|
|
76
|
-
[ -n "${FAKE_TOKEN_FAIL:-}" ] && { echo "sign-in aborted" >&2; exit 1; }
|
|
76
|
+
[ -n "${FAKE_TOKEN_FAIL:-}" ] && { echo "${FAKE_TOKEN_FAIL_MSG:-sign-in aborted}" >&2; exit 1; }
|
|
77
77
|
if [ -n "${FAKE_TOKEN_FULL:-}" ]; then
|
|
78
|
-
# The real client's shape: a
|
|
79
|
-
#
|
|
80
|
-
#
|
|
78
|
+
# The real client's shape (2.1.251): a success line, the token on its own line
|
|
79
|
+
# between "Your OAuth token …:" and "Store this token securely", 108 characters.
|
|
80
|
+
# The renderer HARD-WRAPS it at the terminal's width (an unsized pty renders as 80
|
|
81
|
+
# — the 79-character first line every panel mint of 2026-08-28 saved), and even at
|
|
82
|
+
# 400 columns emits it as cursor-positioned, styled segments (my-mini, 2026-08-29).
|
|
81
83
|
tok="sk-ant-oat01-$(printf '%66s' '' | tr ' ' W)$(printf '%23s' '' | tr ' ' T)TAILOK"
|
|
82
84
|
cols="$(stty size 2>/dev/null | awk '{print $2}')"
|
|
83
|
-
|
|
85
|
+
printf 'Long-lived authentication token created successfully!\n'
|
|
86
|
+
printf 'Your\033[5GOAuth\033[11Gtoken\033[17G(valid\033[24Gfor\033[28G1\033[30Gyear):\n'
|
|
87
|
+
if [ -n "${FAKE_TOKEN_SPLIT:-}" ]; then
|
|
88
|
+
printf '\033[2G%s\033[38;5;246m\033[15G%s\033[39m\033[60G%s\033[0m\r\n' "${tok:0:13}" "${tok:13:45}" "${tok:58}"
|
|
89
|
+
elif [ -n "${FAKE_TOKEN_WRAP:-}" ] || { [ -n "$cols" ] && [ "$cols" -lt 108 ]; }; then
|
|
84
90
|
printf ' %s\n%s\n' "${tok:0:79}" "${tok:79}"
|
|
85
91
|
else
|
|
86
92
|
printf ' %s\n' "$tok"
|
|
87
93
|
fi
|
|
94
|
+
printf 'Store\033[7Gthis\033[12Gtoken\033[18Gsecurely.\n'
|
|
95
|
+
exit 0
|
|
96
|
+
fi
|
|
97
|
+
if [ -n "${FAKE_TOKEN_APIKEY:-}" ]; then
|
|
98
|
+
# A browser session signed into a Console (API-billing) org: the client mints an
|
|
99
|
+
# API key, which is not a subscription token and must not be saved.
|
|
100
|
+
printf 'Your OAuth token (valid for 1 year):\nsk-ant-api03-%s\nStore this token securely.\n' "$(printf '%80s' '' | tr ' ' K)"
|
|
88
101
|
exit 0
|
|
89
102
|
fi
|
|
90
103
|
echo "Your token: sk-ant-oat01-FAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKE"
|
|
@@ -1383,25 +1396,54 @@ rows = {a["id"]: a for a in json.load(sys.stdin)["accounts"]}
|
|
|
1383
1396
|
print(rows["acct-04"].get("token_verified"))')"
|
|
1384
1397
|
[ "$out" = "True" ] && t_ok "list --json reports the token as verified here" \
|
|
1385
1398
|
|| t_fail "list --json token_verified" "expected True, got: $out"
|
|
1386
|
-
# The 2026-08-28 shape: the client's TUI wrapped the 108-character token at 80 columns
|
|
1387
|
-
#
|
|
1388
|
-
# mint must refuse to save it and say WHY (seven fleet-wide 401s were the old answer).
|
|
1399
|
+
# The 2026-08-28 shape: the client's TUI wrapped the 108-character token at 80 columns.
|
|
1400
|
+
# The token block is joined across the break, so the capture is WHOLE — and proven.
|
|
1389
1401
|
orig="$(cat "$ACC/acct-04/server.token")"
|
|
1390
1402
|
out="$(FAKE_TOKEN_FULL=1 FAKE_TOKEN_WRAP=1 claude-accounts mint acct-04 2>&1 </dev/null)"
|
|
1391
1403
|
rc=$?
|
|
1392
|
-
[ "$rc"
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
#
|
|
1400
|
-
|
|
1404
|
+
[ "$rc" = "0" ] && t_ok "a wrapped token is reassembled from the transcript" || t_fail "wrapped token rc" "rc=$rc: $(printf '%s' "$out" | tail -c 200)"
|
|
1405
|
+
case "$(tr -d '[:space:]' < "$ACC/acct-04/server.token")" in
|
|
1406
|
+
*TAILOK) [ "$(tr -d '[:space:]' < "$ACC/acct-04/server.token" | wc -c | tr -d ' ')" = "108" ] \
|
|
1407
|
+
&& t_ok "the reassembled token is the complete one" || t_fail "reassembled token" "wrong length" ;;
|
|
1408
|
+
*) t_fail "reassembled token" "tail missing" ;;
|
|
1409
|
+
esac
|
|
1410
|
+
printf '%s' "$orig" > "$ACC/acct-04/server.token"; rm -f "$ACC/acct-04/.server-token-verified"
|
|
1411
|
+
# The 2026-08-29 shape: at 400 columns the renderer emits the token as cursor-positioned,
|
|
1412
|
+
# styled segments — a raw-bytes grep saw no token at all right after "token created
|
|
1413
|
+
# successfully". Stripped and joined, it is whole.
|
|
1414
|
+
out="$(FAKE_TOKEN_FULL=1 FAKE_TOKEN_SPLIT=1 claude-accounts mint acct-04 2>&1 </dev/null)"
|
|
1401
1415
|
rc=$?
|
|
1402
|
-
[ "$rc"
|
|
1403
|
-
|| t_fail "
|
|
1404
|
-
|
|
1416
|
+
[ "$rc" = "0" ] && t_ok "a token the renderer split into positioned segments is captured" \
|
|
1417
|
+
|| t_fail "split token rc" "rc=$rc: $(printf '%s' "$out" | tail -c 200)"
|
|
1418
|
+
case "$(tr -d '[:space:]' < "$ACC/acct-04/server.token")" in
|
|
1419
|
+
sk-ant-oat01-WWW*TAILOK) t_ok "the split token was joined correctly" ;;
|
|
1420
|
+
*) t_fail "split token" "unexpected content" ;;
|
|
1421
|
+
esac
|
|
1422
|
+
[ -f "$ACC/acct-04/.server-token-verified" ] && t_ok "the joined token was proven by a real call" \
|
|
1423
|
+
|| t_fail "split token proof" "marker missing"
|
|
1424
|
+
printf '%s' "$orig" > "$ACC/acct-04/server.token"; rm -f "$ACC/acct-04/.server-token-verified"
|
|
1425
|
+
# A browser session on a Console (API-billing) org: the client mints an API KEY. Not a
|
|
1426
|
+
# subscription token — refused, with the cause named and the raw capture kept.
|
|
1427
|
+
before_raw="$(ls "$ACC"/tmp/mint-failed.*.raw 2>/dev/null | wc -l | tr -d ' ')"
|
|
1428
|
+
out="$(FAKE_TOKEN_APIKEY=1 claude-accounts mint acct-04 2>&1 </dev/null)"
|
|
1429
|
+
rc=$?
|
|
1430
|
+
[ "$rc" != "0" ] && t_ok "an API key minted by a Console session is refused" || t_fail "apikey rc" "rc=0"
|
|
1431
|
+
check "the API-key refusal names the cause" "API KEY" "$out"
|
|
1432
|
+
check "the API-key refusal points at the org" "Console" "$out"
|
|
1433
|
+
case "$out" in *sk-ant-api03-K*) t_fail "the API key never reaches the operator's screen" "leaked" ;; *) t_ok "the API key never reaches the operator's screen" ;; esac
|
|
1434
|
+
[ "$(cat "$ACC/acct-04/server.token")" = "$orig" ] && t_ok "an API key is never saved as the token" \
|
|
1435
|
+
|| t_fail "apikey save" "server.token was overwritten"
|
|
1436
|
+
[ "$(ls "$ACC"/tmp/mint-failed.*.raw 2>/dev/null | wc -l | tr -d ' ')" -gt "$before_raw" ] \
|
|
1437
|
+
&& t_ok "the raw capture is kept for a closer look" || t_fail "raw capture" "no .raw kept"
|
|
1438
|
+
[ "$(python3 -c 'import os, sys; print(oct(os.stat(sys.argv[1]).st_mode & 0o777))' "$(ls -t "$ACC"/tmp/mint-failed.*.raw | head -1)")" = "0o600" ] \
|
|
1439
|
+
&& t_ok "the raw capture is private" || t_fail "raw capture mode" "not 0600"
|
|
1440
|
+
# A 79-character FRAGMENT that really is all there is (a paste from a narrow terminal)
|
|
1441
|
+
# with a probe that cannot decide: refused, and it says why.
|
|
1442
|
+
out="$(printf 'sk-ant-oat01-%s' "$(printf '%66s' '' | tr ' ' W)" | FAKE_PROBE_FLAKY=1 claude-accounts mint acct-04 --paste 2>&1)"
|
|
1443
|
+
rc=$?
|
|
1444
|
+
[ "$rc" != "0" ] && t_ok "a bare fragment is refused even when the probe is inconclusive" \
|
|
1445
|
+
|| t_fail "fragment+inconclusive rc" "rc=0"
|
|
1446
|
+
check "the fragment refusal names the truncation" "79 characters" "$out"
|
|
1405
1447
|
[ "$(cat "$ACC/acct-04/server.token")" = "$orig" ] && t_ok "an unproven fragment saves nothing" \
|
|
1406
1448
|
|| t_fail "unproven fragment" "server.token was overwritten"
|
|
1407
1449
|
# …while a COMPLETE token the probe cannot reach right now is saved as UNVERIFIED — the
|
|
@@ -1415,6 +1457,24 @@ check "the inconclusive save says so" "saved as UNVERIFIED" "$out"
|
|
|
1415
1457
|
[ ! -f "$ACC/acct-04/.server-token-verified" ] && t_ok "an unproven save records no proof" \
|
|
1416
1458
|
|| t_fail "unproven proof marker" "marker present"
|
|
1417
1459
|
printf '%s' "$orig" > "$ACC/acct-04/server.token"
|
|
1460
|
+
# The client ended the ceremony without a token and said why: that sentence must
|
|
1461
|
+
# reach the operator (the panel shows the mint's own error line), and the redacted
|
|
1462
|
+
# transcript must be kept for a closer look — "no token captured" alone left an
|
|
1463
|
+
# operator with nothing to act on (2026-08-29).
|
|
1464
|
+
out="$(FAKE_TOKEN_FAIL=1 FAKE_TOKEN_FAIL_MSG="Your account is on hold. You can close this window." claude-accounts mint acct-04 2>&1 </dev/null)"
|
|
1465
|
+
rc=$?
|
|
1466
|
+
[ "$rc" != "0" ] && t_ok "a ceremony that ends without a token fails the mint" || t_fail "no-token mint rc" "rc=0"
|
|
1467
|
+
check "the mint error carries the client's last words" "the client said: Your account is on hold" "$out"
|
|
1468
|
+
check "the mint error names the kept transcript" "transcript: $ACC/tmp/mint-failed." "$out"
|
|
1469
|
+
tr="$(printf '%s' "$out" | sed -n 's/.*transcript: \([^)]*\)).*/\1/p' | head -1)"
|
|
1470
|
+
[ -n "$tr" ] && [ -s "$tr" ] && t_ok "the redacted transcript exists" || t_fail "transcript file" "missing: '$tr'"
|
|
1471
|
+
grep -q 'Your account is on hold' "$tr" 2>/dev/null && t_ok "the transcript holds the client's words" \
|
|
1472
|
+
|| t_fail "transcript content" "message missing"
|
|
1473
|
+
# (python, not stat: GNU stat reads -f as "file system" and answers something else)
|
|
1474
|
+
[ "$(python3 -c 'import os, sys; print(oct(os.stat(sys.argv[1]).st_mode & 0o777))' "$tr")" = "0o600" ] \
|
|
1475
|
+
&& t_ok "the transcript is private" || t_fail "transcript mode" "not 0600"
|
|
1476
|
+
[ "$(cat "$ACC/acct-04/server.token")" = "$orig" ] && t_ok "a ceremony without a token saves nothing" \
|
|
1477
|
+
|| t_fail "no-token save" "server.token was overwritten"
|
|
1418
1478
|
# A pasted token gets the same proof: a revoked one is refused, not saved.
|
|
1419
1479
|
# (A complete-length token, so the verdict is the rejection itself and not the
|
|
1420
1480
|
# length rule that catches wrapped fragments first.)
|