claude-multiacc 2.0.16 → 2.0.17

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.
@@ -943,69 +943,18 @@ commit_ceremony_token() {
943
943
  CEREMONY_LAST_WORDS=""
944
944
  CEREMONY_TRANSCRIPT=""
945
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
946
+ "$PYBIN" "$LIB_DIR/ceremony.py" debrief "$1" "$2" 2>/dev/null
980
947
  }
981
948
 
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.
949
+ # The token, out of the transcript. `claude setup-token` is a TUI: its renderer places
950
+ # words with absolute cursor moves, so the bytes are terminal OPERATIONS and the token
951
+ # exists only in the RENDERED result. Stripping escapes reassembles it wrongly a
952
+ # `sk-ant-\x1b[10Gat01-…` stream loses the `o` and three mints failed as "no token
953
+ # captured" seconds after the client said the token was created (2026-08-29). lib/
954
+ # ceremony.py replays the transcript onto a virtual screen and reads what the operator
955
+ # saw; the real inference in commit_ceremony_token then proves whatever came out.
990
956
  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
957
+ "$PYBIN" "$LIB_DIR/ceremony.py" extract "$1" 2>/dev/null
1009
958
  }
1010
959
 
1011
960
  CEREMONY_TOKEN=""
@@ -0,0 +1,193 @@
1
+ """Read what the sign-in ceremony actually PUT ON THE SCREEN.
2
+
3
+ `claude setup-token` is a TUI. Its renderer places words with absolute cursor
4
+ moves rather than spaces, so the bytes it emits are terminal OPERATIONS, not
5
+ text: on 2026-08-29 a perfectly good token arrived as
6
+
7
+ sk-ant-\x1b[10Gat01-w_C6…
8
+
9
+ Stripping the escapes yields `sk-ant-at01-…` — the sequence's own final byte ate
10
+ the token's `o` — and no amount of whitespace-joining puts it back. Three mints
11
+ failed as "no token captured" seconds after the client printed
12
+ "✓ Long-lived authentication token created successfully!".
13
+
14
+ So the transcript is REPLAYED onto a virtual screen and the result is read, which
15
+ is by definition what the operator saw. Only the handful of sequences this TUI
16
+ uses are honoured; everything else is skipped, and any byte that would move the
17
+ cursor off-screen is clamped rather than trusted.
18
+
19
+ Run directly: ceremony.py extract <capture> -> the setup-token, or nothing
20
+ ceremony.py debrief <capture> <out> -> last words; writes a
21
+ redacted transcript (0600)
22
+ """
23
+
24
+ import os
25
+ import re
26
+ import sys
27
+
28
+ TOKEN = re.compile(r'sk-ant-oat\d{2}-[A-Za-z0-9_-]{40,}')
29
+ ANY_CRED = re.compile(r'sk-ant-[A-Za-z0-9]+-[A-Za-z0-9_-]+')
30
+ API_KEY = re.compile(r'sk-ant-api\d{2}-')
31
+ _CSI = re.compile(r'\x1b\[([0-9;?<>=]*)([@-~])')
32
+ _CHARSET = re.compile(r'\x1b[()][A-Z0-9]|\x1b[78=>]')
33
+ # A screen this TUI could never legitimately need; a corrupt stream must not make
34
+ # the renderer allocate without bound.
35
+ MAX_ROWS = 2000
36
+ MAX_COLS = 4000
37
+
38
+
39
+ def render(raw):
40
+ """The transcript's final screen, as a list of lines."""
41
+ rows, cur, col = [[]], 0, 0
42
+
43
+ def put(ch):
44
+ nonlocal col
45
+ if cur >= MAX_ROWS or col >= MAX_COLS:
46
+ return
47
+ while len(rows) <= cur:
48
+ rows.append([])
49
+ row = rows[cur]
50
+ while len(row) <= col:
51
+ row.append(' ')
52
+ row[col] = ch
53
+ col += 1
54
+
55
+ i, n = 0, len(raw)
56
+ while i < n:
57
+ ch = raw[i]
58
+ if ch == '\x1b':
59
+ if raw.startswith('\x1b]', i): # OSC … BEL | ST
60
+ bel, st = raw.find('\x07', i), raw.find('\x1b\\', i)
61
+ ends = [x for x in ((bel, 1), (st, 2)) if x[0] != -1]
62
+ i = min(ends)[0] + min(ends)[1] if ends else n
63
+ continue
64
+ m = _CSI.match(raw, i)
65
+ if m:
66
+ params, fin = m.group(1), m.group(2)
67
+ nums = [int(p) for p in params.split(';') if p.isdigit()]
68
+ a = nums[0] if nums else 1
69
+ if fin == 'G':
70
+ col = min(max(0, a - 1), MAX_COLS)
71
+ elif fin == 'C':
72
+ col = min(col + a, MAX_COLS)
73
+ elif fin == 'D':
74
+ col = max(0, col - a)
75
+ elif fin == 'A':
76
+ cur = max(0, cur - a)
77
+ elif fin == 'B':
78
+ cur = min(cur + a, MAX_ROWS)
79
+ elif fin == 'H':
80
+ cur = min(max(0, a - 1), MAX_ROWS)
81
+ col = min(max(0, (nums[1] if len(nums) > 1 else 1) - 1), MAX_COLS)
82
+ elif fin == 'K':
83
+ while len(rows) <= cur:
84
+ rows.append([])
85
+ if params.startswith('2'):
86
+ rows[cur] = []
87
+ elif not params or params == '0':
88
+ rows[cur] = rows[cur][:col]
89
+ elif fin == 'J' and params.startswith('2'):
90
+ rows, cur, col = [[]], 0, 0
91
+ i = m.end()
92
+ continue
93
+ m = _CHARSET.match(raw, i)
94
+ i = m.end() if m else i + 1
95
+ continue
96
+ if ch == '\r':
97
+ col = 0
98
+ elif ch == '\n':
99
+ cur = min(cur + 1, MAX_ROWS)
100
+ col = 0
101
+ elif ord(ch) >= 32:
102
+ put(ch)
103
+ i += 1
104
+ return [''.join(r).rstrip() for r in rows]
105
+
106
+
107
+ # The block the client prints the token inside. Joining WITHIN it is what survives a
108
+ # terminal narrow enough to wrap the token across rows, without gluing the token to
109
+ # whatever the next paragraph happens to say.
110
+ _BLOCK_START = re.compile(r'Your\s*OAuth\s*token[^\n]{0,80}?:')
111
+ _BLOCK_END = re.compile(r'Store\s*this\s*token')
112
+
113
+
114
+ def extract_token(raw):
115
+ """The setup-token the ceremony printed, or ''.
116
+
117
+ Every reading is a candidate and the LONGEST wins, because the two shapes fail
118
+ each other's method: a token painted out of order is whole only on the rendered
119
+ line, while one the terminal wrapped is whole only across rows. A per-line match
120
+ on a wrapped token returns its first 79 characters — exactly the truncation this
121
+ whole path exists to prevent — so the joined reading has to compete with it.
122
+ """
123
+ lines = render(raw)
124
+ text = '\n'.join(lines)
125
+ cands = [m.group(0) for ln in lines for m in [TOKEN.search(ln)] if m]
126
+ for m in _BLOCK_START.finditer(text):
127
+ block = text[m.end(): m.end() + 4000]
128
+ stop = _BLOCK_END.search(block)
129
+ if stop:
130
+ block = block[: stop.start()]
131
+ cands += TOKEN.findall(re.sub(r'\s+', '', block))
132
+ if not cands: # a TUI this renderer misreads
133
+ flat = _CSI.sub('', _CHARSET.sub('', raw))
134
+ cands = TOKEN.findall(flat) + TOKEN.findall(re.sub(r'\s+', '', flat))
135
+ return max(cands, key=len) if cands else ''
136
+
137
+
138
+ def debrief(raw):
139
+ """(readable lines, api_key_minted). The screen the operator was left looking
140
+ at, with every credential-shaped string masked."""
141
+ api_key = False
142
+ for ln in render(raw):
143
+ if API_KEY.search(ln):
144
+ api_key = True
145
+ if extract_token(raw):
146
+ api_key = False
147
+ lines = []
148
+ for ln in render(raw):
149
+ ln = ANY_CRED.sub('sk-ant-***', ln).strip()
150
+ if not ln or re.fullmatch(r'[\W_]+', ln): # spinner frames, logo art
151
+ continue
152
+ if 'https://' in ln or re.search(r'Paste\s*code\s*here', ln):
153
+ continue
154
+ if lines and lines[-1] == ln:
155
+ continue
156
+ lines.append(ln)
157
+ if api_key:
158
+ # Said LAST, so it is what the operator reads.
159
+ lines.append('the client minted an API KEY (sk-ant-api…), not a subscription '
160
+ 'setup-token: the browser session belongs to a Console / '
161
+ 'API-billing organization — sign in as the subscription account '
162
+ 'and try again')
163
+ return lines, api_key
164
+
165
+
166
+ def _read(path):
167
+ with open(path, 'rb') as f:
168
+ return f.read().decode('utf-8', 'ignore')
169
+
170
+
171
+ def main(argv):
172
+ if len(argv) < 2:
173
+ return 2
174
+ verb, path = argv[0], argv[1]
175
+ if verb == 'extract':
176
+ sys.stdout.write(extract_token(_read(path)))
177
+ return 0
178
+ if verb == 'debrief':
179
+ lines, _ = debrief(_read(path))
180
+ if len(argv) > 2:
181
+ try:
182
+ fd = os.open(argv[2], os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
183
+ with os.fdopen(fd, 'w') as f:
184
+ f.write('\n'.join(lines) + '\n')
185
+ except OSError:
186
+ pass
187
+ print(' | '.join(lines[-3:])[:400])
188
+ return 0
189
+ return 2
190
+
191
+
192
+ if __name__ == '__main__':
193
+ sys.exit(main(sys.argv[1:]))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-multiacc",
3
- "version": "2.0.16",
3
+ "version": "2.0.17",
4
4
  "description": "Unified Claude Code and OpenAI Codex subscription pooling with quota-aware selection.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -94,6 +94,16 @@ if [ "${1:-}" = "setup-token" ]; then
94
94
  printf 'Store\033[7Gthis\033[12Gtoken\033[18Gsecurely.\n'
95
95
  exit 0
96
96
  fi
97
+ if [ -n "${FAKE_TOKEN_CURSOR:-}" ]; then
98
+ # The 2026-08-29 shape: the TUI paints the token OUT OF ORDER with absolute cursor
99
+ # moves, so the byte stream is `sk-ant-<ESC>[10Gat01-…` and escape-stripping loses
100
+ # the `o`. Only replaying the transcript onto a screen recovers it.
101
+ tok="sk-ant-oat01-$(printf '%66s' '' | tr ' ' W)$(printf '%23s' '' | tr ' ' T)TAILOK"
102
+ printf ' Your\033[6GOAuth\033[12Gtoken\033[18G(valid\033[25Gfor\033[29G1\033[31Gyear):\n'
103
+ printf ' %s\033[10G%s\033[9G%s\n' "${tok:0:7}" "${tok:8}" "${tok:7:1}"
104
+ printf ' Store\033[7Gthis\033[12Gtoken\033[18Gsecurely.\n'
105
+ exit 0
106
+ fi
97
107
  if [ -n "${FAKE_TOKEN_APIKEY:-}" ]; then
98
108
  # A browser session signed into a Console (API-billing) org: the client mints an
99
109
  # API key, which is not a subscription token and must not be saved.
@@ -1475,6 +1485,28 @@ esac
1475
1485
  [ -f "$ACC/acct-04/.server-token-verified" ] && t_ok "the joined token was proven by a real call" \
1476
1486
  || t_fail "split token proof" "marker missing"
1477
1487
  printf '%s' "$orig" > "$ACC/acct-04/server.token"; rm -f "$ACC/acct-04/.server-token-verified"
1488
+ # The token painted OUT OF ORDER with absolute cursor moves — the shape that made three
1489
+ # real mints fail as "no token captured" right after "token created successfully".
1490
+ out="$(FAKE_TOKEN_CURSOR=1 claude-accounts mint acct-04 2>&1 </dev/null)"
1491
+ rc=$?
1492
+ [ "$rc" = "0" ] && t_ok "a cursor-painted token is read off the rendered screen" \
1493
+ || t_fail "cursor-painted mint rc" "rc=$rc: $(printf '%s' "$out" | tail -c 220)"
1494
+ case "$(tr -d '[:space:]' < "$ACC/acct-04/server.token")" in
1495
+ sk-ant-oat01-WWW*TAILOK) [ "$(tr -d '[:space:]' < "$ACC/acct-04/server.token" | wc -c | tr -d ' ')" = "108" ] \
1496
+ && t_ok "the rendered token is complete and in order" || t_fail "rendered token" "wrong length" ;;
1497
+ *) t_fail "rendered token" "wrong content — escape-stripping order bug" ;;
1498
+ esac
1499
+ printf '%s' "$orig" > "$ACC/acct-04/server.token"; rm -f "$ACC/acct-04/.server-token-verified"
1500
+ # The renderer itself, on the exact byte pattern from the field.
1501
+ printf ' sk-ant-\033[10Gat01-%s\033[9Go\n' "$(printf '%95s' '' | tr ' ' Z)" > "$WORK/painted.raw"
1502
+ n="$(python3 "$REPO_DIR/lib/ceremony.py" extract "$WORK/painted.raw" | wc -c | tr -d ' ')"
1503
+ [ "$n" = "108" ] && t_ok "ceremony.py renders a cursor-painted token whole" \
1504
+ || t_fail "ceremony.py extract" "got $n characters, expected 108"
1505
+ # ...and the debrief reads as prose, not as words jammed together by the stripping.
1506
+ printf ' Store\033[8Gthis\033[13Gtoken\033[19Gsecurely.\n' > "$WORK/prose.raw"
1507
+ out="$(python3 "$REPO_DIR/lib/ceremony.py" debrief "$WORK/prose.raw" "$WORK/prose.out")"
1508
+ check "the debrief renders readable words" "Store this token securely." "$out"
1509
+
1478
1510
  # A browser session on a Console (API-billing) org: the client mints an API KEY. Not a
1479
1511
  # subscription token — refused, with the cause named and the raw capture kept.
1480
1512
  before_raw="$(ls "$ACC"/tmp/mint-failed.*.raw 2>/dev/null | wc -l | tr -d ' ')"