export const apiMethodHttpMethods = [ "GET", "POST", "PUT", "PATCH", "DELETE", ] as const; export type ApiMethodHttpMethod = (typeof apiMethodHttpMethods)[number]; export interface ApiMethodOptions { path: string; method?: ApiMethodHttpMethod; isServerOnly?: boolean; isClientOnly?: boolean; isExternalOnly?: boolean; noResult?: boolean; requireSession?: boolean; requireHeaders?: boolean; requireBearerToken?: boolean; headersComment?: string; note?: string; clientOnlyNote?: string; serverOnlyNote?: string; resultVariable?: string; forceAsBody?: boolean; forceAsParam?: boolean; forceAsQuery?: boolean; } export interface ApiMethodProperty { optional: boolean; description: string | null; name: string; type: string; exampleValue: string | null; serverOnly: boolean; path: string[]; nullable: boolean; clientOnly: boolean; } export interface ParsedApiMethod { functionName: string; properties: ApiMethodProperty[]; codePrefix: string; codeSuffix: string; typeDefinition: string; } const indentation = " "; const typeDeclarationPattern = /^type\s+([A-Za-z_$][\w$]*)\s*=\s*\{$/; const propertyPattern = /^([A-Za-z_$][\w$]*)(\?)?:\s*(.+)$/; function parsePropertyLine(line: string) { const match = propertyPattern.exec(line); if (!match) return null; const [, name, optional, declaration] = match; const exampleSeparator = declaration.indexOf(" = "); const type = ( exampleSeparator === -1 ? declaration : declaration.slice(0, exampleSeparator) ) .trim() .replace(/[;,]$/, ""); const exampleValue = exampleSeparator === -1 ? null : declaration .slice(exampleSeparator + 3) .trim() .replace(/[;,]$/, ""); return { name, optional: optional === "?", type, exampleValue, }; } function readDirective(value: string, directive: string): string | null { if (value !== directive && !value.startsWith(`${directive} `)) return null; return value .slice(directive.length) .replace(/^\s*-\s*/, "") .trim(); } function isPathRestricted(restrictedPaths: Set, path: string[]) { for (let length = 1; length <= path.length; length++) { if (restrictedPaths.has(path.slice(0, length).join("."))) return true; } return false; } export function parseApiMethod(source: string): ParsedApiMethod { const lines = source.replaceAll("\r\n", "\n").trim().split("\n"); const properties: ApiMethodProperty[] = []; const nestedPath: string[] = []; const serverOnlyPaths = new Set(); const clientOnlyPaths = new Set(); let functionName = ""; let typeStart = -1; let typeEnd = -1; let description = ""; let serverOnly = false; let clientOnly = false; let nullable = false; for (const [index, originalLine] of lines.entries()) { const line = originalLine.trim(); if (typeStart === -1) { const declaration = typeDeclarationPattern.exec(line); if (!declaration) continue; functionName = declaration[1]; typeStart = index; continue; } if (/^}[,;]?$/.test(line)) { if (nestedPath.length > 0) { nestedPath.pop(); continue; } typeEnd = index; break; } if (line.startsWith("/*") || line.startsWith("*/")) continue; if (line.startsWith("*")) { const value = line.slice(1).trim(); if (!value) continue; const serverOnlyDescription = readDirective(value, "@serverOnly"); if (serverOnlyDescription !== null) { serverOnly = true; if (serverOnlyDescription) { description += `${serverOnlyDescription} `; } continue; } const clientOnlyDescription = readDirective(value, "@clientOnly"); if (clientOnlyDescription !== null) { clientOnly = true; if (clientOnlyDescription) { description += `${clientOnlyDescription} `; } continue; } const nullableDescription = readDirective(value, "@nullable"); if (nullableDescription !== null) { nullable = true; if (nullableDescription) description += `${nullableDescription} `; continue; } description += `${value} `; continue; } if (!line) continue; const parsed = parsePropertyLine(line); if (!parsed) continue; const propertyPath = nestedPath.slice(); const nested = parsed.type === "{"; if (nested) { nestedPath.push(parsed.name); if (serverOnly) serverOnlyPaths.add(nestedPath.join(".")); if (clientOnly) clientOnlyPaths.add(nestedPath.join(".")); } properties.push({ name: parsed.name, optional: parsed.optional, type: nested ? "Object" : parsed.type, exampleValue: parsed.exampleValue, description: description.trim() || null, serverOnly: serverOnly || isPathRestricted(serverOnlyPaths, nestedPath), clientOnly: clientOnly || isPathRestricted(clientOnlyPaths, nestedPath), path: propertyPath, nullable, }); description = ""; serverOnly = false; clientOnly = false; nullable = false; } if (typeStart === -1 || typeEnd === -1) { return { functionName: "", properties: [], codePrefix: "", codeSuffix: "", typeDefinition: source.trim(), }; } const prefix = lines.slice(0, typeStart).join("\n").trim(); const suffix = lines .slice(typeEnd + 1) .join("\n") .trim(); return { functionName, properties, codePrefix: prefix ? `${prefix}\n` : "", codeSuffix: suffix ? `\n${suffix}` : "", typeDefinition: lines.slice(typeStart, typeEnd + 1).join("\n"), }; } function pathToMethodName(path: string): string { return path .split("/") .filter((segment) => segment.length > 0) .map((segment) => segment .split("-") .map((word, index) => index === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1), ) .join(""), ) .join("."); } function buildPropertyLine( property: ApiMethodProperty, indentLevel: number, additionalComments: string[] = [], ): string { const comments = [...additionalComments]; if (!property.optional) comments.push("required"); const indent = indentation.repeat(indentLevel); const value = property.exampleValue ? `: ${property.exampleValue}` : ""; const comment = comments.length > 0 ? ` // ${comments.join(", ")}` : ""; return property.type === "Object" ? `${indent}${property.name}${value}: {${comment}\n` : `${indent}${property.name}${value},${comment}\n`; } function usesQuery( method: ApiMethodHttpMethod, options: Pick< ApiMethodOptions, "forceAsBody" | "forceAsParam" | "forceAsQuery" >, ): boolean { if (options.forceAsQuery) return true; if (options.forceAsBody || options.forceAsParam) return false; return method === "GET"; } function closeNestedProperties( properties: ApiMethodProperty[], index: number, baseIndentLevel: number, ): string { const property = properties[index]; const currentDepth = property.path.length + (property.type === "Object" ? 1 : 0); const nextDepth = properties[index + 1]?.path.length ?? 0; if (nextDepth >= currentDepth) return ""; let result = ""; for (let depth = currentDepth - 1; depth >= nextDepth; depth--) { result += `${indentation.repeat(depth + baseIndentLevel)}},\n`; } return result; } function createClientBody( properties: ApiMethodProperty[], method: ApiMethodHttpMethod, options: ApiMethodOptions, ): string { const query = usesQuery(method, options); const baseIndentLevel = query ? 2 : 1; const clientProperties = properties.filter( (property) => !property.serverOnly, ); let params = ""; for (const [index, property] of clientProperties.entries()) { if (!params) params = "{\n"; params += buildPropertyLine( property, property.path.length + baseIndentLevel, ); params += closeNestedProperties(clientProperties, index, baseIndentLevel); } if (!params) return ""; if (query) return `{\n query: ${params} },\n}`; return `${params}}`; } function createServerBody( properties: ApiMethodProperty[], method: ApiMethodHttpMethod, options: ApiMethodOptions, ): string { const query = usesQuery(method, options); const serverProperties = properties.filter( (property) => !property.clientOnly, ); let propertiesContent = ""; for (const [index, property] of serverProperties.entries()) { if (!propertiesContent) propertiesContent = "{\n"; const parentPath = property.path.slice(0, -1).join("."); const inheritedServerOnly = property.serverOnly && !properties.some( (candidate) => candidate.path.join(".") === parentPath && candidate.name === property.path.at(-1), ); propertiesContent += buildPropertyLine( property, property.path.length + 2, inheritedServerOnly ? ["server-only"] : [], ); propertiesContent += closeNestedProperties(serverProperties, index, 2); } if (propertiesContent) propertiesContent += " },"; let headers = ""; const requestHeadersRequired = options.requireSession || options.requireHeaders; if (options.requireSession) { headers += "\n // This endpoint requires session cookies."; } else if (options.requireHeaders) { headers += `\n // ${ options.headersComment ?? "Pass the current request headers so Better Auth can read and set cookies." }`; } if (options.requireBearerToken) { headers += "\n // This endpoint requires a bearer authentication token."; } if (requestHeadersRequired && options.requireBearerToken) { headers += "\n headers: {\n ...(await headers()),\n authorization: 'Bearer ',\n },"; } else if (requestHeadersRequired) { headers += "\n headers: await headers(),"; } else if (options.requireBearerToken) { headers += "\n headers: { authorization: 'Bearer ' },"; } if (serverProperties.length > 0) { const parameter = query ? "query" : options.forceAsParam ? "params" : "body"; return `{\n ${parameter}: ${propertiesContent}${headers}\n}`; } return headers ? `{${headers}\n}` : ""; } export function generateApiMethodExamples( definition: ParsedApiMethod, options: ApiMethodOptions, ) { const method = options.method ?? "GET"; const resultVariable = options.resultVariable ?? "data"; const clientResult = options.noResult ? "" : `const { data${resultVariable === "data" ? "" : `: ${resultVariable}`}, error } = `; const serverResult = options.noResult ? "" : `const ${resultVariable} = `; return { client: `${definition.codePrefix}${clientResult}await authClient.${pathToMethodName(options.path)}(${createClientBody(definition.properties, method, options)});${definition.codeSuffix}`, server: `${definition.codePrefix}${serverResult}await auth.api.${definition.functionName}(${createServerBody(definition.properties, method, options)});${definition.codeSuffix}`, }; }