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
}
|