Files
Codewalkers/apps/server/trpc/routers/account.ts
Lukas May 2b06f9627b feat: Add addAccountByToken tRPC mutation with upsert logic
Adds the addAccountByToken procedure to accountProcedures(), which
accepts an email and raw OAuth token, stores the token as claudeAiOauth
credentials, and upserts the account (create or updateAccountAuth based
on findByEmail). Covers the four scenarios with unit tests: new account,
existing account, empty email, and empty token validation errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 20:58:44 +01:00

101 lines
3.2 KiB
TypeScript

/**
* Account Router — list, add, remove, refresh, update auth, mark exhausted, providers
*/
import { z } from 'zod';
import type { ProcedureBuilder } from '../trpc.js';
import { requireAccountRepository } from './_helpers.js';
import { listProviders as listProviderNames } from '../../agent/providers/registry.js';
export function accountProcedures(publicProcedure: ProcedureBuilder) {
return {
listAccounts: publicProcedure
.query(async ({ ctx }) => {
const repo = requireAccountRepository(ctx);
return repo.findAll();
}),
addAccount: publicProcedure
.input(z.object({
email: z.string().min(1),
provider: z.string().default('claude'),
configJson: z.string().optional(),
credentials: z.string().optional(),
}))
.mutation(async ({ ctx, input }) => {
const repo = requireAccountRepository(ctx);
return repo.create({
email: input.email,
provider: input.provider,
configJson: input.configJson,
credentials: input.credentials,
});
}),
removeAccount: publicProcedure
.input(z.object({ id: z.string().min(1) }))
.mutation(async ({ ctx, input }) => {
const repo = requireAccountRepository(ctx);
await repo.delete(input.id);
return { success: true };
}),
refreshAccounts: publicProcedure
.mutation(async ({ ctx }) => {
const repo = requireAccountRepository(ctx);
const cleared = await repo.clearExpiredExhaustion();
return { cleared };
}),
updateAccountAuth: publicProcedure
.input(z.object({
id: z.string().min(1),
configJson: z.string(),
credentials: z.string(),
}))
.mutation(async ({ ctx, input }) => {
const repo = requireAccountRepository(ctx);
return repo.updateAccountAuth(input.id, input.configJson, input.credentials);
}),
markAccountExhausted: publicProcedure
.input(z.object({
id: z.string().min(1),
until: z.string().datetime(),
}))
.mutation(async ({ ctx, input }) => {
const repo = requireAccountRepository(ctx);
return repo.markExhausted(input.id, new Date(input.until));
}),
listProviderNames: publicProcedure
.query(() => {
return listProviderNames();
}),
addAccountByToken: publicProcedure
.input(z.object({
email: z.string().min(1),
provider: z.string().default('claude'),
token: z.string().min(1),
}))
.mutation(async ({ ctx, input }) => {
const repo = requireAccountRepository(ctx);
const credentials = JSON.stringify({ claudeAiOauth: { accessToken: input.token } });
const configJson = JSON.stringify({ hasCompletedOnboarding: true });
const existing = await repo.findByEmail(input.email);
if (existing) {
const account = await repo.updateAccountAuth(existing.id, configJson, credentials);
return { upserted: true, account };
}
const account = await repo.create({
email: input.email,
provider: input.provider,
configJson,
credentials,
});
return { upserted: false, account };
}),
};
}