api: oauth for widgest
vi did:web:vt3e.cat
Sun, 05 Jul 2026 22:53:07 +0100
1 files changed,
65 insertions(+),
5 deletions(-)
jump to
M
apps/api/src/routes/auth.ts
→
apps/api/src/routes/auth.ts
@@ -1,27 +1,41 @@
import type { Error, ProvisionalTokenResponse } from "@purrkit/types"; -import { IS_PROD } from "@stealth-developers/config"; -import db, { sessions, upsertUser } from "@stealth-developers/db"; +import config, { IS_PROD } from "@stealth-developers/config"; +import db, { getActor, sessions, upsertUser } from "@stealth-developers/db"; import Elysia, { t } from "elysia"; import { createClient } from "../discord"; import { CLIENT_ID, CLIENT_SECRET, FRONTEND_URL, REDIRECT_URI } from "../lib/constants"; + +const SCOPES = { + regular: ["identify", "guilds"], + widgets: ["sdk.social_layer", "sdk.social_layer_presence", "openid"], +}; export const authRoutes = new Elysia().group("/auth", (app) => app .get( "/login", - ({ redirect }) => { - const redirectUri = `https://discord.com/oauth2/authorize?client_id=${CLIENT_ID}&response_type=code&redirect_uri=${encodeURIComponent(REDIRECT_URI)}&scope=identify+guilds`; + ({ query: { actorId }, redirect }) => { + const isWidgetFlow = !!actorId; + const scopes = isWidgetFlow ? SCOPES.widgets : SCOPES.regular; + + let redirectUri = `https://discord.com/oauth2/authorize?client_id=${CLIENT_ID}&response_type=code&redirect_uri=${encodeURIComponent(REDIRECT_URI)}&scope=${encodeURIComponent(scopes.join(" "))}`; + + if (isWidgetFlow) redirectUri += `&state=${encodeURIComponent(actorId)}`; + return redirect(redirectUri, 302); }, { tags: ["auth"], + query: t.Object({ + actorId: t.Optional(t.String()), + }), }, ) .get( "/callback", - async ({ query: { code }, cookie: { session_id }, redirect }) => { + async ({ query: { code, state }, cookie: { session_id }, redirect }) => { if (!session_id) throw new Error("No session ID"); const params = new URLSearchParams({@@ -32,6 +46,51 @@ grant_type: "authorization_code",
redirect_uri: REDIRECT_URI, }); + if (state) { + const actor = await getActor(Number(state)); + if (!actor) throw new Error("Actor not found"); + if (!actor.robloxId) throw new Error("Actor does not have a linked Roblox account"); + + const botToken = config.discord.token; + if (!botToken) throw new Error("config.discord.token is not defined"); + + const provisionResponse = await fetch( + "https://discord.com/api/v10/partner-sdk/token/bot", + { + method: "POST", + headers: { + Authorization: `Bot ${botToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + external_user_id: actor.robloxId, + }), + }, + ); + + if (!provisionResponse.ok) { + const responseText = await provisionResponse.text(); + let isNonProvisional = false; + + try { + const provisionErrorData = JSON.parse(responseText) as { + code?: number; + message?: string; + }; + if (provisionErrorData.code === 530010) { + isNonProvisional = true; + } + } catch {} + + if (!isNonProvisional) + throw new Error(`Provisional account creation failed: ${responseText}`); + } else { + const provisionData = (await provisionResponse.json()) as { access_token: string }; + params.append("external_auth_type", "DISCORD_BOT_ISSUED_ACCESS_TOKEN"); + params.append("external_auth_token", provisionData.access_token); + } + } + const tokenResponse = await fetch("https://discord.com/api/oauth2/token", { method: "POST", body: params,@@ -84,6 +143,7 @@ {
tags: ["auth"], query: t.Object({ code: t.String(), + state: t.Optional(t.String()), }), }, ),