Skip to content

Commit d8bd888

Browse files
jgravelleclaude
andcommitted
release: v1.108.163 — license-key transport hardening (WS-8) + benchmark integrity (WS-7)
WS-8 (audit V12): validate.php checks POST the key in the request body; starter-pack downloads carry it in the X-JCM-License header. The key never rides a URL query string, so it can't land in server/proxy access logs. Each path keeps a one-shot legacy fallback keyed to the exact pre-deploy backend error signature, so ship order can't lock out a paying customer. Also fixes install-pack rendering every 4xx JSON error as a network failure. WS-7 (audit V1): benchmarks/results.md regenerated at v1.108.163 against same-day re-indexes (99.6% aggregate, per-query 99.1-99.9%); README headline reconciled to the same figure (stale 95% capped-index table replaced); METHODOLOGY + provenance measured.json updated in lockstep (drift-guard green); harness dead get_file_content_text fallback fixed; token baseline v1.108.163 captured so analyze_perf(compare_release=) resolves. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent e3178e1 commit d8bd888

11 files changed

Lines changed: 406 additions & 84 deletions

File tree

CHANGELOG.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,43 @@
22

33
All notable changes to jcodemunch-mcp are documented here.
44

5+
## [1.108.163] - 2026-07-23 - License-key transport hardening: key never rides the URL (audit WS-8)
6+
7+
### Security
8+
- **License validation POSTs the key in the request body** (`org/license.py`).
9+
The key previously traveled as a GET query parameter to the validation
10+
endpoint, where it could land in server/proxy/CDN access logs. The check now
11+
sends a form-encoded POST; a one-shot legacy GET fallback fires only on the
12+
exact missing-parameter signature an older backend produces, so a deploy-order
13+
gap can't lock out a paying customer (a genuine key rejection never retries).
14+
- **Starter-pack downloads carry the key in the `X-JCM-License` header**
15+
(`cli/install_pack.py`) instead of `&license=` in the URL, with the same
16+
narrowly-keyed one-shot legacy fallback.
17+
18+
### Fixed
19+
- `install-pack` no longer reports every license/pack error as "Could not reach
20+
the starter packs server": the API returns those as 4xx + JSON, and the old
21+
`raise_for_status()` converted them into a bogus network error before the
22+
real message could be shown. Transport failures still report as unreachable.
23+
24+
### Benchmarks (audit WS-7 — benchmark integrity)
25+
- Regenerated `benchmarks/results.md` at v1.108.163 against same-day re-indexes
26+
of the three canonical repos (express 172 / fastapi 1,000 / gin 109 files):
27+
**99.6% aggregate reduction** (5,799,695 baseline tokens → 25,220), per-query
28+
range 99.1–99.9%. The hand-written A/B test sections are preserved verbatim.
29+
- Reconciled the README headline to the single current figure: the stale
30+
"95% average" table (from an old 34-file capped express index) is replaced by
31+
the regenerated full-index numbers, so README, `benchmarks/results.md`,
32+
`benchmarks/METHODOLOGY.md`, and `benchmarks/provenance/measured.json` now
33+
all quote the same 99.6% aggregate. "95%+" floor phrasing elsewhere is
34+
unchanged (conservative floor, still true).
35+
- Fixed the benchmark harness's dead baseline fallback: `measure_baseline`
36+
called nonexistent `store.get_file_content_text(...)`; it now uses the real
37+
`store.get_file_content(...)` reader.
38+
- Captured `benchmarks/token_baselines/v1.108.163.json` so
39+
`analyze_perf(compare_release="1.108.163")` resolves a real comparison
40+
instead of a baseline-missing error.
41+
542
## [1.108.162] - 2026-07-23 - Canonical handoff contract: finalize_handoff + munch://handoff/<id> (#374)
643

