@effectify/node-better-auth 0.5.12 → 0.5.13-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.
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effectify/node-better-auth",
3
- "version": "0.5.12",
3
+ "version": "0.5.13-beta.0",
4
4
  "description": "Integration of better-auth with Effect for Node.js applications",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,9 +1,11 @@
1
1
  import type { Auth } from "better-auth";
2
2
  import * as Effect from "effect/Effect";
3
- import { BetterAuthApiError } from "./better-auth-error.js";
4
3
  import type { ConfigError } from "effect/Config";
5
4
  import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
6
5
  import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
7
- export declare const toEffectHandler: (auth: {
8
- handler: Auth["handler"];
9
- } | Auth["handler"]) => Effect.Effect<HttpServerResponse.HttpServerResponse, BetterAuthApiError | ConfigError, HttpServerRequest.HttpServerRequest>;
6
+ type BetterAuthHandler = Auth["handler"] | {
7
+ readonly handler: Auth["handler"];
8
+ readonly options?: Pick<Auth["options"], "trustedOrigins">;
9
+ };
10
+ export declare const toEffectHandler: (auth: BetterAuthHandler) => Effect.Effect<HttpServerResponse.HttpServerResponse, ConfigError, HttpServerRequest.HttpServerRequest>;
11
+ export {};
@@ -5,8 +5,18 @@ import { BetterAuthApiError } from "./better-auth-error.js";
5
5
  import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
6
6
  import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
7
7
  import * as NodeHttpServerRequest from "@effect/platform-node/NodeHttpServerRequest";
