---
name: refresh-token
description: "Refreshes Microsoft Graph API tokens used by OpenJaw for Teams and Outlook."
whenToUse: "Graph API tokens are expired or need refreshing for Teams/Outlook automation"
---

# Skill: Refresh Graph Token

## Description
Refreshes Microsoft Graph API tokens (graph-chat + outlook-mail) used by OpenJaw for Teams and Outlook automation.

## Prerequisites
- Edge running with `--remote-debugging-port=9222 --remote-allow-origins=*`
- Logged into outlook.office.com in Edge
- Node.js 22+ with `ws` package (`npm i -g ws` if not installed)

## How to Execute

Write the following script to a temp file and run it. The script is fully self-contained.

```javascript
// Save this as a temp .mjs file and run with: node <tempfile>.mjs
import { WebSocket } from 'ws';
import { writeFileSync, readFileSync, mkdirSync, existsSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';

const CDP_PORT = 9222;
const CLIENT_ID = '9199bf20-a13f-4107-85dc-02114787ef48';
const TOKEN_DIR = join(homedir(), '.graph-token', 'tokens');

const AUDIENCES = [
  { name: 'graph-chat', audience: 'https://graph.microsoft.com' },
  { name: 'outlook-mail', audience: 'https://outlook.office.com' },
];

function decodeJwt(jwt) {
  try { return JSON.parse(Buffer.from(jwt.split('.')[1], 'base64url').toString()); }
  catch { return null; }
}

async function listPages() {
  return await (await fetch(`http://localhost:${CDP_PORT}/json`)).json();
}

async function evalInTab(wsUrl, expr) {
  return new Promise(resolve => {
    const ws = new WebSocket(wsUrl);
    const timer = setTimeout(() => { ws.close(); resolve(null); }, 15000);
    ws.on('open', () => ws.send(JSON.stringify({ id: 1, method: 'Runtime.evaluate', params: { expression: expr, returnByValue: true } })));
    ws.on('message', d => { const r = JSON.parse(d.toString()); if (r.id === 1) { clearTimeout(timer); ws.close(); resolve(r.result?.result?.value ?? null); } });
    ws.on('error', () => { clearTimeout(timer); resolve(null); });
  });
}

async function openTab(url) {
  const ver = await (await fetch(`http://localhost:${CDP_PORT}/json/version`)).json();
  return new Promise(resolve => {
    const ws = new WebSocket(ver.webSocketDebuggerUrl);
    const timer = setTimeout(() => { ws.close(); resolve(null); }, 15000);
    ws.on('open', () => ws.send(JSON.stringify({ id: 1, method: 'Target.createTarget', params: { url } })));
    ws.on('message', d => { const r = JSON.parse(d.toString()); if (r.id === 1) { clearTimeout(timer); ws.close(); resolve(r.result?.targetId); } });
    ws.on('error', () => { clearTimeout(timer); resolve(null); });
  });
}

const EXTRACT_RT = `(function(){
  var rtKeys=Object.keys(localStorage).filter(k=>k.includes('refreshtoken'));
  if(!rtKeys.length) return JSON.stringify({error:'no_rt'});
  var rtKey=rtKeys.find(k=>k.includes('|refreshtoken|${CLIENT_ID}|'))||rtKeys[0];
  var rt=JSON.parse(localStorage.getItem(rtKey));
  var acctKeys=JSON.parse(localStorage.getItem('msal.2.account.keys')||'[]');
  if(!acctKeys.length) return JSON.stringify({error:'no_account'});
  var acct=JSON.parse(localStorage.getItem(acctKeys[0]));
  var tid=acct.realm||acct.tenantId||acct.homeAccountId?.split('.')[1]||'';
  return JSON.stringify({refreshToken:rt.data||rt.secret||'',tenantId:tid});
})()`;

