RSS Git Download  Clone
Raw Blame History 4kB 117 lines
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { join, dirname } from 'node:path';
import { homedir } from 'node:os';

const CONFIG_DIR = process.env.P3X_ONENOTE_CONFIG_DIR
    || join(homedir(), '.config', 'p3x-onenote-mcp');
const TOKEN_FILE = join(CONFIG_DIR, 'tokens.json');

const TENANT = process.env.P3X_ONENOTE_TENANT || 'common';
const SCOPES = 'Notes.ReadWrite User.Read offline_access';
const AUTH_BASE = `https://login.microsoftonline.com/${TENANT}/oauth2/v2.0`;

const clientId = () => {
    const id = process.env.P3X_ONENOTE_CLIENT_ID;
    if (!id) {
        throw new Error(
            'Missing P3X_ONENOTE_CLIENT_ID. Register a public client app in Azure Portal '
            + '(App registrations → New → "Public client/native", redirect '
            + 'https://login.microsoftonline.com/common/oauth2/nativeclient) and add delegated '
            + 'Microsoft Graph permissions: Notes.ReadWrite.All, User.Read. Then set '
            + 'P3X_ONENOTE_CLIENT_ID to the Application (client) ID.'
        );
    }
    return id;
};

async function readTokens() {
    try {
        const raw = await readFile(TOKEN_FILE, 'utf8');
        return JSON.parse(raw);
    } catch {
        return null;
    }
}

async function writeTokens(tokens) {
    await mkdir(dirname(TOKEN_FILE), { recursive: true, mode: 0o700 });
    await writeFile(TOKEN_FILE, JSON.stringify(tokens, null, 2), { mode: 0o600 });
}

export async function startDeviceCode() {
    const res = await fetch(`${AUTH_BASE}/devicecode`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: new URLSearchParams({ client_id: clientId(), scope: SCOPES }),
    });
    if (!res.ok) throw new Error(`devicecode: ${res.status} ${await res.text()}`);
    return res.json();
}

export async function pollDeviceCode(deviceCode, intervalSeconds = 5, expiresInSeconds = 900) {
    const deadline = Date.now() + expiresInSeconds * 1000;
    while (Date.now() < deadline) {
        await new Promise(r => setTimeout(r, intervalSeconds * 1000));
        const res = await fetch(`${AUTH_BASE}/token`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: new URLSearchParams({
                grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
                client_id: clientId(),
                device_code: deviceCode,
            }),
        });
        const json = await res.json();
        if (res.ok) {
            const tokens = normalizeTokens(json);
            await writeTokens(tokens);
            return tokens;
        }
        if (json.error === 'authorization_pending') continue;
        if (json.error === 'slow_down') { intervalSeconds += 5; continue; }
        throw new Error(`token: ${json.error} — ${json.error_description || ''}`);
    }
    throw new Error('Device code expired before authorization completed');
}

async function refresh(refreshToken) {
    const res = await fetch(`${AUTH_BASE}/token`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: new URLSearchParams({
            grant_type: 'refresh_token',
            client_id: clientId(),
            refresh_token: refreshToken,
            scope: SCOPES,
        }),
    });
    const json = await res.json();
    if (!res.ok) throw new Error(`refresh: ${json.error} — ${json.error_description || ''}`);
    return normalizeTokens(json);
}

function normalizeTokens(json) {
    return {
        accessToken: json.access_token,
        refreshToken: json.refresh_token,
        expiresAt: Date.now() + (json.expires_in ?? 3600) * 1000,
        scope: json.scope,
    };
}

export async function getAccessToken() {
    const tokens = await readTokens();
    if (!tokens) throw new Error('Not authenticated. Call the `authenticate` tool first.');
    if (Date.now() < tokens.expiresAt - 60_000) return tokens.accessToken;
    const fresh = await refresh(tokens.refreshToken);
    await writeTokens(fresh);
    return fresh.accessToken;
}

export async function isAuthenticated() {
    const tokens = await readTokens();
    return !!tokens?.refreshToken;
}

export { CONFIG_DIR, TOKEN_FILE };