@proofkit/better-auth 0.2.4 → 0.3.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,26 +1,18 @@
1
- import path from "path";
1
+ import path from "node:path";
2
2
  import fs from "fs-extra";
3
3
 
4
4
  export function stripJsonComments(jsonString: string): string {
5
- return jsonString
6
- .replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) =>
7
- g ? "" : m,
8
- )
9
- .replace(/,(?=\s*[}\]])/g, "");
5
+ return jsonString
6
+ .replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => (g ? "" : m))
7
+ .replace(/,(?=\s*[}\]])/g, "");
10
8
  }
11
9
  export function getTsconfigInfo(cwd?: string, flatPath?: string) {
12
- let tsConfigPath: string;
13
- if (flatPath) {
14
- tsConfigPath = flatPath;
15
- } else {
16
- tsConfigPath = cwd
17
- ? path.join(cwd, "tsconfig.json")
18
- : path.join("tsconfig.json");
19
- }
20
- try {
21
- const text = fs.readFileSync(tsConfigPath, "utf-8");
22
- return JSON.parse(stripJsonComments(text));
23
- } catch (error) {
24
- throw error;
25
- }
10
+ let tsConfigPath: string;
11
+ if (flatPath) {
12
+ tsConfigPath = flatPath;
13
+ } else {
14
+ tsConfigPath = cwd ? path.join(cwd, "tsconfig.json") : path.join("tsconfig.json");
15
+ }
16
+ const text = fs.readFileSync(tsConfigPath, "utf-8");
17
+ return JSON.parse(stripJsonComments(text));
26
18
  }
package/src/cli/index.ts CHANGED
@@ -1,19 +1,14 @@
1
1
  #!/usr/bin/env node --no-warnings
2
2
  import { Command } from "@commander-js/extra-typings";
3
- import fs from "fs-extra";
4
-
5
- import {
6
- executeMigration,
7
- planMigration,
8
- prettyPrintMigrationPlan,
9
- } from "../migrate";
10
- import { getAdapter, getAuthTables } from "better-auth/db";
11
- import { getConfig } from "../better-auth-cli/utils/get-config";
12
3
  import { logger } from "better-auth";
13
- import prompts from "prompts";
4
+ import { getAdapter, getAuthTables } from "better-auth/db";
14
5
  import chalk from "chalk";
15
- import { AdapterOptions } from "../adapter";
16
- import { createFmOdataFetch } from "../odata";
6
+ import fs from "fs-extra";
7
+ import prompts from "prompts";
8
+ import type { AdapterOptions } from "../adapter";
9
+ import { getConfig } from "../better-auth-cli/utils/get-config";
10
+ import { executeMigration, planMigration, prettyPrintMigrationPlan } from "../migrate";
11
+ import { createRawFetch } from "../odata";
17
12
  import "dotenv/config";
18
13
 
