aboutsummaryrefslogtreecommitdiffstats
path: root/cli/src/tokens.ts
diff options
context:
space:
mode:
authorivar <i@oiee.no>2026-03-09 23:05:38 +0100
committerivar <i@oiee.no>2026-03-09 23:05:38 +0100
commit69448e29a85cad3a94b3be3ad33efbc52764528f (patch)
treec32b8c817322fdf26edbbb3fa75b9505a7020ae8 /cli/src/tokens.ts
parentb35302fa020ec82a9d67a6cb34379d42983d3cfc (diff)
downloadsparebank1-actualbudget-master.tar.xz
sparebank1-actualbudget-master.zip
Add wip cliHEADmaster
Diffstat (limited to 'cli/src/tokens.ts')
-rw-r--r--cli/src/tokens.ts63
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
+}