Skip to content

Commit b759d20

Browse files
Grayson AdamsGrayson Adams
authored andcommitted
feat: programmatic login, dual-gateway support, and token lifecycle overhaul
- Add metlife.sh to repo root with full auth flow (login/profileDevice/PKCE) - Support both Azure APIM (pets, policies) and IBM API Connect (claims) gateways - Track token expiry and proactively refresh before TTL (120s buffer) - Store bearer + refresh + expiry + session_id in .token file - Add cache validation (never cache 401/error responses) - Add purge_bad_cache and metlife_reset utilities - Update skills to use ensure_auth gate and ask for credentials when needed - Gitignore PDFs and temp appeal briefing files - Remove hardcoded absolute paths from skill instructions
1 parent bf65f1d commit b759d20

5 files changed

Lines changed: 476 additions & 239 deletions

File tree

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@ token.txt
88
*.txt
99
!README.txt
1010

11-
# Cached API responses and documents
11+
# Cached API responses, tokens, and documents
1212
.metlife-cache/
1313

14+
# Temp appeal briefing files
15+
appeal-briefing-*.tmp.md
16+
1417
# Desktop skill build artifacts
1518
desktop-skills/
1619

1720
# OS
1821
.DS_Store
22+
*.pdf

CLAUDE.md

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,38 +6,48 @@ You are a personal assistant for managing MetLife pet insurance claims. When the
66

77
- Be conversational. The user will say things like "hey, my claim got denied" or "can you check on my claims" or "I want to appeal this". Just help them.
88
- If you need their policy ID, pet ID, or claim ID, ask for it naturally. Don't make them format it a specific way.
9-
- If `METLIFE_BEARER_TOKEN` isn't set, walk them through getting one: log into mypets.metlife.com, open DevTools, grab the Bearer token from any API request. Keep it simple.
109
- When you fetch data, summarize it in plain English. Don't dump raw JSON unless they ask.
1110
- Guide them through the process step by step. Explain what you're doing and why.
1211
- If they mention a denied claim, ask if they want you to look into whether an appeal makes sense. Don't assume -- ask first, then do the work.
1312
- If an appeal isn't worth pursuing, say so directly and explain why. Don't sugarcoat it.
1413

14+
## Authentication
15+
16+
Authentication is handled by `metlife.sh` in the repo root. **Every Bash call** must start with:
17+
```bash
18+
cd /path/to/claude-metlife
19+
source metlife.sh
20+
AUTH_RESULT=$(ensure_auth)
21+
```
22+
23+
If `AUTH_NEEDED` is returned, ask the user for their mypets.metlife.com email and password, then call `metlife_login "email" "password"`. The script handles the full login flow, token refresh, and persistence automatically.
24+
25+
If automated login fails (ThreatMetrix device profiling block), walk them through the manual fallback: log into mypets.metlife.com, open DevTools Network tab, copy the Bearer token from any API request, and paste it. Save it to `.metlife-cache/.token`.
26+
1527
## What you can do
1628

17-
You have access to the MetLife Pet Insurance API. You can:
29+
You have access to the MetLife Pet Insurance API via two gateways. You can:
1830

31+
- **Log in** -- authenticate with email/password, auto-refresh tokens
1932
- **Look up claims** -- list all claims, get details on a specific claim
20-
- **Pull documents** -- download EOBs, invoices, any claim documents
33+
- **Pull documents** -- download EOBs, invoices, SOAP notes, any claim documents
2134
- **Get the policy** -- download and read the full policy packet
35+
- **List pets and policies** -- see all pets and policy details
2236
- **Evaluate appeals** -- run a full adversarial analysis to determine if a denial is worth fighting
2337
- **Draft appeal letters** -- write a ready-to-send letter if the case has merit
2438

25-
All API details (endpoints, headers, curl templates) are in the skills. Just use them when needed -- the user doesn't need to know the mechanics.
39+
All API details (endpoints, headers, auth) are in the skills and `metlife.sh`. The user doesn't need to know the mechanics.
2640

2741
## Typical conversation flows
2842

2943
**"What's going on with my claims?"**
30-
→ Ask for policy ID and pet ID if you don't have them, fetch all claims, summarize status of each.
44+
-> Ensure auth (ask for credentials if needed), fetch all pets, then all claims for each, summarize status.
3145