async function main() {
  // Check CDP
  try { await fetch(`http://localhost:${CDP_PORT}/json/version`); }
  catch { console.error('❌ CDP not available. Start Edge with --remote-debugging-port=9222'); process.exit(1); }

  // Find Outlook tab
  let pages = await listPages();
  let tab = pages.find(p => (p.url?.includes('outlook.office.com') || p.url?.includes('outlook.cloud.microsoft')) && p.webSocketDebuggerUrl && p.type === 'page');

  if (!tab) {
    console.log('⏳ Opening Outlook...');
    await openTab('https://outlook.office.com/mail/');
    for (let i = 0; i < 20; i++) {
      await new Promise(r => setTimeout(r, 2000));
      pages = await listPages();
      tab = pages.find(p => p.url?.includes('outlook.office') && p.webSocketDebuggerUrl && p.type === 'page');
      if (tab) break;
    }
    if (!tab) { console.error('❌ Outlook did not load'); process.exit(1); }
    await new Promise(r => setTimeout(r, 8000)); // Wait for MSAL
    pages = await listPages();
    tab = pages.find(p => p.url?.includes('outlook.office') && p.webSocketDebuggerUrl && p.type === 'page');
  }

  console.log('✅ Outlook tab found');

  // Extract RT
  const raw = await evalInTab(tab.webSocketDebuggerUrl, EXTRACT_RT);
  if (!raw) { console.error('❌ CDP eval failed'); process.exit(1); }
  const info = JSON.parse(raw);
  if (info.error) { console.error('❌ ' + info.error); process.exit(1); }
  console.log('✅ RT extracted (tenant: ' + info.tenantId.slice(0, 8) + '...)');

  // Exchange for access tokens
  mkdirSync(TOKEN_DIR, { recursive: true });
  for (const { name, audience } of AUDIENCES) {
    const body = new URLSearchParams({ client_id: CLIENT_ID, grant_type: 'refresh_token', refresh_token: info.refreshToken, scope: audience + '/.default offline_access' });
    const resp = await fetch(`https://login.microsoftonline.com/${info.tenantId}/oauth2/v2.0/token`, {
      method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Origin: 'https://outlook.office.com' }, body: body.toString()
    });
    const result = await resp.json();
    if (result.access_token) {
      const exp = decodeJwt(result.access_token)?.exp || 0;
      const mins = Math.floor((exp - Date.now() / 1000) / 60);
      writeFileSync(join(TOKEN_DIR, name + '.json'), JSON.stringify({ access_token: result.access_token, token_type: 'Bearer', saved_at: new Date().toISOString(), source: 'refresh-skill' }, null, 2));
      console.log('✅ ' + name + ': refreshed (' + mins + 'm validity)');
    } else {
      console.error('❌ ' + name + ': ' + (result.error_description || result.error || 'unknown'));
    }
  }
}

main().catch(e => { console.error('❌ ' + e.message); process.exit(1); });
```

## How to Run

The agent should:
1. Write the above script to a temp file: `file_write({ path: "<temp>/refresh-graph.mjs", content: <script> })`
2. Execute: `system_run({ command: "node <temp>/refresh-graph.mjs", timeout: 60000 })`
3. Verify: `outlook_switch_channel("graph")` → `outlook_go_to_inbox()`

## Token File Locations
```
~/.graph-token/tokens/graph-chat.json     — Teams Graph channel
~/.graph-token/tokens/outlook-mail.json   — Outlook Graph channel
```

## Troubleshooting

| Problem | Fix |
|---------|-----|
| CDP not available | Restart Edge with `--remote-debugging-port=9222 --remote-allow-origins=*` |
| `no_rt` error | Outlook Web session expired — log in manually |
| RT exchange failed | Refresh token revoked — close Edge, reopen, log in fresh |
| Tokens expire in ~80 min | Run this skill periodically |

## Verification
After refresh, test with:
```
outlook_switch_channel("graph") → outlook_go_to_inbox()
teams_switch_channel("graph") → teams_read_messages()
```
