@better-auth/core 1.7.2 → 1.7.3
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/api/index.d.mts +3 -0
- package/dist/context/global.mjs +1 -1
- package/dist/context/transaction.mjs +3 -0
- package/dist/db/adapter/atomic-fallback.mjs +134 -0
- package/dist/db/adapter/factory.mjs +22 -4
- package/dist/db/adapter/index.d.mts +15 -11
- package/dist/db/get-tables.mjs +1 -9
- package/dist/db/index.d.mts +2 -2
- package/dist/db/index.mjs +2 -2
- package/dist/db/internal.d.mts +3 -1
- package/dist/db/internal.mjs +3 -1
- package/dist/db/schema/account.d.mts +2 -13
- package/dist/db/schema/account.mjs +1 -19
- package/dist/db/schema-check.d.mts +48 -0
- package/dist/db/schema-check.mjs +80 -0
- package/dist/db/schema-diff.d.mts +104 -0
- package/dist/db/schema-diff.mjs +154 -0
- package/dist/instrumentation/tracer.mjs +1 -1
- package/dist/oauth2/index.d.mts +2 -2
- package/dist/oauth2/oauth-provider.d.mts +0 -10
- package/dist/oauth2/token-endpoint-auth.d.mts +26 -2
- package/dist/oauth2/token-endpoint-auth.mjs +11 -0
- package/dist/social-providers/apple.d.mts +0 -1
- package/dist/social-providers/apple.mjs +0 -1
- package/dist/social-providers/cloudflare.d.mts +132 -0
- package/dist/social-providers/cloudflare.mjs +85 -0
- package/dist/social-providers/cognito.d.mts +0 -1
- package/dist/social-providers/cognito.mjs +0 -1
- package/dist/social-providers/facebook.d.mts +0 -1
- package/dist/social-providers/facebook.mjs +0 -1
- package/dist/social-providers/google.d.mts +0 -1
- package/dist/social-providers/google.mjs +0 -1
- package/dist/social-providers/index.d.mts +53 -21
- package/dist/social-providers/index.mjs +3 -1
- package/dist/social-providers/line.d.mts +0 -1
- package/dist/social-providers/line.mjs +0 -1
- package/dist/social-providers/microsoft-entra-id.d.mts +0 -3
- package/dist/social-providers/microsoft-entra-id.mjs +0 -1
- package/dist/social-providers/paybin.d.mts +0 -1
- package/dist/social-providers/paybin.mjs +0 -1
- package/dist/social-providers/paypal.d.mts +3 -11
- package/dist/social-providers/paypal.mjs +20 -47
- package/dist/social-providers/reddit.mjs +17 -22
- package/dist/social-providers/tiktok.d.mts +1 -0
- package/dist/social-providers/tiktok.mjs +14 -9
- package/dist/types/context.d.mts +11 -0
- package/dist/types/init-options.d.mts +11 -0
- package/dist/utils/ip.mjs +11 -9
- package/package.json +2 -2
- package/src/context/transaction.ts +5 -0
- package/src/db/adapter/atomic-fallback.ts +237 -0
- package/src/db/adapter/factory.ts +33 -17
- package/src/db/adapter/index.ts +15 -11
- package/src/db/get-tables.ts +1 -14
- package/src/db/index.ts +0 -2
- package/src/db/internal.ts +19 -0
- package/src/db/schema/account.ts +3 -22
- package/src/db/schema/user.ts +1 -1
- package/src/db/schema-check.ts +107 -0
- package/src/db/schema-diff.ts +270 -0
- package/src/oauth2/index.ts +2 -0
- package/src/oauth2/oauth-provider.ts +0 -10
- package/src/oauth2/token-endpoint-auth.ts +39 -6
- package/src/social-providers/apple.ts +0 -1
- package/src/social-providers/cloudflare.ts +221 -0
- package/src/social-providers/cognito.ts +0 -1
- package/src/social-providers/facebook.ts +0 -1
- package/src/social-providers/google.ts +0 -1
- package/src/social-providers/index.ts +3 -0
- package/src/social-providers/line.ts +0 -1
- package/src/social-providers/microsoft-entra-id.ts +0 -1
- package/src/social-providers/paybin.ts +0 -1
- package/src/social-providers/paypal.ts +30 -71
- package/src/social-providers/reddit.ts +27 -36
- package/src/social-providers/tiktok.ts +18 -13
- package/src/types/context.ts +11 -0
- package/src/types/init-options.ts +11 -0
- package/src/utils/ip.ts +13 -9
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import { base64 } from "@better-auth/utils/base64";
|
|
2
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
3
2
|
import { decodeJwt } from "jose";
|
|
4
3
|
import { logger } from "../env";
|
|
5
4
|
import { BetterAuthError } from "../error";
|
|
6
|
-
import type {
|
|
7
|
-
|
|
5
|
+
import type {
|
|
6
|
+
OAuthProvider,
|
|
7
|
+
ProviderOptions,
|
|
8
|
+
TokenEndpointAuth,
|
|
9
|
+
} from "../oauth2";
|
|
10
|
+
import {
|
|
11
|
+
createAuthorizationURL,
|
|
12
|
+
refreshAccessToken,
|
|
13
|
+
validateAuthorizationCode,
|
|
14
|
+
} from "../oauth2";
|
|
8
15
|
|
|
9
16
|
export interface PayPalProfile {
|
|
10
17
|
sub?: string | undefined;
|
|
@@ -75,6 +82,13 @@ export const paypal = (options: PayPalOptions) => {
|
|
|
75
82
|
const userInfoEndpoint = isSandbox
|
|
76
83
|
? "https://api-m.sandbox.paypal.com/v1/identity/oauth2/userinfo"
|
|
77
84
|
: "https://api-m.paypal.com/v1/identity/oauth2/userinfo";
|
|
85
|
+
const tokenRequestOptions = {
|
|
86
|
+
clientId: options.clientId,
|
|
87
|
+
clientSecret: options.clientSecret,
|
|
88
|
+
};
|
|
89
|
+
const tokenEndpointAuth = {
|
|
90
|
+
method: "client_secret_basic",
|
|
91
|
+
} satisfies TokenEndpointAuth;
|
|
78
92
|
|
|
79
93
|
return {
|
|
80
94
|
id: "paypal",
|
|
@@ -115,47 +129,16 @@ export const paypal = (options: PayPalOptions) => {
|
|
|
115
129
|
return url;
|
|
116
130
|
},
|
|
117
131
|
|
|
118
|
-
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
119
|
-
/**
|
|
120
|
-
* PayPal requires Basic Auth for token exchange
|
|
121
|
-
**/
|
|
122
|
-
|
|
123
|
-
const credentials = base64.encode(
|
|
124
|
-
`${options.clientId}:${options.clientSecret}`,
|
|
125
|
-
);
|
|
126
|
-
|
|
132
|
+
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
127
133
|
try {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
},
|
|
136
|
-
body: new URLSearchParams({
|
|
137
|
-
grant_type: "authorization_code",
|
|
138
|
-
code: code,
|
|
139
|
-
redirect_uri: redirectURI,
|
|
140
|
-
}).toString(),
|
|
134
|
+
return await validateAuthorizationCode({
|
|
135
|
+
code,
|
|
136
|
+
codeVerifier,
|
|
137
|
+
redirectURI: options.redirectURI || redirectURI,
|
|
138
|
+
options: tokenRequestOptions,
|
|
139
|
+
tokenEndpoint,
|
|
140
|
+
tokenEndpointAuth,
|
|
141
141
|
});
|
|
142
|
-
|
|
143
|
-
if (!response.data) {
|
|
144
|
-
throw new BetterAuthError("FAILED_TO_GET_ACCESS_TOKEN");
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const data = response.data as PayPalTokenResponse;
|
|
148
|
-
|
|
149
|
-
const result = {
|
|
150
|
-
accessToken: data.access_token,
|
|
151
|
-
refreshToken: data.refresh_token,
|
|
152
|
-
accessTokenExpiresAt: data.expires_in
|
|
153
|
-
? new Date(Date.now() + data.expires_in * 1000)
|
|
154
|
-
: undefined,
|
|
155
|
-
idToken: data.id_token,
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
return result;
|
|
159
142
|
} catch (error) {
|
|
160
143
|
logger.error("PayPal token exchange failed:", error);
|
|
161
144
|
throw new BetterAuthError("FAILED_TO_GET_ACCESS_TOKEN");
|
|
@@ -165,37 +148,13 @@ export const paypal = (options: PayPalOptions) => {
|
|
|
165
148
|
refreshAccessToken: options.refreshAccessToken
|
|
166
149
|
? options.refreshAccessToken
|
|
167
150
|
: async (refreshToken) => {
|
|
168
|
-
const credentials = base64.encode(
|
|
169
|
-
`${options.clientId}:${options.clientSecret}`,
|
|
170
|
-
);
|
|
171
|
-
|
|
172
151
|
try {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
"Accept-Language": "en_US",
|
|
179
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
180
|
-
},
|
|
181
|
-
body: new URLSearchParams({
|
|
182
|
-
grant_type: "refresh_token",
|
|
183
|
-
refresh_token: refreshToken,
|
|
184
|
-
}).toString(),
|
|
152
|
+
return await refreshAccessToken({
|
|
153
|
+
refreshToken,
|
|
154
|
+
options: tokenRequestOptions,
|
|
155
|
+
tokenEndpoint,
|
|
156
|
+
tokenEndpointAuth,
|
|
185
157
|
});
|
|
186
|
-
|
|
187
|
-
if (!response.data) {
|
|
188
|
-
throw new BetterAuthError("FAILED_TO_REFRESH_ACCESS_TOKEN");
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const data = response.data as any;
|
|
192
|
-
return {
|
|
193
|
-
accessToken: data.access_token,
|
|
194
|
-
refreshToken: data.refresh_token,
|
|
195
|
-
accessTokenExpiresAt: data.expires_in
|
|
196
|
-
? new Date(Date.now() + data.expires_in * 1000)
|
|
197
|
-
: undefined,
|
|
198
|
-
};
|
|
199
158
|
} catch (error) {
|
|
200
159
|
logger.error("PayPal token refresh failed:", error);
|
|
201
160
|
throw new BetterAuthError("FAILED_TO_REFRESH_ACCESS_TOKEN");
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { base64 } from "@better-auth/utils/base64";
|
|
2
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
3
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
OAuthProvider,
|
|
4
|
+
ProviderOptions,
|
|
5
|
+
TokenEndpointAuth,
|
|
6
|
+
} from "../oauth2";
|
|
4
7
|
import {
|
|
5
8
|
createAuthorizationURL,
|
|
6
|
-
getOAuth2Tokens,
|
|
7
9
|
refreshAccessToken,
|
|
10
|
+
validateAuthorizationCode,
|
|
8
11
|
} from "../oauth2";
|
|
9
12
|
import { createPlaceholderEmail } from "../utils/email";
|
|
10
13
|
|
|
@@ -23,6 +26,15 @@ export interface RedditOptions extends ProviderOptions<RedditProfile> {
|
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
export const reddit = (options: RedditOptions) => {
|
|
29
|
+
const tokenEndpoint = "https://www.reddit.com/api/v1/access_token";
|
|
30
|
+
const tokenRequestOptions = {
|
|
31
|
+
clientId: options.clientId,
|
|
32
|
+
clientSecret: options.clientSecret,
|
|
33
|
+
};
|
|
34
|
+
const tokenEndpointAuth = {
|
|
35
|
+
method: "client_secret_basic",
|
|
36
|
+
} satisfies TokenEndpointAuth;
|
|
37
|
+
|
|
26
38
|
return {
|
|
27
39
|
id: "reddit",
|
|
28
40
|
name: "Reddit",
|
|
@@ -43,34 +55,17 @@ export const reddit = (options: RedditOptions) => {
|
|
|
43
55
|
});
|
|
44
56
|
},
|
|
45
57
|
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
46
|
-
|
|
47
|
-
grant_type: "authorization_code",
|
|
58
|
+
return validateAuthorizationCode({
|
|
48
59
|
code,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
`${options.clientId}:${options.clientSecret}`,
|
|
57
|
-
)}`,
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const { data, error } = await betterFetch<object>(
|
|
61
|
-
"https://www.reddit.com/api/v1/access_token",
|
|
62
|
-
{
|
|
63
|
-
method: "POST",
|
|
64
|
-
headers,
|
|
65
|
-
body: body.toString(),
|
|
60
|
+
redirectURI: options.redirectURI || redirectURI,
|
|
61
|
+
options: tokenRequestOptions,
|
|
62
|
+
tokenEndpoint,
|
|
63
|
+
tokenEndpointAuth,
|
|
64
|
+
headers: {
|
|
65
|
+
accept: "text/plain",
|
|
66
|
+
"user-agent": "better-auth",
|
|
66
67
|
},
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
if (error) {
|
|
70
|
-
throw error;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return getOAuth2Tokens(data);
|
|
68
|
+
});
|
|
74
69
|
},
|
|
75
70
|
|
|
76
71
|
refreshAccessToken: options.refreshAccessToken
|
|
@@ -78,13 +73,9 @@ export const reddit = (options: RedditOptions) => {
|
|
|
78
73
|
: async (refreshToken) => {
|
|
79
74
|
return refreshAccessToken({
|
|
80
75
|
refreshToken,
|
|
81
|
-
options:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
clientSecret: options.clientSecret,
|
|
85
|
-
},
|
|
86
|
-
authentication: "basic",
|
|
87
|
-
tokenEndpoint: "https://www.reddit.com/api/v1/access_token",
|
|
76
|
+
options: tokenRequestOptions,
|
|
77
|
+
tokenEndpoint,
|
|
78
|
+
tokenEndpointAuth,
|
|
88
79
|
});
|
|
89
80
|
},
|
|
90
81
|
async getUserInfo(token) {
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
OAuthProvider,
|
|
4
|
+
ProviderOptions,
|
|
5
|
+
TokenEndpointAuth,
|
|
6
|
+
} from "../oauth2";
|
|
3
7
|
import {
|
|
4
8
|
RESERVED_AUTHORIZATION_PARAMS_SET,
|
|
5
9
|
refreshAccessToken,
|
|
@@ -133,6 +137,13 @@ export interface TiktokOptions extends ProviderOptions<TiktokProfile> {
|
|
|
133
137
|
|
|
134
138
|
export const tiktok = (options: TiktokOptions) => {
|
|
135
139
|
const tokenEndpoint = "https://open.tiktokapis.com/v2/oauth/token/";
|
|
140
|
+
const tokenEndpointAuth = {
|
|
141
|
+
method: "custom",
|
|
142
|
+
customizeRequest({ body }) {
|
|
143
|
+
body.set("client_key", options.clientKey);
|
|
144
|
+
body.set("client_secret", options.clientSecret);
|
|
145
|
+
},
|
|
146
|
+
} satisfies TokenEndpointAuth;
|
|
136
147
|
return {
|
|
137
148
|
id: "tiktok",
|
|
138
149
|
name: "TikTok",
|
|
@@ -159,15 +170,14 @@ export const tiktok = (options: TiktokOptions) => {
|
|
|
159
170
|
return url;
|
|
160
171
|
},
|
|
161
172
|
|
|
162
|
-
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
|
173
|
+
validateAuthorizationCode: async ({ code, codeVerifier, redirectURI }) => {
|
|
163
174
|
return validateAuthorizationCode({
|
|
164
175
|
code,
|
|
176
|
+
codeVerifier,
|
|
165
177
|
redirectURI: options.redirectURI || redirectURI,
|
|
166
|
-
options: {
|
|
167
|
-
clientKey: options.clientKey,
|
|
168
|
-
clientSecret: options.clientSecret,
|
|
169
|
-
},
|
|
178
|
+
options: {},
|
|
170
179
|
tokenEndpoint,
|
|
180
|
+
tokenEndpointAuth,
|
|
171
181
|
});
|
|
172
182
|
},
|
|
173
183
|
refreshAccessToken: options.refreshAccessToken
|
|
@@ -175,14 +185,9 @@ export const tiktok = (options: TiktokOptions) => {
|
|
|
175
185
|
: async (refreshToken) => {
|
|
176
186
|
return refreshAccessToken({
|
|
177
187
|
refreshToken,
|
|
178
|
-
options: {
|
|
179
|
-
clientSecret: options.clientSecret,
|
|
180
|
-
},
|
|
188
|
+
options: {},
|
|
181
189
|
tokenEndpoint,
|
|
182
|
-
|
|
183
|
-
extraParams: {
|
|
184
|
-
client_key: options.clientKey,
|
|
185
|
-
},
|
|
190
|
+
tokenEndpointAuth,
|
|
186
191
|
});
|
|
187
192
|
},
|
|
188
193
|
async getUserInfo(token) {
|
package/src/types/context.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
} from "../db";
|
|
11
11
|
import type { DBAdapter, Where } from "../db/adapter";
|
|
12
12
|
import type { AccountKey } from "../db/schema/account";
|
|
13
|
+
import type { SchemaCheck } from "../db/schema-check";
|
|
13
14
|
import type { createLogger } from "../env";
|
|
14
15
|
import type { OAuthProvider } from "../oauth2";
|
|
15
16
|
import type {
|
|
@@ -417,6 +418,16 @@ export type AuthContext<Options extends BetterAuthOptions = BetterAuthOptions> =
|
|
|
417
418
|
"enabled" | "window" | "max" | "storage"
|
|
418
419
|
>;
|
|
419
420
|
adapter: DBAdapter<Options>;
|
|
421
|
+
/**
|
|
422
|
+
* Confirms the database can hold what this configuration writes.
|
|
423
|
+
*
|
|
424
|
+
* Shared by initialization and requests for this adapter instance;
|
|
425
|
+
* returns nothing once the schema is known to be clean.
|
|
426
|
+
* Context construction does not await the verdict, so migration
|
|
427
|
+
* tooling can still use a context whose schema needs repair.
|
|
428
|
+
* Absent when the check is disabled or the adapter registers none.
|
|
429
|
+
*/
|
|
430
|
+
checkSchema?: SchemaCheck | undefined;
|
|
420
431
|
internalAdapter: InternalAdapter<Options>;
|
|
421
432
|
createAuthCookie: CreateCookieGetterFn;
|
|
422
433
|
secret: string;
|
|
@@ -471,6 +471,17 @@ export type BetterAuthAdvancedOptions = {
|
|
|
471
471
|
* @default false
|
|
472
472
|
*/
|
|
473
473
|
joins?: boolean;
|
|
474
|
+
/**
|
|
475
|
+
* Validate the schema during initialization and report problems
|
|
476
|
+
* through the configured logger. Authentication requests await
|
|
477
|
+
* the same check and fail when the schema does not match.
|
|
478
|
+
* Kysely introspects the database; Drizzle and Prisma inspect
|
|
479
|
+
* local schema metadata without opening a connection.
|
|
480
|
+
* Set `false` to disable runtime schema validation.
|
|
481
|
+
*
|
|
482
|
+
* @default true
|
|
483
|
+
*/
|
|
484
|
+
validateSchema?: boolean;
|
|
474
485
|
}
|
|
475
486
|
| undefined;
|
|
476
487
|
/**
|
package/src/utils/ip.ts
CHANGED
|
@@ -2,6 +2,9 @@ import * as z from "zod";
|
|
|
2
2
|
import { isDevelopment, isTest } from "../env";
|
|
3
3
|
import type { BetterAuthOptions } from "../types";
|
|
4
4
|
|
|
5
|
+
const ipv4Schema = z.ipv4();
|
|
6
|
+
const ipv6Schema = z.ipv6();
|
|
7
|
+
|
|
5
8
|
/**
|
|
6
9
|
* Normalizes an IP address for consistent rate limiting.
|
|
7
10
|
*
|
|
@@ -27,14 +30,15 @@ interface NormalizeIPOptions {
|
|
|
27
30
|
* Checks if an IP is valid IPv4 or IPv6
|
|
28
31
|
*/
|
|
29
32
|
export function isValidIP(ip: string): boolean {
|
|
30
|
-
return
|
|
33
|
+
return isIPv4(ip) || isIPv6(ip);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isIPv4(ip: string): boolean {
|
|
37
|
+
return z.validate(ipv4Schema, ip);
|
|
31
38
|
}
|
|
32
39
|
|
|
33
|
-
/**
|
|
34
|
-
* Checks if an IP is IPv6
|
|
35
|
-
*/
|
|
36
40
|
function isIPv6(ip: string): boolean {
|
|
37
|
-
return z.
|
|
41
|
+
return z.validate(ipv6Schema, ip);
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
/**
|
|
@@ -48,7 +52,7 @@ function extractIPv4FromMapped(ipv6: string): string | null {
|
|
|
48
52
|
if (lower.startsWith("::ffff:")) {
|
|
49
53
|
const ipv4Part = lower.substring(7);
|
|
50
54
|
// Check if it's a valid IPv4
|
|
51
|
-
if (
|
|
55
|
+
if (isIPv4(ipv4Part)) {
|
|
52
56
|
return ipv4Part;
|
|
53
57
|
}
|
|
54
58
|
}
|
|
@@ -57,7 +61,7 @@ function extractIPv4FromMapped(ipv6: string): string | null {
|
|
|
57
61
|
const parts = ipv6.split(":");
|
|
58
62
|
if (parts.length === 7 && parts[5]?.toLowerCase() === "ffff") {
|
|
59
63
|
const ipv4Part = parts[6];
|
|
60
|
-
if (ipv4Part &&
|
|
64
|
+
if (ipv4Part && isIPv4(ipv4Part)) {
|
|
61
65
|
return ipv4Part;
|
|
62
66
|
}
|
|
63
67
|
}
|
|
@@ -176,7 +180,7 @@ export function normalizeIP(
|
|
|
176
180
|
options: NormalizeIPOptions = {},
|
|
177
181
|
): string {
|
|
178
182
|
// IPv4 addresses are already normalized
|
|
179
|
-
if (
|
|
183
|
+
if (isIPv4(ip)) {
|
|
180
184
|
return ip.toLowerCase();
|
|
181
185
|
}
|
|
182
186
|
|
|
@@ -201,7 +205,7 @@ export function normalizeIP(
|
|
|
201
205
|
* Raw bytes of an IP for CIDR comparison. Returns `null` for an invalid IP.
|
|
202
206
|
*/
|
|
203
207
|
function ipToBytes(ip: string): Uint8Array | null {
|
|
204
|
-
if (
|
|
208
|
+
if (isIPv4(ip)) {
|
|
205
209
|
return Uint8Array.from(ip.split(".").map((octet) => Number(octet)));
|
|
206
210
|
}
|
|
207
211
|
if (!isIPv6(ip)) {
|