@hammadj/better-auth-core 1.5.0-beta.9
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/.turbo/turbo-build.log +266 -0
- package/.turbo/turbo-test.log +2 -0
- package/LICENSE.md +20 -0
- package/dist/api/index.d.mts +181 -0
- package/dist/api/index.mjs +34 -0
- package/dist/api/index.mjs.map +1 -0
- package/dist/async_hooks/index.d.mts +7 -0
- package/dist/async_hooks/index.mjs +22 -0
- package/dist/async_hooks/index.mjs.map +1 -0
- package/dist/async_hooks/pure.index.d.mts +7 -0
- package/dist/async_hooks/pure.index.mjs +35 -0
- package/dist/async_hooks/pure.index.mjs.map +1 -0
- package/dist/context/endpoint-context.d.mts +19 -0
- package/dist/context/endpoint-context.mjs +32 -0
- package/dist/context/endpoint-context.mjs.map +1 -0
- package/dist/context/global.d.mts +7 -0
- package/dist/context/global.mjs +38 -0
- package/dist/context/global.mjs.map +1 -0
- package/dist/context/index.d.mts +5 -0
- package/dist/context/index.mjs +6 -0
- package/dist/context/request-state.d.mts +26 -0
- package/dist/context/request-state.mjs +50 -0
- package/dist/context/request-state.mjs.map +1 -0
- package/dist/context/transaction.d.mts +25 -0
- package/dist/context/transaction.mjs +96 -0
- package/dist/context/transaction.mjs.map +1 -0
- package/dist/db/adapter/factory.d.mts +28 -0
- package/dist/db/adapter/factory.mjs +716 -0
- package/dist/db/adapter/factory.mjs.map +1 -0
- package/dist/db/adapter/get-default-field-name.d.mts +19 -0
- package/dist/db/adapter/get-default-field-name.mjs +39 -0
- package/dist/db/adapter/get-default-field-name.mjs.map +1 -0
- package/dist/db/adapter/get-default-model-name.d.mts +13 -0
- package/dist/db/adapter/get-default-model-name.mjs +33 -0
- package/dist/db/adapter/get-default-model-name.mjs.map +1 -0
- package/dist/db/adapter/get-field-attributes.d.mts +30 -0
- package/dist/db/adapter/get-field-attributes.mjs +40 -0
- package/dist/db/adapter/get-field-attributes.mjs.map +1 -0
- package/dist/db/adapter/get-field-name.d.mts +19 -0
- package/dist/db/adapter/get-field-name.mjs +34 -0
- package/dist/db/adapter/get-field-name.mjs.map +1 -0
- package/dist/db/adapter/get-id-field.d.mts +40 -0
- package/dist/db/adapter/get-id-field.mjs +68 -0
- package/dist/db/adapter/get-id-field.mjs.map +1 -0
- package/dist/db/adapter/get-model-name.d.mts +13 -0
- package/dist/db/adapter/get-model-name.mjs +24 -0
- package/dist/db/adapter/get-model-name.mjs.map +1 -0
- package/dist/db/adapter/index.d.mts +515 -0
- package/dist/db/adapter/index.mjs +10 -0
- package/dist/db/adapter/types.d.mts +140 -0
- package/dist/db/adapter/utils.d.mts +8 -0
- package/dist/db/adapter/utils.mjs +39 -0
- package/dist/db/adapter/utils.mjs.map +1 -0
- package/dist/db/get-tables.d.mts +9 -0
- package/dist/db/get-tables.mjs +267 -0
- package/dist/db/get-tables.mjs.map +1 -0
- package/dist/db/index.d.mts +10 -0
- package/dist/db/index.mjs +9 -0
- package/dist/db/plugin.d.mts +13 -0
- package/dist/db/schema/account.d.mts +27 -0
- package/dist/db/schema/account.mjs +20 -0
- package/dist/db/schema/account.mjs.map +1 -0
- package/dist/db/schema/rate-limit.d.mts +15 -0
- package/dist/db/schema/rate-limit.mjs +12 -0
- package/dist/db/schema/rate-limit.mjs.map +1 -0
- package/dist/db/schema/session.d.mts +22 -0
- package/dist/db/schema/session.mjs +15 -0
- package/dist/db/schema/session.mjs.map +1 -0
- package/dist/db/schema/shared.d.mts +11 -0
- package/dist/db/schema/shared.mjs +12 -0
- package/dist/db/schema/shared.mjs.map +1 -0
- package/dist/db/schema/user.d.mts +21 -0
- package/dist/db/schema/user.mjs +14 -0
- package/dist/db/schema/user.mjs.map +1 -0
- package/dist/db/schema/verification.d.mts +20 -0
- package/dist/db/schema/verification.mjs +13 -0
- package/dist/db/schema/verification.mjs.map +1 -0
- package/dist/db/type.d.mts +147 -0
- package/dist/env/color-depth.d.mts +5 -0
- package/dist/env/color-depth.mjs +89 -0
- package/dist/env/color-depth.mjs.map +1 -0
- package/dist/env/env-impl.d.mts +33 -0
- package/dist/env/env-impl.mjs +83 -0
- package/dist/env/env-impl.mjs.map +1 -0
- package/dist/env/index.d.mts +4 -0
- package/dist/env/index.mjs +5 -0
- package/dist/env/logger.d.mts +49 -0
- package/dist/env/logger.mjs +82 -0
- package/dist/env/logger.mjs.map +1 -0
- package/dist/error/codes.d.mts +199 -0
- package/dist/error/codes.mjs +57 -0
- package/dist/error/codes.mjs.map +1 -0
- package/dist/error/index.d.mts +20 -0
- package/dist/error/index.mjs +30 -0
- package/dist/error/index.mjs.map +1 -0
- package/dist/index.d.mts +8 -0
- package/dist/index.mjs +1 -0
- package/dist/oauth2/client-credentials-token.d.mts +37 -0
- package/dist/oauth2/client-credentials-token.mjs +55 -0
- package/dist/oauth2/client-credentials-token.mjs.map +1 -0
- package/dist/oauth2/create-authorization-url.d.mts +46 -0
- package/dist/oauth2/create-authorization-url.mjs +43 -0
- package/dist/oauth2/create-authorization-url.mjs.map +1 -0
- package/dist/oauth2/index.d.mts +8 -0
- package/dist/oauth2/index.mjs +8 -0
- package/dist/oauth2/oauth-provider.d.mts +195 -0
- package/dist/oauth2/refresh-access-token.d.mts +36 -0
- package/dist/oauth2/refresh-access-token.mjs +59 -0
- package/dist/oauth2/refresh-access-token.mjs.map +1 -0
- package/dist/oauth2/utils.d.mts +8 -0
- package/dist/oauth2/utils.mjs +28 -0
- package/dist/oauth2/utils.mjs.map +1 -0
- package/dist/oauth2/validate-authorization-code.d.mts +56 -0
- package/dist/oauth2/validate-authorization-code.mjs +72 -0
- package/dist/oauth2/validate-authorization-code.mjs.map +1 -0
- package/dist/oauth2/verify.d.mts +43 -0
- package/dist/oauth2/verify.mjs +96 -0
- package/dist/oauth2/verify.mjs.map +1 -0
- package/dist/social-providers/apple.d.mts +120 -0
- package/dist/social-providers/apple.mjs +105 -0
- package/dist/social-providers/apple.mjs.map +1 -0
- package/dist/social-providers/atlassian.d.mts +73 -0
- package/dist/social-providers/atlassian.mjs +84 -0
- package/dist/social-providers/atlassian.mjs.map +1 -0
- package/dist/social-providers/cognito.d.mts +88 -0
- package/dist/social-providers/cognito.mjs +166 -0
- package/dist/social-providers/cognito.mjs.map +1 -0
- package/dist/social-providers/discord.d.mts +127 -0
- package/dist/social-providers/discord.mjs +65 -0
- package/dist/social-providers/discord.mjs.map +1 -0
- package/dist/social-providers/dropbox.d.mts +72 -0
- package/dist/social-providers/dropbox.mjs +76 -0
- package/dist/social-providers/dropbox.mjs.map +1 -0
- package/dist/social-providers/facebook.d.mts +82 -0
- package/dist/social-providers/facebook.mjs +121 -0
- package/dist/social-providers/facebook.mjs.map +1 -0
- package/dist/social-providers/figma.d.mts +64 -0
- package/dist/social-providers/figma.mjs +87 -0
- package/dist/social-providers/figma.mjs.map +1 -0
- package/dist/social-providers/github.d.mts +105 -0
- package/dist/social-providers/github.mjs +97 -0
- package/dist/social-providers/github.mjs.map +1 -0
- package/dist/social-providers/gitlab.d.mts +126 -0
- package/dist/social-providers/gitlab.mjs +83 -0
- package/dist/social-providers/gitlab.mjs.map +1 -0
- package/dist/social-providers/google.d.mts +100 -0
- package/dist/social-providers/google.mjs +109 -0
- package/dist/social-providers/google.mjs.map +1 -0
- package/dist/social-providers/huggingface.d.mts +86 -0
- package/dist/social-providers/huggingface.mjs +76 -0
- package/dist/social-providers/huggingface.mjs.map +1 -0
- package/dist/social-providers/index.d.mts +1725 -0
- package/dist/social-providers/index.mjs +77 -0
- package/dist/social-providers/index.mjs.map +1 -0
- package/dist/social-providers/kakao.d.mts +164 -0
- package/dist/social-providers/kakao.mjs +73 -0
- package/dist/social-providers/kakao.mjs.map +1 -0
- package/dist/social-providers/kick.d.mts +76 -0
- package/dist/social-providers/kick.mjs +72 -0
- package/dist/social-providers/kick.mjs.map +1 -0
- package/dist/social-providers/line.d.mts +108 -0
- package/dist/social-providers/line.mjs +114 -0
- package/dist/social-providers/line.mjs.map +1 -0
- package/dist/social-providers/linear.d.mts +71 -0
- package/dist/social-providers/linear.mjs +89 -0
- package/dist/social-providers/linear.mjs.map +1 -0
- package/dist/social-providers/linkedin.d.mts +70 -0
- package/dist/social-providers/linkedin.mjs +77 -0
- package/dist/social-providers/linkedin.mjs.map +1 -0
- package/dist/social-providers/microsoft-entra-id.d.mts +175 -0
- package/dist/social-providers/microsoft-entra-id.mjs +107 -0
- package/dist/social-providers/microsoft-entra-id.mjs.map +1 -0
- package/dist/social-providers/naver.d.mts +95 -0
- package/dist/social-providers/naver.mjs +68 -0
- package/dist/social-providers/naver.mjs.map +1 -0
- package/dist/social-providers/notion.d.mts +67 -0
- package/dist/social-providers/notion.mjs +76 -0
- package/dist/social-providers/notion.mjs.map +1 -0
- package/dist/social-providers/paybin.d.mts +74 -0
- package/dist/social-providers/paybin.mjs +86 -0
- package/dist/social-providers/paybin.mjs.map +1 -0
- package/dist/social-providers/paypal.d.mts +132 -0
- package/dist/social-providers/paypal.mjs +145 -0
- package/dist/social-providers/paypal.mjs.map +1 -0
- package/dist/social-providers/polar.d.mts +77 -0
- package/dist/social-providers/polar.mjs +74 -0
- package/dist/social-providers/polar.mjs.map +1 -0
- package/dist/social-providers/reddit.d.mts +65 -0
- package/dist/social-providers/reddit.mjs +84 -0
- package/dist/social-providers/reddit.mjs.map +1 -0
- package/dist/social-providers/roblox.d.mts +73 -0
- package/dist/social-providers/roblox.mjs +60 -0
- package/dist/social-providers/roblox.mjs.map +1 -0
- package/dist/social-providers/salesforce.d.mts +82 -0
- package/dist/social-providers/salesforce.mjs +92 -0
- package/dist/social-providers/salesforce.mjs.map +1 -0
- package/dist/social-providers/slack.d.mts +86 -0
- package/dist/social-providers/slack.mjs +69 -0
- package/dist/social-providers/slack.mjs.map +1 -0
- package/dist/social-providers/spotify.d.mts +66 -0
- package/dist/social-providers/spotify.mjs +72 -0
- package/dist/social-providers/spotify.mjs.map +1 -0
- package/dist/social-providers/tiktok.d.mts +171 -0
- package/dist/social-providers/tiktok.mjs +63 -0
- package/dist/social-providers/tiktok.mjs.map +1 -0
- package/dist/social-providers/twitch.d.mts +82 -0
- package/dist/social-providers/twitch.mjs +79 -0
- package/dist/social-providers/twitch.mjs.map +1 -0
- package/dist/social-providers/twitter.d.mts +129 -0
- package/dist/social-providers/twitter.mjs +88 -0
- package/dist/social-providers/twitter.mjs.map +1 -0
- package/dist/social-providers/vercel.d.mts +65 -0
- package/dist/social-providers/vercel.mjs +62 -0
- package/dist/social-providers/vercel.mjs.map +1 -0
- package/dist/social-providers/vk.d.mts +73 -0
- package/dist/social-providers/vk.mjs +84 -0
- package/dist/social-providers/vk.mjs.map +1 -0
- package/dist/social-providers/zoom.d.mts +173 -0
- package/dist/social-providers/zoom.mjs +73 -0
- package/dist/social-providers/zoom.mjs.map +1 -0
- package/dist/types/context.d.mts +267 -0
- package/dist/types/cookie.d.mts +16 -0
- package/dist/types/helper.d.mts +10 -0
- package/dist/types/index.d.mts +8 -0
- package/dist/types/init-options.d.mts +1314 -0
- package/dist/types/plugin-client.d.mts +112 -0
- package/dist/types/plugin.d.mts +125 -0
- package/dist/utils/db.d.mts +12 -0
- package/dist/utils/db.mjs +17 -0
- package/dist/utils/db.mjs.map +1 -0
- package/dist/utils/deprecate.d.mts +10 -0
- package/dist/utils/deprecate.mjs +18 -0
- package/dist/utils/deprecate.mjs.map +1 -0
- package/dist/utils/error-codes.d.mts +13 -0
- package/dist/utils/error-codes.mjs +12 -0
- package/dist/utils/error-codes.mjs.map +1 -0
- package/dist/utils/id.d.mts +5 -0
- package/dist/utils/id.mjs +10 -0
- package/dist/utils/id.mjs.map +1 -0
- package/dist/utils/ip.d.mts +55 -0
- package/dist/utils/ip.mjs +119 -0
- package/dist/utils/ip.mjs.map +1 -0
- package/dist/utils/json.d.mts +5 -0
- package/dist/utils/json.mjs +26 -0
- package/dist/utils/json.mjs.map +1 -0
- package/dist/utils/string.d.mts +5 -0
- package/dist/utils/string.mjs +8 -0
- package/dist/utils/string.mjs.map +1 -0
- package/dist/utils/url.d.mts +21 -0
- package/dist/utils/url.mjs +33 -0
- package/dist/utils/url.mjs.map +1 -0
- package/package.json +147 -0
- package/src/api/index.ts +106 -0
- package/src/async_hooks/index.ts +40 -0
- package/src/async_hooks/pure.index.ts +46 -0
- package/src/context/endpoint-context.ts +50 -0
- package/src/context/global.ts +57 -0
- package/src/context/index.ts +23 -0
- package/src/context/request-state.test.ts +94 -0
- package/src/context/request-state.ts +91 -0
- package/src/context/transaction.ts +136 -0
- package/src/db/adapter/factory.ts +1362 -0
- package/src/db/adapter/get-default-field-name.ts +59 -0
- package/src/db/adapter/get-default-model-name.ts +51 -0
- package/src/db/adapter/get-field-attributes.ts +62 -0
- package/src/db/adapter/get-field-name.ts +43 -0
- package/src/db/adapter/get-id-field.ts +141 -0
- package/src/db/adapter/get-model-name.ts +36 -0
- package/src/db/adapter/index.ts +554 -0
- package/src/db/adapter/types.ts +171 -0
- package/src/db/adapter/utils.ts +61 -0
- package/src/db/get-tables.ts +296 -0
- package/src/db/index.ts +18 -0
- package/src/db/plugin.ts +11 -0
- package/src/db/schema/account.ts +34 -0
- package/src/db/schema/rate-limit.ts +21 -0
- package/src/db/schema/session.ts +17 -0
- package/src/db/schema/shared.ts +7 -0
- package/src/db/schema/user.ts +16 -0
- package/src/db/schema/verification.ts +15 -0
- package/src/db/test/get-tables.test.ts +116 -0
- package/src/db/type.ts +180 -0
- package/src/env/color-depth.ts +172 -0
- package/src/env/env-impl.ts +124 -0
- package/src/env/index.ts +23 -0
- package/src/env/logger.test.ts +34 -0
- package/src/env/logger.ts +145 -0
- package/src/error/codes.ts +58 -0
- package/src/error/index.ts +35 -0
- package/src/index.ts +1 -0
- package/src/oauth2/client-credentials-token.ts +102 -0
- package/src/oauth2/create-authorization-url.ts +87 -0
- package/src/oauth2/index.ts +26 -0
- package/src/oauth2/oauth-provider.ts +222 -0
- package/src/oauth2/refresh-access-token.ts +124 -0
- package/src/oauth2/utils.ts +38 -0
- package/src/oauth2/validate-authorization-code.ts +149 -0
- package/src/oauth2/validate-token.test.ts +174 -0
- package/src/oauth2/verify.ts +221 -0
- package/src/social-providers/apple.ts +223 -0
- package/src/social-providers/atlassian.ts +132 -0
- package/src/social-providers/cognito.ts +279 -0
- package/src/social-providers/discord.ts +169 -0
- package/src/social-providers/dropbox.ts +112 -0
- package/src/social-providers/facebook.ts +206 -0
- package/src/social-providers/figma.ts +117 -0
- package/src/social-providers/github.ts +184 -0
- package/src/social-providers/gitlab.ts +155 -0
- package/src/social-providers/google.ts +199 -0
- package/src/social-providers/huggingface.ts +118 -0
- package/src/social-providers/index.ts +127 -0
- package/src/social-providers/kakao.ts +178 -0
- package/src/social-providers/kick.ts +109 -0
- package/src/social-providers/line.ts +169 -0
- package/src/social-providers/linear.ts +121 -0
- package/src/social-providers/linkedin.ts +110 -0
- package/src/social-providers/microsoft-entra-id.ts +259 -0
- package/src/social-providers/naver.ts +112 -0
- package/src/social-providers/notion.ts +108 -0
- package/src/social-providers/paybin.ts +122 -0
- package/src/social-providers/paypal.ts +263 -0
- package/src/social-providers/polar.ts +110 -0
- package/src/social-providers/reddit.ts +122 -0
- package/src/social-providers/roblox.ts +111 -0
- package/src/social-providers/salesforce.ts +159 -0
- package/src/social-providers/slack.ts +111 -0
- package/src/social-providers/spotify.ts +93 -0
- package/src/social-providers/tiktok.ts +209 -0
- package/src/social-providers/twitch.ts +111 -0
- package/src/social-providers/twitter.ts +198 -0
- package/src/social-providers/vercel.ts +87 -0
- package/src/social-providers/vk.ts +124 -0
- package/src/social-providers/zoom.ts +238 -0
- package/src/types/context.ts +396 -0
- package/src/types/cookie.ts +10 -0
- package/src/types/helper.ts +26 -0
- package/src/types/index.ts +32 -0
- package/src/types/init-options.ts +1529 -0
- package/src/types/plugin-client.ts +127 -0
- package/src/types/plugin.ts +157 -0
- package/src/utils/db.ts +20 -0
- package/src/utils/deprecate.test.ts +72 -0
- package/src/utils/deprecate.ts +21 -0
- package/src/utils/error-codes.ts +65 -0
- package/src/utils/id.ts +5 -0
- package/src/utils/ip.test.ts +255 -0
- package/src/utils/ip.ts +211 -0
- package/src/utils/json.ts +25 -0
- package/src/utils/string.ts +3 -0
- package/src/utils/url.ts +43 -0
- package/tsconfig.json +7 -0
- package/tsdown.config.ts +35 -0
- package/vitest.config.ts +3 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createRateLimitKey, isValidIP, normalizeIP } from "./ip";
|
|
3
|
+
|
|
4
|
+
describe("IP Normalization", () => {
|
|
5
|
+
describe("isValidIP", () => {
|
|
6
|
+
it("should validate IPv4 addresses", () => {
|
|
7
|
+
expect(isValidIP("192.168.1.1")).toBe(true);
|
|
8
|
+
expect(isValidIP("127.0.0.1")).toBe(true);
|
|
9
|
+
expect(isValidIP("0.0.0.0")).toBe(true);
|
|
10
|
+
expect(isValidIP("255.255.255.255")).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should validate IPv6 addresses", () => {
|
|
14
|
+
expect(isValidIP("2001:db8::1")).toBe(true);
|
|
15
|
+
expect(isValidIP("::1")).toBe(true);
|
|
16
|
+
expect(isValidIP("::")).toBe(true);
|
|
17
|
+
expect(isValidIP("2001:0db8:0000:0000:0000:0000:0000:0001")).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should reject invalid IPs", () => {
|
|
21
|
+
expect(isValidIP("not-an-ip")).toBe(false);
|
|
22
|
+
expect(isValidIP("999.999.999.999")).toBe(false);
|
|
23
|
+
expect(isValidIP("gggg::1")).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("IPv4 Normalization", () => {
|
|
28
|
+
it("should return IPv4 addresses unchanged", () => {
|
|
29
|
+
expect(normalizeIP("192.168.1.1")).toBe("192.168.1.1");
|
|
30
|
+
expect(normalizeIP("127.0.0.1")).toBe("127.0.0.1");
|
|
31
|
+
expect(normalizeIP("10.0.0.1")).toBe("10.0.0.1");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("IPv6 Normalization", () => {
|
|
36
|
+
it("should normalize compressed IPv6 to full form", () => {
|
|
37
|
+
expect(normalizeIP("2001:db8::1", { ipv6Subnet: 128 })).toBe(
|
|
38
|
+
"2001:0db8:0000:0000:0000:0000:0000:0001",
|
|
39
|
+
);
|
|
40
|
+
expect(normalizeIP("::1", { ipv6Subnet: 128 })).toBe(
|
|
41
|
+
"0000:0000:0000:0000:0000:0000:0000:0001",
|
|
42
|
+
);
|
|
43
|
+
expect(normalizeIP("::", { ipv6Subnet: 128 })).toBe(
|
|
44
|
+
"0000:0000:0000:0000:0000:0000:0000:0000",
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should normalize uppercase to lowercase", () => {
|
|
49
|
+
expect(normalizeIP("2001:DB8::1", { ipv6Subnet: 128 })).toBe(
|
|
50
|
+
"2001:0db8:0000:0000:0000:0000:0000:0001",
|
|
51
|
+
);
|
|
52
|
+
expect(normalizeIP("2001:0DB8:ABCD:EF00::1", { ipv6Subnet: 128 })).toBe(
|
|
53
|
+
"2001:0db8:abcd:ef00:0000:0000:0000:0001",
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should handle various IPv6 formats consistently", () => {
|
|
58
|
+
// All these represent the same address
|
|
59
|
+
const normalized = "2001:0db8:0000:0000:0000:0000:0000:0001";
|
|
60
|
+
expect(normalizeIP("2001:db8::1", { ipv6Subnet: 128 })).toBe(normalized);
|
|
61
|
+
expect(normalizeIP("2001:0db8:0:0:0:0:0:1", { ipv6Subnet: 128 })).toBe(
|
|
62
|
+
normalized,
|
|
63
|
+
);
|
|
64
|
+
expect(normalizeIP("2001:db8:0::1", { ipv6Subnet: 128 })).toBe(
|
|
65
|
+
normalized,
|
|
66
|
+
);
|
|
67
|
+
expect(normalizeIP("2001:0db8::0:0:0:1", { ipv6Subnet: 128 })).toBe(
|
|
68
|
+
normalized,
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should handle IPv6 with :: at different positions", () => {
|
|
73
|
+
expect(
|
|
74
|
+
normalizeIP("2001:db8:85a3::8a2e:370:7334", { ipv6Subnet: 128 }),
|
|
75
|
+
).toBe("2001:0db8:85a3:0000:0000:8a2e:0370:7334");
|
|
76
|
+
expect(normalizeIP("::ffff:192.0.2.1")).not.toContain("::");
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("IPv4-mapped IPv6 Conversion", () => {
|
|
81
|
+
it("should convert IPv4-mapped IPv6 to IPv4", () => {
|
|
82
|
+
expect(normalizeIP("::ffff:192.0.2.1")).toBe("192.0.2.1");
|
|
83
|
+
expect(normalizeIP("::ffff:127.0.0.1")).toBe("127.0.0.1");
|
|
84
|
+
expect(normalizeIP("::FFFF:10.0.0.1")).toBe("10.0.0.1");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should handle hex-encoded IPv4 in mapped addresses", () => {
|
|
88
|
+
// ::ffff:c000:0201 = ::ffff:192.0.2.1 = 192.0.2.1
|
|
89
|
+
expect(normalizeIP("::ffff:c000:0201")).toBe("192.0.2.1");
|
|
90
|
+
// ::ffff:7f00:0001 = ::ffff:127.0.0.1 = 127.0.0.1
|
|
91
|
+
expect(normalizeIP("::ffff:7f00:0001")).toBe("127.0.0.1");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should handle full form IPv4-mapped IPv6", () => {
|
|
95
|
+
expect(normalizeIP("0:0:0:0:0:ffff:192.0.2.1")).toBe("192.0.2.1");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("IPv6 Subnet Support", () => {
|
|
100
|
+
it("should extract /64 subnet", () => {
|
|
101
|
+
/* cspell:disable-next-line */
|
|
102
|
+
const ip1 = normalizeIP("2001:db8:0:0:1234:5678:90ab:cdef", {
|
|
103
|
+
ipv6Subnet: 64,
|
|
104
|
+
});
|
|
105
|
+
const ip2 = normalizeIP("2001:db8:0:0:ffff:ffff:ffff:ffff", {
|
|
106
|
+
ipv6Subnet: 64,
|
|
107
|
+
});
|
|
108
|
+
// Both should have same /64 prefix
|
|
109
|
+
expect(ip1).toBe("2001:0db8:0000:0000:0000:0000:0000:0000");
|
|
110
|
+
expect(ip2).toBe("2001:0db8:0000:0000:0000:0000:0000:0000");
|
|
111
|
+
expect(ip1).toBe(ip2);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should extract /48 subnet", () => {
|
|
115
|
+
/* cspell:disable-next-line */
|
|
116
|
+
const ip1 = normalizeIP("2001:db8:1234:5678:90ab:cdef:1234:5678", {
|
|
117
|
+
ipv6Subnet: 48,
|
|
118
|
+
});
|
|
119
|
+
const ip2 = normalizeIP("2001:db8:1234:ffff:ffff:ffff:ffff:ffff", {
|
|
120
|
+
ipv6Subnet: 48,
|
|
121
|
+
});
|
|
122
|
+
// Both should have same /48 prefix
|
|
123
|
+
expect(ip1).toBe("2001:0db8:1234:0000:0000:0000:0000:0000");
|
|
124
|
+
expect(ip2).toBe("2001:0db8:1234:0000:0000:0000:0000:0000");
|
|
125
|
+
expect(ip1).toBe(ip2);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("should extract /32 subnet", () => {
|
|
129
|
+
/* cspell:disable-next-line */
|
|
130
|
+
const ip1 = normalizeIP("2001:db8:1234:5678:90ab:cdef:1234:5678", {
|
|
131
|
+
ipv6Subnet: 32,
|
|
132
|
+
});
|
|
133
|
+
const ip2 = normalizeIP("2001:db8:ffff:ffff:ffff:ffff:ffff:ffff", {
|
|
134
|
+
ipv6Subnet: 32,
|
|
135
|
+
});
|
|
136
|
+
// Both should have same /32 prefix
|
|
137
|
+
expect(ip1).toBe("2001:0db8:0000:0000:0000:0000:0000:0000");
|
|
138
|
+
expect(ip2).toBe("2001:0db8:0000:0000:0000:0000:0000:0000");
|
|
139
|
+
expect(ip1).toBe(ip2);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("should handle /64 subnet by default", () => {
|
|
143
|
+
const ip1 = normalizeIP("2001:db8::1");
|
|
144
|
+
const ip2 = normalizeIP("2001:db8::1", { ipv6Subnet: 64 });
|
|
145
|
+
expect(ip1).toBe(ip2);
|
|
146
|
+
expect(ip1).toBe("2001:0db8:0000:0000:0000:0000:0000:0000");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should not affect IPv4 addresses when ipv6Subnet is set", () => {
|
|
150
|
+
expect(normalizeIP("192.168.1.1", { ipv6Subnet: 64 })).toBe(
|
|
151
|
+
"192.168.1.1",
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("Rate Limit Key Creation", () => {
|
|
157
|
+
it("should create keys with separator", () => {
|
|
158
|
+
expect(createRateLimitKey("192.168.1.1", "/sign-in")).toBe(
|
|
159
|
+
"192.168.1.1|/sign-in",
|
|
160
|
+
);
|
|
161
|
+
expect(createRateLimitKey("2001:db8::1", "/api/auth")).toBe(
|
|
162
|
+
"2001:db8::1|/api/auth",
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("should prevent collision attacks", () => {
|
|
167
|
+
// Without separator: "192.0.2.1" + "/sign-in" = "192.0.2.1/sign-in"
|
|
168
|
+
// "192.0.2" + ".1/sign-in" = "192.0.2.1/sign-in"
|
|
169
|
+
// With separator: they're different
|
|
170
|
+
const key1 = createRateLimitKey("192.0.2.1", "/sign-in");
|
|
171
|
+
const key2 = createRateLimitKey("192.0.2", ".1/sign-in");
|
|
172
|
+
expect(key1).not.toBe(key2);
|
|
173
|
+
expect(key1).toBe("192.0.2.1|/sign-in");
|
|
174
|
+
expect(key2).toBe("192.0.2|.1/sign-in");
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("Security: Bypass Prevention", () => {
|
|
179
|
+
it("should prevent IPv6 representation bypass", () => {
|
|
180
|
+
// Attacker tries different representations of same address
|
|
181
|
+
const representations = [
|
|
182
|
+
"2001:db8::1",
|
|
183
|
+
"2001:DB8::1",
|
|
184
|
+
"2001:0db8::1",
|
|
185
|
+
"2001:db8:0::1",
|
|
186
|
+
"2001:0db8:0:0:0:0:0:1",
|
|
187
|
+
"2001:db8::0:1",
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
const normalized = representations.map((ip) =>
|
|
191
|
+
normalizeIP(ip, { ipv6Subnet: 128 }),
|
|
192
|
+
);
|
|
193
|
+
// All should normalize to the same value
|
|
194
|
+
const uniqueValues = new Set(normalized);
|
|
195
|
+
expect(uniqueValues.size).toBe(1);
|
|
196
|
+
expect(normalized[0]).toBe("2001:0db8:0000:0000:0000:0000:0000:0001");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("should prevent IPv4-mapped bypass", () => {
|
|
200
|
+
// Attacker switches between IPv4 and IPv4-mapped IPv6
|
|
201
|
+
const ip1 = normalizeIP("192.0.2.1");
|
|
202
|
+
const ip2 = normalizeIP("::ffff:192.0.2.1");
|
|
203
|
+
const ip3 = normalizeIP("::FFFF:192.0.2.1");
|
|
204
|
+
const ip4 = normalizeIP("::ffff:c000:0201");
|
|
205
|
+
|
|
206
|
+
// All should normalize to the same IPv4
|
|
207
|
+
expect(ip1).toBe("192.0.2.1");
|
|
208
|
+
expect(ip2).toBe("192.0.2.1");
|
|
209
|
+
expect(ip3).toBe("192.0.2.1");
|
|
210
|
+
expect(ip4).toBe("192.0.2.1");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("should group IPv6 subnet attacks", () => {
|
|
214
|
+
// Attacker rotates through addresses in their /64 allocation
|
|
215
|
+
const attackIPs = [
|
|
216
|
+
"2001:db8:abcd:1234:0000:0000:0000:0001",
|
|
217
|
+
"2001:db8:abcd:1234:1111:2222:3333:4444",
|
|
218
|
+
"2001:db8:abcd:1234:ffff:ffff:ffff:ffff",
|
|
219
|
+
"2001:db8:abcd:1234:aaaa:bbbb:cccc:dddd",
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
const normalized = attackIPs.map((ip) =>
|
|
223
|
+
normalizeIP(ip, { ipv6Subnet: 64 }),
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// All should map to same /64 subnet
|
|
227
|
+
const uniqueValues = new Set(normalized);
|
|
228
|
+
expect(uniqueValues.size).toBe(1);
|
|
229
|
+
expect(normalized[0]).toBe("2001:0db8:abcd:1234:0000:0000:0000:0000");
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe("Edge Cases", () => {
|
|
234
|
+
it("should handle localhost addresses", () => {
|
|
235
|
+
expect(normalizeIP("127.0.0.1")).toBe("127.0.0.1");
|
|
236
|
+
expect(normalizeIP("::1", { ipv6Subnet: 128 })).toBe(
|
|
237
|
+
"0000:0000:0000:0000:0000:0000:0000:0001",
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("should handle all-zeros address", () => {
|
|
242
|
+
expect(normalizeIP("0.0.0.0")).toBe("0.0.0.0");
|
|
243
|
+
expect(normalizeIP("::", { ipv6Subnet: 128 })).toBe(
|
|
244
|
+
"0000:0000:0000:0000:0000:0000:0000:0000",
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("should handle link-local addresses", () => {
|
|
249
|
+
expect(normalizeIP("169.254.0.1")).toBe("169.254.0.1");
|
|
250
|
+
expect(normalizeIP("fe80::1", { ipv6Subnet: 128 })).toBe(
|
|
251
|
+
"fe80:0000:0000:0000:0000:0000:0000:0001",
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
});
|
package/src/utils/ip.ts
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalizes an IP address for consistent rate limiting.
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Normalizes IPv6 to canonical lowercase form
|
|
8
|
+
* - Converts IPv4-mapped IPv6 to IPv4
|
|
9
|
+
* - Supports IPv6 subnet extraction
|
|
10
|
+
* - Handles all edge cases (::1, ::, etc.)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
interface NormalizeIPOptions {
|
|
14
|
+
/**
|
|
15
|
+
* For IPv6 addresses, extract the subnet prefix instead of full address.
|
|
16
|
+
* Common values: 32, 48, 64, 128 (default: 128 = full address)
|
|
17
|
+
*
|
|
18
|
+
* @default 128
|
|
19
|
+
*/
|
|
20
|
+
ipv6Subnet?: 128 | 64 | 48 | 32;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Checks if an IP is valid IPv4 or IPv6
|
|
25
|
+
*/
|
|
26
|
+
export function isValidIP(ip: string): boolean {
|
|
27
|
+
return z.ipv4().safeParse(ip).success || z.ipv6().safeParse(ip).success;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Checks if an IP is IPv6
|
|
32
|
+
*/
|
|
33
|
+
function isIPv6(ip: string): boolean {
|
|
34
|
+
return z.ipv6().safeParse(ip).success;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Converts IPv4-mapped IPv6 address to IPv4
|
|
39
|
+
* e.g., "::ffff:192.0.2.1" -> "192.0.2.1"
|
|
40
|
+
*/
|
|
41
|
+
function extractIPv4FromMapped(ipv6: string): string | null {
|
|
42
|
+
const lower = ipv6.toLowerCase();
|
|
43
|
+
|
|
44
|
+
// Handle ::ffff:192.0.2.1 format
|
|
45
|
+
if (lower.startsWith("::ffff:")) {
|
|
46
|
+
const ipv4Part = lower.substring(7);
|
|
47
|
+
// Check if it's a valid IPv4
|
|
48
|
+
if (z.ipv4().safeParse(ipv4Part).success) {
|
|
49
|
+
return ipv4Part;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Handle full form: 0:0:0:0:0:ffff:192.0.2.1
|
|
54
|
+
const parts = ipv6.split(":");
|
|
55
|
+
if (parts.length === 7 && parts[5]?.toLowerCase() === "ffff") {
|
|
56
|
+
const ipv4Part = parts[6];
|
|
57
|
+
if (ipv4Part && z.ipv4().safeParse(ipv4Part).success) {
|
|
58
|
+
return ipv4Part;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Handle hex-encoded IPv4 in mapped address
|
|
63
|
+
// e.g., ::ffff:c000:0201 -> 192.0.2.1
|
|
64
|
+
if (lower.includes("::ffff:") || lower.includes(":ffff:")) {
|
|
65
|
+
const groups = expandIPv6(ipv6);
|
|
66
|
+
if (
|
|
67
|
+
groups.length === 8 &&
|
|
68
|
+
groups[0] === "0000" &&
|
|
69
|
+
groups[1] === "0000" &&
|
|
70
|
+
groups[2] === "0000" &&
|
|
71
|
+
groups[3] === "0000" &&
|
|
72
|
+
groups[4] === "0000" &&
|
|
73
|
+
groups[5] === "ffff" &&
|
|
74
|
+
groups[6] &&
|
|
75
|
+
groups[7]
|
|
76
|
+
) {
|
|
77
|
+
// Convert last two groups to IPv4
|
|
78
|
+
const byte1 = Number.parseInt(groups[6].substring(0, 2), 16);
|
|
79
|
+
const byte2 = Number.parseInt(groups[6].substring(2, 4), 16);
|
|
80
|
+
const byte3 = Number.parseInt(groups[7].substring(0, 2), 16);
|
|
81
|
+
const byte4 = Number.parseInt(groups[7].substring(2, 4), 16);
|
|
82
|
+
return `${byte1}.${byte2}.${byte3}.${byte4}`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Expands a compressed IPv6 address to full form
|
|
91
|
+
* e.g., "2001:db8::1" -> ["2001", "0db8", "0000", "0000", "0000", "0000", "0000", "0001"]
|
|
92
|
+
*/
|
|
93
|
+
function expandIPv6(ipv6: string): string[] {
|
|
94
|
+
// Handle :: notation (zero compression)
|
|
95
|
+
if (ipv6.includes("::")) {
|
|
96
|
+
const sides = ipv6.split("::");
|
|
97
|
+
const left = sides[0] ? sides[0].split(":") : [];
|
|
98
|
+
const right = sides[1] ? sides[1].split(":") : [];
|
|
99
|
+
|
|
100
|
+
// Calculate missing groups
|
|
101
|
+
const totalGroups = 8;
|
|
102
|
+
const missingGroups = totalGroups - left.length - right.length;
|
|
103
|
+
const zeros = Array(missingGroups).fill("0000");
|
|
104
|
+
|
|
105
|
+
// Pad existing groups to 4 digits
|
|
106
|
+
const paddedLeft = left.map((g) => g.padStart(4, "0"));
|
|
107
|
+
const paddedRight = right.map((g) => g.padStart(4, "0"));
|
|
108
|
+
|
|
109
|
+
return [...paddedLeft, ...zeros, ...paddedRight];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// No compression, just pad each group
|
|
113
|
+
return ipv6.split(":").map((g) => g.padStart(4, "0"));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Normalizes an IPv6 address to canonical form
|
|
118
|
+
* e.g., "2001:DB8::1" -> "2001:0db8:0000:0000:0000:0000:0000:0001"
|
|
119
|
+
*/
|
|
120
|
+
function normalizeIPv6(
|
|
121
|
+
ipv6: string,
|
|
122
|
+
subnetPrefix?: 128 | 32 | 48 | 64,
|
|
123
|
+
): string {
|
|
124
|
+
const groups = expandIPv6(ipv6);
|
|
125
|
+
|
|
126
|
+
if (subnetPrefix && subnetPrefix < 128) {
|
|
127
|
+
// Apply subnet mask
|
|
128
|
+
const prefix = subnetPrefix;
|
|
129
|
+
let bitsRemaining: number = prefix;
|
|
130
|
+
|
|
131
|
+
const maskedGroups = groups.map((group) => {
|
|
132
|
+
if (bitsRemaining <= 0) {
|
|
133
|
+
return "0000";
|
|
134
|
+
}
|
|
135
|
+
if (bitsRemaining >= 16) {
|
|
136
|
+
bitsRemaining -= 16;
|
|
137
|
+
return group;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Partial mask for this group
|
|
141
|
+
const value = Number.parseInt(group, 16);
|
|
142
|
+
const mask = (0xffff << (16 - bitsRemaining)) & 0xffff;
|
|
143
|
+
const masked = value & mask;
|
|
144
|
+
bitsRemaining = 0;
|
|
145
|
+
return masked.toString(16).padStart(4, "0");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return maskedGroups.join(":").toLowerCase();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return groups.join(":").toLowerCase();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Normalizes an IP address (IPv4 or IPv6) for consistent rate limiting.
|
|
156
|
+
*
|
|
157
|
+
* @param ip - The IP address to normalize
|
|
158
|
+
* @param options - Normalization options
|
|
159
|
+
* @returns Normalized IP address
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* normalizeIP("2001:DB8::1")
|
|
163
|
+
* // -> "2001:0db8:0000:0000:0000:0000:0000:0000"
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* normalizeIP("::ffff:192.0.2.1")
|
|
167
|
+
* // -> "192.0.2.1" (converted to IPv4)
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* normalizeIP("2001:db8::1", { ipv6Subnet: 64 })
|
|
171
|
+
* // -> "2001:0db8:0000:0000:0000:0000:0000:0000" (subnet /64)
|
|
172
|
+
*/
|
|
173
|
+
export function normalizeIP(
|
|
174
|
+
ip: string,
|
|
175
|
+
options: NormalizeIPOptions = {},
|
|
176
|
+
): string {
|
|
177
|
+
// IPv4 addresses are already normalized
|
|
178
|
+
if (z.ipv4().safeParse(ip).success) {
|
|
179
|
+
return ip.toLowerCase();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check if it's IPv6
|
|
183
|
+
if (!isIPv6(ip)) {
|
|
184
|
+
// Return as-is if not valid (shouldn't happen due to prior validation)
|
|
185
|
+
return ip.toLowerCase();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Check for IPv4-mapped IPv6
|
|
189
|
+
const ipv4 = extractIPv4FromMapped(ip);
|
|
190
|
+
if (ipv4) {
|
|
191
|
+
return ipv4.toLowerCase();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Normalize IPv6
|
|
195
|
+
const subnetPrefix = options.ipv6Subnet || 64;
|
|
196
|
+
return normalizeIPv6(ip, subnetPrefix);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Creates a rate limit key from IP and path
|
|
201
|
+
* Uses a separator to prevent collision attacks
|
|
202
|
+
*
|
|
203
|
+
* @param ip - The IP address (should be normalized)
|
|
204
|
+
* @param path - The request path
|
|
205
|
+
* @returns Rate limit key
|
|
206
|
+
*/
|
|
207
|
+
export function createRateLimitKey(ip: string, path: string): string {
|
|
208
|
+
// Use | as separator to prevent collision attacks
|
|
209
|
+
// e.g., "192.0.2.1" + "/sign-in" vs "192.0.2" + ".1/sign-in"
|
|
210
|
+
return `${ip}|${path}`;
|
|
211
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { logger } from "../env";
|
|
2
|
+
|
|
3
|
+
export function safeJSONParse<T>(data: unknown): T | null {
|
|
4
|
+
function reviver(_: string, value: any): any {
|
|
5
|
+
if (typeof value === "string") {
|
|
6
|
+
const iso8601Regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
|
|
7
|
+
if (iso8601Regex.test(value)) {
|
|
8
|
+
const date = new Date(value);
|
|
9
|
+
if (!isNaN(date.getTime())) {
|
|
10
|
+
return date;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
if (typeof data !== "string") {
|
|
18
|
+
return data as T;
|
|
19
|
+
}
|
|
20
|
+
return JSON.parse(data, reviver);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
logger.error("Error parsing JSON", { error: e });
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/utils/url.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes a request pathname by removing the basePath prefix and trailing slashes.
|
|
3
|
+
* This is useful for matching paths against configured path lists.
|
|
4
|
+
*
|
|
5
|
+
* @param requestUrl - The full request URL
|
|
6
|
+
* @param basePath - The base path of the auth API (e.g., "/api/auth")
|
|
7
|
+
* @returns The normalized path without basePath prefix or trailing slashes,
|
|
8
|
+
* or "/" if URL parsing fails
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* normalizePathname("http://localhost:3000/api/auth/sso/saml2/callback/provider1", "/api/auth")
|
|
12
|
+
* // Returns: "/sso/saml2/callback/provider1"
|
|
13
|
+
*
|
|
14
|
+
* normalizePathname("http://localhost:3000/sso/saml2/callback/provider1/", "/")
|
|
15
|
+
* // Returns: "/sso/saml2/callback/provider1"
|
|
16
|
+
*/
|
|
17
|
+
export function normalizePathname(
|
|
18
|
+
requestUrl: string,
|
|
19
|
+
basePath: string,
|
|
20
|
+
): string {
|
|
21
|
+
let pathname: string;
|
|
22
|
+
try {
|
|
23
|
+
pathname = new URL(requestUrl).pathname.replace(/\/+$/, "") || "/";
|
|
24
|
+
} catch {
|
|
25
|
+
return "/";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (basePath === "/" || basePath === "") {
|
|
29
|
+
return pathname;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check for exact match or proper path boundary (basePath followed by "/" or end)
|
|
33
|
+
// This prevents "/api/auth" from matching "/api/authevil/..."
|
|
34
|
+
if (pathname === basePath) {
|
|
35
|
+
return "/";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (pathname.startsWith(basePath + "/")) {
|
|
39
|
+
return pathname.slice(basePath.length).replace(/\/+$/, "") || "/";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return pathname;
|
|
43
|
+
}
|
package/tsconfig.json
ADDED
package/tsdown.config.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { defineConfig } from "tsdown";
|
|
3
|
+
|
|
4
|
+
const packageJson = JSON.parse(
|
|
5
|
+
await readFile(new URL("./package.json", import.meta.url), "utf-8"),
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
dts: { build: true, incremental: true },
|
|
10
|
+
format: ["esm"],
|
|
11
|
+
entry: [
|
|
12
|
+
"./src/index.ts",
|
|
13
|
+
"./src/db/index.ts",
|
|
14
|
+
"./src/db/adapter/index.ts",
|
|
15
|
+
"./src/async_hooks/index.ts",
|
|
16
|
+
"./src/async_hooks/pure.index.ts",
|
|
17
|
+
"./src/context/index.ts",
|
|
18
|
+
"./src/env/index.ts",
|
|
19
|
+
"./src/oauth2/index.ts",
|
|
20
|
+
"./src/api/index.ts",
|
|
21
|
+
"./src/social-providers/index.ts",
|
|
22
|
+
"./src/utils/*.ts",
|
|
23
|
+
"!./src/utils/*.test.ts",
|
|
24
|
+
"./src/error/index.ts",
|
|
25
|
+
],
|
|
26
|
+
external: ["@better-auth/core/async_hooks"],
|
|
27
|
+
env: {
|
|
28
|
+
BETTER_AUTH_VERSION: packageJson.version,
|
|
29
|
+
BETTER_AUTH_TELEMETRY_ENDPOINT:
|
|
30
|
+
process.env.BETTER_AUTH_TELEMETRY_ENDPOINT ?? "",
|
|
31
|
+
},
|
|
32
|
+
sourcemap: true,
|
|
33
|
+
unbundle: true,
|
|
34
|
+
clean: true,
|
|
35
|
+
});
|
package/vitest.config.ts
ADDED