diff options
| author | ivar <i@oiee.no> | 2026-03-09 23:05:38 +0100 |
|---|---|---|
| committer | ivar <i@oiee.no> | 2026-03-09 23:05:38 +0100 |
| commit | 69448e29a85cad3a94b3be3ad33efbc52764528f (patch) | |
| tree | c32b8c817322fdf26edbbb3fa75b9505a7020ae8 /cli/src/tokens.ts | |
| parent | b35302fa020ec82a9d67a6cb34379d42983d3cfc (diff) | |
| download | sparebank1-actualbudget-master.tar.xz sparebank1-actualbudget-master.zip | |
Diffstat (limited to 'cli/src/tokens.ts')
| -rw-r--r-- | cli/src/tokens.ts | 63 |
1 files changed, 63 insertions, 0 deletions
diff --git a/cli/src/tokens.ts b/cli/src/tokens.ts new file mode 100644 index 0000000..b1918d2 --- /dev/null +++ b/cli/src/tokens.ts @@ -0,0 +1,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 +} |