8
- const TRAILING_SLASH_REGEX = /\/+$/;
9
- const PROTOCOL_REGEX = /(https?:\/\/)+/;
8
+ const canonicalOrigin = (value) => {
9
+ if (!URL.canParse(value))
10
+ return undefined;
11
+ const url = new URL(value);
12
+ if ((url.protocol !== "http:" && url.protocol !== "https:") ||
13
+ url.username !== "" ||
14
+ url.password !== "" ||
15
+ url.hostname.includes("*")) {
16
+ return undefined;
17
+ }
18
+ return url.origin;
19
+ };
10
20
  export const toEffectHandler = (auth) => Effect.gen(function* () {
11
21
  const request = yield* HttpServerRequest.HttpServerRequest;
12
22
  const nodeRequest = NodeHttpServerRequest.toIncomingMessage(request);
@@ -14,14 +24,21 @@ export const toEffectHandler = (auth) => Effect.gen(function* () {
14
24
  const appUrl = yield* Config.url("BETTER_AUTH_URL");
15
25
  // Debug: log the configured app URL so we can diagnose Config errors
16
26
  yield* Effect.log(`toEffectHandler: BETTER_AUTH_URL=${String(appUrl)}`);
17
- const normalizeUrl = (url) => url
18
- .toString()
19
- .replace(TRAILING_SLASH_REGEX, "")
20
- .replace(PROTOCOL_REGEX, "http://");
21
- const allowedOrigins = [normalizeUrl(appUrl)];
22
- const origin = nodeRequest.headers.origin ? normalizeUrl(appUrl) : "";
23
- if (allowedOrigins.includes(origin)) {
24
- nodeResponse.setHeader("Access-Control-Allow-Origin", nodeRequest.headers.origin || "");
27
+ const allowedOrigins = new Set([appUrl.origin]);
28
+ const trustedOrigins = typeof auth === "function" ? undefined : auth.options?.trustedOrigins;
29
+ if (Array.isArray(trustedOrigins)) {
30
+ for (const trustedOrigin of trustedOrigins) {
31
+ if (typeof trustedOrigin !== "string")
32
+ continue;
33
+ const origin = canonicalOrigin(trustedOrigin);
34
+ if (origin !== undefined)
35
+ allowedOrigins.add(origin);
36
+ }
37
+ }
38
+ const requestOrigin = nodeRequest.headers.origin;
39
+ const origin = requestOrigin === undefined ? undefined : canonicalOrigin(requestOrigin);
40
+ if (origin !== undefined && allowedOrigins.has(origin)) {
41
+ nodeResponse.setHeader("Access-Control-Allow-Origin", origin);
25
42
  nodeResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
26
43
  nodeResponse.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
27
44
  nodeResponse.setHeader("Access-Control-Max-Age", "600");
@@ -35,40 +52,28 @@ export const toEffectHandler = (auth) => Effect.gen(function* () {
35
52
  }
36
53
  // Log incoming request for debugging
37
54
  const authHeader = nodeRequest.headers.authorization;
38
- yield* Effect.log(`toEffectHandler: incoming ${nodeRequest.method} ${String(nodeRequest.url)} headers: cookie=${nodeRequest.headers.cookie?.substring(0, 50) || "none"}, auth=${authHeader?.substring(0, 30) || "none"}`);
55
+ yield* Effect.log(`toEffectHandler: incoming ${nodeRequest.method} ${String(nodeRequest.url)} headers: cookie=${nodeRequest.headers.cookie ? "present" : "none"}, auth=${authHeader ? "present" : "none"}`);
39
56
  // If no cookie but has Authorization header (bearer token), set it as cookie for better-auth
40
57
  if (!nodeRequest.headers.cookie && authHeader?.startsWith("Bearer ")) {
41
58
  const token = authHeader.slice(7); // Remove "Bearer " prefix
42
59
  nodeRequest.headers.cookie = `better-auth.session_token=${token}`;
43
60
  yield* Effect.log(`toEffectHandler: using token from Authorization header as cookie`);
44
61
  }
45
- try {
46
- yield* Effect.tryPromise({
47
- try: () => "handler" in auth
48
- ? toNodeHandler(auth.handler)(nodeRequest, nodeResponse)
49
- : toNodeHandler(auth)(nodeRequest, nodeResponse),
50
- catch: (cause) => new BetterAuthApiError({ cause }),
51
- });
52
- // Log the response status after the handler completes
53
- yield* Effect.log(`toEffectHandler: completed ${nodeRequest.method} ${String(nodeRequest.url)} -> ${nodeResponse.statusCode}`);
54
- }
55
- catch (err) {
56
- // Ensure we log errors from the underlying handler for debugging
57
- yield* Effect.log(`toEffectHandler: error handling ${nodeRequest.method} ${String(nodeRequest.url)}: ${String(err)}`);
58
- try {
59
- // Try to return the error to the client as JSON to make debugging easier
60
- nodeResponse.statusCode = 500;
61
- nodeResponse.setHeader("Content-Type", "application/json");
62
- const payload = JSON.stringify({ error: String(err) });
63
- nodeResponse.end(payload);
64
- }
65
- catch (writeErr) {
66
- // If writing the error response fails, log that too
67
- yield* Effect.log(`toEffectHandler: failed to write error response: ${String(writeErr)}`);
62
+ const handler = typeof auth === "function" ? auth : auth.handler;
63
+ return yield* Effect.tryPromise({
64
+ try: () => toNodeHandler(handler)(nodeRequest, nodeResponse),
65
+ catch: (cause) => new BetterAuthApiError({ cause }),
66
+ }).pipe(Effect.tap(() => Effect.log(`toEffectHandler: completed ${nodeRequest.method} ${String(nodeRequest.url)} -> ${nodeResponse.statusCode}`)), Effect.map(() => HttpServerResponse.empty({
67
+ status: nodeResponse.writableEnded ? nodeResponse.statusCode : 499,
68
+ })), Effect.catchTag("BetterAuthApiError", (error) => Effect.gen(function* () {
69
+ const errorMessage = `${String(error)}: ${String(error.cause)}`;
70
+ yield* Effect.log(`toEffectHandler: error handling ${nodeRequest.method} ${String(nodeRequest.url)}: ${errorMessage}`);
71
+ if (nodeResponse.headersSent || nodeResponse.writableEnded) {
72
+ if (!nodeResponse.writableEnded) {
73
+ yield* Effect.sync(() => nodeResponse.end());
74
+ }
75
+ return HttpServerResponse.empty({ status: nodeResponse.statusCode });
68
76
  }
69
- return HttpServerResponse.empty({ status: 500 });
70
- }
71
- return HttpServerResponse.empty({
72
- status: nodeResponse.writableFinished ? nodeResponse.statusCode : 499,
73
- });
77
+ return HttpServerResponse.jsonUnsafe({ error: "Internal Server Error" }, { status: 500 });
78
+ })));
74
79
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effectify/node-better-auth",
3
- "version": "0.5.12",
3
+ "version": "0.5.13-beta.0",
4
4
  "description": "Integration of better-auth with Effect for Node.js applications",
5
5
  "repository": {
6
6
  "type": "git",