From 2860dacd45052d7761ab6477c4960cc1853afa9e Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Tue, 5 May 2026 22:09:22 +0200 Subject: [PATCH 1/3] feat(status): show tier + expiry date in recover.sh status output Signed-off-by: Saurabh Jain --- CHANGELOG.md | 1 + runtime-e2e/v1-paid-tier/test.sh | 54 ++++++++++++++++++ scripts/recover.sh | 95 +++++++++++++++++++++++++++++++- skills/pro-tier-status/SKILL.md | 27 +++++++-- 4 files changed, 168 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 857fc02..785b1cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Changed +- **`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. - **`scripts/recover.sh status` now surfaces tenant_id + upgrade URL.** Free-tier users need to find their `tenant_id` (`cs_`) 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). ### Added diff --git a/runtime-e2e/v1-paid-tier/test.sh b/runtime-e2e/v1-paid-tier/test.sh index e587123..776df64 100755 --- a/runtime-e2e/v1-paid-tier/test.sh +++ b/runtime-e2e/v1-paid-tier/test.sh @@ -299,6 +299,60 @@ else fi rm -rf "$TMP_REG_DIR" +# 6f: V1 SaaS Plugin Pro tier-line surface parity. The status `tier` line +# must surface the JWT `exp` claim from the configured license token in +# three shapes (matching cursor / claude / openclaw): +# - "Pro tier active (expires YYYY-MM-DD, N days remaining)" (exp future) +# - "Free tier (Pro expired YYYY-MM-DD — visit ... to renew)" (exp past) +# - "Free tier (no AXON- license token configured)" (no token) +mint_axon_jwt() { + local exp_epoch="$1" + local hdr + hdr=$(printf '%s' '{"alg":"EdDSA","typ":"JWT"}' | base64 | tr '+/' '-_' | tr -d '=') + local payload + payload=$(printf '{"sub":"smoke","exp":%s}' "$exp_epoch" | base64 | tr '+/' '-_' | tr -d '=') + local sig="placeholder-signature-padding-padding-padding-padding-padding-pa" + printf 'AXON-%s.%s.%s' "$hdr" "$payload" "$sig" +} + +# Pro-active path: exp ~30d future. Expect explicit YYYY-MM-DD + days remaining. +PRO_EXP=$(( $(date -u +%s) + 30 * 86400 )) +PRO_TOKEN=$(mint_axon_jwt "$PRO_EXP") +PRO_STATUS=$(HOME="$TMP_HOME3" AXONFLOW_LICENSE_TOKEN="$PRO_TOKEN" bash "$RECOVER" status 2>&1 || true) +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 + PASS "status Pro-active tier line shape (expires YYYY-MM-DD, N days remaining)" +else + FAIL "status Pro-active line missing expected shape: $PRO_STATUS" +fi +if echo "$PRO_STATUS" | grep -qF "$PRO_TOKEN"; then + FAIL "status leaked Pro-active token to stdout" +else + PASS "status redacts Pro-active token" +fi + +# Pro-expired path: exp ~365d past. Expect "Free tier (Pro expired YYYY-MM-DD — visit ... to renew)". +EXPIRED_EXP=$(( $(date -u +%s) - 365 * 86400 )) +EXPIRED_TOKEN=$(mint_axon_jwt "$EXPIRED_EXP") +EXPIRED_STATUS=$(HOME="$TMP_HOME3" AXONFLOW_LICENSE_TOKEN="$EXPIRED_TOKEN" bash "$RECOVER" status 2>&1 || true) +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 + PASS "status Pro-expired tier line shape (Pro expired YYYY-MM-DD — visit ... to renew)" +else + FAIL "status Pro-expired line missing expected shape: $EXPIRED_STATUS" +fi +if echo "$EXPIRED_STATUS" | grep -qF "$EXPIRED_TOKEN"; then + FAIL "status leaked expired token to stdout" +else + PASS "status redacts expired token" +fi + +# Free path: no token at all. Expect "Free tier (no AXON- license token configured)". +FREE_STATUS=$(HOME="$TMP_HOME3" bash "$RECOVER" status 2>&1 || true) +if echo "$FREE_STATUS" | grep -qE "tier[[:space:]]+Free tier \(no AXON- license token configured\)"; then + PASS "status Free-tier line shape (no AXON- license token configured)" +else + FAIL "status Free-tier line missing expected shape: $FREE_STATUS" +fi + # ----------------------------------------------------------------------------- # Test 7: apply-token persists into TOML. # ----------------------------------------------------------------------------- diff --git a/scripts/recover.sh b/scripts/recover.sh index 22a454d..dbcc15a 100755 --- a/scripts/recover.sh +++ b/scripts/recover.sh @@ -257,6 +257,48 @@ cmd_apply_token() { printf 'Pro-tier license token persisted. Restart Codex to pick it up.\n' >&2 } +# extract_jwt_exp → prints unix-epoch integer to stdout, exits 0 +# on success, non-zero on any parse failure. Pure stdout/stderr; never +# raises. The caller decides how to render a parse failure. +# +# AxonFlow license tokens are formatted `AXON-` where is a +# standard `header.payload.signature` triple. We base64url-decode the +# middle segment, then look for `"exp":`. Signature is NEVER +# validated here — display only. +extract_jwt_exp() { + local tok="$1" + [ -n "$tok" ] || return 1 + local jwt="${tok#AXON-}" + local payload + payload=$(printf '%s' "$jwt" | cut -d. -f2) + [ -n "$payload" ] || return 1 + payload=$(printf '%s' "$payload" | tr '_-' '/+') + local pad=$(( 4 - ${#payload} % 4 )) + if [ "$pad" -ne 4 ]; then + payload="${payload}$(printf '=%.0s' $(seq 1 "$pad"))" + fi + local decoded + decoded=$(printf '%s' "$payload" | base64 -d 2>/dev/null) \ + || decoded=$(printf '%s' "$payload" | base64 -D 2>/dev/null) \ + || return 1 + [ -n "$decoded" ] || return 1 + local exp + exp=$(printf '%s' "$decoded" | grep -oE '"exp"[[:space:]]*:[[:space:]]*[0-9]+' | head -1 | grep -oE '[0-9]+$') + [ -n "$exp" ] || return 1 + printf '%s' "$exp" +} + +# format_unix_to_date → prints YYYY-MM-DD (UTC) to stdout. +format_unix_to_date() { + local epoch="$1" + [ -n "$epoch" ] || return 1 + local out + out=$(date -u -d "@${epoch}" +%Y-%m-%d 2>/dev/null) \ + || out=$(date -u -r "${epoch}" +%Y-%m-%d 2>/dev/null) \ + || return 1 + printf '%s' "$out" +} + cmd_status() { axonflow_resolve_license_token local file="${AXONFLOW_CODEX_CONFIG:-$HOME/.codex/axonflow.toml}" @@ -278,9 +320,21 @@ cmd_status() { fi fi + local upgrade_url="${AXONFLOW_UPGRADE_URL:-https://getaxonflow.com/pro}" + + # Tier-line resolution. Three shapes, matching cursor / claude / openclaw + # (V1 SaaS Plugin Pro tier-line surface parity): + # - "Pro tier active (expires YYYY-MM-DD, N days remaining)" exp future + # - "Pro tier active (expires UNKNOWN — could not parse token)" parse fail + # - "Free tier (Pro expired YYYY-MM-DD — visit to renew)" exp past + # - "Free tier (no AXON- license token configured)" no token + # + # The leading "Pro tier active" / "Free tier" preserves the existing + # contract (runtime-e2e/v1-paid-tier/test.sh greps for "Pro tier active" + # and "Free tier") so we extend without breaking the older assertions. local tier token_display + local pro_expired_flag=0 if [ -n "${AXONFLOW_LICENSE_TOKEN_RESOLVED:-}" ]; then - tier="Pro tier active" # Never print the full token to a terminal — it's a bearer credential and # `recover.sh status` may be screen-shared, copy-pasted into a support # ticket, or logged. Show a fixed prefix + last 4 chars only, padding @@ -290,13 +344,34 @@ cmd_status() { tail4="${AXONFLOW_LICENSE_TOKEN_RESOLVED: -4}" fi token_display="set (AXON-...${tail4})" + + local exp_epoch + exp_epoch=$(extract_jwt_exp "$AXONFLOW_LICENSE_TOKEN_RESOLVED" 2>/dev/null || true) + if [ -n "$exp_epoch" ]; then + local exp_date + exp_date=$(format_unix_to_date "$exp_epoch" 2>/dev/null || true) + if [ -n "$exp_date" ]; then + local now_epoch + now_epoch=$(date -u +%s) + if [ "$exp_epoch" -gt "$now_epoch" ]; then + local secs_left=$(( exp_epoch - now_epoch )) + local days_left=$(( (secs_left + 86399) / 86400 )) + tier="Pro tier active (expires ${exp_date}, ${days_left} days remaining)" + else + tier="Free tier (Pro expired ${exp_date} — visit ${upgrade_url} to renew)" + pro_expired_flag=1 + fi + else + tier="Pro tier active (expires UNKNOWN — could not parse token)" + fi + else + tier="Pro tier active (expires UNKNOWN — could not parse token)" + fi else tier="Free tier (no AXON- license token configured)" token_display="unset" fi - local upgrade_url="${AXONFLOW_UPGRADE_URL:-https://getaxonflow.com/pro}" - cat <` on every governed request, and the agent's `PluginClaimMiddleware` validates the Ed25519 signature + DB row, then stamps a Pro-tier context on the request. +- **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. +- **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: ` on every governed request, and the agent's `PluginClaimMiddleware` validates the Ed25519 signature + DB row, then stamps a Pro-tier context on the request. Invoke the status surface via `exec_command`: @@ -14,13 +14,28 @@ Invoke the status surface via `exec_command`: bash $PLUGIN_DIR/scripts/recover.sh status ``` -The output reports: +## Tier line shape + +The script's `tier` line takes one of three shapes — surface whichever one the user got: + +- `tier Pro tier active (expires 2026-08-03, 90 days remaining)` — paid Pro tier active. +- `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. +- `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=` or `scripts/recover.sh apply-token`. +- `tier Free tier (no AXON- license token configured)` — no token loaded. + +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. + +## Other lines the script reports - the active endpoint (`AXONFLOW_ENDPOINT` or the community-saas default) - whether `~/.codex/axonflow.toml` exists -- whether a license token is currently resolvable -- the tier (`Pro tier active` or `Free tier (no AXON- license token configured)`) +- the user's `tenant_id` (read from `~/.config/axonflow/try-registration.json`) — needed to paste into the Stripe checkout custom field at /pro +- a redacted preview of the configured license token (`set (AXON-...XXXX)` — last 4 chars only, never the full bearer credential) + +## Renewal + upgrade path 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. 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. + +The script extracts the JWT `exp` claim for display only; signature validation is the platform's job. From 90d28e52618b6d9d254410a86e390f5ffb8d4926 Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Tue, 5 May 2026 22:16:35 +0200 Subject: [PATCH 2/3] fix(test-hooks): drop sister-plugin reference from comment for static lint Signed-off-by: Saurabh Jain --- scripts/recover.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/recover.sh b/scripts/recover.sh index dbcc15a..381b142 100755 --- a/scripts/recover.sh +++ b/scripts/recover.sh @@ -322,8 +322,8 @@ cmd_status() { local upgrade_url="${AXONFLOW_UPGRADE_URL:-https://getaxonflow.com/pro}" - # Tier-line resolution. Three shapes, matching cursor / claude / openclaw - # (V1 SaaS Plugin Pro tier-line surface parity): + # Tier-line resolution. Three shapes (V1 SaaS Plugin Pro tier-line + # surface parity across the AxonFlow plugin set): # - "Pro tier active (expires YYYY-MM-DD, N days remaining)" exp future # - "Pro tier active (expires UNKNOWN — could not parse token)" parse fail # - "Free tier (Pro expired YYYY-MM-DD — visit to renew)" exp past From fed07929b5d3c3f21e3cd0e96dc0509d39b1b96b Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Tue, 5 May 2026 22:17:08 +0200 Subject: [PATCH 3/3] test: drop sister-plugin reference from runtime-e2e comment Signed-off-by: Saurabh Jain --- runtime-e2e/v1-paid-tier/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime-e2e/v1-paid-tier/test.sh b/runtime-e2e/v1-paid-tier/test.sh index 776df64..a58545a 100755 --- a/runtime-e2e/v1-paid-tier/test.sh +++ b/runtime-e2e/v1-paid-tier/test.sh @@ -301,7 +301,7 @@ rm -rf "$TMP_REG_DIR" # 6f: V1 SaaS Plugin Pro tier-line surface parity. The status `tier` line # must surface the JWT `exp` claim from the configured license token in -# three shapes (matching cursor / claude / openclaw): +# three shapes: # - "Pro tier active (expires YYYY-MM-DD, N days remaining)" (exp future) # - "Free tier (Pro expired YYYY-MM-DD — visit ... to renew)" (exp past) # - "Free tier (no AXON- license token configured)" (no token)