Skip to content

Commit bfcb619

Browse files
feat(status): show tier + expiry date in recover.sh status output (#50)
* feat(status): show tier + expiry date in recover.sh status output Signed-off-by: Saurabh Jain <saurabhjain1592@gmail.com> * fix(test-hooks): drop sister-plugin reference from comment for static lint Signed-off-by: Saurabh Jain <saurabhjain1592@gmail.com> * test: drop sister-plugin reference from runtime-e2e comment Signed-off-by: Saurabh Jain <saurabhjain1592@gmail.com> --------- Signed-off-by: Saurabh Jain <saurabhjain1592@gmail.com>
1 parent 3ecd76c commit bfcb619

4 files changed

Lines changed: 168 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Changed
66

7+
- **`scripts/recover.sh status` tier line now surfaces Pro license expiry date.** The status output's `tier` line parses the JWT `exp` claim from the configured Pro license token and renders one of three shapes: `Pro tier active (expires YYYY-MM-DD, N days remaining)` when active, `Free tier (Pro expired YYYY-MM-DD — visit https://getaxonflow.com/pro to renew)` when the token is on disk but its `exp` has passed (plugin will not forward an expired token), or `Free tier (no AXON- license token configured)` when no token is loaded. Lets users see their renewal date without hitting the agent and catches the lapsed-token state before their next governed call. Display only — JWT signature validation remains the platform's job. Pre-existing `Pro tier active` and `Free tier` substring assertions still hold.
78
- **`scripts/recover.sh status` now surfaces tenant_id + upgrade URL.** Free-tier users need to find their `tenant_id` (`cs_<uuid>`) to paste into the Stripe Checkout custom field at `getaxonflow.com/pro`. The status output now reads `~/.config/axonflow/try-registration.json` (the auto-bootstrap registration file) and prints the tenant_id alongside endpoint + license-token state. Adds an `upgrade` line (default `https://getaxonflow.com/pro`, override via `AXONFLOW_UPGRADE_URL`) and copy-paste-ready upgrade instructions. Token still redacted to last 4 chars (no full bearer credential in stdout — see PR #41).
89

910
### Added

runtime-e2e/v1-paid-tier/test.sh

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,60 @@ else
299299
fi
300300
rm -rf "$TMP_REG_DIR"
301301

302+
# 6f: V1 SaaS Plugin Pro tier-line surface parity. The status `tier` line
303+
# must surface the JWT `exp` claim from the configured license token in
304+
# three shapes:
305+
# - "Pro tier active (expires YYYY-MM-DD, N days remaining)" (exp future)
306+
# - "Free tier (Pro expired YYYY-MM-DD — visit ... to renew)" (exp past)
307+
# - "Free tier (no AXON- license token configured)" (no token)
308+
mint_axon_jwt() {
309+
local exp_epoch="$1"
310+
local hdr
311+
hdr=$(printf '%s' '{"alg":"EdDSA","typ":"JWT"}' | base64 | tr '+/' '-_' | tr -d '=')
312+
local payload
313+
payload=$(printf '{"sub":"smoke","exp":%s}' "$exp_epoch" | base64 | tr '+/' '-_' | tr -d '=')
314+
local sig="placeholder-signature-padding-padding-padding-padding-padding-pa"
315+
printf 'AXON-%s.%s.%s' "$hdr" "$payload" "$sig"
316+
}
317+
318+
# Pro-active path: exp ~30d future. Expect explicit YYYY-MM-DD + days remaining.
319+
PRO_EXP=$(( $(date -u +%s) + 30 * 86400 ))
320+
PRO_TOKEN=$(mint_axon_jwt "$PRO_EXP")
321+
PRO_STATUS=$(HOME="$TMP_HOME3" AXONFLOW_LICENSE_TOKEN="$PRO_TOKEN" bash "$RECOVER" status 2>&1 || true)
322+
if echo "$PRO_STATUS" | grep -qE "tier[[:space:]]+Pro tier active \(expires [0-9]{4}-[0-9]{2}-[0-9]{2}, [0-9]+ days remaining\)"; then
323+
PASS "status Pro-active tier line shape (expires YYYY-MM-DD, N days remaining)"
324+
else
325+
FAIL "status Pro-active line missing expected shape: $PRO_STATUS"
326+
fi
327+
if echo "$PRO_STATUS" | grep -qF "$PRO_TOKEN"; then
328+
FAIL "status leaked Pro-active token to stdout"
329+
else
330+
PASS "status redacts Pro-active token"
331+
fi
332+
333+
# Pro-expired path: exp ~365d past. Expect "Free tier (Pro expired YYYY-MM-DD — visit ... to renew)".
334+
EXPIRED_EXP=$(( $(date -u +%s) - 365 * 86400 ))
335+
EXPIRED_TOKEN=$(mint_axon_jwt "$EXPIRED_EXP")
336+
EXPIRED_STATUS=$(HOME="$TMP_HOME3" AXONFLOW_LICENSE_TOKEN="$EXPIRED_TOKEN" bash "$RECOVER" status 2>&1 || true)
337+
if echo "$EXPIRED_STATUS" | grep -qE "tier[[:space:]]+Free tier \(Pro expired [0-9]{4}-[0-9]{2}-[0-9]{2} — visit https?://[^ ]+ to renew\)"; then
338+
PASS "status Pro-expired tier line shape (Pro expired YYYY-MM-DD — visit ... to renew)"
339+
else
340+
FAIL "status Pro-expired line missing expected shape: $EXPIRED_STATUS"
341+
fi
342+
if echo "$EXPIRED_STATUS" | grep -qF "$EXPIRED_TOKEN"; then
343+
FAIL "status leaked expired token to stdout"
344+
else
345+
PASS "status redacts expired token"
346+
fi
347+
348+
# Free path: no token at all. Expect "Free tier (no AXON- license token configured)".
349+
FREE_STATUS=$(HOME="$TMP_HOME3" bash "$RECOVER" status 2>&1 || true)
350+
if echo "$FREE_STATUS" | grep -qE "tier[[:space:]]+Free tier \(no AXON- license token configured\)"; then
351+
PASS "status Free-tier line shape (no AXON- license token configured)"
352+
else
353+
FAIL "status Free-tier line missing expected shape: $FREE_STATUS"
354+
fi
355+
302356
# -----------------------------------------------------------------------------
303357
# Test 7: apply-token persists into TOML.
304358
# -----------------------------------------------------------------------------

scripts/recover.sh

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,48 @@ cmd_apply_token() {
257257
printf 'Pro-tier license token persisted. Restart Codex to pick it up.\n' >&2
258258
}
259259

260+
# extract_jwt_exp <token> → prints unix-epoch integer to stdout, exits 0
261+
# on success, non-zero on any parse failure. Pure stdout/stderr; never
262+
# raises. The caller decides how to render a parse failure.
263+
#
264+
# AxonFlow license tokens are formatted `AXON-<JWT>` where <JWT> is a
265+
# standard `header.payload.signature` triple. We base64url-decode the
266+
# middle segment, then look for `"exp":<digits>`. Signature is NEVER
267+
# validated here — display only.
268+
extract_jwt_exp() {
269+
local tok="$1"
270+
[ -n "$tok" ] || return 1
271+
local jwt="${tok#AXON-}"
272+
local payload
273+
payload=$(printf '%s' "$jwt" | cut -d. -f2)
274+
[ -n "$payload" ] || return 1
275+
payload=$(printf '%s' "$payload" | tr '_-' '/+')
276+
local pad=$(( 4 - ${#payload} % 4 ))
277+
if [ "$pad" -ne 4 ]; then
278+
payload="${payload}$(printf '=%.0s' $(seq 1 "$pad"))"
279+
fi
280+
local decoded
281+
decoded=$(printf '%s' "$payload" | base64 -d 2>/dev/null) \
282+
|| decoded=$(printf '%s' "$payload" | base64 -D 2>/dev/null) \
283+
|| return 1
284+
[ -n "$decoded" ] || return 1
285+
local exp
286+
exp=$(printf '%s' "$decoded" | grep -oE '"exp"[[:space:]]*:[[:space:]]*[0-9]+' | head -1 | grep -oE '[0-9]+$')
287+
[ -n "$exp" ] || return 1
288+
printf '%s' "$exp"
289+
}
290+
291+
# format_unix_to_date <unix-epoch> → prints YYYY-MM-DD (UTC) to stdout.
292+
format_unix_to_date() {
293+
local epoch="$1"
294+
[ -n "$epoch" ] || return 1
295+
local out
296+
out=$(date -u -d "@${epoch}" +%Y-%m-%d 2>/dev/null) \
297+
|| out=$(date -u -r "${epoch}" +%Y-%m-%d 2>/dev/null) \
298+
|| return 1
299+
printf '%s' "$out"
300+
}
301+
260302
cmd_status() {
261303
axonflow_resolve_license_token
262304
local file="${AXONFLOW_CODEX_CONFIG:-$HOME/.codex/axonflow.toml}"
@@ -278,9 +320,21 @@ cmd_status() {
278320
fi
279321
fi
280322

323+
local upgrade_url="${AXONFLOW_UPGRADE_URL:-https://getaxonflow.com/pro}"
324+
325+
# Tier-line resolution. Three shapes (V1 SaaS Plugin Pro tier-line
326+
# surface parity across the AxonFlow plugin set):
327+
# - "Pro tier active (expires YYYY-MM-DD, N days remaining)" exp future
328+
# - "Pro tier active (expires UNKNOWN — could not parse token)" parse fail
329+
# - "Free tier (Pro expired YYYY-MM-DD — visit <url> to renew)" exp past
330+
# - "Free tier (no AXON- license token configured)" no token
331+
#
332+
# The leading "Pro tier active" / "Free tier" preserves the existing
333+
# contract (runtime-e2e/v1-paid-tier/test.sh greps for "Pro tier active"
334+
# and "Free tier") so we extend without breaking the older assertions.
281335
local tier token_display
336+
local pro_expired_flag=0
282337
if [ -n "${AXONFLOW_LICENSE_TOKEN_RESOLVED:-}" ]; then
283-
tier="Pro tier active"
284338
# Never print the full token to a terminal — it's a bearer credential and
285339
# `recover.sh status` may be screen-shared, copy-pasted into a support
286340
# ticket, or logged. Show a fixed prefix + last 4 chars only, padding
@@ -290,13 +344,34 @@ cmd_status() {
290344
tail4="${AXONFLOW_LICENSE_TOKEN_RESOLVED: -4}"
291345
fi
292346
token_display="set (AXON-...${tail4})"
347+
348+
local exp_epoch
349+
exp_epoch=$(extract_jwt_exp "$AXONFLOW_LICENSE_TOKEN_RESOLVED" 2>/dev/null || true)
350+
if [ -n "$exp_epoch" ]; then
351+
local exp_date
352+
exp_date=$(format_unix_to_date "$exp_epoch" 2>/dev/null || true)
353+
if [ -n "$exp_date" ]; then
354+
local now_epoch
355+
now_epoch=$(date -u +%s)
356+
if [ "$exp_epoch" -gt "$now_epoch" ]; then
357+
local secs_left=$(( exp_epoch - now_epoch ))
358+
local days_left=$(( (secs_left + 86399) / 86400 ))
359+
tier="Pro tier active (expires ${exp_date}, ${days_left} days remaining)"
360+
else
361+
tier="Free tier (Pro expired ${exp_date} — visit ${upgrade_url} to renew)"
362+
pro_expired_flag=1
363+
fi
364+
else
365+
tier="Pro tier active (expires UNKNOWN — could not parse token)"
366+
fi
367+
else
368+
tier="Pro tier active (expires UNKNOWN — could not parse token)"
369+
fi
293370
else
294371
tier="Free tier (no AXON- license token configured)"
295372
token_display="unset"
296373
fi
297374

298-
local upgrade_url="${AXONFLOW_UPGRADE_URL:-https://getaxonflow.com/pro}"
299-
300375
cat <<EOF
301376
AxonFlow Codex plugin — status
302377
@@ -307,6 +382,20 @@ AxonFlow Codex plugin — status
307382
tier $tier
308383
upgrade $upgrade_url
309384
385+
EOF
386+
387+
if [ "$pro_expired_flag" -eq 1 ]; then
388+
cat <<EOF
389+
Your Pro license token is on disk but its 'exp' has passed; the plugin will
390+
not forward an expired token. After buying a renewal, replace the token via:
391+
AXONFLOW_LICENSE_TOKEN=AXON-... codex …
392+
or persist with:
393+
scripts/recover.sh apply-token
394+
395+
EOF
396+
fi
397+
398+
cat <<EOF
310399
To upgrade to Pro (\$9.99 one-time), copy your tenant_id above, then visit
311400
$upgrade_url, paste the tenant_id into the "Your AxonFlow tenant ID" field,
312401
and complete checkout. The license token arrives by email; set it via:

skills/pro-tier-status/SKILL.md

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,41 @@
11
---
22
name: pro-tier-status
3-
description: Report the user's current AxonFlow tier (free or Pro), endpoint, and whether a license token is configured. Use when the user asks "am I on Pro?", "what tier am I on?", "is my license active?", or wants to know which AxonFlow they're talking to.
3+
description: Report the user's current AxonFlow tier (Free or Pro), Pro license expiry date, endpoint, and whether a license token is configured. Use when the user asks "am I on Pro?", "what tier am I on?", "when does my Pro license expire?", "is my license active?", or wants to know which AxonFlow they're talking to.
44
---
55

66
The Codex plugin runs in one of two tiers:
77

8-
- **Free.** No `AXONFLOW_LICENSE_TOKEN` env var and no `license_token = "..."` line in `~/.codex/axonflow.toml`. The plugin omits the `X-License-Token` HTTP header on every governed request, and the agent applies free-tier quota / retention defaults.
9-
- **Pro tier active.** Either `AXONFLOW_LICENSE_TOKEN` is exported in the Codex environment (operator override; CI use) or `~/.codex/axonflow.toml` contains a `license_token = "AXON-..."` line. The plugin sends `X-License-Token: <token>` on every governed request, and the agent's `PluginClaimMiddleware` validates the Ed25519 signature + DB row, then stamps a Pro-tier context on the request.
8+
- **Free.** No `AXONFLOW_LICENSE_TOKEN` env var and no `license_token = "..."` line in `~/.codex/axonflow.toml` (or the line is there but its JWT `exp` is in the past — the plugin will not forward an expired token). The plugin omits the `X-License-Token` HTTP header on every governed request, and the agent applies free-tier quota / retention defaults.
9+
- **Pro tier active.** Either `AXONFLOW_LICENSE_TOKEN` is exported in the Codex environment (operator override; CI use) or `~/.codex/axonflow.toml` contains a `license_token = "AXON-..."` line whose JWT `exp` is in the future. The plugin sends `X-License-Token: <token>` on every governed request, and the agent's `PluginClaimMiddleware` validates the Ed25519 signature + DB row, then stamps a Pro-tier context on the request.
1010

1111
Invoke the status surface via `exec_command`:
1212

1313
```bash
1414
bash $PLUGIN_DIR/scripts/recover.sh status
1515
```
1616

17-
The output reports:
17+
## Tier line shape
18+
19+
The script's `tier` line takes one of three shapes — surface whichever one the user got:
20+
21+
- `tier Pro tier active (expires 2026-08-03, 90 days remaining)` — paid Pro tier active.
22+
- `tier Pro tier active (expires UNKNOWN — could not parse token)` — token configured but the JWT body did not parse. Treat as Pro for display; the platform is the source of truth on validity.
23+
- `tier Free tier (Pro expired 2026-02-04 — visit https://getaxonflow.com/pro to renew)` — token is on disk but its `exp` has passed. The plugin will not forward an expired token; the user must buy a renewal and replace the token via `AXONFLOW_LICENSE_TOKEN=<new>` or `scripts/recover.sh apply-token`.
24+
- `tier Free tier (no AXON- license token configured)` — no token loaded.
25+
26+
When the user lands on `Free tier (Pro expired …)`, point them at the renew URL embedded in the line and the `scripts/recover.sh apply-token` hint the script prints below.
27+
28+
## Other lines the script reports
1829

1930
- the active endpoint (`AXONFLOW_ENDPOINT` or the community-saas default)
2031
- whether `~/.codex/axonflow.toml` exists
21-
- whether a license token is currently resolvable
22-
- the tier (`Pro tier active` or `Free tier (no AXON- license token configured)`)
32+
- the user's `tenant_id` (read from `~/.config/axonflow/try-registration.json`) — needed to paste into the Stripe checkout custom field at /pro
33+
- a redacted preview of the configured license token (`set (AXON-...XXXX)` — last 4 chars only, never the full bearer credential)
34+
35+
## Renewal + upgrade path
2336

2437
If the user is on Free and asks about upgrading, tell them: a Pro license token arrives by email after Stripe Checkout completes, and they install it with `scripts/recover.sh apply-token` (or by setting `AXONFLOW_LICENSE_TOKEN`). Don't paste the token into chat — the script reads from stdin or env.
2538

2639
For richer governance activity (policy hits, override usage, audit volume), point the user to the `governance-status` skill, which calls the platform's `get_policy_stats` MCP tool.
40+
41+
The script extracts the JWT `exp` claim for display only; signature validation is the platform's job.

0 commit comments

Comments
 (0)