19
14
  async function main() {
@@ -21,11 +16,7 @@ async function main() {
21
16
 
22
17
  program
23
18
  .command("migrate", { isDefault: true })
24
- .option(
25
- "--cwd <path>",
26
- "Path to the current working directory",
27
- process.cwd(),
28
- )
19
+ .option("--cwd <path>", "Path to the current working directory", process.cwd())
29
20
  .option("--config <path>", "Path to the config file")
30
21
  .option("-u, --username <username>", "Full Access Username")
31
22
  .option("-p, --password <password>", "Full Access Password")
@@ -55,16 +46,14 @@ async function main() {
55
46
  });
56
47
 
57
48
  if (adapter.id !== "filemaker") {
58
- logger.error(
59
- "This generator is only compatible with the FileMaker adapter.",
60
- );
49
+ logger.error("This generator is only compatible with the FileMaker adapter.");
61
50
  return;
62
51
  }
63
52
 
64
53
  const betterAuthSchema = getAuthTables(config);
65
54
 
66
55
  const adapterConfig = (adapter.options as AdapterOptions).config;
67
- const fetch = createFmOdataFetch({
56
+ const { fetch } = createRawFetch({
68
57
  ...adapterConfig.odata,
69
58
  auth:
70
59
  // If the username and password are provided in the CLI, use them to authenticate instead of what's in the config file.
@@ -74,13 +63,10 @@ async function main() {
74
63
  password: options.password,
75
64
  }
76
65
  : adapterConfig.odata.auth,
66
+ logging: "verbose", // Enable logging for CLI operations
77
67
  });
78
68
 
79
- const migrationPlan = await planMigration(
80
- fetch,
81
- betterAuthSchema,
82
- adapterConfig.odata.database,
83
- );
69
+ const migrationPlan = await planMigration(fetch, betterAuthSchema, adapterConfig.odata.database);
84
70
 
85
71
  if (migrationPlan.length === 0) {
86
72
  logger.info("No changes to apply. Database is up to date.");
@@ -91,11 +77,7 @@ async function main() {
91
77
  prettyPrintMigrationPlan(migrationPlan);
92
78
 
93
79
  if (migrationPlan.length > 0) {
94
- console.log(
95
- chalk.gray(
96
- "💡 Tip: You can use the --yes flag to skip this confirmation.",
97
- ),
98
- );
80
+ console.log(chalk.gray("💡 Tip: You can use the --yes flag to skip this confirmation."));
99
81
  }
100
82
 
101
83
  const { confirm } = await prompts({
package/src/index.ts CHANGED
@@ -1 +1,2 @@
1
+ /** biome-ignore-all lint/performance/noBarrelFile: But I want to */
1
2
  export { FileMakerAdapter } from "./adapter";
package/src/migrate.ts CHANGED
@@ -1,13 +1,10 @@
1
- import { type BetterAuthDbSchema } from "better-auth/db";
2
- import { type Metadata } from "fm-odata-client";
1
+ import type { BetterAuthDbSchema } from "better-auth/db";
3
2
  import chalk from "chalk";
3
+ import type { Metadata } from "fm-odata-client";
4
4
  import z from "zod/v4";
5
- import { createFmOdataFetch } from "./odata";
5
+ import type { createRawFetch } from "./odata";
6
6
 
7
- export async function getMetadata(
8
- fetch: ReturnType<typeof createFmOdataFetch>,
9
- databaseName: string,
10
- ) {
7
+ export async function getMetadata(fetch: ReturnType<typeof createRawFetch>["fetch"], databaseName: string) {
11
8
  console.log("getting metadata...");
12
9
  const result = await fetch("/$metadata", {
13
10
  method: "GET",
@@ -21,18 +18,23 @@ export async function getMetadata(
21
18
  .catch(null),
22
19
  });
23
20
 
21
+ if (result.error) {
22
+ console.error("Failed to get metadata:", result.error);
23
+ return null;
24
+ }
25
+
24
26
  return (result.data?.[databaseName] ?? null) as Metadata | null;
25
27
  }
26
28
 
27
29
  export async function planMigration(
28
- fetch: ReturnType<typeof createFmOdataFetch>,
30
+ fetch: ReturnType<typeof createRawFetch>["fetch"],
29
31
  betterAuthSchema: BetterAuthDbSchema,
30
32
  databaseName: string,
31
33
  ): Promise<MigrationPlan> {
32
34
  const metadata = await getMetadata(fetch, databaseName);
33
35
 
34
36
  // Build a map from entity set name to entity type key
35
- let entitySetToType: Record<string, string> = {};
37
+ const entitySetToType: Record<string, string> = {};
36
38
  if (metadata) {
37
39
  for (const [key, value] of Object.entries(metadata)) {
38
40
  if (value.$Kind === "EntitySet" && value.$Type) {
@@ -47,27 +49,32 @@ export async function planMigration(
47
49
  ? Object.entries(entitySetToType).reduce(
48
50
  (acc, [entitySetName, entityTypeKey]) => {
49
51
  const entityType = metadata[entityTypeKey];
50
- if (!entityType) return acc;
52
+ if (!entityType) {
53
+ return acc;
54
+ }
51
55
  const fields = Object.entries(entityType)
52
56
  .filter(
53
- ([fieldKey, fieldValue]) =>
54
- typeof fieldValue === "object" &&
55
- fieldValue !== null &&
56
- "$Type" in fieldValue,
57
+ ([_fieldKey, fieldValue]) =>
58
+ typeof fieldValue === "object" && fieldValue !== null && "$Type" in fieldValue,
57
59
  )
58
- .map(([fieldKey, fieldValue]) => ({
59
- name: fieldKey,
60
- type:
61
- fieldValue.$Type === "Edm.String"
62
- ? "varchar"
63
- : fieldValue.$Type === "Edm.DateTimeOffset"
64
- ? "timestamp"
65
- : fieldValue.$Type === "Edm.Decimal" ||
66
- fieldValue.$Type === "Edm.Int32" ||
67
- fieldValue.$Type === "Edm.Int64"
68
- ? "numeric"
69
- : "varchar",
70
- }));
60
+ .map(([fieldKey, fieldValue]) => {
61
+ let type = "varchar";
62
+ if (fieldValue.$Type === "Edm.String") {
63
+ type = "varchar";
64
+ } else if (fieldValue.$Type === "Edm.DateTimeOffset") {
65
+ type = "timestamp";
66
+ } else if (
67
+ fieldValue.$Type === "Edm.Decimal" ||
68
+ fieldValue.$Type === "Edm.Int32" ||
69
+ fieldValue.$Type === "Edm.Int64"
70
+ ) {
71
+ type = "numeric";
72
+ }
73
+ return {
74
+ name: fieldKey,
75
+ type,
76
+ };
77
+ });
71
78
  acc[entitySetName] = fields;
72
79
  return acc;
73
80
  },
@@ -85,42 +92,24 @@ export async function planMigration(
85
92
  const migrationPlan: MigrationPlan = [];
86
93
 
87
94
  for (const baTable of baTables) {
88
- const fields: FmField[] = Object.entries(baTable.fields).map(
89
- ([key, field]) => ({
95
+ const fields: FmField[] = Object.entries(baTable.fields).map(([key, field]) => {
96
+ let type: "varchar" | "numeric" | "timestamp" = "varchar";
97
+ if (field.type === "boolean" || field.type.includes("number")) {
98
+ type = "numeric";
99
+ } else if (field.type === "date") {
100
+ type = "timestamp";
101
+ }
102
+ return {
90
103
  name: field.fieldName ?? key,
91
- type:
92
- field.type === "boolean" || field.type.includes("number")
93
- ? "numeric"
94
- : field.type === "date"
95
- ? "timestamp"
96
- : "varchar",
97
- }),
98
- );
104
+ type,
105
+ };
106
+ });
99
107
 
100
108
  // get existing table or create it
101
- const tableExists = Object.prototype.hasOwnProperty.call(
102
- existingTables,
103
- baTable.modelName,
104
- );
109
+ const tableExists = baTable.modelName in existingTables;
105
110
 
106
- if (!tableExists) {
107
- migrationPlan.push({
108
- tableName: baTable.modelName,
109
- operation: "create",
110
- fields: [
111
- {
112
- name: "id",
113
- type: "varchar",
114
- primary: true,
115
- unique: true,
116
- },
117
- ...fields,
118
- ],
119
- });
120
- } else {
121
- const existingFields = (existingTables[baTable.modelName] || []).map(
122
- (f) => f.name,
123
- );
111
+ if (tableExists) {
112
+ const existingFields = (existingTables[baTable.modelName] || []).map((f) => f.name);
124
113
  const existingFieldMap = (existingTables[baTable.modelName] || []).reduce(
125
114
  (acc, f) => {
126
115
  acc[f.name] = f.type;
@@ -129,19 +118,14 @@ export async function planMigration(
129
118
  {} as Record<string, string>,
130
119
  );
131
120
  // Warn about type mismatches (optional, not in plan)
132
- fields.forEach((field) => {
133
- if (
134
- existingFields.includes(field.name) &&
135
- existingFieldMap[field.name] !== field.type
136
- ) {
121
+ for (const field of fields) {
122
+ if (existingFields.includes(field.name) && existingFieldMap[field.name] !== field.type) {
137
123
  console.warn(
138
124
  `⚠️ WARNING: Field '${field.name}' in table '${baTable.modelName}' exists but has type '${existingFieldMap[field.name]}' (expected '${field.type}'). Change the field type in FileMaker to avoid potential errors.`,
139
125
  );
140
126
  }
141
- });
142
- const fieldsToAdd = fields.filter(
143
- (f) => !existingFields.includes(f.name),
144
- );
127
+ }
128
+ const fieldsToAdd = fields.filter((f) => !existingFields.includes(f.name));
145
129
  if (fieldsToAdd.length > 0) {
146
130
  migrationPlan.push({
147
131
  tableName: baTable.modelName,
@@ -149,6 +133,20 @@ export async function planMigration(
149
133
  fields: fieldsToAdd,
150
134
  });
151
135
  }
136
+ } else {
137
+ migrationPlan.push({
138
+ tableName: baTable.modelName,
139
+ operation: "create",
140
+ fields: [
141
+ {
142
+ name: "id",
143
+ type: "varchar",
144
+ primary: true,
145
+ unique: true,
146
+ },
147
+ ...fields,
148
+ ],
149
+ });
152
150
  }
153
151
  }
154
152
 
@@ -156,24 +154,35 @@ export async function planMigration(
156
154
  }
157
155
 
158
156
  export async function executeMigration(
159
- fetch: ReturnType<typeof createFmOdataFetch>,
157
+ fetch: ReturnType<typeof createRawFetch>["fetch"],
160
158
  migrationPlan: MigrationPlan,
161
159
  ) {
162
160
  for (const step of migrationPlan) {
163
161
  if (step.operation === "create") {
164
162
  console.log("Creating table:", step.tableName);
165
- await fetch("@post/FileMaker_Tables", {
163
+ const result = await fetch("/FileMaker_Tables", {
164
+ method: "POST",
166
165
  body: {
167
166
  tableName: step.tableName,
168
167
  fields: step.fields,
169
168
  },
170
169
  });
170
+
171
+ if (result.error) {
172
+ console.error(`Failed to create table ${step.tableName}:`, result.error);
173
+ throw new Error(`Migration failed: ${result.error}`);
174
+ }
171
175
  } else if (step.operation === "update") {
172
176
  console.log("Adding fields to table:", step.tableName);
173
- await fetch("@post/FileMaker_Tables/:tableName", {
174
- params: { tableName: step.tableName },
177
+ const result = await fetch(`/FileMaker_Tables/${step.tableName}`, {
178
+ method: "PATCH",
175
179
  body: { fields: step.fields },
176
180
  });
181
+
182
+ if (result.error) {
183
+ console.error(`Failed to update table ${step.tableName}:`, result.error);
184
+ throw new Error(`Migration failed: ${result.error}`);
185
+ }
177
186
  }
178
187
  }
179
188
  }
@@ -252,8 +261,12 @@ export function prettyPrintMigrationPlan(migrationPlan: MigrationPlan) {
252
261
  if (step.fields.length) {
253
262
  for (const field of step.fields) {
254
263
  let fieldDesc = ` - ${field.name} (${field.type}`;
255
- if (field.primary) fieldDesc += ", primary";
256
- if (field.unique) fieldDesc += ", unique";
264
+ if (field.primary) {
265
+ fieldDesc += ", primary";
266
+ }
267
+ if (field.unique) {
268
+ fieldDesc += ", unique";
269
+ }
257
270
  fieldDesc += ")";
258
271
  console.log(fieldDesc);
259
272
  }
@@ -1,102 +1,219 @@
1
- import { createFetch, createSchema } from "@better-fetch/fetch";
2
- import { logger } from "@better-fetch/logger";
1
+ /** biome-ignore-all lint/suspicious/noExplicitAny: library code */
3
2
  import { logger as betterAuthLogger } from "better-auth";
4
- import { err, ok, Result } from "neverthrow";
5
- import { z } from "zod/v4";
3
+ import { err, ok, type Result } from "neverthrow";
4
+ import type { z } from "zod/v4";
6
5
 
7
- type BasicAuthCredentials = {
6
+ interface BasicAuthCredentials {
8
7
  username: string;
9
8
  password: string;
10
- };
11
- type OttoAPIKeyAuth = {
9
+ }
10
+ interface OttoAPIKeyAuth {
12
11
  apiKey: string;
13
- };
12
+ }
14
13
  type ODataAuth = BasicAuthCredentials | OttoAPIKeyAuth;
15
14
 
16
- export type FmOdataConfig = {
15
+ export interface FmOdataConfig {
17
16
  serverUrl: string;
18
17
  auth: ODataAuth;
19
18
  database: string;
20
19
  logging?: true | "verbose" | "none";
21
- };
22
-
23
- const schema = createSchema({
24
- /**
25
- * Create a new table
26
- */
27
- "@post/FileMaker_Tables": {
28
- input: z.object({ tableName: z.string(), fields: z.array(z.any()) }),
29
- },
30
- /**
31
- * Add fields to a table
32
- */
33
- "@patch/FileMaker_Tables/:tableName": {
34
- params: z.object({ tableName: z.string() }),
35
- input: z.object({ fields: z.array(z.any()) }),
36
- },
37
- /**
38
- * Delete a table
39
- */
40
- "@delete/FileMaker_Tables/:tableName": {
41
- params: z.object({ tableName: z.string() }),
42
- },
43
- /**
44
- * Delete a field from a table
45
- */
46
- "@delete/FileMaker_Tables/:tableName/:fieldName": {
47
- params: z.object({ tableName: z.string(), fieldName: z.string() }),
48
- },
49
- });
50
-
51
- export function createFmOdataFetch(args: FmOdataConfig) {
20
+ }
21
+
22
+ export function validateUrl(input: string): Result<URL, unknown> {
23
+ try {
24
+ const url = new URL(input);
25
+ return ok(url);
26
+ } catch (error) {
27
+ return err(error);
28
+ }
29
+ }
30
+
31
+ export function createRawFetch(args: FmOdataConfig) {
52
32
  const result = validateUrl(args.serverUrl);
53
33
 
54
34
  if (result.isErr()) {
55
35
  throw new Error("Invalid server URL");
56
36
  }
37
+
57
38
  let baseURL = result.value.origin;
58
39
  if ("apiKey" in args.auth) {
59
- baseURL += `/otto`;
40
+ baseURL += "/otto";
60
41
  }
61
42
  baseURL += `/fmi/odata/v4/${args.database}`;
62
43
 
63
- return createFetch({
64
- baseURL,
65
- auth:
66
- "apiKey" in args.auth
67
- ? { type: "Bearer", token: args.auth.apiKey }
68
- : {
69
- type: "Basic",
70
- username: args.auth.username,
71
- password: args.auth.password,
72
- },
73
- onError: (error) => {
74
- console.error("url", error.request.url.toString());
75
- console.log(error.error);
76
- console.log("error.request.body", JSON.stringify(error.request.body));
44
+ // Create authentication headers
45
+ const authHeaders: Record<string, string> = {};
46
+ if ("apiKey" in args.auth) {
47
+ authHeaders.Authorization = `Bearer ${args.auth.apiKey}`;
48
+ } else {
49
+ const credentials = btoa(`${args.auth.username}:${args.auth.password}`);
50
+ authHeaders.Authorization = `Basic ${credentials}`;
51
+ }
52
+
53
+ // Enhanced fetch function with body handling, validation, and structured responses
54
+ const wrappedFetch = async <TOutput = any>(
55
+ input: string | URL | Request,
56
+ options?: Omit<RequestInit, "body"> & {
57
+ body?: any; // Allow any type for body
58
+ output?: z.ZodSchema<TOutput>; // Optional schema for validation
77
59
  },
78
- schema,
79
- plugins: [
80
- logger({
81
- verbose: args.logging === "verbose",
82
- enabled: args.logging === "verbose" || !!args.logging,
83
- console: {
84
- fail: (...args) => betterAuthLogger.error("better-fetch", ...args),
85
- success: (...args) => betterAuthLogger.info("better-fetch", ...args),
86
- log: (...args) => betterAuthLogger.info("better-fetch", ...args),
87
- error: (...args) => betterAuthLogger.error("better-fetch", ...args),
88
- warn: (...args) => betterAuthLogger.warn("better-fetch", ...args),
89
- },
90
- }),
91
- ],
92
- });
93
- }
60
+ ): Promise<{ data?: TOutput; error?: string; response?: Response }> => {
61
+ try {
62
+ let url: string;
94
63
 
95
- export function validateUrl(input: string): Result<URL, unknown> {
96
- try {
97
- const url = new URL(input);
98
- return ok(url);
99
- } catch (error) {
100
- return err(error);
101
- }
64
+ // Handle different input types
65
+ if (typeof input === "string") {
66
+ // If it's already a full URL, use as-is, otherwise prepend baseURL
67
+ url = input.startsWith("http") ? input : `${baseURL}${input.startsWith("/") ? input : `/${input}`}`;
68
+ } else if (input instanceof URL) {
69
+ url = input.toString();
70
+ } else if (input instanceof Request) {
71
+ url = input.url;
72
+ } else {
73
+ url = String(input);
74
+ }
75
+
76
+ // Handle body serialization
77
+ let processedBody = options?.body;
78
+ if (
79
+ processedBody &&
80
+ typeof processedBody === "object" &&
81
+ !(processedBody instanceof FormData) &&
82
+ !(processedBody instanceof URLSearchParams) &&
83
+ !(processedBody instanceof ReadableStream)
84
+ ) {
85
+ processedBody = JSON.stringify(processedBody);
86
+ }
87
+
88
+ // Merge headers
89
+ const headers = {
90
+ "Content-Type": "application/json",
91
+ ...authHeaders,
92
+ ...(options?.headers || {}),
93
+ };
94
+
95
+ const requestInit: RequestInit = {
96
+ ...options,
97
+ headers,
98
+ body: processedBody,
99
+ };
100
+
101
+ // Optional logging
102
+ if (args.logging === "verbose" || args.logging === true) {
103
+ betterAuthLogger.info("raw-fetch", `${requestInit.method || "GET"} ${url}`);
104
+ if (requestInit.body) {
105
+ betterAuthLogger.info("raw-fetch", "Request body:", requestInit.body);
106
+ }
107
+ }
108
+
109
+ const response = await fetch(url, requestInit);
110
+
111
+ // Optional logging for response details
112
+ if (args.logging === "verbose" || args.logging === true) {
113
+ betterAuthLogger.info("raw-fetch", `Response status: ${response.status} ${response.statusText}`);
114
+ betterAuthLogger.info("raw-fetch", "Response headers:", Object.fromEntries(response.headers.entries()));
115
+ }
116
+
117
+ // Check if response is ok
118
+ if (!response.ok) {
119
+ const errorText = await response.text().catch(() => "Unknown error");
120
+ if (args.logging === "verbose" || args.logging === true) {
121
+ betterAuthLogger.error("raw-fetch", `HTTP Error ${response.status}: ${errorText}`);
122
+ }
123
+ return {
124
+ error: `HTTP ${response.status}: ${errorText}`,
125
+ response,
126
+ };
127
+ }
128
+
129
+ // Parse response based on content type
130
+ let responseData: any;
131
+ const contentType = response.headers.get("content-type");
132
+
133
+ if (args.logging === "verbose" || args.logging === true) {
134
+ betterAuthLogger.info("raw-fetch", `Response content-type: ${contentType || "none"}`);
135
+ }
136
+
137
+ if (contentType?.includes("application/json")) {
138
+ try {
139
+ const responseText = await response.text();
140
+ if (args.logging === "verbose" || args.logging === true) {
141
+ betterAuthLogger.info("raw-fetch", `Raw response text: "${responseText}"`);
142
+ betterAuthLogger.info("raw-fetch", `Response text length: ${responseText.length}`);
143
+ }
144
+
145
+ // Handle empty responses
146
+ if (responseText.trim() === "") {
147
+ if (args.logging === "verbose" || args.logging === true) {
148
+ betterAuthLogger.info("raw-fetch", "Empty JSON response, returning null");
149
+ }
150
+ responseData = null;
151
+ } else {
152
+ responseData = JSON.parse(responseText);
153
+ if (args.logging === "verbose" || args.logging === true) {
154
+ betterAuthLogger.info("raw-fetch", "Successfully parsed JSON response");
155
+ }
156
+ }
157
+ } catch (parseError) {
158
+ if (args.logging === "verbose" || args.logging === true) {
159
+ betterAuthLogger.error("raw-fetch", "JSON parse error:", parseError);
160
+ }
161
+ return {
162
+ error: `Failed to parse JSON response: ${parseError instanceof Error ? parseError.message : "Unknown parse error"}`,
163
+ response,
164
+ };
165
+ }
166
+ } else if (contentType?.includes("text/")) {
167
+ // Handle text responses (text/plain, text/html, etc.)
168
+ responseData = await response.text();
169
+ if (args.logging === "verbose" || args.logging === true) {
170
+ betterAuthLogger.info("raw-fetch", `Text response: "${responseData}"`);
171
+ }
172
+ } else {
173
+ // For other content types, try to get text but don't fail if it's binary
174
+ try {
175
+ responseData = await response.text();
176
+ if (args.logging === "verbose" || args.logging === true) {
177
+ betterAuthLogger.info("raw-fetch", `Unknown content-type response as text: "${responseData}"`);
178
+ }
179
+ } catch {
180
+ // If text parsing fails (e.g., binary data), return null
181
+ responseData = null;
182
+ if (args.logging === "verbose" || args.logging === true) {
183
+ betterAuthLogger.info("raw-fetch", "Could not parse response as text, returning null");
184
+ }
185
+ }
186
+ }
187
+
188
+ // Validate output if schema provided
189
+ if (options?.output) {
190
+ const validation = options.output.safeParse(responseData);
191
+ if (validation.success) {
192
+ return {
193
+ data: validation.data,
194
+ response,
195
+ };
196
+ }
197
+ return {
198
+ error: `Validation failed: ${validation.error.message}`,
199
+ response,
200
+ };
201
+ }
202
+
203
+ // Return unvalidated data
204
+ return {
205
+ data: responseData as TOutput,
206
+ response,
207
+ };
208
+ } catch (error) {
209
+ return {
210
+ error: error instanceof Error ? error.message : "Unknown error occurred",
211
+ };
212
+ }
213
+ };
214
+
215
+ return {
216
+ baseURL,
217
+ fetch: wrappedFetch,
218
+ };
102
219
  }