scripts/utils/oauth.ts

78 lines
1.9 KiB
TypeScript

import type { MaybePromise } from '@fuman/utils'
import * as fsp from 'node:fs/promises'
import { z } from 'zod'
export interface OauthStorage {
write: (value: string) => MaybePromise<void>
read: () => MaybePromise<string | null>
}
export class LocalOauthStorage implements OauthStorage {
constructor(private filename: string) {}
async write(value: string) {
await fsp.writeFile(this.filename, value)
}
async read() {
try {
return await fsp.readFile(this.filename, 'utf8')
} catch (e) {
return null
}
}
}
const OauthState = z.object({
accessToken: z.string(),
refreshToken: z.string().optional(),
expiresAt: z.number(),
})
type OauthState = z.infer<typeof OauthState>
export class OauthHandler {
constructor(private params: {
storage: OauthStorage
refreshToken: (refreshToken: string) => MaybePromise<{
accessToken: string
refreshToken: string
expiresIn: number
}>
/** number of milliseconds to subtract from token expiration time */
jitter?: number
}) {
this.params.jitter = this.params.jitter ?? 5000
}
#cache: OauthState | null = null
async readOauthState() {
if (this.#cache) return this.#cache
const value = await this.params.storage.read()
if (!value) return null
return OauthState.parse(JSON.parse(value))
}
async writeOauthState(value: OauthState) {
this.#cache = value
await this.params.storage.write(JSON.stringify(value))
}
async getAccessToken() {
const state = await this.readOauthState()
if (!state) return null
if (state.expiresAt < Date.now() + this.params.jitter!) {
if (!state.refreshToken) return null
const { accessToken, refreshToken, expiresIn } = await this.params.refreshToken(state.refreshToken)
await this.writeOauthState({
accessToken,
refreshToken,
expiresAt: Date.now() + expiresIn * 1000,
})
return accessToken
}
return state.accessToken
}
}