aboutsummaryrefslogtreecommitdiffstats
path: root/cli/src/tokens.ts
blob: b1918d2d4420871a5644c551f7982a1ff3871d5f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import { mkdirSync, existsSync, readFileSync, writeFileSync } from "node:fs"
import { Temporal } from "temporal-polyfill"
import { CONFIG_DIR, TOKENS_PATH } from "./config"
import type { Sb1Tokens } from "./types"

type StoredTokens = Sb1Tokens & {
    accessTokenCreated: number
    refreshTokenCreated: number
}

export function loadTokens(): StoredTokens | null {
    if (!existsSync(TOKENS_PATH)) return null
    return JSON.parse(readFileSync(TOKENS_PATH, "utf8"))
}

export function saveTokens(tokens: Sb1Tokens): void {
    mkdirSync(CONFIG_DIR, { recursive: true })
    const now = Temporal.Now.instant().epochMilliseconds
    const stored: StoredTokens = { ...tokens, accessTokenCreated: now, refreshTokenCreated: now }
    writeFileSync(TOKENS_PATH, JSON.stringify(stored, null, 2))
}

export async function getAccessToken(clientId: string, clientSecret: string): Promise<string> {
    const stored = loadTokens()
    if (!stored) throw new Error("Not authenticated. Run `sb1-actual auth` first.")

    const now = Temporal.Now.instant()

    const accessExpiry = Temporal.Instant.fromEpochMilliseconds(stored.accessTokenCreated)
        .add({ seconds: stored.expires_in })

    if (Temporal.Instant.compare(now, accessExpiry) < 0) {
        return stored.access_token
    }

    const refreshExpiry = Temporal.Instant.fromEpochMilliseconds(stored.refreshTokenCreated)
        .add({ seconds: stored.refresh_token_expires_in })

    if (Temporal.Instant.compare(now, refreshExpiry) >= 0) {
        throw new Error("Session expired. Run `sb1-actual auth` again.")
    }

    return refreshAccessToken(stored.refresh_token, clientId, clientSecret)
}

async function refreshAccessToken(refreshToken: string, clientId: string, clientSecret: string): Promise<string> {
    console.log("Refreshing access token...")
    const params = new URLSearchParams({
        client_id: clientId,
        client_secret: clientSecret,
        refresh_token: refreshToken,
        grant_type: "refresh_token",
    })
    const res = await fetch("https://api.sparebank1.no/oauth/token", {
        method: "POST",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        body: params,
    })
    if (!res.ok) throw new Error(`Token refresh failed: ${await res.text()}`)
    const tokens = await res.json() as Sb1Tokens
    saveTokens(tokens)
    return tokens.access_token
}