diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index dd1fdf03..5aed7a06 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -7,7 +7,7 @@ import { ctx } from "context"; import { and, desc, eq, isNotNull, or } from "drizzle-orm"; import { Hono } from "hono"; import { cors } from "hono/cors"; -import { createAgent } from "lib/agent"; +import { createAgent, pdsSessionExpired } from "lib/agent"; import { ScrobbleBlockedError, ScrobbleBotFlaggedError, @@ -174,7 +174,7 @@ app.post("/now-playing", async (c) => { `[now-playing] no agent for ${chalk.cyan(did)}, returning 401`, ); c.status(401); - return c.text("Unauthorized"); + return c.json(pdsSessionExpired); } consola.debug( `[now-playing] agent created for ${chalk.cyan(did)}, calling scrobbleTrack`, @@ -271,6 +271,11 @@ app.post("/likes", async (c) => { const { did } = await verifyToken(bearer); const agent = await createAgent(ctx.oauthClient, did); + if (!agent) { + consola.warn(`[likes] no agent for ${chalk.cyan(did)}, returning 401`); + c.status(401); + return c.json(pdsSessionExpired); + } const user = await ctx.db .select() @@ -308,6 +313,11 @@ app.delete("/likes/:sha256", async (c) => { const { did } = await verifyToken(bearer); const agent = await createAgent(ctx.oauthClient, did); + if (!agent) { + consola.warn(`[likes] no agent for ${chalk.cyan(did)}, returning 401`); + c.status(401); + return c.json(pdsSessionExpired); + } const user = await ctx.db .select() @@ -526,8 +536,9 @@ app.post("/tracks", async (c) => { const agent = await createAgent(ctx.oauthClient, did); if (!agent) { + consola.warn(`[tracks] no agent for ${chalk.cyan(did)}, returning 401`); c.status(401); - return c.text("Unauthorized"); + return c.json(pdsSessionExpired); } try { diff --git a/apps/api/src/lib/agent.ts b/apps/api/src/lib/agent.ts index 70ee18fd..c3d7fde6 100644 --- a/apps/api/src/lib/agent.ts +++ b/apps/api/src/lib/agent.ts @@ -4,6 +4,17 @@ import { consola } from "consola"; import extractPdsFromDid from "./extractPdsFromDid"; import { ctx } from "context"; +// Body for 401 responses when the user's OAuth session with their PDS is dead +// (refresh token expired or revoked). Their Rocksky JWT may still be valid, so +// clients must distinguish this from a bad token: stop retrying and prompt the +// user to log in again. +export const pdsSessionExpired = { + status: "unauthorized", + error: "pds_session_expired", + message: + "Your session with your PDS has expired. Please log in to Rocksky again.", +} as const; + export async function createAgent( oauthClient: NodeOAuthClient, did: string,