better-call 1.0.28-beta.1 → 1.0.28-beta.2

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.
package/README.md CHANGED
@@ -231,6 +231,93 @@ const createItem = createEndpoint("/item", {
231
231
  })
232
232
  ```
233
233
 
234
+ #### Allowed Media Types
235
+
236
+ You can restrict which media types (MIME types) are allowed for request bodies using the `allowedMediaTypes` option. This can be configured at both the router level and the endpoint level, with endpoint-level configuration taking precedence.
237
+
238
+ When a request is made with a disallowed media type, the endpoint will return a `415 Unsupported Media Type` error.
239
+
240
+ **Router-level configuration:**
241
+
242
+ ```ts
243
+ const router = createRouter({
244
+ createItem,
245
+ updateItem
246
+ }, {
247
+ // All endpoints in this router will only accept JSON
248
+ allowedMediaTypes: ["application/json"]
249
+ })
250
+ ```
251
+
252
+ **Endpoint-level configuration:**
253
+
254
+ ```ts
255
+ const uploadFile = createEndpoint("/upload", {
256
+ method: "POST",
257
+ metadata: {
258
+ // This endpoint will only accept form data
259
+ allowedMediaTypes: ["multipart/form-data"]
260
+ }
261
+ }, async (ctx) => {
262
+ return { success: true }
263
+ })
264
+ ```
265
+
266
+ **Multiple media types:**
267
+
268
+ ```ts
269
+ const createItem = createEndpoint("/item", {
270
+ method: "POST",
271
+ body: z.object({
272
+ id: z.string()
273
+ }),
274
+ metadata: {
275
+ // Accept both JSON and form-urlencoded
276
+ allowedMediaTypes: [
277
+ "application/json",
278
+ "application/x-www-form-urlencoded"
279
+ ]
280
+ }
281
+ }, async (ctx) => {
282
+ return {
283
+ item: {
284
+ id: ctx.body.id
285
+ }
286
+ }
287
+ })
288
+ ```
289
+
290
+ **Endpoint overriding router:**
291
+
292
+ ```ts
293
+ const router = createRouter({
294
+ createItem,
295
+ uploadFile
296
+ }, {
297
+ // Default: only accept JSON
298
+ allowedMediaTypes: ["application/json"]
299
+ })
300
+
301
+ const uploadFile = createEndpoint("/upload", {
302
+ method: "POST",
303
+ metadata: {
304
+ // This endpoint overrides the router setting
305
+ allowedMediaTypes: ["multipart/form-data", "application/octet-stream"]
306
+ }
307
+ }, async (ctx) => {
308
+ return { success: true }
309
+ })
310
+ ```
311
+
312
+ Common media types:
313
+ - `application/json` - JSON data
314
+ - `application/x-www-form-urlencoded` - Form data
315
+ - `multipart/form-data` - File uploads
316
+ - `text/plain` - Plain text
317
+ - `application/octet-stream` - Binary data
318
+
319
+ > **Note:** The validation is case-insensitive and handles charset parameters automatically (e.g., `application/json; charset=utf-8` will match `application/json`).
320
+
234
321
  #### Require Headers
235
322
 
236
323
  The `requireHeaders` option is used to require the request to have headers. If the request doesn't have headers, the endpoint will throw an error. This is only useful when you call the endpoint as a function.
package/dist/client.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { Endpoint, HasRequiredKeys, Router, UnionToIntersection } from "./router-BV-cToj2.cjs";
1
+ import { Endpoint, HasRequiredKeys, Router, UnionToIntersection } from "./router-rGV6mTr8.cjs";
2
2
  import { BetterFetchOption, BetterFetchResponse } from "@better-fetch/fetch";
3
3
 
4
4
  //#region src/client.d.ts
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Endpoint, HasRequiredKeys, Router, UnionToIntersection } from "./router-CuwMJjp1.js";
1
+ import { Endpoint, HasRequiredKeys, Router, UnionToIntersection } from "./router-NaFkuy-s.js";
2
2
  import { BetterFetchOption, BetterFetchResponse } from "@better-fetch/fetch";
3
3
 
4
4
  //#region src/client.d.ts
package/dist/index.cjs CHANGED
@@ -1,10 +1,181 @@
1
1
  const require_chunk = require('./chunk-CUT6urMc.cjs');
2
- const require_utils = require('./utils-JxxvmTPm.cjs');
3
2
  let __better_auth_utils = require("@better-auth/utils");
4
3
  __better_auth_utils = require_chunk.__toESM(__better_auth_utils);
5
4
  let rou3 = require("rou3");
6
5
  rou3 = require_chunk.__toESM(rou3);
7
6
 
7
+ //#region src/error.ts
8
+ function isErrorStackTraceLimitWritable() {
9
+ const desc = Object.getOwnPropertyDescriptor(Error, "stackTraceLimit");
10
+ if (desc === void 0) return Object.isExtensible(Error);
11
+ return Object.prototype.hasOwnProperty.call(desc, "writable") ? desc.writable : desc.set !== void 0;
12
+ }
13
+ /**
14
+ * Hide internal stack frames from the error stack trace.
15
+ */
16
+ function hideInternalStackFrames(stack) {
17
+ const lines = stack.split("\n at ");
18
+ if (lines.length <= 1) return stack;
19
+ lines.splice(1, 1);
20
+ return lines.join("\n at ");
21
+ }
22
+ /**
23
+ * Creates a custom error class that hides stack frames.
24
+ */
25
+ function makeErrorForHideStackFrame(Base, clazz) {
26
+ class HideStackFramesError extends Base {
27
+ #hiddenStack;
28
+ constructor(...args) {
29
+ if (isErrorStackTraceLimitWritable()) {
30
+ const limit = Error.stackTraceLimit;
31
+ Error.stackTraceLimit = 0;
32
+ super(...args);
33
+ Error.stackTraceLimit = limit;
34
+ } else super(...args);
35
+ const stack = (/* @__PURE__ */ new Error()).stack;
36
+ if (stack) this.#hiddenStack = hideInternalStackFrames(stack.replace(/^Error/, this.name));
37
+ }
38
+ get errorStack() {
39
+ return this.#hiddenStack;
40
+ }
41
+ }
42
+ Object.defineProperty(HideStackFramesError.prototype, "constructor", {
43
+ get() {
44
+ return clazz;
45
+ },
46
+ enumerable: false,
47
+ configurable: true
48
+ });
49
+ return HideStackFramesError;
50
+ }
51
+ const statusCodes = {
52
+ OK: 200,
53
+ CREATED: 201,
54
+ ACCEPTED: 202,
55
+ NO_CONTENT: 204,
56
+ MULTIPLE_CHOICES: 300,
57
+ MOVED_PERMANENTLY: 301,
58
+ FOUND: 302,
59
+ SEE_OTHER: 303,
60
+ NOT_MODIFIED: 304,
61
+ TEMPORARY_REDIRECT: 307,
62
+ BAD_REQUEST: 400,
63
+ UNAUTHORIZED: 401,
64
+ PAYMENT_REQUIRED: 402,
65
+ FORBIDDEN: 403,
66
+ NOT_FOUND: 404,
67
+ METHOD_NOT_ALLOWED: 405,
68
+ NOT_ACCEPTABLE: 406,
69
+ PROXY_AUTHENTICATION_REQUIRED: 407,
70
+ REQUEST_TIMEOUT: 408,
71
+ CONFLICT: 409,
72
+ GONE: 410,
73
+ LENGTH_REQUIRED: 411,
74
+ PRECONDITION_FAILED: 412,
75
+ PAYLOAD_TOO_LARGE: 413,
76
+ URI_TOO_LONG: 414,
77
+ UNSUPPORTED_MEDIA_TYPE: 415,
78
+ RANGE_NOT_SATISFIABLE: 416,
79
+ EXPECTATION_FAILED: 417,
80
+ "I'M_A_TEAPOT": 418,
81
+ MISDIRECTED_REQUEST: 421,
82
+ UNPROCESSABLE_ENTITY: 422,
83
+ LOCKED: 423,
84
+ FAILED_DEPENDENCY: 424,
85
+ TOO_EARLY: 425,
86
+ UPGRADE_REQUIRED: 426,
87
+ PRECONDITION_REQUIRED: 428,
88
+ TOO_MANY_REQUESTS: 429,
89
+ REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
90
+ UNAVAILABLE_FOR_LEGAL_REASONS: 451,
91
+ INTERNAL_SERVER_ERROR: 500,
92
+ NOT_IMPLEMENTED: 501,
93
+ BAD_GATEWAY: 502,
94
+ SERVICE_UNAVAILABLE: 503,
95
+ GATEWAY_TIMEOUT: 504,
96
+ HTTP_VERSION_NOT_SUPPORTED: 505,
97
+ VARIANT_ALSO_NEGOTIATES: 506,
98
+ INSUFFICIENT_STORAGE: 507,
99
+ LOOP_DETECTED: 508,
100
+ NOT_EXTENDED: 510,
101
+ NETWORK_AUTHENTICATION_REQUIRED: 511
102
+ };
103
+ var InternalAPIError = class extends Error {
104
+ constructor(status = "INTERNAL_SERVER_ERROR", body = void 0, headers = {}, statusCode = typeof status === "number" ? status : statusCodes[status]) {
105
+ super(body?.message, body?.cause ? { cause: body.cause } : void 0);
106
+ this.status = status;
107
+ this.body = body;
108
+ this.headers = headers;
109
+ this.statusCode = statusCode;
110
+ this.name = "APIError";
111
+ this.status = status;
112
+ this.headers = headers;
113
+ this.statusCode = statusCode;
114
+ this.body = body ? {
115
+ code: body?.message?.toUpperCase().replace(/ /g, "_").replace(/[^A-Z0-9_]/g, ""),
116
+ ...body
117
+ } : void 0;
118
+ }
119
+ };
120
+ var BetterCallError = class extends Error {
121
+ constructor(message) {
122
+ super(message);
123
+ this.name = "BetterCallError";
124
+ }
125
+ };
126
+ const APIError = makeErrorForHideStackFrame(InternalAPIError, Error);
127
+
128
+ //#endregion
129
+ //#region src/utils.ts
130
+ async function getBody(request, allowedMediaTypes) {
131
+ const contentType = request.headers.get("content-type") || "";
132
+ const normalizedContentType = contentType.toLowerCase();
133
+ if (!request.body) return;
134
+ if (allowedMediaTypes && allowedMediaTypes.length > 0) {
135
+ if (!allowedMediaTypes.some((allowed) => {
136
+ const normalizedContentTypeBase = normalizedContentType.split(";")[0].trim();
137
+ const normalizedAllowed = allowed.toLowerCase().trim();
138
+ return normalizedContentTypeBase === normalizedAllowed || normalizedContentTypeBase.includes(normalizedAllowed);
139
+ })) throw new APIError(415, {
140
+ message: `Content-Type "${contentType}" is not allowed. Allowed types: ${allowedMediaTypes.join(", ")}`,
141
+ code: "UNSUPPORTED_MEDIA_TYPE"
142
+ });
143
+ }
144
+ if (normalizedContentType.includes("application/json")) return await request.json();
145
+ if (normalizedContentType.includes("application/x-www-form-urlencoded")) {
146
+ const formData = await request.formData();
147
+ const result = {};
148
+ formData.forEach((value, key) => {
149
+ result[key] = value.toString();
150
+ });
151
+ return result;
152
+ }
153
+ if (normalizedContentType.includes("multipart/form-data")) {
154
+ const formData = await request.formData();
155
+ const result = {};
156
+ formData.forEach((value, key) => {
157
+ result[key] = value;
158
+ });
159
+ return result;
160
+ }
161
+ if (normalizedContentType.includes("text/plain")) return await request.text();
162
+ if (normalizedContentType.includes("application/octet-stream")) return await request.arrayBuffer();
163
+ if (normalizedContentType.includes("application/pdf") || normalizedContentType.includes("image/") || normalizedContentType.includes("video/")) return await request.blob();
164
+ if (normalizedContentType.includes("application/stream") || request.body instanceof ReadableStream) return request.body;
165
+ return await request.text();
166
+ }
167
+ function isAPIError(error) {
168
+ return error instanceof APIError || error?.name === "APIError";
169
+ }
170
+ function tryDecode(str) {
171
+ try {
172
+ return str.includes("%") ? decodeURIComponent(str) : str;
173
+ } catch {
174
+ return str;
175
+ }
176
+ }
177
+
178
+ //#endregion
8
179
  //#region src/to-response.ts
9
180
  function isJSONSerializable(value) {
10
181
  if (value === void 0) return false;
@@ -57,7 +228,7 @@ function toResponse(data, init) {
57
228
  statusText: init?.statusText ?? routerResponse?.statusText
58
229
  });
59
230
  }
60
- if (require_utils.isAPIError(data)) return toResponse(data.body, {
231
+ if (isAPIError(data)) return toResponse(data.body, {
61
232
  status: init?.status ?? data.statusCode,
62
233
  statusText: data.status.toString(),
63
234
  headers: init?.headers || data.headers
@@ -208,7 +379,7 @@ function parseCookies(str) {
208
379
  if (!cookies.has(key)) {
209
380
  let val = str.slice(eqIdx + 1, endIdx).trim();
210
381
  if (val.codePointAt(0) === 34) val = val.slice(1, -1);
211
- cookies.set(key, require_utils.tryDecode(val));
382
+ cookies.set(key, tryDecode(val));
212
383
  }
213
384
  index = endIdx + 1;
214
385
  }
@@ -258,28 +429,8 @@ const serializeSignedCookie = async (key, value, secret, opt) => {
258
429
  const createInternalContext = async (context, { options, path }) => {
259
430
  const headers = new Headers();
260
431
  let responseStatus = void 0;
261
- if (context.request && !context.body && !options.disableBody) {
262
- const { getBody: getBody$1 } = await Promise.resolve().then(() => require("./utils-C1kQcShE.cjs"));
263
- const allowedContentTypes = options.metadata?.allowedContentTypes;
264
- context.body = await getBody$1(context.request, allowedContentTypes);
265
- } else if (context.request && options.metadata?.allowedContentTypes) {
266
- const allowedContentTypes = options.metadata.allowedContentTypes;
267
- if (allowedContentTypes.length > 0) {
268
- const contentType = context.request.headers.get("content-type") || "";
269
- if (contentType && context.request.body) {
270
- if (!allowedContentTypes.some((allowed) => {
271
- const normalizedContentType = contentType.toLowerCase().split(";")[0].trim();
272
- const normalizedAllowed = allowed.toLowerCase().trim();
273
- return normalizedContentType === normalizedAllowed || normalizedContentType.includes(normalizedAllowed);
274
- })) throw new require_utils.APIError(415, {
275
- message: `Content-Type "${contentType}" is not allowed. Allowed types: ${allowedContentTypes.join(", ")}`,
276
- code: "UNSUPPORTED_MEDIA_TYPE"
277
- });
278
- }
279
- }
280
- }
281
432
  const { data, error } = await runValidation(options, context);
282
- if (error) throw new require_utils.APIError(400, {
433
+ if (error) throw new APIError(400, {
283
434
  message: error.message,
284
435
  code: "VALIDATION_ERROR"
285
436
  });
@@ -333,10 +484,10 @@ const createInternalContext = async (context, { options, path }) => {
333
484
  },
334
485
  redirect: (url) => {
335
486
  headers.set("location", url);
336
- return new require_utils.APIError("FOUND", void 0, headers);
487
+ return new APIError("FOUND", void 0, headers);
337
488
  },
338
489
  error: (status, body, headers$1) => {
339
- return new require_utils.APIError(status, body, headers$1);
490
+ return new APIError(status, body, headers$1);
340
491
  },
341
492
  setStatus: (status) => {
342
493
  responseStatus = status;
@@ -374,7 +525,7 @@ const createInternalContext = async (context, { options, path }) => {
374
525
  //#endregion
375
526
  //#region src/endpoint.ts
376
527
  const createEndpoint = (path, options, handler) => {
377
- if ((options.method === "GET" || options.method === "HEAD") && options.body) throw new require_utils.BetterCallError("Body is not allowed with GET or HEAD methods");
528
+ if ((options.method === "GET" || options.method === "HEAD") && options.body) throw new BetterCallError("Body is not allowed with GET or HEAD methods");
378
529
  const internalHandler = async (...inputCtx) => {
379
530
  const context = inputCtx[0] || {};
380
531
  const internalContext = await createInternalContext(context, {
@@ -382,7 +533,7 @@ const createEndpoint = (path, options, handler) => {
382
533
  path
383
534
  });
384
535
  const response = await handler(internalContext).catch(async (e) => {
385
- if (require_utils.isAPIError(e)) {
536
+ if (isAPIError(e)) {
386
537
  const onAPIError = options.onAPIError;
387
538
  if (onAPIError) await onAPIError(e);
388
539
  if (context.asResponse) return e;
@@ -2352,14 +2503,14 @@ const createRouter = (endpoints, config$1) => {
2352
2503
  });
2353
2504
  const handler = route.data;
2354
2505
  try {
2355
- const allowedContentTypes = handler.options.metadata?.allowedContentTypes || config$1?.allowedContentTypes;
2506
+ const allowedMediaTypes = handler.options.metadata?.allowedMediaTypes || config$1?.allowedMediaTypes;
2356
2507
  const context = {
2357
2508
  path,
2358
2509
  method: request.method,
2359
2510
  headers: request.headers,
2360
2511
  params: route.params ? JSON.parse(JSON.stringify(route.params)) : {},
2361
2512
  request,
2362
- body: handler.options.disableBody ? void 0 : await require_utils.getBody(handler.options.cloneRequest ? request.clone() : request, allowedContentTypes),
2513
+ body: handler.options.disableBody ? void 0 : await getBody(handler.options.cloneRequest ? request.clone() : request, allowedMediaTypes),
2363
2514
  query,
2364
2515
  _flag: "router",
2365
2516
  asResponse: true,
@@ -2380,11 +2531,11 @@ const createRouter = (endpoints, config$1) => {
2380
2531
  const errorResponse = await config$1.onError(error);
2381
2532
  if (errorResponse instanceof Response) return toResponse(errorResponse);
2382
2533
  } catch (error$1) {
2383
- if (require_utils.isAPIError(error$1)) return toResponse(error$1);
2534
+ if (isAPIError(error$1)) return toResponse(error$1);
2384
2535
  throw error$1;
2385
2536
  }
2386
2537
  if (config$1?.throwError) throw error;
2387
- if (require_utils.isAPIError(error)) return toResponse(error);
2538
+ if (isAPIError(error)) return toResponse(error);
2388
2539
  console.error(`# SERVER_ERROR: `, error);
