import got from "got"; import * as client from "openid-client"; import { Api } from "./api.js"; import { CDN } from "./cdn.js"; import { Creator } from "./creator.js"; import { User } from "./user.js"; import { TokenEndpointResponse } from "openid-client"; import { Content } from "./content.js"; import { Core } from "./Core.js"; export const version = "5.2.8"; export type AuthToken = TokenEndpointResponse & { expiresEpoch?: number }; export type OnDeviceCode = (response: client.DeviceAuthorizationResponse) => any; export type OnAuthToken = (authToken: AuthToken) => void; export type AuthConfig = { authToken?: AuthToken; onAuthToken?: OnAuthToken; onDeviceCode: OnDeviceCode; clientId: string; clientSecret?: string; serverUrl?: string; }; export type FloatplaneSettings = { authConfig: AuthConfig; userAgent?: string; baseUrl?: string; }; export class Floatplane { public got: typeof got; public user: User; public api: Api; public creator: Creator; public cdn: CDN; public content: Content; private oauthConfig?: client.Configuration; public readonly authConfig: AuthConfig; public readonly userAgent: string = `FloatplaneAPI/${version} (Inrix, +https://github.com/Inrixia/floatplaneAPI.ts), CFNetwork`; constructor({ authConfig, baseUrl, userAgent }: FloatplaneSettings) { this.authConfig = authConfig; baseUrl ??= Core.BaseUrl; const headers = { "User-Agent": userAgent ?? this.userAgent, accept: "application/json", connection: "keep-alive", }; this.got = got.extend({ headers, retry: { limit: 5, // Maximum number of retries }, hooks: { beforeRequest: [ async ({ headers, url, prefixUrl }) => { if (!new URL(url ?? prefixUrl).href.startsWith(baseUrl)) return; await this.refreshAuthToken(); if (this.authToken?.access_token) headers.authorization = `Bearer ${this.authToken.access_token}`; }, ], }, }); this.user = new User(this.got, baseUrl); this.api = new Api(this.got, baseUrl); this.creator = new Creator(this.got, baseUrl); this.cdn = new CDN(this.got, baseUrl); this.content = new Content(this.got, baseUrl); } public extend(...params: Parameters) { this.got = this.got.extend(...params); this.user.got = this.got; this.api.got = this.got; this.creator.got = this.got; this.cdn.got = this.got; this.content.got = this.got; } private get authToken() { return this.authConfig.authToken; } private set authToken(authToken: AuthToken | null | undefined) { if (authToken) authToken.expiresEpoch = Date.now() + (authToken?.expires_in ?? 30) * 1000; this.authConfig.authToken = authToken ?? undefined; if (this.authConfig.onAuthToken && authToken) this.authConfig.onAuthToken(authToken); } /** * Time untl token expiry in ms */ public authTokenExpiresIn(authToken: AuthToken | null | undefined = this.authToken): number { const expiresEpoch = authToken?.expiresEpoch; if (!expiresEpoch) return -1; return Math.max(expiresEpoch - Date.now(), 0); } public refreshAuthToken = async () => { if (!this.authToken) return this.login(); if (this.authTokenExpiresIn() < 60000) { const refreshToken = this.authToken.refresh_token; if (!refreshToken) throw new Error("No refresh token available to refresh OAuth token!"); this.oauthConfig ??= await client.discovery( new URL(this.authConfig.serverUrl ?? "https://auth.floatplane.com/realms/floatplane"), this.authConfig.clientId, this.authConfig.clientSecret, ); const refreshedTokenSet = await client.refreshTokenGrant(this.oauthConfig, refreshToken); if (refreshedTokenSet.access_token === undefined) throw new Error("No access token received when refreshing token!"); this.authToken = refreshedTokenSet; } }; /** * Login to floatplane so future requests are authenticated using the Device flow * @returns {Promise} User object. */ deviceLogin = async (onDeviceCode = this.authConfig.onDeviceCode) => { this.oauthConfig ??= await client.discovery( new URL(this.authConfig.serverUrl ?? "https://auth.floatplane.com/realms/floatplane"), this.authConfig.clientId, this.authConfig.clientSecret, ); if (!this.authToken) { const response = await client.initiateDeviceAuthorization(this.oauthConfig, { scope: "openid profile email offline_access" }); await onDeviceCode(response); const authToken = await client.pollDeviceAuthorizationGrant(this.oauthConfig, response); if (authToken.access_token === undefined) throw new Error("No access token received from device authorization flow!"); this.authToken = authToken; } return this.user.self(); }; private login = this.deviceLogin; /** * Returns true if authenticated or Error if not. */ isAuthenticated = async (): Promise => this.user .self() .then((): true => true) .catch((err: Error) => err); }