3246
**"My claim for Peaches got denied"**
33-
Ask which claim (or fetch the list and ask them to confirm). Pull the claim details and EOB. Explain what the denial reason is in plain English.
47+
-> Ask which claim (or fetch the list and ask them to confirm). Pull the claim details and EOB. Explain the denial reason in plain English.
3448

3549
**"Can you appeal this?"**
36-
Pull the EOB, policy packet, and claim history. Run the adversarial simulation. Come back with an honest assessment. If it's worth it, draft the letter. If not, explain why.
50+
-> Pull the EOB, policy packet, and claim history. Run the adversarial simulation. Come back with an honest assessment. If it's worth it, draft the letter. If not, explain why.
3751

3852
**"I just want to understand my policy"**
39-
→ Download the policy packet, read it, and answer their questions about coverage, exclusions, deductibles, waiting periods, etc.
40-
41-
## API setup
42-
43-
The bearer token comes from `$METLIFE_BEARER_TOKEN`. If it's not set or a request returns 401, help the user get a fresh one.
53+
-> Download the policy packet, read it, and answer their questions about coverage, exclusions, deductibles, waiting periods, etc.

metlife.sh

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
#!/bin/bash
2+
# MetLife Pet Insurance API helper
3+
# Source this at the start of every Bash tool call that needs API access.
4+
# All secrets (tokens) stay in .metlife-cache/ which is gitignored.
5+
6+
METLIFE_CACHE_DIR=".metlife-cache"
7+
TOKEN_FILE="$METLIFE_CACHE_DIR/.token"
8+
9+
# Public client keys (embedded in mypets.metlife.com SPA JavaScript)
10+
APIM_KEY="979fd0c2ea204f1095d7faa8154c39b0"
11+
IBM_CLIENT_ID="634dc387-c737-4a6a-86ef-f49056e30898"
12+
CHANNEL_ID="PetMobile"
13+
APP_VERSION="4.5.1"
14+
ORIGIN="https://mypets.metlife.com"
15+
REFERER="https://mypets.metlife.com/"
16+
17+
# Gateway base URLs
18+
APIM_BASE="https://apis.metlife.com/external/pet-services"
19+
IBM_BASE="https://api.metlife.com/metlife/production/api/pet-services/pingv2"
20+
21+
# ── Utilities ────────────────────────────────────────────────────────────────
22+
23+
_session_id() {
24+
python3 -c "import time,random,string; print(str(int(time.time()*1000))+''.join(random.choices(string.ascii_lowercase,k=3)))"
25+
}
26+
27+
_pkce() {
28+
python3 -c "import os,base64; print(base64.urlsafe_b64encode(os.urandom(32)).rstrip(b'=').decode())"
29+
}
30+
31+
# ── Token storage ────────────────────────────────────────────────────────────
32+
# File format (4 lines): bearer_token / refresh_token / expiry_epoch / session_id
33+
34+
load_tokens() {
35+
METLIFE_BEARER_TOKEN=$(sed -n '1p' "$TOKEN_FILE" 2>/dev/null)
36+
METLIFE_REFRESH_TOKEN=$(sed -n '2p' "$TOKEN_FILE" 2>/dev/null)
37+
METLIFE_TOKEN_EXPIRY=$(sed -n '3p' "$TOKEN_FILE" 2>/dev/null)
38+
METLIFE_SESSION_ID=$(sed -n '4p' "$TOKEN_FILE" 2>/dev/null)
39+
[ -z "$METLIFE_SESSION_ID" ] && METLIFE_SESSION_ID=$(_session_id)
40+
}
41+
42+
save_tokens() {
43+
mkdir -p "$METLIFE_CACHE_DIR"
44+
printf '%s\n%s\n%s\n%s\n' \
45+
"$METLIFE_BEARER_TOKEN" \
46+
"$METLIFE_REFRESH_TOKEN" \
47+
"$METLIFE_TOKEN_EXPIRY" \
48+
"$METLIFE_SESSION_ID" > "$TOKEN_FILE"
49+
}
50+
51+
token_is_expired() {
52+
[ -z "$METLIFE_BEARER_TOKEN" ] || [ -z "$METLIFE_TOKEN_EXPIRY" ] && return 0
53+
local now; now=$(date +%s)
54+
[ "$now" -ge $((METLIFE_TOKEN_EXPIRY - 120)) ]
55+
}
56+
57+
# ── Login ────────────────────────────────────────────────────────────────────
58+
# Usage: metlife_login "email" "password"
59+
60+
metlife_login() {
61+
local username="$1" password="$2"
62+
[ -z "$username" ] || [ -z "$password" ] && { echo "ERROR: Username and password required" >&2; return 1; }
63+
64+
METLIFE_SESSION_ID=$(_session_id)
65+
local cv; cv=$(_pkce)
66+
67+
# Step 1: POST /authentication/login
68+
echo "Logging in..." >&2
69+
local body
70+
body=$(python3 -c "import json,sys; print(json.dumps({'userName':sys.argv[1],'password':sys.argv[2],'codeChallenge':sys.argv[3]}))" "$username" "$password" "$cv")
71+
72+
local r1
73+
r1=$(curl -s "$APIM_BASE/authentication/login" \
74+
-H 'content-type: application/json' \
75+
-H "channel-id: $CHANNEL_ID" \
76+
-H 'is-ping-token: true' \
77+
-H "ocp-apim-subscription-key: $APIM_KEY" \
78+
-H "origin: $ORIGIN" -H "referer: $REFERER" \
79+
-H "session-id: $METLIFE_SESSION_ID" \
80+
-H "transaction-id: $METLIFE_SESSION_ID" \
81+
-H "x-app-version: $APP_VERSION" \
82+
-H 'cache-control: no-cache, no-store' \
83+
--data-raw "$body" 2>/dev/null)
84+
85+
local flow_id corr_id status
86+
read -r flow_id corr_id status < <(echo "$r1" | python3 -c "
87+
import sys,json
88+
d=json.load(sys.stdin)
89+
print(d.get('id',''), d.get('extension',{}).get('correlationId',''), d.get('status',''))
90+
" 2>/dev/null)
91+
92+
if [ -z "$flow_id" ]; then
93+
echo "ERROR: Login failed. Response: $r1" >&2
94+
return 1
95+
fi
96+
echo "Login step 1 OK (status: $status). Completing auth..." >&2
97+
98+
# Brief pause — server needs a moment after device profile check
99+
sleep 2
100+
101+
# Step 2: POST /authentication/profileDevice
102+
local body2
103+
body2=$(python3 -c "import json,sys; print(json.dumps({'flowId':sys.argv[1],'codeVerifier':sys.argv[2],'extension':{'correlationId':sys.argv[3]}}))" "$flow_id" "$cv" "$corr_id")
104+
105+
local r2
106+
r2=$(curl -s "$APIM_BASE/authentication/profileDevice" \
107+
-H 'content-type: application/json' \
108+
-H "channel-id: $CHANNEL_ID" \
109+
-H 'is-ping-token: true' \
110+
-H "ocp-apim-subscription-key: $APIM_KEY" \
111+
-H "origin: $ORIGIN" -H "referer: $REFERER" \
112+
-H "session-id: $METLIFE_SESSION_ID" \
113+
-H "transaction-id: $METLIFE_SESSION_ID" \
114+
-H "x-app-version: $APP_VERSION" \
115+
-H 'cache-control: no-cache, no-store' \
116+
--data-raw "$body2" 2>/dev/null)
117+
118+
local auth_status
119+
auth_status=$(echo "$r2" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status',''))" 2>/dev/null)
120+
if [ "$auth_status" != "COMPLETED" ]; then
121+
echo "ERROR: Auth failed (status: $auth_status). Response: $r2" >&2
122+
return 1
123+
fi
124+
125+
# Extract tokens
126+
local now; now=$(date +%s)
127+
read -r METLIFE_BEARER_TOKEN METLIFE_REFRESH_TOKEN expires_in < <(echo "$r2" | python3 -c "
128+
import sys,json
129+
t=json.load(sys.stdin)['tokenResponse']
130+
print(t['access_token'], t['refresh_token'], t['expires_in'])
131+
" 2>/dev/null)
132+
METLIFE_TOKEN_EXPIRY=$((now + expires_in))
133+
134+
if [ -z "$METLIFE_BEARER_TOKEN" ] || [ "$METLIFE_BEARER_TOKEN" = "None" ]; then
135+
echo "ERROR: No access token in response" >&2; return 1
136+
fi
137+
138+
save_tokens
139+
echo "Login successful. Token expires in ${expires_in}s (~$((expires_in/60))m)." >&2
140+
}
141+
142+
# ── Token refresh ────────────────────────────────────────────────────────────
143+
144+
refresh_token() {
145+
load_tokens
146+
[ -z "$METLIFE_REFRESH_TOKEN" ] && { echo "ERROR: No refresh token. Login required." >&2; return 1; }
147+
148+
local sid; sid=$(_session_id)
149+
echo "Refreshing token..." >&2
150+
local r
151+
r=$(curl -s "$APIM_BASE/authentication/v2/refreshToken_v2" \
152+
-H 'content-type: application/json' \
153+
-H "channel-id: $CHANNEL_ID" \
154+
-H 'is-ping-token: true' \
155+
-H "ocp-apim-subscription-key: $APIM_KEY" \
156+
-H "origin: $ORIGIN" -H "referer: $REFERER" \
157+
-H "session-id: $sid" -H "transaction-id: $sid" \
158+
-H "x-app-version: $APP_VERSION" \
159+
-H 'cache-control: no-cache, no-store' \
160+
--data-raw "\"$METLIFE_REFRESH_TOKEN\"" 2>/dev/null)
161+
162+
local new_tok new_ref exp_in
163+
read -r new_tok new_ref exp_in < <(echo "$r" | python3 -c "
164+
import sys,json
165+
d=json.load(sys.stdin)
166+
print(d.get('access_token',''), d.get('refresh_token',''), d.get('expires_in',899))
167+
" 2>/dev/null)
168+
169+
if [ -n "$new_tok" ] && [ "$new_tok" != "None" ]; then
170+
local now; now=$(date +%s)
171+
METLIFE_BEARER_TOKEN="$new_tok"
172+
[ -n "$new_ref" ] && [ "$new_ref" != "None" ] && METLIFE_REFRESH_TOKEN="$new_ref"
173+
METLIFE_TOKEN_EXPIRY=$((now + exp_in))
174+
save_tokens
175+
echo "Token refreshed. Expires in ${exp_in}s (~$((exp_in/60))m)." >&2
176+
return 0
177+
fi
178+
echo "ERROR: Refresh failed. Response: $r" >&2
179+
return 1
180+
}
181+
182+
# ── Auth gate ────────────────────────────────────────────────────────────────
183+
# Call before any API operation. Returns 0 if ready, 1 if login needed.
184+
# On failure, prints AUTH_NEEDED to stdout so the caller can detect it.
185+
186+
ensure_auth() {
187+
load_tokens
188+
if token_is_expired; then
189+
if [ -n "$METLIFE_REFRESH_TOKEN" ]; then
190+
refresh_token && return 0
191+
fi
192+
echo "AUTH_NEEDED"
193+
return 1
194+
fi
195+
return 0
196+
}
197+
198+
# ── Response validation ──────────────────────────────────────────────────────
199+
200+
_is_error() {
201+
echo "$1" | grep -q '"httpCode":"401"\|"Unauthorized"\|"Token is not active"\|"httpCode":"403"'
202+
}
203+
204+
is_cacheable() {
205+
[ -n "$1" ] && ! _is_error "$1"
206+
}
207+
208+
# ── API wrappers ─────────────────────────────────────────────────────────────
209+
210+
# IBM API Connect gateway: claims, customer details, payments, search
211+
# Usage: metlife_ibm "URL" [extra curl args...]
212+
metlife_ibm() {
213+
load_tokens
214+
local url="$1"; shift
215+
local r
216+
r=$(curl -s "$url" \
217+
-H "authorization: Bearer $METLIFE_BEARER_TOKEN" \
218+
-H "x-ibm-client-id: $IBM_CLIENT_ID" \
219+
-H "x-session-id: $METLIFE_SESSION_ID" \
220+
-H "x-app-version: $APP_VERSION" \
221+
-H "app-version: $APP_VERSION" \
222+
-H 'cache-control: no-cache, no-store' \
223+
-H "origin: $ORIGIN" -H "referer: $REFERER" \
224+
-H 'accept: */*' \
225+
"$@")
226+
227+
if _is_error "$r"; then
228+
echo "Got 401, refreshing..." >&2
229+
if refresh_token; then
230+
r=$(curl -s "$url" \
231+
-H "authorization: Bearer $METLIFE_BEARER_TOKEN" \
232+
-H "x-ibm-client-id: $IBM_CLIENT_ID" \
233+
-H "x-session-id: $METLIFE_SESSION_ID" \
234+
-H "x-app-version: $APP_VERSION" \
235+
-H "app-version: $APP_VERSION" \
236+
-H 'cache-control: no-cache, no-store' \
237+
-H "origin: $ORIGIN" -H "referer: $REFERER" \
238+
-H 'accept: */*' \
239+
"$@")
240+
fi
241+
fi
242+
echo "$r"
243+
}
244+
245+
# Azure APIM gateway: pets, policies, rewards
246+
# Usage: metlife_apim "URL" [extra curl args...]
247+
metlife_apim() {
248+
load_tokens
249+
local url="$1"; shift
250+
local r
251+
r=$(curl -s "$url" \
252+
-H "authorization: Bearer $METLIFE_BEARER_TOKEN" \
253+
-H "ocp-apim-subscription-key: $APIM_KEY" \
254+
-H "channel-id: $CHANNEL_ID" \
255+
-H 'is-ping-token: true' \
256+
-H "session-id: $METLIFE_SESSION_ID" \
257+
-H "transaction-id: $METLIFE_SESSION_ID" \
258+
-H "x-app-version: $APP_VERSION" \
259+
-H 'cache-control: no-cache, no-store' \
260+
-H "origin: $ORIGIN" -H "referer: $REFERER" \
261+
-H 'accept: */*' \
262+
"$@")
263+
264+
if _is_error "$r"; then
265+
echo "Got 401, refreshing..." >&2
266+
if refresh_token; then
267+
r=$(curl -s "$url" \
268+
-H "authorization: Bearer $METLIFE_BEARER_TOKEN" \
269+
-H "ocp-apim-subscription-key: $APIM_KEY" \
270+
-H "channel-id: $CHANNEL_ID" \
271+
-H 'is-ping-token: true' \
272+
-H "session-id: $METLIFE_SESSION_ID" \
273+
-H "transaction-id: $METLIFE_SESSION_ID" \
274+
-H "x-app-version: $APP_VERSION" \
275+
-H 'cache-control: no-cache, no-store' \
276+
-H "origin: $ORIGIN" -H "referer: $REFERER" \
277+
-H 'accept: */*' \
278+
"$@")
279+
fi
280+
fi
281+
echo "$r"
282+
}
283+
284+
# Auto-routing wrapper: picks the right gateway based on URL
285+
metlife_api() {
286+
local url="$1"
287+
if [[ "$url" == *"apis.metlife.com"* ]]; then
288+
metlife_apim "$@"
289+
else
290+
metlife_ibm "$@"
291+
fi
292+
}
293+
294+
# ── Cache cleanup ────────────────────────────────────────────────────────────
295+
296+
# Purge any cached JSON files that contain error responses
297+
purge_bad_cache() {
298+
local count=0
299+
while IFS= read -r -d '' f; do
300+
if grep -q '"httpCode":"401"\|"Token is not active"\|"Unauthorized"' "$f" 2>/dev/null; then
301+
rm "$f"
302+
((count++))
303+
fi
304+
done < <(find "$METLIFE_CACHE_DIR" -name '*.json' -print0 2>/dev/null)
305+
[ "$count" -gt 0 ] && echo "Purged $count cached error responses." >&2
306+
}
307+
308+
# Full reset: clear all cached data and tokens
309+
metlife_reset() {
310+
rm -f "$TOKEN_FILE"
311+
rm -rf "$METLIFE_CACHE_DIR/claims" "$METLIFE_CACHE_DIR/documents" "$METLIFE_CACHE_DIR/policies"
312+
mkdir -p "$METLIFE_CACHE_DIR/claims" "$METLIFE_CACHE_DIR/documents" "$METLIFE_CACHE_DIR/policies"
313+
echo "Cache and tokens cleared." >&2
314+
}
315+
316+
# ── Auto-init ────────────────────────────────────────────────────────────────
317+
318+
load_tokens
319+
purge_bad_cache

0 commit comments

Comments
 (0)