2389
2540
  return new Response(null, {
2390
2541
  status: 500,
@@ -2406,8 +2557,8 @@ const createRouter = (endpoints, config$1) => {
2406
2557
  };
2407
2558
 
2408
2559
  //#endregion
2409
- exports.APIError = require_utils.APIError;
2410
- exports.BetterCallError = require_utils.BetterCallError;
2560
+ exports.APIError = APIError;
2561
+ exports.BetterCallError = BetterCallError;
2411
2562
  exports.createEndpoint = createEndpoint;
2412
2563
  exports.createInternalContext = createInternalContext;
2413
2564
  exports.createMiddleware = createMiddleware;
@@ -2415,11 +2566,11 @@ exports.createRouter = createRouter;
2415
2566
  exports.generator = generator;
2416
2567
  exports.getCookieKey = getCookieKey;
2417
2568
  exports.getHTML = getHTML;
2418
- exports.hideInternalStackFrames = require_utils.hideInternalStackFrames;
2419
- exports.makeErrorForHideStackFrame = require_utils.makeErrorForHideStackFrame;
2569
+ exports.hideInternalStackFrames = hideInternalStackFrames;
2570
+ exports.makeErrorForHideStackFrame = makeErrorForHideStackFrame;
2420
2571
  exports.parseCookies = parseCookies;
2421
2572
  exports.serializeCookie = serializeCookie;
2422
2573
  exports.serializeSignedCookie = serializeSignedCookie;
2423
- exports.statusCodes = require_utils.statusCodes;
2574
+ exports.statusCodes = statusCodes;
2424
2575
  exports.toResponse = toResponse;
2425
2576
  //# sourceMappingURL=index.cjs.map