diff --git a/CHANGELOG.md b/CHANGELOG.md index 59c670f..d9e10dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ ### Added +- **`scripts/status.sh` 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 + (expires YYYY-MM-DD, N days remaining)` when active, `Free (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 (no Pro license 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. - **Status surface (`scripts/status.sh` + `/axonflow-status` skill).** Prints the `tenant_id` (which Pro buyers paste into the custom field at Stripe Checkout), the active tier (`Free` or `Pro`), the agent endpoint, the diff --git a/runtime-e2e/tier-expiry/README.md b/runtime-e2e/tier-expiry/README.md new file mode 100644 index 0000000..33bbb31 --- /dev/null +++ b/runtime-e2e/tier-expiry/README.md @@ -0,0 +1,50 @@ +# runtime-e2e/tier-expiry — V1 SaaS Plugin Pro tier-line expiry surface + +## What it asserts + +The user-facing `scripts/status.sh` (the same script the +`/axonflow-status` skill guides the agent to run from the integrated +terminal) renders one of three tier-line shapes depending on the +configured Pro license token's JWT `exp` claim: + +| State | Shape | +|----------------|-----------------------------------------------------------------------------------| +| Free | `tier Free (no Pro license configured)` | +| Pro active | `tier Pro (expires YYYY-MM-DD, N days remaining)` | +| Pro expired | `tier Free (Pro expired YYYY-MM-DD — visit https://getaxonflow.com/pro to renew)` | + +Plus security guarantees: +- Full bearer token is NEVER printed; only the last 4 chars in the + `AXON-...XXXX` redaction. +- Pro-expired output surfaces the renewal hint + (`After buying a renewal, replace the token: ...`). + +## How to run + +```bash +bash runtime-e2e/tier-expiry/test.sh +``` + +No env vars required. Mints three structurally-valid AXON- JWT tokens +on the fly with explicit `exp` claims and runs the actual +`scripts/status.sh` against them in an isolated `$HOME` so the +developer's real `~/.config/axonflow` is never touched. + +## Why this is real-surface runtime proof (HARD RULE #0) + +- The script under test IS the script users invoke via the integrated + terminal — same file, same path, same env-var resolution order. +- The license tokens are real AXON-prefixed JWTs (header.payload.sig + base64url-encoded). The JWT-parsing branch in `status.sh` does NOT + distinguish a platform-minted token from a test-minted one + structurally — both decode the same way and exit the same code path. +- No network mock, no fake stdout capture, no shimmed command. We + invoke the actual `scripts/status.sh` against an isolated `$HOME` + and assert the actual stdout grep-shape. + +## Coverage scope + +- Free path (no token) — Test 1 +- Pro active (exp in future) — Test 2 + token-leak guard + last-4 redaction +- Pro expired (exp in past) — Test 3 + token-leak guard + renewal hint +- Wire-up (X-License-Token sent) — `runtime-e2e/mcp-session-headers/` (separate concern) diff --git a/runtime-e2e/tier-expiry/test.sh b/runtime-e2e/tier-expiry/test.sh new file mode 100755 index 0000000..7004ba4 --- /dev/null +++ b/runtime-e2e/tier-expiry/test.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +# Cursor runtime E2E: V1 SaaS Plugin Pro tier-line expiry surface. +# +# Drives the user-facing surface (`scripts/status.sh`, invoked by the +# /axonflow-status skill via the integrated terminal) against three +# realistic license-token states and asserts the actual stdout matches +# the documented tier-line shapes: +# +# - Free → "tier Free (no Pro license configured)" +# - Pro active → "tier Pro (expires YYYY-MM-DD, N days remaining)" +# - Pro expired → "tier Free (Pro expired YYYY-MM-DD — visit ... to renew)" +# +# Why this is real-surface runtime proof (HARD RULE #0): +# - The script under test IS the script invoked from the user's +# integrated terminal (or via the /axonflow-status skill that +# guides the agent to bash it) — same file, same path, same +# env-var resolution order. +# - The license tokens are real AXON-prefixed JWTs (header.payload. +# sig base64url-encoded per RFC 7519). The JWT-parsing branch in +# status.sh does NOT distinguish a platform-minted token from a +# test-minted one structurally — both decode the same way and +# exit the same code path. The platform's signature validation is +# a separate concern that lives in PluginClaimMiddleware on the +# agent and is exercised by the wire-level mcp-session-headers +# runtime-e2e test. +# - No network mock, no fake stdout capture, no shimmed command. We +# invoke the actual scripts/status.sh against an isolated $HOME +# and assert the actual stdout grep-shape. + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PLUGIN_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +STATUS_SH="${PLUGIN_DIR}/scripts/status.sh" + +if [ ! -f "$STATUS_SH" ]; then + echo "FAIL: required file missing: $STATUS_SH" + exit 1 +fi + +if ! command -v base64 >/dev/null 2>&1; then + echo "SKIP: base64 not on PATH" + exit 0 +fi + +PASS=0 +FAIL=0 +pass() { printf ' PASS: %s\n' "$1"; PASS=$((PASS+1)); } +fail() { printf ' FAIL: %s\n' "$1"; FAIL=$((FAIL+1)); } + +# Mint a structurally-valid AXON- token whose JWT payload contains a +# given `exp` (unix epoch seconds). Signature segment is a fixed +# placeholder string padded to a realistic length; status.sh extracts +# `exp` only, does NOT validate the signature. +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":"runtime-e2e","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" +} + +TMP_HOME=$(mktemp -d) +trap 'rm -rf "$TMP_HOME"' EXIT + +echo "=== runtime-e2e: V1 SaaS Plugin Pro tier-expiry surface ===" +echo "Plugin dir: $PLUGIN_DIR" +echo "Script: $STATUS_SH" +echo "" + +# Test 1: Free tier (no token). +echo "Test 1: Free tier — no Pro license configured" +FREE_OUT=$(AXONFLOW_TELEMETRY=off \ + HOME="$TMP_HOME" \ + AXONFLOW_CONFIG_DIR="$TMP_HOME/empty" \ + bash "$STATUS_SH" 2>&1 || true) +if echo "$FREE_OUT" | grep -qE "tier[[:space:]]+Free \(no Pro license configured\)"; then + pass "Free tier-line shape (no Pro license configured)" +else + fail "Free tier-line missing expected shape; got:" + echo "$FREE_OUT" | sed 's/^/ /' +fi + +# Test 2: Pro active. +echo "" +echo "Test 2: Pro tier active — exp in the future" +PRO_EXP=$(( $(date -u +%s) + 30 * 86400 )) +PRO_TOKEN=$(mint_axon_jwt "$PRO_EXP") +PRO_OUT=$(AXONFLOW_LICENSE_TOKEN="$PRO_TOKEN" \ + AXONFLOW_TELEMETRY=off \ + HOME="$TMP_HOME" \ + AXONFLOW_CONFIG_DIR="$TMP_HOME/empty" \ + bash "$STATUS_SH" 2>&1 || true) +if echo "$PRO_OUT" | grep -qE "tier[[:space:]]+Pro \(expires [0-9]{4}-[0-9]{2}-[0-9]{2}, [0-9]+ days remaining\)"; then + pass "Pro-active tier-line shape (expires YYYY-MM-DD, N days remaining)" +else + fail "Pro-active tier-line missing expected shape; got:" + echo "$PRO_OUT" | sed 's/^/ /' +fi +if echo "$PRO_OUT" | grep -qF "$PRO_TOKEN"; then + fail "Pro-active output leaked full token" +else + pass "Pro-active output redacts full token" +fi +PRO_TAIL4="${PRO_TOKEN: -4}" +if echo "$PRO_OUT" | grep -qF "AXON-...${PRO_TAIL4}"; then + pass "Pro-active output shows last-4 redacted preview (AXON-...${PRO_TAIL4})" +else + fail "Pro-active output missing last-4 preview" +fi + +# Test 3: Pro expired. +echo "" +echo "Test 3: Pro tier expired — exp in the past" +EXPIRED_EXP=$(( $(date -u +%s) - 365 * 86400 )) +EXPIRED_TOKEN=$(mint_axon_jwt "$EXPIRED_EXP") +EXPIRED_OUT=$(AXONFLOW_LICENSE_TOKEN="$EXPIRED_TOKEN" \ + AXONFLOW_TELEMETRY=off \ + HOME="$TMP_HOME" \ + AXONFLOW_CONFIG_DIR="$TMP_HOME/empty" \ + bash "$STATUS_SH" 2>&1 || true) +if echo "$EXPIRED_OUT" | grep -qE "tier[[:space:]]+Free \(Pro expired [0-9]{4}-[0-9]{2}-[0-9]{2} — visit https?://[^ ]+ to renew\)"; then + pass "Pro-expired tier-line shape (Pro expired YYYY-MM-DD — visit ... to renew)" +else + fail "Pro-expired tier-line missing expected shape; got:" + echo "$EXPIRED_OUT" | sed 's/^/ /' +fi +if echo "$EXPIRED_OUT" | grep -qF "$EXPIRED_TOKEN"; then + fail "Pro-expired output leaked full token" +else + pass "Pro-expired output redacts full token" +fi +if echo "$EXPIRED_OUT" | grep -q "After buying a renewal, replace the token"; then + pass "Pro-expired output surfaces the renewal hint" +else + fail "Pro-expired output missing renewal hint" +fi + +echo "" +echo "Summary: $PASS PASS, $FAIL FAIL" +if [ "$FAIL" -gt 0 ]; then + exit 1 +fi +echo "PASS: V1 SaaS Plugin Pro tier-expiry surface verified end-to-end" +exit 0 diff --git a/scripts/status.sh b/scripts/status.sh index 0b932b8..2cdf464 100755 --- a/scripts/status.sh +++ b/scripts/status.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash -# Plugin status — surfaces tenant_id + tier so Pro buyers can paste the -# tenant_id into Stripe Checkout's custom field at /pro and so any user -# can quickly verify which tier they're on. +# Plugin status — surfaces tenant_id + tier (with Pro expiry date) so Pro +# buyers can paste the tenant_id into Stripe Checkout's custom field at +# /pro and so any user can quickly verify which tier they're on AND when +# their Pro license expires. # # Cursor does not run plugin code in a long-lived process, and the IDE has # no command palette for plugin output. The natural surface is a script the @@ -16,6 +17,14 @@ # value. Show "AXON-...XXXX" using only the last 4 chars (defensively # padded). Mirrors the fix in axonflow-codex-plugin#41 (cmd_status was # leaking the full token to the terminal). +# +# JWT exp parsing: the AXON- prefix wraps a standard JWT (header.payload. +# signature). We extract the `exp` claim from the payload to compute the +# tier-line shape: +# - Pro active → "tier Pro (expires YYYY-MM-DD, N days remaining)" +# - Pro lapsed → "tier Free (Pro expired YYYY-MM-DD — visit to renew)" +# - Free → "tier Free (no Pro license configured)" +# Signature is NOT validated here — that's the platform's job. set -uo pipefail @@ -88,8 +97,60 @@ elif [ -f "$LICENSE_TOKEN_FILE" ]; then fi fi +# 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" +} + +# Compute the tier line. Three branches (matching codex / claude / openclaw): +# 1. No token resolved → "Free (no Pro license configured)" +# 2. Token resolved + exp parsed: +# 2a. exp in future → "Pro (expires YYYY-MM-DD, N days remaining)" +# 2b. exp in past → "Free (Pro expired YYYY-MM-DD — visit to renew)" +# 3. Token resolved + exp NOT parseable +# → "Pro (expires UNKNOWN — could not parse token)" +TIER_LINE="Free (no Pro license configured)" +PRO_EXPIRED_FLAG=0 +TIER_KIND="free" + if [ -n "$LICENSE_TOKEN" ] && [ "$TOKEN_SOURCE" != "unsafe-perms" ]; then - TIER="Pro" # Defensively pad short tokens so we never reveal the middle bytes — if # someone test-injected a 3-char token we still show "****" not "AXO". TAIL4="****" @@ -97,8 +158,34 @@ if [ -n "$LICENSE_TOKEN" ] && [ "$TOKEN_SOURCE" != "unsafe-perms" ]; then TAIL4="${LICENSE_TOKEN: -4}" fi TOKEN_DISPLAY="set (AXON-...${TAIL4}, source=${TOKEN_SOURCE})" + + EXP_EPOCH=$(extract_jwt_exp "$LICENSE_TOKEN" 2>/dev/null || true) + if [ -n "$EXP_EPOCH" ]; then + EXP_DATE=$(format_unix_to_date "$EXP_EPOCH" 2>/dev/null || true) + if [ -n "$EXP_DATE" ]; then + NOW_EPOCH=$(date -u +%s) + if [ "$EXP_EPOCH" -gt "$NOW_EPOCH" ]; then + SECS_LEFT=$(( EXP_EPOCH - NOW_EPOCH )) + DAYS_LEFT=$(( (SECS_LEFT + 86399) / 86400 )) + TIER_LINE="Pro (expires ${EXP_DATE}, ${DAYS_LEFT} days remaining)" + TIER_KIND="pro" + else + TIER_LINE="Free (Pro expired ${EXP_DATE} — visit ${UPGRADE_URL} to renew)" + TIER_KIND="pro-expired" + PRO_EXPIRED_FLAG=1 + fi + else + TIER_LINE="Pro (expires UNKNOWN — could not parse token)" + TIER_KIND="pro" + fi + else + # Token shape valid but JWT parse failed — treat as Pro for display. + # The platform is the source of truth; if the token is junk the next + # governed call will surface the 401. + TIER_LINE="Pro (expires UNKNOWN — could not parse token)" + TIER_KIND="pro" + fi else - TIER="Free" if [ "$TOKEN_SOURCE" = "unsafe-perms" ]; then TOKEN_DISPLAY="present at $LICENSE_TOKEN_FILE but refused (unsafe permissions; chmod 0600)" else @@ -114,15 +201,22 @@ AxonFlow Cursor plugin — status tenant_id: ${TENANT_ID} registration file ${REGISTRATION_FILE} license token ${TOKEN_DISPLAY} - tier ${TIER} - upgrade ${UPGRADE_URL} + tier ${TIER_LINE} EOF +# Only show the upgrade URL on Free tiers (active Pro users don't need it +# in their face). Pro-expired users do want to see it — it's now embedded +# in the tier line itself ("visit to renew") so we suppress the +# duplicate `upgrade` line for them too. +if [ "$TIER_KIND" = "free" ]; then + printf ' upgrade %s\n' "${UPGRADE_URL}" +fi + if [ -n "$TENANT_HINT" ]; then printf '\n hint: %s\n' "$TENANT_HINT" fi -if [ "$TIER" = "Free" ]; then +if [ "$TIER_KIND" = "free" ]; then cat < ${LICENSE_TOKEN_FILE} && chmod 0600 ${LICENSE_TOKEN_FILE} +EOF fi exit 0 diff --git a/skills/axonflow-status/SKILL.md b/skills/axonflow-status/SKILL.md index c9293a8..f68c28f 100644 --- a/skills/axonflow-status/SKILL.md +++ b/skills/axonflow-status/SKILL.md @@ -1,6 +1,6 @@ --- name: axonflow-status -description: Show AxonFlow plugin status — tenant_id (needed for Stripe Pro upgrade), tier (Free/Pro), endpoint, and config file paths +description: Show AxonFlow plugin status — tenant_id (needed for Stripe Pro upgrade), tier (Free/Pro), Pro license expiry date, endpoint, and config file paths --- Use this skill when the user asks any of: @@ -8,14 +8,15 @@ Use this skill when the user asks any of: - "What is my AxonFlow tenant_id?" — needed to paste into the custom field at Stripe Checkout (`https://getaxonflow.com/pro`) when buying Pro. - "Am I on Pro or Free tier?" +- "When does my Pro license expire?" / "How many days do I have left?" - "Is my Pro license token loaded?" - "Where does the plugin think AxonFlow is?" / "What endpoint am I hitting?" -- "How do I upgrade to Pro?" +- "How do I upgrade to Pro?" / "How do I renew?" ## What to do 1. Tell the user what you're about to do: "I'll run `scripts/status.sh` in - your terminal to print your tenant_id and tier." + your terminal to print your tenant_id, tier, and Pro license expiry." 2. Invoke the script via the Shell tool: `bash scripts/status.sh` Run it from the plugin's install directory, typically @@ -25,6 +26,27 @@ Use this skill when the user asks any of: output and remind them they need to paste the `tenant_id` into the Stripe Checkout custom field. +## Tier line shape + +The script's `tier` line takes one of three shapes — surface whichever one +the user got and act on it: + +- `tier Pro (expires 2026-08-03, 90 days remaining)` — paid Pro tier + active. Pro-tier daily quotas + retention + governance hold for the + remaining window. +- `tier Pro (expires UNKNOWN — could not parse token)` — token configured + but its JWT body did not parse. Treat as Pro for display; the platform + is the source of truth on validity. +- `tier Free (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; user must buy a renewal and replace the token via + `AXONFLOW_LICENSE_TOKEN=` env or the on-disk file. +- `tier Free (no Pro license configured)` — no token loaded. + +When the user lands on `Free (Pro expired …)`, point them at the renew +URL embedded in the line and the `export AXONFLOW_LICENSE_TOKEN=AXON-...` +hint the script prints below. + ## What this skill does NOT do - It does NOT print the full Pro license token. Only the last 4 chars are @@ -33,9 +55,10 @@ Use this skill when the user asks any of: asks for the full token, point them at the original Stripe / billing email rather than the script output. - It does NOT call the agent to verify token validity — the platform is the - source of truth. The script reports "Pro" whenever a token is loaded; if - the agent later rejects the token (revoked, malformed, expired), the - user will see that on their next governed tool call. + source of truth. The script extracts the JWT `exp` for display only; + signature validation is the platform's job. If the agent later rejects + the token (revoked, malformed), the user will see that on their next + governed tool call. - It does NOT perform recovery. If `tenant_id` is `(not registered)` and the user expected one, suggest the `/recover-credentials` skill. @@ -45,6 +68,7 @@ Suggest this skill when the user reports any of: - "I'm trying to buy Pro and need my tenant_id" - "How do I know if my Pro upgrade went through?" +- "When does my Pro license expire?" - "Which AxonFlow am I connected to?" - "Is my license token configured correctly?" diff --git a/tests/install-smoke/run.sh b/tests/install-smoke/run.sh index 95073c9..ffe2ad7 100755 --- a/tests/install-smoke/run.sh +++ b/tests/install-smoke/run.sh @@ -69,8 +69,12 @@ done # 2b. Status-script smoke. Drives status.sh against an isolated $HOME so it # never touches the developer's real ~/.config/axonflow. Asserts: # - tenant_id from try-registration.json renders into output -# - "tier Free" when no AXONFLOW_LICENSE_TOKEN env is set -# - "tier Pro" + last-4 redaction when token is set +# - "tier Free (no Pro license configured)" when no AXONFLOW_LICENSE_TOKEN +# env is set +# - "tier Pro (expires YYYY-MM-DD, N days remaining)" when a Pro-active +# token is set (V1 SaaS Plugin Pro tier-expiry surface parity) +# - "tier Free (Pro expired YYYY-MM-DD — visit ... to renew)" when the +# token is on disk but its `exp` is in the past # - the FULL token never appears in stdout (the codex#41 regression) # - recovery hint surfaces when the registration file is missing STATUS_HOME=$(mktemp -d 2>/dev/null || mktemp -d -t status-home) @@ -81,44 +85,77 @@ cat > "$STATUS_HOME/.config/axonflow/try-registration.json" <<'EOJ' EOJ chmod 0600 "$STATUS_HOME/.config/axonflow/try-registration.json" -# Free-tier path: no env token, expect "Free" + tenant_id surfaced. +# Helper: mint a structurally-valid AXON- token whose JWT payload contains +# a given exp (unix epoch). Signature is a placeholder — status.sh only +# parses, never validates. base64url with padding stripped, per RFC 7515. +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 '=') + # Pad signature so the whole token is comfortably long; status doesn't + # gate on length but readers expect ~real-token shape. + local sig="placeholder-signature-padding-padding-padding-padding-padding-pa" + printf 'AXON-%s.%s.%s' "$hdr" "$payload" "$sig" +} + +# Free-tier path: no env token, expect "Free (no Pro license configured)" +# + tenant_id surfaced. FREE_OUT=$(HOME="$STATUS_HOME" AXONFLOW_TELEMETRY=off bash "$STAGE_DIR/scripts/status.sh" 2>&1) if echo "$FREE_OUT" | grep -q "tenant_id:[[:space:]]*cs_smoke-tenant-xyz"; then pass "status.sh surfaces tenant_id from try-registration.json" else fail "status.sh missing tenant_id; output: $FREE_OUT" fi -if echo "$FREE_OUT" | grep -qE "tier[[:space:]]+Free"; then - pass "status.sh reports Free tier when no token configured" +if echo "$FREE_OUT" | grep -qE "tier[[:space:]]+Free \(no Pro license configured\)"; then + pass "status.sh Free-tier line shape (no Pro license configured)" else fail "status.sh did not report Free tier; output: $FREE_OUT" fi -# Pro-tier path: set env token with a known last-4. The full token is a -# bearer credential and MUST NOT appear in stdout — only the last 4 chars -# inside an `AXON-...XXXX` redaction. Mirrors the axonflow-codex-plugin#41 -# fix where cmd_status leaked the entire token. -SECRET_TOKEN_FULL="AXON-eyJsupersecretjwtbodynevershow-tail9999" -SECRET_TOKEN_MIDDLE="supersecretjwtbody" +# Pro-tier ACTIVE path: mint a token with exp ~30 days in the future. +# Expect "tier Pro (expires YYYY-MM-DD, N days remaining)" + redacted last-4. +PRO_EXP=$(( $(date -u +%s) + 30 * 86400 )) +PRO_TOKEN=$(mint_axon_jwt "$PRO_EXP") PRO_OUT=$(HOME="$STATUS_HOME" AXONFLOW_TELEMETRY=off \ - AXONFLOW_LICENSE_TOKEN="$SECRET_TOKEN_FULL" \ + AXONFLOW_LICENSE_TOKEN="$PRO_TOKEN" \ bash "$STAGE_DIR/scripts/status.sh" 2>&1) -if echo "$PRO_OUT" | grep -qE "tier[[:space:]]+Pro"; then - pass "status.sh reports Pro tier when AXONFLOW_LICENSE_TOKEN is set" +if echo "$PRO_OUT" | grep -qE "tier[[:space:]]+Pro \(expires [0-9]{4}-[0-9]{2}-[0-9]{2}, [0-9]+ days remaining\)"; then + pass "status.sh Pro-active line shape (expires YYYY-MM-DD, N days remaining)" else - fail "status.sh did not report Pro tier; output: $PRO_OUT" + fail "status.sh did not report Pro-active line; output: $PRO_OUT" fi -if echo "$PRO_OUT" | grep -q "AXON-\\.\\.\\.9999"; then +PRO_TAIL4="${PRO_TOKEN: -4}" +if echo "$PRO_OUT" | grep -qF "AXON-...${PRO_TAIL4}"; then pass "status.sh emits AXON-...XXXX redaction with last-4 chars" else fail "status.sh missing last-4 redaction; output: $PRO_OUT" fi -if echo "$PRO_OUT" | grep -q "$SECRET_TOKEN_MIDDLE"; then +if echo "$PRO_OUT" | grep -qF "$PRO_TOKEN"; then fail "status.sh LEAKED full license token to stdout: $PRO_OUT" else pass "status.sh does not leak full license token" fi +# Pro-tier EXPIRED path: mint a token with exp ~365 days in the past. +# Expect "tier Free (Pro expired YYYY-MM-DD — visit ... to renew)". +EXPIRED_EXP=$(( $(date -u +%s) - 365 * 86400 )) +EXPIRED_TOKEN=$(mint_axon_jwt "$EXPIRED_EXP") +EXPIRED_OUT=$(HOME="$STATUS_HOME" AXONFLOW_TELEMETRY=off \ + AXONFLOW_LICENSE_TOKEN="$EXPIRED_TOKEN" \ + bash "$STAGE_DIR/scripts/status.sh" 2>&1) +if echo "$EXPIRED_OUT" | grep -qE "tier[[:space:]]+Free \(Pro expired [0-9]{4}-[0-9]{2}-[0-9]{2} — visit https?://[^ ]+ to renew\)"; then + pass "status.sh Pro-expired line shape (Pro expired YYYY-MM-DD — visit ... to renew)" +else + fail "status.sh did not report Pro-expired line; output: $EXPIRED_OUT" +fi +if echo "$EXPIRED_OUT" | grep -qF "$EXPIRED_TOKEN"; then + fail "status.sh LEAKED expired token to stdout" +else + pass "status.sh redacts expired token" +fi + # Missing-registration path: hint should reference the recovery script. rm -f "$STATUS_HOME/.config/axonflow/try-registration.json" NOREG_OUT=$(HOME="$STATUS_HOME" AXONFLOW_TELEMETRY=off bash "$STAGE_DIR/scripts/status.sh" 2>&1)