feat(spotify): implement milestone M2 auth
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import { access, readFile } from "node:fs/promises";
|
||||
import { constants } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { homedir } from "node:os";
|
||||
|
||||
import type { SpotifyConfig } from "./types.js";
|
||||
|
||||
export interface SpotifyPaths {
|
||||
configDir: string;
|
||||
configPath: string;
|
||||
tokenPath: string;
|
||||
}
|
||||
|
||||
export interface ResolveConfigOptions {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
startDir?: string;
|
||||
homeDir?: string;
|
||||
}
|
||||
|
||||
const skillDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
function pathExists(path: string): Promise<boolean> {
|
||||
return access(path, constants.F_OK).then(() => true, () => false);
|
||||
}
|
||||
|
||||
function expandHome(path: string, homeDir: string): string {
|
||||
if (path === "~") {
|
||||
return homeDir;
|
||||
}
|
||||
if (path.startsWith("~/")) {
|
||||
return join(homeDir, path.slice(2));
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
async function findUpwardCredentialDir(startDir: string): Promise<string | undefined> {
|
||||
let current = resolve(startDir);
|
||||
while (true) {
|
||||
const candidate = join(current, ".clawdbot", "credentials", "spotify");
|
||||
if (await pathExists(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
const parent = dirname(current);
|
||||
if (parent === current) {
|
||||
return undefined;
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveSpotifyPaths(options: ResolveConfigOptions = {}): Promise<SpotifyPaths> {
|
||||
const env = options.env ?? process.env;
|
||||
const homeDir = options.homeDir ?? homedir();
|
||||
|
||||
const candidates: Array<Promise<string | undefined>> = [];
|
||||
if (env.SPOTIFY_CONFIG_DIR) {
|
||||
candidates.push(Promise.resolve(resolve(expandHome(env.SPOTIFY_CONFIG_DIR, homeDir))));
|
||||
}
|
||||
candidates.push(Promise.resolve(join(homeDir, ".openclaw", "workspace", ".clawdbot", "credentials", "spotify")));
|
||||
candidates.push(findUpwardCredentialDir(options.startDir ?? skillDir));
|
||||
candidates.push(Promise.resolve(join(homeDir, ".clawdbot", "credentials", "spotify")));
|
||||
|
||||
for (const candidatePromise of candidates) {
|
||||
const candidate = await candidatePromise;
|
||||
if (candidate && await pathExists(candidate)) {
|
||||
return {
|
||||
configDir: candidate,
|
||||
configPath: join(candidate, "config.json"),
|
||||
tokenPath: join(candidate, "token.json")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const fallback = env.SPOTIFY_CONFIG_DIR
|
||||
? resolve(expandHome(env.SPOTIFY_CONFIG_DIR, homeDir))
|
||||
: join(homeDir, ".openclaw", "workspace", ".clawdbot", "credentials", "spotify");
|
||||
return {
|
||||
configDir: fallback,
|
||||
configPath: join(fallback, "config.json"),
|
||||
tokenPath: join(fallback, "token.json")
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadSpotifyConfig(options: ResolveConfigOptions = {}): Promise<SpotifyConfig> {
|
||||
const paths = await resolveSpotifyPaths(options);
|
||||
let raw: string;
|
||||
try {
|
||||
raw = await readFile(paths.configPath, "utf8");
|
||||
} catch {
|
||||
throw new Error(`Spotify config not found. Create ${paths.configPath} with clientId and redirectUri.`);
|
||||
}
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch {
|
||||
throw new Error("Spotify config is not valid JSON.");
|
||||
}
|
||||
|
||||
if (
|
||||
!parsed ||
|
||||
typeof parsed !== "object" ||
|
||||
typeof (parsed as SpotifyConfig).clientId !== "string" ||
|
||||
typeof (parsed as SpotifyConfig).redirectUri !== "string" ||
|
||||
!(parsed as SpotifyConfig).clientId.trim() ||
|
||||
!(parsed as SpotifyConfig).redirectUri.trim()
|
||||
) {
|
||||
throw new Error("Spotify config must include non-empty clientId and redirectUri.");
|
||||
}
|
||||
|
||||
return {
|
||||
clientId: (parsed as SpotifyConfig).clientId,
|
||||
redirectUri: (parsed as SpotifyConfig).redirectUri
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user