mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-16 14:01:09 +00:00
Fix Foundry UI bugs: org names, sessions, and repo selection (#250)
* Fix Foundry auth: migrate to Better Auth adapter, fix access token retrieval - Remove @ts-nocheck from better-auth.ts, auth-user/index.ts, app-shell.ts and fix all type errors - Fix getAccessTokenForSession: read GitHub token directly from account record instead of calling Better Auth's internal /get-access-token endpoint which returns 403 on server-side calls - Re-implement workspaceAuth helper functions (workspaceAuthColumn, normalizeAuthValue, workspaceAuthClause, workspaceAuthWhere) that were accidentally deleted - Remove all retry logic (withRetries, isRetryableAppActorError) - Implement CORS origin allowlist from configured environment - Document cachedAppWorkspace singleton pattern - Add inline org sync fallback in buildAppSnapshot for post-OAuth flow - Add no-retry rule to CLAUDE.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add Foundry dev panel from fix-git-data branch Port the dev panel component that was left out when PR #243 was replaced by PR #247. Adapted to remove runtime/mock-debug references that don't exist on the current branch. - Toggle with Shift+D, persists visibility to localStorage - Shows context, session, GitHub sync status sections - Dev-only (import.meta.env.DEV) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add full Docker image defaults, fix actor deadlocks, and improve dev experience - Add Dockerfile.full and --all flag to install-agent CLI for pre-built images - Centralize Docker image constant (FULL_IMAGE) pinned to 0.3.1-full - Remove examples/shared/Dockerfile{,.dev} and daytona snapshot example - Expand Docker docs with full runnable Dockerfile - Fix self-deadlock in createWorkbenchSession (fire-and-forget provisioning) - Audit and convert 12 task actions from wait:true to wait:false - Add bun --hot for dev backend hot reload - Remove --force from pnpm install in dev Dockerfile for faster startup - Add env_file support to compose.dev.yaml for automatic credential loading - Add mock frontend compose config and dev panel - Update CLAUDE.md with wait:true policy and dev environment setup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * WIP: async action fixes and interest manager Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix Foundry UI bugs: org names, hanging sessions, and wrong repo creation - Fix org display name using GitHub description instead of name field - Fix createWorkbenchSession hanging when sandbox is provisioning - Fix auto-session creation retry storm on errors - Fix task creation using wrong repo due to React state race conditions - Remove Bun hot-reload from backend Dockerfile (causes port drift) - Add GitHub sync/install status to dev panel Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
58c54156f1
commit
d8b8b49f37
88 changed files with 9252 additions and 1933 deletions
|
|
@ -262,11 +262,11 @@ export class GitHubAppClient {
|
|||
}
|
||||
|
||||
async listOrganizations(accessToken: string): Promise<GitHubOrgIdentity[]> {
|
||||
const organizations = await this.paginate<{ id: number; login: string; description?: string | null }>("/user/orgs?per_page=100", accessToken);
|
||||
const organizations = await this.paginate<{ id: number; login: string; name?: string | null }>("/user/orgs?per_page=100", accessToken);
|
||||
return organizations.map((organization) => ({
|
||||
id: String(organization.id),
|
||||
login: organization.login,
|
||||
name: organization.description?.trim() || organization.login,
|
||||
name: organization.name?.trim() || organization.login,
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
|||
533
foundry/packages/backend/src/services/better-auth.ts
Normal file
533
foundry/packages/backend/src/services/better-auth.ts
Normal file
|
|
@ -0,0 +1,533 @@
|
|||
import { betterAuth } from "better-auth";
|
||||
import { createAdapterFactory } from "better-auth/adapters";
|
||||
import { APP_SHELL_WORKSPACE_ID } from "../actors/workspace/app-shell.js";
|
||||
import { authUserKey, workspaceKey } from "../actors/keys.js";
|
||||
import { logger } from "../logging.js";
|
||||
|
||||
const AUTH_BASE_PATH = "/v1/auth";
|
||||
const SESSION_COOKIE = "better-auth.session_token";
|
||||
|
||||
let betterAuthService: BetterAuthService | null = null;
|
||||
|
||||
function requireEnv(name: string): string {
|
||||
const value = process.env[name]?.trim();
|
||||
if (!value) {
|
||||
throw new Error(`${name} is required`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function stripTrailingSlash(value: string): string {
|
||||
return value.replace(/\/$/, "");
|
||||
}
|
||||
|
||||
function buildCookieHeaders(sessionToken: string): Headers {
|
||||
return new Headers({
|
||||
cookie: `${SESSION_COOKIE}=${encodeURIComponent(sessionToken)}`,
|
||||
});
|
||||
}
|
||||
|
||||
async function readJsonSafe(response: Response): Promise<any> {
|
||||
const text = await response.text();
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
async function callAuthEndpoint(auth: any, url: string, init?: RequestInit): Promise<Response> {
|
||||
return await auth.handler(new Request(url, init));
|
||||
}
|
||||
|
||||
function resolveRouteUserId(workspace: any, resolved: any): string | null {
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
if (typeof resolved === "string") {
|
||||
return resolved;
|
||||
}
|
||||
if (typeof resolved.userId === "string" && resolved.userId.length > 0) {
|
||||
return resolved.userId;
|
||||
}
|
||||
if (typeof resolved.id === "string" && resolved.id.length > 0) {
|
||||
return resolved.id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export interface BetterAuthService {
|
||||
auth: any;
|
||||
resolveSession(headers: Headers): Promise<{ session: any; user: any } | null>;
|
||||
signOut(headers: Headers): Promise<Response>;
|
||||
getAuthState(sessionId: string): Promise<any | null>;
|
||||
upsertUserProfile(userId: string, patch: Record<string, unknown>): Promise<any>;
|
||||
setActiveOrganization(sessionId: string, activeOrganizationId: string | null): Promise<any>;
|
||||
getAccessTokenForSession(sessionId: string): Promise<{ accessToken: string; scopes: string[] } | null>;
|
||||
}
|
||||
|
||||
export function initBetterAuthService(actorClient: any, options: { apiUrl: string; appUrl: string }): BetterAuthService {
|
||||
if (betterAuthService) {
|
||||
return betterAuthService;
|
||||
}
|
||||
|
||||
// getOrCreate is intentional here: the adapter runs during Better Auth callbacks
|
||||
// which can fire before any explicit create path. The app workspace and auth user
|
||||
// actors must exist by the time the adapter needs them.
|
||||
const appWorkspace = () =>
|
||||
actorClient.workspace.getOrCreate(workspaceKey(APP_SHELL_WORKSPACE_ID), {
|
||||
createWithInput: APP_SHELL_WORKSPACE_ID,
|
||||
});
|
||||
|
||||
// getOrCreate is intentional: Better Auth creates user records during OAuth
|
||||
// callbacks, so the auth-user actor must be lazily provisioned on first access.
|
||||
const getAuthUser = async (userId: string) =>
|
||||
await actorClient.authUser.getOrCreate(authUserKey(userId), {
|
||||
createWithInput: { userId },
|
||||
});
|
||||
|
||||
const adapter = createAdapterFactory({
|
||||
config: {
|
||||
adapterId: "rivetkit-actor",
|
||||
adapterName: "RivetKit Actor Adapter",
|
||||
supportsBooleans: false,
|
||||
supportsDates: false,
|
||||
supportsJSON: false,
|
||||
},
|
||||
adapter: ({ transformInput, transformOutput, transformWhereClause }) => {
|
||||
const resolveUserIdForQuery = async (model: string, where?: any[], data?: Record<string, unknown>): Promise<string | null> => {
|
||||
const clauses = where ?? [];
|
||||
const direct = (field: string) => clauses.find((entry) => entry.field === field)?.value;
|
||||
|
||||
if (model === "user") {
|
||||
const fromId = direct("id") ?? data?.id;
|
||||
if (typeof fromId === "string" && fromId.length > 0) {
|
||||
return fromId;
|
||||
}
|
||||
const email = direct("email");
|
||||
if (typeof email === "string" && email.length > 0) {
|
||||
const workspace = await appWorkspace();
|
||||
const resolved = await workspace.authFindEmailIndex({ email: email.toLowerCase() });
|
||||
return resolveRouteUserId(workspace, resolved);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (model === "session") {
|
||||
const fromUserId = direct("userId") ?? data?.userId;
|
||||
if (typeof fromUserId === "string" && fromUserId.length > 0) {
|
||||
return fromUserId;
|
||||
}
|
||||
const sessionId = direct("id") ?? data?.id;
|
||||
const sessionToken = direct("token") ?? data?.token;
|
||||
if (typeof sessionId === "string" || typeof sessionToken === "string") {
|
||||
const workspace = await appWorkspace();
|
||||
const resolved = await workspace.authFindSessionIndex({
|
||||
...(typeof sessionId === "string" ? { sessionId } : {}),
|
||||
...(typeof sessionToken === "string" ? { sessionToken } : {}),
|
||||
});
|
||||
return resolveRouteUserId(workspace, resolved);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (model === "account") {
|
||||
const fromUserId = direct("userId") ?? data?.userId;
|
||||
if (typeof fromUserId === "string" && fromUserId.length > 0) {
|
||||
return fromUserId;
|
||||
}
|
||||
const accountRecordId = direct("id") ?? data?.id;
|
||||
const providerId = direct("providerId") ?? data?.providerId;
|
||||
const accountId = direct("accountId") ?? data?.accountId;
|
||||
const workspace = await appWorkspace();
|
||||
if (typeof accountRecordId === "string" && accountRecordId.length > 0) {
|
||||
const resolved = await workspace.authFindAccountIndex({ id: accountRecordId });
|
||||
return resolveRouteUserId(workspace, resolved);
|
||||
}
|
||||
if (typeof providerId === "string" && providerId.length > 0 && typeof accountId === "string" && accountId.length > 0) {
|
||||
const resolved = await workspace.authFindAccountIndex({ providerId, accountId });
|
||||
return resolveRouteUserId(workspace, resolved);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const ensureWorkspaceVerification = async (method: string, payload: Record<string, unknown>) => {
|
||||
const workspace = await appWorkspace();
|
||||
return await workspace[method](payload);
|
||||
};
|
||||
|
||||
return {
|
||||
options: {
|
||||
useDatabaseGeneratedIds: false,
|
||||
},
|
||||
|
||||
create: async ({ model, data }) => {
|
||||
const transformed = await transformInput(data, model, "create", true);
|
||||
if (model === "verification") {
|
||||
return await ensureWorkspaceVerification("authCreateVerification", { data: transformed });
|
||||
}
|
||||
|
||||
const userId = await resolveUserIdForQuery(model, undefined, transformed);
|
||||
if (!userId) {
|
||||
throw new Error(`Unable to resolve auth actor for create(${model})`);
|
||||
}
|
||||
|
||||
const userActor = await getAuthUser(userId);
|
||||
const created = await userActor.createAuthRecord({ model, data: transformed });
|
||||
const workspace = await appWorkspace();
|
||||
|
||||
if (model === "user" && typeof transformed.email === "string" && transformed.email.length > 0) {
|
||||
await workspace.authUpsertEmailIndex({
|
||||
email: transformed.email.toLowerCase(),
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
if (model === "session") {
|
||||
await workspace.authUpsertSessionIndex({
|
||||
sessionId: String(created.id),
|
||||
sessionToken: String(created.token),
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
if (model === "account") {
|
||||
await workspace.authUpsertAccountIndex({
|
||||
id: String(created.id),
|
||||
providerId: String(created.providerId),
|
||||
accountId: String(created.accountId),
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
return (await transformOutput(created, model)) as any;
|
||||
},
|
||||
|
||||
findOne: async ({ model, where, join }) => {
|
||||
const transformedWhere = transformWhereClause({ model, where, action: "findOne" });
|
||||
if (model === "verification") {
|
||||
return await ensureWorkspaceVerification("authFindOneVerification", { where: transformedWhere, join });
|
||||
}
|
||||
|
||||
const userId = await resolveUserIdForQuery(model, transformedWhere);
|
||||
if (!userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const userActor = await getAuthUser(userId);
|
||||
const found = await userActor.findOneAuthRecord({ model, where: transformedWhere, join });
|
||||
return found ? ((await transformOutput(found, model, undefined, join)) as any) : null;
|
||||
},
|
||||
|
||||
findMany: async ({ model, where, limit, sortBy, offset, join }) => {
|
||||
const transformedWhere = transformWhereClause({ model, where, action: "findMany" });
|
||||
if (model === "verification") {
|
||||
return await ensureWorkspaceVerification("authFindManyVerification", {
|
||||
where: transformedWhere,
|
||||
limit,
|
||||
sortBy,
|
||||
offset,
|
||||
join,
|
||||
});
|
||||
}
|
||||
|
||||
if (model === "session") {
|
||||
const tokenClause = transformedWhere?.find((entry: any) => entry.field === "token" && entry.operator === "in");
|
||||
if (tokenClause && Array.isArray(tokenClause.value)) {
|
||||
const workspace = await appWorkspace();
|
||||
const resolved = await Promise.all(
|
||||
(tokenClause.value as string[]).map(async (sessionToken: string) => ({
|
||||
sessionToken,
|
||||
route: await workspace.authFindSessionIndex({ sessionToken }),
|
||||
})),
|
||||
);
|
||||
const byUser = new Map<string, string[]>();
|
||||
for (const item of resolved) {
|
||||
if (!item.route?.userId) {
|
||||
continue;
|
||||
}
|
||||
const tokens = byUser.get(item.route.userId) ?? [];
|
||||
tokens.push(item.sessionToken);
|
||||
byUser.set(item.route.userId, tokens);
|
||||
}
|
||||
|
||||
const rows = [];
|
||||
for (const [userId, tokens] of byUser) {
|
||||
const userActor = await getAuthUser(userId);
|
||||
const scopedWhere = transformedWhere.map((entry: any) =>
|
||||
entry.field === "token" && entry.operator === "in" ? { ...entry, value: tokens } : entry,
|
||||
);
|
||||
const found = await userActor.findManyAuthRecords({ model, where: scopedWhere, limit, sortBy, offset, join });
|
||||
rows.push(...found);
|
||||
}
|
||||
return await Promise.all(rows.map(async (row: any) => await transformOutput(row, model, undefined, join)));
|
||||
}
|
||||
}
|
||||
|
||||
const userId = await resolveUserIdForQuery(model, transformedWhere);
|
||||
if (!userId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const userActor = await getAuthUser(userId);
|
||||
const found = await userActor.findManyAuthRecords({ model, where: transformedWhere, limit, sortBy, offset, join });
|
||||
return await Promise.all(found.map(async (row: any) => await transformOutput(row, model, undefined, join)));
|
||||
},
|
||||
|
||||
update: async ({ model, where, update }) => {
|
||||
const transformedWhere = transformWhereClause({ model, where, action: "update" });
|
||||
const transformedUpdate = (await transformInput(update as Record<string, unknown>, model, "update", true)) as Record<string, unknown>;
|
||||
if (model === "verification") {
|
||||
return await ensureWorkspaceVerification("authUpdateVerification", { where: transformedWhere, update: transformedUpdate });
|
||||
}
|
||||
|
||||
const userId = await resolveUserIdForQuery(model, transformedWhere, transformedUpdate);
|
||||
if (!userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const userActor = await getAuthUser(userId);
|
||||
const before =
|
||||
model === "user"
|
||||
? await userActor.findOneAuthRecord({ model, where: transformedWhere })
|
||||
: model === "account"
|
||||
? await userActor.findOneAuthRecord({ model, where: transformedWhere })
|
||||
: model === "session"
|
||||
? await userActor.findOneAuthRecord({ model, where: transformedWhere })
|
||||
: null;
|
||||
const updated = await userActor.updateAuthRecord({ model, where: transformedWhere, update: transformedUpdate });
|
||||
const workspace = await appWorkspace();
|
||||
|
||||
if (model === "user" && updated) {
|
||||
if (before?.email && before.email !== updated.email) {
|
||||
await workspace.authDeleteEmailIndex({ email: before.email.toLowerCase() });
|
||||
}
|
||||
if (updated.email) {
|
||||
await workspace.authUpsertEmailIndex({ email: updated.email.toLowerCase(), userId });
|
||||
}
|
||||
}
|
||||
|
||||
if (model === "session" && updated) {
|
||||
await workspace.authUpsertSessionIndex({
|
||||
sessionId: String(updated.id),
|
||||
sessionToken: String(updated.token),
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
if (model === "account" && updated) {
|
||||
await workspace.authUpsertAccountIndex({
|
||||
id: String(updated.id),
|
||||
providerId: String(updated.providerId),
|
||||
accountId: String(updated.accountId),
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
return updated ? ((await transformOutput(updated, model)) as any) : null;
|
||||
},
|
||||
|
||||
updateMany: async ({ model, where, update }) => {
|
||||
const transformedWhere = transformWhereClause({ model, where, action: "updateMany" });
|
||||
const transformedUpdate = (await transformInput(update as Record<string, unknown>, model, "update", true)) as Record<string, unknown>;
|
||||
if (model === "verification") {
|
||||
return await ensureWorkspaceVerification("authUpdateManyVerification", { where: transformedWhere, update: transformedUpdate });
|
||||
}
|
||||
|
||||
const userId = await resolveUserIdForQuery(model, transformedWhere, transformedUpdate);
|
||||
if (!userId) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const userActor = await getAuthUser(userId);
|
||||
return await userActor.updateManyAuthRecords({ model, where: transformedWhere, update: transformedUpdate });
|
||||
},
|
||||
|
||||
delete: async ({ model, where }) => {
|
||||
const transformedWhere = transformWhereClause({ model, where, action: "delete" });
|
||||
if (model === "verification") {
|
||||
await ensureWorkspaceVerification("authDeleteVerification", { where: transformedWhere });
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = await resolveUserIdForQuery(model, transformedWhere);
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userActor = await getAuthUser(userId);
|
||||
const workspace = await appWorkspace();
|
||||
const before = await userActor.findOneAuthRecord({ model, where: transformedWhere });
|
||||
await userActor.deleteAuthRecord({ model, where: transformedWhere });
|
||||
|
||||
if (model === "session" && before) {
|
||||
await workspace.authDeleteSessionIndex({
|
||||
sessionId: before.id,
|
||||
sessionToken: before.token,
|
||||
});
|
||||
}
|
||||
|
||||
if (model === "account" && before) {
|
||||
await workspace.authDeleteAccountIndex({
|
||||
id: before.id,
|
||||
providerId: before.providerId,
|
||||
accountId: before.accountId,
|
||||
});
|
||||
}
|
||||
|
||||
if (model === "user" && before?.email) {
|
||||
await workspace.authDeleteEmailIndex({ email: before.email.toLowerCase() });
|
||||
}
|
||||
},
|
||||
|
||||
deleteMany: async ({ model, where }) => {
|
||||
const transformedWhere = transformWhereClause({ model, where, action: "deleteMany" });
|
||||
if (model === "verification") {
|
||||
return await ensureWorkspaceVerification("authDeleteManyVerification", { where: transformedWhere });
|
||||
}
|
||||
|
||||
if (model === "session") {
|
||||
const userId = await resolveUserIdForQuery(model, transformedWhere);
|
||||
if (!userId) {
|
||||
return 0;
|
||||
}
|
||||
const userActor = await getAuthUser(userId);
|
||||
const workspace = await appWorkspace();
|
||||
const sessions = await userActor.findManyAuthRecords({ model, where: transformedWhere, limit: 5000 });
|
||||
const deleted = await userActor.deleteManyAuthRecords({ model, where: transformedWhere });
|
||||
for (const session of sessions) {
|
||||
await workspace.authDeleteSessionIndex({
|
||||
sessionId: session.id,
|
||||
sessionToken: session.token,
|
||||
});
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
|
||||
const userId = await resolveUserIdForQuery(model, transformedWhere);
|
||||
if (!userId) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const userActor = await getAuthUser(userId);
|
||||
const deleted = await userActor.deleteManyAuthRecords({ model, where: transformedWhere });
|
||||
return deleted;
|
||||
},
|
||||
|
||||
count: async ({ model, where }) => {
|
||||
const transformedWhere = transformWhereClause({ model, where, action: "count" });
|
||||
if (model === "verification") {
|
||||
return await ensureWorkspaceVerification("authCountVerification", { where: transformedWhere });
|
||||
}
|
||||
|
||||
const userId = await resolveUserIdForQuery(model, transformedWhere);
|
||||
if (!userId) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const userActor = await getAuthUser(userId);
|
||||
return await userActor.countAuthRecords({ model, where: transformedWhere });
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const auth = betterAuth({
|
||||
baseURL: stripTrailingSlash(process.env.BETTER_AUTH_URL ?? options.apiUrl),
|
||||
basePath: AUTH_BASE_PATH,
|
||||
secret: requireEnv("BETTER_AUTH_SECRET"),
|
||||
database: adapter,
|
||||
trustedOrigins: [stripTrailingSlash(options.appUrl), stripTrailingSlash(options.apiUrl)],
|
||||
session: {
|
||||
cookieCache: {
|
||||
enabled: true,
|
||||
maxAge: 5 * 60,
|
||||
strategy: "compact",
|
||||
},
|
||||
},
|
||||
socialProviders: {
|
||||
github: {
|
||||
clientId: requireEnv("GITHUB_CLIENT_ID"),
|
||||
clientSecret: requireEnv("GITHUB_CLIENT_SECRET"),
|
||||
scope: ["read:org", "repo"],
|
||||
redirectURI: process.env.GITHUB_REDIRECT_URI || undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
betterAuthService = {
|
||||
auth,
|
||||
|
||||
async resolveSession(headers: Headers) {
|
||||
return (await auth.api.getSession({ headers })) ?? null;
|
||||
},
|
||||
|
||||
async signOut(headers: Headers) {
|
||||
return await callAuthEndpoint(auth, `${stripTrailingSlash(process.env.BETTER_AUTH_URL ?? options.apiUrl)}${AUTH_BASE_PATH}/sign-out`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
});
|
||||
},
|
||||
|
||||
async getAuthState(sessionId: string) {
|
||||
const workspace = await appWorkspace();
|
||||
const route = await workspace.authFindSessionIndex({ sessionId });
|
||||
if (!route?.userId) {
|
||||
return null;
|
||||
}
|
||||
const userActor = await getAuthUser(route.userId);
|
||||
return await userActor.getAppAuthState({ sessionId });
|
||||
},
|
||||
|
||||
async upsertUserProfile(userId: string, patch: Record<string, unknown>) {
|
||||
const userActor = await getAuthUser(userId);
|
||||
return await userActor.upsertUserProfile({ userId, patch });
|
||||
},
|
||||
|
||||
async setActiveOrganization(sessionId: string, activeOrganizationId: string | null) {
|
||||
const authState = await this.getAuthState(sessionId);
|
||||
if (!authState?.user?.id) {
|
||||
throw new Error(`Unknown auth session ${sessionId}`);
|
||||
}
|
||||
const userActor = await getAuthUser(authState.user.id);
|
||||
return await userActor.upsertSessionState({ sessionId, activeOrganizationId });
|
||||
},
|
||||
|
||||
async getAccessTokenForSession(sessionId: string) {
|
||||
// Read the GitHub access token directly from the account record stored in the
|
||||
// auth user actor. Better Auth's internal /get-access-token endpoint requires
|
||||
// session middleware resolution which fails for server-side internal calls (403),
|
||||
// so we bypass it and read the stored token from our adapter layer directly.
|
||||
const authState = await this.getAuthState(sessionId);
|
||||
if (!authState?.user?.id || !authState?.accounts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const githubAccount = authState.accounts.find((account: any) => account.providerId === "github");
|
||||
if (!githubAccount?.accessToken) {
|
||||
logger.warn({ sessionId, userId: authState.user.id }, "get_access_token_no_github_account");
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken: githubAccount.accessToken,
|
||||
scopes: githubAccount.scope ? githubAccount.scope.split(/[, ]+/) : [],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
return betterAuthService;
|
||||
}
|
||||
|
||||
export function getBetterAuthService(): BetterAuthService {
|
||||
if (!betterAuthService) {
|
||||
throw new Error("BetterAuth service is not initialized");
|
||||
}
|
||||
return betterAuthService;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue