| 22 | // `origin` header is required — Better Auth rejects state-changing requests |
| 23 | // without it. |
| 24 | export const signInSession = async ( |
| 25 | baseUrl: string, |
| 26 | credentials: { readonly email: string; readonly password: string }, |
| 27 | ): Promise<{ |
| 28 | readonly cookieHeader: string; |
| 29 | readonly cookies: ReadonlyArray<{ readonly name: string; readonly value: string }>; |
| 30 | }> => { |
| 31 | const response = await fetch(new URL("/api/auth/sign-in/email", baseUrl), { |
| 32 | method: "POST", |
| 33 | headers: { "content-type": "application/json", origin: new URL(baseUrl).origin }, |
| 34 | body: JSON.stringify(credentials), |
| 35 | redirect: "manual", |
| 36 | }); |
| 37 | const pairs = (response.headers.getSetCookie?.() ?? []).map((c) => c.split(";")[0]!.trim()); |
| 38 | if (pairs.length === 0) throw new Error(`selfhost: sign-in set no cookie (${response.status})`); |
| 39 | const cookies = pairs.map((pair) => { |
| 40 | const eq = pair.indexOf("="); |
| 41 | return { name: pair.slice(0, eq), value: pair.slice(eq + 1) }; |
| 42 | }); |
| 43 | return { cookieHeader: pairs.join("; "), cookies }; |
| 44 | }; |
| 45 | |
| 46 | // Headless MCP OAuth consent. The self-host serving layer forces |
| 47 | // `prompt=consent` on every MCP authorize (src/auth/force-mcp-consent), so an |