744
### Added

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
> **Real results, live from production**
66
> **335B+ tokens saved** · **48,000+ developers** · **$1.69M+ in AI spend avoided** · **40,000+ kg CO₂ prevented**
7-
> Live telemetry at **[jcodemunch.com](https://jcodemunch.com/)** — benchmark: **95% average token reduction** (15 tasks / 3 repos, 99.8% peak).
7+
> Live telemetry at **[jcodemunch.com](https://jcodemunch.com/)** — benchmark: **99.6% average token reduction** (15 tasks / 3 repos, 99.9% peak; run 2026-07-23).
88
99
Works with **Autohand Code**, **Claude Code**, **Cursor**, **VS Code**, **Codex CLI**, **Continue**, **Windsurf**, and any MCP-compatible client.
1010

@@ -203,12 +203,12 @@ Measured with `tiktoken cl100k_base` across three public repos. Workflow: `searc
203203

204204
| Repository | Files | Symbols | Baseline tokens | jCodeMunch tokens | Reduction |
205205
|------------|------:|--------:|----------------:|------------------:|----------:|
206-
| expressjs/express | 34 | 117 | 73,838 | ~1,300 avg | **98.4%** |
207-
| fastapi/fastapi | 156 | 1,359 | 214,312 | ~15,600 avg | **92.7%** |
208-
| gin-gonic/gin | 40 | 805 | 84,892 | ~1,730 avg | **98.0%** |
209-
| **Grand total (15 task-runs)** | | | **1,865,210** | **92,515** | **95.0%** |
206+
| expressjs/express | 172 | 182 | 143,355 | ~1,040 avg | **99.3%** |
207+
| fastapi/fastapi | 1,000 | 6,722 | 823,784 | ~2,490 avg | **99.7%** |
208+
| gin-gonic/gin | 109 | 1,502 | 192,800 | ~1,510 avg | **99.2%** |
209+
| **Grand total (15 task-runs)** | | | **5,799,695** | **25,220** | **99.6%** |
210210

211-
Per-query results range from 79.7% (dense FastAPI router query) to 99.8% (sparse context-bind query on Express). The 95% figure is the aggregate. Run `python benchmarks/harness/run_benchmark.py` to reproduce.
211+
Per-query results range from 99.1% to 99.9%. The 99.6% figure is the aggregate (run 2026-07-23, v1.108.163, full un-capped indexes). Run `python benchmarks/harness/run_benchmark.py` to reproduce.
212212

213213
### A/B test on production codebase
214214

@@ -379,7 +379,7 @@ Everything jCodeMunch does beyond answering a tool call is listed here. All of i
379379
- **Local index storage.** Indexes live at `~/.code-index/` (override with `CODE_INDEX_PATH`). Delete the directory and every trace of indexing is gone.
380380
- **Live session journal.** While the server runs, it periodically writes a small `_session_live.json` in `~/.code-index/` recording the files and searches the agent touched this session (paths and query strings only, no file contents). It exists so the out-of-process PreCompact hook can restore session orientation after context compaction. Throttled, atomically written, overwritten in place; disable with `JCODEMUNCH_LIVE_JOURNAL=0`.
381381
- **User-invoked network calls.** A few commands you run explicitly reach the network. None run in the background or fire on a plain import; each happens only when you invoke the command:
382-
- **License validation.** `license`, `org-rollup`, and `install-pack --license` send your license key to `validate.php` on `j.gravelle.us` to confirm it. This gates only the team `org-rollup` feature; the individual tools never call it.
382+
- **License validation.** `license`, `org-rollup`, and `install-pack --license` send your license key to `validate.php` on `j.gravelle.us` to confirm it. The key travels in the request body / a header, never the URL, so it can't land in intermediary access logs. This gates only the team `org-rollup` feature; the individual tools never call it.
383383
- **Starter-pack download.** `install-pack` fetches the pack catalog and any pre-built index pack you request from `j.gravelle.us` (a premium pack also sends your license key).
384384
- **Embedding-model download.** `download-model` — and the first semantic encode when the `[local-embed]` extra is installed — downloads the ONNX model (`all-MiniLM-L6-v2`, ~23 MB, one time) from `huggingface.co`; after that, semantic search needs no network.
385385

@@ -1060,7 +1060,7 @@ suite stays usable in plan mode.
10601060
**How much can I save on Claude / Opus tokens?**
10611061
In retrieval-heavy workflows, code-reading tokens typically drop **95%+** because
10621062
the agent fetches exact symbols instead of brute-reading whole files — benchmarked
1063-
at a 95% average reduction across 15 tasks / 3 repositories, with peaks of 99.8%
1063+
at a 99.6% average reduction across 15 tasks / 3 repositories, with peaks of 99.9%
10641064
on large repos. Compact [MUNCH](SPEC_MUNCH.md) encoding then trims another ~45%
10651065
off the wire. Full methodology and harness: [TOKEN_SAVINGS.md](TOKEN_SAVINGS.md)
10661066
and [benchmarks/](benchmarks/).

benchmarks/METHODOLOGY.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ jcodemunch's standard skip patterns (node_modules, __pycache__, etc.).
1717

1818
| Repository | Files Indexed | Symbols Extracted | Baseline Tokens |
1919
|------------|:------------:|:-----------------:|:--------------:|
20-
| expressjs/express | 165 | 181 | 137,978 |
21-
| fastapi/fastapi | 951 | 5,325 | 699,425 |
22-
| gin-gonic/gin | 98 | 1,489 | 187,018 |
20+
| expressjs/express | 172 | 182 | 143,355 |
21+
| fastapi/fastapi | 1,000 | 6,722 | 823,784 |
22+
| gin-gonic/gin | 109 | 1,502 | 192,800 |
2323

2424
## Query Corpus
2525

@@ -145,11 +145,11 @@ details.
145145
## Common Misreadings
146146

147147
**"The claim is up to 99%."**
148-
The primary claim is **99.6% average** across all 15 task-runs (5,122,105 baseline tokens →
149-
19,406 jCodeMunch tokens). Individual queries reach 99.9% on large repos with tight symbol
150-
matches (e.g., `error exception` on fastapi/fastapi: 99.9%, 801x). The 99.6% aggregate
151-
is the honest headline across the current index state (express 165 files, fastapi 951 files,
152-
gin 98 files; run 2026-03-28).
148+
The primary claim is **99.6% average** across all 15 task-runs (5,799,695 baseline tokens →
149+
25,220 jCodeMunch tokens). Individual queries reach 99.9% on large repos with tight symbol
150+
matches (e.g., `error exception` on fastapi/fastapi). The 99.6% aggregate
151+
is the honest headline across the current index state (express 172 files, fastapi 1,000 files,
152+
gin 109 files; run 2026-07-23, v1.108.163, repos re-indexed same day).
153153

154154
**"I tested a different repo and got 80%."**
155155
Results vary by repo structure. Flat script collections (e.g., a repository of hundreds

benchmarks/harness/run_benchmark.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,9 @@ def measure_baseline(store: IndexStore, owner: str, name: str) -> dict:
127127
content = abs_path.read_text(encoding="utf-8", errors="replace")
128128
except Exception:
129129
try:
130-
content = store.get_file_content_text(owner, name, rel_path) or ""
130+
# Store-backed fallback (the real reader; the old
131+
# get_file_content_text name never existed → dead fallback).
132+
content = store.get_file_content(owner, name, rel_path, index) or ""
131133
except Exception:
132134
content = ""
133135
total_tokens += count_tokens(content)

benchmarks/provenance/measured.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
"token_reduction": {
44
"average_pct": 99.6,
55
"task_runs": 15,
6-
"baseline_tokens": 5122105,
7-
"jcodemunch_tokens": 19406,
6+
"baseline_tokens": 5799695,
7+
"jcodemunch_tokens": 25220,
88
"tokenizer": "cl100k_base",
9-
"run_date": "2026-03-28",
9+
"run_date": "2026-07-23",
1010
"methodology": "benchmarks/METHODOLOGY.md",
1111
"results": "benchmarks/results.md"
1212
},

benchmarks/results.md

Lines changed: 48 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,97 @@
11
# jcodemunch-mcp -- Token Efficiency Benchmark
22

33
**Tokenizer:** `cl100k_base` (tiktoken)
4-
**Workflow:** `search_symbols` (top 5) + `get_symbol_source` x 3
4+
**Workflow:** `search_symbols` (top 5) + `get_symbol` x 3
55
**Baseline:** all source files concatenated (minimum for "open every file" agent)
6+
**Run:** 2026-07-23, jcodemunch-mcp v1.108.163 (repos re-indexed same day)
67

78
## expressjs/express
89

910
| Metric | Value |
1011
|--------|-------|
11-
| Files indexed | **165** |
12-
| Symbols extracted | **181** |
13-
| Baseline tokens (all files) | **137,978** |
12+
| Files indexed | **172** |
13+
| Symbols extracted | **182** |
14+
| Baseline tokens (all files) | **143,355** |
1415

1516
| Query | Baseline&nbsp;tokens | jMunch&nbsp;tokens | Reduction | Ratio |
1617
|-------|---------------------:|-------------------:|----------:|------:|
17-
| `router route handler` | 137,978 | 886 | **99.4%** | 155.7x |
18-
| `middleware` | 137,978 | 1,008 | **99.3%** | 136.9x |
19-
| `error exception` | 137,978 | 859 | **99.4%** | 160.6x |
20-
| `request response` | 137,978 | 872 | **99.4%** | 158.2x |
21-
| `context bind` | 137,978 | 993 | **99.3%** | 139.0x |
22-
| **Average** ||| **99.4%** | **150.1x** |
18+
| `router route handler` | 143,355 | 1,342 | **99.1%** | 106.8x |
19+
| `middleware` | 143,355 | 1,284 | **99.1%** | 111.6x |
20+
| `error exception` | 143,355 | 1,167 | **99.2%** | 122.8x |
21+
| `request response` | 143,355 | 1,193 | **99.2%** | 120.2x |
22+
| `context bind` | 143,355 | 209 | **99.9%** | 685.9x |
23+
| **Average** ||| **99.3%** | **229.5x** |
2324

2425
<details><summary>Query detail (search + fetch tokens, latency)</summary>
2526

2627
| Query | Search&nbsp;tokens | Fetch&nbsp;tokens | Hits&nbsp;fetched | Search&nbsp;ms |
2728
|-------|-----------------:|------------------:|------------------:|---------------:|
28-
| `router route handler` | 381 | 505 | 3 | 6.2 |
29-
| `middleware` | 370 | 638 | 3 | 0.2 |
30-
| `error exception` | 362 | 497 | 3 | 7.1 |
31-
| `request response` | 372 | 500 | 3 | 0.5 |
32-
| `context bind` | 372 | 621 | 3 | 0.3 |
29+
| `router route handler` | 543 | 799 | 3 | 33.8 |
30+
| `middleware` | 495 | 789 | 3 | 1.2 |
31+
| `error exception` | 493 | 674 | 3 | 17.3 |
32+
| `request response` | 527 | 666 | 3 | 2.3 |
33+
| `context bind` | 209 | 0 | 0 | 5.6 |
3334

3435
</details>
3536

3637
## fastapi/fastapi
3738

3839
| Metric | Value |
3940
|--------|-------|
40-
| Files indexed | **951** |
41-
| Symbols extracted | **5,325** |
42-
| Baseline tokens (all files) | **699,425** |
41+
| Files indexed | **1,000** |
42+
| Symbols extracted | **6,722** |
43+
| Baseline tokens (all files) | **823,784** |
4344

4445
| Query | Baseline&nbsp;tokens | jMunch&nbsp;tokens | Reduction | Ratio |
4546
|-------|---------------------:|-------------------:|----------:|------:|
46-
| `router route handler` | 699,425 | 1,199 | **99.8%** | 583.3x |
47-
| `middleware` | 699,425 | 1,643 | **99.8%** | 425.7x |
48-
| `error exception` | 699,425 | 873 | **99.9%** | 801.2x |
49-
| `request response` | 699,425 | 4,439 | **99.4%** | 157.6x |
50-
| `context bind` | 699,425 | 1,016 | **99.9%** | 688.4x |
51-
| **Average** ||| **99.8%** | **531.2x** |
47+
| `router route handler` | 823,784 | 1,608 | **99.8%** | 512.3x |
48+
| `middleware` | 823,784 | 1,837 | **99.8%** | 448.4x |
49+
| `error exception` | 823,784 | 1,231 | **99.9%** | 669.2x |
50+
| `request response` | 823,784 | 4,689 | **99.4%** | 175.7x |
51+
| `context bind` | 823,784 | 3,103 | **99.6%** | 265.5x |
52+
| **Average** ||| **99.7%** | **414.2x** |
5253

5354
<details><summary>Query detail (search + fetch tokens, latency)</summary>
5455

5556
| Query | Search&nbsp;tokens | Fetch&nbsp;tokens | Hits&nbsp;fetched | Search&nbsp;ms |
5657
|-------|-----------------:|------------------:|------------------:|---------------:|
57-
| `router route handler` | 464 | 735 | 3 | 131.4 |
58-
| `middleware` | 460 | 1,183 | 3 | 0.3 |
59-
| `error exception` | 383 | 490 | 3 | 0.7 |
60-
| `request response` | 430 | 4,009 | 3 | 12.0 |
61-
| `context bind` | 402 | 614 | 3 | 0.4 |
58+
| `router route handler` | 605 | 1,003 | 3 | 776.0 |
59+
| `middleware` | 556 | 1,281 | 3 | 1.5 |
60+
| `error exception` | 514 | 717 | 3 | 1.8 |
61+
| `request response` | 548 | 4,141 | 3 | 16.8 |
62+
| `context bind` | 556 | 2,547 | 3 | 1.9 |
6263

6364
</details>
6465

6566
## gin-gonic/gin
6667

6768
| Metric | Value |
6869
|--------|-------|
69-
| Files indexed | **98** |
70-
| Symbols extracted | **1,489** |
71-
| Baseline tokens (all files) | **187,018** |
70+
| Files indexed | **109** |
71+
| Symbols extracted | **1,502** |
72+
| Baseline tokens (all files) | **192,800** |
7273

7374
| Query | Baseline&nbsp;tokens | jMunch&nbsp;tokens | Reduction | Ratio |
7475
|-------|---------------------:|-------------------:|----------:|------:|
75-
| `router route handler` | 187,018 | 1,151 | **99.4%** | 162.5x |
76-
| `middleware` | 187,018 | 1,130 | **99.4%** | 165.5x |
77-
| `error exception` | 187,018 | 818 | **99.6%** | 228.6x |
78-
| `request response` | 187,018 | 1,083 | **99.4%** | 172.7x |
79-
| `context bind` | 187,018 | 1,436 | **99.2%** | 130.2x |
80-
| **Average** ||| **99.4%** | **171.9x** |
76+
| `router route handler` | 192,800 | 1,568 | **99.2%** | 123.0x |
77+
| `middleware` | 192,800 | 1,726 | **99.1%** | 111.7x |
78+
| `error exception` | 192,800 | 1,099 | **99.4%** | 175.4x |
79+
| `request response` | 192,800 | 1,530 | **99.2%** | 126.0x |
80+
| `context bind` | 192,800 | 1,634 | **99.2%** | 118.0x |
81+
| **Average** ||| **99.2%** | **130.8x** |
8182

8283
<details><summary>Query detail (search + fetch tokens, latency)</summary>
8384

8485
| Query | Search&nbsp;tokens | Fetch&nbsp;tokens | Hits&nbsp;fetched | Search&nbsp;ms |
8586
|-------|-----------------:|------------------:|------------------:|---------------:|
86-
| `router route handler` | 440 | 711 | 3 | 43.5 |
87-
| `middleware` | 371 | 759 | 3 | 5.7 |
88-
| `error exception` | 345 | 473 | 3 | 0.8 |
89-
| `request response` | 387 | 696 | 3 | 0.8 |
90-
| `context bind` | 391 | 1,045 | 3 | 7.9 |
87+
| `router route handler` | 522 | 1,046 | 3 | 155.2 |
88+
| `middleware` | 441 | 1,285 | 3 | 15.4 |
89+
| `error exception` | 464 | 635 | 3 | 1.8 |
90+
| `request response` | 671 | 859 | 3 | 1.7 |
91+
| `context bind` | 490 | 1,144 | 3 | 18.1 |
9192

9293
</details>
9394

94-
---
95-
9695
## Real-world A/B test: naming audit task (2026-03-18)
9796

9897
50-iteration test by @Mharbulous comparing JCodeMunch vs native tools (Grep/Glob/Read) on a real Vue 3 + Firebase production codebase. Full report: [ab-test-naming-audit-2026-03-18.md](ab-test-naming-audit-2026-03-18.md)
@@ -132,9 +131,9 @@ Raw data: https://gist.github.com/Mharbulous/bb097396fa92ef1d34d03a72b56b2c61
132131

133132
| | Tokens |
134133
|--|-------:|
135-
| Baseline total (15 task-runs) | 5,122,105 |
136-
| jMunch total | 19,406 |
134+
| Baseline total (15 task-runs) | 5,799,695 |
135+
| jMunch total | 25,220 |
137136
| **Reduction** | **99.6%** |
138-
| **Ratio** | **263.9x** |
137+
| **Ratio** | **230.0x** |
139138

140-
> Measured with tiktoken `cl100k_base`. Baseline = all indexed source files. jMunch = search_symbols (top 5) + get_symbol_source x 3 per query.
139+
> Measured with tiktoken `cl100k_base`. Baseline = all indexed source files. jMunch = search_symbols (top 5) + get_symbol x 3 per query.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"version": "1.108.163",
3+
"captured_at": "2026-07-23T18:57:47+00:00",
4+
"session": {
5+
"session_calls": 16,
6+
"session_tokens_saved": 380286,
7+
"session_duration_s": 1.8
8+
},
9+
"tools": {
10+
"search_symbols": {
11+
"tokens_saved": 132876
12+
},
13+
"get_symbol_source": {
14+
"tokens_saved": 244668
15+
},
16+
"get_file_outline": {
17+
"tokens_saved": 2742
18+
}
19+
}
20+
}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "jcodemunch-mcp"
3-
version = "1.108.162"
3+
version = "1.108.163"
44
description = "Token-efficient MCP server for source code exploration via tree-sitter AST parsing"
55
readme = "README.md"
66
requires-python = ">=3.10"

src/jcodemunch_mcp/cli/install_pack.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,22 @@ def resolve_effective_license_key(explicit: Optional[str]) -> Optional[str]:
5959
return None
6060

6161

62+
def _looks_like_missing_license_response(resp) -> bool:
63+
"""True only for the exact answer a pre-header backend gives a licensed-pack
64+
request whose key rode the X-JCM-License header it doesn't read: the
65+
no-license 403. A real key rejection says "Invalid or expired license."
66+
and must NOT retry over the legacy query string."""
67+
if "application/json" not in resp.headers.get("content-type", ""):
68+
return False
69+
try:
70+
data = resp.json()
71+
except Exception:
72+
return False
73+
if not isinstance(data, dict):
74+
return False
75+
return "requires a jcodemunch license" in (data.get("error") or "").lower()
76+
77+
6278
def _mask_license(key: str) -> str:
6379
"""Mask a license key for display: first 4 + last 4 chars."""
6480
if len(key) <= 8:
@@ -136,22 +152,40 @@ def _install_pack(
136152
print(f" Pack '{pack_id}' is already installed. Use --force to re-download.")
137153
return 0
138154

139-
# Build download URL
155+
# Build download URL. The license key travels in the X-JCM-License header,
156+
# NEVER the URL query string, so it can't land in server/proxy access logs
157+
# (audit finding V12).
140158
url = f"{STARTER_PACK_API}?action=download&pack={pack_id}"
141-
if license_key:
142-
url += f"&license={license_key}"
159+
headers = {"X-JCM-License": license_key} if license_key else None
143160

144161
print(f" Downloading starter pack '{pack_id}'...", flush=True)
162+
# No raise_for_status: the API reports license/pack errors as 4xx + JSON
163+
# (handled below); raising on them rendered every one as a bogus
164+
# "could not reach the server". Only transport failures are unreachable.
145165
try:
146-
resp = httpx.get(url, timeout=120, follow_redirects=True)
147-
resp.raise_for_status()
166+
resp = httpx.get(url, timeout=120, follow_redirects=True, headers=headers)
148167
except httpx.HTTPError:
149168
print(
150169
f" {_RED}{_CROSS} Could not reach the starter packs server. "
151170
f"Check your network connection.{_RESET}",
152171
)
153172
return 1
154173

174+
# Transitional: a backend that predates header support never saw the key
175+
# and answers a licensed pack with its no-license 403. Retry ONCE over the
176+
# legacy query-string transport so a paying customer isn't punished by a
177+
# deploy-order gap. Remove once the deployed API is confirmed header-aware.
178+
if license_key and _looks_like_missing_license_response(resp):
179+
legacy_url = url + f"&license={license_key}"
180+
try:
181+
resp = httpx.get(legacy_url, timeout=120, follow_redirects=True)
182+
except httpx.HTTPError:
183+
print(
184+
f" {_RED}{_CROSS} Could not reach the starter packs server. "
185+
f"Check your network connection.{_RESET}",
186+
)
187+
return 1
188+
155189
content_type = resp.headers.get("content-type", "")
156190

157191
# Error response (JSON)

0 commit comments

Comments
 (0)