| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- import { AXIOS_TIMEOUT_MS, COOKIE_NAME, ONE_YEAR_MS } from "@shared/const";
- import { ForbiddenError } from "@shared/_core/errors";
- import axios, { type AxiosInstance } from "axios";
- import { parse as parseCookieHeader } from "cookie";
- import type { Request } from "express";
- import { SignJWT, jwtVerify } from "jose";
- import type { User } from "../../drizzle/schema";
- import * as db from "../db";
- import { ENV } from "./env";
- import type {
- ExchangeTokenRequest,
- ExchangeTokenResponse,
- GetUserInfoResponse,
- GetUserInfoWithJwtRequest,
- GetUserInfoWithJwtResponse,
- } from "./types/manusTypes";
- // Utility function
- const isNonEmptyString = (value: unknown): value is string =>
- typeof value === "string" && value.length > 0;
- export type SessionPayload = {
- openId: string;
- appId: string;
- name: string;
- };
- const EXCHANGE_TOKEN_PATH = `/webdev.v1.WebDevAuthPublicService/ExchangeToken`;
- const GET_USER_INFO_PATH = `/webdev.v1.WebDevAuthPublicService/GetUserInfo`;
- const GET_USER_INFO_WITH_JWT_PATH = `/webdev.v1.WebDevAuthPublicService/GetUserInfoWithJwt`;
- class OAuthService {
- constructor(private client: ReturnType<typeof axios.create>) {
- console.log("[OAuth] Initialized with baseURL:", ENV.oAuthServerUrl);
- if (!ENV.oAuthServerUrl) {
- console.error(
- "[OAuth] ERROR: OAUTH_SERVER_URL is not configured! Set OAUTH_SERVER_URL environment variable."
- );
- }
- }
- private decodeState(state: string): string {
- const redirectUri = atob(state);
- return redirectUri;
- }
- async getTokenByCode(
- code: string,
- state: string
- ): Promise<ExchangeTokenResponse> {
- const payload: ExchangeTokenRequest = {
- clientId: ENV.appId,
- grantType: "authorization_code",
- code,
- redirectUri: this.decodeState(state),
- };
- const { data } = await this.client.post<ExchangeTokenResponse>(
- EXCHANGE_TOKEN_PATH,
- payload
- );
- return data;
- }
- async getUserInfoByToken(
- token: ExchangeTokenResponse
- ): Promise<GetUserInfoResponse> {
- const { data } = await this.client.post<GetUserInfoResponse>(
- GET_USER_INFO_PATH,
- {
- accessToken: token.accessToken,
- }
- );
- return data;
- }
- }
- const createOAuthHttpClient = (): AxiosInstance =>
- axios.create({
- baseURL: ENV.oAuthServerUrl,
- timeout: AXIOS_TIMEOUT_MS,
- });
- class SDKServer {
- private readonly client: AxiosInstance;
- private readonly oauthService: OAuthService;
- constructor(client: AxiosInstance = createOAuthHttpClient()) {
- this.client = client;
- this.oauthService = new OAuthService(this.client);
- }
- private deriveLoginMethod(
- platforms: unknown,
- fallback: string | null | undefined
- ): string | null {
- if (fallback && fallback.length > 0) return fallback;
- if (!Array.isArray(platforms) || platforms.length === 0) return null;
- const set = new Set<string>(
- platforms.filter((p): p is string => typeof p === "string")
- );
- if (set.has("REGISTERED_PLATFORM_EMAIL")) return "email";
- if (set.has("REGISTERED_PLATFORM_GOOGLE")) return "google";
- if (set.has("REGISTERED_PLATFORM_APPLE")) return "apple";
- if (
- set.has("REGISTERED_PLATFORM_MICROSOFT") ||
- set.has("REGISTERED_PLATFORM_AZURE")
- )
- return "microsoft";
- if (set.has("REGISTERED_PLATFORM_GITHUB")) return "github";
- const first = Array.from(set)[0];
- return first ? first.toLowerCase() : null;
- }
- /**
- * Exchange OAuth authorization code for access token
- * @example
- * const tokenResponse = await sdk.exchangeCodeForToken(code, state);
- */
- async exchangeCodeForToken(
- code: string,
- state: string
- ): Promise<ExchangeTokenResponse> {
- return this.oauthService.getTokenByCode(code, state);
- }
- /**
- * Get user information using access token
- * @example
- * const userInfo = await sdk.getUserInfo(tokenResponse.accessToken);
- */
- async getUserInfo(accessToken: string): Promise<GetUserInfoResponse> {
- const data = await this.oauthService.getUserInfoByToken({
- accessToken,
- } as ExchangeTokenResponse);
- const loginMethod = this.deriveLoginMethod(
- (data as any)?.platforms,
- (data as any)?.platform ?? data.platform ?? null
- );
- return {
- ...(data as any),
- platform: loginMethod,
- loginMethod,
- } as GetUserInfoResponse;
- }
- private parseCookies(cookieHeader: string | undefined) {
- if (!cookieHeader) {
- return new Map<string, string>();
- }
- const parsed = parseCookieHeader(cookieHeader);
- return new Map(Object.entries(parsed));
- }
- private getSessionSecret() {
- const secret = ENV.cookieSecret;
- return new TextEncoder().encode(secret);
- }
- /**
- * Create a session token for a Manus user openId
- * @example
- * const sessionToken = await sdk.createSessionToken(userInfo.openId);
- */
- async createSessionToken(
- openId: string,
- options: { expiresInMs?: number; name?: string } = {}
- ): Promise<string> {
- return this.signSession(
- {
- openId,
- appId: ENV.appId,
- name: options.name || "",
- },
- options
- );
- }
- async signSession(
- payload: SessionPayload,
- options: { expiresInMs?: number } = {}
- ): Promise<string> {
- const issuedAt = Date.now();
- const expiresInMs = options.expiresInMs ?? ONE_YEAR_MS;
- const expirationSeconds = Math.floor((issuedAt + expiresInMs) / 1000);
- const secretKey = this.getSessionSecret();
- return new SignJWT({
- openId: payload.openId,
- appId: payload.appId,
- name: payload.name,
- })
- .setProtectedHeader({ alg: "HS256", typ: "JWT" })
- .setExpirationTime(expirationSeconds)
- .sign(secretKey);
- }
- async verifySession(
- cookieValue: string | undefined | null
- ): Promise<{ openId: string; appId: string; name: string } | null> {
- if (!cookieValue) {
- console.warn("[Auth] Missing session cookie");
- return null;
- }
- try {
- const secretKey = this.getSessionSecret();
- const { payload } = await jwtVerify(cookieValue, secretKey, {
- algorithms: ["HS256"],
- });
- const { openId, appId, name } = payload as Record<string, unknown>;
- if (
- !isNonEmptyString(openId) ||
- !isNonEmptyString(appId) ||
- !isNonEmptyString(name)
- ) {
- console.warn("[Auth] Session payload missing required fields");
- return null;
- }
- return {
- openId,
- appId,
- name,
- };
- } catch (error) {
- console.warn("[Auth] Session verification failed", String(error));
- return null;
- }
- }
- async getUserInfoWithJwt(
- jwtToken: string
- ): Promise<GetUserInfoWithJwtResponse> {
- const payload: GetUserInfoWithJwtRequest = {
- jwtToken,
- projectId: ENV.appId,
- };
- const { data } = await this.client.post<GetUserInfoWithJwtResponse>(
- GET_USER_INFO_WITH_JWT_PATH,
- payload
- );
- const loginMethod = this.deriveLoginMethod(
- (data as any)?.platforms,
- (data as any)?.platform ?? data.platform ?? null
- );
- return {
- ...(data as any),
- platform: loginMethod,
- loginMethod,
- } as GetUserInfoWithJwtResponse;
- }
- async authenticateRequest(req: Request): Promise<User> {
- // Regular authentication flow
- const cookies = this.parseCookies(req.headers.cookie);
- const sessionCookie = cookies.get(COOKIE_NAME);
- const session = await this.verifySession(sessionCookie);
- if (!session) {
- throw ForbiddenError("Invalid session cookie");
- }
- const sessionUserId = session.openId;
- const signedInAt = new Date();
- let user = await db.getUserByOpenId(sessionUserId);
- // If user not in DB, sync from OAuth server automatically
- if (!user) {
- try {
- const userInfo = await this.getUserInfoWithJwt(sessionCookie ?? "");
- await db.upsertUser({
- openId: userInfo.openId,
- name: userInfo.name || null,
- email: userInfo.email ?? null,
- loginMethod: userInfo.loginMethod ?? userInfo.platform ?? null,
- lastSignedIn: signedInAt,
- });
- user = await db.getUserByOpenId(userInfo.openId);
- } catch (error) {
- console.error("[Auth] Failed to sync user from OAuth:", error);
- throw ForbiddenError("Failed to sync user info");
- }
- }
- if (!user) {
- throw ForbiddenError("User not found");
- }
- await db.upsertUser({
- openId: user.openId,
- lastSignedIn: signedInAt,
- });
- return user;
- }
- }
- export const sdk = new SDKServer();
|